Compare commits
269 Commits
ordered-mu
...
missing-gi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
59bba2a98c | ||
|
|
5315d38cf4 | ||
|
|
337b9e62d2 | ||
|
|
73c487c222 | ||
|
|
ffcad71bfa | ||
|
|
3ab48b31a1 | ||
|
|
00971fbe41 | ||
|
|
9c132fece5 | ||
|
|
ad46c5b567 | ||
|
|
c28c767b40 | ||
|
|
e1bb9570df | ||
|
|
09967ac3d0 | ||
|
|
13089d7ec6 | ||
|
|
c24f22cd14 | ||
|
|
8fc5d227a4 | ||
|
|
01bcbf3b0d | ||
|
|
592642fbfc | ||
|
|
35886e38e5 | ||
|
|
8ed8b4d2ec | ||
|
|
b4d8b1be3f | ||
|
|
d459cd517e | ||
|
|
69e6910c9c | ||
|
|
f08b1d78ec | ||
|
|
1f2205d75c | ||
|
|
53fcd7cc92 | ||
|
|
88ff44f2e8 | ||
|
|
b96f62f4c9 | ||
|
|
b1b2e812b1 | ||
|
|
00b1964940 | ||
|
|
a6f83c283c | ||
|
|
97cda3f410 | ||
|
|
7cbcca2881 | ||
|
|
10792ee0ad | ||
|
|
c61f12dd22 | ||
|
|
1cdfbe2d5f | ||
|
|
fa0261e3ad | ||
|
|
5931af810e | ||
|
|
8b3d315e40 | ||
|
|
10b6bc2508 | ||
|
|
4270f89956 | ||
|
|
0a70627f00 | ||
|
|
5d1c56829a | ||
|
|
0671be215f | ||
|
|
3374514f82 | ||
|
|
4e5b11a0a7 | ||
|
|
d81a4ec7ec | ||
|
|
980ce5fbf2 | ||
|
|
1dbca5d9a0 | ||
|
|
e1919b4121 | ||
|
|
c0dd7e8367 | ||
|
|
b7244af093 | ||
|
|
ca01a8b9cb | ||
|
|
9114ca973c | ||
|
|
e506efa9bf | ||
|
|
f0239c0a89 | ||
|
|
b710945949 | ||
|
|
59738f88c2 | ||
|
|
2f5abe2b5a | ||
|
|
44a7614a74 | ||
|
|
9369b72475 | ||
|
|
971a91ced7 | ||
|
|
6d81ad1e0b | ||
|
|
70b1e0eec0 | ||
|
|
ffe503d77c | ||
|
|
5a25751521 | ||
|
|
aaf432fcd2 | ||
|
|
e1a6d9a485 | ||
|
|
37db1dcd48 | ||
|
|
17a7495332 | ||
|
|
6b29616c95 | ||
|
|
868e3f75b2 | ||
|
|
da4bad3a55 | ||
|
|
630d0add19 | ||
|
|
0a89d1a479 | ||
|
|
992125bec2 | ||
|
|
74c4dbd237 | ||
|
|
c252b5db16 | ||
|
|
f5e8048fcb | ||
|
|
88b5f069fb | ||
|
|
fef567bb49 | ||
|
|
5a955e208c | ||
|
|
0963401a8d | ||
|
|
22b7042b9e | ||
|
|
7c1132ed88 | ||
|
|
f366b97899 | ||
|
|
aa3da35e8e | ||
|
|
b13498a5dd | ||
|
|
b02baea9d2 | ||
|
|
d6a2a0b04a | ||
|
|
58db66ef44 | ||
|
|
6f0f9d631e | ||
|
|
5704b50fb1 | ||
|
|
871f98bc4d | ||
|
|
69bb0a0597 | ||
|
|
cfe0932c0a | ||
|
|
7da60995cc | ||
|
|
ee422dea6e | ||
|
|
f8c436fe7f | ||
|
|
b5d4b17f60 | ||
|
|
3e68f7fde4 | ||
|
|
e768eb0a34 | ||
|
|
667396c44b | ||
|
|
c64b26110c | ||
|
|
8c7096f7a6 | ||
|
|
27d1c689cf | ||
|
|
4ab4e87266 | ||
|
|
4f98157e64 | ||
|
|
9b031d747f | ||
|
|
aea36f0eff | ||
|
|
cae712e740 | ||
|
|
bce9a9a6f4 | ||
|
|
c50cb90d6f | ||
|
|
a0269aba77 | ||
|
|
88b485f5cc | ||
|
|
28536498c9 | ||
|
|
cc2ebb96a6 | ||
|
|
d400bdea76 | ||
|
|
71f2cbe798 | ||
|
|
6659aea13b | ||
|
|
386cfacb25 | ||
|
|
e5c3273486 | ||
|
|
556b0eb4f1 | ||
|
|
93f8ccaaee | ||
|
|
29e559d60c | ||
|
|
9a22ef2fd5 | ||
|
|
66e0898425 | ||
|
|
cf4539ec79 | ||
|
|
8bce896395 | ||
|
|
ea66a54cf8 | ||
|
|
225f0c4d12 | ||
|
|
daf09fa532 | ||
|
|
8742c18107 | ||
|
|
66d0cdfd91 | ||
|
|
e17f307189 | ||
|
|
3d3ac2c470 | ||
|
|
0919f10037 | ||
|
|
2442c49048 | ||
|
|
4c29e1ff07 | ||
|
|
b6e680ea3d | ||
|
|
dfd11c3d3b | ||
|
|
28b80455f9 | ||
|
|
13b7be12bd | ||
|
|
e2d6d4bcb2 | ||
|
|
1ec91a8738 | ||
|
|
11e095b56a | ||
|
|
45708d2680 | ||
|
|
27a413a5e3 | ||
|
|
a47a7fb6a9 | ||
|
|
4f63423d56 | ||
|
|
a8741dc310 | ||
|
|
a864168c27 | ||
|
|
6b48a6e690 | ||
|
|
f45d58f01a | ||
|
|
8edcaec1bf | ||
|
|
eb820ab800 | ||
|
|
08c834ced0 | ||
|
|
52aed4849a | ||
|
|
f14ef40a13 | ||
|
|
1d3e9b22b0 | ||
|
|
1301c41cea | ||
|
|
1dd2bbe2ba | ||
|
|
4885ace107 | ||
|
|
691de6b4b3 | ||
|
|
422d57e8a2 | ||
|
|
aa42e206b3 | ||
|
|
4a65315f3b | ||
|
|
f4f51c198c | ||
|
|
f7d2b5300c | ||
|
|
2f82374926 | ||
|
|
af461f8165 | ||
|
|
f6824e3eaa | ||
|
|
5bd7eaa173 | ||
|
|
39d45bcbc1 | ||
|
|
d0152f9eb4 | ||
|
|
a3c7dc3321 | ||
|
|
66e2028313 | ||
|
|
52a3013d73 | ||
|
|
17872260e6 | ||
|
|
9a6b9e3124 | ||
|
|
93c7b54caa | ||
|
|
4d9659adc4 | ||
|
|
3af37ddf6d | ||
|
|
df16ef209c | ||
|
|
1e96663e20 | ||
|
|
de3702bedc | ||
|
|
d04800c329 | ||
|
|
e42b6e6905 | ||
|
|
0c94bdc8e4 | ||
|
|
027fe1b4b5 | ||
|
|
990bdde5e8 | ||
|
|
af6548c745 | ||
|
|
6f467281e0 | ||
|
|
be4c3cfbd2 | ||
|
|
0ad2aeb2e9 | ||
|
|
f2b3f3a9ab | ||
|
|
e1af35aa15 | ||
|
|
cb15753694 | ||
|
|
5914ccdc51 | ||
|
|
8be73bf187 | ||
|
|
35fbe1ef3d | ||
|
|
517e519bdc | ||
|
|
ff43b6875b | ||
|
|
4c8b5ea4f7 | ||
|
|
f29b33ec85 | ||
|
|
e5bc0486b5 | ||
|
|
b6e54ae2f1 | ||
|
|
9c3482083b | ||
|
|
c28a4204ee | ||
|
|
4892286465 | ||
|
|
419780d702 | ||
|
|
7bf4fd6c46 | ||
|
|
2c950cf7f5 | ||
|
|
87b0f62041 | ||
|
|
9d6c0e57a0 | ||
|
|
399e2c1ed3 | ||
|
|
7adf9cb1a0 | ||
|
|
e23e03592b | ||
|
|
4ab372d6b5 | ||
|
|
d2828e8722 | ||
|
|
d1b8fedc9c | ||
|
|
429dbf7129 | ||
|
|
9e4555797d | ||
|
|
48dba9a9d9 | ||
|
|
51f07e3382 | ||
|
|
5e210c083f | ||
|
|
5e449c84fe | ||
|
|
41de83fe1f | ||
|
|
e721dac367 | ||
|
|
1bc54c2c20 | ||
|
|
e662e819fe | ||
|
|
b62812c49e | ||
|
|
154cffb9d5 | ||
|
|
a6c0388ce9 | ||
|
|
0434b4b9ae | ||
|
|
53f4ad8ad4 | ||
|
|
303cce0cbc | ||
|
|
19383036d5 | ||
|
|
ff72c6358e | ||
|
|
508c08bb86 | ||
|
|
e970690cfa | ||
|
|
e584586cb0 | ||
|
|
73c7f8aa8f | ||
|
|
ade3e45a36 | ||
|
|
974b9eec85 | ||
|
|
943d46c465 | ||
|
|
bd21334013 | ||
|
|
ee0d2a8d94 | ||
|
|
4cef772364 | ||
|
|
a62ce45126 | ||
|
|
b9e0aae49f | ||
|
|
31fa414422 | ||
|
|
706f7be5e7 | ||
|
|
baac01cea4 | ||
|
|
f8dddf0a5c | ||
|
|
a03b7624f1 | ||
|
|
8603a908c1 | ||
|
|
e5943975f9 | ||
|
|
8442e2b9d8 | ||
|
|
5ecff157aa | ||
|
|
fb9b4ee842 | ||
|
|
07161d65d0 | ||
|
|
9bf5e55233 | ||
|
|
46f45464be | ||
|
|
d2d9f492b9 | ||
|
|
6d4ccb0eb1 | ||
|
|
dbdf140ca1 | ||
|
|
06936c69f6 | ||
|
|
43f3491d50 | ||
|
|
16004d4c6a |
15
.github/ISSUE_TEMPLATE/0_feature_request.yml
vendored
15
.github/ISSUE_TEMPLATE/0_feature_request.yml
vendored
@@ -1,15 +0,0 @@
|
||||
name: Feature Request
|
||||
description: "Tip: open this issue template from within Zed with the `request feature` command palette action"
|
||||
type: "Feature"
|
||||
body:
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Describe the feature
|
||||
description: A one line summary, and description of what you want to happen.
|
||||
value: |
|
||||
Summary:
|
||||
|
||||
Description:
|
||||
|
||||
Screenshots:
|
||||
<!-- drag files here -->
|
||||
6
.github/ISSUE_TEMPLATE/1_bug_report.yml
vendored
6
.github/ISSUE_TEMPLATE/1_bug_report.yml
vendored
@@ -5,10 +5,10 @@ type: "Bug"
|
||||
body:
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Describe the bug / provide steps to reproduce it
|
||||
description: A one line summary, and detailed reproduction steps
|
||||
label: Summary
|
||||
description: Describe the bug with a one line summary, and provide detailed reproduction steps
|
||||
value: |
|
||||
Summary:
|
||||
<!-- Please insert a one line summary of the issue below -->
|
||||
|
||||
<!-- Include all steps necessary to reproduce from a clean Zed installation. Be verbose -->
|
||||
Steps to trigger the problem:
|
||||
|
||||
6
.github/ISSUE_TEMPLATE/2_crash_report.yml
vendored
6
.github/ISSUE_TEMPLATE/2_crash_report.yml
vendored
@@ -4,10 +4,10 @@ type: "Crash"
|
||||
body:
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Describe the bug / provide steps to reproduce it
|
||||
description: A one line summary, and detailed reproduction steps
|
||||
label: Summary
|
||||
description: Describe the bug with a one line summary, and provide detailed reproduction steps
|
||||
value: |
|
||||
Summary:
|
||||
<!-- Please insert a one line summary of the issue below -->
|
||||
|
||||
<!-- Include all steps necessary to reproduce from a clean Zed installation. Be verbose -->
|
||||
Steps to trigger the problem:
|
||||
|
||||
3
.github/ISSUE_TEMPLATE/config.yml
vendored
3
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -1,6 +1,9 @@
|
||||
# yaml-language-server: $schema=https://json.schemastore.org/github-issue-config.json
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: Feature Request
|
||||
url: https://github.com/zed-industries/zed/discussions/new/choose
|
||||
about: To request a feature, open a new Discussion in one of the appropriate Discussion categories
|
||||
- name: Zed Discussion Forum
|
||||
url: https://github.com/zed-industries/zed/discussions
|
||||
about: A community discussion forum
|
||||
|
||||
2
.github/actions/run_tests/action.yml
vendored
2
.github/actions/run_tests/action.yml
vendored
@@ -10,7 +10,7 @@ runs:
|
||||
cargo install cargo-nextest --locked
|
||||
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4
|
||||
uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4
|
||||
with:
|
||||
node-version: "18"
|
||||
|
||||
|
||||
26
.github/actions/run_tests_windows/action.yml
vendored
Normal file
26
.github/actions/run_tests_windows/action.yml
vendored
Normal file
@@ -0,0 +1,26 @@
|
||||
name: "Run tests on Windows"
|
||||
description: "Runs the tests on Windows"
|
||||
|
||||
inputs:
|
||||
working-directory:
|
||||
description: "The working directory"
|
||||
required: true
|
||||
default: "."
|
||||
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- name: Install Rust
|
||||
shell: pwsh
|
||||
working-directory: ${{ inputs.working-directory }}
|
||||
run: cargo install cargo-nextest --locked
|
||||
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4
|
||||
with:
|
||||
node-version: "18"
|
||||
|
||||
- name: Run tests
|
||||
shell: pwsh
|
||||
working-directory: ${{ inputs.working-directory }}
|
||||
run: cargo nextest run --workspace --no-fail-fast
|
||||
33
.github/workflows/ci.yml
vendored
33
.github/workflows/ci.yml
vendored
@@ -135,6 +135,7 @@ jobs:
|
||||
cargo check -p gpui --features "macos-blade"
|
||||
cargo check -p workspace
|
||||
cargo build -p remote_server
|
||||
cargo check -p gpui --examples
|
||||
script/check-rust-livekit-macos
|
||||
|
||||
# Since the macOS runners are stateful, so we need to remove the config file to prevent potential bug.
|
||||
@@ -181,6 +182,7 @@ jobs:
|
||||
run: |
|
||||
cargo build -p zed
|
||||
cargo check -p workspace
|
||||
cargo check -p gpui --examples
|
||||
|
||||
# Even the Linux runner is not stateful, in theory there is no need to do this cleanup.
|
||||
# But, to avoid potential issues in the future if we choose to use a stateful Linux runner and forget to add code
|
||||
@@ -226,7 +228,6 @@ jobs:
|
||||
if: always()
|
||||
run: rm -rf ./../.cargo
|
||||
|
||||
# todo(windows): Actually run the tests
|
||||
windows_tests:
|
||||
timeout-minutes: 60
|
||||
name: (Windows) Run Clippy and tests
|
||||
@@ -236,33 +237,55 @@ jobs:
|
||||
# more info here:- https://github.com/rust-lang/cargo/issues/13020
|
||||
- name: Enable longer pathnames for git
|
||||
run: git config --system core.longpaths true
|
||||
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
with:
|
||||
clean: false
|
||||
|
||||
- name: Create Dev Drive using ReFS
|
||||
run: ./script/setup-dev-driver.ps1
|
||||
|
||||
# actions/checkout does not let us clone into anywhere outside ${{ github.workspace }}, so we have to copy the clone...
|
||||
- name: Copy Git Repo to Dev Drive
|
||||
run: |
|
||||
Copy-Item -Path "${{ github.workspace }}" -Destination "${{ env.ZED_WORKSPACE }}" -Recurse
|
||||
|
||||
- name: Cache dependencies
|
||||
uses: swatinem/rust-cache@f0deed1e0edfc6a9be95417288c0e1099b1eeec3 # v2
|
||||
with:
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
workspaces: ${{ env.ZED_WORKSPACE }}
|
||||
cache-provider: "github"
|
||||
|
||||
- name: Configure CI
|
||||
run: |
|
||||
mkdir -p ./../.cargo
|
||||
cp ./.cargo/ci-config.toml ./../.cargo/config.toml
|
||||
mkdir -p ${{ env.CARGO_HOME }} -ErrorAction Ignore
|
||||
cp ./.cargo/ci-config.toml ${{ env.CARGO_HOME }}/config.toml
|
||||
|
||||
- name: cargo clippy
|
||||
working-directory: ${{ env.ZED_WORKSPACE }}
|
||||
# Windows can't run shell scripts, so we need to use `cargo xtask`.
|
||||
run: cargo xtask clippy
|
||||
|
||||
- name: Run tests
|
||||
uses: ./.github/actions/run_tests_windows
|
||||
with:
|
||||
working-directory: ${{ env.ZED_WORKSPACE }}
|
||||
|
||||
- name: Build Zed
|
||||
working-directory: ${{ env.ZED_WORKSPACE }}
|
||||
run: cargo build
|
||||
|
||||
- name: Check dev drive space
|
||||
working-directory: ${{ env.ZED_WORKSPACE }}
|
||||
# `setup-dev-driver.ps1` creates a 100GB drive, with CI taking up ~45GB of the drive.
|
||||
run: ./script/exit-ci-if-dev-drive-is-full.ps1 95
|
||||
|
||||
# Since the Windows runners are stateful, so we need to remove the config file to prevent potential bug.
|
||||
- name: Clean CI config file
|
||||
if: always()
|
||||
run: Remove-Item -Path "./../.cargo" -Recurse -Force
|
||||
run: Remove-Item -Path "${{ env.CARGO_HOME }}/config.toml" -Force
|
||||
|
||||
bundle-mac:
|
||||
timeout-minutes: 120
|
||||
@@ -283,7 +306,7 @@ jobs:
|
||||
DIGITALOCEAN_SPACES_SECRET_KEY: ${{ secrets.DIGITALOCEAN_SPACES_SECRET_KEY }}
|
||||
steps:
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4
|
||||
uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4
|
||||
with:
|
||||
node-version: "18"
|
||||
|
||||
|
||||
@@ -29,8 +29,7 @@ jobs:
|
||||
maxLength: 2000
|
||||
truncationSymbol: "..."
|
||||
- name: Discord Webhook Action
|
||||
uses: tsickert/discord-webhook@86dc739f3f165f16dadc5666051c367efa1692f4 # v6.0.0
|
||||
uses: tsickert/discord-webhook@c840d45a03a323fbc3f7507ac7769dbd91bfb164 # v5.3.0
|
||||
with:
|
||||
webhook-url: ${{ secrets.DISCORD_WEBHOOK_URL }}
|
||||
content: ${{ steps.get-content.outputs.string }}
|
||||
flags: 4 # suppress embeds - https://discord.com/developers/docs/resources/message#message-object-message-flags
|
||||
|
||||
2
.github/workflows/danger.yml
vendored
2
.github/workflows/danger.yml
vendored
@@ -22,7 +22,7 @@ jobs:
|
||||
version: 9
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4
|
||||
uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4
|
||||
with:
|
||||
node-version: "20"
|
||||
cache: "pnpm"
|
||||
|
||||
7
.github/workflows/deploy_cloudflare.yml
vendored
7
.github/workflows/deploy_cloudflare.yml
vendored
@@ -63,3 +63,10 @@ jobs:
|
||||
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
||||
command: deploy .cloudflare/docs-proxy/src/worker.js
|
||||
|
||||
- name: Preserve Wrangler logs
|
||||
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4
|
||||
if: always()
|
||||
with:
|
||||
name: wrangler_logs
|
||||
path: /home/runner/.config/.wrangler/logs/
|
||||
|
||||
2
.github/workflows/randomized_tests.yml
vendored
2
.github/workflows/randomized_tests.yml
vendored
@@ -23,7 +23,7 @@ jobs:
|
||||
- buildjet-16vcpu-ubuntu-2204
|
||||
steps:
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4
|
||||
uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4
|
||||
with:
|
||||
node-version: "18"
|
||||
|
||||
|
||||
2
.github/workflows/release_nightly.yml
vendored
2
.github/workflows/release_nightly.yml
vendored
@@ -70,7 +70,7 @@ jobs:
|
||||
ZED_CLOUD_PROVIDER_ADDITIONAL_MODELS_JSON: ${{ secrets.ZED_CLOUD_PROVIDER_ADDITIONAL_MODELS_JSON }}
|
||||
steps:
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4
|
||||
uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4
|
||||
with:
|
||||
node-version: "18"
|
||||
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -30,6 +30,7 @@ DerivedData/
|
||||
.vscode
|
||||
.wrangler
|
||||
.flatpak-builder
|
||||
.envrc
|
||||
|
||||
# Don't commit any secrets to the repo.
|
||||
.env.secret.toml
|
||||
|
||||
@@ -52,3 +52,9 @@ Zed is made up of several smaller crates - let's go over those you're most likel
|
||||
- [`rpc`](/crates/rpc) defines messages to be exchanged with collaboration server.
|
||||
- [`theme`](/crates/theme) defines the theme system and provides a default theme.
|
||||
- [`ui`](/crates/ui) is a collection of UI components and common patterns used throughout Zed.
|
||||
- [`cli`](/crates/cli) is the CLI crate which invokes the Zed binary.
|
||||
- [`zed`](/crates/zed) is where all things come together, and the `main` entry point for Zed.
|
||||
|
||||
## Packaging Zed
|
||||
|
||||
Check our [notes for packaging Zed](https://zed.dev/docs/development/linux#notes-for-packaging-zed).
|
||||
|
||||
837
Cargo.lock
generated
837
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
48
Cargo.toml
48
Cargo.toml
@@ -2,7 +2,6 @@
|
||||
resolver = "2"
|
||||
members = [
|
||||
"crates/activity_indicator",
|
||||
"crates/zed_predict_tos",
|
||||
"crates/anthropic",
|
||||
"crates/assets",
|
||||
"crates/assistant",
|
||||
@@ -31,8 +30,9 @@ members = [
|
||||
"crates/context_server_settings",
|
||||
"crates/copilot",
|
||||
"crates/db",
|
||||
"crates/diagnostics",
|
||||
"crates/deepseek",
|
||||
"crates/diagnostics",
|
||||
"crates/diff",
|
||||
"crates/docs_preprocessor",
|
||||
"crates/editor",
|
||||
"crates/evals",
|
||||
@@ -45,16 +45,17 @@ members = [
|
||||
"crates/feedback",
|
||||
"crates/file_finder",
|
||||
"crates/file_icons",
|
||||
"crates/fireworks",
|
||||
"crates/fs",
|
||||
"crates/fsevent",
|
||||
"crates/fuzzy",
|
||||
"crates/git",
|
||||
"crates/git_hosting_providers",
|
||||
"crates/git_ui",
|
||||
"crates/go_to_line",
|
||||
"crates/google_ai",
|
||||
"crates/gpui",
|
||||
"crates/gpui_macros",
|
||||
"crates/gpui_tokio",
|
||||
"crates/html_to_markdown",
|
||||
"crates/http_client",
|
||||
"crates/image_viewer",
|
||||
@@ -87,6 +88,7 @@ members = [
|
||||
"crates/open_ai",
|
||||
"crates/outline",
|
||||
"crates/outline_panel",
|
||||
"crates/panel",
|
||||
"crates/paths",
|
||||
"crates/picker",
|
||||
"crates/prettier",
|
||||
@@ -106,6 +108,7 @@ members = [
|
||||
"crates/rich_text",
|
||||
"crates/rope",
|
||||
"crates/rpc",
|
||||
"crates/schema_generator",
|
||||
"crates/search",
|
||||
"crates/semantic_index",
|
||||
"crates/semantic_version",
|
||||
@@ -141,8 +144,8 @@ members = [
|
||||
"crates/ui",
|
||||
"crates/ui_input",
|
||||
"crates/ui_macros",
|
||||
"crates/reqwest_client",
|
||||
"crates/util",
|
||||
"crates/util_macros",
|
||||
"crates/vcs_menu",
|
||||
"crates/vim",
|
||||
"crates/vim_mode_setting",
|
||||
@@ -152,7 +155,6 @@ members = [
|
||||
"crates/zed",
|
||||
"crates/zed_actions",
|
||||
"crates/zeta",
|
||||
"crates/git_ui",
|
||||
|
||||
#
|
||||
# Extensions
|
||||
@@ -169,7 +171,6 @@ members = [
|
||||
"extensions/lua",
|
||||
"extensions/php",
|
||||
"extensions/perplexity",
|
||||
"extensions/prisma",
|
||||
"extensions/proto",
|
||||
"extensions/purescript",
|
||||
"extensions/ruff",
|
||||
@@ -201,7 +202,6 @@ edition = "2021"
|
||||
|
||||
activity_indicator = { path = "crates/activity_indicator" }
|
||||
ai = { path = "crates/ai" }
|
||||
zed_predict_tos = { path = "crates/zed_predict_tos" }
|
||||
anthropic = { path = "crates/anthropic" }
|
||||
assets = { path = "crates/assets" }
|
||||
assistant = { path = "crates/assistant" }
|
||||
@@ -232,6 +232,7 @@ copilot = { path = "crates/copilot" }
|
||||
db = { path = "crates/db" }
|
||||
deepseek = { path = "crates/deepseek" }
|
||||
diagnostics = { path = "crates/diagnostics" }
|
||||
diff = { path = "crates/diff" }
|
||||
editor = { path = "crates/editor" }
|
||||
extension = { path = "crates/extension" }
|
||||
extension_host = { path = "crates/extension_host" }
|
||||
@@ -240,19 +241,19 @@ feature_flags = { path = "crates/feature_flags" }
|
||||
feedback = { path = "crates/feedback" }
|
||||
file_finder = { path = "crates/file_finder" }
|
||||
file_icons = { path = "crates/file_icons" }
|
||||
fireworks = { path = "crates/fireworks" }
|
||||
fs = { path = "crates/fs" }
|
||||
fsevent = { path = "crates/fsevent" }
|
||||
fuzzy = { path = "crates/fuzzy" }
|
||||
git = { path = "crates/git" }
|
||||
git_ui = { path = "crates/git_ui" }
|
||||
git_hosting_providers = { path = "crates/git_hosting_providers" }
|
||||
git_ui = { path = "crates/git_ui" }
|
||||
go_to_line = { path = "crates/go_to_line" }
|
||||
google_ai = { path = "crates/google_ai" }
|
||||
gpui = { path = "crates/gpui", default-features = false, features = [
|
||||
"http_client",
|
||||
] }
|
||||
gpui_macros = { path = "crates/gpui_macros" }
|
||||
gpui_tokio = { path = "crates/gpui_tokio" }
|
||||
html_to_markdown = { path = "crates/html_to_markdown" }
|
||||
http_client = { path = "crates/http_client" }
|
||||
image_viewer = { path = "crates/image_viewer" }
|
||||
@@ -286,6 +287,7 @@ open_ai = { path = "crates/open_ai" }
|
||||
outline = { path = "crates/outline" }
|
||||
outline_panel = { path = "crates/outline_panel" }
|
||||
paths = { path = "crates/paths" }
|
||||
panel = { path = "crates/panel" }
|
||||
picker = { path = "crates/picker" }
|
||||
plugin = { path = "crates/plugin" }
|
||||
plugin_macros = { path = "crates/plugin_macros" }
|
||||
@@ -341,6 +343,7 @@ ui = { path = "crates/ui" }
|
||||
ui_input = { path = "crates/ui_input" }
|
||||
ui_macros = { path = "crates/ui_macros" }
|
||||
util = { path = "crates/util" }
|
||||
util_macros = { path = "crates/util_macros" }
|
||||
vcs_menu = { path = "crates/vcs_menu" }
|
||||
vim = { path = "crates/vim" }
|
||||
vim_mode_setting = { path = "crates/vim_mode_setting" }
|
||||
@@ -361,7 +364,7 @@ alacritty_terminal = { git = "https://github.com/alacritty/alacritty.git", rev =
|
||||
any_vec = "0.14"
|
||||
anyhow = "1.0.86"
|
||||
arrayvec = { version = "0.7.4", features = ["serde"] }
|
||||
ashpd = { version = "0.10", default-features = false, features = ["async-std"]}
|
||||
ashpd = { version = "0.10", default-features = false, features = ["async-std"] }
|
||||
async-compat = "0.2.1"
|
||||
async-compression = { version = "0.4", features = ["gzip", "futures-io"] }
|
||||
async-dispatcher = "0.1"
|
||||
@@ -375,9 +378,10 @@ async-watch = "0.3.1"
|
||||
async_zip = { version = "0.0.17", features = ["deflate", "deflate64"] }
|
||||
base64 = "0.22"
|
||||
bitflags = "2.6.0"
|
||||
blade-graphics = { git = "https://github.com/kvark/blade", rev = "091a8401033847bb9b6ace3fcf70448d069621c5" }
|
||||
blade-macros = { git = "https://github.com/kvark/blade", rev = "091a8401033847bb9b6ace3fcf70448d069621c5" }
|
||||
blade-util = { git = "https://github.com/kvark/blade", rev = "091a8401033847bb9b6ace3fcf70448d069621c5" }
|
||||
blade-graphics = { git = "https://github.com/kvark/blade", rev = "b16f5c7bd873c7126f48c82c39e7ae64602ae74f" }
|
||||
blade-macros = { git = "https://github.com/kvark/blade", rev = "b16f5c7bd873c7126f48c82c39e7ae64602ae74f" }
|
||||
blade-util = { git = "https://github.com/kvark/blade", rev = "b16f5c7bd873c7126f48c82c39e7ae64602ae74f" }
|
||||
naga = { version = "23.1.0", features = ["wgsl-in"] }
|
||||
blake3 = "1.5.3"
|
||||
bytes = "1.0"
|
||||
cargo_metadata = "0.19"
|
||||
@@ -422,7 +426,11 @@ jupyter-websocket-client = { version = "0.9.0" }
|
||||
libc = "0.2"
|
||||
libsqlite3-sys = { version = "0.30.1", features = ["bundled"] }
|
||||
linkify = "0.10.0"
|
||||
livekit = { git = "https://github.com/zed-industries/livekit-rust-sdks", rev="060964da10574cd9bf06463a53bf6e0769c5c45e", features = ["dispatcher", "services-dispatcher", "rustls-tls-native-roots"], default-features = false }
|
||||
livekit = { git = "https://github.com/zed-industries/livekit-rust-sdks", rev = "811ceae29fabee455f110c56cd66b3f49a7e5003", features = [
|
||||
"dispatcher",
|
||||
"services-dispatcher",
|
||||
"rustls-tls-native-roots",
|
||||
], default-features = false }
|
||||
log = { version = "0.4.16", features = ["kv_unstable_serde", "serde"] }
|
||||
markup5ever_rcdom = "0.3.0"
|
||||
nanoid = "0.4"
|
||||
@@ -442,11 +450,13 @@ pet-poetry = { git = "https://github.com/microsoft/python-environment-tools.git"
|
||||
pet-reporter = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "1abe5cec5ebfbe97ca71746a4cfc7fe89bddf8e0" }
|
||||
postage = { version = "0.5", features = ["futures-traits"] }
|
||||
pretty_assertions = { version = "1.3.0", features = ["unstable"] }
|
||||
proc-macro2 = "1.0.93"
|
||||
profiling = "1"
|
||||
prost = "0.9"
|
||||
prost-build = "0.9"
|
||||
prost-types = "0.9"
|
||||
pulldown-cmark = { version = "0.12.0", default-features = false }
|
||||
quote = "1.0.9"
|
||||
rand = "0.8.5"
|
||||
rayon = "1.8"
|
||||
regex = "1.5"
|
||||
@@ -466,7 +476,7 @@ runtimelib = { version = "0.25.0", default-features = false, features = [
|
||||
rustc-demangle = "0.1.23"
|
||||
rust-embed = { version = "8.4", features = ["include-exclude"] }
|
||||
rustc-hash = "2.1.0"
|
||||
rustls = "0.21.12"
|
||||
rustls = { version = "0.23.22" }
|
||||
rustls-native-certs = "0.8.0"
|
||||
schemars = { version = "0.8", features = ["impl_json_schema", "indexmap2"] }
|
||||
semver = "1.0"
|
||||
@@ -490,6 +500,7 @@ sqlformat = "0.2"
|
||||
strsim = "0.11"
|
||||
strum = { version = "0.26.0", features = ["derive"] }
|
||||
subtle = "2.5.0"
|
||||
syn = { version = "1.0.72", features = ["full", "extra-traits"] }
|
||||
sys-locale = "0.3.1"
|
||||
sysinfo = "0.31.0"
|
||||
take-until = "0.2.0"
|
||||
@@ -514,6 +525,7 @@ tree-sitter-cpp = "0.23"
|
||||
tree-sitter-css = "0.23"
|
||||
tree-sitter-elixir = "0.3"
|
||||
tree-sitter-embedded-template = "0.23.0"
|
||||
tree-sitter-gitcommit = {git = "https://github.com/zed-industries/tree-sitter-git-commit", rev = "88309716a69dd13ab83443721ba6e0b491d37ee9"}
|
||||
tree-sitter-go = "0.23"
|
||||
tree-sitter-go-mod = { git = "https://github.com/camdencheek/tree-sitter-go-mod", rev = "6efb59652d30e0e9cd5f3b3a669afd6f1a926d3c", package = "tree-sitter-gomod" }
|
||||
tree-sitter-gowork = { git = "https://github.com/zed-industries/tree-sitter-go-work", rev = "acb0617bf7f4fda02c6217676cc64acb89536dc7" }
|
||||
@@ -524,13 +536,13 @@ tree-sitter-jsdoc = "0.23"
|
||||
tree-sitter-json = "0.24"
|
||||
tree-sitter-md = { git = "https://github.com/tree-sitter-grammars/tree-sitter-markdown", rev = "9a23c1a96c0513d8fc6520972beedd419a973539" }
|
||||
tree-sitter-python = "0.23"
|
||||
tree-sitter-regex = "0.23"
|
||||
tree-sitter-regex = "0.24"
|
||||
tree-sitter-ruby = "0.23"
|
||||
tree-sitter-rust = "0.23"
|
||||
tree-sitter-typescript = "0.23"
|
||||
tree-sitter-yaml = { git = "https://github.com/zed-industries/tree-sitter-yaml", rev = "baff0b51c64ef6a1fb1f8390f3ad6015b83ec13a" }
|
||||
unicase = "2.6"
|
||||
unindent = "0.1.7"
|
||||
unindent = "0.2.0"
|
||||
unicode-segmentation = "1.10"
|
||||
unicode-script = "0.5.7"
|
||||
url = "2.2"
|
||||
@@ -547,6 +559,7 @@ wasmtime = { version = "24", default-features = false, features = [
|
||||
wasmtime-wasi = "24"
|
||||
which = "6.0.0"
|
||||
wit-component = "0.201"
|
||||
zed_llm_client = "0.2"
|
||||
zstd = "0.11"
|
||||
metal = "0.31"
|
||||
|
||||
@@ -607,6 +620,7 @@ features = [
|
||||
# TODO livekit https://github.com/RustAudio/cpal/pull/891
|
||||
[patch.crates-io]
|
||||
cpal = { git = "https://github.com/zed-industries/cpal", rev = "fd8bc2fd39f1f5fdee5a0690656caff9a26d9d50" }
|
||||
real-async-tls = { git = "https://github.com/zed-industries/async-tls", rev = "1e759a4b5e370f87dc15e40756ac4f8815b61d9d", package = "async-tls"}
|
||||
|
||||
[profile.dev]
|
||||
split-debuginfo = "unpacked"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
Copyright 2022 - 2024 Zed Industries, Inc.
|
||||
Copyright 2022 - 2025 Zed Industries, Inc.
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
Copyright 2022 - 2024 Zed Industries, Inc.
|
||||
Copyright 2022 - 2025 Zed Industries, Inc.
|
||||
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
|
||||
1
assets/icons/circle.svg
Normal file
1
assets/icons/circle.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg"><circle cx="7.25" cy="7.25" r="3" fill="currentColor"></circle></svg>
|
||||
|
After Width: | Height: | Size: 165 B |
@@ -42,6 +42,12 @@
|
||||
"elm": "elm",
|
||||
"erl": "erlang",
|
||||
"escript": "erlang",
|
||||
"eslint.config.cjs": "eslint",
|
||||
"eslint.config.cts": "eslint",
|
||||
"eslint.config.js": "eslint",
|
||||
"eslint.config.mjs": "eslint",
|
||||
"eslint.config.mts": "eslint",
|
||||
"eslint.config.ts": "eslint",
|
||||
"eslintrc": "eslint",
|
||||
"eslintrc.js": "eslint",
|
||||
"eslintrc.json": "eslint",
|
||||
@@ -80,8 +86,8 @@
|
||||
"hpp": "cpp",
|
||||
"hrl": "erlang",
|
||||
"hs": "haskell",
|
||||
"htm": "template",
|
||||
"html": "template",
|
||||
"htm": "html",
|
||||
"html": "html",
|
||||
"hxx": "cpp",
|
||||
"ib": "storage",
|
||||
"ico": "image",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M7 8.9V11C5.34478 11 4.65522 11 3 11V10.4L7 5.6V5H3V7.1" stroke="black" stroke-width="1.5"/>
|
||||
<path d="M12 5L14 8L12 11" stroke="black" stroke-width="1.5"/>
|
||||
<path d="M10 6.5L11 8L10 9.5" stroke="black" stroke-width="1.5"/>
|
||||
<path d="M7.5 8.9V11C5.43097 11 4.56903 11 2.5 11V10.4L7.5 5.6V5H2.5V7.1" stroke="black" stroke-width="1.5"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 334 B After Width: | Height: | Size: 342 B |
19
assets/icons/zed_predict_bg.svg
Normal file
19
assets/icons/zed_predict_bg.svg
Normal file
@@ -0,0 +1,19 @@
|
||||
<svg width="440" height="128" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<pattern id="tilePattern" width="22" height="22" patternUnits="userSpaceOnUse">
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12 5L14 8L12 11" stroke="black" stroke-width="1.5"/>
|
||||
<path d="M10 6.5L11 8L10 9.5" stroke="black" stroke-width="1.5"/>
|
||||
<path d="M7.5 8.9V11C5.43097 11 4.56903 11 2.5 11V10.4L7.5 5.6V5H2.5V7.1" stroke="black" stroke-width="1.5"/>
|
||||
</svg>
|
||||
</pattern>
|
||||
<linearGradient id="fade" y2="1" x2="0">
|
||||
<stop offset="0" stop-color="white" stop-opacity=".24"/>
|
||||
<stop offset="1" stop-color="white" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
<mask id="fadeMask" maskContentUnits="objectBoundingBox">
|
||||
<rect width="1" height="1" fill="url(#fade)"/>
|
||||
</mask>
|
||||
</defs>
|
||||
<rect width="100%" height="100%" fill="url(#tilePattern)" mask="url(#fadeMask)"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 971 B |
6
assets/icons/zed_predict_disabled.svg
Normal file
6
assets/icons/zed_predict_disabled.svg
Normal file
@@ -0,0 +1,6 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path opacity="0.6" fill-rule="evenodd" clip-rule="evenodd" d="M6.75 9.31247L8.25 10.5576V11.75H1.75V10.0803L4.49751 7.44273L5.65909 8.40693L3.73923 10.25H6.75V9.31247ZM8.25 5.85739V4.25H6.31358L8.25 5.85739ZM1.75 5.16209V7.1H3.25V6.4072L1.75 5.16209Z" fill="black"/>
|
||||
<path opacity="0.6" fill-rule="evenodd" clip-rule="evenodd" d="M10.9624 9.40853L11.9014 8L10.6241 6.08397L9.37598 6.91603L10.0986 8L9.80184 8.44518L10.9624 9.40853Z" fill="black"/>
|
||||
<path opacity="0.6" fill-rule="evenodd" clip-rule="evenodd" d="M12.8936 11.0116L14.9014 8L12.6241 4.58397L11.376 5.41603L13.0986 8L11.7331 10.0483L12.8936 11.0116Z" fill="black"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M14.1225 13.809C14.0341 13.9146 13.877 13.9289 13.7711 13.8409L1.19311 3.40021C1.08659 3.31178 1.07221 3.15362 1.16104 3.04743L1.87752 2.19101C1.96588 2.0854 2.123 2.07112 2.22895 2.15906L14.8069 12.5998C14.9134 12.6882 14.9278 12.8464 14.839 12.9526L14.1225 13.809Z" fill="black"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.0 KiB |
@@ -122,8 +122,7 @@
|
||||
"ctrl-i": "editor::ShowSignatureHelp",
|
||||
"alt-g b": "editor::ToggleGitBlame",
|
||||
"menu": "editor::OpenContextMenu",
|
||||
"shift-f10": "editor::OpenContextMenu",
|
||||
"alt-enter": "editor::OpenSelectionsInMultibuffer"
|
||||
"shift-f10": "editor::OpenContextMenu"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -137,11 +136,12 @@
|
||||
"ctrl-k z": "editor::ToggleSoftWrap",
|
||||
"find": "buffer_search::Deploy",
|
||||
"ctrl-f": "buffer_search::Deploy",
|
||||
"ctrl-h": ["buffer_search::Deploy", { "replace_enabled": true }],
|
||||
"ctrl-h": "buffer_search::DeployReplace",
|
||||
// "cmd-e": ["buffer_search::Deploy", { "focus": false }],
|
||||
"ctrl->": "assistant::QuoteSelection",
|
||||
"ctrl-<": "assistant::InsertIntoEditor",
|
||||
"ctrl-alt-e": "editor::SelectEnclosingSymbol"
|
||||
"ctrl-alt-e": "editor::SelectEnclosingSymbol",
|
||||
"alt-enter": "editor::OpenSelectionsInMultibuffer"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -203,8 +203,8 @@
|
||||
"enter": "search::SelectNextMatch",
|
||||
"shift-enter": "search::SelectPrevMatch",
|
||||
"alt-enter": "search::SelectAllMatches",
|
||||
"ctrl-f": "search::FocusSearch",
|
||||
"find": "search::FocusSearch",
|
||||
"ctrl-f": "search::FocusSearch",
|
||||
"ctrl-h": "search::ToggleReplace",
|
||||
"ctrl-l": "search::ToggleSelection"
|
||||
}
|
||||
@@ -290,15 +290,15 @@
|
||||
"f3": "search::SelectNextMatch",
|
||||
"ctrl-alt-shift-g": "search::SelectPrevMatch",
|
||||
"shift-f3": "search::SelectPrevMatch",
|
||||
"ctrl-shift-f": "project_search::ToggleFocus",
|
||||
"shift-find": "project_search::ToggleFocus",
|
||||
"ctrl-shift-f": "project_search::ToggleFocus",
|
||||
"ctrl-alt-shift-h": "search::ToggleReplace",
|
||||
"ctrl-alt-shift-l": "search::ToggleSelection",
|
||||
"alt-enter": "search::SelectAllMatches",
|
||||
"alt-c": "search::ToggleCaseSensitive",
|
||||
"alt-w": "search::ToggleWholeWord",
|
||||
"alt-ctrl-f": "project_search::ToggleFilters",
|
||||
"alt-find": "project_search::ToggleFilters",
|
||||
"alt-ctrl-f": "project_search::ToggleFilters",
|
||||
"ctrl-alt-shift-r": "search::ToggleRegex",
|
||||
"ctrl-alt-shift-x": "search::ToggleRegex",
|
||||
"alt-r": "search::ToggleRegex",
|
||||
@@ -503,7 +503,14 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && inline_completion && !showing_completions",
|
||||
"context": "Editor && inline_completion",
|
||||
"bindings": {
|
||||
// Changing the modifier currently breaks accepting while you also an LSP completions menu open
|
||||
"alt-enter": "editor::AcceptInlineCompletion"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && inline_completion && !inline_completion_requires_modifier",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"tab": "editor::AcceptInlineCompletion"
|
||||
@@ -680,8 +687,8 @@
|
||||
"ctrl-delete": ["project_panel::Delete", { "skip_prompt": false }],
|
||||
"alt-ctrl-r": "project_panel::RevealInFileManager",
|
||||
"ctrl-shift-enter": "project_panel::OpenWithSystem",
|
||||
"ctrl-shift-f": "project_panel::NewSearchInDirectory",
|
||||
"shift-find": "project_panel::NewSearchInDirectory",
|
||||
"ctrl-shift-f": "project_panel::NewSearchInDirectory",
|
||||
"shift-down": "menu::SelectNext",
|
||||
"shift-up": "menu::SelectPrev",
|
||||
"escape": "menu::Cancel"
|
||||
@@ -798,6 +805,8 @@
|
||||
"shift-insert": "terminal::Paste",
|
||||
"ctrl-shift-v": "terminal::Paste",
|
||||
"ctrl-enter": "assistant::InlineAssist",
|
||||
"alt-b": ["terminal::SendText", "\u001bb"],
|
||||
"alt-f": ["terminal::SendText", "\u001bf"],
|
||||
// Overrides for conflicting keybindings
|
||||
"ctrl-w": ["terminal::SendKeystroke", "ctrl-w"],
|
||||
"ctrl-shift-a": "editor::SelectAll",
|
||||
@@ -821,5 +830,12 @@
|
||||
"shift-end": "terminal::ScrollToBottom",
|
||||
"ctrl-shift-space": "terminal::ToggleViMode"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "ZedPredictModal",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"escape": "menu::Cancel"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -132,8 +132,7 @@
|
||||
"cmd-alt-g b": "editor::ToggleGitBlame",
|
||||
"cmd-i": "editor::ShowSignatureHelp",
|
||||
"ctrl-f12": "editor::GoToDeclaration",
|
||||
"alt-ctrl-f12": "editor::GoToDeclarationSplit",
|
||||
"alt-enter": "editor::OpenSelectionsInMultibuffer"
|
||||
"alt-ctrl-f12": "editor::GoToDeclarationSplit"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -146,12 +145,13 @@
|
||||
"cmd-shift-enter": "editor::NewlineAbove",
|
||||
"cmd-k z": "editor::ToggleSoftWrap",
|
||||
"cmd-f": "buffer_search::Deploy",
|
||||
"cmd-alt-f": ["buffer_search::Deploy", { "replace_enabled": true }],
|
||||
"cmd-alt-f": "buffer_search::DeployReplace",
|
||||
"cmd-alt-l": ["buffer_search::Deploy", { "selection_search_enabled": true }],
|
||||
"cmd-e": ["buffer_search::Deploy", { "focus": false }],
|
||||
"cmd->": "assistant::QuoteSelection",
|
||||
"cmd-<": "assistant::InsertIntoEditor",
|
||||
"cmd-alt-e": "editor::SelectEnclosingSymbol"
|
||||
"cmd-alt-e": "editor::SelectEnclosingSymbol",
|
||||
"alt-enter": "editor::OpenSelectionsInMultibuffer"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -580,7 +580,14 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && inline_completion && !showing_completions",
|
||||
"context": "Editor && inline_completion",
|
||||
"bindings": {
|
||||
// Changing the modifier currently breaks accepting while you also an LSP completions menu open
|
||||
"alt-tab": "editor::AcceptInlineCompletion"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && inline_completion && !inline_completion_requires_modifier",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"tab": "editor::AcceptInlineCompletion"
|
||||
@@ -834,6 +841,8 @@
|
||||
// Terminal.app compatibility
|
||||
"alt-left": ["terminal::SendText", "\u001bb"],
|
||||
"alt-right": ["terminal::SendText", "\u001bf"],
|
||||
"alt-b": ["terminal::SendText", "\u001bb"],
|
||||
"alt-f": ["terminal::SendText", "\u001bf"],
|
||||
// There are conflicting bindings for these keys in the global context.
|
||||
// these bindings override them, remove at your own risk:
|
||||
"up": ["terminal::SendKeystroke", "up"],
|
||||
@@ -881,7 +890,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "ZedPredictTos",
|
||||
"context": "ZedPredictModal",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"escape": "menu::Cancel"
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
"cmd-b": "editor::GoToDefinition",
|
||||
"cmd-j": "editor::ScrollCursorCenter",
|
||||
"cmd-enter": "editor::NewlineBelow",
|
||||
"cmd-alt-enter": "editor::NewLineAbove",
|
||||
"cmd-alt-enter": "editor::NewlineAbove",
|
||||
"cmd-shift-l": "editor::SelectLine",
|
||||
"cmd-shift-t": "outline::Toggle",
|
||||
"alt-backspace": "editor::DeleteToPreviousWordStart",
|
||||
@@ -70,7 +70,7 @@
|
||||
"bindings": {
|
||||
"cmd-backspace": ["project_panel::Trash", { "skip_prompt": true }],
|
||||
"cmd-d": "project_panel::Duplicate",
|
||||
"cmd-n": "project_panel::NewFolder",
|
||||
"cmd-n": "project_panel::NewDirectory",
|
||||
"return": "project_panel::Rename",
|
||||
"cmd-c": "project_panel::Copy",
|
||||
"cmd-v": "project_panel::Paste",
|
||||
|
||||
@@ -421,7 +421,8 @@
|
||||
"i": "vim::IndentObj",
|
||||
"shift-i": ["vim::IndentObj", { "includeBelow": true }],
|
||||
"f": "vim::Method",
|
||||
"c": "vim::Class"
|
||||
"c": "vim::Class",
|
||||
"e": "vim::EntireFile"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -610,10 +611,12 @@
|
||||
"ctrl-w shift-s": "pane::SplitHorizontal",
|
||||
"ctrl-w ctrl-s": "pane::SplitHorizontal",
|
||||
"ctrl-w s": "pane::SplitHorizontal",
|
||||
"ctrl-w ctrl-c": "pane::CloseAllItems",
|
||||
"ctrl-w c": "pane::CloseAllItems",
|
||||
"ctrl-w ctrl-q": "pane::CloseAllItems",
|
||||
"ctrl-w q": "pane::CloseAllItems",
|
||||
"ctrl-w ctrl-c": "pane::CloseActiveItem",
|
||||
"ctrl-w c": "pane::CloseActiveItem",
|
||||
"ctrl-w ctrl-q": "pane::CloseActiveItem",
|
||||
"ctrl-w q": "pane::CloseActiveItem",
|
||||
"ctrl-w ctrl-a": "pane::CloseAllItems",
|
||||
"ctrl-w a": "pane::CloseAllItems",
|
||||
"ctrl-w ctrl-o": "workspace::CloseInactiveTabsAndPanes",
|
||||
"ctrl-w o": "workspace::CloseInactiveTabsAndPanes",
|
||||
"ctrl-w ctrl-n": "workspace::NewFileSplitHorizontal",
|
||||
|
||||
@@ -10,8 +10,9 @@
|
||||
"light": "One Light",
|
||||
"dark": "One Dark"
|
||||
},
|
||||
"icon_theme": "Zed (Default)",
|
||||
// The name of a base set of key bindings to use.
|
||||
// This setting can take four values, each named after another
|
||||
// This setting can take six values, each named after another
|
||||
// text editor:
|
||||
//
|
||||
// 1. "VSCode"
|
||||
@@ -23,7 +24,7 @@
|
||||
"base_keymap": "VSCode",
|
||||
// Features that can be globally enabled or disabled
|
||||
"features": {
|
||||
// Which inline completion provider to use.
|
||||
// Which edit prediction provider to use.
|
||||
"inline_completion_provider": "copilot"
|
||||
},
|
||||
// The name of a font to use for rendering text in the editor
|
||||
@@ -160,8 +161,8 @@
|
||||
/// Whether to show the signature help after completion or a bracket pair inserted.
|
||||
/// If `auto_signature_help` is enabled, this setting will be treated as enabled also.
|
||||
"show_signature_help_after_edits": false,
|
||||
/// Whether to show the inline completions next to the completions provided by a language server.
|
||||
/// Only has an effect if inline completion provider supports it.
|
||||
/// Whether to show the edit predictions next to the completions provided by a language server.
|
||||
/// Only has an effect if edit prediction provider supports it.
|
||||
"show_inline_completions_in_menu": true,
|
||||
// Whether to show wrap guides (vertical rulers) in the editor.
|
||||
// Setting this to true will show a guide at the 'preferred_line_length' value
|
||||
@@ -195,14 +196,14 @@
|
||||
// Otherwise(when `true`), the closing characters are always skipped over and auto-removed
|
||||
// no matter how they were inserted.
|
||||
"always_treat_brackets_as_autoclosed": false,
|
||||
// Controls whether inline completions are shown immediately (true)
|
||||
// Controls whether edit predictions are shown immediately (true)
|
||||
// or manually by triggering `editor::ShowInlineCompletion` (false).
|
||||
"show_inline_completions": true,
|
||||
// Controls whether inline completions are shown in a given language scope.
|
||||
// Controls whether edit predictions are shown in a given language scope.
|
||||
// Example: ["string", "comment"]
|
||||
"inline_completions_disabled_in": [],
|
||||
// Whether to show tabs and spaces in the editor.
|
||||
// This setting can take three values:
|
||||
// This setting can take four values:
|
||||
//
|
||||
// 1. Draw tabs and spaces only for the selected text (default):
|
||||
// "selection"
|
||||
@@ -392,7 +393,7 @@
|
||||
/// Scrollbar-related settings
|
||||
"scrollbar": {
|
||||
/// When to show the scrollbar in the project panel.
|
||||
/// This setting can take four values:
|
||||
/// This setting can take five values:
|
||||
///
|
||||
/// 1. null (default): Inherit editor settings
|
||||
/// 2. Show the scrollbar if there's important information or
|
||||
@@ -464,7 +465,7 @@
|
||||
/// Scrollbar-related settings
|
||||
"scrollbar": {
|
||||
/// When to show the scrollbar in the project panel.
|
||||
/// This setting can take four values:
|
||||
/// This setting can take five values:
|
||||
///
|
||||
/// 1. null (default): Inherit editor settings
|
||||
/// 2. Show the scrollbar if there's important information or
|
||||
@@ -774,8 +775,22 @@
|
||||
// "load_direnv": "shell_hook"
|
||||
"load_direnv": "direct",
|
||||
"inline_completions": {
|
||||
// A list of globs representing files that inline completions should be disabled for.
|
||||
"disabled_globs": [".env"]
|
||||
// A list of globs representing files that edit predictions should be disabled for.
|
||||
"disabled_globs": [
|
||||
"**/.env*",
|
||||
"**/*.pem",
|
||||
"**/*.key",
|
||||
"**/*.cert",
|
||||
"**/*.crt",
|
||||
"**/secrets.yml"
|
||||
],
|
||||
// When to show edit predictions previews in buffer.
|
||||
// This setting takes two possible values:
|
||||
// 1. Display inline when there are no language server completions available.
|
||||
// "inline_preview": "auto"
|
||||
// 2. Display inline when holding modifier key (alt by default).
|
||||
// "inline_preview": "when_holding_modifier"
|
||||
"inline_preview": "auto"
|
||||
},
|
||||
// Settings specific to journaling
|
||||
"journal": {
|
||||
@@ -914,7 +929,7 @@
|
||||
/// Scrollbar-related settings
|
||||
"scrollbar": {
|
||||
/// When to show the scrollbar in the terminal.
|
||||
/// This setting can take four values:
|
||||
/// This setting can take five values:
|
||||
///
|
||||
/// 1. null (default): Inherit editor settings
|
||||
/// 2. Show the scrollbar if there's important information or
|
||||
|
||||
@@ -3,7 +3,7 @@ name = "anthropic"
|
||||
version = "0.1.0"
|
||||
edition.workspace = true
|
||||
publish.workspace = true
|
||||
license = "AGPL-3.0-or-later"
|
||||
license = "GPL-3.0-or-later"
|
||||
|
||||
[features]
|
||||
default = []
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
../../LICENSE-AGPL
|
||||
@@ -250,7 +250,7 @@ pub async fn stream_completion(
|
||||
.map(|output| output.0)
|
||||
}
|
||||
|
||||
/// https://docs.anthropic.com/en/api/rate-limits#response-headers
|
||||
/// <https://docs.anthropic.com/en/api/rate-limits#response-headers>
|
||||
#[derive(Debug)]
|
||||
pub struct RateLimitInfo {
|
||||
pub requests_limit: usize,
|
||||
@@ -626,7 +626,7 @@ pub struct ApiError {
|
||||
}
|
||||
|
||||
/// An Anthropic API error code.
|
||||
/// https://docs.anthropic.com/en/api/errors#http-errors
|
||||
/// <https://docs.anthropic.com/en/api/errors#http-errors>
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Copy, EnumString)]
|
||||
#[strum(serialize_all = "snake_case")]
|
||||
pub enum ApiErrorCode {
|
||||
|
||||
@@ -3,7 +3,7 @@ use std::sync::LazyLock;
|
||||
|
||||
/// Returns whether the given country code is supported by Anthropic.
|
||||
///
|
||||
/// https://www.anthropic.com/supported-countries
|
||||
/// <https://www.anthropic.com/supported-countries>
|
||||
pub fn is_supported_country(country_code: &str) -> bool {
|
||||
SUPPORTED_COUNTRIES.contains(&country_code)
|
||||
}
|
||||
|
||||
@@ -11,7 +11,6 @@ use assistant_context_editor::{
|
||||
};
|
||||
use assistant_settings::{AssistantDockPosition, AssistantSettings};
|
||||
use assistant_slash_command::SlashCommandWorkingSet;
|
||||
use assistant_tool::ToolWorkingSet;
|
||||
use client::{proto, Client, Status};
|
||||
use editor::{Editor, EditorEvent};
|
||||
use fs::Fs;
|
||||
@@ -100,11 +99,10 @@ impl AssistantPanel {
|
||||
) -> Task<Result<Entity<Self>>> {
|
||||
cx.spawn(|mut cx| async move {
|
||||
let slash_commands = Arc::new(SlashCommandWorkingSet::default());
|
||||
let tools = Arc::new(ToolWorkingSet::default());
|
||||
let context_store = workspace
|
||||
.update(&mut cx, |workspace, cx| {
|
||||
let project = workspace.project().clone();
|
||||
ContextStore::new(project, prompt_builder.clone(), slash_commands, tools, cx)
|
||||
ContextStore::new(project, prompt_builder.clone(), slash_commands, cx)
|
||||
})?
|
||||
.await?;
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ use std::sync::Arc;
|
||||
use collections::HashMap;
|
||||
use gpui::{AnyView, App, EventEmitter, FocusHandle, Focusable, Subscription};
|
||||
use language_model::{LanguageModelProvider, LanguageModelProviderId, LanguageModelRegistry};
|
||||
use ui::{prelude::*, ElevationIndex};
|
||||
use ui::{prelude::*, Divider, DividerColor, ElevationIndex};
|
||||
use zed_actions::assistant::DeployPromptLibrary;
|
||||
|
||||
pub struct AssistantConfiguration {
|
||||
@@ -91,38 +91,47 @@ impl AssistantConfiguration {
|
||||
.cloned();
|
||||
|
||||
v_flex()
|
||||
.gap_2()
|
||||
.gap_1p5()
|
||||
.child(
|
||||
h_flex()
|
||||
.justify_between()
|
||||
.child(Headline::new(provider_name.clone()).size(HeadlineSize::Small))
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.child(
|
||||
Icon::new(provider.icon())
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.child(Label::new(provider_name.clone())),
|
||||
)
|
||||
.when(provider.is_authenticated(cx), |parent| {
|
||||
parent.child(
|
||||
h_flex().justify_end().child(
|
||||
Button::new(
|
||||
SharedString::from(format!("new-thread-{provider_id}")),
|
||||
"Open New Thread",
|
||||
)
|
||||
.icon_position(IconPosition::Start)
|
||||
.icon(IconName::Plus)
|
||||
.style(ButtonStyle::Filled)
|
||||
.layer(ElevationIndex::ModalSurface)
|
||||
.on_click(cx.listener({
|
||||
let provider = provider.clone();
|
||||
move |_this, _event, _window, cx| {
|
||||
cx.emit(AssistantConfigurationEvent::NewThread(
|
||||
provider.clone(),
|
||||
))
|
||||
}
|
||||
})),
|
||||
),
|
||||
Button::new(
|
||||
SharedString::from(format!("new-thread-{provider_id}")),
|
||||
"Start New Thread",
|
||||
)
|
||||
.icon_position(IconPosition::Start)
|
||||
.icon(IconName::Plus)
|
||||
.icon_size(IconSize::Small)
|
||||
.style(ButtonStyle::Filled)
|
||||
.layer(ElevationIndex::ModalSurface)
|
||||
.label_size(LabelSize::Small)
|
||||
.on_click(cx.listener({
|
||||
let provider = provider.clone();
|
||||
move |_this, _event, _window, cx| {
|
||||
cx.emit(AssistantConfigurationEvent::NewThread(
|
||||
provider.clone(),
|
||||
))
|
||||
}
|
||||
})),
|
||||
)
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.p(DynamicSpacing::Base08.rems(cx))
|
||||
.bg(cx.theme().colors().surface_background)
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.border_1()
|
||||
.border_color(cx.theme().colors().border_variant)
|
||||
.rounded_md()
|
||||
@@ -143,26 +152,43 @@ impl Render for AssistantConfiguration {
|
||||
v_flex()
|
||||
.id("assistant-configuration")
|
||||
.track_focus(&self.focus_handle(cx))
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.bg(cx.theme().colors().panel_background)
|
||||
.size_full()
|
||||
.overflow_y_scroll()
|
||||
.child(
|
||||
h_flex().p(DynamicSpacing::Base16.rems(cx)).child(
|
||||
Button::new("open-prompt-library", "Open Prompt Library")
|
||||
.style(ButtonStyle::Filled)
|
||||
.full_width()
|
||||
.icon(IconName::Book)
|
||||
.icon_size(IconSize::Small)
|
||||
.icon_position(IconPosition::Start)
|
||||
.on_click(|_event, _window, cx| cx.dispatch_action(&DeployPromptLibrary)),
|
||||
),
|
||||
v_flex()
|
||||
.p(DynamicSpacing::Base16.rems(cx))
|
||||
.gap_1()
|
||||
.child(Headline::new("Prompt Library").size(HeadlineSize::Small))
|
||||
.child(
|
||||
Button::new("open-prompt-library", "Open Prompt Library")
|
||||
.style(ButtonStyle::Filled)
|
||||
.layer(ElevationIndex::ModalSurface)
|
||||
.full_width()
|
||||
.icon(IconName::Book)
|
||||
.icon_size(IconSize::Small)
|
||||
.icon_position(IconPosition::Start)
|
||||
.on_click(|_event, _window, cx| {
|
||||
cx.dispatch_action(&DeployPromptLibrary)
|
||||
}),
|
||||
),
|
||||
)
|
||||
.child(Divider::horizontal().color(DividerColor::Border))
|
||||
.child(
|
||||
v_flex()
|
||||
.p(DynamicSpacing::Base16.rems(cx))
|
||||
.mt_1()
|
||||
.gap_6()
|
||||
.flex_1()
|
||||
.child(
|
||||
v_flex()
|
||||
.gap_0p5()
|
||||
.child(Headline::new("LLM Providers").size(HeadlineSize::Small))
|
||||
.child(
|
||||
Label::new("Add at least one provider to use AI-powered features.")
|
||||
.color(Color::Muted),
|
||||
),
|
||||
)
|
||||
.children(
|
||||
providers
|
||||
.into_iter()
|
||||
|
||||
@@ -134,7 +134,6 @@ impl AssistantPanel {
|
||||
project,
|
||||
prompt_builder.clone(),
|
||||
slash_commands,
|
||||
tools.clone(),
|
||||
cx,
|
||||
)
|
||||
})?
|
||||
@@ -443,7 +442,7 @@ impl AssistantPanel {
|
||||
|
||||
fn handle_assistant_configuration_event(
|
||||
&mut self,
|
||||
_model: &Entity<AssistantConfiguration>,
|
||||
_entity: &Entity<AssistantConfiguration>,
|
||||
event: &AssistantConfigurationEvent,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
@@ -613,7 +612,7 @@ impl AssistantPanel {
|
||||
})
|
||||
.unwrap_or_else(|| SharedString::from("Loading Summary…")),
|
||||
ActiveView::History | ActiveView::PromptEditorHistory => "History".into(),
|
||||
ActiveView::Configuration => "Configuration".into(),
|
||||
ActiveView::Configuration => "Assistant Settings".into(),
|
||||
};
|
||||
|
||||
let sub_title = match self.active_view {
|
||||
@@ -700,7 +699,7 @@ impl AssistantPanel {
|
||||
IconButton::new("configure-assistant", IconName::Settings)
|
||||
.icon_size(IconSize::Small)
|
||||
.style(ButtonStyle::Subtle)
|
||||
.tooltip(Tooltip::text("Configure Assistant"))
|
||||
.tooltip(Tooltip::text("Assistant Settings"))
|
||||
.on_click(move |_event, window, cx| {
|
||||
window.dispatch_action(OpenConfiguration.boxed_clone(), cx);
|
||||
}),
|
||||
|
||||
@@ -79,8 +79,8 @@ impl ContextStore {
|
||||
project.open_buffer(project_path.clone(), cx)
|
||||
})?;
|
||||
|
||||
let buffer_model = open_buffer_task.await?;
|
||||
let buffer_id = this.update(&mut cx, |_, cx| buffer_model.read(cx).remote_id())?;
|
||||
let buffer_entity = open_buffer_task.await?;
|
||||
let buffer_id = this.update(&mut cx, |_, cx| buffer_entity.read(cx).remote_id())?;
|
||||
|
||||
let already_included = this.update(&mut cx, |this, _cx| {
|
||||
match this.will_include_buffer(buffer_id, &project_path.path) {
|
||||
@@ -98,10 +98,10 @@ impl ContextStore {
|
||||
}
|
||||
|
||||
let (buffer_info, text_task) = this.update(&mut cx, |_, cx| {
|
||||
let buffer = buffer_model.read(cx);
|
||||
let buffer = buffer_entity.read(cx);
|
||||
collect_buffer_info_and_text(
|
||||
project_path.path.clone(),
|
||||
buffer_model,
|
||||
buffer_entity,
|
||||
buffer,
|
||||
cx.to_async(),
|
||||
)
|
||||
@@ -119,18 +119,18 @@ impl ContextStore {
|
||||
|
||||
pub fn add_file_from_buffer(
|
||||
&mut self,
|
||||
buffer_model: Entity<Buffer>,
|
||||
buffer_entity: Entity<Buffer>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Task<Result<()>> {
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
let (buffer_info, text_task) = this.update(&mut cx, |_, cx| {
|
||||
let buffer = buffer_model.read(cx);
|
||||
let buffer = buffer_entity.read(cx);
|
||||
let Some(file) = buffer.file() else {
|
||||
return Err(anyhow!("Buffer has no path."));
|
||||
};
|
||||
Ok(collect_buffer_info_and_text(
|
||||
file.path().clone(),
|
||||
buffer_model,
|
||||
buffer_entity,
|
||||
buffer,
|
||||
cx.to_async(),
|
||||
))
|
||||
@@ -207,11 +207,11 @@ impl ContextStore {
|
||||
let mut buffer_infos = Vec::new();
|
||||
let mut text_tasks = Vec::new();
|
||||
this.update(&mut cx, |_, cx| {
|
||||
for (path, buffer_model) in files.into_iter().zip(buffers) {
|
||||
let buffer_model = buffer_model?;
|
||||
let buffer = buffer_model.read(cx);
|
||||
for (path, buffer_entity) in files.into_iter().zip(buffers) {
|
||||
let buffer_entity = buffer_entity?;
|
||||
let buffer = buffer_entity.read(cx);
|
||||
let (buffer_info, text_task) =
|
||||
collect_buffer_info_and_text(path, buffer_model, buffer, cx.to_async());
|
||||
collect_buffer_info_and_text(path, buffer_entity, buffer, cx.to_async());
|
||||
buffer_infos.push(buffer_info);
|
||||
text_tasks.push(text_task);
|
||||
}
|
||||
@@ -429,7 +429,7 @@ pub enum FileInclusion {
|
||||
|
||||
// ContextBuffer without text.
|
||||
struct BufferInfo {
|
||||
buffer_model: Entity<Buffer>,
|
||||
buffer_entity: Entity<Buffer>,
|
||||
id: BufferId,
|
||||
version: clock::Global,
|
||||
}
|
||||
@@ -437,7 +437,7 @@ struct BufferInfo {
|
||||
fn make_context_buffer(info: BufferInfo, text: SharedString) -> ContextBuffer {
|
||||
ContextBuffer {
|
||||
id: info.id,
|
||||
buffer: info.buffer_model,
|
||||
buffer: info.buffer_entity,
|
||||
version: info.version,
|
||||
text,
|
||||
}
|
||||
@@ -445,13 +445,13 @@ fn make_context_buffer(info: BufferInfo, text: SharedString) -> ContextBuffer {
|
||||
|
||||
fn collect_buffer_info_and_text(
|
||||
path: Arc<Path>,
|
||||
buffer_model: Entity<Buffer>,
|
||||
buffer_entity: Entity<Buffer>,
|
||||
buffer: &Buffer,
|
||||
cx: AsyncApp,
|
||||
) -> (BufferInfo, Task<SharedString>) {
|
||||
let buffer_info = BufferInfo {
|
||||
id: buffer.remote_id(),
|
||||
buffer_model,
|
||||
buffer_entity,
|
||||
version: buffer.version(),
|
||||
};
|
||||
// Important to collect version at the same time as content so that staleness logic is correct.
|
||||
|
||||
@@ -92,8 +92,8 @@ impl ContextStrip {
|
||||
let active_item = workspace.read(cx).active_item(cx)?;
|
||||
|
||||
let editor = active_item.to_any().downcast::<Editor>().ok()?.read(cx);
|
||||
let active_buffer_model = editor.buffer().read(cx).as_singleton()?;
|
||||
let active_buffer = active_buffer_model.read(cx);
|
||||
let active_buffer_entity = editor.buffer().read(cx).as_singleton()?;
|
||||
let active_buffer = active_buffer_entity.read(cx);
|
||||
|
||||
let path = active_buffer.file()?.path();
|
||||
|
||||
@@ -115,7 +115,7 @@ impl ContextStrip {
|
||||
|
||||
Some(SuggestedContext::File {
|
||||
name,
|
||||
buffer: active_buffer_model.downgrade(),
|
||||
buffer: active_buffer_entity.downgrade(),
|
||||
icon_path,
|
||||
})
|
||||
}
|
||||
@@ -393,9 +393,9 @@ impl Render for ContextStrip {
|
||||
.on_action(cx.listener(Self::remove_focused_context))
|
||||
.on_action(cx.listener(Self::accept_suggested_context))
|
||||
.on_children_prepainted({
|
||||
let model = cx.entity().downgrade();
|
||||
let entity = cx.entity().downgrade();
|
||||
move |children_bounds, _window, cx| {
|
||||
model
|
||||
entity
|
||||
.update(cx, |this, _| {
|
||||
this.children_bounds = Some(children_bounds);
|
||||
})
|
||||
|
||||
@@ -12,6 +12,7 @@ use language_model_selector::LanguageModelSelector;
|
||||
use rope::Point;
|
||||
use settings::Settings;
|
||||
use std::time::Duration;
|
||||
use text::Bias;
|
||||
use theme::ThemeSettings;
|
||||
use ui::{
|
||||
prelude::*, ButtonLike, KeyBinding, PopoverMenu, PopoverMenuHandle, Switch, TintColor, Tooltip,
|
||||
@@ -239,7 +240,10 @@ impl MessageEditor {
|
||||
let snapshot = editor.buffer().read(cx).snapshot(cx);
|
||||
let newest_cursor = editor.selections.newest::<Point>(cx).head();
|
||||
if newest_cursor.column > 0 {
|
||||
let behind_cursor = Point::new(newest_cursor.row, newest_cursor.column - 1);
|
||||
let behind_cursor = snapshot.clip_point(
|
||||
Point::new(newest_cursor.row, newest_cursor.column - 1),
|
||||
Bias::Left,
|
||||
);
|
||||
let char_behind_cursor = snapshot.chars_at(behind_cursor).next();
|
||||
if char_behind_cursor == Some('@') {
|
||||
self.inline_context_picker_menu_handle.show(window, cx);
|
||||
|
||||
@@ -16,14 +16,12 @@ anyhow.workspace = true
|
||||
assistant_settings.workspace = true
|
||||
assistant_slash_command.workspace = true
|
||||
assistant_slash_commands.workspace = true
|
||||
assistant_tool.workspace = true
|
||||
chrono.workspace = true
|
||||
client.workspace = true
|
||||
clock.workspace = true
|
||||
collections.workspace = true
|
||||
context_server.workspace = true
|
||||
editor.workspace = true
|
||||
feature_flags.workspace = true
|
||||
fs.workspace = true
|
||||
futures.workspace = true
|
||||
fuzzy.workspace = true
|
||||
|
||||
@@ -8,11 +8,9 @@ use assistant_slash_command::{
|
||||
SlashCommandResult, SlashCommandWorkingSet,
|
||||
};
|
||||
use assistant_slash_commands::FileCommandMetadata;
|
||||
use assistant_tool::ToolWorkingSet;
|
||||
use client::{self, proto, telemetry::Telemetry};
|
||||
use clock::ReplicaId;
|
||||
use collections::{HashMap, HashSet};
|
||||
use feature_flags::{FeatureFlagAppExt, ToolUseFeatureFlag};
|
||||
use fs::{Fs, RemoveOptions};
|
||||
use futures::{future::Shared, FutureExt, StreamExt};
|
||||
use gpui::{
|
||||
@@ -23,7 +21,6 @@ use language::{AnchorRangeExt, Bias, Buffer, LanguageRegistry, OffsetRangeExt, P
|
||||
use language_model::{
|
||||
LanguageModel, LanguageModelCacheConfiguration, LanguageModelCompletionEvent,
|
||||
LanguageModelImage, LanguageModelRegistry, LanguageModelRequest, LanguageModelRequestMessage,
|
||||
LanguageModelRequestTool, LanguageModelToolResult, LanguageModelToolUse,
|
||||
LanguageModelToolUseId, MessageContent, Role, StopReason,
|
||||
};
|
||||
use language_models::{
|
||||
@@ -438,11 +435,6 @@ pub enum ContextEvent {
|
||||
SlashCommandOutputSectionAdded {
|
||||
section: SlashCommandOutputSection<language::Anchor>,
|
||||
},
|
||||
UsePendingTools,
|
||||
ToolFinished {
|
||||
tool_use_id: LanguageModelToolUseId,
|
||||
output_range: Range<language::Anchor>,
|
||||
},
|
||||
Operation(ContextOperation),
|
||||
}
|
||||
|
||||
@@ -528,21 +520,12 @@ pub enum Content {
|
||||
render_image: Arc<RenderImage>,
|
||||
image: Shared<Task<Option<LanguageModelImage>>>,
|
||||
},
|
||||
ToolUse {
|
||||
range: Range<language::Anchor>,
|
||||
tool_use: LanguageModelToolUse,
|
||||
},
|
||||
ToolResult {
|
||||
range: Range<language::Anchor>,
|
||||
tool_use_id: LanguageModelToolUseId,
|
||||
},
|
||||
}
|
||||
|
||||
impl Content {
|
||||
fn range(&self) -> Range<language::Anchor> {
|
||||
match self {
|
||||
Self::Image { anchor, .. } => *anchor..*anchor,
|
||||
Self::ToolUse { range, .. } | Self::ToolResult { range, .. } => range.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -599,9 +582,7 @@ pub struct AssistantContext {
|
||||
invoked_slash_commands: HashMap<InvokedSlashCommandId, InvokedSlashCommand>,
|
||||
edits_since_last_parse: language::Subscription,
|
||||
slash_commands: Arc<SlashCommandWorkingSet>,
|
||||
tools: Arc<ToolWorkingSet>,
|
||||
slash_command_output_sections: Vec<SlashCommandOutputSection<language::Anchor>>,
|
||||
pending_tool_uses_by_id: HashMap<LanguageModelToolUseId, PendingToolUse>,
|
||||
message_anchors: Vec<MessageAnchor>,
|
||||
contents: Vec<Content>,
|
||||
messages_metadata: HashMap<MessageId, MessageMetadata>,
|
||||
@@ -654,7 +635,6 @@ impl AssistantContext {
|
||||
telemetry: Option<Arc<Telemetry>>,
|
||||
prompt_builder: Arc<PromptBuilder>,
|
||||
slash_commands: Arc<SlashCommandWorkingSet>,
|
||||
tools: Arc<ToolWorkingSet>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
Self::new(
|
||||
@@ -664,7 +644,6 @@ impl AssistantContext {
|
||||
language_registry,
|
||||
prompt_builder,
|
||||
slash_commands,
|
||||
tools,
|
||||
project,
|
||||
telemetry,
|
||||
cx,
|
||||
@@ -679,7 +658,6 @@ impl AssistantContext {
|
||||
language_registry: Arc<LanguageRegistry>,
|
||||
prompt_builder: Arc<PromptBuilder>,
|
||||
slash_commands: Arc<SlashCommandWorkingSet>,
|
||||
tools: Arc<ToolWorkingSet>,
|
||||
project: Option<Entity<Project>>,
|
||||
telemetry: Option<Arc<Telemetry>>,
|
||||
cx: &mut Context<Self>,
|
||||
@@ -707,7 +685,6 @@ impl AssistantContext {
|
||||
messages_metadata: Default::default(),
|
||||
parsed_slash_commands: Vec::new(),
|
||||
invoked_slash_commands: HashMap::default(),
|
||||
pending_tool_uses_by_id: HashMap::default(),
|
||||
slash_command_output_sections: Vec::new(),
|
||||
edits_since_last_parse: edits_since_last_slash_command_parse,
|
||||
summary: None,
|
||||
@@ -725,7 +702,6 @@ impl AssistantContext {
|
||||
project,
|
||||
language_registry,
|
||||
slash_commands,
|
||||
tools,
|
||||
patches: Vec::new(),
|
||||
xml_tags: Vec::new(),
|
||||
prompt_builder,
|
||||
@@ -802,7 +778,6 @@ impl AssistantContext {
|
||||
language_registry: Arc<LanguageRegistry>,
|
||||
prompt_builder: Arc<PromptBuilder>,
|
||||
slash_commands: Arc<SlashCommandWorkingSet>,
|
||||
tools: Arc<ToolWorkingSet>,
|
||||
project: Option<Entity<Project>>,
|
||||
telemetry: Option<Arc<Telemetry>>,
|
||||
cx: &mut Context<Self>,
|
||||
@@ -815,7 +790,6 @@ impl AssistantContext {
|
||||
language_registry,
|
||||
prompt_builder,
|
||||
slash_commands,
|
||||
tools,
|
||||
project,
|
||||
telemetry,
|
||||
cx,
|
||||
@@ -848,10 +822,6 @@ impl AssistantContext {
|
||||
&self.slash_commands
|
||||
}
|
||||
|
||||
pub fn tools(&self) -> &Arc<ToolWorkingSet> {
|
||||
&self.tools
|
||||
}
|
||||
|
||||
pub fn set_capability(&mut self, capability: language::Capability, cx: &mut Context<Self>) {
|
||||
self.buffer
|
||||
.update(cx, |buffer, cx| buffer.set_capability(capability, cx));
|
||||
@@ -1177,14 +1147,6 @@ impl AssistantContext {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn pending_tool_uses(&self) -> Vec<&PendingToolUse> {
|
||||
self.pending_tool_uses_by_id.values().collect()
|
||||
}
|
||||
|
||||
pub fn get_tool_use_by_id(&self, id: &LanguageModelToolUseId) -> Option<&PendingToolUse> {
|
||||
self.pending_tool_uses_by_id.get(id)
|
||||
}
|
||||
|
||||
fn set_language(&mut self, cx: &mut Context<Self>) {
|
||||
let markdown = self.language_registry.language_for_name("Markdown");
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
@@ -2206,68 +2168,6 @@ impl AssistantContext {
|
||||
);
|
||||
}
|
||||
|
||||
pub fn insert_tool_output(
|
||||
&mut self,
|
||||
tool_use_id: LanguageModelToolUseId,
|
||||
output: Task<Result<String>>,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let insert_output_task = cx.spawn(|this, mut cx| {
|
||||
let tool_use_id = tool_use_id.clone();
|
||||
async move {
|
||||
let output = output.await;
|
||||
this.update(&mut cx, |this, cx| match output {
|
||||
Ok(mut output) => {
|
||||
const NEWLINE: char = '\n';
|
||||
|
||||
if !output.ends_with(NEWLINE) {
|
||||
output.push(NEWLINE);
|
||||
}
|
||||
|
||||
let anchor_range = this.buffer.update(cx, |buffer, cx| {
|
||||
let insert_start = buffer.len().to_offset(buffer);
|
||||
let insert_end = insert_start;
|
||||
|
||||
let start = insert_start;
|
||||
let end = start + output.len() - NEWLINE.len_utf8();
|
||||
|
||||
buffer.edit([(insert_start..insert_end, output)], None, cx);
|
||||
|
||||
let output_range = buffer.anchor_after(start)..buffer.anchor_after(end);
|
||||
|
||||
output_range
|
||||
});
|
||||
|
||||
this.insert_content(
|
||||
Content::ToolResult {
|
||||
range: anchor_range.clone(),
|
||||
tool_use_id: tool_use_id.clone(),
|
||||
},
|
||||
cx,
|
||||
);
|
||||
|
||||
cx.emit(ContextEvent::ToolFinished {
|
||||
tool_use_id,
|
||||
output_range: anchor_range,
|
||||
});
|
||||
}
|
||||
Err(err) => {
|
||||
if let Some(tool_use) = this.pending_tool_uses_by_id.get_mut(&tool_use_id) {
|
||||
tool_use.status = PendingToolUseStatus::Error(err.to_string());
|
||||
}
|
||||
}
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
});
|
||||
|
||||
if let Some(tool_use) = self.pending_tool_uses_by_id.get_mut(&tool_use_id) {
|
||||
tool_use.status = PendingToolUseStatus::Running {
|
||||
_task: insert_output_task.shared(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
pub fn completion_provider_changed(&mut self, cx: &mut Context<Self>) {
|
||||
self.count_remaining_tokens(cx);
|
||||
}
|
||||
@@ -2298,23 +2198,7 @@ impl AssistantContext {
|
||||
// Compute which messages to cache, including the last one.
|
||||
self.mark_cache_anchors(&model.cache_configuration(), false, cx);
|
||||
|
||||
let mut request = self.to_completion_request(request_type, cx);
|
||||
|
||||
// Don't attach tools for now; we'll be removing tool use from
|
||||
// Assistant1 shortly.
|
||||
#[allow(clippy::overly_complex_bool_expr)]
|
||||
if false && cx.has_flag::<ToolUseFeatureFlag>() {
|
||||
request.tools = self
|
||||
.tools
|
||||
.tools(cx)
|
||||
.into_iter()
|
||||
.map(|tool| LanguageModelRequestTool {
|
||||
name: tool.name(),
|
||||
description: tool.description(),
|
||||
input_schema: tool.input_schema(),
|
||||
})
|
||||
.collect();
|
||||
}
|
||||
let request = self.to_completion_request(request_type, cx);
|
||||
|
||||
let assistant_message = self
|
||||
.insert_message_after(last_message_id, Role::Assistant, MessageStatus::Pending, cx)
|
||||
@@ -2371,44 +2255,7 @@ impl AssistantContext {
|
||||
cx,
|
||||
);
|
||||
}
|
||||
LanguageModelCompletionEvent::ToolUse(tool_use) => {
|
||||
const NEWLINE: char = '\n';
|
||||
|
||||
let mut text = String::new();
|
||||
text.push(NEWLINE);
|
||||
text.push_str(
|
||||
&serde_json::to_string_pretty(&tool_use)
|
||||
.expect("failed to serialize tool use to JSON"),
|
||||
);
|
||||
text.push(NEWLINE);
|
||||
let text_len = text.len();
|
||||
|
||||
buffer.edit(
|
||||
[(
|
||||
message_old_end_offset..message_old_end_offset,
|
||||
text,
|
||||
)],
|
||||
None,
|
||||
cx,
|
||||
);
|
||||
|
||||
let start_ix = message_old_end_offset + NEWLINE.len_utf8();
|
||||
let end_ix =
|
||||
message_old_end_offset + text_len - NEWLINE.len_utf8();
|
||||
let source_range = buffer.anchor_after(start_ix)
|
||||
..buffer.anchor_after(end_ix);
|
||||
|
||||
this.pending_tool_uses_by_id.insert(
|
||||
tool_use.id.clone(),
|
||||
PendingToolUse {
|
||||
id: tool_use.id,
|
||||
name: tool_use.name,
|
||||
input: tool_use.input,
|
||||
status: PendingToolUseStatus::Idle,
|
||||
source_range,
|
||||
},
|
||||
);
|
||||
}
|
||||
LanguageModelCompletionEvent::ToolUse(_) => {}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -2491,9 +2338,7 @@ impl AssistantContext {
|
||||
|
||||
if let Ok(stop_reason) = result {
|
||||
match stop_reason {
|
||||
StopReason::ToolUse => {
|
||||
cx.emit(ContextEvent::UsePendingTools);
|
||||
}
|
||||
StopReason::ToolUse => {}
|
||||
StopReason::EndTurn => {}
|
||||
StopReason::MaxTokens => {}
|
||||
}
|
||||
@@ -2572,23 +2417,6 @@ impl AssistantContext {
|
||||
.push(language_model::MessageContent::Image(image));
|
||||
}
|
||||
}
|
||||
Content::ToolUse { tool_use, .. } => {
|
||||
request_message
|
||||
.content
|
||||
.push(language_model::MessageContent::ToolUse(tool_use.clone()));
|
||||
}
|
||||
Content::ToolResult { tool_use_id, .. } => {
|
||||
request_message.content.push(
|
||||
language_model::MessageContent::ToolResult(
|
||||
LanguageModelToolResult {
|
||||
tool_use_id: tool_use_id.to_string(),
|
||||
is_error: false,
|
||||
content: collect_text_content(buffer, range.clone())
|
||||
.unwrap_or_default(),
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
offset = range.end;
|
||||
|
||||
@@ -8,7 +8,6 @@ use assistant_slash_command::{
|
||||
SlashCommandOutputSection, SlashCommandRegistry, SlashCommandResult, SlashCommandWorkingSet,
|
||||
};
|
||||
use assistant_slash_commands::FileSlashCommand;
|
||||
use assistant_tool::ToolWorkingSet;
|
||||
use collections::{HashMap, HashSet};
|
||||
use fs::FakeFs;
|
||||
use futures::{
|
||||
@@ -56,7 +55,6 @@ fn test_inserting_and_removing_messages(cx: &mut App) {
|
||||
None,
|
||||
prompt_builder.clone(),
|
||||
Arc::new(SlashCommandWorkingSet::default()),
|
||||
Arc::new(ToolWorkingSet::default()),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
@@ -197,7 +195,6 @@ fn test_message_splitting(cx: &mut App) {
|
||||
None,
|
||||
prompt_builder.clone(),
|
||||
Arc::new(SlashCommandWorkingSet::default()),
|
||||
Arc::new(ToolWorkingSet::default()),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
@@ -300,7 +297,6 @@ fn test_messages_for_offsets(cx: &mut App) {
|
||||
None,
|
||||
prompt_builder.clone(),
|
||||
Arc::new(SlashCommandWorkingSet::default()),
|
||||
Arc::new(ToolWorkingSet::default()),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
@@ -414,7 +410,6 @@ async fn test_slash_commands(cx: &mut TestAppContext) {
|
||||
None,
|
||||
prompt_builder.clone(),
|
||||
Arc::new(SlashCommandWorkingSet::default()),
|
||||
Arc::new(ToolWorkingSet::default()),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
@@ -704,7 +699,6 @@ async fn test_workflow_step_parsing(cx: &mut TestAppContext) {
|
||||
None,
|
||||
prompt_builder.clone(),
|
||||
Arc::new(SlashCommandWorkingSet::default()),
|
||||
Arc::new(ToolWorkingSet::default()),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
@@ -969,7 +963,6 @@ async fn test_workflow_step_parsing(cx: &mut TestAppContext) {
|
||||
registry.clone(),
|
||||
prompt_builder.clone(),
|
||||
Arc::new(SlashCommandWorkingSet::default()),
|
||||
Arc::new(ToolWorkingSet::default()),
|
||||
None,
|
||||
None,
|
||||
cx,
|
||||
@@ -1088,7 +1081,6 @@ async fn test_serialization(cx: &mut TestAppContext) {
|
||||
None,
|
||||
prompt_builder.clone(),
|
||||
Arc::new(SlashCommandWorkingSet::default()),
|
||||
Arc::new(ToolWorkingSet::default()),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
@@ -1132,7 +1124,6 @@ async fn test_serialization(cx: &mut TestAppContext) {
|
||||
registry.clone(),
|
||||
prompt_builder.clone(),
|
||||
Arc::new(SlashCommandWorkingSet::default()),
|
||||
Arc::new(ToolWorkingSet::default()),
|
||||
None,
|
||||
None,
|
||||
cx,
|
||||
@@ -1191,7 +1182,6 @@ async fn test_random_context_collaboration(cx: &mut TestAppContext, mut rng: Std
|
||||
registry.clone(),
|
||||
prompt_builder.clone(),
|
||||
Arc::new(SlashCommandWorkingSet::default()),
|
||||
Arc::new(ToolWorkingSet::default()),
|
||||
None,
|
||||
None,
|
||||
cx,
|
||||
@@ -1451,7 +1441,6 @@ fn test_mark_cache_anchors(cx: &mut App) {
|
||||
None,
|
||||
prompt_builder.clone(),
|
||||
Arc::new(SlashCommandWorkingSet::default()),
|
||||
Arc::new(ToolWorkingSet::default()),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
@@ -5,7 +5,6 @@ use assistant_slash_commands::{
|
||||
selections_creases, DefaultSlashCommand, DocsSlashCommand, DocsSlashCommandArgs,
|
||||
FileSlashCommand,
|
||||
};
|
||||
use assistant_tool::ToolWorkingSet;
|
||||
use client::{proto, zed_urls};
|
||||
use collections::{hash_map, BTreeSet, HashMap, HashSet};
|
||||
use editor::{
|
||||
@@ -33,7 +32,7 @@ use indexed_docs::IndexedDocsStore;
|
||||
use language::{language_settings::SoftWrap, BufferSnapshot, LspAdapterDelegate, ToOffset};
|
||||
use language_model::{
|
||||
LanguageModelImage, LanguageModelProvider, LanguageModelProviderTosView, LanguageModelRegistry,
|
||||
LanguageModelToolUse, Role,
|
||||
Role,
|
||||
};
|
||||
use language_model_selector::{LanguageModelSelector, LanguageModelSelectorPopoverMenu};
|
||||
use multi_buffer::MultiBufferRow;
|
||||
@@ -171,7 +170,6 @@ pub struct ContextEditor {
|
||||
context: Entity<AssistantContext>,
|
||||
fs: Arc<dyn Fs>,
|
||||
slash_commands: Arc<SlashCommandWorkingSet>,
|
||||
tools: Arc<ToolWorkingSet>,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
project: Entity<Project>,
|
||||
lsp_adapter_delegate: Option<Arc<dyn LspAdapterDelegate>>,
|
||||
@@ -182,7 +180,6 @@ pub struct ContextEditor {
|
||||
remote_id: Option<workspace::ViewId>,
|
||||
pending_slash_command_creases: HashMap<Range<language::Anchor>, CreaseId>,
|
||||
invoked_slash_command_creases: HashMap<InvokedSlashCommandId, CreaseId>,
|
||||
pending_tool_use_creases: HashMap<Range<language::Anchor>, CreaseId>,
|
||||
_subscriptions: Vec<Subscription>,
|
||||
patches: HashMap<Range<language::Anchor>, PatchViewState>,
|
||||
active_patch: Option<Range<language::Anchor>>,
|
||||
@@ -244,11 +241,9 @@ impl ContextEditor {
|
||||
let sections = context.read(cx).slash_command_output_sections().to_vec();
|
||||
let patch_ranges = context.read(cx).patch_ranges().collect::<Vec<_>>();
|
||||
let slash_commands = context.read(cx).slash_commands().clone();
|
||||
let tools = context.read(cx).tools().clone();
|
||||
let mut this = Self {
|
||||
context,
|
||||
slash_commands,
|
||||
tools,
|
||||
editor,
|
||||
lsp_adapter_delegate,
|
||||
blocks: Default::default(),
|
||||
@@ -260,7 +255,6 @@ impl ContextEditor {
|
||||
project,
|
||||
pending_slash_command_creases: HashMap::default(),
|
||||
invoked_slash_command_creases: HashMap::default(),
|
||||
pending_tool_use_creases: HashMap::default(),
|
||||
_subscriptions,
|
||||
patches: HashMap::default(),
|
||||
active_patch: None,
|
||||
@@ -465,7 +459,7 @@ impl ContextEditor {
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
if self.editor.read(cx).has_active_completions_menu() {
|
||||
if self.editor.read(cx).has_visible_completions_menu() {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -580,87 +574,6 @@ impl ContextEditor {
|
||||
cx,
|
||||
);
|
||||
}
|
||||
|
||||
let new_tool_uses = self
|
||||
.context
|
||||
.read(cx)
|
||||
.pending_tool_uses()
|
||||
.into_iter()
|
||||
.filter(|tool_use| {
|
||||
!self
|
||||
.pending_tool_use_creases
|
||||
.contains_key(&tool_use.source_range)
|
||||
})
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let buffer = editor.buffer().read(cx).snapshot(cx);
|
||||
let (excerpt_id, _buffer_id, _) = buffer.as_singleton().unwrap();
|
||||
let excerpt_id = *excerpt_id;
|
||||
|
||||
let mut buffer_rows_to_fold = BTreeSet::new();
|
||||
|
||||
let creases = new_tool_uses
|
||||
.iter()
|
||||
.map(|tool_use| {
|
||||
let placeholder = FoldPlaceholder {
|
||||
render: render_fold_icon_button(
|
||||
cx.entity().downgrade(),
|
||||
IconName::PocketKnife,
|
||||
tool_use.name.clone().into(),
|
||||
),
|
||||
..Default::default()
|
||||
};
|
||||
let render_trailer =
|
||||
move |_row, _unfold, _window: &mut Window, _cx: &mut App| {
|
||||
Empty.into_any()
|
||||
};
|
||||
|
||||
let start = buffer
|
||||
.anchor_in_excerpt(excerpt_id, tool_use.source_range.start)
|
||||
.unwrap();
|
||||
let end = buffer
|
||||
.anchor_in_excerpt(excerpt_id, tool_use.source_range.end)
|
||||
.unwrap();
|
||||
|
||||
let buffer_row = MultiBufferRow(start.to_point(&buffer).row);
|
||||
buffer_rows_to_fold.insert(buffer_row);
|
||||
|
||||
self.context.update(cx, |context, cx| {
|
||||
context.insert_content(
|
||||
Content::ToolUse {
|
||||
range: tool_use.source_range.clone(),
|
||||
tool_use: LanguageModelToolUse {
|
||||
id: tool_use.id.clone(),
|
||||
name: tool_use.name.clone(),
|
||||
input: tool_use.input.clone(),
|
||||
},
|
||||
},
|
||||
cx,
|
||||
);
|
||||
});
|
||||
|
||||
Crease::inline(
|
||||
start..end,
|
||||
placeholder,
|
||||
fold_toggle("tool-use"),
|
||||
render_trailer,
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let crease_ids = editor.insert_creases(creases, cx);
|
||||
|
||||
for buffer_row in buffer_rows_to_fold.into_iter().rev() {
|
||||
editor.fold_at(&FoldAt { buffer_row }, window, cx);
|
||||
}
|
||||
|
||||
self.pending_tool_use_creases.extend(
|
||||
new_tool_uses
|
||||
.iter()
|
||||
.map(|tool_use| tool_use.source_range.clone())
|
||||
.zip(crease_ids),
|
||||
);
|
||||
});
|
||||
}
|
||||
ContextEvent::PatchesUpdated { removed, updated } => {
|
||||
@@ -758,66 +671,6 @@ impl ContextEditor {
|
||||
ContextEvent::SlashCommandOutputSectionAdded { section } => {
|
||||
self.insert_slash_command_output_sections([section.clone()], false, window, cx);
|
||||
}
|
||||
ContextEvent::UsePendingTools => {
|
||||
let pending_tool_uses = self
|
||||
.context
|
||||
.read(cx)
|
||||
.pending_tool_uses()
|
||||
.into_iter()
|
||||
.filter(|tool_use| tool_use.status.is_idle())
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
for tool_use in pending_tool_uses {
|
||||
if let Some(tool) = self.tools.tool(&tool_use.name, cx) {
|
||||
let task = tool.run(tool_use.input, self.workspace.clone(), window, cx);
|
||||
|
||||
self.context.update(cx, |context, cx| {
|
||||
context.insert_tool_output(tool_use.id.clone(), task, cx);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
ContextEvent::ToolFinished {
|
||||
tool_use_id,
|
||||
output_range,
|
||||
} => {
|
||||
self.editor.update(cx, |editor, cx| {
|
||||
let buffer = editor.buffer().read(cx).snapshot(cx);
|
||||
let (excerpt_id, _buffer_id, _) = buffer.as_singleton().unwrap();
|
||||
let excerpt_id = *excerpt_id;
|
||||
|
||||
let placeholder = FoldPlaceholder {
|
||||
render: render_fold_icon_button(
|
||||
cx.entity().downgrade(),
|
||||
IconName::PocketKnife,
|
||||
format!("Tool Result: {tool_use_id}").into(),
|
||||
),
|
||||
..Default::default()
|
||||
};
|
||||
let render_trailer =
|
||||
move |_row, _unfold, _window: &mut Window, _cx: &mut App| Empty.into_any();
|
||||
|
||||
let start = buffer
|
||||
.anchor_in_excerpt(excerpt_id, output_range.start)
|
||||
.unwrap();
|
||||
let end = buffer
|
||||
.anchor_in_excerpt(excerpt_id, output_range.end)
|
||||
.unwrap();
|
||||
|
||||
let buffer_row = MultiBufferRow(start.to_point(&buffer).row);
|
||||
|
||||
let crease = Crease::inline(
|
||||
start..end,
|
||||
placeholder,
|
||||
fold_toggle("tool-use"),
|
||||
render_trailer,
|
||||
);
|
||||
|
||||
editor.insert_creases([crease], cx);
|
||||
editor.fold_at(&FoldAt { buffer_row }, window, cx);
|
||||
});
|
||||
}
|
||||
ContextEvent::Operation(_) => {}
|
||||
ContextEvent::ShowAssistError(error_message) => {
|
||||
self.last_error = Some(AssistError::Message(error_message.clone()));
|
||||
@@ -2112,18 +1965,13 @@ impl ContextEditor {
|
||||
.context
|
||||
.read(cx)
|
||||
.contents(cx)
|
||||
.filter_map(|content| {
|
||||
if let Content::Image {
|
||||
anchor,
|
||||
render_image,
|
||||
..
|
||||
} = content
|
||||
{
|
||||
Some((anchor, render_image))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.map(
|
||||
|Content::Image {
|
||||
anchor,
|
||||
render_image,
|
||||
..
|
||||
}| (anchor, render_image),
|
||||
)
|
||||
.filter_map(|(anchor, render_image)| {
|
||||
const MAX_HEIGHT_IN_LINES: u32 = 8;
|
||||
let anchor = buffer.anchor_in_excerpt(excerpt_id, anchor).unwrap();
|
||||
|
||||
@@ -4,12 +4,11 @@ use crate::{
|
||||
};
|
||||
use anyhow::{anyhow, Context as _, Result};
|
||||
use assistant_slash_command::{SlashCommandId, SlashCommandWorkingSet};
|
||||
use assistant_tool::{ToolId, ToolWorkingSet};
|
||||
use client::{proto, telemetry::Telemetry, Client, TypedEnvelope};
|
||||
use clock::ReplicaId;
|
||||
use collections::HashMap;
|
||||
use context_server::manager::ContextServerManager;
|
||||
use context_server::{ContextServerFactoryRegistry, ContextServerTool};
|
||||
use context_server::ContextServerFactoryRegistry;
|
||||
use fs::Fs;
|
||||
use futures::StreamExt;
|
||||
use fuzzy::StringMatchCandidate;
|
||||
@@ -32,11 +31,11 @@ use std::{
|
||||
use util::{ResultExt, TryFutureExt};
|
||||
|
||||
pub(crate) fn init(client: &AnyProtoClient) {
|
||||
client.add_model_message_handler(ContextStore::handle_advertise_contexts);
|
||||
client.add_model_request_handler(ContextStore::handle_open_context);
|
||||
client.add_model_request_handler(ContextStore::handle_create_context);
|
||||
client.add_model_message_handler(ContextStore::handle_update_context);
|
||||
client.add_model_request_handler(ContextStore::handle_synchronize_contexts);
|
||||
client.add_entity_message_handler(ContextStore::handle_advertise_contexts);
|
||||
client.add_entity_request_handler(ContextStore::handle_open_context);
|
||||
client.add_entity_request_handler(ContextStore::handle_create_context);
|
||||
client.add_entity_message_handler(ContextStore::handle_update_context);
|
||||
client.add_entity_request_handler(ContextStore::handle_synchronize_contexts);
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
@@ -50,12 +49,10 @@ pub struct ContextStore {
|
||||
contexts_metadata: Vec<SavedContextMetadata>,
|
||||
context_server_manager: Entity<ContextServerManager>,
|
||||
context_server_slash_command_ids: HashMap<Arc<str>, Vec<SlashCommandId>>,
|
||||
context_server_tool_ids: HashMap<Arc<str>, Vec<ToolId>>,
|
||||
host_contexts: Vec<RemoteContextMetadata>,
|
||||
fs: Arc<dyn Fs>,
|
||||
languages: Arc<LanguageRegistry>,
|
||||
slash_commands: Arc<SlashCommandWorkingSet>,
|
||||
tools: Arc<ToolWorkingSet>,
|
||||
telemetry: Arc<Telemetry>,
|
||||
_watch_updates: Task<Option<()>>,
|
||||
client: Arc<Client>,
|
||||
@@ -98,7 +95,6 @@ impl ContextStore {
|
||||
project: Entity<Project>,
|
||||
prompt_builder: Arc<PromptBuilder>,
|
||||
slash_commands: Arc<SlashCommandWorkingSet>,
|
||||
tools: Arc<ToolWorkingSet>,
|
||||
cx: &mut App,
|
||||
) -> Task<Result<Entity<Self>>> {
|
||||
let fs = project.read(cx).fs().clone();
|
||||
@@ -119,12 +115,10 @@ impl ContextStore {
|
||||
contexts_metadata: Vec::new(),
|
||||
context_server_manager,
|
||||
context_server_slash_command_ids: HashMap::default(),
|
||||
context_server_tool_ids: HashMap::default(),
|
||||
host_contexts: Vec::new(),
|
||||
fs,
|
||||
languages,
|
||||
slash_commands,
|
||||
tools,
|
||||
telemetry,
|
||||
_watch_updates: cx.spawn(|this, mut cx| {
|
||||
async move {
|
||||
@@ -150,11 +144,9 @@ impl ContextStore {
|
||||
this.handle_project_changed(project.clone(), cx);
|
||||
this.synchronize_contexts(cx);
|
||||
this.register_context_server_handlers(cx);
|
||||
this.reload(cx).detach_and_log_err(cx);
|
||||
this
|
||||
})?;
|
||||
this.update(&mut cx, |this, cx| this.reload(cx))?
|
||||
.await
|
||||
.log_err();
|
||||
|
||||
Ok(this)
|
||||
})
|
||||
@@ -318,7 +310,7 @@ impl ContextStore {
|
||||
.client
|
||||
.subscribe_to_entity(remote_id)
|
||||
.log_err()
|
||||
.map(|subscription| subscription.set_model(&cx.entity(), &mut cx.to_async()));
|
||||
.map(|subscription| subscription.set_entity(&cx.entity(), &mut cx.to_async()));
|
||||
self.advertise_contexts(cx);
|
||||
} else {
|
||||
self.client_subscription = None;
|
||||
@@ -367,7 +359,6 @@ impl ContextStore {
|
||||
Some(self.telemetry.clone()),
|
||||
self.prompt_builder.clone(),
|
||||
self.slash_commands.clone(),
|
||||
self.tools.clone(),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
@@ -391,7 +382,6 @@ impl ContextStore {
|
||||
let telemetry = self.telemetry.clone();
|
||||
let prompt_builder = self.prompt_builder.clone();
|
||||
let slash_commands = self.slash_commands.clone();
|
||||
let tools = self.tools.clone();
|
||||
let request = self.client.request(proto::CreateContext { project_id });
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
let response = request.await?;
|
||||
@@ -405,7 +395,6 @@ impl ContextStore {
|
||||
language_registry,
|
||||
prompt_builder,
|
||||
slash_commands,
|
||||
tools,
|
||||
Some(project),
|
||||
Some(telemetry),
|
||||
cx,
|
||||
@@ -456,7 +445,6 @@ impl ContextStore {
|
||||
});
|
||||
let prompt_builder = self.prompt_builder.clone();
|
||||
let slash_commands = self.slash_commands.clone();
|
||||
let tools = self.tools.clone();
|
||||
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
let saved_context = load.await?;
|
||||
@@ -467,7 +455,6 @@ impl ContextStore {
|
||||
languages,
|
||||
prompt_builder,
|
||||
slash_commands,
|
||||
tools,
|
||||
Some(project),
|
||||
Some(telemetry),
|
||||
cx,
|
||||
@@ -535,7 +522,6 @@ impl ContextStore {
|
||||
});
|
||||
let prompt_builder = self.prompt_builder.clone();
|
||||
let slash_commands = self.slash_commands.clone();
|
||||
let tools = self.tools.clone();
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
let response = request.await?;
|
||||
let context_proto = response.context.context("invalid context")?;
|
||||
@@ -547,7 +533,6 @@ impl ContextStore {
|
||||
language_registry,
|
||||
prompt_builder,
|
||||
slash_commands,
|
||||
tools,
|
||||
Some(project),
|
||||
Some(telemetry),
|
||||
cx,
|
||||
@@ -816,7 +801,6 @@ impl ContextStore {
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let slash_command_working_set = self.slash_commands.clone();
|
||||
let tool_working_set = self.tools.clone();
|
||||
match event {
|
||||
context_server::manager::Event::ServerStarted { server_id } => {
|
||||
if let Some(server) = context_server_manager.read(cx).get_server(server_id) {
|
||||
@@ -856,29 +840,6 @@ impl ContextStore {
|
||||
.log_err();
|
||||
}
|
||||
}
|
||||
|
||||
if protocol.capable(context_server::protocol::ServerCapability::Tools) {
|
||||
if let Some(tools) = protocol.list_tools().await.log_err() {
|
||||
let tool_ids = tools.tools.into_iter().map(|tool| {
|
||||
log::info!("registering context server tool: {:?}", tool.name);
|
||||
tool_working_set.insert(
|
||||
Arc::new(ContextServerTool::new(
|
||||
context_server_manager.clone(),
|
||||
server.id(),
|
||||
tool,
|
||||
)),
|
||||
)
|
||||
|
||||
}).collect::<Vec<_>>();
|
||||
|
||||
|
||||
this.update(&mut cx, |this, _cx| {
|
||||
this.context_server_tool_ids
|
||||
.insert(server_id, tool_ids);
|
||||
})
|
||||
.log_err();
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
@@ -890,10 +851,6 @@ impl ContextStore {
|
||||
{
|
||||
slash_command_working_set.remove(&slash_command_ids);
|
||||
}
|
||||
|
||||
if let Some(tool_ids) = self.context_server_tool_ids.remove(server_id) {
|
||||
tool_working_set.remove(&tool_ids);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ use assistant_slash_command::{AfterCompletion, SlashCommandLine, SlashCommandWor
|
||||
use editor::{CompletionProvider, Editor};
|
||||
use fuzzy::{match_strings, StringMatchCandidate};
|
||||
use gpui::{App, Context, Entity, Task, WeakEntity, Window};
|
||||
use language::{Anchor, Buffer, Documentation, LanguageServerId, ToPoint};
|
||||
use language::{Anchor, Buffer, CompletionDocumentation, LanguageServerId, ToPoint};
|
||||
use parking_lot::Mutex;
|
||||
use project::CompletionIntent;
|
||||
use rope::Point;
|
||||
@@ -120,7 +120,9 @@ impl SlashCommandCompletionProvider {
|
||||
});
|
||||
Some(project::Completion {
|
||||
old_range: name_range.clone(),
|
||||
documentation: Some(Documentation::SingleLine(command.description())),
|
||||
documentation: Some(CompletionDocumentation::SingleLine(
|
||||
command.description(),
|
||||
)),
|
||||
new_text,
|
||||
label: command.label(cx),
|
||||
server_id: LanguageServerId(0),
|
||||
|
||||
@@ -434,7 +434,7 @@ pub struct LegacyAssistantSettingsContent {
|
||||
pub default_open_ai_model: Option<OpenAiModel>,
|
||||
/// OpenAI API base URL to use when creating new chats.
|
||||
///
|
||||
/// Default: https://api.openai.com/v1
|
||||
/// Default: <https://api.openai.com/v1>
|
||||
pub openai_api_url: Option<String>,
|
||||
}
|
||||
|
||||
|
||||
@@ -323,7 +323,14 @@ fn collect_files(
|
||||
)))?;
|
||||
directory_stack.push(entry.path.clone());
|
||||
} else {
|
||||
let entry_name = format!("{}/{}", prefix_paths, &filename);
|
||||
// todo(windows)
|
||||
// Potential bug: this assumes that the path separator is always `\` on Windows
|
||||
let entry_name = format!(
|
||||
"{}{}{}",
|
||||
prefix_paths,
|
||||
std::path::MAIN_SEPARATOR_STR,
|
||||
&filename
|
||||
);
|
||||
events_tx.unbounded_send(Ok(SlashCommandEvent::StartSection {
|
||||
icon: IconName::Folder,
|
||||
label: entry_name.clone().into(),
|
||||
@@ -455,6 +462,7 @@ mod custom_path_matcher {
|
||||
use std::{fmt::Debug as _, path::Path};
|
||||
|
||||
use globset::{Glob, GlobSet, GlobSetBuilder};
|
||||
use util::paths::SanitizedPath;
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct PathMatcher {
|
||||
@@ -481,7 +489,7 @@ mod custom_path_matcher {
|
||||
pub fn new(globs: &[String]) -> Result<Self, globset::Error> {
|
||||
let globs = globs
|
||||
.into_iter()
|
||||
.map(|glob| Glob::new(&glob))
|
||||
.map(|glob| Glob::new(&SanitizedPath::from(glob).to_glob_string()))
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
let sources = globs.iter().map(|glob| glob.glob().to_owned()).collect();
|
||||
let sources_with_trailing_slash = globs
|
||||
@@ -507,7 +515,9 @@ mod custom_path_matcher {
|
||||
.zip(self.sources_with_trailing_slash.iter())
|
||||
.any(|(source, with_slash)| {
|
||||
let as_bytes = other_path.as_os_str().as_encoded_bytes();
|
||||
let with_slash = if source.ends_with("/") {
|
||||
// todo(windows)
|
||||
// Potential bug: this assumes that the path separator is always `\` on Windows
|
||||
let with_slash = if source.ends_with(std::path::MAIN_SEPARATOR_STR) {
|
||||
source.as_bytes()
|
||||
} else {
|
||||
with_slash.as_bytes()
|
||||
@@ -569,6 +579,7 @@ mod test {
|
||||
use serde_json::json;
|
||||
use settings::SettingsStore;
|
||||
use smol::stream::StreamExt;
|
||||
use util::{path, separator};
|
||||
|
||||
use super::collect_files;
|
||||
|
||||
@@ -592,7 +603,7 @@ mod test {
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
|
||||
fs.insert_tree(
|
||||
"/root",
|
||||
path!("/root"),
|
||||
json!({
|
||||
"dir": {
|
||||
"subdir": {
|
||||
@@ -607,7 +618,7 @@ mod test {
|
||||
)
|
||||
.await;
|
||||
|
||||
let project = Project::test(fs, ["/root".as_ref()], cx).await;
|
||||
let project = Project::test(fs, [path!("/root").as_ref()], cx).await;
|
||||
|
||||
let result_1 =
|
||||
cx.update(|cx| collect_files(project.clone(), &["root/dir".to_string()], cx));
|
||||
@@ -615,7 +626,7 @@ mod test {
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert!(result_1.text.starts_with("root/dir"));
|
||||
assert!(result_1.text.starts_with(separator!("root/dir")));
|
||||
// 4 files + 2 directories
|
||||
assert_eq!(result_1.sections.len(), 6);
|
||||
|
||||
@@ -631,7 +642,7 @@ mod test {
|
||||
cx.update(|cx| collect_files(project.clone(), &["root/dir*".to_string()], cx).boxed());
|
||||
let result = SlashCommandOutput::from_event_stream(result).await.unwrap();
|
||||
|
||||
assert!(result.text.starts_with("root/dir"));
|
||||
assert!(result.text.starts_with(separator!("root/dir")));
|
||||
// 5 files + 2 directories
|
||||
assert_eq!(result.sections.len(), 7);
|
||||
|
||||
@@ -645,7 +656,7 @@ mod test {
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
|
||||
fs.insert_tree(
|
||||
"/zed",
|
||||
path!("/zed"),
|
||||
json!({
|
||||
"assets": {
|
||||
"dir1": {
|
||||
@@ -670,7 +681,7 @@ mod test {
|
||||
)
|
||||
.await;
|
||||
|
||||
let project = Project::test(fs, ["/zed".as_ref()], cx).await;
|
||||
let project = Project::test(fs, [path!("/zed").as_ref()], cx).await;
|
||||
|
||||
let result =
|
||||
cx.update(|cx| collect_files(project.clone(), &["zed/assets/themes".to_string()], cx));
|
||||
@@ -679,27 +690,36 @@ mod test {
|
||||
.unwrap();
|
||||
|
||||
// Sanity check
|
||||
assert!(result.text.starts_with("zed/assets/themes\n"));
|
||||
assert!(result.text.starts_with(separator!("zed/assets/themes\n")));
|
||||
assert_eq!(result.sections.len(), 7);
|
||||
|
||||
// Ensure that full file paths are included in the real output
|
||||
assert!(result.text.contains("zed/assets/themes/andromeda/LICENSE"));
|
||||
assert!(result.text.contains("zed/assets/themes/ayu/LICENSE"));
|
||||
assert!(result.text.contains("zed/assets/themes/summercamp/LICENSE"));
|
||||
assert!(result
|
||||
.text
|
||||
.contains(separator!("zed/assets/themes/andromeda/LICENSE")));
|
||||
assert!(result
|
||||
.text
|
||||
.contains(separator!("zed/assets/themes/ayu/LICENSE")));
|
||||
assert!(result
|
||||
.text
|
||||
.contains(separator!("zed/assets/themes/summercamp/LICENSE")));
|
||||
|
||||
assert_eq!(result.sections[5].label, "summercamp");
|
||||
|
||||
// Ensure that things are in descending order, with properly relativized paths
|
||||
assert_eq!(
|
||||
result.sections[0].label,
|
||||
"zed/assets/themes/andromeda/LICENSE"
|
||||
separator!("zed/assets/themes/andromeda/LICENSE")
|
||||
);
|
||||
assert_eq!(result.sections[1].label, "andromeda");
|
||||
assert_eq!(result.sections[2].label, "zed/assets/themes/ayu/LICENSE");
|
||||
assert_eq!(
|
||||
result.sections[2].label,
|
||||
separator!("zed/assets/themes/ayu/LICENSE")
|
||||
);
|
||||
assert_eq!(result.sections[3].label, "ayu");
|
||||
assert_eq!(
|
||||
result.sections[4].label,
|
||||
"zed/assets/themes/summercamp/LICENSE"
|
||||
separator!("zed/assets/themes/summercamp/LICENSE")
|
||||
);
|
||||
|
||||
// Ensure that the project lasts until after the last await
|
||||
@@ -712,7 +732,7 @@ mod test {
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
|
||||
fs.insert_tree(
|
||||
"/zed",
|
||||
path!("/zed"),
|
||||
json!({
|
||||
"assets": {
|
||||
"themes": {
|
||||
@@ -732,7 +752,7 @@ mod test {
|
||||
)
|
||||
.await;
|
||||
|
||||
let project = Project::test(fs, ["/zed".as_ref()], cx).await;
|
||||
let project = Project::test(fs, [path!("/zed").as_ref()], cx).await;
|
||||
|
||||
let result =
|
||||
cx.update(|cx| collect_files(project.clone(), &["zed/assets/themes".to_string()], cx));
|
||||
@@ -740,26 +760,29 @@ mod test {
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert!(result.text.starts_with("zed/assets/themes\n"));
|
||||
assert_eq!(result.sections[0].label, "zed/assets/themes/LICENSE");
|
||||
assert!(result.text.starts_with(separator!("zed/assets/themes\n")));
|
||||
assert_eq!(
|
||||
result.sections[0].label,
|
||||
separator!("zed/assets/themes/LICENSE")
|
||||
);
|
||||
assert_eq!(
|
||||
result.sections[1].label,
|
||||
"zed/assets/themes/summercamp/LICENSE"
|
||||
separator!("zed/assets/themes/summercamp/LICENSE")
|
||||
);
|
||||
assert_eq!(
|
||||
result.sections[2].label,
|
||||
"zed/assets/themes/summercamp/subdir/LICENSE"
|
||||
separator!("zed/assets/themes/summercamp/subdir/LICENSE")
|
||||
);
|
||||
assert_eq!(
|
||||
result.sections[3].label,
|
||||
"zed/assets/themes/summercamp/subdir/subsubdir/LICENSE"
|
||||
separator!("zed/assets/themes/summercamp/subdir/subsubdir/LICENSE")
|
||||
);
|
||||
assert_eq!(result.sections[4].label, "subsubdir");
|
||||
assert_eq!(result.sections[5].label, "subdir");
|
||||
assert_eq!(result.sections[6].label, "summercamp");
|
||||
assert_eq!(result.sections[7].label, "zed/assets/themes");
|
||||
assert_eq!(result.sections[7].label, separator!("zed/assets/themes"));
|
||||
|
||||
assert_eq!(result.text, "zed/assets/themes\n```zed/assets/themes/LICENSE\n1\n```\n\nsummercamp\n```zed/assets/themes/summercamp/LICENSE\n1\n```\n\nsubdir\n```zed/assets/themes/summercamp/subdir/LICENSE\n1\n```\n\nsubsubdir\n```zed/assets/themes/summercamp/subdir/subsubdir/LICENSE\n3\n```\n\n");
|
||||
assert_eq!(result.text, separator!("zed/assets/themes\n```zed/assets/themes/LICENSE\n1\n```\n\nsummercamp\n```zed/assets/themes/summercamp/LICENSE\n1\n```\n\nsubdir\n```zed/assets/themes/summercamp/subdir/LICENSE\n1\n```\n\nsubsubdir\n```zed/assets/themes/summercamp/subdir/subsubdir/LICENSE\n3\n```\n\n"));
|
||||
|
||||
// Ensure that the project lasts until after the last await
|
||||
drop(project);
|
||||
|
||||
@@ -9,7 +9,7 @@ use release_channel::{AppVersion, ReleaseChannel};
|
||||
use serde::Deserialize;
|
||||
use smol::io::AsyncReadExt;
|
||||
use util::ResultExt as _;
|
||||
use workspace::notifications::NotificationId;
|
||||
use workspace::notifications::{show_app_notification, NotificationId};
|
||||
use workspace::Workspace;
|
||||
|
||||
use crate::update_notification::UpdateNotification;
|
||||
@@ -17,6 +17,7 @@ use crate::update_notification::UpdateNotification;
|
||||
actions!(auto_update, [ViewReleaseNotesLocally]);
|
||||
|
||||
pub fn init(cx: &mut App) {
|
||||
notify_if_app_was_updated(cx);
|
||||
cx.observe_new(|workspace: &mut Workspace, _window, _cx| {
|
||||
workspace.register_action(|workspace, _: &ViewReleaseNotesLocally, window, cx| {
|
||||
view_release_notes_locally(workspace, window, cx);
|
||||
@@ -124,31 +125,35 @@ fn view_release_notes_locally(
|
||||
.detach();
|
||||
}
|
||||
|
||||
pub fn notify_of_any_new_update(window: &mut Window, cx: &mut Context<Workspace>) -> Option<()> {
|
||||
let updater = AutoUpdater::get(cx)?;
|
||||
/// Shows a notification across all workspaces if an update was previously automatically installed
|
||||
/// and this notification had not yet been shown.
|
||||
pub fn notify_if_app_was_updated(cx: &mut App) {
|
||||
let Some(updater) = AutoUpdater::get(cx) else {
|
||||
return;
|
||||
};
|
||||
let version = updater.read(cx).current_version();
|
||||
let should_show_notification = updater.read(cx).should_show_update_notification(cx);
|
||||
|
||||
cx.spawn_in(window, |workspace, mut cx| async move {
|
||||
cx.spawn(|cx| async move {
|
||||
let should_show_notification = should_show_notification.await?;
|
||||
if should_show_notification {
|
||||
workspace.update(&mut cx, |workspace, cx| {
|
||||
let workspace_handle = workspace.weak_handle();
|
||||
workspace.show_notification(
|
||||
cx.update(|cx| {
|
||||
show_app_notification(
|
||||
NotificationId::unique::<UpdateNotification>(),
|
||||
cx,
|
||||
|cx| cx.new(|_| UpdateNotification::new(version, workspace_handle)),
|
||||
move |cx| {
|
||||
let workspace_handle = cx.entity().downgrade();
|
||||
cx.new(|_| UpdateNotification::new(version, workspace_handle))
|
||||
},
|
||||
);
|
||||
updater.update(cx, |updater, cx| {
|
||||
updater
|
||||
.set_should_show_update_notification(false, cx)
|
||||
.detach_and_log_err(cx);
|
||||
});
|
||||
})
|
||||
})?;
|
||||
}
|
||||
anyhow::Ok(())
|
||||
})
|
||||
.detach();
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
@@ -532,6 +532,10 @@ impl Room {
|
||||
&self.local_participant
|
||||
}
|
||||
|
||||
pub fn local_participant_user(&self, cx: &App) -> Option<Arc<User>> {
|
||||
self.user_store.read(cx).current_user()
|
||||
}
|
||||
|
||||
pub fn remote_participants(&self) -> &BTreeMap<u64, RemoteParticipant> {
|
||||
&self.remote_participants
|
||||
}
|
||||
|
||||
@@ -588,6 +588,10 @@ impl Room {
|
||||
&self.local_participant
|
||||
}
|
||||
|
||||
pub fn local_participant_user(&self, cx: &App) -> Option<Arc<User>> {
|
||||
self.user_store.read(cx).current_user()
|
||||
}
|
||||
|
||||
pub fn remote_participants(&self) -> &BTreeMap<u64, RemoteParticipant> {
|
||||
&self.remote_participants
|
||||
}
|
||||
|
||||
@@ -15,8 +15,8 @@ use util::ResultExt;
|
||||
pub const ACKNOWLEDGE_DEBOUNCE_INTERVAL: Duration = Duration::from_millis(250);
|
||||
|
||||
pub(crate) fn init(client: &AnyProtoClient) {
|
||||
client.add_model_message_handler(ChannelBuffer::handle_update_channel_buffer);
|
||||
client.add_model_message_handler(ChannelBuffer::handle_update_channel_buffer_collaborators);
|
||||
client.add_entity_message_handler(ChannelBuffer::handle_update_channel_buffer);
|
||||
client.add_entity_message_handler(ChannelBuffer::handle_update_channel_buffer_collaborators);
|
||||
}
|
||||
|
||||
pub struct ChannelBuffer {
|
||||
@@ -81,7 +81,7 @@ impl ChannelBuffer {
|
||||
collaborators: Default::default(),
|
||||
acknowledge_task: None,
|
||||
channel_id: channel.id,
|
||||
subscription: Some(subscription.set_model(&cx.entity(), &mut cx.to_async())),
|
||||
subscription: Some(subscription.set_entity(&cx.entity(), &mut cx.to_async())),
|
||||
user_store,
|
||||
channel_store,
|
||||
};
|
||||
|
||||
@@ -95,9 +95,9 @@ pub enum ChannelChatEvent {
|
||||
|
||||
impl EventEmitter<ChannelChatEvent> for ChannelChat {}
|
||||
pub fn init(client: &AnyProtoClient) {
|
||||
client.add_model_message_handler(ChannelChat::handle_message_sent);
|
||||
client.add_model_message_handler(ChannelChat::handle_message_removed);
|
||||
client.add_model_message_handler(ChannelChat::handle_message_updated);
|
||||
client.add_entity_message_handler(ChannelChat::handle_message_sent);
|
||||
client.add_entity_message_handler(ChannelChat::handle_message_removed);
|
||||
client.add_entity_message_handler(ChannelChat::handle_message_updated);
|
||||
}
|
||||
|
||||
impl ChannelChat {
|
||||
@@ -132,7 +132,7 @@ impl ChannelChat {
|
||||
last_acknowledged_id: None,
|
||||
rng: StdRng::from_entropy(),
|
||||
first_loaded_message_id: None,
|
||||
_subscription: subscription.set_model(&cx.entity(), &mut cx.to_async()),
|
||||
_subscription: subscription.set_entity(&cx.entity(), &mut cx.to_async()),
|
||||
}
|
||||
})?;
|
||||
Self::handle_loaded_messages(
|
||||
|
||||
@@ -39,8 +39,8 @@ pub struct ChannelStore {
|
||||
channel_states: HashMap<ChannelId, ChannelState>,
|
||||
outgoing_invites: HashSet<(ChannelId, UserId)>,
|
||||
update_channels_tx: mpsc::UnboundedSender<proto::UpdateChannels>,
|
||||
opened_buffers: HashMap<ChannelId, OpenedModelHandle<ChannelBuffer>>,
|
||||
opened_chats: HashMap<ChannelId, OpenedModelHandle<ChannelChat>>,
|
||||
opened_buffers: HashMap<ChannelId, OpenEntityHandle<ChannelBuffer>>,
|
||||
opened_chats: HashMap<ChannelId, OpenEntityHandle<ChannelChat>>,
|
||||
client: Arc<Client>,
|
||||
did_subscribe: bool,
|
||||
user_store: Entity<UserStore>,
|
||||
@@ -142,7 +142,7 @@ pub enum ChannelEvent {
|
||||
|
||||
impl EventEmitter<ChannelEvent> for ChannelStore {}
|
||||
|
||||
enum OpenedModelHandle<E> {
|
||||
enum OpenEntityHandle<E> {
|
||||
Open(WeakEntity<E>),
|
||||
Loading(Shared<Task<Result<Entity<E>, Arc<anyhow::Error>>>>),
|
||||
}
|
||||
@@ -292,7 +292,7 @@ impl ChannelStore {
|
||||
|
||||
pub fn has_open_channel_buffer(&self, channel_id: ChannelId, _cx: &App) -> bool {
|
||||
if let Some(buffer) = self.opened_buffers.get(&channel_id) {
|
||||
if let OpenedModelHandle::Open(buffer) = buffer {
|
||||
if let OpenEntityHandle::Open(buffer) = buffer {
|
||||
return buffer.upgrade().is_some();
|
||||
}
|
||||
}
|
||||
@@ -453,7 +453,7 @@ impl ChannelStore {
|
||||
fn open_channel_resource<T, F, Fut>(
|
||||
&mut self,
|
||||
channel_id: ChannelId,
|
||||
get_map: fn(&mut Self) -> &mut HashMap<ChannelId, OpenedModelHandle<T>>,
|
||||
get_map: fn(&mut Self) -> &mut HashMap<ChannelId, OpenEntityHandle<T>>,
|
||||
load: F,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Task<Result<Entity<T>>>
|
||||
@@ -465,15 +465,15 @@ impl ChannelStore {
|
||||
let task = loop {
|
||||
match get_map(self).entry(channel_id) {
|
||||
hash_map::Entry::Occupied(e) => match e.get() {
|
||||
OpenedModelHandle::Open(model) => {
|
||||
if let Some(model) = model.upgrade() {
|
||||
break Task::ready(Ok(model)).shared();
|
||||
OpenEntityHandle::Open(entity) => {
|
||||
if let Some(entity) = entity.upgrade() {
|
||||
break Task::ready(Ok(entity)).shared();
|
||||
} else {
|
||||
get_map(self).remove(&channel_id);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
OpenedModelHandle::Loading(task) => {
|
||||
OpenEntityHandle::Loading(task) => {
|
||||
break task.clone();
|
||||
}
|
||||
},
|
||||
@@ -490,7 +490,7 @@ impl ChannelStore {
|
||||
})
|
||||
.shared();
|
||||
|
||||
e.insert(OpenedModelHandle::Loading(task.clone()));
|
||||
e.insert(OpenEntityHandle::Loading(task.clone()));
|
||||
cx.spawn({
|
||||
let task = task.clone();
|
||||
move |this, mut cx| async move {
|
||||
@@ -499,7 +499,7 @@ impl ChannelStore {
|
||||
Ok(model) => {
|
||||
get_map(this).insert(
|
||||
channel_id,
|
||||
OpenedModelHandle::Open(model.downgrade()),
|
||||
OpenEntityHandle::Open(model.downgrade()),
|
||||
);
|
||||
}
|
||||
Err(_) => {
|
||||
@@ -900,7 +900,7 @@ impl ChannelStore {
|
||||
self.disconnect_channel_buffers_task.take();
|
||||
|
||||
for chat in self.opened_chats.values() {
|
||||
if let OpenedModelHandle::Open(chat) = chat {
|
||||
if let OpenEntityHandle::Open(chat) = chat {
|
||||
if let Some(chat) = chat.upgrade() {
|
||||
chat.update(cx, |chat, cx| {
|
||||
chat.rejoin(cx);
|
||||
@@ -911,7 +911,7 @@ impl ChannelStore {
|
||||
|
||||
let mut buffer_versions = Vec::new();
|
||||
for buffer in self.opened_buffers.values() {
|
||||
if let OpenedModelHandle::Open(buffer) = buffer {
|
||||
if let OpenEntityHandle::Open(buffer) = buffer {
|
||||
if let Some(buffer) = buffer.upgrade() {
|
||||
let channel_buffer = buffer.read(cx);
|
||||
let buffer = channel_buffer.buffer().read(cx);
|
||||
@@ -937,7 +937,7 @@ impl ChannelStore {
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.opened_buffers.retain(|_, buffer| match buffer {
|
||||
OpenedModelHandle::Open(channel_buffer) => {
|
||||
OpenEntityHandle::Open(channel_buffer) => {
|
||||
let Some(channel_buffer) = channel_buffer.upgrade() else {
|
||||
return false;
|
||||
};
|
||||
@@ -998,7 +998,7 @@ impl ChannelStore {
|
||||
false
|
||||
})
|
||||
}
|
||||
OpenedModelHandle::Loading(_) => true,
|
||||
OpenEntityHandle::Loading(_) => true,
|
||||
});
|
||||
})
|
||||
.ok();
|
||||
@@ -1018,7 +1018,7 @@ impl ChannelStore {
|
||||
if let Some(this) = this.upgrade() {
|
||||
this.update(&mut cx, |this, cx| {
|
||||
for (_, buffer) in this.opened_buffers.drain() {
|
||||
if let OpenedModelHandle::Open(buffer) = buffer {
|
||||
if let OpenEntityHandle::Open(buffer) = buffer {
|
||||
if let Some(buffer) = buffer.upgrade() {
|
||||
buffer.update(cx, |buffer, cx| buffer.disconnect(cx));
|
||||
}
|
||||
@@ -1082,7 +1082,7 @@ impl ChannelStore {
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if let Some(OpenedModelHandle::Open(buffer)) =
|
||||
if let Some(OpenEntityHandle::Open(buffer)) =
|
||||
self.opened_buffers.remove(&channel_id)
|
||||
{
|
||||
if let Some(buffer) = buffer.upgrade() {
|
||||
@@ -1098,7 +1098,7 @@ impl ChannelStore {
|
||||
let channel_changed = index.insert(channel);
|
||||
|
||||
if channel_changed {
|
||||
if let Some(OpenedModelHandle::Open(buffer)) = self.opened_buffers.get(&id) {
|
||||
if let Some(OpenEntityHandle::Open(buffer)) = self.opened_buffers.get(&id) {
|
||||
if let Some(buffer) = buffer.upgrade() {
|
||||
buffer.update(cx, ChannelBuffer::channel_changed);
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
use std::process::Command;
|
||||
|
||||
fn main() {
|
||||
if std::env::var("ZED_UPDATE_EXPLANATION").is_ok() {
|
||||
println!(r#"cargo:rustc-cfg=feature="no-bundled-uninstall""#);
|
||||
@@ -8,4 +10,18 @@ fn main() {
|
||||
// Weakly link ScreenCaptureKit to ensure can be used on macOS 10.15+.
|
||||
println!("cargo:rustc-link-arg=-Wl,-weak_framework,ScreenCaptureKit");
|
||||
}
|
||||
|
||||
// Populate git sha environment variable if git is available
|
||||
println!("cargo:rerun-if-changed=../../.git/logs/HEAD");
|
||||
if let Some(output) = Command::new("git")
|
||||
.args(["rev-parse", "HEAD"])
|
||||
.output()
|
||||
.ok()
|
||||
.filter(|output| output.status.success())
|
||||
{
|
||||
let git_sha = String::from_utf8_lossy(&output.stdout);
|
||||
let git_sha = git_sha.trim();
|
||||
|
||||
println!("cargo:rustc-env=ZED_COMMIT_SHA={git_sha}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,7 +33,19 @@ trait InstalledApp {
|
||||
#[command(
|
||||
name = "zed",
|
||||
disable_version_flag = true,
|
||||
after_help = "To read from stdin, append '-' (e.g. 'ps axf | zed -')"
|
||||
before_help = "The Zed CLI binary.
|
||||
This CLI is a separate binary that invokes Zed.
|
||||
|
||||
Examples:
|
||||
`zed`
|
||||
Simply opens Zed
|
||||
`zed --foreground`
|
||||
Runs in foreground (shows all logs)
|
||||
`zed path-to-your-project`
|
||||
Open your project in Zed
|
||||
`zed -n path-to-file `
|
||||
Open file/folder in a new window",
|
||||
after_help = "To read from stdin, append '-', e.g. 'ps axf | zed -'"
|
||||
)]
|
||||
struct Args {
|
||||
/// Wait for all of the given paths to be opened/closed before exiting.
|
||||
@@ -45,10 +57,9 @@ struct Args {
|
||||
/// Create a new workspace
|
||||
#[arg(short, long, overrides_with = "add")]
|
||||
new: bool,
|
||||
/// A sequence of space-separated paths that you want to open.
|
||||
/// The paths to open in Zed (space-separated).
|
||||
///
|
||||
/// Use `path:line:row` syntax to open a file at a specific location.
|
||||
/// Non-existing paths and directories will ignore `:line:row` suffix.
|
||||
/// Use `path:line:column` syntax to open a file at the given line and column.
|
||||
paths_with_position: Vec<String>,
|
||||
/// Print Zed's version and the app path.
|
||||
#[arg(short, long)]
|
||||
@@ -328,13 +339,17 @@ mod linux {
|
||||
impl InstalledApp for App {
|
||||
fn zed_version_string(&self) -> String {
|
||||
format!(
|
||||
"Zed {}{} – {}",
|
||||
"Zed {}{}{} – {}",
|
||||
if *RELEASE_CHANNEL == "stable" {
|
||||
"".to_string()
|
||||
} else {
|
||||
format!(" {} ", *RELEASE_CHANNEL)
|
||||
format!("{} ", *RELEASE_CHANNEL)
|
||||
},
|
||||
option_env!("RELEASE_VERSION").unwrap_or_default(),
|
||||
match option_env!("ZED_COMMIT_SHA") {
|
||||
Some(commit_sha) => format!(" {commit_sha} "),
|
||||
None => "".to_string(),
|
||||
},
|
||||
self.0.display(),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -146,6 +146,8 @@ pub fn init_settings(cx: &mut App) {
|
||||
}
|
||||
|
||||
pub fn init(client: &Arc<Client>, cx: &mut App) {
|
||||
let _ = rustls::crypto::aws_lc_rs::default_provider().install_default();
|
||||
|
||||
let client = Arc::downgrade(client);
|
||||
cx.on_action({
|
||||
let client = client.clone();
|
||||
@@ -379,7 +381,7 @@ pub struct PendingEntitySubscription<T: 'static> {
|
||||
}
|
||||
|
||||
impl<T: 'static> PendingEntitySubscription<T> {
|
||||
pub fn set_model(mut self, model: &Entity<T>, cx: &AsyncApp) -> Subscription {
|
||||
pub fn set_entity(mut self, entity: &Entity<T>, cx: &AsyncApp) -> Subscription {
|
||||
self.consumed = true;
|
||||
let mut handlers = self.client.handler_set.lock();
|
||||
let id = (TypeId::of::<T>(), self.remote_id);
|
||||
@@ -392,7 +394,7 @@ impl<T: 'static> PendingEntitySubscription<T> {
|
||||
handlers.entities_by_type_and_remote_id.insert(
|
||||
id,
|
||||
EntityMessageSubscriber::Entity {
|
||||
handle: model.downgrade().into(),
|
||||
handle: entity.downgrade().into(),
|
||||
},
|
||||
);
|
||||
drop(handlers);
|
||||
@@ -686,8 +688,8 @@ impl Client {
|
||||
H: 'static + Sync + Fn(Entity<E>, TypedEnvelope<M>, AsyncApp) -> F + Send + Sync,
|
||||
F: 'static + Future<Output = Result<()>>,
|
||||
{
|
||||
self.add_message_handler_impl(entity, move |model, message, _, cx| {
|
||||
handler(model, message, cx)
|
||||
self.add_message_handler_impl(entity, move |entity, message, _, cx| {
|
||||
handler(entity, message, cx)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -709,7 +711,7 @@ impl Client {
|
||||
let message_type_id = TypeId::of::<M>();
|
||||
let mut state = self.handler_set.lock();
|
||||
state
|
||||
.models_by_message_type
|
||||
.entities_by_message_type
|
||||
.insert(message_type_id, entity.into());
|
||||
|
||||
let prev_handler = state.message_handlers.insert(
|
||||
@@ -738,7 +740,7 @@ impl Client {
|
||||
|
||||
pub fn add_request_handler<M, E, H, F>(
|
||||
self: &Arc<Self>,
|
||||
model: WeakEntity<E>,
|
||||
entity: WeakEntity<E>,
|
||||
handler: H,
|
||||
) -> Subscription
|
||||
where
|
||||
@@ -747,7 +749,7 @@ impl Client {
|
||||
H: 'static + Sync + Fn(Entity<E>, TypedEnvelope<M>, AsyncApp) -> F + Send + Sync,
|
||||
F: 'static + Future<Output = Result<M::Response>>,
|
||||
{
|
||||
self.add_message_handler_impl(model, move |handle, envelope, this, cx| {
|
||||
self.add_message_handler_impl(entity, move |handle, envelope, this, cx| {
|
||||
Self::respond_to_request(envelope.receipt(), handler(handle, envelope, cx), this)
|
||||
})
|
||||
}
|
||||
@@ -1131,15 +1133,8 @@ impl Client {
|
||||
for error in root_certs.errors {
|
||||
log::warn!("error loading native certs: {:?}", error);
|
||||
}
|
||||
root_store.add_parsable_certificates(
|
||||
&root_certs
|
||||
.certs
|
||||
.into_iter()
|
||||
.map(|cert| cert.as_ref().to_owned())
|
||||
.collect::<Vec<_>>(),
|
||||
);
|
||||
root_store.add_parsable_certificates(root_certs.certs);
|
||||
rustls::ClientConfig::builder()
|
||||
.with_safe_defaults()
|
||||
.with_root_certificates(root_store)
|
||||
.with_no_client_auth()
|
||||
};
|
||||
@@ -1948,9 +1943,9 @@ mod tests {
|
||||
|
||||
let (done_tx1, done_rx1) = smol::channel::unbounded();
|
||||
let (done_tx2, done_rx2) = smol::channel::unbounded();
|
||||
AnyProtoClient::from(client.clone()).add_model_message_handler(
|
||||
move |model: Entity<TestModel>, _: TypedEnvelope<proto::JoinProject>, mut cx| {
|
||||
match model.update(&mut cx, |model, _| model.id).unwrap() {
|
||||
AnyProtoClient::from(client.clone()).add_entity_message_handler(
|
||||
move |entity: Entity<TestEntity>, _: TypedEnvelope<proto::JoinProject>, mut cx| {
|
||||
match entity.update(&mut cx, |entity, _| entity.id).unwrap() {
|
||||
1 => done_tx1.try_send(()).unwrap(),
|
||||
2 => done_tx2.try_send(()).unwrap(),
|
||||
_ => unreachable!(),
|
||||
@@ -1958,15 +1953,15 @@ mod tests {
|
||||
async { Ok(()) }
|
||||
},
|
||||
);
|
||||
let model1 = cx.new(|_| TestModel {
|
||||
let entity1 = cx.new(|_| TestEntity {
|
||||
id: 1,
|
||||
subscription: None,
|
||||
});
|
||||
let model2 = cx.new(|_| TestModel {
|
||||
let entity2 = cx.new(|_| TestEntity {
|
||||
id: 2,
|
||||
subscription: None,
|
||||
});
|
||||
let model3 = cx.new(|_| TestModel {
|
||||
let entity3 = cx.new(|_| TestEntity {
|
||||
id: 3,
|
||||
subscription: None,
|
||||
});
|
||||
@@ -1974,17 +1969,17 @@ mod tests {
|
||||
let _subscription1 = client
|
||||
.subscribe_to_entity(1)
|
||||
.unwrap()
|
||||
.set_model(&model1, &mut cx.to_async());
|
||||
.set_entity(&entity1, &mut cx.to_async());
|
||||
let _subscription2 = client
|
||||
.subscribe_to_entity(2)
|
||||
.unwrap()
|
||||
.set_model(&model2, &mut cx.to_async());
|
||||
.set_entity(&entity2, &mut cx.to_async());
|
||||
// Ensure dropping a subscription for the same entity type still allows receiving of
|
||||
// messages for other entity IDs of the same type.
|
||||
let subscription3 = client
|
||||
.subscribe_to_entity(3)
|
||||
.unwrap()
|
||||
.set_model(&model3, &mut cx.to_async());
|
||||
.set_entity(&entity3, &mut cx.to_async());
|
||||
drop(subscription3);
|
||||
|
||||
server.send(proto::JoinProject { project_id: 1 });
|
||||
@@ -2006,11 +2001,11 @@ mod tests {
|
||||
});
|
||||
let server = FakeServer::for_client(user_id, &client, cx).await;
|
||||
|
||||
let model = cx.new(|_| TestModel::default());
|
||||
let entity = cx.new(|_| TestEntity::default());
|
||||
let (done_tx1, _done_rx1) = smol::channel::unbounded();
|
||||
let (done_tx2, done_rx2) = smol::channel::unbounded();
|
||||
let subscription1 = client.add_message_handler(
|
||||
model.downgrade(),
|
||||
entity.downgrade(),
|
||||
move |_, _: TypedEnvelope<proto::Ping>, _| {
|
||||
done_tx1.try_send(()).unwrap();
|
||||
async { Ok(()) }
|
||||
@@ -2018,7 +2013,7 @@ mod tests {
|
||||
);
|
||||
drop(subscription1);
|
||||
let _subscription2 = client.add_message_handler(
|
||||
model.downgrade(),
|
||||
entity.downgrade(),
|
||||
move |_, _: TypedEnvelope<proto::Ping>, _| {
|
||||
done_tx2.try_send(()).unwrap();
|
||||
async { Ok(()) }
|
||||
@@ -2041,27 +2036,27 @@ mod tests {
|
||||
});
|
||||
let server = FakeServer::for_client(user_id, &client, cx).await;
|
||||
|
||||
let model = cx.new(|_| TestModel::default());
|
||||
let entity = cx.new(|_| TestEntity::default());
|
||||
let (done_tx, done_rx) = smol::channel::unbounded();
|
||||
let subscription = client.add_message_handler(
|
||||
model.clone().downgrade(),
|
||||
move |model: Entity<TestModel>, _: TypedEnvelope<proto::Ping>, mut cx| {
|
||||
model
|
||||
.update(&mut cx, |model, _| model.subscription.take())
|
||||
entity.clone().downgrade(),
|
||||
move |entity: Entity<TestEntity>, _: TypedEnvelope<proto::Ping>, mut cx| {
|
||||
entity
|
||||
.update(&mut cx, |entity, _| entity.subscription.take())
|
||||
.unwrap();
|
||||
done_tx.try_send(()).unwrap();
|
||||
async { Ok(()) }
|
||||
},
|
||||
);
|
||||
model.update(cx, |model, _| {
|
||||
model.subscription = Some(subscription);
|
||||
entity.update(cx, |entity, _| {
|
||||
entity.subscription = Some(subscription);
|
||||
});
|
||||
server.send(proto::Ping {});
|
||||
done_rx.recv().await.unwrap();
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct TestModel {
|
||||
struct TestEntity {
|
||||
id: usize,
|
||||
subscription: Option<Subscription>,
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ mod event_coalescer;
|
||||
use crate::TelemetrySettings;
|
||||
use anyhow::Result;
|
||||
use clock::SystemClock;
|
||||
use collections::{HashMap, HashSet};
|
||||
use futures::channel::mpsc;
|
||||
use futures::{Future, StreamExt};
|
||||
use gpui::{App, BackgroundExecutor, Task};
|
||||
@@ -12,14 +11,13 @@ use parking_lot::Mutex;
|
||||
use release_channel::ReleaseChannel;
|
||||
use settings::{Settings, SettingsStore};
|
||||
use sha2::{Digest, Sha256};
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::fs::File;
|
||||
use std::io::Write;
|
||||
use std::sync::LazyLock;
|
||||
use std::time::Instant;
|
||||
use std::{env, mem, path::PathBuf, sync::Arc, time::Duration};
|
||||
use telemetry_events::{
|
||||
AppEvent, AssistantEvent, AssistantPhase, EditEvent, Event, EventRequestBody, EventWrapper,
|
||||
};
|
||||
use telemetry_events::{AssistantEvent, AssistantPhase, Event, EventRequestBody, EventWrapper};
|
||||
use util::{ResultExt, TryFutureExt};
|
||||
use worktree::{UpdatedEntriesSet, WorktreeId};
|
||||
|
||||
@@ -285,7 +283,7 @@ impl Telemetry {
|
||||
// TestAppContext ends up calling this function on shutdown and it panics when trying to find the TelemetrySettings
|
||||
#[cfg(not(any(test, feature = "test-support")))]
|
||||
fn shutdown_telemetry(self: &Arc<Self>) -> impl Future<Output = ()> {
|
||||
self.report_app_event("close".to_string());
|
||||
telemetry::event!("App Closed");
|
||||
// TODO: close final edit period and make sure it's sent
|
||||
Task::ready(())
|
||||
}
|
||||
@@ -355,30 +353,23 @@ impl Telemetry {
|
||||
);
|
||||
}
|
||||
|
||||
pub fn report_app_event(self: &Arc<Self>, operation: String) -> Event {
|
||||
let event = Event::App(AppEvent { operation });
|
||||
|
||||
self.report_event(event.clone());
|
||||
|
||||
event
|
||||
}
|
||||
|
||||
pub fn log_edit_event(self: &Arc<Self>, environment: &'static str, is_via_ssh: bool) {
|
||||
let mut state = self.state.lock();
|
||||
let period_data = state.event_coalescer.log_event(environment);
|
||||
drop(state);
|
||||
|
||||
if let Some((start, end, environment)) = period_data {
|
||||
let event = Event::Edit(EditEvent {
|
||||
duration: end
|
||||
.saturating_duration_since(start)
|
||||
.min(Duration::from_secs(60 * 60 * 24))
|
||||
.as_millis() as i64,
|
||||
environment: environment.to_string(),
|
||||
is_via_ssh,
|
||||
});
|
||||
let duration = end
|
||||
.saturating_duration_since(start)
|
||||
.min(Duration::from_secs(60 * 60 * 24))
|
||||
.as_millis() as i64;
|
||||
|
||||
self.report_event(event);
|
||||
telemetry::event!(
|
||||
"Editor Edited",
|
||||
duration = duration,
|
||||
environment = environment.to_string(),
|
||||
is_via_ssh = is_via_ssh
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -422,9 +413,8 @@ impl Telemetry {
|
||||
.collect()
|
||||
};
|
||||
|
||||
// Done on purpose to avoid calling `self.state.lock()` multiple times
|
||||
for project_type_name in project_type_names {
|
||||
self.report_app_event(format!("open {} project", project_type_name));
|
||||
telemetry::event!("Project Opened", project_type = project_type_name);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -590,6 +580,7 @@ mod tests {
|
||||
use clock::FakeSystemClock;
|
||||
use gpui::TestAppContext;
|
||||
use http_client::FakeHttpClient;
|
||||
use telemetry_events::FlexibleEvent;
|
||||
|
||||
#[gpui::test]
|
||||
fn test_telemetry_flush_on_max_queue_size(cx: &mut TestAppContext) {
|
||||
@@ -609,15 +600,17 @@ mod tests {
|
||||
assert!(is_empty_state(&telemetry));
|
||||
|
||||
let first_date_time = clock.utc_now();
|
||||
let operation = "test".to_string();
|
||||
let event_properties = HashMap::from_iter([(
|
||||
"test_key".to_string(),
|
||||
serde_json::Value::String("test_value".to_string()),
|
||||
)]);
|
||||
|
||||
let event = telemetry.report_app_event(operation.clone());
|
||||
assert_eq!(
|
||||
event,
|
||||
Event::App(AppEvent {
|
||||
operation: operation.clone(),
|
||||
})
|
||||
);
|
||||
let event = FlexibleEvent {
|
||||
event_type: "test".to_string(),
|
||||
event_properties,
|
||||
};
|
||||
|
||||
telemetry.report_event(Event::Flexible(event.clone()));
|
||||
assert_eq!(telemetry.state.lock().events_queue.len(), 1);
|
||||
assert!(telemetry.state.lock().flush_events_task.is_some());
|
||||
assert_eq!(
|
||||
@@ -627,13 +620,7 @@ mod tests {
|
||||
|
||||
clock.advance(Duration::from_millis(100));
|
||||
|
||||
let event = telemetry.report_app_event(operation.clone());
|
||||
assert_eq!(
|
||||
event,
|
||||
Event::App(AppEvent {
|
||||
operation: operation.clone(),
|
||||
})
|
||||
);
|
||||
telemetry.report_event(Event::Flexible(event.clone()));
|
||||
assert_eq!(telemetry.state.lock().events_queue.len(), 2);
|
||||
assert!(telemetry.state.lock().flush_events_task.is_some());
|
||||
assert_eq!(
|
||||
@@ -643,13 +630,7 @@ mod tests {
|
||||
|
||||
clock.advance(Duration::from_millis(100));
|
||||
|
||||
let event = telemetry.report_app_event(operation.clone());
|
||||
assert_eq!(
|
||||
event,
|
||||
Event::App(AppEvent {
|
||||
operation: operation.clone(),
|
||||
})
|
||||
);
|
||||
telemetry.report_event(Event::Flexible(event.clone()));
|
||||
assert_eq!(telemetry.state.lock().events_queue.len(), 3);
|
||||
assert!(telemetry.state.lock().flush_events_task.is_some());
|
||||
assert_eq!(
|
||||
@@ -660,14 +641,7 @@ mod tests {
|
||||
clock.advance(Duration::from_millis(100));
|
||||
|
||||
// Adding a 4th event should cause a flush
|
||||
let event = telemetry.report_app_event(operation.clone());
|
||||
assert_eq!(
|
||||
event,
|
||||
Event::App(AppEvent {
|
||||
operation: operation.clone(),
|
||||
})
|
||||
);
|
||||
|
||||
telemetry.report_event(Event::Flexible(event));
|
||||
assert!(is_empty_state(&telemetry));
|
||||
});
|
||||
}
|
||||
@@ -690,17 +664,19 @@ mod tests {
|
||||
telemetry.start(system_id, installation_id, session_id, cx);
|
||||
|
||||
assert!(is_empty_state(&telemetry));
|
||||
|
||||
let first_date_time = clock.utc_now();
|
||||
let operation = "test".to_string();
|
||||
|
||||
let event = telemetry.report_app_event(operation.clone());
|
||||
assert_eq!(
|
||||
event,
|
||||
Event::App(AppEvent {
|
||||
operation: operation.clone(),
|
||||
})
|
||||
);
|
||||
let event_properties = HashMap::from_iter([(
|
||||
"test_key".to_string(),
|
||||
serde_json::Value::String("test_value".to_string()),
|
||||
)]);
|
||||
|
||||
let event = FlexibleEvent {
|
||||
event_type: "test".to_string(),
|
||||
event_properties,
|
||||
};
|
||||
|
||||
telemetry.report_event(Event::Flexible(event));
|
||||
assert_eq!(telemetry.state.lock().events_queue.len(), 1);
|
||||
assert!(telemetry.state.lock().flush_events_task.is_some());
|
||||
assert_eq!(
|
||||
|
||||
@@ -121,9 +121,7 @@ pub enum Event {
|
||||
},
|
||||
ShowContacts,
|
||||
ParticipantIndicesChanged,
|
||||
TermsStatusUpdated {
|
||||
accepted: bool,
|
||||
},
|
||||
PrivateUserInfoUpdated,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
@@ -203,9 +201,8 @@ impl UserStore {
|
||||
|
||||
cx.update(|cx| {
|
||||
if let Some(info) = info {
|
||||
let disable_staff = std::env::var("ZED_DISABLE_STAFF")
|
||||
.map_or(false, |v| !v.is_empty() && v != "0");
|
||||
let staff = info.staff && !disable_staff;
|
||||
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()),
|
||||
@@ -227,9 +224,7 @@ impl UserStore {
|
||||
};
|
||||
|
||||
this.set_current_user_accepted_tos_at(accepted_tos_at);
|
||||
cx.emit(Event::TermsStatusUpdated {
|
||||
accepted: accepted_tos_at.is_some(),
|
||||
});
|
||||
cx.emit(Event::PrivateUserInfoUpdated);
|
||||
})
|
||||
} else {
|
||||
anyhow::Ok(())
|
||||
@@ -244,6 +239,8 @@ impl UserStore {
|
||||
Status::SignedOut => {
|
||||
current_user_tx.send(None).await.ok();
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.accepted_tos_at = None;
|
||||
cx.emit(Event::PrivateUserInfoUpdated);
|
||||
cx.notify();
|
||||
this.clear_contacts()
|
||||
})?
|
||||
@@ -714,7 +711,7 @@ impl UserStore {
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.set_current_user_accepted_tos_at(Some(response.accepted_tos_at));
|
||||
cx.emit(Event::TermsStatusUpdated { accepted: true });
|
||||
cx.emit(Event::PrivateUserInfoUpdated);
|
||||
})
|
||||
} else {
|
||||
Err(anyhow!("client not found"))
|
||||
|
||||
@@ -33,8 +33,8 @@ clock.workspace = true
|
||||
collections.workspace = true
|
||||
dashmap.workspace = true
|
||||
derive_more.workspace = true
|
||||
diff.workspace = true
|
||||
envy = "0.4.2"
|
||||
fireworks.workspace = true
|
||||
futures.workspace = true
|
||||
google_ai.workspace = true
|
||||
hex.workspace = true
|
||||
|
||||
@@ -100,6 +100,7 @@ CREATE TABLE "worktree_repositories" (
|
||||
"branch" VARCHAR,
|
||||
"scan_id" INTEGER NOT NULL,
|
||||
"is_deleted" BOOL NOT NULL,
|
||||
"current_merge_conflicts" VARCHAR,
|
||||
PRIMARY KEY(project_id, worktree_id, work_directory_id),
|
||||
FOREIGN KEY(project_id, worktree_id) REFERENCES worktrees (project_id, id) ON DELETE CASCADE,
|
||||
FOREIGN KEY(project_id, worktree_id, work_directory_id) REFERENCES worktree_entries (project_id, worktree_id, id) ON DELETE CASCADE
|
||||
@@ -401,6 +402,15 @@ CREATE TABLE extension_versions (
|
||||
schema_version INTEGER NOT NULL DEFAULT 0,
|
||||
wasm_api_version TEXT,
|
||||
download_count INTEGER NOT NULL DEFAULT 0,
|
||||
provides_themes BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
provides_icon_themes BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
provides_languages BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
provides_grammars BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
provides_language_servers BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
provides_context_servers BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
provides_slash_commands BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
provides_indexed_docs_providers BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
provides_snippets BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
PRIMARY KEY (extension_id, version)
|
||||
);
|
||||
|
||||
@@ -430,6 +440,7 @@ CREATE TABLE IF NOT EXISTS billing_customers (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
user_id INTEGER NOT NULL REFERENCES users(id),
|
||||
has_overdue_invoices BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
stripe_customer_id TEXT NOT NULL
|
||||
);
|
||||
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
alter table billing_customers
|
||||
add column has_overdue_invoices bool not null default false;
|
||||
@@ -0,0 +1,10 @@
|
||||
alter table extension_versions
|
||||
add column provides_themes bool not null default false,
|
||||
add column provides_icon_themes bool not null default false,
|
||||
add column provides_languages bool not null default false,
|
||||
add column provides_grammars bool not null default false,
|
||||
add column provides_language_servers bool not null default false,
|
||||
add column provides_context_servers bool not null default false,
|
||||
add column provides_slash_commands bool not null default false,
|
||||
add column provides_indexed_docs_providers bool not null default false,
|
||||
add column provides_snippets bool not null default false;
|
||||
@@ -0,0 +1,2 @@
|
||||
ALTER TABLE worktree_repositories
|
||||
ADD COLUMN current_merge_conflicts VARCHAR NULL;
|
||||
@@ -5,6 +5,7 @@ pub mod extensions;
|
||||
pub mod ips_file;
|
||||
pub mod slack;
|
||||
|
||||
use crate::api::events::SnowflakeRow;
|
||||
use crate::{
|
||||
auth,
|
||||
db::{User, UserId},
|
||||
@@ -99,6 +100,7 @@ pub fn routes(rpc_server: Arc<rpc::Server>) -> Router<(), Body> {
|
||||
.route("/user", get(get_authenticated_user))
|
||||
.route("/users/:id/access_tokens", post(create_access_token))
|
||||
.route("/rpc_server_snapshot", get(get_rpc_server_snapshot))
|
||||
.route("/snowflake/events", post(write_snowflake_event))
|
||||
.merge(billing::router())
|
||||
.merge(contributors::router())
|
||||
.layer(
|
||||
@@ -245,3 +247,19 @@ async fn create_access_token(
|
||||
encrypted_access_token,
|
||||
}))
|
||||
}
|
||||
|
||||
/// An endpoint that writes a Snowflake event to our event stream.
|
||||
///
|
||||
/// This endpoint is exposed such that other internal services can write
|
||||
/// telemetry events without needing to talk to AWS Kinesis directly.
|
||||
async fn write_snowflake_event(
|
||||
Extension(app): Extension<Arc<AppState>>,
|
||||
Json(event): Json<SnowflakeRow>,
|
||||
) -> Result<()> {
|
||||
let kinesis_client = app.kinesis_client.clone();
|
||||
let kinesis_stream = app.config.kinesis_stream.clone();
|
||||
|
||||
event.write(&kinesis_client, &kinesis_stream).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -249,29 +249,31 @@ async fn create_billing_subscription(
|
||||
));
|
||||
}
|
||||
|
||||
if app.db.has_overdue_billing_subscriptions(user.id).await? {
|
||||
return Err(Error::http(
|
||||
StatusCode::PAYMENT_REQUIRED,
|
||||
"user has overdue billing subscriptions".into(),
|
||||
));
|
||||
let existing_billing_customer = app.db.get_billing_customer_by_user_id(user.id).await?;
|
||||
if let Some(existing_billing_customer) = &existing_billing_customer {
|
||||
if existing_billing_customer.has_overdue_invoices {
|
||||
return Err(Error::http(
|
||||
StatusCode::PAYMENT_REQUIRED,
|
||||
"user has overdue invoices".into(),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
let customer_id =
|
||||
if let Some(existing_customer) = app.db.get_billing_customer_by_user_id(user.id).await? {
|
||||
CustomerId::from_str(&existing_customer.stripe_customer_id)
|
||||
.context("failed to parse customer ID")?
|
||||
} else {
|
||||
let customer = Customer::create(
|
||||
&stripe_client,
|
||||
CreateCustomer {
|
||||
email: user.email_address.as_deref(),
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
let customer_id = if let Some(existing_customer) = existing_billing_customer {
|
||||
CustomerId::from_str(&existing_customer.stripe_customer_id)
|
||||
.context("failed to parse customer ID")?
|
||||
} else {
|
||||
let customer = Customer::create(
|
||||
&stripe_client,
|
||||
CreateCustomer {
|
||||
email: user.email_address.as_deref(),
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
|
||||
customer.id
|
||||
};
|
||||
customer.id
|
||||
};
|
||||
|
||||
let default_model = llm_db.model(rpc::LanguageModelProvider::Anthropic, "claude-3-5-sonnet")?;
|
||||
let stripe_model = stripe_billing.register_model(default_model).await?;
|
||||
@@ -666,6 +668,27 @@ async fn handle_customer_subscription_event(
|
||||
.await?
|
||||
.ok_or_else(|| anyhow!("billing customer not found"))?;
|
||||
|
||||
let was_canceled_due_to_payment_failure = subscription.status == SubscriptionStatus::Canceled
|
||||
&& subscription
|
||||
.cancellation_details
|
||||
.as_ref()
|
||||
.and_then(|details| details.reason)
|
||||
.map_or(false, |reason| {
|
||||
reason == CancellationDetailsReason::PaymentFailed
|
||||
});
|
||||
|
||||
if was_canceled_due_to_payment_failure {
|
||||
app.db
|
||||
.update_billing_customer(
|
||||
billing_customer.id,
|
||||
&UpdateBillingCustomerParams {
|
||||
has_overdue_invoices: ActiveValue::set(true),
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
if let Some(existing_subscription) = app
|
||||
.db
|
||||
.get_billing_subscription_by_stripe_subscription_id(&subscription.id)
|
||||
|
||||
@@ -495,6 +495,10 @@ fn for_snowflake(
|
||||
body.events.into_iter().flat_map(move |event| {
|
||||
let timestamp =
|
||||
first_event_at + Duration::milliseconds(event.milliseconds_since_first_event);
|
||||
// We will need to double check, but I believe all of the events that
|
||||
// are being transformed here are now migrated over to use the
|
||||
// telemetry::event! macro, as of this commit so this code can go away
|
||||
// when we feel enough users have upgraded past this point.
|
||||
let (event_type, mut event_properties) = match &event.event {
|
||||
Event::Editor(e) => (
|
||||
match e.operation.as_str() {
|
||||
@@ -506,7 +510,7 @@ fn for_snowflake(
|
||||
),
|
||||
Event::InlineCompletion(e) => (
|
||||
format!(
|
||||
"Inline Completion {}",
|
||||
"Edit Prediction {}",
|
||||
if e.suggestion_accepted {
|
||||
"Accepted"
|
||||
} else {
|
||||
@@ -516,7 +520,7 @@ fn for_snowflake(
|
||||
serde_json::to_value(e).unwrap(),
|
||||
),
|
||||
Event::InlineCompletionRating(e) => (
|
||||
"Inline Completion Rated".to_string(),
|
||||
"Edit Prediction Rated".to_string(),
|
||||
serde_json::to_value(e).unwrap(),
|
||||
),
|
||||
Event::Call(e) => {
|
||||
|
||||
@@ -9,10 +9,11 @@ use axum::{
|
||||
routing::get,
|
||||
Extension, Json, Router,
|
||||
};
|
||||
use collections::HashMap;
|
||||
use rpc::{ExtensionApiManifest, GetExtensionsResponse};
|
||||
use collections::{BTreeSet, HashMap};
|
||||
use rpc::{ExtensionApiManifest, ExtensionProvides, GetExtensionsResponse};
|
||||
use semantic_version::SemanticVersion;
|
||||
use serde::Deserialize;
|
||||
use std::str::FromStr;
|
||||
use std::{sync::Arc, time::Duration};
|
||||
use time::PrimitiveDateTime;
|
||||
use util::{maybe, ResultExt};
|
||||
@@ -35,6 +36,14 @@ pub fn router() -> Router {
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct GetExtensionsParams {
|
||||
filter: Option<String>,
|
||||
/// A comma-delimited list of features that the extension must provide.
|
||||
///
|
||||
/// For example:
|
||||
/// - `themes`
|
||||
/// - `themes,icon-themes`
|
||||
/// - `languages,language-servers`
|
||||
#[serde(default)]
|
||||
provides: Option<String>,
|
||||
#[serde(default)]
|
||||
max_schema_version: i32,
|
||||
}
|
||||
@@ -43,9 +52,22 @@ async fn get_extensions(
|
||||
Extension(app): Extension<Arc<AppState>>,
|
||||
Query(params): Query<GetExtensionsParams>,
|
||||
) -> Result<Json<GetExtensionsResponse>> {
|
||||
let provides_filter = params.provides.map(|provides| {
|
||||
provides
|
||||
.split(',')
|
||||
.map(|value| value.trim())
|
||||
.filter_map(|value| ExtensionProvides::from_str(value).ok())
|
||||
.collect::<BTreeSet<_>>()
|
||||
});
|
||||
|
||||
let mut extensions = app
|
||||
.db
|
||||
.get_extensions(params.filter.as_deref(), params.max_schema_version, 500)
|
||||
.get_extensions(
|
||||
params.filter.as_deref(),
|
||||
provides_filter.as_ref(),
|
||||
params.max_schema_version,
|
||||
500,
|
||||
)
|
||||
.await?;
|
||||
|
||||
if let Some(filter) = params.filter.as_deref() {
|
||||
@@ -391,6 +413,7 @@ async fn fetch_extension_manifest(
|
||||
repository: manifest.repository,
|
||||
schema_version: manifest.schema_version.unwrap_or(0),
|
||||
wasm_api_version: manifest.wasm_api_version,
|
||||
provides: manifest.provides,
|
||||
published_at,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -6,10 +6,11 @@ pub mod tests;
|
||||
|
||||
use crate::{executor::Executor, Error, Result};
|
||||
use anyhow::anyhow;
|
||||
use collections::{BTreeMap, HashMap, HashSet};
|
||||
use collections::{BTreeMap, BTreeSet, HashMap, HashSet};
|
||||
use dashmap::DashMap;
|
||||
use futures::StreamExt;
|
||||
use rand::{prelude::StdRng, Rng, SeedableRng};
|
||||
use rpc::ExtensionProvides;
|
||||
use rpc::{
|
||||
proto::{self},
|
||||
ConnectionId, ExtensionMetadata,
|
||||
@@ -781,6 +782,7 @@ pub struct NewExtensionVersion {
|
||||
pub repository: String,
|
||||
pub schema_version: i32,
|
||||
pub wasm_api_version: Option<String>,
|
||||
pub provides: BTreeSet<ExtensionProvides>,
|
||||
pub published_at: PrimitiveDateTime,
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ pub struct CreateBillingCustomerParams {
|
||||
pub struct UpdateBillingCustomerParams {
|
||||
pub user_id: ActiveValue<UserId>,
|
||||
pub stripe_customer_id: ActiveValue<String>,
|
||||
pub has_overdue_invoices: ActiveValue<bool>,
|
||||
}
|
||||
|
||||
impl Database {
|
||||
@@ -43,6 +44,7 @@ impl Database {
|
||||
id: ActiveValue::set(id),
|
||||
user_id: params.user_id.clone(),
|
||||
stripe_customer_id: params.stripe_customer_id.clone(),
|
||||
has_overdue_invoices: params.has_overdue_invoices.clone(),
|
||||
..Default::default()
|
||||
})
|
||||
.exec(&*tx)
|
||||
|
||||
@@ -170,40 +170,4 @@ impl Database {
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
/// Returns whether the user has any overdue billing subscriptions.
|
||||
pub async fn has_overdue_billing_subscriptions(&self, user_id: UserId) -> Result<bool> {
|
||||
Ok(self.count_overdue_billing_subscriptions(user_id).await? > 0)
|
||||
}
|
||||
|
||||
/// Returns the count of the overdue billing subscriptions for the user with the specified ID.
|
||||
///
|
||||
/// This includes subscriptions:
|
||||
/// - Whose status is `past_due`
|
||||
/// - Whose status is `canceled` and the cancellation reason is `payment_failed`
|
||||
pub async fn count_overdue_billing_subscriptions(&self, user_id: UserId) -> Result<usize> {
|
||||
self.transaction(|tx| async move {
|
||||
let past_due = billing_subscription::Column::StripeSubscriptionStatus
|
||||
.eq(StripeSubscriptionStatus::PastDue);
|
||||
let payment_failed = billing_subscription::Column::StripeSubscriptionStatus
|
||||
.eq(StripeSubscriptionStatus::Canceled)
|
||||
.and(
|
||||
billing_subscription::Column::StripeCancellationReason
|
||||
.eq(StripeCancellationReason::PaymentFailed),
|
||||
);
|
||||
|
||||
let count = billing_subscription::Entity::find()
|
||||
.inner_join(billing_customer::Entity)
|
||||
.filter(
|
||||
billing_customer::Column::UserId
|
||||
.eq(user_id)
|
||||
.and(past_due.or(payment_failed)),
|
||||
)
|
||||
.count(&*tx)
|
||||
.await?;
|
||||
|
||||
Ok(count as usize)
|
||||
})
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ impl Database {
|
||||
pub async fn get_extensions(
|
||||
&self,
|
||||
filter: Option<&str>,
|
||||
provides_filter: Option<&BTreeSet<ExtensionProvides>>,
|
||||
max_schema_version: i32,
|
||||
limit: usize,
|
||||
) -> Result<Vec<ExtensionMetadata>> {
|
||||
@@ -26,6 +27,10 @@ impl Database {
|
||||
condition = condition.add(Expr::cust_with_expr("name ILIKE $1", fuzzy_name_filter));
|
||||
}
|
||||
|
||||
if let Some(provides_filter) = provides_filter {
|
||||
condition = apply_provides_filter(condition, provides_filter);
|
||||
}
|
||||
|
||||
self.get_extensions_where(condition, Some(limit as u64), &tx)
|
||||
.await
|
||||
})
|
||||
@@ -282,6 +287,39 @@ impl Database {
|
||||
description: ActiveValue::Set(version.description.clone()),
|
||||
schema_version: ActiveValue::Set(version.schema_version),
|
||||
wasm_api_version: ActiveValue::Set(version.wasm_api_version.clone()),
|
||||
provides_themes: ActiveValue::Set(
|
||||
version.provides.contains(&ExtensionProvides::Themes),
|
||||
),
|
||||
provides_icon_themes: ActiveValue::Set(
|
||||
version.provides.contains(&ExtensionProvides::IconThemes),
|
||||
),
|
||||
provides_languages: ActiveValue::Set(
|
||||
version.provides.contains(&ExtensionProvides::Languages),
|
||||
),
|
||||
provides_grammars: ActiveValue::Set(
|
||||
version.provides.contains(&ExtensionProvides::Grammars),
|
||||
),
|
||||
provides_language_servers: ActiveValue::Set(
|
||||
version
|
||||
.provides
|
||||
.contains(&ExtensionProvides::LanguageServers),
|
||||
),
|
||||
provides_context_servers: ActiveValue::Set(
|
||||
version
|
||||
.provides
|
||||
.contains(&ExtensionProvides::ContextServers),
|
||||
),
|
||||
provides_slash_commands: ActiveValue::Set(
|
||||
version.provides.contains(&ExtensionProvides::SlashCommands),
|
||||
),
|
||||
provides_indexed_docs_providers: ActiveValue::Set(
|
||||
version
|
||||
.provides
|
||||
.contains(&ExtensionProvides::IndexedDocsProviders),
|
||||
),
|
||||
provides_snippets: ActiveValue::Set(
|
||||
version.provides.contains(&ExtensionProvides::Snippets),
|
||||
),
|
||||
download_count: ActiveValue::NotSet,
|
||||
}
|
||||
}))
|
||||
@@ -352,10 +390,55 @@ impl Database {
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_provides_filter(
|
||||
mut condition: Condition,
|
||||
provides_filter: &BTreeSet<ExtensionProvides>,
|
||||
) -> Condition {
|
||||
if provides_filter.contains(&ExtensionProvides::Themes) {
|
||||
condition = condition.add(extension_version::Column::ProvidesThemes.eq(true));
|
||||
}
|
||||
|
||||
if provides_filter.contains(&ExtensionProvides::IconThemes) {
|
||||
condition = condition.add(extension_version::Column::ProvidesIconThemes.eq(true));
|
||||
}
|
||||
|
||||
if provides_filter.contains(&ExtensionProvides::Languages) {
|
||||
condition = condition.add(extension_version::Column::ProvidesLanguages.eq(true));
|
||||
}
|
||||
|
||||
if provides_filter.contains(&ExtensionProvides::Grammars) {
|
||||
condition = condition.add(extension_version::Column::ProvidesGrammars.eq(true));
|
||||
}
|
||||
|
||||
if provides_filter.contains(&ExtensionProvides::LanguageServers) {
|
||||
condition = condition.add(extension_version::Column::ProvidesLanguageServers.eq(true));
|
||||
}
|
||||
|
||||
if provides_filter.contains(&ExtensionProvides::ContextServers) {
|
||||
condition = condition.add(extension_version::Column::ProvidesContextServers.eq(true));
|
||||
}
|
||||
|
||||
if provides_filter.contains(&ExtensionProvides::SlashCommands) {
|
||||
condition = condition.add(extension_version::Column::ProvidesSlashCommands.eq(true));
|
||||
}
|
||||
|
||||
if provides_filter.contains(&ExtensionProvides::IndexedDocsProviders) {
|
||||
condition = condition.add(extension_version::Column::ProvidesIndexedDocsProviders.eq(true));
|
||||
}
|
||||
|
||||
if provides_filter.contains(&ExtensionProvides::Snippets) {
|
||||
condition = condition.add(extension_version::Column::ProvidesSnippets.eq(true));
|
||||
}
|
||||
|
||||
condition
|
||||
}
|
||||
|
||||
fn metadata_from_extension_and_version(
|
||||
extension: extension::Model,
|
||||
version: extension_version::Model,
|
||||
) -> ExtensionMetadata {
|
||||
let provides = version.provides();
|
||||
|
||||
ExtensionMetadata {
|
||||
id: extension.external_id.into(),
|
||||
manifest: rpc::ExtensionApiManifest {
|
||||
@@ -370,6 +453,7 @@ fn metadata_from_extension_and_version(
|
||||
repository: version.repository,
|
||||
schema_version: Some(version.schema_version),
|
||||
wasm_api_version: version.wasm_api_version,
|
||||
provides,
|
||||
},
|
||||
|
||||
published_at: convert_time_to_chrono(version.published_at),
|
||||
|
||||
@@ -333,6 +333,9 @@ impl Database {
|
||||
scan_id: ActiveValue::set(update.scan_id as i64),
|
||||
branch: ActiveValue::set(repository.branch.clone()),
|
||||
is_deleted: ActiveValue::set(false),
|
||||
current_merge_conflicts: ActiveValue::Set(Some(
|
||||
serde_json::to_string(&repository.current_merge_conflicts).unwrap(),
|
||||
)),
|
||||
},
|
||||
))
|
||||
.on_conflict(
|
||||
@@ -769,6 +772,13 @@ impl Database {
|
||||
updated_statuses.push(db_status_to_proto(status_entry)?);
|
||||
}
|
||||
|
||||
let current_merge_conflicts = db_repository_entry
|
||||
.current_merge_conflicts
|
||||
.as_ref()
|
||||
.map(|conflicts| serde_json::from_str(&conflicts))
|
||||
.transpose()?
|
||||
.unwrap_or_default();
|
||||
|
||||
worktree.repository_entries.insert(
|
||||
db_repository_entry.work_directory_id as u64,
|
||||
proto::RepositoryEntry {
|
||||
@@ -776,6 +786,7 @@ impl Database {
|
||||
branch: db_repository_entry.branch,
|
||||
updated_statuses,
|
||||
removed_statuses: Vec::new(),
|
||||
current_merge_conflicts,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -736,11 +736,19 @@ impl Database {
|
||||
}
|
||||
}
|
||||
|
||||
let current_merge_conflicts = db_repository
|
||||
.current_merge_conflicts
|
||||
.as_ref()
|
||||
.map(|conflicts| serde_json::from_str(&conflicts))
|
||||
.transpose()?
|
||||
.unwrap_or_default();
|
||||
|
||||
worktree.updated_repositories.push(proto::RepositoryEntry {
|
||||
work_directory_id: db_repository.work_directory_id as u64,
|
||||
branch: db_repository.branch,
|
||||
updated_statuses,
|
||||
removed_statuses,
|
||||
current_merge_conflicts,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ pub struct Model {
|
||||
pub id: BillingCustomerId,
|
||||
pub user_id: UserId,
|
||||
pub stripe_customer_id: String,
|
||||
pub has_overdue_invoices: bool,
|
||||
pub created_at: DateTime,
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
use crate::db::ExtensionId;
|
||||
use collections::BTreeSet;
|
||||
use rpc::ExtensionProvides;
|
||||
use sea_orm::entity::prelude::*;
|
||||
use time::PrimitiveDateTime;
|
||||
|
||||
@@ -16,6 +18,58 @@ pub struct Model {
|
||||
pub schema_version: i32,
|
||||
pub wasm_api_version: Option<String>,
|
||||
pub download_count: i64,
|
||||
pub provides_themes: bool,
|
||||
pub provides_icon_themes: bool,
|
||||
pub provides_languages: bool,
|
||||
pub provides_grammars: bool,
|
||||
pub provides_language_servers: bool,
|
||||
pub provides_context_servers: bool,
|
||||
pub provides_slash_commands: bool,
|
||||
pub provides_indexed_docs_providers: bool,
|
||||
pub provides_snippets: bool,
|
||||
}
|
||||
|
||||
impl Model {
|
||||
pub fn provides(&self) -> BTreeSet<ExtensionProvides> {
|
||||
let mut provides = BTreeSet::default();
|
||||
if self.provides_themes {
|
||||
provides.insert(ExtensionProvides::Themes);
|
||||
}
|
||||
|
||||
if self.provides_icon_themes {
|
||||
provides.insert(ExtensionProvides::IconThemes);
|
||||
}
|
||||
|
||||
if self.provides_languages {
|
||||
provides.insert(ExtensionProvides::Languages);
|
||||
}
|
||||
|
||||
if self.provides_grammars {
|
||||
provides.insert(ExtensionProvides::Grammars);
|
||||
}
|
||||
|
||||
if self.provides_language_servers {
|
||||
provides.insert(ExtensionProvides::LanguageServers);
|
||||
}
|
||||
|
||||
if self.provides_context_servers {
|
||||
provides.insert(ExtensionProvides::ContextServers);
|
||||
}
|
||||
|
||||
if self.provides_slash_commands {
|
||||
provides.insert(ExtensionProvides::SlashCommands);
|
||||
}
|
||||
|
||||
if self.provides_indexed_docs_providers {
|
||||
provides.insert(ExtensionProvides::IndexedDocsProviders);
|
||||
}
|
||||
|
||||
if self.provides_snippets {
|
||||
provides.insert(ExtensionProvides::Snippets);
|
||||
}
|
||||
|
||||
provides
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
|
||||
@@ -13,6 +13,8 @@ pub struct Model {
|
||||
pub scan_id: i64,
|
||||
pub branch: Option<String>,
|
||||
pub is_deleted: bool,
|
||||
// JSON array typed string
|
||||
pub current_merge_conflicts: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::db::billing_subscription::{StripeCancellationReason, StripeSubscriptionStatus};
|
||||
use crate::db::billing_subscription::StripeSubscriptionStatus;
|
||||
use crate::db::tests::new_test_user;
|
||||
use crate::db::{CreateBillingCustomerParams, CreateBillingSubscriptionParams};
|
||||
use crate::test_both_dbs;
|
||||
@@ -88,113 +88,3 @@ async fn test_get_active_billing_subscriptions(db: &Arc<Database>) {
|
||||
assert_eq!(subscription_count, 0);
|
||||
}
|
||||
}
|
||||
|
||||
test_both_dbs!(
|
||||
test_count_overdue_billing_subscriptions,
|
||||
test_count_overdue_billing_subscriptions_postgres,
|
||||
test_count_overdue_billing_subscriptions_sqlite
|
||||
);
|
||||
|
||||
async fn test_count_overdue_billing_subscriptions(db: &Arc<Database>) {
|
||||
// A user with no subscription has no overdue billing subscriptions.
|
||||
{
|
||||
let user_id = new_test_user(db, "no-subscription-user@example.com").await;
|
||||
let subscription_count = db
|
||||
.count_overdue_billing_subscriptions(user_id)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(subscription_count, 0);
|
||||
}
|
||||
|
||||
// A user with a past-due subscription has an overdue billing subscription.
|
||||
{
|
||||
let user_id = new_test_user(db, "past-due-user@example.com").await;
|
||||
let customer = db
|
||||
.create_billing_customer(&CreateBillingCustomerParams {
|
||||
user_id,
|
||||
stripe_customer_id: "cus_past_due_user".into(),
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(customer.stripe_customer_id, "cus_past_due_user".to_string());
|
||||
|
||||
db.create_billing_subscription(&CreateBillingSubscriptionParams {
|
||||
billing_customer_id: customer.id,
|
||||
stripe_subscription_id: "sub_past_due_user".into(),
|
||||
stripe_subscription_status: StripeSubscriptionStatus::PastDue,
|
||||
stripe_cancellation_reason: None,
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let subscription_count = db
|
||||
.count_overdue_billing_subscriptions(user_id)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(subscription_count, 1);
|
||||
}
|
||||
|
||||
// A user with a canceled subscription with a reason of `payment_failed` has an overdue billing subscription.
|
||||
{
|
||||
let user_id =
|
||||
new_test_user(db, "canceled-subscription-payment-failed-user@example.com").await;
|
||||
let customer = db
|
||||
.create_billing_customer(&CreateBillingCustomerParams {
|
||||
user_id,
|
||||
stripe_customer_id: "cus_canceled_subscription_payment_failed_user".into(),
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
customer.stripe_customer_id,
|
||||
"cus_canceled_subscription_payment_failed_user".to_string()
|
||||
);
|
||||
|
||||
db.create_billing_subscription(&CreateBillingSubscriptionParams {
|
||||
billing_customer_id: customer.id,
|
||||
stripe_subscription_id: "sub_canceled_subscription_payment_failed_user".into(),
|
||||
stripe_subscription_status: StripeSubscriptionStatus::Canceled,
|
||||
stripe_cancellation_reason: Some(StripeCancellationReason::PaymentFailed),
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let subscription_count = db
|
||||
.count_overdue_billing_subscriptions(user_id)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(subscription_count, 1);
|
||||
}
|
||||
|
||||
// A user with a canceled subscription with a reason of `cancellation_requested` has no overdue billing subscriptions.
|
||||
{
|
||||
let user_id = new_test_user(db, "canceled-subscription-user@example.com").await;
|
||||
let customer = db
|
||||
.create_billing_customer(&CreateBillingCustomerParams {
|
||||
user_id,
|
||||
stripe_customer_id: "cus_canceled_subscription_user".into(),
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
customer.stripe_customer_id,
|
||||
"cus_canceled_subscription_user".to_string()
|
||||
);
|
||||
|
||||
db.create_billing_subscription(&CreateBillingSubscriptionParams {
|
||||
billing_customer_id: customer.id,
|
||||
stripe_subscription_id: "sub_canceled_subscription_user".into(),
|
||||
stripe_subscription_status: StripeSubscriptionStatus::Canceled,
|
||||
stripe_cancellation_reason: Some(StripeCancellationReason::CancellationRequested),
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let subscription_count = db
|
||||
.count_overdue_billing_subscriptions(user_id)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(subscription_count, 0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
use std::collections::BTreeSet;
|
||||
use std::sync::Arc;
|
||||
|
||||
use rpc::ExtensionProvides;
|
||||
|
||||
use super::Database;
|
||||
use crate::db::ExtensionVersionConstraints;
|
||||
use crate::{
|
||||
db::{queries::extensions::convert_time_to_chrono, ExtensionMetadata, NewExtensionVersion},
|
||||
test_both_dbs,
|
||||
};
|
||||
use std::sync::Arc;
|
||||
|
||||
test_both_dbs!(
|
||||
test_extensions,
|
||||
@@ -16,7 +20,7 @@ async fn test_extensions(db: &Arc<Database>) {
|
||||
let versions = db.get_known_extension_versions().await.unwrap();
|
||||
assert!(versions.is_empty());
|
||||
|
||||
let extensions = db.get_extensions(None, 1, 5).await.unwrap();
|
||||
let extensions = db.get_extensions(None, None, 1, 5).await.unwrap();
|
||||
assert!(extensions.is_empty());
|
||||
|
||||
let t0 = time::OffsetDateTime::from_unix_timestamp_nanos(0).unwrap();
|
||||
@@ -37,6 +41,7 @@ async fn test_extensions(db: &Arc<Database>) {
|
||||
repository: "ext1/repo".into(),
|
||||
schema_version: 1,
|
||||
wasm_api_version: None,
|
||||
provides: BTreeSet::default(),
|
||||
published_at: t0,
|
||||
},
|
||||
NewExtensionVersion {
|
||||
@@ -47,6 +52,7 @@ async fn test_extensions(db: &Arc<Database>) {
|
||||
repository: "ext1/repo".into(),
|
||||
schema_version: 1,
|
||||
wasm_api_version: None,
|
||||
provides: BTreeSet::default(),
|
||||
published_at: t0,
|
||||
},
|
||||
],
|
||||
@@ -61,6 +67,7 @@ async fn test_extensions(db: &Arc<Database>) {
|
||||
repository: "ext2/repo".into(),
|
||||
schema_version: 0,
|
||||
wasm_api_version: None,
|
||||
provides: BTreeSet::default(),
|
||||
published_at: t0,
|
||||
}],
|
||||
),
|
||||
@@ -83,7 +90,7 @@ async fn test_extensions(db: &Arc<Database>) {
|
||||
);
|
||||
|
||||
// The latest version of each extension is returned.
|
||||
let extensions = db.get_extensions(None, 1, 5).await.unwrap();
|
||||
let extensions = db.get_extensions(None, None, 1, 5).await.unwrap();
|
||||
assert_eq!(
|
||||
extensions,
|
||||
&[
|
||||
@@ -97,6 +104,7 @@ async fn test_extensions(db: &Arc<Database>) {
|
||||
repository: "ext1/repo".into(),
|
||||
schema_version: Some(1),
|
||||
wasm_api_version: None,
|
||||
provides: BTreeSet::default(),
|
||||
},
|
||||
published_at: t0_chrono,
|
||||
download_count: 0,
|
||||
@@ -111,6 +119,7 @@ async fn test_extensions(db: &Arc<Database>) {
|
||||
repository: "ext2/repo".into(),
|
||||
schema_version: Some(0),
|
||||
wasm_api_version: None,
|
||||
provides: BTreeSet::default(),
|
||||
},
|
||||
published_at: t0_chrono,
|
||||
download_count: 0
|
||||
@@ -119,7 +128,7 @@ async fn test_extensions(db: &Arc<Database>) {
|
||||
);
|
||||
|
||||
// Extensions with too new of a schema version are excluded.
|
||||
let extensions = db.get_extensions(None, 0, 5).await.unwrap();
|
||||
let extensions = db.get_extensions(None, None, 0, 5).await.unwrap();
|
||||
assert_eq!(
|
||||
extensions,
|
||||
&[ExtensionMetadata {
|
||||
@@ -132,6 +141,7 @@ async fn test_extensions(db: &Arc<Database>) {
|
||||
repository: "ext2/repo".into(),
|
||||
schema_version: Some(0),
|
||||
wasm_api_version: None,
|
||||
provides: BTreeSet::default(),
|
||||
},
|
||||
published_at: t0_chrono,
|
||||
download_count: 0
|
||||
@@ -158,7 +168,7 @@ async fn test_extensions(db: &Arc<Database>) {
|
||||
.unwrap());
|
||||
|
||||
// Extensions are returned in descending order of total downloads.
|
||||
let extensions = db.get_extensions(None, 1, 5).await.unwrap();
|
||||
let extensions = db.get_extensions(None, None, 1, 5).await.unwrap();
|
||||
assert_eq!(
|
||||
extensions,
|
||||
&[
|
||||
@@ -172,6 +182,7 @@ async fn test_extensions(db: &Arc<Database>) {
|
||||
repository: "ext2/repo".into(),
|
||||
schema_version: Some(0),
|
||||
wasm_api_version: None,
|
||||
provides: BTreeSet::default(),
|
||||
},
|
||||
published_at: t0_chrono,
|
||||
download_count: 7
|
||||
@@ -186,6 +197,7 @@ async fn test_extensions(db: &Arc<Database>) {
|
||||
repository: "ext1/repo".into(),
|
||||
schema_version: Some(1),
|
||||
wasm_api_version: None,
|
||||
provides: BTreeSet::default(),
|
||||
},
|
||||
published_at: t0_chrono,
|
||||
download_count: 5,
|
||||
@@ -207,6 +219,7 @@ async fn test_extensions(db: &Arc<Database>) {
|
||||
repository: "ext1/repo".into(),
|
||||
schema_version: 1,
|
||||
wasm_api_version: None,
|
||||
provides: BTreeSet::default(),
|
||||
published_at: t0,
|
||||
}],
|
||||
),
|
||||
@@ -220,6 +233,7 @@ async fn test_extensions(db: &Arc<Database>) {
|
||||
repository: "ext2/repo".into(),
|
||||
schema_version: 0,
|
||||
wasm_api_version: None,
|
||||
provides: BTreeSet::default(),
|
||||
published_at: t0,
|
||||
}],
|
||||
),
|
||||
@@ -244,7 +258,7 @@ async fn test_extensions(db: &Arc<Database>) {
|
||||
.collect()
|
||||
);
|
||||
|
||||
let extensions = db.get_extensions(None, 1, 5).await.unwrap();
|
||||
let extensions = db.get_extensions(None, None, 1, 5).await.unwrap();
|
||||
assert_eq!(
|
||||
extensions,
|
||||
&[
|
||||
@@ -258,6 +272,7 @@ async fn test_extensions(db: &Arc<Database>) {
|
||||
repository: "ext2/repo".into(),
|
||||
schema_version: Some(0),
|
||||
wasm_api_version: None,
|
||||
provides: BTreeSet::default(),
|
||||
},
|
||||
published_at: t0_chrono,
|
||||
download_count: 7
|
||||
@@ -272,6 +287,7 @@ async fn test_extensions(db: &Arc<Database>) {
|
||||
repository: "ext1/repo".into(),
|
||||
schema_version: Some(1),
|
||||
wasm_api_version: None,
|
||||
provides: BTreeSet::default(),
|
||||
},
|
||||
published_at: t0_chrono,
|
||||
download_count: 5,
|
||||
@@ -290,7 +306,7 @@ async fn test_extensions_by_id(db: &Arc<Database>) {
|
||||
let versions = db.get_known_extension_versions().await.unwrap();
|
||||
assert!(versions.is_empty());
|
||||
|
||||
let extensions = db.get_extensions(None, 1, 5).await.unwrap();
|
||||
let extensions = db.get_extensions(None, None, 1, 5).await.unwrap();
|
||||
assert!(extensions.is_empty());
|
||||
|
||||
let t0 = time::OffsetDateTime::from_unix_timestamp_nanos(0).unwrap();
|
||||
@@ -311,6 +327,10 @@ async fn test_extensions_by_id(db: &Arc<Database>) {
|
||||
repository: "ext1/repo".into(),
|
||||
schema_version: 1,
|
||||
wasm_api_version: Some("0.0.4".into()),
|
||||
provides: BTreeSet::from_iter([
|
||||
ExtensionProvides::Grammars,
|
||||
ExtensionProvides::Languages,
|
||||
]),
|
||||
published_at: t0,
|
||||
},
|
||||
NewExtensionVersion {
|
||||
@@ -321,6 +341,11 @@ async fn test_extensions_by_id(db: &Arc<Database>) {
|
||||
repository: "ext1/repo".into(),
|
||||
schema_version: 1,
|
||||
wasm_api_version: Some("0.0.4".into()),
|
||||
provides: BTreeSet::from_iter([
|
||||
ExtensionProvides::Grammars,
|
||||
ExtensionProvides::Languages,
|
||||
ExtensionProvides::LanguageServers,
|
||||
]),
|
||||
published_at: t0,
|
||||
},
|
||||
NewExtensionVersion {
|
||||
@@ -331,6 +356,11 @@ async fn test_extensions_by_id(db: &Arc<Database>) {
|
||||
repository: "ext1/repo".into(),
|
||||
schema_version: 1,
|
||||
wasm_api_version: Some("0.0.5".into()),
|
||||
provides: BTreeSet::from_iter([
|
||||
ExtensionProvides::Grammars,
|
||||
ExtensionProvides::Languages,
|
||||
ExtensionProvides::LanguageServers,
|
||||
]),
|
||||
published_at: t0,
|
||||
},
|
||||
],
|
||||
@@ -345,6 +375,7 @@ async fn test_extensions_by_id(db: &Arc<Database>) {
|
||||
repository: "ext2/repo".into(),
|
||||
schema_version: 0,
|
||||
wasm_api_version: None,
|
||||
provides: BTreeSet::default(),
|
||||
published_at: t0,
|
||||
}],
|
||||
),
|
||||
@@ -378,6 +409,11 @@ async fn test_extensions_by_id(db: &Arc<Database>) {
|
||||
repository: "ext1/repo".into(),
|
||||
schema_version: Some(1),
|
||||
wasm_api_version: Some("0.0.4".into()),
|
||||
provides: BTreeSet::from_iter([
|
||||
ExtensionProvides::Grammars,
|
||||
ExtensionProvides::Languages,
|
||||
ExtensionProvides::LanguageServers,
|
||||
]),
|
||||
},
|
||||
published_at: t0_chrono,
|
||||
download_count: 0,
|
||||
|
||||
@@ -21,15 +21,12 @@ use chrono::{DateTime, Duration, Utc};
|
||||
use collections::HashMap;
|
||||
use db::TokenUsage;
|
||||
use db::{usage_measure::UsageMeasure, ActiveUserCount, LlmDatabase};
|
||||
use futures::{FutureExt, Stream, StreamExt as _};
|
||||
use futures::{Stream, StreamExt as _};
|
||||
use reqwest_client::ReqwestClient;
|
||||
use rpc::{
|
||||
proto::Plan, LanguageModelProvider, PerformCompletionParams, EXPIRED_LLM_TOKEN_HEADER_NAME,
|
||||
};
|
||||
use rpc::{
|
||||
ListModelsResponse, PredictEditsParams, PredictEditsResponse,
|
||||
MAX_LLM_MONTHLY_SPEND_REACHED_HEADER_NAME,
|
||||
};
|
||||
use rpc::{ListModelsResponse, MAX_LLM_MONTHLY_SPEND_REACHED_HEADER_NAME};
|
||||
use serde_json::json;
|
||||
use std::{
|
||||
pin::Pin,
|
||||
@@ -42,6 +39,8 @@ use util::ResultExt;
|
||||
|
||||
pub use token::*;
|
||||
|
||||
const ACTIVE_USER_COUNT_CACHE_DURATION: Duration = Duration::seconds(30);
|
||||
|
||||
pub struct LlmState {
|
||||
pub config: Config,
|
||||
pub executor: Executor,
|
||||
@@ -52,8 +51,6 @@ pub struct LlmState {
|
||||
RwLock<HashMap<(LanguageModelProvider, String), (DateTime<Utc>, ActiveUserCount)>>,
|
||||
}
|
||||
|
||||
const ACTIVE_USER_COUNT_CACHE_DURATION: Duration = Duration::seconds(30);
|
||||
|
||||
impl LlmState {
|
||||
pub async fn new(config: Config, executor: Executor) -> Result<Arc<Self>> {
|
||||
let database_url = config
|
||||
@@ -120,7 +117,6 @@ pub fn routes() -> Router<(), Body> {
|
||||
Router::new()
|
||||
.route("/models", get(list_models))
|
||||
.route("/completion", post(perform_completion))
|
||||
.route("/predict_edits", post(predict_edits))
|
||||
.layer(middleware::from_fn(validate_api_token))
|
||||
}
|
||||
|
||||
@@ -434,156 +430,6 @@ fn normalize_model_name(known_models: Vec<String>, name: String) -> String {
|
||||
}
|
||||
}
|
||||
|
||||
async fn predict_edits(
|
||||
Extension(state): Extension<Arc<LlmState>>,
|
||||
Extension(claims): Extension<LlmTokenClaims>,
|
||||
_country_code_header: Option<TypedHeader<CloudflareIpCountryHeader>>,
|
||||
Json(params): Json<PredictEditsParams>,
|
||||
) -> Result<impl IntoResponse> {
|
||||
if !claims.is_staff && !claims.has_predict_edits_feature_flag {
|
||||
return Err(Error::http(
|
||||
StatusCode::FORBIDDEN,
|
||||
"no access to Zed's edit prediction feature".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
let sample_input_output = claims.is_staff && rand::random::<f32>() < 0.1;
|
||||
|
||||
let api_url = state
|
||||
.config
|
||||
.prediction_api_url
|
||||
.as_ref()
|
||||
.context("no PREDICTION_API_URL configured on the server")?;
|
||||
let api_key = state
|
||||
.config
|
||||
.prediction_api_key
|
||||
.as_ref()
|
||||
.context("no PREDICTION_API_KEY configured on the server")?;
|
||||
let model = state
|
||||
.config
|
||||
.prediction_model
|
||||
.as_ref()
|
||||
.context("no PREDICTION_MODEL configured on the server")?;
|
||||
|
||||
let outline_prefix = params
|
||||
.outline
|
||||
.as_ref()
|
||||
.map(|outline| format!("### Outline for current file:\n{}\n", outline))
|
||||
.unwrap_or_default();
|
||||
|
||||
let prompt = include_str!("./llm/prediction_prompt.md")
|
||||
.replace("<outline>", &outline_prefix)
|
||||
.replace("<events>", ¶ms.input_events)
|
||||
.replace("<excerpt>", ¶ms.input_excerpt);
|
||||
|
||||
let request_start = std::time::Instant::now();
|
||||
let timeout = state
|
||||
.executor
|
||||
.sleep(std::time::Duration::from_secs(2))
|
||||
.fuse();
|
||||
let response = fireworks::complete(
|
||||
&state.http_client,
|
||||
api_url,
|
||||
api_key,
|
||||
fireworks::CompletionRequest {
|
||||
model: model.to_string(),
|
||||
prompt: prompt.clone(),
|
||||
max_tokens: 2048,
|
||||
temperature: 0.,
|
||||
prediction: Some(fireworks::Prediction::Content {
|
||||
content: params.input_excerpt.clone(),
|
||||
}),
|
||||
rewrite_speculation: Some(true),
|
||||
},
|
||||
)
|
||||
.fuse();
|
||||
futures::pin_mut!(timeout);
|
||||
futures::pin_mut!(response);
|
||||
|
||||
futures::select! {
|
||||
_ = timeout => {
|
||||
state.executor.spawn_detached({
|
||||
let kinesis_client = state.kinesis_client.clone();
|
||||
let kinesis_stream = state.config.kinesis_stream.clone();
|
||||
let model = model.clone();
|
||||
async move {
|
||||
SnowflakeRow::new(
|
||||
"Fireworks Completion Timeout",
|
||||
claims.metrics_id,
|
||||
claims.is_staff,
|
||||
claims.system_id.clone(),
|
||||
json!({
|
||||
"model": model.to_string(),
|
||||
"prompt": prompt,
|
||||
}),
|
||||
)
|
||||
.write(&kinesis_client, &kinesis_stream)
|
||||
.await
|
||||
.log_err();
|
||||
}
|
||||
});
|
||||
Err(anyhow!("request timed out"))?
|
||||
},
|
||||
response = response => {
|
||||
let duration = request_start.elapsed();
|
||||
|
||||
let mut response = response?;
|
||||
let choice = response
|
||||
.completion
|
||||
.choices
|
||||
.pop()
|
||||
.context("no output from completion response")?;
|
||||
|
||||
state.executor.spawn_detached({
|
||||
let kinesis_client = state.kinesis_client.clone();
|
||||
let kinesis_stream = state.config.kinesis_stream.clone();
|
||||
let model = model.clone();
|
||||
let output = choice.text.clone();
|
||||
|
||||
async move {
|
||||
let properties = if sample_input_output {
|
||||
json!({
|
||||
"model": model.to_string(),
|
||||
"headers": response.headers,
|
||||
"usage": response.completion.usage,
|
||||
"duration": duration.as_secs_f64(),
|
||||
"prompt": prompt,
|
||||
"input_excerpt": params.input_excerpt,
|
||||
"input_events": params.input_events,
|
||||
"outline": params.outline,
|
||||
"output": output,
|
||||
"is_sampled": true,
|
||||
})
|
||||
} else {
|
||||
json!({
|
||||
"model": model.to_string(),
|
||||
"headers": response.headers,
|
||||
"usage": response.completion.usage,
|
||||
"duration": duration.as_secs_f64(),
|
||||
"is_sampled": false,
|
||||
})
|
||||
};
|
||||
|
||||
SnowflakeRow::new(
|
||||
"Fireworks Completion Requested",
|
||||
claims.metrics_id,
|
||||
claims.is_staff,
|
||||
claims.system_id.clone(),
|
||||
properties,
|
||||
)
|
||||
.write(&kinesis_client, &kinesis_stream)
|
||||
.await
|
||||
.log_err();
|
||||
}
|
||||
});
|
||||
|
||||
Ok(Json(PredictEditsResponse {
|
||||
output_excerpt: choice.text,
|
||||
}))
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// The maximum monthly spending an individual user can reach on the free tier
|
||||
/// before they have to pay.
|
||||
pub const FREE_TIER_MONTHLY_SPENDING_LIMIT: Cents = Cents::from_dollars(10);
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
<outline>## Task
|
||||
Below is an instruction that describes a task, paired with an input that provides further context. Write a response that appropriately completes the request.
|
||||
|
||||
### Instruction:
|
||||
You are a code completion assistant and your task is to analyze user edits and then rewrite an excerpt that the user provides, suggesting the appropriate edits within the excerpt, taking into account the cursor location.
|
||||
|
||||
### Events:
|
||||
<events>
|
||||
|
||||
### Input:
|
||||
<excerpt>
|
||||
|
||||
### Response:
|
||||
@@ -309,7 +309,8 @@ impl Server {
|
||||
.add_request_handler(forward_read_only_project_request::<proto::ResolveInlayHint>)
|
||||
.add_request_handler(forward_read_only_project_request::<proto::OpenBufferByPath>)
|
||||
.add_request_handler(forward_read_only_project_request::<proto::GitBranches>)
|
||||
.add_request_handler(forward_read_only_project_request::<proto::GetStagedText>)
|
||||
.add_request_handler(forward_read_only_project_request::<proto::OpenUnstagedDiff>)
|
||||
.add_request_handler(forward_read_only_project_request::<proto::OpenUncommittedDiff>)
|
||||
.add_request_handler(
|
||||
forward_mutating_project_request::<proto::RegisterBufferWithLanguageServers>,
|
||||
)
|
||||
@@ -348,7 +349,7 @@ impl Server {
|
||||
.add_message_handler(broadcast_project_message_from_host::<proto::UpdateBufferFile>)
|
||||
.add_message_handler(broadcast_project_message_from_host::<proto::BufferReloaded>)
|
||||
.add_message_handler(broadcast_project_message_from_host::<proto::BufferSaved>)
|
||||
.add_message_handler(broadcast_project_message_from_host::<proto::UpdateDiffBase>)
|
||||
.add_message_handler(broadcast_project_message_from_host::<proto::UpdateDiffBases>)
|
||||
.add_request_handler(get_users)
|
||||
.add_request_handler(fuzzy_search_users)
|
||||
.add_request_handler(request_contact)
|
||||
@@ -391,6 +392,10 @@ impl Server {
|
||||
.add_request_handler(forward_mutating_project_request::<proto::OpenContext>)
|
||||
.add_request_handler(forward_mutating_project_request::<proto::CreateContext>)
|
||||
.add_request_handler(forward_mutating_project_request::<proto::SynchronizeContexts>)
|
||||
.add_request_handler(forward_mutating_project_request::<proto::Stage>)
|
||||
.add_request_handler(forward_mutating_project_request::<proto::Unstage>)
|
||||
.add_request_handler(forward_mutating_project_request::<proto::Commit>)
|
||||
.add_request_handler(forward_mutating_project_request::<proto::OpenCommitMessageBuffer>)
|
||||
.add_message_handler(broadcast_project_message_from_host::<proto::AdvertiseContexts>)
|
||||
.add_message_handler(update_context)
|
||||
.add_request_handler({
|
||||
|
||||
@@ -342,7 +342,7 @@ async fn test_multiple_handles_to_channel_buffer(
|
||||
future::try_join3(channel_buffer_1, channel_buffer_2, channel_buffer_3)
|
||||
.await
|
||||
.unwrap();
|
||||
let channel_buffer_model_id = channel_buffer.entity_id();
|
||||
let channel_buffer_entity_id = channel_buffer.entity_id();
|
||||
assert_eq!(channel_buffer, channel_buffer_2);
|
||||
assert_eq!(channel_buffer, channel_buffer_3);
|
||||
|
||||
@@ -366,7 +366,7 @@ async fn test_multiple_handles_to_channel_buffer(
|
||||
.update(cx_a, |store, cx| store.open_channel_buffer(channel_id, cx))
|
||||
.await
|
||||
.unwrap();
|
||||
assert_ne!(channel_buffer.entity_id(), channel_buffer_model_id);
|
||||
assert_ne!(channel_buffer.entity_id(), channel_buffer_entity_id);
|
||||
channel_buffer.update(cx_a, |buffer, cx| {
|
||||
buffer.buffer().update(cx, |buffer, _| {
|
||||
assert_eq!(buffer.text(), "hello");
|
||||
|
||||
@@ -1991,10 +1991,9 @@ async fn test_git_blame_is_forwarded(cx_a: &mut TestAppContext, cx_b: &mut TestA
|
||||
.collect(),
|
||||
remote_url: Some("git@github.com:zed-industries/zed.git".to_string()),
|
||||
};
|
||||
client_a.fs().set_blame_for_repo(
|
||||
Path::new("/my-repo/.git"),
|
||||
vec![(Path::new("file.txt"), blame)],
|
||||
);
|
||||
client_a
|
||||
.fs()
|
||||
.set_blame_for_repo(Path::new("/my-repo/.git"), vec![("file.txt".into(), blame)]);
|
||||
|
||||
let (project_a, worktree_id) = client_a.build_local_project("/my-repo", cx_a).await;
|
||||
let project_id = active_call_a
|
||||
|
||||
@@ -8,7 +8,6 @@ use crate::{
|
||||
use anyhow::{anyhow, Result};
|
||||
use assistant_context_editor::ContextStore;
|
||||
use assistant_slash_command::SlashCommandWorkingSet;
|
||||
use assistant_tool::ToolWorkingSet;
|
||||
use call::{room, ActiveCall, ParticipantLocation, Room};
|
||||
use client::{User, RECEIVE_TIMEOUT};
|
||||
use collections::{HashMap, HashSet};
|
||||
@@ -2559,13 +2558,27 @@ async fn test_git_diff_base_change(
|
||||
|
||||
let project_remote = client_b.join_remote_project(project_id, cx_b).await;
|
||||
|
||||
let diff_base = "
|
||||
let staged_text = "
|
||||
one
|
||||
three
|
||||
"
|
||||
.unindent();
|
||||
|
||||
let new_diff_base = "
|
||||
let committed_text = "
|
||||
one
|
||||
TWO
|
||||
three
|
||||
"
|
||||
.unindent();
|
||||
|
||||
let new_committed_text = "
|
||||
one
|
||||
TWO_HUNDRED
|
||||
three
|
||||
"
|
||||
.unindent();
|
||||
|
||||
let new_staged_text = "
|
||||
one
|
||||
two
|
||||
"
|
||||
@@ -2573,7 +2586,11 @@ async fn test_git_diff_base_change(
|
||||
|
||||
client_a.fs().set_index_for_repo(
|
||||
Path::new("/dir/.git"),
|
||||
&[(Path::new("a.txt"), diff_base.clone())],
|
||||
&[("a.txt".into(), staged_text.clone())],
|
||||
);
|
||||
client_a.fs().set_head_for_repo(
|
||||
Path::new("/dir/.git"),
|
||||
&[("a.txt".into(), committed_text.clone())],
|
||||
);
|
||||
|
||||
// Create the buffer
|
||||
@@ -2581,25 +2598,25 @@ async fn test_git_diff_base_change(
|
||||
.update(cx_a, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
|
||||
.await
|
||||
.unwrap();
|
||||
let change_set_local_a = project_local
|
||||
let local_unstaged_diff_a = project_local
|
||||
.update(cx_a, |p, cx| {
|
||||
p.open_unstaged_changes(buffer_local_a.clone(), cx)
|
||||
p.open_unstaged_diff(buffer_local_a.clone(), cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Wait for it to catch up to the new diff
|
||||
executor.run_until_parked();
|
||||
change_set_local_a.read_with(cx_a, |change_set, cx| {
|
||||
local_unstaged_diff_a.read_with(cx_a, |diff, cx| {
|
||||
let buffer = buffer_local_a.read(cx);
|
||||
assert_eq!(
|
||||
change_set.base_text_string().as_deref(),
|
||||
Some(diff_base.as_str())
|
||||
diff.base_text_string().as_deref(),
|
||||
Some(staged_text.as_str())
|
||||
);
|
||||
git::diff::assert_hunks(
|
||||
change_set.diff_to_buffer.hunks_in_row_range(0..4, buffer),
|
||||
diff::assert_hunks(
|
||||
diff.snapshot.hunks_in_row_range(0..4, buffer),
|
||||
buffer,
|
||||
&diff_base,
|
||||
&diff.base_text_string().unwrap(),
|
||||
&[(1..2, "", "two\n")],
|
||||
);
|
||||
});
|
||||
@@ -2609,73 +2626,113 @@ async fn test_git_diff_base_change(
|
||||
.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
|
||||
.await
|
||||
.unwrap();
|
||||
let change_set_remote_a = project_remote
|
||||
let remote_unstaged_diff_a = project_remote
|
||||
.update(cx_b, |p, cx| {
|
||||
p.open_unstaged_changes(buffer_remote_a.clone(), cx)
|
||||
p.open_unstaged_diff(buffer_remote_a.clone(), cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Wait remote buffer to catch up to the new diff
|
||||
executor.run_until_parked();
|
||||
change_set_remote_a.read_with(cx_b, |change_set, cx| {
|
||||
remote_unstaged_diff_a.read_with(cx_b, |diff, cx| {
|
||||
let buffer = buffer_remote_a.read(cx);
|
||||
assert_eq!(
|
||||
change_set.base_text_string().as_deref(),
|
||||
Some(diff_base.as_str())
|
||||
diff.base_text_string().as_deref(),
|
||||
Some(staged_text.as_str())
|
||||
);
|
||||
git::diff::assert_hunks(
|
||||
change_set.diff_to_buffer.hunks_in_row_range(0..4, buffer),
|
||||
diff::assert_hunks(
|
||||
diff.snapshot.hunks_in_row_range(0..4, buffer),
|
||||
buffer,
|
||||
&diff_base,
|
||||
&diff.base_text_string().unwrap(),
|
||||
&[(1..2, "", "two\n")],
|
||||
);
|
||||
});
|
||||
|
||||
// Update the staged text of the open buffer
|
||||
// Open uncommitted changes on the guest, without opening them on the host first
|
||||
let remote_uncommitted_diff_a = project_remote
|
||||
.update(cx_b, |p, cx| {
|
||||
p.open_uncommitted_diff(buffer_remote_a.clone(), cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
executor.run_until_parked();
|
||||
remote_uncommitted_diff_a.read_with(cx_b, |diff, cx| {
|
||||
let buffer = buffer_remote_a.read(cx);
|
||||
assert_eq!(
|
||||
diff.base_text_string().as_deref(),
|
||||
Some(committed_text.as_str())
|
||||
);
|
||||
diff::assert_hunks(
|
||||
diff.snapshot.hunks_in_row_range(0..4, buffer),
|
||||
buffer,
|
||||
&diff.base_text_string().unwrap(),
|
||||
&[(1..2, "TWO\n", "two\n")],
|
||||
);
|
||||
});
|
||||
|
||||
// Update the index text of the open buffer
|
||||
client_a.fs().set_index_for_repo(
|
||||
Path::new("/dir/.git"),
|
||||
&[(Path::new("a.txt"), new_diff_base.clone())],
|
||||
&[("a.txt".into(), new_staged_text.clone())],
|
||||
);
|
||||
client_a.fs().set_head_for_repo(
|
||||
Path::new("/dir/.git"),
|
||||
&[("a.txt".into(), new_committed_text.clone())],
|
||||
);
|
||||
|
||||
// Wait for buffer_local_a to receive it
|
||||
executor.run_until_parked();
|
||||
change_set_local_a.read_with(cx_a, |change_set, cx| {
|
||||
local_unstaged_diff_a.read_with(cx_a, |diff, cx| {
|
||||
let buffer = buffer_local_a.read(cx);
|
||||
assert_eq!(
|
||||
change_set.base_text_string().as_deref(),
|
||||
Some(new_diff_base.as_str())
|
||||
diff.base_text_string().as_deref(),
|
||||
Some(new_staged_text.as_str())
|
||||
);
|
||||
git::diff::assert_hunks(
|
||||
change_set.diff_to_buffer.hunks_in_row_range(0..4, buffer),
|
||||
diff::assert_hunks(
|
||||
diff.snapshot.hunks_in_row_range(0..4, buffer),
|
||||
buffer,
|
||||
&new_diff_base,
|
||||
&diff.base_text_string().unwrap(),
|
||||
&[(2..3, "", "three\n")],
|
||||
);
|
||||
});
|
||||
|
||||
change_set_remote_a.read_with(cx_b, |change_set, cx| {
|
||||
remote_unstaged_diff_a.read_with(cx_b, |diff, cx| {
|
||||
let buffer = buffer_remote_a.read(cx);
|
||||
assert_eq!(
|
||||
change_set.base_text_string().as_deref(),
|
||||
Some(new_diff_base.as_str())
|
||||
diff.base_text_string().as_deref(),
|
||||
Some(new_staged_text.as_str())
|
||||
);
|
||||
git::diff::assert_hunks(
|
||||
change_set.diff_to_buffer.hunks_in_row_range(0..4, buffer),
|
||||
diff::assert_hunks(
|
||||
diff.snapshot.hunks_in_row_range(0..4, buffer),
|
||||
buffer,
|
||||
&new_diff_base,
|
||||
&diff.base_text_string().unwrap(),
|
||||
&[(2..3, "", "three\n")],
|
||||
);
|
||||
});
|
||||
|
||||
remote_uncommitted_diff_a.read_with(cx_b, |diff, cx| {
|
||||
let buffer = buffer_remote_a.read(cx);
|
||||
assert_eq!(
|
||||
diff.base_text_string().as_deref(),
|
||||
Some(new_committed_text.as_str())
|
||||
);
|
||||
diff::assert_hunks(
|
||||
diff.snapshot.hunks_in_row_range(0..4, buffer),
|
||||
buffer,
|
||||
&diff.base_text_string().unwrap(),
|
||||
&[(1..2, "TWO_HUNDRED\n", "two\n")],
|
||||
);
|
||||
});
|
||||
|
||||
// Nested git dir
|
||||
let diff_base = "
|
||||
let staged_text = "
|
||||
one
|
||||
three
|
||||
"
|
||||
.unindent();
|
||||
|
||||
let new_diff_base = "
|
||||
let new_staged_text = "
|
||||
one
|
||||
two
|
||||
"
|
||||
@@ -2683,7 +2740,7 @@ async fn test_git_diff_base_change(
|
||||
|
||||
client_a.fs().set_index_for_repo(
|
||||
Path::new("/dir/sub/.git"),
|
||||
&[(Path::new("b.txt"), diff_base.clone())],
|
||||
&[("b.txt".into(), staged_text.clone())],
|
||||
);
|
||||
|
||||
// Create the buffer
|
||||
@@ -2691,25 +2748,25 @@ async fn test_git_diff_base_change(
|
||||
.update(cx_a, |p, cx| p.open_buffer((worktree_id, "sub/b.txt"), cx))
|
||||
.await
|
||||
.unwrap();
|
||||
let change_set_local_b = project_local
|
||||
let local_unstaged_diff_b = project_local
|
||||
.update(cx_a, |p, cx| {
|
||||
p.open_unstaged_changes(buffer_local_b.clone(), cx)
|
||||
p.open_unstaged_diff(buffer_local_b.clone(), cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Wait for it to catch up to the new diff
|
||||
executor.run_until_parked();
|
||||
change_set_local_b.read_with(cx_a, |change_set, cx| {
|
||||
local_unstaged_diff_b.read_with(cx_a, |diff, cx| {
|
||||
let buffer = buffer_local_b.read(cx);
|
||||
assert_eq!(
|
||||
change_set.base_text_string().as_deref(),
|
||||
Some(diff_base.as_str())
|
||||
diff.base_text_string().as_deref(),
|
||||
Some(staged_text.as_str())
|
||||
);
|
||||
git::diff::assert_hunks(
|
||||
change_set.diff_to_buffer.hunks_in_row_range(0..4, buffer),
|
||||
diff::assert_hunks(
|
||||
diff.snapshot.hunks_in_row_range(0..4, buffer),
|
||||
buffer,
|
||||
&diff_base,
|
||||
&diff.base_text_string().unwrap(),
|
||||
&[(1..2, "", "two\n")],
|
||||
);
|
||||
});
|
||||
@@ -2719,60 +2776,60 @@ async fn test_git_diff_base_change(
|
||||
.update(cx_b, |p, cx| p.open_buffer((worktree_id, "sub/b.txt"), cx))
|
||||
.await
|
||||
.unwrap();
|
||||
let change_set_remote_b = project_remote
|
||||
let remote_unstaged_diff_b = project_remote
|
||||
.update(cx_b, |p, cx| {
|
||||
p.open_unstaged_changes(buffer_remote_b.clone(), cx)
|
||||
p.open_unstaged_diff(buffer_remote_b.clone(), cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
executor.run_until_parked();
|
||||
change_set_remote_b.read_with(cx_b, |change_set, cx| {
|
||||
remote_unstaged_diff_b.read_with(cx_b, |diff, cx| {
|
||||
let buffer = buffer_remote_b.read(cx);
|
||||
assert_eq!(
|
||||
change_set.base_text_string().as_deref(),
|
||||
Some(diff_base.as_str())
|
||||
diff.base_text_string().as_deref(),
|
||||
Some(staged_text.as_str())
|
||||
);
|
||||
git::diff::assert_hunks(
|
||||
change_set.diff_to_buffer.hunks_in_row_range(0..4, buffer),
|
||||
diff::assert_hunks(
|
||||
diff.snapshot.hunks_in_row_range(0..4, buffer),
|
||||
buffer,
|
||||
&diff_base,
|
||||
&staged_text,
|
||||
&[(1..2, "", "two\n")],
|
||||
);
|
||||
});
|
||||
|
||||
// Update the staged text
|
||||
// Updatet the staged text
|
||||
client_a.fs().set_index_for_repo(
|
||||
Path::new("/dir/sub/.git"),
|
||||
&[(Path::new("b.txt"), new_diff_base.clone())],
|
||||
&[("b.txt".into(), new_staged_text.clone())],
|
||||
);
|
||||
|
||||
// Wait for buffer_local_b to receive it
|
||||
executor.run_until_parked();
|
||||
change_set_local_b.read_with(cx_a, |change_set, cx| {
|
||||
local_unstaged_diff_b.read_with(cx_a, |diff, cx| {
|
||||
let buffer = buffer_local_b.read(cx);
|
||||
assert_eq!(
|
||||
change_set.base_text_string().as_deref(),
|
||||
Some(new_diff_base.as_str())
|
||||
diff.base_text_string().as_deref(),
|
||||
Some(new_staged_text.as_str())
|
||||
);
|
||||
git::diff::assert_hunks(
|
||||
change_set.diff_to_buffer.hunks_in_row_range(0..4, buffer),
|
||||
diff::assert_hunks(
|
||||
diff.snapshot.hunks_in_row_range(0..4, buffer),
|
||||
buffer,
|
||||
&new_diff_base,
|
||||
&new_staged_text,
|
||||
&[(2..3, "", "three\n")],
|
||||
);
|
||||
});
|
||||
|
||||
change_set_remote_b.read_with(cx_b, |change_set, cx| {
|
||||
remote_unstaged_diff_b.read_with(cx_b, |diff, cx| {
|
||||
let buffer = buffer_remote_b.read(cx);
|
||||
assert_eq!(
|
||||
change_set.base_text_string().as_deref(),
|
||||
Some(new_diff_base.as_str())
|
||||
diff.base_text_string().as_deref(),
|
||||
Some(new_staged_text.as_str())
|
||||
);
|
||||
git::diff::assert_hunks(
|
||||
change_set.diff_to_buffer.hunks_in_row_range(0..4, buffer),
|
||||
diff::assert_hunks(
|
||||
diff.snapshot.hunks_in_row_range(0..4, buffer),
|
||||
buffer,
|
||||
&new_diff_base,
|
||||
&new_staged_text,
|
||||
&[(2..3, "", "three\n")],
|
||||
);
|
||||
});
|
||||
@@ -6547,7 +6604,6 @@ async fn test_context_collaboration_with_reconnect(
|
||||
project_a.clone(),
|
||||
prompt_builder.clone(),
|
||||
Arc::new(SlashCommandWorkingSet::default()),
|
||||
Arc::new(ToolWorkingSet::default()),
|
||||
cx,
|
||||
)
|
||||
})
|
||||
@@ -6559,7 +6615,6 @@ async fn test_context_collaboration_with_reconnect(
|
||||
project_b.clone(),
|
||||
prompt_builder.clone(),
|
||||
Arc::new(SlashCommandWorkingSet::default()),
|
||||
Arc::new(ToolWorkingSet::default()),
|
||||
cx,
|
||||
)
|
||||
})
|
||||
|
||||
@@ -953,8 +953,8 @@ impl RandomizedTest for ProjectCollaborationTest {
|
||||
|
||||
let dot_git_dir = repo_path.join(".git");
|
||||
let contents = contents
|
||||
.iter()
|
||||
.map(|(path, contents)| (path.as_path(), contents.clone()))
|
||||
.into_iter()
|
||||
.map(|(path, contents)| (path.into(), contents))
|
||||
.collect::<Vec<_>>();
|
||||
if client.fs().metadata(&dot_git_dir).await?.is_none() {
|
||||
client.fs().create_dir(&dot_git_dir).await?;
|
||||
@@ -1339,7 +1339,7 @@ impl RandomizedTest for ProjectCollaborationTest {
|
||||
project
|
||||
.buffer_store()
|
||||
.read(cx)
|
||||
.get_unstaged_changes(host_buffer.read(cx).remote_id())
|
||||
.get_unstaged_diff(host_buffer.read(cx).remote_id(), cx)
|
||||
.unwrap()
|
||||
.read(cx)
|
||||
.base_text_string()
|
||||
@@ -1348,7 +1348,7 @@ impl RandomizedTest for ProjectCollaborationTest {
|
||||
project
|
||||
.buffer_store()
|
||||
.read(cx)
|
||||
.get_unstaged_changes(guest_buffer.read(cx).remote_id())
|
||||
.get_unstaged_diff(guest_buffer.read(cx).remote_id(), cx)
|
||||
.unwrap()
|
||||
.read(cx)
|
||||
.base_text_string()
|
||||
|
||||
@@ -849,10 +849,10 @@ impl TestClient {
|
||||
) -> (Entity<Workspace>, &'a mut VisualTestContext) {
|
||||
let window = cx.update(|cx| cx.active_window().unwrap().downcast::<Workspace>().unwrap());
|
||||
|
||||
let model = window.root(cx).unwrap();
|
||||
let entity = window.root(cx).unwrap();
|
||||
let cx = VisualTestContext::from_window(*window.deref(), cx).as_mut();
|
||||
// it might be nice to try and cleanup these at the end of each test.
|
||||
(model, cx)
|
||||
(entity, cx)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -861,9 +861,9 @@ pub fn open_channel_notes(
|
||||
cx: &mut VisualTestContext,
|
||||
) -> Task<anyhow::Result<Entity<ChannelView>>> {
|
||||
let window = cx.update(|_, cx| cx.active_window().unwrap().downcast::<Workspace>().unwrap());
|
||||
let model = window.root(cx).unwrap();
|
||||
let entity = window.root(cx).unwrap();
|
||||
|
||||
cx.update(|window, cx| ChannelView::open(channel_id, None, model.clone(), window, cx))
|
||||
cx.update(|window, cx| ChannelView::open(channel_id, None, entity.clone(), window, cx))
|
||||
}
|
||||
|
||||
impl Drop for TestClient {
|
||||
|
||||
@@ -97,14 +97,14 @@ impl ChatPanel {
|
||||
});
|
||||
|
||||
cx.new(|cx| {
|
||||
let model = cx.entity().downgrade();
|
||||
let entity = cx.entity().downgrade();
|
||||
let message_list = ListState::new(
|
||||
0,
|
||||
gpui::ListAlignment::Bottom,
|
||||
px(1000.),
|
||||
move |ix, window, cx| {
|
||||
if let Some(model) = model.upgrade() {
|
||||
model.update(cx, |this: &mut Self, cx| {
|
||||
if let Some(entity) = entity.upgrade() {
|
||||
entity.update(cx, |this: &mut Self, cx| {
|
||||
this.render_message(ix, window, cx).into_any_element()
|
||||
})
|
||||
} else {
|
||||
|
||||
@@ -239,14 +239,14 @@ impl CollabPanel {
|
||||
)
|
||||
.detach();
|
||||
|
||||
let model = cx.entity().downgrade();
|
||||
let entity = cx.entity().downgrade();
|
||||
let list_state = ListState::new(
|
||||
0,
|
||||
gpui::ListAlignment::Top,
|
||||
px(1000.),
|
||||
move |ix, window, cx| {
|
||||
if let Some(model) = model.upgrade() {
|
||||
model.update(cx, |this, cx| this.render_list_entry(ix, window, cx))
|
||||
if let Some(entity) = entity.upgrade() {
|
||||
entity.update(cx, |this, cx| this.render_list_entry(ix, window, cx))
|
||||
} else {
|
||||
div().into_any()
|
||||
}
|
||||
|
||||
@@ -110,13 +110,13 @@ impl NotificationPanel {
|
||||
})
|
||||
.detach();
|
||||
|
||||
let model = cx.entity().downgrade();
|
||||
let entity = cx.entity().downgrade();
|
||||
let notification_list =
|
||||
ListState::new(0, ListAlignment::Top, px(1000.), move |ix, window, cx| {
|
||||
model
|
||||
entity
|
||||
.upgrade()
|
||||
.and_then(|model| {
|
||||
model.update(cx, |this, cx| this.render_notification(ix, window, cx))
|
||||
.and_then(|entity| {
|
||||
entity.update(cx, |this, cx| this.render_notification(ix, window, cx))
|
||||
})
|
||||
.unwrap_or_else(|| div().into_any())
|
||||
});
|
||||
@@ -323,9 +323,9 @@ impl NotificationPanel {
|
||||
.justify_end()
|
||||
.child(Button::new("decline", "Decline").on_click({
|
||||
let notification = notification.clone();
|
||||
let model = cx.entity().clone();
|
||||
let entity = cx.entity().clone();
|
||||
move |_, _, cx| {
|
||||
model.update(cx, |this, cx| {
|
||||
entity.update(cx, |this, cx| {
|
||||
this.respond_to_notification(
|
||||
notification.clone(),
|
||||
false,
|
||||
@@ -336,9 +336,9 @@ impl NotificationPanel {
|
||||
}))
|
||||
.child(Button::new("accept", "Accept").on_click({
|
||||
let notification = notification.clone();
|
||||
let model = cx.entity().clone();
|
||||
let entity = cx.entity().clone();
|
||||
move |_, _, cx| {
|
||||
model.update(cx, |this, cx| {
|
||||
entity.update(cx, |this, cx| {
|
||||
this.respond_to_notification(
|
||||
notification.clone(),
|
||||
true,
|
||||
|
||||
@@ -59,20 +59,20 @@ workspace.workspace = true
|
||||
async-std = { version = "1.12.0", features = ["unstable"] }
|
||||
|
||||
[dev-dependencies]
|
||||
indoc.workspace = true
|
||||
serde_json.workspace = true
|
||||
clock = { workspace = true, features = ["test-support"] }
|
||||
client = { workspace = true, features = ["test-support"] }
|
||||
clock = { workspace = true, features = ["test-support"] }
|
||||
collections = { workspace = true, features = ["test-support"] }
|
||||
editor = { workspace = true, features = ["test-support"] }
|
||||
fs = { workspace = true, features = ["test-support"] }
|
||||
gpui = { workspace = true, features = ["test-support"] }
|
||||
http_client = { workspace = true, features = ["test-support"] }
|
||||
indoc.workspace = true
|
||||
language = { workspace = true, features = ["test-support"] }
|
||||
lsp = { workspace = true, features = ["test-support"] }
|
||||
node_runtime = { workspace = true, features = ["test-support"] }
|
||||
project = { workspace = true, features = ["test-support"] }
|
||||
rpc = { workspace = true, features = ["test-support"] }
|
||||
serde_json.workspace = true
|
||||
settings = { workspace = true, features = ["test-support"] }
|
||||
theme = { workspace = true, features = ["test-support"] }
|
||||
util = { workspace = true, features = ["test-support"] }
|
||||
|
||||
@@ -1061,6 +1061,7 @@ async fn get_copilot_lsp(http: Arc<dyn HttpClient>) -> anyhow::Result<PathBuf> {
|
||||
mod tests {
|
||||
use super::*;
|
||||
use gpui::TestAppContext;
|
||||
use util::path;
|
||||
|
||||
#[gpui::test(iterations = 10)]
|
||||
async fn test_buffer_management(cx: &mut TestAppContext) {
|
||||
@@ -1123,7 +1124,7 @@ mod tests {
|
||||
buffer_1.update(cx, |buffer, cx| {
|
||||
buffer.file_updated(
|
||||
Arc::new(File {
|
||||
abs_path: "/root/child/buffer-1".into(),
|
||||
abs_path: path!("/root/child/buffer-1").into(),
|
||||
path: Path::new("child/buffer-1").into(),
|
||||
}),
|
||||
cx,
|
||||
@@ -1136,7 +1137,7 @@ mod tests {
|
||||
text_document: lsp::TextDocumentIdentifier::new(buffer_1_uri),
|
||||
}
|
||||
);
|
||||
let buffer_1_uri = lsp::Url::from_file_path("/root/child/buffer-1").unwrap();
|
||||
let buffer_1_uri = lsp::Url::from_file_path(path!("/root/child/buffer-1")).unwrap();
|
||||
assert_eq!(
|
||||
lsp.receive_notification::<lsp::notification::DidOpenTextDocument>()
|
||||
.await,
|
||||
|
||||
@@ -36,8 +36,8 @@ pub enum Model {
|
||||
Gpt3_5Turbo,
|
||||
#[serde(alias = "o1", rename = "o1")]
|
||||
O1,
|
||||
#[serde(alias = "o1-mini", rename = "o1-mini")]
|
||||
O1Mini,
|
||||
#[serde(alias = "o1-mini", rename = "o3-mini")]
|
||||
O3Mini,
|
||||
#[serde(alias = "claude-3-5-sonnet", rename = "claude-3.5-sonnet")]
|
||||
Claude3_5Sonnet,
|
||||
}
|
||||
@@ -46,7 +46,7 @@ impl Model {
|
||||
pub fn uses_streaming(&self) -> bool {
|
||||
match self {
|
||||
Self::Gpt4o | Self::Gpt4 | Self::Gpt3_5Turbo | Self::Claude3_5Sonnet => true,
|
||||
Self::O1Mini | Self::O1 => false,
|
||||
Self::O3Mini | Self::O1 => false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,7 +56,7 @@ impl Model {
|
||||
"gpt-4" => Ok(Self::Gpt4),
|
||||
"gpt-3.5-turbo" => Ok(Self::Gpt3_5Turbo),
|
||||
"o1" => Ok(Self::O1),
|
||||
"o1-mini" => Ok(Self::O1Mini),
|
||||
"o3-mini" => Ok(Self::O3Mini),
|
||||
"claude-3-5-sonnet" => Ok(Self::Claude3_5Sonnet),
|
||||
_ => Err(anyhow!("Invalid model id: {}", id)),
|
||||
}
|
||||
@@ -67,7 +67,7 @@ impl Model {
|
||||
Self::Gpt3_5Turbo => "gpt-3.5-turbo",
|
||||
Self::Gpt4 => "gpt-4",
|
||||
Self::Gpt4o => "gpt-4o",
|
||||
Self::O1Mini => "o1-mini",
|
||||
Self::O3Mini => "o3-mini",
|
||||
Self::O1 => "o1",
|
||||
Self::Claude3_5Sonnet => "claude-3-5-sonnet",
|
||||
}
|
||||
@@ -78,7 +78,7 @@ impl Model {
|
||||
Self::Gpt3_5Turbo => "GPT-3.5",
|
||||
Self::Gpt4 => "GPT-4",
|
||||
Self::Gpt4o => "GPT-4o",
|
||||
Self::O1Mini => "o1-mini",
|
||||
Self::O3Mini => "o3-mini",
|
||||
Self::O1 => "o1",
|
||||
Self::Claude3_5Sonnet => "Claude 3.5 Sonnet",
|
||||
}
|
||||
@@ -89,7 +89,7 @@ impl Model {
|
||||
Self::Gpt4o => 64000,
|
||||
Self::Gpt4 => 32768,
|
||||
Self::Gpt3_5Turbo => 12288,
|
||||
Self::O1Mini => 20000,
|
||||
Self::O3Mini => 20000,
|
||||
Self::O1 => 20000,
|
||||
Self::Claude3_5Sonnet => 200_000,
|
||||
}
|
||||
|
||||
@@ -2,10 +2,8 @@ use crate::{Completion, Copilot};
|
||||
use anyhow::Result;
|
||||
use gpui::{App, Context, Entity, EntityId, Task};
|
||||
use inline_completion::{Direction, InlineCompletion, InlineCompletionProvider};
|
||||
use language::{
|
||||
language_settings::{all_language_settings, AllLanguageSettings},
|
||||
Buffer, OffsetRangeExt, ToOffset,
|
||||
};
|
||||
use language::{language_settings::AllLanguageSettings, Buffer, OffsetRangeExt, ToOffset};
|
||||
use project::Project;
|
||||
use settings::Settings;
|
||||
use std::{path::Path, time::Duration};
|
||||
|
||||
@@ -73,23 +71,16 @@ impl InlineCompletionProvider for CopilotCompletionProvider {
|
||||
|
||||
fn is_enabled(
|
||||
&self,
|
||||
buffer: &Entity<Buffer>,
|
||||
cursor_position: language::Anchor,
|
||||
_buffer: &Entity<Buffer>,
|
||||
_cursor_position: language::Anchor,
|
||||
cx: &App,
|
||||
) -> bool {
|
||||
if !self.copilot.read(cx).status().is_authorized() {
|
||||
return false;
|
||||
}
|
||||
|
||||
let buffer = buffer.read(cx);
|
||||
let file = buffer.file();
|
||||
let language = buffer.language_at(cursor_position);
|
||||
let settings = all_language_settings(file, cx);
|
||||
settings.inline_completions_enabled(language.as_ref(), file.map(|f| f.path().as_ref()), cx)
|
||||
self.copilot.read(cx).status().is_authorized()
|
||||
}
|
||||
|
||||
fn refresh(
|
||||
&mut self,
|
||||
_project: Option<Entity<Project>>,
|
||||
buffer: Entity<Buffer>,
|
||||
cursor_position: language::Anchor,
|
||||
debounce: bool,
|
||||
@@ -205,7 +196,7 @@ impl InlineCompletionProvider for CopilotCompletionProvider {
|
||||
fn discard(&mut self, cx: &mut Context<Self>) {
|
||||
let settings = AllLanguageSettings::get_global(cx);
|
||||
|
||||
let copilot_enabled = settings.inline_completions_enabled(None, None, cx);
|
||||
let copilot_enabled = settings.show_inline_completions(None, cx);
|
||||
|
||||
if !copilot_enabled {
|
||||
return;
|
||||
@@ -290,7 +281,10 @@ mod tests {
|
||||
use serde_json::json;
|
||||
use settings::SettingsStore;
|
||||
use std::future::Future;
|
||||
use util::test::{marked_text_ranges_by, TextRangeMarker};
|
||||
use util::{
|
||||
path,
|
||||
test::{marked_text_ranges_by, TextRangeMarker},
|
||||
};
|
||||
|
||||
#[gpui::test(iterations = 10)]
|
||||
async fn test_copilot(executor: BackgroundExecutor, cx: &mut TestAppContext) {
|
||||
@@ -341,7 +335,6 @@ mod tests {
|
||||
executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
assert!(editor.context_menu_visible());
|
||||
assert!(!editor.context_menu_contains_inline_completion());
|
||||
assert!(!editor.has_active_inline_completion());
|
||||
// Since we have both, the copilot suggestion is not shown inline
|
||||
assert_eq!(editor.text(cx), "one.\ntwo\nthree\n");
|
||||
@@ -394,12 +387,11 @@ mod tests {
|
||||
assert_eq!(editor.text(cx), "one.\ntwo\nthree\n");
|
||||
});
|
||||
|
||||
// Ensure existing inline completion is interpolated when inserting again.
|
||||
// Ensure existing edit prediction is interpolated when inserting again.
|
||||
cx.simulate_keystroke("c");
|
||||
executor.run_until_parked();
|
||||
cx.update_editor(|editor, _, cx| {
|
||||
assert!(!editor.context_menu_visible());
|
||||
assert!(!editor.context_menu_contains_inline_completion());
|
||||
assert!(editor.has_active_inline_completion());
|
||||
assert_eq!(editor.display_text(cx), "one.copilot1\ntwo\nthree\n");
|
||||
assert_eq!(editor.text(cx), "one.c\ntwo\nthree\n");
|
||||
@@ -419,7 +411,6 @@ mod tests {
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
assert!(!editor.context_menu_visible());
|
||||
assert!(editor.has_active_inline_completion());
|
||||
assert!(!editor.context_menu_contains_inline_completion());
|
||||
assert_eq!(editor.display_text(cx), "one.copilot2\ntwo\nthree\n");
|
||||
assert_eq!(editor.text(cx), "one.c\ntwo\nthree\n");
|
||||
|
||||
@@ -934,7 +925,6 @@ mod tests {
|
||||
executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
|
||||
cx.update_editor(|editor, _, cx| {
|
||||
assert!(editor.context_menu_visible());
|
||||
assert!(!editor.context_menu_contains_inline_completion());
|
||||
assert!(!editor.has_active_inline_completion(),);
|
||||
assert_eq!(editor.text(cx), "one\ntwo.\nthree\n");
|
||||
});
|
||||
@@ -953,24 +943,24 @@ mod tests {
|
||||
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
fs.insert_tree(
|
||||
"/test",
|
||||
path!("/test"),
|
||||
json!({
|
||||
".env": "SECRET=something\n",
|
||||
"README.md": "hello\nworld\nhow\nare\nyou\ntoday"
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
let project = Project::test(fs, ["/test".as_ref()], cx).await;
|
||||
let project = Project::test(fs, [path!("/test").as_ref()], cx).await;
|
||||
|
||||
let private_buffer = project
|
||||
.update(cx, |project, cx| {
|
||||
project.open_local_buffer("/test/.env", cx)
|
||||
project.open_local_buffer(path!("/test/.env"), cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
let public_buffer = project
|
||||
.update(cx, |project, cx| {
|
||||
project.open_local_buffer("/test/README.md", cx)
|
||||
project.open_local_buffer(path!("/test/README.md"), cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@@ -750,7 +750,7 @@ impl Item for ProjectDiagnosticsEditor {
|
||||
}
|
||||
|
||||
fn telemetry_event_text(&self) -> Option<&'static str> {
|
||||
Some("project diagnostics")
|
||||
Some("Project Diagnostics Opened")
|
||||
}
|
||||
|
||||
fn for_each_project_item(
|
||||
@@ -933,7 +933,7 @@ fn diagnostic_header_renderer(diagnostic: Diagnostic) -> RenderBlock {
|
||||
.when_some(diagnostic.code.as_ref(), |stack, code| {
|
||||
stack.child(
|
||||
div()
|
||||
.child(SharedString::from(format!("({code})")))
|
||||
.child(SharedString::from(format!("({code:?})")))
|
||||
.text_color(color.text_muted),
|
||||
)
|
||||
}),
|
||||
|
||||
@@ -150,7 +150,7 @@ async fn test_diagnostics(cx: &mut TestAppContext) {
|
||||
});
|
||||
|
||||
// Open the project diagnostics view while there are already diagnostics.
|
||||
let diagnostics = window.build_model(cx, |window, cx| {
|
||||
let diagnostics = window.build_entity(cx, |window, cx| {
|
||||
ProjectDiagnosticsEditor::new_with_context(
|
||||
1,
|
||||
true,
|
||||
@@ -485,7 +485,7 @@ async fn test_diagnostics_multiple_servers(cx: &mut TestAppContext) {
|
||||
let cx = &mut VisualTestContext::from_window(*window, cx);
|
||||
let workspace = window.root(cx).unwrap();
|
||||
|
||||
let diagnostics = window.build_model(cx, |window, cx| {
|
||||
let diagnostics = window.build_entity(cx, |window, cx| {
|
||||
ProjectDiagnosticsEditor::new_with_context(
|
||||
1,
|
||||
true,
|
||||
@@ -763,7 +763,7 @@ async fn test_random_diagnostics(cx: &mut TestAppContext, mut rng: StdRng) {
|
||||
let cx = &mut VisualTestContext::from_window(*window, cx);
|
||||
let workspace = window.root(cx).unwrap();
|
||||
|
||||
let mutated_diagnostics = window.build_model(cx, |window, cx| {
|
||||
let mutated_diagnostics = window.build_entity(cx, |window, cx| {
|
||||
ProjectDiagnosticsEditor::new_with_context(
|
||||
1,
|
||||
true,
|
||||
@@ -870,7 +870,7 @@ async fn test_random_diagnostics(cx: &mut TestAppContext, mut rng: StdRng) {
|
||||
cx.run_until_parked();
|
||||
|
||||
log::info!("constructing reference diagnostics view");
|
||||
let reference_diagnostics = window.build_model(cx, |window, cx| {
|
||||
let reference_diagnostics = window.build_entity(cx, |window, cx| {
|
||||
ProjectDiagnosticsEditor::new_with_context(
|
||||
1,
|
||||
true,
|
||||
|
||||
@@ -86,9 +86,6 @@ impl Render for DiagnosticIndicator {
|
||||
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.pl_1()
|
||||
.border_l_1()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.child(
|
||||
ButtonLike::new("diagnostic-indicator")
|
||||
.child(diagnostic_indicator)
|
||||
@@ -160,7 +157,7 @@ impl DiagnosticIndicator {
|
||||
(buffer, cursor_position)
|
||||
});
|
||||
let new_diagnostic = buffer
|
||||
.diagnostics_in_range::<_, usize>(cursor_position..cursor_position)
|
||||
.diagnostics_in_range::<usize>(cursor_position..cursor_position)
|
||||
.filter(|entry| !entry.range.is_empty())
|
||||
.min_by_key(|entry| (entry.diagnostic.severity, entry.range.len()))
|
||||
.map(|entry| entry.diagnostic);
|
||||
|
||||
32
crates/diff/Cargo.toml
Normal file
32
crates/diff/Cargo.toml
Normal file
@@ -0,0 +1,32 @@
|
||||
[package]
|
||||
name = "diff"
|
||||
version = "0.1.0"
|
||||
edition.workspace = true
|
||||
publish.workspace = true
|
||||
license = "GPL-3.0-or-later"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[lib]
|
||||
path = "src/diff.rs"
|
||||
|
||||
[dependencies]
|
||||
futures.workspace = true
|
||||
git2.workspace = true
|
||||
gpui.workspace = true
|
||||
language.workspace = true
|
||||
log.workspace = true
|
||||
rope.workspace = true
|
||||
sum_tree.workspace = true
|
||||
text.workspace = true
|
||||
util.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
unindent.workspace = true
|
||||
serde_json.workspace = true
|
||||
pretty_assertions.workspace = true
|
||||
text = {workspace = true, features = ["test-support"]}
|
||||
|
||||
[features]
|
||||
test-support = []
|
||||
@@ -1,10 +1,12 @@
|
||||
use futures::{channel::oneshot, future::OptionFuture};
|
||||
use git2::{DiffLineType as GitDiffLineType, DiffOptions as GitOptions, Patch as GitPatch};
|
||||
use gpui::{App, Context, Entity, EventEmitter};
|
||||
use language::{Language, LanguageRegistry};
|
||||
use rope::Rope;
|
||||
use std::{cmp, iter, ops::Range};
|
||||
use std::{cmp, future::Future, iter, ops::Range, sync::Arc};
|
||||
use sum_tree::SumTree;
|
||||
use text::{Anchor, BufferSnapshot, OffsetRangeExt, Point};
|
||||
|
||||
pub use git2 as libgit;
|
||||
use libgit::{DiffLineType as GitDiffLineType, DiffOptions as GitOptions, Patch as GitPatch};
|
||||
use text::{Anchor, BufferId, OffsetRangeExt, Point};
|
||||
use util::ResultExt;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub enum DiffHunkStatus {
|
||||
@@ -62,43 +64,146 @@ impl sum_tree::Summary for DiffHunkSummary {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct BufferDiff {
|
||||
tree: SumTree<InternalDiffHunk>,
|
||||
#[derive(Clone)]
|
||||
pub struct BufferDiffSnapshot {
|
||||
hunks: SumTree<InternalDiffHunk>,
|
||||
pub base_text: Option<language::BufferSnapshot>,
|
||||
}
|
||||
|
||||
impl BufferDiff {
|
||||
pub fn new(buffer: &BufferSnapshot) -> BufferDiff {
|
||||
BufferDiff {
|
||||
tree: SumTree::new(buffer),
|
||||
impl std::fmt::Debug for BufferDiffSnapshot {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("BufferDiffSnapshot")
|
||||
.field("hunks", &self.hunks)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl BufferDiffSnapshot {
|
||||
pub fn new(buffer: &text::BufferSnapshot) -> BufferDiffSnapshot {
|
||||
BufferDiffSnapshot {
|
||||
hunks: SumTree::new(buffer),
|
||||
base_text: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn build(diff_base: &str, buffer: &text::BufferSnapshot) -> Self {
|
||||
let mut tree = SumTree::new(buffer);
|
||||
pub fn new_with_single_insertion(cx: &mut App) -> Self {
|
||||
let base_text = language::Buffer::build_empty_snapshot(cx);
|
||||
Self {
|
||||
hunks: SumTree::from_item(
|
||||
InternalDiffHunk {
|
||||
buffer_range: Anchor::MIN..Anchor::MAX,
|
||||
diff_base_byte_range: 0..0,
|
||||
},
|
||||
&base_text,
|
||||
),
|
||||
base_text: Some(base_text),
|
||||
}
|
||||
}
|
||||
|
||||
let buffer_text = buffer.as_rope().to_string();
|
||||
let patch = Self::diff(diff_base, &buffer_text);
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub fn build_sync(
|
||||
buffer: text::BufferSnapshot,
|
||||
diff_base: String,
|
||||
cx: &mut gpui::TestAppContext,
|
||||
) -> Self {
|
||||
let snapshot =
|
||||
cx.update(|cx| Self::build(buffer, Some(Arc::new(diff_base)), None, None, cx));
|
||||
cx.executor().block(snapshot)
|
||||
}
|
||||
|
||||
if let Some(patch) = patch {
|
||||
let mut divergence = 0;
|
||||
for hunk_index in 0..patch.num_hunks() {
|
||||
let hunk = Self::process_patch_hunk(&patch, hunk_index, buffer, &mut divergence);
|
||||
tree.push(hunk, buffer);
|
||||
pub fn build(
|
||||
buffer: text::BufferSnapshot,
|
||||
diff_base: Option<Arc<String>>,
|
||||
language: Option<Arc<Language>>,
|
||||
language_registry: Option<Arc<LanguageRegistry>>,
|
||||
cx: &mut App,
|
||||
) -> impl Future<Output = Self> {
|
||||
let base_text_snapshot = diff_base.as_ref().map(|base_text| {
|
||||
language::Buffer::build_snapshot(
|
||||
Rope::from(base_text.as_str()),
|
||||
language.clone(),
|
||||
language_registry.clone(),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
let base_text_snapshot = cx
|
||||
.background_executor()
|
||||
.spawn(OptionFuture::from(base_text_snapshot));
|
||||
|
||||
let hunks = cx.background_executor().spawn({
|
||||
let buffer = buffer.clone();
|
||||
async move { Self::recalculate_hunks(diff_base, buffer) }
|
||||
});
|
||||
|
||||
async move {
|
||||
let (base_text, hunks) = futures::join!(base_text_snapshot, hunks);
|
||||
Self { base_text, hunks }
|
||||
}
|
||||
}
|
||||
|
||||
pub fn build_with_base_buffer(
|
||||
buffer: text::BufferSnapshot,
|
||||
diff_base: Option<Arc<String>>,
|
||||
diff_base_buffer: Option<language::BufferSnapshot>,
|
||||
cx: &App,
|
||||
) -> impl Future<Output = Self> {
|
||||
cx.background_executor().spawn({
|
||||
let buffer = buffer.clone();
|
||||
async move {
|
||||
let hunks = Self::recalculate_hunks(diff_base, buffer);
|
||||
Self {
|
||||
hunks,
|
||||
base_text: diff_base_buffer,
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn recalculate_hunks(
|
||||
diff_base: Option<Arc<String>>,
|
||||
buffer: text::BufferSnapshot,
|
||||
) -> SumTree<InternalDiffHunk> {
|
||||
let mut tree = SumTree::new(&buffer);
|
||||
|
||||
if let Some(diff_base) = diff_base {
|
||||
let buffer_text = buffer.as_rope().to_string();
|
||||
let patch = Self::diff(&diff_base, &buffer_text);
|
||||
|
||||
// A common case in Zed is that the empty buffer is represented as just a newline,
|
||||
// but if we just compute a naive diff you get a "preserved" line in the middle,
|
||||
// which is a bit odd.
|
||||
if buffer_text == "\n" && diff_base.ends_with("\n") && diff_base.len() > 1 {
|
||||
tree.push(
|
||||
InternalDiffHunk {
|
||||
buffer_range: buffer.anchor_before(0)..buffer.anchor_before(0),
|
||||
diff_base_byte_range: 0..diff_base.len() - 1,
|
||||
},
|
||||
&buffer,
|
||||
);
|
||||
return tree;
|
||||
}
|
||||
|
||||
if let Some(patch) = patch {
|
||||
let mut divergence = 0;
|
||||
for hunk_index in 0..patch.num_hunks() {
|
||||
let hunk =
|
||||
Self::process_patch_hunk(&patch, hunk_index, &buffer, &mut divergence);
|
||||
tree.push(hunk, &buffer);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Self { tree }
|
||||
tree
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.tree.is_empty()
|
||||
self.hunks.is_empty()
|
||||
}
|
||||
|
||||
pub fn hunks_in_row_range<'a>(
|
||||
&'a self,
|
||||
range: Range<u32>,
|
||||
buffer: &'a BufferSnapshot,
|
||||
buffer: &'a text::BufferSnapshot,
|
||||
) -> impl 'a + Iterator<Item = DiffHunk> {
|
||||
let start = buffer.anchor_before(Point::new(range.start, 0));
|
||||
let end = buffer.anchor_after(Point::new(range.end, 0));
|
||||
@@ -109,13 +214,16 @@ impl BufferDiff {
|
||||
pub fn hunks_intersecting_range<'a>(
|
||||
&'a self,
|
||||
range: Range<Anchor>,
|
||||
buffer: &'a BufferSnapshot,
|
||||
buffer: &'a text::BufferSnapshot,
|
||||
) -> impl 'a + Iterator<Item = DiffHunk> {
|
||||
let range = range.to_offset(buffer);
|
||||
|
||||
let mut cursor = self
|
||||
.tree
|
||||
.hunks
|
||||
.filter::<_, DiffHunkSummary>(buffer, move |summary| {
|
||||
let before_start = summary.buffer_range.end.cmp(&range.start, buffer).is_lt();
|
||||
let after_end = summary.buffer_range.start.cmp(&range.end, buffer).is_gt();
|
||||
let summary_range = summary.buffer_range.to_offset(buffer);
|
||||
let before_start = summary_range.end < range.start;
|
||||
let after_end = summary_range.start > range.end;
|
||||
!before_start && !after_end
|
||||
});
|
||||
|
||||
@@ -137,31 +245,35 @@ impl BufferDiff {
|
||||
});
|
||||
|
||||
let mut summaries = buffer.summaries_for_anchors_with_payload::<Point, _, _>(anchor_iter);
|
||||
iter::from_fn(move || {
|
||||
iter::from_fn(move || loop {
|
||||
let (start_point, (start_anchor, start_base)) = summaries.next()?;
|
||||
let (mut end_point, (mut end_anchor, end_base)) = summaries.next()?;
|
||||
|
||||
if !start_anchor.is_valid(buffer) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if end_point.column > 0 {
|
||||
end_point.row += 1;
|
||||
end_point.column = 0;
|
||||
end_anchor = buffer.anchor_before(end_point);
|
||||
}
|
||||
|
||||
Some(DiffHunk {
|
||||
return Some(DiffHunk {
|
||||
row_range: start_point.row..end_point.row,
|
||||
diff_base_byte_range: start_base..end_base,
|
||||
buffer_range: start_anchor..end_anchor,
|
||||
})
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
pub fn hunks_intersecting_range_rev<'a>(
|
||||
&'a self,
|
||||
range: Range<Anchor>,
|
||||
buffer: &'a BufferSnapshot,
|
||||
buffer: &'a text::BufferSnapshot,
|
||||
) -> impl 'a + Iterator<Item = DiffHunk> {
|
||||
let mut cursor = self
|
||||
.tree
|
||||
.hunks
|
||||
.filter::<_, DiffHunkSummary>(buffer, move |summary| {
|
||||
let before_start = summary.buffer_range.end.cmp(&range.start, buffer).is_lt();
|
||||
let after_end = summary.buffer_range.start.cmp(&range.end, buffer).is_gt();
|
||||
@@ -187,9 +299,13 @@ impl BufferDiff {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn compare(&self, old: &Self, new_snapshot: &BufferSnapshot) -> Option<Range<Anchor>> {
|
||||
let mut new_cursor = self.tree.cursor::<()>(new_snapshot);
|
||||
let mut old_cursor = old.tree.cursor::<()>(new_snapshot);
|
||||
pub fn compare(
|
||||
&self,
|
||||
old: &Self,
|
||||
new_snapshot: &text::BufferSnapshot,
|
||||
) -> Option<Range<Anchor>> {
|
||||
let mut new_cursor = self.hunks.cursor::<()>(new_snapshot);
|
||||
let mut old_cursor = old.hunks.cursor::<()>(new_snapshot);
|
||||
old_cursor.next(new_snapshot);
|
||||
new_cursor.next(new_snapshot);
|
||||
let mut start = None;
|
||||
@@ -252,15 +368,11 @@ impl BufferDiff {
|
||||
|
||||
#[cfg(test)]
|
||||
fn clear(&mut self, buffer: &text::BufferSnapshot) {
|
||||
self.tree = SumTree::new(buffer);
|
||||
}
|
||||
|
||||
pub fn update(&mut self, diff_base: &Rope, buffer: &text::BufferSnapshot) {
|
||||
*self = Self::build(&diff_base.to_string(), buffer);
|
||||
self.hunks = SumTree::new(buffer);
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn hunks<'a>(&'a self, text: &'a BufferSnapshot) -> impl 'a + Iterator<Item = DiffHunk> {
|
||||
fn hunks<'a>(&'a self, text: &'a text::BufferSnapshot) -> impl 'a + Iterator<Item = DiffHunk> {
|
||||
let start = text.anchor_before(Point::new(0, 0));
|
||||
let end = text.anchor_after(Point::new(u32::MAX, u32::MAX));
|
||||
self.hunks_intersecting_range(start..end, text)
|
||||
@@ -355,12 +467,171 @@ impl BufferDiff {
|
||||
}
|
||||
}
|
||||
|
||||
pub struct BufferDiff {
|
||||
pub buffer_id: BufferId,
|
||||
pub snapshot: BufferDiffSnapshot,
|
||||
pub unstaged_diff: Option<Entity<BufferDiff>>,
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for BufferDiff {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("BufferChangeSet")
|
||||
.field("buffer_id", &self.buffer_id)
|
||||
.field("snapshot", &self.snapshot)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
pub enum BufferDiffEvent {
|
||||
DiffChanged { changed_range: Range<text::Anchor> },
|
||||
LanguageChanged,
|
||||
}
|
||||
|
||||
impl EventEmitter<BufferDiffEvent> for BufferDiff {}
|
||||
|
||||
impl BufferDiff {
|
||||
pub fn set_state(
|
||||
&mut self,
|
||||
snapshot: BufferDiffSnapshot,
|
||||
buffer: &text::BufferSnapshot,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
if let Some(base_text) = snapshot.base_text.as_ref() {
|
||||
let changed_range = if Some(base_text.remote_id())
|
||||
!= self
|
||||
.snapshot
|
||||
.base_text
|
||||
.as_ref()
|
||||
.map(|buffer| buffer.remote_id())
|
||||
{
|
||||
Some(text::Anchor::MIN..text::Anchor::MAX)
|
||||
} else {
|
||||
snapshot.compare(&self.snapshot, buffer)
|
||||
};
|
||||
if let Some(changed_range) = changed_range {
|
||||
cx.emit(BufferDiffEvent::DiffChanged { changed_range });
|
||||
}
|
||||
}
|
||||
self.snapshot = snapshot;
|
||||
}
|
||||
|
||||
pub fn diff_hunks_intersecting_range<'a>(
|
||||
&'a self,
|
||||
range: Range<text::Anchor>,
|
||||
buffer_snapshot: &'a text::BufferSnapshot,
|
||||
) -> impl 'a + Iterator<Item = DiffHunk> {
|
||||
self.snapshot
|
||||
.hunks_intersecting_range(range, buffer_snapshot)
|
||||
}
|
||||
|
||||
pub fn diff_hunks_intersecting_range_rev<'a>(
|
||||
&'a self,
|
||||
range: Range<text::Anchor>,
|
||||
buffer_snapshot: &'a text::BufferSnapshot,
|
||||
) -> impl 'a + Iterator<Item = DiffHunk> {
|
||||
self.snapshot
|
||||
.hunks_intersecting_range_rev(range, buffer_snapshot)
|
||||
}
|
||||
|
||||
/// Used in cases where the change set isn't derived from git.
|
||||
pub fn set_base_text(
|
||||
&mut self,
|
||||
base_buffer: Entity<language::Buffer>,
|
||||
buffer: text::BufferSnapshot,
|
||||
cx: &mut Context<Self>,
|
||||
) -> oneshot::Receiver<()> {
|
||||
let (tx, rx) = oneshot::channel();
|
||||
let this = cx.weak_entity();
|
||||
let base_buffer = base_buffer.read(cx);
|
||||
let language_registry = base_buffer.language_registry();
|
||||
let base_buffer = base_buffer.snapshot();
|
||||
let base_text = Arc::new(base_buffer.text());
|
||||
|
||||
let snapshot = BufferDiffSnapshot::build(
|
||||
buffer.clone(),
|
||||
Some(base_text),
|
||||
base_buffer.language().cloned(),
|
||||
language_registry,
|
||||
cx,
|
||||
);
|
||||
let complete_on_drop = util::defer(|| {
|
||||
tx.send(()).ok();
|
||||
});
|
||||
cx.spawn(|_, mut cx| async move {
|
||||
let snapshot = snapshot.await;
|
||||
let Some(this) = this.upgrade() else {
|
||||
return;
|
||||
};
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.set_state(snapshot, &buffer, cx);
|
||||
})
|
||||
.log_err();
|
||||
drop(complete_on_drop)
|
||||
})
|
||||
.detach();
|
||||
rx
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub fn base_text_string(&self) -> Option<String> {
|
||||
self.snapshot.base_text.as_ref().map(|buffer| buffer.text())
|
||||
}
|
||||
|
||||
pub fn new(buffer: &Entity<language::Buffer>, cx: &mut App) -> Self {
|
||||
BufferDiff {
|
||||
buffer_id: buffer.read(cx).remote_id(),
|
||||
snapshot: BufferDiffSnapshot::new(&buffer.read(cx)),
|
||||
unstaged_diff: None,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub fn new_with_base_text(
|
||||
base_text: &str,
|
||||
buffer: &Entity<language::Buffer>,
|
||||
cx: &mut App,
|
||||
) -> Self {
|
||||
let mut base_text = base_text.to_owned();
|
||||
text::LineEnding::normalize(&mut base_text);
|
||||
let snapshot = BufferDiffSnapshot::build(
|
||||
buffer.read(cx).text_snapshot(),
|
||||
Some(base_text.into()),
|
||||
None,
|
||||
None,
|
||||
cx,
|
||||
);
|
||||
let snapshot = cx.background_executor().block(snapshot);
|
||||
BufferDiff {
|
||||
buffer_id: buffer.read(cx).remote_id(),
|
||||
snapshot,
|
||||
unstaged_diff: None,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub fn recalculate_diff_sync(&mut self, buffer: text::BufferSnapshot, cx: &mut Context<Self>) {
|
||||
let base_text = self
|
||||
.snapshot
|
||||
.base_text
|
||||
.as_ref()
|
||||
.map(|base_text| base_text.text());
|
||||
let snapshot = BufferDiffSnapshot::build_with_base_buffer(
|
||||
buffer.clone(),
|
||||
base_text.clone().map(Arc::new),
|
||||
self.snapshot.base_text.clone(),
|
||||
cx,
|
||||
);
|
||||
let snapshot = cx.background_executor().block(snapshot);
|
||||
self.set_state(snapshot, &buffer, cx);
|
||||
}
|
||||
}
|
||||
|
||||
/// Range (crossing new lines), old, new
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
#[track_caller]
|
||||
pub fn assert_hunks<Iter>(
|
||||
diff_hunks: Iter,
|
||||
buffer: &BufferSnapshot,
|
||||
buffer: &text::BufferSnapshot,
|
||||
diff_base: &str,
|
||||
expected_hunks: &[(Range<u32>, &str, &str)],
|
||||
) where
|
||||
@@ -393,18 +664,18 @@ mod tests {
|
||||
use std::assert_eq;
|
||||
|
||||
use super::*;
|
||||
use gpui::TestAppContext;
|
||||
use text::{Buffer, BufferId};
|
||||
use unindent::Unindent as _;
|
||||
|
||||
#[test]
|
||||
fn test_buffer_diff_simple() {
|
||||
#[gpui::test]
|
||||
async fn test_buffer_diff_simple(cx: &mut gpui::TestAppContext) {
|
||||
let diff_base = "
|
||||
one
|
||||
two
|
||||
three
|
||||
"
|
||||
.unindent();
|
||||
let diff_base_rope = Rope::from(diff_base.clone());
|
||||
|
||||
let buffer_text = "
|
||||
one
|
||||
@@ -414,8 +685,7 @@ mod tests {
|
||||
.unindent();
|
||||
|
||||
let mut buffer = Buffer::new(0, BufferId::new(1).unwrap(), buffer_text);
|
||||
let mut diff = BufferDiff::new(&buffer);
|
||||
diff.update(&diff_base_rope, &buffer);
|
||||
let mut diff = BufferDiffSnapshot::build_sync(buffer.clone(), diff_base.clone(), cx);
|
||||
assert_hunks(
|
||||
diff.hunks(&buffer),
|
||||
&buffer,
|
||||
@@ -424,7 +694,7 @@ mod tests {
|
||||
);
|
||||
|
||||
buffer.edit([(0..0, "point five\n")]);
|
||||
diff.update(&diff_base_rope, &buffer);
|
||||
diff = BufferDiffSnapshot::build_sync(buffer.clone(), diff_base.clone(), cx);
|
||||
assert_hunks(
|
||||
diff.hunks(&buffer),
|
||||
&buffer,
|
||||
@@ -436,9 +706,10 @@ mod tests {
|
||||
assert_hunks(diff.hunks(&buffer), &buffer, &diff_base, &[]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_buffer_diff_range() {
|
||||
let diff_base = "
|
||||
#[gpui::test]
|
||||
async fn test_buffer_diff_range(cx: &mut TestAppContext) {
|
||||
let diff_base = Arc::new(
|
||||
"
|
||||
one
|
||||
two
|
||||
three
|
||||
@@ -450,8 +721,8 @@ mod tests {
|
||||
nine
|
||||
ten
|
||||
"
|
||||
.unindent();
|
||||
let diff_base_rope = Rope::from(diff_base.clone());
|
||||
.unindent(),
|
||||
);
|
||||
|
||||
let buffer_text = "
|
||||
A
|
||||
@@ -475,8 +746,17 @@ mod tests {
|
||||
.unindent();
|
||||
|
||||
let buffer = Buffer::new(0, BufferId::new(1).unwrap(), buffer_text);
|
||||
let mut diff = BufferDiff::new(&buffer);
|
||||
diff.update(&diff_base_rope, &buffer);
|
||||
let diff = cx
|
||||
.update(|cx| {
|
||||
BufferDiffSnapshot::build(
|
||||
buffer.snapshot(),
|
||||
Some(diff_base.clone()),
|
||||
None,
|
||||
None,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.await;
|
||||
assert_eq!(diff.hunks(&buffer).count(), 8);
|
||||
|
||||
assert_hunks(
|
||||
@@ -491,8 +771,8 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_buffer_diff_compare() {
|
||||
#[gpui::test]
|
||||
async fn test_buffer_diff_compare(cx: &mut TestAppContext) {
|
||||
let base_text = "
|
||||
zero
|
||||
one
|
||||
@@ -521,8 +801,8 @@ mod tests {
|
||||
|
||||
let mut buffer = Buffer::new(0, BufferId::new(1).unwrap(), buffer_text_1);
|
||||
|
||||
let empty_diff = BufferDiff::new(&buffer);
|
||||
let diff_1 = BufferDiff::build(&base_text, &buffer);
|
||||
let empty_diff = BufferDiffSnapshot::new(&buffer);
|
||||
let diff_1 = BufferDiffSnapshot::build_sync(buffer.clone(), base_text.clone(), cx);
|
||||
let range = diff_1.compare(&empty_diff, &buffer).unwrap();
|
||||
assert_eq!(range.to_point(&buffer), Point::new(0, 0)..Point::new(8, 0));
|
||||
|
||||
@@ -540,7 +820,7 @@ mod tests {
|
||||
"
|
||||
.unindent(),
|
||||
);
|
||||
let diff_2 = BufferDiff::build(&base_text, &buffer);
|
||||
let diff_2 = BufferDiffSnapshot::build_sync(buffer.clone(), base_text.clone(), cx);
|
||||
assert_eq!(None, diff_2.compare(&diff_1, &buffer));
|
||||
|
||||
// Edit turns a deletion hunk into a modification.
|
||||
@@ -557,7 +837,7 @@ mod tests {
|
||||
"
|
||||
.unindent(),
|
||||
);
|
||||
let diff_3 = BufferDiff::build(&base_text, &buffer);
|
||||
let diff_3 = BufferDiffSnapshot::build_sync(buffer.clone(), base_text.clone(), cx);
|
||||
let range = diff_3.compare(&diff_2, &buffer).unwrap();
|
||||
assert_eq!(range.to_point(&buffer), Point::new(1, 0)..Point::new(2, 0));
|
||||
|
||||
@@ -574,7 +854,7 @@ mod tests {
|
||||
"
|
||||
.unindent(),
|
||||
);
|
||||
let diff_4 = BufferDiff::build(&base_text, &buffer);
|
||||
let diff_4 = BufferDiffSnapshot::build_sync(buffer.clone(), base_text.clone(), cx);
|
||||
let range = diff_4.compare(&diff_3, &buffer).unwrap();
|
||||
assert_eq!(range.to_point(&buffer), Point::new(3, 4)..Point::new(4, 0));
|
||||
|
||||
@@ -592,7 +872,7 @@ mod tests {
|
||||
"
|
||||
.unindent(),
|
||||
);
|
||||
let diff_5 = BufferDiff::build(&base_text, &buffer);
|
||||
let diff_5 = BufferDiffSnapshot::build_sync(buffer.snapshot(), base_text.clone(), cx);
|
||||
let range = diff_5.compare(&diff_4, &buffer).unwrap();
|
||||
assert_eq!(range.to_point(&buffer), Point::new(3, 0)..Point::new(4, 0));
|
||||
|
||||
@@ -610,7 +890,7 @@ mod tests {
|
||||
"
|
||||
.unindent(),
|
||||
);
|
||||
let diff_6 = BufferDiff::build(&base_text, &buffer);
|
||||
let diff_6 = BufferDiffSnapshot::build_sync(buffer.snapshot(), base_text, cx);
|
||||
let range = diff_6.compare(&diff_5, &buffer).unwrap();
|
||||
assert_eq!(range.to_point(&buffer), Point::new(7, 0)..Point::new(8, 0));
|
||||
}
|
||||
@@ -38,8 +38,8 @@ clock.workspace = true
|
||||
collections.workspace = true
|
||||
convert_case.workspace = true
|
||||
db.workspace = true
|
||||
diff.workspace = true
|
||||
emojis.workspace = true
|
||||
feature_flags.workspace = true
|
||||
file_icons.workspace = true
|
||||
futures.workspace = true
|
||||
fuzzy.workspace = true
|
||||
@@ -88,7 +88,7 @@ url.workspace = true
|
||||
util.workspace = true
|
||||
uuid.workspace = true
|
||||
workspace.workspace = true
|
||||
zed_predict_tos.workspace = true
|
||||
zed_actions.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
ctor.workspace = true
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user