Compare commits

...

66 Commits

Author SHA1 Message Date
Max Brunsfeld
f3896a2d51 Bump Tree-sitter to 0.25.5 for YAML-editing crash fix (#31603)
Closes https://github.com/zed-industries/zed/issues/31380

See https://github.com/tree-sitter/tree-sitter/pull/4472 for the fix

Release Notes:

- Fixed a crash that could occur when editing YAML files.
2025-05-29 15:48:51 -07:00
Zed Bot
ce7015db19 Bump to 0.188.4 for @maxbrunsfeld 2025-05-29 22:47:23 +00:00
Max Brunsfeld
1b8ed570c2 Fix editor rendering slowness with large folds (#31569)
Closes https://github.com/zed-industries/zed/issues/31565

* Looking up settings on every row was very slow in the case of large
folds, especially if there was an `.editorconfig` file with numerous
glob patterns
* Checking whether each indent guide was within a fold was very slow,
when a fold spanned many indent guides.

Release Notes:

- Fixed slowness that could happen when editing in the presence of large
folds.
2025-05-29 15:38:00 -07:00
Marshall Bowers
22342eff0e Pass up intent with completion requests (#31710)
This PR adds a new `intent` field to completion requests to assist in
categorizing them correctly.

Release Notes:

- N/A

---------

Co-authored-by: Ben Brandt <benjamin.j.brandt@gmail.com>
2025-05-29 18:36:04 -04:00
Joseph T. Lyons
774878816a v0.188.x stable 2025-05-28 10:16:29 -04:00
Antonio Scandurra
6582192741 Fix lag when interacting with MarkdownElement (#31585)
Previously, we forgot to associate the `Markdown` entity to
`MarkdownElement` during `prepaint`. This caused calls to
`Context<Markdown>::notify` to not invalidate the view cache, which
meant we would have to wait for some other invalidation before seeing
the results of that initial notify.

Release Notes:

- Improved responsiveness of mouse interactions with the agent panel.

Co-authored-by: Ben Brandt <benjamin.j.brandt@gmail.com>
2025-05-28 10:10:05 -04:00
Smit Barmase
e30ee62e45 editor: Inline Code Actions Indicator (#31432)
Follow up to https://github.com/zed-industries/zed/pull/30140 and
https://github.com/zed-industries/zed/pull/31236

This PR introduces an inline code action indicator that shows up at the
start of a buffer line when there's enough space. If space is tight, it
adjusts to lines above or below instead. It also adjusts when cursor is
near indicator.

The indicator won't appear if there's no space within about 8 rows in
either direction, and it also stays hidden for folded ranges. It also
won't show up in case there is not space in multi buffer excerpt. These
cases account for very little because practically all languages do have
indents.


https://github.com/user-attachments/assets/1363ee8a-3178-4665-89a7-c86c733f2885

This PR also sets the existing `toolbar.code_actions` setting to `false`
in favor of this.

Release Notes:

- Added code action indicator which shows up inline at the start of the
row. This can be disabled by setting `inline_code_actions` to `false`.
2025-05-28 18:44:40 +05:30
Anthony Eid
c84a4705ab debugger beta: Autoscroll to recently saved debug scenario when saving a scenario (#31528)
I added a test to this too as one of my first steps of improving
`NewSessionModal`'s test coverage.


Release Notes:

- debugger beta: Select saved debug config when opening debug.json from
`NewSessionModal`
2025-05-27 15:50:22 -04:00
Anthony Eid
f945cbb4dc debugger beta: Fix gdb/delve JSON data conversion from New Session Modal (#31501)
test that check's that each conversion works properly based on the
adapter's config validation function. 

Co-authored-by: Zed AI \<ai@zed.dev\>

Release Notes:

- debugger beta: Fix bug where Go/GDB configuration's wouldn't work from
NewSessionModal
2025-05-27 15:50:13 -04:00
Cole Miller
591516215c debugger: Don't try to open <node_internals> paths (#31524)
The JS DAP returns these, and they don't point to anything real on the
filesystem.

Release Notes:

- N/A
2025-05-27 15:50:02 -04:00
Cole Miller
f4b2d69f0d debugger: Improve keyboard navigability of variable list (#31462)
This PR adds actions for copying variable names and values and editing
variable values from the variable list. Previously these were only
accessible using the mouse. It also fills in keybindings for expanding
and collapsing entries on Linux that we already had on macOS.

Release Notes:

- Debugger Beta: Added the `variable_list::EditVariable`,
`variable_list::CopyVariableName`, and
`variable_list::CopyVariableValue` actions and default keybindings.
2025-05-27 15:49:51 -04:00
Joseph T. Lyons
0b923757d6 zed 0.188.3 2025-05-27 10:02:17 -04:00
Kirill Bulatov
81ed86b92d Keep file permissions when extracting zip archives on Unix (#31304)
Follow-up of https://github.com/zed-industries/zed/pull/31080

Stop doing

```rs
#[cfg(not(windows))]
{
    file.set_permissions(<fs::Permissions as fs::unix::PermissionsExt>::from_mode(
        0o755,
    ))
    .await?;
}
```

after extracting zip archives on Unix, and use an API that provides the
file permissions data for each archive entry.

Release Notes:

- N/A
2025-05-27 09:47:45 -04:00
Anthony Eid
a9e1515111 debugger beta: Update Javascript's DAP to allow passing in url instead of program (#31494)
Closes #31375

Release Notes:

- debugger beta: Allow passing in URL instead of program for Javascript
launch request
2025-05-27 09:30:51 -04:00
Raphael Lüthy
7fbd1ca90e debugger beta: Fix install detection for Debugpy in venv (#31339)
Based on my report on discord when chatting with Anthony and Remco:
https://discord.com/channels/869392257814519848/1375129714645012530

Root Cause: Zed was incorrectly trying to execute a directory path
instead of properly invoking the debugpy module when debugpy was
installed via package managers (pip, conda, etc.) rather than downloaded
from GitHub releases.

Solution:

- Automatic Detection: Zed now automatically detects whether debugpy is
installed via pip/conda or downloaded from GitHub
- Correct Invocation: For pip-installed debugpy, Zed now uses python -m
debugpy.adapter instead of trying to execute file paths
- Added a `installed_in_venv` flag to differentiate the setup properly
- Backward Compatibility: GitHub-downloaded debugpy releases continue to
work as before
- Enhanced Logging: Added logging to show which debugpy installation
method is being used (I had to verify it somehow)

I verified with the following setups (can be confirmed with the debug
logs):
- `conda` with installed debugpy, went to installed instance
- `uv` with installed debugpy, went to installed instance
- `uv` without installed debugpy, went to github releases
- Homebrew global python install, went to github releases

Release Notes:

- Fix issue where debugpy from different environments won't load as
intended
2025-05-27 09:30:22 -04:00
Cole Miller
78fe611abb debugger: Add an action to rerun the last session (#31442)
This works the same as selecting the first history match in the new
session modal.

Release Notes:

- Debugger Beta: Added the `debugger: rerun last session` action, bound
by default to `alt-f4`.
2025-05-27 09:29:44 -04:00
Cole Miller
1324691cf3 debugger: Add missing StepOut handler (#31463)
Closes #31317

Release Notes:

- Debugger Beta: Fixed a bug that prevented keybindings for the
`StepOut` action from working.
2025-05-27 09:29:44 -04:00
Cole Miller
df7284b3eb debugger: Fix wrong port used for SSH debugging (#31474)
We were trying to connect on the user's machine to the port number used
by the debugger on the remote machine, instead of the randomly-assigned
local available port.

Release Notes:

- Debugger Beta: Fixed a bug that caused connecting to a debug adapter
over SSH to hang.
2025-05-27 09:29:24 -04:00
Finn Evers
5241d25a00 language: Improve auto-indentation when using round brackets in Python (#31260)
Follow-up to #29625 and #30902

This PR reintroduces auto-intents for brackets in Python and fixes some
cases where an indentation would be triggered if it should not. For
example, upon typing

```python
a = []
```
and inserting a newline after, the next line would be indented although
it shoud not be.

Bracket auto-indentation was tested prior to #29625 but removed there
and the test updated accordingly. #30902 reintroduced this for all
brackets but `()`. I reintroduced this here, reverted the changes to the
test so that indents also happen after typing `()`. This is frequently
used for tuples and multiline statements in Python.

Release Notes:

- Improved auto-indentation when using round brackets in Python.
2025-05-27 05:48:21 +05:30
Cole Miller
d746da9fce debugger: Add keyboard navigation for breakpoint list (#31221)
Release Notes:

- Debugger Beta: made it possible to navigate the breakpoint list using
menu keybindings.
2025-05-26 15:46:32 -04:00
gcp-cherry-pick-bot[bot]
ee6a93f718 Allow LSP adapters to decide, which diagnostics to underline (cherry-pick #31450) (#31452)
Cherry-picked Allow LSP adapters to decide, which diagnostics to
underline (#31450)

Closes
https://github.com/zed-industries/zed/pull/31355#issuecomment-2910439798

<img width="1728" alt="image"

src="https://github.com/user-attachments/assets/2eaa8e9b-00bc-4e99-ac09-fceb2d932e41"
/>


Release Notes:

- N/A

Co-authored-by: Kirill Bulatov <kirill@zed.dev>
2025-05-26 22:34:43 +03:00
Cole Miller
79a0f73784 debugger: Fix misleading error logs (#31293)
Release Notes:

- N/A
2025-05-26 14:21:38 -04:00
gcp-cherry-pick-bot[bot]
f4767e33d2 Remove the ability to book onboarding (cherry-pick #31404) (#31426)
Cherry-picked Remove the ability to book onboarding (#31404)

Closes: https://github.com/zed-industries/zed/issues/31394

Onboarding has been valuable, but we're moving into a new phase as our
user base grows, and our ability to chat with everyone who books a call
will not scale linearly. For now, we are removing the option to book a
call from the application.

Release Notes:

- N/A

Co-authored-by: Joseph T. Lyons <JosephTLyons@gmail.com>
2025-05-26 15:28:35 +03:00
Anthony Eid
6259ba0ad2 debugger beta: Fix regression where we sent launch args twice to any dap (#31325)
This regression happens because our tests weren't properly catching this
edge case anymore. I updated the tests to only send the raw config to
the Fake Adapter Client.

Release Notes:

- debugger beta: Fix bug where launch args were sent twice
2025-05-25 17:55:27 -04:00
Cole Miller
033e1040a4 debugger: Fix adapter names in initial-debug-tasks.json (#31283)
Closes #31134

Release Notes:

- N/A

---------

Co-authored-by: Piotr <piotr@zed.dev>
2025-05-25 17:55:17 -04:00
Anthony Eid
34022b7ee5 debugger beta: Auto download Delve (Go's DAP) & fix grammar errors in docs (#31273)
Release Notes:

- debugger beta: Go's debug adapter will now automatically download if
not found on user's PATH

Co-authored-by: Remco Smits <djsmits12@gmail.com>
2025-05-25 17:55:07 -04:00
gcp-cherry-pick-bot[bot]
682d265d0e Do not underline unnecessary diagnostics (cherry-pick #31355) (#31357)
Cherry-picked Do not underline unnecessary diagnostics (#31355)

Closes
https://github.com/zed-industries/zed/pull/31229#issuecomment-2906946881

Follow

https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#diagnosticTag

> Clients are allowed to render diagnostics with this tag faded out
instead of having an error squiggle.

and do not underline any unnecessary diagnostic at all.

Release Notes:

- Fixed clangd's inactive regions diagnostics excessive highlights

Co-authored-by: Kirill Bulatov <kirill@zed.dev>
2025-05-24 23:25:50 +03:00
smit
72a0f9d06c Add Code Actions to the Toolbar (#31236)
Closes issue #31120.


https://github.com/user-attachments/assets/a4b3c86d-7358-49ac-b8d9-e9af50daf671

Release Notes:

- Added a code actions icon to the toolbar. This icon can be disabled by
setting `toolbar.code_actions` to `false`.
2025-05-23 10:17:08 -04:00
Anthony Eid
0274625030 debugger beta: Add error handling when gdb doesn't send thread names (#31279)
If gdb doesn't send a thread name we display the thread's process id in
the thread drop down menu instead now.

Co-authored-by: Remco Smits \<djsmits12@gmail.com\>

Release Notes:

- debugger beta: Handle bug where DAPs don't send thread names
2025-05-23 10:08:15 -04:00
Antonio Scandurra
8eed13cac6 Ensure client reconnects after erroring during the handshake (#31278)
Release Notes:

- Fixed a bug that prevented Zed from reconnecting after erroring during
the initial handshake with the server.
2025-05-23 09:50:20 -04:00
smit
12c1ce511a editor: Fix issue where newline on * as prefix adds comment delimiter (#31271)
Release Notes:

- Fixed issue where pressing Enter on a line starting with * incorrectly
added comment delimiter.

---------

Co-authored-by: Piotr Osiewicz <peterosiewicz@gmail.com>
Co-authored-by: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com>
2025-05-23 09:20:06 -04:00
Piotr Osiewicz
918f44a957 debugger: Change placeholder text for Custom/Run text input (#31264)
Before: 

![image](https://github.com/user-attachments/assets/6cdef5bb-c901-4954-a2ec-39c59f8314db)

After:

![image](https://github.com/user-attachments/assets/c4f60a23-249c-47ab-8a9e-a39e2277dd00)


Release Notes:

- N/A
2025-05-23 09:20:00 -04:00
Joseph T. Lyons
9a833d65ad zed 0.188.2 2025-05-23 08:32:21 -04:00
Cole Miller
774c34820d debugger: More focus tweaks (#31232)
- Make remembering focus work with `ActivatePaneDown` as well
- Tone down the console's focus-in behavior so clicking doesn't
misbehave

Release Notes:

- N/A
2025-05-23 08:04:24 -04:00
Remco Smits
1e6412c8c8 debugger: Detect debugpy from virtual env (#31211)
Release Notes:

- Debugger Beta: Detect debugpy from virtual env
2025-05-23 08:04:24 -04:00
gcp-cherry-pick-bot[bot]
26662728ed Change default diagnostics_max_severity to 'hint' (cherry-pick #31229) (#31245)
Cherry-picked 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>

Co-authored-by: Bo Lopker <blopker@users.noreply.github.com>
Co-authored-by: Kirill Bulatov <kirill@zed.dev>
2025-05-23 11:29:05 +03:00
Shardul Vaidya
8651123519 bedrock: Support Claude 4 models (#31214)
Release Notes:

- AWS Bedrock: Added support for Claude 4.
2025-05-22 19:14:41 -04:00
Marshall Bowers
9e3b485660 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 17:46:12 -04:00
Marshall Bowers
5c68f10780 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 17:46:04 -04:00
Oleksiy Syvokon
803e8cebe6 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 17:45:51 -04:00
Umesh Yadav
ed31f8051c anthropic: Fix Claude 4 model display names to match official order (#31218)
Release Notes:

- N/A
2025-05-22 17:26:38 -04:00
Cole Miller
7c9e1a3669 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:15:20 -04:00
Marshall Bowers
2eebf7d437 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 15:14:56 -04:00
Marshall Bowers
2b58f16ef2 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 15:02:54 -04:00
Umesh Yadav
07e412586e 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:27:01 -04:00
Marshall Bowers
4f539773fb 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 14:11:32 -04:00
Anthony Eid
38f8e5f66b 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 14:11:11 -04:00
smit
c5a27c81bc 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:32:33 +05:30
Piotr Osiewicz
9a4e44b41f 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 11:43:55 -04:00
Piotr Osiewicz
5f82e2c9a3 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 10:38:58 -04:00
Anthony Eid
b126f2e868 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:38:54 -04:00
smit
866eb9a84c 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 18:23:37 +05:30
Anthony Eid
025993868c debugger beta: Fix panic that could occur when parsing an invalid dap schema (#31175)
Release Notes:

- N/A
2025-05-22 07:27:13 -04:00
Anthony Eid
4837d47c33 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:27:02 -04:00
Julia Ryan
10b8bbc478 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 07:24:02 -04:00
Piotr Osiewicz
b432a144c7 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 07:23:47 -04:00
Remco Smits
6f1fe2d2c0 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 07:01:54 -04:00
Anthony Eid
bf72cbecf5 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:51:42 -04:00
Kirill Bulatov
0bd0f8619b Fix unzipping clangd and codelldb on Windows (#31080)
Closes https://github.com/zed-industries/zed/pull/30454

Release Notes:

- N/A
2025-05-22 05:51:37 -04:00
Cole Miller
c5ac8353b3 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 05:02:16 -04:00
Cole Miller
bd1fe6e600 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 05:02:01 -04:00
Cole Miller
a6757d4993 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-22 05:01:42 -04:00
Joseph T. Lyons
26930acdad zed 0.188.1 2025-05-21 16:10:47 -04:00
Max Brunsfeld
561710ea0e 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 16:08:08 -04:00
Umesh Yadav
a6c0307a06 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:52:55 -04:00
Joseph T. Lyons
219c7d5f83 v0.188.x preview 2025-05-21 11:14:17 -04:00
160 changed files with 6180 additions and 2656 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

64
Cargo.lock generated
View File

@@ -531,6 +531,7 @@ dependencies = [
"workspace",
"workspace-hack",
"zed_actions",
"zed_llm_client",
]
[[package]]
@@ -3002,6 +3003,7 @@ dependencies = [
"context_server",
"ctor",
"dap",
"dap_adapters",
"dashmap 6.1.0",
"debugger_ui",
"derive_more",
@@ -4029,6 +4031,7 @@ dependencies = [
"smallvec",
"smol",
"task",
"telemetry",
"util",
"workspace-hack",
]
@@ -4036,7 +4039,7 @@ dependencies = [
[[package]]
name = "dap-types"
version = "0.0.1"
source = "git+https://github.com/zed-industries/dap-types?rev=be69a016ba710191b9fdded28c8b042af4b617f7#be69a016ba710191b9fdded28c8b042af4b617f7"
source = "git+https://github.com/zed-industries/dap-types?rev=68516de327fa1be15214133a0a2e52a12982ce75#68516de327fa1be15214133a0a2e52a12982ce75"
dependencies = [
"schemars",
"serde",
@@ -4052,7 +4055,9 @@ dependencies = [
"dap",
"futures 0.3.31",
"gpui",
"json_dotpath",
"language",
"log",
"paths",
"serde",
"serde_json",
@@ -4179,6 +4184,8 @@ dependencies = [
"dap",
"extension",
"gpui",
"serde_json",
"task",
"workspace-hack",
]
@@ -4242,6 +4249,7 @@ dependencies = [
"util",
"workspace",
"workspace-hack",
"zed_actions",
]
[[package]]
@@ -4401,6 +4409,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"
@@ -4665,6 +4682,7 @@ dependencies = [
"command_palette_hooks",
"convert_case 0.8.0",
"ctor",
"dap",
"db",
"emojis",
"env_logger 0.11.8",
@@ -5026,6 +5044,7 @@ dependencies = [
"util",
"uuid",
"workspace-hack",
"zed_llm_client",
]
[[package]]
@@ -6129,6 +6148,7 @@ dependencies = [
"workspace",
"workspace-hack",
"zed_actions",
"zed_llm_client",
]
[[package]]
@@ -8535,6 +8555,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"
@@ -8676,6 +8708,7 @@ dependencies = [
"clock",
"collections",
"ctor",
"diffy",
"ec4rs",
"env_logger 0.11.8",
"fs",
@@ -8880,6 +8913,7 @@ dependencies = [
"async-tar",
"async-trait",
"collections",
"dap",
"futures 0.3.31",
"gpui",
"http_client",
@@ -10076,7 +10110,6 @@ dependencies = [
"async-tar",
"async-trait",
"async-watch",
"async_zip",
"futures 0.3.31",
"http_client",
"log",
@@ -10085,9 +10118,7 @@ dependencies = [
"serde",
"serde_json",
"smol",
"tempfile",
"util",
"walkdir",
"which 6.0.3",
"workspace-hack",
]
@@ -10192,6 +10223,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"
@@ -16390,7 +16430,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",
@@ -16417,9 +16457,9 @@ dependencies = [
[[package]]
name = "tree-sitter"
version = "0.25.3"
version = "0.25.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9ac5ea5e7f2f1700842ec071401010b9c59bf735295f6e9fa079c3dc035b167"
checksum = "ac5fff5c47490dfdf473b5228039bfacad9d765d9b6939d26bf7cc064c1c7822"
dependencies = [
"cc",
"regex",
@@ -17029,6 +17069,7 @@ version = "0.1.0"
dependencies = [
"anyhow",
"async-fs",
"async_zip",
"collections",
"dirs 4.0.0",
"dunce",
@@ -17044,12 +17085,14 @@ dependencies = [
"rust-embed",
"serde",
"serde_json",
"serde_json_lenient",
"smol",
"take-until",
"tempfile",
"tendril",
"unicase",
"util_macros",
"walkdir",
"workspace-hack",
]
@@ -19137,6 +19180,7 @@ dependencies = [
"aho-corasick",
"anstream",
"arrayvec",
"async-compression",
"async-std",
"async-tungstenite",
"aws-config",
@@ -19652,7 +19696,7 @@ dependencies = [
[[package]]
name = "zed"
version = "0.188.0"
version = "0.188.4"
dependencies = [
"activity_indicator",
"agent",
@@ -19847,9 +19891,9 @@ dependencies = [
[[package]]
name = "zed_llm_client"
version = "0.8.1"
version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "16d993fc42f9ec43ab76fa46c6eb579a66e116bb08cd2bc9a67f3afcaa05d39d"
checksum = "de7d9523255f4e00ee3d0918e5407bd252d798a4a8e71f6d37f23317a1588203"
dependencies = [
"anyhow",
"serde",

View File

@@ -430,7 +430,7 @@ core-foundation-sys = "0.8.6"
core-video = { version = "0.4.3", features = ["metal"] }
criterion = { version = "0.5", features = ["html_reports"] }
ctor = "0.4.0"
dap-types = { git = "https://github.com/zed-industries/dap-types", rev = "be69a016ba710191b9fdded28c8b042af4b617f7" }
dap-types = { git = "https://github.com/zed-industries/dap-types", rev = "68516de327fa1be15214133a0a2e52a12982ce75" }
dashmap = "6.0"
derive_more = "0.99.17"
dirs = "4.0"
@@ -462,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" }
@@ -568,7 +569,7 @@ tokio = { version = "1" }
tokio-tungstenite = { version = "0.26", features = ["__rustls-tls"] }
toml = "0.8"
tower-http = "0.4.4"
tree-sitter = { version = "0.25.3", features = ["wasm"] }
tree-sitter = { version = "0.25.5", features = ["wasm"] }
tree-sitter-bash = "0.23"
tree-sitter-c = "0.23"
tree-sitter-cpp = "0.23"
@@ -598,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"
@@ -615,7 +616,7 @@ 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.4"
zstd = "0.11"
[workspace.dependencies.async-stripe]

View File

@@ -1,3 +1,3 @@
<svg width="8" height="8" viewBox="0 0 8 8" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M4.76019 3.50003H6.50231C6.71012 3.50003 6.89761 3.62971 6.95698 3.82346C7.04292 4.01876 6.98823 4.23906 6.83199 4.37656L2.83214 7.87643C2.65558 8.02954 2.39731 8.04204 2.20857 7.90455C2.01967 7.76705 1.95092 7.51706 2.04295 7.30301L3.24462 4.49999H1.48844C1.29423 4.49999 1.10767 4.37031 1.0344 4.17657C0.961132 3.98126 1.01643 3.76096 1.17323 3.62346L5.17261 0.123753C5.34917 -0.0299914 5.60697 -0.0417097 5.79603 0.0954726C5.98508 0.232749 6.05383 0.482177 5.96165 0.69695L4.76013 3.49981L4.76019 3.50003Z" fill="white"/>
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M9.3 1.75L3 7.35H5.8L4.7 12.25L11 6.65H8.2L9.3 1.75Z" stroke="black" stroke-width="1.25" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 633 B

After

Width:  |  Height:  |  Size: 227 B

View File

@@ -0,0 +1,3 @@
<svg width="8" height="8" viewBox="0 0 8 8" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M4.76019 3.50003H6.50231C6.71012 3.50003 6.89761 3.62971 6.95698 3.82346C7.04292 4.01876 6.98823 4.23906 6.83199 4.37656L2.83214 7.87643C2.65558 8.02954 2.39731 8.04204 2.20857 7.90455C2.01967 7.76705 1.95092 7.51706 2.04295 7.30301L3.24462 4.49999H1.48844C1.29423 4.49999 1.10767 4.37031 1.0344 4.17657C0.961132 3.98126 1.01643 3.76096 1.17323 3.62346L5.17261 0.123753C5.34917 -0.0299914 5.60697 -0.0417097 5.79603 0.0954726C5.98508 0.232749 6.05383 0.482177 5.96165 0.69695L4.76013 3.49981L4.76019 3.50003Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 633 B

View File

@@ -31,8 +31,10 @@
"ctrl-,": "zed::OpenSettings",
"ctrl-q": "zed::Quit",
"f4": "debugger::Start",
"alt-f4": "debugger::RerunLastSession",
"f5": "debugger::Continue",
"shift-f5": "debugger::Stop",
"ctrl-shift-f5": "debugger::Restart",
"f6": "debugger::Pause",
"f7": "debugger::StepOver",
"cmd-f11": "debugger::StepInto",
@@ -558,6 +560,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 +598,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 +864,30 @@
"alt-l": "git::GenerateCommitMessage"
}
},
{
"context": "DebugPanel",
"bindings": {
"ctrl-t": "debugger::ToggleThreadPicker",
"ctrl-i": "debugger::ToggleSessionPicker"
}
},
{
"context": "VariableList",
"bindings": {
"left": "variable_list::CollapseSelectedEntry",
"right": "variable_list::ExpandSelectedEntry",
"enter": "variable_list::EditVariable",
"ctrl-c": "variable_list::CopyVariableValue",
"ctrl-alt-c": "variable_list::CopyVariableName"
}
},
{
"context": "BreakpointList",
"bindings": {
"space": "debugger::ToggleEnableBreakpoint",
"backspace": "debugger::UnsetBreakpoint"
}
},
{
"context": "CollabPanel && not_editing",
"bindings": {

View File

@@ -15,8 +15,10 @@
"use_key_equivalents": true,
"bindings": {
"f4": "debugger::Start",
"alt-f4": "debugger::RerunLastSession",
"f5": "debugger::Continue",
"shift-f5": "debugger::Stop",
"shift-cmd-f5": "debugger::Restart",
"f6": "debugger::Pause",
"f7": "debugger::StepOver",
"f11": "debugger::StepInto",
@@ -624,6 +626,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",
@@ -844,7 +847,10 @@
"use_key_equivalents": true,
"bindings": {
"left": "variable_list::CollapseSelectedEntry",
"right": "variable_list::ExpandSelectedEntry"
"right": "variable_list::ExpandSelectedEntry",
"enter": "variable_list::EditVariable",
"cmd-c": "variable_list::CopyVariableValue",
"cmd-alt-c": "variable_list::CopyVariableName"
}
},
{
@@ -929,6 +935,20 @@
"alt-tab": "git::GenerateCommitMessage"
}
},
{
"context": "DebugPanel",
"bindings": {
"cmd-t": "debugger::ToggleThreadPicker",
"cmd-i": "debugger::ToggleSessionPicker"
}
},
{
"context": "BreakpointList",
"bindings": {
"space": "debugger::ToggleEnableBreakpoint",
"backspace": "debugger::UnsetBreakpoint"
}
},
{
"context": "CollabPanel && not_editing",
"use_key_equivalents": true,

View File

@@ -213,6 +213,8 @@
// Whether to show the signature help after completion or a bracket pair inserted.
// If `auto_signature_help` is enabled, this setting will be treated as enabled also.
"show_signature_help_after_edits": false,
// Whether to show code action button at start of buffer line.
"inline_code_actions": true,
// What to do when go to definition yields no results.
//
// 1. Do nothing: `none`
@@ -230,11 +232,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
@@ -322,7 +324,9 @@
// Whether to show the Selections menu in the editor toolbar.
"selections_menu": true,
// Whether to show agent review buttons in the editor toolbar.
"agent_review": true
"agent_review": true,
// Whether to show code action buttons in the editor toolbar.
"code_actions": false
},
// Titlebar related settings
"title_bar": {

View File

@@ -1,32 +1,30 @@
[
{
"label": "Debug active PHP file",
"adapter": "php",
"adapter": "PHP",
"program": "$ZED_FILE",
"request": "launch",
"cwd": "$ZED_WORKTREE_ROOT"
},
{
"label": "Debug active Python file",
"adapter": "python",
"adapter": "Debugpy",
"program": "$ZED_FILE",
"request": "launch",
"cwd": "$ZED_WORKTREE_ROOT"
},
{
"label": "Debug active JavaScript file",
"adapter": "javascript",
"adapter": "JavaScript",
"program": "$ZED_FILE",
"request": "launch",
"cwd": "$ZED_WORKTREE_ROOT"
},
{
"label": "JavaScript debug terminal",
"adapter": "javascript",
"adapter": "JavaScript",
"request": "launch",
"cwd": "$ZED_WORKTREE_ROOT",
"initialize_args": {
"console": "integratedTerminal"
}
"console": "integratedTerminal"
}
]

View File

@@ -54,6 +54,7 @@ use util::ResultExt as _;
use util::markdown::MarkdownCodeBlock;
use workspace::Workspace;
use zed_actions::assistant::OpenRulesLibrary;
use zed_llm_client::CompletionIntent;
pub struct ActiveThread {
context_store: Entity<ContextStore>,
@@ -1412,6 +1413,7 @@ impl ActiveThread {
let request = language_model::LanguageModelRequest {
thread_id: None,
prompt_id: None,
intent: None,
mode: None,
messages: vec![request_message],
tools: vec![],
@@ -1573,7 +1575,12 @@ impl ActiveThread {
this.thread.update(cx, |thread, cx| {
thread.advance_prompt_id();
thread.send_to_model(model.model, Some(window.window_handle()), cx);
thread.send_to_model(
model.model,
CompletionIntent::UserPrompt,
Some(window.window_handle()),
cx,
);
});
this._load_edited_message_context_task = None;
cx.notify();

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

@@ -34,6 +34,7 @@ use std::{
};
use streaming_diff::{CharOperation, LineDiff, LineOperation, StreamingDiff};
use telemetry_events::{AssistantEventData, AssistantKind, AssistantPhase};
use zed_llm_client::CompletionIntent;
pub struct BufferCodegen {
alternatives: Vec<Entity<CodegenAlternative>>,
@@ -464,6 +465,7 @@ impl CodegenAlternative {
LanguageModelRequest {
thread_id: None,
prompt_id: None,
intent: Some(CompletionIntent::InlineAssist),
mode: None,
tools: Vec::new(),
tool_choice: None,

View File

@@ -41,6 +41,7 @@ use theme::ThemeSettings;
use ui::{Disclosure, KeyBinding, PopoverMenuHandle, Tooltip, prelude::*};
use util::{ResultExt as _, maybe};
use workspace::{CollaboratorId, Workspace};
use zed_llm_client::CompletionIntent;
use crate::context_picker::{ContextPicker, ContextPickerCompletionProvider, crease_for_mention};
use crate::context_store::ContextStore;
@@ -358,7 +359,12 @@ impl MessageEditor {
thread
.update(cx, |thread, cx| {
thread.advance_prompt_id();
thread.send_to_model(model, Some(window_handle), cx);
thread.send_to_model(
model,
CompletionIntent::UserPrompt,
Some(window_handle),
cx,
);
})
.log_err();
})
@@ -1248,6 +1254,7 @@ impl MessageEditor {
let request = language_model::LanguageModelRequest {
thread_id: None,
prompt_id: None,
intent: None,
mode: None,
messages: vec![request_message],
tools: vec![],

View File

@@ -25,6 +25,7 @@ use terminal_view::TerminalView;
use ui::prelude::*;
use util::ResultExt;
use workspace::{Toast, Workspace, notifications::NotificationId};
use zed_llm_client::CompletionIntent;
pub fn init(
fs: Arc<dyn Fs>,
@@ -291,6 +292,7 @@ impl TerminalInlineAssistant {
thread_id: None,
prompt_id: None,
mode: None,
intent: Some(CompletionIntent::TerminalInlineAssist),
messages: vec![request_message],
tools: Vec::new(),
tool_choice: None,

View File

@@ -38,7 +38,7 @@ use thiserror::Error;
use ui::Window;
use util::{ResultExt as _, post_inc};
use uuid::Uuid;
use zed_llm_client::CompletionRequestStatus;
use zed_llm_client::{CompletionIntent, CompletionRequestStatus};
use crate::ThreadStore;
use crate::context::{AgentContext, AgentContextHandle, ContextLoadResult, LoadedContext};
@@ -1157,6 +1157,7 @@ impl Thread {
pub fn send_to_model(
&mut self,
model: Arc<dyn LanguageModel>,
intent: CompletionIntent,
window: Option<AnyWindowHandle>,
cx: &mut Context<Self>,
) {
@@ -1166,7 +1167,7 @@ impl Thread {
self.remaining_turns -= 1;
let request = self.to_completion_request(model.clone(), cx);
let request = self.to_completion_request(model.clone(), intent, cx);
self.stream_completion(request, model, window, cx);
}
@@ -1186,11 +1187,13 @@ impl Thread {
pub fn to_completion_request(
&self,
model: Arc<dyn LanguageModel>,
intent: CompletionIntent,
cx: &mut Context<Self>,
) -> LanguageModelRequest {
let mut request = LanguageModelRequest {
thread_id: Some(self.id.to_string()),
prompt_id: Some(self.last_prompt_id.to_string()),
intent: Some(intent),
mode: None,
messages: vec![],
tools: Vec::new(),
@@ -1344,12 +1347,14 @@ impl Thread {
fn to_summarize_request(
&self,
model: &Arc<dyn LanguageModel>,
intent: CompletionIntent,
added_user_message: String,
cx: &App,
) -> LanguageModelRequest {
let mut request = LanguageModelRequest {
thread_id: None,
prompt_id: None,
intent: Some(intent),
mode: None,
messages: vec![],
tools: Vec::new(),
@@ -1693,6 +1698,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| {
@@ -1789,7 +1831,12 @@ impl Thread {
If the conversation is about a specific subject, include it in the title. \
Be descriptive. DO NOT speak in the first person.";
let request = self.to_summarize_request(&model.model, added_user_message.into(), cx);
let request = self.to_summarize_request(
&model.model,
CompletionIntent::ThreadSummarization,
added_user_message.into(),
cx,
);
self.summary = ThreadSummary::Generating;
@@ -1890,7 +1937,12 @@ impl Thread {
4. Any action items or next steps if any\n\
Format it in Markdown with headings and bullet points.";
let request = self.to_summarize_request(&model, added_user_message.into(), cx);
let request = self.to_summarize_request(
&model,
CompletionIntent::ThreadContextSummarization,
added_user_message.into(),
cx,
);
*self.detailed_summary_tx.borrow_mut() = DetailedSummaryState::Generating {
message_id: last_message_id,
@@ -1982,7 +2034,8 @@ impl Thread {
model: Arc<dyn LanguageModel>,
) -> Vec<PendingToolUse> {
self.auto_capture_telemetry(cx);
let request = Arc::new(self.to_completion_request(model.clone(), cx));
let request =
Arc::new(self.to_completion_request(model.clone(), CompletionIntent::ToolResults, cx));
let pending_tool_uses = self
.tool_use
.pending_tool_uses()
@@ -2178,7 +2231,7 @@ impl Thread {
if self.all_tools_finished() {
if let Some(ConfiguredModel { model, .. }) = self.configured_model.as_ref() {
if !canceled {
self.send_to_model(model.clone(), window, cx);
self.send_to_model(model.clone(), CompletionIntent::ToolResults, window, cx);
}
self.auto_capture_telemetry(cx);
}
@@ -2865,7 +2918,7 @@ fn main() {{
// Check message in request
let request = thread.update(cx, |thread, cx| {
thread.to_completion_request(model.clone(), cx)
thread.to_completion_request(model.clone(), CompletionIntent::UserPrompt, cx)
});
assert_eq!(request.messages.len(), 2);
@@ -2960,7 +3013,7 @@ fn main() {{
// Check entire request to make sure all contexts are properly included
let request = thread.update(cx, |thread, cx| {
thread.to_completion_request(model.clone(), cx)
thread.to_completion_request(model.clone(), CompletionIntent::UserPrompt, cx)
});
// The request should contain all 3 messages
@@ -3067,7 +3120,7 @@ fn main() {{
// Check message in request
let request = thread.update(cx, |thread, cx| {
thread.to_completion_request(model.clone(), cx)
thread.to_completion_request(model.clone(), CompletionIntent::UserPrompt, cx)
});
assert_eq!(request.messages.len(), 2);
@@ -3093,7 +3146,7 @@ fn main() {{
// Check that both messages appear in the request
let request = thread.update(cx, |thread, cx| {
thread.to_completion_request(model.clone(), cx)
thread.to_completion_request(model.clone(), CompletionIntent::UserPrompt, cx)
});
assert_eq!(request.messages.len(), 3);
@@ -3137,7 +3190,7 @@ fn main() {{
// Create a request and check that it doesn't have a stale buffer warning yet
let initial_request = thread.update(cx, |thread, cx| {
thread.to_completion_request(model.clone(), cx)
thread.to_completion_request(model.clone(), CompletionIntent::UserPrompt, cx)
});
// Make sure we don't have a stale file warning yet
@@ -3173,7 +3226,7 @@ fn main() {{
// Create a new request and check for the stale buffer warning
let new_request = thread.update(cx, |thread, cx| {
thread.to_completion_request(model.clone(), cx)
thread.to_completion_request(model.clone(), CompletionIntent::UserPrompt, cx)
});
// We should have a stale file warning as the last message
@@ -3223,7 +3276,7 @@ fn main() {{
});
let request = thread.update(cx, |thread, cx| {
thread.to_completion_request(model.clone(), cx)
thread.to_completion_request(model.clone(), CompletionIntent::UserPrompt, cx)
});
assert_eq!(request.temperature, Some(0.66));
@@ -3243,7 +3296,7 @@ fn main() {{
});
let request = thread.update(cx, |thread, cx| {
thread.to_completion_request(model.clone(), cx)
thread.to_completion_request(model.clone(), CompletionIntent::UserPrompt, cx)
});
assert_eq!(request.temperature, Some(0.66));
@@ -3263,7 +3316,7 @@ fn main() {{
});
let request = thread.update(cx, |thread, cx| {
thread.to_completion_request(model.clone(), cx)
thread.to_completion_request(model.clone(), CompletionIntent::UserPrompt, cx)
});
assert_eq!(request.temperature, Some(0.66));
@@ -3283,7 +3336,7 @@ fn main() {{
});
let request = thread.update(cx, |thread, cx| {
thread.to_completion_request(model.clone(), cx)
thread.to_completion_request(model.clone(), CompletionIntent::UserPrompt, cx)
});
assert_eq!(request.temperature, None);
}
@@ -3315,7 +3368,12 @@ fn main() {{
// Send a message
thread.update(cx, |thread, cx| {
thread.insert_user_message("Hi!", ContextLoadResult::default(), None, vec![], cx);
thread.send_to_model(model.clone(), None, cx);
thread.send_to_model(
model.clone(),
CompletionIntent::ThreadSummarization,
None,
cx,
);
});
let fake_model = model.as_fake();
@@ -3410,7 +3468,7 @@ fn main() {{
vec![],
cx,
);
thread.send_to_model(model.clone(), None, cx);
thread.send_to_model(model.clone(), CompletionIntent::UserPrompt, None, cx);
});
let fake_model = model.as_fake();
@@ -3448,7 +3506,12 @@ fn main() {{
) {
thread.update(cx, |thread, cx| {
thread.insert_user_message("Hi!", ContextLoadResult::default(), None, vec![], cx);
thread.send_to_model(model.clone(), None, cx);
thread.send_to_model(
model.clone(),
CompletionIntent::ThreadSummarization,
None,
cx,
);
});
let fake_model = model.as_fake();

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

@@ -57,6 +57,7 @@ uuid.workspace = true
workspace-hack.workspace = true
workspace.workspace = true
zed_actions.workspace = true
zed_llm_client.workspace = true
[dev-dependencies]
language_model = { workspace = true, features = ["test-support"] }

View File

@@ -44,6 +44,7 @@ use text::{BufferSnapshot, ToPoint};
use ui::IconName;
use util::{ResultExt, TryFutureExt, post_inc};
use uuid::Uuid;
use zed_llm_client::CompletionIntent;
#[derive(Clone, Debug, Eq, PartialEq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
pub struct ContextId(String);
@@ -2204,6 +2205,7 @@ impl AssistantContext {
StopReason::ToolUse => {}
StopReason::EndTurn => {}
StopReason::MaxTokens => {}
StopReason::Refusal => {}
}
}
})
@@ -2261,6 +2263,7 @@ impl AssistantContext {
let mut completion_request = LanguageModelRequest {
thread_id: None,
prompt_id: None,
intent: Some(CompletionIntent::UserPrompt),
mode: None,
messages: Vec::new(),
tools: Vec::new(),

View File

@@ -25,6 +25,7 @@ use serde::{Deserialize, Serialize};
use std::{cmp, iter, mem, ops::Range, path::PathBuf, sync::Arc, task::Poll};
use streaming_diff::{CharOperation, StreamingDiff};
use util::debug_panic;
use zed_llm_client::CompletionIntent;
#[derive(Serialize)]
struct CreateFilePromptTemplate {
@@ -102,7 +103,9 @@ impl EditAgent {
edit_description,
}
.render(&this.templates)?;
let new_chunks = this.request(conversation, prompt, cx).await?;
let new_chunks = this
.request(conversation, CompletionIntent::CreateFile, prompt, cx)
.await?;
let (output, mut inner_events) = this.overwrite_with_chunks(buffer, new_chunks, cx);
while let Some(event) = inner_events.next().await {
@@ -226,7 +229,9 @@ impl EditAgent {
edit_description,
}
.render(&this.templates)?;
let edit_chunks = this.request(conversation, prompt, cx).await?;
let edit_chunks = this
.request(conversation, CompletionIntent::EditFile, prompt, cx)
.await?;
let (output, mut inner_events) = this.apply_edit_chunks(buffer, edit_chunks, cx);
while let Some(event) = inner_events.next().await {
@@ -517,6 +522,7 @@ impl EditAgent {
async fn request(
&self,
mut conversation: LanguageModelRequest,
intent: CompletionIntent,
prompt: String,
cx: &mut AsyncApp,
) -> Result<BoxStream<'static, Result<String, LanguageModelCompletionError>>> {
@@ -574,6 +580,7 @@ impl EditAgent {
let request = LanguageModelRequest {
thread_id: conversation.thread_id,
prompt_id: conversation.prompt_id,
intent: Some(intent),
mode: conversation.mode,
messages: conversation.messages,
tool_choice,

View File

@@ -34,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(
@@ -49,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`.
"})],
@@ -83,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),
),
);
}
@@ -649,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;
}
@@ -1074,7 +1092,8 @@ impl EvalInput {
#[derive(Clone)]
struct EvalSample {
text: String,
text_before: String,
text_after: String,
edit_output: EditAgentOutput,
diff: String,
}
@@ -1131,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
@@ -1141,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 {
@@ -1225,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);
}
@@ -1470,6 +1505,7 @@ impl EditAgentTest {
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));
@@ -1498,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

@@ -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

@@ -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

@@ -905,7 +905,15 @@ impl Client {
}
futures::select_biased! {
result = self.set_connection(conn, cx).fuse() => ConnectionResult::Result(result.context("client auth and connect")),
result = self.set_connection(conn, cx).fuse() => {
match result.context("client auth and connect") {
Ok(()) => ConnectionResult::Result(Ok(())),
Err(err) => {
self.set_status(Status::ConnectionError, cx);
ConnectionResult::Result(Err(err))
},
}
},
_ = timeout => {
self.set_status(Status::ConnectionError, cx);
ConnectionResult::Timeout

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

@@ -56,6 +56,12 @@ pub use sea_orm::ConnectOptions;
pub use tables::user::Model as User;
pub use tables::*;
#[cfg(test)]
pub struct DatabaseTestOptions {
pub runtime: tokio::runtime::Runtime,
pub query_failure_probability: parking_lot::Mutex<f64>,
}
/// Database gives you a handle that lets you access the database.
/// It handles pooling internally.
pub struct Database {
@@ -68,7 +74,7 @@ pub struct Database {
notification_kinds_by_id: HashMap<NotificationKindId, &'static str>,
notification_kinds_by_name: HashMap<String, NotificationKindId>,
#[cfg(test)]
runtime: Option<tokio::runtime::Runtime>,
test_options: Option<DatabaseTestOptions>,
}
// The `Database` type has so many methods that its impl blocks are split into
@@ -87,7 +93,7 @@ impl Database {
notification_kinds_by_name: HashMap::default(),
executor,
#[cfg(test)]
runtime: None,
test_options: None,
})
}
@@ -355,11 +361,16 @@ impl Database {
{
#[cfg(test)]
{
let test_options = self.test_options.as_ref().unwrap();
if let Executor::Deterministic(executor) = &self.executor {
executor.simulate_random_delay().await;
let fail_probability = *test_options.query_failure_probability.lock();
if executor.rng().gen_bool(fail_probability) {
return Err(anyhow!("simulated query failure"))?;
}
}
self.runtime.as_ref().unwrap().block_on(future)
test_options.runtime.block_on(future)
}
#[cfg(not(test))]

View File

@@ -30,7 +30,7 @@ pub struct TestDb {
}
impl TestDb {
pub fn sqlite(background: BackgroundExecutor) -> Self {
pub fn sqlite(executor: BackgroundExecutor) -> Self {
let url = "sqlite::memory:";
let runtime = tokio::runtime::Builder::new_current_thread()
.enable_io()
@@ -41,7 +41,7 @@ impl TestDb {
let mut db = runtime.block_on(async {
let mut options = ConnectOptions::new(url);
options.max_connections(5);
let mut db = Database::new(options, Executor::Deterministic(background))
let mut db = Database::new(options, Executor::Deterministic(executor.clone()))
.await
.unwrap();
let sql = include_str!(concat!(
@@ -59,7 +59,10 @@ impl TestDb {
db
});
db.runtime = Some(runtime);
db.test_options = Some(DatabaseTestOptions {
runtime,
query_failure_probability: parking_lot::Mutex::new(0.0),
});
Self {
db: Some(Arc::new(db)),
@@ -67,7 +70,7 @@ impl TestDb {
}
}
pub fn postgres(background: BackgroundExecutor) -> Self {
pub fn postgres(executor: BackgroundExecutor) -> Self {
static LOCK: Mutex<()> = Mutex::new(());
let _guard = LOCK.lock();
@@ -90,7 +93,7 @@ impl TestDb {
options
.max_connections(5)
.idle_timeout(Duration::from_secs(0));
let mut db = Database::new(options, Executor::Deterministic(background))
let mut db = Database::new(options, Executor::Deterministic(executor.clone()))
.await
.unwrap();
let migrations_path = concat!(env!("CARGO_MANIFEST_DIR"), "/migrations");
@@ -101,7 +104,10 @@ impl TestDb {
db
});
db.runtime = Some(runtime);
db.test_options = Some(DatabaseTestOptions {
runtime,
query_failure_probability: parking_lot::Mutex::new(0.0),
});
Self {
db: Some(Arc::new(db)),
@@ -112,6 +118,12 @@ impl TestDb {
pub fn db(&self) -> &Arc<Database> {
self.db.as_ref().unwrap()
}
pub fn set_query_failure_probability(&self, probability: f64) {
let database = self.db.as_ref().unwrap();
let test_options = database.test_options.as_ref().unwrap();
*test_options.query_failure_probability.lock() = probability;
}
}
#[macro_export]
@@ -136,7 +148,7 @@ impl Drop for TestDb {
fn drop(&mut self) {
let db = self.db.take().unwrap();
if let sea_orm::DatabaseBackend::Postgres = db.pool.get_database_backend() {
db.runtime.as_ref().unwrap().block_on(async {
db.test_options.as_ref().unwrap().runtime.block_on(async {
use util::ResultExt;
let query = "
SELECT pg_terminate_backend(pg_stat_activity.pid)

View File

@@ -679,7 +679,7 @@ async fn test_collaborating_with_code_actions(
editor_b.update_in(cx_b, |editor, window, cx| {
editor.toggle_code_actions(
&ToggleCodeActions {
deployed_from_indicator: None,
deployed_from: None,
quick_launch: false,
},
window,

View File

@@ -61,6 +61,35 @@ fn init_logger() {
}
}
#[gpui::test(iterations = 10)]
async fn test_database_failure_during_client_reconnection(
executor: BackgroundExecutor,
cx: &mut TestAppContext,
) {
let mut server = TestServer::start(executor.clone()).await;
let client = server.create_client(cx, "user_a").await;
// Keep disconnecting the client until a database failure prevents it from
// reconnecting.
server.test_db.set_query_failure_probability(0.3);
loop {
server.disconnect_client(client.peer_id().unwrap());
executor.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT);
if !client.status().borrow().is_connected() {
break;
}
}
// Make the database healthy again and ensure the client can finally connect.
server.test_db.set_query_failure_probability(0.);
executor.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT);
assert!(
matches!(*client.status().borrow(), client::Status::Connected { .. }),
"status was {:?}",
*client.status().borrow()
);
}
#[gpui::test(iterations = 10)]
async fn test_basic_calls(
executor: BackgroundExecutor,

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

@@ -52,11 +52,11 @@ use livekit_client::test::TestServer as LivekitTestServer;
pub struct TestServer {
pub app_state: Arc<AppState>,
pub test_livekit_server: Arc<LivekitTestServer>,
pub test_db: TestDb,
server: Arc<Server>,
next_github_user_id: i32,
connection_killers: Arc<Mutex<HashMap<PeerId, Arc<AtomicBool>>>>,
forbid_connections: Arc<AtomicBool>,
_test_db: TestDb,
}
pub struct TestClient {
@@ -117,7 +117,7 @@ impl TestServer {
connection_killers: Default::default(),
forbid_connections: Default::default(),
next_github_user_id: 0,
_test_db: test_db,
test_db,
test_livekit_server: livekit_server,
}
}
@@ -241,7 +241,12 @@ impl TestServer {
let user = db
.get_user_by_id(user_id)
.await
.expect("retrieving user failed")
.map_err(|e| {
EstablishConnectionError::Other(anyhow!(
"retrieving user failed: {}",
e
))
})?
.unwrap();
cx.background_spawn(server.handle_connection(
server_conn,

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, 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"))]
@@ -434,32 +400,6 @@ impl FakeAdapter {
pub fn new() -> Self {
Self {}
}
fn request_args(&self, config: &DebugTaskDefinition) -> StartDebuggingRequestArguments {
use serde_json::json;
use task::DebugRequest;
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()
});
let request = match config.request {
DebugRequest::Launch(_) => dap_types::StartDebuggingRequestArgumentsRequest::Launch,
DebugRequest::Attach(_) => dap_types::StartDebuggingRequestArgumentsRequest::Attach,
};
StartDebuggingRequestArguments {
configuration: value,
request,
}
}
}
#[cfg(any(test, feature = "test-support"))]
@@ -469,10 +409,45 @@ 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>,
config: &DebugTaskDefinition,
task_definition: &DebugTaskDefinition,
_: Option<PathBuf>,
_: &mut AsyncApp,
) -> Result<DebugAdapterBinary> {
@@ -482,7 +457,10 @@ impl DebugAdapter for FakeAdapter {
connection: None,
envs: HashMap::default(),
cwd: None,
request_args: self.request_args(config),
request_args: StartDebuggingRequestArguments {
request: self.validate_config(&task_definition.config)?,
configuration: task_definition.config.clone(),
},
})
}
}

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,7 +26,9 @@ async-trait.workspace = true
dap.workspace = true
futures.workspace = true
gpui.workspace = true
json_dotpath.workspace = true
language.workspace = true
log.workspace = true
paths.workspace = true
serde.workspace = true
serde_json.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,283 @@ 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"]
}
]
}
},
{
"if": {
"properties": {
"request": {
"enum": ["attach"]
}
}
},
"then": {
"oneOf": [
{
"required": ["pid"]
},
{
"required": ["program"]
}
]
}
}
]
})
}
async fn get_binary(
&self,
delegate: &Arc<dyn DapDelegate>,
@@ -142,12 +405,12 @@ impl DebugAdapter for CodeLldbDebugAdapter {
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,139 @@ 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("request".into(), "attach".into());
obj.insert("pid".into(), attach.process_id.into());
}
dap::DebugRequest::Launch(launch) => {
obj.insert("request".into(), "launch".into());
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 +177,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,8 +1,13 @@
use anyhow::Context as _;
use dap::{StartDebuggingRequestArguments, adapters::DebugTaskDefinition};
use anyhow::{Context as _, anyhow, bail};
use dap::{
StartDebuggingRequestArguments, StartDebuggingRequestArgumentsRequest,
adapters::DebugTaskDefinition,
};
use gpui::{AsyncApp, SharedString};
use language::LanguageName;
use std::{collections::HashMap, ffi::OsStr, path::PathBuf};
use util;
use crate::*;
@@ -11,32 +16,7 @@ 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 {
dap::DebugRequest::Attach(attach_config) => {
json!({
"processId": attach_config.process_id,
})
}
dap::DebugRequest::Launch(launch_config) => json!({
"program": launch_config.program,
"cwd": launch_config.cwd,
"args": launch_config.args,
"env": launch_config.env_json()
}),
};
let map = args.as_object_mut().unwrap();
if let Some(stop_on_entry) = config.stop_on_entry {
map.insert("stopOnEntry".into(), stop_on_entry.into());
}
StartDebuggingRequestArguments {
configuration: args,
request: config.request.to_dap(),
}
}
const DEFAULT_TIMEOUT_MS: u64 = 60000;
}
#[async_trait(?Send)]
@@ -49,33 +29,400 @@ impl DebugAdapter for GoDebugAdapter {
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!({
"request": "attach",
"mode": "debug",
"processId": attach_config.process_id,
})
}
dap::DebugRequest::Launch(launch_config) => json!({
"request": "launch",
"mode": "debug",
"program": launch_config.program,
"cwd": launch_config.cwd,
"args": launch_config.args,
"env": launch_config.env_json()
}),
};
let map = args.as_object_mut().unwrap();
if let Some(stop_on_entry) = zed_scenario.stop_on_entry {
map.insert("stopOnEntry".into(), stop_on_entry.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> {
let delve_path = delegate
.which(OsStr::new("dlv"))
.await
.and_then(|p| p.to_str().map(|p| p.to_string()))
.context("Dlv not found in path")?;
let adapter_path = paths::debug_adapters_dir().join(&Self::ADAPTER_NAME);
let dlv_path = adapter_path.join("dlv");
let delve_path = if let Some(path) = delegate.which(OsStr::new("dlv")).await {
path.to_string_lossy().to_string()
} else if delegate.fs().is_file(&dlv_path).await {
dlv_path.to_string_lossy().to_string()
} else {
let go = delegate
.which(OsStr::new("go"))
.await
.context("Go not found in path. Please install Go first, then Dlv will be installed automatically.")?;
let adapter_path = paths::debug_adapters_dir().join(&Self::ADAPTER_NAME);
let install_output = util::command::new_smol_command(&go)
.env("GO111MODULE", "on")
.env("GOBIN", &adapter_path)
.args(&["install", "github.com/go-delve/delve/cmd/dlv@latest"])
.output()
.await?;
if !install_output.status.success() {
bail!(
"failed to install dlv via `go install`. stdout: {:?}, stderr: {:?}\n Please try installing it manually using 'go install github.com/go-delve/delve/cmd/dlv@latest'",
String::from_utf8_lossy(&install_output.stdout),
String::from_utf8_lossy(&install_output.stderr)
);
}
adapter_path.join("dlv").to_string_lossy().to_string()
};
let mut tcp_connection = task_definition.tcp_connection.clone().unwrap_or_default();
if tcp_connection.timeout.is_none()
|| tcp_connection.timeout.unwrap_or(0) < Self::DEFAULT_TIMEOUT_MS
{
tcp_connection.timeout = Some(Self::DEFAULT_TIMEOUT_MS);
}
let tcp_connection = config.tcp_connection.clone().unwrap_or_default();
let (host, port, timeout) = crate::configure_tcp_connection(tcp_connection).await?;
let cwd = task_definition
.config
.get("cwd")
.and_then(|s| s.as_str())
.map(PathBuf::from)
.unwrap_or_else(|| delegate.worktree_root_path().to_path_buf());
let arguments = if cfg!(windows) {
vec![
"dap".into(),
"--listen".into(),
format!("{}:{}", host, port),
"--headless".into(),
]
} else {
vec![
"dap".into(),
"--listen".into(),
format!("{}:{}", host, port),
]
};
Ok(DebugAdapterBinary {
command: delve_path,
arguments: vec!["dap".into(), "--listen".into(), format!("{host}:{port}")],
cwd: None,
arguments,
cwd: Some(cwd),
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,331 @@ 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() && config.get("url").is_none() {
return Err(anyhow!(
"either program or url is required for launch request"
));
}
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) => {
if launch.program.starts_with("http://") {
map.insert("url".into(), launch.program.clone().into());
} else {
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"
}
}
},
"oneOf": [
{ "required": ["program"] },
{ "required": ["url"] }
]
}
]
},
{
"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>,
@@ -68,7 +50,7 @@ impl PhpDebugAdapter {
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 +68,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 +90,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 as DebugAdapter>::validate_config(self, &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 +296,40 @@ impl DebugAdapter for PhpDebugAdapter {
Some(SharedString::new_static("PHP").into())
}
fn validate_config(
&self,
_: &serde_json::Value,
) -> Result<StartDebuggingRequestArgumentsRequest> {
Ok(StartDebuggingRequestArgumentsRequest::Launch)
}
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,9 +1,20 @@
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 language::LanguageName;
use std::{collections::HashMap, ffi::OsStr, path::PathBuf, sync::OnceLock};
use json_dotpath::DotPaths;
use language::{LanguageName, Toolchain};
use serde_json::Value;
use std::net::Ipv4Addr;
use std::{
collections::HashMap,
ffi::OsStr,
path::{Path, PathBuf},
sync::OnceLock,
};
use util::ResultExt;
#[derive(Default)]
@@ -17,39 +28,78 @@ 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());
}
async fn generate_debugpy_arguments(
&self,
host: &Ipv4Addr,
port: u16,
user_installed_path: Option<&Path>,
installed_in_venv: bool,
) -> Result<Vec<String>> {
if let Some(user_installed_path) = user_installed_path {
log::debug!(
"Using user-installed debugpy adapter from: {}",
user_installed_path.display()
);
Ok(vec![
user_installed_path
.join(Self::ADAPTER_PATH)
.to_string_lossy()
.to_string(),
format!("--host={}", host),
format!("--port={}", port),
])
} else if installed_in_venv {
log::debug!("Using venv-installed debugpy");
Ok(vec![
"-m".to_string(),
"debugpy.adapter".to_string(),
format!("--host={}", host),
format!("--port={}", port),
])
} else {
let adapter_path = paths::debug_adapters_dir().join(self.name().as_ref());
let file_name_prefix = format!("{}_", Self::ADAPTER_NAME);
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 debugpy_dir =
util::fs::find_file_name_in_dir(adapter_path.as_path(), |file_name| {
file_name.starts_with(&file_name_prefix)
})
.await
.context("Debugpy directory not found")?;
log::debug!(
"Using GitHub-downloaded debugpy adapter from: {}",
debugpy_dir.display()
);
Ok(vec![
debugpy_dir
.join(Self::ADAPTER_PATH)
.to_string_lossy()
.to_string(),
format!("--host={}", host),
format!("--port={}", port),
])
}
}
fn request_args(
&self,
task_definition: &DebugTaskDefinition,
) -> Result<StartDebuggingRequestArguments> {
let request = self.validate_config(&task_definition.config)?;
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,
@@ -97,35 +147,13 @@ impl PythonDebugAdapter {
delegate: &Arc<dyn DapDelegate>,
config: &DebugTaskDefinition,
user_installed_path: Option<PathBuf>,
cx: &mut AsyncApp,
toolchain: Option<Toolchain>,
installed_in_venv: bool,
) -> Result<DebugAdapterBinary> {
const BINARY_NAMES: [&str; 3] = ["python3", "python", "py"];
let tcp_connection = config.tcp_connection.clone().unwrap_or_default();
let (host, port, timeout) = crate::configure_tcp_connection(tcp_connection).await?;
let debugpy_dir = if let Some(user_installed_path) = user_installed_path {
user_installed_path
} else {
let adapter_path = paths::debug_adapters_dir().join(self.name().as_ref());
let file_name_prefix = format!("{}_", Self::ADAPTER_NAME);
util::fs::find_file_name_in_dir(adapter_path.as_path(), |file_name| {
file_name.starts_with(&file_name_prefix)
})
.await
.context("Debugpy directory not found")?
};
let toolchain = delegate
.toolchain_store()
.active_toolchain(
delegate.worktree_id(),
Arc::from("".as_ref()),
language::LanguageName::new(Self::LANGUAGE_NAME),
cx,
)
.await;
let python_path = if let Some(toolchain) = toolchain {
Some(toolchain.path.to_string())
} else {
@@ -143,24 +171,35 @@ impl PythonDebugAdapter {
name
};
let python_command = python_path.context("failed to find binary path for Python")?;
log::debug!("Using Python executable: {}", python_command);
let arguments = self
.generate_debugpy_arguments(
&host,
port,
user_installed_path.as_deref(),
installed_in_venv,
)
.await?;
log::debug!(
"Starting debugpy adapter with command: {} {}",
python_command,
arguments.join(" ")
);
Ok(DebugAdapterBinary {
command: python_path.context("failed to find binary path for Python")?,
arguments: vec![
debugpy_dir
.join(Self::ADAPTER_PATH)
.to_string_lossy()
.to_string(),
format!("--port={}", port),
format!("--host={}", host),
],
command: python_command,
arguments,
connection: Some(adapters::TcpArguments {
host,
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 +214,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>,
@@ -182,6 +612,47 @@ impl DebugAdapter for PythonDebugAdapter {
user_installed_path: Option<PathBuf>,
cx: &mut AsyncApp,
) -> Result<DebugAdapterBinary> {
if let Some(local_path) = &user_installed_path {
log::debug!(
"Using user-installed debugpy adapter from: {}",
local_path.display()
);
return self
.get_installed_binary(delegate, &config, Some(local_path.clone()), None, false)
.await;
}
let toolchain = delegate
.toolchain_store()
.active_toolchain(
delegate.worktree_id(),
Arc::from("".as_ref()),
language::LanguageName::new(Self::LANGUAGE_NAME),
cx,
)
.await;
if let Some(toolchain) = &toolchain {
if let Some(path) = Path::new(&toolchain.path.to_string()).parent() {
let debugpy_path = path.join("debugpy");
if delegate.fs().is_file(&debugpy_path).await {
log::debug!(
"Found debugpy in toolchain environment: {}",
debugpy_path.display()
);
return self
.get_installed_binary(
delegate,
&config,
None,
Some(toolchain.clone()),
true,
)
.await;
}
}
}
if self.checked.set(()).is_ok() {
delegate.output_to_console(format!("Checking latest version of {}...", self.name()));
if let Some(version) = self.fetch_latest_adapter_version(delegate).await.log_err() {
@@ -189,7 +660,49 @@ impl DebugAdapter for PythonDebugAdapter {
}
}
self.get_installed_binary(delegate, &config, user_installed_path, cx)
self.get_installed_binary(delegate, &config, None, None, false)
.await
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::{net::Ipv4Addr, path::PathBuf};
#[gpui::test]
async fn test_debugpy_install_path_cases() {
let adapter = PythonDebugAdapter::default();
let host = Ipv4Addr::new(127, 0, 0, 1);
let port = 5678;
// Case 1: User-defined debugpy path (highest precedence)
let user_path = PathBuf::from("/custom/path/to/debugpy");
let user_args = adapter
.generate_debugpy_arguments(&host, port, Some(&user_path), false)
.await
.unwrap();
// Case 2: Venv-installed debugpy (uses -m debugpy.adapter)
let venv_args = adapter
.generate_debugpy_arguments(&host, port, None, true)
.await
.unwrap();
assert!(user_args[0].ends_with("src/debugpy/adapter"));
assert_eq!(user_args[1], "--host=127.0.0.1");
assert_eq!(user_args[2], "--port=5678");
assert_eq!(venv_args[0], "-m");
assert_eq!(venv_args[1], "debugpy.adapter");
assert_eq!(venv_args[2], "--host=127.0.0.1");
assert_eq!(venv_args[3], "--port=5678");
// Note: Case 3 (GitHub-downloaded debugpy) is not tested since this requires mocking the Github API.
}
#[test]
fn test_adapter_path_constant() {
assert_eq!(PythonDebugAdapter::ADAPTER_PATH, "src/debugpy/adapter");
}
}

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

@@ -65,6 +65,7 @@ workspace-hack.workspace = true
env_logger = { workspace = true, optional = true }
debugger_tools = { workspace = true, optional = true }
unindent = { workspace = true, optional = true }
zed_actions.workspace = true
[dev-dependencies]
dap = { workspace = true, features = ["test-support"] }

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,20 +65,33 @@ 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>,
_subscriptions: [Subscription; 1],
}
impl DebugPanel {
pub fn new(
workspace: &Workspace,
_window: &mut Window,
window: &mut Window,
cx: &mut Context<Workspace>,
) -> Entity<Self> {
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 {
let focus_subscription = cx.on_focus(
&focus_handle,
window,
|this: &mut DebugPanel, window, cx| {
this.focus_active_item(window, cx);
},
);
Self {
size: px(300.),
sessions: vec![],
active_session: None,
@@ -87,9 +100,10 @@ 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,
_subscriptions: [focus_subscription],
}
})
}
@@ -97,15 +111,12 @@ impl DebugPanel {
let Some(session) = self.active_session.clone() else {
return;
};
let Some(active_pane) = session
let active_pane = session
.read(cx)
.running_state()
.read(cx)
.active_pane()
.cloned()
else {
return;
};
.clone();
active_pane.update(cx, |pane, cx| {
pane.focus_active_item(window, cx);
});
@@ -269,7 +280,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 +306,7 @@ impl DebugPanel {
cx.spawn(async move |_, cx| {
if let Err(error) = task.await {
log::error!("{error}");
session
.update(cx, |session, cx| {
session
@@ -310,72 +322,63 @@ impl DebugPanel {
.detach_and_log_err(cx);
}
pub(crate) fn rerun_last_session(
&mut self,
workspace: &mut Workspace,
window: &mut Window,
cx: &mut Context<Self>,
) {
let task_store = workspace.project().read(cx).task_store().clone();
let Some(task_inventory) = task_store.read(cx).task_inventory() else {
return;
};
let Some(scenario) = task_inventory.read(cx).last_scheduled_scenario().cloned() else {
return;
};
let workspace = self.workspace.clone();
cx.spawn_in(window, async move |this, cx| {
let task_contexts = workspace
.update_in(cx, |workspace, window, cx| {
tasks_ui::task_contexts(workspace, window, cx)
})?
.await;
let task_context = task_contexts.active_context().cloned().unwrap_or_default();
let worktree_id = task_contexts.worktree();
this.update_in(cx, |this, window, cx| {
this.start_session(
scenario.clone(),
task_context,
None,
worktree_id,
window,
cx,
);
})
})
.detach();
}
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)
}
@@ -392,7 +395,7 @@ impl DebugPanel {
}
let Some(worktree) = curr_session.read(cx).worktree() else {
log::error!("Attempted to start a child session from non local debug session");
log::error!("Attempted to restart a non-running session");
return;
};
@@ -413,7 +416,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);
@@ -427,7 +430,7 @@ impl DebugPanel {
cx: &mut Context<Self>,
) {
let Some(worktree) = parent_session.read(cx).worktree() else {
log::error!("Attempted to start a child session from non local debug session");
log::error!("Attempted to start a child-session from a non-running session");
return;
};
@@ -436,7 +439,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 +449,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 +748,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 +907,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 +935,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 +1001,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 +1286,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,9 @@ actions!(
FocusLoadedSources,
FocusTerminal,
ShowStackTrace,
ToggleThreadPicker,
ToggleSessionPicker,
RerunLastSession,
]
);
@@ -60,16 +63,7 @@ pub fn init(cx: &mut App) {
cx.when_flag_enabled::<DebuggerFeatureFlag>(window, |workspace, _, _| {
workspace
.register_action(|workspace, _: &ToggleFocus, window, cx| {
let did_focus_panel = workspace.toggle_panel_focus::<DebugPanel>(window, cx);
if !did_focus_panel {
return;
};
let Some(panel) = workspace.panel::<DebugPanel>(cx) else {
return;
};
panel.update(cx, |panel, cx| {
panel.focus_active_item(window, cx);
})
workspace.toggle_panel_focus::<DebugPanel>(window, cx);
})
.register_action(|workspace, _: &Pause, _, cx| {
if let Some(debug_panel) = workspace.panel::<DebugPanel>(cx) {
@@ -93,6 +87,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| {
@@ -115,6 +120,17 @@ pub fn init(cx: &mut App) {
}
}
})
.register_action(|workspace, _: &StepOut, _, 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.step_out(cx))
}
}
})
.register_action(|workspace, _: &StepBack, _, cx| {
if let Some(debug_panel) = workspace.panel::<DebugPanel>(cx) {
if let Some(active_item) = debug_panel.read_with(cx, |panel, cx| {
@@ -193,7 +209,18 @@ pub fn init(cx: &mut App) {
)
.register_action(|workspace: &mut Workspace, _: &Start, window, cx| {
NewSessionModal::show(workspace, window, cx);
});
})
.register_action(
|workspace: &mut Workspace, _: &RerunLastSession, window, cx| {
let Some(debug_panel) = workspace.panel::<DebugPanel>(cx) else {
return;
};
debug_panel.update(cx, |debug_panel, cx| {
debug_panel.rerun_last_session(workspace, window, cx);
})
},
);
})
})
.detach();

View File

@@ -132,7 +132,8 @@ impl DebugPanel {
this
}),
)
.style(DropdownStyle::Ghost),
.style(DropdownStyle::Ghost)
.handle(self.session_picker_menu_handle.clone()),
)
} else {
None
@@ -155,7 +156,13 @@ impl DebugPanel {
let selected_thread_name = threads
.iter()
.find(|(thread, _)| thread_id.map(|id| id.0) == Some(thread.id))
.map(|(thread, _)| thread.name.clone());
.map(|(thread, _)| {
thread
.name
.is_empty()
.then(|| format!("Tid: {}", thread.id))
.unwrap_or_else(|| thread.name.clone())
});
if let Some(selected_thread_name) = selected_thread_name {
let trigger = DebugPanel::dropdown_label(selected_thread_name).into_any_element();
@@ -163,11 +170,17 @@ 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;
this = this.entry(thread.name, None, move |window, cx| {
let entry_name = thread
.name
.is_empty()
.then(|| format!("Tid: {}", thread.id))
.unwrap_or_else(|| thread.name);
this = this.entry(entry_name, None, move |window, cx| {
running_state.update(cx, |running_state, cx| {
running_state.select_thread(ThreadId(thread_id), window, cx);
});
@@ -177,7 +190,8 @@ impl DebugPanel {
}),
)
.disabled(session_terminated)
.style(DropdownStyle::Ghost),
.style(DropdownStyle::Ghost)
.handle(self.thread_picker_menu_handle.clone()),
)
} else {
None

View File

@@ -1,5 +1,5 @@
use collections::FxHashMap;
use language::LanguageRegistry;
use language::{LanguageRegistry, Point, Selection};
use std::{
borrow::Cow,
ops::Not,
@@ -10,10 +10,9 @@ use std::{
};
use dap::{
DapRegistry, DebugRequest,
adapters::{DebugAdapterName, DebugTaskDefinition},
DapRegistry, DebugRequest, TelemetrySpawnLocation, adapters::DebugAdapterName, send_telemetry,
};
use editor::{Editor, EditorElement, EditorStyle};
use editor::{Anchor, Editor, EditorElement, EditorStyle, scroll::Autoscroll};
use fuzzy::{StringMatch, StringMatchCandidate};
use gpui::{
Animation, AnimationExt as _, App, AppContext, DismissEvent, Entity, EventEmitter, FocusHandle,
@@ -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,
@@ -38,7 +37,7 @@ use crate::{attach_modal::AttachModal, debugger_panel::DebugPanel};
enum SaveScenarioState {
Saving,
Saved(ProjectPath),
Saved((ProjectPath, SharedString)),
Failed(SharedString),
}
@@ -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,179 @@ 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 save_debug_scenario(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let Some((save_scenario, scenario_label)) = self
.debugger
.as_ref()
.and_then(|debugger| self.debug_scenario(&debugger, cx))
.zip(self.task_contexts(cx).and_then(|tcx| tcx.worktree()))
.and_then(|(scenario, worktree_id)| {
self.debug_panel
.update(cx, |panel, cx| {
panel.save_scenario(&scenario, worktree_id, window, cx)
})
.ok()
.zip(Some(scenario.label.clone()))
})
else {
return;
};
self.save_scenario_state = Some(SaveScenarioState::Saving);
cx.spawn(async move |this, cx| {
let res = save_scenario.await;
this.update(cx, |this, _| match res {
Ok(saved_file) => {
this.save_scenario_state =
Some(SaveScenarioState::Saved((saved_file, scenario_label)))
}
Err(error) => {
this.save_scenario_state =
Some(SaveScenarioState::Failed(error.to_string().into()))
}
})
.ok();
cx.background_executor().timer(Duration::from_secs(3)).await;
this.update(cx, |this, _| this.save_scenario_state.take())
.ok();
})
.detach();
}
fn render_save_state(&self, cx: &mut Context<Self>) -> impl IntoElement {
let this_entity = cx.weak_entity().clone();
div().when_some(self.save_scenario_state.as_ref(), {
let this_entity = this_entity.clone();
move |this, save_state| match save_state {
SaveScenarioState::Saved((saved_path, scenario_label)) => this.child(
IconButton::new("new-session-modal-go-to-file", IconName::ArrowUpRight)
.icon_size(IconSize::Small)
.icon_color(Color::Muted)
.on_click({
let this_entity = this_entity.clone();
let saved_path = saved_path.clone();
let scenario_label = scenario_label.clone();
move |_, window, cx| {
window
.spawn(cx, {
let this_entity = this_entity.clone();
let saved_path = saved_path.clone();
let scenario_label = scenario_label.clone();
async move |cx| {
let editor = this_entity
.update_in(cx, |this, window, cx| {
this.workspace.update(cx, |workspace, cx| {
workspace.open_path(
saved_path.clone(),
None,
true,
window,
cx,
)
})
})??
.await?;
cx.update(|window, cx| {
if let Some(editor) = editor.act_as::<Editor>(cx) {
editor.update(cx, |editor, cx| {
let row = editor
.text(cx)
.lines()
.enumerate()
.find_map(|(row, text)| {
if text.contains(
scenario_label.as_ref(),
) {
Some(row)
} else {
None
}
})?;
let buffer = editor.buffer().read(cx);
let excerpt_id =
*buffer.excerpt_ids().first()?;
let snapshot = buffer
.as_singleton()?
.read(cx)
.snapshot();
let anchor = snapshot.anchor_before(
Point::new(row as u32, 0),
);
let anchor = Anchor {
buffer_id: anchor.buffer_id,
excerpt_id,
text_anchor: anchor,
diff_base_anchor: None,
};
editor.change_selections(
Some(Autoscroll::center()),
window,
cx,
|selections| {
let id =
selections.new_selection_id();
selections.select_anchors(
vec![Selection {
id,
start: anchor,
end: anchor,
reversed: false,
goal: language::SelectionGoal::None
}],
);
},
);
Some(())
});
}
})?;
this_entity
.update(cx, |_, cx| cx.emit(DismissEvent))
.ok();
anyhow::Ok(())
}
})
.detach();
}
}),
),
SaveScenarioState::Saving => this.child(
Icon::new(IconName::Spinner)
.size(IconSize::Small)
.color(Color::Muted)
.with_animation(
"Spinner",
Animation::new(Duration::from_secs(3)).repeat(),
|icon, delta| icon.transform(Transformation::rotate(percentage(delta))),
),
),
SaveScenarioState::Failed(error_msg) => this.child(
IconButton::new("Failed Scenario Saved", IconName::X)
.icon_size(IconSize::Small)
.icon_color(Color::Error)
.tooltip(ui::Tooltip::text(error_msg.clone())),
),
}
})
}
fn adapter_drop_down_menu(
@@ -354,7 +526,7 @@ impl NewSessionModal {
static SELECT_DEBUGGER_LABEL: SharedString = SharedString::new_static("Select Debugger");
#[derive(Clone)]
enum NewSessionMode {
pub(crate) enum NewSessionMode {
Custom,
Attach,
Launch,
@@ -422,8 +594,6 @@ impl Render for NewSessionModal {
window: &mut ui::Window,
cx: &mut ui::Context<Self>,
) -> impl ui::IntoElement {
let this = cx.weak_entity().clone();
v_flex()
.size_full()
.w(rems(34.))
@@ -533,58 +703,7 @@ impl Render for NewSessionModal {
.child(
Button::new("new-session-modal-back", "Save to .zed/debug.json...")
.on_click(cx.listener(|this, _, window, cx| {
let Some(save_scenario) = this
.debugger
.as_ref()
.and_then(|debugger| this.debug_scenario(&debugger, cx))
.zip(
this.task_contexts(cx)
.and_then(|tcx| tcx.worktree()),
)
.and_then(|(scenario, worktree_id)| {
this.debug_panel
.update(cx, |panel, cx| {
panel.save_scenario(
&scenario,
worktree_id,
window,
cx,
)
})
.ok()
})
else {
return;
};
this.save_scenario_state = Some(SaveScenarioState::Saving);
cx.spawn(async move |this, cx| {
let res = save_scenario.await;
this.update(cx, |this, _| match res {
Ok(saved_file) => {
this.save_scenario_state =
Some(SaveScenarioState::Saved(saved_file))
}
Err(error) => {
this.save_scenario_state =
Some(SaveScenarioState::Failed(
error.to_string().into(),
))
}
})
.ok();
cx.background_executor()
.timer(Duration::from_secs(2))
.await;
this.update(cx, |this, _| {
this.save_scenario_state.take()
})
.ok();
})
.detach();
this.save_debug_scenario(window, cx);
}))
.disabled(
self.debugger.is_none()
@@ -597,83 +716,7 @@ impl Render for NewSessionModal {
|| self.save_scenario_state.is_some(),
),
)
.when_some(self.save_scenario_state.as_ref(), {
let this_entity = this.clone();
move |this, save_state| match save_state {
SaveScenarioState::Saved(saved_path) => this.child(
IconButton::new(
"new-session-modal-go-to-file",
IconName::ArrowUpRight,
)
.icon_size(IconSize::Small)
.icon_color(Color::Muted)
.on_click({
let this_entity = this_entity.clone();
let saved_path = saved_path.clone();
move |_, window, cx| {
window
.spawn(cx, {
let this_entity = this_entity.clone();
let saved_path = saved_path.clone();
async move |cx| {
this_entity
.update_in(
cx,
|this, window, cx| {
this.workspace.update(
cx,
|workspace, cx| {
workspace.open_path(
saved_path
.clone(),
None,
true,
window,
cx,
)
},
)
},
)??
.await?;
this_entity
.update(cx, |_, cx| {
cx.emit(DismissEvent)
})
.ok();
anyhow::Ok(())
}
})
.detach();
}
}),
),
SaveScenarioState::Saving => this.child(
Icon::new(IconName::Spinner)
.size(IconSize::Small)
.color(Color::Muted)
.with_animation(
"Spinner",
Animation::new(Duration::from_secs(3)).repeat(),
|icon, delta| {
icon.transform(Transformation::rotate(
percentage(delta),
))
},
),
),
SaveScenarioState::Failed(error_msg) => this.child(
IconButton::new("Failed Scenario Saved", IconName::X)
.icon_size(IconSize::Small)
.icon_color(Color::Error)
.tooltip(ui::Tooltip::text(error_msg.clone())),
),
}
}),
.child(self.render_save_state(cx)),
})
.child(
Button::new("debugger-spawn", "Start")
@@ -749,7 +792,10 @@ impl CustomMode {
let program = cx.new(|cx| Editor::single_line(window, cx));
program.update(cx, |this, cx| {
this.set_placeholder_text("Run", cx);
this.set_placeholder_text(
"ALPHA=\"Windows\" BETA=\"Wen\" your_program --arg1 --arg2=arg3",
cx,
);
if let Some(past_program) = past_program {
this.set_text(past_program, window, cx);
@@ -805,8 +851,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 +906,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 +917,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 +947,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 +980,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 +1000,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 +1121,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 +1136,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);
@@ -1151,7 +1168,7 @@ impl PickerDelegate for DebugScenarioDelegate {
let task_kind = &self.candidates[hit.candidate_id].0;
let icon = match task_kind {
Some(TaskSourceKind::Lsp(..)) => Some(Icon::new(IconName::Bolt)),
Some(TaskSourceKind::Lsp(..)) => Some(Icon::new(IconName::BoltFilled)),
Some(TaskSourceKind::UserInput) => Some(Icon::new(IconName::Terminal)),
Some(TaskSourceKind::AbsPath { .. }) => Some(Icon::new(IconName::Settings)),
Some(TaskSourceKind::Worktree { .. }) => Some(Icon::new(IconName::FileTree)),
@@ -1173,34 +1190,77 @@ 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)]
impl NewSessionModal {
pub(crate) fn set_custom(
&mut self,
program: impl AsRef<str>,
cwd: impl AsRef<str>,
stop_on_entry: bool,
window: &mut Window,
cx: &mut Context<Self>,
) {
self.mode = NewSessionMode::Custom;
self.debugger = Some(dap::adapters::DebugAdapterName("fake-adapter".into()));
self.custom_mode.update(cx, |custom, cx| {
custom.program.update(cx, |editor, cx| {
editor.clear(window, cx);
editor.set_text(program.as_ref(), window, cx);
});
custom.cwd.update(cx, |editor, cx| {
editor.clear(window, cx);
editor.set_text(cwd.as_ref(), window, cx);
});
custom.stop_on_entry = match stop_on_entry {
true => ToggleState::Selected,
_ => ToggleState::Unselected,
}
})
}
pub(crate) fn save_scenario(&mut self, window: &mut Window, cx: &mut Context<Self>) {
self.save_debug_scenario(window, cx);
}
}
#[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::{
@@ -71,7 +74,7 @@ pub struct RunningState {
console: Entity<Console>,
breakpoint_list: Entity<BreakpointList>,
panes: PaneGroup,
active_pane: Option<Entity<Pane>>,
active_pane: Entity<Pane>,
pane_close_subscriptions: HashMap<EntityId, Subscription>,
dock_axis: Axis,
_schedule_serialize: Option<Task<()>>,
@@ -82,8 +85,8 @@ impl RunningState {
self.thread_id
}
pub(crate) fn active_pane(&self) -> Option<&Entity<Pane>> {
self.active_pane.as_ref()
pub(crate) fn active_pane(&self) -> &Entity<Pane> {
&self.active_pane
}
}
@@ -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))
}
}
@@ -493,13 +496,22 @@ pub(crate) fn new_debugger_pane(
pub struct DebugTerminal {
pub terminal: Option<Entity<TerminalView>>,
focus_handle: FocusHandle,
_subscriptions: [Subscription; 1],
}
impl DebugTerminal {
fn empty(cx: &mut Context<Self>) -> Self {
fn empty(window: &mut Window, cx: &mut Context<Self>) -> Self {
let focus_handle = cx.focus_handle();
let focus_subscription = cx.on_focus(&focus_handle, window, |this, window, cx| {
if let Some(terminal) = this.terminal.as_ref() {
terminal.focus_handle(cx).focus(window);
}
});
Self {
terminal: None,
focus_handle: cx.focus_handle(),
focus_handle,
_subscriptions: [focus_subscription],
}
}
}
@@ -519,6 +531,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>,
@@ -535,7 +597,7 @@ impl RunningState {
StackFrameList::new(workspace.clone(), session.clone(), weak_state, window, cx)
});
let debug_terminal = cx.new(DebugTerminal::empty);
let debug_terminal = cx.new(|cx| DebugTerminal::empty(window, cx));
let variable_list =
cx.new(|cx| VariableList::new(session.clone(), stack_frame_list.clone(), window, cx));
@@ -561,15 +623,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 +701,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,
@@ -640,6 +712,7 @@ impl RunningState {
workspace::PaneGroup::with_root(root)
};
let active_pane = panes.first_pane();
Self {
session,
@@ -652,7 +725,7 @@ impl RunningState {
stack_frame_list,
session_id,
panes,
active_pane: None,
active_pane,
module_list,
console,
breakpoint_list,
@@ -705,6 +778,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 +788,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 +829,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 +908,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,
})
})
@@ -1177,7 +1240,7 @@ impl RunningState {
cx.notify();
}
Event::Focus => {
this.active_pane = Some(source_pane.clone());
this.active_pane = source_pane.clone();
}
Event::ZoomIn => {
source_pane.update(cx, |pane, cx| {
@@ -1201,10 +1264,10 @@ impl RunningState {
window: &mut Window,
cx: &mut Context<Self>,
) {
let active_pane = self.active_pane.clone();
if let Some(pane) = self
.active_pane
.as_ref()
.and_then(|pane| self.panes.find_pane_in_direction(pane, direction, cx))
.panes
.find_pane_in_direction(&active_pane, direction, cx)
{
pane.update(cx, |pane, cx| {
pane.focus_active_item(window, cx);
@@ -1468,10 +1531,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 +1574,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 +1608,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

@@ -1,13 +1,14 @@
use std::{
path::{Path, PathBuf},
sync::Arc,
time::Duration,
};
use dap::ExceptionBreakpointsFilter;
use editor::Editor;
use gpui::{
AppContext, Entity, FocusHandle, Focusable, ListState, MouseButton, Stateful, Task, WeakEntity,
list,
AppContext, Entity, FocusHandle, Focusable, MouseButton, ScrollStrategy, Stateful, Task,
UniformListScrollHandle, WeakEntity, uniform_list,
};
use language::Point;
use project::{
@@ -19,25 +20,27 @@ use project::{
worktree_store::WorktreeStore,
};
use ui::{
App, Clickable, Color, Context, Div, Icon, IconButton, IconName, Indicator, InteractiveElement,
IntoElement, Label, LabelCommon, LabelSize, ListItem, ParentElement, Render, RenderOnce,
Scrollbar, ScrollbarState, SharedString, StatefulInteractiveElement, Styled, Tooltip, Window,
div, h_flex, px, v_flex,
App, ButtonCommon, Clickable, Color, Context, Div, FluentBuilder as _, Icon, IconButton,
IconName, Indicator, InteractiveElement, IntoElement, Label, LabelCommon, LabelSize, ListItem,
ParentElement, Render, Scrollbar, ScrollbarState, SharedString, StatefulInteractiveElement,
Styled, Toggleable, Tooltip, Window, div, h_flex, px, v_flex,
};
use util::{ResultExt, maybe};
use util::ResultExt;
use workspace::Workspace;
use zed_actions::{ToggleEnableBreakpoint, UnsetBreakpoint};
pub(crate) struct BreakpointList {
workspace: WeakEntity<Workspace>,
breakpoint_store: Entity<BreakpointStore>,
worktree_store: Entity<WorktreeStore>,
list_state: ListState,
scrollbar_state: ScrollbarState,
breakpoints: Vec<BreakpointEntry>,
session: Entity<Session>,
hide_scrollbar_task: Option<Task<()>>,
show_scrollbar: bool,
focus_handle: FocusHandle,
scroll_handle: UniformListScrollHandle,
selected_ix: Option<usize>,
}
impl Focusable for BreakpointList {
@@ -56,38 +59,205 @@ impl BreakpointList {
let project = project.read(cx);
let breakpoint_store = project.breakpoint_store();
let worktree_store = project.worktree_store();
let focus_handle = cx.focus_handle();
let scroll_handle = UniformListScrollHandle::new();
let scrollbar_state = ScrollbarState::new(scroll_handle.clone());
cx.new(|cx| {
let weak: gpui::WeakEntity<Self> = cx.weak_entity();
let list_state = ListState::new(
0,
gpui::ListAlignment::Top,
px(1000.),
move |ix, window, cx| {
let Ok(Some(breakpoint)) =
weak.update(cx, |this, _| this.breakpoints.get(ix).cloned())
else {
return div().into_any_element();
};
breakpoint.render(window, cx).into_any_element()
},
);
cx.new(|_| {
Self {
breakpoint_store,
worktree_store,
scrollbar_state: ScrollbarState::new(list_state.clone()),
list_state,
scrollbar_state,
// list_state,
breakpoints: Default::default(),
hide_scrollbar_task: None,
show_scrollbar: false,
workspace,
session,
focus_handle: cx.focus_handle(),
focus_handle,
scroll_handle,
selected_ix: None,
}
})
}
fn edit_line_breakpoint(
&mut self,
path: Arc<Path>,
row: u32,
action: BreakpointEditAction,
cx: &mut Context<Self>,
) {
self.breakpoint_store.update(cx, |breakpoint_store, cx| {
if let Some((buffer, breakpoint)) = breakpoint_store.breakpoint_at_row(&path, row, cx) {
breakpoint_store.toggle_breakpoint(buffer, breakpoint, action, cx);
} else {
log::error!("Couldn't find breakpoint at row event though it exists: row {row}")
}
})
}
fn go_to_line_breakpoint(
&mut self,
path: Arc<Path>,
row: u32,
window: &mut Window,
cx: &mut Context<Self>,
) {
let task = self
.worktree_store
.update(cx, |this, cx| this.find_or_create_worktree(path, false, cx));
cx.spawn_in(window, async move |this, cx| {
let (worktree, relative_path) = task.await?;
let worktree_id = worktree.update(cx, |this, _| this.id())?;
let item = this
.update_in(cx, |this, window, cx| {
this.workspace.update(cx, |this, cx| {
this.open_path((worktree_id, relative_path), None, true, window, cx)
})
})??
.await?;
if let Some(editor) = item.downcast::<Editor>() {
editor
.update_in(cx, |this, window, cx| {
this.go_to_singleton_buffer_point(Point { row, column: 0 }, window, cx);
})
.ok();
}
anyhow::Ok(())
})
.detach();
}
fn select_ix(&mut self, ix: Option<usize>, cx: &mut Context<Self>) {
self.selected_ix = ix;
if let Some(ix) = ix {
self.scroll_handle
.scroll_to_item(ix, ScrollStrategy::Center);
}
cx.notify();
}
fn select_next(&mut self, _: &menu::SelectNext, _window: &mut Window, cx: &mut Context<Self>) {
let ix = match self.selected_ix {
_ if self.breakpoints.len() == 0 => None,
None => Some(0),
Some(ix) => {
if ix == self.breakpoints.len() - 1 {
Some(0)
} else {
Some(ix + 1)
}
}
};
self.select_ix(ix, cx);
}
fn select_previous(
&mut self,
_: &menu::SelectPrevious,
_window: &mut Window,
cx: &mut Context<Self>,
) {
let ix = match self.selected_ix {
_ if self.breakpoints.len() == 0 => None,
None => Some(self.breakpoints.len() - 1),
Some(ix) => {
if ix == 0 {
Some(self.breakpoints.len() - 1)
} else {
Some(ix - 1)
}
}
};
self.select_ix(ix, cx);
}
fn select_first(
&mut self,
_: &menu::SelectFirst,
_window: &mut Window,
cx: &mut Context<Self>,
) {
let ix = if self.breakpoints.len() > 0 {
Some(0)
} else {
None
};
self.select_ix(ix, cx);
}
fn select_last(&mut self, _: &menu::SelectLast, _window: &mut Window, cx: &mut Context<Self>) {
let ix = if self.breakpoints.len() > 0 {
Some(self.breakpoints.len() - 1)
} else {
None
};
self.select_ix(ix, cx);
}
fn confirm(&mut self, _: &menu::Confirm, window: &mut Window, cx: &mut Context<Self>) {
let Some(entry) = self.selected_ix.and_then(|ix| self.breakpoints.get_mut(ix)) else {
return;
};
match &mut entry.kind {
BreakpointEntryKind::LineBreakpoint(line_breakpoint) => {
let path = line_breakpoint.breakpoint.path.clone();
let row = line_breakpoint.breakpoint.row;
self.go_to_line_breakpoint(path, row, window, cx);
}
BreakpointEntryKind::ExceptionBreakpoint(_) => {}
}
}
fn toggle_enable_breakpoint(
&mut self,
_: &ToggleEnableBreakpoint,
_window: &mut Window,
cx: &mut Context<Self>,
) {
let Some(entry) = self.selected_ix.and_then(|ix| self.breakpoints.get_mut(ix)) else {
return;
};
match &mut entry.kind {
BreakpointEntryKind::LineBreakpoint(line_breakpoint) => {
let path = line_breakpoint.breakpoint.path.clone();
let row = line_breakpoint.breakpoint.row;
self.edit_line_breakpoint(path, row, BreakpointEditAction::InvertState, cx);
}
BreakpointEntryKind::ExceptionBreakpoint(exception_breakpoint) => {
let id = exception_breakpoint.id.clone();
self.session.update(cx, |session, cx| {
session.toggle_exception_breakpoint(&id, cx);
});
}
}
cx.notify();
}
fn unset_breakpoint(
&mut self,
_: &UnsetBreakpoint,
_window: &mut Window,
cx: &mut Context<Self>,
) {
let Some(entry) = self.selected_ix.and_then(|ix| self.breakpoints.get_mut(ix)) else {
return;
};
match &mut entry.kind {
BreakpointEntryKind::LineBreakpoint(line_breakpoint) => {
let path = line_breakpoint.breakpoint.path.clone();
let row = line_breakpoint.breakpoint.row;
self.edit_line_breakpoint(path, row, BreakpointEditAction::Toggle, cx);
}
BreakpointEntryKind::ExceptionBreakpoint(_) => {}
}
cx.notify();
}
fn hide_scrollbar(&mut self, window: &mut Window, cx: &mut Context<Self>) {
const SCROLLBAR_SHOW_INTERVAL: Duration = Duration::from_secs(1);
self.hide_scrollbar_task = Some(cx.spawn_in(window, async move |panel, cx| {
@@ -103,6 +273,30 @@ impl BreakpointList {
}))
}
fn render_list(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let selected_ix = self.selected_ix;
let focus_handle = self.focus_handle.clone();
uniform_list(
cx.entity(),
"breakpoint-list",
self.breakpoints.len(),
move |this, range, window, cx| {
range
.clone()
.zip(&mut this.breakpoints[range])
.map(|(ix, breakpoint)| {
breakpoint
.render(ix, focus_handle.clone(), window, cx)
.toggle_state(Some(ix) == selected_ix)
.into_any_element()
})
.collect()
},
)
.track_scroll(self.scroll_handle.clone())
.flex_grow()
}
fn render_vertical_scrollbar(&self, cx: &mut Context<Self>) -> Option<Stateful<Div>> {
if !(self.show_scrollbar || self.scrollbar_state.is_dragging()) {
return None;
@@ -142,12 +336,8 @@ impl BreakpointList {
}
}
impl Render for BreakpointList {
fn render(
&mut self,
_window: &mut ui::Window,
cx: &mut ui::Context<Self>,
) -> impl ui::IntoElement {
let old_len = self.breakpoints.len();
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl ui::IntoElement {
// let old_len = self.breakpoints.len();
let breakpoints = self.breakpoint_store.read(cx).all_source_breakpoints(cx);
self.breakpoints.clear();
let weak = cx.weak_entity();
@@ -183,7 +373,7 @@ impl Render for BreakpointList {
.map(ToOwned::to_owned)
.map(SharedString::from)?;
let weak = weak.clone();
let line = format!("Line {}", breakpoint.row + 1).into();
let line = breakpoint.row + 1;
Some(BreakpointEntry {
kind: BreakpointEntryKind::LineBreakpoint(LineBreakpoint {
name,
@@ -209,11 +399,9 @@ impl Render for BreakpointList {
});
self.breakpoints
.extend(breakpoints.chain(exception_breakpoints));
if self.breakpoints.len() != old_len {
self.list_state.reset(self.breakpoints.len());
}
v_flex()
.id("breakpoint-list")
.key_context("BreakpointList")
.track_focus(&self.focus_handle)
.on_hover(cx.listener(|this, hovered, window, cx| {
if *hovered {
@@ -224,9 +412,16 @@ impl Render for BreakpointList {
this.hide_scrollbar(window, cx);
}
}))
.on_action(cx.listener(Self::select_next))
.on_action(cx.listener(Self::select_previous))
.on_action(cx.listener(Self::select_first))
.on_action(cx.listener(Self::select_last))
.on_action(cx.listener(Self::confirm))
.on_action(cx.listener(Self::toggle_enable_breakpoint))
.on_action(cx.listener(Self::unset_breakpoint))
.size_full()
.m_0p5()
.child(list(self.list_state.clone()).flex_grow())
.child(self.render_list(window, cx))
.children(self.render_vertical_scrollbar(cx))
}
}
@@ -234,55 +429,58 @@ impl Render for BreakpointList {
struct LineBreakpoint {
name: SharedString,
dir: Option<SharedString>,
line: SharedString,
line: u32,
breakpoint: SourceBreakpoint,
}
impl LineBreakpoint {
fn render(self, weak: WeakEntity<BreakpointList>) -> ListItem {
let LineBreakpoint {
name,
dir,
line,
breakpoint,
} = self;
let icon_name = if breakpoint.state.is_enabled() {
fn render(
&mut self,
ix: usize,
focus_handle: FocusHandle,
weak: WeakEntity<BreakpointList>,
) -> ListItem {
let icon_name = if self.breakpoint.state.is_enabled() {
IconName::DebugBreakpoint
} else {
IconName::DebugDisabledBreakpoint
};
let path = breakpoint.path;
let row = breakpoint.row;
let path = self.breakpoint.path.clone();
let row = self.breakpoint.row;
let is_enabled = self.breakpoint.state.is_enabled();
let indicator = div()
.id(SharedString::from(format!(
"breakpoint-ui-toggle-{:?}/{}:{}",
dir, name, line
self.dir, self.name, self.line
)))
.cursor_pointer()
.tooltip(Tooltip::text(if breakpoint.state.is_enabled() {
"Disable Breakpoint"
} else {
"Enable Breakpoint"
}))
.tooltip({
let focus_handle = focus_handle.clone();
move |window, cx| {
Tooltip::for_action_in(
if is_enabled {
"Disable Breakpoint"
} else {
"Enable Breakpoint"
},
&ToggleEnableBreakpoint,
&focus_handle,
window,
cx,
)
}
})
.on_click({
let weak = weak.clone();
let path = path.clone();
move |_, _, cx| {
weak.update(cx, |this, cx| {
this.breakpoint_store.update(cx, |this, cx| {
if let Some((buffer, breakpoint)) =
this.breakpoint_at_row(&path, row, cx)
{
this.toggle_breakpoint(
buffer,
breakpoint,
BreakpointEditAction::InvertState,
cx,
);
} else {
log::error!("Couldn't find breakpoint at row event though it exists: row {row}")
}
})
weak.update(cx, |breakpoint_list, cx| {
breakpoint_list.edit_line_breakpoint(
path.clone(),
row,
BreakpointEditAction::InvertState,
cx,
);
})
.ok();
}
@@ -291,8 +489,17 @@ impl LineBreakpoint {
.on_mouse_down(MouseButton::Left, move |_, _, _| {});
ListItem::new(SharedString::from(format!(
"breakpoint-ui-item-{:?}/{}:{}",
dir, name, line
self.dir, self.name, self.line
)))
.on_click({
let weak = weak.clone();
move |_, _, cx| {
weak.update(cx, |breakpoint_list, cx| {
breakpoint_list.select_ix(Some(ix), cx);
})
.ok();
}
})
.start_slot(indicator)
.rounded()
.on_secondary_mouse_down(|_, _, cx| {
@@ -302,7 +509,7 @@ impl LineBreakpoint {
IconButton::new(
SharedString::from(format!(
"breakpoint-ui-on-click-go-to-line-remove-{:?}/{}:{}",
dir, name, line
self.dir, self.name, self.line
)),
IconName::Close,
)
@@ -310,103 +517,60 @@ impl LineBreakpoint {
let weak = weak.clone();
let path = path.clone();
move |_, _, cx| {
weak.update(cx, |this, cx| {
this.breakpoint_store.update(cx, |this, cx| {
if let Some((buffer, breakpoint)) =
this.breakpoint_at_row(&path, row, cx)
{
this.toggle_breakpoint(
buffer,
breakpoint,
BreakpointEditAction::Toggle,
cx,
);
} else {
log::error!("Couldn't find breakpoint at row event though it exists: row {row}")
}
})
weak.update(cx, |breakpoint_list, cx| {
breakpoint_list.edit_line_breakpoint(
path.clone(),
row,
BreakpointEditAction::Toggle,
cx,
);
})
.ok();
}
})
.icon_size(ui::IconSize::XSmall),
.tooltip(move |window, cx| {
Tooltip::for_action_in(
"Unset Breakpoint",
&UnsetBreakpoint,
&focus_handle,
window,
cx,
)
})
.icon_size(ui::IconSize::Indicator),
)
.child(
v_flex()
.py_1()
.gap_1()
.min_h(px(22.))
.justify_center()
.id(SharedString::from(format!(
"breakpoint-ui-on-click-go-to-line-{:?}/{}:{}",
dir, name, line
self.dir, self.name, self.line
)))
.on_click(move |_, window, cx| {
let path = path.clone();
let weak = weak.clone();
let row = breakpoint.row;
maybe!({
let task = weak
.update(cx, |this, cx| {
this.worktree_store.update(cx, |this, cx| {
this.find_or_create_worktree(path, false, cx)
})
})
.ok()?;
window
.spawn(cx, async move |cx| {
let (worktree, relative_path) = task.await?;
let worktree_id = worktree.update(cx, |this, _| this.id())?;
let item = weak
.update_in(cx, |this, window, cx| {
this.workspace.update(cx, |this, cx| {
this.open_path(
(worktree_id, relative_path),
None,
true,
window,
cx,
)
})
})??
.await?;
if let Some(editor) = item.downcast::<Editor>() {
editor
.update_in(cx, |this, window, cx| {
this.go_to_singleton_buffer_point(
Point { row, column: 0 },
window,
cx,
);
})
.ok();
}
anyhow::Ok(())
})
.detach();
Some(())
});
weak.update(cx, |breakpoint_list, cx| {
breakpoint_list.select_ix(Some(ix), cx);
breakpoint_list.go_to_line_breakpoint(path.clone(), row, window, cx);
})
.ok();
})
.cursor_pointer()
.py_1()
.items_center()
.child(
h_flex()
.gap_1()
.child(
Label::new(name)
Label::new(format!("{}:{}", self.name, self.line))
.size(LabelSize::Small)
.line_height_style(ui::LineHeightStyle::UiLabel),
)
.children(dir.map(|dir| {
.children(self.dir.clone().map(|dir| {
Label::new(dir)
.color(Color::Muted)
.size(LabelSize::Small)
.line_height_style(ui::LineHeightStyle::UiLabel)
})),
)
.child(
Label::new(line)
.size(LabelSize::XSmall)
.color(Color::Muted)
.line_height_style(ui::LineHeightStyle::UiLabel),
),
)
}
@@ -419,17 +583,31 @@ struct ExceptionBreakpoint {
}
impl ExceptionBreakpoint {
fn render(self, list: WeakEntity<BreakpointList>) -> ListItem {
fn render(
&mut self,
ix: usize,
focus_handle: FocusHandle,
list: WeakEntity<BreakpointList>,
) -> ListItem {
let color = if self.is_enabled {
Color::Debugger
} else {
Color::Muted
};
let id = SharedString::from(&self.id);
let is_enabled = self.is_enabled;
ListItem::new(SharedString::from(format!(
"exception-breakpoint-ui-item-{}",
self.id
)))
.on_click({
let list = list.clone();
move |_, _, cx| {
list.update(cx, |list, cx| list.select_ix(Some(ix), cx))
.ok();
}
})
.rounded()
.on_secondary_mouse_down(|_, _, cx| {
cx.stop_propagation();
@@ -440,38 +618,49 @@ impl ExceptionBreakpoint {
"exception-breakpoint-ui-item-{}-click-handler",
self.id
)))
.tooltip(Tooltip::text(if self.is_enabled {
"Disable Exception Breakpoint"
} else {
"Enable Exception Breakpoint"
}))
.on_click(move |_, _, cx| {
list.update(cx, |this, cx| {
this.session.update(cx, |this, cx| {
this.toggle_exception_breakpoint(&id, cx);
});
cx.notify();
})
.ok();
.tooltip(move |window, cx| {
Tooltip::for_action_in(
if is_enabled {
"Disable Exception Breakpoint"
} else {
"Enable Exception Breakpoint"
},
&ToggleEnableBreakpoint,
&focus_handle,
window,
cx,
)
})
.on_click({
let list = list.clone();
move |_, _, cx| {
list.update(cx, |this, cx| {
this.session.update(cx, |this, cx| {
this.toggle_exception_breakpoint(&id, cx);
});
cx.notify();
})
.ok();
}
})
.cursor_pointer()
.child(Indicator::icon(Icon::new(IconName::Flame)).color(color)),
)
.child(
div()
v_flex()
.py_1()
.gap_1()
.min_h(px(22.))
.justify_center()
.id(("exception-breakpoint-label", ix))
.child(
Label::new(self.data.label)
Label::new(self.data.label.clone())
.size(LabelSize::Small)
.line_height_style(ui::LineHeightStyle::UiLabel),
)
.children(self.data.description.map(|description| {
Label::new(description)
.size(LabelSize::XSmall)
.line_height_style(ui::LineHeightStyle::UiLabel)
.color(Color::Muted)
})),
.when_some(self.data.description.clone(), |el, description| {
el.tooltip(Tooltip::text(description))
}),
)
}
}
@@ -486,14 +675,21 @@ struct BreakpointEntry {
kind: BreakpointEntryKind,
weak: WeakEntity<BreakpointList>,
}
impl RenderOnce for BreakpointEntry {
fn render(self, _: &mut ui::Window, _: &mut App) -> impl ui::IntoElement {
match self.kind {
impl BreakpointEntry {
fn render(
&mut self,
ix: usize,
focus_handle: FocusHandle,
_: &mut Window,
_: &mut App,
) -> ListItem {
match &mut self.kind {
BreakpointEntryKind::LineBreakpoint(line_breakpoint) => {
line_breakpoint.render(self.weak)
line_breakpoint.render(ix, focus_handle, self.weak.clone())
}
BreakpointEntryKind::ExceptionBreakpoint(exception_breakpoint) => {
exception_breakpoint.render(self.weak)
exception_breakpoint.render(ix, focus_handle, self.weak.clone())
}
}
}

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,7 +79,12 @@ impl Console {
let _subscriptions = vec![
cx.subscribe(&stack_frame_list, Self::handle_stack_frame_list_events),
cx.on_focus_in(&focus_handle, window, |console, window, cx| {
cx.subscribe_in(&session, window, |this, _, event, window, cx| {
if let SessionEvent::ConsoleOutput = event {
this.update_output(window, cx)
}
}),
cx.on_focus(&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

@@ -250,6 +250,9 @@ impl StackFrameList {
let Some(abs_path) = Self::abs_path_from_stack_frame(&stack_frame) else {
return Task::ready(Err(anyhow!("Project path not found")));
};
if abs_path.starts_with("<node_internals>") {
return Task::ready(Ok(()));
}
let row = stack_frame.line.saturating_sub(1) as u32;
cx.emit(StackFrameListEvent::SelectedStackFrameChanged(
stack_frame_id,

View File

@@ -2,17 +2,26 @@ use super::stack_frame_list::{StackFrameList, StackFrameListEvent};
use dap::{ScopePresentationHint, StackFrameId, VariablePresentationHintKind, VariableReference};
use editor::Editor;
use gpui::{
AnyElement, ClickEvent, ClipboardItem, Context, DismissEvent, Entity, FocusHandle, Focusable,
Hsla, MouseButton, MouseDownEvent, Point, Stateful, Subscription, TextStyleRefinement,
UniformListScrollHandle, actions, anchored, deferred, uniform_list,
Action, AnyElement, ClickEvent, ClipboardItem, Context, DismissEvent, Entity, FocusHandle,
Focusable, Hsla, MouseButton, MouseDownEvent, Point, Stateful, Subscription,
TextStyleRefinement, UniformListScrollHandle, actions, anchored, deferred, uniform_list,
};
use menu::{SelectFirst, SelectLast, SelectNext, SelectPrevious};
use project::debugger::session::{Session, SessionEvent};
use std::{collections::HashMap, ops::Range, sync::Arc};
use ui::{ContextMenu, ListItem, Scrollbar, ScrollbarState, prelude::*};
use util::{debug_panic, maybe};
use util::debug_panic;
actions!(variable_list, [ExpandSelectedEntry, CollapseSelectedEntry]);
actions!(
variable_list,
[
ExpandSelectedEntry,
CollapseSelectedEntry,
CopyVariableName,
CopyVariableValue,
EditVariable
]
);
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub(crate) struct EntryState {
@@ -154,12 +163,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 +312,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,27 +356,27 @@ 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);
self.cancel(&Default::default(), window, cx);
if let Some(variable) = self.entries.first() {
self.selection = Some(variable.path.clone());
cx.notify();
self.build_entries(cx);
}
}
fn select_last(&mut self, _: &SelectLast, window: &mut Window, cx: &mut Context<Self>) {
self.cancel_variable_edit(&Default::default(), window, cx);
self.cancel(&Default::default(), window, cx);
if let Some(variable) = self.entries.last() {
self.selection = Some(variable.path.clone());
cx.notify();
self.build_entries(cx);
}
}
fn select_prev(&mut self, _: &SelectPrevious, window: &mut Window, cx: &mut Context<Self>) {
self.cancel_variable_edit(&Default::default(), window, cx);
self.cancel(&Default::default(), window, cx);
if let Some(selection) = &self.selection {
let index = self.entries.iter().enumerate().find_map(|(ix, var)| {
if &var.path == selection && ix > 0 {
@@ -378,7 +390,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);
}
@@ -388,7 +400,7 @@ impl VariableList {
}
fn select_next(&mut self, _: &SelectNext, window: &mut Window, cx: &mut Context<Self>) {
self.cancel_variable_edit(&Default::default(), window, cx);
self.cancel(&Default::default(), window, cx);
if let Some(selection) = &self.selection {
let index = self.entries.iter().enumerate().find_map(|(ix, var)| {
if &var.path == selection {
@@ -402,7 +414,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);
}
@@ -411,40 +423,26 @@ impl VariableList {
}
}
fn cancel_variable_edit(
&mut self,
_: &menu::Cancel,
window: &mut Window,
cx: &mut Context<Self>,
) {
fn cancel(&mut self, _: &menu::Cancel, window: &mut Window, cx: &mut Context<Self>) {
self.edited_path.take();
self.focus_handle.focus(window);
cx.notify();
}
fn confirm_variable_edit(
&mut self,
_: &menu::Confirm,
_window: &mut Window,
cx: &mut Context<Self>,
) {
let res = maybe!({
let (var_path, editor) = self.edited_path.take()?;
let state = self.entry_states.get(&var_path)?;
fn confirm(&mut self, _: &menu::Confirm, _window: &mut Window, cx: &mut Context<Self>) {
if let Some((var_path, editor)) = self.edited_path.take() {
let Some(state) = self.entry_states.get(&var_path) else {
return;
};
let variables_reference = state.parent_reference;
let name = var_path.leaf_name?;
let Some(name) = var_path.leaf_name else {
return;
};
let value = editor.read(cx).text(cx);
self.session.update(cx, |session, cx| {
session.set_variable_value(variables_reference, name.into(), value, cx)
});
Some(())
});
if res.is_none() {
log::error!(
"Couldn't confirm variable edit because variable doesn't have a leaf name or a parent reference id"
);
}
}
@@ -464,7 +462,7 @@ impl VariableList {
self.select_prev(&SelectPrevious, window, cx);
} else {
entry_state.is_expanded = false;
cx.notify();
self.build_entries(cx);
}
}
}
@@ -485,45 +483,23 @@ impl VariableList {
self.select_next(&SelectNext, window, cx);
} else {
entry_state.is_expanded = true;
cx.notify();
self.build_entries(cx);
}
}
}
fn deploy_variable_context_menu(
&mut self,
variable: ListEntry,
_variable: ListEntry,
position: Point<Pixels>,
window: &mut Window,
cx: &mut Context<Self>,
) {
let Some(dap_var) = variable.as_variable() else {
debug_panic!("Trying to open variable context menu on a scope");
return;
};
let variable_value = dap_var.value.clone();
let variable_name = dap_var.name.clone();
let this = cx.entity().clone();
let context_menu = ContextMenu::build(window, cx, |menu, _, _| {
menu.entry("Copy name", None, move |_, cx| {
cx.write_to_clipboard(ClipboardItem::new_string(variable_name.clone()))
})
.entry("Copy value", None, {
let variable_value = variable_value.clone();
move |_, cx| {
cx.write_to_clipboard(ClipboardItem::new_string(variable_value.clone()))
}
})
.entry("Set value", None, move |window, cx| {
this.update(cx, |variable_list, cx| {
let editor = Self::create_variable_editor(&variable_value, window, cx);
variable_list.edited_path = Some((variable.path.clone(), editor));
cx.notify();
});
})
menu.action("Copy Name", CopyVariableName.boxed_clone())
.action("Copy Value", CopyVariableValue.boxed_clone())
.action("Edit Value", EditVariable.boxed_clone())
.context(self.focus_handle.clone())
});
cx.focus_view(&context_menu, window);
@@ -544,6 +520,59 @@ impl VariableList {
self.open_context_menu = Some((context_menu, position, subscription));
}
fn copy_variable_name(
&mut self,
_: &CopyVariableName,
_window: &mut Window,
cx: &mut Context<Self>,
) {
let Some(selection) = self.selection.as_ref() else {
return;
};
let Some(entry) = self.entries.iter().find(|entry| &entry.path == selection) else {
return;
};
let Some(variable) = entry.as_variable() else {
return;
};
cx.write_to_clipboard(ClipboardItem::new_string(variable.name.clone()));
}
fn copy_variable_value(
&mut self,
_: &CopyVariableValue,
_window: &mut Window,
cx: &mut Context<Self>,
) {
let Some(selection) = self.selection.as_ref() else {
return;
};
let Some(entry) = self.entries.iter().find(|entry| &entry.path == selection) else {
return;
};
let Some(variable) = entry.as_variable() else {
return;
};
cx.write_to_clipboard(ClipboardItem::new_string(variable.value.clone()));
}
fn edit_variable(&mut self, _: &EditVariable, window: &mut Window, cx: &mut Context<Self>) {
let Some(selection) = self.selection.as_ref() else {
return;
};
let Some(entry) = self.entries.iter().find(|entry| &entry.path == selection) else {
return;
};
let Some(variable) = entry.as_variable() else {
return;
};
let editor = Self::create_variable_editor(&variable.value, window, cx);
self.edited_path = Some((entry.path.clone(), editor));
cx.notify();
}
#[track_caller]
#[cfg(test)]
pub(crate) fn assert_visual_entries(&self, expected: Vec<&str>) {
@@ -812,12 +841,14 @@ impl VariableList {
.on_secondary_mouse_down(cx.listener({
let variable = variable.clone();
move |this, event: &MouseDownEvent, window, cx| {
this.selection = Some(variable.path.clone());
this.deploy_variable_context_menu(
variable.clone(),
event.position,
window,
cx,
)
);
cx.stop_propagation();
}
}))
.child(
@@ -929,8 +960,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")
@@ -942,11 +971,13 @@ impl Render for VariableList {
.on_action(cx.listener(Self::select_last))
.on_action(cx.listener(Self::select_prev))
.on_action(cx.listener(Self::select_next))
.on_action(cx.listener(Self::cancel))
.on_action(cx.listener(Self::confirm))
.on_action(cx.listener(Self::expand_selected_entry))
.on_action(cx.listener(Self::collapse_selected_entry))
.on_action(cx.listener(Self::cancel_variable_edit))
.on_action(cx.listener(Self::confirm_variable_edit))
//
.on_action(cx.listener(Self::copy_variable_name))
.on_action(cx.listener(Self::copy_variable_value))
.on_action(cx.listener(Self::edit_variable))
.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,8 @@ mod inline_values;
#[cfg(test)]
mod module_list;
#[cfg(test)]
mod new_session_modal;
#[cfg(test)]
mod persistence;
#[cfg(test)]
mod stack_frame_list;
@@ -136,16 +138,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));
@@ -1413,13 +1439,7 @@ async fn test_we_send_arguments_from_user_config(
client.on_request::<dap::requests::Launch, _>(move |_, args| {
launch_handler_called.store(true, Ordering::SeqCst);
let obj = args.raw.as_object().unwrap();
let sent_definition = serde_json::from_value::<DebugTaskDefinition>(
obj.get(&"raw_request".to_owned()).unwrap().clone(),
)
.unwrap();
assert_eq!(sent_definition, debug_definition);
assert_eq!(args.raw, debug_definition.config);
Ok(())
});

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,342 @@
use dap::DapRegistry;
use gpui::{BackgroundExecutor, TestAppContext, VisualTestContext};
use project::{FakeFs, Fs, Project};
use serde_json::json;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use task::{DebugRequest, DebugScenario, LaunchRequest, TaskContext, VariableName, ZedDebugConfig};
use util::path;
use crate::tests::{init_test, init_test_workspace};
#[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);
let test_variables = vec![(
VariableName::WorktreeRoot,
path!("/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 test_cases: Vec<(&'static str, &'static str)> = vec![
// Absolute path - should not be relativized
(
path!("/absolute/path/to/program"),
path!("/absolute/path/to/program"),
),
// Relative path - should be prefixed with worktree root
(
format!(".{0}src{0}program", std::path::MAIN_SEPARATOR).leak(),
path!("/test/worktree/path/src/program"),
),
// Home directory path - should be expanded to full home directory path
(
format!("~{0}src{0}program", std::path::MAIN_SEPARATOR).leak(),
home_dir
.join("src")
.join("program")
.to_string_lossy()
.to_string()
.leak(),
),
// Path with $ZED_WORKTREE_ROOT - should be substituted without double appending
(
format!(
"$ZED_WORKTREE_ROOT{0}src{0}program",
std::path::MAIN_SEPARATOR
)
.leak(),
path!("/test/worktree/path/src/program"),
),
];
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();
move |client| {
client.on_request::<dap::requests::Launch, _>({
let called_launch = called_launch.clone();
move |_, args| {
let config = args.raw.as_object().unwrap();
assert_eq!(
config["program"].as_str().unwrap(),
expected_path,
"Program path was not correctly substituted for input: {}",
input_path
);
assert_eq!(
config["cwd"].as_str().unwrap(),
expected_path,
"CWD path was not correctly substituted for input: {}",
input_path
);
let expected_other_field = if input_path.contains("$ZED_WORKTREE_ROOT") {
input_path
.replace("$ZED_WORKTREE_ROOT", &path!("/test/worktree/path"))
.to_owned()
} 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);
}
}
#[gpui::test]
async fn test_save_debug_scenario_to_file(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.clone(), [path!("/project").as_ref()], cx).await;
let workspace = init_test_workspace(&project, cx).await;
let cx = &mut VisualTestContext::from_window(*workspace, cx);
workspace
.update(cx, |workspace, window, cx| {
crate::new_session_modal::NewSessionModal::show(workspace, window, cx);
})
.unwrap();
cx.run_until_parked();
let modal = workspace
.update(cx, |workspace, _, cx| {
workspace.active_modal::<crate::new_session_modal::NewSessionModal>(cx)
})
.unwrap()
.expect("Modal should be active");
modal.update_in(cx, |modal, window, cx| {
modal.set_custom("/project/main", "/project", false, window, cx);
modal.save_scenario(window, cx);
});
cx.executor().run_until_parked();
let debug_json_content = fs
.load(path!("/project/.zed/debug.json").as_ref())
.await
.expect("debug.json should exist");
let expected_content = vec![
"[",
" {",
r#" "adapter": "fake-adapter","#,
r#" "label": "main (fake-adapter)","#,
r#" "request": "launch","#,
r#" "program": "/project/main","#,
r#" "cwd": "/project","#,
r#" "args": [],"#,
r#" "env": {}"#,
" }",
"]",
];
let actual_lines: Vec<&str> = debug_json_content.lines().collect();
pretty_assertions::assert_eq!(expected_content, actual_lines);
modal.update_in(cx, |modal, window, cx| {
modal.set_custom("/project/other", "/project", true, window, cx);
modal.save_scenario(window, cx);
});
cx.executor().run_until_parked();
let debug_json_content = fs
.load(path!("/project/.zed/debug.json").as_ref())
.await
.expect("debug.json should exist after second save");
let expected_content = vec![
"[",
" {",
r#" "adapter": "fake-adapter","#,
r#" "label": "main (fake-adapter)","#,
r#" "request": "launch","#,
r#" "program": "/project/main","#,
r#" "cwd": "/project","#,
r#" "args": [],"#,
r#" "env": {}"#,
" },",
" {",
r#" "adapter": "fake-adapter","#,
r#" "label": "other (fake-adapter)","#,
r#" "request": "launch","#,
r#" "program": "/project/other","#,
r#" "cwd": "/project","#,
r#" "args": [],"#,
r#" "env": {}"#,
" }",
"]",
];
let actual_lines: Vec<&str> = debug_json_content.lines().collect();
pretty_assertions::assert_eq!(expected_content, actual_lines);
}
#[gpui::test]
async fn test_dap_adapter_config_conversion_and_validation(cx: &mut TestAppContext) {
init_test(cx);
let mut expected_adapters = vec![
"CodeLLDB",
"Debugpy",
"PHP",
"JavaScript",
"Ruby",
"Delve",
"GDB",
"fake-adapter",
];
let adapter_names = cx.update(|cx| {
let registry = DapRegistry::global(cx);
registry.enumerate_adapters()
});
let zed_config = ZedDebugConfig {
label: "test_debug_session".into(),
adapter: "test_adapter".into(),
request: DebugRequest::Launch(LaunchRequest {
program: "test_program".into(),
cwd: None,
args: vec![],
env: Default::default(),
}),
stop_on_entry: Some(true),
};
for adapter_name in adapter_names {
let adapter_str = adapter_name.to_string();
if let Some(pos) = expected_adapters.iter().position(|&x| x == adapter_str) {
expected_adapters.remove(pos);
}
let adapter = cx
.update(|cx| {
let registry = DapRegistry::global(cx);
registry.adapter(adapter_name.as_ref())
})
.unwrap_or_else(|| panic!("Adapter {} should exist", adapter_name));
let mut adapter_specific_config = zed_config.clone();
adapter_specific_config.adapter = adapter_name.to_string().into();
let debug_scenario = adapter
.config_from_zed_format(adapter_specific_config)
.unwrap_or_else(|_| {
panic!(
"Adapter {} should successfully convert from Zed format",
adapter_name
)
});
assert!(
debug_scenario.config.is_object(),
"Adapter {} should produce a JSON object for config",
adapter_name
);
let request_type = adapter
.validate_config(&debug_scenario.config)
.unwrap_or_else(|_| {
panic!(
"Adapter {} should validate the config successfully",
adapter_name
)
});
match request_type {
dap::StartDebuggingRequestArgumentsRequest::Launch => {}
dap::StartDebuggingRequestArgumentsRequest::Attach => {
panic!(
"Expected Launch request but got Attach for adapter {}",
adapter_name
);
}
}
}
assert!(
expected_adapters.is_empty(),
"The following expected adapters were not found in the registry: {:?}",
expected_adapters
);
}

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

@@ -74,16 +74,22 @@ pub struct SelectToEndOfLine {
#[derive(PartialEq, Clone, Deserialize, Default, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct ToggleCodeActions {
// Display row from which the action was deployed.
// Source from which the action was deployed.
#[serde(default)]
#[serde(skip)]
pub deployed_from_indicator: Option<DisplayRow>,
pub deployed_from: Option<CodeActionSource>,
// Run first available task if there is only one.
#[serde(default)]
#[serde(skip)]
pub quick_launch: bool,
}
#[derive(PartialEq, Clone, Debug)]
pub enum CodeActionSource {
Indicator(DisplayRow),
QuickActionBar,
}
#[derive(PartialEq, Clone, Deserialize, Default, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct ConfirmCompletion {

View File

@@ -26,6 +26,7 @@ use task::ResolvedTask;
use ui::{Color, IntoElement, ListItem, Pixels, Popover, Styled, prelude::*};
use util::ResultExt;
use crate::CodeActionSource;
use crate::editor_settings::SnippetSortOrder;
use crate::hover_popover::{hover_markdown_style, open_markdown_url};
use crate::{
@@ -168,6 +169,7 @@ impl CodeContextMenu {
pub enum ContextMenuOrigin {
Cursor,
GutterIndicator(DisplayRow),
QuickActionBar,
}
#[derive(Clone, Debug)]
@@ -840,7 +842,7 @@ pub struct AvailableCodeAction {
}
#[derive(Clone)]
pub(crate) struct CodeActionContents {
pub struct CodeActionContents {
tasks: Option<Rc<ResolvedTasks>>,
actions: Option<Rc<[AvailableCodeAction]>>,
debug_scenarios: Vec<DebugScenario>,
@@ -968,12 +970,12 @@ impl CodeActionsItem {
}
}
pub(crate) struct CodeActionsMenu {
pub struct CodeActionsMenu {
pub actions: CodeActionContents,
pub buffer: Entity<Buffer>,
pub selected_item: usize,
pub scroll_handle: UniformListScrollHandle,
pub deployed_from_indicator: Option<DisplayRow>,
pub deployed_from: Option<CodeActionSource>,
}
impl CodeActionsMenu {
@@ -1042,10 +1044,10 @@ impl CodeActionsMenu {
}
fn origin(&self) -> ContextMenuOrigin {
if let Some(row) = self.deployed_from_indicator {
ContextMenuOrigin::GutterIndicator(row)
} else {
ContextMenuOrigin::Cursor
match &self.deployed_from {
Some(CodeActionSource::Indicator(row)) => ContextMenuOrigin::GutterIndicator(*row),
Some(CodeActionSource::QuickActionBar) => ContextMenuOrigin::QuickActionBar,
None => ContextMenuOrigin::Cursor,
}
}

View File

@@ -961,7 +961,7 @@ impl DisplaySnapshot {
if chunk.is_unnecessary {
diagnostic_highlight.fade_out = Some(editor_style.unnecessary_code_fade);
}
if editor_style.show_underlines {
if chunk.underline && editor_style.show_underlines {
let diagnostic_color = super::diagnostic_style(severity, &editor_style.status);
diagnostic_highlight.underline = Some(UnderlineStyle {
color: Some(diagnostic_color),

View File

@@ -1255,6 +1255,8 @@ pub struct Chunk<'a> {
pub diagnostic_severity: Option<lsp::DiagnosticSeverity>,
/// Whether this chunk of text is marked as unnecessary.
pub is_unnecessary: bool,
/// Whether this chunk of text should be underlined.
pub underline: bool,
/// Whether this chunk of text was originally a tab character.
pub is_tab: bool,
/// An optional recipe for how the chunk should be presented.
@@ -1422,6 +1424,7 @@ impl<'a> Iterator for FoldChunks<'a> {
diagnostic_severity: chunk.diagnostic_severity,
is_unnecessary: chunk.is_unnecessary,
is_tab: chunk.is_tab,
underline: chunk.underline,
renderer: None,
});
}

View File

@@ -15,7 +15,7 @@
pub mod actions;
mod blink_manager;
mod clangd_ext;
mod code_context_menus;
pub mod code_context_menus;
pub mod display_map;
mod editor_settings;
mod editor_settings_controls;
@@ -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::{
@@ -776,7 +777,7 @@ impl RunnableTasks {
}
#[derive(Clone)]
struct ResolvedTasks {
pub struct ResolvedTasks {
templates: SmallVec<[(TaskSourceKind, ResolvedTask); 1]>,
position: Anchor,
}
@@ -1071,6 +1072,7 @@ pub struct EditorSnapshot {
show_gutter: bool,
show_line_numbers: Option<bool>,
show_git_diff_gutter: Option<bool>,
show_code_actions: Option<bool>,
show_runnables: Option<bool>,
show_breakpoints: Option<bool>,
git_blame_gutter_max_author_length: Option<usize>,
@@ -2305,6 +2307,7 @@ impl Editor {
show_gutter: self.show_gutter,
show_line_numbers: self.show_line_numbers,
show_git_diff_gutter: self.show_git_diff_gutter,
show_code_actions: self.show_code_actions,
show_runnables: self.show_runnables,
show_breakpoints: self.show_breakpoints,
git_blame_gutter_max_author_length,
@@ -3964,15 +3967,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 {
@@ -4001,6 +4007,13 @@ impl Editor {
tab_size: len,
} = language.documentation()?;
let is_within_block_comment = buffer
.language_scope_at(start_point)
.is_some_and(|scope| scope.override_name() == Some("comment"));
if !is_within_block_comment {
return None;
}
let (snapshot, range) =
buffer.buffer_line_for_row(MultiBufferRow(start_point.row))?;
@@ -4009,6 +4022,8 @@ impl Editor {
.take_while(|c| c.is_whitespace())
.count();
// It is safe to use a column from MultiBufferPoint in context of a single buffer ranges, because we're only ever looking at a single line at a time.
let column = start_point.column;
let cursor_is_after_start_tag = {
let start_tag_len = start_tag.len();
let start_tag_line = snapshot
@@ -4017,8 +4032,7 @@ impl Editor {
.take(start_tag_len)
.collect::<String>();
if start_tag_line.starts_with(start_tag.as_ref()) {
num_of_whitespaces + start_tag_len
<= start_point.column as usize
num_of_whitespaces + start_tag_len <= column as usize
} else {
false
}
@@ -4032,39 +4046,35 @@ impl Editor {
.take(delimiter_trim.len())
.collect::<String>();
if delimiter_line.starts_with(delimiter_trim) {
num_of_whitespaces + delimiter_trim.len()
<= start_point.column as usize
num_of_whitespaces + delimiter_trim.len() <= column as usize
} else {
false
}
};
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 cursor_is_before_end_tag =
ordering != Ordering::Greater;
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 = 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;
column == end_tag_offset;
if cursor_is_at_start_of_end_tag {
indent_on_extra_newline.len = (*len).into();
}
@@ -5373,7 +5383,7 @@ impl Editor {
let quick_launch = action.quick_launch;
let mut context_menu = self.context_menu.borrow_mut();
if let Some(CodeContextMenu::CodeActions(code_actions)) = context_menu.as_ref() {
if code_actions.deployed_from_indicator == action.deployed_from_indicator {
if code_actions.deployed_from == action.deployed_from {
// Toggle if we're selecting the same one
*context_menu = None;
cx.notify();
@@ -5386,7 +5396,7 @@ impl Editor {
}
drop(context_menu);
let snapshot = self.snapshot(window, cx);
let deployed_from_indicator = action.deployed_from_indicator;
let deployed_from = action.deployed_from.clone();
let mut task = self.code_actions_task.take();
let action = action.clone();
cx.spawn_in(window, async move |editor, cx| {
@@ -5397,10 +5407,12 @@ impl Editor {
let spawned_test_task = editor.update_in(cx, |editor, window, cx| {
if editor.focus_handle.is_focused(window) {
let multibuffer_point = action
.deployed_from_indicator
.map(|row| DisplayPoint::new(row, 0).to_point(&snapshot))
.unwrap_or_else(|| editor.selections.newest::<Point>(cx).head());
let multibuffer_point = match &action.deployed_from {
Some(CodeActionSource::Indicator(row)) => {
DisplayPoint::new(*row, 0).to_point(&snapshot)
}
_ => editor.selections.newest::<Point>(cx).head(),
};
let (buffer, buffer_row) = snapshot
.buffer_snapshot
.buffer_line_for_row(MultiBufferRow(multibuffer_point.row))
@@ -5524,7 +5536,7 @@ impl Editor {
),
selected_item: Default::default(),
scroll_handle: UniformListScrollHandle::default(),
deployed_from_indicator,
deployed_from,
}));
if spawn_straight_away {
if let Some(task) = editor.confirm_code_action(
@@ -5616,6 +5628,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(())))
@@ -5743,6 +5756,68 @@ impl Editor {
self.refresh_code_actions(window, cx);
}
pub fn code_actions_enabled_for_toolbar(&self, cx: &App) -> bool {
!self.code_action_providers.is_empty()
&& EditorSettings::get_global(cx).toolbar.code_actions
}
pub fn has_available_code_actions(&self) -> bool {
self.available_code_actions
.as_ref()
.is_some_and(|(_, actions)| !actions.is_empty())
}
fn render_inline_code_actions(
&self,
icon_size: ui::IconSize,
display_row: DisplayRow,
is_active: bool,
cx: &mut Context<Self>,
) -> AnyElement {
let show_tooltip = !self.context_menu_visible();
IconButton::new("inline_code_actions", ui::IconName::BoltFilled)
.icon_size(icon_size)
.shape(ui::IconButtonShape::Square)
.style(ButtonStyle::Transparent)
.icon_color(ui::Color::Hidden)
.toggle_state(is_active)
.when(show_tooltip, |this| {
this.tooltip({
let focus_handle = self.focus_handle.clone();
move |window, cx| {
Tooltip::for_action_in(
"Toggle Code Actions",
&ToggleCodeActions {
deployed_from: None,
quick_launch: false,
},
&focus_handle,
window,
cx,
)
}
})
})
.on_click(cx.listener(move |editor, _: &ClickEvent, window, cx| {
window.focus(&editor.focus_handle(cx));
editor.toggle_code_actions(
&crate::actions::ToggleCodeActions {
deployed_from: Some(crate::actions::CodeActionSource::Indicator(
display_row,
)),
quick_launch: false,
},
window,
cx,
);
}))
.into_any_element()
}
pub fn context_menu(&self) -> &RefCell<Option<CodeContextMenu>> {
&self.context_menu
}
fn refresh_code_actions(&mut self, window: &mut Window, cx: &mut Context<Self>) -> Option<()> {
let newest_selection = self.selections.newest_anchor().clone();
let newest_selection_adjusted = self.selections.newest_adjusted(cx).clone();
@@ -7263,24 +7338,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 +7392,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,
)
})
}
@@ -7475,7 +7555,7 @@ impl Editor {
window.focus(&editor.focus_handle(cx));
editor.toggle_code_actions(
&ToggleCodeActions {
deployed_from_indicator: Some(row),
deployed_from: Some(CodeActionSource::Indicator(row)),
quick_launch,
},
window,
@@ -7496,7 +7576,7 @@ impl Editor {
.map_or(false, |menu| menu.visible())
}
fn context_menu_origin(&self) -> Option<ContextMenuOrigin> {
pub fn context_menu_origin(&self) -> Option<ContextMenuOrigin> {
self.context_menu
.borrow()
.as_ref()
@@ -8515,7 +8595,7 @@ impl Editor {
}
}
fn render_context_menu(
pub fn render_context_menu(
&self,
style: &EditorStyle,
max_height_in_lines: u32,
@@ -20236,6 +20316,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

@@ -47,6 +47,7 @@ pub struct EditorSettings {
pub snippet_sort_order: SnippetSortOrder,
#[serde(default)]
pub diagnostics_max_severity: Option<DiagnosticSeverity>,
pub inline_code_actions: bool,
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
@@ -109,6 +110,7 @@ pub struct Toolbar {
pub quick_actions: bool,
pub selections_menu: bool,
pub agent_review: bool,
pub code_actions: bool,
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
@@ -481,6 +483,11 @@ pub struct EditorSettingsContent {
/// Default: warning
#[serde(default)]
pub diagnostics_max_severity: Option<DiagnosticSeverity>,
/// Whether to show code action button at start of buffer line.
///
/// Default: true
pub inline_code_actions: Option<bool>,
}
// Toolbar related settings
@@ -503,6 +510,10 @@ pub struct ToolbarContent {
///
/// Default: true
pub agent_review: Option<bool>,
/// Whether to display code action buttons in the editor toolbar.
///
/// Default: false
pub code_actions: Option<bool>,
}
/// Scrollbar related settings

View File

@@ -2821,23 +2821,65 @@ async fn test_newline_comments(cx: &mut TestAppContext) {
}
#[gpui::test]
async fn test_newline_documentation_comments(cx: &mut TestAppContext) {
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 {
documentation: Some(language::DocumentationConfig {
start: "/**".into(),
end: "*/".into(),
prefix: "* ".into(),
tab_size: NonZeroU32::new(1).unwrap(),
}),
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| {
settings.defaults.tab_size = NonZeroU32::new(4)
});
let language = Arc::new(
Language::new(
LanguageConfig {
documentation: Some(language::DocumentationConfig {
start: "/**".into(),
end: "*/".into(),
prefix: "* ".into(),
tab_size: NonZeroU32::new(1).unwrap(),
}),
..LanguageConfig::default()
},
Some(tree_sitter_rust::LANGUAGE.into()),
)
.with_override_query("[(line_comment)(block_comment)] @comment.inclusive")
.unwrap(),
);
{
let mut cx = EditorTestContext::new(cx).await;
cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
@@ -2975,6 +3017,43 @@ 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 if not comment block it 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| {
@@ -14062,7 +14141,7 @@ async fn test_context_menus_hide_hover_popover(cx: &mut gpui::TestAppContext) {
cx.update_editor(|editor, window, cx| {
editor.toggle_code_actions(
&ToggleCodeActions {
deployed_from_indicator: None,
deployed_from: None,
quick_launch: false,
},
window,
@@ -16571,9 +16650,9 @@ fn indent_guide(buffer_id: BufferId, start_row: u32, end_row: u32, depth: u32) -
async fn test_indent_guide_single_line(cx: &mut TestAppContext) {
let (buffer_id, mut cx) = setup_indent_guides_editor(
&"
fn main() {
let a = 1;
}"
fn main() {
let a = 1;
}"
.unindent(),
cx,
)
@@ -16586,10 +16665,10 @@ async fn test_indent_guide_single_line(cx: &mut TestAppContext) {
async fn test_indent_guide_simple_block(cx: &mut TestAppContext) {
let (buffer_id, mut cx) = setup_indent_guides_editor(
&"
fn main() {
let a = 1;
let b = 2;
}"
fn main() {
let a = 1;
let b = 2;
}"
.unindent(),
cx,
)
@@ -16602,14 +16681,14 @@ async fn test_indent_guide_simple_block(cx: &mut TestAppContext) {
async fn test_indent_guide_nested(cx: &mut TestAppContext) {
let (buffer_id, mut cx) = setup_indent_guides_editor(
&"
fn main() {
let a = 1;
if a == 3 {
let b = 2;
} else {
let c = 3;
}
}"
fn main() {
let a = 1;
if a == 3 {
let b = 2;
} else {
let c = 3;
}
}"
.unindent(),
cx,
)
@@ -16631,11 +16710,11 @@ async fn test_indent_guide_nested(cx: &mut TestAppContext) {
async fn test_indent_guide_tab(cx: &mut TestAppContext) {
let (buffer_id, mut cx) = setup_indent_guides_editor(
&"
fn main() {
let a = 1;
let b = 2;
let c = 3;
}"
fn main() {
let a = 1;
let b = 2;
let c = 3;
}"
.unindent(),
cx,
)
@@ -16765,6 +16844,72 @@ async fn test_indent_guide_ends_off_screen(cx: &mut TestAppContext) {
);
}
#[gpui::test]
async fn test_indent_guide_with_folds(cx: &mut TestAppContext) {
let (buffer_id, mut cx) = setup_indent_guides_editor(
&"
fn main() {
if a {
b(
c,
d,
)
} else {
e(
f
)
}
}"
.unindent(),
cx,
)
.await;
assert_indent_guides(
0..11,
vec![
indent_guide(buffer_id, 1, 10, 0),
indent_guide(buffer_id, 2, 5, 1),
indent_guide(buffer_id, 7, 9, 1),
indent_guide(buffer_id, 3, 4, 2),
indent_guide(buffer_id, 8, 8, 2),
],
None,
&mut cx,
);
cx.update_editor(|editor, window, cx| {
editor.fold_at(MultiBufferRow(2), window, cx);
assert_eq!(
editor.display_text(cx),
"
fn main() {
if a {
b(⋯
)
} else {
e(
f
)
}
}"
.unindent()
);
});
assert_indent_guides(
0..11,
vec![
indent_guide(buffer_id, 1, 10, 0),
indent_guide(buffer_id, 2, 5, 1),
indent_guide(buffer_id, 7, 9, 1),
indent_guide(buffer_id, 8, 8, 2),
],
None,
&mut cx,
);
}
#[gpui::test]
async fn test_indent_guide_without_brackets(cx: &mut TestAppContext) {
let (buffer_id, mut cx) = setup_indent_guides_editor(
@@ -20662,7 +20807,7 @@ async fn test_indent_on_newline_for_python(cx: &mut TestAppContext) {
ˇ
"});
// test correct indent after newline in curly brackets
// test correct indent after newline in brackets
cx.set_state(indoc! {"
{ˇ}
"});
@@ -20675,6 +20820,32 @@ async fn test_indent_on_newline_for_python(cx: &mut TestAppContext) {
ˇ
}
"});
cx.set_state(indoc! {"
(ˇ)
"});
cx.update_editor(|editor, window, cx| {
editor.newline(&Newline, window, cx);
});
cx.run_until_parked();
cx.assert_editor_state(indoc! {"
(
ˇ
)
"});
// do not indent after empty lists or dictionaries
cx.set_state(indoc! {"
a = []ˇ
"});
cx.update_editor(|editor, window, cx| {
editor.newline(&Newline, window, cx);
});
cx.run_until_parked();
cx.assert_editor_state(indoc! {"
a = []
ˇ
"});
}
fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {

View File

@@ -1,12 +1,12 @@
use crate::{
ActiveDiagnostic, BlockId, COLUMNAR_SELECTION_MODIFIERS, CURSORS_VISIBLE_FOR,
ChunkRendererContext, ChunkReplacement, ConflictsOurs, ConflictsOursMarker, ConflictsOuter,
ConflictsTheirs, ConflictsTheirsMarker, ContextMenuPlacement, CursorShape, CustomBlockId,
DisplayDiffHunk, DisplayPoint, DisplayRow, DocumentHighlightRead, DocumentHighlightWrite,
EditDisplayMode, Editor, EditorMode, EditorSettings, EditorSnapshot, EditorStyle,
FILE_HEADER_HEIGHT, FocusedBlock, GutterDimensions, HalfPageDown, HalfPageUp, HandleInput,
HoveredCursor, InlayHintRefreshReason, InlineCompletion, JumpData, LineDown, LineHighlight,
LineUp, MAX_LINE_LEN, MIN_LINE_NUMBER_DIGITS, MINIMAP_FONT_SIZE,
ChunkRendererContext, ChunkReplacement, CodeActionSource, ConflictsOurs, ConflictsOursMarker,
ConflictsOuter, ConflictsTheirs, ConflictsTheirsMarker, ContextMenuPlacement, CursorShape,
CustomBlockId, DisplayDiffHunk, DisplayPoint, DisplayRow, DocumentHighlightRead,
DocumentHighlightWrite, EditDisplayMode, Editor, EditorMode, EditorSettings, EditorSnapshot,
EditorStyle, FILE_HEADER_HEIGHT, FocusedBlock, GutterDimensions, HalfPageDown, HalfPageUp,
HandleInput, HoveredCursor, InlayHintRefreshReason, InlineCompletion, JumpData, LineDown,
LineHighlight, LineUp, MAX_LINE_LEN, MIN_LINE_NUMBER_DIGITS, MINIMAP_FONT_SIZE,
MULTI_BUFFER_EXCERPT_HEADER_HEIGHT, OpenExcerpts, PageDown, PageUp, PhantomBreakpointIndicator,
Point, RowExt, RowRangeExt, SelectPhase, SelectedTextHighlight, Selection, SoftWrap,
StickyHeaderExcerpt, ToPoint, ToggleFold,
@@ -1930,6 +1930,159 @@ impl EditorElement {
elements
}
fn layout_inline_code_actions(
&self,
display_point: DisplayPoint,
content_origin: gpui::Point<Pixels>,
scroll_pixel_position: gpui::Point<Pixels>,
line_height: Pixels,
snapshot: &EditorSnapshot,
window: &mut Window,
cx: &mut App,
) -> Option<AnyElement> {
if !snapshot
.show_code_actions
.unwrap_or(EditorSettings::get_global(cx).inline_code_actions)
{
return None;
}
let icon_size = ui::IconSize::XSmall;
let mut button = self.editor.update(cx, |editor, cx| {
editor.available_code_actions.as_ref()?;
let active = editor
.context_menu
.borrow()
.as_ref()
.and_then(|menu| {
if let crate::CodeContextMenu::CodeActions(CodeActionsMenu {
deployed_from,
..
}) = menu
{
deployed_from.as_ref()
} else {
None
}
})
.map_or(false, |source| {
matches!(source, CodeActionSource::Indicator(..))
});
Some(editor.render_inline_code_actions(icon_size, display_point.row(), active, cx))
})?;
let buffer_point = display_point.to_point(&snapshot.display_snapshot);
// do not show code action for folded line
if snapshot.is_line_folded(MultiBufferRow(buffer_point.row)) {
return None;
}
// do not show code action for blank line with cursor
let line_indent = snapshot
.display_snapshot
.buffer_snapshot
.line_indent_for_row(MultiBufferRow(buffer_point.row));
if line_indent.is_line_blank() {
return None;
}
const INLINE_SLOT_CHAR_LIMIT: u32 = 4;
const MAX_ALTERNATE_DISTANCE: u32 = 8;
let excerpt_id = snapshot
.display_snapshot
.buffer_snapshot
.excerpt_containing(buffer_point..buffer_point)
.map(|excerpt| excerpt.id());
let is_valid_row = |row_candidate: u32| -> bool {
// move to other row if folded row
if snapshot.is_line_folded(MultiBufferRow(row_candidate)) {
return false;
}
if buffer_point.row == row_candidate {
// move to other row if cursor is in slot
if buffer_point.column < INLINE_SLOT_CHAR_LIMIT {
return false;
}
} else {
let candidate_point = MultiBufferPoint {
row: row_candidate,
column: 0,
};
let candidate_excerpt_id = snapshot
.display_snapshot
.buffer_snapshot
.excerpt_containing(candidate_point..candidate_point)
.map(|excerpt| excerpt.id());
// move to other row if different excerpt
if excerpt_id != candidate_excerpt_id {
return false;
}
}
let line_indent = snapshot
.display_snapshot
.buffer_snapshot
.line_indent_for_row(MultiBufferRow(row_candidate));
// use this row if it's blank
if line_indent.is_line_blank() {
true
} else {
// use this row if code starts after slot
let indent_size = snapshot
.display_snapshot
.buffer_snapshot
.indent_size_for_line(MultiBufferRow(row_candidate));
indent_size.len >= INLINE_SLOT_CHAR_LIMIT
}
};
let new_buffer_row = if is_valid_row(buffer_point.row) {
Some(buffer_point.row)
} else {
let max_row = snapshot.display_snapshot.buffer_snapshot.max_point().row;
(1..=MAX_ALTERNATE_DISTANCE).find_map(|offset| {
let row_above = buffer_point.row.saturating_sub(offset);
let row_below = buffer_point.row + offset;
if row_above != buffer_point.row && is_valid_row(row_above) {
Some(row_above)
} else if row_below <= max_row && is_valid_row(row_below) {
Some(row_below)
} else {
None
}
})
}?;
let new_display_row = snapshot
.display_snapshot
.point_to_display_point(
Point {
row: new_buffer_row,
column: buffer_point.column,
},
text::Bias::Left,
)
.row();
let start_y = content_origin.y
+ ((new_display_row.as_f32() - (scroll_pixel_position.y / line_height)) * line_height)
+ (line_height / 2.0)
- (icon_size.square(window, cx) / 2.);
let start_x = content_origin.x - scroll_pixel_position.x + (window.rem_size() * 0.1);
let absolute_offset = gpui::point(start_x, start_y);
button.layout_as_root(gpui::AvailableSpace::min_size(), window, cx);
button.prepaint_as_root(
absolute_offset,
gpui::AvailableSpace::min_size(),
window,
cx,
);
Some(button)
}
fn layout_inline_blame(
&self,
display_row: DisplayRow,
@@ -2385,7 +2538,7 @@ impl EditorElement {
self.editor.update(cx, |editor, cx| {
let active_task_indicator_row =
if let Some(crate::CodeContextMenu::CodeActions(CodeActionsMenu {
deployed_from_indicator,
deployed_from,
actions,
..
})) = editor.context_menu.borrow().as_ref()
@@ -2393,7 +2546,10 @@ impl EditorElement {
actions
.tasks()
.map(|tasks| tasks.position.to_display_point(snapshot).row())
.or(*deployed_from_indicator)
.or_else(|| match deployed_from {
Some(CodeActionSource::Indicator(row)) => Some(*row),
_ => None,
})
} else {
None
};
@@ -5294,6 +5450,7 @@ impl EditorElement {
self.paint_cursors(layout, window, cx);
self.paint_inline_diagnostics(layout, window, cx);
self.paint_inline_blame(layout, window, cx);
self.paint_inline_code_actions(layout, window, cx);
self.paint_diff_hunk_controls(layout, window, cx);
window.with_element_namespace("crease_trailers", |window| {
for trailer in layout.crease_trailers.iter_mut().flatten() {
@@ -5917,6 +6074,19 @@ impl EditorElement {
}
}
fn paint_inline_code_actions(
&mut self,
layout: &mut EditorLayout,
window: &mut Window,
cx: &mut App,
) {
if let Some(mut inline_code_actions) = layout.inline_code_actions.take() {
window.paint_layer(layout.position_map.text_hitbox.bounds, |window| {
inline_code_actions.paint(window, cx);
})
}
}
fn paint_diff_hunk_controls(
&mut self,
layout: &mut EditorLayout,
@@ -7966,15 +8136,27 @@ impl Element for EditorElement {
);
let mut inline_blame = None;
let mut inline_code_actions = None;
if let Some(newest_selection_head) = newest_selection_head {
let display_row = newest_selection_head.row();
if (start_row..end_row).contains(&display_row)
&& !row_block_types.contains_key(&display_row)
{
inline_code_actions = self.layout_inline_code_actions(
newest_selection_head,
content_origin,
scroll_pixel_position,
line_height,
&snapshot,
window,
cx,
);
let line_ix = display_row.minus(start_row) as usize;
let row_info = &row_infos[line_ix];
let line_layout = &line_layouts[line_ix];
let crease_trailer_layout = crease_trailers[line_ix].as_ref();
inline_blame = self.layout_inline_blame(
display_row,
row_info,
@@ -8318,6 +8500,7 @@ impl Element for EditorElement {
blamed_display_rows,
inline_diagnostics,
inline_blame,
inline_code_actions,
blocks,
cursors,
visible_cursors,
@@ -8497,6 +8680,7 @@ pub struct EditorLayout {
blamed_display_rows: Option<Vec<AnyElement>>,
inline_diagnostics: HashMap<DisplayRow, AnyElement>,
inline_blame: Option<AnyElement>,
inline_code_actions: Option<AnyElement>,
blocks: Vec<BlockLayout>,
highlighted_ranges: Vec<(Range<DisplayPoint>, Hsla)>,
highlighted_gutter_ranges: Vec<(Range<DisplayPoint>, Hsla)>,

View File

@@ -1,9 +1,9 @@
use std::{ops::Range, time::Duration};
use std::{cmp::Ordering, ops::Range, time::Duration};
use collections::HashSet;
use gpui::{App, AppContext as _, Context, Task, Window};
use language::language_settings::language_settings;
use multi_buffer::{IndentGuide, MultiBufferRow};
use multi_buffer::{IndentGuide, MultiBufferRow, ToPoint};
use text::{LineIndent, Point};
use util::ResultExt;
@@ -154,12 +154,28 @@ pub fn indent_guides_in_range(
snapshot: &DisplaySnapshot,
cx: &App,
) -> Vec<IndentGuide> {
let start_anchor = snapshot
let start_offset = snapshot
.buffer_snapshot
.anchor_before(Point::new(visible_buffer_range.start.0, 0));
let end_anchor = snapshot
.point_to_offset(Point::new(visible_buffer_range.start.0, 0));
let end_offset = snapshot
.buffer_snapshot
.anchor_after(Point::new(visible_buffer_range.end.0, 0));
.point_to_offset(Point::new(visible_buffer_range.end.0, 0));
let start_anchor = snapshot.buffer_snapshot.anchor_before(start_offset);
let end_anchor = snapshot.buffer_snapshot.anchor_after(end_offset);
let mut fold_ranges = Vec::<Range<Point>>::new();
let mut folds = snapshot.folds_in_range(start_offset..end_offset).peekable();
while let Some(fold) = folds.next() {
let start = fold.range.start.to_point(&snapshot.buffer_snapshot);
let end = fold.range.end.to_point(&snapshot.buffer_snapshot);
if let Some(last_range) = fold_ranges.last_mut() {
if last_range.end >= start {
last_range.end = last_range.end.max(end);
continue;
}
}
fold_ranges.push(start..end);
}
snapshot
.buffer_snapshot
@@ -169,15 +185,19 @@ pub fn indent_guides_in_range(
return false;
}
let start = MultiBufferRow(indent_guide.start_row.0.saturating_sub(1));
// Filter out indent guides that are inside a fold
// All indent guides that are starting "offscreen" have a start value of the first visible row minus one
// Therefore checking if a line is folded at first visible row minus one causes the other indent guides that are not related to the fold to disappear as well
let is_folded = snapshot.is_line_folded(start);
let line_indent = snapshot.line_indent_for_buffer_row(start);
let contained_in_fold =
line_indent.len(indent_guide.tab_size) <= indent_guide.indent_level();
!(is_folded && contained_in_fold)
let has_containing_fold = fold_ranges
.binary_search_by(|fold_range| {
if fold_range.start >= Point::new(indent_guide.start_row.0, 0) {
Ordering::Greater
} else if fold_range.end < Point::new(indent_guide.end_row.0, 0) {
Ordering::Less
} else {
Ordering::Equal
}
})
.is_ok();
!has_containing_fold
})
.collect()
}

View File

@@ -226,7 +226,7 @@ pub fn deploy_context_menu(
.action(
"Show Code Actions",
Box::new(ToggleCodeActions {
deployed_from_indicator: None,
deployed_from: None,
quick_launch: false,
}),
)

View File

@@ -67,3 +67,4 @@ unindent.workspace = true
util.workspace = true
uuid.workspace = true
workspace-hack.workspace = true
zed_llm_client.workspace = true

View File

@@ -19,6 +19,7 @@ use collections::HashMap;
use futures::{FutureExt as _, StreamExt, channel::mpsc, select_biased};
use gpui::{App, AppContext, AsyncApp, Entity};
use language_model::{LanguageModel, Role, StopReason};
use zed_llm_client::CompletionIntent;
pub const THREAD_EVENT_TIMEOUT: Duration = Duration::from_secs(60 * 2);
@@ -231,6 +232,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();
}
@@ -303,7 +308,7 @@ impl ExampleContext {
let message_count_before = self.app.update_entity(&self.agent_thread, |thread, cx| {
thread.set_remaining_turns(iterations);
thread.send_to_model(model, None, cx);
thread.send_to_model(model, CompletionIntent::UserPrompt, None, cx);
thread.messages().len()
})?;

View File

@@ -576,6 +576,7 @@ impl ExampleInstance {
thread_id: None,
prompt_id: None,
mode: None,
intent: None,
messages: vec![LanguageModelRequestMessage {
role: Role::User,
content: vec![MessageContent::Text(to_prompt(assertion.description))],

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

@@ -398,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 {
@@ -710,100 +724,3 @@ impl CacheStore for IncrementalCompilationCache {
true
}
}
#[cfg(test)]
mod tests {
use std::collections::BTreeMap;
use extension::{
ExtensionCapability, ExtensionLibraryKind, LanguageServerManifestEntry, LibManifestEntry,
SchemaVersion,
extension_builder::{CompileExtensionOptions, ExtensionBuilder},
};
use gpui::TestAppContext;
use reqwest_client::ReqwestClient;
use super::*;
#[gpui::test]
fn test_cache_size_for_test_extension(cx: &TestAppContext) {
let cache_store = cache_store();
let engine = wasm_engine();
let wasm_bytes = wasm_bytes(cx, &mut manifest());
Component::new(&engine, wasm_bytes).unwrap();
cache_store.cache.run_pending_tasks();
let size: usize = cache_store
.cache
.iter()
.map(|(k, v)| k.len() + v.len())
.sum();
// If this assertion fails, it means extensions got larger and we may want to
// reconsider our cache size.
assert!(size < 512 * 1024);
}
fn wasm_bytes(cx: &TestAppContext, manifest: &mut ExtensionManifest) -> Vec<u8> {
let extension_builder = extension_builder();
let path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.parent()
.unwrap()
.parent()
.unwrap()
.join("extensions/test-extension");
cx.executor()
.block(extension_builder.compile_extension(
&path,
manifest,
CompileExtensionOptions { release: true },
))
.unwrap();
std::fs::read(path.join("extension.wasm")).unwrap()
}
fn extension_builder() -> ExtensionBuilder {
let user_agent = format!(
"Zed Extension CLI/{} ({}; {})",
env!("CARGO_PKG_VERSION"),
std::env::consts::OS,
std::env::consts::ARCH
);
let http_client = Arc::new(ReqwestClient::user_agent(&user_agent).unwrap());
// Local dir so that we don't have to download it on every run
let build_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("benches/.build");
ExtensionBuilder::new(http_client, build_dir)
}
fn manifest() -> ExtensionManifest {
ExtensionManifest {
id: "test-extension".into(),
name: "Test Extension".into(),
version: "0.1.0".into(),
schema_version: SchemaVersion(1),
description: Some("An extension for use in tests.".into()),
authors: Vec::new(),
repository: None,
themes: Default::default(),
icon_themes: Vec::new(),
lib: LibManifestEntry {
kind: Some(ExtensionLibraryKind::Rust),
version: Some(SemanticVersion::new(0, 1, 0)),
},
languages: Vec::new(),
grammars: BTreeMap::default(),
language_servers: [("gleam".into(), LanguageServerManifestEntry::default())]
.into_iter()
.collect(),
context_servers: BTreeMap::default(),
slash_commands: BTreeMap::default(),
indexed_docs_providers: BTreeMap::default(),
snippets: None,
capabilities: vec![ExtensionCapability::ProcessExec {
command: "echo".into(),
args: vec!["hello!".into()],
}],
debug_adapters: Vec::new(),
}
}
}

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> {

View File

@@ -15,6 +15,7 @@ use std::{
path::{Path, PathBuf},
sync::{Arc, OnceLock},
};
use util::archive::extract_zip;
use util::maybe;
use wasmtime::component::{Linker, Resource};
@@ -543,9 +544,9 @@ impl ExtensionImports for WasmState {
}
DownloadedFileType::Zip => {
futures::pin_mut!(body);
node_runtime::extract_zip(&destination_path, body)
extract_zip(&destination_path, body)
.await
.with_context(|| format!("failed to unzip {} archive", path.display()))?;
.with_context(|| format!("unzipping {path:?} archive"))?;
}
}

View File

@@ -1,7 +1,7 @@
use crate::wasm_host::wit::since_v0_6_0::{
dap::{
AttachRequest, DebugRequest, LaunchRequest, StartDebuggingRequestArguments,
StartDebuggingRequestArgumentsRequest, TcpArguments, TcpArgumentsTemplate,
StartDebuggingRequestArguments, StartDebuggingRequestArgumentsRequest, TcpArguments,
TcpArgumentsTemplate,
},
slash_command::SlashCommandOutputSection,
};
@@ -27,7 +27,7 @@ use std::{
path::{Path, PathBuf},
sync::{Arc, OnceLock},
};
use util::maybe;
use util::{archive::extract_zip, maybe};
use wasmtime::component::{Linker, Resource};
pub const MIN_VERSION: SemanticVersion = SemanticVersion::new(0, 6, 0);
@@ -79,17 +79,6 @@ impl From<Command> for extension::Command {
}
}
impl From<extension::LaunchRequest> for LaunchRequest {
fn from(value: extension::LaunchRequest) -> Self {
Self {
program: value.program,
cwd: value.cwd.map(|path| path.to_string_lossy().into_owned()),
envs: value.env.into_iter().collect(),
args: value.args,
}
}
}
impl From<StartDebuggingRequestArgumentsRequest>
for extension::StartDebuggingRequestArgumentsRequest
{
@@ -129,32 +118,14 @@ impl From<extension::TcpArgumentsTemplate> for TcpArgumentsTemplate {
}
}
}
impl From<extension::AttachRequest> for AttachRequest {
fn from(value: extension::AttachRequest) -> Self {
Self {
process_id: value.process_id,
}
}
}
impl From<extension::DebugRequest> for DebugRequest {
fn from(value: extension::DebugRequest) -> Self {
match value {
extension::DebugRequest::Launch(launch_request) => Self::Launch(launch_request.into()),
extension::DebugRequest::Attach(attach_request) => Self::Attach(attach_request.into()),
}
}
}
impl TryFrom<extension::DebugTaskDefinition> for DebugTaskDefinition {
type Error = anyhow::Error;
fn try_from(value: extension::DebugTaskDefinition) -> Result<Self, Self::Error> {
let initialize_args = value.initialize_args.map(|s| s.to_string());
Ok(Self {
label: value.label.to_string(),
adapter: value.adapter.to_string(),
request: value.request.into(),
initialize_args,
stop_on_entry: value.stop_on_entry,
config: value.config.to_string(),
tcp_connection: value.tcp_connection.map(Into::into),
})
}
@@ -906,9 +877,9 @@ impl ExtensionImports for WasmState {
}
DownloadedFileType::Zip => {
futures::pin_mut!(body);
node_runtime::extract_zip(&destination_path, body)
extract_zip(&destination_path, body)
.await
.with_context(|| format!("failed to unzip {} archive", path.display()))?;
.with_context(|| format!("unzipping {path:?} archive"))?;
}
}

View File

@@ -59,6 +59,7 @@ util.workspace = true
workspace.workspace = true
zed_actions.workspace = true
workspace-hack.workspace = true
zed_llm_client.workspace = true
[target.'cfg(windows)'.dependencies]
windows.workspace = true

View File

@@ -13,7 +13,6 @@ use anyhow::Context as _;
use askpass::AskPassDelegate;
use assistant_settings::AssistantSettings;
use db::kvp::KEY_VALUE_STORE;
use editor::{
Editor, EditorElement, EditorMode, EditorSettings, MultiBuffer, ShowScrollbar,
scroll::ScrollbarAutoHide,
@@ -42,6 +41,7 @@ use language_model::{
};
use menu::{Confirm, SecondaryConfirm, SelectFirst, SelectLast, SelectNext, SelectPrevious};
use multi_buffer::ExcerptInfo;
use notifications::status_toast::{StatusToast, ToastIcon};
use panel::{
PanelHeader, panel_button, panel_editor_container, panel_editor_style, panel_filled_button,
panel_icon_button,
@@ -64,13 +64,12 @@ use ui::{
};
use util::{ResultExt, TryFutureExt, maybe};
use workspace::AppState;
use notifications::status_toast::{StatusToast, ToastIcon};
use workspace::{
Workspace,
dock::{DockPosition, Panel, PanelEvent},
notifications::DetachAndPromptErr,
};
use zed_llm_client::CompletionIntent;
actions!(
git_panel,
@@ -1765,6 +1764,7 @@ impl GitPanel {
let request = LanguageModelRequest {
thread_id: None,
prompt_id: None,
intent: Some(CompletionIntent::GenerateGitCommitMessage),
mode: None,
messages: vec![LanguageModelRequestMessage {
role: Role::User,

View File

@@ -41,6 +41,7 @@ pub enum IconName {
Binary,
Blocks,
Bolt,
BoltFilled,
Book,
BookCopy,
BookPlus,

View File

@@ -66,6 +66,7 @@ tree-sitter.workspace = true
unicase = "2.6"
util.workspace = true
workspace-hack.workspace = true
diffy = "0.4.2"
[dev-dependencies]
collections = { workspace = true, features = ["test-support"] }

View File

@@ -231,6 +231,8 @@ pub struct Diagnostic {
pub is_unnecessary: bool,
/// Data from language server that produced this diagnostic. Passed back to the LS when we request code actions for this diagnostic.
pub data: Option<Value>,
/// Whether to underline the corresponding text range in the editor.
pub underline: bool,
}
/// An operation used to synchronize this buffer with its other replicas.
@@ -462,6 +464,7 @@ pub struct BufferChunks<'a> {
information_depth: usize,
hint_depth: usize,
unnecessary_depth: usize,
underline: bool,
highlights: Option<BufferChunkHighlights<'a>>,
}
@@ -482,6 +485,8 @@ pub struct Chunk<'a> {
pub is_unnecessary: bool,
/// Whether this chunk of text was originally a tab character.
pub is_tab: bool,
/// Whether to underline the corresponding text range in the editor.
pub underline: bool,
}
/// A set of edits to a given version of a buffer, computed asynchronously.
@@ -492,10 +497,11 @@ pub struct Diff {
pub edits: Vec<(Range<usize>, Arc<str>)>,
}
#[derive(Clone, Copy)]
#[derive(Debug, Clone, Copy)]
pub(crate) struct DiagnosticEndpoint {
offset: usize,
is_start: bool,
underline: bool,
severity: DiagnosticSeverity,
is_unnecessary: bool,
}
@@ -2901,7 +2907,7 @@ impl BufferSnapshot {
end
};
if let Some((start, end)) = start.zip(end) {
if start.row == end.row && !significant_indentation {
if start.row == end.row && (!significant_indentation || start.column < end.column) {
continue;
}
let range = start..end;
@@ -4388,6 +4394,7 @@ impl<'a> BufferChunks<'a> {
information_depth: 0,
hint_depth: 0,
unnecessary_depth: 0,
underline: true,
highlights,
};
this.initialize_diagnostic_endpoints();
@@ -4448,12 +4455,14 @@ impl<'a> BufferChunks<'a> {
is_start: true,
severity: entry.diagnostic.severity,
is_unnecessary: entry.diagnostic.is_unnecessary,
underline: entry.diagnostic.underline,
});
diagnostic_endpoints.push(DiagnosticEndpoint {
offset: entry.range.end,
is_start: false,
severity: entry.diagnostic.severity,
is_unnecessary: entry.diagnostic.is_unnecessary,
underline: entry.diagnostic.underline,
});
}
diagnostic_endpoints
@@ -4559,6 +4568,7 @@ impl<'a> Iterator for BufferChunks<'a> {
if endpoint.offset <= self.range.start {
self.update_diagnostic_depths(endpoint);
diagnostic_endpoints.next();
self.underline = endpoint.underline;
} else {
next_diagnostic_endpoint = endpoint.offset;
break;
@@ -4590,9 +4600,10 @@ impl<'a> Iterator for BufferChunks<'a> {
Some(Chunk {
text: slice,
syntax_highlight_id: highlight_id,
underline: self.underline,
diagnostic_severity: self.current_diagnostic_severity(),
is_unnecessary: self.current_code_is_unnecessary(),
..Default::default()
..Chunk::default()
})
} else {
None
@@ -4632,6 +4643,7 @@ impl Default for Diagnostic {
is_primary: false,
is_disk_based: false,
is_unnecessary: false,
underline: true,
data: None,
}
}

View File

@@ -65,7 +65,9 @@ use std::{num::NonZeroU32, sync::OnceLock};
use syntax_map::{QueryCursorHandle, SyntaxSnapshot};
use task::RunnableTag;
pub use task_context::{ContextProvider, RunnableRange};
pub use text_diff::{DiffOptions, line_diff, text_diff, text_diff_with_options, unified_diff};
pub use text_diff::{
DiffOptions, apply_diff_patch, line_diff, text_diff, text_diff_with_options, unified_diff,
};
use theme::SyntaxTheme;
pub use toolchain::{LanguageToolchainStore, Toolchain, ToolchainList, ToolchainLister};
use tree_sitter::{self, Query, QueryCursor, WasmStore, wasmtime};
@@ -243,6 +245,10 @@ impl CachedLspAdapter {
self.adapter.retain_old_diagnostic(previous_diagnostic, cx)
}
pub fn underline_diagnostic(&self, diagnostic: &lsp::Diagnostic) -> bool {
self.adapter.underline_diagnostic(diagnostic)
}
pub fn diagnostic_message_to_markdown(&self, message: &str) -> Option<String> {
self.adapter.diagnostic_message_to_markdown(message)
}
@@ -468,6 +474,16 @@ pub trait LspAdapter: 'static + Send + Sync {
false
}
/// Whether to underline a given diagnostic or not, when rendering in the editor.
///
/// https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#diagnosticTag
/// states that
/// > Clients are allowed to render diagnostics with this tag faded out instead of having an error squiggle.
/// for the unnecessary diagnostics, so do not underline them.
fn underline_diagnostic(&self, _diagnostic: &lsp::Diagnostic) -> bool {
true
}
/// Post-processes completions provided by the language server.
async fn process_completions(&self, _: &mut [lsp::CompletionItem]) {}

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