Merge branch 'read-file-tool' into edit-file-tool
This commit is contained in:
commit
a7bcc0f97a
55 changed files with 2142 additions and 426 deletions
5
.github/actionlint.yml
vendored
5
.github/actionlint.yml
vendored
|
@ -5,6 +5,11 @@ self-hosted-runner:
|
||||||
# GitHub-hosted Runners
|
# GitHub-hosted Runners
|
||||||
- github-8vcpu-ubuntu-2404
|
- github-8vcpu-ubuntu-2404
|
||||||
- github-16vcpu-ubuntu-2404
|
- github-16vcpu-ubuntu-2404
|
||||||
|
- github-32vcpu-ubuntu-2404
|
||||||
|
- github-8vcpu-ubuntu-2204
|
||||||
|
- github-16vcpu-ubuntu-2204
|
||||||
|
- github-32vcpu-ubuntu-2204
|
||||||
|
- github-16vcpu-ubuntu-2204-arm
|
||||||
- windows-2025-16
|
- windows-2025-16
|
||||||
- windows-2025-32
|
- windows-2025-32
|
||||||
- windows-2025-64
|
- windows-2025-64
|
||||||
|
|
2
.github/actions/build_docs/action.yml
vendored
2
.github/actions/build_docs/action.yml
vendored
|
@ -13,7 +13,7 @@ runs:
|
||||||
uses: swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2
|
uses: swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2
|
||||||
with:
|
with:
|
||||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||||
cache-provider: "buildjet"
|
# cache-provider: "buildjet"
|
||||||
|
|
||||||
- name: Install Linux dependencies
|
- name: Install Linux dependencies
|
||||||
shell: bash -euxo pipefail {0}
|
shell: bash -euxo pipefail {0}
|
||||||
|
|
2
.github/workflows/bump_patch_version.yml
vendored
2
.github/workflows/bump_patch_version.yml
vendored
|
@ -16,7 +16,7 @@ jobs:
|
||||||
bump_patch_version:
|
bump_patch_version:
|
||||||
if: github.repository_owner == 'zed-industries'
|
if: github.repository_owner == 'zed-industries'
|
||||||
runs-on:
|
runs-on:
|
||||||
- buildjet-16vcpu-ubuntu-2204
|
- github-16vcpu-ubuntu-2204
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||||
|
|
19
.github/workflows/ci.yml
vendored
19
.github/workflows/ci.yml
vendored
|
@ -137,7 +137,7 @@ jobs:
|
||||||
github.repository_owner == 'zed-industries' &&
|
github.repository_owner == 'zed-industries' &&
|
||||||
needs.job_spec.outputs.run_tests == 'true'
|
needs.job_spec.outputs.run_tests == 'true'
|
||||||
runs-on:
|
runs-on:
|
||||||
- buildjet-8vcpu-ubuntu-2204
|
- github-8vcpu-ubuntu-2204
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repo
|
- name: Checkout repo
|
||||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||||
|
@ -168,7 +168,7 @@ jobs:
|
||||||
needs: [job_spec]
|
needs: [job_spec]
|
||||||
if: github.repository_owner == 'zed-industries'
|
if: github.repository_owner == 'zed-industries'
|
||||||
runs-on:
|
runs-on:
|
||||||
- buildjet-8vcpu-ubuntu-2204
|
- github-8vcpu-ubuntu-2204
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repo
|
- name: Checkout repo
|
||||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||||
|
@ -221,7 +221,7 @@ jobs:
|
||||||
github.repository_owner == 'zed-industries' &&
|
github.repository_owner == 'zed-industries' &&
|
||||||
(needs.job_spec.outputs.run_tests == 'true' || needs.job_spec.outputs.run_docs == 'true')
|
(needs.job_spec.outputs.run_tests == 'true' || needs.job_spec.outputs.run_docs == 'true')
|
||||||
runs-on:
|
runs-on:
|
||||||
- buildjet-8vcpu-ubuntu-2204
|
- github-8vcpu-ubuntu-2204
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repo
|
- name: Checkout repo
|
||||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||||
|
@ -328,7 +328,7 @@ jobs:
|
||||||
github.repository_owner == 'zed-industries' &&
|
github.repository_owner == 'zed-industries' &&
|
||||||
needs.job_spec.outputs.run_tests == 'true'
|
needs.job_spec.outputs.run_tests == 'true'
|
||||||
runs-on:
|
runs-on:
|
||||||
- buildjet-16vcpu-ubuntu-2204
|
- github-16vcpu-ubuntu-2204
|
||||||
steps:
|
steps:
|
||||||
- name: Add Rust to the PATH
|
- name: Add Rust to the PATH
|
||||||
run: echo "$HOME/.cargo/bin" >> "$GITHUB_PATH"
|
run: echo "$HOME/.cargo/bin" >> "$GITHUB_PATH"
|
||||||
|
@ -342,7 +342,7 @@ jobs:
|
||||||
uses: swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2
|
uses: swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2
|
||||||
with:
|
with:
|
||||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||||
cache-provider: "buildjet"
|
# cache-provider: "buildjet"
|
||||||
|
|
||||||
- name: Install Linux dependencies
|
- name: Install Linux dependencies
|
||||||
run: ./script/linux
|
run: ./script/linux
|
||||||
|
@ -380,7 +380,7 @@ jobs:
|
||||||
github.repository_owner == 'zed-industries' &&
|
github.repository_owner == 'zed-industries' &&
|
||||||
needs.job_spec.outputs.run_tests == 'true'
|
needs.job_spec.outputs.run_tests == 'true'
|
||||||
runs-on:
|
runs-on:
|
||||||
- buildjet-8vcpu-ubuntu-2204
|
- github-8vcpu-ubuntu-2204
|
||||||
steps:
|
steps:
|
||||||
- name: Add Rust to the PATH
|
- name: Add Rust to the PATH
|
||||||
run: echo "$HOME/.cargo/bin" >> "$GITHUB_PATH"
|
run: echo "$HOME/.cargo/bin" >> "$GITHUB_PATH"
|
||||||
|
@ -394,7 +394,7 @@ jobs:
|
||||||
uses: swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2
|
uses: swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2
|
||||||
with:
|
with:
|
||||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||||
cache-provider: "buildjet"
|
# cache-provider: "buildjet"
|
||||||
|
|
||||||
- name: Install Clang & Mold
|
- name: Install Clang & Mold
|
||||||
run: ./script/remote-server && ./script/install-mold 2.34.0
|
run: ./script/remote-server && ./script/install-mold 2.34.0
|
||||||
|
@ -597,7 +597,8 @@ jobs:
|
||||||
timeout-minutes: 60
|
timeout-minutes: 60
|
||||||
name: Linux x86_x64 release bundle
|
name: Linux x86_x64 release bundle
|
||||||
runs-on:
|
runs-on:
|
||||||
- buildjet-16vcpu-ubuntu-2004 # ubuntu 20.04 for minimal glibc
|
- github-16vcpu-ubuntu-2204
|
||||||
|
# - buildjet-16vcpu-ubuntu-2004 # ubuntu 20.04 for minimal glibc
|
||||||
if: |
|
if: |
|
||||||
startsWith(github.ref, 'refs/tags/v')
|
startsWith(github.ref, 'refs/tags/v')
|
||||||
|| contains(github.event.pull_request.labels.*.name, 'run-bundling')
|
|| contains(github.event.pull_request.labels.*.name, 'run-bundling')
|
||||||
|
@ -650,7 +651,7 @@ jobs:
|
||||||
timeout-minutes: 60
|
timeout-minutes: 60
|
||||||
name: Linux arm64 release bundle
|
name: Linux arm64 release bundle
|
||||||
runs-on:
|
runs-on:
|
||||||
- buildjet-32vcpu-ubuntu-2204-arm
|
- github-16vcpu-ubuntu-2204-arm
|
||||||
if: |
|
if: |
|
||||||
startsWith(github.ref, 'refs/tags/v')
|
startsWith(github.ref, 'refs/tags/v')
|
||||||
|| contains(github.event.pull_request.labels.*.name, 'run-bundling')
|
|| contains(github.event.pull_request.labels.*.name, 'run-bundling')
|
||||||
|
|
2
.github/workflows/deploy_cloudflare.yml
vendored
2
.github/workflows/deploy_cloudflare.yml
vendored
|
@ -9,7 +9,7 @@ jobs:
|
||||||
deploy-docs:
|
deploy-docs:
|
||||||
name: Deploy Docs
|
name: Deploy Docs
|
||||||
if: github.repository_owner == 'zed-industries'
|
if: github.repository_owner == 'zed-industries'
|
||||||
runs-on: buildjet-16vcpu-ubuntu-2204
|
runs-on: github-16vcpu-ubuntu-2204
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repo
|
- name: Checkout repo
|
||||||
|
|
6
.github/workflows/deploy_collab.yml
vendored
6
.github/workflows/deploy_collab.yml
vendored
|
@ -61,7 +61,7 @@ jobs:
|
||||||
- style
|
- style
|
||||||
- tests
|
- tests
|
||||||
runs-on:
|
runs-on:
|
||||||
- buildjet-16vcpu-ubuntu-2204
|
- github-16vcpu-ubuntu-2204
|
||||||
steps:
|
steps:
|
||||||
- name: Install doctl
|
- name: Install doctl
|
||||||
uses: digitalocean/action-doctl@v2
|
uses: digitalocean/action-doctl@v2
|
||||||
|
@ -94,7 +94,7 @@ jobs:
|
||||||
needs:
|
needs:
|
||||||
- publish
|
- publish
|
||||||
runs-on:
|
runs-on:
|
||||||
- buildjet-16vcpu-ubuntu-2204
|
- github-16vcpu-ubuntu-2204
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repo
|
- name: Checkout repo
|
||||||
|
@ -137,12 +137,14 @@ jobs:
|
||||||
|
|
||||||
export ZED_SERVICE_NAME=collab
|
export ZED_SERVICE_NAME=collab
|
||||||
export ZED_LOAD_BALANCER_SIZE_UNIT=$ZED_COLLAB_LOAD_BALANCER_SIZE_UNIT
|
export ZED_LOAD_BALANCER_SIZE_UNIT=$ZED_COLLAB_LOAD_BALANCER_SIZE_UNIT
|
||||||
|
export DATABASE_MAX_CONNECTIONS=850
|
||||||
envsubst < crates/collab/k8s/collab.template.yml | kubectl apply -f -
|
envsubst < crates/collab/k8s/collab.template.yml | kubectl apply -f -
|
||||||
kubectl -n "$ZED_KUBE_NAMESPACE" rollout status deployment/$ZED_SERVICE_NAME --watch
|
kubectl -n "$ZED_KUBE_NAMESPACE" rollout status deployment/$ZED_SERVICE_NAME --watch
|
||||||
echo "deployed ${ZED_SERVICE_NAME} to ${ZED_KUBE_NAMESPACE}"
|
echo "deployed ${ZED_SERVICE_NAME} to ${ZED_KUBE_NAMESPACE}"
|
||||||
|
|
||||||
export ZED_SERVICE_NAME=api
|
export ZED_SERVICE_NAME=api
|
||||||
export ZED_LOAD_BALANCER_SIZE_UNIT=$ZED_API_LOAD_BALANCER_SIZE_UNIT
|
export ZED_LOAD_BALANCER_SIZE_UNIT=$ZED_API_LOAD_BALANCER_SIZE_UNIT
|
||||||
|
export DATABASE_MAX_CONNECTIONS=60
|
||||||
envsubst < crates/collab/k8s/collab.template.yml | kubectl apply -f -
|
envsubst < crates/collab/k8s/collab.template.yml | kubectl apply -f -
|
||||||
kubectl -n "$ZED_KUBE_NAMESPACE" rollout status deployment/$ZED_SERVICE_NAME --watch
|
kubectl -n "$ZED_KUBE_NAMESPACE" rollout status deployment/$ZED_SERVICE_NAME --watch
|
||||||
echo "deployed ${ZED_SERVICE_NAME} to ${ZED_KUBE_NAMESPACE}"
|
echo "deployed ${ZED_SERVICE_NAME} to ${ZED_KUBE_NAMESPACE}"
|
||||||
|
|
4
.github/workflows/eval.yml
vendored
4
.github/workflows/eval.yml
vendored
|
@ -32,7 +32,7 @@ jobs:
|
||||||
github.repository_owner == 'zed-industries' &&
|
github.repository_owner == 'zed-industries' &&
|
||||||
(github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'run-eval'))
|
(github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'run-eval'))
|
||||||
runs-on:
|
runs-on:
|
||||||
- buildjet-16vcpu-ubuntu-2204
|
- github-16vcpu-ubuntu-2204
|
||||||
steps:
|
steps:
|
||||||
- name: Add Rust to the PATH
|
- name: Add Rust to the PATH
|
||||||
run: echo "$HOME/.cargo/bin" >> "$GITHUB_PATH"
|
run: echo "$HOME/.cargo/bin" >> "$GITHUB_PATH"
|
||||||
|
@ -46,7 +46,7 @@ jobs:
|
||||||
uses: swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2
|
uses: swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2
|
||||||
with:
|
with:
|
||||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||||
cache-provider: "buildjet"
|
# cache-provider: "buildjet"
|
||||||
|
|
||||||
- name: Install Linux dependencies
|
- name: Install Linux dependencies
|
||||||
run: ./script/linux
|
run: ./script/linux
|
||||||
|
|
2
.github/workflows/nix.yml
vendored
2
.github/workflows/nix.yml
vendored
|
@ -20,7 +20,7 @@ jobs:
|
||||||
matrix:
|
matrix:
|
||||||
system:
|
system:
|
||||||
- os: x86 Linux
|
- os: x86 Linux
|
||||||
runner: buildjet-16vcpu-ubuntu-2204
|
runner: github-16vcpu-ubuntu-2204
|
||||||
install_nix: true
|
install_nix: true
|
||||||
- os: arm Mac
|
- os: arm Mac
|
||||||
runner: [macOS, ARM64, test]
|
runner: [macOS, ARM64, test]
|
||||||
|
|
2
.github/workflows/randomized_tests.yml
vendored
2
.github/workflows/randomized_tests.yml
vendored
|
@ -20,7 +20,7 @@ jobs:
|
||||||
name: Run randomized tests
|
name: Run randomized tests
|
||||||
if: github.repository_owner == 'zed-industries'
|
if: github.repository_owner == 'zed-industries'
|
||||||
runs-on:
|
runs-on:
|
||||||
- buildjet-16vcpu-ubuntu-2204
|
- github-16vcpu-ubuntu-2204
|
||||||
steps:
|
steps:
|
||||||
- name: Install Node
|
- name: Install Node
|
||||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||||
|
|
5
.github/workflows/release_nightly.yml
vendored
5
.github/workflows/release_nightly.yml
vendored
|
@ -128,7 +128,8 @@ jobs:
|
||||||
name: Create a Linux *.tar.gz bundle for x86
|
name: Create a Linux *.tar.gz bundle for x86
|
||||||
if: github.repository_owner == 'zed-industries'
|
if: github.repository_owner == 'zed-industries'
|
||||||
runs-on:
|
runs-on:
|
||||||
- buildjet-16vcpu-ubuntu-2004
|
- github-16vcpu-ubuntu-2204
|
||||||
|
# - buildjet-16vcpu-ubuntu-2004
|
||||||
needs: tests
|
needs: tests
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repo
|
- name: Checkout repo
|
||||||
|
@ -168,7 +169,7 @@ jobs:
|
||||||
name: Create a Linux *.tar.gz bundle for ARM
|
name: Create a Linux *.tar.gz bundle for ARM
|
||||||
if: github.repository_owner == 'zed-industries'
|
if: github.repository_owner == 'zed-industries'
|
||||||
runs-on:
|
runs-on:
|
||||||
- buildjet-32vcpu-ubuntu-2204-arm
|
- github-16vcpu-ubuntu-2204-arm
|
||||||
needs: tests
|
needs: tests
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repo
|
- name: Checkout repo
|
||||||
|
|
4
.github/workflows/unit_evals.yml
vendored
4
.github/workflows/unit_evals.yml
vendored
|
@ -23,7 +23,7 @@ jobs:
|
||||||
timeout-minutes: 60
|
timeout-minutes: 60
|
||||||
name: Run unit evals
|
name: Run unit evals
|
||||||
runs-on:
|
runs-on:
|
||||||
- buildjet-16vcpu-ubuntu-2204
|
- github-16vcpu-ubuntu-2204
|
||||||
steps:
|
steps:
|
||||||
- name: Add Rust to the PATH
|
- name: Add Rust to the PATH
|
||||||
run: echo "$HOME/.cargo/bin" >> "$GITHUB_PATH"
|
run: echo "$HOME/.cargo/bin" >> "$GITHUB_PATH"
|
||||||
|
@ -37,7 +37,7 @@ jobs:
|
||||||
uses: swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2
|
uses: swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2
|
||||||
with:
|
with:
|
||||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||||
cache-provider: "buildjet"
|
# cache-provider: "buildjet"
|
||||||
|
|
||||||
- name: Install Linux dependencies
|
- name: Install Linux dependencies
|
||||||
run: ./script/linux
|
run: ./script/linux
|
||||||
|
|
7
Cargo.lock
generated
7
Cargo.lock
generated
|
@ -138,9 +138,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "agent-client-protocol"
|
name = "agent-client-protocol"
|
||||||
version = "0.0.22"
|
version = "0.0.23"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "88ea41139e9680f53bbfd0d3a60d92f2832e00645f2ffb1365f76992ff2f6a79"
|
checksum = "3fad72b7b8ee4331b3a4c8d43c107e982a4725564b4ee658ae5c4e79d2b486e8"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"futures 0.3.31",
|
"futures 0.3.31",
|
||||||
|
@ -174,11 +174,13 @@ dependencies = [
|
||||||
"gpui_tokio",
|
"gpui_tokio",
|
||||||
"handlebars 4.5.0",
|
"handlebars 4.5.0",
|
||||||
"indoc",
|
"indoc",
|
||||||
|
"itertools 0.14.0",
|
||||||
"language",
|
"language",
|
||||||
"language_model",
|
"language_model",
|
||||||
"language_models",
|
"language_models",
|
||||||
"log",
|
"log",
|
||||||
"paths",
|
"paths",
|
||||||
|
"pretty_assertions",
|
||||||
"project",
|
"project",
|
||||||
"prompt_store",
|
"prompt_store",
|
||||||
"reqwest_client",
|
"reqwest_client",
|
||||||
|
@ -12639,6 +12641,7 @@ dependencies = [
|
||||||
"editor",
|
"editor",
|
||||||
"file_icons",
|
"file_icons",
|
||||||
"git",
|
"git",
|
||||||
|
"git_ui",
|
||||||
"gpui",
|
"gpui",
|
||||||
"indexmap",
|
"indexmap",
|
||||||
"language",
|
"language",
|
||||||
|
|
|
@ -425,7 +425,7 @@ zlog_settings = { path = "crates/zlog_settings" }
|
||||||
#
|
#
|
||||||
|
|
||||||
agentic-coding-protocol = "0.0.10"
|
agentic-coding-protocol = "0.0.10"
|
||||||
agent-client-protocol = "0.0.22"
|
agent-client-protocol = "0.0.23"
|
||||||
aho-corasick = "1.1"
|
aho-corasick = "1.1"
|
||||||
alacritty_terminal = { git = "https://github.com/zed-industries/alacritty.git", branch = "add-hush-login-flag" }
|
alacritty_terminal = { git = "https://github.com/zed-industries/alacritty.git", branch = "add-hush-login-flag" }
|
||||||
any_vec = "0.14"
|
any_vec = "0.14"
|
||||||
|
|
File diff suppressed because one or more lines are too long
Before Width: | Height: | Size: 6.4 KiB |
File diff suppressed because one or more lines are too long
Before Width: | Height: | Size: 2.5 KiB After Width: | Height: | Size: 5.3 KiB |
1
assets/images/pro_user_stamp.svg
Normal file
1
assets/images/pro_user_stamp.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 5.5 KiB |
|
@ -848,6 +848,7 @@
|
||||||
"ctrl-delete": ["project_panel::Delete", { "skip_prompt": false }],
|
"ctrl-delete": ["project_panel::Delete", { "skip_prompt": false }],
|
||||||
"alt-ctrl-r": "project_panel::RevealInFileManager",
|
"alt-ctrl-r": "project_panel::RevealInFileManager",
|
||||||
"ctrl-shift-enter": "project_panel::OpenWithSystem",
|
"ctrl-shift-enter": "project_panel::OpenWithSystem",
|
||||||
|
"alt-d": "project_panel::CompareMarkedFiles",
|
||||||
"shift-find": "project_panel::NewSearchInDirectory",
|
"shift-find": "project_panel::NewSearchInDirectory",
|
||||||
"ctrl-alt-shift-f": "project_panel::NewSearchInDirectory",
|
"ctrl-alt-shift-f": "project_panel::NewSearchInDirectory",
|
||||||
"shift-down": "menu::SelectNext",
|
"shift-down": "menu::SelectNext",
|
||||||
|
@ -1102,6 +1103,13 @@
|
||||||
"ctrl-enter": "menu::Confirm"
|
"ctrl-enter": "menu::Confirm"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"context": "OnboardingAiConfigurationModal",
|
||||||
|
"use_key_equivalents": true,
|
||||||
|
"bindings": {
|
||||||
|
"escape": "menu::Cancel"
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"context": "Diagnostics",
|
"context": "Diagnostics",
|
||||||
"use_key_equivalents": true,
|
"use_key_equivalents": true,
|
||||||
|
@ -1178,7 +1186,8 @@
|
||||||
"ctrl-1": "onboarding::ActivateBasicsPage",
|
"ctrl-1": "onboarding::ActivateBasicsPage",
|
||||||
"ctrl-2": "onboarding::ActivateEditingPage",
|
"ctrl-2": "onboarding::ActivateEditingPage",
|
||||||
"ctrl-3": "onboarding::ActivateAISetupPage",
|
"ctrl-3": "onboarding::ActivateAISetupPage",
|
||||||
"ctrl-escape": "onboarding::Finish"
|
"ctrl-escape": "onboarding::Finish",
|
||||||
|
"alt-tab": "onboarding::SignIn"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
|
@ -907,6 +907,7 @@
|
||||||
"cmd-delete": ["project_panel::Delete", { "skip_prompt": false }],
|
"cmd-delete": ["project_panel::Delete", { "skip_prompt": false }],
|
||||||
"alt-cmd-r": "project_panel::RevealInFileManager",
|
"alt-cmd-r": "project_panel::RevealInFileManager",
|
||||||
"ctrl-shift-enter": "project_panel::OpenWithSystem",
|
"ctrl-shift-enter": "project_panel::OpenWithSystem",
|
||||||
|
"alt-d": "project_panel::CompareMarkedFiles",
|
||||||
"cmd-alt-backspace": ["project_panel::Delete", { "skip_prompt": false }],
|
"cmd-alt-backspace": ["project_panel::Delete", { "skip_prompt": false }],
|
||||||
"cmd-alt-shift-f": "project_panel::NewSearchInDirectory",
|
"cmd-alt-shift-f": "project_panel::NewSearchInDirectory",
|
||||||
"shift-down": "menu::SelectNext",
|
"shift-down": "menu::SelectNext",
|
||||||
|
@ -1204,6 +1205,13 @@
|
||||||
"cmd-enter": "menu::Confirm"
|
"cmd-enter": "menu::Confirm"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"context": "OnboardingAiConfigurationModal",
|
||||||
|
"use_key_equivalents": true,
|
||||||
|
"bindings": {
|
||||||
|
"escape": "menu::Cancel"
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"context": "Diagnostics",
|
"context": "Diagnostics",
|
||||||
"use_key_equivalents": true,
|
"use_key_equivalents": true,
|
||||||
|
@ -1280,7 +1288,8 @@
|
||||||
"cmd-1": "onboarding::ActivateBasicsPage",
|
"cmd-1": "onboarding::ActivateBasicsPage",
|
||||||
"cmd-2": "onboarding::ActivateEditingPage",
|
"cmd-2": "onboarding::ActivateEditingPage",
|
||||||
"cmd-3": "onboarding::ActivateAISetupPage",
|
"cmd-3": "onboarding::ActivateAISetupPage",
|
||||||
"cmd-escape": "onboarding::Finish"
|
"cmd-escape": "onboarding::Finish",
|
||||||
|
"alt-tab": "onboarding::SignIn"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
|
@ -813,6 +813,7 @@
|
||||||
"p": "project_panel::Open",
|
"p": "project_panel::Open",
|
||||||
"x": "project_panel::RevealInFileManager",
|
"x": "project_panel::RevealInFileManager",
|
||||||
"s": "project_panel::OpenWithSystem",
|
"s": "project_panel::OpenWithSystem",
|
||||||
|
"z d": "project_panel::CompareMarkedFiles",
|
||||||
"] c": "project_panel::SelectNextGitEntry",
|
"] c": "project_panel::SelectNextGitEntry",
|
||||||
"[ c": "project_panel::SelectPrevGitEntry",
|
"[ c": "project_panel::SelectPrevGitEntry",
|
||||||
"] d": "project_panel::SelectNextDiagnostic",
|
"] d": "project_panel::SelectNextDiagnostic",
|
||||||
|
|
|
@ -14,8 +14,8 @@ workspace = true
|
||||||
[dependencies]
|
[dependencies]
|
||||||
acp_thread.workspace = true
|
acp_thread.workspace = true
|
||||||
agent-client-protocol.workspace = true
|
agent-client-protocol.workspace = true
|
||||||
agent_settings.workspace = true
|
|
||||||
agent_servers.workspace = true
|
agent_servers.workspace = true
|
||||||
|
agent_settings.workspace = true
|
||||||
anyhow.workspace = true
|
anyhow.workspace = true
|
||||||
assistant_tool.workspace = true
|
assistant_tool.workspace = true
|
||||||
assistant_tools.workspace = true
|
assistant_tools.workspace = true
|
||||||
|
@ -26,6 +26,7 @@ futures.workspace = true
|
||||||
gpui.workspace = true
|
gpui.workspace = true
|
||||||
handlebars = { workspace = true, features = ["rust-embed"] }
|
handlebars = { workspace = true, features = ["rust-embed"] }
|
||||||
indoc.workspace = true
|
indoc.workspace = true
|
||||||
|
itertools.workspace = true
|
||||||
language.workspace = true
|
language.workspace = true
|
||||||
language_model.workspace = true
|
language_model.workspace = true
|
||||||
language_models.workspace = true
|
language_models.workspace = true
|
||||||
|
@ -59,3 +60,4 @@ project = { workspace = true, "features" = ["test-support"] }
|
||||||
reqwest_client.workspace = true
|
reqwest_client.workspace = true
|
||||||
settings = { workspace = true, "features" = ["test-support"] }
|
settings = { workspace = true, "features" = ["test-support"] }
|
||||||
worktree = { workspace = true, "features" = ["test-support"] }
|
worktree = { workspace = true, "features" = ["test-support"] }
|
||||||
|
pretty_assertions.workspace = true
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
use crate::{templates::Templates, AgentResponseEvent, Thread};
|
use crate::{templates::Templates, AgentResponseEvent, Thread};
|
||||||
use crate::{EditFileTool, FindPathTool, ToolCallAuthorization};
|
use crate::{EditFileTool, FindPathTool, ReadFileTool, ThinkingTool, ToolCallAuthorization};
|
||||||
use acp_thread::ModelSelector;
|
use acp_thread::ModelSelector;
|
||||||
use agent_client_protocol as acp;
|
use agent_client_protocol as acp;
|
||||||
use anyhow::{anyhow, Context as _, Result};
|
use anyhow::{anyhow, Context as _, Result};
|
||||||
|
@ -414,7 +414,9 @@ impl acp_thread::AgentConnection for NativeAgentConnection {
|
||||||
|
|
||||||
let thread = cx.new(|cx| {
|
let thread = cx.new(|cx| {
|
||||||
let mut thread = Thread::new(project.clone(), agent.project_context.clone(), action_log.clone(), agent.templates.clone(), default_model);
|
let mut thread = Thread::new(project.clone(), agent.project_context.clone(), action_log.clone(), agent.templates.clone(), default_model);
|
||||||
|
thread.add_tool(ThinkingTool);
|
||||||
thread.add_tool(FindPathTool::new(project.clone()));
|
thread.add_tool(FindPathTool::new(project.clone()));
|
||||||
|
thread.add_tool(ReadFileTool::new(project.clone(), action_log));
|
||||||
thread.add_tool(EditFileTool::new(project.clone(), cx.entity()));
|
thread.add_tool(EditFileTool::new(project.clone(), cx.entity()));
|
||||||
thread
|
thread
|
||||||
});
|
});
|
||||||
|
|
|
@ -270,14 +270,14 @@ async fn test_tool_authorization(cx: &mut TestAppContext) {
|
||||||
vec![
|
vec![
|
||||||
MessageContent::ToolResult(LanguageModelToolResult {
|
MessageContent::ToolResult(LanguageModelToolResult {
|
||||||
tool_use_id: tool_call_auth_1.tool_call.id.0.to_string().into(),
|
tool_use_id: tool_call_auth_1.tool_call.id.0.to_string().into(),
|
||||||
tool_name: tool_call_auth_1.tool_call.title.into(),
|
tool_name: ToolRequiringPermission.name().into(),
|
||||||
is_error: false,
|
is_error: false,
|
||||||
content: "Allowed".into(),
|
content: "Allowed".into(),
|
||||||
output: None
|
output: None
|
||||||
}),
|
}),
|
||||||
MessageContent::ToolResult(LanguageModelToolResult {
|
MessageContent::ToolResult(LanguageModelToolResult {
|
||||||
tool_use_id: tool_call_auth_2.tool_call.id.0.to_string().into(),
|
tool_use_id: tool_call_auth_2.tool_call.id.0.to_string().into(),
|
||||||
tool_name: tool_call_auth_2.tool_call.title.into(),
|
tool_name: ToolRequiringPermission.name().into(),
|
||||||
is_error: true,
|
is_error: true,
|
||||||
content: "Permission to run tool denied by user".into(),
|
content: "Permission to run tool denied by user".into(),
|
||||||
output: None
|
output: None
|
||||||
|
@ -639,6 +639,77 @@ async fn test_agent_connection(cx: &mut TestAppContext) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
async fn test_tool_updates_to_completion(cx: &mut TestAppContext) {
|
||||||
|
let ThreadTest { thread, model, .. } = setup(cx, TestModel::Fake).await;
|
||||||
|
thread.update(cx, |thread, _cx| thread.add_tool(ThinkingTool));
|
||||||
|
let fake_model = model.as_fake();
|
||||||
|
|
||||||
|
let mut events = thread.update(cx, |thread, cx| thread.send(model.clone(), "Think", cx));
|
||||||
|
cx.run_until_parked();
|
||||||
|
|
||||||
|
let input = json!({ "content": "Thinking hard!" });
|
||||||
|
fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse(
|
||||||
|
LanguageModelToolUse {
|
||||||
|
id: "1".into(),
|
||||||
|
name: ThinkingTool.name().into(),
|
||||||
|
raw_input: input.to_string(),
|
||||||
|
input,
|
||||||
|
is_input_complete: true,
|
||||||
|
},
|
||||||
|
));
|
||||||
|
fake_model.end_last_completion_stream();
|
||||||
|
cx.run_until_parked();
|
||||||
|
|
||||||
|
let tool_call = expect_tool_call(&mut events).await;
|
||||||
|
assert_eq!(
|
||||||
|
tool_call,
|
||||||
|
acp::ToolCall {
|
||||||
|
id: acp::ToolCallId("1".into()),
|
||||||
|
title: "Thinking".into(),
|
||||||
|
kind: acp::ToolKind::Think,
|
||||||
|
status: acp::ToolCallStatus::Pending,
|
||||||
|
content: vec![],
|
||||||
|
locations: vec![],
|
||||||
|
raw_input: Some(json!({ "content": "Thinking hard!" })),
|
||||||
|
raw_output: None,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
let update = expect_tool_call_update(&mut events).await;
|
||||||
|
assert_eq!(
|
||||||
|
update,
|
||||||
|
acp::ToolCallUpdate {
|
||||||
|
id: acp::ToolCallId("1".into()),
|
||||||
|
fields: acp::ToolCallUpdateFields {
|
||||||
|
status: Some(acp::ToolCallStatus::InProgress,),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
let update = expect_tool_call_update(&mut events).await;
|
||||||
|
assert_eq!(
|
||||||
|
update,
|
||||||
|
acp::ToolCallUpdate {
|
||||||
|
id: acp::ToolCallId("1".into()),
|
||||||
|
fields: acp::ToolCallUpdateFields {
|
||||||
|
content: Some(vec!["Thinking hard!".into()]),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
let update = expect_tool_call_update(&mut events).await;
|
||||||
|
assert_eq!(
|
||||||
|
update,
|
||||||
|
acp::ToolCallUpdate {
|
||||||
|
id: acp::ToolCallId("1".into()),
|
||||||
|
fields: acp::ToolCallUpdateFields {
|
||||||
|
status: Some(acp::ToolCallStatus::Completed),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/// Filters out the stop events for asserting against in tests
|
/// Filters out the stop events for asserting against in tests
|
||||||
fn stop_events(
|
fn stop_events(
|
||||||
result_events: Vec<Result<AgentResponseEvent, LanguageModelCompletionError>>,
|
result_events: Vec<Result<AgentResponseEvent, LanguageModelCompletionError>>,
|
||||||
|
|
|
@ -23,8 +23,8 @@ impl AgentTool for EchoTool {
|
||||||
acp::ToolKind::Other
|
acp::ToolKind::Other
|
||||||
}
|
}
|
||||||
|
|
||||||
fn needs_authorization(&self, _input: Self::Input, _cx: &App) -> bool {
|
fn initial_title(&self, _: Self::Input) -> SharedString {
|
||||||
false
|
"Echo".into()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn run(
|
fn run(
|
||||||
|
@ -53,12 +53,12 @@ impl AgentTool for DelayTool {
|
||||||
"delay".into()
|
"delay".into()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn kind(&self) -> acp::ToolKind {
|
fn initial_title(&self, input: Self::Input) -> SharedString {
|
||||||
acp::ToolKind::Other
|
format!("Delay {}ms", input.ms).into()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn needs_authorization(&self, _input: Self::Input, _cx: &App) -> bool {
|
fn kind(&self) -> acp::ToolKind {
|
||||||
false
|
acp::ToolKind::Other
|
||||||
}
|
}
|
||||||
|
|
||||||
fn run(
|
fn run(
|
||||||
|
@ -93,21 +93,21 @@ impl AgentTool for ToolRequiringPermission {
|
||||||
acp::ToolKind::Other
|
acp::ToolKind::Other
|
||||||
}
|
}
|
||||||
|
|
||||||
fn needs_authorization(&self, _input: Self::Input, _cx: &App) -> bool {
|
fn initial_title(&self, _input: Self::Input) -> SharedString {
|
||||||
true
|
"This tool requires permission".into()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn run(
|
fn run(
|
||||||
self: Arc<Self>,
|
self: Arc<Self>,
|
||||||
_input: Self::Input,
|
input: Self::Input,
|
||||||
_event_stream: ToolCallEventStream,
|
event_stream: ToolCallEventStream,
|
||||||
cx: &mut App,
|
cx: &mut App,
|
||||||
) -> Task<Result<String>>
|
) -> Task<Result<String>> {
|
||||||
where
|
let auth_check = self.authorize(input, event_stream);
|
||||||
Self: Sized,
|
cx.foreground_executor().spawn(async move {
|
||||||
{
|
auth_check.await?;
|
||||||
cx.foreground_executor()
|
Ok("Allowed".to_string())
|
||||||
.spawn(async move { Ok("Allowed".to_string()) })
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -127,8 +127,8 @@ impl AgentTool for InfiniteTool {
|
||||||
acp::ToolKind::Other
|
acp::ToolKind::Other
|
||||||
}
|
}
|
||||||
|
|
||||||
fn needs_authorization(&self, _input: Self::Input, _cx: &App) -> bool {
|
fn initial_title(&self, _input: Self::Input) -> SharedString {
|
||||||
false
|
"This is the tool that never ends... it just goes on and on my friends!".into()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn run(
|
fn run(
|
||||||
|
@ -177,8 +177,8 @@ impl AgentTool for WordListTool {
|
||||||
acp::ToolKind::Other
|
acp::ToolKind::Other
|
||||||
}
|
}
|
||||||
|
|
||||||
fn needs_authorization(&self, _input: Self::Input, _cx: &App) -> bool {
|
fn initial_title(&self, _input: Self::Input) -> SharedString {
|
||||||
false
|
"List of random words".into()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn run(
|
fn run(
|
||||||
|
|
|
@ -2,7 +2,7 @@ use crate::templates::{SystemPromptTemplate, Template, Templates};
|
||||||
use acp_thread::Diff;
|
use acp_thread::Diff;
|
||||||
use agent_client_protocol as acp;
|
use agent_client_protocol as acp;
|
||||||
use anyhow::{anyhow, Context as _, Result};
|
use anyhow::{anyhow, Context as _, Result};
|
||||||
use assistant_tool::ActionLog;
|
use assistant_tool::{adapt_schema_to_format, ActionLog};
|
||||||
use cloud_llm_client::{CompletionIntent, CompletionMode};
|
use cloud_llm_client::{CompletionIntent, CompletionMode};
|
||||||
use collections::HashMap;
|
use collections::HashMap;
|
||||||
use futures::{
|
use futures::{
|
||||||
|
@ -20,7 +20,7 @@ use log;
|
||||||
use project::Project;
|
use project::Project;
|
||||||
use prompt_store::ProjectContext;
|
use prompt_store::ProjectContext;
|
||||||
use schemars::{JsonSchema, Schema};
|
use schemars::{JsonSchema, Schema};
|
||||||
use serde::Deserialize;
|
use serde::{Deserialize, Serialize};
|
||||||
use smol::stream::StreamExt;
|
use smol::stream::StreamExt;
|
||||||
use std::{cell::RefCell, collections::BTreeMap, fmt::Write, future::Future, rc::Rc, sync::Arc};
|
use std::{cell::RefCell, collections::BTreeMap, fmt::Write, future::Future, rc::Rc, sync::Arc};
|
||||||
use util::{markdown::MarkdownCodeBlock, ResultExt};
|
use util::{markdown::MarkdownCodeBlock, ResultExt};
|
||||||
|
@ -469,12 +469,7 @@ impl Thread {
|
||||||
});
|
});
|
||||||
|
|
||||||
if push_new_tool_use {
|
if push_new_tool_use {
|
||||||
event_stream.send_tool_call(
|
event_stream.send_tool_call(tool.as_ref(), &tool_use);
|
||||||
&tool_use,
|
|
||||||
tool.as_ref()
|
|
||||||
.map(|t| t.kind())
|
|
||||||
.unwrap_or(acp::ToolKind::Other),
|
|
||||||
);
|
|
||||||
last_message
|
last_message
|
||||||
.content
|
.content
|
||||||
.push(MessageContent::ToolUse(tool_use.clone()));
|
.push(MessageContent::ToolUse(tool_use.clone()));
|
||||||
|
@ -531,23 +526,13 @@ impl Thread {
|
||||||
event_stream: AgentResponseEventStream,
|
event_stream: AgentResponseEventStream,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) -> Task<Result<String>> {
|
) -> Task<Result<String>> {
|
||||||
// TODO: should we push this down into the tool itself?
|
|
||||||
let needs_authorization = tool.needs_authorization(tool_use.input.clone(), cx);
|
|
||||||
cx.spawn(async move |_this, cx| {
|
cx.spawn(async move |_this, cx| {
|
||||||
if needs_authorization? {
|
|
||||||
event_stream.authorize_tool_call(&tool_use).await?;
|
|
||||||
}
|
|
||||||
|
|
||||||
let tool_event_stream = ToolCallEventStream::new(tool_use.id, event_stream);
|
let tool_event_stream = ToolCallEventStream::new(tool_use.id, event_stream);
|
||||||
tool_event_stream.send_update(acp::ToolCallUpdateFields {
|
tool_event_stream.send_update(acp::ToolCallUpdateFields {
|
||||||
status: Some(acp::ToolCallStatus::InProgress),
|
status: Some(acp::ToolCallStatus::InProgress),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
});
|
});
|
||||||
log::trace!(
|
|
||||||
"Running tool {:?} with input: {}",
|
|
||||||
tool_use.name,
|
|
||||||
serde_json::to_string_pretty(&tool_use.input).unwrap_or_default()
|
|
||||||
);
|
|
||||||
cx.update(|cx| tool.run(tool_use.input, tool_event_stream, cx))?
|
cx.update(|cx| tool.run(tool_use.input, tool_event_stream, cx))?
|
||||||
.await
|
.await
|
||||||
})
|
})
|
||||||
|
@ -618,7 +603,7 @@ impl Thread {
|
||||||
name: tool_name,
|
name: tool_name,
|
||||||
description: tool.description(cx).to_string(),
|
description: tool.description(cx).to_string(),
|
||||||
input_schema: tool
|
input_schema: tool
|
||||||
.input_schema(LanguageModelToolSchemaFormat::JsonSchema)
|
.input_schema(self.selected_model.tool_input_format())
|
||||||
.log_err()?,
|
.log_err()?,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -685,7 +670,7 @@ pub trait AgentTool
|
||||||
where
|
where
|
||||||
Self: 'static + Sized,
|
Self: 'static + Sized,
|
||||||
{
|
{
|
||||||
type Input: for<'de> Deserialize<'de> + JsonSchema;
|
type Input: for<'de> Deserialize<'de> + Serialize + JsonSchema;
|
||||||
|
|
||||||
fn name(&self) -> SharedString;
|
fn name(&self) -> SharedString;
|
||||||
|
|
||||||
|
@ -701,14 +686,23 @@ where
|
||||||
|
|
||||||
fn kind(&self) -> acp::ToolKind;
|
fn kind(&self) -> acp::ToolKind;
|
||||||
|
|
||||||
|
/// The initial tool title to display. Can be updated during the tool run.
|
||||||
|
fn initial_title(&self, input: Self::Input) -> SharedString;
|
||||||
|
|
||||||
/// Returns the JSON schema that describes the tool's input.
|
/// Returns the JSON schema that describes the tool's input.
|
||||||
fn input_schema(&self, _format: LanguageModelToolSchemaFormat) -> Schema {
|
fn input_schema(&self) -> Schema {
|
||||||
schemars::schema_for!(Self::Input)
|
schemars::schema_for!(Self::Input)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns true if the tool needs the users's authorization
|
/// Allows the tool to authorize a given tool call with the user if necessary
|
||||||
/// before running.
|
fn authorize(
|
||||||
fn needs_authorization(&self, input: Self::Input, cx: &App) -> bool;
|
&self,
|
||||||
|
input: Self::Input,
|
||||||
|
event_stream: ToolCallEventStream,
|
||||||
|
) -> impl use<Self> + Future<Output = Result<()>> {
|
||||||
|
let json_input = serde_json::json!(&input);
|
||||||
|
event_stream.authorize(self.initial_title(input).into(), self.kind(), json_input)
|
||||||
|
}
|
||||||
|
|
||||||
/// Runs the tool with the provided input.
|
/// Runs the tool with the provided input.
|
||||||
fn run(
|
fn run(
|
||||||
|
@ -729,8 +723,8 @@ pub trait AnyAgentTool {
|
||||||
fn name(&self) -> SharedString;
|
fn name(&self) -> SharedString;
|
||||||
fn description(&self, cx: &mut App) -> SharedString;
|
fn description(&self, cx: &mut App) -> SharedString;
|
||||||
fn kind(&self) -> acp::ToolKind;
|
fn kind(&self) -> acp::ToolKind;
|
||||||
|
fn initial_title(&self, input: serde_json::Value) -> Result<SharedString>;
|
||||||
fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result<serde_json::Value>;
|
fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result<serde_json::Value>;
|
||||||
fn needs_authorization(&self, input: serde_json::Value, cx: &mut App) -> Result<bool>;
|
|
||||||
fn run(
|
fn run(
|
||||||
self: Arc<Self>,
|
self: Arc<Self>,
|
||||||
input: serde_json::Value,
|
input: serde_json::Value,
|
||||||
|
@ -755,16 +749,15 @@ where
|
||||||
self.0.kind()
|
self.0.kind()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result<serde_json::Value> {
|
fn initial_title(&self, input: serde_json::Value) -> Result<SharedString> {
|
||||||
Ok(serde_json::to_value(self.0.input_schema(format))?)
|
let parsed_input = serde_json::from_value(input)?;
|
||||||
|
Ok(self.0.initial_title(parsed_input))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn needs_authorization(&self, input: serde_json::Value, cx: &mut App) -> Result<bool> {
|
fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result<serde_json::Value> {
|
||||||
let parsed_input: Result<T::Input> = serde_json::from_value(input).map_err(Into::into);
|
let mut json = serde_json::to_value(self.0.input_schema())?;
|
||||||
match parsed_input {
|
adapt_schema_to_format(&mut json, format)?;
|
||||||
Ok(input) => Ok(self.0.needs_authorization(input, cx)),
|
Ok(json)
|
||||||
Err(error) => Err(anyhow!(error)),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn run(
|
fn run(
|
||||||
|
@ -801,22 +794,16 @@ impl AgentResponseEventStream {
|
||||||
|
|
||||||
fn authorize_tool_call(
|
fn authorize_tool_call(
|
||||||
&self,
|
&self,
|
||||||
tool_use: &LanguageModelToolUse,
|
id: &LanguageModelToolUseId,
|
||||||
|
title: String,
|
||||||
|
kind: acp::ToolKind,
|
||||||
|
input: serde_json::Value,
|
||||||
) -> impl use<> + Future<Output = Result<()>> {
|
) -> impl use<> + Future<Output = Result<()>> {
|
||||||
let (response_tx, response_rx) = oneshot::channel();
|
let (response_tx, response_rx) = oneshot::channel();
|
||||||
self.0
|
self.0
|
||||||
.unbounded_send(Ok(AgentResponseEvent::ToolCallAuthorization(
|
.unbounded_send(Ok(AgentResponseEvent::ToolCallAuthorization(
|
||||||
ToolCallAuthorization {
|
ToolCallAuthorization {
|
||||||
tool_call: acp::ToolCall {
|
tool_call: Self::initial_tool_call(id, title, kind, input),
|
||||||
id: acp::ToolCallId(tool_use.id.to_string().into()),
|
|
||||||
title: tool_use.name.to_string(),
|
|
||||||
kind: acp::ToolKind::Other,
|
|
||||||
status: acp::ToolCallStatus::Pending,
|
|
||||||
content: vec![],
|
|
||||||
locations: vec![],
|
|
||||||
raw_input: Some(tool_use.input.clone()),
|
|
||||||
raw_output: None,
|
|
||||||
},
|
|
||||||
options: vec![
|
options: vec![
|
||||||
acp::PermissionOption {
|
acp::PermissionOption {
|
||||||
id: acp::PermissionOptionId("always_allow".into()),
|
id: acp::PermissionOptionId("always_allow".into()),
|
||||||
|
@ -846,19 +833,39 @@ impl AgentResponseEventStream {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn send_tool_call(&self, tool_use: &LanguageModelToolUse, kind: acp::ToolKind) {
|
fn send_tool_call(
|
||||||
|
&self,
|
||||||
|
tool: Option<&Arc<dyn AnyAgentTool>>,
|
||||||
|
tool_use: &LanguageModelToolUse,
|
||||||
|
) {
|
||||||
self.0
|
self.0
|
||||||
.unbounded_send(Ok(AgentResponseEvent::ToolCall(acp::ToolCall {
|
.unbounded_send(Ok(AgentResponseEvent::ToolCall(Self::initial_tool_call(
|
||||||
id: acp::ToolCallId(tool_use.id.to_string().into()),
|
&tool_use.id,
|
||||||
title: tool_use.name.to_string(),
|
tool.and_then(|t| t.initial_title(tool_use.input.clone()).ok())
|
||||||
|
.map(|i| i.into())
|
||||||
|
.unwrap_or_else(|| tool_use.name.to_string()),
|
||||||
|
tool.map(|t| t.kind()).unwrap_or(acp::ToolKind::Other),
|
||||||
|
tool_use.input.clone(),
|
||||||
|
))))
|
||||||
|
.ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn initial_tool_call(
|
||||||
|
id: &LanguageModelToolUseId,
|
||||||
|
title: String,
|
||||||
|
kind: acp::ToolKind,
|
||||||
|
input: serde_json::Value,
|
||||||
|
) -> acp::ToolCall {
|
||||||
|
acp::ToolCall {
|
||||||
|
id: acp::ToolCallId(id.to_string().into()),
|
||||||
|
title,
|
||||||
kind,
|
kind,
|
||||||
status: acp::ToolCallStatus::Pending,
|
status: acp::ToolCallStatus::Pending,
|
||||||
content: vec![],
|
content: vec![],
|
||||||
locations: vec![],
|
locations: vec![],
|
||||||
raw_input: Some(tool_use.input.clone()),
|
raw_input: Some(input),
|
||||||
raw_output: None,
|
raw_output: None,
|
||||||
})))
|
}
|
||||||
.ok();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn send_tool_call_update(
|
fn send_tool_call_update(
|
||||||
|
@ -932,4 +939,39 @@ impl ToolCallEventStream {
|
||||||
diff,
|
diff,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn authorize(
|
||||||
|
&self,
|
||||||
|
title: String,
|
||||||
|
kind: acp::ToolKind,
|
||||||
|
input: serde_json::Value,
|
||||||
|
) -> impl use<> + Future<Output = Result<()>> {
|
||||||
|
self.stream
|
||||||
|
.authorize_tool_call(&self.tool_use_id, title, kind, input)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
pub struct TestToolCallEventStream {
|
||||||
|
stream: ToolCallEventStream,
|
||||||
|
_events_rx: mpsc::UnboundedReceiver<Result<AgentResponseEvent, LanguageModelCompletionError>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
impl TestToolCallEventStream {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
let (events_tx, events_rx) =
|
||||||
|
mpsc::unbounded::<Result<AgentResponseEvent, LanguageModelCompletionError>>();
|
||||||
|
|
||||||
|
let stream = ToolCallEventStream::new("test".into(), AgentResponseEventStream(events_tx));
|
||||||
|
|
||||||
|
Self {
|
||||||
|
stream,
|
||||||
|
_events_rx: events_rx,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn stream(&self) -> ToolCallEventStream {
|
||||||
|
self.stream.clone()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,9 @@
|
||||||
mod edit_file_tool;
|
mod edit_file_tool;
|
||||||
mod find_path_tool;
|
mod find_path_tool;
|
||||||
|
mod read_file_tool;
|
||||||
|
mod thinking_tool;
|
||||||
|
|
||||||
pub use edit_file_tool::*;
|
pub use edit_file_tool::*;
|
||||||
pub use find_path_tool::*;
|
pub use find_path_tool::*;
|
||||||
|
pub use read_file_tool::*;
|
||||||
|
pub use thinking_tool::*;
|
||||||
|
|
|
@ -12,7 +12,6 @@ use project::lsp_store::{FormatTrigger, LspFormatTarget};
|
||||||
use project::{Project, ProjectPath};
|
use project::{Project, ProjectPath};
|
||||||
use schemars::JsonSchema;
|
use schemars::JsonSchema;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use settings::Settings as _;
|
|
||||||
use smol::stream::StreamExt as _;
|
use smol::stream::StreamExt as _;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
@ -112,39 +111,60 @@ impl AgentTool for EditFileTool {
|
||||||
acp::ToolKind::Edit
|
acp::ToolKind::Edit
|
||||||
}
|
}
|
||||||
|
|
||||||
fn needs_authorization(&self, input: Self::Input, cx: &App) -> bool {
|
fn initial_title(&self, input: Self::Input) -> SharedString {
|
||||||
if agent_settings::AgentSettings::get_global(cx).always_allow_tool_actions {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If any path component matches the local settings folder, then this could affect
|
|
||||||
// the editor in ways beyond the project source, so prompt.
|
|
||||||
let local_settings_folder = paths::local_settings_folder_relative_path();
|
|
||||||
let path = Path::new(&input.path);
|
let path = Path::new(&input.path);
|
||||||
|
let mut description = input.display_description.clone();
|
||||||
|
|
||||||
|
// Add context about why confirmation may be needed
|
||||||
|
let local_settings_folder = paths::local_settings_folder_relative_path();
|
||||||
if path
|
if path
|
||||||
.components()
|
.components()
|
||||||
.any(|component| component.as_os_str() == local_settings_folder.as_os_str())
|
.any(|c| c.as_os_str() == local_settings_folder.as_os_str())
|
||||||
{
|
{
|
||||||
return true;
|
description.push_str(" (local settings)");
|
||||||
}
|
} else if let Ok(canonical_path) = std::fs::canonicalize(&input.path) {
|
||||||
|
|
||||||
// It's also possible that the global config dir is configured to be inside the project,
|
|
||||||
// so check for that edge case too.
|
|
||||||
if let Ok(canonical_path) = std::fs::canonicalize(&input.path) {
|
|
||||||
if canonical_path.starts_with(paths::config_dir()) {
|
if canonical_path.starts_with(paths::config_dir()) {
|
||||||
return true;
|
description.push_str(" (global settings)");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if path is inside the global config directory
|
description.into()
|
||||||
// First check if it's already inside project - if not, try to canonicalize
|
|
||||||
let project_path = self.project.read(cx).find_project_path(&input.path, cx);
|
|
||||||
|
|
||||||
// If the path is inside the project, and it's not one of the above edge cases,
|
|
||||||
// then no confirmation is necessary. Otherwise, confirmation is necessary.
|
|
||||||
project_path.is_none()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// todo!
|
||||||
|
// fn needs_authorization(&self, input: Self::Input, cx: &App) -> bool {
|
||||||
|
// if agent_settings::AgentSettings::get_global(cx).always_allow_tool_actions {
|
||||||
|
// return false;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // If any path component matches the local settings folder, then this could affect
|
||||||
|
// // the editor in ways beyond the project source, so prompt.
|
||||||
|
// let local_settings_folder = paths::local_settings_folder_relative_path();
|
||||||
|
// let path = Path::new(&input.path);
|
||||||
|
// if path
|
||||||
|
// .components()
|
||||||
|
// .any(|component| component.as_os_str() == local_settings_folder.as_os_str())
|
||||||
|
// {
|
||||||
|
// return true;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // It's also possible that the global config dir is configured to be inside the project,
|
||||||
|
// // so check for that edge case too.
|
||||||
|
// if let Ok(canonical_path) = std::fs::canonicalize(&input.path) {
|
||||||
|
// if canonical_path.starts_with(paths::config_dir()) {
|
||||||
|
// return true;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // Check if path is inside the global config directory
|
||||||
|
// // First check if it's already inside project - if not, try to canonicalize
|
||||||
|
// let project_path = self.project.read(cx).find_project_path(&input.path, cx);
|
||||||
|
|
||||||
|
// // If the path is inside the project, and it's not one of the above edge cases,
|
||||||
|
// // then no confirmation is necessary. Otherwise, confirmation is necessary.
|
||||||
|
// project_path.is_none()
|
||||||
|
// }
|
||||||
|
|
||||||
fn run(
|
fn run(
|
||||||
self: Arc<Self>,
|
self: Arc<Self>,
|
||||||
input: Self::Input,
|
input: Self::Input,
|
||||||
|
@ -182,6 +202,14 @@ impl AgentTool for EditFileTool {
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let diff = cx.new(|cx| Diff::new(buffer.clone(), cx))?;
|
let diff = cx.new(|cx| Diff::new(buffer.clone(), cx))?;
|
||||||
|
event_stream.send_update(acp::ToolCallUpdateFields {
|
||||||
|
locations: Some(vec![acp::ToolCallLocation {
|
||||||
|
path: project_path.path.to_path_buf(),
|
||||||
|
// todo!
|
||||||
|
line: None
|
||||||
|
}]),
|
||||||
|
..Default::default()
|
||||||
|
});
|
||||||
event_stream.send_diff(diff.clone());
|
event_stream.send_diff(diff.clone());
|
||||||
|
|
||||||
let old_snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot())?;
|
let old_snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot())?;
|
||||||
|
@ -193,20 +221,20 @@ impl AgentTool for EditFileTool {
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
|
|
||||||
let mut events = if matches!(input.mode, EditFileMode::Edit) {
|
let (output, mut events) = if matches!(input.mode, EditFileMode::Edit) {
|
||||||
edit_agent.edit(
|
edit_agent.edit(
|
||||||
buffer.clone(),
|
buffer.clone(),
|
||||||
input.display_description.clone(),
|
input.display_description.clone(),
|
||||||
&request,
|
&request,
|
||||||
cx,
|
cx,
|
||||||
).1
|
)
|
||||||
} else {
|
} else {
|
||||||
edit_agent.overwrite(
|
edit_agent.overwrite(
|
||||||
buffer.clone(),
|
buffer.clone(),
|
||||||
input.display_description.clone(),
|
input.display_description.clone(),
|
||||||
&request,
|
&request,
|
||||||
cx,
|
cx,
|
||||||
).1
|
)
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut hallucinated_old_text = false;
|
let mut hallucinated_old_text = false;
|
||||||
|
@ -234,6 +262,8 @@ impl AgentTool for EditFileTool {
|
||||||
})
|
})
|
||||||
.unwrap_or(false);
|
.unwrap_or(false);
|
||||||
|
|
||||||
|
let _ = output.await?;
|
||||||
|
|
||||||
if format_on_save_enabled {
|
if format_on_save_enabled {
|
||||||
action_log.update(cx, |log, cx| {
|
action_log.update(cx, |log, cx| {
|
||||||
log.buffer_edited(buffer.clone(), cx);
|
log.buffer_edited(buffer.clone(), cx);
|
||||||
|
@ -271,6 +301,8 @@ impl AgentTool for EditFileTool {
|
||||||
})
|
})
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
|
println!("\n\n{}\n\n", unified_diff);
|
||||||
|
|
||||||
diff.update(cx, |diff, cx| {
|
diff.update(cx, |diff, cx| {
|
||||||
diff.finalize(cx);
|
diff.finalize(cx);
|
||||||
}).ok();
|
}).ok();
|
||||||
|
|
|
@ -66,8 +66,8 @@ impl AgentTool for FindPathTool {
|
||||||
acp::ToolKind::Search
|
acp::ToolKind::Search
|
||||||
}
|
}
|
||||||
|
|
||||||
fn needs_authorization(&self, _: Self::Input, _: &App) -> bool {
|
fn initial_title(&self, input: Self::Input) -> SharedString {
|
||||||
false
|
format!("Find paths matching “`{}`”", input.glob).into()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn run(
|
fn run(
|
||||||
|
|
970
crates/agent2/src/tools/read_file_tool.rs
Normal file
970
crates/agent2/src/tools/read_file_tool.rs
Normal file
|
@ -0,0 +1,970 @@
|
||||||
|
use agent_client_protocol::{self as acp};
|
||||||
|
use anyhow::{anyhow, Result};
|
||||||
|
use assistant_tool::{outline, ActionLog};
|
||||||
|
use gpui::{Entity, Task};
|
||||||
|
use indoc::formatdoc;
|
||||||
|
use language::{Anchor, Point};
|
||||||
|
use project::{AgentLocation, Project, WorktreeSettings};
|
||||||
|
use schemars::JsonSchema;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use settings::Settings;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use ui::{App, SharedString};
|
||||||
|
|
||||||
|
use crate::{AgentTool, ToolCallEventStream};
|
||||||
|
|
||||||
|
/// Reads the content of the given file in the project.
|
||||||
|
///
|
||||||
|
/// - Never attempt to read a path that hasn't been previously mentioned.
|
||||||
|
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
||||||
|
pub struct ReadFileToolInput {
|
||||||
|
/// The relative path of the file to read.
|
||||||
|
///
|
||||||
|
/// This path should never be absolute, and the first component
|
||||||
|
/// of the path should always be a root directory in a project.
|
||||||
|
///
|
||||||
|
/// <example>
|
||||||
|
/// If the project has the following root directories:
|
||||||
|
///
|
||||||
|
/// - /a/b/directory1
|
||||||
|
/// - /c/d/directory2
|
||||||
|
///
|
||||||
|
/// If you want to access `file.txt` in `directory1`, you should use the path `directory1/file.txt`.
|
||||||
|
/// If you want to access `file.txt` in `directory2`, you should use the path `directory2/file.txt`.
|
||||||
|
/// </example>
|
||||||
|
pub path: String,
|
||||||
|
|
||||||
|
/// Optional line number to start reading on (1-based index)
|
||||||
|
#[serde(default)]
|
||||||
|
pub start_line: Option<u32>,
|
||||||
|
|
||||||
|
/// Optional line number to end reading on (1-based index, inclusive)
|
||||||
|
#[serde(default)]
|
||||||
|
pub end_line: Option<u32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ReadFileTool {
|
||||||
|
project: Entity<Project>,
|
||||||
|
action_log: Entity<ActionLog>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ReadFileTool {
|
||||||
|
pub fn new(project: Entity<Project>, action_log: Entity<ActionLog>) -> Self {
|
||||||
|
Self {
|
||||||
|
project,
|
||||||
|
action_log,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AgentTool for ReadFileTool {
|
||||||
|
type Input = ReadFileToolInput;
|
||||||
|
|
||||||
|
fn name(&self) -> SharedString {
|
||||||
|
"read_file".into()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn kind(&self) -> acp::ToolKind {
|
||||||
|
acp::ToolKind::Read
|
||||||
|
}
|
||||||
|
|
||||||
|
fn initial_title(&self, input: Self::Input) -> SharedString {
|
||||||
|
let path = &input.path;
|
||||||
|
match (input.start_line, input.end_line) {
|
||||||
|
(Some(start), Some(end)) => {
|
||||||
|
format!(
|
||||||
|
"[Read file `{}` (lines {}-{})](@selection:{}:({}-{}))",
|
||||||
|
path, start, end, path, start, end
|
||||||
|
)
|
||||||
|
}
|
||||||
|
(Some(start), None) => {
|
||||||
|
format!(
|
||||||
|
"[Read file `{}` (from line {})](@selection:{}:({}-{}))",
|
||||||
|
path, start, path, start, start
|
||||||
|
)
|
||||||
|
}
|
||||||
|
_ => format!("[Read file `{}`](@file:{})", path, path),
|
||||||
|
}
|
||||||
|
.into()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run(
|
||||||
|
self: Arc<Self>,
|
||||||
|
input: Self::Input,
|
||||||
|
event_stream: ToolCallEventStream,
|
||||||
|
cx: &mut App,
|
||||||
|
) -> Task<Result<String>> {
|
||||||
|
let Some(project_path) = self.project.read(cx).find_project_path(&input.path, cx) else {
|
||||||
|
return Task::ready(Err(anyhow!("Path {} not found in project", &input.path)));
|
||||||
|
};
|
||||||
|
|
||||||
|
// Error out if this path is either excluded or private in global settings
|
||||||
|
let global_settings = WorktreeSettings::get_global(cx);
|
||||||
|
if global_settings.is_path_excluded(&project_path.path) {
|
||||||
|
return Task::ready(Err(anyhow!(
|
||||||
|
"Cannot read file because its path matches the global `file_scan_exclusions` setting: {}",
|
||||||
|
&input.path
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
|
if global_settings.is_path_private(&project_path.path) {
|
||||||
|
return Task::ready(Err(anyhow!(
|
||||||
|
"Cannot read file because its path matches the global `private_files` setting: {}",
|
||||||
|
&input.path
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Error out if this path is either excluded or private in worktree settings
|
||||||
|
let worktree_settings = WorktreeSettings::get(Some((&project_path).into()), cx);
|
||||||
|
if worktree_settings.is_path_excluded(&project_path.path) {
|
||||||
|
return Task::ready(Err(anyhow!(
|
||||||
|
"Cannot read file because its path matches the worktree `file_scan_exclusions` setting: {}",
|
||||||
|
&input.path
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
|
if worktree_settings.is_path_private(&project_path.path) {
|
||||||
|
return Task::ready(Err(anyhow!(
|
||||||
|
"Cannot read file because its path matches the worktree `private_files` setting: {}",
|
||||||
|
&input.path
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
|
let file_path = input.path.clone();
|
||||||
|
|
||||||
|
event_stream.send_update(acp::ToolCallUpdateFields {
|
||||||
|
locations: Some(vec![acp::ToolCallLocation {
|
||||||
|
path: project_path.path.to_path_buf(),
|
||||||
|
line: input.start_line,
|
||||||
|
// TODO (tracked): use full range
|
||||||
|
}]),
|
||||||
|
..Default::default()
|
||||||
|
});
|
||||||
|
|
||||||
|
// TODO (tracked): images
|
||||||
|
// if image_store::is_image_file(&self.project, &project_path, cx) {
|
||||||
|
// let model = &self.thread.read(cx).selected_model;
|
||||||
|
|
||||||
|
// if !model.supports_images() {
|
||||||
|
// return Task::ready(Err(anyhow!(
|
||||||
|
// "Attempted to read an image, but Zed doesn't currently support sending images to {}.",
|
||||||
|
// model.name().0
|
||||||
|
// )))
|
||||||
|
// .into();
|
||||||
|
// }
|
||||||
|
|
||||||
|
// return cx.spawn(async move |cx| -> Result<ToolResultOutput> {
|
||||||
|
// let image_entity: Entity<ImageItem> = cx
|
||||||
|
// .update(|cx| {
|
||||||
|
// self.project.update(cx, |project, cx| {
|
||||||
|
// project.open_image(project_path.clone(), cx)
|
||||||
|
// })
|
||||||
|
// })?
|
||||||
|
// .await?;
|
||||||
|
|
||||||
|
// let image =
|
||||||
|
// image_entity.read_with(cx, |image_item, _| Arc::clone(&image_item.image))?;
|
||||||
|
|
||||||
|
// let language_model_image = cx
|
||||||
|
// .update(|cx| LanguageModelImage::from_image(image, cx))?
|
||||||
|
// .await
|
||||||
|
// .context("processing image")?;
|
||||||
|
|
||||||
|
// Ok(ToolResultOutput {
|
||||||
|
// content: ToolResultContent::Image(language_model_image),
|
||||||
|
// output: None,
|
||||||
|
// })
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
|
||||||
|
let project = self.project.clone();
|
||||||
|
let action_log = self.action_log.clone();
|
||||||
|
|
||||||
|
cx.spawn(async move |cx| {
|
||||||
|
let buffer = cx
|
||||||
|
.update(|cx| {
|
||||||
|
project.update(cx, |project, cx| project.open_buffer(project_path, cx))
|
||||||
|
})?
|
||||||
|
.await?;
|
||||||
|
if buffer.read_with(cx, |buffer, _| {
|
||||||
|
buffer
|
||||||
|
.file()
|
||||||
|
.as_ref()
|
||||||
|
.map_or(true, |file| !file.disk_state().exists())
|
||||||
|
})? {
|
||||||
|
anyhow::bail!("{file_path} not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
project.update(cx, |project, cx| {
|
||||||
|
project.set_agent_location(
|
||||||
|
Some(AgentLocation {
|
||||||
|
buffer: buffer.downgrade(),
|
||||||
|
position: Anchor::MIN,
|
||||||
|
}),
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
})?;
|
||||||
|
|
||||||
|
// Check if specific line ranges are provided
|
||||||
|
if input.start_line.is_some() || input.end_line.is_some() {
|
||||||
|
let mut anchor = None;
|
||||||
|
let result = buffer.read_with(cx, |buffer, _cx| {
|
||||||
|
let text = buffer.text();
|
||||||
|
// .max(1) because despite instructions to be 1-indexed, sometimes the model passes 0.
|
||||||
|
let start = input.start_line.unwrap_or(1).max(1);
|
||||||
|
let start_row = start - 1;
|
||||||
|
if start_row <= buffer.max_point().row {
|
||||||
|
let column = buffer.line_indent_for_row(start_row).raw_len();
|
||||||
|
anchor = Some(buffer.anchor_before(Point::new(start_row, column)));
|
||||||
|
}
|
||||||
|
|
||||||
|
let lines = text.split('\n').skip(start_row as usize);
|
||||||
|
if let Some(end) = input.end_line {
|
||||||
|
let count = end.saturating_sub(start).saturating_add(1); // Ensure at least 1 line
|
||||||
|
itertools::intersperse(lines.take(count as usize), "\n").collect::<String>()
|
||||||
|
} else {
|
||||||
|
itertools::intersperse(lines, "\n").collect::<String>()
|
||||||
|
}
|
||||||
|
})?;
|
||||||
|
|
||||||
|
action_log.update(cx, |log, cx| {
|
||||||
|
log.buffer_read(buffer.clone(), cx);
|
||||||
|
})?;
|
||||||
|
|
||||||
|
if let Some(anchor) = anchor {
|
||||||
|
project.update(cx, |project, cx| {
|
||||||
|
project.set_agent_location(
|
||||||
|
Some(AgentLocation {
|
||||||
|
buffer: buffer.downgrade(),
|
||||||
|
position: anchor,
|
||||||
|
}),
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(result)
|
||||||
|
} else {
|
||||||
|
// No line ranges specified, so check file size to see if it's too big.
|
||||||
|
let file_size = buffer.read_with(cx, |buffer, _cx| buffer.text().len())?;
|
||||||
|
|
||||||
|
if file_size <= outline::AUTO_OUTLINE_SIZE {
|
||||||
|
// File is small enough, so return its contents.
|
||||||
|
let result = buffer.read_with(cx, |buffer, _cx| buffer.text())?;
|
||||||
|
|
||||||
|
action_log.update(cx, |log, cx| {
|
||||||
|
log.buffer_read(buffer, cx);
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Ok(result)
|
||||||
|
} else {
|
||||||
|
// File is too big, so return the outline
|
||||||
|
// and a suggestion to read again with line numbers.
|
||||||
|
let outline =
|
||||||
|
outline::file_outline(project, file_path, action_log, None, cx).await?;
|
||||||
|
Ok(formatdoc! {"
|
||||||
|
This file was too big to read all at once.
|
||||||
|
|
||||||
|
Here is an outline of its symbols:
|
||||||
|
|
||||||
|
{outline}
|
||||||
|
|
||||||
|
Using the line numbers in this outline, you can call this tool again
|
||||||
|
while specifying the start_line and end_line fields to see the
|
||||||
|
implementations of symbols in the outline.
|
||||||
|
|
||||||
|
Alternatively, you can fall back to the `grep` tool (if available)
|
||||||
|
to search the file for specific content."
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use crate::TestToolCallEventStream;
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
use gpui::{AppContext, TestAppContext, UpdateGlobal as _};
|
||||||
|
use language::{tree_sitter_rust, Language, LanguageConfig, LanguageMatcher};
|
||||||
|
use project::{FakeFs, Project};
|
||||||
|
use serde_json::json;
|
||||||
|
use settings::SettingsStore;
|
||||||
|
use util::path;
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
async fn test_read_nonexistent_file(cx: &mut TestAppContext) {
|
||||||
|
init_test(cx);
|
||||||
|
|
||||||
|
let fs = FakeFs::new(cx.executor());
|
||||||
|
fs.insert_tree(path!("/root"), json!({})).await;
|
||||||
|
let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
|
||||||
|
let action_log = cx.new(|_| ActionLog::new(project.clone()));
|
||||||
|
let tool = Arc::new(ReadFileTool::new(project, action_log));
|
||||||
|
let event_stream = TestToolCallEventStream::new();
|
||||||
|
|
||||||
|
let result = cx
|
||||||
|
.update(|cx| {
|
||||||
|
let input = ReadFileToolInput {
|
||||||
|
path: "root/nonexistent_file.txt".to_string(),
|
||||||
|
start_line: None,
|
||||||
|
end_line: None,
|
||||||
|
};
|
||||||
|
tool.run(input, event_stream.stream(), cx)
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
assert_eq!(
|
||||||
|
result.unwrap_err().to_string(),
|
||||||
|
"root/nonexistent_file.txt not found"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
#[gpui::test]
|
||||||
|
async fn test_read_small_file(cx: &mut TestAppContext) {
|
||||||
|
init_test(cx);
|
||||||
|
|
||||||
|
let fs = FakeFs::new(cx.executor());
|
||||||
|
fs.insert_tree(
|
||||||
|
path!("/root"),
|
||||||
|
json!({
|
||||||
|
"small_file.txt": "This is a small file content"
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
|
||||||
|
let action_log = cx.new(|_| ActionLog::new(project.clone()));
|
||||||
|
let tool = Arc::new(ReadFileTool::new(project, action_log));
|
||||||
|
let event_stream = TestToolCallEventStream::new();
|
||||||
|
let result = cx
|
||||||
|
.update(|cx| {
|
||||||
|
let input = ReadFileToolInput {
|
||||||
|
path: "root/small_file.txt".into(),
|
||||||
|
start_line: None,
|
||||||
|
end_line: None,
|
||||||
|
};
|
||||||
|
tool.run(input, event_stream.stream(), cx)
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
assert_eq!(result.unwrap(), "This is a small file content");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
async fn test_read_large_file(cx: &mut TestAppContext) {
|
||||||
|
init_test(cx);
|
||||||
|
|
||||||
|
let fs = FakeFs::new(cx.executor());
|
||||||
|
fs.insert_tree(
|
||||||
|
path!("/root"),
|
||||||
|
json!({
|
||||||
|
"large_file.rs": (0..1000).map(|i| format!("struct Test{} {{\n a: u32,\n b: usize,\n}}", i)).collect::<Vec<_>>().join("\n")
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
|
||||||
|
let language_registry = project.read_with(cx, |project, _| project.languages().clone());
|
||||||
|
language_registry.add(Arc::new(rust_lang()));
|
||||||
|
let action_log = cx.new(|_| ActionLog::new(project.clone()));
|
||||||
|
let tool = Arc::new(ReadFileTool::new(project, action_log));
|
||||||
|
let event_stream = TestToolCallEventStream::new();
|
||||||
|
let content = cx
|
||||||
|
.update(|cx| {
|
||||||
|
let input = ReadFileToolInput {
|
||||||
|
path: "root/large_file.rs".into(),
|
||||||
|
start_line: None,
|
||||||
|
end_line: None,
|
||||||
|
};
|
||||||
|
tool.clone().run(input, event_stream.stream(), cx)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
content.lines().skip(4).take(6).collect::<Vec<_>>(),
|
||||||
|
vec![
|
||||||
|
"struct Test0 [L1-4]",
|
||||||
|
" a [L2]",
|
||||||
|
" b [L3]",
|
||||||
|
"struct Test1 [L5-8]",
|
||||||
|
" a [L6]",
|
||||||
|
" b [L7]",
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
let result = cx
|
||||||
|
.update(|cx| {
|
||||||
|
let input = ReadFileToolInput {
|
||||||
|
path: "root/large_file.rs".into(),
|
||||||
|
start_line: None,
|
||||||
|
end_line: None,
|
||||||
|
};
|
||||||
|
tool.run(input, event_stream.stream(), cx)
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
let content = result.unwrap();
|
||||||
|
let expected_content = (0..1000)
|
||||||
|
.flat_map(|i| {
|
||||||
|
vec![
|
||||||
|
format!("struct Test{} [L{}-{}]", i, i * 4 + 1, i * 4 + 4),
|
||||||
|
format!(" a [L{}]", i * 4 + 2),
|
||||||
|
format!(" b [L{}]", i * 4 + 3),
|
||||||
|
]
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
pretty_assertions::assert_eq!(
|
||||||
|
content
|
||||||
|
.lines()
|
||||||
|
.skip(4)
|
||||||
|
.take(expected_content.len())
|
||||||
|
.collect::<Vec<_>>(),
|
||||||
|
expected_content
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
async fn test_read_file_with_line_range(cx: &mut TestAppContext) {
|
||||||
|
init_test(cx);
|
||||||
|
|
||||||
|
let fs = FakeFs::new(cx.executor());
|
||||||
|
fs.insert_tree(
|
||||||
|
path!("/root"),
|
||||||
|
json!({
|
||||||
|
"multiline.txt": "Line 1\nLine 2\nLine 3\nLine 4\nLine 5"
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
|
||||||
|
|
||||||
|
let action_log = cx.new(|_| ActionLog::new(project.clone()));
|
||||||
|
let tool = Arc::new(ReadFileTool::new(project, action_log));
|
||||||
|
let event_stream = TestToolCallEventStream::new();
|
||||||
|
let result = cx
|
||||||
|
.update(|cx| {
|
||||||
|
let input = ReadFileToolInput {
|
||||||
|
path: "root/multiline.txt".to_string(),
|
||||||
|
start_line: Some(2),
|
||||||
|
end_line: Some(4),
|
||||||
|
};
|
||||||
|
tool.run(input, event_stream.stream(), cx)
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
assert_eq!(result.unwrap(), "Line 2\nLine 3\nLine 4");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
async fn test_read_file_line_range_edge_cases(cx: &mut TestAppContext) {
|
||||||
|
init_test(cx);
|
||||||
|
|
||||||
|
let fs = FakeFs::new(cx.executor());
|
||||||
|
fs.insert_tree(
|
||||||
|
path!("/root"),
|
||||||
|
json!({
|
||||||
|
"multiline.txt": "Line 1\nLine 2\nLine 3\nLine 4\nLine 5"
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
|
||||||
|
let action_log = cx.new(|_| ActionLog::new(project.clone()));
|
||||||
|
let tool = Arc::new(ReadFileTool::new(project, action_log));
|
||||||
|
let event_stream = TestToolCallEventStream::new();
|
||||||
|
|
||||||
|
// start_line of 0 should be treated as 1
|
||||||
|
let result = cx
|
||||||
|
.update(|cx| {
|
||||||
|
let input = ReadFileToolInput {
|
||||||
|
path: "root/multiline.txt".to_string(),
|
||||||
|
start_line: Some(0),
|
||||||
|
end_line: Some(2),
|
||||||
|
};
|
||||||
|
tool.clone().run(input, event_stream.stream(), cx)
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
assert_eq!(result.unwrap(), "Line 1\nLine 2");
|
||||||
|
|
||||||
|
// end_line of 0 should result in at least 1 line
|
||||||
|
let result = cx
|
||||||
|
.update(|cx| {
|
||||||
|
let input = ReadFileToolInput {
|
||||||
|
path: "root/multiline.txt".to_string(),
|
||||||
|
start_line: Some(1),
|
||||||
|
end_line: Some(0),
|
||||||
|
};
|
||||||
|
tool.clone().run(input, event_stream.stream(), cx)
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
assert_eq!(result.unwrap(), "Line 1");
|
||||||
|
|
||||||
|
// when start_line > end_line, should still return at least 1 line
|
||||||
|
let result = cx
|
||||||
|
.update(|cx| {
|
||||||
|
let input = ReadFileToolInput {
|
||||||
|
path: "root/multiline.txt".to_string(),
|
||||||
|
start_line: Some(3),
|
||||||
|
end_line: Some(2),
|
||||||
|
};
|
||||||
|
tool.clone().run(input, event_stream.stream(), cx)
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
assert_eq!(result.unwrap(), "Line 3");
|
||||||
|
}
|
||||||
|
|
||||||
|
fn init_test(cx: &mut TestAppContext) {
|
||||||
|
cx.update(|cx| {
|
||||||
|
let settings_store = SettingsStore::test(cx);
|
||||||
|
cx.set_global(settings_store);
|
||||||
|
language::init(cx);
|
||||||
|
Project::init_settings(cx);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn rust_lang() -> Language {
|
||||||
|
Language::new(
|
||||||
|
LanguageConfig {
|
||||||
|
name: "Rust".into(),
|
||||||
|
matcher: LanguageMatcher {
|
||||||
|
path_suffixes: vec!["rs".to_string()],
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
Some(tree_sitter_rust::LANGUAGE.into()),
|
||||||
|
)
|
||||||
|
.with_outline_query(
|
||||||
|
r#"
|
||||||
|
(line_comment) @annotation
|
||||||
|
|
||||||
|
(struct_item
|
||||||
|
"struct" @context
|
||||||
|
name: (_) @name) @item
|
||||||
|
(enum_item
|
||||||
|
"enum" @context
|
||||||
|
name: (_) @name) @item
|
||||||
|
(enum_variant
|
||||||
|
name: (_) @name) @item
|
||||||
|
(field_declaration
|
||||||
|
name: (_) @name) @item
|
||||||
|
(impl_item
|
||||||
|
"impl" @context
|
||||||
|
trait: (_)? @name
|
||||||
|
"for"? @context
|
||||||
|
type: (_) @name
|
||||||
|
body: (_ "{" (_)* "}")) @item
|
||||||
|
(function_item
|
||||||
|
"fn" @context
|
||||||
|
name: (_) @name) @item
|
||||||
|
(mod_item
|
||||||
|
"mod" @context
|
||||||
|
name: (_) @name) @item
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
async fn test_read_file_security(cx: &mut TestAppContext) {
|
||||||
|
init_test(cx);
|
||||||
|
|
||||||
|
let fs = FakeFs::new(cx.executor());
|
||||||
|
|
||||||
|
fs.insert_tree(
|
||||||
|
path!("/"),
|
||||||
|
json!({
|
||||||
|
"project_root": {
|
||||||
|
"allowed_file.txt": "This file is in the project",
|
||||||
|
".mysecrets": "SECRET_KEY=abc123",
|
||||||
|
".secretdir": {
|
||||||
|
"config": "special configuration"
|
||||||
|
},
|
||||||
|
".mymetadata": "custom metadata",
|
||||||
|
"subdir": {
|
||||||
|
"normal_file.txt": "Normal file content",
|
||||||
|
"special.privatekey": "private key content",
|
||||||
|
"data.mysensitive": "sensitive data"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"outside_project": {
|
||||||
|
"sensitive_file.txt": "This file is outside the project"
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
cx.update(|cx| {
|
||||||
|
use gpui::UpdateGlobal;
|
||||||
|
use project::WorktreeSettings;
|
||||||
|
use settings::SettingsStore;
|
||||||
|
SettingsStore::update_global(cx, |store, cx| {
|
||||||
|
store.update_user_settings::<WorktreeSettings>(cx, |settings| {
|
||||||
|
settings.file_scan_exclusions = Some(vec![
|
||||||
|
"**/.secretdir".to_string(),
|
||||||
|
"**/.mymetadata".to_string(),
|
||||||
|
]);
|
||||||
|
settings.private_files = Some(vec![
|
||||||
|
"**/.mysecrets".to_string(),
|
||||||
|
"**/*.privatekey".to_string(),
|
||||||
|
"**/*.mysensitive".to_string(),
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
let project = Project::test(fs.clone(), [path!("/project_root").as_ref()], cx).await;
|
||||||
|
let action_log = cx.new(|_| ActionLog::new(project.clone()));
|
||||||
|
let tool = Arc::new(ReadFileTool::new(project, action_log));
|
||||||
|
let event_stream = TestToolCallEventStream::new();
|
||||||
|
|
||||||
|
// Reading a file outside the project worktree should fail
|
||||||
|
let result = cx
|
||||||
|
.update(|cx| {
|
||||||
|
let input = ReadFileToolInput {
|
||||||
|
path: "/outside_project/sensitive_file.txt".to_string(),
|
||||||
|
start_line: None,
|
||||||
|
end_line: None,
|
||||||
|
};
|
||||||
|
tool.clone().run(input, event_stream.stream(), cx)
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
assert!(
|
||||||
|
result.is_err(),
|
||||||
|
"read_file_tool should error when attempting to read an absolute path outside a worktree"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Reading a file within the project should succeed
|
||||||
|
let result = cx
|
||||||
|
.update(|cx| {
|
||||||
|
let input = ReadFileToolInput {
|
||||||
|
path: "project_root/allowed_file.txt".to_string(),
|
||||||
|
start_line: None,
|
||||||
|
end_line: None,
|
||||||
|
};
|
||||||
|
tool.clone().run(input, event_stream.stream(), cx)
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
assert!(
|
||||||
|
result.is_ok(),
|
||||||
|
"read_file_tool should be able to read files inside worktrees"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Reading files that match file_scan_exclusions should fail
|
||||||
|
let result = cx
|
||||||
|
.update(|cx| {
|
||||||
|
let input = ReadFileToolInput {
|
||||||
|
path: "project_root/.secretdir/config".to_string(),
|
||||||
|
start_line: None,
|
||||||
|
end_line: None,
|
||||||
|
};
|
||||||
|
tool.clone().run(input, event_stream.stream(), cx)
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
assert!(
|
||||||
|
result.is_err(),
|
||||||
|
"read_file_tool should error when attempting to read files in .secretdir (file_scan_exclusions)"
|
||||||
|
);
|
||||||
|
|
||||||
|
let result = cx
|
||||||
|
.update(|cx| {
|
||||||
|
let input = ReadFileToolInput {
|
||||||
|
path: "project_root/.mymetadata".to_string(),
|
||||||
|
start_line: None,
|
||||||
|
end_line: None,
|
||||||
|
};
|
||||||
|
tool.clone().run(input, event_stream.stream(), cx)
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
assert!(
|
||||||
|
result.is_err(),
|
||||||
|
"read_file_tool should error when attempting to read .mymetadata files (file_scan_exclusions)"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Reading private files should fail
|
||||||
|
let result = cx
|
||||||
|
.update(|cx| {
|
||||||
|
let input = ReadFileToolInput {
|
||||||
|
path: "project_root/.mysecrets".to_string(),
|
||||||
|
start_line: None,
|
||||||
|
end_line: None,
|
||||||
|
};
|
||||||
|
tool.clone().run(input, event_stream.stream(), cx)
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
assert!(
|
||||||
|
result.is_err(),
|
||||||
|
"read_file_tool should error when attempting to read .mysecrets (private_files)"
|
||||||
|
);
|
||||||
|
|
||||||
|
let result = cx
|
||||||
|
.update(|cx| {
|
||||||
|
let input = ReadFileToolInput {
|
||||||
|
path: "project_root/subdir/special.privatekey".to_string(),
|
||||||
|
start_line: None,
|
||||||
|
end_line: None,
|
||||||
|
};
|
||||||
|
tool.clone().run(input, event_stream.stream(), cx)
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
assert!(
|
||||||
|
result.is_err(),
|
||||||
|
"read_file_tool should error when attempting to read .privatekey files (private_files)"
|
||||||
|
);
|
||||||
|
|
||||||
|
let result = cx
|
||||||
|
.update(|cx| {
|
||||||
|
let input = ReadFileToolInput {
|
||||||
|
path: "project_root/subdir/data.mysensitive".to_string(),
|
||||||
|
start_line: None,
|
||||||
|
end_line: None,
|
||||||
|
};
|
||||||
|
tool.clone().run(input, event_stream.stream(), cx)
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
assert!(
|
||||||
|
result.is_err(),
|
||||||
|
"read_file_tool should error when attempting to read .mysensitive files (private_files)"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Reading a normal file should still work, even with private_files configured
|
||||||
|
let result = cx
|
||||||
|
.update(|cx| {
|
||||||
|
let input = ReadFileToolInput {
|
||||||
|
path: "project_root/subdir/normal_file.txt".to_string(),
|
||||||
|
start_line: None,
|
||||||
|
end_line: None,
|
||||||
|
};
|
||||||
|
tool.clone().run(input, event_stream.stream(), cx)
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
assert!(result.is_ok(), "Should be able to read normal files");
|
||||||
|
assert_eq!(result.unwrap(), "Normal file content");
|
||||||
|
|
||||||
|
// Path traversal attempts with .. should fail
|
||||||
|
let result = cx
|
||||||
|
.update(|cx| {
|
||||||
|
let input = ReadFileToolInput {
|
||||||
|
path: "project_root/../outside_project/sensitive_file.txt".to_string(),
|
||||||
|
start_line: None,
|
||||||
|
end_line: None,
|
||||||
|
};
|
||||||
|
tool.run(input, event_stream.stream(), cx)
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
assert!(
|
||||||
|
result.is_err(),
|
||||||
|
"read_file_tool should error when attempting to read a relative path that resolves to outside a worktree"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
async fn test_read_file_with_multiple_worktree_settings(cx: &mut TestAppContext) {
|
||||||
|
init_test(cx);
|
||||||
|
|
||||||
|
let fs = FakeFs::new(cx.executor());
|
||||||
|
|
||||||
|
// Create first worktree with its own private_files setting
|
||||||
|
fs.insert_tree(
|
||||||
|
path!("/worktree1"),
|
||||||
|
json!({
|
||||||
|
"src": {
|
||||||
|
"main.rs": "fn main() { println!(\"Hello from worktree1\"); }",
|
||||||
|
"secret.rs": "const API_KEY: &str = \"secret_key_1\";",
|
||||||
|
"config.toml": "[database]\nurl = \"postgres://localhost/db1\""
|
||||||
|
},
|
||||||
|
"tests": {
|
||||||
|
"test.rs": "mod tests { fn test_it() {} }",
|
||||||
|
"fixture.sql": "CREATE TABLE users (id INT, name VARCHAR(255));"
|
||||||
|
},
|
||||||
|
".zed": {
|
||||||
|
"settings.json": r#"{
|
||||||
|
"file_scan_exclusions": ["**/fixture.*"],
|
||||||
|
"private_files": ["**/secret.rs", "**/config.toml"]
|
||||||
|
}"#
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
// Create second worktree with different private_files setting
|
||||||
|
fs.insert_tree(
|
||||||
|
path!("/worktree2"),
|
||||||
|
json!({
|
||||||
|
"lib": {
|
||||||
|
"public.js": "export function greet() { return 'Hello from worktree2'; }",
|
||||||
|
"private.js": "const SECRET_TOKEN = \"private_token_2\";",
|
||||||
|
"data.json": "{\"api_key\": \"json_secret_key\"}"
|
||||||
|
},
|
||||||
|
"docs": {
|
||||||
|
"README.md": "# Public Documentation",
|
||||||
|
"internal.md": "# Internal Secrets and Configuration"
|
||||||
|
},
|
||||||
|
".zed": {
|
||||||
|
"settings.json": r#"{
|
||||||
|
"file_scan_exclusions": ["**/internal.*"],
|
||||||
|
"private_files": ["**/private.js", "**/data.json"]
|
||||||
|
}"#
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
// Set global settings
|
||||||
|
cx.update(|cx| {
|
||||||
|
SettingsStore::update_global(cx, |store, cx| {
|
||||||
|
store.update_user_settings::<WorktreeSettings>(cx, |settings| {
|
||||||
|
settings.file_scan_exclusions =
|
||||||
|
Some(vec!["**/.git".to_string(), "**/node_modules".to_string()]);
|
||||||
|
settings.private_files = Some(vec!["**/.env".to_string()]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
let project = Project::test(
|
||||||
|
fs.clone(),
|
||||||
|
[path!("/worktree1").as_ref(), path!("/worktree2").as_ref()],
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let action_log = cx.new(|_| ActionLog::new(project.clone()));
|
||||||
|
let tool = Arc::new(ReadFileTool::new(project.clone(), action_log.clone()));
|
||||||
|
let event_stream = TestToolCallEventStream::new();
|
||||||
|
|
||||||
|
// Test reading allowed files in worktree1
|
||||||
|
let result = cx
|
||||||
|
.update(|cx| {
|
||||||
|
let input = ReadFileToolInput {
|
||||||
|
path: "worktree1/src/main.rs".to_string(),
|
||||||
|
start_line: None,
|
||||||
|
end_line: None,
|
||||||
|
};
|
||||||
|
tool.clone().run(input, event_stream.stream(), cx)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(result, "fn main() { println!(\"Hello from worktree1\"); }");
|
||||||
|
|
||||||
|
// Test reading private file in worktree1 should fail
|
||||||
|
let result = cx
|
||||||
|
.update(|cx| {
|
||||||
|
let input = ReadFileToolInput {
|
||||||
|
path: "worktree1/src/secret.rs".to_string(),
|
||||||
|
start_line: None,
|
||||||
|
end_line: None,
|
||||||
|
};
|
||||||
|
tool.clone().run(input, event_stream.stream(), cx)
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
|
||||||
|
assert!(result.is_err());
|
||||||
|
assert!(
|
||||||
|
result
|
||||||
|
.unwrap_err()
|
||||||
|
.to_string()
|
||||||
|
.contains("worktree `private_files` setting"),
|
||||||
|
"Error should mention worktree private_files setting"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Test reading excluded file in worktree1 should fail
|
||||||
|
let result = cx
|
||||||
|
.update(|cx| {
|
||||||
|
let input = ReadFileToolInput {
|
||||||
|
path: "worktree1/tests/fixture.sql".to_string(),
|
||||||
|
start_line: None,
|
||||||
|
end_line: None,
|
||||||
|
};
|
||||||
|
tool.clone().run(input, event_stream.stream(), cx)
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
|
||||||
|
assert!(result.is_err());
|
||||||
|
assert!(
|
||||||
|
result
|
||||||
|
.unwrap_err()
|
||||||
|
.to_string()
|
||||||
|
.contains("worktree `file_scan_exclusions` setting"),
|
||||||
|
"Error should mention worktree file_scan_exclusions setting"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Test reading allowed files in worktree2
|
||||||
|
let result = cx
|
||||||
|
.update(|cx| {
|
||||||
|
let input = ReadFileToolInput {
|
||||||
|
path: "worktree2/lib/public.js".to_string(),
|
||||||
|
start_line: None,
|
||||||
|
end_line: None,
|
||||||
|
};
|
||||||
|
tool.clone().run(input, event_stream.stream(), cx)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
result,
|
||||||
|
"export function greet() { return 'Hello from worktree2'; }"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Test reading private file in worktree2 should fail
|
||||||
|
let result = cx
|
||||||
|
.update(|cx| {
|
||||||
|
let input = ReadFileToolInput {
|
||||||
|
path: "worktree2/lib/private.js".to_string(),
|
||||||
|
start_line: None,
|
||||||
|
end_line: None,
|
||||||
|
};
|
||||||
|
tool.clone().run(input, event_stream.stream(), cx)
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
|
||||||
|
assert!(result.is_err());
|
||||||
|
assert!(
|
||||||
|
result
|
||||||
|
.unwrap_err()
|
||||||
|
.to_string()
|
||||||
|
.contains("worktree `private_files` setting"),
|
||||||
|
"Error should mention worktree private_files setting"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Test reading excluded file in worktree2 should fail
|
||||||
|
let result = cx
|
||||||
|
.update(|cx| {
|
||||||
|
let input = ReadFileToolInput {
|
||||||
|
path: "worktree2/docs/internal.md".to_string(),
|
||||||
|
start_line: None,
|
||||||
|
end_line: None,
|
||||||
|
};
|
||||||
|
tool.clone().run(input, event_stream.stream(), cx)
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
|
||||||
|
assert!(result.is_err());
|
||||||
|
assert!(
|
||||||
|
result
|
||||||
|
.unwrap_err()
|
||||||
|
.to_string()
|
||||||
|
.contains("worktree `file_scan_exclusions` setting"),
|
||||||
|
"Error should mention worktree file_scan_exclusions setting"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Test that files allowed in one worktree but not in another are handled correctly
|
||||||
|
// (e.g., config.toml is private in worktree1 but doesn't exist in worktree2)
|
||||||
|
let result = cx
|
||||||
|
.update(|cx| {
|
||||||
|
let input = ReadFileToolInput {
|
||||||
|
path: "worktree1/src/config.toml".to_string(),
|
||||||
|
start_line: None,
|
||||||
|
end_line: None,
|
||||||
|
};
|
||||||
|
tool.clone().run(input, event_stream.stream(), cx)
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
|
||||||
|
assert!(result.is_err());
|
||||||
|
assert!(
|
||||||
|
result
|
||||||
|
.unwrap_err()
|
||||||
|
.to_string()
|
||||||
|
.contains("worktree `private_files` setting"),
|
||||||
|
"Config.toml should be blocked by worktree1's private_files setting"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
48
crates/agent2/src/tools/thinking_tool.rs
Normal file
48
crates/agent2/src/tools/thinking_tool.rs
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
use agent_client_protocol as acp;
|
||||||
|
use anyhow::Result;
|
||||||
|
use gpui::{App, SharedString, Task};
|
||||||
|
use schemars::JsonSchema;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use crate::{AgentTool, ToolCallEventStream};
|
||||||
|
|
||||||
|
/// A tool for thinking through problems, brainstorming ideas, or planning without executing any actions.
|
||||||
|
/// Use this tool when you need to work through complex problems, develop strategies, or outline approaches before taking action.
|
||||||
|
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
||||||
|
pub struct ThinkingToolInput {
|
||||||
|
/// Content to think about. This should be a description of what to think about or
|
||||||
|
/// a problem to solve.
|
||||||
|
content: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ThinkingTool;
|
||||||
|
|
||||||
|
impl AgentTool for ThinkingTool {
|
||||||
|
type Input = ThinkingToolInput;
|
||||||
|
|
||||||
|
fn name(&self) -> SharedString {
|
||||||
|
"thinking".into()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn kind(&self) -> acp::ToolKind {
|
||||||
|
acp::ToolKind::Think
|
||||||
|
}
|
||||||
|
|
||||||
|
fn initial_title(&self, _input: Self::Input) -> SharedString {
|
||||||
|
"Thinking".into()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run(
|
||||||
|
self: Arc<Self>,
|
||||||
|
input: Self::Input,
|
||||||
|
event_stream: ToolCallEventStream,
|
||||||
|
_cx: &mut App,
|
||||||
|
) -> Task<Result<String>> {
|
||||||
|
event_stream.send_update(acp::ToolCallUpdateFields {
|
||||||
|
content: Some(vec![input.content.into()]),
|
||||||
|
..Default::default()
|
||||||
|
});
|
||||||
|
Task::ready(Ok("Finished thinking.".to_string()))
|
||||||
|
}
|
||||||
|
}
|
|
@ -45,6 +45,11 @@ impl<T> MessageHistory<T> {
|
||||||
None
|
None
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
pub fn items(&self) -> &[T] {
|
||||||
|
&self.items
|
||||||
|
}
|
||||||
}
|
}
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
|
|
|
@ -31,7 +31,7 @@ use language::{Buffer, Language};
|
||||||
use markdown::{HeadingLevelStyles, Markdown, MarkdownElement, MarkdownStyle};
|
use markdown::{HeadingLevelStyles, Markdown, MarkdownElement, MarkdownStyle};
|
||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
use project::Project;
|
use project::Project;
|
||||||
use settings::Settings as _;
|
use settings::{Settings as _, SettingsStore};
|
||||||
use text::{Anchor, BufferSnapshot};
|
use text::{Anchor, BufferSnapshot};
|
||||||
use theme::ThemeSettings;
|
use theme::ThemeSettings;
|
||||||
use ui::{
|
use ui::{
|
||||||
|
@ -80,6 +80,7 @@ pub struct AcpThreadView {
|
||||||
editor_expanded: bool,
|
editor_expanded: bool,
|
||||||
message_history: Rc<RefCell<MessageHistory<Vec<acp::ContentBlock>>>>,
|
message_history: Rc<RefCell<MessageHistory<Vec<acp::ContentBlock>>>>,
|
||||||
_cancel_task: Option<Task<()>>,
|
_cancel_task: Option<Task<()>>,
|
||||||
|
_subscriptions: [Subscription; 1],
|
||||||
}
|
}
|
||||||
|
|
||||||
enum ThreadState {
|
enum ThreadState {
|
||||||
|
@ -178,6 +179,8 @@ impl AcpThreadView {
|
||||||
|
|
||||||
let list_state = ListState::new(0, gpui::ListAlignment::Bottom, px(2048.0));
|
let list_state = ListState::new(0, gpui::ListAlignment::Bottom, px(2048.0));
|
||||||
|
|
||||||
|
let subscription = cx.observe_global_in::<SettingsStore>(window, Self::settings_changed);
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
agent: agent.clone(),
|
agent: agent.clone(),
|
||||||
workspace: workspace.clone(),
|
workspace: workspace.clone(),
|
||||||
|
@ -200,6 +203,7 @@ impl AcpThreadView {
|
||||||
plan_expanded: false,
|
plan_expanded: false,
|
||||||
editor_expanded: false,
|
editor_expanded: false,
|
||||||
message_history,
|
message_history,
|
||||||
|
_subscriptions: [subscription],
|
||||||
_cancel_task: None,
|
_cancel_task: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -377,6 +381,11 @@ impl AcpThreadView {
|
||||||
editor.display_map.update(cx, |map, cx| {
|
editor.display_map.update(cx, |map, cx| {
|
||||||
let snapshot = map.snapshot(cx);
|
let snapshot = map.snapshot(cx);
|
||||||
for (crease_id, crease) in snapshot.crease_snapshot.creases() {
|
for (crease_id, crease) in snapshot.crease_snapshot.creases() {
|
||||||
|
// Skip creases that have been edited out of the message buffer.
|
||||||
|
if !crease.range().start.is_valid(&snapshot.buffer_snapshot) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
if let Some(project_path) =
|
if let Some(project_path) =
|
||||||
self.mention_set.lock().path_for_crease_id(crease_id)
|
self.mention_set.lock().path_for_crease_id(crease_id)
|
||||||
{
|
{
|
||||||
|
@ -704,15 +713,7 @@ impl AcpThreadView {
|
||||||
editor.set_show_code_actions(false, cx);
|
editor.set_show_code_actions(false, cx);
|
||||||
editor.set_show_git_diff_gutter(false, cx);
|
editor.set_show_git_diff_gutter(false, cx);
|
||||||
editor.set_expand_all_diff_hunks(cx);
|
editor.set_expand_all_diff_hunks(cx);
|
||||||
editor.set_text_style_refinement(TextStyleRefinement {
|
editor.set_text_style_refinement(diff_editor_text_style_refinement(cx));
|
||||||
font_size: Some(
|
|
||||||
TextSize::Small
|
|
||||||
.rems(cx)
|
|
||||||
.to_pixels(ThemeSettings::get_global(cx).agent_font_size(cx))
|
|
||||||
.into(),
|
|
||||||
),
|
|
||||||
..Default::default()
|
|
||||||
});
|
|
||||||
editor
|
editor
|
||||||
});
|
});
|
||||||
let entity_id = multibuffer.entity_id();
|
let entity_id = multibuffer.entity_id();
|
||||||
|
@ -2600,6 +2601,15 @@ impl AcpThreadView {
|
||||||
.cursor_default()
|
.cursor_default()
|
||||||
.children(Scrollbar::vertical(self.scrollbar_state.clone()).map(|s| s.auto_hide(cx)))
|
.children(Scrollbar::vertical(self.scrollbar_state.clone()).map(|s| s.auto_hide(cx)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn settings_changed(&mut self, _window: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
for diff_editor in self.diff_editors.values() {
|
||||||
|
diff_editor.update(cx, |diff_editor, cx| {
|
||||||
|
diff_editor.set_text_style_refinement(diff_editor_text_style_refinement(cx));
|
||||||
|
cx.notify();
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Focusable for AcpThreadView {
|
impl Focusable for AcpThreadView {
|
||||||
|
@ -2877,6 +2887,18 @@ fn plan_label_markdown_style(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn diff_editor_text_style_refinement(cx: &mut App) -> TextStyleRefinement {
|
||||||
|
TextStyleRefinement {
|
||||||
|
font_size: Some(
|
||||||
|
TextSize::Small
|
||||||
|
.rems(cx)
|
||||||
|
.to_pixels(ThemeSettings::get_global(cx).agent_font_size(cx))
|
||||||
|
.into(),
|
||||||
|
),
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use agent_client_protocol::SessionId;
|
use agent_client_protocol::SessionId;
|
||||||
|
@ -2884,8 +2906,12 @@ mod tests {
|
||||||
use fs::FakeFs;
|
use fs::FakeFs;
|
||||||
use futures::future::try_join_all;
|
use futures::future::try_join_all;
|
||||||
use gpui::{SemanticVersion, TestAppContext, VisualTestContext};
|
use gpui::{SemanticVersion, TestAppContext, VisualTestContext};
|
||||||
|
use lsp::{CompletionContext, CompletionTriggerKind};
|
||||||
|
use project::CompletionIntent;
|
||||||
use rand::Rng;
|
use rand::Rng;
|
||||||
|
use serde_json::json;
|
||||||
use settings::SettingsStore;
|
use settings::SettingsStore;
|
||||||
|
use util::path;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
@ -2998,6 +3024,109 @@ mod tests {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
async fn test_crease_removal(cx: &mut TestAppContext) {
|
||||||
|
init_test(cx);
|
||||||
|
|
||||||
|
let fs = FakeFs::new(cx.executor());
|
||||||
|
fs.insert_tree("/project", json!({"file": ""})).await;
|
||||||
|
let project = Project::test(fs, [Path::new(path!("/project"))], cx).await;
|
||||||
|
let agent = StubAgentServer::default();
|
||||||
|
let (workspace, cx) =
|
||||||
|
cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
|
||||||
|
let thread_view = cx.update(|window, cx| {
|
||||||
|
cx.new(|cx| {
|
||||||
|
AcpThreadView::new(
|
||||||
|
Rc::new(agent),
|
||||||
|
workspace.downgrade(),
|
||||||
|
project,
|
||||||
|
Rc::new(RefCell::new(MessageHistory::default())),
|
||||||
|
1,
|
||||||
|
None,
|
||||||
|
window,
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
cx.run_until_parked();
|
||||||
|
|
||||||
|
let message_editor = cx.read(|cx| thread_view.read(cx).message_editor.clone());
|
||||||
|
let excerpt_id = message_editor.update(cx, |editor, cx| {
|
||||||
|
editor
|
||||||
|
.buffer()
|
||||||
|
.read(cx)
|
||||||
|
.excerpt_ids()
|
||||||
|
.into_iter()
|
||||||
|
.next()
|
||||||
|
.unwrap()
|
||||||
|
});
|
||||||
|
let completions = message_editor.update_in(cx, |editor, window, cx| {
|
||||||
|
editor.set_text("Hello @", window, cx);
|
||||||
|
let buffer = editor.buffer().read(cx).as_singleton().unwrap();
|
||||||
|
let completion_provider = editor.completion_provider().unwrap();
|
||||||
|
completion_provider.completions(
|
||||||
|
excerpt_id,
|
||||||
|
&buffer,
|
||||||
|
Anchor::MAX,
|
||||||
|
CompletionContext {
|
||||||
|
trigger_kind: CompletionTriggerKind::TRIGGER_CHARACTER,
|
||||||
|
trigger_character: Some("@".into()),
|
||||||
|
},
|
||||||
|
window,
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
});
|
||||||
|
let [_, completion]: [_; 2] = completions
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.into_iter()
|
||||||
|
.flat_map(|response| response.completions)
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.try_into()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
message_editor.update_in(cx, |editor, window, cx| {
|
||||||
|
let snapshot = editor.buffer().read(cx).snapshot(cx);
|
||||||
|
let start = snapshot
|
||||||
|
.anchor_in_excerpt(excerpt_id, completion.replace_range.start)
|
||||||
|
.unwrap();
|
||||||
|
let end = snapshot
|
||||||
|
.anchor_in_excerpt(excerpt_id, completion.replace_range.end)
|
||||||
|
.unwrap();
|
||||||
|
editor.edit([(start..end, completion.new_text)], cx);
|
||||||
|
(completion.confirm.unwrap())(CompletionIntent::Complete, window, cx);
|
||||||
|
});
|
||||||
|
|
||||||
|
cx.run_until_parked();
|
||||||
|
|
||||||
|
// Backspace over the inserted crease (and the following space).
|
||||||
|
message_editor.update_in(cx, |editor, window, cx| {
|
||||||
|
editor.backspace(&Default::default(), window, cx);
|
||||||
|
editor.backspace(&Default::default(), window, cx);
|
||||||
|
});
|
||||||
|
|
||||||
|
thread_view.update_in(cx, |thread_view, window, cx| {
|
||||||
|
thread_view.chat(&Chat, window, cx);
|
||||||
|
});
|
||||||
|
|
||||||
|
cx.run_until_parked();
|
||||||
|
|
||||||
|
let content = thread_view.update_in(cx, |thread_view, _window, _cx| {
|
||||||
|
thread_view
|
||||||
|
.message_history
|
||||||
|
.borrow()
|
||||||
|
.items()
|
||||||
|
.iter()
|
||||||
|
.flatten()
|
||||||
|
.cloned()
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
});
|
||||||
|
|
||||||
|
// We don't send a resource link for the deleted crease.
|
||||||
|
pretty_assertions::assert_matches!(content.as_slice(), [acp::ContentBlock::Text { .. }]);
|
||||||
|
}
|
||||||
|
|
||||||
async fn setup_thread_view(
|
async fn setup_thread_view(
|
||||||
agent: impl AgentServer + 'static,
|
agent: impl AgentServer + 'static,
|
||||||
cx: &mut TestAppContext,
|
cx: &mut TestAppContext,
|
||||||
|
|
|
@ -137,7 +137,7 @@ impl RenderOnce for AiUpsellCard {
|
||||||
.size(rems_from_px(72.))
|
.size(rems_from_px(72.))
|
||||||
.child(
|
.child(
|
||||||
Vector::new(
|
Vector::new(
|
||||||
VectorName::CertifiedUserStamp,
|
VectorName::ProUserStamp,
|
||||||
rems_from_px(72.),
|
rems_from_px(72.),
|
||||||
rems_from_px(72.),
|
rems_from_px(72.),
|
||||||
)
|
)
|
||||||
|
|
|
@ -134,7 +134,12 @@ impl Settings for AutoUpdateSetting {
|
||||||
type FileContent = Option<AutoUpdateSettingContent>;
|
type FileContent = Option<AutoUpdateSettingContent>;
|
||||||
|
|
||||||
fn load(sources: SettingsSources<Self::FileContent>, _: &mut App) -> Result<Self> {
|
fn load(sources: SettingsSources<Self::FileContent>, _: &mut App) -> Result<Self> {
|
||||||
let auto_update = [sources.server, sources.release_channel, sources.user]
|
let auto_update = [
|
||||||
|
sources.server,
|
||||||
|
sources.release_channel,
|
||||||
|
sources.operating_system,
|
||||||
|
sources.user,
|
||||||
|
]
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.find_map(|value| value.copied().flatten())
|
.find_map(|value| value.copied().flatten())
|
||||||
.unwrap_or(sources.default.ok_or_else(Self::missing_default)?);
|
.unwrap_or(sources.default.ok_or_else(Self::missing_default)?);
|
||||||
|
|
|
@ -16,6 +16,7 @@ use clock::SystemClock;
|
||||||
use cloud_api_client::CloudApiClient;
|
use cloud_api_client::CloudApiClient;
|
||||||
use cloud_api_client::websocket_protocol::MessageToClient;
|
use cloud_api_client::websocket_protocol::MessageToClient;
|
||||||
use credentials_provider::CredentialsProvider;
|
use credentials_provider::CredentialsProvider;
|
||||||
|
use feature_flags::FeatureFlagAppExt as _;
|
||||||
use futures::{
|
use futures::{
|
||||||
AsyncReadExt, FutureExt, SinkExt, Stream, StreamExt, TryFutureExt as _, TryStreamExt,
|
AsyncReadExt, FutureExt, SinkExt, Stream, StreamExt, TryFutureExt as _, TryStreamExt,
|
||||||
channel::oneshot, future::BoxFuture,
|
channel::oneshot, future::BoxFuture,
|
||||||
|
@ -192,6 +193,8 @@ pub fn init(client: &Arc<Client>, cx: &mut App) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub type MessageToClientHandler = Box<dyn Fn(&MessageToClient, &App) + Send + Sync + 'static>;
|
||||||
|
|
||||||
struct GlobalClient(Arc<Client>);
|
struct GlobalClient(Arc<Client>);
|
||||||
|
|
||||||
impl Global for GlobalClient {}
|
impl Global for GlobalClient {}
|
||||||
|
@ -205,6 +208,7 @@ pub struct Client {
|
||||||
credentials_provider: ClientCredentialsProvider,
|
credentials_provider: ClientCredentialsProvider,
|
||||||
state: RwLock<ClientState>,
|
state: RwLock<ClientState>,
|
||||||
handler_set: parking_lot::Mutex<ProtoMessageHandlerSet>,
|
handler_set: parking_lot::Mutex<ProtoMessageHandlerSet>,
|
||||||
|
message_to_client_handlers: parking_lot::Mutex<Vec<MessageToClientHandler>>,
|
||||||
|
|
||||||
#[allow(clippy::type_complexity)]
|
#[allow(clippy::type_complexity)]
|
||||||
#[cfg(any(test, feature = "test-support"))]
|
#[cfg(any(test, feature = "test-support"))]
|
||||||
|
@ -554,6 +558,7 @@ impl Client {
|
||||||
credentials_provider: ClientCredentialsProvider::new(cx),
|
credentials_provider: ClientCredentialsProvider::new(cx),
|
||||||
state: Default::default(),
|
state: Default::default(),
|
||||||
handler_set: Default::default(),
|
handler_set: Default::default(),
|
||||||
|
message_to_client_handlers: parking_lot::Mutex::new(Vec::new()),
|
||||||
|
|
||||||
#[cfg(any(test, feature = "test-support"))]
|
#[cfg(any(test, feature = "test-support"))]
|
||||||
authenticate: Default::default(),
|
authenticate: Default::default(),
|
||||||
|
@ -960,25 +965,51 @@ impl Client {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Performs a sign-in and also connects to Collab.
|
/// Performs a sign-in and also (optionally) connects to Collab.
|
||||||
///
|
///
|
||||||
/// This is called in places where we *don't* need to connect in the future. We will replace these calls with calls
|
/// Only Zed staff automatically connect to Collab.
|
||||||
/// to `sign_in` when we're ready to remove auto-connection to Collab.
|
|
||||||
pub async fn sign_in_with_optional_connect(
|
pub async fn sign_in_with_optional_connect(
|
||||||
self: &Arc<Self>,
|
self: &Arc<Self>,
|
||||||
try_provider: bool,
|
try_provider: bool,
|
||||||
cx: &AsyncApp,
|
cx: &AsyncApp,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
|
let (is_staff_tx, is_staff_rx) = oneshot::channel::<bool>();
|
||||||
|
let mut is_staff_tx = Some(is_staff_tx);
|
||||||
|
cx.update(|cx| {
|
||||||
|
cx.on_flags_ready(move |state, _cx| {
|
||||||
|
if let Some(is_staff_tx) = is_staff_tx.take() {
|
||||||
|
is_staff_tx.send(state.is_staff).log_err();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
})
|
||||||
|
.log_err();
|
||||||
|
|
||||||
let credentials = self.sign_in(try_provider, cx).await?;
|
let credentials = self.sign_in(try_provider, cx).await?;
|
||||||
|
|
||||||
self.connect_to_cloud(cx).await.log_err();
|
self.connect_to_cloud(cx).await.log_err();
|
||||||
|
|
||||||
let connect_result = match self.connect_with_credentials(credentials, cx).await {
|
cx.update(move |cx| {
|
||||||
|
cx.spawn({
|
||||||
|
let client = self.clone();
|
||||||
|
async move |cx| {
|
||||||
|
let is_staff = is_staff_rx.await?;
|
||||||
|
if is_staff {
|
||||||
|
match client.connect_with_credentials(credentials, cx).await {
|
||||||
ConnectionResult::Timeout => Err(anyhow!("connection timed out")),
|
ConnectionResult::Timeout => Err(anyhow!("connection timed out")),
|
||||||
ConnectionResult::ConnectionReset => Err(anyhow!("connection reset")),
|
ConnectionResult::ConnectionReset => Err(anyhow!("connection reset")),
|
||||||
ConnectionResult::Result(result) => result.context("client auth and connect"),
|
ConnectionResult::Result(result) => {
|
||||||
};
|
result.context("client auth and connect")
|
||||||
connect_result.log_err();
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.detach_and_log_err(cx);
|
||||||
|
})
|
||||||
|
.log_err();
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
@ -1651,10 +1682,22 @@ impl Client {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_message_to_client(self: &Arc<Client>, message: MessageToClient, _cx: &AsyncApp) {
|
pub fn add_message_to_client_handler(
|
||||||
match message {
|
self: &Arc<Client>,
|
||||||
MessageToClient::UserUpdated => {}
|
handler: impl Fn(&MessageToClient, &App) + Send + Sync + 'static,
|
||||||
|
) {
|
||||||
|
self.message_to_client_handlers
|
||||||
|
.lock()
|
||||||
|
.push(Box::new(handler));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn handle_message_to_client(self: &Arc<Client>, message: MessageToClient, cx: &AsyncApp) {
|
||||||
|
cx.update(|cx| {
|
||||||
|
for handler in self.message_to_client_handlers.lock().iter() {
|
||||||
|
handler(&message, cx);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn telemetry(&self) -> &Arc<Telemetry> {
|
pub fn telemetry(&self) -> &Arc<Telemetry> {
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
use super::{Client, Status, TypedEnvelope, proto};
|
use super::{Client, Status, TypedEnvelope, proto};
|
||||||
use anyhow::{Context as _, Result, anyhow};
|
use anyhow::{Context as _, Result, anyhow};
|
||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
|
use cloud_api_client::websocket_protocol::MessageToClient;
|
||||||
use cloud_api_client::{GetAuthenticatedUserResponse, PlanInfo};
|
use cloud_api_client::{GetAuthenticatedUserResponse, PlanInfo};
|
||||||
use cloud_llm_client::{
|
use cloud_llm_client::{
|
||||||
EDIT_PREDICTIONS_USAGE_AMOUNT_HEADER_NAME, EDIT_PREDICTIONS_USAGE_LIMIT_HEADER_NAME,
|
EDIT_PREDICTIONS_USAGE_AMOUNT_HEADER_NAME, EDIT_PREDICTIONS_USAGE_LIMIT_HEADER_NAME,
|
||||||
|
@ -181,6 +182,12 @@ impl UserStore {
|
||||||
client.add_message_handler(cx.weak_entity(), Self::handle_update_invite_info),
|
client.add_message_handler(cx.weak_entity(), Self::handle_update_invite_info),
|
||||||
client.add_message_handler(cx.weak_entity(), Self::handle_show_contacts),
|
client.add_message_handler(cx.weak_entity(), Self::handle_show_contacts),
|
||||||
];
|
];
|
||||||
|
|
||||||
|
client.add_message_to_client_handler({
|
||||||
|
let this = cx.weak_entity();
|
||||||
|
move |message, cx| Self::handle_message_to_client(this.clone(), message, cx)
|
||||||
|
});
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
users: Default::default(),
|
users: Default::default(),
|
||||||
by_github_login: Default::default(),
|
by_github_login: Default::default(),
|
||||||
|
@ -813,6 +820,32 @@ impl UserStore {
|
||||||
cx.emit(Event::PrivateUserInfoUpdated);
|
cx.emit(Event::PrivateUserInfoUpdated);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn handle_message_to_client(this: WeakEntity<Self>, message: &MessageToClient, cx: &App) {
|
||||||
|
cx.spawn(async move |cx| {
|
||||||
|
match message {
|
||||||
|
MessageToClient::UserUpdated => {
|
||||||
|
let cloud_client = cx
|
||||||
|
.update(|cx| {
|
||||||
|
this.read_with(cx, |this, _cx| {
|
||||||
|
this.client.upgrade().map(|client| client.cloud_client())
|
||||||
|
})
|
||||||
|
})??
|
||||||
|
.ok_or(anyhow::anyhow!("Failed to get Cloud client"))?;
|
||||||
|
|
||||||
|
let response = cloud_client.get_authenticated_user().await?;
|
||||||
|
cx.update(|cx| {
|
||||||
|
this.update(cx, |this, cx| {
|
||||||
|
this.update_authenticated_user(response, cx);
|
||||||
|
})
|
||||||
|
})??;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
anyhow::Ok(())
|
||||||
|
})
|
||||||
|
.detach_and_log_err(cx);
|
||||||
|
}
|
||||||
|
|
||||||
pub fn watch_current_user(&self) -> watch::Receiver<Option<Arc<User>>> {
|
pub fn watch_current_user(&self) -> watch::Receiver<Option<Arc<User>>> {
|
||||||
self.current_user.clone()
|
self.current_user.clone()
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,5 +2,6 @@ ZED_ENVIRONMENT=production
|
||||||
RUST_LOG=info
|
RUST_LOG=info
|
||||||
INVITE_LINK_PREFIX=https://zed.dev/invites/
|
INVITE_LINK_PREFIX=https://zed.dev/invites/
|
||||||
AUTO_JOIN_CHANNEL_ID=283
|
AUTO_JOIN_CHANNEL_ID=283
|
||||||
DATABASE_MAX_CONNECTIONS=250
|
# Set DATABASE_MAX_CONNECTIONS max connections in the `deploy_collab.yml`:
|
||||||
|
# https://github.com/zed-industries/zed/blob/main/.github/workflows/deploy_collab.yml
|
||||||
LLM_DATABASE_MAX_CONNECTIONS=25
|
LLM_DATABASE_MAX_CONNECTIONS=25
|
||||||
|
|
|
@ -699,7 +699,10 @@ impl Database {
|
||||||
language_server::Column::ProjectId,
|
language_server::Column::ProjectId,
|
||||||
language_server::Column::Id,
|
language_server::Column::Id,
|
||||||
])
|
])
|
||||||
.update_column(language_server::Column::Name)
|
.update_columns([
|
||||||
|
language_server::Column::Name,
|
||||||
|
language_server::Column::Capabilities,
|
||||||
|
])
|
||||||
.to_owned(),
|
.to_owned(),
|
||||||
)
|
)
|
||||||
.exec(&*tx)
|
.exec(&*tx)
|
||||||
|
|
|
@ -3053,7 +3053,7 @@ impl Render for CollabPanel {
|
||||||
.on_action(cx.listener(CollabPanel::move_channel_down))
|
.on_action(cx.listener(CollabPanel::move_channel_down))
|
||||||
.track_focus(&self.focus_handle)
|
.track_focus(&self.focus_handle)
|
||||||
.size_full()
|
.size_full()
|
||||||
.child(if self.user_store.read(cx).current_user().is_none() {
|
.child(if !self.client.status().borrow().is_connected() {
|
||||||
self.render_signed_out(cx)
|
self.render_signed_out(cx)
|
||||||
} else {
|
} else {
|
||||||
self.render_signed_in(window, cx)
|
self.render_signed_in(window, cx)
|
||||||
|
|
|
@ -2705,6 +2705,11 @@ impl Editor {
|
||||||
self.completion_provider = provider;
|
self.completion_provider = provider;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(any(test, feature = "test-support"))]
|
||||||
|
pub fn completion_provider(&self) -> Option<Rc<dyn CompletionProvider>> {
|
||||||
|
self.completion_provider.clone()
|
||||||
|
}
|
||||||
|
|
||||||
pub fn semantics_provider(&self) -> Option<Rc<dyn SemanticsProvider>> {
|
pub fn semantics_provider(&self) -> Option<Rc<dyn SemanticsProvider>> {
|
||||||
self.semantics_provider.clone()
|
self.semantics_provider.clone()
|
||||||
}
|
}
|
||||||
|
|
|
@ -158,6 +158,11 @@ where
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct OnFlagsReady {
|
||||||
|
pub is_staff: bool,
|
||||||
|
}
|
||||||
|
|
||||||
pub trait FeatureFlagAppExt {
|
pub trait FeatureFlagAppExt {
|
||||||
fn wait_for_flag<T: FeatureFlag>(&mut self) -> WaitForFlag;
|
fn wait_for_flag<T: FeatureFlag>(&mut self) -> WaitForFlag;
|
||||||
|
|
||||||
|
@ -169,6 +174,10 @@ pub trait FeatureFlagAppExt {
|
||||||
fn has_flag<T: FeatureFlag>(&self) -> bool;
|
fn has_flag<T: FeatureFlag>(&self) -> bool;
|
||||||
fn is_staff(&self) -> bool;
|
fn is_staff(&self) -> bool;
|
||||||
|
|
||||||
|
fn on_flags_ready<F>(&mut self, callback: F) -> Subscription
|
||||||
|
where
|
||||||
|
F: FnMut(OnFlagsReady, &mut App) + 'static;
|
||||||
|
|
||||||
fn observe_flag<T: FeatureFlag, F>(&mut self, callback: F) -> Subscription
|
fn observe_flag<T: FeatureFlag, F>(&mut self, callback: F) -> Subscription
|
||||||
where
|
where
|
||||||
F: FnMut(bool, &mut App) + 'static;
|
F: FnMut(bool, &mut App) + 'static;
|
||||||
|
@ -198,6 +207,21 @@ impl FeatureFlagAppExt for App {
|
||||||
.unwrap_or(false)
|
.unwrap_or(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn on_flags_ready<F>(&mut self, mut callback: F) -> Subscription
|
||||||
|
where
|
||||||
|
F: FnMut(OnFlagsReady, &mut App) + 'static,
|
||||||
|
{
|
||||||
|
self.observe_global::<FeatureFlags>(move |cx| {
|
||||||
|
let feature_flags = cx.global::<FeatureFlags>();
|
||||||
|
callback(
|
||||||
|
OnFlagsReady {
|
||||||
|
is_staff: feature_flags.staff,
|
||||||
|
},
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
fn observe_flag<T: FeatureFlag, F>(&mut self, mut callback: F) -> Subscription
|
fn observe_flag<T: FeatureFlag, F>(&mut self, mut callback: F) -> Subscription
|
||||||
where
|
where
|
||||||
F: FnMut(bool, &mut App) + 'static,
|
F: FnMut(bool, &mut App) + 'static,
|
||||||
|
|
|
@ -1,9 +1,6 @@
|
||||||
use notify::EventKind;
|
use notify::EventKind;
|
||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
use std::{
|
use std::sync::{Arc, OnceLock};
|
||||||
collections::HashMap,
|
|
||||||
sync::{Arc, OnceLock},
|
|
||||||
};
|
|
||||||
use util::{ResultExt, paths::SanitizedPath};
|
use util::{ResultExt, paths::SanitizedPath};
|
||||||
|
|
||||||
use crate::{PathEvent, PathEventKind, Watcher};
|
use crate::{PathEvent, PathEventKind, Watcher};
|
||||||
|
@ -11,7 +8,6 @@ use crate::{PathEvent, PathEventKind, Watcher};
|
||||||
pub struct FsWatcher {
|
pub struct FsWatcher {
|
||||||
tx: smol::channel::Sender<()>,
|
tx: smol::channel::Sender<()>,
|
||||||
pending_path_events: Arc<Mutex<Vec<PathEvent>>>,
|
pending_path_events: Arc<Mutex<Vec<PathEvent>>>,
|
||||||
registrations: Mutex<HashMap<Arc<std::path::Path>, WatcherRegistrationId>>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FsWatcher {
|
impl FsWatcher {
|
||||||
|
@ -22,24 +18,10 @@ impl FsWatcher {
|
||||||
Self {
|
Self {
|
||||||
tx,
|
tx,
|
||||||
pending_path_events,
|
pending_path_events,
|
||||||
registrations: Default::default(),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Drop for FsWatcher {
|
|
||||||
fn drop(&mut self) {
|
|
||||||
let mut registrations = self.registrations.lock();
|
|
||||||
let registrations = registrations.drain();
|
|
||||||
|
|
||||||
let _ = global(|g| {
|
|
||||||
for (_, registration) in registrations {
|
|
||||||
g.remove(registration);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Watcher for FsWatcher {
|
impl Watcher for FsWatcher {
|
||||||
fn add(&self, path: &std::path::Path) -> anyhow::Result<()> {
|
fn add(&self, path: &std::path::Path) -> anyhow::Result<()> {
|
||||||
let root_path = SanitizedPath::from(path);
|
let root_path = SanitizedPath::from(path);
|
||||||
|
@ -47,19 +29,11 @@ impl Watcher for FsWatcher {
|
||||||
let tx = self.tx.clone();
|
let tx = self.tx.clone();
|
||||||
let pending_paths = self.pending_path_events.clone();
|
let pending_paths = self.pending_path_events.clone();
|
||||||
|
|
||||||
let path: Arc<std::path::Path> = path.into();
|
use notify::Watcher;
|
||||||
|
|
||||||
if self.registrations.lock().contains_key(&path) {
|
global({
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
|
|
||||||
let registration_id = global({
|
|
||||||
let path = path.clone();
|
|
||||||
|g| {
|
|g| {
|
||||||
g.add(
|
g.add(move |event: ¬ify::Event| {
|
||||||
path,
|
|
||||||
notify::RecursiveMode::NonRecursive,
|
|
||||||
move |event: ¬ify::Event| {
|
|
||||||
let kind = match event.kind {
|
let kind = match event.kind {
|
||||||
EventKind::Create(_) => Some(PathEventKind::Created),
|
EventKind::Create(_) => Some(PathEventKind::Created),
|
||||||
EventKind::Modify(_) => Some(PathEventKind::Changed),
|
EventKind::Modify(_) => Some(PathEventKind::Changed),
|
||||||
|
@ -91,92 +65,39 @@ impl Watcher for FsWatcher {
|
||||||
|a, b| a.path.cmp(&b.path),
|
|a, b| a.path.cmp(&b.path),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
},
|
})
|
||||||
)
|
|
||||||
}
|
}
|
||||||
})??;
|
})?;
|
||||||
|
|
||||||
self.registrations.lock().insert(path, registration_id);
|
global(|g| {
|
||||||
|
g.watcher
|
||||||
|
.lock()
|
||||||
|
.watch(path, notify::RecursiveMode::NonRecursive)
|
||||||
|
})??;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn remove(&self, path: &std::path::Path) -> anyhow::Result<()> {
|
fn remove(&self, path: &std::path::Path) -> anyhow::Result<()> {
|
||||||
let Some(registration) = self.registrations.lock().remove(path) else {
|
use notify::Watcher;
|
||||||
return Ok(());
|
Ok(global(|w| w.watcher.lock().unwatch(path))??)
|
||||||
};
|
|
||||||
|
|
||||||
global(|w| w.remove(registration))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default, Debug, Copy, Clone, PartialEq, Eq, Hash)]
|
|
||||||
pub struct WatcherRegistrationId(u32);
|
|
||||||
|
|
||||||
struct WatcherRegistrationState {
|
|
||||||
callback: Box<dyn Fn(¬ify::Event) + Send + Sync>,
|
|
||||||
path: Arc<std::path::Path>,
|
|
||||||
}
|
|
||||||
|
|
||||||
struct WatcherState {
|
|
||||||
// two mutexes because calling watcher.add triggers an watcher.event, which needs watchers.
|
|
||||||
#[cfg(target_os = "linux")]
|
|
||||||
watcher: notify::INotifyWatcher,
|
|
||||||
#[cfg(target_os = "freebsd")]
|
|
||||||
watcher: notify::KqueueWatcher,
|
|
||||||
#[cfg(target_os = "windows")]
|
|
||||||
watcher: notify::ReadDirectoryChangesWatcher,
|
|
||||||
|
|
||||||
watchers: HashMap<WatcherRegistrationId, WatcherRegistrationState>,
|
|
||||||
path_registrations: HashMap<Arc<std::path::Path>, u32>,
|
|
||||||
last_registration: WatcherRegistrationId,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct GlobalWatcher {
|
pub struct GlobalWatcher {
|
||||||
state: Mutex<WatcherState>,
|
// two mutexes because calling watcher.add triggers an watcher.event, which needs watchers.
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
pub(super) watcher: Mutex<notify::INotifyWatcher>,
|
||||||
|
#[cfg(target_os = "freebsd")]
|
||||||
|
pub(super) watcher: Mutex<notify::KqueueWatcher>,
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
pub(super) watcher: Mutex<notify::ReadDirectoryChangesWatcher>,
|
||||||
|
pub(super) watchers: Mutex<Vec<Box<dyn Fn(¬ify::Event) + Send + Sync>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl GlobalWatcher {
|
impl GlobalWatcher {
|
||||||
#[must_use]
|
pub(super) fn add(&self, cb: impl Fn(¬ify::Event) + Send + Sync + 'static) {
|
||||||
fn add(
|
self.watchers.lock().push(Box::new(cb))
|
||||||
&self,
|
|
||||||
path: Arc<std::path::Path>,
|
|
||||||
mode: notify::RecursiveMode,
|
|
||||||
cb: impl Fn(¬ify::Event) + Send + Sync + 'static,
|
|
||||||
) -> anyhow::Result<WatcherRegistrationId> {
|
|
||||||
use notify::Watcher;
|
|
||||||
let mut state = self.state.lock();
|
|
||||||
|
|
||||||
state.watcher.watch(&path, mode)?;
|
|
||||||
|
|
||||||
let id = state.last_registration;
|
|
||||||
state.last_registration = WatcherRegistrationId(id.0 + 1);
|
|
||||||
|
|
||||||
let registration_state = WatcherRegistrationState {
|
|
||||||
callback: Box::new(cb),
|
|
||||||
path: path.clone(),
|
|
||||||
};
|
|
||||||
state.watchers.insert(id, registration_state);
|
|
||||||
*state.path_registrations.entry(path.clone()).or_insert(0) += 1;
|
|
||||||
|
|
||||||
Ok(id)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn remove(&self, id: WatcherRegistrationId) {
|
|
||||||
use notify::Watcher;
|
|
||||||
let mut state = self.state.lock();
|
|
||||||
let Some(registration_state) = state.watchers.remove(&id) else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
let Some(count) = state.path_registrations.get_mut(®istration_state.path) else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
*count -= 1;
|
|
||||||
if *count == 0 {
|
|
||||||
state.watcher.unwatch(®istration_state.path).log_err();
|
|
||||||
state.path_registrations.remove(®istration_state.path);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -193,10 +114,8 @@ fn handle_event(event: Result<notify::Event, notify::Error>) {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
global::<()>(move |watcher| {
|
global::<()>(move |watcher| {
|
||||||
let state = watcher.state.lock();
|
for f in watcher.watchers.lock().iter() {
|
||||||
for registration in state.watchers.values() {
|
f(&event)
|
||||||
let callback = ®istration.callback;
|
|
||||||
callback(&event);
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.log_err();
|
.log_err();
|
||||||
|
@ -205,12 +124,8 @@ fn handle_event(event: Result<notify::Event, notify::Error>) {
|
||||||
pub fn global<T>(f: impl FnOnce(&GlobalWatcher) -> T) -> anyhow::Result<T> {
|
pub fn global<T>(f: impl FnOnce(&GlobalWatcher) -> T) -> anyhow::Result<T> {
|
||||||
let result = FS_WATCHER_INSTANCE.get_or_init(|| {
|
let result = FS_WATCHER_INSTANCE.get_or_init(|| {
|
||||||
notify::recommended_watcher(handle_event).map(|file_watcher| GlobalWatcher {
|
notify::recommended_watcher(handle_event).map(|file_watcher| GlobalWatcher {
|
||||||
state: Mutex::new(WatcherState {
|
watcher: Mutex::new(file_watcher),
|
||||||
watcher: file_watcher,
|
|
||||||
watchers: Default::default(),
|
watchers: Default::default(),
|
||||||
path_registrations: Default::default(),
|
|
||||||
last_registration: Default::default(),
|
|
||||||
}),
|
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
match result {
|
match result {
|
||||||
|
|
|
@ -308,7 +308,11 @@ impl Settings for LineIndicatorFormat {
|
||||||
type FileContent = Option<LineIndicatorFormatContent>;
|
type FileContent = Option<LineIndicatorFormatContent>;
|
||||||
|
|
||||||
fn load(sources: SettingsSources<Self::FileContent>, _: &mut App) -> anyhow::Result<Self> {
|
fn load(sources: SettingsSources<Self::FileContent>, _: &mut App) -> anyhow::Result<Self> {
|
||||||
let format = [sources.release_channel, sources.user]
|
let format = [
|
||||||
|
sources.release_channel,
|
||||||
|
sources.operating_system,
|
||||||
|
sources.user,
|
||||||
|
]
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.find_map(|value| value.copied().flatten())
|
.find_map(|value| value.copied().flatten())
|
||||||
.unwrap_or(sources.default.ok_or_else(Self::missing_default)?);
|
.unwrap_or(sources.default.ok_or_else(Self::missing_default)?);
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use ai_onboarding::{AiUpsellCard, SignInStatus};
|
use ai_onboarding::AiUpsellCard;
|
||||||
use client::UserStore;
|
use client::{Client, UserStore};
|
||||||
use fs::Fs;
|
use fs::Fs;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
Action, AnyView, App, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, WeakEntity,
|
Action, AnyView, App, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, WeakEntity,
|
||||||
|
@ -12,8 +12,8 @@ use language_model::{LanguageModelProvider, LanguageModelProviderId, LanguageMod
|
||||||
use project::DisableAiSettings;
|
use project::DisableAiSettings;
|
||||||
use settings::{Settings, update_settings_file};
|
use settings::{Settings, update_settings_file};
|
||||||
use ui::{
|
use ui::{
|
||||||
Badge, ButtonLike, Divider, Modal, ModalFooter, ModalHeader, Section, SwitchField, ToggleState,
|
Badge, ButtonLike, Divider, KeyBinding, Modal, ModalFooter, ModalHeader, Section, SwitchField,
|
||||||
prelude::*, tooltip_container,
|
ToggleState, prelude::*, tooltip_container,
|
||||||
};
|
};
|
||||||
use util::ResultExt;
|
use util::ResultExt;
|
||||||
use workspace::{ModalView, Workspace};
|
use workspace::{ModalView, Workspace};
|
||||||
|
@ -88,7 +88,7 @@ fn render_privacy_card(tab_index: &mut isize, disabled: bool, cx: &mut App) -> i
|
||||||
h_flex()
|
h_flex()
|
||||||
.gap_2()
|
.gap_2()
|
||||||
.justify_between()
|
.justify_between()
|
||||||
.child(Label::new("We don't train models using your data"))
|
.child(Label::new("Privacy is the default for Zed"))
|
||||||
.child(
|
.child(
|
||||||
h_flex().gap_1().child(privacy_badge()).child(
|
h_flex().gap_1().child(privacy_badge()).child(
|
||||||
Button::new("learn_more", "Learn More")
|
Button::new("learn_more", "Learn More")
|
||||||
|
@ -109,7 +109,7 @@ fn render_privacy_card(tab_index: &mut isize, disabled: bool, cx: &mut App) -> i
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
Label::new(
|
Label::new(
|
||||||
"Feel confident in the security and privacy of your projects using Zed.",
|
"Any use or storage of your data is with your explicit, single-use, opt-in consent.",
|
||||||
)
|
)
|
||||||
.size(LabelSize::Small)
|
.size(LabelSize::Small)
|
||||||
.color(Color::Muted),
|
.color(Color::Muted),
|
||||||
|
@ -240,6 +240,7 @@ fn render_llm_provider_card(
|
||||||
pub(crate) fn render_ai_setup_page(
|
pub(crate) fn render_ai_setup_page(
|
||||||
workspace: WeakEntity<Workspace>,
|
workspace: WeakEntity<Workspace>,
|
||||||
user_store: Entity<UserStore>,
|
user_store: Entity<UserStore>,
|
||||||
|
client: Arc<Client>,
|
||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut App,
|
cx: &mut App,
|
||||||
) -> impl IntoElement {
|
) -> impl IntoElement {
|
||||||
|
@ -283,15 +284,16 @@ pub(crate) fn render_ai_setup_page(
|
||||||
v_flex()
|
v_flex()
|
||||||
.mt_2()
|
.mt_2()
|
||||||
.gap_6()
|
.gap_6()
|
||||||
.child(AiUpsellCard {
|
.child({
|
||||||
sign_in_status: SignInStatus::SignedIn,
|
let mut ai_upsell_card =
|
||||||
sign_in: Arc::new(|_, _| {}),
|
AiUpsellCard::new(client, &user_store, user_store.read(cx).plan(), cx);
|
||||||
account_too_young: user_store.read(cx).account_too_young(),
|
|
||||||
user_plan: user_store.read(cx).plan(),
|
ai_upsell_card.tab_index = Some({
|
||||||
tab_index: Some({
|
|
||||||
tab_index += 1;
|
tab_index += 1;
|
||||||
tab_index - 1
|
tab_index - 1
|
||||||
}),
|
});
|
||||||
|
|
||||||
|
ai_upsell_card
|
||||||
})
|
})
|
||||||
.child(render_llm_provider_section(
|
.child(render_llm_provider_section(
|
||||||
&mut tab_index,
|
&mut tab_index,
|
||||||
|
@ -336,6 +338,10 @@ impl AiConfigurationModal {
|
||||||
selected_provider,
|
selected_provider,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn cancel(&mut self, _: &menu::Cancel, cx: &mut Context<Self>) {
|
||||||
|
cx.emit(DismissEvent);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ModalView for AiConfigurationModal {}
|
impl ModalView for AiConfigurationModal {}
|
||||||
|
@ -349,11 +355,15 @@ impl Focusable for AiConfigurationModal {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Render for AiConfigurationModal {
|
impl Render for AiConfigurationModal {
|
||||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||||
v_flex()
|
v_flex()
|
||||||
|
.key_context("OnboardingAiConfigurationModal")
|
||||||
.w(rems(34.))
|
.w(rems(34.))
|
||||||
.elevation_3(cx)
|
.elevation_3(cx)
|
||||||
.track_focus(&self.focus_handle)
|
.track_focus(&self.focus_handle)
|
||||||
|
.on_action(
|
||||||
|
cx.listener(|this, _: &menu::Cancel, _window, cx| this.cancel(&menu::Cancel, cx)),
|
||||||
|
)
|
||||||
.child(
|
.child(
|
||||||
Modal::new("onboarding-ai-setup-modal", None)
|
Modal::new("onboarding-ai-setup-modal", None)
|
||||||
.header(
|
.header(
|
||||||
|
@ -368,18 +378,19 @@ impl Render for AiConfigurationModal {
|
||||||
.section(Section::new().child(self.configuration_view.clone()))
|
.section(Section::new().child(self.configuration_view.clone()))
|
||||||
.footer(
|
.footer(
|
||||||
ModalFooter::new().end_slot(
|
ModalFooter::new().end_slot(
|
||||||
h_flex()
|
Button::new("ai-onb-modal-Done", "Done")
|
||||||
.gap_1()
|
.key_binding(
|
||||||
.child(
|
KeyBinding::for_action_in(
|
||||||
Button::new("onboarding-closing-cancel", "Cancel")
|
&menu::Cancel,
|
||||||
.on_click(cx.listener(|_, _, _, cx| cx.emit(DismissEvent))),
|
&self.focus_handle.clone(),
|
||||||
|
window,
|
||||||
|
cx,
|
||||||
)
|
)
|
||||||
.child(Button::new("save-btn", "Done").on_click(cx.listener(
|
.map(|kb| kb.size(rems_from_px(12.))),
|
||||||
|_, _, window, cx| {
|
)
|
||||||
window.dispatch_action(menu::Confirm.boxed_clone(), cx);
|
.on_click(cx.listener(|this, _event, _window, cx| {
|
||||||
cx.emit(DismissEvent);
|
this.cancel(&menu::Cancel, cx)
|
||||||
},
|
})),
|
||||||
))),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
@ -396,7 +407,7 @@ impl AiPrivacyTooltip {
|
||||||
|
|
||||||
impl Render for AiPrivacyTooltip {
|
impl Render for AiPrivacyTooltip {
|
||||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||||
const DESCRIPTION: &'static str = "One of Zed's most important principles is transparency. This is why we are and value open-source so much. And it wouldn't be any different with AI.";
|
const DESCRIPTION: &'static str = "We believe in opt-in data sharing as the default for building AI products, rather than opt-out. We'll only use or store your data if you affirmatively send it to us. ";
|
||||||
|
|
||||||
tooltip_container(window, cx, move |this, _, _| {
|
tooltip_container(window, cx, move |this, _, _| {
|
||||||
this.child(
|
this.child(
|
||||||
|
@ -407,7 +418,7 @@ impl Render for AiPrivacyTooltip {
|
||||||
.size(IconSize::Small)
|
.size(IconSize::Small)
|
||||||
.color(Color::Muted),
|
.color(Color::Muted),
|
||||||
)
|
)
|
||||||
.child(Label::new("Privacy Principle")),
|
.child(Label::new("Privacy First")),
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
div().max_w_64().child(
|
div().max_w_64().child(
|
||||||
|
|
|
@ -201,12 +201,15 @@ fn render_telemetry_section(tab_index: &mut isize, cx: &App) -> impl IntoElement
|
||||||
let fs = <dyn Fs>::global(cx);
|
let fs = <dyn Fs>::global(cx);
|
||||||
|
|
||||||
v_flex()
|
v_flex()
|
||||||
|
.pt_6()
|
||||||
.gap_4()
|
.gap_4()
|
||||||
|
.border_t_1()
|
||||||
|
.border_color(cx.theme().colors().border_variant.opacity(0.5))
|
||||||
.child(Label::new("Telemetry").size(LabelSize::Large))
|
.child(Label::new("Telemetry").size(LabelSize::Large))
|
||||||
.child(SwitchField::new(
|
.child(SwitchField::new(
|
||||||
"onboarding-telemetry-metrics",
|
"onboarding-telemetry-metrics",
|
||||||
"Help Improve Zed",
|
"Help Improve Zed",
|
||||||
Some("Sending anonymous usage data helps us build the right features and create the best experience.".into()),
|
Some("Anonymous usage data helps us build the right features and improve your experience.".into()),
|
||||||
if TelemetrySettings::get_global(cx).metrics {
|
if TelemetrySettings::get_global(cx).metrics {
|
||||||
ui::ToggleState::Selected
|
ui::ToggleState::Selected
|
||||||
} else {
|
} else {
|
||||||
|
@ -294,7 +297,7 @@ fn render_base_keymap_section(tab_index: &mut isize, cx: &mut App) -> impl IntoE
|
||||||
ToggleButtonWithIcon::new("Emacs", IconName::EditorEmacs, |_, _, cx| {
|
ToggleButtonWithIcon::new("Emacs", IconName::EditorEmacs, |_, _, cx| {
|
||||||
write_keymap_base(BaseKeymap::Emacs, cx);
|
write_keymap_base(BaseKeymap::Emacs, cx);
|
||||||
}),
|
}),
|
||||||
ToggleButtonWithIcon::new("Cursor (Beta)", IconName::EditorCursor, |_, _, cx| {
|
ToggleButtonWithIcon::new("Cursor", IconName::EditorCursor, |_, _, cx| {
|
||||||
write_keymap_base(BaseKeymap::Cursor, cx);
|
write_keymap_base(BaseKeymap::Cursor, cx);
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
|
@ -326,10 +329,7 @@ fn render_vim_mode_switch(tab_index: &mut isize, cx: &mut App) -> impl IntoEleme
|
||||||
SwitchField::new(
|
SwitchField::new(
|
||||||
"onboarding-vim-mode",
|
"onboarding-vim-mode",
|
||||||
"Vim Mode",
|
"Vim Mode",
|
||||||
Some(
|
Some("Coming from Neovim? Use our first-class implementation of Vim Mode.".into()),
|
||||||
"Coming from Neovim? Zed's first-class implementation of Vim Mode has got your back."
|
|
||||||
.into(),
|
|
||||||
),
|
|
||||||
toggle_state,
|
toggle_state,
|
||||||
{
|
{
|
||||||
let fs = <dyn Fs>::global(cx);
|
let fs = <dyn Fs>::global(cx);
|
||||||
|
|
|
@ -584,11 +584,15 @@ fn render_popular_settings_section(
|
||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut App,
|
cx: &mut App,
|
||||||
) -> impl IntoElement {
|
) -> impl IntoElement {
|
||||||
const LIGATURE_TOOLTIP: &'static str = "Ligatures are when a font creates a special character out of combining two characters into one. For example, with ligatures turned on, =/= would become ≠.";
|
const LIGATURE_TOOLTIP: &'static str =
|
||||||
|
"Font ligatures combine two characters into one. For example, turning =/= into ≠.";
|
||||||
|
|
||||||
v_flex()
|
v_flex()
|
||||||
.gap_5()
|
.pt_6()
|
||||||
.child(Label::new("Popular Settings").size(LabelSize::Large).mt_8())
|
.gap_4()
|
||||||
|
.border_t_1()
|
||||||
|
.border_color(cx.theme().colors().border_variant.opacity(0.5))
|
||||||
|
.child(Label::new("Popular Settings").size(LabelSize::Large))
|
||||||
.child(render_font_customization_section(tab_index, window, cx))
|
.child(render_font_customization_section(tab_index, window, cx))
|
||||||
.child(
|
.child(
|
||||||
SwitchField::new(
|
SwitchField::new(
|
||||||
|
@ -683,7 +687,10 @@ fn render_popular_settings_section(
|
||||||
[
|
[
|
||||||
ToggleButtonSimple::new("Auto", |_, _, cx| {
|
ToggleButtonSimple::new("Auto", |_, _, cx| {
|
||||||
write_show_mini_map(ShowMinimap::Auto, cx);
|
write_show_mini_map(ShowMinimap::Auto, cx);
|
||||||
}),
|
})
|
||||||
|
.tooltip(Tooltip::text(
|
||||||
|
"Show the minimap if the editor's scrollbar is visible.",
|
||||||
|
)),
|
||||||
ToggleButtonSimple::new("Always", |_, _, cx| {
|
ToggleButtonSimple::new("Always", |_, _, cx| {
|
||||||
write_show_mini_map(ShowMinimap::Always, cx);
|
write_show_mini_map(ShowMinimap::Always, cx);
|
||||||
}),
|
}),
|
||||||
|
@ -707,7 +714,7 @@ fn render_popular_settings_section(
|
||||||
pub(crate) fn render_editing_page(window: &mut Window, cx: &mut App) -> impl IntoElement {
|
pub(crate) fn render_editing_page(window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||||
let mut tab_index = 0;
|
let mut tab_index = 0;
|
||||||
v_flex()
|
v_flex()
|
||||||
.gap_4()
|
.gap_6()
|
||||||
.child(render_import_settings_section(&mut tab_index, cx))
|
.child(render_import_settings_section(&mut tab_index, cx))
|
||||||
.child(render_popular_settings_section(&mut tab_index, window, cx))
|
.child(render_popular_settings_section(&mut tab_index, window, cx))
|
||||||
}
|
}
|
||||||
|
|
|
@ -77,6 +77,8 @@ actions!(
|
||||||
ActivateAISetupPage,
|
ActivateAISetupPage,
|
||||||
/// Finish the onboarding process.
|
/// Finish the onboarding process.
|
||||||
Finish,
|
Finish,
|
||||||
|
/// Sign in while in the onboarding flow.
|
||||||
|
SignIn
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -376,6 +378,7 @@ impl Onboarding {
|
||||||
cx,
|
cx,
|
||||||
)
|
)
|
||||||
.map(|kb| kb.size(rems_from_px(12.)));
|
.map(|kb| kb.size(rems_from_px(12.)));
|
||||||
|
|
||||||
if ai_setup_page {
|
if ai_setup_page {
|
||||||
this.child(
|
this.child(
|
||||||
ButtonLike::new("start_building")
|
ButtonLike::new("start_building")
|
||||||
|
@ -387,14 +390,7 @@ impl Onboarding {
|
||||||
.w_full()
|
.w_full()
|
||||||
.justify_between()
|
.justify_between()
|
||||||
.child(Label::new("Start Building"))
|
.child(Label::new("Start Building"))
|
||||||
.child(keybinding.map_or_else(
|
.children(keybinding),
|
||||||
|| {
|
|
||||||
Icon::new(IconName::Check)
|
|
||||||
.size(IconSize::Small)
|
|
||||||
.into_any_element()
|
|
||||||
},
|
|
||||||
IntoElement::into_any_element,
|
|
||||||
)),
|
|
||||||
)
|
)
|
||||||
.on_click(|_, window, cx| {
|
.on_click(|_, window, cx| {
|
||||||
window.dispatch_action(Finish.boxed_clone(), cx);
|
window.dispatch_action(Finish.boxed_clone(), cx);
|
||||||
|
@ -409,11 +405,10 @@ impl Onboarding {
|
||||||
.ml_1()
|
.ml_1()
|
||||||
.w_full()
|
.w_full()
|
||||||
.justify_between()
|
.justify_between()
|
||||||
.child(Label::new("Skip All"))
|
.child(
|
||||||
.child(keybinding.map_or_else(
|
Label::new("Skip All").color(Color::Muted),
|
||||||
|| gpui::Empty.into_any_element(),
|
)
|
||||||
IntoElement::into_any_element,
|
.children(keybinding),
|
||||||
)),
|
|
||||||
)
|
)
|
||||||
.on_click(|_, window, cx| {
|
.on_click(|_, window, cx| {
|
||||||
window.dispatch_action(Finish.boxed_clone(), cx);
|
window.dispatch_action(Finish.boxed_clone(), cx);
|
||||||
|
@ -435,8 +430,26 @@ impl Onboarding {
|
||||||
Button::new("sign_in", "Sign In")
|
Button::new("sign_in", "Sign In")
|
||||||
.full_width()
|
.full_width()
|
||||||
.style(ButtonStyle::Outlined)
|
.style(ButtonStyle::Outlined)
|
||||||
|
.size(ButtonSize::Medium)
|
||||||
|
.key_binding(
|
||||||
|
KeyBinding::for_action_in(&SignIn, &self.focus_handle, window, cx)
|
||||||
|
.map(|kb| kb.size(rems_from_px(12.))),
|
||||||
|
)
|
||||||
.on_click(|_, window, cx| {
|
.on_click(|_, window, cx| {
|
||||||
|
window.dispatch_action(SignIn.boxed_clone(), cx);
|
||||||
|
})
|
||||||
|
.into_any_element()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_finish(_: &Finish, _: &mut Window, cx: &mut App) {
|
||||||
|
go_to_welcome_page(cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_sign_in(_: &SignIn, window: &mut Window, cx: &mut App) {
|
||||||
let client = Client::global(cx);
|
let client = Client::global(cx);
|
||||||
|
|
||||||
window
|
window
|
||||||
.spawn(cx, async move |cx| {
|
.spawn(cx, async move |cx| {
|
||||||
client
|
client
|
||||||
|
@ -445,13 +458,11 @@ impl Onboarding {
|
||||||
.notify_async_err(cx);
|
.notify_async_err(cx);
|
||||||
})
|
})
|
||||||
.detach();
|
.detach();
|
||||||
})
|
|
||||||
.into_any_element()
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_page(&mut self, window: &mut Window, cx: &mut Context<Self>) -> AnyElement {
|
fn render_page(&mut self, window: &mut Window, cx: &mut Context<Self>) -> AnyElement {
|
||||||
|
let client = Client::global(cx);
|
||||||
|
|
||||||
match self.selected_page {
|
match self.selected_page {
|
||||||
SelectedPage::Basics => crate::basics_page::render_basics_page(cx).into_any_element(),
|
SelectedPage::Basics => crate::basics_page::render_basics_page(cx).into_any_element(),
|
||||||
SelectedPage::Editing => {
|
SelectedPage::Editing => {
|
||||||
|
@ -460,16 +471,13 @@ impl Onboarding {
|
||||||
SelectedPage::AiSetup => crate::ai_setup_page::render_ai_setup_page(
|
SelectedPage::AiSetup => crate::ai_setup_page::render_ai_setup_page(
|
||||||
self.workspace.clone(),
|
self.workspace.clone(),
|
||||||
self.user_store.clone(),
|
self.user_store.clone(),
|
||||||
|
client,
|
||||||
window,
|
window,
|
||||||
cx,
|
cx,
|
||||||
)
|
)
|
||||||
.into_any_element(),
|
.into_any_element(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn on_finish(_: &Finish, _: &mut Window, cx: &mut App) {
|
|
||||||
go_to_welcome_page(cx);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Render for Onboarding {
|
impl Render for Onboarding {
|
||||||
|
@ -486,6 +494,7 @@ impl Render for Onboarding {
|
||||||
.size_full()
|
.size_full()
|
||||||
.bg(cx.theme().colors().editor_background)
|
.bg(cx.theme().colors().editor_background)
|
||||||
.on_action(Self::on_finish)
|
.on_action(Self::on_finish)
|
||||||
|
.on_action(Self::handle_sign_in)
|
||||||
.on_action(cx.listener(|this, _: &ActivateBasicsPage, _, cx| {
|
.on_action(cx.listener(|this, _: &ActivateBasicsPage, _, cx| {
|
||||||
this.set_page(SelectedPage::Basics, cx);
|
this.set_page(SelectedPage::Basics, cx);
|
||||||
}))
|
}))
|
||||||
|
|
|
@ -19,6 +19,7 @@ command_palette_hooks.workspace = true
|
||||||
db.workspace = true
|
db.workspace = true
|
||||||
editor.workspace = true
|
editor.workspace = true
|
||||||
file_icons.workspace = true
|
file_icons.workspace = true
|
||||||
|
git_ui.workspace = true
|
||||||
indexmap.workspace = true
|
indexmap.workspace = true
|
||||||
git.workspace = true
|
git.workspace = true
|
||||||
gpui.workspace = true
|
gpui.workspace = true
|
||||||
|
|
|
@ -16,6 +16,7 @@ use editor::{
|
||||||
};
|
};
|
||||||
use file_icons::FileIcons;
|
use file_icons::FileIcons;
|
||||||
use git::status::GitSummary;
|
use git::status::GitSummary;
|
||||||
|
use git_ui::file_diff_view::FileDiffView;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
Action, AnyElement, App, ArcCow, AsyncWindowContext, Bounds, ClipboardItem, Context,
|
Action, AnyElement, App, ArcCow, AsyncWindowContext, Bounds, ClipboardItem, Context,
|
||||||
CursorStyle, DismissEvent, Div, DragMoveEvent, Entity, EventEmitter, ExternalPaths,
|
CursorStyle, DismissEvent, Div, DragMoveEvent, Entity, EventEmitter, ExternalPaths,
|
||||||
|
@ -93,7 +94,7 @@ pub struct ProjectPanel {
|
||||||
unfolded_dir_ids: HashSet<ProjectEntryId>,
|
unfolded_dir_ids: HashSet<ProjectEntryId>,
|
||||||
// Currently selected leaf entry (see auto-folding for a definition of that) in a file tree
|
// Currently selected leaf entry (see auto-folding for a definition of that) in a file tree
|
||||||
selection: Option<SelectedEntry>,
|
selection: Option<SelectedEntry>,
|
||||||
marked_entries: BTreeSet<SelectedEntry>,
|
marked_entries: Vec<SelectedEntry>,
|
||||||
context_menu: Option<(Entity<ContextMenu>, Point<Pixels>, Subscription)>,
|
context_menu: Option<(Entity<ContextMenu>, Point<Pixels>, Subscription)>,
|
||||||
edit_state: Option<EditState>,
|
edit_state: Option<EditState>,
|
||||||
filename_editor: Entity<Editor>,
|
filename_editor: Entity<Editor>,
|
||||||
|
@ -280,6 +281,8 @@ actions!(
|
||||||
SelectNextDirectory,
|
SelectNextDirectory,
|
||||||
/// Selects the previous directory.
|
/// Selects the previous directory.
|
||||||
SelectPrevDirectory,
|
SelectPrevDirectory,
|
||||||
|
/// Opens a diff view to compare two marked files.
|
||||||
|
CompareMarkedFiles,
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -376,7 +379,7 @@ struct DraggedProjectEntryView {
|
||||||
selection: SelectedEntry,
|
selection: SelectedEntry,
|
||||||
details: EntryDetails,
|
details: EntryDetails,
|
||||||
click_offset: Point<Pixels>,
|
click_offset: Point<Pixels>,
|
||||||
selections: Arc<BTreeSet<SelectedEntry>>,
|
selections: Arc<[SelectedEntry]>,
|
||||||
}
|
}
|
||||||
|
|
||||||
struct ItemColors {
|
struct ItemColors {
|
||||||
|
@ -442,8 +445,16 @@ impl ProjectPanel {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
project::Event::ActiveEntryChanged(None) => {
|
project::Event::ActiveEntryChanged(None) => {
|
||||||
|
let is_active_item_file_diff_view = this
|
||||||
|
.workspace
|
||||||
|
.upgrade()
|
||||||
|
.and_then(|ws| ws.read(cx).active_item(cx))
|
||||||
|
.map(|item| item.act_as_type(TypeId::of::<FileDiffView>(), cx).is_some())
|
||||||
|
.unwrap_or(false);
|
||||||
|
if !is_active_item_file_diff_view {
|
||||||
this.marked_entries.clear();
|
this.marked_entries.clear();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
project::Event::RevealInProjectPanel(entry_id) => {
|
project::Event::RevealInProjectPanel(entry_id) => {
|
||||||
if let Some(()) = this
|
if let Some(()) = this
|
||||||
.reveal_entry(project.clone(), *entry_id, false, cx)
|
.reveal_entry(project.clone(), *entry_id, false, cx)
|
||||||
|
@ -676,7 +687,7 @@ impl ProjectPanel {
|
||||||
project_panel.update(cx, |project_panel, _| {
|
project_panel.update(cx, |project_panel, _| {
|
||||||
let entry = SelectedEntry { worktree_id, entry_id };
|
let entry = SelectedEntry { worktree_id, entry_id };
|
||||||
project_panel.marked_entries.clear();
|
project_panel.marked_entries.clear();
|
||||||
project_panel.marked_entries.insert(entry);
|
project_panel.marked_entries.push(entry);
|
||||||
project_panel.selection = Some(entry);
|
project_panel.selection = Some(entry);
|
||||||
});
|
});
|
||||||
if !focus_opened_item {
|
if !focus_opened_item {
|
||||||
|
@ -887,6 +898,7 @@ impl ProjectPanel {
|
||||||
let should_hide_rename = is_root
|
let should_hide_rename = is_root
|
||||||
&& (cfg!(target_os = "windows")
|
&& (cfg!(target_os = "windows")
|
||||||
|| (settings.hide_root && visible_worktrees_count == 1));
|
|| (settings.hide_root && visible_worktrees_count == 1));
|
||||||
|
let should_show_compare = !is_dir && self.file_abs_paths_to_diff(cx).is_some();
|
||||||
|
|
||||||
let context_menu = ContextMenu::build(window, cx, |menu, _, _| {
|
let context_menu = ContextMenu::build(window, cx, |menu, _, _| {
|
||||||
menu.context(self.focus_handle.clone()).map(|menu| {
|
menu.context(self.focus_handle.clone()).map(|menu| {
|
||||||
|
@ -918,6 +930,10 @@ impl ProjectPanel {
|
||||||
.when(is_foldable, |menu| {
|
.when(is_foldable, |menu| {
|
||||||
menu.action("Fold Directory", Box::new(FoldDirectory))
|
menu.action("Fold Directory", Box::new(FoldDirectory))
|
||||||
})
|
})
|
||||||
|
.when(should_show_compare, |menu| {
|
||||||
|
menu.separator()
|
||||||
|
.action("Compare marked files", Box::new(CompareMarkedFiles))
|
||||||
|
})
|
||||||
.separator()
|
.separator()
|
||||||
.action("Cut", Box::new(Cut))
|
.action("Cut", Box::new(Cut))
|
||||||
.action("Copy", Box::new(Copy))
|
.action("Copy", Box::new(Copy))
|
||||||
|
@ -1262,7 +1278,7 @@ impl ProjectPanel {
|
||||||
};
|
};
|
||||||
self.selection = Some(selection);
|
self.selection = Some(selection);
|
||||||
if window.modifiers().shift {
|
if window.modifiers().shift {
|
||||||
self.marked_entries.insert(selection);
|
self.marked_entries.push(selection);
|
||||||
}
|
}
|
||||||
self.autoscroll(cx);
|
self.autoscroll(cx);
|
||||||
cx.notify();
|
cx.notify();
|
||||||
|
@ -2007,7 +2023,7 @@ impl ProjectPanel {
|
||||||
};
|
};
|
||||||
self.selection = Some(selection);
|
self.selection = Some(selection);
|
||||||
if window.modifiers().shift {
|
if window.modifiers().shift {
|
||||||
self.marked_entries.insert(selection);
|
self.marked_entries.push(selection);
|
||||||
}
|
}
|
||||||
|
|
||||||
self.autoscroll(cx);
|
self.autoscroll(cx);
|
||||||
|
@ -2244,7 +2260,7 @@ impl ProjectPanel {
|
||||||
};
|
};
|
||||||
self.selection = Some(selection);
|
self.selection = Some(selection);
|
||||||
if window.modifiers().shift {
|
if window.modifiers().shift {
|
||||||
self.marked_entries.insert(selection);
|
self.marked_entries.push(selection);
|
||||||
}
|
}
|
||||||
self.autoscroll(cx);
|
self.autoscroll(cx);
|
||||||
cx.notify();
|
cx.notify();
|
||||||
|
@ -2572,6 +2588,43 @@ impl ProjectPanel {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn file_abs_paths_to_diff(&self, cx: &Context<Self>) -> Option<(PathBuf, PathBuf)> {
|
||||||
|
let mut selections_abs_path = self
|
||||||
|
.marked_entries
|
||||||
|
.iter()
|
||||||
|
.filter_map(|entry| {
|
||||||
|
let project = self.project.read(cx);
|
||||||
|
let worktree = project.worktree_for_id(entry.worktree_id, cx)?;
|
||||||
|
let entry = worktree.read(cx).entry_for_id(entry.entry_id)?;
|
||||||
|
if !entry.is_file() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
worktree.read(cx).absolutize(&entry.path).ok()
|
||||||
|
})
|
||||||
|
.rev();
|
||||||
|
|
||||||
|
let last_path = selections_abs_path.next()?;
|
||||||
|
let previous_to_last = selections_abs_path.next()?;
|
||||||
|
Some((previous_to_last, last_path))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn compare_marked_files(
|
||||||
|
&mut self,
|
||||||
|
_: &CompareMarkedFiles,
|
||||||
|
window: &mut Window,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) {
|
||||||
|
let selected_files = self.file_abs_paths_to_diff(cx);
|
||||||
|
if let Some((file_path1, file_path2)) = selected_files {
|
||||||
|
self.workspace
|
||||||
|
.update(cx, |workspace, cx| {
|
||||||
|
FileDiffView::open(file_path1, file_path2, workspace, window, cx)
|
||||||
|
.detach_and_log_err(cx);
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn open_system(&mut self, _: &OpenWithSystem, _: &mut Window, cx: &mut Context<Self>) {
|
fn open_system(&mut self, _: &OpenWithSystem, _: &mut Window, cx: &mut Context<Self>) {
|
||||||
if let Some((worktree, entry)) = self.selected_entry(cx) {
|
if let Some((worktree, entry)) = self.selected_entry(cx) {
|
||||||
let abs_path = worktree.abs_path().join(&entry.path);
|
let abs_path = worktree.abs_path().join(&entry.path);
|
||||||
|
@ -3914,11 +3967,9 @@ impl ProjectPanel {
|
||||||
|
|
||||||
let depth = details.depth;
|
let depth = details.depth;
|
||||||
let worktree_id = details.worktree_id;
|
let worktree_id = details.worktree_id;
|
||||||
let selections = Arc::new(self.marked_entries.clone());
|
|
||||||
|
|
||||||
let dragged_selection = DraggedSelection {
|
let dragged_selection = DraggedSelection {
|
||||||
active_selection: selection,
|
active_selection: selection,
|
||||||
marked_selections: selections,
|
marked_selections: Arc::from(self.marked_entries.clone()),
|
||||||
};
|
};
|
||||||
|
|
||||||
let bg_color = if is_marked {
|
let bg_color = if is_marked {
|
||||||
|
@ -4089,7 +4140,7 @@ impl ProjectPanel {
|
||||||
});
|
});
|
||||||
if drag_state.items().count() == 1 {
|
if drag_state.items().count() == 1 {
|
||||||
this.marked_entries.clear();
|
this.marked_entries.clear();
|
||||||
this.marked_entries.insert(drag_state.active_selection);
|
this.marked_entries.push(drag_state.active_selection);
|
||||||
}
|
}
|
||||||
this.hover_expand_task.take();
|
this.hover_expand_task.take();
|
||||||
|
|
||||||
|
@ -4156,65 +4207,69 @@ impl ProjectPanel {
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.on_click(
|
.on_click(
|
||||||
cx.listener(move |this, event: &gpui::ClickEvent, window, cx| {
|
cx.listener(move |project_panel, event: &gpui::ClickEvent, window, cx| {
|
||||||
if event.is_right_click() || event.first_focus()
|
if event.is_right_click() || event.first_focus()
|
||||||
|| show_editor
|
|| show_editor
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if event.standard_click() {
|
if event.standard_click() {
|
||||||
this.mouse_down = false;
|
project_panel.mouse_down = false;
|
||||||
}
|
}
|
||||||
cx.stop_propagation();
|
cx.stop_propagation();
|
||||||
|
|
||||||
if let Some(selection) = this.selection.filter(|_| event.modifiers().shift) {
|
if let Some(selection) = project_panel.selection.filter(|_| event.modifiers().shift) {
|
||||||
let current_selection = this.index_for_selection(selection);
|
let current_selection = project_panel.index_for_selection(selection);
|
||||||
let clicked_entry = SelectedEntry {
|
let clicked_entry = SelectedEntry {
|
||||||
entry_id,
|
entry_id,
|
||||||
worktree_id,
|
worktree_id,
|
||||||
};
|
};
|
||||||
let target_selection = this.index_for_selection(clicked_entry);
|
let target_selection = project_panel.index_for_selection(clicked_entry);
|
||||||
if let Some(((_, _, source_index), (_, _, target_index))) =
|
if let Some(((_, _, source_index), (_, _, target_index))) =
|
||||||
current_selection.zip(target_selection)
|
current_selection.zip(target_selection)
|
||||||
{
|
{
|
||||||
let range_start = source_index.min(target_index);
|
let range_start = source_index.min(target_index);
|
||||||
let range_end = source_index.max(target_index) + 1;
|
let range_end = source_index.max(target_index) + 1;
|
||||||
let mut new_selections = BTreeSet::new();
|
let mut new_selections = Vec::new();
|
||||||
this.for_each_visible_entry(
|
project_panel.for_each_visible_entry(
|
||||||
range_start..range_end,
|
range_start..range_end,
|
||||||
window,
|
window,
|
||||||
cx,
|
cx,
|
||||||
|entry_id, details, _, _| {
|
|entry_id, details, _, _| {
|
||||||
new_selections.insert(SelectedEntry {
|
new_selections.push(SelectedEntry {
|
||||||
entry_id,
|
entry_id,
|
||||||
worktree_id: details.worktree_id,
|
worktree_id: details.worktree_id,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
this.marked_entries = this
|
for selection in &new_selections {
|
||||||
.marked_entries
|
if !project_panel.marked_entries.contains(selection) {
|
||||||
.union(&new_selections)
|
project_panel.marked_entries.push(*selection);
|
||||||
.cloned()
|
}
|
||||||
.collect();
|
}
|
||||||
|
|
||||||
this.selection = Some(clicked_entry);
|
project_panel.selection = Some(clicked_entry);
|
||||||
this.marked_entries.insert(clicked_entry);
|
if !project_panel.marked_entries.contains(&clicked_entry) {
|
||||||
|
project_panel.marked_entries.push(clicked_entry);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else if event.modifiers().secondary() {
|
} else if event.modifiers().secondary() {
|
||||||
if event.click_count() > 1 {
|
if event.click_count() > 1 {
|
||||||
this.split_entry(entry_id, cx);
|
project_panel.split_entry(entry_id, cx);
|
||||||
} else {
|
} else {
|
||||||
this.selection = Some(selection);
|
project_panel.selection = Some(selection);
|
||||||
if !this.marked_entries.insert(selection) {
|
if let Some(position) = project_panel.marked_entries.iter().position(|e| *e == selection) {
|
||||||
this.marked_entries.remove(&selection);
|
project_panel.marked_entries.remove(position);
|
||||||
|
} else {
|
||||||
|
project_panel.marked_entries.push(selection);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if kind.is_dir() {
|
} else if kind.is_dir() {
|
||||||
this.marked_entries.clear();
|
project_panel.marked_entries.clear();
|
||||||
if is_sticky {
|
if is_sticky {
|
||||||
if let Some((_, _, index)) = this.index_for_entry(entry_id, worktree_id) {
|
if let Some((_, _, index)) = project_panel.index_for_entry(entry_id, worktree_id) {
|
||||||
this.scroll_handle.scroll_to_item_with_offset(index, ScrollStrategy::Top, sticky_index.unwrap_or(0));
|
project_panel.scroll_handle.scroll_to_item_with_offset(index, ScrollStrategy::Top, sticky_index.unwrap_or(0));
|
||||||
cx.notify();
|
cx.notify();
|
||||||
// move down by 1px so that clicked item
|
// move down by 1px so that clicked item
|
||||||
// don't count as sticky anymore
|
// don't count as sticky anymore
|
||||||
|
@ -4230,16 +4285,16 @@ impl ProjectPanel {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if event.modifiers().alt {
|
if event.modifiers().alt {
|
||||||
this.toggle_expand_all(entry_id, window, cx);
|
project_panel.toggle_expand_all(entry_id, window, cx);
|
||||||
} else {
|
} else {
|
||||||
this.toggle_expanded(entry_id, window, cx);
|
project_panel.toggle_expanded(entry_id, window, cx);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
let preview_tabs_enabled = PreviewTabsSettings::get_global(cx).enabled;
|
let preview_tabs_enabled = PreviewTabsSettings::get_global(cx).enabled;
|
||||||
let click_count = event.click_count();
|
let click_count = event.click_count();
|
||||||
let focus_opened_item = !preview_tabs_enabled || click_count > 1;
|
let focus_opened_item = !preview_tabs_enabled || click_count > 1;
|
||||||
let allow_preview = preview_tabs_enabled && click_count == 1;
|
let allow_preview = preview_tabs_enabled && click_count == 1;
|
||||||
this.open_entry(entry_id, focus_opened_item, allow_preview, cx);
|
project_panel.open_entry(entry_id, focus_opened_item, allow_preview, cx);
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
@ -4810,12 +4865,21 @@ impl ProjectPanel {
|
||||||
{
|
{
|
||||||
anyhow::bail!("can't reveal an ignored entry in the project panel");
|
anyhow::bail!("can't reveal an ignored entry in the project panel");
|
||||||
}
|
}
|
||||||
|
let is_active_item_file_diff_view = self
|
||||||
|
.workspace
|
||||||
|
.upgrade()
|
||||||
|
.and_then(|ws| ws.read(cx).active_item(cx))
|
||||||
|
.map(|item| item.act_as_type(TypeId::of::<FileDiffView>(), cx).is_some())
|
||||||
|
.unwrap_or(false);
|
||||||
|
if is_active_item_file_diff_view {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
let worktree_id = worktree.id();
|
let worktree_id = worktree.id();
|
||||||
self.expand_entry(worktree_id, entry_id, cx);
|
self.expand_entry(worktree_id, entry_id, cx);
|
||||||
self.update_visible_entries(Some((worktree_id, entry_id)), cx);
|
self.update_visible_entries(Some((worktree_id, entry_id)), cx);
|
||||||
self.marked_entries.clear();
|
self.marked_entries.clear();
|
||||||
self.marked_entries.insert(SelectedEntry {
|
self.marked_entries.push(SelectedEntry {
|
||||||
worktree_id,
|
worktree_id,
|
||||||
entry_id,
|
entry_id,
|
||||||
});
|
});
|
||||||
|
@ -5170,6 +5234,7 @@ impl Render for ProjectPanel {
|
||||||
.on_action(cx.listener(Self::unfold_directory))
|
.on_action(cx.listener(Self::unfold_directory))
|
||||||
.on_action(cx.listener(Self::fold_directory))
|
.on_action(cx.listener(Self::fold_directory))
|
||||||
.on_action(cx.listener(Self::remove_from_project))
|
.on_action(cx.listener(Self::remove_from_project))
|
||||||
|
.on_action(cx.listener(Self::compare_marked_files))
|
||||||
.when(!project.is_read_only(cx), |el| {
|
.when(!project.is_read_only(cx), |el| {
|
||||||
el.on_action(cx.listener(Self::new_file))
|
el.on_action(cx.listener(Self::new_file))
|
||||||
.on_action(cx.listener(Self::new_directory))
|
.on_action(cx.listener(Self::new_directory))
|
||||||
|
|
|
@ -8,7 +8,7 @@ use settings::SettingsStore;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use util::path;
|
use util::path;
|
||||||
use workspace::{
|
use workspace::{
|
||||||
AppState, Pane,
|
AppState, ItemHandle, Pane,
|
||||||
item::{Item, ProjectItem},
|
item::{Item, ProjectItem},
|
||||||
register_project_item,
|
register_project_item,
|
||||||
};
|
};
|
||||||
|
@ -3068,7 +3068,7 @@ async fn test_multiple_marked_entries(cx: &mut gpui::TestAppContext) {
|
||||||
panel.update(cx, |this, cx| {
|
panel.update(cx, |this, cx| {
|
||||||
let drag = DraggedSelection {
|
let drag = DraggedSelection {
|
||||||
active_selection: this.selection.unwrap(),
|
active_selection: this.selection.unwrap(),
|
||||||
marked_selections: Arc::new(this.marked_entries.clone()),
|
marked_selections: this.marked_entries.clone().into(),
|
||||||
};
|
};
|
||||||
let target_entry = this
|
let target_entry = this
|
||||||
.project
|
.project
|
||||||
|
@ -5562,10 +5562,10 @@ async fn test_highlight_entry_for_selection_drag(cx: &mut gpui::TestAppContext)
|
||||||
worktree_id,
|
worktree_id,
|
||||||
entry_id: child_file.id,
|
entry_id: child_file.id,
|
||||||
},
|
},
|
||||||
marked_selections: Arc::new(BTreeSet::from([SelectedEntry {
|
marked_selections: Arc::new([SelectedEntry {
|
||||||
worktree_id,
|
worktree_id,
|
||||||
entry_id: child_file.id,
|
entry_id: child_file.id,
|
||||||
}])),
|
}]),
|
||||||
};
|
};
|
||||||
let result =
|
let result =
|
||||||
panel.highlight_entry_for_selection_drag(parent_dir, worktree, &dragged_selection, cx);
|
panel.highlight_entry_for_selection_drag(parent_dir, worktree, &dragged_selection, cx);
|
||||||
|
@ -5604,7 +5604,7 @@ async fn test_highlight_entry_for_selection_drag(cx: &mut gpui::TestAppContext)
|
||||||
worktree_id,
|
worktree_id,
|
||||||
entry_id: child_file.id,
|
entry_id: child_file.id,
|
||||||
},
|
},
|
||||||
marked_selections: Arc::new(BTreeSet::from([
|
marked_selections: Arc::new([
|
||||||
SelectedEntry {
|
SelectedEntry {
|
||||||
worktree_id,
|
worktree_id,
|
||||||
entry_id: child_file.id,
|
entry_id: child_file.id,
|
||||||
|
@ -5613,7 +5613,7 @@ async fn test_highlight_entry_for_selection_drag(cx: &mut gpui::TestAppContext)
|
||||||
worktree_id,
|
worktree_id,
|
||||||
entry_id: sibling_file.id,
|
entry_id: sibling_file.id,
|
||||||
},
|
},
|
||||||
])),
|
]),
|
||||||
};
|
};
|
||||||
let result =
|
let result =
|
||||||
panel.highlight_entry_for_selection_drag(parent_dir, worktree, &dragged_selection, cx);
|
panel.highlight_entry_for_selection_drag(parent_dir, worktree, &dragged_selection, cx);
|
||||||
|
@ -5821,6 +5821,186 @@ async fn test_hide_root(cx: &mut gpui::TestAppContext) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
async fn test_compare_selected_files(cx: &mut gpui::TestAppContext) {
|
||||||
|
init_test_with_editor(cx);
|
||||||
|
|
||||||
|
let fs = FakeFs::new(cx.executor().clone());
|
||||||
|
fs.insert_tree(
|
||||||
|
"/root",
|
||||||
|
json!({
|
||||||
|
"file1.txt": "content of file1",
|
||||||
|
"file2.txt": "content of file2",
|
||||||
|
"dir1": {
|
||||||
|
"file3.txt": "content of file3"
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await;
|
||||||
|
let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
|
||||||
|
let cx = &mut VisualTestContext::from_window(*workspace, cx);
|
||||||
|
let panel = workspace.update(cx, ProjectPanel::new).unwrap();
|
||||||
|
|
||||||
|
let file1_path = path!("root/file1.txt");
|
||||||
|
let file2_path = path!("root/file2.txt");
|
||||||
|
select_path_with_mark(&panel, file1_path, cx);
|
||||||
|
select_path_with_mark(&panel, file2_path, cx);
|
||||||
|
|
||||||
|
panel.update_in(cx, |panel, window, cx| {
|
||||||
|
panel.compare_marked_files(&CompareMarkedFiles, window, cx);
|
||||||
|
});
|
||||||
|
cx.executor().run_until_parked();
|
||||||
|
|
||||||
|
workspace
|
||||||
|
.update(cx, |workspace, _, cx| {
|
||||||
|
let active_items = workspace
|
||||||
|
.panes()
|
||||||
|
.iter()
|
||||||
|
.filter_map(|pane| pane.read(cx).active_item())
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
assert_eq!(active_items.len(), 1);
|
||||||
|
let diff_view = active_items
|
||||||
|
.into_iter()
|
||||||
|
.next()
|
||||||
|
.unwrap()
|
||||||
|
.downcast::<FileDiffView>()
|
||||||
|
.expect("Open item should be an FileDiffView");
|
||||||
|
assert_eq!(diff_view.tab_content_text(0, cx), "file1.txt ↔ file2.txt");
|
||||||
|
assert_eq!(
|
||||||
|
diff_view.tab_tooltip_text(cx).unwrap(),
|
||||||
|
format!("{} ↔ {}", file1_path, file2_path)
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let file1_entry_id = find_project_entry(&panel, file1_path, cx).unwrap();
|
||||||
|
let file2_entry_id = find_project_entry(&panel, file2_path, cx).unwrap();
|
||||||
|
let worktree_id = panel.update(cx, |panel, cx| {
|
||||||
|
panel
|
||||||
|
.project
|
||||||
|
.read(cx)
|
||||||
|
.worktrees(cx)
|
||||||
|
.next()
|
||||||
|
.unwrap()
|
||||||
|
.read(cx)
|
||||||
|
.id()
|
||||||
|
});
|
||||||
|
|
||||||
|
let expected_entries = [
|
||||||
|
SelectedEntry {
|
||||||
|
worktree_id,
|
||||||
|
entry_id: file1_entry_id,
|
||||||
|
},
|
||||||
|
SelectedEntry {
|
||||||
|
worktree_id,
|
||||||
|
entry_id: file2_entry_id,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
panel.update(cx, |panel, _cx| {
|
||||||
|
assert_eq!(
|
||||||
|
&panel.marked_entries, &expected_entries,
|
||||||
|
"Should keep marked entries after comparison"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
panel.update(cx, |panel, cx| {
|
||||||
|
panel.project.update(cx, |_, cx| {
|
||||||
|
cx.emit(project::Event::RevealInProjectPanel(file2_entry_id))
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
panel.update(cx, |panel, _cx| {
|
||||||
|
assert_eq!(
|
||||||
|
&panel.marked_entries, &expected_entries,
|
||||||
|
"Marked entries should persist after focusing back on the project panel"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
async fn test_compare_files_context_menu(cx: &mut gpui::TestAppContext) {
|
||||||
|
init_test_with_editor(cx);
|
||||||
|
|
||||||
|
let fs = FakeFs::new(cx.executor().clone());
|
||||||
|
fs.insert_tree(
|
||||||
|
"/root",
|
||||||
|
json!({
|
||||||
|
"file1.txt": "content of file1",
|
||||||
|
"file2.txt": "content of file2",
|
||||||
|
"dir1": {},
|
||||||
|
"dir2": {
|
||||||
|
"file3.txt": "content of file3"
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await;
|
||||||
|
let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
|
||||||
|
let cx = &mut VisualTestContext::from_window(*workspace, cx);
|
||||||
|
let panel = workspace.update(cx, ProjectPanel::new).unwrap();
|
||||||
|
|
||||||
|
// Test 1: When only one file is selected, there should be no compare option
|
||||||
|
select_path(&panel, "root/file1.txt", cx);
|
||||||
|
|
||||||
|
let selected_files = panel.update(cx, |panel, cx| panel.file_abs_paths_to_diff(cx));
|
||||||
|
assert_eq!(
|
||||||
|
selected_files, None,
|
||||||
|
"Should not have compare option when only one file is selected"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Test 2: When multiple files are selected, there should be a compare option
|
||||||
|
select_path_with_mark(&panel, "root/file1.txt", cx);
|
||||||
|
select_path_with_mark(&panel, "root/file2.txt", cx);
|
||||||
|
|
||||||
|
let selected_files = panel.update(cx, |panel, cx| panel.file_abs_paths_to_diff(cx));
|
||||||
|
assert!(
|
||||||
|
selected_files.is_some(),
|
||||||
|
"Should have files selected for comparison"
|
||||||
|
);
|
||||||
|
if let Some((file1, file2)) = selected_files {
|
||||||
|
assert!(
|
||||||
|
file1.to_string_lossy().ends_with("file1.txt")
|
||||||
|
&& file2.to_string_lossy().ends_with("file2.txt"),
|
||||||
|
"Should have file1.txt and file2.txt as the selected files when multi-selecting"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test 3: Selecting a directory shouldn't count as a comparable file
|
||||||
|
select_path_with_mark(&panel, "root/dir1", cx);
|
||||||
|
|
||||||
|
let selected_files = panel.update(cx, |panel, cx| panel.file_abs_paths_to_diff(cx));
|
||||||
|
assert!(
|
||||||
|
selected_files.is_some(),
|
||||||
|
"Directory selection should not affect comparable files"
|
||||||
|
);
|
||||||
|
if let Some((file1, file2)) = selected_files {
|
||||||
|
assert!(
|
||||||
|
file1.to_string_lossy().ends_with("file1.txt")
|
||||||
|
&& file2.to_string_lossy().ends_with("file2.txt"),
|
||||||
|
"Selecting a directory should not affect the number of comparable files"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test 4: Selecting one more file
|
||||||
|
select_path_with_mark(&panel, "root/dir2/file3.txt", cx);
|
||||||
|
|
||||||
|
let selected_files = panel.update(cx, |panel, cx| panel.file_abs_paths_to_diff(cx));
|
||||||
|
assert!(
|
||||||
|
selected_files.is_some(),
|
||||||
|
"Directory selection should not affect comparable files"
|
||||||
|
);
|
||||||
|
if let Some((file1, file2)) = selected_files {
|
||||||
|
assert!(
|
||||||
|
file1.to_string_lossy().ends_with("file2.txt")
|
||||||
|
&& file2.to_string_lossy().ends_with("file3.txt"),
|
||||||
|
"Selecting a directory should not affect the number of comparable files"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn select_path(panel: &Entity<ProjectPanel>, path: impl AsRef<Path>, cx: &mut VisualTestContext) {
|
fn select_path(panel: &Entity<ProjectPanel>, path: impl AsRef<Path>, cx: &mut VisualTestContext) {
|
||||||
let path = path.as_ref();
|
let path = path.as_ref();
|
||||||
panel.update(cx, |panel, cx| {
|
panel.update(cx, |panel, cx| {
|
||||||
|
@ -5855,7 +6035,7 @@ fn select_path_with_mark(
|
||||||
entry_id,
|
entry_id,
|
||||||
};
|
};
|
||||||
if !panel.marked_entries.contains(&entry) {
|
if !panel.marked_entries.contains(&entry) {
|
||||||
panel.marked_entries.insert(entry);
|
panel.marked_entries.push(entry);
|
||||||
}
|
}
|
||||||
panel.selection = Some(entry);
|
panel.selection = Some(entry);
|
||||||
return;
|
return;
|
||||||
|
|
|
@ -16,6 +16,7 @@ use serde_json::{Value, json};
|
||||||
use smallvec::SmallVec;
|
use smallvec::SmallVec;
|
||||||
use std::{
|
use std::{
|
||||||
any::{Any, TypeId, type_name},
|
any::{Any, TypeId, type_name},
|
||||||
|
env,
|
||||||
fmt::Debug,
|
fmt::Debug,
|
||||||
ops::Range,
|
ops::Range,
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
|
@ -126,6 +127,8 @@ pub struct SettingsSources<'a, T> {
|
||||||
pub user: Option<&'a T>,
|
pub user: Option<&'a T>,
|
||||||
/// The user settings for the current release channel.
|
/// The user settings for the current release channel.
|
||||||
pub release_channel: Option<&'a T>,
|
pub release_channel: Option<&'a T>,
|
||||||
|
/// The user settings for the current operating system.
|
||||||
|
pub operating_system: Option<&'a T>,
|
||||||
/// The settings associated with an enabled settings profile
|
/// The settings associated with an enabled settings profile
|
||||||
pub profile: Option<&'a T>,
|
pub profile: Option<&'a T>,
|
||||||
/// The server's settings.
|
/// The server's settings.
|
||||||
|
@ -147,6 +150,7 @@ impl<'a, T: Serialize> SettingsSources<'a, T> {
|
||||||
.chain(self.extensions)
|
.chain(self.extensions)
|
||||||
.chain(self.user)
|
.chain(self.user)
|
||||||
.chain(self.release_channel)
|
.chain(self.release_channel)
|
||||||
|
.chain(self.operating_system)
|
||||||
.chain(self.profile)
|
.chain(self.profile)
|
||||||
.chain(self.server)
|
.chain(self.server)
|
||||||
.chain(self.project.iter().copied())
|
.chain(self.project.iter().copied())
|
||||||
|
@ -336,6 +340,11 @@ impl SettingsStore {
|
||||||
.log_err();
|
.log_err();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let mut os_settings_value = None;
|
||||||
|
if let Some(os_settings) = &self.raw_user_settings.get(env::consts::OS) {
|
||||||
|
os_settings_value = setting_value.deserialize_setting(os_settings).log_err();
|
||||||
|
}
|
||||||
|
|
||||||
let mut profile_value = None;
|
let mut profile_value = None;
|
||||||
if let Some(active_profile) = cx.try_global::<ActiveSettingsProfileName>() {
|
if let Some(active_profile) = cx.try_global::<ActiveSettingsProfileName>() {
|
||||||
if let Some(profiles) = self.raw_user_settings.get("profiles") {
|
if let Some(profiles) = self.raw_user_settings.get("profiles") {
|
||||||
|
@ -366,6 +375,7 @@ impl SettingsStore {
|
||||||
extensions: extension_value.as_ref(),
|
extensions: extension_value.as_ref(),
|
||||||
user: user_value.as_ref(),
|
user: user_value.as_ref(),
|
||||||
release_channel: release_channel_value.as_ref(),
|
release_channel: release_channel_value.as_ref(),
|
||||||
|
operating_system: os_settings_value.as_ref(),
|
||||||
profile: profile_value.as_ref(),
|
profile: profile_value.as_ref(),
|
||||||
server: server_value.as_ref(),
|
server: server_value.as_ref(),
|
||||||
project: &[],
|
project: &[],
|
||||||
|
@ -1092,7 +1102,7 @@ impl SettingsStore {
|
||||||
"$schema": meta_schema,
|
"$schema": meta_schema,
|
||||||
"title": "Zed Settings",
|
"title": "Zed Settings",
|
||||||
"unevaluatedProperties": false,
|
"unevaluatedProperties": false,
|
||||||
// ZedSettings + settings overrides for each release stage / profiles
|
// ZedSettings + settings overrides for each release stage / OS / profiles
|
||||||
"allOf": [
|
"allOf": [
|
||||||
zed_settings_ref,
|
zed_settings_ref,
|
||||||
{
|
{
|
||||||
|
@ -1101,6 +1111,9 @@ impl SettingsStore {
|
||||||
"nightly": zed_settings_override_ref,
|
"nightly": zed_settings_override_ref,
|
||||||
"stable": zed_settings_override_ref,
|
"stable": zed_settings_override_ref,
|
||||||
"preview": zed_settings_override_ref,
|
"preview": zed_settings_override_ref,
|
||||||
|
"linux": zed_settings_override_ref,
|
||||||
|
"macos": zed_settings_override_ref,
|
||||||
|
"windows": zed_settings_override_ref,
|
||||||
"profiles": {
|
"profiles": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"description": "Configures any number of settings profiles.",
|
"description": "Configures any number of settings profiles.",
|
||||||
|
@ -1164,6 +1177,13 @@ impl SettingsStore {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let mut os_settings = None;
|
||||||
|
if let Some(settings) = &self.raw_user_settings.get(env::consts::OS) {
|
||||||
|
if let Some(settings) = setting_value.deserialize_setting(settings).log_err() {
|
||||||
|
os_settings = Some(settings);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let mut profile_settings = None;
|
let mut profile_settings = None;
|
||||||
if let Some(active_profile) = cx.try_global::<ActiveSettingsProfileName>() {
|
if let Some(active_profile) = cx.try_global::<ActiveSettingsProfileName>() {
|
||||||
if let Some(profiles) = self.raw_user_settings.get("profiles") {
|
if let Some(profiles) = self.raw_user_settings.get("profiles") {
|
||||||
|
@ -1184,6 +1204,7 @@ impl SettingsStore {
|
||||||
extensions: extension_settings.as_ref(),
|
extensions: extension_settings.as_ref(),
|
||||||
user: user_settings.as_ref(),
|
user: user_settings.as_ref(),
|
||||||
release_channel: release_channel_settings.as_ref(),
|
release_channel: release_channel_settings.as_ref(),
|
||||||
|
operating_system: os_settings.as_ref(),
|
||||||
profile: profile_settings.as_ref(),
|
profile: profile_settings.as_ref(),
|
||||||
server: server_settings.as_ref(),
|
server: server_settings.as_ref(),
|
||||||
project: &[],
|
project: &[],
|
||||||
|
@ -1237,6 +1258,7 @@ impl SettingsStore {
|
||||||
extensions: extension_settings.as_ref(),
|
extensions: extension_settings.as_ref(),
|
||||||
user: user_settings.as_ref(),
|
user: user_settings.as_ref(),
|
||||||
release_channel: release_channel_settings.as_ref(),
|
release_channel: release_channel_settings.as_ref(),
|
||||||
|
operating_system: os_settings.as_ref(),
|
||||||
profile: profile_settings.as_ref(),
|
profile: profile_settings.as_ref(),
|
||||||
server: server_settings.as_ref(),
|
server: server_settings.as_ref(),
|
||||||
project: &project_settings_stack.iter().collect::<Vec<_>>(),
|
project: &project_settings_stack.iter().collect::<Vec<_>>(),
|
||||||
|
@ -1363,6 +1385,9 @@ impl<T: Settings> AnySettingValue for SettingValue<T> {
|
||||||
release_channel: values
|
release_channel: values
|
||||||
.release_channel
|
.release_channel
|
||||||
.map(|value| value.0.downcast_ref::<T::FileContent>().unwrap()),
|
.map(|value| value.0.downcast_ref::<T::FileContent>().unwrap()),
|
||||||
|
operating_system: values
|
||||||
|
.operating_system
|
||||||
|
.map(|value| value.0.downcast_ref::<T::FileContent>().unwrap()),
|
||||||
profile: values
|
profile: values
|
||||||
.profile
|
.profile
|
||||||
.map(|value| value.0.downcast_ref::<T::FileContent>().unwrap()),
|
.map(|value| value.0.downcast_ref::<T::FileContent>().unwrap()),
|
||||||
|
|
|
@ -867,6 +867,7 @@ impl settings::Settings for ThemeSettings {
|
||||||
.user
|
.user
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.chain(sources.release_channel)
|
.chain(sources.release_channel)
|
||||||
|
.chain(sources.operating_system)
|
||||||
.chain(sources.profile)
|
.chain(sources.profile)
|
||||||
.chain(sources.server)
|
.chain(sources.server)
|
||||||
{
|
{
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
|
use std::rc::Rc;
|
||||||
|
|
||||||
use gpui::{AnyView, ClickEvent};
|
use gpui::{AnyView, ClickEvent};
|
||||||
|
|
||||||
use crate::{ButtonLike, ButtonLikeRounding, ElevationIndex, TintColor, prelude::*};
|
use crate::{ButtonLike, ButtonLikeRounding, ElevationIndex, TintColor, Tooltip, prelude::*};
|
||||||
|
|
||||||
/// The position of a [`ToggleButton`] within a group of buttons.
|
/// The position of a [`ToggleButton`] within a group of buttons.
|
||||||
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
|
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
|
||||||
|
@ -301,6 +303,7 @@ pub struct ButtonConfiguration {
|
||||||
icon: Option<IconName>,
|
icon: Option<IconName>,
|
||||||
on_click: Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>,
|
on_click: Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>,
|
||||||
selected: bool,
|
selected: bool,
|
||||||
|
tooltip: Option<Rc<dyn Fn(&mut Window, &mut App) -> AnyView>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
mod private {
|
mod private {
|
||||||
|
@ -315,6 +318,7 @@ pub struct ToggleButtonSimple {
|
||||||
label: SharedString,
|
label: SharedString,
|
||||||
on_click: Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>,
|
on_click: Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>,
|
||||||
selected: bool,
|
selected: bool,
|
||||||
|
tooltip: Option<Rc<dyn Fn(&mut Window, &mut App) -> AnyView>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ToggleButtonSimple {
|
impl ToggleButtonSimple {
|
||||||
|
@ -326,6 +330,7 @@ impl ToggleButtonSimple {
|
||||||
label: label.into(),
|
label: label.into(),
|
||||||
on_click: Box::new(on_click),
|
on_click: Box::new(on_click),
|
||||||
selected: false,
|
selected: false,
|
||||||
|
tooltip: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -333,6 +338,11 @@ impl ToggleButtonSimple {
|
||||||
self.selected = selected;
|
self.selected = selected;
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn tooltip(mut self, tooltip: impl Fn(&mut Window, &mut App) -> AnyView + 'static) -> Self {
|
||||||
|
self.tooltip = Some(Rc::new(tooltip));
|
||||||
|
self
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl private::ToggleButtonStyle for ToggleButtonSimple {}
|
impl private::ToggleButtonStyle for ToggleButtonSimple {}
|
||||||
|
@ -344,6 +354,7 @@ impl ButtonBuilder for ToggleButtonSimple {
|
||||||
icon: None,
|
icon: None,
|
||||||
on_click: self.on_click,
|
on_click: self.on_click,
|
||||||
selected: self.selected,
|
selected: self.selected,
|
||||||
|
tooltip: self.tooltip,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -353,6 +364,7 @@ pub struct ToggleButtonWithIcon {
|
||||||
icon: IconName,
|
icon: IconName,
|
||||||
on_click: Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>,
|
on_click: Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>,
|
||||||
selected: bool,
|
selected: bool,
|
||||||
|
tooltip: Option<Rc<dyn Fn(&mut Window, &mut App) -> AnyView>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ToggleButtonWithIcon {
|
impl ToggleButtonWithIcon {
|
||||||
|
@ -366,6 +378,7 @@ impl ToggleButtonWithIcon {
|
||||||
icon,
|
icon,
|
||||||
on_click: Box::new(on_click),
|
on_click: Box::new(on_click),
|
||||||
selected: false,
|
selected: false,
|
||||||
|
tooltip: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -373,6 +386,11 @@ impl ToggleButtonWithIcon {
|
||||||
self.selected = selected;
|
self.selected = selected;
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn tooltip(mut self, tooltip: impl Fn(&mut Window, &mut App) -> AnyView + 'static) -> Self {
|
||||||
|
self.tooltip = Some(Rc::new(tooltip));
|
||||||
|
self
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl private::ToggleButtonStyle for ToggleButtonWithIcon {}
|
impl private::ToggleButtonStyle for ToggleButtonWithIcon {}
|
||||||
|
@ -384,6 +402,7 @@ impl ButtonBuilder for ToggleButtonWithIcon {
|
||||||
icon: Some(self.icon),
|
icon: Some(self.icon),
|
||||||
on_click: self.on_click,
|
on_click: self.on_click,
|
||||||
selected: self.selected,
|
selected: self.selected,
|
||||||
|
tooltip: self.tooltip,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -486,11 +505,13 @@ impl<T: ButtonBuilder, const COLS: usize, const ROWS: usize> RenderOnce
|
||||||
icon,
|
icon,
|
||||||
on_click,
|
on_click,
|
||||||
selected,
|
selected,
|
||||||
|
tooltip,
|
||||||
} = button.into_configuration();
|
} = button.into_configuration();
|
||||||
|
|
||||||
let entry_index = row_index * COLS + col_index;
|
let entry_index = row_index * COLS + col_index;
|
||||||
|
|
||||||
ButtonLike::new((self.group_name, entry_index))
|
ButtonLike::new((self.group_name, entry_index))
|
||||||
|
.rounding(None)
|
||||||
.when_some(self.tab_index, |this, tab_index| {
|
.when_some(self.tab_index, |this, tab_index| {
|
||||||
this.tab_index(tab_index + entry_index as isize)
|
this.tab_index(tab_index + entry_index as isize)
|
||||||
})
|
})
|
||||||
|
@ -498,7 +519,6 @@ impl<T: ButtonBuilder, const COLS: usize, const ROWS: usize> RenderOnce
|
||||||
this.toggle_state(true)
|
this.toggle_state(true)
|
||||||
.selected_style(ButtonStyle::Tinted(TintColor::Accent))
|
.selected_style(ButtonStyle::Tinted(TintColor::Accent))
|
||||||
})
|
})
|
||||||
.rounding(None)
|
|
||||||
.when(self.style == ToggleButtonGroupStyle::Filled, |button| {
|
.when(self.style == ToggleButtonGroupStyle::Filled, |button| {
|
||||||
button.style(ButtonStyle::Filled)
|
button.style(ButtonStyle::Filled)
|
||||||
})
|
})
|
||||||
|
@ -527,6 +547,9 @@ impl<T: ButtonBuilder, const COLS: usize, const ROWS: usize> RenderOnce
|
||||||
|this| this.color(Color::Accent),
|
|this| this.color(Color::Accent),
|
||||||
)),
|
)),
|
||||||
)
|
)
|
||||||
|
.when_some(tooltip, |this, tooltip| {
|
||||||
|
this.tooltip(move |window, cx| tooltip(window, cx))
|
||||||
|
})
|
||||||
.on_click(on_click)
|
.on_click(on_click)
|
||||||
.into_any_element()
|
.into_any_element()
|
||||||
})
|
})
|
||||||
|
@ -920,6 +943,23 @@ impl<T: ButtonBuilder, const COLS: usize, const ROWS: usize> Component
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
)])
|
)])
|
||||||
|
.children(vec![single_example(
|
||||||
|
"With Tooltips",
|
||||||
|
ToggleButtonGroup::single_row(
|
||||||
|
"with_tooltips",
|
||||||
|
[
|
||||||
|
ToggleButtonSimple::new("First", |_, _, _| {})
|
||||||
|
.tooltip(Tooltip::text("This is a tooltip. Hello!")),
|
||||||
|
ToggleButtonSimple::new("Second", |_, _, _| {})
|
||||||
|
.tooltip(Tooltip::text("This is a tooltip. Hey?")),
|
||||||
|
ToggleButtonSimple::new("Third", |_, _, _| {})
|
||||||
|
.tooltip(Tooltip::text("This is a tooltip. Get out of here now!")),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
.selected_index(1)
|
||||||
|
.button_width(rems_from_px(100.))
|
||||||
|
.into_any_element(),
|
||||||
|
)])
|
||||||
.into_any_element(),
|
.into_any_element(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,10 +14,10 @@ use crate::prelude::*;
|
||||||
#[strum(serialize_all = "snake_case")]
|
#[strum(serialize_all = "snake_case")]
|
||||||
pub enum VectorName {
|
pub enum VectorName {
|
||||||
AiGrid,
|
AiGrid,
|
||||||
CertifiedUserStamp,
|
|
||||||
DebuggerGrid,
|
DebuggerGrid,
|
||||||
Grid,
|
Grid,
|
||||||
ProTrialStamp,
|
ProTrialStamp,
|
||||||
|
ProUserStamp,
|
||||||
ZedLogo,
|
ZedLogo,
|
||||||
ZedXCopilot,
|
ZedXCopilot,
|
||||||
}
|
}
|
||||||
|
|
|
@ -62,7 +62,7 @@ pub struct SelectedEntry {
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct DraggedSelection {
|
pub struct DraggedSelection {
|
||||||
pub active_selection: SelectedEntry,
|
pub active_selection: SelectedEntry,
|
||||||
pub marked_selections: Arc<BTreeSet<SelectedEntry>>,
|
pub marked_selections: Arc<[SelectedEntry]>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl DraggedSelection {
|
impl DraggedSelection {
|
||||||
|
|
|
@ -1086,6 +1086,7 @@ pub struct Workspace {
|
||||||
follower_states: HashMap<CollaboratorId, FollowerState>,
|
follower_states: HashMap<CollaboratorId, FollowerState>,
|
||||||
last_leaders_by_pane: HashMap<WeakEntity<Pane>, CollaboratorId>,
|
last_leaders_by_pane: HashMap<WeakEntity<Pane>, CollaboratorId>,
|
||||||
window_edited: bool,
|
window_edited: bool,
|
||||||
|
last_window_title: Option<String>,
|
||||||
dirty_items: HashMap<EntityId, Subscription>,
|
dirty_items: HashMap<EntityId, Subscription>,
|
||||||
active_call: Option<(Entity<ActiveCall>, Vec<Subscription>)>,
|
active_call: Option<(Entity<ActiveCall>, Vec<Subscription>)>,
|
||||||
leader_updates_tx: mpsc::UnboundedSender<(PeerId, proto::UpdateFollowers)>,
|
leader_updates_tx: mpsc::UnboundedSender<(PeerId, proto::UpdateFollowers)>,
|
||||||
|
@ -1418,6 +1419,7 @@ impl Workspace {
|
||||||
last_leaders_by_pane: Default::default(),
|
last_leaders_by_pane: Default::default(),
|
||||||
dispatching_keystrokes: Default::default(),
|
dispatching_keystrokes: Default::default(),
|
||||||
window_edited: false,
|
window_edited: false,
|
||||||
|
last_window_title: None,
|
||||||
dirty_items: Default::default(),
|
dirty_items: Default::default(),
|
||||||
active_call,
|
active_call,
|
||||||
database_id: workspace_id,
|
database_id: workspace_id,
|
||||||
|
@ -4403,7 +4405,13 @@ impl Workspace {
|
||||||
title.push_str(" ↗");
|
title.push_str(" ↗");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let Some(last_title) = self.last_window_title.as_ref() {
|
||||||
|
if &title == last_title {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
window.set_window_title(&title);
|
window.set_window_title(&title);
|
||||||
|
self.last_window_title = Some(title);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn update_window_edited(&mut self, window: &mut Window, cx: &mut App) {
|
fn update_window_edited(&mut self, window: &mut Window, cx: &mut App) {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue