Compare commits
56 commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
d4bd7c03e4 | ||
![]() |
7b4e191629 | ||
![]() |
3d75d7986e | ||
![]() |
ec0f22263d | ||
![]() |
9628f5a9ee | ||
![]() |
7a0634f3bc | ||
![]() |
bb40043362 | ||
![]() |
75959537ba | ||
![]() |
7e30d220e1 | ||
![]() |
43e40fc7c7 | ||
![]() |
885355ced4 | ||
![]() |
cbf5dd1f23 | ||
![]() |
4529fca3de | ||
![]() |
c1056991e3 | ||
![]() |
6ac4a57fce | ||
![]() |
53a3270410 | ||
![]() |
0dad4b7a41 | ||
![]() |
ec8b5e2dd4 | ||
![]() |
11b91c07eb | ||
![]() |
3dc1c88469 | ||
![]() |
604fd98c1d | ||
![]() |
8484dca903 | ||
![]() |
ef4484e2ab | ||
![]() |
d512ef1e56 | ||
![]() |
5a70f2131c | ||
![]() |
6e0999fb4f | ||
![]() |
7fdbfc9e8d | ||
![]() |
cdeddaab21 | ||
![]() |
2e3c30e733 | ||
![]() |
2b8e8f03fa | ||
![]() |
4d229f84d7 | ||
![]() |
3d8a3a4574 | ||
![]() |
2aac7b2ea1 | ||
![]() |
03693498d6 | ||
![]() |
35ea2acd1c | ||
![]() |
f14a8148b6 | ||
![]() |
52a60e5115 | ||
![]() |
8b25b8172b | ||
![]() |
09845c0a3a | ||
![]() |
a6b9668355 | ||
![]() |
d5b6a4d710 | ||
![]() |
bf6f715961 | ||
![]() |
1934e5c23e | ||
![]() |
295da0757e | ||
![]() |
85261bb5cc | ||
![]() |
8c159d0fbd | ||
![]() |
25f3f88a34 | ||
![]() |
0c24950911 | ||
![]() |
900fe328c0 | ||
![]() |
863e39b774 | ||
![]() |
7d58eb200b | ||
![]() |
87c9f6a52e | ||
![]() |
059a409235 | ||
![]() |
5c450693fa | ||
![]() |
910507d7e5 | ||
![]() |
3d48f14248 |
100 changed files with 2505 additions and 927 deletions
36
.github/actionlint.yml
vendored
36
.github/actionlint.yml
vendored
|
@ -5,26 +5,28 @@ self-hosted-runner:
|
||||||
# GitHub-hosted Runners
|
# GitHub-hosted Runners
|
||||||
- github-8vcpu-ubuntu-2404
|
- github-8vcpu-ubuntu-2404
|
||||||
- github-16vcpu-ubuntu-2404
|
- github-16vcpu-ubuntu-2404
|
||||||
|
- github-32vcpu-ubuntu-2404
|
||||||
|
- github-8vcpu-ubuntu-2204
|
||||||
|
- github-16vcpu-ubuntu-2204
|
||||||
|
- github-32vcpu-ubuntu-2204
|
||||||
|
- github-16vcpu-ubuntu-2204-arm
|
||||||
- windows-2025-16
|
- windows-2025-16
|
||||||
- windows-2025-32
|
- windows-2025-32
|
||||||
- windows-2025-64
|
- windows-2025-64
|
||||||
# Buildjet Ubuntu 20.04 - AMD x86_64
|
# Namespace Ubuntu 20.04 (Release builds)
|
||||||
- buildjet-2vcpu-ubuntu-2004
|
- namespace-profile-16x32-ubuntu-2004
|
||||||
- buildjet-4vcpu-ubuntu-2004
|
- namespace-profile-32x64-ubuntu-2004
|
||||||
- buildjet-8vcpu-ubuntu-2004
|
- namespace-profile-16x32-ubuntu-2004-arm
|
||||||
- buildjet-16vcpu-ubuntu-2004
|
- namespace-profile-32x64-ubuntu-2004-arm
|
||||||
- buildjet-32vcpu-ubuntu-2004
|
# Namespace Ubuntu 22.04 (Everything else)
|
||||||
# Buildjet Ubuntu 22.04 - AMD x86_64
|
- namespace-profile-2x4-ubuntu-2204
|
||||||
- buildjet-2vcpu-ubuntu-2204
|
- namespace-profile-4x8-ubuntu-2204
|
||||||
- buildjet-4vcpu-ubuntu-2204
|
- namespace-profile-8x16-ubuntu-2204
|
||||||
- buildjet-8vcpu-ubuntu-2204
|
- namespace-profile-16x32-ubuntu-2204
|
||||||
- buildjet-16vcpu-ubuntu-2204
|
- namespace-profile-32x64-ubuntu-2204
|
||||||
- buildjet-32vcpu-ubuntu-2204
|
# Namespace Limited Preview
|
||||||
# Buildjet Ubuntu 22.04 - Graviton aarch64
|
- namespace-profile-8x16-ubuntu-2004-arm-m4
|
||||||
- buildjet-8vcpu-ubuntu-2204-arm
|
- namespace-profile-8x32-ubuntu-2004-arm-m4
|
||||||
- buildjet-16vcpu-ubuntu-2204-arm
|
|
||||||
- buildjet-32vcpu-ubuntu-2204-arm
|
|
||||||
- buildjet-64vcpu-ubuntu-2204-arm
|
|
||||||
# Self Hosted Runners
|
# Self Hosted Runners
|
||||||
- self-mini-macos
|
- self-mini-macos
|
||||||
- self-32vcpu-windows-2022
|
- self-32vcpu-windows-2022
|
||||||
|
|
2
.github/actions/build_docs/action.yml
vendored
2
.github/actions/build_docs/action.yml
vendored
|
@ -13,7 +13,7 @@ runs:
|
||||||
uses: swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2
|
uses: swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2
|
||||||
with:
|
with:
|
||||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||||
cache-provider: "buildjet"
|
# cache-provider: "buildjet"
|
||||||
|
|
||||||
- name: Install Linux dependencies
|
- name: Install Linux dependencies
|
||||||
shell: bash -euxo pipefail {0}
|
shell: bash -euxo pipefail {0}
|
||||||
|
|
2
.github/workflows/bump_patch_version.yml
vendored
2
.github/workflows/bump_patch_version.yml
vendored
|
@ -16,7 +16,7 @@ jobs:
|
||||||
bump_patch_version:
|
bump_patch_version:
|
||||||
if: github.repository_owner == 'zed-industries'
|
if: github.repository_owner == 'zed-industries'
|
||||||
runs-on:
|
runs-on:
|
||||||
- buildjet-16vcpu-ubuntu-2204
|
- namespace-profile-16x32-ubuntu-2204
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||||
|
|
32
.github/workflows/ci.yml
vendored
32
.github/workflows/ci.yml
vendored
|
@ -136,7 +136,7 @@ jobs:
|
||||||
github.repository_owner == 'zed-industries' &&
|
github.repository_owner == 'zed-industries' &&
|
||||||
needs.job_spec.outputs.run_tests == 'true'
|
needs.job_spec.outputs.run_tests == 'true'
|
||||||
runs-on:
|
runs-on:
|
||||||
- buildjet-8vcpu-ubuntu-2204
|
- namespace-profile-8x16-ubuntu-2204
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repo
|
- name: Checkout repo
|
||||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||||
|
@ -167,7 +167,7 @@ jobs:
|
||||||
needs: [job_spec]
|
needs: [job_spec]
|
||||||
if: github.repository_owner == 'zed-industries'
|
if: github.repository_owner == 'zed-industries'
|
||||||
runs-on:
|
runs-on:
|
||||||
- buildjet-8vcpu-ubuntu-2204
|
- namespace-profile-4x8-ubuntu-2204
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repo
|
- name: Checkout repo
|
||||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||||
|
@ -220,7 +220,7 @@ jobs:
|
||||||
github.repository_owner == 'zed-industries' &&
|
github.repository_owner == 'zed-industries' &&
|
||||||
(needs.job_spec.outputs.run_tests == 'true' || needs.job_spec.outputs.run_docs == 'true')
|
(needs.job_spec.outputs.run_tests == 'true' || needs.job_spec.outputs.run_docs == 'true')
|
||||||
runs-on:
|
runs-on:
|
||||||
- buildjet-8vcpu-ubuntu-2204
|
- namespace-profile-8x16-ubuntu-2204
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repo
|
- name: Checkout repo
|
||||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||||
|
@ -327,7 +327,7 @@ jobs:
|
||||||
github.repository_owner == 'zed-industries' &&
|
github.repository_owner == 'zed-industries' &&
|
||||||
needs.job_spec.outputs.run_tests == 'true'
|
needs.job_spec.outputs.run_tests == 'true'
|
||||||
runs-on:
|
runs-on:
|
||||||
- buildjet-16vcpu-ubuntu-2204
|
- namespace-profile-16x32-ubuntu-2204
|
||||||
steps:
|
steps:
|
||||||
- name: Add Rust to the PATH
|
- name: Add Rust to the PATH
|
||||||
run: echo "$HOME/.cargo/bin" >> "$GITHUB_PATH"
|
run: echo "$HOME/.cargo/bin" >> "$GITHUB_PATH"
|
||||||
|
@ -341,7 +341,7 @@ jobs:
|
||||||
uses: swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2
|
uses: swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2
|
||||||
with:
|
with:
|
||||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||||
cache-provider: "buildjet"
|
# cache-provider: "buildjet"
|
||||||
|
|
||||||
- name: Install Linux dependencies
|
- name: Install Linux dependencies
|
||||||
run: ./script/linux
|
run: ./script/linux
|
||||||
|
@ -379,7 +379,7 @@ jobs:
|
||||||
github.repository_owner == 'zed-industries' &&
|
github.repository_owner == 'zed-industries' &&
|
||||||
needs.job_spec.outputs.run_tests == 'true'
|
needs.job_spec.outputs.run_tests == 'true'
|
||||||
runs-on:
|
runs-on:
|
||||||
- buildjet-8vcpu-ubuntu-2204
|
- namespace-profile-16x32-ubuntu-2204
|
||||||
steps:
|
steps:
|
||||||
- name: Add Rust to the PATH
|
- name: Add Rust to the PATH
|
||||||
run: echo "$HOME/.cargo/bin" >> "$GITHUB_PATH"
|
run: echo "$HOME/.cargo/bin" >> "$GITHUB_PATH"
|
||||||
|
@ -393,7 +393,7 @@ jobs:
|
||||||
uses: swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2
|
uses: swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2
|
||||||
with:
|
with:
|
||||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||||
cache-provider: "buildjet"
|
# cache-provider: "buildjet"
|
||||||
|
|
||||||
- name: Install Clang & Mold
|
- name: Install Clang & Mold
|
||||||
run: ./script/remote-server && ./script/install-mold 2.34.0
|
run: ./script/remote-server && ./script/install-mold 2.34.0
|
||||||
|
@ -510,8 +510,8 @@ jobs:
|
||||||
runs-on:
|
runs-on:
|
||||||
- self-mini-macos
|
- self-mini-macos
|
||||||
if: |
|
if: |
|
||||||
startsWith(github.ref, 'refs/tags/v')
|
( startsWith(github.ref, 'refs/tags/v')
|
||||||
|| contains(github.event.pull_request.labels.*.name, 'run-bundling')
|
|| contains(github.event.pull_request.labels.*.name, 'run-bundling') )
|
||||||
needs: [macos_tests]
|
needs: [macos_tests]
|
||||||
env:
|
env:
|
||||||
MACOS_CERTIFICATE: ${{ secrets.MACOS_CERTIFICATE }}
|
MACOS_CERTIFICATE: ${{ secrets.MACOS_CERTIFICATE }}
|
||||||
|
@ -596,10 +596,10 @@ jobs:
|
||||||
timeout-minutes: 60
|
timeout-minutes: 60
|
||||||
name: Linux x86_x64 release bundle
|
name: Linux x86_x64 release bundle
|
||||||
runs-on:
|
runs-on:
|
||||||
- buildjet-16vcpu-ubuntu-2004 # ubuntu 20.04 for minimal glibc
|
- namespace-profile-16x32-ubuntu-2004 # ubuntu 20.04 for minimal glibc
|
||||||
if: |
|
if: |
|
||||||
startsWith(github.ref, 'refs/tags/v')
|
( startsWith(github.ref, 'refs/tags/v')
|
||||||
|| contains(github.event.pull_request.labels.*.name, 'run-bundling')
|
|| contains(github.event.pull_request.labels.*.name, 'run-bundling') )
|
||||||
needs: [linux_tests]
|
needs: [linux_tests]
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repo
|
- name: Checkout repo
|
||||||
|
@ -649,7 +649,7 @@ jobs:
|
||||||
timeout-minutes: 60
|
timeout-minutes: 60
|
||||||
name: Linux arm64 release bundle
|
name: Linux arm64 release bundle
|
||||||
runs-on:
|
runs-on:
|
||||||
- buildjet-16vcpu-ubuntu-2204-arm
|
- namespace-profile-8x32-ubuntu-2004-arm-m4 # ubuntu 20.04 for minimal glibc
|
||||||
if: |
|
if: |
|
||||||
startsWith(github.ref, 'refs/tags/v')
|
startsWith(github.ref, 'refs/tags/v')
|
||||||
|| contains(github.event.pull_request.labels.*.name, 'run-bundling')
|
|| contains(github.event.pull_request.labels.*.name, 'run-bundling')
|
||||||
|
@ -702,10 +702,8 @@ jobs:
|
||||||
timeout-minutes: 60
|
timeout-minutes: 60
|
||||||
runs-on: github-8vcpu-ubuntu-2404
|
runs-on: github-8vcpu-ubuntu-2404
|
||||||
if: |
|
if: |
|
||||||
false && (
|
( startsWith(github.ref, 'refs/tags/v')
|
||||||
startsWith(github.ref, 'refs/tags/v')
|
|| contains(github.event.pull_request.labels.*.name, 'run-bundling') )
|
||||||
|| contains(github.event.pull_request.labels.*.name, 'run-bundling')
|
|
||||||
)
|
|
||||||
needs: [linux_tests]
|
needs: [linux_tests]
|
||||||
name: Build Zed on FreeBSD
|
name: Build Zed on FreeBSD
|
||||||
steps:
|
steps:
|
||||||
|
|
2
.github/workflows/deploy_cloudflare.yml
vendored
2
.github/workflows/deploy_cloudflare.yml
vendored
|
@ -9,7 +9,7 @@ jobs:
|
||||||
deploy-docs:
|
deploy-docs:
|
||||||
name: Deploy Docs
|
name: Deploy Docs
|
||||||
if: github.repository_owner == 'zed-industries'
|
if: github.repository_owner == 'zed-industries'
|
||||||
runs-on: buildjet-16vcpu-ubuntu-2204
|
runs-on: namespace-profile-16x32-ubuntu-2204
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repo
|
- name: Checkout repo
|
||||||
|
|
4
.github/workflows/deploy_collab.yml
vendored
4
.github/workflows/deploy_collab.yml
vendored
|
@ -61,7 +61,7 @@ jobs:
|
||||||
- style
|
- style
|
||||||
- tests
|
- tests
|
||||||
runs-on:
|
runs-on:
|
||||||
- buildjet-16vcpu-ubuntu-2204
|
- namespace-profile-16x32-ubuntu-2204
|
||||||
steps:
|
steps:
|
||||||
- name: Install doctl
|
- name: Install doctl
|
||||||
uses: digitalocean/action-doctl@v2
|
uses: digitalocean/action-doctl@v2
|
||||||
|
@ -94,7 +94,7 @@ jobs:
|
||||||
needs:
|
needs:
|
||||||
- publish
|
- publish
|
||||||
runs-on:
|
runs-on:
|
||||||
- buildjet-16vcpu-ubuntu-2204
|
- namespace-profile-16x32-ubuntu-2204
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repo
|
- name: Checkout repo
|
||||||
|
|
4
.github/workflows/eval.yml
vendored
4
.github/workflows/eval.yml
vendored
|
@ -32,7 +32,7 @@ jobs:
|
||||||
github.repository_owner == 'zed-industries' &&
|
github.repository_owner == 'zed-industries' &&
|
||||||
(github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'run-eval'))
|
(github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'run-eval'))
|
||||||
runs-on:
|
runs-on:
|
||||||
- buildjet-16vcpu-ubuntu-2204
|
- namespace-profile-16x32-ubuntu-2204
|
||||||
steps:
|
steps:
|
||||||
- name: Add Rust to the PATH
|
- name: Add Rust to the PATH
|
||||||
run: echo "$HOME/.cargo/bin" >> "$GITHUB_PATH"
|
run: echo "$HOME/.cargo/bin" >> "$GITHUB_PATH"
|
||||||
|
@ -46,7 +46,7 @@ jobs:
|
||||||
uses: swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2
|
uses: swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2
|
||||||
with:
|
with:
|
||||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||||
cache-provider: "buildjet"
|
# cache-provider: "buildjet"
|
||||||
|
|
||||||
- name: Install Linux dependencies
|
- name: Install Linux dependencies
|
||||||
run: ./script/linux
|
run: ./script/linux
|
||||||
|
|
2
.github/workflows/nix.yml
vendored
2
.github/workflows/nix.yml
vendored
|
@ -20,7 +20,7 @@ jobs:
|
||||||
matrix:
|
matrix:
|
||||||
system:
|
system:
|
||||||
- os: x86 Linux
|
- os: x86 Linux
|
||||||
runner: buildjet-16vcpu-ubuntu-2204
|
runner: namespace-profile-16x32-ubuntu-2204
|
||||||
install_nix: true
|
install_nix: true
|
||||||
- os: arm Mac
|
- os: arm Mac
|
||||||
runner: [macOS, ARM64, test]
|
runner: [macOS, ARM64, test]
|
||||||
|
|
2
.github/workflows/randomized_tests.yml
vendored
2
.github/workflows/randomized_tests.yml
vendored
|
@ -20,7 +20,7 @@ jobs:
|
||||||
name: Run randomized tests
|
name: Run randomized tests
|
||||||
if: github.repository_owner == 'zed-industries'
|
if: github.repository_owner == 'zed-industries'
|
||||||
runs-on:
|
runs-on:
|
||||||
- buildjet-16vcpu-ubuntu-2204
|
- namespace-profile-16x32-ubuntu-2204
|
||||||
steps:
|
steps:
|
||||||
- name: Install Node
|
- name: Install Node
|
||||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||||
|
|
4
.github/workflows/release_nightly.yml
vendored
4
.github/workflows/release_nightly.yml
vendored
|
@ -127,7 +127,7 @@ jobs:
|
||||||
name: Create a Linux *.tar.gz bundle for x86
|
name: Create a Linux *.tar.gz bundle for x86
|
||||||
if: github.repository_owner == 'zed-industries'
|
if: github.repository_owner == 'zed-industries'
|
||||||
runs-on:
|
runs-on:
|
||||||
- buildjet-16vcpu-ubuntu-2004
|
- namespace-profile-16x32-ubuntu-2004 # ubuntu 20.04 for minimal glibc
|
||||||
needs: tests
|
needs: tests
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repo
|
- name: Checkout repo
|
||||||
|
@ -167,7 +167,7 @@ jobs:
|
||||||
name: Create a Linux *.tar.gz bundle for ARM
|
name: Create a Linux *.tar.gz bundle for ARM
|
||||||
if: github.repository_owner == 'zed-industries'
|
if: github.repository_owner == 'zed-industries'
|
||||||
runs-on:
|
runs-on:
|
||||||
- buildjet-16vcpu-ubuntu-2204-arm
|
- namespace-profile-8x32-ubuntu-2004-arm-m4 # ubuntu 20.04 for minimal glibc
|
||||||
needs: tests
|
needs: tests
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repo
|
- name: Checkout repo
|
||||||
|
|
4
.github/workflows/unit_evals.yml
vendored
4
.github/workflows/unit_evals.yml
vendored
|
@ -23,7 +23,7 @@ jobs:
|
||||||
timeout-minutes: 60
|
timeout-minutes: 60
|
||||||
name: Run unit evals
|
name: Run unit evals
|
||||||
runs-on:
|
runs-on:
|
||||||
- buildjet-16vcpu-ubuntu-2204
|
- namespace-profile-16x32-ubuntu-2204
|
||||||
steps:
|
steps:
|
||||||
- name: Add Rust to the PATH
|
- name: Add Rust to the PATH
|
||||||
run: echo "$HOME/.cargo/bin" >> "$GITHUB_PATH"
|
run: echo "$HOME/.cargo/bin" >> "$GITHUB_PATH"
|
||||||
|
@ -37,7 +37,7 @@ jobs:
|
||||||
uses: swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2
|
uses: swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2
|
||||||
with:
|
with:
|
||||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||||
cache-provider: "buildjet"
|
# cache-provider: "buildjet"
|
||||||
|
|
||||||
- name: Install Linux dependencies
|
- name: Install Linux dependencies
|
||||||
run: ./script/linux
|
run: ./script/linux
|
||||||
|
|
116
Cargo.lock
generated
116
Cargo.lock
generated
|
@ -114,7 +114,6 @@ dependencies = [
|
||||||
"pretty_assertions",
|
"pretty_assertions",
|
||||||
"project",
|
"project",
|
||||||
"prompt_store",
|
"prompt_store",
|
||||||
"proto",
|
|
||||||
"rand 0.8.5",
|
"rand 0.8.5",
|
||||||
"ref-cast",
|
"ref-cast",
|
||||||
"rope",
|
"rope",
|
||||||
|
@ -355,10 +354,10 @@ name = "ai_onboarding"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"client",
|
"client",
|
||||||
|
"cloud_llm_client",
|
||||||
"component",
|
"component",
|
||||||
"gpui",
|
"gpui",
|
||||||
"language_model",
|
"language_model",
|
||||||
"proto",
|
|
||||||
"serde",
|
"serde",
|
||||||
"smallvec",
|
"smallvec",
|
||||||
"telemetry",
|
"telemetry",
|
||||||
|
@ -1075,17 +1074,6 @@ dependencies = [
|
||||||
"tracing",
|
"tracing",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "async-recursion"
|
|
||||||
version = "0.3.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "d7d78656ba01f1b93024b7c3a0467f1608e4be67d725749fdcd7d2c7678fd7a2"
|
|
||||||
dependencies = [
|
|
||||||
"proc-macro2",
|
|
||||||
"quote",
|
|
||||||
"syn 1.0.109",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "async-recursion"
|
name = "async-recursion"
|
||||||
version = "1.1.1"
|
version = "1.1.1"
|
||||||
|
@ -1378,7 +1366,7 @@ dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"arrayvec",
|
"arrayvec",
|
||||||
"log",
|
"log",
|
||||||
"nom",
|
"nom 7.1.3",
|
||||||
"num-rational",
|
"num-rational",
|
||||||
"v_frame",
|
"v_frame",
|
||||||
]
|
]
|
||||||
|
@ -2752,7 +2740,7 @@ version = "0.6.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766"
|
checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"nom",
|
"nom 7.1.3",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -2971,11 +2959,11 @@ name = "client"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"async-recursion 0.3.2",
|
|
||||||
"async-tungstenite",
|
"async-tungstenite",
|
||||||
"base64 0.22.1",
|
"base64 0.22.1",
|
||||||
"chrono",
|
"chrono",
|
||||||
"clock",
|
"clock",
|
||||||
|
"cloud_api_client",
|
||||||
"cloud_llm_client",
|
"cloud_llm_client",
|
||||||
"cocoa 0.26.0",
|
"cocoa 0.26.0",
|
||||||
"collections",
|
"collections",
|
||||||
|
@ -3031,6 +3019,36 @@ dependencies = [
|
||||||
"workspace-hack",
|
"workspace-hack",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cloud_api_client"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
"cloud_api_types",
|
||||||
|
"futures 0.3.31",
|
||||||
|
"gpui",
|
||||||
|
"gpui_tokio",
|
||||||
|
"http_client",
|
||||||
|
"parking_lot",
|
||||||
|
"serde_json",
|
||||||
|
"workspace-hack",
|
||||||
|
"yawc",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cloud_api_types"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
"chrono",
|
||||||
|
"ciborium",
|
||||||
|
"cloud_llm_client",
|
||||||
|
"pretty_assertions",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"workspace-hack",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cloud_llm_client"
|
name = "cloud_llm_client"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
@ -4960,6 +4978,7 @@ dependencies = [
|
||||||
"theme",
|
"theme",
|
||||||
"time",
|
"time",
|
||||||
"tree-sitter-bash",
|
"tree-sitter-bash",
|
||||||
|
"tree-sitter-c",
|
||||||
"tree-sitter-html",
|
"tree-sitter-html",
|
||||||
"tree-sitter-python",
|
"tree-sitter-python",
|
||||||
"tree-sitter-rust",
|
"tree-sitter-rust",
|
||||||
|
@ -6362,7 +6381,6 @@ dependencies = [
|
||||||
"buffer_diff",
|
"buffer_diff",
|
||||||
"call",
|
"call",
|
||||||
"chrono",
|
"chrono",
|
||||||
"client",
|
|
||||||
"cloud_llm_client",
|
"cloud_llm_client",
|
||||||
"collections",
|
"collections",
|
||||||
"command_palette_hooks",
|
"command_palette_hooks",
|
||||||
|
@ -7860,6 +7878,7 @@ dependencies = [
|
||||||
"http 1.3.1",
|
"http 1.3.1",
|
||||||
"http-body 1.0.1",
|
"http-body 1.0.1",
|
||||||
"log",
|
"log",
|
||||||
|
"parking_lot",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"url",
|
"url",
|
||||||
|
@ -9069,6 +9088,7 @@ dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"base64 0.22.1",
|
"base64 0.22.1",
|
||||||
"client",
|
"client",
|
||||||
|
"cloud_api_types",
|
||||||
"cloud_llm_client",
|
"cloud_llm_client",
|
||||||
"collections",
|
"collections",
|
||||||
"futures 0.3.31",
|
"futures 0.3.31",
|
||||||
|
@ -9126,7 +9146,6 @@ dependencies = [
|
||||||
"open_router",
|
"open_router",
|
||||||
"partial-json-fixer",
|
"partial-json-fixer",
|
||||||
"project",
|
"project",
|
||||||
"proto",
|
|
||||||
"release_channel",
|
"release_channel",
|
||||||
"schemars",
|
"schemars",
|
||||||
"serde",
|
"serde",
|
||||||
|
@ -9864,7 +9883,7 @@ name = "markdown_preview"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"async-recursion 1.1.1",
|
"async-recursion",
|
||||||
"collections",
|
"collections",
|
||||||
"editor",
|
"editor",
|
||||||
"fs",
|
"fs",
|
||||||
|
@ -10462,6 +10481,15 @@ dependencies = [
|
||||||
"minimal-lexical",
|
"minimal-lexical",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "nom"
|
||||||
|
version = "8.0.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405"
|
||||||
|
dependencies = [
|
||||||
|
"memchr",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "noop_proc_macro"
|
name = "noop_proc_macro"
|
||||||
version = "0.3.0"
|
version = "0.3.0"
|
||||||
|
@ -15185,7 +15213,7 @@ version = "0.2.6"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7bba3a93db0cc4f7bdece8bb09e77e2e785c20bfebf79eb8340ed80708048790"
|
checksum = "7bba3a93db0cc4f7bdece8bb09e77e2e785c20bfebf79eb8340ed80708048790"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"nom",
|
"nom 7.1.3",
|
||||||
"unicode_categories",
|
"unicode_categories",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -16188,7 +16216,7 @@ version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"assistant_slash_command",
|
"assistant_slash_command",
|
||||||
"async-recursion 1.1.1",
|
"async-recursion",
|
||||||
"breadcrumbs",
|
"breadcrumbs",
|
||||||
"client",
|
"client",
|
||||||
"collections",
|
"collections",
|
||||||
|
@ -16388,9 +16416,8 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tiktoken-rs"
|
name = "tiktoken-rs"
|
||||||
version = "0.7.0"
|
version = "0.8.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "git+https://github.com/zed-industries/tiktoken-rs?rev=30c32a4522751699adeda0d5840c71c3b75ae73d#30c32a4522751699adeda0d5840c71c3b75ae73d"
|
||||||
checksum = "25563eeba904d770acf527e8b370fe9a5547bacd20ff84a0b6c3bc41288e5625"
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"base64 0.22.1",
|
"base64 0.22.1",
|
||||||
|
@ -16537,6 +16564,7 @@ dependencies = [
|
||||||
"call",
|
"call",
|
||||||
"chrono",
|
"chrono",
|
||||||
"client",
|
"client",
|
||||||
|
"cloud_llm_client",
|
||||||
"collections",
|
"collections",
|
||||||
"db",
|
"db",
|
||||||
"gpui",
|
"gpui",
|
||||||
|
@ -19615,7 +19643,7 @@ version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"any_vec",
|
"any_vec",
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"async-recursion 1.1.1",
|
"async-recursion",
|
||||||
"bincode",
|
"bincode",
|
||||||
"call",
|
"call",
|
||||||
"client",
|
"client",
|
||||||
|
@ -19731,7 +19759,9 @@ dependencies = [
|
||||||
"heck 0.4.1",
|
"heck 0.4.1",
|
||||||
"hmac",
|
"hmac",
|
||||||
"hyper 0.14.32",
|
"hyper 0.14.32",
|
||||||
|
"hyper 1.6.0",
|
||||||
"hyper-rustls 0.27.5",
|
"hyper-rustls 0.27.5",
|
||||||
|
"hyper-util",
|
||||||
"idna",
|
"idna",
|
||||||
"indexmap",
|
"indexmap",
|
||||||
"inout",
|
"inout",
|
||||||
|
@ -19752,7 +19782,7 @@ dependencies = [
|
||||||
"mio",
|
"mio",
|
||||||
"naga",
|
"naga",
|
||||||
"nix 0.29.0",
|
"nix 0.29.0",
|
||||||
"nom",
|
"nom 7.1.3",
|
||||||
"num-bigint",
|
"num-bigint",
|
||||||
"num-bigint-dig",
|
"num-bigint-dig",
|
||||||
"num-integer",
|
"num-integer",
|
||||||
|
@ -20087,6 +20117,34 @@ version = "1.0.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049"
|
checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "yawc"
|
||||||
|
version = "0.2.4"
|
||||||
|
source = "git+https://github.com/deviant-forks/yawc?rev=1899688f3e69ace4545aceb97b2a13881cf26142#1899688f3e69ace4545aceb97b2a13881cf26142"
|
||||||
|
dependencies = [
|
||||||
|
"base64 0.22.1",
|
||||||
|
"bytes 1.10.1",
|
||||||
|
"flate2",
|
||||||
|
"futures 0.3.31",
|
||||||
|
"http-body-util",
|
||||||
|
"hyper 1.6.0",
|
||||||
|
"hyper-util",
|
||||||
|
"js-sys",
|
||||||
|
"nom 8.0.0",
|
||||||
|
"pin-project",
|
||||||
|
"rand 0.8.5",
|
||||||
|
"sha1",
|
||||||
|
"thiserror 1.0.69",
|
||||||
|
"tokio",
|
||||||
|
"tokio-rustls 0.26.2",
|
||||||
|
"tokio-util",
|
||||||
|
"url",
|
||||||
|
"wasm-bindgen",
|
||||||
|
"wasm-bindgen-futures",
|
||||||
|
"web-sys",
|
||||||
|
"webpki-roots",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "yazi"
|
name = "yazi"
|
||||||
version = "0.2.1"
|
version = "0.2.1"
|
||||||
|
@ -20140,7 +20198,7 @@ dependencies = [
|
||||||
"async-io",
|
"async-io",
|
||||||
"async-lock",
|
"async-lock",
|
||||||
"async-process",
|
"async-process",
|
||||||
"async-recursion 1.1.1",
|
"async-recursion",
|
||||||
"async-task",
|
"async-task",
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"blocking",
|
"blocking",
|
||||||
|
@ -20193,7 +20251,7 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zed"
|
name = "zed"
|
||||||
version = "0.198.0"
|
version = "0.198.6"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"activity_indicator",
|
"activity_indicator",
|
||||||
"agent",
|
"agent",
|
||||||
|
@ -20572,6 +20630,7 @@ dependencies = [
|
||||||
"call",
|
"call",
|
||||||
"client",
|
"client",
|
||||||
"clock",
|
"clock",
|
||||||
|
"cloud_api_types",
|
||||||
"cloud_llm_client",
|
"cloud_llm_client",
|
||||||
"collections",
|
"collections",
|
||||||
"command_palette_hooks",
|
"command_palette_hooks",
|
||||||
|
@ -20592,7 +20651,6 @@ dependencies = [
|
||||||
"menu",
|
"menu",
|
||||||
"postage",
|
"postage",
|
||||||
"project",
|
"project",
|
||||||
"proto",
|
|
||||||
"regex",
|
"regex",
|
||||||
"release_channel",
|
"release_channel",
|
||||||
"reqwest_client",
|
"reqwest_client",
|
||||||
|
|
10
Cargo.toml
10
Cargo.toml
|
@ -29,6 +29,8 @@ members = [
|
||||||
"crates/cli",
|
"crates/cli",
|
||||||
"crates/client",
|
"crates/client",
|
||||||
"crates/clock",
|
"crates/clock",
|
||||||
|
"crates/cloud_api_client",
|
||||||
|
"crates/cloud_api_types",
|
||||||
"crates/cloud_llm_client",
|
"crates/cloud_llm_client",
|
||||||
"crates/collab",
|
"crates/collab",
|
||||||
"crates/collab_ui",
|
"crates/collab_ui",
|
||||||
|
@ -251,6 +253,8 @@ channel = { path = "crates/channel" }
|
||||||
cli = { path = "crates/cli" }
|
cli = { path = "crates/cli" }
|
||||||
client = { path = "crates/client" }
|
client = { path = "crates/client" }
|
||||||
clock = { path = "crates/clock" }
|
clock = { path = "crates/clock" }
|
||||||
|
cloud_api_client = { path = "crates/cloud_api_client" }
|
||||||
|
cloud_api_types = { path = "crates/cloud_api_types" }
|
||||||
cloud_llm_client = { path = "crates/cloud_llm_client" }
|
cloud_llm_client = { path = "crates/cloud_llm_client" }
|
||||||
collab = { path = "crates/collab" }
|
collab = { path = "crates/collab" }
|
||||||
collab_ui = { path = "crates/collab_ui" }
|
collab_ui = { path = "crates/collab_ui" }
|
||||||
|
@ -450,6 +454,7 @@ bytes = "1.0"
|
||||||
cargo_metadata = "0.19"
|
cargo_metadata = "0.19"
|
||||||
cargo_toml = "0.21"
|
cargo_toml = "0.21"
|
||||||
chrono = { version = "0.4", features = ["serde"] }
|
chrono = { version = "0.4", features = ["serde"] }
|
||||||
|
ciborium = "0.2"
|
||||||
circular-buffer = "1.0"
|
circular-buffer = "1.0"
|
||||||
clap = { version = "4.4", features = ["derive"] }
|
clap = { version = "4.4", features = ["derive"] }
|
||||||
cocoa = "0.26"
|
cocoa = "0.26"
|
||||||
|
@ -586,7 +591,7 @@ sysinfo = "0.31.0"
|
||||||
take-until = "0.2.0"
|
take-until = "0.2.0"
|
||||||
tempfile = "3.20.0"
|
tempfile = "3.20.0"
|
||||||
thiserror = "2.0.12"
|
thiserror = "2.0.12"
|
||||||
tiktoken-rs = "0.7.0"
|
tiktoken-rs = { git = "https://github.com/zed-industries/tiktoken-rs", rev = "30c32a4522751699adeda0d5840c71c3b75ae73d" }
|
||||||
time = { version = "0.3", features = [
|
time = { version = "0.3", features = [
|
||||||
"macros",
|
"macros",
|
||||||
"parsing",
|
"parsing",
|
||||||
|
@ -646,6 +651,9 @@ which = "6.0.0"
|
||||||
windows-core = "0.61"
|
windows-core = "0.61"
|
||||||
wit-component = "0.221"
|
wit-component = "0.221"
|
||||||
workspace-hack = "0.1.0"
|
workspace-hack = "0.1.0"
|
||||||
|
# We can switch back to the published version once https://github.com/infinitefield/yawc/pull/16 is merged and a new
|
||||||
|
# version is released.
|
||||||
|
yawc = { git = "https://github.com/deviant-forks/yawc", rev = "1899688f3e69ace4545aceb97b2a13881cf26142" }
|
||||||
zstd = "0.11"
|
zstd = "0.11"
|
||||||
|
|
||||||
[workspace.dependencies.async-stripe]
|
[workspace.dependencies.async-stripe]
|
||||||
|
|
|
@ -95,7 +95,7 @@
|
||||||
"ctrl-shift-r": ["pane::DeploySearch", { "replace_enabled": true }],
|
"ctrl-shift-r": ["pane::DeploySearch", { "replace_enabled": true }],
|
||||||
"alt-shift-f10": "task::Spawn",
|
"alt-shift-f10": "task::Spawn",
|
||||||
"ctrl-e": "file_finder::Toggle",
|
"ctrl-e": "file_finder::Toggle",
|
||||||
"ctrl-k": "git_panel::ToggleFocus", // bug: This should also focus commit editor
|
// "ctrl-k": "git_panel::ToggleFocus", // bug: This should also focus commit editor
|
||||||
"ctrl-shift-n": "file_finder::Toggle",
|
"ctrl-shift-n": "file_finder::Toggle",
|
||||||
"ctrl-shift-a": "command_palette::Toggle",
|
"ctrl-shift-a": "command_palette::Toggle",
|
||||||
"shift shift": "command_palette::Toggle",
|
"shift shift": "command_palette::Toggle",
|
||||||
|
@ -166,7 +166,7 @@
|
||||||
{ "context": "Diagnostics > Editor", "bindings": { "alt-6": "pane::CloseActiveItem" } },
|
{ "context": "Diagnostics > Editor", "bindings": { "alt-6": "pane::CloseActiveItem" } },
|
||||||
{ "context": "OutlinePanel", "bindings": { "alt-7": "workspace::CloseActiveDock" } },
|
{ "context": "OutlinePanel", "bindings": { "alt-7": "workspace::CloseActiveDock" } },
|
||||||
{
|
{
|
||||||
"context": "Dock || Workspace || Terminal || OutlinePanel || ProjectPanel || CollabPanel || (Editor && mode == auto_height)",
|
"context": "Dock || Workspace || OutlinePanel || ProjectPanel || CollabPanel || (Editor && mode == auto_height)",
|
||||||
"bindings": {
|
"bindings": {
|
||||||
"escape": "editor::ToggleFocus",
|
"escape": "editor::ToggleFocus",
|
||||||
"shift-escape": "workspace::CloseActiveDock"
|
"shift-escape": "workspace::CloseActiveDock"
|
||||||
|
|
|
@ -97,7 +97,7 @@
|
||||||
"cmd-shift-r": ["pane::DeploySearch", { "replace_enabled": true }],
|
"cmd-shift-r": ["pane::DeploySearch", { "replace_enabled": true }],
|
||||||
"ctrl-alt-r": "task::Spawn",
|
"ctrl-alt-r": "task::Spawn",
|
||||||
"cmd-e": "file_finder::Toggle",
|
"cmd-e": "file_finder::Toggle",
|
||||||
"cmd-k": "git_panel::ToggleFocus", // bug: This should also focus commit editor
|
// "cmd-k": "git_panel::ToggleFocus", // bug: This should also focus commit editor
|
||||||
"cmd-shift-o": "file_finder::Toggle",
|
"cmd-shift-o": "file_finder::Toggle",
|
||||||
"cmd-shift-a": "command_palette::Toggle",
|
"cmd-shift-a": "command_palette::Toggle",
|
||||||
"shift shift": "command_palette::Toggle",
|
"shift shift": "command_palette::Toggle",
|
||||||
|
@ -167,7 +167,7 @@
|
||||||
{ "context": "Diagnostics > Editor", "bindings": { "cmd-6": "pane::CloseActiveItem" } },
|
{ "context": "Diagnostics > Editor", "bindings": { "cmd-6": "pane::CloseActiveItem" } },
|
||||||
{ "context": "OutlinePanel", "bindings": { "cmd-7": "workspace::CloseActiveDock" } },
|
{ "context": "OutlinePanel", "bindings": { "cmd-7": "workspace::CloseActiveDock" } },
|
||||||
{
|
{
|
||||||
"context": "Dock || Workspace || Terminal || OutlinePanel || ProjectPanel || CollabPanel || (Editor && mode == auto_height)",
|
"context": "Dock || Workspace || OutlinePanel || ProjectPanel || CollabPanel || (Editor && mode == auto_height)",
|
||||||
"bindings": {
|
"bindings": {
|
||||||
"escape": "editor::ToggleFocus",
|
"escape": "editor::ToggleFocus",
|
||||||
"shift-escape": "workspace::CloseActiveDock"
|
"shift-escape": "workspace::CloseActiveDock"
|
||||||
|
|
|
@ -47,7 +47,6 @@ paths.workspace = true
|
||||||
postage.workspace = true
|
postage.workspace = true
|
||||||
project.workspace = true
|
project.workspace = true
|
||||||
prompt_store.workspace = true
|
prompt_store.workspace = true
|
||||||
proto.workspace = true
|
|
||||||
ref-cast.workspace = true
|
ref-cast.workspace = true
|
||||||
rope.workspace = true
|
rope.workspace = true
|
||||||
schemars.workspace = true
|
schemars.workspace = true
|
||||||
|
|
|
@ -13,7 +13,7 @@ use anyhow::{Result, anyhow};
|
||||||
use assistant_tool::{ActionLog, AnyToolCard, Tool, ToolWorkingSet};
|
use assistant_tool::{ActionLog, AnyToolCard, Tool, ToolWorkingSet};
|
||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
use client::{ModelRequestUsage, RequestUsage};
|
use client::{ModelRequestUsage, RequestUsage};
|
||||||
use cloud_llm_client::{CompletionIntent, CompletionRequestStatus, UsageLimit};
|
use cloud_llm_client::{CompletionIntent, CompletionRequestStatus, Plan, UsageLimit};
|
||||||
use collections::HashMap;
|
use collections::HashMap;
|
||||||
use feature_flags::{self, FeatureFlagAppExt};
|
use feature_flags::{self, FeatureFlagAppExt};
|
||||||
use futures::{FutureExt, StreamExt as _, future::Shared};
|
use futures::{FutureExt, StreamExt as _, future::Shared};
|
||||||
|
@ -37,7 +37,6 @@ use project::{
|
||||||
git_store::{GitStore, GitStoreCheckpoint, RepositoryState},
|
git_store::{GitStore, GitStoreCheckpoint, RepositoryState},
|
||||||
};
|
};
|
||||||
use prompt_store::{ModelContext, PromptBuilder};
|
use prompt_store::{ModelContext, PromptBuilder};
|
||||||
use proto::Plan;
|
|
||||||
use schemars::JsonSchema;
|
use schemars::JsonSchema;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use settings::Settings;
|
use settings::Settings;
|
||||||
|
@ -3255,8 +3254,10 @@ impl Thread {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn update_model_request_usage(&self, amount: u32, limit: UsageLimit, cx: &mut Context<Self>) {
|
fn update_model_request_usage(&self, amount: u32, limit: UsageLimit, cx: &mut Context<Self>) {
|
||||||
self.project.update(cx, |project, cx| {
|
self.project
|
||||||
project.user_store().update(cx, |user_store, cx| {
|
.read(cx)
|
||||||
|
.user_store()
|
||||||
|
.update(cx, |user_store, cx| {
|
||||||
user_store.update_model_request_usage(
|
user_store.update_model_request_usage(
|
||||||
ModelRequestUsage(RequestUsage {
|
ModelRequestUsage(RequestUsage {
|
||||||
amount: amount as i32,
|
amount: amount as i32,
|
||||||
|
@ -3264,8 +3265,7 @@ impl Thread {
|
||||||
}),
|
}),
|
||||||
cx,
|
cx,
|
||||||
)
|
)
|
||||||
})
|
});
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn deny_tool_use(
|
pub fn deny_tool_use(
|
||||||
|
|
|
@ -7,6 +7,7 @@ use std::{sync::Arc, time::Duration};
|
||||||
|
|
||||||
use agent_settings::AgentSettings;
|
use agent_settings::AgentSettings;
|
||||||
use assistant_tool::{ToolSource, ToolWorkingSet};
|
use assistant_tool::{ToolSource, ToolWorkingSet};
|
||||||
|
use cloud_llm_client::Plan;
|
||||||
use collections::HashMap;
|
use collections::HashMap;
|
||||||
use context_server::ContextServerId;
|
use context_server::ContextServerId;
|
||||||
use extension::ExtensionManifest;
|
use extension::ExtensionManifest;
|
||||||
|
@ -25,7 +26,6 @@ use project::{
|
||||||
context_server_store::{ContextServerConfiguration, ContextServerStatus, ContextServerStore},
|
context_server_store::{ContextServerConfiguration, ContextServerStatus, ContextServerStore},
|
||||||
project_settings::{ContextServerSettings, ProjectSettings},
|
project_settings::{ContextServerSettings, ProjectSettings},
|
||||||
};
|
};
|
||||||
use proto::Plan;
|
|
||||||
use settings::{Settings, update_settings_file};
|
use settings::{Settings, update_settings_file};
|
||||||
use ui::{
|
use ui::{
|
||||||
Chip, ContextMenu, Disclosure, Divider, DividerColor, ElevationIndex, Indicator, PopoverMenu,
|
Chip, ContextMenu, Disclosure, Divider, DividerColor, ElevationIndex, Indicator, PopoverMenu,
|
||||||
|
@ -180,7 +180,7 @@ impl AgentConfiguration {
|
||||||
let current_plan = if is_zed_provider {
|
let current_plan = if is_zed_provider {
|
||||||
self.workspace
|
self.workspace
|
||||||
.upgrade()
|
.upgrade()
|
||||||
.and_then(|workspace| workspace.read(cx).user_store().read(cx).current_plan())
|
.and_then(|workspace| workspace.read(cx).user_store().read(cx).plan())
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
|
@ -502,7 +502,7 @@ impl AgentConfiguration {
|
||||||
.blend(cx.theme().colors().text_accent.opacity(0.2));
|
.blend(cx.theme().colors().text_accent.opacity(0.2));
|
||||||
|
|
||||||
let (plan_name, label_color, bg_color) = match plan {
|
let (plan_name, label_color, bg_color) = match plan {
|
||||||
Plan::Free => ("Free", Color::Default, free_chip_bg),
|
Plan::ZedFree => ("Free", Color::Default, free_chip_bg),
|
||||||
Plan::ZedProTrial => ("Pro Trial", Color::Accent, pro_chip_bg),
|
Plan::ZedProTrial => ("Pro Trial", Color::Accent, pro_chip_bg),
|
||||||
Plan::ZedPro => ("Pro", Color::Accent, pro_chip_bg),
|
Plan::ZedPro => ("Pro", Color::Accent, pro_chip_bg),
|
||||||
};
|
};
|
||||||
|
|
|
@ -43,8 +43,8 @@ use anyhow::{Result, anyhow};
|
||||||
use assistant_context::{AssistantContext, ContextEvent, ContextSummary};
|
use assistant_context::{AssistantContext, ContextEvent, ContextSummary};
|
||||||
use assistant_slash_command::SlashCommandWorkingSet;
|
use assistant_slash_command::SlashCommandWorkingSet;
|
||||||
use assistant_tool::ToolWorkingSet;
|
use assistant_tool::ToolWorkingSet;
|
||||||
use client::{DisableAiSettings, UserStore, zed_urls};
|
use client::{UserStore, zed_urls};
|
||||||
use cloud_llm_client::{CompletionIntent, UsageLimit};
|
use cloud_llm_client::{CompletionIntent, Plan, UsageLimit};
|
||||||
use editor::{Anchor, AnchorRangeExt as _, Editor, EditorEvent, MultiBuffer};
|
use editor::{Anchor, AnchorRangeExt as _, Editor, EditorEvent, MultiBuffer};
|
||||||
use feature_flags::{self, FeatureFlagAppExt};
|
use feature_flags::{self, FeatureFlagAppExt};
|
||||||
use fs::Fs;
|
use fs::Fs;
|
||||||
|
@ -58,9 +58,8 @@ use language::LanguageRegistry;
|
||||||
use language_model::{
|
use language_model::{
|
||||||
ConfigurationError, ConfiguredModel, LanguageModelProviderTosView, LanguageModelRegistry,
|
ConfigurationError, ConfiguredModel, LanguageModelProviderTosView, LanguageModelRegistry,
|
||||||
};
|
};
|
||||||
use project::{Project, ProjectPath, Worktree};
|
use project::{DisableAiSettings, Project, ProjectPath, Worktree};
|
||||||
use prompt_store::{PromptBuilder, PromptStore, UserPromptId};
|
use prompt_store::{PromptBuilder, PromptStore, UserPromptId};
|
||||||
use proto::Plan;
|
|
||||||
use rules_library::{RulesLibrary, open_rules_library};
|
use rules_library::{RulesLibrary, open_rules_library};
|
||||||
use search::{BufferSearchBar, buffer_search};
|
use search::{BufferSearchBar, buffer_search};
|
||||||
use settings::{Settings, update_settings_file};
|
use settings::{Settings, update_settings_file};
|
||||||
|
@ -579,7 +578,6 @@ impl AgentPanel {
|
||||||
MessageEditor::new(
|
MessageEditor::new(
|
||||||
fs.clone(),
|
fs.clone(),
|
||||||
workspace.clone(),
|
workspace.clone(),
|
||||||
user_store.clone(),
|
|
||||||
message_editor_context_store.clone(),
|
message_editor_context_store.clone(),
|
||||||
prompt_store.clone(),
|
prompt_store.clone(),
|
||||||
thread_store.downgrade(),
|
thread_store.downgrade(),
|
||||||
|
@ -848,7 +846,6 @@ impl AgentPanel {
|
||||||
MessageEditor::new(
|
MessageEditor::new(
|
||||||
self.fs.clone(),
|
self.fs.clone(),
|
||||||
self.workspace.clone(),
|
self.workspace.clone(),
|
||||||
self.user_store.clone(),
|
|
||||||
context_store.clone(),
|
context_store.clone(),
|
||||||
self.prompt_store.clone(),
|
self.prompt_store.clone(),
|
||||||
self.thread_store.downgrade(),
|
self.thread_store.downgrade(),
|
||||||
|
@ -1122,7 +1119,6 @@ impl AgentPanel {
|
||||||
MessageEditor::new(
|
MessageEditor::new(
|
||||||
self.fs.clone(),
|
self.fs.clone(),
|
||||||
self.workspace.clone(),
|
self.workspace.clone(),
|
||||||
self.user_store.clone(),
|
|
||||||
context_store,
|
context_store,
|
||||||
self.prompt_store.clone(),
|
self.prompt_store.clone(),
|
||||||
self.thread_store.downgrade(),
|
self.thread_store.downgrade(),
|
||||||
|
@ -2293,10 +2289,10 @@ impl AgentPanel {
|
||||||
| ActiveView::Configuration => return false,
|
| ActiveView::Configuration => return false,
|
||||||
}
|
}
|
||||||
|
|
||||||
let plan = self.user_store.read(cx).current_plan();
|
let plan = self.user_store.read(cx).plan();
|
||||||
let has_previous_trial = self.user_store.read(cx).trial_started_at().is_some();
|
let has_previous_trial = self.user_store.read(cx).trial_started_at().is_some();
|
||||||
|
|
||||||
matches!(plan, Some(Plan::Free)) && has_previous_trial
|
matches!(plan, Some(Plan::ZedFree)) && has_previous_trial
|
||||||
}
|
}
|
||||||
|
|
||||||
fn should_render_onboarding(&self, cx: &mut Context<Self>) -> bool {
|
fn should_render_onboarding(&self, cx: &mut Context<Self>) -> bool {
|
||||||
|
@ -2911,7 +2907,7 @@ impl AgentPanel {
|
||||||
) -> AnyElement {
|
) -> AnyElement {
|
||||||
let error_message = match plan {
|
let error_message = match plan {
|
||||||
Plan::ZedPro => "Upgrade to usage-based billing for more prompts.",
|
Plan::ZedPro => "Upgrade to usage-based billing for more prompts.",
|
||||||
Plan::ZedProTrial | Plan::Free => "Upgrade to Zed Pro for more prompts.",
|
Plan::ZedProTrial | Plan::ZedFree => "Upgrade to Zed Pro for more prompts.",
|
||||||
};
|
};
|
||||||
|
|
||||||
let icon = Icon::new(IconName::XCircle)
|
let icon = Icon::new(IconName::XCircle)
|
||||||
|
|
|
@ -31,7 +31,7 @@ use std::sync::Arc;
|
||||||
use agent::{Thread, ThreadId};
|
use agent::{Thread, ThreadId};
|
||||||
use agent_settings::{AgentProfileId, AgentSettings, LanguageModelSelection};
|
use agent_settings::{AgentProfileId, AgentSettings, LanguageModelSelection};
|
||||||
use assistant_slash_command::SlashCommandRegistry;
|
use assistant_slash_command::SlashCommandRegistry;
|
||||||
use client::{Client, DisableAiSettings};
|
use client::Client;
|
||||||
use command_palette_hooks::CommandPaletteFilter;
|
use command_palette_hooks::CommandPaletteFilter;
|
||||||
use feature_flags::FeatureFlagAppExt as _;
|
use feature_flags::FeatureFlagAppExt as _;
|
||||||
use fs::Fs;
|
use fs::Fs;
|
||||||
|
@ -40,6 +40,7 @@ use language::LanguageRegistry;
|
||||||
use language_model::{
|
use language_model::{
|
||||||
ConfiguredModel, LanguageModel, LanguageModelId, LanguageModelProviderId, LanguageModelRegistry,
|
ConfiguredModel, LanguageModel, LanguageModelId, LanguageModelProviderId, LanguageModelRegistry,
|
||||||
};
|
};
|
||||||
|
use project::DisableAiSettings;
|
||||||
use prompt_store::PromptBuilder;
|
use prompt_store::PromptBuilder;
|
||||||
use schemars::JsonSchema;
|
use schemars::JsonSchema;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
|
@ -16,7 +16,7 @@ use agent::{
|
||||||
};
|
};
|
||||||
use agent_settings::AgentSettings;
|
use agent_settings::AgentSettings;
|
||||||
use anyhow::{Context as _, Result};
|
use anyhow::{Context as _, Result};
|
||||||
use client::{DisableAiSettings, telemetry::Telemetry};
|
use client::telemetry::Telemetry;
|
||||||
use collections::{HashMap, HashSet, VecDeque, hash_map};
|
use collections::{HashMap, HashSet, VecDeque, hash_map};
|
||||||
use editor::SelectionEffects;
|
use editor::SelectionEffects;
|
||||||
use editor::{
|
use editor::{
|
||||||
|
@ -39,7 +39,7 @@ use language_model::{
|
||||||
};
|
};
|
||||||
use multi_buffer::MultiBufferRow;
|
use multi_buffer::MultiBufferRow;
|
||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
use project::{CodeAction, LspAction, Project, ProjectTransaction};
|
use project::{CodeAction, DisableAiSettings, LspAction, Project, ProjectTransaction};
|
||||||
use prompt_store::{PromptBuilder, PromptStore};
|
use prompt_store::{PromptBuilder, PromptStore};
|
||||||
use settings::{Settings, SettingsStore};
|
use settings::{Settings, SettingsStore};
|
||||||
use telemetry_events::{AssistantEventData, AssistantKind, AssistantPhase};
|
use telemetry_events::{AssistantEventData, AssistantKind, AssistantPhase};
|
||||||
|
|
|
@ -17,7 +17,6 @@ use agent::{
|
||||||
use agent_settings::{AgentSettings, CompletionMode};
|
use agent_settings::{AgentSettings, CompletionMode};
|
||||||
use ai_onboarding::ApiKeysWithProviders;
|
use ai_onboarding::ApiKeysWithProviders;
|
||||||
use buffer_diff::BufferDiff;
|
use buffer_diff::BufferDiff;
|
||||||
use client::UserStore;
|
|
||||||
use cloud_llm_client::CompletionIntent;
|
use cloud_llm_client::CompletionIntent;
|
||||||
use collections::{HashMap, HashSet};
|
use collections::{HashMap, HashSet};
|
||||||
use editor::actions::{MoveUp, Paste};
|
use editor::actions::{MoveUp, Paste};
|
||||||
|
@ -43,7 +42,6 @@ use language_model::{
|
||||||
use multi_buffer;
|
use multi_buffer;
|
||||||
use project::Project;
|
use project::Project;
|
||||||
use prompt_store::PromptStore;
|
use prompt_store::PromptStore;
|
||||||
use proto::Plan;
|
|
||||||
use settings::Settings;
|
use settings::Settings;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use theme::ThemeSettings;
|
use theme::ThemeSettings;
|
||||||
|
@ -79,7 +77,6 @@ pub struct MessageEditor {
|
||||||
editor: Entity<Editor>,
|
editor: Entity<Editor>,
|
||||||
workspace: WeakEntity<Workspace>,
|
workspace: WeakEntity<Workspace>,
|
||||||
project: Entity<Project>,
|
project: Entity<Project>,
|
||||||
user_store: Entity<UserStore>,
|
|
||||||
context_store: Entity<ContextStore>,
|
context_store: Entity<ContextStore>,
|
||||||
prompt_store: Option<Entity<PromptStore>>,
|
prompt_store: Option<Entity<PromptStore>>,
|
||||||
history_store: Option<WeakEntity<HistoryStore>>,
|
history_store: Option<WeakEntity<HistoryStore>>,
|
||||||
|
@ -159,7 +156,6 @@ impl MessageEditor {
|
||||||
pub fn new(
|
pub fn new(
|
||||||
fs: Arc<dyn Fs>,
|
fs: Arc<dyn Fs>,
|
||||||
workspace: WeakEntity<Workspace>,
|
workspace: WeakEntity<Workspace>,
|
||||||
user_store: Entity<UserStore>,
|
|
||||||
context_store: Entity<ContextStore>,
|
context_store: Entity<ContextStore>,
|
||||||
prompt_store: Option<Entity<PromptStore>>,
|
prompt_store: Option<Entity<PromptStore>>,
|
||||||
thread_store: WeakEntity<ThreadStore>,
|
thread_store: WeakEntity<ThreadStore>,
|
||||||
|
@ -231,7 +227,6 @@ impl MessageEditor {
|
||||||
Self {
|
Self {
|
||||||
editor: editor.clone(),
|
editor: editor.clone(),
|
||||||
project: thread.read(cx).project().clone(),
|
project: thread.read(cx).project().clone(),
|
||||||
user_store,
|
|
||||||
thread,
|
thread,
|
||||||
incompatible_tools_state: incompatible_tools.clone(),
|
incompatible_tools_state: incompatible_tools.clone(),
|
||||||
workspace,
|
workspace,
|
||||||
|
@ -1287,24 +1282,12 @@ impl MessageEditor {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
let user_store = self.user_store.read(cx);
|
let user_store = self.project.read(cx).user_store().read(cx);
|
||||||
|
if user_store.is_usage_based_billing_enabled() {
|
||||||
let ubb_enable = user_store
|
|
||||||
.usage_based_billing_enabled()
|
|
||||||
.map_or(false, |enabled| enabled);
|
|
||||||
|
|
||||||
if ubb_enable {
|
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
let plan = user_store
|
let plan = user_store.plan().unwrap_or(cloud_llm_client::Plan::ZedFree);
|
||||||
.current_plan()
|
|
||||||
.map(|plan| match plan {
|
|
||||||
Plan::Free => cloud_llm_client::Plan::ZedFree,
|
|
||||||
Plan::ZedPro => cloud_llm_client::Plan::ZedPro,
|
|
||||||
Plan::ZedProTrial => cloud_llm_client::Plan::ZedProTrial,
|
|
||||||
})
|
|
||||||
.unwrap_or(cloud_llm_client::Plan::ZedFree);
|
|
||||||
|
|
||||||
let usage = user_store.model_request_usage()?;
|
let usage = user_store.model_request_usage()?;
|
||||||
|
|
||||||
|
@ -1769,7 +1752,6 @@ impl AgentPreview for MessageEditor {
|
||||||
) -> Option<AnyElement> {
|
) -> Option<AnyElement> {
|
||||||
if let Some(workspace) = workspace.upgrade() {
|
if let Some(workspace) = workspace.upgrade() {
|
||||||
let fs = workspace.read(cx).app_state().fs.clone();
|
let fs = workspace.read(cx).app_state().fs.clone();
|
||||||
let user_store = workspace.read(cx).app_state().user_store.clone();
|
|
||||||
let project = workspace.read(cx).project().clone();
|
let project = workspace.read(cx).project().clone();
|
||||||
let weak_project = project.downgrade();
|
let weak_project = project.downgrade();
|
||||||
let context_store = cx.new(|_cx| ContextStore::new(weak_project, None));
|
let context_store = cx.new(|_cx| ContextStore::new(weak_project, None));
|
||||||
|
@ -1782,7 +1764,6 @@ impl AgentPreview for MessageEditor {
|
||||||
MessageEditor::new(
|
MessageEditor::new(
|
||||||
fs,
|
fs,
|
||||||
workspace.downgrade(),
|
workspace.downgrade(),
|
||||||
user_store,
|
|
||||||
context_store,
|
context_store,
|
||||||
None,
|
None,
|
||||||
thread_store.downgrade(),
|
thread_store.downgrade(),
|
||||||
|
|
|
@ -16,10 +16,10 @@ default = []
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
client.workspace = true
|
client.workspace = true
|
||||||
|
cloud_llm_client.workspace = true
|
||||||
component.workspace = true
|
component.workspace = true
|
||||||
gpui.workspace = true
|
gpui.workspace = true
|
||||||
language_model.workspace = true
|
language_model.workspace = true
|
||||||
proto.workspace = true
|
|
||||||
serde.workspace = true
|
serde.workspace = true
|
||||||
smallvec.workspace = true
|
smallvec.workspace = true
|
||||||
telemetry.workspace = true
|
telemetry.workspace = true
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use client::{Client, UserStore};
|
use client::{Client, UserStore};
|
||||||
|
use cloud_llm_client::Plan;
|
||||||
use gpui::{Entity, IntoElement, ParentElement};
|
use gpui::{Entity, IntoElement, ParentElement};
|
||||||
use language_model::{LanguageModelRegistry, ZED_CLOUD_PROVIDER_ID};
|
use language_model::{LanguageModelRegistry, ZED_CLOUD_PROVIDER_ID};
|
||||||
use ui::prelude::*;
|
use ui::prelude::*;
|
||||||
|
@ -56,15 +57,8 @@ impl AgentPanelOnboarding {
|
||||||
|
|
||||||
impl Render for AgentPanelOnboarding {
|
impl Render for AgentPanelOnboarding {
|
||||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||||
let enrolled_in_trial = matches!(
|
let enrolled_in_trial = self.user_store.read(cx).plan() == Some(Plan::ZedProTrial);
|
||||||
self.user_store.read(cx).current_plan(),
|
let is_pro_user = self.user_store.read(cx).plan() == Some(Plan::ZedPro);
|
||||||
Some(proto::Plan::ZedProTrial)
|
|
||||||
);
|
|
||||||
|
|
||||||
let is_pro_user = matches!(
|
|
||||||
self.user_store.read(cx).current_plan(),
|
|
||||||
Some(proto::Plan::ZedPro)
|
|
||||||
);
|
|
||||||
|
|
||||||
AgentPanelOnboardingCard::new()
|
AgentPanelOnboardingCard::new()
|
||||||
.child(
|
.child(
|
||||||
|
|
|
@ -9,6 +9,7 @@ pub use agent_api_keys_onboarding::{ApiKeysWithProviders, ApiKeysWithoutProvider
|
||||||
pub use agent_panel_onboarding_card::AgentPanelOnboardingCard;
|
pub use agent_panel_onboarding_card::AgentPanelOnboardingCard;
|
||||||
pub use agent_panel_onboarding_content::AgentPanelOnboarding;
|
pub use agent_panel_onboarding_content::AgentPanelOnboarding;
|
||||||
pub use ai_upsell_card::AiUpsellCard;
|
pub use ai_upsell_card::AiUpsellCard;
|
||||||
|
use cloud_llm_client::Plan;
|
||||||
pub use edit_prediction_onboarding_content::EditPredictionOnboarding;
|
pub use edit_prediction_onboarding_content::EditPredictionOnboarding;
|
||||||
pub use young_account_banner::YoungAccountBanner;
|
pub use young_account_banner::YoungAccountBanner;
|
||||||
|
|
||||||
|
@ -79,7 +80,7 @@ impl From<client::Status> for SignInStatus {
|
||||||
pub struct ZedAiOnboarding {
|
pub struct ZedAiOnboarding {
|
||||||
pub sign_in_status: SignInStatus,
|
pub sign_in_status: SignInStatus,
|
||||||
pub has_accepted_terms_of_service: bool,
|
pub has_accepted_terms_of_service: bool,
|
||||||
pub plan: Option<proto::Plan>,
|
pub plan: Option<Plan>,
|
||||||
pub account_too_young: bool,
|
pub account_too_young: bool,
|
||||||
pub continue_with_zed_ai: Arc<dyn Fn(&mut Window, &mut App)>,
|
pub continue_with_zed_ai: Arc<dyn Fn(&mut Window, &mut App)>,
|
||||||
pub sign_in: Arc<dyn Fn(&mut Window, &mut App)>,
|
pub sign_in: Arc<dyn Fn(&mut Window, &mut App)>,
|
||||||
|
@ -99,8 +100,8 @@ impl ZedAiOnboarding {
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
sign_in_status: status.into(),
|
sign_in_status: status.into(),
|
||||||
has_accepted_terms_of_service: store.current_user_has_accepted_terms().unwrap_or(false),
|
has_accepted_terms_of_service: store.has_accepted_terms_of_service(),
|
||||||
plan: store.current_plan(),
|
plan: store.plan(),
|
||||||
account_too_young: store.account_too_young(),
|
account_too_young: store.account_too_young(),
|
||||||
continue_with_zed_ai,
|
continue_with_zed_ai,
|
||||||
accept_terms_of_service: Arc::new({
|
accept_terms_of_service: Arc::new({
|
||||||
|
@ -113,11 +114,9 @@ impl ZedAiOnboarding {
|
||||||
sign_in: Arc::new(move |_window, cx| {
|
sign_in: Arc::new(move |_window, cx| {
|
||||||
cx.spawn({
|
cx.spawn({
|
||||||
let client = client.clone();
|
let client = client.clone();
|
||||||
async move |cx| {
|
async move |cx| client.sign_in_with_optional_connect(true, cx).await
|
||||||
client.authenticate_and_connect(true, cx).await;
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
.detach();
|
.detach_and_log_err(cx);
|
||||||
}),
|
}),
|
||||||
dismiss_onboarding: None,
|
dismiss_onboarding: None,
|
||||||
}
|
}
|
||||||
|
@ -411,9 +410,9 @@ impl RenderOnce for ZedAiOnboarding {
|
||||||
if matches!(self.sign_in_status, SignInStatus::SignedIn) {
|
if matches!(self.sign_in_status, SignInStatus::SignedIn) {
|
||||||
if self.has_accepted_terms_of_service {
|
if self.has_accepted_terms_of_service {
|
||||||
match self.plan {
|
match self.plan {
|
||||||
None | Some(proto::Plan::Free) => self.render_free_plan_state(cx),
|
None | Some(Plan::ZedFree) => self.render_free_plan_state(cx),
|
||||||
Some(proto::Plan::ZedProTrial) => self.render_trial_state(cx),
|
Some(Plan::ZedProTrial) => self.render_trial_state(cx),
|
||||||
Some(proto::Plan::ZedPro) => self.render_pro_plan_state(cx),
|
Some(Plan::ZedPro) => self.render_pro_plan_state(cx),
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
self.render_accept_terms_of_service()
|
self.render_accept_terms_of_service()
|
||||||
|
@ -433,7 +432,7 @@ impl Component for ZedAiOnboarding {
|
||||||
fn onboarding(
|
fn onboarding(
|
||||||
sign_in_status: SignInStatus,
|
sign_in_status: SignInStatus,
|
||||||
has_accepted_terms_of_service: bool,
|
has_accepted_terms_of_service: bool,
|
||||||
plan: Option<proto::Plan>,
|
plan: Option<Plan>,
|
||||||
account_too_young: bool,
|
account_too_young: bool,
|
||||||
) -> AnyElement {
|
) -> AnyElement {
|
||||||
ZedAiOnboarding {
|
ZedAiOnboarding {
|
||||||
|
@ -468,25 +467,15 @@ impl Component for ZedAiOnboarding {
|
||||||
),
|
),
|
||||||
single_example(
|
single_example(
|
||||||
"Free Plan",
|
"Free Plan",
|
||||||
onboarding(SignInStatus::SignedIn, true, Some(proto::Plan::Free), false),
|
onboarding(SignInStatus::SignedIn, true, Some(Plan::ZedFree), false),
|
||||||
),
|
),
|
||||||
single_example(
|
single_example(
|
||||||
"Pro Trial",
|
"Pro Trial",
|
||||||
onboarding(
|
onboarding(SignInStatus::SignedIn, true, Some(Plan::ZedProTrial), false),
|
||||||
SignInStatus::SignedIn,
|
|
||||||
true,
|
|
||||||
Some(proto::Plan::ZedProTrial),
|
|
||||||
false,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
single_example(
|
single_example(
|
||||||
"Pro Plan",
|
"Pro Plan",
|
||||||
onboarding(
|
onboarding(SignInStatus::SignedIn, true, Some(Plan::ZedPro), false),
|
||||||
SignInStatus::SignedIn,
|
|
||||||
true,
|
|
||||||
Some(proto::Plan::ZedPro),
|
|
||||||
false,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
])
|
])
|
||||||
.into_any_element(),
|
.into_any_element(),
|
||||||
|
|
|
@ -21,11 +21,9 @@ impl AiUpsellCard {
|
||||||
sign_in: Arc::new(move |_window, cx| {
|
sign_in: Arc::new(move |_window, cx| {
|
||||||
cx.spawn({
|
cx.spawn({
|
||||||
let client = client.clone();
|
let client = client.clone();
|
||||||
async move |cx| {
|
async move |cx| client.sign_in_with_optional_connect(true, cx).await
|
||||||
client.authenticate_and_connect(true, cx).await;
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
.detach();
|
.detach_and_log_err(cx);
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -36,11 +36,18 @@ pub enum AnthropicModelMode {
|
||||||
pub enum Model {
|
pub enum Model {
|
||||||
#[serde(rename = "claude-opus-4", alias = "claude-opus-4-latest")]
|
#[serde(rename = "claude-opus-4", alias = "claude-opus-4-latest")]
|
||||||
ClaudeOpus4,
|
ClaudeOpus4,
|
||||||
|
#[serde(rename = "claude-opus-4-1", alias = "claude-opus-4-1-latest")]
|
||||||
|
ClaudeOpus4_1,
|
||||||
#[serde(
|
#[serde(
|
||||||
rename = "claude-opus-4-thinking",
|
rename = "claude-opus-4-thinking",
|
||||||
alias = "claude-opus-4-thinking-latest"
|
alias = "claude-opus-4-thinking-latest"
|
||||||
)]
|
)]
|
||||||
ClaudeOpus4Thinking,
|
ClaudeOpus4Thinking,
|
||||||
|
#[serde(
|
||||||
|
rename = "claude-opus-4-1-thinking",
|
||||||
|
alias = "claude-opus-4-1-thinking-latest"
|
||||||
|
)]
|
||||||
|
ClaudeOpus4_1Thinking,
|
||||||
#[default]
|
#[default]
|
||||||
#[serde(rename = "claude-sonnet-4", alias = "claude-sonnet-4-latest")]
|
#[serde(rename = "claude-sonnet-4", alias = "claude-sonnet-4-latest")]
|
||||||
ClaudeSonnet4,
|
ClaudeSonnet4,
|
||||||
|
@ -91,10 +98,18 @@ impl Model {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn from_id(id: &str) -> Result<Self> {
|
pub fn from_id(id: &str) -> Result<Self> {
|
||||||
|
if id.starts_with("claude-opus-4-1-thinking") {
|
||||||
|
return Ok(Self::ClaudeOpus4_1Thinking);
|
||||||
|
}
|
||||||
|
|
||||||
if id.starts_with("claude-opus-4-thinking") {
|
if id.starts_with("claude-opus-4-thinking") {
|
||||||
return Ok(Self::ClaudeOpus4Thinking);
|
return Ok(Self::ClaudeOpus4Thinking);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if id.starts_with("claude-opus-4-1") {
|
||||||
|
return Ok(Self::ClaudeOpus4_1);
|
||||||
|
}
|
||||||
|
|
||||||
if id.starts_with("claude-opus-4") {
|
if id.starts_with("claude-opus-4") {
|
||||||
return Ok(Self::ClaudeOpus4);
|
return Ok(Self::ClaudeOpus4);
|
||||||
}
|
}
|
||||||
|
@ -141,7 +156,9 @@ impl Model {
|
||||||
pub fn id(&self) -> &str {
|
pub fn id(&self) -> &str {
|
||||||
match self {
|
match self {
|
||||||
Self::ClaudeOpus4 => "claude-opus-4-latest",
|
Self::ClaudeOpus4 => "claude-opus-4-latest",
|
||||||
|
Self::ClaudeOpus4_1 => "claude-opus-4-1-latest",
|
||||||
Self::ClaudeOpus4Thinking => "claude-opus-4-thinking-latest",
|
Self::ClaudeOpus4Thinking => "claude-opus-4-thinking-latest",
|
||||||
|
Self::ClaudeOpus4_1Thinking => "claude-opus-4-1-thinking-latest",
|
||||||
Self::ClaudeSonnet4 => "claude-sonnet-4-latest",
|
Self::ClaudeSonnet4 => "claude-sonnet-4-latest",
|
||||||
Self::ClaudeSonnet4Thinking => "claude-sonnet-4-thinking-latest",
|
Self::ClaudeSonnet4Thinking => "claude-sonnet-4-thinking-latest",
|
||||||
Self::Claude3_5Sonnet => "claude-3-5-sonnet-latest",
|
Self::Claude3_5Sonnet => "claude-3-5-sonnet-latest",
|
||||||
|
@ -159,6 +176,7 @@ impl Model {
|
||||||
pub fn request_id(&self) -> &str {
|
pub fn request_id(&self) -> &str {
|
||||||
match self {
|
match self {
|
||||||
Self::ClaudeOpus4 | Self::ClaudeOpus4Thinking => "claude-opus-4-20250514",
|
Self::ClaudeOpus4 | Self::ClaudeOpus4Thinking => "claude-opus-4-20250514",
|
||||||
|
Self::ClaudeOpus4_1 | Self::ClaudeOpus4_1Thinking => "claude-opus-4-1-20250805",
|
||||||
Self::ClaudeSonnet4 | Self::ClaudeSonnet4Thinking => "claude-sonnet-4-20250514",
|
Self::ClaudeSonnet4 | Self::ClaudeSonnet4Thinking => "claude-sonnet-4-20250514",
|
||||||
Self::Claude3_5Sonnet => "claude-3-5-sonnet-latest",
|
Self::Claude3_5Sonnet => "claude-3-5-sonnet-latest",
|
||||||
Self::Claude3_7Sonnet | Self::Claude3_7SonnetThinking => "claude-3-7-sonnet-latest",
|
Self::Claude3_7Sonnet | Self::Claude3_7SonnetThinking => "claude-3-7-sonnet-latest",
|
||||||
|
@ -173,7 +191,9 @@ impl Model {
|
||||||
pub fn display_name(&self) -> &str {
|
pub fn display_name(&self) -> &str {
|
||||||
match self {
|
match self {
|
||||||
Self::ClaudeOpus4 => "Claude Opus 4",
|
Self::ClaudeOpus4 => "Claude Opus 4",
|
||||||
|
Self::ClaudeOpus4_1 => "Claude Opus 4.1",
|
||||||
Self::ClaudeOpus4Thinking => "Claude Opus 4 Thinking",
|
Self::ClaudeOpus4Thinking => "Claude Opus 4 Thinking",
|
||||||
|
Self::ClaudeOpus4_1Thinking => "Claude Opus 4.1 Thinking",
|
||||||
Self::ClaudeSonnet4 => "Claude Sonnet 4",
|
Self::ClaudeSonnet4 => "Claude Sonnet 4",
|
||||||
Self::ClaudeSonnet4Thinking => "Claude Sonnet 4 Thinking",
|
Self::ClaudeSonnet4Thinking => "Claude Sonnet 4 Thinking",
|
||||||
Self::Claude3_7Sonnet => "Claude 3.7 Sonnet",
|
Self::Claude3_7Sonnet => "Claude 3.7 Sonnet",
|
||||||
|
@ -192,7 +212,9 @@ impl Model {
|
||||||
pub fn cache_configuration(&self) -> Option<AnthropicModelCacheConfiguration> {
|
pub fn cache_configuration(&self) -> Option<AnthropicModelCacheConfiguration> {
|
||||||
match self {
|
match self {
|
||||||
Self::ClaudeOpus4
|
Self::ClaudeOpus4
|
||||||
|
| Self::ClaudeOpus4_1
|
||||||
| Self::ClaudeOpus4Thinking
|
| Self::ClaudeOpus4Thinking
|
||||||
|
| Self::ClaudeOpus4_1Thinking
|
||||||
| Self::ClaudeSonnet4
|
| Self::ClaudeSonnet4
|
||||||
| Self::ClaudeSonnet4Thinking
|
| Self::ClaudeSonnet4Thinking
|
||||||
| Self::Claude3_5Sonnet
|
| Self::Claude3_5Sonnet
|
||||||
|
@ -215,7 +237,9 @@ impl Model {
|
||||||
pub fn max_token_count(&self) -> u64 {
|
pub fn max_token_count(&self) -> u64 {
|
||||||
match self {
|
match self {
|
||||||
Self::ClaudeOpus4
|
Self::ClaudeOpus4
|
||||||
|
| Self::ClaudeOpus4_1
|
||||||
| Self::ClaudeOpus4Thinking
|
| Self::ClaudeOpus4Thinking
|
||||||
|
| Self::ClaudeOpus4_1Thinking
|
||||||
| Self::ClaudeSonnet4
|
| Self::ClaudeSonnet4
|
||||||
| Self::ClaudeSonnet4Thinking
|
| Self::ClaudeSonnet4Thinking
|
||||||
| Self::Claude3_5Sonnet
|
| Self::Claude3_5Sonnet
|
||||||
|
@ -232,7 +256,9 @@ impl Model {
|
||||||
pub fn max_output_tokens(&self) -> u64 {
|
pub fn max_output_tokens(&self) -> u64 {
|
||||||
match self {
|
match self {
|
||||||
Self::ClaudeOpus4
|
Self::ClaudeOpus4
|
||||||
|
| Self::ClaudeOpus4_1
|
||||||
| Self::ClaudeOpus4Thinking
|
| Self::ClaudeOpus4Thinking
|
||||||
|
| Self::ClaudeOpus4_1Thinking
|
||||||
| Self::ClaudeSonnet4
|
| Self::ClaudeSonnet4
|
||||||
| Self::ClaudeSonnet4Thinking
|
| Self::ClaudeSonnet4Thinking
|
||||||
| Self::Claude3_5Sonnet
|
| Self::Claude3_5Sonnet
|
||||||
|
@ -249,7 +275,9 @@ impl Model {
|
||||||
pub fn default_temperature(&self) -> f32 {
|
pub fn default_temperature(&self) -> f32 {
|
||||||
match self {
|
match self {
|
||||||
Self::ClaudeOpus4
|
Self::ClaudeOpus4
|
||||||
|
| Self::ClaudeOpus4_1
|
||||||
| Self::ClaudeOpus4Thinking
|
| Self::ClaudeOpus4Thinking
|
||||||
|
| Self::ClaudeOpus4_1Thinking
|
||||||
| Self::ClaudeSonnet4
|
| Self::ClaudeSonnet4
|
||||||
| Self::ClaudeSonnet4Thinking
|
| Self::ClaudeSonnet4Thinking
|
||||||
| Self::Claude3_5Sonnet
|
| Self::Claude3_5Sonnet
|
||||||
|
@ -269,6 +297,7 @@ impl Model {
|
||||||
pub fn mode(&self) -> AnthropicModelMode {
|
pub fn mode(&self) -> AnthropicModelMode {
|
||||||
match self {
|
match self {
|
||||||
Self::ClaudeOpus4
|
Self::ClaudeOpus4
|
||||||
|
| Self::ClaudeOpus4_1
|
||||||
| Self::ClaudeSonnet4
|
| Self::ClaudeSonnet4
|
||||||
| Self::Claude3_5Sonnet
|
| Self::Claude3_5Sonnet
|
||||||
| Self::Claude3_7Sonnet
|
| Self::Claude3_7Sonnet
|
||||||
|
@ -277,6 +306,7 @@ impl Model {
|
||||||
| Self::Claude3Sonnet
|
| Self::Claude3Sonnet
|
||||||
| Self::Claude3Haiku => AnthropicModelMode::Default,
|
| Self::Claude3Haiku => AnthropicModelMode::Default,
|
||||||
Self::ClaudeOpus4Thinking
|
Self::ClaudeOpus4Thinking
|
||||||
|
| Self::ClaudeOpus4_1Thinking
|
||||||
| Self::ClaudeSonnet4Thinking
|
| Self::ClaudeSonnet4Thinking
|
||||||
| Self::Claude3_7SonnetThinking => AnthropicModelMode::Thinking {
|
| Self::Claude3_7SonnetThinking => AnthropicModelMode::Thinking {
|
||||||
budget_tokens: Some(4_096),
|
budget_tokens: Some(4_096),
|
||||||
|
|
|
@ -630,6 +630,11 @@ impl ActionLog {
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
if tracked_buffer.unreviewed_edits.is_empty() {
|
||||||
|
if let TrackedBufferStatus::Created { .. } = &mut tracked_buffer.status {
|
||||||
|
tracked_buffer.status = TrackedBufferStatus::Modified;
|
||||||
|
}
|
||||||
|
}
|
||||||
tracked_buffer.schedule_diff_update(ChangeAuthor::User, cx);
|
tracked_buffer.schedule_diff_update(ChangeAuthor::User, cx);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -775,6 +780,9 @@ impl ActionLog {
|
||||||
.retain(|_buffer, tracked_buffer| match tracked_buffer.status {
|
.retain(|_buffer, tracked_buffer| match tracked_buffer.status {
|
||||||
TrackedBufferStatus::Deleted => false,
|
TrackedBufferStatus::Deleted => false,
|
||||||
_ => {
|
_ => {
|
||||||
|
if let TrackedBufferStatus::Created { .. } = &mut tracked_buffer.status {
|
||||||
|
tracked_buffer.status = TrackedBufferStatus::Modified;
|
||||||
|
}
|
||||||
tracked_buffer.unreviewed_edits.clear();
|
tracked_buffer.unreviewed_edits.clear();
|
||||||
tracked_buffer.diff_base = tracked_buffer.snapshot.as_rope().clone();
|
tracked_buffer.diff_base = tracked_buffer.snapshot.as_rope().clone();
|
||||||
tracked_buffer.schedule_diff_update(ChangeAuthor::User, cx);
|
tracked_buffer.schedule_diff_update(ChangeAuthor::User, cx);
|
||||||
|
@ -2075,6 +2083,134 @@ mod tests {
|
||||||
assert_eq!(content, "ai content\nuser added this line");
|
assert_eq!(content, "ai content\nuser added this line");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
async fn test_reject_after_accepting_hunk_on_created_file(cx: &mut TestAppContext) {
|
||||||
|
init_test(cx);
|
||||||
|
|
||||||
|
let fs = FakeFs::new(cx.executor());
|
||||||
|
let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
|
||||||
|
let action_log = cx.new(|_| ActionLog::new(project.clone()));
|
||||||
|
|
||||||
|
let file_path = project
|
||||||
|
.read_with(cx, |project, cx| {
|
||||||
|
project.find_project_path("dir/new_file", cx)
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
let buffer = project
|
||||||
|
.update(cx, |project, cx| project.open_buffer(file_path.clone(), cx))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// AI creates file with initial content
|
||||||
|
cx.update(|cx| {
|
||||||
|
action_log.update(cx, |log, cx| log.buffer_created(buffer.clone(), cx));
|
||||||
|
buffer.update(cx, |buffer, cx| buffer.set_text("ai content v1", cx));
|
||||||
|
action_log.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx));
|
||||||
|
});
|
||||||
|
project
|
||||||
|
.update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
cx.run_until_parked();
|
||||||
|
assert_ne!(unreviewed_hunks(&action_log, cx), vec![]);
|
||||||
|
|
||||||
|
// User accepts the single hunk
|
||||||
|
action_log.update(cx, |log, cx| {
|
||||||
|
log.keep_edits_in_range(buffer.clone(), Anchor::MIN..Anchor::MAX, cx)
|
||||||
|
});
|
||||||
|
cx.run_until_parked();
|
||||||
|
assert_eq!(unreviewed_hunks(&action_log, cx), vec![]);
|
||||||
|
assert!(fs.is_file(path!("/dir/new_file").as_ref()).await);
|
||||||
|
|
||||||
|
// AI modifies the file
|
||||||
|
cx.update(|cx| {
|
||||||
|
buffer.update(cx, |buffer, cx| buffer.set_text("ai content v2", cx));
|
||||||
|
action_log.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx));
|
||||||
|
});
|
||||||
|
project
|
||||||
|
.update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
cx.run_until_parked();
|
||||||
|
assert_ne!(unreviewed_hunks(&action_log, cx), vec![]);
|
||||||
|
|
||||||
|
// User rejects the hunk
|
||||||
|
action_log
|
||||||
|
.update(cx, |log, cx| {
|
||||||
|
log.reject_edits_in_ranges(buffer.clone(), vec![Anchor::MIN..Anchor::MAX], cx)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
cx.run_until_parked();
|
||||||
|
assert!(fs.is_file(path!("/dir/new_file").as_ref()).await,);
|
||||||
|
assert_eq!(
|
||||||
|
buffer.read_with(cx, |buffer, _| buffer.text()),
|
||||||
|
"ai content v1"
|
||||||
|
);
|
||||||
|
assert_eq!(unreviewed_hunks(&action_log, cx), vec![]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
async fn test_reject_edits_on_previously_accepted_created_file(cx: &mut TestAppContext) {
|
||||||
|
init_test(cx);
|
||||||
|
|
||||||
|
let fs = FakeFs::new(cx.executor());
|
||||||
|
let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
|
||||||
|
let action_log = cx.new(|_| ActionLog::new(project.clone()));
|
||||||
|
|
||||||
|
let file_path = project
|
||||||
|
.read_with(cx, |project, cx| {
|
||||||
|
project.find_project_path("dir/new_file", cx)
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
let buffer = project
|
||||||
|
.update(cx, |project, cx| project.open_buffer(file_path.clone(), cx))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// AI creates file with initial content
|
||||||
|
cx.update(|cx| {
|
||||||
|
action_log.update(cx, |log, cx| log.buffer_created(buffer.clone(), cx));
|
||||||
|
buffer.update(cx, |buffer, cx| buffer.set_text("ai content v1", cx));
|
||||||
|
action_log.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx));
|
||||||
|
});
|
||||||
|
project
|
||||||
|
.update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
cx.run_until_parked();
|
||||||
|
|
||||||
|
// User clicks "Accept All"
|
||||||
|
action_log.update(cx, |log, cx| log.keep_all_edits(cx));
|
||||||
|
cx.run_until_parked();
|
||||||
|
assert!(fs.is_file(path!("/dir/new_file").as_ref()).await);
|
||||||
|
assert_eq!(unreviewed_hunks(&action_log, cx), vec![]); // Hunks are cleared
|
||||||
|
|
||||||
|
// AI modifies file again
|
||||||
|
cx.update(|cx| {
|
||||||
|
buffer.update(cx, |buffer, cx| buffer.set_text("ai content v2", cx));
|
||||||
|
action_log.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx));
|
||||||
|
});
|
||||||
|
project
|
||||||
|
.update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
cx.run_until_parked();
|
||||||
|
assert_ne!(unreviewed_hunks(&action_log, cx), vec![]);
|
||||||
|
|
||||||
|
// User clicks "Reject All"
|
||||||
|
action_log
|
||||||
|
.update(cx, |log, cx| log.reject_all_edits(cx))
|
||||||
|
.await;
|
||||||
|
cx.run_until_parked();
|
||||||
|
assert!(fs.is_file(path!("/dir/new_file").as_ref()).await);
|
||||||
|
assert_eq!(
|
||||||
|
buffer.read_with(cx, |buffer, _| buffer.text()),
|
||||||
|
"ai content v1"
|
||||||
|
);
|
||||||
|
assert_eq!(unreviewed_hunks(&action_log, cx), vec![]);
|
||||||
|
}
|
||||||
|
|
||||||
#[gpui::test(iterations = 100)]
|
#[gpui::test(iterations = 100)]
|
||||||
async fn test_random_diffs(mut rng: StdRng, cx: &mut TestAppContext) {
|
async fn test_random_diffs(mut rng: StdRng, cx: &mut TestAppContext) {
|
||||||
init_test(cx);
|
init_test(cx);
|
||||||
|
|
|
@ -32,11 +32,18 @@ pub enum Model {
|
||||||
ClaudeSonnet4Thinking,
|
ClaudeSonnet4Thinking,
|
||||||
#[serde(rename = "claude-opus-4", alias = "claude-opus-4-latest")]
|
#[serde(rename = "claude-opus-4", alias = "claude-opus-4-latest")]
|
||||||
ClaudeOpus4,
|
ClaudeOpus4,
|
||||||
|
#[serde(rename = "claude-opus-4-1", alias = "claude-opus-4-1-latest")]
|
||||||
|
ClaudeOpus4_1,
|
||||||
#[serde(
|
#[serde(
|
||||||
rename = "claude-opus-4-thinking",
|
rename = "claude-opus-4-thinking",
|
||||||
alias = "claude-opus-4-thinking-latest"
|
alias = "claude-opus-4-thinking-latest"
|
||||||
)]
|
)]
|
||||||
ClaudeOpus4Thinking,
|
ClaudeOpus4Thinking,
|
||||||
|
#[serde(
|
||||||
|
rename = "claude-opus-4-1-thinking",
|
||||||
|
alias = "claude-opus-4-1-thinking-latest"
|
||||||
|
)]
|
||||||
|
ClaudeOpus4_1Thinking,
|
||||||
#[serde(rename = "claude-3-5-sonnet-v2", alias = "claude-3-5-sonnet-latest")]
|
#[serde(rename = "claude-3-5-sonnet-v2", alias = "claude-3-5-sonnet-latest")]
|
||||||
Claude3_5SonnetV2,
|
Claude3_5SonnetV2,
|
||||||
#[serde(rename = "claude-3-7-sonnet", alias = "claude-3-7-sonnet-latest")]
|
#[serde(rename = "claude-3-7-sonnet", alias = "claude-3-7-sonnet-latest")]
|
||||||
|
@ -147,7 +154,9 @@ impl Model {
|
||||||
Model::ClaudeSonnet4 => "claude-4-sonnet",
|
Model::ClaudeSonnet4 => "claude-4-sonnet",
|
||||||
Model::ClaudeSonnet4Thinking => "claude-4-sonnet-thinking",
|
Model::ClaudeSonnet4Thinking => "claude-4-sonnet-thinking",
|
||||||
Model::ClaudeOpus4 => "claude-4-opus",
|
Model::ClaudeOpus4 => "claude-4-opus",
|
||||||
|
Model::ClaudeOpus4_1 => "claude-4-opus-1",
|
||||||
Model::ClaudeOpus4Thinking => "claude-4-opus-thinking",
|
Model::ClaudeOpus4Thinking => "claude-4-opus-thinking",
|
||||||
|
Model::ClaudeOpus4_1Thinking => "claude-4-opus-1-thinking",
|
||||||
Model::Claude3_5SonnetV2 => "claude-3-5-sonnet-v2",
|
Model::Claude3_5SonnetV2 => "claude-3-5-sonnet-v2",
|
||||||
Model::Claude3_5Sonnet => "claude-3-5-sonnet",
|
Model::Claude3_5Sonnet => "claude-3-5-sonnet",
|
||||||
Model::Claude3Opus => "claude-3-opus",
|
Model::Claude3Opus => "claude-3-opus",
|
||||||
|
@ -208,6 +217,9 @@ impl Model {
|
||||||
Model::ClaudeOpus4 | Model::ClaudeOpus4Thinking => {
|
Model::ClaudeOpus4 | Model::ClaudeOpus4Thinking => {
|
||||||
"anthropic.claude-opus-4-20250514-v1:0"
|
"anthropic.claude-opus-4-20250514-v1:0"
|
||||||
}
|
}
|
||||||
|
Model::ClaudeOpus4_1 | Model::ClaudeOpus4_1Thinking => {
|
||||||
|
"anthropic.claude-opus-4-1-20250805-v1:0"
|
||||||
|
}
|
||||||
Model::Claude3_5SonnetV2 => "anthropic.claude-3-5-sonnet-20241022-v2:0",
|
Model::Claude3_5SonnetV2 => "anthropic.claude-3-5-sonnet-20241022-v2:0",
|
||||||
Model::Claude3_5Sonnet => "anthropic.claude-3-5-sonnet-20240620-v1:0",
|
Model::Claude3_5Sonnet => "anthropic.claude-3-5-sonnet-20240620-v1:0",
|
||||||
Model::Claude3Opus => "anthropic.claude-3-opus-20240229-v1:0",
|
Model::Claude3Opus => "anthropic.claude-3-opus-20240229-v1:0",
|
||||||
|
@ -266,7 +278,9 @@ impl Model {
|
||||||
Self::ClaudeSonnet4 => "Claude Sonnet 4",
|
Self::ClaudeSonnet4 => "Claude Sonnet 4",
|
||||||
Self::ClaudeSonnet4Thinking => "Claude Sonnet 4 Thinking",
|
Self::ClaudeSonnet4Thinking => "Claude Sonnet 4 Thinking",
|
||||||
Self::ClaudeOpus4 => "Claude Opus 4",
|
Self::ClaudeOpus4 => "Claude Opus 4",
|
||||||
|
Self::ClaudeOpus4_1 => "Claude Opus 4.1",
|
||||||
Self::ClaudeOpus4Thinking => "Claude Opus 4 Thinking",
|
Self::ClaudeOpus4Thinking => "Claude Opus 4 Thinking",
|
||||||
|
Self::ClaudeOpus4_1Thinking => "Claude Opus 4.1 Thinking",
|
||||||
Self::Claude3_5SonnetV2 => "Claude 3.5 Sonnet v2",
|
Self::Claude3_5SonnetV2 => "Claude 3.5 Sonnet v2",
|
||||||
Self::Claude3_5Sonnet => "Claude 3.5 Sonnet",
|
Self::Claude3_5Sonnet => "Claude 3.5 Sonnet",
|
||||||
Self::Claude3Opus => "Claude 3 Opus",
|
Self::Claude3Opus => "Claude 3 Opus",
|
||||||
|
@ -330,8 +344,10 @@ impl Model {
|
||||||
| Self::Claude3_7Sonnet
|
| Self::Claude3_7Sonnet
|
||||||
| Self::ClaudeSonnet4
|
| Self::ClaudeSonnet4
|
||||||
| Self::ClaudeOpus4
|
| Self::ClaudeOpus4
|
||||||
|
| Self::ClaudeOpus4_1
|
||||||
| Self::ClaudeSonnet4Thinking
|
| Self::ClaudeSonnet4Thinking
|
||||||
| Self::ClaudeOpus4Thinking => 200_000,
|
| Self::ClaudeOpus4Thinking
|
||||||
|
| Self::ClaudeOpus4_1Thinking => 200_000,
|
||||||
Self::AmazonNovaPremier => 1_000_000,
|
Self::AmazonNovaPremier => 1_000_000,
|
||||||
Self::PalmyraWriterX5 => 1_000_000,
|
Self::PalmyraWriterX5 => 1_000_000,
|
||||||
Self::PalmyraWriterX4 => 128_000,
|
Self::PalmyraWriterX4 => 128_000,
|
||||||
|
@ -348,7 +364,9 @@ impl Model {
|
||||||
| Self::ClaudeSonnet4
|
| Self::ClaudeSonnet4
|
||||||
| Self::ClaudeSonnet4Thinking
|
| Self::ClaudeSonnet4Thinking
|
||||||
| Self::ClaudeOpus4
|
| Self::ClaudeOpus4
|
||||||
| Model::ClaudeOpus4Thinking => 128_000,
|
| Model::ClaudeOpus4Thinking
|
||||||
|
| Self::ClaudeOpus4_1
|
||||||
|
| Model::ClaudeOpus4_1Thinking => 128_000,
|
||||||
Self::Claude3_5SonnetV2 | Self::PalmyraWriterX4 | Self::PalmyraWriterX5 => 8_192,
|
Self::Claude3_5SonnetV2 | Self::PalmyraWriterX4 | Self::PalmyraWriterX5 => 8_192,
|
||||||
Self::Custom {
|
Self::Custom {
|
||||||
max_output_tokens, ..
|
max_output_tokens, ..
|
||||||
|
@ -366,6 +384,8 @@ impl Model {
|
||||||
| Self::Claude3_7Sonnet
|
| Self::Claude3_7Sonnet
|
||||||
| Self::ClaudeOpus4
|
| Self::ClaudeOpus4
|
||||||
| Self::ClaudeOpus4Thinking
|
| Self::ClaudeOpus4Thinking
|
||||||
|
| Self::ClaudeOpus4_1
|
||||||
|
| Self::ClaudeOpus4_1Thinking
|
||||||
| Self::ClaudeSonnet4
|
| Self::ClaudeSonnet4
|
||||||
| Self::ClaudeSonnet4Thinking => 1.0,
|
| Self::ClaudeSonnet4Thinking => 1.0,
|
||||||
Self::Custom {
|
Self::Custom {
|
||||||
|
@ -387,6 +407,8 @@ impl Model {
|
||||||
| Self::Claude3_7SonnetThinking
|
| Self::Claude3_7SonnetThinking
|
||||||
| Self::ClaudeOpus4
|
| Self::ClaudeOpus4
|
||||||
| Self::ClaudeOpus4Thinking
|
| Self::ClaudeOpus4Thinking
|
||||||
|
| Self::ClaudeOpus4_1
|
||||||
|
| Self::ClaudeOpus4_1Thinking
|
||||||
| Self::ClaudeSonnet4
|
| Self::ClaudeSonnet4
|
||||||
| Self::ClaudeSonnet4Thinking
|
| Self::ClaudeSonnet4Thinking
|
||||||
| Self::Claude3_5Haiku => true,
|
| Self::Claude3_5Haiku => true,
|
||||||
|
@ -420,7 +442,9 @@ impl Model {
|
||||||
| Self::ClaudeSonnet4
|
| Self::ClaudeSonnet4
|
||||||
| Self::ClaudeSonnet4Thinking
|
| Self::ClaudeSonnet4Thinking
|
||||||
| Self::ClaudeOpus4
|
| Self::ClaudeOpus4
|
||||||
| Self::ClaudeOpus4Thinking => true,
|
| Self::ClaudeOpus4Thinking
|
||||||
|
| Self::ClaudeOpus4_1
|
||||||
|
| Self::ClaudeOpus4_1Thinking => true,
|
||||||
|
|
||||||
// Custom models - check if they have cache configuration
|
// Custom models - check if they have cache configuration
|
||||||
Self::Custom {
|
Self::Custom {
|
||||||
|
@ -440,7 +464,9 @@ impl Model {
|
||||||
| Self::ClaudeSonnet4
|
| Self::ClaudeSonnet4
|
||||||
| Self::ClaudeSonnet4Thinking
|
| Self::ClaudeSonnet4Thinking
|
||||||
| Self::ClaudeOpus4
|
| Self::ClaudeOpus4
|
||||||
| Self::ClaudeOpus4Thinking => Some(BedrockModelCacheConfiguration {
|
| Self::ClaudeOpus4Thinking
|
||||||
|
| Self::ClaudeOpus4_1
|
||||||
|
| Self::ClaudeOpus4_1Thinking => Some(BedrockModelCacheConfiguration {
|
||||||
max_cache_anchors: 4,
|
max_cache_anchors: 4,
|
||||||
min_total_token: 1024,
|
min_total_token: 1024,
|
||||||
}),
|
}),
|
||||||
|
@ -467,9 +493,11 @@ impl Model {
|
||||||
Model::ClaudeSonnet4Thinking => BedrockModelMode::Thinking {
|
Model::ClaudeSonnet4Thinking => BedrockModelMode::Thinking {
|
||||||
budget_tokens: Some(4096),
|
budget_tokens: Some(4096),
|
||||||
},
|
},
|
||||||
Model::ClaudeOpus4Thinking => BedrockModelMode::Thinking {
|
Model::ClaudeOpus4Thinking | Model::ClaudeOpus4_1Thinking => {
|
||||||
budget_tokens: Some(4096),
|
BedrockModelMode::Thinking {
|
||||||
},
|
budget_tokens: Some(4096),
|
||||||
|
}
|
||||||
|
}
|
||||||
_ => BedrockModelMode::Default,
|
_ => BedrockModelMode::Default,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -518,6 +546,8 @@ impl Model {
|
||||||
| Model::ClaudeSonnet4Thinking
|
| Model::ClaudeSonnet4Thinking
|
||||||
| Model::ClaudeOpus4
|
| Model::ClaudeOpus4
|
||||||
| Model::ClaudeOpus4Thinking
|
| Model::ClaudeOpus4Thinking
|
||||||
|
| Model::ClaudeOpus4_1
|
||||||
|
| Model::ClaudeOpus4_1Thinking
|
||||||
| Model::Claude3Haiku
|
| Model::Claude3Haiku
|
||||||
| Model::Claude3Opus
|
| Model::Claude3Opus
|
||||||
| Model::Claude3Sonnet
|
| Model::Claude3Sonnet
|
||||||
|
|
|
@ -259,20 +259,6 @@ async fn test_channel_messages(cx: &mut TestAppContext) {
|
||||||
assert_channels(&channel_store, &[(0, "the-channel".to_string())], cx);
|
assert_channels(&channel_store, &[(0, "the-channel".to_string())], cx);
|
||||||
});
|
});
|
||||||
|
|
||||||
let get_users = server.receive::<proto::GetUsers>().await.unwrap();
|
|
||||||
assert_eq!(get_users.payload.user_ids, vec![5]);
|
|
||||||
server.respond(
|
|
||||||
get_users.receipt(),
|
|
||||||
proto::UsersResponse {
|
|
||||||
users: vec![proto::User {
|
|
||||||
id: 5,
|
|
||||||
github_login: "nathansobo".into(),
|
|
||||||
avatar_url: "http://avatar.com/nathansobo".into(),
|
|
||||||
name: None,
|
|
||||||
}],
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
// Join a channel and populate its existing messages.
|
// Join a channel and populate its existing messages.
|
||||||
let channel = channel_store.update(cx, |store, cx| {
|
let channel = channel_store.update(cx, |store, cx| {
|
||||||
let channel_id = store.ordered_channels().next().unwrap().1.id;
|
let channel_id = store.ordered_channels().next().unwrap().1.id;
|
||||||
|
@ -334,7 +320,7 @@ async fn test_channel_messages(cx: &mut TestAppContext) {
|
||||||
.map(|message| (message.sender.github_login.clone(), message.body.clone()))
|
.map(|message| (message.sender.github_login.clone(), message.body.clone()))
|
||||||
.collect::<Vec<_>>(),
|
.collect::<Vec<_>>(),
|
||||||
&[
|
&[
|
||||||
("nathansobo".into(), "a".into()),
|
("user-5".into(), "a".into()),
|
||||||
("maxbrunsfeld".into(), "b".into())
|
("maxbrunsfeld".into(), "b".into())
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
@ -437,7 +423,7 @@ async fn test_channel_messages(cx: &mut TestAppContext) {
|
||||||
.map(|message| (message.sender.github_login.clone(), message.body.clone()))
|
.map(|message| (message.sender.github_login.clone(), message.body.clone()))
|
||||||
.collect::<Vec<_>>(),
|
.collect::<Vec<_>>(),
|
||||||
&[
|
&[
|
||||||
("nathansobo".into(), "y".into()),
|
("user-5".into(), "y".into()),
|
||||||
("maxbrunsfeld".into(), "z".into())
|
("maxbrunsfeld".into(), "z".into())
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
|
@ -17,11 +17,11 @@ test-support = ["clock/test-support", "collections/test-support", "gpui/test-sup
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow.workspace = true
|
anyhow.workspace = true
|
||||||
async-recursion = "0.3"
|
|
||||||
async-tungstenite = { workspace = true, features = ["tokio", "tokio-rustls-manual-roots"] }
|
async-tungstenite = { workspace = true, features = ["tokio", "tokio-rustls-manual-roots"] }
|
||||||
base64.workspace = true
|
base64.workspace = true
|
||||||
chrono = { workspace = true, features = ["serde"] }
|
chrono = { workspace = true, features = ["serde"] }
|
||||||
clock.workspace = true
|
clock.workspace = true
|
||||||
|
cloud_api_client.workspace = true
|
||||||
cloud_llm_client.workspace = true
|
cloud_llm_client.workspace = true
|
||||||
collections.workspace = true
|
collections.workspace = true
|
||||||
credentials_provider.workspace = true
|
credentials_provider.workspace = true
|
||||||
|
|
|
@ -6,22 +6,23 @@ pub mod telemetry;
|
||||||
pub mod user;
|
pub mod user;
|
||||||
pub mod zed_urls;
|
pub mod zed_urls;
|
||||||
|
|
||||||
use anyhow::{Context as _, Result, anyhow, bail};
|
use anyhow::{Context as _, Result, anyhow};
|
||||||
use async_recursion::async_recursion;
|
|
||||||
use async_tungstenite::tungstenite::{
|
use async_tungstenite::tungstenite::{
|
||||||
client::IntoClientRequest,
|
client::IntoClientRequest,
|
||||||
error::Error as WebsocketError,
|
error::Error as WebsocketError,
|
||||||
http::{HeaderValue, Request, StatusCode},
|
http::{HeaderValue, Request, StatusCode},
|
||||||
};
|
};
|
||||||
use chrono::{DateTime, Utc};
|
|
||||||
use clock::SystemClock;
|
use clock::SystemClock;
|
||||||
|
use cloud_api_client::CloudApiClient;
|
||||||
|
use cloud_api_client::websocket_protocol::MessageToClient;
|
||||||
use credentials_provider::CredentialsProvider;
|
use credentials_provider::CredentialsProvider;
|
||||||
|
use feature_flags::FeatureFlagAppExt as _;
|
||||||
use futures::{
|
use futures::{
|
||||||
AsyncReadExt, FutureExt, SinkExt, Stream, StreamExt, TryFutureExt as _, TryStreamExt,
|
AsyncReadExt, FutureExt, SinkExt, Stream, StreamExt, TryFutureExt as _, TryStreamExt,
|
||||||
channel::oneshot, future::BoxFuture,
|
channel::oneshot, future::BoxFuture,
|
||||||
};
|
};
|
||||||
use gpui::{App, AsyncApp, Entity, Global, Task, WeakEntity, actions};
|
use gpui::{App, AsyncApp, Entity, Global, Task, WeakEntity, actions};
|
||||||
use http_client::{AsyncBody, HttpClient, HttpClientWithUrl, http};
|
use http_client::{HttpClient, HttpClientWithUrl, http};
|
||||||
use parking_lot::RwLock;
|
use parking_lot::RwLock;
|
||||||
use postage::watch;
|
use postage::watch;
|
||||||
use proxy::connect_proxy_stream;
|
use proxy::connect_proxy_stream;
|
||||||
|
@ -151,7 +152,6 @@ impl Settings for ProxySettings {
|
||||||
|
|
||||||
pub fn init_settings(cx: &mut App) {
|
pub fn init_settings(cx: &mut App) {
|
||||||
TelemetrySettings::register(cx);
|
TelemetrySettings::register(cx);
|
||||||
DisableAiSettings::register(cx);
|
|
||||||
ClientSettings::register(cx);
|
ClientSettings::register(cx);
|
||||||
ProxySettings::register(cx);
|
ProxySettings::register(cx);
|
||||||
}
|
}
|
||||||
|
@ -162,20 +162,8 @@ pub fn init(client: &Arc<Client>, cx: &mut App) {
|
||||||
let client = client.clone();
|
let client = client.clone();
|
||||||
move |_: &SignIn, cx| {
|
move |_: &SignIn, cx| {
|
||||||
if let Some(client) = client.upgrade() {
|
if let Some(client) = client.upgrade() {
|
||||||
cx.spawn(
|
cx.spawn(async move |cx| client.sign_in_with_optional_connect(true, &cx).await)
|
||||||
async move |cx| match client.authenticate_and_connect(true, &cx).await {
|
.detach_and_log_err(cx);
|
||||||
ConnectionResult::Timeout => {
|
|
||||||
log::error!("Initial authentication timed out");
|
|
||||||
}
|
|
||||||
ConnectionResult::ConnectionReset => {
|
|
||||||
log::error!("Initial authentication connection reset");
|
|
||||||
}
|
|
||||||
ConnectionResult::Result(r) => {
|
|
||||||
r.log_err();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.detach();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -205,6 +193,8 @@ pub fn init(client: &Arc<Client>, cx: &mut App) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub type MessageToClientHandler = Box<dyn Fn(&MessageToClient, &mut App) + Send + Sync + 'static>;
|
||||||
|
|
||||||
struct GlobalClient(Arc<Client>);
|
struct GlobalClient(Arc<Client>);
|
||||||
|
|
||||||
impl Global for GlobalClient {}
|
impl Global for GlobalClient {}
|
||||||
|
@ -213,10 +203,12 @@ pub struct Client {
|
||||||
id: AtomicU64,
|
id: AtomicU64,
|
||||||
peer: Arc<Peer>,
|
peer: Arc<Peer>,
|
||||||
http: Arc<HttpClientWithUrl>,
|
http: Arc<HttpClientWithUrl>,
|
||||||
|
cloud_client: Arc<CloudApiClient>,
|
||||||
telemetry: Arc<Telemetry>,
|
telemetry: Arc<Telemetry>,
|
||||||
credentials_provider: ClientCredentialsProvider,
|
credentials_provider: ClientCredentialsProvider,
|
||||||
state: RwLock<ClientState>,
|
state: RwLock<ClientState>,
|
||||||
handler_set: parking_lot::Mutex<ProtoMessageHandlerSet>,
|
handler_set: parking_lot::Mutex<ProtoMessageHandlerSet>,
|
||||||
|
message_to_client_handlers: parking_lot::Mutex<Vec<MessageToClientHandler>>,
|
||||||
|
|
||||||
#[allow(clippy::type_complexity)]
|
#[allow(clippy::type_complexity)]
|
||||||
#[cfg(any(test, feature = "test-support"))]
|
#[cfg(any(test, feature = "test-support"))]
|
||||||
|
@ -283,6 +275,8 @@ pub enum Status {
|
||||||
SignedOut,
|
SignedOut,
|
||||||
UpgradeRequired,
|
UpgradeRequired,
|
||||||
Authenticating,
|
Authenticating,
|
||||||
|
Authenticated,
|
||||||
|
AuthenticationError,
|
||||||
Connecting,
|
Connecting,
|
||||||
ConnectionError,
|
ConnectionError,
|
||||||
Connected {
|
Connected {
|
||||||
|
@ -549,33 +543,6 @@ impl settings::Settings for TelemetrySettings {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Whether to disable all AI features in Zed.
|
|
||||||
///
|
|
||||||
/// Default: false
|
|
||||||
#[derive(Copy, Clone, Debug)]
|
|
||||||
pub struct DisableAiSettings {
|
|
||||||
pub disable_ai: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl settings::Settings for DisableAiSettings {
|
|
||||||
const KEY: Option<&'static str> = Some("disable_ai");
|
|
||||||
|
|
||||||
type FileContent = Option<bool>;
|
|
||||||
|
|
||||||
fn load(sources: SettingsSources<Self::FileContent>, _: &mut App) -> Result<Self> {
|
|
||||||
Ok(Self {
|
|
||||||
disable_ai: sources
|
|
||||||
.user
|
|
||||||
.or(sources.server)
|
|
||||||
.copied()
|
|
||||||
.flatten()
|
|
||||||
.unwrap_or(sources.default.ok_or_else(Self::missing_default)?),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn import_from_vscode(_vscode: &settings::VsCodeSettings, _current: &mut Self::FileContent) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Client {
|
impl Client {
|
||||||
pub fn new(
|
pub fn new(
|
||||||
clock: Arc<dyn SystemClock>,
|
clock: Arc<dyn SystemClock>,
|
||||||
|
@ -586,10 +553,12 @@ impl Client {
|
||||||
id: AtomicU64::new(0),
|
id: AtomicU64::new(0),
|
||||||
peer: Peer::new(0),
|
peer: Peer::new(0),
|
||||||
telemetry: Telemetry::new(clock, http.clone(), cx),
|
telemetry: Telemetry::new(clock, http.clone(), cx),
|
||||||
|
cloud_client: Arc::new(CloudApiClient::new(http.clone())),
|
||||||
http,
|
http,
|
||||||
credentials_provider: ClientCredentialsProvider::new(cx),
|
credentials_provider: ClientCredentialsProvider::new(cx),
|
||||||
state: Default::default(),
|
state: Default::default(),
|
||||||
handler_set: Default::default(),
|
handler_set: Default::default(),
|
||||||
|
message_to_client_handlers: parking_lot::Mutex::new(Vec::new()),
|
||||||
|
|
||||||
#[cfg(any(test, feature = "test-support"))]
|
#[cfg(any(test, feature = "test-support"))]
|
||||||
authenticate: Default::default(),
|
authenticate: Default::default(),
|
||||||
|
@ -618,6 +587,10 @@ impl Client {
|
||||||
self.http.clone()
|
self.http.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn cloud_client(&self) -> Arc<CloudApiClient> {
|
||||||
|
self.cloud_client.clone()
|
||||||
|
}
|
||||||
|
|
||||||
pub fn set_id(&self, id: u64) -> &Self {
|
pub fn set_id(&self, id: u64) -> &Self {
|
||||||
self.id.store(id, Ordering::SeqCst);
|
self.id.store(id, Ordering::SeqCst);
|
||||||
self
|
self
|
||||||
|
@ -704,7 +677,7 @@ impl Client {
|
||||||
|
|
||||||
let mut delay = INITIAL_RECONNECTION_DELAY;
|
let mut delay = INITIAL_RECONNECTION_DELAY;
|
||||||
loop {
|
loop {
|
||||||
match client.authenticate_and_connect(true, &cx).await {
|
match client.connect(true, &cx).await {
|
||||||
ConnectionResult::Timeout => {
|
ConnectionResult::Timeout => {
|
||||||
log::error!("client connect attempt timed out")
|
log::error!("client connect attempt timed out")
|
||||||
}
|
}
|
||||||
|
@ -720,7 +693,10 @@ impl Client {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if matches!(*client.status().borrow(), Status::ConnectionError) {
|
if matches!(
|
||||||
|
*client.status().borrow(),
|
||||||
|
Status::AuthenticationError | Status::ConnectionError
|
||||||
|
) {
|
||||||
client.set_status(
|
client.set_status(
|
||||||
Status::ReconnectionError {
|
Status::ReconnectionError {
|
||||||
next_reconnection: Instant::now() + delay,
|
next_reconnection: Instant::now() + delay,
|
||||||
|
@ -874,17 +850,181 @@ impl Client {
|
||||||
.is_some()
|
.is_some()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_recursion(?Send)]
|
pub async fn sign_in(
|
||||||
pub async fn authenticate_and_connect(
|
self: &Arc<Self>,
|
||||||
|
try_provider: bool,
|
||||||
|
cx: &AsyncApp,
|
||||||
|
) -> Result<Credentials> {
|
||||||
|
if self.status().borrow().is_signed_out() {
|
||||||
|
self.set_status(Status::Authenticating, cx);
|
||||||
|
} else {
|
||||||
|
self.set_status(Status::Reauthenticating, cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut credentials = None;
|
||||||
|
|
||||||
|
let old_credentials = self.state.read().credentials.clone();
|
||||||
|
if let Some(old_credentials) = old_credentials {
|
||||||
|
if self.validate_credentials(&old_credentials, cx).await? {
|
||||||
|
credentials = Some(old_credentials);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if credentials.is_none() && try_provider {
|
||||||
|
if let Some(stored_credentials) = self.credentials_provider.read_credentials(cx).await {
|
||||||
|
if self.validate_credentials(&stored_credentials, cx).await? {
|
||||||
|
credentials = Some(stored_credentials);
|
||||||
|
} else {
|
||||||
|
self.credentials_provider
|
||||||
|
.delete_credentials(cx)
|
||||||
|
.await
|
||||||
|
.log_err();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if credentials.is_none() {
|
||||||
|
let mut status_rx = self.status();
|
||||||
|
let _ = status_rx.next().await;
|
||||||
|
futures::select_biased! {
|
||||||
|
authenticate = self.authenticate(cx).fuse() => {
|
||||||
|
match authenticate {
|
||||||
|
Ok(creds) => {
|
||||||
|
if IMPERSONATE_LOGIN.is_none() {
|
||||||
|
self.credentials_provider
|
||||||
|
.write_credentials(creds.user_id, creds.access_token.clone(), cx)
|
||||||
|
.await
|
||||||
|
.log_err();
|
||||||
|
}
|
||||||
|
|
||||||
|
credentials = Some(creds);
|
||||||
|
},
|
||||||
|
Err(err) => {
|
||||||
|
self.set_status(Status::AuthenticationError, cx);
|
||||||
|
return Err(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ = status_rx.next().fuse() => {
|
||||||
|
return Err(anyhow!("authentication canceled"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let credentials = credentials.unwrap();
|
||||||
|
self.set_id(credentials.user_id);
|
||||||
|
self.cloud_client
|
||||||
|
.set_credentials(credentials.user_id as u32, credentials.access_token.clone());
|
||||||
|
self.state.write().credentials = Some(credentials.clone());
|
||||||
|
self.set_status(Status::Authenticated, cx);
|
||||||
|
|
||||||
|
Ok(credentials)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn validate_credentials(
|
||||||
|
self: &Arc<Self>,
|
||||||
|
credentials: &Credentials,
|
||||||
|
cx: &AsyncApp,
|
||||||
|
) -> Result<bool> {
|
||||||
|
match self
|
||||||
|
.cloud_client
|
||||||
|
.validate_credentials(credentials.user_id as u32, &credentials.access_token)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(valid) => Ok(valid),
|
||||||
|
Err(err) => {
|
||||||
|
self.set_status(Status::AuthenticationError, cx);
|
||||||
|
Err(anyhow!("failed to validate credentials: {}", err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Establishes a WebSocket connection with Cloud for receiving updates from the server.
|
||||||
|
async fn connect_to_cloud(self: &Arc<Self>, cx: &AsyncApp) -> Result<()> {
|
||||||
|
let connect_task = cx.update({
|
||||||
|
let cloud_client = self.cloud_client.clone();
|
||||||
|
move |cx| cloud_client.connect(cx)
|
||||||
|
})??;
|
||||||
|
let connection = connect_task.await?;
|
||||||
|
|
||||||
|
let (mut messages, task) = cx.update(|cx| connection.spawn(cx))?;
|
||||||
|
task.detach();
|
||||||
|
|
||||||
|
cx.spawn({
|
||||||
|
let this = self.clone();
|
||||||
|
async move |cx| {
|
||||||
|
while let Some(message) = messages.next().await {
|
||||||
|
if let Some(message) = message.log_err() {
|
||||||
|
this.handle_message_to_client(message, cx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Performs a sign-in and also (optionally) connects to Collab.
|
||||||
|
///
|
||||||
|
/// Only Zed staff automatically connect to Collab.
|
||||||
|
pub async fn sign_in_with_optional_connect(
|
||||||
|
self: &Arc<Self>,
|
||||||
|
try_provider: bool,
|
||||||
|
cx: &AsyncApp,
|
||||||
|
) -> Result<()> {
|
||||||
|
let (is_staff_tx, is_staff_rx) = oneshot::channel::<bool>();
|
||||||
|
let mut is_staff_tx = Some(is_staff_tx);
|
||||||
|
cx.update(|cx| {
|
||||||
|
cx.on_flags_ready(move |state, _cx| {
|
||||||
|
if let Some(is_staff_tx) = is_staff_tx.take() {
|
||||||
|
is_staff_tx.send(state.is_staff).log_err();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
})
|
||||||
|
.log_err();
|
||||||
|
|
||||||
|
let credentials = self.sign_in(try_provider, cx).await?;
|
||||||
|
|
||||||
|
self.connect_to_cloud(cx).await.log_err();
|
||||||
|
|
||||||
|
cx.update(move |cx| {
|
||||||
|
cx.spawn({
|
||||||
|
let client = self.clone();
|
||||||
|
async move |cx| {
|
||||||
|
let is_staff = is_staff_rx.await?;
|
||||||
|
if is_staff {
|
||||||
|
match client.connect_with_credentials(credentials, cx).await {
|
||||||
|
ConnectionResult::Timeout => Err(anyhow!("connection timed out")),
|
||||||
|
ConnectionResult::ConnectionReset => Err(anyhow!("connection reset")),
|
||||||
|
ConnectionResult::Result(result) => {
|
||||||
|
result.context("client auth and connect")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.detach_and_log_err(cx);
|
||||||
|
})
|
||||||
|
.log_err();
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn connect(
|
||||||
self: &Arc<Self>,
|
self: &Arc<Self>,
|
||||||
try_provider: bool,
|
try_provider: bool,
|
||||||
cx: &AsyncApp,
|
cx: &AsyncApp,
|
||||||
) -> ConnectionResult<()> {
|
) -> ConnectionResult<()> {
|
||||||
let was_disconnected = match *self.status().borrow() {
|
let was_disconnected = match *self.status().borrow() {
|
||||||
Status::SignedOut => true,
|
Status::SignedOut | Status::Authenticated => true,
|
||||||
Status::ConnectionError
|
Status::ConnectionError
|
||||||
| Status::ConnectionLost
|
| Status::ConnectionLost
|
||||||
| Status::Authenticating { .. }
|
| Status::Authenticating { .. }
|
||||||
|
| Status::AuthenticationError
|
||||||
| Status::Reauthenticating { .. }
|
| Status::Reauthenticating { .. }
|
||||||
| Status::ReconnectionError { .. } => false,
|
| Status::ReconnectionError { .. } => false,
|
||||||
Status::Connected { .. } | Status::Connecting { .. } | Status::Reconnecting { .. } => {
|
Status::Connected { .. } | Status::Connecting { .. } | Status::Reconnecting { .. } => {
|
||||||
|
@ -897,39 +1037,10 @@ impl Client {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
if was_disconnected {
|
let credentials = match self.sign_in(try_provider, cx).await {
|
||||||
self.set_status(Status::Authenticating, cx);
|
Ok(credentials) => credentials,
|
||||||
} else {
|
Err(err) => return ConnectionResult::Result(Err(err)),
|
||||||
self.set_status(Status::Reauthenticating, cx)
|
};
|
||||||
}
|
|
||||||
|
|
||||||
let mut read_from_provider = false;
|
|
||||||
let mut credentials = self.state.read().credentials.clone();
|
|
||||||
if credentials.is_none() && try_provider {
|
|
||||||
credentials = self.credentials_provider.read_credentials(cx).await;
|
|
||||||
read_from_provider = credentials.is_some();
|
|
||||||
}
|
|
||||||
|
|
||||||
if credentials.is_none() {
|
|
||||||
let mut status_rx = self.status();
|
|
||||||
let _ = status_rx.next().await;
|
|
||||||
futures::select_biased! {
|
|
||||||
authenticate = self.authenticate(cx).fuse() => {
|
|
||||||
match authenticate {
|
|
||||||
Ok(creds) => credentials = Some(creds),
|
|
||||||
Err(err) => {
|
|
||||||
self.set_status(Status::ConnectionError, cx);
|
|
||||||
return ConnectionResult::Result(Err(err));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ = status_rx.next().fuse() => {
|
|
||||||
return ConnectionResult::Result(Err(anyhow!("authentication canceled")));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let credentials = credentials.unwrap();
|
|
||||||
self.set_id(credentials.user_id);
|
|
||||||
|
|
||||||
if was_disconnected {
|
if was_disconnected {
|
||||||
self.set_status(Status::Connecting, cx);
|
self.set_status(Status::Connecting, cx);
|
||||||
|
@ -937,17 +1048,20 @@ impl Client {
|
||||||
self.set_status(Status::Reconnecting, cx);
|
self.set_status(Status::Reconnecting, cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
self.connect_with_credentials(credentials, cx).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn connect_with_credentials(
|
||||||
|
self: &Arc<Self>,
|
||||||
|
credentials: Credentials,
|
||||||
|
cx: &AsyncApp,
|
||||||
|
) -> ConnectionResult<()> {
|
||||||
let mut timeout =
|
let mut timeout =
|
||||||
futures::FutureExt::fuse(cx.background_executor().timer(CONNECTION_TIMEOUT));
|
futures::FutureExt::fuse(cx.background_executor().timer(CONNECTION_TIMEOUT));
|
||||||
futures::select_biased! {
|
futures::select_biased! {
|
||||||
connection = self.establish_connection(&credentials, cx).fuse() => {
|
connection = self.establish_connection(&credentials, cx).fuse() => {
|
||||||
match connection {
|
match connection {
|
||||||
Ok(conn) => {
|
Ok(conn) => {
|
||||||
self.state.write().credentials = Some(credentials.clone());
|
|
||||||
if !read_from_provider && IMPERSONATE_LOGIN.is_none() {
|
|
||||||
self.credentials_provider.write_credentials(credentials.user_id, credentials.access_token, cx).await.log_err();
|
|
||||||
}
|
|
||||||
|
|
||||||
futures::select_biased! {
|
futures::select_biased! {
|
||||||
result = self.set_connection(conn, cx).fuse() => {
|
result = self.set_connection(conn, cx).fuse() => {
|
||||||
match result.context("client auth and connect") {
|
match result.context("client auth and connect") {
|
||||||
|
@ -965,15 +1079,8 @@ impl Client {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(EstablishConnectionError::Unauthorized) => {
|
Err(EstablishConnectionError::Unauthorized) => {
|
||||||
self.state.write().credentials.take();
|
self.set_status(Status::ConnectionError, cx);
|
||||||
if read_from_provider {
|
ConnectionResult::Result(Err(EstablishConnectionError::Unauthorized).context("client auth and connect"))
|
||||||
self.credentials_provider.delete_credentials(cx).await.log_err();
|
|
||||||
self.set_status(Status::SignedOut, cx);
|
|
||||||
self.authenticate_and_connect(false, cx).await
|
|
||||||
} else {
|
|
||||||
self.set_status(Status::ConnectionError, cx);
|
|
||||||
ConnectionResult::Result(Err(EstablishConnectionError::Unauthorized).context("client auth and connect"))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Err(EstablishConnectionError::UpgradeRequired) => {
|
Err(EstablishConnectionError::UpgradeRequired) => {
|
||||||
self.set_status(Status::UpgradeRequired, cx);
|
self.set_status(Status::UpgradeRequired, cx);
|
||||||
|
@ -1368,96 +1475,31 @@ impl Client {
|
||||||
self: &Arc<Self>,
|
self: &Arc<Self>,
|
||||||
http: Arc<HttpClientWithUrl>,
|
http: Arc<HttpClientWithUrl>,
|
||||||
login: String,
|
login: String,
|
||||||
mut api_token: String,
|
api_token: String,
|
||||||
) -> Result<Credentials> {
|
) -> Result<Credentials> {
|
||||||
#[derive(Deserialize)]
|
#[derive(Serialize)]
|
||||||
struct AuthenticatedUserResponse {
|
struct ImpersonateUserBody {
|
||||||
user: User,
|
github_login: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
struct User {
|
struct ImpersonateUserResponse {
|
||||||
id: u64,
|
user_id: u64,
|
||||||
|
access_token: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
let github_user = {
|
let url = self
|
||||||
#[derive(Deserialize)]
|
.http
|
||||||
struct GithubUser {
|
.build_zed_cloud_url("/internal/users/impersonate", &[])?;
|
||||||
id: i32,
|
let request = Request::post(url.as_str())
|
||||||
login: String,
|
.header("Content-Type", "application/json")
|
||||||
created_at: DateTime<Utc>,
|
.header("Authorization", format!("Bearer {api_token}"))
|
||||||
}
|
.body(
|
||||||
|
serde_json::to_string(&ImpersonateUserBody {
|
||||||
let request = {
|
github_login: login,
|
||||||
let mut request_builder =
|
})?
|
||||||
Request::get(&format!("https://api.github.com/users/{login}"));
|
.into(),
|
||||||
if let Ok(github_token) = std::env::var("GITHUB_TOKEN") {
|
)?;
|
||||||
request_builder =
|
|
||||||
request_builder.header("Authorization", format!("Bearer {}", github_token));
|
|
||||||
}
|
|
||||||
|
|
||||||
request_builder.body(AsyncBody::empty())?
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut response = http
|
|
||||||
.send(request)
|
|
||||||
.await
|
|
||||||
.context("error fetching GitHub user")?;
|
|
||||||
|
|
||||||
let mut body = Vec::new();
|
|
||||||
response
|
|
||||||
.body_mut()
|
|
||||||
.read_to_end(&mut body)
|
|
||||||
.await
|
|
||||||
.context("error reading GitHub user")?;
|
|
||||||
|
|
||||||
if !response.status().is_success() {
|
|
||||||
let text = String::from_utf8_lossy(body.as_slice());
|
|
||||||
bail!(
|
|
||||||
"status error {}, response: {text:?}",
|
|
||||||
response.status().as_u16()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
serde_json::from_slice::<GithubUser>(body.as_slice()).map_err(|err| {
|
|
||||||
log::error!("Error deserializing: {:?}", err);
|
|
||||||
log::error!(
|
|
||||||
"GitHub API response text: {:?}",
|
|
||||||
String::from_utf8_lossy(body.as_slice())
|
|
||||||
);
|
|
||||||
anyhow!("error deserializing GitHub user")
|
|
||||||
})?
|
|
||||||
};
|
|
||||||
|
|
||||||
let query_params = [
|
|
||||||
("github_login", &github_user.login),
|
|
||||||
("github_user_id", &github_user.id.to_string()),
|
|
||||||
(
|
|
||||||
"github_user_created_at",
|
|
||||||
&github_user.created_at.to_rfc3339(),
|
|
||||||
),
|
|
||||||
];
|
|
||||||
|
|
||||||
// Use the collab server's admin API to retrieve the ID
|
|
||||||
// of the impersonated user.
|
|
||||||
let mut url = self.rpc_url(http.clone(), None).await?;
|
|
||||||
url.set_path("/user");
|
|
||||||
url.set_query(Some(
|
|
||||||
&query_params
|
|
||||||
.iter()
|
|
||||||
.map(|(key, value)| {
|
|
||||||
format!(
|
|
||||||
"{}={}",
|
|
||||||
key,
|
|
||||||
url::form_urlencoded::byte_serialize(value.as_bytes()).collect::<String>()
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.collect::<Vec<String>>()
|
|
||||||
.join("&"),
|
|
||||||
));
|
|
||||||
let request: http_client::Request<AsyncBody> = Request::get(url.as_str())
|
|
||||||
.header("Authorization", format!("token {api_token}"))
|
|
||||||
.body("".into())?;
|
|
||||||
|
|
||||||
let mut response = http.send(request).await?;
|
let mut response = http.send(request).await?;
|
||||||
let mut body = String::new();
|
let mut body = String::new();
|
||||||
|
@ -1468,18 +1510,17 @@ impl Client {
|
||||||
response.status().as_u16(),
|
response.status().as_u16(),
|
||||||
body,
|
body,
|
||||||
);
|
);
|
||||||
let response: AuthenticatedUserResponse = serde_json::from_str(&body)?;
|
let response: ImpersonateUserResponse = serde_json::from_str(&body)?;
|
||||||
|
|
||||||
// Use the admin API token to authenticate as the impersonated user.
|
|
||||||
api_token.insert_str(0, "ADMIN_TOKEN:");
|
|
||||||
Ok(Credentials {
|
Ok(Credentials {
|
||||||
user_id: response.user.id,
|
user_id: response.user_id,
|
||||||
access_token: api_token,
|
access_token: response.access_token,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn sign_out(self: &Arc<Self>, cx: &AsyncApp) {
|
pub async fn sign_out(self: &Arc<Self>, cx: &AsyncApp) {
|
||||||
self.state.write().credentials = None;
|
self.state.write().credentials = None;
|
||||||
|
self.cloud_client.clear_credentials();
|
||||||
self.disconnect(cx);
|
self.disconnect(cx);
|
||||||
|
|
||||||
if self.has_credentials(cx).await {
|
if self.has_credentials(cx).await {
|
||||||
|
@ -1641,6 +1682,24 @@ impl Client {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn add_message_to_client_handler(
|
||||||
|
self: &Arc<Client>,
|
||||||
|
handler: impl Fn(&MessageToClient, &mut App) + Send + Sync + 'static,
|
||||||
|
) {
|
||||||
|
self.message_to_client_handlers
|
||||||
|
.lock()
|
||||||
|
.push(Box::new(handler));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_message_to_client(self: &Arc<Client>, message: MessageToClient, cx: &AsyncApp) {
|
||||||
|
cx.update(|cx| {
|
||||||
|
for handler in self.message_to_client_handlers.lock().iter() {
|
||||||
|
handler(&message, cx);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
|
}
|
||||||
|
|
||||||
pub fn telemetry(&self) -> &Arc<Telemetry> {
|
pub fn telemetry(&self) -> &Arc<Telemetry> {
|
||||||
&self.telemetry
|
&self.telemetry
|
||||||
}
|
}
|
||||||
|
@ -1708,7 +1767,7 @@ pub fn parse_zed_link<'a>(link: &'a str, cx: &App) -> Option<&'a str> {
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::test::FakeServer;
|
use crate::test::{FakeServer, parse_authorization_header};
|
||||||
|
|
||||||
use clock::FakeSystemClock;
|
use clock::FakeSystemClock;
|
||||||
use gpui::{AppContext as _, BackgroundExecutor, TestAppContext};
|
use gpui::{AppContext as _, BackgroundExecutor, TestAppContext};
|
||||||
|
@ -1759,6 +1818,46 @@ mod tests {
|
||||||
assert_eq!(server.auth_count(), 2); // Client re-authenticated due to an invalid token
|
assert_eq!(server.auth_count(), 2); // Client re-authenticated due to an invalid token
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[gpui::test(iterations = 10)]
|
||||||
|
async fn test_auth_failure_during_reconnection(cx: &mut TestAppContext) {
|
||||||
|
init_test(cx);
|
||||||
|
let http_client = FakeHttpClient::with_200_response();
|
||||||
|
let client =
|
||||||
|
cx.update(|cx| Client::new(Arc::new(FakeSystemClock::new()), http_client.clone(), cx));
|
||||||
|
let server = FakeServer::for_client(42, &client, cx).await;
|
||||||
|
let mut status = client.status();
|
||||||
|
assert!(matches!(
|
||||||
|
status.next().await,
|
||||||
|
Some(Status::Connected { .. })
|
||||||
|
));
|
||||||
|
assert_eq!(server.auth_count(), 1);
|
||||||
|
|
||||||
|
// Simulate an auth failure during reconnection.
|
||||||
|
http_client
|
||||||
|
.as_fake()
|
||||||
|
.replace_handler(|_, _request| async move {
|
||||||
|
Ok(http_client::Response::builder()
|
||||||
|
.status(503)
|
||||||
|
.body("".into())
|
||||||
|
.unwrap())
|
||||||
|
});
|
||||||
|
server.disconnect();
|
||||||
|
while !matches!(status.next().await, Some(Status::ReconnectionError { .. })) {}
|
||||||
|
|
||||||
|
// Restore the ability to authenticate.
|
||||||
|
http_client
|
||||||
|
.as_fake()
|
||||||
|
.replace_handler(|_, _request| async move {
|
||||||
|
Ok(http_client::Response::builder()
|
||||||
|
.status(200)
|
||||||
|
.body("".into())
|
||||||
|
.unwrap())
|
||||||
|
});
|
||||||
|
cx.executor().advance_clock(Duration::from_secs(10));
|
||||||
|
while !matches!(status.next().await, Some(Status::Connected { .. })) {}
|
||||||
|
assert_eq!(server.auth_count(), 1); // Client reused the cached credentials when reconnecting
|
||||||
|
}
|
||||||
|
|
||||||
#[gpui::test(iterations = 10)]
|
#[gpui::test(iterations = 10)]
|
||||||
async fn test_connection_timeout(executor: BackgroundExecutor, cx: &mut TestAppContext) {
|
async fn test_connection_timeout(executor: BackgroundExecutor, cx: &mut TestAppContext) {
|
||||||
init_test(cx);
|
init_test(cx);
|
||||||
|
@ -1789,7 +1888,7 @@ mod tests {
|
||||||
});
|
});
|
||||||
let auth_and_connect = cx.spawn({
|
let auth_and_connect = cx.spawn({
|
||||||
let client = client.clone();
|
let client = client.clone();
|
||||||
|cx| async move { client.authenticate_and_connect(false, &cx).await }
|
|cx| async move { client.connect(false, &cx).await }
|
||||||
});
|
});
|
||||||
executor.run_until_parked();
|
executor.run_until_parked();
|
||||||
assert!(matches!(status.next().await, Some(Status::Connecting)));
|
assert!(matches!(status.next().await, Some(Status::Connecting)));
|
||||||
|
@ -1834,6 +1933,75 @@ mod tests {
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[gpui::test(iterations = 10)]
|
||||||
|
async fn test_reauthenticate_only_if_unauthorized(cx: &mut TestAppContext) {
|
||||||
|
init_test(cx);
|
||||||
|
let auth_count = Arc::new(Mutex::new(0));
|
||||||
|
let http_client = FakeHttpClient::create(|_request| async move {
|
||||||
|
Ok(http_client::Response::builder()
|
||||||
|
.status(200)
|
||||||
|
.body("".into())
|
||||||
|
.unwrap())
|
||||||
|
});
|
||||||
|
let client =
|
||||||
|
cx.update(|cx| Client::new(Arc::new(FakeSystemClock::new()), http_client.clone(), cx));
|
||||||
|
client.override_authenticate({
|
||||||
|
let auth_count = auth_count.clone();
|
||||||
|
move |cx| {
|
||||||
|
let auth_count = auth_count.clone();
|
||||||
|
cx.background_spawn(async move {
|
||||||
|
*auth_count.lock() += 1;
|
||||||
|
Ok(Credentials {
|
||||||
|
user_id: 1,
|
||||||
|
access_token: auth_count.lock().to_string(),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let credentials = client.sign_in(false, &cx.to_async()).await.unwrap();
|
||||||
|
assert_eq!(*auth_count.lock(), 1);
|
||||||
|
assert_eq!(credentials.access_token, "1");
|
||||||
|
|
||||||
|
// If credentials are still valid, signing in doesn't trigger authentication.
|
||||||
|
let credentials = client.sign_in(false, &cx.to_async()).await.unwrap();
|
||||||
|
assert_eq!(*auth_count.lock(), 1);
|
||||||
|
assert_eq!(credentials.access_token, "1");
|
||||||
|
|
||||||
|
// If the server is unavailable, signing in doesn't trigger authentication.
|
||||||
|
http_client
|
||||||
|
.as_fake()
|
||||||
|
.replace_handler(|_, _request| async move {
|
||||||
|
Ok(http_client::Response::builder()
|
||||||
|
.status(503)
|
||||||
|
.body("".into())
|
||||||
|
.unwrap())
|
||||||
|
});
|
||||||
|
client.sign_in(false, &cx.to_async()).await.unwrap_err();
|
||||||
|
assert_eq!(*auth_count.lock(), 1);
|
||||||
|
|
||||||
|
// If credentials became invalid, signing in triggers authentication.
|
||||||
|
http_client
|
||||||
|
.as_fake()
|
||||||
|
.replace_handler(|_, request| async move {
|
||||||
|
let credentials = parse_authorization_header(&request).unwrap();
|
||||||
|
if credentials.access_token == "2" {
|
||||||
|
Ok(http_client::Response::builder()
|
||||||
|
.status(200)
|
||||||
|
.body("".into())
|
||||||
|
.unwrap())
|
||||||
|
} else {
|
||||||
|
Ok(http_client::Response::builder()
|
||||||
|
.status(401)
|
||||||
|
.body("".into())
|
||||||
|
.unwrap())
|
||||||
|
}
|
||||||
|
});
|
||||||
|
let credentials = client.sign_in(false, &cx.to_async()).await.unwrap();
|
||||||
|
assert_eq!(*auth_count.lock(), 2);
|
||||||
|
assert_eq!(credentials.access_token, "2");
|
||||||
|
}
|
||||||
|
|
||||||
#[gpui::test(iterations = 10)]
|
#[gpui::test(iterations = 10)]
|
||||||
async fn test_authenticating_more_than_once(
|
async fn test_authenticating_more_than_once(
|
||||||
cx: &mut TestAppContext,
|
cx: &mut TestAppContext,
|
||||||
|
@ -1866,7 +2034,7 @@ mod tests {
|
||||||
|
|
||||||
let _authenticate = cx.spawn({
|
let _authenticate = cx.spawn({
|
||||||
let client = client.clone();
|
let client = client.clone();
|
||||||
move |cx| async move { client.authenticate_and_connect(false, &cx).await }
|
move |cx| async move { client.connect(false, &cx).await }
|
||||||
});
|
});
|
||||||
executor.run_until_parked();
|
executor.run_until_parked();
|
||||||
assert_eq!(*auth_count.lock(), 1);
|
assert_eq!(*auth_count.lock(), 1);
|
||||||
|
@ -1874,7 +2042,7 @@ mod tests {
|
||||||
|
|
||||||
let _authenticate = cx.spawn({
|
let _authenticate = cx.spawn({
|
||||||
let client = client.clone();
|
let client = client.clone();
|
||||||
|cx| async move { client.authenticate_and_connect(false, &cx).await }
|
|cx| async move { client.connect(false, &cx).await }
|
||||||
});
|
});
|
||||||
executor.run_until_parked();
|
executor.run_until_parked();
|
||||||
assert_eq!(*auth_count.lock(), 2);
|
assert_eq!(*auth_count.lock(), 2);
|
||||||
|
|
|
@ -1,8 +1,11 @@
|
||||||
use crate::{Client, Connection, Credentials, EstablishConnectionError, UserStore};
|
use crate::{Client, Connection, Credentials, EstablishConnectionError, UserStore};
|
||||||
use anyhow::{Context as _, Result, anyhow};
|
use anyhow::{Context as _, Result, anyhow};
|
||||||
use chrono::Duration;
|
use chrono::Duration;
|
||||||
|
use cloud_api_client::{AuthenticatedUser, GetAuthenticatedUserResponse, PlanInfo};
|
||||||
|
use cloud_llm_client::{CurrentUsage, Plan, UsageData, UsageLimit};
|
||||||
use futures::{StreamExt, stream::BoxStream};
|
use futures::{StreamExt, stream::BoxStream};
|
||||||
use gpui::{AppContext as _, BackgroundExecutor, Entity, TestAppContext};
|
use gpui::{AppContext as _, BackgroundExecutor, Entity, TestAppContext};
|
||||||
|
use http_client::{AsyncBody, Method, Request, http};
|
||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
use rpc::{
|
use rpc::{
|
||||||
ConnectionId, Peer, Receipt, TypedEnvelope,
|
ConnectionId, Peer, Receipt, TypedEnvelope,
|
||||||
|
@ -39,6 +42,44 @@ impl FakeServer {
|
||||||
executor: cx.executor(),
|
executor: cx.executor(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
client.http_client().as_fake().replace_handler({
|
||||||
|
let state = server.state.clone();
|
||||||
|
move |old_handler, req| {
|
||||||
|
let state = state.clone();
|
||||||
|
let old_handler = old_handler.clone();
|
||||||
|
async move {
|
||||||
|
match (req.method(), req.uri().path()) {
|
||||||
|
(&Method::GET, "/client/users/me") => {
|
||||||
|
let credentials = parse_authorization_header(&req);
|
||||||
|
if credentials
|
||||||
|
!= Some(Credentials {
|
||||||
|
user_id: client_user_id,
|
||||||
|
access_token: state.lock().access_token.to_string(),
|
||||||
|
})
|
||||||
|
{
|
||||||
|
return Ok(http_client::Response::builder()
|
||||||
|
.status(401)
|
||||||
|
.body("Unauthorized".into())
|
||||||
|
.unwrap());
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(http_client::Response::builder()
|
||||||
|
.status(200)
|
||||||
|
.body(
|
||||||
|
serde_json::to_string(&make_get_authenticated_user_response(
|
||||||
|
client_user_id as i32,
|
||||||
|
format!("user-{client_user_id}"),
|
||||||
|
))
|
||||||
|
.unwrap()
|
||||||
|
.into(),
|
||||||
|
)
|
||||||
|
.unwrap())
|
||||||
|
}
|
||||||
|
_ => old_handler(req).await,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
client
|
client
|
||||||
.override_authenticate({
|
.override_authenticate({
|
||||||
let state = Arc::downgrade(&server.state);
|
let state = Arc::downgrade(&server.state);
|
||||||
|
@ -105,7 +146,7 @@ impl FakeServer {
|
||||||
});
|
});
|
||||||
|
|
||||||
client
|
client
|
||||||
.authenticate_and_connect(false, &cx.to_async())
|
.connect(false, &cx.to_async())
|
||||||
.await
|
.await
|
||||||
.into_response()
|
.into_response()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
@ -223,3 +264,54 @@ impl Drop for FakeServer {
|
||||||
self.disconnect();
|
self.disconnect();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn parse_authorization_header(req: &Request<AsyncBody>) -> Option<Credentials> {
|
||||||
|
let mut auth_header = req
|
||||||
|
.headers()
|
||||||
|
.get(http::header::AUTHORIZATION)?
|
||||||
|
.to_str()
|
||||||
|
.ok()?
|
||||||
|
.split_whitespace();
|
||||||
|
let user_id = auth_header.next()?.parse().ok()?;
|
||||||
|
let access_token = auth_header.next()?;
|
||||||
|
Some(Credentials {
|
||||||
|
user_id,
|
||||||
|
access_token: access_token.to_string(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn make_get_authenticated_user_response(
|
||||||
|
user_id: i32,
|
||||||
|
github_login: String,
|
||||||
|
) -> GetAuthenticatedUserResponse {
|
||||||
|
GetAuthenticatedUserResponse {
|
||||||
|
user: AuthenticatedUser {
|
||||||
|
id: user_id,
|
||||||
|
metrics_id: format!("metrics-id-{user_id}"),
|
||||||
|
avatar_url: "".to_string(),
|
||||||
|
github_login,
|
||||||
|
name: None,
|
||||||
|
is_staff: false,
|
||||||
|
accepted_tos_at: None,
|
||||||
|
},
|
||||||
|
feature_flags: vec![],
|
||||||
|
plan: PlanInfo {
|
||||||
|
plan: Plan::ZedPro,
|
||||||
|
subscription_period: None,
|
||||||
|
usage: CurrentUsage {
|
||||||
|
model_requests: UsageData {
|
||||||
|
used: 0,
|
||||||
|
limit: UsageLimit::Limited(500),
|
||||||
|
},
|
||||||
|
edit_predictions: UsageData {
|
||||||
|
used: 250,
|
||||||
|
limit: UsageLimit::Unlimited,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
trial_started_at: None,
|
||||||
|
is_usage_based_billing_enabled: false,
|
||||||
|
is_account_too_young: false,
|
||||||
|
has_overdue_invoices: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
use super::{Client, Status, TypedEnvelope, proto};
|
use super::{Client, Status, TypedEnvelope, proto};
|
||||||
use anyhow::{Context as _, Result, anyhow};
|
use anyhow::{Context as _, Result, anyhow};
|
||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
|
use cloud_api_client::websocket_protocol::MessageToClient;
|
||||||
|
use cloud_api_client::{GetAuthenticatedUserResponse, PlanInfo};
|
||||||
use cloud_llm_client::{
|
use cloud_llm_client::{
|
||||||
EDIT_PREDICTIONS_USAGE_AMOUNT_HEADER_NAME, EDIT_PREDICTIONS_USAGE_LIMIT_HEADER_NAME,
|
EDIT_PREDICTIONS_USAGE_AMOUNT_HEADER_NAME, EDIT_PREDICTIONS_USAGE_LIMIT_HEADER_NAME,
|
||||||
MODEL_REQUESTS_USAGE_AMOUNT_HEADER_NAME, MODEL_REQUESTS_USAGE_LIMIT_HEADER_NAME, UsageLimit,
|
MODEL_REQUESTS_USAGE_AMOUNT_HEADER_NAME, MODEL_REQUESTS_USAGE_LIMIT_HEADER_NAME, UsageLimit,
|
||||||
|
@ -20,7 +22,7 @@ use std::{
|
||||||
sync::{Arc, Weak},
|
sync::{Arc, Weak},
|
||||||
};
|
};
|
||||||
use text::ReplicaId;
|
use text::ReplicaId;
|
||||||
use util::{TryFutureExt as _, maybe};
|
use util::{ResultExt, TryFutureExt as _};
|
||||||
|
|
||||||
pub type UserId = u64;
|
pub type UserId = u64;
|
||||||
|
|
||||||
|
@ -110,16 +112,11 @@ pub struct UserStore {
|
||||||
by_github_login: HashMap<String, u64>,
|
by_github_login: HashMap<String, u64>,
|
||||||
participant_indices: HashMap<u64, ParticipantIndex>,
|
participant_indices: HashMap<u64, ParticipantIndex>,
|
||||||
update_contacts_tx: mpsc::UnboundedSender<UpdateContacts>,
|
update_contacts_tx: mpsc::UnboundedSender<UpdateContacts>,
|
||||||
current_plan: Option<proto::Plan>,
|
|
||||||
subscription_period: Option<(DateTime<Utc>, DateTime<Utc>)>,
|
|
||||||
trial_started_at: Option<DateTime<Utc>>,
|
|
||||||
model_request_usage: Option<ModelRequestUsage>,
|
model_request_usage: Option<ModelRequestUsage>,
|
||||||
edit_prediction_usage: Option<EditPredictionUsage>,
|
edit_prediction_usage: Option<EditPredictionUsage>,
|
||||||
is_usage_based_billing_enabled: Option<bool>,
|
plan_info: Option<PlanInfo>,
|
||||||
account_too_young: Option<bool>,
|
|
||||||
has_overdue_invoices: Option<bool>,
|
|
||||||
current_user: watch::Receiver<Option<Arc<User>>>,
|
current_user: watch::Receiver<Option<Arc<User>>>,
|
||||||
accepted_tos_at: Option<Option<DateTime<Utc>>>,
|
accepted_tos_at: Option<Option<cloud_api_client::Timestamp>>,
|
||||||
contacts: Vec<Arc<Contact>>,
|
contacts: Vec<Arc<Contact>>,
|
||||||
incoming_contact_requests: Vec<Arc<User>>,
|
incoming_contact_requests: Vec<Arc<User>>,
|
||||||
outgoing_contact_requests: Vec<Arc<User>>,
|
outgoing_contact_requests: Vec<Arc<User>>,
|
||||||
|
@ -145,6 +142,7 @@ pub enum Event {
|
||||||
ShowContacts,
|
ShowContacts,
|
||||||
ParticipantIndicesChanged,
|
ParticipantIndicesChanged,
|
||||||
PrivateUserInfoUpdated,
|
PrivateUserInfoUpdated,
|
||||||
|
PlanUpdated,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy)]
|
#[derive(Clone, Copy)]
|
||||||
|
@ -184,18 +182,19 @@ impl UserStore {
|
||||||
client.add_message_handler(cx.weak_entity(), Self::handle_update_invite_info),
|
client.add_message_handler(cx.weak_entity(), Self::handle_update_invite_info),
|
||||||
client.add_message_handler(cx.weak_entity(), Self::handle_show_contacts),
|
client.add_message_handler(cx.weak_entity(), Self::handle_show_contacts),
|
||||||
];
|
];
|
||||||
|
|
||||||
|
client.add_message_to_client_handler({
|
||||||
|
let this = cx.weak_entity();
|
||||||
|
move |message, cx| Self::handle_message_to_client(this.clone(), message, cx)
|
||||||
|
});
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
users: Default::default(),
|
users: Default::default(),
|
||||||
by_github_login: Default::default(),
|
by_github_login: Default::default(),
|
||||||
current_user: current_user_rx,
|
current_user: current_user_rx,
|
||||||
current_plan: None,
|
plan_info: None,
|
||||||
subscription_period: None,
|
|
||||||
trial_started_at: None,
|
|
||||||
model_request_usage: None,
|
model_request_usage: None,
|
||||||
edit_prediction_usage: None,
|
edit_prediction_usage: None,
|
||||||
is_usage_based_billing_enabled: None,
|
|
||||||
account_too_young: None,
|
|
||||||
has_overdue_invoices: None,
|
|
||||||
accepted_tos_at: None,
|
accepted_tos_at: None,
|
||||||
contacts: Default::default(),
|
contacts: Default::default(),
|
||||||
incoming_contact_requests: Default::default(),
|
incoming_contact_requests: Default::default(),
|
||||||
|
@ -225,54 +224,48 @@ impl UserStore {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
};
|
};
|
||||||
match status {
|
match status {
|
||||||
Status::Connected { .. } => {
|
Status::Authenticated | Status::Connected { .. } => {
|
||||||
if let Some(user_id) = client.user_id() {
|
if let Some(user_id) = client.user_id() {
|
||||||
let fetch_user = if let Ok(fetch_user) =
|
let response = client
|
||||||
this.update(cx, |this, cx| this.get_user(user_id, cx).log_err())
|
.cloud_client()
|
||||||
{
|
.get_authenticated_user()
|
||||||
fetch_user
|
.await
|
||||||
|
.log_err();
|
||||||
|
|
||||||
|
let current_user_and_response = if let Some(response) = response {
|
||||||
|
let user = Arc::new(User {
|
||||||
|
id: user_id,
|
||||||
|
github_login: response.user.github_login.clone(),
|
||||||
|
avatar_uri: response.user.avatar_url.clone().into(),
|
||||||
|
name: response.user.name.clone(),
|
||||||
|
});
|
||||||
|
|
||||||
|
Some((user, response))
|
||||||
} else {
|
} else {
|
||||||
break;
|
None
|
||||||
};
|
};
|
||||||
let fetch_private_user_info =
|
current_user_tx
|
||||||
client.request(proto::GetPrivateUserInfo {}).log_err();
|
.send(
|
||||||
let (user, info) =
|
current_user_and_response
|
||||||
futures::join!(fetch_user, fetch_private_user_info);
|
.as_ref()
|
||||||
|
.map(|(user, _)| user.clone()),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.ok();
|
||||||
|
|
||||||
cx.update(|cx| {
|
cx.update(|cx| {
|
||||||
if let Some(info) = info {
|
if let Some((user, response)) = current_user_and_response {
|
||||||
let staff =
|
|
||||||
info.staff && !*feature_flags::ZED_DISABLE_STAFF;
|
|
||||||
cx.update_flags(staff, info.flags);
|
|
||||||
client.telemetry.set_authenticated_user_info(
|
|
||||||
Some(info.metrics_id.clone()),
|
|
||||||
staff,
|
|
||||||
);
|
|
||||||
|
|
||||||
this.update(cx, |this, cx| {
|
this.update(cx, |this, cx| {
|
||||||
let accepted_tos_at = {
|
this.by_github_login
|
||||||
#[cfg(debug_assertions)]
|
.insert(user.github_login.clone(), user_id);
|
||||||
if std::env::var("ZED_IGNORE_ACCEPTED_TOS").is_ok()
|
this.users.insert(user_id, user);
|
||||||
{
|
this.update_authenticated_user(response, cx)
|
||||||
None
|
|
||||||
} else {
|
|
||||||
info.accepted_tos_at
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(not(debug_assertions))]
|
|
||||||
info.accepted_tos_at
|
|
||||||
};
|
|
||||||
|
|
||||||
this.set_current_user_accepted_tos_at(accepted_tos_at);
|
|
||||||
cx.emit(Event::PrivateUserInfoUpdated);
|
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
anyhow::Ok(())
|
anyhow::Ok(())
|
||||||
}
|
}
|
||||||
})??;
|
})??;
|
||||||
|
|
||||||
current_user_tx.send(user).await.ok();
|
|
||||||
|
|
||||||
this.update(cx, |_, cx| cx.notify())?;
|
this.update(cx, |_, cx| cx.notify())?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -352,59 +345,22 @@ impl UserStore {
|
||||||
|
|
||||||
async fn handle_update_plan(
|
async fn handle_update_plan(
|
||||||
this: Entity<Self>,
|
this: Entity<Self>,
|
||||||
message: TypedEnvelope<proto::UpdateUserPlan>,
|
_message: TypedEnvelope<proto::UpdateUserPlan>,
|
||||||
mut cx: AsyncApp,
|
mut cx: AsyncApp,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
|
let client = this
|
||||||
|
.read_with(&cx, |this, _| this.client.upgrade())?
|
||||||
|
.context("client was dropped")?;
|
||||||
|
|
||||||
|
let response = client
|
||||||
|
.cloud_client()
|
||||||
|
.get_authenticated_user()
|
||||||
|
.await
|
||||||
|
.context("failed to fetch authenticated user")?;
|
||||||
|
|
||||||
this.update(&mut cx, |this, cx| {
|
this.update(&mut cx, |this, cx| {
|
||||||
this.current_plan = Some(message.payload.plan());
|
this.update_authenticated_user(response, cx);
|
||||||
this.subscription_period = maybe!({
|
})
|
||||||
let period = message.payload.subscription_period?;
|
|
||||||
let started_at = DateTime::from_timestamp(period.started_at as i64, 0)?;
|
|
||||||
let ended_at = DateTime::from_timestamp(period.ended_at as i64, 0)?;
|
|
||||||
|
|
||||||
Some((started_at, ended_at))
|
|
||||||
});
|
|
||||||
this.trial_started_at = message
|
|
||||||
.payload
|
|
||||||
.trial_started_at
|
|
||||||
.and_then(|trial_started_at| DateTime::from_timestamp(trial_started_at as i64, 0));
|
|
||||||
this.is_usage_based_billing_enabled = message.payload.is_usage_based_billing_enabled;
|
|
||||||
this.account_too_young = message.payload.account_too_young;
|
|
||||||
this.has_overdue_invoices = message.payload.has_overdue_invoices;
|
|
||||||
|
|
||||||
if let Some(usage) = message.payload.usage {
|
|
||||||
// limits are always present even though they are wrapped in Option
|
|
||||||
this.model_request_usage = usage
|
|
||||||
.model_requests_usage_limit
|
|
||||||
.and_then(|limit| {
|
|
||||||
RequestUsage::from_proto(usage.model_requests_usage_amount, limit)
|
|
||||||
})
|
|
||||||
.map(ModelRequestUsage);
|
|
||||||
this.edit_prediction_usage = usage
|
|
||||||
.edit_predictions_usage_limit
|
|
||||||
.and_then(|limit| {
|
|
||||||
RequestUsage::from_proto(usage.model_requests_usage_amount, limit)
|
|
||||||
})
|
|
||||||
.map(EditPredictionUsage);
|
|
||||||
}
|
|
||||||
|
|
||||||
cx.notify();
|
|
||||||
})?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn update_model_request_usage(&mut self, usage: ModelRequestUsage, cx: &mut Context<Self>) {
|
|
||||||
self.model_request_usage = Some(usage);
|
|
||||||
cx.notify();
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn update_edit_prediction_usage(
|
|
||||||
&mut self,
|
|
||||||
usage: EditPredictionUsage,
|
|
||||||
cx: &mut Context<Self>,
|
|
||||||
) {
|
|
||||||
self.edit_prediction_usage = Some(usage);
|
|
||||||
cx.notify();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn update_contacts(&mut self, message: UpdateContacts, cx: &Context<Self>) -> Task<Result<()>> {
|
fn update_contacts(&mut self, message: UpdateContacts, cx: &Context<Self>) -> Task<Result<()>> {
|
||||||
|
@ -763,59 +719,157 @@ impl UserStore {
|
||||||
self.current_user.borrow().clone()
|
self.current_user.borrow().clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn current_plan(&self) -> Option<proto::Plan> {
|
pub fn plan(&self) -> Option<cloud_llm_client::Plan> {
|
||||||
#[cfg(debug_assertions)]
|
#[cfg(debug_assertions)]
|
||||||
if let Ok(plan) = std::env::var("ZED_SIMULATE_PLAN").as_ref() {
|
if let Ok(plan) = std::env::var("ZED_SIMULATE_PLAN").as_ref() {
|
||||||
return match plan.as_str() {
|
return match plan.as_str() {
|
||||||
"free" => Some(proto::Plan::Free),
|
"free" => Some(cloud_llm_client::Plan::ZedFree),
|
||||||
"trial" => Some(proto::Plan::ZedProTrial),
|
"trial" => Some(cloud_llm_client::Plan::ZedProTrial),
|
||||||
"pro" => Some(proto::Plan::ZedPro),
|
"pro" => Some(cloud_llm_client::Plan::ZedPro),
|
||||||
_ => {
|
_ => {
|
||||||
panic!("ZED_SIMULATE_PLAN must be one of 'free', 'trial', or 'pro'");
|
panic!("ZED_SIMULATE_PLAN must be one of 'free', 'trial', or 'pro'");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
self.current_plan
|
self.plan_info.as_ref().map(|info| info.plan)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn subscription_period(&self) -> Option<(DateTime<Utc>, DateTime<Utc>)> {
|
pub fn subscription_period(&self) -> Option<(DateTime<Utc>, DateTime<Utc>)> {
|
||||||
self.subscription_period
|
self.plan_info
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|plan| plan.subscription_period)
|
||||||
|
.map(|subscription_period| {
|
||||||
|
(
|
||||||
|
subscription_period.started_at.0,
|
||||||
|
subscription_period.ended_at.0,
|
||||||
|
)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn trial_started_at(&self) -> Option<DateTime<Utc>> {
|
pub fn trial_started_at(&self) -> Option<DateTime<Utc>> {
|
||||||
self.trial_started_at
|
self.plan_info
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|plan| plan.trial_started_at)
|
||||||
|
.map(|trial_started_at| trial_started_at.0)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn usage_based_billing_enabled(&self) -> Option<bool> {
|
/// Returns whether the user's account is too new to use the service.
|
||||||
self.is_usage_based_billing_enabled
|
pub fn account_too_young(&self) -> bool {
|
||||||
|
self.plan_info
|
||||||
|
.as_ref()
|
||||||
|
.map(|plan| plan.is_account_too_young)
|
||||||
|
.unwrap_or_default()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns whether the current user has overdue invoices and usage should be blocked.
|
||||||
|
pub fn has_overdue_invoices(&self) -> bool {
|
||||||
|
self.plan_info
|
||||||
|
.as_ref()
|
||||||
|
.map(|plan| plan.has_overdue_invoices)
|
||||||
|
.unwrap_or_default()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_usage_based_billing_enabled(&self) -> bool {
|
||||||
|
self.plan_info
|
||||||
|
.as_ref()
|
||||||
|
.map(|plan| plan.is_usage_based_billing_enabled)
|
||||||
|
.unwrap_or_default()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn model_request_usage(&self) -> Option<ModelRequestUsage> {
|
pub fn model_request_usage(&self) -> Option<ModelRequestUsage> {
|
||||||
self.model_request_usage
|
self.model_request_usage
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn update_model_request_usage(&mut self, usage: ModelRequestUsage, cx: &mut Context<Self>) {
|
||||||
|
self.model_request_usage = Some(usage);
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
|
||||||
pub fn edit_prediction_usage(&self) -> Option<EditPredictionUsage> {
|
pub fn edit_prediction_usage(&self) -> Option<EditPredictionUsage> {
|
||||||
self.edit_prediction_usage
|
self.edit_prediction_usage
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn update_edit_prediction_usage(
|
||||||
|
&mut self,
|
||||||
|
usage: EditPredictionUsage,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) {
|
||||||
|
self.edit_prediction_usage = Some(usage);
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_authenticated_user(
|
||||||
|
&mut self,
|
||||||
|
response: GetAuthenticatedUserResponse,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) {
|
||||||
|
let staff = response.user.is_staff && !*feature_flags::ZED_DISABLE_STAFF;
|
||||||
|
cx.update_flags(staff, response.feature_flags);
|
||||||
|
if let Some(client) = self.client.upgrade() {
|
||||||
|
client
|
||||||
|
.telemetry
|
||||||
|
.set_authenticated_user_info(Some(response.user.metrics_id.clone()), staff);
|
||||||
|
}
|
||||||
|
|
||||||
|
let accepted_tos_at = {
|
||||||
|
#[cfg(debug_assertions)]
|
||||||
|
if std::env::var("ZED_IGNORE_ACCEPTED_TOS").is_ok() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
response.user.accepted_tos_at
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(debug_assertions))]
|
||||||
|
response.user.accepted_tos_at
|
||||||
|
};
|
||||||
|
|
||||||
|
self.accepted_tos_at = Some(accepted_tos_at);
|
||||||
|
self.model_request_usage = Some(ModelRequestUsage(RequestUsage {
|
||||||
|
limit: response.plan.usage.model_requests.limit,
|
||||||
|
amount: response.plan.usage.model_requests.used as i32,
|
||||||
|
}));
|
||||||
|
self.edit_prediction_usage = Some(EditPredictionUsage(RequestUsage {
|
||||||
|
limit: response.plan.usage.edit_predictions.limit,
|
||||||
|
amount: response.plan.usage.edit_predictions.used as i32,
|
||||||
|
}));
|
||||||
|
self.plan_info = Some(response.plan);
|
||||||
|
cx.emit(Event::PrivateUserInfoUpdated);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_message_to_client(this: WeakEntity<Self>, message: &MessageToClient, cx: &App) {
|
||||||
|
cx.spawn(async move |cx| {
|
||||||
|
match message {
|
||||||
|
MessageToClient::UserUpdated => {
|
||||||
|
let cloud_client = cx
|
||||||
|
.update(|cx| {
|
||||||
|
this.read_with(cx, |this, _cx| {
|
||||||
|
this.client.upgrade().map(|client| client.cloud_client())
|
||||||
|
})
|
||||||
|
})??
|
||||||
|
.ok_or(anyhow::anyhow!("Failed to get Cloud client"))?;
|
||||||
|
|
||||||
|
let response = cloud_client.get_authenticated_user().await?;
|
||||||
|
cx.update(|cx| {
|
||||||
|
this.update(cx, |this, cx| {
|
||||||
|
this.update_authenticated_user(response, cx);
|
||||||
|
})
|
||||||
|
})??;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
anyhow::Ok(())
|
||||||
|
})
|
||||||
|
.detach_and_log_err(cx);
|
||||||
|
}
|
||||||
|
|
||||||
pub fn watch_current_user(&self) -> watch::Receiver<Option<Arc<User>>> {
|
pub fn watch_current_user(&self) -> watch::Receiver<Option<Arc<User>>> {
|
||||||
self.current_user.clone()
|
self.current_user.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns whether the user's account is too new to use the service.
|
pub fn has_accepted_terms_of_service(&self) -> bool {
|
||||||
pub fn account_too_young(&self) -> bool {
|
|
||||||
self.account_too_young.unwrap_or(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns whether the current user has overdue invoices and usage should be blocked.
|
|
||||||
pub fn has_overdue_invoices(&self) -> bool {
|
|
||||||
self.has_overdue_invoices.unwrap_or(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn current_user_has_accepted_terms(&self) -> Option<bool> {
|
|
||||||
self.accepted_tos_at
|
self.accepted_tos_at
|
||||||
.map(|accepted_tos_at| accepted_tos_at.is_some())
|
.map_or(false, |accepted_tos_at| accepted_tos_at.is_some())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn accept_terms_of_service(&self, cx: &Context<Self>) -> Task<Result<()>> {
|
pub fn accept_terms_of_service(&self, cx: &Context<Self>) -> Task<Result<()>> {
|
||||||
|
@ -827,23 +881,18 @@ impl UserStore {
|
||||||
cx.spawn(async move |this, cx| -> anyhow::Result<()> {
|
cx.spawn(async move |this, cx| -> anyhow::Result<()> {
|
||||||
let client = client.upgrade().context("client not found")?;
|
let client = client.upgrade().context("client not found")?;
|
||||||
let response = client
|
let response = client
|
||||||
.request(proto::AcceptTermsOfService {})
|
.cloud_client()
|
||||||
|
.accept_terms_of_service()
|
||||||
.await
|
.await
|
||||||
.context("error accepting tos")?;
|
.context("error accepting tos")?;
|
||||||
this.update(cx, |this, cx| {
|
this.update(cx, |this, cx| {
|
||||||
this.set_current_user_accepted_tos_at(Some(response.accepted_tos_at));
|
this.accepted_tos_at = Some(response.user.accepted_tos_at);
|
||||||
cx.emit(Event::PrivateUserInfoUpdated);
|
cx.emit(Event::PrivateUserInfoUpdated);
|
||||||
})?;
|
})?;
|
||||||
Ok(())
|
Ok(())
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_current_user_accepted_tos_at(&mut self, accepted_tos_at: Option<u64>) {
|
|
||||||
self.accepted_tos_at = Some(
|
|
||||||
accepted_tos_at.and_then(|timestamp| DateTime::from_timestamp(timestamp as i64, 0)),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn load_users(
|
fn load_users(
|
||||||
&self,
|
&self,
|
||||||
request: impl RequestMessage<Response = UsersResponse>,
|
request: impl RequestMessage<Response = UsersResponse>,
|
||||||
|
|
24
crates/cloud_api_client/Cargo.toml
Normal file
24
crates/cloud_api_client/Cargo.toml
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
[package]
|
||||||
|
name = "cloud_api_client"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition.workspace = true
|
||||||
|
publish.workspace = true
|
||||||
|
license = "Apache-2.0"
|
||||||
|
|
||||||
|
[lints]
|
||||||
|
workspace = true
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
path = "src/cloud_api_client.rs"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
anyhow.workspace = true
|
||||||
|
cloud_api_types.workspace = true
|
||||||
|
futures.workspace = true
|
||||||
|
gpui.workspace = true
|
||||||
|
gpui_tokio.workspace = true
|
||||||
|
http_client.workspace = true
|
||||||
|
parking_lot.workspace = true
|
||||||
|
serde_json.workspace = true
|
||||||
|
workspace-hack.workspace = true
|
||||||
|
yawc.workspace = true
|
1
crates/cloud_api_client/LICENSE-APACHE
Symbolic link
1
crates/cloud_api_client/LICENSE-APACHE
Symbolic link
|
@ -0,0 +1 @@
|
||||||
|
../../LICENSE-APACHE
|
231
crates/cloud_api_client/src/cloud_api_client.rs
Normal file
231
crates/cloud_api_client/src/cloud_api_client.rs
Normal file
|
@ -0,0 +1,231 @@
|
||||||
|
mod websocket;
|
||||||
|
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use anyhow::{Context, Result, anyhow};
|
||||||
|
use cloud_api_types::websocket_protocol::{PROTOCOL_VERSION, PROTOCOL_VERSION_HEADER_NAME};
|
||||||
|
pub use cloud_api_types::*;
|
||||||
|
use futures::AsyncReadExt as _;
|
||||||
|
use gpui::{App, Task};
|
||||||
|
use gpui_tokio::Tokio;
|
||||||
|
use http_client::http::request;
|
||||||
|
use http_client::{AsyncBody, HttpClientWithUrl, Method, Request, StatusCode};
|
||||||
|
use parking_lot::RwLock;
|
||||||
|
use yawc::WebSocket;
|
||||||
|
|
||||||
|
use crate::websocket::Connection;
|
||||||
|
|
||||||
|
struct Credentials {
|
||||||
|
user_id: u32,
|
||||||
|
access_token: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct CloudApiClient {
|
||||||
|
credentials: RwLock<Option<Credentials>>,
|
||||||
|
http_client: Arc<HttpClientWithUrl>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CloudApiClient {
|
||||||
|
pub fn new(http_client: Arc<HttpClientWithUrl>) -> Self {
|
||||||
|
Self {
|
||||||
|
credentials: RwLock::new(None),
|
||||||
|
http_client,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn has_credentials(&self) -> bool {
|
||||||
|
self.credentials.read().is_some()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_credentials(&self, user_id: u32, access_token: String) {
|
||||||
|
*self.credentials.write() = Some(Credentials {
|
||||||
|
user_id,
|
||||||
|
access_token,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn clear_credentials(&self) {
|
||||||
|
*self.credentials.write() = None;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_request(
|
||||||
|
&self,
|
||||||
|
req: request::Builder,
|
||||||
|
body: impl Into<AsyncBody>,
|
||||||
|
) -> Result<Request<AsyncBody>> {
|
||||||
|
let credentials = self.credentials.read();
|
||||||
|
let credentials = credentials.as_ref().context("no credentials provided")?;
|
||||||
|
build_request(req, body, credentials)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_authenticated_user(&self) -> Result<GetAuthenticatedUserResponse> {
|
||||||
|
let request = self.build_request(
|
||||||
|
Request::builder().method(Method::GET).uri(
|
||||||
|
self.http_client
|
||||||
|
.build_zed_cloud_url("/client/users/me", &[])?
|
||||||
|
.as_ref(),
|
||||||
|
),
|
||||||
|
AsyncBody::default(),
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let mut response = self.http_client.send(request).await?;
|
||||||
|
|
||||||
|
if !response.status().is_success() {
|
||||||
|
let mut body = String::new();
|
||||||
|
response.body_mut().read_to_string(&mut body).await?;
|
||||||
|
|
||||||
|
anyhow::bail!(
|
||||||
|
"Failed to get authenticated user.\nStatus: {:?}\nBody: {body}",
|
||||||
|
response.status()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut body = String::new();
|
||||||
|
response.body_mut().read_to_string(&mut body).await?;
|
||||||
|
|
||||||
|
Ok(serde_json::from_str(&body)?)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn connect(&self, cx: &App) -> Result<Task<Result<Connection>>> {
|
||||||
|
let mut connect_url = self
|
||||||
|
.http_client
|
||||||
|
.build_zed_cloud_url("/client/users/connect", &[])?;
|
||||||
|
connect_url
|
||||||
|
.set_scheme(match connect_url.scheme() {
|
||||||
|
"https" => "wss",
|
||||||
|
"http" => "ws",
|
||||||
|
scheme => Err(anyhow!("invalid URL scheme: {scheme}"))?,
|
||||||
|
})
|
||||||
|
.map_err(|_| anyhow!("failed to set URL scheme"))?;
|
||||||
|
|
||||||
|
let credentials = self.credentials.read();
|
||||||
|
let credentials = credentials.as_ref().context("no credentials provided")?;
|
||||||
|
let authorization_header = format!("{} {}", credentials.user_id, credentials.access_token);
|
||||||
|
|
||||||
|
Ok(cx.spawn(async move |cx| {
|
||||||
|
let handle = cx
|
||||||
|
.update(|cx| Tokio::handle(cx))
|
||||||
|
.ok()
|
||||||
|
.context("failed to get Tokio handle")?;
|
||||||
|
let _guard = handle.enter();
|
||||||
|
|
||||||
|
let ws = WebSocket::connect(connect_url)
|
||||||
|
.with_request(
|
||||||
|
request::Builder::new()
|
||||||
|
.header("Authorization", authorization_header)
|
||||||
|
.header(PROTOCOL_VERSION_HEADER_NAME, PROTOCOL_VERSION.to_string()),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(Connection::new(ws))
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn accept_terms_of_service(&self) -> Result<AcceptTermsOfServiceResponse> {
|
||||||
|
let request = self.build_request(
|
||||||
|
Request::builder().method(Method::POST).uri(
|
||||||
|
self.http_client
|
||||||
|
.build_zed_cloud_url("/client/terms_of_service/accept", &[])?
|
||||||
|
.as_ref(),
|
||||||
|
),
|
||||||
|
AsyncBody::default(),
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let mut response = self.http_client.send(request).await?;
|
||||||
|
|
||||||
|
if !response.status().is_success() {
|
||||||
|
let mut body = String::new();
|
||||||
|
response.body_mut().read_to_string(&mut body).await?;
|
||||||
|
|
||||||
|
anyhow::bail!(
|
||||||
|
"Failed to accept terms of service.\nStatus: {:?}\nBody: {body}",
|
||||||
|
response.status()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut body = String::new();
|
||||||
|
response.body_mut().read_to_string(&mut body).await?;
|
||||||
|
|
||||||
|
Ok(serde_json::from_str(&body)?)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn create_llm_token(
|
||||||
|
&self,
|
||||||
|
system_id: Option<String>,
|
||||||
|
) -> Result<CreateLlmTokenResponse> {
|
||||||
|
let mut request_builder = Request::builder().method(Method::POST).uri(
|
||||||
|
self.http_client
|
||||||
|
.build_zed_cloud_url("/client/llm_tokens", &[])?
|
||||||
|
.as_ref(),
|
||||||
|
);
|
||||||
|
|
||||||
|
if let Some(system_id) = system_id {
|
||||||
|
request_builder = request_builder.header(ZED_SYSTEM_ID_HEADER_NAME, system_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
let request = self.build_request(request_builder, AsyncBody::default())?;
|
||||||
|
|
||||||
|
let mut response = self.http_client.send(request).await?;
|
||||||
|
|
||||||
|
if !response.status().is_success() {
|
||||||
|
let mut body = String::new();
|
||||||
|
response.body_mut().read_to_string(&mut body).await?;
|
||||||
|
|
||||||
|
anyhow::bail!(
|
||||||
|
"Failed to create LLM token.\nStatus: {:?}\nBody: {body}",
|
||||||
|
response.status()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut body = String::new();
|
||||||
|
response.body_mut().read_to_string(&mut body).await?;
|
||||||
|
|
||||||
|
Ok(serde_json::from_str(&body)?)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn validate_credentials(&self, user_id: u32, access_token: &str) -> Result<bool> {
|
||||||
|
let request = build_request(
|
||||||
|
Request::builder().method(Method::GET).uri(
|
||||||
|
self.http_client
|
||||||
|
.build_zed_cloud_url("/client/users/me", &[])?
|
||||||
|
.as_ref(),
|
||||||
|
),
|
||||||
|
AsyncBody::default(),
|
||||||
|
&Credentials {
|
||||||
|
user_id,
|
||||||
|
access_token: access_token.into(),
|
||||||
|
},
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let mut response = self.http_client.send(request).await?;
|
||||||
|
|
||||||
|
if response.status().is_success() {
|
||||||
|
Ok(true)
|
||||||
|
} else {
|
||||||
|
let mut body = String::new();
|
||||||
|
response.body_mut().read_to_string(&mut body).await?;
|
||||||
|
if response.status() == StatusCode::UNAUTHORIZED {
|
||||||
|
return Ok(false);
|
||||||
|
} else {
|
||||||
|
return Err(anyhow!(
|
||||||
|
"Failed to get authenticated user.\nStatus: {:?}\nBody: {body}",
|
||||||
|
response.status()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_request(
|
||||||
|
req: request::Builder,
|
||||||
|
body: impl Into<AsyncBody>,
|
||||||
|
credentials: &Credentials,
|
||||||
|
) -> Result<Request<AsyncBody>> {
|
||||||
|
Ok(req
|
||||||
|
.header("Content-Type", "application/json")
|
||||||
|
.header(
|
||||||
|
"Authorization",
|
||||||
|
format!("{} {}", credentials.user_id, credentials.access_token),
|
||||||
|
)
|
||||||
|
.body(body.into())?)
|
||||||
|
}
|
73
crates/cloud_api_client/src/websocket.rs
Normal file
73
crates/cloud_api_client/src/websocket.rs
Normal file
|
@ -0,0 +1,73 @@
|
||||||
|
use std::pin::Pin;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use anyhow::Result;
|
||||||
|
use cloud_api_types::websocket_protocol::MessageToClient;
|
||||||
|
use futures::channel::mpsc::unbounded;
|
||||||
|
use futures::stream::{SplitSink, SplitStream};
|
||||||
|
use futures::{FutureExt as _, SinkExt as _, Stream, StreamExt as _, TryStreamExt as _, pin_mut};
|
||||||
|
use gpui::{App, BackgroundExecutor, Task};
|
||||||
|
use yawc::WebSocket;
|
||||||
|
use yawc::frame::{FrameView, OpCode};
|
||||||
|
|
||||||
|
const KEEPALIVE_INTERVAL: Duration = Duration::from_secs(1);
|
||||||
|
|
||||||
|
pub type MessageStream = Pin<Box<dyn Stream<Item = Result<MessageToClient>>>>;
|
||||||
|
|
||||||
|
pub struct Connection {
|
||||||
|
tx: SplitSink<WebSocket, FrameView>,
|
||||||
|
rx: SplitStream<WebSocket>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Connection {
|
||||||
|
pub fn new(ws: WebSocket) -> Self {
|
||||||
|
let (tx, rx) = ws.split();
|
||||||
|
|
||||||
|
Self { tx, rx }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn spawn(self, cx: &App) -> (MessageStream, Task<()>) {
|
||||||
|
let (mut tx, rx) = (self.tx, self.rx);
|
||||||
|
|
||||||
|
let (message_tx, message_rx) = unbounded();
|
||||||
|
|
||||||
|
let handle_io = |executor: BackgroundExecutor| async move {
|
||||||
|
// Send messages on this frequency so the connection isn't closed.
|
||||||
|
let keepalive_timer = executor.timer(KEEPALIVE_INTERVAL).fuse();
|
||||||
|
futures::pin_mut!(keepalive_timer);
|
||||||
|
|
||||||
|
let rx = rx.fuse();
|
||||||
|
pin_mut!(rx);
|
||||||
|
|
||||||
|
loop {
|
||||||
|
futures::select_biased! {
|
||||||
|
_ = keepalive_timer => {
|
||||||
|
let _ = tx.send(FrameView::ping(Vec::new())).await;
|
||||||
|
|
||||||
|
keepalive_timer.set(executor.timer(KEEPALIVE_INTERVAL).fuse());
|
||||||
|
}
|
||||||
|
frame = rx.next() => {
|
||||||
|
let Some(frame) = frame else {
|
||||||
|
break;
|
||||||
|
};
|
||||||
|
|
||||||
|
match frame.opcode {
|
||||||
|
OpCode::Binary => {
|
||||||
|
let message_result = MessageToClient::deserialize(&frame.payload);
|
||||||
|
message_tx.unbounded_send(message_result).ok();
|
||||||
|
}
|
||||||
|
OpCode::Close => {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let task = cx.spawn(async move |cx| handle_io(cx.background_executor().clone()).await);
|
||||||
|
|
||||||
|
(message_rx.into_stream().boxed(), task)
|
||||||
|
}
|
||||||
|
}
|
24
crates/cloud_api_types/Cargo.toml
Normal file
24
crates/cloud_api_types/Cargo.toml
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
[package]
|
||||||
|
name = "cloud_api_types"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition.workspace = true
|
||||||
|
publish.workspace = true
|
||||||
|
license = "Apache-2.0"
|
||||||
|
|
||||||
|
[lints]
|
||||||
|
workspace = true
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
path = "src/cloud_api_types.rs"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
anyhow.workspace = true
|
||||||
|
chrono.workspace = true
|
||||||
|
ciborium.workspace = true
|
||||||
|
cloud_llm_client.workspace = true
|
||||||
|
serde.workspace = true
|
||||||
|
workspace-hack.workspace = true
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
pretty_assertions.workspace = true
|
||||||
|
serde_json.workspace = true
|
1
crates/cloud_api_types/LICENSE-APACHE
Symbolic link
1
crates/cloud_api_types/LICENSE-APACHE
Symbolic link
|
@ -0,0 +1 @@
|
||||||
|
../../LICENSE-APACHE
|
56
crates/cloud_api_types/src/cloud_api_types.rs
Normal file
56
crates/cloud_api_types/src/cloud_api_types.rs
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
mod timestamp;
|
||||||
|
pub mod websocket_protocol;
|
||||||
|
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
pub use crate::timestamp::Timestamp;
|
||||||
|
|
||||||
|
pub const ZED_SYSTEM_ID_HEADER_NAME: &str = "x-zed-system-id";
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Serialize, Deserialize)]
|
||||||
|
pub struct GetAuthenticatedUserResponse {
|
||||||
|
pub user: AuthenticatedUser,
|
||||||
|
pub feature_flags: Vec<String>,
|
||||||
|
pub plan: PlanInfo,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Serialize, Deserialize)]
|
||||||
|
pub struct AuthenticatedUser {
|
||||||
|
pub id: i32,
|
||||||
|
pub metrics_id: String,
|
||||||
|
pub avatar_url: String,
|
||||||
|
pub github_login: String,
|
||||||
|
pub name: Option<String>,
|
||||||
|
pub is_staff: bool,
|
||||||
|
pub accepted_tos_at: Option<Timestamp>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Serialize, Deserialize)]
|
||||||
|
pub struct PlanInfo {
|
||||||
|
pub plan: cloud_llm_client::Plan,
|
||||||
|
pub subscription_period: Option<SubscriptionPeriod>,
|
||||||
|
pub usage: cloud_llm_client::CurrentUsage,
|
||||||
|
pub trial_started_at: Option<Timestamp>,
|
||||||
|
pub is_usage_based_billing_enabled: bool,
|
||||||
|
pub is_account_too_young: bool,
|
||||||
|
pub has_overdue_invoices: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Clone, Copy, Serialize, Deserialize)]
|
||||||
|
pub struct SubscriptionPeriod {
|
||||||
|
pub started_at: Timestamp,
|
||||||
|
pub ended_at: Timestamp,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Serialize, Deserialize)]
|
||||||
|
pub struct AcceptTermsOfServiceResponse {
|
||||||
|
pub user: AuthenticatedUser,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct LlmToken(pub String);
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct CreateLlmTokenResponse {
|
||||||
|
pub token: LlmToken,
|
||||||
|
}
|
166
crates/cloud_api_types/src/timestamp.rs
Normal file
166
crates/cloud_api_types/src/timestamp.rs
Normal file
|
@ -0,0 +1,166 @@
|
||||||
|
use chrono::{DateTime, NaiveDateTime, SecondsFormat, Utc};
|
||||||
|
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
||||||
|
|
||||||
|
/// A timestamp with a serialized representation in RFC 3339 format.
|
||||||
|
#[derive(Debug, PartialEq, Eq, Hash, Clone, Copy)]
|
||||||
|
pub struct Timestamp(pub DateTime<Utc>);
|
||||||
|
|
||||||
|
impl Timestamp {
|
||||||
|
pub fn new(datetime: DateTime<Utc>) -> Self {
|
||||||
|
Self(datetime)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<DateTime<Utc>> for Timestamp {
|
||||||
|
fn from(value: DateTime<Utc>) -> Self {
|
||||||
|
Self(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<NaiveDateTime> for Timestamp {
|
||||||
|
fn from(value: NaiveDateTime) -> Self {
|
||||||
|
Self(value.and_utc())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Serialize for Timestamp {
|
||||||
|
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||||
|
where
|
||||||
|
S: Serializer,
|
||||||
|
{
|
||||||
|
let rfc3339_string = self.0.to_rfc3339_opts(SecondsFormat::Millis, true);
|
||||||
|
serializer.serialize_str(&rfc3339_string)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'de> Deserialize<'de> for Timestamp {
|
||||||
|
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||||
|
where
|
||||||
|
D: Deserializer<'de>,
|
||||||
|
{
|
||||||
|
let value = String::deserialize(deserializer)?;
|
||||||
|
let datetime = DateTime::parse_from_rfc3339(&value)
|
||||||
|
.map_err(serde::de::Error::custom)?
|
||||||
|
.to_utc();
|
||||||
|
Ok(Self(datetime))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use chrono::NaiveDate;
|
||||||
|
use pretty_assertions::assert_eq;
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_timestamp_serialization() {
|
||||||
|
let datetime = DateTime::parse_from_rfc3339("2023-12-25T14:30:45.123Z")
|
||||||
|
.unwrap()
|
||||||
|
.to_utc();
|
||||||
|
let timestamp = Timestamp::new(datetime);
|
||||||
|
|
||||||
|
let json = serde_json::to_string(×tamp).unwrap();
|
||||||
|
assert_eq!(json, "\"2023-12-25T14:30:45.123Z\"");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_timestamp_deserialization() {
|
||||||
|
let json = "\"2023-12-25T14:30:45.123Z\"";
|
||||||
|
let timestamp: Timestamp = serde_json::from_str(json).unwrap();
|
||||||
|
|
||||||
|
let expected = DateTime::parse_from_rfc3339("2023-12-25T14:30:45.123Z")
|
||||||
|
.unwrap()
|
||||||
|
.to_utc();
|
||||||
|
|
||||||
|
assert_eq!(timestamp.0, expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_timestamp_roundtrip() {
|
||||||
|
let original = DateTime::parse_from_rfc3339("2023-12-25T14:30:45.123Z")
|
||||||
|
.unwrap()
|
||||||
|
.to_utc();
|
||||||
|
|
||||||
|
let timestamp = Timestamp::new(original);
|
||||||
|
let json = serde_json::to_string(×tamp).unwrap();
|
||||||
|
let deserialized: Timestamp = serde_json::from_str(&json).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(deserialized.0, original);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_timestamp_from_datetime_utc() {
|
||||||
|
let datetime = DateTime::parse_from_rfc3339("2023-12-25T14:30:45.123Z")
|
||||||
|
.unwrap()
|
||||||
|
.to_utc();
|
||||||
|
|
||||||
|
let timestamp = Timestamp::from(datetime);
|
||||||
|
assert_eq!(timestamp.0, datetime);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_timestamp_from_naive_datetime() {
|
||||||
|
let naive_dt = NaiveDate::from_ymd_opt(2023, 12, 25)
|
||||||
|
.unwrap()
|
||||||
|
.and_hms_milli_opt(14, 30, 45, 123)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let timestamp = Timestamp::from(naive_dt);
|
||||||
|
let expected = naive_dt.and_utc();
|
||||||
|
|
||||||
|
assert_eq!(timestamp.0, expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_timestamp_serialization_with_microseconds() {
|
||||||
|
// Test that microseconds are truncated to milliseconds
|
||||||
|
let datetime = NaiveDate::from_ymd_opt(2023, 12, 25)
|
||||||
|
.unwrap()
|
||||||
|
.and_hms_micro_opt(14, 30, 45, 123456)
|
||||||
|
.unwrap()
|
||||||
|
.and_utc();
|
||||||
|
|
||||||
|
let timestamp = Timestamp::new(datetime);
|
||||||
|
let json = serde_json::to_string(×tamp).unwrap();
|
||||||
|
|
||||||
|
// Should be truncated to milliseconds
|
||||||
|
assert_eq!(json, "\"2023-12-25T14:30:45.123Z\"");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_timestamp_deserialization_without_milliseconds() {
|
||||||
|
let json = "\"2023-12-25T14:30:45Z\"";
|
||||||
|
let timestamp: Timestamp = serde_json::from_str(json).unwrap();
|
||||||
|
|
||||||
|
let expected = NaiveDate::from_ymd_opt(2023, 12, 25)
|
||||||
|
.unwrap()
|
||||||
|
.and_hms_opt(14, 30, 45)
|
||||||
|
.unwrap()
|
||||||
|
.and_utc();
|
||||||
|
|
||||||
|
assert_eq!(timestamp.0, expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_timestamp_deserialization_with_timezone() {
|
||||||
|
let json = "\"2023-12-25T14:30:45.123+05:30\"";
|
||||||
|
let timestamp: Timestamp = serde_json::from_str(json).unwrap();
|
||||||
|
|
||||||
|
// Should be converted to UTC
|
||||||
|
let expected = NaiveDate::from_ymd_opt(2023, 12, 25)
|
||||||
|
.unwrap()
|
||||||
|
.and_hms_milli_opt(9, 0, 45, 123) // 14:30:45 + 5:30 = 20:00:45, but we want UTC so subtract 5:30
|
||||||
|
.unwrap()
|
||||||
|
.and_utc();
|
||||||
|
|
||||||
|
assert_eq!(timestamp.0, expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_timestamp_deserialization_with_invalid_format() {
|
||||||
|
let json = "\"invalid-date\"";
|
||||||
|
let result: Result<Timestamp, _> = serde_json::from_str(json);
|
||||||
|
assert!(result.is_err());
|
||||||
|
}
|
||||||
|
}
|
28
crates/cloud_api_types/src/websocket_protocol.rs
Normal file
28
crates/cloud_api_types/src/websocket_protocol.rs
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
use anyhow::{Context as _, Result};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
/// The version of the Cloud WebSocket protocol.
|
||||||
|
pub const PROTOCOL_VERSION: u32 = 0;
|
||||||
|
|
||||||
|
/// The name of the header used to indicate the protocol version in use.
|
||||||
|
pub const PROTOCOL_VERSION_HEADER_NAME: &str = "x-zed-protocol-version";
|
||||||
|
|
||||||
|
/// A message from Cloud to the Zed client.
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
pub enum MessageToClient {
|
||||||
|
/// The user was updated and should be refreshed.
|
||||||
|
UserUpdated,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MessageToClient {
|
||||||
|
pub fn serialize(&self) -> Result<Vec<u8>> {
|
||||||
|
let mut buffer = Vec::new();
|
||||||
|
ciborium::into_writer(self, &mut buffer).context("failed to serialize message")?;
|
||||||
|
|
||||||
|
Ok(buffer)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deserialize(data: &[u8]) -> Result<Self> {
|
||||||
|
ciborium::from_reader(data).context("failed to deserialize message")
|
||||||
|
}
|
||||||
|
}
|
|
@ -308,13 +308,13 @@ pub struct GetSubscriptionResponse {
|
||||||
pub usage: Option<CurrentUsage>,
|
pub usage: Option<CurrentUsage>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, PartialEq, Serialize, Deserialize)]
|
||||||
pub struct CurrentUsage {
|
pub struct CurrentUsage {
|
||||||
pub model_requests: UsageData,
|
pub model_requests: UsageData,
|
||||||
pub edit_predictions: UsageData,
|
pub edit_predictions: UsageData,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, PartialEq, Serialize, Deserialize)]
|
||||||
pub struct UsageData {
|
pub struct UsageData {
|
||||||
pub used: u32,
|
pub used: u32,
|
||||||
pub limit: UsageLimit,
|
pub limit: UsageLimit,
|
||||||
|
|
|
@ -1286,7 +1286,7 @@ async fn test_calls_on_multiple_connections(
|
||||||
client_b1.disconnect(&cx_b1.to_async());
|
client_b1.disconnect(&cx_b1.to_async());
|
||||||
executor.advance_clock(RECEIVE_TIMEOUT);
|
executor.advance_clock(RECEIVE_TIMEOUT);
|
||||||
client_b1
|
client_b1
|
||||||
.authenticate_and_connect(false, &cx_b1.to_async())
|
.connect(false, &cx_b1.to_async())
|
||||||
.await
|
.await
|
||||||
.into_response()
|
.into_response()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
@ -1667,7 +1667,7 @@ async fn test_project_reconnect(
|
||||||
// Client A reconnects. Their project is re-shared, and client B re-joins it.
|
// Client A reconnects. Their project is re-shared, and client B re-joins it.
|
||||||
server.allow_connections();
|
server.allow_connections();
|
||||||
client_a
|
client_a
|
||||||
.authenticate_and_connect(false, &cx_a.to_async())
|
.connect(false, &cx_a.to_async())
|
||||||
.await
|
.await
|
||||||
.into_response()
|
.into_response()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
@ -1796,7 +1796,7 @@ async fn test_project_reconnect(
|
||||||
// Client B reconnects. They re-join the room and the remaining shared project.
|
// Client B reconnects. They re-join the room and the remaining shared project.
|
||||||
server.allow_connections();
|
server.allow_connections();
|
||||||
client_b
|
client_b
|
||||||
.authenticate_and_connect(false, &cx_b.to_async())
|
.connect(false, &cx_b.to_async())
|
||||||
.await
|
.await
|
||||||
.into_response()
|
.into_response()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
@ -5738,7 +5738,7 @@ async fn test_contacts(
|
||||||
|
|
||||||
server.allow_connections();
|
server.allow_connections();
|
||||||
client_c
|
client_c
|
||||||
.authenticate_and_connect(false, &cx_c.to_async())
|
.connect(false, &cx_c.to_async())
|
||||||
.await
|
.await
|
||||||
.into_response()
|
.into_response()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
@ -6269,7 +6269,7 @@ async fn test_contact_requests(
|
||||||
client.disconnect(&cx.to_async());
|
client.disconnect(&cx.to_async());
|
||||||
client.clear_contacts(cx).await;
|
client.clear_contacts(cx).await;
|
||||||
client
|
client
|
||||||
.authenticate_and_connect(false, &cx.to_async())
|
.connect(false, &cx.to_async())
|
||||||
.await
|
.await
|
||||||
.into_response()
|
.into_response()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
|
@ -3,6 +3,7 @@ use std::sync::Arc;
|
||||||
use gpui::{BackgroundExecutor, TestAppContext};
|
use gpui::{BackgroundExecutor, TestAppContext};
|
||||||
use notifications::NotificationEvent;
|
use notifications::NotificationEvent;
|
||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
|
use pretty_assertions::assert_eq;
|
||||||
use rpc::{Notification, proto};
|
use rpc::{Notification, proto};
|
||||||
|
|
||||||
use crate::tests::TestServer;
|
use crate::tests::TestServer;
|
||||||
|
@ -17,6 +18,9 @@ async fn test_notifications(
|
||||||
let client_a = server.create_client(cx_a, "user_a").await;
|
let client_a = server.create_client(cx_a, "user_a").await;
|
||||||
let client_b = server.create_client(cx_b, "user_b").await;
|
let client_b = server.create_client(cx_b, "user_b").await;
|
||||||
|
|
||||||
|
// Wait for authentication/connection to Collab to be established.
|
||||||
|
executor.run_until_parked();
|
||||||
|
|
||||||
let notification_events_a = Arc::new(Mutex::new(Vec::new()));
|
let notification_events_a = Arc::new(Mutex::new(Vec::new()));
|
||||||
let notification_events_b = Arc::new(Mutex::new(Vec::new()));
|
let notification_events_b = Arc::new(Mutex::new(Vec::new()));
|
||||||
client_a.notification_store().update(cx_a, |_, cx| {
|
client_a.notification_store().update(cx_a, |_, cx| {
|
||||||
|
|
|
@ -8,6 +8,7 @@ use crate::{
|
||||||
use anyhow::anyhow;
|
use anyhow::anyhow;
|
||||||
use call::ActiveCall;
|
use call::ActiveCall;
|
||||||
use channel::{ChannelBuffer, ChannelStore};
|
use channel::{ChannelBuffer, ChannelStore};
|
||||||
|
use client::test::{make_get_authenticated_user_response, parse_authorization_header};
|
||||||
use client::{
|
use client::{
|
||||||
self, ChannelId, Client, Connection, Credentials, EstablishConnectionError, UserStore,
|
self, ChannelId, Client, Connection, Credentials, EstablishConnectionError, UserStore,
|
||||||
proto::PeerId,
|
proto::PeerId,
|
||||||
|
@ -20,7 +21,7 @@ use fs::FakeFs;
|
||||||
use futures::{StreamExt as _, channel::oneshot};
|
use futures::{StreamExt as _, channel::oneshot};
|
||||||
use git::GitHostingProviderRegistry;
|
use git::GitHostingProviderRegistry;
|
||||||
use gpui::{AppContext as _, BackgroundExecutor, Entity, Task, TestAppContext, VisualTestContext};
|
use gpui::{AppContext as _, BackgroundExecutor, Entity, Task, TestAppContext, VisualTestContext};
|
||||||
use http_client::FakeHttpClient;
|
use http_client::{FakeHttpClient, Method};
|
||||||
use language::LanguageRegistry;
|
use language::LanguageRegistry;
|
||||||
use node_runtime::NodeRuntime;
|
use node_runtime::NodeRuntime;
|
||||||
use notifications::NotificationStore;
|
use notifications::NotificationStore;
|
||||||
|
@ -161,6 +162,8 @@ impl TestServer {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn create_client(&mut self, cx: &mut TestAppContext, name: &str) -> TestClient {
|
pub async fn create_client(&mut self, cx: &mut TestAppContext, name: &str) -> TestClient {
|
||||||
|
const ACCESS_TOKEN: &str = "the-token";
|
||||||
|
|
||||||
let fs = FakeFs::new(cx.executor());
|
let fs = FakeFs::new(cx.executor());
|
||||||
|
|
||||||
cx.update(|cx| {
|
cx.update(|cx| {
|
||||||
|
@ -175,7 +178,7 @@ impl TestServer {
|
||||||
});
|
});
|
||||||
|
|
||||||
let clock = Arc::new(FakeSystemClock::new());
|
let clock = Arc::new(FakeSystemClock::new());
|
||||||
let http = FakeHttpClient::with_404_response();
|
|
||||||
let user_id = if let Ok(Some(user)) = self.app_state.db.get_user_by_github_login(name).await
|
let user_id = if let Ok(Some(user)) = self.app_state.db.get_user_by_github_login(name).await
|
||||||
{
|
{
|
||||||
user.id
|
user.id
|
||||||
|
@ -197,6 +200,47 @@ impl TestServer {
|
||||||
.expect("creating user failed")
|
.expect("creating user failed")
|
||||||
.user_id
|
.user_id
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let http = FakeHttpClient::create({
|
||||||
|
let name = name.to_string();
|
||||||
|
move |req| {
|
||||||
|
let name = name.clone();
|
||||||
|
async move {
|
||||||
|
match (req.method(), req.uri().path()) {
|
||||||
|
(&Method::GET, "/client/users/me") => {
|
||||||
|
let credentials = parse_authorization_header(&req);
|
||||||
|
if credentials
|
||||||
|
!= Some(Credentials {
|
||||||
|
user_id: user_id.to_proto(),
|
||||||
|
access_token: ACCESS_TOKEN.into(),
|
||||||
|
})
|
||||||
|
{
|
||||||
|
return Ok(http_client::Response::builder()
|
||||||
|
.status(401)
|
||||||
|
.body("Unauthorized".into())
|
||||||
|
.unwrap());
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(http_client::Response::builder()
|
||||||
|
.status(200)
|
||||||
|
.body(
|
||||||
|
serde_json::to_string(&make_get_authenticated_user_response(
|
||||||
|
user_id.0, name,
|
||||||
|
))
|
||||||
|
.unwrap()
|
||||||
|
.into(),
|
||||||
|
)
|
||||||
|
.unwrap())
|
||||||
|
}
|
||||||
|
_ => Ok(http_client::Response::builder()
|
||||||
|
.status(404)
|
||||||
|
.body("Not Found".into())
|
||||||
|
.unwrap()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
let client_name = name.to_string();
|
let client_name = name.to_string();
|
||||||
let mut client = cx.update(|cx| Client::new(clock, http.clone(), cx));
|
let mut client = cx.update(|cx| Client::new(clock, http.clone(), cx));
|
||||||
let server = self.server.clone();
|
let server = self.server.clone();
|
||||||
|
@ -208,11 +252,10 @@ impl TestServer {
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.set_id(user_id.to_proto())
|
.set_id(user_id.to_proto())
|
||||||
.override_authenticate(move |cx| {
|
.override_authenticate(move |cx| {
|
||||||
let access_token = "the-token".to_string();
|
|
||||||
cx.spawn(async move |_| {
|
cx.spawn(async move |_| {
|
||||||
Ok(Credentials {
|
Ok(Credentials {
|
||||||
user_id: user_id.to_proto(),
|
user_id: user_id.to_proto(),
|
||||||
access_token,
|
access_token: ACCESS_TOKEN.into(),
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -221,7 +264,7 @@ impl TestServer {
|
||||||
credentials,
|
credentials,
|
||||||
&Credentials {
|
&Credentials {
|
||||||
user_id: user_id.0 as u64,
|
user_id: user_id.0 as u64,
|
||||||
access_token: "the-token".into()
|
access_token: ACCESS_TOKEN.into(),
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -319,7 +362,7 @@ impl TestServer {
|
||||||
});
|
});
|
||||||
|
|
||||||
client
|
client
|
||||||
.authenticate_and_connect(false, &cx.to_async())
|
.connect(false, &cx.to_async())
|
||||||
.await
|
.await
|
||||||
.into_response()
|
.into_response()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
|
@ -2331,7 +2331,7 @@ impl CollabPanel {
|
||||||
let client = this.client.clone();
|
let client = this.client.clone();
|
||||||
cx.spawn_in(window, async move |_, cx| {
|
cx.spawn_in(window, async move |_, cx| {
|
||||||
client
|
client
|
||||||
.authenticate_and_connect(true, &cx)
|
.connect(true, &cx)
|
||||||
.await
|
.await
|
||||||
.into_response()
|
.into_response()
|
||||||
.notify_async_err(cx);
|
.notify_async_err(cx);
|
||||||
|
@ -3061,7 +3061,7 @@ impl Render for CollabPanel {
|
||||||
.on_action(cx.listener(CollabPanel::move_channel_down))
|
.on_action(cx.listener(CollabPanel::move_channel_down))
|
||||||
.track_focus(&self.focus_handle)
|
.track_focus(&self.focus_handle)
|
||||||
.size_full()
|
.size_full()
|
||||||
.child(if self.user_store.read(cx).current_user().is_none() {
|
.child(if !self.client.status().borrow().is_connected() {
|
||||||
self.render_signed_out(cx)
|
self.render_signed_out(cx)
|
||||||
} else {
|
} else {
|
||||||
self.render_signed_in(window, cx)
|
self.render_signed_in(window, cx)
|
||||||
|
|
|
@ -634,13 +634,13 @@ impl Render for NotificationPanel {
|
||||||
.child(Icon::new(IconName::Envelope)),
|
.child(Icon::new(IconName::Envelope)),
|
||||||
)
|
)
|
||||||
.map(|this| {
|
.map(|this| {
|
||||||
if self.client.user_id().is_none() {
|
if !self.client.status().borrow().is_connected() {
|
||||||
this.child(
|
this.child(
|
||||||
v_flex()
|
v_flex()
|
||||||
.gap_2()
|
.gap_2()
|
||||||
.p_4()
|
.p_4()
|
||||||
.child(
|
.child(
|
||||||
Button::new("sign_in_prompt_button", "Sign in")
|
Button::new("connect_prompt_button", "Connect")
|
||||||
.icon_color(Color::Muted)
|
.icon_color(Color::Muted)
|
||||||
.icon(IconName::Github)
|
.icon(IconName::Github)
|
||||||
.icon_position(IconPosition::Start)
|
.icon_position(IconPosition::Start)
|
||||||
|
@ -652,10 +652,7 @@ impl Render for NotificationPanel {
|
||||||
let client = client.clone();
|
let client = client.clone();
|
||||||
window
|
window
|
||||||
.spawn(cx, async move |cx| {
|
.spawn(cx, async move |cx| {
|
||||||
match client
|
match client.connect(true, &cx).await {
|
||||||
.authenticate_and_connect(true, &cx)
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
util::ConnectionResult::Timeout => {
|
util::ConnectionResult::Timeout => {
|
||||||
log::error!("Connection timeout");
|
log::error!("Connection timeout");
|
||||||
}
|
}
|
||||||
|
@ -673,7 +670,7 @@ impl Render for NotificationPanel {
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
div().flex().w_full().items_center().child(
|
div().flex().w_full().items_center().child(
|
||||||
Label::new("Sign in to view notifications.")
|
Label::new("Connect to view notifications.")
|
||||||
.color(Color::Muted)
|
.color(Color::Muted)
|
||||||
.size(LabelSize::Small),
|
.size(LabelSize::Small),
|
||||||
),
|
),
|
||||||
|
|
|
@ -6,7 +6,6 @@ mod sign_in;
|
||||||
use crate::sign_in::initiate_sign_in_within_workspace;
|
use crate::sign_in::initiate_sign_in_within_workspace;
|
||||||
use ::fs::Fs;
|
use ::fs::Fs;
|
||||||
use anyhow::{Context as _, Result, anyhow};
|
use anyhow::{Context as _, Result, anyhow};
|
||||||
use client::DisableAiSettings;
|
|
||||||
use collections::{HashMap, HashSet};
|
use collections::{HashMap, HashSet};
|
||||||
use command_palette_hooks::CommandPaletteFilter;
|
use command_palette_hooks::CommandPaletteFilter;
|
||||||
use futures::{Future, FutureExt, TryFutureExt, channel::oneshot, future::Shared};
|
use futures::{Future, FutureExt, TryFutureExt, channel::oneshot, future::Shared};
|
||||||
|
@ -24,6 +23,7 @@ use language::{
|
||||||
use lsp::{LanguageServer, LanguageServerBinary, LanguageServerId, LanguageServerName};
|
use lsp::{LanguageServer, LanguageServerBinary, LanguageServerId, LanguageServerName};
|
||||||
use node_runtime::NodeRuntime;
|
use node_runtime::NodeRuntime;
|
||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
|
use project::DisableAiSettings;
|
||||||
use request::StatusNotification;
|
use request::StatusNotification;
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
use settings::Settings;
|
use settings::Settings;
|
||||||
|
|
|
@ -295,7 +295,7 @@ mod tests {
|
||||||
request: dap_types::StartDebuggingRequestArgumentsRequest::Launch,
|
request: dap_types::StartDebuggingRequestArgumentsRequest::Launch,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Box::new(|_| panic!("Did not expect to hit this code path")),
|
Box::new(|_| {}),
|
||||||
&mut cx.to_async(),
|
&mut cx.to_async(),
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
|
|
|
@ -883,6 +883,7 @@ impl FakeTransport {
|
||||||
break Err(anyhow!("exit in response to request"));
|
break Err(anyhow!("exit in response to request"));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
let success = response.success;
|
||||||
let message =
|
let message =
|
||||||
serde_json::to_string(&Message::Response(response)).unwrap();
|
serde_json::to_string(&Message::Response(response)).unwrap();
|
||||||
|
|
||||||
|
@ -893,6 +894,25 @@ impl FakeTransport {
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
|
if request.command == dap_types::requests::Initialize::COMMAND
|
||||||
|
&& success
|
||||||
|
{
|
||||||
|
let message = serde_json::to_string(&Message::Event(Box::new(
|
||||||
|
dap_types::messages::Events::Initialized(Some(
|
||||||
|
Default::default(),
|
||||||
|
)),
|
||||||
|
)))
|
||||||
|
.unwrap();
|
||||||
|
writer
|
||||||
|
.write_all(
|
||||||
|
TransportDelegate::build_rpc_message(message)
|
||||||
|
.as_bytes(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
writer.flush().await.unwrap();
|
writer.flush().await.unwrap();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,6 +22,7 @@ test-support = [
|
||||||
"theme/test-support",
|
"theme/test-support",
|
||||||
"util/test-support",
|
"util/test-support",
|
||||||
"workspace/test-support",
|
"workspace/test-support",
|
||||||
|
"tree-sitter-c",
|
||||||
"tree-sitter-rust",
|
"tree-sitter-rust",
|
||||||
"tree-sitter-typescript",
|
"tree-sitter-typescript",
|
||||||
"tree-sitter-html",
|
"tree-sitter-html",
|
||||||
|
@ -76,6 +77,7 @@ telemetry.workspace = true
|
||||||
text.workspace = true
|
text.workspace = true
|
||||||
time.workspace = true
|
time.workspace = true
|
||||||
theme.workspace = true
|
theme.workspace = true
|
||||||
|
tree-sitter-c = { workspace = true, optional = true }
|
||||||
tree-sitter-html = { workspace = true, optional = true }
|
tree-sitter-html = { workspace = true, optional = true }
|
||||||
tree-sitter-rust = { workspace = true, optional = true }
|
tree-sitter-rust = { workspace = true, optional = true }
|
||||||
tree-sitter-typescript = { workspace = true, optional = true }
|
tree-sitter-typescript = { workspace = true, optional = true }
|
||||||
|
@ -106,6 +108,7 @@ settings = { workspace = true, features = ["test-support"] }
|
||||||
tempfile.workspace = true
|
tempfile.workspace = true
|
||||||
text = { workspace = true, features = ["test-support"] }
|
text = { workspace = true, features = ["test-support"] }
|
||||||
theme = { workspace = true, features = ["test-support"] }
|
theme = { workspace = true, features = ["test-support"] }
|
||||||
|
tree-sitter-c.workspace = true
|
||||||
tree-sitter-html.workspace = true
|
tree-sitter-html.workspace = true
|
||||||
tree-sitter-rust.workspace = true
|
tree-sitter-rust.workspace = true
|
||||||
tree-sitter-typescript.workspace = true
|
tree-sitter-typescript.workspace = true
|
||||||
|
|
|
@ -56,7 +56,7 @@ use aho_corasick::AhoCorasick;
|
||||||
use anyhow::{Context as _, Result, anyhow};
|
use anyhow::{Context as _, Result, anyhow};
|
||||||
use blink_manager::BlinkManager;
|
use blink_manager::BlinkManager;
|
||||||
use buffer_diff::DiffHunkStatus;
|
use buffer_diff::DiffHunkStatus;
|
||||||
use client::{Collaborator, DisableAiSettings, ParticipantIndex};
|
use client::{Collaborator, ParticipantIndex};
|
||||||
use clock::{AGENT_REPLICA_ID, ReplicaId};
|
use clock::{AGENT_REPLICA_ID, ReplicaId};
|
||||||
use collections::{BTreeMap, HashMap, HashSet, VecDeque};
|
use collections::{BTreeMap, HashMap, HashSet, VecDeque};
|
||||||
use convert_case::{Case, Casing};
|
use convert_case::{Case, Casing};
|
||||||
|
@ -125,7 +125,7 @@ use markdown::Markdown;
|
||||||
use mouse_context_menu::MouseContextMenu;
|
use mouse_context_menu::MouseContextMenu;
|
||||||
use persistence::DB;
|
use persistence::DB;
|
||||||
use project::{
|
use project::{
|
||||||
BreakpointWithPosition, CompletionResponse, ProjectPath,
|
BreakpointWithPosition, CompletionResponse, DisableAiSettings, ProjectPath,
|
||||||
debugger::{
|
debugger::{
|
||||||
breakpoint_store::{
|
breakpoint_store::{
|
||||||
BreakpointEditAction, BreakpointSessionState, BreakpointState, BreakpointStore,
|
BreakpointEditAction, BreakpointSessionState, BreakpointState, BreakpointStore,
|
||||||
|
@ -1305,6 +1305,7 @@ impl Default for SelectionHistoryMode {
|
||||||
///
|
///
|
||||||
/// Similarly, you might want to disable scrolling if you don't want the viewport to
|
/// Similarly, you might want to disable scrolling if you don't want the viewport to
|
||||||
/// move.
|
/// move.
|
||||||
|
#[derive(Clone)]
|
||||||
pub struct SelectionEffects {
|
pub struct SelectionEffects {
|
||||||
nav_history: Option<bool>,
|
nav_history: Option<bool>,
|
||||||
completions: bool,
|
completions: bool,
|
||||||
|
@ -2944,10 +2945,12 @@ impl Editor {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let selection_anchors = self.selections.disjoint_anchors();
|
||||||
|
|
||||||
if self.focus_handle.is_focused(window) && self.leader_id.is_none() {
|
if self.focus_handle.is_focused(window) && self.leader_id.is_none() {
|
||||||
self.buffer.update(cx, |buffer, cx| {
|
self.buffer.update(cx, |buffer, cx| {
|
||||||
buffer.set_active_selections(
|
buffer.set_active_selections(
|
||||||
&self.selections.disjoint_anchors(),
|
&selection_anchors,
|
||||||
self.selections.line_mode,
|
self.selections.line_mode,
|
||||||
self.cursor_shape,
|
self.cursor_shape,
|
||||||
cx,
|
cx,
|
||||||
|
@ -2964,9 +2967,8 @@ impl Editor {
|
||||||
self.select_next_state = None;
|
self.select_next_state = None;
|
||||||
self.select_prev_state = None;
|
self.select_prev_state = None;
|
||||||
self.select_syntax_node_history.try_clear();
|
self.select_syntax_node_history.try_clear();
|
||||||
self.invalidate_autoclose_regions(&self.selections.disjoint_anchors(), buffer);
|
self.invalidate_autoclose_regions(&selection_anchors, buffer);
|
||||||
self.snippet_stack
|
self.snippet_stack.invalidate(&selection_anchors, buffer);
|
||||||
.invalidate(&self.selections.disjoint_anchors(), buffer);
|
|
||||||
self.take_rename(false, window, cx);
|
self.take_rename(false, window, cx);
|
||||||
|
|
||||||
let newest_selection = self.selections.newest_anchor();
|
let newest_selection = self.selections.newest_anchor();
|
||||||
|
@ -4047,7 +4049,8 @@ impl Editor {
|
||||||
// then don't insert that closing bracket again; just move the selection
|
// then don't insert that closing bracket again; just move the selection
|
||||||
// past the closing bracket.
|
// past the closing bracket.
|
||||||
let should_skip = selection.end == region.range.end.to_point(&snapshot)
|
let should_skip = selection.end == region.range.end.to_point(&snapshot)
|
||||||
&& text.as_ref() == region.pair.end.as_str();
|
&& text.as_ref() == region.pair.end.as_str()
|
||||||
|
&& snapshot.contains_str_at(region.range.end, text.as_ref());
|
||||||
if should_skip {
|
if should_skip {
|
||||||
let anchor = snapshot.anchor_after(selection.end);
|
let anchor = snapshot.anchor_after(selection.end);
|
||||||
new_selections
|
new_selections
|
||||||
|
@ -4973,13 +4976,17 @@ impl Editor {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Remove any autoclose regions that no longer contain their selection.
|
/// Remove any autoclose regions that no longer contain their selection or have invalid anchors in ranges.
|
||||||
fn invalidate_autoclose_regions(
|
fn invalidate_autoclose_regions(
|
||||||
&mut self,
|
&mut self,
|
||||||
mut selections: &[Selection<Anchor>],
|
mut selections: &[Selection<Anchor>],
|
||||||
buffer: &MultiBufferSnapshot,
|
buffer: &MultiBufferSnapshot,
|
||||||
) {
|
) {
|
||||||
self.autoclose_regions.retain(|state| {
|
self.autoclose_regions.retain(|state| {
|
||||||
|
if !state.range.start.is_valid(buffer) || !state.range.end.is_valid(buffer) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
let mut i = 0;
|
let mut i = 0;
|
||||||
while let Some(selection) = selections.get(i) {
|
while let Some(selection) = selections.get(i) {
|
||||||
if selection.end.cmp(&state.range.start, buffer).is_lt() {
|
if selection.end.cmp(&state.range.start, buffer).is_lt() {
|
||||||
|
@ -5891,18 +5898,20 @@ impl Editor {
|
||||||
text: new_text[common_prefix_len..].into(),
|
text: new_text[common_prefix_len..].into(),
|
||||||
});
|
});
|
||||||
|
|
||||||
self.transact(window, cx, |this, window, cx| {
|
self.transact(window, cx, |editor, window, cx| {
|
||||||
if let Some(mut snippet) = snippet {
|
if let Some(mut snippet) = snippet {
|
||||||
snippet.text = new_text.to_string();
|
snippet.text = new_text.to_string();
|
||||||
this.insert_snippet(&ranges, snippet, window, cx).log_err();
|
editor
|
||||||
|
.insert_snippet(&ranges, snippet, window, cx)
|
||||||
|
.log_err();
|
||||||
} else {
|
} else {
|
||||||
this.buffer.update(cx, |buffer, cx| {
|
editor.buffer.update(cx, |multi_buffer, cx| {
|
||||||
let auto_indent = match completion.insert_text_mode {
|
let auto_indent = match completion.insert_text_mode {
|
||||||
Some(InsertTextMode::AS_IS) => None,
|
Some(InsertTextMode::AS_IS) => None,
|
||||||
_ => this.autoindent_mode.clone(),
|
_ => editor.autoindent_mode.clone(),
|
||||||
};
|
};
|
||||||
let edits = ranges.into_iter().map(|range| (range, new_text.as_str()));
|
let edits = ranges.into_iter().map(|range| (range, new_text.as_str()));
|
||||||
buffer.edit(edits, auto_indent, cx);
|
multi_buffer.edit(edits, auto_indent, cx);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
for (buffer, edits) in linked_edits {
|
for (buffer, edits) in linked_edits {
|
||||||
|
@ -5921,8 +5930,9 @@ impl Editor {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
this.refresh_inline_completion(true, false, window, cx);
|
editor.refresh_inline_completion(true, false, window, cx);
|
||||||
});
|
});
|
||||||
|
self.invalidate_autoclose_regions(&self.selections.disjoint_anchors(), &snapshot);
|
||||||
|
|
||||||
let show_new_completions_on_confirm = completion
|
let show_new_completions_on_confirm = completion
|
||||||
.confirm
|
.confirm
|
||||||
|
@ -6993,6 +7003,10 @@ impl Editor {
|
||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) -> Option<()> {
|
) -> Option<()> {
|
||||||
|
if DisableAiSettings::get_global(cx).disable_ai {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
let provider = self.edit_prediction_provider()?;
|
let provider = self.edit_prediction_provider()?;
|
||||||
let cursor = self.selections.newest_anchor().head();
|
let cursor = self.selections.newest_anchor().head();
|
||||||
let (buffer, cursor_buffer_position) =
|
let (buffer, cursor_buffer_position) =
|
||||||
|
@ -7050,6 +7064,7 @@ impl Editor {
|
||||||
pub fn update_edit_prediction_settings(&mut self, cx: &mut Context<Self>) {
|
pub fn update_edit_prediction_settings(&mut self, cx: &mut Context<Self>) {
|
||||||
if self.edit_prediction_provider.is_none() || DisableAiSettings::get_global(cx).disable_ai {
|
if self.edit_prediction_provider.is_none() || DisableAiSettings::get_global(cx).disable_ai {
|
||||||
self.edit_prediction_settings = EditPredictionSettings::Disabled;
|
self.edit_prediction_settings = EditPredictionSettings::Disabled;
|
||||||
|
self.discard_inline_completion(false, cx);
|
||||||
} else {
|
} else {
|
||||||
let selection = self.selections.newest_anchor();
|
let selection = self.selections.newest_anchor();
|
||||||
let cursor = selection.head();
|
let cursor = selection.head();
|
||||||
|
@ -7667,6 +7682,10 @@ impl Editor {
|
||||||
_window: &mut Window,
|
_window: &mut Window,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) -> Option<()> {
|
) -> Option<()> {
|
||||||
|
if DisableAiSettings::get_global(cx).disable_ai {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
let selection = self.selections.newest_anchor();
|
let selection = self.selections.newest_anchor();
|
||||||
let cursor = selection.head();
|
let cursor = selection.head();
|
||||||
let multibuffer = self.buffer.read(cx).snapshot(cx);
|
let multibuffer = self.buffer.read(cx).snapshot(cx);
|
||||||
|
@ -9562,27 +9581,46 @@ impl Editor {
|
||||||
// Check whether the just-entered snippet ends with an auto-closable bracket.
|
// Check whether the just-entered snippet ends with an auto-closable bracket.
|
||||||
if self.autoclose_regions.is_empty() {
|
if self.autoclose_regions.is_empty() {
|
||||||
let snapshot = self.buffer.read(cx).snapshot(cx);
|
let snapshot = self.buffer.read(cx).snapshot(cx);
|
||||||
for selection in &mut self.selections.all::<Point>(cx) {
|
let mut all_selections = self.selections.all::<Point>(cx);
|
||||||
|
for selection in &mut all_selections {
|
||||||
let selection_head = selection.head();
|
let selection_head = selection.head();
|
||||||
let Some(scope) = snapshot.language_scope_at(selection_head) else {
|
let Some(scope) = snapshot.language_scope_at(selection_head) else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut bracket_pair = None;
|
let mut bracket_pair = None;
|
||||||
let next_chars = snapshot.chars_at(selection_head).collect::<String>();
|
let max_lookup_length = scope
|
||||||
let prev_chars = snapshot
|
.brackets()
|
||||||
.reversed_chars_at(selection_head)
|
.map(|(pair, _)| {
|
||||||
.collect::<String>();
|
pair.start
|
||||||
for (pair, enabled) in scope.brackets() {
|
.as_str()
|
||||||
if enabled
|
.chars()
|
||||||
&& pair.close
|
.count()
|
||||||
&& prev_chars.starts_with(pair.start.as_str())
|
.max(pair.end.as_str().chars().count())
|
||||||
&& next_chars.starts_with(pair.end.as_str())
|
})
|
||||||
{
|
.max();
|
||||||
bracket_pair = Some(pair.clone());
|
if let Some(max_lookup_length) = max_lookup_length {
|
||||||
break;
|
let next_text = snapshot
|
||||||
|
.chars_at(selection_head)
|
||||||
|
.take(max_lookup_length)
|
||||||
|
.collect::<String>();
|
||||||
|
let prev_text = snapshot
|
||||||
|
.reversed_chars_at(selection_head)
|
||||||
|
.take(max_lookup_length)
|
||||||
|
.collect::<String>();
|
||||||
|
|
||||||
|
for (pair, enabled) in scope.brackets() {
|
||||||
|
if enabled
|
||||||
|
&& pair.close
|
||||||
|
&& prev_text.starts_with(pair.start.as_str())
|
||||||
|
&& next_text.starts_with(pair.end.as_str())
|
||||||
|
{
|
||||||
|
bracket_pair = Some(pair.clone());
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(pair) = bracket_pair {
|
if let Some(pair) = bracket_pair {
|
||||||
let snapshot_settings = snapshot.language_settings_at(selection_head, cx);
|
let snapshot_settings = snapshot.language_settings_at(selection_head, cx);
|
||||||
let autoclose_enabled =
|
let autoclose_enabled =
|
||||||
|
|
|
@ -13400,6 +13400,178 @@ async fn test_as_is_completions(cx: &mut TestAppContext) {
|
||||||
cx.assert_editor_state("fn a() {}\n unsafeˇ");
|
cx.assert_editor_state("fn a() {}\n unsafeˇ");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
async fn test_panic_during_c_completions(cx: &mut TestAppContext) {
|
||||||
|
init_test(cx, |_| {});
|
||||||
|
let language =
|
||||||
|
Arc::try_unwrap(languages::language("c", tree_sitter_c::LANGUAGE.into())).unwrap();
|
||||||
|
let mut cx = EditorLspTestContext::new(
|
||||||
|
language,
|
||||||
|
lsp::ServerCapabilities {
|
||||||
|
completion_provider: Some(lsp::CompletionOptions {
|
||||||
|
..lsp::CompletionOptions::default()
|
||||||
|
}),
|
||||||
|
..lsp::ServerCapabilities::default()
|
||||||
|
},
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
cx.set_state(
|
||||||
|
"#ifndef BAR_H
|
||||||
|
#define BAR_H
|
||||||
|
|
||||||
|
#include <stdbool.h>
|
||||||
|
|
||||||
|
int fn_branch(bool do_branch1, bool do_branch2);
|
||||||
|
|
||||||
|
#endif // BAR_H
|
||||||
|
ˇ",
|
||||||
|
);
|
||||||
|
cx.executor().run_until_parked();
|
||||||
|
cx.update_editor(|editor, window, cx| {
|
||||||
|
editor.handle_input("#", window, cx);
|
||||||
|
});
|
||||||
|
cx.executor().run_until_parked();
|
||||||
|
cx.update_editor(|editor, window, cx| {
|
||||||
|
editor.handle_input("i", window, cx);
|
||||||
|
});
|
||||||
|
cx.executor().run_until_parked();
|
||||||
|
cx.update_editor(|editor, window, cx| {
|
||||||
|
editor.handle_input("n", window, cx);
|
||||||
|
});
|
||||||
|
cx.executor().run_until_parked();
|
||||||
|
cx.assert_editor_state(
|
||||||
|
"#ifndef BAR_H
|
||||||
|
#define BAR_H
|
||||||
|
|
||||||
|
#include <stdbool.h>
|
||||||
|
|
||||||
|
int fn_branch(bool do_branch1, bool do_branch2);
|
||||||
|
|
||||||
|
#endif // BAR_H
|
||||||
|
#inˇ",
|
||||||
|
);
|
||||||
|
|
||||||
|
cx.lsp
|
||||||
|
.set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
|
||||||
|
Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
|
||||||
|
is_incomplete: false,
|
||||||
|
item_defaults: None,
|
||||||
|
items: vec![lsp::CompletionItem {
|
||||||
|
kind: Some(lsp::CompletionItemKind::SNIPPET),
|
||||||
|
label_details: Some(lsp::CompletionItemLabelDetails {
|
||||||
|
detail: Some("header".to_string()),
|
||||||
|
description: None,
|
||||||
|
}),
|
||||||
|
label: " include".to_string(),
|
||||||
|
text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
|
||||||
|
range: lsp::Range {
|
||||||
|
start: lsp::Position {
|
||||||
|
line: 8,
|
||||||
|
character: 1,
|
||||||
|
},
|
||||||
|
end: lsp::Position {
|
||||||
|
line: 8,
|
||||||
|
character: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
new_text: "include \"$0\"".to_string(),
|
||||||
|
})),
|
||||||
|
sort_text: Some("40b67681include".to_string()),
|
||||||
|
insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
|
||||||
|
filter_text: Some("include".to_string()),
|
||||||
|
insert_text: Some("include \"$0\"".to_string()),
|
||||||
|
..lsp::CompletionItem::default()
|
||||||
|
}],
|
||||||
|
})))
|
||||||
|
});
|
||||||
|
cx.update_editor(|editor, window, cx| {
|
||||||
|
editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
|
||||||
|
});
|
||||||
|
cx.executor().run_until_parked();
|
||||||
|
cx.update_editor(|editor, window, cx| {
|
||||||
|
editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
|
||||||
|
});
|
||||||
|
cx.executor().run_until_parked();
|
||||||
|
cx.assert_editor_state(
|
||||||
|
"#ifndef BAR_H
|
||||||
|
#define BAR_H
|
||||||
|
|
||||||
|
#include <stdbool.h>
|
||||||
|
|
||||||
|
int fn_branch(bool do_branch1, bool do_branch2);
|
||||||
|
|
||||||
|
#endif // BAR_H
|
||||||
|
#include \"ˇ\"",
|
||||||
|
);
|
||||||
|
|
||||||
|
cx.lsp
|
||||||
|
.set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
|
||||||
|
Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
|
||||||
|
is_incomplete: true,
|
||||||
|
item_defaults: None,
|
||||||
|
items: vec![lsp::CompletionItem {
|
||||||
|
kind: Some(lsp::CompletionItemKind::FILE),
|
||||||
|
label: "AGL/".to_string(),
|
||||||
|
text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
|
||||||
|
range: lsp::Range {
|
||||||
|
start: lsp::Position {
|
||||||
|
line: 8,
|
||||||
|
character: 10,
|
||||||
|
},
|
||||||
|
end: lsp::Position {
|
||||||
|
line: 8,
|
||||||
|
character: 11,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
new_text: "AGL/".to_string(),
|
||||||
|
})),
|
||||||
|
sort_text: Some("40b67681AGL/".to_string()),
|
||||||
|
insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
|
||||||
|
filter_text: Some("AGL/".to_string()),
|
||||||
|
insert_text: Some("AGL/".to_string()),
|
||||||
|
..lsp::CompletionItem::default()
|
||||||
|
}],
|
||||||
|
})))
|
||||||
|
});
|
||||||
|
cx.update_editor(|editor, window, cx| {
|
||||||
|
editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
|
||||||
|
});
|
||||||
|
cx.executor().run_until_parked();
|
||||||
|
cx.update_editor(|editor, window, cx| {
|
||||||
|
editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
|
||||||
|
});
|
||||||
|
cx.executor().run_until_parked();
|
||||||
|
cx.assert_editor_state(
|
||||||
|
r##"#ifndef BAR_H
|
||||||
|
#define BAR_H
|
||||||
|
|
||||||
|
#include <stdbool.h>
|
||||||
|
|
||||||
|
int fn_branch(bool do_branch1, bool do_branch2);
|
||||||
|
|
||||||
|
#endif // BAR_H
|
||||||
|
#include "AGL/ˇ"##,
|
||||||
|
);
|
||||||
|
|
||||||
|
cx.update_editor(|editor, window, cx| {
|
||||||
|
editor.handle_input("\"", window, cx);
|
||||||
|
});
|
||||||
|
cx.executor().run_until_parked();
|
||||||
|
cx.assert_editor_state(
|
||||||
|
r##"#ifndef BAR_H
|
||||||
|
#define BAR_H
|
||||||
|
|
||||||
|
#include <stdbool.h>
|
||||||
|
|
||||||
|
int fn_branch(bool do_branch1, bool do_branch2);
|
||||||
|
|
||||||
|
#endif // BAR_H
|
||||||
|
#include "AGL/"ˇ"##,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
async fn test_no_duplicated_completion_requests(cx: &mut TestAppContext) {
|
async fn test_no_duplicated_completion_requests(cx: &mut TestAppContext) {
|
||||||
init_test(cx, |_| {});
|
init_test(cx, |_| {});
|
||||||
|
|
|
@ -8024,12 +8024,20 @@ impl Element for EditorElement {
|
||||||
autoscroll_containing_element,
|
autoscroll_containing_element,
|
||||||
needs_horizontal_autoscroll,
|
needs_horizontal_autoscroll,
|
||||||
) = self.editor.update(cx, |editor, cx| {
|
) = self.editor.update(cx, |editor, cx| {
|
||||||
let autoscroll_request = editor.autoscroll_request();
|
let autoscroll_request = editor.scroll_manager.take_autoscroll_request();
|
||||||
|
|
||||||
let autoscroll_containing_element =
|
let autoscroll_containing_element =
|
||||||
autoscroll_request.is_some() || editor.has_pending_selection();
|
autoscroll_request.is_some() || editor.has_pending_selection();
|
||||||
|
|
||||||
let (needs_horizontal_autoscroll, was_scrolled) = editor
|
let (needs_horizontal_autoscroll, was_scrolled) = editor
|
||||||
.autoscroll_vertically(bounds, line_height, max_scroll_top, window, cx);
|
.autoscroll_vertically(
|
||||||
|
bounds,
|
||||||
|
line_height,
|
||||||
|
max_scroll_top,
|
||||||
|
autoscroll_request,
|
||||||
|
window,
|
||||||
|
cx,
|
||||||
|
);
|
||||||
if was_scrolled.0 {
|
if was_scrolled.0 {
|
||||||
snapshot = editor.snapshot(window, cx);
|
snapshot = editor.snapshot(window, cx);
|
||||||
}
|
}
|
||||||
|
@ -8419,7 +8427,11 @@ impl Element for EditorElement {
|
||||||
Ok(blocks) => blocks,
|
Ok(blocks) => blocks,
|
||||||
Err(resized_blocks) => {
|
Err(resized_blocks) => {
|
||||||
self.editor.update(cx, |editor, cx| {
|
self.editor.update(cx, |editor, cx| {
|
||||||
editor.resize_blocks(resized_blocks, autoscroll_request, cx)
|
editor.resize_blocks(
|
||||||
|
resized_blocks,
|
||||||
|
autoscroll_request.map(|(autoscroll, _)| autoscroll),
|
||||||
|
cx,
|
||||||
|
)
|
||||||
});
|
});
|
||||||
return self.prepaint(None, _inspector_id, bounds, &mut (), window, cx);
|
return self.prepaint(None, _inspector_id, bounds, &mut (), window, cx);
|
||||||
}
|
}
|
||||||
|
@ -8464,6 +8476,7 @@ impl Element for EditorElement {
|
||||||
scroll_width,
|
scroll_width,
|
||||||
em_advance,
|
em_advance,
|
||||||
&line_layouts,
|
&line_layouts,
|
||||||
|
autoscroll_request,
|
||||||
window,
|
window,
|
||||||
cx,
|
cx,
|
||||||
)
|
)
|
||||||
|
|
|
@ -348,8 +348,8 @@ impl ScrollManager {
|
||||||
self.show_scrollbars
|
self.show_scrollbars
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn autoscroll_request(&self) -> Option<Autoscroll> {
|
pub fn take_autoscroll_request(&mut self) -> Option<(Autoscroll, bool)> {
|
||||||
self.autoscroll_request.map(|(autoscroll, _)| autoscroll)
|
self.autoscroll_request.take()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn active_scrollbar_state(&self) -> Option<&ActiveScrollbarState> {
|
pub fn active_scrollbar_state(&self) -> Option<&ActiveScrollbarState> {
|
||||||
|
|
|
@ -102,15 +102,12 @@ impl AutoscrollStrategy {
|
||||||
pub(crate) struct NeedsHorizontalAutoscroll(pub(crate) bool);
|
pub(crate) struct NeedsHorizontalAutoscroll(pub(crate) bool);
|
||||||
|
|
||||||
impl Editor {
|
impl Editor {
|
||||||
pub fn autoscroll_request(&self) -> Option<Autoscroll> {
|
|
||||||
self.scroll_manager.autoscroll_request()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn autoscroll_vertically(
|
pub(crate) fn autoscroll_vertically(
|
||||||
&mut self,
|
&mut self,
|
||||||
bounds: Bounds<Pixels>,
|
bounds: Bounds<Pixels>,
|
||||||
line_height: Pixels,
|
line_height: Pixels,
|
||||||
max_scroll_top: f32,
|
max_scroll_top: f32,
|
||||||
|
autoscroll_request: Option<(Autoscroll, bool)>,
|
||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut Context<Editor>,
|
cx: &mut Context<Editor>,
|
||||||
) -> (NeedsHorizontalAutoscroll, WasScrolled) {
|
) -> (NeedsHorizontalAutoscroll, WasScrolled) {
|
||||||
|
@ -137,7 +134,7 @@ impl Editor {
|
||||||
WasScrolled(false)
|
WasScrolled(false)
|
||||||
};
|
};
|
||||||
|
|
||||||
let Some((autoscroll, local)) = self.scroll_manager.autoscroll_request.take() else {
|
let Some((autoscroll, local)) = autoscroll_request else {
|
||||||
return (NeedsHorizontalAutoscroll(false), editor_was_scrolled);
|
return (NeedsHorizontalAutoscroll(false), editor_was_scrolled);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -284,9 +281,12 @@ impl Editor {
|
||||||
scroll_width: Pixels,
|
scroll_width: Pixels,
|
||||||
em_advance: Pixels,
|
em_advance: Pixels,
|
||||||
layouts: &[LineWithInvisibles],
|
layouts: &[LineWithInvisibles],
|
||||||
|
autoscroll_request: Option<(Autoscroll, bool)>,
|
||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) -> Option<gpui::Point<f32>> {
|
) -> Option<gpui::Point<f32>> {
|
||||||
|
let (_, local) = autoscroll_request?;
|
||||||
|
|
||||||
let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
|
let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
|
||||||
let selections = self.selections.all::<Point>(cx);
|
let selections = self.selections.all::<Point>(cx);
|
||||||
let mut scroll_position = self.scroll_manager.scroll_position(&display_map);
|
let mut scroll_position = self.scroll_manager.scroll_position(&display_map);
|
||||||
|
@ -335,10 +335,10 @@ impl Editor {
|
||||||
|
|
||||||
let was_scrolled = if target_left < scroll_left {
|
let was_scrolled = if target_left < scroll_left {
|
||||||
scroll_position.x = target_left / em_advance;
|
scroll_position.x = target_left / em_advance;
|
||||||
self.set_scroll_position_internal(scroll_position, true, true, window, cx)
|
self.set_scroll_position_internal(scroll_position, local, true, window, cx)
|
||||||
} else if target_right > scroll_right {
|
} else if target_right > scroll_right {
|
||||||
scroll_position.x = (target_right - viewport_width) / em_advance;
|
scroll_position.x = (target_right - viewport_width) / em_advance;
|
||||||
self.set_scroll_position_internal(scroll_position, true, true, window, cx)
|
self.set_scroll_position_internal(scroll_position, local, true, window, cx)
|
||||||
} else {
|
} else {
|
||||||
WasScrolled(false)
|
WasScrolled(false)
|
||||||
};
|
};
|
||||||
|
|
|
@ -158,6 +158,11 @@ where
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct OnFlagsReady {
|
||||||
|
pub is_staff: bool,
|
||||||
|
}
|
||||||
|
|
||||||
pub trait FeatureFlagAppExt {
|
pub trait FeatureFlagAppExt {
|
||||||
fn wait_for_flag<T: FeatureFlag>(&mut self) -> WaitForFlag;
|
fn wait_for_flag<T: FeatureFlag>(&mut self) -> WaitForFlag;
|
||||||
|
|
||||||
|
@ -169,6 +174,10 @@ pub trait FeatureFlagAppExt {
|
||||||
fn has_flag<T: FeatureFlag>(&self) -> bool;
|
fn has_flag<T: FeatureFlag>(&self) -> bool;
|
||||||
fn is_staff(&self) -> bool;
|
fn is_staff(&self) -> bool;
|
||||||
|
|
||||||
|
fn on_flags_ready<F>(&mut self, callback: F) -> Subscription
|
||||||
|
where
|
||||||
|
F: FnMut(OnFlagsReady, &mut App) + 'static;
|
||||||
|
|
||||||
fn observe_flag<T: FeatureFlag, F>(&mut self, callback: F) -> Subscription
|
fn observe_flag<T: FeatureFlag, F>(&mut self, callback: F) -> Subscription
|
||||||
where
|
where
|
||||||
F: FnMut(bool, &mut App) + 'static;
|
F: FnMut(bool, &mut App) + 'static;
|
||||||
|
@ -198,6 +207,21 @@ impl FeatureFlagAppExt for App {
|
||||||
.unwrap_or(false)
|
.unwrap_or(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn on_flags_ready<F>(&mut self, mut callback: F) -> Subscription
|
||||||
|
where
|
||||||
|
F: FnMut(OnFlagsReady, &mut App) + 'static,
|
||||||
|
{
|
||||||
|
self.observe_global::<FeatureFlags>(move |cx| {
|
||||||
|
let feature_flags = cx.global::<FeatureFlags>();
|
||||||
|
callback(
|
||||||
|
OnFlagsReady {
|
||||||
|
is_staff: feature_flags.staff,
|
||||||
|
},
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
fn observe_flag<T: FeatureFlag, F>(&mut self, mut callback: F) -> Subscription
|
fn observe_flag<T: FeatureFlag, F>(&mut self, mut callback: F) -> Subscription
|
||||||
where
|
where
|
||||||
F: FnMut(bool, &mut App) + 'static,
|
F: FnMut(bool, &mut App) + 'static,
|
||||||
|
|
|
@ -23,7 +23,6 @@ askpass.workspace = true
|
||||||
buffer_diff.workspace = true
|
buffer_diff.workspace = true
|
||||||
call.workspace = true
|
call.workspace = true
|
||||||
chrono.workspace = true
|
chrono.workspace = true
|
||||||
client.workspace = true
|
|
||||||
cloud_llm_client.workspace = true
|
cloud_llm_client.workspace = true
|
||||||
collections.workspace = true
|
collections.workspace = true
|
||||||
command_palette_hooks.workspace = true
|
command_palette_hooks.workspace = true
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
use crate::branch_picker::{self, BranchList};
|
use crate::branch_picker::{self, BranchList};
|
||||||
use crate::git_panel::{GitPanel, commit_message_editor};
|
use crate::git_panel::{GitPanel, commit_message_editor};
|
||||||
use client::DisableAiSettings;
|
|
||||||
use git::repository::CommitOptions;
|
use git::repository::CommitOptions;
|
||||||
use git::{Amend, Commit, GenerateCommitMessage, Signoff};
|
use git::{Amend, Commit, GenerateCommitMessage, Signoff};
|
||||||
use panel::{panel_button, panel_editor_style};
|
use panel::{panel_button, panel_editor_style};
|
||||||
|
use project::DisableAiSettings;
|
||||||
use settings::Settings;
|
use settings::Settings;
|
||||||
use ui::{
|
use ui::{
|
||||||
ContextMenu, KeybindingHint, PopoverMenu, PopoverMenuHandle, SplitButton, Tooltip, prelude::*,
|
ContextMenu, KeybindingHint, PopoverMenu, PopoverMenuHandle, SplitButton, Tooltip, prelude::*,
|
||||||
|
|
|
@ -12,7 +12,6 @@ use crate::{
|
||||||
use agent_settings::AgentSettings;
|
use agent_settings::AgentSettings;
|
||||||
use anyhow::Context as _;
|
use anyhow::Context as _;
|
||||||
use askpass::AskPassDelegate;
|
use askpass::AskPassDelegate;
|
||||||
use client::DisableAiSettings;
|
|
||||||
use db::kvp::KEY_VALUE_STORE;
|
use db::kvp::KEY_VALUE_STORE;
|
||||||
use editor::{
|
use editor::{
|
||||||
Editor, EditorElement, EditorMode, EditorSettings, MultiBuffer, ShowScrollbar,
|
Editor, EditorElement, EditorMode, EditorSettings, MultiBuffer, ShowScrollbar,
|
||||||
|
@ -51,10 +50,9 @@ use panel::{
|
||||||
PanelHeader, panel_button, panel_editor_container, panel_editor_style, panel_filled_button,
|
PanelHeader, panel_button, panel_editor_container, panel_editor_style, panel_filled_button,
|
||||||
panel_icon_button,
|
panel_icon_button,
|
||||||
};
|
};
|
||||||
use project::git_store::{RepositoryEvent, RepositoryId};
|
|
||||||
use project::{
|
use project::{
|
||||||
Fs, Project, ProjectPath,
|
DisableAiSettings, Fs, Project, ProjectPath,
|
||||||
git_store::{GitStoreEvent, Repository},
|
git_store::{GitStoreEvent, Repository, RepositoryEvent, RepositoryId},
|
||||||
};
|
};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use settings::{Settings, SettingsStore};
|
use settings::{Settings, SettingsStore};
|
||||||
|
@ -5115,7 +5113,6 @@ mod tests {
|
||||||
language::init(cx);
|
language::init(cx);
|
||||||
editor::init(cx);
|
editor::init(cx);
|
||||||
Project::init_settings(cx);
|
Project::init_settings(cx);
|
||||||
client::DisableAiSettings::register(cx);
|
|
||||||
crate::init(cx);
|
crate::init(cx);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -606,7 +606,7 @@ impl BladeRenderer {
|
||||||
xy_position: v.xy_position,
|
xy_position: v.xy_position,
|
||||||
st_position: v.st_position,
|
st_position: v.st_position,
|
||||||
color: path.color,
|
color: path.color,
|
||||||
bounds: path.bounds.intersect(&path.content_mask.bounds),
|
bounds: path.clipped_bounds(),
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
let vertex_buf = unsafe { self.instance_belt.alloc_typed(&vertices, &self.gpu) };
|
let vertex_buf = unsafe { self.instance_belt.alloc_typed(&vertices, &self.gpu) };
|
||||||
|
@ -735,13 +735,13 @@ impl BladeRenderer {
|
||||||
paths
|
paths
|
||||||
.iter()
|
.iter()
|
||||||
.map(|path| PathSprite {
|
.map(|path| PathSprite {
|
||||||
bounds: path.bounds,
|
bounds: path.clipped_bounds(),
|
||||||
})
|
})
|
||||||
.collect()
|
.collect()
|
||||||
} else {
|
} else {
|
||||||
let mut bounds = first_path.bounds;
|
let mut bounds = first_path.clipped_bounds();
|
||||||
for path in paths.iter().skip(1) {
|
for path in paths.iter().skip(1) {
|
||||||
bounds = bounds.union(&path.bounds);
|
bounds = bounds.union(&path.clipped_bounds());
|
||||||
}
|
}
|
||||||
vec![PathSprite { bounds }]
|
vec![PathSprite { bounds }]
|
||||||
};
|
};
|
||||||
|
|
|
@ -1004,12 +1004,13 @@ impl X11Client {
|
||||||
let mut keystroke = crate::Keystroke::from_xkb(&state.xkb, modifiers, code);
|
let mut keystroke = crate::Keystroke::from_xkb(&state.xkb, modifiers, code);
|
||||||
let keysym = state.xkb.key_get_one_sym(code);
|
let keysym = state.xkb.key_get_one_sym(code);
|
||||||
|
|
||||||
// should be called after key_get_one_sym
|
|
||||||
state.xkb.update_key(code, xkbc::KeyDirection::Down);
|
|
||||||
|
|
||||||
if keysym.is_modifier_key() {
|
if keysym.is_modifier_key() {
|
||||||
return Some(());
|
return Some(());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// should be called after key_get_one_sym
|
||||||
|
state.xkb.update_key(code, xkbc::KeyDirection::Down);
|
||||||
|
|
||||||
if let Some(mut compose_state) = state.compose_state.take() {
|
if let Some(mut compose_state) = state.compose_state.take() {
|
||||||
compose_state.feed(keysym);
|
compose_state.feed(keysym);
|
||||||
match compose_state.status() {
|
match compose_state.status() {
|
||||||
|
@ -1067,12 +1068,13 @@ impl X11Client {
|
||||||
let keystroke = crate::Keystroke::from_xkb(&state.xkb, modifiers, code);
|
let keystroke = crate::Keystroke::from_xkb(&state.xkb, modifiers, code);
|
||||||
let keysym = state.xkb.key_get_one_sym(code);
|
let keysym = state.xkb.key_get_one_sym(code);
|
||||||
|
|
||||||
// should be called after key_get_one_sym
|
|
||||||
state.xkb.update_key(code, xkbc::KeyDirection::Up);
|
|
||||||
|
|
||||||
if keysym.is_modifier_key() {
|
if keysym.is_modifier_key() {
|
||||||
return Some(());
|
return Some(());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// should be called after key_get_one_sym
|
||||||
|
state.xkb.update_key(code, xkbc::KeyDirection::Up);
|
||||||
|
|
||||||
keystroke
|
keystroke
|
||||||
};
|
};
|
||||||
drop(state);
|
drop(state);
|
||||||
|
|
|
@ -791,13 +791,13 @@ impl MetalRenderer {
|
||||||
sprites = paths
|
sprites = paths
|
||||||
.iter()
|
.iter()
|
||||||
.map(|path| PathSprite {
|
.map(|path| PathSprite {
|
||||||
bounds: path.bounds,
|
bounds: path.clipped_bounds(),
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
} else {
|
} else {
|
||||||
let mut bounds = first_path.bounds;
|
let mut bounds = first_path.clipped_bounds();
|
||||||
for path in paths.iter().skip(1) {
|
for path in paths.iter().skip(1) {
|
||||||
bounds = bounds.union(&path.bounds);
|
bounds = bounds.union(&path.clipped_bounds());
|
||||||
}
|
}
|
||||||
sprites = vec![PathSprite { bounds }];
|
sprites = vec![PathSprite { bounds }];
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,7 +8,12 @@ use crate::{
|
||||||
AtlasTextureId, AtlasTile, Background, Bounds, ContentMask, Corners, Edges, Hsla, Pixels,
|
AtlasTextureId, AtlasTile, Background, Bounds, ContentMask, Corners, Edges, Hsla, Pixels,
|
||||||
Point, Radians, ScaledPixels, Size, bounds_tree::BoundsTree, point,
|
Point, Radians, ScaledPixels, Size, bounds_tree::BoundsTree, point,
|
||||||
};
|
};
|
||||||
use std::{fmt::Debug, iter::Peekable, ops::Range, slice};
|
use std::{
|
||||||
|
fmt::Debug,
|
||||||
|
iter::Peekable,
|
||||||
|
ops::{Add, Range, Sub},
|
||||||
|
slice,
|
||||||
|
};
|
||||||
|
|
||||||
#[allow(non_camel_case_types, unused)]
|
#[allow(non_camel_case_types, unused)]
|
||||||
pub(crate) type PathVertex_ScaledPixels = PathVertex<ScaledPixels>;
|
pub(crate) type PathVertex_ScaledPixels = PathVertex<ScaledPixels>;
|
||||||
|
@ -793,6 +798,16 @@ impl Path<Pixels> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl<T> Path<T>
|
||||||
|
where
|
||||||
|
T: Clone + Debug + Default + PartialEq + PartialOrd + Add<T, Output = T> + Sub<Output = T>,
|
||||||
|
{
|
||||||
|
#[allow(unused)]
|
||||||
|
pub(crate) fn clipped_bounds(&self) -> Bounds<T> {
|
||||||
|
self.bounds.intersect(&self.content_mask.bounds)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl From<Path<ScaledPixels>> for Primitive {
|
impl From<Path<ScaledPixels>> for Primitive {
|
||||||
fn from(path: Path<ScaledPixels>) -> Self {
|
fn from(path: Path<ScaledPixels>) -> Self {
|
||||||
Primitive::Path(path)
|
Primitive::Path(path)
|
||||||
|
|
|
@ -23,6 +23,7 @@ futures.workspace = true
|
||||||
http.workspace = true
|
http.workspace = true
|
||||||
http-body.workspace = true
|
http-body.workspace = true
|
||||||
log.workspace = true
|
log.workspace = true
|
||||||
|
parking_lot.workspace = true
|
||||||
serde.workspace = true
|
serde.workspace = true
|
||||||
serde_json.workspace = true
|
serde_json.workspace = true
|
||||||
url.workspace = true
|
url.workspace = true
|
||||||
|
|
|
@ -9,12 +9,10 @@ pub use http::{self, Method, Request, Response, StatusCode, Uri};
|
||||||
|
|
||||||
use futures::future::BoxFuture;
|
use futures::future::BoxFuture;
|
||||||
use http::request::Builder;
|
use http::request::Builder;
|
||||||
|
use parking_lot::Mutex;
|
||||||
#[cfg(feature = "test-support")]
|
#[cfg(feature = "test-support")]
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
use std::{
|
use std::{any::type_name, sync::Arc};
|
||||||
any::type_name,
|
|
||||||
sync::{Arc, Mutex},
|
|
||||||
};
|
|
||||||
pub use url::Url;
|
pub use url::Url;
|
||||||
|
|
||||||
#[derive(Default, Debug, Clone, PartialEq, Eq, Hash)]
|
#[derive(Default, Debug, Clone, PartialEq, Eq, Hash)]
|
||||||
|
@ -86,6 +84,11 @@ pub trait HttpClient: 'static + Send + Sync {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn proxy(&self) -> Option<&Url>;
|
fn proxy(&self) -> Option<&Url>;
|
||||||
|
|
||||||
|
#[cfg(feature = "test-support")]
|
||||||
|
fn as_fake(&self) -> &FakeHttpClient {
|
||||||
|
panic!("called as_fake on {}", type_name::<Self>())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// An [`HttpClient`] that may have a proxy.
|
/// An [`HttpClient`] that may have a proxy.
|
||||||
|
@ -132,6 +135,11 @@ impl HttpClient for HttpClientWithProxy {
|
||||||
fn type_name(&self) -> &'static str {
|
fn type_name(&self) -> &'static str {
|
||||||
self.client.type_name()
|
self.client.type_name()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "test-support")]
|
||||||
|
fn as_fake(&self) -> &FakeHttpClient {
|
||||||
|
self.client.as_fake()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl HttpClient for Arc<HttpClientWithProxy> {
|
impl HttpClient for Arc<HttpClientWithProxy> {
|
||||||
|
@ -153,6 +161,11 @@ impl HttpClient for Arc<HttpClientWithProxy> {
|
||||||
fn type_name(&self) -> &'static str {
|
fn type_name(&self) -> &'static str {
|
||||||
self.client.type_name()
|
self.client.type_name()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "test-support")]
|
||||||
|
fn as_fake(&self) -> &FakeHttpClient {
|
||||||
|
self.client.as_fake()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// An [`HttpClient`] that has a base URL.
|
/// An [`HttpClient`] that has a base URL.
|
||||||
|
@ -199,20 +212,13 @@ impl HttpClientWithUrl {
|
||||||
|
|
||||||
/// Returns the base URL.
|
/// Returns the base URL.
|
||||||
pub fn base_url(&self) -> String {
|
pub fn base_url(&self) -> String {
|
||||||
self.base_url
|
self.base_url.lock().clone()
|
||||||
.lock()
|
|
||||||
.map_or_else(|_| Default::default(), |url| url.clone())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sets the base URL.
|
/// Sets the base URL.
|
||||||
pub fn set_base_url(&self, base_url: impl Into<String>) {
|
pub fn set_base_url(&self, base_url: impl Into<String>) {
|
||||||
let base_url = base_url.into();
|
let base_url = base_url.into();
|
||||||
self.base_url
|
*self.base_url.lock() = base_url;
|
||||||
.lock()
|
|
||||||
.map(|mut url| {
|
|
||||||
*url = base_url;
|
|
||||||
})
|
|
||||||
.ok();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Builds a URL using the given path.
|
/// Builds a URL using the given path.
|
||||||
|
@ -236,6 +242,22 @@ impl HttpClientWithUrl {
|
||||||
)?)
|
)?)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Builds a Zed Cloud URL using the given path.
|
||||||
|
pub fn build_zed_cloud_url(&self, path: &str, query: &[(&str, &str)]) -> Result<Url> {
|
||||||
|
let base_url = self.base_url();
|
||||||
|
let base_api_url = match base_url.as_ref() {
|
||||||
|
"https://zed.dev" => "https://cloud.zed.dev",
|
||||||
|
"https://staging.zed.dev" => "https://cloud.zed.dev",
|
||||||
|
"http://localhost:3000" => "http://localhost:8787",
|
||||||
|
other => other,
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(Url::parse_with_params(
|
||||||
|
&format!("{}{}", base_api_url, path),
|
||||||
|
query,
|
||||||
|
)?)
|
||||||
|
}
|
||||||
|
|
||||||
/// Builds a Zed LLM URL using the given path.
|
/// Builds a Zed LLM URL using the given path.
|
||||||
pub fn build_zed_llm_url(&self, path: &str, query: &[(&str, &str)]) -> Result<Url> {
|
pub fn build_zed_llm_url(&self, path: &str, query: &[(&str, &str)]) -> Result<Url> {
|
||||||
let base_url = self.base_url();
|
let base_url = self.base_url();
|
||||||
|
@ -272,6 +294,11 @@ impl HttpClient for Arc<HttpClientWithUrl> {
|
||||||
fn type_name(&self) -> &'static str {
|
fn type_name(&self) -> &'static str {
|
||||||
self.client.type_name()
|
self.client.type_name()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "test-support")]
|
||||||
|
fn as_fake(&self) -> &FakeHttpClient {
|
||||||
|
self.client.as_fake()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl HttpClient for HttpClientWithUrl {
|
impl HttpClient for HttpClientWithUrl {
|
||||||
|
@ -293,6 +320,11 @@ impl HttpClient for HttpClientWithUrl {
|
||||||
fn type_name(&self) -> &'static str {
|
fn type_name(&self) -> &'static str {
|
||||||
self.client.type_name()
|
self.client.type_name()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "test-support")]
|
||||||
|
fn as_fake(&self) -> &FakeHttpClient {
|
||||||
|
self.client.as_fake()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn read_proxy_from_env() -> Option<Url> {
|
pub fn read_proxy_from_env() -> Option<Url> {
|
||||||
|
@ -344,10 +376,15 @@ impl HttpClient for BlockedHttpClient {
|
||||||
fn type_name(&self) -> &'static str {
|
fn type_name(&self) -> &'static str {
|
||||||
type_name::<Self>()
|
type_name::<Self>()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "test-support")]
|
||||||
|
fn as_fake(&self) -> &FakeHttpClient {
|
||||||
|
panic!("called as_fake on {}", type_name::<Self>())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "test-support")]
|
#[cfg(feature = "test-support")]
|
||||||
type FakeHttpHandler = Box<
|
type FakeHttpHandler = Arc<
|
||||||
dyn Fn(Request<AsyncBody>) -> BoxFuture<'static, anyhow::Result<Response<AsyncBody>>>
|
dyn Fn(Request<AsyncBody>) -> BoxFuture<'static, anyhow::Result<Response<AsyncBody>>>
|
||||||
+ Send
|
+ Send
|
||||||
+ Sync
|
+ Sync
|
||||||
|
@ -356,7 +393,7 @@ type FakeHttpHandler = Box<
|
||||||
|
|
||||||
#[cfg(feature = "test-support")]
|
#[cfg(feature = "test-support")]
|
||||||
pub struct FakeHttpClient {
|
pub struct FakeHttpClient {
|
||||||
handler: FakeHttpHandler,
|
handler: Mutex<Option<FakeHttpHandler>>,
|
||||||
user_agent: HeaderValue,
|
user_agent: HeaderValue,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -371,7 +408,7 @@ impl FakeHttpClient {
|
||||||
base_url: Mutex::new("http://test.example".into()),
|
base_url: Mutex::new("http://test.example".into()),
|
||||||
client: HttpClientWithProxy {
|
client: HttpClientWithProxy {
|
||||||
client: Arc::new(Self {
|
client: Arc::new(Self {
|
||||||
handler: Box::new(move |req| Box::pin(handler(req))),
|
handler: Mutex::new(Some(Arc::new(move |req| Box::pin(handler(req))))),
|
||||||
user_agent: HeaderValue::from_static(type_name::<Self>()),
|
user_agent: HeaderValue::from_static(type_name::<Self>()),
|
||||||
}),
|
}),
|
||||||
proxy: None,
|
proxy: None,
|
||||||
|
@ -396,6 +433,18 @@ impl FakeHttpClient {
|
||||||
.unwrap())
|
.unwrap())
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn replace_handler<Fut, F>(&self, new_handler: F)
|
||||||
|
where
|
||||||
|
Fut: futures::Future<Output = anyhow::Result<Response<AsyncBody>>> + Send + 'static,
|
||||||
|
F: Fn(FakeHttpHandler, Request<AsyncBody>) -> Fut + Send + Sync + 'static,
|
||||||
|
{
|
||||||
|
let mut handler = self.handler.lock();
|
||||||
|
let old_handler = handler.take().unwrap();
|
||||||
|
*handler = Some(Arc::new(move |req| {
|
||||||
|
Box::pin(new_handler(old_handler.clone(), req))
|
||||||
|
}));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "test-support")]
|
#[cfg(feature = "test-support")]
|
||||||
|
@ -411,7 +460,7 @@ impl HttpClient for FakeHttpClient {
|
||||||
&self,
|
&self,
|
||||||
req: Request<AsyncBody>,
|
req: Request<AsyncBody>,
|
||||||
) -> BoxFuture<'static, anyhow::Result<Response<AsyncBody>>> {
|
) -> BoxFuture<'static, anyhow::Result<Response<AsyncBody>>> {
|
||||||
let future = (self.handler)(req);
|
let future = (self.handler.lock().as_ref().unwrap())(req);
|
||||||
future
|
future
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -426,4 +475,8 @@ impl HttpClient for FakeHttpClient {
|
||||||
fn type_name(&self) -> &'static str {
|
fn type_name(&self) -> &'static str {
|
||||||
type_name::<Self>()
|
type_name::<Self>()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn as_fake(&self) -> &FakeHttpClient {
|
||||||
|
self
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,6 +25,7 @@ indoc.workspace = true
|
||||||
inline_completion.workspace = true
|
inline_completion.workspace = true
|
||||||
language.workspace = true
|
language.workspace = true
|
||||||
paths.workspace = true
|
paths.workspace = true
|
||||||
|
project.workspace = true
|
||||||
regex.workspace = true
|
regex.workspace = true
|
||||||
settings.workspace = true
|
settings.workspace = true
|
||||||
supermaven.workspace = true
|
supermaven.workspace = true
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use client::{DisableAiSettings, UserStore, zed_urls};
|
use client::{UserStore, zed_urls};
|
||||||
use cloud_llm_client::UsageLimit;
|
use cloud_llm_client::UsageLimit;
|
||||||
use copilot::{Copilot, Status};
|
use copilot::{Copilot, Status};
|
||||||
use editor::{
|
use editor::{
|
||||||
|
@ -19,6 +19,7 @@ use language::{
|
||||||
EditPredictionsMode, File, Language,
|
EditPredictionsMode, File, Language,
|
||||||
language_settings::{self, AllLanguageSettings, EditPredictionProvider, all_language_settings},
|
language_settings::{self, AllLanguageSettings, EditPredictionProvider, all_language_settings},
|
||||||
};
|
};
|
||||||
|
use project::DisableAiSettings;
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
use settings::{Settings, SettingsStore, update_settings_file};
|
use settings::{Settings, SettingsStore, update_settings_file};
|
||||||
use std::{
|
use std::{
|
||||||
|
@ -246,12 +247,15 @@ impl Render for InlineCompletionButton {
|
||||||
};
|
};
|
||||||
|
|
||||||
if zeta::should_show_upsell_modal(&self.user_store, cx) {
|
if zeta::should_show_upsell_modal(&self.user_store, cx) {
|
||||||
let tooltip_meta =
|
let tooltip_meta = if self.user_store.read(cx).current_user().is_some() {
|
||||||
match self.user_store.read(cx).current_user_has_accepted_terms() {
|
if self.user_store.read(cx).has_accepted_terms_of_service() {
|
||||||
Some(true) => "Choose a Plan",
|
"Choose a Plan"
|
||||||
Some(false) => "Accept the Terms of Service",
|
} else {
|
||||||
None => "Sign In",
|
"Accept the Terms of Service"
|
||||||
};
|
}
|
||||||
|
} else {
|
||||||
|
"Sign In"
|
||||||
|
};
|
||||||
|
|
||||||
return div().child(
|
return div().child(
|
||||||
IconButton::new("zed-predict-pending-button", zeta_icon)
|
IconButton::new("zed-predict-pending-button", zeta_icon)
|
||||||
|
@ -387,9 +391,9 @@ impl InlineCompletionButton {
|
||||||
language: None,
|
language: None,
|
||||||
file: None,
|
file: None,
|
||||||
edit_prediction_provider: None,
|
edit_prediction_provider: None,
|
||||||
|
user_store,
|
||||||
popover_menu_handle,
|
popover_menu_handle,
|
||||||
fs,
|
fs,
|
||||||
user_store,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -20,6 +20,7 @@ anthropic = { workspace = true, features = ["schemars"] }
|
||||||
anyhow.workspace = true
|
anyhow.workspace = true
|
||||||
base64.workspace = true
|
base64.workspace = true
|
||||||
client.workspace = true
|
client.workspace = true
|
||||||
|
cloud_api_types.workspace = true
|
||||||
cloud_llm_client.workspace = true
|
cloud_llm_client.workspace = true
|
||||||
collections.workspace = true
|
collections.workspace = true
|
||||||
futures.workspace = true
|
futures.workspace = true
|
||||||
|
|
|
@ -3,10 +3,9 @@ use std::sync::Arc;
|
||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use client::Client;
|
use client::Client;
|
||||||
use gpui::{
|
use cloud_api_types::websocket_protocol::MessageToClient;
|
||||||
App, AppContext as _, AsyncApp, Context, Entity, EventEmitter, Global, ReadGlobal as _,
|
use cloud_llm_client::Plan;
|
||||||
};
|
use gpui::{App, AppContext as _, Context, Entity, EventEmitter, Global, ReadGlobal as _};
|
||||||
use proto::{Plan, TypedEnvelope};
|
|
||||||
use smol::lock::{RwLock, RwLockUpgradableReadGuard, RwLockWriteGuard};
|
use smol::lock::{RwLock, RwLockUpgradableReadGuard, RwLockWriteGuard};
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
|
@ -30,7 +29,7 @@ pub struct ModelRequestLimitReachedError {
|
||||||
impl fmt::Display for ModelRequestLimitReachedError {
|
impl fmt::Display for ModelRequestLimitReachedError {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
let message = match self.plan {
|
let message = match self.plan {
|
||||||
Plan::Free => "Model request limit reached. Upgrade to Zed Pro for more requests.",
|
Plan::ZedFree => "Model request limit reached. Upgrade to Zed Pro for more requests.",
|
||||||
Plan::ZedPro => {
|
Plan::ZedPro => {
|
||||||
"Model request limit reached. Upgrade to usage-based billing for more requests."
|
"Model request limit reached. Upgrade to usage-based billing for more requests."
|
||||||
}
|
}
|
||||||
|
@ -64,9 +63,14 @@ impl LlmApiToken {
|
||||||
mut lock: RwLockWriteGuard<'_, Option<String>>,
|
mut lock: RwLockWriteGuard<'_, Option<String>>,
|
||||||
client: &Arc<Client>,
|
client: &Arc<Client>,
|
||||||
) -> Result<String> {
|
) -> Result<String> {
|
||||||
let response = client.request(proto::GetLlmToken {}).await?;
|
let system_id = client
|
||||||
*lock = Some(response.token.clone());
|
.telemetry()
|
||||||
Ok(response.token.clone())
|
.system_id()
|
||||||
|
.map(|system_id| system_id.to_string());
|
||||||
|
|
||||||
|
let response = client.cloud_client().create_llm_token(system_id).await?;
|
||||||
|
*lock = Some(response.token.0.clone());
|
||||||
|
Ok(response.token.0.clone())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -76,9 +80,7 @@ impl Global for GlobalRefreshLlmTokenListener {}
|
||||||
|
|
||||||
pub struct RefreshLlmTokenEvent;
|
pub struct RefreshLlmTokenEvent;
|
||||||
|
|
||||||
pub struct RefreshLlmTokenListener {
|
pub struct RefreshLlmTokenListener;
|
||||||
_llm_token_subscription: client::Subscription,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl EventEmitter<RefreshLlmTokenEvent> for RefreshLlmTokenListener {}
|
impl EventEmitter<RefreshLlmTokenEvent> for RefreshLlmTokenListener {}
|
||||||
|
|
||||||
|
@ -93,17 +95,21 @@ impl RefreshLlmTokenListener {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn new(client: Arc<Client>, cx: &mut Context<Self>) -> Self {
|
fn new(client: Arc<Client>, cx: &mut Context<Self>) -> Self {
|
||||||
Self {
|
client.add_message_to_client_handler({
|
||||||
_llm_token_subscription: client
|
let this = cx.entity();
|
||||||
.add_message_handler(cx.weak_entity(), Self::handle_refresh_llm_token),
|
move |message, cx| {
|
||||||
}
|
Self::handle_refresh_llm_token(this.clone(), message, cx);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Self
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn handle_refresh_llm_token(
|
fn handle_refresh_llm_token(this: Entity<Self>, message: &MessageToClient, cx: &mut App) {
|
||||||
this: Entity<Self>,
|
match message {
|
||||||
_: TypedEnvelope<proto::RefreshLlmToken>,
|
MessageToClient::UserUpdated => {
|
||||||
mut cx: AsyncApp,
|
this.update(cx, |_this, cx| cx.emit(RefreshLlmTokenEvent));
|
||||||
) -> Result<()> {
|
}
|
||||||
this.update(&mut cx, |_this, cx| cx.emit(RefreshLlmTokenEvent))
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -44,7 +44,6 @@ ollama = { workspace = true, features = ["schemars"] }
|
||||||
open_ai = { workspace = true, features = ["schemars"] }
|
open_ai = { workspace = true, features = ["schemars"] }
|
||||||
open_router = { workspace = true, features = ["schemars"] }
|
open_router = { workspace = true, features = ["schemars"] }
|
||||||
partial-json-fixer.workspace = true
|
partial-json-fixer.workspace = true
|
||||||
proto.workspace = true
|
|
||||||
release_channel.workspace = true
|
release_channel.workspace = true
|
||||||
schemars.workspace = true
|
schemars.workspace = true
|
||||||
serde.workspace = true
|
serde.workspace = true
|
||||||
|
|
|
@ -6,7 +6,7 @@ use client::{Client, ModelRequestUsage, UserStore, zed_urls};
|
||||||
use cloud_llm_client::{
|
use cloud_llm_client::{
|
||||||
CLIENT_SUPPORTS_STATUS_MESSAGES_HEADER_NAME, CURRENT_PLAN_HEADER_NAME, CompletionBody,
|
CLIENT_SUPPORTS_STATUS_MESSAGES_HEADER_NAME, CURRENT_PLAN_HEADER_NAME, CompletionBody,
|
||||||
CompletionEvent, CompletionRequestStatus, CountTokensBody, CountTokensResponse,
|
CompletionEvent, CompletionRequestStatus, CountTokensBody, CountTokensResponse,
|
||||||
EXPIRED_LLM_TOKEN_HEADER_NAME, ListModelsResponse, MODEL_REQUESTS_RESOURCE_HEADER_VALUE,
|
EXPIRED_LLM_TOKEN_HEADER_NAME, ListModelsResponse, MODEL_REQUESTS_RESOURCE_HEADER_VALUE, Plan,
|
||||||
SERVER_SUPPORTS_STATUS_MESSAGES_HEADER_NAME, SUBSCRIPTION_LIMIT_RESOURCE_HEADER_NAME,
|
SERVER_SUPPORTS_STATUS_MESSAGES_HEADER_NAME, SUBSCRIPTION_LIMIT_RESOURCE_HEADER_NAME,
|
||||||
TOOL_USE_LIMIT_REACHED_HEADER_NAME, ZED_VERSION_HEADER_NAME,
|
TOOL_USE_LIMIT_REACHED_HEADER_NAME, ZED_VERSION_HEADER_NAME,
|
||||||
};
|
};
|
||||||
|
@ -27,7 +27,6 @@ use language_model::{
|
||||||
LanguageModelToolChoice, LanguageModelToolSchemaFormat, LlmApiToken,
|
LanguageModelToolChoice, LanguageModelToolSchemaFormat, LlmApiToken,
|
||||||
ModelRequestLimitReachedError, PaymentRequiredError, RateLimiter, RefreshLlmTokenListener,
|
ModelRequestLimitReachedError, PaymentRequiredError, RateLimiter, RefreshLlmTokenListener,
|
||||||
};
|
};
|
||||||
use proto::Plan;
|
|
||||||
use release_channel::AppVersion;
|
use release_channel::AppVersion;
|
||||||
use schemars::JsonSchema;
|
use schemars::JsonSchema;
|
||||||
use serde::{Deserialize, Serialize, de::DeserializeOwned};
|
use serde::{Deserialize, Serialize, de::DeserializeOwned};
|
||||||
|
@ -137,11 +136,11 @@ impl State {
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let refresh_llm_token_listener = RefreshLlmTokenListener::global(cx);
|
let refresh_llm_token_listener = RefreshLlmTokenListener::global(cx);
|
||||||
|
let mut current_user = user_store.read(cx).watch_current_user();
|
||||||
Self {
|
Self {
|
||||||
client: client.clone(),
|
client: client.clone(),
|
||||||
llm_api_token: LlmApiToken::default(),
|
llm_api_token: LlmApiToken::default(),
|
||||||
user_store,
|
user_store: user_store.clone(),
|
||||||
status,
|
status,
|
||||||
accept_terms_of_service_task: None,
|
accept_terms_of_service_task: None,
|
||||||
models: Vec::new(),
|
models: Vec::new(),
|
||||||
|
@ -153,21 +152,14 @@ impl State {
|
||||||
let (client, llm_api_token) = this
|
let (client, llm_api_token) = this
|
||||||
.read_with(cx, |this, _cx| (client.clone(), this.llm_api_token.clone()))?;
|
.read_with(cx, |this, _cx| (client.clone(), this.llm_api_token.clone()))?;
|
||||||
|
|
||||||
loop {
|
while current_user.borrow().is_none() {
|
||||||
let status = this.read_with(cx, |this, _cx| this.status)?;
|
current_user.next().await;
|
||||||
if matches!(status, client::Status::Connected { .. }) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
cx.background_executor()
|
|
||||||
.timer(Duration::from_millis(100))
|
|
||||||
.await;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let response = Self::fetch_models(client, llm_api_token).await?;
|
let response =
|
||||||
this.update(cx, |this, cx| {
|
Self::fetch_models(client.clone(), llm_api_token.clone()).await?;
|
||||||
this.update_models(response, cx);
|
this.update(cx, |this, cx| this.update_models(response, cx))?;
|
||||||
})
|
anyhow::Ok(())
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.context("failed to fetch Zed models")
|
.context("failed to fetch Zed models")
|
||||||
|
@ -194,26 +186,20 @@ impl State {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn is_signed_out(&self) -> bool {
|
fn is_signed_out(&self, cx: &App) -> bool {
|
||||||
self.status.is_signed_out()
|
self.user_store.read(cx).current_user().is_none()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn authenticate(&self, cx: &mut Context<Self>) -> Task<Result<()>> {
|
fn authenticate(&self, cx: &mut Context<Self>) -> Task<Result<()>> {
|
||||||
let client = self.client.clone();
|
let client = self.client.clone();
|
||||||
cx.spawn(async move |state, cx| {
|
cx.spawn(async move |state, cx| {
|
||||||
client
|
client.sign_in_with_optional_connect(true, &cx).await?;
|
||||||
.authenticate_and_connect(true, &cx)
|
|
||||||
.await
|
|
||||||
.into_response()?;
|
|
||||||
state.update(cx, |_, cx| cx.notify())
|
state.update(cx, |_, cx| cx.notify())
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn has_accepted_terms_of_service(&self, cx: &App) -> bool {
|
fn has_accepted_terms_of_service(&self, cx: &App) -> bool {
|
||||||
self.user_store
|
self.user_store.read(cx).has_accepted_terms_of_service()
|
||||||
.read(cx)
|
|
||||||
.current_user_has_accepted_terms()
|
|
||||||
.unwrap_or(false)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn accept_terms_of_service(&mut self, cx: &mut Context<Self>) {
|
fn accept_terms_of_service(&mut self, cx: &mut Context<Self>) {
|
||||||
|
@ -398,7 +384,7 @@ impl LanguageModelProvider for CloudLanguageModelProvider {
|
||||||
|
|
||||||
fn is_authenticated(&self, cx: &App) -> bool {
|
fn is_authenticated(&self, cx: &App) -> bool {
|
||||||
let state = self.state.read(cx);
|
let state = self.state.read(cx);
|
||||||
!state.is_signed_out() && state.has_accepted_terms_of_service(cx)
|
!state.is_signed_out(cx) && state.has_accepted_terms_of_service(cx)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn authenticate(&self, _cx: &mut App) -> Task<Result<(), AuthenticateError>> {
|
fn authenticate(&self, _cx: &mut App) -> Task<Result<(), AuthenticateError>> {
|
||||||
|
@ -613,11 +599,6 @@ impl CloudLanguageModel {
|
||||||
.and_then(|plan| plan.to_str().ok())
|
.and_then(|plan| plan.to_str().ok())
|
||||||
.and_then(|plan| cloud_llm_client::Plan::from_str(plan).ok())
|
.and_then(|plan| cloud_llm_client::Plan::from_str(plan).ok())
|
||||||
{
|
{
|
||||||
let plan = match plan {
|
|
||||||
cloud_llm_client::Plan::ZedFree => Plan::Free,
|
|
||||||
cloud_llm_client::Plan::ZedPro => Plan::ZedPro,
|
|
||||||
cloud_llm_client::Plan::ZedProTrial => Plan::ZedProTrial,
|
|
||||||
};
|
|
||||||
return Err(anyhow!(ModelRequestLimitReachedError { plan }));
|
return Err(anyhow!(ModelRequestLimitReachedError { plan }));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1118,7 +1099,7 @@ fn response_lines<T: DeserializeOwned>(
|
||||||
#[derive(IntoElement, RegisterComponent)]
|
#[derive(IntoElement, RegisterComponent)]
|
||||||
struct ZedAiConfiguration {
|
struct ZedAiConfiguration {
|
||||||
is_connected: bool,
|
is_connected: bool,
|
||||||
plan: Option<proto::Plan>,
|
plan: Option<Plan>,
|
||||||
subscription_period: Option<(DateTime<Utc>, DateTime<Utc>)>,
|
subscription_period: Option<(DateTime<Utc>, DateTime<Utc>)>,
|
||||||
eligible_for_trial: bool,
|
eligible_for_trial: bool,
|
||||||
has_accepted_terms_of_service: bool,
|
has_accepted_terms_of_service: bool,
|
||||||
|
@ -1132,15 +1113,15 @@ impl RenderOnce for ZedAiConfiguration {
|
||||||
fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement {
|
fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement {
|
||||||
let young_account_banner = YoungAccountBanner;
|
let young_account_banner = YoungAccountBanner;
|
||||||
|
|
||||||
let is_pro = self.plan == Some(proto::Plan::ZedPro);
|
let is_pro = self.plan == Some(Plan::ZedPro);
|
||||||
let subscription_text = match (self.plan, self.subscription_period) {
|
let subscription_text = match (self.plan, self.subscription_period) {
|
||||||
(Some(proto::Plan::ZedPro), Some(_)) => {
|
(Some(Plan::ZedPro), Some(_)) => {
|
||||||
"You have access to Zed's hosted models through your Pro subscription."
|
"You have access to Zed's hosted models through your Pro subscription."
|
||||||
}
|
}
|
||||||
(Some(proto::Plan::ZedProTrial), Some(_)) => {
|
(Some(Plan::ZedProTrial), Some(_)) => {
|
||||||
"You have access to Zed's hosted models through your Pro trial."
|
"You have access to Zed's hosted models through your Pro trial."
|
||||||
}
|
}
|
||||||
(Some(proto::Plan::Free), Some(_)) => {
|
(Some(Plan::ZedFree), Some(_)) => {
|
||||||
"You have basic access to Zed's hosted models through the Free plan."
|
"You have basic access to Zed's hosted models through the Free plan."
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
|
@ -1265,8 +1246,8 @@ impl Render for ConfigurationView {
|
||||||
let user_store = state.user_store.read(cx);
|
let user_store = state.user_store.read(cx);
|
||||||
|
|
||||||
ZedAiConfiguration {
|
ZedAiConfiguration {
|
||||||
is_connected: !state.is_signed_out(),
|
is_connected: !state.is_signed_out(cx),
|
||||||
plan: user_store.current_plan(),
|
plan: user_store.plan(),
|
||||||
subscription_period: user_store.subscription_period(),
|
subscription_period: user_store.subscription_period(),
|
||||||
eligible_for_trial: user_store.trial_started_at().is_none(),
|
eligible_for_trial: user_store.trial_started_at().is_none(),
|
||||||
has_accepted_terms_of_service: state.has_accepted_terms_of_service(cx),
|
has_accepted_terms_of_service: state.has_accepted_terms_of_service(cx),
|
||||||
|
@ -1286,7 +1267,7 @@ impl Component for ZedAiConfiguration {
|
||||||
fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
|
fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
|
||||||
fn configuration(
|
fn configuration(
|
||||||
is_connected: bool,
|
is_connected: bool,
|
||||||
plan: Option<proto::Plan>,
|
plan: Option<Plan>,
|
||||||
eligible_for_trial: bool,
|
eligible_for_trial: bool,
|
||||||
account_too_young: bool,
|
account_too_young: bool,
|
||||||
has_accepted_terms_of_service: bool,
|
has_accepted_terms_of_service: bool,
|
||||||
|
@ -1330,15 +1311,15 @@ impl Component for ZedAiConfiguration {
|
||||||
),
|
),
|
||||||
single_example(
|
single_example(
|
||||||
"Free Plan",
|
"Free Plan",
|
||||||
configuration(true, Some(proto::Plan::Free), true, false, true),
|
configuration(true, Some(Plan::ZedFree), true, false, true),
|
||||||
),
|
),
|
||||||
single_example(
|
single_example(
|
||||||
"Zed Pro Trial Plan",
|
"Zed Pro Trial Plan",
|
||||||
configuration(true, Some(proto::Plan::ZedProTrial), true, false, true),
|
configuration(true, Some(Plan::ZedProTrial), true, false, true),
|
||||||
),
|
),
|
||||||
single_example(
|
single_example(
|
||||||
"Zed Pro Plan",
|
"Zed Pro Plan",
|
||||||
configuration(true, Some(proto::Plan::ZedPro), true, false, true),
|
configuration(true, Some(Plan::ZedPro), true, false, true),
|
||||||
),
|
),
|
||||||
])
|
])
|
||||||
.into_any_element(),
|
.into_any_element(),
|
||||||
|
|
|
@ -674,6 +674,10 @@ pub fn count_open_ai_tokens(
|
||||||
| Model::O3
|
| Model::O3
|
||||||
| Model::O3Mini
|
| Model::O3Mini
|
||||||
| Model::O4Mini => tiktoken_rs::num_tokens_from_messages(model.id(), &messages),
|
| Model::O4Mini => tiktoken_rs::num_tokens_from_messages(model.id(), &messages),
|
||||||
|
// GPT-5 models don't have tiktoken support yet; fall back on gpt-4o tokenizer
|
||||||
|
Model::Five | Model::FiveMini | Model::FiveNano => {
|
||||||
|
tiktoken_rs::num_tokens_from_messages("gpt-4o", &messages)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.map(|tokens| tokens as u64)
|
.map(|tokens| tokens as u64)
|
||||||
})
|
})
|
||||||
|
|
|
@ -13,14 +13,15 @@ use parking_lot::Mutex;
|
||||||
use smol::io::BufReader;
|
use smol::io::BufReader;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
AnyNotification, AnyResponse, CONTENT_LEN_HEADER, IoHandler, IoKind, RequestId, ResponseHandler,
|
AnyResponse, CONTENT_LEN_HEADER, IoHandler, IoKind, NotificationOrRequest, RequestId,
|
||||||
|
ResponseHandler,
|
||||||
};
|
};
|
||||||
|
|
||||||
const HEADER_DELIMITER: &[u8; 4] = b"\r\n\r\n";
|
const HEADER_DELIMITER: &[u8; 4] = b"\r\n\r\n";
|
||||||
/// Handler for stdout of language server.
|
/// Handler for stdout of language server.
|
||||||
pub struct LspStdoutHandler {
|
pub struct LspStdoutHandler {
|
||||||
pub(super) loop_handle: Task<Result<()>>,
|
pub(super) loop_handle: Task<Result<()>>,
|
||||||
pub(super) notifications_channel: UnboundedReceiver<AnyNotification>,
|
pub(super) incoming_messages: UnboundedReceiver<NotificationOrRequest>,
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn read_headers<Stdout>(reader: &mut BufReader<Stdout>, buffer: &mut Vec<u8>) -> Result<()>
|
async fn read_headers<Stdout>(reader: &mut BufReader<Stdout>, buffer: &mut Vec<u8>) -> Result<()>
|
||||||
|
@ -54,13 +55,13 @@ impl LspStdoutHandler {
|
||||||
let loop_handle = cx.spawn(Self::handler(stdout, tx, response_handlers, io_handlers));
|
let loop_handle = cx.spawn(Self::handler(stdout, tx, response_handlers, io_handlers));
|
||||||
Self {
|
Self {
|
||||||
loop_handle,
|
loop_handle,
|
||||||
notifications_channel,
|
incoming_messages: notifications_channel,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn handler<Input>(
|
async fn handler<Input>(
|
||||||
stdout: Input,
|
stdout: Input,
|
||||||
notifications_sender: UnboundedSender<AnyNotification>,
|
notifications_sender: UnboundedSender<NotificationOrRequest>,
|
||||||
response_handlers: Arc<Mutex<Option<HashMap<RequestId, ResponseHandler>>>>,
|
response_handlers: Arc<Mutex<Option<HashMap<RequestId, ResponseHandler>>>>,
|
||||||
io_handlers: Arc<Mutex<HashMap<i32, IoHandler>>>,
|
io_handlers: Arc<Mutex<HashMap<i32, IoHandler>>>,
|
||||||
) -> anyhow::Result<()>
|
) -> anyhow::Result<()>
|
||||||
|
@ -96,7 +97,7 @@ impl LspStdoutHandler {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Ok(msg) = serde_json::from_slice::<AnyNotification>(&buffer) {
|
if let Ok(msg) = serde_json::from_slice::<NotificationOrRequest>(&buffer) {
|
||||||
notifications_sender.unbounded_send(msg)?;
|
notifications_sender.unbounded_send(msg)?;
|
||||||
} else if let Ok(AnyResponse {
|
} else if let Ok(AnyResponse {
|
||||||
id, error, result, ..
|
id, error, result, ..
|
||||||
|
|
|
@ -242,7 +242,7 @@ struct Notification<'a, T> {
|
||||||
|
|
||||||
/// Language server RPC notification message before it is deserialized into a concrete type.
|
/// Language server RPC notification message before it is deserialized into a concrete type.
|
||||||
#[derive(Debug, Clone, Deserialize)]
|
#[derive(Debug, Clone, Deserialize)]
|
||||||
struct AnyNotification {
|
struct NotificationOrRequest {
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
id: Option<RequestId>,
|
id: Option<RequestId>,
|
||||||
method: String,
|
method: String,
|
||||||
|
@ -252,7 +252,10 @@ struct AnyNotification {
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
struct Error {
|
struct Error {
|
||||||
|
code: i64,
|
||||||
message: String,
|
message: String,
|
||||||
|
#[serde(default)]
|
||||||
|
data: Option<serde_json::Value>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait LspRequestFuture<O>: Future<Output = ConnectionResult<O>> {
|
pub trait LspRequestFuture<O>: Future<Output = ConnectionResult<O>> {
|
||||||
|
@ -364,6 +367,7 @@ impl LanguageServer {
|
||||||
notification.method,
|
notification.method,
|
||||||
serde_json::to_string_pretty(¬ification.params).unwrap(),
|
serde_json::to_string_pretty(¬ification.params).unwrap(),
|
||||||
);
|
);
|
||||||
|
false
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -389,7 +393,7 @@ impl LanguageServer {
|
||||||
Stdin: AsyncWrite + Unpin + Send + 'static,
|
Stdin: AsyncWrite + Unpin + Send + 'static,
|
||||||
Stdout: AsyncRead + Unpin + Send + 'static,
|
Stdout: AsyncRead + Unpin + Send + 'static,
|
||||||
Stderr: AsyncRead + Unpin + Send + 'static,
|
Stderr: AsyncRead + Unpin + Send + 'static,
|
||||||
F: FnMut(AnyNotification) + 'static + Send + Sync + Clone,
|
F: Fn(&NotificationOrRequest) -> bool + 'static + Send + Sync + Clone,
|
||||||
{
|
{
|
||||||
let (outbound_tx, outbound_rx) = channel::unbounded::<String>();
|
let (outbound_tx, outbound_rx) = channel::unbounded::<String>();
|
||||||
let (output_done_tx, output_done_rx) = barrier::channel();
|
let (output_done_tx, output_done_rx) = barrier::channel();
|
||||||
|
@ -400,14 +404,34 @@ impl LanguageServer {
|
||||||
let io_handlers = Arc::new(Mutex::new(HashMap::default()));
|
let io_handlers = Arc::new(Mutex::new(HashMap::default()));
|
||||||
|
|
||||||
let stdout_input_task = cx.spawn({
|
let stdout_input_task = cx.spawn({
|
||||||
let on_unhandled_notification = on_unhandled_notification.clone();
|
let unhandled_notification_wrapper = {
|
||||||
|
let response_channel = outbound_tx.clone();
|
||||||
|
async move |msg: NotificationOrRequest| {
|
||||||
|
let did_handle = on_unhandled_notification(&msg);
|
||||||
|
if !did_handle && let Some(message_id) = msg.id {
|
||||||
|
let response = AnyResponse {
|
||||||
|
jsonrpc: JSON_RPC_VERSION,
|
||||||
|
id: message_id,
|
||||||
|
error: Some(Error {
|
||||||
|
code: -32601,
|
||||||
|
message: format!("Unrecognized method `{}`", msg.method),
|
||||||
|
data: None,
|
||||||
|
}),
|
||||||
|
result: None,
|
||||||
|
};
|
||||||
|
if let Ok(response) = serde_json::to_string(&response) {
|
||||||
|
response_channel.send(response).await.ok();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
let notification_handlers = notification_handlers.clone();
|
let notification_handlers = notification_handlers.clone();
|
||||||
let response_handlers = response_handlers.clone();
|
let response_handlers = response_handlers.clone();
|
||||||
let io_handlers = io_handlers.clone();
|
let io_handlers = io_handlers.clone();
|
||||||
async move |cx| {
|
async move |cx| {
|
||||||
Self::handle_input(
|
Self::handle_incoming_messages(
|
||||||
stdout,
|
stdout,
|
||||||
on_unhandled_notification,
|
unhandled_notification_wrapper,
|
||||||
notification_handlers,
|
notification_handlers,
|
||||||
response_handlers,
|
response_handlers,
|
||||||
io_handlers,
|
io_handlers,
|
||||||
|
@ -433,7 +457,7 @@ impl LanguageServer {
|
||||||
stdout.or(stderr)
|
stdout.or(stderr)
|
||||||
});
|
});
|
||||||
let output_task = cx.background_spawn({
|
let output_task = cx.background_spawn({
|
||||||
Self::handle_output(
|
Self::handle_outgoing_messages(
|
||||||
stdin,
|
stdin,
|
||||||
outbound_rx,
|
outbound_rx,
|
||||||
output_done_tx,
|
output_done_tx,
|
||||||
|
@ -479,9 +503,9 @@ impl LanguageServer {
|
||||||
self.code_action_kinds.clone()
|
self.code_action_kinds.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn handle_input<Stdout, F>(
|
async fn handle_incoming_messages<Stdout>(
|
||||||
stdout: Stdout,
|
stdout: Stdout,
|
||||||
mut on_unhandled_notification: F,
|
on_unhandled_notification: impl AsyncFn(NotificationOrRequest) + 'static + Send,
|
||||||
notification_handlers: Arc<Mutex<HashMap<&'static str, NotificationHandler>>>,
|
notification_handlers: Arc<Mutex<HashMap<&'static str, NotificationHandler>>>,
|
||||||
response_handlers: Arc<Mutex<Option<HashMap<RequestId, ResponseHandler>>>>,
|
response_handlers: Arc<Mutex<Option<HashMap<RequestId, ResponseHandler>>>>,
|
||||||
io_handlers: Arc<Mutex<HashMap<i32, IoHandler>>>,
|
io_handlers: Arc<Mutex<HashMap<i32, IoHandler>>>,
|
||||||
|
@ -489,7 +513,6 @@ impl LanguageServer {
|
||||||
) -> anyhow::Result<()>
|
) -> anyhow::Result<()>
|
||||||
where
|
where
|
||||||
Stdout: AsyncRead + Unpin + Send + 'static,
|
Stdout: AsyncRead + Unpin + Send + 'static,
|
||||||
F: FnMut(AnyNotification) + 'static + Send,
|
|
||||||
{
|
{
|
||||||
use smol::stream::StreamExt;
|
use smol::stream::StreamExt;
|
||||||
let stdout = BufReader::new(stdout);
|
let stdout = BufReader::new(stdout);
|
||||||
|
@ -506,15 +529,19 @@ impl LanguageServer {
|
||||||
cx.background_executor().clone(),
|
cx.background_executor().clone(),
|
||||||
);
|
);
|
||||||
|
|
||||||
while let Some(msg) = input_handler.notifications_channel.next().await {
|
while let Some(msg) = input_handler.incoming_messages.next().await {
|
||||||
{
|
let unhandled_message = {
|
||||||
let mut notification_handlers = notification_handlers.lock();
|
let mut notification_handlers = notification_handlers.lock();
|
||||||
if let Some(handler) = notification_handlers.get_mut(msg.method.as_str()) {
|
if let Some(handler) = notification_handlers.get_mut(msg.method.as_str()) {
|
||||||
handler(msg.id, msg.params.unwrap_or(Value::Null), cx);
|
handler(msg.id, msg.params.unwrap_or(Value::Null), cx);
|
||||||
|
None
|
||||||
} else {
|
} else {
|
||||||
drop(notification_handlers);
|
Some(msg)
|
||||||
on_unhandled_notification(msg);
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(msg) = unhandled_message {
|
||||||
|
on_unhandled_notification(msg).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Don't starve the main thread when receiving lots of notifications at once.
|
// Don't starve the main thread when receiving lots of notifications at once.
|
||||||
|
@ -558,7 +585,7 @@ impl LanguageServer {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn handle_output<Stdin>(
|
async fn handle_outgoing_messages<Stdin>(
|
||||||
stdin: Stdin,
|
stdin: Stdin,
|
||||||
outbound_rx: channel::Receiver<String>,
|
outbound_rx: channel::Receiver<String>,
|
||||||
output_done_tx: barrier::Sender,
|
output_done_tx: barrier::Sender,
|
||||||
|
@ -1036,7 +1063,9 @@ impl LanguageServer {
|
||||||
jsonrpc: JSON_RPC_VERSION,
|
jsonrpc: JSON_RPC_VERSION,
|
||||||
id,
|
id,
|
||||||
value: LspResult::Error(Some(Error {
|
value: LspResult::Error(Some(Error {
|
||||||
|
code: lsp_types::error_codes::REQUEST_FAILED,
|
||||||
message: error.to_string(),
|
message: error.to_string(),
|
||||||
|
data: None,
|
||||||
})),
|
})),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -1057,7 +1086,9 @@ impl LanguageServer {
|
||||||
id,
|
id,
|
||||||
result: None,
|
result: None,
|
||||||
error: Some(Error {
|
error: Some(Error {
|
||||||
|
code: -32700, // Parse error
|
||||||
message: error.to_string(),
|
message: error.to_string(),
|
||||||
|
data: None,
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
if let Some(response) = serde_json::to_string(&response).log_err() {
|
if let Some(response) = serde_json::to_string(&response).log_err() {
|
||||||
|
@ -1559,7 +1590,7 @@ impl FakeLanguageServer {
|
||||||
root,
|
root,
|
||||||
Some(workspace_folders.clone()),
|
Some(workspace_folders.clone()),
|
||||||
cx,
|
cx,
|
||||||
|_| {},
|
|_| false,
|
||||||
);
|
);
|
||||||
server.process_name = process_name;
|
server.process_name = process_name;
|
||||||
let fake = FakeLanguageServer {
|
let fake = FakeLanguageServer {
|
||||||
|
@ -1582,9 +1613,10 @@ impl FakeLanguageServer {
|
||||||
notifications_tx
|
notifications_tx
|
||||||
.try_send((
|
.try_send((
|
||||||
msg.method.to_string(),
|
msg.method.to_string(),
|
||||||
msg.params.unwrap_or(Value::Null).to_string(),
|
msg.params.as_ref().unwrap_or(&Value::Null).to_string(),
|
||||||
))
|
))
|
||||||
.ok();
|
.ok();
|
||||||
|
true
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
server.process_name = name.as_str().into();
|
server.process_name = name.as_str().into();
|
||||||
|
@ -1862,7 +1894,7 @@ mod tests {
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
fn test_deserialize_string_digit_id() {
|
fn test_deserialize_string_digit_id() {
|
||||||
let json = r#"{"jsonrpc":"2.0","id":"2","method":"workspace/configuration","params":{"items":[{"scopeUri":"file:///Users/mph/Devel/personal/hello-scala/","section":"metals"}]}}"#;
|
let json = r#"{"jsonrpc":"2.0","id":"2","method":"workspace/configuration","params":{"items":[{"scopeUri":"file:///Users/mph/Devel/personal/hello-scala/","section":"metals"}]}}"#;
|
||||||
let notification = serde_json::from_str::<AnyNotification>(json)
|
let notification = serde_json::from_str::<NotificationOrRequest>(json)
|
||||||
.expect("message with string id should be parsed");
|
.expect("message with string id should be parsed");
|
||||||
let expected_id = RequestId::Str("2".to_string());
|
let expected_id = RequestId::Str("2".to_string());
|
||||||
assert_eq!(notification.id, Some(expected_id));
|
assert_eq!(notification.id, Some(expected_id));
|
||||||
|
@ -1871,7 +1903,7 @@ mod tests {
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
fn test_deserialize_string_id() {
|
fn test_deserialize_string_id() {
|
||||||
let json = r#"{"jsonrpc":"2.0","id":"anythingAtAll","method":"workspace/configuration","params":{"items":[{"scopeUri":"file:///Users/mph/Devel/personal/hello-scala/","section":"metals"}]}}"#;
|
let json = r#"{"jsonrpc":"2.0","id":"anythingAtAll","method":"workspace/configuration","params":{"items":[{"scopeUri":"file:///Users/mph/Devel/personal/hello-scala/","section":"metals"}]}}"#;
|
||||||
let notification = serde_json::from_str::<AnyNotification>(json)
|
let notification = serde_json::from_str::<NotificationOrRequest>(json)
|
||||||
.expect("message with string id should be parsed");
|
.expect("message with string id should be parsed");
|
||||||
let expected_id = RequestId::Str("anythingAtAll".to_string());
|
let expected_id = RequestId::Str("anythingAtAll".to_string());
|
||||||
assert_eq!(notification.id, Some(expected_id));
|
assert_eq!(notification.id, Some(expected_id));
|
||||||
|
@ -1880,7 +1912,7 @@ mod tests {
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
fn test_deserialize_int_id() {
|
fn test_deserialize_int_id() {
|
||||||
let json = r#"{"jsonrpc":"2.0","id":2,"method":"workspace/configuration","params":{"items":[{"scopeUri":"file:///Users/mph/Devel/personal/hello-scala/","section":"metals"}]}}"#;
|
let json = r#"{"jsonrpc":"2.0","id":2,"method":"workspace/configuration","params":{"items":[{"scopeUri":"file:///Users/mph/Devel/personal/hello-scala/","section":"metals"}]}}"#;
|
||||||
let notification = serde_json::from_str::<AnyNotification>(json)
|
let notification = serde_json::from_str::<NotificationOrRequest>(json)
|
||||||
.expect("message with string id should be parsed");
|
.expect("message with string id should be parsed");
|
||||||
let expected_id = RequestId::Int(2);
|
let expected_id = RequestId::Int(2);
|
||||||
assert_eq!(notification.id, Some(expected_id));
|
assert_eq!(notification.id, Some(expected_id));
|
||||||
|
|
|
@ -167,10 +167,10 @@ impl Anchor {
|
||||||
if *self == Anchor::min() || *self == Anchor::max() {
|
if *self == Anchor::min() || *self == Anchor::max() {
|
||||||
true
|
true
|
||||||
} else if let Some(excerpt) = snapshot.excerpt(self.excerpt_id) {
|
} else if let Some(excerpt) = snapshot.excerpt(self.excerpt_id) {
|
||||||
excerpt.contains(self)
|
(self.text_anchor == excerpt.range.context.start
|
||||||
&& (self.text_anchor == excerpt.range.context.start
|
|| self.text_anchor == excerpt.range.context.end
|
||||||
|| self.text_anchor == excerpt.range.context.end
|
|| self.text_anchor.is_valid(&excerpt.buffer))
|
||||||
|| self.text_anchor.is_valid(&excerpt.buffer))
|
&& excerpt.contains(self)
|
||||||
} else {
|
} else {
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
|
@ -74,6 +74,12 @@ pub enum Model {
|
||||||
O3,
|
O3,
|
||||||
#[serde(rename = "o4-mini")]
|
#[serde(rename = "o4-mini")]
|
||||||
O4Mini,
|
O4Mini,
|
||||||
|
#[serde(rename = "gpt-5")]
|
||||||
|
Five,
|
||||||
|
#[serde(rename = "gpt-5-mini")]
|
||||||
|
FiveMini,
|
||||||
|
#[serde(rename = "gpt-5-nano")]
|
||||||
|
FiveNano,
|
||||||
|
|
||||||
#[serde(rename = "custom")]
|
#[serde(rename = "custom")]
|
||||||
Custom {
|
Custom {
|
||||||
|
@ -105,6 +111,9 @@ impl Model {
|
||||||
"o3-mini" => Ok(Self::O3Mini),
|
"o3-mini" => Ok(Self::O3Mini),
|
||||||
"o3" => Ok(Self::O3),
|
"o3" => Ok(Self::O3),
|
||||||
"o4-mini" => Ok(Self::O4Mini),
|
"o4-mini" => Ok(Self::O4Mini),
|
||||||
|
"gpt-5" => Ok(Self::Five),
|
||||||
|
"gpt-5-mini" => Ok(Self::FiveMini),
|
||||||
|
"gpt-5-nano" => Ok(Self::FiveNano),
|
||||||
invalid_id => anyhow::bail!("invalid model id '{invalid_id}'"),
|
invalid_id => anyhow::bail!("invalid model id '{invalid_id}'"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -123,6 +132,9 @@ impl Model {
|
||||||
Self::O3Mini => "o3-mini",
|
Self::O3Mini => "o3-mini",
|
||||||
Self::O3 => "o3",
|
Self::O3 => "o3",
|
||||||
Self::O4Mini => "o4-mini",
|
Self::O4Mini => "o4-mini",
|
||||||
|
Self::Five => "gpt-5",
|
||||||
|
Self::FiveMini => "gpt-5-mini",
|
||||||
|
Self::FiveNano => "gpt-5-nano",
|
||||||
Self::Custom { name, .. } => name,
|
Self::Custom { name, .. } => name,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -141,6 +153,9 @@ impl Model {
|
||||||
Self::O3Mini => "o3-mini",
|
Self::O3Mini => "o3-mini",
|
||||||
Self::O3 => "o3",
|
Self::O3 => "o3",
|
||||||
Self::O4Mini => "o4-mini",
|
Self::O4Mini => "o4-mini",
|
||||||
|
Self::Five => "gpt-5",
|
||||||
|
Self::FiveMini => "gpt-5-mini",
|
||||||
|
Self::FiveNano => "gpt-5-nano",
|
||||||
Self::Custom {
|
Self::Custom {
|
||||||
name, display_name, ..
|
name, display_name, ..
|
||||||
} => display_name.as_ref().unwrap_or(name),
|
} => display_name.as_ref().unwrap_or(name),
|
||||||
|
@ -161,6 +176,9 @@ impl Model {
|
||||||
Self::O3Mini => 200_000,
|
Self::O3Mini => 200_000,
|
||||||
Self::O3 => 200_000,
|
Self::O3 => 200_000,
|
||||||
Self::O4Mini => 200_000,
|
Self::O4Mini => 200_000,
|
||||||
|
Self::Five => 272_000,
|
||||||
|
Self::FiveMini => 272_000,
|
||||||
|
Self::FiveNano => 272_000,
|
||||||
Self::Custom { max_tokens, .. } => *max_tokens,
|
Self::Custom { max_tokens, .. } => *max_tokens,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -182,6 +200,9 @@ impl Model {
|
||||||
Self::O3Mini => Some(100_000),
|
Self::O3Mini => Some(100_000),
|
||||||
Self::O3 => Some(100_000),
|
Self::O3 => Some(100_000),
|
||||||
Self::O4Mini => Some(100_000),
|
Self::O4Mini => Some(100_000),
|
||||||
|
Self::Five => Some(128_000),
|
||||||
|
Self::FiveMini => Some(128_000),
|
||||||
|
Self::FiveNano => Some(128_000),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -197,7 +218,10 @@ impl Model {
|
||||||
| Self::FourOmniMini
|
| Self::FourOmniMini
|
||||||
| Self::FourPointOne
|
| Self::FourPointOne
|
||||||
| Self::FourPointOneMini
|
| Self::FourPointOneMini
|
||||||
| Self::FourPointOneNano => true,
|
| Self::FourPointOneNano
|
||||||
|
| Self::Five
|
||||||
|
| Self::FiveMini
|
||||||
|
| Self::FiveNano => true,
|
||||||
Self::O1 | Self::O3 | Self::O3Mini | Self::O4Mini | Model::Custom { .. } => false,
|
Self::O1 | Self::O3 | Self::O3Mini | Self::O4Mini | Model::Custom { .. } => false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
use std::{path::Path, sync::Arc};
|
use std::{path::Path, sync::Arc};
|
||||||
|
|
||||||
use dap::client::DebugAdapterClient;
|
use dap::client::DebugAdapterClient;
|
||||||
use gpui::{App, AppContext, Subscription};
|
use gpui::{App, Subscription};
|
||||||
|
|
||||||
use super::session::{Session, SessionStateEvent};
|
use super::session::{Session, SessionStateEvent};
|
||||||
|
|
||||||
|
@ -19,14 +19,6 @@ pub fn intercept_debug_sessions<T: Fn(&Arc<DebugAdapterClient>) + 'static>(
|
||||||
let client = session.adapter_client().unwrap();
|
let client = session.adapter_client().unwrap();
|
||||||
register_default_handlers(session, &client, cx);
|
register_default_handlers(session, &client, cx);
|
||||||
configure(&client);
|
configure(&client);
|
||||||
cx.background_spawn(async move {
|
|
||||||
client
|
|
||||||
.fake_event(dap::messages::Events::Initialized(
|
|
||||||
Some(Default::default()),
|
|
||||||
))
|
|
||||||
.await
|
|
||||||
})
|
|
||||||
.detach();
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.detach();
|
.detach();
|
||||||
|
|
|
@ -2269,7 +2269,7 @@ impl LspCommand for GetCompletions {
|
||||||
// the range based on the syntax tree.
|
// the range based on the syntax tree.
|
||||||
None => {
|
None => {
|
||||||
if self.position != clipped_position {
|
if self.position != clipped_position {
|
||||||
log::info!("completion out of expected range");
|
log::info!("completion out of expected range ");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2483,7 +2483,9 @@ pub(crate) fn parse_completion_text_edit(
|
||||||
let start = snapshot.clip_point_utf16(range.start, Bias::Left);
|
let start = snapshot.clip_point_utf16(range.start, Bias::Left);
|
||||||
let end = snapshot.clip_point_utf16(range.end, Bias::Left);
|
let end = snapshot.clip_point_utf16(range.end, Bias::Left);
|
||||||
if start != range.start.0 || end != range.end.0 {
|
if start != range.start.0 || end != range.end.0 {
|
||||||
log::info!("completion out of expected range");
|
log::info!(
|
||||||
|
"completion out of expected range, start: {start:?}, end: {end:?}, range: {range:?}"
|
||||||
|
);
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
snapshot.anchor_before(start)..snapshot.anchor_after(end)
|
snapshot.anchor_before(start)..snapshot.anchor_after(end)
|
||||||
|
|
|
@ -97,7 +97,7 @@ use rpc::{
|
||||||
};
|
};
|
||||||
use search::{SearchInputKind, SearchQuery, SearchResult};
|
use search::{SearchInputKind, SearchQuery, SearchResult};
|
||||||
use search_history::SearchHistory;
|
use search_history::SearchHistory;
|
||||||
use settings::{InvalidSettingsError, Settings, SettingsLocation, SettingsStore};
|
use settings::{InvalidSettingsError, Settings, SettingsLocation, SettingsSources, SettingsStore};
|
||||||
use smol::channel::Receiver;
|
use smol::channel::Receiver;
|
||||||
use snippet::Snippet;
|
use snippet::Snippet;
|
||||||
use snippet_provider::SnippetProvider;
|
use snippet_provider::SnippetProvider;
|
||||||
|
@ -942,10 +942,38 @@ pub enum PulledDiagnostics {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Whether to disable all AI features in Zed.
|
||||||
|
///
|
||||||
|
/// Default: false
|
||||||
|
#[derive(Copy, Clone, Debug)]
|
||||||
|
pub struct DisableAiSettings {
|
||||||
|
pub disable_ai: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl settings::Settings for DisableAiSettings {
|
||||||
|
const KEY: Option<&'static str> = Some("disable_ai");
|
||||||
|
|
||||||
|
type FileContent = Option<bool>;
|
||||||
|
|
||||||
|
fn load(sources: SettingsSources<Self::FileContent>, _: &mut App) -> Result<Self> {
|
||||||
|
Ok(Self {
|
||||||
|
disable_ai: sources
|
||||||
|
.user
|
||||||
|
.or(sources.server)
|
||||||
|
.copied()
|
||||||
|
.flatten()
|
||||||
|
.unwrap_or(sources.default.ok_or_else(Self::missing_default)?),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn import_from_vscode(_vscode: &settings::VsCodeSettings, _current: &mut Self::FileContent) {}
|
||||||
|
}
|
||||||
|
|
||||||
impl Project {
|
impl Project {
|
||||||
pub fn init_settings(cx: &mut App) {
|
pub fn init_settings(cx: &mut App) {
|
||||||
WorktreeSettings::register(cx);
|
WorktreeSettings::register(cx);
|
||||||
ProjectSettings::register(cx);
|
ProjectSettings::register(cx);
|
||||||
|
DisableAiSettings::register(cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn init(client: &Arc<Client>, cx: &mut App) {
|
pub fn init(client: &Arc<Client>, cx: &mut App) {
|
||||||
|
@ -1362,10 +1390,7 @@ impl Project {
|
||||||
fs: Arc<dyn Fs>,
|
fs: Arc<dyn Fs>,
|
||||||
cx: AsyncApp,
|
cx: AsyncApp,
|
||||||
) -> Result<Entity<Self>> {
|
) -> Result<Entity<Self>> {
|
||||||
client
|
client.connect(true, &cx).await.into_response()?;
|
||||||
.authenticate_and_connect(true, &cx)
|
|
||||||
.await
|
|
||||||
.into_response()?;
|
|
||||||
|
|
||||||
let subscriptions = [
|
let subscriptions = [
|
||||||
EntitySubscription::Project(client.subscribe_to_entity::<Self>(remote_id)?),
|
EntitySubscription::Project(client.subscribe_to_entity::<Self>(remote_id)?),
|
||||||
|
|
|
@ -99,7 +99,9 @@ impl Anchor {
|
||||||
} else if self.buffer_id != Some(buffer.remote_id) {
|
} else if self.buffer_id != Some(buffer.remote_id) {
|
||||||
false
|
false
|
||||||
} else {
|
} else {
|
||||||
let fragment_id = buffer.fragment_id_for_anchor(self);
|
let Some(fragment_id) = buffer.try_fragment_id_for_anchor(self) else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
let mut fragment_cursor = buffer.fragments.cursor::<(Option<&Locator>, usize)>(&None);
|
let mut fragment_cursor = buffer.fragments.cursor::<(Option<&Locator>, usize)>(&None);
|
||||||
fragment_cursor.seek(&Some(fragment_id), Bias::Left);
|
fragment_cursor.seek(&Some(fragment_id), Bias::Left);
|
||||||
fragment_cursor
|
fragment_cursor
|
||||||
|
|
|
@ -2330,10 +2330,19 @@ impl BufferSnapshot {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn fragment_id_for_anchor(&self, anchor: &Anchor) -> &Locator {
|
fn fragment_id_for_anchor(&self, anchor: &Anchor) -> &Locator {
|
||||||
|
self.try_fragment_id_for_anchor(anchor).unwrap_or_else(|| {
|
||||||
|
panic!(
|
||||||
|
"invalid anchor {:?}. buffer id: {}, version: {:?}",
|
||||||
|
anchor, self.remote_id, self.version,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn try_fragment_id_for_anchor(&self, anchor: &Anchor) -> Option<&Locator> {
|
||||||
if *anchor == Anchor::MIN {
|
if *anchor == Anchor::MIN {
|
||||||
Locator::min_ref()
|
Some(Locator::min_ref())
|
||||||
} else if *anchor == Anchor::MAX {
|
} else if *anchor == Anchor::MAX {
|
||||||
Locator::max_ref()
|
Some(Locator::max_ref())
|
||||||
} else {
|
} else {
|
||||||
let anchor_key = InsertionFragmentKey {
|
let anchor_key = InsertionFragmentKey {
|
||||||
timestamp: anchor.timestamp,
|
timestamp: anchor.timestamp,
|
||||||
|
@ -2354,20 +2363,12 @@ impl BufferSnapshot {
|
||||||
insertion_cursor.prev();
|
insertion_cursor.prev();
|
||||||
}
|
}
|
||||||
|
|
||||||
let Some(insertion) = insertion_cursor.item().filter(|insertion| {
|
insertion_cursor
|
||||||
if cfg!(debug_assertions) {
|
.item()
|
||||||
insertion.timestamp == anchor.timestamp
|
.filter(|insertion| {
|
||||||
} else {
|
!cfg!(debug_assertions) || insertion.timestamp == anchor.timestamp
|
||||||
true
|
})
|
||||||
}
|
.map(|insertion| &insertion.fragment_id)
|
||||||
}) else {
|
|
||||||
panic!(
|
|
||||||
"invalid anchor {:?}. buffer id: {}, version: {:?}",
|
|
||||||
anchor, self.remote_id, self.version
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
&insertion.fragment_id
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -32,6 +32,7 @@ auto_update.workspace = true
|
||||||
call.workspace = true
|
call.workspace = true
|
||||||
chrono.workspace = true
|
chrono.workspace = true
|
||||||
client.workspace = true
|
client.workspace = true
|
||||||
|
cloud_llm_client.workspace = true
|
||||||
db.workspace = true
|
db.workspace = true
|
||||||
gpui = { workspace = true, features = ["screen-capture"] }
|
gpui = { workspace = true, features = ["screen-capture"] }
|
||||||
notifications.workspace = true
|
notifications.workspace = true
|
||||||
|
|
|
@ -21,6 +21,7 @@ use crate::application_menu::{
|
||||||
use auto_update::AutoUpdateStatus;
|
use auto_update::AutoUpdateStatus;
|
||||||
use call::ActiveCall;
|
use call::ActiveCall;
|
||||||
use client::{Client, UserStore, zed_urls};
|
use client::{Client, UserStore, zed_urls};
|
||||||
|
use cloud_llm_client::Plan;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
Action, AnyElement, App, Context, Corner, Element, Entity, Focusable, InteractiveElement,
|
Action, AnyElement, App, Context, Corner, Element, Entity, Focusable, InteractiveElement,
|
||||||
IntoElement, MouseButton, ParentElement, Render, StatefulInteractiveElement, Styled,
|
IntoElement, MouseButton, ParentElement, Render, StatefulInteractiveElement, Styled,
|
||||||
|
@ -28,7 +29,6 @@ use gpui::{
|
||||||
};
|
};
|
||||||
use onboarding_banner::OnboardingBanner;
|
use onboarding_banner::OnboardingBanner;
|
||||||
use project::Project;
|
use project::Project;
|
||||||
use rpc::proto;
|
|
||||||
use settings::Settings as _;
|
use settings::Settings as _;
|
||||||
use settings_ui::keybindings;
|
use settings_ui::keybindings;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
@ -179,24 +179,23 @@ impl Render for TitleBar {
|
||||||
children.push(self.banner.clone().into_any_element())
|
children.push(self.banner.clone().into_any_element())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let status = self.client.status();
|
||||||
|
let status = &*status.borrow();
|
||||||
|
let user = self.user_store.read(cx).current_user();
|
||||||
|
|
||||||
children.push(
|
children.push(
|
||||||
h_flex()
|
h_flex()
|
||||||
.gap_1()
|
.gap_1()
|
||||||
.pr_1()
|
.pr_1()
|
||||||
.on_mouse_down(MouseButton::Left, |_, _, cx| cx.stop_propagation())
|
.on_mouse_down(MouseButton::Left, |_, _, cx| cx.stop_propagation())
|
||||||
.children(self.render_call_controls(window, cx))
|
.children(self.render_call_controls(window, cx))
|
||||||
.map(|el| {
|
.children(self.render_connection_status(status, cx))
|
||||||
let status = self.client.status();
|
.when(
|
||||||
let status = &*status.borrow();
|
user.is_none() && TitleBarSettings::get_global(cx).show_sign_in,
|
||||||
if matches!(status, client::Status::Connected { .. }) {
|
|el| el.child(self.render_sign_in_button(cx)),
|
||||||
el.child(self.render_user_menu_button(cx))
|
)
|
||||||
} else {
|
.when(user.is_some(), |parent| {
|
||||||
el.children(self.render_connection_status(status, cx))
|
parent.child(self.render_user_menu_button(cx))
|
||||||
.when(TitleBarSettings::get_global(cx).show_sign_in, |el| {
|
|
||||||
el.child(self.render_sign_in_button(cx))
|
|
||||||
})
|
|
||||||
.child(self.render_user_menu_button(cx))
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
.into_any_element(),
|
.into_any_element(),
|
||||||
);
|
);
|
||||||
|
@ -618,9 +617,8 @@ impl TitleBar {
|
||||||
window
|
window
|
||||||
.spawn(cx, async move |cx| {
|
.spawn(cx, async move |cx| {
|
||||||
client
|
client
|
||||||
.authenticate_and_connect(true, &cx)
|
.sign_in_with_optional_connect(true, &cx)
|
||||||
.await
|
.await
|
||||||
.into_response()
|
|
||||||
.notify_async_err(cx);
|
.notify_async_err(cx);
|
||||||
})
|
})
|
||||||
.detach();
|
.detach();
|
||||||
|
@ -630,8 +628,8 @@ impl TitleBar {
|
||||||
pub fn render_user_menu_button(&mut self, cx: &mut Context<Self>) -> impl Element {
|
pub fn render_user_menu_button(&mut self, cx: &mut Context<Self>) -> impl Element {
|
||||||
let user_store = self.user_store.read(cx);
|
let user_store = self.user_store.read(cx);
|
||||||
if let Some(user) = user_store.current_user() {
|
if let Some(user) = user_store.current_user() {
|
||||||
let has_subscription_period = self.user_store.read(cx).subscription_period().is_some();
|
let has_subscription_period = user_store.subscription_period().is_some();
|
||||||
let plan = self.user_store.read(cx).current_plan().filter(|_| {
|
let plan = user_store.plan().filter(|_| {
|
||||||
// Since the user might be on the legacy free plan we filter based on whether we have a subscription period.
|
// Since the user might be on the legacy free plan we filter based on whether we have a subscription period.
|
||||||
has_subscription_period
|
has_subscription_period
|
||||||
});
|
});
|
||||||
|
@ -658,13 +656,9 @@ impl TitleBar {
|
||||||
let user_login = user.github_login.clone();
|
let user_login = user.github_login.clone();
|
||||||
|
|
||||||
let (plan_name, label_color, bg_color) = match plan {
|
let (plan_name, label_color, bg_color) = match plan {
|
||||||
None | Some(proto::Plan::Free) => {
|
None | Some(Plan::ZedFree) => ("Free", Color::Default, free_chip_bg),
|
||||||
("Free", Color::Default, free_chip_bg)
|
Some(Plan::ZedProTrial) => ("Pro Trial", Color::Accent, pro_chip_bg),
|
||||||
}
|
Some(Plan::ZedPro) => ("Pro", Color::Accent, pro_chip_bg),
|
||||||
Some(proto::Plan::ZedProTrial) => {
|
|
||||||
("Pro Trial", Color::Accent, pro_chip_bg)
|
|
||||||
}
|
|
||||||
Some(proto::Plan::ZedPro) => ("Pro", Color::Accent, pro_chip_bg),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
menu.custom_entry(
|
menu.custom_entry(
|
||||||
|
|
|
@ -1,10 +1,11 @@
|
||||||
use client::{DisableAiSettings, TelemetrySettings, telemetry::Telemetry};
|
use client::{TelemetrySettings, telemetry::Telemetry};
|
||||||
use db::kvp::KEY_VALUE_STORE;
|
use db::kvp::KEY_VALUE_STORE;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
Action, App, Context, Entity, EventEmitter, FocusHandle, Focusable, InteractiveElement,
|
Action, App, Context, Entity, EventEmitter, FocusHandle, Focusable, InteractiveElement,
|
||||||
ParentElement, Render, Styled, Subscription, Task, WeakEntity, Window, actions, svg,
|
ParentElement, Render, Styled, Subscription, Task, WeakEntity, Window, actions, svg,
|
||||||
};
|
};
|
||||||
use language::language_settings::{EditPredictionProvider, all_language_settings};
|
use language::language_settings::{EditPredictionProvider, all_language_settings};
|
||||||
|
use project::DisableAiSettings;
|
||||||
use settings::{Settings, SettingsStore};
|
use settings::{Settings, SettingsStore};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use ui::{CheckboxWithLabel, ElevationIndex, Tooltip, prelude::*};
|
use ui::{CheckboxWithLabel, ElevationIndex, Tooltip, prelude::*};
|
||||||
|
|
|
@ -5689,7 +5689,6 @@ impl Workspace {
|
||||||
|
|
||||||
let client = project.read(cx).client();
|
let client = project.read(cx).client();
|
||||||
let user_store = project.read(cx).user_store();
|
let user_store = project.read(cx).user_store();
|
||||||
|
|
||||||
let workspace_store = cx.new(|cx| WorkspaceStore::new(client.clone(), cx));
|
let workspace_store = cx.new(|cx| WorkspaceStore::new(client.clone(), cx));
|
||||||
let session = cx.new(|cx| AppSession::new(Session::test(), cx));
|
let session = cx.new(|cx| AppSession::new(Session::test(), cx));
|
||||||
window.activate_window();
|
window.activate_window();
|
||||||
|
@ -6894,10 +6893,13 @@ async fn join_channel_internal(
|
||||||
match status {
|
match status {
|
||||||
Status::Connecting
|
Status::Connecting
|
||||||
| Status::Authenticating
|
| Status::Authenticating
|
||||||
|
| Status::Authenticated
|
||||||
| Status::Reconnecting
|
| Status::Reconnecting
|
||||||
| Status::Reauthenticating => continue,
|
| Status::Reauthenticating => continue,
|
||||||
Status::Connected { .. } => break 'outer,
|
Status::Connected { .. } => break 'outer,
|
||||||
Status::SignedOut => return Err(ErrorCode::SignedOut.into()),
|
Status::SignedOut | Status::AuthenticationError => {
|
||||||
|
return Err(ErrorCode::SignedOut.into());
|
||||||
|
}
|
||||||
Status::UpgradeRequired => return Err(ErrorCode::UpgradeRequired.into()),
|
Status::UpgradeRequired => return Err(ErrorCode::UpgradeRequired.into()),
|
||||||
Status::ConnectionError | Status::ConnectionLost | Status::ReconnectionError { .. } => {
|
Status::ConnectionError | Status::ConnectionLost | Status::ReconnectionError { .. } => {
|
||||||
return Err(ErrorCode::Disconnected.into());
|
return Err(ErrorCode::Disconnected.into());
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
description = "The fast, collaborative code editor."
|
description = "The fast, collaborative code editor."
|
||||||
edition.workspace = true
|
edition.workspace = true
|
||||||
name = "zed"
|
name = "zed"
|
||||||
version = "0.198.0"
|
version = "0.198.6"
|
||||||
publish.workspace = true
|
publish.workspace = true
|
||||||
license = "GPL-3.0-or-later"
|
license = "GPL-3.0-or-later"
|
||||||
authors = ["Zed Team <hi@zed.dev>"]
|
authors = ["Zed Team <hi@zed.dev>"]
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
dev
|
stable
|
|
@ -42,7 +42,7 @@ use theme::{
|
||||||
ActiveTheme, IconThemeNotFoundError, SystemAppearance, ThemeNotFoundError, ThemeRegistry,
|
ActiveTheme, IconThemeNotFoundError, SystemAppearance, ThemeNotFoundError, ThemeRegistry,
|
||||||
ThemeSettings,
|
ThemeSettings,
|
||||||
};
|
};
|
||||||
use util::{ConnectionResult, ResultExt, TryFutureExt, maybe};
|
use util::{ResultExt, TryFutureExt, maybe};
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
use welcome::{FIRST_OPEN, show_welcome_view};
|
use welcome::{FIRST_OPEN, show_welcome_view};
|
||||||
use workspace::{
|
use workspace::{
|
||||||
|
@ -681,17 +681,9 @@ pub fn main() {
|
||||||
|
|
||||||
cx.spawn({
|
cx.spawn({
|
||||||
let client = app_state.client.clone();
|
let client = app_state.client.clone();
|
||||||
async move |cx| match authenticate(client, &cx).await {
|
async move |cx| authenticate(client, &cx).await
|
||||||
ConnectionResult::Timeout => log::error!("Timeout during initial auth"),
|
|
||||||
ConnectionResult::ConnectionReset => {
|
|
||||||
log::error!("Connection reset during initial auth")
|
|
||||||
}
|
|
||||||
ConnectionResult::Result(r) => {
|
|
||||||
r.log_err();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
.detach();
|
.detach_and_log_err(cx);
|
||||||
|
|
||||||
let urls: Vec<_> = args
|
let urls: Vec<_> = args
|
||||||
.paths_or_urls
|
.paths_or_urls
|
||||||
|
@ -841,15 +833,7 @@ fn handle_open_request(request: OpenRequest, app_state: Arc<AppState>, cx: &mut
|
||||||
let client = app_state.client.clone();
|
let client = app_state.client.clone();
|
||||||
// we continue even if authentication fails as join_channel/ open channel notes will
|
// we continue even if authentication fails as join_channel/ open channel notes will
|
||||||
// show a visible error message.
|
// show a visible error message.
|
||||||
match authenticate(client, &cx).await {
|
authenticate(client, &cx).await.log_err();
|
||||||
ConnectionResult::Timeout => {
|
|
||||||
log::error!("Timeout during open request handling")
|
|
||||||
}
|
|
||||||
ConnectionResult::ConnectionReset => {
|
|
||||||
log::error!("Connection reset during open request handling")
|
|
||||||
}
|
|
||||||
ConnectionResult::Result(r) => r?,
|
|
||||||
};
|
|
||||||
|
|
||||||
if let Some(channel_id) = request.join_channel {
|
if let Some(channel_id) = request.join_channel {
|
||||||
cx.update(|cx| {
|
cx.update(|cx| {
|
||||||
|
@ -899,18 +883,18 @@ fn handle_open_request(request: OpenRequest, app_state: Arc<AppState>, cx: &mut
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn authenticate(client: Arc<Client>, cx: &AsyncApp) -> ConnectionResult<()> {
|
async fn authenticate(client: Arc<Client>, cx: &AsyncApp) -> Result<()> {
|
||||||
if stdout_is_a_pty() {
|
if stdout_is_a_pty() {
|
||||||
if client::IMPERSONATE_LOGIN.is_some() {
|
if client::IMPERSONATE_LOGIN.is_some() {
|
||||||
return client.authenticate_and_connect(false, cx).await;
|
client.sign_in_with_optional_connect(false, cx).await?;
|
||||||
} else if client.has_credentials(cx).await {
|
} else if client.has_credentials(cx).await {
|
||||||
return client.authenticate_and_connect(true, cx).await;
|
client.sign_in_with_optional_connect(true, cx).await?;
|
||||||
}
|
}
|
||||||
} else if client.has_credentials(cx).await {
|
} else if client.has_credentials(cx).await {
|
||||||
return client.authenticate_and_connect(true, cx).await;
|
client.sign_in_with_optional_connect(true, cx).await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
ConnectionResult::Result(Ok(()))
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn system_id() -> Result<IdType> {
|
async fn system_id() -> Result<IdType> {
|
||||||
|
|
|
@ -139,8 +139,7 @@ impl ComponentPreview {
|
||||||
let project_clone = project.clone();
|
let project_clone = project.clone();
|
||||||
|
|
||||||
cx.spawn_in(window, async move |entity, cx| {
|
cx.spawn_in(window, async move |entity, cx| {
|
||||||
let thread_store_future =
|
let thread_store_future = load_preview_thread_store(project_clone.clone(), cx);
|
||||||
load_preview_thread_store(workspace_clone.clone(), project_clone.clone(), cx);
|
|
||||||
let text_thread_store_future =
|
let text_thread_store_future =
|
||||||
load_preview_text_thread_store(workspace_clone.clone(), project_clone.clone(), cx);
|
load_preview_text_thread_store(workspace_clone.clone(), project_clone.clone(), cx);
|
||||||
|
|
||||||
|
|
|
@ -12,21 +12,19 @@ use ui::{App, Window};
|
||||||
use workspace::Workspace;
|
use workspace::Workspace;
|
||||||
|
|
||||||
pub fn load_preview_thread_store(
|
pub fn load_preview_thread_store(
|
||||||
workspace: WeakEntity<Workspace>,
|
|
||||||
project: Entity<Project>,
|
project: Entity<Project>,
|
||||||
cx: &mut AsyncApp,
|
cx: &mut AsyncApp,
|
||||||
) -> Task<Result<Entity<ThreadStore>>> {
|
) -> Task<Result<Entity<ThreadStore>>> {
|
||||||
workspace
|
cx.update(|cx| {
|
||||||
.update(cx, |_, cx| {
|
ThreadStore::load(
|
||||||
ThreadStore::load(
|
project.clone(),
|
||||||
project.clone(),
|
cx.new(|_| ToolWorkingSet::default()),
|
||||||
cx.new(|_| ToolWorkingSet::default()),
|
None,
|
||||||
None,
|
Arc::new(PromptBuilder::new(None).unwrap()),
|
||||||
Arc::new(PromptBuilder::new(None).unwrap()),
|
cx,
|
||||||
cx,
|
)
|
||||||
)
|
})
|
||||||
})
|
.unwrap_or(Task::ready(Err(anyhow!("workspace dropped"))))
|
||||||
.unwrap_or(Task::ready(Err(anyhow!("workspace dropped"))))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn load_preview_text_thread_store(
|
pub fn load_preview_text_thread_store(
|
||||||
|
|
|
@ -1,15 +1,14 @@
|
||||||
use client::{Client, DisableAiSettings, UserStore};
|
use client::{Client, UserStore};
|
||||||
use collections::HashMap;
|
use collections::HashMap;
|
||||||
use copilot::{Copilot, CopilotCompletionProvider};
|
use copilot::{Copilot, CopilotCompletionProvider};
|
||||||
use editor::Editor;
|
use editor::Editor;
|
||||||
use gpui::{AnyWindowHandle, App, AppContext as _, Context, Entity, WeakEntity};
|
use gpui::{AnyWindowHandle, App, AppContext as _, Context, Entity, WeakEntity};
|
||||||
use language::language_settings::{EditPredictionProvider, all_language_settings};
|
use language::language_settings::{EditPredictionProvider, all_language_settings};
|
||||||
|
use project::DisableAiSettings;
|
||||||
use settings::{Settings as _, SettingsStore};
|
use settings::{Settings as _, SettingsStore};
|
||||||
use smol::stream::StreamExt;
|
|
||||||
use std::{cell::RefCell, rc::Rc, sync::Arc};
|
use std::{cell::RefCell, rc::Rc, sync::Arc};
|
||||||
use supermaven::{Supermaven, SupermavenCompletionProvider};
|
use supermaven::{Supermaven, SupermavenCompletionProvider};
|
||||||
use ui::Window;
|
use ui::Window;
|
||||||
use util::ResultExt;
|
|
||||||
use workspace::Workspace;
|
use workspace::Workspace;
|
||||||
use zeta::{ProviderDataCollection, ZetaInlineCompletionProvider};
|
use zeta::{ProviderDataCollection, ZetaInlineCompletionProvider};
|
||||||
|
|
||||||
|
@ -59,25 +58,20 @@ pub fn init(client: Arc<Client>, user_store: Entity<UserStore>, cx: &mut App) {
|
||||||
cx.on_action(clear_zeta_edit_history);
|
cx.on_action(clear_zeta_edit_history);
|
||||||
|
|
||||||
let mut provider = all_language_settings(None, cx).edit_predictions.provider;
|
let mut provider = all_language_settings(None, cx).edit_predictions.provider;
|
||||||
cx.spawn({
|
cx.subscribe(&user_store, {
|
||||||
let user_store = user_store.clone();
|
|
||||||
let editors = editors.clone();
|
let editors = editors.clone();
|
||||||
let client = client.clone();
|
let client = client.clone();
|
||||||
|
move |user_store, event, cx| match event {
|
||||||
async move |cx| {
|
client::user::Event::PrivateUserInfoUpdated => {
|
||||||
let mut status = client.status();
|
assign_edit_prediction_providers(
|
||||||
while let Some(_status) = status.next().await {
|
&editors,
|
||||||
cx.update(|cx| {
|
provider,
|
||||||
assign_edit_prediction_providers(
|
&client,
|
||||||
&editors,
|
user_store.clone(),
|
||||||
provider,
|
cx,
|
||||||
&client,
|
);
|
||||||
user_store.clone(),
|
|
||||||
cx,
|
|
||||||
);
|
|
||||||
})
|
|
||||||
.log_err();
|
|
||||||
}
|
}
|
||||||
|
_ => {}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.detach();
|
.detach();
|
||||||
|
@ -90,10 +84,7 @@ pub fn init(client: Arc<Client>, user_store: Entity<UserStore>, cx: &mut App) {
|
||||||
let new_provider = all_language_settings(None, cx).edit_predictions.provider;
|
let new_provider = all_language_settings(None, cx).edit_predictions.provider;
|
||||||
|
|
||||||
if new_provider != provider {
|
if new_provider != provider {
|
||||||
let tos_accepted = user_store
|
let tos_accepted = user_store.read(cx).has_accepted_terms_of_service();
|
||||||
.read(cx)
|
|
||||||
.current_user_has_accepted_terms()
|
|
||||||
.unwrap_or(false);
|
|
||||||
|
|
||||||
telemetry::event!(
|
telemetry::event!(
|
||||||
"Edit Prediction Provider Changed",
|
"Edit Prediction Provider Changed",
|
||||||
|
@ -244,7 +235,7 @@ fn assign_edit_prediction_provider(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
EditPredictionProvider::Zed => {
|
EditPredictionProvider::Zed => {
|
||||||
if client.status().borrow().is_connected() {
|
if user_store.read(cx).current_user().is_some() {
|
||||||
let mut worktree = None;
|
let mut worktree = None;
|
||||||
|
|
||||||
if let Some(buffer) = &singleton_buffer {
|
if let Some(buffer) = &singleton_buffer {
|
||||||
|
|
|
@ -2,7 +2,6 @@ mod preview;
|
||||||
mod repl_menu;
|
mod repl_menu;
|
||||||
|
|
||||||
use agent_settings::AgentSettings;
|
use agent_settings::AgentSettings;
|
||||||
use client::DisableAiSettings;
|
|
||||||
use editor::actions::{
|
use editor::actions::{
|
||||||
AddSelectionAbove, AddSelectionBelow, CodeActionSource, DuplicateLineDown, GoToDiagnostic,
|
AddSelectionAbove, AddSelectionBelow, CodeActionSource, DuplicateLineDown, GoToDiagnostic,
|
||||||
GoToHunk, GoToPreviousDiagnostic, GoToPreviousHunk, MoveLineDown, MoveLineUp, SelectAll,
|
GoToHunk, GoToPreviousDiagnostic, GoToPreviousHunk, MoveLineDown, MoveLineUp, SelectAll,
|
||||||
|
@ -16,6 +15,7 @@ use gpui::{
|
||||||
FocusHandle, Focusable, InteractiveElement, ParentElement, Render, Styled, Subscription,
|
FocusHandle, Focusable, InteractiveElement, ParentElement, Render, Styled, Subscription,
|
||||||
WeakEntity, Window, anchored, deferred, point,
|
WeakEntity, Window, anchored, deferred, point,
|
||||||
};
|
};
|
||||||
|
use project::DisableAiSettings;
|
||||||
use project::project_settings::DiagnosticSeverity;
|
use project::project_settings::DiagnosticSeverity;
|
||||||
use search::{BufferSearchBar, buffer_search};
|
use search::{BufferSearchBar, buffer_search};
|
||||||
use settings::{Settings, SettingsStore};
|
use settings::{Settings, SettingsStore};
|
||||||
|
|
|
@ -40,7 +40,6 @@ log.workspace = true
|
||||||
menu.workspace = true
|
menu.workspace = true
|
||||||
postage.workspace = true
|
postage.workspace = true
|
||||||
project.workspace = true
|
project.workspace = true
|
||||||
proto.workspace = true
|
|
||||||
regex.workspace = true
|
regex.workspace = true
|
||||||
release_channel.workspace = true
|
release_channel.workspace = true
|
||||||
serde.workspace = true
|
serde.workspace = true
|
||||||
|
@ -59,9 +58,11 @@ worktree.workspace = true
|
||||||
zed_actions.workspace = true
|
zed_actions.workspace = true
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
collections = { workspace = true, features = ["test-support"] }
|
call = { workspace = true, features = ["test-support"] }
|
||||||
client = { workspace = true, features = ["test-support"] }
|
client = { workspace = true, features = ["test-support"] }
|
||||||
clock = { workspace = true, features = ["test-support"] }
|
clock = { workspace = true, features = ["test-support"] }
|
||||||
|
cloud_api_types.workspace = true
|
||||||
|
collections = { workspace = true, features = ["test-support"] }
|
||||||
ctor.workspace = true
|
ctor.workspace = true
|
||||||
editor = { workspace = true, features = ["test-support"] }
|
editor = { workspace = true, features = ["test-support"] }
|
||||||
gpui = { workspace = true, features = ["test-support"] }
|
gpui = { workspace = true, features = ["test-support"] }
|
||||||
|
@ -77,5 +78,4 @@ tree-sitter-rust.workspace = true
|
||||||
unindent.workspace = true
|
unindent.workspace = true
|
||||||
workspace = { workspace = true, features = ["test-support"] }
|
workspace = { workspace = true, features = ["test-support"] }
|
||||||
worktree = { workspace = true, features = ["test-support"] }
|
worktree = { workspace = true, features = ["test-support"] }
|
||||||
call = { workspace = true, features = ["test-support"] }
|
|
||||||
zlog.workspace = true
|
zlog.workspace = true
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
use std::any::{Any, TypeId};
|
use std::any::{Any, TypeId};
|
||||||
|
|
||||||
use client::DisableAiSettings;
|
|
||||||
use command_palette_hooks::CommandPaletteFilter;
|
use command_palette_hooks::CommandPaletteFilter;
|
||||||
use feature_flags::{FeatureFlagAppExt as _, PredictEditsRateCompletionsFeatureFlag};
|
use feature_flags::{FeatureFlagAppExt as _, PredictEditsRateCompletionsFeatureFlag};
|
||||||
use gpui::actions;
|
use gpui::actions;
|
||||||
use language::language_settings::{AllLanguageSettings, EditPredictionProvider};
|
use language::language_settings::{AllLanguageSettings, EditPredictionProvider};
|
||||||
|
use project::DisableAiSettings;
|
||||||
use settings::{Settings, SettingsStore, update_settings_file};
|
use settings::{Settings, SettingsStore, update_settings_file};
|
||||||
use ui::App;
|
use ui::App;
|
||||||
use workspace::Workspace;
|
use workspace::Workspace;
|
||||||
|
|
|
@ -121,9 +121,10 @@ impl Dismissable for ZedPredictUpsell {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn should_show_upsell_modal(user_store: &Entity<UserStore>, cx: &App) -> bool {
|
pub fn should_show_upsell_modal(user_store: &Entity<UserStore>, cx: &App) -> bool {
|
||||||
match user_store.read(cx).current_user_has_accepted_terms() {
|
if user_store.read(cx).has_accepted_terms_of_service() {
|
||||||
Some(true) => !ZedPredictUpsell::dismissed(),
|
!ZedPredictUpsell::dismissed()
|
||||||
Some(false) | None => true,
|
} else {
|
||||||
|
true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -226,12 +227,9 @@ pub struct Zeta {
|
||||||
data_collection_choice: Entity<DataCollectionChoice>,
|
data_collection_choice: Entity<DataCollectionChoice>,
|
||||||
llm_token: LlmApiToken,
|
llm_token: LlmApiToken,
|
||||||
_llm_token_subscription: Subscription,
|
_llm_token_subscription: Subscription,
|
||||||
/// Whether the terms of service have been accepted.
|
|
||||||
tos_accepted: bool,
|
|
||||||
/// Whether an update to a newer version of Zed is required to continue using Zeta.
|
/// Whether an update to a newer version of Zed is required to continue using Zeta.
|
||||||
update_required: bool,
|
update_required: bool,
|
||||||
user_store: Entity<UserStore>,
|
user_store: Entity<UserStore>,
|
||||||
_user_store_subscription: Subscription,
|
|
||||||
license_detection_watchers: HashMap<WorktreeId, Rc<LicenseDetectionWatcher>>,
|
license_detection_watchers: HashMap<WorktreeId, Rc<LicenseDetectionWatcher>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -306,22 +304,7 @@ impl Zeta {
|
||||||
.detach_and_log_err(cx);
|
.detach_and_log_err(cx);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
tos_accepted: user_store
|
|
||||||
.read(cx)
|
|
||||||
.current_user_has_accepted_terms()
|
|
||||||
.unwrap_or(false),
|
|
||||||
update_required: false,
|
update_required: false,
|
||||||
_user_store_subscription: cx.subscribe(&user_store, |this, user_store, event, cx| {
|
|
||||||
match event {
|
|
||||||
client::user::Event::PrivateUserInfoUpdated => {
|
|
||||||
this.tos_accepted = user_store
|
|
||||||
.read(cx)
|
|
||||||
.current_user_has_accepted_terms()
|
|
||||||
.unwrap_or(false);
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
license_detection_watchers: HashMap::default(),
|
license_detection_watchers: HashMap::default(),
|
||||||
user_store,
|
user_store,
|
||||||
}
|
}
|
||||||
|
@ -1573,7 +1556,12 @@ impl inline_completion::EditPredictionProvider for ZetaInlineCompletionProvider
|
||||||
}
|
}
|
||||||
|
|
||||||
fn needs_terms_acceptance(&self, cx: &App) -> bool {
|
fn needs_terms_acceptance(&self, cx: &App) -> bool {
|
||||||
!self.zeta.read(cx).tos_accepted
|
!self
|
||||||
|
.zeta
|
||||||
|
.read(cx)
|
||||||
|
.user_store
|
||||||
|
.read(cx)
|
||||||
|
.has_accepted_terms_of_service()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn is_refreshing(&self) -> bool {
|
fn is_refreshing(&self) -> bool {
|
||||||
|
@ -1588,7 +1576,7 @@ impl inline_completion::EditPredictionProvider for ZetaInlineCompletionProvider
|
||||||
_debounce: bool,
|
_debounce: bool,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) {
|
) {
|
||||||
if !self.zeta.read(cx).tos_accepted {
|
if self.needs_terms_acceptance(cx) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1600,8 +1588,8 @@ impl inline_completion::EditPredictionProvider for ZetaInlineCompletionProvider
|
||||||
.zeta
|
.zeta
|
||||||
.read(cx)
|
.read(cx)
|
||||||
.user_store
|
.user_store
|
||||||
.read_with(cx, |user_store, _| {
|
.read_with(cx, |cloud_user_store, _cx| {
|
||||||
user_store.account_too_young() || user_store.has_overdue_invoices()
|
cloud_user_store.account_too_young() || cloud_user_store.has_overdue_invoices()
|
||||||
})
|
})
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
|
@ -1817,13 +1805,14 @@ fn tokens_for_bytes(bytes: usize) -> usize {
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
|
use client::UserStore;
|
||||||
use client::test::FakeServer;
|
use client::test::FakeServer;
|
||||||
use clock::FakeSystemClock;
|
use clock::FakeSystemClock;
|
||||||
|
use cloud_api_types::{CreateLlmTokenResponse, LlmToken};
|
||||||
use gpui::TestAppContext;
|
use gpui::TestAppContext;
|
||||||
use http_client::FakeHttpClient;
|
use http_client::FakeHttpClient;
|
||||||
use indoc::indoc;
|
use indoc::indoc;
|
||||||
use language::Point;
|
use language::Point;
|
||||||
use rpc::proto;
|
|
||||||
use settings::SettingsStore;
|
use settings::SettingsStore;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
@ -2027,28 +2016,45 @@ mod tests {
|
||||||
<|editable_region_end|>
|
<|editable_region_end|>
|
||||||
```"};
|
```"};
|
||||||
|
|
||||||
let http_client = FakeHttpClient::create(move |_| async move {
|
let http_client = FakeHttpClient::create(move |req| async move {
|
||||||
Ok(http_client::Response::builder()
|
match (req.method(), req.uri().path()) {
|
||||||
.status(200)
|
(&Method::POST, "/client/llm_tokens") => Ok(http_client::Response::builder()
|
||||||
.body(
|
.status(200)
|
||||||
serde_json::to_string(&PredictEditsResponse {
|
.body(
|
||||||
request_id: Uuid::parse_str("7e86480f-3536-4d2c-9334-8213e3445d45")
|
serde_json::to_string(&CreateLlmTokenResponse {
|
||||||
.unwrap(),
|
token: LlmToken("the-llm-token".to_string()),
|
||||||
output_excerpt: completion_response.to_string(),
|
})
|
||||||
})
|
.unwrap()
|
||||||
.unwrap()
|
.into(),
|
||||||
.into(),
|
)
|
||||||
)
|
.unwrap()),
|
||||||
.unwrap())
|
(&Method::POST, "/predict_edits/v2") => Ok(http_client::Response::builder()
|
||||||
|
.status(200)
|
||||||
|
.body(
|
||||||
|
serde_json::to_string(&PredictEditsResponse {
|
||||||
|
request_id: Uuid::parse_str("7e86480f-3536-4d2c-9334-8213e3445d45")
|
||||||
|
.unwrap(),
|
||||||
|
output_excerpt: completion_response.to_string(),
|
||||||
|
})
|
||||||
|
.unwrap()
|
||||||
|
.into(),
|
||||||
|
)
|
||||||
|
.unwrap()),
|
||||||
|
_ => Ok(http_client::Response::builder()
|
||||||
|
.status(404)
|
||||||
|
.body("Not Found".into())
|
||||||
|
.unwrap()),
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
let client = cx.update(|cx| Client::new(Arc::new(FakeSystemClock::new()), http_client, cx));
|
let client = cx.update(|cx| Client::new(Arc::new(FakeSystemClock::new()), http_client, cx));
|
||||||
cx.update(|cx| {
|
cx.update(|cx| {
|
||||||
RefreshLlmTokenListener::register(client.clone(), cx);
|
RefreshLlmTokenListener::register(client.clone(), cx);
|
||||||
});
|
});
|
||||||
let server = FakeServer::for_client(42, &client, cx).await;
|
// Construct the fake server to authenticate.
|
||||||
|
let _server = FakeServer::for_client(42, &client, cx).await;
|
||||||
let user_store = cx.new(|cx| UserStore::new(client.clone(), cx));
|
let user_store = cx.new(|cx| UserStore::new(client.clone(), cx));
|
||||||
let zeta = cx.new(|cx| Zeta::new(None, client, user_store, cx));
|
let zeta = cx.new(|cx| Zeta::new(None, client, user_store.clone(), cx));
|
||||||
|
|
||||||
let buffer = cx.new(|cx| Buffer::local(buffer_content, cx));
|
let buffer = cx.new(|cx| Buffer::local(buffer_content, cx));
|
||||||
let cursor = buffer.read_with(cx, |buffer, _| buffer.anchor_before(Point::new(1, 0)));
|
let cursor = buffer.read_with(cx, |buffer, _| buffer.anchor_before(Point::new(1, 0)));
|
||||||
|
@ -2056,13 +2062,6 @@ mod tests {
|
||||||
zeta.request_completion(None, &buffer, cursor, false, cx)
|
zeta.request_completion(None, &buffer, cursor, false, cx)
|
||||||
});
|
});
|
||||||
|
|
||||||
server.receive::<proto::GetUsers>().await.unwrap();
|
|
||||||
let token_request = server.receive::<proto::GetLlmToken>().await.unwrap();
|
|
||||||
server.respond(
|
|
||||||
token_request.receipt(),
|
|
||||||
proto::GetLlmTokenResponse { token: "".into() },
|
|
||||||
);
|
|
||||||
|
|
||||||
let completion = completion_task.await.unwrap().unwrap();
|
let completion = completion_task.await.unwrap().unwrap();
|
||||||
buffer.update(cx, |buffer, cx| {
|
buffer.update(cx, |buffer, cx| {
|
||||||
buffer.edit(completion.edits.iter().cloned(), None, cx)
|
buffer.edit(completion.edits.iter().cloned(), None, cx)
|
||||||
|
@ -2079,20 +2078,36 @@ mod tests {
|
||||||
cx: &mut TestAppContext,
|
cx: &mut TestAppContext,
|
||||||
) -> Vec<(Range<Point>, String)> {
|
) -> Vec<(Range<Point>, String)> {
|
||||||
let completion_response = completion_response.to_string();
|
let completion_response = completion_response.to_string();
|
||||||
let http_client = FakeHttpClient::create(move |_| {
|
let http_client = FakeHttpClient::create(move |req| {
|
||||||
let completion = completion_response.clone();
|
let completion = completion_response.clone();
|
||||||
async move {
|
async move {
|
||||||
Ok(http_client::Response::builder()
|
match (req.method(), req.uri().path()) {
|
||||||
.status(200)
|
(&Method::POST, "/client/llm_tokens") => Ok(http_client::Response::builder()
|
||||||
.body(
|
.status(200)
|
||||||
serde_json::to_string(&PredictEditsResponse {
|
.body(
|
||||||
request_id: Uuid::new_v4(),
|
serde_json::to_string(&CreateLlmTokenResponse {
|
||||||
output_excerpt: completion,
|
token: LlmToken("the-llm-token".to_string()),
|
||||||
})
|
})
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.into(),
|
.into(),
|
||||||
)
|
)
|
||||||
.unwrap())
|
.unwrap()),
|
||||||
|
(&Method::POST, "/predict_edits/v2") => Ok(http_client::Response::builder()
|
||||||
|
.status(200)
|
||||||
|
.body(
|
||||||
|
serde_json::to_string(&PredictEditsResponse {
|
||||||
|
request_id: Uuid::new_v4(),
|
||||||
|
output_excerpt: completion,
|
||||||
|
})
|
||||||
|
.unwrap()
|
||||||
|
.into(),
|
||||||
|
)
|
||||||
|
.unwrap()),
|
||||||
|
_ => Ok(http_client::Response::builder()
|
||||||
|
.status(404)
|
||||||
|
.body("Not Found".into())
|
||||||
|
.unwrap()),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -2100,9 +2115,10 @@ mod tests {
|
||||||
cx.update(|cx| {
|
cx.update(|cx| {
|
||||||
RefreshLlmTokenListener::register(client.clone(), cx);
|
RefreshLlmTokenListener::register(client.clone(), cx);
|
||||||
});
|
});
|
||||||
let server = FakeServer::for_client(42, &client, cx).await;
|
// Construct the fake server to authenticate.
|
||||||
|
let _server = FakeServer::for_client(42, &client, cx).await;
|
||||||
let user_store = cx.new(|cx| UserStore::new(client.clone(), cx));
|
let user_store = cx.new(|cx| UserStore::new(client.clone(), cx));
|
||||||
let zeta = cx.new(|cx| Zeta::new(None, client, user_store, cx));
|
let zeta = cx.new(|cx| Zeta::new(None, client, user_store.clone(), cx));
|
||||||
|
|
||||||
let buffer = cx.new(|cx| Buffer::local(buffer_content, cx));
|
let buffer = cx.new(|cx| Buffer::local(buffer_content, cx));
|
||||||
let snapshot = buffer.read_with(cx, |buffer, _| buffer.snapshot());
|
let snapshot = buffer.read_with(cx, |buffer, _| buffer.snapshot());
|
||||||
|
@ -2111,13 +2127,6 @@ mod tests {
|
||||||
zeta.request_completion(None, &buffer, cursor, false, cx)
|
zeta.request_completion(None, &buffer, cursor, false, cx)
|
||||||
});
|
});
|
||||||
|
|
||||||
server.receive::<proto::GetUsers>().await.unwrap();
|
|
||||||
let token_request = server.receive::<proto::GetLlmToken>().await.unwrap();
|
|
||||||
server.respond(
|
|
||||||
token_request.receipt(),
|
|
||||||
proto::GetLlmTokenResponse { token: "".into() },
|
|
||||||
);
|
|
||||||
|
|
||||||
let completion = completion_task.await.unwrap().unwrap();
|
let completion = completion_task.await.unwrap().unwrap();
|
||||||
completion
|
completion
|
||||||
.edits
|
.edits
|
||||||
|
|
|
@ -213,7 +213,7 @@ setTimeout(() => {
|
||||||
platform === "win32"
|
platform === "win32"
|
||||||
? "http://127.0.0.1:8080/rpc"
|
? "http://127.0.0.1:8080/rpc"
|
||||||
: "http://localhost:8080/rpc",
|
: "http://localhost:8080/rpc",
|
||||||
ZED_ADMIN_API_TOKEN: "secret",
|
ZED_ADMIN_API_TOKEN: "internal-api-key-secret",
|
||||||
ZED_WINDOW_SIZE: size,
|
ZED_WINDOW_SIZE: size,
|
||||||
ZED_CLIENT_CHECKSUM_SEED: "development-checksum-seed",
|
ZED_CLIENT_CHECKSUM_SEED: "development-checksum-seed",
|
||||||
RUST_LOG: process.env.RUST_LOG || "info",
|
RUST_LOG: process.env.RUST_LOG || "info",
|
||||||
|
|
|
@ -70,7 +70,7 @@ handlebars = { version = "4", features = ["rust-embed"] }
|
||||||
hashbrown-3575ec1268b04181 = { package = "hashbrown", version = "0.15", features = ["serde"] }
|
hashbrown-3575ec1268b04181 = { package = "hashbrown", version = "0.15", features = ["serde"] }
|
||||||
hashbrown-582f2526e08bb6a0 = { package = "hashbrown", version = "0.14", features = ["raw"] }
|
hashbrown-582f2526e08bb6a0 = { package = "hashbrown", version = "0.14", features = ["raw"] }
|
||||||
hmac = { version = "0.12", default-features = false, features = ["reset"] }
|
hmac = { version = "0.12", default-features = false, features = ["reset"] }
|
||||||
hyper = { version = "0.14", features = ["client", "http1", "http2", "runtime", "server", "stream"] }
|
hyper-582f2526e08bb6a0 = { package = "hyper", version = "0.14", features = ["client", "http1", "http2", "runtime", "server", "stream"] }
|
||||||
idna = { version = "1" }
|
idna = { version = "1" }
|
||||||
indexmap = { version = "2", features = ["serde"] }
|
indexmap = { version = "2", features = ["serde"] }
|
||||||
jiff = { version = "0.2" }
|
jiff = { version = "0.2" }
|
||||||
|
@ -199,7 +199,7 @@ hashbrown-3575ec1268b04181 = { package = "hashbrown", version = "0.15", features
|
||||||
hashbrown-582f2526e08bb6a0 = { package = "hashbrown", version = "0.14", features = ["raw"] }
|
hashbrown-582f2526e08bb6a0 = { package = "hashbrown", version = "0.14", features = ["raw"] }
|
||||||
heck = { version = "0.4", features = ["unicode"] }
|
heck = { version = "0.4", features = ["unicode"] }
|
||||||
hmac = { version = "0.12", default-features = false, features = ["reset"] }
|
hmac = { version = "0.12", default-features = false, features = ["reset"] }
|
||||||
hyper = { version = "0.14", features = ["client", "http1", "http2", "runtime", "server", "stream"] }
|
hyper-582f2526e08bb6a0 = { package = "hyper", version = "0.14", features = ["client", "http1", "http2", "runtime", "server", "stream"] }
|
||||||
idna = { version = "1" }
|
idna = { version = "1" }
|
||||||
indexmap = { version = "2", features = ["serde"] }
|
indexmap = { version = "2", features = ["serde"] }
|
||||||
itertools-594e8ee84c453af0 = { package = "itertools", version = "0.13" }
|
itertools-594e8ee84c453af0 = { package = "itertools", version = "0.13" }
|
||||||
|
@ -287,7 +287,9 @@ core-foundation-sys = { version = "0.8" }
|
||||||
foldhash = { version = "0.1", default-features = false, features = ["std"] }
|
foldhash = { version = "0.1", default-features = false, features = ["std"] }
|
||||||
getrandom-468e82937335b1c9 = { package = "getrandom", version = "0.3", default-features = false, features = ["std"] }
|
getrandom-468e82937335b1c9 = { package = "getrandom", version = "0.3", default-features = false, features = ["std"] }
|
||||||
gimli = { version = "0.31", default-features = false, features = ["read", "std", "write"] }
|
gimli = { version = "0.31", default-features = false, features = ["read", "std", "write"] }
|
||||||
|
hyper-dff4ba8e3ae991db = { package = "hyper", version = "1", features = ["client", "http1", "http2"] }
|
||||||
hyper-rustls = { version = "0.27", default-features = false, features = ["http1", "http2", "native-tokio", "ring", "tls12"] }
|
hyper-rustls = { version = "0.27", default-features = false, features = ["http1", "http2", "native-tokio", "ring", "tls12"] }
|
||||||
|
hyper-util = { version = "0.1", features = ["client-legacy", "http1", "http2"] }
|
||||||
itertools-5ef9efb8ec2df382 = { package = "itertools", version = "0.12" }
|
itertools-5ef9efb8ec2df382 = { package = "itertools", version = "0.12" }
|
||||||
naga = { version = "25", features = ["msl-out", "wgsl-in"] }
|
naga = { version = "25", features = ["msl-out", "wgsl-in"] }
|
||||||
nix = { version = "0.29", features = ["fs", "pthread", "signal", "user"] }
|
nix = { version = "0.29", features = ["fs", "pthread", "signal", "user"] }
|
||||||
|
@ -303,7 +305,7 @@ scopeguard = { version = "1" }
|
||||||
security-framework = { version = "3", features = ["OSX_10_14"] }
|
security-framework = { version = "3", features = ["OSX_10_14"] }
|
||||||
security-framework-sys = { version = "2", features = ["OSX_10_14"] }
|
security-framework-sys = { version = "2", features = ["OSX_10_14"] }
|
||||||
sync_wrapper = { version = "1", default-features = false, features = ["futures"] }
|
sync_wrapper = { version = "1", default-features = false, features = ["futures"] }
|
||||||
tokio-rustls = { version = "0.26", default-features = false, features = ["ring"] }
|
tokio-rustls = { version = "0.26", default-features = false, features = ["logging", "ring"] }
|
||||||
tokio-socks = { version = "0.5", features = ["futures-io"] }
|
tokio-socks = { version = "0.5", features = ["futures-io"] }
|
||||||
tokio-stream = { version = "0.1", features = ["fs"] }
|
tokio-stream = { version = "0.1", features = ["fs"] }
|
||||||
tower = { version = "0.5", default-features = false, features = ["timeout", "util"] }
|
tower = { version = "0.5", default-features = false, features = ["timeout", "util"] }
|
||||||
|
@ -315,7 +317,9 @@ core-foundation-sys = { version = "0.8" }
|
||||||
foldhash = { version = "0.1", default-features = false, features = ["std"] }
|
foldhash = { version = "0.1", default-features = false, features = ["std"] }
|
||||||
getrandom-468e82937335b1c9 = { package = "getrandom", version = "0.3", default-features = false, features = ["std"] }
|
getrandom-468e82937335b1c9 = { package = "getrandom", version = "0.3", default-features = false, features = ["std"] }
|
||||||
gimli = { version = "0.31", default-features = false, features = ["read", "std", "write"] }
|
gimli = { version = "0.31", default-features = false, features = ["read", "std", "write"] }
|
||||||
|
hyper-dff4ba8e3ae991db = { package = "hyper", version = "1", features = ["client", "http1", "http2"] }
|
||||||
hyper-rustls = { version = "0.27", default-features = false, features = ["http1", "http2", "native-tokio", "ring", "tls12"] }
|
hyper-rustls = { version = "0.27", default-features = false, features = ["http1", "http2", "native-tokio", "ring", "tls12"] }
|
||||||
|
hyper-util = { version = "0.1", features = ["client-legacy", "http1", "http2"] }
|
||||||
itertools-5ef9efb8ec2df382 = { package = "itertools", version = "0.12" }
|
itertools-5ef9efb8ec2df382 = { package = "itertools", version = "0.12" }
|
||||||
naga = { version = "25", features = ["msl-out", "wgsl-in"] }
|
naga = { version = "25", features = ["msl-out", "wgsl-in"] }
|
||||||
nix = { version = "0.29", features = ["fs", "pthread", "signal", "user"] }
|
nix = { version = "0.29", features = ["fs", "pthread", "signal", "user"] }
|
||||||
|
@ -332,7 +336,7 @@ scopeguard = { version = "1" }
|
||||||
security-framework = { version = "3", features = ["OSX_10_14"] }
|
security-framework = { version = "3", features = ["OSX_10_14"] }
|
||||||
security-framework-sys = { version = "2", features = ["OSX_10_14"] }
|
security-framework-sys = { version = "2", features = ["OSX_10_14"] }
|
||||||
sync_wrapper = { version = "1", default-features = false, features = ["futures"] }
|
sync_wrapper = { version = "1", default-features = false, features = ["futures"] }
|
||||||
tokio-rustls = { version = "0.26", default-features = false, features = ["ring"] }
|
tokio-rustls = { version = "0.26", default-features = false, features = ["logging", "ring"] }
|
||||||
tokio-socks = { version = "0.5", features = ["futures-io"] }
|
tokio-socks = { version = "0.5", features = ["futures-io"] }
|
||||||
tokio-stream = { version = "0.1", features = ["fs"] }
|
tokio-stream = { version = "0.1", features = ["fs"] }
|
||||||
tower = { version = "0.5", default-features = false, features = ["timeout", "util"] }
|
tower = { version = "0.5", default-features = false, features = ["timeout", "util"] }
|
||||||
|
@ -344,7 +348,9 @@ core-foundation-sys = { version = "0.8" }
|
||||||
foldhash = { version = "0.1", default-features = false, features = ["std"] }
|
foldhash = { version = "0.1", default-features = false, features = ["std"] }
|
||||||
getrandom-468e82937335b1c9 = { package = "getrandom", version = "0.3", default-features = false, features = ["std"] }
|
getrandom-468e82937335b1c9 = { package = "getrandom", version = "0.3", default-features = false, features = ["std"] }
|
||||||
gimli = { version = "0.31", default-features = false, features = ["read", "std", "write"] }
|
gimli = { version = "0.31", default-features = false, features = ["read", "std", "write"] }
|
||||||
|
hyper-dff4ba8e3ae991db = { package = "hyper", version = "1", features = ["client", "http1", "http2"] }
|
||||||
hyper-rustls = { version = "0.27", default-features = false, features = ["http1", "http2", "native-tokio", "ring", "tls12"] }
|
hyper-rustls = { version = "0.27", default-features = false, features = ["http1", "http2", "native-tokio", "ring", "tls12"] }
|
||||||
|
hyper-util = { version = "0.1", features = ["client-legacy", "http1", "http2"] }
|
||||||
itertools-5ef9efb8ec2df382 = { package = "itertools", version = "0.12" }
|
itertools-5ef9efb8ec2df382 = { package = "itertools", version = "0.12" }
|
||||||
naga = { version = "25", features = ["msl-out", "wgsl-in"] }
|
naga = { version = "25", features = ["msl-out", "wgsl-in"] }
|
||||||
nix = { version = "0.29", features = ["fs", "pthread", "signal", "user"] }
|
nix = { version = "0.29", features = ["fs", "pthread", "signal", "user"] }
|
||||||
|
@ -360,7 +366,7 @@ scopeguard = { version = "1" }
|
||||||
security-framework = { version = "3", features = ["OSX_10_14"] }
|
security-framework = { version = "3", features = ["OSX_10_14"] }
|
||||||
security-framework-sys = { version = "2", features = ["OSX_10_14"] }
|
security-framework-sys = { version = "2", features = ["OSX_10_14"] }
|
||||||
sync_wrapper = { version = "1", default-features = false, features = ["futures"] }
|
sync_wrapper = { version = "1", default-features = false, features = ["futures"] }
|
||||||
tokio-rustls = { version = "0.26", default-features = false, features = ["ring"] }
|
tokio-rustls = { version = "0.26", default-features = false, features = ["logging", "ring"] }
|
||||||
tokio-socks = { version = "0.5", features = ["futures-io"] }
|
tokio-socks = { version = "0.5", features = ["futures-io"] }
|
||||||
tokio-stream = { version = "0.1", features = ["fs"] }
|
tokio-stream = { version = "0.1", features = ["fs"] }
|
||||||
tower = { version = "0.5", default-features = false, features = ["timeout", "util"] }
|
tower = { version = "0.5", default-features = false, features = ["timeout", "util"] }
|
||||||
|
@ -372,7 +378,9 @@ core-foundation-sys = { version = "0.8" }
|
||||||
foldhash = { version = "0.1", default-features = false, features = ["std"] }
|
foldhash = { version = "0.1", default-features = false, features = ["std"] }
|
||||||
getrandom-468e82937335b1c9 = { package = "getrandom", version = "0.3", default-features = false, features = ["std"] }
|
getrandom-468e82937335b1c9 = { package = "getrandom", version = "0.3", default-features = false, features = ["std"] }
|
||||||
gimli = { version = "0.31", default-features = false, features = ["read", "std", "write"] }
|
gimli = { version = "0.31", default-features = false, features = ["read", "std", "write"] }
|
||||||
|
hyper-dff4ba8e3ae991db = { package = "hyper", version = "1", features = ["client", "http1", "http2"] }
|
||||||
hyper-rustls = { version = "0.27", default-features = false, features = ["http1", "http2", "native-tokio", "ring", "tls12"] }
|
hyper-rustls = { version = "0.27", default-features = false, features = ["http1", "http2", "native-tokio", "ring", "tls12"] }
|
||||||
|
hyper-util = { version = "0.1", features = ["client-legacy", "http1", "http2"] }
|
||||||
itertools-5ef9efb8ec2df382 = { package = "itertools", version = "0.12" }
|
itertools-5ef9efb8ec2df382 = { package = "itertools", version = "0.12" }
|
||||||
naga = { version = "25", features = ["msl-out", "wgsl-in"] }
|
naga = { version = "25", features = ["msl-out", "wgsl-in"] }
|
||||||
nix = { version = "0.29", features = ["fs", "pthread", "signal", "user"] }
|
nix = { version = "0.29", features = ["fs", "pthread", "signal", "user"] }
|
||||||
|
@ -389,7 +397,7 @@ scopeguard = { version = "1" }
|
||||||
security-framework = { version = "3", features = ["OSX_10_14"] }
|
security-framework = { version = "3", features = ["OSX_10_14"] }
|
||||||
security-framework-sys = { version = "2", features = ["OSX_10_14"] }
|
security-framework-sys = { version = "2", features = ["OSX_10_14"] }
|
||||||
sync_wrapper = { version = "1", default-features = false, features = ["futures"] }
|
sync_wrapper = { version = "1", default-features = false, features = ["futures"] }
|
||||||
tokio-rustls = { version = "0.26", default-features = false, features = ["ring"] }
|
tokio-rustls = { version = "0.26", default-features = false, features = ["logging", "ring"] }
|
||||||
tokio-socks = { version = "0.5", features = ["futures-io"] }
|
tokio-socks = { version = "0.5", features = ["futures-io"] }
|
||||||
tokio-stream = { version = "0.1", features = ["fs"] }
|
tokio-stream = { version = "0.1", features = ["fs"] }
|
||||||
tower = { version = "0.5", default-features = false, features = ["timeout", "util"] }
|
tower = { version = "0.5", default-features = false, features = ["timeout", "util"] }
|
||||||
|
@ -407,7 +415,9 @@ foldhash = { version = "0.1", default-features = false, features = ["std"] }
|
||||||
getrandom-468e82937335b1c9 = { package = "getrandom", version = "0.3", default-features = false, features = ["std"] }
|
getrandom-468e82937335b1c9 = { package = "getrandom", version = "0.3", default-features = false, features = ["std"] }
|
||||||
getrandom-6f8ce4dd05d13bba = { package = "getrandom", version = "0.2", default-features = false, features = ["js", "rdrand"] }
|
getrandom-6f8ce4dd05d13bba = { package = "getrandom", version = "0.2", default-features = false, features = ["js", "rdrand"] }
|
||||||
gimli = { version = "0.31", default-features = false, features = ["read", "std", "write"] }
|
gimli = { version = "0.31", default-features = false, features = ["read", "std", "write"] }
|
||||||
|
hyper-dff4ba8e3ae991db = { package = "hyper", version = "1", features = ["client", "http1", "http2"] }
|
||||||
hyper-rustls = { version = "0.27", default-features = false, features = ["http1", "http2", "native-tokio", "ring", "tls12"] }
|
hyper-rustls = { version = "0.27", default-features = false, features = ["http1", "http2", "native-tokio", "ring", "tls12"] }
|
||||||
|
hyper-util = { version = "0.1", features = ["client-legacy", "http1", "http2"] }
|
||||||
inout = { version = "0.1", default-features = false, features = ["block-padding"] }
|
inout = { version = "0.1", default-features = false, features = ["block-padding"] }
|
||||||
itertools-5ef9efb8ec2df382 = { package = "itertools", version = "0.12" }
|
itertools-5ef9efb8ec2df382 = { package = "itertools", version = "0.12" }
|
||||||
linux-raw-sys-274715c4dabd11b0 = { package = "linux-raw-sys", version = "0.9", default-features = false, features = ["elf", "errno", "general", "if_ether", "ioctl", "net", "netlink", "no_std", "prctl", "xdp"] }
|
linux-raw-sys-274715c4dabd11b0 = { package = "linux-raw-sys", version = "0.9", default-features = false, features = ["elf", "errno", "general", "if_ether", "ioctl", "net", "netlink", "no_std", "prctl", "xdp"] }
|
||||||
|
@ -426,7 +436,7 @@ rustix-dff4ba8e3ae991db = { package = "rustix", version = "1", features = ["fs",
|
||||||
scopeguard = { version = "1" }
|
scopeguard = { version = "1" }
|
||||||
syn-f595c2ba2a3f28df = { package = "syn", version = "2", features = ["extra-traits", "fold", "full", "visit", "visit-mut"] }
|
syn-f595c2ba2a3f28df = { package = "syn", version = "2", features = ["extra-traits", "fold", "full", "visit", "visit-mut"] }
|
||||||
sync_wrapper = { version = "1", default-features = false, features = ["futures"] }
|
sync_wrapper = { version = "1", default-features = false, features = ["futures"] }
|
||||||
tokio-rustls = { version = "0.26", default-features = false, features = ["ring"] }
|
tokio-rustls = { version = "0.26", default-features = false, features = ["logging", "ring"] }
|
||||||
tokio-socks = { version = "0.5", features = ["futures-io"] }
|
tokio-socks = { version = "0.5", features = ["futures-io"] }
|
||||||
tokio-stream = { version = "0.1", features = ["fs"] }
|
tokio-stream = { version = "0.1", features = ["fs"] }
|
||||||
toml_datetime = { version = "0.6", default-features = false, features = ["serde"] }
|
toml_datetime = { version = "0.6", default-features = false, features = ["serde"] }
|
||||||
|
@ -447,7 +457,9 @@ foldhash = { version = "0.1", default-features = false, features = ["std"] }
|
||||||
getrandom-468e82937335b1c9 = { package = "getrandom", version = "0.3", default-features = false, features = ["std"] }
|
getrandom-468e82937335b1c9 = { package = "getrandom", version = "0.3", default-features = false, features = ["std"] }
|
||||||
getrandom-6f8ce4dd05d13bba = { package = "getrandom", version = "0.2", default-features = false, features = ["js", "rdrand"] }
|
getrandom-6f8ce4dd05d13bba = { package = "getrandom", version = "0.2", default-features = false, features = ["js", "rdrand"] }
|
||||||
gimli = { version = "0.31", default-features = false, features = ["read", "std", "write"] }
|
gimli = { version = "0.31", default-features = false, features = ["read", "std", "write"] }
|
||||||
|
hyper-dff4ba8e3ae991db = { package = "hyper", version = "1", features = ["client", "http1", "http2"] }
|
||||||
hyper-rustls = { version = "0.27", default-features = false, features = ["http1", "http2", "native-tokio", "ring", "tls12"] }
|
hyper-rustls = { version = "0.27", default-features = false, features = ["http1", "http2", "native-tokio", "ring", "tls12"] }
|
||||||
|
hyper-util = { version = "0.1", features = ["client-legacy", "http1", "http2"] }
|
||||||
inout = { version = "0.1", default-features = false, features = ["block-padding"] }
|
inout = { version = "0.1", default-features = false, features = ["block-padding"] }
|
||||||
itertools-5ef9efb8ec2df382 = { package = "itertools", version = "0.12" }
|
itertools-5ef9efb8ec2df382 = { package = "itertools", version = "0.12" }
|
||||||
linux-raw-sys-274715c4dabd11b0 = { package = "linux-raw-sys", version = "0.9", default-features = false, features = ["elf", "errno", "general", "if_ether", "ioctl", "net", "netlink", "no_std", "prctl", "xdp"] }
|
linux-raw-sys-274715c4dabd11b0 = { package = "linux-raw-sys", version = "0.9", default-features = false, features = ["elf", "errno", "general", "if_ether", "ioctl", "net", "netlink", "no_std", "prctl", "xdp"] }
|
||||||
|
@ -464,7 +476,7 @@ rustix-d585fab2519d2d1 = { package = "rustix", version = "0.38", features = ["ev
|
||||||
rustix-dff4ba8e3ae991db = { package = "rustix", version = "1", features = ["fs", "net", "process", "termios", "time"] }
|
rustix-dff4ba8e3ae991db = { package = "rustix", version = "1", features = ["fs", "net", "process", "termios", "time"] }
|
||||||
scopeguard = { version = "1" }
|
scopeguard = { version = "1" }
|
||||||
sync_wrapper = { version = "1", default-features = false, features = ["futures"] }
|
sync_wrapper = { version = "1", default-features = false, features = ["futures"] }
|
||||||
tokio-rustls = { version = "0.26", default-features = false, features = ["ring"] }
|
tokio-rustls = { version = "0.26", default-features = false, features = ["logging", "ring"] }
|
||||||
tokio-socks = { version = "0.5", features = ["futures-io"] }
|
tokio-socks = { version = "0.5", features = ["futures-io"] }
|
||||||
tokio-stream = { version = "0.1", features = ["fs"] }
|
tokio-stream = { version = "0.1", features = ["fs"] }
|
||||||
toml_datetime = { version = "0.6", default-features = false, features = ["serde"] }
|
toml_datetime = { version = "0.6", default-features = false, features = ["serde"] }
|
||||||
|
@ -485,7 +497,9 @@ foldhash = { version = "0.1", default-features = false, features = ["std"] }
|
||||||
getrandom-468e82937335b1c9 = { package = "getrandom", version = "0.3", default-features = false, features = ["std"] }
|
getrandom-468e82937335b1c9 = { package = "getrandom", version = "0.3", default-features = false, features = ["std"] }
|
||||||
getrandom-6f8ce4dd05d13bba = { package = "getrandom", version = "0.2", default-features = false, features = ["js", "rdrand"] }
|
getrandom-6f8ce4dd05d13bba = { package = "getrandom", version = "0.2", default-features = false, features = ["js", "rdrand"] }
|
||||||
gimli = { version = "0.31", default-features = false, features = ["read", "std", "write"] }
|
gimli = { version = "0.31", default-features = false, features = ["read", "std", "write"] }
|
||||||
|
hyper-dff4ba8e3ae991db = { package = "hyper", version = "1", features = ["client", "http1", "http2"] }
|
||||||
hyper-rustls = { version = "0.27", default-features = false, features = ["http1", "http2", "native-tokio", "ring", "tls12"] }
|
hyper-rustls = { version = "0.27", default-features = false, features = ["http1", "http2", "native-tokio", "ring", "tls12"] }
|
||||||
|
hyper-util = { version = "0.1", features = ["client-legacy", "http1", "http2"] }
|
||||||
inout = { version = "0.1", default-features = false, features = ["block-padding"] }
|
inout = { version = "0.1", default-features = false, features = ["block-padding"] }
|
||||||
itertools-5ef9efb8ec2df382 = { package = "itertools", version = "0.12" }
|
itertools-5ef9efb8ec2df382 = { package = "itertools", version = "0.12" }
|
||||||
linux-raw-sys-274715c4dabd11b0 = { package = "linux-raw-sys", version = "0.9", default-features = false, features = ["elf", "errno", "general", "if_ether", "ioctl", "net", "netlink", "no_std", "prctl", "xdp"] }
|
linux-raw-sys-274715c4dabd11b0 = { package = "linux-raw-sys", version = "0.9", default-features = false, features = ["elf", "errno", "general", "if_ether", "ioctl", "net", "netlink", "no_std", "prctl", "xdp"] }
|
||||||
|
@ -504,7 +518,7 @@ rustix-dff4ba8e3ae991db = { package = "rustix", version = "1", features = ["fs",
|
||||||
scopeguard = { version = "1" }
|
scopeguard = { version = "1" }
|
||||||
syn-f595c2ba2a3f28df = { package = "syn", version = "2", features = ["extra-traits", "fold", "full", "visit", "visit-mut"] }
|
syn-f595c2ba2a3f28df = { package = "syn", version = "2", features = ["extra-traits", "fold", "full", "visit", "visit-mut"] }
|
||||||
sync_wrapper = { version = "1", default-features = false, features = ["futures"] }
|
sync_wrapper = { version = "1", default-features = false, features = ["futures"] }
|
||||||
tokio-rustls = { version = "0.26", default-features = false, features = ["ring"] }
|
tokio-rustls = { version = "0.26", default-features = false, features = ["logging", "ring"] }
|
||||||
tokio-socks = { version = "0.5", features = ["futures-io"] }
|
tokio-socks = { version = "0.5", features = ["futures-io"] }
|
||||||
tokio-stream = { version = "0.1", features = ["fs"] }
|
tokio-stream = { version = "0.1", features = ["fs"] }
|
||||||
toml_datetime = { version = "0.6", default-features = false, features = ["serde"] }
|
toml_datetime = { version = "0.6", default-features = false, features = ["serde"] }
|
||||||
|
@ -525,7 +539,9 @@ foldhash = { version = "0.1", default-features = false, features = ["std"] }
|
||||||
getrandom-468e82937335b1c9 = { package = "getrandom", version = "0.3", default-features = false, features = ["std"] }
|
getrandom-468e82937335b1c9 = { package = "getrandom", version = "0.3", default-features = false, features = ["std"] }
|
||||||
getrandom-6f8ce4dd05d13bba = { package = "getrandom", version = "0.2", default-features = false, features = ["js", "rdrand"] }
|
getrandom-6f8ce4dd05d13bba = { package = "getrandom", version = "0.2", default-features = false, features = ["js", "rdrand"] }
|
||||||
gimli = { version = "0.31", default-features = false, features = ["read", "std", "write"] }
|
gimli = { version = "0.31", default-features = false, features = ["read", "std", "write"] }
|
||||||
|
hyper-dff4ba8e3ae991db = { package = "hyper", version = "1", features = ["client", "http1", "http2"] }
|
||||||
hyper-rustls = { version = "0.27", default-features = false, features = ["http1", "http2", "native-tokio", "ring", "tls12"] }
|
hyper-rustls = { version = "0.27", default-features = false, features = ["http1", "http2", "native-tokio", "ring", "tls12"] }
|
||||||
|
hyper-util = { version = "0.1", features = ["client-legacy", "http1", "http2"] }
|
||||||
inout = { version = "0.1", default-features = false, features = ["block-padding"] }
|
inout = { version = "0.1", default-features = false, features = ["block-padding"] }
|
||||||
itertools-5ef9efb8ec2df382 = { package = "itertools", version = "0.12" }
|
itertools-5ef9efb8ec2df382 = { package = "itertools", version = "0.12" }
|
||||||
linux-raw-sys-274715c4dabd11b0 = { package = "linux-raw-sys", version = "0.9", default-features = false, features = ["elf", "errno", "general", "if_ether", "ioctl", "net", "netlink", "no_std", "prctl", "xdp"] }
|
linux-raw-sys-274715c4dabd11b0 = { package = "linux-raw-sys", version = "0.9", default-features = false, features = ["elf", "errno", "general", "if_ether", "ioctl", "net", "netlink", "no_std", "prctl", "xdp"] }
|
||||||
|
@ -542,7 +558,7 @@ rustix-d585fab2519d2d1 = { package = "rustix", version = "0.38", features = ["ev
|
||||||
rustix-dff4ba8e3ae991db = { package = "rustix", version = "1", features = ["fs", "net", "process", "termios", "time"] }
|
rustix-dff4ba8e3ae991db = { package = "rustix", version = "1", features = ["fs", "net", "process", "termios", "time"] }
|
||||||
scopeguard = { version = "1" }
|
scopeguard = { version = "1" }
|
||||||
sync_wrapper = { version = "1", default-features = false, features = ["futures"] }
|
sync_wrapper = { version = "1", default-features = false, features = ["futures"] }
|
||||||
tokio-rustls = { version = "0.26", default-features = false, features = ["ring"] }
|
tokio-rustls = { version = "0.26", default-features = false, features = ["logging", "ring"] }
|
||||||
tokio-socks = { version = "0.5", features = ["futures-io"] }
|
tokio-socks = { version = "0.5", features = ["futures-io"] }
|
||||||
tokio-stream = { version = "0.1", features = ["fs"] }
|
tokio-stream = { version = "0.1", features = ["fs"] }
|
||||||
toml_datetime = { version = "0.6", default-features = false, features = ["serde"] }
|
toml_datetime = { version = "0.6", default-features = false, features = ["serde"] }
|
||||||
|
@ -556,14 +572,16 @@ flume = { version = "0.11" }
|
||||||
foldhash = { version = "0.1", default-features = false, features = ["std"] }
|
foldhash = { version = "0.1", default-features = false, features = ["std"] }
|
||||||
getrandom-468e82937335b1c9 = { package = "getrandom", version = "0.3", default-features = false, features = ["std"] }
|
getrandom-468e82937335b1c9 = { package = "getrandom", version = "0.3", default-features = false, features = ["std"] }
|
||||||
getrandom-6f8ce4dd05d13bba = { package = "getrandom", version = "0.2", default-features = false, features = ["js", "rdrand"] }
|
getrandom-6f8ce4dd05d13bba = { package = "getrandom", version = "0.2", default-features = false, features = ["js", "rdrand"] }
|
||||||
|
hyper-dff4ba8e3ae991db = { package = "hyper", version = "1", features = ["client", "http1", "http2"] }
|
||||||
hyper-rustls = { version = "0.27", default-features = false, features = ["http1", "http2", "native-tokio", "ring", "tls12"] }
|
hyper-rustls = { version = "0.27", default-features = false, features = ["http1", "http2", "native-tokio", "ring", "tls12"] }
|
||||||
|
hyper-util = { version = "0.1", features = ["client-legacy", "http1", "http2"] }
|
||||||
itertools-5ef9efb8ec2df382 = { package = "itertools", version = "0.12" }
|
itertools-5ef9efb8ec2df382 = { package = "itertools", version = "0.12" }
|
||||||
naga = { version = "25", features = ["spv-out", "wgsl-in"] }
|
naga = { version = "25", features = ["spv-out", "wgsl-in"] }
|
||||||
ring = { version = "0.17", features = ["std"] }
|
ring = { version = "0.17", features = ["std"] }
|
||||||
rustix-d585fab2519d2d1 = { package = "rustix", version = "0.38", features = ["event"] }
|
rustix-d585fab2519d2d1 = { package = "rustix", version = "0.38", features = ["event"] }
|
||||||
scopeguard = { version = "1" }
|
scopeguard = { version = "1" }
|
||||||
sync_wrapper = { version = "1", default-features = false, features = ["futures"] }
|
sync_wrapper = { version = "1", default-features = false, features = ["futures"] }
|
||||||
tokio-rustls = { version = "0.26", default-features = false, features = ["ring"] }
|
tokio-rustls = { version = "0.26", default-features = false, features = ["logging", "ring"] }
|
||||||
tokio-socks = { version = "0.5", features = ["futures-io"] }
|
tokio-socks = { version = "0.5", features = ["futures-io"] }
|
||||||
tokio-stream = { version = "0.1", features = ["fs"] }
|
tokio-stream = { version = "0.1", features = ["fs"] }
|
||||||
tower = { version = "0.5", default-features = false, features = ["timeout", "util"] }
|
tower = { version = "0.5", default-features = false, features = ["timeout", "util"] }
|
||||||
|
@ -580,7 +598,9 @@ flume = { version = "0.11" }
|
||||||
foldhash = { version = "0.1", default-features = false, features = ["std"] }
|
foldhash = { version = "0.1", default-features = false, features = ["std"] }
|
||||||
getrandom-468e82937335b1c9 = { package = "getrandom", version = "0.3", default-features = false, features = ["std"] }
|
getrandom-468e82937335b1c9 = { package = "getrandom", version = "0.3", default-features = false, features = ["std"] }
|
||||||
getrandom-6f8ce4dd05d13bba = { package = "getrandom", version = "0.2", default-features = false, features = ["js", "rdrand"] }
|
getrandom-6f8ce4dd05d13bba = { package = "getrandom", version = "0.2", default-features = false, features = ["js", "rdrand"] }
|
||||||
|
hyper-dff4ba8e3ae991db = { package = "hyper", version = "1", features = ["client", "http1", "http2"] }
|
||||||
hyper-rustls = { version = "0.27", default-features = false, features = ["http1", "http2", "native-tokio", "ring", "tls12"] }
|
hyper-rustls = { version = "0.27", default-features = false, features = ["http1", "http2", "native-tokio", "ring", "tls12"] }
|
||||||
|
hyper-util = { version = "0.1", features = ["client-legacy", "http1", "http2"] }
|
||||||
itertools-5ef9efb8ec2df382 = { package = "itertools", version = "0.12" }
|
itertools-5ef9efb8ec2df382 = { package = "itertools", version = "0.12" }
|
||||||
naga = { version = "25", features = ["spv-out", "wgsl-in"] }
|
naga = { version = "25", features = ["spv-out", "wgsl-in"] }
|
||||||
proc-macro2 = { version = "1", default-features = false, features = ["span-locations"] }
|
proc-macro2 = { version = "1", default-features = false, features = ["span-locations"] }
|
||||||
|
@ -588,7 +608,7 @@ ring = { version = "0.17", features = ["std"] }
|
||||||
rustix-d585fab2519d2d1 = { package = "rustix", version = "0.38", features = ["event"] }
|
rustix-d585fab2519d2d1 = { package = "rustix", version = "0.38", features = ["event"] }
|
||||||
scopeguard = { version = "1" }
|
scopeguard = { version = "1" }
|
||||||
sync_wrapper = { version = "1", default-features = false, features = ["futures"] }
|
sync_wrapper = { version = "1", default-features = false, features = ["futures"] }
|
||||||
tokio-rustls = { version = "0.26", default-features = false, features = ["ring"] }
|
tokio-rustls = { version = "0.26", default-features = false, features = ["logging", "ring"] }
|
||||||
tokio-socks = { version = "0.5", features = ["futures-io"] }
|
tokio-socks = { version = "0.5", features = ["futures-io"] }
|
||||||
tokio-stream = { version = "0.1", features = ["fs"] }
|
tokio-stream = { version = "0.1", features = ["fs"] }
|
||||||
tower = { version = "0.5", default-features = false, features = ["timeout", "util"] }
|
tower = { version = "0.5", default-features = false, features = ["timeout", "util"] }
|
||||||
|
@ -612,7 +632,9 @@ foldhash = { version = "0.1", default-features = false, features = ["std"] }
|
||||||
getrandom-468e82937335b1c9 = { package = "getrandom", version = "0.3", default-features = false, features = ["std"] }
|
getrandom-468e82937335b1c9 = { package = "getrandom", version = "0.3", default-features = false, features = ["std"] }
|
||||||
getrandom-6f8ce4dd05d13bba = { package = "getrandom", version = "0.2", default-features = false, features = ["js", "rdrand"] }
|
getrandom-6f8ce4dd05d13bba = { package = "getrandom", version = "0.2", default-features = false, features = ["js", "rdrand"] }
|
||||||
gimli = { version = "0.31", default-features = false, features = ["read", "std", "write"] }
|
gimli = { version = "0.31", default-features = false, features = ["read", "std", "write"] }
|
||||||
|
hyper-dff4ba8e3ae991db = { package = "hyper", version = "1", features = ["client", "http1", "http2"] }
|
||||||
hyper-rustls = { version = "0.27", default-features = false, features = ["http1", "http2", "native-tokio", "ring", "tls12"] }
|
hyper-rustls = { version = "0.27", default-features = false, features = ["http1", "http2", "native-tokio", "ring", "tls12"] }
|
||||||
|
hyper-util = { version = "0.1", features = ["client-legacy", "http1", "http2"] }
|
||||||
inout = { version = "0.1", default-features = false, features = ["block-padding"] }
|
inout = { version = "0.1", default-features = false, features = ["block-padding"] }
|
||||||
itertools-5ef9efb8ec2df382 = { package = "itertools", version = "0.12" }
|
itertools-5ef9efb8ec2df382 = { package = "itertools", version = "0.12" }
|
||||||
linux-raw-sys-274715c4dabd11b0 = { package = "linux-raw-sys", version = "0.9", default-features = false, features = ["elf", "errno", "general", "if_ether", "ioctl", "net", "netlink", "no_std", "prctl", "xdp"] }
|
linux-raw-sys-274715c4dabd11b0 = { package = "linux-raw-sys", version = "0.9", default-features = false, features = ["elf", "errno", "general", "if_ether", "ioctl", "net", "netlink", "no_std", "prctl", "xdp"] }
|
||||||
|
@ -631,7 +653,7 @@ rustix-dff4ba8e3ae991db = { package = "rustix", version = "1", features = ["fs",
|
||||||
scopeguard = { version = "1" }
|
scopeguard = { version = "1" }
|
||||||
syn-f595c2ba2a3f28df = { package = "syn", version = "2", features = ["extra-traits", "fold", "full", "visit", "visit-mut"] }
|
syn-f595c2ba2a3f28df = { package = "syn", version = "2", features = ["extra-traits", "fold", "full", "visit", "visit-mut"] }
|
||||||
sync_wrapper = { version = "1", default-features = false, features = ["futures"] }
|
sync_wrapper = { version = "1", default-features = false, features = ["futures"] }
|
||||||
tokio-rustls = { version = "0.26", default-features = false, features = ["ring"] }
|
tokio-rustls = { version = "0.26", default-features = false, features = ["logging", "ring"] }
|
||||||
tokio-socks = { version = "0.5", features = ["futures-io"] }
|
tokio-socks = { version = "0.5", features = ["futures-io"] }
|
||||||
tokio-stream = { version = "0.1", features = ["fs"] }
|
tokio-stream = { version = "0.1", features = ["fs"] }
|
||||||
toml_datetime = { version = "0.6", default-features = false, features = ["serde"] }
|
toml_datetime = { version = "0.6", default-features = false, features = ["serde"] }
|
||||||
|
@ -652,7 +674,9 @@ foldhash = { version = "0.1", default-features = false, features = ["std"] }
|
||||||
getrandom-468e82937335b1c9 = { package = "getrandom", version = "0.3", default-features = false, features = ["std"] }
|
getrandom-468e82937335b1c9 = { package = "getrandom", version = "0.3", default-features = false, features = ["std"] }
|
||||||
getrandom-6f8ce4dd05d13bba = { package = "getrandom", version = "0.2", default-features = false, features = ["js", "rdrand"] }
|
getrandom-6f8ce4dd05d13bba = { package = "getrandom", version = "0.2", default-features = false, features = ["js", "rdrand"] }
|
||||||
gimli = { version = "0.31", default-features = false, features = ["read", "std", "write"] }
|
gimli = { version = "0.31", default-features = false, features = ["read", "std", "write"] }
|
||||||
|
hyper-dff4ba8e3ae991db = { package = "hyper", version = "1", features = ["client", "http1", "http2"] }
|
||||||
hyper-rustls = { version = "0.27", default-features = false, features = ["http1", "http2", "native-tokio", "ring", "tls12"] }
|
hyper-rustls = { version = "0.27", default-features = false, features = ["http1", "http2", "native-tokio", "ring", "tls12"] }
|
||||||
|
hyper-util = { version = "0.1", features = ["client-legacy", "http1", "http2"] }
|
||||||
inout = { version = "0.1", default-features = false, features = ["block-padding"] }
|
inout = { version = "0.1", default-features = false, features = ["block-padding"] }
|
||||||
itertools-5ef9efb8ec2df382 = { package = "itertools", version = "0.12" }
|
itertools-5ef9efb8ec2df382 = { package = "itertools", version = "0.12" }
|
||||||
linux-raw-sys-274715c4dabd11b0 = { package = "linux-raw-sys", version = "0.9", default-features = false, features = ["elf", "errno", "general", "if_ether", "ioctl", "net", "netlink", "no_std", "prctl", "xdp"] }
|
linux-raw-sys-274715c4dabd11b0 = { package = "linux-raw-sys", version = "0.9", default-features = false, features = ["elf", "errno", "general", "if_ether", "ioctl", "net", "netlink", "no_std", "prctl", "xdp"] }
|
||||||
|
@ -669,7 +693,7 @@ rustix-d585fab2519d2d1 = { package = "rustix", version = "0.38", features = ["ev
|
||||||
rustix-dff4ba8e3ae991db = { package = "rustix", version = "1", features = ["fs", "net", "process", "termios", "time"] }
|
rustix-dff4ba8e3ae991db = { package = "rustix", version = "1", features = ["fs", "net", "process", "termios", "time"] }
|
||||||
scopeguard = { version = "1" }
|
scopeguard = { version = "1" }
|
||||||
sync_wrapper = { version = "1", default-features = false, features = ["futures"] }
|
sync_wrapper = { version = "1", default-features = false, features = ["futures"] }
|
||||||
tokio-rustls = { version = "0.26", default-features = false, features = ["ring"] }
|
tokio-rustls = { version = "0.26", default-features = false, features = ["logging", "ring"] }
|
||||||
tokio-socks = { version = "0.5", features = ["futures-io"] }
|
tokio-socks = { version = "0.5", features = ["futures-io"] }
|
||||||
tokio-stream = { version = "0.1", features = ["fs"] }
|
tokio-stream = { version = "0.1", features = ["fs"] }
|
||||||
toml_datetime = { version = "0.6", default-features = false, features = ["serde"] }
|
toml_datetime = { version = "0.6", default-features = false, features = ["serde"] }
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue