Compare commits

..

58 Commits

Author SHA1 Message Date
Zed Bot
d846d45519 Bump to 0.211.5 for @smitbarmase 2025-11-06 15:05:46 +00:00
Smit Barmase
f70f507f61 language: Fix completion menu no longer prioritizes relevant items for Typescript and Python (#42065)
Closes #41672

Regressed in https://github.com/zed-industries/zed/pull/40242

Release Notes:

- Fixed issue where completion menu no longer prioritizes relevant items
for TypeScript and Python.
2025-11-06 13:37:06 +05:30
Anthony Eid
e517719132 AI: Fix Github Copilot edit predictions failing to start (#41934)
Closes #41457 #41806 #41801

Copilot started using `node:sqlite` module which is an experimental
feature between node v22-v23 (stable in v24). The fix was passing in the
experimental flag when Zed starts the copilot LSP.

I tested this with v20.19.5 and v24.11.0. The fix got v20.19 working and
didn't affect v24.11 which was already working.

Release Notes:

- AI: Fix Github Copilot edit predictions failing to start
2025-11-06 13:30:28 +05:30
Richard Feldman
74efd3adbc Run ACP login from same cwd as agent server (#42038)
This makes it possible to do login via things like `cmd: "node", args:
["my-node-file.js", "login"]`

Also, that command will now use Zed's managed `node` instance.

Release Notes:

- ACP extensions can now run terminal login commands using relative
paths
2025-11-06 08:11:16 +01:00
Danilo Leal
9f1a9016b6 agent_ui: Fix how icons from external agents are displayed (#42034)
Release Notes:

- N/A
2025-11-06 08:07:59 +01:00
Danilo Leal
506f333ce1 gpui: Add support for rendering SVG from external files (#42024)
Release Notes:

- N/A

---------

Co-authored-by: Mikayla Maki <mikayla.c.maki@gmail.com>
2025-11-06 08:06:59 +01:00
Finn Evers
214a0bc116 svg_preview: Update preview on every buffer edit (#41270)
Closes https://github.com/zed-industries/zed/issues/39104

This fixes an issue where the preview would not work for remote buffers
in the process.

Release Notes:

- Fixed an issue where the SVG preview would not work in remote
scenarios.
- The SVG preview will now rerender on every keypress instead of only on
saves.
2025-11-06 08:06:54 +01:00
Conrad Irwin
d461acbc7b Revert "Don't draft release notes"
This reverts commit 62ece18dfe.
2025-11-05 23:51:19 -07:00
zed-zippy[bot]
7acefd50cc Refresh zed.dev releases page after releases (#42060) (cherry-pick to stable) (#42064)
Cherry-pick of #42060 to stable

----
Release Notes:

- N/A

Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
2025-11-06 06:49:23 +00:00
Conrad Irwin
62ece18dfe Don't draft release notes 2025-11-05 10:45:11 -07:00
Joseph T. Lyons
4475689e4c v0.211.x stable 2025-11-05 12:07:10 -05:00
Ben Kunkle
5ea5b5e7a9 Fix integer underflow in autosave mode after delay in the settings (cherry-pick #41898) (#42013)
Closes #41774 

Release Notes:

- settings_ui: Fixed an integer underflow panic when attempting to hit
the `-` sign on settings item that take delays in milliseconds

Co-authored-by: Ignasius <96295999+ignasius-j-s@users.noreply.github.com>
2025-11-05 11:30:12 -05:00
Richard Feldman
d93f528a37 Use our node runtime for ACP extensions (#41955)
Release Notes:

- Now ACP extensions use Zed's managed Node.js runtime

---------

Co-authored-by: Mikayla Maki <mikayla.c.maki@gmail.com>
2025-11-05 12:24:23 +01:00
Richard Feldman
44140197a6 Add ACP terminal-login via _meta field (#41954)
As discussed with @benbrandt and @mikayla-maki:

* We now tell ACP clients we support the nonstandard `terminal-auth`
`_meta` field for terminal-based authentication
* In the future, we anticipate ACP itself supporting *some* form of
terminal-based authentication, but that hasn't been designed yet or gone
through the RFD process
* For now, this unblocks terminal-based auth

Release Notes:

- Added experimental terminal-based authentication to ACP support
2025-11-05 11:40:11 +01:00
Lukas Wirth
2e746791b1 project: Fetch latest lsp data in deduplicate_range_based_lsp_requests 2025-11-05 09:24:05 +01:00
Lukas Wirth
a3f230f760 zed: Reduce number of rayon threads, spawn with bigger stacks (#41812)
We already do this for the cli and remote server but forgot to do so for
the main binary

Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-11-05 09:13:16 +01:00
zed-zippy[bot]
ff0eef98c9 Fetch (just) enough refs in script/cherry-pick (#41949) (cherry-pick to preview) (#41951)
Cherry-pick of #41949 to preview

----
Before this change we'd download all the tagged commits, but none of
their ancestors,
this was slow and made cherry-picking fail.

Release Notes:

- N/A

Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
2025-11-04 19:45:56 -07:00
Zed Bot
712f75ddb4 Bump to 0.211.4 for @ConradIrwin 2025-11-05 00:15:16 +00:00
Conrad Irwin
74031e2243 More tweaks to CI pipeline (#41941)
Closes #ISSUE

Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-11-04 16:59:39 -07:00
zed-zippy[bot]
8ea85828f5 lsp: Fix dynamic registration of document diagnostics (#41929) (cherry-pick to preview) (#41947)
Cherry-pick of #41929 to preview

----
- lsp: Fix dynamic registration of diagnostic capabilities not taking
effect when an initial capability is not specified
Gist of the issue lies within use of .get_mut instead of .entry. If we
had not created any dynamic capability beforehand, we'd miss a
registration, essentially

- **Determine whether to update remote caps in a smarter manner**

Release Notes:

- Fixed document diagnostics with Ty language server.

Co-authored-by: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com>
2025-11-04 23:33:47 +00:00
zed-zippy[bot]
7deac6cd2c Improve compare_perf.yml, cherry_pick.yml (#41606) (cherry-pick) (#41946)
Cherry-pick of #41606

----
Release Notes:

- N/A

---------

Co-authored-by: Nia Espera <nia@zed.dev>

Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
Co-authored-by: Nia Espera <nia@zed.dev>
2025-11-04 23:01:35 +00:00
zed-zippy[bot]
03a0f9b944 Shell out to real tar in extension builder (#41856) (cherry-pick) (#41945)
Cherry-pick of #41856

----
We see `test_extension_store_with_test_extension` hang in untarring the
WASI SDK some times.

In lieu of trying to debug the problem, let's try shelling out for now
in the hope that the test becomes more reliable.

There's a bit of risk here because we're using async-tar for other
things (but probably not 300Mb tar files...)

Assisted-By: Zed AI

Closes #ISSUE

Release Notes:

- N/A

Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
2025-11-04 22:39:24 +00:00
Conrad Irwin
c374532318 settings_ui: Fix dropdowns after #41036 (#41920) (cherry-pick) (#41930)
Cherry-pick of #41920

----
Closes #41533

Both of the issues in the release notes that are fixed in this PR, were
caused by incorrect usage of the `window.use_state` API.
The first issue was caused by calling `window.use_state` in a render
helper, resulting in the element ID used to share state being the same
across different pages, resulting in the state being re-used when it
should have been re-created. The fix for this was to move the
`window.state` (and rendering logic) into a `impl RenderOnce` component,
so that the IDs are resolved during the render, avoiding the state
conflicts.

The second issue is caused by using a `move` closure in the
`window.use_state` call, resulting in stale closure values when the
window state is re-used.

Release Notes:

- settings_ui: Fixed an issue where some dropdown menus would show
options from a different dropdown when clicked
- settings_ui: Fixed an issue where attempting to change a setting in a
dropdown back to it's original value after changing it would do nothing

Co-authored-by: Ben Kunkle <ben@zed.dev>
2025-11-04 14:01:06 -05:00
Zed Bot
a677ecc889 Bump to 0.211.3 for @ConradIrwin 2025-11-04 05:35:12 +00:00
Conrad Irwin
59213b26c9 Fix merge conflict (#41853)
Closes #ISSUE

Release Notes:

- N/A
2025-11-03 22:31:09 -07:00
Piotr Osiewicz
2b901ad3ae ci: Enable namespace caching for Linux workers (#41652)
Release Notes:

- N/A
2025-11-03 22:30:51 -07:00
Conrad Irwin
61dddc4d65 Re-use the existing bundle steps for nightly too (#41699)
One of the reasons we didn't spot that we were missing the telemetry env
vars for the production builds was that nightly (which was working) had
its own set of build steps. This re-uses those and pushes the env vars
down from the workflow to the job.

It also fixes nightly releases to upload all-in-one go so that all
platforms update in sync.

Closes #41655

Release Notes:

- N/A
2025-11-03 22:30:45 -07:00
Ben Kunkle
14281179f2 gh-workflow unit evals (#41637)
Closes #ISSUE

Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-11-03 22:30:35 -07:00
Conrad Irwin
2f3c2082f4 Fix branch diff hunk expansion (#41873)
Closes #ISSUE

Release Notes:

- (preview only) Fixes a bug where hunks were not expanded when viewing
branch diff
2025-11-03 22:30:16 -07:00
Kirill Bulatov
f8979f1ed7 Do not pull diagnostics when those are disabled (#41865)
Based on 

[hang.log](https://github.com/user-attachments/files/23319081/hang.log)


Release Notes:

- N/A
2025-11-04 00:43:07 +02:00
Kirill Bulatov
921be53241 Remove incorrectly added test
During cherry-picking of https://github.com/zed-industries/zed/pull/41859 , one test was incorrectly merged in.
This was only added in https://github.com/zed-industries/zed/pull/41342 which is not cherry-picked, hence the test will fail.
2025-11-04 00:38:38 +02:00
Kirill Bulatov
af2d462bf7 Fix racy inlay hints queries (#41816)
Follow-up of https://github.com/zed-industries/zed/pull/40183

Release Notes:

- (Preview only) Fixed inlay hints duplicating when multiple editors are
open for the same buffer

---------

Co-authored-by: Lukas Wirth <lukas@zed.dev>
2025-11-04 00:17:00 +02:00
Kirill Bulatov
713903fe76 Fix incorrect search ranges when rendering search matches in the outline panel (#41859)
Closes https://github.com/zed-industries/zed/issues/41792

Release Notes:

- Fixed outline panel panicking when rendering certain search matches
2025-11-04 00:16:44 +02:00
Lukas Wirth
f4c077bdcf Remove unused import 2025-11-03 22:13:40 +01:00
Lukas Wirth
48fdd8893f file_finder: Fix highlighting panic in open path prompt (#41808)
Closes https://github.com/zed-industries/zed/issues/41249

Couldn't quite come up with a test case here but verified it works.

Release Notes:

- Fixed a panic in file finder when deleting characters
2025-11-03 22:02:10 +01:00
Lukas Wirth
93495542de editor: Fix refresh_linked_ranges panics due to old snapshot use (#41657)
Fixes ZED-29Z

Release Notes:

- Fixed panic in `refresh_linked_ranges`
2025-11-03 22:01:54 +01:00
Finn Evers
311abd0e1b extension_host: Do not try auto installing suppressed extensions (#41551)
Release Notes:

- Fixed an issue where Zed would try to install extensions specified
under `auto_install_extensions` which were moved into core.
2025-11-03 21:34:28 +01:00
Lukas Wirth
c5e298b5d8 Revert "sum_tree: Replace rayon with futures (#41586)"
This reverts commit f2ce06c7b0.
2025-11-03 20:02:37 +01:00
Zed Bot
ec813e9833 Bump to 0.211.2 for @ConradIrwin 2025-11-01 05:00:53 +00:00
Conrad Irwin
68063eaa45 Fix telemetry in release builds (#41695)
This was inadvertently broken in v0.211.1-pre when we rewrote the
release build

Release Notes:

- N/A
2025-10-31 22:56:18 -06:00
Conrad Irwin
7dadd4a240 Delete old ci.yml (#41668)
The new one is much better

Release Notes:

- N/A
2025-10-31 22:55:54 -06:00
Ben Kunkle
4f8c3782cc Fix release.yml workflow (#41675)
Closes #ISSUE

Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-10-31 16:31:11 -04:00
Conrad Irwin
6eb3cdbf20 blerg 2025-10-31 13:11:39 -06:00
Conrad Irwin
8677bb57f0 Skip release notes for a hot second 2025-10-31 13:07:53 -06:00
Zed Bot
45accd931e Bump to 0.211.1 for @ConradIrwin 2025-10-31 18:32:04 +00:00
Conrad Irwin
671c4eb133 Delete old ci.yml 2025-10-31 12:27:21 -06:00
Ben Kunkle
011e3c155b gh-workflow release (#41502)
Closes #ISSUE

Rewrite our release pipeline to be generated by `gh-workflow`

Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-10-31 12:26:32 -06:00
claytonrcarter
3eb9d7765a bundle: Restore local install on macOS (#41482)
I just pulled and ran a local build via `script/bundle-mac -l -i` but
found that the resulting bundle wasn't installed as expected. (me:
"ToggleAllDocks!! Wait! Where is it?!") Looking into, it looks like the
`-l` flag was removed in #41392, leaving the `$local_only` var orphaned,
which then left the `-i/$local_install` flag unreachable. I suspect that
this was unintentional, so this PR re-adds the `-l/$local_only` flag to
`script/bundle-mac`.

I ran the build again and confirmed that local install seemed to work as
expected. (ie "ToggleAllDocks!! 🎉")

While here, I also removed the last reference to `$local_arch`, because
all other references to that were removed in #41392.

/cc @osiewicz 

Release Notes:

- N/A

---------

Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
2025-10-31 12:26:19 -06:00
Conrad Irwin
d6c49fbc3c Add a no-op compare_perf workflow (#41605)
Testing PR for @zed-zippy

Release Notes:

- N/A
2025-10-31 12:24:42 -06:00
Conrad Irwin
7bf3f92a0a Don't skip tests in nightly release (#41573)
Release Notes:

- N/A
2025-10-31 12:24:25 -06:00
Conrad Irwin
2751512ae8 Use gh-workflow for tests (take 2) (#41420)
This re-implements the reverted commit 8b051d6cc3.

Closes #ISSUE

Release Notes:

- N/A

---------

Co-authored-by: Ben Kunkle <ben@zed.dev>
2025-10-31 12:09:43 -06:00
Conrad Irwin
032ab87cbe Generate dwarf files for builds again (#41651)
Closes #ISSUE

Release Notes:

- N/A
2025-10-31 11:03:48 -06:00
Lukas Wirth
dbabcfcac8 sum_tree: Replace rayon with futures (#41586)
Release Notes:

- N/A *or* Added/Fixed/Improved ...

Co-authored by: Kate <kate@zed.dev>
2025-10-31 12:20:42 +01:00
Bennet Fenner
9436818467 agent_ui: Fix agent: Chat with follow not working (#41581)
Release Notes:

- Fixed an issue where `agent: Chat with follow` was not working anymore

Co-authored-by: Ben Brandt <benjamin.j.brandt@gmail.com>
2025-10-30 17:17:06 +01:00
Lukas Wirth
501f78aacb project: Fix inlay hints duplicatig on chunk start (#41461)
Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-10-29 16:23:03 +01:00
Finn Evers
0d5bfaf7b4 Revert "Support relative line number on wrapped lines (#39268)" (#41450)
Closes #41422

This completely broke line numbering as described in the linked issue
and scrolling up does not have the correct numbers any more.

Release Notes:

- NOTE: The `relative_line_numbers` change
(https://github.com/zed-industries/zed/pull/39268) was reverted and did
not make the release cut!
2025-10-29 16:22:00 +01:00
Finn Evers
b1935b95a1 ui: Don't show scrollbar track in too many cases (#41455)
Follow-up to https://github.com/zed-industries/zed/pull/41354 which
introduced a small regression.

Release Notes:

- N/A
2025-10-29 16:22:00 +01:00
Conrad Irwin
f98c12513b v0.211.x preview 2025-10-28 19:43:56 -06:00
271 changed files with 5639 additions and 13488 deletions

View File

@@ -35,8 +35,10 @@ body:
attributes:
label: If applicable, attach your `Zed.log` file to this issue.
description: |
From the command palette, run `zed: open log` to see the last 1000 lines.
Or run `zed: reveal log in file manager` to reveal the log file itself.
macOS: `~/Library/Logs/Zed/Zed.log`
Windows: `C:\Users\YOU\AppData\Local\Zed\logs\Zed.log`
Linux: `~/.local/share/zed/logs/Zed.log` or $XDG_DATA_HOME
If you only need the most recent lines, you can run the `zed: open log` command palette action to see the last 1000.
value: |
<details><summary>Zed.log</summary>

69
.github/workflows/after_release.yml vendored Normal file
View File

@@ -0,0 +1,69 @@
# Generated from xtask::workflows::after_release
# Rebuild with `cargo xtask workflows`.
name: after_release
on:
release:
types:
- published
jobs:
rebuild_releases_page:
if: github.repository_owner == 'zed-industries'
runs-on: namespace-profile-2x4-ubuntu-2404
steps:
- name: after_release::rebuild_releases_page
run: 'curl https://zed.dev/api/revalidate-releases -H "Authorization: Bearer ${RELEASE_NOTES_API_TOKEN}"'
shell: bash -euxo pipefail {0}
env:
RELEASE_NOTES_API_TOKEN: ${{ secrets.RELEASE_NOTES_API_TOKEN }}
post_to_discord:
needs:
- rebuild_releases_page
if: github.repository_owner == 'zed-industries'
runs-on: namespace-profile-2x4-ubuntu-2404
steps:
- id: get-release-url
name: after_release::post_to_discord::get_release_url
run: |
if [ "${{ github.event.release.prerelease }}" == "true" ]; then
URL="https://zed.dev/releases/preview"
else
URL="https://zed.dev/releases/stable"
fi
echo "URL=$URL" >> "$GITHUB_OUTPUT"
shell: bash -euxo pipefail {0}
- id: get-content
name: after_release::post_to_discord::get_content
uses: 2428392/gh-truncate-string-action@b3ff790d21cf42af3ca7579146eedb93c8fb0757
with:
stringToTruncate: |
📣 Zed [${{ github.event.release.tag_name }}](<${{ steps.get-release-url.outputs.URL }}>) was just released!
${{ github.event.release.body }}
maxLength: 2000
truncationSymbol: '...'
- name: after_release::post_to_discord::discord_webhook_action
uses: tsickert/discord-webhook@c840d45a03a323fbc3f7507ac7769dbd91bfb164
with:
webhook-url: ${{ secrets.DISCORD_WEBHOOK_RELEASE_NOTES }}
content: ${{ steps.get-content.outputs.string }}
publish_winget:
runs-on: namespace-profile-2x4-ubuntu-2404
steps:
- id: set-package-name
name: after_release::publish_winget::set_package_name
run: |
if [ "${{ github.event.release.prerelease }}" == "true" ]; then
PACKAGE_NAME=ZedIndustries.Zed.Preview
else
PACKAGE_NAME=ZedIndustries.Zed
fi
echo "PACKAGE_NAME=$PACKAGE_NAME" >> "$GITHUB_OUTPUT"
shell: bash -euxo pipefail {0}
- name: after_release::publish_winget::winget_releaser
uses: vedantmgoyal9/winget-releaser@19e706d4c9121098010096f9c495a70a7518b30f
with:
identifier: ${{ steps.set-package-name.outputs.PACKAGE_NAME }}
max-versions-to-keep: 5
token: ${{ secrets.WINGET_TOKEN }}

View File

@@ -1,93 +0,0 @@
# IF YOU UPDATE THE NAME OF ANY GITHUB SECRET, YOU MUST CHERRY PICK THE COMMIT
# TO BOTH STABLE AND PREVIEW CHANNELS
name: Release Actions
on:
release:
types: [published]
jobs:
discord_release:
if: github.repository_owner == 'zed-industries'
runs-on: ubuntu-latest
steps:
- name: Get release URL
id: get-release-url
run: |
if [ "${{ github.event.release.prerelease }}" == "true" ]; then
URL="https://zed.dev/releases/preview"
else
URL="https://zed.dev/releases/stable"
fi
echo "URL=$URL" >> "$GITHUB_OUTPUT"
- name: Get content
uses: 2428392/gh-truncate-string-action@b3ff790d21cf42af3ca7579146eedb93c8fb0757 # v1.4.1
id: get-content
with:
stringToTruncate: |
📣 Zed [${{ github.event.release.tag_name }}](<${{ steps.get-release-url.outputs.URL }}>) was just released!
${{ github.event.release.body }}
maxLength: 2000
truncationSymbol: "..."
- name: Discord Webhook Action
uses: tsickert/discord-webhook@c840d45a03a323fbc3f7507ac7769dbd91bfb164 # v5.3.0
with:
webhook-url: ${{ secrets.DISCORD_WEBHOOK_RELEASE_NOTES }}
content: ${{ steps.get-content.outputs.string }}
publish-winget:
runs-on:
- ubuntu-latest
steps:
- name: Set Package Name
id: set-package-name
run: |
if [ "${{ github.event.release.prerelease }}" == "true" ]; then
PACKAGE_NAME=ZedIndustries.Zed.Preview
else
PACKAGE_NAME=ZedIndustries.Zed
fi
echo "PACKAGE_NAME=$PACKAGE_NAME" >> "$GITHUB_OUTPUT"
- uses: vedantmgoyal9/winget-releaser@19e706d4c9121098010096f9c495a70a7518b30f # v2
with:
identifier: ${{ steps.set-package-name.outputs.PACKAGE_NAME }}
max-versions-to-keep: 5
token: ${{ secrets.WINGET_TOKEN }}
send_release_notes_email:
if: false && github.repository_owner == 'zed-industries' && !github.event.release.prerelease
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
with:
fetch-depth: 0
- name: Check if release was promoted from preview
id: check-promotion-from-preview
run: |
VERSION="${{ github.event.release.tag_name }}"
PREVIEW_TAG="${VERSION}-pre"
if git rev-parse "$PREVIEW_TAG" > /dev/null 2>&1; then
echo "was_promoted_from_preview=true" >> "$GITHUB_OUTPUT"
else
echo "was_promoted_from_preview=false" >> "$GITHUB_OUTPUT"
fi
- name: Send release notes email
if: steps.check-promotion-from-preview.outputs.was_promoted_from_preview == 'true'
run: |
TAG="${{ github.event.release.tag_name }}"
cat << 'EOF' > release_body.txt
${{ github.event.release.body }}
EOF
jq -n --arg tag "$TAG" --rawfile body release_body.txt '{version: $tag, markdown_body: $body}' \
> release_data.json
curl -X POST "https://zed.dev/api/send_release_notes_email" \
-H "Authorization: Bearer ${{ secrets.RELEASE_NOTES_API_TOKEN }}" \
-H "Content-Type: application/json" \
-d @release_data.json

View File

@@ -467,7 +467,10 @@ jobs:
auto_release_preview:
needs:
- upload_release_assets
if: startsWith(github.ref, 'refs/tags/v') && endsWith(github.ref, '-pre') && !endsWith(github.ref, '.0-pre')
if: |
false
&& startsWith(github.ref, 'refs/tags/v')
&& endsWith(github.ref, '-pre') && !endsWith(github.ref, '.0-pre')
runs-on: namespace-profile-2x4-ubuntu-2404
steps:
- name: gh release edit "$GITHUB_REF_NAME" --repo=zed-industries/zed --draft=false

35
Cargo.lock generated
View File

@@ -1339,7 +1339,6 @@ dependencies = [
"settings",
"smol",
"tempfile",
"util",
"which 6.0.3",
"workspace",
]
@@ -1351,7 +1350,6 @@ dependencies = [
"anyhow",
"log",
"simplelog",
"tempfile",
"windows 0.61.3",
"winresource",
]
@@ -4530,15 +4528,12 @@ dependencies = [
"fs",
"futures 0.3.31",
"gpui",
"http_client",
"json_dotpath",
"language",
"log",
"node_runtime",
"paths",
"serde",
"serde_json",
"settings",
"smol",
"task",
"util",
@@ -4937,7 +4932,6 @@ dependencies = [
"editor",
"gpui",
"indoc",
"itertools 0.14.0",
"language",
"log",
"lsp",
@@ -7078,7 +7072,6 @@ dependencies = [
"serde_json",
"settings",
"url",
"urlencoding",
"util",
]
@@ -7117,8 +7110,6 @@ dependencies = [
"picker",
"pretty_assertions",
"project",
"recent_projects",
"remote",
"schemars 1.0.4",
"serde",
"serde_json",
@@ -7270,7 +7261,6 @@ dependencies = [
"async-task",
"backtrace",
"bindgen 0.71.1",
"bitflags 2.9.4",
"blade-graphics",
"blade-macros",
"blade-util",
@@ -7350,7 +7340,6 @@ dependencies = [
"wayland-cursor",
"wayland-protocols 0.31.2",
"wayland-protocols-plasma",
"wayland-protocols-wlr",
"windows 0.61.3",
"windows-core 0.61.2",
"windows-numerics",
@@ -13132,6 +13121,7 @@ dependencies = [
"paths",
"rope",
"serde",
"serde_json",
"text",
"util",
"uuid",
@@ -18049,7 +18039,7 @@ dependencies = [
[[package]]
name = "tree-sitter-gomod"
version = "1.1.1"
source = "git+https://github.com/camdencheek/tree-sitter-go-mod?rev=2e886870578eeba1927a2dc4bd2e2b3f598c5f9a#2e886870578eeba1927a2dc4bd2e2b3f598c5f9a"
source = "git+https://github.com/camdencheek/tree-sitter-go-mod?rev=6efb59652d30e0e9cd5f3b3a669afd6f1a926d3c#6efb59652d30e0e9cd5f3b3a669afd6f1a926d3c"
dependencies = [
"cc",
"tree-sitter-language",
@@ -19498,19 +19488,6 @@ dependencies = [
"wayland-scanner",
]
[[package]]
name = "wayland-protocols-wlr"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "efd94963ed43cf9938a090ca4f7da58eb55325ec8200c3848963e98dc25b78ec"
dependencies = [
"bitflags 2.9.4",
"wayland-backend",
"wayland-client",
"wayland-protocols 0.32.9",
"wayland-scanner",
]
[[package]]
name = "wayland-scanner"
version = "0.31.7"
@@ -20951,6 +20928,7 @@ dependencies = [
"gh-workflow",
"indexmap 2.11.4",
"indoc",
"serde",
"toml 0.8.23",
"toml_edit 0.22.27",
]
@@ -21135,7 +21113,7 @@ dependencies = [
[[package]]
name = "zed"
version = "0.212.0"
version = "0.211.5"
dependencies = [
"acp_tools",
"activity_indicator",
@@ -21710,7 +21688,6 @@ dependencies = [
"ordered-float 2.10.1",
"pretty_assertions",
"project",
"regex-syntax",
"serde",
"serde_json",
"settings",
@@ -21742,7 +21719,6 @@ dependencies = [
"futures 0.3.31",
"gpui",
"gpui_tokio",
"indoc",
"language",
"language_extension",
"language_model",
@@ -21753,10 +21729,8 @@ dependencies = [
"ordered-float 2.10.1",
"paths",
"polars",
"pretty_assertions",
"project",
"prompt_store",
"pulldown-cmark 0.12.2",
"release_channel",
"reqwest_client",
"serde",
@@ -21766,7 +21740,6 @@ dependencies = [
"smol",
"soa-rs",
"terminal_view",
"toml 0.8.23",
"util",
"watch",
"zeta",

View File

@@ -680,7 +680,7 @@ tree-sitter-elixir = "0.3"
tree-sitter-embedded-template = "0.23.0"
tree-sitter-gitcommit = { git = "https://github.com/zed-industries/tree-sitter-git-commit", rev = "88309716a69dd13ab83443721ba6e0b491d37ee9" }
tree-sitter-go = "0.23"
tree-sitter-go-mod = { git = "https://github.com/camdencheek/tree-sitter-go-mod", rev = "2e886870578eeba1927a2dc4bd2e2b3f598c5f9a", package = "tree-sitter-gomod" }
tree-sitter-go-mod = { git = "https://github.com/camdencheek/tree-sitter-go-mod", rev = "6efb59652d30e0e9cd5f3b3a669afd6f1a926d3c", package = "tree-sitter-gomod" }
tree-sitter-gowork = { git = "https://github.com/zed-industries/tree-sitter-go-work", rev = "acb0617bf7f4fda02c6217676cc64acb89536dc7" }
tree-sitter-heex = { git = "https://github.com/zed-industries/tree-sitter-heex", rev = "1dd45142fbb05562e35b2040c6129c9bca346592" }
tree-sitter-html = "0.23"

View File

@@ -8,110 +8,100 @@
; to other areas too.
<all>
= @cole-miller
= @ConradIrwin
= @danilo-leal
= @dinocosta
= @HactarCE
= @kubkon
= @maxdeviant
= @p1n3appl3
= @probably-neb
= @smitbarmase
= @SomeoneToIgnore
= @Veykril
ai
= @benbrandt
= @bennetbo
= @danilo-leal
= @rtfeldman
audio
= @dvdsk
crashes
= @p1n3appl3
= @Veykril
debugger
= @Anthony-Eid
= @kubkon
= @osiewicz
design
= @danilo-leal
docs
= @probably-neb
extension
= @danilo-leal
= @Veykril
= @kubkon
= @p1n3appl3
= @dinocosta
= @smitbarmase
= @cole-miller
vim
= @ConradIrwin
= @probably-neb
= @p1n3appl3
= @dinocosta
gpui
= @mikayla-maki
git
= @cole-miller
= @danilo-leal
gpui
= @Anthony-Eid
= @cameron1024
= @mikayla-maki
= @probably-neb
linux
= @dvdsk
= @smitbarmase
= @p1n3appl3
= @cole-miller
windows
= @reflectronic
= @localcc
pickers
= @p1n3appl3
= @dvdsk
= @SomeoneToIgnore
audio
= @dvdsk
helix
= @kubkon
languages
= @osiewicz
= @probably-neb
= @smitbarmase
= @SomeoneToIgnore
= @Veykril
linux
= @cole-miller
= @dvdsk
= @p1n3appl3
= @probably-neb
= @smitbarmase
lsp
= @osiewicz
= @smitbarmase
= @SomeoneToIgnore
= @Veykril
multi_buffer
= @Veykril
= @SomeoneToIgnore
pickers
= @dvdsk
= @p1n3appl3
= @SomeoneToIgnore
project_panel
= @smitbarmase
settings_ui
= @Anthony-Eid
= @danilo-leal
= @probably-neb
tasks
= @SomeoneToIgnore
= @Veykril
terminal
= @kubkon
= @Veykril
vim
= @ConradIrwin
= @dinocosta
= @p1n3appl3
= @probably-neb
debugger
= @kubkon
= @osiewicz
= @Anthony-Eid
windows
= @localcc
= @reflectronic
extension
= @kubkon
settings_ui
= @probably-neb
= @danilo-leal
= @Anthony-Eid
crashes
= @p1n3appl3
= @Veykril
ai
= @rtfeldman
= @danilo-leal
= @benbrandt
design
= @danilo-leal
multi_buffer
= @Veykril
= @SomeoneToIgnore
lsp
= @osiewicz
= @Veykril
= @smitbarmase
= @SomeoneToIgnore
languages
= @osiewicz
= @Veykril
= @smitbarmase
= @SomeoneToIgnore
project_panel
= @smitbarmase
tasks
= @SomeoneToIgnore
= @Veykril

View File

@@ -1,4 +0,0 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M11.3335 13.3333L8.00017 10L4.66685 13.3333" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M11.3335 2.66669L8.00017 6.00002L4.66685 2.66669" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 382 B

View File

@@ -1,5 +0,0 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6.2 11H5C4.20435 11 3.44129 10.6839 2.87868 10.1213C2.31607 9.55871 2 8.79565 2 8C2 7.20435 2.31607 6.44129 2.87868 5.87868C3.44129 5.31607 4.20435 5 5 5H6.2" stroke="#C4CAD4" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M9.80005 5H11C11.7957 5 12.5588 5.31607 13.1214 5.87868C13.684 6.44129 14 7.20435 14 8C14 8.79565 13.684 9.55871 13.1214 10.1213C12.5588 10.6839 11.7957 11 11 11H9.80005" stroke="#C4CAD4" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M5.6001 8H10.4001" stroke="#C4CAD4" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 735 B

View File

Before

Width:  |  Height:  |  Size: 5.5 KiB

After

Width:  |  Height:  |  Size: 5.5 KiB

View File

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 3.9 KiB

View File

@@ -43,8 +43,7 @@
"f11": "zed::ToggleFullScreen",
"ctrl-alt-z": "edit_prediction::RateCompletions",
"ctrl-alt-shift-i": "edit_prediction::ToggleMenu",
"ctrl-alt-l": "lsp_tool::ToggleMenu",
"ctrl-alt-.": "project_panel::ToggleHideHidden"
"ctrl-alt-l": "lsp_tool::ToggleMenu"
}
},
{
@@ -236,13 +235,12 @@
"ctrl-alt-n": "agent::NewTextThread",
"ctrl-shift-h": "agent::OpenHistory",
"ctrl-alt-c": "agent::OpenSettings",
"ctrl-alt-p": "agent::ManageProfiles",
"ctrl-alt-l": "agent::OpenRulesLibrary",
"ctrl-alt-p": "agent::OpenRulesLibrary",
"ctrl-i": "agent::ToggleProfileSelector",
"ctrl-alt-/": "agent::ToggleModelSelector",
"ctrl-shift-a": "agent::ToggleContextPicker",
"ctrl-shift-j": "agent::ToggleNavigationMenu",
"ctrl-alt-i": "agent::ToggleOptionsMenu",
"ctrl-shift-i": "agent::ToggleOptionsMenu",
"ctrl-alt-shift-n": "agent::ToggleNewThreadMenu",
"shift-alt-escape": "agent::ExpandMessageEditor",
"ctrl->": "agent::AddSelectionToThread",
@@ -409,7 +407,6 @@
"bindings": {
"escape": "project_search::ToggleFocus",
"shift-find": "search::FocusSearch",
"shift-enter": "project_search::ToggleAllSearchResults",
"ctrl-shift-f": "search::FocusSearch",
"ctrl-shift-h": "search::ToggleReplace",
"alt-ctrl-g": "search::ToggleRegex",
@@ -482,7 +479,6 @@
"alt-w": "search::ToggleWholeWord",
"alt-find": "project_search::ToggleFilters",
"alt-ctrl-f": "project_search::ToggleFilters",
"shift-enter": "project_search::ToggleAllSearchResults",
"ctrl-alt-shift-r": "search::ToggleRegex",
"ctrl-alt-shift-x": "search::ToggleRegex",
"alt-r": "search::ToggleRegex",
@@ -735,14 +731,6 @@
"tab": "editor::ComposeCompletion"
}
},
{
"context": "Editor && in_snippet",
"use_key_equivalents": true,
"bindings": {
"alt-right": "editor::NextSnippetTabstop",
"alt-left": "editor::PreviousSnippetTabstop"
}
},
// Bindings for accepting edit predictions
//
// alt-l is provided as an alternative to tab/alt-tab. and will be displayed in the UI. This is
@@ -945,7 +933,6 @@
"ctrl-g ctrl-g": "git::Fetch",
"ctrl-g up": "git::Push",
"ctrl-g down": "git::Pull",
"ctrl-g shift-down": "git::PullRebase",
"ctrl-g shift-up": "git::ForcePush",
"ctrl-g d": "git::Diff",
"ctrl-g backspace": "git::RestoreTrackedFiles",
@@ -1025,8 +1012,7 @@
"context": "CollabPanel",
"bindings": {
"alt-up": "collab_panel::MoveChannelUp",
"alt-down": "collab_panel::MoveChannelDown",
"alt-enter": "collab_panel::OpenSelectedChannelNotes"
"alt-down": "collab_panel::MoveChannelDown"
}
},
{
@@ -1140,8 +1126,7 @@
"ctrl-shift-space": "terminal::ToggleViMode",
"ctrl-shift-r": "terminal::RerunTask",
"ctrl-alt-r": "terminal::RerunTask",
"alt-t": "terminal::RerunTask",
"ctrl-shift-5": "pane::SplitRight"
"alt-t": "terminal::RerunTask"
}
},
{
@@ -1257,14 +1242,6 @@
"ctrl-shift-enter": "workspace::OpenWithSystem"
}
},
{
"context": "GitWorktreeSelector || (GitWorktreeSelector > Picker > Editor)",
"use_key_equivalents": true,
"bindings": {
"ctrl-shift-space": "git::WorktreeFromDefaultOnWindow",
"ctrl-space": "git::WorktreeFromDefault"
}
},
{
"context": "SettingsWindow",
"use_key_equivalents": true,

View File

@@ -49,8 +49,7 @@
"ctrl-cmd-f": "zed::ToggleFullScreen",
"ctrl-cmd-z": "edit_prediction::RateCompletions",
"ctrl-cmd-i": "edit_prediction::ToggleMenu",
"ctrl-cmd-l": "lsp_tool::ToggleMenu",
"cmd-alt-.": "project_panel::ToggleHideHidden"
"ctrl-cmd-l": "lsp_tool::ToggleMenu"
}
},
{
@@ -275,13 +274,12 @@
"cmd-alt-n": "agent::NewTextThread",
"cmd-shift-h": "agent::OpenHistory",
"cmd-alt-c": "agent::OpenSettings",
"cmd-alt-l": "agent::OpenRulesLibrary",
"cmd-alt-p": "agent::ManageProfiles",
"cmd-alt-p": "agent::OpenRulesLibrary",
"cmd-i": "agent::ToggleProfileSelector",
"cmd-alt-/": "agent::ToggleModelSelector",
"cmd-shift-a": "agent::ToggleContextPicker",
"cmd-shift-j": "agent::ToggleNavigationMenu",
"cmd-alt-m": "agent::ToggleOptionsMenu",
"cmd-shift-i": "agent::ToggleOptionsMenu",
"cmd-alt-shift-n": "agent::ToggleNewThreadMenu",
"shift-alt-escape": "agent::ExpandMessageEditor",
"cmd->": "agent::AddSelectionToThread",
@@ -470,7 +468,6 @@
"bindings": {
"escape": "project_search::ToggleFocus",
"cmd-shift-j": "project_search::ToggleFilters",
"shift-enter": "project_search::ToggleAllSearchResults",
"cmd-shift-f": "search::FocusSearch",
"cmd-shift-h": "search::ToggleReplace",
"alt-cmd-g": "search::ToggleRegex",
@@ -499,7 +496,6 @@
"bindings": {
"escape": "project_search::ToggleFocus",
"cmd-shift-j": "project_search::ToggleFilters",
"shift-enter": "project_search::ToggleAllSearchResults",
"cmd-shift-h": "search::ToggleReplace",
"alt-cmd-g": "search::ToggleRegex",
"alt-cmd-x": "search::ToggleRegex"
@@ -805,14 +801,6 @@
"tab": "editor::ComposeCompletion"
}
},
{
"context": "Editor && in_snippet",
"use_key_equivalents": true,
"bindings": {
"alt-right": "editor::NextSnippetTabstop",
"alt-left": "editor::PreviousSnippetTabstop"
}
},
{
"context": "Editor && edit_prediction",
"bindings": {
@@ -1038,7 +1026,6 @@
"ctrl-g ctrl-g": "git::Fetch",
"ctrl-g up": "git::Push",
"ctrl-g down": "git::Pull",
"ctrl-g shift-down": "git::PullRebase",
"ctrl-g shift-up": "git::ForcePush",
"ctrl-g d": "git::Diff",
"ctrl-g backspace": "git::RestoreTrackedFiles",
@@ -1090,8 +1077,7 @@
"use_key_equivalents": true,
"bindings": {
"alt-up": "collab_panel::MoveChannelUp",
"alt-down": "collab_panel::MoveChannelDown",
"alt-enter": "collab_panel::OpenSelectedChannelNotes"
"alt-down": "collab_panel::MoveChannelDown"
}
},
{
@@ -1223,7 +1209,6 @@
"ctrl-alt-down": "pane::SplitDown",
"ctrl-alt-left": "pane::SplitLeft",
"ctrl-alt-right": "pane::SplitRight",
"cmd-d": "pane::SplitRight",
"cmd-alt-r": "terminal::RerunTask"
}
},
@@ -1362,14 +1347,6 @@
"ctrl-shift-enter": "workspace::OpenWithSystem"
}
},
{
"context": "GitWorktreeSelector || (GitWorktreeSelector > Picker > Editor)",
"use_key_equivalents": true,
"bindings": {
"ctrl-shift-space": "git::WorktreeFromDefaultOnWindow",
"ctrl-space": "git::WorktreeFromDefault"
}
},
{
"context": "SettingsWindow",
"use_key_equivalents": true,

View File

@@ -41,8 +41,7 @@
"shift-f11": "debugger::StepOut",
"f11": "zed::ToggleFullScreen",
"ctrl-shift-i": "edit_prediction::ToggleMenu",
"shift-alt-l": "lsp_tool::ToggleMenu",
"ctrl-alt-.": "project_panel::ToggleHideHidden"
"shift-alt-l": "lsp_tool::ToggleMenu"
}
},
{
@@ -237,13 +236,12 @@
"shift-alt-n": "agent::NewTextThread",
"ctrl-shift-h": "agent::OpenHistory",
"shift-alt-c": "agent::OpenSettings",
"shift-alt-l": "agent::OpenRulesLibrary",
"shift-alt-p": "agent::ManageProfiles",
"shift-alt-p": "agent::OpenRulesLibrary",
"ctrl-i": "agent::ToggleProfileSelector",
"shift-alt-/": "agent::ToggleModelSelector",
"ctrl-shift-a": "agent::ToggleContextPicker",
"ctrl-shift-j": "agent::ToggleNavigationMenu",
"ctrl-alt-i": "agent::ToggleOptionsMenu",
"ctrl-shift-i": "agent::ToggleOptionsMenu",
// "ctrl-shift-alt-n": "agent::ToggleNewThreadMenu",
"shift-alt-escape": "agent::ExpandMessageEditor",
"ctrl-shift-.": "agent::AddSelectionToThread",
@@ -490,7 +488,6 @@
"alt-c": "search::ToggleCaseSensitive",
"alt-w": "search::ToggleWholeWord",
"alt-f": "project_search::ToggleFilters",
"shift-enter": "project_search::ToggleAllSearchResults",
"alt-r": "search::ToggleRegex",
// "ctrl-shift-alt-x": "search::ToggleRegex",
"ctrl-k shift-enter": "pane::TogglePinTab"
@@ -739,14 +736,6 @@
"tab": "editor::ComposeCompletion"
}
},
{
"context": "Editor && in_snippet",
"use_key_equivalents": true,
"bindings": {
"alt-right": "editor::NextSnippetTabstop",
"alt-left": "editor::PreviousSnippetTabstop"
}
},
// Bindings for accepting edit predictions
//
// alt-l is provided as an alternative to tab/alt-tab. and will be displayed in the UI. This is
@@ -954,7 +943,6 @@
"ctrl-g ctrl-g": "git::Fetch",
"ctrl-g up": "git::Push",
"ctrl-g down": "git::Pull",
"ctrl-g shift-down": "git::PullRebase",
"ctrl-g shift-up": "git::ForcePush",
"ctrl-g d": "git::Diff",
"ctrl-g backspace": "git::RestoreTrackedFiles",
@@ -1042,8 +1030,7 @@
"use_key_equivalents": true,
"bindings": {
"alt-up": "collab_panel::MoveChannelUp",
"alt-down": "collab_panel::MoveChannelDown",
"alt-enter": "collab_panel::OpenSelectedChannelNotes"
"alt-down": "collab_panel::MoveChannelDown"
}
},
{
@@ -1165,8 +1152,7 @@
"ctrl-shift-space": "terminal::ToggleViMode",
"ctrl-shift-r": "terminal::RerunTask",
"ctrl-alt-r": "terminal::RerunTask",
"alt-t": "terminal::RerunTask",
"ctrl-shift-5": "pane::SplitRight"
"alt-t": "terminal::RerunTask"
}
},
{
@@ -1284,14 +1270,6 @@
"shift-alt-a": "onboarding::OpenAccount"
}
},
{
"context": "GitWorktreeSelector || (GitWorktreeSelector > Picker > Editor)",
"use_key_equivalents": true,
"bindings": {
"ctrl-shift-space": "git::WorktreeFromDefaultOnWindow",
"ctrl-space": "git::WorktreeFromDefault"
}
},
{
"context": "SettingsWindow",
"use_key_equivalents": true,

View File

@@ -421,12 +421,6 @@
"ctrl-[": "editor::Cancel"
}
},
{
"context": "vim_mode == helix_select && !menu",
"bindings": {
"escape": "vim::SwitchToHelixNormalMode"
}
},
{
"context": "(vim_mode == helix_normal || vim_mode == helix_select) && !menu",
"bindings": {

View File

@@ -0,0 +1,179 @@
You are a highly skilled software engineer with extensive knowledge in many programming languages, frameworks, design patterns, and best practices.
## Communication
1. Be conversational but professional.
2. Refer to the user in the second person and yourself in the first person.
3. Format your responses in markdown. Use backticks to format file, directory, function, and class names.
4. NEVER lie or make things up.
5. Refrain from apologizing all the time when results are unexpected. Instead, just try your best to proceed or explain the circumstances to the user without apologizing.
{{#if has_tools}}
## Tool Use
1. Make sure to adhere to the tools schema.
2. Provide every required argument.
3. DO NOT use tools to access items that are already available in the context section.
4. Use only the tools that are currently available.
5. DO NOT use a tool that is not available just because it appears in the conversation. This means the user turned it off.
6. NEVER run commands that don't terminate on their own such as web servers (like `npm run start`, `npm run dev`, `python -m http.server`, etc) or file watchers.
7. Avoid HTML entity escaping - use plain characters instead.
## Searching and Reading
If you are unsure how to fulfill the user's request, gather more information with tool calls and/or clarifying questions.
{{! TODO: If there are files, we should mention it but otherwise omit that fact }}
If appropriate, use tool calls to explore the current project, which contains the following root directories:
{{#each worktrees}}
- `{{abs_path}}`
{{/each}}
- Bias towards not asking the user for help if you can find the answer yourself.
- When providing paths to tools, the path should always start with the name of a project root directory listed above.
- Before you read or edit a file, you must first find the full path. DO NOT ever guess a file path!
{{# if (has_tool 'grep') }}
- When looking for symbols in the project, prefer the `grep` tool.
- As you learn about the structure of the project, use that information to scope `grep` searches to targeted subtrees of the project.
- The user might specify a partial file path. If you don't know the full path, use `find_path` (not `grep`) before you read the file.
{{/if}}
{{else}}
You are being tasked with providing a response, but you have no ability to use tools or to read or write any aspect of the user's system (other than any context the user might have provided to you).
As such, if you need the user to perform any actions for you, you must request them explicitly. Bias towards giving a response to the best of your ability, and then making requests for the user to take action (e.g. to give you more context) only optionally.
The one exception to this is if the user references something you don't know about - for example, the name of a source code file, function, type, or other piece of code that you have no awareness of. In this case, you MUST NOT MAKE SOMETHING UP, or assume you know what that thing is or how it works. Instead, you must ask the user for clarification rather than giving a response.
{{/if}}
## Code Block Formatting
Whenever you mention a code block, you MUST use ONLY use the following format:
```path/to/Something.blah#L123-456
(code goes here)
```
The `#L123-456` means the line number range 123 through 456, and the path/to/Something.blah
is a path in the project. (If there is no valid path in the project, then you can use
/dev/null/path.extension for its path.) This is the ONLY valid way to format code blocks, because the Markdown parser
does not understand the more common ```language syntax, or bare ``` blocks. It only
understands this path-based syntax, and if the path is missing, then it will error and you will have to do it over again.
Just to be really clear about this, if you ever find yourself writing three backticks followed by a language name, STOP!
You have made a mistake. You can only ever put paths after triple backticks!
<example>
Based on all the information I've gathered, here's a summary of how this system works:
1. The README file is loaded into the system.
2. The system finds the first two headers, including everything in between. In this case, that would be:
```path/to/README.md#L8-12
# First Header
This is the info under the first header.
## Sub-header
```
3. Then the system finds the last header in the README:
```path/to/README.md#L27-29
## Last Header
This is the last header in the README.
```
4. Finally, it passes this information on to the next process.
</example>
<example>
In Markdown, hash marks signify headings. For example:
```/dev/null/example.md#L1-3
# Level 1 heading
## Level 2 heading
### Level 3 heading
```
</example>
Here are examples of ways you must never render code blocks:
<bad_example_do_not_do_this>
In Markdown, hash marks signify headings. For example:
```
# Level 1 heading
## Level 2 heading
### Level 3 heading
```
</bad_example_do_not_do_this>
This example is unacceptable because it does not include the path.
<bad_example_do_not_do_this>
In Markdown, hash marks signify headings. For example:
```markdown
# Level 1 heading
## Level 2 heading
### Level 3 heading
```
</bad_example_do_not_do_this>
This example is unacceptable because it has the language instead of the path.
<bad_example_do_not_do_this>
In Markdown, hash marks signify headings. For example:
# Level 1 heading
## Level 2 heading
### Level 3 heading
</bad_example_do_not_do_this>
This example is unacceptable because it uses indentation to mark the code block
instead of backticks with a path.
<bad_example_do_not_do_this>
In Markdown, hash marks signify headings. For example:
```markdown
/dev/null/example.md#L1-3
# Level 1 heading
## Level 2 heading
### Level 3 heading
```
</bad_example_do_not_do_this>
This example is unacceptable because the path is in the wrong place. The path must be directly after the opening backticks.
{{#if has_tools}}
## Fixing Diagnostics
1. Make 1-2 attempts at fixing diagnostics, then defer to the user.
2. Never simplify code you've written just to solve diagnostics. Complete, mostly correct code is more valuable than perfect code that doesn't solve the problem.
## Debugging
When debugging, only make code changes if you are certain that you can solve the problem.
Otherwise, follow debugging best practices:
1. Address the root cause instead of the symptoms.
2. Add descriptive logging statements and error messages to track variable and code state.
3. Add test functions and statements to isolate the problem.
{{/if}}
## Calling External APIs
1. Unless explicitly requested by the user, use the best suited external APIs and packages to solve the task. There is no need to ask the user for permission.
2. When selecting which version of an API or package to use, choose one that is compatible with the user's dependency management file(s). If no such file exists or if the package is not present, use the latest version that is in your training data.
3. If an external API requires an API Key, be sure to point this out to the user. Adhere to best security practices (e.g. DO NOT hardcode an API key in a place where it can be exposed)
## System Information
Operating System: {{os}}
Default Shell: {{shell}}
{{#if (or has_rules has_user_rules)}}
## User's Custom Instructions
The following additional instructions are provided by the user, and should be followed to the best of your ability{{#if has_tools}} without interfering with the tool use guidelines{{/if}}.
{{#if has_rules}}
There are project rules that apply to these root directories:
{{#each worktrees}}
{{#if rules_file}}
`{{root_name}}/{{rules_file.path_in_worktree}}`:
``````
{{{rules_file.text}}}
``````
{{/if}}
{{/each}}
{{/if}}
{{#if has_user_rules}}
The user has specified the following rules that should be applied:
{{#each user_rules}}
{{#if title}}
Rules title: {{title}}
{{/if}}
``````
{{contents}}
``````
{{/each}}
{{/if}}
{{/if}}

View File

@@ -255,19 +255,6 @@
// Whether to display inline and alongside documentation for items in the
// completions menu
"show_completion_documentation": true,
// When to show the scrollbar in the completion menu.
// This setting can take four values:
//
// 1. Show the scrollbar if there's important information or
// follow the system's configured behavior
// "auto"
// 2. Match the system's configured behavior:
// "system"
// 3. Always show the scrollbar:
// "always"
// 4. Never show the scrollbar:
// "never" (default)
"completion_menu_scrollbar": "never",
// Show method signatures in the editor, when inside parentheses.
"auto_signature_help": false,
// Whether to show the signature help after completion or a bracket pair inserted.
@@ -605,7 +592,7 @@
// to both the horizontal and vertical delta values while scrolling. Fast scrolling
// happens when a user holds the alt or option key while scrolling.
"fast_scroll_sensitivity": 4.0,
"relative_line_numbers": "disabled",
"relative_line_numbers": false,
// If 'search_wrap' is disabled, search result do not wrap around the end of the file.
"search_wrap": true,
// Search options to enable by default when opening new project and buffer searches.
@@ -615,9 +602,7 @@
"whole_word": false,
"case_sensitive": false,
"include_ignored": false,
"regex": false,
// Whether to center the cursor on each search match when navigating.
"center_on_match": false
"regex": false
},
// When to populate a new search's query based on the text under the cursor.
// This setting can take the following three values:
@@ -1247,9 +1232,6 @@
// that are overly broad can slow down Zed's file scanning. `file_scan_exclusions` takes
// precedence over these inclusions.
"file_scan_inclusions": [".env*"],
// Globs to match files that will be considered "hidden". These files can be hidden from the
// project panel by toggling the "hide_hidden" setting.
"hidden_files": ["**/.*"],
// Git gutter behavior configuration.
"git": {
// Control whether the git gutter is shown. May take 2 values:
@@ -1347,7 +1329,7 @@
"model": null,
"max_tokens": null
},
// Whether edit predictions are enabled when editing text threads in the agent panel.
// Whether edit predictions are enabled when editing text threads.
// This setting has no effect if globally disabled.
"enabled_in_text_threads": true
},
@@ -1718,7 +1700,6 @@
"preferred_line_length": 72
},
"Go": {
"hard_tabs": true,
"code_actions_on_format": {
"source.organizeImports": true
},
@@ -1737,9 +1718,6 @@
"allowed": true
}
},
"HTML+ERB": {
"language_servers": ["herb", "!ruby-lsp", "..."]
},
"Java": {
"prettier": {
"allowed": true,
@@ -1762,9 +1740,6 @@
"allowed": true
}
},
"JS+ERB": {
"language_servers": ["!ruby-lsp", "..."]
},
"Kotlin": {
"language_servers": ["!kotlin-language-server", "kotlin-lsp", "..."]
},
@@ -1779,7 +1754,6 @@
"Markdown": {
"format_on_save": "off",
"use_on_type_format": false,
"remove_trailing_whitespace_on_save": false,
"allow_rewrap": "anywhere",
"soft_wrap": "editor_width",
"prettier": {
@@ -1795,8 +1769,7 @@
}
},
"Plain Text": {
"allow_rewrap": "anywhere",
"soft_wrap": "editor_width"
"allow_rewrap": "anywhere"
},
"Python": {
"code_actions_on_format": {
@@ -1870,9 +1843,6 @@
"allowed": true
}
},
"YAML+ERB": {
"language_servers": ["!ruby-lsp", "..."]
},
"Zig": {
"language_servers": ["zls", "..."]
}

View File

@@ -6,8 +6,8 @@
{
"name": "Gruvbox Dark",
"appearance": "dark",
"accents": ["#cc241dff", "#98971aff", "#d79921ff", "#458588ff", "#b16286ff", "#689d6aff", "#d65d0eff"],
"style": {
"accents": ["#cc241dff", "#98971aff", "#d79921ff", "#458588ff", "#b16286ff", "#689d6aff", "#d65d0eff"],
"border": "#5b534dff",
"border.variant": "#494340ff",
"border.focused": "#303a36ff",
@@ -412,8 +412,8 @@
{
"name": "Gruvbox Dark Hard",
"appearance": "dark",
"accents": ["#cc241dff", "#98971aff", "#d79921ff", "#458588ff", "#b16286ff", "#689d6aff", "#d65d0eff"],
"style": {
"accents": ["#cc241dff", "#98971aff", "#d79921ff", "#458588ff", "#b16286ff", "#689d6aff", "#d65d0eff"],
"border": "#5b534dff",
"border.variant": "#494340ff",
"border.focused": "#303a36ff",
@@ -818,8 +818,8 @@
{
"name": "Gruvbox Dark Soft",
"appearance": "dark",
"accents": ["#cc241dff", "#98971aff", "#d79921ff", "#458588ff", "#b16286ff", "#689d6aff", "#d65d0eff"],
"style": {
"accents": ["#cc241dff", "#98971aff", "#d79921ff", "#458588ff", "#b16286ff", "#689d6aff", "#d65d0eff"],
"border": "#5b534dff",
"border.variant": "#494340ff",
"border.focused": "#303a36ff",
@@ -1224,8 +1224,8 @@
{
"name": "Gruvbox Light",
"appearance": "light",
"accents": ["#cc241dff", "#98971aff", "#d79921ff", "#458588ff", "#b16286ff", "#689d6aff", "#d65d0eff"],
"style": {
"accents": ["#cc241dff", "#98971aff", "#d79921ff", "#458588ff", "#b16286ff", "#689d6aff", "#d65d0eff"],
"border": "#c8b899ff",
"border.variant": "#ddcca7ff",
"border.focused": "#adc5ccff",
@@ -1630,8 +1630,8 @@
{
"name": "Gruvbox Light Hard",
"appearance": "light",
"accents": ["#cc241dff", "#98971aff", "#d79921ff", "#458588ff", "#b16286ff", "#689d6aff", "#d65d0eff"],
"style": {
"accents": ["#cc241dff", "#98971aff", "#d79921ff", "#458588ff", "#b16286ff", "#689d6aff", "#d65d0eff"],
"border": "#c8b899ff",
"border.variant": "#ddcca7ff",
"border.focused": "#adc5ccff",
@@ -2036,8 +2036,8 @@
{
"name": "Gruvbox Light Soft",
"appearance": "light",
"accents": ["#cc241dff", "#98971aff", "#d79921ff", "#458588ff", "#b16286ff", "#689d6aff", "#d65d0eff"],
"style": {
"accents": ["#cc241dff", "#98971aff", "#d79921ff", "#458588ff", "#b16286ff", "#689d6aff", "#d65d0eff"],
"border": "#c8b899ff",
"border.variant": "#ddcca7ff",
"border.focused": "#adc5ccff",

View File

@@ -3,6 +3,7 @@ mod diff;
mod mention;
mod terminal;
use ::terminal::terminal_settings::TerminalSettings;
use agent_settings::AgentSettings;
use collections::HashSet;
pub use connection::*;
@@ -11,7 +12,7 @@ use language::language_settings::FormatOnSave;
pub use mention::*;
use project::lsp_store::{FormatTrigger, LspFormatTarget};
use serde::{Deserialize, Serialize};
use settings::Settings as _;
use settings::{Settings as _, SettingsLocation};
use task::{Shell, ShellBuilder};
pub use terminal::*;
@@ -2140,9 +2141,17 @@ impl AcpThread {
) -> Task<Result<Entity<Terminal>>> {
let env = match &cwd {
Some(dir) => self.project.update(cx, |project, cx| {
project.environment().update(cx, |env, cx| {
env.directory_environment(dir.as_path().into(), cx)
})
let worktree = project.find_worktree(dir.as_path(), cx);
let shell = TerminalSettings::get(
worktree.as_ref().map(|(worktree, path)| SettingsLocation {
worktree_id: worktree.read(cx).id(),
path: &path,
}),
cx,
)
.shell
.clone();
project.directory_environment(&shell, dir.as_path().into(), cx)
}),
None => Task::ready(None).shared(),
};

View File

@@ -5,8 +5,10 @@ use gpui::{App, AppContext, AsyncApp, Context, Entity, Task};
use language::LanguageRegistry;
use markdown::Markdown;
use project::Project;
use settings::{Settings as _, SettingsLocation};
use std::{path::PathBuf, process::ExitStatus, sync::Arc, time::Instant};
use task::Shell;
use terminal::terminal_settings::TerminalSettings;
use util::get_default_system_shell_preferring_bash;
pub struct Terminal {
@@ -185,9 +187,17 @@ pub async fn create_terminal_entity(
let mut env = if let Some(dir) = &cwd {
project
.update(cx, |project, cx| {
project.environment().update(cx, |env, cx| {
env.directory_environment(dir.clone().into(), cx)
})
let worktree = project.find_worktree(dir.as_path(), cx);
let shell = TerminalSettings::get(
worktree.as_ref().map(|(worktree, path)| SettingsLocation {
worktree_id: worktree.read(cx).id(),
path: &path,
}),
cx,
)
.shell
.clone();
project.directory_environment(&shell, dir.clone().into(), cx)
})?
.await
.unwrap_or_default()

View File

@@ -19,7 +19,7 @@ use markdown::{CodeBlockRenderer, Markdown, MarkdownElement, MarkdownStyle};
use project::Project;
use settings::Settings;
use theme::ThemeSettings;
use ui::{Tooltip, WithScrollbar, prelude::*};
use ui::{Tooltip, prelude::*};
use util::ResultExt as _;
use workspace::{
Item, ItemHandle, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace,
@@ -259,15 +259,6 @@ impl AcpTools {
serde_json::to_string_pretty(&messages).ok()
}
fn clear_messages(&mut self, cx: &mut Context<Self>) {
if let Some(connection) = self.watched_connection.as_mut() {
connection.messages.clear();
connection.list_state.reset(0);
self.expanded.clear();
cx.notify();
}
}
fn render_message(
&mut self,
index: usize,
@@ -291,19 +282,17 @@ impl AcpTools {
let expanded = self.expanded.contains(&index);
v_flex()
.id(index)
.group("message")
.cursor_pointer()
.font_buffer(cx)
.w_full()
.px_4()
.py_3()
.pl_4()
.pr_5()
.gap_2()
.items_start()
.text_size(base_size)
.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) {
@@ -325,14 +314,15 @@ impl AcpTools {
h_flex()
.w_full()
.gap_2()
.items_center()
.flex_shrink_0()
.child(match message.direction {
acp::StreamMessageDirection::Incoming => Icon::new(IconName::ArrowDown)
.color(Color::Error)
.size(IconSize::Small),
acp::StreamMessageDirection::Outgoing => Icon::new(IconName::ArrowUp)
.color(Color::Success)
.size(IconSize::Small),
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())
@@ -502,7 +492,7 @@ impl Focusable for AcpTools {
}
impl Render for AcpTools {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
v_flex()
.track_focus(&self.focus_handle)
.size_full()
@@ -517,19 +507,13 @@ impl Render for AcpTools {
.child("No messages recorded yet")
.into_any()
} else {
div()
.size_full()
.flex_grow()
.child(
list(
connection.list_state.clone(),
cx.processor(Self::render_message),
)
.with_sizing_behavior(gpui::ListSizingBehavior::Auto)
.size_full(),
)
.vertical_scrollbar_for(connection.list_state.clone(), window, cx)
.into_any()
list(
connection.list_state.clone(),
cx.processor(Self::render_message),
)
.with_sizing_behavior(gpui::ListSizingBehavior::Auto)
.flex_grow()
.into_any()
}
}
None => h_flex()
@@ -563,16 +547,10 @@ impl Render for AcpToolsToolbarItemView {
};
let acp_tools = acp_tools.clone();
let has_messages = acp_tools
.read(cx)
.watched_connection
.as_ref()
.is_some_and(|connection| !connection.messages.is_empty());
h_flex()
.gap_2()
.child({
let acp_tools = acp_tools.clone();
.child(
IconButton::new(
"copy_all_messages",
if self.just_copied {
@@ -587,7 +565,13 @@ impl Render for AcpToolsToolbarItemView {
} else {
"Copy All Messages"
}))
.disabled(!has_messages)
.disabled(
acp_tools
.read(cx)
.watched_connection
.as_ref()
.is_none_or(|connection| connection.messages.is_empty()),
)
.on_click(cx.listener(move |this, _, _window, cx| {
if let Some(content) = acp_tools.read(cx).serialize_observed_messages() {
cx.write_to_clipboard(ClipboardItem::new_string(content));
@@ -602,18 +586,7 @@ impl Render for AcpToolsToolbarItemView {
})
.detach();
}
}))
})
.child(
IconButton::new("clear_messages", IconName::Trash)
.icon_size(IconSize::Small)
.tooltip(Tooltip::text("Clear Messages"))
.disabled(!has_messages)
.on_click(cx.listener(move |_this, _, _window, cx| {
acp_tools.update(cx, |acp_tools, cx| {
acp_tools.clear_messages(cx);
});
})),
})),
)
.into_any()
}

View File

@@ -11,7 +11,7 @@ use language::{
LanguageServerStatusUpdate, ServerHealth,
};
use project::{
LanguageServerProgress, LspStoreEvent, ProgressToken, Project, ProjectEnvironmentEvent,
LanguageServerProgress, LspStoreEvent, Project, ProjectEnvironmentEvent,
git_store::{GitStoreEvent, Repository},
};
use smallvec::SmallVec;
@@ -61,7 +61,7 @@ struct ServerStatus {
struct PendingWork<'a> {
language_server_id: LanguageServerId,
progress_token: &'a ProgressToken,
progress_token: &'a str,
progress: &'a LanguageServerProgress,
}
@@ -313,9 +313,9 @@ impl ActivityIndicator {
let mut pending_work = status
.pending_work
.iter()
.map(|(progress_token, progress)| PendingWork {
.map(|(token, progress)| PendingWork {
language_server_id: server_id,
progress_token,
progress_token: token.as_str(),
progress,
})
.collect::<SmallVec<[_; 4]>>();
@@ -358,7 +358,11 @@ impl ActivityIndicator {
..
}) = pending_work.next()
{
let mut message = progress.title.clone().unwrap_or(progress_token.to_string());
let mut message = progress
.title
.as_deref()
.unwrap_or(progress_token)
.to_string();
if let Some(percentage) = progress.percentage {
write!(&mut message, " ({}%)", percentage).unwrap();
@@ -769,7 +773,7 @@ impl Render for ActivityIndicator {
let Some(content) = self.content_to_render(cx) else {
return result;
};
let activity_indicator = cx.entity().downgrade();
let this = cx.entity().downgrade();
let truncate_content = content.message.len() > MAX_MESSAGE_LEN;
result.gap_2().child(
PopoverMenu::new("activity-indicator-popover")
@@ -811,21 +815,22 @@ impl Render for ActivityIndicator {
)
.anchor(gpui::Corner::BottomLeft)
.menu(move |window, cx| {
let strong_this = activity_indicator.upgrade()?;
let strong_this = this.upgrade()?;
let mut has_work = false;
let menu = ContextMenu::build(window, cx, |mut menu, _, cx| {
for work in strong_this.read(cx).pending_language_server_work(cx) {
has_work = true;
let activity_indicator = activity_indicator.clone();
let this = this.clone();
let mut title = work
.progress
.title
.clone()
.unwrap_or(work.progress_token.to_string());
.as_deref()
.unwrap_or(work.progress_token)
.to_owned();
if work.progress.is_cancellable {
let language_server_id = work.language_server_id;
let token = work.progress_token.clone();
let token = work.progress_token.to_string();
let title = SharedString::from(title);
menu = menu.custom_entry(
move |_, _| {
@@ -837,23 +842,18 @@ impl Render for ActivityIndicator {
.into_any_element()
},
move |_, cx| {
let token = token.clone();
activity_indicator
.update(cx, |activity_indicator, cx| {
activity_indicator.project.update(
this.update(cx, |this, cx| {
this.project.update(cx, |project, cx| {
project.cancel_language_server_work(
language_server_id,
Some(token.clone()),
cx,
|project, cx| {
project.cancel_language_server_work(
language_server_id,
Some(token),
cx,
);
},
);
activity_indicator.context_menu_handle.hide(cx);
cx.notify();
})
.ok();
});
this.context_menu_handle.hide(cx);
cx.notify();
})
.ok();
},
);
} else {

View File

@@ -13,15 +13,7 @@ const EDITS_END_TAG: &str = "</edits>";
const SEARCH_MARKER: &str = "<<<<<<< SEARCH";
const SEPARATOR_MARKER: &str = "=======";
const REPLACE_MARKER: &str = ">>>>>>> REPLACE";
const SONNET_PARAMETER_INVOKE_1: &str = "</parameter>\n</invoke>";
const SONNET_PARAMETER_INVOKE_2: &str = "</parameter></invoke>";
const END_TAGS: [&str; 5] = [
OLD_TEXT_END_TAG,
NEW_TEXT_END_TAG,
EDITS_END_TAG,
SONNET_PARAMETER_INVOKE_1, // Remove this after switching to streaming tool call
SONNET_PARAMETER_INVOKE_2,
];
const END_TAGS: [&str; 3] = [OLD_TEXT_END_TAG, NEW_TEXT_END_TAG, EDITS_END_TAG];
#[derive(Debug)]
pub enum EditParserEvent {
@@ -555,37 +547,6 @@ mod tests {
);
}
#[gpui::test(iterations = 1000)]
fn test_xml_edits_with_closing_parameter_invoke(mut rng: StdRng) {
// This case is a regression with Claude Sonnet 4.5.
// Sometimes Sonnet thinks that it's doing a tool call
// and closes its response with '</parameter></invoke>'
// instead of properly closing </new_text>
let mut parser = EditParser::new(EditFormat::XmlTags);
assert_eq!(
parse_random_chunks(
indoc! {"
<old_text>some text</old_text><new_text>updated text</parameter></invoke>
"},
&mut parser,
&mut rng
),
vec![Edit {
old_text: "some text".to_string(),
new_text: "updated text".to_string(),
line_hint: None,
},]
);
assert_eq!(
parser.finish(),
EditParserMetrics {
tags: 2,
mismatched_tags: 1
}
);
}
#[gpui::test(iterations = 1000)]
fn test_xml_nested_tags(mut rng: StdRng) {
let mut parser = EditParser::new(EditFormat::XmlTags);
@@ -1074,11 +1035,6 @@ mod tests {
last_ix = chunk_ix;
}
if new_text.is_some() {
pending_edit.new_text = new_text.take().unwrap();
edits.push(pending_edit);
}
edits
}
}

View File

@@ -1581,7 +1581,6 @@ impl EditAgentTest {
let template = crate::SystemPromptTemplate {
project: &project_context,
available_tools: tool_names,
model_name: None,
};
let templates = Templates::new();
template.render(&templates).unwrap()

View File

@@ -38,7 +38,6 @@ pub struct SystemPromptTemplate<'a> {
#[serde(flatten)]
pub project: &'a prompt_store::ProjectContext,
pub available_tools: Vec<SharedString>,
pub model_name: Option<String>,
}
impl Template for SystemPromptTemplate<'_> {
@@ -80,11 +79,9 @@ mod tests {
let template = SystemPromptTemplate {
project: &project,
available_tools: vec!["echo".into()],
model_name: Some("test-model".to_string()),
};
let templates = Templates::new();
let rendered = template.render(&templates).unwrap();
assert!(rendered.contains("## Fixing Diagnostics"));
assert!(rendered.contains("test-model"));
}
}

View File

@@ -150,12 +150,6 @@ Otherwise, follow debugging best practices:
Operating System: {{os}}
Default Shell: {{shell}}
{{#if model_name}}
## Model Information
You are powered by the model named {{model_name}}.
{{/if}}
{{#if (or has_rules has_user_rules)}}
## User's Custom Instructions

View File

@@ -1928,7 +1928,6 @@ impl Thread {
let system_prompt = SystemPromptTemplate {
project: self.project_context.read(cx),
available_tools,
model_name: self.model.as_ref().map(|m| m.name().0.to_string()),
}
.render(&self.templates)
.context("failed to build system prompt")

View File

@@ -4,7 +4,7 @@ use acp_thread::{AcpThread, AgentThreadEntry};
use agent::HistoryStore;
use agent_client_protocol::{self as acp, ToolCallId};
use collections::HashMap;
use editor::{Editor, EditorMode, MinimapVisibility, SizingBehavior};
use editor::{Editor, EditorMode, MinimapVisibility};
use gpui::{
AnyEntity, App, AppContext as _, Entity, EntityId, EventEmitter, FocusHandle, Focusable,
ScrollHandle, SharedString, TextStyleRefinement, WeakEntity, Window,
@@ -357,7 +357,7 @@ fn create_editor_diff(
EditorMode::Full {
scale_ui_elements_with_buffer_font_size: false,
show_active_line_background: false,
sizing_behavior: SizingBehavior::SizeByContent,
sized_by_content: true,
},
diff.read(cx).multibuffer().clone(),
None,

View File

@@ -16,7 +16,6 @@ use editor::{
MultiBuffer, ToOffset,
actions::Paste,
display_map::{Crease, CreaseId, FoldId},
scroll::Autoscroll,
};
use futures::{
FutureExt as _,
@@ -236,16 +235,8 @@ impl MessageEditor {
window: &mut Window,
cx: &mut Context<Self>,
) {
let uri = MentionUri::Thread {
id: thread.id.clone(),
name: thread.title.to_string(),
};
let content = format!("{}\n", uri.as_link());
let content_len = content.len() - 1;
let start = self.editor.update(cx, |editor, cx| {
editor.set_text(content, window, cx);
editor.set_text(format!("{}\n", thread.title), window, cx);
editor
.buffer()
.read(cx)
@@ -254,8 +245,18 @@ impl MessageEditor {
.text_anchor
});
self.confirm_mention_completion(thread.title, start, content_len, uri, window, cx)
.detach();
self.confirm_mention_completion(
thread.title.clone(),
start,
thread.title.len(),
MentionUri::Thread {
id: thread.id.clone(),
name: thread.title.to_string(),
},
window,
cx,
)
.detach();
}
#[cfg(test)]
@@ -592,21 +593,6 @@ impl MessageEditor {
),
);
}
// Take this explanation with a grain of salt but, with creases being
// inserted, GPUI's recomputes the editor layout in the next frames, so
// directly calling `editor.request_autoscroll` wouldn't work as
// expected. We're leveraging `cx.on_next_frame` to wait 2 frames and
// ensure that the layout has been recalculated so that the autoscroll
// request actually shows the cursor's new position.
let editor = self.editor.clone();
cx.on_next_frame(window, move |_, window, cx| {
cx.on_next_frame(window, move |_, _, cx| {
editor.update(cx, |editor, cx| {
editor.request_autoscroll(Autoscroll::fit(), cx)
});
});
});
}
fn confirm_mention_for_thread(
@@ -1046,7 +1032,6 @@ impl MessageEditor {
self.editor.update(cx, |message_editor, cx| {
message_editor.edit([(cursor_anchor..cursor_anchor, completion.new_text)], cx);
message_editor.request_autoscroll(Autoscroll::fit(), cx);
});
if let Some(confirm) = completion.confirm {
confirm(CompletionIntent::Complete, window, cx);
@@ -1616,7 +1601,6 @@ mod tests {
use gpui::{
AppContext, Entity, EventEmitter, FocusHandle, Focusable, TestAppContext, VisualTestContext,
};
use language_model::LanguageModelRegistry;
use lsp::{CompletionContext, CompletionTriggerKind};
use project::{CompletionIntent, Project, ProjectPath};
use serde_json::json;
@@ -2763,82 +2747,6 @@ mod tests {
}
}
#[gpui::test]
async fn test_insert_thread_summary(cx: &mut TestAppContext) {
init_test(cx);
cx.update(LanguageModelRegistry::test);
let fs = FakeFs::new(cx.executor());
fs.insert_tree("/project", json!({"file": ""})).await;
let project = Project::test(fs, [Path::new(path!("/project"))], cx).await;
let (workspace, cx) =
cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
let text_thread_store = cx.new(|cx| TextThreadStore::fake(project.clone(), cx));
let history_store = cx.new(|cx| HistoryStore::new(text_thread_store, cx));
// Create a thread metadata to insert as summary
let thread_metadata = agent::DbThreadMetadata {
id: acp::SessionId("thread-123".into()),
title: "Previous Conversation".into(),
updated_at: chrono::Utc::now(),
};
let message_editor = cx.update(|window, cx| {
cx.new(|cx| {
let mut editor = MessageEditor::new(
workspace.downgrade(),
project.clone(),
history_store.clone(),
None,
Default::default(),
Default::default(),
"Test Agent".into(),
"Test",
EditorMode::AutoHeight {
min_lines: 1,
max_lines: None,
},
window,
cx,
);
editor.insert_thread_summary(thread_metadata.clone(), window, cx);
editor
})
});
// Construct expected values for verification
let expected_uri = MentionUri::Thread {
id: thread_metadata.id.clone(),
name: thread_metadata.title.to_string(),
};
let expected_link = format!("[@{}]({})", thread_metadata.title, expected_uri.to_uri());
message_editor.read_with(cx, |editor, cx| {
let text = editor.text(cx);
assert!(
text.contains(&expected_link),
"Expected editor text to contain thread mention link.\nExpected substring: {}\nActual text: {}",
expected_link,
text
);
let mentions = editor.mentions();
assert_eq!(
mentions.len(),
1,
"Expected exactly one mention after inserting thread summary"
);
assert!(
mentions.contains(&expected_uri),
"Expected mentions to contain the thread URI"
);
});
}
#[gpui::test]
async fn test_whitespace_trimming(cx: &mut TestAppContext) {
init_test(cx);
@@ -2896,161 +2804,4 @@ mod tests {
})]
);
}
#[gpui::test]
async fn test_autoscroll_after_insert_selections(cx: &mut TestAppContext) {
init_test(cx);
let app_state = cx.update(AppState::test);
cx.update(|cx| {
language::init(cx);
editor::init(cx);
workspace::init(app_state.clone(), cx);
Project::init_settings(cx);
});
app_state
.fs
.as_fake()
.insert_tree(
path!("/dir"),
json!({
"test.txt": "line1\nline2\nline3\nline4\nline5\n",
}),
)
.await;
let project = Project::test(app_state.fs.clone(), [path!("/dir").as_ref()], cx).await;
let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
let workspace = window.root(cx).unwrap();
let worktree = project.update(cx, |project, cx| {
let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
assert_eq!(worktrees.len(), 1);
worktrees.pop().unwrap()
});
let worktree_id = worktree.read_with(cx, |worktree, _| worktree.id());
let mut cx = VisualTestContext::from_window(*window, cx);
// Open a regular editor with the created file, and select a portion of
// the text that will be used for the selections that are meant to be
// inserted in the agent panel.
let editor = workspace
.update_in(&mut cx, |workspace, window, cx| {
workspace.open_path(
ProjectPath {
worktree_id,
path: rel_path("test.txt").into(),
},
None,
false,
window,
cx,
)
})
.await
.unwrap()
.downcast::<Editor>()
.unwrap();
editor.update_in(&mut cx, |editor, window, cx| {
editor.change_selections(Default::default(), window, cx, |selections| {
selections.select_ranges([Point::new(0, 0)..Point::new(0, 5)]);
});
});
let text_thread_store = cx.new(|cx| TextThreadStore::fake(project.clone(), cx));
let history_store = cx.new(|cx| HistoryStore::new(text_thread_store, cx));
// Create a new `MessageEditor`. The `EditorMode::full()` has to be used
// to ensure we have a fixed viewport, so we can eventually actually
// place the cursor outside of the visible area.
let message_editor = workspace.update_in(&mut cx, |workspace, window, cx| {
let workspace_handle = cx.weak_entity();
let message_editor = cx.new(|cx| {
MessageEditor::new(
workspace_handle,
project.clone(),
history_store.clone(),
None,
Default::default(),
Default::default(),
"Test Agent".into(),
"Test",
EditorMode::full(),
window,
cx,
)
});
workspace.active_pane().update(cx, |pane, cx| {
pane.add_item(
Box::new(cx.new(|_| MessageEditorItem(message_editor.clone()))),
true,
true,
None,
window,
cx,
);
});
message_editor
});
message_editor.update_in(&mut cx, |message_editor, window, cx| {
message_editor.editor.update(cx, |editor, cx| {
// Update the Agent Panel's Message Editor text to have 100
// lines, ensuring that the cursor is set at line 90 and that we
// then scroll all the way to the top, so the cursor's position
// remains off screen.
let mut lines = String::new();
for _ in 1..=100 {
lines.push_str(&"Another line in the agent panel's message editor\n");
}
editor.set_text(lines.as_str(), window, cx);
editor.change_selections(Default::default(), window, cx, |selections| {
selections.select_ranges([Point::new(90, 0)..Point::new(90, 0)]);
});
editor.set_scroll_position(gpui::Point::new(0., 0.), window, cx);
});
});
cx.run_until_parked();
// Before proceeding, let's assert that the cursor is indeed off screen,
// otherwise the rest of the test doesn't make sense.
message_editor.update_in(&mut cx, |message_editor, window, cx| {
message_editor.editor.update(cx, |editor, cx| {
let snapshot = editor.snapshot(window, cx);
let cursor_row = editor.selections.newest::<Point>(&snapshot).head().row;
let scroll_top = snapshot.scroll_position().y as u32;
let visible_lines = editor.visible_line_count().unwrap() as u32;
let visible_range = scroll_top..(scroll_top + visible_lines);
assert!(!visible_range.contains(&cursor_row));
})
});
// Now let's insert the selection in the Agent Panel's editor and
// confirm that, after the insertion, the cursor is now in the visible
// range.
message_editor.update_in(&mut cx, |message_editor, window, cx| {
message_editor.insert_selections(window, cx);
});
cx.run_until_parked();
message_editor.update_in(&mut cx, |message_editor, window, cx| {
message_editor.editor.update(cx, |editor, cx| {
let snapshot = editor.snapshot(window, cx);
let cursor_row = editor.selections.newest::<Point>(&snapshot).head().row;
let scroll_top = snapshot.scroll_position().y as u32;
let visible_lines = editor.visible_line_count().unwrap() as u32;
let visible_range = scroll_top..(scroll_top + visible_lines);
assert!(visible_range.contains(&cursor_row));
})
});
}
}

View File

@@ -1,10 +1,8 @@
use acp_thread::AgentSessionModes;
use agent_client_protocol as acp;
use agent_servers::AgentServer;
use agent_settings::AgentSettings;
use fs::Fs;
use gpui::{Context, Entity, FocusHandle, WeakEntity, Window, prelude::*};
use settings::Settings as _;
use std::{rc::Rc, sync::Arc};
use ui::{
Button, ContextMenu, ContextMenuEntry, DocumentationEdge, DocumentationSide, KeyBinding,
@@ -86,14 +84,6 @@ impl ModeSelector {
let current_mode = self.connection.current_mode();
let default_mode = self.agent_server.default_mode(cx);
let settings = AgentSettings::get_global(cx);
let side = match settings.dock {
settings::DockPosition::Left => DocumentationSide::Right,
settings::DockPosition::Bottom | settings::DockPosition::Right => {
DocumentationSide::Left
}
};
for mode in all_modes {
let is_selected = &mode.id == &current_mode;
let is_default = Some(&mode.id) == default_mode.as_ref();
@@ -101,7 +91,7 @@ impl ModeSelector {
.toggleable(IconPosition::End, is_selected);
let entry = if let Some(description) = &mode.description {
entry.documentation_aside(side, DocumentationEdge::Bottom, {
entry.documentation_aside(DocumentationSide::Left, DocumentationEdge::Bottom, {
let description = description.clone();
move |cx| {

View File

@@ -450,7 +450,6 @@ impl Render for AcpThreadHistory {
v_flex()
.key_context("ThreadHistory")
.size_full()
.bg(cx.theme().colors().panel_background)
.on_action(cx.listener(Self::select_previous))
.on_action(cx.listener(Self::select_next))
.on_action(cx.listener(Self::select_first))

View File

@@ -17,9 +17,7 @@ use client::zed_urls;
use cloud_llm_client::PlanV1;
use collections::{HashMap, HashSet};
use editor::scroll::Autoscroll;
use editor::{
Editor, EditorEvent, EditorMode, MultiBuffer, PathKey, SelectionEffects, SizingBehavior,
};
use editor::{Editor, EditorEvent, EditorMode, MultiBuffer, PathKey, SelectionEffects};
use file_icons::FileIcons;
use fs::Fs;
use futures::FutureExt as _;
@@ -104,7 +102,7 @@ impl ThreadError {
{
Self::AuthenticationRequired(acp_error.message.clone().into())
} else {
let string = format!("{:#}", error);
let string = error.to_string();
// TODO: we should have Gemini return better errors here.
if agent.clone().downcast::<agent_servers::Gemini>().is_some()
&& string.contains("Could not load the default credentials")
@@ -113,7 +111,7 @@ impl ThreadError {
{
Self::AuthenticationRequired(string.into())
} else {
Self::Other(string.into())
Self::Other(error.to_string().into())
}
}
}
@@ -795,8 +793,7 @@ impl AcpThreadView {
if let Some(load_err) = err.downcast_ref::<LoadError>() {
self.thread_state = ThreadState::LoadError(load_err.clone());
} else {
self.thread_state =
ThreadState::LoadError(LoadError::Other(format!("{:#}", err).into()))
self.thread_state = ThreadState::LoadError(LoadError::Other(err.to_string().into()))
}
if self.message_editor.focus_handle(cx).is_focused(window) {
self.focus_handle.focus(window)
@@ -884,7 +881,6 @@ impl AcpThreadView {
cx: &mut Context<Self>,
) {
self.set_editor_is_expanded(!self.editor_expanded, cx);
cx.stop_propagation();
cx.notify();
}
@@ -896,7 +892,7 @@ impl AcpThreadView {
EditorMode::Full {
scale_ui_elements_with_buffer_font_size: false,
show_active_line_background: false,
sizing_behavior: SizingBehavior::ExcludeOverscrollMargin,
sized_by_content: false,
},
cx,
)
@@ -1506,6 +1502,12 @@ impl AcpThreadView {
})
.unwrap_or_default();
// Run SpawnInTerminal in the same dir as the ACP server
let cwd = connection
.clone()
.downcast::<agent_servers::AcpConnection>()
.map(|acp_conn| acp_conn.root_dir().to_path_buf());
// Build SpawnInTerminal from _meta
let login = task::SpawnInTerminal {
id: task::TaskId(format!("external-agent-{}-login", label)),
@@ -1514,6 +1516,7 @@ impl AcpThreadView {
command: Some(command.to_string()),
args,
command_label: label.to_string(),
cwd,
env,
use_new_terminal: true,
allow_concurrent_runs: true,
@@ -1526,8 +1529,9 @@ impl AcpThreadView {
pending_auth_method.replace(method.clone());
if let Some(workspace) = self.workspace.upgrade() {
let project = self.project.clone();
let authenticate = Self::spawn_external_agent_login(
login, workspace, false, window, cx,
login, workspace, project, false, true, window, cx,
);
cx.notify();
self.auth_task = Some(cx.spawn_in(window, {
@@ -1671,7 +1675,10 @@ impl AcpThreadView {
&& let Some(login) = self.login.clone()
{
if let Some(workspace) = self.workspace.upgrade() {
Self::spawn_external_agent_login(login, workspace, false, window, cx)
let project = self.project.clone();
Self::spawn_external_agent_login(
login, workspace, project, false, false, window, cx,
)
} else {
Task::ready(Ok(()))
}
@@ -1721,17 +1728,40 @@ impl AcpThreadView {
fn spawn_external_agent_login(
login: task::SpawnInTerminal,
workspace: Entity<Workspace>,
project: Entity<Project>,
previous_attempt: bool,
check_exit_code: bool,
window: &mut Window,
cx: &mut App,
) -> Task<Result<()>> {
let Some(terminal_panel) = workspace.read(cx).panel::<TerminalPanel>(cx) else {
return Task::ready(Ok(()));
};
let project = workspace.read(cx).project().clone();
window.spawn(cx, async move |cx| {
let mut task = login.clone();
if let Some(cmd) = &task.command {
// Have "node" command use Zed's managed Node runtime by default
if cmd == "node" {
let resolved_node_runtime = project
.update(cx, |project, cx| {
let agent_server_store = project.agent_server_store().clone();
agent_server_store.update(cx, |store, cx| {
store.node_runtime().map(|node_runtime| {
cx.background_spawn(async move {
node_runtime.binary_path().await
})
})
})
});
if let Ok(Some(resolve_task)) = resolved_node_runtime {
if let Ok(node_path) = resolve_task.await {
task.command = Some(node_path.to_string_lossy().to_string());
}
}
}
}
task.shell = task::Shell::WithArguments {
program: task.command.take().expect("login command should be set"),
args: std::mem::take(&mut task.args),
@@ -1749,44 +1779,65 @@ impl AcpThreadView {
})?;
let terminal = terminal.await?;
let mut exit_status = terminal
.read_with(cx, |terminal, cx| terminal.wait_for_completed_task(cx))?
.fuse();
let logged_in = cx
.spawn({
let terminal = terminal.clone();
async move |cx| {
loop {
cx.background_executor().timer(Duration::from_secs(1)).await;
let content =
terminal.update(cx, |terminal, _cx| terminal.get_content())?;
if content.contains("Login successful")
|| content.contains("Type your message")
{
return anyhow::Ok(());
if check_exit_code {
// For extension-based auth, wait for the process to exit and check exit code
let exit_status = terminal
.read_with(cx, |terminal, cx| terminal.wait_for_completed_task(cx))?
.await;
match exit_status {
Some(status) if status.success() => {
Ok(())
}
Some(status) => {
Err(anyhow!("Login command failed with exit code: {:?}", status.code()))
}
None => {
Err(anyhow!("Login command terminated without exit status"))
}
}
} else {
// For hardcoded agents (claude-login, gemini-cli): look for specific output
let mut exit_status = terminal
.read_with(cx, |terminal, cx| terminal.wait_for_completed_task(cx))?
.fuse();
let logged_in = cx
.spawn({
let terminal = terminal.clone();
async move |cx| {
loop {
cx.background_executor().timer(Duration::from_secs(1)).await;
let content =
terminal.update(cx, |terminal, _cx| terminal.get_content())?;
if content.contains("Login successful")
|| content.contains("Type your message")
{
return anyhow::Ok(());
}
}
}
})
.fuse();
futures::pin_mut!(logged_in);
futures::select_biased! {
result = logged_in => {
if let Err(e) = result {
log::error!("{e}");
return Err(anyhow!("exited before logging in"));
}
}
})
.fuse();
futures::pin_mut!(logged_in);
futures::select_biased! {
result = logged_in => {
if let Err(e) = result {
log::error!("{e}");
_ = exit_status => {
if !previous_attempt && project.read_with(cx, |project, _| project.is_via_remote_server())? && login.label.contains("gemini") {
return cx.update(|window, cx| Self::spawn_external_agent_login(login, workspace, project.clone(), true, false, window, cx))?.await
}
return Err(anyhow!("exited before logging in"));
}
}
_ = exit_status => {
if !previous_attempt && project.read_with(cx, |project, _| project.is_via_remote_server())? && login.label.contains("gemini") {
return cx.update(|window, cx| Self::spawn_external_agent_login(login, workspace, true, window, cx))?.await
}
return Err(anyhow!("exited before logging in"));
}
terminal.update(cx, |terminal, _| terminal.kill_active_task())?;
Ok(())
}
terminal.update(cx, |terminal, _| terminal.kill_active_task())?;
Ok(())
})
}
@@ -3735,7 +3786,6 @@ impl AcpThreadView {
.child(
h_flex()
.id("edits-container")
.cursor_pointer()
.gap_1()
.child(Disclosure::new("edits-disclosure", expanded))
.map(|this| {
@@ -3875,7 +3925,6 @@ impl AcpThreadView {
Label::new(name.to_string())
.size(LabelSize::XSmall)
.buffer_font(cx)
.ml_1p5()
});
let file_icon = FileIcons::get_icon(path.as_std_path(), cx)
@@ -3907,30 +3956,14 @@ impl AcpThreadView {
})
.child(
h_flex()
.id(("file-name-row", index))
.relative()
.id(("file-name", index))
.pr_8()
.gap_1p5()
.w_full()
.overflow_x_scroll()
.child(
h_flex()
.id(("file-name-path", index))
.cursor_pointer()
.pr_0p5()
.gap_0p5()
.hover(|s| s.bg(cx.theme().colors().element_hover))
.rounded_xs()
.child(file_icon)
.children(file_name)
.children(file_path)
.tooltip(Tooltip::text("Go to File"))
.on_click({
let buffer = buffer.clone();
cx.listener(move |this, _, window, cx| {
this.open_edited_buffer(&buffer, window, cx);
})
}),
)
.child(file_icon)
.child(h_flex().gap_0p5().children(file_name).children(file_path))
.child(
div()
.absolute()
@@ -3940,7 +3973,13 @@ impl AcpThreadView {
.bottom_0()
.right_0()
.bg(overlay_gradient),
),
)
.on_click({
let buffer = buffer.clone();
cx.listener(move |this, _, window, cx| {
this.open_edited_buffer(&buffer, window, cx);
})
}),
)
.child(
h_flex()
@@ -4082,12 +4121,8 @@ impl AcpThreadView {
)
}
})
.on_click(cx.listener(|this, _, window, cx| {
this.expand_message_editor(
&ExpandMessageEditor,
window,
cx,
);
.on_click(cx.listener(|_, _, window, cx| {
window.dispatch_action(Box::new(ExpandMessageEditor), cx);
})),
),
),
@@ -4691,29 +4726,14 @@ impl AcpThreadView {
window: &mut Window,
cx: &mut Context<Self>,
) {
if !self.notifications.is_empty() {
return;
}
let settings = AgentSettings::get_global(cx);
let window_is_inactive = !window.is_window_active();
let panel_is_hidden = self
.workspace
.upgrade()
.map(|workspace| AgentPanel::is_hidden(&workspace, cx))
.unwrap_or(true);
let should_notify = window_is_inactive || panel_is_hidden;
if !should_notify {
if window.is_window_active() || !self.notifications.is_empty() {
return;
}
// TODO: Change this once we have title summarization for external agents.
let title = self.agent.name();
match settings.notify_when_agent_waiting {
match AgentSettings::get_global(cx).notify_when_agent_waiting {
NotifyWhenAgentWaiting::PrimaryScreen => {
if let Some(primary) = cx.primary_display() {
self.pop_up(icon, caption.into(), title, window, primary, cx);
@@ -5716,7 +5736,7 @@ fn default_markdown_style(
let theme_settings = ThemeSettings::get_global(cx);
let colors = cx.theme().colors();
let buffer_font_size = theme_settings.agent_buffer_font_size(cx);
let buffer_font_size = TextSize::Small.rems(cx);
let mut text_style = window.text_style();
let line_height = buffer_font_size * 1.75;
@@ -5728,9 +5748,9 @@ fn default_markdown_style(
};
let font_size = if buffer_font {
theme_settings.agent_buffer_font_size(cx)
TextSize::Small.rems(cx)
} else {
theme_settings.agent_ui_font_size(cx)
TextSize::Default.rems(cx)
};
let text_color = if muted_text {
@@ -6027,107 +6047,6 @@ pub(crate) mod tests {
);
}
#[gpui::test]
async fn test_notification_when_panel_hidden(cx: &mut TestAppContext) {
init_test(cx);
let (thread_view, cx) = setup_thread_view(StubAgentServer::default_response(), cx).await;
add_to_workspace(thread_view.clone(), cx);
let message_editor = cx.read(|cx| thread_view.read(cx).message_editor.clone());
message_editor.update_in(cx, |editor, window, cx| {
editor.set_text("Hello", window, cx);
});
// Window is active (don't deactivate), but panel will be hidden
// Note: In the test environment, the panel is not actually added to the dock,
// so is_agent_panel_hidden will return true
thread_view.update_in(cx, |thread_view, window, cx| {
thread_view.send(window, cx);
});
cx.run_until_parked();
// Should show notification because window is active but panel is hidden
assert!(
cx.windows()
.iter()
.any(|window| window.downcast::<AgentNotification>().is_some()),
"Expected notification when panel is hidden"
);
}
#[gpui::test]
async fn test_notification_still_works_when_window_inactive(cx: &mut TestAppContext) {
init_test(cx);
let (thread_view, cx) = setup_thread_view(StubAgentServer::default_response(), cx).await;
let message_editor = cx.read(|cx| thread_view.read(cx).message_editor.clone());
message_editor.update_in(cx, |editor, window, cx| {
editor.set_text("Hello", window, cx);
});
// Deactivate window - should show notification regardless of setting
cx.deactivate_window();
thread_view.update_in(cx, |thread_view, window, cx| {
thread_view.send(window, cx);
});
cx.run_until_parked();
// Should still show notification when window is inactive (existing behavior)
assert!(
cx.windows()
.iter()
.any(|window| window.downcast::<AgentNotification>().is_some()),
"Expected notification when window is inactive"
);
}
#[gpui::test]
async fn test_notification_respects_never_setting(cx: &mut TestAppContext) {
init_test(cx);
// Set notify_when_agent_waiting to Never
cx.update(|cx| {
AgentSettings::override_global(
AgentSettings {
notify_when_agent_waiting: NotifyWhenAgentWaiting::Never,
..AgentSettings::get_global(cx).clone()
},
cx,
);
});
let (thread_view, cx) = setup_thread_view(StubAgentServer::default_response(), cx).await;
let message_editor = cx.read(|cx| thread_view.read(cx).message_editor.clone());
message_editor.update_in(cx, |editor, window, cx| {
editor.set_text("Hello", window, cx);
});
// Window is active
thread_view.update_in(cx, |thread_view, window, cx| {
thread_view.send(window, cx);
});
cx.run_until_parked();
// Should NOT show notification because notify_when_agent_waiting is Never
assert!(
!cx.windows()
.iter()
.any(|window| window.downcast::<AgentNotification>().is_some()),
"Expected no notification when notify_when_agent_waiting is Never"
);
}
async fn setup_thread_view(
agent: impl AgentServer + 'static,
cx: &mut TestAppContext,

View File

@@ -23,17 +23,15 @@ use language::LanguageRegistry;
use language_model::{
LanguageModelProvider, LanguageModelProviderId, LanguageModelRegistry, ZED_CLOUD_PROVIDER_ID,
};
use language_models::AllLanguageModelSettings;
use notifications::status_toast::{StatusToast, ToastIcon};
use project::{
agent_server_store::{AgentServerStore, CLAUDE_CODE_NAME, CODEX_NAME, GEMINI_NAME},
context_server_store::{ContextServerConfiguration, ContextServerStatus, ContextServerStore},
};
use settings::{Settings, SettingsStore, update_settings_file};
use settings::{SettingsStore, update_settings_file};
use ui::{
Button, ButtonStyle, Chip, CommonAnimationExt, ContextMenu, Disclosure, Divider, DividerColor,
ElevationIndex, IconName, IconPosition, IconSize, Indicator, LabelSize, PopoverMenu, Switch,
SwitchColor, Tooltip, WithScrollbar, prelude::*,
Chip, CommonAnimationExt, ContextMenu, Disclosure, Divider, DividerColor, ElevationIndex,
Indicator, PopoverMenu, Switch, SwitchColor, Tooltip, WithScrollbar, prelude::*,
};
use util::ResultExt as _;
use workspace::{Workspace, create_and_open_local_file};
@@ -154,42 +152,7 @@ pub enum AssistantConfigurationEvent {
impl EventEmitter<AssistantConfigurationEvent> for AgentConfiguration {}
enum AgentIcon {
Name(IconName),
Path(SharedString),
}
impl AgentConfiguration {
fn render_section_title(
&mut self,
title: impl Into<SharedString>,
description: impl Into<SharedString>,
menu: AnyElement,
) -> impl IntoElement {
h_flex()
.p_4()
.pb_0()
.mb_2p5()
.items_start()
.justify_between()
.child(
v_flex()
.w_full()
.gap_0p5()
.child(
h_flex()
.pr_1()
.w_full()
.gap_2()
.justify_between()
.flex_wrap()
.child(Headline::new(title.into()))
.child(menu),
)
.child(Label::new(description.into()).color(Color::Muted)),
)
}
fn render_provider_configuration_block(
&mut self,
provider: &Arc<dyn LanguageModelProvider>,
@@ -324,7 +287,7 @@ impl AgentConfiguration {
"Start New Thread",
)
.full_width()
.style(ButtonStyle::Outlined)
.style(ButtonStyle::Filled)
.layer(ElevationIndex::ModalSurface)
.icon_position(IconPosition::Start)
.icon(IconName::Thread)
@@ -340,122 +303,89 @@ impl AgentConfiguration {
}
})),
)
})
.when(
is_expanded && is_removable_provider(&provider.id(), cx),
|this| {
this.child(
Button::new(
SharedString::from(format!("delete-provider-{provider_id}")),
"Remove Provider",
)
.full_width()
.style(ButtonStyle::Outlined)
.icon_position(IconPosition::Start)
.icon(IconName::Trash)
.icon_size(IconSize::Small)
.icon_color(Color::Muted)
.label_size(LabelSize::Small)
.on_click(cx.listener({
let provider = provider.clone();
move |this, _event, window, cx| {
this.delete_provider(provider.clone(), window, cx);
}
})),
)
},
),
}),
)
}
fn delete_provider(
&mut self,
provider: Arc<dyn LanguageModelProvider>,
window: &mut Window,
cx: &mut Context<Self>,
) {
let fs = self.fs.clone();
let provider_id = provider.id();
cx.spawn_in(window, async move |_, cx| {
cx.update(|_window, cx| {
update_settings_file(fs.clone(), cx, {
let provider_id = provider_id.clone();
move |settings, _| {
if let Some(ref mut openai_compatible) = settings
.language_models
.as_mut()
.and_then(|lm| lm.openai_compatible.as_mut())
{
let key_to_remove: Arc<str> = Arc::from(provider_id.0.as_ref());
openai_compatible.remove(&key_to_remove);
}
}
});
})
.log_err();
cx.update(|_window, cx| {
LanguageModelRegistry::global(cx).update(cx, {
let provider_id = provider_id.clone();
move |registry, cx| {
registry.unregister_provider(provider_id, cx);
}
})
})
.log_err();
anyhow::Ok(())
})
.detach_and_log_err(cx);
}
fn render_provider_configuration_section(
&mut self,
cx: &mut Context<Self>,
) -> impl IntoElement {
let providers = LanguageModelRegistry::read_global(cx).providers();
let popover_menu = PopoverMenu::new("add-provider-popover")
.trigger(
Button::new("add-provider", "Add Provider")
.style(ButtonStyle::Outlined)
.icon_position(IconPosition::Start)
.icon(IconName::Plus)
.icon_size(IconSize::Small)
.icon_color(Color::Muted)
.label_size(LabelSize::Small),
)
.anchor(gpui::Corner::TopRight)
.menu({
let workspace = self.workspace.clone();
move |window, cx| {
Some(ContextMenu::build(window, cx, |menu, _window, _cx| {
menu.header("Compatible APIs").entry("OpenAI", None, {
let workspace = workspace.clone();
move |window, cx| {
workspace
.update(cx, |workspace, cx| {
AddLlmProviderModal::toggle(
LlmCompatibleProvider::OpenAi,
workspace,
window,
cx,
);
})
.log_err();
}
})
}))
}
});
v_flex()
.w_full()
.child(self.render_section_title(
"LLM Providers",
"Add at least one provider to use AI-powered features with Zed's native agent.",
popover_menu.into_any_element(),
))
.child(
h_flex()
.p(DynamicSpacing::Base16.rems(cx))
.pr(DynamicSpacing::Base20.rems(cx))
.pb_0()
.mb_2p5()
.items_start()
.justify_between()
.child(
v_flex()
.w_full()
.gap_0p5()
.child(
h_flex()
.pr_1()
.w_full()
.gap_2()
.justify_between()
.child(Headline::new("LLM Providers"))
.child(
PopoverMenu::new("add-provider-popover")
.trigger(
Button::new("add-provider", "Add Provider")
.style(ButtonStyle::Filled)
.layer(ElevationIndex::ModalSurface)
.icon_position(IconPosition::Start)
.icon(IconName::Plus)
.icon_size(IconSize::Small)
.icon_color(Color::Muted)
.label_size(LabelSize::Small),
)
.anchor(gpui::Corner::TopRight)
.menu({
let workspace = self.workspace.clone();
move |window, cx| {
Some(ContextMenu::build(
window,
cx,
|menu, _window, _cx| {
menu.header("Compatible APIs").entry(
"OpenAI",
None,
{
let workspace =
workspace.clone();
move |window, cx| {
workspace
.update(cx, |workspace, cx| {
AddLlmProviderModal::toggle(
LlmCompatibleProvider::OpenAi,
workspace,
window,
cx,
);
})
.log_err();
}
},
)
},
))
}
}),
),
)
.child(
Label::new("Add at least one provider to use AI-powered features with Zed's native agent.")
.color(Color::Muted),
),
),
)
.child(
div()
.w_full()
@@ -534,7 +464,8 @@ impl AgentConfiguration {
let add_server_popover = PopoverMenu::new("add-server-popover")
.trigger(
Button::new("add-server", "Add Server")
.style(ButtonStyle::Outlined)
.style(ButtonStyle::Filled)
.layer(ElevationIndex::ModalSurface)
.icon_position(IconPosition::Start)
.icon(IconName::Plus)
.icon_size(IconSize::Small)
@@ -567,57 +498,61 @@ impl AgentConfiguration {
});
v_flex()
.p(DynamicSpacing::Base16.rems(cx))
.pr(DynamicSpacing::Base20.rems(cx))
.gap_2()
.border_b_1()
.border_color(cx.theme().colors().border)
.child(self.render_section_title(
"Model Context Protocol (MCP) Servers",
"All MCP servers connected directly or via a Zed extension.",
add_server_popover.into_any_element(),
))
.child(
v_flex()
.pl_4()
.pb_4()
.pr_5()
h_flex()
.w_full()
.items_start()
.justify_between()
.gap_1()
.map(|mut parent| {
if context_server_ids.is_empty() {
parent.child(
h_flex()
.p_4()
.justify_center()
.border_1()
.border_dashed()
.border_color(cx.theme().colors().border.opacity(0.6))
.rounded_sm()
.child(
Label::new("No MCP servers added yet.")
.color(Color::Muted)
.size(LabelSize::Small),
),
)
} else {
for (index, context_server_id) in
context_server_ids.into_iter().enumerate()
{
if index > 0 {
parent = parent.child(
Divider::horizontal()
.color(DividerColor::BorderFaded)
.into_any_element(),
);
}
parent = parent.child(self.render_context_server(
context_server_id,
window,
cx,
));
}
parent
}
}),
.child(
v_flex()
.gap_0p5()
.child(Headline::new("Model Context Protocol (MCP) Servers"))
.child(
Label::new(
"All MCP servers connected directly or via a Zed extension.",
)
.color(Color::Muted),
),
)
.child(add_server_popover),
)
.child(v_flex().w_full().gap_1().map(|mut parent| {
if context_server_ids.is_empty() {
parent.child(
h_flex()
.p_4()
.justify_center()
.border_1()
.border_dashed()
.border_color(cx.theme().colors().border.opacity(0.6))
.rounded_sm()
.child(
Label::new("No MCP servers added yet.")
.color(Color::Muted)
.size(LabelSize::Small),
),
)
} else {
for (index, context_server_id) in context_server_ids.into_iter().enumerate() {
if index > 0 {
parent = parent.child(
Divider::horizontal()
.color(DividerColor::BorderFaded)
.into_any_element(),
);
}
parent =
parent.child(self.render_context_server(context_server_id, window, cx));
}
parent
}
}))
}
fn render_context_server(
@@ -662,12 +597,12 @@ impl AgentConfiguration {
let (source_icon, source_tooltip) = if is_from_extension {
(
IconName::ZedSrcExtension,
IconName::ZedMcpExtension,
"This MCP server was installed from an extension.",
)
} else {
(
IconName::ZedSrcCustom,
IconName::ZedMcpCustom,
"This custom MCP server was installed directly.",
)
};
@@ -949,9 +884,9 @@ impl AgentConfiguration {
}
fn render_agent_servers_section(&mut self, cx: &mut Context<Self>) -> impl IntoElement {
let agent_server_store = self.agent_server_store.read(cx);
let user_defined_agents = agent_server_store
let user_defined_agents = self
.agent_server_store
.read(cx)
.external_agents()
.filter(|name| {
name.0 != GEMINI_NAME && name.0 != CLAUDE_CODE_NAME && name.0 != CODEX_NAME
@@ -962,121 +897,102 @@ impl AgentConfiguration {
let user_defined_agents = user_defined_agents
.into_iter()
.map(|name| {
let icon = if let Some(icon_path) = agent_server_store.agent_icon(&name) {
AgentIcon::Path(icon_path)
} else {
AgentIcon::Name(IconName::Ai)
};
self.render_agent_server(icon, name, true)
self.render_agent_server(IconName::Ai, name)
.into_any_element()
})
.collect::<Vec<_>>();
let add_agens_button = Button::new("add-agent", "Add Agent")
.style(ButtonStyle::Outlined)
.icon_position(IconPosition::Start)
.icon(IconName::Plus)
.icon_size(IconSize::Small)
.icon_color(Color::Muted)
.label_size(LabelSize::Small)
.on_click(move |_, window, cx| {
if let Some(workspace) = window.root().flatten() {
let workspace = workspace.downgrade();
window
.spawn(cx, async |cx| {
open_new_agent_servers_entry_in_settings_editor(workspace, cx).await
})
.detach_and_log_err(cx);
}
});
v_flex()
.border_b_1()
.border_color(cx.theme().colors().border)
.child(
v_flex()
.child(self.render_section_title(
"External Agents",
"All agents connected through the Agent Client Protocol.",
add_agens_button.into_any_element(),
))
.p(DynamicSpacing::Base16.rems(cx))
.pr(DynamicSpacing::Base20.rems(cx))
.gap_2()
.child(
v_flex()
.p_4()
.pt_0()
.gap_2()
.child(self.render_agent_server(
AgentIcon::Name(IconName::AiClaude),
"Claude Code",
false,
))
.child(Divider::horizontal().color(DividerColor::BorderFaded))
.child(self.render_agent_server(
AgentIcon::Name(IconName::AiOpenAi),
"Codex",
false,
))
.child(Divider::horizontal().color(DividerColor::BorderFaded))
.child(self.render_agent_server(
AgentIcon::Name(IconName::AiGemini),
"Gemini CLI",
false,
))
.map(|mut parent| {
for agent in user_defined_agents {
parent = parent
.child(
Divider::horizontal().color(DividerColor::BorderFaded),
)
.child(agent);
}
parent
}),
),
.gap_0p5()
.child(
h_flex()
.pr_1()
.w_full()
.gap_2()
.justify_between()
.child(Headline::new("External Agents"))
.child(
Button::new("add-agent", "Add Agent")
.style(ButtonStyle::Filled)
.layer(ElevationIndex::ModalSurface)
.icon_position(IconPosition::Start)
.icon(IconName::Plus)
.icon_size(IconSize::Small)
.icon_color(Color::Muted)
.label_size(LabelSize::Small)
.on_click(
move |_, window, cx| {
if let Some(workspace) = window.root().flatten() {
let workspace = workspace.downgrade();
window
.spawn(cx, async |cx| {
open_new_agent_servers_entry_in_settings_editor(
workspace,
cx,
).await
})
.detach_and_log_err(cx);
}
}
),
)
)
.child(
Label::new(
"All agents connected through the Agent Client Protocol.",
)
.color(Color::Muted),
),
)
.child(self.render_agent_server(
IconName::AiClaude,
"Claude Code",
))
.child(Divider::horizontal().color(DividerColor::BorderFaded))
.child(self.render_agent_server(
IconName::AiOpenAi,
"Codex",
))
.child(Divider::horizontal().color(DividerColor::BorderFaded))
.child(self.render_agent_server(
IconName::AiGemini,
"Gemini CLI",
))
.map(|mut parent| {
for agent in user_defined_agents {
parent = parent.child(Divider::horizontal().color(DividerColor::BorderFaded))
.child(agent);
}
parent
})
)
}
fn render_agent_server(
&self,
icon: AgentIcon,
icon: IconName,
name: impl Into<SharedString>,
external: bool,
) -> impl IntoElement {
let name = name.into();
let icon = match icon {
AgentIcon::Name(icon_name) => Icon::new(icon_name)
.size(IconSize::Small)
.color(Color::Muted),
AgentIcon::Path(icon_path) => Icon::from_path(icon_path)
.size(IconSize::Small)
.color(Color::Muted),
};
let tooltip_id = SharedString::new(format!("agent-source-{}", name));
let tooltip_message = format!("The {} agent was installed from an extension.", name);
h_flex()
.gap_1p5()
.child(icon)
.child(Label::new(name))
.when(external, |this| {
this.child(
div()
.id(tooltip_id)
.flex_none()
.tooltip(Tooltip::text(tooltip_message))
.child(
Icon::new(IconName::ZedSrcExtension)
.size(IconSize::Small)
.color(Color::Muted),
),
)
})
.child(
Icon::new(IconName::Check)
.color(Color::Success)
.size(IconSize::Small),
)
h_flex().gap_1p5().justify_between().child(
h_flex()
.gap_1p5()
.child(Icon::new(icon).size(IconSize::Small).color(Color::Muted))
.child(Label::new(name.into()))
.child(
Icon::new(IconName::Check)
.size(IconSize::Small)
.color(Color::Success),
),
)
}
}
@@ -1305,14 +1221,3 @@ fn find_text_in_buffer(
None
}
}
// OpenAI-compatible providers are user-configured and can be removed,
// whereas built-in providers (like Anthropic, OpenAI, Google, etc.) can't.
//
// If in the future we have more "API-compatible-type" of providers,
// they should be included here as removable providers.
fn is_removable_provider(provider_id: &LanguageModelProviderId, cx: &App) -> bool {
AllLanguageModelSettings::get_global(cx)
.openai_compatible
.contains_key(provider_id.0.as_ref())
}

View File

@@ -70,6 +70,14 @@ impl AgentDiffThread {
}
}
fn is_generating(&self, cx: &App) -> bool {
match self {
AgentDiffThread::AcpThread(thread) => {
thread.read(cx).status() == acp_thread::ThreadStatus::Generating
}
}
}
fn has_pending_edit_tool_uses(&self, cx: &App) -> bool {
match self {
AgentDiffThread::AcpThread(thread) => thread.read(cx).has_pending_edit_tool_calls(),
@@ -962,7 +970,9 @@ impl AgentDiffToolbar {
None => ToolbarItemLocation::Hidden,
Some(AgentDiffToolbarItem::Pane(_)) => ToolbarItemLocation::PrimaryRight,
Some(AgentDiffToolbarItem::Editor { state, .. }) => match state {
EditorState::Reviewing => ToolbarItemLocation::PrimaryRight,
EditorState::Generating | EditorState::Reviewing => {
ToolbarItemLocation::PrimaryRight
}
EditorState::Idle => ToolbarItemLocation::Hidden,
},
}
@@ -1040,6 +1050,7 @@ impl Render for AgentDiffToolbar {
let content = match state {
EditorState::Idle => return Empty.into_any(),
EditorState::Generating => vec![spinner_icon],
EditorState::Reviewing => vec![
h_flex()
.child(
@@ -1211,6 +1222,7 @@ pub struct AgentDiff {
pub enum EditorState {
Idle,
Reviewing,
Generating,
}
struct WorkspaceThread {
@@ -1533,11 +1545,15 @@ impl AgentDiff {
multibuffer.add_diff(diff_handle.clone(), cx);
});
let reviewing_state = EditorState::Reviewing;
let new_state = if thread.is_generating(cx) {
EditorState::Generating
} else {
EditorState::Reviewing
};
let previous_state = self
.reviewing_editors
.insert(weak_editor.clone(), reviewing_state.clone());
.insert(weak_editor.clone(), new_state.clone());
if previous_state.is_none() {
editor.update(cx, |editor, cx| {
@@ -1550,9 +1566,7 @@ impl AgentDiff {
unaffected.remove(weak_editor);
}
if reviewing_state == EditorState::Reviewing
&& previous_state != Some(reviewing_state)
{
if new_state == EditorState::Reviewing && previous_state != Some(new_state) {
// Jump to first hunk when we enter review mode
editor.update(cx, |editor, cx| {
let snapshot = multibuffer.read(cx).snapshot(cx);

View File

@@ -19,6 +19,8 @@ use settings::{
use zed_actions::OpenBrowser;
use zed_actions::agent::{OpenClaudeCodeOnboardingModal, ReauthenticateAgent};
use crate::acp::{AcpThreadHistory, ThreadHistoryEvent};
use crate::context_store::ContextStore;
use crate::ui::{AcpOnboardingModal, ClaudeCodeOnboardingModal};
use crate::{
AddContextServer, AgentDiffPane, DeleteRecentlyOpenThread, Follow, InlineAssistant,
@@ -31,14 +33,9 @@ use crate::{
text_thread_editor::{AgentPanelDelegate, TextThreadEditor, make_lsp_adapter_delegate},
ui::{AgentOnboardingModal, EndTrialUpsell},
};
use crate::{
ExpandMessageEditor,
acp::{AcpThreadHistory, ThreadHistoryEvent},
};
use crate::{
ExternalAgent, NewExternalAgentThread, NewNativeAgentThreadFromSummary, placeholder_command,
};
use crate::{ManageProfiles, context_store::ContextStore};
use agent_settings::AgentSettings;
use ai_onboarding::AgentPanelOnboarding;
use anyhow::{Result, anyhow};
@@ -109,12 +106,6 @@ pub fn init(cx: &mut App) {
}
},
)
.register_action(|workspace, _: &ExpandMessageEditor, window, cx| {
if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
workspace.focus_panel::<AgentPanel>(window, cx);
panel.update(cx, |panel, cx| panel.expand_message_editor(window, cx));
}
})
.register_action(|workspace, _: &OpenHistory, window, cx| {
if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
workspace.focus_panel::<AgentPanel>(window, cx);
@@ -738,25 +729,6 @@ impl AgentPanel {
&self.context_server_registry
}
pub fn is_hidden(workspace: &Entity<Workspace>, cx: &App) -> bool {
let workspace_read = workspace.read(cx);
workspace_read
.panel::<AgentPanel>(cx)
.map(|panel| {
let panel_id = Entity::entity_id(&panel);
let is_visible = workspace_read.all_docks().iter().any(|dock| {
dock.read(cx)
.visible_panel()
.is_some_and(|visible_panel| visible_panel.panel_id() == panel_id)
});
!is_visible
})
.unwrap_or(true)
}
fn active_thread_view(&self) -> Option<&Entity<AcpThreadView>> {
match &self.active_view {
ActiveView::ExternalAgentThread { thread_view, .. } => Some(thread_view),
@@ -953,15 +925,6 @@ impl AgentPanel {
.detach_and_log_err(cx);
}
fn expand_message_editor(&mut self, window: &mut Window, cx: &mut Context<Self>) {
if let Some(thread_view) = self.active_thread_view() {
thread_view.update(cx, |view, cx| {
view.expand_message_editor(&ExpandMessageEditor, window, cx);
view.focus_handle(cx).focus(window);
});
}
}
fn open_history(&mut self, window: &mut Window, cx: &mut Context<Self>) {
if matches!(self.active_view, ActiveView::History) {
if let Some(previous_view) = self.previous_view.take() {
@@ -1780,9 +1743,10 @@ impl AgentPanel {
}),
)
.action("Add Custom Server…", Box::new(AddContextServer))
.separator()
.separator();
menu = menu
.action("Rules", Box::new(OpenRulesLibrary::default()))
.action("Profiles", Box::new(ManageProfiles::default()))
.action("Settings", Box::new(OpenSettings))
.separator()
.action(full_screen_label, Box::new(ToggleZoom));
@@ -2076,7 +2040,7 @@ impl AgentPanel {
let mut entry =
ContextMenuEntry::new(format!("New {} Thread", agent_name));
if let Some(icon_path) = icon_path {
entry = entry.custom_icon_path(icon_path);
entry = entry.custom_icon_svg(icon_path);
} else {
entry = entry.icon(IconName::Terminal);
}
@@ -2145,7 +2109,7 @@ impl AgentPanel {
.when_some(selected_agent_custom_icon, |this, icon_path| {
let label = selected_agent_label.clone();
this.px(DynamicSpacing::Base02.rems(cx))
.child(Icon::from_path(icon_path).color(Color::Muted))
.child(Icon::from_external_svg(icon_path).color(Color::Muted))
.tooltip(move |_window, cx| {
Tooltip::with_meta(label.clone(), None, "Selected Agent", cx)
})

View File

@@ -620,18 +620,8 @@ impl TextThreadContextHandle {
impl Display for TextThreadContext {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
write!(f, "<text_thread title=\"")?;
for c in self.title.chars() {
match c {
'&' => write!(f, "&amp;")?,
'<' => write!(f, "&lt;")?,
'>' => write!(f, "&gt;")?,
'"' => write!(f, "&quot;")?,
'\'' => write!(f, "&apos;")?,
_ => write!(f, "{}", c)?,
}
}
writeln!(f, "\">")?;
// TODO: escape title?
writeln!(f, "<text_thread title=\"{}\">", self.title)?;
write!(f, "{}", self.text.trim())?;
write!(f, "\n</text_thread>")
}

View File

@@ -260,10 +260,10 @@ impl<T: 'static> PromptEditor<T> {
let agent_panel_keybinding =
ui::text_for_action(&zed_actions::assistant::ToggleFocus, window, cx)
.map(|keybinding| format!("{keybinding} to chat"))
.map(|keybinding| format!("{keybinding} to chat"))
.unwrap_or_default();
format!("{action}… ({agent_panel_keybinding}↓↑ for history — @ to include context)")
format!("{action}… ({agent_panel_keybinding}↓↑ for history)")
}
pub fn prompt(&self, cx: &App) -> String {

View File

@@ -477,7 +477,7 @@ impl TextThreadEditor {
editor.insert(&format!("/{name}"), window, cx);
if command.accepts_arguments() {
editor.insert(" ", window, cx);
editor.show_completions(&ShowCompletions, window, cx);
editor.show_completions(&ShowCompletions::default(), window, cx);
}
});
});
@@ -2591,12 +2591,11 @@ impl SearchableItem for TextThreadEditor {
&mut self,
index: usize,
matches: &[Self::Match],
collapse: bool,
window: &mut Window,
cx: &mut Context<Self>,
) {
self.editor.update(cx, |editor, cx| {
editor.activate_match(index, matches, collapse, window, cx);
editor.activate_match(index, matches, window, cx);
});
}

View File

@@ -26,7 +26,6 @@ serde_json.workspace = true
settings.workspace = true
smol.workspace = true
tempfile.workspace = true
util.workspace = true
workspace.workspace = true
[target.'cfg(not(target_os = "windows"))'.dependencies]

View File

@@ -331,16 +331,6 @@ impl AutoUpdater {
pub fn start_polling(&self, cx: &mut Context<Self>) -> Task<Result<()>> {
cx.spawn(async move |this, cx| {
#[cfg(target_os = "windows")]
{
use util::ResultExt;
cleanup_windows()
.await
.context("failed to cleanup old directories")
.log_err();
}
loop {
this.update(cx, |this, cx| this.poll(UpdateCheckType::Automatic, cx))?;
cx.background_executor().timer(POLL_INTERVAL).await;
@@ -933,32 +923,6 @@ async fn install_release_macos(
Ok(None)
}
#[cfg(target_os = "windows")]
async fn cleanup_windows() -> Result<()> {
use util::ResultExt;
let parent = std::env::current_exe()?
.parent()
.context("No parent dir for Zed.exe")?
.to_owned();
// keep in sync with crates/auto_update_helper/src/updater.rs
smol::fs::remove_dir(parent.join("updates"))
.await
.context("failed to remove updates dir")
.log_err();
smol::fs::remove_dir(parent.join("install"))
.await
.context("failed to remove install dir")
.log_err();
smol::fs::remove_dir(parent.join("old"))
.await
.context("failed to remove old version dir")
.log_err();
Ok(())
}
async fn install_release_windows(downloaded_installer: PathBuf) -> Result<Option<PathBuf>> {
let output = Command::new(downloaded_installer)
.arg("/verysilent")
@@ -998,7 +962,7 @@ pub async fn finalize_auto_update_on_quit() {
.parent()
.map(|p| p.join("tools").join("auto_update_helper.exe"))
{
let mut command = util::command::new_smol_command(helper);
let mut command = smol::process::Command::new(helper);
command.arg("--launch");
command.arg("false");
if let Ok(mut cmd) = command.spawn() {

View File

@@ -21,9 +21,6 @@ simplelog.workspace = true
[target.'cfg(target_os = "windows")'.dependencies]
windows.workspace = true
[target.'cfg(target_os = "windows")'.dev-dependencies]
tempfile.workspace = true
[target.'cfg(target_os = "windows")'.build-dependencies]
winresource = "0.1"

View File

@@ -1,5 +1,4 @@
use std::{
cell::LazyCell,
path::Path,
time::{Duration, Instant},
};
@@ -12,274 +11,210 @@ use windows::Win32::{
use crate::windows_impl::WM_JOB_UPDATED;
pub(crate) struct Job {
pub apply: Box<dyn Fn(&Path) -> Result<()>>,
pub rollback: Box<dyn Fn(&Path) -> Result<()>>,
}
type Job = fn(&Path) -> Result<()>;
impl Job {
pub fn mkdir(name: &'static Path) -> Self {
Job {
apply: Box::new(move |app_dir| {
let dir = app_dir.join(name);
std::fs::create_dir_all(&dir)
.context(format!("Failed to create directory {}", dir.display()))
}),
rollback: Box::new(move |app_dir| {
let dir = app_dir.join(name);
std::fs::remove_dir_all(&dir)
.context(format!("Failed to remove directory {}", dir.display()))
}),
}
}
pub fn mkdir_if_exists(name: &'static Path, check: &'static Path) -> Self {
Job {
apply: Box::new(move |app_dir| {
let dir = app_dir.join(name);
let check = app_dir.join(check);
if check.exists() {
std::fs::create_dir_all(&dir)
.context(format!("Failed to create directory {}", dir.display()))?
}
Ok(())
}),
rollback: Box::new(move |app_dir| {
let dir = app_dir.join(name);
if dir.exists() {
std::fs::remove_dir_all(&dir)
.context(format!("Failed to remove directory {}", dir.display()))?
}
Ok(())
}),
}
}
pub fn move_file(filename: &'static Path, new_filename: &'static Path) -> Self {
Job {
apply: Box::new(move |app_dir| {
let old_file = app_dir.join(filename);
let new_file = app_dir.join(new_filename);
log::info!(
"Moving file: {}->{}",
old_file.display(),
new_file.display()
);
std::fs::rename(&old_file, new_file)
.context(format!("Failed to move file {}", old_file.display()))
}),
rollback: Box::new(move |app_dir| {
let old_file = app_dir.join(filename);
let new_file = app_dir.join(new_filename);
log::info!(
"Rolling back file move: {}->{}",
old_file.display(),
new_file.display()
);
std::fs::rename(&new_file, &old_file).context(format!(
"Failed to rollback file move {}->{}",
new_file.display(),
old_file.display()
))
}),
}
}
pub fn move_if_exists(filename: &'static Path, new_filename: &'static Path) -> Self {
Job {
apply: Box::new(move |app_dir| {
let old_file = app_dir.join(filename);
let new_file = app_dir.join(new_filename);
if old_file.exists() {
log::info!(
"Moving file: {}->{}",
old_file.display(),
new_file.display()
);
std::fs::rename(&old_file, new_file)
.context(format!("Failed to move file {}", old_file.display()))?;
}
Ok(())
}),
rollback: Box::new(move |app_dir| {
let old_file = app_dir.join(filename);
let new_file = app_dir.join(new_filename);
if new_file.exists() {
log::info!(
"Rolling back file move: {}->{}",
old_file.display(),
new_file.display()
);
std::fs::rename(&new_file, &old_file).context(format!(
"Failed to rollback file move {}->{}",
new_file.display(),
old_file.display()
))?
}
Ok(())
}),
}
}
pub fn rmdir_nofail(filename: &'static Path) -> Self {
Job {
apply: Box::new(move |app_dir| {
let filename = app_dir.join(filename);
log::info!("Removing file: {}", filename.display());
if let Err(e) = std::fs::remove_dir_all(&filename) {
log::warn!("Failed to remove directory: {}", e);
}
Ok(())
}),
rollback: Box::new(move |app_dir| {
let filename = app_dir.join(filename);
anyhow::bail!(
"Delete operations cannot be rolled back, file: {}",
filename.display()
)
}),
}
}
}
// app is single threaded
#[cfg(not(test))]
#[allow(clippy::declare_interior_mutable_const)]
pub(crate) const JOBS: LazyCell<[Job; 22]> = LazyCell::new(|| {
fn p(value: &str) -> &Path {
Path::new(value)
}
[
// Move old files
// Not deleting because installing new files can fail
Job::mkdir(p("old")),
Job::move_file(p("Zed.exe"), p("old\\Zed.exe")),
Job::mkdir(p("old\\bin")),
Job::move_file(p("bin\\Zed.exe"), p("old\\bin\\Zed.exe")),
Job::move_file(p("bin\\zed"), p("old\\bin\\zed")),
//
// TODO: remove after a few weeks once everyone is on the new version and this file never exists
Job::move_if_exists(p("OpenConsole.exe"), p("old\\OpenConsole.exe")),
Job::mkdir(p("old\\x64")),
Job::mkdir(p("old\\arm64")),
Job::move_if_exists(p("x64\\OpenConsole.exe"), p("old\\x64\\OpenConsole.exe")),
Job::move_if_exists(
p("arm64\\OpenConsole.exe"),
p("old\\arm64\\OpenConsole.exe"),
),
//
Job::move_file(p("conpty.dll"), p("old\\conpty.dll")),
// Copy new files
Job::move_file(p("install\\Zed.exe"), p("Zed.exe")),
Job::move_file(p("install\\bin\\Zed.exe"), p("bin\\Zed.exe")),
Job::move_file(p("install\\bin\\zed"), p("bin\\zed")),
//
Job::mkdir_if_exists(p("x64"), p("install\\x64")),
Job::mkdir_if_exists(p("arm64"), p("install\\arm64")),
Job::move_if_exists(
p("install\\x64\\OpenConsole.exe"),
p("x64\\OpenConsole.exe"),
),
Job::move_if_exists(
p("install\\arm64\\OpenConsole.exe"),
p("arm64\\OpenConsole.exe"),
),
//
Job::move_file(p("install\\conpty.dll"), p("conpty.dll")),
// Cleanup installer and updates folder
Job::rmdir_nofail(p("updates")),
Job::rmdir_nofail(p("install")),
// Cleanup old installation
Job::rmdir_nofail(p("old")),
]
});
pub(crate) const JOBS: &[Job] = &[
// Delete old files
|app_dir| {
let zed_executable = app_dir.join("Zed.exe");
log::info!("Removing old file: {}", zed_executable.display());
std::fs::remove_file(&zed_executable).context(format!(
"Failed to remove old file {}",
zed_executable.display()
))
},
|app_dir| {
let zed_cli = app_dir.join("bin\\zed.exe");
log::info!("Removing old file: {}", zed_cli.display());
std::fs::remove_file(&zed_cli)
.context(format!("Failed to remove old file {}", zed_cli.display()))
},
|app_dir| {
let zed_wsl = app_dir.join("bin\\zed");
log::info!("Removing old file: {}", zed_wsl.display());
std::fs::remove_file(&zed_wsl)
.context(format!("Failed to remove old file {}", zed_wsl.display()))
},
// TODO: remove after a few weeks once everyone is on the new version and this file never exists
|app_dir| {
let open_console = app_dir.join("OpenConsole.exe");
if open_console.exists() {
log::info!("Removing old file: {}", open_console.display());
std::fs::remove_file(&open_console).context(format!(
"Failed to remove old file {}",
open_console.display()
))?
}
Ok(())
},
|app_dir| {
let archs = ["x64", "arm64"];
for arch in archs {
let open_console = app_dir.join(format!("{arch}\\OpenConsole.exe"));
if open_console.exists() {
log::info!("Removing old file: {}", open_console.display());
std::fs::remove_file(&open_console).context(format!(
"Failed to remove old file {}",
open_console.display()
))?
}
}
Ok(())
},
|app_dir| {
let conpty = app_dir.join("conpty.dll");
log::info!("Removing old file: {}", conpty.display());
std::fs::remove_file(&conpty)
.context(format!("Failed to remove old file {}", conpty.display()))
},
// Copy new files
|app_dir| {
let zed_executable_source = app_dir.join("install\\Zed.exe");
let zed_executable_dest = app_dir.join("Zed.exe");
log::info!(
"Copying new file {} to {}",
zed_executable_source.display(),
zed_executable_dest.display()
);
std::fs::copy(&zed_executable_source, &zed_executable_dest)
.map(|_| ())
.context(format!(
"Failed to copy new file {} to {}",
zed_executable_source.display(),
zed_executable_dest.display()
))
},
|app_dir| {
let zed_cli_source = app_dir.join("install\\bin\\zed.exe");
let zed_cli_dest = app_dir.join("bin\\zed.exe");
log::info!(
"Copying new file {} to {}",
zed_cli_source.display(),
zed_cli_dest.display()
);
std::fs::copy(&zed_cli_source, &zed_cli_dest)
.map(|_| ())
.context(format!(
"Failed to copy new file {} to {}",
zed_cli_source.display(),
zed_cli_dest.display()
))
},
|app_dir| {
let zed_wsl_source = app_dir.join("install\\bin\\zed");
let zed_wsl_dest = app_dir.join("bin\\zed");
log::info!(
"Copying new file {} to {}",
zed_wsl_source.display(),
zed_wsl_dest.display()
);
std::fs::copy(&zed_wsl_source, &zed_wsl_dest)
.map(|_| ())
.context(format!(
"Failed to copy new file {} to {}",
zed_wsl_source.display(),
zed_wsl_dest.display()
))
},
|app_dir| {
let archs = ["x64", "arm64"];
for arch in archs {
let open_console_source = app_dir.join(format!("install\\{arch}\\OpenConsole.exe"));
let open_console_dest = app_dir.join(format!("{arch}\\OpenConsole.exe"));
if open_console_source.exists() {
log::info!(
"Copying new file {} to {}",
open_console_source.display(),
open_console_dest.display()
);
let parent = open_console_dest.parent().context(format!(
"Failed to get parent directory of {}",
open_console_dest.display()
))?;
std::fs::create_dir_all(parent)
.context(format!("Failed to create directory {}", parent.display()))?;
std::fs::copy(&open_console_source, &open_console_dest)
.map(|_| ())
.context(format!(
"Failed to copy new file {} to {}",
open_console_source.display(),
open_console_dest.display()
))?
}
}
Ok(())
},
|app_dir| {
let conpty_source = app_dir.join("install\\conpty.dll");
let conpty_dest = app_dir.join("conpty.dll");
log::info!(
"Copying new file {} to {}",
conpty_source.display(),
conpty_dest.display()
);
std::fs::copy(&conpty_source, &conpty_dest)
.map(|_| ())
.context(format!(
"Failed to copy new file {} to {}",
conpty_source.display(),
conpty_dest.display()
))
},
// Clean up installer folder and updates folder
|app_dir| {
let updates_folder = app_dir.join("updates");
log::info!("Cleaning up: {}", updates_folder.display());
std::fs::remove_dir_all(&updates_folder).context(format!(
"Failed to remove updates folder {}",
updates_folder.display()
))
},
|app_dir| {
let installer_folder = app_dir.join("install");
log::info!("Cleaning up: {}", installer_folder.display());
std::fs::remove_dir_all(&installer_folder).context(format!(
"Failed to remove installer folder {}",
installer_folder.display()
))
},
];
// app is single threaded
#[cfg(test)]
#[allow(clippy::declare_interior_mutable_const)]
pub(crate) const JOBS: LazyCell<[Job; 9]> = LazyCell::new(|| {
fn p(value: &str) -> &Path {
Path::new(value)
}
[
Job {
apply: Box::new(|_| {
std::thread::sleep(Duration::from_millis(1000));
if let Ok(config) = std::env::var("ZED_AUTO_UPDATE") {
match config.as_str() {
"err1" => Err(std::io::Error::other("Simulated error")).context("Anyhow!"),
"err2" => Ok(()),
_ => panic!("Unknown ZED_AUTO_UPDATE value: {}", config),
}
} else {
Ok(())
}
}),
rollback: Box::new(|_| {
unsafe { std::env::set_var("ZED_AUTO_UPDATE_RB", "rollback1") };
Ok(())
}),
},
Job::mkdir(p("test1")),
Job::mkdir_if_exists(p("test_exists"), p("test1")),
Job::mkdir_if_exists(p("test_missing"), p("dont")),
Job {
apply: Box::new(|folder| {
std::fs::write(folder.join("test1/test"), "test")?;
Ok(())
}),
rollback: Box::new(|folder| {
std::fs::remove_file(folder.join("test1/test"))?;
Ok(())
}),
},
Job::move_file(p("test1/test"), p("test1/moved")),
Job::move_if_exists(p("test1/test"), p("test1/noop")),
Job {
apply: Box::new(|_| {
std::thread::sleep(Duration::from_millis(1000));
if let Ok(config) = std::env::var("ZED_AUTO_UPDATE") {
match config.as_str() {
"err1" => Ok(()),
"err2" => Err(std::io::Error::other("Simulated error")).context("Anyhow!"),
_ => panic!("Unknown ZED_AUTO_UPDATE value: {}", config),
}
} else {
Ok(())
}
}),
rollback: Box::new(|_| Ok(())),
},
Job::rmdir_nofail(p("test1/nofolder")),
]
});
pub(crate) const JOBS: &[Job] = &[
|_| {
std::thread::sleep(Duration::from_millis(1000));
if let Ok(config) = std::env::var("ZED_AUTO_UPDATE") {
match config.as_str() {
"err" => Err(std::io::Error::other("Simulated error")).context("Anyhow!"),
_ => panic!("Unknown ZED_AUTO_UPDATE value: {}", config),
}
} else {
Ok(())
}
},
|_| {
std::thread::sleep(Duration::from_millis(1000));
if let Ok(config) = std::env::var("ZED_AUTO_UPDATE") {
match config.as_str() {
"err" => Err(std::io::Error::other("Simulated error")).context("Anyhow!"),
_ => panic!("Unknown ZED_AUTO_UPDATE value: {}", config),
}
} else {
Ok(())
}
},
];
pub(crate) fn perform_update(app_dir: &Path, hwnd: Option<isize>, launch: bool) -> Result<()> {
let hwnd = hwnd.map(|ptr| HWND(ptr as _));
let mut last_successful_job = None;
'outer: for (i, job) in JOBS.iter().enumerate() {
for job in JOBS.iter() {
let start = Instant::now();
loop {
if start.elapsed().as_secs() > 2 {
log::error!("Timed out, rolling back");
break 'outer;
}
match (job.apply)(app_dir) {
anyhow::ensure!(start.elapsed().as_secs() <= 2, "Timed out");
match (*job)(app_dir) {
Ok(_) => {
last_successful_job = Some(i);
unsafe { PostMessageW(hwnd, WM_JOB_UPDATED, WPARAM(0), LPARAM(0))? };
break;
}
@@ -288,7 +223,6 @@ pub(crate) fn perform_update(app_dir: &Path, hwnd: Option<isize>, launch: bool)
let io_err = err.downcast_ref::<std::io::Error>().unwrap();
if io_err.kind() == std::io::ErrorKind::NotFound {
log::warn!("File or folder not found.");
last_successful_job = Some(i);
unsafe { PostMessageW(hwnd, WM_JOB_UPDATED, WPARAM(0), LPARAM(0))? };
break;
}
@@ -299,28 +233,6 @@ pub(crate) fn perform_update(app_dir: &Path, hwnd: Option<isize>, launch: bool)
}
}
}
if last_successful_job
.map(|job| job != JOBS.len() - 1)
.unwrap_or(true)
{
let Some(last_successful_job) = last_successful_job else {
anyhow::bail!("Autoupdate failed, nothing to rollback");
};
for job in (0..=last_successful_job).rev() {
let job = &JOBS[job];
if let Err(e) = (job.rollback)(app_dir) {
anyhow::bail!(
"Job rollback failed, the app might be left in an inconsistent state: ({:?})",
e
);
}
}
anyhow::bail!("Autoupdate failed, rollback successful");
}
if launch {
#[allow(clippy::disallowed_methods, reason = "doesn't run in the main binary")]
let _ = std::process::Command::new(app_dir.join("Zed.exe")).spawn();
@@ -335,27 +247,12 @@ mod test {
#[test]
fn test_perform_update() {
let app_dir = tempfile::tempdir().unwrap();
let app_dir = app_dir.path();
let app_dir = std::path::Path::new("C:/");
assert!(perform_update(app_dir, None, false).is_ok());
let app_dir = tempfile::tempdir().unwrap();
let app_dir = app_dir.path();
// Simulate a timeout
unsafe { std::env::set_var("ZED_AUTO_UPDATE", "err1") };
unsafe { std::env::set_var("ZED_AUTO_UPDATE", "err") };
let ret = perform_update(app_dir, None, false);
assert!(
ret.is_err_and(|e| e.to_string().as_str() == "Autoupdate failed, nothing to rollback")
);
let app_dir = tempfile::tempdir().unwrap();
let app_dir = app_dir.path();
// Simulate a timeout
unsafe { std::env::set_var("ZED_AUTO_UPDATE", "err2") };
let ret = perform_update(app_dir, None, false);
assert!(
ret.is_err_and(|e| e.to_string().as_str() == "Autoupdate failed, rollback successful")
);
assert!(std::env::var("ZED_AUTO_UPDATE_RB").is_ok_and(|e| e == "rollback1"));
assert!(ret.is_err_and(|e| e.to_string().as_str() == "Timed out"));
}
}

View File

@@ -66,8 +66,6 @@ pub enum Model {
Claude3Sonnet,
#[serde(rename = "claude-3-5-haiku", alias = "claude-3-5-haiku-latest")]
Claude3_5Haiku,
#[serde(rename = "claude-haiku-4-5", alias = "claude-haiku-4-5-latest")]
ClaudeHaiku4_5,
Claude3_5Sonnet,
Claude3Haiku,
// Amazon Nova Models
@@ -149,8 +147,6 @@ impl Model {
Ok(Self::Claude3Sonnet)
} else if id.starts_with("claude-3-5-haiku") {
Ok(Self::Claude3_5Haiku)
} else if id.starts_with("claude-haiku-4-5") {
Ok(Self::ClaudeHaiku4_5)
} else if id.starts_with("claude-3-7-sonnet") {
Ok(Self::Claude3_7Sonnet)
} else if id.starts_with("claude-3-7-sonnet-thinking") {
@@ -184,7 +180,6 @@ impl Model {
Model::Claude3Sonnet => "claude-3-sonnet",
Model::Claude3Haiku => "claude-3-haiku",
Model::Claude3_5Haiku => "claude-3-5-haiku",
Model::ClaudeHaiku4_5 => "claude-haiku-4-5",
Model::Claude3_7Sonnet => "claude-3-7-sonnet",
Model::Claude3_7SonnetThinking => "claude-3-7-sonnet-thinking",
Model::AmazonNovaLite => "amazon-nova-lite",
@@ -251,7 +246,6 @@ impl Model {
Model::Claude3Sonnet => "anthropic.claude-3-sonnet-20240229-v1:0",
Model::Claude3Haiku => "anthropic.claude-3-haiku-20240307-v1:0",
Model::Claude3_5Haiku => "anthropic.claude-3-5-haiku-20241022-v1:0",
Model::ClaudeHaiku4_5 => "anthropic.claude-haiku-4-5-20251001-v1:0",
Model::Claude3_7Sonnet | Model::Claude3_7SonnetThinking => {
"anthropic.claude-3-7-sonnet-20250219-v1:0"
}
@@ -315,7 +309,6 @@ impl Model {
Self::Claude3Sonnet => "Claude 3 Sonnet",
Self::Claude3Haiku => "Claude 3 Haiku",
Self::Claude3_5Haiku => "Claude 3.5 Haiku",
Self::ClaudeHaiku4_5 => "Claude Haiku 4.5",
Self::Claude3_7Sonnet => "Claude 3.7 Sonnet",
Self::Claude3_7SonnetThinking => "Claude 3.7 Sonnet Thinking",
Self::AmazonNovaLite => "Amazon Nova Lite",
@@ -370,7 +363,6 @@ impl Model {
| Self::Claude3Opus
| Self::Claude3Sonnet
| Self::Claude3_5Haiku
| Self::ClaudeHaiku4_5
| Self::Claude3_7Sonnet
| Self::ClaudeSonnet4
| Self::ClaudeOpus4
@@ -393,7 +385,7 @@ impl Model {
Self::Claude3Opus | Self::Claude3Sonnet | Self::Claude3_5Haiku => 4_096,
Self::Claude3_7Sonnet | Self::Claude3_7SonnetThinking => 128_000,
Self::ClaudeSonnet4 | Self::ClaudeSonnet4Thinking => 64_000,
Self::ClaudeSonnet4_5 | Self::ClaudeSonnet4_5Thinking | Self::ClaudeHaiku4_5 => 64_000,
Self::ClaudeSonnet4_5 | Self::ClaudeSonnet4_5Thinking => 64_000,
Self::ClaudeOpus4
| Self::ClaudeOpus4Thinking
| Self::ClaudeOpus4_1
@@ -412,7 +404,6 @@ impl Model {
| Self::Claude3Opus
| Self::Claude3Sonnet
| Self::Claude3_5Haiku
| Self::ClaudeHaiku4_5
| Self::Claude3_7Sonnet
| Self::ClaudeOpus4
| Self::ClaudeOpus4Thinking
@@ -447,8 +438,7 @@ impl Model {
| Self::ClaudeSonnet4Thinking
| Self::ClaudeSonnet4_5
| Self::ClaudeSonnet4_5Thinking
| Self::Claude3_5Haiku
| Self::ClaudeHaiku4_5 => true,
| Self::Claude3_5Haiku => true,
// Amazon Nova models (all support tool use)
Self::AmazonNovaPremier
@@ -474,7 +464,6 @@ impl Model {
// Nova models support only text caching
// https://docs.aws.amazon.com/bedrock/latest/userguide/prompt-caching.html#prompt-caching-models
Self::Claude3_5Haiku
| Self::ClaudeHaiku4_5
| Self::Claude3_7Sonnet
| Self::Claude3_7SonnetThinking
| Self::ClaudeSonnet4
@@ -511,7 +500,7 @@ impl Model {
min_total_token: 1024,
}),
Self::Claude3_5Haiku | Self::ClaudeHaiku4_5 => Some(BedrockModelCacheConfiguration {
Self::Claude3_5Haiku => Some(BedrockModelCacheConfiguration {
max_cache_anchors: 4,
min_total_token: 2048,
}),
@@ -580,7 +569,6 @@ impl Model {
(
Model::AmazonNovaPremier
| Model::Claude3_5Haiku
| Model::ClaudeHaiku4_5
| Model::Claude3_5Sonnet
| Model::Claude3_5SonnetV2
| Model::Claude3_7Sonnet
@@ -618,7 +606,6 @@ impl Model {
// Models available in EU
(
Model::Claude3_5Sonnet
| Model::ClaudeHaiku4_5
| Model::Claude3_7Sonnet
| Model::Claude3_7SonnetThinking
| Model::ClaudeSonnet4
@@ -637,7 +624,6 @@ impl Model {
(
Model::Claude3_5Sonnet
| Model::Claude3_5SonnetV2
| Model::ClaudeHaiku4_5
| Model::Claude3Haiku
| Model::Claude3Sonnet
| Model::Claude3_7Sonnet

View File

@@ -100,21 +100,13 @@ impl Render for Breadcrumbs {
let breadcrumbs_stack = h_flex().gap_1().children(breadcrumbs);
let prefix_element = active_item.breadcrumb_prefix(window, cx);
let breadcrumbs = if let Some(prefix) = prefix_element {
h_flex().gap_1p5().child(prefix).child(breadcrumbs_stack)
} else {
breadcrumbs_stack
};
match active_item
.downcast::<Editor>()
.map(|editor| editor.downgrade())
{
Some(editor) => element.child(
ButtonLike::new("toggle outline view")
.child(breadcrumbs)
.child(breadcrumbs_stack)
.style(ButtonStyle::Transparent)
.on_click({
let editor = editor.clone();
@@ -149,7 +141,7 @@ impl Render for Breadcrumbs {
// Match the height and padding of the `ButtonLike` in the other arm.
.h(rems_from_px(22.))
.pl_1()
.child(breadcrumbs),
.child(breadcrumbs_stack),
}
}
}

View File

@@ -1,5 +1,4 @@
pub mod predict_edits_v3;
pub mod udiff;
use std::str::FromStr;
use std::sync::Arc;

View File

@@ -1,294 +0,0 @@
use std::{borrow::Cow, fmt::Display};
#[derive(Debug, PartialEq)]
pub enum DiffLine<'a> {
OldPath { path: Cow<'a, str> },
NewPath { path: Cow<'a, str> },
HunkHeader(Option<HunkLocation>),
Context(&'a str),
Deletion(&'a str),
Addition(&'a str),
Garbage(&'a str),
}
#[derive(Debug, PartialEq)]
pub struct HunkLocation {
start_line_old: u32,
count_old: u32,
start_line_new: u32,
count_new: u32,
}
impl<'a> DiffLine<'a> {
pub fn parse(line: &'a str) -> Self {
Self::try_parse(line).unwrap_or(Self::Garbage(line))
}
fn try_parse(line: &'a str) -> Option<Self> {
if let Some(header) = line.strip_prefix("---").and_then(eat_required_whitespace) {
let path = parse_header_path("a/", header);
Some(Self::OldPath { path })
} else if let Some(header) = line.strip_prefix("+++").and_then(eat_required_whitespace) {
Some(Self::NewPath {
path: parse_header_path("b/", header),
})
} else if let Some(header) = line.strip_prefix("@@").and_then(eat_required_whitespace) {
if header.starts_with("...") {
return Some(Self::HunkHeader(None));
}
let (start_line_old, header) = header.strip_prefix('-')?.split_once(',')?;
let mut parts = header.split_ascii_whitespace();
let count_old = parts.next()?;
let (start_line_new, count_new) = parts.next()?.strip_prefix('+')?.split_once(',')?;
Some(Self::HunkHeader(Some(HunkLocation {
start_line_old: start_line_old.parse::<u32>().ok()?.saturating_sub(1),
count_old: count_old.parse().ok()?,
start_line_new: start_line_new.parse::<u32>().ok()?.saturating_sub(1),
count_new: count_new.parse().ok()?,
})))
} else if let Some(deleted_header) = line.strip_prefix("-") {
Some(Self::Deletion(deleted_header))
} else if line.is_empty() {
Some(Self::Context(""))
} else if let Some(context) = line.strip_prefix(" ") {
Some(Self::Context(context))
} else {
Some(Self::Addition(line.strip_prefix("+")?))
}
}
}
impl<'a> Display for DiffLine<'a> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
DiffLine::OldPath { path } => write!(f, "--- {path}"),
DiffLine::NewPath { path } => write!(f, "+++ {path}"),
DiffLine::HunkHeader(Some(hunk_location)) => {
write!(
f,
"@@ -{},{} +{},{} @@",
hunk_location.start_line_old + 1,
hunk_location.count_old,
hunk_location.start_line_new + 1,
hunk_location.count_new
)
}
DiffLine::HunkHeader(None) => write!(f, "@@ ... @@"),
DiffLine::Context(content) => write!(f, " {content}"),
DiffLine::Deletion(content) => write!(f, "-{content}"),
DiffLine::Addition(content) => write!(f, "+{content}"),
DiffLine::Garbage(line) => write!(f, "{line}"),
}
}
}
fn parse_header_path<'a>(strip_prefix: &'static str, header: &'a str) -> Cow<'a, str> {
if !header.contains(['"', '\\']) {
let path = header.split_ascii_whitespace().next().unwrap_or(header);
return Cow::Borrowed(path.strip_prefix(strip_prefix).unwrap_or(path));
}
let mut path = String::with_capacity(header.len());
let mut in_quote = false;
let mut chars = header.chars().peekable();
let mut strip_prefix = Some(strip_prefix);
while let Some(char) = chars.next() {
if char == '"' {
in_quote = !in_quote;
} else if char == '\\' {
let Some(&next_char) = chars.peek() else {
break;
};
chars.next();
path.push(next_char);
} else if char.is_ascii_whitespace() && !in_quote {
break;
} else {
path.push(char);
}
if let Some(prefix) = strip_prefix
&& path == prefix
{
strip_prefix.take();
path.clear();
}
}
Cow::Owned(path)
}
fn eat_required_whitespace(header: &str) -> Option<&str> {
let trimmed = header.trim_ascii_start();
if trimmed.len() == header.len() {
None
} else {
Some(trimmed)
}
}
#[cfg(test)]
mod tests {
use super::*;
use indoc::indoc;
#[test]
fn parse_lines_simple() {
let input = indoc! {"
diff --git a/text.txt b/text.txt
index 86c770d..a1fd855 100644
--- a/file.txt
+++ b/file.txt
@@ -1,2 +1,3 @@
context
-deleted
+inserted
garbage
--- b/file.txt
+++ a/file.txt
"};
let lines = input.lines().map(DiffLine::parse).collect::<Vec<_>>();
pretty_assertions::assert_eq!(
lines,
&[
DiffLine::Garbage("diff --git a/text.txt b/text.txt"),
DiffLine::Garbage("index 86c770d..a1fd855 100644"),
DiffLine::OldPath {
path: "file.txt".into()
},
DiffLine::NewPath {
path: "file.txt".into()
},
DiffLine::HunkHeader(Some(HunkLocation {
start_line_old: 0,
count_old: 2,
start_line_new: 0,
count_new: 3
})),
DiffLine::Context("context"),
DiffLine::Deletion("deleted"),
DiffLine::Addition("inserted"),
DiffLine::Garbage("garbage"),
DiffLine::Context(""),
DiffLine::OldPath {
path: "b/file.txt".into()
},
DiffLine::NewPath {
path: "a/file.txt".into()
},
]
);
}
#[test]
fn file_header_extra_space() {
let options = ["--- file", "--- file", "---\tfile"];
for option in options {
pretty_assertions::assert_eq!(
DiffLine::parse(option),
DiffLine::OldPath {
path: "file".into()
},
"{option}",
);
}
}
#[test]
fn hunk_header_extra_space() {
let options = [
"@@ -1,2 +1,3 @@",
"@@ -1,2 +1,3 @@",
"@@\t-1,2\t+1,3\t@@",
"@@ -1,2 +1,3 @@",
"@@ -1,2 +1,3 @@",
"@@ -1,2 +1,3 @@",
"@@ -1,2 +1,3 @@ garbage",
];
for option in options {
pretty_assertions::assert_eq!(
DiffLine::parse(option),
DiffLine::HunkHeader(Some(HunkLocation {
start_line_old: 0,
count_old: 2,
start_line_new: 0,
count_new: 3
})),
"{option}",
);
}
}
#[test]
fn hunk_header_without_location() {
pretty_assertions::assert_eq!(DiffLine::parse("@@ ... @@"), DiffLine::HunkHeader(None));
}
#[test]
fn test_parse_path() {
assert_eq!(parse_header_path("a/", "foo.txt"), "foo.txt");
assert_eq!(
parse_header_path("a/", "foo/bar/baz.txt"),
"foo/bar/baz.txt"
);
assert_eq!(parse_header_path("a/", "a/foo.txt"), "foo.txt");
assert_eq!(
parse_header_path("a/", "a/foo/bar/baz.txt"),
"foo/bar/baz.txt"
);
// Extra
assert_eq!(
parse_header_path("a/", "a/foo/bar/baz.txt 2025"),
"foo/bar/baz.txt"
);
assert_eq!(
parse_header_path("a/", "a/foo/bar/baz.txt\t2025"),
"foo/bar/baz.txt"
);
assert_eq!(
parse_header_path("a/", "a/foo/bar/baz.txt \""),
"foo/bar/baz.txt"
);
// Quoted
assert_eq!(
parse_header_path("a/", "a/foo/bar/\"baz quox.txt\""),
"foo/bar/baz quox.txt"
);
assert_eq!(
parse_header_path("a/", "\"a/foo/bar/baz quox.txt\""),
"foo/bar/baz quox.txt"
);
assert_eq!(
parse_header_path("a/", "\"foo/bar/baz quox.txt\""),
"foo/bar/baz quox.txt"
);
assert_eq!(parse_header_path("a/", "\"whatever 🤷\""), "whatever 🤷");
assert_eq!(
parse_header_path("a/", "\"foo/bar/baz quox.txt\" 2025"),
"foo/bar/baz quox.txt"
);
// unescaped quotes are dropped
assert_eq!(parse_header_path("a/", "foo/\"bar\""), "foo/bar");
// Escaped
assert_eq!(
parse_header_path("a/", "\"foo/\\\"bar\\\"/baz.txt\""),
"foo/\"bar\"/baz.txt"
);
assert_eq!(
parse_header_path("a/", "\"C:\\\\Projects\\\\My App\\\\old file.txt\""),
"C:\\Projects\\My App\\old file.txt"
);
}
}

View File

@@ -182,8 +182,8 @@ pub fn build_prompt(
}
for related_file in &request.included_files {
write_codeblock(
&related_file.path,
writeln!(&mut prompt, "`````filename={}", related_file.path.display()).unwrap();
write_excerpts(
&related_file.excerpts,
if related_file.path == request.excerpt_path {
&insertions
@@ -194,6 +194,7 @@ pub fn build_prompt(
request.prompt_format == PromptFormat::NumLinesUniDiff,
&mut prompt,
);
write!(&mut prompt, "`````\n\n").unwrap();
}
}
@@ -204,25 +205,6 @@ pub fn build_prompt(
Ok((prompt, section_labels))
}
pub fn write_codeblock<'a>(
path: &Path,
excerpts: impl IntoIterator<Item = &'a Excerpt>,
sorted_insertions: &[(Point, &str)],
file_line_count: Line,
include_line_numbers: bool,
output: &'a mut String,
) {
writeln!(output, "`````{}", path.display()).unwrap();
write_excerpts(
excerpts,
sorted_insertions,
file_line_count,
include_line_numbers,
output,
);
write!(output, "`````\n\n").unwrap();
}
pub fn write_excerpts<'a>(
excerpts: impl IntoIterator<Item = &'a Excerpt>,
sorted_insertions: &[(Point, &str)],
@@ -615,7 +597,8 @@ impl<'a> SyntaxBasedPrompt<'a> {
disjoint_snippets.push(current_snippet);
}
writeln!(output, "`````path={}", file_path.display()).ok();
// TODO: remove filename=?
writeln!(output, "`````filename={}", file_path.display()).ok();
let mut skipped_last_snippet = false;
for (snippet, range) in disjoint_snippets {
let section_index = section_ranges.len();

View File

@@ -66,14 +66,6 @@ impl CodestralCompletionProvider {
Self::api_key(cx).is_some()
}
/// This is so we can immediately show Codestral as a provider users can
/// switch to in the edit prediction menu, if the API has been added
pub fn ensure_api_key_loaded(http_client: Arc<dyn HttpClient>, cx: &mut App) {
MistralLanguageModelProvider::global(http_client, cx)
.load_codestral_api_key(cx)
.detach();
}
fn api_key(cx: &App) -> Option<Arc<str>> {
MistralLanguageModelProvider::try_global(cx)
.and_then(|provider| provider.codestral_api_key(CODESTRAL_API_URL, cx))

View File

@@ -17,14 +17,12 @@ use editor::{
use fs::Fs;
use futures::{SinkExt, StreamExt, channel::mpsc, lock::Mutex};
use git::repository::repo_path;
use gpui::{
App, Rgba, SharedString, TestAppContext, UpdateGlobal, VisualContext, VisualTestContext,
};
use gpui::{App, Rgba, TestAppContext, UpdateGlobal, VisualContext, VisualTestContext};
use indoc::indoc;
use language::FakeLspAdapter;
use lsp::LSP_REQUEST_TIMEOUT;
use project::{
ProgressToken, ProjectPath, SERVER_PROGRESS_THROTTLE_TIMEOUT,
ProjectPath, SERVER_PROGRESS_THROTTLE_TIMEOUT,
lsp_store::lsp_ext_command::{ExpandedMacro, LspExtExpandMacro},
};
use recent_projects::disconnected_overlay::DisconnectedOverlay;
@@ -1286,14 +1284,12 @@ async fn test_language_server_statuses(cx_a: &mut TestAppContext, cx_b: &mut Tes
});
executor.run_until_parked();
let token = ProgressToken::String(SharedString::from("the-token"));
project_a.read_with(cx_a, |project, cx| {
let status = project.language_server_statuses(cx).next().unwrap().1;
assert_eq!(status.name.0, "the-language-server");
assert_eq!(status.pending_work.len(), 1);
assert_eq!(
status.pending_work[&token].message.as_ref().unwrap(),
status.pending_work["the-token"].message.as_ref().unwrap(),
"the-message"
);
});
@@ -1327,7 +1323,7 @@ async fn test_language_server_statuses(cx_a: &mut TestAppContext, cx_b: &mut Tes
assert_eq!(status.name.0, "the-language-server");
assert_eq!(status.pending_work.len(), 1);
assert_eq!(
status.pending_work[&token].message.as_ref().unwrap(),
status.pending_work["the-token"].message.as_ref().unwrap(),
"the-message-2"
);
});
@@ -1337,7 +1333,7 @@ async fn test_language_server_statuses(cx_a: &mut TestAppContext, cx_b: &mut Tes
assert_eq!(status.name.0, "the-language-server");
assert_eq!(status.pending_work.len(), 1);
assert_eq!(
status.pending_work[&token].message.as_ref().unwrap(),
status.pending_work["the-token"].message.as_ref().unwrap(),
"the-message-2"
);
});

View File

@@ -54,10 +54,6 @@ actions!(
CollapseSelectedChannel,
/// Expands the selected channel in the tree view.
ExpandSelectedChannel,
/// Opens the meeting notes for the selected channel in the panel.
///
/// Use `collab::OpenChannelNotes` to open the channel notes for the current call.
OpenSelectedChannelNotes,
/// Starts moving a channel to a new location.
StartMoveChannel,
/// Moves the selected item to the current location.
@@ -1860,17 +1856,6 @@ impl CollabPanel {
}
}
fn open_selected_channel_notes(
&mut self,
_: &OpenSelectedChannelNotes,
window: &mut Window,
cx: &mut Context<Self>,
) {
if let Some(channel) = self.selected_channel() {
self.open_channel_notes(channel.id, window, cx);
}
}
fn set_channel_visibility(
&mut self,
channel_id: ChannelId,
@@ -2991,7 +2976,6 @@ impl Render for CollabPanel {
.on_action(cx.listener(CollabPanel::remove_selected_channel))
.on_action(cx.listener(CollabPanel::show_inline_context_menu))
.on_action(cx.listener(CollabPanel::rename_selected_channel))
.on_action(cx.listener(CollabPanel::open_selected_channel_notes))
.on_action(cx.listener(CollabPanel::collapse_selected_channel))
.on_action(cx.listener(CollabPanel::expand_selected_channel))
.on_action(cx.listener(CollabPanel::start_move_selected_channel))

View File

@@ -41,10 +41,6 @@ util.workspace = true
[dev-dependencies]
dap = { workspace = true, features = ["test-support"] }
fs = { workspace = true, features = ["test-support"] }
gpui = { workspace = true, features = ["test-support"] }
http_client.workspace = true
node_runtime.workspace = true
settings = { workspace = true, features = ["test-support"] }
task = { workspace = true, features = ["test-support"] }
util = { workspace = true, features = ["test-support"] }

View File

@@ -4,8 +4,6 @@ mod go;
mod javascript;
mod python;
#[cfg(test)]
use std::path::PathBuf;
use std::sync::Arc;
use anyhow::Result;
@@ -40,65 +38,3 @@ pub fn init(cx: &mut App) {
}
})
}
#[cfg(test)]
mod test_mocks {
use super::*;
pub(crate) struct MockDelegate {
worktree_root: PathBuf,
}
impl MockDelegate {
pub(crate) fn new() -> Arc<dyn adapters::DapDelegate> {
Arc::new(Self {
worktree_root: PathBuf::from("/tmp/test"),
})
}
}
#[async_trait::async_trait]
impl adapters::DapDelegate for MockDelegate {
fn worktree_id(&self) -> settings::WorktreeId {
settings::WorktreeId::from_usize(0)
}
fn worktree_root_path(&self) -> &std::path::Path {
&self.worktree_root
}
fn http_client(&self) -> Arc<dyn http_client::HttpClient> {
unimplemented!("Not needed for tests")
}
fn node_runtime(&self) -> node_runtime::NodeRuntime {
unimplemented!("Not needed for tests")
}
fn toolchain_store(&self) -> Arc<dyn language::LanguageToolchainStore> {
unimplemented!("Not needed for tests")
}
fn fs(&self) -> Arc<dyn fs::Fs> {
unimplemented!("Not needed for tests")
}
fn output_to_console(&self, _msg: String) {}
async fn which(&self, _command: &std::ffi::OsStr) -> Option<PathBuf> {
None
}
async fn read_text_file(&self, _path: &util::rel_path::RelPath) -> Result<String> {
Ok(String::new())
}
async fn shell_env(&self) -> collections::HashMap<String, String> {
collections::HashMap::default()
}
fn is_headless(&self) -> bool {
false
}
}
}

View File

@@ -23,11 +23,6 @@ use std::{
use util::command::new_smol_command;
use util::{ResultExt, paths::PathStyle, rel_path::RelPath};
enum DebugpyLaunchMode<'a> {
Normal,
AttachWithConnect { host: Option<&'a str> },
}
#[derive(Default)]
pub(crate) struct PythonDebugAdapter {
base_venv_path: OnceCell<Result<Arc<Path>, String>>,
@@ -41,11 +36,10 @@ impl PythonDebugAdapter {
const LANGUAGE_NAME: &'static str = "Python";
async fn generate_debugpy_arguments<'a>(
host: &'a Ipv4Addr,
async fn generate_debugpy_arguments(
host: &Ipv4Addr,
port: u16,
launch_mode: DebugpyLaunchMode<'a>,
user_installed_path: Option<&'a Path>,
user_installed_path: Option<&Path>,
user_args: Option<Vec<String>>,
) -> Result<Vec<String>> {
let mut args = if let Some(user_installed_path) = user_installed_path {
@@ -68,20 +62,7 @@ impl PythonDebugAdapter {
args.extend(if let Some(args) = user_args {
args
} else {
match launch_mode {
DebugpyLaunchMode::Normal => {
vec![format!("--host={}", host), format!("--port={}", port)]
}
DebugpyLaunchMode::AttachWithConnect { host } => {
let mut args = vec!["connect".to_string()];
if let Some(host) = host {
args.push(format!("{host}:"));
}
args.push(format!("{port}"));
args
}
}
vec![format!("--host={}", host), format!("--port={}", port)]
});
Ok(args)
}
@@ -334,46 +315,7 @@ impl PythonDebugAdapter {
user_env: Option<HashMap<String, String>>,
python_from_toolchain: Option<String>,
) -> Result<DebugAdapterBinary> {
let mut tcp_connection = config.tcp_connection.clone().unwrap_or_default();
let (config_port, config_host) = config
.config
.get("connect")
.map(|value| {
(
value
.get("port")
.and_then(|val| val.as_u64().map(|p| p as u16)),
value.get("host").and_then(|val| val.as_str()),
)
})
.unwrap_or_else(|| {
(
config
.config
.get("port")
.and_then(|port| port.as_u64().map(|p| p as u16)),
config.config.get("host").and_then(|host| host.as_str()),
)
});
let is_attach_with_connect = if config
.config
.get("request")
.is_some_and(|val| val.as_str().is_some_and(|request| request == "attach"))
{
if tcp_connection.host.is_some() && config_host.is_some() {
bail!("Cannot have two different hosts in debug configuration")
} else if tcp_connection.port.is_some() && config_port.is_some() {
bail!("Cannot have two different ports in debug configuration")
}
tcp_connection.port = config_port;
DebugpyLaunchMode::AttachWithConnect { host: config_host }
} else {
DebugpyLaunchMode::Normal
};
let tcp_connection = config.tcp_connection.clone().unwrap_or_default();
let (host, port, timeout) = crate::configure_tcp_connection(tcp_connection).await?;
let python_path = if let Some(toolchain) = python_from_toolchain {
@@ -388,7 +330,6 @@ impl PythonDebugAdapter {
let arguments = Self::generate_debugpy_arguments(
&host,
port,
is_attach_with_connect,
user_installed_path.as_deref(),
user_args,
)
@@ -824,58 +765,29 @@ impl DebugAdapter for PythonDebugAdapter {
.await;
}
let base_paths = ["cwd", "program", "module"]
.into_iter()
.filter_map(|key| {
config.config.get(key).and_then(|cwd| {
RelPath::new(
cwd.as_str()
.map(Path::new)?
.strip_prefix(delegate.worktree_root_path())
.ok()?,
PathStyle::local(),
)
.ok()
})
})
.chain(
// While Debugpy's wiki saids absolute paths are required, but it actually supports relative paths when cwd is passed in.
// (Which should always be the case because Zed defaults to the cwd worktree root)
// So we want to check that these relative paths find toolchains as well. Otherwise, they won't be checked
// because the strip prefix in the iteration above will return an error
config
.config
.get("cwd")
.map(|_| {
["program", "module"].into_iter().filter_map(|key| {
config.config.get(key).and_then(|value| {
let path = Path::new(value.as_str()?);
RelPath::new(path, PathStyle::local()).ok()
})
})
})
.into_iter()
.flatten(),
)
.chain([RelPath::empty().into()]);
let mut toolchain = None;
for base_path in base_paths {
if let Some(found_toolchain) = delegate
.toolchain_store()
.active_toolchain(
delegate.worktree_id(),
base_path.into_arc(),
language::LanguageName::new(Self::LANGUAGE_NAME),
cx,
let base_path = config
.config
.get("cwd")
.and_then(|cwd| {
RelPath::new(
cwd.as_str()
.map(Path::new)?
.strip_prefix(delegate.worktree_root_path())
.ok()?,
PathStyle::local(),
)
.await
{
toolchain = Some(found_toolchain);
break;
}
}
.ok()
})
.unwrap_or_else(|| RelPath::empty().into());
let toolchain = delegate
.toolchain_store()
.active_toolchain(
delegate.worktree_id(),
base_path.into_arc(),
language::LanguageName::new(Self::LANGUAGE_NAME),
cx,
)
.await;
self.fetch_debugpy_whl(toolchain.clone(), delegate)
.await
@@ -912,148 +824,7 @@ mod tests {
use util::path;
use super::*;
use task::TcpArgumentsTemplate;
#[gpui::test]
async fn test_tcp_connection_conflict_with_connect_args() {
let adapter = PythonDebugAdapter {
base_venv_path: OnceCell::new(),
debugpy_whl_base_path: OnceCell::new(),
};
let config_with_port_conflict = json!({
"request": "attach",
"connect": {
"port": 5679
}
});
let tcp_connection = TcpArgumentsTemplate {
host: None,
port: Some(5678),
timeout: None,
};
let task_def = DebugTaskDefinition {
label: "test".into(),
adapter: PythonDebugAdapter::ADAPTER_NAME.into(),
config: config_with_port_conflict,
tcp_connection: Some(tcp_connection.clone()),
};
let result = adapter
.get_installed_binary(
&test_mocks::MockDelegate::new(),
&task_def,
None,
None,
None,
Some("python3".to_string()),
)
.await;
assert!(result.is_err());
assert!(
result
.unwrap_err()
.to_string()
.contains("Cannot have two different ports")
);
let host = Ipv4Addr::new(127, 0, 0, 1);
let config_with_host_conflict = json!({
"request": "attach",
"connect": {
"host": "192.168.1.1",
"port": 5678
}
});
let tcp_connection_with_host = TcpArgumentsTemplate {
host: Some(host),
port: None,
timeout: None,
};
let task_def_host = DebugTaskDefinition {
label: "test".into(),
adapter: PythonDebugAdapter::ADAPTER_NAME.into(),
config: config_with_host_conflict,
tcp_connection: Some(tcp_connection_with_host),
};
let result_host = adapter
.get_installed_binary(
&test_mocks::MockDelegate::new(),
&task_def_host,
None,
None,
None,
Some("python3".to_string()),
)
.await;
assert!(result_host.is_err());
assert!(
result_host
.unwrap_err()
.to_string()
.contains("Cannot have two different hosts")
);
}
#[gpui::test]
async fn test_attach_with_connect_mode_generates_correct_arguments() {
let host = Ipv4Addr::new(127, 0, 0, 1);
let port = 5678;
let args_without_host = PythonDebugAdapter::generate_debugpy_arguments(
&host,
port,
DebugpyLaunchMode::AttachWithConnect { host: None },
None,
None,
)
.await
.unwrap();
let expected_suffix = path!("debug_adapters/Debugpy/debugpy/adapter");
assert!(args_without_host[0].ends_with(expected_suffix));
assert_eq!(args_without_host[1], "connect");
assert_eq!(args_without_host[2], "5678");
let args_with_host = PythonDebugAdapter::generate_debugpy_arguments(
&host,
port,
DebugpyLaunchMode::AttachWithConnect {
host: Some("192.168.1.100"),
},
None,
None,
)
.await
.unwrap();
assert!(args_with_host[0].ends_with(expected_suffix));
assert_eq!(args_with_host[1], "connect");
assert_eq!(args_with_host[2], "192.168.1.100:");
assert_eq!(args_with_host[3], "5678");
let args_normal = PythonDebugAdapter::generate_debugpy_arguments(
&host,
port,
DebugpyLaunchMode::Normal,
None,
None,
)
.await
.unwrap();
assert!(args_normal[0].ends_with(expected_suffix));
assert_eq!(args_normal[1], "--host=127.0.0.1");
assert_eq!(args_normal[2], "--port=5678");
assert!(!args_normal.contains(&"connect".to_string()));
}
use std::{net::Ipv4Addr, path::PathBuf};
#[gpui::test]
async fn test_debugpy_install_path_cases() {
@@ -1062,25 +833,15 @@ mod tests {
// Case 1: User-defined debugpy path (highest precedence)
let user_path = PathBuf::from("/custom/path/to/debugpy/src/debugpy/adapter");
let user_args = PythonDebugAdapter::generate_debugpy_arguments(
&host,
port,
DebugpyLaunchMode::Normal,
Some(&user_path),
None,
)
.await
.unwrap();
let user_args =
PythonDebugAdapter::generate_debugpy_arguments(&host, port, Some(&user_path), None)
.await
.unwrap();
let venv_args = PythonDebugAdapter::generate_debugpy_arguments(
&host,
port,
DebugpyLaunchMode::Normal,
None,
None,
)
.await
.unwrap();
// Case 2: Venv-installed debugpy (uses -m debugpy.adapter)
let venv_args = PythonDebugAdapter::generate_debugpy_arguments(&host, port, None, None)
.await
.unwrap();
assert_eq!(user_args[0], "/custom/path/to/debugpy/src/debugpy/adapter");
assert_eq!(user_args[1], "--host=127.0.0.1");
@@ -1095,7 +856,6 @@ mod tests {
let user_args = PythonDebugAdapter::generate_debugpy_arguments(
&host,
port,
DebugpyLaunchMode::Normal,
Some(&user_path),
Some(vec!["foo".into()]),
)
@@ -1104,7 +864,6 @@ mod tests {
let venv_args = PythonDebugAdapter::generate_debugpy_arguments(
&host,
port,
DebugpyLaunchMode::Normal,
None,
Some(vec!["foo".into()]),
)

View File

@@ -1029,13 +1029,11 @@ impl SearchableItem for DapLogView {
&mut self,
index: usize,
matches: &[Self::Match],
collapse: bool,
window: &mut Window,
cx: &mut Context<Self>,
) {
self.editor.update(cx, |e, cx| {
e.activate_match(index, matches, collapse, window, cx)
})
self.editor
.update(cx, |e, cx| e.activate_match(index, matches, window, cx))
}
fn select_matches(

View File

@@ -697,7 +697,6 @@ impl Render for NewProcessModal {
.justify_between()
.border_t_1()
.border_color(cx.theme().colors().border_variant);
let secondary_action = menu::SecondaryConfirm.boxed_clone();
match self.mode {
NewProcessMode::Launch => el.child(
container
@@ -707,7 +706,6 @@ impl Render for NewProcessModal {
.on_click(cx.listener(|this, _, window, cx| {
this.save_debug_scenario(window, cx);
}))
.key_binding(KeyBinding::for_action(&*secondary_action, cx))
.disabled(
self.debugger.is_none()
|| self
@@ -751,6 +749,7 @@ impl Render for NewProcessModal {
container
.child(div().child({
Button::new("edit-attach-task", "Edit in debug.json")
.label_size(LabelSize::Small)
.key_binding(KeyBinding::for_action(&*secondary_action, cx))
.on_click(move |_, window, cx| {
window.dispatch_action(secondary_action.boxed_clone(), cx)
@@ -1193,7 +1192,7 @@ impl PickerDelegate for DebugDelegate {
}
fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> std::sync::Arc<str> {
"Find a debug task, or debug a command".into()
"Find a debug task, or debug a command.".into()
}
fn update_matches(
@@ -1454,17 +1453,18 @@ impl PickerDelegate for DebugDelegate {
.child({
let action = menu::SecondaryConfirm.boxed_clone();
if self.matches.is_empty() {
Button::new("edit-debug-json", "Edit debug.json").on_click(cx.listener(
|_picker, _, window, cx| {
Button::new("edit-debug-json", "Edit debug.json")
.label_size(LabelSize::Small)
.on_click(cx.listener(|_picker, _, window, cx| {
window.dispatch_action(
zed_actions::OpenProjectDebugTasks.boxed_clone(),
cx,
);
cx.emit(DismissEvent);
},
))
}))
} else {
Button::new("edit-debug-task", "Edit in debug.json")
.label_size(LabelSize::Small)
.key_binding(KeyBinding::for_action(&*action, cx))
.on_click(move |_, window, cx| {
window.dispatch_action(action.boxed_clone(), cx)

View File

@@ -12,7 +12,6 @@ use gpui::{
Action, AppContext, ClickEvent, Entity, FocusHandle, Focusable, MouseButton, ScrollStrategy,
Task, UniformListScrollHandle, WeakEntity, actions, uniform_list,
};
use itertools::Itertools;
use language::Point;
use project::{
Project,
@@ -25,7 +24,7 @@ use project::{
};
use ui::{
Divider, DividerColor, FluentBuilder as _, Indicator, IntoElement, ListItem, Render,
ScrollAxes, StatefulInteractiveElement, Tooltip, WithScrollbar, prelude::*,
StatefulInteractiveElement, Tooltip, WithScrollbar, prelude::*,
};
use util::rel_path::RelPath;
use workspace::Workspace;
@@ -56,7 +55,6 @@ pub(crate) struct BreakpointList {
focus_handle: FocusHandle,
scroll_handle: UniformListScrollHandle,
selected_ix: Option<usize>,
max_width_index: Option<usize>,
input: Entity<Editor>,
strip_mode: Option<ActiveBreakpointStripMode>,
serialize_exception_breakpoints_task: Option<Task<anyhow::Result<()>>>,
@@ -97,7 +95,6 @@ impl BreakpointList {
dap_store,
worktree_store,
breakpoints: Default::default(),
max_width_index: None,
workspace,
session,
focus_handle,
@@ -549,7 +546,7 @@ impl BreakpointList {
.session
.as_ref()
.map(|session| SupportedBreakpointProperties::from(session.read(cx).capabilities()))
.unwrap_or_else(SupportedBreakpointProperties::all);
.unwrap_or_else(SupportedBreakpointProperties::empty);
let strip_mode = self.strip_mode;
uniform_list(
@@ -573,8 +570,6 @@ impl BreakpointList {
.collect()
}),
)
.with_horizontal_sizing_behavior(gpui::ListHorizontalSizingBehavior::Unconstrained)
.with_width_from_item(self.max_width_index)
.track_scroll(self.scroll_handle.clone())
.flex_1()
}
@@ -737,26 +732,6 @@ impl Render for BreakpointList {
.chain(exception_breakpoints),
);
let text_pixels = ui::TextSize::Default.pixels(cx).to_f64() as f32;
self.max_width_index = self
.breakpoints
.iter()
.map(|entry| match &entry.kind {
BreakpointEntryKind::LineBreakpoint(line_bp) => {
let name_and_line = format!("{}:{}", line_bp.name, line_bp.line);
let dir_len = line_bp.dir.as_ref().map(|d| d.len()).unwrap_or(0);
(name_and_line.len() + dir_len) as f32 * text_pixels
}
BreakpointEntryKind::ExceptionBreakpoint(exc_bp) => {
exc_bp.data.label.len() as f32 * text_pixels
}
BreakpointEntryKind::DataBreakpoint(data_bp) => {
data_bp.0.context.human_readable_label().len() as f32 * text_pixels
}
})
.position_max_by(|left, right| left.total_cmp(right));
v_flex()
.id("breakpoint-list")
.key_context("BreakpointList")
@@ -774,14 +749,7 @@ impl Render for BreakpointList {
.size_full()
.pt_1()
.child(self.render_list(cx))
.custom_scrollbars(
ui::Scrollbars::new(ScrollAxes::Both)
.tracked_scroll_handle(self.scroll_handle.clone())
.with_track_along(ScrollAxes::Both, cx.theme().colors().panel_background)
.tracked_entity(cx.entity_id()),
window,
cx,
)
.vertical_scrollbar_for(self.scroll_handle.clone(), window, cx)
.when_some(self.strip_mode, |this, _| {
this.child(Divider::horizontal().color(DividerColor::Border))
.child(
@@ -1408,10 +1376,8 @@ impl RenderOnce for BreakpointOptionsStrip {
h_flex()
.gap_px()
.mr_3() // Space to avoid overlapping with the scrollbar
.justify_end()
.when(has_logs || self.is_selected, |this| {
this.child(
div()
.child(
div()
.map(self.add_focus_styles(
ActiveBreakpointStripMode::Log,
supports_logs,
@@ -1440,46 +1406,45 @@ impl RenderOnce for BreakpointOptionsStrip {
)
}),
)
)
})
.when(has_condition || self.is_selected, |this| {
this.child(
div()
.map(self.add_focus_styles(
ActiveBreakpointStripMode::Condition,
supports_condition,
window,
cx,
))
.child(
IconButton::new(
SharedString::from(format!("{id}-condition-toggle")),
IconName::SplitAlt,
)
.shape(ui::IconButtonShape::Square)
.style(style_for_toggle(
ActiveBreakpointStripMode::Condition,
has_condition,
))
.icon_size(IconSize::Small)
.icon_color(color_for_toggle(has_condition))
.when(has_condition, |this| this.indicator(Indicator::dot().color(Color::Info)))
.disabled(!supports_condition)
.toggle_state(self.is_toggled(ActiveBreakpointStripMode::Condition))
.on_click(self.on_click_callback(ActiveBreakpointStripMode::Condition))
.tooltip(|_window, cx| {
Tooltip::with_meta(
"Set Condition",
None,
"Set condition to evaluate when a breakpoint is hit. Program execution will stop only when the condition is met.",
cx,
)
}),
.when(!has_logs && !self.is_selected, |this| this.invisible()),
)
.child(
div()
.map(self.add_focus_styles(
ActiveBreakpointStripMode::Condition,
supports_condition,
window,
cx,
))
.child(
IconButton::new(
SharedString::from(format!("{id}-condition-toggle")),
IconName::SplitAlt,
)
)
})
.when(has_hit_condition || self.is_selected, |this| {
this.child(div()
.shape(ui::IconButtonShape::Square)
.style(style_for_toggle(
ActiveBreakpointStripMode::Condition,
has_condition,
))
.icon_size(IconSize::Small)
.icon_color(color_for_toggle(has_condition))
.when(has_condition, |this| this.indicator(Indicator::dot().color(Color::Info)))
.disabled(!supports_condition)
.toggle_state(self.is_toggled(ActiveBreakpointStripMode::Condition))
.on_click(self.on_click_callback(ActiveBreakpointStripMode::Condition))
.tooltip(|_window, cx| {
Tooltip::with_meta(
"Set Condition",
None,
"Set condition to evaluate when a breakpoint is hit. Program execution will stop only when the condition is met.",
cx,
)
}),
)
.when(!has_condition && !self.is_selected, |this| this.invisible()),
)
.child(
div()
.map(self.add_focus_styles(
ActiveBreakpointStripMode::HitCondition,
supports_hit_condition,
@@ -1510,8 +1475,10 @@ impl RenderOnce for BreakpointOptionsStrip {
cx,
)
}),
))
})
)
.when(!has_hit_condition && !self.is_selected, |this| {
this.invisible()
}),
)
}
}

View File

@@ -6,10 +6,7 @@ use alacritty_terminal::vte::ansi;
use anyhow::Result;
use collections::HashMap;
use dap::{CompletionItem, CompletionItemType, OutputEvent};
use editor::{
Bias, CompletionProvider, Editor, EditorElement, EditorMode, EditorStyle, ExcerptId,
SizingBehavior,
};
use editor::{Bias, CompletionProvider, Editor, EditorElement, EditorStyle, ExcerptId};
use fuzzy::StringMatchCandidate;
use gpui::{
Action as _, AppContext, Context, Corner, Entity, FocusHandle, Focusable, HighlightStyle, Hsla,
@@ -62,11 +59,6 @@ impl Console {
) -> Self {
let console = cx.new(|cx| {
let mut editor = Editor::multi_line(window, cx);
editor.set_mode(EditorMode::Full {
scale_ui_elements_with_buffer_font_size: true,
show_active_line_background: true,
sizing_behavior: SizingBehavior::ExcludeOverscrollMargin,
});
editor.move_to_end(&editor::actions::MoveToEnd, window, cx);
editor.set_read_only(true);
editor.disable_scrollbars_and_minimap(window, cx);

View File

@@ -10,9 +10,8 @@ use std::{
use editor::{Editor, EditorElement, EditorStyle};
use gpui::{
Action, Along, AppContext, Axis, DismissEvent, DragMoveEvent, Empty, Entity, FocusHandle,
Focusable, ListHorizontalSizingBehavior, MouseButton, Point, ScrollStrategy, ScrollWheelEvent,
Subscription, Task, TextStyle, UniformList, UniformListScrollHandle, WeakEntity, actions,
anchored, deferred, uniform_list,
Focusable, MouseButton, Point, ScrollStrategy, ScrollWheelEvent, Subscription, Task, TextStyle,
UniformList, UniformListScrollHandle, WeakEntity, actions, anchored, deferred, uniform_list,
};
use notifications::status_toast::{StatusToast, ToastIcon};
use project::debugger::{MemoryCell, dap_command::DataBreakpointContext, session::Session};
@@ -230,7 +229,6 @@ impl MemoryView {
},
)
.track_scroll(view_state.scroll_handle)
.with_horizontal_sizing_behavior(ListHorizontalSizingBehavior::Unconstrained)
.on_scroll_wheel(cx.listener(|this, evt: &ScrollWheelEvent, window, _| {
let mut view_state = this.view_state();
let delta = evt.delta.pixel_delta(window.line_height());
@@ -919,17 +917,7 @@ impl Render for MemoryView {
)
.with_priority(1)
}))
.custom_scrollbars(
ui::Scrollbars::new(ui::ScrollAxes::Both)
.tracked_scroll_handle(self.view_state_handle.clone())
.with_track_along(
ui::ScrollAxes::Both,
cx.theme().colors().panel_background,
)
.tracked_entity(cx.entity_id()),
window,
cx,
),
.vertical_scrollbar_for(self.view_state_handle.clone(), window, cx),
)
}
}

View File

@@ -11,18 +11,15 @@ use gpui::{
FocusHandle, Focusable, Hsla, MouseDownEvent, Point, Subscription, TextStyleRefinement,
UniformListScrollHandle, WeakEntity, actions, anchored, deferred, uniform_list,
};
use itertools::Itertools;
use menu::{SelectFirst, SelectLast, SelectNext, SelectPrevious};
use project::debugger::{
dap_command::DataBreakpointContext,
session::{Session, SessionEvent, Watcher},
};
use std::{collections::HashMap, ops::Range, sync::Arc};
use ui::{ContextMenu, ListItem, ScrollAxes, ScrollableHandle, Tooltip, WithScrollbar, prelude::*};
use ui::{ContextMenu, ListItem, ScrollableHandle, Tooltip, WithScrollbar, prelude::*};
use util::{debug_panic, maybe};
static INDENT_STEP_SIZE: Pixels = px(10.0);
actions!(
variable_list,
[
@@ -188,7 +185,6 @@ struct VariableColor {
pub struct VariableList {
entries: Vec<ListEntry>,
max_width_index: Option<usize>,
entry_states: HashMap<EntryPath, EntryState>,
selected_stack_frame_id: Option<StackFrameId>,
list_handle: UniformListScrollHandle,
@@ -247,7 +243,6 @@ impl VariableList {
disabled: false,
edited_path: None,
entries: Default::default(),
max_width_index: None,
entry_states: Default::default(),
weak_running,
memory_view,
@@ -373,26 +368,6 @@ impl VariableList {
}
self.entries = entries;
let text_pixels = ui::TextSize::Default.pixels(cx).to_f64() as f32;
let indent_size = INDENT_STEP_SIZE.to_f64() as f32;
self.max_width_index = self
.entries
.iter()
.map(|entry| match &entry.entry {
DapEntry::Scope(scope) => scope.name.len() as f32 * text_pixels,
DapEntry::Variable(variable) => {
(variable.value.len() + variable.name.len()) as f32 * text_pixels
+ (entry.path.indices.len() as f32 * indent_size)
}
DapEntry::Watcher(watcher) => {
(watcher.value.len() + watcher.expression.len()) as f32 * text_pixels
+ (entry.path.indices.len() as f32 * indent_size)
}
})
.position_max_by(|left, right| left.total_cmp(right));
cx.notify();
}
@@ -1269,7 +1244,7 @@ impl VariableList {
.disabled(self.disabled)
.selectable(false)
.indent_level(state.depth)
.indent_step_size(INDENT_STEP_SIZE)
.indent_step_size(px(10.))
.always_show_disclosure_icon(true)
.when(var_ref > 0, |list_item| {
list_item.toggle(state.is_expanded).on_toggle(cx.listener({
@@ -1470,7 +1445,7 @@ impl VariableList {
.disabled(self.disabled)
.selectable(false)
.indent_level(state.depth)
.indent_step_size(INDENT_STEP_SIZE)
.indent_step_size(px(10.))
.always_show_disclosure_icon(true)
.when(var_ref > 0, |list_item| {
list_item.toggle(state.is_expanded).on_toggle(cx.listener({
@@ -1532,6 +1507,7 @@ impl Render for VariableList {
.key_context("VariableList")
.id("variable-list")
.group("variable-list")
.overflow_y_scroll()
.size_full()
.on_action(cx.listener(Self::select_first))
.on_action(cx.listener(Self::select_last))
@@ -1557,9 +1533,6 @@ impl Render for VariableList {
}),
)
.track_scroll(self.list_handle.clone())
.with_width_from_item(self.max_width_index)
.with_sizing_behavior(gpui::ListSizingBehavior::Auto)
.with_horizontal_sizing_behavior(gpui::ListHorizontalSizingBehavior::Unconstrained)
.gap_1_5()
.size_full()
.flex_grow(),
@@ -1573,15 +1546,7 @@ impl Render for VariableList {
)
.with_priority(1)
}))
// .vertical_scrollbar_for(self.list_handle.clone(), window, cx)
.custom_scrollbars(
ui::Scrollbars::new(ScrollAxes::Both)
.tracked_scroll_handle(self.list_handle.clone())
.with_track_along(ScrollAxes::Both, cx.theme().colors().panel_background)
.tracked_entity(cx.entity_id()),
window,
cx,
)
.vertical_scrollbar_for(self.list_handle.clone(), window, cx)
}
}

View File

@@ -34,7 +34,6 @@ theme.workspace = true
ui.workspace = true
util.workspace = true
workspace.workspace = true
itertools.workspace = true
[dev-dependencies]
client = { workspace = true, features = ["test-support"] }

View File

@@ -1,5 +1,5 @@
use crate::{
DIAGNOSTICS_UPDATE_DEBOUNCE, IncludeWarnings, ToggleWarnings, context_range_for_entry,
DIAGNOSTICS_UPDATE_DELAY, IncludeWarnings, ToggleWarnings, context_range_for_entry,
diagnostic_renderer::{DiagnosticBlock, DiagnosticRenderer},
toolbar_controls::DiagnosticsToolbarEditor,
};
@@ -283,7 +283,7 @@ impl BufferDiagnosticsEditor {
self.update_excerpts_task = Some(cx.spawn_in(window, async move |editor, cx| {
cx.background_executor()
.timer(DIAGNOSTICS_UPDATE_DEBOUNCE)
.timer(DIAGNOSTICS_UPDATE_DELAY)
.await;
if let Some(buffer) = buffer {
@@ -938,6 +938,10 @@ impl DiagnosticsToolbarEditor for WeakEntity<BufferDiagnosticsEditor> {
.unwrap_or(false)
}
fn has_stale_excerpts(&self, _cx: &App) -> bool {
false
}
fn is_updating(&self, cx: &App) -> bool {
self.read_with(cx, |buffer_diagnostics_editor, cx| {
buffer_diagnostics_editor.update_excerpts_task.is_some()

View File

@@ -9,7 +9,7 @@ mod diagnostics_tests;
use anyhow::Result;
use buffer_diagnostics::BufferDiagnosticsEditor;
use collections::{BTreeSet, HashMap, HashSet};
use collections::{BTreeSet, HashMap};
use diagnostic_renderer::DiagnosticBlock;
use editor::{
Editor, EditorEvent, ExcerptRange, MultiBuffer, PathKey,
@@ -17,11 +17,10 @@ use editor::{
multibuffer_context_lines,
};
use gpui::{
AnyElement, AnyView, App, AsyncApp, Context, Entity, EventEmitter, FocusHandle, FocusOutEvent,
Focusable, Global, InteractiveElement, IntoElement, ParentElement, Render, SharedString,
Styled, Subscription, Task, WeakEntity, Window, actions, div,
AnyElement, AnyView, App, AsyncApp, Context, Entity, EventEmitter, FocusHandle, Focusable,
Global, InteractiveElement, IntoElement, ParentElement, Render, SharedString, Styled,
Subscription, Task, WeakEntity, Window, actions, div,
};
use itertools::Itertools as _;
use language::{
Bias, Buffer, BufferRow, BufferSnapshot, DiagnosticEntry, DiagnosticEntryRef, Point,
ToTreeSitterPoint,
@@ -33,7 +32,7 @@ use project::{
use settings::Settings;
use std::{
any::{Any, TypeId},
cmp,
cmp::{self, Ordering},
ops::{Range, RangeInclusive},
sync::Arc,
time::Duration,
@@ -90,8 +89,8 @@ pub(crate) struct ProjectDiagnosticsEditor {
impl EventEmitter<EditorEvent> for ProjectDiagnosticsEditor {}
const DIAGNOSTICS_UPDATE_DEBOUNCE: Duration = Duration::from_millis(50);
const DIAGNOSTICS_SUMMARY_UPDATE_DEBOUNCE: Duration = Duration::from_millis(30);
const DIAGNOSTICS_UPDATE_DELAY: Duration = Duration::from_millis(50);
const DIAGNOSTICS_SUMMARY_UPDATE_DELAY: Duration = Duration::from_millis(30);
impl Render for ProjectDiagnosticsEditor {
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
@@ -150,12 +149,6 @@ impl Render for ProjectDiagnosticsEditor {
}
}
#[derive(PartialEq, Eq, Copy, Clone, Debug)]
enum RetainExcerpts {
Yes,
No,
}
impl ProjectDiagnosticsEditor {
pub fn register(
workspace: &mut Workspace,
@@ -172,21 +165,14 @@ impl ProjectDiagnosticsEditor {
window: &mut Window,
cx: &mut Context<Self>,
) -> Self {
let project_event_subscription = cx.subscribe_in(
&project_handle,
window,
|this, _project, event, window, cx| match event {
let project_event_subscription =
cx.subscribe_in(&project_handle, window, |this, _project, event, window, cx| match event {
project::Event::DiskBasedDiagnosticsStarted { .. } => {
cx.notify();
}
project::Event::DiskBasedDiagnosticsFinished { language_server_id } => {
log::debug!("disk based diagnostics finished for server {language_server_id}");
this.close_diagnosticless_buffers(
window,
cx,
this.editor.focus_handle(cx).contains_focused(window, cx)
|| this.focus_handle.contains_focused(window, cx),
);
this.update_stale_excerpts(window, cx);
}
project::Event::DiagnosticsUpdated {
language_server_id,
@@ -195,39 +181,34 @@ impl ProjectDiagnosticsEditor {
this.paths_to_update.extend(paths.clone());
this.diagnostic_summary_update = cx.spawn(async move |this, cx| {
cx.background_executor()
.timer(DIAGNOSTICS_SUMMARY_UPDATE_DEBOUNCE)
.timer(DIAGNOSTICS_SUMMARY_UPDATE_DELAY)
.await;
this.update(cx, |this, cx| {
this.update_diagnostic_summary(cx);
})
.log_err();
});
cx.emit(EditorEvent::TitleChanged);
log::debug!(
"diagnostics updated for server {language_server_id}, \
paths {paths:?}. updating excerpts"
);
let focused = this.editor.focus_handle(cx).contains_focused(window, cx)
|| this.focus_handle.contains_focused(window, cx);
this.update_stale_excerpts(
if focused {
RetainExcerpts::Yes
} else {
RetainExcerpts::No
},
window,
cx,
);
if this.editor.focus_handle(cx).contains_focused(window, cx) || this.focus_handle.contains_focused(window, cx) {
log::debug!("diagnostics updated for server {language_server_id}, paths {paths:?}. recording change");
} else {
log::debug!("diagnostics updated for server {language_server_id}, paths {paths:?}. updating excerpts");
this.update_stale_excerpts(window, cx);
}
}
_ => {}
},
);
});
let focus_handle = cx.focus_handle();
cx.on_focus_in(&focus_handle, window, Self::focus_in)
.detach();
cx.on_focus_out(&focus_handle, window, Self::focus_out)
.detach();
cx.on_focus_in(&focus_handle, window, |this, window, cx| {
this.focus_in(window, cx)
})
.detach();
cx.on_focus_out(&focus_handle, window, |this, _event, window, cx| {
this.focus_out(window, cx)
})
.detach();
let excerpts = cx.new(|cx| MultiBuffer::new(project_handle.read(cx).capability()));
let editor = cx.new(|cx| {
@@ -257,11 +238,8 @@ impl ProjectDiagnosticsEditor {
window.focus(&this.focus_handle);
}
}
EditorEvent::Blurred => this.close_diagnosticless_buffers(window, cx, false),
EditorEvent::Saved => this.close_diagnosticless_buffers(window, cx, true),
EditorEvent::SelectionsChanged { .. } => {
this.close_diagnosticless_buffers(window, cx, true)
}
EditorEvent::Blurred => this.update_stale_excerpts(window, cx),
EditorEvent::Saved => this.update_stale_excerpts(window, cx),
_ => {}
}
},
@@ -305,67 +283,15 @@ impl ProjectDiagnosticsEditor {
this
}
/// Closes all excerpts of buffers that:
/// - have no diagnostics anymore
/// - are saved (not dirty)
/// - and, if `reatin_selections` is true, do not have selections within them
fn close_diagnosticless_buffers(
&mut self,
_window: &mut Window,
cx: &mut Context<Self>,
retain_selections: bool,
) {
let buffer_ids = self.multibuffer.read(cx).all_buffer_ids();
let selected_buffers = self.editor.update(cx, |editor, cx| {
editor
.selections
.all_anchors(cx)
.iter()
.filter_map(|anchor| anchor.start.buffer_id)
.collect::<HashSet<_>>()
});
for buffer_id in buffer_ids {
if retain_selections && selected_buffers.contains(&buffer_id) {
continue;
}
let has_blocks = self
.blocks
.get(&buffer_id)
.is_none_or(|blocks| blocks.is_empty());
if !has_blocks {
continue;
}
let is_dirty = self
.multibuffer
.read(cx)
.buffer(buffer_id)
.is_some_and(|buffer| buffer.read(cx).is_dirty());
if !is_dirty {
continue;
}
self.multibuffer.update(cx, |b, cx| {
b.remove_excerpts_for_buffer(buffer_id, cx);
});
}
}
fn update_stale_excerpts(
&mut self,
mut retain_excerpts: RetainExcerpts,
window: &mut Window,
cx: &mut Context<Self>,
) {
if self.update_excerpts_task.is_some() {
fn update_stale_excerpts(&mut self, window: &mut Window, cx: &mut Context<Self>) {
if self.update_excerpts_task.is_some() || self.multibuffer.read(cx).is_dirty(cx) {
return;
}
if self.multibuffer.read(cx).is_dirty(cx) {
retain_excerpts = RetainExcerpts::Yes;
}
let project_handle = self.project.clone();
self.update_excerpts_task = Some(cx.spawn_in(window, async move |this, cx| {
cx.background_executor()
.timer(DIAGNOSTICS_UPDATE_DEBOUNCE)
.timer(DIAGNOSTICS_UPDATE_DELAY)
.await;
loop {
let Some(path) = this.update(cx, |this, cx| {
@@ -386,7 +312,7 @@ impl ProjectDiagnosticsEditor {
.log_err()
{
this.update_in(cx, |this, window, cx| {
this.update_excerpts(buffer, retain_excerpts, window, cx)
this.update_excerpts(buffer, window, cx)
})?
.await?;
}
@@ -452,10 +378,10 @@ impl ProjectDiagnosticsEditor {
}
}
fn focus_out(&mut self, _: FocusOutEvent, window: &mut Window, cx: &mut Context<Self>) {
fn focus_out(&mut self, window: &mut Window, cx: &mut Context<Self>) {
if !self.focus_handle.is_focused(window) && !self.editor.focus_handle(cx).is_focused(window)
{
self.close_diagnosticless_buffers(window, cx, false);
self.update_stale_excerpts(window, cx);
}
}
@@ -477,13 +403,12 @@ impl ProjectDiagnosticsEditor {
});
}
}
multibuffer.clear(cx);
});
self.paths_to_update = project_paths;
});
self.update_stale_excerpts(RetainExcerpts::No, window, cx);
self.update_stale_excerpts(window, cx);
}
fn diagnostics_are_unchanged(
@@ -506,7 +431,6 @@ impl ProjectDiagnosticsEditor {
fn update_excerpts(
&mut self,
buffer: Entity<Buffer>,
retain_excerpts: RetainExcerpts,
window: &mut Window,
cx: &mut Context<Self>,
) -> Task<Result<()>> {
@@ -573,27 +497,24 @@ impl ProjectDiagnosticsEditor {
)
})?;
blocks.extend(more);
for item in more {
let i = blocks
.binary_search_by(|probe| {
probe
.initial_range
.start
.cmp(&item.initial_range.start)
.then(probe.initial_range.end.cmp(&item.initial_range.end))
.then(Ordering::Greater)
})
.unwrap_or_else(|i| i);
blocks.insert(i, item);
}
}
let mut excerpt_ranges: Vec<ExcerptRange<Point>> = match retain_excerpts {
RetainExcerpts::No => Vec::new(),
RetainExcerpts::Yes => this.update(cx, |this, cx| {
this.multibuffer.update(cx, |multi_buffer, cx| {
multi_buffer
.excerpts_for_buffer(buffer_id, cx)
.into_iter()
.map(|(_, range)| ExcerptRange {
context: range.context.to_point(&buffer_snapshot),
primary: range.primary.to_point(&buffer_snapshot),
})
.collect()
})
})?,
};
let mut result_blocks = vec![None; excerpt_ranges.len()];
let mut excerpt_ranges: Vec<ExcerptRange<Point>> = Vec::new();
let context_lines = cx.update(|_, cx| multibuffer_context_lines(cx))?;
for b in blocks {
for b in blocks.iter() {
let excerpt_range = context_range_for_entry(
b.initial_range.clone(),
context_lines,
@@ -620,8 +541,7 @@ impl ProjectDiagnosticsEditor {
context: excerpt_range,
primary: b.initial_range.clone(),
},
);
result_blocks.insert(i, Some(b));
)
}
this.update_in(cx, |this, window, cx| {
@@ -642,7 +562,7 @@ impl ProjectDiagnosticsEditor {
)
});
#[cfg(test)]
let cloned_blocks = result_blocks.clone();
let cloned_blocks = blocks.clone();
if was_empty && let Some(anchor_range) = anchor_ranges.first() {
let range_to_select = anchor_range.start..anchor_range.start;
@@ -656,20 +576,22 @@ impl ProjectDiagnosticsEditor {
}
}
let editor_blocks = anchor_ranges
.into_iter()
.zip_eq(result_blocks.into_iter())
.filter_map(|(anchor, block)| {
let block = block?;
let editor = this.editor.downgrade();
Some(BlockProperties {
placement: BlockPlacement::Near(anchor.start),
height: Some(1),
style: BlockStyle::Flex,
render: Arc::new(move |bcx| block.render_block(editor.clone(), bcx)),
priority: 1,
})
});
let editor_blocks =
anchor_ranges
.into_iter()
.zip(blocks.into_iter())
.map(|(anchor, block)| {
let editor = this.editor.downgrade();
BlockProperties {
placement: BlockPlacement::Near(anchor.start),
height: Some(1),
style: BlockStyle::Flex,
render: Arc::new(move |bcx| {
block.render_block(editor.clone(), bcx)
}),
priority: 1,
}
});
let block_ids = this.editor.update(cx, |editor, cx| {
editor.display_map.update(cx, |display_map, cx| {
@@ -679,9 +601,7 @@ impl ProjectDiagnosticsEditor {
#[cfg(test)]
{
for (block_id, block) in
block_ids.iter().zip(cloned_blocks.into_iter().flatten())
{
for (block_id, block) in block_ids.iter().zip(cloned_blocks.iter()) {
let markdown = block.markdown.clone();
editor::test::set_block_content_for_tests(
&this.editor,
@@ -706,7 +626,6 @@ impl ProjectDiagnosticsEditor {
fn update_diagnostic_summary(&mut self, cx: &mut Context<Self>) {
self.summary = self.project.read(cx).diagnostic_summary(false, cx);
cx.emit(EditorEvent::TitleChanged);
}
}
@@ -924,6 +843,13 @@ impl DiagnosticsToolbarEditor for WeakEntity<ProjectDiagnosticsEditor> {
.unwrap_or(false)
}
fn has_stale_excerpts(&self, cx: &App) -> bool {
self.read_with(cx, |project_diagnostics_editor, _cx| {
!project_diagnostics_editor.paths_to_update.is_empty()
})
.unwrap_or(false)
}
fn is_updating(&self, cx: &App) -> bool {
self.read_with(cx, |project_diagnostics_editor, cx| {
project_diagnostics_editor.update_excerpts_task.is_some()
@@ -1084,6 +1010,12 @@ async fn heuristic_syntactic_expand(
return;
}
}
log::info!(
"Expanding to ancestor started on {} node\
exceeding row limit of {max_row_count}.",
node.grammar_name()
);
*ancestor_range = Some(None);
}
})

View File

@@ -119,7 +119,7 @@ async fn test_diagnostics(cx: &mut TestAppContext) {
let editor = diagnostics.update(cx, |diagnostics, _| diagnostics.editor.clone());
diagnostics
.next_notification(DIAGNOSTICS_UPDATE_DEBOUNCE + Duration::from_millis(10), cx)
.next_notification(DIAGNOSTICS_UPDATE_DELAY + Duration::from_millis(10), cx)
.await;
pretty_assertions::assert_eq!(
@@ -190,7 +190,7 @@ async fn test_diagnostics(cx: &mut TestAppContext) {
});
diagnostics
.next_notification(DIAGNOSTICS_UPDATE_DEBOUNCE + Duration::from_millis(10), cx)
.next_notification(DIAGNOSTICS_UPDATE_DELAY + Duration::from_millis(10), cx)
.await;
pretty_assertions::assert_eq!(
@@ -277,7 +277,7 @@ async fn test_diagnostics(cx: &mut TestAppContext) {
});
diagnostics
.next_notification(DIAGNOSTICS_UPDATE_DEBOUNCE + Duration::from_millis(10), cx)
.next_notification(DIAGNOSTICS_UPDATE_DELAY + Duration::from_millis(10), cx)
.await;
pretty_assertions::assert_eq!(
@@ -391,7 +391,7 @@ async fn test_diagnostics_with_folds(cx: &mut TestAppContext) {
// Only the first language server's diagnostics are shown.
cx.executor()
.advance_clock(DIAGNOSTICS_UPDATE_DEBOUNCE + Duration::from_millis(10));
.advance_clock(DIAGNOSTICS_UPDATE_DELAY + Duration::from_millis(10));
cx.executor().run_until_parked();
editor.update_in(cx, |editor, window, cx| {
editor.fold_ranges(vec![Point::new(0, 0)..Point::new(3, 0)], false, window, cx);
@@ -490,7 +490,7 @@ async fn test_diagnostics_multiple_servers(cx: &mut TestAppContext) {
// Only the first language server's diagnostics are shown.
cx.executor()
.advance_clock(DIAGNOSTICS_UPDATE_DEBOUNCE + Duration::from_millis(10));
.advance_clock(DIAGNOSTICS_UPDATE_DELAY + Duration::from_millis(10));
cx.executor().run_until_parked();
pretty_assertions::assert_eq!(
@@ -530,7 +530,7 @@ async fn test_diagnostics_multiple_servers(cx: &mut TestAppContext) {
// Both language server's diagnostics are shown.
cx.executor()
.advance_clock(DIAGNOSTICS_UPDATE_DEBOUNCE + Duration::from_millis(10));
.advance_clock(DIAGNOSTICS_UPDATE_DELAY + Duration::from_millis(10));
cx.executor().run_until_parked();
pretty_assertions::assert_eq!(
@@ -587,7 +587,7 @@ async fn test_diagnostics_multiple_servers(cx: &mut TestAppContext) {
// Only the first language server's diagnostics are updated.
cx.executor()
.advance_clock(DIAGNOSTICS_UPDATE_DEBOUNCE + Duration::from_millis(10));
.advance_clock(DIAGNOSTICS_UPDATE_DELAY + Duration::from_millis(10));
cx.executor().run_until_parked();
pretty_assertions::assert_eq!(
@@ -629,7 +629,7 @@ async fn test_diagnostics_multiple_servers(cx: &mut TestAppContext) {
// Both language servers' diagnostics are updated.
cx.executor()
.advance_clock(DIAGNOSTICS_UPDATE_DEBOUNCE + Duration::from_millis(10));
.advance_clock(DIAGNOSTICS_UPDATE_DELAY + Duration::from_millis(10));
cx.executor().run_until_parked();
pretty_assertions::assert_eq!(
@@ -760,7 +760,7 @@ async fn test_random_diagnostics_blocks(cx: &mut TestAppContext, mut rng: StdRng
.unwrap()
});
cx.executor()
.advance_clock(DIAGNOSTICS_UPDATE_DEBOUNCE + Duration::from_millis(10));
.advance_clock(DIAGNOSTICS_UPDATE_DELAY + Duration::from_millis(10));
cx.run_until_parked();
}
@@ -769,7 +769,7 @@ async fn test_random_diagnostics_blocks(cx: &mut TestAppContext, mut rng: StdRng
log::info!("updating mutated diagnostics view");
mutated_diagnostics.update_in(cx, |diagnostics, window, cx| {
diagnostics.update_stale_excerpts(RetainExcerpts::No, window, cx)
diagnostics.update_stale_excerpts(window, cx)
});
log::info!("constructing reference diagnostics view");
@@ -777,7 +777,7 @@ async fn test_random_diagnostics_blocks(cx: &mut TestAppContext, mut rng: StdRng
ProjectDiagnosticsEditor::new(true, project.clone(), workspace.downgrade(), window, cx)
});
cx.executor()
.advance_clock(DIAGNOSTICS_UPDATE_DEBOUNCE + Duration::from_millis(10));
.advance_clock(DIAGNOSTICS_UPDATE_DELAY + Duration::from_millis(10));
cx.run_until_parked();
let mutated_excerpts =
@@ -789,12 +789,7 @@ async fn test_random_diagnostics_blocks(cx: &mut TestAppContext, mut rng: StdRng
// The mutated view may contain more than the reference view as
// we don't currently shrink excerpts when diagnostics were removed.
let mut ref_iter = reference_excerpts.lines().filter(|line| {
// ignore $ ---- and $ <file>.rs
!line.starts_with('§')
|| line.starts_with("§ diagnostic")
|| line.starts_with("§ related info")
});
let mut ref_iter = reference_excerpts.lines().filter(|line| *line != "§ -----");
let mut next_ref_line = ref_iter.next();
let mut skipped_block = false;
@@ -802,12 +797,7 @@ async fn test_random_diagnostics_blocks(cx: &mut TestAppContext, mut rng: StdRng
if let Some(ref_line) = next_ref_line {
if mut_line == ref_line {
next_ref_line = ref_iter.next();
} else if mut_line.contains('§')
// ignore $ ---- and $ <file>.rs
&& (!mut_line.starts_with('§')
|| mut_line.starts_with("§ diagnostic")
|| mut_line.starts_with("§ related info"))
{
} else if mut_line.contains('§') && mut_line != "§ -----" {
skipped_block = true;
}
}
@@ -959,7 +949,7 @@ async fn test_random_diagnostics_with_inlays(cx: &mut TestAppContext, mut rng: S
.unwrap()
});
cx.executor()
.advance_clock(DIAGNOSTICS_UPDATE_DEBOUNCE + Duration::from_millis(10));
.advance_clock(DIAGNOSTICS_UPDATE_DELAY + Duration::from_millis(10));
cx.run_until_parked();
}
@@ -968,11 +958,11 @@ async fn test_random_diagnostics_with_inlays(cx: &mut TestAppContext, mut rng: S
log::info!("updating mutated diagnostics view");
mutated_diagnostics.update_in(cx, |diagnostics, window, cx| {
diagnostics.update_stale_excerpts(RetainExcerpts::No, window, cx)
diagnostics.update_stale_excerpts(window, cx)
});
cx.executor()
.advance_clock(DIAGNOSTICS_UPDATE_DEBOUNCE + Duration::from_millis(10));
.advance_clock(DIAGNOSTICS_UPDATE_DELAY + Duration::from_millis(10));
cx.run_until_parked();
}
@@ -1437,7 +1427,7 @@ async fn test_diagnostics_with_code(cx: &mut TestAppContext) {
let editor = diagnostics.update(cx, |diagnostics, _| diagnostics.editor.clone());
diagnostics
.next_notification(DIAGNOSTICS_UPDATE_DEBOUNCE + Duration::from_millis(10), cx)
.next_notification(DIAGNOSTICS_UPDATE_DELAY + Duration::from_millis(10), cx)
.await;
// Verify that the diagnostic codes are displayed correctly
@@ -1714,7 +1704,7 @@ async fn test_buffer_diagnostics(cx: &mut TestAppContext) {
// wait a little bit to ensure that the buffer diagnostic's editor content
// is rendered.
cx.executor()
.advance_clock(DIAGNOSTICS_UPDATE_DEBOUNCE + Duration::from_millis(10));
.advance_clock(DIAGNOSTICS_UPDATE_DELAY + Duration::from_millis(10));
pretty_assertions::assert_eq!(
editor_content_with_blocks(&editor, cx),
@@ -1847,7 +1837,7 @@ async fn test_buffer_diagnostics_without_warnings(cx: &mut TestAppContext) {
// wait a little bit to ensure that the buffer diagnostic's editor content
// is rendered.
cx.executor()
.advance_clock(DIAGNOSTICS_UPDATE_DEBOUNCE + Duration::from_millis(10));
.advance_clock(DIAGNOSTICS_UPDATE_DELAY + Duration::from_millis(10));
pretty_assertions::assert_eq!(
editor_content_with_blocks(&editor, cx),
@@ -1981,7 +1971,7 @@ async fn test_buffer_diagnostics_multiple_servers(cx: &mut TestAppContext) {
// wait a little bit to ensure that the buffer diagnostic's editor content
// is rendered.
cx.executor()
.advance_clock(DIAGNOSTICS_UPDATE_DEBOUNCE + Duration::from_millis(10));
.advance_clock(DIAGNOSTICS_UPDATE_DELAY + Duration::from_millis(10));
pretty_assertions::assert_eq!(
editor_content_with_blocks(&editor, cx),

View File

@@ -16,6 +16,9 @@ pub(crate) trait DiagnosticsToolbarEditor: Send + Sync {
/// Toggles whether warning diagnostics should be displayed by the
/// diagnostics editor.
fn toggle_warnings(&self, window: &mut Window, cx: &mut App);
/// Indicates whether any of the excerpts displayed by the diagnostics
/// editor are stale.
fn has_stale_excerpts(&self, cx: &App) -> bool;
/// Indicates whether the diagnostics editor is currently updating the
/// diagnostics.
fn is_updating(&self, cx: &App) -> bool;
@@ -34,12 +37,14 @@ pub(crate) trait DiagnosticsToolbarEditor: Send + Sync {
impl Render for ToolbarControls {
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let mut has_stale_excerpts = false;
let mut include_warnings = false;
let mut is_updating = false;
match &self.editor {
Some(editor) => {
include_warnings = editor.include_warnings(cx);
has_stale_excerpts = editor.has_stale_excerpts(cx);
is_updating = editor.is_updating(cx);
}
None => {}
@@ -81,6 +86,7 @@ impl Render for ToolbarControls {
IconButton::new("refresh-diagnostics", IconName::ArrowCircle)
.icon_color(Color::Info)
.shape(IconButtonShape::Square)
.disabled(!has_stale_excerpts)
.tooltip(Tooltip::for_action_title(
"Refresh diagnostics",
&ToggleDiagnosticsRefresh,

View File

@@ -1,5 +1,5 @@
use anyhow::Result;
use client::{Client, UserStore, zed_urls};
use client::{UserStore, zed_urls};
use cloud_llm_client::UsageLimit;
use codestral::CodestralCompletionProvider;
use copilot::{Copilot, Status};
@@ -192,7 +192,6 @@ impl Render for EditPredictionButton {
Some(ContextMenu::build(window, cx, |menu, _, _| {
let fs = fs.clone();
let activate_url = activate_url.clone();
menu.entry("Sign In", None, move |_, cx| {
cx.open_url(activate_url.as_str())
})
@@ -245,8 +244,15 @@ impl Render for EditPredictionButton {
} else {
Some(ContextMenu::build(window, cx, |menu, _, _| {
let fs = fs.clone();
menu.entry(
menu.entry("Use Zed AI instead", None, move |_, cx| {
set_completion_provider(
fs.clone(),
cx,
EditPredictionProvider::Zed,
)
})
.separator()
.entry(
"Configure Codestral API Key",
None,
move |window, cx| {
@@ -256,18 +262,6 @@ impl Render for EditPredictionButton {
);
},
)
.separator()
.entry(
"Use Zed AI instead",
None,
move |_, cx| {
set_completion_provider(
fs.clone(),
cx,
EditPredictionProvider::Zed,
)
},
)
}))
}
})
@@ -418,7 +412,6 @@ impl EditPredictionButton {
fs: Arc<dyn Fs>,
user_store: Entity<UserStore>,
popover_menu_handle: PopoverMenuHandle<ContextMenu>,
client: Arc<Client>,
cx: &mut Context<Self>,
) -> Self {
if let Some(copilot) = Copilot::global(cx) {
@@ -428,8 +421,6 @@ impl EditPredictionButton {
cx.observe_global::<SettingsStore>(move |_, cx| cx.notify())
.detach();
CodestralCompletionProvider::ensure_api_key_loaded(client.http_client(), cx);
Self {
editor_subscription: None,
editor_enabled: None,
@@ -444,89 +435,6 @@ impl EditPredictionButton {
}
}
fn get_available_providers(&self, cx: &App) -> Vec<EditPredictionProvider> {
let mut providers = Vec::new();
providers.push(EditPredictionProvider::Zed);
if let Some(copilot) = Copilot::global(cx) {
if matches!(copilot.read(cx).status(), Status::Authorized) {
providers.push(EditPredictionProvider::Copilot);
}
}
if let Some(supermaven) = Supermaven::global(cx) {
if let Supermaven::Spawned(agent) = supermaven.read(cx) {
if matches!(agent.account_status, AccountStatus::Ready) {
providers.push(EditPredictionProvider::Supermaven);
}
}
}
if CodestralCompletionProvider::has_api_key(cx) {
providers.push(EditPredictionProvider::Codestral);
}
providers
}
fn add_provider_switching_section(
&self,
mut menu: ContextMenu,
current_provider: EditPredictionProvider,
cx: &App,
) -> ContextMenu {
let available_providers = self.get_available_providers(cx);
let other_providers: Vec<_> = available_providers
.into_iter()
.filter(|p| *p != current_provider && *p != EditPredictionProvider::None)
.collect();
if !other_providers.is_empty() {
menu = menu.separator().header("Switch Providers");
for provider in other_providers {
let fs = self.fs.clone();
menu = match provider {
EditPredictionProvider::Zed => menu.item(
ContextMenuEntry::new("Zed AI")
.documentation_aside(
DocumentationSide::Left,
DocumentationEdge::Top,
|_| {
Label::new("Zed's edit prediction is powered by Zeta, an open-source, dataset mode.")
.into_any_element()
},
)
.handler(move |_, cx| {
set_completion_provider(fs.clone(), cx, provider);
}),
),
EditPredictionProvider::Copilot => {
menu.entry("GitHub Copilot", None, move |_, cx| {
set_completion_provider(fs.clone(), cx, provider);
})
}
EditPredictionProvider::Supermaven => {
menu.entry("Supermaven", None, move |_, cx| {
set_completion_provider(fs.clone(), cx, provider);
})
}
EditPredictionProvider::Codestral => {
menu.entry("Codestral", None, move |_, cx| {
set_completion_provider(fs.clone(), cx, provider);
})
}
EditPredictionProvider::None => continue,
};
}
}
menu
}
pub fn build_copilot_start_menu(
&mut self,
window: &mut Window,
@@ -664,10 +572,8 @@ impl EditPredictionButton {
}
menu = menu.separator().header("Privacy");
if let Some(provider) = &self.edit_prediction_provider {
let data_collection = provider.data_collection_state(cx);
if data_collection.is_supported() {
let provider = provider.clone();
let enabled = data_collection.is_enabled();
@@ -785,7 +691,7 @@ impl EditPredictionButton {
}
}),
).item(
ContextMenuEntry::new("View Docs")
ContextMenuEntry::new("View Documentation")
.icon(IconName::FileGeneric)
.icon_color(Color::Muted)
.handler(move |_, cx| {
@@ -805,7 +711,6 @@ impl EditPredictionButton {
if let Some(editor_focus_handle) = self.editor_focus_handle.clone() {
menu = menu
.separator()
.header("Actions")
.entry(
"Predict Edit at Cursor",
Some(Box::new(ShowEditPrediction)),
@@ -816,11 +721,7 @@ impl EditPredictionButton {
}
},
)
.context(editor_focus_handle)
.when(
cx.has_flag::<PredictEditsRateCompletionsFeatureFlag>(),
|this| this.action("Rate Completions", RateCompletions.boxed_clone()),
);
.context(editor_focus_handle);
}
menu
@@ -832,11 +733,15 @@ impl EditPredictionButton {
cx: &mut Context<Self>,
) -> Entity<ContextMenu> {
ContextMenu::build(window, cx, |menu, window, cx| {
let menu = self.build_language_settings_menu(menu, window, cx);
let menu =
self.add_provider_switching_section(menu, EditPredictionProvider::Copilot, cx);
menu.separator()
self.build_language_settings_menu(menu, window, cx)
.separator()
.entry("Use Zed AI instead", None, {
let fs = self.fs.clone();
move |_window, cx| {
set_completion_provider(fs.clone(), cx, EditPredictionProvider::Zed)
}
})
.separator()
.link(
"Go to Copilot Settings",
OpenBrowser {
@@ -854,11 +759,8 @@ impl EditPredictionButton {
cx: &mut Context<Self>,
) -> Entity<ContextMenu> {
ContextMenu::build(window, cx, |menu, window, cx| {
let menu = self.build_language_settings_menu(menu, window, cx);
let menu =
self.add_provider_switching_section(menu, EditPredictionProvider::Supermaven, cx);
menu.separator()
self.build_language_settings_menu(menu, window, cx)
.separator()
.action("Sign Out", supermaven::SignOut.boxed_clone())
})
}
@@ -868,12 +770,14 @@ impl EditPredictionButton {
window: &mut Window,
cx: &mut Context<Self>,
) -> Entity<ContextMenu> {
let fs = self.fs.clone();
ContextMenu::build(window, cx, |menu, window, cx| {
let menu = self.build_language_settings_menu(menu, window, cx);
let menu =
self.add_provider_switching_section(menu, EditPredictionProvider::Codestral, cx);
menu.separator()
self.build_language_settings_menu(menu, window, cx)
.separator()
.entry("Use Zed AI instead", None, move |_, cx| {
set_completion_provider(fs.clone(), cx, EditPredictionProvider::Zed)
})
.separator()
.entry("Configure Codestral API Key", None, move |window, cx| {
window.dispatch_action(zed_actions::agent::OpenSettings.boxed_clone(), cx);
})
@@ -968,10 +872,10 @@ impl EditPredictionButton {
.separator();
}
let menu = self.build_language_settings_menu(menu, window, cx);
let menu = self.add_provider_switching_section(menu, EditPredictionProvider::Zed, cx);
menu
self.build_language_settings_menu(menu, window, cx).when(
cx.has_flag::<PredictEditsRateCompletionsFeatureFlag>(),
|this| this.action("Rate Completions", RateCompletions.boxed_clone()),
)
})
}

View File

@@ -213,6 +213,15 @@ pub struct ExpandExcerptsDown {
pub(super) lines: u32,
}
/// Shows code completion suggestions at the cursor position.
#[derive(PartialEq, Clone, Deserialize, Default, JsonSchema, Action)]
#[action(namespace = editor)]
#[serde(deny_unknown_fields)]
pub struct ShowCompletions {
#[serde(default)]
pub(super) trigger: Option<String>,
}
/// Handles text input in the editor.
#[derive(PartialEq, Clone, Deserialize, Default, JsonSchema, Action)]
#[action(namespace = editor)]
@@ -612,8 +621,6 @@ actions!(
NextEditPrediction,
/// Scrolls to the next screen.
NextScreen,
/// Goes to the next snippet tabstop if one exists.
NextSnippetTabstop,
/// Opens the context menu at cursor position.
OpenContextMenu,
/// Opens excerpts from the current file.
@@ -647,8 +654,6 @@ actions!(
Paste,
/// Navigates to the previous edit prediction.
PreviousEditPrediction,
/// Goes to the previous snippet tabstop if one exists.
PreviousSnippetTabstop,
/// Redoes the last undone edit.
Redo,
/// Redoes the last selection change.
@@ -727,8 +732,6 @@ actions!(
SelectToStartOfParagraph,
/// Extends selection up.
SelectUp,
/// Shows code completion suggestions at the cursor position.
ShowCompletions,
/// Shows the system character palette.
ShowCharacterPalette,
/// Shows edit prediction at cursor.

View File

@@ -28,12 +28,10 @@ use std::{
rc::Rc,
};
use task::ResolvedTask;
use ui::{
Color, IntoElement, ListItem, Pixels, Popover, ScrollAxes, Scrollbars, Styled, WithScrollbar,
prelude::*,
};
use ui::{Color, IntoElement, ListItem, Pixels, Popover, Styled, prelude::*};
use util::ResultExt;
use crate::CodeActionSource;
use crate::hover_popover::{hover_markdown_style, open_markdown_url};
use crate::{
CodeActionProvider, CompletionId, CompletionItemKind, CompletionProvider, DisplayRow, Editor,
@@ -41,8 +39,7 @@ use crate::{
actions::{ConfirmCodeAction, ConfirmCompletion},
split_words, styled_runs_for_code_label,
};
use crate::{CodeActionSource, EditorSettings};
use settings::{Settings, SnippetSortOrder};
use settings::SnippetSortOrder;
pub const MENU_GAP: Pixels = px(4.);
pub const MENU_ASIDE_X_PADDING: Pixels = px(16.);
@@ -252,17 +249,8 @@ enum MarkdownCacheKey {
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum CompletionsMenuSource {
/// Show all completions (words, snippets, LSP)
Normal,
/// Show only snippets (not words or LSP)
///
/// Used after typing a non-word character
SnippetsOnly,
/// Tab stops within a snippet that have a predefined finite set of choices
SnippetChoices,
/// Show only words (not snippets or LSP)
///
/// Used when word completions are explicitly triggered
Words { ignore_threshold: bool },
}
@@ -273,20 +261,6 @@ impl Drop for CompletionsMenu {
}
}
struct CompletionMenuScrollBarSetting;
impl ui::scrollbars::GlobalSetting for CompletionMenuScrollBarSetting {
fn get_value(_cx: &App) -> &Self {
&Self
}
}
impl ui::scrollbars::ScrollbarVisibility for CompletionMenuScrollBarSetting {
fn visibility(&self, cx: &App) -> ui::scrollbars::ShowScrollbar {
EditorSettings::get_global(cx).completion_menu_scrollbar
}
}
impl CompletionsMenu {
pub fn new(
id: CompletionId,
@@ -924,17 +898,7 @@ impl CompletionsMenu {
}
});
Popover::new()
.child(
div().child(list).custom_scrollbars(
Scrollbars::for_settings::<CompletionMenuScrollBarSetting>()
.show_along(ScrollAxes::Vertical)
.tracked_scroll_handle(self.scroll_handle.clone()),
window,
cx,
),
)
.into_any_element()
Popover::new().child(list).into_any_element()
}
fn render_aside(

View File

@@ -17,9 +17,6 @@
//! [Editor]: crate::Editor
//! [EditorElement]: crate::element::EditorElement
#[macro_use]
mod dimensions;
mod block_map;
mod crease_map;
mod custom_highlights;
@@ -170,12 +167,11 @@ impl DisplayMap {
}
pub fn snapshot(&mut self, cx: &mut Context<Self>) -> DisplaySnapshot {
let tab_size = Self::tab_size(&self.buffer, cx);
let buffer_snapshot = self.buffer.read(cx).snapshot(cx);
let edits = self.buffer_subscription.consume().into_inner();
let (inlay_snapshot, edits) = self.inlay_map.sync(buffer_snapshot, edits);
let (fold_snapshot, edits) = self.fold_map.read(inlay_snapshot, edits);
let tab_size = Self::tab_size(&self.buffer, cx);
let (tab_snapshot, edits) = self.tab_map.sync(fold_snapshot, edits, tab_size);
let (wrap_snapshot, edits) = self
.wrap_map
@@ -919,7 +915,7 @@ impl DisplaySnapshot {
pub fn text_chunks(&self, display_row: DisplayRow) -> impl Iterator<Item = &str> {
self.block_snapshot
.chunks(
BlockRow(display_row.0)..BlockRow(self.max_point().row().next_row().0),
display_row.0..self.max_point().row().next_row().0,
false,
self.masked,
Highlights::default(),
@@ -931,12 +927,7 @@ impl DisplaySnapshot {
pub fn reverse_text_chunks(&self, display_row: DisplayRow) -> impl Iterator<Item = &str> {
(0..=display_row.0).rev().flat_map(move |row| {
self.block_snapshot
.chunks(
BlockRow(row)..BlockRow(row + 1),
false,
self.masked,
Highlights::default(),
)
.chunks(row..row + 1, false, self.masked, Highlights::default())
.map(|h| h.text)
.collect::<Vec<_>>()
.into_iter()
@@ -951,7 +942,7 @@ impl DisplaySnapshot {
highlight_styles: HighlightStyles,
) -> DisplayChunks<'_> {
self.block_snapshot.chunks(
BlockRow(display_rows.start.0)..BlockRow(display_rows.end.0),
display_rows.start.0..display_rows.end.0,
language_aware,
self.masked,
Highlights {
@@ -1187,8 +1178,8 @@ impl DisplaySnapshot {
rows: Range<DisplayRow>,
) -> impl Iterator<Item = (DisplayRow, &Block)> {
self.block_snapshot
.blocks_in_range(BlockRow(rows.start.0)..BlockRow(rows.end.0))
.map(|(row, block)| (DisplayRow(row.0), block))
.blocks_in_range(rows.start.0..rows.end.0)
.map(|(row, block)| (DisplayRow(row), block))
}
pub fn sticky_header_excerpt(&self, row: f64) -> Option<StickyHeaderExcerpt<'_>> {
@@ -1220,7 +1211,7 @@ impl DisplaySnapshot {
pub fn soft_wrap_indent(&self, display_row: DisplayRow) -> Option<u32> {
let wrap_row = self
.block_snapshot
.to_wrap_point(BlockPoint::new(BlockRow(display_row.0), 0), Bias::Left)
.to_wrap_point(BlockPoint::new(display_row.0, 0), Bias::Left)
.row();
self.wrap_snapshot().soft_wrap_indent(wrap_row)
}
@@ -1251,7 +1242,7 @@ impl DisplaySnapshot {
}
pub fn longest_row(&self) -> DisplayRow {
DisplayRow(self.block_snapshot.longest_row().0)
DisplayRow(self.block_snapshot.longest_row())
}
pub fn longest_row_in_range(&self, range: Range<DisplayRow>) -> DisplayRow {

File diff suppressed because it is too large Load Diff

View File

@@ -1,96 +0,0 @@
#[derive(Copy, Clone, Debug, Default, Eq, Ord, PartialOrd, PartialEq)]
pub struct RowDelta(pub u32);
impl RowDelta {
pub fn saturating_sub(self, other: RowDelta) -> RowDelta {
RowDelta(self.0.saturating_sub(other.0))
}
}
impl ::std::ops::Add for RowDelta {
type Output = RowDelta;
fn add(self, rhs: RowDelta) -> Self::Output {
RowDelta(self.0 + rhs.0)
}
}
impl ::std::ops::Sub for RowDelta {
type Output = RowDelta;
fn sub(self, rhs: RowDelta) -> Self::Output {
RowDelta(self.0 - rhs.0)
}
}
impl ::std::ops::AddAssign for RowDelta {
fn add_assign(&mut self, rhs: RowDelta) {
self.0 += rhs.0;
}
}
impl ::std::ops::SubAssign for RowDelta {
fn sub_assign(&mut self, rhs: RowDelta) {
self.0 -= rhs.0;
}
}
macro_rules! impl_for_row_types {
($row:ident => $row_delta:ident) => {
impl $row {
pub fn saturating_sub(self, other: $row_delta) -> $row {
$row(self.0.saturating_sub(other.0))
}
}
impl ::std::ops::Add for $row {
type Output = Self;
fn add(self, rhs: Self) -> Self::Output {
Self(self.0 + rhs.0)
}
}
impl ::std::ops::Add<$row_delta> for $row {
type Output = Self;
fn add(self, rhs: $row_delta) -> Self::Output {
Self(self.0 + rhs.0)
}
}
impl ::std::ops::Sub for $row {
type Output = $row_delta;
fn sub(self, rhs: Self) -> Self::Output {
$row_delta(self.0 - rhs.0)
}
}
impl ::std::ops::Sub<$row_delta> for $row {
type Output = $row;
fn sub(self, rhs: $row_delta) -> Self::Output {
$row(self.0 - rhs.0)
}
}
impl ::std::ops::AddAssign for $row {
fn add_assign(&mut self, rhs: Self) {
self.0 += rhs.0;
}
}
impl ::std::ops::AddAssign<$row_delta> for $row {
fn add_assign(&mut self, rhs: $row_delta) {
self.0 += rhs.0;
}
}
impl ::std::ops::SubAssign<$row_delta> for $row {
fn sub_assign(&mut self, rhs: $row_delta) {
self.0 -= rhs.0;
}
}
};
}

View File

@@ -1440,7 +1440,7 @@ where
self.current_chunk.as_ref().and_then(|(chunk, idx)| {
let mut idx = *idx;
let mut diff = 0;
while idx > 0 && chunk.chars & (1u128.unbounded_shl(idx)) == 0 {
while idx > 0 && chunk.chars & (1 << idx) == 0 {
idx -= 1;
diff += 1;
}
@@ -1460,7 +1460,7 @@ where
fn is_char_boundary(&self) -> bool {
self.current_chunk
.as_ref()
.is_some_and(|(chunk, idx)| (chunk.chars & 1u128.unbounded_shl(*idx)) != 0)
.is_some_and(|(chunk, idx)| (chunk.chars & (1 << *idx.min(&127))) != 0)
}
/// distance: length to move forward while searching for the next tab stop
@@ -1483,20 +1483,18 @@ where
self.byte_offset += overshoot;
self.char_offset += get_char_offset(
chunk_position..(chunk_position + overshoot).saturating_sub(1),
chunk_position..(chunk_position + overshoot).saturating_sub(1).min(127),
chunk.chars,
);
if chunk_position + overshoot < 128 {
self.current_chunk = Some((chunk, chunk_position + overshoot));
}
self.current_chunk = Some((chunk, chunk_position + overshoot));
return None;
}
self.byte_offset += chunk_distance;
self.char_offset += get_char_offset(
chunk_position..(chunk_position + chunk_distance).saturating_sub(1),
chunk_position..(chunk_position + chunk_distance).saturating_sub(1).min(127),
chunk.chars,
);
distance_traversed += chunk_distance;
@@ -1548,6 +1546,8 @@ where
#[inline(always)]
fn get_char_offset(range: Range<u32>, bit_map: u128) -> u32 {
// This edge case can happen when we're at chunk position 128
if range.start == range.end {
return if (1u128 << range.start) & bit_map == 0 {
0
@@ -1555,7 +1555,7 @@ fn get_char_offset(range: Range<u32>, bit_map: u128) -> u32 {
1
};
}
let end_shift: u128 = 127u128 - range.end as u128;
let end_shift: u128 = 127u128 - range.end.min(127) as u128;
let mut bit_mask = (u128::MAX >> range.start) << range.start;
bit_mask = (bit_mask << end_shift) >> end_shift;
let bit_map = bit_map & bit_mask;

View File

@@ -1,6 +1,5 @@
use super::{
Highlights,
dimensions::RowDelta,
fold_map::{Chunk, FoldRows},
tab_map::{self, TabEdit, TabPoint, TabSnapshot},
};
@@ -8,20 +7,13 @@ use gpui::{App, AppContext as _, Context, Entity, Font, LineWrapper, Pixels, Tas
use language::Point;
use multi_buffer::{MultiBufferSnapshot, RowInfo};
use smol::future::yield_now;
use std::{cmp, collections::VecDeque, mem, ops::Range, sync::LazyLock, time::Duration};
use std::sync::LazyLock;
use std::{cmp, collections::VecDeque, mem, ops::Range, time::Duration};
use sum_tree::{Bias, Cursor, Dimensions, SumTree};
use text::Patch;
pub use super::tab_map::TextSummary;
pub type WrapEdit = text::Edit<WrapRow>;
pub type WrapPatch = text::Patch<WrapRow>;
#[derive(Copy, Clone, Debug, Default, Eq, Ord, PartialOrd, PartialEq)]
pub struct WrapRow(pub u32);
impl_for_row_types! {
WrapRow => RowDelta
}
pub type WrapEdit = text::Edit<u32>;
/// Handles soft wrapping of text.
///
@@ -29,8 +21,8 @@ impl_for_row_types! {
pub struct WrapMap {
snapshot: WrapSnapshot,
pending_edits: VecDeque<(TabSnapshot, Vec<TabEdit>)>,
interpolated_edits: WrapPatch,
edits_since_sync: WrapPatch,
interpolated_edits: Patch<u32>,
edits_since_sync: Patch<u32>,
wrap_width: Option<Pixels>,
background_task: Option<Task<()>>,
font_with_size: (Font, Pixels),
@@ -62,7 +54,7 @@ pub struct WrapChunks<'a> {
input_chunks: tab_map::TabChunks<'a>,
input_chunk: Chunk<'a>,
output_position: WrapPoint,
max_output_row: WrapRow,
max_output_row: u32,
transforms: Cursor<'a, 'static, Transform, Dimensions<WrapPoint, TabPoint>>,
snapshot: &'a WrapSnapshot,
}
@@ -71,19 +63,19 @@ pub struct WrapChunks<'a> {
pub struct WrapRows<'a> {
input_buffer_rows: FoldRows<'a>,
input_buffer_row: RowInfo,
output_row: WrapRow,
output_row: u32,
soft_wrapped: bool,
max_output_row: WrapRow,
max_output_row: u32,
transforms: Cursor<'a, 'static, Transform, Dimensions<WrapPoint, TabPoint>>,
}
impl WrapRows<'_> {
pub(crate) fn seek(&mut self, start_row: WrapRow) {
pub(crate) fn seek(&mut self, start_row: u32) {
self.transforms
.seek(&WrapPoint::new(start_row, 0), Bias::Left);
let mut input_row = self.transforms.start().1.row();
if self.transforms.item().is_some_and(|t| t.is_isomorphic()) {
input_row += (start_row - self.transforms.start().0.row()).0;
input_row += start_row - self.transforms.start().0.row();
}
self.soft_wrapped = self.transforms.item().is_some_and(|t| !t.is_isomorphic());
self.input_buffer_rows.seek(input_row);
@@ -128,7 +120,7 @@ impl WrapMap {
tab_snapshot: TabSnapshot,
edits: Vec<TabEdit>,
cx: &mut Context<Self>,
) -> (WrapSnapshot, WrapPatch) {
) -> (WrapSnapshot, Patch<u32>) {
if self.wrap_width.is_some() {
self.pending_edits.push_back((tab_snapshot, edits));
self.flush_edits(cx);
@@ -234,8 +226,8 @@ impl WrapMap {
let new_rows = self.snapshot.transforms.summary().output.lines.row + 1;
self.snapshot.interpolated = false;
self.edits_since_sync = self.edits_since_sync.compose(Patch::new(vec![WrapEdit {
old: WrapRow(0)..WrapRow(old_rows),
new: WrapRow(0)..WrapRow(new_rows),
old: 0..old_rows,
new: 0..new_rows,
}]));
}
}
@@ -339,7 +331,7 @@ impl WrapSnapshot {
self.tab_snapshot.buffer_snapshot()
}
fn interpolate(&mut self, new_tab_snapshot: TabSnapshot, tab_edits: &[TabEdit]) -> WrapPatch {
fn interpolate(&mut self, new_tab_snapshot: TabSnapshot, tab_edits: &[TabEdit]) -> Patch<u32> {
let mut new_transforms;
if tab_edits.is_empty() {
new_transforms = self.transforms.clone();
@@ -409,7 +401,7 @@ impl WrapSnapshot {
tab_edits: &[TabEdit],
wrap_width: Pixels,
line_wrapper: &mut LineWrapper,
) -> WrapPatch {
) -> Patch<u32> {
#[derive(Debug)]
struct RowEdit {
old_rows: Range<u32>,
@@ -562,7 +554,7 @@ impl WrapSnapshot {
old_snapshot.compute_edits(tab_edits, self)
}
fn compute_edits(&self, tab_edits: &[TabEdit], new_snapshot: &WrapSnapshot) -> WrapPatch {
fn compute_edits(&self, tab_edits: &[TabEdit], new_snapshot: &WrapSnapshot) -> Patch<u32> {
let mut wrap_edits = Vec::with_capacity(tab_edits.len());
let mut old_cursor = self.transforms.cursor::<TransformSummary>(());
let mut new_cursor = new_snapshot.transforms.cursor::<TransformSummary>(());
@@ -576,21 +568,24 @@ impl WrapSnapshot {
let mut old_start = old_cursor.start().output.lines;
old_start += tab_edit.old.start.0 - old_cursor.start().input.lines;
old_cursor.seek_forward(&tab_edit.old.end, Bias::Right);
// todo(lw): Should these be seek_forward?
old_cursor.seek(&tab_edit.old.end, Bias::Right);
let mut old_end = old_cursor.start().output.lines;
old_end += tab_edit.old.end.0 - old_cursor.start().input.lines;
// todo(lw): Should these be seek_forward?
new_cursor.seek(&tab_edit.new.start, Bias::Right);
let mut new_start = new_cursor.start().output.lines;
new_start += tab_edit.new.start.0 - new_cursor.start().input.lines;
new_cursor.seek_forward(&tab_edit.new.end, Bias::Right);
// todo(lw): Should these be seek_forward?
new_cursor.seek(&tab_edit.new.end, Bias::Right);
let mut new_end = new_cursor.start().output.lines;
new_end += tab_edit.new.end.0 - new_cursor.start().input.lines;
wrap_edits.push(WrapEdit {
old: WrapRow(old_start.row)..WrapRow(old_end.row),
new: WrapRow(new_start.row)..WrapRow(new_end.row),
old: old_start.row..old_end.row,
new: new_start.row..new_end.row,
});
}
@@ -600,7 +595,7 @@ impl WrapSnapshot {
pub(crate) fn chunks<'a>(
&'a self,
rows: Range<WrapRow>,
rows: Range<u32>,
language_aware: bool,
highlights: Highlights<'a>,
) -> WrapChunks<'a> {
@@ -635,17 +630,17 @@ impl WrapSnapshot {
WrapPoint(self.transforms.summary().output.lines)
}
pub fn line_len(&self, row: WrapRow) -> u32 {
pub fn line_len(&self, row: u32) -> u32 {
let (start, _, item) = self.transforms.find::<Dimensions<WrapPoint, TabPoint>, _>(
(),
&WrapPoint::new(row + WrapRow(1), 0),
&WrapPoint::new(row + 1, 0),
Bias::Left,
);
if item.is_some_and(|transform| transform.is_isomorphic()) {
let overshoot = row - start.0.row();
let tab_row = start.1.row() + overshoot.0;
let tab_row = start.1.row() + overshoot;
let tab_line_len = self.tab_snapshot.line_len(tab_row);
if overshoot.0 == 0 {
if overshoot == 0 {
start.0.column() + (tab_line_len - start.1.column())
} else {
tab_line_len
@@ -655,7 +650,7 @@ impl WrapSnapshot {
}
}
pub fn text_summary_for_range(&self, rows: Range<WrapRow>) -> TextSummary {
pub fn text_summary_for_range(&self, rows: Range<u32>) -> TextSummary {
let mut summary = TextSummary::default();
let start = WrapPoint::new(rows.start, 0);
@@ -716,12 +711,10 @@ impl WrapSnapshot {
summary
}
pub fn soft_wrap_indent(&self, row: WrapRow) -> Option<u32> {
let (.., item) = self.transforms.find::<WrapPoint, _>(
(),
&WrapPoint::new(row + WrapRow(1), 0),
Bias::Right,
);
pub fn soft_wrap_indent(&self, row: u32) -> Option<u32> {
let (.., item) =
self.transforms
.find::<WrapPoint, _>((), &WrapPoint::new(row + 1, 0), Bias::Right);
item.and_then(|transform| {
if transform.is_isomorphic() {
None
@@ -735,14 +728,14 @@ impl WrapSnapshot {
self.transforms.summary().output.longest_row
}
pub fn row_infos(&self, start_row: WrapRow) -> WrapRows<'_> {
pub fn row_infos(&self, start_row: u32) -> WrapRows<'_> {
let mut transforms = self
.transforms
.cursor::<Dimensions<WrapPoint, TabPoint>>(());
transforms.seek(&WrapPoint::new(start_row, 0), Bias::Left);
let mut input_row = transforms.start().1.row();
if transforms.item().is_some_and(|t| t.is_isomorphic()) {
input_row += (start_row - transforms.start().0.row()).0;
input_row += start_row - transforms.start().0.row();
}
let soft_wrapped = transforms.item().is_some_and(|t| !t.is_isomorphic());
let mut input_buffer_rows = self.tab_snapshot.rows(input_row);
@@ -797,9 +790,9 @@ impl WrapSnapshot {
self.tab_point_to_wrap_point(self.tab_snapshot.clip_point(self.to_tab_point(point), bias))
}
pub fn prev_row_boundary(&self, mut point: WrapPoint) -> WrapRow {
pub fn prev_row_boundary(&self, mut point: WrapPoint) -> u32 {
if self.transforms.is_empty() {
return WrapRow(0);
return 0;
}
*point.column_mut() = 0;
@@ -823,7 +816,7 @@ impl WrapSnapshot {
unreachable!()
}
pub fn next_row_boundary(&self, mut point: WrapPoint) -> Option<WrapRow> {
pub fn next_row_boundary(&self, mut point: WrapPoint) -> Option<u32> {
point.0 += Point::new(1, 0);
let mut cursor = self
@@ -843,13 +836,13 @@ impl WrapSnapshot {
#[cfg(test)]
pub fn text(&self) -> String {
self.text_chunks(WrapRow(0)).collect()
self.text_chunks(0).collect()
}
#[cfg(test)]
pub fn text_chunks(&self, wrap_row: WrapRow) -> impl Iterator<Item = &str> {
pub fn text_chunks(&self, wrap_row: u32) -> impl Iterator<Item = &str> {
self.chunks(
wrap_row..self.max_point().row() + WrapRow(1),
wrap_row..self.max_point().row() + 1,
false,
Highlights::default(),
)
@@ -877,22 +870,21 @@ impl WrapSnapshot {
let mut input_buffer_rows = self.tab_snapshot.rows(0);
let mut expected_buffer_rows = Vec::new();
let mut prev_tab_row = 0;
for display_row in 0..=self.max_point().row().0 {
let display_row = WrapRow(display_row);
for display_row in 0..=self.max_point().row() {
let tab_point = self.to_tab_point(WrapPoint::new(display_row, 0));
if tab_point.row() == prev_tab_row && display_row != WrapRow(0) {
if tab_point.row() == prev_tab_row && display_row != 0 {
expected_buffer_rows.push(None);
} else {
expected_buffer_rows.push(input_buffer_rows.next().unwrap().buffer_row);
}
prev_tab_row = tab_point.row();
assert_eq!(self.line_len(display_row), text.line_len(display_row.0));
assert_eq!(self.line_len(display_row), text.line_len(display_row));
}
for start_display_row in 0..expected_buffer_rows.len() {
assert_eq!(
self.row_infos(WrapRow(start_display_row as u32))
self.row_infos(start_display_row as u32)
.map(|row_info| row_info.buffer_row)
.collect::<Vec<_>>(),
&expected_buffer_rows[start_display_row..],
@@ -905,7 +897,7 @@ impl WrapSnapshot {
}
impl WrapChunks<'_> {
pub(crate) fn seek(&mut self, rows: Range<WrapRow>) {
pub(crate) fn seek(&mut self, rows: Range<u32>) {
let output_start = WrapPoint::new(rows.start, 0);
let output_end = WrapPoint::new(rows.end, 0);
self.transforms.seek(&output_start, Bias::Right);
@@ -942,7 +934,7 @@ impl<'a> Iterator for WrapChunks<'a> {
// Exclude newline starting prior to the desired row.
start_ix = 1;
summary.row = 0;
} else if self.output_position.row() + WrapRow(1) >= self.max_output_row {
} else if self.output_position.row() + 1 >= self.max_output_row {
// Exclude soft indentation ending after the desired row.
end_ix = 1;
summary.column = 0;
@@ -1008,7 +1000,7 @@ impl Iterator for WrapRows<'_> {
let soft_wrapped = self.soft_wrapped;
let diff_status = self.input_buffer_row.diff_status;
self.output_row += WrapRow(1);
self.output_row += 1;
self.transforms
.seek_forward(&WrapPoint::new(self.output_row, 0), Bias::Left);
if self.transforms.item().is_some_and(|t| t.is_isomorphic()) {
@@ -1025,7 +1017,6 @@ impl Iterator for WrapRows<'_> {
multibuffer_row: None,
diff_status,
expand_info: None,
wrapped_buffer_row: buffer_row.buffer_row,
}
} else {
buffer_row
@@ -1119,12 +1110,12 @@ impl SumTreeExt for SumTree<Transform> {
}
impl WrapPoint {
pub fn new(row: WrapRow, column: u32) -> Self {
Self(Point::new(row.0, column))
pub fn new(row: u32, column: u32) -> Self {
Self(Point::new(row, column))
}
pub fn row(self) -> WrapRow {
WrapRow(self.0.row)
pub fn row(self) -> u32 {
self.0.row
}
pub fn row_mut(&mut self) -> &mut u32 {
@@ -1429,14 +1420,14 @@ mod tests {
for (snapshot, patch) in edits {
let snapshot_text = Rope::from(snapshot.text().as_str());
for edit in &patch {
let old_start = initial_text.point_to_offset(Point::new(edit.new.start.0, 0));
let old_start = initial_text.point_to_offset(Point::new(edit.new.start, 0));
let old_end = initial_text.point_to_offset(cmp::min(
Point::new(edit.new.start.0 + (edit.old.end - edit.old.start).0, 0),
Point::new(edit.new.start + edit.old.len() as u32, 0),
initial_text.max_point(),
));
let new_start = snapshot_text.point_to_offset(Point::new(edit.new.start.0, 0));
let new_start = snapshot_text.point_to_offset(Point::new(edit.new.start, 0));
let new_end = snapshot_text.point_to_offset(cmp::min(
Point::new(edit.new.end.0, 0),
Point::new(edit.new.end, 0),
snapshot_text.max_point(),
));
let new_text = snapshot_text
@@ -1496,11 +1487,11 @@ mod tests {
impl WrapSnapshot {
fn verify_chunks(&mut self, rng: &mut impl Rng) {
for _ in 0..5 {
let mut end_row = rng.random_range(0..=self.max_point().row().0);
let mut end_row = rng.random_range(0..=self.max_point().row());
let start_row = rng.random_range(0..=end_row);
end_row += 1;
let mut expected_text = self.text_chunks(WrapRow(start_row)).collect::<String>();
let mut expected_text = self.text_chunks(start_row).collect::<String>();
if expected_text.ends_with('\n') {
expected_text.push('\n');
}
@@ -1509,16 +1500,12 @@ mod tests {
.take((end_row - start_row) as usize)
.collect::<Vec<_>>()
.join("\n");
if end_row <= self.max_point().row().0 {
if end_row <= self.max_point().row() {
expected_text.push('\n');
}
let actual_text = self
.chunks(
WrapRow(start_row)..WrapRow(end_row),
true,
Highlights::default(),
)
.chunks(start_row..end_row, true, Highlights::default())
.map(|c| c.text)
.collect::<String>();
assert_eq!(

View File

@@ -1,13 +1,12 @@
use edit_prediction::EditPredictionProvider;
use gpui::{Entity, KeyBinding, Modifiers, prelude::*};
use gpui::{Entity, prelude::*};
use indoc::indoc;
use multi_buffer::{Anchor, MultiBufferSnapshot, ToPoint};
use std::ops::Range;
use text::{Point, ToOffset};
use crate::{
AcceptEditPrediction, EditPrediction, MenuEditPredictionsPolicy, editor_tests::init_test,
test::editor_test_context::EditorTestContext,
EditPrediction, editor_tests::init_test, test::editor_test_context::EditorTestContext,
};
#[gpui::test]
@@ -271,63 +270,6 @@ async fn test_edit_prediction_jump_disabled_for_non_zed_providers(cx: &mut gpui:
});
}
#[gpui::test]
async fn test_edit_prediction_preview_cleanup_on_toggle_off(cx: &mut gpui::TestAppContext) {
init_test(cx, |_| {});
// Bind `ctrl-shift-a` to accept the provided edit prediction. The actual key
// binding here doesn't matter, we simply need to confirm that holding the
// binding's modifiers triggers the edit prediction preview.
cx.update(|cx| cx.bind_keys([KeyBinding::new("ctrl-shift-a", AcceptEditPrediction, None)]));
let mut cx = EditorTestContext::new(cx).await;
let provider = cx.new(|_| FakeEditPredictionProvider::default());
assign_editor_completion_provider(provider.clone(), &mut cx);
cx.set_state("let x = ˇ;");
propose_edits(&provider, vec![(8..8, "42")], &mut cx);
cx.update_editor(|editor, window, cx| {
editor.set_menu_edit_predictions_policy(MenuEditPredictionsPolicy::ByProvider);
editor.update_visible_edit_prediction(window, cx)
});
cx.editor(|editor, _, _| {
assert!(editor.has_active_edit_prediction());
});
// Simulate pressing the modifiers for `AcceptEditPrediction`, namely
// `ctrl-shift`, so that we can confirm that the edit prediction preview is
// activated.
let modifiers = Modifiers::control_shift();
cx.simulate_modifiers_change(modifiers);
cx.run_until_parked();
cx.editor(|editor, _, _| {
assert!(editor.edit_prediction_preview_is_active());
});
// Disable showing edit predictions without issuing a new modifiers changed
// event, to confirm that the edit prediction preview is still active.
cx.update_editor(|editor, window, cx| {
editor.set_show_edit_predictions(Some(false), window, cx);
});
cx.editor(|editor, _, _| {
assert!(!editor.has_active_edit_prediction());
assert!(editor.edit_prediction_preview_is_active());
});
// Now release the modifiers
// Simulate releasing all modifiers, ensuring that even with edit prediction
// disabled, the edit prediction preview is cleaned up.
cx.simulate_modifiers_change(Modifiers::none());
cx.run_until_parked();
cx.editor(|editor, _, _| {
assert!(!editor.edit_prediction_preview_is_active());
});
}
fn assert_editor_active_edit_completion(
cx: &mut EditorTestContext,
assert: impl FnOnce(MultiBufferSnapshot, &Vec<(Range<Anchor>, String)>),
@@ -453,7 +395,7 @@ impl EditPredictionProvider for FakeEditPredictionProvider {
}
fn show_completions_in_menu() -> bool {
true
false
}
fn supports_jump_to_edit() -> bool {

View File

@@ -163,10 +163,7 @@ use rpc::{ErrorCode, ErrorExt, proto::PeerId};
use scroll::{Autoscroll, OngoingScroll, ScrollAnchor, ScrollManager};
use selections_collection::{MutableSelectionsCollection, SelectionsCollection};
use serde::{Deserialize, Serialize};
use settings::{
GitGutterSetting, RelativeLineNumbers, Settings, SettingsLocation, SettingsStore,
update_settings_file,
};
use settings::{GitGutterSetting, Settings, SettingsLocation, SettingsStore, update_settings_file};
use smallvec::{SmallVec, smallvec};
use snippet::Snippet;
use std::{
@@ -455,20 +452,6 @@ pub enum SelectMode {
All,
}
#[derive(Copy, Clone, Default, PartialEq, Eq, Debug)]
pub enum SizingBehavior {
/// The editor will layout itself using `size_full` and will include the vertical
/// scroll margin as requested by user settings.
#[default]
Default,
/// The editor will layout itself using `size_full`, but will not have any
/// vertical overscroll.
ExcludeOverscrollMargin,
/// The editor will request a vertical size according to its content and will be
/// layouted without a vertical scroll margin.
SizeByContent,
}
#[derive(Clone, PartialEq, Eq, Debug)]
pub enum EditorMode {
SingleLine,
@@ -481,8 +464,8 @@ pub enum EditorMode {
scale_ui_elements_with_buffer_font_size: bool,
/// When set to `true`, the editor will render a background for the active line.
show_active_line_background: bool,
/// Determines the sizing behavior for this editor
sizing_behavior: SizingBehavior,
/// When set to `true`, the editor's height will be determined by its content.
sized_by_content: bool,
},
Minimap {
parent: WeakEntity<Editor>,
@@ -494,7 +477,7 @@ impl EditorMode {
Self::Full {
scale_ui_elements_with_buffer_font_size: true,
show_active_line_background: true,
sizing_behavior: SizingBehavior::Default,
sized_by_content: false,
}
}
@@ -1086,6 +1069,7 @@ pub struct Editor {
searchable: bool,
cursor_shape: CursorShape,
current_line_highlight: Option<CurrentLineHighlight>,
collapse_matches: bool,
autoindent_mode: Option<AutoindentMode>,
workspace: Option<(WeakEntity<Workspace>, Option<WorkspaceId>)>,
input_enabled: bool,
@@ -2141,7 +2125,7 @@ impl Editor {
.unwrap_or_default(),
current_line_highlight: None,
autoindent_mode: Some(AutoindentMode::EachLine),
collapse_matches: false,
workspace: None,
input_enabled: !is_minimap,
use_modal_editing: full_mode,
@@ -2294,7 +2278,7 @@ impl Editor {
}
}
EditorEvent::Edited { .. } => {
if vim_flavor(cx).is_none() {
if !vim_enabled(cx) {
let display_map = editor.display_snapshot(cx);
let selections = editor.selections.all_adjusted_display(&display_map);
let pop_state = editor
@@ -2461,10 +2445,6 @@ impl Editor {
key_context.add("renaming");
}
if !self.snippet_stack.is_empty() {
key_context.add("in_snippet");
}
match self.context_menu.borrow().as_ref() {
Some(CodeContextMenu::Completions(menu)) => {
if menu.visible() {
@@ -2903,12 +2883,12 @@ impl Editor {
self.current_line_highlight = current_line_highlight;
}
pub fn range_for_match<T: std::marker::Copy>(
&self,
range: &Range<T>,
collapse: bool,
) -> Range<T> {
if collapse {
pub fn set_collapse_matches(&mut self, collapse_matches: bool) {
self.collapse_matches = collapse_matches;
}
pub fn range_for_match<T: std::marker::Copy>(&self, range: &Range<T>) -> Range<T> {
if self.collapse_matches {
return range.start..range.start;
}
range.clone()
@@ -3142,7 +3122,7 @@ impl Editor {
};
if continue_showing {
self.open_or_update_completions_menu(None, None, false, window, cx);
self.show_completions(&ShowCompletions { trigger: None }, window, cx);
} else {
self.hide_context_menu(window, cx);
}
@@ -4972,18 +4952,57 @@ impl Editor {
ignore_threshold: false,
}),
None,
trigger_in_words,
window,
cx,
);
}
_ => self.open_or_update_completions_menu(
None,
Some(text.to_owned()).filter(|x| !x.is_empty()),
true,
window,
Some(CompletionsMenuSource::Normal)
| Some(CompletionsMenuSource::SnippetChoices)
| None
if self.is_completion_trigger(
text,
trigger_in_words,
completions_source.is_some(),
cx,
) =>
{
self.show_completions(
&ShowCompletions {
trigger: Some(text.to_owned()).filter(|x| !x.is_empty()),
},
window,
cx,
)
}
_ => {
self.hide_context_menu(window, cx);
}
}
}
fn is_completion_trigger(
&self,
text: &str,
trigger_in_words: bool,
menu_is_open: bool,
cx: &mut Context<Self>,
) -> bool {
let position = self.selections.newest_anchor().head();
let Some(buffer) = self.buffer.read(cx).buffer_for_anchor(position, cx) else {
return false;
};
if let Some(completion_provider) = &self.completion_provider {
completion_provider.is_completion_trigger(
&buffer,
position.text_anchor,
text,
trigger_in_words,
menu_is_open,
cx,
),
)
} else {
false
}
}
@@ -5261,7 +5280,6 @@ impl Editor {
ignore_threshold: true,
}),
None,
false,
window,
cx,
);
@@ -5269,18 +5287,17 @@ impl Editor {
pub fn show_completions(
&mut self,
_: &ShowCompletions,
options: &ShowCompletions,
window: &mut Window,
cx: &mut Context<Self>,
) {
self.open_or_update_completions_menu(None, None, false, window, cx);
self.open_or_update_completions_menu(None, options.trigger.as_deref(), window, cx);
}
fn open_or_update_completions_menu(
&mut self,
requested_source: Option<CompletionsMenuSource>,
trigger: Option<String>,
trigger_in_words: bool,
trigger: Option<&str>,
window: &mut Window,
cx: &mut Context<Self>,
) {
@@ -5288,15 +5305,6 @@ impl Editor {
return;
}
let completions_source = self
.context_menu
.borrow()
.as_ref()
.and_then(|menu| match menu {
CodeContextMenu::Completions(completions_menu) => Some(completions_menu.source),
CodeContextMenu::CodeActions(_) => None,
});
let multibuffer_snapshot = self.buffer.read(cx).read(cx);
// Typically `start` == `end`, but with snippet tabstop choices the default choice is
@@ -5344,8 +5352,7 @@ impl Editor {
ignore_word_threshold = ignore_threshold;
None
}
Some(CompletionsMenuSource::SnippetChoices)
| Some(CompletionsMenuSource::SnippetsOnly) => {
Some(CompletionsMenuSource::SnippetChoices) => {
log::error!("bug: SnippetChoices requested_source is not handled");
None
}
@@ -5359,19 +5366,13 @@ impl Editor {
.as_ref()
.is_none_or(|provider| provider.filter_completions());
let was_snippets_only = matches!(
completions_source,
Some(CompletionsMenuSource::SnippetsOnly)
);
if let Some(CodeContextMenu::Completions(menu)) = self.context_menu.borrow_mut().as_mut() {
if filter_completions {
menu.filter(query.clone(), provider.clone(), window, cx);
}
// When `is_incomplete` is false, no need to re-query completions when the current query
// is a suffix of the initial query.
let was_complete = !menu.is_incomplete;
if was_complete && !was_snippets_only {
if !menu.is_incomplete {
// If the new query is a suffix of the old query (typing more characters) and
// the previous result was complete, the existing completions can be filtered.
//
@@ -5395,6 +5396,23 @@ impl Editor {
}
};
let trigger_kind = match trigger {
Some(trigger) if buffer.read(cx).completion_triggers().contains(trigger) => {
CompletionTriggerKind::TRIGGER_CHARACTER
}
_ => CompletionTriggerKind::INVOKED,
};
let completion_context = CompletionContext {
trigger_character: trigger.and_then(|trigger| {
if trigger_kind == CompletionTriggerKind::TRIGGER_CHARACTER {
Some(String::from(trigger))
} else {
None
}
}),
trigger_kind,
};
let Anchor {
excerpt_id: buffer_excerpt_id,
text_anchor: buffer_position,
@@ -5452,72 +5470,49 @@ impl Editor {
&& match &query {
Some(query) => query.chars().count() < completion_settings.words_min_length,
None => completion_settings.words_min_length != 0,
})
|| (provider.is_some() && completion_settings.words == WordsCompletionMode::Disabled);
});
let mut 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,
})
})
};
let load_provider_completions = provider.as_ref().is_some_and(|provider| {
trigger.as_ref().is_none_or(|trigger| {
provider.is_completion_trigger(
let (mut words, provider_responses) = match &provider {
Some(provider) => {
let provider_responses = provider.completions(
buffer_excerpt_id,
&buffer,
position.text_anchor,
trigger,
trigger_in_words,
completions_source.is_some(),
buffer_position,
completion_context,
window,
cx,
)
})
});
);
let provider_responses = if let Some(provider) = &provider
&& load_provider_completions
{
let trigger_character =
trigger.filter(|trigger| buffer.read(cx).completion_triggers().contains(trigger));
let completion_context = CompletionContext {
trigger_kind: match &trigger_character {
Some(_) => CompletionTriggerKind::TRIGGER_CHARACTER,
None => CompletionTriggerKind::INVOKED,
},
trigger_character,
};
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,
range: word_search_range,
skip_digits,
})
}),
};
provider.completions(
buffer_excerpt_id,
&buffer,
buffer_position,
completion_context,
window,
cx,
)
} else {
Task::ready(Ok(Vec::new()))
};
let snippets = if let Some(provider) = &provider
&& provider.show_snippets()
&& let Some(project) = self.project()
{
project.update(cx, |project, cx| {
snippet_completions(project, &buffer, buffer_position, cx)
})
} else {
Task::ready(Ok(CompletionResponse {
completions: Vec::new(),
display_options: Default::default(),
is_incomplete: false,
}))
(words, provider_responses)
}
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,
})
})
};
(words, Task::ready(Ok(Vec::new())))
}
};
let snippet_sort_order = EditorSettings::get_global(cx).snippet_sort_order;
@@ -5575,13 +5570,6 @@ impl Editor {
confirm: None,
}));
completions.extend(
snippets
.await
.into_iter()
.flat_map(|response| response.completions),
);
let menu = if completions.is_empty() {
None
} else {
@@ -5593,11 +5581,7 @@ impl Editor {
.map(|workspace| workspace.read(cx).app_state().languages.clone());
let menu = CompletionsMenu::new(
id,
requested_source.unwrap_or(if load_provider_completions {
CompletionsMenuSource::Normal
} else {
CompletionsMenuSource::SnippetsOnly
}),
requested_source.unwrap_or(CompletionsMenuSource::Normal),
sort_completions,
show_completion_documentation,
position,
@@ -5927,7 +5911,7 @@ impl Editor {
.as_ref()
.is_some_and(|confirm| confirm(intent, window, cx));
if show_new_completions_on_confirm {
self.open_or_update_completions_menu(None, None, false, window, cx);
self.show_completions(&ShowCompletions { trigger: None }, window, cx);
}
let provider = self.completion_provider.as_ref()?;
@@ -7572,14 +7556,7 @@ impl Editor {
window: &mut Window,
cx: &mut Context<Self>,
) {
// Ensure that the edit prediction preview is updated, even when not
// enabled, if there's an active edit prediction preview.
if self.show_edit_predictions_in_menu()
|| matches!(
self.edit_prediction_preview,
EditPredictionPreview::Active { .. }
)
{
if self.show_edit_predictions_in_menu() {
self.update_edit_prediction_preview(&modifiers, window, cx);
}
@@ -9976,38 +9953,6 @@ impl Editor {
self.outdent(&Outdent, window, cx);
}
pub fn next_snippet_tabstop(
&mut self,
_: &NextSnippetTabstop,
window: &mut Window,
cx: &mut Context<Self>,
) {
if self.mode.is_single_line() || self.snippet_stack.is_empty() {
return;
}
if self.move_to_next_snippet_tabstop(window, cx) {
self.hide_mouse_cursor(HideMouseCursorOrigin::TypingAction, cx);
return;
}
}
pub fn previous_snippet_tabstop(
&mut self,
_: &PreviousSnippetTabstop,
window: &mut Window,
cx: &mut Context<Self>,
) {
if self.mode.is_single_line() || self.snippet_stack.is_empty() {
return;
}
if self.move_to_prev_snippet_tabstop(window, cx) {
self.hide_mouse_cursor(HideMouseCursorOrigin::TypingAction, cx);
return;
}
}
pub fn tab(&mut self, _: &Tab, window: &mut Window, cx: &mut Context<Self>) {
if self.mode.is_single_line() {
cx.propagate();
@@ -12720,10 +12665,6 @@ impl Editor {
});
}
// 🤔 | .. | show_in_menu |
// | .. | true true
// | had_edit_prediction | false true
let trigger_in_words =
this.show_edit_predictions_in_menu() || !had_active_edit_prediction;
@@ -15907,7 +15848,7 @@ impl Editor {
) {
let current_scroll_position = self.scroll_position(cx);
let lines_to_expand = EditorSettings::get_global(cx).expand_excerpt_lines;
let mut scroll = None;
let mut should_scroll_up = false;
if direction == ExpandExcerptDirection::Down {
let multi_buffer = self.buffer.read(cx);
@@ -15920,30 +15861,17 @@ impl Editor {
let excerpt_end_row = Point::from_anchor(&excerpt_range.end, &buffer_snapshot).row;
let last_row = buffer_snapshot.max_point().row;
let lines_below = last_row.saturating_sub(excerpt_end_row);
if lines_below >= lines_to_expand {
scroll = Some(
current_scroll_position
+ gpui::Point::new(0.0, lines_to_expand as ScrollOffset),
);
}
should_scroll_up = lines_below >= lines_to_expand;
}
}
if direction == ExpandExcerptDirection::Up
&& self
.buffer
.read(cx)
.snapshot(cx)
.excerpt_before(excerpt)
.is_none()
{
scroll = Some(current_scroll_position);
}
self.buffer.update(cx, |buffer, cx| {
buffer.expand_excerpts([excerpt], lines_to_expand, direction, cx)
});
if let Some(new_scroll_position) = scroll {
if should_scroll_up {
let new_scroll_position =
current_scroll_position + gpui::Point::new(0.0, lines_to_expand as ScrollOffset);
self.set_scroll_position(new_scroll_position, window, cx);
}
}
@@ -16025,6 +15953,7 @@ impl Editor {
}
fn filtered<'a>(
snapshot: EditorSnapshot,
severity: GoToDiagnosticSeverityFilter,
diagnostics: impl Iterator<Item = DiagnosticEntryRef<'a, usize>>,
) -> impl Iterator<Item = DiagnosticEntryRef<'a, usize>> {
@@ -16032,15 +15961,19 @@ impl Editor {
.filter(move |entry| severity.matches(entry.diagnostic.severity))
.filter(|entry| entry.range.start != entry.range.end)
.filter(|entry| !entry.diagnostic.is_unnecessary)
.filter(move |entry| !snapshot.intersects_fold(entry.range.start))
}
let snapshot = self.snapshot(window, cx);
let before = filtered(
snapshot.clone(),
severity,
buffer
.diagnostics_in_range(0..selection.start)
.filter(|entry| entry.range.start <= selection.start),
);
let after = filtered(
snapshot,
severity,
buffer
.diagnostics_in_range(selection.start..buffer.len())
@@ -16079,15 +16012,6 @@ impl Editor {
let Some(buffer_id) = buffer.buffer_id_for_anchor(next_diagnostic_start) else {
return;
};
let snapshot = self.snapshot(window, cx);
if snapshot.intersects_fold(next_diagnostic.range.start) {
self.unfold_ranges(
std::slice::from_ref(&next_diagnostic.range),
true,
false,
cx,
);
}
self.change_selections(Default::default(), window, cx, |s| {
s.select_ranges(vec![
next_diagnostic.range.start..next_diagnostic.range.start,
@@ -16687,7 +16611,7 @@ impl Editor {
editor.update_in(cx, |editor, window, cx| {
let range = target_range.to_point(target_buffer.read(cx));
let range = editor.range_for_match(&range, false);
let range = editor.range_for_match(&range);
let range = collapse_multiline_range(range);
if !split
@@ -19556,16 +19480,9 @@ impl Editor {
EditorSettings::get_global(cx).gutter.line_numbers
}
pub fn relative_line_numbers(&self, cx: &mut App) -> RelativeLineNumbers {
match (
self.use_relative_line_numbers,
EditorSettings::get_global(cx).relative_line_numbers,
) {
(None, setting) => setting,
(Some(false), _) => RelativeLineNumbers::Disabled,
(Some(true), RelativeLineNumbers::Wrapped) => RelativeLineNumbers::Wrapped,
(Some(true), _) => RelativeLineNumbers::Enabled,
}
pub fn should_use_relative_line_numbers(&self, cx: &mut App) -> bool {
self.use_relative_line_numbers
.unwrap_or(EditorSettings::get_global(cx).relative_line_numbers)
}
pub fn toggle_relative_line_numbers(
@@ -19574,8 +19491,8 @@ impl Editor {
_: &mut Window,
cx: &mut Context<Self>,
) {
let is_relative = self.relative_line_numbers(cx);
self.set_relative_line_number(Some(!is_relative.enabled()), cx)
let is_relative = self.should_use_relative_line_numbers(cx);
self.set_relative_line_number(Some(!is_relative), cx)
}
pub fn set_relative_line_number(&mut self, is_relative: Option<bool>, cx: &mut Context<Self>) {
@@ -21498,7 +21415,7 @@ impl Editor {
.and_then(|e| e.to_str())
.map(|a| a.to_string()));
let vim_mode = vim_flavor(cx).is_some();
let vim_mode = vim_enabled(cx);
let edit_predictions_provider = all_language_settings(file, cx).edit_predictions.provider;
let copilot_enabled = edit_predictions_provider
@@ -22129,26 +22046,10 @@ fn edit_for_markdown_paste<'a>(
(range, new_text)
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
pub enum VimFlavor {
Vim,
Helix,
}
pub fn vim_flavor(cx: &App) -> Option<VimFlavor> {
if vim_mode_setting::HelixModeSetting::try_get(cx)
.map(|helix_mode| helix_mode.0)
.unwrap_or(false)
{
Some(VimFlavor::Helix)
} else if vim_mode_setting::VimModeSetting::try_get(cx)
fn vim_enabled(cx: &App) -> bool {
vim_mode_setting::VimModeSetting::try_get(cx)
.map(|vim_mode| vim_mode.0)
.unwrap_or(false)
{
Some(VimFlavor::Vim)
} else {
None // neither vim nor helix mode
}
}
fn process_completion_for_edit(
@@ -22925,10 +22826,6 @@ pub trait CompletionProvider {
fn filter_completions(&self) -> bool {
true
}
fn show_snippets(&self) -> bool {
false
}
}
pub trait CodeActionProvider {
@@ -23189,8 +23086,16 @@ impl CompletionProvider for Entity<Project> {
cx: &mut Context<Editor>,
) -> Task<Result<Vec<CompletionResponse>>> {
self.update(cx, |project, cx| {
let task = project.completions(buffer, buffer_position, options, cx);
cx.background_spawn(task)
let snippets = snippet_completions(project, buffer, buffer_position, cx);
let project_completions = project.completions(buffer, buffer_position, options, cx);
cx.background_spawn(async move {
let mut responses = project_completions.await?;
let snippets = snippets.await?;
if !snippets.completions.is_empty() {
responses.push(snippets);
}
Ok(responses)
})
})
}
@@ -23262,10 +23167,6 @@ impl CompletionProvider for Entity<Project> {
buffer.completion_triggers().contains(text)
}
fn show_snippets(&self) -> bool {
true
}
}
impl SemanticsProvider for Entity<Project> {
@@ -24194,10 +24095,6 @@ impl EntityInputHandler for Editor {
let utf16_offset = anchor.to_offset_utf16(&position_map.snapshot.buffer_snapshot());
Some(utf16_offset.0)
}
fn accepts_text_input(&self, _window: &mut Window, _cx: &mut Context<Self>) -> bool {
self.input_enabled
}
}
trait SelectionExt {

View File

@@ -3,12 +3,12 @@ use core::num;
use gpui::App;
use language::CursorShape;
use project::project_settings::DiagnosticSeverity;
use settings::Settings;
pub use settings::{
CurrentLineHighlight, DelayMs, DisplayIn, DocumentColorsRenderMode, DoubleClickInMultibuffer,
GoToDefinitionFallback, HideMouseMode, MinimapThumb, MinimapThumbBorder, MultiCursorModifier,
ScrollBeyondLastLine, ScrollbarDiagnostics, SeedQuerySetting, ShowMinimap, SnippetSortOrder,
};
use settings::{RelativeLineNumbers, Settings};
use ui::scrollbars::{ScrollbarVisibility, ShowScrollbar};
/// Imports from the VSCode settings at
@@ -33,7 +33,7 @@ pub struct EditorSettings {
pub horizontal_scroll_margin: f32,
pub scroll_sensitivity: f32,
pub fast_scroll_sensitivity: f32,
pub relative_line_numbers: RelativeLineNumbers,
pub relative_line_numbers: bool,
pub seed_search_query_from_cursor: SeedQuerySetting,
pub use_smartcase_search: bool,
pub multi_cursor_modifier: MultiCursorModifier,
@@ -55,7 +55,6 @@ pub struct EditorSettings {
pub drag_and_drop_selection: DragAndDropSelection,
pub lsp_document_colors: DocumentColorsRenderMode,
pub minimum_contrast_for_highlights: f32,
pub completion_menu_scrollbar: ShowScrollbar,
}
#[derive(Debug, Clone)]
pub struct Jupyter {
@@ -160,7 +159,6 @@ pub struct SearchSettings {
pub case_sensitive: bool,
pub include_ignored: bool,
pub regex: bool,
pub center_on_match: bool,
}
impl EditorSettings {
@@ -251,7 +249,6 @@ impl Settings for EditorSettings {
case_sensitive: search.case_sensitive.unwrap(),
include_ignored: search.include_ignored.unwrap(),
regex: search.regex.unwrap(),
center_on_match: search.center_on_match.unwrap(),
},
auto_signature_help: editor.auto_signature_help.unwrap(),
show_signature_help_after_edits: editor.show_signature_help_after_edits.unwrap(),
@@ -269,7 +266,6 @@ impl Settings for EditorSettings {
},
lsp_document_colors: editor.lsp_document_colors.unwrap(),
minimum_contrast_for_highlights: editor.minimum_contrast_for_highlights.unwrap().0,
completion_menu_scrollbar: editor.completion_menu_scrollbar.map(Into::into).unwrap(),
}
}
}

View File

@@ -3136,77 +3136,6 @@ fn test_newline(cx: &mut TestAppContext) {
});
}
#[gpui::test]
async fn test_newline_yaml(cx: &mut TestAppContext) {
init_test(cx, |_| {});
let mut cx = EditorTestContext::new(cx).await;
let yaml_language = languages::language("yaml", tree_sitter_yaml::LANGUAGE.into());
cx.update_buffer(|buffer, cx| buffer.set_language(Some(yaml_language), cx));
// Object (between 2 fields)
cx.set_state(indoc! {"
test:ˇ
hello: bye"});
cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
cx.assert_editor_state(indoc! {"
test:
ˇ
hello: bye"});
// Object (first and single line)
cx.set_state(indoc! {"
test:ˇ"});
cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
cx.assert_editor_state(indoc! {"
test:
ˇ"});
// Array with objects (after first element)
cx.set_state(indoc! {"
test:
- foo: barˇ"});
cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
cx.assert_editor_state(indoc! {"
test:
- foo: bar
ˇ"});
// Array with objects and comment
cx.set_state(indoc! {"
test:
- foo: bar
- bar: # testˇ"});
cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
cx.assert_editor_state(indoc! {"
test:
- foo: bar
- bar: # test
ˇ"});
// Array with objects (after second element)
cx.set_state(indoc! {"
test:
- foo: bar
- bar: fooˇ"});
cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
cx.assert_editor_state(indoc! {"
test:
- foo: bar
- bar: foo
ˇ"});
// Array with strings (after first element)
cx.set_state(indoc! {"
test:
- fooˇ"});
cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
cx.assert_editor_state(indoc! {"
test:
- foo
ˇ"});
}
#[gpui::test]
fn test_newline_with_old_selections(cx: &mut TestAppContext) {
init_test(cx, |_| {});
@@ -11137,129 +11066,6 @@ async fn test_snippet_placeholder_choices(cx: &mut TestAppContext) {
});
}
#[gpui::test]
async fn test_snippet_tabstop_navigation_with_placeholders(cx: &mut TestAppContext) {
init_test(cx, |_| {});
fn assert_state(editor: &mut Editor, cx: &mut Context<Editor>, marked_text: &str) {
let (expected_text, selection_ranges) = marked_text_ranges(marked_text, false);
assert_eq!(editor.text(cx), expected_text);
assert_eq!(
editor
.selections
.ranges::<usize>(&editor.display_snapshot(cx)),
selection_ranges
);
}
let (text, insertion_ranges) = marked_text_ranges(
indoc! {"
ˇ
"},
false,
);
let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx));
let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
_ = editor.update_in(cx, |editor, window, cx| {
let snippet = Snippet::parse("type ${1|,i32,u32|} = $2; $3").unwrap();
editor
.insert_snippet(&insertion_ranges, snippet, window, cx)
.unwrap();
assert_state(
editor,
cx,
indoc! {"
type «» = ;•
"},
);
assert!(
editor.context_menu_visible(),
"Context menu should be visible for placeholder choices"
);
editor.next_snippet_tabstop(&NextSnippetTabstop, window, cx);
assert_state(
editor,
cx,
indoc! {"
type = «»;•
"},
);
assert!(
!editor.context_menu_visible(),
"Context menu should be hidden after moving to next tabstop"
);
editor.next_snippet_tabstop(&NextSnippetTabstop, window, cx);
assert_state(
editor,
cx,
indoc! {"
type = ; ˇ
"},
);
editor.next_snippet_tabstop(&NextSnippetTabstop, window, cx);
assert_state(
editor,
cx,
indoc! {"
type = ; ˇ
"},
);
});
_ = editor.update_in(cx, |editor, window, cx| {
editor.select_all(&SelectAll, window, cx);
editor.backspace(&Backspace, window, cx);
let snippet = Snippet::parse("fn ${1|,foo,bar|} = ${2:value}; $3").unwrap();
let insertion_ranges = editor
.selections
.all(&editor.display_snapshot(cx))
.iter()
.map(|s| s.range())
.collect::<Vec<_>>();
editor
.insert_snippet(&insertion_ranges, snippet, window, cx)
.unwrap();
assert_state(editor, cx, "fn «» = value;•");
assert!(
editor.context_menu_visible(),
"Context menu should be visible for placeholder choices"
);
editor.next_snippet_tabstop(&NextSnippetTabstop, window, cx);
assert_state(editor, cx, "fn = «valueˇ»;•");
editor.previous_snippet_tabstop(&PreviousSnippetTabstop, window, cx);
assert_state(editor, cx, "fn «» = value;•");
assert!(
editor.context_menu_visible(),
"Context menu should be visible again after returning to first tabstop"
);
editor.previous_snippet_tabstop(&PreviousSnippetTabstop, window, cx);
assert_state(editor, cx, "fn «» = value;•");
});
}
#[gpui::test]
async fn test_snippets(cx: &mut TestAppContext) {
init_test(cx, |_| {});
@@ -13883,7 +13689,7 @@ async fn test_completion_mode(cx: &mut TestAppContext) {
cx.set_state(&run.initial_state);
cx.update_editor(|editor, window, cx| {
editor.show_completions(&ShowCompletions, window, cx);
editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
});
let counter = Arc::new(AtomicUsize::new(0));
@@ -13943,7 +13749,7 @@ async fn test_completion_with_mode_specified_by_action(cx: &mut TestAppContext)
cx.set_state(initial_state);
cx.update_editor(|editor, window, cx| {
editor.show_completions(&ShowCompletions, window, cx);
editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
});
let counter = Arc::new(AtomicUsize::new(0));
@@ -13979,7 +13785,7 @@ async fn test_completion_with_mode_specified_by_action(cx: &mut TestAppContext)
cx.set_state(initial_state);
cx.update_editor(|editor, window, cx| {
editor.show_completions(&ShowCompletions, window, cx);
editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
});
handle_completion_request_with_insert_and_replace(
&mut cx,
@@ -14066,7 +13872,7 @@ async fn test_completion_replacing_surrounding_text_with_multicursors(cx: &mut T
"};
cx.set_state(initial_state);
cx.update_editor(|editor, window, cx| {
editor.show_completions(&ShowCompletions, window, cx);
editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
});
handle_completion_request_with_insert_and_replace(
&mut cx,
@@ -14120,7 +13926,7 @@ async fn test_completion_replacing_surrounding_text_with_multicursors(cx: &mut T
"};
cx.set_state(initial_state);
cx.update_editor(|editor, window, cx| {
editor.show_completions(&ShowCompletions, window, cx);
editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
});
handle_completion_request_with_insert_and_replace(
&mut cx,
@@ -14169,7 +13975,7 @@ async fn test_completion_replacing_surrounding_text_with_multicursors(cx: &mut T
"};
cx.set_state(initial_state);
cx.update_editor(|editor, window, cx| {
editor.show_completions(&ShowCompletions, window, cx);
editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
});
handle_completion_request_with_insert_and_replace(
&mut cx,
@@ -14288,7 +14094,7 @@ async fn test_completion_in_multibuffer_with_replace_range(cx: &mut TestAppConte
EditorMode::Full {
scale_ui_elements_with_buffer_font_size: false,
show_active_line_background: false,
sizing_behavior: SizingBehavior::Default,
sized_by_content: false,
},
multi_buffer.clone(),
Some(project.clone()),
@@ -14320,7 +14126,7 @@ async fn test_completion_in_multibuffer_with_replace_range(cx: &mut TestAppConte
});
editor.update_in(cx, |editor, window, cx| {
editor.show_completions(&ShowCompletions, window, cx);
editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
});
fake_server
@@ -14559,7 +14365,7 @@ async fn test_completion(cx: &mut TestAppContext) {
cx.assert_editor_state("editor.cloˇ");
assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
cx.update_editor(|editor, window, cx| {
editor.show_completions(&ShowCompletions, window, cx);
editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
});
handle_completion_request(
"editor.<clo|>",
@@ -14958,7 +14764,7 @@ async fn test_word_completions_usually_skip_digits(cx: &mut TestAppContext) {
4.5f32
"});
cx.update_editor(|editor, window, cx| {
editor.show_completions(&ShowCompletions, window, cx);
editor.show_completions(&ShowCompletions::default(), window, cx);
});
cx.executor().run_until_parked();
cx.condition(|editor, _| editor.context_menu_visible())
@@ -14984,7 +14790,7 @@ async fn test_word_completions_usually_skip_digits(cx: &mut TestAppContext) {
33.35f32
"});
cx.update_editor(|editor, window, cx| {
editor.show_completions(&ShowCompletions, window, cx);
editor.show_completions(&ShowCompletions::default(), window, cx);
});
cx.executor().run_until_parked();
cx.condition(|editor, _| editor.context_menu_visible())
@@ -15408,7 +15214,13 @@ async fn test_as_is_completions(cx: &mut TestAppContext) {
cx.set_state("fn a() {}\n");
cx.executor().run_until_parked();
cx.update_editor(|editor, window, cx| {
editor.trigger_completion_on_input("n", true, window, cx)
editor.show_completions(
&ShowCompletions {
trigger: Some("\n".into()),
},
window,
cx,
);
});
cx.executor().run_until_parked();
@@ -15506,7 +15318,7 @@ int fn_branch(bool do_branch1, bool do_branch2);
})))
});
cx.update_editor(|editor, window, cx| {
editor.show_completions(&ShowCompletions, window, cx);
editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
});
cx.executor().run_until_parked();
cx.update_editor(|editor, window, cx| {
@@ -15555,7 +15367,7 @@ int fn_branch(bool do_branch1, bool do_branch2);
})))
});
cx.update_editor(|editor, window, cx| {
editor.show_completions(&ShowCompletions, window, cx);
editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
});
cx.executor().run_until_parked();
cx.update_editor(|editor, window, cx| {
@@ -18045,7 +17857,7 @@ async fn test_context_menus_hide_hover_popover(cx: &mut gpui::TestAppContext) {
}
});
cx.update_editor(|editor, window, cx| {
editor.show_completions(&ShowCompletions, window, cx);
editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
});
completion_requests.next().await;
cx.condition(|editor, _| editor.context_menu_visible())
@@ -24441,7 +24253,7 @@ async fn test_html_linked_edits_on_completion(cx: &mut TestAppContext) {
])))
});
editor.update_in(cx, |editor, window, cx| {
editor.show_completions(&ShowCompletions, window, cx);
editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
});
cx.run_until_parked();
completion_handle.next().await.unwrap();

View File

@@ -8,8 +8,8 @@ use crate::{
HandleInput, HoveredCursor, InlayHintRefreshReason, JumpData, LineDown, LineHighlight, LineUp,
MAX_LINE_LEN, MINIMAP_FONT_SIZE, MULTI_BUFFER_EXCERPT_HEADER_HEIGHT, OpenExcerpts, PageDown,
PageUp, PhantomBreakpointIndicator, Point, RowExt, RowRangeExt, SelectPhase,
SelectedTextHighlight, Selection, SelectionDragState, SizingBehavior, SoftWrap,
StickyHeaderExcerpt, ToPoint, ToggleFold, ToggleFoldAll,
SelectedTextHighlight, Selection, SelectionDragState, SoftWrap, StickyHeaderExcerpt, ToPoint,
ToggleFold, ToggleFoldAll,
code_context_menus::{CodeActionsMenu, MENU_ASIDE_MAX_WIDTH, MENU_ASIDE_MIN_WIDTH, MENU_GAP},
display_map::{
Block, BlockContext, BlockStyle, ChunkRendererId, DisplaySnapshot, EditorMargins,
@@ -232,8 +232,6 @@ impl EditorElement {
register_action(editor, window, Editor::blame_hover);
register_action(editor, window, Editor::delete);
register_action(editor, window, Editor::tab);
register_action(editor, window, Editor::next_snippet_tabstop);
register_action(editor, window, Editor::previous_snippet_tabstop);
register_action(editor, window, Editor::backtab);
register_action(editor, window, Editor::indent);
register_action(editor, window, Editor::outdent);
@@ -766,14 +764,8 @@ impl EditorElement {
.row;
if line_numbers
.get(&MultiBufferRow(multi_buffer_row))
.is_some_and(|line_layout| {
line_layout.segments.iter().any(|segment| {
segment
.hitbox
.as_ref()
.is_some_and(|hitbox| hitbox.contains(&event.position))
})
})
.and_then(|line_number| line_number.hitbox.as_ref())
.is_some_and(|hitbox| hitbox.contains(&event.position))
{
let line_offset_from_top = display_row - position_map.scroll_position.y as u32;
@@ -1316,14 +1308,7 @@ impl EditorElement {
hover_at(editor, Some(anchor), window, cx);
Self::update_visible_cursor(editor, point, position_map, window, cx);
} else {
editor.update_inlay_link_and_hover_points(
&position_map.snapshot,
point_for_position,
modifiers.secondary(),
modifiers.shift,
window,
cx,
);
hover_at(editor, None, window, cx);
}
} else {
editor.hide_hovered_link(cx);
@@ -3163,7 +3148,6 @@ impl EditorElement {
snapshot: &EditorSnapshot,
rows: &Range<DisplayRow>,
relative_to: Option<DisplayRow>,
count_wrapped_lines: bool,
) -> HashMap<DisplayRow, DisplayRowDelta> {
let mut relative_rows: HashMap<DisplayRow, DisplayRowDelta> = Default::default();
let Some(relative_to) = relative_to else {
@@ -3181,15 +3165,8 @@ impl EditorElement {
let head_idx = relative_to.minus(start);
let mut delta = 1;
let mut i = head_idx + 1;
let should_count_line = |row_info: &RowInfo| {
if count_wrapped_lines {
row_info.buffer_row.is_some() || row_info.wrapped_buffer_row.is_some()
} else {
row_info.buffer_row.is_some()
}
};
while i < buffer_rows.len() as u32 {
if should_count_line(&buffer_rows[i as usize]) {
if buffer_rows[i as usize].buffer_row.is_some() {
if rows.contains(&DisplayRow(i + start.0)) {
relative_rows.insert(DisplayRow(i + start.0), delta);
}
@@ -3199,13 +3176,13 @@ impl EditorElement {
}
delta = 1;
i = head_idx.min(buffer_rows.len().saturating_sub(1) as u32);
while i > 0 && buffer_rows[i as usize].buffer_row.is_none() && !count_wrapped_lines {
while i > 0 && buffer_rows[i as usize].buffer_row.is_none() {
i -= 1;
}
while i > 0 {
i -= 1;
if should_count_line(&buffer_rows[i as usize]) {
if buffer_rows[i as usize].buffer_row.is_some() {
if rows.contains(&DisplayRow(i + start.0)) {
relative_rows.insert(DisplayRow(i + start.0), delta);
}
@@ -3237,7 +3214,7 @@ impl EditorElement {
return Arc::default();
}
let (newest_selection_head, relative) = self.editor.update(cx, |editor, cx| {
let (newest_selection_head, is_relative) = self.editor.update(cx, |editor, cx| {
let newest_selection_head = newest_selection_head.unwrap_or_else(|| {
let newest = editor
.selections
@@ -3253,93 +3230,79 @@ impl EditorElement {
)
.head
});
let relative = editor.relative_line_numbers(cx);
(newest_selection_head, relative)
let is_relative = editor.should_use_relative_line_numbers(cx);
(newest_selection_head, is_relative)
});
let relative_to = if relative.enabled() {
let relative_to = if is_relative {
Some(newest_selection_head.row())
} else {
None
};
let relative_rows =
self.calculate_relative_line_numbers(snapshot, &rows, relative_to, relative.wrapped());
let relative_rows = self.calculate_relative_line_numbers(snapshot, &rows, relative_to);
let mut line_number = String::new();
let segments = buffer_rows.iter().enumerate().flat_map(|(ix, row_info)| {
let display_row = DisplayRow(rows.start.0 + ix as u32);
line_number.clear();
let non_relative_number = if relative.wrapped() {
row_info.buffer_row.or(row_info.wrapped_buffer_row)? + 1
} else {
row_info.buffer_row? + 1
};
let number = relative_rows
.get(&display_row)
.unwrap_or(&non_relative_number);
write!(&mut line_number, "{number}").unwrap();
if row_info
.diff_status
.is_some_and(|status| status.is_deleted())
{
return None;
}
let line_numbers = buffer_rows
.iter()
.enumerate()
.flat_map(|(ix, row_info)| {
let display_row = DisplayRow(rows.start.0 + ix as u32);
line_number.clear();
let non_relative_number = row_info.buffer_row? + 1;
let number = relative_rows
.get(&display_row)
.unwrap_or(&non_relative_number);
write!(&mut line_number, "{number}").unwrap();
if row_info
.diff_status
.is_some_and(|status| status.is_deleted())
{
return None;
}
let color = active_rows
.get(&display_row)
.map(|spec| {
if spec.breakpoint {
cx.theme().colors().debugger_accent
} else {
cx.theme().colors().editor_active_line_number
}
})
.unwrap_or_else(|| cx.theme().colors().editor_line_number);
let shaped_line =
self.shape_line_number(SharedString::from(&line_number), color, window);
let scroll_top = scroll_position.y * ScrollPixelOffset::from(line_height);
let line_origin = gutter_hitbox.map(|hitbox| {
hitbox.origin
+ point(
hitbox.size.width - shaped_line.width - gutter_dimensions.right_padding,
ix as f32 * line_height
- Pixels::from(scroll_top % ScrollPixelOffset::from(line_height)),
let color = active_rows
.get(&display_row)
.map(|spec| {
if spec.breakpoint {
cx.theme().colors().debugger_accent
} else {
cx.theme().colors().editor_active_line_number
}
})
.unwrap_or_else(|| cx.theme().colors().editor_line_number);
let shaped_line =
self.shape_line_number(SharedString::from(&line_number), color, window);
let scroll_top = scroll_position.y * ScrollPixelOffset::from(line_height);
let line_origin = gutter_hitbox.map(|hitbox| {
hitbox.origin
+ point(
hitbox.size.width - shaped_line.width - gutter_dimensions.right_padding,
ix as f32 * line_height
- Pixels::from(scroll_top % ScrollPixelOffset::from(line_height)),
)
});
#[cfg(not(test))]
let hitbox = line_origin.map(|line_origin| {
window.insert_hitbox(
Bounds::new(line_origin, size(shaped_line.width, line_height)),
HitboxBehavior::Normal,
)
});
});
#[cfg(test)]
let hitbox = {
let _ = line_origin;
None
};
#[cfg(not(test))]
let hitbox = line_origin.map(|line_origin| {
window.insert_hitbox(
Bounds::new(line_origin, size(shaped_line.width, line_height)),
HitboxBehavior::Normal,
)
});
#[cfg(test)]
let hitbox = {
let _ = line_origin;
None
};
let segment = LineNumberSegment {
shaped_line,
hitbox,
};
let buffer_row = DisplayPoint::new(display_row, 0).to_point(snapshot).row;
let multi_buffer_row = MultiBufferRow(buffer_row);
Some((multi_buffer_row, segment))
});
let mut line_numbers: HashMap<MultiBufferRow, LineNumberLayout> = HashMap::default();
for (buffer_row, segment) in segments {
line_numbers
.entry(buffer_row)
.or_insert_with(|| LineNumberLayout {
segments: Default::default(),
})
.segments
.push(segment);
}
let multi_buffer_row = DisplayPoint::new(display_row, 0).to_point(snapshot).row;
let multi_buffer_row = MultiBufferRow(multi_buffer_row);
let line_number = LineNumberLayout {
shaped_line,
hitbox,
};
Some((multi_buffer_row, line_number))
})
.collect();
Arc::new(line_numbers)
}
@@ -5144,26 +5107,23 @@ impl EditorElement {
snapshot,
visible_display_row_range.clone(),
max_size,
&editor.text_layout_details(window),
window,
cx,
)
});
let Some((popover_position, hover_popovers)) = hover_popovers else {
let Some((position, hover_popovers)) = hover_popovers else {
return;
};
// This is safe because we check on layout whether the required row is available
let hovered_row_layout = &line_layouts[popover_position
.row()
.minus(visible_display_row_range.start)
as usize];
let hovered_row_layout =
&line_layouts[position.row().minus(visible_display_row_range.start) as usize];
// Compute Hovered Point
let x = hovered_row_layout.x_for_index(popover_position.column() as usize)
let x = hovered_row_layout.x_for_index(position.column() as usize)
- Pixels::from(scroll_pixel_position.x);
let y = Pixels::from(
popover_position.row().as_f64() * ScrollPixelOffset::from(line_height)
position.row().as_f64() * ScrollPixelOffset::from(line_height)
- scroll_pixel_position.y,
);
let hovered_point = content_origin + point(x, y);
@@ -5880,36 +5840,34 @@ impl EditorElement {
let line_height = layout.position_map.line_height;
window.set_cursor_style(CursorStyle::Arrow, &layout.gutter_hitbox);
for line_layout in layout.line_numbers.values() {
for LineNumberSegment {
shaped_line,
hitbox,
} in &line_layout.segments
{
let Some(hitbox) = hitbox else {
continue;
};
for LineNumberLayout {
shaped_line,
hitbox,
} in layout.line_numbers.values()
{
let Some(hitbox) = hitbox else {
continue;
};
let Some(()) = (if !is_singleton && hitbox.is_hovered(window) {
let color = cx.theme().colors().editor_hover_line_number;
let Some(()) = (if !is_singleton && hitbox.is_hovered(window) {
let color = cx.theme().colors().editor_hover_line_number;
let line = self.shape_line_number(shaped_line.text.clone(), color, window);
line.paint(hitbox.origin, line_height, window, cx).log_err()
} else {
shaped_line
.paint(hitbox.origin, line_height, window, cx)
.log_err()
}) else {
continue;
};
let line = self.shape_line_number(shaped_line.text.clone(), color, window);
line.paint(hitbox.origin, line_height, window, cx).log_err()
} else {
shaped_line
.paint(hitbox.origin, line_height, window, cx)
.log_err()
}) else {
continue;
};
// In singleton buffers, we select corresponding lines on the line number click, so use | -like cursor.
// In multi buffers, we open file at the line number clicked, so use a pointing hand cursor.
if is_singleton {
window.set_cursor_style(CursorStyle::IBeam, hitbox);
} else {
window.set_cursor_style(CursorStyle::PointingHand, hitbox);
}
// In singleton buffers, we select corresponding lines on the line number click, so use | -like cursor.
// In multi buffers, we open file at the line number clicked, so use a pointing hand cursor.
if is_singleton {
window.set_cursor_style(CursorStyle::IBeam, hitbox);
} else {
window.set_cursor_style(CursorStyle::PointingHand, hitbox);
}
}
}
@@ -8478,11 +8436,11 @@ impl Element for EditorElement {
window.request_layout(style, None, cx)
}
EditorMode::Full {
sizing_behavior, ..
sized_by_content, ..
} => {
let mut style = Style::default();
style.size.width = relative(1.).into();
if sizing_behavior == SizingBehavior::SizeByContent {
if sized_by_content {
let snapshot = editor.snapshot(window, cx);
let line_height =
self.style.text.line_height_in_pixels(window.rem_size());
@@ -8646,8 +8604,7 @@ impl Element for EditorElement {
EditorMode::SingleLine
| EditorMode::AutoHeight { .. }
| EditorMode::Full {
sizing_behavior: SizingBehavior::ExcludeOverscrollMargin
| SizingBehavior::SizeByContent,
sized_by_content: true,
..
}
) {
@@ -9823,17 +9780,11 @@ impl EditorLayout {
}
}
#[derive(Debug)]
struct LineNumberSegment {
struct LineNumberLayout {
shaped_line: ShapedLine,
hitbox: Option<Hitbox>,
}
#[derive(Debug)]
struct LineNumberLayout {
segments: SmallVec<[LineNumberSegment; 1]>,
}
struct ColoredRange<T> {
start: T,
end: T,
@@ -10897,7 +10848,6 @@ mod tests {
&snapshot,
&(DisplayRow(0)..DisplayRow(6)),
Some(DisplayRow(3)),
false,
)
})
.unwrap();
@@ -10916,7 +10866,6 @@ mod tests {
&snapshot,
&(DisplayRow(3)..DisplayRow(6)),
Some(DisplayRow(1)),
false,
)
})
.unwrap();
@@ -10933,7 +10882,6 @@ mod tests {
&snapshot,
&(DisplayRow(0)..DisplayRow(3)),
Some(DisplayRow(6)),
false,
)
})
.unwrap();
@@ -10943,81 +10891,6 @@ mod tests {
assert_eq!(relative_rows[&DisplayRow(2)], 3);
}
#[gpui::test]
fn test_shape_line_numbers_wrapping(cx: &mut TestAppContext) {
init_test(cx, |_| {});
let window = cx.add_window(|window, cx| {
let buffer = MultiBuffer::build_simple(&sample_text(6, 6, 'a'), cx);
Editor::new(EditorMode::full(), buffer, None, window, cx)
});
update_test_language_settings(cx, |s| {
s.defaults.preferred_line_length = Some(5_u32);
s.defaults.soft_wrap = Some(language_settings::SoftWrap::PreferredLineLength);
});
let editor = window.root(cx).unwrap();
let style = cx.update(|cx| editor.read(cx).style().unwrap().clone());
let line_height = window
.update(cx, |_, window, _| {
style.text.line_height_in_pixels(window.rem_size())
})
.unwrap();
let element = EditorElement::new(&editor, style);
let snapshot = window
.update(cx, |editor, window, cx| editor.snapshot(window, cx))
.unwrap();
let layouts = cx
.update_window(*window, |_, window, cx| {
element.layout_line_numbers(
None,
GutterDimensions {
left_padding: Pixels::ZERO,
right_padding: Pixels::ZERO,
width: px(30.0),
margin: Pixels::ZERO,
git_blame_entries_width: None,
},
line_height,
gpui::Point::default(),
DisplayRow(0)..DisplayRow(6),
&(0..6)
.map(|row| RowInfo {
buffer_row: Some(row),
..Default::default()
})
.collect::<Vec<_>>(),
&BTreeMap::default(),
Some(DisplayPoint::new(DisplayRow(0), 0)),
&snapshot,
window,
cx,
)
})
.unwrap();
assert_eq!(layouts.len(), 3);
let relative_rows = window
.update(cx, |editor, window, cx| {
let snapshot = editor.snapshot(window, cx);
element.calculate_relative_line_numbers(
&snapshot,
&(DisplayRow(0)..DisplayRow(6)),
Some(DisplayRow(3)),
true,
)
})
.unwrap();
assert_eq!(relative_rows[&DisplayRow(0)], 3);
assert_eq!(relative_rows[&DisplayRow(1)], 2);
assert_eq!(relative_rows[&DisplayRow(2)], 1);
// current line has no relative number
assert_eq!(relative_rows[&DisplayRow(4)], 1);
assert_eq!(relative_rows[&DisplayRow(5)], 2);
}
#[gpui::test]
async fn test_vim_visual_selections(cx: &mut TestAppContext) {
init_test(cx, |_| {});
@@ -11131,13 +11004,7 @@ mod tests {
state
.line_numbers
.get(&MultiBufferRow(0))
.map(|line_number| line_number
.segments
.first()
.unwrap()
.shaped_line
.text
.as_ref()),
.map(|line_number| line_number.shaped_line.text.as_ref()),
Some("1")
);
}

View File

@@ -1,7 +1,6 @@
use crate::Editor;
use anyhow::Result;
use collections::HashMap;
use futures::StreamExt;
use git::{
GitHostingProviderRegistry, GitRemote, Oid,
blame::{Blame, BlameEntry, ParsedCommitMessage},
@@ -508,7 +507,7 @@ impl GitBlame {
let buffer_edits = buffer.update(cx, |buffer, _| buffer.subscribe());
let blame_buffer = project.blame_buffer(&buffer, None, cx);
Some(async move { (id, snapshot, buffer_edits, blame_buffer.await) })
Some((id, snapshot, buffer_edits, blame_buffer))
})
.collect::<Vec<_>>()
});
@@ -518,14 +517,10 @@ impl GitBlame {
let (result, errors) = cx
.background_spawn({
async move {
let blame = futures::stream::iter(blame)
.buffered(4)
.collect::<Vec<_>>()
.await;
let mut res = vec![];
let mut errors = vec![];
for (id, snapshot, buffer_edits, blame) in blame {
match blame {
match blame.await {
Ok(Some(Blame {
entries,
messages,
@@ -602,7 +597,6 @@ impl GitBlame {
}
fn regenerate_on_edit(&mut self, cx: &mut Context<Self>) {
// todo(lw): hot foreground spawn
self.regenerate_on_edit_task = cx.spawn(async move |this, cx| {
cx.background_executor()
.timer(REGENERATE_ON_EDIT_DEBOUNCE_INTERVAL)

View File

@@ -3,7 +3,6 @@ use crate::{
EditorSnapshot, GlobalDiagnosticRenderer, Hover,
display_map::{InlayOffset, ToDisplayPoint, invisibles::is_invisible},
hover_links::{InlayHighlight, RangeInEditor},
movement::TextLayoutDetails,
scroll::ScrollAmount,
};
use anyhow::Context as _;
@@ -767,13 +766,9 @@ impl HoverState {
snapshot: &EditorSnapshot,
visible_rows: Range<DisplayRow>,
max_size: Size<Pixels>,
text_layout_details: &TextLayoutDetails,
window: &mut Window,
cx: &mut Context<Editor>,
) -> Option<(DisplayPoint, Vec<AnyElement>)> {
if !self.visible() {
return None;
}
// If there is a diagnostic, position the popovers based on that.
// Otherwise use the start of the hover range
let anchor = self
@@ -796,32 +791,10 @@ impl HoverState {
}
})
})?;
let mut point = anchor.to_display_point(&snapshot.display_snapshot);
// Clamp the point within the visible rows in case the popup source spans multiple lines
if visible_rows.end <= point.row() {
point = crate::movement::up_by_rows(
&snapshot.display_snapshot,
point,
1 + (point.row() - visible_rows.end).0,
text::SelectionGoal::None,
true,
text_layout_details,
)
.0;
} else if point.row() < visible_rows.start {
point = crate::movement::down_by_rows(
&snapshot.display_snapshot,
point,
(visible_rows.start - point.row()).0,
text::SelectionGoal::None,
true,
text_layout_details,
)
.0;
}
let point = anchor.to_display_point(&snapshot.display_snapshot);
if !visible_rows.contains(&point.row()) {
log::error!("Hover popover point out of bounds after moving");
// Don't render if the relevant point isn't on screen
if !self.visible() || !visible_rows.contains(&point.row()) {
return None;
}

View File

@@ -1188,17 +1188,17 @@ pub mod tests {
})
.unwrap();
let progress_token = 42;
let progress_token = "test_progress_token";
fake_server
.request::<lsp::request::WorkDoneProgressCreate>(lsp::WorkDoneProgressCreateParams {
token: lsp::ProgressToken::Number(progress_token),
token: lsp::ProgressToken::String(progress_token.to_string()),
})
.await
.into_response()
.expect("work done progress create request failed");
cx.executor().run_until_parked();
fake_server.notify::<lsp::notification::Progress>(lsp::ProgressParams {
token: lsp::ProgressToken::Number(progress_token),
token: lsp::ProgressToken::String(progress_token.to_string()),
value: lsp::ProgressParamsValue::WorkDone(lsp::WorkDoneProgress::Begin(
lsp::WorkDoneProgressBegin::default(),
)),
@@ -1218,7 +1218,7 @@ pub mod tests {
.unwrap();
fake_server.notify::<lsp::notification::Progress>(lsp::ProgressParams {
token: lsp::ProgressToken::Number(progress_token),
token: lsp::ProgressToken::String(progress_token.to_string()),
value: lsp::ProgressParamsValue::WorkDone(lsp::WorkDoneProgress::End(
lsp::WorkDoneProgressEnd::default(),
)),

View File

@@ -1587,18 +1587,12 @@ impl SearchableItem for Editor {
&mut self,
index: usize,
matches: &[Range<Anchor>],
collapse: bool,
window: &mut Window,
cx: &mut Context<Self>,
) {
self.unfold_ranges(&[matches[index].clone()], false, true, cx);
let range = self.range_for_match(&matches[index], collapse);
let autoscroll = if EditorSettings::get_global(cx).search.center_on_match {
Autoscroll::center()
} else {
Autoscroll::fit()
};
self.change_selections(SelectionEffects::scroll(autoscroll), window, cx, |s| {
let range = self.range_for_match(&matches[index]);
self.change_selections(Default::default(), window, cx, |s| {
s.select_ranges([range]);
})
}

View File

@@ -60,10 +60,8 @@ async fn lsp_task_context(
buffer: &Entity<Buffer>,
cx: &mut AsyncApp,
) -> Option<TaskContext> {
let (worktree_store, environment) = project
.read_with(cx, |project, _| {
(project.worktree_store(), project.environment().clone())
})
let worktree_store = project
.read_with(cx, |project, _| project.worktree_store())
.ok()?;
let worktree_abs_path = cx
@@ -76,9 +74,9 @@ async fn lsp_task_context(
})
.ok()?;
let project_env = environment
.update(cx, |environment, cx| {
environment.buffer_environment(buffer, &worktree_store, cx)
let project_env = project
.update(cx, |project, cx| {
project.buffer_environment(buffer, &worktree_store, cx)
})
.ok()?
.await;

View File

@@ -173,6 +173,10 @@ pub struct AgentServerManifestEntry {
/// cmd = "node"
/// args = ["index.js", "--port", "3000"]
/// ```
///
/// Note: All commands are executed with the archive extraction directory as the
/// working directory, so relative paths in args (like "index.js") will resolve
/// relative to the extracted archive contents.
pub targets: HashMap<String, TargetConfig>,
}

View File

@@ -145,10 +145,6 @@ fn extension_provides(manifest: &ExtensionManifest) -> BTreeSet<ExtensionProvide
provides.insert(ExtensionProvides::ContextServers);
}
if !manifest.agent_servers.is_empty() {
provides.insert(ExtensionProvides::AgentServers);
}
if manifest.snippets.is_some() {
provides.insert(ExtensionProvides::Snippets);
}

View File

@@ -225,9 +225,6 @@ impl ExtensionFilter {
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]
enum Feature {
AgentClaude,
AgentCodex,
AgentGemini,
ExtensionRuff,
ExtensionTailwind,
Git,
@@ -247,9 +244,6 @@ fn keywords_by_feature() -> &'static BTreeMap<Feature, Vec<&'static str>> {
static KEYWORDS_BY_FEATURE: OnceLock<BTreeMap<Feature, Vec<&'static str>>> = OnceLock::new();
KEYWORDS_BY_FEATURE.get_or_init(|| {
BTreeMap::from_iter([
(Feature::AgentClaude, vec!["claude", "claude code"]),
(Feature::AgentCodex, vec!["codex", "codex cli"]),
(Feature::AgentGemini, vec!["gemini", "gemini cli"]),
(Feature::ExtensionRuff, vec!["ruff"]),
(Feature::ExtensionTailwind, vec!["tail", "tailwind"]),
(Feature::Git, vec!["git"]),
@@ -805,22 +799,25 @@ impl ExtensionsPage {
)
.child(
h_flex()
.gap_1()
.gap_2()
.justify_between()
.child(
Icon::new(IconName::Person)
.size(IconSize::XSmall)
.color(Color::Muted),
)
.child(
Label::new(extension.manifest.authors.join(", "))
.size(LabelSize::Small)
.color(Color::Muted)
.truncate(),
h_flex()
.gap_1()
.child(
Icon::new(IconName::Person)
.size(IconSize::XSmall)
.color(Color::Muted),
)
.child(
Label::new(extension.manifest.authors.join(", "))
.size(LabelSize::Small)
.color(Color::Muted)
.truncate(),
),
)
.child(
h_flex()
.ml_auto()
.gap_1()
.child(
IconButton::new(
@@ -1425,24 +1422,6 @@ impl ExtensionsPage {
for feature in &self.upsells {
let banner = match feature {
Feature::AgentClaude => self.render_feature_upsell_banner(
"Claude Code support is built-in to Zed!".into(),
"https://zed.dev/docs/ai/external-agents#claude-code".into(),
false,
cx,
),
Feature::AgentCodex => self.render_feature_upsell_banner(
"Codex CLI support is built-in to Zed!".into(),
"https://zed.dev/docs/ai/external-agents#codex-cli".into(),
false,
cx,
),
Feature::AgentGemini => self.render_feature_upsell_banner(
"Gemini CLI support is built-in to Zed!".into(),
"https://zed.dev/docs/ai/external-agents#gemini-cli".into(),
false,
cx,
),
Feature::ExtensionRuff => self.render_feature_upsell_banner(
"Ruff (linter for Python) support is built-in to Zed!".into(),
"https://zed.dev/docs/languages/python#code-formatting--linting".into(),

View File

@@ -7,7 +7,6 @@ use menu::{Confirm, SelectNext, SelectPrevious};
use pretty_assertions::{assert_eq, assert_matches};
use project::{FS_WATCH_LATENCY, RemoveOptions};
use serde_json::json;
use settings::SettingsStore;
use util::{path, rel_path::rel_path};
use workspace::{AppState, CloseActiveItem, OpenOptions, ToggleFileFinder, Workspace, open_paths};
@@ -659,147 +658,6 @@ async fn test_matching_cancellation(cx: &mut TestAppContext) {
});
}
#[gpui::test]
async fn test_ignored_root_with_file_inclusions(cx: &mut TestAppContext) {
let app_state = init_test(cx);
cx.update(|cx| {
cx.update_global::<SettingsStore, _>(|store, cx| {
store.update_user_settings(cx, |settings| {
settings.project.worktree.file_scan_inclusions = Some(vec![
"height_demo/**/hi_bonjour".to_string(),
"**/height_1".to_string(),
]);
});
})
});
app_state
.fs
.as_fake()
.insert_tree(
"/ancestor",
json!({
".gitignore": "ignored-root",
"ignored-root": {
"happiness": "",
"height": "",
"hi": "",
"hiccup": "",
},
"tracked-root": {
".gitignore": "height*",
"happiness": "",
"height": "",
"heights": {
"height_1": "",
"height_2": "",
},
"height_demo": {
"test_1": {
"hi_bonjour": "hi_bonjour",
"hi": "hello",
},
"hihi": "bye",
"test_2": {
"hoi": "nl"
}
},
"height_include": {
"height_1_include": "",
"height_2_include": "",
},
"hi": "",
"hiccup": "",
},
}),
)
.await;
let project = Project::test(
app_state.fs.clone(),
[
Path::new(path!("/ancestor/tracked-root")),
Path::new(path!("/ancestor/ignored-root")),
],
cx,
)
.await;
let (picker, _workspace, cx) = build_find_picker(project, cx);
picker
.update_in(cx, |picker, window, cx| {
picker
.delegate
.spawn_search(test_path_position("hi"), window, cx)
})
.await;
picker.update(cx, |picker, _| {
let matches = collect_search_matches(picker);
assert_eq!(matches.history.len(), 0);
assert_eq!(
matches.search,
vec![
rel_path("ignored-root/hi").into(),
rel_path("tracked-root/hi").into(),
rel_path("ignored-root/hiccup").into(),
rel_path("tracked-root/hiccup").into(),
rel_path("tracked-root/height_demo/test_1/hi_bonjour").into(),
rel_path("ignored-root/height").into(),
rel_path("tracked-root/heights/height_1").into(),
rel_path("ignored-root/happiness").into(),
rel_path("tracked-root/happiness").into(),
],
"All ignored files that were indexed are found for default ignored mode"
);
});
}
#[gpui::test]
async fn test_ignored_root_with_file_inclusions_repro(cx: &mut TestAppContext) {
let app_state = init_test(cx);
cx.update(|cx| {
cx.update_global::<SettingsStore, _>(|store, cx| {
store.update_user_settings(cx, |settings| {
settings.project.worktree.file_scan_inclusions = Some(vec!["**/.env".to_string()]);
});
})
});
app_state
.fs
.as_fake()
.insert_tree(
"/src",
json!({
".gitignore": "node_modules",
"node_modules": {
"package.json": "// package.json",
".env": "BAR=FOO"
},
".env": "FOO=BAR"
}),
)
.await;
let project = Project::test(app_state.fs.clone(), [Path::new(path!("/src"))], cx).await;
let (picker, _workspace, cx) = build_find_picker(project, cx);
picker
.update_in(cx, |picker, window, cx| {
picker
.delegate
.spawn_search(test_path_position("json"), window, cx)
})
.await;
picker.update(cx, |picker, _| {
let matches = collect_search_matches(picker);
assert_eq!(matches.history.len(), 0);
assert_eq!(
matches.search,
vec![],
"All ignored files that were indexed are found for default ignored mode"
);
});
}
#[gpui::test]
async fn test_ignored_root(cx: &mut TestAppContext) {
let app_state = init_test(cx);

View File

@@ -7,7 +7,7 @@ use git::{
blame::Blame,
repository::{
AskPassDelegate, Branch, CommitDetails, CommitOptions, FetchOptions, GitRepository,
GitRepositoryCheckpoint, PushOptions, Remote, RepoPath, ResetMode, Worktree,
GitRepositoryCheckpoint, PushOptions, Remote, RepoPath, ResetMode,
},
status::{
DiffTreeType, FileStatus, GitStatus, StatusCode, TrackedStatus, TreeDiff, TreeDiffStatus,
@@ -387,19 +387,6 @@ impl GitRepository for FakeGitRepository {
})
}
fn worktrees(&self) -> BoxFuture<'_, Result<Vec<Worktree>>> {
unimplemented!()
}
fn create_worktree(
&self,
_: String,
_: PathBuf,
_: Option<String>,
) -> BoxFuture<'_, Result<()>> {
unimplemented!()
}
fn change_branch(&self, name: String) -> BoxFuture<'_, Result<()>> {
self.with_state_async(true, |state| {
state.current_branch_name = Some(name);
@@ -543,7 +530,6 @@ impl GitRepository for FakeGitRepository {
&self,
_branch: String,
_remote: String,
_rebase: bool,
_askpass: AskPassDelegate,
_env: Arc<HashMap<String, String>>,
_cx: AsyncApp,

View File

@@ -377,7 +377,7 @@ impl Fs for RealFs {
#[cfg(windows)]
if smol::fs::metadata(&target).await?.is_dir() {
let status = new_smol_command("cmd")
let status = smol::process::Command::new("cmd")
.args(["/C", "mklink", "/J"])
.args([path, target.as_path()])
.status()

View File

@@ -72,8 +72,6 @@ actions!(
ForcePush,
/// Pulls changes from the remote repository.
Pull,
/// Pulls changes from the remote repository with rebase.
PullRebase,
/// Fetches changes from the remote repository.
Fetch,
/// Fetches changes from a specific remote.

View File

@@ -72,50 +72,6 @@ impl Branch {
}
}
#[derive(Clone, Debug, Hash, PartialEq, Eq)]
pub struct Worktree {
pub path: PathBuf,
pub ref_name: SharedString,
pub sha: SharedString,
}
impl Worktree {
pub fn branch(&self) -> &str {
self.ref_name
.as_ref()
.strip_prefix("refs/heads/")
.or_else(|| self.ref_name.as_ref().strip_prefix("refs/remotes/"))
.unwrap_or(self.ref_name.as_ref())
}
}
pub fn parse_worktrees_from_str<T: AsRef<str>>(raw_worktrees: T) -> Vec<Worktree> {
let mut worktrees = Vec::new();
let entries = raw_worktrees.as_ref().split("\n\n");
for entry in entries {
let mut parts = entry.splitn(3, '\n');
let path = parts
.next()
.and_then(|p| p.split_once(' ').map(|(_, path)| path.to_string()));
let sha = parts
.next()
.and_then(|p| p.split_once(' ').map(|(_, sha)| sha.to_string()));
let ref_name = parts
.next()
.and_then(|p| p.split_once(' ').map(|(_, ref_name)| ref_name.to_string()));
if let (Some(path), Some(sha), Some(ref_name)) = (path, sha, ref_name) {
worktrees.push(Worktree {
path: PathBuf::from(path),
ref_name: ref_name.into(),
sha: sha.into(),
})
}
}
worktrees
}
#[derive(Clone, Debug, Hash, PartialEq, Eq)]
pub struct Upstream {
pub ref_name: SharedString,
@@ -434,15 +390,6 @@ pub trait GitRepository: Send + Sync {
fn create_branch(&self, name: String) -> BoxFuture<'_, Result<()>>;
fn rename_branch(&self, branch: String, new_name: String) -> BoxFuture<'_, Result<()>>;
fn worktrees(&self) -> BoxFuture<'_, Result<Vec<Worktree>>>;
fn create_worktree(
&self,
name: String,
directory: PathBuf,
from_commit: Option<String>,
) -> BoxFuture<'_, Result<()>>;
fn reset(
&self,
commit: String,
@@ -533,7 +480,6 @@ pub trait GitRepository: Send + Sync {
&self,
branch_name: String,
upstream_name: String,
rebase: bool,
askpass: AskPassDelegate,
env: Arc<HashMap<String, String>>,
// This method takes an AsyncApp to ensure it's invoked on the main thread,
@@ -1260,66 +1206,6 @@ impl GitRepository for RealGitRepository {
.boxed()
}
fn worktrees(&self) -> BoxFuture<'_, Result<Vec<Worktree>>> {
let git_binary_path = self.any_git_binary_path.clone();
let working_directory = self.working_directory();
self.executor
.spawn(async move {
let output = new_smol_command(&git_binary_path)
.current_dir(working_directory?)
.args(&["--no-optional-locks", "worktree", "list", "--porcelain"])
.output()
.await?;
if output.status.success() {
let stdout = String::from_utf8_lossy(&output.stdout);
Ok(parse_worktrees_from_str(&stdout))
} else {
let stderr = String::from_utf8_lossy(&output.stderr);
anyhow::bail!("git worktree list failed: {stderr}");
}
})
.boxed()
}
fn create_worktree(
&self,
name: String,
directory: PathBuf,
from_commit: Option<String>,
) -> BoxFuture<'_, Result<()>> {
let git_binary_path = self.any_git_binary_path.clone();
let working_directory = self.working_directory();
let final_path = directory.join(&name);
let mut args = vec![
OsString::from("--no-optional-locks"),
OsString::from("worktree"),
OsString::from("add"),
OsString::from(final_path.as_os_str()),
];
if let Some(from_commit) = from_commit {
args.extend([
OsString::from("-b"),
OsString::from(name.as_str()),
OsString::from(from_commit),
]);
}
self.executor
.spawn(async move {
let output = new_smol_command(&git_binary_path)
.current_dir(working_directory?)
.args(args)
.output()
.await?;
if output.status.success() {
Ok(())
} else {
let stderr = String::from_utf8_lossy(&output.stderr);
anyhow::bail!("git worktree list failed: {stderr}");
}
})
.boxed()
}
fn change_branch(&self, name: String) -> BoxFuture<'_, Result<()>> {
let repo = self.repository.clone();
let working_directory = self.working_directory();
@@ -1393,17 +1279,18 @@ impl GitRepository for RealGitRepository {
.remote_url("upstream")
.or_else(|| self.remote_url("origin"));
async move {
crate::blame::Blame::for_path(
&git_binary_path,
&working_directory?,
&path,
&content,
remote_url,
)
.await
}
.boxed()
self.executor
.spawn(async move {
crate::blame::Blame::for_path(
&git_binary_path,
&working_directory?,
&path,
&content,
remote_url,
)
.await
})
.boxed()
}
fn diff(&self, diff: DiffType) -> BoxFuture<'_, Result<String>> {
@@ -1691,7 +1578,6 @@ impl GitRepository for RealGitRepository {
&self,
branch_name: String,
remote_name: String,
rebase: bool,
ask_pass: AskPassDelegate,
env: Arc<HashMap<String, String>>,
cx: AsyncApp,
@@ -1705,13 +1591,7 @@ impl GitRepository for RealGitRepository {
command
.envs(env.iter())
.current_dir(&working_directory?)
.arg("pull");
if rebase {
command.arg("--rebase");
}
command
.args(["pull"])
.arg(remote_name)
.arg(branch_name)
.stdout(smol::process::Stdio::piped())

View File

@@ -23,7 +23,6 @@ serde.workspace = true
serde_json.workspace = true
settings.workspace = true
url.workspace = true
urlencoding.workspace = true
util.workspace = true
[dev-dependencies]

View File

@@ -1,11 +1,5 @@
use std::{str::FromStr, sync::Arc};
use std::str::FromStr;
use anyhow::{Context as _, Result, bail};
use async_trait::async_trait;
use futures::AsyncReadExt;
use gpui::SharedString;
use http_client::{AsyncBody, HttpClient, HttpRequestExt, Request};
use serde::Deserialize;
use url::Url;
use git::{
@@ -15,55 +9,6 @@ use git::{
pub struct Gitee;
#[derive(Debug, Deserialize)]
struct CommitDetails {
author: Option<Author>,
}
#[derive(Debug, Deserialize)]
struct Author {
avatar_url: String,
}
impl Gitee {
async fn fetch_gitee_commit_author(
&self,
repo_owner: &str,
repo: &str,
commit: &str,
client: &Arc<dyn HttpClient>,
) -> Result<Option<Author>> {
let url = format!("https://gitee.com/api/v5/repos/{repo_owner}/{repo}/commits/{commit}");
let request = Request::get(&url)
.header("Content-Type", "application/json")
.follow_redirects(http_client::RedirectPolicy::FollowAll);
let mut response = client
.send(request.body(AsyncBody::default())?)
.await
.with_context(|| format!("error fetching Gitee commit details at {:?}", url))?;
let mut body = Vec::new();
response.body_mut().read_to_end(&mut body).await?;
if response.status().is_client_error() {
let text = String::from_utf8_lossy(body.as_slice());
bail!(
"status error {}, response: {text:?}",
response.status().as_u16()
);
}
let body_str = std::str::from_utf8(&body)?;
serde_json::from_str::<CommitDetails>(body_str)
.map(|commit| commit.author)
.context("failed to deserialize Gitee commit details")
}
}
#[async_trait]
impl GitHostingProvider for Gitee {
fn name(&self) -> String {
"Gitee".to_string()
@@ -74,7 +19,7 @@ impl GitHostingProvider for Gitee {
}
fn supports_avatars(&self) -> bool {
true
false
}
fn format_line_number(&self, line: u32) -> String {
@@ -135,26 +80,6 @@ impl GitHostingProvider for Gitee {
);
permalink
}
async fn commit_author_avatar_url(
&self,
repo_owner: &str,
repo: &str,
commit: SharedString,
http_client: Arc<dyn HttpClient>,
) -> Result<Option<Url>> {
let commit = commit.to_string();
let avatar_url = self
.fetch_gitee_commit_author(repo_owner, repo, &commit, &http_client)
.await?
.map(|author| -> Result<Url, url::ParseError> {
let mut url = Url::parse(&author.avatar_url)?;
url.set_query(Some("width=128"));
Ok(url)
})
.transpose()?;
Ok(avatar_url)
}
}
#[cfg(test)]

View File

@@ -1,11 +1,6 @@
use std::{str::FromStr, sync::Arc};
use std::str::FromStr;
use anyhow::{Context as _, Result, bail};
use async_trait::async_trait;
use futures::AsyncReadExt;
use gpui::SharedString;
use http_client::{AsyncBody, HttpClient, HttpRequestExt, Request};
use serde::Deserialize;
use anyhow::{Result, bail};
use url::Url;
use git::{
@@ -15,16 +10,6 @@ use git::{
use crate::get_host_from_git_remote_url;
#[derive(Debug, Deserialize)]
struct CommitDetails {
author_email: String,
}
#[derive(Debug, Deserialize)]
struct AvatarInfo {
avatar_url: String,
}
#[derive(Debug)]
pub struct Gitlab {
name: String,
@@ -61,79 +46,8 @@ impl Gitlab {
Url::parse(&format!("https://{}", host))?,
))
}
async fn fetch_gitlab_commit_author(
&self,
repo_owner: &str,
repo: &str,
commit: &str,
client: &Arc<dyn HttpClient>,
) -> Result<Option<AvatarInfo>> {
let Some(host) = self.base_url.host_str() else {
bail!("failed to get host from gitlab base url");
};
let project_path = format!("{}/{}", repo_owner, repo);
let project_path_encoded = urlencoding::encode(&project_path);
let url = format!(
"https://{host}/api/v4/projects/{project_path_encoded}/repository/commits/{commit}"
);
let request = Request::get(&url)
.header("Content-Type", "application/json")
.follow_redirects(http_client::RedirectPolicy::FollowAll);
let mut response = client
.send(request.body(AsyncBody::default())?)
.await
.with_context(|| format!("error fetching GitLab commit details at {:?}", url))?;
let mut body = Vec::new();
response.body_mut().read_to_end(&mut body).await?;
if response.status().is_client_error() {
let text = String::from_utf8_lossy(body.as_slice());
bail!(
"status error {}, response: {text:?}",
response.status().as_u16()
);
}
let body_str = std::str::from_utf8(&body)?;
let author_email = serde_json::from_str::<CommitDetails>(body_str)
.map(|commit| commit.author_email)
.context("failed to deserialize GitLab commit details")?;
let avatar_info_url = format!("https://{host}/api/v4/avatar?email={author_email}");
let request = Request::get(&avatar_info_url)
.header("Content-Type", "application/json")
.follow_redirects(http_client::RedirectPolicy::FollowAll);
let mut response = client
.send(request.body(AsyncBody::default())?)
.await
.with_context(|| format!("error fetching GitLab avatar info at {:?}", url))?;
let mut body = Vec::new();
response.body_mut().read_to_end(&mut body).await?;
if response.status().is_client_error() {
let text = String::from_utf8_lossy(body.as_slice());
bail!(
"status error {}, response: {text:?}",
response.status().as_u16()
);
}
let body_str = std::str::from_utf8(&body)?;
serde_json::from_str::<Option<AvatarInfo>>(body_str)
.context("failed to deserialize GitLab avatar info")
}
}
#[async_trait]
impl GitHostingProvider for Gitlab {
fn name(&self) -> String {
self.name.clone()
@@ -144,7 +58,7 @@ impl GitHostingProvider for Gitlab {
}
fn supports_avatars(&self) -> bool {
true
false
}
fn format_line_number(&self, line: u32) -> String {
@@ -208,39 +122,6 @@ impl GitHostingProvider for Gitlab {
);
permalink
}
async fn commit_author_avatar_url(
&self,
repo_owner: &str,
repo: &str,
commit: SharedString,
http_client: Arc<dyn HttpClient>,
) -> Result<Option<Url>> {
let commit = commit.to_string();
let avatar_url = self
.fetch_gitlab_commit_author(repo_owner, repo, &commit, &http_client)
.await?
.map(|author| -> Result<Url, url::ParseError> {
let mut url = Url::parse(&author.avatar_url)?;
if let Some(host) = url.host_str() {
let size_query = if host.contains("gravatar") || host.contains("libravatar") {
Some("s=128")
} else if self
.base_url
.host_str()
.is_some_and(|base_host| host.contains(base_host))
{
Some("width=128")
} else {
None
};
url.set_query(size_query);
}
Ok(url)
})
.transpose()?;
Ok(avatar_url)
}
}
#[cfg(test)]
@@ -253,8 +134,8 @@ mod tests {
#[test]
fn test_invalid_self_hosted_remote_url() {
let remote_url = "https://gitlab.com/zed-industries/zed.git";
let gitlab = Gitlab::from_remote_url(remote_url);
assert!(gitlab.is_err());
let github = Gitlab::from_remote_url(remote_url);
assert!(github.is_err());
}
#[test]

View File

@@ -45,8 +45,6 @@ notifications.workspace = true
panel.workspace = true
picker.workspace = true
project.workspace = true
recent_projects.workspace = true
remote.workspace = true
schemars.workspace = true
serde.workspace = true
serde_json.workspace = true

View File

@@ -58,8 +58,8 @@ use std::{collections::HashSet, sync::Arc, time::Duration, usize};
use strum::{IntoEnumIterator, VariantNames};
use time::OffsetDateTime;
use ui::{
ButtonLike, Checkbox, CommonAnimationExt, ContextMenu, ElevationIndex, PopoverMenu, ScrollAxes,
Scrollbars, SplitButton, Tooltip, WithScrollbar, prelude::*,
Checkbox, CommonAnimationExt, ContextMenu, ElevationIndex, IconPosition, Label, LabelSize,
PopoverMenu, ScrollAxes, Scrollbars, SplitButton, Tooltip, WithScrollbar, prelude::*,
};
use util::paths::PathStyle;
use util::{ResultExt, TryFutureExt, maybe};
@@ -286,12 +286,6 @@ struct PendingOperation {
op_id: usize,
}
impl PendingOperation {
fn contains_path(&self, path: &RepoPath) -> bool {
self.entries.iter().any(|p| &p.repo_path == path)
}
}
pub struct GitPanel {
pub(crate) active_repository: Option<Entity<Repository>>,
pub(crate) commit_editor: Entity<Editor>,
@@ -1246,21 +1240,19 @@ impl GitPanel {
};
let (stage, repo_paths) = match entry {
GitListEntry::Status(status_entry) => {
let repo_paths = vec![status_entry.clone()];
let stage = if let Some(status) = self.entry_staging(&status_entry) {
!status.is_fully_staged()
} else if status_entry.status.staging().is_fully_staged() {
if status_entry.status.staging().is_fully_staged() {
if let Some(op) = self.bulk_staging.clone()
&& op.anchor == status_entry.repo_path
{
self.bulk_staging = None;
}
false
(false, vec![status_entry.clone()])
} else {
self.set_bulk_staging_anchor(status_entry.repo_path.clone(), cx);
true
};
(stage, repo_paths)
(true, vec![status_entry.clone()])
}
}
GitListEntry::Header(section) => {
let goal_staged_state = !self.header_state(section.header).selected();
@@ -2219,7 +2211,7 @@ impl GitPanel {
.detach();
}
pub(crate) fn pull(&mut self, rebase: bool, window: &mut Window, cx: &mut Context<Self>) {
pub(crate) fn pull(&mut self, window: &mut Window, cx: &mut Context<Self>) {
if !self.can_push_and_pull(cx) {
return;
}
@@ -2254,7 +2246,6 @@ impl GitPanel {
repo.pull(
branch.name().to_owned().into(),
remote.name.clone(),
rebase,
askpass,
cx,
)
@@ -2686,7 +2677,10 @@ impl GitPanel {
if self.pending.iter().any(|pending| {
pending.target_status == TargetStatus::Reverted
&& !pending.finished
&& pending.contains_path(&entry.repo_path)
&& pending
.entries
.iter()
.any(|pending| pending.repo_path == entry.repo_path)
}) {
continue;
}
@@ -2737,7 +2731,10 @@ impl GitPanel {
last_pending_staged = pending.entries.first().cloned();
}
if let Some(single_staged) = &single_staged_entry
&& pending.contains_path(&single_staged.repo_path)
&& pending
.entries
.iter()
.any(|entry| entry.repo_path == single_staged.repo_path)
{
pending_status_for_single_staged = Some(pending.target_status);
}
@@ -2800,7 +2797,7 @@ impl GitPanel {
&& let Some(index) = bulk_staging_anchor_new_index
&& let Some(entry) = self.entries.get(index)
&& let Some(entry) = entry.status_entry()
&& self.entry_staging(entry).unwrap_or(entry.staging) == StageStatus::Staged
&& self.entry_staging(entry) == StageStatus::Staged
{
self.bulk_staging = bulk_staging;
}
@@ -2848,47 +2845,39 @@ impl GitPanel {
self.entry_count += 1;
if repo.had_conflict_on_last_merge_head_change(&status_entry.repo_path) {
self.conflicted_count += 1;
if self
.entry_staging(status_entry)
.unwrap_or(status_entry.staging)
.has_staged()
{
if self.entry_staging(status_entry).has_staged() {
self.conflicted_staged_count += 1;
}
} else if status_entry.status.is_created() {
self.new_count += 1;
if self
.entry_staging(status_entry)
.unwrap_or(status_entry.staging)
.has_staged()
{
if self.entry_staging(status_entry).has_staged() {
self.new_staged_count += 1;
}
} else {
self.tracked_count += 1;
if self
.entry_staging(status_entry)
.unwrap_or(status_entry.staging)
.has_staged()
{
if self.entry_staging(status_entry).has_staged() {
self.tracked_staged_count += 1;
}
}
}
}
fn entry_staging(&self, entry: &GitStatusEntry) -> Option<StageStatus> {
fn entry_staging(&self, entry: &GitStatusEntry) -> StageStatus {
for pending in self.pending.iter().rev() {
if pending.contains_path(&entry.repo_path) {
if pending
.entries
.iter()
.any(|pending_entry| pending_entry.repo_path == entry.repo_path)
{
match pending.target_status {
TargetStatus::Staged => return Some(StageStatus::Staged),
TargetStatus::Unstaged => return Some(StageStatus::Unstaged),
TargetStatus::Staged => return StageStatus::Staged,
TargetStatus::Unstaged => return StageStatus::Unstaged,
TargetStatus::Reverted => continue,
TargetStatus::Unchanged => continue,
}
}
}
None
entry.staging
}
pub(crate) fn has_staged_changes(&self) -> bool {
@@ -3506,12 +3495,6 @@ impl GitPanel {
let amend = self.amend_pending();
let signoff = self.signoff_enabled;
let label_color = if self.pending_commit.is_some() {
Color::Disabled
} else {
Color::Default
};
div()
.id("commit-wrapper")
.on_hover(cx.listener(move |this, hovered, _, cx| {
@@ -3520,15 +3503,14 @@ impl GitPanel {
cx.notify()
}))
.child(SplitButton::new(
ButtonLike::new_rounded_left(ElementId::Name(
ui::ButtonLike::new_rounded_left(ElementId::Name(
format!("split-button-left-{}", title).into(),
))
.layer(ElevationIndex::ModalSurface)
.size(ButtonSize::Compact)
.layer(ui::ElevationIndex::ModalSurface)
.size(ui::ButtonSize::Compact)
.child(
Label::new(title)
.size(LabelSize::Small)
.color(label_color)
div()
.child(Label::new(title).size(LabelSize::Small))
.mr_0p5(),
)
.on_click({
@@ -3728,8 +3710,7 @@ impl GitPanel {
let ix = self.entry_by_path(&repo_path, cx)?;
let entry = self.entries.get(ix)?;
let status = entry.status_entry()?;
let entry_staging = self.entry_staging(status).unwrap_or(status.staging);
let entry_staging = self.entry_staging(entry.status_entry()?);
let checkbox = Checkbox::new("stage-file", entry_staging.as_bool().into())
.disabled(!self.has_write_access(cx))
@@ -4023,8 +4004,8 @@ impl GitPanel {
let checkbox_id: ElementId =
ElementId::Name(format!("entry_{}_{}_checkbox", display_name, ix).into());
let entry_staging = self.entry_staging(entry).unwrap_or(entry.staging);
let mut is_staged: ToggleState = entry_staging.as_bool().into();
let entry_staging = self.entry_staging(entry);
let mut is_staged: ToggleState = self.entry_staging(entry).as_bool().into();
if self.show_placeholders && !self.has_staged_changes() && !entry.status.is_created() {
is_staged = ToggleState::Selected;
}

View File

@@ -46,7 +46,6 @@ pub(crate) mod remote_output;
pub mod repository_selector;
pub mod stash_picker;
pub mod text_diff_view;
pub mod worktree_picker;
actions!(
git,
@@ -73,7 +72,6 @@ pub fn init(cx: &mut App) {
git_panel::register(workspace);
repository_selector::register(workspace);
branch_picker::register(workspace);
worktree_picker::register(workspace);
stash_picker::register(workspace);
let project = workspace.project().read(cx);
@@ -126,15 +124,7 @@ pub fn init(cx: &mut App) {
return;
};
panel.update(cx, |panel, cx| {
panel.pull(false, window, cx);
});
});
workspace.register_action(|workspace, _: &git::PullRebase, window, cx| {
let Some(panel) = workspace.panel::<git_panel::GitPanel>(cx) else {
return;
};
panel.update(cx, |panel, cx| {
panel.pull(true, window, cx);
panel.pull(window, cx);
});
});
}
@@ -605,7 +595,6 @@ mod remote_button {
.action("Fetch", git::Fetch.boxed_clone())
.action("Fetch From", git::FetchFrom.boxed_clone())
.action("Pull", git::Pull.boxed_clone())
.action("Pull (Rebase)", git::PullRebase.boxed_clone())
.separator()
.action("Push", git::Push.boxed_clone())
.action("Push To", git::PushTo.boxed_clone())

View File

@@ -5,14 +5,16 @@ use git::stash::StashEntry;
use gpui::{
Action, AnyElement, App, Context, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable,
InteractiveElement, IntoElement, Modifiers, ModifiersChangedEvent, ParentElement, Render,
SharedString, Styled, Subscription, Task, WeakEntity, Window, actions, rems,
SharedString, Styled, Subscription, Task, WeakEntity, Window, actions, rems, svg,
};
use picker::{Picker, PickerDelegate};
use project::git_store::{Repository, RepositoryEvent};
use std::sync::Arc;
use time::{OffsetDateTime, UtcOffset};
use time_format;
use ui::{HighlightedLabel, KeyBinding, ListItem, ListItemSpacing, Tooltip, prelude::*};
use ui::{
ButtonLike, HighlightedLabel, KeyBinding, ListItem, ListItemSpacing, Tooltip, prelude::*,
};
use util::ResultExt;
use workspace::notifications::DetachAndPromptErr;
use workspace::{ModalView, Workspace};
@@ -432,7 +434,7 @@ impl PickerDelegate for StashListDelegate {
ix: usize,
selected: bool,
_window: &mut Window,
_cx: &mut Context<Picker<Self>>,
cx: &mut Context<Picker<Self>>,
) -> Option<Self::ListItem> {
let entry_match = &self.matches[ix];
@@ -444,14 +446,23 @@ impl PickerDelegate for StashListDelegate {
.into_any_element();
let branch_name = entry_match.entry.branch.clone().unwrap_or_default();
let branch_info = h_flex()
let branch_label = h_flex()
.gap_1p5()
.w_full()
.child(
Label::new(branch_name)
.truncate()
.color(Color::Muted)
.size(LabelSize::Small),
h_flex()
.gap_0p5()
.child(
Icon::new(IconName::GitBranch)
.color(Color::Muted)
.size(IconSize::Small),
)
.child(
Label::new(branch_name)
.truncate()
.color(Color::Muted)
.size(LabelSize::Small),
),
)
.child(
Label::new("")
@@ -465,12 +476,42 @@ impl PickerDelegate for StashListDelegate {
.size(LabelSize::Small),
);
let show_button = div()
.group("show-button-hover")
.child(
ButtonLike::new("show-button")
.child(
svg()
.size(IconSize::Medium.rems())
.flex_none()
.path(IconName::Eye.path())
.text_color(Color::Default.color(cx))
.group_hover("show-button-hover", |this| {
this.text_color(Color::Accent.color(cx))
})
.hover(|this| this.text_color(Color::Accent.color(cx))),
)
.tooltip(Tooltip::for_action_title("Show Stash", &ShowStashItem))
.on_click(cx.listener(move |picker, _, window, cx| {
cx.stop_propagation();
picker.delegate.show_stash_at(ix, window, cx);
})),
)
.into_any_element();
Some(
ListItem::new(SharedString::from(format!("stash-{ix}")))
.inset(true)
.spacing(ListItemSpacing::Sparse)
.toggle_state(selected)
.child(v_flex().w_full().child(stash_label).child(branch_info))
.end_slot(show_button)
.child(
v_flex()
.w_full()
.overflow_hidden()
.child(stash_label)
.child(branch_label.into_element()),
)
.tooltip(Tooltip::text(format!(
"stash@{{{}}}",
entry_match.entry.index
@@ -493,6 +534,26 @@ impl PickerDelegate for StashListDelegate {
.justify_end()
.border_t_1()
.border_color(cx.theme().colors().border_variant)
.child(
Button::new("apply-stash", "Apply")
.key_binding(
KeyBinding::for_action_in(&menu::Confirm, &focus_handle, cx)
.map(|kb| kb.size(rems_from_px(12.))),
)
.on_click(|_, window, cx| {
window.dispatch_action(menu::Confirm.boxed_clone(), cx)
}),
)
.child(
Button::new("pop-stash", "Pop")
.key_binding(
KeyBinding::for_action_in(&menu::SecondaryConfirm, &focus_handle, cx)
.map(|kb| kb.size(rems_from_px(12.))),
)
.on_click(|_, window, cx| {
window.dispatch_action(menu::SecondaryConfirm.boxed_clone(), cx)
}),
)
.child(
Button::new("drop-stash", "Drop")
.key_binding(
@@ -507,42 +568,6 @@ impl PickerDelegate for StashListDelegate {
window.dispatch_action(stash_picker::DropStashItem.boxed_clone(), cx)
}),
)
.child(
Button::new("view-stash", "View")
.key_binding(
KeyBinding::for_action_in(
&stash_picker::ShowStashItem,
&focus_handle,
cx,
)
.map(|kb| kb.size(rems_from_px(12.))),
)
.on_click(cx.listener(move |picker, _, window, cx| {
cx.stop_propagation();
let selected_ix = picker.delegate.selected_index();
picker.delegate.show_stash_at(selected_ix, window, cx);
})),
)
.child(
Button::new("pop-stash", "Pop")
.key_binding(
KeyBinding::for_action_in(&menu::SecondaryConfirm, &focus_handle, cx)
.map(|kb| kb.size(rems_from_px(12.))),
)
.on_click(|_, window, cx| {
window.dispatch_action(menu::SecondaryConfirm.boxed_clone(), cx)
}),
)
.child(
Button::new("apply-stash", "Apply")
.key_binding(
KeyBinding::for_action_in(&menu::Confirm, &focus_handle, cx)
.map(|kb| kb.size(rems_from_px(12.))),
)
.on_click(|_, window, cx| {
window.dispatch_action(menu::Confirm.boxed_clone(), cx)
}),
)
.into_any(),
)
}

View File

@@ -1,743 +0,0 @@
use anyhow::Context as _;
use fuzzy::StringMatchCandidate;
use git::repository::Worktree as GitWorktree;
use gpui::{
Action, App, AsyncApp, Context, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable,
InteractiveElement, IntoElement, Modifiers, ModifiersChangedEvent, ParentElement,
PathPromptOptions, Render, SharedString, Styled, Subscription, Task, WeakEntity, Window,
actions, rems,
};
use picker::{Picker, PickerDelegate, PickerEditorPosition};
use project::{DirectoryLister, git_store::Repository};
use recent_projects::{RemoteConnectionModal, connect};
use remote::{RemoteConnectionOptions, remote_client::ConnectionIdentifier};
use std::{path::PathBuf, sync::Arc};
use ui::{HighlightedLabel, KeyBinding, ListItem, ListItemSpacing, Tooltip, prelude::*};
use util::ResultExt;
use workspace::{ModalView, Workspace, notifications::DetachAndPromptErr};
actions!(git, [WorktreeFromDefault, WorktreeFromDefaultOnWindow]);
pub fn register(workspace: &mut Workspace) {
workspace.register_action(open);
}
pub fn open(
workspace: &mut Workspace,
_: &zed_actions::git::Worktree,
window: &mut Window,
cx: &mut Context<Workspace>,
) {
let repository = workspace.project().read(cx).active_repository(cx);
let workspace_handle = workspace.weak_handle();
workspace.toggle_modal(window, cx, |window, cx| {
WorktreeList::new(repository, workspace_handle, rems(34.), window, cx)
})
}
pub struct WorktreeList {
width: Rems,
pub picker: Entity<Picker<WorktreeListDelegate>>,
picker_focus_handle: FocusHandle,
_subscription: Subscription,
}
impl WorktreeList {
fn new(
repository: Option<Entity<Repository>>,
workspace: WeakEntity<Workspace>,
width: Rems,
window: &mut Window,
cx: &mut Context<Self>,
) -> Self {
let all_worktrees_request = repository
.clone()
.map(|repository| repository.update(cx, |repository, _| repository.worktrees()));
let default_branch_request = repository
.clone()
.map(|repository| repository.update(cx, |repository, _| repository.default_branch()));
cx.spawn_in(window, async move |this, cx| {
let all_worktrees = all_worktrees_request
.context("No active repository")?
.await??;
let default_branch = default_branch_request
.context("No active repository")?
.await
.map(Result::ok)
.ok()
.flatten()
.flatten();
this.update_in(cx, |this, window, cx| {
this.picker.update(cx, |picker, cx| {
picker.delegate.all_worktrees = Some(all_worktrees);
picker.delegate.default_branch = default_branch;
picker.refresh(window, cx);
})
})?;
anyhow::Ok(())
})
.detach_and_log_err(cx);
let delegate = WorktreeListDelegate::new(workspace, repository, window, cx);
let picker = cx.new(|cx| Picker::uniform_list(delegate, window, cx));
let picker_focus_handle = picker.focus_handle(cx);
picker.update(cx, |picker, _| {
picker.delegate.focus_handle = picker_focus_handle.clone();
});
let _subscription = cx.subscribe(&picker, |_, _, _, cx| {
cx.emit(DismissEvent);
});
Self {
picker,
picker_focus_handle,
width,
_subscription,
}
}
fn handle_modifiers_changed(
&mut self,
ev: &ModifiersChangedEvent,
_: &mut Window,
cx: &mut Context<Self>,
) {
self.picker
.update(cx, |picker, _| picker.delegate.modifiers = ev.modifiers)
}
fn handle_new_worktree(
&mut self,
replace_current_window: bool,
window: &mut Window,
cx: &mut Context<Self>,
) {
self.picker.update(cx, |picker, cx| {
let ix = picker.delegate.selected_index();
let Some(entry) = picker.delegate.matches.get(ix) else {
return;
};
let Some(default_branch) = picker.delegate.default_branch.clone() else {
return;
};
if !entry.is_new {
return;
}
picker.delegate.create_worktree(
entry.worktree.branch(),
replace_current_window,
Some(default_branch.into()),
window,
cx,
);
})
}
}
impl ModalView for WorktreeList {}
impl EventEmitter<DismissEvent> for WorktreeList {}
impl Focusable for WorktreeList {
fn focus_handle(&self, _: &App) -> FocusHandle {
self.picker_focus_handle.clone()
}
}
impl Render for WorktreeList {
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
v_flex()
.key_context("GitWorktreeSelector")
.w(self.width)
.on_modifiers_changed(cx.listener(Self::handle_modifiers_changed))
.on_action(cx.listener(|this, _: &WorktreeFromDefault, w, cx| {
this.handle_new_worktree(false, w, cx)
}))
.on_action(cx.listener(|this, _: &WorktreeFromDefaultOnWindow, w, cx| {
this.handle_new_worktree(true, w, cx)
}))
.child(self.picker.clone())
.on_mouse_down_out({
cx.listener(move |this, _, window, cx| {
this.picker.update(cx, |this, cx| {
this.cancel(&Default::default(), window, cx);
})
})
})
}
}
#[derive(Debug, Clone)]
struct WorktreeEntry {
worktree: GitWorktree,
positions: Vec<usize>,
is_new: bool,
}
pub struct WorktreeListDelegate {
matches: Vec<WorktreeEntry>,
all_worktrees: Option<Vec<GitWorktree>>,
workspace: WeakEntity<Workspace>,
repo: Option<Entity<Repository>>,
selected_index: usize,
last_query: String,
modifiers: Modifiers,
focus_handle: FocusHandle,
default_branch: Option<SharedString>,
}
impl WorktreeListDelegate {
fn new(
workspace: WeakEntity<Workspace>,
repo: Option<Entity<Repository>>,
_window: &mut Window,
cx: &mut Context<WorktreeList>,
) -> Self {
Self {
matches: vec![],
all_worktrees: None,
workspace,
selected_index: 0,
repo,
last_query: Default::default(),
modifiers: Default::default(),
focus_handle: cx.focus_handle(),
default_branch: None,
}
}
fn create_worktree(
&self,
worktree_branch: &str,
replace_current_window: bool,
commit: Option<String>,
window: &mut Window,
cx: &mut Context<Picker<Self>>,
) {
let workspace = self.workspace.clone();
let Some(repo) = self.repo.clone() else {
return;
};
let worktree_path = self
.workspace
.clone()
.update(cx, |this, cx| {
this.prompt_for_open_path(
PathPromptOptions {
files: false,
directories: true,
multiple: false,
prompt: Some("Select directory for new worktree".into()),
},
DirectoryLister::Project(this.project().clone()),
window,
cx,
)
})
.log_err();
let Some(worktree_path) = worktree_path else {
return;
};
let branch = worktree_branch.to_string();
let window_handle = window.window_handle();
cx.spawn_in(window, async move |_, cx| {
let Some(paths) = worktree_path.await? else {
return anyhow::Ok(());
};
let path = paths.get(0).cloned().context("No path selected")?;
repo.update(cx, |repo, _| {
repo.create_worktree(branch.clone(), path.clone(), commit)
})?
.await??;
let final_path = path.join(branch);
let (connection_options, app_state, is_local) =
workspace.update(cx, |workspace, cx| {
let project = workspace.project().clone();
let connection_options = project.read(cx).remote_connection_options(cx);
let app_state = workspace.app_state().clone();
let is_local = project.read(cx).is_local();
(connection_options, app_state, is_local)
})?;
if is_local {
workspace
.update_in(cx, |workspace, window, cx| {
workspace.open_workspace_for_paths(
replace_current_window,
vec![final_path],
window,
cx,
)
})?
.await?;
} else if let Some(connection_options) = connection_options {
open_remote_worktree(
connection_options,
vec![final_path],
app_state,
window_handle,
replace_current_window,
cx,
)
.await?;
}
anyhow::Ok(())
})
.detach_and_prompt_err("Failed to create worktree", window, cx, |e, _, _| {
Some(e.to_string())
});
}
fn open_worktree(
&self,
worktree_path: &PathBuf,
replace_current_window: bool,
window: &mut Window,
cx: &mut Context<Picker<Self>>,
) {
let workspace = self.workspace.clone();
let path = worktree_path.clone();
let Some((connection_options, app_state, is_local)) = workspace
.update(cx, |workspace, cx| {
let project = workspace.project().clone();
let connection_options = project.read(cx).remote_connection_options(cx);
let app_state = workspace.app_state().clone();
let is_local = project.read(cx).is_local();
(connection_options, app_state, is_local)
})
.log_err()
else {
return;
};
if is_local {
let open_task = workspace.update(cx, |workspace, cx| {
workspace.open_workspace_for_paths(replace_current_window, vec![path], window, cx)
});
cx.spawn(async move |_, _| {
open_task?.await?;
anyhow::Ok(())
})
.detach_and_prompt_err(
"Failed to open worktree",
window,
cx,
|e, _, _| Some(e.to_string()),
);
} else if let Some(connection_options) = connection_options {
let window_handle = window.window_handle();
cx.spawn_in(window, async move |_, cx| {
open_remote_worktree(
connection_options,
vec![path],
app_state,
window_handle,
replace_current_window,
cx,
)
.await
})
.detach_and_prompt_err(
"Failed to open worktree",
window,
cx,
|e, _, _| Some(e.to_string()),
);
}
cx.emit(DismissEvent);
}
fn base_branch<'a>(&'a self, cx: &'a mut Context<Picker<Self>>) -> Option<&'a str> {
self.repo
.as_ref()
.and_then(|repo| repo.read(cx).branch.as_ref().map(|b| b.name()))
}
}
async fn open_remote_worktree(
connection_options: RemoteConnectionOptions,
paths: Vec<PathBuf>,
app_state: Arc<workspace::AppState>,
window: gpui::AnyWindowHandle,
replace_current_window: bool,
cx: &mut AsyncApp,
) -> anyhow::Result<()> {
let workspace_window = window
.downcast::<Workspace>()
.ok_or_else(|| anyhow::anyhow!("Window is not a Workspace window"))?;
let connect_task = workspace_window.update(cx, |workspace, window, cx| {
workspace.toggle_modal(window, cx, |window, cx| {
RemoteConnectionModal::new(&connection_options, Vec::new(), window, cx)
});
let prompt = workspace
.active_modal::<RemoteConnectionModal>(cx)
.expect("Modal just created")
.read(cx)
.prompt
.clone();
connect(
ConnectionIdentifier::setup(),
connection_options.clone(),
prompt,
window,
cx,
)
.prompt_err("Failed to connect", window, cx, |_, _, _| None)
})?;
let session = connect_task.await;
workspace_window.update(cx, |workspace, _window, cx| {
if let Some(prompt) = workspace.active_modal::<RemoteConnectionModal>(cx) {
prompt.update(cx, |prompt, cx| prompt.finished(cx))
}
})?;
let Some(Some(session)) = session else {
return Ok(());
};
let new_project = cx.update(|cx| {
project::Project::remote(
session,
app_state.client.clone(),
app_state.node_runtime.clone(),
app_state.user_store.clone(),
app_state.languages.clone(),
app_state.fs.clone(),
cx,
)
})?;
let window_to_use = if replace_current_window {
workspace_window
} else {
let workspace_position = cx
.update(|cx| {
workspace::remote_workspace_position_from_db(connection_options.clone(), &paths, cx)
})?
.await
.context("fetching workspace position from db")?;
let mut options =
cx.update(|cx| (app_state.build_window_options)(workspace_position.display, cx))?;
options.window_bounds = workspace_position.window_bounds;
cx.open_window(options, |window, cx| {
cx.new(|cx| {
let mut workspace =
Workspace::new(None, new_project.clone(), app_state.clone(), window, cx);
workspace.centered_layout = workspace_position.centered_layout;
workspace
})
})?
};
workspace::open_remote_project_with_existing_connection(
connection_options,
new_project,
paths,
app_state,
window_to_use,
cx,
)
.await?;
Ok(())
}
impl PickerDelegate for WorktreeListDelegate {
type ListItem = ListItem;
fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> {
"Select worktree…".into()
}
fn editor_position(&self) -> PickerEditorPosition {
PickerEditorPosition::Start
}
fn match_count(&self) -> usize {
self.matches.len()
}
fn selected_index(&self) -> usize {
self.selected_index
}
fn set_selected_index(
&mut self,
ix: usize,
_window: &mut Window,
_: &mut Context<Picker<Self>>,
) {
self.selected_index = ix;
}
fn update_matches(
&mut self,
query: String,
window: &mut Window,
cx: &mut Context<Picker<Self>>,
) -> Task<()> {
let Some(all_worktrees) = self.all_worktrees.clone() else {
return Task::ready(());
};
cx.spawn_in(window, async move |picker, cx| {
let mut matches: Vec<WorktreeEntry> = if query.is_empty() {
all_worktrees
.into_iter()
.map(|worktree| WorktreeEntry {
worktree,
positions: Vec::new(),
is_new: false,
})
.collect()
} else {
let candidates = all_worktrees
.iter()
.enumerate()
.map(|(ix, worktree)| StringMatchCandidate::new(ix, worktree.branch()))
.collect::<Vec<StringMatchCandidate>>();
fuzzy::match_strings(
&candidates,
&query,
true,
true,
10000,
&Default::default(),
cx.background_executor().clone(),
)
.await
.into_iter()
.map(|candidate| WorktreeEntry {
worktree: all_worktrees[candidate.candidate_id].clone(),
positions: candidate.positions,
is_new: false,
})
.collect()
};
picker
.update(cx, |picker, _| {
if !query.is_empty()
&& !matches
.first()
.is_some_and(|entry| entry.worktree.branch() == query)
{
let query = query.replace(' ', "-");
matches.push(WorktreeEntry {
worktree: GitWorktree {
path: Default::default(),
ref_name: format!("refs/heads/{query}").into(),
sha: Default::default(),
},
positions: Vec::new(),
is_new: true,
})
}
let delegate = &mut picker.delegate;
delegate.matches = matches;
if delegate.matches.is_empty() {
delegate.selected_index = 0;
} else {
delegate.selected_index =
core::cmp::min(delegate.selected_index, delegate.matches.len() - 1);
}
delegate.last_query = query;
})
.log_err();
})
}
fn confirm(&mut self, secondary: bool, window: &mut Window, cx: &mut Context<Picker<Self>>) {
let Some(entry) = self.matches.get(self.selected_index()) else {
return;
};
if entry.is_new {
self.create_worktree(&entry.worktree.branch(), secondary, None, window, cx);
} else {
self.open_worktree(&entry.worktree.path, secondary, window, cx);
}
cx.emit(DismissEvent);
}
fn dismissed(&mut self, _: &mut Window, cx: &mut Context<Picker<Self>>) {
cx.emit(DismissEvent);
}
fn render_match(
&self,
ix: usize,
selected: bool,
_window: &mut Window,
cx: &mut Context<Picker<Self>>,
) -> Option<Self::ListItem> {
let entry = &self.matches.get(ix)?;
let path = entry.worktree.path.to_string_lossy().to_string();
let sha = entry
.worktree
.sha
.clone()
.chars()
.take(7)
.collect::<String>();
let focus_handle = self.focus_handle.clone();
let icon = if let Some(default_branch) = self.default_branch.clone()
&& entry.is_new
{
Some(
IconButton::new("worktree-from-default", IconName::GitBranchAlt)
.on_click(|_, window, cx| {
window.dispatch_action(WorktreeFromDefault.boxed_clone(), cx)
})
.on_right_click(|_, window, cx| {
window.dispatch_action(WorktreeFromDefaultOnWindow.boxed_clone(), cx)
})
.tooltip(move |_, cx| {
Tooltip::for_action_in(
format!("From default branch {default_branch}"),
&WorktreeFromDefault,
&focus_handle,
cx,
)
}),
)
} else {
None
};
let branch_name = if entry.is_new {
h_flex()
.gap_1()
.child(
Icon::new(IconName::Plus)
.size(IconSize::Small)
.color(Color::Muted),
)
.child(
Label::new(format!("Create worktree \"{}\"", entry.worktree.branch()))
.single_line()
.truncate(),
)
.into_any_element()
} else {
h_flex()
.gap_1()
.child(
Icon::new(IconName::GitBranch)
.size(IconSize::Small)
.color(Color::Muted),
)
.child(HighlightedLabel::new(
entry.worktree.branch().to_owned(),
entry.positions.clone(),
))
.truncate()
.into_any_element()
};
let sublabel = if entry.is_new {
format!(
"based off {}",
self.base_branch(cx).unwrap_or("the current branch")
)
} else {
format!("at {}", path)
};
Some(
ListItem::new(SharedString::from(format!("worktree-menu-{ix}")))
.inset(true)
.spacing(ListItemSpacing::Sparse)
.toggle_state(selected)
.child(
v_flex()
.w_full()
.overflow_hidden()
.child(
h_flex()
.gap_6()
.justify_between()
.overflow_x_hidden()
.child(branch_name)
.when(!entry.is_new, |el| {
el.child(
Label::new(sha)
.size(LabelSize::Small)
.color(Color::Muted)
.into_element(),
)
}),
)
.child(
div().max_w_96().child(
Label::new(sublabel)
.size(LabelSize::Small)
.color(Color::Muted)
.truncate()
.into_any_element(),
),
),
)
.end_slot::<IconButton>(icon),
)
}
fn no_matches_text(&self, _window: &mut Window, _cx: &mut App) -> Option<SharedString> {
Some("No worktrees found".into())
}
fn render_footer(&self, _: &mut Window, cx: &mut Context<Picker<Self>>) -> Option<AnyElement> {
let focus_handle = self.focus_handle.clone();
Some(
h_flex()
.w_full()
.p_1p5()
.gap_0p5()
.justify_end()
.border_t_1()
.border_color(cx.theme().colors().border_variant)
.child(
Button::new("open-in-new-window", "Open in new window")
.key_binding(
KeyBinding::for_action_in(&menu::Confirm, &focus_handle, cx)
.map(|kb| kb.size(rems_from_px(12.))),
)
.on_click(|_, window, cx| {
window.dispatch_action(menu::Confirm.boxed_clone(), cx)
}),
)
.child(
Button::new("open-in-window", "Open")
.key_binding(
KeyBinding::for_action_in(&menu::SecondaryConfirm, &focus_handle, cx)
.map(|kb| kb.size(rems_from_px(12.))),
)
.on_click(|_, window, cx| {
window.dispatch_action(menu::SecondaryConfirm.boxed_clone(), cx)
}),
)
.into_any(),
)
}
}

View File

@@ -1,4 +1,4 @@
use editor::{Editor, EditorEvent, MultiBufferSnapshot};
use editor::{Editor, MultiBufferSnapshot};
use gpui::{App, Entity, FocusHandle, Focusable, Styled, Subscription, Task, WeakEntity};
use settings::Settings;
use std::{fmt::Write, num::NonZeroU32, time::Duration};
@@ -81,7 +81,7 @@ impl CursorPosition {
fn update_position(
&mut self,
editor: &Entity<Editor>,
editor: Entity<Editor>,
debounce: Option<Duration>,
window: &mut Window,
cx: &mut Context<Self>,
@@ -269,21 +269,19 @@ impl StatusItemView for CursorPosition {
cx: &mut Context<Self>,
) {
if let Some(editor) = active_pane_item.and_then(|item| item.act_as::<Editor>(cx)) {
self._observe_active_editor = Some(cx.subscribe_in(
&editor,
window,
|cursor_position, editor, event, window, cx| match event {
EditorEvent::SelectionsChanged { .. } => Self::update_position(
cursor_position,
editor,
Some(UPDATE_DEBOUNCE),
window,
cx,
),
_ => {}
},
));
self.update_position(&editor, None, window, cx);
self._observe_active_editor =
Some(
cx.observe_in(&editor, window, |cursor_position, editor, window, cx| {
Self::update_position(
cursor_position,
editor,
Some(UPDATE_DEBOUNCE),
window,
cx,
)
}),
);
self.update_position(editor, None, window, cx);
} else {
self.position = None;
self._observe_active_editor = None;

View File

@@ -16,7 +16,7 @@ use text::{Bias, Point};
use theme::ActiveTheme;
use ui::prelude::*;
use util::paths::FILE_ROW_COLUMN_DELIMITER;
use workspace::{DismissDecision, ModalView};
use workspace::ModalView;
pub fn init(cx: &mut App) {
LineIndicatorFormat::register(cx);
@@ -31,16 +31,7 @@ pub struct GoToLine {
_subscriptions: Vec<Subscription>,
}
impl ModalView for GoToLine {
fn on_before_dismiss(
&mut self,
_window: &mut Window,
_cx: &mut Context<Self>,
) -> DismissDecision {
self.prev_scroll_position.take();
DismissDecision::Dismiss(true)
}
}
impl ModalView for GoToLine {}
impl Focusable for GoToLine {
fn focus_handle(&self, cx: &App) -> FocusHandle {
@@ -778,171 +769,4 @@ mod tests {
state
})
}
#[gpui::test]
async fn test_scroll_position_on_outside_click(cx: &mut TestAppContext) {
init_test(cx);
let fs = FakeFs::new(cx.executor());
let file_content = (0..100)
.map(|i| format!("struct Line{};", i))
.collect::<Vec<_>>()
.join("\n");
fs.insert_tree(path!("/dir"), json!({"a.rs": file_content}))
.await;
let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
let (workspace, cx) =
cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
let worktree_id = workspace.update(cx, |workspace, cx| {
workspace.project().update(cx, |project, cx| {
project.worktrees(cx).next().unwrap().read(cx).id()
})
});
let _buffer = project
.update(cx, |project, cx| {
project.open_local_buffer(path!("/dir/a.rs"), cx)
})
.await
.unwrap();
let editor = workspace
.update_in(cx, |workspace, window, cx| {
workspace.open_path((worktree_id, rel_path("a.rs")), None, true, window, cx)
})
.await
.unwrap()
.downcast::<Editor>()
.unwrap();
let go_to_line_view = open_go_to_line_view(&workspace, cx);
let scroll_position_before_input =
editor.update(cx, |editor, cx| editor.scroll_position(cx));
cx.simulate_input("47");
let scroll_position_after_input =
editor.update(cx, |editor, cx| editor.scroll_position(cx));
assert_ne!(scroll_position_before_input, scroll_position_after_input);
drop(go_to_line_view);
workspace.update_in(cx, |workspace, window, cx| {
workspace.hide_modal(window, cx);
});
cx.run_until_parked();
let scroll_position_after_auto_dismiss =
editor.update(cx, |editor, cx| editor.scroll_position(cx));
assert_eq!(
scroll_position_after_auto_dismiss, scroll_position_after_input,
"Dismissing via outside click should maintain new scroll position"
);
}
#[gpui::test]
async fn test_scroll_position_on_cancel(cx: &mut TestAppContext) {
init_test(cx);
let fs = FakeFs::new(cx.executor());
let file_content = (0..100)
.map(|i| format!("struct Line{};", i))
.collect::<Vec<_>>()
.join("\n");
fs.insert_tree(path!("/dir"), json!({"a.rs": file_content}))
.await;
let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
let (workspace, cx) =
cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
let worktree_id = workspace.update(cx, |workspace, cx| {
workspace.project().update(cx, |project, cx| {
project.worktrees(cx).next().unwrap().read(cx).id()
})
});
let _buffer = project
.update(cx, |project, cx| {
project.open_local_buffer(path!("/dir/a.rs"), cx)
})
.await
.unwrap();
let editor = workspace
.update_in(cx, |workspace, window, cx| {
workspace.open_path((worktree_id, rel_path("a.rs")), None, true, window, cx)
})
.await
.unwrap()
.downcast::<Editor>()
.unwrap();
let go_to_line_view = open_go_to_line_view(&workspace, cx);
let scroll_position_before_input =
editor.update(cx, |editor, cx| editor.scroll_position(cx));
cx.simulate_input("47");
let scroll_position_after_input =
editor.update(cx, |editor, cx| editor.scroll_position(cx));
assert_ne!(scroll_position_before_input, scroll_position_after_input);
cx.dispatch_action(menu::Cancel);
drop(go_to_line_view);
cx.run_until_parked();
let scroll_position_after_cancel =
editor.update(cx, |editor, cx| editor.scroll_position(cx));
assert_eq!(
scroll_position_after_cancel, scroll_position_after_input,
"Cancel should maintain new scroll position"
);
}
#[gpui::test]
async fn test_scroll_position_on_confirm(cx: &mut TestAppContext) {
init_test(cx);
let fs = FakeFs::new(cx.executor());
let file_content = (0..100)
.map(|i| format!("struct Line{};", i))
.collect::<Vec<_>>()
.join("\n");
fs.insert_tree(path!("/dir"), json!({"a.rs": file_content}))
.await;
let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
let (workspace, cx) =
cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
let worktree_id = workspace.update(cx, |workspace, cx| {
workspace.project().update(cx, |project, cx| {
project.worktrees(cx).next().unwrap().read(cx).id()
})
});
let _buffer = project
.update(cx, |project, cx| {
project.open_local_buffer(path!("/dir/a.rs"), cx)
})
.await
.unwrap();
let editor = workspace
.update_in(cx, |workspace, window, cx| {
workspace.open_path((worktree_id, rel_path("a.rs")), None, true, window, cx)
})
.await
.unwrap()
.downcast::<Editor>()
.unwrap();
let go_to_line_view = open_go_to_line_view(&workspace, cx);
let scroll_position_before_input =
editor.update(cx, |editor, cx| editor.scroll_position(cx));
cx.simulate_input("47");
let scroll_position_after_input =
editor.update(cx, |editor, cx| editor.scroll_position(cx));
assert_ne!(scroll_position_before_input, scroll_position_after_input);
cx.dispatch_action(menu::Confirm);
drop(go_to_line_view);
cx.run_until_parked();
let scroll_position_after_confirm =
editor.update(cx, |editor, cx| editor.scroll_position(cx));
assert_eq!(
scroll_position_after_confirm, scroll_position_after_input,
"Confirm should maintain new scroll position"
);
}
}

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