Compare commits
60 Commits
editor-mod
...
cherry-pic
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5eb04c7b28 | ||
|
|
0470baca50 | ||
|
|
4605b96630 | ||
|
|
949398cb93 | ||
|
|
79e74b880b | ||
|
|
59af2a7d1f | ||
|
|
c786c0150f | ||
|
|
5fd29d37a6 | ||
|
|
f1204dfc33 | ||
|
|
2e1ca47241 | ||
|
|
5c346a4ccf | ||
|
|
a102b08743 | ||
|
|
2dc4f156b3 | ||
|
|
557753d092 | ||
|
|
65fb17e2c9 | ||
|
|
2fe3dbed31 | ||
|
|
fda5111dc0 | ||
|
|
69127d2bea | ||
|
|
db949546cf | ||
|
|
2b5a302972 | ||
|
|
4c0ad95acc | ||
|
|
8c83281399 | ||
|
|
dfc99de7b8 | ||
|
|
fe5e81203f | ||
|
|
c48197b280 | ||
|
|
11545c669e | ||
|
|
a79aef7bdd | ||
|
|
d8bffd7ef2 | ||
|
|
54c7d9dc5f | ||
|
|
dd6fce6d4e | ||
|
|
de5f87e8f2 | ||
|
|
1b91f3de41 | ||
|
|
19764794b7 | ||
|
|
d49409caba | ||
|
|
60ea4754b2 | ||
|
|
61bc1cc441 | ||
|
|
70575d1115 | ||
|
|
ea42013746 | ||
|
|
5da31fdb72 | ||
|
|
f48a8f2b6a | ||
|
|
d24cad30f3 | ||
|
|
153724aad3 | ||
|
|
bc566fe18e | ||
|
|
91b2a84001 | ||
|
|
e6267c42f7 | ||
|
|
f649c31bf9 | ||
|
|
639417c2bc | ||
|
|
896a35f7be | ||
|
|
4560d1ec58 | ||
|
|
18ac4ac5ef | ||
|
|
72bd248544 | ||
|
|
42ae3301d0 | ||
|
|
eb0f9ddcdc | ||
|
|
ac9fdaa1da | ||
|
|
8204ef1e51 | ||
|
|
3d2fa72d1f | ||
|
|
92bbcdeb7d | ||
|
|
54df43e06f | ||
|
|
4f0fad6996 | ||
|
|
3b7c1744b4 |
151
.github/workflows/cherry-pick.yml
vendored
Normal file
151
.github/workflows/cherry-pick.yml
vendored
Normal file
@@ -0,0 +1,151 @@
|
||||
name: Cherry Pick
|
||||
|
||||
on:
|
||||
issue_comment:
|
||||
types: [created]
|
||||
pull_request_target:
|
||||
types: [closed]
|
||||
|
||||
jobs:
|
||||
cherry-pick:
|
||||
# This job will run when a PR is merged with a specific comment,
|
||||
# or when a comment is added to an already merged PR.
|
||||
runs-on: ubuntu-latest
|
||||
# Use pull_request_target so that we can add comments back to the PR
|
||||
# if the cherry-pick fails.
|
||||
permissions:
|
||||
pull-requests: write
|
||||
contents: write
|
||||
issues: write
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0 # Required to get all history for cherry-picking
|
||||
|
||||
- name: Extract info and determine trigger
|
||||
id: info
|
||||
run: |
|
||||
# Default to failure unless a valid trigger is found
|
||||
echo "valid=false" >> $GITHUB_OUTPUT
|
||||
|
||||
if [[ "${{ github.event_name }}" == "pull_request_target" && "${{ github.event.pull_request.merged }}" == "true" ]]; then
|
||||
echo "Triggered by PR merge"
|
||||
PR_NUMBER="${{ github.event.pull_request.number }}"
|
||||
|
||||
# Check PR body first, then fall back to comments
|
||||
TEXT_TO_SEARCH="${{ github.event.pull_request.body }}"
|
||||
if [[ ! "$TEXT_TO_SEARCH" =~ /cherry-pick[[:space:]]+(stable|preview) ]]; then
|
||||
echo "Command not found in PR body. Checking comments..."
|
||||
TEXT_TO_SEARCH=$(gh pr view $PR_NUMBER --json comments -q '.comments[].body' | tail -n 100)
|
||||
fi
|
||||
|
||||
if [[ "$TEXT_TO_SEARCH" =~ /cherry-pick[[:space:]]+(stable|preview) ]]; then
|
||||
echo "Found cherry-pick command."
|
||||
MERGE_SHA="${{ github.event.pull_request.merge_commit_sha }}"
|
||||
# Get the last matching command in the text
|
||||
CHANNEL=$(echo "$TEXT_TO_SEARCH" | grep -oP '/cherry-pick[[:space:]]+\K(stable|preview)' | tail -n1)
|
||||
|
||||
echo "valid=true" >> $GITHUB_OUTPUT
|
||||
echo "merge_sha=$MERGE_SHA" >> $GITHUB_OUTPUT
|
||||
echo "pr_number=$PR_NUMBER" >> $GITHUB_OUTPUT
|
||||
echo "channel=$CHANNEL" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "No cherry-pick command found in PR body or recent comments. Exiting."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
elif [[ "${{ github.event_name }}" == "issue_comment" && "${{ github.event.issue.pull_request }}" != "" ]]; then
|
||||
echo "Triggered by issue comment"
|
||||
COMMENT_BODY="${{ github.event.comment.body }}"
|
||||
if [[ ! "$COMMENT_BODY" =~ /cherry-pick[[:space:]]+(stable|preview) ]]; then
|
||||
echo "Comment does not contain cherry-pick command. Exiting."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
PR_NUMBER="${{ github.event.issue.number }}"
|
||||
|
||||
# Check if the PR is merged
|
||||
MERGE_SHA=$(gh pr view $PR_NUMBER --json mergeCommit -q .mergeCommit.oid)
|
||||
if [[ -z "$MERGE_SHA" ]]; then
|
||||
echo "PR #$PR_NUMBER is not merged. Exiting."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
CHANNEL=$(echo "$COMMENT_BODY" | grep -oP '/cherry-pick[[:space:]]+\K(stable|preview)' | head -n1)
|
||||
|
||||
echo "valid=true" >> $GITHUB_OUTPUT
|
||||
echo "merge_sha=$MERGE_SHA" >> $GITHUB_OUTPUT
|
||||
echo "pr_number=$PR_NUMBER" >> $GITHUB_OUTPUT
|
||||
echo "channel=$CHANNEL" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Cherry-pick
|
||||
if: steps.info.outputs.valid == 'true'
|
||||
run: |
|
||||
set -e
|
||||
|
||||
CHANNEL="${{ steps.info.outputs.channel }}"
|
||||
MERGE_SHA="${{ steps.info.outputs.merge_sha }}"
|
||||
PR_NUMBER="${{ steps.info.outputs.pr_number }}"
|
||||
|
||||
# Get the latest version for the channel
|
||||
echo "Fetching latest version for '$CHANNEL' channel..."
|
||||
query=""
|
||||
case $CHANNEL in
|
||||
stable)
|
||||
;;
|
||||
preview)
|
||||
query="&preview=1"
|
||||
;;
|
||||
*)
|
||||
echo "Invalid channel: $CHANNEL" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
LATEST_VERSION=$(curl -s "https://zed.dev/api/releases/latest?asset=zed&os=macos&arch=aarch64$query" | jq -r .version)
|
||||
|
||||
if [[ -z "$LATEST_VERSION" ]]; then
|
||||
echo "Could not fetch latest version for channel '$CHANNEL'"
|
||||
gh pr comment $PR_NUMBER --body "Could not fetch latest version for channel '$CHANNEL' from zed.dev API."
|
||||
exit 1
|
||||
fi
|
||||
echo "Latest version is $LATEST_VERSION"
|
||||
|
||||
# Construct target branch name (e.g., v0.85.4 -> v0.85.x)
|
||||
TARGET_BRANCH=$(echo "$LATEST_VERSION" | sed -E 's/v([0-9]+\.[0-9]+)\..*/v\1.x/')
|
||||
echo "Target branch is $TARGET_BRANCH"
|
||||
|
||||
# Configure git
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||
|
||||
# Create and push the cherry-pick branch
|
||||
NEW_BRANCH="cherry-pick/pr-${PR_NUMBER}-to-${TARGET_BRANCH}"
|
||||
|
||||
git fetch origin $TARGET_BRANCH
|
||||
git checkout -b $NEW_BRANCH "origin/$TARGET_BRANCH"
|
||||
|
||||
echo "Attempting to cherry-pick $MERGE_SHA..."
|
||||
if ! git cherry-pick $MERGE_SHA; then
|
||||
echo "Cherry-pick failed. Please resolve conflicts manually."
|
||||
gh pr comment $PR_NUMBER --body "Automated cherry-pick to \`$TARGET_BRANCH\` failed due to conflicts. Please resolve them manually."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Pushing new branch $NEW_BRANCH..."
|
||||
git push -u origin $NEW_BRANCH
|
||||
|
||||
# Create the pull request
|
||||
echo "Creating pull request..."
|
||||
gh pr create \
|
||||
--title "Cherry-pick PR #${PR_NUMBER} to ${TARGET_BRANCH}" \
|
||||
--body "This PR cherry-picks the changes from #${PR_NUMBER} to the \`$TARGET_BRANCH\` branch." \
|
||||
--base $TARGET_BRANCH \
|
||||
--head $NEW_BRANCH \
|
||||
--reviewer "${{ github.actor }}"
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
129
Cargo.lock
generated
129
Cargo.lock
generated
@@ -39,6 +39,26 @@ dependencies = [
|
||||
"workspace-hack",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "acp_tools"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"agent-client-protocol",
|
||||
"collections",
|
||||
"gpui",
|
||||
"language",
|
||||
"markdown",
|
||||
"project",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"settings",
|
||||
"theme",
|
||||
"ui",
|
||||
"util",
|
||||
"workspace",
|
||||
"workspace-hack",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "action_log"
|
||||
version = "0.1.0"
|
||||
@@ -171,11 +191,12 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "agent-client-protocol"
|
||||
version = "0.0.30"
|
||||
version = "0.0.31"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5f792e009ba59b137ee1db560bc37e567887ad4b5af6f32181d381fff690e2d4"
|
||||
checksum = "289eb34ee17213dadcca47eedadd386a5e7678094095414e475965d1bcca2860"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-broadcast",
|
||||
"futures 0.3.31",
|
||||
"log",
|
||||
"parking_lot",
|
||||
@@ -264,10 +285,10 @@ name = "agent_servers"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"acp_thread",
|
||||
"acp_tools",
|
||||
"action_log",
|
||||
"agent-client-protocol",
|
||||
"agent_settings",
|
||||
"agentic-coding-protocol",
|
||||
"anyhow",
|
||||
"client",
|
||||
"collections",
|
||||
@@ -313,7 +334,6 @@ dependencies = [
|
||||
"anyhow",
|
||||
"cloud_llm_client",
|
||||
"collections",
|
||||
"editor_mode_setting",
|
||||
"fs",
|
||||
"gpui",
|
||||
"language_model",
|
||||
@@ -355,7 +375,6 @@ dependencies = [
|
||||
"context_server",
|
||||
"db",
|
||||
"editor",
|
||||
"editor_mode_setting",
|
||||
"extension",
|
||||
"extension_host",
|
||||
"feature_flags",
|
||||
@@ -384,6 +403,7 @@ dependencies = [
|
||||
"parking_lot",
|
||||
"paths",
|
||||
"picker",
|
||||
"postage",
|
||||
"pretty_assertions",
|
||||
"project",
|
||||
"prompt_store",
|
||||
@@ -423,24 +443,6 @@ dependencies = [
|
||||
"zed_actions",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "agentic-coding-protocol"
|
||||
version = "0.0.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a3e6ae951b36fa2f8d9dd6e1af6da2fcaba13d7c866cf6a9e65deda9dc6c5fe4"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"chrono",
|
||||
"derive_more 2.0.1",
|
||||
"futures 0.3.31",
|
||||
"log",
|
||||
"parking_lot",
|
||||
"schemars",
|
||||
"semver",
|
||||
"serde",
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ahash"
|
||||
version = "0.7.8"
|
||||
@@ -856,7 +858,7 @@ dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
"collections",
|
||||
"derive_more 0.99.19",
|
||||
"derive_more",
|
||||
"extension",
|
||||
"futures 0.3.31",
|
||||
"gpui",
|
||||
@@ -919,7 +921,7 @@ dependencies = [
|
||||
"clock",
|
||||
"collections",
|
||||
"ctor",
|
||||
"derive_more 0.99.19",
|
||||
"derive_more",
|
||||
"gpui",
|
||||
"icons",
|
||||
"indoc",
|
||||
@@ -956,7 +958,7 @@ dependencies = [
|
||||
"cloud_llm_client",
|
||||
"collections",
|
||||
"component",
|
||||
"derive_more 0.99.19",
|
||||
"derive_more",
|
||||
"diffy",
|
||||
"editor",
|
||||
"feature_flags",
|
||||
@@ -3069,7 +3071,7 @@ dependencies = [
|
||||
"cocoa 0.26.0",
|
||||
"collections",
|
||||
"credentials_provider",
|
||||
"derive_more 0.99.19",
|
||||
"derive_more",
|
||||
"feature_flags",
|
||||
"fs",
|
||||
"futures 0.3.31",
|
||||
@@ -3501,7 +3503,7 @@ name = "command_palette_hooks"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"collections",
|
||||
"derive_more 0.99.19",
|
||||
"derive_more",
|
||||
"gpui",
|
||||
"workspace-hack",
|
||||
]
|
||||
@@ -4549,7 +4551,6 @@ dependencies = [
|
||||
"db",
|
||||
"debugger_tools",
|
||||
"editor",
|
||||
"editor_mode_setting",
|
||||
"file_icons",
|
||||
"futures 0.3.31",
|
||||
"fuzzy",
|
||||
@@ -4665,27 +4666,6 @@ dependencies = [
|
||||
"syn 2.0.101",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "derive_more"
|
||||
version = "2.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "093242cf7570c207c83073cf82f79706fe7b8317e98620a47d5be7c3d8497678"
|
||||
dependencies = [
|
||||
"derive_more-impl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "derive_more-impl"
|
||||
version = "2.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.101",
|
||||
"unicode-xid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "derive_refineable"
|
||||
version = "0.1.0"
|
||||
@@ -4706,7 +4686,6 @@ dependencies = [
|
||||
"component",
|
||||
"ctor",
|
||||
"editor",
|
||||
"futures 0.3.31",
|
||||
"gpui",
|
||||
"indoc",
|
||||
"language",
|
||||
@@ -5064,7 +5043,6 @@ dependencies = [
|
||||
"dap",
|
||||
"db",
|
||||
"edit_prediction",
|
||||
"editor_mode_setting",
|
||||
"emojis",
|
||||
"file_icons",
|
||||
"fs",
|
||||
@@ -5125,19 +5103,6 @@ dependencies = [
|
||||
"zlog",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "editor_mode_setting"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"gpui",
|
||||
"schemars",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"settings",
|
||||
"workspace-hack",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "either"
|
||||
version = "1.15.0"
|
||||
@@ -5629,7 +5594,6 @@ dependencies = [
|
||||
"collections",
|
||||
"db",
|
||||
"editor",
|
||||
"editor_mode_setting",
|
||||
"extension",
|
||||
"extension_host",
|
||||
"fs",
|
||||
@@ -5650,6 +5614,7 @@ dependencies = [
|
||||
"theme",
|
||||
"ui",
|
||||
"util",
|
||||
"vim_mode_setting",
|
||||
"workspace",
|
||||
"workspace-hack",
|
||||
"zed_actions",
|
||||
@@ -6436,7 +6401,7 @@ dependencies = [
|
||||
"askpass",
|
||||
"async-trait",
|
||||
"collections",
|
||||
"derive_more 0.99.19",
|
||||
"derive_more",
|
||||
"futures 0.3.31",
|
||||
"git2",
|
||||
"gpui",
|
||||
@@ -7466,7 +7431,7 @@ dependencies = [
|
||||
"core-video",
|
||||
"cosmic-text",
|
||||
"ctor",
|
||||
"derive_more 0.99.19",
|
||||
"derive_more",
|
||||
"embed-resource",
|
||||
"env_logger 0.11.8",
|
||||
"etagere",
|
||||
@@ -7991,7 +7956,7 @@ version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bytes 1.10.1",
|
||||
"derive_more 0.99.19",
|
||||
"derive_more",
|
||||
"futures 0.3.31",
|
||||
"http 1.3.1",
|
||||
"http-body 1.0.1",
|
||||
@@ -8503,6 +8468,7 @@ dependencies = [
|
||||
"theme",
|
||||
"ui",
|
||||
"util",
|
||||
"util_macros",
|
||||
"workspace",
|
||||
"workspace-hack",
|
||||
"zed_actions",
|
||||
@@ -11118,7 +11084,6 @@ dependencies = [
|
||||
"db",
|
||||
"documented",
|
||||
"editor",
|
||||
"editor_mode_setting",
|
||||
"fs",
|
||||
"fuzzy",
|
||||
"git",
|
||||
@@ -11137,6 +11102,7 @@ dependencies = [
|
||||
"theme",
|
||||
"ui",
|
||||
"util",
|
||||
"vim_mode_setting",
|
||||
"workspace",
|
||||
"workspace-hack",
|
||||
"zed_actions",
|
||||
@@ -13979,7 +13945,6 @@ dependencies = [
|
||||
"anyhow",
|
||||
"collections",
|
||||
"editor",
|
||||
"editor_mode_setting",
|
||||
"gpui",
|
||||
"language",
|
||||
"language_model",
|
||||
@@ -14395,12 +14360,10 @@ version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fe8c9d1c68d67dd9f97ecbc6f932b60eb289c5dbddd8aa1405484a8fd2fcd984"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"dyn-clone",
|
||||
"indexmap",
|
||||
"ref-cast",
|
||||
"schemars_derive",
|
||||
"semver",
|
||||
"serde",
|
||||
"serde_json",
|
||||
]
|
||||
@@ -16484,7 +16447,7 @@ version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"collections",
|
||||
"derive_more 0.99.19",
|
||||
"derive_more",
|
||||
"fs",
|
||||
"futures 0.3.31",
|
||||
"gpui",
|
||||
@@ -17970,7 +17933,6 @@ dependencies = [
|
||||
"command_palette_hooks",
|
||||
"db",
|
||||
"editor",
|
||||
"editor_mode_setting",
|
||||
"env_logger 0.11.8",
|
||||
"futures 0.3.31",
|
||||
"git_ui",
|
||||
@@ -18000,11 +17962,22 @@ dependencies = [
|
||||
"tokio",
|
||||
"ui",
|
||||
"util",
|
||||
"vim_mode_setting",
|
||||
"workspace",
|
||||
"workspace-hack",
|
||||
"zed_actions",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "vim_mode_setting"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"gpui",
|
||||
"settings",
|
||||
"workspace-hack",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "vscode_theme"
|
||||
version = "0.2.0"
|
||||
@@ -19815,7 +19788,6 @@ dependencies = [
|
||||
"any_vec",
|
||||
"anyhow",
|
||||
"async-recursion",
|
||||
"bincode",
|
||||
"call",
|
||||
"client",
|
||||
"clock",
|
||||
@@ -19834,6 +19806,7 @@ dependencies = [
|
||||
"node_runtime",
|
||||
"parking_lot",
|
||||
"postage",
|
||||
"pretty_assertions",
|
||||
"project",
|
||||
"remote",
|
||||
"schemars",
|
||||
@@ -19989,7 +19962,6 @@ dependencies = [
|
||||
"rustix 1.0.7",
|
||||
"rustls 0.23.26",
|
||||
"rustls-webpki 0.103.1",
|
||||
"schemars",
|
||||
"scopeguard",
|
||||
"sea-orm",
|
||||
"sea-query-binder",
|
||||
@@ -20425,6 +20397,7 @@ dependencies = [
|
||||
name = "zed"
|
||||
version = "0.202.0"
|
||||
dependencies = [
|
||||
"acp_tools",
|
||||
"activity_indicator",
|
||||
"agent",
|
||||
"agent_servers",
|
||||
@@ -20463,7 +20436,6 @@ dependencies = [
|
||||
"diagnostics",
|
||||
"edit_prediction_button",
|
||||
"editor",
|
||||
"editor_mode_setting",
|
||||
"env_logger 0.11.8",
|
||||
"extension",
|
||||
"extension_host",
|
||||
@@ -20563,6 +20535,7 @@ dependencies = [
|
||||
"util",
|
||||
"uuid",
|
||||
"vim",
|
||||
"vim_mode_setting",
|
||||
"watch",
|
||||
"web_search",
|
||||
"web_search_providers",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
[workspace]
|
||||
resolver = "2"
|
||||
members = [
|
||||
"crates/acp_tools",
|
||||
"crates/acp_thread",
|
||||
"crates/action_log",
|
||||
"crates/activity_indicator",
|
||||
@@ -181,7 +182,7 @@ members = [
|
||||
"crates/util_macros",
|
||||
"crates/vercel",
|
||||
"crates/vim",
|
||||
"crates/editor_mode_setting",
|
||||
"crates/vim_mode_setting",
|
||||
"crates/watch",
|
||||
"crates/web_search",
|
||||
"crates/web_search_providers",
|
||||
@@ -227,6 +228,7 @@ edition = "2024"
|
||||
# Workspace member crates
|
||||
#
|
||||
|
||||
acp_tools = { path = "crates/acp_tools" }
|
||||
acp_thread = { path = "crates/acp_thread" }
|
||||
action_log = { path = "crates/action_log" }
|
||||
agent = { path = "crates/agent" }
|
||||
@@ -406,7 +408,7 @@ util = { path = "crates/util" }
|
||||
util_macros = { path = "crates/util_macros" }
|
||||
vercel = { path = "crates/vercel" }
|
||||
vim = { path = "crates/vim" }
|
||||
editor_mode_setting = { path = "crates/editor_mode_setting" }
|
||||
vim_mode_setting = { path = "crates/vim_mode_setting" }
|
||||
|
||||
watch = { path = "crates/watch" }
|
||||
web_search = { path = "crates/web_search" }
|
||||
@@ -424,8 +426,7 @@ zlog_settings = { path = "crates/zlog_settings" }
|
||||
# External crates
|
||||
#
|
||||
|
||||
agentic-coding-protocol = "0.0.10"
|
||||
agent-client-protocol = "0.0.30"
|
||||
agent-client-protocol = "0.0.31"
|
||||
aho-corasick = "1.1"
|
||||
alacritty_terminal = { git = "https://github.com/zed-industries/alacritty.git", branch = "add-hush-login-flag" }
|
||||
any_vec = "0.14"
|
||||
|
||||
2
Procfile.web
Normal file
2
Procfile.web
Normal file
@@ -0,0 +1,2 @@
|
||||
postgrest_llm: postgrest crates/collab/postgrest_llm.conf
|
||||
website: cd ../zed.dev; npm run dev -- --port=3000
|
||||
3
assets/icons/attach.svg
Normal file
3
assets/icons/attach.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M7.37288 4.48506L7.43539 10.6638C7.43539 10.9365 7.54373 11.1981 7.73655 11.3909C7.92938 11.5837 8.19092 11.6921 8.46362 11.6921C8.73632 11.6921 8.99785 11.5837 9.19068 11.3909C9.38351 11.1981 9.49184 10.9366 9.49184 10.6638L9.42933 4.48506C9.42933 3.93975 9.2127 3.41678 8.82711 3.03119C8.44152 2.6456 7.91855 2.42898 7.37324 2.42898C6.82794 2.42898 6.30496 2.6456 5.91937 3.03119C5.53378 3.41678 5.31716 3.93975 5.31716 4.48506L5.37968 10.6384C5.37636 11.0455 5.45368 11.4492 5.60718 11.8263C5.76067 12.2034 5.98731 12.5463 6.27401 12.8354C6.56071 13.1244 6.9018 13.3538 7.27761 13.5104C7.65341 13.667 8.0565 13.7476 8.46362 13.7476C8.87073 13.7476 9.27382 13.667 9.64963 13.5104C10.0254 13.3538 10.3665 13.1244 10.6532 12.8354C10.9399 12.5463 11.1666 12.2034 11.3201 11.8263C11.4736 11.4492 11.5509 11.0455 11.5476 10.6384L11.485 4.48506" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.0 KiB |
@@ -1 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2" d="M12.286 6H7.048C6.469 6 6 6.469 6 7.048v5.238c0 .578.469 1.047 1.048 1.047h5.238c.578 0 1.047-.469 1.047-1.047V7.048c0-.579-.469-1.048-1.047-1.048Z"/><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2" d="M3.714 10a1.05 1.05 0 0 1-1.047-1.048V3.714a1.05 1.05 0 0 1 1.047-1.047h5.238A1.05 1.05 0 0 1 10 3.714"/></svg>
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12.486 6.2H7.24795C6.66895 6.2 6.19995 6.669 6.19995 7.248V12.486C6.19995 13.064 6.66895 13.533 7.24795 13.533H12.486C13.064 13.533 13.533 13.064 13.533 12.486V7.248C13.533 6.669 13.064 6.2 12.486 6.2Z" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M3.91712 10.203C3.63951 10.2022 3.37351 10.0915 3.1773 9.89511C2.98109 9.69872 2.87064 9.43261 2.87012 9.155V3.917C2.87091 3.63956 2.98147 3.37371 3.17765 3.17753C3.37383 2.98135 3.63968 2.87079 3.91712 2.87H9.15512C9.43273 2.87053 9.69883 2.98097 9.89523 3.17718C10.0916 3.37339 10.2023 3.63939 10.2031 3.917" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 515 B After Width: | Height: | Size: 802 B |
@@ -1,3 +1,3 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M9.95231 10.2159C10.0803 9.58974 9.95231 9.57261 10.9111 8.46959C11.4686 7.82822 11.8699 7.09214 11.8699 6.27818C11.8699 5.28184 11.4658 4.32631 10.7467 3.62179C10.0275 2.91728 9.05201 2.52148 8.03492 2.52148C7.01782 2.52148 6.04239 2.91728 5.32319 3.62179C4.604 4.32631 4.19995 5.28184 4.19995 6.27818C4.19995 6.9043 4.32779 7.65565 5.1587 8.46959C6.11744 9.59098 5.98965 9.58974 6.11748 10.2159M9.95231 10.2159V12.2989C9.95231 12.9504 9.41327 13.4786 8.7482 13.4786H7.32165C6.65658 13.4786 6.11744 12.9504 6.11744 12.2989L6.11748 10.2159M9.95231 10.2159H8.03492H6.11748" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M9.9526 10.2625C10.0833 9.62316 9.9526 9.60566 10.9315 8.47946C11.5008 7.82461 11.9105 7.07306 11.9105 6.242C11.9105 5.22472 11.4979 4.2491 10.7637 3.52978C10.0294 2.81046 9.03338 2.40634 7.99491 2.40634C6.95644 2.40634 5.96051 2.81046 5.22619 3.52978C4.49189 4.2491 4.07935 5.22472 4.07935 6.242C4.07935 6.88128 4.20987 7.64842 5.05825 8.47946C6.03714 9.62442 5.90666 9.62316 6.03718 10.2625M9.9526 10.2625V12.3893C9.9526 13.0544 9.40223 13.5937 8.72319 13.5937H7.26665C6.58761 13.5937 6.03714 13.0544 6.03714 12.3893L6.03718 10.2625M9.9526 10.2625H7.99491H6.03718" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 768 B After Width: | Height: | Size: 762 B |
@@ -16,7 +16,6 @@
|
||||
"up": "menu::SelectPrevious",
|
||||
"enter": "menu::Confirm",
|
||||
"ctrl-enter": "menu::SecondaryConfirm",
|
||||
"ctrl-escape": "menu::Cancel",
|
||||
"ctrl-c": "menu::Cancel",
|
||||
"escape": "menu::Cancel",
|
||||
"alt-shift-enter": "menu::Restart",
|
||||
@@ -856,7 +855,7 @@
|
||||
"ctrl-backspace": ["project_panel::Delete", { "skip_prompt": false }],
|
||||
"ctrl-delete": ["project_panel::Delete", { "skip_prompt": false }],
|
||||
"alt-ctrl-r": "project_panel::RevealInFileManager",
|
||||
"ctrl-shift-enter": "project_panel::OpenWithSystem",
|
||||
"ctrl-shift-enter": "workspace::OpenWithSystem",
|
||||
"alt-d": "project_panel::CompareMarkedFiles",
|
||||
"shift-find": "project_panel::NewSearchInDirectory",
|
||||
"ctrl-alt-shift-f": "project_panel::NewSearchInDirectory",
|
||||
@@ -1195,9 +1194,16 @@
|
||||
"ctrl-1": "onboarding::ActivateBasicsPage",
|
||||
"ctrl-2": "onboarding::ActivateEditingPage",
|
||||
"ctrl-3": "onboarding::ActivateAISetupPage",
|
||||
"ctrl-escape": "onboarding::Finish",
|
||||
"alt-tab": "onboarding::SignIn",
|
||||
"ctrl-enter": "onboarding::Finish",
|
||||
"alt-shift-l": "onboarding::SignIn",
|
||||
"alt-shift-a": "onboarding::OpenAccount"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "InvalidBuffer",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"ctrl-shift-enter": "workspace::OpenWithSystem"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -915,7 +915,7 @@
|
||||
"cmd-backspace": ["project_panel::Trash", { "skip_prompt": true }],
|
||||
"cmd-delete": ["project_panel::Delete", { "skip_prompt": false }],
|
||||
"alt-cmd-r": "project_panel::RevealInFileManager",
|
||||
"ctrl-shift-enter": "project_panel::OpenWithSystem",
|
||||
"ctrl-shift-enter": "workspace::OpenWithSystem",
|
||||
"alt-d": "project_panel::CompareMarkedFiles",
|
||||
"cmd-alt-backspace": ["project_panel::Delete", { "skip_prompt": false }],
|
||||
"cmd-alt-shift-f": "project_panel::NewSearchInDirectory",
|
||||
@@ -1301,5 +1301,12 @@
|
||||
"alt-tab": "onboarding::SignIn",
|
||||
"alt-shift-a": "onboarding::OpenAccount"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "InvalidBuffer",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"ctrl-shift-enter": "workspace::OpenWithSystem"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -819,7 +819,7 @@
|
||||
"v": "project_panel::OpenPermanent",
|
||||
"p": "project_panel::Open",
|
||||
"x": "project_panel::RevealInFileManager",
|
||||
"s": "project_panel::OpenWithSystem",
|
||||
"s": "workspace::OpenWithSystem",
|
||||
"z d": "project_panel::CompareMarkedFiles",
|
||||
"] c": "project_panel::SelectNextGitEntry",
|
||||
"[ c": "project_panel::SelectPrevGitEntry",
|
||||
|
||||
@@ -111,10 +111,6 @@
|
||||
// 2. Maps `Control` on Linux and Windows and to `Command` on MacOS:
|
||||
// "cmd_or_ctrl" (alias: "cmd", "ctrl")
|
||||
"multi_cursor_modifier": "alt",
|
||||
/// Weather to set editor mode to vim, vim insert, helix, etc.
|
||||
///
|
||||
/// Default: default
|
||||
"editor_mode": "default",
|
||||
// Whether to enable vim modes and key bindings.
|
||||
"vim_mode": false,
|
||||
// Whether to enable helix mode and key bindings.
|
||||
@@ -166,6 +162,12 @@
|
||||
// 2. Always quit the application
|
||||
// "on_last_window_closed": "quit_app",
|
||||
"on_last_window_closed": "platform_default",
|
||||
// Whether to show padding for zoomed panels.
|
||||
// When enabled, zoomed center panels (e.g. code editor) will have padding all around,
|
||||
// while zoomed bottom/left/right panels will have padding to the top/right/left (respectively).
|
||||
//
|
||||
// Default: true
|
||||
"zoomed_padding": true,
|
||||
// Whether to use the system provided dialogs for Open and Save As.
|
||||
// When set to false, Zed will use the built-in keyboard-first pickers.
|
||||
"use_system_path_prompts": true,
|
||||
@@ -889,11 +891,7 @@
|
||||
/// Whether to have terminal cards in the agent panel expanded, showing the whole command output.
|
||||
///
|
||||
/// Default: true
|
||||
"expand_terminal_card": true,
|
||||
/// Weather to inherit or override the editor mode for the agent panel.
|
||||
///
|
||||
/// Default: inherit
|
||||
"editor_mode": "inherit"
|
||||
"expand_terminal_card": true
|
||||
},
|
||||
// The settings for slash commands.
|
||||
"slash_commands": {
|
||||
@@ -1141,11 +1139,6 @@
|
||||
// The minimum severity of the diagnostics to show inline.
|
||||
// Inherits editor's diagnostics' max severity settings when `null`.
|
||||
"max_severity": null
|
||||
},
|
||||
"cargo": {
|
||||
// When enabled, Zed disables rust-analyzer's check on save and starts to query
|
||||
// Cargo diagnostics separately.
|
||||
"fetch_cargo_diagnostics": false
|
||||
}
|
||||
},
|
||||
// Files or globs of files that will be excluded by Zed entirely. They will be skipped during file
|
||||
@@ -1511,6 +1504,11 @@
|
||||
//
|
||||
// Default: fallback
|
||||
"words": "fallback",
|
||||
// Minimum number of characters required to automatically trigger word-based completions.
|
||||
// Before that value, it's still possible to trigger the words-based completion manually with the corresponding editor command.
|
||||
//
|
||||
// Default: 3
|
||||
"words_min_length": 3,
|
||||
// Whether to fetch LSP completions or not.
|
||||
//
|
||||
// Default: true
|
||||
@@ -1637,6 +1635,9 @@
|
||||
"allowed": true
|
||||
}
|
||||
},
|
||||
"Kotlin": {
|
||||
"language_servers": ["kotlin-language-server", "!kotlin-lsp", "..."]
|
||||
},
|
||||
"LaTeX": {
|
||||
"formatter": "language_server",
|
||||
"language_servers": ["texlab", "..."],
|
||||
@@ -1650,9 +1651,6 @@
|
||||
"use_on_type_format": false,
|
||||
"allow_rewrap": "anywhere",
|
||||
"soft_wrap": "editor_width",
|
||||
"completions": {
|
||||
"words": "disabled"
|
||||
},
|
||||
"prettier": {
|
||||
"allowed": true
|
||||
}
|
||||
@@ -1666,9 +1664,6 @@
|
||||
}
|
||||
},
|
||||
"Plain Text": {
|
||||
"completions": {
|
||||
"words": "disabled"
|
||||
},
|
||||
"allow_rewrap": "anywhere"
|
||||
},
|
||||
"Python": {
|
||||
|
||||
@@ -93,7 +93,7 @@
|
||||
"terminal.ansi.bright_cyan": "#4c806fff",
|
||||
"terminal.ansi.dim_cyan": "#cbf2e4ff",
|
||||
"terminal.ansi.white": "#bfbdb6ff",
|
||||
"terminal.ansi.bright_white": "#bfbdb6ff",
|
||||
"terminal.ansi.bright_white": "#fafafaff",
|
||||
"terminal.ansi.dim_white": "#787876ff",
|
||||
"link_text.hover": "#5ac1feff",
|
||||
"conflict": "#feb454ff",
|
||||
@@ -479,7 +479,7 @@
|
||||
"terminal.ansi.bright_cyan": "#ace0cbff",
|
||||
"terminal.ansi.dim_cyan": "#2a5f4aff",
|
||||
"terminal.ansi.white": "#fcfcfcff",
|
||||
"terminal.ansi.bright_white": "#fcfcfcff",
|
||||
"terminal.ansi.bright_white": "#ffffffff",
|
||||
"terminal.ansi.dim_white": "#bcbec0ff",
|
||||
"link_text.hover": "#3b9ee5ff",
|
||||
"conflict": "#f1ad49ff",
|
||||
@@ -865,7 +865,7 @@
|
||||
"terminal.ansi.bright_cyan": "#4c806fff",
|
||||
"terminal.ansi.dim_cyan": "#cbf2e4ff",
|
||||
"terminal.ansi.white": "#cccac2ff",
|
||||
"terminal.ansi.bright_white": "#cccac2ff",
|
||||
"terminal.ansi.bright_white": "#fafafaff",
|
||||
"terminal.ansi.dim_white": "#898a8aff",
|
||||
"link_text.hover": "#72cffeff",
|
||||
"conflict": "#fecf72ff",
|
||||
|
||||
@@ -94,7 +94,7 @@
|
||||
"terminal.ansi.bright_cyan": "#45603eff",
|
||||
"terminal.ansi.dim_cyan": "#c7dfbdff",
|
||||
"terminal.ansi.white": "#fbf1c7ff",
|
||||
"terminal.ansi.bright_white": "#fbf1c7ff",
|
||||
"terminal.ansi.bright_white": "#ffffffff",
|
||||
"terminal.ansi.dim_white": "#b0a189ff",
|
||||
"link_text.hover": "#83a598ff",
|
||||
"version_control.added": "#b7bb26ff",
|
||||
@@ -494,7 +494,7 @@
|
||||
"terminal.ansi.bright_cyan": "#45603eff",
|
||||
"terminal.ansi.dim_cyan": "#c7dfbdff",
|
||||
"terminal.ansi.white": "#fbf1c7ff",
|
||||
"terminal.ansi.bright_white": "#fbf1c7ff",
|
||||
"terminal.ansi.bright_white": "#ffffffff",
|
||||
"terminal.ansi.dim_white": "#b0a189ff",
|
||||
"link_text.hover": "#83a598ff",
|
||||
"version_control.added": "#b7bb26ff",
|
||||
@@ -894,7 +894,7 @@
|
||||
"terminal.ansi.bright_cyan": "#45603eff",
|
||||
"terminal.ansi.dim_cyan": "#c7dfbdff",
|
||||
"terminal.ansi.white": "#fbf1c7ff",
|
||||
"terminal.ansi.bright_white": "#fbf1c7ff",
|
||||
"terminal.ansi.bright_white": "#ffffffff",
|
||||
"terminal.ansi.dim_white": "#b0a189ff",
|
||||
"link_text.hover": "#83a598ff",
|
||||
"version_control.added": "#b7bb26ff",
|
||||
@@ -1294,7 +1294,7 @@
|
||||
"terminal.ansi.bright_cyan": "#9fbca8ff",
|
||||
"terminal.ansi.dim_cyan": "#253e2eff",
|
||||
"terminal.ansi.white": "#fbf1c7ff",
|
||||
"terminal.ansi.bright_white": "#fbf1c7ff",
|
||||
"terminal.ansi.bright_white": "#ffffffff",
|
||||
"terminal.ansi.dim_white": "#b0a189ff",
|
||||
"link_text.hover": "#0b6678ff",
|
||||
"version_control.added": "#797410ff",
|
||||
@@ -1694,7 +1694,7 @@
|
||||
"terminal.ansi.bright_cyan": "#9fbca8ff",
|
||||
"terminal.ansi.dim_cyan": "#253e2eff",
|
||||
"terminal.ansi.white": "#f9f5d7ff",
|
||||
"terminal.ansi.bright_white": "#f9f5d7ff",
|
||||
"terminal.ansi.bright_white": "#ffffffff",
|
||||
"terminal.ansi.dim_white": "#b0a189ff",
|
||||
"link_text.hover": "#0b6678ff",
|
||||
"version_control.added": "#797410ff",
|
||||
@@ -2094,7 +2094,7 @@
|
||||
"terminal.ansi.bright_cyan": "#9fbca8ff",
|
||||
"terminal.ansi.dim_cyan": "#253e2eff",
|
||||
"terminal.ansi.white": "#f2e5bcff",
|
||||
"terminal.ansi.bright_white": "#f2e5bcff",
|
||||
"terminal.ansi.bright_white": "#ffffffff",
|
||||
"terminal.ansi.dim_white": "#b0a189ff",
|
||||
"link_text.hover": "#0b6678ff",
|
||||
"version_control.added": "#797410ff",
|
||||
|
||||
@@ -93,7 +93,7 @@
|
||||
"terminal.ansi.bright_cyan": "#3a565bff",
|
||||
"terminal.ansi.dim_cyan": "#b9d9dfff",
|
||||
"terminal.ansi.white": "#dce0e5ff",
|
||||
"terminal.ansi.bright_white": "#dce0e5ff",
|
||||
"terminal.ansi.bright_white": "#fafafaff",
|
||||
"terminal.ansi.dim_white": "#575d65ff",
|
||||
"link_text.hover": "#74ade8ff",
|
||||
"version_control.added": "#27a657ff",
|
||||
@@ -468,7 +468,7 @@
|
||||
"terminal.bright_foreground": "#242529ff",
|
||||
"terminal.dim_foreground": "#fafafaff",
|
||||
"terminal.ansi.black": "#242529ff",
|
||||
"terminal.ansi.bright_black": "#242529ff",
|
||||
"terminal.ansi.bright_black": "#747579ff",
|
||||
"terminal.ansi.dim_black": "#97979aff",
|
||||
"terminal.ansi.red": "#d36151ff",
|
||||
"terminal.ansi.bright_red": "#f0b0a4ff",
|
||||
@@ -489,7 +489,7 @@
|
||||
"terminal.ansi.bright_cyan": "#a3bedaff",
|
||||
"terminal.ansi.dim_cyan": "#254058ff",
|
||||
"terminal.ansi.white": "#fafafaff",
|
||||
"terminal.ansi.bright_white": "#fafafaff",
|
||||
"terminal.ansi.bright_white": "#ffffffff",
|
||||
"terminal.ansi.dim_white": "#aaaaaaff",
|
||||
"link_text.hover": "#5c78e2ff",
|
||||
"version_control.added": "#27a657ff",
|
||||
|
||||
@@ -509,7 +509,7 @@ impl ContentBlock {
|
||||
"`Image`".into()
|
||||
}
|
||||
|
||||
fn to_markdown<'a>(&'a self, cx: &'a App) -> &'a str {
|
||||
pub fn to_markdown<'a>(&'a self, cx: &'a App) -> &'a str {
|
||||
match self {
|
||||
ContentBlock::Empty => "",
|
||||
ContentBlock::Markdown { markdown } => markdown.read(cx).source(),
|
||||
@@ -756,6 +756,8 @@ pub struct AcpThread {
|
||||
connection: Rc<dyn AgentConnection>,
|
||||
session_id: acp::SessionId,
|
||||
token_usage: Option<TokenUsage>,
|
||||
prompt_capabilities: acp::PromptCapabilities,
|
||||
_observe_prompt_capabilities: Task<anyhow::Result<()>>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -770,11 +772,12 @@ pub enum AcpThreadEvent {
|
||||
Stopped,
|
||||
Error,
|
||||
LoadError(LoadError),
|
||||
PromptCapabilitiesUpdated,
|
||||
}
|
||||
|
||||
impl EventEmitter<AcpThreadEvent> for AcpThread {}
|
||||
|
||||
#[derive(PartialEq, Eq)]
|
||||
#[derive(PartialEq, Eq, Debug)]
|
||||
pub enum ThreadStatus {
|
||||
Idle,
|
||||
WaitingForToolConfirmation,
|
||||
@@ -821,7 +824,20 @@ impl AcpThread {
|
||||
project: Entity<Project>,
|
||||
action_log: Entity<ActionLog>,
|
||||
session_id: acp::SessionId,
|
||||
mut prompt_capabilities_rx: watch::Receiver<acp::PromptCapabilities>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
let prompt_capabilities = *prompt_capabilities_rx.borrow();
|
||||
let task = cx.spawn::<_, anyhow::Result<()>>(async move |this, cx| {
|
||||
loop {
|
||||
let caps = prompt_capabilities_rx.recv().await?;
|
||||
this.update(cx, |this, cx| {
|
||||
this.prompt_capabilities = caps;
|
||||
cx.emit(AcpThreadEvent::PromptCapabilitiesUpdated);
|
||||
})?;
|
||||
}
|
||||
});
|
||||
|
||||
Self {
|
||||
action_log,
|
||||
shared_buffers: Default::default(),
|
||||
@@ -833,9 +849,15 @@ impl AcpThread {
|
||||
connection,
|
||||
session_id,
|
||||
token_usage: None,
|
||||
prompt_capabilities,
|
||||
_observe_prompt_capabilities: task,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn prompt_capabilities(&self) -> acp::PromptCapabilities {
|
||||
self.prompt_capabilities
|
||||
}
|
||||
|
||||
pub fn connection(&self) -> &Rc<dyn AgentConnection> {
|
||||
&self.connection
|
||||
}
|
||||
@@ -1373,6 +1395,10 @@ impl AcpThread {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn can_resume(&self, cx: &App) -> bool {
|
||||
self.connection.resume(&self.session_id, cx).is_some()
|
||||
}
|
||||
|
||||
pub fn resume(&mut self, cx: &mut Context<Self>) -> BoxFuture<'static, Result<()>> {
|
||||
self.run_turn(cx, async move |this, cx| {
|
||||
this.update(cx, |this, cx| {
|
||||
@@ -2595,13 +2621,19 @@ mod tests {
|
||||
.into(),
|
||||
);
|
||||
let action_log = cx.new(|_| ActionLog::new(project.clone()));
|
||||
let thread = cx.new(|_cx| {
|
||||
let thread = cx.new(|cx| {
|
||||
AcpThread::new(
|
||||
"Test",
|
||||
self.clone(),
|
||||
project,
|
||||
action_log,
|
||||
session_id.clone(),
|
||||
watch::Receiver::constant(acp::PromptCapabilities {
|
||||
image: true,
|
||||
audio: true,
|
||||
embedded_context: true,
|
||||
}),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
self.sessions.lock().insert(session_id, thread.downgrade());
|
||||
@@ -2635,14 +2667,6 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
fn prompt_capabilities(&self) -> acp::PromptCapabilities {
|
||||
acp::PromptCapabilities {
|
||||
image: true,
|
||||
audio: true,
|
||||
embedded_context: true,
|
||||
}
|
||||
}
|
||||
|
||||
fn cancel(&self, session_id: &acp::SessionId, cx: &mut App) {
|
||||
let sessions = self.sessions.lock();
|
||||
let thread = sessions.get(session_id).unwrap().clone();
|
||||
@@ -2659,7 +2683,7 @@ mod tests {
|
||||
fn truncate(
|
||||
&self,
|
||||
session_id: &acp::SessionId,
|
||||
_cx: &mut App,
|
||||
_cx: &App,
|
||||
) -> Option<Rc<dyn AgentSessionTruncate>> {
|
||||
Some(Rc::new(FakeAgentSessionEditor {
|
||||
_session_id: session_id.clone(),
|
||||
|
||||
@@ -38,12 +38,10 @@ pub trait AgentConnection {
|
||||
cx: &mut App,
|
||||
) -> Task<Result<acp::PromptResponse>>;
|
||||
|
||||
fn prompt_capabilities(&self) -> acp::PromptCapabilities;
|
||||
|
||||
fn resume(
|
||||
&self,
|
||||
_session_id: &acp::SessionId,
|
||||
_cx: &mut App,
|
||||
_cx: &App,
|
||||
) -> Option<Rc<dyn AgentSessionResume>> {
|
||||
None
|
||||
}
|
||||
@@ -53,7 +51,7 @@ pub trait AgentConnection {
|
||||
fn truncate(
|
||||
&self,
|
||||
_session_id: &acp::SessionId,
|
||||
_cx: &mut App,
|
||||
_cx: &App,
|
||||
) -> Option<Rc<dyn AgentSessionTruncate>> {
|
||||
None
|
||||
}
|
||||
@@ -61,7 +59,7 @@ pub trait AgentConnection {
|
||||
fn set_title(
|
||||
&self,
|
||||
_session_id: &acp::SessionId,
|
||||
_cx: &mut App,
|
||||
_cx: &App,
|
||||
) -> Option<Rc<dyn AgentSessionSetTitle>> {
|
||||
None
|
||||
}
|
||||
@@ -329,13 +327,19 @@ mod test_support {
|
||||
) -> Task<gpui::Result<Entity<AcpThread>>> {
|
||||
let session_id = acp::SessionId(self.sessions.lock().len().to_string().into());
|
||||
let action_log = cx.new(|_| ActionLog::new(project.clone()));
|
||||
let thread = cx.new(|_cx| {
|
||||
let thread = cx.new(|cx| {
|
||||
AcpThread::new(
|
||||
"Test",
|
||||
self.clone(),
|
||||
project,
|
||||
action_log,
|
||||
session_id.clone(),
|
||||
watch::Receiver::constant(acp::PromptCapabilities {
|
||||
image: true,
|
||||
audio: true,
|
||||
embedded_context: true,
|
||||
}),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
self.sessions.lock().insert(
|
||||
@@ -348,14 +352,6 @@ mod test_support {
|
||||
Task::ready(Ok(thread))
|
||||
}
|
||||
|
||||
fn prompt_capabilities(&self) -> acp::PromptCapabilities {
|
||||
acp::PromptCapabilities {
|
||||
image: true,
|
||||
audio: true,
|
||||
embedded_context: true,
|
||||
}
|
||||
}
|
||||
|
||||
fn authenticate(
|
||||
&self,
|
||||
_method_id: acp::AuthMethodId,
|
||||
@@ -439,7 +435,7 @@ mod test_support {
|
||||
fn truncate(
|
||||
&self,
|
||||
_session_id: &agent_client_protocol::SessionId,
|
||||
_cx: &mut App,
|
||||
_cx: &App,
|
||||
) -> Option<Rc<dyn AgentSessionTruncate>> {
|
||||
Some(Rc::new(StubAgentSessionEditor))
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ use prompt_store::{PromptId, UserPromptId};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{
|
||||
fmt,
|
||||
ops::Range,
|
||||
ops::RangeInclusive,
|
||||
path::{Path, PathBuf},
|
||||
str::FromStr,
|
||||
};
|
||||
@@ -17,13 +17,14 @@ pub enum MentionUri {
|
||||
File {
|
||||
abs_path: PathBuf,
|
||||
},
|
||||
PastedImage,
|
||||
Directory {
|
||||
abs_path: PathBuf,
|
||||
},
|
||||
Symbol {
|
||||
path: PathBuf,
|
||||
abs_path: PathBuf,
|
||||
name: String,
|
||||
line_range: Range<u32>,
|
||||
line_range: RangeInclusive<u32>,
|
||||
},
|
||||
Thread {
|
||||
id: acp::SessionId,
|
||||
@@ -38,8 +39,9 @@ pub enum MentionUri {
|
||||
name: String,
|
||||
},
|
||||
Selection {
|
||||
path: PathBuf,
|
||||
line_range: Range<u32>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
abs_path: Option<PathBuf>,
|
||||
line_range: RangeInclusive<u32>,
|
||||
},
|
||||
Fetch {
|
||||
url: Url,
|
||||
@@ -48,36 +50,44 @@ pub enum MentionUri {
|
||||
|
||||
impl MentionUri {
|
||||
pub fn parse(input: &str) -> Result<Self> {
|
||||
fn parse_line_range(fragment: &str) -> Result<RangeInclusive<u32>> {
|
||||
let range = fragment
|
||||
.strip_prefix("L")
|
||||
.context("Line range must start with \"L\"")?;
|
||||
let (start, end) = range
|
||||
.split_once(":")
|
||||
.context("Line range must use colon as separator")?;
|
||||
let range = start
|
||||
.parse::<u32>()
|
||||
.context("Parsing line range start")?
|
||||
.checked_sub(1)
|
||||
.context("Line numbers should be 1-based")?
|
||||
..=end
|
||||
.parse::<u32>()
|
||||
.context("Parsing line range end")?
|
||||
.checked_sub(1)
|
||||
.context("Line numbers should be 1-based")?;
|
||||
Ok(range)
|
||||
}
|
||||
|
||||
let url = url::Url::parse(input)?;
|
||||
let path = url.path();
|
||||
match url.scheme() {
|
||||
"file" => {
|
||||
let path = url.to_file_path().ok().context("Extracting file path")?;
|
||||
if let Some(fragment) = url.fragment() {
|
||||
let range = fragment
|
||||
.strip_prefix("L")
|
||||
.context("Line range must start with \"L\"")?;
|
||||
let (start, end) = range
|
||||
.split_once(":")
|
||||
.context("Line range must use colon as separator")?;
|
||||
let line_range = start
|
||||
.parse::<u32>()
|
||||
.context("Parsing line range start")?
|
||||
.checked_sub(1)
|
||||
.context("Line numbers should be 1-based")?
|
||||
..end
|
||||
.parse::<u32>()
|
||||
.context("Parsing line range end")?
|
||||
.checked_sub(1)
|
||||
.context("Line numbers should be 1-based")?;
|
||||
let line_range = parse_line_range(fragment)?;
|
||||
if let Some(name) = single_query_param(&url, "symbol")? {
|
||||
Ok(Self::Symbol {
|
||||
name,
|
||||
path,
|
||||
abs_path: path,
|
||||
line_range,
|
||||
})
|
||||
} else {
|
||||
Ok(Self::Selection { path, line_range })
|
||||
Ok(Self::Selection {
|
||||
abs_path: Some(path),
|
||||
line_range,
|
||||
})
|
||||
}
|
||||
} else if input.ends_with("/") {
|
||||
Ok(Self::Directory { abs_path: path })
|
||||
@@ -105,6 +115,17 @@ impl MentionUri {
|
||||
id: rule_id.into(),
|
||||
name,
|
||||
})
|
||||
} else if path.starts_with("/agent/pasted-image") {
|
||||
Ok(Self::PastedImage)
|
||||
} else if path.starts_with("/agent/untitled-buffer") {
|
||||
let fragment = url
|
||||
.fragment()
|
||||
.context("Missing fragment for untitled buffer selection")?;
|
||||
let line_range = parse_line_range(fragment)?;
|
||||
Ok(Self::Selection {
|
||||
abs_path: None,
|
||||
line_range,
|
||||
})
|
||||
} else {
|
||||
bail!("invalid zed url: {:?}", input);
|
||||
}
|
||||
@@ -121,13 +142,16 @@ impl MentionUri {
|
||||
.unwrap_or_default()
|
||||
.to_string_lossy()
|
||||
.into_owned(),
|
||||
MentionUri::PastedImage => "Image".to_string(),
|
||||
MentionUri::Symbol { name, .. } => name.clone(),
|
||||
MentionUri::Thread { name, .. } => name.clone(),
|
||||
MentionUri::TextThread { name, .. } => name.clone(),
|
||||
MentionUri::Rule { name, .. } => name.clone(),
|
||||
MentionUri::Selection {
|
||||
path, line_range, ..
|
||||
} => selection_name(path, line_range),
|
||||
abs_path: path,
|
||||
line_range,
|
||||
..
|
||||
} => selection_name(path.as_deref(), line_range),
|
||||
MentionUri::Fetch { url } => url.to_string(),
|
||||
}
|
||||
}
|
||||
@@ -137,6 +161,7 @@ impl MentionUri {
|
||||
MentionUri::File { abs_path } => {
|
||||
FileIcons::get_icon(abs_path, cx).unwrap_or_else(|| IconName::File.path().into())
|
||||
}
|
||||
MentionUri::PastedImage => IconName::Image.path().into(),
|
||||
MentionUri::Directory { .. } => FileIcons::get_folder_icon(false, cx)
|
||||
.unwrap_or_else(|| IconName::Folder.path().into()),
|
||||
MentionUri::Symbol { .. } => IconName::Code.path().into(),
|
||||
@@ -157,29 +182,40 @@ impl MentionUri {
|
||||
MentionUri::File { abs_path } => {
|
||||
Url::from_file_path(abs_path).expect("mention path should be absolute")
|
||||
}
|
||||
MentionUri::PastedImage => Url::parse("zed:///agent/pasted-image").unwrap(),
|
||||
MentionUri::Directory { abs_path } => {
|
||||
Url::from_directory_path(abs_path).expect("mention path should be absolute")
|
||||
}
|
||||
MentionUri::Symbol {
|
||||
path,
|
||||
abs_path,
|
||||
name,
|
||||
line_range,
|
||||
} => {
|
||||
let mut url = Url::from_file_path(path).expect("mention path should be absolute");
|
||||
let mut url =
|
||||
Url::from_file_path(abs_path).expect("mention path should be absolute");
|
||||
url.query_pairs_mut().append_pair("symbol", name);
|
||||
url.set_fragment(Some(&format!(
|
||||
"L{}:{}",
|
||||
line_range.start + 1,
|
||||
line_range.end + 1
|
||||
line_range.start() + 1,
|
||||
line_range.end() + 1
|
||||
)));
|
||||
url
|
||||
}
|
||||
MentionUri::Selection { path, line_range } => {
|
||||
let mut url = Url::from_file_path(path).expect("mention path should be absolute");
|
||||
MentionUri::Selection {
|
||||
abs_path: path,
|
||||
line_range,
|
||||
} => {
|
||||
let mut url = if let Some(path) = path {
|
||||
Url::from_file_path(path).expect("mention path should be absolute")
|
||||
} else {
|
||||
let mut url = Url::parse("zed:///").unwrap();
|
||||
url.set_path("/agent/untitled-buffer");
|
||||
url
|
||||
};
|
||||
url.set_fragment(Some(&format!(
|
||||
"L{}:{}",
|
||||
line_range.start + 1,
|
||||
line_range.end + 1
|
||||
line_range.start() + 1,
|
||||
line_range.end() + 1
|
||||
)));
|
||||
url
|
||||
}
|
||||
@@ -191,7 +227,10 @@ impl MentionUri {
|
||||
}
|
||||
MentionUri::TextThread { path, name } => {
|
||||
let mut url = Url::parse("zed:///").unwrap();
|
||||
url.set_path(&format!("/agent/text-thread/{}", path.to_string_lossy()));
|
||||
url.set_path(&format!(
|
||||
"/agent/text-thread/{}",
|
||||
path.to_string_lossy().trim_start_matches('/')
|
||||
));
|
||||
url.query_pairs_mut().append_pair("name", name);
|
||||
url
|
||||
}
|
||||
@@ -237,12 +276,14 @@ fn single_query_param(url: &Url, name: &'static str) -> Result<Option<String>> {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn selection_name(path: &Path, line_range: &Range<u32>) -> String {
|
||||
pub fn selection_name(path: Option<&Path>, line_range: &RangeInclusive<u32>) -> String {
|
||||
format!(
|
||||
"{} ({}:{})",
|
||||
path.file_name().unwrap_or_default().display(),
|
||||
line_range.start + 1,
|
||||
line_range.end + 1
|
||||
path.and_then(|path| path.file_name())
|
||||
.unwrap_or("Untitled".as_ref())
|
||||
.display(),
|
||||
*line_range.start() + 1,
|
||||
*line_range.end() + 1
|
||||
)
|
||||
}
|
||||
|
||||
@@ -302,14 +343,14 @@ mod tests {
|
||||
let parsed = MentionUri::parse(symbol_uri).unwrap();
|
||||
match &parsed {
|
||||
MentionUri::Symbol {
|
||||
path,
|
||||
abs_path: path,
|
||||
name,
|
||||
line_range,
|
||||
} => {
|
||||
assert_eq!(path.to_str().unwrap(), path!("/path/to/file.rs"));
|
||||
assert_eq!(name, "MySymbol");
|
||||
assert_eq!(line_range.start, 9);
|
||||
assert_eq!(line_range.end, 19);
|
||||
assert_eq!(line_range.start(), &9);
|
||||
assert_eq!(line_range.end(), &19);
|
||||
}
|
||||
_ => panic!("Expected Symbol variant"),
|
||||
}
|
||||
@@ -321,16 +362,39 @@ mod tests {
|
||||
let selection_uri = uri!("file:///path/to/file.rs#L5:15");
|
||||
let parsed = MentionUri::parse(selection_uri).unwrap();
|
||||
match &parsed {
|
||||
MentionUri::Selection { path, line_range } => {
|
||||
assert_eq!(path.to_str().unwrap(), path!("/path/to/file.rs"));
|
||||
assert_eq!(line_range.start, 4);
|
||||
assert_eq!(line_range.end, 14);
|
||||
MentionUri::Selection {
|
||||
abs_path: path,
|
||||
line_range,
|
||||
} => {
|
||||
assert_eq!(
|
||||
path.as_ref().unwrap().to_str().unwrap(),
|
||||
path!("/path/to/file.rs")
|
||||
);
|
||||
assert_eq!(line_range.start(), &4);
|
||||
assert_eq!(line_range.end(), &14);
|
||||
}
|
||||
_ => panic!("Expected Selection variant"),
|
||||
}
|
||||
assert_eq!(parsed.to_uri().to_string(), selection_uri);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_untitled_selection_uri() {
|
||||
let selection_uri = uri!("zed:///agent/untitled-buffer#L1:10");
|
||||
let parsed = MentionUri::parse(selection_uri).unwrap();
|
||||
match &parsed {
|
||||
MentionUri::Selection {
|
||||
abs_path: None,
|
||||
line_range,
|
||||
} => {
|
||||
assert_eq!(line_range.start(), &0);
|
||||
assert_eq!(line_range.end(), &9);
|
||||
}
|
||||
_ => panic!("Expected Selection variant without path"),
|
||||
}
|
||||
assert_eq!(parsed.to_uri().to_string(), selection_uri);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_thread_uri() {
|
||||
let thread_uri = "zed:///agent/thread/session123?name=Thread+name";
|
||||
|
||||
30
crates/acp_tools/Cargo.toml
Normal file
30
crates/acp_tools/Cargo.toml
Normal file
@@ -0,0 +1,30 @@
|
||||
[package]
|
||||
name = "acp_tools"
|
||||
version = "0.1.0"
|
||||
edition.workspace = true
|
||||
publish.workspace = true
|
||||
license = "GPL-3.0-or-later"
|
||||
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[lib]
|
||||
path = "src/acp_tools.rs"
|
||||
doctest = false
|
||||
|
||||
[dependencies]
|
||||
agent-client-protocol.workspace = true
|
||||
collections.workspace = true
|
||||
gpui.workspace = true
|
||||
language.workspace= true
|
||||
markdown.workspace = true
|
||||
project.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
settings.workspace = true
|
||||
theme.workspace = true
|
||||
ui.workspace = true
|
||||
util.workspace = true
|
||||
workspace-hack.workspace = true
|
||||
workspace.workspace = true
|
||||
494
crates/acp_tools/src/acp_tools.rs
Normal file
494
crates/acp_tools/src/acp_tools.rs
Normal file
@@ -0,0 +1,494 @@
|
||||
use std::{
|
||||
cell::RefCell,
|
||||
collections::HashSet,
|
||||
fmt::Display,
|
||||
rc::{Rc, Weak},
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
use agent_client_protocol as acp;
|
||||
use collections::HashMap;
|
||||
use gpui::{
|
||||
App, Empty, Entity, EventEmitter, FocusHandle, Focusable, Global, ListAlignment, ListState,
|
||||
StyleRefinement, Subscription, Task, TextStyleRefinement, Window, actions, list, prelude::*,
|
||||
};
|
||||
use language::LanguageRegistry;
|
||||
use markdown::{CodeBlockRenderer, Markdown, MarkdownElement, MarkdownStyle};
|
||||
use project::Project;
|
||||
use settings::Settings;
|
||||
use theme::ThemeSettings;
|
||||
use ui::prelude::*;
|
||||
use util::ResultExt as _;
|
||||
use workspace::{Item, Workspace};
|
||||
|
||||
actions!(acp, [OpenDebugTools]);
|
||||
|
||||
pub fn init(cx: &mut App) {
|
||||
cx.observe_new(
|
||||
|workspace: &mut Workspace, _window, _cx: &mut Context<Workspace>| {
|
||||
workspace.register_action(|workspace, _: &OpenDebugTools, window, cx| {
|
||||
let acp_tools =
|
||||
Box::new(cx.new(|cx| AcpTools::new(workspace.project().clone(), cx)));
|
||||
workspace.add_item_to_active_pane(acp_tools, None, true, window, cx);
|
||||
});
|
||||
},
|
||||
)
|
||||
.detach();
|
||||
}
|
||||
|
||||
struct GlobalAcpConnectionRegistry(Entity<AcpConnectionRegistry>);
|
||||
|
||||
impl Global for GlobalAcpConnectionRegistry {}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct AcpConnectionRegistry {
|
||||
active_connection: RefCell<Option<ActiveConnection>>,
|
||||
}
|
||||
|
||||
struct ActiveConnection {
|
||||
server_name: SharedString,
|
||||
connection: Weak<acp::ClientSideConnection>,
|
||||
}
|
||||
|
||||
impl AcpConnectionRegistry {
|
||||
pub fn default_global(cx: &mut App) -> Entity<Self> {
|
||||
if cx.has_global::<GlobalAcpConnectionRegistry>() {
|
||||
cx.global::<GlobalAcpConnectionRegistry>().0.clone()
|
||||
} else {
|
||||
let registry = cx.new(|_cx| AcpConnectionRegistry::default());
|
||||
cx.set_global(GlobalAcpConnectionRegistry(registry.clone()));
|
||||
registry
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_active_connection(
|
||||
&self,
|
||||
server_name: impl Into<SharedString>,
|
||||
connection: &Rc<acp::ClientSideConnection>,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.active_connection.replace(Some(ActiveConnection {
|
||||
server_name: server_name.into(),
|
||||
connection: Rc::downgrade(connection),
|
||||
}));
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
|
||||
struct AcpTools {
|
||||
project: Entity<Project>,
|
||||
focus_handle: FocusHandle,
|
||||
expanded: HashSet<usize>,
|
||||
watched_connection: Option<WatchedConnection>,
|
||||
connection_registry: Entity<AcpConnectionRegistry>,
|
||||
_subscription: Subscription,
|
||||
}
|
||||
|
||||
struct WatchedConnection {
|
||||
server_name: SharedString,
|
||||
messages: Vec<WatchedConnectionMessage>,
|
||||
list_state: ListState,
|
||||
connection: Weak<acp::ClientSideConnection>,
|
||||
incoming_request_methods: HashMap<i32, Arc<str>>,
|
||||
outgoing_request_methods: HashMap<i32, Arc<str>>,
|
||||
_task: Task<()>,
|
||||
}
|
||||
|
||||
impl AcpTools {
|
||||
fn new(project: Entity<Project>, cx: &mut Context<Self>) -> Self {
|
||||
let connection_registry = AcpConnectionRegistry::default_global(cx);
|
||||
|
||||
let subscription = cx.observe(&connection_registry, |this, _, cx| {
|
||||
this.update_connection(cx);
|
||||
cx.notify();
|
||||
});
|
||||
|
||||
let mut this = Self {
|
||||
project,
|
||||
focus_handle: cx.focus_handle(),
|
||||
expanded: HashSet::default(),
|
||||
watched_connection: None,
|
||||
connection_registry,
|
||||
_subscription: subscription,
|
||||
};
|
||||
this.update_connection(cx);
|
||||
this
|
||||
}
|
||||
|
||||
fn update_connection(&mut self, cx: &mut Context<Self>) {
|
||||
let active_connection = self.connection_registry.read(cx).active_connection.borrow();
|
||||
let Some(active_connection) = active_connection.as_ref() else {
|
||||
return;
|
||||
};
|
||||
|
||||
if let Some(watched_connection) = self.watched_connection.as_ref() {
|
||||
if Weak::ptr_eq(
|
||||
&watched_connection.connection,
|
||||
&active_connection.connection,
|
||||
) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(connection) = active_connection.connection.upgrade() {
|
||||
let mut receiver = connection.subscribe();
|
||||
let task = cx.spawn(async move |this, cx| {
|
||||
while let Ok(message) = receiver.recv().await {
|
||||
this.update(cx, |this, cx| {
|
||||
this.push_stream_message(message, cx);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
});
|
||||
|
||||
self.watched_connection = Some(WatchedConnection {
|
||||
server_name: active_connection.server_name.clone(),
|
||||
messages: vec![],
|
||||
list_state: ListState::new(0, ListAlignment::Bottom, px(2048.)),
|
||||
connection: active_connection.connection.clone(),
|
||||
incoming_request_methods: HashMap::default(),
|
||||
outgoing_request_methods: HashMap::default(),
|
||||
_task: task,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn push_stream_message(&mut self, stream_message: acp::StreamMessage, cx: &mut Context<Self>) {
|
||||
let Some(connection) = self.watched_connection.as_mut() else {
|
||||
return;
|
||||
};
|
||||
let language_registry = self.project.read(cx).languages().clone();
|
||||
let index = connection.messages.len();
|
||||
|
||||
let (request_id, method, message_type, params) = match stream_message.message {
|
||||
acp::StreamMessageContent::Request { id, method, params } => {
|
||||
let method_map = match stream_message.direction {
|
||||
acp::StreamMessageDirection::Incoming => {
|
||||
&mut connection.incoming_request_methods
|
||||
}
|
||||
acp::StreamMessageDirection::Outgoing => {
|
||||
&mut connection.outgoing_request_methods
|
||||
}
|
||||
};
|
||||
|
||||
method_map.insert(id, method.clone());
|
||||
(Some(id), method.into(), MessageType::Request, Ok(params))
|
||||
}
|
||||
acp::StreamMessageContent::Response { id, result } => {
|
||||
let method_map = match stream_message.direction {
|
||||
acp::StreamMessageDirection::Incoming => {
|
||||
&mut connection.outgoing_request_methods
|
||||
}
|
||||
acp::StreamMessageDirection::Outgoing => {
|
||||
&mut connection.incoming_request_methods
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(method) = method_map.remove(&id) {
|
||||
(Some(id), method.into(), MessageType::Response, result)
|
||||
} else {
|
||||
(
|
||||
Some(id),
|
||||
"[unrecognized response]".into(),
|
||||
MessageType::Response,
|
||||
result,
|
||||
)
|
||||
}
|
||||
}
|
||||
acp::StreamMessageContent::Notification { method, params } => {
|
||||
(None, method.into(), MessageType::Notification, Ok(params))
|
||||
}
|
||||
};
|
||||
|
||||
let message = WatchedConnectionMessage {
|
||||
name: method,
|
||||
message_type,
|
||||
request_id,
|
||||
direction: stream_message.direction,
|
||||
collapsed_params_md: match params.as_ref() {
|
||||
Ok(params) => params
|
||||
.as_ref()
|
||||
.map(|params| collapsed_params_md(params, &language_registry, cx)),
|
||||
Err(err) => {
|
||||
if let Ok(err) = &serde_json::to_value(err) {
|
||||
Some(collapsed_params_md(&err, &language_registry, cx))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
expanded_params_md: None,
|
||||
params,
|
||||
};
|
||||
|
||||
connection.messages.push(message);
|
||||
connection.list_state.splice(index..index, 1);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn render_message(
|
||||
&mut self,
|
||||
index: usize,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> AnyElement {
|
||||
let Some(connection) = self.watched_connection.as_ref() else {
|
||||
return Empty.into_any();
|
||||
};
|
||||
|
||||
let Some(message) = connection.messages.get(index) else {
|
||||
return Empty.into_any();
|
||||
};
|
||||
|
||||
let base_size = TextSize::Editor.rems(cx);
|
||||
|
||||
let theme_settings = ThemeSettings::get_global(cx);
|
||||
let text_style = window.text_style();
|
||||
|
||||
let colors = cx.theme().colors();
|
||||
let expanded = self.expanded.contains(&index);
|
||||
|
||||
v_flex()
|
||||
.w_full()
|
||||
.px_4()
|
||||
.py_3()
|
||||
.border_color(colors.border)
|
||||
.border_b_1()
|
||||
.gap_2()
|
||||
.items_start()
|
||||
.font_buffer(cx)
|
||||
.text_size(base_size)
|
||||
.id(index)
|
||||
.group("message")
|
||||
.hover(|this| this.bg(colors.element_background.opacity(0.5)))
|
||||
.on_click(cx.listener(move |this, _, _, cx| {
|
||||
if this.expanded.contains(&index) {
|
||||
this.expanded.remove(&index);
|
||||
} else {
|
||||
this.expanded.insert(index);
|
||||
let Some(connection) = &mut this.watched_connection else {
|
||||
return;
|
||||
};
|
||||
let Some(message) = connection.messages.get_mut(index) else {
|
||||
return;
|
||||
};
|
||||
message.expanded(this.project.read(cx).languages().clone(), cx);
|
||||
connection.list_state.scroll_to_reveal_item(index);
|
||||
}
|
||||
cx.notify()
|
||||
}))
|
||||
.child(
|
||||
h_flex()
|
||||
.w_full()
|
||||
.gap_2()
|
||||
.items_center()
|
||||
.flex_shrink_0()
|
||||
.child(match message.direction {
|
||||
acp::StreamMessageDirection::Incoming => {
|
||||
ui::Icon::new(ui::IconName::ArrowDown).color(Color::Error)
|
||||
}
|
||||
acp::StreamMessageDirection::Outgoing => {
|
||||
ui::Icon::new(ui::IconName::ArrowUp).color(Color::Success)
|
||||
}
|
||||
})
|
||||
.child(
|
||||
Label::new(message.name.clone())
|
||||
.buffer_font(cx)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.child(div().flex_1())
|
||||
.child(
|
||||
div()
|
||||
.child(ui::Chip::new(message.message_type.to_string()))
|
||||
.visible_on_hover("message"),
|
||||
)
|
||||
.children(
|
||||
message
|
||||
.request_id
|
||||
.map(|req_id| div().child(ui::Chip::new(req_id.to_string()))),
|
||||
),
|
||||
)
|
||||
// I'm aware using markdown is a hack. Trying to get something working for the demo.
|
||||
// Will clean up soon!
|
||||
.when_some(
|
||||
if expanded {
|
||||
message.expanded_params_md.clone()
|
||||
} else {
|
||||
message.collapsed_params_md.clone()
|
||||
},
|
||||
|this, params| {
|
||||
this.child(
|
||||
div().pl_6().w_full().child(
|
||||
MarkdownElement::new(
|
||||
params,
|
||||
MarkdownStyle {
|
||||
base_text_style: text_style,
|
||||
selection_background_color: colors.element_selection_background,
|
||||
syntax: cx.theme().syntax().clone(),
|
||||
code_block_overflow_x_scroll: true,
|
||||
code_block: StyleRefinement {
|
||||
text: Some(TextStyleRefinement {
|
||||
font_family: Some(
|
||||
theme_settings.buffer_font.family.clone(),
|
||||
),
|
||||
font_size: Some((base_size * 0.8).into()),
|
||||
..Default::default()
|
||||
}),
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.code_block_renderer(
|
||||
CodeBlockRenderer::Default {
|
||||
copy_button: false,
|
||||
copy_button_on_hover: expanded,
|
||||
border: false,
|
||||
},
|
||||
),
|
||||
),
|
||||
)
|
||||
},
|
||||
)
|
||||
.into_any()
|
||||
}
|
||||
}
|
||||
|
||||
struct WatchedConnectionMessage {
|
||||
name: SharedString,
|
||||
request_id: Option<i32>,
|
||||
direction: acp::StreamMessageDirection,
|
||||
message_type: MessageType,
|
||||
params: Result<Option<serde_json::Value>, acp::Error>,
|
||||
collapsed_params_md: Option<Entity<Markdown>>,
|
||||
expanded_params_md: Option<Entity<Markdown>>,
|
||||
}
|
||||
|
||||
impl WatchedConnectionMessage {
|
||||
fn expanded(&mut self, language_registry: Arc<LanguageRegistry>, cx: &mut App) {
|
||||
let params_md = match &self.params {
|
||||
Ok(Some(params)) => Some(expanded_params_md(params, &language_registry, cx)),
|
||||
Err(err) => {
|
||||
if let Some(err) = &serde_json::to_value(err).log_err() {
|
||||
Some(expanded_params_md(&err, &language_registry, cx))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
self.expanded_params_md = params_md;
|
||||
}
|
||||
}
|
||||
|
||||
fn collapsed_params_md(
|
||||
params: &serde_json::Value,
|
||||
language_registry: &Arc<LanguageRegistry>,
|
||||
cx: &mut App,
|
||||
) -> Entity<Markdown> {
|
||||
let params_json = serde_json::to_string(params).unwrap_or_default();
|
||||
let mut spaced_out_json = String::with_capacity(params_json.len() + params_json.len() / 4);
|
||||
|
||||
for ch in params_json.chars() {
|
||||
match ch {
|
||||
'{' => spaced_out_json.push_str("{ "),
|
||||
'}' => spaced_out_json.push_str(" }"),
|
||||
':' => spaced_out_json.push_str(": "),
|
||||
',' => spaced_out_json.push_str(", "),
|
||||
c => spaced_out_json.push(c),
|
||||
}
|
||||
}
|
||||
|
||||
let params_md = format!("```json\n{}\n```", spaced_out_json);
|
||||
cx.new(|cx| Markdown::new(params_md.into(), Some(language_registry.clone()), None, cx))
|
||||
}
|
||||
|
||||
fn expanded_params_md(
|
||||
params: &serde_json::Value,
|
||||
language_registry: &Arc<LanguageRegistry>,
|
||||
cx: &mut App,
|
||||
) -> Entity<Markdown> {
|
||||
let params_json = serde_json::to_string_pretty(params).unwrap_or_default();
|
||||
let params_md = format!("```json\n{}\n```", params_json);
|
||||
cx.new(|cx| Markdown::new(params_md.into(), Some(language_registry.clone()), None, cx))
|
||||
}
|
||||
|
||||
enum MessageType {
|
||||
Request,
|
||||
Response,
|
||||
Notification,
|
||||
}
|
||||
|
||||
impl Display for MessageType {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
MessageType::Request => write!(f, "Request"),
|
||||
MessageType::Response => write!(f, "Response"),
|
||||
MessageType::Notification => write!(f, "Notification"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum AcpToolsEvent {}
|
||||
|
||||
impl EventEmitter<AcpToolsEvent> for AcpTools {}
|
||||
|
||||
impl Item for AcpTools {
|
||||
type Event = AcpToolsEvent;
|
||||
|
||||
fn tab_content_text(&self, _detail: usize, _cx: &App) -> ui::SharedString {
|
||||
format!(
|
||||
"ACP: {}",
|
||||
self.watched_connection
|
||||
.as_ref()
|
||||
.map_or("Disconnected", |connection| &connection.server_name)
|
||||
)
|
||||
.into()
|
||||
}
|
||||
|
||||
fn tab_icon(&self, _window: &Window, _cx: &App) -> Option<Icon> {
|
||||
Some(ui::Icon::new(IconName::Thread))
|
||||
}
|
||||
}
|
||||
|
||||
impl Focusable for AcpTools {
|
||||
fn focus_handle(&self, _cx: &App) -> FocusHandle {
|
||||
self.focus_handle.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for AcpTools {
|
||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
v_flex()
|
||||
.track_focus(&self.focus_handle)
|
||||
.size_full()
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.child(match self.watched_connection.as_ref() {
|
||||
Some(connection) => {
|
||||
if connection.messages.is_empty() {
|
||||
h_flex()
|
||||
.size_full()
|
||||
.justify_center()
|
||||
.items_center()
|
||||
.child("No messages recorded yet")
|
||||
.into_any()
|
||||
} else {
|
||||
list(
|
||||
connection.list_state.clone(),
|
||||
cx.processor(Self::render_message),
|
||||
)
|
||||
.with_sizing_behavior(gpui::ListSizingBehavior::Auto)
|
||||
.flex_grow()
|
||||
.into_any()
|
||||
}
|
||||
}
|
||||
None => h_flex()
|
||||
.size_full()
|
||||
.justify_center()
|
||||
.items_center()
|
||||
.child("No active connection")
|
||||
.into_any(),
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -893,8 +893,19 @@ impl ThreadsDatabase {
|
||||
|
||||
let needs_migration_from_heed = mdb_path.exists();
|
||||
|
||||
let connection = if *ZED_STATELESS || cfg!(any(feature = "test-support", test)) {
|
||||
let connection = if *ZED_STATELESS {
|
||||
Connection::open_memory(Some("THREAD_FALLBACK_DB"))
|
||||
} else if cfg!(any(feature = "test-support", test)) {
|
||||
// rust stores the name of the test on the current thread.
|
||||
// We use this to automatically create a database that will
|
||||
// be shared within the test (for the test_retrieve_old_thread)
|
||||
// but not with concurrent tests.
|
||||
let thread = std::thread::current();
|
||||
let test_name = thread.name();
|
||||
Connection::open_memory(Some(&format!(
|
||||
"THREAD_FALLBACK_{}",
|
||||
test_name.unwrap_or_default()
|
||||
)))
|
||||
} else {
|
||||
Connection::open_file(&sqlite_path.to_string_lossy())
|
||||
};
|
||||
|
||||
@@ -180,7 +180,7 @@ impl NativeAgent {
|
||||
fs: Arc<dyn Fs>,
|
||||
cx: &mut AsyncApp,
|
||||
) -> Result<Entity<NativeAgent>> {
|
||||
log::info!("Creating new NativeAgent");
|
||||
log::debug!("Creating new NativeAgent");
|
||||
|
||||
let project_context = cx
|
||||
.update(|cx| Self::build_project_context(&project, prompt_store.as_ref(), cx))?
|
||||
@@ -240,13 +240,16 @@ impl NativeAgent {
|
||||
let title = thread.title();
|
||||
let project = thread.project.clone();
|
||||
let action_log = thread.action_log.clone();
|
||||
let acp_thread = cx.new(|_cx| {
|
||||
let prompt_capabilities_rx = thread.prompt_capabilities_rx.clone();
|
||||
let acp_thread = cx.new(|cx| {
|
||||
acp_thread::AcpThread::new(
|
||||
title,
|
||||
connection,
|
||||
project.clone(),
|
||||
action_log.clone(),
|
||||
session_id.clone(),
|
||||
prompt_capabilities_rx,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
let subscriptions = vec![
|
||||
@@ -756,7 +759,7 @@ impl NativeAgentConnection {
|
||||
}
|
||||
}
|
||||
|
||||
log::info!("Response stream completed");
|
||||
log::debug!("Response stream completed");
|
||||
anyhow::Ok(acp::PromptResponse {
|
||||
stop_reason: acp::StopReason::EndTurn,
|
||||
})
|
||||
@@ -781,7 +784,7 @@ impl AgentModelSelector for NativeAgentConnection {
|
||||
model_id: acp_thread::AgentModelId,
|
||||
cx: &mut App,
|
||||
) -> Task<Result<()>> {
|
||||
log::info!("Setting model for session {}: {}", session_id, model_id);
|
||||
log::debug!("Setting model for session {}: {}", session_id, model_id);
|
||||
let Some(thread) = self
|
||||
.0
|
||||
.read(cx)
|
||||
@@ -852,7 +855,7 @@ impl acp_thread::AgentConnection for NativeAgentConnection {
|
||||
cx: &mut App,
|
||||
) -> Task<Result<Entity<acp_thread::AcpThread>>> {
|
||||
let agent = self.0.clone();
|
||||
log::info!("Creating new thread for project at: {:?}", cwd);
|
||||
log::debug!("Creating new thread for project at: {:?}", cwd);
|
||||
|
||||
cx.spawn(async move |cx| {
|
||||
log::debug!("Starting thread creation in async context");
|
||||
@@ -917,7 +920,7 @@ impl acp_thread::AgentConnection for NativeAgentConnection {
|
||||
.into_iter()
|
||||
.map(Into::into)
|
||||
.collect::<Vec<_>>();
|
||||
log::info!("Converted prompt to message: {} chars", content.len());
|
||||
log::debug!("Converted prompt to message: {} chars", content.len());
|
||||
log::debug!("Message id: {:?}", id);
|
||||
log::debug!("Message content: {:?}", content);
|
||||
|
||||
@@ -925,18 +928,10 @@ impl acp_thread::AgentConnection for NativeAgentConnection {
|
||||
})
|
||||
}
|
||||
|
||||
fn prompt_capabilities(&self) -> acp::PromptCapabilities {
|
||||
acp::PromptCapabilities {
|
||||
image: true,
|
||||
audio: false,
|
||||
embedded_context: true,
|
||||
}
|
||||
}
|
||||
|
||||
fn resume(
|
||||
&self,
|
||||
session_id: &acp::SessionId,
|
||||
_cx: &mut App,
|
||||
_cx: &App,
|
||||
) -> Option<Rc<dyn acp_thread::AgentSessionResume>> {
|
||||
Some(Rc::new(NativeAgentSessionResume {
|
||||
connection: self.clone(),
|
||||
@@ -956,9 +951,9 @@ impl acp_thread::AgentConnection for NativeAgentConnection {
|
||||
fn truncate(
|
||||
&self,
|
||||
session_id: &agent_client_protocol::SessionId,
|
||||
cx: &mut App,
|
||||
cx: &App,
|
||||
) -> Option<Rc<dyn acp_thread::AgentSessionTruncate>> {
|
||||
self.0.update(cx, |agent, _cx| {
|
||||
self.0.read_with(cx, |agent, _cx| {
|
||||
agent.sessions.get(session_id).map(|session| {
|
||||
Rc::new(NativeAgentSessionEditor {
|
||||
thread: session.thread.clone(),
|
||||
@@ -971,7 +966,7 @@ impl acp_thread::AgentConnection for NativeAgentConnection {
|
||||
fn set_title(
|
||||
&self,
|
||||
session_id: &acp::SessionId,
|
||||
_cx: &mut App,
|
||||
_cx: &App,
|
||||
) -> Option<Rc<dyn acp_thread::AgentSessionSetTitle>> {
|
||||
Some(Rc::new(NativeAgentSessionSetTitle {
|
||||
connection: self.clone(),
|
||||
|
||||
@@ -266,8 +266,19 @@ impl ThreadsDatabase {
|
||||
}
|
||||
|
||||
pub fn new(executor: BackgroundExecutor) -> Result<Self> {
|
||||
let connection = if *ZED_STATELESS || cfg!(any(feature = "test-support", test)) {
|
||||
let connection = if *ZED_STATELESS {
|
||||
Connection::open_memory(Some("THREAD_FALLBACK_DB"))
|
||||
} else if cfg!(any(feature = "test-support", test)) {
|
||||
// rust stores the name of the test on the current thread.
|
||||
// We use this to automatically create a database that will
|
||||
// be shared within the test (for the test_retrieve_old_thread)
|
||||
// but not with concurrent tests.
|
||||
let thread = std::thread::current();
|
||||
let test_name = thread.name();
|
||||
Connection::open_memory(Some(&format!(
|
||||
"THREAD_FALLBACK_{}",
|
||||
test_name.unwrap_or_default()
|
||||
)))
|
||||
} else {
|
||||
let threads_dir = paths::data_dir().join("threads");
|
||||
std::fs::create_dir_all(&threads_dir)?;
|
||||
|
||||
@@ -3,7 +3,7 @@ use std::{any::Any, path::Path, rc::Rc, sync::Arc};
|
||||
use agent_servers::AgentServer;
|
||||
use anyhow::Result;
|
||||
use fs::Fs;
|
||||
use gpui::{App, Entity, Task};
|
||||
use gpui::{App, Entity, SharedString, Task};
|
||||
use project::Project;
|
||||
use prompt_store::PromptStore;
|
||||
|
||||
@@ -22,16 +22,20 @@ impl NativeAgentServer {
|
||||
}
|
||||
|
||||
impl AgentServer for NativeAgentServer {
|
||||
fn name(&self) -> &'static str {
|
||||
"Native Agent"
|
||||
fn telemetry_id(&self) -> &'static str {
|
||||
"zed"
|
||||
}
|
||||
|
||||
fn empty_state_headline(&self) -> &'static str {
|
||||
"Welcome to the Agent Panel"
|
||||
fn name(&self) -> SharedString {
|
||||
"Zed Agent".into()
|
||||
}
|
||||
|
||||
fn empty_state_message(&self) -> &'static str {
|
||||
""
|
||||
fn empty_state_headline(&self) -> SharedString {
|
||||
self.name()
|
||||
}
|
||||
|
||||
fn empty_state_message(&self) -> SharedString {
|
||||
"".into()
|
||||
}
|
||||
|
||||
fn logo(&self) -> ui::IconName {
|
||||
@@ -44,7 +48,7 @@ impl AgentServer for NativeAgentServer {
|
||||
project: &Entity<Project>,
|
||||
cx: &mut App,
|
||||
) -> Task<Result<Rc<dyn acp_thread::AgentConnection>>> {
|
||||
log::info!(
|
||||
log::debug!(
|
||||
"NativeAgentServer::connect called for path: {:?}",
|
||||
_root_dir
|
||||
);
|
||||
@@ -63,7 +67,7 @@ impl AgentServer for NativeAgentServer {
|
||||
|
||||
// Create the connection wrapper
|
||||
let connection = NativeAgentConnection(agent);
|
||||
log::info!("NativeAgentServer connection established successfully");
|
||||
log::debug!("NativeAgentServer connection established successfully");
|
||||
|
||||
Ok(Rc::new(connection) as Rc<dyn acp_thread::AgentConnection>)
|
||||
})
|
||||
|
||||
@@ -4,26 +4,37 @@ use agent_client_protocol::{self as acp};
|
||||
use agent_settings::AgentProfileId;
|
||||
use anyhow::Result;
|
||||
use client::{Client, UserStore};
|
||||
use cloud_llm_client::CompletionIntent;
|
||||
use collections::IndexMap;
|
||||
use context_server::{ContextServer, ContextServerCommand, ContextServerId};
|
||||
use fs::{FakeFs, Fs};
|
||||
use futures::{StreamExt, channel::mpsc::UnboundedReceiver};
|
||||
use futures::{
|
||||
StreamExt,
|
||||
channel::{
|
||||
mpsc::{self, UnboundedReceiver},
|
||||
oneshot,
|
||||
},
|
||||
};
|
||||
use gpui::{
|
||||
App, AppContext, Entity, Task, TestAppContext, UpdateGlobal, http_client::FakeHttpClient,
|
||||
};
|
||||
use indoc::indoc;
|
||||
use language_model::{
|
||||
LanguageModel, LanguageModelCompletionError, LanguageModelCompletionEvent, LanguageModelId,
|
||||
LanguageModelProviderName, LanguageModelRegistry, LanguageModelRequestMessage,
|
||||
LanguageModelToolResult, LanguageModelToolUse, MessageContent, Role, StopReason,
|
||||
fake_provider::FakeLanguageModel,
|
||||
LanguageModelProviderName, LanguageModelRegistry, LanguageModelRequest,
|
||||
LanguageModelRequestMessage, LanguageModelToolResult, LanguageModelToolSchemaFormat,
|
||||
LanguageModelToolUse, MessageContent, Role, StopReason, fake_provider::FakeLanguageModel,
|
||||
};
|
||||
use pretty_assertions::assert_eq;
|
||||
use project::Project;
|
||||
use project::{
|
||||
Project, context_server_store::ContextServerStore, project_settings::ProjectSettings,
|
||||
};
|
||||
use prompt_store::ProjectContext;
|
||||
use reqwest_client::ReqwestClient;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::json;
|
||||
use settings::SettingsStore;
|
||||
use settings::{Settings, SettingsStore};
|
||||
use std::{path::Path, rc::Rc, sync::Arc, time::Duration};
|
||||
use util::path;
|
||||
|
||||
@@ -663,15 +674,6 @@ async fn test_resume_after_tool_use_limit(cx: &mut TestAppContext) {
|
||||
"}
|
||||
)
|
||||
});
|
||||
|
||||
// Ensure we error if calling resume when tool use limit was *not* reached.
|
||||
let error = thread
|
||||
.update(cx, |thread, cx| thread.resume(cx))
|
||||
.unwrap_err();
|
||||
assert_eq!(
|
||||
error.to_string(),
|
||||
"can only resume after tool use limit is reached"
|
||||
)
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
@@ -931,6 +933,334 @@ async fn test_profiles(cx: &mut TestAppContext) {
|
||||
assert_eq!(tool_names, vec![InfiniteTool::name()]);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_mcp_tools(cx: &mut TestAppContext) {
|
||||
let ThreadTest {
|
||||
model,
|
||||
thread,
|
||||
context_server_store,
|
||||
fs,
|
||||
..
|
||||
} = setup(cx, TestModel::Fake).await;
|
||||
let fake_model = model.as_fake();
|
||||
|
||||
// Override profiles and wait for settings to be loaded.
|
||||
fs.insert_file(
|
||||
paths::settings_file(),
|
||||
json!({
|
||||
"agent": {
|
||||
"profiles": {
|
||||
"test": {
|
||||
"name": "Test Profile",
|
||||
"enable_all_context_servers": true,
|
||||
"tools": {
|
||||
EchoTool::name(): true,
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
})
|
||||
.to_string()
|
||||
.into_bytes(),
|
||||
)
|
||||
.await;
|
||||
cx.run_until_parked();
|
||||
thread.update(cx, |thread, _| {
|
||||
thread.set_profile(AgentProfileId("test".into()))
|
||||
});
|
||||
|
||||
let mut mcp_tool_calls = setup_context_server(
|
||||
"test_server",
|
||||
vec![context_server::types::Tool {
|
||||
name: "echo".into(),
|
||||
description: None,
|
||||
input_schema: serde_json::to_value(
|
||||
EchoTool.input_schema(LanguageModelToolSchemaFormat::JsonSchema),
|
||||
)
|
||||
.unwrap(),
|
||||
output_schema: None,
|
||||
annotations: None,
|
||||
}],
|
||||
&context_server_store,
|
||||
cx,
|
||||
);
|
||||
|
||||
let events = thread.update(cx, |thread, cx| {
|
||||
thread.send(UserMessageId::new(), ["Hey"], cx).unwrap()
|
||||
});
|
||||
cx.run_until_parked();
|
||||
|
||||
// Simulate the model calling the MCP tool.
|
||||
let completion = fake_model.pending_completions().pop().unwrap();
|
||||
assert_eq!(tool_names_for_completion(&completion), vec!["echo"]);
|
||||
fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse(
|
||||
LanguageModelToolUse {
|
||||
id: "tool_1".into(),
|
||||
name: "echo".into(),
|
||||
raw_input: json!({"text": "test"}).to_string(),
|
||||
input: json!({"text": "test"}),
|
||||
is_input_complete: true,
|
||||
},
|
||||
));
|
||||
fake_model.end_last_completion_stream();
|
||||
cx.run_until_parked();
|
||||
|
||||
let (tool_call_params, tool_call_response) = mcp_tool_calls.next().await.unwrap();
|
||||
assert_eq!(tool_call_params.name, "echo");
|
||||
assert_eq!(tool_call_params.arguments, Some(json!({"text": "test"})));
|
||||
tool_call_response
|
||||
.send(context_server::types::CallToolResponse {
|
||||
content: vec![context_server::types::ToolResponseContent::Text {
|
||||
text: "test".into(),
|
||||
}],
|
||||
is_error: None,
|
||||
meta: None,
|
||||
structured_content: None,
|
||||
})
|
||||
.unwrap();
|
||||
cx.run_until_parked();
|
||||
|
||||
assert_eq!(tool_names_for_completion(&completion), vec!["echo"]);
|
||||
fake_model.send_last_completion_stream_text_chunk("Done!");
|
||||
fake_model.end_last_completion_stream();
|
||||
events.collect::<Vec<_>>().await;
|
||||
|
||||
// Send again after adding the echo tool, ensuring the name collision is resolved.
|
||||
let events = thread.update(cx, |thread, cx| {
|
||||
thread.add_tool(EchoTool);
|
||||
thread.send(UserMessageId::new(), ["Go"], cx).unwrap()
|
||||
});
|
||||
cx.run_until_parked();
|
||||
let completion = fake_model.pending_completions().pop().unwrap();
|
||||
assert_eq!(
|
||||
tool_names_for_completion(&completion),
|
||||
vec!["echo", "test_server_echo"]
|
||||
);
|
||||
fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse(
|
||||
LanguageModelToolUse {
|
||||
id: "tool_2".into(),
|
||||
name: "test_server_echo".into(),
|
||||
raw_input: json!({"text": "mcp"}).to_string(),
|
||||
input: json!({"text": "mcp"}),
|
||||
is_input_complete: true,
|
||||
},
|
||||
));
|
||||
fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse(
|
||||
LanguageModelToolUse {
|
||||
id: "tool_3".into(),
|
||||
name: "echo".into(),
|
||||
raw_input: json!({"text": "native"}).to_string(),
|
||||
input: json!({"text": "native"}),
|
||||
is_input_complete: true,
|
||||
},
|
||||
));
|
||||
fake_model.end_last_completion_stream();
|
||||
cx.run_until_parked();
|
||||
|
||||
let (tool_call_params, tool_call_response) = mcp_tool_calls.next().await.unwrap();
|
||||
assert_eq!(tool_call_params.name, "echo");
|
||||
assert_eq!(tool_call_params.arguments, Some(json!({"text": "mcp"})));
|
||||
tool_call_response
|
||||
.send(context_server::types::CallToolResponse {
|
||||
content: vec![context_server::types::ToolResponseContent::Text { text: "mcp".into() }],
|
||||
is_error: None,
|
||||
meta: None,
|
||||
structured_content: None,
|
||||
})
|
||||
.unwrap();
|
||||
cx.run_until_parked();
|
||||
|
||||
// Ensure the tool results were inserted with the correct names.
|
||||
let completion = fake_model.pending_completions().pop().unwrap();
|
||||
assert_eq!(
|
||||
completion.messages.last().unwrap().content,
|
||||
vec![
|
||||
MessageContent::ToolResult(LanguageModelToolResult {
|
||||
tool_use_id: "tool_3".into(),
|
||||
tool_name: "echo".into(),
|
||||
is_error: false,
|
||||
content: "native".into(),
|
||||
output: Some("native".into()),
|
||||
},),
|
||||
MessageContent::ToolResult(LanguageModelToolResult {
|
||||
tool_use_id: "tool_2".into(),
|
||||
tool_name: "test_server_echo".into(),
|
||||
is_error: false,
|
||||
content: "mcp".into(),
|
||||
output: Some("mcp".into()),
|
||||
},),
|
||||
]
|
||||
);
|
||||
fake_model.end_last_completion_stream();
|
||||
events.collect::<Vec<_>>().await;
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_mcp_tool_truncation(cx: &mut TestAppContext) {
|
||||
let ThreadTest {
|
||||
model,
|
||||
thread,
|
||||
context_server_store,
|
||||
fs,
|
||||
..
|
||||
} = setup(cx, TestModel::Fake).await;
|
||||
let fake_model = model.as_fake();
|
||||
|
||||
// Set up a profile with all tools enabled
|
||||
fs.insert_file(
|
||||
paths::settings_file(),
|
||||
json!({
|
||||
"agent": {
|
||||
"profiles": {
|
||||
"test": {
|
||||
"name": "Test Profile",
|
||||
"enable_all_context_servers": true,
|
||||
"tools": {
|
||||
EchoTool::name(): true,
|
||||
DelayTool::name(): true,
|
||||
WordListTool::name(): true,
|
||||
ToolRequiringPermission::name(): true,
|
||||
InfiniteTool::name(): true,
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
})
|
||||
.to_string()
|
||||
.into_bytes(),
|
||||
)
|
||||
.await;
|
||||
cx.run_until_parked();
|
||||
|
||||
thread.update(cx, |thread, _| {
|
||||
thread.set_profile(AgentProfileId("test".into()));
|
||||
thread.add_tool(EchoTool);
|
||||
thread.add_tool(DelayTool);
|
||||
thread.add_tool(WordListTool);
|
||||
thread.add_tool(ToolRequiringPermission);
|
||||
thread.add_tool(InfiniteTool);
|
||||
});
|
||||
|
||||
// Set up multiple context servers with some overlapping tool names
|
||||
let _server1_calls = setup_context_server(
|
||||
"xxx",
|
||||
vec![
|
||||
context_server::types::Tool {
|
||||
name: "echo".into(), // Conflicts with native EchoTool
|
||||
description: None,
|
||||
input_schema: serde_json::to_value(
|
||||
EchoTool.input_schema(LanguageModelToolSchemaFormat::JsonSchema),
|
||||
)
|
||||
.unwrap(),
|
||||
output_schema: None,
|
||||
annotations: None,
|
||||
},
|
||||
context_server::types::Tool {
|
||||
name: "unique_tool_1".into(),
|
||||
description: None,
|
||||
input_schema: json!({"type": "object", "properties": {}}),
|
||||
output_schema: None,
|
||||
annotations: None,
|
||||
},
|
||||
],
|
||||
&context_server_store,
|
||||
cx,
|
||||
);
|
||||
|
||||
let _server2_calls = setup_context_server(
|
||||
"yyy",
|
||||
vec![
|
||||
context_server::types::Tool {
|
||||
name: "echo".into(), // Also conflicts with native EchoTool
|
||||
description: None,
|
||||
input_schema: serde_json::to_value(
|
||||
EchoTool.input_schema(LanguageModelToolSchemaFormat::JsonSchema),
|
||||
)
|
||||
.unwrap(),
|
||||
output_schema: None,
|
||||
annotations: None,
|
||||
},
|
||||
context_server::types::Tool {
|
||||
name: "unique_tool_2".into(),
|
||||
description: None,
|
||||
input_schema: json!({"type": "object", "properties": {}}),
|
||||
output_schema: None,
|
||||
annotations: None,
|
||||
},
|
||||
context_server::types::Tool {
|
||||
name: "a".repeat(MAX_TOOL_NAME_LENGTH - 2),
|
||||
description: None,
|
||||
input_schema: json!({"type": "object", "properties": {}}),
|
||||
output_schema: None,
|
||||
annotations: None,
|
||||
},
|
||||
context_server::types::Tool {
|
||||
name: "b".repeat(MAX_TOOL_NAME_LENGTH - 1),
|
||||
description: None,
|
||||
input_schema: json!({"type": "object", "properties": {}}),
|
||||
output_schema: None,
|
||||
annotations: None,
|
||||
},
|
||||
],
|
||||
&context_server_store,
|
||||
cx,
|
||||
);
|
||||
let _server3_calls = setup_context_server(
|
||||
"zzz",
|
||||
vec![
|
||||
context_server::types::Tool {
|
||||
name: "a".repeat(MAX_TOOL_NAME_LENGTH - 2),
|
||||
description: None,
|
||||
input_schema: json!({"type": "object", "properties": {}}),
|
||||
output_schema: None,
|
||||
annotations: None,
|
||||
},
|
||||
context_server::types::Tool {
|
||||
name: "b".repeat(MAX_TOOL_NAME_LENGTH - 1),
|
||||
description: None,
|
||||
input_schema: json!({"type": "object", "properties": {}}),
|
||||
output_schema: None,
|
||||
annotations: None,
|
||||
},
|
||||
context_server::types::Tool {
|
||||
name: "c".repeat(MAX_TOOL_NAME_LENGTH + 1),
|
||||
description: None,
|
||||
input_schema: json!({"type": "object", "properties": {}}),
|
||||
output_schema: None,
|
||||
annotations: None,
|
||||
},
|
||||
],
|
||||
&context_server_store,
|
||||
cx,
|
||||
);
|
||||
|
||||
thread
|
||||
.update(cx, |thread, cx| {
|
||||
thread.send(UserMessageId::new(), ["Go"], cx)
|
||||
})
|
||||
.unwrap();
|
||||
cx.run_until_parked();
|
||||
let completion = fake_model.pending_completions().pop().unwrap();
|
||||
assert_eq!(
|
||||
tool_names_for_completion(&completion),
|
||||
vec![
|
||||
"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
|
||||
"cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc",
|
||||
"delay",
|
||||
"echo",
|
||||
"infinite",
|
||||
"tool_requiring_permission",
|
||||
"unique_tool_1",
|
||||
"unique_tool_2",
|
||||
"word_list",
|
||||
"xxx_echo",
|
||||
"y_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
||||
"yyy_echo",
|
||||
"z_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
#[cfg_attr(not(feature = "e2e"), ignore)]
|
||||
async fn test_cancellation(cx: &mut TestAppContext) {
|
||||
@@ -1355,6 +1685,7 @@ async fn test_truncate_second_message(cx: &mut TestAppContext) {
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
#[cfg_attr(target_os = "windows", ignore)] // TODO: Fix this test on Windows
|
||||
async fn test_title_generation(cx: &mut TestAppContext) {
|
||||
let ThreadTest { model, thread, .. } = setup(cx, TestModel::Fake).await;
|
||||
let fake_model = model.as_fake();
|
||||
@@ -1400,6 +1731,81 @@ async fn test_title_generation(cx: &mut TestAppContext) {
|
||||
thread.read_with(cx, |thread, _| assert_eq!(thread.title(), "Hello world"));
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_building_request_with_pending_tools(cx: &mut TestAppContext) {
|
||||
let ThreadTest { model, thread, .. } = setup(cx, TestModel::Fake).await;
|
||||
let fake_model = model.as_fake();
|
||||
|
||||
let _events = thread
|
||||
.update(cx, |thread, cx| {
|
||||
thread.add_tool(ToolRequiringPermission);
|
||||
thread.add_tool(EchoTool);
|
||||
thread.send(UserMessageId::new(), ["Hey!"], cx)
|
||||
})
|
||||
.unwrap();
|
||||
cx.run_until_parked();
|
||||
|
||||
let permission_tool_use = LanguageModelToolUse {
|
||||
id: "tool_id_1".into(),
|
||||
name: ToolRequiringPermission::name().into(),
|
||||
raw_input: "{}".into(),
|
||||
input: json!({}),
|
||||
is_input_complete: true,
|
||||
};
|
||||
let echo_tool_use = LanguageModelToolUse {
|
||||
id: "tool_id_2".into(),
|
||||
name: EchoTool::name().into(),
|
||||
raw_input: json!({"text": "test"}).to_string(),
|
||||
input: json!({"text": "test"}),
|
||||
is_input_complete: true,
|
||||
};
|
||||
fake_model.send_last_completion_stream_text_chunk("Hi!");
|
||||
fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse(
|
||||
permission_tool_use,
|
||||
));
|
||||
fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse(
|
||||
echo_tool_use.clone(),
|
||||
));
|
||||
fake_model.end_last_completion_stream();
|
||||
cx.run_until_parked();
|
||||
|
||||
// Ensure pending tools are skipped when building a request.
|
||||
let request = thread
|
||||
.read_with(cx, |thread, cx| {
|
||||
thread.build_completion_request(CompletionIntent::EditFile, cx)
|
||||
})
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
request.messages[1..],
|
||||
vec![
|
||||
LanguageModelRequestMessage {
|
||||
role: Role::User,
|
||||
content: vec!["Hey!".into()],
|
||||
cache: true
|
||||
},
|
||||
LanguageModelRequestMessage {
|
||||
role: Role::Assistant,
|
||||
content: vec![
|
||||
MessageContent::Text("Hi!".into()),
|
||||
MessageContent::ToolUse(echo_tool_use.clone())
|
||||
],
|
||||
cache: false
|
||||
},
|
||||
LanguageModelRequestMessage {
|
||||
role: Role::User,
|
||||
content: vec![MessageContent::ToolResult(LanguageModelToolResult {
|
||||
tool_use_id: echo_tool_use.id.clone(),
|
||||
tool_name: echo_tool_use.name,
|
||||
is_error: false,
|
||||
content: "test".into(),
|
||||
output: Some("test".into())
|
||||
})],
|
||||
cache: false
|
||||
},
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_agent_connection(cx: &mut TestAppContext) {
|
||||
cx.update(settings::init);
|
||||
@@ -1692,6 +2098,7 @@ async fn test_send_retry_on_error(cx: &mut TestAppContext) {
|
||||
.unwrap();
|
||||
cx.run_until_parked();
|
||||
|
||||
fake_model.send_last_completion_stream_text_chunk("Hey,");
|
||||
fake_model.send_last_completion_stream_error(LanguageModelCompletionError::ServerOverloaded {
|
||||
provider: LanguageModelProviderName::new("Anthropic"),
|
||||
retry_after: Some(Duration::from_secs(3)),
|
||||
@@ -1701,8 +2108,9 @@ async fn test_send_retry_on_error(cx: &mut TestAppContext) {
|
||||
cx.executor().advance_clock(Duration::from_secs(3));
|
||||
cx.run_until_parked();
|
||||
|
||||
fake_model.send_last_completion_stream_text_chunk("Hey!");
|
||||
fake_model.send_last_completion_stream_text_chunk("there!");
|
||||
fake_model.end_last_completion_stream();
|
||||
cx.run_until_parked();
|
||||
|
||||
let mut retry_events = Vec::new();
|
||||
while let Some(Ok(event)) = events.next().await {
|
||||
@@ -1730,12 +2138,94 @@ async fn test_send_retry_on_error(cx: &mut TestAppContext) {
|
||||
|
||||
## Assistant
|
||||
|
||||
Hey!
|
||||
Hey,
|
||||
|
||||
[resume]
|
||||
|
||||
## Assistant
|
||||
|
||||
there!
|
||||
"}
|
||||
)
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_send_retry_finishes_tool_calls_on_error(cx: &mut TestAppContext) {
|
||||
let ThreadTest { thread, model, .. } = setup(cx, TestModel::Fake).await;
|
||||
let fake_model = model.as_fake();
|
||||
|
||||
let events = thread
|
||||
.update(cx, |thread, cx| {
|
||||
thread.set_completion_mode(agent_settings::CompletionMode::Burn, cx);
|
||||
thread.add_tool(EchoTool);
|
||||
thread.send(UserMessageId::new(), ["Call the echo tool!"], cx)
|
||||
})
|
||||
.unwrap();
|
||||
cx.run_until_parked();
|
||||
|
||||
let tool_use_1 = LanguageModelToolUse {
|
||||
id: "tool_1".into(),
|
||||
name: EchoTool::name().into(),
|
||||
raw_input: json!({"text": "test"}).to_string(),
|
||||
input: json!({"text": "test"}),
|
||||
is_input_complete: true,
|
||||
};
|
||||
fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse(
|
||||
tool_use_1.clone(),
|
||||
));
|
||||
fake_model.send_last_completion_stream_error(LanguageModelCompletionError::ServerOverloaded {
|
||||
provider: LanguageModelProviderName::new("Anthropic"),
|
||||
retry_after: Some(Duration::from_secs(3)),
|
||||
});
|
||||
fake_model.end_last_completion_stream();
|
||||
|
||||
cx.executor().advance_clock(Duration::from_secs(3));
|
||||
let completion = fake_model.pending_completions().pop().unwrap();
|
||||
assert_eq!(
|
||||
completion.messages[1..],
|
||||
vec![
|
||||
LanguageModelRequestMessage {
|
||||
role: Role::User,
|
||||
content: vec!["Call the echo tool!".into()],
|
||||
cache: false
|
||||
},
|
||||
LanguageModelRequestMessage {
|
||||
role: Role::Assistant,
|
||||
content: vec![language_model::MessageContent::ToolUse(tool_use_1.clone())],
|
||||
cache: false
|
||||
},
|
||||
LanguageModelRequestMessage {
|
||||
role: Role::User,
|
||||
content: vec![language_model::MessageContent::ToolResult(
|
||||
LanguageModelToolResult {
|
||||
tool_use_id: tool_use_1.id.clone(),
|
||||
tool_name: tool_use_1.name.clone(),
|
||||
is_error: false,
|
||||
content: "test".into(),
|
||||
output: Some("test".into())
|
||||
}
|
||||
)],
|
||||
cache: true
|
||||
},
|
||||
]
|
||||
);
|
||||
|
||||
fake_model.send_last_completion_stream_text_chunk("Done");
|
||||
fake_model.end_last_completion_stream();
|
||||
cx.run_until_parked();
|
||||
events.collect::<Vec<_>>().await;
|
||||
thread.read_with(cx, |thread, _cx| {
|
||||
assert_eq!(
|
||||
thread.last_message(),
|
||||
Some(Message::Agent(AgentMessage {
|
||||
content: vec![AgentMessageContent::Text("Done".into())],
|
||||
tool_results: IndexMap::default()
|
||||
}))
|
||||
);
|
||||
})
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_send_max_retries_exceeded(cx: &mut TestAppContext) {
|
||||
let ThreadTest { thread, model, .. } = setup(cx, TestModel::Fake).await;
|
||||
@@ -1806,6 +2296,7 @@ struct ThreadTest {
|
||||
model: Arc<dyn LanguageModel>,
|
||||
thread: Entity<Thread>,
|
||||
project_context: Entity<ProjectContext>,
|
||||
context_server_store: Entity<ContextServerStore>,
|
||||
fs: Arc<FakeFs>,
|
||||
}
|
||||
|
||||
@@ -1844,6 +2335,7 @@ async fn setup(cx: &mut TestAppContext, model: TestModel) -> ThreadTest {
|
||||
WordListTool::name(): true,
|
||||
ToolRequiringPermission::name(): true,
|
||||
InfiniteTool::name(): true,
|
||||
ThinkingTool::name(): true,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1900,8 +2392,9 @@ async fn setup(cx: &mut TestAppContext, model: TestModel) -> ThreadTest {
|
||||
.await;
|
||||
|
||||
let project_context = cx.new(|_cx| ProjectContext::default());
|
||||
let context_server_store = project.read_with(cx, |project, _| project.context_server_store());
|
||||
let context_server_registry =
|
||||
cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
|
||||
cx.new(|cx| ContextServerRegistry::new(context_server_store.clone(), cx));
|
||||
let thread = cx.new(|cx| {
|
||||
Thread::new(
|
||||
project,
|
||||
@@ -1916,6 +2409,7 @@ async fn setup(cx: &mut TestAppContext, model: TestModel) -> ThreadTest {
|
||||
model,
|
||||
thread,
|
||||
project_context,
|
||||
context_server_store,
|
||||
fs,
|
||||
}
|
||||
}
|
||||
@@ -1950,3 +2444,89 @@ fn watch_settings(fs: Arc<dyn Fs>, cx: &mut App) {
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
fn tool_names_for_completion(completion: &LanguageModelRequest) -> Vec<String> {
|
||||
completion
|
||||
.tools
|
||||
.iter()
|
||||
.map(|tool| tool.name.clone())
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn setup_context_server(
|
||||
name: &'static str,
|
||||
tools: Vec<context_server::types::Tool>,
|
||||
context_server_store: &Entity<ContextServerStore>,
|
||||
cx: &mut TestAppContext,
|
||||
) -> mpsc::UnboundedReceiver<(
|
||||
context_server::types::CallToolParams,
|
||||
oneshot::Sender<context_server::types::CallToolResponse>,
|
||||
)> {
|
||||
cx.update(|cx| {
|
||||
let mut settings = ProjectSettings::get_global(cx).clone();
|
||||
settings.context_servers.insert(
|
||||
name.into(),
|
||||
project::project_settings::ContextServerSettings::Custom {
|
||||
enabled: true,
|
||||
command: ContextServerCommand {
|
||||
path: "somebinary".into(),
|
||||
args: Vec::new(),
|
||||
env: None,
|
||||
},
|
||||
},
|
||||
);
|
||||
ProjectSettings::override_global(settings, cx);
|
||||
});
|
||||
|
||||
let (mcp_tool_calls_tx, mcp_tool_calls_rx) = mpsc::unbounded();
|
||||
let fake_transport = context_server::test::create_fake_transport(name, cx.executor())
|
||||
.on_request::<context_server::types::requests::Initialize, _>(move |_params| async move {
|
||||
context_server::types::InitializeResponse {
|
||||
protocol_version: context_server::types::ProtocolVersion(
|
||||
context_server::types::LATEST_PROTOCOL_VERSION.to_string(),
|
||||
),
|
||||
server_info: context_server::types::Implementation {
|
||||
name: name.into(),
|
||||
version: "1.0.0".to_string(),
|
||||
},
|
||||
capabilities: context_server::types::ServerCapabilities {
|
||||
tools: Some(context_server::types::ToolsCapabilities {
|
||||
list_changed: Some(true),
|
||||
}),
|
||||
..Default::default()
|
||||
},
|
||||
meta: None,
|
||||
}
|
||||
})
|
||||
.on_request::<context_server::types::requests::ListTools, _>(move |_params| {
|
||||
let tools = tools.clone();
|
||||
async move {
|
||||
context_server::types::ListToolsResponse {
|
||||
tools,
|
||||
next_cursor: None,
|
||||
meta: None,
|
||||
}
|
||||
}
|
||||
})
|
||||
.on_request::<context_server::types::requests::CallTool, _>(move |params| {
|
||||
let mcp_tool_calls_tx = mcp_tool_calls_tx.clone();
|
||||
async move {
|
||||
let (response_tx, response_rx) = oneshot::channel();
|
||||
mcp_tool_calls_tx
|
||||
.unbounded_send((params, response_tx))
|
||||
.unwrap();
|
||||
response_rx.await.unwrap()
|
||||
}
|
||||
});
|
||||
context_server_store.update(cx, |store, cx| {
|
||||
store.start_server(
|
||||
Arc::new(ContextServer::new(
|
||||
ContextServerId(name.into()),
|
||||
Arc::new(fake_transport),
|
||||
)),
|
||||
cx,
|
||||
);
|
||||
});
|
||||
cx.run_until_parked();
|
||||
mcp_tool_calls_rx
|
||||
}
|
||||
|
||||
@@ -9,15 +9,15 @@ use action_log::ActionLog;
|
||||
use agent::thread::{GitState, ProjectSnapshot, WorktreeSnapshot};
|
||||
use agent_client_protocol as acp;
|
||||
use agent_settings::{
|
||||
AgentProfileId, AgentSettings, CompletionMode, SUMMARIZE_THREAD_DETAILED_PROMPT,
|
||||
SUMMARIZE_THREAD_PROMPT,
|
||||
AgentProfileId, AgentProfileSettings, AgentSettings, CompletionMode,
|
||||
SUMMARIZE_THREAD_DETAILED_PROMPT, SUMMARIZE_THREAD_PROMPT,
|
||||
};
|
||||
use anyhow::{Context as _, Result, anyhow};
|
||||
use assistant_tool::adapt_schema_to_format;
|
||||
use chrono::{DateTime, Utc};
|
||||
use client::{ModelRequestUsage, RequestUsage};
|
||||
use cloud_llm_client::{CompletionIntent, CompletionRequestStatus, UsageLimit};
|
||||
use collections::{HashMap, IndexMap};
|
||||
use collections::{HashMap, HashSet, IndexMap};
|
||||
use fs::Fs;
|
||||
use futures::{
|
||||
FutureExt,
|
||||
@@ -45,17 +45,19 @@ use schemars::{JsonSchema, Schema};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::{Settings, update_settings_file};
|
||||
use smol::stream::StreamExt;
|
||||
use std::fmt::Write;
|
||||
use std::{
|
||||
collections::BTreeMap,
|
||||
ops::RangeInclusive,
|
||||
path::Path,
|
||||
sync::Arc,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use std::{fmt::Write, ops::Range};
|
||||
use util::{ResultExt, markdown::MarkdownCodeBlock};
|
||||
use util::{ResultExt, debug_panic, markdown::MarkdownCodeBlock};
|
||||
use uuid::Uuid;
|
||||
|
||||
const TOOL_CANCELED_MESSAGE: &str = "Tool canceled by user";
|
||||
pub const MAX_TOOL_NAME_LENGTH: usize = 64;
|
||||
|
||||
/// The ID of the user prompt that initiated a request.
|
||||
///
|
||||
@@ -121,7 +123,7 @@ impl Message {
|
||||
match self {
|
||||
Message::User(message) => message.to_markdown(),
|
||||
Message::Agent(message) => message.to_markdown(),
|
||||
Message::Resume => "[resumed after tool use limit was reached]".into(),
|
||||
Message::Resume => "[resume]\n".into(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -186,6 +188,7 @@ impl UserMessage {
|
||||
const OPEN_FILES_TAG: &str = "<files>";
|
||||
const OPEN_DIRECTORIES_TAG: &str = "<directories>";
|
||||
const OPEN_SYMBOLS_TAG: &str = "<symbols>";
|
||||
const OPEN_SELECTIONS_TAG: &str = "<selections>";
|
||||
const OPEN_THREADS_TAG: &str = "<threads>";
|
||||
const OPEN_FETCH_TAG: &str = "<fetched_urls>";
|
||||
const OPEN_RULES_TAG: &str =
|
||||
@@ -194,6 +197,7 @@ impl UserMessage {
|
||||
let mut file_context = OPEN_FILES_TAG.to_string();
|
||||
let mut directory_context = OPEN_DIRECTORIES_TAG.to_string();
|
||||
let mut symbol_context = OPEN_SYMBOLS_TAG.to_string();
|
||||
let mut selection_context = OPEN_SELECTIONS_TAG.to_string();
|
||||
let mut thread_context = OPEN_THREADS_TAG.to_string();
|
||||
let mut fetch_context = OPEN_FETCH_TAG.to_string();
|
||||
let mut rules_context = OPEN_RULES_TAG.to_string();
|
||||
@@ -210,7 +214,7 @@ impl UserMessage {
|
||||
match uri {
|
||||
MentionUri::File { abs_path } => {
|
||||
write!(
|
||||
&mut symbol_context,
|
||||
&mut file_context,
|
||||
"\n{}",
|
||||
MarkdownCodeBlock {
|
||||
tag: &codeblock_tag(abs_path, None),
|
||||
@@ -219,17 +223,19 @@ impl UserMessage {
|
||||
)
|
||||
.ok();
|
||||
}
|
||||
MentionUri::PastedImage => {
|
||||
debug_panic!("pasted image URI should not be used in mention content")
|
||||
}
|
||||
MentionUri::Directory { .. } => {
|
||||
write!(&mut directory_context, "\n{}\n", content).ok();
|
||||
}
|
||||
MentionUri::Symbol {
|
||||
path, line_range, ..
|
||||
}
|
||||
| MentionUri::Selection {
|
||||
path, line_range, ..
|
||||
abs_path: path,
|
||||
line_range,
|
||||
..
|
||||
} => {
|
||||
write!(
|
||||
&mut rules_context,
|
||||
&mut symbol_context,
|
||||
"\n{}",
|
||||
MarkdownCodeBlock {
|
||||
tag: &codeblock_tag(path, Some(line_range)),
|
||||
@@ -238,6 +244,24 @@ impl UserMessage {
|
||||
)
|
||||
.ok();
|
||||
}
|
||||
MentionUri::Selection {
|
||||
abs_path: path,
|
||||
line_range,
|
||||
..
|
||||
} => {
|
||||
write!(
|
||||
&mut selection_context,
|
||||
"\n{}",
|
||||
MarkdownCodeBlock {
|
||||
tag: &codeblock_tag(
|
||||
path.as_deref().unwrap_or("Untitled".as_ref()),
|
||||
Some(line_range)
|
||||
),
|
||||
text: content
|
||||
}
|
||||
)
|
||||
.ok();
|
||||
}
|
||||
MentionUri::Thread { .. } => {
|
||||
write!(&mut thread_context, "\n{}\n", content).ok();
|
||||
}
|
||||
@@ -290,6 +314,13 @@ impl UserMessage {
|
||||
.push(language_model::MessageContent::Text(symbol_context));
|
||||
}
|
||||
|
||||
if selection_context.len() > OPEN_SELECTIONS_TAG.len() {
|
||||
selection_context.push_str("</selections>\n");
|
||||
message
|
||||
.content
|
||||
.push(language_model::MessageContent::Text(selection_context));
|
||||
}
|
||||
|
||||
if thread_context.len() > OPEN_THREADS_TAG.len() {
|
||||
thread_context.push_str("</threads>\n");
|
||||
message
|
||||
@@ -325,7 +356,7 @@ impl UserMessage {
|
||||
}
|
||||
}
|
||||
|
||||
fn codeblock_tag(full_path: &Path, line_range: Option<&Range<u32>>) -> String {
|
||||
fn codeblock_tag(full_path: &Path, line_range: Option<&RangeInclusive<u32>>) -> String {
|
||||
let mut result = String::new();
|
||||
|
||||
if let Some(extension) = full_path.extension().and_then(|ext| ext.to_str()) {
|
||||
@@ -335,10 +366,10 @@ fn codeblock_tag(full_path: &Path, line_range: Option<&Range<u32>>) -> String {
|
||||
let _ = write!(result, "{}", full_path.display());
|
||||
|
||||
if let Some(range) = line_range {
|
||||
if range.start == range.end {
|
||||
let _ = write!(result, ":{}", range.start + 1);
|
||||
if range.start() == range.end() {
|
||||
let _ = write!(result, ":{}", range.start() + 1);
|
||||
} else {
|
||||
let _ = write!(result, ":{}-{}", range.start + 1, range.end + 1);
|
||||
let _ = write!(result, ":{}-{}", range.start() + 1, range.end() + 1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -417,24 +448,33 @@ impl AgentMessage {
|
||||
cache: false,
|
||||
};
|
||||
for chunk in &self.content {
|
||||
let chunk = match chunk {
|
||||
match chunk {
|
||||
AgentMessageContent::Text(text) => {
|
||||
language_model::MessageContent::Text(text.clone())
|
||||
assistant_message
|
||||
.content
|
||||
.push(language_model::MessageContent::Text(text.clone()));
|
||||
}
|
||||
AgentMessageContent::Thinking { text, signature } => {
|
||||
language_model::MessageContent::Thinking {
|
||||
text: text.clone(),
|
||||
signature: signature.clone(),
|
||||
}
|
||||
assistant_message
|
||||
.content
|
||||
.push(language_model::MessageContent::Thinking {
|
||||
text: text.clone(),
|
||||
signature: signature.clone(),
|
||||
});
|
||||
}
|
||||
AgentMessageContent::RedactedThinking(value) => {
|
||||
language_model::MessageContent::RedactedThinking(value.clone())
|
||||
assistant_message.content.push(
|
||||
language_model::MessageContent::RedactedThinking(value.clone()),
|
||||
);
|
||||
}
|
||||
AgentMessageContent::ToolUse(value) => {
|
||||
language_model::MessageContent::ToolUse(value.clone())
|
||||
AgentMessageContent::ToolUse(tool_use) => {
|
||||
if self.tool_results.contains_key(&tool_use.id) {
|
||||
assistant_message
|
||||
.content
|
||||
.push(language_model::MessageContent::ToolUse(tool_use.clone()));
|
||||
}
|
||||
}
|
||||
};
|
||||
assistant_message.content.push(chunk);
|
||||
}
|
||||
|
||||
let mut user_message = LanguageModelRequestMessage {
|
||||
@@ -535,11 +575,22 @@ pub struct Thread {
|
||||
templates: Arc<Templates>,
|
||||
model: Option<Arc<dyn LanguageModel>>,
|
||||
summarization_model: Option<Arc<dyn LanguageModel>>,
|
||||
prompt_capabilities_tx: watch::Sender<acp::PromptCapabilities>,
|
||||
pub(crate) prompt_capabilities_rx: watch::Receiver<acp::PromptCapabilities>,
|
||||
pub(crate) project: Entity<Project>,
|
||||
pub(crate) action_log: Entity<ActionLog>,
|
||||
}
|
||||
|
||||
impl Thread {
|
||||
fn prompt_capabilities(model: Option<&dyn LanguageModel>) -> acp::PromptCapabilities {
|
||||
let image = model.map_or(true, |model| model.supports_images());
|
||||
acp::PromptCapabilities {
|
||||
image,
|
||||
audio: false,
|
||||
embedded_context: true,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new(
|
||||
project: Entity<Project>,
|
||||
project_context: Entity<ProjectContext>,
|
||||
@@ -550,6 +601,8 @@ impl Thread {
|
||||
) -> Self {
|
||||
let profile_id = AgentSettings::get_global(cx).default_profile.clone();
|
||||
let action_log = cx.new(|_cx| ActionLog::new(project.clone()));
|
||||
let (prompt_capabilities_tx, prompt_capabilities_rx) =
|
||||
watch::channel(Self::prompt_capabilities(model.as_deref()));
|
||||
Self {
|
||||
id: acp::SessionId(uuid::Uuid::new_v4().to_string().into()),
|
||||
prompt_id: PromptId::new(),
|
||||
@@ -577,6 +630,8 @@ impl Thread {
|
||||
templates,
|
||||
model,
|
||||
summarization_model: None,
|
||||
prompt_capabilities_tx,
|
||||
prompt_capabilities_rx,
|
||||
project,
|
||||
action_log,
|
||||
}
|
||||
@@ -627,7 +682,20 @@ impl Thread {
|
||||
stream: &ThreadEventStream,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let Some(tool) = self.tools.get(tool_use.name.as_ref()) else {
|
||||
let tool = self.tools.get(tool_use.name.as_ref()).cloned().or_else(|| {
|
||||
self.context_server_registry
|
||||
.read(cx)
|
||||
.servers()
|
||||
.find_map(|(_, tools)| {
|
||||
if let Some(tool) = tools.get(tool_use.name.as_ref()) {
|
||||
Some(tool.clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
let Some(tool) = tool else {
|
||||
stream
|
||||
.0
|
||||
.unbounded_send(Ok(ThreadEvent::ToolCall(acp::ToolCall {
|
||||
@@ -697,6 +765,8 @@ impl Thread {
|
||||
.or_else(|| registry.default_model())
|
||||
.map(|model| model.model)
|
||||
});
|
||||
let (prompt_capabilities_tx, prompt_capabilities_rx) =
|
||||
watch::channel(Self::prompt_capabilities(model.as_deref()));
|
||||
|
||||
Self {
|
||||
id,
|
||||
@@ -726,6 +796,8 @@ impl Thread {
|
||||
project,
|
||||
action_log,
|
||||
updated_at: db_thread.updated_at,
|
||||
prompt_capabilities_tx,
|
||||
prompt_capabilities_rx,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -893,10 +965,12 @@ impl Thread {
|
||||
pub fn set_model(&mut self, model: Arc<dyn LanguageModel>, cx: &mut Context<Self>) {
|
||||
let old_usage = self.latest_token_usage();
|
||||
self.model = Some(model);
|
||||
let new_caps = Self::prompt_capabilities(self.model.as_deref());
|
||||
let new_usage = self.latest_token_usage();
|
||||
if old_usage != new_usage {
|
||||
cx.emit(TokenUsageUpdated(new_usage));
|
||||
}
|
||||
self.prompt_capabilities_tx.send(new_caps).log_err();
|
||||
cx.notify()
|
||||
}
|
||||
|
||||
@@ -1032,15 +1106,10 @@ impl Thread {
|
||||
&mut self,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Result<mpsc::UnboundedReceiver<Result<ThreadEvent>>> {
|
||||
anyhow::ensure!(
|
||||
self.tool_use_limit_reached,
|
||||
"can only resume after tool use limit is reached"
|
||||
);
|
||||
|
||||
self.messages.push(Message::Resume);
|
||||
cx.notify();
|
||||
|
||||
log::info!("Total messages in thread: {}", self.messages.len());
|
||||
log::debug!("Total messages in thread: {}", self.messages.len());
|
||||
self.run_turn(cx)
|
||||
}
|
||||
|
||||
@@ -1058,7 +1127,7 @@ impl Thread {
|
||||
{
|
||||
let model = self.model().context("No language model configured")?;
|
||||
|
||||
log::info!("Thread::send called with model: {:?}", model.name());
|
||||
log::info!("Thread::send called with model: {}", model.name().0);
|
||||
self.advance_prompt_id();
|
||||
|
||||
let content = content.into_iter().map(Into::into).collect::<Vec<_>>();
|
||||
@@ -1068,7 +1137,7 @@ impl Thread {
|
||||
.push(Message::User(UserMessage { id, content }));
|
||||
cx.notify();
|
||||
|
||||
log::info!("Total messages in thread: {}", self.messages.len());
|
||||
log::debug!("Total messages in thread: {}", self.messages.len());
|
||||
self.run_turn(cx)
|
||||
}
|
||||
|
||||
@@ -1079,6 +1148,10 @@ impl Thread {
|
||||
self.cancel(cx);
|
||||
|
||||
let model = self.model.clone().context("No language model configured")?;
|
||||
let profile = AgentSettings::get_global(cx)
|
||||
.profiles
|
||||
.get(&self.profile_id)
|
||||
.context("Profile not found")?;
|
||||
let (events_tx, events_rx) = mpsc::unbounded::<Result<ThreadEvent>>();
|
||||
let event_stream = ThreadEventStream(events_tx);
|
||||
let message_ix = self.messages.len().saturating_sub(1);
|
||||
@@ -1086,45 +1159,16 @@ impl Thread {
|
||||
self.summary = None;
|
||||
self.running_turn = Some(RunningTurn {
|
||||
event_stream: event_stream.clone(),
|
||||
tools: self.enabled_tools(profile, &model, cx),
|
||||
_task: cx.spawn(async move |this, cx| {
|
||||
log::info!("Starting agent turn execution");
|
||||
log::debug!("Starting agent turn execution");
|
||||
|
||||
let turn_result: Result<()> = async {
|
||||
let mut intent = CompletionIntent::UserPrompt;
|
||||
loop {
|
||||
Self::stream_completion(&this, &model, intent, &event_stream, cx).await?;
|
||||
|
||||
let mut end_turn = true;
|
||||
this.update(cx, |this, cx| {
|
||||
// Generate title if needed.
|
||||
if this.title.is_none() && this.pending_title_generation.is_none() {
|
||||
this.generate_title(cx);
|
||||
}
|
||||
|
||||
// End the turn if the model didn't use tools.
|
||||
let message = this.pending_message.as_ref();
|
||||
end_turn =
|
||||
message.map_or(true, |message| message.tool_results.is_empty());
|
||||
this.flush_pending_message(cx);
|
||||
})?;
|
||||
|
||||
if this.read_with(cx, |this, _| this.tool_use_limit_reached)? {
|
||||
log::info!("Tool use limit reached, completing turn");
|
||||
return Err(language_model::ToolUseLimitReachedError.into());
|
||||
} else if end_turn {
|
||||
log::info!("No tool uses found, completing turn");
|
||||
return Ok(());
|
||||
} else {
|
||||
intent = CompletionIntent::ToolResults;
|
||||
}
|
||||
}
|
||||
}
|
||||
.await;
|
||||
let turn_result = Self::run_turn_internal(&this, model, &event_stream, cx).await;
|
||||
_ = this.update(cx, |this, cx| this.flush_pending_message(cx));
|
||||
|
||||
match turn_result {
|
||||
Ok(()) => {
|
||||
log::info!("Turn execution completed");
|
||||
log::debug!("Turn execution completed");
|
||||
event_stream.send_stop(acp::StopReason::EndTurn);
|
||||
}
|
||||
Err(error) => {
|
||||
@@ -1150,20 +1194,18 @@ impl Thread {
|
||||
Ok(events_rx)
|
||||
}
|
||||
|
||||
async fn stream_completion(
|
||||
async fn run_turn_internal(
|
||||
this: &WeakEntity<Self>,
|
||||
model: &Arc<dyn LanguageModel>,
|
||||
completion_intent: CompletionIntent,
|
||||
model: Arc<dyn LanguageModel>,
|
||||
event_stream: &ThreadEventStream,
|
||||
cx: &mut AsyncApp,
|
||||
) -> Result<()> {
|
||||
log::debug!("Stream completion started successfully");
|
||||
let request = this.update(cx, |this, cx| {
|
||||
this.build_completion_request(completion_intent, cx)
|
||||
})??;
|
||||
let mut attempt = 0;
|
||||
let mut intent = CompletionIntent::UserPrompt;
|
||||
loop {
|
||||
let request =
|
||||
this.update(cx, |this, cx| this.build_completion_request(intent, cx))??;
|
||||
|
||||
let mut attempt = None;
|
||||
'retry: loop {
|
||||
telemetry::event!(
|
||||
"Agent Thread Completion",
|
||||
thread_id = this.read_with(cx, |this, _| this.id.to_string())?,
|
||||
@@ -1173,75 +1215,31 @@ impl Thread {
|
||||
attempt
|
||||
);
|
||||
|
||||
log::info!(
|
||||
"Calling model.stream_completion, attempt {}",
|
||||
attempt.unwrap_or(0)
|
||||
);
|
||||
log::debug!("Calling model.stream_completion, attempt {}", attempt);
|
||||
let mut events = model
|
||||
.stream_completion(request.clone(), cx)
|
||||
.stream_completion(request, cx)
|
||||
.await
|
||||
.map_err(|error| anyhow!(error))?;
|
||||
let mut tool_results = FuturesUnordered::new();
|
||||
|
||||
let mut error = None;
|
||||
while let Some(event) = events.next().await {
|
||||
log::trace!("Received completion event: {:?}", event);
|
||||
match event {
|
||||
Ok(event) => {
|
||||
log::trace!("Received completion event: {:?}", event);
|
||||
tool_results.extend(this.update(cx, |this, cx| {
|
||||
this.handle_streamed_completion_event(event, event_stream, cx)
|
||||
this.handle_completion_event(event, event_stream, cx)
|
||||
})??);
|
||||
}
|
||||
Err(error) => {
|
||||
let completion_mode =
|
||||
this.read_with(cx, |thread, _cx| thread.completion_mode())?;
|
||||
if completion_mode == CompletionMode::Normal {
|
||||
return Err(anyhow!(error))?;
|
||||
}
|
||||
|
||||
let Some(strategy) = Self::retry_strategy_for(&error) else {
|
||||
return Err(anyhow!(error))?;
|
||||
};
|
||||
|
||||
let max_attempts = match &strategy {
|
||||
RetryStrategy::ExponentialBackoff { max_attempts, .. } => *max_attempts,
|
||||
RetryStrategy::Fixed { max_attempts, .. } => *max_attempts,
|
||||
};
|
||||
|
||||
let attempt = attempt.get_or_insert(0u8);
|
||||
|
||||
*attempt += 1;
|
||||
|
||||
let attempt = *attempt;
|
||||
if attempt > max_attempts {
|
||||
return Err(anyhow!(error))?;
|
||||
}
|
||||
|
||||
let delay = match &strategy {
|
||||
RetryStrategy::ExponentialBackoff { initial_delay, .. } => {
|
||||
let delay_secs =
|
||||
initial_delay.as_secs() * 2u64.pow((attempt - 1) as u32);
|
||||
Duration::from_secs(delay_secs)
|
||||
}
|
||||
RetryStrategy::Fixed { delay, .. } => *delay,
|
||||
};
|
||||
log::debug!("Retry attempt {attempt} with delay {delay:?}");
|
||||
|
||||
event_stream.send_retry(acp_thread::RetryStatus {
|
||||
last_error: error.to_string().into(),
|
||||
attempt: attempt as usize,
|
||||
max_attempts: max_attempts as usize,
|
||||
started_at: Instant::now(),
|
||||
duration: delay,
|
||||
});
|
||||
|
||||
cx.background_executor().timer(delay).await;
|
||||
continue 'retry;
|
||||
Err(err) => {
|
||||
error = Some(err);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let end_turn = tool_results.is_empty();
|
||||
while let Some(tool_result) = tool_results.next().await {
|
||||
log::info!("Tool finished {:?}", tool_result);
|
||||
log::debug!("Tool finished {:?}", tool_result);
|
||||
|
||||
event_stream.update_tool_call_fields(
|
||||
&tool_result.tool_use_id,
|
||||
@@ -1262,31 +1260,83 @@ impl Thread {
|
||||
})?;
|
||||
}
|
||||
|
||||
return Ok(());
|
||||
this.update(cx, |this, cx| {
|
||||
this.flush_pending_message(cx);
|
||||
if this.title.is_none() && this.pending_title_generation.is_none() {
|
||||
this.generate_title(cx);
|
||||
}
|
||||
})?;
|
||||
|
||||
if let Some(error) = error {
|
||||
attempt += 1;
|
||||
let retry =
|
||||
this.update(cx, |this, _| this.handle_completion_error(error, attempt))??;
|
||||
let timer = cx.background_executor().timer(retry.duration);
|
||||
event_stream.send_retry(retry);
|
||||
timer.await;
|
||||
this.update(cx, |this, _cx| {
|
||||
if let Some(Message::Agent(message)) = this.messages.last() {
|
||||
if message.tool_results.is_empty() {
|
||||
intent = CompletionIntent::UserPrompt;
|
||||
this.messages.push(Message::Resume);
|
||||
}
|
||||
}
|
||||
})?;
|
||||
} else if this.read_with(cx, |this, _| this.tool_use_limit_reached)? {
|
||||
return Err(language_model::ToolUseLimitReachedError.into());
|
||||
} else if end_turn {
|
||||
return Ok(());
|
||||
} else {
|
||||
intent = CompletionIntent::ToolResults;
|
||||
attempt = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn build_system_message(&self, cx: &App) -> LanguageModelRequestMessage {
|
||||
log::debug!("Building system message");
|
||||
let prompt = SystemPromptTemplate {
|
||||
project: self.project_context.read(cx),
|
||||
available_tools: self.tools.keys().cloned().collect(),
|
||||
fn handle_completion_error(
|
||||
&mut self,
|
||||
error: LanguageModelCompletionError,
|
||||
attempt: u8,
|
||||
) -> Result<acp_thread::RetryStatus> {
|
||||
if self.completion_mode == CompletionMode::Normal {
|
||||
return Err(anyhow!(error));
|
||||
}
|
||||
.render(&self.templates)
|
||||
.context("failed to build system prompt")
|
||||
.expect("Invalid template");
|
||||
log::debug!("System message built");
|
||||
LanguageModelRequestMessage {
|
||||
role: Role::System,
|
||||
content: vec![prompt.into()],
|
||||
cache: true,
|
||||
|
||||
let Some(strategy) = Self::retry_strategy_for(&error) else {
|
||||
return Err(anyhow!(error));
|
||||
};
|
||||
|
||||
let max_attempts = match &strategy {
|
||||
RetryStrategy::ExponentialBackoff { max_attempts, .. } => *max_attempts,
|
||||
RetryStrategy::Fixed { max_attempts, .. } => *max_attempts,
|
||||
};
|
||||
|
||||
if attempt > max_attempts {
|
||||
return Err(anyhow!(error));
|
||||
}
|
||||
|
||||
let delay = match &strategy {
|
||||
RetryStrategy::ExponentialBackoff { initial_delay, .. } => {
|
||||
let delay_secs = initial_delay.as_secs() * 2u64.pow((attempt - 1) as u32);
|
||||
Duration::from_secs(delay_secs)
|
||||
}
|
||||
RetryStrategy::Fixed { delay, .. } => *delay,
|
||||
};
|
||||
log::debug!("Retry attempt {attempt} with delay {delay:?}");
|
||||
|
||||
Ok(acp_thread::RetryStatus {
|
||||
last_error: error.to_string().into(),
|
||||
attempt: attempt as usize,
|
||||
max_attempts: max_attempts as usize,
|
||||
started_at: Instant::now(),
|
||||
duration: delay,
|
||||
})
|
||||
}
|
||||
|
||||
/// A helper method that's called on every streamed completion event.
|
||||
/// Returns an optional tool result task, which the main agentic loop will
|
||||
/// send back to the model when it resolves.
|
||||
fn handle_streamed_completion_event(
|
||||
fn handle_completion_event(
|
||||
&mut self,
|
||||
event: LanguageModelCompletionEvent,
|
||||
event_stream: &ThreadEventStream,
|
||||
@@ -1417,7 +1467,7 @@ impl Thread {
|
||||
) -> Option<Task<LanguageModelToolResult>> {
|
||||
cx.notify();
|
||||
|
||||
let tool = self.tools.get(tool_use.name.as_ref()).cloned();
|
||||
let tool = self.tool(tool_use.name.as_ref());
|
||||
let mut title = SharedString::from(&tool_use.name);
|
||||
let mut kind = acp::ToolKind::Other;
|
||||
if let Some(tool) = tool.as_ref() {
|
||||
@@ -1481,7 +1531,7 @@ impl Thread {
|
||||
});
|
||||
let supports_images = self.model().is_some_and(|model| model.supports_images());
|
||||
let tool_result = tool.run(tool_use.input, tool_event_stream, cx);
|
||||
log::info!("Running tool {}", tool_use.name);
|
||||
log::debug!("Running tool {}", tool_use.name);
|
||||
Some(cx.foreground_executor().spawn(async move {
|
||||
let tool_result = tool_result.await.and_then(|output| {
|
||||
if let LanguageModelToolResultContent::Image(_) = &output.llm_output
|
||||
@@ -1593,7 +1643,7 @@ impl Thread {
|
||||
summary.extend(lines.next());
|
||||
}
|
||||
|
||||
log::info!("Setting summary: {}", summary);
|
||||
log::debug!("Setting summary: {}", summary);
|
||||
let summary = SharedString::from(summary);
|
||||
|
||||
this.update(cx, |this, cx| {
|
||||
@@ -1610,7 +1660,7 @@ impl Thread {
|
||||
return;
|
||||
};
|
||||
|
||||
log::info!(
|
||||
log::debug!(
|
||||
"Generating title with model: {:?}",
|
||||
self.summarization_model.as_ref().map(|model| model.name())
|
||||
);
|
||||
@@ -1696,6 +1746,10 @@ impl Thread {
|
||||
return;
|
||||
};
|
||||
|
||||
if message.content.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
for content in &message.content {
|
||||
let AgentMessageContent::ToolUse(tool_use) = content else {
|
||||
continue;
|
||||
@@ -1724,34 +1778,32 @@ impl Thread {
|
||||
pub(crate) fn build_completion_request(
|
||||
&self,
|
||||
completion_intent: CompletionIntent,
|
||||
cx: &mut App,
|
||||
cx: &App,
|
||||
) -> Result<LanguageModelRequest> {
|
||||
let model = self.model().context("No language model configured")?;
|
||||
let tools = if let Some(turn) = self.running_turn.as_ref() {
|
||||
turn.tools
|
||||
.iter()
|
||||
.filter_map(|(tool_name, tool)| {
|
||||
log::trace!("Including tool: {}", tool_name);
|
||||
Some(LanguageModelRequestTool {
|
||||
name: tool_name.to_string(),
|
||||
description: tool.description().to_string(),
|
||||
input_schema: tool.input_schema(model.tool_input_format()).log_err()?,
|
||||
})
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
|
||||
log::debug!("Building completion request");
|
||||
log::debug!("Completion intent: {:?}", completion_intent);
|
||||
log::debug!("Completion mode: {:?}", self.completion_mode);
|
||||
|
||||
let messages = self.build_request_messages(cx);
|
||||
log::info!("Request will include {} messages", messages.len());
|
||||
|
||||
let tools = if let Some(tools) = self.tools(cx).log_err() {
|
||||
tools
|
||||
.filter_map(|tool| {
|
||||
let tool_name = tool.name().to_string();
|
||||
log::trace!("Including tool: {}", tool_name);
|
||||
Some(LanguageModelRequestTool {
|
||||
name: tool_name,
|
||||
description: tool.description().to_string(),
|
||||
input_schema: tool.input_schema(model.tool_input_format()).log_err()?,
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
|
||||
log::info!("Request includes {} tools", tools.len());
|
||||
log::debug!("Request will include {} messages", messages.len());
|
||||
log::debug!("Request includes {} tools", tools.len());
|
||||
|
||||
let request = LanguageModelRequest {
|
||||
thread_id: Some(self.id.to_string()),
|
||||
@@ -1770,37 +1822,76 @@ impl Thread {
|
||||
Ok(request)
|
||||
}
|
||||
|
||||
fn tools<'a>(&'a self, cx: &'a App) -> Result<impl Iterator<Item = &'a Arc<dyn AnyAgentTool>>> {
|
||||
let model = self.model().context("No language model configured")?;
|
||||
fn enabled_tools(
|
||||
&self,
|
||||
profile: &AgentProfileSettings,
|
||||
model: &Arc<dyn LanguageModel>,
|
||||
cx: &App,
|
||||
) -> BTreeMap<SharedString, Arc<dyn AnyAgentTool>> {
|
||||
fn truncate(tool_name: &SharedString) -> SharedString {
|
||||
if tool_name.len() > MAX_TOOL_NAME_LENGTH {
|
||||
let mut truncated = tool_name.to_string();
|
||||
truncated.truncate(MAX_TOOL_NAME_LENGTH);
|
||||
truncated.into()
|
||||
} else {
|
||||
tool_name.clone()
|
||||
}
|
||||
}
|
||||
|
||||
let profile = AgentSettings::get_global(cx)
|
||||
.profiles
|
||||
.get(&self.profile_id)
|
||||
.context("profile not found")?;
|
||||
let provider_id = model.provider_id();
|
||||
|
||||
Ok(self
|
||||
let mut tools = self
|
||||
.tools
|
||||
.iter()
|
||||
.filter(move |(_, tool)| tool.supported_provider(&provider_id))
|
||||
.filter_map(|(tool_name, tool)| {
|
||||
if profile.is_tool_enabled(tool_name) {
|
||||
Some(tool)
|
||||
if tool.supported_provider(&model.provider_id())
|
||||
&& profile.is_tool_enabled(tool_name)
|
||||
{
|
||||
Some((truncate(tool_name), tool.clone()))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.chain(self.context_server_registry.read(cx).servers().flat_map(
|
||||
|(server_id, tools)| {
|
||||
tools.iter().filter_map(|(tool_name, tool)| {
|
||||
if profile.is_context_server_tool_enabled(&server_id.0, tool_name) {
|
||||
Some(tool)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
},
|
||||
)))
|
||||
.collect::<BTreeMap<_, _>>();
|
||||
|
||||
let mut context_server_tools = Vec::new();
|
||||
let mut seen_tools = tools.keys().cloned().collect::<HashSet<_>>();
|
||||
let mut duplicate_tool_names = HashSet::default();
|
||||
for (server_id, server_tools) in self.context_server_registry.read(cx).servers() {
|
||||
for (tool_name, tool) in server_tools {
|
||||
if profile.is_context_server_tool_enabled(&server_id.0, &tool_name) {
|
||||
let tool_name = truncate(tool_name);
|
||||
if !seen_tools.insert(tool_name.clone()) {
|
||||
duplicate_tool_names.insert(tool_name.clone());
|
||||
}
|
||||
context_server_tools.push((server_id.clone(), tool_name, tool.clone()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// When there are duplicate tool names, disambiguate by prefixing them
|
||||
// with the server ID. In the rare case there isn't enough space for the
|
||||
// disambiguated tool name, keep only the last tool with this name.
|
||||
for (server_id, tool_name, tool) in context_server_tools {
|
||||
if duplicate_tool_names.contains(&tool_name) {
|
||||
let available = MAX_TOOL_NAME_LENGTH.saturating_sub(tool_name.len());
|
||||
if available >= 2 {
|
||||
let mut disambiguated = server_id.0.to_string();
|
||||
disambiguated.truncate(available - 1);
|
||||
disambiguated.push('_');
|
||||
disambiguated.push_str(&tool_name);
|
||||
tools.insert(disambiguated.into(), tool.clone());
|
||||
} else {
|
||||
tools.insert(tool_name, tool.clone());
|
||||
}
|
||||
} else {
|
||||
tools.insert(tool_name, tool.clone());
|
||||
}
|
||||
}
|
||||
|
||||
tools
|
||||
}
|
||||
|
||||
fn tool(&self, name: &str) -> Option<Arc<dyn AnyAgentTool>> {
|
||||
self.running_turn.as_ref()?.tools.get(name).cloned()
|
||||
}
|
||||
|
||||
fn build_request_messages(&self, cx: &App) -> Vec<LanguageModelRequestMessage> {
|
||||
@@ -1808,21 +1899,29 @@ impl Thread {
|
||||
"Building request messages from {} thread messages",
|
||||
self.messages.len()
|
||||
);
|
||||
let mut messages = vec![self.build_system_message(cx)];
|
||||
|
||||
let system_prompt = SystemPromptTemplate {
|
||||
project: self.project_context.read(cx),
|
||||
available_tools: self.tools.keys().cloned().collect(),
|
||||
}
|
||||
.render(&self.templates)
|
||||
.context("failed to build system prompt")
|
||||
.expect("Invalid template");
|
||||
let mut messages = vec![LanguageModelRequestMessage {
|
||||
role: Role::System,
|
||||
content: vec![system_prompt.into()],
|
||||
cache: false,
|
||||
}];
|
||||
for message in &self.messages {
|
||||
messages.extend(message.to_request());
|
||||
}
|
||||
|
||||
if let Some(message) = self.pending_message.as_ref() {
|
||||
messages.extend(message.to_request());
|
||||
if let Some(last_message) = messages.last_mut() {
|
||||
last_message.cache = true;
|
||||
}
|
||||
|
||||
if let Some(last_user_message) = messages
|
||||
.iter_mut()
|
||||
.rev()
|
||||
.find(|message| message.role == Role::User)
|
||||
{
|
||||
last_user_message.cache = true;
|
||||
if let Some(message) = self.pending_message.as_ref() {
|
||||
messages.extend(message.to_request());
|
||||
}
|
||||
|
||||
messages
|
||||
@@ -1965,6 +2064,8 @@ struct RunningTurn {
|
||||
/// The current event stream for the running turn. Used to report a final
|
||||
/// cancellation event if we cancel the turn.
|
||||
event_stream: ThreadEventStream,
|
||||
/// The tools that were enabled for this turn.
|
||||
tools: BTreeMap<SharedString, Arc<dyn AnyAgentTool>>,
|
||||
}
|
||||
|
||||
impl RunningTurn {
|
||||
|
||||
@@ -136,12 +136,17 @@ impl AgentTool for FetchTool {
|
||||
fn run(
|
||||
self: Arc<Self>,
|
||||
input: Self::Input,
|
||||
_event_stream: ToolCallEventStream,
|
||||
event_stream: ToolCallEventStream,
|
||||
cx: &mut App,
|
||||
) -> Task<Result<Self::Output>> {
|
||||
let authorize = event_stream.authorize(input.url.clone(), cx);
|
||||
|
||||
let text = cx.background_spawn({
|
||||
let http_client = self.http_client.clone();
|
||||
async move { Self::build_message(http_client, &input.url).await }
|
||||
async move {
|
||||
authorize.await?;
|
||||
Self::build_message(http_client, &input.url).await
|
||||
}
|
||||
});
|
||||
|
||||
cx.foreground_executor().spawn(async move {
|
||||
|
||||
@@ -165,16 +165,17 @@ fn search_paths(glob: &str, project: Entity<Project>, cx: &mut App) -> Task<Resu
|
||||
.collect();
|
||||
|
||||
cx.background_spawn(async move {
|
||||
Ok(snapshots
|
||||
.iter()
|
||||
.flat_map(|snapshot| {
|
||||
let mut results = Vec::new();
|
||||
for snapshot in snapshots {
|
||||
for entry in snapshot.entries(false, 0) {
|
||||
let root_name = PathBuf::from(snapshot.root_name());
|
||||
snapshot
|
||||
.entries(false, 0)
|
||||
.map(move |entry| root_name.join(&entry.path))
|
||||
.filter(|path| path_matcher.is_match(&path))
|
||||
})
|
||||
.collect())
|
||||
if path_matcher.is_match(root_name.join(&entry.path)) {
|
||||
results.push(snapshot.abs_path().join(entry.path.as_ref()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(results)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -215,8 +216,8 @@ mod test {
|
||||
assert_eq!(
|
||||
matches,
|
||||
&[
|
||||
PathBuf::from("root/apple/banana/carrot"),
|
||||
PathBuf::from("root/apple/bandana/carbonara")
|
||||
PathBuf::from(path!("/root/apple/banana/carrot")),
|
||||
PathBuf::from(path!("/root/apple/bandana/carbonara"))
|
||||
]
|
||||
);
|
||||
|
||||
@@ -227,8 +228,8 @@ mod test {
|
||||
assert_eq!(
|
||||
matches,
|
||||
&[
|
||||
PathBuf::from("root/apple/banana/carrot"),
|
||||
PathBuf::from("root/apple/bandana/carbonara")
|
||||
PathBuf::from(path!("/root/apple/banana/carrot")),
|
||||
PathBuf::from(path!("/root/apple/bandana/carbonara"))
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ use project::{AgentLocation, ImageItem, Project, WorktreeSettings, image_store};
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::Settings;
|
||||
use std::sync::Arc;
|
||||
use std::{path::Path, sync::Arc};
|
||||
|
||||
use crate::{AgentTool, ToolCallEventStream};
|
||||
|
||||
@@ -68,27 +68,12 @@ impl AgentTool for ReadFileTool {
|
||||
}
|
||||
|
||||
fn initial_title(&self, input: Result<Self::Input, serde_json::Value>) -> SharedString {
|
||||
if let Ok(input) = input {
|
||||
let path = &input.path;
|
||||
match (input.start_line, input.end_line) {
|
||||
(Some(start), Some(end)) => {
|
||||
format!(
|
||||
"[Read file `{}` (lines {}-{})](@selection:{}:({}-{}))",
|
||||
path, start, end, path, start, end
|
||||
)
|
||||
}
|
||||
(Some(start), None) => {
|
||||
format!(
|
||||
"[Read file `{}` (from line {})](@selection:{}:({}-{}))",
|
||||
path, start, path, start, start
|
||||
)
|
||||
}
|
||||
_ => format!("[Read file `{}`](@file:{})", path, path),
|
||||
}
|
||||
.into()
|
||||
} else {
|
||||
"Read file".into()
|
||||
}
|
||||
input
|
||||
.ok()
|
||||
.as_ref()
|
||||
.and_then(|input| Path::new(&input.path).file_name())
|
||||
.map(|file_name| file_name.to_string_lossy().to_string().into())
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
fn run(
|
||||
|
||||
@@ -17,11 +17,11 @@ path = "src/agent_servers.rs"
|
||||
doctest = false
|
||||
|
||||
[dependencies]
|
||||
acp_tools.workspace = true
|
||||
acp_thread.workspace = true
|
||||
action_log.workspace = true
|
||||
agent-client-protocol.workspace = true
|
||||
agent_settings.workspace = true
|
||||
agentic-coding-protocol.workspace = true
|
||||
anyhow.workspace = true
|
||||
client = { workspace = true, optional = true }
|
||||
collections.workspace = true
|
||||
|
||||
@@ -1,34 +1,390 @@
|
||||
use std::{path::Path, rc::Rc};
|
||||
|
||||
use crate::AgentServerCommand;
|
||||
use acp_thread::AgentConnection;
|
||||
use anyhow::Result;
|
||||
use gpui::AsyncApp;
|
||||
use acp_tools::AcpConnectionRegistry;
|
||||
use action_log::ActionLog;
|
||||
use agent_client_protocol::{self as acp, Agent as _, ErrorCode};
|
||||
use anyhow::anyhow;
|
||||
use collections::HashMap;
|
||||
use futures::AsyncBufReadExt as _;
|
||||
use futures::channel::oneshot;
|
||||
use futures::io::BufReader;
|
||||
use project::Project;
|
||||
use serde::Deserialize;
|
||||
use std::{any::Any, cell::RefCell};
|
||||
use std::{path::Path, rc::Rc};
|
||||
use thiserror::Error;
|
||||
|
||||
mod v0;
|
||||
mod v1;
|
||||
use anyhow::{Context as _, Result};
|
||||
use gpui::{App, AppContext as _, AsyncApp, Entity, SharedString, Task, WeakEntity};
|
||||
|
||||
use acp_thread::{AcpThread, AuthRequired, LoadError};
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
#[error("Unsupported version")]
|
||||
pub struct UnsupportedVersion;
|
||||
|
||||
pub struct AcpConnection {
|
||||
server_name: SharedString,
|
||||
connection: Rc<acp::ClientSideConnection>,
|
||||
sessions: Rc<RefCell<HashMap<acp::SessionId, AcpSession>>>,
|
||||
auth_methods: Vec<acp::AuthMethod>,
|
||||
prompt_capabilities: acp::PromptCapabilities,
|
||||
_io_task: Task<Result<()>>,
|
||||
}
|
||||
|
||||
pub struct AcpSession {
|
||||
thread: WeakEntity<AcpThread>,
|
||||
suppress_abort_err: bool,
|
||||
}
|
||||
|
||||
pub async fn connect(
|
||||
server_name: &'static str,
|
||||
server_name: SharedString,
|
||||
command: AgentServerCommand,
|
||||
root_dir: &Path,
|
||||
cx: &mut AsyncApp,
|
||||
) -> Result<Rc<dyn AgentConnection>> {
|
||||
let conn = v1::AcpConnection::stdio(server_name, command.clone(), root_dir, cx).await;
|
||||
let conn = AcpConnection::stdio(server_name, command.clone(), root_dir, cx).await?;
|
||||
Ok(Rc::new(conn) as _)
|
||||
}
|
||||
|
||||
match conn {
|
||||
Ok(conn) => Ok(Rc::new(conn) as _),
|
||||
Err(err) if err.is::<UnsupportedVersion>() => {
|
||||
// Consider re-using initialize response and subprocess when adding another version here
|
||||
let conn: Rc<dyn AgentConnection> =
|
||||
Rc::new(v0::AcpConnection::stdio(server_name, command, root_dir, cx).await?);
|
||||
Ok(conn)
|
||||
const MINIMUM_SUPPORTED_VERSION: acp::ProtocolVersion = acp::V1;
|
||||
|
||||
impl AcpConnection {
|
||||
pub async fn stdio(
|
||||
server_name: SharedString,
|
||||
command: AgentServerCommand,
|
||||
root_dir: &Path,
|
||||
cx: &mut AsyncApp,
|
||||
) -> Result<Self> {
|
||||
let mut child = util::command::new_smol_command(&command.path)
|
||||
.args(command.args.iter().map(|arg| arg.as_str()))
|
||||
.envs(command.env.iter().flatten())
|
||||
.current_dir(root_dir)
|
||||
.stdin(std::process::Stdio::piped())
|
||||
.stdout(std::process::Stdio::piped())
|
||||
.stderr(std::process::Stdio::piped())
|
||||
.kill_on_drop(true)
|
||||
.spawn()?;
|
||||
|
||||
let stdout = child.stdout.take().context("Failed to take stdout")?;
|
||||
let stdin = child.stdin.take().context("Failed to take stdin")?;
|
||||
let stderr = child.stderr.take().context("Failed to take stderr")?;
|
||||
log::trace!("Spawned (pid: {})", child.id());
|
||||
|
||||
let sessions = Rc::new(RefCell::new(HashMap::default()));
|
||||
|
||||
let client = ClientDelegate {
|
||||
sessions: sessions.clone(),
|
||||
cx: cx.clone(),
|
||||
};
|
||||
let (connection, io_task) = acp::ClientSideConnection::new(client, stdin, stdout, {
|
||||
let foreground_executor = cx.foreground_executor().clone();
|
||||
move |fut| {
|
||||
foreground_executor.spawn(fut).detach();
|
||||
}
|
||||
});
|
||||
|
||||
let io_task = cx.background_spawn(io_task);
|
||||
|
||||
cx.background_spawn(async move {
|
||||
let mut stderr = BufReader::new(stderr);
|
||||
let mut line = String::new();
|
||||
while let Ok(n) = stderr.read_line(&mut line).await
|
||||
&& n > 0
|
||||
{
|
||||
log::warn!("agent stderr: {}", &line);
|
||||
line.clear();
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
|
||||
cx.spawn({
|
||||
let sessions = sessions.clone();
|
||||
async move |cx| {
|
||||
let status = child.status().await?;
|
||||
|
||||
for session in sessions.borrow().values() {
|
||||
session
|
||||
.thread
|
||||
.update(cx, |thread, cx| {
|
||||
thread.emit_load_error(LoadError::Exited { status }, cx)
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
|
||||
anyhow::Ok(())
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
|
||||
let connection = Rc::new(connection);
|
||||
|
||||
cx.update(|cx| {
|
||||
AcpConnectionRegistry::default_global(cx).update(cx, |registry, cx| {
|
||||
registry.set_active_connection(server_name.clone(), &connection, cx)
|
||||
});
|
||||
})?;
|
||||
|
||||
let response = connection
|
||||
.initialize(acp::InitializeRequest {
|
||||
protocol_version: acp::VERSION,
|
||||
client_capabilities: acp::ClientCapabilities {
|
||||
fs: acp::FileSystemCapability {
|
||||
read_text_file: true,
|
||||
write_text_file: true,
|
||||
},
|
||||
},
|
||||
})
|
||||
.await?;
|
||||
|
||||
if response.protocol_version < MINIMUM_SUPPORTED_VERSION {
|
||||
return Err(UnsupportedVersion.into());
|
||||
}
|
||||
Err(err) => Err(err),
|
||||
|
||||
Ok(Self {
|
||||
auth_methods: response.auth_methods,
|
||||
connection,
|
||||
server_name,
|
||||
sessions,
|
||||
prompt_capabilities: response.agent_capabilities.prompt_capabilities,
|
||||
_io_task: io_task,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl AgentConnection for AcpConnection {
|
||||
fn new_thread(
|
||||
self: Rc<Self>,
|
||||
project: Entity<Project>,
|
||||
cwd: &Path,
|
||||
cx: &mut App,
|
||||
) -> Task<Result<Entity<AcpThread>>> {
|
||||
let conn = self.connection.clone();
|
||||
let sessions = self.sessions.clone();
|
||||
let cwd = cwd.to_path_buf();
|
||||
cx.spawn(async move |cx| {
|
||||
let response = conn
|
||||
.new_session(acp::NewSessionRequest {
|
||||
mcp_servers: vec![],
|
||||
cwd,
|
||||
})
|
||||
.await
|
||||
.map_err(|err| {
|
||||
if err.code == acp::ErrorCode::AUTH_REQUIRED.code {
|
||||
let mut error = AuthRequired::new();
|
||||
|
||||
if err.message != acp::ErrorCode::AUTH_REQUIRED.message {
|
||||
error = error.with_description(err.message);
|
||||
}
|
||||
|
||||
anyhow!(error)
|
||||
} else {
|
||||
anyhow!(err)
|
||||
}
|
||||
})?;
|
||||
|
||||
let session_id = response.session_id;
|
||||
let action_log = cx.new(|_| ActionLog::new(project.clone()))?;
|
||||
let thread = cx.new(|cx| {
|
||||
AcpThread::new(
|
||||
self.server_name.clone(),
|
||||
self.clone(),
|
||||
project,
|
||||
action_log,
|
||||
session_id.clone(),
|
||||
// ACP doesn't currently support per-session prompt capabilities or changing capabilities dynamically.
|
||||
watch::Receiver::constant(self.prompt_capabilities),
|
||||
cx,
|
||||
)
|
||||
})?;
|
||||
|
||||
let session = AcpSession {
|
||||
thread: thread.downgrade(),
|
||||
suppress_abort_err: false,
|
||||
};
|
||||
sessions.borrow_mut().insert(session_id, session);
|
||||
|
||||
Ok(thread)
|
||||
})
|
||||
}
|
||||
|
||||
fn auth_methods(&self) -> &[acp::AuthMethod] {
|
||||
&self.auth_methods
|
||||
}
|
||||
|
||||
fn authenticate(&self, method_id: acp::AuthMethodId, cx: &mut App) -> Task<Result<()>> {
|
||||
let conn = self.connection.clone();
|
||||
cx.foreground_executor().spawn(async move {
|
||||
let result = conn
|
||||
.authenticate(acp::AuthenticateRequest {
|
||||
method_id: method_id.clone(),
|
||||
})
|
||||
.await?;
|
||||
|
||||
Ok(result)
|
||||
})
|
||||
}
|
||||
|
||||
fn prompt(
|
||||
&self,
|
||||
_id: Option<acp_thread::UserMessageId>,
|
||||
params: acp::PromptRequest,
|
||||
cx: &mut App,
|
||||
) -> Task<Result<acp::PromptResponse>> {
|
||||
let conn = self.connection.clone();
|
||||
let sessions = self.sessions.clone();
|
||||
let session_id = params.session_id.clone();
|
||||
cx.foreground_executor().spawn(async move {
|
||||
let result = conn.prompt(params).await;
|
||||
|
||||
let mut suppress_abort_err = false;
|
||||
|
||||
if let Some(session) = sessions.borrow_mut().get_mut(&session_id) {
|
||||
suppress_abort_err = session.suppress_abort_err;
|
||||
session.suppress_abort_err = false;
|
||||
}
|
||||
|
||||
match result {
|
||||
Ok(response) => Ok(response),
|
||||
Err(err) => {
|
||||
if err.code != ErrorCode::INTERNAL_ERROR.code {
|
||||
anyhow::bail!(err)
|
||||
}
|
||||
|
||||
let Some(data) = &err.data else {
|
||||
anyhow::bail!(err)
|
||||
};
|
||||
|
||||
// Temporary workaround until the following PR is generally available:
|
||||
// https://github.com/google-gemini/gemini-cli/pull/6656
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
struct ErrorDetails {
|
||||
details: Box<str>,
|
||||
}
|
||||
|
||||
match serde_json::from_value(data.clone()) {
|
||||
Ok(ErrorDetails { details }) => {
|
||||
if suppress_abort_err && details.contains("This operation was aborted")
|
||||
{
|
||||
Ok(acp::PromptResponse {
|
||||
stop_reason: acp::StopReason::Cancelled,
|
||||
})
|
||||
} else {
|
||||
Err(anyhow!(details))
|
||||
}
|
||||
}
|
||||
Err(_) => Err(anyhow!(err)),
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn cancel(&self, session_id: &acp::SessionId, cx: &mut App) {
|
||||
if let Some(session) = self.sessions.borrow_mut().get_mut(session_id) {
|
||||
session.suppress_abort_err = true;
|
||||
}
|
||||
let conn = self.connection.clone();
|
||||
let params = acp::CancelNotification {
|
||||
session_id: session_id.clone(),
|
||||
};
|
||||
cx.foreground_executor()
|
||||
.spawn(async move { conn.cancel(params).await })
|
||||
.detach();
|
||||
}
|
||||
|
||||
fn into_any(self: Rc<Self>) -> Rc<dyn Any> {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
struct ClientDelegate {
|
||||
sessions: Rc<RefCell<HashMap<acp::SessionId, AcpSession>>>,
|
||||
cx: AsyncApp,
|
||||
}
|
||||
|
||||
impl acp::Client for ClientDelegate {
|
||||
async fn request_permission(
|
||||
&self,
|
||||
arguments: acp::RequestPermissionRequest,
|
||||
) -> Result<acp::RequestPermissionResponse, acp::Error> {
|
||||
let cx = &mut self.cx.clone();
|
||||
let rx = self
|
||||
.sessions
|
||||
.borrow()
|
||||
.get(&arguments.session_id)
|
||||
.context("Failed to get session")?
|
||||
.thread
|
||||
.update(cx, |thread, cx| {
|
||||
thread.request_tool_call_authorization(arguments.tool_call, arguments.options, cx)
|
||||
})?;
|
||||
|
||||
let result = rx?.await;
|
||||
|
||||
let outcome = match result {
|
||||
Ok(option) => acp::RequestPermissionOutcome::Selected { option_id: option },
|
||||
Err(oneshot::Canceled) => acp::RequestPermissionOutcome::Cancelled,
|
||||
};
|
||||
|
||||
Ok(acp::RequestPermissionResponse { outcome })
|
||||
}
|
||||
|
||||
async fn write_text_file(
|
||||
&self,
|
||||
arguments: acp::WriteTextFileRequest,
|
||||
) -> Result<(), acp::Error> {
|
||||
let cx = &mut self.cx.clone();
|
||||
let task = self
|
||||
.sessions
|
||||
.borrow()
|
||||
.get(&arguments.session_id)
|
||||
.context("Failed to get session")?
|
||||
.thread
|
||||
.update(cx, |thread, cx| {
|
||||
thread.write_text_file(arguments.path, arguments.content, cx)
|
||||
})?;
|
||||
|
||||
task.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn read_text_file(
|
||||
&self,
|
||||
arguments: acp::ReadTextFileRequest,
|
||||
) -> Result<acp::ReadTextFileResponse, acp::Error> {
|
||||
let cx = &mut self.cx.clone();
|
||||
let task = self
|
||||
.sessions
|
||||
.borrow()
|
||||
.get(&arguments.session_id)
|
||||
.context("Failed to get session")?
|
||||
.thread
|
||||
.update(cx, |thread, cx| {
|
||||
thread.read_text_file(arguments.path, arguments.line, arguments.limit, false, cx)
|
||||
})?;
|
||||
|
||||
let content = task.await?;
|
||||
|
||||
Ok(acp::ReadTextFileResponse { content })
|
||||
}
|
||||
|
||||
async fn session_notification(
|
||||
&self,
|
||||
notification: acp::SessionNotification,
|
||||
) -> Result<(), acp::Error> {
|
||||
let cx = &mut self.cx.clone();
|
||||
let sessions = self.sessions.borrow();
|
||||
let session = sessions
|
||||
.get(¬ification.session_id)
|
||||
.context("Failed to get session")?;
|
||||
|
||||
session.thread.update(cx, |thread, cx| {
|
||||
thread.handle_session_update(notification.update, cx)
|
||||
})??;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use acp_tools::AcpConnectionRegistry;
|
||||
use action_log::ActionLog;
|
||||
use agent_client_protocol::{self as acp, Agent as _, ErrorCode};
|
||||
use anyhow::anyhow;
|
||||
@@ -101,6 +102,14 @@ impl AcpConnection {
|
||||
})
|
||||
.detach();
|
||||
|
||||
let connection = Rc::new(connection);
|
||||
|
||||
cx.update(|cx| {
|
||||
AcpConnectionRegistry::default_global(cx).update(cx, |registry, cx| {
|
||||
registry.set_active_connection(server_name, &connection, cx)
|
||||
});
|
||||
})?;
|
||||
|
||||
let response = connection
|
||||
.initialize(acp::InitializeRequest {
|
||||
protocol_version: acp::VERSION,
|
||||
@@ -119,7 +128,7 @@ impl AcpConnection {
|
||||
|
||||
Ok(Self {
|
||||
auth_methods: response.auth_methods,
|
||||
connection: connection.into(),
|
||||
connection,
|
||||
server_name,
|
||||
sessions,
|
||||
prompt_capabilities: response.agent_capabilities.prompt_capabilities,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
mod acp;
|
||||
mod claude;
|
||||
mod custom;
|
||||
mod gemini;
|
||||
mod settings;
|
||||
|
||||
@@ -7,6 +8,7 @@ mod settings;
|
||||
pub mod e2e_tests;
|
||||
|
||||
pub use claude::*;
|
||||
pub use custom::*;
|
||||
pub use gemini::*;
|
||||
pub use settings::*;
|
||||
|
||||
@@ -31,9 +33,10 @@ pub fn init(cx: &mut App) {
|
||||
|
||||
pub trait AgentServer: Send {
|
||||
fn logo(&self) -> ui::IconName;
|
||||
fn name(&self) -> &'static str;
|
||||
fn empty_state_headline(&self) -> &'static str;
|
||||
fn empty_state_message(&self) -> &'static str;
|
||||
fn name(&self) -> SharedString;
|
||||
fn empty_state_headline(&self) -> SharedString;
|
||||
fn empty_state_message(&self) -> SharedString;
|
||||
fn telemetry_id(&self) -> &'static str;
|
||||
|
||||
fn connect(
|
||||
&self,
|
||||
@@ -95,7 +98,7 @@ pub struct AgentServerCommand {
|
||||
}
|
||||
|
||||
impl AgentServerCommand {
|
||||
pub(crate) async fn resolve(
|
||||
pub async fn resolve(
|
||||
path_bin_name: &'static str,
|
||||
extra_args: &[&'static str],
|
||||
fallback_path: Option<&Path>,
|
||||
|
||||
@@ -30,7 +30,7 @@ use futures::{
|
||||
io::BufReader,
|
||||
select_biased,
|
||||
};
|
||||
use gpui::{App, AppContext, AsyncApp, Entity, Task, WeakEntity};
|
||||
use gpui::{App, AppContext, AsyncApp, Entity, SharedString, Task, WeakEntity};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use util::{ResultExt, debug_panic};
|
||||
|
||||
@@ -43,16 +43,20 @@ use acp_thread::{AcpThread, AgentConnection, AuthRequired, LoadError, MentionUri
|
||||
pub struct ClaudeCode;
|
||||
|
||||
impl AgentServer for ClaudeCode {
|
||||
fn name(&self) -> &'static str {
|
||||
"Welcome to Claude Code"
|
||||
fn telemetry_id(&self) -> &'static str {
|
||||
"claude-code"
|
||||
}
|
||||
|
||||
fn empty_state_headline(&self) -> &'static str {
|
||||
fn name(&self) -> SharedString {
|
||||
"Claude Code".into()
|
||||
}
|
||||
|
||||
fn empty_state_headline(&self) -> SharedString {
|
||||
self.name()
|
||||
}
|
||||
|
||||
fn empty_state_message(&self) -> &'static str {
|
||||
"How can I help you today?"
|
||||
fn empty_state_message(&self) -> SharedString {
|
||||
"How can I help you today?".into()
|
||||
}
|
||||
|
||||
fn logo(&self) -> ui::IconName {
|
||||
@@ -249,13 +253,19 @@ impl AgentConnection for ClaudeAgentConnection {
|
||||
});
|
||||
|
||||
let action_log = cx.new(|_| ActionLog::new(project.clone()))?;
|
||||
let thread = cx.new(|_cx| {
|
||||
let thread = cx.new(|cx| {
|
||||
AcpThread::new(
|
||||
"Claude Code",
|
||||
self.clone(),
|
||||
project,
|
||||
action_log,
|
||||
session_id.clone(),
|
||||
watch::Receiver::constant(acp::PromptCapabilities {
|
||||
image: true,
|
||||
audio: false,
|
||||
embedded_context: true,
|
||||
}),
|
||||
cx,
|
||||
)
|
||||
})?;
|
||||
|
||||
@@ -319,14 +329,6 @@ impl AgentConnection for ClaudeAgentConnection {
|
||||
cx.foreground_executor().spawn(async move { end_rx.await? })
|
||||
}
|
||||
|
||||
fn prompt_capabilities(&self) -> acp::PromptCapabilities {
|
||||
acp::PromptCapabilities {
|
||||
image: true,
|
||||
audio: false,
|
||||
embedded_context: true,
|
||||
}
|
||||
}
|
||||
|
||||
fn cancel(&self, session_id: &acp::SessionId, _cx: &mut App) {
|
||||
let sessions = self.sessions.borrow();
|
||||
let Some(session) = sessions.get(session_id) else {
|
||||
|
||||
63
crates/agent_servers/src/custom.rs
Normal file
63
crates/agent_servers/src/custom.rs
Normal file
@@ -0,0 +1,63 @@
|
||||
use crate::{AgentServerCommand, AgentServerSettings};
|
||||
use acp_thread::AgentConnection;
|
||||
use anyhow::Result;
|
||||
use gpui::{App, Entity, SharedString, Task};
|
||||
use project::Project;
|
||||
use std::{path::Path, rc::Rc};
|
||||
use ui::IconName;
|
||||
|
||||
/// A generic agent server implementation for custom user-defined agents
|
||||
pub struct CustomAgentServer {
|
||||
name: SharedString,
|
||||
command: AgentServerCommand,
|
||||
}
|
||||
|
||||
impl CustomAgentServer {
|
||||
pub fn new(name: SharedString, settings: &AgentServerSettings) -> Self {
|
||||
Self {
|
||||
name,
|
||||
command: settings.command.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl crate::AgentServer for CustomAgentServer {
|
||||
fn telemetry_id(&self) -> &'static str {
|
||||
"custom"
|
||||
}
|
||||
|
||||
fn name(&self) -> SharedString {
|
||||
self.name.clone()
|
||||
}
|
||||
|
||||
fn logo(&self) -> IconName {
|
||||
IconName::Terminal
|
||||
}
|
||||
|
||||
fn empty_state_headline(&self) -> SharedString {
|
||||
"No conversations yet".into()
|
||||
}
|
||||
|
||||
fn empty_state_message(&self) -> SharedString {
|
||||
format!("Start a conversation with {}", self.name).into()
|
||||
}
|
||||
|
||||
fn connect(
|
||||
&self,
|
||||
root_dir: &Path,
|
||||
_project: &Entity<Project>,
|
||||
cx: &mut App,
|
||||
) -> Task<Result<Rc<dyn AgentConnection>>> {
|
||||
let server_name = self.name();
|
||||
let command = self.command.clone();
|
||||
let root_dir = root_dir.to_path_buf();
|
||||
|
||||
cx.spawn(async move |mut cx| {
|
||||
crate::acp::connect(server_name, command, &root_dir, &mut cx).await
|
||||
})
|
||||
}
|
||||
|
||||
fn into_any(self: Rc<Self>) -> Rc<dyn std::any::Any> {
|
||||
self
|
||||
}
|
||||
}
|
||||
@@ -1,17 +1,15 @@
|
||||
use crate::AgentServer;
|
||||
use acp_thread::{AcpThread, AgentThreadEntry, ToolCall, ToolCallStatus};
|
||||
use agent_client_protocol as acp;
|
||||
use futures::{FutureExt, StreamExt, channel::mpsc, select};
|
||||
use gpui::{AppContext, Entity, TestAppContext};
|
||||
use indoc::indoc;
|
||||
use project::{FakeFs, Project};
|
||||
use std::{
|
||||
path::{Path, PathBuf},
|
||||
sync::Arc,
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use crate::AgentServer;
|
||||
use acp_thread::{AcpThread, AgentThreadEntry, ToolCall, ToolCallStatus};
|
||||
use agent_client_protocol as acp;
|
||||
|
||||
use futures::{FutureExt, StreamExt, channel::mpsc, select};
|
||||
use gpui::{AppContext, Entity, TestAppContext};
|
||||
use indoc::indoc;
|
||||
use project::{FakeFs, Project};
|
||||
use util::path;
|
||||
|
||||
pub async fn test_basic<T, F>(server: F, cx: &mut TestAppContext)
|
||||
@@ -479,6 +477,7 @@ pub async fn init_test(cx: &mut TestAppContext) -> Arc<FakeFs> {
|
||||
gemini: Some(crate::AgentServerSettings {
|
||||
command: crate::gemini::tests::local_command(),
|
||||
}),
|
||||
custom: collections::HashMap::default(),
|
||||
},
|
||||
cx,
|
||||
);
|
||||
|
||||
@@ -4,11 +4,10 @@ use std::{any::Any, path::Path};
|
||||
use crate::{AgentServer, AgentServerCommand};
|
||||
use acp_thread::{AgentConnection, LoadError};
|
||||
use anyhow::Result;
|
||||
use gpui::{Entity, Task};
|
||||
use gpui::{App, Entity, SharedString, Task};
|
||||
use language_models::provider::google::GoogleLanguageModelProvider;
|
||||
use project::Project;
|
||||
use settings::SettingsStore;
|
||||
use ui::App;
|
||||
|
||||
use crate::AllAgentServersSettings;
|
||||
|
||||
@@ -18,16 +17,20 @@ pub struct Gemini;
|
||||
const ACP_ARG: &str = "--experimental-acp";
|
||||
|
||||
impl AgentServer for Gemini {
|
||||
fn name(&self) -> &'static str {
|
||||
"Gemini CLI"
|
||||
fn telemetry_id(&self) -> &'static str {
|
||||
"gemini-cli"
|
||||
}
|
||||
|
||||
fn empty_state_headline(&self) -> &'static str {
|
||||
"Welcome to Gemini CLI"
|
||||
fn name(&self) -> SharedString {
|
||||
"Gemini CLI".into()
|
||||
}
|
||||
|
||||
fn empty_state_message(&self) -> &'static str {
|
||||
"Ask questions, edit files, run commands"
|
||||
fn empty_state_headline(&self) -> SharedString {
|
||||
self.name()
|
||||
}
|
||||
|
||||
fn empty_state_message(&self) -> SharedString {
|
||||
"Ask questions, edit files, run commands".into()
|
||||
}
|
||||
|
||||
fn logo(&self) -> ui::IconName {
|
||||
@@ -54,7 +57,7 @@ impl AgentServer for Gemini {
|
||||
return Err(LoadError::NotInstalled {
|
||||
error_message: "Failed to find Gemini CLI binary".into(),
|
||||
install_message: "Install Gemini CLI".into(),
|
||||
install_command: "npm install -g @google/gemini-cli@preview".into()
|
||||
install_command: Self::install_command().into(),
|
||||
}.into());
|
||||
};
|
||||
|
||||
@@ -89,7 +92,7 @@ impl AgentServer for Gemini {
|
||||
current_version
|
||||
).into(),
|
||||
upgrade_message: "Upgrade Gemini CLI to latest".into(),
|
||||
upgrade_command: "npm install -g @google/gemini-cli@preview".into(),
|
||||
upgrade_command: Self::upgrade_command().into(),
|
||||
}.into())
|
||||
}
|
||||
}
|
||||
@@ -102,6 +105,20 @@ impl AgentServer for Gemini {
|
||||
}
|
||||
}
|
||||
|
||||
impl Gemini {
|
||||
pub fn binary_name() -> &'static str {
|
||||
"gemini"
|
||||
}
|
||||
|
||||
pub fn install_command() -> &'static str {
|
||||
"npm install -g @google/gemini-cli@preview"
|
||||
}
|
||||
|
||||
pub fn upgrade_command() -> &'static str {
|
||||
"npm install -g @google/gemini-cli@preview"
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) mod tests {
|
||||
use super::*;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use crate::AgentServerCommand;
|
||||
use anyhow::Result;
|
||||
use gpui::App;
|
||||
use collections::HashMap;
|
||||
use gpui::{App, SharedString};
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::{Settings, SettingsSources};
|
||||
@@ -13,9 +14,13 @@ pub fn init(cx: &mut App) {
|
||||
pub struct AllAgentServersSettings {
|
||||
pub gemini: Option<AgentServerSettings>,
|
||||
pub claude: Option<AgentServerSettings>,
|
||||
|
||||
/// Custom agent servers configured by the user
|
||||
#[serde(flatten)]
|
||||
pub custom: HashMap<SharedString, AgentServerSettings>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Clone, JsonSchema, Debug)]
|
||||
#[derive(Deserialize, Serialize, Clone, JsonSchema, Debug, PartialEq)]
|
||||
pub struct AgentServerSettings {
|
||||
#[serde(flatten)]
|
||||
pub command: AgentServerCommand,
|
||||
@@ -29,13 +34,26 @@ impl settings::Settings for AllAgentServersSettings {
|
||||
fn load(sources: SettingsSources<Self::FileContent>, _: &mut App) -> Result<Self> {
|
||||
let mut settings = AllAgentServersSettings::default();
|
||||
|
||||
for AllAgentServersSettings { gemini, claude } in sources.defaults_and_customizations() {
|
||||
for AllAgentServersSettings {
|
||||
gemini,
|
||||
claude,
|
||||
custom,
|
||||
} in sources.defaults_and_customizations()
|
||||
{
|
||||
if gemini.is_some() {
|
||||
settings.gemini = gemini.clone();
|
||||
}
|
||||
if claude.is_some() {
|
||||
settings.claude = claude.clone();
|
||||
}
|
||||
|
||||
// Merge custom agents
|
||||
for (name, config) in custom {
|
||||
// Skip built-in agent names to avoid conflicts
|
||||
if name != "gemini" && name != "claude" {
|
||||
settings.custom.insert(name.clone(), config.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(settings)
|
||||
|
||||
@@ -19,10 +19,8 @@ gpui.workspace = true
|
||||
language_model.workspace = true
|
||||
schemars.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
settings.workspace = true
|
||||
workspace-hack.workspace = true
|
||||
editor_mode_setting.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
fs.workspace = true
|
||||
|
||||
@@ -4,11 +4,10 @@ use std::sync::Arc;
|
||||
|
||||
use anyhow::{Result, bail};
|
||||
use collections::IndexMap;
|
||||
use editor_mode_setting::EditorMode;
|
||||
use gpui::{App, Pixels, SharedString};
|
||||
use language_model::LanguageModel;
|
||||
use schemars::{JsonSchema, json_schema};
|
||||
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::{Settings, SettingsSources};
|
||||
use std::borrow::Cow;
|
||||
|
||||
@@ -49,67 +48,6 @@ pub enum NotifyWhenAgentWaiting {
|
||||
Never,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq, Default)]
|
||||
pub enum AgentEditorMode {
|
||||
EditorModeOverride(EditorMode),
|
||||
#[default]
|
||||
Inherit,
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for AgentEditorMode {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let s = String::deserialize(deserializer)?;
|
||||
if s == "inherit" {
|
||||
Ok(AgentEditorMode::Inherit)
|
||||
} else {
|
||||
let mode = EditorMode::deserialize(serde::de::value::StringDeserializer::new(s))?;
|
||||
Ok(AgentEditorMode::EditorModeOverride(mode))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Serialize for AgentEditorMode {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
match self {
|
||||
AgentEditorMode::EditorModeOverride(mode) => mode.serialize(serializer),
|
||||
AgentEditorMode::Inherit => serializer.serialize_str("inherit"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl JsonSchema for AgentEditorMode {
|
||||
fn schema_name() -> Cow<'static, str> {
|
||||
"AgentEditorMode".into()
|
||||
}
|
||||
|
||||
fn json_schema(_gen: &mut schemars::SchemaGenerator) -> schemars::Schema {
|
||||
use editor_mode_setting::EditorMode;
|
||||
|
||||
let mut options = vec![serde_json::json!({
|
||||
"const": "inherit",
|
||||
"description": "Inherit editor mode from global settings"
|
||||
})];
|
||||
options.extend(EditorMode::get_schema_options());
|
||||
|
||||
json_schema!({
|
||||
"oneOf": options,
|
||||
"description": "Agent editor mode - either inherit from global settings or override with a specific mode"
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl From<EditorMode> for AgentEditorMode {
|
||||
fn from(b: EditorMode) -> Self {
|
||||
AgentEditorMode::EditorModeOverride(b)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Clone, Debug)]
|
||||
pub struct AgentSettings {
|
||||
pub enabled: bool,
|
||||
@@ -137,7 +75,6 @@ pub struct AgentSettings {
|
||||
pub expand_edit_card: bool,
|
||||
pub expand_terminal_card: bool,
|
||||
pub use_modifier_to_send: bool,
|
||||
pub editor_mode: AgentEditorMode,
|
||||
}
|
||||
|
||||
impl AgentSettings {
|
||||
@@ -378,10 +315,6 @@ pub struct AgentSettingsContent {
|
||||
///
|
||||
/// Default: false
|
||||
use_modifier_to_send: Option<bool>,
|
||||
/// Weather to inherit or override the editor mode for the agent panel.
|
||||
///
|
||||
/// Default: inherit
|
||||
editor_mode: Option<AgentEditorMode>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Default)]
|
||||
@@ -537,7 +470,6 @@ impl Settings for AgentSettings {
|
||||
&mut settings.use_modifier_to_send,
|
||||
value.use_modifier_to_send,
|
||||
);
|
||||
merge(&mut settings.editor_mode, value.editor_mode);
|
||||
|
||||
settings
|
||||
.model_parameters
|
||||
|
||||
@@ -67,6 +67,7 @@ ordered-float.workspace = true
|
||||
parking_lot.workspace = true
|
||||
paths.workspace = true
|
||||
picker.workspace = true
|
||||
postage.workspace = true
|
||||
project.workspace = true
|
||||
prompt_store.workspace = true
|
||||
proto.workspace = true
|
||||
@@ -100,7 +101,6 @@ watch.workspace = true
|
||||
workspace-hack.workspace = true
|
||||
workspace.workspace = true
|
||||
zed_actions.workspace = true
|
||||
editor_mode_setting.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
acp_thread = { workspace = true, features = ["test-support"] }
|
||||
|
||||
@@ -247,9 +247,9 @@ impl ContextPickerCompletionProvider {
|
||||
|
||||
let abs_path = project.read(cx).absolute_path(&symbol.path, cx)?;
|
||||
let uri = MentionUri::Symbol {
|
||||
path: abs_path,
|
||||
abs_path,
|
||||
name: symbol.name.clone(),
|
||||
line_range: symbol.range.start.0.row..symbol.range.end.0.row,
|
||||
line_range: symbol.range.start.0.row..=symbol.range.end.0.row,
|
||||
};
|
||||
let new_text = format!("{} ", uri.as_link());
|
||||
let new_text_len = new_text.len();
|
||||
|
||||
@@ -4,7 +4,7 @@ use acp_thread::{AcpThread, AgentThreadEntry};
|
||||
use agent_client_protocol::{PromptCapabilities, ToolCallId};
|
||||
use agent2::HistoryStore;
|
||||
use collections::HashMap;
|
||||
use editor::{Editor, EditorDisplayMode, MinimapVisibility};
|
||||
use editor::{Editor, EditorMode, MinimapVisibility};
|
||||
use gpui::{
|
||||
AnyEntity, App, AppContext as _, Entity, EntityId, EventEmitter, Focusable,
|
||||
TextStyleRefinement, WeakEntity, Window,
|
||||
@@ -87,7 +87,7 @@ impl EntryViewState {
|
||||
self.prompt_capabilities.clone(),
|
||||
"Edit message - @ to include context",
|
||||
self.prevent_slash_commands,
|
||||
editor::EditorDisplayMode::AutoHeight {
|
||||
editor::EditorMode::AutoHeight {
|
||||
min_lines: 1,
|
||||
max_lines: None,
|
||||
},
|
||||
@@ -287,7 +287,7 @@ fn create_editor_diff(
|
||||
) -> Entity<Editor> {
|
||||
cx.new(|cx| {
|
||||
let mut editor = Editor::new(
|
||||
EditorDisplayMode::Full {
|
||||
EditorMode::Full {
|
||||
scale_ui_elements_with_buffer_font_size: false,
|
||||
show_active_line_background: false,
|
||||
sized_by_content: true,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -462,7 +462,7 @@ impl AcpThreadHistory {
|
||||
|
||||
cx.notify();
|
||||
}))
|
||||
.end_slot::<IconButton>(if hovered || selected {
|
||||
.end_slot::<IconButton>(if hovered {
|
||||
Some(
|
||||
IconButton::new("delete", IconName::Trash)
|
||||
.shape(IconButtonShape::Square)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1595,11 +1595,6 @@ impl ActiveThread {
|
||||
return;
|
||||
};
|
||||
|
||||
if model.provider.must_accept_terms(cx) {
|
||||
cx.notify();
|
||||
return;
|
||||
}
|
||||
|
||||
let edited_text = state.editor.read(cx).text(cx);
|
||||
|
||||
let creases = state.editor.update(cx, extract_message_creases);
|
||||
@@ -1723,7 +1718,7 @@ impl ActiveThread {
|
||||
|
||||
let editor = cx.new(|cx| {
|
||||
let mut editor = Editor::new(
|
||||
editor::EditorDisplayMode::AutoHeight {
|
||||
editor::EditorMode::AutoHeight {
|
||||
min_lines: 1,
|
||||
max_lines: Some(4),
|
||||
},
|
||||
|
||||
@@ -5,6 +5,7 @@ mod tool_picker;
|
||||
|
||||
use std::{sync::Arc, time::Duration};
|
||||
|
||||
use agent_servers::{AgentServerCommand, AllAgentServersSettings, Gemini};
|
||||
use agent_settings::AgentSettings;
|
||||
use assistant_tool::{ToolSource, ToolWorkingSet};
|
||||
use cloud_llm_client::Plan;
|
||||
@@ -15,7 +16,7 @@ use extension_host::ExtensionStore;
|
||||
use fs::Fs;
|
||||
use gpui::{
|
||||
Action, Animation, AnimationExt as _, AnyView, App, Corner, Entity, EventEmitter, FocusHandle,
|
||||
Focusable, ScrollHandle, Subscription, Task, Transformation, WeakEntity, percentage,
|
||||
Focusable, Hsla, ScrollHandle, Subscription, Task, Transformation, WeakEntity, percentage,
|
||||
};
|
||||
use language::LanguageRegistry;
|
||||
use language_model::{
|
||||
@@ -23,10 +24,11 @@ use language_model::{
|
||||
};
|
||||
use notifications::status_toast::{StatusToast, ToastIcon};
|
||||
use project::{
|
||||
Project,
|
||||
context_server_store::{ContextServerConfiguration, ContextServerStatus, ContextServerStore},
|
||||
project_settings::{ContextServerSettings, ProjectSettings},
|
||||
};
|
||||
use settings::{Settings, update_settings_file};
|
||||
use settings::{Settings, SettingsStore, update_settings_file};
|
||||
use ui::{
|
||||
Chip, ContextMenu, Disclosure, Divider, DividerColor, ElevationIndex, Indicator, PopoverMenu,
|
||||
Scrollbar, ScrollbarState, Switch, SwitchColor, SwitchField, Tooltip, prelude::*,
|
||||
@@ -39,7 +41,7 @@ pub(crate) use configure_context_server_modal::ConfigureContextServerModal;
|
||||
pub(crate) use manage_profiles_modal::ManageProfilesModal;
|
||||
|
||||
use crate::{
|
||||
AddContextServer,
|
||||
AddContextServer, ExternalAgent, NewExternalAgentThread,
|
||||
agent_configuration::add_llm_provider_modal::{AddLlmProviderModal, LlmCompatibleProvider},
|
||||
};
|
||||
|
||||
@@ -47,6 +49,7 @@ pub struct AgentConfiguration {
|
||||
fs: Arc<dyn Fs>,
|
||||
language_registry: Arc<LanguageRegistry>,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
project: WeakEntity<Project>,
|
||||
focus_handle: FocusHandle,
|
||||
configuration_views_by_provider: HashMap<LanguageModelProviderId, AnyView>,
|
||||
context_server_store: Entity<ContextServerStore>,
|
||||
@@ -56,6 +59,8 @@ pub struct AgentConfiguration {
|
||||
_registry_subscription: Subscription,
|
||||
scroll_handle: ScrollHandle,
|
||||
scrollbar_state: ScrollbarState,
|
||||
gemini_is_installed: bool,
|
||||
_check_for_gemini: Task<()>,
|
||||
}
|
||||
|
||||
impl AgentConfiguration {
|
||||
@@ -65,6 +70,7 @@ impl AgentConfiguration {
|
||||
tools: Entity<ToolWorkingSet>,
|
||||
language_registry: Arc<LanguageRegistry>,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
project: WeakEntity<Project>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
@@ -89,33 +95,34 @@ impl AgentConfiguration {
|
||||
|
||||
cx.subscribe(&context_server_store, |_, _, _, cx| cx.notify())
|
||||
.detach();
|
||||
cx.observe_global_in::<SettingsStore>(window, |this, _, cx| {
|
||||
this.check_for_gemini(cx);
|
||||
cx.notify();
|
||||
})
|
||||
.detach();
|
||||
|
||||
let scroll_handle = ScrollHandle::new();
|
||||
let scrollbar_state = ScrollbarState::new(scroll_handle.clone());
|
||||
|
||||
let mut expanded_provider_configurations = HashMap::default();
|
||||
if LanguageModelRegistry::read_global(cx)
|
||||
.provider(&ZED_CLOUD_PROVIDER_ID)
|
||||
.is_some_and(|cloud_provider| cloud_provider.must_accept_terms(cx))
|
||||
{
|
||||
expanded_provider_configurations.insert(ZED_CLOUD_PROVIDER_ID, true);
|
||||
}
|
||||
|
||||
let mut this = Self {
|
||||
fs,
|
||||
language_registry,
|
||||
workspace,
|
||||
project,
|
||||
focus_handle,
|
||||
configuration_views_by_provider: HashMap::default(),
|
||||
context_server_store,
|
||||
expanded_context_server_tools: HashMap::default(),
|
||||
expanded_provider_configurations,
|
||||
expanded_provider_configurations: HashMap::default(),
|
||||
tools,
|
||||
_registry_subscription: registry_subscription,
|
||||
scroll_handle,
|
||||
scrollbar_state,
|
||||
gemini_is_installed: false,
|
||||
_check_for_gemini: Task::ready(()),
|
||||
};
|
||||
this.build_provider_configuration_views(window, cx);
|
||||
this.check_for_gemini(cx);
|
||||
this
|
||||
}
|
||||
|
||||
@@ -145,6 +152,34 @@ impl AgentConfiguration {
|
||||
self.configuration_views_by_provider
|
||||
.insert(provider.id(), configuration_view);
|
||||
}
|
||||
|
||||
fn check_for_gemini(&mut self, cx: &mut Context<Self>) {
|
||||
let project = self.project.clone();
|
||||
let settings = AllAgentServersSettings::get_global(cx).clone();
|
||||
self._check_for_gemini = cx.spawn({
|
||||
async move |this, cx| {
|
||||
let Some(project) = project.upgrade() else {
|
||||
return;
|
||||
};
|
||||
let gemini_is_installed = AgentServerCommand::resolve(
|
||||
Gemini::binary_name(),
|
||||
&[],
|
||||
// TODO expose fallback path from the Gemini/CC types so we don't have to hardcode it again here
|
||||
None,
|
||||
settings.gemini,
|
||||
&project,
|
||||
cx,
|
||||
)
|
||||
.await
|
||||
.is_some();
|
||||
this.update(cx, |this, cx| {
|
||||
this.gemini_is_installed = gemini_is_installed;
|
||||
cx.notify();
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl Focusable for AgentConfiguration {
|
||||
@@ -219,7 +254,6 @@ impl AgentConfiguration {
|
||||
.child(
|
||||
h_flex()
|
||||
.id(provider_id_string.clone())
|
||||
.cursor_pointer()
|
||||
.px_2()
|
||||
.py_0p5()
|
||||
.w_full()
|
||||
@@ -239,10 +273,7 @@ impl AgentConfiguration {
|
||||
h_flex()
|
||||
.w_full()
|
||||
.gap_1()
|
||||
.child(
|
||||
Label::new(provider_name.clone())
|
||||
.size(LabelSize::Large),
|
||||
)
|
||||
.child(Label::new(provider_name.clone()))
|
||||
.map(|this| {
|
||||
if is_zed_provider && is_signed_in {
|
||||
this.child(
|
||||
@@ -287,7 +318,7 @@ impl AgentConfiguration {
|
||||
"Start New Thread",
|
||||
)
|
||||
.icon_position(IconPosition::Start)
|
||||
.icon(IconName::Plus)
|
||||
.icon(IconName::Thread)
|
||||
.icon_size(IconSize::Small)
|
||||
.icon_color(Color::Muted)
|
||||
.label_size(LabelSize::Small)
|
||||
@@ -386,7 +417,7 @@ impl AgentConfiguration {
|
||||
),
|
||||
)
|
||||
.child(
|
||||
Label::new("Add at least one provider to use AI-powered features.")
|
||||
Label::new("Add at least one provider to use AI-powered features with Zed's native agent.")
|
||||
.color(Color::Muted),
|
||||
),
|
||||
),
|
||||
@@ -527,6 +558,14 @@ impl AgentConfiguration {
|
||||
}
|
||||
}
|
||||
|
||||
fn card_item_bg_color(&self, cx: &mut Context<Self>) -> Hsla {
|
||||
cx.theme().colors().background.opacity(0.25)
|
||||
}
|
||||
|
||||
fn card_item_border_color(&self, cx: &mut Context<Self>) -> Hsla {
|
||||
cx.theme().colors().border.opacity(0.6)
|
||||
}
|
||||
|
||||
fn render_context_servers_section(
|
||||
&mut self,
|
||||
window: &mut Window,
|
||||
@@ -544,7 +583,12 @@ impl AgentConfiguration {
|
||||
v_flex()
|
||||
.gap_0p5()
|
||||
.child(Headline::new("Model Context Protocol (MCP) Servers"))
|
||||
.child(Label::new("Connect to context servers through the Model Context Protocol, either using Zed extensions or directly.").color(Color::Muted)),
|
||||
.child(
|
||||
Label::new(
|
||||
"All context servers connected through the Model Context Protocol.",
|
||||
)
|
||||
.color(Color::Muted),
|
||||
),
|
||||
)
|
||||
.children(
|
||||
context_server_ids.into_iter().map(|context_server_id| {
|
||||
@@ -554,7 +598,7 @@ impl AgentConfiguration {
|
||||
.child(
|
||||
h_flex()
|
||||
.justify_between()
|
||||
.gap_2()
|
||||
.gap_1p5()
|
||||
.child(
|
||||
h_flex().w_full().child(
|
||||
Button::new("add-context-server", "Add Custom Server")
|
||||
@@ -645,8 +689,6 @@ impl AgentConfiguration {
|
||||
.map_or([].as_slice(), |tools| tools.as_slice());
|
||||
let tool_count = tools.len();
|
||||
|
||||
let border_color = cx.theme().colors().border.opacity(0.6);
|
||||
|
||||
let (source_icon, source_tooltip) = if is_from_extension {
|
||||
(
|
||||
IconName::ZedMcpExtension,
|
||||
@@ -789,8 +831,8 @@ impl AgentConfiguration {
|
||||
.id(item_id.clone())
|
||||
.border_1()
|
||||
.rounded_md()
|
||||
.border_color(border_color)
|
||||
.bg(cx.theme().colors().background.opacity(0.2))
|
||||
.border_color(self.card_item_border_color(cx))
|
||||
.bg(self.card_item_bg_color(cx))
|
||||
.overflow_hidden()
|
||||
.child(
|
||||
h_flex()
|
||||
@@ -798,7 +840,11 @@ impl AgentConfiguration {
|
||||
.justify_between()
|
||||
.when(
|
||||
error.is_some() || are_tools_expanded && tool_count >= 1,
|
||||
|element| element.border_b_1().border_color(border_color),
|
||||
|element| {
|
||||
element
|
||||
.border_b_1()
|
||||
.border_color(self.card_item_border_color(cx))
|
||||
},
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
@@ -980,6 +1026,166 @@ impl AgentConfiguration {
|
||||
))
|
||||
})
|
||||
}
|
||||
|
||||
fn render_agent_servers_section(&mut self, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let settings = AllAgentServersSettings::get_global(cx).clone();
|
||||
let user_defined_agents = settings
|
||||
.custom
|
||||
.iter()
|
||||
.map(|(name, settings)| {
|
||||
self.render_agent_server(
|
||||
IconName::Ai,
|
||||
name.clone(),
|
||||
ExternalAgent::Custom {
|
||||
name: name.clone(),
|
||||
settings: settings.clone(),
|
||||
},
|
||||
None,
|
||||
cx,
|
||||
)
|
||||
.into_any_element()
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
v_flex()
|
||||
.border_b_1()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.child(
|
||||
v_flex()
|
||||
.p(DynamicSpacing::Base16.rems(cx))
|
||||
.pr(DynamicSpacing::Base20.rems(cx))
|
||||
.gap_2()
|
||||
.child(
|
||||
v_flex()
|
||||
.gap_0p5()
|
||||
.child(Headline::new("External Agents"))
|
||||
.child(
|
||||
Label::new(
|
||||
"Use the full power of Zed's UI with your favorite agent, connected via the Agent Client Protocol.",
|
||||
)
|
||||
.color(Color::Muted),
|
||||
),
|
||||
)
|
||||
.child(self.render_agent_server(
|
||||
IconName::AiGemini,
|
||||
"Gemini CLI",
|
||||
ExternalAgent::Gemini,
|
||||
(!self.gemini_is_installed).then_some(Gemini::install_command().into()),
|
||||
cx,
|
||||
))
|
||||
// TODO add CC
|
||||
.children(user_defined_agents),
|
||||
)
|
||||
}
|
||||
|
||||
fn render_agent_server(
|
||||
&self,
|
||||
icon: IconName,
|
||||
name: impl Into<SharedString>,
|
||||
agent: ExternalAgent,
|
||||
install_command: Option<SharedString>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> impl IntoElement {
|
||||
let name = name.into();
|
||||
h_flex()
|
||||
.p_1()
|
||||
.pl_2()
|
||||
.gap_1p5()
|
||||
.justify_between()
|
||||
.border_1()
|
||||
.rounded_md()
|
||||
.border_color(self.card_item_border_color(cx))
|
||||
.bg(self.card_item_bg_color(cx))
|
||||
.overflow_hidden()
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_1p5()
|
||||
.child(Icon::new(icon).size(IconSize::Small).color(Color::Muted))
|
||||
.child(Label::new(name.clone())),
|
||||
)
|
||||
.map(|this| {
|
||||
if let Some(install_command) = install_command {
|
||||
this.child(
|
||||
Button::new(
|
||||
SharedString::from(format!("install_external_agent-{name}")),
|
||||
"Install Agent",
|
||||
)
|
||||
.label_size(LabelSize::Small)
|
||||
.icon(IconName::Plus)
|
||||
.icon_position(IconPosition::Start)
|
||||
.icon_size(IconSize::XSmall)
|
||||
.icon_color(Color::Muted)
|
||||
.tooltip(Tooltip::text(install_command.clone()))
|
||||
.on_click(cx.listener(
|
||||
move |this, _, window, cx| {
|
||||
let Some(project) = this.project.upgrade() else {
|
||||
return;
|
||||
};
|
||||
let Some(workspace) = this.workspace.upgrade() else {
|
||||
return;
|
||||
};
|
||||
let cwd = project.read(cx).first_project_directory(cx);
|
||||
let shell =
|
||||
project.read(cx).terminal_settings(&cwd, cx).shell.clone();
|
||||
let spawn_in_terminal = task::SpawnInTerminal {
|
||||
id: task::TaskId(install_command.to_string()),
|
||||
full_label: install_command.to_string(),
|
||||
label: install_command.to_string(),
|
||||
command: Some(install_command.to_string()),
|
||||
args: Vec::new(),
|
||||
command_label: install_command.to_string(),
|
||||
cwd,
|
||||
env: Default::default(),
|
||||
use_new_terminal: true,
|
||||
allow_concurrent_runs: true,
|
||||
reveal: Default::default(),
|
||||
reveal_target: Default::default(),
|
||||
hide: Default::default(),
|
||||
shell,
|
||||
show_summary: true,
|
||||
show_command: true,
|
||||
show_rerun: false,
|
||||
};
|
||||
let task = workspace.update(cx, |workspace, cx| {
|
||||
workspace.spawn_in_terminal(spawn_in_terminal, window, cx)
|
||||
});
|
||||
cx.spawn(async move |this, cx| {
|
||||
task.await;
|
||||
this.update(cx, |this, cx| {
|
||||
this.check_for_gemini(cx);
|
||||
})
|
||||
.ok();
|
||||
})
|
||||
.detach();
|
||||
},
|
||||
)),
|
||||
)
|
||||
} else {
|
||||
this.child(
|
||||
h_flex().gap_1().child(
|
||||
Button::new(
|
||||
SharedString::from(format!("start_acp_thread-{name}")),
|
||||
"Start New Thread",
|
||||
)
|
||||
.label_size(LabelSize::Small)
|
||||
.icon(IconName::Thread)
|
||||
.icon_position(IconPosition::Start)
|
||||
.icon_size(IconSize::XSmall)
|
||||
.icon_color(Color::Muted)
|
||||
.on_click(move |_, window, cx| {
|
||||
window.dispatch_action(
|
||||
NewExternalAgentThread {
|
||||
agent: Some(agent.clone()),
|
||||
}
|
||||
.boxed_clone(),
|
||||
cx,
|
||||
);
|
||||
}),
|
||||
),
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for AgentConfiguration {
|
||||
@@ -999,6 +1205,7 @@ impl Render for AgentConfiguration {
|
||||
.size_full()
|
||||
.overflow_y_scroll()
|
||||
.child(self.render_general_settings_section(cx))
|
||||
.child(self.render_agent_servers_section(cx))
|
||||
.child(self.render_context_servers_section(window, cx))
|
||||
.child(self.render_provider_configuration_section(cx)),
|
||||
)
|
||||
|
||||
@@ -1046,7 +1046,7 @@ impl ToolbarItemView for AgentDiffToolbar {
|
||||
}
|
||||
|
||||
if let Some(editor) = item.act_as::<Editor>(cx)
|
||||
&& editor.read(cx).display_mode().is_full()
|
||||
&& editor.read(cx).mode().is_full()
|
||||
{
|
||||
let agent_diff = AgentDiff::global(cx);
|
||||
|
||||
@@ -1529,6 +1529,7 @@ impl AgentDiff {
|
||||
| AcpThreadEvent::TokenUsageUpdated
|
||||
| AcpThreadEvent::EntriesRemoved(_)
|
||||
| AcpThreadEvent::ToolAuthorizationRequired
|
||||
| AcpThreadEvent::PromptCapabilitiesUpdated
|
||||
| AcpThreadEvent::Retry(_) => {}
|
||||
}
|
||||
}
|
||||
@@ -1549,7 +1550,7 @@ impl AgentDiff {
|
||||
}
|
||||
|
||||
fn full_editor_buffer(editor: &Editor, cx: &App) -> Option<WeakEntity<Buffer>> {
|
||||
if editor.display_mode().is_full() {
|
||||
if editor.mode().is_full() {
|
||||
editor
|
||||
.buffer()
|
||||
.read(cx)
|
||||
|
||||
@@ -5,9 +5,11 @@ use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use acp_thread::AcpThread;
|
||||
use agent_servers::AgentServerSettings;
|
||||
use agent2::{DbThreadMetadata, HistoryEntry};
|
||||
use db::kvp::{Dismissable, KEY_VALUE_STORE};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use zed_actions::agent::ReauthenticateAgent;
|
||||
|
||||
use crate::acp::{AcpThreadHistory, ThreadHistoryEvent};
|
||||
use crate::agent_diff::AgentDiffThread;
|
||||
@@ -54,9 +56,7 @@ use gpui::{
|
||||
Pixels, Subscription, Task, UpdateGlobal, WeakEntity, prelude::*, pulsating_between,
|
||||
};
|
||||
use language::LanguageRegistry;
|
||||
use language_model::{
|
||||
ConfigurationError, ConfiguredModel, LanguageModelProviderTosView, LanguageModelRegistry,
|
||||
};
|
||||
use language_model::{ConfigurationError, ConfiguredModel, LanguageModelRegistry};
|
||||
use project::{DisableAiSettings, Project, ProjectPath, Worktree};
|
||||
use prompt_store::{PromptBuilder, PromptStore, UserPromptId};
|
||||
use rules_library::{RulesLibrary, open_rules_library};
|
||||
@@ -130,7 +130,7 @@ pub fn init(cx: &mut App) {
|
||||
if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
|
||||
workspace.focus_panel::<AgentPanel>(window, cx);
|
||||
panel.update(cx, |panel, cx| {
|
||||
panel.external_thread(action.agent, None, None, window, cx)
|
||||
panel.external_thread(action.agent.clone(), None, None, window, cx)
|
||||
});
|
||||
}
|
||||
})
|
||||
@@ -241,7 +241,8 @@ enum WhichFontSize {
|
||||
None,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
// TODO unify this with ExternalAgent
|
||||
#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub enum AgentType {
|
||||
#[default]
|
||||
Zed,
|
||||
@@ -249,23 +250,29 @@ pub enum AgentType {
|
||||
Gemini,
|
||||
ClaudeCode,
|
||||
NativeAgent,
|
||||
Custom {
|
||||
name: SharedString,
|
||||
settings: AgentServerSettings,
|
||||
},
|
||||
}
|
||||
|
||||
impl AgentType {
|
||||
fn label(self) -> impl Into<SharedString> {
|
||||
fn label(&self) -> SharedString {
|
||||
match self {
|
||||
Self::Zed | Self::TextThread => "Zed Agent",
|
||||
Self::NativeAgent => "Agent 2",
|
||||
Self::Gemini => "Gemini CLI",
|
||||
Self::ClaudeCode => "Claude Code",
|
||||
Self::Zed | Self::TextThread => "Zed Agent".into(),
|
||||
Self::NativeAgent => "Agent 2".into(),
|
||||
Self::Gemini => "Gemini CLI".into(),
|
||||
Self::ClaudeCode => "Claude Code".into(),
|
||||
Self::Custom { name, .. } => name.into(),
|
||||
}
|
||||
}
|
||||
|
||||
fn icon(self) -> Option<IconName> {
|
||||
fn icon(&self) -> Option<IconName> {
|
||||
match self {
|
||||
Self::Zed | Self::NativeAgent | Self::TextThread => None,
|
||||
Self::Gemini => Some(IconName::AiGemini),
|
||||
Self::ClaudeCode => Some(IconName::AiClaude),
|
||||
Self::Custom { .. } => Some(IconName::Terminal),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -519,7 +526,7 @@ pub struct AgentPanel {
|
||||
impl AgentPanel {
|
||||
fn serialize(&mut self, cx: &mut Context<Self>) {
|
||||
let width = self.width;
|
||||
let selected_agent = self.selected_agent;
|
||||
let selected_agent = self.selected_agent.clone();
|
||||
self.pending_serialization = Some(cx.background_spawn(async move {
|
||||
KEY_VALUE_STORE
|
||||
.write_kvp(
|
||||
@@ -609,7 +616,7 @@ impl AgentPanel {
|
||||
panel.update(cx, |panel, cx| {
|
||||
panel.width = serialized_panel.width.map(|w| w.round());
|
||||
if let Some(selected_agent) = serialized_panel.selected_agent {
|
||||
panel.selected_agent = selected_agent;
|
||||
panel.selected_agent = selected_agent.clone();
|
||||
panel.new_agent_thread(selected_agent, window, cx);
|
||||
}
|
||||
cx.notify();
|
||||
@@ -1019,6 +1026,8 @@ impl AgentPanel {
|
||||
}
|
||||
|
||||
fn new_prompt_editor(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
telemetry::event!("Agent Thread Started", agent = "zed-text");
|
||||
|
||||
let context = self
|
||||
.context_store
|
||||
.update(cx, |context_store, cx| context_store.create(cx));
|
||||
@@ -1079,14 +1088,17 @@ impl AgentPanel {
|
||||
cx.spawn_in(window, async move |this, cx| {
|
||||
let ext_agent = match agent_choice {
|
||||
Some(agent) => {
|
||||
cx.background_spawn(async move {
|
||||
if let Some(serialized) =
|
||||
serde_json::to_string(&LastUsedExternalAgent { agent }).log_err()
|
||||
{
|
||||
KEY_VALUE_STORE
|
||||
.write_kvp(LAST_USED_EXTERNAL_AGENT_KEY.to_string(), serialized)
|
||||
.await
|
||||
.log_err();
|
||||
cx.background_spawn({
|
||||
let agent = agent.clone();
|
||||
async move {
|
||||
if let Some(serialized) =
|
||||
serde_json::to_string(&LastUsedExternalAgent { agent }).log_err()
|
||||
{
|
||||
KEY_VALUE_STORE
|
||||
.write_kvp(LAST_USED_EXTERNAL_AGENT_KEY.to_string(), serialized)
|
||||
.await
|
||||
.log_err();
|
||||
}
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
@@ -1108,11 +1120,15 @@ impl AgentPanel {
|
||||
}
|
||||
};
|
||||
|
||||
telemetry::event!("Agent Thread Started", agent = ext_agent.name());
|
||||
|
||||
let server = ext_agent.server(fs, history);
|
||||
|
||||
this.update_in(cx, |this, window, cx| {
|
||||
match ext_agent {
|
||||
crate::ExternalAgent::Gemini | crate::ExternalAgent::NativeAgent => {
|
||||
crate::ExternalAgent::Gemini
|
||||
| crate::ExternalAgent::NativeAgent
|
||||
| crate::ExternalAgent::Custom { .. } => {
|
||||
if !cx.has_flag::<GeminiAndNativeFeatureFlag>() {
|
||||
return;
|
||||
}
|
||||
@@ -1463,6 +1479,7 @@ impl AgentPanel {
|
||||
tools,
|
||||
self.language_registry.clone(),
|
||||
self.workspace.clone(),
|
||||
self.project.downgrade(),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
@@ -1841,14 +1858,14 @@ impl AgentPanel {
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
if self.selected_agent != agent {
|
||||
self.selected_agent = agent;
|
||||
self.selected_agent = agent.clone();
|
||||
self.serialize(cx);
|
||||
}
|
||||
self.new_agent_thread(agent, window, cx);
|
||||
}
|
||||
|
||||
pub fn selected_agent(&self) -> AgentType {
|
||||
self.selected_agent
|
||||
self.selected_agent.clone()
|
||||
}
|
||||
|
||||
pub fn new_agent_thread(
|
||||
@@ -1887,6 +1904,13 @@ impl AgentPanel {
|
||||
window,
|
||||
cx,
|
||||
),
|
||||
AgentType::Custom { name, settings } => self.external_thread(
|
||||
Some(crate::ExternalAgent::Custom { name, settings }),
|
||||
None,
|
||||
None,
|
||||
window,
|
||||
cx,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2041,9 +2065,11 @@ impl AgentPanel {
|
||||
match state {
|
||||
ThreadSummary::Pending => Label::new(ThreadSummary::DEFAULT)
|
||||
.truncate()
|
||||
.color(Color::Muted)
|
||||
.into_any_element(),
|
||||
ThreadSummary::Generating => Label::new(LOADING_SUMMARY_PLACEHOLDER)
|
||||
.truncate()
|
||||
.color(Color::Muted)
|
||||
.into_any_element(),
|
||||
ThreadSummary::Ready(_) => div()
|
||||
.w_full()
|
||||
@@ -2097,7 +2123,8 @@ impl AgentPanel {
|
||||
.child(title_editor)
|
||||
.into_any_element()
|
||||
} else {
|
||||
Label::new(thread_view.read(cx).title(cx))
|
||||
Label::new(thread_view.read(cx).title())
|
||||
.color(Color::Muted)
|
||||
.truncate()
|
||||
.into_any_element()
|
||||
}
|
||||
@@ -2111,6 +2138,7 @@ impl AgentPanel {
|
||||
|
||||
match summary {
|
||||
ContextSummary::Pending => Label::new(ContextSummary::DEFAULT)
|
||||
.color(Color::Muted)
|
||||
.truncate()
|
||||
.into_any_element(),
|
||||
ContextSummary::Content(summary) => {
|
||||
@@ -2122,6 +2150,7 @@ impl AgentPanel {
|
||||
} else {
|
||||
Label::new(LOADING_SUMMARY_PLACEHOLDER)
|
||||
.truncate()
|
||||
.color(Color::Muted)
|
||||
.into_any_element()
|
||||
}
|
||||
}
|
||||
@@ -2182,6 +2211,8 @@ impl AgentPanel {
|
||||
"Enable Full Screen"
|
||||
};
|
||||
|
||||
let selected_agent = self.selected_agent.clone();
|
||||
|
||||
PopoverMenu::new("agent-options-menu")
|
||||
.trigger_with_tooltip(
|
||||
IconButton::new("agent-options-menu", IconName::Ellipsis)
|
||||
@@ -2261,6 +2292,11 @@ impl AgentPanel {
|
||||
.action("Settings", Box::new(OpenSettings))
|
||||
.separator()
|
||||
.action(full_screen_label, Box::new(ToggleZoom));
|
||||
|
||||
if selected_agent == AgentType::Gemini {
|
||||
menu = menu.action("Reauthenticate", Box::new(ReauthenticateAgent))
|
||||
}
|
||||
|
||||
menu
|
||||
}))
|
||||
}
|
||||
@@ -2295,6 +2331,8 @@ impl AgentPanel {
|
||||
.menu({
|
||||
let menu = self.assistant_navigation_menu.clone();
|
||||
move |window, cx| {
|
||||
telemetry::event!("View Thread History Clicked");
|
||||
|
||||
if let Some(menu) = menu.as_ref() {
|
||||
menu.update(cx, |_, cx| {
|
||||
cx.defer_in(window, |menu, window, cx| {
|
||||
@@ -2473,6 +2511,8 @@ impl AgentPanel {
|
||||
let workspace = self.workspace.clone();
|
||||
|
||||
move |window, cx| {
|
||||
telemetry::event!("New Thread Clicked");
|
||||
|
||||
let active_thread = active_thread.clone();
|
||||
Some(ContextMenu::build(window, cx, |mut menu, _window, cx| {
|
||||
menu = menu
|
||||
@@ -2607,13 +2647,55 @@ impl AgentPanel {
|
||||
}
|
||||
}),
|
||||
)
|
||||
})
|
||||
.when(cx.has_flag::<GeminiAndNativeFeatureFlag>(), |mut menu| {
|
||||
// Add custom agents from settings
|
||||
let settings =
|
||||
agent_servers::AllAgentServersSettings::get_global(cx);
|
||||
for (agent_name, agent_settings) in &settings.custom {
|
||||
menu = menu.item(
|
||||
ContextMenuEntry::new(format!("New {} Thread", agent_name))
|
||||
.icon(IconName::Terminal)
|
||||
.icon_color(Color::Muted)
|
||||
.handler({
|
||||
let workspace = workspace.clone();
|
||||
let agent_name = agent_name.clone();
|
||||
let agent_settings = agent_settings.clone();
|
||||
move |window, cx| {
|
||||
if let Some(workspace) = workspace.upgrade() {
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
if let Some(panel) =
|
||||
workspace.panel::<AgentPanel>(cx)
|
||||
{
|
||||
panel.update(cx, |panel, cx| {
|
||||
panel.set_selected_agent(
|
||||
AgentType::Custom {
|
||||
name: agent_name
|
||||
.clone(),
|
||||
settings:
|
||||
agent_settings
|
||||
.clone(),
|
||||
},
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
menu
|
||||
});
|
||||
menu
|
||||
}))
|
||||
}
|
||||
});
|
||||
|
||||
let selected_agent_label = self.selected_agent.label().into();
|
||||
let selected_agent_label = self.selected_agent.label();
|
||||
let selected_agent = div()
|
||||
.id("selected_agent_icon")
|
||||
.when_some(self.selected_agent.icon(), |this, icon| {
|
||||
@@ -3198,17 +3280,6 @@ impl AgentPanel {
|
||||
ConfigurationError::ModelNotFound
|
||||
| ConfigurationError::ProviderNotAuthenticated(_)
|
||||
| ConfigurationError::NoProvider => callout.into_any_element(),
|
||||
ConfigurationError::ProviderPendingTermsAcceptance(provider) => {
|
||||
Banner::new()
|
||||
.severity(Severity::Warning)
|
||||
.child(h_flex().w_full().children(
|
||||
provider.render_accept_terms(
|
||||
LanguageModelProviderTosView::ThreadEmptyState,
|
||||
cx,
|
||||
),
|
||||
))
|
||||
.into_any_element()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3698,6 +3769,11 @@ impl Render for AgentPanel {
|
||||
}
|
||||
}))
|
||||
.on_action(cx.listener(Self::toggle_burn_mode))
|
||||
.on_action(cx.listener(|this, _: &ReauthenticateAgent, window, cx| {
|
||||
if let Some(thread_view) = this.active_thread_view() {
|
||||
thread_view.update(cx, |thread_view, cx| thread_view.reauthenticate(window, cx))
|
||||
}
|
||||
}))
|
||||
.child(self.render_toolbar(window, cx))
|
||||
.children(self.render_onboarding(window, cx))
|
||||
.map(|parent| match &self.active_view {
|
||||
|
||||
@@ -28,13 +28,14 @@ use std::rc::Rc;
|
||||
use std::sync::Arc;
|
||||
|
||||
use agent::{Thread, ThreadId};
|
||||
use agent_servers::AgentServerSettings;
|
||||
use agent_settings::{AgentProfileId, AgentSettings, LanguageModelSelection};
|
||||
use assistant_slash_command::SlashCommandRegistry;
|
||||
use client::Client;
|
||||
use command_palette_hooks::CommandPaletteFilter;
|
||||
use feature_flags::FeatureFlagAppExt as _;
|
||||
use fs::Fs;
|
||||
use gpui::{Action, App, Entity, actions};
|
||||
use gpui::{Action, App, Entity, SharedString, actions};
|
||||
use language::LanguageRegistry;
|
||||
use language_model::{
|
||||
ConfiguredModel, LanguageModel, LanguageModelId, LanguageModelProviderId, LanguageModelRegistry,
|
||||
@@ -159,25 +160,43 @@ pub struct NewNativeAgentThreadFromSummary {
|
||||
from_session_id: agent_client_protocol::SessionId,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, Copy, PartialEq, Serialize, Deserialize, JsonSchema)]
|
||||
// TODO unify this with AgentType
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
enum ExternalAgent {
|
||||
#[default]
|
||||
Gemini,
|
||||
ClaudeCode,
|
||||
NativeAgent,
|
||||
Custom {
|
||||
name: SharedString,
|
||||
settings: AgentServerSettings,
|
||||
},
|
||||
}
|
||||
|
||||
impl ExternalAgent {
|
||||
fn name(&self) -> &'static str {
|
||||
match self {
|
||||
Self::NativeAgent => "zed",
|
||||
Self::Gemini => "gemini-cli",
|
||||
Self::ClaudeCode => "claude-code",
|
||||
Self::Custom { .. } => "custom",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn server(
|
||||
&self,
|
||||
fs: Arc<dyn fs::Fs>,
|
||||
history: Entity<agent2::HistoryStore>,
|
||||
) -> Rc<dyn agent_servers::AgentServer> {
|
||||
match self {
|
||||
ExternalAgent::Gemini => Rc::new(agent_servers::Gemini),
|
||||
ExternalAgent::ClaudeCode => Rc::new(agent_servers::ClaudeCode),
|
||||
ExternalAgent::NativeAgent => Rc::new(agent2::NativeAgentServer::new(fs, history)),
|
||||
Self::Gemini => Rc::new(agent_servers::Gemini),
|
||||
Self::ClaudeCode => Rc::new(agent_servers::ClaudeCode),
|
||||
Self::NativeAgent => Rc::new(agent2::NativeAgentServer::new(fs, history)),
|
||||
Self::Custom { name, settings } => Rc::new(agent_servers::CustomAgentServer::new(
|
||||
name.clone(),
|
||||
settings,
|
||||
)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1244,7 +1244,7 @@ mod tests {
|
||||
let editor = workspace.update_in(&mut cx, |workspace, window, cx| {
|
||||
let editor = cx.new(|cx| {
|
||||
Editor::new(
|
||||
editor::EditorDisplayMode::full(),
|
||||
editor::EditorMode::full(),
|
||||
multi_buffer::MultiBuffer::build_simple("", cx),
|
||||
None,
|
||||
window,
|
||||
|
||||
@@ -16,8 +16,7 @@ use db::kvp::Dismissable;
|
||||
use editor::actions::Paste;
|
||||
use editor::display_map::EditorMargins;
|
||||
use editor::{
|
||||
ContextMenuOptions, Editor, EditorDisplayMode, EditorElement, EditorEvent, EditorStyle,
|
||||
MultiBuffer,
|
||||
ContextMenuOptions, Editor, EditorElement, EditorEvent, EditorMode, EditorStyle, MultiBuffer,
|
||||
actions::{MoveDown, MoveUp},
|
||||
};
|
||||
use feature_flags::{FeatureFlagAppExt as _, ZedProFeatureFlag};
|
||||
@@ -870,7 +869,7 @@ impl PromptEditor<BufferCodegen> {
|
||||
|
||||
let prompt_editor = cx.new(|cx| {
|
||||
let mut editor = Editor::new(
|
||||
EditorDisplayMode::AutoHeight {
|
||||
EditorMode::AutoHeight {
|
||||
min_lines: 1,
|
||||
max_lines: Some(Self::MAX_LINES as usize),
|
||||
},
|
||||
@@ -1049,7 +1048,7 @@ impl PromptEditor<TerminalCodegen> {
|
||||
|
||||
let prompt_editor = cx.new(|cx| {
|
||||
let mut editor = Editor::new(
|
||||
EditorDisplayMode::AutoHeight {
|
||||
EditorMode::AutoHeight {
|
||||
min_lines: 1,
|
||||
max_lines: Some(Self::MAX_LINES as usize),
|
||||
},
|
||||
|
||||
@@ -22,8 +22,8 @@ use collections::{HashMap, HashSet};
|
||||
use editor::actions::{MoveUp, Paste};
|
||||
use editor::display_map::CreaseId;
|
||||
use editor::{
|
||||
Addon, AnchorRangeExt, ContextMenuOptions, ContextMenuPlacement, Editor, EditorDisplayMode,
|
||||
EditorElement, EditorEvent, EditorStyle, MultiBuffer,
|
||||
Addon, AnchorRangeExt, ContextMenuOptions, ContextMenuPlacement, Editor, EditorElement,
|
||||
EditorEvent, EditorMode, EditorStyle, MultiBuffer,
|
||||
};
|
||||
use file_icons::FileIcons;
|
||||
use fs::Fs;
|
||||
@@ -114,17 +114,8 @@ pub(crate) fn create_editor(
|
||||
let editor = cx.new(|cx| {
|
||||
let buffer = cx.new(|cx| Buffer::local("", cx).with_language(Arc::new(language), cx));
|
||||
let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
|
||||
let settings = agent_settings::AgentSettings::get_global(cx);
|
||||
|
||||
let editor_mode = match settings.editor_mode {
|
||||
agent_settings::AgentEditorMode::EditorModeOverride(mode) => mode,
|
||||
agent_settings::AgentEditorMode::Inherit => {
|
||||
editor_mode_setting::EditorModeSetting::get_global(cx).0
|
||||
}
|
||||
};
|
||||
|
||||
let mut editor = Editor::new(
|
||||
editor::EditorDisplayMode::AutoHeight {
|
||||
editor::EditorMode::AutoHeight {
|
||||
min_lines,
|
||||
max_lines,
|
||||
},
|
||||
@@ -136,7 +127,7 @@ pub(crate) fn create_editor(
|
||||
editor.set_placeholder_text("Message the agent – @ to include context", cx);
|
||||
editor.set_show_indent_guides(false, cx);
|
||||
editor.set_soft_wrap();
|
||||
editor.set_editor_mode(editor_mode, cx);
|
||||
editor.set_use_modal_editing(true);
|
||||
editor.set_context_menu_options(ContextMenuOptions {
|
||||
min_entries_visible: 12,
|
||||
max_entries_visible: 12,
|
||||
@@ -236,18 +227,6 @@ impl MessageEditor {
|
||||
cx.observe(&thread.read(cx).action_log().clone(), |_, _, cx| {
|
||||
cx.notify()
|
||||
}),
|
||||
cx.observe_global::<AgentSettings>(move |this, cx| {
|
||||
let settings = agent_settings::AgentSettings::get_global(cx);
|
||||
let editor_mode = match settings.editor_mode {
|
||||
agent_settings::AgentEditorMode::EditorModeOverride(mode) => mode,
|
||||
agent_settings::AgentEditorMode::Inherit => {
|
||||
editor_mode_setting::EditorModeSetting::get_global(cx).0
|
||||
}
|
||||
};
|
||||
this.editor.update(cx, |editor, cx| {
|
||||
editor.set_editor_mode(editor_mode, cx);
|
||||
});
|
||||
}),
|
||||
];
|
||||
|
||||
let model_selector = cx.new(|cx| {
|
||||
@@ -320,13 +299,13 @@ impl MessageEditor {
|
||||
self.editor_is_expanded = is_expanded;
|
||||
self.editor.update(cx, |editor, _| {
|
||||
if self.editor_is_expanded {
|
||||
editor.set_display_mode(EditorDisplayMode::Full {
|
||||
editor.set_mode(EditorMode::Full {
|
||||
scale_ui_elements_with_buffer_font_size: false,
|
||||
show_active_line_background: false,
|
||||
sized_by_content: false,
|
||||
})
|
||||
} else {
|
||||
editor.set_display_mode(EditorDisplayMode::AutoHeight {
|
||||
editor.set_mode(EditorMode::AutoHeight {
|
||||
min_lines: MIN_EDITOR_LINES,
|
||||
max_lines: Some(MAX_EDITOR_LINES),
|
||||
})
|
||||
@@ -399,18 +378,13 @@ impl MessageEditor {
|
||||
}
|
||||
|
||||
fn send_to_model(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let Some(ConfiguredModel { model, provider }) = self
|
||||
let Some(ConfiguredModel { model, .. }) = self
|
||||
.thread
|
||||
.update(cx, |thread, cx| thread.get_or_init_configured_model(cx))
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
if provider.must_accept_terms(cx) {
|
||||
cx.notify();
|
||||
return;
|
||||
}
|
||||
|
||||
let (user_message, user_message_creases) = self.editor.update(cx, |editor, cx| {
|
||||
let creases = extract_message_creases(editor, cx);
|
||||
let text = editor.text(cx);
|
||||
|
||||
@@ -190,7 +190,6 @@ pub struct TextThreadEditor {
|
||||
invoked_slash_command_creases: HashMap<InvokedSlashCommandId, CreaseId>,
|
||||
_subscriptions: Vec<Subscription>,
|
||||
last_error: Option<AssistError>,
|
||||
show_accept_terms: bool,
|
||||
pub(crate) slash_menu_handle:
|
||||
PopoverMenuHandle<Picker<slash_command_picker::SlashCommandDelegate>>,
|
||||
// dragged_file_worktrees is used to keep references to worktrees that were added
|
||||
@@ -289,7 +288,6 @@ impl TextThreadEditor {
|
||||
invoked_slash_command_creases: HashMap::default(),
|
||||
_subscriptions,
|
||||
last_error: None,
|
||||
show_accept_terms: false,
|
||||
slash_menu_handle: Default::default(),
|
||||
dragged_file_worktrees: Vec::new(),
|
||||
language_model_selector: cx.new(|cx| {
|
||||
@@ -363,24 +361,12 @@ impl TextThreadEditor {
|
||||
if self.sending_disabled(cx) {
|
||||
return;
|
||||
}
|
||||
telemetry::event!("Agent Message Sent", agent = "zed-text");
|
||||
self.send_to_model(window, cx);
|
||||
}
|
||||
|
||||
fn send_to_model(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let provider = LanguageModelRegistry::read_global(cx)
|
||||
.default_model()
|
||||
.map(|default| default.provider);
|
||||
if provider
|
||||
.as_ref()
|
||||
.is_some_and(|provider| provider.must_accept_terms(cx))
|
||||
{
|
||||
self.show_accept_terms = true;
|
||||
cx.notify();
|
||||
return;
|
||||
}
|
||||
|
||||
self.last_error = None;
|
||||
|
||||
if let Some(user_message) = self.context.update(cx, |context, cx| context.assist(cx)) {
|
||||
let new_selection = {
|
||||
let cursor = user_message
|
||||
@@ -1930,7 +1916,6 @@ impl TextThreadEditor {
|
||||
ConfigurationError::NoProvider
|
||||
| ConfigurationError::ModelNotFound
|
||||
| ConfigurationError::ProviderNotAuthenticated(_) => true,
|
||||
ConfigurationError::ProviderPendingTermsAcceptance(_) => self.show_accept_terms,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -86,23 +86,18 @@ impl RenderOnce for UsageCallout {
|
||||
(IconName::Warning, Severity::Warning)
|
||||
};
|
||||
|
||||
div()
|
||||
.border_t_1()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.child(
|
||||
Callout::new()
|
||||
.icon(icon)
|
||||
.severity(severity)
|
||||
.icon(icon)
|
||||
.title(title)
|
||||
.description(message)
|
||||
.actions_slot(
|
||||
Button::new("upgrade", button_text)
|
||||
.label_size(LabelSize::Small)
|
||||
.on_click(move |_, _, cx| {
|
||||
cx.open_url(&url);
|
||||
}),
|
||||
),
|
||||
Callout::new()
|
||||
.icon(icon)
|
||||
.severity(severity)
|
||||
.icon(icon)
|
||||
.title(title)
|
||||
.description(message)
|
||||
.actions_slot(
|
||||
Button::new("upgrade", button_text)
|
||||
.label_size(LabelSize::Small)
|
||||
.on_click(move |_, _, cx| {
|
||||
cx.open_url(&url);
|
||||
}),
|
||||
)
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ use std::sync::Arc;
|
||||
|
||||
use client::{Client, UserStore, zed_urls};
|
||||
use gpui::{AnyElement, Entity, IntoElement, ParentElement};
|
||||
use ui::{Divider, RegisterComponent, TintColor, Tooltip, prelude::*};
|
||||
use ui::{Divider, RegisterComponent, Tooltip, prelude::*};
|
||||
|
||||
#[derive(PartialEq)]
|
||||
pub enum SignInStatus {
|
||||
@@ -43,12 +43,10 @@ impl From<client::Status> for SignInStatus {
|
||||
#[derive(RegisterComponent, IntoElement)]
|
||||
pub struct ZedAiOnboarding {
|
||||
pub sign_in_status: SignInStatus,
|
||||
pub has_accepted_terms_of_service: bool,
|
||||
pub plan: Option<Plan>,
|
||||
pub account_too_young: bool,
|
||||
pub continue_with_zed_ai: Arc<dyn Fn(&mut Window, &mut App)>,
|
||||
pub sign_in: Arc<dyn Fn(&mut Window, &mut App)>,
|
||||
pub accept_terms_of_service: Arc<dyn Fn(&mut Window, &mut App)>,
|
||||
pub dismiss_onboarding: Option<Arc<dyn Fn(&mut Window, &mut App)>>,
|
||||
}
|
||||
|
||||
@@ -64,17 +62,9 @@ impl ZedAiOnboarding {
|
||||
|
||||
Self {
|
||||
sign_in_status: status.into(),
|
||||
has_accepted_terms_of_service: store.has_accepted_terms_of_service(),
|
||||
plan: store.plan(),
|
||||
account_too_young: store.account_too_young(),
|
||||
continue_with_zed_ai,
|
||||
accept_terms_of_service: Arc::new({
|
||||
let store = user_store.clone();
|
||||
move |_window, cx| {
|
||||
let task = store.update(cx, |store, cx| store.accept_terms_of_service(cx));
|
||||
task.detach_and_log_err(cx);
|
||||
}
|
||||
}),
|
||||
sign_in: Arc::new(move |_window, cx| {
|
||||
cx.spawn({
|
||||
let client = client.clone();
|
||||
@@ -94,42 +84,6 @@ impl ZedAiOnboarding {
|
||||
self
|
||||
}
|
||||
|
||||
fn render_accept_terms_of_service(&self) -> AnyElement {
|
||||
v_flex()
|
||||
.gap_1()
|
||||
.w_full()
|
||||
.child(Headline::new("Accept Terms of Service"))
|
||||
.child(
|
||||
Label::new("We don’t sell your data, track you across the web, or compromise your privacy.")
|
||||
.color(Color::Muted)
|
||||
.mb_2(),
|
||||
)
|
||||
.child(
|
||||
Button::new("terms_of_service", "Review Terms of Service")
|
||||
.full_width()
|
||||
.style(ButtonStyle::Outlined)
|
||||
.icon(IconName::ArrowUpRight)
|
||||
.icon_color(Color::Muted)
|
||||
.icon_size(IconSize::Small)
|
||||
.on_click(move |_, _window, cx| {
|
||||
telemetry::event!("Review Terms of Service Clicked");
|
||||
cx.open_url(&zed_urls::terms_of_service(cx))
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
Button::new("accept_terms", "Accept")
|
||||
.full_width()
|
||||
.style(ButtonStyle::Tinted(TintColor::Accent))
|
||||
.on_click({
|
||||
let callback = self.accept_terms_of_service.clone();
|
||||
move |_, window, cx| {
|
||||
telemetry::event!("Terms of Service Accepted");
|
||||
(callback)(window, cx)}
|
||||
}),
|
||||
)
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
fn render_sign_in_disclaimer(&self, _cx: &mut App) -> AnyElement {
|
||||
let signing_in = matches!(self.sign_in_status, SignInStatus::SigningIn);
|
||||
let plan_definitions = PlanDefinitions;
|
||||
@@ -359,14 +313,10 @@ impl ZedAiOnboarding {
|
||||
impl RenderOnce for ZedAiOnboarding {
|
||||
fn render(self, _window: &mut ui::Window, cx: &mut App) -> impl IntoElement {
|
||||
if matches!(self.sign_in_status, SignInStatus::SignedIn) {
|
||||
if self.has_accepted_terms_of_service {
|
||||
match self.plan {
|
||||
None | Some(Plan::ZedFree) => self.render_free_plan_state(cx),
|
||||
Some(Plan::ZedProTrial) => self.render_trial_state(cx),
|
||||
Some(Plan::ZedPro) => self.render_pro_plan_state(cx),
|
||||
}
|
||||
} else {
|
||||
self.render_accept_terms_of_service()
|
||||
match self.plan {
|
||||
None | Some(Plan::ZedFree) => self.render_free_plan_state(cx),
|
||||
Some(Plan::ZedProTrial) => self.render_trial_state(cx),
|
||||
Some(Plan::ZedPro) => self.render_pro_plan_state(cx),
|
||||
}
|
||||
} else {
|
||||
self.render_sign_in_disclaimer(cx)
|
||||
@@ -390,18 +340,15 @@ impl Component for ZedAiOnboarding {
|
||||
fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
|
||||
fn onboarding(
|
||||
sign_in_status: SignInStatus,
|
||||
has_accepted_terms_of_service: bool,
|
||||
plan: Option<Plan>,
|
||||
account_too_young: bool,
|
||||
) -> AnyElement {
|
||||
ZedAiOnboarding {
|
||||
sign_in_status,
|
||||
has_accepted_terms_of_service,
|
||||
plan,
|
||||
account_too_young,
|
||||
continue_with_zed_ai: Arc::new(|_, _| {}),
|
||||
sign_in: Arc::new(|_, _| {}),
|
||||
accept_terms_of_service: Arc::new(|_, _| {}),
|
||||
dismiss_onboarding: None,
|
||||
}
|
||||
.into_any_element()
|
||||
@@ -415,27 +362,23 @@ impl Component for ZedAiOnboarding {
|
||||
.children(vec![
|
||||
single_example(
|
||||
"Not Signed-in",
|
||||
onboarding(SignInStatus::SignedOut, false, None, false),
|
||||
),
|
||||
single_example(
|
||||
"Not Accepted ToS",
|
||||
onboarding(SignInStatus::SignedIn, false, None, false),
|
||||
onboarding(SignInStatus::SignedOut, None, false),
|
||||
),
|
||||
single_example(
|
||||
"Young Account",
|
||||
onboarding(SignInStatus::SignedIn, true, None, true),
|
||||
onboarding(SignInStatus::SignedIn, None, true),
|
||||
),
|
||||
single_example(
|
||||
"Free Plan",
|
||||
onboarding(SignInStatus::SignedIn, true, Some(Plan::ZedFree), false),
|
||||
onboarding(SignInStatus::SignedIn, Some(Plan::ZedFree), false),
|
||||
),
|
||||
single_example(
|
||||
"Pro Trial",
|
||||
onboarding(SignInStatus::SignedIn, true, Some(Plan::ZedProTrial), false),
|
||||
onboarding(SignInStatus::SignedIn, Some(Plan::ZedProTrial), false),
|
||||
),
|
||||
single_example(
|
||||
"Pro Plan",
|
||||
onboarding(SignInStatus::SignedIn, true, Some(Plan::ZedPro), false),
|
||||
onboarding(SignInStatus::SignedIn, Some(Plan::ZedPro), false),
|
||||
),
|
||||
])
|
||||
.into_any_element(),
|
||||
|
||||
@@ -12,11 +12,11 @@ use crate::{SignInStatus, YoungAccountBanner, plan_definitions::PlanDefinitions}
|
||||
|
||||
#[derive(IntoElement, RegisterComponent)]
|
||||
pub struct AiUpsellCard {
|
||||
pub sign_in_status: SignInStatus,
|
||||
pub sign_in: Arc<dyn Fn(&mut Window, &mut App)>,
|
||||
pub account_too_young: bool,
|
||||
pub user_plan: Option<Plan>,
|
||||
pub tab_index: Option<isize>,
|
||||
sign_in_status: SignInStatus,
|
||||
sign_in: Arc<dyn Fn(&mut Window, &mut App)>,
|
||||
account_too_young: bool,
|
||||
user_plan: Option<Plan>,
|
||||
tab_index: Option<isize>,
|
||||
}
|
||||
|
||||
impl AiUpsellCard {
|
||||
@@ -43,6 +43,11 @@ impl AiUpsellCard {
|
||||
tab_index: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn tab_index(mut self, tab_index: Option<isize>) -> Self {
|
||||
self.tab_index = tab_index;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for AiUpsellCard {
|
||||
|
||||
@@ -11,7 +11,7 @@ use assistant_tool::{
|
||||
AnyToolCard, Tool, ToolCard, ToolResult, ToolResultContent, ToolResultOutput, ToolUseStatus,
|
||||
};
|
||||
use buffer_diff::{BufferDiff, BufferDiffSnapshot};
|
||||
use editor::{Editor, EditorDisplayMode, MinimapVisibility, MultiBuffer, PathKey};
|
||||
use editor::{Editor, EditorMode, MinimapVisibility, MultiBuffer, PathKey};
|
||||
use futures::StreamExt;
|
||||
use gpui::{
|
||||
Animation, AnimationExt, AnyWindowHandle, App, AppContext, AsyncApp, Entity, Task,
|
||||
@@ -582,7 +582,7 @@ impl EditFileToolCard {
|
||||
|
||||
let editor = cx.new(|cx| {
|
||||
let mut editor = Editor::new(
|
||||
EditorDisplayMode::Full {
|
||||
EditorMode::Full {
|
||||
scale_ui_elements_with_buffer_font_size: false,
|
||||
show_active_line_background: false,
|
||||
sized_by_content: true,
|
||||
|
||||
@@ -118,7 +118,7 @@ impl Tool for FetchTool {
|
||||
}
|
||||
|
||||
fn needs_confirmation(&self, _: &serde_json::Value, _: &Entity<Project>, _: &App) -> bool {
|
||||
false
|
||||
true
|
||||
}
|
||||
|
||||
fn may_perform_edits(&self) -> bool {
|
||||
|
||||
@@ -435,8 +435,8 @@ mod test {
|
||||
assert_eq!(
|
||||
matches,
|
||||
&[
|
||||
PathBuf::from("root/apple/banana/carrot"),
|
||||
PathBuf::from("root/apple/bandana/carbonara")
|
||||
PathBuf::from(path!("root/apple/banana/carrot")),
|
||||
PathBuf::from(path!("root/apple/bandana/carbonara"))
|
||||
]
|
||||
);
|
||||
|
||||
@@ -447,8 +447,8 @@ mod test {
|
||||
assert_eq!(
|
||||
matches,
|
||||
&[
|
||||
PathBuf::from("root/apple/banana/carrot"),
|
||||
PathBuf::from("root/apple/bandana/carbonara")
|
||||
PathBuf::from(path!("root/apple/banana/carrot")),
|
||||
PathBuf::from(path!("root/apple/bandana/carbonara"))
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
@@ -68,7 +68,7 @@ impl Tool for ReadFileTool {
|
||||
}
|
||||
|
||||
fn icon(&self) -> IconName {
|
||||
IconName::ToolRead
|
||||
IconName::ToolSearch
|
||||
}
|
||||
|
||||
fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result<serde_json::Value> {
|
||||
|
||||
@@ -66,6 +66,8 @@ pub static IMPERSONATE_LOGIN: LazyLock<Option<String>> = LazyLock::new(|| {
|
||||
.and_then(|s| if s.is_empty() { None } else { Some(s) })
|
||||
});
|
||||
|
||||
pub static USE_WEB_LOGIN: LazyLock<bool> = LazyLock::new(|| std::env::var("ZED_WEB_LOGIN").is_ok());
|
||||
|
||||
pub static ADMIN_API_TOKEN: LazyLock<Option<String>> = LazyLock::new(|| {
|
||||
std::env::var("ZED_ADMIN_API_TOKEN")
|
||||
.ok()
|
||||
@@ -1392,11 +1394,13 @@ impl Client {
|
||||
if let Some((login, token)) =
|
||||
IMPERSONATE_LOGIN.as_ref().zip(ADMIN_API_TOKEN.as_ref())
|
||||
{
|
||||
eprintln!("authenticate as admin {login}, {token}");
|
||||
if !*USE_WEB_LOGIN {
|
||||
eprintln!("authenticate as admin {login}, {token}");
|
||||
|
||||
return this
|
||||
.authenticate_as_admin(http, login.clone(), token.clone())
|
||||
.await;
|
||||
return this
|
||||
.authenticate_as_admin(http, login.clone(), token.clone())
|
||||
.await;
|
||||
}
|
||||
}
|
||||
|
||||
// Start an HTTP server to receive the redirect from Zed's sign-in page.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use super::{Client, Status, TypedEnvelope, proto};
|
||||
use anyhow::{Context as _, Result, anyhow};
|
||||
use anyhow::{Context as _, Result};
|
||||
use chrono::{DateTime, Utc};
|
||||
use cloud_api_client::websocket_protocol::MessageToClient;
|
||||
use cloud_api_client::{GetAuthenticatedUserResponse, PlanInfo};
|
||||
@@ -46,11 +46,6 @@ impl ProjectId {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, serde::Serialize, serde::Deserialize,
|
||||
)]
|
||||
pub struct DevServerProjectId(pub u64);
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct ParticipantIndex(pub u32);
|
||||
|
||||
@@ -116,7 +111,6 @@ pub struct UserStore {
|
||||
edit_prediction_usage: Option<EditPredictionUsage>,
|
||||
plan_info: Option<PlanInfo>,
|
||||
current_user: watch::Receiver<Option<Arc<User>>>,
|
||||
accepted_tos_at: Option<Option<cloud_api_client::Timestamp>>,
|
||||
contacts: Vec<Arc<Contact>>,
|
||||
incoming_contact_requests: Vec<Arc<User>>,
|
||||
outgoing_contact_requests: Vec<Arc<User>>,
|
||||
@@ -194,7 +188,6 @@ impl UserStore {
|
||||
plan_info: None,
|
||||
model_request_usage: None,
|
||||
edit_prediction_usage: None,
|
||||
accepted_tos_at: None,
|
||||
contacts: Default::default(),
|
||||
incoming_contact_requests: Default::default(),
|
||||
participant_indices: Default::default(),
|
||||
@@ -271,7 +264,6 @@ impl UserStore {
|
||||
Status::SignedOut => {
|
||||
current_user_tx.send(None).await.ok();
|
||||
this.update(cx, |this, cx| {
|
||||
this.accepted_tos_at = None;
|
||||
cx.emit(Event::PrivateUserInfoUpdated);
|
||||
cx.notify();
|
||||
this.clear_contacts()
|
||||
@@ -791,19 +783,6 @@ impl UserStore {
|
||||
.set_authenticated_user_info(Some(response.user.metrics_id.clone()), staff);
|
||||
}
|
||||
|
||||
let accepted_tos_at = {
|
||||
#[cfg(debug_assertions)]
|
||||
if std::env::var("ZED_IGNORE_ACCEPTED_TOS").is_ok() {
|
||||
None
|
||||
} else {
|
||||
response.user.accepted_tos_at
|
||||
}
|
||||
|
||||
#[cfg(not(debug_assertions))]
|
||||
response.user.accepted_tos_at
|
||||
};
|
||||
|
||||
self.accepted_tos_at = Some(accepted_tos_at);
|
||||
self.model_request_usage = Some(ModelRequestUsage(RequestUsage {
|
||||
limit: response.plan.usage.model_requests.limit,
|
||||
amount: response.plan.usage.model_requests.used as i32,
|
||||
@@ -846,32 +825,6 @@ impl UserStore {
|
||||
self.current_user.clone()
|
||||
}
|
||||
|
||||
pub fn has_accepted_terms_of_service(&self) -> bool {
|
||||
self.accepted_tos_at
|
||||
.is_some_and(|accepted_tos_at| accepted_tos_at.is_some())
|
||||
}
|
||||
|
||||
pub fn accept_terms_of_service(&self, cx: &Context<Self>) -> Task<Result<()>> {
|
||||
if self.current_user().is_none() {
|
||||
return Task::ready(Err(anyhow!("no current user")));
|
||||
};
|
||||
|
||||
let client = self.client.clone();
|
||||
cx.spawn(async move |this, cx| -> anyhow::Result<()> {
|
||||
let client = client.upgrade().context("client not found")?;
|
||||
let response = client
|
||||
.cloud_client()
|
||||
.accept_terms_of_service()
|
||||
.await
|
||||
.context("error accepting tos")?;
|
||||
this.update(cx, |this, cx| {
|
||||
this.accepted_tos_at = Some(response.user.accepted_tos_at);
|
||||
cx.emit(Event::PrivateUserInfoUpdated);
|
||||
})?;
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
fn load_users(
|
||||
&self,
|
||||
request: impl RequestMessage<Response = UsersResponse>,
|
||||
|
||||
@@ -115,34 +115,6 @@ impl CloudApiClient {
|
||||
}))
|
||||
}
|
||||
|
||||
pub async fn accept_terms_of_service(&self) -> Result<AcceptTermsOfServiceResponse> {
|
||||
let request = self.build_request(
|
||||
Request::builder().method(Method::POST).uri(
|
||||
self.http_client
|
||||
.build_zed_cloud_url("/client/terms_of_service/accept", &[])?
|
||||
.as_ref(),
|
||||
),
|
||||
AsyncBody::default(),
|
||||
)?;
|
||||
|
||||
let mut response = self.http_client.send(request).await?;
|
||||
|
||||
if !response.status().is_success() {
|
||||
let mut body = String::new();
|
||||
response.body_mut().read_to_string(&mut body).await?;
|
||||
|
||||
anyhow::bail!(
|
||||
"Failed to accept terms of service.\nStatus: {:?}\nBody: {body}",
|
||||
response.status()
|
||||
)
|
||||
}
|
||||
|
||||
let mut body = String::new();
|
||||
response.body_mut().read_to_string(&mut body).await?;
|
||||
|
||||
Ok(serde_json::from_str(&body)?)
|
||||
}
|
||||
|
||||
pub async fn create_llm_token(
|
||||
&self,
|
||||
system_id: Option<String>,
|
||||
|
||||
@@ -970,7 +970,7 @@ async fn test_peers_following_each_other(cx_a: &mut TestAppContext, cx_b: &mut T
|
||||
// the follow.
|
||||
workspace_b.update_in(cx_b, |workspace, window, cx| {
|
||||
workspace.active_pane().update(cx, |pane, cx| {
|
||||
pane.activate_prev_item(true, window, cx);
|
||||
pane.activate_previous_item(&Default::default(), window, cx);
|
||||
});
|
||||
});
|
||||
executor.run_until_parked();
|
||||
@@ -1073,7 +1073,7 @@ async fn test_peers_following_each_other(cx_a: &mut TestAppContext, cx_b: &mut T
|
||||
// Client A cycles through some tabs.
|
||||
workspace_a.update_in(cx_a, |workspace, window, cx| {
|
||||
workspace.active_pane().update(cx, |pane, cx| {
|
||||
pane.activate_prev_item(true, window, cx);
|
||||
pane.activate_previous_item(&Default::default(), window, cx);
|
||||
});
|
||||
});
|
||||
executor.run_until_parked();
|
||||
@@ -1117,7 +1117,7 @@ async fn test_peers_following_each_other(cx_a: &mut TestAppContext, cx_b: &mut T
|
||||
|
||||
workspace_a.update_in(cx_a, |workspace, window, cx| {
|
||||
workspace.active_pane().update(cx, |pane, cx| {
|
||||
pane.activate_prev_item(true, window, cx);
|
||||
pane.activate_previous_item(&Default::default(), window, cx);
|
||||
});
|
||||
});
|
||||
executor.run_until_parked();
|
||||
@@ -1164,7 +1164,7 @@ async fn test_peers_following_each_other(cx_a: &mut TestAppContext, cx_b: &mut T
|
||||
|
||||
workspace_a.update_in(cx_a, |workspace, window, cx| {
|
||||
workspace.active_pane().update(cx, |pane, cx| {
|
||||
pane.activate_prev_item(true, window, cx);
|
||||
pane.activate_previous_item(&Default::default(), window, cx);
|
||||
});
|
||||
});
|
||||
executor.run_until_parked();
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use anyhow::Context as _;
|
||||
use collections::HashMap;
|
||||
use futures::{Stream, StreamExt as _, lock::Mutex};
|
||||
use futures::{FutureExt, Stream, StreamExt as _, future::BoxFuture, lock::Mutex};
|
||||
use gpui::BackgroundExecutor;
|
||||
use std::{pin::Pin, sync::Arc};
|
||||
|
||||
@@ -14,9 +14,12 @@ pub fn create_fake_transport(
|
||||
executor: BackgroundExecutor,
|
||||
) -> FakeTransport {
|
||||
let name = name.into();
|
||||
FakeTransport::new(executor).on_request::<crate::types::requests::Initialize>(move |_params| {
|
||||
create_initialize_response(name.clone())
|
||||
})
|
||||
FakeTransport::new(executor).on_request::<crate::types::requests::Initialize, _>(
|
||||
move |_params| {
|
||||
let name = name.clone();
|
||||
async move { create_initialize_response(name.clone()) }
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fn create_initialize_response(server_name: String) -> InitializeResponse {
|
||||
@@ -32,8 +35,10 @@ fn create_initialize_response(server_name: String) -> InitializeResponse {
|
||||
}
|
||||
|
||||
pub struct FakeTransport {
|
||||
request_handlers:
|
||||
HashMap<&'static str, Arc<dyn Fn(serde_json::Value) -> serde_json::Value + Send + Sync>>,
|
||||
request_handlers: HashMap<
|
||||
&'static str,
|
||||
Arc<dyn Send + Sync + Fn(serde_json::Value) -> BoxFuture<'static, serde_json::Value>>,
|
||||
>,
|
||||
tx: futures::channel::mpsc::UnboundedSender<String>,
|
||||
rx: Arc<Mutex<futures::channel::mpsc::UnboundedReceiver<String>>>,
|
||||
executor: BackgroundExecutor,
|
||||
@@ -50,18 +55,25 @@ impl FakeTransport {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn on_request<T: crate::types::Request>(
|
||||
pub fn on_request<T, Fut>(
|
||||
mut self,
|
||||
handler: impl Fn(T::Params) -> T::Response + Send + Sync + 'static,
|
||||
) -> Self {
|
||||
handler: impl 'static + Send + Sync + Fn(T::Params) -> Fut,
|
||||
) -> Self
|
||||
where
|
||||
T: crate::types::Request,
|
||||
Fut: 'static + Send + Future<Output = T::Response>,
|
||||
{
|
||||
self.request_handlers.insert(
|
||||
T::METHOD,
|
||||
Arc::new(move |value| {
|
||||
let params = value.get("params").expect("Missing parameters").clone();
|
||||
let params = value
|
||||
.get("params")
|
||||
.cloned()
|
||||
.unwrap_or(serde_json::Value::Null);
|
||||
let params: T::Params =
|
||||
serde_json::from_value(params).expect("Invalid parameters received");
|
||||
let response = handler(params);
|
||||
serde_json::to_value(response).unwrap()
|
||||
async move { serde_json::to_value(response.await).unwrap() }.boxed()
|
||||
}),
|
||||
);
|
||||
self
|
||||
@@ -77,7 +89,7 @@ impl Transport for FakeTransport {
|
||||
if let Some(method) = msg.get("method") {
|
||||
let method = method.as_str().expect("Invalid method received");
|
||||
if let Some(handler) = self.request_handlers.get(method) {
|
||||
let payload = handler(msg);
|
||||
let payload = handler(msg).await;
|
||||
let response = serde_json::json!({
|
||||
"jsonrpc": "2.0",
|
||||
"id": id,
|
||||
|
||||
@@ -301,6 +301,7 @@ mod tests {
|
||||
init_test(cx, |settings| {
|
||||
settings.defaults.completions = Some(CompletionSettings {
|
||||
words: WordsCompletionMode::Disabled,
|
||||
words_min_length: 0,
|
||||
lsp: true,
|
||||
lsp_fetch_timeout_ms: 0,
|
||||
lsp_insert_mode: LspInsertMode::Insert,
|
||||
@@ -533,6 +534,7 @@ mod tests {
|
||||
init_test(cx, |settings| {
|
||||
settings.defaults.completions = Some(CompletionSettings {
|
||||
words: WordsCompletionMode::Disabled,
|
||||
words_min_length: 0,
|
||||
lsp: true,
|
||||
lsp_fetch_timeout_ms: 0,
|
||||
lsp_insert_mode: LspInsertMode::Insert,
|
||||
|
||||
@@ -76,7 +76,6 @@ util.workspace = true
|
||||
workspace-hack.workspace = true
|
||||
workspace.workspace = true
|
||||
zed_actions.workspace = true
|
||||
editor_mode_setting.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
dap = { workspace = true, features = ["test-support"] }
|
||||
|
||||
@@ -7,7 +7,6 @@ use anyhow::Result;
|
||||
use collections::HashMap;
|
||||
use dap::{CompletionItem, CompletionItemType, OutputEvent};
|
||||
use editor::{Bias, CompletionProvider, Editor, EditorElement, EditorStyle, ExcerptId};
|
||||
use editor_mode_setting::EditorMode;
|
||||
use fuzzy::StringMatchCandidate;
|
||||
use gpui::{
|
||||
Action as _, AppContext, Context, Corner, Entity, FocusHandle, Focusable, HighlightStyle, Hsla,
|
||||
@@ -75,7 +74,7 @@ impl Console {
|
||||
editor.set_show_wrap_guides(false, cx);
|
||||
editor.set_show_indent_guides(false, cx);
|
||||
editor.set_show_edit_predictions(Some(false), window, cx);
|
||||
editor.set_editor_mode(EditorMode::Default, cx);
|
||||
editor.set_use_modal_editing(false);
|
||||
editor.set_soft_wrap_mode(language::language_settings::SoftWrap::EditorWidth, cx);
|
||||
editor
|
||||
});
|
||||
|
||||
@@ -14,7 +14,7 @@ use dap::{
|
||||
},
|
||||
};
|
||||
use editor::{
|
||||
ActiveDebugLine, Editor, EditorDisplayMode, MultiBuffer,
|
||||
ActiveDebugLine, Editor, EditorMode, MultiBuffer,
|
||||
actions::{self},
|
||||
};
|
||||
use gpui::{BackgroundExecutor, TestAppContext, VisualTestContext};
|
||||
@@ -1121,7 +1121,7 @@ async fn test_send_breakpoints_when_editor_has_been_saved(
|
||||
|
||||
let (editor, cx) = cx.add_window_view(|window, cx| {
|
||||
Editor::new(
|
||||
EditorDisplayMode::full(),
|
||||
EditorMode::full(),
|
||||
MultiBuffer::build_from_buffer(buffer, cx),
|
||||
Some(project.clone()),
|
||||
window,
|
||||
@@ -1290,7 +1290,7 @@ async fn test_unsetting_breakpoints_on_clear_breakpoint_action(
|
||||
|
||||
let (first_editor, cx) = cx.add_window_view(|window, cx| {
|
||||
Editor::new(
|
||||
EditorDisplayMode::full(),
|
||||
EditorMode::full(),
|
||||
MultiBuffer::build_from_buffer(first, cx),
|
||||
Some(project.clone()),
|
||||
window,
|
||||
@@ -1300,7 +1300,7 @@ async fn test_unsetting_breakpoints_on_clear_breakpoint_action(
|
||||
|
||||
let (second_editor, cx) = cx.add_window_view(|window, cx| {
|
||||
Editor::new(
|
||||
EditorDisplayMode::full(),
|
||||
EditorMode::full(),
|
||||
MultiBuffer::build_from_buffer(second, cx),
|
||||
Some(project.clone()),
|
||||
window,
|
||||
@@ -1513,7 +1513,7 @@ async fn test_active_debug_line_setting(executor: BackgroundExecutor, cx: &mut T
|
||||
|
||||
let (main_editor, cx) = cx.add_window_view(|window, cx| {
|
||||
Editor::new(
|
||||
EditorDisplayMode::full(),
|
||||
EditorMode::full(),
|
||||
MultiBuffer::build_from_buffer(main_buffer, cx),
|
||||
Some(project.clone()),
|
||||
window,
|
||||
@@ -1523,7 +1523,7 @@ async fn test_active_debug_line_setting(executor: BackgroundExecutor, cx: &mut T
|
||||
|
||||
let (second_editor, cx) = cx.add_window_view(|window, cx| {
|
||||
Editor::new(
|
||||
EditorDisplayMode::full(),
|
||||
EditorMode::full(),
|
||||
MultiBuffer::build_from_buffer(second_buffer, cx),
|
||||
Some(project.clone()),
|
||||
window,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use std::{path::Path, sync::Arc};
|
||||
|
||||
use dap::{Scope, StackFrame, Variable, requests::Variables};
|
||||
use editor::{Editor, EditorDisplayMode, MultiBuffer};
|
||||
use editor::{Editor, EditorMode, MultiBuffer};
|
||||
use gpui::{BackgroundExecutor, TestAppContext, VisualTestContext};
|
||||
use language::{Language, LanguageConfig, LanguageMatcher, tree_sitter_python, tree_sitter_rust};
|
||||
use project::{FakeFs, Project};
|
||||
@@ -226,7 +226,7 @@ fn main() {
|
||||
|
||||
let (editor, cx) = cx.add_window_view(|window, cx| {
|
||||
Editor::new(
|
||||
EditorDisplayMode::full(),
|
||||
EditorMode::full(),
|
||||
MultiBuffer::build_from_buffer(buffer, cx),
|
||||
Some(project),
|
||||
window,
|
||||
@@ -1595,7 +1595,7 @@ def process_data(untyped_param, typed_param: int, another_typed: str):
|
||||
|
||||
let (editor, cx) = cx.add_window_view(|window, cx| {
|
||||
Editor::new(
|
||||
EditorDisplayMode::full(),
|
||||
EditorMode::full(),
|
||||
MultiBuffer::build_from_buffer(buffer, cx),
|
||||
Some(project),
|
||||
window,
|
||||
@@ -2093,7 +2093,7 @@ async fn test_inline_values_util(
|
||||
|
||||
let (editor, cx) = cx.add_window_view(|window, cx| {
|
||||
Editor::new(
|
||||
EditorDisplayMode::full(),
|
||||
EditorMode::full(),
|
||||
MultiBuffer::build_from_buffer(buffer, cx),
|
||||
Some(project),
|
||||
window,
|
||||
|
||||
@@ -18,7 +18,6 @@ collections.workspace = true
|
||||
component.workspace = true
|
||||
ctor.workspace = true
|
||||
editor.workspace = true
|
||||
futures.workspace = true
|
||||
gpui.workspace = true
|
||||
indoc.workspace = true
|
||||
language.workspace = true
|
||||
|
||||
@@ -13,7 +13,6 @@ use editor::{
|
||||
DEFAULT_MULTIBUFFER_CONTEXT, Editor, EditorEvent, ExcerptRange, MultiBuffer, PathKey,
|
||||
display_map::{BlockPlacement, BlockProperties, BlockStyle, CustomBlockId},
|
||||
};
|
||||
use futures::future::join_all;
|
||||
use gpui::{
|
||||
AnyElement, AnyView, App, AsyncApp, Context, Entity, EventEmitter, FocusHandle, Focusable,
|
||||
Global, InteractiveElement, IntoElement, ParentElement, Render, SharedString, Styled,
|
||||
@@ -24,7 +23,6 @@ use language::{
|
||||
};
|
||||
use project::{
|
||||
DiagnosticSummary, Project, ProjectPath,
|
||||
lsp_store::rust_analyzer_ext::{cancel_flycheck, run_flycheck},
|
||||
project_settings::{DiagnosticSeverity, ProjectSettings},
|
||||
};
|
||||
use settings::Settings;
|
||||
@@ -79,17 +77,10 @@ pub(crate) struct ProjectDiagnosticsEditor {
|
||||
paths_to_update: BTreeSet<ProjectPath>,
|
||||
include_warnings: bool,
|
||||
update_excerpts_task: Option<Task<Result<()>>>,
|
||||
cargo_diagnostics_fetch: CargoDiagnosticsFetchState,
|
||||
diagnostic_summary_update: Task<()>,
|
||||
_subscription: Subscription,
|
||||
}
|
||||
|
||||
struct CargoDiagnosticsFetchState {
|
||||
fetch_task: Option<Task<()>>,
|
||||
cancel_task: Option<Task<()>>,
|
||||
diagnostic_sources: Arc<Vec<ProjectPath>>,
|
||||
}
|
||||
|
||||
impl EventEmitter<EditorEvent> for ProjectDiagnosticsEditor {}
|
||||
|
||||
const DIAGNOSTICS_UPDATE_DELAY: Duration = Duration::from_millis(50);
|
||||
@@ -260,11 +251,7 @@ impl ProjectDiagnosticsEditor {
|
||||
)
|
||||
});
|
||||
this.diagnostics.clear();
|
||||
this.update_all_diagnostics(false, window, cx);
|
||||
})
|
||||
.detach();
|
||||
cx.observe_release(&cx.entity(), |editor, _, cx| {
|
||||
editor.stop_cargo_diagnostics_fetch(cx);
|
||||
this.update_all_excerpts(window, cx);
|
||||
})
|
||||
.detach();
|
||||
|
||||
@@ -281,15 +268,10 @@ impl ProjectDiagnosticsEditor {
|
||||
editor,
|
||||
paths_to_update: Default::default(),
|
||||
update_excerpts_task: None,
|
||||
cargo_diagnostics_fetch: CargoDiagnosticsFetchState {
|
||||
fetch_task: None,
|
||||
cancel_task: None,
|
||||
diagnostic_sources: Arc::new(Vec::new()),
|
||||
},
|
||||
diagnostic_summary_update: Task::ready(()),
|
||||
_subscription: project_event_subscription,
|
||||
};
|
||||
this.update_all_diagnostics(true, window, cx);
|
||||
this.update_all_excerpts(window, cx);
|
||||
this
|
||||
}
|
||||
|
||||
@@ -373,20 +355,10 @@ impl ProjectDiagnosticsEditor {
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let fetch_cargo_diagnostics = ProjectSettings::get_global(cx)
|
||||
.diagnostics
|
||||
.fetch_cargo_diagnostics();
|
||||
|
||||
if fetch_cargo_diagnostics {
|
||||
if self.cargo_diagnostics_fetch.fetch_task.is_some() {
|
||||
self.stop_cargo_diagnostics_fetch(cx);
|
||||
} else {
|
||||
self.update_all_diagnostics(false, window, cx);
|
||||
}
|
||||
} else if self.update_excerpts_task.is_some() {
|
||||
if self.update_excerpts_task.is_some() {
|
||||
self.update_excerpts_task = None;
|
||||
} else {
|
||||
self.update_all_diagnostics(false, window, cx);
|
||||
self.update_all_excerpts(window, cx);
|
||||
}
|
||||
cx.notify();
|
||||
}
|
||||
@@ -404,73 +376,6 @@ impl ProjectDiagnosticsEditor {
|
||||
}
|
||||
}
|
||||
|
||||
fn update_all_diagnostics(
|
||||
&mut self,
|
||||
first_launch: bool,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let cargo_diagnostics_sources = self.cargo_diagnostics_sources(cx);
|
||||
if cargo_diagnostics_sources.is_empty() {
|
||||
self.update_all_excerpts(window, cx);
|
||||
} else if first_launch && !self.summary.is_empty() {
|
||||
self.update_all_excerpts(window, cx);
|
||||
} else {
|
||||
self.fetch_cargo_diagnostics(Arc::new(cargo_diagnostics_sources), cx);
|
||||
}
|
||||
}
|
||||
|
||||
fn fetch_cargo_diagnostics(
|
||||
&mut self,
|
||||
diagnostics_sources: Arc<Vec<ProjectPath>>,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let project = self.project.clone();
|
||||
self.cargo_diagnostics_fetch.cancel_task = None;
|
||||
self.cargo_diagnostics_fetch.fetch_task = None;
|
||||
self.cargo_diagnostics_fetch.diagnostic_sources = diagnostics_sources.clone();
|
||||
if self.cargo_diagnostics_fetch.diagnostic_sources.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
self.cargo_diagnostics_fetch.fetch_task = Some(cx.spawn(async move |editor, cx| {
|
||||
let mut fetch_tasks = Vec::new();
|
||||
for buffer_path in diagnostics_sources.iter().cloned() {
|
||||
if cx
|
||||
.update(|cx| {
|
||||
fetch_tasks.push(run_flycheck(project.clone(), buffer_path, cx));
|
||||
})
|
||||
.is_err()
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let _ = join_all(fetch_tasks).await;
|
||||
editor
|
||||
.update(cx, |editor, _| {
|
||||
editor.cargo_diagnostics_fetch.fetch_task = None;
|
||||
})
|
||||
.ok();
|
||||
}));
|
||||
}
|
||||
|
||||
fn stop_cargo_diagnostics_fetch(&mut self, cx: &mut App) {
|
||||
self.cargo_diagnostics_fetch.fetch_task = None;
|
||||
let mut cancel_gasks = Vec::new();
|
||||
for buffer_path in std::mem::take(&mut self.cargo_diagnostics_fetch.diagnostic_sources)
|
||||
.iter()
|
||||
.cloned()
|
||||
{
|
||||
cancel_gasks.push(cancel_flycheck(self.project.clone(), buffer_path, cx));
|
||||
}
|
||||
|
||||
self.cargo_diagnostics_fetch.cancel_task = Some(cx.background_spawn(async move {
|
||||
let _ = join_all(cancel_gasks).await;
|
||||
log::info!("Finished fetching cargo diagnostics");
|
||||
}));
|
||||
}
|
||||
|
||||
/// Enqueue an update of all excerpts. Updates all paths that either
|
||||
/// currently have diagnostics or are currently present in this view.
|
||||
fn update_all_excerpts(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
@@ -695,30 +600,6 @@ impl ProjectDiagnosticsEditor {
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
pub fn cargo_diagnostics_sources(&self, cx: &App) -> Vec<ProjectPath> {
|
||||
let fetch_cargo_diagnostics = ProjectSettings::get_global(cx)
|
||||
.diagnostics
|
||||
.fetch_cargo_diagnostics();
|
||||
if !fetch_cargo_diagnostics {
|
||||
return Vec::new();
|
||||
}
|
||||
self.project
|
||||
.read(cx)
|
||||
.worktrees(cx)
|
||||
.filter_map(|worktree| {
|
||||
let _cargo_toml_entry = worktree.read(cx).entry_for_path("Cargo.toml")?;
|
||||
let rust_file_entry = worktree.read(cx).entries(false, 0).find(|entry| {
|
||||
entry
|
||||
.path
|
||||
.extension()
|
||||
.and_then(|extension| extension.to_str())
|
||||
== Some("rs")
|
||||
})?;
|
||||
self.project.read(cx).path_for_entry(rust_file_entry.id, cx)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
impl Focusable for ProjectDiagnosticsEditor {
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::{ProjectDiagnosticsEditor, ToggleDiagnosticsRefresh};
|
||||
use gpui::{Context, Entity, EventEmitter, ParentElement, Render, WeakEntity, Window};
|
||||
use ui::prelude::*;
|
||||
@@ -15,26 +13,18 @@ impl Render for ToolbarControls {
|
||||
let mut include_warnings = false;
|
||||
let mut has_stale_excerpts = false;
|
||||
let mut is_updating = false;
|
||||
let cargo_diagnostics_sources = Arc::new(self.diagnostics().map_or(Vec::new(), |editor| {
|
||||
editor.read(cx).cargo_diagnostics_sources(cx)
|
||||
}));
|
||||
let fetch_cargo_diagnostics = !cargo_diagnostics_sources.is_empty();
|
||||
|
||||
if let Some(editor) = self.diagnostics() {
|
||||
let diagnostics = editor.read(cx);
|
||||
include_warnings = diagnostics.include_warnings;
|
||||
has_stale_excerpts = !diagnostics.paths_to_update.is_empty();
|
||||
is_updating = if fetch_cargo_diagnostics {
|
||||
diagnostics.cargo_diagnostics_fetch.fetch_task.is_some()
|
||||
} else {
|
||||
diagnostics.update_excerpts_task.is_some()
|
||||
|| diagnostics
|
||||
.project
|
||||
.read(cx)
|
||||
.language_servers_running_disk_based_diagnostics(cx)
|
||||
.next()
|
||||
.is_some()
|
||||
};
|
||||
is_updating = diagnostics.update_excerpts_task.is_some()
|
||||
|| diagnostics
|
||||
.project
|
||||
.read(cx)
|
||||
.language_servers_running_disk_based_diagnostics(cx)
|
||||
.next()
|
||||
.is_some();
|
||||
}
|
||||
|
||||
let tooltip = if include_warnings {
|
||||
@@ -64,7 +54,6 @@ impl Render for ToolbarControls {
|
||||
.on_click(cx.listener(move |toolbar_controls, _, _, cx| {
|
||||
if let Some(diagnostics) = toolbar_controls.diagnostics() {
|
||||
diagnostics.update(cx, |diagnostics, cx| {
|
||||
diagnostics.stop_cargo_diagnostics_fetch(cx);
|
||||
diagnostics.update_excerpts_task = None;
|
||||
cx.notify();
|
||||
});
|
||||
@@ -76,7 +65,7 @@ impl Render for ToolbarControls {
|
||||
IconButton::new("refresh-diagnostics", IconName::ArrowCircle)
|
||||
.icon_color(Color::Info)
|
||||
.shape(IconButtonShape::Square)
|
||||
.disabled(!has_stale_excerpts && !fetch_cargo_diagnostics)
|
||||
.disabled(!has_stale_excerpts)
|
||||
.tooltip(Tooltip::for_action_title(
|
||||
"Refresh diagnostics",
|
||||
&ToggleDiagnosticsRefresh,
|
||||
@@ -84,17 +73,8 @@ impl Render for ToolbarControls {
|
||||
.on_click(cx.listener({
|
||||
move |toolbar_controls, _, window, cx| {
|
||||
if let Some(diagnostics) = toolbar_controls.diagnostics() {
|
||||
let cargo_diagnostics_sources =
|
||||
Arc::clone(&cargo_diagnostics_sources);
|
||||
diagnostics.update(cx, move |diagnostics, cx| {
|
||||
if fetch_cargo_diagnostics {
|
||||
diagnostics.fetch_cargo_diagnostics(
|
||||
cargo_diagnostics_sources,
|
||||
cx,
|
||||
);
|
||||
} else {
|
||||
diagnostics.update_all_excerpts(window, cx);
|
||||
}
|
||||
diagnostics.update_all_excerpts(window, cx);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -89,9 +89,6 @@ pub trait EditPredictionProvider: 'static + Sized {
|
||||
debounce: bool,
|
||||
cx: &mut Context<Self>,
|
||||
);
|
||||
fn needs_terms_acceptance(&self, _cx: &App) -> bool {
|
||||
false
|
||||
}
|
||||
fn cycle(
|
||||
&mut self,
|
||||
buffer: Entity<Buffer>,
|
||||
@@ -124,7 +121,6 @@ pub trait EditPredictionProviderHandle {
|
||||
fn data_collection_state(&self, cx: &App) -> DataCollectionState;
|
||||
fn usage(&self, cx: &App) -> Option<EditPredictionUsage>;
|
||||
fn toggle_data_collection(&self, cx: &mut App);
|
||||
fn needs_terms_acceptance(&self, cx: &App) -> bool;
|
||||
fn is_refreshing(&self, cx: &App) -> bool;
|
||||
fn refresh(
|
||||
&self,
|
||||
@@ -196,10 +192,6 @@ where
|
||||
self.read(cx).is_enabled(buffer, cursor_position, cx)
|
||||
}
|
||||
|
||||
fn needs_terms_acceptance(&self, cx: &App) -> bool {
|
||||
self.read(cx).needs_terms_acceptance(cx)
|
||||
}
|
||||
|
||||
fn is_refreshing(&self, cx: &App) -> bool {
|
||||
self.read(cx).is_refreshing()
|
||||
}
|
||||
|
||||
@@ -242,13 +242,9 @@ impl Render for EditPredictionButton {
|
||||
IconName::ZedPredictDisabled
|
||||
};
|
||||
|
||||
if zeta::should_show_upsell_modal(&self.user_store, cx) {
|
||||
if zeta::should_show_upsell_modal() {
|
||||
let tooltip_meta = if self.user_store.read(cx).current_user().is_some() {
|
||||
if self.user_store.read(cx).has_accepted_terms_of_service() {
|
||||
"Choose a Plan"
|
||||
} else {
|
||||
"Accept the Terms of Service"
|
||||
}
|
||||
"Choose a Plan"
|
||||
} else {
|
||||
"Sign In"
|
||||
};
|
||||
|
||||
@@ -92,7 +92,6 @@ uuid.workspace = true
|
||||
workspace.workspace = true
|
||||
zed_actions.workspace = true
|
||||
workspace-hack.workspace = true
|
||||
editor_mode_setting.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
ctor.workspace = true
|
||||
|
||||
@@ -94,7 +94,6 @@ use convert_case::{Case, Casing};
|
||||
use dap::TelemetrySpawnLocation;
|
||||
use display_map::*;
|
||||
use edit_prediction::{EditPredictionProvider, EditPredictionProviderHandle};
|
||||
use editor_mode_setting::{EditorMode, EditorModeSetting};
|
||||
use editor_settings::{GoToDefinitionFallback, Minimap as MinimapSettings};
|
||||
use element::{AcceptEditPredictionBinding, LineWithInvisibles, PositionMap, layout_line};
|
||||
use futures::{
|
||||
@@ -254,7 +253,6 @@ pub type RenderDiffHunkControlsFn = Arc<
|
||||
enum ReportEditorEvent {
|
||||
Saved { auto_saved: bool },
|
||||
EditorOpened,
|
||||
ZetaTosClicked,
|
||||
Closed,
|
||||
}
|
||||
|
||||
@@ -263,7 +261,6 @@ impl ReportEditorEvent {
|
||||
match self {
|
||||
Self::Saved { .. } => "Editor Saved",
|
||||
Self::EditorOpened => "Editor Opened",
|
||||
Self::ZetaTosClicked => "Edit Prediction Provider ToS Clicked",
|
||||
Self::Closed => "Editor Closed",
|
||||
}
|
||||
}
|
||||
@@ -495,7 +492,7 @@ pub enum SelectMode {
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq, Eq, Debug)]
|
||||
pub enum EditorDisplayMode {
|
||||
pub enum EditorMode {
|
||||
SingleLine,
|
||||
AutoHeight {
|
||||
min_lines: usize,
|
||||
@@ -514,7 +511,7 @@ pub enum EditorDisplayMode {
|
||||
},
|
||||
}
|
||||
|
||||
impl EditorDisplayMode {
|
||||
impl EditorMode {
|
||||
pub fn full() -> Self {
|
||||
Self::Full {
|
||||
scale_ui_elements_with_buffer_font_size: true,
|
||||
@@ -758,7 +755,7 @@ pub enum MinimapVisibility {
|
||||
}
|
||||
|
||||
impl MinimapVisibility {
|
||||
fn for_display_mode(mode: &EditorDisplayMode, cx: &App) -> Self {
|
||||
fn for_mode(mode: &EditorMode, cx: &App) -> Self {
|
||||
if mode.is_full() {
|
||||
Self::Enabled {
|
||||
setting_configuration: EditorSettings::get_global(cx).minimap.minimap_enabled(),
|
||||
@@ -1045,7 +1042,7 @@ pub struct Editor {
|
||||
show_cursor_names: bool,
|
||||
hovered_cursors: HashMap<HoveredCursor, Task<()>>,
|
||||
pub show_local_selections: bool,
|
||||
display_mode: EditorDisplayMode,
|
||||
mode: EditorMode,
|
||||
show_breadcrumbs: bool,
|
||||
show_gutter: bool,
|
||||
show_scrollbars: ScrollbarAxes,
|
||||
@@ -1093,6 +1090,7 @@ pub struct Editor {
|
||||
autoindent_mode: Option<AutoindentMode>,
|
||||
workspace: Option<(WeakEntity<Workspace>, Option<WorkspaceId>)>,
|
||||
input_enabled: bool,
|
||||
use_modal_editing: bool,
|
||||
read_only: bool,
|
||||
leader_id: Option<CollaboratorId>,
|
||||
remote_id: Option<ViewId>,
|
||||
@@ -1180,7 +1178,6 @@ pub struct Editor {
|
||||
next_color_inlay_id: usize,
|
||||
colors: Option<LspColorData>,
|
||||
folding_newlines: Task<()>,
|
||||
editor_mode: editor_mode_setting::EditorMode,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq, Default)]
|
||||
@@ -1203,7 +1200,7 @@ impl NextScrollCursorCenterTopBottom {
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct EditorSnapshot {
|
||||
pub display_mode: EditorDisplayMode,
|
||||
pub mode: EditorMode,
|
||||
show_gutter: bool,
|
||||
show_line_numbers: Option<bool>,
|
||||
show_git_diff_gutter: Option<bool>,
|
||||
@@ -1671,13 +1668,13 @@ impl Editor {
|
||||
pub fn single_line(window: &mut Window, cx: &mut Context<Self>) -> Self {
|
||||
let buffer = cx.new(|cx| Buffer::local("", cx));
|
||||
let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
|
||||
Self::new(EditorDisplayMode::SingleLine, buffer, None, window, cx)
|
||||
Self::new(EditorMode::SingleLine, buffer, None, window, cx)
|
||||
}
|
||||
|
||||
pub fn multi_line(window: &mut Window, cx: &mut Context<Self>) -> Self {
|
||||
let buffer = cx.new(|cx| Buffer::local("", cx));
|
||||
let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
|
||||
Self::new(EditorDisplayMode::full(), buffer, None, window, cx)
|
||||
Self::new(EditorMode::full(), buffer, None, window, cx)
|
||||
}
|
||||
|
||||
pub fn auto_height(
|
||||
@@ -1689,7 +1686,7 @@ impl Editor {
|
||||
let buffer = cx.new(|cx| Buffer::local("", cx));
|
||||
let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
|
||||
Self::new(
|
||||
EditorDisplayMode::AutoHeight {
|
||||
EditorMode::AutoHeight {
|
||||
min_lines,
|
||||
max_lines: Some(max_lines),
|
||||
},
|
||||
@@ -1710,7 +1707,7 @@ impl Editor {
|
||||
let buffer = cx.new(|cx| Buffer::local("", cx));
|
||||
let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
|
||||
Self::new(
|
||||
EditorDisplayMode::AutoHeight {
|
||||
EditorMode::AutoHeight {
|
||||
min_lines,
|
||||
max_lines: None,
|
||||
},
|
||||
@@ -1728,7 +1725,7 @@ impl Editor {
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
|
||||
Self::new(EditorDisplayMode::full(), buffer, project, window, cx)
|
||||
Self::new(EditorMode::full(), buffer, project, window, cx)
|
||||
}
|
||||
|
||||
pub fn for_multibuffer(
|
||||
@@ -1737,12 +1734,12 @@ impl Editor {
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
Self::new(EditorDisplayMode::full(), buffer, project, window, cx)
|
||||
Self::new(EditorMode::full(), buffer, project, window, cx)
|
||||
}
|
||||
|
||||
pub fn clone(&self, window: &mut Window, cx: &mut Context<Self>) -> Self {
|
||||
let mut clone = Self::new(
|
||||
self.display_mode.clone(),
|
||||
self.mode.clone(),
|
||||
self.buffer.clone(),
|
||||
self.project.clone(),
|
||||
window,
|
||||
@@ -1763,17 +1760,17 @@ impl Editor {
|
||||
}
|
||||
|
||||
pub fn new(
|
||||
display_mode: EditorDisplayMode,
|
||||
mode: EditorMode,
|
||||
buffer: Entity<MultiBuffer>,
|
||||
project: Option<Entity<Project>>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
Editor::new_internal(display_mode, buffer, project, None, window, cx)
|
||||
Editor::new_internal(mode, buffer, project, None, window, cx)
|
||||
}
|
||||
|
||||
fn new_internal(
|
||||
display_mode: EditorDisplayMode,
|
||||
mode: EditorMode,
|
||||
buffer: Entity<MultiBuffer>,
|
||||
project: Option<Entity<Project>>,
|
||||
display_map: Option<Entity<DisplayMap>>,
|
||||
@@ -1781,12 +1778,12 @@ impl Editor {
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
debug_assert!(
|
||||
display_map.is_none() || display_mode.is_minimap(),
|
||||
display_map.is_none() || mode.is_minimap(),
|
||||
"Providing a display map for a new editor is only intended for the minimap and might have unintended side effects otherwise!"
|
||||
);
|
||||
|
||||
let full_mode = display_mode.is_full();
|
||||
let is_minimap = display_mode.is_minimap();
|
||||
let full_mode = mode.is_full();
|
||||
let is_minimap = mode.is_minimap();
|
||||
let diagnostics_max_severity = if full_mode {
|
||||
EditorSettings::get_global(cx)
|
||||
.diagnostics_max_severity
|
||||
@@ -1855,8 +1852,8 @@ impl Editor {
|
||||
blink_manager
|
||||
});
|
||||
|
||||
let soft_wrap_mode_override = matches!(display_mode, EditorDisplayMode::SingleLine)
|
||||
.then(|| language_settings::SoftWrap::None);
|
||||
let soft_wrap_mode_override =
|
||||
matches!(mode, EditorMode::SingleLine).then(|| language_settings::SoftWrap::None);
|
||||
|
||||
let mut project_subscriptions = Vec::new();
|
||||
if full_mode && let Some(project) = project.as_ref() {
|
||||
@@ -2035,19 +2032,15 @@ impl Editor {
|
||||
.detach();
|
||||
}
|
||||
|
||||
let show_indent_guides = if matches!(
|
||||
display_mode,
|
||||
EditorDisplayMode::SingleLine | EditorDisplayMode::Minimap { .. }
|
||||
) {
|
||||
Some(false)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let show_indent_guides =
|
||||
if matches!(mode, EditorMode::SingleLine | EditorMode::Minimap { .. }) {
|
||||
Some(false)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let breakpoint_store = match (&display_mode, project.as_ref()) {
|
||||
(EditorDisplayMode::Full { .. }, Some(project)) => {
|
||||
Some(project.read(cx).breakpoint_store())
|
||||
}
|
||||
let breakpoint_store = match (&mode, project.as_ref()) {
|
||||
(EditorMode::Full { .. }, Some(project)) => Some(project.read(cx).breakpoint_store()),
|
||||
_ => None,
|
||||
};
|
||||
|
||||
@@ -2103,8 +2096,8 @@ impl Editor {
|
||||
horizontal: full_mode,
|
||||
vertical: full_mode,
|
||||
},
|
||||
minimap_visibility: MinimapVisibility::for_display_mode(&display_mode, cx),
|
||||
offset_content: !matches!(display_mode, EditorDisplayMode::SingleLine),
|
||||
minimap_visibility: MinimapVisibility::for_mode(&mode, cx),
|
||||
offset_content: !matches!(mode, EditorMode::SingleLine),
|
||||
show_breadcrumbs: EditorSettings::get_global(cx).toolbar.breadcrumbs,
|
||||
show_gutter: full_mode,
|
||||
show_line_numbers: (!full_mode).then_some(false),
|
||||
@@ -2152,6 +2145,7 @@ impl Editor {
|
||||
collapse_matches: false,
|
||||
workspace: None,
|
||||
input_enabled: !is_minimap,
|
||||
use_modal_editing: full_mode,
|
||||
read_only: is_minimap,
|
||||
use_autoclose: true,
|
||||
use_auto_surround: true,
|
||||
@@ -2259,14 +2253,9 @@ impl Editor {
|
||||
.hide_mouse
|
||||
.unwrap_or_default(),
|
||||
change_list: ChangeList::new(),
|
||||
display_mode,
|
||||
mode,
|
||||
selection_drag_state: SelectionDragState::None,
|
||||
folding_newlines: Task::ready(()),
|
||||
editor_mode: if full_mode {
|
||||
EditorModeSetting::get_global(cx).0
|
||||
} else {
|
||||
editor_mode_setting::EditorMode::default()
|
||||
},
|
||||
};
|
||||
|
||||
if is_minimap {
|
||||
@@ -2302,7 +2291,7 @@ impl Editor {
|
||||
}
|
||||
}
|
||||
EditorEvent::Edited { .. } => {
|
||||
if !editor.editor_mode().is_modal() {
|
||||
if !vim_enabled(cx) {
|
||||
let (map, selections) = editor.selections.all_adjusted_display(cx);
|
||||
let pop_state = editor
|
||||
.change_list
|
||||
@@ -2392,7 +2381,7 @@ impl Editor {
|
||||
editor.update_lsp_data(false, None, window, cx);
|
||||
}
|
||||
|
||||
if editor.display_mode.is_full() {
|
||||
if editor.mode.is_full() {
|
||||
editor.report_editor_event(ReportEditorEvent::EditorOpened, None, cx);
|
||||
}
|
||||
|
||||
@@ -2461,18 +2450,18 @@ impl Editor {
|
||||
) -> KeyContext {
|
||||
let mut key_context = KeyContext::new_with_defaults();
|
||||
key_context.add("Editor");
|
||||
let display_mode = match self.display_mode {
|
||||
EditorDisplayMode::SingleLine => "single_line",
|
||||
EditorDisplayMode::AutoHeight { .. } => "auto_height",
|
||||
EditorDisplayMode::Minimap { .. } => "minimap",
|
||||
EditorDisplayMode::Full { .. } => "full",
|
||||
let mode = match self.mode {
|
||||
EditorMode::SingleLine => "single_line",
|
||||
EditorMode::AutoHeight { .. } => "auto_height",
|
||||
EditorMode::Minimap { .. } => "minimap",
|
||||
EditorMode::Full { .. } => "full",
|
||||
};
|
||||
|
||||
if EditorSettings::jupyter_enabled(cx) {
|
||||
key_context.add("jupyter");
|
||||
}
|
||||
|
||||
key_context.set("mode", display_mode);
|
||||
key_context.set("mode", mode);
|
||||
if self.pending_rename.is_some() {
|
||||
key_context.add("renaming");
|
||||
}
|
||||
@@ -2729,7 +2718,7 @@ impl Editor {
|
||||
.flatten();
|
||||
|
||||
EditorSnapshot {
|
||||
display_mode: self.display_mode.clone(),
|
||||
mode: self.mode.clone(),
|
||||
show_gutter: self.show_gutter,
|
||||
show_line_numbers: self.show_line_numbers,
|
||||
show_git_diff_gutter: self.show_git_diff_gutter,
|
||||
@@ -2766,12 +2755,12 @@ impl Editor {
|
||||
.excerpt_containing(self.selections.newest_anchor().head(), cx)
|
||||
}
|
||||
|
||||
pub fn display_mode(&self) -> &EditorDisplayMode {
|
||||
&self.display_mode
|
||||
pub fn mode(&self) -> &EditorMode {
|
||||
&self.mode
|
||||
}
|
||||
|
||||
pub fn set_display_mode(&mut self, mode: EditorDisplayMode) {
|
||||
self.display_mode = mode;
|
||||
pub fn set_mode(&mut self, mode: EditorMode) {
|
||||
self.mode = mode;
|
||||
}
|
||||
|
||||
pub fn collaboration_hub(&self) -> Option<&dyn CollaborationHub> {
|
||||
@@ -3003,19 +2992,12 @@ impl Editor {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn set_editor_mode(&mut self, to: editor_mode_setting::EditorMode, cx: &mut Context<Self>) {
|
||||
let from = self.editor_mode;
|
||||
if from != to {
|
||||
self.editor_mode = to;
|
||||
cx.emit(EditorEvent::EditorModeChanged {
|
||||
old_mode: from,
|
||||
new_mode: to,
|
||||
});
|
||||
}
|
||||
pub fn set_use_modal_editing(&mut self, to: bool) {
|
||||
self.use_modal_editing = to;
|
||||
}
|
||||
|
||||
pub fn editor_mode(&self) -> editor_mode_setting::EditorMode {
|
||||
self.editor_mode
|
||||
pub fn use_modal_editing(&self) -> bool {
|
||||
self.use_modal_editing
|
||||
}
|
||||
|
||||
fn selections_did_change(
|
||||
@@ -3218,7 +3200,7 @@ impl Editor {
|
||||
use text::ToOffset as _;
|
||||
use text::ToPoint as _;
|
||||
|
||||
if self.display_mode.is_minimap()
|
||||
if self.mode.is_minimap()
|
||||
|| WorkspaceSettings::get(None, cx).restore_on_startup == RestoreOnStartupBehavior::None
|
||||
{
|
||||
return;
|
||||
@@ -3901,7 +3883,7 @@ impl Editor {
|
||||
return;
|
||||
}
|
||||
|
||||
if self.display_mode.is_full()
|
||||
if self.mode.is_full()
|
||||
&& self.change_selections(Default::default(), window, cx, |s| s.try_cancel())
|
||||
{
|
||||
return;
|
||||
@@ -3944,9 +3926,7 @@ impl Editor {
|
||||
return true;
|
||||
}
|
||||
|
||||
if self.display_mode.is_full()
|
||||
&& matches!(self.active_diagnostics, ActiveDiagnostic::Group(_))
|
||||
{
|
||||
if self.mode.is_full() && matches!(self.active_diagnostics, ActiveDiagnostic::Group(_)) {
|
||||
self.dismiss_diagnostics(cx);
|
||||
return true;
|
||||
}
|
||||
@@ -5160,7 +5140,7 @@ impl Editor {
|
||||
}
|
||||
|
||||
fn refresh_inlay_hints(&mut self, reason: InlayHintRefreshReason, cx: &mut Context<Self>) {
|
||||
if self.semantics_provider.is_none() || !self.display_mode.is_full() {
|
||||
if self.semantics_provider.is_none() || !self.mode.is_full() {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -5594,6 +5574,11 @@ impl Editor {
|
||||
.as_ref()
|
||||
.is_none_or(|query| !query.chars().any(|c| c.is_digit(10)));
|
||||
|
||||
let omit_word_completions = match &query {
|
||||
Some(query) => query.chars().count() < completion_settings.words_min_length,
|
||||
None => completion_settings.words_min_length != 0,
|
||||
};
|
||||
|
||||
let (mut words, provider_responses) = match &provider {
|
||||
Some(provider) => {
|
||||
let provider_responses = provider.completions(
|
||||
@@ -5605,9 +5590,11 @@ impl Editor {
|
||||
cx,
|
||||
);
|
||||
|
||||
let words = match completion_settings.words {
|
||||
WordsCompletionMode::Disabled => Task::ready(BTreeMap::default()),
|
||||
WordsCompletionMode::Enabled | WordsCompletionMode::Fallback => cx
|
||||
let words = match (omit_word_completions, completion_settings.words) {
|
||||
(true, _) | (_, WordsCompletionMode::Disabled) => {
|
||||
Task::ready(BTreeMap::default())
|
||||
}
|
||||
(false, WordsCompletionMode::Enabled | WordsCompletionMode::Fallback) => cx
|
||||
.background_spawn(async move {
|
||||
buffer_snapshot.words_in_range(WordsQuery {
|
||||
fuzzy_contents: None,
|
||||
@@ -5619,16 +5606,20 @@ impl Editor {
|
||||
|
||||
(words, provider_responses)
|
||||
}
|
||||
None => (
|
||||
cx.background_spawn(async move {
|
||||
buffer_snapshot.words_in_range(WordsQuery {
|
||||
fuzzy_contents: None,
|
||||
range: word_search_range,
|
||||
skip_digits,
|
||||
None => {
|
||||
let words = if omit_word_completions {
|
||||
Task::ready(BTreeMap::default())
|
||||
} else {
|
||||
cx.background_spawn(async move {
|
||||
buffer_snapshot.words_in_range(WordsQuery {
|
||||
fuzzy_contents: None,
|
||||
range: word_search_range,
|
||||
skip_digits,
|
||||
})
|
||||
})
|
||||
}),
|
||||
Task::ready(Ok(Vec::new())),
|
||||
),
|
||||
};
|
||||
(words, Task::ready(Ok(Vec::new())))
|
||||
}
|
||||
};
|
||||
|
||||
let snippet_sort_order = EditorSettings::get_global(cx).snippet_sort_order;
|
||||
@@ -6837,7 +6828,7 @@ impl Editor {
|
||||
&mut self,
|
||||
cx: &mut Context<Editor>,
|
||||
) -> Option<(String, Range<Anchor>)> {
|
||||
if matches!(self.display_mode, EditorDisplayMode::SingleLine) {
|
||||
if matches!(self.mode, EditorMode::SingleLine) {
|
||||
return None;
|
||||
}
|
||||
if !EditorSettings::get_global(cx).selection_highlight {
|
||||
@@ -6936,7 +6927,7 @@ impl Editor {
|
||||
fn refresh_single_line_folds(&mut self, window: &mut Window, cx: &mut Context<Editor>) {
|
||||
struct NewlineFold;
|
||||
let type_id = std::any::TypeId::of::<NewlineFold>();
|
||||
if !self.display_mode.is_single_line() {
|
||||
if !self.mode.is_single_line() {
|
||||
return;
|
||||
}
|
||||
let snapshot = self.snapshot(window, cx);
|
||||
@@ -7162,7 +7153,7 @@ impl Editor {
|
||||
buffer_position: language::Anchor,
|
||||
cx: &App,
|
||||
) -> EditPredictionSettings {
|
||||
if !self.display_mode.is_full()
|
||||
if !self.mode.is_full()
|
||||
|| !self.show_edit_predictions_override.unwrap_or(true)
|
||||
|| self.edit_predictions_disabled_in_scope(buffer, buffer_position, cx)
|
||||
{
|
||||
@@ -8501,7 +8492,7 @@ impl Editor {
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Option<(AnyElement, gpui::Point<Pixels>)> {
|
||||
if self.display_mode().is_minimap() {
|
||||
if self.mode().is_minimap() {
|
||||
return None;
|
||||
}
|
||||
let active_edit_prediction = self.active_edit_prediction.as_ref()?;
|
||||
@@ -9187,45 +9178,6 @@ impl Editor {
|
||||
let provider = self.edit_prediction_provider.as_ref()?;
|
||||
let provider_icon = Self::get_prediction_provider_icon_name(&self.edit_prediction_provider);
|
||||
|
||||
if provider.provider.needs_terms_acceptance(cx) {
|
||||
return Some(
|
||||
h_flex()
|
||||
.min_w(min_width)
|
||||
.flex_1()
|
||||
.px_2()
|
||||
.py_1()
|
||||
.gap_3()
|
||||
.elevation_2(cx)
|
||||
.hover(|style| style.bg(cx.theme().colors().element_hover))
|
||||
.id("accept-terms")
|
||||
.cursor_pointer()
|
||||
.on_mouse_down(MouseButton::Left, |_, window, _| window.prevent_default())
|
||||
.on_click(cx.listener(|this, _event, window, cx| {
|
||||
cx.stop_propagation();
|
||||
this.report_editor_event(ReportEditorEvent::ZetaTosClicked, None, cx);
|
||||
window.dispatch_action(
|
||||
zed_actions::OpenZedPredictOnboarding.boxed_clone(),
|
||||
cx,
|
||||
);
|
||||
}))
|
||||
.child(
|
||||
h_flex()
|
||||
.flex_1()
|
||||
.gap_2()
|
||||
.child(Icon::new(provider_icon))
|
||||
.child(Label::new("Accept Terms of Service"))
|
||||
.child(div().w_full())
|
||||
.child(
|
||||
Icon::new(IconName::ArrowUpRight)
|
||||
.color(Color::Muted)
|
||||
.size(IconSize::Small),
|
||||
)
|
||||
.into_any_element(),
|
||||
)
|
||||
.into_any(),
|
||||
);
|
||||
}
|
||||
|
||||
let is_refreshing = provider.provider.is_refreshing(cx);
|
||||
|
||||
fn pending_completion_container(icon: IconName) -> Div {
|
||||
@@ -9827,6 +9779,9 @@ impl Editor {
|
||||
}
|
||||
|
||||
pub fn backspace(&mut self, _: &Backspace, window: &mut Window, cx: &mut Context<Self>) {
|
||||
if self.read_only(cx) {
|
||||
return;
|
||||
}
|
||||
self.hide_mouse_cursor(HideMouseCursorOrigin::TypingAction, cx);
|
||||
self.transact(window, cx, |this, window, cx| {
|
||||
this.select_autoclose_pair(window, cx);
|
||||
@@ -9920,6 +9875,9 @@ impl Editor {
|
||||
}
|
||||
|
||||
pub fn delete(&mut self, _: &Delete, window: &mut Window, cx: &mut Context<Self>) {
|
||||
if self.read_only(cx) {
|
||||
return;
|
||||
}
|
||||
self.hide_mouse_cursor(HideMouseCursorOrigin::TypingAction, cx);
|
||||
self.transact(window, cx, |this, window, cx| {
|
||||
this.change_selections(Default::default(), window, cx, |s| {
|
||||
@@ -9938,7 +9896,7 @@ impl Editor {
|
||||
}
|
||||
|
||||
pub fn backtab(&mut self, _: &Backtab, window: &mut Window, cx: &mut Context<Self>) {
|
||||
if self.display_mode.is_single_line() {
|
||||
if self.mode.is_single_line() {
|
||||
cx.propagate();
|
||||
return;
|
||||
}
|
||||
@@ -9951,7 +9909,7 @@ impl Editor {
|
||||
}
|
||||
|
||||
pub fn tab(&mut self, _: &Tab, window: &mut Window, cx: &mut Context<Self>) {
|
||||
if self.display_mode.is_single_line() {
|
||||
if self.mode.is_single_line() {
|
||||
cx.propagate();
|
||||
return;
|
||||
}
|
||||
@@ -10074,7 +10032,7 @@ impl Editor {
|
||||
if self.read_only(cx) {
|
||||
return;
|
||||
}
|
||||
if self.display_mode.is_single_line() {
|
||||
if self.mode.is_single_line() {
|
||||
cx.propagate();
|
||||
return;
|
||||
}
|
||||
@@ -10182,7 +10140,7 @@ impl Editor {
|
||||
if self.read_only(cx) {
|
||||
return;
|
||||
}
|
||||
if self.display_mode.is_single_line() {
|
||||
if self.mode.is_single_line() {
|
||||
cx.propagate();
|
||||
return;
|
||||
}
|
||||
@@ -10259,7 +10217,7 @@ impl Editor {
|
||||
if self.read_only(cx) {
|
||||
return;
|
||||
}
|
||||
if self.display_mode.is_single_line() {
|
||||
if self.mode.is_single_line() {
|
||||
cx.propagate();
|
||||
return;
|
||||
}
|
||||
@@ -11504,7 +11462,7 @@ impl Editor {
|
||||
|
||||
pub fn move_line_up(&mut self, _: &MoveLineUp, window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.hide_mouse_cursor(HideMouseCursorOrigin::TypingAction, cx);
|
||||
if self.display_mode.is_single_line() {
|
||||
if self.mode.is_single_line() {
|
||||
cx.propagate();
|
||||
return;
|
||||
}
|
||||
@@ -11615,7 +11573,7 @@ impl Editor {
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.hide_mouse_cursor(HideMouseCursorOrigin::TypingAction, cx);
|
||||
if self.display_mode.is_single_line() {
|
||||
if self.mode.is_single_line() {
|
||||
cx.propagate();
|
||||
return;
|
||||
}
|
||||
@@ -11767,7 +11725,7 @@ impl Editor {
|
||||
|
||||
pub fn rewrap(&mut self, _: &Rewrap, _: &mut Window, cx: &mut Context<Self>) {
|
||||
self.hide_mouse_cursor(HideMouseCursorOrigin::TypingAction, cx);
|
||||
if self.display_mode.is_single_line() {
|
||||
if self.mode.is_single_line() {
|
||||
cx.propagate();
|
||||
return;
|
||||
}
|
||||
@@ -12478,7 +12436,7 @@ impl Editor {
|
||||
return;
|
||||
}
|
||||
|
||||
if self.display_mode.is_single_line() {
|
||||
if self.mode.is_single_line() {
|
||||
cx.propagate();
|
||||
return;
|
||||
}
|
||||
@@ -12521,7 +12479,7 @@ impl Editor {
|
||||
return;
|
||||
}
|
||||
|
||||
if self.display_mode.is_single_line() {
|
||||
if self.mode.is_single_line() {
|
||||
cx.propagate();
|
||||
return;
|
||||
}
|
||||
@@ -12558,7 +12516,7 @@ impl Editor {
|
||||
return;
|
||||
}
|
||||
|
||||
if self.display_mode.is_single_line() {
|
||||
if self.mode.is_single_line() {
|
||||
cx.propagate();
|
||||
return;
|
||||
}
|
||||
@@ -12656,7 +12614,7 @@ impl Editor {
|
||||
return;
|
||||
}
|
||||
|
||||
if matches!(self.display_mode, EditorDisplayMode::SingleLine) {
|
||||
if matches!(self.mode, EditorMode::SingleLine) {
|
||||
cx.propagate();
|
||||
return;
|
||||
}
|
||||
@@ -12706,7 +12664,7 @@ impl Editor {
|
||||
pub fn move_down(&mut self, _: &MoveDown, window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.take_rename(true, window, cx);
|
||||
|
||||
if self.display_mode.is_single_line() {
|
||||
if self.mode.is_single_line() {
|
||||
cx.propagate();
|
||||
return;
|
||||
}
|
||||
@@ -12780,7 +12738,7 @@ impl Editor {
|
||||
return;
|
||||
}
|
||||
|
||||
if matches!(self.display_mode, EditorDisplayMode::SingleLine) {
|
||||
if matches!(self.mode, EditorMode::SingleLine) {
|
||||
cx.propagate();
|
||||
return;
|
||||
}
|
||||
@@ -13264,7 +13222,7 @@ impl Editor {
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
if matches!(self.display_mode, EditorDisplayMode::SingleLine) {
|
||||
if matches!(self.mode, EditorMode::SingleLine) {
|
||||
cx.propagate();
|
||||
return;
|
||||
}
|
||||
@@ -13285,7 +13243,7 @@ impl Editor {
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
if matches!(self.display_mode, EditorDisplayMode::SingleLine) {
|
||||
if matches!(self.mode, EditorMode::SingleLine) {
|
||||
cx.propagate();
|
||||
return;
|
||||
}
|
||||
@@ -13306,7 +13264,7 @@ impl Editor {
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
if matches!(self.display_mode, EditorDisplayMode::SingleLine) {
|
||||
if matches!(self.mode, EditorMode::SingleLine) {
|
||||
cx.propagate();
|
||||
return;
|
||||
}
|
||||
@@ -13327,7 +13285,7 @@ impl Editor {
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
if matches!(self.display_mode, EditorDisplayMode::SingleLine) {
|
||||
if matches!(self.mode, EditorMode::SingleLine) {
|
||||
cx.propagate();
|
||||
return;
|
||||
}
|
||||
@@ -13348,7 +13306,7 @@ impl Editor {
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
if matches!(self.display_mode, EditorDisplayMode::SingleLine) {
|
||||
if matches!(self.mode, EditorMode::SingleLine) {
|
||||
cx.propagate();
|
||||
return;
|
||||
}
|
||||
@@ -13373,7 +13331,7 @@ impl Editor {
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
if matches!(self.display_mode, EditorDisplayMode::SingleLine) {
|
||||
if matches!(self.mode, EditorMode::SingleLine) {
|
||||
cx.propagate();
|
||||
return;
|
||||
}
|
||||
@@ -13398,7 +13356,7 @@ impl Editor {
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
if matches!(self.display_mode, EditorDisplayMode::SingleLine) {
|
||||
if matches!(self.mode, EditorMode::SingleLine) {
|
||||
cx.propagate();
|
||||
return;
|
||||
}
|
||||
@@ -13423,7 +13381,7 @@ impl Editor {
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
if matches!(self.display_mode, EditorDisplayMode::SingleLine) {
|
||||
if matches!(self.mode, EditorMode::SingleLine) {
|
||||
cx.propagate();
|
||||
return;
|
||||
}
|
||||
@@ -13448,7 +13406,7 @@ impl Editor {
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
if matches!(self.display_mode, EditorDisplayMode::SingleLine) {
|
||||
if matches!(self.mode, EditorMode::SingleLine) {
|
||||
cx.propagate();
|
||||
return;
|
||||
}
|
||||
@@ -13469,7 +13427,7 @@ impl Editor {
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
if matches!(self.display_mode, EditorDisplayMode::SingleLine) {
|
||||
if matches!(self.mode, EditorMode::SingleLine) {
|
||||
cx.propagate();
|
||||
return;
|
||||
}
|
||||
@@ -13490,7 +13448,7 @@ impl Editor {
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
if matches!(self.display_mode, EditorDisplayMode::SingleLine) {
|
||||
if matches!(self.mode, EditorMode::SingleLine) {
|
||||
cx.propagate();
|
||||
return;
|
||||
}
|
||||
@@ -13511,7 +13469,7 @@ impl Editor {
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
if matches!(self.display_mode, EditorDisplayMode::SingleLine) {
|
||||
if matches!(self.mode, EditorMode::SingleLine) {
|
||||
cx.propagate();
|
||||
return;
|
||||
}
|
||||
@@ -13532,7 +13490,7 @@ impl Editor {
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
if matches!(self.display_mode, EditorDisplayMode::SingleLine) {
|
||||
if matches!(self.mode, EditorMode::SingleLine) {
|
||||
cx.propagate();
|
||||
return;
|
||||
}
|
||||
@@ -13557,7 +13515,7 @@ impl Editor {
|
||||
}
|
||||
|
||||
pub fn move_to_end(&mut self, _: &MoveToEnd, window: &mut Window, cx: &mut Context<Self>) {
|
||||
if matches!(self.display_mode, EditorDisplayMode::SingleLine) {
|
||||
if matches!(self.mode, EditorMode::SingleLine) {
|
||||
cx.propagate();
|
||||
return;
|
||||
}
|
||||
@@ -14606,7 +14564,7 @@ impl Editor {
|
||||
let advance_downwards = action.advance_downwards
|
||||
&& selections_on_single_row
|
||||
&& !selections_selecting
|
||||
&& !matches!(this.display_mode, EditorDisplayMode::SingleLine);
|
||||
&& !matches!(this.mode, EditorMode::SingleLine);
|
||||
|
||||
if advance_downwards {
|
||||
let snapshot = this.buffer.read(cx).snapshot(cx);
|
||||
@@ -16931,7 +16889,7 @@ impl Editor {
|
||||
}
|
||||
|
||||
pub fn diagnostics_enabled(&self) -> bool {
|
||||
self.diagnostics_enabled && self.display_mode.is_full()
|
||||
self.diagnostics_enabled && self.mode.is_full()
|
||||
}
|
||||
|
||||
pub fn inline_diagnostics_enabled(&self) -> bool {
|
||||
@@ -17091,7 +17049,7 @@ impl Editor {
|
||||
window: &Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Option<()> {
|
||||
if !self.display_mode().is_full() {
|
||||
if !self.mode().is_full() {
|
||||
return None;
|
||||
}
|
||||
let pull_diagnostics_settings = ProjectSettings::get_global(cx)
|
||||
@@ -18373,7 +18331,7 @@ impl Editor {
|
||||
const MINIMAP_FONT_WEIGHT: gpui::FontWeight = gpui::FontWeight::BLACK;
|
||||
|
||||
let mut minimap = Editor::new_internal(
|
||||
EditorDisplayMode::Minimap {
|
||||
EditorMode::Minimap {
|
||||
parent: cx.weak_entity(),
|
||||
},
|
||||
self.buffer.clone(),
|
||||
@@ -18475,7 +18433,7 @@ impl Editor {
|
||||
// We intentionally do not inform the display map about the minimap style
|
||||
// so that wrapping is not recalculated and stays consistent for the editor
|
||||
// and its linked minimap.
|
||||
if !self.display_mode.is_minimap() {
|
||||
if !self.mode.is_minimap() {
|
||||
let rem_size = window.rem_size();
|
||||
self.display_map.update(cx, |map, cx| {
|
||||
map.set_font(
|
||||
@@ -19021,9 +18979,7 @@ impl Editor {
|
||||
}
|
||||
|
||||
pub fn render_git_blame_gutter(&self, cx: &App) -> bool {
|
||||
!self.display_mode().is_minimap()
|
||||
&& self.show_git_blame_gutter
|
||||
&& self.has_blame_entries(cx)
|
||||
!self.mode().is_minimap() && self.show_git_blame_gutter && self.has_blame_entries(cx)
|
||||
}
|
||||
|
||||
pub fn render_git_blame_inline(&self, window: &Window, cx: &App) -> bool {
|
||||
@@ -20294,9 +20250,9 @@ impl Editor {
|
||||
|
||||
let project_settings = ProjectSettings::get_global(cx);
|
||||
self.serialize_dirty_buffers =
|
||||
!self.display_mode.is_minimap() && project_settings.session.restore_unsaved_buffers;
|
||||
!self.mode.is_minimap() && project_settings.session.restore_unsaved_buffers;
|
||||
|
||||
if self.display_mode.is_full() {
|
||||
if self.mode.is_full() {
|
||||
let show_inline_diagnostics = project_settings.diagnostics.inline.enabled;
|
||||
let inline_blame_enabled = project_settings.git.inline_blame_enabled();
|
||||
if self.show_inline_diagnostics != show_inline_diagnostics {
|
||||
@@ -20314,7 +20270,7 @@ impl Editor {
|
||||
!= minimap_settings.minimap_enabled()
|
||||
{
|
||||
self.set_minimap_visibility(
|
||||
MinimapVisibility::for_display_mode(self.display_mode(), cx),
|
||||
MinimapVisibility::for_mode(self.mode(), cx),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
@@ -20663,7 +20619,7 @@ impl Editor {
|
||||
.and_then(|e| e.to_str())
|
||||
.map(|a| a.to_string()));
|
||||
|
||||
let vim_mode = self.editor_mode.is_modal();
|
||||
let vim_mode = vim_enabled(cx);
|
||||
|
||||
let edit_predictions_provider = all_language_settings(file, cx).edit_predictions.provider;
|
||||
let copilot_enabled = edit_predictions_provider
|
||||
@@ -21147,7 +21103,7 @@ impl Editor {
|
||||
}
|
||||
|
||||
pub fn register_addon<T: Addon>(&mut self, instance: T) {
|
||||
if self.display_mode.is_minimap() {
|
||||
if self.mode.is_minimap() {
|
||||
return;
|
||||
}
|
||||
self.addons
|
||||
@@ -21200,7 +21156,7 @@ impl Editor {
|
||||
cx: &mut Context<Editor>,
|
||||
) {
|
||||
if self.is_singleton(cx)
|
||||
&& !self.display_mode.is_minimap()
|
||||
&& !self.mode.is_minimap()
|
||||
&& WorkspaceSettings::get(None, cx).restore_on_startup != RestoreOnStartupBehavior::None
|
||||
{
|
||||
let buffer_snapshot = OnceCell::new();
|
||||
@@ -21254,6 +21210,13 @@ impl Editor {
|
||||
}
|
||||
}
|
||||
|
||||
fn vim_enabled(cx: &App) -> bool {
|
||||
cx.global::<SettingsStore>()
|
||||
.raw_user_settings()
|
||||
.get("vim_mode")
|
||||
== Some(&serde_json::Value::Bool(true))
|
||||
}
|
||||
|
||||
fn process_completion_for_edit(
|
||||
completion: &Completion,
|
||||
intent: CompletionIntent,
|
||||
@@ -22910,10 +22873,6 @@ pub enum EditorEvent {
|
||||
anchor: Anchor,
|
||||
is_deactivate: bool,
|
||||
},
|
||||
EditorModeChanged {
|
||||
new_mode: EditorMode,
|
||||
old_mode: EditorMode,
|
||||
},
|
||||
}
|
||||
|
||||
impl EventEmitter<EditorEvent> for Editor {}
|
||||
@@ -22928,8 +22887,8 @@ impl Render for Editor {
|
||||
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let settings = ThemeSettings::get_global(cx);
|
||||
|
||||
let mut text_style = match self.display_mode {
|
||||
EditorDisplayMode::SingleLine | EditorDisplayMode::AutoHeight { .. } => TextStyle {
|
||||
let mut text_style = match self.mode {
|
||||
EditorMode::SingleLine | EditorMode::AutoHeight { .. } => TextStyle {
|
||||
color: cx.theme().colors().editor_foreground,
|
||||
font_family: settings.ui_font.family.clone(),
|
||||
font_features: settings.ui_font.features.clone(),
|
||||
@@ -22939,7 +22898,7 @@ impl Render for Editor {
|
||||
line_height: relative(settings.buffer_line_height.value()),
|
||||
..Default::default()
|
||||
},
|
||||
EditorDisplayMode::Full { .. } | EditorDisplayMode::Minimap { .. } => TextStyle {
|
||||
EditorMode::Full { .. } | EditorMode::Minimap { .. } => TextStyle {
|
||||
color: cx.theme().colors().editor_foreground,
|
||||
font_family: settings.buffer_font.family.clone(),
|
||||
font_features: settings.buffer_font.features.clone(),
|
||||
@@ -22954,11 +22913,11 @@ impl Render for Editor {
|
||||
text_style.refine(text_style_refinement)
|
||||
}
|
||||
|
||||
let background = match self.display_mode {
|
||||
EditorDisplayMode::SingleLine => cx.theme().system().transparent,
|
||||
EditorDisplayMode::AutoHeight { .. } => cx.theme().system().transparent,
|
||||
EditorDisplayMode::Full { .. } => cx.theme().colors().editor_background,
|
||||
EditorDisplayMode::Minimap { .. } => cx.theme().colors().editor_background.opacity(0.7),
|
||||
let background = match self.mode {
|
||||
EditorMode::SingleLine => cx.theme().system().transparent,
|
||||
EditorMode::AutoHeight { .. } => cx.theme().system().transparent,
|
||||
EditorMode::Full { .. } => cx.theme().colors().editor_background,
|
||||
EditorMode::Minimap { .. } => cx.theme().colors().editor_background.opacity(0.7),
|
||||
};
|
||||
|
||||
EditorElement::new(
|
||||
@@ -23643,7 +23602,7 @@ impl BreakpointPromptEditor {
|
||||
|
||||
let prompt = cx.new(|cx| {
|
||||
let mut prompt = Editor::new(
|
||||
EditorDisplayMode::AutoHeight {
|
||||
EditorMode::AutoHeight {
|
||||
min_lines: 1,
|
||||
max_lines: Some(Self::MAX_LINES as usize),
|
||||
},
|
||||
|
||||
@@ -57,7 +57,9 @@ use util::{
|
||||
use workspace::{
|
||||
CloseActiveItem, CloseAllItems, CloseOtherItems, MoveItemToPaneInDirection, NavigationEntry,
|
||||
OpenOptions, ViewId,
|
||||
invalid_buffer_view::InvalidBufferView,
|
||||
item::{FollowEvent, FollowableItem, Item, ItemHandle, SaveOptions},
|
||||
register_project_item,
|
||||
};
|
||||
|
||||
#[gpui::test]
|
||||
@@ -10075,7 +10077,7 @@ async fn test_multibuffer_format_during_save(cx: &mut TestAppContext) {
|
||||
});
|
||||
let multi_buffer_editor = cx.new_window_entity(|window, cx| {
|
||||
Editor::new(
|
||||
EditorDisplayMode::full(),
|
||||
EditorMode::full(),
|
||||
multi_buffer,
|
||||
Some(project.clone()),
|
||||
window,
|
||||
@@ -10252,7 +10254,7 @@ async fn test_autosave_with_dirty_buffers(cx: &mut TestAppContext) {
|
||||
|
||||
let editor = cx.new_window_entity(|window, cx| {
|
||||
Editor::new(
|
||||
EditorDisplayMode::full(),
|
||||
EditorMode::full(),
|
||||
multi_buffer,
|
||||
Some(project.clone()),
|
||||
window,
|
||||
@@ -12237,6 +12239,7 @@ async fn test_completion_mode(cx: &mut TestAppContext) {
|
||||
settings.defaults.completions = Some(CompletionSettings {
|
||||
lsp_insert_mode,
|
||||
words: WordsCompletionMode::Disabled,
|
||||
words_min_length: 0,
|
||||
lsp: true,
|
||||
lsp_fetch_timeout_ms: 0,
|
||||
});
|
||||
@@ -12295,6 +12298,7 @@ async fn test_completion_with_mode_specified_by_action(cx: &mut TestAppContext)
|
||||
update_test_language_settings(&mut cx, |settings| {
|
||||
settings.defaults.completions = Some(CompletionSettings {
|
||||
words: WordsCompletionMode::Disabled,
|
||||
words_min_length: 0,
|
||||
// set the opposite here to ensure that the action is overriding the default behavior
|
||||
lsp_insert_mode: LspInsertMode::Insert,
|
||||
lsp: true,
|
||||
@@ -12331,6 +12335,7 @@ async fn test_completion_with_mode_specified_by_action(cx: &mut TestAppContext)
|
||||
update_test_language_settings(&mut cx, |settings| {
|
||||
settings.defaults.completions = Some(CompletionSettings {
|
||||
words: WordsCompletionMode::Disabled,
|
||||
words_min_length: 0,
|
||||
// set the opposite here to ensure that the action is overriding the default behavior
|
||||
lsp_insert_mode: LspInsertMode::Replace,
|
||||
lsp: true,
|
||||
@@ -12646,7 +12651,7 @@ async fn test_completion_in_multibuffer_with_replace_range(cx: &mut TestAppConte
|
||||
.update(cx, |_, window, cx| {
|
||||
cx.new(|cx| {
|
||||
Editor::new(
|
||||
EditorDisplayMode::Full {
|
||||
EditorMode::Full {
|
||||
scale_ui_elements_with_buffer_font_size: false,
|
||||
show_active_line_background: false,
|
||||
sized_by_content: false,
|
||||
@@ -13072,6 +13077,7 @@ async fn test_word_completion(cx: &mut TestAppContext) {
|
||||
init_test(cx, |language_settings| {
|
||||
language_settings.defaults.completions = Some(CompletionSettings {
|
||||
words: WordsCompletionMode::Fallback,
|
||||
words_min_length: 0,
|
||||
lsp: true,
|
||||
lsp_fetch_timeout_ms: 10,
|
||||
lsp_insert_mode: LspInsertMode::Insert,
|
||||
@@ -13168,6 +13174,7 @@ async fn test_word_completions_do_not_duplicate_lsp_ones(cx: &mut TestAppContext
|
||||
init_test(cx, |language_settings| {
|
||||
language_settings.defaults.completions = Some(CompletionSettings {
|
||||
words: WordsCompletionMode::Enabled,
|
||||
words_min_length: 0,
|
||||
lsp: true,
|
||||
lsp_fetch_timeout_ms: 0,
|
||||
lsp_insert_mode: LspInsertMode::Insert,
|
||||
@@ -13231,6 +13238,7 @@ async fn test_word_completions_continue_on_typing(cx: &mut TestAppContext) {
|
||||
init_test(cx, |language_settings| {
|
||||
language_settings.defaults.completions = Some(CompletionSettings {
|
||||
words: WordsCompletionMode::Disabled,
|
||||
words_min_length: 0,
|
||||
lsp: true,
|
||||
lsp_fetch_timeout_ms: 0,
|
||||
lsp_insert_mode: LspInsertMode::Insert,
|
||||
@@ -13304,6 +13312,7 @@ async fn test_word_completions_usually_skip_digits(cx: &mut TestAppContext) {
|
||||
init_test(cx, |language_settings| {
|
||||
language_settings.defaults.completions = Some(CompletionSettings {
|
||||
words: WordsCompletionMode::Fallback,
|
||||
words_min_length: 0,
|
||||
lsp: false,
|
||||
lsp_fetch_timeout_ms: 0,
|
||||
lsp_insert_mode: LspInsertMode::Insert,
|
||||
@@ -13361,6 +13370,56 @@ async fn test_word_completions_usually_skip_digits(cx: &mut TestAppContext) {
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_word_completions_do_not_show_before_threshold(cx: &mut TestAppContext) {
|
||||
init_test(cx, |language_settings| {
|
||||
language_settings.defaults.completions = Some(CompletionSettings {
|
||||
words: WordsCompletionMode::Enabled,
|
||||
words_min_length: 3,
|
||||
lsp: true,
|
||||
lsp_fetch_timeout_ms: 0,
|
||||
lsp_insert_mode: LspInsertMode::Insert,
|
||||
});
|
||||
});
|
||||
|
||||
let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
|
||||
cx.set_state(indoc! {"ˇ
|
||||
wow
|
||||
wowen
|
||||
wowser
|
||||
"});
|
||||
cx.simulate_keystroke("w");
|
||||
cx.executor().run_until_parked();
|
||||
cx.update_editor(|editor, _, _| {
|
||||
if editor.context_menu.borrow_mut().is_some() {
|
||||
panic!(
|
||||
"expected completion menu to be hidden, as words completion threshold is not met"
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
cx.simulate_keystroke("o");
|
||||
cx.executor().run_until_parked();
|
||||
cx.update_editor(|editor, _, _| {
|
||||
if editor.context_menu.borrow_mut().is_some() {
|
||||
panic!(
|
||||
"expected completion menu to be hidden, as words completion threshold is not met still"
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
cx.simulate_keystroke("w");
|
||||
cx.executor().run_until_parked();
|
||||
cx.update_editor(|editor, _, _| {
|
||||
if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
|
||||
{
|
||||
assert_eq!(completion_menu_entries(menu), &["wowen", "wowser"], "After word completion threshold is met, matching words should be shown, excluding the already typed word");
|
||||
} else {
|
||||
panic!("expected completion menu to be open after the word completions threshold is met");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn gen_text_edit(params: &CompletionParams, text: &str) -> Option<lsp::CompletionTextEdit> {
|
||||
let position = || lsp::Position {
|
||||
line: params.text_document_position.position.line,
|
||||
@@ -17433,7 +17492,7 @@ async fn test_multibuffer_in_navigation_history(cx: &mut TestAppContext) {
|
||||
let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
|
||||
let multi_buffer_editor = cx.new_window_entity(|window, cx| {
|
||||
Editor::new(
|
||||
EditorDisplayMode::full(),
|
||||
EditorMode::full(),
|
||||
multi_buffer,
|
||||
Some(project.clone()),
|
||||
window,
|
||||
@@ -17900,9 +17959,8 @@ async fn test_toggle_diff_expand_in_multi_buffer(cx: &mut TestAppContext) {
|
||||
multibuffer
|
||||
});
|
||||
|
||||
let editor = cx.add_window(|window, cx| {
|
||||
Editor::new(EditorDisplayMode::full(), multi_buffer, None, window, cx)
|
||||
});
|
||||
let editor =
|
||||
cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
|
||||
editor
|
||||
.update(cx, |editor, _window, cx| {
|
||||
for (buffer, diff_base) in [
|
||||
@@ -18012,9 +18070,8 @@ async fn test_expand_diff_hunk_at_excerpt_boundary(cx: &mut TestAppContext) {
|
||||
multibuffer
|
||||
});
|
||||
|
||||
let editor = cx.add_window(|window, cx| {
|
||||
Editor::new(EditorDisplayMode::full(), multi_buffer, None, window, cx)
|
||||
});
|
||||
let editor =
|
||||
cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
|
||||
editor
|
||||
.update(cx, |editor, _window, cx| {
|
||||
let diff = cx.new(|cx| BufferDiff::new_with_base_text(base, &buffer, cx));
|
||||
@@ -19695,13 +19752,7 @@ async fn test_display_diff_hunks(cx: &mut TestAppContext) {
|
||||
});
|
||||
|
||||
let editor = cx.add_window(|window, cx| {
|
||||
Editor::new(
|
||||
EditorDisplayMode::full(),
|
||||
multibuffer,
|
||||
Some(project),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
Editor::new(EditorMode::full(), multibuffer, Some(project), window, cx)
|
||||
});
|
||||
cx.run_until_parked();
|
||||
|
||||
@@ -20213,7 +20264,7 @@ async fn test_find_enclosing_node_with_task(cx: &mut TestAppContext) {
|
||||
|
||||
let editor = cx.new_window_entity(|window, cx| {
|
||||
Editor::new(
|
||||
EditorDisplayMode::full(),
|
||||
EditorMode::full(),
|
||||
multi_buffer,
|
||||
Some(project.clone()),
|
||||
window,
|
||||
@@ -20340,7 +20391,7 @@ async fn test_folding_buffers(cx: &mut TestAppContext) {
|
||||
});
|
||||
let multi_buffer_editor = cx.new_window_entity(|window, cx| {
|
||||
Editor::new(
|
||||
EditorDisplayMode::full(),
|
||||
EditorMode::full(),
|
||||
multi_buffer.clone(),
|
||||
Some(project.clone()),
|
||||
window,
|
||||
@@ -20497,7 +20548,7 @@ async fn test_folding_buffers_with_one_excerpt(cx: &mut TestAppContext) {
|
||||
|
||||
let multi_buffer_editor = cx.new_window_entity(|window, cx| {
|
||||
Editor::new(
|
||||
EditorDisplayMode::full(),
|
||||
EditorMode::full(),
|
||||
multi_buffer,
|
||||
Some(project.clone()),
|
||||
window,
|
||||
@@ -20615,7 +20666,7 @@ async fn test_folding_buffer_when_multibuffer_has_only_one_excerpt(cx: &mut Test
|
||||
});
|
||||
let multi_buffer_editor = cx.new_window_entity(|window, cx| {
|
||||
Editor::new(
|
||||
EditorDisplayMode::full(),
|
||||
EditorMode::full(),
|
||||
multi_buffer,
|
||||
Some(project.clone()),
|
||||
window,
|
||||
@@ -20667,13 +20718,7 @@ async fn test_multi_buffer_navigation_with_folded_buffers(cx: &mut TestAppContex
|
||||
],
|
||||
cx,
|
||||
);
|
||||
let mut editor = Editor::new(
|
||||
EditorDisplayMode::full(),
|
||||
multi_buffer.clone(),
|
||||
None,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
let mut editor = Editor::new(EditorMode::full(), multi_buffer.clone(), None, window, cx);
|
||||
|
||||
let buffer_ids = multi_buffer.read(cx).excerpt_buffer_ids();
|
||||
// fold all but the second buffer, so that we test navigating between two
|
||||
@@ -20985,7 +21030,7 @@ async fn assert_highlighted_edits(
|
||||
) {
|
||||
let window = cx.add_window(|window, cx| {
|
||||
let buffer = MultiBuffer::build_simple(text, cx);
|
||||
Editor::new(EditorDisplayMode::full(), buffer, None, window, cx)
|
||||
Editor::new(EditorMode::full(), buffer, None, window, cx)
|
||||
});
|
||||
let cx = &mut VisualTestContext::from_window(*window, cx);
|
||||
|
||||
@@ -21145,7 +21190,7 @@ async fn test_breakpoint_toggling(cx: &mut TestAppContext) {
|
||||
|
||||
let (editor, cx) = cx.add_window_view(|window, cx| {
|
||||
Editor::new(
|
||||
EditorDisplayMode::full(),
|
||||
EditorMode::full(),
|
||||
MultiBuffer::build_from_buffer(buffer, cx),
|
||||
Some(project.clone()),
|
||||
window,
|
||||
@@ -21259,7 +21304,7 @@ async fn test_log_breakpoint_editing(cx: &mut TestAppContext) {
|
||||
|
||||
let (editor, cx) = cx.add_window_view(|window, cx| {
|
||||
Editor::new(
|
||||
EditorDisplayMode::full(),
|
||||
EditorMode::full(),
|
||||
MultiBuffer::build_from_buffer(buffer, cx),
|
||||
Some(project.clone()),
|
||||
window,
|
||||
@@ -21429,7 +21474,7 @@ async fn test_breakpoint_enabling_and_disabling(cx: &mut TestAppContext) {
|
||||
|
||||
let (editor, cx) = cx.add_window_view(|window, cx| {
|
||||
Editor::new(
|
||||
EditorDisplayMode::full(),
|
||||
EditorMode::full(),
|
||||
MultiBuffer::build_from_buffer(buffer, cx),
|
||||
Some(project.clone()),
|
||||
window,
|
||||
@@ -22387,7 +22432,7 @@ async fn test_hide_mouse_context_menu_on_modal_opened(cx: &mut TestAppContext) {
|
||||
let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
|
||||
let editor = cx.new_window_entity(|window, cx| {
|
||||
Editor::new(
|
||||
EditorDisplayMode::full(),
|
||||
EditorMode::full(),
|
||||
buffer,
|
||||
Some(project.clone()),
|
||||
window,
|
||||
@@ -22670,7 +22715,7 @@ async fn test_invisible_worktree_servers(cx: &mut TestAppContext) {
|
||||
.await
|
||||
.unwrap();
|
||||
pane.update_in(cx, |pane, window, cx| {
|
||||
pane.navigate_backward(window, cx);
|
||||
pane.navigate_backward(&Default::default(), window, cx);
|
||||
});
|
||||
cx.run_until_parked();
|
||||
pane.update(cx, |pane, cx| {
|
||||
@@ -24257,7 +24302,7 @@ async fn test_document_colors(cx: &mut TestAppContext) {
|
||||
workspace
|
||||
.update(cx, |workspace, window, cx| {
|
||||
workspace.active_pane().update(cx, |pane, cx| {
|
||||
pane.navigate_backward(window, cx);
|
||||
pane.navigate_backward(&Default::default(), window, cx);
|
||||
})
|
||||
})
|
||||
.unwrap();
|
||||
@@ -24305,6 +24350,41 @@ async fn test_newline_replacement_in_single_line(cx: &mut TestAppContext) {
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_non_utf_8_opens(cx: &mut TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
|
||||
cx.update(|cx| {
|
||||
register_project_item::<Editor>(cx);
|
||||
});
|
||||
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
fs.insert_tree("/root1", json!({})).await;
|
||||
fs.insert_file("/root1/one.pdf", vec![0xff, 0xfe, 0xfd])
|
||||
.await;
|
||||
|
||||
let project = Project::test(fs, ["/root1".as_ref()], cx).await;
|
||||
let (workspace, cx) =
|
||||
cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
|
||||
|
||||
let worktree_id = project.update(cx, |project, cx| {
|
||||
project.worktrees(cx).next().unwrap().read(cx).id()
|
||||
});
|
||||
|
||||
let handle = workspace
|
||||
.update_in(cx, |workspace, window, cx| {
|
||||
let project_path = (worktree_id, "one.pdf");
|
||||
workspace.open_path(project_path, None, true, window, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
handle.to_any().entity_type(),
|
||||
TypeId::of::<InvalidBufferView>()
|
||||
);
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
fn extract_color_inlays(editor: &Editor, cx: &App) -> Vec<Rgba> {
|
||||
editor
|
||||
|
||||
@@ -3,7 +3,7 @@ use crate::{
|
||||
CodeActionSource, ColumnarMode, ConflictsOurs, ConflictsOursMarker, ConflictsOuter,
|
||||
ConflictsTheirs, ConflictsTheirsMarker, ContextMenuPlacement, CursorShape, CustomBlockId,
|
||||
DisplayDiffHunk, DisplayPoint, DisplayRow, DocumentHighlightRead, DocumentHighlightWrite,
|
||||
EditDisplayMode, EditPrediction, Editor, EditorDisplayMode, EditorSettings, EditorSnapshot,
|
||||
EditDisplayMode, EditPrediction, Editor, EditorMode, EditorSettings, EditorSnapshot,
|
||||
EditorStyle, FILE_HEADER_HEIGHT, FocusedBlock, GutterDimensions, HalfPageDown, HalfPageUp,
|
||||
HandleInput, HoveredCursor, InlayHintRefreshReason, JumpData, LineDown, LineHighlight, LineUp,
|
||||
MAX_LINE_LEN, MINIMAP_FONT_SIZE, MULTI_BUFFER_EXCERPT_HEADER_HEIGHT, OpenExcerpts, PageDown,
|
||||
@@ -74,6 +74,7 @@ use std::{
|
||||
fmt::{self, Write},
|
||||
iter, mem,
|
||||
ops::{Deref, Range},
|
||||
path::{self, Path},
|
||||
rc::Rc,
|
||||
sync::Arc,
|
||||
time::{Duration, Instant},
|
||||
@@ -89,8 +90,8 @@ use unicode_segmentation::UnicodeSegmentation;
|
||||
use util::post_inc;
|
||||
use util::{RangeExt, ResultExt, debug_panic};
|
||||
use workspace::{
|
||||
CollaboratorId, OpenInTerminal, OpenTerminal, RevealInProjectPanel, Workspace, item::Item,
|
||||
notifications::NotifyTaskExt,
|
||||
CollaboratorId, ItemSettings, OpenInTerminal, OpenTerminal, RevealInProjectPanel, Workspace,
|
||||
item::Item, notifications::NotifyTaskExt,
|
||||
};
|
||||
|
||||
/// Determines what kinds of highlights should be applied to a lines background.
|
||||
@@ -1816,7 +1817,7 @@ impl EditorElement {
|
||||
}
|
||||
}
|
||||
|
||||
if !snapshot.display_mode.is_full()
|
||||
if !snapshot.mode.is_full()
|
||||
|| minimap_width.is_zero()
|
||||
|| matches!(
|
||||
minimap_settings.show,
|
||||
@@ -3212,7 +3213,7 @@ impl EditorElement {
|
||||
cx: &mut App,
|
||||
) -> Vec<Option<AnyElement>> {
|
||||
let include_fold_statuses = EditorSettings::get_global(cx).gutter.folds
|
||||
&& snapshot.display_mode.is_full()
|
||||
&& snapshot.mode.is_full()
|
||||
&& self.editor.read(cx).is_singleton(cx);
|
||||
if include_fold_statuses {
|
||||
row_infos
|
||||
@@ -3314,7 +3315,7 @@ impl EditorElement {
|
||||
style,
|
||||
MAX_LINE_LEN,
|
||||
rows.len(),
|
||||
&snapshot.display_mode,
|
||||
&snapshot.mode,
|
||||
editor_width,
|
||||
is_row_soft_wrapped,
|
||||
window,
|
||||
@@ -3602,171 +3603,187 @@ impl EditorElement {
|
||||
let focus_handle = editor.focus_handle(cx);
|
||||
let colors = cx.theme().colors();
|
||||
|
||||
let header =
|
||||
div()
|
||||
.p_1()
|
||||
.w_full()
|
||||
.h(FILE_HEADER_HEIGHT as f32 * window.line_height())
|
||||
.child(
|
||||
h_flex()
|
||||
.size_full()
|
||||
.gap_2()
|
||||
.flex_basis(Length::Definite(DefiniteLength::Fraction(0.667)))
|
||||
.pl_0p5()
|
||||
.pr_5()
|
||||
.rounded_sm()
|
||||
.when(is_sticky, |el| el.shadow_md())
|
||||
.border_1()
|
||||
.map(|div| {
|
||||
let border_color = if is_selected
|
||||
&& is_folded
|
||||
&& focus_handle.contains_focused(window, cx)
|
||||
{
|
||||
colors.border_focused
|
||||
} else {
|
||||
colors.border
|
||||
};
|
||||
div.border_color(border_color)
|
||||
})
|
||||
.bg(colors.editor_subheader_background)
|
||||
.hover(|style| style.bg(colors.element_hover))
|
||||
.map(|header| {
|
||||
let editor = self.editor.clone();
|
||||
let buffer_id = for_excerpt.buffer_id;
|
||||
let toggle_chevron_icon =
|
||||
FileIcons::get_chevron_icon(!is_folded, cx).map(Icon::from_path);
|
||||
header.child(
|
||||
div()
|
||||
.hover(|style| style.bg(colors.element_selected))
|
||||
.rounded_xs()
|
||||
.child(
|
||||
ButtonLike::new("toggle-buffer-fold")
|
||||
.style(ui::ButtonStyle::Transparent)
|
||||
.height(px(28.).into())
|
||||
.width(px(28.))
|
||||
.children(toggle_chevron_icon)
|
||||
.tooltip({
|
||||
let focus_handle = focus_handle.clone();
|
||||
move |window, cx| {
|
||||
Tooltip::with_meta_in(
|
||||
"Toggle Excerpt Fold",
|
||||
Some(&ToggleFold),
|
||||
"Alt+click to toggle all",
|
||||
&focus_handle,
|
||||
let header = div()
|
||||
.p_1()
|
||||
.w_full()
|
||||
.h(FILE_HEADER_HEIGHT as f32 * window.line_height())
|
||||
.child(
|
||||
h_flex()
|
||||
.size_full()
|
||||
.gap_2()
|
||||
.flex_basis(Length::Definite(DefiniteLength::Fraction(0.667)))
|
||||
.pl_0p5()
|
||||
.pr_5()
|
||||
.rounded_sm()
|
||||
.when(is_sticky, |el| el.shadow_md())
|
||||
.border_1()
|
||||
.map(|div| {
|
||||
let border_color = if is_selected
|
||||
&& is_folded
|
||||
&& focus_handle.contains_focused(window, cx)
|
||||
{
|
||||
colors.border_focused
|
||||
} else {
|
||||
colors.border
|
||||
};
|
||||
div.border_color(border_color)
|
||||
})
|
||||
.bg(colors.editor_subheader_background)
|
||||
.hover(|style| style.bg(colors.element_hover))
|
||||
.map(|header| {
|
||||
let editor = self.editor.clone();
|
||||
let buffer_id = for_excerpt.buffer_id;
|
||||
let toggle_chevron_icon =
|
||||
FileIcons::get_chevron_icon(!is_folded, cx).map(Icon::from_path);
|
||||
header.child(
|
||||
div()
|
||||
.hover(|style| style.bg(colors.element_selected))
|
||||
.rounded_xs()
|
||||
.child(
|
||||
ButtonLike::new("toggle-buffer-fold")
|
||||
.style(ui::ButtonStyle::Transparent)
|
||||
.height(px(28.).into())
|
||||
.width(px(28.))
|
||||
.children(toggle_chevron_icon)
|
||||
.tooltip({
|
||||
let focus_handle = focus_handle.clone();
|
||||
move |window, cx| {
|
||||
Tooltip::with_meta_in(
|
||||
"Toggle Excerpt Fold",
|
||||
Some(&ToggleFold),
|
||||
"Alt+click to toggle all",
|
||||
&focus_handle,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
}
|
||||
})
|
||||
.on_click(move |event, window, cx| {
|
||||
if event.modifiers().alt {
|
||||
// Alt+click toggles all buffers
|
||||
editor.update(cx, |editor, cx| {
|
||||
editor.toggle_fold_all(
|
||||
&ToggleFoldAll,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
}
|
||||
})
|
||||
.on_click(move |event, window, cx| {
|
||||
if event.modifiers().alt {
|
||||
// Alt+click toggles all buffers
|
||||
);
|
||||
});
|
||||
} else {
|
||||
// Regular click toggles single buffer
|
||||
if is_folded {
|
||||
editor.update(cx, |editor, cx| {
|
||||
editor.toggle_fold_all(
|
||||
&ToggleFoldAll,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
editor.unfold_buffer(buffer_id, cx);
|
||||
});
|
||||
} else {
|
||||
// Regular click toggles single buffer
|
||||
if is_folded {
|
||||
editor.update(cx, |editor, cx| {
|
||||
editor.unfold_buffer(buffer_id, cx);
|
||||
});
|
||||
} else {
|
||||
editor.update(cx, |editor, cx| {
|
||||
editor.fold_buffer(buffer_id, cx);
|
||||
});
|
||||
}
|
||||
editor.update(cx, |editor, cx| {
|
||||
editor.fold_buffer(buffer_id, cx);
|
||||
});
|
||||
}
|
||||
}),
|
||||
),
|
||||
)
|
||||
})
|
||||
.children(
|
||||
editor
|
||||
.addons
|
||||
.values()
|
||||
.filter_map(|addon| {
|
||||
addon.render_buffer_header_controls(for_excerpt, window, cx)
|
||||
})
|
||||
.take(1),
|
||||
}
|
||||
}),
|
||||
),
|
||||
)
|
||||
.children(indicator)
|
||||
.child(
|
||||
h_flex()
|
||||
.cursor_pointer()
|
||||
.id("path header block")
|
||||
.size_full()
|
||||
.justify_between()
|
||||
.overflow_hidden()
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.child(
|
||||
Label::new(
|
||||
filename
|
||||
.map(SharedString::from)
|
||||
.unwrap_or_else(|| "untitled".into()),
|
||||
)
|
||||
.single_line()
|
||||
.when_some(file_status, |el, status| {
|
||||
el.color(if status.is_conflicted() {
|
||||
Color::Conflict
|
||||
} else if status.is_modified() {
|
||||
Color::Modified
|
||||
} else if status.is_deleted() {
|
||||
Color::Disabled
|
||||
} else {
|
||||
Color::Created
|
||||
})
|
||||
.when(status.is_deleted(), |el| el.strikethrough())
|
||||
}),
|
||||
)
|
||||
.when_some(parent_path, |then, path| {
|
||||
then.child(div().child(path).text_color(
|
||||
if file_status.is_some_and(FileStatus::is_deleted) {
|
||||
colors.text_disabled
|
||||
} else {
|
||||
colors.text_muted
|
||||
})
|
||||
.children(
|
||||
editor
|
||||
.addons
|
||||
.values()
|
||||
.filter_map(|addon| {
|
||||
addon.render_buffer_header_controls(for_excerpt, window, cx)
|
||||
})
|
||||
.take(1),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.size(Pixels(12.0))
|
||||
.justify_center()
|
||||
.children(indicator),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.cursor_pointer()
|
||||
.id("path header block")
|
||||
.size_full()
|
||||
.justify_between()
|
||||
.overflow_hidden()
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.map(|path_header| {
|
||||
let filename = filename
|
||||
.map(SharedString::from)
|
||||
.unwrap_or_else(|| "untitled".into());
|
||||
|
||||
path_header
|
||||
.when(ItemSettings::get_global(cx).file_icons, |el| {
|
||||
let path = path::Path::new(filename.as_str());
|
||||
let icon = FileIcons::get_icon(path, cx)
|
||||
.unwrap_or_default();
|
||||
let icon =
|
||||
Icon::from_path(icon).color(Color::Muted);
|
||||
el.child(icon)
|
||||
})
|
||||
.child(Label::new(filename).single_line().when_some(
|
||||
file_status,
|
||||
|el, status| {
|
||||
el.color(if status.is_conflicted() {
|
||||
Color::Conflict
|
||||
} else if status.is_modified() {
|
||||
Color::Modified
|
||||
} else if status.is_deleted() {
|
||||
Color::Disabled
|
||||
} else {
|
||||
Color::Created
|
||||
})
|
||||
.when(status.is_deleted(), |el| {
|
||||
el.strikethrough()
|
||||
})
|
||||
},
|
||||
))
|
||||
}),
|
||||
)
|
||||
.when(
|
||||
can_open_excerpts && is_selected && relative_path.is_some(),
|
||||
|el| {
|
||||
el.child(
|
||||
h_flex()
|
||||
.id("jump-to-file-button")
|
||||
.gap_2p5()
|
||||
.child(Label::new("Jump To File"))
|
||||
.children(
|
||||
KeyBinding::for_action_in(
|
||||
&OpenExcerpts,
|
||||
&focus_handle,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
.map(|binding| binding.into_any_element()),
|
||||
),
|
||||
)
|
||||
},
|
||||
)
|
||||
.on_mouse_down(MouseButton::Left, |_, _, cx| cx.stop_propagation())
|
||||
.on_click(window.listener_for(&self.editor, {
|
||||
move |editor, e: &ClickEvent, window, cx| {
|
||||
editor.open_excerpts_common(
|
||||
Some(jump_data.clone()),
|
||||
e.modifiers().secondary(),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
})),
|
||||
),
|
||||
);
|
||||
})
|
||||
.when_some(parent_path, |then, path| {
|
||||
then.child(div().child(path).text_color(
|
||||
if file_status.is_some_and(FileStatus::is_deleted) {
|
||||
colors.text_disabled
|
||||
} else {
|
||||
colors.text_muted
|
||||
},
|
||||
))
|
||||
}),
|
||||
)
|
||||
.when(
|
||||
can_open_excerpts && is_selected && relative_path.is_some(),
|
||||
|el| {
|
||||
el.child(
|
||||
h_flex()
|
||||
.id("jump-to-file-button")
|
||||
.gap_2p5()
|
||||
.child(Label::new("Jump To File"))
|
||||
.children(
|
||||
KeyBinding::for_action_in(
|
||||
&OpenExcerpts,
|
||||
&focus_handle,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
.map(|binding| binding.into_any_element()),
|
||||
),
|
||||
)
|
||||
},
|
||||
)
|
||||
.on_mouse_down(MouseButton::Left, |_, _, cx| cx.stop_propagation())
|
||||
.on_click(window.listener_for(&self.editor, {
|
||||
move |editor, e: &ClickEvent, window, cx| {
|
||||
editor.open_excerpts_common(
|
||||
Some(jump_data.clone()),
|
||||
e.modifiers().secondary(),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
})),
|
||||
),
|
||||
);
|
||||
|
||||
let file = for_excerpt.buffer.file().cloned();
|
||||
let editor = self.editor.clone();
|
||||
@@ -3782,25 +3799,31 @@ impl EditorElement {
|
||||
&& let Some(worktree) =
|
||||
project.read(cx).worktree_for_id(file.worktree_id(cx), cx)
|
||||
{
|
||||
let worktree = worktree.read(cx);
|
||||
let relative_path = file.path();
|
||||
let entry_for_path = worktree.read(cx).entry_for_path(relative_path);
|
||||
let abs_path = entry_for_path.and_then(|e| e.canonical_path.as_deref());
|
||||
let has_relative_path =
|
||||
worktree.read(cx).root_entry().is_some_and(Entry::is_dir);
|
||||
let entry_for_path = worktree.entry_for_path(relative_path);
|
||||
let abs_path = entry_for_path.map(|e| {
|
||||
e.canonical_path.as_deref().map_or_else(
|
||||
|| worktree.abs_path().join(relative_path),
|
||||
Path::to_path_buf,
|
||||
)
|
||||
});
|
||||
let has_relative_path = worktree.root_entry().is_some_and(Entry::is_dir);
|
||||
|
||||
let parent_abs_path =
|
||||
abs_path.and_then(|abs_path| Some(abs_path.parent()?.to_path_buf()));
|
||||
let parent_abs_path = abs_path
|
||||
.as_ref()
|
||||
.and_then(|abs_path| Some(abs_path.parent()?.to_path_buf()));
|
||||
let relative_path = has_relative_path
|
||||
.then_some(relative_path)
|
||||
.map(ToOwned::to_owned);
|
||||
|
||||
let visible_in_project_panel =
|
||||
relative_path.is_some() && worktree.read(cx).is_visible();
|
||||
relative_path.is_some() && worktree.is_visible();
|
||||
let reveal_in_project_panel = entry_for_path
|
||||
.filter(|_| visible_in_project_panel)
|
||||
.map(|entry| entry.id);
|
||||
menu = menu
|
||||
.when_some(abs_path.map(ToOwned::to_owned), |menu, abs_path| {
|
||||
.when_some(abs_path, |menu, abs_path| {
|
||||
menu.entry(
|
||||
"Copy Path",
|
||||
Some(Box::new(zed_actions::workspace::CopyPath)),
|
||||
@@ -5282,14 +5305,14 @@ impl EditorElement {
|
||||
|
||||
if matches!(
|
||||
layout.mode,
|
||||
EditorDisplayMode::Full { .. } | EditorDisplayMode::Minimap { .. }
|
||||
EditorMode::Full { .. } | EditorMode::Minimap { .. }
|
||||
) {
|
||||
let show_active_line_background = match layout.mode {
|
||||
EditorDisplayMode::Full {
|
||||
EditorMode::Full {
|
||||
show_active_line_background,
|
||||
..
|
||||
} => show_active_line_background,
|
||||
EditorDisplayMode::Minimap { .. } => true,
|
||||
EditorMode::Minimap { .. } => true,
|
||||
_ => false,
|
||||
};
|
||||
let mut active_rows = layout.active_rows.iter().peekable();
|
||||
@@ -7311,7 +7334,7 @@ impl LineWithInvisibles {
|
||||
editor_style: &EditorStyle,
|
||||
max_line_len: usize,
|
||||
max_line_count: usize,
|
||||
editor_mode: &EditorDisplayMode,
|
||||
editor_mode: &EditorMode,
|
||||
text_width: Pixels,
|
||||
is_row_soft_wrapped: impl Copy + Fn(usize) -> bool,
|
||||
window: &mut Window,
|
||||
@@ -7839,12 +7862,12 @@ impl EditorElement {
|
||||
///
|
||||
/// This allows UI elements to scale based on the `buffer_font_size`.
|
||||
fn rem_size(&self, cx: &mut App) -> Option<Pixels> {
|
||||
match self.editor.read(cx).display_mode {
|
||||
EditorDisplayMode::Full {
|
||||
match self.editor.read(cx).mode {
|
||||
EditorMode::Full {
|
||||
scale_ui_elements_with_buffer_font_size: true,
|
||||
..
|
||||
}
|
||||
| EditorDisplayMode::Minimap { .. } => {
|
||||
| EditorMode::Minimap { .. } => {
|
||||
let buffer_font_size = self.style.text.font_size;
|
||||
match buffer_font_size {
|
||||
AbsoluteLength::Pixels(pixels) => {
|
||||
@@ -7877,7 +7900,7 @@ impl EditorElement {
|
||||
}
|
||||
|
||||
fn editor_with_selections(&self, cx: &App) -> Option<Entity<Editor>> {
|
||||
if let EditorDisplayMode::Minimap { parent } = self.editor.read(cx).display_mode() {
|
||||
if let EditorMode::Minimap { parent } = self.editor.read(cx).mode() {
|
||||
parent.upgrade()
|
||||
} else {
|
||||
Some(self.editor.clone())
|
||||
@@ -7909,8 +7932,8 @@ impl Element for EditorElement {
|
||||
self.editor.update(cx, |editor, cx| {
|
||||
editor.set_style(self.style.clone(), window, cx);
|
||||
|
||||
let layout_id = match editor.display_mode {
|
||||
EditorDisplayMode::SingleLine => {
|
||||
let layout_id = match editor.mode {
|
||||
EditorMode::SingleLine => {
|
||||
let rem_size = window.rem_size();
|
||||
let height = self.style.text.line_height_in_pixels(rem_size);
|
||||
let mut style = Style::default();
|
||||
@@ -7918,7 +7941,7 @@ impl Element for EditorElement {
|
||||
style.size.width = relative(1.).into();
|
||||
window.request_layout(style, None, cx)
|
||||
}
|
||||
EditorDisplayMode::AutoHeight {
|
||||
EditorMode::AutoHeight {
|
||||
min_lines,
|
||||
max_lines,
|
||||
} => {
|
||||
@@ -7945,13 +7968,13 @@ impl Element for EditorElement {
|
||||
},
|
||||
)
|
||||
}
|
||||
EditorDisplayMode::Minimap { .. } => {
|
||||
EditorMode::Minimap { .. } => {
|
||||
let mut style = Style::default();
|
||||
style.size.width = relative(1.).into();
|
||||
style.size.height = relative(1.).into();
|
||||
window.request_layout(style, None, cx)
|
||||
}
|
||||
EditorDisplayMode::Full {
|
||||
EditorMode::Full {
|
||||
sized_by_content, ..
|
||||
} => {
|
||||
let mut style = Style::default();
|
||||
@@ -7990,7 +8013,7 @@ impl Element for EditorElement {
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let is_minimap = self.editor.read(cx).display_mode.is_minimap();
|
||||
let is_minimap = self.editor.read(cx).mode.is_minimap();
|
||||
|
||||
if !is_minimap {
|
||||
let focus_handle = self.editor.focus_handle(cx);
|
||||
@@ -8065,9 +8088,8 @@ impl Element for EditorElement {
|
||||
editor.set_visible_column_count(editor_width / em_advance);
|
||||
|
||||
if matches!(
|
||||
editor.display_mode,
|
||||
EditorDisplayMode::AutoHeight { .. }
|
||||
| EditorDisplayMode::Minimap { .. }
|
||||
editor.mode,
|
||||
EditorMode::AutoHeight { .. } | EditorMode::Minimap { .. }
|
||||
) {
|
||||
snapshot
|
||||
} else {
|
||||
@@ -8113,10 +8135,10 @@ impl Element for EditorElement {
|
||||
|
||||
// The max scroll position for the top of the window
|
||||
let max_scroll_top = if matches!(
|
||||
snapshot.display_mode,
|
||||
EditorDisplayMode::SingleLine
|
||||
| EditorDisplayMode::AutoHeight { .. }
|
||||
| EditorDisplayMode::Full {
|
||||
snapshot.mode,
|
||||
EditorMode::SingleLine
|
||||
| EditorMode::AutoHeight { .. }
|
||||
| EditorMode::Full {
|
||||
sized_by_content: true,
|
||||
..
|
||||
}
|
||||
@@ -8981,7 +9003,7 @@ impl Element for EditorElement {
|
||||
None,
|
||||
);
|
||||
|
||||
let mode = snapshot.display_mode.clone();
|
||||
let mode = snapshot.mode.clone();
|
||||
|
||||
let (diff_hunk_controls, diff_hunk_control_bounds) = if is_read_only {
|
||||
(vec![], vec![])
|
||||
@@ -9210,7 +9232,7 @@ pub struct EditorLayout {
|
||||
content_origin: gpui::Point<Pixels>,
|
||||
scrollbars_layout: Option<EditorScrollbars>,
|
||||
minimap: Option<MinimapLayout>,
|
||||
mode: EditorDisplayMode,
|
||||
mode: EditorMode,
|
||||
wrap_guides: SmallVec<[(Pixels, bool); 2]>,
|
||||
indent_guides: Option<Vec<IndentGuideLayout>>,
|
||||
visible_display_row_range: Range<DisplayRow>,
|
||||
@@ -9789,7 +9811,7 @@ pub fn layout_line(
|
||||
style,
|
||||
MAX_LINE_LEN,
|
||||
1,
|
||||
&snapshot.display_mode,
|
||||
&snapshot.mode,
|
||||
text_width,
|
||||
is_row_soft_wrapped,
|
||||
window,
|
||||
@@ -10198,7 +10220,7 @@ mod tests {
|
||||
let window = cx.add_window(|window, cx| {
|
||||
let buffer = MultiBuffer::build_simple(&"a ".to_string().repeat(100), cx);
|
||||
let mut editor = Editor::new(
|
||||
EditorDisplayMode::AutoHeight {
|
||||
EditorMode::AutoHeight {
|
||||
min_lines: 1,
|
||||
max_lines: None,
|
||||
},
|
||||
@@ -10234,7 +10256,7 @@ mod tests {
|
||||
|
||||
let window = cx.add_window(|window, cx| {
|
||||
let buffer = MultiBuffer::build_simple(&"a ".to_string().repeat(100), cx);
|
||||
let mut editor = Editor::new(EditorDisplayMode::full(), buffer, None, window, cx);
|
||||
let mut editor = Editor::new(EditorMode::full(), buffer, None, window, cx);
|
||||
editor.set_soft_wrap_mode(language_settings::SoftWrap::EditorWidth, cx);
|
||||
editor
|
||||
});
|
||||
@@ -10261,7 +10283,7 @@ mod tests {
|
||||
init_test(cx, |_| {});
|
||||
let window = cx.add_window(|window, cx| {
|
||||
let buffer = MultiBuffer::build_simple(&sample_text(6, 6, 'a'), cx);
|
||||
Editor::new(EditorDisplayMode::full(), buffer, None, window, cx)
|
||||
Editor::new(EditorMode::full(), buffer, None, window, cx)
|
||||
});
|
||||
|
||||
let editor = window.root(cx).unwrap();
|
||||
@@ -10362,7 +10384,7 @@ mod tests {
|
||||
|
||||
let window = cx.add_window(|window, cx| {
|
||||
let buffer = MultiBuffer::build_simple(&(sample_text(6, 6, 'a') + "\n"), cx);
|
||||
Editor::new(EditorDisplayMode::full(), buffer, None, window, cx)
|
||||
Editor::new(EditorMode::full(), buffer, None, window, cx)
|
||||
});
|
||||
let cx = &mut VisualTestContext::from_window(*window, cx);
|
||||
let editor = window.root(cx).unwrap();
|
||||
@@ -10433,7 +10455,7 @@ mod tests {
|
||||
|
||||
let window = cx.add_window(|window, cx| {
|
||||
let buffer = MultiBuffer::build_simple("", cx);
|
||||
Editor::new(EditorDisplayMode::full(), buffer, None, window, cx)
|
||||
Editor::new(EditorMode::full(), buffer, None, window, cx)
|
||||
});
|
||||
let cx = &mut VisualTestContext::from_window(*window, cx);
|
||||
let editor = window.root(cx).unwrap();
|
||||
@@ -10519,7 +10541,7 @@ mod tests {
|
||||
|
||||
let actual_invisibles = collect_invisibles_from_new_editor(
|
||||
cx,
|
||||
EditorDisplayMode::full(),
|
||||
EditorMode::full(),
|
||||
input_text,
|
||||
px(500.0),
|
||||
show_line_numbers,
|
||||
@@ -10537,8 +10559,8 @@ mod tests {
|
||||
});
|
||||
|
||||
for editor_mode_without_invisibles in [
|
||||
EditorDisplayMode::SingleLine,
|
||||
EditorDisplayMode::AutoHeight {
|
||||
EditorMode::SingleLine,
|
||||
EditorMode::AutoHeight {
|
||||
min_lines: 1,
|
||||
max_lines: Some(100),
|
||||
},
|
||||
@@ -10616,7 +10638,7 @@ mod tests {
|
||||
|
||||
let actual_invisibles = collect_invisibles_from_new_editor(
|
||||
cx,
|
||||
EditorDisplayMode::full(),
|
||||
EditorMode::full(),
|
||||
&input_text,
|
||||
px(editor_width),
|
||||
show_line_numbers,
|
||||
@@ -10655,7 +10677,7 @@ mod tests {
|
||||
|
||||
fn collect_invisibles_from_new_editor(
|
||||
cx: &mut TestAppContext,
|
||||
editor_mode: EditorDisplayMode,
|
||||
editor_mode: EditorMode,
|
||||
input_text: &str,
|
||||
editor_width: Pixels,
|
||||
show_line_numbers: bool,
|
||||
|
||||
@@ -42,6 +42,7 @@ use ui::{IconDecorationKind, prelude::*};
|
||||
use util::{ResultExt, TryFutureExt, paths::PathExt};
|
||||
use workspace::{
|
||||
CollaboratorId, ItemId, ItemNavHistory, ToolbarItemLocation, ViewId, Workspace, WorkspaceId,
|
||||
invalid_buffer_view::InvalidBufferView,
|
||||
item::{FollowableItem, Item, ItemEvent, ProjectItem, SaveOptions},
|
||||
searchable::{Direction, SearchEvent, SearchableItem, SearchableItemHandle},
|
||||
};
|
||||
@@ -1267,7 +1268,7 @@ impl SerializableItem for Editor {
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Option<Task<Result<()>>> {
|
||||
if self.display_mode.is_minimap() {
|
||||
if self.mode.is_minimap() {
|
||||
return None;
|
||||
}
|
||||
let mut serialize_dirty_buffers = self.serialize_dirty_buffers;
|
||||
@@ -1401,6 +1402,16 @@ impl ProjectItem for Editor {
|
||||
|
||||
editor
|
||||
}
|
||||
|
||||
fn for_broken_project_item(
|
||||
abs_path: PathBuf,
|
||||
is_local: bool,
|
||||
e: &anyhow::Error,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Option<InvalidBufferView> {
|
||||
Some(InvalidBufferView::new(abs_path, is_local, e, window, cx))
|
||||
}
|
||||
}
|
||||
|
||||
fn clip_ranges<'a>(
|
||||
@@ -1424,7 +1435,7 @@ impl Editor {
|
||||
cx: &mut Context<Self>,
|
||||
write: impl for<'a> FnOnce(&'a mut RestorationData) + 'static,
|
||||
) {
|
||||
if self.display_mode.is_minimap() || !WorkspaceSettings::get(None, cx).restore_on_file_reopen {
|
||||
if self.mode.is_minimap() || !WorkspaceSettings::get(None, cx).restore_on_file_reopen {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -150,7 +150,7 @@ impl Editor {
|
||||
_: &Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
if !self.display_mode().is_full() {
|
||||
if !self.mode().is_full() {
|
||||
return;
|
||||
}
|
||||
let Some(project) = self.project.clone() else {
|
||||
|
||||
@@ -153,7 +153,7 @@ pub fn deploy_context_menu(
|
||||
}
|
||||
|
||||
// Don't show context menu for inline editors
|
||||
if !editor.display_mode().is_full() {
|
||||
if !editor.mode().is_full() {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -26,6 +26,17 @@ fn is_rust_language(language: &Language) -> bool {
|
||||
}
|
||||
|
||||
pub fn apply_related_actions(editor: &Entity<Editor>, window: &mut Window, cx: &mut App) {
|
||||
if editor.read(cx).project().is_some_and(|project| {
|
||||
project
|
||||
.read(cx)
|
||||
.language_server_statuses(cx)
|
||||
.any(|(_, status)| status.name == RUST_ANALYZER_NAME)
|
||||
}) {
|
||||
register_action(editor, window, cancel_flycheck_action);
|
||||
register_action(editor, window, run_flycheck_action);
|
||||
register_action(editor, window, clear_flycheck_action);
|
||||
}
|
||||
|
||||
if editor
|
||||
.read(cx)
|
||||
.buffer()
|
||||
@@ -38,9 +49,6 @@ pub fn apply_related_actions(editor: &Entity<Editor>, window: &mut Window, cx: &
|
||||
register_action(editor, window, go_to_parent_module);
|
||||
register_action(editor, window, expand_macro_recursively);
|
||||
register_action(editor, window, open_docs);
|
||||
register_action(editor, window, cancel_flycheck_action);
|
||||
register_action(editor, window, run_flycheck_action);
|
||||
register_action(editor, window, clear_flycheck_action);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -309,7 +317,7 @@ fn cancel_flycheck_action(
|
||||
let Some(project) = &editor.project else {
|
||||
return;
|
||||
};
|
||||
let Some(buffer_id) = editor
|
||||
let buffer_id = editor
|
||||
.selections
|
||||
.disjoint_anchors()
|
||||
.iter()
|
||||
@@ -321,10 +329,7 @@ fn cancel_flycheck_action(
|
||||
.read(cx)
|
||||
.entry_id(cx)?;
|
||||
project.path_for_entry(entry_id, cx)
|
||||
})
|
||||
else {
|
||||
return;
|
||||
};
|
||||
});
|
||||
cancel_flycheck(project.clone(), buffer_id, cx).detach_and_log_err(cx);
|
||||
}
|
||||
|
||||
@@ -337,7 +342,7 @@ fn run_flycheck_action(
|
||||
let Some(project) = &editor.project else {
|
||||
return;
|
||||
};
|
||||
let Some(buffer_id) = editor
|
||||
let buffer_id = editor
|
||||
.selections
|
||||
.disjoint_anchors()
|
||||
.iter()
|
||||
@@ -349,10 +354,7 @@ fn run_flycheck_action(
|
||||
.read(cx)
|
||||
.entry_id(cx)?;
|
||||
project.path_for_entry(entry_id, cx)
|
||||
})
|
||||
else {
|
||||
return;
|
||||
};
|
||||
});
|
||||
run_flycheck(project.clone(), buffer_id, cx).detach_and_log_err(cx);
|
||||
}
|
||||
|
||||
@@ -365,7 +367,7 @@ fn clear_flycheck_action(
|
||||
let Some(project) = &editor.project else {
|
||||
return;
|
||||
};
|
||||
let Some(buffer_id) = editor
|
||||
let buffer_id = editor
|
||||
.selections
|
||||
.disjoint_anchors()
|
||||
.iter()
|
||||
@@ -377,9 +379,6 @@ fn clear_flycheck_action(
|
||||
.read(cx)
|
||||
.entry_id(cx)?;
|
||||
project.path_for_entry(entry_id, cx)
|
||||
})
|
||||
else {
|
||||
return;
|
||||
};
|
||||
});
|
||||
clear_flycheck(project.clone(), buffer_id, cx).detach_and_log_err(cx);
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ pub(crate) mod scroll_amount;
|
||||
|
||||
use crate::editor_settings::ScrollBeyondLastLine;
|
||||
use crate::{
|
||||
Anchor, DisplayPoint, DisplayRow, Editor, EditorDisplayMode, EditorEvent, EditorSettings,
|
||||
Anchor, DisplayPoint, DisplayRow, Editor, EditorEvent, EditorMode, EditorSettings,
|
||||
InlayHintRefreshReason, MultiBufferSnapshot, RowExt, ToPoint,
|
||||
display_map::{DisplaySnapshot, ToDisplayPoint},
|
||||
hover_popover::hide_hover,
|
||||
@@ -675,7 +675,7 @@ impl Editor {
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
if matches!(self.display_mode, EditorDisplayMode::SingleLine) {
|
||||
if matches!(self.mode, EditorMode::SingleLine) {
|
||||
cx.propagate();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use super::Axis;
|
||||
use crate::{
|
||||
Autoscroll, Editor, EditorDisplayMode, NextScreen, NextScrollCursorCenterTopBottom,
|
||||
Autoscroll, Editor, EditorMode, NextScreen, NextScrollCursorCenterTopBottom,
|
||||
SCROLL_CENTER_TOP_BOTTOM_DEBOUNCE_TIMEOUT, ScrollCursorBottom, ScrollCursorCenter,
|
||||
ScrollCursorCenterTopBottom, ScrollCursorTop, display_map::DisplayRow,
|
||||
};
|
||||
@@ -16,7 +16,7 @@ impl Editor {
|
||||
return;
|
||||
}
|
||||
|
||||
if matches!(self.display_mode, EditorDisplayMode::SingleLine) {
|
||||
if matches!(self.mode, EditorMode::SingleLine) {
|
||||
cx.propagate();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use crate::{
|
||||
DisplayRow, Editor, EditorDisplayMode, LineWithInvisibles, RowExt, SelectionEffects,
|
||||
DisplayRow, Editor, EditorMode, LineWithInvisibles, RowExt, SelectionEffects,
|
||||
display_map::ToDisplayPoint, scroll::WasScrolled,
|
||||
};
|
||||
use gpui::{Bounds, Context, Pixels, Window, px};
|
||||
@@ -184,7 +184,7 @@ impl Editor {
|
||||
}
|
||||
}
|
||||
|
||||
let margin = if matches!(self.display_mode, EditorDisplayMode::AutoHeight { .. }) {
|
||||
let margin = if matches!(self.mode, EditorMode::AutoHeight { .. }) {
|
||||
0.
|
||||
} else {
|
||||
((visible_lines - (target_bottom - target_top)) / 2.0).floor()
|
||||
|
||||
@@ -5,7 +5,7 @@ use std::{rc::Rc, sync::LazyLock};
|
||||
|
||||
pub use crate::rust_analyzer_ext::expand_macro_recursively;
|
||||
use crate::{
|
||||
DisplayPoint, Editor, EditorDisplayMode, FoldPlaceholder, MultiBuffer, SelectionEffects,
|
||||
DisplayPoint, Editor, EditorMode, FoldPlaceholder, MultiBuffer, SelectionEffects,
|
||||
display_map::{
|
||||
Block, BlockPlacement, CustomBlockId, DisplayMap, DisplayRow, DisplaySnapshot,
|
||||
ToDisplayPoint,
|
||||
@@ -121,7 +121,7 @@ pub(crate) fn build_editor(
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Editor>,
|
||||
) -> Editor {
|
||||
Editor::new(EditorDisplayMode::full(), buffer, None, window, cx)
|
||||
Editor::new(EditorMode::full(), buffer, None, window, cx)
|
||||
}
|
||||
|
||||
pub(crate) fn build_editor_with_project(
|
||||
@@ -130,7 +130,7 @@ pub(crate) fn build_editor_with_project(
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Editor>,
|
||||
) -> Editor {
|
||||
Editor::new(EditorDisplayMode::full(), buffer, Some(project), window, cx)
|
||||
Editor::new(EditorMode::full(), buffer, Some(project), window, cx)
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
|
||||
@@ -1,200 +0,0 @@
|
||||
//! Contains the [`VimModeSetting`] and [`HelixModeSetting`] used to enable/disable Vim and Helix modes.
|
||||
//!
|
||||
//! This is in its own crate as we want other crates to be able to enable or
|
||||
//! disable Vim/Helix modes without having to depend on the `vim` crate in its
|
||||
//! entirety.
|
||||
|
||||
use anyhow::Result;
|
||||
use gpui::App;
|
||||
use schemars::{JsonSchema, Schema, json_schema};
|
||||
|
||||
use serde::de::Error;
|
||||
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
||||
use settings::{Settings, SettingsSources};
|
||||
use std::borrow::Cow;
|
||||
use std::fmt::Display;
|
||||
|
||||
/// Initializes the `editor_mode_setting` crate.
|
||||
pub fn init(cx: &mut App) {
|
||||
EditorModeSetting::register(cx);
|
||||
}
|
||||
|
||||
/// Whether or not to enable Vim mode.
|
||||
///
|
||||
/// Default: `EditMode::Default`
|
||||
pub struct EditorModeSetting(pub EditorMode);
|
||||
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq, Default)]
|
||||
pub enum EditorMode {
|
||||
#[default]
|
||||
Default,
|
||||
Vim(ModalMode),
|
||||
Helix(ModalMode),
|
||||
}
|
||||
|
||||
impl JsonSchema for EditorMode {
|
||||
fn schema_name() -> Cow<'static, str> {
|
||||
"EditorMode".into()
|
||||
}
|
||||
|
||||
fn json_schema(_gen: &mut schemars::SchemaGenerator) -> Schema {
|
||||
let options = Self::get_schema_options();
|
||||
json_schema!({
|
||||
"oneOf": options,
|
||||
"description": "Editor mode"
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for EditorMode {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let s = String::deserialize(deserializer)?;
|
||||
match s.as_str() {
|
||||
"default" => Ok(EditorMode::Default),
|
||||
"vim" => Ok(EditorMode::Vim(ModalMode::Normal)),
|
||||
"vim_normal" => Ok(EditorMode::Vim(ModalMode::Normal)),
|
||||
"vim_insert" => Ok(EditorMode::Vim(ModalMode::Insert)),
|
||||
"vim_replace" => Ok(EditorMode::Vim(ModalMode::Replace)),
|
||||
"vim_visual" => Ok(EditorMode::Vim(ModalMode::Visual)),
|
||||
"vim_visual_line" => Ok(EditorMode::Vim(ModalMode::VisualLine)),
|
||||
"vim_visual_block" => Ok(EditorMode::Vim(ModalMode::VisualBlock)),
|
||||
"helix_experimental" => Ok(EditorMode::Helix(ModalMode::HelixNormal)),
|
||||
_ => Err(D::Error::custom(format!("Unknown editor mode: {}", s))),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Serialize for EditorMode {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
let s = match self {
|
||||
EditorMode::Default => "default",
|
||||
EditorMode::Vim(ModalMode::Normal) => "vim",
|
||||
EditorMode::Vim(ModalMode::Insert) => "vim_insert",
|
||||
EditorMode::Vim(ModalMode::Replace) => "vim_replace",
|
||||
EditorMode::Vim(ModalMode::Visual) => "vim_visual",
|
||||
EditorMode::Vim(ModalMode::VisualLine) => "vim_visual_line",
|
||||
EditorMode::Vim(ModalMode::VisualBlock) => "vim_visual_block",
|
||||
EditorMode::Helix(ModalMode::HelixNormal) => "helix_experimental",
|
||||
_ => return Err(serde::ser::Error::custom("unsupported editor mode variant")),
|
||||
};
|
||||
serializer.serialize_str(s)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
|
||||
pub enum ModalMode {
|
||||
Normal,
|
||||
Insert,
|
||||
Replace,
|
||||
Visual,
|
||||
VisualLine,
|
||||
VisualBlock,
|
||||
HelixNormal,
|
||||
}
|
||||
|
||||
impl Display for ModalMode {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
ModalMode::Normal => write!(f, "NORMAL"),
|
||||
ModalMode::Insert => write!(f, "INSERT"),
|
||||
ModalMode::Replace => write!(f, "REPLACE"),
|
||||
ModalMode::Visual => write!(f, "VISUAL"),
|
||||
ModalMode::VisualLine => write!(f, "VISUAL LINE"),
|
||||
ModalMode::VisualBlock => write!(f, "VISUAL BLOCK"),
|
||||
ModalMode::HelixNormal => write!(f, "HELIX NORMAL"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ModalMode {
|
||||
pub fn is_visual(&self) -> bool {
|
||||
match self {
|
||||
Self::Visual | Self::VisualLine | Self::VisualBlock => true,
|
||||
Self::Normal | Self::Insert | Self::Replace | Self::HelixNormal => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for ModalMode {
|
||||
fn default() -> Self {
|
||||
Self::Normal
|
||||
}
|
||||
}
|
||||
|
||||
impl Settings for EditorModeSetting {
|
||||
const KEY: Option<&'static str> = Some("editor_mode");
|
||||
|
||||
type FileContent = Option<EditorMode>;
|
||||
|
||||
fn load(sources: SettingsSources<Self::FileContent>, _: &mut App) -> Result<Self> {
|
||||
Ok(Self(
|
||||
sources
|
||||
.user
|
||||
.or(sources.server)
|
||||
.copied()
|
||||
.flatten()
|
||||
.unwrap_or(sources.default.ok_or_else(Self::missing_default)?),
|
||||
))
|
||||
}
|
||||
|
||||
fn import_from_vscode(_vscode: &settings::VsCodeSettings, _current: &mut Self::FileContent) {
|
||||
// TODO: could possibly check if any of the `vim.<foo>` keys are set?
|
||||
}
|
||||
}
|
||||
|
||||
impl EditorMode {
|
||||
pub fn is_modal(&self) -> bool {
|
||||
matches!(self, EditorMode::Vim(_) | EditorMode::Helix(_))
|
||||
}
|
||||
|
||||
pub fn vim() -> EditorMode {
|
||||
EditorMode::Vim(ModalMode::default())
|
||||
}
|
||||
|
||||
pub fn get_schema_options() -> Vec<serde_json::Value> {
|
||||
vec![
|
||||
serde_json::json!({
|
||||
"const": "default",
|
||||
"description": "Standard editing mode"
|
||||
}),
|
||||
serde_json::json!({
|
||||
"const": "vim",
|
||||
"description": "Vim normal mode"
|
||||
}),
|
||||
serde_json::json!({
|
||||
"const": "vim_normal",
|
||||
"description": "Vim normal mode"
|
||||
}),
|
||||
serde_json::json!({
|
||||
"const": "vim_insert",
|
||||
"description": "Vim insert mode"
|
||||
}),
|
||||
serde_json::json!({
|
||||
"const": "vim_replace",
|
||||
"description": "Vim replace mode"
|
||||
}),
|
||||
serde_json::json!({
|
||||
"const": "vim_visual",
|
||||
"description": "Vim visual mode"
|
||||
}),
|
||||
serde_json::json!({
|
||||
"const": "vim_visual_line",
|
||||
"description": "Vim visual line mode"
|
||||
}),
|
||||
serde_json::json!({
|
||||
"const": "vim_visual_block",
|
||||
"description": "Vim visual block mode"
|
||||
}),
|
||||
serde_json::json!({
|
||||
"const": "helix_experimental",
|
||||
"description": "Helix mode (experimental)"
|
||||
}),
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -37,7 +37,7 @@ telemetry.workspace = true
|
||||
theme.workspace = true
|
||||
ui.workspace = true
|
||||
util.workspace = true
|
||||
editor_mode_setting.workspace = true
|
||||
vim_mode_setting.workspace = true
|
||||
workspace-hack.workspace = true
|
||||
workspace.workspace = true
|
||||
zed_actions.workspace = true
|
||||
|
||||
@@ -10,7 +10,6 @@ use anyhow::Context as _;
|
||||
use client::{ExtensionMetadata, ExtensionProvides};
|
||||
use collections::{BTreeMap, BTreeSet};
|
||||
use editor::{Editor, EditorElement, EditorStyle};
|
||||
use editor_mode_setting::{EditorMode, EditorModeSetting};
|
||||
use extension_host::{ExtensionManifest, ExtensionOperation, ExtensionStore};
|
||||
use fuzzy::{StringMatchCandidate, match_strings};
|
||||
use gpui::{
|
||||
@@ -28,6 +27,7 @@ use ui::{
|
||||
CheckboxWithLabel, Chip, ContextMenu, PopoverMenu, ScrollableHandle, Scrollbar, ScrollbarState,
|
||||
ToggleButton, Tooltip, prelude::*,
|
||||
};
|
||||
use vim_mode_setting::VimModeSetting;
|
||||
use workspace::{
|
||||
Workspace, WorkspaceId,
|
||||
item::{Item, ItemEvent},
|
||||
@@ -1335,26 +1335,17 @@ impl ExtensionsPage {
|
||||
.child(CheckboxWithLabel::new(
|
||||
"enable-vim",
|
||||
Label::new("Enable vim mode"),
|
||||
{
|
||||
let editor_mode = EditorModeSetting::get_global(cx).0;
|
||||
if editor_mode.is_modal() {
|
||||
ui::ToggleState::Selected
|
||||
} else {
|
||||
ui::ToggleState::Unselected
|
||||
}
|
||||
if VimModeSetting::get_global(cx).0 {
|
||||
ui::ToggleState::Selected
|
||||
} else {
|
||||
ui::ToggleState::Unselected
|
||||
},
|
||||
cx.listener(move |this, selection, _, cx| {
|
||||
telemetry::event!("Vim Mode Toggled", source = "Feature Upsell");
|
||||
this.update_settings::<EditorModeSetting>(
|
||||
this.update_settings::<VimModeSetting>(
|
||||
selection,
|
||||
cx,
|
||||
|setting, value| {
|
||||
*setting = Some(if value {
|
||||
EditorMode::vim()
|
||||
} else {
|
||||
EditorMode::default()
|
||||
});
|
||||
},
|
||||
|setting, value| *setting = Some(value),
|
||||
);
|
||||
}),
|
||||
)),
|
||||
|
||||
@@ -1401,13 +1401,16 @@ impl PickerDelegate for FileFinderDelegate {
|
||||
#[cfg(windows)]
|
||||
let raw_query = raw_query.trim().to_owned().replace("/", "\\");
|
||||
#[cfg(not(windows))]
|
||||
let raw_query = raw_query.trim().to_owned();
|
||||
let raw_query = raw_query.trim();
|
||||
|
||||
let file_query_end = if path_position.path.to_str().unwrap_or(&raw_query) == raw_query {
|
||||
let raw_query = raw_query.trim_end_matches(':').to_owned();
|
||||
let path = path_position.path.to_str();
|
||||
let path_trimmed = path.unwrap_or(&raw_query).trim_end_matches(':');
|
||||
let file_query_end = if path_trimmed == raw_query {
|
||||
None
|
||||
} else {
|
||||
// Safe to unwrap as we won't get here when the unwrap in if fails
|
||||
Some(path_position.path.to_str().unwrap().len())
|
||||
Some(path.unwrap().len())
|
||||
};
|
||||
|
||||
let query = FileSearchQuery {
|
||||
|
||||
@@ -218,6 +218,7 @@ async fn test_matching_paths(cx: &mut TestAppContext) {
|
||||
" ndan ",
|
||||
" band ",
|
||||
"a bandana",
|
||||
"bandana:",
|
||||
] {
|
||||
picker
|
||||
.update_in(cx, |picker, window, cx| {
|
||||
@@ -252,6 +253,53 @@ async fn test_matching_paths(cx: &mut TestAppContext) {
|
||||
}
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_matching_paths_with_colon(cx: &mut TestAppContext) {
|
||||
let app_state = init_test(cx);
|
||||
app_state
|
||||
.fs
|
||||
.as_fake()
|
||||
.insert_tree(
|
||||
path!("/root"),
|
||||
json!({
|
||||
"a": {
|
||||
"foo:bar.rs": "",
|
||||
"foo.rs": "",
|
||||
}
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
|
||||
|
||||
let (picker, _, cx) = build_find_picker(project, cx);
|
||||
|
||||
// 'foo:' matches both files
|
||||
cx.simulate_input("foo:");
|
||||
picker.update(cx, |picker, _| {
|
||||
assert_eq!(picker.delegate.matches.len(), 3);
|
||||
assert_match_at_position(picker, 0, "foo.rs");
|
||||
assert_match_at_position(picker, 1, "foo:bar.rs");
|
||||
});
|
||||
|
||||
// 'foo:b' matches one of the files
|
||||
cx.simulate_input("b");
|
||||
picker.update(cx, |picker, _| {
|
||||
assert_eq!(picker.delegate.matches.len(), 2);
|
||||
assert_match_at_position(picker, 0, "foo:bar.rs");
|
||||
});
|
||||
|
||||
cx.dispatch_action(editor::actions::Backspace);
|
||||
|
||||
// 'foo:1' matches both files, specifying which row to jump to
|
||||
cx.simulate_input("1");
|
||||
picker.update(cx, |picker, _| {
|
||||
assert_eq!(picker.delegate.matches.len(), 3);
|
||||
assert_match_at_position(picker, 0, "foo.rs");
|
||||
assert_match_at_position(picker, 1, "foo:bar.rs");
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_unicode_paths(cx: &mut TestAppContext) {
|
||||
let app_state = init_test(cx);
|
||||
|
||||
@@ -44,7 +44,7 @@ impl editor::Addon for ConflictAddon {
|
||||
|
||||
pub fn register_editor(editor: &mut Editor, buffer: Entity<MultiBuffer>, cx: &mut Context<Editor>) {
|
||||
// Only show conflict UI for singletons and in the project diff.
|
||||
if !editor.display_mode().is_full()
|
||||
if !editor.mode().is_full()
|
||||
|| (!editor.buffer().read(cx).is_singleton()
|
||||
&& !editor.buffer().read(cx).all_diff_hunks_expanded())
|
||||
{
|
||||
|
||||
@@ -14,7 +14,7 @@ use anyhow::Context as _;
|
||||
use askpass::AskPassDelegate;
|
||||
use db::kvp::KEY_VALUE_STORE;
|
||||
use editor::{
|
||||
Editor, EditorDisplayMode, EditorElement, EditorSettings, MultiBuffer, ShowScrollbar,
|
||||
Editor, EditorElement, EditorMode, EditorSettings, MultiBuffer, ShowScrollbar,
|
||||
scroll::ScrollbarAutoHide,
|
||||
};
|
||||
use futures::StreamExt as _;
|
||||
@@ -394,7 +394,7 @@ pub(crate) fn commit_message_editor(
|
||||
let buffer = cx.new(|cx| MultiBuffer::singleton(commit_message_buffer, cx));
|
||||
let max_lines = if in_panel { MAX_PANEL_EDITOR_LINES } else { 18 };
|
||||
let mut commit_editor = Editor::new(
|
||||
EditorDisplayMode::AutoHeight {
|
||||
EditorMode::AutoHeight {
|
||||
min_lines: 1,
|
||||
max_lines: Some(max_lines),
|
||||
},
|
||||
@@ -406,7 +406,7 @@ pub(crate) fn commit_message_editor(
|
||||
commit_editor.set_collaboration_hub(Box::new(project));
|
||||
commit_editor.set_use_autoclose(false);
|
||||
commit_editor.set_show_gutter(false, cx);
|
||||
// commit_editor.set_use_modal_editing(true); TODO
|
||||
commit_editor.set_use_modal_editing(true);
|
||||
commit_editor.set_show_wrap_guides(false, cx);
|
||||
commit_editor.set_show_indent_guides(false, cx);
|
||||
let placeholder = placeholder.unwrap_or("Enter commit message".into());
|
||||
|
||||
@@ -104,14 +104,14 @@ impl CursorPosition {
|
||||
cursor_position.update(cx, |cursor_position, cx| {
|
||||
cursor_position.selected_count = SelectionStats::default();
|
||||
cursor_position.selected_count.selections = editor.selections.count();
|
||||
match editor.display_mode() {
|
||||
editor::EditorDisplayMode::AutoHeight { .. }
|
||||
| editor::EditorDisplayMode::SingleLine
|
||||
| editor::EditorDisplayMode::Minimap { .. } => {
|
||||
match editor.mode() {
|
||||
editor::EditorMode::AutoHeight { .. }
|
||||
| editor::EditorMode::SingleLine
|
||||
| editor::EditorMode::Minimap { .. } => {
|
||||
cursor_position.position = None;
|
||||
cursor_position.context = None;
|
||||
}
|
||||
editor::EditorDisplayMode::Full { .. } => {
|
||||
editor::EditorMode::Full { .. } => {
|
||||
let mut last_selection = None::<Selection<Point>>;
|
||||
let snapshot = editor.buffer().read(cx).snapshot(cx);
|
||||
if snapshot.excerpts().count() > 0 {
|
||||
|
||||
@@ -9,10 +9,8 @@ use parking::Parker;
|
||||
use parking_lot::Mutex;
|
||||
use util::ResultExt;
|
||||
use windows::{
|
||||
Foundation::TimeSpan,
|
||||
System::Threading::{
|
||||
ThreadPool, ThreadPoolTimer, TimerElapsedHandler, WorkItemHandler, WorkItemOptions,
|
||||
WorkItemPriority,
|
||||
ThreadPool, ThreadPoolTimer, TimerElapsedHandler, WorkItemHandler, WorkItemPriority,
|
||||
},
|
||||
Win32::{
|
||||
Foundation::{LPARAM, WPARAM},
|
||||
@@ -56,12 +54,7 @@ impl WindowsDispatcher {
|
||||
Ok(())
|
||||
})
|
||||
};
|
||||
ThreadPool::RunWithPriorityAndOptionsAsync(
|
||||
&handler,
|
||||
WorkItemPriority::High,
|
||||
WorkItemOptions::TimeSliced,
|
||||
)
|
||||
.log_err();
|
||||
ThreadPool::RunWithPriorityAsync(&handler, WorkItemPriority::High).log_err();
|
||||
}
|
||||
|
||||
fn dispatch_on_threadpool_after(&self, runnable: Runnable, duration: Duration) {
|
||||
@@ -72,12 +65,7 @@ impl WindowsDispatcher {
|
||||
Ok(())
|
||||
})
|
||||
};
|
||||
let delay = TimeSpan {
|
||||
// A time period expressed in 100-nanosecond units.
|
||||
// 10,000,000 ticks per second
|
||||
Duration: (duration.as_nanos() / 100) as i64,
|
||||
};
|
||||
ThreadPoolTimer::CreateTimer(&handler, delay).log_err();
|
||||
ThreadPoolTimer::CreateTimer(&handler, duration.into()).log_err();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -34,6 +34,7 @@ pub enum IconName {
|
||||
ArrowRightLeft,
|
||||
ArrowUp,
|
||||
ArrowUpRight,
|
||||
Attach,
|
||||
AudioOff,
|
||||
AudioOn,
|
||||
Backspace,
|
||||
|
||||
@@ -24,6 +24,7 @@ serde_json_lenient.workspace = true
|
||||
theme.workspace = true
|
||||
ui.workspace = true
|
||||
util.workspace = true
|
||||
util_macros.workspace = true
|
||||
workspace-hack.workspace = true
|
||||
workspace.workspace = true
|
||||
zed_actions.workspace = true
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user