Compare commits

..

1 Commits

Author SHA1 Message Date
Richard Feldman
de95efb7bb wip 2025-03-28 10:34:57 -04:00
1438 changed files with 50633 additions and 72645 deletions

View File

@@ -14,10 +14,10 @@ linker = "clang"
rustflags = ["-C", "link-arg=-fuse-ld=mold"]
[target.aarch64-apple-darwin]
rustflags = ["-C", "link-args=-all_load"]
rustflags = ["-C", "link-args=-Objc -all_load"]
[target.x86_64-apple-darwin]
rustflags = ["-C", "link-args=-all_load"]
rustflags = ["-C", "link-args=-Objc -all_load"]
[target.'cfg(target_os = "windows")']
rustflags = [

View File

@@ -1,43 +0,0 @@
# This file contains settings for `cargo hakari`.
# See https://docs.rs/cargo-hakari/latest/cargo_hakari/config for a full list of options.
hakari-package = "workspace-hack"
resolver = "2"
dep-format-version = "4"
workspace-hack-line-style = "workspace-dotted"
# this should be the same list as "targets" in ../rust-toolchain.toml
platforms = [
"x86_64-apple-darwin",
"aarch64-apple-darwin",
"x86_64-unknown-linux-gnu",
"aarch64-unknown-linux-gnu",
"x86_64-pc-windows-msvc",
"x86_64-unknown-linux-musl", # remote server
]
[traversal-excludes]
workspace-members = [
"remote_server",
]
third-party = [
{ name = "reqwest", version = "0.11.27" },
]
[final-excludes]
workspace-members = [
"zed_extension_api",
# exclude all extensions
"zed_emmet",
"zed_glsl",
"zed_html",
"perplexity",
"zed_proto",
"zed_ruff",
"slash_commands_example",
"zed_snippets",
"zed_test_extension",
"zed_toml",
]

View File

@@ -1,36 +0,0 @@
name: Bug Report (Agent Panel)
description: Zed Agent Panel Bugs
type: "Bug"
labels: ["agent", "ai"]
title: "Agent Panel: <a short description of the Agent Panel bug>"
body:
- type: textarea
attributes:
label: Summary
description: Describe the bug with a one line summary, and provide detailed reproduction steps
value: |
<!-- Please insert a one line summary of the issue below -->
SUMMARY_SENTENCE_HERE
### Description
<!-- Describe with sufficient detail to reproduce from a clean Zed install. -->
<!-- Please include the LLM provider and model name you are using -->
Steps to trigger the problem:
1.
2.
3.
Actual Behavior:
Expected Behavior:
validations:
required: true
- type: textarea
id: environment
attributes:
label: Zed Version and System Specs
description: 'Open Zed, and in the command palette select "zed: Copy System Specs Into Clipboard"'
placeholder: |
Output of "zed: Copy System Specs Into Clipboard"
validations:
required: true

View File

@@ -1,36 +0,0 @@
name: Bug Report (Edit Predictions)
description: Zed Edit Predictions bugs
type: "Bug"
labels: ["ai", "inline completion", "zeta"]
title: "Edit Predictions: <a short description of the Edit Prediction bug>"
body:
- type: textarea
attributes:
label: Summary
description: Describe the bug with a one line summary, and provide detailed reproduction steps
value: |
<!-- Please insert a one line summary of the issue below -->
SUMMARY_SENTENCE_HERE
### Description
<!-- Describe with sufficient detail to reproduce from a clean Zed install. -->
<!-- Please include the LLM provider and model name you are using -->
Steps to trigger the problem:
1.
2.
3.
Actual Behavior:
Expected Behavior:
validations:
required: true
- type: textarea
id: environment
attributes:
label: Zed Version and System Specs
description: 'Open Zed, and in the command palette select "zed: Copy System Specs Into Clipboard"'
placeholder: |
Output of "zed: Copy System Specs Into Clipboard"
validations:
required: true

View File

@@ -1,35 +0,0 @@
name: Bug Report (Git)
description: Zed Git-Related Bugs
type: "Bug"
labels: ["git"]
title: "Git: <a short description of the Git bug>"
body:
- type: textarea
attributes:
label: Summary
description: Describe the bug with a one line summary, and provide detailed reproduction steps
value: |
<!-- Please insert a one line summary of the issue below -->
SUMMARY_SENTENCE_HERE
### Description
<!-- Describe with sufficient detail to reproduce from a clean Zed install. -->
Steps to trigger the problem:
1.
2.
3.
Actual Behavior:
Expected Behavior:
validations:
required: true
- type: textarea
id: environment
attributes:
label: Zed Version and System Specs
description: 'Open Zed, and in the command palette select "zed: Copy System Specs Into Clipboard"'
placeholder: |
Output of "zed: Copy System Specs Into Clipboard"
validations:
required: true

View File

@@ -1,56 +0,0 @@
name: Bug Report (Other)
description: |
Something else is broken in Zed (exclude crashing).
type: "Bug"
body:
- type: textarea
attributes:
label: Summary
description: Provide a one sentence summary and detailed reproduction steps
value: |
<!-- Begin your issue with a one sentence summary -->
SUMMARY_SENTENCE_HERE
### Description
<!-- Describe with sufficient detail to reproduce from a clean Zed install.
- Any code must be sufficient to reproduce (include context!)
- Code must as text, not just as a screenshot.
- Issues with insufficient detail may be summarily closed.
-->
Steps to reproduce:
1.
2.
3.
4.
Expected Behavior:
Actual Behavior:
<!-- Before Submitting, did you:
1. Include settings.json, keymap.json, .editorconfig if relevant?
2. Check your Zed.log for relevant errors? (please include!)
3. Click Preview to ensure everything looks right?
4. Hide videos, large images and logs in ``` inside collapsible blocks:
<details><summary>click to expand</summary>
```json
```
</details>
-->
validations:
required: true
- type: textarea
id: environment
attributes:
label: Zed Version and System Specs
description: |
Open Zed, from the command palette select "zed: Copy System Specs Into Clipboard"
placeholder: |
Output of "zed: Copy System Specs Into Clipboard"
validations:
required: true

57
.github/ISSUE_TEMPLATE/1_bug_report.yml vendored Normal file
View File

@@ -0,0 +1,57 @@
name: Bug Report
description: |
Something is broken in Zed (exclude crashing).
type: "Bug"
body:
- type: textarea
attributes:
label: Summary
description: Describe the bug with a one line summary, and provide detailed reproduction steps
value: |
<!-- Please insert a one line summary of the issue below -->
SUMMARY_SENTENCE_HERE
<!-- Be verbose: Include all steps necessary to reproduce from a clean Zed installation. -->
<!-- Code snippets are better than images, a repository link that reproduces the issue is ideal. -->
Steps to trigger the problem:
1.
2.
3.
4.
Actual Behavior:
Expected Behavior:
<!--
Is there anything additional necessary to reproduce this issue?
- settings.json, keymap.json, .editorconfig etc?
- Does it happen intermittently or only with specific projects / file types?
- Have you found a workaround?
Did you check your Zed.log to see if there is any relevant details there?
- When including large items (videos, screenshots, logs, configs) please wrap with:
<details><summary>See inside for XXXXYYY</summary>
```shell
code
```
</details>
-->
validations:
required: true
- type: textarea
id: environment
attributes:
label: Zed Version and System Specs
description: 'Open Zed, and in the command palette select "zed: Copy System Specs Into Clipboard"'
placeholder: |
Output of "zed: Copy System Specs Into Clipboard"
validations:
required: true

View File

@@ -5,12 +5,10 @@ body:
- type: textarea
attributes:
label: Summary
description: Summarize the issue with detailed reproduction steps
description: Describe the bug with a one line summary, and provide detailed reproduction steps
value: |
<!-- Begin your issue with a one sentence summary -->
SUMMARY_SENTENCE_HERE
<!-- Please insert a one line summary of the issue below -->
### Description
<!-- Include all steps necessary to reproduce from a clean Zed installation. Be verbose -->
Steps to trigger the problem:
1.
@@ -18,6 +16,7 @@ body:
3.
Actual Behavior:
Expected Behavior:
validations:
@@ -41,11 +40,10 @@ body:
value: |
<details><summary>Zed.log</summary>
<!-- Paste your log inside the code block. -->
```log
<!-- Click below this line and paste or drag-and-drop your log-->
```
</details>
```
<!-- Click above this line and paste or drag-and-drop your log--></details>
validations:
required: false

View File

@@ -1,19 +0,0 @@
name: Other [Staff Only]
description: Zed Staff Only
body:
- type: textarea
attributes:
label: Summary
value: |
<!-- Please insert a one line summary of the issue below -->
SUMMARY_SENTENCE_HERE
### Description
IF YOU DO NOT WORK FOR ZED INDUSTRIES DO NOT CREATE ISSUES WITH THIS TEMPLATE.
THEY WILL BE AUTO-CLOSED AND MAY RESULT IN YOU BEING BANNED FROM THE ZED ISSUE TRACKER.
FEATURE REQUESTS / SUPPORT REQUESTS SHOULD BE OPENED AS DISCUSSIONS:
https://github.com/zed-industries/zed/discussions/new/choose
validations:
required: true

View File

@@ -4,6 +4,9 @@ contact_links:
- name: Feature Request
url: https://github.com/zed-industries/zed/discussions/new/choose
about: To request a feature, open a new Discussion in one of the appropriate Discussion categories
- name: "Zed Discord"
- name: Zed Discussion Forum
url: https://github.com/zed-industries/zed/discussions
about: A community discussion forum
- name: "Zed Discord: #Support Channel"
url: https://zed.dev/community-links
about: Real-time discussion and user support

View File

@@ -23,4 +23,4 @@ runs:
- name: Run tests
shell: pwsh
working-directory: ${{ inputs.working-directory }}
run: cargo nextest run --workspace --no-fail-fast --config='profile.dev.debug="limited"'
run: cargo nextest run --workspace --no-fail-fast

View File

@@ -110,39 +110,6 @@ jobs:
input: "crates/proto/proto/"
against: "https://github.com/${GITHUB_REPOSITORY}.git#branch=${BUF_BASE_BRANCH},subdir=crates/proto/proto/"
workspace_hack:
timeout-minutes: 60
name: Check workspace-hack crate
needs: [job_spec]
if: |
github.repository_owner == 'zed-industries' &&
needs.job_spec.outputs.run_tests == 'true'
runs-on:
- buildjet-8vcpu-ubuntu-2204
steps:
- name: Checkout repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
- name: Add Rust to the PATH
run: echo "$HOME/.cargo/bin" >> $GITHUB_PATH
- name: Install cargo-hakari
uses: clechasseur/rs-cargo@8435b10f6e71c2e3d4d3b7573003a8ce4bfc6386 # v2
with:
command: install
args: cargo-hakari@0.9.35
- name: Check workspace-hack Cargo.toml is up-to-date
run: |
cargo hakari generate --diff || {
echo "To fix, run script/update-workspace-hack or script/update-workspace-hack.ps1";
false
}
- name: Check all crates depend on workspace-hack
run: |
cargo hakari manage-deps --dry-run || {
echo "To fix, run script/update-workspace-hack or script/update-workspace-hack.ps1"
false
}
style:
timeout-minutes: 60
name: Check formatting and spelling
@@ -225,7 +192,7 @@ jobs:
- name: Check for new vulnerable dependencies
if: github.event_name == 'pull_request'
uses: actions/dependency-review-action@67d4f4bd7a9b17a0db54d2a7519187c65e339de8 # v4
uses: actions/dependency-review-action@3b139cfc5fae8b618d3eae3675e383bb1769c019 # v4
with:
license-check: false
@@ -242,6 +209,7 @@ jobs:
cargo check -p workspace
cargo build -p remote_server
cargo check -p gpui --examples
script/check-rust-livekit-macos
# Since the macOS runners are stateful, so we need to remove the config file to prevent potential bug.
- name: Clean CI config file
@@ -465,8 +433,6 @@ jobs:
- job_spec
- style
- migration_checks
# run_tests: If adding required tests, add them here and to script below.
- workspace_hack
- linux_tests
- build_remote_server
- macos_tests
@@ -483,14 +449,11 @@ jobs:
# Only check test jobs if they were supposed to run
if [[ "${{ needs.job_spec.outputs.run_tests }}" == "true" ]]; then
[[ "${{ needs.workspace_hack.result }}" != 'success' ]] && { RET_CODE=1; echo "Workspace Hack failed"; }
[[ "${{ needs.macos_tests.result }}" != 'success' ]] && { RET_CODE=1; echo "macOS tests failed"; }
[[ "${{ needs.linux_tests.result }}" != 'success' ]] && { RET_CODE=1; echo "Linux tests failed"; }
[[ "${{ needs.windows_tests.result }}" != 'success' ]] && { RET_CODE=1; echo "Windows tests failed"; }
[[ "${{ needs.windows_clippy.result }}" != 'success' ]] && { RET_CODE=1; echo "Windows clippy failed"; }
[[ "${{ needs.build_remote_server.result }}" != 'success' ]] && { RET_CODE=1; echo "Remote server build failed"; }
# This check is intentionally disabled. See: https://github.com/zed-industries/zed/pull/28431
# [[ "${{ needs.migration_checks.result }}" != 'success' ]] && { RET_CODE=1; echo "Migration Checks failed"; }
fi
if [[ "$RET_CODE" -eq 0 ]]; then
echo "All tests passed successfully!"
@@ -594,7 +557,7 @@ jobs:
timeout-minutes: 60
name: Linux x86_x64 release bundle
runs-on:
- buildjet-16vcpu-ubuntu-2004 # ubuntu 20.04 for minimal glibc
- buildjet-16vcpu-ubuntu-2004
if: |
startsWith(github.ref, 'refs/tags/v')
|| contains(github.event.pull_request.labels.*.name, 'run-bundling')
@@ -622,23 +585,26 @@ jobs:
- name: Create Linux .tar.gz bundle
run: script/bundle-linux
- name: Upload Artifact to Workflow - zed (run-bundling)
- name: Upload Linux bundle to workflow run if main branch or specific label
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
if: contains(github.event.pull_request.labels.*.name, 'run-bundling')
if: |
github.ref == 'refs/heads/main'
|| contains(github.event.pull_request.labels.*.name, 'run-bundling')
with:
name: zed-${{ github.event.pull_request.head.sha || github.sha }}-x86_64-unknown-linux-gnu.tar.gz
path: target/release/zed-*.tar.gz
- name: Upload Artifact to Workflow - zed-remote-server (run-bundling)
- name: Upload Linux remote server to workflow run if main branch or specific label
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
if: contains(github.event.pull_request.labels.*.name, 'run-bundling')
if: |
github.ref == 'refs/heads/main'
|| contains(github.event.pull_request.labels.*.name, 'run-bundling')
with:
name: zed-remote-server-${{ github.event.pull_request.head.sha || github.sha }}-x86_64-unknown-linux-gnu.gz
path: target/zed-remote-server-linux-x86_64.gz
- name: Upload Artifacts to release
- name: Upload app bundle to release
uses: softprops/action-gh-release@de2c0eb89ae2a093876385947365aca7b0e5f844 # v1
if: ${{ !(contains(github.event.pull_request.labels.*.name, 'run-bundling')) }}
with:
draft: true
prerelease: ${{ env.RELEASE_CHANNEL == 'preview' }}
@@ -677,26 +643,29 @@ jobs:
# This exports RELEASE_CHANNEL into env (GITHUB_ENV)
script/determine-release-channel
- name: Create and upload Linux .tar.gz bundles
- name: Create and upload Linux .tar.gz bundle
run: script/bundle-linux
- name: Upload Artifact to Workflow - zed (run-bundling)
- name: Upload Linux bundle to workflow run if main branch or specific label
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
if: contains(github.event.pull_request.labels.*.name, 'run-bundling')
if: |
github.ref == 'refs/heads/main'
|| contains(github.event.pull_request.labels.*.name, 'run-bundling')
with:
name: zed-${{ github.event.pull_request.head.sha || github.sha }}-aarch64-unknown-linux-gnu.tar.gz
path: target/release/zed-*.tar.gz
- name: Upload Artifact to Workflow - zed-remote-server (run-bundling)
- name: Upload Linux remote server to workflow run if main branch or specific label
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
if: contains(github.event.pull_request.labels.*.name, 'run-bundling')
if: |
github.ref == 'refs/heads/main'
|| contains(github.event.pull_request.labels.*.name, 'run-bundling')
with:
name: zed-remote-server-${{ github.event.pull_request.head.sha || github.sha }}-aarch64-unknown-linux-gnu.gz
path: target/zed-remote-server-linux-aarch64.gz
- name: Upload Artifacts to release
- name: Upload app bundle to release
uses: softprops/action-gh-release@de2c0eb89ae2a093876385947365aca7b0e5f844 # v1
if: ${{ !(contains(github.event.pull_request.labels.*.name, 'run-bundling')) }}
with:
draft: true
prerelease: ${{ env.RELEASE_CHANNEL == 'preview' }}
@@ -706,51 +675,6 @@ jobs:
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
nix-build:
timeout-minutes: 60
name: Nix Build
continue-on-error: true
if: github.repository_owner == 'zed-industries' && contains(github.event.pull_request.labels.*.name, 'run-nix')
strategy:
fail-fast: false
matrix:
system:
- os: x86 Linux
runner: buildjet-16vcpu-ubuntu-2204
install_nix: true
- os: arm Mac
runner: [macOS, ARM64, test]
install_nix: false
runs-on: ${{ matrix.system.runner }}
env:
ZED_CLIENT_CHECKSUM_SEED: ${{ secrets.ZED_CLIENT_CHECKSUM_SEED }}
ZED_CLOUD_PROVIDER_ADDITIONAL_MODELS_JSON: ${{ secrets.ZED_CLOUD_PROVIDER_ADDITIONAL_MODELS_JSON }}
GIT_LFS_SKIP_SMUDGE: 1 # breaks the livekit rust sdk examples which we don't actually depend on
steps:
- name: Checkout repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
with:
clean: false
- name: Set path
if: ${{ ! matrix.system.install_nix }}
run: |
echo "/nix/var/nix/profiles/default/bin" >> $GITHUB_PATH
echo "/Users/administrator/.nix-profile/bin" >> $GITHUB_PATH
- uses: cachix/install-nix-action@d1ca217b388ee87b2507a9a93bf01368bde7cec2 # v31
if: ${{ matrix.system.install_nix }}
with:
github_access_token: ${{ secrets.GITHUB_TOKEN }}
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
with:
name: zed-industries
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
skipPush: true
- run: nix build .#debug
- name: Limit /nix/store to 50GB
run: "[ $(du -sm /nix/store | cut -f1) -gt 50000 ] && nix-collect-garbage -d"
auto-release-preview:
name: Auto release preview
if: |

View File

@@ -59,9 +59,7 @@ jobs:
if: steps.check-promotion-from-preview.outputs.was_promoted_from_preview == 'true'
run: |
TAG="${{ github.event.release.tag_name }}"
cat << 'EOF' > release_body.txt
${{ github.event.release.body }}
EOF
echo \"${{ toJSON(github.event.release.body) }}\" > release_body.txt
jq -n --arg tag "$TAG" --rawfile body release_body.txt '{version: $tag, markdown_body: $body}' \
> release_data.json
curl -X POST "https://zed.dev/api/send_release_notes_email" \

View File

@@ -117,10 +117,12 @@ jobs:
export ZED_KUBE_NAMESPACE=production
export ZED_COLLAB_LOAD_BALANCER_SIZE_UNIT=10
export ZED_API_LOAD_BALANCER_SIZE_UNIT=2
export ZED_LLM_LOAD_BALANCER_SIZE_UNIT=2
elif [[ $GITHUB_REF_NAME = "collab-staging" ]]; then
export ZED_KUBE_NAMESPACE=staging
export ZED_COLLAB_LOAD_BALANCER_SIZE_UNIT=1
export ZED_API_LOAD_BALANCER_SIZE_UNIT=1
export ZED_LLM_LOAD_BALANCER_SIZE_UNIT=1
else
echo "cowardly refusing to deploy from an unknown branch"
exit 1
@@ -145,3 +147,9 @@ jobs:
envsubst < crates/collab/k8s/collab.template.yml | kubectl apply -f -
kubectl -n "$ZED_KUBE_NAMESPACE" rollout status deployment/$ZED_SERVICE_NAME --watch
echo "deployed ${ZED_SERVICE_NAME} to ${ZED_KUBE_NAMESPACE}"
export ZED_SERVICE_NAME=llm
export ZED_LOAD_BALANCER_SIZE_UNIT=$ZED_LLM_LOAD_BALANCER_SIZE_UNIT
envsubst < crates/collab/k8s/collab.template.yml | kubectl apply -f -
kubectl -n "$ZED_KUBE_NAMESPACE" rollout status deployment/$ZED_SERVICE_NAME --watch
echo "deployed ${ZED_SERVICE_NAME} to ${ZED_KUBE_NAMESPACE}"

View File

@@ -184,6 +184,9 @@ jobs:
- os: arm Mac
runner: [macOS, ARM64, test]
install_nix: false
- os: arm Linux
runner: buildjet-16vcpu-ubuntu-2204-arm
install_nix: true
if: github.repository_owner == 'zed-industries'
runs-on: ${{ matrix.system.runner }}
needs: tests
@@ -206,7 +209,7 @@ jobs:
echo "/nix/var/nix/profiles/default/bin" >> $GITHUB_PATH
echo "/Users/administrator/.nix-profile/bin" >> $GITHUB_PATH
- uses: cachix/install-nix-action@d1ca217b388ee87b2507a9a93bf01368bde7cec2 # v31
- uses: cachix/install-nix-action@02a151ada4993995686f9ed4f1be7cfbb229e56f # v31
if: ${{ matrix.system.install_nix }}
with:
github_access_token: ${{ secrets.GITHUB_TOKEN }}
@@ -216,8 +219,7 @@ jobs:
name: zed-industries
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
- run: nix build
- name: Limit /nix/store to 50GB
run: '[ $(du -sm /nix/store | cut -f1) -gt 50000 ] && nix-collect-garbage -d'
- run: nix-collect-garbage -d
update-nightly-tag:
name: Update nightly tag

View File

@@ -1,14 +1,14 @@
[
{
"label": "Debug Zed (CodeLLDB)",
"adapter": "CodeLLDB",
"label": "Debug Zed with LLDB",
"adapter": "lldb",
"program": "$ZED_WORKTREE_ROOT/target/debug/zed",
"request": "launch",
"cwd": "$ZED_WORKTREE_ROOT"
},
{
"label": "Debug Zed (GDB)",
"adapter": "GDB",
"label": "Debug Zed with GDB",
"adapter": "gdb",
"program": "$ZED_WORKTREE_ROOT/target/debug/zed",
"request": "launch",
"cwd": "$ZED_WORKTREE_ROOT",

2986
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -2,12 +2,13 @@
resolver = "2"
members = [
"crates/activity_indicator",
"crates/agent",
"crates/anthropic",
"crates/askpass",
"crates/assets",
"crates/assistant",
"crates/assistant2",
"crates/assistant_context_editor",
"crates/assistant_eval",
"crates/assistant_settings",
"crates/assistant_slash_command",
"crates/assistant_slash_commands",
@@ -15,7 +16,6 @@ members = [
"crates/assistant_tools",
"crates/audio",
"crates/auto_update",
"crates/auto_update_helper",
"crates/auto_update_ui",
"crates/aws_http_client",
"crates/bedrock",
@@ -46,7 +46,6 @@ members = [
"crates/diagnostics",
"crates/docs_preprocessor",
"crates/editor",
"crates/eval",
"crates/evals",
"crates/extension",
"crates/extension_api",
@@ -88,6 +87,7 @@ members = [
"crates/languages",
"crates/livekit_api",
"crates/livekit_client",
"crates/livekit_client_macos",
"crates/lmstudio",
"crates/lsp",
"crates/markdown",
@@ -193,14 +193,13 @@ members = [
# Tooling
#
"tooling/workspace-hack",
"tooling/xtask",
]
default-members = ["crates/zed"]
[workspace.package]
publish = false
edition = "2024"
edition = "2021"
[workspace.dependencies]
@@ -209,13 +208,14 @@ edition = "2024"
#
activity_indicator = { path = "crates/activity_indicator" }
agent = { path = "crates/agent" }
ai = { path = "crates/ai" }
anthropic = { path = "crates/anthropic" }
askpass = { path = "crates/askpass" }
assets = { path = "crates/assets" }
assistant = { path = "crates/assistant" }
assistant2 = { path = "crates/assistant2" }
assistant_context_editor = { path = "crates/assistant_context_editor" }
assistant_eval = { path = "crates/assistant_eval" }
assistant_settings = { path = "crates/assistant_settings" }
assistant_slash_command = { path = "crates/assistant_slash_command" }
assistant_slash_commands = { path = "crates/assistant_slash_commands" }
@@ -223,7 +223,6 @@ assistant_tool = { path = "crates/assistant_tool" }
assistant_tools = { path = "crates/assistant_tools" }
audio = { path = "crates/audio" }
auto_update = { path = "crates/auto_update" }
auto_update_helper = { path = "crates/auto_update_helper" }
auto_update_ui = { path = "crates/auto_update_ui" }
aws_http_client = { path = "crates/aws_http_client" }
bedrock = { path = "crates/bedrock" }
@@ -293,6 +292,7 @@ language_tools = { path = "crates/language_tools" }
languages = { path = "crates/languages" }
livekit_api = { path = "crates/livekit_api" }
livekit_client = { path = "crates/livekit_client" }
livekit_client_macos = { path = "crates/livekit_client_macos" }
lmstudio = { path = "crates/lmstudio" }
lsp = { path = "crates/lsp" }
markdown = { path = "crates/markdown" }
@@ -397,18 +397,14 @@ async-pipe = { git = "https://github.com/zed-industries/async-pipe-rs", rev = "8
async-recursion = "1.0.0"
async-tar = "0.5.0"
async-trait = "0.1"
async-tungstenite = "0.29.1"
async-tungstenite = "0.28"
async-watch = "0.3.1"
async_zip = { version = "0.0.17", features = ["deflate", "deflate64"] }
aws-config = { version = "1.6.1", features = ["behavior-version-latest"] }
aws-credential-types = { version = "1.2.2", features = [
"hardcoded-credentials",
] }
aws-sdk-bedrockruntime = { version = "1.80.0", features = [
"behavior-version-latest",
] }
aws-smithy-runtime-api = { version = "1.7.4", features = ["http-1x", "client"] }
aws-smithy-types = { version = "1.3.0", features = ["http-body-1-x"] }
aws-config = { version = "1.5.16", features = ["behavior-version-latest"] }
aws-credential-types = { version = "1.2.1", features = ["hardcoded-credentials"] }
aws-sdk-bedrockruntime = { version = "1.73.0", features = ["behavior-version-latest"] }
aws-smithy-runtime-api = { version = "1.7.3", features = ["http-1x", "client"] }
aws-smithy-types = { version = "1.2.13", features = ["http-body-1-x"] }
base64 = "0.22"
bitflags = "2.6.0"
blade-graphics = { git = "https://github.com/kvark/blade", rev = "b16f5c7bd873c7126f48c82c39e7ae64602ae74f" }
@@ -424,13 +420,12 @@ circular-buffer = "1.0"
clap = { version = "4.4", features = ["derive"] }
cocoa = "0.26"
cocoa-foundation = "0.2.0"
core-video = { version = "0.4.3", features = ["metal"] }
convert_case = "0.8.0"
core-foundation = "0.10.0"
core-foundation = "0.9.3"
core-foundation-sys = "0.8.6"
ctor = "0.4.0"
dashmap = "6.0"
dap-types = { git = "https://github.com/zed-industries/dap-types", rev = "be69a016ba710191b9fdded28c8b042af4b617f7" }
dap-types = { git = "https://github.com/zed-industries/dap-types", rev = "bfd4af0" }
derive_more = "0.99.17"
dirs = "4.0"
ec4rs = "1.1"
@@ -445,7 +440,6 @@ futures-lite = "1.13"
git2 = { version = "0.20.1", default-features = false }
globset = "0.4"
handlebars = "4.3"
heck = "0.5"
heed = { version = "0.21.0", features = ["read-txn-no-tls"] }
hex = "0.4.3"
html5ever = "0.27.0"
@@ -459,32 +453,36 @@ indoc = "2"
inventory = "0.3.19"
itertools = "0.14.0"
jsonwebtoken = "9.3"
jupyter-protocol = { git = "https://github.com/ConradIrwin/runtimed", rev = "7130c804216b6914355d15d0b91ea91f6babd734" }
jupyter-websocket-client = { git = "https://github.com/ConradIrwin/runtimed" ,rev = "7130c804216b6914355d15d0b91ea91f6babd734" }
jupyter-protocol = { version = "0.6.0" }
jupyter-websocket-client = { version = "0.9.0" }
libc = "0.2"
libsqlite3-sys = { version = "0.30.1", features = ["bundled"] }
linkify = "0.10.0"
linkme = "0.3.31"
livekit = { git = "https://github.com/zed-industries/livekit-rust-sdks", rev = "811ceae29fabee455f110c56cd66b3f49a7e5003", features = [
"dispatcher",
"services-dispatcher",
"rustls-tls-native-roots",
], default-features = false }
log = { version = "0.4.16", features = ["kv_unstable_serde", "serde"] }
markup5ever_rcdom = "0.3.0"
mlua = { version = "0.10", features = ["lua54", "vendored", "async", "send"] }
nanoid = "0.4"
nbformat = { git = "https://github.com/ConradIrwin/runtimed", rev = "7130c804216b6914355d15d0b91ea91f6babd734" }
nbformat = { version = "0.10.0" }
nix = "0.29"
objc = "0.2"
open = "5.0.0"
num-format = "0.4.4"
ordered-float = "2.1.1"
palette = { version = "0.7.5", default-features = false, features = ["std"] }
parking_lot = "0.12.1"
pathdiff = "0.2"
pet = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "845945b830297a50de0e24020b980a65e4820559" }
pet-fs = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "845945b830297a50de0e24020b980a65e4820559" }
pet-pixi = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "845945b830297a50de0e24020b980a65e4820559" }
pet-conda = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "845945b830297a50de0e24020b980a65e4820559" }
pet-core = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "845945b830297a50de0e24020b980a65e4820559" }
pet-poetry = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "845945b830297a50de0e24020b980a65e4820559" }
pet-reporter = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "845945b830297a50de0e24020b980a65e4820559" }
pet = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "1abe5cec5ebfbe97ca71746a4cfc7fe89bddf8e0" }
pet-fs = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "1abe5cec5ebfbe97ca71746a4cfc7fe89bddf8e0" }
pet-pixi = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "1abe5cec5ebfbe97ca71746a4cfc7fe89bddf8e0" }
pet-conda = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "1abe5cec5ebfbe97ca71746a4cfc7fe89bddf8e0" }
pet-core = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "1abe5cec5ebfbe97ca71746a4cfc7fe89bddf8e0" }
pet-poetry = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "1abe5cec5ebfbe97ca71746a4cfc7fe89bddf8e0" }
pet-reporter = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "1abe5cec5ebfbe97ca71746a4cfc7fe89bddf8e0" }
postage = { version = "0.5", features = ["futures-traits"] }
pretty_assertions = { version = "1.3.0", features = ["unstable"] }
proc-macro2 = "1.0.93"
@@ -507,15 +505,14 @@ reqwest = { git = "https://github.com/zed-industries/reqwest.git", rev = "fd110f
"stream",
] }
rsa = "0.9.6"
runtimelib = { git = "https://github.com/ConradIrwin/runtimed", rev = "7130c804216b6914355d15d0b91ea91f6babd734", default-features = false, features = [
runtimelib = { version = "0.25.0", default-features = false, features = [
"async-dispatcher-runtime",
] }
rustc-demangle = "0.1.23"
rust-embed = { version = "8.4", features = ["include-exclude"] }
rustc-hash = "2.1.0"
rustls = { version = "0.23.26" }
rustls = { version = "0.23.22" }
rustls-platform-verifier = "0.5.0"
scap = { git = "https://github.com/zed-industries/scap", rev = "08f0a01417505cc0990b9931a37e5120db92e0d0", default-features = false }
schemars = { version = "0.8", features = ["impl_json_schema", "indexmap2"] }
semver = "1.0"
serde = { version = "1.0", features = ["derive", "rc"] }
@@ -555,7 +552,6 @@ time = { version = "0.3", features = [
tiny_http = "0.8"
toml = "0.8"
tokio = { version = "1" }
tokio-tungstenite = { version = "0.26", features = ["__rustls-tls"] }
tower-http = "0.4.4"
tree-sitter = { version = "0.25.3", features = ["wasm"] }
tree-sitter-bash = "0.23"
@@ -577,7 +573,7 @@ tree-sitter-md = { git = "https://github.com/tree-sitter-grammars/tree-sitter-ma
tree-sitter-python = "0.23"
tree-sitter-regex = "0.24"
tree-sitter-ruby = "0.23"
tree-sitter-rust = "0.24"
tree-sitter-rust = "0.23"
tree-sitter-typescript = "0.23"
tree-sitter-yaml = { git = "https://github.com/zed-industries/tree-sitter-yaml", rev = "baff0b51c64ef6a1fb1f8390f3ad6015b83ec13a" }
unicase = "2.6"
@@ -587,7 +583,6 @@ unicode-script = "0.5.7"
url = "2.2"
urlencoding = "2.1.2"
uuid = { version = "1.1.2", features = ["v4", "v5", "v7", "serde"] }
walkdir = "2.3"
wasmparser = "0.221"
wasm-encoder = "0.221"
wasmtime = { version = "29", default-features = false, features = [
@@ -600,10 +595,9 @@ wasmtime = { version = "29", default-features = false, features = [
wasmtime-wasi = "29"
which = "6.0.0"
wit-component = "0.221"
workspace-hack = "0.1.0"
zed_llm_client = "0.4"
zstd = "0.11"
metal = "0.29"
metal = "0.31"
[workspace.dependencies.async-stripe]
git = "https://github.com/zed-industries/async-stripe"
@@ -622,10 +616,12 @@ features = [
[workspace.dependencies.windows]
version = "0.61"
features = [
"Foundation_Collections",
"Foundation_Numerics",
"Storage_Search",
"Storage_Streams",
"System_Threading",
"UI_StartScreen",
"UI_ViewManagement",
"Wdk_System_SystemServices",
"Win32_Globalization",
@@ -652,7 +648,6 @@ features = [
"Win32_System_SystemInformation",
"Win32_System_SystemServices",
"Win32_System_Threading",
"Win32_System_Variant",
"Win32_System_WinRT",
"Win32_UI_Controls",
"Win32_UI_HiDpi",
@@ -660,21 +655,17 @@ features = [
"Win32_UI_Input_KeyboardAndMouse",
"Win32_UI_Shell",
"Win32_UI_Shell_Common",
"Win32_UI_Shell_PropertiesSystem",
"Win32_UI_WindowsAndMessaging",
]
# TODO livekit https://github.com/RustAudio/cpal/pull/891
[patch.crates-io]
cpal = { git = "https://github.com/zed-industries/cpal", rev = "fd8bc2fd39f1f5fdee5a0690656caff9a26d9d50" }
notify = { git = "https://github.com/zed-industries/notify.git", rev = "bbb9ea5ae52b253e095737847e367c30653a2e96" }
notify-types = { git = "https://github.com/zed-industries/notify.git", rev = "bbb9ea5ae52b253e095737847e367c30653a2e96" }
# Makes the workspace hack crate refer to the local one, but only when you're building locally
workspace-hack = { path = "tooling/workspace-hack" }
real-async-tls = { git = "https://github.com/zed-industries/async-tls", rev = "1e759a4b5e370f87dc15e40756ac4f8815b61d9d", package = "async-tls" }
[profile.dev]
split-debuginfo = "unpacked"
debug = "limited"
codegen-units = 16
[profile.dev.package]
@@ -784,12 +775,4 @@ let_underscore_future = "allow"
too_many_arguments = "allow"
[workspace.metadata.cargo-machete]
ignored = [
"bindgen",
"cbindgen",
"prost_build",
"serde",
"component",
"linkme",
"workspace-hack",
]
ignored = ["bindgen", "cbindgen", "prost_build", "serde", "component", "linkme"]

View File

@@ -1,6 +1,6 @@
# syntax = docker/dockerfile:1.2
FROM rust:1.86-bookworm as builder
FROM rust:1.81-bookworm as builder
WORKDIR app
COPY . .

View File

@@ -0,0 +1,12 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="16" height="16" rx="2" fill="black" fill-opacity="0.2"/>
<g clip-path="url(#clip0_1916_18)">
<path d="M10.652 3.79999H8.816L12.164 12.2H14L10.652 3.79999Z" fill="#1F1F1E"/>
<path d="M5.348 3.79999L2 12.2H3.872L4.55672 10.436H8.05927L8.744 12.2H10.616L7.268 3.79999H5.348ZM5.16224 8.87599L6.308 5.92399L7.45374 8.87599H5.16224Z" fill="#1F1F1E"/>
</g>
<defs>
<clipPath id="clip0_1916_18">
<rect width="12" height="8.4" fill="white" transform="translate(2 3.79999)"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 601 B

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-arrow-down-right-icon lucide-arrow-down-right"><path d="m7 7 10 10"/><path d="M17 7v10H7"/></svg>

Before

Width:  |  Height:  |  Size: 300 B

View File

@@ -1 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-arrow-up-right-icon lucide-arrow-up-right"><path d="M7 7h10v10"/><path d="M7 17 17 7"/></svg>
<svg width="8" height="8" viewBox="0 0 8 8" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6 2H6.5C6.5 1.86739 6.44732 1.74021 6.35355 1.64645C6.25979 1.55268 6.13261 1.5 6 1.5V2ZM2 1.5C1.72386 1.5 1.5 1.72386 1.5 2C1.5 2.27614 1.72386 2.5 2 2.5L2 1.5ZM5.5 6C5.5 6.27614 5.72386 6.5 6 6.5C6.27614 6.5 6.5 6.27614 6.5 6H5.5ZM1.64645 5.64645C1.45118 5.84171 1.45118 6.15829 1.64645 6.35355C1.84171 6.54882 2.15829 6.54882 2.35355 6.35355L1.64645 5.64645ZM6 1.5H2L2 2.5H6V1.5ZM5.5 2V6H6.5V2H5.5ZM5.64645 1.64645L1.64645 5.64645L2.35355 6.35355L6.35355 2.35355L5.64645 1.64645Z" fill="white"/>
</svg>

Before

Width:  |  Height:  |  Size: 296 B

After

Width:  |  Height:  |  Size: 608 B

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-binary-icon lucide-binary"><rect x="14" y="14" width="4" height="6" rx="2"/><rect x="6" y="4" width="4" height="6" rx="2"/><path d="M6 20h4"/><path d="M14 10h4"/><path d="M6 14h2v6"/><path d="M14 4h2v6"/></svg>

Before

Width:  |  Height:  |  Size: 413 B

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-bug-off-icon lucide-bug-off"><path d="M15 7.13V6a3 3 0 0 0-5.14-2.1L8 2"/><path d="M14.12 3.88 16 2"/><path d="M22 13h-4v-2a4 4 0 0 0-4-4h-1.3"/><path d="M20.97 5c0 2.1-1.6 3.8-3.5 4"/><path d="m2 2 20 20"/><path d="M7.7 7.7A4 4 0 0 0 6 11v3a6 6 0 0 0 11.13 3.13"/><path d="M12 20v-8"/><path d="M6 13H2"/><path d="M3 21c0-2.1 1.7-3.9 3.8-4"/></svg>

Before

Width:  |  Height:  |  Size: 551 B

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-check-check-icon lucide-check-check"><path d="M18 6 7 17l-5-5"/><path d="m22 10-7.5 7.5L13 16"/></svg>

Before

Width:  |  Height:  |  Size: 305 B

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-circle-off-icon lucide-circle-off"><path d="m2 2 20 20"/><path d="M8.35 2.69A10 10 0 0 1 21.3 15.65"/><path d="M19.08 19.08A10 10 0 1 1 4.92 4.92"/></svg>

Before

Width:  |  Height:  |  Size: 357 B

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" stroke="currentColor" fill="none" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-circle"><circle cx="12" cy="12" r="10"/></svg>

Before

Width:  |  Height:  |  Size: 249 B

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-message-circle"><path d="M7.9 20A9 9 0 1 0 4 16.1L2 22Z"/></svg>

Before

Width:  |  Height:  |  Size: 267 B

View File

@@ -1 +0,0 @@
<svg width="16" height="16" fill="none" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><g style="fill:#000;fill-opacity:1" fill="#180c25"><path d="m-116.1-101.4-28.9-28.9a6.7 6.7 0 0 1-1.8-4.7v-41.2c0-2.4-2.4-4.8-4.8-4.8h-9.6a5.2 5.2 0 0 0-4.8 4.8v48c0 2.5 1 5 2.7 6.8l33.6 33.6a9.6 9.6 0 0 0 6.8 2.8h4.8c2.7 0 4.8-2.2 4.8-4.8v-4.8c0-2.5-1-5-2.8-6.8zM-79.6-176.2c0-2.4-2.4-4.8-4.8-4.8h-9.7a5.2 5.2 0 0 0-4.7 4.8v41.2c0 1.8-.8 3.5-2 4.7l-9.6 9.7a9.5 9.5 0 0 0-2.8 6.8v4.8c0 2.6 2.1 4.7 4.8 4.7h4.8c2.4 0 4.9-.9 6.7-2.8l14.4-14.3a9.6 9.6 0 0 0 2.8-6.8v-48z" style="fill:#000;fill-opacity:1;stroke-width:.255894" transform="translate(21.6 22.7) scale(.11067)"/></g></svg>

Before

Width:  |  Height:  |  Size: 677 B

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-flame-icon lucide-flame"><path d="M8.5 14.5A2.5 2.5 0 0 0 11 12c0-1.38-.5-2-1-3-1.072-2.143-.224-4.054 2-6 .5 2.5 2 4.9 4 6.5 2 1.6 3 3.5 3 5.5a7 7 0 1 1-14 0c0-1.153.433-2.294 1-3a2.5 2.5 0 0 0 2.5 2.5z"/></svg>

Before

Width:  |  Height:  |  Size: 415 B

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-forward-icon lucide-forward"><polyline points="15 17 20 12 15 7"/><path d="M4 18v-2a4 4 0 0 1 4-4h12"/></svg>

Before

Width:  |  Height:  |  Size: 312 B

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-square-function-icon lucide-square-function"><rect width="18" height="18" x="3" y="3" rx="2" ry="2"/><path d="M9 17c2 0 2.8-1 2.8-2.8V10c0-2 1-3.3 3.2-3"/><path d="M9 11.2h5.7"/></svg>

Before

Width:  |  Height:  |  Size: 387 B

View File

@@ -1,5 +0,0 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M20 14H4C3.44772 14 3 14.4477 3 15V20C3 20.5523 3.44772 21 4 21H20C20.5523 21 21 20.5523 21 20V15C21 14.4477 20.5523 14 20 14Z" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M11 3H4C3.44772 3 3 3.44772 3 4V9C3 9.55228 3.44772 10 4 10H11C11.5523 10 12 9.55228 12 9V4C12 3.44772 11.5523 3 11 3Z" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M20 3H17C16.4477 3 16 3.44772 16 4V9C16 9.55228 16.4477 10 17 10H20C20.5523 10 21 9.55228 21 9V4C21 3.44772 20.5523 3 20 3Z" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 746 B

View File

@@ -1,3 +0,0 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M10.1331 11.3776C10.2754 10.6665 10.1331 9.78593 11.1998 8.53327C11.82 7.80489 12.2664 6.96894 12.2664 6.04456C12.2664 4.91305 11.8169 3.82788 11.0168 3.02778C10.2167 2.22769 9.13152 1.7782 8.00001 1.7782C6.8685 1.7782 5.78334 2.22769 4.98324 3.02778C4.18314 3.82788 3.73364 4.91305 3.73364 6.04456C3.73364 6.75562 3.87586 7.6089 4.80024 8.53327C5.86683 9.80679 5.72462 10.6665 5.86683 11.3776M10.1331 11.3776V12.8821C10.1331 13.622 9.53341 14.2218 8.79353 14.2218H7.2065C6.46662 14.2218 5.86683 13.622 5.86683 12.8821V11.3776M10.1331 11.3776H5.86683" stroke="black" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 751 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-message-circle-more"><path d="M7.9 20A9 9 0 1 0 4 16.1L2 22Z"/><path d="M8 12h.01"/><path d="M12 12h.01"/><path d="M16 12h.01"/></svg>

After

Width:  |  Height:  |  Size: 337 B

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-power-icon lucide-power"><path d="M12 2v10"/><path d="M18.4 6.6a9 9 0 1 1-12.77.04"/></svg>

Before

Width:  |  Height:  |  Size: 294 B

View File

@@ -1,4 +0,0 @@
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3.09666 3.02263C3.0567 3.00312 3.01178 2.9961 2.96778 3.0025C2.92377 3.00889 2.88271 3.02839 2.84995 3.05847C2.8172 3.08854 2.79426 3.12778 2.78413 3.17108C2.77401 3.21439 2.77716 3.25973 2.79319 3.30121L4.05638 6.69C4.13088 6.89005 4.13088 7.11022 4.05638 7.31027L2.79363 10.6991C2.77769 10.7405 2.77457 10.7858 2.78469 10.829C2.79481 10.8722 2.8177 10.9114 2.85038 10.9414C2.88306 10.9715 2.92402 10.991 2.96794 10.9975C3.01186 11.0039 3.05671 10.997 3.09666 10.9776L11.0943 7.20097C11.1324 7.18297 11.1645 7.15455 11.187 7.11899C11.2096 7.08344 11.2215 7.04222 11.2215 7.00014C11.2215 6.95805 11.2096 6.91683 11.187 6.88128C11.1645 6.84573 11.1324 6.8173 11.0943 6.79931L3.09666 3.02263Z" stroke="black" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M4.11255 7.00014H11.2216" stroke="black" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 1014 B

View File

@@ -1,3 +0,0 @@
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M4 9.8V4.2C4 4.08954 4.08954 4 4.2 4H9.8C9.91046 4 10 4.08954 10 4.2V9.8C10 9.91046 9.91046 10 9.8 10H4.2C4.08954 10 4 9.91046 4 9.8Z" fill="#C56757" stroke="#C56757" stroke-width="1.25" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 325 B

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-user-round-pen-icon lucide-user-round-pen"><path d="M2 21a8 8 0 0 1 10.821-7.487"/><path d="M21.378 16.626a1 1 0 0 0-3.004-3.004l-4.01 4.012a2 2 0 0 0-.506.854l-.837 2.87a.5.5 0 0 0 .62.62l2.87-.837a2 2 0 0 0 .854-.506z"/><circle cx="10" cy="8" r="5"/></svg>

Before

Width:  |  Height:  |  Size: 461 B

View File

@@ -126,6 +126,7 @@
// "alt-v": ["editor::MovePageUp", { "center_cursor": true }],
"ctrl-alt-space": "editor::ShowCharacterPalette",
"ctrl-;": "editor::ToggleLineNumbers",
"ctrl-k ctrl-r": "git::Restore",
"ctrl-'": "editor::ToggleSelectedDiffHunks",
"ctrl-\"": "editor::ExpandAllDiffHunks",
"ctrl-i": "editor::ShowSignatureHelp",
@@ -137,24 +138,6 @@
"shift-f9": "editor::EditLogBreakpoint"
}
},
{
"context": "Editor && !agent_diff",
"bindings": {
"ctrl-k ctrl-r": "git::Restore",
"ctrl-alt-y": "git::ToggleStaged",
"alt-y": "git::StageAndNext",
"alt-shift-y": "git::UnstageAndNext"
}
},
{
"context": "AgentDiff",
"bindings": {
"ctrl-y": "agent::Keep",
"ctrl-n": "agent::Reject",
"ctrl-shift-y": "agent::KeepAll",
"ctrl-shift-n": "agent::RejectAll"
}
},
{
"context": "Editor && mode == full",
"bindings": {
@@ -313,7 +296,6 @@
"ctrl-k t": ["pane::CloseItemsToTheRight", { "close_pinned": false }],
"ctrl-k u": ["pane::CloseCleanItems", { "close_pinned": false }],
"ctrl-k w": ["pane::CloseAllItems", { "close_pinned": false }],
"ctrl-k ctrl-w": "workspace::CloseAllItemsAndPanes",
"back": "pane::GoBack",
"ctrl-alt--": "pane::GoBack",
"ctrl-alt-_": "pane::GoForward",
@@ -354,11 +336,11 @@
"alt-shift-left": "editor::SelectSmallerSyntaxNode", // Shrink Selection
"ctrl-shift-l": "editor::SelectAllMatches", // Select all occurrences of current selection
"ctrl-f2": "editor::SelectAllMatches", // Select all occurrences of current word
"ctrl-d": ["editor::SelectNext", { "replace_newest": false }], // editor.action.addSelectionToNextFindMatch / find_under_expand
"ctrl-shift-down": ["editor::SelectNext", { "replace_newest": false }], // editor.action.addSelectionToNextFindMatch
"ctrl-shift-up": ["editor::SelectPrevious", { "replace_newest": false }], // editor.action.addSelectionToPreviousFindMatch
"ctrl-k ctrl-d": ["editor::SelectNext", { "replace_newest": true }], // editor.action.moveSelectionToNextFindMatch / find_under_expand_skip
"ctrl-k ctrl-shift-d": ["editor::SelectPrevious", { "replace_newest": true }], // editor.action.moveSelectionToPreviousFindMatch
"ctrl-d": ["editor::SelectNext", { "replace_newest": false }],
"ctrl-shift-down": ["editor::SelectNext", { "replace_newest": false }], // Add selection to Next Find Match
"ctrl-shift-up": ["editor::SelectPrevious", { "replace_newest": false }],
"ctrl-k ctrl-d": ["editor::SelectNext", { "replace_newest": true }],
"ctrl-k ctrl-shift-d": ["editor::SelectPrevious", { "replace_newest": true }],
"ctrl-k ctrl-i": "editor::Hover",
"ctrl-/": ["editor::ToggleComments", { "advance_downwards": false }],
"ctrl-u": "editor::UndoSelection",
@@ -400,6 +382,9 @@
"ctrl-k v": "markdown::OpenPreviewToTheSide",
"ctrl-shift-v": "markdown::OpenPreview",
"ctrl-alt-shift-c": "editor::DisplayCursorNames",
"ctrl-alt-y": "git::ToggleStaged",
"alt-y": "git::StageAndNext",
"alt-shift-y": "git::UnstageAndNext",
"alt-.": "editor::GoToHunk",
"alt-,": "editor::GoToPreviousHunk"
}
@@ -484,8 +469,6 @@
"alt-shift-r": ["task::Spawn", { "reveal_target": "center" }]
// also possible to spawn tasks by name:
// "foo-bar": ["task::Spawn", { "task_name": "MyTask", "reveal_target": "dock" }]
// or by tag:
// "foo-bar": ["task::Spawn", { "task_tag": "MyTag" }],
}
},
{
@@ -534,7 +517,6 @@
"context": "Editor && showing_completions",
"bindings": {
"enter": "editor::ConfirmCompletion",
"shift-enter": "editor::ConfirmCompletionReplace",
"tab": "editor::ComposeCompletion"
}
},
@@ -600,6 +582,13 @@
"ctrl-:": "editor::ToggleInlayHints"
}
},
{
"context": "ProposedChangesEditor",
"bindings": {
"ctrl-shift-y": "editor::ApplyDiffHunk",
"ctrl-alt-a": "editor::ApplyAllDiffHunks"
}
},
{
"context": "Editor && jupyter && !ContextEditor",
"bindings": {
@@ -624,52 +613,34 @@
}
},
{
"context": "AgentPanel",
"context": "AssistantPanel2",
"bindings": {
"ctrl-n": "agent::NewThread",
"ctrl-alt-n": "agent::NewTextThread",
"ctrl-shift-h": "agent::OpenHistory",
"ctrl-alt-c": "agent::OpenConfiguration",
"ctrl-i": "agent::ToggleProfileSelector",
"ctrl-n": "assistant2::NewThread",
"new": "assistant2::NewThread",
"ctrl-shift-h": "assistant2::OpenHistory",
"ctrl-alt-/": "assistant::ToggleModelSelector",
"ctrl-shift-a": "agent::ToggleContextPicker",
"shift-escape": "agent::ExpandMessageEditor",
"ctrl-e": "agent::ChatMode",
"ctrl-alt-e": "agent::RemoveAllContext"
"ctrl-shift-a": "assistant2::ToggleContextPicker",
"ctrl-e": "assistant2::ChatMode",
"ctrl-alt-e": "assistant2::RemoveAllContext"
}
},
{
"context": "AgentPanel > Markdown",
"context": "AssistantPanel2 && prompt_editor",
"use_key_equivalents": true,
"bindings": {
"copy": "markdown::CopyAsMarkdown",
"ctrl-c": "markdown::CopyAsMarkdown"
}
},
{
"context": "AgentPanel && prompt_editor",
"bindings": {
"cmd-n": "agent::NewTextThread",
"cmd-alt-t": "agent::NewThread"
"cmd-n": "assistant2::NewPromptEditor",
"cmd-alt-t": "assistant2::NewThread"
}
},
{
"context": "MessageEditor > Editor",
"bindings": {
"enter": "agent::Chat",
"ctrl-i": "agent::ToggleProfileSelector",
"shift-ctrl-r": "agent::OpenAgentDiff"
"enter": "assistant2::Chat"
}
},
{
"context": "EditMessageEditor > Editor",
"bindings": {
"escape": "menu::Cancel",
"enter": "menu::Confirm",
"alt-enter": "editor::Newline"
}
},
{
"context": "AgentFeedbackMessageEditor > Editor",
"use_key_equivalents": true,
"bindings": {
"escape": "menu::Cancel",
"enter": "menu::Confirm",
@@ -679,18 +650,18 @@
{
"context": "ContextStrip",
"bindings": {
"up": "agent::FocusUp",
"right": "agent::FocusRight",
"left": "agent::FocusLeft",
"down": "agent::FocusDown",
"backspace": "agent::RemoveFocusedContext",
"enter": "agent::AcceptSuggestedContext"
"up": "assistant2::FocusUp",
"right": "assistant2::FocusRight",
"left": "assistant2::FocusLeft",
"down": "assistant2::FocusDown",
"backspace": "assistant2::RemoveFocusedContext",
"enter": "assistant2::AcceptSuggestedContext"
}
},
{
"context": "ThreadHistory",
"bindings": {
"backspace": "agent::RemoveSelectedThread"
"backspace": "assistant2::RemoveSelectedThread"
}
},
{
@@ -698,7 +669,7 @@
"bindings": {
"ctrl-[": "assistant::CyclePreviousInlineAssist",
"ctrl-]": "assistant::CycleNextInlineAssist",
"ctrl-alt-e": "agent::RemoveAllContext"
"ctrl-alt-e": "assistant2::RemoveAllContext"
}
},
{
@@ -782,7 +753,6 @@
"shift-tab": "git_panel::FocusEditor",
"escape": "git_panel::ToggleFocus",
"ctrl-enter": "git::Commit",
"ctrl-shift-enter": "git::Amend",
"alt-enter": "menu::SecondaryConfirm",
"delete": ["git::RestoreFile", { "skip_prompt": false }],
"backspace": ["git::RestoreFile", { "skip_prompt": false }],
@@ -791,25 +761,18 @@
"ctrl-delete": ["git::RestoreFile", { "skip_prompt": false }]
}
},
{
"context": "GitPanel && CommitEditor",
"use_key_equivalents": true,
"bindings": {
"escape": "git::Cancel"
}
},
{
"context": "GitCommit > Editor",
"bindings": {
"escape": "menu::Cancel",
"enter": "editor::Newline",
"ctrl-enter": "git::Commit",
"ctrl-shift-enter": "git::Amend",
"alt-l": "git::GenerateCommitMessage"
}
},
{
"context": "GitPanel",
"use_key_equivalents": true,
"bindings": {
"ctrl-g ctrl-g": "git::Fetch",
"ctrl-g up": "git::Push",
@@ -826,7 +789,6 @@
"context": "GitDiff > Editor",
"bindings": {
"ctrl-enter": "git::Commit",
"ctrl-shift-enter": "git::Amend",
"ctrl-space": "git::StageAll",
"ctrl-shift-space": "git::UnstageAll"
}
@@ -845,7 +807,6 @@
"shift-tab": "git_panel::FocusChanges",
"enter": "editor::Newline",
"ctrl-enter": "git::Commit",
"ctrl-shift-enter": "git::Amend",
"alt-up": "git_panel::FocusChanges",
"alt-l": "git::GenerateCommitMessage"
}

View File

@@ -147,6 +147,10 @@
"ctrl-shift-v": ["editor::MovePageUp", { "center_cursor": true }],
"ctrl-cmd-space": "editor::ShowCharacterPalette",
"cmd-;": "editor::ToggleLineNumbers",
"cmd-alt-z": "git::Restore",
"cmd-alt-y": "git::ToggleStaged",
"cmd-y": "git::StageAndNext",
"cmd-shift-y": "git::UnstageAndNext",
"cmd-'": "editor::ToggleSelectedDiffHunks",
"cmd-\"": "editor::ExpandAllDiffHunks",
"cmd-alt-g b": "editor::ToggleGitBlame",
@@ -227,26 +231,6 @@
"ctrl-alt-enter": "repl::RunInPlace"
}
},
{
"context": "Editor && !agent_diff",
"use_key_equivalents": true,
"bindings": {
"cmd-alt-z": "git::Restore",
"cmd-alt-y": "git::ToggleStaged",
"cmd-y": "git::StageAndNext",
"cmd-shift-y": "git::UnstageAndNext"
}
},
{
"context": "AgentDiff",
"use_key_equivalents": true,
"bindings": {
"cmd-y": "agent::Keep",
"cmd-n": "agent::Reject",
"cmd-shift-y": "agent::KeepAll",
"cmd-shift-n": "agent::RejectAll"
}
},
{
"context": "AssistantPanel",
"use_key_equivalents": true,
@@ -279,43 +263,33 @@
}
},
{
"context": "AgentPanel",
"context": "AssistantPanel2",
"use_key_equivalents": true,
"bindings": {
"cmd-n": "agent::NewThread",
"cmd-alt-n": "agent::NewTextThread",
"cmd-shift-h": "agent::OpenHistory",
"cmd-alt-c": "agent::OpenConfiguration",
"cmd-i": "agent::ToggleProfileSelector",
"cmd-n": "assistant2::NewThread",
"cmd-alt-p": "assistant2::NewPromptEditor",
"cmd-shift-h": "assistant2::OpenHistory",
"cmd-alt-/": "assistant::ToggleModelSelector",
"cmd-shift-a": "agent::ToggleContextPicker",
"shift-escape": "agent::ExpandMessageEditor",
"cmd-e": "agent::ChatMode",
"cmd-alt-e": "agent::RemoveAllContext"
"cmd-shift-a": "assistant2::ToggleContextPicker",
"cmd-e": "assistant2::ChatMode",
"cmd-alt-e": "assistant2::RemoveAllContext"
}
},
{
"context": "AgentPanel > Markdown",
"context": "AssistantPanel2 && prompt_editor",
"use_key_equivalents": true,
"bindings": {
"cmd-c": "markdown::CopyAsMarkdown"
}
},
{
"context": "AgentPanel && prompt_editor",
"use_key_equivalents": true,
"bindings": {
"cmd-n": "agent::NewTextThread",
"cmd-alt-t": "agent::NewThread"
"cmd-n": "assistant2::NewPromptEditor",
"cmd-alt-t": "assistant2::NewThread"
}
},
{
"context": "MessageEditor > Editor",
"use_key_equivalents": true,
"bindings": {
"enter": "agent::Chat",
"cmd-i": "agent::ToggleProfileSelector",
"shift-ctrl-r": "agent::OpenAgentDiff"
"enter": "assistant2::Chat",
"cmd-g d": "git::Diff",
"shift-escape": "git::ExpandCommitEditor"
}
},
{
@@ -327,49 +301,22 @@
"alt-enter": "editor::Newline"
}
},
{
"context": "AgentFeedbackMessageEditor > Editor",
"use_key_equivalents": true,
"bindings": {
"escape": "menu::Cancel",
"enter": "menu::Confirm",
"alt-enter": "editor::Newline"
}
},
{
"context": "ContextStrip",
"use_key_equivalents": true,
"bindings": {
"up": "agent::FocusUp",
"right": "agent::FocusRight",
"left": "agent::FocusLeft",
"down": "agent::FocusDown",
"backspace": "agent::RemoveFocusedContext",
"enter": "agent::AcceptSuggestedContext"
}
},
{
"context": "AgentConfiguration",
"bindings": {
"ctrl--": "pane::GoBack"
"up": "assistant2::FocusUp",
"right": "assistant2::FocusRight",
"left": "assistant2::FocusLeft",
"down": "assistant2::FocusDown",
"backspace": "assistant2::RemoveFocusedContext",
"enter": "assistant2::AcceptSuggestedContext"
}
},
{
"context": "ThreadHistory",
"bindings": {
"ctrl--": "pane::GoBack"
}
},
{
"context": "ThreadHistory",
"bindings": {
"ctrl--": "pane::GoBack"
}
},
{
"context": "ThreadHistory > Editor",
"bindings": {
"shift-backspace": "agent::RemoveSelectedThread"
"backspace": "assistant2::RemoveSelectedThread"
}
},
{
@@ -458,8 +405,7 @@
"cmd-k e": ["pane::CloseItemsToTheLeft", { "close_pinned": false }],
"cmd-k t": ["pane::CloseItemsToTheRight", { "close_pinned": false }],
"cmd-k u": ["pane::CloseCleanItems", { "close_pinned": false }],
"cmd-k w": ["pane::CloseAllItems", { "close_pinned": false }],
"cmd-k cmd-w": "workspace::CloseAllItemsAndPanes",
"cmd-k cmd-w": ["pane::CloseAllItems", { "close_pinned": false }],
"cmd-f": "project_search::ToggleFocus",
"cmd-g": "search::SelectNextMatch",
"cmd-shift-g": "search::SelectPreviousMatch",
@@ -491,15 +437,12 @@
"alt-shift-down": "editor::DuplicateLineDown",
"ctrl-shift-right": "editor::SelectLargerSyntaxNode", // Expand Selection
"ctrl-shift-left": "editor::SelectSmallerSyntaxNode", // Shrink Selection
"cmd-d": ["editor::SelectNext", { "replace_newest": false }], // editor.action.addSelectionToNextFindMatch / find_under_expand
"cmd-d": ["editor::SelectNext", { "replace_newest": false }], // Add selection to Next Find Match
"cmd-shift-l": "editor::SelectAllMatches", // Select all occurrences of current selection
"cmd-f2": "editor::SelectAllMatches", // Select all occurrences of current word
"cmd-k cmd-d": ["editor::SelectNext", { "replace_newest": true }], // editor.action.moveSelectionToNextFindMatch / find_under_expand_skip
// macOS binds `ctrl-cmd-d` to Show Dictionary which breaks these two binds
// To use `ctrl-cmd-d` or `ctrl-k ctrl-cmd-d` in Zed you must execute this command and then restart:
// defaults write com.apple.symbolichotkeys AppleSymbolicHotKeys -dict-add 70 '<dict><key>enabled</key><false/></dict>'
"ctrl-cmd-d": ["editor::SelectPrevious", { "replace_newest": false }], // editor.action.addSelectionToPreviousFindMatch
"cmd-k ctrl-cmd-d": ["editor::SelectPrevious", { "replace_newest": true }], // editor.action.moveSelectionToPreviousFindMatch
"ctrl-cmd-d": ["editor::SelectPrevious", { "replace_newest": false }],
"cmd-k cmd-d": ["editor::SelectNext", { "replace_newest": true }],
"cmd-k ctrl-cmd-d": ["editor::SelectPrevious", { "replace_newest": true }],
"cmd-k cmd-i": "editor::Hover",
"cmd-/": ["editor::ToggleComments", { "advance_downwards": false }],
"cmd-u": "editor::UndoSelection",
@@ -531,7 +474,7 @@
"cmd-k cmd-9": ["editor::FoldAtLevel", 9],
"cmd-k cmd-0": "editor::FoldAll",
"cmd-k cmd-j": "editor::UnfoldAll",
// Using `ctrl-space` / `ctrl-shift-space` in Zed requires disabling the macOS global shortcut.
// Using `ctrl-space` in Zed requires disabling the macOS global shortcut.
// System Preferences->Keyboard->Keyboard Shortcuts->Input Sources->Select the previous input source (uncheck)
"ctrl-space": "editor::ShowCompletions",
"ctrl-shift-space": "editor::ShowWordCompletions",
@@ -639,8 +582,6 @@
"ctrl-alt-shift-r": ["task::Spawn", { "reveal_target": "center" }]
// also possible to spawn tasks by name:
// "foo-bar": ["task::Spawn", { "task_name": "MyTask", "reveal_target": "dock" }]
// or by tag:
// "foo-bar": ["task::Spawn", { "task_tag": "MyTag" }],
}
},
// Bindings from Sublime Text
@@ -687,7 +628,6 @@
"use_key_equivalents": true,
"bindings": {
"enter": "editor::ConfirmCompletion",
"shift-enter": "editor::ConfirmCompletionReplace",
"tab": "editor::ComposeCompletion"
}
},
@@ -748,13 +688,21 @@
"ctrl-:": "editor::ToggleInlayHints"
}
},
{
"context": "ProposedChangesEditor",
"use_key_equivalents": true,
"bindings": {
"cmd-shift-y": "editor::ApplyDiffHunk",
"cmd-shift-a": "editor::ApplyAllDiffHunks"
}
},
{
"context": "PromptEditor",
"use_key_equivalents": true,
"bindings": {
"cmd-shift-a": "agent::ToggleContextPicker",
"cmd-shift-a": "assistant2::ToggleContextPicker",
"cmd-alt-/": "assistant::ToggleModelSelector",
"cmd-alt-e": "agent::RemoveAllContext",
"cmd-alt-e": "assistant2::RemoveAllContext",
"ctrl-[": "assistant::CyclePreviousInlineAssist",
"ctrl-]": "assistant::CycleNextInlineAssist"
}
@@ -855,26 +803,17 @@
"shift-tab": "git_panel::FocusEditor",
"escape": "git_panel::ToggleFocus",
"cmd-enter": "git::Commit",
"cmd-shift-enter": "git::Amend",
"backspace": ["git::RestoreFile", { "skip_prompt": false }],
"delete": ["git::RestoreFile", { "skip_prompt": false }],
"cmd-backspace": ["git::RestoreFile", { "skip_prompt": true }],
"cmd-delete": ["git::RestoreFile", { "skip_prompt": true }]
}
},
{
"context": "GitPanel && CommitEditor",
"use_key_equivalents": true,
"bindings": {
"escape": "git::Cancel"
}
},
{
"context": "GitDiff > Editor",
"use_key_equivalents": true,
"bindings": {
"cmd-enter": "git::Commit",
"cmd-shift-enter": "git::Amend",
"cmd-ctrl-y": "git::StageAll",
"cmd-ctrl-shift-y": "git::UnstageAll"
}
@@ -885,7 +824,6 @@
"bindings": {
"enter": "editor::Newline",
"cmd-enter": "git::Commit",
"cmd-shift-enter": "git::Amend",
"tab": "git_panel::FocusChanges",
"shift-tab": "git_panel::FocusChanges",
"alt-up": "git_panel::FocusChanges",
@@ -915,7 +853,6 @@
"enter": "editor::Newline",
"escape": "menu::Cancel",
"cmd-enter": "git::Commit",
"cmd-shift-enter": "git::Amend",
"alt-tab": "git::GenerateCommitMessage"
}
},
@@ -1021,8 +958,6 @@
"cmd-home": "terminal::ScrollToTop",
"shift-end": "terminal::ScrollToBottom",
"cmd-end": "terminal::ScrollToBottom",
// Using `ctrl-shift-space` in Zed requires disabling the macOS global shortcut.
// System Preferences->Keyboard->Keyboard Shortcuts->Input Sources->Select the previous input source (uncheck)
"ctrl-shift-space": "terminal::ToggleViMode",
"ctrl-k up": "pane::SplitUp",
"ctrl-k down": "pane::SplitDown",

View File

@@ -58,8 +58,7 @@
"ctrl-shift-home": "editor::SelectToBeginning",
"ctrl-shift-end": "editor::SelectToEnd",
"ctrl-f8": "editor::ToggleBreakpoint",
"ctrl-shift-f8": "editor::EditLogBreakpoint",
"ctrl-shift-u": "editor::ToggleCase"
"ctrl-shift-f8": "editor::EditLogBreakpoint"
}
},
{

View File

@@ -37,8 +37,6 @@
"ctrl-shift-a": "editor::SelectLargerSyntaxNode",
"ctrl-shift-d": "editor::DuplicateSelection",
"alt-f3": "editor::SelectAllMatches", // find_all_under
// "ctrl-f3": "", // find_under (cancels any selections)
// "cmd-alt-shift-g": "" // find_under_prev (cancels any selections)
"f9": "editor::SortLinesCaseSensitive",
"ctrl-f9": "editor::SortLinesCaseInsensitive",
"f12": "editor::GoToDefinition",
@@ -51,9 +49,7 @@
"ctrl-k ctrl-l": "editor::ConvertToLowerCase",
"shift-alt-m": "markdown::OpenPreviewToTheSide",
"ctrl-backspace": "editor::DeleteToPreviousWordStart",
"ctrl-delete": "editor::DeleteToNextWordEnd",
"f3": "editor::FindNextMatch",
"shift-f3": "editor::FindPreviousMatch"
"ctrl-delete": "editor::DeleteToNextWordEnd"
}
},
{
@@ -62,12 +58,6 @@
"ctrl-r": "outline::Toggle"
}
},
{
"context": "Editor && !agent_diff",
"bindings": {
"ctrl-k ctrl-z": "git::Restore"
}
},
{
"context": "Pane",
"bindings": {

View File

@@ -55,8 +55,7 @@
"cmd-shift-home": "editor::SelectToBeginning",
"cmd-shift-end": "editor::SelectToEnd",
"ctrl-f8": "editor::ToggleBreakpoint",
"ctrl-shift-f8": "editor::EditLogBreakpoint",
"cmd-shift-u": "editor::ToggleCase"
"ctrl-shift-f8": "editor::EditLogBreakpoint"
}
},
{

View File

@@ -38,8 +38,6 @@
"cmd-shift-a": "editor::SelectLargerSyntaxNode",
"cmd-shift-d": "editor::DuplicateSelection",
"ctrl-cmd-g": "editor::SelectAllMatches", // find_all_under
// "cmd-alt-g": "", // find_under (cancels any selections)
// "cmd-alt-shift-g": "" // find_under_prev (cancels any selections)
"f5": "editor::SortLinesCaseSensitive",
"ctrl-f5": "editor::SortLinesCaseInsensitive",
"shift-f12": "editor::FindAllReferences",
@@ -53,9 +51,7 @@
"cmd-shift-j": "editor::JoinLines",
"shift-alt-m": "markdown::OpenPreviewToTheSide",
"ctrl-backspace": "editor::DeleteToPreviousWordStart",
"ctrl-delete": "editor::DeleteToNextWordEnd",
"cmd-g": "editor::FindNextMatch",
"cmd-shift-g": "editor::FindPreviousMatch"
"ctrl-delete": "editor::DeleteToNextWordEnd"
}
},
{
@@ -64,12 +60,6 @@
"cmd-r": "outline::Toggle"
}
},
{
"context": "Editor && !agent_diff",
"bindings": {
"cmd-k cmd-z": "git::Restore"
}
},
{
"context": "Pane",
"bindings": {

View File

@@ -44,12 +44,6 @@
"[ /": "vim::PreviousComment",
"] *": "vim::NextComment",
"] /": "vim::NextComment",
"[ -": "vim::PreviousLesserIndent",
"[ +": "vim::PreviousGreaterIndent",
"[ =": "vim::PreviousSameIndent",
"] -": "vim::NextLesserIndent",
"] +": "vim::NextGreaterIndent",
"] =": "vim::NextSameIndent",
// Word motions
"w": "vim::NextWordStart",
"e": "vim::NextWordEnd",
@@ -203,7 +197,6 @@
"c": "vim::PushChange",
"shift-c": "vim::ChangeToEndOfLine",
"d": "vim::PushDelete",
"delete": "vim::DeleteRight",
"shift-d": "vim::DeleteToEndOfLine",
"shift-j": "vim::JoinLines",
"g shift-j": "vim::JoinLinesNoWhitespace",
@@ -234,8 +227,6 @@
"g u": "vim::PushLowercase",
"g shift-u": "vim::PushUppercase",
"g ~": "vim::PushOppositeCase",
"g ?": "vim::PushRot13",
// "g ?": "vim::PushRot47",
"\"": "vim::PushRegister",
"g w": "vim::PushRewrap",
"g q": "vim::PushRewrap",
@@ -267,10 +258,9 @@
"u": "vim::ConvertToLowerCase",
"shift-u": "vim::ConvertToUpperCase",
"shift-o": "vim::OtherEnd",
"o": "vim::OtherEndRowAware",
"o": "vim::OtherEnd",
"d": "vim::VisualDelete",
"x": "vim::VisualDelete",
"delete": "vim::VisualDelete",
"shift-d": "vim::VisualDeleteLine",
"shift-x": "vim::VisualDeleteLine",
"y": "vim::VisualYank",
@@ -307,8 +297,6 @@
"g r": ["vim::Paste", { "preserve_clipboard": true }],
"g c": "vim::ToggleComments",
"g q": "vim::Rewrap",
"g ?": "vim::ConvertToRot13",
// "g ?": "vim::ConvertToRot47",
"\"": "vim::PushRegister",
// tree-sitter related commands
"[ x": "editor::SelectLargerSyntaxNode",
@@ -342,106 +330,27 @@
}
},
{
"context": "vim_mode == helix_normal && !menu",
"context": "vim_mode == helix_normal",
"bindings": {
"escape": "editor::Cancel",
"ctrl-[": "editor::Cancel",
":": "command_palette::Toggle",
"shift-d": "vim::DeleteToEndOfLine",
"shift-j": "vim::JoinLines",
"y": "editor::Copy",
"shift-y": "vim::YankLine",
"i": "vim::InsertBefore",
"shift-i": "vim::InsertFirstNonWhitespace",
"a": "vim::InsertAfter",
"shift-a": "vim::InsertEndOfLine",
"o": "vim::InsertLineBelow",
"shift-o": "vim::InsertLineAbove",
"~": "vim::ChangeCase",
"ctrl-a": "vim::Increment",
"ctrl-x": "vim::Decrement",
"p": "vim::Paste",
"shift-p": ["vim::Paste", { "before": true }],
"u": "vim::Undo",
"ctrl-r": "vim::Redo",
"r": "vim::PushReplace",
"s": "vim::Substitute",
"shift-s": "vim::SubstituteLine",
">": "vim::Indent",
"<": "vim::Outdent",
"=": "vim::AutoIndent",
"g u": "vim::PushLowercase",
"g shift-u": "vim::PushUppercase",
"g ~": "vim::PushOppositeCase",
"\"": "vim::PushRegister",
"g q": "vim::PushRewrap",
"g w": "vim::PushRewrap",
"ctrl-pagedown": "pane::ActivateNextItem",
"ctrl-pageup": "pane::ActivatePreviousItem",
"insert": "vim::InsertBefore",
// tree-sitter related commands
"[ x": "editor::SelectLargerSyntaxNode",
"] x": "editor::SelectSmallerSyntaxNode",
"] d": "editor::GoToDiagnostic",
"[ d": "editor::GoToPreviousDiagnostic",
"] c": "editor::GoToHunk",
"[ c": "editor::GoToPreviousHunk",
// Goto mode
"g n": "pane::ActivateNextItem",
"g p": "pane::ActivatePreviousItem",
// "tab": "pane::ActivateNextItem",
// "shift-tab": "pane::ActivatePrevItem",
"shift-h": "pane::ActivatePreviousItem",
"shift-l": "pane::ActivateNextItem",
"g l": "vim::EndOfLine",
"g h": "vim::StartOfLine",
"g s": "vim::FirstNonWhitespace", // "g s" default behavior is "space s"
"g e": "vim::EndOfDocument",
"g y": "editor::GoToTypeDefinition",
"g r": "editor::FindAllReferences", // zed specific
"g t": "vim::WindowTop",
"g c": "vim::WindowMiddle",
"g b": "vim::WindowBottom",
"x": "editor::SelectLine",
"shift-x": "editor::SelectLine",
// Window mode
"space w h": "workspace::ActivatePaneLeft",
"space w l": "workspace::ActivatePaneRight",
"space w k": "workspace::ActivatePaneUp",
"space w j": "workspace::ActivatePaneDown",
"space w q": "pane::CloseActiveItem",
"space w s": "pane::SplitRight",
"space w r": "pane::SplitRight",
"space w v": "pane::SplitDown",
"space w d": "pane::SplitDown",
// Space mode
"space f": "file_finder::Toggle",
"space k": "editor::Hover",
"space s": "outline::Toggle",
"space shift-s": "project_symbols::Toggle",
"space d": "editor::GoToDiagnostic",
"space r": "editor::Rename",
"space a": "editor::ToggleCodeActions",
"space h": "editor::SelectAllMatches",
"space c": "editor::ToggleComments",
"space y": "editor::Copy",
"space p": "editor::Paste",
// Match mode
"m m": "vim::Matching",
"m i w": ["workspace::SendKeystrokes", "v i w"],
"shift-u": "editor::Redo",
"ctrl-c": "editor::ToggleComments",
"d": "vim::HelixDelete",
"c": "vim::Substitute",
"shift-c": "editor::AddSelectionBelow"
"w": "vim::NextWordStart",
"e": "vim::NextWordEnd",
"b": "vim::PreviousWordStart",
"h": "vim::Left",
"j": "vim::Down",
"k": "vim::Up",
"l": "vim::Right"
}
},
{
"context": "vim_mode == insert && !(showing_code_actions || showing_completions)",
"bindings": {
"ctrl-p": "editor::ShowWordCompletions",
"ctrl-n": "editor::ShowWordCompletions"
"ctrl-p": "editor::ShowCompletions",
"ctrl-n": "editor::ShowCompletions"
}
},
{
@@ -539,7 +448,6 @@
"bindings": {
"d": "vim::CurrentLine",
"s": "vim::PushDeleteSurrounds",
"v": "vim::PushForcedMotion", // "d v"
"o": "editor::ToggleSelectedDiffHunks", // "d o"
"shift-o": "git::ToggleStaged",
"p": "git::Restore", // "d p"
@@ -568,13 +476,6 @@
"~": "vim::CurrentLine"
}
},
{
"context": "vim_operator == g?",
"bindings": {
"g ?": "vim::CurrentLine",
"?": "vim::CurrentLine"
}
},
{
"context": "vim_operator == gq",
"bindings": {
@@ -588,7 +489,6 @@
"context": "vim_operator == y",
"bindings": {
"y": "vim::CurrentLine",
"v": "vim::PushForcedMotion",
"s": ["vim::PushAddSurrounds", {}]
}
},

View File

@@ -1,148 +1,17 @@
You are an AI assistant integrated into a code editor. You have the programming ability of an expert programmer who takes pride in writing high-quality code and is driven to the point of obsession about solving problems effectively. Your goal is to do one of the following two things:
You are an AI assistant integrated into a text editor. Your goal is to do one of the following two things:
1. Help users answer questions and perform tasks related to their codebase.
2. Answer general-purpose questions unrelated to their particular codebase.
It will be up to you to decide which of these you are doing based on what the user has told you. When unclear, ask clarifying questions to understand the user's intent before proceeding.
You should only perform actions that modify the user's system if explicitly requested by the user:
- If the user asks a question about how to accomplish a task, provide guidance or information, and use read-only tools (e.g., search) to assist. You may suggest potential actions, but do not directly modify the user's system without explicit instruction.
You should only perform actions that modify the users system if explicitly requested by the user:
- If the user asks a question about how to accomplish a task, provide guidance or information, and use read-only tools (e.g., search) to assist. You may suggest potential actions, but do not directly modify the users system without explicit instruction.
- If the user clearly requests that you perform an action, carry out the action directly without explaining why you are doing so.
When answering questions, it's okay to give incomplete examples containing comments about what would go there in a real version. When being asked to directly perform tasks on the code base, you must ALWAYS make fully working code. You may never "simplify" the code by omitting or deleting functionality you know the user has requested, and you must NEVER write comments like "in a full version, this would..." - instead, you must actually implement the real version. Don't be lazy!
Note that project files are automatically backed up. The user can always get them back later if anything goes wrong, so there's
no need to create backup files (e.g. `.bak` files) because these files will just take up unnecessary space on the user's disk.
When attempting to resolve issues around failing tests, never simply remove the failing tests. Unless the user explicitly asks you to remove tests, ALWAYS attempt to fix the code causing the tests to fail.
Ignore "TODO"-type comments unless they're relevant to the user's explicit request or the user specifically asks you to address them. It is, however, okay to include them in codebase summaries.
<style>
Editing code:
- Make sure to take previous edits into account.
- The edits you perform might lead to errors or warnings. At the end of your changes, check whether you introduced any problems, and fix them before providing a summary of the changes you made.
- You may only attempt to fix these up to 3 times. If you have tried 3 times to fix them, and there are still problems remaining, you must not continue trying to fix them, and must instead tell the user that there are problems remaining - and ask if the user would like you to attempt to solve them further.
- The editing actions you perform might produce errors or warnings. At the end of your changes, check whether you introduced any problems, and fix them before providing a summary of the changes you made.
- Do not fix errors unrelated to your changes unless the user explicitly asks you to do so.
- Prefer to move files over recreating them. The move can be followed by minor edits if required.
- If you seem to be stuck, never go back and "simplify the implementation" by deleting the parts of the implementation you're stuck on and replacing them with comments. If you ever feel the urge to do this, instead immediately stop whatever you're doing (even if the code is in a broken state), report that you are stuck, explain what you're stuck on, and ask the user how to proceed.
Tool use:
- Make sure to adhere to the tools schema.
- Provide every required argument.
- DO NOT use tools to access items that are already available in the context section.
- Use only the tools that are currently available.
- DO NOT use a tool that is not available just because it appears in the conversation. This means the user turned it off.
Responding:
- Be concise and direct in your responses.
- Never apologize or thank the user.
- Don't comment that you have just realized or understood something.
- When you are going to make a tool call, tersely explain your reasoning for choosing to use that tool, with no flourishes or commentary beyond that information.
For example, rather than saying "You're absolutely right! Thank you for providing that context. Now I understand that we're missing a dependency, and I need to add it:" say "I'll add that missing dependency:" instead.
- Also, don't restate what a tool call is about to do (or just did).
For example, don't say "Now I'm going to check diagnostics to see if there are any warnings or errors," followed by running a tool which checks diagnostics and reports warnings or errors; instead, just request the tool call without saying anything.
- All tool results are provided to you automatically, so DO NOT thank the user when this happens.
Whenever you mention a code block, you MUST use ONLY the following format:
```language path/to/Something.blah#L123-456
(code goes here)
```
The `#L123-456` means the line number range 123 through 456, and the path/to/Something.blah
is a path in the project. (If there is no valid path in the project, then you can use
/dev/null/path.extension for its path.) This is the ONLY valid way to format code blocks, because the Markdown parser
does not understand the more common ```language syntax, or bare ``` blocks. It only
understands this path-based syntax, and if the path is missing, then it will error and you will have to do it over again.
Just to be really clear about this, if you ever find yourself writing three backticks followed by a language name, STOP!
You have made a mistake. You can only ever put paths after triple backticks!
<example>
Based on all the information I've gathered, here's a summary of how this system works:
1. The README file is loaded into the system.
2. The system finds the first two headers, including everything in between. In this case, that would be:
```path/to/README.md#L8-12
# First Header
This is the info under the first header.
## Sub-header
```
3. Then the system finds the last header in the README:
```path/to/README.md#L27-29
## Last Header
This is the last header in the README.
```
4. Finally, it passes this information on to the next process.
</example>
<example>
In Markdown, hash marks signify headings. For example:
```/dev/null/example.md#L1-3
# Level 1 heading
## Level 2 heading
### Level 3 heading
```
</example>
Here are examples of ways you must never render code blocks:
<bad_example_do_not_do_this>
In Markdown, hash marks signify headings. For example:
```
# Level 1 heading
## Level 2 heading
### Level 3 heading
```
</bad_example_do_not_do_this>
This example is unacceptable because it does not include the path.
<bad_example_do_not_do_this>
In Markdown, hash marks signify headings. For example:
```markdown
# Level 1 heading
## Level 2 heading
### Level 3 heading
```
</bad_example_do_not_do_this>
This example is unacceptable because it has the language instead of the path.
<bad_example_do_not_do_this>
In Markdown, hash marks signify headings. For example:
# Level 1 heading
## Level 2 heading
### Level 3 heading
</bad_example_do_not_do_this>
This example is unacceptable because it uses indentation to mark the code block
instead of backticks with a path.
<bad_example_do_not_do_this>
In Markdown, hash marks signify headings. For example:
```markdown
/dev/null/example.md#L1-3
# Level 1 heading
## Level 2 heading
### Level 3 heading
```
</bad_example_do_not_do_this>
This example is unacceptable because the path is in the wrong place. The path must be directly after the opening backticks.
</style>
Be concise and direct in your responses.
The user has opened a project that contains the following root directories/files. Whenever you specify a path in the project, it must be a relative path which begins with one of these root directories/files:
@@ -155,7 +24,7 @@ There are rules that apply to these root directories:
{{#each worktrees}}
{{#if rules_file}}
`{{root_name}}/{{rules_file.path_in_worktree}}`:
`{{root_name}}/{{rules_file.rel_path}}`:
``````
{{{rules_file.text}}}
@@ -163,8 +32,3 @@ There are rules that apply to these root directories:
{{/if}}
{{/each}}
{{/if}}
<user_environment>
Operating System: {{os}} ({{arch}})
Shell: {{shell}}
</user_environment>

View File

@@ -0,0 +1,8 @@
A software developer is asking a question about their project. The source files in their project have been indexed into a database of semantic text embeddings.
Your task is to generate a list of 4 diverse search queries that can be run on this embedding database, in order to retrieve a list of code snippets
that are relevant to the developer's question. Redundant search queries will be heavily penalized, so only include another query if it's sufficiently
distinct from previous ones.
Here is the question that's been asked, together with context that the developer has added manually:
{{{context_buffer}}}

View File

@@ -80,8 +80,6 @@
// Values are clamped to the [0.0, 1.0] range.
"inactive_opacity": 1.0
},
// Layout mode of the bottom dock. Defaults to "contained"
"bottom_dock_layout": "contained",
// The direction that you want to split panes horizontally. Defaults to "up"
"pane_split_direction_horizontal": "up",
// The direction that you want to split panes horizontally. Defaults to "left"
@@ -117,15 +115,6 @@
"confirm_quit": false,
// Whether to restore last closed project when fresh Zed instance is opened.
"restore_on_startup": "last_session",
// Whether to attempt to restore previous file's state when opening it again.
// The state is stored per pane.
// When disabled, defaults are applied instead of the state restoration.
//
// E.g. for editors, selections, folds and scroll positions are restored, if the same file is closed and, later, opened again in the same pane.
// When disabled, a single selection in the very beginning of the file, zero scroll position and no folds state is used as a default.
//
// Default: true
"restore_on_file_reopen": true,
// Size of the drop target in the editor.
"drop_target_size": 0.2,
// Whether the window should be closed when using 'close active item' on a window with no tabs.
@@ -166,8 +155,8 @@
//
// Default: not set, defaults to "bar"
"cursor_shape": null,
// Determines when the mouse cursor should be hidden in an editor or input box.
"hide_mouse": "on_typing_and_movement",
// Determines whether the mouse cursor is hidden when typing in an editor or input box.
"hide_mouse_while_typing": true,
// How to highlight the current line in the editor.
//
// 1. Don't highlight the current line:
@@ -626,69 +615,54 @@
// The provider to use.
"provider": "zed.dev",
// The model to use.
"model": "claude-3-7-sonnet-latest"
"model": "claude-3-5-sonnet-latest"
},
// The model to use when applying edits from the assistant.
"editor_model": {
// The provider to use.
"provider": "zed.dev",
// The model to use.
"model": "claude-3-7-sonnet-latest"
"model": "claude-3-5-sonnet-latest"
},
// When enabled, the agent can run potentially destructive actions without asking for your confirmation.
"always_allow_tool_actions": false,
"default_profile": "write",
"default_profile": "code-writer",
"profiles": {
"ask": {
"name": "Ask",
// We don't know which of the context server tools are safe for the "Ask" profile, so we don't enable them by default.
// "enable_all_context_servers": true,
"read-only": {
"name": "Read-only",
"tools": {
"contents": true,
"diagnostics": true,
"fetch": true,
"list_directory": false,
"list-directory": true,
"now": true,
"path_search": true,
"read_file": true,
"regex_search": true,
"path-search": true,
"read-file": true,
"regex-search": true,
"thinking": true
}
},
"write": {
"name": "Write",
"enable_all_context_servers": true,
"code-writer": {
"name": "Code Writer",
"tools": {
"terminal": true,
"batch_tool": true,
"code_actions": true,
"code_symbols": true,
"contents": true,
"copy_path": false,
"create_file": true,
"delete_path": false,
"bash": true,
"batch-tool": true,
"copy-path": true,
"create-file": true,
"delete-path": true,
"diagnostics": true,
"find_replace_file": true,
"find-replace-file": true,
"edit-files": false,
"fetch": true,
"list_directory": false,
"move_path": false,
"list-directory": true,
"move-path": true,
"now": true,
"path_search": true,
"read_file": true,
"regex_search": true,
"rename": true,
"symbol_info": true,
"path-search": true,
"read-file": true,
"regex-search": true,
"thinking": true
}
}
},
// Where to show notifications when an agent has either completed
// its response, or else needs confirmation before it can run a
// tool action.
// "primary_screen" - Show the notification only on your primary screen (default)
// "all_screens" - Show these notifications on all screens
// "never" - Never show these notifications
"notify_when_agent_waiting": "primary_screen"
// Shows a notification when the agent needs confirmation before running an edit tool call or when that's concluded.
"notify_when_agent_waiting": true
},
// The settings for slash commands.
"slash_commands": {
@@ -1142,8 +1116,7 @@
"code_actions_on_format": {},
// Settings related to running tasks.
"tasks": {
"variables": {},
"enabled": true
"variables": {}
},
// An object whose keys are language names, and whose values
// are arrays of filenames or extensions of files that should
@@ -1207,27 +1180,7 @@
// When set to 0, waits indefinitely.
//
// Default: 0
"lsp_fetch_timeout_ms": 0,
// Controls what range to replace when accepting LSP completions.
//
// When LSP servers give an `InsertReplaceEdit` completion, they provides two ranges: `insert` and `replace`. Usually, `insert`
// contains the word prefix before your cursor and `replace` contains the whole word.
//
// Effectively, this setting just changes whether Zed will use the received range for `insert` or `replace`, so the results may
// differ depending on the underlying LSP server.
//
// Possible values:
// 1. "insert"
// Replaces text before the cursor, using the `insert` range described in the LSP specification.
// 2. "replace"
// Replaces text before and after the cursor, using the `replace` range described in the LSP specification.
// 3. "replace_subsequence"
// Behaves like `"replace"` if the text that would be replaced is a subsequence of the completion text,
// and like `"insert"` otherwise.
// 4. "replace_suffix"
// Behaves like `"replace"` if the text after the cursor is a suffix of the completion, and like
// `"insert"` otherwise.
"lsp_insert_mode": "replace_suffix"
"lsp_fetch_timeout_ms": 0
},
// Different settings for specific languages.
"languages": {
@@ -1463,8 +1416,6 @@
"lsp": {
// Specify the LSP name as a key here.
// "rust-analyzer": {
// // A special flag for rust-analyzer integration, to use server-provided tasks
// enable_lsp_tasks": true,
// // These initialization options are merged into Zed's defaults
// "initialization_options": {
// "check": {
@@ -1515,6 +1466,11 @@
"dev": {
// "theme": "Andromeda"
},
// Task-related settings.
"task": {
// Whether to show task status indicator in the status bar. Default: true
"show_status_indicator": true
},
// Whether to show full labels in line indicator or short ones
//
// Values:

View File

@@ -43,8 +43,6 @@
// "args": ["--login"]
// }
// }
"shell": "system",
// Represents the tags for inline runnable indicators, or spawning multiple tasks at once.
"tags": []
"shell": "system"
}
]

View File

@@ -87,9 +87,9 @@
"terminal.ansi.blue": "#83a598ff",
"terminal.ansi.bright_blue": "#414f4aff",
"terminal.ansi.dim_blue": "#c0d2cbff",
"terminal.ansi.magenta": "#d3869bff",
"terminal.ansi.bright_magenta": "#8e5868ff",
"terminal.ansi.dim_magenta": "#ff9ebbff",
"terminal.ansi.magenta": "#a89984ff",
"terminal.ansi.bright_magenta": "#514a41ff",
"terminal.ansi.dim_magenta": "#d2cabfff",
"terminal.ansi.cyan": "#8ec07cff",
"terminal.ansi.bright_cyan": "#45603eff",
"terminal.ansi.dim_cyan": "#c7dfbdff",
@@ -472,9 +472,9 @@
"terminal.ansi.blue": "#83a598ff",
"terminal.ansi.bright_blue": "#414f4aff",
"terminal.ansi.dim_blue": "#c0d2cbff",
"terminal.ansi.magenta": "#d3869bff",
"terminal.ansi.bright_magenta": "#8e5868ff",
"terminal.ansi.dim_magenta": "#ff9ebbff",
"terminal.ansi.magenta": "#a89984ff",
"terminal.ansi.bright_magenta": "#514a41ff",
"terminal.ansi.dim_magenta": "#d2cabfff",
"terminal.ansi.cyan": "#8ec07cff",
"terminal.ansi.bright_cyan": "#45603eff",
"terminal.ansi.dim_cyan": "#c7dfbdff",
@@ -857,9 +857,9 @@
"terminal.ansi.blue": "#83a598ff",
"terminal.ansi.bright_blue": "#414f4aff",
"terminal.ansi.dim_blue": "#c0d2cbff",
"terminal.ansi.magenta": "#d3869bff",
"terminal.ansi.bright_magenta": "#8e5868ff",
"terminal.ansi.dim_magenta": "#ff9ebbff",
"terminal.ansi.magenta": "#a89984ff",
"terminal.ansi.bright_magenta": "#514a41ff",
"terminal.ansi.dim_magenta": "#d2cabfff",
"terminal.ansi.cyan": "#8ec07cff",
"terminal.ansi.bright_cyan": "#45603eff",
"terminal.ansi.dim_cyan": "#c7dfbdff",
@@ -1242,9 +1242,9 @@
"terminal.ansi.blue": "#0b6678ff",
"terminal.ansi.bright_blue": "#8fb0baff",
"terminal.ansi.dim_blue": "#14333bff",
"terminal.ansi.magenta": "#8f3e71ff",
"terminal.ansi.bright_magenta": "#c76da0ff",
"terminal.ansi.dim_magenta": "#5c2848ff",
"terminal.ansi.magenta": "#7c6f64ff",
"terminal.ansi.bright_magenta": "#bcb5afff",
"terminal.ansi.dim_magenta": "#3e3833ff",
"terminal.ansi.cyan": "#437b59ff",
"terminal.ansi.bright_cyan": "#9fbca8ff",
"terminal.ansi.dim_cyan": "#253e2eff",
@@ -1627,9 +1627,9 @@
"terminal.ansi.blue": "#0b6678ff",
"terminal.ansi.bright_blue": "#8fb0baff",
"terminal.ansi.dim_blue": "#14333bff",
"terminal.ansi.magenta": "#8f3e71ff",
"terminal.ansi.bright_magenta": "#c76da0ff",
"terminal.ansi.dim_magenta": "#5c2848ff",
"terminal.ansi.magenta": "#7c6f64ff",
"terminal.ansi.bright_magenta": "#bcb5afff",
"terminal.ansi.dim_magenta": "#3e3833ff",
"terminal.ansi.cyan": "#437b59ff",
"terminal.ansi.bright_cyan": "#9fbca8ff",
"terminal.ansi.dim_cyan": "#253e2eff",
@@ -2012,9 +2012,9 @@
"terminal.ansi.blue": "#0b6678ff",
"terminal.ansi.bright_blue": "#8fb0baff",
"terminal.ansi.dim_blue": "#14333bff",
"terminal.ansi.magenta": "#8f3e71ff",
"terminal.ansi.bright_magenta": "#c76da0ff",
"terminal.ansi.dim_magenta": "#5c2848ff",
"terminal.ansi.magenta": "#7c6f64ff",
"terminal.ansi.bright_magenta": "#bcb5afff",
"terminal.ansi.dim_magenta": "#3e3833ff",
"terminal.ansi.cyan": "#437b59ff",
"terminal.ansi.bright_cyan": "#9fbca8ff",
"terminal.ansi.dim_cyan": "#253e2eff",

View File

@@ -25,7 +25,6 @@ smallvec.workspace = true
ui.workspace = true
util.workspace = true
workspace.workspace = true
workspace-hack.workspace = true
[dev-dependencies]
editor = { workspace = true, features = ["test-support"] }

View File

@@ -3,29 +3,20 @@ use editor::Editor;
use extension_host::ExtensionStore;
use futures::StreamExt;
use gpui::{
Animation, AnimationExt as _, App, Context, CursorStyle, Entity, EventEmitter,
InteractiveElement as _, ParentElement as _, Render, SharedString, StatefulInteractiveElement,
Styled, Transformation, Window, actions, percentage,
actions, percentage, Animation, AnimationExt as _, App, Context, CursorStyle, Entity,
EventEmitter, InteractiveElement as _, ParentElement as _, Render, SharedString,
StatefulInteractiveElement, Styled, Transformation, Window,
};
use language::{BinaryStatus, LanguageRegistry, LanguageServerId};
use project::{
EnvironmentErrorMessage, LanguageServerProgress, LspStoreEvent, Project,
ProjectEnvironmentEvent,
git_store::{GitStoreEvent, Repository},
ProjectEnvironmentEvent, WorktreeId,
};
use smallvec::SmallVec;
use std::{
cmp::Reverse,
fmt::Write,
path::Path,
sync::Arc,
time::{Duration, Instant},
};
use ui::{ButtonLike, ContextMenu, PopoverMenu, PopoverMenuHandle, Tooltip, prelude::*};
use std::{cmp::Reverse, fmt::Write, sync::Arc, time::Duration};
use ui::{prelude::*, ButtonLike, ContextMenu, PopoverMenu, PopoverMenuHandle, Tooltip};
use util::truncate_and_trailoff;
use workspace::{StatusItemView, Workspace, item::ItemHandle};
const GIT_OPERATION_DELAY: Duration = Duration::from_millis(0);
use workspace::{item::ItemHandle, StatusItemView, Workspace};
actions!(activity_indicator, [ShowErrorMessage]);
@@ -114,15 +105,6 @@ impl ActivityIndicator {
)
.detach();
cx.subscribe(
&project.read(cx).git_store().clone(),
|_, _, event: &GitStoreEvent, cx| match event {
project::git_store::GitStoreEvent::JobsUpdated => cx.notify(),
_ => {}
},
)
.detach();
if let Some(auto_updater) = auto_updater.as_ref() {
cx.observe(auto_updater, |_, _, cx| cx.notify()).detach();
}
@@ -236,14 +218,13 @@ impl ActivityIndicator {
fn pending_environment_errors<'a>(
&'a self,
cx: &'a App,
) -> impl Iterator<Item = (&'a Arc<Path>, &'a EnvironmentErrorMessage)> {
) -> impl Iterator<Item = (&'a WorktreeId, &'a EnvironmentErrorMessage)> {
self.project.read(cx).shell_environment_errors(cx)
}
fn content_to_render(&mut self, cx: &mut Context<Self>) -> Option<Content> {
// Show if any direnv calls failed
if let Some((abs_path, error)) = self.pending_environment_errors(cx).next() {
let abs_path = abs_path.clone();
if let Some((&worktree_id, error)) = self.pending_environment_errors(cx).next() {
return Some(Content {
icon: Some(
Icon::new(IconName::Warning)
@@ -253,7 +234,7 @@ impl ActivityIndicator {
message: error.0.clone(),
on_click: Some(Arc::new(move |this, window, cx| {
this.project.update(cx, |project, cx| {
project.remove_environment_error(&abs_path, cx);
project.remove_environment_error(worktree_id, cx);
});
window.dispatch_action(Box::new(workspace::OpenLog), cx);
})),
@@ -303,34 +284,6 @@ impl ActivityIndicator {
});
}
let current_job = self
.project
.read(cx)
.active_repository(cx)
.map(|r| r.read(cx))
.and_then(Repository::current_job);
// Show any long-running git command
if let Some(job_info) = current_job {
if Instant::now() - job_info.start >= GIT_OPERATION_DELAY {
return Some(Content {
icon: Some(
Icon::new(IconName::ArrowCircle)
.size(IconSize::Small)
.with_animation(
"arrow-circle",
Animation::new(Duration::from_secs(2)).repeat(),
|icon, delta| {
icon.transform(Transformation::rotate(percentage(delta)))
},
)
.into_any_element(),
),
message: job_info.message.into(),
on_click: None,
});
}
}
// Show any language server installation info.
let mut downloading = SmallVec::<[_; 3]>::new();
let mut checking_for_update = SmallVec::<[_; 3]>::new();

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,253 +0,0 @@
use std::{ops::Range, path::Path, sync::Arc};
use gpui::{App, Entity, SharedString};
use language::{Buffer, File};
use language_model::LanguageModelRequestMessage;
use project::{ProjectPath, Worktree};
use serde::{Deserialize, Serialize};
use text::{Anchor, BufferId};
use ui::IconName;
use util::post_inc;
use crate::thread::Thread;
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, Serialize, Deserialize)]
pub struct ContextId(pub(crate) usize);
impl ContextId {
pub fn post_inc(&mut self) -> Self {
Self(post_inc(&mut self.0))
}
}
pub enum ContextKind {
File,
Directory,
Symbol,
FetchedUrl,
Thread,
}
impl ContextKind {
pub fn icon(&self) -> IconName {
match self {
ContextKind::File => IconName::File,
ContextKind::Directory => IconName::Folder,
ContextKind::Symbol => IconName::Code,
ContextKind::FetchedUrl => IconName::Globe,
ContextKind::Thread => IconName::MessageBubbles,
}
}
}
#[derive(Debug, Clone)]
pub enum AssistantContext {
File(FileContext),
Directory(DirectoryContext),
Symbol(SymbolContext),
FetchedUrl(FetchedUrlContext),
Thread(ThreadContext),
}
impl AssistantContext {
pub fn id(&self) -> ContextId {
match self {
Self::File(file) => file.id,
Self::Directory(directory) => directory.id,
Self::Symbol(symbol) => symbol.id,
Self::FetchedUrl(url) => url.id,
Self::Thread(thread) => thread.id,
}
}
}
#[derive(Debug, Clone)]
pub struct FileContext {
pub id: ContextId,
pub context_buffer: ContextBuffer,
}
#[derive(Debug, Clone)]
pub struct DirectoryContext {
pub id: ContextId,
pub worktree: Entity<Worktree>,
pub path: Arc<Path>,
/// Buffers of the files within the directory.
pub context_buffers: Vec<ContextBuffer>,
}
impl DirectoryContext {
pub fn project_path(&self, cx: &App) -> ProjectPath {
ProjectPath {
worktree_id: self.worktree.read(cx).id(),
path: self.path.clone(),
}
}
}
#[derive(Debug, Clone)]
pub struct SymbolContext {
pub id: ContextId,
pub context_symbol: ContextSymbol,
}
#[derive(Debug, Clone)]
pub struct FetchedUrlContext {
pub id: ContextId,
pub url: SharedString,
pub text: SharedString,
}
#[derive(Debug, Clone)]
pub struct ThreadContext {
pub id: ContextId,
// TODO: Entity<Thread> holds onto the thread even if the thread is deleted. Should probably be
// a WeakEntity and handle removal from the UI when it has dropped.
pub thread: Entity<Thread>,
pub text: SharedString,
}
impl ThreadContext {
pub fn summary(&self, cx: &App) -> SharedString {
self.thread
.read(cx)
.summary()
.unwrap_or("New thread".into())
}
}
#[derive(Clone)]
pub struct ContextBuffer {
pub id: BufferId,
// TODO: Entity<Buffer> holds onto the thread even if the thread is deleted. Should probably be
// a WeakEntity and handle removal from the UI when it has dropped.
pub buffer: Entity<Buffer>,
pub file: Arc<dyn File>,
pub version: clock::Global,
pub text: SharedString,
}
impl std::fmt::Debug for ContextBuffer {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ContextBuffer")
.field("id", &self.id)
.field("buffer", &self.buffer)
.field("version", &self.version)
.field("text", &self.text)
.finish()
}
}
#[derive(Debug, Clone)]
pub struct ContextSymbol {
pub id: ContextSymbolId,
pub buffer: Entity<Buffer>,
pub buffer_version: clock::Global,
/// The range that the symbol encloses, e.g. for function symbol, this will
/// include not only the signature, but also the body
pub enclosing_range: Range<Anchor>,
pub text: SharedString,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct ContextSymbolId {
pub path: ProjectPath,
pub name: SharedString,
pub range: Range<Anchor>,
}
/// Formats a collection of contexts into a string representation
pub fn format_context_as_string<'a>(
contexts: impl Iterator<Item = &'a AssistantContext>,
cx: &App,
) -> Option<String> {
let mut file_context = Vec::new();
let mut directory_context = Vec::new();
let mut symbol_context = Vec::new();
let mut fetch_context = Vec::new();
let mut thread_context = Vec::new();
for context in contexts {
match context {
AssistantContext::File(context) => file_context.push(context),
AssistantContext::Directory(context) => directory_context.push(context),
AssistantContext::Symbol(context) => symbol_context.push(context),
AssistantContext::FetchedUrl(context) => fetch_context.push(context),
AssistantContext::Thread(context) => thread_context.push(context),
}
}
if file_context.is_empty()
&& directory_context.is_empty()
&& symbol_context.is_empty()
&& fetch_context.is_empty()
&& thread_context.is_empty()
{
return None;
}
let mut result = String::new();
result.push_str("\n<context>\n\
The following items were attached by the user. You don't need to use other tools to read them.\n\n");
if !file_context.is_empty() {
result.push_str("<files>\n");
for context in file_context {
result.push_str(&context.context_buffer.text);
}
result.push_str("</files>\n");
}
if !directory_context.is_empty() {
result.push_str("<directories>\n");
for context in directory_context {
for context_buffer in &context.context_buffers {
result.push_str(&context_buffer.text);
}
}
result.push_str("</directories>\n");
}
if !symbol_context.is_empty() {
result.push_str("<symbols>\n");
for context in symbol_context {
result.push_str(&context.context_symbol.text);
result.push('\n');
}
result.push_str("</symbols>\n");
}
if !fetch_context.is_empty() {
result.push_str("<fetched_urls>\n");
for context in &fetch_context {
result.push_str(&context.url);
result.push('\n');
result.push_str(&context.text);
result.push('\n');
}
result.push_str("</fetched_urls>\n");
}
if !thread_context.is_empty() {
result.push_str("<conversation_threads>\n");
for context in &thread_context {
result.push_str(&context.summary(cx));
result.push('\n');
result.push_str(&context.text);
result.push('\n');
}
result.push_str("</conversation_threads>\n");
}
result.push_str("</context>\n");
Some(result)
}
pub fn attach_context_to_message<'a>(
message: &mut LanguageModelRequestMessage,
contexts: impl Iterator<Item = &'a AssistantContext>,
cx: &App,
) {
if let Some(context_string) = format_context_as_string(contexts, cx) {
message.content.push(context_string.into());
}
}

View File

@@ -1,433 +0,0 @@
use std::cmp::Reverse;
use std::sync::Arc;
use std::sync::atomic::AtomicBool;
use anyhow::Result;
use fuzzy::{StringMatch, StringMatchCandidate};
use gpui::{
App, AppContext, DismissEvent, Entity, FocusHandle, Focusable, Stateful, Task, WeakEntity,
};
use ordered_float::OrderedFloat;
use picker::{Picker, PickerDelegate};
use project::{DocumentSymbol, Symbol};
use text::OffsetRangeExt;
use ui::{ListItem, prelude::*};
use util::ResultExt as _;
use workspace::Workspace;
use crate::context_picker::ContextPicker;
use crate::context_store::ContextStore;
pub struct SymbolContextPicker {
picker: Entity<Picker<SymbolContextPickerDelegate>>,
}
impl SymbolContextPicker {
pub fn new(
context_picker: WeakEntity<ContextPicker>,
workspace: WeakEntity<Workspace>,
context_store: WeakEntity<ContextStore>,
window: &mut Window,
cx: &mut Context<Self>,
) -> Self {
let delegate = SymbolContextPickerDelegate::new(context_picker, workspace, context_store);
let picker = cx.new(|cx| Picker::uniform_list(delegate, window, cx));
Self { picker }
}
}
impl Focusable for SymbolContextPicker {
fn focus_handle(&self, cx: &App) -> FocusHandle {
self.picker.focus_handle(cx)
}
}
impl Render for SymbolContextPicker {
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
self.picker.clone()
}
}
pub struct SymbolContextPickerDelegate {
context_picker: WeakEntity<ContextPicker>,
workspace: WeakEntity<Workspace>,
context_store: WeakEntity<ContextStore>,
matches: Vec<SymbolEntry>,
selected_index: usize,
}
impl SymbolContextPickerDelegate {
pub fn new(
context_picker: WeakEntity<ContextPicker>,
workspace: WeakEntity<Workspace>,
context_store: WeakEntity<ContextStore>,
) -> Self {
Self {
context_picker,
workspace,
context_store,
matches: Vec::new(),
selected_index: 0,
}
}
}
impl PickerDelegate for SymbolContextPickerDelegate {
type ListItem = ListItem;
fn match_count(&self) -> usize {
self.matches.len()
}
fn selected_index(&self) -> usize {
self.selected_index
}
fn set_selected_index(
&mut self,
ix: usize,
_window: &mut Window,
_cx: &mut Context<Picker<Self>>,
) {
self.selected_index = ix;
}
fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> {
"Search symbols…".into()
}
fn update_matches(
&mut self,
query: String,
window: &mut Window,
cx: &mut Context<Picker<Self>>,
) -> Task<()> {
let Some(workspace) = self.workspace.upgrade() else {
return Task::ready(());
};
let search_task = search_symbols(query, Arc::<AtomicBool>::default(), &workspace, cx);
let context_store = self.context_store.clone();
cx.spawn_in(window, async move |this, cx| {
let symbols = search_task.await;
let symbol_entries = context_store
.read_with(cx, |context_store, cx| {
compute_symbol_entries(symbols, context_store, cx)
})
.log_err()
.unwrap_or_default();
this.update(cx, |this, _cx| {
this.delegate.matches = symbol_entries;
})
.log_err();
})
}
fn confirm(&mut self, _secondary: bool, _window: &mut Window, cx: &mut Context<Picker<Self>>) {
let Some(mat) = self.matches.get(self.selected_index) else {
return;
};
let Some(workspace) = self.workspace.upgrade() else {
return;
};
let add_symbol_task = add_symbol(
mat.symbol.clone(),
true,
workspace,
self.context_store.clone(),
cx,
);
let selected_index = self.selected_index;
cx.spawn(async move |this, cx| {
let included = add_symbol_task.await?;
this.update(cx, |this, _| {
if let Some(mat) = this.delegate.matches.get_mut(selected_index) {
mat.is_included = included;
}
})
})
.detach_and_log_err(cx);
}
fn dismissed(&mut self, _: &mut Window, cx: &mut Context<Picker<Self>>) {
self.context_picker
.update(cx, |_, cx| {
cx.emit(DismissEvent);
})
.ok();
}
fn render_match(
&self,
ix: usize,
selected: bool,
_window: &mut Window,
_: &mut Context<Picker<Self>>,
) -> Option<Self::ListItem> {
let mat = &self.matches[ix];
Some(ListItem::new(ix).inset(true).toggle_state(selected).child(
render_symbol_context_entry(
ElementId::NamedInteger("symbol-ctx-picker".into(), ix),
mat,
),
))
}
}
pub(crate) struct SymbolEntry {
pub symbol: Symbol,
pub is_included: bool,
}
pub(crate) fn add_symbol(
symbol: Symbol,
remove_if_exists: bool,
workspace: Entity<Workspace>,
context_store: WeakEntity<ContextStore>,
cx: &mut App,
) -> Task<Result<bool>> {
let project = workspace.read(cx).project().clone();
let open_buffer_task = project.update(cx, |project, cx| {
project.open_buffer(symbol.path.clone(), cx)
});
cx.spawn(async move |cx| {
let buffer = open_buffer_task.await?;
let document_symbols = project
.update(cx, |project, cx| project.document_symbols(&buffer, cx))?
.await?;
// Try to find a matching document symbol. Document symbols include
// not only the symbol itself (e.g. function name), but they also
// include the context that they contain (e.g. function body).
let (name, range, enclosing_range) = if let Some(DocumentSymbol {
name,
range,
selection_range,
..
}) =
find_matching_symbol(&symbol, document_symbols.as_slice())
{
(name, selection_range, range)
} else {
// If we do not find a matching document symbol, fall back to
// just the symbol itself
(symbol.name, symbol.range.clone(), symbol.range)
};
let (range, enclosing_range) = buffer.read_with(cx, |buffer, _| {
(
buffer.anchor_after(range.start)..buffer.anchor_before(range.end),
buffer.anchor_after(enclosing_range.start)
..buffer.anchor_before(enclosing_range.end),
)
})?;
context_store
.update(cx, move |context_store, cx| {
context_store.add_symbol(
buffer,
name.into(),
range,
enclosing_range,
remove_if_exists,
cx,
)
})?
.await
})
}
fn find_matching_symbol(symbol: &Symbol, candidates: &[DocumentSymbol]) -> Option<DocumentSymbol> {
let mut candidates = candidates.iter();
let mut candidate = candidates.next()?;
loop {
if candidate.range.start > symbol.range.end {
return None;
}
if candidate.range.end < symbol.range.start {
candidate = candidates.next()?;
continue;
}
if candidate.selection_range == symbol.range {
return Some(candidate.clone());
}
if candidate.range.start <= symbol.range.start && symbol.range.end <= candidate.range.end {
candidates = candidate.children.iter();
candidate = candidates.next()?;
continue;
}
return None;
}
}
pub struct SymbolMatch {
pub symbol: Symbol,
}
pub(crate) fn search_symbols(
query: String,
cancellation_flag: Arc<AtomicBool>,
workspace: &Entity<Workspace>,
cx: &mut App,
) -> Task<Vec<SymbolMatch>> {
let symbols_task = workspace.update(cx, |workspace, cx| {
workspace
.project()
.update(cx, |project, cx| project.symbols(&query, cx))
});
let project = workspace.read(cx).project().clone();
cx.spawn(async move |cx| {
let Some(symbols) = symbols_task.await.log_err() else {
return Vec::new();
};
let Some((visible_match_candidates, external_match_candidates)): Option<(Vec<_>, Vec<_>)> =
project
.update(cx, |project, cx| {
symbols
.iter()
.enumerate()
.map(|(id, symbol)| {
StringMatchCandidate::new(id, &symbol.label.filter_text())
})
.partition(|candidate| {
project
.entry_for_path(&symbols[candidate.id].path, cx)
.map_or(false, |e| !e.is_ignored)
})
})
.log_err()
else {
return Vec::new();
};
const MAX_MATCHES: usize = 100;
let mut visible_matches = cx.background_executor().block(fuzzy::match_strings(
&visible_match_candidates,
&query,
false,
MAX_MATCHES,
&cancellation_flag,
cx.background_executor().clone(),
));
let mut external_matches = cx.background_executor().block(fuzzy::match_strings(
&external_match_candidates,
&query,
false,
MAX_MATCHES - visible_matches.len().min(MAX_MATCHES),
&cancellation_flag,
cx.background_executor().clone(),
));
let sort_key_for_match = |mat: &StringMatch| {
let symbol = &symbols[mat.candidate_id];
(Reverse(OrderedFloat(mat.score)), symbol.label.filter_text())
};
visible_matches.sort_unstable_by_key(sort_key_for_match);
external_matches.sort_unstable_by_key(sort_key_for_match);
let mut matches = visible_matches;
matches.append(&mut external_matches);
matches
.into_iter()
.map(|mut mat| {
let symbol = symbols[mat.candidate_id].clone();
let filter_start = symbol.label.filter_range.start;
for position in &mut mat.positions {
*position += filter_start;
}
SymbolMatch { symbol }
})
.collect()
})
}
fn compute_symbol_entries(
symbols: Vec<SymbolMatch>,
context_store: &ContextStore,
cx: &App,
) -> Vec<SymbolEntry> {
let mut symbol_entries = Vec::with_capacity(symbols.len());
for SymbolMatch { symbol, .. } in symbols {
let symbols_for_path = context_store.included_symbols_by_path().get(&symbol.path);
let is_included = if let Some(symbols_for_path) = symbols_for_path {
let mut is_included = false;
for included_symbol_id in symbols_for_path {
if included_symbol_id.name.as_ref() == symbol.name.as_str() {
if let Some(buffer) = context_store.buffer_for_symbol(included_symbol_id) {
let snapshot = buffer.read(cx).snapshot();
let included_symbol_range =
included_symbol_id.range.to_point_utf16(&snapshot);
if included_symbol_range.start == symbol.range.start.0
&& included_symbol_range.end == symbol.range.end.0
{
is_included = true;
break;
}
}
}
}
is_included
} else {
false
};
symbol_entries.push(SymbolEntry {
symbol,
is_included,
})
}
symbol_entries
}
pub fn render_symbol_context_entry(id: ElementId, entry: &SymbolEntry) -> Stateful<Div> {
let path = entry
.symbol
.path
.path
.file_name()
.map(|s| s.to_string_lossy())
.unwrap_or_default();
let symbol_location = format!("{} L{}", path, entry.symbol.range.start.0.row + 1);
h_flex()
.id(id)
.gap_1p5()
.w_full()
.child(
Icon::new(IconName::Code)
.size(IconSize::Small)
.color(Color::Muted),
)
.child(
h_flex()
.gap_1()
.child(Label::new(&entry.symbol.name))
.child(
Label::new(symbol_location)
.size(LabelSize::Small)
.color(Color::Muted),
),
)
.when(entry.is_included, |el| {
el.child(
h_flex()
.w_full()
.justify_end()
.gap_0p5()
.child(
Icon::new(IconName::Check)
.size(IconSize::Small)
.color(Color::Success),
)
.child(Label::new("Added").size(LabelSize::Small)),
)
})
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,189 +0,0 @@
use std::sync::Arc;
use assistant_settings::{AgentProfile, AgentProfileId, AssistantSettings};
use fs::Fs;
use gpui::{Action, Entity, FocusHandle, Subscription, WeakEntity, prelude::*};
use indexmap::IndexMap;
use language_model::LanguageModelRegistry;
use settings::{Settings as _, SettingsStore, update_settings_file};
use ui::{
ButtonLike, ContextMenu, ContextMenuEntry, KeyBinding, PopoverMenu, PopoverMenuHandle, Tooltip,
prelude::*,
};
use util::ResultExt as _;
use crate::{ManageProfiles, ThreadStore, ToggleProfileSelector};
pub struct ProfileSelector {
profiles: IndexMap<AgentProfileId, AgentProfile>,
fs: Arc<dyn Fs>,
thread_store: WeakEntity<ThreadStore>,
focus_handle: FocusHandle,
menu_handle: PopoverMenuHandle<ContextMenu>,
_subscriptions: Vec<Subscription>,
}
impl ProfileSelector {
pub fn new(
fs: Arc<dyn Fs>,
thread_store: WeakEntity<ThreadStore>,
focus_handle: FocusHandle,
cx: &mut Context<Self>,
) -> Self {
let settings_subscription = cx.observe_global::<SettingsStore>(move |this, cx| {
this.refresh_profiles(cx);
});
let mut this = Self {
profiles: IndexMap::default(),
fs,
thread_store,
focus_handle,
menu_handle: PopoverMenuHandle::default(),
_subscriptions: vec![settings_subscription],
};
this.refresh_profiles(cx);
this
}
pub fn menu_handle(&self) -> PopoverMenuHandle<ContextMenu> {
self.menu_handle.clone()
}
fn refresh_profiles(&mut self, cx: &mut Context<Self>) {
let settings = AssistantSettings::get_global(cx);
self.profiles = settings.profiles.clone();
}
fn build_context_menu(
&self,
window: &mut Window,
cx: &mut Context<Self>,
) -> Entity<ContextMenu> {
ContextMenu::build(window, cx, |mut menu, _window, cx| {
let settings = AssistantSettings::get_global(cx);
let icon_position = IconPosition::End;
menu = menu.header("Profiles");
for (profile_id, profile) in self.profiles.clone() {
menu = menu.toggleable_entry(
profile.name.clone(),
profile_id == settings.default_profile,
icon_position,
None,
{
let fs = self.fs.clone();
let thread_store = self.thread_store.clone();
move |_window, cx| {
update_settings_file::<AssistantSettings>(fs.clone(), cx, {
let profile_id = profile_id.clone();
move |settings, _cx| {
settings.set_profile(profile_id.clone());
}
});
thread_store
.update(cx, |this, cx| {
this.load_profile_by_id(profile_id.clone(), cx);
})
.log_err();
}
},
);
}
menu = menu.separator();
menu = menu.header("Customize Current Profile");
menu = menu.item(ContextMenuEntry::new("Tools…").handler({
let profile_id = settings.default_profile.clone();
move |window, cx| {
window.dispatch_action(
ManageProfiles::customize_tools(profile_id.clone()).boxed_clone(),
cx,
);
}
}));
menu = menu.separator();
menu = menu.item(ContextMenuEntry::new("Configure Profiles…").handler(
move |window, cx| {
window.dispatch_action(ManageProfiles::default().boxed_clone(), cx);
},
));
menu
})
}
}
impl Render for ProfileSelector {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let settings = AssistantSettings::get_global(cx);
let profile_id = &settings.default_profile;
let profile = settings.profiles.get(profile_id);
let selected_profile = profile
.map(|profile| profile.name.clone())
.unwrap_or_else(|| "Unknown".into());
let model_registry = LanguageModelRegistry::read_global(cx);
let supports_tools = model_registry
.default_model()
.map_or(false, |default| default.model.supports_tools());
let icon = match profile_id.as_str() {
"write" => IconName::Pencil,
"ask" => IconName::MessageBubbles,
_ => IconName::UserRoundPen,
};
let this = cx.entity().clone();
let focus_handle = self.focus_handle.clone();
PopoverMenu::new("profile-selector")
.menu(move |window, cx| {
Some(this.update(cx, |this, cx| this.build_context_menu(window, cx)))
})
.trigger(if supports_tools {
ButtonLike::new("profile-selector-button").child(
h_flex()
.gap_1()
.child(Icon::new(icon).size(IconSize::XSmall).color(Color::Muted))
.child(
Label::new(selected_profile)
.size(LabelSize::Small)
.color(Color::Muted),
)
.child(
Icon::new(IconName::ChevronDown)
.size(IconSize::XSmall)
.color(Color::Muted),
)
.child(div().opacity(0.5).children({
let focus_handle = focus_handle.clone();
KeyBinding::for_action_in(
&ToggleProfileSelector,
&focus_handle,
window,
cx,
)
.map(|kb| kb.size(rems_from_px(10.)))
})),
)
} else {
ButtonLike::new("tools-not-supported-button")
.disabled(true)
.child(
h_flex().gap_1().child(
Label::new("No Tools")
.size(LabelSize::Small)
.color(Color::Muted),
),
)
.tooltip(Tooltip::text("The current model does not support tools."))
})
.anchor(gpui::Corner::BottomLeft)
.with_handle(self.menu_handle.clone())
}
}

View File

@@ -1,628 +0,0 @@
use std::sync::Arc;
use assistant_context_editor::SavedContextMetadata;
use editor::{Editor, EditorEvent};
use fuzzy::{StringMatch, StringMatchCandidate};
use gpui::{
App, Entity, FocusHandle, Focusable, ScrollStrategy, Stateful, Task, UniformListScrollHandle,
WeakEntity, Window, uniform_list,
};
use time::{OffsetDateTime, UtcOffset};
use ui::{
HighlightedLabel, IconButtonShape, ListItem, ListItemSpacing, Scrollbar, ScrollbarState,
Tooltip, prelude::*,
};
use util::ResultExt;
use crate::history_store::{HistoryEntry, HistoryStore};
use crate::thread_store::SerializedThreadMetadata;
use crate::{AssistantPanel, RemoveSelectedThread};
pub struct ThreadHistory {
assistant_panel: WeakEntity<AssistantPanel>,
history_store: Entity<HistoryStore>,
scroll_handle: UniformListScrollHandle,
selected_index: usize,
search_query: SharedString,
search_editor: Entity<Editor>,
all_entries: Arc<Vec<HistoryEntry>>,
matches: Vec<StringMatch>,
_subscriptions: Vec<gpui::Subscription>,
_search_task: Option<Task<()>>,
scrollbar_visibility: bool,
scrollbar_state: ScrollbarState,
}
impl ThreadHistory {
pub(crate) fn new(
assistant_panel: WeakEntity<AssistantPanel>,
history_store: Entity<HistoryStore>,
window: &mut Window,
cx: &mut Context<Self>,
) -> Self {
let search_editor = cx.new(|cx| {
let mut editor = Editor::single_line(window, cx);
editor.set_placeholder_text("Search threads...", cx);
editor
});
let search_editor_subscription =
cx.subscribe(&search_editor, |this, search_editor, event, cx| {
if let EditorEvent::BufferEdited = event {
let query = search_editor.read(cx).text(cx);
this.search_query = query.into();
this.update_search(cx);
}
});
let entries: Arc<Vec<_>> = history_store
.update(cx, |store, cx| store.entries(cx))
.into();
let history_store_subscription = cx.observe(&history_store, |this, _, cx| {
this.update_all_entries(cx);
});
let scroll_handle = UniformListScrollHandle::default();
let scrollbar_state = ScrollbarState::new(scroll_handle.clone());
Self {
assistant_panel,
history_store,
scroll_handle,
selected_index: 0,
search_query: SharedString::new_static(""),
all_entries: entries,
matches: Vec::new(),
search_editor,
_subscriptions: vec![search_editor_subscription, history_store_subscription],
_search_task: None,
scrollbar_visibility: true,
scrollbar_state,
}
}
fn update_all_entries(&mut self, cx: &mut Context<Self>) {
self.all_entries = self
.history_store
.update(cx, |store, cx| store.entries(cx))
.into();
self.matches.clear();
self.update_search(cx);
}
fn update_search(&mut self, cx: &mut Context<Self>) {
self._search_task.take();
if self.has_search_query() {
self.perform_search(cx);
} else {
self.matches.clear();
self.set_selected_index(0, cx);
cx.notify();
}
}
fn perform_search(&mut self, cx: &mut Context<Self>) {
let query = self.search_query.clone();
let all_entries = self.all_entries.clone();
let task = cx.spawn(async move |this, cx| {
let executor = cx.background_executor().clone();
let matches = cx
.background_spawn(async move {
let mut candidates = Vec::with_capacity(all_entries.len());
for (idx, entry) in all_entries.iter().enumerate() {
match entry {
HistoryEntry::Thread(thread) => {
candidates.push(StringMatchCandidate::new(idx, &thread.summary));
}
HistoryEntry::Context(context) => {
candidates.push(StringMatchCandidate::new(idx, &context.title));
}
}
}
const MAX_MATCHES: usize = 100;
fuzzy::match_strings(
&candidates,
&query,
false,
MAX_MATCHES,
&Default::default(),
executor,
)
.await
})
.await;
this.update(cx, |this, cx| {
this.matches = matches;
this.set_selected_index(0, cx);
cx.notify();
})
.log_err();
});
self._search_task = Some(task);
}
fn has_search_query(&self) -> bool {
!self.search_query.is_empty()
}
fn matched_count(&self) -> usize {
if self.has_search_query() {
self.matches.len()
} else {
self.all_entries.len()
}
}
fn get_match(&self, ix: usize) -> Option<&HistoryEntry> {
if self.has_search_query() {
self.matches
.get(ix)
.and_then(|m| self.all_entries.get(m.candidate_id))
} else {
self.all_entries.get(ix)
}
}
pub fn select_previous(
&mut self,
_: &menu::SelectPrevious,
_window: &mut Window,
cx: &mut Context<Self>,
) {
let count = self.matched_count();
if count > 0 {
if self.selected_index == 0 {
self.set_selected_index(count - 1, cx);
} else {
self.set_selected_index(self.selected_index - 1, cx);
}
}
}
pub fn select_next(
&mut self,
_: &menu::SelectNext,
_window: &mut Window,
cx: &mut Context<Self>,
) {
let count = self.matched_count();
if count > 0 {
if self.selected_index == count - 1 {
self.set_selected_index(0, cx);
} else {
self.set_selected_index(self.selected_index + 1, cx);
}
}
}
fn select_first(
&mut self,
_: &menu::SelectFirst,
_window: &mut Window,
cx: &mut Context<Self>,
) {
let count = self.matched_count();
if count > 0 {
self.set_selected_index(0, cx);
}
}
fn select_last(&mut self, _: &menu::SelectLast, _window: &mut Window, cx: &mut Context<Self>) {
let count = self.matched_count();
if count > 0 {
self.set_selected_index(count - 1, cx);
}
}
fn set_selected_index(&mut self, index: usize, cx: &mut Context<Self>) {
self.selected_index = index;
self.scroll_handle
.scroll_to_item(index, ScrollStrategy::Top);
cx.notify();
}
fn render_scrollbar(&self, cx: &mut Context<Self>) -> Option<Stateful<Div>> {
if !(self.scrollbar_visibility || self.scrollbar_state.is_dragging()) {
return None;
}
Some(
div()
.occlude()
.id("thread-history-scroll")
.h_full()
.bg(cx.theme().colors().panel_background.opacity(0.8))
.border_l_1()
.border_color(cx.theme().colors().border_variant)
.absolute()
.right_1()
.top_0()
.bottom_0()
.w_4()
.pl_1()
.cursor_default()
.on_mouse_move(cx.listener(|_, _, _window, cx| {
cx.notify();
cx.stop_propagation()
}))
.on_hover(|_, _window, cx| {
cx.stop_propagation();
})
.on_any_mouse_down(|_, _window, cx| {
cx.stop_propagation();
})
.on_scroll_wheel(cx.listener(|_, _, _window, cx| {
cx.notify();
}))
.children(Scrollbar::vertical(self.scrollbar_state.clone())),
)
}
fn confirm(&mut self, _: &menu::Confirm, window: &mut Window, cx: &mut Context<Self>) {
if let Some(entry) = self.get_match(self.selected_index) {
let task_result = match entry {
HistoryEntry::Thread(thread) => self
.assistant_panel
.update(cx, move |this, cx| this.open_thread(&thread.id, window, cx)),
HistoryEntry::Context(context) => {
self.assistant_panel.update(cx, move |this, cx| {
this.open_saved_prompt_editor(context.path.clone(), window, cx)
})
}
};
if let Some(task) = task_result.log_err() {
task.detach_and_log_err(cx);
};
cx.notify();
}
}
fn remove_selected_thread(
&mut self,
_: &RemoveSelectedThread,
_window: &mut Window,
cx: &mut Context<Self>,
) {
if let Some(entry) = self.get_match(self.selected_index) {
let task_result = match entry {
HistoryEntry::Thread(thread) => self
.assistant_panel
.update(cx, |this, cx| this.delete_thread(&thread.id, cx)),
HistoryEntry::Context(context) => self
.assistant_panel
.update(cx, |this, cx| this.delete_context(context.path.clone(), cx)),
};
if let Some(task) = task_result.log_err() {
task.detach_and_log_err(cx);
};
cx.notify();
}
}
}
impl Focusable for ThreadHistory {
fn focus_handle(&self, cx: &App) -> FocusHandle {
self.search_editor.focus_handle(cx)
}
}
impl Render for ThreadHistory {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let selected_index = self.selected_index;
v_flex()
.key_context("ThreadHistory")
.size_full()
.on_action(cx.listener(Self::select_previous))
.on_action(cx.listener(Self::select_next))
.on_action(cx.listener(Self::select_first))
.on_action(cx.listener(Self::select_last))
.on_action(cx.listener(Self::confirm))
.on_action(cx.listener(Self::remove_selected_thread))
.when(!self.all_entries.is_empty(), |parent| {
parent.child(
h_flex()
.h(px(41.)) // Match the toolbar perfectly
.w_full()
.py_1()
.px_2()
.gap_2()
.justify_between()
.border_b_1()
.border_color(cx.theme().colors().border)
.child(
Icon::new(IconName::MagnifyingGlass)
.color(Color::Muted)
.size(IconSize::Small),
)
.child(self.search_editor.clone()),
)
})
.child({
let view = v_flex()
.id("list-container")
.relative()
.overflow_hidden()
.flex_grow();
if self.all_entries.is_empty() {
view.justify_center()
.child(
h_flex().w_full().justify_center().child(
Label::new("You don't have any past threads yet.")
.size(LabelSize::Small),
),
)
} else if self.has_search_query() && self.matches.is_empty() {
view.justify_center().child(
h_flex().w_full().justify_center().child(
Label::new("No threads match your search.").size(LabelSize::Small),
),
)
} else {
view.pr_5()
.child(
uniform_list(
cx.entity().clone(),
"thread-history",
self.matched_count(),
move |history, range, _window, _cx| {
let range_start = range.start;
let assistant_panel = history.assistant_panel.clone();
let render_item = |index: usize,
entry: &HistoryEntry,
highlight_positions: Vec<usize>|
-> Div {
h_flex().w_full().pb_1().child(match entry {
HistoryEntry::Thread(thread) => PastThread::new(
thread.clone(),
assistant_panel.clone(),
selected_index == index + range_start,
highlight_positions,
)
.into_any_element(),
HistoryEntry::Context(context) => PastContext::new(
context.clone(),
assistant_panel.clone(),
selected_index == index + range_start,
highlight_positions,
)
.into_any_element(),
})
};
if history.has_search_query() {
history.matches[range]
.iter()
.enumerate()
.filter_map(|(index, m)| {
history.all_entries.get(m.candidate_id).map(
|entry| {
render_item(
index,
entry,
m.positions.clone(),
)
},
)
})
.collect()
} else {
history.all_entries[range]
.iter()
.enumerate()
.map(|(index, entry)| render_item(index, entry, vec![]))
.collect()
}
},
)
.p_1()
.track_scroll(self.scroll_handle.clone())
.flex_grow(),
)
.when_some(self.render_scrollbar(cx), |div, scrollbar| {
div.child(scrollbar)
})
}
})
}
}
#[derive(IntoElement)]
pub struct PastThread {
thread: SerializedThreadMetadata,
assistant_panel: WeakEntity<AssistantPanel>,
selected: bool,
highlight_positions: Vec<usize>,
}
impl PastThread {
pub fn new(
thread: SerializedThreadMetadata,
assistant_panel: WeakEntity<AssistantPanel>,
selected: bool,
highlight_positions: Vec<usize>,
) -> Self {
Self {
thread,
assistant_panel,
selected,
highlight_positions,
}
}
}
impl RenderOnce for PastThread {
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
let summary = self.thread.summary;
let thread_timestamp = time_format::format_localized_timestamp(
OffsetDateTime::from_unix_timestamp(self.thread.updated_at.timestamp()).unwrap(),
OffsetDateTime::now_utc(),
self.assistant_panel
.update(cx, |this, _cx| this.local_timezone())
.unwrap_or(UtcOffset::UTC),
time_format::TimestampFormat::EnhancedAbsolute,
);
ListItem::new(SharedString::from(self.thread.id.to_string()))
.rounded()
.toggle_state(self.selected)
.spacing(ListItemSpacing::Sparse)
.start_slot(
div().max_w_4_5().child(
HighlightedLabel::new(summary, self.highlight_positions)
.size(LabelSize::Small)
.truncate(),
),
)
.end_slot(
h_flex()
.gap_1p5()
.child(
Label::new(thread_timestamp)
.color(Color::Muted)
.size(LabelSize::XSmall),
)
.child(
IconButton::new("delete", IconName::TrashAlt)
.shape(IconButtonShape::Square)
.icon_size(IconSize::XSmall)
.icon_color(Color::Muted)
.tooltip(move |window, cx| {
Tooltip::for_action("Delete", &RemoveSelectedThread, window, cx)
})
.on_click({
let assistant_panel = self.assistant_panel.clone();
let id = self.thread.id.clone();
move |_event, _window, cx| {
assistant_panel
.update(cx, |this, cx| {
this.delete_thread(&id, cx).detach_and_log_err(cx);
})
.ok();
}
}),
),
)
.on_click({
let assistant_panel = self.assistant_panel.clone();
let id = self.thread.id.clone();
move |_event, window, cx| {
assistant_panel
.update(cx, |this, cx| {
this.open_thread(&id, window, cx).detach_and_log_err(cx);
})
.ok();
}
})
}
}
#[derive(IntoElement)]
pub struct PastContext {
context: SavedContextMetadata,
assistant_panel: WeakEntity<AssistantPanel>,
selected: bool,
highlight_positions: Vec<usize>,
}
impl PastContext {
pub fn new(
context: SavedContextMetadata,
assistant_panel: WeakEntity<AssistantPanel>,
selected: bool,
highlight_positions: Vec<usize>,
) -> Self {
Self {
context,
assistant_panel,
selected,
highlight_positions,
}
}
}
impl RenderOnce for PastContext {
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
let summary = self.context.title;
let context_timestamp = time_format::format_localized_timestamp(
OffsetDateTime::from_unix_timestamp(self.context.mtime.timestamp()).unwrap(),
OffsetDateTime::now_utc(),
self.assistant_panel
.update(cx, |this, _cx| this.local_timezone())
.unwrap_or(UtcOffset::UTC),
time_format::TimestampFormat::EnhancedAbsolute,
);
ListItem::new(SharedString::from(
self.context.path.to_string_lossy().to_string(),
))
.rounded()
.toggle_state(self.selected)
.spacing(ListItemSpacing::Sparse)
.start_slot(
div().max_w_4_5().child(
HighlightedLabel::new(summary, self.highlight_positions)
.size(LabelSize::Small)
.truncate(),
),
)
.end_slot(
h_flex()
.gap_1p5()
.child(
Label::new(context_timestamp)
.color(Color::Muted)
.size(LabelSize::XSmall),
)
.child(
IconButton::new("delete", IconName::TrashAlt)
.shape(IconButtonShape::Square)
.icon_size(IconSize::XSmall)
.icon_color(Color::Muted)
.tooltip(move |window, cx| {
Tooltip::for_action("Delete", &RemoveSelectedThread, window, cx)
})
.on_click({
let assistant_panel = self.assistant_panel.clone();
let path = self.context.path.clone();
move |_event, _window, cx| {
assistant_panel
.update(cx, |this, cx| {
this.delete_context(path.clone(), cx)
.detach_and_log_err(cx);
})
.ok();
}
}),
),
)
.on_click({
let assistant_panel = self.assistant_panel.clone();
let path = self.context.path.clone();
move |_event, window, cx| {
assistant_panel
.update(cx, |this, cx| {
this.open_saved_prompt_editor(path.clone(), window, cx)
.detach_and_log_err(cx);
})
.ok();
}
})
}
}

View File

@@ -1,89 +0,0 @@
use std::sync::Arc;
use assistant_tool::{Tool, ToolWorkingSet, ToolWorkingSetEvent};
use collections::HashMap;
use gpui::{App, Context, Entity, IntoElement, Render, Subscription, Window};
use language_model::{LanguageModel, LanguageModelToolSchemaFormat};
use ui::prelude::*;
pub struct IncompatibleToolsState {
cache: HashMap<LanguageModelToolSchemaFormat, Vec<Arc<dyn Tool>>>,
tool_working_set: Entity<ToolWorkingSet>,
_tool_working_set_subscription: Subscription,
}
impl IncompatibleToolsState {
pub fn new(tool_working_set: Entity<ToolWorkingSet>, cx: &mut Context<Self>) -> Self {
let _tool_working_set_subscription =
cx.subscribe(&tool_working_set, |this, _, event, _| match event {
ToolWorkingSetEvent::EnabledToolsChanged => {
this.cache.clear();
}
});
Self {
cache: HashMap::default(),
tool_working_set,
_tool_working_set_subscription,
}
}
pub fn incompatible_tools(
&mut self,
model: &Arc<dyn LanguageModel>,
cx: &App,
) -> &[Arc<dyn Tool>] {
self.cache
.entry(model.tool_input_format())
.or_insert_with(|| {
self.tool_working_set
.read(cx)
.enabled_tools(cx)
.iter()
.filter(|tool| tool.input_schema(model.tool_input_format()).is_err())
.cloned()
.collect()
})
}
}
pub struct IncompatibleToolsTooltip {
pub incompatible_tools: Vec<Arc<dyn Tool>>,
}
impl Render for IncompatibleToolsTooltip {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
ui::tooltip_container(window, cx, |container, _, cx| {
container
.w_72()
.child(Label::new("Incompatible Tools").size(LabelSize::Small))
.child(
Label::new(
"This model is incompatible with the following tools from your MCPs:",
)
.size(LabelSize::Small)
.color(Color::Muted),
)
.child(
v_flex()
.my_1p5()
.py_0p5()
.border_b_1()
.border_color(cx.theme().colors().border_variant)
.children(
self.incompatible_tools
.iter()
.map(|tool| Label::new(tool.name()).size(LabelSize::Small).buffer_font(cx)),
),
)
.child(Label::new("What To Do Instead").size(LabelSize::Small))
.child(
Label::new(
"Every other tool continues to work with this model, but to specifically use those, switch to another model.",
)
.size(LabelSize::Small)
.color(Color::Muted),
)
})
}
}

View File

@@ -1,7 +0,0 @@
mod agent_notification;
mod context_pill;
mod user_spending;
pub use agent_notification::*;
pub use context_pill::*;
// pub use user_spending::*;

View File

@@ -1,186 +0,0 @@
use gpui::{Entity, Render};
use ui::{ProgressBar, prelude::*};
#[derive(RegisterComponent)]
pub struct UserSpending {
free_tier_current: u32,
free_tier_cap: u32,
over_tier_current: u32,
over_tier_cap: u32,
free_tier_progress: Entity<ProgressBar>,
over_tier_progress: Entity<ProgressBar>,
}
impl UserSpending {
pub fn new(
free_tier_current: u32,
free_tier_cap: u32,
over_tier_current: u32,
over_tier_cap: u32,
cx: &mut App,
) -> Self {
let free_tier_capped = free_tier_current == free_tier_cap;
let free_tier_near_capped =
free_tier_current as f32 / 100.0 >= free_tier_cap as f32 / 100.0 * 0.9;
let over_tier_capped = over_tier_current == over_tier_cap;
let over_tier_near_capped =
over_tier_current as f32 / 100.0 >= over_tier_cap as f32 / 100.0 * 0.9;
let free_tier_progress = cx.new(|cx| {
ProgressBar::new(
"free_tier",
free_tier_current as f32,
free_tier_cap as f32,
cx,
)
});
let over_tier_progress = cx.new(|cx| {
ProgressBar::new(
"over_tier",
over_tier_current as f32,
over_tier_cap as f32,
cx,
)
});
if free_tier_capped {
free_tier_progress.update(cx, |progress_bar, cx| {
progress_bar.fg_color(cx.theme().status().error);
});
} else if free_tier_near_capped {
free_tier_progress.update(cx, |progress_bar, cx| {
progress_bar.fg_color(cx.theme().status().warning);
});
}
if over_tier_capped {
over_tier_progress.update(cx, |progress_bar, cx| {
progress_bar.fg_color(cx.theme().status().error);
});
} else if over_tier_near_capped {
over_tier_progress.update(cx, |progress_bar, cx| {
progress_bar.fg_color(cx.theme().status().warning);
});
}
Self {
free_tier_current,
free_tier_cap,
over_tier_current,
over_tier_cap,
free_tier_progress,
over_tier_progress,
}
}
}
impl Render for UserSpending {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let formatted_free_tier = format!(
"${} / ${}",
self.free_tier_current as f32 / 100.0,
self.free_tier_cap as f32 / 100.0
);
let formatted_over_tier = format!(
"${} / ${}",
self.over_tier_current as f32 / 100.0,
self.over_tier_cap as f32 / 100.0
);
v_group()
.elevation_2(cx)
.py_1p5()
.px_2p5()
.w(px(360.))
.child(
v_flex()
.child(
v_flex()
.p_1p5()
.gap_0p5()
.child(
h_flex()
.justify_between()
.child(Label::new("Free Tier Usage").size(LabelSize::Small))
.child(
Label::new(formatted_free_tier)
.size(LabelSize::Small)
.color(Color::Muted),
),
)
.child(self.free_tier_progress.clone()),
)
.child(
v_flex()
.p_1p5()
.gap_0p5()
.child(
h_flex()
.justify_between()
.child(Label::new("Current Spending").size(LabelSize::Small))
.child(
Label::new(formatted_over_tier)
.size(LabelSize::Small)
.color(Color::Muted),
),
)
.child(self.over_tier_progress.clone()),
),
)
}
}
impl Component for UserSpending {
fn scope() -> ComponentScope {
ComponentScope::None
}
fn preview(_window: &mut Window, cx: &mut App) -> Option<AnyElement> {
let new_user = cx.new(|cx| UserSpending::new(0, 2000, 0, 2000, cx));
let free_capped = cx.new(|cx| UserSpending::new(2000, 2000, 0, 2000, cx));
let free_near_capped = cx.new(|cx| UserSpending::new(1800, 2000, 0, 2000, cx));
let over_near_capped = cx.new(|cx| UserSpending::new(2000, 2000, 1800, 2000, cx));
let over_capped = cx.new(|cx| UserSpending::new(1000, 2000, 2000, 2000, cx));
Some(
v_flex()
.gap_6()
.p_4()
.children(vec![example_group(vec![
single_example(
"New User",
div().size_full().child(new_user.clone()).into_any_element(),
),
single_example(
"Free Tier Capped",
div()
.size_full()
.child(free_capped.clone())
.into_any_element(),
),
single_example(
"Free Tier Near Capped",
div()
.size_full()
.child(free_near_capped.clone())
.into_any_element(),
),
single_example(
"Over Tier Near Capped",
div()
.size_full()
.child(over_near_capped.clone())
.into_any_element(),
),
single_example(
"Over Tier Capped",
div()
.size_full()
.child(over_capped.clone())
.into_any_element(),
),
])])
.into_any_element(),
)
}
}

View File

@@ -25,4 +25,4 @@ serde.workspace = true
serde_json.workspace = true
strum.workspace = true
thiserror.workspace = true
workspace-hack.workspace = true
util.workspace = true

View File

@@ -1,15 +1,16 @@
mod supported_countries;
use std::str::FromStr;
use std::{pin::Pin, str::FromStr};
use anyhow::{Context as _, Result, anyhow};
use anyhow::{anyhow, Context as _, Result};
use chrono::{DateTime, Utc};
use futures::{AsyncBufReadExt, AsyncReadExt, StreamExt, io::BufReader, stream::BoxStream};
use futures::{io::BufReader, stream::BoxStream, AsyncBufReadExt, AsyncReadExt, Stream, StreamExt};
use http_client::http::{HeaderMap, HeaderValue};
use http_client::{AsyncBody, HttpClient, Method, Request as HttpRequest};
use serde::{Deserialize, Serialize};
use strum::{EnumIter, EnumString};
use thiserror::Error;
use util::ResultExt as _;
pub use supported_countries::*;
@@ -36,9 +37,9 @@ pub enum AnthropicModelMode {
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, EnumIter)]
pub enum Model {
#[default]
#[serde(rename = "claude-3-5-sonnet", alias = "claude-3-5-sonnet-latest")]
Claude3_5Sonnet,
#[default]
#[serde(rename = "claude-3-7-sonnet", alias = "claude-3-7-sonnet-latest")]
Claude3_7Sonnet,
#[serde(
@@ -219,23 +220,16 @@ impl Model {
.map(|header| header.to_string())
.collect::<Vec<_>>();
match self {
Self::Claude3_7Sonnet | Self::Claude3_7SonnetThinking => {
// Try beta token-efficient tool use (supported in Claude 3.7 Sonnet only)
// https://docs.anthropic.com/en/docs/build-with-claude/tool-use/token-efficient-tool-use
headers.push("token-efficient-tools-2025-02-19".to_string());
}
Self::Custom {
extra_beta_headers, ..
} => {
headers.extend(
extra_beta_headers
.iter()
.filter(|header| !header.trim().is_empty())
.cloned(),
);
}
_ => {}
if let Self::Custom {
extra_beta_headers, ..
} = self
{
headers.extend(
extra_beta_headers
.iter()
.filter(|header| !header.trim().is_empty())
.cloned(),
);
}
headers.join(",")
@@ -320,68 +314,38 @@ pub async fn stream_completion(
.map(|output| output.0)
}
/// An individual rate limit.
#[derive(Debug)]
pub struct RateLimit {
pub limit: usize,
pub remaining: usize,
pub reset: DateTime<Utc>,
}
impl RateLimit {
fn from_headers(resource: &str, headers: &HeaderMap<HeaderValue>) -> Result<Self> {
let limit =
get_header(&format!("anthropic-ratelimit-{resource}-limit"), headers)?.parse()?;
let remaining = get_header(
&format!("anthropic-ratelimit-{resource}-remaining"),
headers,
)?
.parse()?;
let reset = DateTime::parse_from_rfc3339(get_header(
&format!("anthropic-ratelimit-{resource}-reset"),
headers,
)?)?
.to_utc();
Ok(Self {
limit,
remaining,
reset,
})
}
}
/// <https://docs.anthropic.com/en/api/rate-limits#response-headers>
#[derive(Debug)]
pub struct RateLimitInfo {
pub requests: Option<RateLimit>,
pub tokens: Option<RateLimit>,
pub input_tokens: Option<RateLimit>,
pub output_tokens: Option<RateLimit>,
pub requests_limit: usize,
pub requests_remaining: usize,
pub requests_reset: DateTime<Utc>,
pub tokens_limit: usize,
pub tokens_remaining: usize,
pub tokens_reset: DateTime<Utc>,
}
impl RateLimitInfo {
fn from_headers(headers: &HeaderMap<HeaderValue>) -> Self {
// Check if any rate limit headers exist
let has_rate_limit_headers = headers
.keys()
.any(|k| k.as_str().starts_with("anthropic-ratelimit-"));
fn from_headers(headers: &HeaderMap<HeaderValue>) -> Result<Self> {
let tokens_limit = get_header("anthropic-ratelimit-tokens-limit", headers)?.parse()?;
let requests_limit = get_header("anthropic-ratelimit-requests-limit", headers)?.parse()?;
let tokens_remaining =
get_header("anthropic-ratelimit-tokens-remaining", headers)?.parse()?;
let requests_remaining =
get_header("anthropic-ratelimit-requests-remaining", headers)?.parse()?;
let requests_reset = get_header("anthropic-ratelimit-requests-reset", headers)?;
let tokens_reset = get_header("anthropic-ratelimit-tokens-reset", headers)?;
let requests_reset = DateTime::parse_from_rfc3339(requests_reset)?.to_utc();
let tokens_reset = DateTime::parse_from_rfc3339(tokens_reset)?.to_utc();
if !has_rate_limit_headers {
return Self {
requests: None,
tokens: None,
input_tokens: None,
output_tokens: None,
};
}
Self {
requests: RateLimit::from_headers("requests", headers).ok(),
tokens: RateLimit::from_headers("tokens", headers).ok(),
input_tokens: RateLimit::from_headers("input-tokens", headers).ok(),
output_tokens: RateLimit::from_headers("output-tokens", headers).ok(),
}
Ok(Self {
requests_limit,
tokens_limit,
requests_remaining,
tokens_remaining,
requests_reset,
tokens_reset,
})
}
}
@@ -447,7 +411,7 @@ pub async fn stream_completion_with_rate_limit_info(
}
})
.boxed();
Ok((stream, Some(rate_limits)))
Ok((stream, rate_limits.log_err()))
} else {
let mut body = Vec::new();
response
@@ -473,6 +437,50 @@ pub async fn stream_completion_with_rate_limit_info(
}
}
pub async fn extract_tool_args_from_events(
tool_name: String,
mut events: Pin<Box<dyn Send + Stream<Item = Result<Event>>>>,
) -> Result<impl Send + Stream<Item = Result<String>>> {
let mut tool_use_index = None;
while let Some(event) = events.next().await {
if let Event::ContentBlockStart {
index,
content_block: ResponseContent::ToolUse { name, .. },
} = event?
{
if name == tool_name {
tool_use_index = Some(index);
break;
}
}
}
let Some(tool_use_index) = tool_use_index else {
return Err(anyhow!("tool not used"));
};
Ok(events.filter_map(move |event| {
let result = match event {
Err(error) => Some(Err(error)),
Ok(Event::ContentBlockDelta { index, delta }) => match delta {
ContentDelta::TextDelta { .. } => None,
ContentDelta::ThinkingDelta { .. } => None,
ContentDelta::SignatureDelta { .. } => None,
ContentDelta::InputJsonDelta { partial_json } => {
if index == tool_use_index {
Some(Ok(partial_json))
} else {
None
}
}
},
_ => None,
};
async move { result }
}))
}
#[derive(Debug, Serialize, Deserialize, Copy, Clone)]
#[serde(rename_all = "lowercase")]
pub enum CacheControlType {
@@ -737,54 +745,4 @@ impl ApiError {
pub fn is_rate_limit_error(&self) -> bool {
matches!(self.error_type.as_str(), "rate_limit_error")
}
pub fn match_window_exceeded(&self) -> Option<usize> {
let Some(ApiErrorCode::InvalidRequestError) = self.code() else {
return None;
};
parse_prompt_too_long(&self.message)
}
}
pub fn parse_prompt_too_long(message: &str) -> Option<usize> {
message
.strip_prefix("prompt is too long: ")?
.split_once(" tokens")?
.0
.parse::<usize>()
.ok()
}
#[test]
fn test_match_window_exceeded() {
let error = ApiError {
error_type: "invalid_request_error".to_string(),
message: "prompt is too long: 220000 tokens > 200000".to_string(),
};
assert_eq!(error.match_window_exceeded(), Some(220_000));
let error = ApiError {
error_type: "invalid_request_error".to_string(),
message: "prompt is too long: 1234953 tokens".to_string(),
};
assert_eq!(error.match_window_exceeded(), Some(1234953));
let error = ApiError {
error_type: "invalid_request_error".to_string(),
message: "not a prompt length error".to_string(),
};
assert_eq!(error.match_window_exceeded(), None);
let error = ApiError {
error_type: "rate_limit_error".to_string(),
message: "prompt is too long: 12345 tokens".to_string(),
};
assert_eq!(error.match_window_exceeded(), None);
let error = ApiError {
error_type: "invalid_request_error".to_string(),
message: "prompt is too long: invalid tokens".to_string(),
};
assert_eq!(error.match_window_exceeded(), None);
}

View File

@@ -19,4 +19,3 @@ smol.workspace = true
tempfile.workspace = true
util.workspace = true
which.workspace = true
workspace-hack.workspace = true

View File

@@ -5,9 +5,9 @@ use std::time::Duration;
use anyhow::Context as _;
use futures::channel::{mpsc, oneshot};
#[cfg(unix)]
use futures::{AsyncBufReadExt as _, io::BufReader};
use futures::{io::BufReader, AsyncBufReadExt as _};
#[cfg(unix)]
use futures::{AsyncWriteExt as _, FutureExt as _, select_biased};
use futures::{select_biased, AsyncWriteExt as _, FutureExt as _};
use futures::{SinkExt, StreamExt};
use gpui::{AsyncApp, BackgroundExecutor, Task};
#[cfg(unix)]

View File

@@ -15,4 +15,3 @@ workspace = true
anyhow.workspace = true
gpui.workspace = true
rust-embed.workspace = true
workspace-hack.workspace = true

View File

@@ -57,11 +57,10 @@ impl Assets {
pub fn load_test_fonts(&self, cx: &App) {
cx.text_system()
.add_fonts(vec![
self.load("fonts/plex-mono/ZedPlexMono-Regular.ttf")
.unwrap()
.unwrap(),
])
.add_fonts(vec![self
.load("fonts/plex-mono/ZedPlexMono-Regular.ttf")
.unwrap()
.unwrap()])
.unwrap()
}
}

View File

@@ -69,7 +69,6 @@ ui.workspace = true
util.workspace = true
workspace.workspace = true
zed_actions.workspace = true
workspace-hack.workspace = true
[dev-dependencies]
ctor.workspace = true

View File

@@ -14,7 +14,7 @@ use client::Client;
use command_palette_hooks::CommandPaletteFilter;
use feature_flags::FeatureFlagAppExt;
use fs::Fs;
use gpui::{App, Global, ReadGlobal, UpdateGlobal, actions};
use gpui::{actions, App, Global, ReadGlobal, UpdateGlobal};
use language_model::{
LanguageModelId, LanguageModelProviderId, LanguageModelRegistry, LanguageModelResponseMessage,
};
@@ -161,38 +161,12 @@ fn init_language_model_settings(cx: &mut App) {
fn update_active_language_model_from_settings(cx: &mut App) {
let settings = AssistantSettings::get_global(cx);
// Default model - used as fallback
let active_model_provider_name =
LanguageModelProviderId::from(settings.default_model.provider.clone());
let active_model_id = LanguageModelId::from(settings.default_model.model.clone());
// Inline assistant model
let inline_assistant_model = settings
.inline_assistant_model
.as_ref()
.unwrap_or(&settings.default_model);
let inline_assistant_provider_name =
LanguageModelProviderId::from(inline_assistant_model.provider.clone());
let inline_assistant_model_id = LanguageModelId::from(inline_assistant_model.model.clone());
// Commit message model
let commit_message_model = settings
.commit_message_model
.as_ref()
.unwrap_or(&settings.default_model);
let commit_message_provider_name =
LanguageModelProviderId::from(commit_message_model.provider.clone());
let commit_message_model_id = LanguageModelId::from(commit_message_model.model.clone());
// Thread summary model
let thread_summary_model = settings
.thread_summary_model
.as_ref()
.unwrap_or(&settings.default_model);
let thread_summary_provider_name =
LanguageModelProviderId::from(thread_summary_model.provider.clone());
let thread_summary_model_id = LanguageModelId::from(thread_summary_model.model.clone());
let editor_provider_name =
LanguageModelProviderId::from(settings.editor_model.provider.clone());
let editor_model_id = LanguageModelId::from(settings.editor_model.model.clone());
let inline_alternatives = settings
.inline_alternatives
.iter()
@@ -203,29 +177,9 @@ fn update_active_language_model_from_settings(cx: &mut App) {
)
})
.collect::<Vec<_>>();
LanguageModelRegistry::global(cx).update(cx, |registry, cx| {
// Set the default model
registry.select_default_model(&active_model_provider_name, &active_model_id, cx);
// Set the specific models
registry.select_inline_assistant_model(
&inline_assistant_provider_name,
&inline_assistant_model_id,
cx,
);
registry.select_commit_message_model(
&commit_message_provider_name,
&commit_message_model_id,
cx,
);
registry.select_thread_summary_model(
&thread_summary_provider_name,
&thread_summary_model_id,
cx,
);
// Set the alternatives
registry.select_active_model(&active_model_provider_name, &active_model_id, cx);
registry.select_editor_model(&editor_provider_name, &editor_model_id, cx);
registry.select_inline_alternative_models(inline_alternatives, cx);
});
}

View File

@@ -1,9 +1,9 @@
use std::sync::Arc;
use collections::HashMap;
use gpui::{AnyView, App, EventEmitter, FocusHandle, Focusable, Subscription, canvas};
use gpui::{canvas, AnyView, App, EventEmitter, FocusHandle, Focusable, Subscription};
use language_model::{LanguageModelProvider, LanguageModelProviderId, LanguageModelRegistry};
use ui::{ElevationIndex, prelude::*};
use ui::{prelude::*, ElevationIndex};
use workspace::Item;
pub struct ConfigurationView {

View File

@@ -1,47 +1,45 @@
use crate::Assistant;
use crate::assistant_configuration::{ConfigurationView, ConfigurationViewEvent};
use crate::Assistant;
use crate::{
DeployHistory, InlineAssistant, NewChat, terminal_inline_assistant::TerminalInlineAssistant,
terminal_inline_assistant::TerminalInlineAssistant, DeployHistory, InlineAssistant, NewChat,
};
use anyhow::{Result, anyhow};
use anyhow::{anyhow, Result};
use assistant_context_editor::{
AssistantContext, AssistantPanelDelegate, ContextEditor, ContextEditorToolbarItem,
ContextEditorToolbarItemEvent, ContextHistory, ContextId, ContextStore, ContextStoreEvent,
DEFAULT_TAB_TITLE, InsertDraggedFiles, SlashCommandCompletionProvider,
make_lsp_adapter_delegate,
make_lsp_adapter_delegate, AssistantContext, AssistantPanelDelegate, ContextEditor,
ContextEditorToolbarItem, ContextEditorToolbarItemEvent, ContextHistory, ContextId,
ContextStore, ContextStoreEvent, InsertDraggedFiles, SlashCommandCompletionProvider,
DEFAULT_TAB_TITLE,
};
use assistant_settings::{AssistantDockPosition, AssistantSettings};
use assistant_slash_command::SlashCommandWorkingSet;
use client::{Client, Status, proto};
use client::{proto, Client, Status};
use editor::{Editor, EditorEvent};
use fs::Fs;
use gpui::{
Action, App, AsyncWindowContext, Entity, EventEmitter, ExternalPaths, FocusHandle, Focusable,
InteractiveElement, IntoElement, ParentElement, Pixels, Render, Styled, Subscription, Task,
UpdateGlobal, WeakEntity, prelude::*,
prelude::*, Action, App, AsyncWindowContext, Entity, EventEmitter, ExternalPaths, FocusHandle,
Focusable, InteractiveElement, IntoElement, ParentElement, Pixels, Render, Styled,
Subscription, Task, UpdateGlobal, WeakEntity,
};
use language::LanguageRegistry;
use language_model::{
AuthenticateError, ConfiguredModel, LanguageModelProviderId, LanguageModelRegistry,
ZED_CLOUD_PROVIDER_ID,
AuthenticateError, LanguageModelProviderId, LanguageModelRegistry, ZED_CLOUD_PROVIDER_ID,
};
use project::Project;
use prompt_library::{PromptLibrary, open_prompt_library};
use prompt_library::{open_prompt_library, PromptLibrary};
use prompt_store::PromptBuilder;
use search::{BufferSearchBar, buffer_search::DivRegistrar};
use settings::{Settings, update_settings_file};
use search::{buffer_search::DivRegistrar, BufferSearchBar};
use settings::{update_settings_file, Settings};
use smol::stream::StreamExt;
use std::{ops::ControlFlow, path::PathBuf, sync::Arc};
use terminal_view::{TerminalView, terminal_panel::TerminalPanel};
use ui::{ContextMenu, PopoverMenu, Tooltip, prelude::*};
use util::{ResultExt, maybe};
use terminal_view::{terminal_panel::TerminalPanel, TerminalView};
use ui::{prelude::*, ContextMenu, PopoverMenu, Tooltip};
use util::{maybe, ResultExt};
use workspace::DraggedTab;
use workspace::{
DraggedSelection, Pane, ToggleZoom, Workspace,
dock::{DockPosition, Panel, PanelEvent},
pane,
pane, DraggedSelection, Pane, ShowConfiguration, ToggleZoom, Workspace,
};
use zed_actions::assistant::{InlineAssist, OpenPromptLibrary, ShowConfiguration, ToggleFocus};
use zed_actions::assistant::{InlineAssist, OpenPromptLibrary, ToggleFocus};
pub fn init(cx: &mut App) {
workspace::FollowableViewRegistry::register::<ContextEditor>(cx);
@@ -54,15 +52,7 @@ pub fn init(cx: &mut App) {
.register_action(ContextEditor::insert_dragged_files)
.register_action(AssistantPanel::show_configuration)
.register_action(AssistantPanel::create_new_context)
.register_action(AssistantPanel::restart_context_servers)
.register_action(|workspace, _: &OpenPromptLibrary, window, cx| {
if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
workspace.focus_panel::<AssistantPanel>(window, cx);
panel.update(cx, |panel, cx| {
panel.deploy_prompt_library(&OpenPromptLibrary, window, cx)
});
}
});
.register_action(AssistantPanel::restart_context_servers);
},
)
.detach();
@@ -307,10 +297,8 @@ impl AssistantPanel {
&LanguageModelRegistry::global(cx),
window,
|this, _, event: &language_model::Event, window, cx| match event {
language_model::Event::DefaultModelChanged
| language_model::Event::InlineAssistantModelChanged
| language_model::Event::CommitMessageModelChanged
| language_model::Event::ThreadSummaryModelChanged => {
language_model::Event::ActiveModelChanged
| language_model::Event::EditorModelChanged => {
this.completion_provider_changed(window, cx);
}
language_model::Event::ProviderStateChanged => {
@@ -479,12 +467,12 @@ impl AssistantPanel {
}
fn update_zed_ai_notice_visibility(&mut self, client_status: Status, cx: &mut Context<Self>) {
let model = LanguageModelRegistry::read_global(cx).default_model();
let active_provider = LanguageModelRegistry::read_global(cx).active_provider();
// If we're signed out and don't have a provider configured, or we're signed-out AND Zed.dev is
// the provider, we want to show a nudge to sign in.
let show_zed_ai_notice = client_status.is_signed_out()
&& model.map_or(true, |model| model.provider.id().0 == ZED_CLOUD_PROVIDER_ID);
&& active_provider.map_or(true, |provider| provider.id().0 == ZED_CLOUD_PROVIDER_ID);
self.show_zed_ai_notice = show_zed_ai_notice;
cx.notify();
@@ -552,8 +540,8 @@ impl AssistantPanel {
}
let Some(new_provider_id) = LanguageModelRegistry::read_global(cx)
.default_model()
.map(|default| default.provider.id())
.active_provider()
.map(|p| p.id())
else {
return;
};
@@ -579,9 +567,7 @@ impl AssistantPanel {
return;
}
let Some(ConfiguredModel { provider, .. }) =
LanguageModelRegistry::read_global(cx).default_model()
else {
let Some(provider) = LanguageModelRegistry::read_global(cx).active_provider() else {
return;
};
@@ -989,8 +975,8 @@ impl AssistantPanel {
|this, _, event: &ConfigurationViewEvent, window, cx| match event {
ConfigurationViewEvent::NewProviderContextEditor(provider) => {
if LanguageModelRegistry::read_global(cx)
.default_model()
.map_or(true, |default| default.provider.id() != provider.id())
.active_provider()
.map_or(true, |p| p.id() != provider.id())
{
if let Some(model) = provider.default_model(cx) {
update_settings_file::<AssistantSettings>(
@@ -1168,8 +1154,8 @@ impl AssistantPanel {
fn is_authenticated(&mut self, cx: &mut Context<Self>) -> bool {
LanguageModelRegistry::read_global(cx)
.default_model()
.map_or(false, |default| default.provider.is_authenticated(cx))
.active_provider()
.map_or(false, |provider| provider.is_authenticated(cx))
}
fn authenticate(
@@ -1177,8 +1163,8 @@ impl AssistantPanel {
cx: &mut Context<Self>,
) -> Option<Task<Result<(), AuthenticateError>>> {
LanguageModelRegistry::read_global(cx)
.default_model()
.map_or(None, |default| Some(default.provider.authenticate(cx)))
.active_provider()
.map_or(None, |provider| Some(provider.authenticate(cx)))
}
fn restart_context_servers(

View File

@@ -2,40 +2,39 @@ use crate::{
Assistant, AssistantPanel, AssistantPanelEvent, CycleNextInlineAssist,
CyclePreviousInlineAssist,
};
use anyhow::{Context as _, Result, anyhow};
use assistant_context_editor::{RequestType, humanize_token_count};
use anyhow::{anyhow, Context as _, Result};
use assistant_context_editor::{humanize_token_count, RequestType};
use assistant_settings::AssistantSettings;
use client::{ErrorExt, telemetry::Telemetry};
use collections::{HashMap, HashSet, VecDeque, hash_map};
use client::{telemetry::Telemetry, ErrorExt};
use collections::{hash_map, HashMap, HashSet, VecDeque};
use editor::{
Anchor, AnchorRangeExt, CodeActionProvider, Editor, EditorElement, EditorEvent, EditorMode,
EditorStyle, ExcerptId, ExcerptRange, GutterDimensions, MultiBuffer, MultiBufferSnapshot,
ToOffset as _, ToPoint,
actions::{MoveDown, MoveUp, SelectAll},
display_map::{
BlockContext, BlockPlacement, BlockProperties, BlockStyle, CustomBlockId, RenderBlock,
ToDisplayPoint,
},
Anchor, AnchorRangeExt, CodeActionProvider, Editor, EditorElement, EditorEvent, EditorMode,
EditorStyle, ExcerptId, ExcerptRange, GutterDimensions, MultiBuffer, MultiBufferSnapshot,
ToOffset as _, ToPoint,
};
use feature_flags::{
Assistant2FeatureFlag, FeatureFlagAppExt as _, FeatureFlagViewExt as _, ZedPro,
};
use fs::Fs;
use futures::{
SinkExt, Stream, StreamExt,
channel::mpsc,
future::{BoxFuture, LocalBoxFuture},
join,
join, SinkExt, Stream, StreamExt,
};
use gpui::{
AnyElement, App, ClickEvent, Context, CursorStyle, Entity, EventEmitter, FocusHandle,
Focusable, FontWeight, Global, HighlightStyle, Subscription, Task, TextStyle, UpdateGlobal,
WeakEntity, Window, anchored, deferred, point,
anchored, deferred, point, AnyElement, App, ClickEvent, Context, CursorStyle, Entity,
EventEmitter, FocusHandle, Focusable, FontWeight, Global, HighlightStyle, Subscription, Task,
TextStyle, UpdateGlobal, WeakEntity, Window,
};
use language::{Buffer, IndentKind, Point, Selection, TransactionId, line_diff};
use language::{line_diff, Buffer, IndentKind, Point, Selection, TransactionId};
use language_model::{
ConfiguredModel, LanguageModel, LanguageModelRegistry, LanguageModelRequest,
LanguageModelRequestMessage, LanguageModelTextStream, Role, report_assistant_event,
report_assistant_event, LanguageModel, LanguageModelRegistry, LanguageModelRequest,
LanguageModelRequestMessage, LanguageModelTextStream, Role,
};
use language_model_selector::{LanguageModelSelector, LanguageModelSelectorPopoverMenu};
use multi_buffer::MultiBufferRow;
@@ -43,7 +42,7 @@ use parking_lot::Mutex;
use project::{CodeAction, LspAction, ProjectTransaction};
use prompt_store::PromptBuilder;
use rope::Rope;
use settings::{Settings, SettingsStore, update_settings_file};
use settings::{update_settings_file, Settings, SettingsStore};
use smol::future::FutureExt;
use std::{
cmp,
@@ -57,15 +56,15 @@ use std::{
time::{Duration, Instant},
};
use streaming_diff::{CharOperation, LineDiff, LineOperation, StreamingDiff};
use telemetry_events::{AssistantEventData, AssistantKind, AssistantPhase};
use telemetry_events::{AssistantEvent, AssistantKind, AssistantPhase};
use terminal_view::terminal_panel::TerminalPanel;
use text::{OffsetRangeExt, ToPoint as _};
use theme::ThemeSettings;
use ui::{
CheckboxWithLabel, IconButtonShape, KeyBinding, Popover, Tooltip, prelude::*, text_for_action,
prelude::*, text_for_action, CheckboxWithLabel, IconButtonShape, KeyBinding, Popover, Tooltip,
};
use util::{RangeExt, ResultExt};
use workspace::{ItemHandle, Toast, Workspace, notifications::NotificationId};
use workspace::{notifications::NotificationId, ItemHandle, Toast, Workspace};
pub fn init(
fs: Arc<dyn Fs>,
@@ -312,10 +311,8 @@ impl InlineAssistant {
start..end,
));
if let Some(ConfiguredModel { model, .. }) =
LanguageModelRegistry::read_global(cx).default_model()
{
self.telemetry.report_assistant_event(AssistantEventData {
if let Some(model) = LanguageModelRegistry::read_global(cx).active_model() {
self.telemetry.report_assistant_event(AssistantEvent {
conversation_id: None,
kind: AssistantKind::Inline,
phase: AssistantPhase::Invoked,
@@ -527,14 +524,14 @@ impl InlineAssistant {
BlockProperties {
style: BlockStyle::Sticky,
placement: BlockPlacement::Above(range.start),
height: Some(prompt_editor_height),
height: prompt_editor_height,
render: build_assist_editor_renderer(prompt_editor),
priority: 0,
},
BlockProperties {
style: BlockStyle::Sticky,
placement: BlockPlacement::Below(range.end),
height: None,
height: 0,
render: Arc::new(|cx| {
v_flex()
.h_full()
@@ -879,9 +876,7 @@ impl InlineAssistant {
let active_alternative = assist.codegen.read(cx).active_alternative().clone();
let message_id = active_alternative.read(cx).message_id.clone();
if let Some(ConfiguredModel { model, .. }) =
LanguageModelRegistry::read_global(cx).default_model()
{
if let Some(model) = LanguageModelRegistry::read_global(cx).active_model() {
let language_name = assist.editor.upgrade().and_then(|editor| {
let multibuffer = editor.read(cx).buffer().read(cx);
let multibuffer_snapshot = multibuffer.snapshot(cx);
@@ -892,7 +887,7 @@ impl InlineAssistant {
.map(|language| language.name())
});
report_assistant_event(
AssistantEventData {
AssistantEvent {
conversation_id: None,
kind: AssistantKind::Inline,
message_id,
@@ -1274,7 +1269,10 @@ impl InlineAssistant {
multi_buffer.update(cx, |multi_buffer, cx| {
multi_buffer.push_excerpts(
old_buffer.clone(),
Some(ExcerptRange::new(buffer_start..buffer_end)),
Some(ExcerptRange {
context: buffer_start..buffer_end,
primary: None,
}),
cx,
);
});
@@ -1301,7 +1299,7 @@ impl InlineAssistant {
deleted_lines_editor.update(cx, |editor, cx| editor.max_point(cx).row().0 + 1);
new_blocks.push(BlockProperties {
placement: BlockPlacement::Above(new_row),
height: Some(height),
height,
style: BlockStyle::Flex,
render: Arc::new(move |cx| {
div()
@@ -1633,8 +1631,8 @@ impl Render for PromptEditor {
format!(
"Using {}",
LanguageModelRegistry::read_global(cx)
.default_model()
.map(|default| default.model.name().0)
.active_model()
.map(|model| model.name().0)
.unwrap_or_else(|| "No model selected".into()),
),
None,
@@ -2081,7 +2079,7 @@ impl PromptEditor {
let disabled = matches!(codegen.status(cx), CodegenStatus::Idle);
let model_registry = LanguageModelRegistry::read_global(cx);
let default_model = model_registry.default_model().map(|default| default.model);
let default_model = model_registry.active_model();
let alternative_models = model_registry.inline_alternative_models();
let get_model_name = |index: usize| -> String {
@@ -2187,9 +2185,7 @@ impl PromptEditor {
}
fn render_token_count(&self, cx: &mut Context<Self>) -> Option<impl IntoElement> {
let model = LanguageModelRegistry::read_global(cx)
.default_model()?
.model;
let model = LanguageModelRegistry::read_global(cx).active_model()?;
let token_counts = self.token_counts?;
let max_token_count = model.max_token_count();
@@ -2644,9 +2640,8 @@ impl Codegen {
}
let primary_model = LanguageModelRegistry::read_global(cx)
.default_model()
.context("no active model")?
.model;
.active_model()
.context("no active model")?;
for (model, alternative) in iter::once(primary_model)
.chain(alternative_models)
@@ -2870,9 +2865,7 @@ impl CodegenAlternative {
assistant_panel_context: Option<LanguageModelRequest>,
cx: &App,
) -> BoxFuture<'static, Result<TokenCounts>> {
if let Some(ConfiguredModel { model, .. }) =
LanguageModelRegistry::read_global(cx).inline_assistant_model()
{
if let Some(model) = LanguageModelRegistry::read_global(cx).active_model() {
let request = self.build_request(user_prompt, assistant_panel_context.clone(), cx);
match request {
Ok(request) => {
@@ -3148,7 +3141,7 @@ impl CodegenAlternative {
let error_message = result.as_ref().err().map(|error| error.to_string());
report_assistant_event(
AssistantEventData {
AssistantEvent {
conversation_id: None,
message_id,
kind: AssistantKind::Inline,
@@ -3717,8 +3710,8 @@ mod tests {
use gpui::TestAppContext;
use indoc::indoc;
use language::{
Buffer, Language, LanguageConfig, LanguageMatcher, Point, language_settings,
tree_sitter_rust,
language_settings, tree_sitter_rust, Buffer, Language, LanguageConfig, LanguageMatcher,
Point,
};
use language_model::{LanguageModelRegistry, TokenUsage};
use rand::prelude::*;

View File

@@ -1,39 +1,39 @@
use crate::{AssistantPanel, AssistantPanelEvent, DEFAULT_CONTEXT_LINES};
use anyhow::{Context as _, Result};
use assistant_context_editor::{RequestType, humanize_token_count};
use assistant_context_editor::{humanize_token_count, RequestType};
use assistant_settings::AssistantSettings;
use client::telemetry::Telemetry;
use collections::{HashMap, VecDeque};
use editor::{
Editor, EditorElement, EditorEvent, EditorMode, EditorStyle, MultiBuffer,
actions::{MoveDown, MoveUp, SelectAll},
Editor, EditorElement, EditorEvent, EditorMode, EditorStyle, MultiBuffer,
};
use fs::Fs;
use futures::{SinkExt, StreamExt, channel::mpsc};
use futures::{channel::mpsc, SinkExt, StreamExt};
use gpui::{
App, Context, Entity, EventEmitter, FocusHandle, Focusable, Global, Subscription, Task,
TextStyle, UpdateGlobal, WeakEntity,
};
use language::Buffer;
use language_model::{
ConfiguredModel, LanguageModelRegistry, LanguageModelRequest, LanguageModelRequestMessage,
Role, report_assistant_event,
report_assistant_event, LanguageModelRegistry, LanguageModelRequest,
LanguageModelRequestMessage, Role,
};
use language_model_selector::{LanguageModelSelector, LanguageModelSelectorPopoverMenu};
use prompt_store::PromptBuilder;
use settings::{Settings, update_settings_file};
use settings::{update_settings_file, Settings};
use std::{
cmp,
sync::Arc,
time::{Duration, Instant},
};
use telemetry_events::{AssistantEventData, AssistantKind, AssistantPhase};
use telemetry_events::{AssistantEvent, AssistantKind, AssistantPhase};
use terminal::Terminal;
use terminal_view::TerminalView;
use theme::ThemeSettings;
use ui::{IconButtonShape, Tooltip, prelude::*, text_for_action};
use ui::{prelude::*, text_for_action, IconButtonShape, Tooltip};
use util::ResultExt;
use workspace::{Toast, Workspace, notifications::NotificationId};
use workspace::{notifications::NotificationId, Toast, Workspace};
pub fn init(
fs: Arc<dyn Fs>,
@@ -318,13 +318,11 @@ impl TerminalInlineAssistant {
})
.log_err();
if let Some(ConfiguredModel { model, .. }) =
LanguageModelRegistry::read_global(cx).inline_assistant_model()
{
if let Some(model) = LanguageModelRegistry::read_global(cx).active_model() {
let codegen = assist.codegen.read(cx);
let executor = cx.background_executor().clone();
report_assistant_event(
AssistantEventData {
AssistantEvent {
conversation_id: None,
kind: AssistantKind::InlineTerminal,
message_id: codegen.message_id.clone(),
@@ -654,8 +652,8 @@ impl Render for PromptEditor {
format!(
"Using {}",
LanguageModelRegistry::read_global(cx)
.inline_assistant_model()
.map(|inline_assistant| inline_assistant.model.name().0)
.active_model()
.map(|model| model.name().0)
.unwrap_or_else(|| "No model selected".into()),
),
None,
@@ -824,9 +822,7 @@ impl PromptEditor {
fn count_tokens(&mut self, cx: &mut Context<Self>) {
let assist_id = self.id;
let Some(ConfiguredModel { model, .. }) =
LanguageModelRegistry::read_global(cx).inline_assistant_model()
else {
let Some(model) = LanguageModelRegistry::read_global(cx).active_model() else {
return;
};
self.pending_token_count = cx.spawn(async move |this, cx| {
@@ -984,9 +980,7 @@ impl PromptEditor {
}
fn render_token_count(&self, cx: &mut Context<Self>) -> Option<impl IntoElement> {
let model = LanguageModelRegistry::read_global(cx)
.inline_assistant_model()?
.model;
let model = LanguageModelRegistry::read_global(cx).active_model()?;
let token_count = self.token_count?;
let max_token_count = model.max_token_count();
@@ -1137,9 +1131,7 @@ impl Codegen {
}
pub fn start(&mut self, prompt: LanguageModelRequest, cx: &mut Context<Self>) {
let Some(ConfiguredModel { model, .. }) =
LanguageModelRegistry::read_global(cx).inline_assistant_model()
else {
let Some(model) = LanguageModelRegistry::read_global(cx).active_model() else {
return;
};
@@ -1183,7 +1175,7 @@ impl Codegen {
let error_message = result.as_ref().err().map(|error| error.to_string());
report_assistant_event(
AssistantEventData {
AssistantEvent {
conversation_id: None,
kind: AssistantKind::InlineTerminal,
message_id,

View File

@@ -1,5 +1,5 @@
[package]
name = "agent"
name = "assistant2"
version = "0.1.0"
edition.workspace = true
publish.workspace = true
@@ -25,13 +25,11 @@ assistant_settings.workspace = true
assistant_slash_command.workspace = true
assistant_tool.workspace = true
async-watch.workspace = true
buffer_diff.workspace = true
chrono.workspace = true
client.workspace = true
clock.workspace = true
collections.workspace = true
command_palette_hooks.workspace = true
component.workspace = true
context_server.workspace = true
convert_case.workspace = true
db.workspace = true
@@ -42,6 +40,7 @@ fs.workspace = true
futures.workspace = true
fuzzy.workspace = true
git.workspace = true
git_ui.workspace = true
gpui.workspace = true
heed.workspace = true
html_to_markdown.workspace = true
@@ -51,13 +50,11 @@ itertools.workspace = true
language.workspace = true
language_model.workspace = true
language_model_selector.workspace = true
linkme.workspace = true
log.workspace = true
lsp.workspace = true
markdown.workspace = true
menu.workspace = true
multi_buffer.workspace = true
ordered-float.workspace = true
parking_lot.workspace = true
paths.workspace = true
picker.workspace = true
@@ -67,7 +64,6 @@ prompt_store.workspace = true
proto.workspace = true
release_channel.workspace = true
rope.workspace = true
schemars.workspace = true
serde.workspace = true
serde_json.workspace = true
settings.workspace = true
@@ -80,19 +76,16 @@ terminal.workspace = true
terminal_view.workspace = true
text.workspace = true
theme.workspace = true
thiserror.workspace = true
time.workspace = true
time_format.workspace = true
ui.workspace = true
ui_input.workspace = true
util.workspace = true
uuid.workspace = true
workspace-hack.workspace = true
vim_mode_setting.workspace = true
workspace.workspace = true
zed_actions.workspace = true
[dev-dependencies]
buffer_diff = { workspace = true, features = ["test-support"] }
editor = { workspace = true, features = ["test-support"] }
gpui = { workspace = true, "features" = ["test-support"] }
indoc.workspace = true

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,4 @@
mod active_thread;
mod agent_diff;
mod assistant_configuration;
mod assistant_model_selector;
mod assistant_panel;
@@ -18,23 +17,19 @@ mod terminal_inline_assistant;
mod thread;
mod thread_history;
mod thread_store;
mod tool_compatibility;
mod tool_use;
mod ui;
use std::sync::Arc;
use assistant_settings::{AgentProfileId, AssistantSettings};
use assistant_settings::AssistantSettings;
use client::Client;
use command_palette_hooks::CommandPaletteFilter;
use feature_flags::{Assistant2FeatureFlag, FeatureFlagAppExt};
use fs::Fs;
use gpui::{App, actions, impl_actions};
use gpui::{actions, App};
use prompt_store::PromptBuilder;
use schemars::JsonSchema;
use serde::Deserialize;
use settings::Settings as _;
use thread::ThreadId;
pub use crate::active_thread::ActiveThread;
use crate::assistant_configuration::{AddContextServerModal, ManageProfilesModal};
@@ -42,17 +37,17 @@ pub use crate::assistant_panel::{AssistantPanel, ConcreteAssistantPanelDelegate}
pub use crate::inline_assistant::InlineAssistant;
pub use crate::thread::{Message, RequestKind, Thread, ThreadEvent};
pub use crate::thread_store::ThreadStore;
pub use agent_diff::{AgentDiff, AgentDiffToolbar};
actions!(
agent,
assistant2,
[
NewTextThread,
NewThread,
NewPromptEditor,
ToggleContextPicker,
ToggleProfileSelector,
RemoveAllContext,
ExpandMessageEditor,
OpenHistory,
OpenConfiguration,
ManageProfiles,
AddContextServer,
RemoveSelectedThread,
Chat,
@@ -65,40 +60,13 @@ actions!(
FocusRight,
RemoveFocusedContext,
AcceptSuggestedContext,
OpenActiveThreadAsMarkdown,
OpenAgentDiff,
Keep,
Reject,
RejectAll,
KeepAll
OpenActiveThreadAsMarkdown
]
);
#[derive(Default, Clone, PartialEq, Deserialize, JsonSchema)]
pub struct NewThread {
#[serde(default)]
from_thread_id: Option<ThreadId>,
}
const NAMESPACE: &str = "assistant2";
#[derive(PartialEq, Clone, Default, Debug, Deserialize, JsonSchema)]
pub struct ManageProfiles {
#[serde(default)]
pub customize_tools: Option<AgentProfileId>,
}
impl ManageProfiles {
pub fn customize_tools(profile_id: AgentProfileId) -> Self {
Self {
customize_tools: Some(profile_id),
}
}
}
impl_actions!(agent, [NewThread, ManageProfiles]);
const NAMESPACE: &str = "agent";
/// Initializes the `agent` crate.
/// Initializes the `assistant2` crate.
pub fn init(
fs: Arc<dyn Fs>,
client: Arc<Client>,
@@ -124,10 +92,10 @@ pub fn init(
cx.observe_new(AddContextServerModal::register).detach();
cx.observe_new(ManageProfilesModal::register).detach();
feature_gate_agent_actions(cx);
feature_gate_assistant2_actions(cx);
}
fn feature_gate_agent_actions(cx: &mut App) {
fn feature_gate_assistant2_actions(cx: &mut App) {
CommandPaletteFilter::update_global(cx, |filter, _cx| {
filter.hide_namespace(NAMESPACE);
});

View File

@@ -1,20 +1,16 @@
mod add_context_server_modal;
mod manage_profiles_modal;
mod profile_picker;
mod tool_picker;
use std::sync::Arc;
use assistant_settings::AssistantSettings;
use assistant_tool::{ToolSource, ToolWorkingSet};
use collections::HashMap;
use context_server::manager::ContextServerManager;
use fs::Fs;
use gpui::{Action, AnyView, App, Entity, EventEmitter, FocusHandle, Focusable, Subscription};
use language_model::{LanguageModelProvider, LanguageModelProviderId, LanguageModelRegistry};
use settings::{Settings, update_settings_file};
use ui::{
Disclosure, Divider, DividerColor, ElevationIndex, Indicator, Switch, Tooltip, prelude::*,
};
use ui::{prelude::*, Disclosure, Divider, DividerColor, ElevationIndex, Indicator, Switch};
use util::ResultExt as _;
use zed_actions::ExtensionCategoryFilter;
@@ -24,20 +20,18 @@ pub(crate) use manage_profiles_modal::ManageProfilesModal;
use crate::AddContextServer;
pub struct AssistantConfiguration {
fs: Arc<dyn Fs>,
focus_handle: FocusHandle,
configuration_views_by_provider: HashMap<LanguageModelProviderId, AnyView>,
context_server_manager: Entity<ContextServerManager>,
expanded_context_server_tools: HashMap<Arc<str>, bool>,
tools: Entity<ToolWorkingSet>,
tools: Arc<ToolWorkingSet>,
_registry_subscription: Subscription,
}
impl AssistantConfiguration {
pub fn new(
fs: Arc<dyn Fs>,
context_server_manager: Entity<ContextServerManager>,
tools: Entity<ToolWorkingSet>,
tools: Arc<ToolWorkingSet>,
window: &mut Window,
cx: &mut Context<Self>,
) -> Self {
@@ -61,7 +55,6 @@ impl AssistantConfiguration {
);
let mut this = Self {
fs,
focus_handle,
configuration_views_by_provider: HashMap::default(),
context_server_manager,
@@ -113,7 +106,7 @@ impl AssistantConfiguration {
&mut self,
provider: &Arc<dyn LanguageModelProvider>,
cx: &mut Context<Self>,
) -> impl IntoElement + use<> {
) -> impl IntoElement {
let provider_id = provider.id().0.clone();
let provider_name = provider.name().0.clone();
let configuration_view = self
@@ -175,58 +168,9 @@ impl AssistantConfiguration {
)
}
fn render_command_permission(&mut self, cx: &mut Context<Self>) -> impl IntoElement {
let always_allow_tool_actions = AssistantSettings::get_global(cx).always_allow_tool_actions;
const HEADING: &str = "Allow running tools without asking for confirmation";
v_flex()
.p(DynamicSpacing::Base16.rems(cx))
.gap_2()
.flex_1()
.child(Headline::new("General Settings").size(HeadlineSize::Small))
.child(
h_flex()
.p_2p5()
.rounded_sm()
.bg(cx.theme().colors().editor_background)
.border_1()
.border_color(cx.theme().colors().border)
.gap_4()
.justify_between()
.flex_wrap()
.child(
v_flex()
.gap_0p5()
.max_w_5_6()
.child(Label::new(HEADING))
.child(Label::new("When enabled, the agent can perform potentially destructive actions without asking for your confirmation.").color(Color::Muted)),
)
.child(
Switch::new(
"always-allow-tool-actions-switch",
always_allow_tool_actions.into(),
)
.on_click({
let fs = self.fs.clone();
move |state, _window, cx| {
let allow = state == &ToggleState::Selected;
update_settings_file::<AssistantSettings>(
fs.clone(),
cx,
move |settings, _| {
settings.set_always_allow_tool_actions(allow);
},
);
}
}),
),
)
}
fn render_context_servers_section(&mut self, cx: &mut Context<Self>) -> impl IntoElement {
let context_servers = self.context_server_manager.read(cx).all_servers().clone();
let tools_by_source = self.tools.read(cx).tools_by_source(cx);
let tools_by_source = self.tools.tools_by_source(cx);
let empty = Vec::new();
const SUBHEADING: &str = "Connect to context servers via the Model Context Protocol either via Zed extensions or directly.";
@@ -238,10 +182,7 @@ impl AssistantConfiguration {
.child(
v_flex()
.gap_0p5()
.child(
Headline::new("Model Context Protocol (MCP) Servers")
.size(HeadlineSize::Small),
)
.child(Headline::new("Context Servers (MCP)").size(HeadlineSize::Small))
.child(Label::new(SUBHEADING).color(Color::Muted)),
)
.children(context_servers.into_iter().map(|context_server| {
@@ -267,9 +208,10 @@ impl AssistantConfiguration {
.bg(cx.theme().colors().editor_background)
.child(
h_flex()
.p_1()
.justify_between()
.when(are_tools_expanded && tool_count > 1, |element| {
.px_2()
.py_1()
.when(are_tools_expanded, |element| {
element
.border_b_1()
.border_color(cx.theme().colors().border)
@@ -279,7 +221,6 @@ impl AssistantConfiguration {
.gap_2()
.child(
Disclosure::new("tool-list-disclosure", are_tools_expanded)
.disabled(tool_count == 0)
.on_click(cx.listener({
let context_server_id = context_server.id();
move |this, _event, _window, _cx| {
@@ -300,11 +241,10 @@ impl AssistantConfiguration {
.child(Label::new(context_server.id()))
.child(
Label::new(format!("{tool_count} tools"))
.color(Color::Muted)
.size(LabelSize::Small),
.color(Color::Muted),
),
)
.child(
.child(h_flex().child(
Switch::new("context-server-switch", is_running.into()).on_click({
let context_server_manager =
self.context_server_manager.clone();
@@ -340,7 +280,7 @@ impl AssistantConfiguration {
}
}
}),
),
)),
)
.map(|parent| {
if !are_tools_expanded {
@@ -350,29 +290,14 @@ impl AssistantConfiguration {
parent.child(v_flex().children(tools.into_iter().enumerate().map(
|(ix, tool)| {
h_flex()
.id("tool-item")
.pl_2()
.pr_1()
.px_2()
.py_1()
.gap_2()
.justify_between()
.when(ix < tool_count - 1, |element| {
element
.border_b_1()
.border_color(cx.theme().colors().border_variant)
.border_color(cx.theme().colors().border)
})
.child(
Label::new(tool.name())
.buffer_font(cx)
.size(LabelSize::Small),
)
.child(
IconButton::new(("tool-description", ix), IconName::Info)
.shape(ui::IconButtonShape::Square)
.icon_size(IconSize::Small)
.icon_color(Color::Ignored)
.tooltip(Tooltip::text(tool.description())),
)
.child(Label::new(tool.name()))
},
)))
})
@@ -383,7 +308,7 @@ impl AssistantConfiguration {
.gap_2()
.child(
h_flex().w_full().child(
Button::new("add-context-server", "Add MCPs Directly")
Button::new("add-context-server", "Add Context Server")
.style(ButtonStyle::Filled)
.layer(ElevationIndex::ModalSurface)
.full_width()
@@ -399,7 +324,7 @@ impl AssistantConfiguration {
h_flex().w_full().child(
Button::new(
"install-context-server-extensions",
"Install MCP Extensions",
"Install Context Server Extensions",
)
.style(ButtonStyle::Filled)
.layer(ElevationIndex::ModalSurface)
@@ -430,13 +355,10 @@ impl Render for AssistantConfiguration {
v_flex()
.id("assistant-configuration")
.key_context("AgentConfiguration")
.track_focus(&self.focus_handle(cx))
.bg(cx.theme().colors().panel_background)
.size_full()
.overflow_y_scroll()
.child(self.render_command_permission(cx))
.child(Divider::horizontal().color(DividerColor::Border))
.child(self.render_context_servers_section(cx))
.child(Divider::horizontal().color(DividerColor::Border))
.child(

View File

@@ -1,17 +1,17 @@
use context_server::{ContextServerSettings, ServerCommand, ServerConfig};
use gpui::{DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, WeakEntity, prelude::*};
use editor::Editor;
use gpui::{prelude::*, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, WeakEntity};
use serde_json::json;
use settings::update_settings_file;
use ui::{Modal, ModalFooter, ModalHeader, Section, Tooltip, prelude::*};
use ui_input::SingleLineInput;
use ui::{prelude::*, Modal, ModalFooter, ModalHeader, Section, Tooltip};
use workspace::{ModalView, Workspace};
use crate::AddContextServer;
pub struct AddContextServerModal {
workspace: WeakEntity<Workspace>,
name_editor: Entity<SingleLineInput>,
command_editor: Entity<SingleLineInput>,
name_editor: Entity<Editor>,
command_editor: Entity<Editor>,
}
impl AddContextServerModal {
@@ -33,10 +33,15 @@ impl AddContextServerModal {
window: &mut Window,
cx: &mut Context<Self>,
) -> Self {
let name_editor =
cx.new(|cx| SingleLineInput::new(window, cx, "Your server name").label("Name"));
let command_editor = cx.new(|cx| {
SingleLineInput::new(window, cx, "Command").label("Command to run the context server")
let name_editor = cx.new(|cx| Editor::single_line(window, cx));
let command_editor = cx.new(|cx| Editor::single_line(window, cx));
name_editor.update(cx, |editor, cx| {
editor.set_placeholder_text("Context server name", cx);
});
command_editor.update(cx, |editor, cx| {
editor.set_placeholder_text("Command to run the context server", cx);
});
Self {
@@ -47,22 +52,8 @@ impl AddContextServerModal {
}
fn confirm(&mut self, cx: &mut Context<Self>) {
let name = self
.name_editor
.read(cx)
.editor()
.read(cx)
.text(cx)
.trim()
.to_string();
let command = self
.command_editor
.read(cx)
.editor()
.read(cx)
.text(cx)
.trim()
.to_string();
let name = self.name_editor.read(cx).text(cx).trim().to_string();
let command = self.command_editor.read(cx).text(cx).trim().to_string();
if name.is_empty() || command.is_empty() {
return;
@@ -113,8 +104,8 @@ impl EventEmitter<DismissEvent> for AddContextServerModal {}
impl Render for AddContextServerModal {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let is_name_empty = self.name_editor.read(cx).is_empty(cx);
let is_command_empty = self.command_editor.read(cx).is_empty(cx);
let is_name_empty = self.name_editor.read(cx).text(cx).trim().is_empty();
let is_command_empty = self.command_editor.read(cx).text(cx).trim().is_empty();
div()
.elevation_3(cx)
@@ -131,8 +122,18 @@ impl Render for AddContextServerModal {
.header(ModalHeader::new().headline("Add Context Server"))
.section(
Section::new()
.child(self.name_editor.clone())
.child(self.command_editor.clone()),
.child(
v_flex()
.gap_1()
.child(Label::new("Name"))
.child(self.name_editor.clone()),
)
.child(
v_flex()
.gap_1()
.child(Label::new("Command"))
.child(self.command_editor.clone()),
),
)
.footer(
ModalFooter::new()

View File

@@ -2,76 +2,74 @@ mod profile_modal_header;
use std::sync::Arc;
use assistant_settings::{AgentProfile, AgentProfileId, AssistantSettings};
use assistant_settings::{
AgentProfile, AgentProfileContent, AssistantSettings, AssistantSettingsContent,
ContextServerPresetContent, VersionedAssistantSettingsContent,
};
use assistant_tool::ToolWorkingSet;
use convert_case::{Case, Casing as _};
use editor::Editor;
use fs::Fs;
use gpui::{
DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, Subscription, WeakEntity,
prelude::*,
prelude::*, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, Subscription,
WeakEntity,
};
use settings::{Settings as _, update_settings_file};
use ui::{
KeyBinding, ListItem, ListItemSpacing, ListSeparator, Navigable, NavigableEntry, prelude::*,
};
use util::ResultExt as _;
use settings::{update_settings_file, Settings as _};
use ui::{prelude::*, ListItem, ListItemSpacing, ListSeparator, Navigable, NavigableEntry};
use workspace::{ModalView, Workspace};
use crate::assistant_configuration::manage_profiles_modal::profile_modal_header::ProfileModalHeader;
use crate::assistant_configuration::profile_picker::{ProfilePicker, ProfilePickerDelegate};
use crate::assistant_configuration::tool_picker::{ToolPicker, ToolPickerDelegate};
use crate::{AssistantPanel, ManageProfiles, ThreadStore};
enum Mode {
ChooseProfile(ChooseProfileMode),
ChooseProfile {
profile_picker: Entity<ProfilePicker>,
_subscription: Subscription,
},
NewProfile(NewProfileMode),
ViewProfile(ViewProfileMode),
ConfigureTools {
profile_id: AgentProfileId,
profile_id: Arc<str>,
tool_picker: Entity<ToolPicker>,
_subscription: Subscription,
},
}
impl Mode {
pub fn choose_profile(_window: &mut Window, cx: &mut Context<ManageProfilesModal>) -> Self {
let settings = AssistantSettings::get_global(cx);
pub fn choose_profile(window: &mut Window, cx: &mut Context<ManageProfilesModal>) -> Self {
let this = cx.entity();
let mut profiles = settings.profiles.clone();
profiles.sort_unstable_by(|_, a, _, b| a.name.cmp(&b.name));
let profile_picker = cx.new(|cx| {
let delegate = ProfilePickerDelegate::new(
move |profile_id, window, cx| {
this.update(cx, |this, cx| {
this.view_profile(profile_id.clone(), window, cx);
})
},
cx,
);
ProfilePicker::new(delegate, window, cx)
});
let dismiss_subscription = cx.subscribe_in(
&profile_picker,
window,
|_this, _profile_picker, _: &DismissEvent, _window, cx| {
cx.emit(DismissEvent);
},
);
let profiles = profiles
.into_iter()
.map(|(id, profile)| ProfileEntry {
id,
name: profile.name,
navigation: NavigableEntry::focusable(cx),
})
.collect::<Vec<_>>();
Self::ChooseProfile(ChooseProfileMode {
profiles,
add_new_profile: NavigableEntry::focusable(cx),
})
Self::ChooseProfile {
profile_picker,
_subscription: dismiss_subscription,
}
}
}
#[derive(Clone)]
struct ProfileEntry {
pub id: AgentProfileId,
pub name: SharedString,
pub navigation: NavigableEntry,
}
#[derive(Clone)]
pub struct ChooseProfileMode {
profiles: Vec<ProfileEntry>,
add_new_profile: NavigableEntry,
}
#[derive(Clone)]
pub struct ViewProfileMode {
profile_id: AgentProfileId,
profile_id: Arc<str>,
fork_profile: NavigableEntry,
configure_tools: NavigableEntry,
}
@@ -79,12 +77,12 @@ pub struct ViewProfileMode {
#[derive(Clone)]
pub struct NewProfileMode {
name_editor: Entity<Editor>,
base_profile_id: Option<AgentProfileId>,
base_profile_id: Option<Arc<str>>,
}
pub struct ManageProfilesModal {
fs: Arc<dyn Fs>,
tools: Entity<ToolWorkingSet>,
tools: Arc<ToolWorkingSet>,
thread_store: WeakEntity<ThreadStore>,
focus_handle: FocusHandle,
mode: Mode,
@@ -96,20 +94,14 @@ impl ManageProfilesModal {
_window: Option<&mut Window>,
_cx: &mut Context<Workspace>,
) {
workspace.register_action(|workspace, action: &ManageProfiles, window, cx| {
workspace.register_action(|workspace, _: &ManageProfiles, window, cx| {
if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
let fs = workspace.app_state().fs.clone();
let thread_store = panel.read(cx).thread_store();
let tools = thread_store.read(cx).tools();
let thread_store = thread_store.downgrade();
workspace.toggle_modal(window, cx, |window, cx| {
let mut this = Self::new(fs, tools, thread_store, window, cx);
if let Some(profile_id) = action.customize_tools.clone() {
this.configure_tools(profile_id, window, cx);
}
this
Self::new(fs, tools, thread_store, window, cx)
})
}
});
@@ -117,7 +109,7 @@ impl ManageProfilesModal {
pub fn new(
fs: Arc<dyn Fs>,
tools: Entity<ToolWorkingSet>,
tools: Arc<ToolWorkingSet>,
thread_store: WeakEntity<ThreadStore>,
window: &mut Window,
cx: &mut Context<Self>,
@@ -140,7 +132,7 @@ impl ManageProfilesModal {
fn new_profile(
&mut self,
base_profile_id: Option<AgentProfileId>,
base_profile_id: Option<Arc<str>>,
window: &mut Window,
cx: &mut Context<Self>,
) {
@@ -158,7 +150,7 @@ impl ManageProfilesModal {
pub fn view_profile(
&mut self,
profile_id: AgentProfileId,
profile_id: Arc<str>,
window: &mut Window,
cx: &mut Context<Self>,
) {
@@ -172,7 +164,7 @@ impl ManageProfilesModal {
fn configure_tools(
&mut self,
profile_id: AgentProfileId,
profile_id: Arc<str>,
window: &mut Window,
cx: &mut Context<Self>,
) {
@@ -219,7 +211,7 @@ impl ManageProfilesModal {
.and_then(|profile_id| settings.profiles.get(profile_id).cloned());
let name = mode.name_editor.read(cx).text(cx);
let profile_id = AgentProfileId(name.to_case(Case::Kebab).into());
let profile_id: Arc<str> = name.to_case(Case::Kebab).into();
let profile = AgentProfile {
name: name.into(),
@@ -227,10 +219,6 @@ impl ManageProfilesModal {
.as_ref()
.map(|profile| profile.tools.clone())
.unwrap_or_default(),
enable_all_context_servers: base_profile
.as_ref()
.map(|profile| profile.enable_all_context_servers)
.unwrap_or_default(),
context_servers: base_profile
.map(|profile| profile.context_servers)
.unwrap_or_default(),
@@ -246,9 +234,7 @@ impl ManageProfilesModal {
fn cancel(&mut self, window: &mut Window, cx: &mut Context<Self>) {
match &self.mode {
Mode::ChooseProfile { .. } => {
cx.emit(DismissEvent);
}
Mode::ChooseProfile { .. } => {}
Mode::NewProfile(mode) => {
if let Some(profile_id) = mode.base_profile_id.clone() {
self.view_profile(profile_id, window, cx);
@@ -261,15 +247,39 @@ impl ManageProfilesModal {
}
}
fn create_profile(
&self,
profile_id: AgentProfileId,
profile: AgentProfile,
cx: &mut Context<Self>,
) {
fn create_profile(&self, profile_id: Arc<str>, profile: AgentProfile, cx: &mut Context<Self>) {
update_settings_file::<AssistantSettings>(self.fs.clone(), cx, {
move |settings, _cx| {
settings.create_profile(profile_id, profile).log_err();
move |settings, _cx| match settings {
AssistantSettingsContent::Versioned(VersionedAssistantSettingsContent::V2(
settings,
)) => {
let profiles = settings.profiles.get_or_insert_default();
if profiles.contains_key(&profile_id) {
log::error!("profile with ID '{profile_id}' already exists");
return;
}
profiles.insert(
profile_id,
AgentProfileContent {
name: profile.name.into(),
tools: profile.tools,
context_servers: profile
.context_servers
.into_iter()
.map(|(server_id, preset)| {
(
server_id,
ContextServerPresetContent {
tools: preset.tools,
},
)
})
.collect(),
},
);
}
_ => {}
}
});
}
@@ -280,7 +290,7 @@ impl ModalView for ManageProfilesModal {}
impl Focusable for ManageProfilesModal {
fn focus_handle(&self, cx: &App) -> FocusHandle {
match &self.mode {
Mode::ChooseProfile(_) => self.focus_handle.clone(),
Mode::ChooseProfile { profile_picker, .. } => profile_picker.focus_handle(cx),
Mode::NewProfile(mode) => mode.name_editor.focus_handle(cx),
Mode::ViewProfile(_) => self.focus_handle.clone(),
Mode::ConfigureTools { tool_picker, .. } => tool_picker.focus_handle(cx),
@@ -291,133 +301,15 @@ impl Focusable for ManageProfilesModal {
impl EventEmitter<DismissEvent> for ManageProfilesModal {}
impl ManageProfilesModal {
fn render_choose_profile(
&mut self,
mode: ChooseProfileMode,
window: &mut Window,
cx: &mut Context<Self>,
) -> impl IntoElement {
Navigable::new(
div()
.track_focus(&self.focus_handle(cx))
.size_full()
.child(ProfileModalHeader::new(
"Agent Profiles",
IconName::ZedAssistant,
))
.child(
v_flex()
.pb_1()
.child(ListSeparator)
.children(mode.profiles.iter().map(|profile| {
div()
.id(SharedString::from(format!("profile-{}", profile.id)))
.track_focus(&profile.navigation.focus_handle)
.on_action({
let profile_id = profile.id.clone();
cx.listener(move |this, _: &menu::Confirm, window, cx| {
this.view_profile(profile_id.clone(), window, cx);
})
})
.child(
ListItem::new(SharedString::from(format!(
"profile-{}",
profile.id
)))
.toggle_state(
profile
.navigation
.focus_handle
.contains_focused(window, cx),
)
.inset(true)
.spacing(ListItemSpacing::Sparse)
.child(Label::new(profile.name.clone()))
.end_slot(
h_flex()
.gap_1()
.child(Label::new("Customize").size(LabelSize::Small))
.children(KeyBinding::for_action_in(
&menu::Confirm,
&self.focus_handle,
window,
cx,
)),
)
.on_click({
let profile_id = profile.id.clone();
cx.listener(move |this, _, window, cx| {
this.view_profile(profile_id.clone(), window, cx);
})
}),
)
}))
.child(ListSeparator)
.child(
div()
.id("new-profile")
.track_focus(&mode.add_new_profile.focus_handle)
.on_action(cx.listener(|this, _: &menu::Confirm, window, cx| {
this.new_profile(None, window, cx);
}))
.child(
ListItem::new("new-profile")
.toggle_state(
mode.add_new_profile
.focus_handle
.contains_focused(window, cx),
)
.inset(true)
.spacing(ListItemSpacing::Sparse)
.start_slot(Icon::new(IconName::Plus))
.child(Label::new("Add New Profile"))
.on_click({
cx.listener(move |this, _, window, cx| {
this.new_profile(None, window, cx);
})
}),
),
),
)
.into_any_element(),
)
.map(|mut navigable| {
for profile in mode.profiles {
navigable = navigable.entry(profile.navigation);
}
navigable
})
.entry(mode.add_new_profile)
}
fn render_new_profile(
&mut self,
mode: NewProfileMode,
_window: &mut Window,
cx: &mut Context<Self>,
) -> impl IntoElement {
let settings = AssistantSettings::get_global(cx);
let base_profile_name = mode.base_profile_id.as_ref().map(|base_profile_id| {
settings
.profiles
.get(base_profile_id)
.map(|profile| profile.name.clone())
.unwrap_or_else(|| "Unknown".into())
});
v_flex()
.id("new-profile")
.track_focus(&self.focus_handle(cx))
.child(ProfileModalHeader::new(
match base_profile_name {
Some(base_profile) => format!("Fork {base_profile}"),
None => "New Profile".into(),
},
IconName::Plus,
))
.child(ListSeparator)
.child(h_flex().p_2().child(mode.name_editor.clone()))
}
@@ -536,8 +428,10 @@ impl Render for ManageProfilesModal {
}))
.on_mouse_down_out(cx.listener(|_this, _, _, cx| cx.emit(DismissEvent)))
.child(match &self.mode {
Mode::ChooseProfile(mode) => self
.render_choose_profile(mode.clone(), window, cx)
Mode::ChooseProfile { profile_picker, .. } => div()
.child(ProfileModalHeader::new("Profiles", IconName::ZedAssistant))
.child(ListSeparator)
.child(profile_picker.clone())
.into_any_element(),
Mode::NewProfile(mode) => self
.render_new_profile(mode.clone(), window, cx)

View File

@@ -0,0 +1,194 @@
use std::sync::Arc;
use assistant_settings::AssistantSettings;
use fuzzy::{match_strings, StringMatch, StringMatchCandidate};
use gpui::{
App, Context, DismissEvent, Entity, EventEmitter, Focusable, SharedString, Task, WeakEntity,
Window,
};
use picker::{Picker, PickerDelegate};
use settings::Settings;
use ui::{prelude::*, HighlightedLabel, ListItem, ListItemSpacing};
use util::ResultExt as _;
pub struct ProfilePicker {
picker: Entity<Picker<ProfilePickerDelegate>>,
}
impl ProfilePicker {
pub fn new(
delegate: ProfilePickerDelegate,
window: &mut Window,
cx: &mut Context<Self>,
) -> Self {
let picker = cx.new(|cx| Picker::uniform_list(delegate, window, cx).modal(false));
Self { picker }
}
}
impl EventEmitter<DismissEvent> for ProfilePicker {}
impl Focusable for ProfilePicker {
fn focus_handle(&self, cx: &App) -> gpui::FocusHandle {
self.picker.focus_handle(cx)
}
}
impl Render for ProfilePicker {
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
v_flex().w(rems(34.)).child(self.picker.clone())
}
}
#[derive(Debug)]
pub struct ProfileEntry {
pub id: Arc<str>,
pub name: SharedString,
}
pub struct ProfilePickerDelegate {
profile_picker: WeakEntity<ProfilePicker>,
profiles: Vec<ProfileEntry>,
matches: Vec<StringMatch>,
selected_index: usize,
on_confirm: Arc<dyn Fn(&Arc<str>, &mut Window, &mut App) + 'static>,
}
impl ProfilePickerDelegate {
pub fn new(
on_confirm: impl Fn(&Arc<str>, &mut Window, &mut App) + 'static,
cx: &mut Context<ProfilePicker>,
) -> Self {
let settings = AssistantSettings::get_global(cx);
let profiles = settings
.profiles
.iter()
.map(|(id, profile)| ProfileEntry {
id: id.clone(),
name: profile.name.clone(),
})
.collect::<Vec<_>>();
Self {
profile_picker: cx.entity().downgrade(),
profiles,
matches: Vec::new(),
selected_index: 0,
on_confirm: Arc::new(on_confirm),
}
}
}
impl PickerDelegate for ProfilePickerDelegate {
type ListItem = ListItem;
fn match_count(&self) -> usize {
self.matches.len()
}
fn selected_index(&self) -> usize {
self.selected_index
}
fn set_selected_index(
&mut self,
ix: usize,
_window: &mut Window,
_cx: &mut Context<Picker<Self>>,
) {
self.selected_index = ix;
}
fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> {
"Search profiles…".into()
}
fn update_matches(
&mut self,
query: String,
window: &mut Window,
cx: &mut Context<Picker<Self>>,
) -> Task<()> {
let background = cx.background_executor().clone();
let candidates = self
.profiles
.iter()
.enumerate()
.map(|(id, profile)| StringMatchCandidate::new(id, profile.name.as_ref()))
.collect::<Vec<_>>();
cx.spawn_in(window, async move |this, cx| {
let matches = if query.is_empty() {
candidates
.into_iter()
.enumerate()
.map(|(index, candidate)| StringMatch {
candidate_id: index,
string: candidate.string,
positions: Vec::new(),
score: 0.,
})
.collect()
} else {
match_strings(
&candidates,
&query,
false,
100,
&Default::default(),
background,
)
.await
};
this.update(cx, |this, _cx| {
this.delegate.matches = matches;
this.delegate.selected_index = this
.delegate
.selected_index
.min(this.delegate.matches.len().saturating_sub(1));
})
.log_err();
})
}
fn confirm(&mut self, _secondary: bool, window: &mut Window, cx: &mut Context<Picker<Self>>) {
if self.matches.is_empty() {
self.dismissed(window, cx);
return;
}
let candidate_id = self.matches[self.selected_index].candidate_id;
let profile = &self.profiles[candidate_id];
(self.on_confirm)(&profile.id, window, cx);
}
fn dismissed(&mut self, _window: &mut Window, cx: &mut Context<Picker<Self>>) {
self.profile_picker
.update(cx, |_this, cx| cx.emit(DismissEvent))
.log_err();
}
fn render_match(
&self,
ix: usize,
selected: bool,
_window: &mut Window,
_cx: &mut Context<Picker<Self>>,
) -> Option<Self::ListItem> {
let profile_match = &self.matches[ix];
Some(
ListItem::new(ix)
.inset(true)
.spacing(ListItemSpacing::Sparse)
.toggle_state(selected)
.child(HighlightedLabel::new(
profile_match.string.clone(),
profile_match.positions.clone(),
)),
)
}
}

View File

@@ -1,16 +1,16 @@
use std::sync::Arc;
use assistant_settings::{
AgentProfile, AgentProfileContent, AgentProfileId, AssistantSettings, AssistantSettingsContent,
AgentProfile, AgentProfileContent, AssistantSettings, AssistantSettingsContent,
ContextServerPresetContent, VersionedAssistantSettingsContent,
};
use assistant_tool::{ToolSource, ToolWorkingSet};
use fs::Fs;
use fuzzy::{StringMatch, StringMatchCandidate, match_strings};
use fuzzy::{match_strings, StringMatch, StringMatchCandidate};
use gpui::{App, Context, DismissEvent, Entity, EventEmitter, Focusable, Task, WeakEntity, Window};
use picker::{Picker, PickerDelegate};
use settings::{Settings as _, update_settings_file};
use ui::{HighlightedLabel, ListItem, ListItemSpacing, prelude::*};
use settings::{update_settings_file, Settings as _};
use ui::{prelude::*, HighlightedLabel, ListItem, ListItemSpacing};
use util::ResultExt as _;
use crate::ThreadStore;
@@ -51,7 +51,7 @@ pub struct ToolPickerDelegate {
thread_store: WeakEntity<ThreadStore>,
fs: Arc<dyn Fs>,
tools: Vec<ToolEntry>,
profile_id: AgentProfileId,
profile_id: Arc<str>,
profile: AgentProfile,
matches: Vec<StringMatch>,
selected_index: usize,
@@ -60,15 +60,15 @@ pub struct ToolPickerDelegate {
impl ToolPickerDelegate {
pub fn new(
fs: Arc<dyn Fs>,
tool_set: Entity<ToolWorkingSet>,
tool_set: Arc<ToolWorkingSet>,
thread_store: WeakEntity<ThreadStore>,
profile_id: AgentProfileId,
profile_id: Arc<str>,
profile: AgentProfile,
cx: &mut Context<ToolPicker>,
) -> Self {
let mut tool_entries = Vec::new();
for (source, tools) in tool_set.read(cx).tools_by_source(cx) {
for (source, tools) in tool_set.tools_by_source(cx) {
tool_entries.extend(tools.into_iter().map(|tool| ToolEntry {
name: tool.name().into(),
source: source.clone(),
@@ -191,8 +191,8 @@ impl PickerDelegate for ToolPickerDelegate {
let active_profile_id = &AssistantSettings::get_global(cx).default_profile;
if active_profile_id == &self.profile_id {
self.thread_store
.update(cx, |this, cx| {
this.load_profile(self.profile.clone(), cx);
.update(cx, |this, _cx| {
this.load_profile(&self.profile);
})
.log_err();
}
@@ -202,43 +202,40 @@ impl PickerDelegate for ToolPickerDelegate {
let default_profile = self.profile.clone();
let tool = tool.clone();
move |settings, _cx| match settings {
AssistantSettingsContent::Versioned(boxed) => {
if let VersionedAssistantSettingsContent::V2(ref mut settings) = **boxed {
let profiles = settings.profiles.get_or_insert_default();
let profile =
profiles
.entry(profile_id)
.or_insert_with(|| AgentProfileContent {
name: default_profile.name.into(),
tools: default_profile.tools,
enable_all_context_servers: Some(
default_profile.enable_all_context_servers,
),
context_servers: default_profile
.context_servers
.into_iter()
.map(|(server_id, preset)| {
(
server_id,
ContextServerPresetContent {
tools: preset.tools,
},
)
})
.collect(),
});
match tool.source {
ToolSource::Native => {
*profile.tools.entry(tool.name).or_default() = is_enabled;
}
ToolSource::ContextServer { id } => {
let preset = profile
AssistantSettingsContent::Versioned(VersionedAssistantSettingsContent::V2(
settings,
)) => {
let profiles = settings.profiles.get_or_insert_default();
let profile =
profiles
.entry(profile_id)
.or_insert_with(|| AgentProfileContent {
name: default_profile.name.into(),
tools: default_profile.tools,
context_servers: default_profile
.context_servers
.entry(id.clone().into())
.or_default();
*preset.tools.entry(tool.name.clone()).or_default() = is_enabled;
}
.into_iter()
.map(|(server_id, preset)| {
(
server_id,
ContextServerPresetContent {
tools: preset.tools,
},
)
})
.collect(),
});
match tool.source {
ToolSource::Native => {
*profile.tools.entry(tool.name).or_default() = is_enabled;
}
ToolSource::ContextServer { id } => {
let preset = profile
.context_servers
.entry(id.clone().into())
.or_default();
*preset.tools.entry(tool.name.clone()).or_default() = is_enabled;
}
}
}

View File

@@ -7,19 +7,12 @@ use language_model_selector::{
};
use settings::update_settings_file;
use std::sync::Arc;
use ui::{ButtonLike, PopoverMenuHandle, Tooltip, prelude::*};
#[derive(Clone, Copy)]
pub enum ModelType {
Default,
InlineAssistant,
}
use ui::{prelude::*, ButtonLike, PopoverMenuHandle, Tooltip};
pub struct AssistantModelSelector {
selector: Entity<LanguageModelSelector>,
menu_handle: PopoverMenuHandle<LanguageModelSelector>,
focus_handle: FocusHandle,
model_type: ModelType,
}
impl AssistantModelSelector {
@@ -27,7 +20,6 @@ impl AssistantModelSelector {
fs: Arc<dyn Fs>,
menu_handle: PopoverMenuHandle<LanguageModelSelector>,
focus_handle: FocusHandle,
model_type: ModelType,
window: &mut Window,
cx: &mut App,
) -> Self {
@@ -36,32 +28,11 @@ impl AssistantModelSelector {
let fs = fs.clone();
LanguageModelSelector::new(
move |model, cx| {
let provider = model.provider_id().0.to_string();
let model_id = model.id().0.to_string();
match model_type {
ModelType::Default => {
update_settings_file::<AssistantSettings>(
fs.clone(),
cx,
move |settings, _cx| {
settings.set_model(model.clone());
},
);
}
ModelType::InlineAssistant => {
update_settings_file::<AssistantSettings>(
fs.clone(),
cx,
move |settings, _cx| {
settings.set_inline_assistant_model(
provider.clone(),
model_id.clone(),
);
},
);
}
}
update_settings_file::<AssistantSettings>(
fs.clone(),
cx,
move |settings, _cx| settings.set_model(model.clone()),
);
},
window,
cx,
@@ -69,7 +40,6 @@ impl AssistantModelSelector {
}),
menu_handle,
focus_handle,
model_type,
}
}
@@ -80,16 +50,11 @@ impl AssistantModelSelector {
impl Render for AssistantModelSelector {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let active_model = LanguageModelRegistry::read_global(cx).active_model();
let focus_handle = self.focus_handle.clone();
let model_registry = LanguageModelRegistry::read_global(cx);
let model = match self.model_type {
ModelType::Default => model_registry.default_model(),
ModelType::InlineAssistant => model_registry.inline_assistant_model(),
};
let (model_name, model_icon) = match model {
Some(model) => (model.model.name().0, Some(model.provider.icon())),
_ => (SharedString::from("No model selected"), None),
let model_name = match active_model {
Some(model) => model.name().0,
_ => SharedString::from("No model selected"),
};
LanguageModelSelectorPopoverMenu::new(
@@ -99,16 +64,10 @@ impl Render for AssistantModelSelector {
.child(
h_flex()
.gap_0p5()
.children(
model_icon.map(|icon| {
Icon::new(icon).color(Color::Muted).size(IconSize::Small)
}),
)
.child(
Label::new(model_name)
.size(LabelSize::Small)
.color(Color::Muted)
.ml_1(),
.color(Color::Muted),
)
.child(
Icon::new(IconName::ChevronDown)

View File

@@ -5,12 +5,12 @@ use anyhow::{Context as _, Result};
use client::telemetry::Telemetry;
use collections::HashSet;
use editor::{Anchor, AnchorRangeExt, MultiBuffer, MultiBufferSnapshot, ToOffset as _, ToPoint};
use futures::{SinkExt, Stream, StreamExt, channel::mpsc, future::LocalBoxFuture, join};
use futures::{channel::mpsc, future::LocalBoxFuture, join, SinkExt, Stream, StreamExt};
use gpui::{App, AppContext as _, Context, Entity, EventEmitter, Subscription, Task};
use language::{Buffer, IndentKind, Point, TransactionId, line_diff};
use language::{line_diff, Buffer, IndentKind, Point, TransactionId};
use language_model::{
LanguageModel, LanguageModelRegistry, LanguageModelRequest, LanguageModelRequestMessage,
LanguageModelTextStream, Role, report_assistant_event,
report_assistant_event, LanguageModel, LanguageModelRegistry, LanguageModelRequest,
LanguageModelRequestMessage, LanguageModelTextStream, Role,
};
use multi_buffer::MultiBufferRow;
use parking_lot::Mutex;
@@ -28,7 +28,7 @@ use std::{
time::Instant,
};
use streaming_diff::{CharOperation, LineDiff, LineOperation, StreamingDiff};
use telemetry_events::{AssistantEventData, AssistantKind, AssistantPhase};
use telemetry_events::{AssistantEvent, AssistantKind, AssistantPhase};
pub struct BufferCodegen {
alternatives: Vec<Entity<CodegenAlternative>>,
@@ -156,9 +156,8 @@ impl BufferCodegen {
}
let primary_model = LanguageModelRegistry::read_global(cx)
.default_model()
.context("no active model")?
.model;
.active_model()
.context("no active model")?;
for (model, alternative) in iter::once(primary_model)
.chain(alternative_models)
@@ -415,11 +414,7 @@ impl CodegenAlternative {
};
if let Some(context_store) = &self.context_store {
attach_context_to_message(
&mut request_message,
context_store.read(cx).context().iter(),
cx,
);
attach_context_to_message(&mut request_message, context_store.read(cx).snapshot(cx));
}
request_message.content.push(prompt.into());
@@ -601,7 +596,7 @@ impl CodegenAlternative {
let error_message = result.as_ref().err().map(|error| error.to_string());
report_assistant_event(
AssistantEventData {
AssistantEvent {
conversation_id: None,
message_id,
kind: AssistantKind::Inline,
@@ -1033,14 +1028,14 @@ impl Diff {
mod tests {
use super::*;
use futures::{
Stream,
stream::{self},
Stream,
};
use gpui::TestAppContext;
use indoc::indoc;
use language::{
Buffer, Language, LanguageConfig, LanguageMatcher, Point, language_settings,
tree_sitter_rust,
language_settings, tree_sitter_rust, Buffer, Language, LanguageConfig, LanguageMatcher,
Point,
};
use language_model::{LanguageModelRegistry, TokenUsage};
use rand::prelude::*;

View File

@@ -0,0 +1,316 @@
use std::path::Path;
use std::rc::Rc;
use file_icons::FileIcons;
use gpui::{App, Entity, SharedString};
use language::Buffer;
use language_model::{LanguageModelRequestMessage, MessageContent};
use serde::{Deserialize, Serialize};
use text::BufferId;
use ui::IconName;
use util::post_inc;
use crate::{context_store::buffer_path_log_err, thread::Thread};
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, Serialize, Deserialize)]
pub struct ContextId(pub(crate) usize);
impl ContextId {
pub fn post_inc(&mut self) -> Self {
Self(post_inc(&mut self.0))
}
}
/// Some context attached to a message in a thread.
#[derive(Debug, Clone)]
pub struct ContextSnapshot {
pub id: ContextId,
pub name: SharedString,
pub parent: Option<SharedString>,
pub tooltip: Option<SharedString>,
pub icon_path: Option<SharedString>,
pub kind: ContextKind,
/// Joining these strings separated by \n yields text for model. Not refreshed by `snapshot`.
pub text: Box<[SharedString]>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ContextKind {
File,
Directory,
FetchedUrl,
Thread,
}
impl ContextKind {
pub fn icon(&self) -> IconName {
match self {
ContextKind::File => IconName::File,
ContextKind::Directory => IconName::Folder,
ContextKind::FetchedUrl => IconName::Globe,
ContextKind::Thread => IconName::MessageCircle,
}
}
}
#[derive(Debug)]
pub enum AssistantContext {
File(FileContext),
Directory(DirectoryContext),
FetchedUrl(FetchedUrlContext),
Thread(ThreadContext),
}
impl AssistantContext {
pub fn id(&self) -> ContextId {
match self {
Self::File(file) => file.id,
Self::Directory(directory) => directory.snapshot.id,
Self::FetchedUrl(url) => url.id,
Self::Thread(thread) => thread.id,
}
}
}
#[derive(Debug)]
pub struct FileContext {
pub id: ContextId,
pub context_buffer: ContextBuffer,
}
#[derive(Debug)]
pub struct DirectoryContext {
pub path: Rc<Path>,
pub context_buffers: Vec<ContextBuffer>,
pub snapshot: ContextSnapshot,
}
#[derive(Debug)]
pub struct FetchedUrlContext {
pub id: ContextId,
pub url: SharedString,
pub text: SharedString,
}
// TODO: Model<Thread> holds onto the thread even if the thread is deleted. Can either handle this
// explicitly or have a WeakModel<Thread> and remove during snapshot.
#[derive(Debug)]
pub struct ThreadContext {
pub id: ContextId,
pub thread: Entity<Thread>,
pub text: SharedString,
}
// TODO: Model<Buffer> holds onto the buffer even if the file is deleted and closed. Should remove
// the context from the message editor in this case.
#[derive(Debug, Clone)]
pub struct ContextBuffer {
pub id: BufferId,
pub buffer: Entity<Buffer>,
pub version: clock::Global,
pub text: SharedString,
}
impl AssistantContext {
pub fn snapshot(&self, cx: &App) -> Option<ContextSnapshot> {
match &self {
Self::File(file_context) => file_context.snapshot(cx),
Self::Directory(directory_context) => Some(directory_context.snapshot()),
Self::FetchedUrl(fetched_url_context) => Some(fetched_url_context.snapshot()),
Self::Thread(thread_context) => Some(thread_context.snapshot(cx)),
}
}
}
impl FileContext {
pub fn snapshot(&self, cx: &App) -> Option<ContextSnapshot> {
let buffer = self.context_buffer.buffer.read(cx);
let path = buffer_path_log_err(buffer)?;
let full_path: SharedString = path.to_string_lossy().into_owned().into();
let name = match path.file_name() {
Some(name) => name.to_string_lossy().into_owned().into(),
None => full_path.clone(),
};
let parent = path
.parent()
.and_then(|p| p.file_name())
.map(|p| p.to_string_lossy().into_owned().into());
let icon_path = FileIcons::get_icon(&path, cx);
Some(ContextSnapshot {
id: self.id,
name,
parent,
tooltip: Some(full_path),
icon_path,
kind: ContextKind::File,
text: Box::new([self.context_buffer.text.clone()]),
})
}
}
impl DirectoryContext {
pub fn new(
id: ContextId,
path: &Path,
context_buffers: Vec<ContextBuffer>,
) -> DirectoryContext {
let full_path: SharedString = path.to_string_lossy().into_owned().into();
let name = match path.file_name() {
Some(name) => name.to_string_lossy().into_owned().into(),
None => full_path.clone(),
};
let parent = path
.parent()
.and_then(|p| p.file_name())
.map(|p| p.to_string_lossy().into_owned().into());
// TODO: include directory path in text?
let text = context_buffers
.iter()
.map(|b| b.text.clone())
.collect::<Vec<_>>()
.into();
DirectoryContext {
path: path.into(),
context_buffers,
snapshot: ContextSnapshot {
id,
name,
parent,
tooltip: Some(full_path),
icon_path: None,
kind: ContextKind::Directory,
text,
},
}
}
pub fn snapshot(&self) -> ContextSnapshot {
self.snapshot.clone()
}
}
impl FetchedUrlContext {
pub fn snapshot(&self) -> ContextSnapshot {
ContextSnapshot {
id: self.id,
name: self.url.clone(),
parent: None,
tooltip: None,
icon_path: None,
kind: ContextKind::FetchedUrl,
text: Box::new([self.text.clone()]),
}
}
}
impl ThreadContext {
pub fn snapshot(&self, cx: &App) -> ContextSnapshot {
let thread = self.thread.read(cx);
ContextSnapshot {
id: self.id,
name: thread.summary().unwrap_or("New thread".into()),
parent: None,
tooltip: None,
icon_path: None,
kind: ContextKind::Thread,
text: Box::new([self.text.clone()]),
}
}
}
pub fn attach_context_to_message(
message: &mut LanguageModelRequestMessage,
contexts: impl Iterator<Item = ContextSnapshot>,
) {
let mut file_context = Vec::new();
let mut directory_context = Vec::new();
let mut fetch_context = Vec::new();
let mut thread_context = Vec::new();
let mut capacity = 0;
for context in contexts {
capacity += context.text.len();
match context.kind {
ContextKind::File => file_context.push(context),
ContextKind::Directory => directory_context.push(context),
ContextKind::FetchedUrl => fetch_context.push(context),
ContextKind::Thread => thread_context.push(context),
}
}
if !file_context.is_empty() {
capacity += 1;
}
if !directory_context.is_empty() {
capacity += 1;
}
if !fetch_context.is_empty() {
capacity += 1 + fetch_context.len();
}
if !thread_context.is_empty() {
capacity += 1 + thread_context.len();
}
if capacity == 0 {
return;
}
let mut context_chunks = Vec::with_capacity(capacity);
if !file_context.is_empty() {
context_chunks.push("The following files are available:\n");
for context in &file_context {
for chunk in &context.text {
context_chunks.push(&chunk);
}
}
}
if !directory_context.is_empty() {
context_chunks.push("The following directories are available:\n");
for context in &directory_context {
for chunk in &context.text {
context_chunks.push(&chunk);
}
}
}
if !fetch_context.is_empty() {
context_chunks.push("The following fetched results are available:\n");
for context in &fetch_context {
context_chunks.push(&context.name);
for chunk in &context.text {
context_chunks.push(&chunk);
}
}
}
if !thread_context.is_empty() {
context_chunks.push("The following previous conversation threads are available:\n");
for context in &thread_context {
context_chunks.push(&context.name);
for chunk in &context.text {
context_chunks.push(&chunk);
}
}
}
debug_assert!(
context_chunks.len() == capacity,
"attach_context_message calculated capacity of {}, but length was {}",
capacity,
context_chunks.len()
);
if !context_chunks.is_empty() {
message
.content
.push(MessageContent::Text(context_chunks.join("\n")));
}
}

View File

@@ -1,43 +1,44 @@
mod completion_provider;
mod fetch_context_picker;
mod file_context_picker;
mod symbol_context_picker;
mod thread_context_picker;
use std::ops::Range;
use std::path::PathBuf;
use std::sync::Arc;
use anyhow::{Result, anyhow};
use anyhow::{anyhow, Result};
use editor::display_map::{Crease, FoldId};
use editor::{Anchor, AnchorRangeExt as _, Editor, ExcerptId, FoldPlaceholder, ToOffset};
use file_context_picker::render_file_context_entry;
use gpui::{
App, DismissEvent, Empty, Entity, EventEmitter, FocusHandle, Focusable, Subscription, Task,
WeakEntity,
App, DismissEvent, Empty, Entity, EventEmitter, FocusHandle, Focusable, Task, WeakEntity,
};
use multi_buffer::MultiBufferRow;
use project::{Entry, ProjectPath};
use symbol_context_picker::SymbolContextPicker;
use thread_context_picker::{ThreadContextEntry, render_thread_context_entry};
use project::ProjectPath;
use thread_context_picker::{render_thread_context_entry, ThreadContextEntry};
use ui::{
ButtonLike, ContextMenu, ContextMenuEntry, ContextMenuItem, Disclosure, TintColor, prelude::*,
prelude::*, ButtonLike, ContextMenu, ContextMenuEntry, ContextMenuItem, Disclosure, TintColor,
};
use workspace::{Workspace, notifications::NotifyResultExt};
use workspace::{notifications::NotifyResultExt, Workspace};
use crate::AssistantPanel;
pub use crate::context_picker::completion_provider::ContextPickerCompletionProvider;
use crate::context_picker::fetch_context_picker::FetchContextPicker;
use crate::context_picker::file_context_picker::FileContextPicker;
use crate::context_picker::thread_context_picker::ThreadContextPicker;
use crate::context_store::ContextStore;
use crate::thread::ThreadId;
use crate::thread_store::ThreadStore;
use crate::AssistantPanel;
#[derive(Debug, Clone, Copy)]
pub enum ConfirmBehavior {
KeepOpen,
Close,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum ContextPickerMode {
File,
Symbol,
Fetch,
Thread,
}
@@ -48,7 +49,6 @@ impl TryFrom<&str> for ContextPickerMode {
fn try_from(value: &str) -> Result<Self, Self::Error> {
match value {
"file" => Ok(Self::File),
"symbol" => Ok(Self::Symbol),
"fetch" => Ok(Self::Fetch),
"thread" => Ok(Self::Thread),
_ => Err(format!("Invalid context picker mode: {}", value)),
@@ -60,7 +60,6 @@ impl ContextPickerMode {
pub fn mention_prefix(&self) -> &'static str {
match self {
Self::File => "file",
Self::Symbol => "symbol",
Self::Fetch => "fetch",
Self::Thread => "thread",
}
@@ -69,18 +68,16 @@ impl ContextPickerMode {
pub fn label(&self) -> &'static str {
match self {
Self::File => "Files & Directories",
Self::Symbol => "Symbols",
Self::Fetch => "Fetch",
Self::Thread => "Threads",
Self::Thread => "Thread",
}
}
pub fn icon(&self) -> IconName {
match self {
Self::File => IconName::File,
Self::Symbol => IconName::Code,
Self::Fetch => IconName::Globe,
Self::Thread => IconName::MessageBubbles,
Self::Thread => IconName::MessageCircle,
}
}
}
@@ -89,7 +86,6 @@ impl ContextPickerMode {
enum ContextPickerState {
Default(Entity<ContextMenu>),
File(Entity<FileContextPicker>),
Symbol(Entity<SymbolContextPicker>),
Fetch(Entity<FetchContextPicker>),
Thread(Entity<ThreadContextPicker>),
}
@@ -99,7 +95,7 @@ pub(super) struct ContextPicker {
workspace: WeakEntity<Workspace>,
context_store: WeakEntity<ContextStore>,
thread_store: Option<WeakEntity<ThreadStore>>,
_subscriptions: Vec<Subscription>,
confirm_behavior: ConfirmBehavior,
}
impl ContextPicker {
@@ -107,25 +103,10 @@ impl ContextPicker {
workspace: WeakEntity<Workspace>,
thread_store: Option<WeakEntity<ThreadStore>>,
context_store: WeakEntity<ContextStore>,
confirm_behavior: ConfirmBehavior,
window: &mut Window,
cx: &mut Context<Self>,
) -> Self {
let subscriptions = context_store
.upgrade()
.map(|context_store| {
cx.observe(&context_store, |this, _, cx| this.notify_current_picker(cx))
})
.into_iter()
.chain(
thread_store
.as_ref()
.and_then(|thread_store| thread_store.upgrade())
.map(|thread_store| {
cx.observe(&thread_store, |this, _, cx| this.notify_current_picker(cx))
}),
)
.collect::<Vec<Subscription>>();
ContextPicker {
mode: ContextPickerState::Default(ContextMenu::build(
window,
@@ -135,7 +116,7 @@ impl ContextPicker {
workspace,
context_store,
thread_store,
_subscriptions: subscriptions,
confirm_behavior,
}
}
@@ -157,32 +138,37 @@ impl ContextPicker {
let modes = supported_context_picker_modes(&self.thread_store);
menu.when(has_recent, |menu| {
menu.custom_row(|_, _| {
div()
.mb_1()
.child(
Label::new("Recent")
.color(Color::Muted)
.size(LabelSize::Small),
)
.into_any_element()
})
})
.extend(recent_entries)
.when(has_recent, |menu| menu.separator())
.extend(modes.into_iter().map(|mode| {
let context_picker = context_picker.clone();
ContextMenuEntry::new(mode.label())
.icon(mode.icon())
.icon_size(IconSize::XSmall)
.icon_color(Color::Muted)
.handler(move |window, cx| {
context_picker.update(cx, |this, cx| this.select_mode(mode, window, cx))
let menu = menu
.when(has_recent, |menu| {
menu.custom_row(|_, _| {
div()
.mb_1()
.child(
Label::new("Recent")
.color(Color::Muted)
.size(LabelSize::Small),
)
.into_any_element()
})
}))
.keep_open_on_confirm()
})
.extend(recent_entries)
.when(has_recent, |menu| menu.separator())
.extend(modes.into_iter().map(|mode| {
let context_picker = context_picker.clone();
ContextMenuEntry::new(mode.label())
.icon(mode.icon())
.icon_size(IconSize::XSmall)
.icon_color(Color::Muted)
.handler(move |window, cx| {
context_picker.update(cx, |this, cx| this.select_mode(mode, window, cx))
})
}));
match self.confirm_behavior {
ConfirmBehavior::KeepOpen => menu.keep_open_on_confirm(),
ConfirmBehavior::Close => menu,
}
});
cx.subscribe(&menu, move |_, _, _: &DismissEvent, cx| {
@@ -213,17 +199,7 @@ impl ContextPicker {
context_picker.clone(),
self.workspace.clone(),
self.context_store.clone(),
window,
cx,
)
}));
}
ContextPickerMode::Symbol => {
self.mode = ContextPickerState::Symbol(cx.new(|cx| {
SymbolContextPicker::new(
context_picker.clone(),
self.workspace.clone(),
self.context_store.clone(),
self.confirm_behavior,
window,
cx,
)
@@ -235,6 +211,7 @@ impl ContextPicker {
context_picker.clone(),
self.workspace.clone(),
self.context_store.clone(),
self.confirm_behavior,
window,
cx,
)
@@ -247,6 +224,7 @@ impl ContextPicker {
thread_store.clone(),
context_picker.clone(),
self.context_store.clone(),
self.confirm_behavior,
window,
cx,
)
@@ -271,14 +249,12 @@ impl ContextPicker {
path_prefix,
} => {
let context_store = self.context_store.clone();
let worktree_id = project_path.worktree_id;
let path = project_path.path.clone();
ContextMenuItem::custom_entry(
move |_window, cx| {
render_file_context_entry(
ElementId::NamedInteger("ctx-recent".into(), ix),
worktree_id,
&path,
&path_prefix,
false,
@@ -363,25 +339,73 @@ impl ContextPicker {
}
fn recent_entries(&self, cx: &mut App) -> Vec<RecentEntry> {
let Some(workspace) = self.workspace.upgrade() else {
let Some(workspace) = self.workspace.upgrade().map(|w| w.read(cx)) else {
return vec![];
};
let Some(context_store) = self.context_store.upgrade() else {
let Some(context_store) = self.context_store.upgrade().map(|cs| cs.read(cx)) else {
return vec![];
};
recent_context_picker_entries(context_store, self.thread_store.clone(), workspace, cx)
}
let mut recent = Vec::with_capacity(6);
fn notify_current_picker(&mut self, cx: &mut Context<Self>) {
match &self.mode {
ContextPickerState::Default(entity) => entity.update(cx, |_, cx| cx.notify()),
ContextPickerState::File(entity) => entity.update(cx, |_, cx| cx.notify()),
ContextPickerState::Symbol(entity) => entity.update(cx, |_, cx| cx.notify()),
ContextPickerState::Fetch(entity) => entity.update(cx, |_, cx| cx.notify()),
ContextPickerState::Thread(entity) => entity.update(cx, |_, cx| cx.notify()),
let mut current_files = context_store.file_paths(cx);
if let Some(active_path) = active_singleton_buffer_path(&workspace, cx) {
current_files.insert(active_path);
}
let project = workspace.project().read(cx);
recent.extend(
workspace
.recent_navigation_history_iter(cx)
.filter(|(path, _)| !current_files.contains(&path.path.to_path_buf()))
.take(4)
.filter_map(|(project_path, _)| {
project
.worktree_for_id(project_path.worktree_id, cx)
.map(|worktree| RecentEntry::File {
project_path,
path_prefix: worktree.read(cx).root_name().into(),
})
}),
);
let mut current_threads = context_store.thread_ids();
if let Some(active_thread) = workspace
.panel::<AssistantPanel>(cx)
.map(|panel| panel.read(cx).active_thread(cx))
{
current_threads.insert(active_thread.read(cx).id().clone());
}
let Some(thread_store) = self
.thread_store
.as_ref()
.and_then(|thread_store| thread_store.upgrade())
else {
return recent;
};
thread_store.update(cx, |thread_store, _cx| {
recent.extend(
thread_store
.threads()
.into_iter()
.filter(|thread| !current_threads.contains(&thread.id))
.take(2)
.map(|thread| {
RecentEntry::Thread(ThreadContextEntry {
id: thread.id,
summary: thread.summary,
})
}),
)
});
recent
}
}
@@ -392,7 +416,6 @@ impl Focusable for ContextPicker {
match &self.mode {
ContextPickerState::Default(menu) => menu.focus_handle(cx),
ContextPickerState::File(file_picker) => file_picker.focus_handle(cx),
ContextPickerState::Symbol(symbol_picker) => symbol_picker.focus_handle(cx),
ContextPickerState::Fetch(fetch_picker) => fetch_picker.focus_handle(cx),
ContextPickerState::Thread(thread_picker) => thread_picker.focus_handle(cx),
}
@@ -407,7 +430,6 @@ impl Render for ContextPicker {
.map(|parent| match &self.mode {
ContextPickerState::Default(menu) => parent.child(menu.clone()),
ContextPickerState::File(file_picker) => parent.child(file_picker.clone()),
ContextPickerState::Symbol(symbol_picker) => parent.child(symbol_picker.clone()),
ContextPickerState::Fetch(fetch_picker) => parent.child(fetch_picker.clone()),
ContextPickerState::Thread(thread_picker) => parent.child(thread_picker.clone()),
})
@@ -424,17 +446,23 @@ enum RecentEntry {
fn supported_context_picker_modes(
thread_store: &Option<WeakEntity<ThreadStore>>,
) -> Vec<ContextPickerMode> {
let mut modes = vec![
ContextPickerMode::File,
ContextPickerMode::Symbol,
ContextPickerMode::Fetch,
];
let mut modes = vec![ContextPickerMode::File, ContextPickerMode::Fetch];
if thread_store.is_some() {
modes.push(ContextPickerMode::Thread);
}
modes
}
fn active_singleton_buffer_path(workspace: &Workspace, cx: &App) -> Option<PathBuf> {
let active_item = workspace.active_item(cx)?;
let editor = active_item.to_any().downcast::<Editor>().ok()?.read(cx);
let buffer = editor.buffer().read(cx).as_singleton()?;
let path = buffer.read(cx).file()?.path().to_path_buf();
Some(path)
}
fn recent_context_picker_entries(
context_store: Entity<ContextStore>,
thread_store: Option<WeakEntity<ThreadStore>>,
@@ -443,14 +471,20 @@ fn recent_context_picker_entries(
) -> Vec<RecentEntry> {
let mut recent = Vec::with_capacity(6);
let current_files = context_store.read(cx).file_paths(cx);
let mut current_files = context_store.read(cx).file_paths(cx);
let workspace = workspace.read(cx);
if let Some(active_path) = active_singleton_buffer_path(workspace, cx) {
current_files.insert(active_path);
}
let project = workspace.project().read(cx);
recent.extend(
workspace
.recent_navigation_history_iter(cx)
.filter(|(path, _)| !current_files.contains(path))
.filter(|(path, _)| !current_files.contains(&path.path.to_path_buf()))
.take(4)
.filter_map(|(project_path, _)| {
project
@@ -491,13 +525,14 @@ fn recent_context_picker_entries(
recent
}
pub(crate) fn insert_fold_for_mention(
pub(crate) fn insert_crease_for_mention(
excerpt_id: ExcerptId,
crease_start: text::Anchor,
content_len: usize,
crease_label: SharedString,
crease_icon_path: SharedString,
editor_entity: Entity<Editor>,
window: &mut Window,
cx: &mut App,
) {
editor_entity.update(cx, |editor, cx| {
@@ -507,7 +542,6 @@ pub(crate) fn insert_fold_for_mention(
return;
};
let start = start.bias_right(&snapshot);
let end = snapshot.anchor_before(start.to_offset(&snapshot) + content_len);
let placeholder = FoldPlaceholder {
@@ -516,7 +550,6 @@ pub(crate) fn insert_fold_for_mention(
crease_label,
editor_entity.downgrade(),
),
merge_adjacent: false,
..Default::default()
};
@@ -530,9 +563,8 @@ pub(crate) fn insert_fold_for_mention(
render_trailer,
);
editor.display_map.update(cx, |display_map, cx| {
display_map.fold(vec![crease], cx);
});
editor.insert_creases(vec![crease.clone()], cx);
editor.fold_creases(vec![crease], false, window, cx);
});
}
@@ -589,13 +621,12 @@ fn render_fold_icon_button(
.gap_1()
.child(
Icon::from_path(icon_path.clone())
.size(IconSize::XSmall)
.size(IconSize::Small)
.color(Color::Muted),
)
.child(
Label::new(label.clone())
.size(LabelSize::Small)
.buffer_font(cx)
.single_line(),
),
)
@@ -620,93 +651,3 @@ fn fold_toggle(
.into_any_element()
}
}
pub enum MentionLink {
File(ProjectPath, Entry),
Symbol(ProjectPath, String),
Fetch(String),
Thread(ThreadId),
}
impl MentionLink {
const FILE: &str = "@file";
const SYMBOL: &str = "@symbol";
const THREAD: &str = "@thread";
const FETCH: &str = "@fetch";
const SEPARATOR: &str = ":";
pub fn is_valid(url: &str) -> bool {
url.starts_with(Self::FILE)
|| url.starts_with(Self::SYMBOL)
|| url.starts_with(Self::FETCH)
|| url.starts_with(Self::THREAD)
}
pub fn for_file(file_name: &str, full_path: &str) -> String {
format!("[@{}]({}:{})", file_name, Self::FILE, full_path)
}
pub fn for_symbol(symbol_name: &str, full_path: &str) -> String {
format!(
"[@{}]({}:{}:{})",
symbol_name,
Self::SYMBOL,
full_path,
symbol_name
)
}
pub fn for_fetch(url: &str) -> String {
format!("[@{}]({}:{})", url, Self::FETCH, url)
}
pub fn for_thread(thread: &ThreadContextEntry) -> String {
format!("[@{}]({}:{})", thread.summary, Self::THREAD, thread.id)
}
pub fn try_parse(link: &str, workspace: &Entity<Workspace>, cx: &App) -> Option<Self> {
fn extract_project_path_from_link(
path: &str,
workspace: &Entity<Workspace>,
cx: &App,
) -> Option<ProjectPath> {
let path = PathBuf::from(path);
let worktree_name = path.iter().next()?;
let path: PathBuf = path.iter().skip(1).collect();
let worktree_id = workspace
.read(cx)
.visible_worktrees(cx)
.find(|worktree| worktree.read(cx).root_name() == worktree_name)
.map(|worktree| worktree.read(cx).id())?;
Some(ProjectPath {
worktree_id,
path: path.into(),
})
}
let (prefix, argument) = link.split_once(Self::SEPARATOR)?;
match prefix {
Self::FILE => {
let project_path = extract_project_path_from_link(argument, workspace, cx)?;
let entry = workspace
.read(cx)
.project()
.read(cx)
.entry_for_path(&project_path, cx)?;
Some(MentionLink::File(project_path, entry))
}
Self::SYMBOL => {
let (path, symbol) = argument.split_once(Self::SEPARATOR)?;
let project_path = extract_project_path_from_link(path, workspace, cx)?;
Some(MentionLink::Symbol(project_path, symbol.to_string()))
}
Self::THREAD => {
let thread_id = ThreadId::from(argument);
Some(MentionLink::Thread(thread_id))
}
Self::FETCH => Some(MentionLink::Fetch(argument.to_string())),
_ => None,
}
}
}

View File

@@ -2,8 +2,8 @@ use std::cell::RefCell;
use std::ops::Range;
use std::path::Path;
use std::rc::Rc;
use std::sync::Arc;
use std::sync::atomic::AtomicBool;
use std::sync::Arc;
use anyhow::Result;
use editor::{CompletionProvider, Editor, ExcerptId};
@@ -12,138 +12,19 @@ use gpui::{App, Entity, Task, WeakEntity};
use http_client::HttpClientWithUrl;
use language::{Buffer, CodeLabel, HighlightId};
use lsp::CompletionContext;
use project::{Completion, CompletionIntent, ProjectPath, Symbol, WorktreeId};
use project::{Completion, CompletionIntent, ProjectPath, WorktreeId};
use rope::Point;
use text::{Anchor, ToPoint};
use ui::prelude::*;
use workspace::Workspace;
use crate::context_picker::file_context_picker::search_files;
use crate::context_picker::symbol_context_picker::search_symbols;
use crate::context::AssistantContext;
use crate::context_store::ContextStore;
use crate::thread_store::ThreadStore;
use super::fetch_context_picker::fetch_url_content;
use super::file_context_picker::FileMatch;
use super::symbol_context_picker::SymbolMatch;
use super::thread_context_picker::{ThreadContextEntry, ThreadMatch, search_threads};
use super::{
ContextPickerMode, MentionLink, RecentEntry, recent_context_picker_entries,
supported_context_picker_modes,
};
pub(crate) enum Match {
Symbol(SymbolMatch),
File(FileMatch),
Thread(ThreadMatch),
Fetch(SharedString),
Mode(ContextPickerMode),
}
fn search(
mode: Option<ContextPickerMode>,
query: String,
cancellation_flag: Arc<AtomicBool>,
recent_entries: Vec<RecentEntry>,
thread_store: Option<WeakEntity<ThreadStore>>,
workspace: Entity<Workspace>,
cx: &mut App,
) -> Task<Vec<Match>> {
match mode {
Some(ContextPickerMode::File) => {
let search_files_task =
search_files(query.clone(), cancellation_flag.clone(), &workspace, cx);
cx.background_spawn(async move {
search_files_task
.await
.into_iter()
.map(Match::File)
.collect()
})
}
Some(ContextPickerMode::Symbol) => {
let search_symbols_task =
search_symbols(query.clone(), cancellation_flag.clone(), &workspace, cx);
cx.background_spawn(async move {
search_symbols_task
.await
.into_iter()
.map(Match::Symbol)
.collect()
})
}
Some(ContextPickerMode::Thread) => {
if let Some(thread_store) = thread_store.as_ref().and_then(|t| t.upgrade()) {
let search_threads_task =
search_threads(query.clone(), cancellation_flag.clone(), thread_store, cx);
cx.background_spawn(async move {
search_threads_task
.await
.into_iter()
.map(Match::Thread)
.collect()
})
} else {
Task::ready(Vec::new())
}
}
Some(ContextPickerMode::Fetch) => {
if !query.is_empty() {
Task::ready(vec![Match::Fetch(query.into())])
} else {
Task::ready(Vec::new())
}
}
None => {
if query.is_empty() {
let mut matches = recent_entries
.into_iter()
.map(|entry| match entry {
super::RecentEntry::File {
project_path,
path_prefix,
} => Match::File(FileMatch {
mat: fuzzy::PathMatch {
score: 1.,
positions: Vec::new(),
worktree_id: project_path.worktree_id.to_usize(),
path: project_path.path,
path_prefix,
is_dir: false,
distance_to_relative_ancestor: 0,
},
is_recent: true,
}),
super::RecentEntry::Thread(thread_context_entry) => {
Match::Thread(ThreadMatch {
thread: thread_context_entry,
is_recent: true,
})
}
})
.collect::<Vec<_>>();
matches.extend(
supported_context_picker_modes(&thread_store)
.into_iter()
.map(Match::Mode),
);
Task::ready(matches)
} else {
let search_files_task =
search_files(query.clone(), cancellation_flag.clone(), &workspace, cx);
cx.background_spawn(async move {
search_files_task
.await
.into_iter()
.map(Match::File)
.collect()
})
}
}
}
}
use super::thread_context_picker::ThreadContextEntry;
use super::{recent_context_picker_entries, supported_context_picker_modes, ContextPickerMode};
pub struct ContextPickerCompletionProvider {
workspace: WeakEntity<Workspace>,
@@ -167,20 +48,96 @@ impl ContextPickerCompletionProvider {
}
}
fn completion_for_mode(source_range: Range<Anchor>, mode: ContextPickerMode) -> Completion {
Completion {
replace_range: source_range.clone(),
new_text: format!("@{} ", mode.mention_prefix()),
label: CodeLabel::plain(mode.label().to_string(), None),
icon_path: Some(mode.icon().path().into()),
documentation: None,
source: project::CompletionSource::Custom,
insert_text_mode: None,
// This ensures that when a user accepts this completion, the
// completion menu will still be shown after "@category " is
// inserted
confirm: Some(Arc::new(|_, _, _| true)),
fn default_completions(
excerpt_id: ExcerptId,
source_range: Range<Anchor>,
context_store: Entity<ContextStore>,
thread_store: Option<WeakEntity<ThreadStore>>,
editor: Entity<Editor>,
workspace: Entity<Workspace>,
cx: &App,
) -> Vec<Completion> {
let mut completions = Vec::new();
completions.extend(
recent_context_picker_entries(
context_store.clone(),
thread_store.clone(),
workspace.clone(),
cx,
)
.iter()
.filter_map(|entry| match entry {
super::RecentEntry::File {
project_path,
path_prefix,
} => Some(Self::completion_for_path(
project_path.clone(),
path_prefix,
true,
false,
excerpt_id,
source_range.clone(),
editor.clone(),
context_store.clone(),
cx,
)),
super::RecentEntry::Thread(thread_context_entry) => {
let thread_store = thread_store
.as_ref()
.and_then(|thread_store| thread_store.upgrade())?;
Some(Self::completion_for_thread(
thread_context_entry.clone(),
excerpt_id,
source_range.clone(),
true,
editor.clone(),
context_store.clone(),
thread_store,
))
}
}),
);
completions.extend(
supported_context_picker_modes(&thread_store)
.iter()
.map(|mode| {
Completion {
old_range: source_range.clone(),
new_text: format!("@{} ", mode.mention_prefix()),
label: CodeLabel::plain(mode.label().to_string(), None),
icon_path: Some(mode.icon().path().into()),
documentation: None,
source: project::CompletionSource::Custom,
// This ensures that when a user accepts this completion, the
// completion menu will still be shown after "@category " is
// inserted
confirm: Some(Arc::new(|_, _, _| true)),
}
}),
);
completions
}
fn build_code_label_for_full_path(
file_name: &str,
directory: Option<&str>,
cx: &App,
) -> CodeLabel {
let comment_id = cx.theme().syntax().highlight_id("comment").map(HighlightId);
let mut label = CodeLabel::default();
label.push_str(&file_name, None);
label.push_str(" ", None);
if let Some(directory) = directory {
label.push_str(&directory, comment_id);
}
label.filter_range = 0..label.text().len();
label
}
fn completion_for_thread(
@@ -195,20 +152,19 @@ impl ContextPickerCompletionProvider {
let icon_for_completion = if recent {
IconName::HistoryRerun
} else {
IconName::MessageBubbles
IconName::MessageCircle
};
let new_text = MentionLink::for_thread(&thread_entry);
let new_text = format!("@thread {}", thread_entry.summary);
let new_text_len = new_text.len();
Completion {
replace_range: source_range.clone(),
old_range: source_range.clone(),
new_text,
label: CodeLabel::plain(thread_entry.summary.to_string(), None),
documentation: None,
insert_text_mode: None,
source: project::CompletionSource::Custom,
icon_path: Some(icon_for_completion.path().into()),
confirm: Some(confirm_completion_callback(
IconName::MessageBubbles.path().into(),
IconName::MessageCircle.path().into(),
thread_entry.summary.clone(),
excerpt_id,
source_range.start,
@@ -242,16 +198,15 @@ impl ContextPickerCompletionProvider {
context_store: Entity<ContextStore>,
http_client: Arc<HttpClientWithUrl>,
) -> Completion {
let new_text = MentionLink::for_fetch(&url_to_fetch);
let new_text = format!("@fetch {}", url_to_fetch);
let new_text_len = new_text.len();
Completion {
replace_range: source_range.clone(),
old_range: source_range.clone(),
new_text,
label: CodeLabel::plain(url_to_fetch.to_string(), None),
documentation: None,
source: project::CompletionSource::Custom,
icon_path: Some(IconName::Globe.path().into()),
insert_text_mode: None,
confirm: Some(confirm_completion_callback(
IconName::Globe.path().into(),
url_to_fetch.clone(),
@@ -275,8 +230,8 @@ impl ContextPickerCompletionProvider {
url_to_fetch.to_string(),
))
.await?;
context_store.update(cx, |context_store, cx| {
context_store.add_fetched_url(url_to_fetch.to_string(), content, cx)
context_store.update(cx, |context_store, _| {
context_store.add_fetched_url(url_to_fetch.to_string(), content)
})
})
.detach_and_log_err(cx);
@@ -301,8 +256,11 @@ impl ContextPickerCompletionProvider {
path_prefix,
);
let label =
build_code_label_for_full_path(&file_name, directory.as_ref().map(|s| s.as_ref()), cx);
let label = Self::build_code_label_for_full_path(
&file_name,
directory.as_ref().map(|s| s.as_ref()),
cx,
);
let full_path = if let Some(directory) = directory {
format!("{}{}", directory, file_name)
} else {
@@ -321,16 +279,15 @@ impl ContextPickerCompletionProvider {
crease_icon_path.clone()
};
let new_text = MentionLink::for_file(&file_name, &full_path);
let new_text = format!("@file {}", full_path);
let new_text_len = new_text.len();
Completion {
replace_range: source_range.clone(),
old_range: source_range.clone(),
new_text,
label,
documentation: None,
source: project::CompletionSource::Custom,
icon_path: Some(completion_icon_path),
insert_text_mode: None,
confirm: Some(confirm_completion_callback(
crease_icon_path,
file_name,
@@ -351,88 +308,6 @@ impl ContextPickerCompletionProvider {
)),
}
}
fn completion_for_symbol(
symbol: Symbol,
excerpt_id: ExcerptId,
source_range: Range<Anchor>,
editor: Entity<Editor>,
context_store: Entity<ContextStore>,
workspace: Entity<Workspace>,
cx: &mut App,
) -> Option<Completion> {
let path_prefix = workspace
.read(cx)
.project()
.read(cx)
.worktree_for_id(symbol.path.worktree_id, cx)?
.read(cx)
.root_name();
let (file_name, directory) = super::file_context_picker::extract_file_name_and_directory(
&symbol.path.path,
path_prefix,
);
let full_path = if let Some(directory) = directory {
format!("{}{}", directory, file_name)
} else {
file_name.to_string()
};
let comment_id = cx.theme().syntax().highlight_id("comment").map(HighlightId);
let mut label = CodeLabel::plain(symbol.name.clone(), None);
label.push_str(" ", None);
label.push_str(&file_name, comment_id);
let new_text = MentionLink::for_symbol(&symbol.name, &full_path);
let new_text_len = new_text.len();
Some(Completion {
replace_range: source_range.clone(),
new_text,
label,
documentation: None,
source: project::CompletionSource::Custom,
icon_path: Some(IconName::Code.path().into()),
insert_text_mode: None,
confirm: Some(confirm_completion_callback(
IconName::Code.path().into(),
symbol.name.clone().into(),
excerpt_id,
source_range.start,
new_text_len,
editor.clone(),
move |cx| {
let symbol = symbol.clone();
let context_store = context_store.clone();
let workspace = workspace.clone();
super::symbol_context_picker::add_symbol(
symbol.clone(),
false,
workspace.clone(),
context_store.downgrade(),
cx,
)
.detach_and_log_err(cx);
},
)),
})
}
}
fn build_code_label_for_full_path(file_name: &str, directory: Option<&str>, cx: &App) -> CodeLabel {
let comment_id = cx.theme().syntax().highlight_id("comment").map(HighlightId);
let mut label = CodeLabel::default();
label.push_str(&file_name, None);
label.push_str(" ", None);
if let Some(directory) = directory {
label.push_str(&directory, comment_id);
}
label.filter_range = 0..label.text().len();
label
}
impl CompletionProvider for ContextPickerCompletionProvider {
@@ -457,103 +332,144 @@ impl CompletionProvider for ContextPickerCompletionProvider {
return Task::ready(Ok(None));
};
let Some((workspace, context_store)) =
self.workspace.upgrade().zip(self.context_store.upgrade())
else {
let Some(workspace) = self.workspace.upgrade() else {
return Task::ready(Ok(None));
};
let Some(context_store) = self.context_store.upgrade() else {
return Task::ready(Ok(None));
};
let snapshot = buffer.read(cx).snapshot();
let source_range = snapshot.anchor_before(state.source_range.start)
let source_range = snapshot.anchor_after(state.source_range.start)
..snapshot.anchor_before(state.source_range.end);
let thread_store = self.thread_store.clone();
let editor = self.editor.clone();
let http_client = workspace.read(cx).client().http_client().clone();
let MentionCompletion { mode, argument, .. } = state;
let query = argument.unwrap_or_else(|| "".to_string());
let recent_entries = recent_context_picker_entries(
context_store.clone(),
thread_store.clone(),
workspace.clone(),
cx,
);
let search_task = search(
mode,
query,
Arc::<AtomicBool>::default(),
recent_entries,
thread_store.clone(),
workspace.clone(),
cx,
);
cx.spawn(async move |_, cx| {
let matches = search_task.await;
let Some(editor) = editor.upgrade() else {
return Ok(None);
};
let mut completions = Vec::new();
Ok(Some(cx.update(|cx| {
matches
.into_iter()
.filter_map(|mat| match mat {
Match::File(FileMatch { mat, is_recent }) => {
Some(Self::completion_for_path(
ProjectPath {
worktree_id: WorktreeId::from_usize(mat.worktree_id),
path: mat.path.clone(),
},
&mat.path_prefix,
is_recent,
mat.is_dir,
excerpt_id,
source_range.clone(),
editor.clone(),
context_store.clone(),
let MentionCompletion {
mode: category,
argument,
..
} = state;
let query = argument.unwrap_or_else(|| "".to_string());
match category {
Some(ContextPickerMode::File) => {
let path_matches = cx
.update(|cx| {
super::file_context_picker::search_paths(
query,
Arc::<AtomicBool>::default(),
&workspace,
cx,
))
}
Match::Symbol(SymbolMatch { symbol, .. }) => Self::completion_for_symbol(
symbol,
excerpt_id,
source_range.clone(),
editor.clone(),
context_store.clone(),
workspace.clone(),
cx,
),
Match::Thread(ThreadMatch {
thread, is_recent, ..
}) => {
let thread_store = thread_store.as_ref().and_then(|t| t.upgrade())?;
Some(Self::completion_for_thread(
thread,
excerpt_id,
)
})?
.await;
if let Some(editor) = editor.upgrade() {
completions.reserve(path_matches.len());
cx.update(|cx| {
completions.extend(path_matches.iter().map(|mat| {
Self::completion_for_path(
ProjectPath {
worktree_id: WorktreeId::from_usize(mat.worktree_id),
path: mat.path.clone(),
},
&mat.path_prefix,
false,
mat.is_dir,
excerpt_id,
source_range.clone(),
editor.clone(),
context_store.clone(),
cx,
)
}));
})?;
}
}
Some(ContextPickerMode::Fetch) => {
if let Some(editor) = editor.upgrade() {
if !query.is_empty() {
completions.push(Self::completion_for_fetch(
source_range.clone(),
is_recent,
query.into(),
excerpt_id,
editor.clone(),
context_store.clone(),
thread_store,
))
http_client.clone(),
));
}
Match::Fetch(url) => Some(Self::completion_for_fetch(
source_range.clone(),
url,
excerpt_id,
editor.clone(),
context_store.clone(),
http_client.clone(),
)),
Match::Mode(mode) => {
Some(Self::completion_for_mode(source_range.clone(), mode))
context_store.update(cx, |store, _| {
let urls = store.context().iter().filter_map(|context| {
if let AssistantContext::FetchedUrl(context) = context {
Some(context.url.clone())
} else {
None
}
});
for url in urls {
completions.push(Self::completion_for_fetch(
source_range.clone(),
url,
excerpt_id,
editor.clone(),
context_store.clone(),
http_client.clone(),
));
}
})?;
}
}
Some(ContextPickerMode::Thread) => {
if let Some((thread_store, editor)) = thread_store
.and_then(|thread_store| thread_store.upgrade())
.zip(editor.upgrade())
{
let threads = cx
.update(|cx| {
super::thread_context_picker::search_threads(
query,
thread_store.clone(),
cx,
)
})?
.await;
for thread in threads {
completions.push(Self::completion_for_thread(
thread.clone(),
excerpt_id,
source_range.clone(),
false,
editor.clone(),
context_store.clone(),
thread_store.clone(),
));
}
})
.collect()
})?))
}
}
None => {
cx.update(|cx| {
if let Some(editor) = editor.upgrade() {
completions.extend(Self::default_completions(
excerpt_id,
source_range.clone(),
context_store.clone(),
thread_store.clone(),
editor,
workspace.clone(),
cx,
));
}
})?;
}
}
Ok(Some(completions))
})
}
@@ -610,20 +526,21 @@ fn confirm_completion_callback(
editor: Entity<Editor>,
add_context_fn: impl Fn(&mut App) -> () + Send + Sync + 'static,
) -> Arc<dyn Fn(CompletionIntent, &mut Window, &mut App) -> bool + Send + Sync> {
Arc::new(move |_, _, cx| {
Arc::new(move |_, window, cx| {
add_context_fn(cx);
let crease_text = crease_text.clone();
let crease_icon_path = crease_icon_path.clone();
let editor = editor.clone();
cx.defer(move |cx| {
crate::context_picker::insert_fold_for_mention(
window.defer(cx, move |window, cx| {
crate::context_picker::insert_crease_for_mention(
excerpt_id,
start,
content_len,
crease_text,
crease_icon_path,
editor,
window,
cx,
);
});
@@ -644,15 +561,6 @@ impl MentionCompletion {
if last_mention_start >= line.len() {
return Some(Self::default());
}
if last_mention_start > 0
&& line
.chars()
.nth(last_mention_start - 1)
.map_or(false, |c| !c.is_whitespace())
{
return None;
}
let rest_of_line = &line[last_mention_start + 1..];
let mut mode = None;
@@ -662,12 +570,7 @@ impl MentionCompletion {
let mut end = last_mention_start + 1;
if let Some(mode_text) = parts.next() {
end += mode_text.len();
if let Some(parsed_mode) = ContextPickerMode::try_from(mode_text).ok() {
mode = Some(parsed_mode);
} else {
argument = Some(mode_text.to_string());
}
mode = ContextPickerMode::try_from(mode_text).ok();
match rest_of_line[mode_text.len()..].find(|c: char| !c.is_whitespace()) {
Some(whitespace_count) => {
if let Some(argument_text) = parts.next() {
@@ -693,14 +596,13 @@ impl MentionCompletion {
#[cfg(test)]
mod tests {
use super::*;
use editor::AnchorRangeExt;
use gpui::{EventEmitter, FocusHandle, Focusable, TestAppContext, VisualTestContext};
use gpui::{Focusable, TestAppContext, VisualTestContext};
use project::{Project, ProjectPath};
use serde_json::json;
use settings::SettingsStore;
use std::ops::Deref;
use std::{ops::Deref, path::PathBuf};
use util::{path, separator};
use workspace::{AppState, Item};
use workspace::AppState;
#[test]
fn test_mention_completion_parse() {
@@ -759,41 +661,6 @@ mod tests {
argument: Some("main.rs".to_string()),
})
);
assert_eq!(
MentionCompletion::try_parse("Lorem @main", 0),
Some(MentionCompletion {
source_range: 6..11,
mode: None,
argument: Some("main".to_string()),
})
);
assert_eq!(MentionCompletion::try_parse("test@", 0), None);
}
struct AtMentionEditor(Entity<Editor>);
impl Item for AtMentionEditor {
type Event = ();
fn include_in_nav_history() -> bool {
false
}
}
impl EventEmitter<()> for AtMentionEditor {}
impl Focusable for AtMentionEditor {
fn focus_handle(&self, cx: &App) -> FocusHandle {
self.0.read(cx).focus_handle(cx).clone()
}
}
impl Render for AtMentionEditor {
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
self.0.clone().into_any_element()
}
}
#[gpui::test]
@@ -871,30 +738,28 @@ mod tests {
.unwrap();
}
let editor = workspace.update_in(&mut cx, |workspace, window, cx| {
let editor = cx.new(|cx| {
Editor::new(
editor::EditorMode::full(),
multi_buffer::MultiBuffer::build_simple("", cx),
let item = workspace
.update_in(&mut cx, |workspace, window, cx| {
workspace.open_path(
ProjectPath {
worktree_id,
path: PathBuf::from("editor").into(),
},
None,
true,
window,
cx,
)
});
workspace.active_pane().update(cx, |pane, cx| {
pane.add_item(
Box::new(cx.new(|_| AtMentionEditor(editor.clone()))),
true,
true,
None,
window,
cx,
);
});
editor
})
.await
.expect("Could not open test file");
let editor = cx.update(|_, cx| {
item.act_as::<Editor>(cx)
.expect("Opened test file wasn't an editor")
});
let context_store = cx.new(|_| ContextStore::new(project.downgrade(), None));
let context_store = cx.new(|_| ContextStore::new(workspace.downgrade()));
let editor_entity = editor.downgrade();
editor.update_in(&mut cx, |editor, window, cx| {
@@ -927,7 +792,6 @@ mod tests {
"five.txt dir/b/",
"four.txt dir/a/",
"Files & Directories",
"Symbols",
"Fetch"
]
);
@@ -964,50 +828,44 @@ mod tests {
});
editor.update(&mut cx, |editor, cx| {
assert_eq!(editor.text(cx), "Lorem [@one.txt](@file:dir/a/one.txt)",);
assert_eq!(editor.text(cx), "Lorem @file dir/a/one.txt",);
assert!(!editor.has_visible_completions_menu());
assert_eq!(
fold_ranges(editor, cx),
vec![Point::new(0, 6)..Point::new(0, 37)]
crease_ranges(editor, cx),
vec![Point::new(0, 6)..Point::new(0, 25)]
);
});
cx.simulate_input(" ");
editor.update(&mut cx, |editor, cx| {
assert_eq!(editor.text(cx), "Lorem [@one.txt](@file:dir/a/one.txt) ",);
assert_eq!(editor.text(cx), "Lorem @file dir/a/one.txt ",);
assert!(!editor.has_visible_completions_menu());
assert_eq!(
fold_ranges(editor, cx),
vec![Point::new(0, 6)..Point::new(0, 37)]
crease_ranges(editor, cx),
vec![Point::new(0, 6)..Point::new(0, 25)]
);
});
cx.simulate_input("Ipsum ");
editor.update(&mut cx, |editor, cx| {
assert_eq!(
editor.text(cx),
"Lorem [@one.txt](@file:dir/a/one.txt) Ipsum ",
);
assert_eq!(editor.text(cx), "Lorem @file dir/a/one.txt Ipsum ",);
assert!(!editor.has_visible_completions_menu());
assert_eq!(
fold_ranges(editor, cx),
vec![Point::new(0, 6)..Point::new(0, 37)]
crease_ranges(editor, cx),
vec![Point::new(0, 6)..Point::new(0, 25)]
);
});
cx.simulate_input("@file ");
editor.update(&mut cx, |editor, cx| {
assert_eq!(
editor.text(cx),
"Lorem [@one.txt](@file:dir/a/one.txt) Ipsum @file ",
);
assert_eq!(editor.text(cx), "Lorem @file dir/a/one.txt Ipsum @file ",);
assert!(editor.has_visible_completions_menu());
assert_eq!(
fold_ranges(editor, cx),
vec![Point::new(0, 6)..Point::new(0, 37)]
crease_ranges(editor, cx),
vec![Point::new(0, 6)..Point::new(0, 25)]
);
});
@@ -1020,14 +878,14 @@ mod tests {
editor.update(&mut cx, |editor, cx| {
assert_eq!(
editor.text(cx),
"Lorem [@one.txt](@file:dir/a/one.txt) Ipsum [@seven.txt](@file:dir/b/seven.txt)"
"Lorem @file dir/a/one.txt Ipsum @file dir/b/seven.txt"
);
assert!(!editor.has_visible_completions_menu());
assert_eq!(
fold_ranges(editor, cx),
crease_ranges(editor, cx),
vec![
Point::new(0, 6)..Point::new(0, 37),
Point::new(0, 44)..Point::new(0, 79)
Point::new(0, 6)..Point::new(0, 25),
Point::new(0, 32)..Point::new(0, 53)
]
);
});
@@ -1037,14 +895,14 @@ mod tests {
editor.update(&mut cx, |editor, cx| {
assert_eq!(
editor.text(cx),
"Lorem [@one.txt](@file:dir/a/one.txt) Ipsum [@seven.txt](@file:dir/b/seven.txt)\n@"
"Lorem @file dir/a/one.txt Ipsum @file dir/b/seven.txt\n@"
);
assert!(editor.has_visible_completions_menu());
assert_eq!(
fold_ranges(editor, cx),
crease_ranges(editor, cx),
vec![
Point::new(0, 6)..Point::new(0, 37),
Point::new(0, 44)..Point::new(0, 79)
Point::new(0, 6)..Point::new(0, 25),
Point::new(0, 32)..Point::new(0, 53)
]
);
});
@@ -1058,27 +916,29 @@ mod tests {
editor.update(&mut cx, |editor, cx| {
assert_eq!(
editor.text(cx),
"Lorem [@one.txt](@file:dir/a/one.txt) Ipsum [@seven.txt](@file:dir/b/seven.txt)\n[@six.txt](@file:dir/b/six.txt)"
"Lorem @file dir/a/one.txt Ipsum @file dir/b/seven.txt\n@file dir/b/six.txt"
);
assert!(!editor.has_visible_completions_menu());
assert_eq!(
fold_ranges(editor, cx),
crease_ranges(editor, cx),
vec![
Point::new(0, 6)..Point::new(0, 37),
Point::new(0, 44)..Point::new(0, 79),
Point::new(1, 0)..Point::new(1, 31)
Point::new(0, 6)..Point::new(0, 25),
Point::new(0, 32)..Point::new(0, 53),
Point::new(1, 0)..Point::new(1, 19)
]
);
});
}
fn fold_ranges(editor: &Editor, cx: &mut App) -> Vec<Range<Point>> {
fn crease_ranges(editor: &Editor, cx: &mut App) -> Vec<Range<Point>> {
let snapshot = editor.buffer().read(cx).snapshot(cx);
editor.display_map.update(cx, |display_map, cx| {
display_map
.snapshot(cx)
.folds_in_range(0..snapshot.len())
.map(|fold| fold.range.to_point(&snapshot))
.crease_snapshot
.crease_items_with_offsets(&snapshot)
.into_iter()
.map(|(_, range)| range)
.collect()
})
}

View File

@@ -2,16 +2,16 @@ use std::cell::RefCell;
use std::rc::Rc;
use std::sync::Arc;
use anyhow::{Context as _, Result, bail};
use anyhow::{bail, Context as _, Result};
use futures::AsyncReadExt as _;
use gpui::{App, DismissEvent, Entity, FocusHandle, Focusable, Task, WeakEntity};
use html_to_markdown::{TagHandler, convert_html_to_markdown, markdown};
use html_to_markdown::{convert_html_to_markdown, markdown, TagHandler};
use http_client::{AsyncBody, HttpClientWithUrl};
use picker::{Picker, PickerDelegate};
use ui::{Context, ListItem, Window, prelude::*};
use ui::{prelude::*, Context, ListItem, Window};
use workspace::Workspace;
use crate::context_picker::ContextPicker;
use crate::context_picker::{ConfirmBehavior, ContextPicker};
use crate::context_store::ContextStore;
pub struct FetchContextPicker {
@@ -23,10 +23,16 @@ impl FetchContextPicker {
context_picker: WeakEntity<ContextPicker>,
workspace: WeakEntity<Workspace>,
context_store: WeakEntity<ContextStore>,
confirm_behavior: ConfirmBehavior,
window: &mut Window,
cx: &mut Context<Self>,
) -> Self {
let delegate = FetchContextPickerDelegate::new(context_picker, workspace, context_store);
let delegate = FetchContextPickerDelegate::new(
context_picker,
workspace,
context_store,
confirm_behavior,
);
let picker = cx.new(|cx| Picker::uniform_list(delegate, window, cx));
Self { picker }
@@ -56,6 +62,7 @@ pub struct FetchContextPickerDelegate {
context_picker: WeakEntity<ContextPicker>,
workspace: WeakEntity<Workspace>,
context_store: WeakEntity<ContextStore>,
confirm_behavior: ConfirmBehavior,
url: String,
}
@@ -64,11 +71,13 @@ impl FetchContextPickerDelegate {
context_picker: WeakEntity<ContextPicker>,
workspace: WeakEntity<Workspace>,
context_store: WeakEntity<ContextStore>,
confirm_behavior: ConfirmBehavior,
) -> Self {
FetchContextPickerDelegate {
context_picker,
workspace,
context_store,
confirm_behavior,
url: String::new(),
}
}
@@ -154,7 +163,11 @@ impl PickerDelegate for FetchContextPickerDelegate {
type ListItem = ListItem;
fn match_count(&self) -> usize {
if self.url.is_empty() { 0 } else { 1 }
if self.url.is_empty() {
0
} else {
1
}
}
fn no_matches_text(&self, _window: &mut Window, _cx: &mut App) -> Option<SharedString> {
@@ -195,15 +208,25 @@ impl PickerDelegate for FetchContextPickerDelegate {
let http_client = workspace.read(cx).client().http_client().clone();
let url = self.url.clone();
let confirm_behavior = self.confirm_behavior;
cx.spawn_in(window, async move |this, cx| {
let text = cx
.background_spawn(fetch_url_content(http_client, url.clone()))
.await?;
this.update(cx, |this, cx| {
this.delegate.context_store.update(cx, |context_store, cx| {
context_store.add_fetched_url(url, text, cx)
})
this.update_in(cx, |this, window, cx| {
this.delegate
.context_store
.update(cx, |context_store, _cx| {
context_store.add_fetched_url(url, text);
})?;
match confirm_behavior {
ConfirmBehavior::KeepOpen => {}
ConfirmBehavior::Close => this.delegate.dismissed(window, cx),
}
anyhow::Ok(())
})??;
anyhow::Ok(())

View File

@@ -1,6 +1,6 @@
use std::path::Path;
use std::sync::Arc;
use std::sync::atomic::AtomicBool;
use std::sync::Arc;
use file_icons::FileIcons;
use fuzzy::PathMatch;
@@ -9,11 +9,11 @@ use gpui::{
};
use picker::{Picker, PickerDelegate};
use project::{PathMatchCandidateSet, ProjectPath, WorktreeId};
use ui::{ListItem, Tooltip, prelude::*};
use ui::{prelude::*, ListItem, Tooltip};
use util::ResultExt as _;
use workspace::Workspace;
use workspace::{notifications::NotifyResultExt, Workspace};
use crate::context_picker::ContextPicker;
use crate::context_picker::{ConfirmBehavior, ContextPicker};
use crate::context_store::{ContextStore, FileInclusion};
pub struct FileContextPicker {
@@ -25,10 +25,16 @@ impl FileContextPicker {
context_picker: WeakEntity<ContextPicker>,
workspace: WeakEntity<Workspace>,
context_store: WeakEntity<ContextStore>,
confirm_behavior: ConfirmBehavior,
window: &mut Window,
cx: &mut Context<Self>,
) -> Self {
let delegate = FileContextPickerDelegate::new(context_picker, workspace, context_store);
let delegate = FileContextPickerDelegate::new(
context_picker,
workspace,
context_store,
confirm_behavior,
);
let picker = cx.new(|cx| Picker::uniform_list(delegate, window, cx));
Self { picker }
@@ -51,7 +57,8 @@ pub struct FileContextPickerDelegate {
context_picker: WeakEntity<ContextPicker>,
workspace: WeakEntity<Workspace>,
context_store: WeakEntity<ContextStore>,
matches: Vec<FileMatch>,
confirm_behavior: ConfirmBehavior,
matches: Vec<PathMatch>,
selected_index: usize,
}
@@ -60,11 +67,13 @@ impl FileContextPickerDelegate {
context_picker: WeakEntity<ContextPicker>,
workspace: WeakEntity<Workspace>,
context_store: WeakEntity<ContextStore>,
confirm_behavior: ConfirmBehavior,
) -> Self {
Self {
context_picker,
workspace,
context_store,
confirm_behavior,
matches: Vec::new(),
selected_index: 0,
}
@@ -105,7 +114,7 @@ impl PickerDelegate for FileContextPickerDelegate {
return Task::ready(());
};
let search_task = search_files(query, Arc::<AtomicBool>::default(), &workspace, cx);
let search_task = search_paths(query, Arc::<AtomicBool>::default(), &workspace, cx);
cx.spawn_in(window, async move |this, cx| {
// TODO: This should be probably be run in the background.
@@ -118,8 +127,8 @@ impl PickerDelegate for FileContextPickerDelegate {
})
}
fn confirm(&mut self, _secondary: bool, _window: &mut Window, cx: &mut Context<Picker<Self>>) {
let Some(FileMatch { mat, .. }) = self.matches.get(self.selected_index) else {
fn confirm(&mut self, _secondary: bool, window: &mut Window, cx: &mut Context<Picker<Self>>) {
let Some(mat) = self.matches.get(self.selected_index) else {
return;
};
@@ -144,7 +153,17 @@ impl PickerDelegate for FileContextPickerDelegate {
return;
};
task.detach_and_log_err(cx);
let confirm_behavior = self.confirm_behavior;
cx.spawn_in(window, async move |this, cx| {
match task.await.notify_async_err(cx) {
None => anyhow::Ok(()),
Some(()) => this.update_in(cx, |this, window, cx| match confirm_behavior {
ConfirmBehavior::KeepOpen => {}
ConfirmBehavior::Close => this.delegate.dismissed(window, cx),
}),
}
})
.detach_and_log_err(cx);
}
fn dismissed(&mut self, _: &mut Window, cx: &mut Context<Picker<Self>>) {
@@ -162,7 +181,7 @@ impl PickerDelegate for FileContextPickerDelegate {
_window: &mut Window,
cx: &mut Context<Picker<Self>>,
) -> Option<Self::ListItem> {
let FileMatch { mat, .. } = &self.matches[ix];
let path_match = &self.matches[ix];
Some(
ListItem::new(ix)
@@ -170,10 +189,9 @@ impl PickerDelegate for FileContextPickerDelegate {
.toggle_state(selected)
.child(render_file_context_entry(
ElementId::NamedInteger("file-ctx-picker".into(), ix),
WorktreeId::from_usize(mat.worktree_id),
&mat.path,
&mat.path_prefix,
mat.is_dir,
&path_match.path,
&path_match.path_prefix,
path_match.is_dir,
self.context_store.clone(),
cx,
)),
@@ -181,17 +199,12 @@ impl PickerDelegate for FileContextPickerDelegate {
}
}
pub struct FileMatch {
pub mat: PathMatch,
pub is_recent: bool,
}
pub(crate) fn search_files(
pub(crate) fn search_paths(
query: String,
cancellation_flag: Arc<AtomicBool>,
workspace: &Entity<Workspace>,
cx: &App,
) -> Task<Vec<FileMatch>> {
) -> Task<Vec<PathMatch>> {
if query.is_empty() {
let workspace = workspace.read(cx);
let project = workspace.project().read(cx);
@@ -200,34 +213,28 @@ pub(crate) fn search_files(
.into_iter()
.filter_map(|(project_path, _)| {
let worktree = project.worktree_for_id(project_path.worktree_id, cx)?;
Some(FileMatch {
mat: PathMatch {
score: 0.,
positions: Vec::new(),
worktree_id: project_path.worktree_id.to_usize(),
path: project_path.path,
path_prefix: worktree.read(cx).root_name().into(),
distance_to_relative_ancestor: 0,
is_dir: false,
},
is_recent: true,
Some(PathMatch {
score: 0.,
positions: Vec::new(),
worktree_id: project_path.worktree_id.to_usize(),
path: project_path.path,
path_prefix: worktree.read(cx).root_name().into(),
distance_to_relative_ancestor: 0,
is_dir: false,
})
});
let file_matches = project.worktrees(cx).flat_map(|worktree| {
let worktree = worktree.read(cx);
let path_prefix: Arc<str> = worktree.root_name().into();
worktree.entries(false, 0).map(move |entry| FileMatch {
mat: PathMatch {
score: 0.,
positions: Vec::new(),
worktree_id: worktree.id().to_usize(),
path: entry.path.clone(),
path_prefix: path_prefix.clone(),
distance_to_relative_ancestor: 0,
is_dir: entry.is_dir(),
},
is_recent: false,
worktree.entries(false, 0).map(move |entry| PathMatch {
score: 0.,
positions: Vec::new(),
worktree_id: worktree.id().to_usize(),
path: entry.path.clone(),
path_prefix: path_prefix.clone(),
distance_to_relative_ancestor: 0,
is_dir: entry.is_dir(),
})
});
@@ -262,12 +269,6 @@ pub(crate) fn search_files(
executor,
)
.await
.into_iter()
.map(|mat| FileMatch {
mat,
is_recent: false,
})
.collect::<Vec<_>>()
})
}
}
@@ -310,26 +311,22 @@ pub fn extract_file_name_and_directory(
pub fn render_file_context_entry(
id: ElementId,
worktree_id: WorktreeId,
path: &Arc<Path>,
path: &Path,
path_prefix: &Arc<str>,
is_directory: bool,
context_store: WeakEntity<ContextStore>,
cx: &App,
) -> Stateful<Div> {
let (file_name, directory) = extract_file_name_and_directory(&path, path_prefix);
let (file_name, directory) = extract_file_name_and_directory(path, path_prefix);
let added = context_store.upgrade().and_then(|context_store| {
let project_path = ProjectPath {
worktree_id,
path: path.clone(),
};
if is_directory {
context_store.read(cx).includes_directory(&project_path)
} else {
context_store
.read(cx)
.will_include_file_path(&project_path, cx)
.includes_directory(path)
.map(FileInclusion::Direct)
} else {
context_store.read(cx).will_include_file_path(path, cx)
}
});
@@ -369,9 +366,8 @@ pub fn render_file_context_entry(
)
.child(Label::new("Added").size(LabelSize::Small)),
),
FileInclusion::InDirectory(directory_project_path) => {
// TODO: Consider using worktree full_path to include worktree name.
let directory_path = directory_project_path.path.to_string_lossy().into_owned();
FileInclusion::InDirectory(dir_name) => {
let dir_name = dir_name.to_string_lossy().into_owned();
el.child(
h_flex()
@@ -385,7 +381,7 @@ pub fn render_file_context_entry(
)
.child(Label::new("Included").size(LabelSize::Small)),
)
.tooltip(Tooltip::text(format!("in {directory_path}")))
.tooltip(Tooltip::text(format!("in {dir_name}")))
}
})
}

View File

@@ -1,12 +1,11 @@
use std::sync::Arc;
use std::sync::atomic::AtomicBool;
use fuzzy::StringMatchCandidate;
use gpui::{App, DismissEvent, Entity, FocusHandle, Focusable, Task, WeakEntity};
use picker::{Picker, PickerDelegate};
use ui::{ListItem, prelude::*};
use ui::{prelude::*, ListItem};
use crate::context_picker::ContextPicker;
use crate::context_picker::{ConfirmBehavior, ContextPicker};
use crate::context_store::{self, ContextStore};
use crate::thread::ThreadId;
use crate::thread_store::ThreadStore;
@@ -20,11 +19,16 @@ impl ThreadContextPicker {
thread_store: WeakEntity<ThreadStore>,
context_picker: WeakEntity<ContextPicker>,
context_store: WeakEntity<context_store::ContextStore>,
confirm_behavior: ConfirmBehavior,
window: &mut Window,
cx: &mut Context<Self>,
) -> Self {
let delegate =
ThreadContextPickerDelegate::new(thread_store, context_picker, context_store);
let delegate = ThreadContextPickerDelegate::new(
thread_store,
context_picker,
context_store,
confirm_behavior,
);
let picker = cx.new(|cx| Picker::uniform_list(delegate, window, cx));
ThreadContextPicker { picker }
@@ -53,6 +57,7 @@ pub struct ThreadContextPickerDelegate {
thread_store: WeakEntity<ThreadStore>,
context_picker: WeakEntity<ContextPicker>,
context_store: WeakEntity<context_store::ContextStore>,
confirm_behavior: ConfirmBehavior,
matches: Vec<ThreadContextEntry>,
selected_index: usize,
}
@@ -62,11 +67,13 @@ impl ThreadContextPickerDelegate {
thread_store: WeakEntity<ThreadStore>,
context_picker: WeakEntity<ContextPicker>,
context_store: WeakEntity<context_store::ContextStore>,
confirm_behavior: ConfirmBehavior,
) -> Self {
ThreadContextPickerDelegate {
thread_store,
context_picker,
context_store,
confirm_behavior,
matches: Vec::new(),
selected_index: 0,
}
@@ -107,11 +114,11 @@ impl PickerDelegate for ThreadContextPickerDelegate {
return Task::ready(());
};
let search_task = search_threads(query, Arc::new(AtomicBool::default()), threads, cx);
let search_task = search_threads(query, threads, cx);
cx.spawn_in(window, async move |this, cx| {
let matches = search_task.await;
this.update(cx, |this, cx| {
this.delegate.matches = matches.into_iter().map(|mat| mat.thread).collect();
this.delegate.matches = matches;
this.delegate.selected_index = 0;
cx.notify();
})
@@ -119,7 +126,7 @@ impl PickerDelegate for ThreadContextPickerDelegate {
})
}
fn confirm(&mut self, _secondary: bool, _window: &mut Window, cx: &mut Context<Picker<Self>>) {
fn confirm(&mut self, _secondary: bool, window: &mut Window, cx: &mut Context<Picker<Self>>) {
let Some(entry) = self.matches.get(self.selected_index) else {
return;
};
@@ -130,15 +137,20 @@ impl PickerDelegate for ThreadContextPickerDelegate {
let open_thread_task = thread_store.update(cx, |this, cx| this.open_thread(&entry.id, cx));
cx.spawn(async move |this, cx| {
cx.spawn_in(window, async move |this, cx| {
let thread = open_thread_task.await?;
this.update(cx, |this, cx| {
this.update_in(cx, |this, window, cx| {
this.delegate
.context_store
.update(cx, |context_store, cx| {
context_store.add_thread(thread, true, cx)
})
.ok();
match this.delegate.confirm_behavior {
ConfirmBehavior::KeepOpen => {}
ConfirmBehavior::Close => this.delegate.dismissed(window, cx),
}
})
})
.detach_and_log_err(cx);
@@ -185,7 +197,7 @@ pub fn render_thread_context_entry(
.gap_1p5()
.max_w_72()
.child(
Icon::new(IconName::MessageBubbles)
Icon::new(IconName::MessageCircle)
.size(IconSize::XSmall)
.color(Color::Muted),
)
@@ -205,18 +217,11 @@ pub fn render_thread_context_entry(
})
}
#[derive(Clone)]
pub struct ThreadMatch {
pub thread: ThreadContextEntry,
pub is_recent: bool,
}
pub(crate) fn search_threads(
query: String,
cancellation_flag: Arc<AtomicBool>,
thread_store: Entity<ThreadStore>,
cx: &mut App,
) -> Task<Vec<ThreadMatch>> {
) -> Task<Vec<ThreadContextEntry>> {
let threads = thread_store.update(cx, |this, _cx| {
this.threads()
.into_iter()
@@ -231,12 +236,6 @@ pub(crate) fn search_threads(
cx.background_spawn(async move {
if query.is_empty() {
threads
.into_iter()
.map(|thread| ThreadMatch {
thread,
is_recent: false,
})
.collect()
} else {
let candidates = threads
.iter()
@@ -248,17 +247,14 @@ pub(crate) fn search_threads(
&query,
false,
100,
&cancellation_flag,
&Default::default(),
executor,
)
.await;
matches
.into_iter()
.map(|mat| ThreadMatch {
thread: threads[mat.candidate_id].clone(),
is_recent: false,
})
.map(|mat| threads[mat.candidate_id].clone())
.collect()
}
})

View File

@@ -1,69 +1,56 @@
use std::ops::Range;
use std::path::Path;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use anyhow::{Context as _, Result, anyhow};
use anyhow::{anyhow, bail, Result};
use collections::{BTreeMap, HashMap, HashSet};
use futures::future::join_all;
use futures::{self, Future, FutureExt, future};
use gpui::{App, AppContext as _, Context, Entity, SharedString, Task, WeakEntity};
use language::{Buffer, File};
use project::{Project, ProjectItem, ProjectPath, Worktree};
use futures::{self, future, Future, FutureExt};
use gpui::{App, AppContext as _, AsyncApp, Context, Entity, SharedString, Task, WeakEntity};
use language::Buffer;
use project::{ProjectPath, Worktree};
use rope::Rope;
use text::{Anchor, BufferId, OffsetRangeExt};
use util::{ResultExt as _, maybe};
use text::BufferId;
use util::maybe;
use workspace::Workspace;
use crate::ThreadStore;
use crate::context::{
AssistantContext, ContextBuffer, ContextId, ContextSymbol, ContextSymbolId, DirectoryContext,
FetchedUrlContext, FileContext, SymbolContext, ThreadContext,
AssistantContext, ContextBuffer, ContextId, ContextSnapshot, DirectoryContext,
FetchedUrlContext, FileContext, ThreadContext,
};
use crate::context_strip::SuggestedContext;
use crate::thread::{Thread, ThreadId};
pub struct ContextStore {
project: WeakEntity<Project>,
workspace: WeakEntity<Workspace>,
context: Vec<AssistantContext>,
thread_store: Option<WeakEntity<ThreadStore>>,
// TODO: If an EntityId is used for all context types (like BufferId), can remove ContextId.
next_context_id: ContextId,
files: BTreeMap<BufferId, ContextId>,
directories: HashMap<ProjectPath, ContextId>,
symbols: HashMap<ContextSymbolId, ContextId>,
symbol_buffers: HashMap<ContextSymbolId, Entity<Buffer>>,
symbols_by_path: HashMap<ProjectPath, Vec<ContextSymbolId>>,
directories: HashMap<PathBuf, ContextId>,
threads: HashMap<ThreadId, ContextId>,
thread_summary_tasks: Vec<Task<()>>,
fetched_urls: HashMap<String, ContextId>,
}
impl ContextStore {
pub fn new(
project: WeakEntity<Project>,
thread_store: Option<WeakEntity<ThreadStore>>,
) -> Self {
pub fn new(workspace: WeakEntity<Workspace>) -> Self {
Self {
project,
thread_store,
workspace,
context: Vec::new(),
next_context_id: ContextId(0),
files: BTreeMap::default(),
directories: HashMap::default(),
symbols: HashMap::default(),
symbol_buffers: HashMap::default(),
symbols_by_path: HashMap::default(),
threads: HashMap::default(),
thread_summary_tasks: Vec::new(),
fetched_urls: HashMap::default(),
}
}
pub fn context(&self) -> &Vec<AssistantContext> {
&self.context
pub fn snapshot<'a>(&'a self, cx: &'a App) -> impl Iterator<Item = ContextSnapshot> + 'a {
self.context()
.iter()
.flat_map(|context| context.snapshot(cx))
}
pub fn context_for_id(&self, id: ContextId) -> Option<&AssistantContext> {
self.context().iter().find(|context| context.id() == id)
pub fn context(&self) -> &Vec<AssistantContext> {
&self.context
}
pub fn clear(&mut self) {
@@ -80,7 +67,12 @@ impl ContextStore {
remove_if_exists: bool,
cx: &mut Context<Self>,
) -> Task<Result<()>> {
let Some(project) = self.project.upgrade() else {
let workspace = self.workspace.clone();
let Some(project) = workspace
.upgrade()
.map(|workspace| workspace.read(cx).project().clone())
else {
return Task::ready(Err(anyhow!("failed to read project")));
};
@@ -89,14 +81,14 @@ impl ContextStore {
project.open_buffer(project_path.clone(), cx)
})?;
let buffer = open_buffer_task.await?;
let buffer_id = this.update(cx, |_, cx| buffer.read(cx).remote_id())?;
let buffer_entity = open_buffer_task.await?;
let buffer_id = this.update(cx, |_, cx| buffer_entity.read(cx).remote_id())?;
let already_included = this.update(cx, |this, cx| {
match this.will_include_buffer(buffer_id, &project_path) {
let already_included = this.update(cx, |this, _cx| {
match this.will_include_buffer(buffer_id, &project_path.path) {
Some(FileInclusion::Direct(context_id)) => {
if remove_if_exists {
this.remove_context(context_id, cx);
this.remove_context(context_id);
}
true
}
@@ -109,13 +101,20 @@ impl ContextStore {
return anyhow::Ok(());
}
let (buffer_info, text_task) =
this.update(cx, |_, cx| collect_buffer_info_and_text(buffer, None, cx))??;
let (buffer_info, text_task) = this.update(cx, |_, cx| {
let buffer = buffer_entity.read(cx);
collect_buffer_info_and_text(
project_path.path.clone(),
buffer_entity,
buffer,
cx.to_async(),
)
})?;
let text = text_task.await;
this.update(cx, |this, cx| {
this.insert_file(make_context_buffer(buffer_info, text), cx);
this.update(cx, |this, _cx| {
this.insert_file(make_context_buffer(buffer_info, text));
})?;
anyhow::Ok(())
@@ -124,29 +123,38 @@ impl ContextStore {
pub fn add_file_from_buffer(
&mut self,
buffer: Entity<Buffer>,
buffer_entity: Entity<Buffer>,
cx: &mut Context<Self>,
) -> Task<Result<()>> {
cx.spawn(async move |this, cx| {
let (buffer_info, text_task) =
this.update(cx, |_, cx| collect_buffer_info_and_text(buffer, None, cx))??;
let (buffer_info, text_task) = this.update(cx, |_, cx| {
let buffer = buffer_entity.read(cx);
let Some(file) = buffer.file() else {
return Err(anyhow!("Buffer has no path."));
};
Ok(collect_buffer_info_and_text(
file.path().clone(),
buffer_entity,
buffer,
cx.to_async(),
))
})??;
let text = text_task.await;
this.update(cx, |this, cx| {
this.insert_file(make_context_buffer(buffer_info, text), cx)
this.update(cx, |this, _cx| {
this.insert_file(make_context_buffer(buffer_info, text))
})?;
anyhow::Ok(())
})
}
fn insert_file(&mut self, context_buffer: ContextBuffer, cx: &mut Context<Self>) {
fn insert_file(&mut self, context_buffer: ContextBuffer) {
let id = self.next_context_id.post_inc();
self.files.insert(context_buffer.id, id);
self.context
.push(AssistantContext::File(FileContext { id, context_buffer }));
cx.notify();
}
pub fn add_directory(
@@ -155,19 +163,22 @@ impl ContextStore {
remove_if_exists: bool,
cx: &mut Context<Self>,
) -> Task<Result<()>> {
let Some(project) = self.project.upgrade() else {
let workspace = self.workspace.clone();
let Some(project) = workspace
.upgrade()
.map(|workspace| workspace.read(cx).project().clone())
else {
return Task::ready(Err(anyhow!("failed to read project")));
};
let already_included = match self.includes_directory(&project_path) {
Some(FileInclusion::Direct(context_id)) => {
if remove_if_exists {
self.remove_context(context_id, cx);
}
true
let already_included = if let Some(context_id) = self.includes_directory(&project_path.path)
{
if remove_if_exists {
self.remove_context(context_id);
}
Some(FileInclusion::InDirectory(_)) => true,
None => false,
true
} else {
false
};
if already_included {
return Task::ready(Ok(()));
@@ -203,11 +214,16 @@ impl ContextStore {
let mut buffer_infos = Vec::new();
let mut text_tasks = Vec::new();
this.update(cx, |_, cx| {
// Skip all binary files and other non-UTF8 files
for buffer in buffers.into_iter().flatten() {
if let Some((buffer_info, text_task)) =
collect_buffer_info_and_text(buffer, None, cx).log_err()
{
for (path, buffer_entity) in files.into_iter().zip(buffers) {
// Skip all binary files and other non-UTF8 files
if let Ok(buffer_entity) = buffer_entity {
let buffer = buffer_entity.read(cx);
let (buffer_info, text_task) = collect_buffer_info_and_text(
path,
buffer_entity,
buffer,
cx.to_async(),
);
buffer_infos.push(buffer_info);
text_tasks.push(text_task);
}
@@ -223,113 +239,27 @@ impl ContextStore {
.collect::<Vec<_>>();
if context_buffers.is_empty() {
let full_path = cx.update(|cx| worktree.read(cx).full_path(&project_path.path))?;
return Err(anyhow!("No text files found in {}", &full_path.display()));
bail!("No text files found in {}", &project_path.path.display());
}
this.update(cx, |this, cx| {
this.insert_directory(worktree, project_path, context_buffers, cx);
this.update(cx, |this, _| {
this.insert_directory(&project_path.path, context_buffers);
})?;
anyhow::Ok(())
})
}
fn insert_directory(
&mut self,
worktree: Entity<Worktree>,
project_path: ProjectPath,
context_buffers: Vec<ContextBuffer>,
cx: &mut Context<Self>,
) {
fn insert_directory(&mut self, path: &Path, context_buffers: Vec<ContextBuffer>) {
let id = self.next_context_id.post_inc();
let path = project_path.path.clone();
self.directories.insert(project_path, id);
self.directories.insert(path.to_path_buf(), id);
self.context
.push(AssistantContext::Directory(DirectoryContext {
.push(AssistantContext::Directory(DirectoryContext::new(
id,
worktree,
path,
context_buffers,
}));
cx.notify();
}
pub fn add_symbol(
&mut self,
buffer: Entity<Buffer>,
symbol_name: SharedString,
symbol_range: Range<Anchor>,
symbol_enclosing_range: Range<Anchor>,
remove_if_exists: bool,
cx: &mut Context<Self>,
) -> Task<Result<bool>> {
let buffer_ref = buffer.read(cx);
let Some(project_path) = buffer_ref.project_path(cx) else {
return Task::ready(Err(anyhow!("buffer has no path")));
};
if let Some(symbols_for_path) = self.symbols_by_path.get(&project_path) {
let mut matching_symbol_id = None;
for symbol in symbols_for_path {
if &symbol.name == &symbol_name {
let snapshot = buffer_ref.snapshot();
if symbol.range.to_offset(&snapshot) == symbol_range.to_offset(&snapshot) {
matching_symbol_id = self.symbols.get(symbol).cloned();
break;
}
}
}
if let Some(id) = matching_symbol_id {
if remove_if_exists {
self.remove_context(id, cx);
}
return Task::ready(Ok(false));
}
}
let (buffer_info, collect_content_task) =
match collect_buffer_info_and_text(buffer, Some(symbol_enclosing_range.clone()), cx) {
Ok((buffer_info, collect_context_task)) => (buffer_info, collect_context_task),
Err(err) => return Task::ready(Err(err)),
};
cx.spawn(async move |this, cx| {
let content = collect_content_task.await;
this.update(cx, |this, cx| {
this.insert_symbol(
make_context_symbol(
buffer_info,
project_path,
symbol_name,
symbol_range,
symbol_enclosing_range,
content,
),
cx,
)
})?;
anyhow::Ok(true)
})
}
fn insert_symbol(&mut self, context_symbol: ContextSymbol, cx: &mut Context<Self>) {
let id = self.next_context_id.post_inc();
self.symbols.insert(context_symbol.id.clone(), id);
self.symbols_by_path
.entry(context_symbol.id.path.clone())
.or_insert_with(Vec::new)
.push(context_symbol.id.clone());
self.symbol_buffers
.insert(context_symbol.id.clone(), context_symbol.buffer.clone());
self.context.push(AssistantContext::Symbol(SymbolContext {
id,
context_symbol,
}));
cx.notify();
)));
}
pub fn add_thread(
@@ -340,70 +270,29 @@ impl ContextStore {
) {
if let Some(context_id) = self.includes_thread(&thread.read(cx).id()) {
if remove_if_exists {
self.remove_context(context_id, cx);
self.remove_context(context_id);
}
} else {
self.insert_thread(thread, cx);
}
}
pub fn wait_for_summaries(&mut self, cx: &App) -> Task<()> {
let tasks = std::mem::take(&mut self.thread_summary_tasks);
cx.spawn(async move |_cx| {
join_all(tasks).await;
})
}
fn insert_thread(&mut self, thread: Entity<Thread>, cx: &mut Context<Self>) {
if let Some(summary_task) =
thread.update(cx, |thread, cx| thread.generate_detailed_summary(cx))
{
let thread = thread.clone();
let thread_store = self.thread_store.clone();
self.thread_summary_tasks.push(cx.spawn(async move |_, cx| {
summary_task.await;
if let Some(thread_store) = thread_store {
// Save thread so its summary can be reused later
let save_task = thread_store
.update(cx, |thread_store, cx| thread_store.save_thread(&thread, cx));
if let Some(save_task) = save_task.ok() {
save_task.await.log_err();
}
}
}));
}
fn insert_thread(&mut self, thread: Entity<Thread>, cx: &App) {
let id = self.next_context_id.post_inc();
let text = thread.read(cx).latest_detailed_summary_or_text();
let text = thread.read(cx).text().into();
self.threads.insert(thread.read(cx).id().clone(), id);
self.context
.push(AssistantContext::Thread(ThreadContext { id, thread, text }));
cx.notify();
}
pub fn add_fetched_url(
&mut self,
url: String,
text: impl Into<SharedString>,
cx: &mut Context<ContextStore>,
) {
pub fn add_fetched_url(&mut self, url: String, text: impl Into<SharedString>) {
if self.includes_url(&url).is_none() {
self.insert_fetched_url(url, text, cx);
self.insert_fetched_url(url, text);
}
}
fn insert_fetched_url(
&mut self,
url: String,
text: impl Into<SharedString>,
cx: &mut Context<ContextStore>,
) {
fn insert_fetched_url(&mut self, url: String, text: impl Into<SharedString>) {
let id = self.next_context_id.post_inc();
self.fetched_urls.insert(url.clone(), id);
@@ -413,7 +302,6 @@ impl ContextStore {
url: url.into(),
text: text.into(),
}));
cx.notify();
}
pub fn accept_suggested_context(
@@ -440,7 +328,7 @@ impl ContextStore {
Task::ready(Ok(()))
}
pub fn remove_context(&mut self, id: ContextId, cx: &mut Context<Self>) {
pub fn remove_context(&mut self, id: ContextId) {
let Some(ix) = self.context.iter().position(|context| context.id() == id) else {
return;
};
@@ -452,19 +340,6 @@ impl ContextStore {
AssistantContext::Directory(_) => {
self.directories.retain(|_, context_id| *context_id != id);
}
AssistantContext::Symbol(symbol) => {
if let Some(symbols_in_path) =
self.symbols_by_path.get_mut(&symbol.context_symbol.id.path)
{
symbols_in_path.retain(|s| {
self.symbols
.get(s)
.map_or(false, |context_id| *context_id != id)
});
}
self.symbol_buffers.remove(&symbol.context_symbol.id);
self.symbols.retain(|_, context_id| *context_id != id);
}
AssistantContext::FetchedUrl(_) => {
self.fetched_urls.retain(|_, context_id| *context_id != id);
}
@@ -472,38 +347,28 @@ impl ContextStore {
self.threads.retain(|_, context_id| *context_id != id);
}
}
cx.notify();
}
/// Returns whether the buffer is already included directly in the context, or if it will be
/// included in the context via a directory. Directory inclusion is based on paths rather than
/// buffer IDs as the directory will be re-scanned.
pub fn will_include_buffer(
&self,
buffer_id: BufferId,
project_path: &ProjectPath,
) -> Option<FileInclusion> {
pub fn will_include_buffer(&self, buffer_id: BufferId, path: &Path) -> Option<FileInclusion> {
if let Some(context_id) = self.files.get(&buffer_id) {
return Some(FileInclusion::Direct(*context_id));
}
self.will_include_file_path_via_directory(project_path)
self.will_include_file_path_via_directory(path)
}
/// Returns whether this file path is already included directly in the context, or if it will be
/// included in the context via a directory.
pub fn will_include_file_path(
&self,
project_path: &ProjectPath,
cx: &App,
) -> Option<FileInclusion> {
pub fn will_include_file_path(&self, path: &Path, cx: &App) -> Option<FileInclusion> {
if !self.files.is_empty() {
let found_file_context = self.context.iter().find(|context| match &context {
AssistantContext::File(file_context) => {
let buffer = file_context.context_buffer.buffer.read(cx);
if let Some(context_path) = buffer.project_path(cx) {
&context_path == project_path
if let Some(file_path) = buffer_path_log_err(buffer) {
*file_path == *path
} else {
false
}
@@ -515,52 +380,27 @@ impl ContextStore {
}
}
self.will_include_file_path_via_directory(project_path)
self.will_include_file_path_via_directory(path)
}
fn will_include_file_path_via_directory(
&self,
project_path: &ProjectPath,
) -> Option<FileInclusion> {
fn will_include_file_path_via_directory(&self, path: &Path) -> Option<FileInclusion> {
if self.directories.is_empty() {
return None;
}
let mut path_buf = project_path.path.to_path_buf();
let mut buf = path.to_path_buf();
while path_buf.pop() {
// TODO: This isn't very efficient. Consider using a better representation of the
// directories map.
let directory_project_path = ProjectPath {
worktree_id: project_path.worktree_id,
path: path_buf.clone().into(),
};
if let Some(_) = self.directories.get(&directory_project_path) {
return Some(FileInclusion::InDirectory(directory_project_path));
while buf.pop() {
if let Some(_) = self.directories.get(&buf) {
return Some(FileInclusion::InDirectory(buf));
}
}
None
}
pub fn includes_directory(&self, project_path: &ProjectPath) -> Option<FileInclusion> {
if let Some(context_id) = self.directories.get(project_path) {
return Some(FileInclusion::Direct(*context_id));
}
self.will_include_file_path_via_directory(project_path)
}
pub fn included_symbol(&self, symbol_id: &ContextSymbolId) -> Option<ContextId> {
self.symbols.get(symbol_id).copied()
}
pub fn included_symbols_by_path(&self) -> &HashMap<ProjectPath, Vec<ContextSymbolId>> {
&self.symbols_by_path
}
pub fn buffer_for_symbol(&self, symbol_id: &ContextSymbolId) -> Option<Entity<Buffer>> {
self.symbol_buffers.get(symbol_id).cloned()
pub fn includes_directory(&self, path: &Path) -> Option<ContextId> {
self.directories.get(path).copied()
}
pub fn includes_thread(&self, thread_id: &ThreadId) -> Option<ContextId> {
@@ -582,16 +422,15 @@ impl ContextStore {
}
}
pub fn file_paths(&self, cx: &App) -> HashSet<ProjectPath> {
pub fn file_paths(&self, cx: &App) -> HashSet<PathBuf> {
self.context
.iter()
.filter_map(|context| match context {
AssistantContext::File(file) => {
let buffer = file.context_buffer.buffer.read(cx);
buffer.project_path(cx)
buffer_path_log_err(buffer).map(|p| p.to_path_buf())
}
AssistantContext::Directory(_)
| AssistantContext::Symbol(_)
| AssistantContext::FetchedUrl(_)
| AssistantContext::Thread(_) => None,
})
@@ -605,71 +444,49 @@ impl ContextStore {
pub enum FileInclusion {
Direct(ContextId),
InDirectory(ProjectPath),
InDirectory(PathBuf),
}
// ContextBuffer without text.
struct BufferInfo {
buffer_entity: Entity<Buffer>,
id: BufferId,
buffer: Entity<Buffer>,
file: Arc<dyn File>,
version: clock::Global,
}
fn make_context_buffer(info: BufferInfo, text: SharedString) -> ContextBuffer {
ContextBuffer {
id: info.id,
buffer: info.buffer,
file: info.file,
buffer: info.buffer_entity,
version: info.version,
text,
}
}
fn make_context_symbol(
info: BufferInfo,
path: ProjectPath,
name: SharedString,
range: Range<Anchor>,
enclosing_range: Range<Anchor>,
text: SharedString,
) -> ContextSymbol {
ContextSymbol {
id: ContextSymbolId { name, range, path },
buffer_version: info.version,
enclosing_range,
buffer: info.buffer,
text,
}
fn collect_buffer_info_and_text(
path: Arc<Path>,
buffer_entity: Entity<Buffer>,
buffer: &Buffer,
cx: AsyncApp,
) -> (BufferInfo, Task<SharedString>) {
let buffer_info = BufferInfo {
id: buffer.remote_id(),
buffer_entity,
version: buffer.version(),
};
// Important to collect version at the same time as content so that staleness logic is correct.
let content = buffer.as_rope().clone();
let text_task = cx.background_spawn(async move { to_fenced_codeblock(&path, content) });
(buffer_info, text_task)
}
fn collect_buffer_info_and_text(
buffer: Entity<Buffer>,
range: Option<Range<Anchor>>,
cx: &App,
) -> Result<(BufferInfo, Task<SharedString>)> {
let buffer_ref = buffer.read(cx);
let file = buffer_ref.file().context("file context must have a path")?;
// Important to collect version at the same time as content so that staleness logic is correct.
let version = buffer_ref.version();
let content = if let Some(range) = range {
buffer_ref.text_for_range(range).collect::<Rope>()
pub fn buffer_path_log_err(buffer: &Buffer) -> Option<Arc<Path>> {
if let Some(file) = buffer.file() {
Some(file.path().clone())
} else {
buffer_ref.as_rope().clone()
};
let buffer_info = BufferInfo {
buffer,
id: buffer_ref.remote_id(),
file: file.clone(),
version,
};
let full_path = file.full_path(cx);
let text_task = cx.background_spawn(async move { to_fenced_codeblock(&full_path, content) });
Ok((buffer_info, text_task))
log::error!("Buffer that had a path unexpectedly no longer has a path.");
None
}
}
fn to_fenced_codeblock(path: &Path, content: Rope) -> SharedString {
@@ -730,7 +547,7 @@ pub fn refresh_context_store_text(
context_store: Entity<ContextStore>,
changed_buffers: &HashSet<Entity<Buffer>>,
cx: &App,
) -> impl Future<Output = Vec<ContextId>> + use<> {
) -> impl Future<Output = Vec<ContextId>> {
let mut tasks = Vec::new();
for context in &context_store.read(cx).context {
@@ -747,13 +564,12 @@ pub fn refresh_context_store_text(
}
}
AssistantContext::Directory(directory_context) => {
let directory_path = directory_context.project_path(cx);
let should_refresh = changed_buffers.is_empty()
|| changed_buffers.iter().any(|buffer| {
let Some(buffer_path) = buffer.read(cx).project_path(cx) else {
return false;
};
buffer_path.starts_with(&directory_path)
let buffer = buffer.read(cx);
buffer_path_log_err(&buffer)
.map_or(false, |path| path.starts_with(&directory_context.path))
});
if should_refresh {
@@ -761,14 +577,6 @@ pub fn refresh_context_store_text(
return refresh_directory_text(context_store, directory_context, cx);
}
}
AssistantContext::Symbol(symbol_context) => {
if changed_buffers.is_empty()
|| changed_buffers.contains(&symbol_context.context_symbol.buffer)
{
let context_store = context_store.clone();
return refresh_symbol_text(context_store, symbol_context, cx);
}
}
AssistantContext::Thread(thread_context) => {
if changed_buffers.is_empty() {
let context_store = context_store.clone();
@@ -839,47 +647,19 @@ fn refresh_directory_text(
let context_buffers = future::join_all(futures);
let id = directory_context.id;
let worktree = directory_context.worktree.clone();
let id = directory_context.snapshot.id;
let path = directory_context.path.clone();
Some(cx.spawn(async move |cx| {
let context_buffers = context_buffers.await;
context_store
.update(cx, |context_store, _| {
let new_directory_context = DirectoryContext {
id,
worktree,
path,
context_buffers,
};
let new_directory_context = DirectoryContext::new(id, &path, context_buffers);
context_store.replace_context(AssistantContext::Directory(new_directory_context));
})
.ok();
}))
}
fn refresh_symbol_text(
context_store: Entity<ContextStore>,
symbol_context: &SymbolContext,
cx: &App,
) -> Option<Task<()>> {
let id = symbol_context.id;
let task = refresh_context_symbol(&symbol_context.context_symbol, cx);
if let Some(task) = task {
Some(cx.spawn(async move |cx| {
let context_symbol = task.await;
context_store
.update(cx, |context_store, _| {
let new_symbol_context = SymbolContext { id, context_symbol };
context_store.replace_context(AssistantContext::Symbol(new_symbol_context));
})
.ok();
}))
} else {
None
}
}
fn refresh_thread_text(
context_store: Entity<ContextStore>,
thread_context: &ThreadContext,
@@ -890,7 +670,7 @@ fn refresh_thread_text(
cx.spawn(async move |cx| {
context_store
.update(cx, |context_store, cx| {
let text = thread.read(cx).latest_detailed_summary_or_text();
let text = thread.read(cx).text().into();
context_store.replace_context(AssistantContext::Thread(ThreadContext {
id,
thread,
@@ -904,44 +684,18 @@ fn refresh_thread_text(
fn refresh_context_buffer(
context_buffer: &ContextBuffer,
cx: &App,
) -> Option<impl Future<Output = ContextBuffer> + use<>> {
) -> Option<impl Future<Output = ContextBuffer>> {
let buffer = context_buffer.buffer.read(cx);
let path = buffer_path_log_err(buffer)?;
if buffer.version.changed_since(&context_buffer.version) {
let (buffer_info, text_task) =
collect_buffer_info_and_text(context_buffer.buffer.clone(), None, cx).log_err()?;
let (buffer_info, text_task) = collect_buffer_info_and_text(
path,
context_buffer.buffer.clone(),
buffer,
cx.to_async(),
);
Some(text_task.map(move |text| make_context_buffer(buffer_info, text)))
} else {
None
}
}
fn refresh_context_symbol(
context_symbol: &ContextSymbol,
cx: &App,
) -> Option<impl Future<Output = ContextSymbol> + use<>> {
let buffer = context_symbol.buffer.read(cx);
let project_path = buffer.project_path(cx)?;
if buffer.version.changed_since(&context_symbol.buffer_version) {
let (buffer_info, text_task) = collect_buffer_info_and_text(
context_symbol.buffer.clone(),
Some(context_symbol.enclosing_range.clone()),
cx,
)
.log_err()?;
let name = context_symbol.id.name.clone();
let range = context_symbol.id.range.clone();
let enclosing_range = context_symbol.enclosing_range.clone();
Some(text_task.map(move |text| {
make_context_symbol(
buffer_info,
project_path,
name,
range,
enclosing_range,
text,
)
}))
} else {
None
}
}

View File

@@ -1,25 +1,23 @@
use std::path::Path;
use std::rc::Rc;
use collections::HashSet;
use editor::Editor;
use file_icons::FileIcons;
use gpui::{
App, Bounds, ClickEvent, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable,
Subscription, WeakEntity,
App, Bounds, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, Subscription,
WeakEntity,
};
use itertools::Itertools;
use language::Buffer;
use project::ProjectItem;
use ui::{KeyBinding, PopoverMenu, PopoverMenuHandle, Tooltip, prelude::*};
use workspace::{Workspace, notifications::NotifyResultExt};
use ui::{prelude::*, KeyBinding, PopoverMenu, PopoverMenuHandle, Tooltip};
use workspace::{notifications::NotifyResultExt, Workspace};
use crate::context::{ContextId, ContextKind};
use crate::context_picker::ContextPicker;
use crate::context::ContextKind;
use crate::context_picker::{ConfirmBehavior, ContextPicker};
use crate::context_store::ContextStore;
use crate::thread::Thread;
use crate::thread_store::ThreadStore;
use crate::ui::{AddedContext, ContextPill};
use crate::ui::ContextPill;
use crate::{
AcceptSuggestedContext, AssistantPanel, FocusDown, FocusLeft, FocusRight, FocusUp,
RemoveAllContext, RemoveFocusedContext, ToggleContextPicker,
@@ -52,6 +50,7 @@ impl ContextStrip {
workspace.clone(),
thread_store.clone(),
context_store.downgrade(),
ConfirmBehavior::KeepOpen,
window,
cx,
)
@@ -60,7 +59,6 @@ impl ContextStrip {
let focus_handle = cx.focus_handle();
let subscriptions = vec![
cx.observe(&context_store, |_, _, cx| cx.notify()),
cx.subscribe_in(&context_picker, window, Self::handle_context_picker_event),
cx.on_focus(&focus_handle, window, Self::handle_focus),
cx.on_blur(&focus_handle, window, Self::handle_blur),
@@ -94,23 +92,26 @@ impl ContextStrip {
let active_buffer_entity = editor.buffer().read(cx).as_singleton()?;
let active_buffer = active_buffer_entity.read(cx);
let project_path = active_buffer.project_path(cx)?;
let path = active_buffer.file()?.path();
if self
.context_store
.read(cx)
.will_include_buffer(active_buffer.remote_id(), &project_path)
.will_include_buffer(active_buffer.remote_id(), path)
.is_some()
{
return None;
}
let file_name = active_buffer.file()?.file_name(cx);
let name = match path.file_name() {
Some(name) => name.to_string_lossy().into_owned().into(),
None => path.to_string_lossy().into_owned().into(),
};
let icon_path = FileIcons::get_icon(&Path::new(&file_name), cx);
let icon_path = FileIcons::get_icon(path, cx);
Some(SuggestedContext::File {
name: file_name.to_string_lossy().into_owned().into(),
name,
buffer: active_buffer_entity.downgrade(),
icon_path,
})
@@ -238,7 +239,11 @@ impl ContextStrip {
let eraser = if bounds.len() < 3 { 0 } else { 1 };
let pills = &bounds[1..bounds.len() - eraser];
if pills.is_empty() { None } else { Some(pills) }
if pills.is_empty() {
None
} else {
Some(pills)
}
}
fn last_pill_index(&self) -> Option<usize> {
@@ -272,14 +277,6 @@ impl ContextStrip {
best.map(|(index, _, _)| index)
}
fn open_context(&mut self, id: ContextId, window: &mut Window, cx: &mut App) {
let Some(workspace) = self.workspace.upgrade() else {
return;
};
crate::active_thread::open_context(id, self.context_store.clone(), workspace, window, cx);
}
fn remove_focused_context(
&mut self,
_: &RemoveFocusedContext,
@@ -289,9 +286,9 @@ impl ContextStrip {
if let Some(index) = self.focused_index {
let mut is_empty = false;
self.context_store.update(cx, |this, cx| {
self.context_store.update(cx, |this, _cx| {
if let Some(item) = this.context().get(index) {
this.remove_context(item.id(), cx);
this.remove_context(item.id());
}
is_empty = this.context().is_empty();
@@ -362,19 +359,19 @@ impl Focusable for ContextStrip {
impl Render for ContextStrip {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let context_store = self.context_store.read(cx);
let context = context_store.context();
let context = context_store
.context()
.iter()
.flat_map(|context| context.snapshot(cx))
.collect::<Vec<_>>();
let context_picker = self.context_picker.clone();
let focus_handle = self.focus_handle.clone();
let suggested_context = self.suggested_context(cx);
let added_contexts = context
let dupe_names = context
.iter()
.map(|c| AddedContext::new(c, cx))
.collect::<Vec<_>>();
let dupe_names = added_contexts
.iter()
.map(|c| c.name.clone())
.map(|context| context.name.clone())
.sorted()
.tuple_windows()
.filter(|(a, b)| a == b)
@@ -460,39 +457,27 @@ impl Render for ContextStrip {
)
}
})
.children(
added_contexts
.into_iter()
.enumerate()
.map(|(i, added_context)| {
let name = added_context.name.clone();
let id = added_context.id;
ContextPill::added(
added_context,
dupe_names.contains(&name),
self.focused_index == Some(i),
Some({
let context_store = self.context_store.clone();
Rc::new(cx.listener(move |_this, _event, _window, cx| {
context_store.update(cx, |this, cx| {
this.remove_context(id, cx);
});
cx.notify();
}))
}),
)
.on_click({
Rc::new(cx.listener(move |this, event: &ClickEvent, window, cx| {
if event.down.click_count > 1 {
this.open_context(id, window, cx);
} else {
this.focused_index = Some(i);
}
cx.notify();
}))
})
.children(context.iter().enumerate().map(|(i, context)| {
ContextPill::added(
context.clone(),
dupe_names.contains(&context.name),
self.focused_index == Some(i),
Some({
let id = context.id;
let context_store = self.context_store.clone();
Rc::new(cx.listener(move |_this, _event, _window, cx| {
context_store.update(cx, |this, _cx| {
this.remove_context(id);
});
cx.notify();
}))
}),
)
)
.on_click(Rc::new(cx.listener(move |this, _, _window, cx| {
this.focused_index = Some(i);
cx.notify();
})))
}))
.when_some(suggested_context, |el, suggested| {
el.child(
ContextPill::suggested(

View File

@@ -1,10 +1,9 @@
use assistant_context_editor::SavedContextMetadata;
use chrono::{DateTime, Utc};
use gpui::{Entity, prelude::*};
use gpui::{prelude::*, Entity};
use crate::thread_store::{SerializedThreadMetadata, ThreadStore};
#[derive(Debug)]
pub enum HistoryEntry {
Thread(SerializedThreadMetadata),
Context(SavedContextMetadata),
@@ -22,35 +21,28 @@ impl HistoryEntry {
pub struct HistoryStore {
thread_store: Entity<ThreadStore>,
context_store: Entity<assistant_context_editor::ContextStore>,
_subscriptions: Vec<gpui::Subscription>,
}
impl HistoryStore {
pub fn new(
thread_store: Entity<ThreadStore>,
context_store: Entity<assistant_context_editor::ContextStore>,
cx: &mut Context<Self>,
_cx: &mut Context<Self>,
) -> Self {
let subscriptions = vec![
cx.observe(&thread_store, |_, _, cx| cx.notify()),
cx.observe(&context_store, |_, _, cx| cx.notify()),
];
Self {
thread_store,
context_store,
_subscriptions: subscriptions,
}
}
/// Returns the number of history entries.
pub fn entry_count(&self, cx: &mut Context<Self>) -> usize {
self.entries(cx).len()
}
pub fn entries(&self, cx: &mut Context<Self>) -> Vec<HistoryEntry> {
let mut history_entries = Vec::new();
#[cfg(debug_assertions)]
if std::env::var("ZED_SIMULATE_NO_THREAD_HISTORY").is_ok() {
return history_entries;
}
for thread in self.thread_store.update(cx, |this, _cx| this.threads()) {
history_entries.push(HistoryEntry::Thread(thread));
}

View File

@@ -7,46 +7,45 @@ use std::sync::Arc;
use anyhow::{Context as _, Result};
use assistant_settings::AssistantSettings;
use client::telemetry::Telemetry;
use collections::{HashMap, HashSet, VecDeque, hash_map};
use collections::{hash_map, HashMap, HashSet, VecDeque};
use editor::{
Anchor, AnchorRangeExt, CodeActionProvider, Editor, EditorEvent, ExcerptId, ExcerptRange,
GutterDimensions, MultiBuffer, MultiBufferSnapshot, ToOffset as _, ToPoint,
actions::SelectAll,
display_map::{
BlockContext, BlockPlacement, BlockProperties, BlockStyle, CustomBlockId, RenderBlock,
ToDisplayPoint,
},
Anchor, AnchorRangeExt, CodeActionProvider, Editor, EditorEvent, ExcerptId, ExcerptRange,
GutterDimensions, MultiBuffer, MultiBufferSnapshot, ToOffset as _, ToPoint,
};
use feature_flags::{Assistant2FeatureFlag, FeatureFlagViewExt as _};
use fs::Fs;
use gpui::{
App, Context, Entity, Focusable, Global, HighlightStyle, Subscription, Task, UpdateGlobal,
WeakEntity, Window, point,
point, App, Context, Entity, Focusable, Global, HighlightStyle, Subscription, Task,
UpdateGlobal, WeakEntity, Window,
};
use language::{Buffer, Point, Selection, TransactionId};
use language_model::{LanguageModelRegistry, report_assistant_event};
use language_model::{report_assistant_event, LanguageModelRegistry};
use multi_buffer::MultiBufferRow;
use parking_lot::Mutex;
use project::LspAction;
use project::Project;
use project::{CodeAction, ProjectTransaction};
use prompt_store::PromptBuilder;
use settings::{Settings, SettingsStore};
use telemetry_events::{AssistantEventData, AssistantKind, AssistantPhase};
use terminal_view::{TerminalView, terminal_panel::TerminalPanel};
use telemetry_events::{AssistantEvent, AssistantKind, AssistantPhase};
use terminal_view::{terminal_panel::TerminalPanel, TerminalView};
use text::{OffsetRangeExt, ToPoint as _};
use ui::prelude::*;
use util::RangeExt;
use util::ResultExt;
use workspace::{ItemHandle, Toast, Workspace, dock::Panel, notifications::NotificationId};
use zed_actions::agent::OpenConfiguration;
use workspace::{dock::Panel, ShowConfiguration};
use workspace::{notifications::NotificationId, ItemHandle, Toast, Workspace};
use crate::AssistantPanel;
use crate::buffer_codegen::{BufferCodegen, CodegenAlternative, CodegenEvent};
use crate::context_store::ContextStore;
use crate::inline_prompt_editor::{CodegenStatus, InlineAssistId, PromptEditor, PromptEditorEvent};
use crate::terminal_inline_assistant::TerminalInlineAssistant;
use crate::thread_store::ThreadStore;
use crate::AssistantPanel;
pub fn init(
fs: Arc<dyn Fs>,
@@ -240,8 +239,8 @@ impl InlineAssistant {
let is_authenticated = || {
LanguageModelRegistry::read_global(cx)
.inline_assistant_model()
.map_or(false, |model| model.provider.is_authenticated(cx))
.active_provider()
.map_or(false, |provider| provider.is_authenticated(cx))
};
let thread_store = workspace
@@ -255,7 +254,6 @@ impl InlineAssistant {
assistant.assist(
&active_editor,
cx.entity().downgrade(),
workspace.project().downgrade(),
thread_store,
window,
cx,
@@ -267,7 +265,6 @@ impl InlineAssistant {
assistant.assist(
&active_terminal,
cx.entity().downgrade(),
workspace.project().downgrade(),
thread_store,
window,
cx,
@@ -282,8 +279,8 @@ impl InlineAssistant {
cx.spawn_in(window, async move |_workspace, cx| {
let Some(task) = cx.update(|_, cx| {
LanguageModelRegistry::read_global(cx)
.inline_assistant_model()
.map_or(None, |model| Some(model.provider.authenticate(cx)))
.active_provider()
.map_or(None, |provider| Some(provider.authenticate(cx)))
})?
else {
let answer = cx
@@ -298,7 +295,7 @@ impl InlineAssistant {
if let Some(answer) = answer {
if answer == 0 {
cx.update(|window, cx| {
window.dispatch_action(Box::new(OpenConfiguration), cx)
window.dispatch_action(Box::new(ShowConfiguration), cx)
})
.ok();
}
@@ -321,7 +318,6 @@ impl InlineAssistant {
&mut self,
editor: &Entity<Editor>,
workspace: WeakEntity<Workspace>,
project: WeakEntity<Project>,
thread_store: Option<WeakEntity<ThreadStore>>,
window: &mut Window,
cx: &mut App,
@@ -405,14 +401,14 @@ impl InlineAssistant {
codegen_ranges.push(anchor_range);
if let Some(model) = LanguageModelRegistry::read_global(cx).inline_assistant_model() {
self.telemetry.report_assistant_event(AssistantEventData {
if let Some(model) = LanguageModelRegistry::read_global(cx).active_model() {
self.telemetry.report_assistant_event(AssistantEvent {
conversation_id: None,
kind: AssistantKind::Inline,
phase: AssistantPhase::Invoked,
message_id: None,
model: model.model.telemetry_id(),
model_provider: model.provider.id().to_string(),
model: model.telemetry_id(),
model_provider: model.provider_id().to_string(),
response_latency: None,
error_message: None,
language_name: buffer.language().map(|language| language.name().to_proto()),
@@ -428,8 +424,7 @@ impl InlineAssistant {
let mut assist_to_focus = None;
for range in codegen_ranges {
let assist_id = self.next_assist_id.post_inc();
let context_store =
cx.new(|_cx| ContextStore::new(project.clone(), thread_store.clone()));
let context_store = cx.new(|_cx| ContextStore::new(workspace.clone()));
let codegen = cx.new(|cx| {
BufferCodegen::new(
editor.read(cx).buffer().clone(),
@@ -523,7 +518,7 @@ impl InlineAssistant {
initial_prompt: String,
initial_transaction_id: Option<TransactionId>,
focus: bool,
workspace: Entity<Workspace>,
workspace: WeakEntity<Workspace>,
thread_store: Option<WeakEntity<ThreadStore>>,
window: &mut Window,
cx: &mut App,
@@ -541,8 +536,7 @@ impl InlineAssistant {
range.end = range.end.bias_right(&snapshot);
}
let project = workspace.read(cx).project().downgrade();
let context_store = cx.new(|_cx| ContextStore::new(project, thread_store.clone()));
let context_store = cx.new(|_cx| ContextStore::new(workspace.clone()));
let codegen = cx.new(|cx| {
BufferCodegen::new(
@@ -566,7 +560,7 @@ impl InlineAssistant {
codegen.clone(),
self.fs.clone(),
context_store,
workspace.downgrade(),
workspace.clone(),
thread_store,
window,
cx,
@@ -593,7 +587,7 @@ impl InlineAssistant {
end_block_id,
range,
codegen.clone(),
workspace.downgrade(),
workspace.clone(),
window,
cx,
),
@@ -625,14 +619,14 @@ impl InlineAssistant {
BlockProperties {
style: BlockStyle::Sticky,
placement: BlockPlacement::Above(range.start),
height: Some(prompt_editor_height),
height: prompt_editor_height,
render: build_assist_editor_renderer(prompt_editor),
priority: 0,
},
BlockProperties {
style: BlockStyle::Sticky,
placement: BlockPlacement::Below(range.end),
height: None,
height: 0,
render: Arc::new(|cx| {
v_flex()
.h_full()
@@ -980,7 +974,7 @@ impl InlineAssistant {
let active_alternative = assist.codegen.read(cx).active_alternative().clone();
let message_id = active_alternative.read(cx).message_id.clone();
if let Some(model) = LanguageModelRegistry::read_global(cx).inline_assistant_model() {
if let Some(model) = LanguageModelRegistry::read_global(cx).active_model() {
let language_name = assist.editor.upgrade().and_then(|editor| {
let multibuffer = editor.read(cx).buffer().read(cx);
let snapshot = multibuffer.snapshot(cx);
@@ -991,7 +985,7 @@ impl InlineAssistant {
.map(|language| language.name())
});
report_assistant_event(
AssistantEventData {
AssistantEvent {
conversation_id: None,
kind: AssistantKind::Inline,
message_id,
@@ -1000,15 +994,15 @@ impl InlineAssistant {
} else {
AssistantPhase::Accepted
},
model: model.model.telemetry_id(),
model_provider: model.model.provider_id().to_string(),
model: model.telemetry_id(),
model_provider: model.provider_id().to_string(),
response_latency: None,
error_message: None,
language_name: language_name.map(|name| name.to_proto()),
},
Some(self.telemetry.clone()),
cx.http_client(),
model.model.api_key(cx),
model.api_key(cx),
cx.background_executor(),
);
}
@@ -1369,7 +1363,10 @@ impl InlineAssistant {
multi_buffer.update(cx, |multi_buffer, cx| {
multi_buffer.push_excerpts(
old_buffer.clone(),
Some(ExcerptRange::new(buffer_start..buffer_end)),
Some(ExcerptRange {
context: buffer_start..buffer_end,
primary: None,
}),
cx,
);
});
@@ -1396,7 +1393,7 @@ impl InlineAssistant {
deleted_lines_editor.update(cx, |editor, cx| editor.max_point(cx).row().0 + 1);
new_blocks.push(BlockProperties {
placement: BlockPlacement::Above(new_row),
height: Some(height),
height,
style: BlockStyle::Flex,
render: Arc::new(move |cx| {
div()
@@ -1783,7 +1780,6 @@ impl CodeActionProvider for AssistantCodeActionProvider {
let workspace = self.workspace.clone();
let thread_store = self.thread_store.clone();
window.spawn(cx, async move |cx| {
let workspace = workspace.upgrade().context("workspace was released")?;
let editor = editor.upgrade().context("editor was released")?;
let range = editor
.update(cx, |editor, cx| {

View File

@@ -1,4 +1,4 @@
use crate::assistant_model_selector::{AssistantModelSelector, ModelType};
use crate::assistant_model_selector::AssistantModelSelector;
use crate::buffer_codegen::BufferCodegen;
use crate::context_picker::ContextPicker;
use crate::context_store::ContextStore;
@@ -10,14 +10,14 @@ use crate::{RemoveAllContext, ToggleContextPicker};
use client::ErrorExt;
use collections::VecDeque;
use editor::{
Editor, EditorElement, EditorEvent, EditorMode, EditorStyle, GutterDimensions, MultiBuffer,
actions::{MoveDown, MoveUp},
Editor, EditorElement, EditorEvent, EditorMode, EditorStyle, GutterDimensions, MultiBuffer,
};
use feature_flags::{FeatureFlagAppExt as _, ZedPro};
use fs::Fs;
use gpui::{
AnyElement, App, ClickEvent, Context, CursorStyle, Entity, EventEmitter, FocusHandle,
Focusable, FontWeight, Subscription, TextStyle, WeakEntity, Window, anchored, deferred, point,
anchored, deferred, point, AnyElement, App, ClickEvent, Context, CursorStyle, Entity,
EventEmitter, FocusHandle, Focusable, FontWeight, Subscription, TextStyle, WeakEntity, Window,
};
use language_model::{LanguageModel, LanguageModelRegistry};
use language_model_selector::ToggleModelSelector;
@@ -28,7 +28,7 @@ use std::sync::Arc;
use theme::ThemeSettings;
use ui::utils::WithRemSize;
use ui::{
CheckboxWithLabel, IconButtonShape, KeyBinding, Popover, PopoverMenuHandle, Tooltip, prelude::*,
prelude::*, CheckboxWithLabel, IconButtonShape, KeyBinding, Popover, PopoverMenuHandle, Tooltip,
};
use util::ResultExt;
use workspace::Workspace;
@@ -455,55 +455,47 @@ impl<T: 'static> PromptEditor<T> {
match codegen_status {
CodegenStatus::Idle => {
vec![
Button::new("start", mode.start_label())
.label_size(LabelSize::Small)
.icon(IconName::Return)
.icon_size(IconSize::XSmall)
.icon_color(Color::Muted)
.on_click(
cx.listener(|_, _, _, cx| cx.emit(PromptEditorEvent::StartRequested)),
)
.into_any_element(),
]
vec![Button::new("start", mode.start_label())
.label_size(LabelSize::Small)
.icon(IconName::Return)
.icon_size(IconSize::XSmall)
.icon_color(Color::Muted)
.on_click(cx.listener(|_, _, _, cx| cx.emit(PromptEditorEvent::StartRequested)))
.into_any_element()]
}
CodegenStatus::Pending => vec![
IconButton::new("stop", IconName::Stop)
.icon_color(Color::Error)
.shape(IconButtonShape::Square)
.tooltip(move |window, cx| {
Tooltip::with_meta(
mode.tooltip_interrupt(),
Some(&menu::Cancel),
"Changes won't be discarded",
window,
cx,
)
})
.on_click(cx.listener(|_, _, _, cx| cx.emit(PromptEditorEvent::StopRequested)))
.into_any_element(),
],
CodegenStatus::Pending => vec![IconButton::new("stop", IconName::Stop)
.icon_color(Color::Error)
.shape(IconButtonShape::Square)
.tooltip(move |window, cx| {
Tooltip::with_meta(
mode.tooltip_interrupt(),
Some(&menu::Cancel),
"Changes won't be discarded",
window,
cx,
)
})
.on_click(cx.listener(|_, _, _, cx| cx.emit(PromptEditorEvent::StopRequested)))
.into_any_element()],
CodegenStatus::Done | CodegenStatus::Error(_) => {
let has_error = matches!(codegen_status, CodegenStatus::Error(_));
if has_error || self.edited_since_done {
vec![
IconButton::new("restart", IconName::RotateCw)
.icon_color(Color::Info)
.shape(IconButtonShape::Square)
.tooltip(move |window, cx| {
Tooltip::with_meta(
mode.tooltip_restart(),
Some(&menu::Confirm),
"Changes will be discarded",
window,
cx,
)
})
.on_click(cx.listener(|_, _, _, cx| {
cx.emit(PromptEditorEvent::StartRequested);
}))
.into_any_element(),
]
vec![IconButton::new("restart", IconName::RotateCw)
.icon_color(Color::Info)
.shape(IconButtonShape::Square)
.tooltip(move |window, cx| {
Tooltip::with_meta(
mode.tooltip_restart(),
Some(&menu::Confirm),
"Changes will be discarded",
window,
cx,
)
})
.on_click(cx.listener(|_, _, _, cx| {
cx.emit(PromptEditorEvent::StartRequested);
}))
.into_any_element()]
} else {
let accept = IconButton::new("accept", IconName::Check)
.icon_color(Color::Info)
@@ -582,7 +574,7 @@ impl<T: 'static> PromptEditor<T> {
let disabled = matches!(codegen.status(cx), CodegenStatus::Idle);
let model_registry = LanguageModelRegistry::read_global(cx);
let default_model = model_registry.default_model().map(|default| default.model);
let default_model = model_registry.active_model();
let alternative_models = model_registry.inline_alternative_models();
let get_model_name = |index: usize| -> String {
@@ -890,7 +882,6 @@ impl PromptEditor<BufferCodegen> {
fs,
model_selector_menu_handle,
prompt_editor.focus_handle(cx),
ModelType::InlineAssistant,
window,
cx,
)
@@ -1043,7 +1034,6 @@ impl PromptEditor<TerminalCodegen> {
fs,
model_selector_menu_handle.clone(),
prompt_editor.focus_handle(cx),
ModelType::InlineAssistant,
window,
cx,
)

View File

@@ -0,0 +1,686 @@
use std::sync::Arc;
use collections::HashSet;
use editor::actions::MoveUp;
use editor::{ContextMenuOptions, ContextMenuPlacement, Editor, EditorElement, EditorStyle};
use fs::Fs;
use git::ExpandCommitEditor;
use git_ui::git_panel;
use gpui::{
point, Animation, AnimationExt, App, DismissEvent, Entity, Focusable, Subscription, TextStyle,
WeakEntity,
};
use language_model::LanguageModelRegistry;
use language_model_selector::ToggleModelSelector;
use project::Project;
use settings::Settings;
use std::time::Duration;
use theme::ThemeSettings;
use ui::{
prelude::*, ButtonLike, KeyBinding, PlatformStyle, PopoverMenu, PopoverMenuHandle, Tooltip,
};
use vim_mode_setting::VimModeSetting;
use workspace::Workspace;
use crate::assistant_model_selector::AssistantModelSelector;
use crate::context_picker::{ConfirmBehavior, ContextPicker, ContextPickerCompletionProvider};
use crate::context_store::{refresh_context_store_text, ContextStore};
use crate::context_strip::{ContextStrip, ContextStripEvent, SuggestContextKind};
use crate::profile_selector::ProfileSelector;
use crate::thread::{RequestKind, Thread};
use crate::thread_store::ThreadStore;
use crate::{Chat, ChatMode, RemoveAllContext, ThreadEvent, ToggleContextPicker};
pub struct MessageEditor {
thread: Entity<Thread>,
editor: Entity<Editor>,
#[allow(dead_code)]
workspace: WeakEntity<Workspace>,
project: Entity<Project>,
context_store: Entity<ContextStore>,
context_strip: Entity<ContextStrip>,
context_picker_menu_handle: PopoverMenuHandle<ContextPicker>,
inline_context_picker: Entity<ContextPicker>,
inline_context_picker_menu_handle: PopoverMenuHandle<ContextPicker>,
model_selector: Entity<AssistantModelSelector>,
profile_selector: Entity<ProfileSelector>,
_subscriptions: Vec<Subscription>,
}
impl MessageEditor {
pub fn new(
fs: Arc<dyn Fs>,
workspace: WeakEntity<Workspace>,
context_store: Entity<ContextStore>,
thread_store: WeakEntity<ThreadStore>,
thread: Entity<Thread>,
window: &mut Window,
cx: &mut Context<Self>,
) -> Self {
let context_picker_menu_handle = PopoverMenuHandle::default();
let inline_context_picker_menu_handle = PopoverMenuHandle::default();
let model_selector_menu_handle = PopoverMenuHandle::default();
let editor = cx.new(|cx| {
let mut editor = Editor::auto_height(10, window, cx);
editor.set_placeholder_text("Ask anything, @ to mention, ↑ to select", cx);
editor.set_show_indent_guides(false, cx);
editor.set_context_menu_options(ContextMenuOptions {
min_entries_visible: 12,
max_entries_visible: 12,
placement: Some(ContextMenuPlacement::Above),
});
editor
});
let editor_entity = editor.downgrade();
editor.update(cx, |editor, _| {
editor.set_completion_provider(Some(Box::new(ContextPickerCompletionProvider::new(
workspace.clone(),
context_store.downgrade(),
Some(thread_store.clone()),
editor_entity,
))));
});
let inline_context_picker = cx.new(|cx| {
ContextPicker::new(
workspace.clone(),
Some(thread_store.clone()),
context_store.downgrade(),
ConfirmBehavior::Close,
window,
cx,
)
});
let context_strip = cx.new(|cx| {
ContextStrip::new(
context_store.clone(),
workspace.clone(),
Some(thread_store.clone()),
context_picker_menu_handle.clone(),
SuggestContextKind::File,
window,
cx,
)
});
let subscriptions = vec![
cx.subscribe_in(
&inline_context_picker,
window,
Self::handle_inline_context_picker_event,
),
cx.subscribe_in(&context_strip, window, Self::handle_context_strip_event),
];
Self {
editor: editor.clone(),
project: thread.read(cx).project().clone(),
thread,
workspace,
context_store,
context_strip,
context_picker_menu_handle,
inline_context_picker,
inline_context_picker_menu_handle,
model_selector: cx.new(|cx| {
AssistantModelSelector::new(
fs.clone(),
model_selector_menu_handle,
editor.focus_handle(cx),
window,
cx,
)
}),
profile_selector: cx.new(|cx| ProfileSelector::new(fs, thread_store, cx)),
_subscriptions: subscriptions,
}
}
fn toggle_chat_mode(&mut self, _: &ChatMode, _window: &mut Window, cx: &mut Context<Self>) {
cx.notify();
}
fn toggle_context_picker(
&mut self,
_: &ToggleContextPicker,
window: &mut Window,
cx: &mut Context<Self>,
) {
self.context_picker_menu_handle.toggle(window, cx);
}
pub fn remove_all_context(
&mut self,
_: &RemoveAllContext,
_window: &mut Window,
cx: &mut Context<Self>,
) {
self.context_store.update(cx, |store, _cx| store.clear());
cx.notify();
}
fn chat(&mut self, _: &Chat, window: &mut Window, cx: &mut Context<Self>) {
if self.is_editor_empty(cx) {
return;
}
if self.thread.read(cx).is_generating() {
return;
}
self.send_to_model(RequestKind::Chat, window, cx);
}
fn is_editor_empty(&self, cx: &App) -> bool {
self.editor.read(cx).text(cx).is_empty()
}
fn is_model_selected(&self, cx: &App) -> bool {
LanguageModelRegistry::read_global(cx)
.active_model()
.is_some()
}
fn send_to_model(
&mut self,
request_kind: RequestKind,
window: &mut Window,
cx: &mut Context<Self>,
) {
let provider = LanguageModelRegistry::read_global(cx).active_provider();
if provider
.as_ref()
.map_or(false, |provider| provider.must_accept_terms(cx))
{
cx.notify();
return;
}
let model_registry = LanguageModelRegistry::read_global(cx);
let Some(model) = model_registry.active_model() else {
return;
};
let user_message = self.editor.update(cx, |editor, cx| {
let text = editor.text(cx);
editor.clear(window, cx);
text
});
let refresh_task =
refresh_context_store_text(self.context_store.clone(), &HashSet::default(), cx);
let system_prompt_context_task = self.thread.read(cx).load_system_prompt_context(cx);
let thread = self.thread.clone();
let context_store = self.context_store.clone();
let checkpoint = self.project.read(cx).git_store().read(cx).checkpoint(cx);
cx.spawn(async move |_, cx| {
let checkpoint = checkpoint.await.ok();
refresh_task.await;
let (system_prompt_context, load_error) = system_prompt_context_task.await;
thread
.update(cx, |thread, cx| {
thread.set_system_prompt_context(system_prompt_context);
if let Some(load_error) = load_error {
cx.emit(ThreadEvent::ShowError(load_error));
}
})
.ok();
thread
.update(cx, |thread, cx| {
let context = context_store.read(cx).snapshot(cx).collect::<Vec<_>>();
thread.insert_user_message(user_message, context, checkpoint, cx);
thread.send_to_model(model, request_kind, cx);
})
.ok();
})
.detach();
}
fn handle_inline_context_picker_event(
&mut self,
_inline_context_picker: &Entity<ContextPicker>,
_event: &DismissEvent,
window: &mut Window,
cx: &mut Context<Self>,
) {
let editor_focus_handle = self.editor.focus_handle(cx);
window.focus(&editor_focus_handle);
}
fn handle_context_strip_event(
&mut self,
_context_strip: &Entity<ContextStrip>,
event: &ContextStripEvent,
window: &mut Window,
cx: &mut Context<Self>,
) {
match event {
ContextStripEvent::PickerDismissed
| ContextStripEvent::BlurredEmpty
| ContextStripEvent::BlurredDown => {
let editor_focus_handle = self.editor.focus_handle(cx);
window.focus(&editor_focus_handle);
}
ContextStripEvent::BlurredUp => {}
}
}
fn move_up(&mut self, _: &MoveUp, window: &mut Window, cx: &mut Context<Self>) {
if self.context_picker_menu_handle.is_deployed()
|| self.inline_context_picker_menu_handle.is_deployed()
{
cx.propagate();
} else {
self.context_strip.focus_handle(cx).focus(window);
}
}
}
impl Focusable for MessageEditor {
fn focus_handle(&self, cx: &App) -> gpui::FocusHandle {
self.editor.focus_handle(cx)
}
}
impl Render for MessageEditor {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let font_size = TextSize::Default.rems(cx);
let line_height = font_size.to_pixels(window.rem_size()) * 1.5;
let focus_handle = self.editor.focus_handle(cx);
let inline_context_picker = self.inline_context_picker.clone();
let empty_thread = self.thread.read(cx).is_empty();
let is_generating = self.thread.read(cx).is_generating();
let is_model_selected = self.is_model_selected(cx);
let is_editor_empty = self.is_editor_empty(cx);
let submit_label_color = if is_editor_empty {
Color::Muted
} else {
Color::Default
};
let vim_mode_enabled = VimModeSetting::get_global(cx).0;
let platform = PlatformStyle::platform();
let linux = platform == PlatformStyle::Linux;
let windows = platform == PlatformStyle::Windows;
let button_width = if linux || windows || vim_mode_enabled {
px(82.)
} else {
px(64.)
};
let project = self.thread.read(cx).project();
let changed_files = if let Some(repository) = project.read(cx).active_repository(cx) {
repository.read(cx).cached_status().count()
} else {
0
};
let border_color = cx.theme().colors().border;
let active_color = cx.theme().colors().element_selected;
let editor_bg_color = cx.theme().colors().editor_background;
let bg_edit_files_disclosure = editor_bg_color.blend(active_color.opacity(0.3));
let edit_files_container = || {
h_flex()
.mx_2()
.py_1()
.pl_2p5()
.pr_1()
.bg(bg_edit_files_disclosure)
.border_1()
.border_color(border_color)
.justify_between()
.flex_wrap()
};
v_flex()
.size_full()
.when(is_generating, |parent| {
let focus_handle = self.editor.focus_handle(cx).clone();
parent.child(
h_flex().py_3().w_full().justify_center().child(
h_flex()
.flex_none()
.pl_2()
.pr_1()
.py_1()
.bg(editor_bg_color)
.border_1()
.border_color(cx.theme().colors().border_variant)
.rounded_lg()
.shadow_md()
.gap_1()
.child(
Icon::new(IconName::ArrowCircle)
.size(IconSize::XSmall)
.color(Color::Muted)
.with_animation(
"arrow-circle",
Animation::new(Duration::from_secs(2)).repeat(),
|icon, delta| {
icon.transform(gpui::Transformation::rotate(
gpui::percentage(delta),
))
},
),
)
.child(
Label::new("Generating…")
.size(LabelSize::XSmall)
.color(Color::Muted),
)
.child(ui::Divider::vertical())
.child(
Button::new("cancel-generation", "Cancel")
.label_size(LabelSize::XSmall)
.key_binding(
KeyBinding::for_action_in(
&editor::actions::Cancel,
&focus_handle,
window,
cx,
)
.map(|kb| kb.size(rems_from_px(10.))),
)
.on_click(move |_event, window, cx| {
focus_handle.dispatch_action(
&editor::actions::Cancel,
window,
cx,
);
}),
),
),
)
})
.when(
changed_files > 0 && !is_generating && !empty_thread,
|parent| {
parent.child(
edit_files_container()
.border_b_0()
.rounded_t_md()
.shadow(smallvec::smallvec![gpui::BoxShadow {
color: gpui::black().opacity(0.15),
offset: point(px(1.), px(-1.)),
blur_radius: px(3.),
spread_radius: px(0.),
}])
.child(
h_flex()
.gap_2()
.child(Label::new("Edits").size(LabelSize::XSmall))
.child(div().size_1().rounded_full().bg(border_color))
.child(
Label::new(format!(
"{} {}",
changed_files,
if changed_files == 1 { "file" } else { "files" }
))
.size(LabelSize::XSmall),
),
)
.child(
h_flex()
.gap_1()
.child(
Button::new("panel", "Open Git Panel")
.label_size(LabelSize::XSmall)
.key_binding({
let focus_handle = focus_handle.clone();
KeyBinding::for_action_in(
&git_panel::ToggleFocus,
&focus_handle,
window,
cx,
)
.map(|kb| kb.size(rems_from_px(10.)))
})
.on_click(|_ev, _window, cx| {
cx.defer(|cx| {
cx.dispatch_action(&git_panel::ToggleFocus)
});
}),
)
.child(
Button::new("review", "Review Diff")
.label_size(LabelSize::XSmall)
.key_binding({
let focus_handle = focus_handle.clone();
KeyBinding::for_action_in(
&git_ui::project_diff::Diff,
&focus_handle,
window,
cx,
)
.map(|kb| kb.size(rems_from_px(10.)))
})
.on_click(|_event, _window, cx| {
cx.defer(|cx| {
cx.dispatch_action(&git_ui::project_diff::Diff)
});
}),
)
.child(
Button::new("commit", "Commit Changes")
.label_size(LabelSize::XSmall)
.key_binding({
let focus_handle = focus_handle.clone();
KeyBinding::for_action_in(
&ExpandCommitEditor,
&focus_handle,
window,
cx,
)
.map(|kb| kb.size(rems_from_px(10.)))
})
.on_click(|_event, _window, cx| {
cx.defer(|cx| {
cx.dispatch_action(&ExpandCommitEditor)
});
}),
),
),
)
},
)
.when(
changed_files > 0 && !is_generating && empty_thread,
|parent| {
parent.child(
edit_files_container()
.mb_2()
.rounded_md()
.child(
h_flex()
.gap_2()
.child(Label::new("Consider committing your changes before starting a fresh thread").size(LabelSize::XSmall))
.child(div().size_1().rounded_full().bg(border_color))
.child(
Label::new(format!(
"{} {}",
changed_files,
if changed_files == 1 { "file" } else { "files" }
))
.size(LabelSize::XSmall),
),
)
.child(
h_flex()
.gap_1()
.child(
Button::new("review", "Review Diff")
.label_size(LabelSize::XSmall)
.key_binding({
let focus_handle = focus_handle.clone();
KeyBinding::for_action_in(
&git_ui::project_diff::Diff,
&focus_handle,
window,
cx,
)
.map(|kb| kb.size(rems_from_px(10.)))
})
.on_click(|_event, _window, cx| {
cx.defer(|cx| {
cx.dispatch_action(&git_ui::project_diff::Diff)
});
}),
)
.child(
Button::new("commit", "Commit Changes")
.label_size(LabelSize::XSmall)
.key_binding({
let focus_handle = focus_handle.clone();
KeyBinding::for_action_in(
&ExpandCommitEditor,
&focus_handle,
window,
cx,
)
.map(|kb| kb.size(rems_from_px(10.)))
})
.on_click(|_event, _window, cx| {
cx.defer(|cx| {
cx.dispatch_action(&ExpandCommitEditor)
});
}),
),
),
)
},
)
.child(
v_flex()
.key_context("MessageEditor")
.on_action(cx.listener(Self::chat))
.on_action(cx.listener(|this, _: &ToggleModelSelector, window, cx| {
this.model_selector
.update(cx, |model_selector, cx| model_selector.toggle(window, cx));
}))
.on_action(cx.listener(Self::toggle_context_picker))
.on_action(cx.listener(Self::remove_all_context))
.on_action(cx.listener(Self::move_up))
.on_action(cx.listener(Self::toggle_chat_mode))
.gap_2()
.p_2()
.bg(editor_bg_color)
.border_t_1()
.border_color(cx.theme().colors().border)
.child(h_flex().justify_between().child(self.context_strip.clone()))
.child(
v_flex()
.gap_5()
.child({
let settings = ThemeSettings::get_global(cx);
let text_style = TextStyle {
color: cx.theme().colors().text,
font_family: settings.ui_font.family.clone(),
font_fallbacks: settings.ui_font.fallbacks.clone(),
font_features: settings.ui_font.features.clone(),
font_size: font_size.into(),
font_weight: settings.ui_font.weight,
line_height: line_height.into(),
..Default::default()
};
EditorElement::new(
&self.editor,
EditorStyle {
background: editor_bg_color,
local_player: cx.theme().players().local(),
text: text_style,
syntax: cx.theme().syntax().clone(),
..Default::default()
},
)
})
.child(
PopoverMenu::new("inline-context-picker")
.menu(move |window, cx| {
inline_context_picker.update(cx, |this, cx| {
this.init(window, cx);
});
Some(inline_context_picker.clone())
})
.attach(gpui::Corner::TopLeft)
.anchor(gpui::Corner::BottomLeft)
.offset(gpui::Point {
x: px(0.0),
y: (-ThemeSettings::get_global(cx).ui_font_size(cx) * 2)
- px(4.0),
})
.with_handle(self.inline_context_picker_menu_handle.clone()),
)
.child(
h_flex()
.justify_between()
.child(h_flex().gap_2().child(self.profile_selector.clone()))
.child(
h_flex().gap_1().child(self.model_selector.clone()).child(
ButtonLike::new("submit-message")
.width(button_width.into())
.style(ButtonStyle::Filled)
.disabled(
is_editor_empty
|| !is_model_selected
|| is_generating,
)
.child(
h_flex()
.w_full()
.justify_between()
.child(
Label::new("Submit")
.size(LabelSize::Small)
.color(submit_label_color),
)
.children(
KeyBinding::for_action_in(
&Chat,
&focus_handle,
window,
cx,
)
.map(|binding| {
binding
.when(vim_mode_enabled, |kb| {
kb.size(rems_from_px(12.))
})
.into_any_element()
}),
),
)
.on_click(move |_event, window, cx| {
focus_handle.dispatch_action(&Chat, window, cx);
})
.when(is_editor_empty, |button| {
button.tooltip(Tooltip::text(
"Type a message to submit",
))
})
.when(is_generating, |button| {
button.tooltip(Tooltip::text(
"Cancel to submit a new message",
))
})
.when(!is_model_selected, |button| {
button.tooltip(Tooltip::text(
"Select a model to continue",
))
}),
),
),
),
),
)
}
}

Some files were not shown because too many files have changed in this diff Show More