Compare commits

...

69 Commits

Author SHA1 Message Date
Ben Brandt
d11bc14451 collab: set thresholds per subscription, not per item 2025-05-23 11:57:07 +02:00
Ben Brandt
508ccde363 Better messaging for accounts that are too young (#31212)
Right now you find this out the first time you try and submit a
completion.

These changes communicate much earlier to the user what the issue is
with their account and what they can do about it.

Release Notes:

- N/A

---------

Co-authored-by: Antonio Scandurra <me@as-cii.com>
2025-05-23 09:32:03 +00:00
Bo Lopker
9f7987c532 Change default diagnostics_max_severity to 'hint' (#31229)
Closes https://github.com/blopker/codebook/issues/79

Recently, the setting `diagnostics_max_severity` was changed from `null`
to `warning`in this PR: https://github.com/zed-industries/zed/pull/30316
This change has caused the various spell checking extensions to not work
as expected by default, most of which use the `hint` diagnostic. This
goes against user expectations when installing one of these extensions.

Without `hint` as the default, extension authors will either need to
change the diagnostic levels, or instruct users to add
`diagnostics_max_severity` to their settings as an additional step,
neither of which is a great user experience.

This PR sets the default `hint`, which is closer to the original
behavior before the aforementioned PR.

Release Notes:

- Changed `diagnostics_max_severity` to `hint` instead of `warning` by
default

---------

Co-authored-by: Kirill Bulatov <kirill@zed.dev>
2025-05-23 07:30:38 +00:00
Marshall Bowers
cb52acbf3d eval: Don't read the model from the user settings (#31230)
This PR fixes an issue where the eval was incorrectly pulling the
provider/model from the user settings, which could cause problems when
running certain evals.

Was introduced in #30168 due to the restructuring after the removal of
the `assistant` crate.

Release Notes:

- N/A
2025-05-23 00:21:35 +00:00
Marshall Bowers
f8b997b25c docs: Fix Claude Sonnet 4 model name (#31226)
This PR fixes the model name for Claude Sonnet 4 to match Anthropic's
new ordering.

Release Notes:

- N/A
2025-05-22 22:05:55 +00:00
morgankrey
73a5856fb8 docs: Add Claude 4 Sonnet to docs (#31225)
Release Notes:

- N/A

---------

Co-authored-by: Marshall Bowers <git@maxdeviant.com>
2025-05-22 22:02:37 +00:00
Shardul Vaidya
e3b6fa2c30 bedrock: Support Claude 4 models (#31214)
Release Notes:

- AWS Bedrock: Added support for Claude 4.
2025-05-22 21:59:23 +00:00
Marshall Bowers
ceb5164114 agent: Remove last turn after a refusal (#31220)
This is a follow-up to https://github.com/zed-industries/zed/pull/31217
that removes the last turn after we get a `refusal` stop reason, as
advised by the Anthropic docs.

Meant to include it in that PR, but accidentally merged it before
pushing these changes 🤦🏻‍♂️.

Release Notes:

- N/A
2025-05-22 21:38:33 +00:00
Umesh Yadav
24a108d876 anthropic: Fix Claude 4 model display names to match official order (#31218)
Release Notes:

- N/A
2025-05-22 21:00:54 +00:00
Marshall Bowers
5c0b161563 Handle new refusal stop reason from Claude 4 models (#31217)
This PR adds support for handling the new [`refusal` stop
reason](https://docs.anthropic.com/en/docs/test-and-evaluate/strengthen-guardrails/handle-streaming-refusals)
from Claude 4 models.

<img width="409" alt="Screenshot 2025-05-22 at 4 31 56 PM"
src="https://github.com/user-attachments/assets/707b04f5-5a52-4a19-95d9-cbd2be2dd86f"
/>

Release Notes:

- Added handling for `"stop_reason": "refusal"` from Claude 4 models.
2025-05-22 16:56:59 -04:00
Cole Miller
ad4645c59b debugger: Fix environment variables not being substituted in debug tasks (#31198)
Release Notes:

- Debugger Beta: Fixed a bug where environment variables were not
substituted in debug tasks in some cases.

Co-authored-by: Anthony Eid <hello@anthonyeid.me>
Co-authored-by: Remco Smits <djsmits12@gmail.com>
2025-05-22 15:14:05 -04:00
Marshall Bowers
37047a6fde language_models: Update default/recommended Anthropic models to Claude Sonnet 4 (#31209)
This PR updates the default/recommended models for the Anthropic and Zed
providers to be Claude Sonnet 4.

Release Notes:

- Updated default/recommended Anthropic models to Claude Sonnet 4.
2025-05-22 19:10:08 +00:00
Marshall Bowers
fc78408ee4 language_model: Allow Max Mode for Claude 4 models (#31207)
This PR adds the Claude 4 models to the list of models that support Max
Mode.

Release Notes:

- Added Max Mode support for Claude 4 models.
2025-05-22 18:50:30 +00:00
Marshall Bowers
37f49ce304 collab: Add support for overage billing for Claude Sonnet 4 (#31206)
This PR adds support for billing for overages for Claude Sonnet 4.

Release Notes:

- N/A
2025-05-22 18:40:06 +00:00
Umesh Yadav
cc428330a9 mistral: Add DevstralSmallLatest model to Mistral and Ollama (#31099)
Mistral just released a sota coding model:
https://mistral.ai/news/devstral

This PR adds support for it in both ollama and mistral

Release Notes:

- Add DevstralSmallLatest model to Mistral and Ollama
2025-05-22 14:22:35 -04:00
Marshall Bowers
1475ace6f1 anthropic: Add support for Claude 4 (#31203)
This PR adds support for [Claude
4](https://www.anthropic.com/news/claude-4).

Release Notes:

- Added support for Claude Opus 4 and Claude Sonnet 4.

---------

Co-authored-by: Antonio Scandurra <me@as-cii.com>
Co-authored-by: Richard Feldman <oss@rtfeldman.com>
2025-05-22 18:09:35 +00:00
smit
dd4e8b9e66 editor: Fix block comment incorrectly continues to next line in some cases (#31204)
Closes #31138

Fix edge case where adding newline if there is text afterwards end
delimiter of multiline comment, would continue the comment prefix. This
is fixed by checking for end delimiter on whole line instead of just
assuming it would always be at end.

- [x] Tests

Release Notes:

- Fixed the issue where in some cases the block comment continues to the
next line even though the comment block is already closed.
2025-05-22 23:27:03 +05:30
Joseph T. Lyons
b188e5d3aa Fix update status logic to preserve previous status (#31202)
Release Notes:

- N/A
2025-05-22 17:34:14 +00:00
Anthony Eid
e3d3daec92 Fix bug where deleted toolchains stay selected on startup (#30562)
This affects python's when debugging because the selected toolchain is
used as the python binary to spawn Debugpy

Release Notes:

- Fix bug where selected toolchain didn't exist
2025-05-22 20:18:33 +03:00
Anthony Eid
ced8e4d88e debugger beta: Move path resolution to resolve scenario instead of just in new session modal (#31185)
This move was done so debug configs could use path resolution, and
saving a configuration from the new session modal wouldn't resolve paths
beforehand.

I also added an integration test to make sure path resolution happens
from an arbitrary config. The test was placed under the new session
modal directory because it has to do with starting a session, and that's
what the new session modal typically does, even if it's implicitly used
in the test.

In the future, I plan to add more tests to the new session modal too.

Release Notes:

- debugger beta: Allow configs from debug.json to resolve paths
2025-05-22 16:59:59 +00:00
Piotr Osiewicz
fa1abd8201 debugger: Always focus the active session whenever it is stopped (#31182)
Closes #ISSUE

Release Notes:

- debugger: Fixed child debug sessions taking precedence over the
parents when spawned.
2025-05-22 15:23:31 +00:00
张小白
ee4e43f1b6 linux: Fix wrong keys are reported when using German layout (#31193)
Part of #31174

Because the keyboard layout parameter wasn’t set correctly, characters
don’t show up properly when using the German layout at launch.

To reproduce:
Switch to the German layout, launch Zed, and press the `7` key. it
should output `7`, but instead it outputs `è`.


Release Notes:

- N/A
2025-05-22 14:47:23 +00:00
Anthony Eid
d61e1e24a7 docs: Fix debugger docs link from summary page (#31195)
Release Notes:

- N/A
2025-05-22 14:39:10 +00:00
Piotr Osiewicz
3c03d53e3e debugger: Use integrated terminal for Python (#31190)
Closes #ISSUE

Release Notes:

- debugger: Use integrated terminal for Python, allowing one to interact
with standard input/output when debugging Python projects.
2025-05-22 14:34:10 +00:00
Anthony Eid
8ab664a52c debugger beta: Update debugger docs for beta (#31192)
The docs include basic information on starting a session but will need
to be further iterated upon once we get deeper into the beta

Release Notes:

- N/A
2025-05-22 10:21:48 -04:00
Finn Evers
2044426634 gpui: Improve displayed keybinds shown in macOS application menus (#28440)
Closes #28164

This PR adresses inproper keybinds being shown in MacOS application
menus. The issue arises because the keybinds shown in MacOS application
menus are unaware of keybind contexts (they are only ever updated [on a
keymap-change](6d1dd109f5/crates/zed/src/zed.rs (L1421))).
Thus, using the keybind that was added last in the keymap can result in
incorrect keybindings being shown quite frequently, as they might belong
to a different context not generally available (applies the same for the
default keymap as well as for user-keymaps).

For example, the linked issue arises because the keybind found last in
the iterator is
6d1dd109f5/assets/keymaps/vim.json (L759),
which is not even available in most contexts (and, additionally, the `e`
of `escape` is rendered here as a keybind which seems to be a seperate
issue).

Additionally, this would result in inconsistent behavior with some
Vim-keybinds. A vim-keybind would be used only when available but
otherwise the default binding would be shown (see `Undo` and `Redo` as
an example below), which seems inconsistent.

This PR fixes this by instead using the first keybind found in keymaps,
which is expected to be the keybind available in most contexts.
Additionally, this allows rendering some more keybinds for actions which
vim-keybind cannot be displayed (Find In Project for example) .This
seems to be more reasonable until [this related
comment](6d1dd109f5/crates/gpui/src/keymap.rs (L199-L204))
is resolved.

This includes a revert of #25878 as well. With this change, the change
made in #25878 becomes obsolete and would also regress the behavior back
to the state prior to that PR.

|  | `main` | This PR |
| --- | --- | --- |
| Edit-menu | <img width="220" alt="main_edit"
src="https://github.com/user-attachments/assets/9f793b64-80b6-4a5b-b7e5-628f0d552166"
/> | <img width="220" alt="PR_edit"
src="https://github.com/user-attachments/assets/bccb444c-7a49-41d5-9377-d90b1639a3ed"
/> |
| View-menu | <img width="214" alt="main_view"
src="https://github.com/user-attachments/assets/0e6a6632-df02-4883-9f5a-facb4d0263b5"
/> | <img width="214" alt="PR_view"
src="https://github.com/user-attachments/assets/14600ece-fcaa-447a-94ef-4fa350eca49c"
/> |


Release Notes:

- Improved keybinds displayed for actions in MacOS application menus.
2025-05-22 09:51:51 -04:00
Joseph T. Lyons
02fa6f6fc2 Surface version to install in update status tooltip (#31179)
Release Notes:

- Surfaced the version that will be installed, in a tooltip, when
hovering on the `Click to restart and update Zed` status.
2025-05-22 12:37:18 +00:00
Anthony Eid
80a00cd241 debugger beta: Fix panic that could occur when parsing an invalid dap schema (#31175)
Release Notes:

- N/A
2025-05-22 07:25:07 -04:00
Anthony Eid
06f725d51b debugger beta: Fix dap_schema for DAP extensions (#31173)
We now actually call dap_schema provided by extensions instead of
defaulting to a null `serde_json::Value`. We still need to update the
Json LSP whenever a new dap is installed.

Release Notes:

- N/A
2025-05-22 07:24:46 -04:00
Julia Ryan
baf6d82cd4 Handle ~ in debugger launch modal (#31087)
@Anthony-Eid I'm pretty sure this maintains the behavior of #30680, and
I added some tests to be sure.

Release Notes:

- `~` now expands to the home directory in the debugger launch modal.

---------

Co-authored-by: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com>
2025-05-22 11:20:00 +00:00
Piotr Osiewicz
28ec7fbb81 debugger: Add telemetry for new session experience (#31171)
This includes the following data:
- Where we spawned the session from (gutter, scenario list, custom form
filled by the user)
- Which debug adapter was used
- Which dock the debugger is in

Closes #ISSUE

Release Notes:

- debugger: Added telemetry for new session experience that includes
data about:
    - How a session was spawned (gutter, scenario list or custom form)
    - Which debug adapter was used
    - Which dock the debugger is in

---------

Co-authored-by: Joseph T. Lyons <JosephTLyons@gmail.com>
2025-05-22 11:15:33 +00:00
Remco Smits
0415e853d5 debugger: Use current worktree directory when spawning an adapter (#31054)
/cc @osiewicz 

I think bringing this back should fix **bloveless** his issue with go
debugger.
This is also nice, so people are not forced to give us a working
directory, because most adapters will use their **cwd** as the project
root directory. For JavaScript, you don't need to specify the **cwd**
anymore because it can already infer it

Release Notes:

- debugger beta: Fixed some adapters fail to determine the right root level of the
debug program.
2025-05-22 06:47:47 -04:00
Anthony Eid
1c9b818342 debugger: Use DAP schema to configure daps (#30833)
This PR allows DAPs to define their own schema so users can see
completion items when editing their debug.json files.

Users facing this aren’t the biggest chance, but behind the scenes, this
affected a lot of code because we manually translated common fields from
Zed's config format to be adapter-specific. Now we store the raw JSON
from a user's configuration file and just send that.

I'm ignoring the Protobuf CICD error because the DebugTaskDefinition
message is not yet user facing and we need to deprecate some fields in
it.

Release Notes:

- debugger beta: Show completion items when editing debug.json
- debugger beta: Breaking change, debug.json schema now relays on what
DAP you have selected instead of always having the same based values.

---------

Co-authored-by: Remco Smits <djsmits12@gmail.com>
Co-authored-by: Cole Miller <m@cole-miller.net>
Co-authored-by: Cole Miller <cole@zed.dev>
2025-05-22 05:48:26 -04:00
Ben Kunkle
0d7f4842f3 Restore scroll after undo edit prediction (#31162)
Closes #29652

Release Notes:

- Fixed an issue where the scroll and cursor position would not be
restored after undoing an inline completion
2025-05-22 09:16:11 +00:00
Oleksiy Syvokon
ab017129d8 agent: Improve Gemini support in the edit_file tool (#31116)
This change improves `eval_extract_handle_command_output` results for
all models:

Model                       | Pass rate before | Pass rate after
----------------------------|------------------|----------------
claude-3.7-sonnet           |  0.96            | 0.98
gemini-2.5-pro              |  0.35            | 0.86
gpt-4.1                     |  0.81            | 1.00

Part of this improvement comes from more robust evaluation, which now
accepts multiple possible outcomes. Another part is from the prompt
adaptation: addressing common Gemini failure modes, adding a few-shot
example, and, in the final commit, auto-rewriting instructions for
clarity and conciseness.

This change still needs validation from larger end-to-end evals.


Release Notes:

- N/A
2025-05-22 12:01:43 +03:00
Cole Miller
71fb17c507 debugger: Update the default layout (#31057)
- Remove the modules list and loaded sources list from the default
layout
- Move the console to the center pane so it's visible initially

Release Notes:

- Debugger Beta: changed the default layout of the debugger panel,
hiding the modules list and loaded sources list by default and making
the console more prominent.

---------

Co-authored-by: Remco Smits <djsmits12@gmail.com>
2025-05-22 04:32:44 +00:00
Cole Miller
97e437c632 Remove test-support feature from auto_update's gpui dep (#31147)
Fixes `cargo run` on main.

Release Notes:

- N/A
2025-05-22 03:32:29 +00:00
Jon Gretar Borgthorsson
66667d1eef Add kernel detection for language support of runnable markdown cells (#29664)
Closes #27757

Release Notes:

- List of runnable markdown cells is now based on detected jupyter
kernels instead of hardcoded to Python and TypeScript
2025-05-21 20:23:05 -07:00
Remco Smits
dce22a965e project search: Reduce clones and allocations (#31133)
Release Notes:

- N/A
2025-05-21 22:11:00 -04:00
Cole Miller
5f452dbca2 debugger: Add a couple more keybindings (#31103)
- Add missing handler for `debugger::Continue` so `f5` works
- Add bindings based on VS Code for `debugger::Restart` and
`debug_panel::ToggleFocus`
- Remove breakpoint-related buttons from the debug panel's top strip,
and surface the bindings for `editor::ToggleBreakpoint` in gutter
tooltip instead

Release Notes:

- Debugger Beta: Added keybindings for `debugger::Continue`,
`debugger::Restart`, and `debug_panel::ToggleFocus`.
- Debugger Beta: Removed breakpoint-related buttons from the top of the
debug panel.
- Compatibility note: on Linux, `ctrl-shift-d` is now bound to
`debug_panel::ToggleFocus` by default, instead of
`editor::DuplicateLineDown`.
2025-05-22 00:59:44 +00:00
Cole Miller
b2a92097ee debugger: Add actions and keybindings for opening the thread and session menus (#31135)
Makes it possible to open and navigate these menus from the keyboard.

I also removed the eager previewing behavior for the thread picker,
which was buggy and came with a jarring layout shift.

Release Notes:

- Debugger Beta: Added the `debugger: open thread picker` and `debugger:
open session picker` actions.
2025-05-21 20:56:39 -04:00
Marshall Bowers
eb35d25a7d collab: Drop billing_events table (#31131)
This PR drops the `billing_events` table, as we're no longer using it.

Release Notes:

- N/A
2025-05-21 22:43:46 +00:00
smit
8742d4ab90 editor: Fix regression causing incorrect delimiter on newline in case of multiple comment prefixes (#31129)
Closes #31115

This fixes regression caused by
https://github.com/zed-industries/zed/pull/30824 while keeping that fix.

- [x] Test

Release Notes:

- Fixed the issue where adding a newline after the `///` comment would
extend it with `//` instead of `///` in Rust and other similar
languages.
2025-05-22 03:56:20 +05:30
Marshall Bowers
b829f72c17 collab: Prefer the plan on the subscription over the one on the usage (#31127)
This PR makes it so we always prefer the plan on the subscription.

The plan stored on the subscription usage is informational only.

Release Notes:

- N/A
2025-05-21 22:00:19 +00:00
Marshall Bowers
ffa8310d04 collab: Drop monthly_usages and lifetime_usages tables (#31124)
This PR drops the `monthly_usages` and `lifetime_usages` tables from the
LLM database, as they are no longer used.

Release Notes:

- N/A
2025-05-21 21:55:48 +00:00
Joseph T. Lyons
3fda539c46 Allow updater to check for updates after downloading one (#31066)
This PR brings back https://github.com/zed-industries/zed/pull/30969 and
adds some initial testing.

https://github.com/zed-industries/zed/pull/30969 did indeed allow Zed to
continue doing downloads after downloading one, but it introduced a bug
where Zed would download a new binary every time it polled, even if the
version was the same as the running instance.

This code could use a refactor to allow more / better testing, but this
is a start.

Release Notes:

- N/A
2025-05-21 17:54:46 -04:00
Marshall Bowers
b444b326cb collab: Remove GET /billing/monthly_spend endpoint (#31123)
This PR removes the `GET /billing/monthly_spend` endpoint, as it is no
longer used.

Release Notes:

- N/A
2025-05-21 21:30:12 +00:00
Peter Tripp
f196288e2d docs: Fix broken link in ai/configuration (#31119)
Release Notes:

- N/A
2025-05-21 20:37:12 +00:00
Rob McBroom
e30cc131b4 Rename 'Quit' to 'Quit Zed' in macOS menu (#31109)
This is standard for Mac apps.

I should have included this with [my other
PR](https://github.com/zed-industries/zed/pull/30697), but didn’t catch
it. 🤦🏻‍♂️

Release Notes:

- N/A
2025-05-21 20:01:08 +00:00
morgankrey
09c8a84935 docs: Link to models supported directly from table (#31112)
Closes #ISSUE

Release Notes:

- N/A

---------

Co-authored-by: Marshall Bowers <git@maxdeviant.com>
2025-05-21 15:30:09 -04:00
Kirill Bulatov
6e5996a815 Fix unzipping clangd and codelldb on Windows (#31080)
Closes https://github.com/zed-industries/zed/pull/30454

Release Notes:

- N/A
2025-05-21 21:17:14 +03:00
Marshall Bowers
c8f56e38b1 Update Cargo.lock (#31105)
This PR updates the `Cargo.lock` file, as running `cargo check` was
producing a diff on `main`.

Release Notes:

- N/A
2025-05-21 17:32:23 +00:00
Max Brunsfeld
cfd3b0ff7b Meter edit predictions by acceptance in free plan (#30984)
TODO:

- [x] Release  a new version of `zed_llm_client`

Release Notes:

- N/A

---------

Co-authored-by: Mikayla Maki <mikayla.c.maki@gmail.com>
Co-authored-by: Antonio Scandurra <me@as-cii.com>
Co-authored-by: Ben Brandt <benjamin.j.brandt@gmail.com>
Co-authored-by: Marshall Bowers <git@maxdeviant.com>
2025-05-21 17:11:42 +00:00
hrou0003
afe23cf85a Canonicalize markdown link paths (#29119)
Closes #28657

Release Notes:

- Fixed markdown preview not canonicalizing file paths
2025-05-21 12:57:51 -04:00
Umesh Yadav
f915c24279 copilot: Fix rate limit due to Copilot-Vision-Request header (#30989)
Issues: #30994

I've implemented an important optimisation in response to GitHub
Copilot's recent rate limit on concurrent Vision API calls. Previously,
our system was defaulting to vision header: true for all API calls. To
prevent unnecessary calls and adhere to the new limits, I've updated our
logic: the vision header is now only sent if the current message is a
vision message, specifically when the preceding message includes an
image.

Prompt used to reproduce and verify the fix: `Give me a context for my
agent crate about. Browse my repo.`

Release Notes:

- copilot: Set Copilot-Vision-Request header based on message content
2025-05-21 12:51:35 -04:00
Joseph T. Lyons
bdd9e015ab Bump Zed to v0.189 (#31101)
Release Notes:

-N/A
2025-05-21 15:38:20 +00:00
smit
6bbab4b55a editor: Fix multi-cursor not added to lines shorter than current cursor column (#31100)
Closes #5255, #1046, #28322, #15728

This PR makes `AddSelectionBelow` and `AddSelectionAbove` not skip lines
that are shorter than the current cursor column. This follows the same
behavior as VSCode and Sublime.

This change is only applicable in the case of an empty selection; if
there is a non-empty selection, it continues to skip empty and shorter
lines to create a Vim-like column selection, which is the better default
for that case.

- [x] Tests

The empty selection no longer skips shorter lines:


https://github.com/user-attachments/assets/4bde2357-20b6-44f2-a9d9-b595c12d3939

Non-empty selection continues to skip shorter lines.


https://github.com/user-attachments/assets/4cd47c9f-b698-40fc-ad50-f2bf64f5519b

Release Notes:

- Improved `AddSelectionBelow` and `AddSelectionAbove` to no longer skip
shorter lines when the selection is empty, aligning with VSCode and
Sublime behavior.
2025-05-21 21:06:33 +05:30
smit
7450b788f3 editor: Prevent overlapping of signature/hover popovers and context menus (#31090)
Closes #29358

If hover popovers or signature popovers ever clash with the context menu
(like code completion or code actions), they find the best spot by
trying different directions around the context menu to show the popover.
If they can’t find a good spot, they just overlap with the context menu.

Not overlapping state:
<img width="350" alt="image"
src="https://github.com/user-attachments/assets/2f1bdc4c-eb01-405c-b5fb-eb28eadc9957"
/>

Overlapping case, moves popover to bottom of context menu:
<img width="350" alt="image"
src="https://github.com/user-attachments/assets/3ce4be23-7701-4711-b604-5e29682360e1"
/>

Overlapping case, moves popover to right of context menu:
<img width="350" alt="image"
src="https://github.com/user-attachments/assets/60d47518-e412-4d64-9d17-a69a17248bdf"
/> <img width="350" alt="image"
src="https://github.com/user-attachments/assets/2a3de176-7443-46d8-99d1-b2973a0ffaa6"
/>

Overlapping case, moves popover to left of context menu:
<img width="350" alt="image"
src="https://github.com/user-attachments/assets/015b799b-8c6e-4405-aee6-e205d4caebec"
/>

Overlapping case, moves popover to top of context menu:
<img width="350" alt="image"
src="https://github.com/user-attachments/assets/fbd03d84-9a49-44eb-846b-a9852d2ff43e"
/>

Release Notes:

- Fixed an issue where hover popovers or signature popovers would
overlap with existing opened completion or code actions context menus.
2025-05-21 18:45:00 +05:30
Anthony Eid
0c03519393 Fix project search panic (#31089)
The panic occurred when querying a second search in the project search
multibuffer while there were dirty buffers.

The panic only happened in Nightly so there's no release notes 

Release Notes:

- N/A
2025-05-21 12:42:20 +00:00
Joseph T. Lyons
636eff2e9a Revert "Allow updater to check for updates after downloading one (#30969)" (#31086)
This reverts commit 5c4f9e57d8.

Release Notes:

- N/A
2025-05-21 12:37:03 +00:00
Julia Ryan
6c8f4002d9 nix: Prevent spurious bindgen rebuilds in the devshell (#31083)
Release Notes:

- N/A
2025-05-21 11:18:14 +00:00
Oleksiy Syvokon
91bc5aefa4 evals: Add system prompt to edit agent evals + fix edit agent (#31082)
1. Add system prompt: this is how it's called from threads. Previously,
we were sending
2. Fix an issue with writing agent thought into a newly created empty
file.

Release Notes:

- N/A

---------

Co-authored-by: Ben Brandt <benjamin.j.brandt@gmail.com>
Co-authored-by: Antonio Scandurra <me@as-cii.com>
2025-05-21 10:14:58 +00:00
Aleksei Gusev
2f3564b85f Add icons to the built-in picker for Open (#30893)
![image](https://github.com/user-attachments/assets/f1167251-627f-48f7-a948-25c06c842e4b)


Release Notes:

- Added icons to the built-in picker for `Open` dialog
2025-05-21 13:07:22 +03:00
Aleksei Gusev
d61a544400 Fix Replace Next Match command (#30890)
Currently, `search::ReplaceNext` works only first time it is executed
because Zed switches the focus to the editor. It seems
`self.editor_focus` call is unnecessary.

Closes #17466

Release Notes:

- Fixed `Replace Next Match` command. Previously it worked once, then
Zed incorrectly switched the focus to the editor


https://github.com/user-attachments/assets/66ef61d6-1efe-43ca-8d8c-6b40540a9930
2025-05-21 13:05:44 +03:00
Adam Sherwood
8061bacee3 Add excluded_files to pane::DeploySearch (#30699)
In accordance with #30327, I saw no reason for included files to get
special treatment, and I actually get use out of prefilling excluded
files because I like not to search symlinked files which, in my
workflow, use a naming convention.

This is simply implementing the same exact changes, but for excluded. It
was tested with `"space /": ["pane::DeploySearch", { "excluded_files":
"**/_*.tf" }]` and works just fine.

Release Notes:

- Added `excluded_files` to `pane::DeploySearch`.
2025-05-21 13:03:39 +03:00
Piotr Osiewicz
77dadfedfe chore: Make terminal_view own the TerminalSlashCommand (#31070)
This reduces 'touch crates/editor/src/editor.rs && cargo +nightly build'
from 8.9s to 8.5s. That same scenario used to take 8s less than a week
ago. :)
I'm measuring with nightly rustc, because it's compile times are better
than those of stable thanks to
https://github.com/rust-lang/rust/pull/138522

main (8.2s total):

![image](https://github.com/user-attachments/assets/767a2ac4-7bba-4147-bd16-9b09eed5b433)

[cargo-timing.html.zip](https://github.com/user-attachments/files/20364175/cargo-timing.html.zip)

#22be776 (7.5s total):

[cargo-timing-20250521T085303.892834Z.html.zip](https://github.com/user-attachments/files/20364391/cargo-timing-20250521T085303.892834Z.html.zip)

![image](https://github.com/user-attachments/assets/c4476df9-cb6e-4403-b0db-de00521f1fd0)


Release Notes:

- N/A
2025-05-21 09:27:54 +00:00
Ben Brandt
0023b37bfc extension_host: fix missing debug adapters (#31069)
Missed because of lack of rebase

Release Notes:

- N/A
2025-05-21 09:01:18 +00:00
Ben Brandt
4ece4a635f extension_host: Use wasmtime incremental compilation (#30948)
Builds on top of https://github.com/zed-industries/zed/pull/30942

This turns on incremental compilation and decreases extension
compilation times by up to another 41%
Putting us at roughly 92% improved extension load times from what is in
the app today.

Because we only have a static engine, I can't reset the cache between
every run. So technically the benchmarks are always running with a
warmed cache. So the first extension we load will take the 8.8ms, and
then any subsequent extensions will be closer to the measured time in
this benchmark.

This is also measuring the entire load process, not just the
compilation. However, since this is the loading we likely think of when
thinking about extensions, I felt it was likely more helpful to see the
impact on the overall time.

This works because our extensions are largely the same Wasm bytecode
(SDK code + std lib functions etc) with minor changes in the trait impl.
The more different that extensions implementation is, there will be less
benefit, however, there will always be a large part of every extension
that is always the same across extensions, so this should be a speedup
regardless.

I used `moka` to provide a bound to the cache. We could use a bare
`DashMap`, however if there was some issue this could lead to a memory
leak. `moka` has some slight overhead, but makes sure that we don't go
over 32mb while using an LRU-style mechanism for deciding which
compilation artifacts to keep.

I measured our current extensions to take roughly 512kb in the cache.
Which means with a cap of 32mb, we can keep roughly 64 *completely
novel* extensions with no overlap. Since our extensions will have more
overlap than this though, we can actually keep much more in the cache
without having to worry about it.

#### Before:

```
load/1                  time:   [8.8301 ms 8.8616 ms 8.8931 ms]
                        change: [-0.1880% +0.3221% +0.8679%] (p = 0.23 > 0.05)
                        No change in performance detected.
```

#### After:

```
load/1                  time:   [5.1575 ms 5.1726 ms 5.1876 ms]
                        change: [-41.894% -41.628% -41.350%] (p = 0.00 < 0.05)
                        Performance has improved.
```

Release Notes:

- N/A
2025-05-21 10:12:16 +02:00
Jonathan LEI
77c2aecf93 Fix socks proxy local DNS resolution not respected (#30619)
Closes #30618

Release Notes:

- Fixed SOCKS proxy incorrectly always uses remote DNS resolution.
2025-05-21 14:55:39 +08:00
167 changed files with 5516 additions and 2379 deletions

View File

@@ -2,13 +2,13 @@
{
"label": "Debug Zed (CodeLLDB)",
"adapter": "CodeLLDB",
"program": "target/debug/zed",
"program": "$ZED_WORKTREE_ROOT/target/debug/zed",
"request": "launch"
},
{
"label": "Debug Zed (GDB)",
"adapter": "GDB",
"program": "target/debug/zed",
"program": "$ZED_WORKTREE_ROOT/target/debug/zed",
"request": "launch",
"initialize_args": {
"stopAtBeginningOfMainSubprogram": true

143
Cargo.lock generated
View File

@@ -86,7 +86,6 @@ dependencies = [
"jsonschema",
"language",
"language_model",
"language_model_selector",
"log",
"lsp",
"markdown",
@@ -492,6 +491,7 @@ dependencies = [
"collections",
"context_server",
"editor",
"feature_flags",
"fs",
"futures 0.3.31",
"fuzzy",
@@ -499,17 +499,18 @@ dependencies = [
"indexed_docs",
"language",
"language_model",
"language_model_selector",
"languages",
"log",
"multi_buffer",
"open_ai",
"ordered-float 2.10.1",
"parking_lot",
"paths",
"picker",
"pretty_assertions",
"project",
"prompt_store",
"proto",
"rand 0.8.5",
"regex",
"rope",
@@ -611,7 +612,6 @@ dependencies = [
"serde_json",
"settings",
"smol",
"terminal_view",
"text",
"toml 0.8.20",
"ui",
@@ -688,6 +688,7 @@ dependencies = [
"portable-pty",
"pretty_assertions",
"project",
"prompt_store",
"rand 0.8.5",
"regex",
"reqwest_client",
@@ -3001,6 +3002,7 @@ dependencies = [
"context_server",
"ctor",
"dap",
"dap_adapters",
"dashmap 6.1.0",
"debugger_ui",
"derive_more",
@@ -3636,9 +3638,12 @@ dependencies = [
"gimli",
"hashbrown 0.14.5",
"log",
"postcard",
"regalloc2",
"rustc-hash 2.1.1",
"serde",
"serde_derive",
"sha2",
"smallvec",
"target-lexicon 0.13.2",
]
@@ -4025,6 +4030,7 @@ dependencies = [
"smallvec",
"smol",
"task",
"telemetry",
"util",
"workspace-hack",
]
@@ -4048,10 +4054,12 @@ dependencies = [
"dap",
"futures 0.3.31",
"gpui",
"json_dotpath",
"language",
"paths",
"serde",
"serde_json",
"smol",
"task",
"util",
"workspace-hack",
@@ -4175,6 +4183,8 @@ dependencies = [
"dap",
"extension",
"gpui",
"serde_json",
"task",
"workspace-hack",
]
@@ -4397,6 +4407,15 @@ version = "0.1.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8"
[[package]]
name = "diffy"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b545b8c50194bdd008283985ab0b31dba153cfd5b3066a92770634fbc0d7d291"
dependencies = [
"nu-ansi-term 0.50.1",
]
[[package]]
name = "digest"
version = "0.10.7"
@@ -4661,6 +4680,7 @@ dependencies = [
"command_palette_hooks",
"convert_case 0.8.0",
"ctor",
"dap",
"db",
"emojis",
"env_logger 0.11.8",
@@ -5016,6 +5036,7 @@ dependencies = [
"shellexpand 2.1.2",
"smol",
"telemetry",
"terminal_view",
"toml 0.8.20",
"unindent",
"util",
@@ -5154,6 +5175,7 @@ dependencies = [
"language_extension",
"log",
"lsp",
"moka",
"node_runtime",
"parking_lot",
"paths",
@@ -5910,6 +5932,20 @@ dependencies = [
"thread_local",
]
[[package]]
name = "generator"
version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d18470a76cb7f8ff746cf1f7470914f900252ec36bbc40b569d74b1258446827"
dependencies = [
"cc",
"cfg-if",
"libc",
"log",
"rustversion",
"windows 0.61.1",
]
[[package]]
name = "generic-array"
version = "0.14.7"
@@ -8515,6 +8551,18 @@ dependencies = [
"wasm-bindgen",
]
[[package]]
name = "json_dotpath"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dbdcfef3cf5591f0cef62da413ae795e3d1f5a00936ccec0b2071499a32efd1a"
dependencies = [
"serde",
"serde_derive",
"serde_json",
"thiserror 1.0.69",
]
[[package]]
name = "jsonschema"
version = "0.30.0"
@@ -8656,6 +8704,7 @@ dependencies = [
"clock",
"collections",
"ctor",
"diffy",
"ec4rs",
"env_logger 0.11.8",
"fs",
@@ -8753,25 +8802,6 @@ dependencies = [
"zed_llm_client",
]
[[package]]
name = "language_model_selector"
version = "0.1.0"
dependencies = [
"collections",
"feature_flags",
"futures 0.3.31",
"fuzzy",
"gpui",
"language_model",
"log",
"ordered-float 2.10.1",
"picker",
"proto",
"ui",
"workspace-hack",
"zed_actions",
]
[[package]]
name = "language_models"
version = "0.1.0"
@@ -8879,6 +8909,7 @@ dependencies = [
"async-tar",
"async-trait",
"collections",
"dap",
"futures 0.3.31",
"gpui",
"http_client",
@@ -9348,6 +9379,19 @@ dependencies = [
"logos-codegen",
]
[[package]]
name = "loom"
version = "0.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "419e0dc8046cb947daa77eb95ae174acfbddb7673b4151f56d1eed8e93fbfaca"
dependencies = [
"cfg-if",
"generator",
"scoped-tls",
"tracing",
"tracing-subscriber",
]
[[package]]
name = "loop9"
version = "0.1.5"
@@ -9526,6 +9570,7 @@ dependencies = [
"async-recursion 1.1.1",
"collections",
"editor",
"fs",
"gpui",
"language",
"linkify",
@@ -9846,6 +9891,25 @@ dependencies = [
"workspace-hack",
]
[[package]]
name = "moka"
version = "0.12.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a9321642ca94a4282428e6ea4af8cc2ca4eac48ac7a6a4ea8f33f76d0ce70926"
dependencies = [
"crossbeam-channel",
"crossbeam-epoch",
"crossbeam-utils",
"loom",
"parking_lot",
"portable-atomic",
"rustc_version",
"smallvec",
"tagptr",
"thiserror 1.0.69",
"uuid",
]
[[package]]
name = "msvc_spectre_libs"
version = "0.1.3"
@@ -10043,7 +10107,6 @@ dependencies = [
"async-tar",
"async-trait",
"async-watch",
"async_zip",
"futures 0.3.31",
"http_client",
"log",
@@ -10052,9 +10115,7 @@ dependencies = [
"serde",
"serde_json",
"smol",
"tempfile",
"util",
"walkdir",
"which 6.0.3",
"workspace-hack",
]
@@ -10159,6 +10220,15 @@ dependencies = [
"winapi",
]
[[package]]
name = "nu-ansi-term"
version = "0.50.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d4a28e057d01f97e61255210fcff094d74ed0466038633e95017f5beb68e4399"
dependencies = [
"windows-sys 0.52.0",
]
[[package]]
name = "num"
version = "0.4.3"
@@ -12782,6 +12852,7 @@ dependencies = [
"hashbrown 0.15.3",
"log",
"rustc-hash 2.1.1",
"serde",
"smallvec",
]
@@ -15433,6 +15504,12 @@ dependencies = [
"slotmap",
]
[[package]]
name = "tagptr"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7b2093cf4c8eb1e67749a6762251bc9cd836b6fc171623bd0a9d324d37af2417"
[[package]]
name = "take-until"
version = "0.2.0"
@@ -15616,6 +15693,7 @@ name = "terminal_view"
version = "0.1.0"
dependencies = [
"anyhow",
"assistant_slash_command",
"async-recursion 1.1.1",
"breadcrumbs",
"client",
@@ -16349,7 +16427,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008"
dependencies = [
"matchers",
"nu-ansi-term",
"nu-ansi-term 0.46.0",
"once_cell",
"regex",
"serde",
@@ -16988,6 +17066,7 @@ version = "0.1.0"
dependencies = [
"anyhow",
"async-fs",
"async_zip",
"collections",
"dirs 4.0.0",
"dunce",
@@ -17003,12 +17082,14 @@ dependencies = [
"rust-embed",
"serde",
"serde_json",
"serde_json_lenient",
"smol",
"take-until",
"tempfile",
"tendril",
"unicase",
"util_macros",
"walkdir",
"workspace-hack",
]
@@ -19096,6 +19177,7 @@ dependencies = [
"aho-corasick",
"anstream",
"arrayvec",
"async-compression",
"async-std",
"async-tungstenite",
"aws-config",
@@ -19128,7 +19210,9 @@ dependencies = [
"core-foundation 0.9.4",
"core-foundation-sys",
"coreaudio-sys",
"cranelift-codegen",
"crc32fast",
"crossbeam-epoch",
"crossbeam-utils",
"crypto-common",
"deranged",
@@ -19204,6 +19288,7 @@ dependencies = [
"rand 0.9.1",
"rand_chacha 0.3.1",
"rand_core 0.6.4",
"regalloc2",
"regex",
"regex-automata 0.4.9",
"regex-syntax 0.8.5",
@@ -19608,7 +19693,7 @@ dependencies = [
[[package]]
name = "zed"
version = "0.188.0"
version = "0.189.0"
dependencies = [
"activity_indicator",
"agent",
@@ -19803,9 +19888,9 @@ dependencies = [
[[package]]
name = "zed_llm_client"
version = "0.8.1"
version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "16d993fc42f9ec43ab76fa46c6eb579a66e116bb08cd2bc9a67f3afcaa05d39d"
checksum = "9be71e2f9b271e1eb8eb3e0d986075e770d1a0a299fb036abc3f1fc13a2fa7eb"
dependencies = [
"anyhow",
"serde",

View File

@@ -80,7 +80,6 @@ members = [
"crates/language",
"crates/language_extension",
"crates/language_model",
"crates/language_model_selector",
"crates/language_models",
"crates/language_selector",
"crates/language_tools",
@@ -287,7 +286,6 @@ journal = { path = "crates/journal" }
language = { path = "crates/language" }
language_extension = { path = "crates/language_extension" }
language_model = { path = "crates/language_model" }
language_model_selector = { path = "crates/language_model_selector" }
language_models = { path = "crates/language_models" }
language_selector = { path = "crates/language_selector" }
language_tools = { path = "crates/language_tools" }
@@ -464,6 +462,7 @@ indoc = "2"
inventory = "0.3.19"
itertools = "0.14.0"
jj-lib = { git = "https://github.com/jj-vcs/jj", rev = "e18eb8e05efaa153fad5ef46576af145bba1807f" }
json_dotpath = "1.1"
jsonschema = "0.30.0"
jsonwebtoken = "9.3"
jupyter-protocol = { git = "https://github.com/ConradIrwin/runtimed", rev = "7130c804216b6914355d15d0b91ea91f6babd734" }
@@ -476,6 +475,7 @@ lsp-types = { git = "https://github.com/zed-industries/lsp-types", rev = "c9c189
markup5ever_rcdom = "0.3.0"
metal = "0.29"
mlua = { version = "0.10", features = ["lua54", "vendored", "async", "send"] }
moka = { version = "0.12.10", features = ["sync"] }
naga = { version = "25.0", features = ["wgsl-in"] }
nanoid = "0.4"
nbformat = { git = "https://github.com/ConradIrwin/runtimed", rev = "7130c804216b6914355d15d0b91ea91f6babd734" }
@@ -599,7 +599,7 @@ unindent = "0.2.0"
url = "2.2"
urlencoding = "2.1.2"
uuid = { version = "1.1.2", features = ["v4", "v5", "v7", "serde"] }
walkdir = "2.3"
walkdir = "2.5"
wasi-preview1-component-adapter-provider = "29"
wasm-encoder = "0.221"
wasmparser = "0.221"
@@ -609,13 +609,14 @@ wasmtime = { version = "29", default-features = false, features = [
"runtime",
"cranelift",
"component-model",
"incremental-cache",
"parallel-compilation",
] }
wasmtime-wasi = "29"
which = "6.0.0"
wit-component = "0.221"
workspace-hack = "0.1.0"
zed_llm_client = "0.8.1"
zed_llm_client = "0.8.2"
zstd = "0.11"
[workspace.dependencies.async-stripe]

View File

@@ -33,6 +33,7 @@
"f4": "debugger::Start",
"f5": "debugger::Continue",
"shift-f5": "debugger::Stop",
"ctrl-shift-f5": "debugger::Restart",
"f6": "debugger::Pause",
"f7": "debugger::StepOver",
"cmd-f11": "debugger::StepInto",
@@ -558,6 +559,7 @@
"ctrl-shift-e": "project_panel::ToggleFocus",
"ctrl-shift-b": "outline_panel::ToggleFocus",
"ctrl-shift-g": "git_panel::ToggleFocus",
"ctrl-shift-d": "debug_panel::ToggleFocus",
"ctrl-?": "agent::ToggleFocus",
"alt-save": "workspace::SaveAll",
"ctrl-alt-s": "workspace::SaveAll",
@@ -595,7 +597,6 @@
{
"context": "Editor",
"bindings": {
"ctrl-shift-d": "editor::DuplicateLineDown",
"ctrl-shift-j": "editor::JoinLines",
"ctrl-alt-backspace": "editor::DeleteToPreviousSubwordStart",
"ctrl-alt-h": "editor::DeleteToPreviousSubwordStart",
@@ -862,6 +863,13 @@
"alt-l": "git::GenerateCommitMessage"
}
},
{
"context": "DebugPanel",
"bindings": {
"ctrl-t": "debugger::ToggleThreadPicker",
"ctrl-i": "debugger::ToggleSessionPicker"
}
},
{
"context": "CollabPanel && not_editing",
"bindings": {

View File

@@ -1,15 +1,4 @@
[
// Moved before Standard macOS bindings so that `cmd-w` is not the last binding for
// `workspace::CloseWindow` and displayed/intercepted by macOS
{
"context": "PromptLibrary",
"use_key_equivalents": true,
"bindings": {
"cmd-n": "rules_library::NewRule",
"cmd-shift-s": "rules_library::ToggleDefaultRule",
"cmd-w": "workspace::CloseWindow"
}
},
// Standard macOS bindings
{
"use_key_equivalents": true,
@@ -17,6 +6,7 @@
"f4": "debugger::Start",
"f5": "debugger::Continue",
"shift-f5": "debugger::Stop",
"shift-cmd-f5": "debugger::Restart",
"f6": "debugger::Pause",
"f7": "debugger::StepOver",
"f11": "debugger::StepInto",
@@ -379,6 +369,15 @@
"shift-backspace": "agent::RemoveSelectedThread"
}
},
{
"context": "PromptLibrary",
"use_key_equivalents": true,
"bindings": {
"cmd-n": "rules_library::NewRule",
"cmd-shift-s": "rules_library::ToggleDefaultRule",
"cmd-w": "workspace::CloseWindow"
}
},
{
"context": "BufferSearchBar",
"use_key_equivalents": true,
@@ -624,6 +623,7 @@
"cmd-shift-e": "project_panel::ToggleFocus",
"cmd-shift-b": "outline_panel::ToggleFocus",
"ctrl-shift-g": "git_panel::ToggleFocus",
"cmd-shift-d": "debug_panel::ToggleFocus",
"cmd-?": "agent::ToggleFocus",
"cmd-alt-s": "workspace::SaveAll",
"cmd-k m": "language_selector::Toggle",
@@ -929,6 +929,13 @@
"alt-tab": "git::GenerateCommitMessage"
}
},
{
"context": "DebugPanel",
"bindings": {
"cmd-t": "debugger::ToggleThreadPicker",
"cmd-i": "debugger::ToggleSessionPicker"
}
},
{
"context": "CollabPanel && not_editing",
"use_key_equivalents": true,

View File

@@ -846,13 +846,5 @@
// and Windows.
"alt-l": "editor::AcceptEditPrediction"
}
},
{
// Fixes https://github.com/zed-industries/zed/issues/29095 by ensuring that
// the last binding for editor::ToggleComments is not ctrl-c.
"context": "hack_to_fix_ctrl-c",
"bindings": {
"g c": "editor::ToggleComments"
}
}
]

View File

@@ -230,11 +230,11 @@
// Possible values:
// - "off" — no diagnostics are allowed
// - "error"
// - "warning" (default)
// - "warning"
// - "info"
// - "hint"
// - null — allow all diagnostics
"diagnostics_max_severity": "warning",
// - null — allow all diagnostics (default)
"diagnostics_max_severity": null,
// Whether to show wrap guides (vertical rulers) in the editor.
// Setting this to true will show a guide at the 'preferred_line_length' value
// if 'soft_wrap' is set to 'preferred_line_length', and will show any

View File

@@ -60,6 +60,7 @@ struct Content {
message: String,
on_click:
Option<Arc<dyn Fn(&mut ActivityIndicator, &mut Window, &mut Context<ActivityIndicator>)>>,
tooltip_message: Option<String>,
}
impl ActivityIndicator {
@@ -262,6 +263,7 @@ impl ActivityIndicator {
});
window.dispatch_action(Box::new(workspace::OpenLog), cx);
})),
tooltip_message: None,
});
}
// Show any language server has pending activity.
@@ -305,6 +307,7 @@ impl ActivityIndicator {
),
message,
on_click: Some(Arc::new(Self::toggle_language_server_work_context_menu)),
tooltip_message: None,
});
}
@@ -332,6 +335,7 @@ impl ActivityIndicator {
),
message: job_info.message.into(),
on_click: None,
tooltip_message: None,
});
}
}
@@ -374,6 +378,7 @@ impl ActivityIndicator {
.retain(|status| !downloading.contains(&status.name));
this.dismiss_error_message(&DismissErrorMessage, window, cx)
})),
tooltip_message: None,
});
}
@@ -402,6 +407,7 @@ impl ActivityIndicator {
.retain(|status| !checking_for_update.contains(&status.name));
this.dismiss_error_message(&DismissErrorMessage, window, cx)
})),
tooltip_message: None,
});
}
@@ -428,6 +434,7 @@ impl ActivityIndicator {
on_click: Some(Arc::new(|this, window, cx| {
this.show_error_message(&Default::default(), window, cx)
})),
tooltip_message: None,
});
}
@@ -446,6 +453,7 @@ impl ActivityIndicator {
});
window.dispatch_action(Box::new(workspace::OpenLog), cx);
})),
tooltip_message: None,
});
}
@@ -462,6 +470,7 @@ impl ActivityIndicator {
on_click: Some(Arc::new(|this, window, cx| {
this.dismiss_error_message(&DismissErrorMessage, window, cx)
})),
tooltip_message: None,
}),
AutoUpdateStatus::Downloading => Some(Content {
icon: Some(
@@ -473,6 +482,7 @@ impl ActivityIndicator {
on_click: Some(Arc::new(|this, window, cx| {
this.dismiss_error_message(&DismissErrorMessage, window, cx)
})),
tooltip_message: None,
}),
AutoUpdateStatus::Installing => Some(Content {
icon: Some(
@@ -484,8 +494,12 @@ impl ActivityIndicator {
on_click: Some(Arc::new(|this, window, cx| {
this.dismiss_error_message(&DismissErrorMessage, window, cx)
})),
tooltip_message: None,
}),
AutoUpdateStatus::Updated { binary_path, .. } => Some(Content {
AutoUpdateStatus::Updated {
binary_path,
version,
} => Some(Content {
icon: None,
message: "Click to restart and update Zed".to_string(),
on_click: Some(Arc::new({
@@ -494,6 +508,14 @@ impl ActivityIndicator {
};
move |_, _, cx| workspace::reload(&reload, cx)
})),
tooltip_message: Some(format!("Install version: {}", {
match version {
auto_update::VersionCheckType::Sha(sha) => sha.to_string(),
auto_update::VersionCheckType::Semantic(semantic_version) => {
semantic_version.to_string()
}
}
})),
}),
AutoUpdateStatus::Errored => Some(Content {
icon: Some(
@@ -505,6 +527,7 @@ impl ActivityIndicator {
on_click: Some(Arc::new(|this, window, cx| {
this.dismiss_error_message(&DismissErrorMessage, window, cx)
})),
tooltip_message: None,
}),
AutoUpdateStatus::Idle => None,
};
@@ -524,6 +547,7 @@ impl ActivityIndicator {
on_click: Some(Arc::new(|this, window, cx| {
this.dismiss_error_message(&DismissErrorMessage, window, cx)
})),
tooltip_message: None,
});
}
}
@@ -575,7 +599,14 @@ impl Render for ActivityIndicator {
)
.tooltip(Tooltip::text(content.message))
} else {
button.child(Label::new(content.message).size(LabelSize::Small))
button
.child(Label::new(content.message).size(LabelSize::Small))
.when_some(
content.tooltip_message,
|this, tooltip_message| {
this.tooltip(Tooltip::text(tooltip_message))
},
)
}
})
.when_some(content.on_click, |this, handler| {

View File

@@ -52,7 +52,6 @@ itertools.workspace = true
jsonschema.workspace = true
language.workspace = true
language_model.workspace = true
language_model_selector.workspace = true
log.workspace = true
lsp.workspace = true
markdown.workspace = true

View File

@@ -117,6 +117,7 @@ pub fn init(
client: Arc<Client>,
prompt_builder: Arc<PromptBuilder>,
language_registry: Arc<LanguageRegistry>,
is_eval: bool,
cx: &mut App,
) {
AssistantSettings::register(cx);
@@ -124,7 +125,11 @@ pub fn init(
assistant_context_editor::init(client.clone(), cx);
rules_library::init(cx);
init_language_model_settings(cx);
if !is_eval {
// Initializing the language model from the user settings messes with the eval, so we only initialize them when
// we're not running inside of the eval.
init_language_model_settings(cx);
}
assistant_slash_command::init(cx);
thread_store::init(cx);
agent_panel::init(cx);
@@ -217,7 +222,6 @@ fn register_slash_commands(cx: &mut App) {
slash_command_registry.register_command(assistant_slash_commands::PromptSlashCommand, true);
slash_command_registry.register_command(assistant_slash_commands::SelectionCommand, true);
slash_command_registry.register_command(assistant_slash_commands::DefaultSlashCommand, false);
slash_command_registry.register_command(assistant_slash_commands::TerminalSlashCommand, true);
slash_command_registry.register_command(assistant_slash_commands::NowSlashCommand, false);
slash_command_registry
.register_command(assistant_slash_commands::DiagnosticsSlashCommand, true);

View File

@@ -1348,6 +1348,7 @@ impl AgentDiff {
ThreadEvent::NewRequest
| ThreadEvent::Stopped(Ok(StopReason::EndTurn))
| ThreadEvent::Stopped(Ok(StopReason::MaxTokens))
| ThreadEvent::Stopped(Ok(StopReason::Refusal))
| ThreadEvent::Stopped(Err(_))
| ThreadEvent::ShowError(_)
| ThreadEvent::CompletionCanceled => {

View File

@@ -3,10 +3,10 @@ use fs::Fs;
use gpui::{Entity, FocusHandle, SharedString};
use crate::Thread;
use language_model::{ConfiguredModel, LanguageModelRegistry};
use language_model_selector::{
use assistant_context_editor::language_model_selector::{
LanguageModelSelector, LanguageModelSelectorPopoverMenu, ToggleModelSelector,
};
use language_model::{ConfiguredModel, LanguageModelRegistry};
use settings::update_settings_file;
use std::sync::Arc;
use ui::{PopoverMenuHandle, Tooltip, prelude::*};

View File

@@ -4,7 +4,6 @@ use std::sync::Arc;
use std::time::Duration;
use db::kvp::{Dismissable, KEY_VALUE_STORE};
use markdown::Markdown;
use serde::{Deserialize, Serialize};
use anyhow::{Result, anyhow};
@@ -17,6 +16,7 @@ use assistant_settings::{AssistantDockPosition, AssistantSettings};
use assistant_slash_command::SlashCommandWorkingSet;
use assistant_tool::ToolWorkingSet;
use assistant_context_editor::language_model_selector::ToggleModelSelector;
use client::{UserStore, zed_urls};
use editor::{Anchor, AnchorRangeExt as _, Editor, EditorEvent, MultiBuffer};
use fs::Fs;
@@ -30,7 +30,6 @@ use language::LanguageRegistry;
use language_model::{
LanguageModelProviderTosView, LanguageModelRegistry, RequestUsage, ZED_CLOUD_PROVIDER_ID,
};
use language_model_selector::ToggleModelSelector;
use project::{Project, ProjectPath, Worktree};
use prompt_store::{PromptBuilder, PromptStore, UserPromptId};
use proto::Plan;
@@ -157,7 +156,7 @@ pub fn init(cx: &mut App) {
window.refresh();
})
.register_action(|_workspace, _: &ResetTrialUpsell, _window, cx| {
TrialUpsell::set_dismissed(false, cx);
Upsell::set_dismissed(false, cx);
})
.register_action(|_workspace, _: &ResetTrialEndUpsell, _window, cx| {
TrialEndUpsell::set_dismissed(false, cx);
@@ -370,8 +369,7 @@ pub struct AgentPanel {
height: Option<Pixels>,
zoomed: bool,
pending_serialization: Option<Task<Result<()>>>,
hide_trial_upsell: bool,
_trial_markdown: Entity<Markdown>,
hide_upsell: bool,
}
impl AgentPanel {
@@ -676,15 +674,6 @@ impl AgentPanel {
},
);
let trial_markdown = cx.new(|cx| {
Markdown::new(
include_str!("trial_markdown.md").into(),
Some(language_registry.clone()),
None,
cx,
)
});
Self {
active_view,
workspace,
@@ -721,8 +710,7 @@ impl AgentPanel {
height: None,
zoomed: false,
pending_serialization: None,
hide_trial_upsell: false,
_trial_markdown: trial_markdown,
hide_upsell: false,
}
}
@@ -1946,7 +1934,7 @@ impl AgentPanel {
return false;
}
if self.hide_trial_upsell || TrialUpsell::dismissed() {
if self.hide_upsell || Upsell::dismissed() {
return false;
}
@@ -1976,7 +1964,7 @@ impl AgentPanel {
true
}
fn render_trial_upsell(
fn render_upsell(
&self,
_window: &mut Window,
cx: &mut Context<Self>,
@@ -1985,6 +1973,14 @@ impl AgentPanel {
return None;
}
if self.user_store.read(cx).current_user_account_too_young() {
Some(self.render_young_account_upsell(cx).into_any_element())
} else {
Some(self.render_trial_upsell(cx).into_any_element())
}
}
fn render_young_account_upsell(&self, cx: &mut Context<Self>) -> impl IntoElement {
let checkbox = CheckboxWithLabel::new(
"dont-show-again",
Label::new("Don't show again").color(Color::Muted),
@@ -1992,7 +1988,70 @@ impl AgentPanel {
move |toggle_state, _window, cx| {
let toggle_state_bool = toggle_state.selected();
TrialUpsell::set_dismissed(toggle_state_bool, cx);
Upsell::set_dismissed(toggle_state_bool, cx);
},
);
let contents = div()
.size_full()
.gap_2()
.flex()
.flex_col()
.child(Headline::new("Build better with Zed Pro").size(HeadlineSize::Small))
.child(
Label::new("Your GitHub account was created less than 30 days ago, so we can't offer you a free trial.")
.size(LabelSize::Small),
)
.child(
Label::new(
"Use your own API keys, upgrade to Zed Pro or send an email to billing-support@zed.dev.",
)
.color(Color::Muted),
)
.child(
h_flex()
.w_full()
.px_neg_1()
.justify_between()
.items_center()
.child(h_flex().items_center().gap_1().child(checkbox))
.child(
h_flex()
.gap_2()
.child(
Button::new("dismiss-button", "Not Now")
.style(ButtonStyle::Transparent)
.color(Color::Muted)
.on_click({
let agent_panel = cx.entity();
move |_, _, cx| {
agent_panel.update(cx, |this, cx| {
this.hide_upsell = true;
cx.notify();
});
}
}),
)
.child(
Button::new("cta-button", "Upgrade to Zed Pro")
.style(ButtonStyle::Transparent)
.on_click(|_, _, cx| cx.open_url(&zed_urls::account_url(cx))),
),
),
);
self.render_upsell_container(cx, contents)
}
fn render_trial_upsell(&self, cx: &mut Context<Self>) -> impl IntoElement {
let checkbox = CheckboxWithLabel::new(
"dont-show-again",
Label::new("Don't show again").color(Color::Muted),
ToggleState::Unselected,
move |toggle_state, _window, cx| {
let toggle_state_bool = toggle_state.selected();
Upsell::set_dismissed(toggle_state_bool, cx);
},
);
@@ -2030,7 +2089,7 @@ impl AgentPanel {
let agent_panel = cx.entity();
move |_, _, cx| {
agent_panel.update(cx, |this, cx| {
this.hide_trial_upsell = true;
this.hide_upsell = true;
cx.notify();
});
}
@@ -2044,7 +2103,7 @@ impl AgentPanel {
),
);
Some(self.render_upsell_container(cx, contents))
self.render_upsell_container(cx, contents)
}
fn render_trial_end_upsell(
@@ -2910,7 +2969,7 @@ impl Render for AgentPanel {
.on_action(cx.listener(Self::reset_font_size))
.on_action(cx.listener(Self::toggle_zoom))
.child(self.render_toolbar(window, cx))
.children(self.render_trial_upsell(window, cx))
.children(self.render_upsell(window, cx))
.children(self.render_trial_end_upsell(window, cx))
.map(|parent| match &self.active_view {
ActiveView::Thread { .. } => parent
@@ -3099,9 +3158,9 @@ impl AgentPanelDelegate for ConcreteAssistantPanelDelegate {
}
}
struct TrialUpsell;
struct Upsell;
impl Dismissable for TrialUpsell {
impl Dismissable for Upsell {
const KEY: &'static str = "dismissed-trial-upsell";
}

View File

@@ -9,6 +9,7 @@ use crate::terminal_codegen::TerminalCodegen;
use crate::thread_store::{TextThreadStore, ThreadStore};
use crate::{CycleNextInlineAssist, CyclePreviousInlineAssist};
use crate::{RemoveAllContext, ToggleContextPicker};
use assistant_context_editor::language_model_selector::ToggleModelSelector;
use client::ErrorExt;
use collections::VecDeque;
use db::kvp::Dismissable;
@@ -24,7 +25,6 @@ use gpui::{
Focusable, FontWeight, Subscription, TextStyle, WeakEntity, Window, anchored, deferred, point,
};
use language_model::{LanguageModel, LanguageModelRegistry};
use language_model_selector::ToggleModelSelector;
use parking_lot::Mutex;
use settings::Settings;
use std::cmp;

View File

@@ -8,6 +8,7 @@ use crate::ui::{
AnimatedLabel, MaxModeTooltip,
preview::{AgentPreview, UsageCallout},
};
use assistant_context_editor::language_model_selector::ToggleModelSelector;
use assistant_settings::{AssistantSettings, CompletionMode};
use buffer_diff::BufferDiff;
use client::UserStore;
@@ -30,7 +31,6 @@ use language_model::{
ConfiguredModel, LanguageModelRequestMessage, MessageContent, RequestUsage,
ZED_CLOUD_PROVIDER_ID,
};
use language_model_selector::ToggleModelSelector;
use multi_buffer;
use project::Project;
use prompt_store::PromptStore;

View File

@@ -1693,6 +1693,43 @@ impl Thread {
project.set_agent_location(None, cx);
});
}
StopReason::Refusal => {
thread.project.update(cx, |project, cx| {
project.set_agent_location(None, cx);
});
// Remove the turn that was refused.
//
// https://docs.anthropic.com/en/docs/test-and-evaluate/strengthen-guardrails/handle-streaming-refusals#reset-context-after-refusal
{
let mut messages_to_remove = Vec::new();
for (ix, message) in thread.messages.iter().enumerate().rev() {
messages_to_remove.push(message.id);
if message.role == Role::User {
if ix == 0 {
break;
}
if let Some(prev_message) = thread.messages.get(ix - 1) {
if prev_message.role == Role::Assistant {
break;
}
}
}
}
for message_id in messages_to_remove {
thread.delete_message(message_id, cx);
}
}
cx.emit(ThreadEvent::ShowError(ThreadError::Message {
header: "Language model refusal".into(),
message: "Model refused to generate content for safety reasons.".into(),
}));
}
},
Err(error) => {
thread.project.update(cx, |project, cx| {

View File

@@ -1,3 +0,0 @@
# Build better with Zed Pro
Try [Zed Pro](https://zed.dev/pricing) for free for 14 days - no credit card required. Only $20/month afterward. Cancel anytime.

View File

@@ -34,7 +34,6 @@ pub enum AnthropicModelMode {
pub enum Model {
#[serde(rename = "claude-3-5-sonnet", alias = "claude-3-5-sonnet-latest")]
Claude3_5Sonnet,
#[default]
#[serde(rename = "claude-3-7-sonnet", alias = "claude-3-7-sonnet-latest")]
Claude3_7Sonnet,
#[serde(
@@ -42,6 +41,21 @@ pub enum Model {
alias = "claude-3-7-sonnet-thinking-latest"
)]
Claude3_7SonnetThinking,
#[serde(rename = "claude-opus-4", alias = "claude-opus-4-latest")]
ClaudeOpus4,
#[serde(
rename = "claude-opus-4-thinking",
alias = "claude-opus-4-thinking-latest"
)]
ClaudeOpus4Thinking,
#[default]
#[serde(rename = "claude-sonnet-4", alias = "claude-sonnet-4-latest")]
ClaudeSonnet4,
#[serde(
rename = "claude-sonnet-4-thinking",
alias = "claude-sonnet-4-thinking-latest"
)]
ClaudeSonnet4Thinking,
#[serde(rename = "claude-3-5-haiku", alias = "claude-3-5-haiku-latest")]
Claude3_5Haiku,
#[serde(rename = "claude-3-opus", alias = "claude-3-opus-latest")]
@@ -89,6 +103,14 @@ impl Model {
Ok(Self::Claude3Sonnet)
} else if id.starts_with("claude-3-haiku") {
Ok(Self::Claude3Haiku)
} else if id.starts_with("claude-opus-4-thinking") {
Ok(Self::ClaudeOpus4Thinking)
} else if id.starts_with("claude-opus-4") {
Ok(Self::ClaudeOpus4)
} else if id.starts_with("claude-sonnet-4-thinking") {
Ok(Self::ClaudeSonnet4Thinking)
} else if id.starts_with("claude-sonnet-4") {
Ok(Self::ClaudeSonnet4)
} else {
anyhow::bail!("invalid model id {id}");
}
@@ -96,6 +118,10 @@ impl Model {
pub fn id(&self) -> &str {
match self {
Model::ClaudeOpus4 => "claude-opus-4-latest",
Model::ClaudeOpus4Thinking => "claude-opus-4-thinking-latest",
Model::ClaudeSonnet4 => "claude-sonnet-4-latest",
Model::ClaudeSonnet4Thinking => "claude-sonnet-4-thinking-latest",
Model::Claude3_5Sonnet => "claude-3-5-sonnet-latest",
Model::Claude3_7Sonnet => "claude-3-7-sonnet-latest",
Model::Claude3_7SonnetThinking => "claude-3-7-sonnet-thinking-latest",
@@ -110,6 +136,8 @@ impl Model {
/// The id of the model that should be used for making API requests
pub fn request_id(&self) -> &str {
match self {
Model::ClaudeOpus4 | Model::ClaudeOpus4Thinking => "claude-opus-4-20250514",
Model::ClaudeSonnet4 | Model::ClaudeSonnet4Thinking => "claude-sonnet-4-20250514",
Model::Claude3_5Sonnet => "claude-3-5-sonnet-latest",
Model::Claude3_7Sonnet | Model::Claude3_7SonnetThinking => "claude-3-7-sonnet-latest",
Model::Claude3_5Haiku => "claude-3-5-haiku-latest",
@@ -122,6 +150,10 @@ impl Model {
pub fn display_name(&self) -> &str {
match self {
Model::ClaudeOpus4 => "Claude Opus 4",
Model::ClaudeOpus4Thinking => "Claude Opus 4 Thinking",
Model::ClaudeSonnet4 => "Claude Sonnet 4",
Model::ClaudeSonnet4Thinking => "Claude Sonnet 4 Thinking",
Self::Claude3_7Sonnet => "Claude 3.7 Sonnet",
Self::Claude3_5Sonnet => "Claude 3.5 Sonnet",
Self::Claude3_7SonnetThinking => "Claude 3.7 Sonnet Thinking",
@@ -137,7 +169,11 @@ impl Model {
pub fn cache_configuration(&self) -> Option<AnthropicModelCacheConfiguration> {
match self {
Self::Claude3_5Sonnet
Self::ClaudeOpus4
| Self::ClaudeOpus4Thinking
| Self::ClaudeSonnet4
| Self::ClaudeSonnet4Thinking
| Self::Claude3_5Sonnet
| Self::Claude3_5Haiku
| Self::Claude3_7Sonnet
| Self::Claude3_7SonnetThinking
@@ -156,7 +192,11 @@ impl Model {
pub fn max_token_count(&self) -> usize {
match self {
Self::Claude3_5Sonnet
Self::ClaudeOpus4
| Self::ClaudeOpus4Thinking
| Self::ClaudeSonnet4
| Self::ClaudeSonnet4Thinking
| Self::Claude3_5Sonnet
| Self::Claude3_5Haiku
| Self::Claude3_7Sonnet
| Self::Claude3_7SonnetThinking
@@ -173,7 +213,11 @@ impl Model {
Self::Claude3_5Sonnet
| Self::Claude3_7Sonnet
| Self::Claude3_7SonnetThinking
| Self::Claude3_5Haiku => 8_192,
| Self::Claude3_5Haiku
| Self::ClaudeOpus4
| Self::ClaudeOpus4Thinking
| Self::ClaudeSonnet4
| Self::ClaudeSonnet4Thinking => 8_192,
Self::Custom {
max_output_tokens, ..
} => max_output_tokens.unwrap_or(4_096),
@@ -182,7 +226,11 @@ impl Model {
pub fn default_temperature(&self) -> f32 {
match self {
Self::Claude3_5Sonnet
Self::ClaudeOpus4
| Self::ClaudeOpus4Thinking
| Self::ClaudeSonnet4
| Self::ClaudeSonnet4Thinking
| Self::Claude3_5Sonnet
| Self::Claude3_7Sonnet
| Self::Claude3_7SonnetThinking
| Self::Claude3_5Haiku
@@ -201,10 +249,14 @@ impl Model {
Self::Claude3_5Sonnet
| Self::Claude3_7Sonnet
| Self::Claude3_5Haiku
| Self::ClaudeOpus4
| Self::ClaudeSonnet4
| Self::Claude3Opus
| Self::Claude3Sonnet
| Self::Claude3Haiku => AnthropicModelMode::Default,
Self::Claude3_7SonnetThinking => AnthropicModelMode::Thinking {
Self::Claude3_7SonnetThinking
| Self::ClaudeOpus4Thinking
| Self::ClaudeSonnet4Thinking => AnthropicModelMode::Thinking {
budget_tokens: Some(4_096),
},
Self::Custom { mode, .. } => mode.clone(),

View File

@@ -22,6 +22,7 @@ clock.workspace = true
collections.workspace = true
context_server.workspace = true
editor.workspace = true
feature_flags.workspace = true
fs.workspace = true
futures.workspace = true
fuzzy.workspace = true
@@ -29,15 +30,16 @@ gpui.workspace = true
indexed_docs.workspace = true
language.workspace = true
language_model.workspace = true
language_model_selector.workspace = true
log.workspace = true
multi_buffer.workspace = true
open_ai.workspace = true
ordered-float.workspace = true
parking_lot.workspace = true
paths.workspace = true
picker.workspace = true
project.workspace = true
prompt_store.workspace = true
proto.workspace = true
regex.workspace = true
rope.workspace = true
rpc.workspace = true

View File

@@ -2,6 +2,7 @@ mod context;
mod context_editor;
mod context_history;
mod context_store;
pub mod language_model_selector;
mod slash_command;
mod slash_command_picker;

View File

@@ -2204,6 +2204,7 @@ impl AssistantContext {
StopReason::ToolUse => {}
StopReason::EndTurn => {}
StopReason::MaxTokens => {}
StopReason::Refusal => {}
}
}
})

View File

@@ -1,3 +1,6 @@
use crate::language_model_selector::{
LanguageModelSelector, LanguageModelSelectorPopoverMenu, ToggleModelSelector,
};
use anyhow::Result;
use assistant_settings::AssistantSettings;
use assistant_slash_command::{SlashCommand, SlashCommandOutputSection, SlashCommandWorkingSet};
@@ -36,9 +39,6 @@ use language_model::{
LanguageModelImage, LanguageModelProvider, LanguageModelProviderTosView, LanguageModelRegistry,
Role,
};
use language_model_selector::{
LanguageModelSelector, LanguageModelSelectorPopoverMenu, ToggleModelSelector,
};
use multi_buffer::MultiBufferRow;
use picker::Picker;
use project::{Project, Worktree};

View File

@@ -9,6 +9,7 @@ use anyhow::Result;
use futures::StreamExt;
use futures::stream::{self, BoxStream};
use gpui::{App, SharedString, Task, WeakEntity, Window};
use language::HighlightId;
use language::{BufferSnapshot, CodeLabel, LspAdapterDelegate, OffsetRangeExt};
pub use language_model::Role;
use serde::{Deserialize, Serialize};
@@ -16,6 +17,7 @@ use std::{
ops::Range,
sync::{Arc, atomic::AtomicBool},
};
use ui::ActiveTheme;
use workspace::{Workspace, ui::IconName};
pub fn init(cx: &mut App) {
@@ -325,6 +327,18 @@ impl SlashCommandLine {
}
}
pub fn create_label_for_command(command_name: &str, arguments: &[&str], cx: &App) -> CodeLabel {
let mut label = CodeLabel::default();
label.push_str(command_name, None);
label.push_str(" ", None);
label.push_str(
&arguments.join(" "),
cx.theme().syntax().highlight_id("comment").map(HighlightId),
);
label.filter_range = 0..command_name.len();
label
}
#[cfg(test)]
mod tests {
use pretty_assertions::assert_eq;

View File

@@ -35,7 +35,6 @@ rope.workspace = true
serde.workspace = true
serde_json.workspace = true
smol.workspace = true
terminal_view.workspace = true
text.workspace = true
toml.workspace = true
ui.workspace = true

View File

@@ -12,11 +12,6 @@ mod selection_command;
mod streaming_example_command;
mod symbols_command;
mod tab_command;
mod terminal_command;
use gpui::App;
use language::{CodeLabel, HighlightId};
use ui::ActiveTheme as _;
pub use crate::cargo_workspace_command::*;
pub use crate::context_server_command::*;
@@ -32,16 +27,5 @@ pub use crate::selection_command::*;
pub use crate::streaming_example_command::*;
pub use crate::symbols_command::*;
pub use crate::tab_command::*;
pub use crate::terminal_command::*;
pub fn create_label_for_command(command_name: &str, arguments: &[&str], cx: &App) -> CodeLabel {
let mut label = CodeLabel::default();
label.push_str(command_name, None);
label.push_str(" ", None);
label.push_str(
&arguments.join(" "),
cx.theme().syntax().highlight_id("comment").map(HighlightId),
);
label.filter_range = 0..command_name.len();
label
}
use assistant_slash_command::create_label_for_command;

View File

@@ -41,6 +41,7 @@ open.workspace = true
paths.workspace = true
portable-pty.workspace = true
project.workspace = true
prompt_store.workspace = true
regex.workspace = true
rust-embed.workspace = true
schemars.workspace = true

View File

@@ -18,6 +18,7 @@ use language_model::{
LanguageModelToolResultContent, LanguageModelToolUse, LanguageModelToolUseId, SelectedModel,
};
use project::Project;
use prompt_store::{ModelContext, ProjectContext, PromptBuilder, WorktreeContext};
use rand::prelude::*;
use reqwest_client::ReqwestClient;
use serde_json::json;
@@ -33,13 +34,30 @@ use util::path;
#[test]
#[cfg_attr(not(feature = "eval"), ignore)]
fn eval_extract_handle_command_output() {
// Test how well agent generates multiple edit hunks.
//
// Model | Pass rate
// ----------------------------|----------
// claude-3.7-sonnet | 0.98
// gemini-2.5-pro | 0.86
// gemini-2.5-flash | 0.11
// gpt-4.1 | 1.00
let input_file_path = "root/blame.rs";
let input_file_content = include_str!("evals/fixtures/extract_handle_command_output/before.rs");
let output_file_content = include_str!("evals/fixtures/extract_handle_command_output/after.rs");
let possible_diffs = vec![
include_str!("evals/fixtures/extract_handle_command_output/possible-01.diff"),
include_str!("evals/fixtures/extract_handle_command_output/possible-02.diff"),
include_str!("evals/fixtures/extract_handle_command_output/possible-03.diff"),
include_str!("evals/fixtures/extract_handle_command_output/possible-04.diff"),
include_str!("evals/fixtures/extract_handle_command_output/possible-05.diff"),
include_str!("evals/fixtures/extract_handle_command_output/possible-06.diff"),
include_str!("evals/fixtures/extract_handle_command_output/possible-07.diff"),
];
let edit_description = "Extract `handle_command_output` method from `run_git_blame`.";
eval(
100,
0.95,
0.7, // Taking the lower bar for Gemini
EvalInput::from_conversation(
vec![
message(
@@ -48,6 +66,7 @@ fn eval_extract_handle_command_output() {
Read the `{input_file_path}` file and extract a method in
the final stanza of `run_git_blame` to deal with command failures,
call it `handle_command_output` and take the std::process::Output as the only parameter.
Do not document the method and do not add any comments.
Add it right next to `run_git_blame` and copy it verbatim from `run_git_blame`.
"})],
@@ -82,7 +101,7 @@ fn eval_extract_handle_command_output() {
),
],
Some(input_file_content.into()),
EvalAssertion::assert_eq(output_file_content),
EvalAssertion::assert_diff_any(possible_diffs),
),
);
}
@@ -648,7 +667,7 @@ fn eval_zode() {
let invalid_starts = [' ', '`', '\n'];
let mut message = String::new();
for start in invalid_starts {
if sample.text.starts_with(start) {
if sample.text_after.starts_with(start) {
message.push_str(&format!("The sample starts with a {:?}\n", start));
break;
}
@@ -895,52 +914,24 @@ fn eval_add_overwrite_test() {
}
#[test]
#[ignore] // until we figure out the mystery described in the comments
// #[cfg_attr(not(feature = "eval"), ignore)]
#[cfg_attr(not(feature = "eval"), ignore)]
fn eval_create_empty_file() {
// Check that Edit Agent can create a file without writing its
// thoughts into it. This issue is not specific to empty files, but
// it's easier to reproduce with them.
//
// NOTE: For some mysterious reason, I could easily reproduce this
// issue roughly 90% of the time in actual Zed. However, once I
// extract the exact LLM request before the failure point and
// generate from that, the reproduction rate drops to 2%!
//
// Things I've tried to make sure it's not a fluke: disabling prompt
// caching, capturing the LLM request via a proxy server, running the
// prompt on Claude separately from evals. Every time it was mostly
// giving good outcomes, which doesn't match my actual experience in
// Zed.
//
// At some point I discovered that simply adding one insignificant
// space or a newline to the prompt suddenly results in an outcome I
// tried to reproduce almost perfectly.
//
// This weirdness happens even outside of the Zed code base and even
// when using a different subscription. The result is the same: an
// extra newline or space changes the model behavior significantly
// enough, so that the pass rate drops from 99% to 0-3%
//
// I have no explanation to this.
//
//
// Model | Pass rate
// ============================================
//
// --------------------------------------------
// Prompt version: 2025-05-19
// Prompt version: 2025-05-21
// --------------------------------------------
//
// claude-3.7-sonnet | 0.98
// + one extra space in prompt | 0.00
// + original prompt again | 0.99
// + extra newline | 0.03
// claude-3.7-sonnet | 1.00
// gemini-2.5-pro-preview-03-25 | 1.00
// gemini-2.5-flash-preview-04-17 | 1.00
// + one extra space | 1.00
// gpt-4.1 | 1.00
// + one extra space | 1.00
//
//
// TODO: gpt-4.1-mini errored 38 times:
@@ -949,8 +940,8 @@ fn eval_create_empty_file() {
let input_file_content = None;
let expected_output_content = String::new();
eval(
1,
1.0,
100,
0.99,
EvalInput::from_conversation(
vec![
message(User, [text("Create a second empty todo file ")]),
@@ -1101,7 +1092,8 @@ impl EvalInput {
#[derive(Clone)]
struct EvalSample {
text: String,
text_before: String,
text_after: String,
edit_output: EditAgentOutput,
diff: String,
}
@@ -1158,7 +1150,7 @@ impl EvalAssertion {
let expected = expected.into();
Self::new(async move |sample, _judge, _cx| {
Ok(EvalAssertionOutcome {
score: if strip_empty_lines(&sample.text) == strip_empty_lines(&expected) {
score: if strip_empty_lines(&sample.text_after) == strip_empty_lines(&expected) {
100
} else {
0
@@ -1168,6 +1160,22 @@ impl EvalAssertion {
})
}
fn assert_diff_any(expected_diffs: Vec<impl Into<String>>) -> Self {
let expected_diffs: Vec<String> = expected_diffs.into_iter().map(Into::into).collect();
Self::new(async move |sample, _judge, _cx| {
let matches = expected_diffs.iter().any(|possible_diff| {
let expected =
language::apply_diff_patch(&sample.text_before, possible_diff).unwrap();
strip_empty_lines(&expected) == strip_empty_lines(&sample.text_after)
});
Ok(EvalAssertionOutcome {
score: if matches { 100 } else { 0 },
message: None,
})
})
}
fn judge_diff(assertions: &'static str) -> Self {
Self::new(async move |sample, judge, cx| {
let prompt = DiffJudgeTemplate {
@@ -1252,7 +1260,7 @@ fn eval(iterations: usize, expected_pass_ratio: f32, mut eval: EvalInput) {
if output.assertion.score < 80 {
failed_count += 1;
failed_evals
.entry(output.sample.text.clone())
.entry(output.sample.text_after.clone())
.or_insert(Vec::new())
.push(output);
}
@@ -1442,26 +1450,62 @@ impl EditAgentTest {
.update(cx, |project, cx| project.open_buffer(path, cx))
.await
.unwrap();
let conversation = LanguageModelRequest {
messages: eval.conversation,
tools: cx.update(|cx| {
ToolRegistry::default_global(cx)
.tools()
.into_iter()
.filter_map(|tool| {
let input_schema = tool
.input_schema(self.agent.model.tool_input_format())
.ok()?;
Some(LanguageModelRequestTool {
name: tool.name(),
description: tool.description(),
input_schema,
})
let tools = cx.update(|cx| {
ToolRegistry::default_global(cx)
.tools()
.into_iter()
.filter_map(|tool| {
let input_schema = tool
.input_schema(self.agent.model.tool_input_format())
.ok()?;
Some(LanguageModelRequestTool {
name: tool.name(),
description: tool.description(),
input_schema,
})
.collect()
}),
})
.collect::<Vec<_>>()
});
let tool_names = tools
.iter()
.map(|tool| tool.name.clone())
.collect::<Vec<_>>();
let worktrees = vec![WorktreeContext {
root_name: "root".to_string(),
rules_file: None,
}];
let prompt_builder = PromptBuilder::new(None)?;
let project_context = ProjectContext::new(worktrees, Vec::default());
let system_prompt = prompt_builder.generate_assistant_system_prompt(
&project_context,
&ModelContext {
available_tools: tool_names,
},
)?;
let has_system_prompt = eval
.conversation
.first()
.map_or(false, |msg| msg.role == Role::System);
let messages = if has_system_prompt {
eval.conversation
} else {
[LanguageModelRequestMessage {
role: Role::System,
content: vec![MessageContent::Text(system_prompt)],
cache: true,
}]
.into_iter()
.chain(eval.conversation)
.collect::<Vec<_>>()
};
let conversation = LanguageModelRequest {
messages,
tools,
..Default::default()
};
let edit_output = if matches!(eval.edit_file_input.mode, EditFileMode::Edit) {
if let Some(input_content) = eval.input_content.as_deref() {
buffer.update(cx, |buffer, cx| buffer.set_text(input_content, cx));
@@ -1490,7 +1534,8 @@ impl EditAgentTest {
eval.input_content.as_deref().unwrap_or_default(),
&buffer_text,
),
text: buffer_text,
text_before: eval.input_content.unwrap_or_default(),
text_after: buffer_text,
};
let assertion = eval
.assertion

View File

@@ -1,375 +0,0 @@
use crate::commit::get_messages;
use crate::{GitRemote, Oid};
use anyhow::{Context as _, Result, anyhow};
use collections::{HashMap, HashSet};
use futures::AsyncWriteExt;
use gpui::SharedString;
use serde::{Deserialize, Serialize};
use std::process::Stdio;
use std::{ops::Range, path::Path};
use text::Rope;
use time::OffsetDateTime;
use time::UtcOffset;
use time::macros::format_description;
pub use git2 as libgit;
#[derive(Debug, Clone, Default)]
pub struct Blame {
pub entries: Vec<BlameEntry>,
pub messages: HashMap<Oid, String>,
pub remote_url: Option<String>,
}
#[derive(Clone, Debug, Default)]
pub struct ParsedCommitMessage {
pub message: SharedString,
pub permalink: Option<url::Url>,
pub pull_request: Option<crate::hosting_provider::PullRequest>,
pub remote: Option<GitRemote>,
}
impl Blame {
pub async fn for_path(
git_binary: &Path,
working_directory: &Path,
path: &Path,
content: &Rope,
remote_url: Option<String>,
) -> Result<Self> {
let output = run_git_blame(git_binary, working_directory, path, content).await?;
let mut entries = parse_git_blame(&output)?;
entries.sort_unstable_by(|a, b| a.range.start.cmp(&b.range.start));
let mut unique_shas = HashSet::default();
for entry in entries.iter_mut() {
unique_shas.insert(entry.sha);
}
let shas = unique_shas.into_iter().collect::<Vec<_>>();
let messages = get_messages(working_directory, &shas)
.await
.context("failed to get commit messages")?;
Ok(Self {
entries,
messages,
remote_url,
})
}
}
const GIT_BLAME_NO_COMMIT_ERROR: &str = "fatal: no such ref: HEAD";
const GIT_BLAME_NO_PATH: &str = "fatal: no such path";
async fn run_git_blame(
git_binary: &Path,
working_directory: &Path,
path: &Path,
contents: &Rope,
) -> Result<String> {
let mut child = util::command::new_smol_command(git_binary)
.current_dir(working_directory)
.arg("blame")
.arg("--incremental")
.arg("--contents")
.arg("-")
.arg(path.as_os_str())
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.context("starting git blame process")?;
let stdin = child
.stdin
.as_mut()
.context("failed to get pipe to stdin of git blame command")?;
for chunk in contents.chunks() {
stdin.write_all(chunk.as_bytes()).await?;
}
stdin.flush().await?;
let output = child.output().await.context("reading git blame output")?;
handle_command_output(output)
}
fn handle_command_output(output: std::process::Output) -> Result<String> {
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
let trimmed = stderr.trim();
if trimmed == GIT_BLAME_NO_COMMIT_ERROR || trimmed.contains(GIT_BLAME_NO_PATH) {
return Ok(String::new());
}
anyhow::bail!("git blame process failed: {stderr}");
}
Ok(String::from_utf8(output.stdout)?)
}
#[derive(Serialize, Deserialize, Default, Debug, Clone, PartialEq, Eq)]
pub struct BlameEntry {
pub sha: Oid,
pub range: Range<u32>,
pub original_line_number: u32,
pub author: Option<String>,
pub author_mail: Option<String>,
pub author_time: Option<i64>,
pub author_tz: Option<String>,
pub committer_name: Option<String>,
pub committer_email: Option<String>,
pub committer_time: Option<i64>,
pub committer_tz: Option<String>,
pub summary: Option<String>,
pub previous: Option<String>,
pub filename: String,
}
impl BlameEntry {
// Returns a BlameEntry by parsing the first line of a `git blame --incremental`
// entry. The line MUST have this format:
//
// <40-byte-hex-sha1> <sourceline> <resultline> <num-lines>
fn new_from_blame_line(line: &str) -> Result<BlameEntry> {
let mut parts = line.split_whitespace();
let sha = parts
.next()
.and_then(|line| line.parse::<Oid>().ok())
.with_context(|| format!("parsing sha from {line}"))?;
let original_line_number = parts
.next()
.and_then(|line| line.parse::<u32>().ok())
.with_context(|| format!("parsing original line number from {line}"))?;
let final_line_number = parts
.next()
.and_then(|line| line.parse::<u32>().ok())
.with_context(|| format!("parsing final line number from {line}"))?;
let line_count = parts
.next()
.and_then(|line| line.parse::<u32>().ok())
.with_context(|| format!("parsing line count from {line}"))?;
let start_line = final_line_number.saturating_sub(1);
let end_line = start_line + line_count;
let range = start_line..end_line;
Ok(Self {
sha,
range,
original_line_number,
..Default::default()
})
}
pub fn author_offset_date_time(&self) -> Result<time::OffsetDateTime> {
if let (Some(author_time), Some(author_tz)) = (self.author_time, &self.author_tz) {
let format = format_description!("[offset_hour][offset_minute]");
let offset = UtcOffset::parse(author_tz, &format)?;
let date_time_utc = OffsetDateTime::from_unix_timestamp(author_time)?;
Ok(date_time_utc.to_offset(offset))
} else {
// Directly return current time in UTC if there's no committer time or timezone
Ok(time::OffsetDateTime::now_utc())
}
}
}
// parse_git_blame parses the output of `git blame --incremental`, which returns
// all the blame-entries for a given path incrementally, as it finds them.
//
// Each entry *always* starts with:
//
// <40-byte-hex-sha1> <sourceline> <resultline> <num-lines>
//
// Each entry *always* ends with:
//
// filename <whitespace-quoted-filename-goes-here>
//
// Line numbers are 1-indexed.
//
// A `git blame --incremental` entry looks like this:
//
// 6ad46b5257ba16d12c5ca9f0d4900320959df7f4 2 2 1
// author Joe Schmoe
// author-mail <joe.schmoe@example.com>
// author-time 1709741400
// author-tz +0100
// committer Joe Schmoe
// committer-mail <joe.schmoe@example.com>
// committer-time 1709741400
// committer-tz +0100
// summary Joe's cool commit
// previous 486c2409237a2c627230589e567024a96751d475 index.js
// filename index.js
//
// If the entry has the same SHA as an entry that was already printed then no
// signature information is printed:
//
// 6ad46b5257ba16d12c5ca9f0d4900320959df7f4 3 4 1
// previous 486c2409237a2c627230589e567024a96751d475 index.js
// filename index.js
//
// More about `--incremental` output: https://mirrors.edge.kernel.org/pub/software/scm/git/docs/git-blame.html
fn parse_git_blame(output: &str) -> Result<Vec<BlameEntry>> {
let mut entries: Vec<BlameEntry> = Vec::new();
let mut index: HashMap<Oid, usize> = HashMap::default();
let mut current_entry: Option<BlameEntry> = None;
for line in output.lines() {
let mut done = false;
match &mut current_entry {
None => {
let mut new_entry = BlameEntry::new_from_blame_line(line)?;
if let Some(existing_entry) = index
.get(&new_entry.sha)
.and_then(|slot| entries.get(*slot))
{
new_entry.author.clone_from(&existing_entry.author);
new_entry
.author_mail
.clone_from(&existing_entry.author_mail);
new_entry.author_time = existing_entry.author_time;
new_entry.author_tz.clone_from(&existing_entry.author_tz);
new_entry
.committer_name
.clone_from(&existing_entry.committer_name);
new_entry
.committer_email
.clone_from(&existing_entry.committer_email);
new_entry.committer_time = existing_entry.committer_time;
new_entry
.committer_tz
.clone_from(&existing_entry.committer_tz);
new_entry.summary.clone_from(&existing_entry.summary);
}
current_entry.replace(new_entry);
}
Some(entry) => {
let Some((key, value)) = line.split_once(' ') else {
continue;
};
let is_committed = !entry.sha.is_zero();
match key {
"filename" => {
entry.filename = value.into();
done = true;
}
"previous" => entry.previous = Some(value.into()),
"summary" if is_committed => entry.summary = Some(value.into()),
"author" if is_committed => entry.author = Some(value.into()),
"author-mail" if is_committed => entry.author_mail = Some(value.into()),
"author-time" if is_committed => {
entry.author_time = Some(value.parse::<i64>()?)
}
"author-tz" if is_committed => entry.author_tz = Some(value.into()),
"committer" if is_committed => entry.committer_name = Some(value.into()),
"committer-mail" if is_committed => entry.committer_email = Some(value.into()),
"committer-time" if is_committed => {
entry.committer_time = Some(value.parse::<i64>()?)
}
"committer-tz" if is_committed => entry.committer_tz = Some(value.into()),
_ => {}
}
}
};
if done {
if let Some(entry) = current_entry.take() {
index.insert(entry.sha, entries.len());
// We only want annotations that have a commit.
if !entry.sha.is_zero() {
entries.push(entry);
}
}
}
}
Ok(entries)
}
#[cfg(test)]
mod tests {
use std::path::PathBuf;
use super::BlameEntry;
use super::parse_git_blame;
fn read_test_data(filename: &str) -> String {
let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
path.push("test_data");
path.push(filename);
std::fs::read_to_string(&path)
.unwrap_or_else(|_| panic!("Could not read test data at {:?}. Is it generated?", path))
}
fn assert_eq_golden(entries: &Vec<BlameEntry>, golden_filename: &str) {
let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
path.push("test_data");
path.push("golden");
path.push(format!("{}.json", golden_filename));
let mut have_json =
serde_json::to_string_pretty(&entries).expect("could not serialize entries to JSON");
// We always want to save with a trailing newline.
have_json.push('\n');
let update = std::env::var("UPDATE_GOLDEN")
.map(|val| val.eq_ignore_ascii_case("true"))
.unwrap_or(false);
if update {
std::fs::create_dir_all(path.parent().unwrap())
.expect("could not create golden test data directory");
std::fs::write(&path, have_json).expect("could not write out golden data");
} else {
let want_json =
std::fs::read_to_string(&path).unwrap_or_else(|_| {
panic!("could not read golden test data file at {:?}. Did you run the test with UPDATE_GOLDEN=true before?", path);
}).replace("\r\n", "\n");
pretty_assertions::assert_eq!(have_json, want_json, "wrong blame entries");
}
}
#[test]
fn test_parse_git_blame_not_committed() {
let output = read_test_data("blame_incremental_not_committed");
let entries = parse_git_blame(&output).unwrap();
assert_eq_golden(&entries, "blame_incremental_not_committed");
}
#[test]
fn test_parse_git_blame_simple() {
let output = read_test_data("blame_incremental_simple");
let entries = parse_git_blame(&output).unwrap();
assert_eq_golden(&entries, "blame_incremental_simple");
}
#[test]
fn test_parse_git_blame_complex() {
let output = read_test_data("blame_incremental_complex");
let entries = parse_git_blame(&output).unwrap();
assert_eq_golden(&entries, "blame_incremental_complex");
}
}

View File

@@ -0,0 +1,11 @@
@@ -94,6 +94,10 @@
let output = child.output().await.context("reading git blame output")?;
+ handle_command_output(output)
+}
+
+fn handle_command_output(output: std::process::Output) -> Result<String> {
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
let trimmed = stderr.trim();

View File

@@ -0,0 +1,26 @@
@@ -95,15 +95,19 @@
let output = child.output().await.context("reading git blame output")?;
if !output.status.success() {
- let stderr = String::from_utf8_lossy(&output.stderr);
- let trimmed = stderr.trim();
- if trimmed == GIT_BLAME_NO_COMMIT_ERROR || trimmed.contains(GIT_BLAME_NO_PATH) {
- return Ok(String::new());
- }
- anyhow::bail!("git blame process failed: {stderr}");
+ return handle_command_output(output);
}
Ok(String::from_utf8(output.stdout)?)
+}
+
+fn handle_command_output(output: std::process::Output) -> Result<String> {
+ let stderr = String::from_utf8_lossy(&output.stderr);
+ let trimmed = stderr.trim();
+ if trimmed == GIT_BLAME_NO_COMMIT_ERROR || trimmed.contains(GIT_BLAME_NO_PATH) {
+ return Ok(String::new());
+ }
+ anyhow::bail!("git blame process failed: {stderr}");
}
#[derive(Serialize, Deserialize, Default, Debug, Clone, PartialEq, Eq)]

View File

@@ -0,0 +1,11 @@
@@ -93,7 +93,10 @@
stdin.flush().await?;
let output = child.output().await.context("reading git blame output")?;
+ handle_command_output(output)
+}
+fn handle_command_output(output: std::process::Output) -> Result<String> {
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
let trimmed = stderr.trim();

View File

@@ -0,0 +1,24 @@
@@ -93,17 +93,20 @@
stdin.flush().await?;
let output = child.output().await.context("reading git blame output")?;
+ handle_command_output(&output)?;
+ Ok(String::from_utf8(output.stdout)?)
+}
+fn handle_command_output(output: &std::process::Output) -> Result<()> {
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
let trimmed = stderr.trim();
if trimmed == GIT_BLAME_NO_COMMIT_ERROR || trimmed.contains(GIT_BLAME_NO_PATH) {
- return Ok(String::new());
+ return Ok(());
}
anyhow::bail!("git blame process failed: {stderr}");
}
-
- Ok(String::from_utf8(output.stdout)?)
+ Ok(())
}
#[derive(Serialize, Deserialize, Default, Debug, Clone, PartialEq, Eq)]

View File

@@ -0,0 +1,26 @@
@@ -95,15 +95,19 @@
let output = child.output().await.context("reading git blame output")?;
if !output.status.success() {
- let stderr = String::from_utf8_lossy(&output.stderr);
- let trimmed = stderr.trim();
- if trimmed == GIT_BLAME_NO_COMMIT_ERROR || trimmed.contains(GIT_BLAME_NO_PATH) {
- return Ok(String::new());
- }
- anyhow::bail!("git blame process failed: {stderr}");
+ return handle_command_output(&output);
}
Ok(String::from_utf8(output.stdout)?)
+}
+
+fn handle_command_output(output: &std::process::Output) -> Result<String> {
+ let stderr = String::from_utf8_lossy(&output.stderr);
+ let trimmed = stderr.trim();
+ if trimmed == GIT_BLAME_NO_COMMIT_ERROR || trimmed.contains(GIT_BLAME_NO_PATH) {
+ return Ok(String::new());
+ }
+ anyhow::bail!("git blame process failed: {stderr}");
}
#[derive(Serialize, Deserialize, Default, Debug, Clone, PartialEq, Eq)]

View File

@@ -0,0 +1,23 @@
@@ -93,7 +93,12 @@
stdin.flush().await?;
let output = child.output().await.context("reading git blame output")?;
+ handle_command_output(&output)?;
+ Ok(String::from_utf8(output.stdout)?)
+}
+
+fn handle_command_output(output: &std::process::Output) -> Result<String> {
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
let trimmed = stderr.trim();
@@ -102,8 +107,7 @@
}
anyhow::bail!("git blame process failed: {stderr}");
}
-
- Ok(String::from_utf8(output.stdout)?)
+ Ok(String::from_utf8_lossy(&output.stdout).into_owned())
}
#[derive(Serialize, Deserialize, Default, Debug, Clone, PartialEq, Eq)]

View File

@@ -0,0 +1,26 @@
@@ -95,15 +95,19 @@
let output = child.output().await.context("reading git blame output")?;
if !output.status.success() {
- let stderr = String::from_utf8_lossy(&output.stderr);
- let trimmed = stderr.trim();
- if trimmed == GIT_BLAME_NO_COMMIT_ERROR || trimmed.contains(GIT_BLAME_NO_PATH) {
- return Ok(String::new());
- }
- anyhow::bail!("git blame process failed: {stderr}");
+ return handle_command_output(output);
}
Ok(String::from_utf8(output.stdout)?)
+}
+
+fn handle_command_output(output: std::process::Output) -> Result<String> {
+ let stderr = String::from_utf8_lossy(&output.stderr);
+ let trimmed = stderr.trim();
+ if trimmed == GIT_BLAME_NO_COMMIT_ERROR || trimmed.contains(GIT_BLAME_NO_PATH) {
+ return Ok(String::new());
+ }
+ anyhow::bail!("git blame process failed: {stderr}");
}
#[derive(Serialize, Deserialize, Default, Debug, Clone, PartialEq, Eq)]

View File

@@ -0,0 +1,26 @@
@@ -95,15 +95,19 @@
let output = child.output().await.context("reading git blame output")?;
if !output.status.success() {
- let stderr = String::from_utf8_lossy(&output.stderr);
- let trimmed = stderr.trim();
- if trimmed == GIT_BLAME_NO_COMMIT_ERROR || trimmed.contains(GIT_BLAME_NO_PATH) {
- return Ok(String::new());
- }
- anyhow::bail!("git blame process failed: {stderr}");
+ return handle_command_output(output);
}
Ok(String::from_utf8(output.stdout)?)
+}
+
+fn handle_command_output(output: std::process::Output) -> Result<String> {
+ let stderr = String::from_utf8_lossy(&output.stderr);
+ let trimmed = stderr.trim();
+ if trimmed == GIT_BLAME_NO_COMMIT_ERROR || trimmed.contains(GIT_BLAME_NO_PATH) {
+ return Ok(String::new());
+ }
+ anyhow::bail!("git blame process failed: {stderr}")
}
#[derive(Serialize, Deserialize, Default, Debug, Clone, PartialEq, Eq)]

View File

@@ -1,12 +1,13 @@
You are an expert engineer and your task is to write a new file from scratch.
<file_to_edit>
You MUST respond directly with the file's content, without explanations, additional text or triple backticks.
The text you output will be saved verbatim as the content of the file.
Tool calls have been disabled. You MUST start your response directly with the file's new content.
<file_path>
{{path}}
</file_to_edit>
</file_path>
<edit_description>
{{edit_description}}
</edit_description>
You MUST respond directly with the file's content, without explanations, additional text or triple backticks.
The text you output will be saved verbatim as the content of the file.

View File

@@ -27,20 +27,57 @@ NEW TEXT 3 HERE
</edits>
```
Rules for editing:
# File Editing Instructions
- Use `<old_text>` and `<new_text>` tags to replace content
- `<old_text>` must exactly match existing file content, including indentation
- `<old_text>` must come from the actual file, not an outline
- `<old_text>` cannot be empty
- Be minimal with replacements:
- For unique lines, include only those lines
- For non-unique lines, include enough context to identify them
- Do not escape quotes, newlines, or other characters within tags
- For multiple occurrences, repeat the same tag pair for each instance
- Edits are sequential - each assumes previous edits are already applied
- Only edit the specified file
- Always close all tags properly
{{!-- This example is important for Gemini 2.5 --}}
<example>
<edits>
<old_text>
struct User {
name: String,
email: String,
}
</old_text>
<new_text>
struct User {
name: String,
email: String,
active: bool,
}
</new_text>
<old_text>
let user = User {
name: String::from("John"),
email: String::from("john@example.com"),
};
</old_text>
<new_text>
let user = User {
name: String::from("John"),
email: String::from("john@example.com"),
active: true,
};
</new_text>
</edits>
</example>
- `old_text` represents lines in the input file that will be replaced with `new_text`.
- `old_text` MUST exactly match the existing file content, character for character, including indentation.
- `old_text` MUST NEVER come from the outline, but from actual lines in the file.
- Strive to be minimal in the lines you replace in `old_text`:
- If the lines you want to replace are unique, you MUST include just those in the `old_text`.
- If the lines you want to replace are NOT unique, you MUST include enough context around them in `old_text` to distinguish them from other lines.
- If you want to replace many occurrences of the same text, repeat the same `old_text`/`new_text` pair multiple times and I will apply them sequentially, one occurrence at a time.
- When reporting multiple edits, each edit assumes the previous one has already been applied! Therefore, you must ensure `old_text` doesn't reference text that has already been modified by a previous edit.
- Don't explain the edits, just report them.
- Only edit the file specified in `<file_to_edit>` and NEVER include edits to other files!
- If you open an <old_text> tag, you MUST close it using </old_text>
- If you open an <new_text> tag, you MUST close it using </new_text>
<file_to_edit>
{{path}}

View File

@@ -39,7 +39,7 @@ struct UpdateRequestBody {
destination: &'static str,
}
#[derive(Clone, PartialEq, Eq)]
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum VersionCheckType {
Sha(String),
Semantic(SemanticVersion),
@@ -491,62 +491,43 @@ impl AutoUpdater {
Self::get_release(this, asset, os, arch, None, release_channel, cx).await
}
fn installed_update_version(&self) -> Option<VersionCheckType> {
match &self.status {
AutoUpdateStatus::Updated { version, .. } => Some(version.clone()),
_ => None,
}
}
async fn update(this: Entity<Self>, mut cx: AsyncApp) -> Result<()> {
let (client, current_version, installed_update_version, release_channel) =
let (client, installed_version, previous_status, release_channel) =
this.update(&mut cx, |this, cx| {
this.status = AutoUpdateStatus::Checking;
cx.notify();
(
this.http_client.clone(),
this.current_version,
this.installed_update_version(),
this.status.clone(),
ReleaseChannel::try_global(cx),
)
})?;
let release =
this.update(&mut cx, |this, cx| {
this.status = AutoUpdateStatus::Checking;
cx.notify();
})?;
let fetched_release_data =
Self::get_latest_release(&this, "zed", OS, ARCH, release_channel, &mut cx).await?;
let fetched_version = fetched_release_data.clone().version;
let app_commit_sha = cx.update(|cx| AppCommitSha::try_global(cx).map(|sha| sha.0));
let newer_version = Self::check_for_newer_version(
*RELEASE_CHANNEL,
app_commit_sha,
installed_version,
previous_status.clone(),
fetched_version,
)?;
let update_version_to_install = match *RELEASE_CHANNEL {
ReleaseChannel::Nightly => {
let should_download = cx
.update(|cx| AppCommitSha::try_global(cx).map(|sha| release.version != sha.0))
.ok()
.flatten()
.unwrap_or(true);
should_download.then(|| VersionCheckType::Sha(release.version.clone()))
}
_ => {
let installed_version =
installed_update_version.unwrap_or(VersionCheckType::Semantic(current_version));
match installed_version {
VersionCheckType::Sha(_) => {
log::warn!("Unexpected SHA-based version in non-nightly build");
Some(installed_version)
}
VersionCheckType::Semantic(semantic_comparison_version) => {
let latest_release_version = release.version.parse::<SemanticVersion>()?;
let should_download = latest_release_version > semantic_comparison_version;
should_download.then(|| VersionCheckType::Semantic(latest_release_version))
}
}
}
};
let Some(update_version) = update_version_to_install else {
this.update(&mut cx, |this, cx| {
this.status = AutoUpdateStatus::Idle;
let Some(newer_version) = newer_version else {
return this.update(&mut cx, |this, cx| {
let status = match previous_status {
AutoUpdateStatus::Updated { .. } => previous_status,
_ => AutoUpdateStatus::Idle,
};
this.status = status;
cx.notify();
})?;
return Ok(());
});
};
this.update(&mut cx, |this, cx| {
@@ -555,6 +536,71 @@ impl AutoUpdater {
})?;
let installer_dir = InstallerDir::new().await?;
let target_path = Self::target_path(&installer_dir).await?;
download_release(&target_path, fetched_release_data, client, &cx).await?;
this.update(&mut cx, |this, cx| {
this.status = AutoUpdateStatus::Installing;
cx.notify();
})?;
let binary_path = Self::binary_path(installer_dir, target_path, &cx).await?;
this.update(&mut cx, |this, cx| {
this.set_should_show_update_notification(true, cx)
.detach_and_log_err(cx);
this.status = AutoUpdateStatus::Updated {
binary_path,
version: newer_version,
};
cx.notify();
})
}
fn check_for_newer_version(
release_channel: ReleaseChannel,
app_commit_sha: Result<Option<String>>,
installed_version: SemanticVersion,
status: AutoUpdateStatus,
fetched_version: String,
) -> Result<Option<VersionCheckType>> {
let parsed_fetched_version = fetched_version.parse::<SemanticVersion>();
if let AutoUpdateStatus::Updated { version, .. } = status {
match version {
VersionCheckType::Sha(cached_version) => {
let should_download = fetched_version != cached_version;
let newer_version =
should_download.then(|| VersionCheckType::Sha(fetched_version));
return Ok(newer_version);
}
VersionCheckType::Semantic(cached_version) => {
return Self::check_for_newer_version_non_nightly(
cached_version,
parsed_fetched_version?,
);
}
}
}
match release_channel {
ReleaseChannel::Nightly => {
let should_download = app_commit_sha
.ok()
.flatten()
.map(|sha| fetched_version != sha)
.unwrap_or(true);
let newer_version = should_download.then(|| VersionCheckType::Sha(fetched_version));
Ok(newer_version)
}
_ => Self::check_for_newer_version_non_nightly(
installed_version,
parsed_fetched_version?,
),
}
}
async fn target_path(installer_dir: &InstallerDir) -> Result<PathBuf> {
let filename = match OS {
"macos" => anyhow::Ok("Zed.dmg"),
"linux" => Ok("zed.tar.gz"),
@@ -568,32 +614,29 @@ impl AutoUpdater {
"Aborting. Could not find rsync which is required for auto-updates."
);
let downloaded_asset = installer_dir.path().join(filename);
download_release(&downloaded_asset, release.clone(), client, &cx).await?;
Ok(installer_dir.path().join(filename))
}
this.update(&mut cx, |this, cx| {
this.status = AutoUpdateStatus::Installing;
cx.notify();
})?;
let binary_path = match OS {
"macos" => install_release_macos(&installer_dir, downloaded_asset, &cx).await,
"linux" => install_release_linux(&installer_dir, downloaded_asset, &cx).await,
"windows" => install_release_windows(downloaded_asset).await,
async fn binary_path(
installer_dir: InstallerDir,
target_path: PathBuf,
cx: &AsyncApp,
) -> Result<PathBuf> {
match OS {
"macos" => install_release_macos(&installer_dir, target_path, cx).await,
"linux" => install_release_linux(&installer_dir, target_path, cx).await,
"windows" => install_release_windows(target_path).await,
unsupported_os => anyhow::bail!("not supported: {unsupported_os}"),
}?;
}
}
this.update(&mut cx, |this, cx| {
this.set_should_show_update_notification(true, cx)
.detach_and_log_err(cx);
this.status = AutoUpdateStatus::Updated {
binary_path,
version: update_version,
};
cx.notify();
})?;
Ok(())
fn check_for_newer_version_non_nightly(
installed_version: SemanticVersion,
fetched_version: SemanticVersion,
) -> Result<Option<VersionCheckType>> {
let should_download = fetched_version > installed_version;
let newer_version = should_download.then(|| VersionCheckType::Semantic(fetched_version));
Ok(newer_version)
}
pub fn set_should_show_update_notification(
@@ -868,3 +911,255 @@ pub fn check_pending_installation() -> bool {
}
false
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_stable_does_not_update_when_fetched_version_is_not_higher() {
let release_channel = ReleaseChannel::Stable;
let app_commit_sha = Ok(Some("a".to_string()));
let installed_version = SemanticVersion::new(1, 0, 0);
let status = AutoUpdateStatus::Idle;
let fetched_version = SemanticVersion::new(1, 0, 0);
let newer_version = AutoUpdater::check_for_newer_version(
release_channel,
app_commit_sha,
installed_version,
status,
fetched_version.to_string(),
);
assert_eq!(newer_version.unwrap(), None);
}
#[test]
fn test_stable_does_update_when_fetched_version_is_higher() {
let release_channel = ReleaseChannel::Stable;
let app_commit_sha = Ok(Some("a".to_string()));
let installed_version = SemanticVersion::new(1, 0, 0);
let status = AutoUpdateStatus::Idle;
let fetched_version = SemanticVersion::new(1, 0, 1);
let newer_version = AutoUpdater::check_for_newer_version(
release_channel,
app_commit_sha,
installed_version,
status,
fetched_version.to_string(),
);
assert_eq!(
newer_version.unwrap(),
Some(VersionCheckType::Semantic(fetched_version))
);
}
#[test]
fn test_stable_does_not_update_when_fetched_version_is_not_higher_than_cached() {
let release_channel = ReleaseChannel::Stable;
let app_commit_sha = Ok(Some("a".to_string()));
let installed_version = SemanticVersion::new(1, 0, 0);
let status = AutoUpdateStatus::Updated {
binary_path: PathBuf::new(),
version: VersionCheckType::Semantic(SemanticVersion::new(1, 0, 1)),
};
let fetched_version = SemanticVersion::new(1, 0, 1);
let newer_version = AutoUpdater::check_for_newer_version(
release_channel,
app_commit_sha,
installed_version,
status,
fetched_version.to_string(),
);
assert_eq!(newer_version.unwrap(), None);
}
#[test]
fn test_stable_does_update_when_fetched_version_is_higher_than_cached() {
let release_channel = ReleaseChannel::Stable;
let app_commit_sha = Ok(Some("a".to_string()));
let installed_version = SemanticVersion::new(1, 0, 0);
let status = AutoUpdateStatus::Updated {
binary_path: PathBuf::new(),
version: VersionCheckType::Semantic(SemanticVersion::new(1, 0, 1)),
};
let fetched_version = SemanticVersion::new(1, 0, 2);
let newer_version = AutoUpdater::check_for_newer_version(
release_channel,
app_commit_sha,
installed_version,
status,
fetched_version.to_string(),
);
assert_eq!(
newer_version.unwrap(),
Some(VersionCheckType::Semantic(fetched_version))
);
}
#[test]
fn test_nightly_does_not_update_when_fetched_sha_is_same() {
let release_channel = ReleaseChannel::Nightly;
let app_commit_sha = Ok(Some("a".to_string()));
let installed_version = SemanticVersion::new(1, 0, 0);
let status = AutoUpdateStatus::Idle;
let fetched_sha = "a".to_string();
let newer_version = AutoUpdater::check_for_newer_version(
release_channel,
app_commit_sha,
installed_version,
status,
fetched_sha,
);
assert_eq!(newer_version.unwrap(), None);
}
#[test]
fn test_nightly_does_update_when_fetched_sha_is_not_same() {
let release_channel = ReleaseChannel::Nightly;
let app_commit_sha = Ok(Some("a".to_string()));
let installed_version = SemanticVersion::new(1, 0, 0);
let status = AutoUpdateStatus::Idle;
let fetched_sha = "b".to_string();
let newer_version = AutoUpdater::check_for_newer_version(
release_channel,
app_commit_sha,
installed_version,
status,
fetched_sha.clone(),
);
assert_eq!(
newer_version.unwrap(),
Some(VersionCheckType::Sha(fetched_sha))
);
}
#[test]
fn test_nightly_does_not_update_when_fetched_sha_is_same_as_cached() {
let release_channel = ReleaseChannel::Nightly;
let app_commit_sha = Ok(Some("a".to_string()));
let installed_version = SemanticVersion::new(1, 0, 0);
let status = AutoUpdateStatus::Updated {
binary_path: PathBuf::new(),
version: VersionCheckType::Sha("b".to_string()),
};
let fetched_sha = "b".to_string();
let newer_version = AutoUpdater::check_for_newer_version(
release_channel,
app_commit_sha,
installed_version,
status,
fetched_sha,
);
assert_eq!(newer_version.unwrap(), None);
}
#[test]
fn test_nightly_does_update_when_fetched_sha_is_not_same_as_cached() {
let release_channel = ReleaseChannel::Nightly;
let app_commit_sha = Ok(Some("a".to_string()));
let installed_version = SemanticVersion::new(1, 0, 0);
let status = AutoUpdateStatus::Updated {
binary_path: PathBuf::new(),
version: VersionCheckType::Sha("b".to_string()),
};
let fetched_sha = "c".to_string();
let newer_version = AutoUpdater::check_for_newer_version(
release_channel,
app_commit_sha,
installed_version,
status,
fetched_sha.clone(),
);
assert_eq!(
newer_version.unwrap(),
Some(VersionCheckType::Sha(fetched_sha))
);
}
#[test]
fn test_nightly_does_update_when_installed_versions_sha_cannot_be_retrieved() {
let release_channel = ReleaseChannel::Nightly;
let app_commit_sha = Ok(None);
let installed_version = SemanticVersion::new(1, 0, 0);
let status = AutoUpdateStatus::Idle;
let fetched_sha = "a".to_string();
let newer_version = AutoUpdater::check_for_newer_version(
release_channel,
app_commit_sha,
installed_version,
status,
fetched_sha.clone(),
);
assert_eq!(
newer_version.unwrap(),
Some(VersionCheckType::Sha(fetched_sha))
);
}
#[test]
fn test_nightly_does_not_update_when_cached_update_is_same_as_fetched_and_installed_versions_sha_cannot_be_retrieved()
{
let release_channel = ReleaseChannel::Nightly;
let app_commit_sha = Ok(None);
let installed_version = SemanticVersion::new(1, 0, 0);
let status = AutoUpdateStatus::Updated {
binary_path: PathBuf::new(),
version: VersionCheckType::Sha("b".to_string()),
};
let fetched_sha = "b".to_string();
let newer_version = AutoUpdater::check_for_newer_version(
release_channel,
app_commit_sha,
installed_version,
status,
fetched_sha,
);
assert_eq!(newer_version.unwrap(), None);
}
#[test]
fn test_nightly_does_update_when_cached_update_is_not_same_as_fetched_and_installed_versions_sha_cannot_be_retrieved()
{
let release_channel = ReleaseChannel::Nightly;
let app_commit_sha = Ok(None);
let installed_version = SemanticVersion::new(1, 0, 0);
let status = AutoUpdateStatus::Updated {
binary_path: PathBuf::new(),
version: VersionCheckType::Sha("b".to_string()),
};
let fetched_sha = "c".to_string();
let newer_version = AutoUpdater::check_for_newer_version(
release_channel,
app_commit_sha,
installed_version,
status,
fetched_sha.clone(),
);
assert_eq!(
newer_version.unwrap(),
Some(VersionCheckType::Sha(fetched_sha))
);
}
}

View File

@@ -15,6 +15,20 @@ pub enum BedrockModelMode {
#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, EnumIter)]
pub enum Model {
// Anthropic models (already included)
#[serde(rename = "claude-sonnet-4", alias = "claude-sonnet-4-latest")]
ClaudeSonnet4,
#[serde(
rename = "claude-sonnet-4-thinking",
alias = "claude-sonnet-4-thinking-latest"
)]
ClaudeSonnet4Thinking,
#[serde(rename = "claude-opus-4", alias = "claude-opus-4-latest")]
ClaudeOpus4,
#[serde(
rename = "claude-opus-4-thinking",
alias = "claude-opus-4-thinking-latest"
)]
ClaudeOpus4Thinking,
#[default]
#[serde(rename = "claude-3-5-sonnet-v2", alias = "claude-3-5-sonnet-latest")]
Claude3_5SonnetV2,
@@ -112,6 +126,12 @@ impl Model {
pub fn id(&self) -> &str {
match self {
Model::ClaudeSonnet4 | Model::ClaudeSonnet4Thinking => {
"anthropic.claude-sonnet-4-20250514-v1:0"
}
Model::ClaudeOpus4 | Model::ClaudeOpus4Thinking => {
"anthropic.claude-opus-4-20250514-v1:0"
}
Model::Claude3_5SonnetV2 => "anthropic.claude-3-5-sonnet-20241022-v2:0",
Model::Claude3_5Sonnet => "anthropic.claude-3-5-sonnet-20240620-v1:0",
Model::Claude3Opus => "anthropic.claude-3-opus-20240229-v1:0",
@@ -163,6 +183,10 @@ impl Model {
pub fn display_name(&self) -> &str {
match self {
Self::ClaudeSonnet4 => "Claude Sonnet 4",
Self::ClaudeSonnet4Thinking => "Claude Sonnet 4 Thinking",
Self::ClaudeOpus4 => "Claude Opus 4",
Self::ClaudeOpus4Thinking => "Claude Opus 4 Thinking",
Self::Claude3_5SonnetV2 => "Claude 3.5 Sonnet v2",
Self::Claude3_5Sonnet => "Claude 3.5 Sonnet",
Self::Claude3Opus => "Claude 3 Opus",
@@ -219,7 +243,9 @@ impl Model {
| Self::Claude3Opus
| Self::Claude3Sonnet
| Self::Claude3_5Haiku
| Self::Claude3_7Sonnet => 200_000,
| Self::Claude3_7Sonnet
| Self::ClaudeSonnet4
| Self::ClaudeOpus4 => 200_000,
Self::AmazonNovaPremier => 1_000_000,
Self::PalmyraWriterX5 => 1_000_000,
Self::PalmyraWriterX4 => 128_000,
@@ -231,7 +257,12 @@ impl Model {
pub fn max_output_tokens(&self) -> u32 {
match self {
Self::Claude3Opus | Self::Claude3Sonnet | Self::Claude3_5Haiku => 4_096,
Self::Claude3_7Sonnet | Self::Claude3_7SonnetThinking => 128_000,
Self::Claude3_7Sonnet
| Self::Claude3_7SonnetThinking
| Self::ClaudeSonnet4
| Self::ClaudeSonnet4Thinking
| Self::ClaudeOpus4
| Model::ClaudeOpus4Thinking => 128_000,
Self::Claude3_5SonnetV2 | Self::PalmyraWriterX4 | Self::PalmyraWriterX5 => 8_192,
Self::Custom {
max_output_tokens, ..
@@ -246,7 +277,11 @@ impl Model {
| Self::Claude3Opus
| Self::Claude3Sonnet
| Self::Claude3_5Haiku
| Self::Claude3_7Sonnet => 1.0,
| Self::Claude3_7Sonnet
| Self::ClaudeOpus4
| Self::ClaudeOpus4Thinking
| Self::ClaudeSonnet4
| Self::ClaudeSonnet4Thinking => 1.0,
Self::Custom {
default_temperature,
..
@@ -264,6 +299,10 @@ impl Model {
| Self::Claude3_5SonnetV2
| Self::Claude3_7Sonnet
| Self::Claude3_7SonnetThinking
| Self::ClaudeOpus4
| Self::ClaudeOpus4Thinking
| Self::ClaudeSonnet4
| Self::ClaudeSonnet4Thinking
| Self::Claude3_5Haiku => true,
// Amazon Nova models (all support tool use)
@@ -289,6 +328,12 @@ impl Model {
Model::Claude3_7SonnetThinking => BedrockModelMode::Thinking {
budget_tokens: Some(4096),
},
Model::ClaudeSonnet4Thinking => BedrockModelMode::Thinking {
budget_tokens: Some(4096),
},
Model::ClaudeOpus4Thinking => BedrockModelMode::Thinking {
budget_tokens: Some(4096),
},
_ => BedrockModelMode::Default,
}
}
@@ -324,6 +369,10 @@ impl Model {
(Model::Claude3Opus, "us")
| (Model::Claude3_5Haiku, "us")
| (Model::Claude3_7Sonnet, "us")
| (Model::ClaudeSonnet4, "us")
| (Model::ClaudeOpus4, "us")
| (Model::ClaudeSonnet4Thinking, "us")
| (Model::ClaudeOpus4Thinking, "us")
| (Model::Claude3_7SonnetThinking, "us")
| (Model::AmazonNovaPremier, "us")
| (Model::MistralPixtralLarge2502V1, "us") => {

View File

@@ -3,7 +3,10 @@
use anyhow::{Context as _, Result};
use http_client::Url;
use tokio::net::TcpStream;
use tokio_socks::tcp::{Socks4Stream, Socks5Stream};
use tokio_socks::{
IntoTargetAddr, TargetAddr,
tcp::{Socks4Stream, Socks5Stream},
};
use super::AsyncReadWrite;
@@ -23,8 +26,14 @@ pub(super) struct Socks5Authorization<'a> {
/// V4 allows idenfication using a user_id
/// V5 allows authorization using a username and password
pub(super) enum SocksVersion<'a> {
V4(Option<Socks4Identification<'a>>),
V5(Option<Socks5Authorization<'a>>),
V4 {
local_dns: bool,
identification: Option<Socks4Identification<'a>>,
},
V5 {
local_dns: bool,
authorization: Option<Socks5Authorization<'a>>,
},
}
pub(super) fn parse_socks_proxy<'t>(scheme: &str, proxy: &'t Url) -> SocksVersion<'t> {
@@ -33,13 +42,19 @@ pub(super) fn parse_socks_proxy<'t>(scheme: &str, proxy: &'t Url) -> SocksVersio
"" => None,
username => Some(Socks4Identification { user_id: username }),
};
SocksVersion::V4(identification)
SocksVersion::V4 {
local_dns: scheme != "socks4a",
identification,
}
} else {
let authorization = proxy.password().map(|password| Socks5Authorization {
username: proxy.username(),
password,
});
SocksVersion::V5(authorization)
SocksVersion::V5 {
local_dns: scheme != "socks5h",
authorization,
}
}
}
@@ -48,26 +63,58 @@ pub(super) async fn connect_socks_proxy_stream(
socks_version: SocksVersion<'_>,
rpc_host: (&str, u16),
) -> Result<Box<dyn AsyncReadWrite>> {
let rpc_host = rpc_host
.into_target_addr()
.context("Failed to parse target addr")?;
let local_dns = match &socks_version {
SocksVersion::V4 { local_dns, .. } => local_dns,
SocksVersion::V5 { local_dns, .. } => local_dns,
};
let rpc_host = match (rpc_host, local_dns) {
(TargetAddr::Domain(domain, port), true) => {
let ip_addr = tokio::net::lookup_host((domain.as_ref(), port))
.await
.with_context(|| format!("Failed to lookup domain {}", domain))?
.next()
.ok_or_else(|| anyhow::anyhow!("Failed to lookup domain {}", domain))?;
TargetAddr::Ip(ip_addr)
}
(rpc_host, _) => rpc_host,
};
match socks_version {
SocksVersion::V4(None) => {
SocksVersion::V4 {
identification: None,
..
} => {
let socks = Socks4Stream::connect_with_socket(stream, rpc_host)
.await
.context("error connecting to socks")?;
Ok(Box::new(socks))
}
SocksVersion::V4(Some(Socks4Identification { user_id })) => {
SocksVersion::V4 {
identification: Some(Socks4Identification { user_id }),
..
} => {
let socks = Socks4Stream::connect_with_userid_and_socket(stream, rpc_host, user_id)
.await
.context("error connecting to socks")?;
Ok(Box::new(socks))
}
SocksVersion::V5(None) => {
SocksVersion::V5 {
authorization: None,
..
} => {
let socks = Socks5Stream::connect_with_socket(stream, rpc_host)
.await
.context("error connecting to socks")?;
Ok(Box::new(socks))
}
SocksVersion::V5(Some(Socks5Authorization { username, password })) => {
SocksVersion::V5 {
authorization: Some(Socks5Authorization { username, password }),
..
} => {
let socks = Socks5Stream::connect_with_password_and_socket(
stream, rpc_host, username, password,
)
@@ -90,7 +137,13 @@ mod tests {
let scheme = proxy.scheme();
let version = parse_socks_proxy(scheme, &proxy);
assert!(matches!(version, SocksVersion::V4(None)))
assert!(matches!(
version,
SocksVersion::V4 {
local_dns: true,
identification: None
}
))
}
#[test]
@@ -101,7 +154,25 @@ mod tests {
let version = parse_socks_proxy(scheme, &proxy);
assert!(matches!(
version,
SocksVersion::V4(Some(Socks4Identification { user_id: "userid" }))
SocksVersion::V4 {
local_dns: true,
identification: Some(Socks4Identification { user_id: "userid" })
}
))
}
#[test]
fn parse_socks4_with_remote_dns() {
let proxy = Url::parse("socks4a://proxy.example.com:1080").unwrap();
let scheme = proxy.scheme();
let version = parse_socks_proxy(scheme, &proxy);
assert!(matches!(
version,
SocksVersion::V4 {
local_dns: false,
identification: None
}
))
}
@@ -111,7 +182,13 @@ mod tests {
let scheme = proxy.scheme();
let version = parse_socks_proxy(scheme, &proxy);
assert!(matches!(version, SocksVersion::V5(None)))
assert!(matches!(
version,
SocksVersion::V5 {
local_dns: true,
authorization: None
}
))
}
#[test]
@@ -122,10 +199,28 @@ mod tests {
let version = parse_socks_proxy(scheme, &proxy);
assert!(matches!(
version,
SocksVersion::V5(Some(Socks5Authorization {
username: "username",
password: "password"
}))
SocksVersion::V5 {
local_dns: true,
authorization: Some(Socks5Authorization {
username: "username",
password: "password"
})
}
))
}
#[test]
fn parse_socks5_with_remote_dns() {
let proxy = Url::parse("socks5h://proxy.example.com:1080").unwrap();
let scheme = proxy.scheme();
let version = parse_socks_proxy(scheme, &proxy);
assert!(matches!(
version,
SocksVersion::V5 {
local_dns: false,
authorization: None
}
))
}
}

View File

@@ -108,6 +108,7 @@ pub struct UserStore {
edit_predictions_usage_amount: Option<u32>,
edit_predictions_usage_limit: Option<proto::UsageLimit>,
is_usage_based_billing_enabled: Option<bool>,
account_too_young: Option<bool>,
current_user: watch::Receiver<Option<Arc<User>>>,
accepted_tos_at: Option<Option<DateTime<Utc>>>,
contacts: Vec<Arc<Contact>>,
@@ -174,6 +175,7 @@ impl UserStore {
edit_predictions_usage_amount: None,
edit_predictions_usage_limit: None,
is_usage_based_billing_enabled: None,
account_too_young: None,
accepted_tos_at: None,
contacts: Default::default(),
incoming_contact_requests: Default::default(),
@@ -347,6 +349,7 @@ impl UserStore {
.trial_started_at
.and_then(|trial_started_at| DateTime::from_timestamp(trial_started_at as i64, 0));
this.is_usage_based_billing_enabled = message.payload.is_usage_based_billing_enabled;
this.account_too_young = message.payload.account_too_young;
if let Some(usage) = message.payload.usage {
this.model_request_usage_amount = Some(usage.model_requests_usage_amount);
@@ -752,6 +755,11 @@ impl UserStore {
self.current_user.clone()
}
/// Check if the current user's account is too new to use the service
pub fn current_user_account_too_young(&self) -> bool {
self.account_too_young.unwrap_or(false)
}
pub fn current_user_has_accepted_terms(&self) -> Option<bool> {
self.accepted_tos_at
.map(|accepted_tos_at| accepted_tos_at.is_some())

View File

@@ -92,6 +92,7 @@ command_palette_hooks.workspace = true
context_server.workspace = true
ctor.workspace = true
dap = { workspace = true, features = ["test-support"] }
dap_adapters = { workspace = true, features = ["test-support"] }
debugger_ui = { workspace = true, features = ["test-support"] }
editor = { workspace = true, features = ["test-support"] }
env_logger.workspace = true

View File

@@ -0,0 +1,2 @@
drop table monthly_usages;
drop table lifetime_usages;

View File

@@ -0,0 +1 @@
drop table billing_events;

View File

@@ -27,11 +27,9 @@ use crate::db::billing_subscription::{
StripeCancellationReason, StripeSubscriptionStatus, SubscriptionKind,
};
use crate::llm::db::subscription_usage_meter::CompletionMode;
use crate::llm::{
AGENT_EXTENDED_TRIAL_FEATURE_FLAG, DEFAULT_MAX_MONTHLY_SPEND, FREE_TIER_MONTHLY_SPENDING_LIMIT,
};
use crate::llm::{AGENT_EXTENDED_TRIAL_FEATURE_FLAG, DEFAULT_MAX_MONTHLY_SPEND};
use crate::rpc::{ResultExt as _, Server};
use crate::{AppState, Cents, Error, Result};
use crate::{AppState, Error, Result};
use crate::{db::UserId, llm::db::LlmDatabase};
use crate::{
db::{
@@ -64,7 +62,6 @@ pub fn router() -> Router {
"/billing/subscriptions/sync",
post(sync_billing_subscription),
)
.route("/billing/monthly_spend", get(get_monthly_spend))
.route("/billing/usage", get(get_current_usage))
}
@@ -1223,54 +1220,6 @@ async fn handle_customer_subscription_event(
Ok(())
}
#[derive(Debug, Deserialize)]
struct GetMonthlySpendParams {
github_user_id: i32,
}
#[derive(Debug, Serialize)]
struct GetMonthlySpendResponse {
monthly_free_tier_spend_in_cents: u32,
monthly_free_tier_allowance_in_cents: u32,
monthly_spend_in_cents: u32,
}
async fn get_monthly_spend(
Extension(app): Extension<Arc<AppState>>,
Query(params): Query<GetMonthlySpendParams>,
) -> Result<Json<GetMonthlySpendResponse>> {
let user = app
.db
.get_user_by_github_user_id(params.github_user_id)
.await?
.context("user not found")?;
let Some(llm_db) = app.llm_db.clone() else {
return Err(Error::http(
StatusCode::NOT_IMPLEMENTED,
"LLM database not available".into(),
));
};
let free_tier = user
.custom_llm_monthly_allowance_in_cents
.map(|allowance| Cents(allowance as u32))
.unwrap_or(FREE_TIER_MONTHLY_SPENDING_LIMIT);
let spending_for_month = llm_db
.get_user_spending_for_month(user.id, Utc::now())
.await?;
let free_tier_spend = Cents::min(spending_for_month, free_tier);
let monthly_spend = spending_for_month.saturating_sub(free_tier);
Ok(Json(GetMonthlySpendResponse {
monthly_free_tier_spend_in_cents: free_tier_spend.0,
monthly_free_tier_allowance_in_cents: free_tier.0,
monthly_spend_in_cents: monthly_spend.0,
}))
}
#[derive(Debug, Deserialize)]
struct GetCurrentUsageParams {
github_user_id: i32,
@@ -1344,15 +1293,10 @@ async fn get_current_usage(
.get_subscription_usage_for_period(user.id, period_start_at, period_end_at)
.await?;
let plan = usage
.as_ref()
.map(|usage| usage.plan.into())
.unwrap_or_else(|| {
subscription
.kind
.map(Into::into)
.unwrap_or(zed_llm_client::Plan::ZedFree)
});
let plan = subscription
.kind
.map(Into::into)
.unwrap_or(zed_llm_client::Plan::ZedFree);
let model_requests_limit = match plan.model_requests_limit() {
zed_llm_client::UsageLimit::Limited(limit) => {
@@ -1555,6 +1499,12 @@ async fn sync_model_request_usage_with_stripe(
.get_active_zed_pro_billing_subscriptions(user_ids)
.await?;
let claude_sonnet_4 = stripe_billing
.find_price_by_lookup_key("claude-sonnet-4-requests")
.await?;
let claude_sonnet_4_max = stripe_billing
.find_price_by_lookup_key("claude-sonnet-4-requests-max")
.await?;
let claude_3_5_sonnet = stripe_billing
.find_price_by_lookup_key("claude-3-5-sonnet-requests")
.await?;
@@ -1588,6 +1538,10 @@ async fn sync_model_request_usage_with_stripe(
let model = llm_db.model_by_id(usage_meter.model_id)?;
let (price, meter_event_name) = match model.name.as_str() {
"claude-sonnet-4" => match usage_meter.mode {
CompletionMode::Normal => (&claude_sonnet_4, "claude_sonnet_4/requests"),
CompletionMode::Max => (&claude_sonnet_4_max, "claude_sonnet_4/requests/max"),
},
"claude-3-5-sonnet" => (&claude_3_5_sonnet, "claude_3_5_sonnet/requests"),
"claude-3-7-sonnet" => match usage_meter.mode {
CompletionMode::Normal => (&claude_3_7_sonnet, "claude_3_7_sonnet/requests"),

View File

@@ -7,10 +7,6 @@ pub use token::*;
pub const AGENT_EXTENDED_TRIAL_FEATURE_FLAG: &str = "agent-extended-trial";
/// The maximum monthly spending an individual user can reach on the free tier
/// before they have to pay.
pub const FREE_TIER_MONTHLY_SPENDING_LIMIT: Cents = Cents::from_dollars(10);
/// The default value to use for maximum spend per month if the user did not
/// explicitly set a maximum spend.
///

View File

@@ -1,7 +1,3 @@
use crate::db::UserId;
use crate::llm::Cents;
use chrono::Datelike;
use futures::StreamExt as _;
use std::str::FromStr;
use strum::IntoEnumIterator as _;
@@ -45,68 +41,4 @@ impl LlmDatabase {
.collect();
Ok(())
}
pub async fn get_user_spending_for_month(
&self,
user_id: UserId,
now: DateTimeUtc,
) -> Result<Cents> {
self.transaction(|tx| async move {
let month = now.date_naive().month() as i32;
let year = now.date_naive().year();
let mut monthly_usages = monthly_usage::Entity::find()
.filter(
monthly_usage::Column::UserId
.eq(user_id)
.and(monthly_usage::Column::Month.eq(month))
.and(monthly_usage::Column::Year.eq(year)),
)
.stream(&*tx)
.await?;
let mut monthly_spending = Cents::ZERO;
while let Some(usage) = monthly_usages.next().await {
let usage = usage?;
let Ok(model) = self.model_by_id(usage.model_id) else {
continue;
};
monthly_spending += calculate_spending(
model,
usage.input_tokens as usize,
usage.cache_creation_input_tokens as usize,
usage.cache_read_input_tokens as usize,
usage.output_tokens as usize,
);
}
Ok(monthly_spending)
})
.await
}
}
fn calculate_spending(
model: &model::Model,
input_tokens_this_month: usize,
cache_creation_input_tokens_this_month: usize,
cache_read_input_tokens_this_month: usize,
output_tokens_this_month: usize,
) -> Cents {
let input_token_cost =
input_tokens_this_month * model.price_per_million_input_tokens as usize / 1_000_000;
let cache_creation_input_token_cost = cache_creation_input_tokens_this_month
* model.price_per_million_cache_creation_input_tokens as usize
/ 1_000_000;
let cache_read_input_token_cost = cache_read_input_tokens_this_month
* model.price_per_million_cache_read_input_tokens as usize
/ 1_000_000;
let output_token_cost =
output_tokens_this_month * model.price_per_million_output_tokens as usize / 1_000_000;
let spending = input_token_cost
+ cache_creation_input_token_cost
+ cache_read_input_token_cost
+ output_token_cost;
Cents::new(spending as u32)
}

View File

@@ -1,5 +1,4 @@
pub mod model;
pub mod monthly_usage;
pub mod provider;
pub mod subscription_usage;
pub mod subscription_usage_meter;

View File

@@ -1,22 +0,0 @@
use crate::{db::UserId, llm::db::ModelId};
use sea_orm::entity::prelude::*;
#[derive(Clone, Debug, PartialEq, DeriveEntityModel)]
#[sea_orm(table_name = "monthly_usages")]
pub struct Model {
#[sea_orm(primary_key)]
pub id: i32,
pub user_id: UserId,
pub model_id: ModelId,
pub month: i32,
pub year: i32,
pub input_tokens: i64,
pub cache_creation_input_tokens: i64,
pub cache_read_input_tokens: i64,
pub output_tokens: i64,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {}
impl ActiveModelBehavior for ActiveModel {}

View File

@@ -2716,6 +2716,7 @@ async fn make_update_user_plan_message(
let plan = current_plan(db, user_id, is_staff).await?;
let billing_customer = db.get_billing_customer_by_user_id(user_id).await?;
let billing_preferences = db.get_billing_preferences(user_id).await?;
let user = db.get_user_by_id(user_id).await?;
let (subscription_period, usage) = if let Some(llm_db) = llm_db {
let subscription = db.get_active_billing_subscription(user_id).await?;
@@ -2736,6 +2737,18 @@ async fn make_update_user_plan_message(
(None, None)
};
// Calculate account_too_young
let account_too_young = if matches!(plan, proto::Plan::ZedPro) {
// If they have paid, then we allow them to use all of the features
false
} else if let Some(user) = user {
// If we have access to the profile age, we use that
chrono::Utc::now().naive_utc() - user.account_created_at() < MIN_ACCOUNT_AGE_FOR_LLM_USE
} else {
// Default to false otherwise
false
};
Ok(proto::UpdateUserPlan {
plan: plan.into(),
trial_started_at: billing_customer
@@ -2752,6 +2765,7 @@ async fn make_update_user_plan_message(
ended_at: ended_at.timestamp() as u64,
}
}),
account_too_young: Some(account_too_young),
usage: usage.map(|usage| {
let plan = match plan {
proto::Plan::Free => zed_llm_client::Plan::ZedFree,

View File

@@ -177,13 +177,11 @@ impl StripeBilling {
const BILLING_THRESHOLD_IN_CENTS: i64 = 20 * 100;
let price_per_unit = price.unit_amount.unwrap_or_default();
let _units_for_billing_threshold = BILLING_THRESHOLD_IN_CENTS / price_per_unit;
stripe::Subscription::update(
&self.client,
subscription_id,
stripe::UpdateSubscription {
billing_thresholds: Some(stripe::SubscriptionBillingThresholds { amount_gte: Some(BILLING_THRESHOLD_IN_CENTS), ..Default::default() }),
items: Some(vec![stripe::UpdateSubscriptionItems {
price: Some(price.id.to_string()),
..Default::default()

View File

@@ -592,9 +592,11 @@ async fn test_remote_server_debugger(
if std::env::var("RUST_LOG").is_ok() {
env_logger::try_init().ok();
}
dap_adapters::init(cx);
});
server_cx.update(|cx| {
release_channel::init(SemanticVersion::default(), cx);
dap_adapters::init(cx);
});
let (opts, server_ssh) = SshRemoteClient::fake_server(cx_a, server_cx);
let remote_fs = FakeFs::new(server_cx.executor());

View File

@@ -581,6 +581,15 @@ async fn stream_completion(
api_key: String,
request: Request,
) -> Result<BoxStream<'static, Result<ResponseEvent>>> {
let is_vision_request = request.messages.last().map_or(false, |message| match message {
ChatMessage::User { content }
| ChatMessage::Assistant { content, .. }
| ChatMessage::Tool { content, .. } => {
matches!(content, ChatMessageContent::Multipart(parts) if parts.iter().any(|part| matches!(part, ChatMessagePart::Image { .. })))
}
_ => false,
});
let request_builder = HttpRequest::builder()
.method(Method::POST)
.uri(COPILOT_CHAT_COMPLETION_URL)
@@ -594,7 +603,7 @@ async fn stream_completion(
.header("Authorization", format!("Bearer {}", api_key))
.header("Content-Type", "application/json")
.header("Copilot-Integration-Id", "vscode-chat")
.header("Copilot-Vision-Request", "true");
.header("Copilot-Vision-Request", is_vision_request.to_string());
let is_streaming = request.stream;

View File

@@ -47,6 +47,7 @@ settings.workspace = true
smallvec.workspace = true
smol.workspace = true
task.workspace = true
telemetry.workspace = true
util.workspace = true
workspace-hack.workspace = true

View File

@@ -1,5 +1,5 @@
use ::fs::Fs;
use anyhow::{Context as _, Result};
use anyhow::{Context as _, Result, anyhow};
use async_compression::futures::bufread::GzipDecoder;
use async_tar::Archive;
use async_trait::async_trait;
@@ -12,7 +12,7 @@ use language::{LanguageName, LanguageToolchainStore};
use node_runtime::NodeRuntime;
use serde::{Deserialize, Serialize};
use settings::WorktreeId;
use smol::{self, fs::File};
use smol::fs::File;
use std::{
borrow::Borrow,
ffi::OsStr,
@@ -22,7 +22,8 @@ use std::{
path::{Path, PathBuf},
sync::Arc,
};
use task::{AttachRequest, DebugRequest, DebugScenario, LaunchRequest, TcpArgumentsTemplate};
use task::{DebugScenario, TcpArgumentsTemplate, ZedDebugConfig};
use util::archive::extract_zip;
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum DapStatus {
@@ -130,13 +131,12 @@ impl TcpArguments {
derive(serde::Deserialize, serde::Serialize)
)]
pub struct DebugTaskDefinition {
/// The name of this debug task
pub label: SharedString,
/// The debug adapter to use
pub adapter: DebugAdapterName,
pub request: DebugRequest,
/// Additional initialization arguments to be sent on DAP initialization
pub initialize_args: Option<serde_json::Value>,
/// Whether to tell the debug adapter to stop on entry
pub stop_on_entry: Option<bool>,
/// The configuration to send to the debug adapter
pub config: serde_json::Value,
/// Optional TCP connection information
///
/// If provided, this will be used to connect to the debug adapter instead of
@@ -146,86 +146,34 @@ pub struct DebugTaskDefinition {
}
impl DebugTaskDefinition {
pub fn cwd(&self) -> Option<&Path> {
if let DebugRequest::Launch(config) = &self.request {
config.cwd.as_ref().map(Path::new)
} else {
None
}
}
pub fn to_scenario(&self) -> DebugScenario {
DebugScenario {
label: self.label.clone(),
adapter: self.adapter.clone().into(),
build: None,
request: Some(self.request.clone()),
stop_on_entry: self.stop_on_entry,
tcp_connection: self.tcp_connection.clone(),
initialize_args: self.initialize_args.clone(),
config: self.config.clone(),
}
}
pub fn to_proto(&self) -> proto::DebugTaskDefinition {
proto::DebugTaskDefinition {
adapter: self.adapter.to_string(),
request: Some(match &self.request {
DebugRequest::Launch(config) => {
proto::debug_task_definition::Request::DebugLaunchRequest(
proto::DebugLaunchRequest {
program: config.program.clone(),
cwd: config.cwd.as_ref().map(|c| c.to_string_lossy().to_string()),
args: config.args.clone(),
env: config
.env
.iter()
.map(|(k, v)| (k.clone(), v.clone()))
.collect(),
},
)
}
DebugRequest::Attach(attach_request) => {
proto::debug_task_definition::Request::DebugAttachRequest(
proto::DebugAttachRequest {
process_id: attach_request.process_id.unwrap_or_default(),
},
)
}
}),
label: self.label.to_string(),
initialize_args: self.initialize_args.as_ref().map(|v| v.to_string()),
tcp_connection: self.tcp_connection.as_ref().map(|t| t.to_proto()),
stop_on_entry: self.stop_on_entry,
label: self.label.clone().into(),
config: self.config.to_string(),
tcp_connection: self.tcp_connection.clone().map(|v| v.to_proto()),
adapter: self.adapter.clone().0.into(),
}
}
pub fn from_proto(proto: proto::DebugTaskDefinition) -> Result<Self> {
let request = proto.request.context("request is required")?;
Ok(Self {
label: proto.label.into(),
initialize_args: proto.initialize_args.map(|v| v.into()),
config: serde_json::from_str(&proto.config)?,
tcp_connection: proto
.tcp_connection
.map(TcpArgumentsTemplate::from_proto)
.transpose()?,
stop_on_entry: proto.stop_on_entry,
adapter: DebugAdapterName(proto.adapter.into()),
request: match request {
proto::debug_task_definition::Request::DebugAttachRequest(config) => {
DebugRequest::Attach(AttachRequest {
process_id: Some(config.process_id),
})
}
proto::debug_task_definition::Request::DebugLaunchRequest(config) => {
DebugRequest::Launch(LaunchRequest {
program: config.program,
cwd: config.cwd.map(|cwd| cwd.into()),
args: config.args,
env: Default::default(),
})
}
},
})
}
}
@@ -358,17 +306,13 @@ pub async fn download_adapter_from_github(
}
DownloadedFileType::Zip | DownloadedFileType::Vsix => {
let zip_path = version_path.with_extension("zip");
let mut file = File::create(&zip_path).await?;
futures::io::copy(response.body_mut(), &mut file).await?;
// we cannot check the status as some adapter include files with names that trigger `Illegal byte sequence`
util::command::new_smol_command("unzip")
.arg(&zip_path)
.arg("-d")
.arg(&version_path)
.output()
.await?;
let file = File::open(&zip_path).await?;
extract_zip(&version_path, BufReader::new(file))
.await
// we cannot check the status as some adapter include files with names that trigger `Illegal byte sequence`
.ok();
util::fs::remove_matching(&adapter_path, |entry| {
entry
@@ -410,6 +354,8 @@ pub async fn fetch_latest_adapter_version_from_github(
pub trait DebugAdapter: 'static + Send + Sync {
fn name(&self) -> DebugAdapterName;
fn config_from_zed_format(&self, zed_scenario: ZedDebugConfig) -> Result<DebugScenario>;
async fn get_binary(
&self,
delegate: &Arc<dyn DapDelegate>,
@@ -422,6 +368,26 @@ pub trait DebugAdapter: 'static + Send + Sync {
fn adapter_language_name(&self) -> Option<LanguageName> {
None
}
fn validate_config(
&self,
config: &serde_json::Value,
) -> Result<StartDebuggingRequestArgumentsRequest> {
let map = config.as_object().context("Config isn't an object")?;
let request_variant = map
.get("request")
.and_then(|val| val.as_str())
.context("request argument is not found or invalid")?;
match request_variant {
"launch" => Ok(StartDebuggingRequestArgumentsRequest::Launch),
"attach" => Ok(StartDebuggingRequestArgumentsRequest::Attach),
_ => Err(anyhow!("request must be either 'launch' or 'attach'")),
}
}
async fn dap_schema(&self) -> serde_json::Value;
}
#[cfg(any(test, feature = "test-support"))]
@@ -435,29 +401,29 @@ impl FakeAdapter {
Self {}
}
fn request_args(&self, config: &DebugTaskDefinition) -> StartDebuggingRequestArguments {
fn request_args(
&self,
task_definition: &DebugTaskDefinition,
) -> StartDebuggingRequestArguments {
use serde_json::json;
use task::DebugRequest;
let obj = task_definition.config.as_object().unwrap();
let request_variant = obj["request"].as_str().unwrap();
let value = json!({
"request": match config.request {
DebugRequest::Launch(_) => "launch",
DebugRequest::Attach(_) => "attach",
},
"process_id": if let DebugRequest::Attach(attach_config) = &config.request {
attach_config.process_id
} else {
None
},
"raw_request": serde_json::to_value(config).unwrap()
"request": request_variant,
"process_id": obj.get("process_id"),
"raw_request": serde_json::to_value(task_definition).unwrap()
});
let request = match config.request {
DebugRequest::Launch(_) => dap_types::StartDebuggingRequestArgumentsRequest::Launch,
DebugRequest::Attach(_) => dap_types::StartDebuggingRequestArgumentsRequest::Attach,
};
StartDebuggingRequestArguments {
configuration: value,
request,
request: match request_variant {
"launch" => dap_types::StartDebuggingRequestArgumentsRequest::Launch,
"attach" => dap_types::StartDebuggingRequestArgumentsRequest::Attach,
_ => unreachable!("Wrong fake adapter input for request field"),
},
}
}
}
@@ -469,6 +435,41 @@ impl DebugAdapter for FakeAdapter {
DebugAdapterName(Self::ADAPTER_NAME.into())
}
async fn dap_schema(&self) -> serde_json::Value {
serde_json::Value::Null
}
fn validate_config(
&self,
config: &serde_json::Value,
) -> Result<StartDebuggingRequestArgumentsRequest> {
let request = config.as_object().unwrap()["request"].as_str().unwrap();
let request = match request {
"launch" => dap_types::StartDebuggingRequestArgumentsRequest::Launch,
"attach" => dap_types::StartDebuggingRequestArgumentsRequest::Attach,
_ => unreachable!("Wrong fake adapter input for request field"),
};
Ok(request)
}
fn adapter_language_name(&self) -> Option<LanguageName> {
None
}
fn config_from_zed_format(&self, zed_scenario: ZedDebugConfig) -> Result<DebugScenario> {
let config = serde_json::to_value(zed_scenario.request).unwrap();
Ok(DebugScenario {
adapter: zed_scenario.adapter,
label: zed_scenario.label,
build: None,
config,
tcp_connection: None,
})
}
async fn get_binary(
&self,
_: &Arc<dyn DapDelegate>,
@@ -482,7 +483,7 @@ impl DebugAdapter for FakeAdapter {
connection: None,
envs: HashMap::default(),
cwd: None,
request_args: self.request_args(config),
request_args: self.request_args(&config),
})
}
}

View File

@@ -9,7 +9,11 @@ pub mod transport;
use std::net::Ipv4Addr;
pub use dap_types::*;
use debugger_settings::DebuggerSettings;
use gpui::App;
pub use registry::{DapLocator, DapRegistry};
use serde::Serialize;
use settings::Settings;
pub use task::DebugRequest;
pub type ScopeId = u64;
@@ -18,7 +22,7 @@ pub type StackFrameId = u64;
#[cfg(any(test, feature = "test-support"))]
pub use adapters::FakeAdapter;
use task::TcpArgumentsTemplate;
use task::{DebugScenario, TcpArgumentsTemplate};
pub async fn configure_tcp_connection(
tcp_connection: TcpArgumentsTemplate,
@@ -34,3 +38,31 @@ pub async fn configure_tcp_connection(
Ok((host, port, timeout))
}
#[derive(Clone, Copy, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum TelemetrySpawnLocation {
Gutter,
ScenarioList,
Custom,
}
pub fn send_telemetry(scenario: &DebugScenario, location: TelemetrySpawnLocation, cx: &App) {
let Some(adapter) = cx.global::<DapRegistry>().adapter(&scenario.adapter) else {
return;
};
let kind = adapter
.validate_config(&scenario.config)
.ok()
.map(serde_json::to_value)
.and_then(Result::ok);
let dock = DebuggerSettings::get_global(cx).dock;
telemetry::event!(
"Debugger Session Started",
spawn_location = location,
with_build_task = scenario.build.is_some(),
kind = kind,
adapter = scenario.adapter.as_ref(),
dock_position = dock,
);
}

View File

@@ -4,7 +4,9 @@ use collections::FxHashMap;
use gpui::{App, Global, SharedString};
use language::LanguageName;
use parking_lot::RwLock;
use task::{DebugRequest, DebugScenario, SpawnInTerminal, TaskTemplate};
use task::{
AdapterSchema, AdapterSchemas, DebugRequest, DebugScenario, SpawnInTerminal, TaskTemplate,
};
use crate::{
adapters::{DebugAdapter, DebugAdapterName},
@@ -41,14 +43,7 @@ impl Global for DapRegistry {}
impl DapRegistry {
pub fn global(cx: &mut App) -> &mut Self {
let ret = cx.default_global::<Self>();
#[cfg(any(test, feature = "test-support"))]
if ret.adapter(crate::FakeAdapter::ADAPTER_NAME).is_none() {
ret.add_adapter(Arc::new(crate::FakeAdapter::new()));
}
ret
cx.default_global::<Self>()
}
pub fn add_adapter(&self, adapter: Arc<dyn DebugAdapter>) {
@@ -69,6 +64,22 @@ impl DapRegistry {
);
}
pub async fn adapters_schema(&self) -> task::AdapterSchemas {
let mut schemas = AdapterSchemas(vec![]);
// Clone to avoid holding lock over await points
let adapters = self.0.read().adapters.clone();
for (name, adapter) in adapters.into_iter() {
schemas.0.push(AdapterSchema {
adapter: name.into(),
schema: adapter.dap_schema().await,
});
}
schemas
}
pub fn add_inline_value_provider(
&self,
language: String,

View File

@@ -26,10 +26,12 @@ async-trait.workspace = true
dap.workspace = true
futures.workspace = true
gpui.workspace = true
json_dotpath.workspace = true
language.workspace = true
paths.workspace = true
serde.workspace = true
serde_json.workspace = true
smol.workspace = true
task.workspace = true
util.workspace = true
workspace-hack.workspace = true

View File

@@ -1,11 +1,15 @@
use std::{collections::HashMap, path::PathBuf, sync::OnceLock};
use anyhow::{Context as _, Result};
use anyhow::{Context as _, Result, anyhow};
use async_trait::async_trait;
use dap::adapters::{DebugTaskDefinition, latest_github_release};
use dap::{
StartDebuggingRequestArgumentsRequest,
adapters::{DebugTaskDefinition, latest_github_release},
};
use futures::StreamExt;
use gpui::AsyncApp;
use task::DebugRequest;
use serde_json::Value;
use task::{DebugRequest, DebugScenario, ZedDebugConfig};
use util::fs::remove_matching;
use crate::*;
@@ -18,45 +22,27 @@ pub(crate) struct CodeLldbDebugAdapter {
impl CodeLldbDebugAdapter {
const ADAPTER_NAME: &'static str = "CodeLLDB";
fn request_args(&self, config: &DebugTaskDefinition) -> dap::StartDebuggingRequestArguments {
let mut configuration = json!({
"request": match config.request {
DebugRequest::Launch(_) => "launch",
DebugRequest::Attach(_) => "attach",
},
});
let map = configuration.as_object_mut().unwrap();
fn request_args(
&self,
task_definition: &DebugTaskDefinition,
) -> Result<dap::StartDebuggingRequestArguments> {
// CodeLLDB uses `name` for a terminal label.
map.insert(
"name".into(),
Value::String(String::from(config.label.as_ref())),
);
let request = config.request.to_dap();
match &config.request {
DebugRequest::Attach(attach) => {
map.insert("pid".into(), attach.process_id.into());
}
DebugRequest::Launch(launch) => {
map.insert("program".into(), launch.program.clone().into());
let mut configuration = task_definition.config.clone();
if !launch.args.is_empty() {
map.insert("args".into(), launch.args.clone().into());
}
if !launch.env.is_empty() {
map.insert("env".into(), launch.env_json());
}
if let Some(stop_on_entry) = config.stop_on_entry {
map.insert("stopOnEntry".into(), stop_on_entry.into());
}
if let Some(cwd) = launch.cwd.as_ref() {
map.insert("cwd".into(), cwd.to_string_lossy().into_owned().into());
}
}
}
dap::StartDebuggingRequestArguments {
configuration
.as_object_mut()
.context("CodeLLDB is not a valid json object")?
.insert(
"name".into(),
Value::String(String::from(task_definition.label.as_ref())),
);
let request = self.validate_config(&configuration)?;
Ok(dap::StartDebuggingRequestArguments {
request,
configuration,
}
})
}
async fn fetch_latest_adapter_version(
@@ -103,6 +89,286 @@ impl DebugAdapter for CodeLldbDebugAdapter {
DebugAdapterName(Self::ADAPTER_NAME.into())
}
fn validate_config(
&self,
config: &serde_json::Value,
) -> Result<StartDebuggingRequestArgumentsRequest> {
let map = config
.as_object()
.ok_or_else(|| anyhow!("Config isn't an object"))?;
let request_variant = map
.get("request")
.and_then(|r| r.as_str())
.ok_or_else(|| anyhow!("request field is required and must be a string"))?;
match request_variant {
"launch" => {
// For launch, verify that one of the required configs exists
if !(map.contains_key("program")
|| map.contains_key("targetCreateCommands")
|| map.contains_key("cargo"))
{
return Err(anyhow!(
"launch request requires either 'program', 'targetCreateCommands', or 'cargo' field"
));
}
Ok(StartDebuggingRequestArgumentsRequest::Launch)
}
"attach" => {
// For attach, verify that either pid or program exists
if !(map.contains_key("pid") || map.contains_key("program")) {
return Err(anyhow!(
"attach request requires either 'pid' or 'program' field"
));
}
Ok(StartDebuggingRequestArgumentsRequest::Attach)
}
_ => Err(anyhow!(
"request must be either 'launch' or 'attach', got '{}'",
request_variant
)),
}
}
fn config_from_zed_format(&self, zed_scenario: ZedDebugConfig) -> Result<DebugScenario> {
let mut configuration = json!({
"request": match zed_scenario.request {
DebugRequest::Launch(_) => "launch",
DebugRequest::Attach(_) => "attach",
},
});
let map = configuration.as_object_mut().unwrap();
// CodeLLDB uses `name` for a terminal label.
map.insert(
"name".into(),
Value::String(String::from(zed_scenario.label.as_ref())),
);
match &zed_scenario.request {
DebugRequest::Attach(attach) => {
map.insert("pid".into(), attach.process_id.into());
}
DebugRequest::Launch(launch) => {
map.insert("program".into(), launch.program.clone().into());
if !launch.args.is_empty() {
map.insert("args".into(), launch.args.clone().into());
}
if !launch.env.is_empty() {
map.insert("env".into(), launch.env_json());
}
if let Some(stop_on_entry) = zed_scenario.stop_on_entry {
map.insert("stopOnEntry".into(), stop_on_entry.into());
}
if let Some(cwd) = launch.cwd.as_ref() {
map.insert("cwd".into(), cwd.to_string_lossy().into_owned().into());
}
}
}
Ok(DebugScenario {
adapter: zed_scenario.adapter,
label: zed_scenario.label,
config: configuration,
build: None,
tcp_connection: None,
})
}
async fn dap_schema(&self) -> serde_json::Value {
json!({
"properties": {
"request": {
"type": "string",
"enum": ["attach", "launch"],
"description": "Debug adapter request type"
},
"program": {
"type": "string",
"description": "Path to the program to debug or attach to"
},
"args": {
"type": ["array", "string"],
"description": "Program arguments"
},
"cwd": {
"type": "string",
"description": "Program working directory"
},
"env": {
"type": "object",
"description": "Additional environment variables",
"patternProperties": {
".*": {
"type": "string"
}
}
},
"envFile": {
"type": "string",
"description": "File to read the environment variables from"
},
"stdio": {
"type": ["null", "string", "array", "object"],
"description": "Destination for stdio streams: null = send to debugger console or a terminal, \"<path>\" = attach to a file/tty/fifo"
},
"terminal": {
"type": "string",
"enum": ["integrated", "console"],
"description": "Terminal type to use",
"default": "integrated"
},
"console": {
"type": "string",
"enum": ["integratedTerminal", "internalConsole"],
"description": "Terminal type to use (compatibility alias of 'terminal')"
},
"stopOnEntry": {
"type": "boolean",
"description": "Automatically stop debuggee after launch",
"default": false
},
"initCommands": {
"type": "array",
"description": "Initialization commands executed upon debugger startup",
"items": {
"type": "string"
}
},
"targetCreateCommands": {
"type": "array",
"description": "Commands that create the debug target",
"items": {
"type": "string"
}
},
"preRunCommands": {
"type": "array",
"description": "Commands executed just before the program is launched",
"items": {
"type": "string"
}
},
"processCreateCommands": {
"type": "array",
"description": "Commands that create the debuggee process",
"items": {
"type": "string"
}
},
"postRunCommands": {
"type": "array",
"description": "Commands executed just after the program has been launched",
"items": {
"type": "string"
}
},
"preTerminateCommands": {
"type": "array",
"description": "Commands executed just before the debuggee is terminated or disconnected from",
"items": {
"type": "string"
}
},
"exitCommands": {
"type": "array",
"description": "Commands executed at the end of debugging session",
"items": {
"type": "string"
}
},
"expressions": {
"type": "string",
"enum": ["simple", "python", "native"],
"description": "The default evaluator type used for expressions"
},
"sourceMap": {
"type": "object",
"description": "Source path remapping between the build machine and the local machine",
"patternProperties": {
".*": {
"type": ["string", "null"]
}
}
},
"relativePathBase": {
"type": "string",
"description": "Base directory used for resolution of relative source paths. Defaults to the workspace folder"
},
"sourceLanguages": {
"type": "array",
"description": "A list of source languages to enable language-specific features for",
"items": {
"type": "string"
}
},
"reverseDebugging": {
"type": "boolean",
"description": "Enable reverse debugging",
"default": false
},
"breakpointMode": {
"type": "string",
"enum": ["path", "file"],
"description": "Specifies how source breakpoints should be set"
},
"pid": {
"type": ["integer", "string"],
"description": "Process id to attach to"
},
"waitFor": {
"type": "boolean",
"description": "Wait for the process to launch (MacOS only)",
"default": false
}
},
"required": ["request"],
"allOf": [
{
"if": {
"properties": {
"request": {
"enum": ["launch"]
}
}
},
"then": {
"oneOf": [
{
"required": ["program"]
},
{
"required": ["targetCreateCommands"]
},
{
"required": ["cargo"]
}
]
}
},
{
"if": {
"properties": {
"request": {
"enum": ["attach"]
}
}
},
"then": {
"oneOf": [
{
"required": ["pid"]
},
{
"required": ["program"]
}
]
}
}
]
})
}
async fn get_binary(
&self,
delegate: &Arc<dyn DapDelegate>,
@@ -136,18 +402,46 @@ impl DebugAdapter for CodeLldbDebugAdapter {
};
let adapter_dir = version_path.join("extension").join("adapter");
let path = adapter_dir.join("codelldb").to_string_lossy().to_string();
// todo("windows")
#[cfg(not(windows))]
{
use smol::fs;
fs::set_permissions(
&path,
<fs::Permissions as fs::unix::PermissionsExt>::from_mode(0o755),
)
.await
.with_context(|| format!("Settings executable permissions to {path:?}"))?;
let lldb_binaries_dir = version_path.join("extension").join("lldb").join("bin");
let mut lldb_binaries =
fs::read_dir(&lldb_binaries_dir).await.with_context(|| {
format!("reading lldb binaries dir contents {lldb_binaries_dir:?}")
})?;
while let Some(binary) = lldb_binaries.next().await {
let binary_entry = binary?;
let path = binary_entry.path();
fs::set_permissions(
&path,
<fs::Permissions as fs::unix::PermissionsExt>::from_mode(0o755),
)
.await
.with_context(|| format!("Settings executable permissions to {path:?}"))?;
}
}
self.path_to_codelldb.set(path.clone()).ok();
command = Some(path);
};
Ok(DebugAdapterBinary {
command: command.unwrap(),
cwd: None,
cwd: Some(delegate.worktree_root_path().to_path_buf()),
arguments: vec![
"--settings".into(),
json!({"sourceLanguages": ["cpp", "rust"]}).to_string(),
],
request_args: self.request_args(config),
request_args: self.request_args(&config)?,
envs: HashMap::default(),
connection: None,
})

View File

@@ -12,7 +12,7 @@ use anyhow::Result;
use async_trait::async_trait;
use codelldb::CodeLldbDebugAdapter;
use dap::{
DapRegistry, DebugRequest,
DapRegistry,
adapters::{
self, AdapterVersion, DapDelegate, DebugAdapter, DebugAdapterBinary, DebugAdapterName,
GithubRepo,
@@ -27,7 +27,8 @@ use javascript::JsDebugAdapter;
use php::PhpDebugAdapter;
use python::PythonDebugAdapter;
use ruby::RubyDebugAdapter;
use serde_json::{Value, json};
use serde_json::json;
use task::{DebugScenario, ZedDebugConfig};
pub fn init(cx: &mut App) {
cx.update_default_global(|registry: &mut DapRegistry, _cx| {
@@ -39,21 +40,13 @@ pub fn init(cx: &mut App) {
registry.add_adapter(Arc::from(GoDebugAdapter));
registry.add_adapter(Arc::from(GdbDebugAdapter));
#[cfg(any(test, feature = "test-support"))]
{
registry.add_adapter(Arc::from(dap::FakeAdapter {}));
}
registry.add_inline_value_provider("Rust".to_string(), Arc::from(RustInlineValueProvider));
registry
.add_inline_value_provider("Python".to_string(), Arc::from(PythonInlineValueProvider));
})
}
trait ToDap {
fn to_dap(&self) -> dap::StartDebuggingRequestArgumentsRequest;
}
impl ToDap for DebugRequest {
fn to_dap(&self) -> dap::StartDebuggingRequestArgumentsRequest {
match self {
Self::Launch(_) => dap::StartDebuggingRequestArgumentsRequest::Launch,
Self::Attach(_) => dap::StartDebuggingRequestArgumentsRequest::Attach,
}
}
}

View File

@@ -4,7 +4,7 @@ use anyhow::{Context as _, Result, bail};
use async_trait::async_trait;
use dap::{StartDebuggingRequestArguments, adapters::DebugTaskDefinition};
use gpui::AsyncApp;
use task::DebugRequest;
use task::{DebugScenario, ZedDebugConfig};
use crate::*;
@@ -13,48 +13,6 @@ pub(crate) struct GdbDebugAdapter;
impl GdbDebugAdapter {
const ADAPTER_NAME: &'static str = "GDB";
fn request_args(&self, config: &DebugTaskDefinition) -> StartDebuggingRequestArguments {
let mut args = json!({
"request": match config.request {
DebugRequest::Launch(_) => "launch",
DebugRequest::Attach(_) => "attach",
},
});
let map = args.as_object_mut().unwrap();
match &config.request {
DebugRequest::Attach(attach) => {
map.insert("pid".into(), attach.process_id.into());
}
DebugRequest::Launch(launch) => {
map.insert("program".into(), launch.program.clone().into());
if !launch.args.is_empty() {
map.insert("args".into(), launch.args.clone().into());
}
if !launch.env.is_empty() {
map.insert("env".into(), launch.env_json());
}
if let Some(stop_on_entry) = config.stop_on_entry {
map.insert(
"stopAtBeginningOfMainSubprogram".into(),
stop_on_entry.into(),
);
}
if let Some(cwd) = launch.cwd.as_ref() {
map.insert("cwd".into(), cwd.to_string_lossy().into_owned().into());
}
}
}
StartDebuggingRequestArguments {
configuration: args,
request: config.request.to_dap(),
}
}
}
#[async_trait(?Send)]
@@ -63,6 +21,137 @@ impl DebugAdapter for GdbDebugAdapter {
DebugAdapterName(Self::ADAPTER_NAME.into())
}
fn config_from_zed_format(&self, zed_scenario: ZedDebugConfig) -> Result<DebugScenario> {
let mut obj = serde_json::Map::default();
match &zed_scenario.request {
dap::DebugRequest::Attach(attach) => {
obj.insert("pid".into(), attach.process_id.into());
}
dap::DebugRequest::Launch(launch) => {
obj.insert("program".into(), launch.program.clone().into());
if !launch.args.is_empty() {
obj.insert("args".into(), launch.args.clone().into());
}
if !launch.env.is_empty() {
obj.insert("env".into(), launch.env_json());
}
if let Some(stop_on_entry) = zed_scenario.stop_on_entry {
obj.insert(
"stopAtBeginningOfMainSubprogram".into(),
stop_on_entry.into(),
);
}
if let Some(cwd) = launch.cwd.as_ref() {
obj.insert("cwd".into(), cwd.to_string_lossy().into_owned().into());
}
}
}
Ok(DebugScenario {
adapter: zed_scenario.adapter,
label: zed_scenario.label,
build: None,
config: serde_json::Value::Object(obj),
tcp_connection: None,
})
}
async fn dap_schema(&self) -> serde_json::Value {
json!({
"oneOf": [
{
"allOf": [
{
"type": "object",
"required": ["request"],
"properties": {
"request": {
"type": "string",
"enum": ["launch"],
"description": "Request to launch a new process"
}
}
},
{
"type": "object",
"properties": {
"program": {
"type": "string",
"description": "The program to debug. This corresponds to the GDB 'file' command."
},
"args": {
"type": "array",
"items": {
"type": "string"
},
"description": "Command line arguments passed to the program. These strings are provided as command-line arguments to the inferior.",
"default": []
},
"cwd": {
"type": "string",
"description": "Working directory for the debugged program. GDB will change its working directory to this directory."
},
"env": {
"type": "object",
"description": "Environment variables for the debugged program. Each key is the name of an environment variable; each value is the value of that variable."
},
"stopAtBeginningOfMainSubprogram": {
"type": "boolean",
"description": "When true, GDB will set a temporary breakpoint at the program's main procedure, like the 'start' command.",
"default": false
},
"stopOnEntry": {
"type": "boolean",
"description": "When true, GDB will set a temporary breakpoint at the program's first instruction, like the 'starti' command.",
"default": false
}
},
"required": ["program"]
}
]
},
{
"allOf": [
{
"type": "object",
"required": ["request"],
"properties": {
"request": {
"type": "string",
"enum": ["attach"],
"description": "Request to attach to an existing process"
}
}
},
{
"type": "object",
"properties": {
"pid": {
"type": "number",
"description": "The process ID to which GDB should attach."
},
"program": {
"type": "string",
"description": "The program to debug (optional). This corresponds to the GDB 'file' command. In many cases, GDB can determine which program is running automatically."
},
"target": {
"type": "string",
"description": "The target to which GDB should connect. This is passed to the 'target remote' command."
}
},
"required": ["pid"]
}
]
}
]
})
}
async fn get_binary(
&self,
delegate: &Arc<dyn DapDelegate>,
@@ -86,13 +175,18 @@ impl DebugAdapter for GdbDebugAdapter {
let gdb_path = user_setting_path.unwrap_or(gdb_path?);
let request_args = StartDebuggingRequestArguments {
request: self.validate_config(&config.config)?,
configuration: config.config.clone(),
};
Ok(DebugAdapterBinary {
command: gdb_path,
arguments: vec!["-i=dap".into()],
envs: HashMap::default(),
cwd: None,
cwd: Some(delegate.worktree_root_path().to_path_buf()),
connection: None,
request_args: self.request_args(config),
request_args,
})
}
}

View File

@@ -1,5 +1,9 @@
use anyhow::Context as _;
use dap::{StartDebuggingRequestArguments, adapters::DebugTaskDefinition};
use anyhow::{Context as _, anyhow};
use dap::{
StartDebuggingRequestArguments, StartDebuggingRequestArgumentsRequest,
adapters::DebugTaskDefinition,
};
use gpui::{AsyncApp, SharedString};
use language::LanguageName;
use std::{collections::HashMap, ffi::OsStr, path::PathBuf};
@@ -11,8 +15,294 @@ pub(crate) struct GoDebugAdapter;
impl GoDebugAdapter {
const ADAPTER_NAME: &'static str = "Delve";
fn request_args(&self, config: &DebugTaskDefinition) -> StartDebuggingRequestArguments {
let mut args = match &config.request {
}
#[async_trait(?Send)]
impl DebugAdapter for GoDebugAdapter {
fn name(&self) -> DebugAdapterName {
DebugAdapterName(Self::ADAPTER_NAME.into())
}
fn adapter_language_name(&self) -> Option<LanguageName> {
Some(SharedString::new_static("Go").into())
}
async fn dap_schema(&self) -> serde_json::Value {
// Create common properties shared between launch and attach
let common_properties = json!({
"debugAdapter": {
"enum": ["legacy", "dlv-dap"],
"description": "Select which debug adapter to use with this configuration.",
"default": "dlv-dap"
},
"stopOnEntry": {
"type": "boolean",
"description": "Automatically stop program after launch or attach.",
"default": false
},
"showLog": {
"type": "boolean",
"description": "Show log output from the delve debugger. Maps to dlv's `--log` flag.",
"default": false
},
"cwd": {
"type": "string",
"description": "Workspace relative or absolute path to the working directory of the program being debugged.",
"default": "${ZED_WORKTREE_ROOT}"
},
"dlvFlags": {
"type": "array",
"description": "Extra flags for `dlv`. See `dlv help` for the full list of supported flags.",
"items": {
"type": "string"
},
"default": []
},
"port": {
"type": "number",
"description": "Debug server port. For remote configurations, this is where to connect.",
"default": 2345
},
"host": {
"type": "string",
"description": "Debug server host. For remote configurations, this is where to connect.",
"default": "127.0.0.1"
},
"substitutePath": {
"type": "array",
"items": {
"type": "object",
"properties": {
"from": {
"type": "string",
"description": "The absolute local path to be replaced."
},
"to": {
"type": "string",
"description": "The absolute remote path to replace with."
}
}
},
"description": "Mappings from local to remote paths for debugging.",
"default": []
},
"trace": {
"type": "string",
"enum": ["verbose", "trace", "log", "info", "warn", "error"],
"default": "error",
"description": "Debug logging level."
},
"backend": {
"type": "string",
"enum": ["default", "native", "lldb", "rr"],
"description": "Backend used by delve. Maps to `dlv`'s `--backend` flag."
},
"logOutput": {
"type": "string",
"enum": ["debugger", "gdbwire", "lldbout", "debuglineerr", "rpc", "dap"],
"description": "Components that should produce debug output.",
"default": "debugger"
},
"logDest": {
"type": "string",
"description": "Log destination for delve."
},
"stackTraceDepth": {
"type": "number",
"description": "Maximum depth of stack traces.",
"default": 50
},
"showGlobalVariables": {
"type": "boolean",
"default": false,
"description": "Show global package variables in variables pane."
},
"showRegisters": {
"type": "boolean",
"default": false,
"description": "Show register variables in variables pane."
},
"hideSystemGoroutines": {
"type": "boolean",
"default": false,
"description": "Hide system goroutines from call stack view."
},
"console": {
"default": "internalConsole",
"description": "Where to launch the debugger.",
"enum": ["internalConsole", "integratedTerminal"]
},
"asRoot": {
"default": false,
"description": "Debug with elevated permissions (on Unix).",
"type": "boolean"
}
});
// Create launch-specific properties
let launch_properties = json!({
"program": {
"type": "string",
"description": "Path to the program folder or file to debug.",
"default": "${ZED_WORKTREE_ROOT}"
},
"args": {
"type": ["array", "string"],
"description": "Command line arguments for the program.",
"items": {
"type": "string"
},
"default": []
},
"env": {
"type": "object",
"description": "Environment variables for the debugged program.",
"default": {}
},
"envFile": {
"type": ["string", "array"],
"items": {
"type": "string"
},
"description": "Path(s) to files with environment variables.",
"default": ""
},
"buildFlags": {
"type": ["string", "array"],
"items": {
"type": "string"
},
"description": "Flags for the Go compiler.",
"default": []
},
"output": {
"type": "string",
"description": "Output path for the binary.",
"default": "debug"
},
"mode": {
"enum": [ "debug", "test", "exec", "replay", "core"],
"description": "Debug mode for launch configuration.",
},
"traceDirPath": {
"type": "string",
"description": "Directory for record trace (for 'replay' mode).",
"default": ""
},
"coreFilePath": {
"type": "string",
"description": "Path to core dump file (for 'core' mode).",
"default": ""
}
});
// Create attach-specific properties
let attach_properties = json!({
"processId": {
"anyOf": [
{
"enum": ["${command:pickProcess}", "${command:pickGoProcess}"],
"description": "Use process picker to select a process."
},
{
"type": "string",
"description": "Process name to attach to."
},
{
"type": "number",
"description": "Process ID to attach to."
}
],
"default": 0
},
"mode": {
"enum": ["local", "remote"],
"description": "Local or remote debugging.",
"default": "local"
},
"remotePath": {
"type": "string",
"description": "Path to source on remote machine.",
"markdownDeprecationMessage": "Use `substitutePath` instead.",
"default": ""
}
});
// Create the final schema
json!({
"oneOf": [
{
"allOf": [
{
"type": "object",
"required": ["request"],
"properties": {
"request": {
"type": "string",
"enum": ["launch"],
"description": "Request to launch a new process"
}
}
},
{
"type": "object",
"properties": common_properties
},
{
"type": "object",
"required": ["program", "mode"],
"properties": launch_properties
}
]
},
{
"allOf": [
{
"type": "object",
"required": ["request"],
"properties": {
"request": {
"type": "string",
"enum": ["attach"],
"description": "Request to attach to an existing process"
}
}
},
{
"type": "object",
"properties": common_properties
},
{
"type": "object",
"required": ["processId", "mode"],
"properties": attach_properties
}
]
}
]
})
}
fn validate_config(
&self,
config: &serde_json::Value,
) -> Result<StartDebuggingRequestArgumentsRequest> {
let map = config.as_object().context("Config isn't an object")?;
let request_variant = map
.get("request")
.and_then(|val| val.as_str())
.context("request argument is not found or invalid")?;
match request_variant {
"launch" => Ok(StartDebuggingRequestArgumentsRequest::Launch),
"attach" => Ok(StartDebuggingRequestArgumentsRequest::Attach),
_ => Err(anyhow!("request must be either 'launch' or 'attach'")),
}
}
fn config_from_zed_format(&self, zed_scenario: ZedDebugConfig) -> Result<DebugScenario> {
let mut args = match &zed_scenario.request {
dap::DebugRequest::Attach(attach_config) => {
json!({
"processId": attach_config.process_id,
@@ -28,31 +318,23 @@ impl GoDebugAdapter {
let map = args.as_object_mut().unwrap();
if let Some(stop_on_entry) = config.stop_on_entry {
if let Some(stop_on_entry) = zed_scenario.stop_on_entry {
map.insert("stopOnEntry".into(), stop_on_entry.into());
}
StartDebuggingRequestArguments {
configuration: args,
request: config.request.to_dap(),
}
}
}
#[async_trait(?Send)]
impl DebugAdapter for GoDebugAdapter {
fn name(&self) -> DebugAdapterName {
DebugAdapterName(Self::ADAPTER_NAME.into())
}
fn adapter_language_name(&self) -> Option<LanguageName> {
Some(SharedString::new_static("Go").into())
Ok(DebugScenario {
adapter: zed_scenario.adapter,
label: zed_scenario.label,
build: None,
config: args,
tcp_connection: None,
})
}
async fn get_binary(
&self,
delegate: &Arc<dyn DapDelegate>,
config: &DebugTaskDefinition,
task_definition: &DebugTaskDefinition,
_user_installed_path: Option<PathBuf>,
_cx: &mut AsyncApp,
) -> Result<DebugAdapterBinary> {
@@ -62,20 +344,23 @@ impl DebugAdapter for GoDebugAdapter {
.and_then(|p| p.to_str().map(|p| p.to_string()))
.context("Dlv not found in path")?;
let tcp_connection = config.tcp_connection.clone().unwrap_or_default();
let tcp_connection = task_definition.tcp_connection.clone().unwrap_or_default();
let (host, port, timeout) = crate::configure_tcp_connection(tcp_connection).await?;
Ok(DebugAdapterBinary {
command: delve_path,
arguments: vec!["dap".into(), "--listen".into(), format!("{host}:{port}")],
cwd: None,
cwd: Some(delegate.worktree_root_path().to_path_buf()),
envs: HashMap::default(),
connection: Some(adapters::TcpArguments {
host,
port,
timeout,
}),
request_args: self.request_args(config),
request_args: StartDebuggingRequestArguments {
configuration: task_definition.config.clone(),
request: self.validate_config(&task_definition.config)?,
},
})
}
}

View File

@@ -1,6 +1,9 @@
use adapters::latest_github_release;
use anyhow::Context as _;
use dap::{StartDebuggingRequestArguments, adapters::DebugTaskDefinition};
use anyhow::{Context as _, anyhow};
use dap::{
StartDebuggingRequestArguments, StartDebuggingRequestArgumentsRequest,
adapters::DebugTaskDefinition,
};
use gpui::AsyncApp;
use std::{collections::HashMap, path::PathBuf, sync::OnceLock};
use task::DebugRequest;
@@ -18,43 +21,6 @@ impl JsDebugAdapter {
const ADAPTER_NPM_NAME: &'static str = "vscode-js-debug";
const ADAPTER_PATH: &'static str = "js-debug/src/dapDebugServer.js";
fn request_args(&self, config: &DebugTaskDefinition) -> StartDebuggingRequestArguments {
let mut args = json!({
"type": "pwa-node",
"request": match config.request {
DebugRequest::Launch(_) => "launch",
DebugRequest::Attach(_) => "attach",
},
});
let map = args.as_object_mut().unwrap();
match &config.request {
DebugRequest::Attach(attach) => {
map.insert("processId".into(), attach.process_id.into());
}
DebugRequest::Launch(launch) => {
map.insert("program".into(), launch.program.clone().into());
if !launch.args.is_empty() {
map.insert("args".into(), launch.args.clone().into());
}
if !launch.env.is_empty() {
map.insert("env".into(), launch.env_json());
}
if let Some(stop_on_entry) = config.stop_on_entry {
map.insert("stopOnEntry".into(), stop_on_entry.into());
}
if let Some(cwd) = launch.cwd.as_ref() {
map.insert("cwd".into(), cwd.to_string_lossy().into_owned().into());
}
}
}
StartDebuggingRequestArguments {
configuration: args,
request: config.request.to_dap(),
}
}
async fn fetch_latest_adapter_version(
&self,
delegate: &Arc<dyn DapDelegate>,
@@ -84,7 +50,7 @@ impl JsDebugAdapter {
async fn get_installed_binary(
&self,
delegate: &Arc<dyn DapDelegate>,
config: &DebugTaskDefinition,
task_definition: &DebugTaskDefinition,
user_installed_path: Option<PathBuf>,
_: &mut AsyncApp,
) -> Result<DebugAdapterBinary> {
@@ -102,7 +68,7 @@ impl JsDebugAdapter {
.context("Couldn't find JavaScript dap directory")?
};
let tcp_connection = config.tcp_connection.clone().unwrap_or_default();
let tcp_connection = task_definition.tcp_connection.clone().unwrap_or_default();
let (host, port, timeout) = crate::configure_tcp_connection(tcp_connection).await?;
Ok(DebugAdapterBinary {
@@ -120,14 +86,17 @@ impl JsDebugAdapter {
port.to_string(),
host.to_string(),
],
cwd: None,
cwd: Some(delegate.worktree_root_path().to_path_buf()),
envs: HashMap::default(),
connection: Some(adapters::TcpArguments {
host,
port,
timeout,
}),
request_args: self.request_args(config),
request_args: StartDebuggingRequestArguments {
configuration: task_definition.config.clone(),
request: self.validate_config(&task_definition.config)?,
},
})
}
}
@@ -138,6 +107,322 @@ impl DebugAdapter for JsDebugAdapter {
DebugAdapterName(Self::ADAPTER_NAME.into())
}
fn validate_config(
&self,
config: &serde_json::Value,
) -> Result<dap::StartDebuggingRequestArgumentsRequest> {
match config.get("request") {
Some(val) if val == "launch" => {
if config.get("program").is_none() {
return Err(anyhow!("program is required"));
}
Ok(StartDebuggingRequestArgumentsRequest::Launch)
}
Some(val) if val == "attach" => {
if !config.get("processId").is_some_and(|val| val.is_u64()) {
return Err(anyhow!("processId must be a number"));
}
Ok(StartDebuggingRequestArgumentsRequest::Attach)
}
_ => Err(anyhow!("missing or invalid request field in config")),
}
}
fn config_from_zed_format(&self, zed_scenario: ZedDebugConfig) -> Result<DebugScenario> {
let mut args = json!({
"type": "pwa-node",
"request": match zed_scenario.request {
DebugRequest::Launch(_) => "launch",
DebugRequest::Attach(_) => "attach",
},
});
let map = args.as_object_mut().unwrap();
match &zed_scenario.request {
DebugRequest::Attach(attach) => {
map.insert("processId".into(), attach.process_id.into());
}
DebugRequest::Launch(launch) => {
map.insert("program".into(), launch.program.clone().into());
if !launch.args.is_empty() {
map.insert("args".into(), launch.args.clone().into());
}
if !launch.env.is_empty() {
map.insert("env".into(), launch.env_json());
}
if let Some(stop_on_entry) = zed_scenario.stop_on_entry {
map.insert("stopOnEntry".into(), stop_on_entry.into());
}
if let Some(cwd) = launch.cwd.as_ref() {
map.insert("cwd".into(), cwd.to_string_lossy().into_owned().into());
}
}
};
Ok(DebugScenario {
adapter: zed_scenario.adapter,
label: zed_scenario.label,
build: None,
config: args,
tcp_connection: None,
})
}
async fn dap_schema(&self) -> serde_json::Value {
json!({
"oneOf": [
{
"allOf": [
{
"type": "object",
"required": ["request"],
"properties": {
"request": {
"type": "string",
"enum": ["launch"],
"description": "Request to launch a new process"
}
}
},
{
"type": "object",
"properties": {
"type": {
"type": "string",
"enum": ["pwa-node", "node", "chrome", "pwa-chrome", "edge", "pwa-edge"],
"description": "The type of debug session",
"default": "pwa-node"
},
"program": {
"type": "string",
"description": "Path to the program or file to debug"
},
"cwd": {
"type": "string",
"description": "Absolute path to the working directory of the program being debugged"
},
"args": {
"type": ["array", "string"],
"description": "Command line arguments passed to the program",
"items": {
"type": "string"
},
"default": []
},
"env": {
"type": "object",
"description": "Environment variables passed to the program",
"default": {}
},
"envFile": {
"type": ["string", "array"],
"description": "Path to a file containing environment variable definitions",
"items": {
"type": "string"
}
},
"stopOnEntry": {
"type": "boolean",
"description": "Automatically stop program after launch",
"default": false
},
"runtimeExecutable": {
"type": ["string", "null"],
"description": "Runtime to use, an absolute path or the name of a runtime available on PATH",
"default": "node"
},
"runtimeArgs": {
"type": ["array", "null"],
"description": "Arguments passed to the runtime executable",
"items": {
"type": "string"
},
"default": []
},
"outFiles": {
"type": "array",
"description": "Glob patterns for locating generated JavaScript files",
"items": {
"type": "string"
},
"default": ["${ZED_WORKTREE_ROOT}/**/*.js", "!**/node_modules/**"]
},
"sourceMaps": {
"type": "boolean",
"description": "Use JavaScript source maps if they exist",
"default": true
},
"sourceMapPathOverrides": {
"type": "object",
"description": "Rewrites the locations of source files from what the sourcemap says to their locations on disk",
"default": {}
},
"restart": {
"type": ["boolean", "object"],
"description": "Restart session after Node.js has terminated",
"default": false
},
"trace": {
"type": ["boolean", "object"],
"description": "Enables logging of the Debug Adapter",
"default": false
},
"console": {
"type": "string",
"enum": ["internalConsole", "integratedTerminal"],
"description": "Where to launch the debug target",
"default": "internalConsole"
},
// Browser-specific
"url": {
"type": ["string", "null"],
"description": "Will navigate to this URL and attach to it (browser debugging)"
},
"webRoot": {
"type": "string",
"description": "Workspace absolute path to the webserver root",
"default": "${ZED_WORKTREE_ROOT}"
},
"userDataDir": {
"type": ["string", "boolean"],
"description": "Path to a custom Chrome user profile (browser debugging)",
"default": true
},
"skipFiles": {
"type": "array",
"description": "An array of glob patterns for files to skip when debugging",
"items": {
"type": "string"
},
"default": ["<node_internals>/**"]
},
"timeout": {
"type": "number",
"description": "Retry for this number of milliseconds to connect to the debug adapter",
"default": 10000
},
"resolveSourceMapLocations": {
"type": ["array", "null"],
"description": "A list of minimatch patterns for source map resolution",
"items": {
"type": "string"
}
}
},
"required": ["program"]
}
]
},
{
"allOf": [
{
"type": "object",
"required": ["request"],
"properties": {
"request": {
"type": "string",
"enum": ["attach"],
"description": "Request to attach to an existing process"
}
}
},
{
"type": "object",
"properties": {
"type": {
"type": "string",
"enum": ["pwa-node", "node", "chrome", "pwa-chrome", "edge", "pwa-edge"],
"description": "The type of debug session",
"default": "pwa-node"
},
"processId": {
"type": ["string", "number"],
"description": "ID of process to attach to (Node.js debugging)"
},
"port": {
"type": "number",
"description": "Debug port to attach to",
"default": 9229
},
"address": {
"type": "string",
"description": "TCP/IP address of the process to be debugged",
"default": "localhost"
},
"restart": {
"type": ["boolean", "object"],
"description": "Restart session after Node.js has terminated",
"default": false
},
"sourceMaps": {
"type": "boolean",
"description": "Use JavaScript source maps if they exist",
"default": true
},
"sourceMapPathOverrides": {
"type": "object",
"description": "Rewrites the locations of source files from what the sourcemap says to their locations on disk",
"default": {}
},
"outFiles": {
"type": "array",
"description": "Glob patterns for locating generated JavaScript files",
"items": {
"type": "string"
},
"default": ["${ZED_WORKTREE_ROOT}/**/*.js", "!**/node_modules/**"]
},
"url": {
"type": "string",
"description": "Will search for a page with this URL and attach to it (browser debugging)"
},
"webRoot": {
"type": "string",
"description": "Workspace absolute path to the webserver root",
"default": "${ZED_WORKTREE_ROOT}"
},
"skipFiles": {
"type": "array",
"description": "An array of glob patterns for files to skip when debugging",
"items": {
"type": "string"
},
"default": ["<node_internals>/**"]
},
"timeout": {
"type": "number",
"description": "Retry for this number of milliseconds to connect to the debug adapter",
"default": 10000
},
"resolveSourceMapLocations": {
"type": ["array", "null"],
"description": "A list of minimatch patterns for source map resolution",
"items": {
"type": "string"
}
},
"remoteRoot": {
"type": ["string", "null"],
"description": "Path to the remote directory containing the program"
},
"localRoot": {
"type": ["string", "null"],
"description": "Path to the local directory containing the program"
}
},
"oneOf": [
{ "required": ["processId"] },
{ "required": ["port"] }
]
}
]
}
]
})
}
async fn get_binary(
&self,
delegate: &Arc<dyn DapDelegate>,

View File

@@ -1,5 +1,8 @@
use adapters::latest_github_release;
use anyhow::Context as _;
use anyhow::bail;
use dap::StartDebuggingRequestArguments;
use dap::StartDebuggingRequestArgumentsRequest;
use dap::adapters::{DebugTaskDefinition, TcpArguments};
use gpui::{AsyncApp, SharedString};
use language::LanguageName;
@@ -18,27 +21,6 @@ impl PhpDebugAdapter {
const ADAPTER_PACKAGE_NAME: &'static str = "vscode-php-debug";
const ADAPTER_PATH: &'static str = "extension/out/phpDebug.js";
fn request_args(
&self,
config: &DebugTaskDefinition,
) -> Result<dap::StartDebuggingRequestArguments> {
match &config.request {
dap::DebugRequest::Attach(_) => {
anyhow::bail!("php adapter does not support attaching")
}
dap::DebugRequest::Launch(launch_config) => Ok(dap::StartDebuggingRequestArguments {
configuration: json!({
"program": launch_config.program,
"cwd": launch_config.cwd,
"args": launch_config.args,
"env": launch_config.env_json(),
"stopOnEntry": config.stop_on_entry.unwrap_or_default(),
}),
request: config.request.to_dap(),
}),
}
}
async fn fetch_latest_adapter_version(
&self,
delegate: &Arc<dyn DapDelegate>,
@@ -65,10 +47,17 @@ impl PhpDebugAdapter {
})
}
fn validate_config(
&self,
_: &serde_json::Value,
) -> Result<StartDebuggingRequestArgumentsRequest> {
Ok(StartDebuggingRequestArgumentsRequest::Launch)
}
async fn get_installed_binary(
&self,
delegate: &Arc<dyn DapDelegate>,
config: &DebugTaskDefinition,
task_definition: &DebugTaskDefinition,
user_installed_path: Option<PathBuf>,
_: &mut AsyncApp,
) -> Result<DebugAdapterBinary> {
@@ -86,7 +75,7 @@ impl PhpDebugAdapter {
.context("Couldn't find PHP dap directory")?
};
let tcp_connection = config.tcp_connection.clone().unwrap_or_default();
let tcp_connection = task_definition.tcp_connection.clone().unwrap_or_default();
let (host, port, timeout) = crate::configure_tcp_connection(tcp_connection).await?;
Ok(DebugAdapterBinary {
@@ -108,15 +97,204 @@ impl PhpDebugAdapter {
host,
timeout,
}),
cwd: None,
cwd: Some(delegate.worktree_root_path().to_path_buf()),
envs: HashMap::default(),
request_args: self.request_args(config)?,
request_args: StartDebuggingRequestArguments {
configuration: task_definition.config.clone(),
request: self.validate_config(&task_definition.config)?,
},
})
}
}
#[async_trait(?Send)]
impl DebugAdapter for PhpDebugAdapter {
async fn dap_schema(&self) -> serde_json::Value {
json!({
"properties": {
"request": {
"type": "string",
"enum": ["launch"],
"description": "The request type for the PHP debug adapter, always \"launch\"",
"default": "launch"
},
"hostname": {
"type": "string",
"description": "The address to bind to when listening for Xdebug (default: all IPv6 connections if available, else all IPv4 connections) or Unix Domain socket (prefix with unix://) or Windows Pipe (\\\\?\\pipe\\name) - cannot be combined with port"
},
"port": {
"type": "integer",
"description": "The port on which to listen for Xdebug (default: 9003). If port is set to 0 a random port is chosen by the system and a placeholder ${port} is replaced with the chosen port in env and runtimeArgs.",
"default": 9003
},
"program": {
"type": "string",
"description": "The PHP script to debug (typically a path to a file)",
"default": "${file}"
},
"cwd": {
"type": "string",
"description": "Working directory for the debugged program"
},
"args": {
"type": "array",
"items": {
"type": "string"
},
"description": "Command line arguments to pass to the program"
},
"env": {
"type": "object",
"description": "Environment variables to pass to the program",
"additionalProperties": {
"type": "string"
}
},
"stopOnEntry": {
"type": "boolean",
"description": "Whether to break at the beginning of the script",
"default": false
},
"pathMappings": {
"type": "array",
"description": "A list of server paths mapping to the local source paths on your machine for remote host debugging",
"items": {
"type": "object",
"properties": {
"serverPath": {
"type": "string",
"description": "Path on the server"
},
"localPath": {
"type": "string",
"description": "Corresponding path on the local machine"
}
},
"required": ["serverPath", "localPath"]
}
},
"log": {
"type": "boolean",
"description": "Whether to log all communication between editor and the adapter to the debug console",
"default": false
},
"ignore": {
"type": "array",
"description": "An array of glob patterns that errors should be ignored from (for example **/vendor/**/*.php)",
"items": {
"type": "string"
}
},
"ignoreExceptions": {
"type": "array",
"description": "An array of exception class names that should be ignored (for example BaseException, \\NS1\\Exception, \\*\\Exception or \\**\\Exception*)",
"items": {
"type": "string"
}
},
"skipFiles": {
"type": "array",
"description": "An array of glob patterns to skip when debugging. Star patterns and negations are allowed.",
"items": {
"type": "string"
}
},
"skipEntryPaths": {
"type": "array",
"description": "An array of glob patterns to immediately detach from and ignore for debugging if the entry script matches",
"items": {
"type": "string"
}
},
"maxConnections": {
"type": "integer",
"description": "Accept only this number of parallel debugging sessions. Additional connections will be dropped.",
"default": 1
},
"proxy": {
"type": "object",
"description": "DBGp Proxy settings",
"properties": {
"enable": {
"type": "boolean",
"description": "To enable proxy registration",
"default": false
},
"host": {
"type": "string",
"description": "The address of the proxy. Supports host name, IP address, or Unix domain socket.",
"default": "127.0.0.1"
},
"port": {
"type": "integer",
"description": "The port where the adapter will register with the proxy",
"default": 9001
},
"key": {
"type": "string",
"description": "A unique key that allows the proxy to match requests to your editor",
"default": "vsc"
},
"timeout": {
"type": "integer",
"description": "The number of milliseconds to wait before giving up on the connection to proxy",
"default": 3000
},
"allowMultipleSessions": {
"type": "boolean",
"description": "If the proxy should forward multiple sessions/connections at the same time or not",
"default": true
}
}
},
"xdebugSettings": {
"type": "object",
"description": "Allows you to override Xdebug's remote debugging settings to fine tune Xdebug to your needs",
"properties": {
"max_children": {
"type": "integer",
"description": "Max number of array or object children to initially retrieve"
},
"max_data": {
"type": "integer",
"description": "Max amount of variable data to initially retrieve"
},
"max_depth": {
"type": "integer",
"description": "Maximum depth that the debugger engine may return when sending arrays, hashes or object structures to the IDE"
},
"show_hidden": {
"type": "integer",
"description": "Whether to show detailed internal information on properties (e.g. private members of classes). Zero means hidden members are not shown.",
"enum": [0, 1]
},
"breakpoint_include_return_value": {
"type": "boolean",
"description": "Determines whether to enable an additional \"return from function\" debugging step, allowing inspection of the return value when a function call returns"
}
}
},
"xdebugCloudToken": {
"type": "string",
"description": "Instead of listening locally, open a connection and register with Xdebug Cloud and accept debugging sessions on that connection"
},
"stream": {
"type": "object",
"description": "Allows to influence DBGp streams. Xdebug only supports stdout",
"properties": {
"stdout": {
"type": "integer",
"description": "Redirect stdout stream: 0 (disable), 1 (copy), 2 (redirect)",
"enum": [0, 1, 2],
"default": 0
}
}
}
},
"required": ["request", "program"]
})
}
fn name(&self) -> DebugAdapterName {
DebugAdapterName(Self::ADAPTER_NAME.into())
}
@@ -125,10 +303,33 @@ impl DebugAdapter for PhpDebugAdapter {
Some(SharedString::new_static("PHP").into())
}
fn config_from_zed_format(&self, zed_scenario: ZedDebugConfig) -> Result<DebugScenario> {
let obj = match &zed_scenario.request {
dap::DebugRequest::Attach(_) => {
bail!("Php adapter doesn't support attaching")
}
dap::DebugRequest::Launch(launch_config) => json!({
"program": launch_config.program,
"cwd": launch_config.cwd,
"args": launch_config.args,
"env": launch_config.env_json(),
"stopOnEntry": zed_scenario.stop_on_entry.unwrap_or_default(),
}),
};
Ok(DebugScenario {
adapter: zed_scenario.adapter,
label: zed_scenario.label,
build: None,
config: obj,
tcp_connection: None,
})
}
async fn get_binary(
&self,
delegate: &Arc<dyn DapDelegate>,
config: &DebugTaskDefinition,
task_definition: &DebugTaskDefinition,
user_installed_path: Option<PathBuf>,
cx: &mut AsyncApp,
) -> Result<DebugAdapterBinary> {
@@ -145,7 +346,7 @@ impl DebugAdapter for PhpDebugAdapter {
}
}
self.get_installed_binary(delegate, &config, user_installed_path, cx)
self.get_installed_binary(delegate, &task_definition, user_installed_path, cx)
.await
}
}

View File

@@ -1,8 +1,13 @@
use crate::*;
use anyhow::Context as _;
use dap::{DebugRequest, StartDebuggingRequestArguments, adapters::DebugTaskDefinition};
use anyhow::{Context as _, anyhow};
use dap::{
DebugRequest, StartDebuggingRequestArguments, StartDebuggingRequestArgumentsRequest,
adapters::DebugTaskDefinition,
};
use gpui::{AsyncApp, SharedString};
use json_dotpath::DotPaths;
use language::LanguageName;
use serde_json::Value;
use std::{collections::HashMap, ffi::OsStr, path::PathBuf, sync::OnceLock};
use util::ResultExt;
@@ -17,39 +22,24 @@ impl PythonDebugAdapter {
const ADAPTER_PATH: &'static str = "src/debugpy/adapter";
const LANGUAGE_NAME: &'static str = "Python";
fn request_args(&self, config: &DebugTaskDefinition) -> StartDebuggingRequestArguments {
let mut args = json!({
"request": match config.request {
DebugRequest::Launch(_) => "launch",
DebugRequest::Attach(_) => "attach",
},
"subProcess": true,
"redirectOutput": true,
});
let map = args.as_object_mut().unwrap();
match &config.request {
DebugRequest::Attach(attach) => {
map.insert("processId".into(), attach.process_id.into());
}
DebugRequest::Launch(launch) => {
map.insert("program".into(), launch.program.clone().into());
map.insert("args".into(), launch.args.clone().into());
if !launch.env.is_empty() {
map.insert("env".into(), launch.env_json());
}
fn request_args(
&self,
task_definition: &DebugTaskDefinition,
) -> Result<StartDebuggingRequestArguments> {
let request = self.validate_config(&task_definition.config)?;
if let Some(stop_on_entry) = config.stop_on_entry {
map.insert("stopOnEntry".into(), stop_on_entry.into());
}
if let Some(cwd) = launch.cwd.as_ref() {
map.insert("cwd".into(), cwd.to_string_lossy().into_owned().into());
}
let mut configuration = task_definition.config.clone();
if let Ok(console) = configuration.dot_get_mut("console") {
// Use built-in Zed terminal if user did not explicitly provide a setting for console.
if console.is_null() {
*console = Value::String("integratedTerminal".into());
}
}
StartDebuggingRequestArguments {
configuration: args,
request: config.request.to_dap(),
}
Ok(StartDebuggingRequestArguments {
configuration,
request,
})
}
async fn fetch_latest_adapter_version(
&self,
@@ -158,9 +148,9 @@ impl PythonDebugAdapter {
port,
timeout,
}),
cwd: None,
cwd: Some(delegate.worktree_root_path().to_path_buf()),
envs: HashMap::default(),
request_args: self.request_args(config),
request_args: self.request_args(config)?,
})
}
}
@@ -175,6 +165,397 @@ impl DebugAdapter for PythonDebugAdapter {
Some(SharedString::new_static("Python").into())
}
fn config_from_zed_format(&self, zed_scenario: ZedDebugConfig) -> Result<DebugScenario> {
let mut args = json!({
"request": match zed_scenario.request {
DebugRequest::Launch(_) => "launch",
DebugRequest::Attach(_) => "attach",
},
"subProcess": true,
"redirectOutput": true,
});
let map = args.as_object_mut().unwrap();
match &zed_scenario.request {
DebugRequest::Attach(attach) => {
map.insert("processId".into(), attach.process_id.into());
}
DebugRequest::Launch(launch) => {
map.insert("program".into(), launch.program.clone().into());
map.insert("args".into(), launch.args.clone().into());
if !launch.env.is_empty() {
map.insert("env".into(), launch.env_json());
}
if let Some(stop_on_entry) = zed_scenario.stop_on_entry {
map.insert("stopOnEntry".into(), stop_on_entry.into());
}
if let Some(cwd) = launch.cwd.as_ref() {
map.insert("cwd".into(), cwd.to_string_lossy().into_owned().into());
}
}
}
Ok(DebugScenario {
adapter: zed_scenario.adapter,
label: zed_scenario.label,
config: args,
build: None,
tcp_connection: None,
})
}
fn validate_config(
&self,
config: &serde_json::Value,
) -> Result<StartDebuggingRequestArgumentsRequest> {
let map = config.as_object().context("Config isn't an object")?;
let request_variant = map
.get("request")
.and_then(|val| val.as_str())
.context("request is not valid")?;
match request_variant {
"launch" => Ok(StartDebuggingRequestArgumentsRequest::Launch),
"attach" => Ok(StartDebuggingRequestArgumentsRequest::Attach),
_ => Err(anyhow!("request must be either 'launch' or 'attach'")),
}
}
async fn dap_schema(&self) -> serde_json::Value {
json!({
"properties": {
"request": {
"type": "string",
"enum": ["attach", "launch"],
"description": "Debug adapter request type"
},
"autoReload": {
"default": {},
"description": "Configures automatic reload of code on edit.",
"properties": {
"enable": {
"default": false,
"description": "Automatically reload code on edit.",
"type": "boolean"
},
"exclude": {
"default": [
"**/.git/**",
"**/.metadata/**",
"**/__pycache__/**",
"**/node_modules/**",
"**/site-packages/**"
],
"description": "Glob patterns of paths to exclude from auto reload.",
"items": {
"type": "string"
},
"type": "array"
},
"include": {
"default": [
"**/*.py",
"**/*.pyw"
],
"description": "Glob patterns of paths to include in auto reload.",
"items": {
"type": "string"
},
"type": "array"
}
},
"type": "object"
},
"debugAdapterPath": {
"description": "Path (fully qualified) to the python debug adapter executable.",
"type": "string"
},
"django": {
"default": false,
"description": "Django debugging.",
"type": "boolean"
},
"jinja": {
"default": null,
"description": "Jinja template debugging (e.g. Flask).",
"enum": [
false,
null,
true
]
},
"justMyCode": {
"default": true,
"description": "If true, show and debug only user-written code. If false, show and debug all code, including library calls.",
"type": "boolean"
},
"logToFile": {
"default": false,
"description": "Enable logging of debugger events to a log file. This file can be found in the debugpy extension install folder.",
"type": "boolean"
},
"pathMappings": {
"default": [],
"items": {
"label": "Path mapping",
"properties": {
"localRoot": {
"default": "${ZED_WORKTREE_ROOT}",
"label": "Local source root.",
"type": "string"
},
"remoteRoot": {
"default": "",
"label": "Remote source root.",
"type": "string"
}
},
"required": [
"localRoot",
"remoteRoot"
],
"type": "object"
},
"label": "Path mappings.",
"type": "array"
},
"redirectOutput": {
"default": true,
"description": "Redirect output.",
"type": "boolean"
},
"showReturnValue": {
"default": true,
"description": "Show return value of functions when stepping.",
"type": "boolean"
},
"subProcess": {
"default": false,
"description": "Whether to enable Sub Process debugging",
"type": "boolean"
},
"consoleName": {
"default": "Python Debug Console",
"description": "Display name of the debug console or terminal",
"type": "string"
},
"clientOS": {
"default": null,
"description": "OS that VS code is using.",
"enum": [
"windows",
null,
"unix"
]
}
},
"required": ["request"],
"allOf": [
{
"if": {
"properties": {
"request": {
"enum": ["attach"]
}
}
},
"then": {
"properties": {
"connect": {
"label": "Attach by connecting to debugpy over a socket.",
"properties": {
"host": {
"default": "127.0.0.1",
"description": "Hostname or IP address to connect to.",
"type": "string"
},
"port": {
"description": "Port to connect to.",
"type": [
"number",
"string"
]
}
},
"required": [
"port"
],
"type": "object"
},
"listen": {
"label": "Attach by listening for incoming socket connection from debugpy",
"properties": {
"host": {
"default": "127.0.0.1",
"description": "Hostname or IP address of the interface to listen on.",
"type": "string"
},
"port": {
"description": "Port to listen on.",
"type": [
"number",
"string"
]
}
},
"required": [
"port"
],
"type": "object"
},
"processId": {
"anyOf": [
{
"default": "${command:pickProcess}",
"description": "Use process picker to select a process to attach, or Process ID as integer.",
"enum": [
"${command:pickProcess}"
]
},
{
"description": "ID of the local process to attach to.",
"type": "integer"
}
]
}
}
}
},
{
"if": {
"properties": {
"request": {
"enum": ["launch"]
}
}
},
"then": {
"properties": {
"args": {
"default": [],
"description": "Command line arguments passed to the program. For string type arguments, it will pass through the shell as is, and therefore all shell variable expansions will apply. But for the array type, the values will be shell-escaped.",
"items": {
"type": "string"
},
"anyOf": [
{
"default": "${command:pickArgs}",
"enum": [
"${command:pickArgs}"
]
},
{
"type": [
"array",
"string"
]
}
]
},
"console": {
"default": "integratedTerminal",
"description": "Where to launch the debug target: internal console, integrated terminal, or external terminal.",
"enum": [
"externalTerminal",
"integratedTerminal",
"internalConsole"
]
},
"cwd": {
"default": "${ZED_WORKTREE_ROOT}",
"description": "Absolute path to the working directory of the program being debugged. Default is the root directory of the file (leave empty).",
"type": "string"
},
"autoStartBrowser": {
"default": false,
"description": "Open external browser to launch the application",
"type": "boolean"
},
"env": {
"additionalProperties": {
"type": "string"
},
"default": {},
"description": "Environment variables defined as a key value pair. Property ends up being the Environment Variable and the value of the property ends up being the value of the Env Variable.",
"type": "object"
},
"envFile": {
"default": "${ZED_WORKTREE_ROOT}/.env",
"description": "Absolute path to a file containing environment variable definitions.",
"type": "string"
},
"gevent": {
"default": false,
"description": "Enable debugging of gevent monkey-patched code.",
"type": "boolean"
},
"module": {
"default": "",
"description": "Name of the module to be debugged.",
"type": "string"
},
"program": {
"default": "${ZED_FILE}",
"description": "Absolute path to the program.",
"type": "string"
},
"purpose": {
"default": [],
"description": "Tells extension to use this configuration for test debugging, or when using debug-in-terminal command.",
"items": {
"enum": [
"debug-test",
"debug-in-terminal"
],
"enumDescriptions": [
"Use this configuration while debugging tests using test view or test debug commands.",
"Use this configuration while debugging a file using debug in terminal button in the editor."
]
},
"type": "array"
},
"pyramid": {
"default": false,
"description": "Whether debugging Pyramid applications.",
"type": "boolean"
},
"python": {
"default": "${command:python.interpreterPath}",
"description": "Absolute path to the Python interpreter executable; overrides workspace configuration if set.",
"type": "string"
},
"pythonArgs": {
"default": [],
"description": "Command-line arguments passed to the Python interpreter. To pass arguments to the debug target, use \"args\".",
"items": {
"type": "string"
},
"type": "array"
},
"stopOnEntry": {
"default": false,
"description": "Automatically stop after launch.",
"type": "boolean"
},
"sudo": {
"default": false,
"description": "Running debug program under elevated permissions (on Unix).",
"type": "boolean"
},
"guiEventLoop": {
"default": "matplotlib",
"description": "The GUI event loop that's going to run. Possible values: \"matplotlib\", \"wx\", \"qt\", \"none\", or a custom function that'll be imported and run.",
"type": "string"
}
}
}
}
]
})
}
async fn get_binary(
&self,
delegate: &Arc<dyn DapDelegate>,

View File

@@ -3,16 +3,17 @@ use async_trait::async_trait;
use dap::{
DebugRequest, StartDebuggingRequestArguments,
adapters::{
self, DapDelegate, DebugAdapter, DebugAdapterBinary, DebugAdapterName, DebugTaskDefinition,
DapDelegate, DebugAdapter, DebugAdapterBinary, DebugAdapterName, DebugTaskDefinition,
},
};
use gpui::{AsyncApp, SharedString};
use language::LanguageName;
use std::{path::PathBuf, sync::Arc};
use serde_json::json;
use std::path::PathBuf;
use std::sync::Arc;
use task::{DebugScenario, ZedDebugConfig};
use util::command::new_smol_command;
use crate::ToDap;
#[derive(Default)]
pub(crate) struct RubyDebugAdapter;
@@ -30,6 +31,187 @@ impl DebugAdapter for RubyDebugAdapter {
Some(SharedString::new_static("Ruby").into())
}
async fn dap_schema(&self) -> serde_json::Value {
json!({
"oneOf": [
{
"allOf": [
{
"type": "object",
"required": ["request"],
"properties": {
"request": {
"type": "string",
"enum": ["launch"],
"description": "Request to launch a new process"
}
}
},
{
"type": "object",
"required": ["script"],
"properties": {
"command": {
"type": "string",
"description": "Command name (ruby, rake, bin/rails, bundle exec ruby, etc)",
"default": "ruby"
},
"script": {
"type": "string",
"description": "Absolute path to a Ruby file."
},
"cwd": {
"type": "string",
"description": "Directory to execute the program in",
"default": "${ZED_WORKTREE_ROOT}"
},
"args": {
"type": "array",
"description": "Command line arguments passed to the program",
"items": {
"type": "string"
},
"default": []
},
"env": {
"type": "object",
"description": "Additional environment variables to pass to the debugging (and debugged) process",
"default": {}
},
"showProtocolLog": {
"type": "boolean",
"description": "Show a log of DAP requests, events, and responses",
"default": false
},
"useBundler": {
"type": "boolean",
"description": "Execute Ruby programs with `bundle exec` instead of directly",
"default": false
},
"bundlePath": {
"type": "string",
"description": "Location of the bundle executable"
},
"rdbgPath": {
"type": "string",
"description": "Location of the rdbg executable"
},
"askParameters": {
"type": "boolean",
"description": "Ask parameters at first."
},
"debugPort": {
"type": "string",
"description": "UNIX domain socket name or TPC/IP host:port"
},
"waitLaunchTime": {
"type": "number",
"description": "Wait time before connection in milliseconds"
},
"localfs": {
"type": "boolean",
"description": "true if the VSCode and debugger run on a same machine",
"default": false
},
"useTerminal": {
"type": "boolean",
"description": "Create a new terminal and then execute commands there",
"default": false
}
}
}
]
},
{
"allOf": [
{
"type": "object",
"required": ["request"],
"properties": {
"request": {
"type": "string",
"enum": ["attach"],
"description": "Request to attach to an existing process"
}
}
},
{
"type": "object",
"properties": {
"rdbgPath": {
"type": "string",
"description": "Location of the rdbg executable"
},
"debugPort": {
"type": "string",
"description": "UNIX domain socket name or TPC/IP host:port"
},
"showProtocolLog": {
"type": "boolean",
"description": "Show a log of DAP requests, events, and responses",
"default": false
},
"localfs": {
"type": "boolean",
"description": "true if the VSCode and debugger run on a same machine",
"default": false
},
"localfsMap": {
"type": "string",
"description": "Specify pairs of remote root path and local root path like `/remote_dir:/local_dir`. You can specify multiple pairs like `/rem1:/loc1,/rem2:/loc2` by concatenating with `,`."
},
"env": {
"type": "object",
"description": "Additional environment variables to pass to the rdbg process",
"default": {}
}
}
}
]
}
]
})
}
fn config_from_zed_format(&self, zed_scenario: ZedDebugConfig) -> Result<DebugScenario> {
let mut config = serde_json::Map::new();
match &zed_scenario.request {
DebugRequest::Launch(launch) => {
config.insert("request".to_string(), json!("launch"));
config.insert("script".to_string(), json!(launch.program));
config.insert("command".to_string(), json!("ruby"));
if !launch.args.is_empty() {
config.insert("args".to_string(), json!(launch.args));
}
if !launch.env.is_empty() {
config.insert("env".to_string(), json!(launch.env));
}
if let Some(cwd) = &launch.cwd {
config.insert("cwd".to_string(), json!(cwd));
}
// Ruby stops on entry so there's no need to handle that case
}
DebugRequest::Attach(attach) => {
config.insert("request".to_string(), json!("attach"));
config.insert("processId".to_string(), json!(attach.process_id));
}
}
Ok(DebugScenario {
adapter: zed_scenario.adapter,
label: zed_scenario.label,
config: serde_json::Value::Object(config),
tcp_connection: None,
build: None,
})
}
async fn get_binary(
&self,
delegate: &Arc<dyn DapDelegate>,
@@ -66,34 +248,25 @@ impl DebugAdapter for RubyDebugAdapter {
let tcp_connection = definition.tcp_connection.clone().unwrap_or_default();
let (host, port, timeout) = crate::configure_tcp_connection(tcp_connection).await?;
let DebugRequest::Launch(launch) = definition.request.clone() else {
anyhow::bail!("rdbg does not yet support attaching");
};
let mut arguments = vec![
let arguments = vec![
"--open".to_string(),
format!("--port={}", port),
format!("--host={}", host),
];
if delegate.which(launch.program.as_ref()).await.is_some() {
arguments.push("--command".to_string())
}
arguments.push(launch.program);
arguments.extend(launch.args);
Ok(DebugAdapterBinary {
command: rdbg_path.to_string_lossy().to_string(),
arguments,
connection: Some(adapters::TcpArguments {
connection: Some(dap::adapters::TcpArguments {
host,
port,
timeout,
}),
cwd: launch.cwd,
envs: launch.env.into_iter().collect(),
cwd: None,
envs: std::collections::HashMap::default(),
request_args: StartDebuggingRequestArguments {
configuration: serde_json::Value::Object(Default::default()),
request: definition.request.to_dap(),
request: self.validate_config(&definition.config)?,
configuration: definition.config.clone(),
},
})
}

View File

@@ -11,6 +11,8 @@ async-trait.workspace = true
dap.workspace = true
extension.workspace = true
gpui.workspace = true
serde_json.workspace = true
task.workspace = true
workspace-hack = { version = "0.1", path = "../../tooling/workspace-hack" }
[lints]

View File

@@ -7,6 +7,7 @@ use dap::adapters::{
};
use extension::{Extension, WorktreeDelegate};
use gpui::AsyncApp;
use task::{DebugScenario, ZedDebugConfig};
pub(crate) struct ExtensionDapAdapter {
extension: Arc<dyn Extension>,
@@ -60,6 +61,10 @@ impl DebugAdapter for ExtensionDapAdapter {
self.debug_adapter_name.as_ref().into()
}
async fn dap_schema(&self) -> serde_json::Value {
self.extension.get_dap_schema().await.unwrap_or_default()
}
async fn get_binary(
&self,
delegate: &Arc<dyn DapDelegate>,
@@ -76,4 +81,8 @@ impl DebugAdapter for ExtensionDapAdapter {
)
.await
}
fn config_from_zed_format(&self, _zed_scenario: ZedDebugConfig) -> Result<DebugScenario> {
Err(anyhow::anyhow!("DAP extensions are not implemented yet"))
}
}

View File

@@ -1,15 +1,15 @@
use dap::DebugRequest;
use dap::adapters::DebugTaskDefinition;
use dap::{DapRegistry, DebugRequest};
use fuzzy::{StringMatch, StringMatchCandidate};
use gpui::{DismissEvent, Entity, EventEmitter, Focusable, Render};
use gpui::{AppContext, DismissEvent, Entity, EventEmitter, Focusable, Render};
use gpui::{Subscription, WeakEntity};
use picker::{Picker, PickerDelegate};
use task::ZedDebugConfig;
use util::debug_panic;
use std::sync::Arc;
use sysinfo::System;
use ui::{Context, Tooltip, prelude::*};
use ui::{ListItem, ListItemSpacing};
use util::debug_panic;
use workspace::{ModalView, Workspace};
use crate::debugger_panel::DebugPanel;
@@ -25,7 +25,7 @@ pub(crate) struct AttachModalDelegate {
selected_index: usize,
matches: Vec<StringMatch>,
placeholder_text: Arc<str>,
pub(crate) definition: DebugTaskDefinition,
pub(crate) definition: ZedDebugConfig,
workspace: WeakEntity<Workspace>,
candidates: Arc<[Candidate]>,
}
@@ -33,7 +33,7 @@ pub(crate) struct AttachModalDelegate {
impl AttachModalDelegate {
fn new(
workspace: WeakEntity<Workspace>,
definition: DebugTaskDefinition,
definition: ZedDebugConfig,
candidates: Arc<[Candidate]>,
) -> Self {
Self {
@@ -54,7 +54,7 @@ pub struct AttachModal {
impl AttachModal {
pub fn new(
definition: DebugTaskDefinition,
definition: ZedDebugConfig,
workspace: WeakEntity<Workspace>,
modal: bool,
window: &mut Window,
@@ -83,7 +83,7 @@ impl AttachModal {
pub(super) fn with_processes(
workspace: WeakEntity<Workspace>,
definition: DebugTaskDefinition,
definition: ZedDebugConfig,
processes: Arc<[Candidate]>,
modal: bool,
window: &mut Window,
@@ -228,7 +228,13 @@ impl PickerDelegate for AttachModalDelegate {
}
}
let scenario = self.definition.to_scenario();
let Some(scenario) = cx.read_global::<DapRegistry, _>(|registry, _| {
registry
.adapter(&self.definition.adapter)
.and_then(|adapter| adapter.config_from_zed_format(self.definition.clone()).ok())
}) else {
return;
};
let panel = self
.workspace

View File

@@ -5,7 +5,7 @@ use crate::{
ClearAllBreakpoints, Continue, Detach, FocusBreakpointList, FocusConsole, FocusFrames,
FocusLoadedSources, FocusModules, FocusTerminal, FocusVariables, Pause, Restart,
ShowStackTrace, StepBack, StepInto, StepOut, StepOver, Stop, ToggleIgnoreBreakpoints,
persistence,
ToggleSessionPicker, ToggleThreadPicker, persistence,
};
use anyhow::{Context as _, Result, anyhow};
use command_palette_hooks::CommandPaletteFilter;
@@ -31,7 +31,7 @@ use settings::Settings;
use std::any::TypeId;
use std::sync::Arc;
use task::{DebugScenario, TaskContext};
use ui::{ContextMenu, Divider, Tooltip, prelude::*};
use ui::{ContextMenu, Divider, PopoverMenuHandle, Tooltip, prelude::*};
use workspace::SplitDirection;
use workspace::{
Pane, Workspace,
@@ -65,6 +65,8 @@ pub struct DebugPanel {
workspace: WeakEntity<Workspace>,
focus_handle: FocusHandle,
context_menu: Option<(Entity<ContextMenu>, Point<Pixels>, Subscription)>,
pub(crate) thread_picker_menu_handle: PopoverMenuHandle<ContextMenu>,
pub(crate) session_picker_menu_handle: PopoverMenuHandle<ContextMenu>,
fs: Arc<dyn Fs>,
}
@@ -77,8 +79,10 @@ impl DebugPanel {
cx.new(|cx| {
let project = workspace.project().clone();
let focus_handle = cx.focus_handle();
let thread_picker_menu_handle = PopoverMenuHandle::default();
let session_picker_menu_handle = PopoverMenuHandle::default();
let debug_panel = Self {
Self {
size: px(300.),
sessions: vec![],
active_session: None,
@@ -87,9 +91,9 @@ impl DebugPanel {
workspace: workspace.weak_handle(),
context_menu: None,
fs: workspace.app_state().fs.clone(),
};
debug_panel
thread_picker_menu_handle,
session_picker_menu_handle,
}
})
}
@@ -269,7 +273,7 @@ impl DebugPanel {
let session = session.clone();
async move |this, cx| {
let debug_session =
Self::register_session(this.clone(), session.clone(), cx).await?;
Self::register_session(this.clone(), session.clone(), true, cx).await?;
let definition = debug_session
.update_in(cx, |debug_session, window, cx| {
debug_session.running_state().update(cx, |running, cx| {
@@ -295,6 +299,7 @@ impl DebugPanel {
cx.spawn(async move |_, cx| {
if let Err(error) = task.await {
log::error!("{error}");
session
.update(cx, |session, cx| {
session
@@ -313,69 +318,21 @@ impl DebugPanel {
pub(crate) async fn register_session(
this: WeakEntity<Self>,
session: Entity<Session>,
focus: bool,
cx: &mut AsyncWindowContext,
) -> Result<Entity<DebugSession>> {
let adapter_name = session.update(cx, |session, _| session.adapter())?;
this.update_in(cx, |_, window, cx| {
cx.subscribe_in(
&session,
window,
move |this, session, event: &SessionStateEvent, window, cx| match event {
SessionStateEvent::Restart => {
this.handle_restart_request(session.clone(), window, cx);
}
SessionStateEvent::SpawnChildSession { request } => {
this.handle_start_debugging_request(request, session.clone(), window, cx);
}
_ => {}
},
)
.detach();
})
.ok();
let debug_session = register_session_inner(&this, session, cx).await?;
let serialized_layout = persistence::get_serialized_layout(adapter_name).await;
let workspace = this.update_in(cx, |this, window, cx| {
if focus {
this.activate_session(debug_session.clone(), window, cx);
}
let (debug_session, workspace) = this.update_in(cx, |this, window, cx| {
this.sessions.retain(|session| {
!session
.read(cx)
.running_state()
.read(cx)
.session()
.read(cx)
.is_terminated()
});
let debug_session = DebugSession::running(
this.project.clone(),
this.workspace.clone(),
session,
cx.weak_entity(),
serialized_layout,
this.position(window, cx).axis(),
window,
cx,
);
// We might want to make this an event subscription and only notify when a new thread is selected
// This is used to filter the command menu correctly
cx.observe(
&debug_session.read(cx).running_state().clone(),
|_, _, cx| cx.notify(),
)
.detach();
this.sessions.push(debug_session.clone());
this.activate_session(debug_session.clone(), window, cx);
(debug_session, this.workspace.clone())
this.workspace.clone()
})?;
workspace.update_in(cx, |workspace, window, cx| {
workspace.focus_panel::<Self>(window, cx);
})?;
Ok(debug_session)
}
@@ -413,7 +370,7 @@ impl DebugPanel {
});
(session, task)
})?;
Self::register_session(this, session, cx).await?;
Self::register_session(this.clone(), session, true, cx).await?;
task.await
})
.detach_and_log_err(cx);
@@ -436,7 +393,6 @@ impl DebugPanel {
let adapter = parent_session.read(cx).adapter().clone();
let mut binary = parent_session.read(cx).binary().clone();
binary.request_args = request.clone();
cx.spawn_in(window, async move |this, cx| {
let (session, task) = dap_store_handle.update(cx, |dap_store, cx| {
let session =
@@ -447,7 +403,7 @@ impl DebugPanel {
});
(session, task)
})?;
Self::register_session(this, session, cx).await?;
Self::register_session(this, session, false, cx).await?;
task.await
})
.detach_and_log_err(cx);
@@ -746,55 +702,6 @@ impl DebugPanel {
}),
)
.child(Divider::vertical())
.child(
IconButton::new(
"debug-enable-breakpoint",
IconName::DebugDisabledBreakpoint,
)
.icon_size(IconSize::XSmall)
.shape(ui::IconButtonShape::Square)
.disabled(thread_status != ThreadStatus::Stopped),
)
.child(
IconButton::new(
"debug-disable-breakpoint",
IconName::CircleOff,
)
.icon_size(IconSize::XSmall)
.shape(ui::IconButtonShape::Square)
.disabled(thread_status != ThreadStatus::Stopped),
)
.child(
IconButton::new(
"debug-disable-all-breakpoints",
IconName::BugOff,
)
.icon_size(IconSize::XSmall)
.shape(ui::IconButtonShape::Square)
.disabled(
thread_status == ThreadStatus::Exited
|| thread_status == ThreadStatus::Ended,
)
.on_click(window.listener_for(
&running_state,
|this, _, _window, cx| {
this.toggle_ignore_breakpoints(cx);
},
))
.tooltip({
let focus_handle = focus_handle.clone();
move |window, cx| {
Tooltip::for_action_in(
"Disable all breakpoints",
&ToggleIgnoreBreakpoints,
&focus_handle,
window,
cx,
)
}
}),
)
.child(Divider::vertical())
.child(
IconButton::new("debug-restart", IconName::DebugRestart)
.icon_size(IconSize::XSmall)
@@ -954,6 +861,21 @@ impl DebugPanel {
}
}
pub(crate) fn activate_session_by_id(
&mut self,
session_id: SessionId,
window: &mut Window,
cx: &mut Context<Self>,
) {
if let Some(session) = self
.sessions
.iter()
.find(|session| session.read(cx).session_id(cx) == session_id)
{
self.activate_session(session.clone(), window, cx);
}
}
pub(crate) fn activate_session(
&mut self,
session_item: Entity<DebugSession>,
@@ -967,7 +889,7 @@ impl DebugPanel {
this.go_to_selected_stack_frame(window, cx);
});
});
self.active_session = Some(session_item.clone());
self.active_session = Some(session_item);
cx.notify();
}
@@ -1033,6 +955,75 @@ impl DebugPanel {
})
.unwrap_or_else(|err| Task::ready(Err(err)))
}
pub(crate) fn toggle_thread_picker(&mut self, window: &mut Window, cx: &mut Context<Self>) {
self.thread_picker_menu_handle.toggle(window, cx);
}
pub(crate) fn toggle_session_picker(&mut self, window: &mut Window, cx: &mut Context<Self>) {
self.session_picker_menu_handle.toggle(window, cx);
}
}
async fn register_session_inner(
this: &WeakEntity<DebugPanel>,
session: Entity<Session>,
cx: &mut AsyncWindowContext,
) -> Result<Entity<DebugSession>> {
let adapter_name = session.update(cx, |session, _| session.adapter())?;
this.update_in(cx, |_, window, cx| {
cx.subscribe_in(
&session,
window,
move |this, session, event: &SessionStateEvent, window, cx| match event {
SessionStateEvent::Restart => {
this.handle_restart_request(session.clone(), window, cx);
}
SessionStateEvent::SpawnChildSession { request } => {
this.handle_start_debugging_request(request, session.clone(), window, cx);
}
_ => {}
},
)
.detach();
})
.ok();
let serialized_layout = persistence::get_serialized_layout(adapter_name).await;
let debug_session = this.update_in(cx, |this, window, cx| {
this.sessions.retain(|session| {
!session
.read(cx)
.running_state()
.read(cx)
.session()
.read(cx)
.is_terminated()
});
let debug_session = DebugSession::running(
this.project.clone(),
this.workspace.clone(),
session,
cx.weak_entity(),
serialized_layout,
this.position(window, cx).axis(),
window,
cx,
);
// We might want to make this an event subscription and only notify when a new thread is selected
// This is used to filter the command menu correctly
cx.observe(
&debug_session.read(cx).running_state().clone(),
|_, _, cx| cx.notify(),
)
.detach();
this.sessions.push(debug_session.clone());
debug_session
})?;
Ok(debug_session)
}
impl EventEmitter<PanelEvent> for DebugPanel {}
@@ -1249,6 +1240,24 @@ impl Render for DebugPanel {
.ok();
}
})
.on_action({
let this = this.clone();
move |_: &ToggleThreadPicker, window, cx| {
this.update(cx, |this, cx| {
this.toggle_thread_picker(window, cx);
})
.ok();
}
})
.on_action({
let this = this.clone();
move |_: &ToggleSessionPicker, window, cx| {
this.update(cx, |this, cx| {
this.toggle_session_picker(window, cx);
})
.ok();
}
})
.when(self.active_session.is_some(), |this| {
this.on_mouse_down(
MouseButton::Right,

View File

@@ -45,6 +45,8 @@ actions!(
FocusLoadedSources,
FocusTerminal,
ShowStackTrace,
ToggleThreadPicker,
ToggleSessionPicker,
]
);
@@ -93,6 +95,17 @@ pub fn init(cx: &mut App) {
}
}
})
.register_action(|workspace, _: &Continue, _, cx| {
if let Some(debug_panel) = workspace.panel::<DebugPanel>(cx) {
if let Some(active_item) = debug_panel.read_with(cx, |panel, cx| {
panel
.active_session()
.map(|session| session.read(cx).running_state().clone())
}) {
active_item.update(cx, |item, cx| item.continue_thread(cx))
}
}
})
.register_action(|workspace, _: &StepInto, _, cx| {
if let Some(debug_panel) = workspace.panel::<DebugPanel>(cx) {
if let Some(active_item) = debug_panel.read_with(cx, |panel, cx| {

View File

@@ -132,7 +132,8 @@ impl DebugPanel {
this
}),
)
.style(DropdownStyle::Ghost),
.style(DropdownStyle::Ghost)
.handle(self.session_picker_menu_handle.clone()),
)
} else {
None
@@ -163,7 +164,7 @@ impl DebugPanel {
DropdownMenu::new_with_element(
("thread-list", session_id.0),
trigger,
ContextMenu::build_eager(window, cx, move |mut this, _, _| {
ContextMenu::build(window, cx, move |mut this, _, _| {
for (thread, _) in threads {
let running_state = running_state.clone();
let thread_id = thread.id;
@@ -177,7 +178,8 @@ impl DebugPanel {
}),
)
.disabled(session_terminated)
.style(DropdownStyle::Ghost),
.style(DropdownStyle::Ghost)
.handle(self.thread_picker_menu_handle.clone()),
)
} else {
None

View File

@@ -10,8 +10,7 @@ use std::{
};
use dap::{
DapRegistry, DebugRequest,
adapters::{DebugAdapterName, DebugTaskDefinition},
DapRegistry, DebugRequest, TelemetrySpawnLocation, adapters::DebugAdapterName, send_telemetry,
};
use editor::{Editor, EditorElement, EditorStyle};
use fuzzy::{StringMatch, StringMatchCandidate};
@@ -22,7 +21,7 @@ use gpui::{
use picker::{Picker, PickerDelegate, highlighted_match_with_paths::HighlightedMatch};
use project::{ProjectPath, TaskContexts, TaskSourceKind, task_store::TaskStore};
use settings::Settings;
use task::{DebugScenario, LaunchRequest};
use task::{DebugScenario, LaunchRequest, ZedDebugConfig};
use theme::ThemeSettings;
use ui::{
ActiveTheme, Button, ButtonCommon, ButtonSize, CheckboxWithLabel, Clickable, Color, Context,
@@ -210,15 +209,16 @@ impl NewSessionModal {
None
};
Some(DebugScenario {
let session_scenario = ZedDebugConfig {
adapter: debugger.to_owned().into(),
label,
request: Some(request),
initialize_args: None,
tcp_connection: None,
request: request,
stop_on_entry,
build: None,
})
};
cx.global::<DapRegistry>()
.adapter(&session_scenario.adapter)
.and_then(|adapter| adapter.config_from_zed_format(session_scenario).ok())
}
fn start_new_session(&self, window: &mut Window, cx: &mut Context<Self>) {
@@ -242,6 +242,7 @@ impl NewSessionModal {
let Some(task_contexts) = self.task_contexts(cx) else {
return;
};
send_telemetry(&config, TelemetrySpawnLocation::Custom, cx);
let task_context = task_contexts.active_context().cloned().unwrap_or_default();
let worktree_id = task_contexts.worktree();
cx.spawn_in(window, async move |this, cx| {
@@ -264,12 +265,12 @@ impl NewSessionModal {
cx: &mut App,
) {
attach.update(cx, |this, cx| {
if adapter != &this.definition.adapter {
this.definition.adapter = adapter.clone();
if adapter.0 != this.definition.adapter {
this.definition.adapter = adapter.0.clone();
this.attach_picker.update(cx, |this, cx| {
this.picker.update(cx, |this, cx| {
this.delegate.definition.adapter = adapter.clone();
this.delegate.definition.adapter = adapter.0.clone();
this.focus(window, cx);
})
});
@@ -279,8 +280,8 @@ impl NewSessionModal {
})
}
fn task_contexts<'a>(&self, cx: &'a mut Context<Self>) -> Option<&'a TaskContexts> {
self.launch_picker.read(cx).delegate.task_contexts.as_ref()
fn task_contexts(&self, cx: &App) -> Option<Arc<TaskContexts>> {
self.launch_picker.read(cx).delegate.task_contexts.clone()
}
fn adapter_drop_down_menu(
@@ -805,8 +806,6 @@ impl CustomMode {
let args = args.collect::<Vec<_>>();
let (program, path) = resolve_paths(program, path);
task::LaunchRequest {
program,
cwd: path.is_empty().not().then(|| PathBuf::from(path)),
@@ -862,7 +861,7 @@ impl CustomMode {
#[derive(Clone)]
pub(super) struct AttachMode {
pub(super) definition: DebugTaskDefinition,
pub(super) definition: ZedDebugConfig,
pub(super) attach_picker: Entity<AttachModal>,
}
@@ -873,12 +872,10 @@ impl AttachMode {
window: &mut Window,
cx: &mut Context<NewSessionModal>,
) -> Entity<Self> {
let definition = DebugTaskDefinition {
adapter: debugger.unwrap_or(DebugAdapterName("".into())),
let definition = ZedDebugConfig {
adapter: debugger.unwrap_or(DebugAdapterName("".into())).0,
label: "Attach New Session Setup".into(),
request: dap::DebugRequest::Attach(task::AttachRequest { process_id: None }),
initialize_args: None,
tcp_connection: None,
stop_on_entry: Some(false),
};
let attach_picker = cx.new(|cx| {
@@ -905,7 +902,7 @@ pub(super) struct DebugScenarioDelegate {
matches: Vec<StringMatch>,
prompt: String,
debug_panel: WeakEntity<DebugPanel>,
task_contexts: Option<TaskContexts>,
task_contexts: Option<Arc<TaskContexts>>,
divider_index: Option<usize>,
last_used_candidate_index: Option<usize>,
}
@@ -938,27 +935,14 @@ impl DebugScenarioDelegate {
});
let language = language.or_else(|| {
scenario
.request
.as_ref()
.and_then(|request| match request {
DebugRequest::Launch(launch) => launch
.program
.rsplit_once(".")
.and_then(|split| languages.language_name_for_extension(split.1))
.map(|name| TaskSourceKind::Language { name: name.into() }),
_ => None,
})
.or_else(|| {
scenario.label.split_whitespace().find_map(|word| {
language_names
.iter()
.find(|name| name.eq_ignore_ascii_case(word))
.map(|name| TaskSourceKind::Language {
name: name.to_owned().into(),
})
scenario.label.split_whitespace().find_map(|word| {
language_names
.iter()
.find(|name| name.eq_ignore_ascii_case(word))
.map(|name| TaskSourceKind::Language {
name: name.to_owned().into(),
})
})
})
});
(language, scenario)
@@ -971,7 +955,7 @@ impl DebugScenarioDelegate {
_window: &mut Window,
cx: &mut Context<Picker<Self>>,
) {
self.task_contexts = Some(task_contexts);
self.task_contexts = Some(Arc::new(task_contexts));
let (recent, scenarios) = self
.task_store
@@ -1092,7 +1076,7 @@ impl PickerDelegate for DebugScenarioDelegate {
.get(self.selected_index())
.and_then(|match_candidate| self.candidates.get(match_candidate.candidate_id).cloned());
let Some((_, mut debug_scenario)) = debug_scenario else {
let Some((_, debug_scenario)) = debug_scenario else {
return;
};
@@ -1107,19 +1091,7 @@ impl PickerDelegate for DebugScenarioDelegate {
})
.unwrap_or_default();
if let Some(launch_config) =
debug_scenario
.request
.as_mut()
.and_then(|request| match request {
DebugRequest::Launch(launch) => Some(launch),
_ => None,
})
{
let (program, _) = resolve_paths(launch_config.program.clone(), String::new());
launch_config.program = program;
}
send_telemetry(&debug_scenario, TelemetrySpawnLocation::ScenarioList, cx);
self.debug_panel
.update(cx, |panel, cx| {
panel.start_session(debug_scenario, task_context, None, worktree_id, window, cx);
@@ -1173,34 +1145,41 @@ impl PickerDelegate for DebugScenarioDelegate {
}
}
fn resolve_paths(program: String, path: String) -> (String, String) {
let program = if let Some(program) = program.strip_prefix('~') {
format!(
pub(crate) fn resolve_path(path: &mut String) {
if path.starts_with('~') {
let home = paths::home_dir().to_string_lossy().to_string();
let trimmed_path = path.trim().to_owned();
*path = trimmed_path.replacen('~', &home, 1);
} else if let Some(strip_path) = path.strip_prefix(&format!(".{}", std::path::MAIN_SEPARATOR)) {
*path = format!(
"$ZED_WORKTREE_ROOT{}{}",
std::path::MAIN_SEPARATOR,
&program
)
} else if !program.starts_with(std::path::MAIN_SEPARATOR) {
format!(
"$ZED_WORKTREE_ROOT{}{}",
std::path::MAIN_SEPARATOR,
&program
)
} else {
program
&strip_path
);
};
let path = if path.starts_with('~') && !path.is_empty() {
format!(
"$ZED_WORKTREE_ROOT{}{}",
std::path::MAIN_SEPARATOR,
&path[1..]
)
} else if !path.starts_with(std::path::MAIN_SEPARATOR) && !path.is_empty() {
format!("$ZED_WORKTREE_ROOT{}{}", std::path::MAIN_SEPARATOR, &path)
} else {
path
};
(program, path)
}
#[cfg(test)]
mod tests {
use paths::home_dir;
#[test]
fn test_normalize_paths() {
let sep = std::path::MAIN_SEPARATOR;
let home = home_dir().to_string_lossy().to_string();
let resolve_path = |path: &str| -> String {
let mut path = path.to_string();
super::resolve_path(&mut path);
path
};
assert_eq!(resolve_path("bin"), format!("bin"));
assert_eq!(resolve_path(&format!("{sep}foo")), format!("{sep}foo"));
assert_eq!(resolve_path(""), format!(""));
assert_eq!(
resolve_path(&format!("~{sep}blah")),
format!("{home}{sep}blah")
);
assert_eq!(resolve_path("~"), home);
}
}

View File

@@ -7,7 +7,10 @@ pub mod variable_list;
use std::{any::Any, ops::ControlFlow, path::PathBuf, sync::Arc, time::Duration};
use crate::persistence::{self, DebuggerPaneItem, SerializedLayout};
use crate::{
new_session_modal::resolve_path,
persistence::{self, DebuggerPaneItem, SerializedLayout},
};
use super::DebugPanelItemEvent;
use anyhow::{Context as _, Result, anyhow};
@@ -15,7 +18,7 @@ use breakpoint_list::BreakpointList;
use collections::{HashMap, IndexMap};
use console::Console;
use dap::{
Capabilities, RunInTerminalRequestArguments, Thread,
Capabilities, DapRegistry, RunInTerminalRequestArguments, Thread,
adapters::{DebugAdapterName, DebugTaskDefinition},
client::SessionId,
debugger_settings::DebuggerSettings,
@@ -38,8 +41,8 @@ use serde_json::Value;
use settings::Settings;
use stack_frame_list::StackFrameList;
use task::{
BuildTaskDefinition, DebugScenario, LaunchRequest, ShellBuilder, SpawnInTerminal, TaskContext,
substitute_variables_in_map, substitute_variables_in_str,
BuildTaskDefinition, DebugScenario, ShellBuilder, SpawnInTerminal, TaskContext, ZedDebugConfig,
substitute_variables_in_str,
};
use terminal_view::TerminalView;
use ui::{
@@ -96,7 +99,7 @@ impl Render for RunningState {
.find(|pane| pane.read(cx).is_zoomed());
let active = self.panes.panes().into_iter().next();
let x = if let Some(ref zoomed_pane) = zoomed_pane {
let pane = if let Some(ref zoomed_pane) = zoomed_pane {
zoomed_pane.update(cx, |pane, cx| pane.render(window, cx).into_any_element())
} else if let Some(active) = active {
self.panes
@@ -122,7 +125,7 @@ impl Render for RunningState {
.size_full()
.key_context("DebugSessionItem")
.track_focus(&self.focus_handle(cx))
.child(h_flex().flex_1().child(x))
.child(h_flex().flex_1().child(pane))
}
}
@@ -519,6 +522,56 @@ impl Focusable for DebugTerminal {
}
impl RunningState {
// todo(debugger) move this to util and make it so you pass a closure to it that converts a string
pub(crate) fn substitute_variables_in_config(
config: &mut serde_json::Value,
context: &TaskContext,
) {
match config {
serde_json::Value::Object(obj) => {
obj.values_mut()
.for_each(|value| Self::substitute_variables_in_config(value, context));
}
serde_json::Value::Array(array) => {
array
.iter_mut()
.for_each(|value| Self::substitute_variables_in_config(value, context));
}
serde_json::Value::String(s) => {
if let Some(substituted) = substitute_variables_in_str(&s, context) {
*s = substituted;
}
}
_ => {}
}
}
pub(crate) fn relativlize_paths(
key: Option<&str>,
config: &mut serde_json::Value,
context: &TaskContext,
) {
match config {
serde_json::Value::Object(obj) => {
obj.iter_mut()
.for_each(|(key, value)| Self::relativlize_paths(Some(key), value, context));
}
serde_json::Value::Array(array) => {
array
.iter_mut()
.for_each(|value| Self::relativlize_paths(None, value, context));
}
serde_json::Value::String(s) if key == Some("program") || key == Some("cwd") => {
resolve_path(s);
if let Some(substituted) = substitute_variables_in_str(&s, context) {
*s = substituted;
}
}
_ => {}
}
}
pub(crate) fn new(
session: Entity<Session>,
project: Entity<Project>,
@@ -561,15 +614,26 @@ impl RunningState {
cx.subscribe_in(&session, window, |this, _, event, window, cx| {
match event {
SessionEvent::Stopped(thread_id) => {
this.workspace
let panel = this
.workspace
.update(cx, |workspace, cx| {
workspace.open_panel::<crate::DebugPanel>(window, cx);
workspace.panel::<crate::DebugPanel>(cx)
})
.log_err();
.log_err()
.flatten();
if let Some(thread_id) = thread_id {
this.select_thread(*thread_id, window, cx);
}
if let Some(panel) = panel {
let id = this.session_id;
window.defer(cx, move |window, cx| {
panel.update(cx, |this, cx| {
this.activate_session_by_id(id, window, cx);
})
})
}
}
SessionEvent::Threads => {
let threads = this.session.update(cx, |this, cx| this.threads(cx));
@@ -628,10 +692,9 @@ impl RunningState {
&workspace,
&stack_frame_list,
&variable_list,
&module_list,
&loaded_source_list,
&console,
&breakpoint_list,
&debug_terminal,
dock_axis,
&mut pane_close_subscriptions,
window,
@@ -705,6 +768,7 @@ impl RunningState {
};
let project = workspace.read(cx).project().clone();
let dap_store = project.read(cx).dap_store().downgrade();
let dap_registry = cx.global::<DapRegistry>().clone();
let task_store = project.read(cx).task_store().downgrade();
let weak_project = project.downgrade();
let weak_workspace = workspace.downgrade();
@@ -714,11 +778,19 @@ impl RunningState {
adapter,
label,
build,
request,
initialize_args,
mut config,
tcp_connection,
stop_on_entry,
} = scenario;
Self::relativlize_paths(None, &mut config, &task_context);
Self::substitute_variables_in_config(&mut config, &task_context);
let request_type = dap_registry
.adapter(&adapter)
.ok_or_else(|| anyhow!("{}: is not a valid adapter name", &adapter))
.and_then(|adapter| adapter.validate_config(&config));
let config_is_valid = request_type.is_ok();
let build_output = if let Some(build) = build {
let (task, locator_name) = match build {
BuildTaskDefinition::Template {
@@ -747,9 +819,9 @@ impl RunningState {
};
let locator_name = if let Some(locator_name) = locator_name {
debug_assert!(request.is_none());
debug_assert!(!config_is_valid);
Some(locator_name)
} else if request.is_none() {
} else if !config_is_valid {
dap_store
.update(cx, |this, cx| {
this.debug_scenario_for_build_task(
@@ -826,63 +898,44 @@ impl RunningState {
} else {
None
};
let request = if let Some(request) = request {
request
if config_is_valid {
// Ok(DebugTaskDefinition {
// label,
// adapter: DebugAdapterName(adapter),
// config,
// tcp_connection,
// })
} else if let Some((task, locator_name)) = build_output {
let locator_name =
locator_name.context("Could not find a valid locator for a build task")?;
dap_store
let request = dap_store
.update(cx, |this, cx| {
this.run_debug_locator(&locator_name, task, cx)
})?
.await?
.await?;
let zed_config = ZedDebugConfig {
label: label.clone(),
adapter: adapter.clone(),
request,
stop_on_entry: None,
};
let scenario = dap_registry
.adapter(&adapter)
.ok_or_else(|| anyhow!("{}: is not a valid adapter name", &adapter))
.map(|adapter| adapter.config_from_zed_format(zed_config))??;
config = scenario.config;
Self::substitute_variables_in_config(&mut config, &task_context);
} else {
anyhow::bail!("No request or build provided");
};
let request = match request {
dap::DebugRequest::Launch(launch_request) => {
let cwd = match launch_request.cwd.as_deref().and_then(|path| path.to_str()) {
Some(cwd) => {
let substituted_cwd = substitute_variables_in_str(&cwd, &task_context)
.context("substituting variables in cwd")?;
Some(PathBuf::from(substituted_cwd))
}
None => None,
};
let env = substitute_variables_in_map(
&launch_request.env.into_iter().collect(),
&task_context,
)
.context("substituting variables in env")?
.into_iter()
.collect();
let new_launch_request = LaunchRequest {
program: substitute_variables_in_str(
&launch_request.program,
&task_context,
)
.context("substituting variables in program")?,
args: launch_request
.args
.into_iter()
.map(|arg| substitute_variables_in_str(&arg, &task_context))
.collect::<Option<Vec<_>>>()
.context("substituting variables in args")?,
cwd,
env,
};
dap::DebugRequest::Launch(new_launch_request)
}
request @ dap::DebugRequest::Attach(_) => request, // todo(debugger): We should check that process_id is valid and if not show the modal
};
Ok(DebugTaskDefinition {
label,
adapter: DebugAdapterName(adapter),
request,
initialize_args,
stop_on_entry,
config,
tcp_connection,
})
})
@@ -1468,10 +1521,9 @@ impl RunningState {
workspace: &WeakEntity<Workspace>,
stack_frame_list: &Entity<StackFrameList>,
variable_list: &Entity<VariableList>,
module_list: &Entity<ModuleList>,
loaded_source_list: &Entity<LoadedSourceList>,
console: &Entity<Console>,
breakpoints: &Entity<BreakpointList>,
debug_terminal: &Entity<DebugTerminal>,
dock_axis: Axis,
subscriptions: &mut HashMap<EntityId, Subscription>,
window: &mut Window,
@@ -1512,6 +1564,26 @@ impl RunningState {
let center_pane = new_debugger_pane(workspace.clone(), project.clone(), window, cx);
center_pane.update(cx, |this, cx| {
let weak_console = console.downgrade();
this.add_item(
Box::new(SubView::new(
console.focus_handle(cx),
console.clone().into(),
DebuggerPaneItem::Console,
Some(Box::new(move |cx| {
weak_console
.read_with(cx, |console, cx| console.show_indicator(cx))
.unwrap_or_default()
})),
cx,
)),
true,
false,
None,
window,
cx,
);
this.add_item(
Box::new(SubView::new(
variable_list.focus_handle(cx),
@@ -1526,54 +1598,20 @@ impl RunningState {
window,
cx,
);
this.add_item(
Box::new(SubView::new(
module_list.focus_handle(cx),
module_list.clone().into(),
DebuggerPaneItem::Modules,
None,
cx,
)),
false,
false,
None,
window,
cx,
);
this.add_item(
Box::new(SubView::new(
loaded_source_list.focus_handle(cx),
loaded_source_list.clone().into(),
DebuggerPaneItem::LoadedSources,
None,
cx,
)),
false,
false,
None,
window,
cx,
);
this.activate_item(0, false, false, window, cx);
});
let rightmost_pane = new_debugger_pane(workspace.clone(), project.clone(), window, cx);
rightmost_pane.update(cx, |this, cx| {
let weak_console = console.downgrade();
this.add_item(
Box::new(SubView::new(
this.focus_handle(cx),
console.clone().into(),
DebuggerPaneItem::Console,
Some(Box::new(move |cx| {
weak_console
.read_with(cx, |console, cx| console.show_indicator(cx))
.unwrap_or_default()
})),
debug_terminal.focus_handle(cx),
debug_terminal.clone().into(),
DebuggerPaneItem::Terminal,
None,
cx,
)),
true,
false,
false,
None,
window,

View File

@@ -14,7 +14,7 @@ use language::{Buffer, CodeLabel, ToOffset};
use menu::Confirm;
use project::{
Completion,
debugger::session::{CompletionsQuery, OutputToken, Session},
debugger::session::{CompletionsQuery, OutputToken, Session, SessionEvent},
};
use settings::Settings;
use std::{cell::RefCell, rc::Rc, usize};
@@ -79,6 +79,11 @@ impl Console {
let _subscriptions = vec![
cx.subscribe(&stack_frame_list, Self::handle_stack_frame_list_events),
cx.subscribe_in(&session, window, |this, _, event, window, cx| {
if let SessionEvent::ConsoleOutput = event {
this.update_output(window, cx)
}
}),
cx.on_focus_in(&focus_handle, window, |console, window, cx| {
if console.is_running(cx) {
console.query_bar.focus_handle(cx).focus(window);
@@ -200,12 +205,11 @@ impl Console {
fn render_query_bar(&self, cx: &Context<Self>) -> impl IntoElement {
EditorElement::new(&self.query_bar, self.editor_style(cx))
}
}
impl Render for Console {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
fn update_output(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let session = self.session.clone();
let token = self.last_token;
self.update_output_task = cx.spawn_in(window, async move |this, cx| {
_ = session.update_in(cx, move |session, window, cx| {
let (output, last_processed_token) = session.output(token);
@@ -220,7 +224,11 @@ impl Render for Console {
});
});
});
}
}
impl Render for Console {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
v_flex()
.track_focus(&self.focus_handle)
.key_context("DebugConsole")

View File

@@ -154,12 +154,15 @@ impl VariableList {
let _subscriptions = vec![
cx.subscribe(&stack_frame_list, Self::handle_stack_frame_list_events),
cx.subscribe(&session, |this, _, event, _| match event {
cx.subscribe(&session, |this, _, event, cx| match event {
SessionEvent::Stopped(_) => {
this.selection.take();
this.edited_path.take();
this.selected_stack_frame_id.take();
}
SessionEvent::Variables => {
this.build_entries(cx);
}
_ => {}
}),
cx.on_focus_out(&focus_handle, window, |this, _, _, cx| {
@@ -300,7 +303,7 @@ impl VariableList {
match event {
StackFrameListEvent::SelectedStackFrameChanged(stack_frame_id) => {
self.selected_stack_frame_id = Some(*stack_frame_id);
cx.notify();
self.build_entries(cx);
}
StackFrameListEvent::BuiltEntries => {}
}
@@ -344,14 +347,14 @@ impl VariableList {
};
entry.is_expanded = !entry.is_expanded;
cx.notify();
self.build_entries(cx);
}
fn select_first(&mut self, _: &SelectFirst, window: &mut Window, cx: &mut Context<Self>) {
self.cancel_variable_edit(&Default::default(), window, cx);
if let Some(variable) = self.entries.first() {
self.selection = Some(variable.path.clone());
cx.notify();
self.build_entries(cx);
}
}
@@ -359,7 +362,7 @@ impl VariableList {
self.cancel_variable_edit(&Default::default(), window, cx);
if let Some(variable) = self.entries.last() {
self.selection = Some(variable.path.clone());
cx.notify();
self.build_entries(cx);
}
}
@@ -378,7 +381,7 @@ impl VariableList {
index.and_then(|ix| self.entries.get(ix).map(|var| var.path.clone()))
{
self.selection = Some(new_selection);
cx.notify();
self.build_entries(cx);
} else {
self.select_last(&SelectLast, window, cx);
}
@@ -402,7 +405,7 @@ impl VariableList {
index.and_then(|ix| self.entries.get(ix).map(|var| var.path.clone()))
{
self.selection = Some(new_selection);
cx.notify();
self.build_entries(cx);
} else {
self.select_first(&SelectFirst, window, cx);
}
@@ -464,7 +467,7 @@ impl VariableList {
self.select_prev(&SelectPrevious, window, cx);
} else {
entry_state.is_expanded = false;
cx.notify();
self.build_entries(cx);
}
}
}
@@ -485,7 +488,7 @@ impl VariableList {
self.select_next(&SelectNext, window, cx);
} else {
entry_state.is_expanded = true;
cx.notify();
self.build_entries(cx);
}
}
}
@@ -929,8 +932,6 @@ impl Focusable for VariableList {
impl Render for VariableList {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
self.build_entries(cx);
v_flex()
.track_focus(&self.focus_handle)
.key_context("VariableList")
@@ -946,7 +947,6 @@ impl Render for VariableList {
.on_action(cx.listener(Self::collapse_selected_entry))
.on_action(cx.listener(Self::cancel_variable_edit))
.on_action(cx.listener(Self::confirm_variable_edit))
//
.child(
uniform_list(
cx.entity().clone(),

View File

@@ -2,7 +2,7 @@ use std::sync::Arc;
use anyhow::{Context as _, Result};
use dap::adapters::DebugTaskDefinition;
use dap::{DebugRequest, client::DebugAdapterClient};
use dap::client::DebugAdapterClient;
use gpui::{Entity, TestAppContext, WindowHandle};
use project::{Project, debugger::session::Session};
use settings::SettingsStore;
@@ -25,6 +25,9 @@ mod inline_values;
#[cfg(test)]
mod module_list;
#[cfg(test)]
#[cfg(not(windows))]
mod new_session_modal;
#[cfg(test)]
mod persistence;
#[cfg(test)]
mod stack_frame_list;
@@ -136,16 +139,18 @@ pub fn start_debug_session<T: Fn(&Arc<DebugAdapterClient>) + 'static>(
cx: &mut gpui::TestAppContext,
configure: T,
) -> Result<Entity<Session>> {
use serde_json::json;
start_debug_session_with(
workspace,
cx,
DebugTaskDefinition {
adapter: "fake-adapter".into(),
request: DebugRequest::Launch(Default::default()),
label: "test".into(),
initialize_args: None,
config: json!({
"request": "launch"
}),
tcp_connection: None,
stop_on_entry: None,
},
configure,
)

View File

@@ -5,7 +5,7 @@ use gpui::{BackgroundExecutor, TestAppContext, VisualTestContext};
use menu::Confirm;
use project::{FakeFs, Project};
use serde_json::json;
use task::{AttachRequest, TcpArgumentsTemplate};
use task::AttachRequest;
use tests::{init_test, init_test_workspace};
use util::path;
@@ -32,13 +32,12 @@ async fn test_direct_attach_to_process(executor: BackgroundExecutor, cx: &mut Te
cx,
DebugTaskDefinition {
adapter: "fake-adapter".into(),
request: dap::DebugRequest::Attach(AttachRequest {
process_id: Some(10),
}),
label: "label".into(),
initialize_args: None,
config: json!({
"request": "attach",
"process_id": 10,
}),
tcp_connection: None,
stop_on_entry: None,
},
|client| {
client.on_request::<dap::requests::Attach, _>(move |_, args| {
@@ -107,13 +106,10 @@ async fn test_show_attach_modal_and_select_process(
workspace.toggle_modal(window, cx, |window, cx| {
AttachModal::with_processes(
workspace_handle,
DebugTaskDefinition {
task::ZedDebugConfig {
adapter: FakeAdapter::ADAPTER_NAME.into(),
request: dap::DebugRequest::Attach(AttachRequest::default()),
label: "attach example".into(),
initialize_args: None,
tcp_connection: Some(TcpArgumentsTemplate::default()),
stop_on_entry: None,
},
vec![

View File

@@ -24,14 +24,12 @@ use project::{
};
use serde_json::json;
use std::{
collections::HashMap,
path::Path,
sync::{
Arc,
atomic::{AtomicBool, Ordering},
},
};
use task::LaunchRequest;
use terminal_view::terminal_panel::TerminalPanel;
use tests::{active_debug_session_panel, init_test, init_test_workspace};
use util::path;
@@ -425,6 +423,13 @@ async fn test_handle_start_debugging_request(
}
});
let sessions = workspace
.update(cx, |workspace, _window, cx| {
let debug_panel = workspace.panel::<DebugPanel>(cx).unwrap();
debug_panel.read(cx).sessions()
})
.unwrap();
assert_eq!(sessions.len(), 1);
client
.fake_reverse_request::<StartDebugging>(StartDebuggingRequestArguments {
request: StartDebuggingRequestArgumentsRequest::Launch,
@@ -437,20 +442,42 @@ async fn test_handle_start_debugging_request(
workspace
.update(cx, |workspace, _window, cx| {
let debug_panel = workspace.panel::<DebugPanel>(cx).unwrap();
// Active session does not change on spawn.
let active_session = debug_panel
.read(cx)
.active_session()
.unwrap()
.read(cx)
.session(cx);
let parent_session = active_session.read(cx).parent_session().unwrap();
assert_eq!(active_session, sessions[0].read(cx).session(cx));
assert!(active_session.read(cx).parent_session().is_none());
let current_sessions = debug_panel.read(cx).sessions();
assert_eq!(current_sessions.len(), 2);
assert_eq!(current_sessions[0], sessions[0]);
let parent_session = current_sessions[1]
.read(cx)
.session(cx)
.read(cx)
.parent_session()
.unwrap();
assert_eq!(parent_session, &sessions[0].read(cx).session(cx));
// We should preserve the original binary (params to spawn process etc.) except for launch params
// (as they come from reverse spawn request).
let mut original_binary = parent_session.read(cx).binary().clone();
original_binary.request_args = StartDebuggingRequestArguments {
request: StartDebuggingRequestArgumentsRequest::Launch,
configuration: fake_config.clone(),
};
assert_eq!(active_session.read(cx).binary(), &original_binary);
assert_eq!(
current_sessions[1].read(cx).session(cx).read(cx).binary(),
&original_binary
);
})
.unwrap();
@@ -1388,16 +1415,15 @@ async fn test_we_send_arguments_from_user_config(
let cx = &mut VisualTestContext::from_window(*workspace, cx);
let debug_definition = DebugTaskDefinition {
adapter: "fake-adapter".into(),
request: dap::DebugRequest::Launch(LaunchRequest {
program: "main.rs".to_owned(),
args: vec!["arg1".to_owned(), "arg2".to_owned()],
cwd: Some(path!("/Random_path").into()),
env: HashMap::from_iter(vec![("KEY".to_owned(), "VALUE".to_owned())]),
config: json!({
"request": "launch",
"program": "main.rs".to_owned(),
"args": vec!["arg1".to_owned(), "arg2".to_owned()],
"cwd": path!("/Random_path"),
"env": json!({ "KEY": "VALUE" }),
}),
label: "test".into(),
initialize_args: None,
tcp_connection: None,
stop_on_entry: None,
};
let launch_handler_called = Arc::new(AtomicBool::new(false));

View File

@@ -1,5 +1,6 @@
use crate::{
debugger_panel::DebugPanel,
persistence::DebuggerPaneItem,
tests::{active_debug_session_panel, init_test, init_test_workspace, start_debug_session},
};
use dap::{
@@ -110,7 +111,8 @@ async fn test_module_list(executor: BackgroundExecutor, cx: &mut TestAppContext)
});
running_state.update_in(cx, |this, window, cx| {
this.activate_item(crate::persistence::DebuggerPaneItem::Modules, window, cx);
this.ensure_pane_item(DebuggerPaneItem::Modules, window, cx);
this.activate_item(DebuggerPaneItem::Modules, window, cx);
cx.refresh_windows();
});

View File

@@ -0,0 +1,157 @@
use gpui::{BackgroundExecutor, TestAppContext, VisualTestContext};
use project::{FakeFs, Project};
use serde_json::json;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use task::{DebugScenario, TaskContext, VariableName};
use util::path;
use crate::tests::{init_test, init_test_workspace};
// todo(tasks) figure out why task replacement is broken on windows
#[gpui::test]
async fn test_debug_session_substitutes_variables_and_relativizes_paths(
executor: BackgroundExecutor,
cx: &mut TestAppContext,
) {
init_test(cx);
let fs = FakeFs::new(executor.clone());
fs.insert_tree(
path!("/project"),
json!({
"main.rs": "fn main() {}"
}),
)
.await;
let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
let workspace = init_test_workspace(&project, cx).await;
let cx = &mut VisualTestContext::from_window(*workspace, cx);
// Set up task variables to simulate a real environment
let test_variables = vec![(
VariableName::WorktreeRoot,
"/test/worktree/path".to_string(),
)]
.into_iter()
.collect();
let task_context = TaskContext {
cwd: None,
task_variables: test_variables,
project_env: Default::default(),
};
let home_dir = paths::home_dir();
let sep = std::path::MAIN_SEPARATOR;
// Test cases for different path formats
let test_cases: Vec<(Arc<String>, Arc<String>)> = vec![
// Absolute path - should not be relativized
(
Arc::from(format!("{0}absolute{0}path{0}to{0}program", sep)),
Arc::from(format!("{0}absolute{0}path{0}to{0}program", sep)),
),
// Relative path - should be prefixed with worktree root
(
Arc::from(format!(".{0}src{0}program", sep)),
Arc::from(format!("{0}test{0}worktree{0}path{0}src{0}program", sep)),
),
// Home directory path - should be prefixed with worktree root
(
Arc::from(format!("~{0}src{0}program", sep)),
Arc::from(format!(
"{1}{0}src{0}program",
sep,
home_dir.to_string_lossy()
)),
),
// Path with $ZED_WORKTREE_ROOT - should be substituted without double appending
(
Arc::from(format!("$ZED_WORKTREE_ROOT{0}src{0}program", sep)),
Arc::from(format!("{0}test{0}worktree{0}path{0}src{0}program", sep)),
),
];
let called_launch = Arc::new(AtomicBool::new(false));
for (input_path, expected_path) in test_cases {
let _subscription = project::debugger::test::intercept_debug_sessions(cx, {
let called_launch = called_launch.clone();
let input_path = input_path.clone();
let expected_path = expected_path.clone();
move |client| {
client.on_request::<dap::requests::Launch, _>({
let called_launch = called_launch.clone();
let input_path = input_path.clone();
let expected_path = expected_path.clone();
move |_, args| {
let config = args.raw.as_object().unwrap();
// Verify the program path was substituted correctly
assert_eq!(
config["program"].as_str().unwrap(),
expected_path.as_str(),
"Program path was not correctly substituted for input: {}",
input_path.as_str()
);
// Verify the cwd path was substituted correctly
assert_eq!(
config["cwd"].as_str().unwrap(),
expected_path.as_str(),
"CWD path was not correctly substituted for input: {}",
input_path.as_str()
);
// Verify that otherField was substituted but not relativized
// It should still have $ZED_WORKTREE_ROOT substituted if present
let expected_other_field = if input_path.contains("$ZED_WORKTREE_ROOT") {
input_path.replace("$ZED_WORKTREE_ROOT", "/test/worktree/path")
} else {
input_path.to_string()
};
assert_eq!(
config["otherField"].as_str().unwrap(),
expected_other_field,
"Other field was incorrectly modified for input: {}",
input_path
);
called_launch.store(true, Ordering::SeqCst);
Ok(())
}
});
}
});
let scenario = DebugScenario {
adapter: "fake-adapter".into(),
label: "test-debug-session".into(),
build: None,
config: json!({
"request": "launch",
"program": input_path,
"cwd": input_path,
"otherField": input_path
}),
tcp_connection: None,
};
workspace
.update(cx, |workspace, window, cx| {
workspace.start_debug_session(scenario, task_context.clone(), None, window, cx)
})
.unwrap();
cx.run_until_parked();
assert!(called_launch.load(Ordering::SeqCst));
called_launch.store(false, Ordering::SeqCst);
}
}

View File

@@ -5,6 +5,7 @@ use std::sync::{
use crate::{
DebugPanel,
persistence::DebuggerPaneItem,
session::running::variable_list::{CollapseSelectedEntry, ExpandSelectedEntry},
tests::{active_debug_session_panel, init_test, init_test_workspace, start_debug_session},
};
@@ -706,7 +707,13 @@ async fn test_keyboard_navigation(executor: BackgroundExecutor, cx: &mut TestApp
cx.focus_self(window);
let running = item.running_state().clone();
let variable_list = running.read_with(cx, |state, _| state.variable_list().clone());
let variable_list = running.update(cx, |state, cx| {
// have to do this because the variable list pane should be shown/active
// for testing keyboard navigation
state.activate_item(DebuggerPaneItem::Variables, window, cx);
state.variable_list().clone()
});
variable_list.update(cx, |_, cx| cx.focus_self(window));
running
});

View File

@@ -37,6 +37,7 @@ clock.workspace = true
collections.workspace = true
command_palette_hooks.workspace = true
convert_case.workspace = true
dap.workspace = true
db.workspace = true
buffer_diff.workspace = true
emojis.workspace = true

View File

@@ -59,6 +59,7 @@ use client::{Collaborator, ParticipantIndex};
use clock::{AGENT_REPLICA_ID, ReplicaId};
use collections::{BTreeMap, HashMap, HashSet, VecDeque};
use convert_case::{Case, Casing};
use dap::TelemetrySpawnLocation;
use display_map::*;
pub use display_map::{ChunkRenderer, ChunkRendererContext, DisplayPoint, FoldPlaceholder};
pub use editor_settings::{
@@ -3964,15 +3965,18 @@ impl Editor {
.skip(num_of_whitespaces)
.take(max_len_of_delimiter)
.collect::<String>();
let (delimiter, trimmed_len) =
delimiters.iter().find_map(|delimiter| {
let trimmed = delimiter.trim_end();
if comment_candidate.starts_with(trimmed) {
Some((delimiter, trimmed.len()))
let (delimiter, trimmed_len) = delimiters
.iter()
.filter_map(|delimiter| {
let prefix = delimiter.trim_end();
if comment_candidate.starts_with(prefix) {
Some((delimiter, prefix.len()))
} else {
None
}
})?;
})
.max_by_key(|(_, len)| *len)?;
let cursor_is_placed_after_comment_marker =
num_of_whitespaces + trimmed_len <= start_point.column as usize;
if cursor_is_placed_after_comment_marker {
@@ -4040,31 +4044,29 @@ impl Editor {
};
let cursor_is_before_end_tag_if_exists = {
let num_of_whitespaces_rev = snapshot
.reversed_chars_for_range(range.clone())
.take_while(|c| c.is_whitespace())
.count();
let mut line_iter = snapshot
.reversed_chars_for_range(range)
.skip(num_of_whitespaces_rev);
let end_tag_exists = end_tag
.chars()
.rev()
.all(|char| line_iter.next() == Some(char));
if end_tag_exists {
let max_point = snapshot.line_len(start_point.row) as usize;
let ordering = (num_of_whitespaces_rev
+ end_tag.len()
+ start_point.column as usize)
.cmp(&max_point);
let mut char_position = 0u32;
let mut end_tag_offset = None;
'outer: for chunk in snapshot.text_for_range(range.clone()) {
if let Some(byte_pos) = chunk.find(&**end_tag) {
let chars_before_match =
chunk[..byte_pos].chars().count() as u32;
end_tag_offset =
Some(char_position + chars_before_match);
break 'outer;
}
char_position += chunk.chars().count() as u32;
}
if let Some(end_tag_offset) = end_tag_offset {
let cursor_is_before_end_tag =
ordering != Ordering::Greater;
start_point.column <= end_tag_offset;
if cursor_is_after_start_tag {
if cursor_is_before_end_tag {
insert_extra_newline = true;
}
let cursor_is_at_start_of_end_tag =
ordering == Ordering::Equal;
start_point.column == end_tag_offset;
if cursor_is_at_start_of_end_tag {
indent_on_extra_newline.len = (*len).into();
}
@@ -5616,6 +5618,7 @@ impl Editor {
let context = actions_menu.actions.context.clone();
workspace.update(cx, |workspace, cx| {
dap::send_telemetry(&scenario, TelemetrySpawnLocation::Gutter, cx);
workspace.start_debug_session(scenario, context, Some(buffer), window, cx);
});
Some(Task::ready(Ok(())))
@@ -6520,6 +6523,10 @@ impl Editor {
provider.accept(cx);
}
// Store the transaction ID and selections before applying the edit
let transaction_id_prev =
self.buffer.read_with(cx, |b, cx| b.last_transaction_id(cx));
let snapshot = self.buffer.read(cx).snapshot(cx);
let last_edit_end = edits.last().unwrap().0.end.bias_right(&snapshot);
@@ -6528,9 +6535,20 @@ impl Editor {
});
self.change_selections(None, window, cx, |s| {
s.select_anchor_ranges([last_edit_end..last_edit_end])
s.select_anchor_ranges([last_edit_end..last_edit_end]);
});
let selections = self.selections.disjoint_anchors();
if let Some(transaction_id_now) =
self.buffer.read_with(cx, |b, cx| b.last_transaction_id(cx))
{
let has_new_transaction = transaction_id_prev != Some(transaction_id_now);
if has_new_transaction {
self.selection_history
.insert_transaction(transaction_id_now, selections);
}
}
self.update_visible_inline_completion(window, cx);
if self.active_inline_completion.is_none() {
self.refresh_inline_completion(true, true, window, cx);
@@ -7263,24 +7281,22 @@ impl Editor {
..Default::default()
};
let primary_action_text = if breakpoint.is_disabled() {
"enable"
"Enable breakpoint"
} else if is_phantom && !collides_with_existing {
"set"
"Set breakpoint"
} else {
"unset"
"Unset breakpoint"
};
let mut primary_text = format!("Click to {primary_action_text}");
if collides_with_existing && !breakpoint.is_disabled() {
use std::fmt::Write;
write!(primary_text, ", {alt_as_text}-click to disable").ok();
}
let primary_text = SharedString::from(primary_text);
let focus_handle = self.focus_handle.clone();
let meta = if is_rejected {
"No executable code is associated with this line."
SharedString::from("No executable code is associated with this line.")
} else if collides_with_existing && !breakpoint.is_disabled() {
SharedString::from(format!(
"{alt_as_text}-click to disable,\nright-click for more options."
))
} else {
"Right-click for more options."
SharedString::from("Right-click for more options.")
};
IconButton::new(("breakpoint_indicator", row.0 as usize), icon)
.icon_size(IconSize::XSmall)
@@ -7319,7 +7335,14 @@ impl Editor {
);
}))
.tooltip(move |window, cx| {
Tooltip::with_meta_in(primary_text.clone(), None, meta, &focus_handle, window, cx)
Tooltip::with_meta_in(
primary_action_text,
Some(&ToggleBreakpoint),
meta.clone(),
&focus_handle,
window,
cx,
)
})
}
@@ -20236,6 +20259,7 @@ impl SemanticsProvider for Entity<Project> {
fn inline_values(
&self,
buffer_handle: Entity<Buffer>,
range: Range<text::Anchor>,
cx: &mut App,
) -> Option<Task<anyhow::Result<Vec<InlayHint>>>> {

View File

@@ -1,6 +1,7 @@
use super::*;
use crate::{
JoinLines,
inline_completion_tests::FakeInlineCompletionProvider,
linked_editing_ranges::LinkedEditingRanges,
scroll::scroll_amount::ScrollAmount,
test::{
@@ -2820,6 +2821,42 @@ async fn test_newline_comments(cx: &mut TestAppContext) {
"});
}
#[gpui::test]
async fn test_newline_comments_with_multiple_delimiters(cx: &mut TestAppContext) {
init_test(cx, |settings| {
settings.defaults.tab_size = NonZeroU32::new(4)
});
let language = Arc::new(Language::new(
LanguageConfig {
line_comments: vec!["// ".into(), "/// ".into()],
..LanguageConfig::default()
},
None,
));
{
let mut cx = EditorTestContext::new(cx).await;
cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
cx.set_state(indoc! {"
//ˇ
"});
cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
cx.assert_editor_state(indoc! {"
//
// ˇ
"});
cx.set_state(indoc! {"
///ˇ
"});
cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
cx.assert_editor_state(indoc! {"
///
/// ˇ
"});
}
}
#[gpui::test]
async fn test_newline_documentation_comments(cx: &mut TestAppContext) {
init_test(cx, |settings| {
@@ -2975,6 +3012,32 @@ async fn test_newline_documentation_comments(cx: &mut TestAppContext) {
*/
ˇ
"});
// Ensure that inline comment followed by code
// doesn't add comment prefix on newline
cx.set_state(indoc! {"
/** */ textˇ
"});
cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
cx.assert_editor_state(indoc! {"
/** */ text
ˇ
"});
// Ensure that text after comment end tag
// doesn't add comment prefix on newline
cx.set_state(indoc! {"
/**
*
*/ˇtext
"});
cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
cx.assert_editor_state(indoc! {"
/**
*
*/
ˇtext
"});
}
// Ensure that comment continuations can be disabled.
update_test_language_settings(cx, |settings| {
@@ -5965,8 +6028,34 @@ async fn test_add_selection_above_below(cx: &mut TestAppContext) {
cx.assert_editor_state(indoc!(
r#"abc
defˇghi
ˇ
jk
nlmo
"#
));
cx.update_editor(|editor, window, cx| {
editor.add_selection_below(&Default::default(), window, cx);
});
cx.assert_editor_state(indoc!(
r#"abc
defˇghi
ˇ
jkˇ
nlmo
"#
));
cx.update_editor(|editor, window, cx| {
editor.add_selection_below(&Default::default(), window, cx);
});
cx.assert_editor_state(indoc!(
r#"abc
defˇghi
ˇ
jkˇ
nlmˇo
"#
));
@@ -5978,10 +6067,10 @@ async fn test_add_selection_above_below(cx: &mut TestAppContext) {
cx.assert_editor_state(indoc!(
r#"abc
defˇghi
jk
ˇ
jkˇ
nlmˇo
"#
ˇ"#
));
// change selections
@@ -6318,6 +6407,98 @@ async fn test_undo_format_scrolls_to_last_edit_pos(cx: &mut TestAppContext) {
"});
}
#[gpui::test]
async fn test_undo_inline_completion_scrolls_to_edit_pos(cx: &mut TestAppContext) {
init_test(cx, |_| {});
let mut cx = EditorTestContext::new(cx).await;
let provider = cx.new(|_| FakeInlineCompletionProvider::default());
cx.update_editor(|editor, window, cx| {
editor.set_edit_prediction_provider(Some(provider.clone()), window, cx);
});
cx.set_state(indoc! {"
line 1
line 2
linˇe 3
line 4
line 5
line 6
line 7
line 8
line 9
line 10
"});
let snapshot = cx.buffer_snapshot();
let edit_position = snapshot.anchor_after(Point::new(2, 4));
cx.update(|_, cx| {
provider.update(cx, |provider, _| {
provider.set_inline_completion(Some(inline_completion::InlineCompletion {
id: None,
edits: vec![(edit_position..edit_position, "X".into())],
edit_preview: None,
}))
})
});
cx.update_editor(|editor, window, cx| editor.update_visible_inline_completion(window, cx));
cx.update_editor(|editor, window, cx| {
editor.accept_edit_prediction(&crate::AcceptEditPrediction, window, cx)
});
cx.assert_editor_state(indoc! {"
line 1
line 2
lineXˇ 3
line 4
line 5
line 6
line 7
line 8
line 9
line 10
"});
cx.update_editor(|editor, window, cx| {
editor.change_selections(None, window, cx, |s| {
s.select_ranges([Point::new(9, 2)..Point::new(9, 2)]);
});
});
cx.assert_editor_state(indoc! {"
line 1
line 2
lineX 3
line 4
line 5
line 6
line 7
line 8
line 9
liˇne 10
"});
cx.update_editor(|editor, window, cx| {
editor.undo(&Default::default(), window, cx);
});
cx.assert_editor_state(indoc! {"
line 1
line 2
lineˇ 3
line 4
line 5
line 6
line 7
line 8
line 9
line 10
"});
}
#[gpui::test]
async fn test_select_next_with_multiple_carets(cx: &mut TestAppContext) {
init_test(cx, |_| {});

View File

@@ -3597,7 +3597,7 @@ impl EditorElement {
style: &EditorStyle,
window: &mut Window,
cx: &mut App,
) {
) -> Option<ContextMenuLayout> {
let mut min_menu_height = Pixels::ZERO;
let mut max_menu_height = Pixels::ZERO;
let mut height_above_menu = Pixels::ZERO;
@@ -3638,7 +3638,7 @@ impl EditorElement {
let visible = edit_prediction_popover_visible || context_menu_visible;
if !visible {
return;
return None;
}
let cursor_row_layout = &line_layouts[cursor.row().minus(start_row) as usize];
@@ -3663,7 +3663,7 @@ impl EditorElement {
let min_height = height_above_menu + min_menu_height + height_below_menu;
let max_height = height_above_menu + max_menu_height + height_below_menu;
let Some((laid_out_popovers, y_flipped)) = self.layout_popovers_above_or_below_line(
let (laid_out_popovers, y_flipped) = self.layout_popovers_above_or_below_line(
target_position,
line_height,
min_height,
@@ -3721,16 +3721,11 @@ impl EditorElement {
.flatten()
.collect::<Vec<_>>()
},
) else {
return;
};
)?;
let Some((menu_ix, (_, menu_bounds))) = laid_out_popovers
let (menu_ix, (_, menu_bounds)) = laid_out_popovers
.iter()
.find_position(|(x, _)| matches!(x, CursorPopoverType::CodeContextMenu))
else {
return;
};
.find_position(|(x, _)| matches!(x, CursorPopoverType::CodeContextMenu))?;
let last_ix = laid_out_popovers.len() - 1;
let menu_is_last = menu_ix == last_ix;
let first_popover_bounds = laid_out_popovers[0].1;
@@ -3771,7 +3766,7 @@ impl EditorElement {
false
};
self.layout_context_menu_aside(
let aside_bounds = self.layout_context_menu_aside(
y_flipped,
*menu_bounds,
target_bounds,
@@ -3783,6 +3778,23 @@ impl EditorElement {
window,
cx,
);
if let Some(menu_bounds) = laid_out_popovers.iter().find_map(|(popover_type, bounds)| {
if matches!(popover_type, CursorPopoverType::CodeContextMenu) {
Some(*bounds)
} else {
None
}
}) {
let bounds = if let Some(aside_bounds) = aside_bounds {
menu_bounds.union(&aside_bounds)
} else {
menu_bounds
};
return Some(ContextMenuLayout { y_flipped, bounds });
}
None
}
fn layout_gutter_menu(
@@ -3988,7 +4000,7 @@ impl EditorElement {
viewport_bounds: Bounds<Pixels>,
window: &mut Window,
cx: &mut App,
) {
) -> Option<Bounds<Pixels>> {
let available_within_viewport = target_bounds.space_within(&viewport_bounds);
let positioned_aside = if available_within_viewport.right >= MENU_ASIDE_MIN_WIDTH
&& !must_place_above_or_below
@@ -3997,16 +4009,14 @@ impl EditorElement {
available_within_viewport.right - px(1.),
MENU_ASIDE_MAX_WIDTH,
);
let Some(mut aside) = self.render_context_menu_aside(
let mut aside = self.render_context_menu_aside(
size(max_width, max_height - POPOVER_Y_PADDING),
window,
cx,
) else {
return;
};
aside.layout_as_root(AvailableSpace::min_size(), window, cx);
)?;
let size = aside.layout_as_root(AvailableSpace::min_size(), window, cx);
let right_position = point(target_bounds.right(), menu_bounds.origin.y);
Some((aside, right_position))
Some((aside, right_position, size))
} else {
let max_size = size(
// TODO(mgsloan): Once the menu is bounded by viewport width the bound on viewport
@@ -4023,9 +4033,7 @@ impl EditorElement {
),
) - POPOVER_Y_PADDING,
);
let Some(mut aside) = self.render_context_menu_aside(max_size, window, cx) else {
return;
};
let mut aside = self.render_context_menu_aside(max_size, window, cx)?;
let actual_size = aside.layout_as_root(AvailableSpace::min_size(), window, cx);
let top_position = point(
@@ -4059,13 +4067,17 @@ impl EditorElement {
// Fallback: fit actual size in window.
.or_else(|| fit_within(available_within_viewport, actual_size));
aside_position.map(|position| (aside, position))
aside_position.map(|position| (aside, position, actual_size))
};
// Skip drawing if it doesn't fit anywhere.
if let Some((aside, position)) = positioned_aside {
if let Some((aside, position, size)) = positioned_aside {
let aside_bounds = Bounds::new(position, size);
window.defer_draw(aside, position, 2);
return Some(aside_bounds);
}
None
}
fn render_context_menu(
@@ -4174,13 +4186,13 @@ impl EditorElement {
&self,
snapshot: &EditorSnapshot,
hitbox: &Hitbox,
text_hitbox: &Hitbox,
visible_display_row_range: Range<DisplayRow>,
content_origin: gpui::Point<Pixels>,
scroll_pixel_position: gpui::Point<Pixels>,
line_layouts: &[LineWithInvisibles],
line_height: Pixels,
em_width: Pixels,
context_menu_layout: Option<ContextMenuLayout>,
window: &mut Window,
cx: &mut App,
) {
@@ -4224,21 +4236,24 @@ impl EditorElement {
let mut overall_height = Pixels::ZERO;
let mut measured_hover_popovers = Vec::new();
for mut hover_popover in hover_popovers {
for (position, mut hover_popover) in hover_popovers.into_iter().with_position() {
let size = hover_popover.layout_as_root(AvailableSpace::min_size(), window, cx);
let horizontal_offset =
(text_hitbox.top_right().x - POPOVER_RIGHT_OFFSET - (hovered_point.x + size.width))
(hitbox.top_right().x - POPOVER_RIGHT_OFFSET - (hovered_point.x + size.width))
.min(Pixels::ZERO);
overall_height += HOVER_POPOVER_GAP + size.height;
match position {
itertools::Position::Middle | itertools::Position::Last => {
overall_height += HOVER_POPOVER_GAP
}
_ => {}
}
overall_height += size.height;
measured_hover_popovers.push(MeasuredHoverPopover {
element: hover_popover,
size,
horizontal_offset,
});
}
overall_height += HOVER_POPOVER_GAP;
fn draw_occluder(
width: Pixels,
@@ -4255,8 +4270,12 @@ impl EditorElement {
window.defer_draw(occlusion, origin, 2);
}
if hovered_point.y > overall_height {
// There is enough space above. Render popovers above the hovered point
fn place_popovers_above(
hovered_point: gpui::Point<Pixels>,
measured_hover_popovers: Vec<MeasuredHoverPopover>,
window: &mut Window,
cx: &mut App,
) {
let mut current_y = hovered_point.y;
for (position, popover) in measured_hover_popovers.into_iter().with_position() {
let size = popover.size;
@@ -4273,8 +4292,15 @@ impl EditorElement {
current_y = popover_origin.y - HOVER_POPOVER_GAP;
}
} else {
// There is not enough space above. Render popovers below the hovered point
}
fn place_popovers_below(
hovered_point: gpui::Point<Pixels>,
measured_hover_popovers: Vec<MeasuredHoverPopover>,
line_height: Pixels,
window: &mut Window,
cx: &mut App,
) {
let mut current_y = hovered_point.y + line_height;
for (position, popover) in measured_hover_popovers.into_iter().with_position() {
let size = popover.size;
@@ -4289,6 +4315,123 @@ impl EditorElement {
current_y = popover_origin.y + size.height + HOVER_POPOVER_GAP;
}
}
let intersects_menu = |bounds: Bounds<Pixels>| -> bool {
context_menu_layout
.as_ref()
.map_or(false, |menu| bounds.intersects(&menu.bounds))
};
let can_place_above = {
let mut bounds_above = Vec::new();
let mut current_y = hovered_point.y;
for popover in &measured_hover_popovers {
let size = popover.size;
let popover_origin = point(
hovered_point.x + popover.horizontal_offset,
current_y - size.height,
);
bounds_above.push(Bounds::new(popover_origin, size));
current_y = popover_origin.y - HOVER_POPOVER_GAP;
}
bounds_above
.iter()
.all(|b| b.is_contained_within(hitbox) && !intersects_menu(*b))
};
let can_place_below = || {
let mut bounds_below = Vec::new();
let mut current_y = hovered_point.y + line_height;
for popover in &measured_hover_popovers {
let size = popover.size;
let popover_origin = point(hovered_point.x + popover.horizontal_offset, current_y);
bounds_below.push(Bounds::new(popover_origin, size));
current_y = popover_origin.y + size.height + HOVER_POPOVER_GAP;
}
bounds_below
.iter()
.all(|b| b.is_contained_within(hitbox) && !intersects_menu(*b))
};
if can_place_above {
// try placing above hovered point
place_popovers_above(hovered_point, measured_hover_popovers, window, cx);
} else if can_place_below() {
// try placing below hovered point
place_popovers_below(
hovered_point,
measured_hover_popovers,
line_height,
window,
cx,
);
} else {
// try to place popovers around the context menu
let origin_surrounding_menu = context_menu_layout.as_ref().and_then(|menu| {
let total_width = measured_hover_popovers
.iter()
.map(|p| p.size.width)
.max()
.unwrap_or(Pixels::ZERO);
let y_for_horizontal_positioning = if menu.y_flipped {
menu.bounds.bottom() - overall_height
} else {
menu.bounds.top()
};
let possible_origins = vec![
// left of context menu
point(
menu.bounds.left() - total_width - HOVER_POPOVER_GAP,
y_for_horizontal_positioning,
),
// right of context menu
point(
menu.bounds.right() + HOVER_POPOVER_GAP,
y_for_horizontal_positioning,
),
// top of context menu
point(
menu.bounds.left(),
menu.bounds.top() - overall_height - HOVER_POPOVER_GAP,
),
// bottom of context menu
point(menu.bounds.left(), menu.bounds.bottom() + HOVER_POPOVER_GAP),
];
possible_origins.into_iter().find(|&origin| {
Bounds::new(origin, size(total_width, overall_height))
.is_contained_within(hitbox)
})
});
if let Some(origin) = origin_surrounding_menu {
let mut current_y = origin.y;
for (position, popover) in measured_hover_popovers.into_iter().with_position() {
let size = popover.size;
let popover_origin = point(origin.x, current_y);
window.defer_draw(popover.element, popover_origin, 2);
if position != itertools::Position::Last {
let origin = point(popover_origin.x, popover_origin.y + size.height);
draw_occluder(size.width, origin, window, cx);
}
current_y = popover_origin.y + size.height + HOVER_POPOVER_GAP;
}
} else {
// fallback to existing above/below cursor logic
// this might overlap menu or overflow in rare case
if can_place_above {
place_popovers_above(hovered_point, measured_hover_popovers, window, cx);
} else {
place_popovers_below(
hovered_point,
measured_hover_popovers,
line_height,
window,
cx,
);
}
}
}
}
fn layout_diff_hunk_controls(
@@ -4395,7 +4538,6 @@ impl EditorElement {
fn layout_signature_help(
&self,
hitbox: &Hitbox,
text_hitbox: &Hitbox,
content_origin: gpui::Point<Pixels>,
scroll_pixel_position: gpui::Point<Pixels>,
newest_selection_head: Option<DisplayPoint>,
@@ -4403,6 +4545,7 @@ impl EditorElement {
line_layouts: &[LineWithInvisibles],
line_height: Pixels,
em_width: Pixels,
context_menu_layout: Option<ContextMenuLayout>,
window: &mut Window,
cx: &mut App,
) {
@@ -4448,22 +4591,82 @@ impl EditorElement {
let target_point = content_origin + point(target_x, target_y);
let actual_size = element.layout_as_root(Size::<AvailableSpace>::default(), window, cx);
let overall_height = actual_size.height + HOVER_POPOVER_GAP;
let popover_origin = if target_point.y > overall_height {
point(target_point.x, target_point.y - actual_size.height)
} else {
point(
target_point.x,
target_point.y + line_height + HOVER_POPOVER_GAP,
let (popover_bounds_above, popover_bounds_below) = {
let horizontal_offset = (hitbox.top_right().x
- POPOVER_RIGHT_OFFSET
- (target_point.x + actual_size.width))
.min(Pixels::ZERO);
let initial_x = target_point.x + horizontal_offset;
(
Bounds::new(
point(initial_x, target_point.y - actual_size.height),
actual_size,
),
Bounds::new(
point(initial_x, target_point.y + line_height + HOVER_POPOVER_GAP),
actual_size,
),
)
};
let horizontal_offset = (text_hitbox.top_right().x
- POPOVER_RIGHT_OFFSET
- (popover_origin.x + actual_size.width))
.min(Pixels::ZERO);
let final_origin = point(popover_origin.x + horizontal_offset, popover_origin.y);
let intersects_menu = |bounds: Bounds<Pixels>| -> bool {
context_menu_layout
.as_ref()
.map_or(false, |menu| bounds.intersects(&menu.bounds))
};
let final_origin = if popover_bounds_above.is_contained_within(hitbox)
&& !intersects_menu(popover_bounds_above)
{
// try placing above cursor
popover_bounds_above.origin
} else if popover_bounds_below.is_contained_within(hitbox)
&& !intersects_menu(popover_bounds_below)
{
// try placing below cursor
popover_bounds_below.origin
} else {
// try surrounding context menu if exists
let origin_surrounding_menu = context_menu_layout.as_ref().and_then(|menu| {
let y_for_horizontal_positioning = if menu.y_flipped {
menu.bounds.bottom() - actual_size.height
} else {
menu.bounds.top()
};
let possible_origins = vec![
// left of context menu
point(
menu.bounds.left() - actual_size.width - HOVER_POPOVER_GAP,
y_for_horizontal_positioning,
),
// right of context menu
point(
menu.bounds.right() + HOVER_POPOVER_GAP,
y_for_horizontal_positioning,
),
// top of context menu
point(
menu.bounds.left(),
menu.bounds.top() - actual_size.height - HOVER_POPOVER_GAP,
),
// bottom of context menu
point(menu.bounds.left(), menu.bounds.bottom() + HOVER_POPOVER_GAP),
];
possible_origins
.into_iter()
.find(|&origin| Bounds::new(origin, actual_size).is_contained_within(hitbox))
});
origin_surrounding_menu.unwrap_or_else(|| {
// fallback to existing above/below cursor logic
// this might overlap menu or overflow in rare case
if popover_bounds_above.is_contained_within(hitbox) {
popover_bounds_above.origin
} else {
popover_bounds_below.origin
}
})
};
window.defer_draw(element, final_origin, 2);
}
@@ -7884,27 +8087,31 @@ impl Element for EditorElement {
let gutter_settings = EditorSettings::get_global(cx).gutter;
if let Some(newest_selection_head) = newest_selection_head {
let newest_selection_point =
newest_selection_head.to_point(&snapshot.display_snapshot);
if (start_row..end_row).contains(&newest_selection_head.row()) {
self.layout_cursor_popovers(
line_height,
&text_hitbox,
content_origin,
right_margin,
start_row,
scroll_pixel_position,
&line_layouts,
newest_selection_head,
newest_selection_point,
&style,
window,
cx,
);
}
}
let context_menu_layout =
if let Some(newest_selection_head) = newest_selection_head {
let newest_selection_point =
newest_selection_head.to_point(&snapshot.display_snapshot);
if (start_row..end_row).contains(&newest_selection_head.row()) {
self.layout_cursor_popovers(
line_height,
&text_hitbox,
content_origin,
right_margin,
start_row,
scroll_pixel_position,
&line_layouts,
newest_selection_head,
newest_selection_point,
&style,
window,
cx,
)
} else {
None
}
} else {
None
};
self.layout_gutter_menu(
line_height,
@@ -7958,7 +8165,6 @@ impl Element for EditorElement {
self.layout_signature_help(
&hitbox,
&text_hitbox,
content_origin,
scroll_pixel_position,
newest_selection_head,
@@ -7966,6 +8172,7 @@ impl Element for EditorElement {
&line_layouts,
line_height,
em_width,
context_menu_layout,
window,
cx,
);
@@ -7974,13 +8181,13 @@ impl Element for EditorElement {
self.layout_hover_popovers(
&snapshot,
&hitbox,
&text_hitbox,
start_row..end_row,
content_origin,
scroll_pixel_position,
&line_layouts,
line_height,
em_width,
context_menu_layout,
window,
cx,
);
@@ -8212,6 +8419,12 @@ pub(super) fn gutter_bounds(
}
}
#[derive(Clone, Copy)]
struct ContextMenuLayout {
y_flipped: bool,
bounds: Bounds<Pixels>,
}
/// Holds information required for layouting the editor scrollbars.
struct ScrollbarLayoutInformation {
/// The bounds of the editor area (excluding the content offset).

View File

@@ -302,8 +302,8 @@ fn assign_editor_completion_provider(
}
#[derive(Default, Clone)]
struct FakeInlineCompletionProvider {
completion: Option<inline_completion::InlineCompletion>,
pub struct FakeInlineCompletionProvider {
pub completion: Option<inline_completion::InlineCompletion>,
}
impl FakeInlineCompletionProvider {

View File

@@ -352,28 +352,32 @@ impl SelectionsCollection {
) -> Option<Selection<Point>> {
let is_empty = positions.start == positions.end;
let line_len = display_map.line_len(row);
let line = display_map.layout_row(row, text_layout_details);
let start_col = line.closest_index_for_x(positions.start) as u32;
if start_col < line_len || (is_empty && positions.start == line.width) {
let (start, end) = if is_empty {
let point = DisplayPoint::new(row, std::cmp::min(start_col, line_len));
(point, point)
} else {
if start_col >= line_len {
return None;
}
let start = DisplayPoint::new(row, start_col);
let end_col = line.closest_index_for_x(positions.end) as u32;
let end = DisplayPoint::new(row, end_col);
(start, end)
};
Some(Selection {
id: post_inc(&mut self.next_selection_id),
start: start.to_point(display_map),
end: end.to_point(display_map),
reversed,
goal: SelectionGoal::HorizontalRange {
start: positions.start.into(),
end: positions.end.into(),
},
})
} else {
None
}
Some(Selection {
id: post_inc(&mut self.next_selection_id),
start: start.to_point(display_map),
end: end.to_point(display_map),
reversed,
goal: SelectionGoal::HorizontalRange {
start: positions.start.into(),
end: positions.end.into(),
},
})
}
pub fn change_with<R>(

View File

@@ -61,6 +61,7 @@ settings.workspace = true
shellexpand.workspace = true
smol.workspace = true
telemetry.workspace = true
terminal_view.workspace = true
toml.workspace = true
unindent.workspace = true
util.workspace = true

View File

@@ -424,6 +424,7 @@ pub fn init(cx: &mut App) -> Arc<AgentAppState> {
language_models::init(user_store.clone(), client.clone(), fs.clone(), cx);
languages::init(languages.clone(), node_runtime.clone(), cx);
prompt_store::init(cx);
terminal_view::init(cx);
let stdout_is_a_pty = false;
let prompt_builder = PromptBuilder::load(fs.clone(), stdout_is_a_pty, cx);
agent::init(
@@ -431,6 +432,7 @@ pub fn init(cx: &mut App) -> Arc<AgentAppState> {
client.clone(),
prompt_builder.clone(),
languages.clone(),
true,
cx,
);
assistant_tools::init(client.http_client(), cx);

View File

@@ -231,6 +231,10 @@ impl ExampleContext {
Ok(StopReason::MaxTokens) => {
tx.try_send(Err(anyhow!("Exceeded maximum tokens"))).ok();
}
Ok(StopReason::Refusal) => {
tx.try_send(Err(anyhow!("Model refused to generate content")))
.ok();
}
Err(err) => {
tx.try_send(Err(anyhow!(err.clone()))).ok();
}

View File

@@ -143,6 +143,8 @@ pub trait Extension: Send + Sync + 'static {
user_installed_path: Option<PathBuf>,
worktree: Arc<dyn WorktreeDelegate>,
) -> Result<DebugAdapterBinary>;
async fn get_dap_schema(&self) -> Result<serde_json::Value>;
}
pub fn parse_wasm_extension_version(

View File

@@ -20,7 +20,7 @@ pub use wit::{
make_file_executable,
zed::extension::context_server::ContextServerConfiguration,
zed::extension::dap::{
DebugAdapterBinary, DebugRequest, DebugTaskDefinition, StartDebuggingRequestArguments,
DebugAdapterBinary, DebugTaskDefinition, StartDebuggingRequestArguments,
StartDebuggingRequestArgumentsRequest, TcpArguments, TcpArgumentsTemplate,
resolve_tcp_template,
},
@@ -203,6 +203,10 @@ pub trait Extension: Send + Sync {
) -> Result<DebugAdapterBinary, String> {
Err("`get_dap_binary` not implemented".to_string())
}
fn dap_schema(&mut self) -> Result<serde_json::Value, String> {
Err("`dap_schema` not implemented".to_string())
}
}
/// Registers the provided type as a Zed extension.
@@ -396,6 +400,10 @@ impl wit::Guest for Component {
) -> Result<wit::DebugAdapterBinary, String> {
extension().get_dap_binary(adapter_name, config, user_installed_path, worktree)
}
fn dap_schema() -> Result<String, String> {
extension().dap_schema().map(|schema| schema.to_string())
}
}
/// The ID of a language server.

View File

@@ -35,9 +35,7 @@ interface dap {
record debug-task-definition {
label: string,
adapter: string,
request: debug-request,
initialize-args: option<string>,
stop-on-entry: option<bool>,
config: string,
tcp-connection: option<tcp-arguments-template>,
}

View File

@@ -134,6 +134,7 @@ world extension {
export labels-for-completions: func(language-server-id: string, completions: list<completion>) -> result<list<option<code-label>>, string>;
export labels-for-symbols: func(language-server-id: string, symbols: list<symbol>) -> result<list<option<code-label>>, string>;
/// Returns the completions that should be shown when completing the provided slash command with the given query.
export complete-slash-command-argument: func(command: slash-command, args: list<string>) -> result<list<slash-command-argument-completion>, string>;
@@ -158,4 +159,6 @@ world extension {
/// Returns a configured debug adapter binary for a given debug task.
export get-dap-binary: func(adapter-name: string, config: debug-task-definition, user-installed-path: option<string>, worktree: borrow<worktree>) -> result<debug-adapter-binary, string>;
/// Get a debug adapter's configuration schema
export dap-schema: func() -> result<string, string>;
}

View File

@@ -31,6 +31,7 @@ http_client.workspace = true
language.workspace = true
log.workspace = true
lsp.workspace = true
moka.workspace = true
node_runtime.workspace = true
paths.workspace = true
project.workspace = true

View File

@@ -22,15 +22,18 @@ use gpui::{App, AsyncApp, BackgroundExecutor, Task};
use http_client::HttpClient;
use language::LanguageName;
use lsp::LanguageServerName;
use moka::sync::Cache;
use node_runtime::NodeRuntime;
use release_channel::ReleaseChannel;
use semantic_version::SemanticVersion;
use std::borrow::Cow;
use std::sync::LazyLock;
use std::{
path::{Path, PathBuf},
sync::{Arc, OnceLock},
sync::Arc,
};
use wasmtime::{
Engine, Store,
CacheStore, Engine, Store,
component::{Component, ResourceTable},
};
use wasmtime_wasi::{self as wasi, WasiView};
@@ -395,6 +398,20 @@ impl extension::Extension for WasmExtension {
})
.await
}
async fn get_dap_schema(&self) -> Result<serde_json::Value> {
self.call(|extension, store| {
async move {
extension
.call_dap_schema(store)
.await
.and_then(|schema| serde_json::to_value(schema).map_err(|err| err.to_string()))
.map_err(|err| anyhow!(err.to_string()))
}
.boxed()
})
.await
}
}
pub struct WasmState {
@@ -411,16 +428,23 @@ type ExtensionCall = Box<
>;
fn wasm_engine() -> wasmtime::Engine {
static WASM_ENGINE: OnceLock<wasmtime::Engine> = OnceLock::new();
static WASM_ENGINE: LazyLock<wasmtime::Engine> = LazyLock::new(|| {
let mut config = wasmtime::Config::new();
config.wasm_component_model(true);
config.async_support(true);
config
.enable_incremental_compilation(cache_store())
.unwrap();
wasmtime::Engine::new(&config).unwrap()
});
WASM_ENGINE
.get_or_init(|| {
let mut config = wasmtime::Config::new();
config.wasm_component_model(true);
config.async_support(true);
wasmtime::Engine::new(&config).unwrap()
})
.clone()
WASM_ENGINE.clone()
}
fn cache_store() -> Arc<IncrementalCompilationCache> {
static CACHE_STORE: LazyLock<Arc<IncrementalCompilationCache>> =
LazyLock::new(|| Arc::new(IncrementalCompilationCache::new()));
CACHE_STORE.clone()
}
impl WasmHost {
@@ -667,3 +691,36 @@ impl wasi::WasiView for WasmState {
&mut self.ctx
}
}
/// Wrapper around a mini-moka bounded cache for storing incremental compilation artifacts.
/// Since wasm modules have many similar elements, this can save us a lot of work at the
/// cost of a small memory footprint. However, we don't want this to be unbounded, so we use
/// a LFU/LRU cache to evict less used cache entries.
#[derive(Debug)]
struct IncrementalCompilationCache {
cache: Cache<Vec<u8>, Vec<u8>>,
}
impl IncrementalCompilationCache {
fn new() -> Self {
let cache = Cache::builder()
// Cap this at 32 MB for now. Our extensions turn into roughly 512kb in the cache,
// which means we could store 64 completely novel extensions in the cache, but in
// practice we will more than that, which is more than enough for our use case.
.max_capacity(32 * 1024 * 1024)
.weigher(|k: &Vec<u8>, v: &Vec<u8>| (k.len() + v.len()).try_into().unwrap_or(u32::MAX))
.build();
Self { cache }
}
}
impl CacheStore for IncrementalCompilationCache {
fn get(&self, key: &[u8]) -> Option<Cow<[u8]>> {
self.cache.get(key).map(|v| v.into())
}
fn insert(&self, key: &[u8], value: Vec<u8>) -> bool {
self.cache.insert(key.to_vec(), value);
true
}
}

View File

@@ -922,6 +922,20 @@ impl Extension {
_ => anyhow::bail!("`get_dap_binary` not available prior to v0.6.0"),
}
}
pub async fn call_dap_schema(&self, store: &mut Store<WasmState>) -> Result<String, String> {
match self {
Extension::V0_6_0(ext) => {
let schema = ext
.call_dap_schema(store)
.await
.map_err(|err| err.to_string())?;
schema
}
_ => Err("`get_dap_binary` not available prior to v0.6.0".to_string()),
}
}
}
trait ToWasmtimeResult<T> {

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