Compare commits

..

118 Commits

Author SHA1 Message Date
Conrad Irwin
0683f3cfbc +Codex 2025-07-16 20:33:52 -06:00
Anthony Eid
c0261a1ea9 keymap ui: Fix keymap editor search bugs (#34579)
Keystroke input now gets cleared when toggling to normal search mode
Main search bar is focused when toggling to normal search mode

This also gets rid of highlight on focus from keystroke_editor because
it also matched the search bool field and was redundant

Release Notes:

- N/A
2025-07-16 18:05:26 -04:00
Marshall Bowers
f43bcc1492 collab: Remove GET /billing/subscriptions endpoint (#34580)
This PR removes the `GET /billing/subscriptions` endpoint, as it has
been moved to `cloud.zed.dev`.

Release Notes:

- N/A
2025-07-16 22:04:53 +00:00
Umesh Yadav
e23a4564cc keymap_ui: Open Keymap editor from settings dropdown (#34576)
@probably-neb I guess we should be opening the keymap editor from title
bar and menu as well. I believe this got missed in this: #34568.

Release Notes:

- Open Keymap editor from settings from menu and title bar.
2025-07-16 17:30:08 -04:00
Peter Tripp
f82ef1f76f agent: Support GEMINI_API_KEY environment variable (#34574)
Google Gemini Docs now recommend usage of `GEMINI_API_KEY` and the
legacy `GOOGLE_AI_API_KEY` variable is no longer supported in the modern
SDKs.

Zed will now accept either.

Release Notes:

- N/A
2025-07-16 20:55:54 +00:00
Richard Feldman
b4c2ae5196 Handle upstream_http_error completion responses (#34573)
Addresses upstream errors such as:
<img width="831" height="100" alt="Screenshot 2025-07-16 at 3 37 03 PM"
src="https://github.com/user-attachments/assets/2aeb0257-6761-4148-b687-25fae93c68d8"
/>

These should now automatically retry like other upstream HTTP error
codes.

Release Notes:

- N/A
2025-07-16 16:31:31 -04:00
Peter Tripp
0023773c68 docs: Add Zed as Git Editor example (#34572)
Release Notes:

- N/A
2025-07-16 19:57:02 +00:00
Anthony Eid
0bde929d54 Add keymap editor UI telemetry events (#34571)
- Search queries
- Keybinding update or removed
- Copy action name
- Copy context name

cc @katie-z-geer 

Release Notes:

- N/A

Co-authored-by: Ben Kunkle <ben@zed.dev>
2025-07-16 19:50:53 +00:00
Joseph T. Lyons
6f60939d30 Bump Zed to v0.197 (#34569)
Release Notes:

-N/A
2025-07-16 18:48:50 +00:00
Ben Kunkle
a6a7a1cc28 keymap_ui: Remove feature flag (#34568)
Closes #ISSUE

Release Notes:

- Rebound the keystroke to open the keymap file, to open the new keymap
editor
2025-07-16 18:28:44 +00:00
Anthony Eid
13f4a093c8 Improve keystroke search in keymap editor (#34567)
This PR improves Keystroke search by:

1.  Allow searching by modifiers without additional keys.
2. Take match count into consideration when deciding if we should show
an action as a search match.
3. Take order into consideration as well.

Release Notes:

- N/A

---------

Co-authored-by: Ben Kunkle <ben@zed.dev>
2025-07-16 18:19:41 +00:00
Ben Kunkle
573836a654 keymap_ui: Replace zed::NoAction with null (#34562)
Closes #ISSUE

This change applies both to the UI (we render `<null>` as muted text
instead of `zed::NoAction`) as well as how we update the keymap file
(the duplicated binding is bound to `null` instead of `"zed::NoAction"`)

Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-07-16 17:55:58 +00:00
Marshall Bowers
048dc47d87 collab: Remove GET /billing/preferences endpoint (#34566)
This PR removes the `GET /billing/preferences` endpoint, as it has been
moved to `cloud.zed.dev`.

Release Notes:

- N/A
2025-07-16 17:55:01 +00:00
Smit Barmase
ffc69b07e5 editor: Fix sometimes green (+) cursor style appearing when cmd-clicking to navigate and back (#34557)
Regressed in https://github.com/zed-industries/zed/pull/33928

This PR clears the selection drag state when the editor focus is out.

To reproduce: 

1. Select some item in buffer that has a go to definition.
2. Cmd+Click mouse down on it, but don't let go.
3. Wait for 300ms+. 
4. Now cursor changed to green + (valid state, this is for selection
drag-n-drop).
5. Now let go of your mouse down, we switched to a different file.
Cursor looks normal.
6. Come back to the previous buffer, see green + cursor style (BUG!).

Release Notes:

- Fixed the issue where the green (+) cursor style sometimes appears
when navigating to the definition and then back to the previous buffer.
2025-07-16 23:24:02 +05:30
Piotr Osiewicz
dc8d0868ec project: Fix up documentation for Path Trie and add a test for having multiple present nodes (#34560)
cc @cole-miller I was worried with
https://github.com/zed-industries/zed/pull/34460#discussion_r2210814806
that PathTrie would not be able to support nested .git repositories, but
it seems fine.

Release Notes:

- N/A
2025-07-16 17:24:34 +00:00
Ben Kunkle
58807f0dd2 keymap_ui: Create language for Zed keybind context (#34558)
Closes #ISSUE

Creates a new language in the languages crate for the DSL used in Zed
keybinding context. Previously, keybind context was highlighted as Rust
in the keymap UI due to the expression syntax of Rust matching that of
the context DSL, however, this had the side effect of highlighting upper
case contexts (e.g. `Editor`) however Rust types would be highlighted
based on the theme. By extracting only the necessary pieces of the Rust
language `highlights.scm`, `brackets.scm`, and `config.toml`, and
continuing to use the Rust grammar, we get a better result across
different themes

Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-07-16 17:00:47 +00:00
Adam
313f5968eb Improve the read_file tool prompt for long files (#34542)
Closes [#ISSUE](https://github.com/zed-industries/zed/issues/31780)

Release Notes:

- Enhanced `read_file` tool call result message for long files.
2025-07-16 16:32:58 +00:00
Anthony Eid
9ab3d55211 Add exact matching option to keymap editor search (#34497)
We know have the ability to filter matches in the keymap editor search
by exact keystroke matches. This allows user's to have the same behavior
as vscode when they toggle all actions with the same bindings

We also fixed a bug where conflicts weren't counted correctly when
saving a keymapping. This cause issues where warnings wouldn't appear
when they were supposed to.

Release Notes:

- N/A

---------

Co-authored-by: Ben Kunkle <ben@zed.dev>
2025-07-16 12:14:09 -04:00
Oleksiy Syvokon
e339566dab agent: Limit the size of patches generated from user edits (#34548)
Gradually remove details from a patch to keep it within the size limit.
This helps avoid using too much context when the user pastes large
files, generates files, or just makes many changes between agent
notifications.

Release Notes:

- N/A
2025-07-16 15:46:13 +00:00
Umesh Yadav
8ee5bf2c38 open_router: Fix tool_choice getting serialized to null (#34532)
Closes #34314

This PR resolves an issue where serde(untagged) caused Rust None values
to serialize as null, which OpenRouter's Mistral API (when tool_choice
is present) incorrectly interprets as a defined value, leading to a 400
error. By replacing serde(untagged) with serde(snake_case), None values
are now correctly omitted from the serialized JSON, fixing the problem.
P.S. A separate PR will address serde(untagged) usage for other
providers, as null is not expected for them either.

Release Notes:

- Fix ToolChoice getting serialized to null on OpenRouter
2025-07-16 11:44:08 -04:00
Marshall Bowers
b0e0485b32 docs: Add redirects for language pages (#34544)
This PR adds some more docs redirects for language pages.

Release Notes:

- N/A
2025-07-16 14:50:54 +00:00
Danilo Leal
2a49f40cf5 docs: Add some improvements to the agent panel page (#34543)
Release Notes:

- N/A
2025-07-16 11:49:53 -03:00
Ben Kunkle
21b4a2ecdd keymap_ui: Infer use key equivalents (#34498)
Closes #ISSUE

This PR attempts to add workarounds for `use_key_equivalents` in the
keymap UI. First of all it makes it so that `use_key_equivalents` is
ignored when searching for a binding to replace so that replacing a
keybind with `use_key_equivalents` set to true does not result in a new
binding. Second, it attempts to infer the value of `use_key_equivalents`
off of a base binding when adding a binding by adding an optional `from`
parameter to the `KeymapUpdateOperation::Add` variant. Neither
workaround will work when the `from` binding for an add or the `target`
binding for a replace are not in the user keymap.

cc: @Anthony-Eid 

Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-07-16 10:49:16 -04:00
Peter Tripp
2a9a82d757 macos: Add mappings for alt-delete and cmd-delete (#34493)
Closes https://github.com/zed-industries/zed/issues/34484

Release Notes:

- macos: Add default mappings for `alt-delete` and `cmd-delete` in
Terminal (delete word to right; delete to end of line)
2025-07-16 10:45:34 -04:00
Marshall Bowers
6e147b3b91 docs: Organize redirects (#34541)
This PR organizes the docs redirects and adds some instructions for
them.

Release Notes:

- N/A
2025-07-16 14:44:24 +00:00
Umesh Yadav
875c86e3ef agent_ui: Fix token count not getting shown in the TextThread (#34485)
Closes #34319 

In this pr: https://github.com/zed-industries/zed/pull/33462 there was
check added for early return for active_thread and message_editor as
those are not present in the TextThread and only available in the Thread
the token count was not getting triggered for TextThread this pr fixes
that regression by moving the logic specific to Thread inside of thread
view match.

<img width="3024" height="1886" alt="CleanShot 2025-07-15 at 23 50
18@2x"
src="https://github.com/user-attachments/assets/bd74ae8b-6c37-4cdd-ab95-d3c253b8a948"
/>


Release Notes:

- Fix token count not getting shown in the TextThread
2025-07-16 10:39:07 -04:00
Oleksiy Syvokon
406ffb1e20 agent: Push diffs of user edits to the agent (#34487)
This change improves user/agent collaborative editing.

When the user edits files that are used by the agent, the
`project_notification` tool now pushes *diffs* of the changes, not just
file names. This helps the agent to stay up to date without needing to
re-read files.

Release Notes:

- Improved user/agent collaborative editing: agent now receives diffs of
user edits
2025-07-16 14:38:58 +00:00
Marshall Bowers
257bedf09b docs: Add missing extensions to redirects (#34539)
Fixes the redirects added in
https://github.com/zed-industries/zed/pull/34537.

Release Notes:

- N/A
2025-07-16 14:15:33 +00:00
Marshall Bowers
37927a5dc8 docs: Add some more redirects (#34537)
This PR adds some more redirects for the docs.

Release Notes:

- N/A
2025-07-16 10:01:31 -04:00
Smit Barmase
d4110fd2ab linux: Fix spacebar not working with multiple keyboard layouts (#34514)
Closes #26468 #16667

This PR fixes the spacebar not working with multiple keyboard layouts on
Linux X11. I have tested this with Czech, Russian, German, German Neo 2,
etc. It seems to work correctly.

`XkbStateNotify` events correctly update XKB state with complete
modifier info (depressed/latched/locked), but `KeyPress/KeyRelease`
events immediately overwrite that state using `update_mask()` with only
raw X11 modifier bits. This breaks xkb state as we reset `latched_mods`
and `locked_mods` to 0, as well as we might not correctly handle cases
where this new xkb state needs to change.

Previous logic is flawed because `KeyPress/KeyRelease` event only gives
you depressed modifiers (`event.state`) and not others, which we try to
fill in from `previous_xkb_state`. This patch was introduced to fix
capitalization issue with Neo 2
(https://github.com/zed-industries/zed/pull/14466) and later to fix
wrong keys with German layout
(https://github.com/zed-industries/zed/pull/31193), both of which I have
tested this PR with.

Now, instead of manually managing XKB state, we use the `update_key`
method, which internally handles modifier states and other cases we
might have missed.
  
From `update_key` docs:

> Update the keyboard state to reflect a given key being pressed or
released.
>
> This entry point is intended for programs which track the keyboard
state explictly (like an evdev client). If the state is serialized to
you by a master process (like a Wayland compositor) using functions like
`xkb_state_serialize_mods()`, you should use `xkb_state_update_mask()`
instead. **_The two functins should not generally be used together._**
>                
> A series of calls to this function should be consistent; that is, a
call with `xkb::KEY_DOWN` for a key should be matched by an
`xkb::KEY_UP`; if a key is pressed twice, it should be released twice;
etc. Otherwise (e.g. due to missed input events), situations like "stuck
modifiers" may occur.
>              
> This function is often used in conjunction with the function
`xkb_state_key_get_syms()` (or `xkb_state_key_get_one_sym()`), for
example, when handling a key event. In this case, you should prefer to
get the keysyms *before* updating the key, such that the keysyms
reported for the key event are not affected by the event itself. This is
the conventional behavior.

  
Release Notes:

- Fix the issue where the spacebar doesn’t work with multiple keyboard
layouts on Linux X11.
2025-07-16 19:25:13 +05:30
Peter Tripp
3d160a6e26 Don't highlight partial indent guide backgrounds (#34433)
Closes https://github.com/zed-industries/zed/issues/33665

Previously if a line was indented something that was not a multiple of
`tab_size` with `"ident_guides": { "background_coloring": "indent_aware"
} }` the background of characters would be highlighted. E.g. indent of 6
with tab_size 4.

| Before / After |
| - |
| <img width="497" height="77" alt="Screenshot 2025-07-14 at 14 43 46"
src="https://github.com/user-attachments/assets/93923117-047d-4d21-9a4f-488345f1ab89"
/>
| <img width="481" height="84" alt="Screenshot 2025-07-14 at 14 43 09"
src="https://github.com/user-attachments/assets/a5d383cb-50c3-4239-ae8c-f72765ae7287"
/> |

CC: @bennetbo Any idea why this partial indent was enabled in your
initial implementation
[here](https://github.com/zed-industries/zed/pull/11503/files#diff-1781b7848dd9630f3c4f62df322c08af9a2de74af736e7eba031ebaeb4a0e2f4R3156-R3160)?
This looks to be intentional.

Release Notes:

- N/A
2025-07-16 09:10:51 -04:00
Ragul R
c29c46d3b6 Appropriately pick venv activation script (#33205)
when `terminal.detect_venv.activate_script` setting is default, pick the
appropriate activate script as per the `terminal.shell` settings
specified by the user. Previously when the activate_script setting is
default, zed always try to use the `activate` script, which only works
when the user shell is `bash or zsh`. But what if the user is using
`fish` shell in zed?

Release Notes:

- python: value of `activate_script` setting is now automatically
inferred based on the kind of shell the user is running with.

---------

Co-authored-by: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com>
2025-07-16 12:52:37 +00:00
Piotr Osiewicz
312369c84f debugger: Improve drag-and-scroll in memory views (#34526)
Closes #34508

Release Notes:

- N/A
2025-07-16 10:00:36 +00:00
Stephen Samra
42b2b65241 Document alternative method to providing intelephense license key (#34502)
This PR updates the [Intelephense section in the
docs](https://zed.dev/docs/languages/php#intelephense) to include an
alternative way to provide the premium license key.

Release Notes:

- N/A
2025-07-16 06:14:18 +00:00
someone13574
a529103825 Disable format-on-save for verilog (#34512)
Disables format-on-save by default for the [verilog
extension](https://github.com/someone13574/zed-verilog-extension), since
there isn't a standard style.

Release Notes:

- N/A
2025-07-16 09:08:16 +03:00
Danilo Leal
1ed3f9eb42 Add user handle and plan chip to the user menu (#34522)
A nicer way to visualize in which plan you're in and a bit of
personalization by adding the GitHub handle you're signed with in the
user menu, as a complement to the avatar photo itself. Taking advantage
of the newly added Chip component.

<img width="320" height="476" alt="CleanShot 2025-07-16 at 1  33 08@2x"
src="https://github.com/user-attachments/assets/36718a42-27d1-499e-ac81-1eef2cd00347"
/>

Release Notes:

- N/A
2025-07-16 01:48:01 -03:00
Danilo Leal
59d524427e ui: Add Chip component (#34521)
Possibly the simplest component in our set, but a nice one to have so we
can standardize how it looks across the app.

Release Notes:

- N/A
2025-07-16 01:15:45 -03:00
Danilo Leal
ee4b9a27a2 ui: Fix wrapping in the banner component (#34516)
Also removing the `icon` field as the banner component always renders
with an icon anyway. Hopefully, this fixes any weird text wrapping that
was happening before.

Release Notes:

- N/A
2025-07-15 23:51:12 -03:00
Peter Tripp
ae65ff95a6 ci: Disable FreeBSD builds (#34511)
Recently FreeBSD zed-remote-server builds are failing 90%+ of the time
for unknown reasons.

Temporarily suspend them.

Example failing builds:
- [2025-07-15 16:15 Nightly
Failure](https://github.com/zed-industries/zed/actions/runs/16302777887/job/46042358675)
- [2025-07-15 12:20 Nightly
Success](https://github.com/zed-industries/zed/actions/runs/16297907892/job/46025281518)
- [2025-07-14 08:21 Nightly
Failure](https://github.com/zed-industries/zed/actions/runs/16266193889/job/45923004940)
- [2025-06-17 Nightly
Failure](https://github.com/zed-industries/zed/actions/runs/15700462603/job/44234573761)

Release Notes:

- Temporarily disable FreeBSD zed-remote-server builds due to CI failures.
2025-07-15 21:24:35 -04:00
Conrad Irwin
fc24102491 Tweaks to ACP for the Gemini PR (#34506)
- **Update to use --experimental-acp**
- **Fix tool locations**

Closes #ISSUE

Release Notes:

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

---------

Co-authored-by: mkorwel <matt.korwel@gmail.com>
Co-authored-by: Agus Zubiaga <agus@zed.dev>
2025-07-15 17:52:50 -06:00
Piotr Osiewicz
7ca3d969e0 debugger: Highlight the size of jumped-to memory (#34504)
Closes #ISSUE

Release Notes:

- N/A
2025-07-15 23:32:48 +00:00
Danilo Leal
afbd2b760f agent: Add plan chip in the Zed section within the settings view (#34503)
| Free | Pro |
|--------|--------|
| <img width="1140" height="368" alt="CleanShot 2025-07-15 at 7  50
48@2x"
src="https://github.com/user-attachments/assets/b54fd46d-d823-4689-b099-0a9aef8b1c9a"
/> | <img width="1136" height="348" alt="CleanShot 2025-07-15 at 7  51
45@2x"
src="https://github.com/user-attachments/assets/d291a1f5-511f-43df-9ce2-041c77d1cb86"
/> |

Release Notes:

- agent: Added a chip communicating which Zed plan you're subscribed to
in the agent panel settings view.
2025-07-15 23:10:44 +00:00
Cole Miller
0a3ef40c2f debugger: Interpret user-specified debug adapter binary paths in a more intuitive way for JS and Python (#33926)
Previously we would append `js-debug/src/dapDebugServer.js` to the value
of the `dap.JavaScript.binary` setting and `src/debugpy/adapter` to the
value of the `dap.Debugpy.binary` setting, which isn't particularly
intuitive. This PR fixes that.

Release Notes:

- debugger: Made the semantics of the `dap.$ADAPTER.binary` setting more
intuitive for the `JavaScript` and `Debugpy` adapters. In the new
semantics, this should be the path to `dapDebugServer.js` for
`JavaScript` and the path to the `src/debugpy/adapter` directory for
`Debugpy`.

---------

Co-authored-by: Remco Smits <djsmits12@gmail.com>
2025-07-15 22:31:28 +00:00
Cole Miller
0ebbeec11c debugger: Remove Start button from the attach modal (#34496)
Right now it doesn't work at all (the PID doesn't get set in the
generated scenario), and it's sort of redundant with the picker
functionality.

Release Notes:

- N/A
2025-07-15 17:06:46 -04:00
Smit Barmase
0ada4ce900 editor: Add ToggleFocus action (#34495)
This PR adds action `editor: toggle focus` which focuses to last active
editor pane item in workspace.

Release Notes:

- Added `editor: toggle focus` action, which focuses to last active
editor pane item.

---------

Co-authored-by: Danilo Leal <daniloleal09@gmail.com>
2025-07-16 01:47:40 +05:30
Anthony Eid
572d3d637a Rename action_input to action_arguments in keybinding contexts (#34480)
Release Notes:

- N/A
2025-07-15 16:06:50 -04:00
Conrad Irwin
3751737621 Add zed://extension/{id} links (#34492)
Release Notes:

- Add zed://extension/{id} links to open the extensions UI with a
specific extension
2025-07-15 13:42:25 -06:00
Umesh Yadav
ec52e9281a Add xAI language model provider (#33593)
Closes #30010

Release Notes:

- Add support for xAI language model provider
2025-07-15 15:35:50 -04:00
Cole Miller
af0031ae8b Fix positioning of terminal inline assist after clearing the screen (#34465)
Closes #33945. Here's my attempt to describe what's going on in that
issue and what this fix is doing:

We always render the terminal inline assistant starting on the line
after the cursor, with a height of 4 lines. When deploying it, we scroll
the viewport to the bottom of the terminal so that the assistant will be
in view.

When scrolling while the assistant is deployed (including in that case),
we need to make an adjustment that "pushes up" the terminal content by
the height of the assistant, so that we can scroll to see all the normal
content plus the assistant itself. That quantity is `scroll_top`, which
represents _how much height in the current viewport is occupied by the
assistant that would otherwise be occupied by terminal content_. So when
you scroll up and a line of the assistant's height goes out of view,
`scroll_top` decreases by 1, etc.

When we scroll to the bottom after deploying the assistant, we set
`scroll_top` to the result of calling `max_scroll_top`, which computes
it this way:

```
block.height.saturating_sub(viewport_lines.saturating_sub(terminal_lines))
```

Which, being interpreted, is "the height of the assistant, minus any
viewport lines that are not occupied by terminal content", i.e. the
assistant is allowed to eat up vertical space below the last line of
terminal content without increasing `scroll_top`.

The problem comes when we clear the screen---this adds a full screen to
`terminal_lines`, but the cursor is positioned at the top of the
viewport with blank lines below, just like at the beginning of a session
when `terminal_lines == 1`. Those blank lines should be available to the
assistant, but the `scroll_top` calculation doesn't reflect that.

I've tried to fix this by basing the `max_scroll_top` calculation on the
position of the cursor instead of the raw `terminal_lines` value. There
was also a special case for `viewport_lines == terminal_lines` that I
think can now be removed.

Release Notes:

- Fixed the positioning of the terminal inline assistant when it's
deployed after clearing the terminal.
2025-07-15 15:16:48 -04:00
Ariel Rzezak
b398935081 Fix comment in default.json (#34481)
Update line to properly reference the intended setting.

Release Notes:

- N/A
2025-07-15 19:07:39 +00:00
Michael Sloan
78b7737368 Remove scap from workspace-hack (#34490)
Regression in #34251 which broke remote_server build

Release Notes:

- N/A
2025-07-15 19:07:01 +00:00
Richard Feldman
57e8f5c5b9 Automatically retry in more situations (#34473)
In #33275 I was very conservative about when to retry when there are
errors in language completions in the Agent panel.

Now we retry in more scenarios (e.g. HTTP 5xx and 4xx errors that aren't
in the specific list of ones that we handle differently, such as 429s),
and also we show a notification if the thread halts for any reason.

<img width="441" height="68" alt="Screenshot 2025-07-15 at 12 51 30 PM"
src="https://github.com/user-attachments/assets/433775d0-a8b2-403d-9427-1e296d164980"
/>
<img width="482" height="322" alt="Screenshot 2025-07-15 at 12 44 15 PM"
src="https://github.com/user-attachments/assets/5a508224-0fe0-4d34-9768-25d95873eab8"
/>


Release Notes:

- Automatic retry for more Agent errors
- Whenever the Agent stops, play a sound (if configured) and show a
notification (if configured) if the Zed window was in the background.
2025-07-15 14:22:13 -04:00
Smit Barmase
729cde33f1 project_panel: Add rename, delete and duplicate actions to workspace (#34478)
Release Notes:

- Added `project panel: rename`, `project panel: delete` and `project
panel: duplicate` actions to workspace.

Co-authored-by: Danilo Leal <daniloleal09@gmail.com>
2025-07-15 23:41:53 +05:30
Ben Kunkle
ebbf02e25b keymap_ui: Keyboard navigation for keybind edit modal (#34482)
Adds keyboard navigation to the keybind edit modal. Using up/down arrows
to select the previous/next input editor, and `cmd-enter` to save +
`escape` to exit

Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-07-15 18:03:19 +00:00
Cole Miller
3ecdfc9b5a Remove auto-width editor type (#34438)
Closes #34044

`EditorMode::SingleLine { auto_width: true }` was only used for the
title editor in the rules library, and following
https://github.com/zed-industries/zed/pull/31994 we can replace that
with a normal single-line editor without problems. The auto-width editor
was interacting badly with the recently-added newline visualization
code, causing a panic during layout---by switching it to
`Editor::single_line` the newline visualization works there too.

Release Notes:

- Fixed a panic that could occur when opening the rules library.

---------

Co-authored-by: Finn <finn@zed.dev>
2025-07-15 17:36:09 +00:00
Anthony Eid
f9561da673 Maintain keymap editor position when deleting or modifying a binding (#34440)
When a key binding is deleted we keep the exact same scroll bar
position. When a keybinding is modified we select that keybinding in
it's new position and scroll to it.

I also changed save/modified keybinding to use fs.write istead of
fs.atomic_write. Atomic write was creating two FS events that some
scrollbar bugs when refreshing the keymap editor.

Co-authored-by: Ben \<ben@zed.dev\>

Release Notes:

- N/A
2025-07-15 17:16:29 +00:00
Finn Evers
b3747d9a21 keymap_ui: Add column for conflict indicator and edit button (#34423)
This PR adds a column to the keymap editor to highlight warnings as well
as add the possibility to click the edit icon there for editing the
corresponding entry in the list.

Release Notes:

- N/A

---------

Co-authored-by: Danilo Leal <67129314+danilo-leal@users.noreply.github.com>
Co-authored-by: Danilo Leal <daniloleal09@gmail.com>
2025-07-15 12:52:21 -04:00
Ben Kunkle
95de2bfc74 keymap_ui: Limit length of keystroke input and hook up actions (#34464)
Closes #ISSUE

Changes direction on the design of the keystroke input. Due to MacOS
limitations, it was decided that the complex repeat keystroke logic
could be avoided by limiting the number of keystrokes so that accidental
repeats were less damaging to ux. This PR follows up on the design pass
in #34437 that assumed these changes would be made, hooking up actions
and greatly improving the keyboard navigability of the keystroke input.

Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-07-15 16:03:16 +00:00
teapo
d7bb1c1d0e lsp: Fix workspace diagnostics lag & add streaming support (#34022)
Closes https://github.com/zed-industries/zed/issues/33980
Closes https://github.com/zed-industries/zed/discussions/33979

- Switches to the debounce task pattern for diagnostic summary
computations, which most importantly lets us do them only once when a
large number of DiagnosticUpdated events are received at once.
- Makes workspace diagnostic requests not time out if a partial result
is received.
- Makes diagnostics from workspace diagnostic partial results get
merged.

There might be some related areas where we're not fully complying with
the LSP spec but they may be outside the scope of what this PR should
include.

Release Notes:

- Added support for streaming LSP workspace diagnostics.
- Fixed editor freeze from large LSP workspace diagnostic responses.
2025-07-15 18:41:45 +03:00
Umesh Yadav
5f3e7a5f91 lsp: Wait for shutdown response before sending exit notification (#33417)
Follow up: #18634

Closes #33328

Release Notes:

- Fixed language server shutdown process to prevent race conditions and
improper termination by waiting for shutdown confirmation before closing
connections.
2025-07-15 18:30:57 +03:00
Taylor Beever
0671a4d5ae Allow for venv activation script to use pyenv (#33119)
Release Notes:

- Allows for configuration and use of `pyenv` as a virtual environment provider
2025-07-15 16:44:40 +02:00
tidely
bd78f2c493 project: Use checked_sub for next/previous in search history (#34408)
Use `checked_sub` instead of checking for bounds manually. Also greatly
simplifies the logic for `next` and `previous`. Removing other manual
bounds checks as well

Release Notes:

- N/A
2025-07-15 16:42:37 +02:00
tidely
d1abba0d33 gpui: Reduce manual shifting & other minor improvements (#34407)
Minor cleanup in gpui.

- Reduce manual shifting by using `u32::to_be_bytes`
- Remove eager `Vec` allocation when listing registered actions
- Remove unnecessary return statements
- Replace manual `if let Some(_)` with `.as_deref_mut()`

Release Notes:

- N/A
2025-07-15 16:39:33 +02:00
tidely
05065985e7 cli: Remove manual std::io::copy implementation (#34409)
Removes a manual implementation of `std::io::copy`. The internal buffer
of `std::io::copy` is also 8 kB and behaves exactly the same. On Linux
`std::io::copy` also has access to some better performing file copying.

Release Notes:

- N/A
2025-07-15 16:37:15 +02:00
Ben Brandt
7ab8f431a7 Update to acp 0.0.9 (#34463)
Release Notes:

- N/A
2025-07-15 14:28:27 +00:00
Hilmar Wiegand
050ed85d71 Add severity argument to GoToDiagnostic actions (#33995)
This PR adds a `severity` argument so severity can be defined when
navigating through diagnostics. This allows keybinds like the following:

```json
{
  "] e": ["editor::GoToDiagnostic", { "severity": "error" }],
  "[ e": ["editor::GoToDiagnostic", { "severity": "error" }]
}
```

I've added test comments and a test. Let me know if there's anything
else you need!

Release Notes:

- Add `severity` argument to `editor::GoToDiagnostic`,
`editor::GoToPreviousDiagnostic`, `project_panel::SelectNextDiagnostic`
and `project_panel::SelectPrevDiagnostic` actions
2025-07-15 14:03:57 +00:00
Danilo Leal
858e176a1c Refine keymap UI design (#34437)
Release Notes:

- N/A

---------

Co-authored-by: Ben Kunkle <Ben.kunkle@gmail.com>
2025-07-15 13:45:59 +00:00
Marshall Bowers
a65c0b2bff collab: Fix typo in log message (#34455)
This PR fixes a small typo in a log message.

Release Notes:

- N/A
2025-07-15 13:16:49 +00:00
Marshall Bowers
848a86a385 collab: Sync model overages for all active Zed Pro subscriptions (#34452)
Release Notes:

- N/A
2025-07-15 13:01:01 +00:00
Piotr Osiewicz
52f2b32557 extension_cli: Copy over snippet file when bundling extensions (#34450)
Closes #30670

Release Notes:

- Fixed snippets from extensions not working.
2025-07-15 11:07:29 +00:00
Alvaro Parker
8dca4d150e Fix border and minimap flickering on pane split (#33973)
Closes #33972

As noted on
https://github.com/zed-industries/zed/pull/31390#discussion_r2147473526,
when splitting panes and having a border size set for the active pane,
or the minimap visibility configured to the active editor only, zed will
shortly show a flicker of the border or the minimap on the pane that's
being deactivated.

Release Notes:

- Fixed an issue where pane activations would sometimes have a brief
delay, causing a flicker in the process.
2025-07-15 10:47:35 +02:00
Peter Tripp
440beb8a90 Improve Java LSP documentation (#34410)
Remove references to
[ABckh/zed-java-eclipse-jdtls](https://github.com/ABckh/zed-java-eclipse-jdtls)
which hasn't seen a new version in 10 months (2024-10-01).

Release Notes:

- N/A
2025-07-14 18:16:43 -04:00
Peter Tripp
ce63a6ddd8 Exclude .repo folders by default (#34431)
These are used by [Google's `repo`
tool](https://android.googlesource.com/tools/repo) used for Android for
managing hundreds of git subprojects.

Originally reported in:
- https://github.com/zed-industries/zed/issues/34302

Release Notes:

- Add Google Repo `.repo` folders to default `file_scan_exclusions`
2025-07-14 18:16:28 -04:00
Finn Evers
26ba6e7e00 editor: Improve minimap performance (#33067)
This PR aims to improve the minimap performace. This is primarily
achieved by disabling/removing stuff that is not shown in the minimal as
well as by assuring the display map is not updated during minimap
prepaint.

This should already be much better in parts, as the block map as well as
the fold map will be less frequently updated due to the minimap
prepainting (optimally, they should never be, but I think we're not
quite there yet).
For this, I had to remove block rendering support for the minimap, which
is not as bad as it sounds: Practically, we were currently not rendering
most blocks anyway, there were issues due to this (e.g. scrolling any
visible block offscreen in the main editor causes scroll jumps
currently) and in the long run, the minimap will most likely need its
own block map or a different approach anyway. The existing
implementation caused resizes to occur very frequently for practically
no benefit. Can pull this out into a separate PR if requested, most
likely makes the other changes here easier to discuss.

This is WIP as we are still hitting some code path here we definitely
should not be hitting. E.g. there seems to be a rerender roughly every
second if the window is unfocused but visible which does not happen when
the minimap is disabled.

While this primarily focuses on the minimap, it also touches a few other
small parts not related to the minimap where I noticed we were doing too
much stuff during prepaint. Happy for any feedback there aswell.

Putting this up here already so we have a place to discuss the changes
early if needed.

Release Notes:

- Improved performance with the minimap enabled.
- Fixed an issue where interacting with blocks in the editor would
sometimes not properly work with the minimap enabled.
2025-07-15 00:29:27 +03:00
Joseph T. Lyons
363a265051 Add test for running Close Others on an inactive item (#34425)
Adds a test for the changes added in:
https://github.com/zed-industries/zed/pull/34355

Release Notes:

- N/A
2025-07-14 20:24:05 +00:00
Michael Sloan
37e73e3277 Only depend on scap x11 feature when gpui x11 feature is enabled (#34251)
Release Notes:

- N/A
2025-07-14 18:34:33 +00:00
Richard Feldman
32f5132bde Fix contrast adjustment for Powerline separators (#34417)
It turns out Starship is using custom Powerline separators in the
Unicode private reserved character range. This addresses some issues
seen in the comments of #34234

Release Notes:

- Fix automatic contrast adjustment for Powerline separators
2025-07-14 18:18:41 +00:00
Anthony Eid
fd5650d4ed debugger: A support for data breakpoint's on variables (#34391)
Closes #ISSUE

Release Notes:

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

---------

Co-authored-by: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com>
Co-authored-by: Mikayla Maki <mikayla.c.maki@gmail.com>
Co-authored-by: Mikayla Maki <mikayla@zed.dev>
2025-07-14 17:45:46 +00:00
domi
8b6b039b63 vim: Add missing normal mode binding for signature help overload (#34278)
Closes #ISSUE

related https://github.com/zed-industries/zed/pull/33199
2025-07-14 17:20:19 +00:00
Piotr Osiewicz
4848bd705e docs/debugger: Remove mention of onboarding calls (#34414)
Closes #ISSUE

Release Notes:

- N/A
2025-07-14 17:18:09 +00:00
Conrad Irwin
45d0686129 Remove unused KeycodeSource (#34403)
Release Notes:

- N/A
2025-07-14 11:03:16 -06:00
Marshall Bowers
eca36c502e Route all LLM traffic through cloud.zed.dev (#34404)
This PR makes it so all LLM traffic is routed through `cloud.zed.dev`.

We're already routing `llm.zed.dev` to `cloud.zed.dev` on the server,
but we want to standardize on `cloud.zed.dev` moving forward.

Release Notes:

- N/A
2025-07-14 16:03:19 +00:00
Piotr Osiewicz
6673c7cd4c debugger: Add memory view (#33955)
This is mostly setting up the UI for now; I expect it to be the biggest
chunk of work.

Release Notes:

- debugger: Added memory view

---------

Co-authored-by: Anthony Eid <hello@anthonyeid.me>
Co-authored-by: Mikayla Maki <mikayla.c.maki@gmail.com>
Co-authored-by: Mikayla Maki <mikayla@zed.dev>
2025-07-14 16:32:06 +02:00
Peter Tripp
a2f5c47e2d Add editor::ToggleFoldAll action (#34317)
In multibuffers adds the ability to alt-click to fold/unfold all
excepts. In singleton buffers it adds the ability to toggle back and
forth between `editor::FoldAll` and `editor::UnfoldAll`.

Bind it in your keymap with:

```json
  {
    "context": "Editor && (mode == full || multibuffer)",
    "bindings": {
      "cmd-k cmd-o": "editor::ToggleFoldAll"
    }
  },
```

<img width="253" height="99" alt="Screenshot 2025-07-11 at 17 04 25"
src="https://github.com/user-attachments/assets/94de8275-d2ee-4cf8-a46c-a698ccdb60e3"
/>

Release Notes:

- Add ability to fold all excerpts in a multibuffer (alt-click) and in
singleton buffers `editor::ToggleFoldAll`
2025-07-14 13:23:51 +00:00
Peter Tripp
c6a6db9754 emacs: Fix cmd-f not working in Terminal (#34400)
Release Notes:

- N/A
2025-07-14 13:16:25 +00:00
Brian Donovan
6f9e052edb languages: Add JS/TS generator functions to outline (#34388)
Functions like `function* iterateElements() {}` would not show up in the
editor's navigation outline. With this change, they do.

| **Before** | **After**
|-|-|
|<img width="453" height="280" alt="Screenshot 2025-07-13 at 4 58 22 PM"
src="https://github.com/user-attachments/assets/822f0774-bda2-4855-a6dd-80ba82fffaf3"
/>|<img width="564" height="373" alt="Screenshot 2025-07-13 at 4 58
55 PM"
src="https://github.com/user-attachments/assets/f4f6b84f-cd26-49b7-923b-724860eb18ad"
/>|

Note that I decided to use Zed's agent assistance features to do this PR
as a sort of test run. I don't normally code with an AI assistant, but
figured it might be good in this case since I'm unfamiliar with the
codebase. I must say I was fairly impressed. All the changes in this PR
were done by Claude Sonnet 4, though I have done a manual review to
ensure the changes look sane and tested the changes by running the
re-built `zed` binary with a toy project.

Closes #21631

Release Notes:

- Fixed JS/TS outlines to show generator functions.
2025-07-14 07:26:17 -05:00
Oleksiy Syvokon
2edf85f054 evals: Switch disable_cursor_blinking to determenistic asserts (#34398)
Release Notes:

- N/A
2025-07-14 11:26:15 +00:00
vipex
00ec243771 pane: 'Close others' now closes relative to right-clicked tab (#34355)
Closes #33445

Fixed the "Close others" context menu action to close tabs relative to
the right-clicked tab instead of the currently active tab. Previously,
when right-clicking on an inactive tab and selecting "Close others", it
would keep the active tab open rather than the right-clicked tab.

## Before/After

https://github.com/user-attachments/assets/d76854c3-c490-4a41-8166-309dec26ba8a



## Changes

- Modified `close_inactive_items()` method to accept an optional
`target_item_id` parameter
- Updated context menu handler to pass the right-clicked tab's ID as the
target
- Maintained backward compatibility by defaulting to active tab when no
target is specified
- Updated all existing call sites to pass `None` for the new parameter

Release Notes:

- Fixed: "Close others" context menu action now correctly keeps the
right-clicked tab open instead of the active tab
2025-07-14 14:06:40 +03:00
feeiyu
84124c60db Fix cannot select in terminal when copy_on_select is enabled (#34131)
Closes #33989


![terminal_select](https://github.com/user-attachments/assets/5027d2f2-f2b3-43a4-8262-3c266fdc5256)

Release Notes:

- N/A
2025-07-14 14:04:54 +03:00
Umesh Yadav
cf1ce1beed languages: Fix ESLint diagnostics not getting shown (#33814)
Closes #33442

Release Notes:

- Resolved an issue where the ESLint language server returned an empty
string for the CodeDescription.href field in diagnostics, leading to
missing diagnostics in editor.
2025-07-14 13:48:56 +03:00
Oleksiy Syvokon
e4effa5e01 linux: Fix keycodes mapping on Wayland (#34396)
We are already converting Wayland keycodes to X11's; double conversion
results in a wrong mapping.


Release Notes:

- N/A
2025-07-14 09:44:29 +00:00
Sergei Surovtsev
f50041779d Language independent hotkeys (#34053)
Addresses #10972 
Closes #24950
Closes #24499

Adds _key_en_ to _Keystroke_ that is derived from key's scan code. This
is more lightweight approach than #32529

Currently has been tested on x11 and windows. Mac code hasn't been
implemented yet.

Release Notes:

- linux: When typing non-ASCII keys on Linux we will now also match
keybindings against the QWERTY-equivalent layout. This should allow most
of Zed's builtin shortcuts to work out of the box on most keyboard
layouts. **Breaking change**: If you had been using `keysym` names in
your keyboard shortcut file (`ctrl-cyrillic_yeru`, etc.) you should now
use the QWERTY-equivalent characters instead.

---------

Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
2025-07-13 23:00:03 -06:00
Smit Barmase
51df8a17ef project_panel: Do not render a single sticky entry when scrolled all the way to the top (#34389)
Fixes root entry not expanding/collapsing on nightly. Regressed in
https://github.com/zed-industries/zed/pull/34367.

Release Notes:

- N/A
2025-07-14 06:59:45 +05:30
Somtoo Chukwurah
85d12548a1 linux: Add file_finder::Toggle key binding (#34380)
This fixes a bug on linux where repeated presses of p while holding down
the ctrl modifier navigates through options in reverse.

Closes #34379 

The main issue is the default biding of ctrl-p on linux is
menu::SelectPrevious hence in context "context": "FileFinder ||
(FileFinder > Picker > Editor)" it would navigate in reverse

Release Notes:

- Fixed `file_finder::Toggle` on Linux not scrolling forward
2025-07-13 23:35:03 +00:00
Finn Evers
0af7d32b7d keymap_ui: Dismiss context menu less frequently (#34387)
This PR fixes an issue where the context menu in the keymap UI would be
immediately dismissed after being opened when using a trackpad on MacOS.

Right clicking on MacOS almost always fires a scroll event with a delta
of 0 pixels right after (which is not the case when using a mouse). The
fired scroll event caused the context menu to be removed on the next
frame. This change ensures the menu is only removed when a vertical
scroll is actually happening.

Release Notes:

- N/A
2025-07-13 21:41:41 +00:00
Smit Barmase
1cadff9311 project_panel: Fix sticky items horizontal scroll and hover propagation (#34367)
Release Notes:

- Fixed horizontal scrolling not working for sticky items in the Project
Panel.
- Fixed issue where hovering over the last sticky item in the Project
Panel showed a hovered state on the entry behind it.
- Improved behavior when clicking a sticky item in the Project Panel so
it scrolls just enough for the item to no longer be sticky.
2025-07-13 06:14:30 +05:30
Anthony Eid
8f6b9f0d65 debugger: Allow users to shutdown debug sessions while they're booting (#34362)
This solves problems where users couldn't shut down sessions while
locators or build tasks are running.

I renamed `debugger::Session::Mode` enum to `SessionState` to be more
clear when it's referenced in other crates. I also embedded the boot
task that is created in `SessionState::Building` variant. This allows
sessions to shut down all created threads in their boot process in a
clean and idiomatic way.

Finally, I added a method on terminal that allows killing the active
task.

Release Notes:

- Debugger: Allow shutting down debug sessions while they're booting up
2025-07-12 19:16:35 -04:00
Cole Miller
970a1066f5 git: Handle shift-click to stage a range of entries in the panel (#34296)
Release Notes:

- git: shift-click can now be used to stage a range of entries in the
git panel.
2025-07-12 19:04:26 +00:00
Remco Smits
833bc6979a debugger: Fix correctly determine replace range for debug console completions (#33959)
Follow-up #33868

This PR fixes a few issues with determining the completion range for
client‑ and variable‑list completions.

1. Non‑word completions
We previously supported only word characters and _, using their combined
length to compute the start offset. In PHP, however, an expression can
contain `$`, `-`, `>`, `[`, `]`, `(`, and `)`. Because these characters
weren’t treated as word characters, the start offset stopped at them,
even when the preceding character was part of a word.

2. Trailing characters inside the search text
When autocompletion occurred in the middle of the search text, we didn’t
account for trailing characters. As a result, the start offset was off
by the number of characters after the cursor. For example, replacing res
with result in print(res) produced `print(rresult)` because the trailing
`)` wasn’t subtracted from the start offset.

The following completions are correctly covered now:

- **Before** `$aut` -> `$aut$author` **After** `$aut` -> `$author`
- **Before** `$author->na` -> `$author->na$author->name` **After**
`$author->na` -> `$author->name`
- **Before** `$author->books[` -> `$author->books[$author->books[0]`
**After** `$author->books[` -> `$author->books[0]`
- **Before** `print(res)` -> `print(rresult)` **After** `print(res)` ->
`print(result)`

**Before**


https://github.com/user-attachments/assets/b530cf31-8d4d-45e6-9650-18574f14314c


https://github.com/user-attachments/assets/52475b7b-2bf2-4749-98ec-0dc933fcc364

**After**


https://github.com/user-attachments/assets/c065701b-31c9-4e0a-b584-d1daffe3a38c


https://github.com/user-attachments/assets/455ebb3e-632e-4a57-aea8-d214d2992c06

Release Notes:

- Debugger: Fixed autocompletion not always replacing the correct search
text
2025-07-12 14:24:49 -04:00
Cole Miller
a8cc927303 debugger: Improve appearance of session list for JavaScript debugging (#34322)
This PR updates the debugger panel's session list to be more useful in
some cases that are commonly hit when using the JavaScript adapter. We
make two adjustments, which only apply to JavaScript sessions:

- For a child session that's the only child of a root session, we
collapse it with its parent. This imitates what VS Code does in the
"call stack" view for JavaScript sessions.
- When a session has exactly one thread, we label the session with that
thread's name, instead of the session label provided by the DAP. VS Code
also makes this adjustment, which surfaces more useful information when
working with browser sessions.

Closes #33072 

Release Notes:

- debugger: Improved the appearance of JavaScript sessions in the debug
panel's session list.

---------

Co-authored-by: Julia <julia@zed.dev>
Co-authored-by: Remco Smits <djsmits12@gmail.com>
2025-07-12 15:56:05 +00:00
Kirill Bulatov
13ddd5e4cb Return back the guards when goto targets are queried for (#34340)
Closes https://github.com/zed-industries/zed/issues/34310
Follow-up of https://github.com/zed-industries/zed/pull/29359

Release Notes:

- Fixed goto definition not working in remote projects in certain
conditions
2025-07-12 18:27:52 +03:00
Cole Miller
1b6e212eba debugger: Fix endless restarts when connecting to TCP adapters over SSH (#34328)
Closes #34323
Closes #34313

The previous PR #33932 introduced a way to "close" the
`pending_requests` buffer of the `TransportDelegate`, preventing any
more requests from being added. This prevents pending requests from
accumulating without ever being drained during the shutdown sequence;
without it, some of our tests hang at this point (due to using a
single-threaded executor).

The bug occurred because we were closing `pending_requests` whenever we
detected the server side of the transport shut down, and this closed
state stuck around and interfered with the retry logic for SSH+TCP
adapter connections.

This PR fixes the bug by only closing `pending_requests` on session
shutdown, and adds a regression test covering the SSH retry logic.

Release Notes:

- debugger: Fixed a bug causing SSH connections to some adapters
(Python, Go, JavaScript) to fail and restart endlessly.
2025-07-12 11:27:18 -04:00
Danilo Leal
46834d31f1 Refine status bar design (#34324)
Experimenting with a set of standardized icons and polishing spacing a
little bit.

Release Notes:

- N/A
2025-07-12 11:48:19 -03:00
Kirill Bulatov
e070c81687 Remove remaining plugin-related language server adapters (#34334)
Follow-up of https://github.com/zed-industries/zed/pull/34208

Release Notes:

- N/A
2025-07-12 11:42:14 +00:00
Cole Miller
5b61b8c8ed agent: Fix crash with pathological fetch output (#34253)
Closes #34029

The crash is due to a stack overflow in our `html_to_markdown`
conversion; I've added a maximum depth of 200 for the recursion in that
crate to guard against this kind of thing.

Separately, we were treating all content-types other than `text/plain`
and `application/json` as HTML; I've changed this to only treat
`text/html` and `application/xhtml+xml` as HTML, and fall back to
plaintext. (In the original crash, the content-type was
`application/octet-stream`.)

Release Notes:

- agent: Fixed a potential crash when fetching large non-HTML files.
2025-07-11 21:01:09 -04:00
Cole Miller
625ce12a3e Revert "git: Intercept signing prompt from GPG when committing" (#34306)
Reverts zed-industries/zed#34096

This introduced a regression, because the unlocked key can't benefit
from caching.

Release Notes:
- N/A
2025-07-11 23:20:35 +00:00
vipex
12bc8907d9 Recall empty, unsaved buffers on app load (#33475)
Closes #33342

This PR implements serialization of pinned tabs regardless of their
state (empty, untitled, etc.)

The root cause was that empty untitled tabs were being skipped during
serialization but their pinned state was still being persisted, leading
to a mismatch between the stored pinned count and actual restorable
tabs, this issue lead to a crash which was patched by @JosephTLyons, but
this PR aims to be a proper fix.

**Note**: I'm still evaluating the best approach for this fix. Currently
exploring whether it's necessary to store the pinned state in the
database schema or if there's a simpler solution that doesn't require
schema changes.

--- 

**Edit from Joseph**

We ended up going with altering our recall logic, where we always
restore all editors, even those that are new, empty, and unsaved. This
prevents the crash that #33335 patched because we are no longer skipping
the restoration of pinned editors that have no text and haven't been
saved, throwing off the count dealing with the number of pinned items.

This solution is rather simple, but I think it's fine. We simply just
restore everything the same, no conditional dropping of anything. This
is also consistent with VS Code, which also restores all editors,
regardless of whether or not a new, unsaved buffers have content or not.

https://github.com/zed-industries/zed/tree/alt-solution-for-%2333342

Release Notes:
- N/A

---------

Co-authored-by: Joseph T. Lyons <JosephTLyons@gmail.com>
2025-07-11 22:23:04 +00:00
Ben Kunkle
67c765a99a keymap_ui: Dual-phase focus for keystroke input (#34312)
Closes #ISSUE

An idea I and @MrSubidubi came up with, to improve UX around the
keystroke input.

Currently, there's a hard tradeoff with what to focus first in the edit
keybind modal, if we focus the keystroke input, it makes keybind
modification very easy, however, if you don't want to edit a keybind,
you must use the mouse to escape the keystroke input before editing
something else - breaking keyboard navigation.

The idea in this PR is to have a dual-phased focus system for the
keystroke input. There is an outer focus that has some sort of visual
indicator to communicate it is focused (currently a border). While the
outer focus region is focused, keystrokes are not intercepted. Then
there is a keybind (currently hardcoded to `enter`) to enter the inner
focus where keystrokes are intercepted, and which must be exited using
the mouse. When the inner focus region is focused, there is a visual
indicator for the fact it is "recording" (currently a hacked together
red pulsing recording icon)


<details><summary>Video</summary>


https://github.com/user-attachments/assets/490538d0-f092-4df1-a53a-a47d7efe157b


</details>

Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-07-11 17:06:06 -05:00
Ben Kunkle
206cce6783 keymap_ui: Support unbinding non-user defined keybindings (#34318)
Closes #ISSUE

Makes it so that `KeymapFile::update_keybinding` treats removals of
bindings that weren't user-defined as creating a new binding to
`zed::NoAction`.


Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-07-11 21:23:14 +00:00
Julia Ryan
c3edc2cfc1 DAP log view improvements (#34311)
Now DAP logs show the label of each session which makes it much easier
to pick out the right one.

Also "initialization sequence" now shows up correctly when that view is
selected.

Release Notes:

- N/A

---------

Co-authored-by: Cole Miller <cole@zed.dev>
2025-07-11 19:34:53 +00:00
Mikayla Maki
625a4b90a5 Tinker with the reporting of telemetry events (#34239)
Release Notes:

- N/A

---------

Co-authored-by: Katie Geer <katie@zed.dev>
2025-07-11 12:02:40 -07:00
Lukas Spiss
fbead09c30 go: Write envFile properties back to env config (#34300)
Closes https://github.com/zed-industries/zed/issues/32984

Note that while https://github.com/zed-industries/zed/pull/33666 did the
reading of the `envFile` just fine, the read values were never passed
along. This was mentioned by [this
comment](https://github.com/zed-industries/zed/pull/33666#issuecomment-3060785970)
and also confirmed by myself.

With the changes here, I successfully debugged a project of mine and all
the environment variables from my `.env` were present.

Release Notes:

- Fix Go debugger ignoring env vars from the envFile setting.
2025-07-11 18:26:46 +00:00
Ben Kunkle
0797f7b66e keymap_ui: Show existing keystrokes as placeholders in edit modal (#34307)
Closes #ISSUE

Previously, the keystroke input would be empty, even when editing an
existing binding. This meant you had to re-enter the bindings even if
you just wanted to edit the context. Now, the existing keystrokes are
rendered as a placeholder, are re-shown if newly entered keystrokes are
cleared, and will be returned from the `KeystrokeInput::keystrokes()`
method if no new keystrokes were entered.

Additionally fixed a bug in `KeymapFile::update_keybinding` where
semantically identical contexts would be treated as unequal due to
formatting differences.

Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-07-11 18:07:04 +00:00
Agus Zubiaga
6f6c2915b2 Display and jump to tool locations (#34304)
Release Notes:

- N/A
2025-07-11 14:52:21 -03:00
Julia Ryan
0bd65829f7 Truncate multi-line debug value hints (#34305)
Release Notes:

- Multi-line debug inline values are now truncated.

Co-authored-by: Anthony Eid <hello@anthonyeid.me>
2025-07-11 17:49:52 +00:00
Finn Evers
90bf602ceb Reduce number of snapshots and notifies during editor scrolling (#34228)
We not do not create new snapshots anymore when autoscrolling
horizontally and also do not notify any longer should the new scroll
position match the old one.

Release Notes:

- N/A

---------

Co-authored-by: Michael Sloan <mgsloan@gmail.com>
2025-07-11 17:34:45 +00:00
localcc
cd024b8870 Add licenses.md for Windows build (#34272)
Release Notes:

- N/A
2025-07-11 19:28:48 +02:00
Finn Evers
af71e15ea0 editor: Fix scrolling stuttering at the top of multibuffers (#34295)
Release Notes:

- Fixed an issue where scrolling would stutter at the top of
multibuffers.
2025-07-11 19:10:39 +02:00
212 changed files with 10507 additions and 3602 deletions

View File

@@ -23,6 +23,8 @@ workspace-members = [
]
third-party = [
{ name = "reqwest", version = "0.11.27" },
# build of remote_server should not include scap / its x11 dependency
{ name = "scap", git = "https://github.com/zed-industries/scap", rev = "270538dc780f5240723233ff901e1054641ed318" },
]
[final-excludes]

View File

@@ -679,8 +679,10 @@ jobs:
timeout-minutes: 60
runs-on: github-8vcpu-ubuntu-2404
if: |
false && (
startsWith(github.ref, 'refs/tags/v')
|| contains(github.event.pull_request.labels.*.name, 'run-bundling')
)
needs: [linux_tests]
name: Build Zed on FreeBSD
steps:
@@ -798,7 +800,7 @@ jobs:
if: |
startsWith(github.ref, 'refs/tags/v')
&& endsWith(github.ref, '-pre') && !endsWith(github.ref, '.0-pre')
needs: [bundle-mac, bundle-linux-x86_x64, bundle-linux-aarch64, bundle-windows-x64, freebsd]
needs: [bundle-mac, bundle-linux-x86_x64, bundle-linux-aarch64, bundle-windows-x64]
runs-on:
- self-hosted
- bundle

View File

@@ -187,7 +187,7 @@ jobs:
freebsd:
timeout-minutes: 60
if: github.repository_owner == 'zed-industries'
if: false && github.repository_owner == 'zed-industries'
runs-on: github-8vcpu-ubuntu-2404
needs: tests
name: Build Zed on FreeBSD

99
Cargo.lock generated
View File

@@ -264,16 +264,18 @@ dependencies = [
[[package]]
name = "agentic-coding-protocol"
version = "0.0.7"
version = "0.0.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a75f520bcc049ebe40c8c99427aa61b48ad78a01bcc96a13b350b903dcfb9438"
checksum = "0e276b798eddd02562a339340a96919d90bbfcf78de118fdddc932524646fac7"
dependencies = [
"anyhow",
"chrono",
"derive_more 2.0.1",
"futures 0.3.31",
"log",
"parking_lot",
"schemars",
"semver",
"serde",
"serde_json",
]
@@ -608,7 +610,6 @@ dependencies = [
"parking_lot",
"smol",
"tempfile",
"unindent",
"util",
"workspace-hack",
]
@@ -677,7 +678,7 @@ dependencies = [
"anyhow",
"async-trait",
"collections",
"derive_more",
"derive_more 0.99.19",
"extension",
"futures 0.3.31",
"gpui",
@@ -740,10 +741,11 @@ dependencies = [
"clock",
"collections",
"ctor",
"derive_more",
"derive_more 0.99.19",
"futures 0.3.31",
"gpui",
"icons",
"indoc",
"language",
"language_model",
"log",
@@ -776,7 +778,8 @@ dependencies = [
"clock",
"collections",
"component",
"derive_more",
"derive_more 0.99.19",
"diffy",
"editor",
"feature_flags",
"fs",
@@ -1233,7 +1236,7 @@ version = "0.1.0"
dependencies = [
"anyhow",
"collections",
"derive_more",
"derive_more 0.99.19",
"gpui",
"parking_lot",
"rodio",
@@ -2926,7 +2929,7 @@ dependencies = [
"cocoa 0.26.0",
"collections",
"credentials_provider",
"derive_more",
"derive_more 0.99.19",
"feature_flags",
"fs",
"futures 0.3.31",
@@ -3110,10 +3113,11 @@ dependencies = [
"context_server",
"ctor",
"dap",
"dap-types",
"dap_adapters",
"dashmap 6.1.0",
"debugger_ui",
"derive_more",
"derive_more 0.99.19",
"editor",
"envy",
"extension",
@@ -3166,6 +3170,7 @@ dependencies = [
"session",
"settings",
"sha2",
"smol",
"sqlx",
"strum 0.27.1",
"subtle",
@@ -3318,7 +3323,7 @@ name = "command_palette_hooks"
version = "0.1.0"
dependencies = [
"collections",
"derive_more",
"derive_more 0.99.19",
"gpui",
"workspace-hack",
]
@@ -4393,12 +4398,15 @@ dependencies = [
"futures 0.3.31",
"fuzzy",
"gpui",
"hex",
"indoc",
"itertools 0.14.0",
"language",
"log",
"menu",
"notifications",
"parking_lot",
"parse_int",
"paths",
"picker",
"pretty_assertions",
@@ -4522,6 +4530,27 @@ dependencies = [
"syn 2.0.101",
]
[[package]]
name = "derive_more"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "093242cf7570c207c83073cf82f79706fe7b8317e98620a47d5be7c3d8497678"
dependencies = [
"derive_more-impl",
]
[[package]]
name = "derive_more-impl"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.101",
"unicode-xid",
]
[[package]]
name = "derive_refineable"
version = "0.1.0"
@@ -6220,7 +6249,7 @@ dependencies = [
"askpass",
"async-trait",
"collections",
"derive_more",
"derive_more 0.99.19",
"futures 0.3.31",
"git2",
"gpui",
@@ -7237,7 +7266,7 @@ dependencies = [
"core-video",
"cosmic-text",
"ctor",
"derive_more",
"derive_more 0.99.19",
"embed-resource",
"env_logger 0.11.8",
"etagere",
@@ -7783,7 +7812,7 @@ version = "0.1.0"
dependencies = [
"anyhow",
"bytes 1.10.1",
"derive_more",
"derive_more 0.99.19",
"futures 0.3.31",
"http 1.3.1",
"log",
@@ -8221,7 +8250,7 @@ dependencies = [
"async-trait",
"cargo_metadata",
"collections",
"derive_more",
"derive_more 0.99.19",
"extension",
"fs",
"futures 0.3.31",
@@ -9033,7 +9062,6 @@ dependencies = [
"credentials_provider",
"deepseek",
"editor",
"feature_flags",
"fs",
"futures 0.3.31",
"google_ai",
@@ -9068,6 +9096,7 @@ dependencies = [
"util",
"vercel",
"workspace-hack",
"x_ai",
"zed_llm_client",
]
@@ -9660,12 +9689,11 @@ dependencies = [
[[package]]
name = "lsp-types"
version = "0.95.1"
source = "git+https://github.com/zed-industries/lsp-types?rev=c9c189f1c5dd53c624a419ce35bc77ad6a908d18#c9c189f1c5dd53c624a419ce35bc77ad6a908d18"
source = "git+https://github.com/zed-industries/lsp-types?rev=6add7052b598ea1f40f7e8913622c3958b009b60#6add7052b598ea1f40f7e8913622c3958b009b60"
dependencies = [
"bitflags 1.3.2",
"serde",
"serde_json",
"serde_repr",
"url",
]
@@ -11277,6 +11305,15 @@ dependencies = [
"windows-targets 0.52.6",
]
[[package]]
name = "parse_int"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1c464266693329dd5a8715098c7f86e6c5fd5d985018b8318f53d9c6c2b21a31"
dependencies = [
"num-traits",
]
[[package]]
name = "partial-json-fixer"
version = "0.5.3"
@@ -12320,6 +12357,7 @@ dependencies = [
"anyhow",
"askpass",
"async-trait",
"base64 0.22.1",
"buffer_diff",
"circular-buffer",
"client",
@@ -12365,6 +12403,7 @@ dependencies = [
"sha2",
"shellexpand 2.1.2",
"shlex",
"smallvec",
"smol",
"snippet",
"snippet_provider",
@@ -14099,7 +14138,7 @@ dependencies = [
[[package]]
name = "scap"
version = "0.0.8"
source = "git+https://github.com/zed-industries/scap?rev=28dd306ff2e3374404936dec778fc1e975b8dd12#28dd306ff2e3374404936dec778fc1e975b8dd12"
source = "git+https://github.com/zed-industries/scap?rev=270538dc780f5240723233ff901e1054641ed318#270538dc780f5240723233ff901e1054641ed318"
dependencies = [
"anyhow",
"cocoa 0.25.0",
@@ -14151,6 +14190,7 @@ dependencies = [
"indexmap",
"ref-cast",
"schemars_derive",
"semver",
"serde",
"serde_json",
]
@@ -14672,16 +14712,20 @@ dependencies = [
"language",
"log",
"menu",
"notifications",
"paths",
"project",
"schemars",
"search",
"serde",
"serde_json",
"settings",
"telemetry",
"theme",
"tree-sitter-json",
"tree-sitter-rust",
"ui",
"ui_input",
"util",
"workspace",
"workspace-hack",
@@ -16109,7 +16153,7 @@ version = "0.1.0"
dependencies = [
"anyhow",
"collections",
"derive_more",
"derive_more 0.99.19",
"fs",
"futures 0.3.31",
"gpui",
@@ -16408,6 +16452,7 @@ dependencies = [
"schemars",
"serde",
"settings",
"settings_ui",
"smallvec",
"story",
"telemetry",
@@ -18376,7 +18421,6 @@ version = "0.1.0"
dependencies = [
"anyhow",
"client",
"feature_flags",
"futures 0.3.31",
"gpui",
"http_client",
@@ -19695,9 +19739,7 @@ dependencies = [
"wasmtime-cranelift",
"wasmtime-environ",
"winapi",
"windows 0.61.1",
"windows-core 0.61.0",
"windows-future",
"windows-numerics",
"windows-sys 0.48.0",
"windows-sys 0.52.0",
@@ -19803,6 +19845,17 @@ version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec107c4503ea0b4a98ef47356329af139c0a4f7750e621cf2973cd3385ebcb3d"
[[package]]
name = "x_ai"
version = "0.1.0"
dependencies = [
"anyhow",
"schemars",
"serde",
"strum 0.27.1",
"workspace-hack",
]
[[package]]
name = "xattr"
version = "0.2.3"
@@ -20044,7 +20097,7 @@ dependencies = [
[[package]]
name = "zed"
version = "0.196.0"
version = "0.197.0"
dependencies = [
"activity_indicator",
"agent",

View File

@@ -179,6 +179,7 @@ members = [
"crates/welcome",
"crates/workspace",
"crates/worktree",
"crates/x_ai",
"crates/zed",
"crates/zed_actions",
"crates/zeta",
@@ -394,6 +395,7 @@ web_search_providers = { path = "crates/web_search_providers" }
welcome = { path = "crates/welcome" }
workspace = { path = "crates/workspace" }
worktree = { path = "crates/worktree" }
x_ai = { path = "crates/x_ai" }
zed = { path = "crates/zed" }
zed_actions = { path = "crates/zed_actions" }
zeta = { path = "crates/zeta" }
@@ -404,7 +406,7 @@ zlog_settings = { path = "crates/zlog_settings" }
# External crates
#
agentic-coding-protocol = "0.0.7"
agentic-coding-protocol = { version = "0.0.9" }
aho-corasick = "1.1"
alacritty_terminal = { git = "https://github.com/zed-industries/alacritty.git", branch = "add-hush-login-flag" }
any_vec = "0.14"
@@ -492,7 +494,7 @@ libc = "0.2"
libsqlite3-sys = { version = "0.30.1", features = ["bundled"] }
linkify = "0.10.0"
log = { version = "0.4.16", features = ["kv_unstable_serde", "serde"] }
lsp-types = { git = "https://github.com/zed-industries/lsp-types", rev = "c9c189f1c5dd53c624a419ce35bc77ad6a908d18" }
lsp-types = { git = "https://github.com/zed-industries/lsp-types", rev = "6add7052b598ea1f40f7e8913622c3958b009b60" }
markup5ever_rcdom = "0.3.0"
metal = "0.29"
moka = { version = "0.12.10", features = ["sync"] }
@@ -507,6 +509,7 @@ ordered-float = "2.1.1"
palette = { version = "0.7.5", default-features = false, features = ["std"] }
parking_lot = "0.12.1"
partial-json-fixer = "0.5.3"
parse_int = "0.9"
pathdiff = "0.2"
pet = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "845945b830297a50de0e24020b980a65e4820559" }
pet-conda = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "845945b830297a50de0e24020b980a65e4820559" }
@@ -546,7 +549,8 @@ rustc-demangle = "0.1.23"
rustc-hash = "2.1.0"
rustls = { version = "0.23.26" }
rustls-platform-verifier = "0.5.0"
scap = { git = "https://github.com/zed-industries/scap", rev = "28dd306ff2e3374404936dec778fc1e975b8dd12", default-features = false }
# When updating scap rev, also update it in .config/hakari.toml
scap = { git = "https://github.com/zed-industries/scap", rev = "270538dc780f5240723233ff901e1054641ed318", default-features = false }
schemars = { version = "1.0", features = ["indexmap2"] }
semver = "1.0"
serde = { version = "1.0", features = ["derive", "rc"] }

3
assets/icons/ai_x_ai.svg Normal file
View File

@@ -0,0 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="m12.414 5.47.27 9.641h2.157l.27-13.15zM15.11.889h-3.293L6.651 7.613l1.647 2.142zM.889 15.11H4.18l1.647-2.142-1.647-2.143zm0-9.641 7.409 9.641h3.292L4.181 5.47z" fill="#000"/>
</svg>

After

Width:  |  Height:  |  Size: 289 B

View File

@@ -1 +1,12 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-bug"><path d="m8 2 1.88 1.88"/><path d="M14.12 3.88 16 2"/><path d="M9 7.13v-1a3.003 3.003 0 1 1 6 0v1"/><path d="M12 20c-3.3 0-6-2.7-6-6v-3a4 4 0 0 1 4-4h4a4 4 0 0 1 4 4v3c0 3.3-2.7 6-6 6"/><path d="M12 20v-9"/><path d="M6.53 9C4.6 8.8 3 7.1 3 5"/><path d="M6 13H2"/><path d="M3 21c0-2.1 1.7-3.9 3.8-4"/><path d="M20.97 5c0 2.1-1.6 3.8-3.5 4"/><path d="M22 13h-4"/><path d="M17.2 17c2.1.1 3.8 1.9 3.8 4"/></svg>
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M5.49219 2.29071L6.41455 3.1933" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M9.61816 3.1933L10.508 2.29071" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M5.7042 5.89221V5.15749C5.69033 4.85975 5.73943 4.56239 5.84856 4.28336C5.95768 4.00434 6.12456 3.74943 6.33913 3.53402C6.55369 3.31862 6.81149 3.14718 7.09697 3.03005C7.38245 2.91292 7.68969 2.85254 8.00014 2.85254C8.3106 2.85254 8.61784 2.91292 8.90332 3.03005C9.18879 3.14718 9.44659 3.31862 9.66116 3.53402C9.87572 3.74943 10.0426 4.00434 10.1517 4.28336C10.2609 4.56239 10.31 4.85975 10.2961 5.15749V5.89221" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M8.00006 13.0426C6.13263 13.0426 4.60474 11.6005 4.60474 9.83792V8.23558C4.60474 7.66895 4.84322 7.12554 5.26772 6.72487C5.69221 6.32421 6.26796 6.09912 6.86829 6.09912H9.13184C9.73217 6.09912 10.3079 6.32421 10.7324 6.72487C11.1569 7.12554 11.3954 7.66895 11.3954 8.23558V9.83792C11.3954 11.6005 9.86749 13.0426 8.00006 13.0426Z" fill="black" fill-opacity="0.15" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M4.60452 6.25196C3.51235 6.13878 2.60693 5.17677 2.60693 3.9884" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M4.60462 8.81659H2.34106" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M2.4541 13.3186C2.4541 12.1302 3.41611 11.1116 4.60448 11.0551" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M13.0761 3.9884C13.0761 5.17677 12.1706 6.13878 11.0955 6.25196" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M13.6591 8.81659H11.3955" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M11.3955 11.0551C12.5839 11.1116 13.5459 12.1302 13.5459 13.3186" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 615 B

After

Width:  |  Height:  |  Size: 2.1 KiB

1
assets/icons/equal.svg Normal file
View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-equal-icon lucide-equal"><line x1="5" x2="19" y1="9" y2="9"/><line x1="5" x2="19" y1="15" y2="15"/></svg>

After

Width:  |  Height:  |  Size: 308 B

View File

@@ -1,5 +1,5 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3.03125 3V3.03125M3.03125 3.03125V9M3.03125 3.03125C3.03125 5 6 5 6 5M3.03125 9C3.03125 11 6 11 6 11M3.03125 9V12" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<rect x="8" y="2.5" width="6" height="5" rx="1.5" fill="black"/>
<rect x="8" y="8.46875" width="6" height="5.0625" rx="1.5" fill="black"/>
<path d="M3 3V3.03125M3 3.03125V9M3 3.03125C3 5 5.96875 5 5.96875 5M3 9C3 11 5.96875 11 5.96875 11M3 9V12" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<rect x="8" y="3" width="5.5" height="4" rx="1.5" fill="black"/>
<rect x="8" y="9" width="5.5" height="4" rx="1.5" fill="black"/>
</svg>

Before

Width:  |  Height:  |  Size: 462 B

After

Width:  |  Height:  |  Size: 423 B

View File

@@ -1,6 +1,7 @@
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M3.75 3.25C4.02614 3.25 4.25 3.02614 4.25 2.75C4.25 2.47386 4.02614 2.25 3.75 2.25C3.47386 2.25 3.25 2.47386 3.25 2.75C3.25 3.02614 3.47386 3.25 3.75 3.25ZM3.75 4.25C4.57843 4.25 5.25 3.57843 5.25 2.75C5.25 1.92157 4.57843 1.25 3.75 1.25C2.92157 1.25 2.25 1.92157 2.25 2.75C2.25 3.57843 2.92157 4.25 3.75 4.25Z" fill="black"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M8.25 3.25C8.52614 3.25 8.75 3.02614 8.75 2.75C8.75 2.47386 8.52614 2.25 8.25 2.25C7.97386 2.25 7.75 2.47386 7.75 2.75C7.75 3.02614 7.97386 3.25 8.25 3.25ZM8.25 4.25C9.07843 4.25 9.75 3.57843 9.75 2.75C9.75 1.92157 9.07843 1.25 8.25 1.25C7.42157 1.25 6.75 1.92157 6.75 2.75C6.75 3.57843 7.42157 4.25 8.25 4.25Z" fill="black"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M3.75 9.75C4.02614 9.75 4.25 9.52614 4.25 9.25C4.25 8.97386 4.02614 8.75 3.75 8.75C3.47386 8.75 3.25 8.97386 3.25 9.25C3.25 9.52614 3.47386 9.75 3.75 9.75ZM3.75 10.75C4.57843 10.75 5.25 10.0784 5.25 9.25C5.25 8.42157 4.57843 7.75 3.75 7.75C2.92157 7.75 2.25 8.42157 2.25 9.25C2.25 10.0784 2.92157 10.75 3.75 10.75Z" fill="black"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M3.25 3.75H4.25V5.59609C4.67823 5.35824 5.24991 5.25 6 5.25H7.25017C7.5262 5.25 7.75 5.02625 7.75 4.75V3.75H8.75V4.75C8.75 5.57832 8.07871 6.25 7.25017 6.25H6C5.14559 6.25 4.77639 6.41132 4.59684 6.56615C4.42571 6.71373 4.33877 6.92604 4.25 7.30651V8.25H3.25V3.75Z" fill="black"/>
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="5" cy="12" r="1.25" stroke="black" stroke-width="1.5"/>
<path d="M5 11V5" stroke="black" stroke-width="1.5"/>
<path d="M5 10C5 10 5.5 8 7 8C7.73103 8 8.69957 8 9.50049 8C10.3289 8 11 7.32843 11 6.5V5" stroke="black" stroke-width="1.5"/>
<circle cx="5" cy="4" r="1.25" stroke="black" stroke-width="1.5"/>
<circle cx="11" cy="4" r="1.25" stroke="black" stroke-width="1.5"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 487 B

View File

@@ -1 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-list-tree"><path d="M21 12h-8"/><path d="M21 6H8"/><path d="M21 18h-8"/><path d="M3 6v4c0 1.1.9 2 2 2h3"/><path d="M3 10v6c0 1.1.9 2 2 2h3"/></svg>
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M13.5 8H9.5" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M13.5 4L6.5 4" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M13.5 12H9.5" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M3 3.5V6.33333C3 7.25 3.72 8 4.6 8H7" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M3 6V10.5C3 11.325 3.72 12 4.6 12H7" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 349 B

After

Width:  |  Height:  |  Size: 680 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-location-edit-icon lucide-location-edit"><path d="M17.97 9.304A8 8 0 0 0 2 10c0 4.69 4.887 9.562 7.022 11.468"/><path d="M21.378 16.626a1 1 0 0 0-3.004-3.004l-4.01 4.012a2 2 0 0 0-.506.854l-.837 2.87a.5.5 0 0 0 .62.62l2.87-.837a2 2 0 0 0 .854-.506z"/><circle cx="10" cy="10" r="3"/></svg>

After

Width:  |  Height:  |  Size: 491 B

View File

@@ -0,0 +1,3 @@
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M5 4L10 7L5 10V4Z" fill="black" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 227 B

View File

@@ -0,0 +1,5 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M11.8889 3H4.11111C3.49746 3 3 3.49746 3 4.11111V11.8889C3 12.5025 3.49746 13 4.11111 13H11.8889C12.5025 13 13 12.5025 13 11.8889V4.11111C13 3.49746 12.5025 3 11.8889 3Z" fill="black" fill-opacity="0.15" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M8.37939 10.3243H10.3794" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M5.64966 9.32837L7.64966 7.32837L5.64966 5.32837" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 659 B

View File

@@ -1,3 +1,5 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M5.9 8.00002C7.44656 8.00002 8.7 6.74637 8.7 5.20002C8.7 3.65368 7.44656 2.40002 5.9 2.40002C4.35344 2.40002 3.1 3.65368 3.1 5.20002C3.1 6.74637 4.35344 8.00002 5.9 8.00002ZM7.00906 9.05002H4.79094C2.69684 9.05002 1 10.7475 1 12.841C1 13.261 1.3395 13.6 1.75819 13.6H10.0409C10.4609 13.6 10.8 13.261 10.8 12.841C10.8 10.7475 9.1025 9.05002 7.00906 9.05002ZM11.4803 9.40002H9.86484C10.87 10.2247 11.5 11.4585 11.5 12.841C11.5 13.121 11.4169 13.3791 11.2812 13.6H14.3C14.6872 13.6 15 13.285 15 12.8803C15 10.9663 13.4338 9.40002 11.4803 9.40002ZM10.45 8.00002C11.8041 8.00002 12.9 6.90409 12.9 5.55002C12.9 4.19596 11.8041 3.10002 10.45 3.10002C9.90072 3.10002 9.39913 3.28716 8.9905 3.59243C9.2425 4.07631 9.4 4.61815 9.4 5.20002C9.4 5.97702 9.13903 6.69059 8.70897 7.27181C9.15281 7.72002 9.7675 8.00002 10.45 8.00002Z" fill="white"/>
<path d="M6.79118 8.27005C8.27568 8.27005 9.4791 7.06663 9.4791 5.58214C9.4791 4.09765 8.27568 2.89423 6.79118 2.89423C5.30669 2.89423 4.10327 4.09765 4.10327 5.58214C4.10327 7.06663 5.30669 8.27005 6.79118 8.27005Z" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M6.79112 8.60443C4.19441 8.60443 2.08936 10.7095 2.08936 13.3062H11.4929C11.4929 10.7095 9.38784 8.60443 6.79112 8.60443Z" fill="black" fill-opacity="0.15" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M14.6984 12.9263C14.6984 10.8893 13.4895 8.99736 12.2806 8.09067C12.6779 7.79254 12.9957 7.40104 13.2057 6.95083C13.4157 6.50062 13.5115 6.00558 13.4846 5.50952C13.4577 5.01346 13.309 4.53168 13.0515 4.10681C12.7941 3.68194 12.4358 3.3271 12.0085 3.07367" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 947 B

After

Width:  |  Height:  |  Size: 999 B

View File

@@ -1,5 +1,5 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8 2L6.72534 5.87534C6.6601 6.07367 6.5492 6.25392 6.40155 6.40155C6.25392 6.5492 6.07367 6.6601 5.87534 6.72534L2 8L5.87534 9.27466C6.07367 9.3399 6.25392 9.4508 6.40155 9.59845C6.5492 9.74608 6.6601 9.92633 6.72534 10.1247L8 14L9.27466 10.1247C9.3399 9.92633 9.4508 9.74608 9.59845 9.59845C9.74608 9.4508 9.92633 9.3399 10.1247 9.27466L14 8L10.1247 6.72534C9.92633 6.6601 9.74608 6.5492 9.59845 6.40155C9.4508 6.25392 9.3399 6.07367 9.27466 5.87534L8 2Z" fill="black" fill-opacity="0.15" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M8 2.93652L6.9243 6.20697C6.86924 6.37435 6.77565 6.52646 6.65105 6.65105C6.52646 6.77565 6.37435 6.86924 6.20697 6.9243L2.93652 8L6.20697 9.0757C6.37435 9.13076 6.52646 9.22435 6.65105 9.34895C6.77565 9.47354 6.86924 9.62565 6.9243 9.79306L8 13.0635L9.0757 9.79306C9.13076 9.62565 9.22435 9.47354 9.34895 9.34895C9.47354 9.22435 9.62565 9.13076 9.79306 9.0757L13.0635 8L9.79306 6.9243C9.62565 6.86924 9.47354 6.77565 9.34895 6.65105C9.22435 6.52646 9.13076 6.37435 9.0757 6.20697L8 2.93652Z" fill="black" fill-opacity="0.15" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M3.33334 2V4.66666M2 3.33334H4.66666" stroke="black" stroke-opacity="0.75" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M12.6665 11.3333V14M11.3333 12.6666H13.9999" stroke="black" stroke-opacity="0.75" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 998 B

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@@ -475,8 +475,8 @@
"ctrl-/": ["editor::ToggleComments", { "advance_downwards": false }],
"ctrl-u": "editor::UndoSelection",
"ctrl-shift-u": "editor::RedoSelection",
"f8": "editor::GoToDiagnostic",
"shift-f8": "editor::GoToPreviousDiagnostic",
"f8": ["editor::GoToDiagnostic", { "severity": { "min": "hint", "max": "error" } }],
"shift-f8": ["editor::GoToPreviousDiagnostic", { "severity": { "min": "hint", "max": "error" } }],
"f2": "editor::Rename",
"f12": "editor::GoToDefinition",
"alt-f12": "editor::GoToDefinitionSplit",
@@ -586,7 +586,7 @@
"ctrl-shift-f": "pane::DeploySearch",
"ctrl-shift-h": ["pane::DeploySearch", { "replace_enabled": true }],
"ctrl-shift-t": "pane::ReopenClosedItem",
"ctrl-k ctrl-s": "zed::OpenKeymap",
"ctrl-k ctrl-s": "zed::OpenKeymapEditor",
"ctrl-k ctrl-t": "theme_selector::Toggle",
"ctrl-t": "project_symbols::Toggle",
"ctrl-p": "file_finder::Toggle",
@@ -856,6 +856,7 @@
"alt-shift-y": "git::UnstageFile",
"ctrl-alt-y": "git::ToggleStaged",
"space": "git::ToggleStaged",
"shift-space": "git::StageRange",
"tab": "git_panel::FocusEditor",
"shift-tab": "git_panel::FocusEditor",
"escape": "git_panel::ToggleFocus",
@@ -998,6 +999,7 @@
{
"context": "FileFinder || (FileFinder > Picker > Editor)",
"bindings": {
"ctrl-p": "file_finder::Toggle",
"ctrl-shift-a": "file_finder::ToggleSplitMenu",
"ctrl-shift-i": "file_finder::ToggleFilterMenu"
}
@@ -1118,5 +1120,30 @@
"alt-ctrl-f": "keymap_editor::ToggleKeystrokeSearch",
"alt-c": "keymap_editor::ToggleConflictFilter"
}
},
{
"context": "KeystrokeInput",
"use_key_equivalents": true,
"bindings": {
"enter": "keystroke_input::StartRecording",
"escape escape escape": "keystroke_input::StopRecording",
"delete": "keystroke_input::ClearKeystrokes"
}
},
{
"context": "KeybindEditorModal",
"use_key_equivalents": true,
"bindings": {
"ctrl-enter": "menu::Confirm",
"escape": "menu::Cancel"
}
},
{
"context": "KeybindEditorModal > Editor",
"use_key_equivalents": true,
"bindings": {
"up": "menu::SelectPrevious",
"down": "menu::SelectNext"
}
}
]

View File

@@ -528,8 +528,8 @@
"cmd-/": ["editor::ToggleComments", { "advance_downwards": false }],
"cmd-u": "editor::UndoSelection",
"cmd-shift-u": "editor::RedoSelection",
"f8": "editor::GoToDiagnostic",
"shift-f8": "editor::GoToPreviousDiagnostic",
"f8": ["editor::GoToDiagnostic", { "severity": { "min": "hint", "max": "error" } }],
"shift-f8": ["editor::GoToPreviousDiagnostic", { "severity": { "min": "hint", "max": "error" } }],
"f2": "editor::Rename",
"f12": "editor::GoToDefinition",
"alt-f12": "editor::GoToDefinitionSplit",
@@ -652,7 +652,7 @@
"cmd-shift-f": "pane::DeploySearch",
"cmd-shift-h": ["pane::DeploySearch", { "replace_enabled": true }],
"cmd-shift-t": "pane::ReopenClosedItem",
"cmd-k cmd-s": "zed::OpenKeymap",
"cmd-k cmd-s": "zed::OpenKeymapEditor",
"cmd-k cmd-t": "theme_selector::Toggle",
"cmd-t": "project_symbols::Toggle",
"cmd-p": "file_finder::Toggle",
@@ -930,6 +930,7 @@
"enter": "menu::Confirm",
"cmd-alt-y": "git::ToggleStaged",
"space": "git::ToggleStaged",
"shift-space": "git::StageRange",
"cmd-y": "git::StageFile",
"cmd-shift-y": "git::UnstageFile",
"alt-down": "git_panel::FocusEditor",
@@ -1097,13 +1098,16 @@
"ctrl-cmd-space": "terminal::ShowCharacterPalette",
"cmd-c": "terminal::Copy",
"cmd-v": "terminal::Paste",
"cmd-f": "buffer_search::Deploy",
"cmd-a": "editor::SelectAll",
"cmd-k": "terminal::Clear",
"cmd-n": "workspace::NewTerminal",
"ctrl-enter": "assistant::InlineAssist",
"ctrl-_": null, // emacs undo
// Some nice conveniences
"cmd-backspace": ["terminal::SendText", "\u0015"],
"cmd-backspace": ["terminal::SendText", "\u0015"], // ctrl-u: clear line
"alt-delete": ["terminal::SendText", "\u001bd"], // alt-d: delete word forward
"cmd-delete": ["terminal::SendText", "\u000b"], // ctrl-k: delete to end of line
"cmd-right": ["terminal::SendText", "\u0005"],
"cmd-left": ["terminal::SendText", "\u0001"],
// Terminal.app compatibility
@@ -1215,5 +1219,30 @@
"cmd-alt-f": "keymap_editor::ToggleKeystrokeSearch",
"cmd-alt-c": "keymap_editor::ToggleConflictFilter"
}
},
{
"context": "KeystrokeInput",
"use_key_equivalents": true,
"bindings": {
"enter": "keystroke_input::StartRecording",
"escape escape escape": "keystroke_input::StopRecording",
"delete": "keystroke_input::ClearKeystrokes"
}
},
{
"context": "KeybindEditorModal",
"use_key_equivalents": true,
"bindings": {
"cmd-enter": "menu::Confirm",
"escape": "menu::Cancel"
}
},
{
"context": "KeybindEditorModal > Editor",
"use_key_equivalents": true,
"bindings": {
"up": "menu::SelectPrevious",
"down": "menu::SelectNext"
}
}
]

View File

@@ -466,7 +466,7 @@
}
},
{
"context": "vim_mode == insert && showing_signature_help && !showing_completions",
"context": "(vim_mode == insert || vim_mode == normal) && showing_signature_help && !showing_completions",
"bindings": {
"ctrl-p": "editor::SignatureHelpPrevious",
"ctrl-n": "editor::SignatureHelpNext"
@@ -841,6 +841,7 @@
"i": "git_panel::FocusEditor",
"x": "git::ToggleStaged",
"shift-x": "git::StageAll",
"g x": "git::StageRange",
"shift-u": "git::UnstageAll"
}
},

View File

@@ -84,7 +84,7 @@
"bottom_dock_layout": "contained",
// The direction that you want to split panes horizontally. Defaults to "up"
"pane_split_direction_horizontal": "up",
// The direction that you want to split panes horizontally. Defaults to "left"
// The direction that you want to split panes vertically. Defaults to "left"
"pane_split_direction_vertical": "left",
// Centered layout related settings.
"centered_layout": {
@@ -1135,6 +1135,7 @@
"**/.svn",
"**/.hg",
"**/.jj",
"**/.repo",
"**/CVS",
"**/.DS_Store",
"**/Thumbs.db",
@@ -1670,6 +1671,10 @@
"allowed": true
}
},
"SystemVerilog": {
"format_on_save": "off",
"use_on_type_format": false
},
"Vue.js": {
"language_servers": ["vue-language-server", "..."],
"prettier": {

View File

@@ -1,10 +1,10 @@
pub use acp::ToolCallId;
use agent_servers::AgentServer;
use agentic_coding_protocol::{self as acp, UserMessageChunk};
use agentic_coding_protocol::{self as acp, ToolCallLocation, UserMessageChunk};
use anyhow::{Context as _, Result, anyhow};
use assistant_tool::ActionLog;
use buffer_diff::BufferDiff;
use editor::{MultiBuffer, PathKey};
use editor::{Bias, MultiBuffer, PathKey};
use futures::{FutureExt, channel::oneshot, future::BoxFuture};
use gpui::{AppContext, AsyncApp, Context, Entity, EventEmitter, SharedString, Task, WeakEntity};
use itertools::Itertools;
@@ -176,6 +176,14 @@ impl AgentThreadEntry {
None
}
}
pub fn locations(&self) -> Option<&[acp::ToolCallLocation]> {
if let AgentThreadEntry::ToolCall(ToolCall { locations, .. }) = self {
Some(locations)
} else {
None
}
}
}
#[derive(Debug)]
@@ -761,6 +769,11 @@ impl AcpThread {
status,
};
let location = call.locations.last().cloned();
if let Some(location) = location {
self.set_project_location(location, cx)
}
self.push_entry(AgentThreadEntry::ToolCall(call), cx);
id
@@ -823,6 +836,11 @@ impl AcpThread {
}
}
let location = call.locations.last().cloned();
if let Some(location) = location {
self.set_project_location(location, cx)
}
cx.emit(AcpThreadEvent::EntryUpdated(ix));
Ok(())
}
@@ -844,6 +862,37 @@ impl AcpThread {
}
}
pub fn set_project_location(&self, location: ToolCallLocation, cx: &mut Context<Self>) {
self.project.update(cx, |project, cx| {
let Some(path) = project.project_path_for_absolute_path(&location.path, cx) else {
return;
};
let buffer = project.open_buffer(path, cx);
cx.spawn(async move |project, cx| {
let buffer = buffer.await?;
project.update(cx, |project, cx| {
let position = if let Some(line) = location.line {
let snapshot = buffer.read(cx).snapshot();
let point = snapshot.clip_point(Point::new(line, 0), Bias::Left);
snapshot.anchor_before(point)
} else {
Anchor::MIN
};
project.set_agent_location(
Some(AgentLocation {
buffer: buffer.downgrade(),
position,
}),
cx,
);
})
})
.detach_and_log_err(cx);
});
}
/// Returns true if the last turn is awaiting tool authorization
pub fn waiting_for_tool_confirmation(&self) -> bool {
for entry in self.entries.iter().rev() {
@@ -867,7 +916,7 @@ impl AcpThread {
&self,
) -> impl use<> + Future<Output = Result<acp::InitializeResponse, acp::Error>> {
let connection = self.connection.clone();
async move { connection.request(acp::InitializeParams).await }
async move { connection.initialize().await }
}
pub fn authenticate(&self) -> impl use<> + Future<Output = Result<(), acp::Error>> {
@@ -1772,7 +1821,7 @@ mod tests {
Ok(AgentServerCommand {
path: "node".into(),
args: vec![cli_path, "--acp".into()],
args: vec![cli_path, "--experimental-acp".into()],
env: None,
})
}
@@ -1831,8 +1880,12 @@ mod tests {
}
impl acp::Agent for FakeAgent {
async fn initialize(&self) -> Result<acp::InitializeResponse, acp::Error> {
async fn initialize(
&self,
params: acp::InitializeParams,
) -> Result<acp::InitializeResponse, acp::Error> {
Ok(acp::InitializeResponse {
protocol_version: params.protocol_version,
is_authenticated: true,
})
}

View File

@@ -448,7 +448,7 @@ impl ActivityIndicator {
.into_any_element(),
),
message: format!("Debug: {}", session.read(cx).adapter()),
tooltip_message: Some(session.read(cx).label().to_string()),
tooltip_message: session.read(cx).label().map(|label| label.to_string()),
on_click: None,
});
}

View File

@@ -21,6 +21,7 @@ use gpui::{
AnyWindowHandle, App, AppContext, AsyncApp, Context, Entity, EventEmitter, SharedString, Task,
WeakEntity, Window,
};
use http_client::StatusCode;
use language_model::{
ConfiguredModel, LanguageModel, LanguageModelCompletionError, LanguageModelCompletionEvent,
LanguageModelExt as _, LanguageModelId, LanguageModelRegistry, LanguageModelRequest,
@@ -51,7 +52,19 @@ use uuid::Uuid;
use zed_llm_client::{CompletionIntent, CompletionRequestStatus, UsageLimit};
const MAX_RETRY_ATTEMPTS: u8 = 3;
const BASE_RETRY_DELAY_SECS: u64 = 5;
const BASE_RETRY_DELAY: Duration = Duration::from_secs(5);
#[derive(Debug, Clone)]
enum RetryStrategy {
ExponentialBackoff {
initial_delay: Duration,
max_attempts: u8,
},
Fixed {
delay: Duration,
max_attempts: u8,
},
}
#[derive(
Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Serialize, Deserialize, JsonSchema,
@@ -1519,7 +1532,9 @@ impl Thread {
) -> Option<PendingToolUse> {
let action_log = self.action_log.read(cx);
action_log.unnotified_stale_buffers(cx).next()?;
if !action_log.has_unnotified_user_edits() {
return None;
}
// Represent notification as a simulated `project_notifications` tool call
let tool_name = Arc::from("project_notifications");
@@ -1933,18 +1948,6 @@ impl Thread {
project.set_agent_location(None, cx);
});
fn emit_generic_error(error: &anyhow::Error, cx: &mut Context<Thread>) {
let error_message = error
.chain()
.map(|err| err.to_string())
.collect::<Vec<_>>()
.join("\n");
cx.emit(ThreadEvent::ShowError(ThreadError::Message {
header: "Error interacting with language model".into(),
message: SharedString::from(error_message.clone()),
}));
}
if error.is::<PaymentRequiredError>() {
cx.emit(ThreadEvent::ShowError(ThreadError::PaymentRequired));
} else if let Some(error) =
@@ -1956,9 +1959,10 @@ impl Thread {
} else if let Some(completion_error) =
error.downcast_ref::<LanguageModelCompletionError>()
{
use LanguageModelCompletionError::*;
match &completion_error {
PromptTooLarge { tokens, .. } => {
LanguageModelCompletionError::PromptTooLarge {
tokens, ..
} => {
let tokens = tokens.unwrap_or_else(|| {
// We didn't get an exact token count from the API, so fall back on our estimate.
thread
@@ -1979,63 +1983,22 @@ impl Thread {
});
cx.notify();
}
RateLimitExceeded {
retry_after: Some(retry_after),
..
}
| ServerOverloaded {
retry_after: Some(retry_after),
..
} => {
thread.handle_rate_limit_error(
&completion_error,
*retry_after,
model.clone(),
intent,
window,
cx,
);
retry_scheduled = true;
}
RateLimitExceeded { .. } | ServerOverloaded { .. } => {
retry_scheduled = thread.handle_retryable_error(
&completion_error,
model.clone(),
intent,
window,
cx,
);
if !retry_scheduled {
emit_generic_error(error, cx);
_ => {
if let Some(retry_strategy) =
Thread::get_retry_strategy(completion_error)
{
retry_scheduled = thread
.handle_retryable_error_with_delay(
&completion_error,
Some(retry_strategy),
model.clone(),
intent,
window,
cx,
);
}
}
ApiInternalServerError { .. }
| ApiReadResponseError { .. }
| HttpSend { .. } => {
retry_scheduled = thread.handle_retryable_error(
&completion_error,
model.clone(),
intent,
window,
cx,
);
if !retry_scheduled {
emit_generic_error(error, cx);
}
}
NoApiKey { .. }
| HttpResponseError { .. }
| BadRequestFormat { .. }
| AuthenticationError { .. }
| PermissionError { .. }
| ApiEndpointNotFound { .. }
| SerializeRequest { .. }
| BuildRequestBody { .. }
| DeserializeResponse { .. }
| Other { .. } => emit_generic_error(error, cx),
}
} else {
emit_generic_error(error, cx);
}
if !retry_scheduled {
@@ -2162,73 +2125,86 @@ impl Thread {
});
}
fn handle_rate_limit_error(
&mut self,
error: &LanguageModelCompletionError,
retry_after: Duration,
model: Arc<dyn LanguageModel>,
intent: CompletionIntent,
window: Option<AnyWindowHandle>,
cx: &mut Context<Self>,
) {
// For rate limit errors, we only retry once with the specified duration
let retry_message = format!("{error}. Retrying in {} seconds…", retry_after.as_secs());
log::warn!(
"Retrying completion request in {} seconds: {error:?}",
retry_after.as_secs(),
);
fn get_retry_strategy(error: &LanguageModelCompletionError) -> Option<RetryStrategy> {
use LanguageModelCompletionError::*;
// Add a UI-only message instead of a regular message
let id = self.next_message_id.post_inc();
self.messages.push(Message {
id,
role: Role::System,
segments: vec![MessageSegment::Text(retry_message)],
loaded_context: LoadedContext::default(),
creases: Vec::new(),
is_hidden: false,
ui_only: true,
});
cx.emit(ThreadEvent::MessageAdded(id));
// Schedule the retry
let thread_handle = cx.entity().downgrade();
cx.spawn(async move |_thread, cx| {
cx.background_executor().timer(retry_after).await;
thread_handle
.update(cx, |thread, cx| {
// Retry the completion
thread.send_to_model(model, intent, window, cx);
// General strategy here:
// - If retrying won't help (e.g. invalid API key or payload too large), return None so we don't retry at all.
// - If it's a time-based issue (e.g. server overloaded, rate limit exceeded), try multiple times with exponential backoff.
// - If it's an issue that *might* be fixed by retrying (e.g. internal server error), just retry once.
match error {
HttpResponseError {
status_code: StatusCode::TOO_MANY_REQUESTS,
..
} => Some(RetryStrategy::ExponentialBackoff {
initial_delay: BASE_RETRY_DELAY,
max_attempts: MAX_RETRY_ATTEMPTS,
}),
ServerOverloaded { retry_after, .. } | RateLimitExceeded { retry_after, .. } => {
Some(RetryStrategy::Fixed {
delay: retry_after.unwrap_or(BASE_RETRY_DELAY),
max_attempts: MAX_RETRY_ATTEMPTS,
})
.log_err();
})
.detach();
}
fn handle_retryable_error(
&mut self,
error: &LanguageModelCompletionError,
model: Arc<dyn LanguageModel>,
intent: CompletionIntent,
window: Option<AnyWindowHandle>,
cx: &mut Context<Self>,
) -> bool {
self.handle_retryable_error_with_delay(error, None, model, intent, window, cx)
}
ApiInternalServerError { .. } => Some(RetryStrategy::Fixed {
delay: BASE_RETRY_DELAY,
max_attempts: 1,
}),
ApiReadResponseError { .. }
| HttpSend { .. }
| DeserializeResponse { .. }
| BadRequestFormat { .. } => Some(RetryStrategy::Fixed {
delay: BASE_RETRY_DELAY,
max_attempts: 1,
}),
// Retrying these errors definitely shouldn't help.
HttpResponseError {
status_code:
StatusCode::PAYLOAD_TOO_LARGE | StatusCode::FORBIDDEN | StatusCode::UNAUTHORIZED,
..
}
| SerializeRequest { .. }
| BuildRequestBody { .. }
| PromptTooLarge { .. }
| AuthenticationError { .. }
| PermissionError { .. }
| ApiEndpointNotFound { .. }
| NoApiKey { .. } => None,
// Retry all other 4xx and 5xx errors once.
HttpResponseError { status_code, .. }
if status_code.is_client_error() || status_code.is_server_error() =>
{
Some(RetryStrategy::Fixed {
delay: BASE_RETRY_DELAY,
max_attempts: 1,
})
}
// Conservatively assume that any other errors are non-retryable
HttpResponseError { .. } | Other(..) => None,
}
}
fn handle_retryable_error_with_delay(
&mut self,
error: &LanguageModelCompletionError,
custom_delay: Option<Duration>,
strategy: Option<RetryStrategy>,
model: Arc<dyn LanguageModel>,
intent: CompletionIntent,
window: Option<AnyWindowHandle>,
cx: &mut Context<Self>,
) -> bool {
let Some(strategy) = strategy.or_else(|| Self::get_retry_strategy(error)) else {
return false;
};
let max_attempts = match &strategy {
RetryStrategy::ExponentialBackoff { max_attempts, .. } => *max_attempts,
RetryStrategy::Fixed { max_attempts, .. } => *max_attempts,
};
let retry_state = self.retry_state.get_or_insert(RetryState {
attempt: 0,
max_attempts: MAX_RETRY_ATTEMPTS,
max_attempts,
intent,
});
@@ -2238,20 +2214,24 @@ impl Thread {
let intent = retry_state.intent;
if attempt <= max_attempts {
// Use custom delay if provided (e.g., from rate limit), otherwise exponential backoff
let delay = if let Some(custom_delay) = custom_delay {
custom_delay
} else {
let delay_secs = BASE_RETRY_DELAY_SECS * 2u64.pow((attempt - 1) as u32);
Duration::from_secs(delay_secs)
let delay = match &strategy {
RetryStrategy::ExponentialBackoff { initial_delay, .. } => {
let delay_secs = initial_delay.as_secs() * 2u64.pow((attempt - 1) as u32);
Duration::from_secs(delay_secs)
}
RetryStrategy::Fixed { delay, .. } => *delay,
};
// Add a transient message to inform the user
let delay_secs = delay.as_secs();
let retry_message = format!(
"{error}. Retrying (attempt {attempt} of {max_attempts}) \
in {delay_secs} seconds..."
);
let retry_message = if max_attempts == 1 {
format!("{error}. Retrying in {delay_secs} seconds...")
} else {
format!(
"{error}. Retrying (attempt {attempt} of {max_attempts}) \
in {delay_secs} seconds..."
)
};
log::warn!(
"Retrying completion request (attempt {attempt} of {max_attempts}) \
in {delay_secs} seconds: {error:?}",
@@ -2290,19 +2270,9 @@ impl Thread {
// Max retries exceeded
self.retry_state = None;
let notification_text = if max_attempts == 1 {
"Failed after retrying.".into()
} else {
format!("Failed after retrying {} times.", max_attempts).into()
};
// Stop generating since we're giving up on retrying.
self.pending_completions.clear();
cx.emit(ThreadEvent::RetriesFailed {
message: notification_text,
});
false
}
}
@@ -3258,9 +3228,6 @@ pub enum ThreadEvent {
CancelEditing,
CompletionCanceled,
ProfileChanged,
RetriesFailed {
message: SharedString,
},
}
impl EventEmitter<ThreadEvent> for Thread {}
@@ -3288,7 +3255,6 @@ mod tests {
use futures::stream::BoxStream;
use gpui::TestAppContext;
use http_client;
use indoc::indoc;
use language_model::fake_provider::{FakeLanguageModel, FakeLanguageModelProvider};
use language_model::{
LanguageModelCompletionError, LanguageModelName, LanguageModelProviderId,
@@ -3649,6 +3615,7 @@ fn main() {{
cx,
);
});
cx.run_until_parked();
// We shouldn't have a stale buffer notification yet
let notifications = thread.read_with(cx, |thread, _| {
@@ -3678,11 +3645,13 @@ fn main() {{
cx,
)
});
cx.run_until_parked();
// Check for the stale buffer warning
thread.update(cx, |thread, cx| {
thread.flush_notifications(model.clone(), CompletionIntent::UserPrompt, cx)
});
cx.run_until_parked();
let notifications = thread.read_with(cx, |thread, _cx| {
find_tool_uses(thread, "project_notifications")
@@ -3696,12 +3665,8 @@ fn main() {{
panic!("`project_notifications` should return text");
};
let expected_content = indoc! {"[The following is an auto-generated notification; do not reply]
These files have changed since the last read:
- code.rs
"};
assert_eq!(notification_content, expected_content);
assert!(notification_content.contains("These files have changed since the last read:"));
assert!(notification_content.contains("code.rs"));
// Insert another user message and flush notifications again
thread.update(cx, |thread, cx| {
@@ -3717,6 +3682,7 @@ fn main() {{
thread.update(cx, |thread, cx| {
thread.flush_notifications(model.clone(), CompletionIntent::UserPrompt, cx)
});
cx.run_until_parked();
// There should be no new notifications (we already flushed one)
let notifications = thread.read_with(cx, |thread, _cx| {
@@ -4192,7 +4158,7 @@ fn main() {{
assert_eq!(retry_state.attempt, 1, "Should be first retry attempt");
assert_eq!(
retry_state.max_attempts, MAX_RETRY_ATTEMPTS,
"Should have default max attempts"
"Should retry MAX_RETRY_ATTEMPTS times for overloaded errors"
);
});
@@ -4265,7 +4231,7 @@ fn main() {{
let retry_state = thread.retry_state.as_ref().unwrap();
assert_eq!(retry_state.attempt, 1, "Should be first retry attempt");
assert_eq!(
retry_state.max_attempts, MAX_RETRY_ATTEMPTS,
retry_state.max_attempts, 1,
"Should have correct max attempts"
);
});
@@ -4281,8 +4247,8 @@ fn main() {{
if let MessageSegment::Text(text) = seg {
text.contains("internal")
&& text.contains("Fake")
&& text
.contains(&format!("attempt 1 of {}", MAX_RETRY_ATTEMPTS))
&& text.contains("Retrying in")
&& !text.contains("attempt")
} else {
false
}
@@ -4320,8 +4286,8 @@ fn main() {{
let project = create_test_project(cx, json!({})).await;
let (_, _, thread, _, _base_model) = setup_test_environment(cx, project.clone()).await;
// Create model that returns overloaded error
let model = Arc::new(ErrorInjector::new(TestError::Overloaded));
// Create model that returns internal server error
let model = Arc::new(ErrorInjector::new(TestError::InternalServerError));
// Insert a user message
thread.update(cx, |thread, cx| {
@@ -4371,11 +4337,14 @@ fn main() {{
assert!(thread.retry_state.is_some(), "Should have retry state");
let retry_state = thread.retry_state.as_ref().unwrap();
assert_eq!(retry_state.attempt, 1, "Should be first retry attempt");
assert_eq!(
retry_state.max_attempts, 1,
"Internal server errors should only retry once"
);
});
// Advance clock for first retry
cx.executor()
.advance_clock(Duration::from_secs(BASE_RETRY_DELAY_SECS));
cx.executor().advance_clock(BASE_RETRY_DELAY);
cx.run_until_parked();
// Should have scheduled second retry - count retry messages
@@ -4395,93 +4364,25 @@ fn main() {{
})
.count()
});
assert_eq!(retry_count, 2, "Should have scheduled second retry");
// Check retry state updated
thread.read_with(cx, |thread, _| {
assert!(thread.retry_state.is_some(), "Should have retry state");
let retry_state = thread.retry_state.as_ref().unwrap();
assert_eq!(retry_state.attempt, 2, "Should be second retry attempt");
assert_eq!(
retry_state.max_attempts, MAX_RETRY_ATTEMPTS,
"Should have correct max attempts"
);
});
// Advance clock for second retry (exponential backoff)
cx.executor()
.advance_clock(Duration::from_secs(BASE_RETRY_DELAY_SECS * 2));
cx.run_until_parked();
// Should have scheduled third retry
// Count all retry messages now
let retry_count = thread.update(cx, |thread, _| {
thread
.messages
.iter()
.filter(|m| {
m.ui_only
&& m.segments.iter().any(|s| {
if let MessageSegment::Text(text) = s {
text.contains("Retrying") && text.contains("seconds")
} else {
false
}
})
})
.count()
});
assert_eq!(
retry_count, MAX_RETRY_ATTEMPTS as usize,
"Should have scheduled third retry"
retry_count, 1,
"Should have only one retry for internal server errors"
);
// Check retry state updated
// For internal server errors, we only retry once and then give up
// Check that retry_state is cleared after the single retry
thread.read_with(cx, |thread, _| {
assert!(thread.retry_state.is_some(), "Should have retry state");
let retry_state = thread.retry_state.as_ref().unwrap();
assert_eq!(
retry_state.attempt, MAX_RETRY_ATTEMPTS,
"Should be at max retry attempt"
);
assert_eq!(
retry_state.max_attempts, MAX_RETRY_ATTEMPTS,
"Should have correct max attempts"
assert!(
thread.retry_state.is_none(),
"Retry state should be cleared after single retry"
);
});
// Advance clock for third retry (exponential backoff)
cx.executor()
.advance_clock(Duration::from_secs(BASE_RETRY_DELAY_SECS * 4));
cx.run_until_parked();
// No more retries should be scheduled after clock was advanced.
let retry_count = thread.update(cx, |thread, _| {
thread
.messages
.iter()
.filter(|m| {
m.ui_only
&& m.segments.iter().any(|s| {
if let MessageSegment::Text(text) = s {
text.contains("Retrying") && text.contains("seconds")
} else {
false
}
})
})
.count()
});
assert_eq!(
retry_count, MAX_RETRY_ATTEMPTS as usize,
"Should not exceed max retries"
);
// Final completion count should be initial + max retries
// Verify total attempts (1 initial + 1 retry)
assert_eq!(
*completion_count.lock(),
(MAX_RETRY_ATTEMPTS + 1) as usize,
"Should have made initial + max retry attempts"
2,
"Should have attempted once plus 1 retry"
);
}
@@ -4501,13 +4402,13 @@ fn main() {{
});
// Track events
let retries_failed = Arc::new(Mutex::new(false));
let retries_failed_clone = retries_failed.clone();
let stopped_with_error = Arc::new(Mutex::new(false));
let stopped_with_error_clone = stopped_with_error.clone();
let _subscription = thread.update(cx, |_, cx| {
cx.subscribe(&thread, move |_, _, event: &ThreadEvent, _| {
if let ThreadEvent::RetriesFailed { .. } = event {
*retries_failed_clone.lock() = true;
if let ThreadEvent::Stopped(Err(_)) = event {
*stopped_with_error_clone.lock() = true;
}
})
});
@@ -4519,23 +4420,11 @@ fn main() {{
cx.run_until_parked();
// Advance through all retries
for i in 0..MAX_RETRY_ATTEMPTS {
let delay = if i == 0 {
BASE_RETRY_DELAY_SECS
} else {
BASE_RETRY_DELAY_SECS * 2u64.pow(i as u32 - 1)
};
cx.executor().advance_clock(Duration::from_secs(delay));
for _ in 0..MAX_RETRY_ATTEMPTS {
cx.executor().advance_clock(BASE_RETRY_DELAY);
cx.run_until_parked();
}
// After the 3rd retry is scheduled, we need to wait for it to execute and fail
// The 3rd retry has a delay of BASE_RETRY_DELAY_SECS * 4 (20 seconds)
let final_delay = BASE_RETRY_DELAY_SECS * 2u64.pow((MAX_RETRY_ATTEMPTS - 1) as u32);
cx.executor()
.advance_clock(Duration::from_secs(final_delay));
cx.run_until_parked();
let retry_count = thread.update(cx, |thread, _| {
thread
.messages
@@ -4553,14 +4442,14 @@ fn main() {{
.count()
});
// After max retries, should emit RetriesFailed event
// After max retries, should emit Stopped(Err(...)) event
assert_eq!(
retry_count, MAX_RETRY_ATTEMPTS as usize,
"Should have attempted max retries"
"Should have attempted MAX_RETRY_ATTEMPTS retries for overloaded errors"
);
assert!(
*retries_failed.lock(),
"Should emit RetriesFailed event after max retries exceeded"
*stopped_with_error.lock(),
"Should emit Stopped(Err(...)) event after max retries exceeded"
);
// Retry state should be cleared
@@ -4578,7 +4467,7 @@ fn main() {{
.count();
assert_eq!(
retry_messages, MAX_RETRY_ATTEMPTS as usize,
"Should have one retry message per attempt"
"Should have MAX_RETRY_ATTEMPTS retry messages for overloaded errors"
);
});
}
@@ -4716,8 +4605,7 @@ fn main() {{
});
// Wait for retry
cx.executor()
.advance_clock(Duration::from_secs(BASE_RETRY_DELAY_SECS));
cx.executor().advance_clock(BASE_RETRY_DELAY);
cx.run_until_parked();
// Stream some successful content
@@ -4879,8 +4767,7 @@ fn main() {{
});
// Wait for retry delay
cx.executor()
.advance_clock(Duration::from_secs(BASE_RETRY_DELAY_SECS));
cx.executor().advance_clock(BASE_RETRY_DELAY);
cx.run_until_parked();
// The retry should now use our FailOnceModel which should succeed
@@ -5039,9 +4926,15 @@ fn main() {{
thread.read_with(cx, |thread, _| {
assert!(
thread.retry_state.is_none(),
"Rate limit errors should not set retry_state"
thread.retry_state.is_some(),
"Rate limit errors should set retry_state"
);
if let Some(retry_state) = &thread.retry_state {
assert_eq!(
retry_state.max_attempts, MAX_RETRY_ATTEMPTS,
"Rate limit errors should use MAX_RETRY_ATTEMPTS"
);
}
});
// Verify we have one retry message
@@ -5074,18 +4967,15 @@ fn main() {{
.find(|msg| msg.role == Role::System && msg.ui_only)
.expect("Should have a retry message");
// Check that the message doesn't contain attempt count
// Check that the message contains attempt count since we use retry_state
if let Some(MessageSegment::Text(text)) = retry_message.segments.first() {
assert!(
!text.contains("attempt"),
"Rate limit retry message should not contain attempt count"
text.contains(&format!("attempt 1 of {}", MAX_RETRY_ATTEMPTS)),
"Rate limit retry message should contain attempt count with MAX_RETRY_ATTEMPTS"
);
assert!(
text.contains(&format!(
"Retrying in {} seconds",
TEST_RATE_LIMIT_RETRY_SECS
)),
"Rate limit retry message should contain retry delay"
text.contains("Retrying"),
"Rate limit retry message should contain retry text"
);
}
});

View File

@@ -56,7 +56,7 @@ pub trait AgentServer: Send {
) -> impl Future<Output = Result<AgentServerVersion>> + Send;
}
const GEMINI_ACP_ARG: &str = "--acp";
const GEMINI_ACP_ARG: &str = "--experimental-acp";
impl AgentServer for Gemini {
async fn command(

View File

@@ -0,0 +1,118 @@
use crate::stdio_agent_server::{StdioAgentServer, find_bin_in_path};
use crate::{AgentServerCommand, AgentServerVersion};
use anyhow::{Context as _, Result};
use gpui::{AsyncApp, Entity};
use project::Project;
use settings::SettingsStore;
use crate::AllAgentServersSettings;
#[derive(Clone)]
pub struct Codex;
const ACP_ARG: &str = "acp";
impl StdioAgentServer for Codex {
fn name(&self) -> &'static str {
"Codex"
}
fn empty_state_headline(&self) -> &'static str {
"Welcome to Codex"
}
fn empty_state_message(&self) -> &'static str {
"Ask questions, edit files, run commands.\nBe specific for the best results."
}
fn logo(&self) -> ui::IconName {
ui::IconName::AiOpenAi
}
async fn command(
&self,
project: &Entity<Project>,
cx: &mut AsyncApp,
) -> Result<AgentServerCommand> {
let custom_command = cx.read_global(|settings: &SettingsStore, _| {
let settings = settings.get::<AllAgentServersSettings>(None);
settings
.codex
.as_ref()
.map(|codex_settings| AgentServerCommand {
path: codex_settings.command.path.clone(),
args: codex_settings
.command
.args
.iter()
.cloned()
.chain(std::iter::once(ACP_ARG.into()))
.collect(),
env: codex_settings.command.env.clone(),
})
})?;
if let Some(custom_command) = custom_command {
return Ok(custom_command);
}
if let Some(path) = find_bin_in_path("codex", project, cx).await {
return Ok(AgentServerCommand {
path,
args: vec![ACP_ARG.into()],
env: None,
});
}
todo!()
// let (fs, node_runtime) = project.update(cx, |project, _| {
// (project.fs().clone(), project.node_runtime().cloned())
// })?;
// let node_runtime = node_runtime.context("codex not found on path")?;
// let directory = ::paths::agent_servers_dir().join("codex");
// fs.create_dir(&directory).await?;
// node_runtime
// .npm_install_packages(&directory, &[("@google/gemini-cli", "latest")])
// .await?;
// let path = directory.join("node_modules/.bin/gemini");
// Ok(AgentServerCommand {
// path,
// args: vec![ACP_ARG.into()],
// env: None,
// })
}
async fn version(&self, command: &AgentServerCommand) -> Result<AgentServerVersion> {
let version_fut = util::command::new_smol_command(&command.path)
.args(command.args.iter())
.arg("--version")
.kill_on_drop(true)
.output();
let help_fut = util::command::new_smol_command(&command.path)
.args(command.args.iter())
.arg("--help")
.kill_on_drop(true)
.output();
let (version_output, help_output) = futures::future::join(version_fut, help_fut).await;
let current_version = String::from_utf8(version_output?.stdout)?;
let supported = String::from_utf8(help_output?.stdout)?.contains(ACP_ARG);
if supported {
Ok(AgentServerVersion::Supported)
} else {
Ok(AgentServerVersion::Unsupported {
error_message: format!(
"Your installed version of Codex {} doesn't support the Agentic Coding Protocol (ACP).",
current_version
).into(),
upgrade_message: "Upgrade Codex to Latest".into(),
upgrade_command: "npm install -g @openai/codex@latest".into(),
})
}
}
}

View File

@@ -925,10 +925,43 @@ impl AcpThreadView {
.size(IconSize::Small)
.color(Color::Muted),
)
.child(self.render_markdown(
tool_call.label.clone(),
default_markdown_style(needs_confirmation, window, cx),
)),
.child(if tool_call.locations.len() == 1 {
let name = tool_call.locations[0]
.path
.file_name()
.unwrap_or_default()
.display()
.to_string();
h_flex()
.id(("open-tool-call-location", entry_ix))
.child(name)
.w_full()
.max_w_full()
.pr_1()
.gap_0p5()
.cursor_pointer()
.rounded_sm()
.opacity(0.8)
.hover(|label| {
label.opacity(1.).bg(cx
.theme()
.colors()
.element_hover
.opacity(0.5))
})
.tooltip(Tooltip::text("Jump to File"))
.on_click(cx.listener(move |this, _, window, cx| {
this.open_tool_call_location(entry_ix, 0, window, cx);
}))
.into_any_element()
} else {
self.render_markdown(
tool_call.label.clone(),
default_markdown_style(needs_confirmation, window, cx),
)
.into_any()
}),
)
.child(
h_flex()
@@ -988,15 +1021,19 @@ impl AcpThreadView {
cx: &Context<Self>,
) -> AnyElement {
match content {
ToolCallContent::Markdown { markdown } => self
.render_markdown(markdown.clone(), default_markdown_style(false, window, cx))
.into_any_element(),
ToolCallContent::Markdown { markdown } => {
div()
.p_2()
.child(self.render_markdown(
markdown.clone(),
default_markdown_style(false, window, cx),
))
.into_any_element()
}
ToolCallContent::Diff {
diff: Diff {
path, multibuffer, ..
},
diff: Diff { multibuffer, .. },
..
} => self.render_diff_editor(multibuffer, path),
} => self.render_diff_editor(multibuffer),
}
}
@@ -1416,10 +1453,9 @@ impl AcpThreadView {
}
}
fn render_diff_editor(&self, multibuffer: &Entity<MultiBuffer>, path: &Path) -> AnyElement {
fn render_diff_editor(&self, multibuffer: &Entity<MultiBuffer>) -> AnyElement {
v_flex()
.h_full()
.child(path.to_string_lossy().to_string())
.child(
if let Some(editor) = self.diff_editors.get(&multibuffer.entity_id()) {
editor.clone().into_any_element()
@@ -2076,6 +2112,64 @@ impl AcpThreadView {
}
}
fn open_tool_call_location(
&self,
entry_ix: usize,
location_ix: usize,
window: &mut Window,
cx: &mut Context<Self>,
) -> Option<()> {
let location = self
.thread()?
.read(cx)
.entries()
.get(entry_ix)?
.locations()?
.get(location_ix)?;
let project_path = self
.project
.read(cx)
.find_project_path(&location.path, cx)?;
let open_task = self
.workspace
.update(cx, |worskpace, cx| {
worskpace.open_path(project_path, None, true, window, cx)
})
.log_err()?;
window
.spawn(cx, async move |cx| {
let item = open_task.await?;
let Some(active_editor) = item.downcast::<Editor>() else {
return anyhow::Ok(());
};
active_editor.update_in(cx, |editor, window, cx| {
let snapshot = editor.buffer().read(cx).snapshot(cx);
let first_hunk = editor
.diff_hunks_in_ranges(
&[editor::Anchor::min()..editor::Anchor::max()],
&snapshot,
)
.next();
if let Some(first_hunk) = first_hunk {
let first_hunk_start = first_hunk.multi_buffer_range().start;
editor.change_selections(Default::default(), window, cx, |selections| {
selections.select_anchor_ranges([first_hunk_start..first_hunk_start]);
})
}
})?;
anyhow::Ok(())
})
.detach_and_log_err(cx);
None
}
pub fn open_thread_as_markdown(
&self,
workspace: Entity<Workspace>,

View File

@@ -996,30 +996,57 @@ impl ActiveThread {
| ThreadEvent::SummaryChanged => {
self.save_thread(cx);
}
ThreadEvent::Stopped(reason) => match reason {
Ok(StopReason::EndTurn | StopReason::MaxTokens) => {
let used_tools = self.thread.read(cx).used_tools_since_last_user_message();
self.play_notification_sound(window, cx);
self.show_notification(
if used_tools {
"Finished running tools"
} else {
"New message"
},
IconName::ZedAssistant,
window,
cx,
);
ThreadEvent::Stopped(reason) => {
match reason {
Ok(StopReason::EndTurn | StopReason::MaxTokens) => {
let used_tools = self.thread.read(cx).used_tools_since_last_user_message();
self.notify_with_sound(
if used_tools {
"Finished running tools"
} else {
"New message"
},
IconName::ZedAssistant,
window,
cx,
);
}
Ok(StopReason::ToolUse) => {
// Don't notify for intermediate tool use
}
Ok(StopReason::Refusal) => {
self.notify_with_sound(
"Language model refused to respond",
IconName::Warning,
window,
cx,
);
}
Err(error) => {
self.notify_with_sound(
"Agent stopped due to an error",
IconName::Warning,
window,
cx,
);
let error_message = error
.chain()
.map(|err| err.to_string())
.collect::<Vec<_>>()
.join("\n");
self.last_error = Some(ThreadError::Message {
header: "Error interacting with language model".into(),
message: error_message.into(),
});
}
}
_ => {}
},
}
ThreadEvent::ToolConfirmationNeeded => {
self.play_notification_sound(window, cx);
self.show_notification("Waiting for tool confirmation", IconName::Info, window, cx);
self.notify_with_sound("Waiting for tool confirmation", IconName::Info, window, cx);
}
ThreadEvent::ToolUseLimitReached => {
self.play_notification_sound(window, cx);
self.show_notification(
self.notify_with_sound(
"Consecutive tool use limit reached.",
IconName::Warning,
window,
@@ -1162,9 +1189,6 @@ impl ActiveThread {
self.save_thread(cx);
cx.notify();
}
ThreadEvent::RetriesFailed { message } => {
self.show_notification(message, ui::IconName::Warning, window, cx);
}
}
}
@@ -1219,6 +1243,17 @@ impl ActiveThread {
}
}
fn notify_with_sound(
&mut self,
caption: impl Into<SharedString>,
icon: IconName,
window: &mut Window,
cx: &mut Context<ActiveThread>,
) {
self.play_notification_sound(window, cx);
self.show_notification(caption, icon, window, cx);
}
fn pop_up(
&mut self,
icon: IconName,

View File

@@ -24,9 +24,10 @@ use project::{
context_server_store::{ContextServerConfiguration, ContextServerStatus, ContextServerStore},
project_settings::{ContextServerSettings, ProjectSettings},
};
use proto::Plan;
use settings::{Settings, update_settings_file};
use ui::{
ContextMenu, Disclosure, Divider, DividerColor, ElevationIndex, Indicator, PopoverMenu,
Chip, ContextMenu, Disclosure, Divider, DividerColor, ElevationIndex, Indicator, PopoverMenu,
Scrollbar, ScrollbarState, Switch, SwitchColor, Tooltip, prelude::*,
};
use util::ResultExt as _;
@@ -171,6 +172,15 @@ impl AgentConfiguration {
.copied()
.unwrap_or(false);
let is_zed_provider = provider.id() == ZED_CLOUD_PROVIDER_ID;
let current_plan = if is_zed_provider {
self.workspace
.upgrade()
.and_then(|workspace| workspace.read(cx).user_store().read(cx).current_plan())
} else {
None
};
v_flex()
.when(is_expanded, |this| this.mb_2())
.child(
@@ -208,14 +218,31 @@ impl AgentConfiguration {
.size(IconSize::Small)
.color(Color::Muted),
)
.child(Label::new(provider_name.clone()).size(LabelSize::Large))
.when(
provider.is_authenticated(cx) && !is_expanded,
|parent| {
parent.child(
Icon::new(IconName::Check).color(Color::Success),
.child(
h_flex()
.gap_1()
.child(
Label::new(provider_name.clone())
.size(LabelSize::Large),
)
},
.map(|this| {
if is_zed_provider {
this.gap_2().child(
self.render_zed_plan_info(current_plan, cx),
)
} else {
this.when(
provider.is_authenticated(cx)
&& !is_expanded,
|parent| {
parent.child(
Icon::new(IconName::Check)
.color(Color::Success),
)
},
)
}
}),
),
)
.child(
@@ -431,6 +458,37 @@ impl AgentConfiguration {
.child(self.render_sound_notification(cx))
}
fn render_zed_plan_info(&self, plan: Option<Plan>, cx: &mut Context<Self>) -> impl IntoElement {
if let Some(plan) = plan {
let free_chip_bg = cx
.theme()
.colors()
.editor_background
.opacity(0.5)
.blend(cx.theme().colors().text_accent.opacity(0.05));
let pro_chip_bg = cx
.theme()
.colors()
.editor_background
.opacity(0.5)
.blend(cx.theme().colors().text_accent.opacity(0.2));
let (plan_name, label_color, bg_color) = match plan {
Plan::Free => ("Free", Color::Default, free_chip_bg),
Plan::ZedProTrial => ("Pro Trial", Color::Accent, pro_chip_bg),
Plan::ZedPro => ("Pro", Color::Accent, pro_chip_bg),
};
Chip::new(plan_name.to_string())
.bg_color(bg_color)
.label_color(label_color)
.into_any_element()
} else {
div().into_any_element()
}
}
fn render_context_servers_section(
&mut self,
window: &mut Window,
@@ -491,6 +549,7 @@ impl AgentConfiguration {
category_filter: Some(
ExtensionCategoryFilter::ContextServers,
),
id: None,
}
.boxed_clone(),
cx,

View File

@@ -1488,7 +1488,6 @@ impl AgentDiff {
| ThreadEvent::ToolConfirmationNeeded
| ThreadEvent::ToolUseLimitReached
| ThreadEvent::CancelEditing
| ThreadEvent::RetriesFailed { .. }
| ThreadEvent::ProfileChanged => {}
}
}

View File

@@ -1921,6 +1921,7 @@ impl AgentPanel {
category_filter: Some(
zed_actions::ExtensionCategoryFilter::ContextServers,
),
id: None,
}),
)
.action("Add Custom Server…", Box::new(AddContextServer))
@@ -1974,48 +1975,45 @@ impl AgentPanel {
}
fn render_token_count(&self, cx: &App) -> Option<AnyElement> {
let (active_thread, message_editor) = match &self.active_view {
match &self.active_view {
ActiveView::Thread {
thread,
message_editor,
..
} => (thread.read(cx), message_editor.read(cx)),
ActiveView::AcpThread { .. } => {
return None;
}
ActiveView::TextThread { .. } | ActiveView::History | ActiveView::Configuration => {
return None;
}
};
} => {
let active_thread = thread.read(cx);
let message_editor = message_editor.read(cx);
let editor_empty = message_editor.is_editor_fully_empty(cx);
let editor_empty = message_editor.is_editor_fully_empty(cx);
if active_thread.is_empty() && editor_empty {
return None;
}
if active_thread.is_empty() && editor_empty {
return None;
}
let thread = active_thread.thread().read(cx);
let is_generating = thread.is_generating();
let conversation_token_usage = thread.total_token_usage()?;
let thread = active_thread.thread().read(cx);
let is_generating = thread.is_generating();
let conversation_token_usage = thread.total_token_usage()?;
let (total_token_usage, is_estimating) =
if let Some((editing_message_id, unsent_tokens)) = active_thread.editing_message_id() {
let combined = thread
.token_usage_up_to_message(editing_message_id)
.add(unsent_tokens);
let (total_token_usage, is_estimating) =
if let Some((editing_message_id, unsent_tokens)) =
active_thread.editing_message_id()
{
let combined = thread
.token_usage_up_to_message(editing_message_id)
.add(unsent_tokens);
(combined, unsent_tokens > 0)
} else {
let unsent_tokens = message_editor.last_estimated_token_count().unwrap_or(0);
let combined = conversation_token_usage.add(unsent_tokens);
(combined, unsent_tokens > 0)
} else {
let unsent_tokens =
message_editor.last_estimated_token_count().unwrap_or(0);
let combined = conversation_token_usage.add(unsent_tokens);
(combined, unsent_tokens > 0)
};
(combined, unsent_tokens > 0)
};
let is_waiting_to_update_token_count = message_editor.is_waiting_to_update_token_count();
let is_waiting_to_update_token_count =
message_editor.is_waiting_to_update_token_count();
match &self.active_view {
ActiveView::Thread { .. } => {
if total_token_usage.total == 0 {
return None;
}

View File

@@ -660,7 +660,6 @@ impl InlineAssistant {
height: Some(prompt_editor_height),
render: build_assist_editor_renderer(prompt_editor),
priority: 0,
render_in_minimap: false,
},
BlockProperties {
style: BlockStyle::Sticky,
@@ -675,7 +674,6 @@ impl InlineAssistant {
.into_any_element()
}),
priority: 0,
render_in_minimap: false,
},
];
@@ -1451,7 +1449,6 @@ impl InlineAssistant {
.into_any_element()
}),
priority: 0,
render_in_minimap: false,
});
}

View File

@@ -1256,7 +1256,6 @@ impl TextThreadEditor {
),
priority: usize::MAX,
render: render_block(MessageMetadata::from(message)),
render_in_minimap: false,
};
let mut new_blocks = vec![];
let mut block_index_to_message = vec![];
@@ -1858,7 +1857,6 @@ impl TextThreadEditor {
.into_any_element()
}),
priority: 0,
render_in_minimap: false,
})
})
.collect::<Vec<_>>();

View File

@@ -19,6 +19,5 @@ net.workspace = true
parking_lot.workspace = true
smol.workspace = true
tempfile.workspace = true
unindent.workspace = true
util.workspace = true
workspace-hack.workspace = true

View File

@@ -40,21 +40,11 @@ impl AskPassDelegate {
self.tx.send((prompt, tx)).await?;
Ok(rx.await?)
}
pub fn new_always_failing() -> Self {
let (tx, _rx) = mpsc::unbounded::<(String, oneshot::Sender<String>)>();
Self {
tx,
_task: Task::ready(()),
}
}
}
pub struct AskPassSession {
#[cfg(not(target_os = "windows"))]
script_path: std::path::PathBuf,
#[cfg(not(target_os = "windows"))]
gpg_script_path: std::path::PathBuf,
#[cfg(target_os = "windows")]
askpass_helper: String,
#[cfg(target_os = "windows")]
@@ -69,9 +59,6 @@ const ASKPASS_SCRIPT_NAME: &str = "askpass.sh";
#[cfg(target_os = "windows")]
const ASKPASS_SCRIPT_NAME: &str = "askpass.ps1";
#[cfg(not(target_os = "windows"))]
const GPG_SCRIPT_NAME: &str = "gpg.sh";
impl AskPassSession {
/// This will create a new AskPassSession.
/// You must retain this session until the master process exits.
@@ -85,8 +72,6 @@ impl AskPassSession {
let temp_dir = tempfile::Builder::new().prefix("zed-askpass").tempdir()?;
let askpass_socket = temp_dir.path().join("askpass.sock");
let askpass_script_path = temp_dir.path().join(ASKPASS_SCRIPT_NAME);
#[cfg(not(target_os = "windows"))]
let gpg_script_path = temp_dir.path().join(GPG_SCRIPT_NAME);
let (askpass_opened_tx, askpass_opened_rx) = oneshot::channel::<()>();
let listener = UnixListener::bind(&askpass_socket).context("creating askpass socket")?;
#[cfg(not(target_os = "windows"))]
@@ -150,20 +135,9 @@ impl AskPassSession {
askpass_script_path.display()
);
#[cfg(not(target_os = "windows"))]
{
let gpg_script = generate_gpg_script();
fs::write(&gpg_script_path, gpg_script)
.await
.with_context(|| format!("creating gpg wrapper script at {gpg_script_path:?}"))?;
make_file_executable(&gpg_script_path).await?;
}
Ok(Self {
#[cfg(not(target_os = "windows"))]
script_path: askpass_script_path,
#[cfg(not(target_os = "windows"))]
gpg_script_path,
#[cfg(target_os = "windows")]
secret,
@@ -186,19 +160,6 @@ impl AskPassSession {
&self.askpass_helper
}
#[cfg(not(target_os = "windows"))]
pub fn gpg_script_path(&self) -> Option<impl AsRef<OsStr>> {
Some(&self.gpg_script_path)
}
#[cfg(target_os = "windows")]
pub fn gpg_script_path(&self) -> Option<impl AsRef<OsStr>> {
// TODO implement wrapping GPG on Windows. This is more difficult than on Unix
// because we can't use --passphrase-fd with a nonstandard FD, and both --passphrase
// and --passphrase-file are insecure.
None::<std::path::PathBuf>
}
// This will run the askpass task forever, resolving as many authentication requests as needed.
// The caller is responsible for examining the result of their own commands and cancelling this
// future when this is no longer needed. Note that this can only be called once, but due to the
@@ -302,23 +263,3 @@ fn generate_askpass_script(zed_path: &std::path::Path, askpass_socket: &std::pat
askpass_socket = askpass_socket.display(),
)
}
#[inline]
#[cfg(not(target_os = "windows"))]
fn generate_gpg_script() -> String {
use unindent::Unindent as _;
r#"
#!/bin/sh
set -eu
unset GIT_CONFIG_PARAMETERS
GPG_PROGRAM=$(git config gpg.program || echo 'gpg')
PROMPT="Enter passphrase to unlock GPG key:"
PASSPHRASE=$(${GIT_ASKPASS} "${PROMPT}")
exec "${GPG_PROGRAM}" --batch --no-tty --yes --passphrase-fd 3 --pinentry-mode loopback "$@" 3<<EOF
${PASSPHRASE}
EOF
"#.unindent()
}

View File

@@ -40,6 +40,7 @@ collections = { workspace = true, features = ["test-support"] }
clock = { workspace = true, features = ["test-support"] }
ctor.workspace = true
gpui = { workspace = true, features = ["test-support"] }
indoc.workspace = true
language = { workspace = true, features = ["test-support"] }
language_model = { workspace = true, features = ["test-support"] }
log.workspace = true

View File

@@ -8,7 +8,10 @@ use language::{Anchor, Buffer, BufferEvent, DiskState, Point, ToPoint};
use project::{Project, ProjectItem, lsp_store::OpenLspBufferHandle};
use std::{cmp, ops::Range, sync::Arc};
use text::{Edit, Patch, Rope};
use util::{RangeExt, ResultExt as _};
use util::{
RangeExt, ResultExt as _,
paths::{PathStyle, RemotePathBuf},
};
/// Tracks actions performed by tools in a thread
pub struct ActionLog {
@@ -18,8 +21,6 @@ pub struct ActionLog {
edited_since_project_diagnostics_check: bool,
/// The project this action log is associated with
project: Entity<Project>,
/// Tracks which buffer versions have already been notified as changed externally
notified_versions: BTreeMap<Entity<Buffer>, clock::Global>,
}
impl ActionLog {
@@ -29,7 +30,6 @@ impl ActionLog {
tracked_buffers: BTreeMap::default(),
edited_since_project_diagnostics_check: false,
project,
notified_versions: BTreeMap::default(),
}
}
@@ -51,6 +51,67 @@ impl ActionLog {
Some(self.tracked_buffers.get(buffer)?.snapshot.clone())
}
pub fn has_unnotified_user_edits(&self) -> bool {
self.tracked_buffers
.values()
.any(|tracked| tracked.has_unnotified_user_edits)
}
/// Return a unified diff patch with user edits made since last read or notification
pub fn unnotified_user_edits(&self, cx: &Context<Self>) -> Option<String> {
if !self.has_unnotified_user_edits() {
return None;
}
let unified_diff = self
.tracked_buffers
.values()
.filter_map(|tracked| {
if !tracked.has_unnotified_user_edits {
return None;
}
let text_with_latest_user_edits = tracked.diff_base.to_string();
let text_with_last_seen_user_edits = tracked.last_seen_base.to_string();
if text_with_latest_user_edits == text_with_last_seen_user_edits {
return None;
}
let patch = language::unified_diff(
&text_with_last_seen_user_edits,
&text_with_latest_user_edits,
);
let buffer = tracked.buffer.clone();
let file_path = buffer
.read(cx)
.file()
.map(|file| RemotePathBuf::new(file.full_path(cx), PathStyle::Posix).to_proto())
.unwrap_or_else(|| format!("buffer_{}", buffer.entity_id()));
let mut result = String::new();
result.push_str(&format!("--- a/{}\n", file_path));
result.push_str(&format!("+++ b/{}\n", file_path));
result.push_str(&patch);
Some(result)
})
.collect::<Vec<_>>()
.join("\n\n");
Some(unified_diff)
}
/// Return a unified diff patch with user edits made since last read/notification
/// and mark them as notified
pub fn flush_unnotified_user_edits(&mut self, cx: &Context<Self>) -> Option<String> {
let patch = self.unnotified_user_edits(cx);
self.tracked_buffers.values_mut().for_each(|tracked| {
tracked.has_unnotified_user_edits = false;
tracked.last_seen_base = tracked.diff_base.clone();
});
patch
}
fn track_buffer_internal(
&mut self,
buffer: Entity<Buffer>,
@@ -59,7 +120,6 @@ impl ActionLog {
) -> &mut TrackedBuffer {
let status = if is_created {
if let Some(tracked) = self.tracked_buffers.remove(&buffer) {
self.notified_versions.remove(&buffer);
match tracked.status {
TrackedBufferStatus::Created {
existing_file_content,
@@ -101,26 +161,31 @@ impl ActionLog {
let diff = cx.new(|cx| BufferDiff::new(&text_snapshot, cx));
let (diff_update_tx, diff_update_rx) = mpsc::unbounded();
let diff_base;
let last_seen_base;
let unreviewed_edits;
if is_created {
diff_base = Rope::default();
last_seen_base = Rope::default();
unreviewed_edits = Patch::new(vec![Edit {
old: 0..1,
new: 0..text_snapshot.max_point().row + 1,
}])
} else {
diff_base = buffer.read(cx).as_rope().clone();
last_seen_base = diff_base.clone();
unreviewed_edits = Patch::default();
}
TrackedBuffer {
buffer: buffer.clone(),
diff_base,
last_seen_base,
unreviewed_edits,
snapshot: text_snapshot.clone(),
status,
version: buffer.read(cx).version(),
diff,
diff_update: diff_update_tx,
has_unnotified_user_edits: false,
_open_lsp_handle: open_lsp_handle,
_maintain_diff: cx.spawn({
let buffer = buffer.clone();
@@ -174,7 +239,6 @@ impl ActionLog {
// If the buffer had been edited by a tool, but it got
// deleted externally, we want to stop tracking it.
self.tracked_buffers.remove(&buffer);
self.notified_versions.remove(&buffer);
}
cx.notify();
}
@@ -188,7 +252,6 @@ impl ActionLog {
// resurrected externally, we want to clear the edits we
// were tracking and reset the buffer's state.
self.tracked_buffers.remove(&buffer);
self.notified_versions.remove(&buffer);
self.track_buffer_internal(buffer, false, cx);
}
cx.notify();
@@ -262,19 +325,23 @@ impl ActionLog {
buffer_snapshot: text::BufferSnapshot,
cx: &mut AsyncApp,
) -> Result<()> {
let rebase = this.read_with(cx, |this, cx| {
let rebase = this.update(cx, |this, cx| {
let tracked_buffer = this
.tracked_buffers
.get(buffer)
.get_mut(buffer)
.context("buffer not tracked")?;
if let ChangeAuthor::User = author {
tracked_buffer.has_unnotified_user_edits = true;
}
let rebase = cx.background_spawn({
let mut base_text = tracked_buffer.diff_base.clone();
let old_snapshot = tracked_buffer.snapshot.clone();
let new_snapshot = buffer_snapshot.clone();
let unreviewed_edits = tracked_buffer.unreviewed_edits.clone();
let edits = diff_snapshots(&old_snapshot, &new_snapshot);
async move {
let edits = diff_snapshots(&old_snapshot, &new_snapshot);
if let ChangeAuthor::User = author {
apply_non_conflicting_edits(
&unreviewed_edits,
@@ -494,7 +561,6 @@ impl ActionLog {
match tracked_buffer.status {
TrackedBufferStatus::Created { .. } => {
self.tracked_buffers.remove(&buffer);
self.notified_versions.remove(&buffer);
cx.notify();
}
TrackedBufferStatus::Modified => {
@@ -520,7 +586,6 @@ impl ActionLog {
match tracked_buffer.status {
TrackedBufferStatus::Deleted => {
self.tracked_buffers.remove(&buffer);
self.notified_versions.remove(&buffer);
cx.notify();
}
_ => {
@@ -629,7 +694,6 @@ impl ActionLog {
};
self.tracked_buffers.remove(&buffer);
self.notified_versions.remove(&buffer);
cx.notify();
task
}
@@ -643,7 +707,6 @@ impl ActionLog {
// Clear all tracked edits for this buffer and start over as if we just read it.
self.tracked_buffers.remove(&buffer);
self.notified_versions.remove(&buffer);
self.buffer_read(buffer.clone(), cx);
cx.notify();
save
@@ -744,33 +807,6 @@ impl ActionLog {
.collect()
}
/// Returns stale buffers that haven't been notified yet
pub fn unnotified_stale_buffers<'a>(
&'a self,
cx: &'a App,
) -> impl Iterator<Item = &'a Entity<Buffer>> {
self.stale_buffers(cx).filter(|buffer| {
let buffer_entity = buffer.read(cx);
self.notified_versions
.get(buffer)
.map_or(true, |notified_version| {
*notified_version != buffer_entity.version
})
})
}
/// Marks the given buffers as notified at their current versions
pub fn mark_buffers_as_notified(
&mut self,
buffers: impl IntoIterator<Item = Entity<Buffer>>,
cx: &App,
) {
for buffer in buffers {
let version = buffer.read(cx).version.clone();
self.notified_versions.insert(buffer, version);
}
}
/// Iterate over buffers changed since last read or edited by the model
pub fn stale_buffers<'a>(&'a self, cx: &'a App) -> impl Iterator<Item = &'a Entity<Buffer>> {
self.tracked_buffers
@@ -914,12 +950,14 @@ enum TrackedBufferStatus {
struct TrackedBuffer {
buffer: Entity<Buffer>,
diff_base: Rope,
last_seen_base: Rope,
unreviewed_edits: Patch<u32>,
status: TrackedBufferStatus,
version: clock::Global,
diff: Entity<BufferDiff>,
snapshot: text::BufferSnapshot,
diff_update: mpsc::UnboundedSender<(ChangeAuthor, text::BufferSnapshot)>,
has_unnotified_user_edits: bool,
_open_lsp_handle: OpenLspBufferHandle,
_maintain_diff: Task<()>,
_subscription: Subscription,
@@ -950,6 +988,7 @@ mod tests {
use super::*;
use buffer_diff::DiffHunkStatusKind;
use gpui::TestAppContext;
use indoc::indoc;
use language::Point;
use project::{FakeFs, Fs, Project, RemoveOptions};
use rand::prelude::*;
@@ -1232,6 +1271,110 @@ mod tests {
assert_eq!(unreviewed_hunks(&action_log, cx), vec![]);
}
#[gpui::test(iterations = 10)]
async fn test_user_edits_notifications(cx: &mut TestAppContext) {
init_test(cx);
let fs = FakeFs::new(cx.executor());
fs.insert_tree(
path!("/dir"),
json!({"file": indoc! {"
abc
def
ghi
jkl
mno"}}),
)
.await;
let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
let action_log = cx.new(|_| ActionLog::new(project.clone()));
let file_path = project
.read_with(cx, |project, cx| project.find_project_path("dir/file", cx))
.unwrap();
let buffer = project
.update(cx, |project, cx| project.open_buffer(file_path, cx))
.await
.unwrap();
// Agent edits
cx.update(|cx| {
action_log.update(cx, |log, cx| log.buffer_read(buffer.clone(), cx));
buffer.update(cx, |buffer, cx| {
buffer
.edit([(Point::new(1, 2)..Point::new(2, 3), "F\nGHI")], None, cx)
.unwrap()
});
action_log.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx));
});
cx.run_until_parked();
assert_eq!(
buffer.read_with(cx, |buffer, _| buffer.text()),
indoc! {"
abc
deF
GHI
jkl
mno"}
);
assert_eq!(
unreviewed_hunks(&action_log, cx),
vec![(
buffer.clone(),
vec![HunkStatus {
range: Point::new(1, 0)..Point::new(3, 0),
diff_status: DiffHunkStatusKind::Modified,
old_text: "def\nghi\n".into(),
}],
)]
);
// User edits
buffer.update(cx, |buffer, cx| {
buffer.edit(
[
(Point::new(0, 2)..Point::new(0, 2), "X"),
(Point::new(3, 0)..Point::new(3, 0), "Y"),
],
None,
cx,
)
});
cx.run_until_parked();
assert_eq!(
buffer.read_with(cx, |buffer, _| buffer.text()),
indoc! {"
abXc
deF
GHI
Yjkl
mno"}
);
// User edits should be stored separately from agent's
let user_edits = action_log.update(cx, |log, cx| log.unnotified_user_edits(cx));
assert_eq!(
user_edits.expect("should have some user edits"),
indoc! {"
--- a/dir/file
+++ b/dir/file
@@ -1,5 +1,5 @@
-abc
+abXc
def
ghi
-jkl
+Yjkl
mno
"}
);
action_log.update(cx, |log, cx| {
log.keep_edits_in_range(buffer.clone(), Point::new(0, 0)..Point::new(1, 0), cx)
});
cx.run_until_parked();
assert_eq!(unreviewed_hunks(&action_log, cx), vec![]);
}
#[gpui::test(iterations = 10)]
async fn test_creating_files(cx: &mut TestAppContext) {
init_test(cx);
@@ -2221,4 +2364,61 @@ mod tests {
.collect()
})
}
#[gpui::test]
async fn test_format_patch(cx: &mut TestAppContext) {
init_test(cx);
let fs = FakeFs::new(cx.executor());
fs.insert_tree(
path!("/dir"),
json!({"test.txt": "line 1\nline 2\nline 3\n"}),
)
.await;
let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
let action_log = cx.new(|_| ActionLog::new(project.clone()));
let file_path = project
.read_with(cx, |project, cx| {
project.find_project_path("dir/test.txt", cx)
})
.unwrap();
let buffer = project
.update(cx, |project, cx| project.open_buffer(file_path, cx))
.await
.unwrap();
cx.update(|cx| {
// Track the buffer and mark it as read first
action_log.update(cx, |log, cx| {
log.buffer_read(buffer.clone(), cx);
});
// Make some edits to create a patch
buffer.update(cx, |buffer, cx| {
buffer
.edit([(Point::new(1, 0)..Point::new(1, 6), "CHANGED")], None, cx)
.unwrap(); // Replace "line2" with "CHANGED"
});
});
cx.run_until_parked();
// Get the patch
let patch = action_log.update(cx, |log, cx| log.unnotified_user_edits(cx));
// Verify the patch format contains expected unified diff elements
assert_eq!(
patch.unwrap(),
indoc! {"
--- a/dir/test.txt
+++ b/dir/test.txt
@@ -1,3 +1,3 @@
line 1
-line 2
+CHANGED
line 3
"}
);
}
}

View File

@@ -63,6 +63,7 @@ which.workspace = true
workspace-hack.workspace = true
workspace.workspace = true
zed_llm_client.workspace = true
diffy = "0.4.2"
[dev-dependencies]
lsp = { workspace = true, features = ["test-support"] }

View File

@@ -365,17 +365,23 @@ fn eval_disable_cursor_blinking() {
// Model | Pass rate
// ============================================
//
// claude-3.7-sonnet | 0.99 (2025-06-14)
// claude-sonnet-4 | 0.85 (2025-06-14)
// gemini-2.5-pro-preview-latest | 0.97 (2025-06-16)
// gemini-2.5-flash-preview-04-17 |
// gpt-4.1 |
// claude-3.7-sonnet | 0.59 (2025-07-14)
// claude-sonnet-4 | 0.81 (2025-07-14)
// gemini-2.5-pro | 0.95 (2025-07-14)
// gemini-2.5-flash-preview-04-17 | 0.78 (2025-07-14)
// gpt-4.1 | 0.00 (2025-07-14) (follows edit_description too literally)
let input_file_path = "root/editor.rs";
let input_file_content = include_str!("evals/fixtures/disable_cursor_blinking/before.rs");
let edit_description = "Comment out the call to `BlinkManager::enable`";
let possible_diffs = vec![
include_str!("evals/fixtures/disable_cursor_blinking/possible-01.diff"),
include_str!("evals/fixtures/disable_cursor_blinking/possible-02.diff"),
include_str!("evals/fixtures/disable_cursor_blinking/possible-03.diff"),
include_str!("evals/fixtures/disable_cursor_blinking/possible-04.diff"),
];
eval(
100,
0.95,
0.51,
0.05,
EvalInput::from_conversation(
vec![
@@ -433,11 +439,7 @@ fn eval_disable_cursor_blinking() {
),
],
Some(input_file_content.into()),
EvalAssertion::judge_diff(indoc! {"
- Calls to BlinkManager in `observe_window_activation` were commented out
- The call to `blink_manager.enable` above the call to show_cursor_names was commented out
- All the edits have valid indentation
"}),
EvalAssertion::assert_diff_any(possible_diffs),
),
);
}

View File

@@ -0,0 +1,28 @@
--- before.rs 2025-07-07 11:37:48.434629001 +0300
+++ expected.rs 2025-07-14 10:33:53.346906775 +0300
@@ -1780,11 +1780,11 @@
cx.observe_window_activation(window, |editor, window, cx| {
let active = window.is_window_active();
editor.blink_manager.update(cx, |blink_manager, cx| {
- if active {
- blink_manager.enable(cx);
- } else {
- blink_manager.disable(cx);
- }
+ // if active {
+ // blink_manager.enable(cx);
+ // } else {
+ // blink_manager.disable(cx);
+ // }
});
}),
],
@@ -18463,7 +18463,7 @@
}
self.blink_manager.update(cx, |blink_manager, cx| {
- blink_manager.enable(cx);
+ // blink_manager.enable(cx);
});
self.show_cursor_names(window, cx);
self.buffer.update(cx, |buffer, cx| {

View File

@@ -0,0 +1,29 @@
@@ -1778,13 +1778,13 @@
cx.observe_global_in::<SettingsStore>(window, Self::settings_changed),
observe_buffer_font_size_adjustment(cx, |_, cx| cx.notify()),
cx.observe_window_activation(window, |editor, window, cx| {
- let active = window.is_window_active();
+ // let active = window.is_window_active();
editor.blink_manager.update(cx, |blink_manager, cx| {
- if active {
- blink_manager.enable(cx);
- } else {
- blink_manager.disable(cx);
- }
+ // if active {
+ // blink_manager.enable(cx);
+ // } else {
+ // blink_manager.disable(cx);
+ // }
});
}),
],
@@ -18463,7 +18463,7 @@
}
self.blink_manager.update(cx, |blink_manager, cx| {
- blink_manager.enable(cx);
+ // blink_manager.enable(cx);
});
self.show_cursor_names(window, cx);
self.buffer.update(cx, |buffer, cx| {

View File

@@ -0,0 +1,34 @@
@@ -1774,17 +1774,17 @@
cx.observe(&buffer, Self::on_buffer_changed),
cx.subscribe_in(&buffer, window, Self::on_buffer_event),
cx.observe_in(&display_map, window, Self::on_display_map_changed),
- cx.observe(&blink_manager, |_, _, cx| cx.notify()),
+ // cx.observe(&blink_manager, |_, _, cx| cx.notify()),
cx.observe_global_in::<SettingsStore>(window, Self::settings_changed),
observe_buffer_font_size_adjustment(cx, |_, cx| cx.notify()),
cx.observe_window_activation(window, |editor, window, cx| {
- let active = window.is_window_active();
+ // let active = window.is_window_active();
editor.blink_manager.update(cx, |blink_manager, cx| {
- if active {
- blink_manager.enable(cx);
- } else {
- blink_manager.disable(cx);
- }
+ // if active {
+ // blink_manager.enable(cx);
+ // } else {
+ // blink_manager.disable(cx);
+ // }
});
}),
],
@@ -18463,7 +18463,7 @@
}
self.blink_manager.update(cx, |blink_manager, cx| {
- blink_manager.enable(cx);
+ // blink_manager.enable(cx);
});
self.show_cursor_names(window, cx);
self.buffer.update(cx, |buffer, cx| {

View File

@@ -0,0 +1,33 @@
@@ -1774,17 +1774,17 @@
cx.observe(&buffer, Self::on_buffer_changed),
cx.subscribe_in(&buffer, window, Self::on_buffer_event),
cx.observe_in(&display_map, window, Self::on_display_map_changed),
- cx.observe(&blink_manager, |_, _, cx| cx.notify()),
+ // cx.observe(&blink_manager, |_, _, cx| cx.notify()),
cx.observe_global_in::<SettingsStore>(window, Self::settings_changed),
observe_buffer_font_size_adjustment(cx, |_, cx| cx.notify()),
cx.observe_window_activation(window, |editor, window, cx| {
let active = window.is_window_active();
editor.blink_manager.update(cx, |blink_manager, cx| {
- if active {
- blink_manager.enable(cx);
- } else {
- blink_manager.disable(cx);
- }
+ // if active {
+ // blink_manager.enable(cx);
+ // } else {
+ // blink_manager.disable(cx);
+ // }
});
}),
],
@@ -18463,7 +18463,7 @@
}
self.blink_manager.update(cx, |blink_manager, cx| {
- blink_manager.enable(cx);
+ // blink_manager.enable(cx);
});
self.show_cursor_names(window, cx);
self.buffer.update(cx, |buffer, cx| {

View File

@@ -69,10 +69,9 @@ impl FetchTool {
.to_str()
.context("invalid Content-Type header")?;
let content_type = match content_type {
"text/html" => ContentType::Html,
"text/plain" => ContentType::Plaintext,
"text/html" | "application/xhtml+xml" => ContentType::Html,
"application/json" => ContentType::Json,
_ => ContentType::Html,
_ => ContentType::Plaintext,
};
match content_type {

View File

@@ -6,8 +6,7 @@ use language_model::{LanguageModel, LanguageModelRequest, LanguageModelToolSchem
use project::Project;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::fmt::Write as _;
use std::sync::Arc;
use std::{fmt::Write, sync::Arc};
use ui::IconName;
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
@@ -52,39 +51,113 @@ impl Tool for ProjectNotificationsTool {
_window: Option<AnyWindowHandle>,
cx: &mut App,
) -> ToolResult {
let mut stale_files = String::new();
let mut notified_buffers = Vec::new();
for stale_file in action_log.read(cx).unnotified_stale_buffers(cx) {
if let Some(file) = stale_file.read(cx).file() {
writeln!(&mut stale_files, "- {}", file.path().display()).ok();
notified_buffers.push(stale_file.clone());
}
}
if !notified_buffers.is_empty() {
action_log.update(cx, |log, cx| {
log.mark_buffers_as_notified(notified_buffers, cx);
});
}
let response = if stale_files.is_empty() {
"No new notifications".to_string()
} else {
// NOTE: Changes to this prompt require a symmetric update in the LLM Worker
const HEADER: &str = include_str!("./project_notifications_tool/prompt_header.txt");
format!("{HEADER}{stale_files}").replace("\r\n", "\n")
let Some(user_edits_diff) =
action_log.update(cx, |log, cx| log.flush_unnotified_user_edits(cx))
else {
return result("No new notifications");
};
Task::ready(Ok(response.into())).into()
// NOTE: Changes to this prompt require a symmetric update in the LLM Worker
const HEADER: &str = include_str!("./project_notifications_tool/prompt_header.txt");
const MAX_BYTES: usize = 8000;
let diff = fit_patch_to_size(&user_edits_diff, MAX_BYTES);
result(&format!("{HEADER}\n\n```diff\n{diff}\n```\n").replace("\r\n", "\n"))
}
}
fn result(response: &str) -> ToolResult {
Task::ready(Ok(response.to_string().into())).into()
}
/// Make sure that the patch fits into the size limit (in bytes).
/// Compress the patch by omitting some parts if needed.
/// Unified diff format is assumed.
fn fit_patch_to_size(patch: &str, max_size: usize) -> String {
if patch.len() <= max_size {
return patch.to_string();
}
// Compression level 1: remove context lines in diff bodies, but
// leave the counts and positions of inserted/deleted lines
let mut current_size = patch.len();
let mut file_patches = split_patch(&patch);
file_patches.sort_by_key(|patch| patch.len());
let compressed_patches = file_patches
.iter()
.rev()
.map(|patch| {
if current_size > max_size {
let compressed = compress_patch(patch).unwrap_or_else(|_| patch.to_string());
current_size -= patch.len() - compressed.len();
compressed
} else {
patch.to_string()
}
})
.collect::<Vec<_>>();
if current_size <= max_size {
return compressed_patches.join("\n\n");
}
// Compression level 2: list paths of the changed files only
let filenames = file_patches
.iter()
.map(|patch| {
let patch = diffy::Patch::from_str(patch).unwrap();
let path = patch
.modified()
.and_then(|path| path.strip_prefix("b/"))
.unwrap_or_default();
format!("- {path}\n")
})
.collect::<Vec<_>>();
filenames.join("")
}
/// Split a potentially multi-file patch into multiple single-file patches
fn split_patch(patch: &str) -> Vec<String> {
let mut result = Vec::new();
let mut current_patch = String::new();
for line in patch.lines() {
if line.starts_with("---") && !current_patch.is_empty() {
result.push(current_patch.trim_end_matches('\n').into());
current_patch = String::new();
}
current_patch.push_str(line);
current_patch.push('\n');
}
if !current_patch.is_empty() {
result.push(current_patch.trim_end_matches('\n').into());
}
result
}
fn compress_patch(patch: &str) -> anyhow::Result<String> {
let patch = diffy::Patch::from_str(patch)?;
let mut out = String::new();
writeln!(out, "--- {}", patch.original().unwrap_or("a"))?;
writeln!(out, "+++ {}", patch.modified().unwrap_or("b"))?;
for hunk in patch.hunks() {
writeln!(out, "@@ -{} +{} @@", hunk.old_range(), hunk.new_range())?;
writeln!(out, "[...skipped...]")?;
}
Ok(out)
}
#[cfg(test)]
mod tests {
use super::*;
use assistant_tool::ToolResultContent;
use gpui::{AppContext, TestAppContext};
use indoc::indoc;
use language_model::{LanguageModelRequest, fake_provider::FakeLanguageModelProvider};
use project::{FakeFs, Project};
use serde_json::json;
@@ -123,6 +196,7 @@ mod tests {
action_log.update(cx, |log, cx| {
log.buffer_read(buffer.clone(), cx);
});
cx.run_until_parked();
// Run the tool before any changes
let tool = Arc::new(ProjectNotificationsTool);
@@ -142,6 +216,7 @@ mod tests {
cx,
)
});
cx.run_until_parked();
let response = result.output.await.unwrap();
let response_text = match &response.content {
@@ -158,6 +233,7 @@ mod tests {
buffer.update(cx, |buffer, cx| {
buffer.edit([(1..1, "\nChange!\n")], None, cx);
});
cx.run_until_parked();
// Run the tool again
let result = cx.update(|cx| {
@@ -171,6 +247,7 @@ mod tests {
cx,
)
});
cx.run_until_parked();
// This time the buffer is stale, so the tool should return a notification
let response = result.output.await.unwrap();
@@ -179,10 +256,12 @@ mod tests {
_ => panic!("Expected text response"),
};
let expected_content = "[The following is an auto-generated notification; do not reply]\n\nThese files have changed since the last read:\n- code.rs\n";
assert_eq!(
response_text.as_str(),
expected_content,
assert!(
response_text.contains("These files have changed"),
"Tool should return the stale buffer notification"
);
assert!(
response_text.contains("test/code.rs"),
"Tool should return the stale buffer notification"
);
@@ -198,6 +277,7 @@ mod tests {
cx,
)
});
cx.run_until_parked();
let response = result.output.await.unwrap();
let response_text = match &response.content {
@@ -212,6 +292,61 @@ mod tests {
);
}
#[test]
fn test_patch_compression() {
// Given a patch that doesn't fit into the size budget
let patch = indoc! {"
--- a/dir/test.txt
+++ b/dir/test.txt
@@ -1,3 +1,3 @@
line 1
-line 2
+CHANGED
line 3
@@ -10,2 +10,2 @@
line 10
-line 11
+line eleven
--- a/dir/another.txt
+++ b/dir/another.txt
@@ -100,1 +1,1 @@
-before
+after
"};
// When the size deficit can be compensated by dropping the body,
// then the body should be trimmed for larger files first
let limit = patch.len() - 10;
let compressed = fit_patch_to_size(patch, limit);
let expected = indoc! {"
--- a/dir/test.txt
+++ b/dir/test.txt
@@ -1,3 +1,3 @@
[...skipped...]
@@ -10,2 +10,2 @@
[...skipped...]
--- a/dir/another.txt
+++ b/dir/another.txt
@@ -100,1 +1,1 @@
-before
+after"};
assert_eq!(compressed, expected);
// When the size deficit is too large, then only file paths
// should be returned
let limit = 10;
let compressed = fit_patch_to_size(patch, limit);
let expected = indoc! {"
- dir/another.txt
- dir/test.txt
"};
assert_eq!(compressed, expected);
}
fn init_test(cx: &mut TestAppContext) {
cx.update(|cx| {
let settings_store = SettingsStore::test(cx);

View File

@@ -285,7 +285,10 @@ impl Tool for ReadFileTool {
Using the line numbers in this outline, you can call this tool again
while specifying the start_line and end_line fields to see the
implementations of symbols in the outline."
implementations of symbols in the outline.
Alternatively, you can fall back to the `grep` tool (if available)
to search the file for specific content."
}
.into())
}

View File

@@ -315,19 +315,19 @@ fn main() -> Result<()> {
});
let stdin_pipe_handle: Option<JoinHandle<anyhow::Result<()>>> =
stdin_tmp_file.map(|tmp_file| {
stdin_tmp_file.map(|mut tmp_file| {
thread::spawn(move || {
let stdin = std::io::stdin().lock();
if io::IsTerminal::is_terminal(&stdin) {
return Ok(());
let mut stdin = std::io::stdin().lock();
if !io::IsTerminal::is_terminal(&stdin) {
io::copy(&mut stdin, &mut tmp_file)?;
}
return pipe_to_tmp(stdin, tmp_file);
Ok(())
})
});
let anonymous_fd_pipe_handles: Vec<JoinHandle<anyhow::Result<()>>> = anonymous_fd_tmp_files
let anonymous_fd_pipe_handles: Vec<_> = anonymous_fd_tmp_files
.into_iter()
.map(|(file, tmp_file)| thread::spawn(move || pipe_to_tmp(file, tmp_file)))
.map(|(mut file, mut tmp_file)| thread::spawn(move || io::copy(&mut file, &mut tmp_file)))
.collect();
if args.foreground {
@@ -349,22 +349,6 @@ fn main() -> Result<()> {
Ok(())
}
fn pipe_to_tmp(mut src: impl io::Read, mut dest: fs::File) -> Result<()> {
let mut buffer = [0; 8 * 1024];
loop {
let bytes_read = match src.read(&mut buffer) {
Err(err) if err.kind() == io::ErrorKind::Interrupted => continue,
res => res?,
};
if bytes_read == 0 {
break;
}
io::Write::write_all(&mut dest, &buffer[..bytes_read])?;
}
io::Write::flush(&mut dest)?;
Ok(())
}
fn anonymous_fd(path: &str) -> Option<fs::File> {
#[cfg(target_os = "linux")]
{

View File

@@ -94,6 +94,7 @@ context_server.workspace = true
ctor.workspace = true
dap = { workspace = true, features = ["test-support"] }
dap_adapters = { workspace = true, features = ["test-support"] }
dap-types.workspace = true
debugger_ui = { workspace = true, features = ["test-support"] }
editor = { workspace = true, features = ["test-support"] }
extension.workspace = true
@@ -126,6 +127,7 @@ sea-orm = { version = "1.1.0-rc.1", features = ["sqlx-sqlite"] }
serde_json.workspace = true
session = { workspace = true, features = ["test-support"] }
settings = { workspace = true, features = ["test-support"] }
smol.workspace = true
sqlx = { version = "0.8", features = ["sqlite"] }
task.workspace = true
theme.workspace = true

View File

@@ -1,11 +1,12 @@
use anyhow::{Context as _, bail};
use axum::routing::put;
use axum::{
Extension, Json, Router,
extract::{self, Query},
routing::{get, post},
};
use chrono::{DateTime, SecondsFormat, Utc};
use collections::HashSet;
use collections::{HashMap, HashSet};
use reqwest::StatusCode;
use sea_orm::ActiveValue;
use serde::{Deserialize, Serialize};
@@ -21,13 +22,14 @@ use stripe::{
PaymentMethod, Subscription, SubscriptionId, SubscriptionStatus,
};
use util::{ResultExt, maybe};
use zed_llm_client::LanguageModelProvider;
use crate::api::events::SnowflakeRow;
use crate::db::billing_subscription::{
StripeCancellationReason, StripeSubscriptionStatus, SubscriptionKind,
};
use crate::llm::db::subscription_usage_meter::CompletionMode;
use crate::llm::{AGENT_EXTENDED_TRIAL_FEATURE_FLAG, DEFAULT_MAX_MONTHLY_SPEND};
use crate::llm::AGENT_EXTENDED_TRIAL_FEATURE_FLAG;
use crate::llm::db::subscription_usage_meter::{self, CompletionMode};
use crate::rpc::{ResultExt as _, Server};
use crate::stripe_client::{
StripeCancellationDetailsReason, StripeClient, StripeCustomerId, StripeSubscription,
@@ -46,14 +48,8 @@ use crate::{
pub fn router() -> Router {
Router::new()
.route(
"/billing/preferences",
get(get_billing_preferences).put(update_billing_preferences),
)
.route(
"/billing/subscriptions",
get(list_billing_subscriptions).post(create_billing_subscription),
)
.route("/billing/preferences", put(update_billing_preferences))
.route("/billing/subscriptions", post(create_billing_subscription))
.route(
"/billing/subscriptions/manage",
post(manage_billing_subscription),
@@ -65,11 +61,6 @@ pub fn router() -> Router {
.route("/billing/usage", get(get_current_usage))
}
#[derive(Debug, Deserialize)]
struct GetBillingPreferencesParams {
github_user_id: i32,
}
#[derive(Debug, Serialize)]
struct BillingPreferencesResponse {
trial_started_at: Option<String>,
@@ -78,43 +69,6 @@ struct BillingPreferencesResponse {
model_request_overages_spend_limit_in_cents: i32,
}
async fn get_billing_preferences(
Extension(app): Extension<Arc<AppState>>,
Query(params): Query<GetBillingPreferencesParams>,
) -> Result<Json<BillingPreferencesResponse>> {
let user = app
.db
.get_user_by_github_user_id(params.github_user_id)
.await?
.context("user not found")?;
let billing_customer = app.db.get_billing_customer_by_user_id(user.id).await?;
let preferences = app.db.get_billing_preferences(user.id).await?;
Ok(Json(BillingPreferencesResponse {
trial_started_at: billing_customer
.and_then(|billing_customer| billing_customer.trial_started_at)
.map(|trial_started_at| {
trial_started_at
.and_utc()
.to_rfc3339_opts(SecondsFormat::Millis, true)
}),
max_monthly_llm_usage_spending_in_cents: preferences
.as_ref()
.map_or(DEFAULT_MAX_MONTHLY_SPEND.0 as i32, |preferences| {
preferences.max_monthly_llm_usage_spending_in_cents
}),
model_request_overages_enabled: preferences.as_ref().map_or(false, |preferences| {
preferences.model_request_overages_enabled
}),
model_request_overages_spend_limit_in_cents: preferences
.as_ref()
.map_or(0, |preferences| {
preferences.model_request_overages_spend_limit_in_cents
}),
}))
}
#[derive(Debug, Deserialize)]
struct UpdateBillingPreferencesBody {
github_user_id: i32,
@@ -209,90 +163,6 @@ async fn update_billing_preferences(
}))
}
#[derive(Debug, Deserialize)]
struct ListBillingSubscriptionsParams {
github_user_id: i32,
}
#[derive(Debug, Serialize)]
struct BillingSubscriptionJson {
id: BillingSubscriptionId,
name: String,
status: StripeSubscriptionStatus,
period: Option<BillingSubscriptionPeriodJson>,
trial_end_at: Option<String>,
cancel_at: Option<String>,
/// Whether this subscription can be canceled.
is_cancelable: bool,
}
#[derive(Debug, Serialize)]
struct BillingSubscriptionPeriodJson {
start_at: String,
end_at: String,
}
#[derive(Debug, Serialize)]
struct ListBillingSubscriptionsResponse {
subscriptions: Vec<BillingSubscriptionJson>,
}
async fn list_billing_subscriptions(
Extension(app): Extension<Arc<AppState>>,
Query(params): Query<ListBillingSubscriptionsParams>,
) -> Result<Json<ListBillingSubscriptionsResponse>> {
let user = app
.db
.get_user_by_github_user_id(params.github_user_id)
.await?
.context("user not found")?;
let subscriptions = app.db.get_billing_subscriptions(user.id).await?;
Ok(Json(ListBillingSubscriptionsResponse {
subscriptions: subscriptions
.into_iter()
.map(|subscription| BillingSubscriptionJson {
id: subscription.id,
name: match subscription.kind {
Some(SubscriptionKind::ZedPro) => "Zed Pro".to_string(),
Some(SubscriptionKind::ZedProTrial) => "Zed Pro (Trial)".to_string(),
Some(SubscriptionKind::ZedFree) => "Zed Free".to_string(),
None => "Zed LLM Usage".to_string(),
},
status: subscription.stripe_subscription_status,
period: maybe!({
let start_at = subscription.current_period_start_at()?;
let end_at = subscription.current_period_end_at()?;
Some(BillingSubscriptionPeriodJson {
start_at: start_at.to_rfc3339_opts(SecondsFormat::Millis, true),
end_at: end_at.to_rfc3339_opts(SecondsFormat::Millis, true),
})
}),
trial_end_at: if subscription.kind == Some(SubscriptionKind::ZedProTrial) {
maybe!({
let end_at = subscription.stripe_current_period_end?;
let end_at = DateTime::from_timestamp(end_at, 0)?;
Some(end_at.to_rfc3339_opts(SecondsFormat::Millis, true))
})
} else {
None
},
cancel_at: subscription.stripe_cancel_at.map(|cancel_at| {
cancel_at
.and_utc()
.to_rfc3339_opts(SecondsFormat::Millis, true)
}),
is_cancelable: subscription.kind != Some(SubscriptionKind::ZedFree)
&& subscription.stripe_subscription_status.is_cancelable()
&& subscription.stripe_cancel_at.is_none(),
})
.collect(),
}))
}
#[derive(Debug, PartialEq, Clone, Copy, Deserialize)]
#[serde(rename_all = "snake_case")]
enum ProductCode {
@@ -1416,18 +1286,21 @@ async fn sync_model_request_usage_with_stripe(
let usage_meters = llm_db
.get_current_subscription_usage_meters(Utc::now())
.await?;
let usage_meters = usage_meters
.into_iter()
.filter(|(_, usage)| !staff_user_ids.contains(&usage.user_id))
.collect::<Vec<_>>();
let user_ids = usage_meters
.iter()
.map(|(_, usage)| usage.user_id)
.collect::<HashSet<UserId>>();
let billing_subscriptions = app
.db
.get_active_zed_pro_billing_subscriptions(user_ids)
.await?;
let mut usage_meters_by_user_id =
HashMap::<UserId, Vec<subscription_usage_meter::Model>>::default();
for (usage_meter, usage) in usage_meters {
let meters = usage_meters_by_user_id.entry(usage.user_id).or_default();
meters.push(usage_meter);
}
log::info!("Stripe usage sync: Retrieving Zed Pro subscriptions");
let get_zed_pro_subscriptions_started_at = Utc::now();
let billing_subscriptions = app.db.get_active_zed_pro_billing_subscriptions().await?;
log::info!(
"Stripe usage sync: Retrieved {} Zed Pro subscriptions in {}",
billing_subscriptions.len(),
Utc::now() - get_zed_pro_subscriptions_started_at
);
let claude_sonnet_4 = stripe_billing
.find_price_by_lookup_key("claude-sonnet-4-requests")
@@ -1451,59 +1324,90 @@ async fn sync_model_request_usage_with_stripe(
.find_price_by_lookup_key("claude-3-7-sonnet-requests-max")
.await?;
let usage_meter_count = usage_meters.len();
let model_mode_combinations = [
("claude-opus-4", CompletionMode::Max),
("claude-opus-4", CompletionMode::Normal),
("claude-sonnet-4", CompletionMode::Max),
("claude-sonnet-4", CompletionMode::Normal),
("claude-3-7-sonnet", CompletionMode::Max),
("claude-3-7-sonnet", CompletionMode::Normal),
("claude-3-5-sonnet", CompletionMode::Normal),
];
log::info!("Stripe usage sync: Syncing {usage_meter_count} usage meters");
let billing_subscription_count = billing_subscriptions.len();
for (usage_meter, usage) in usage_meters {
log::info!("Stripe usage sync: Syncing {billing_subscription_count} Zed Pro subscriptions");
for (user_id, (billing_customer, billing_subscription)) in billing_subscriptions {
maybe!(async {
let Some((billing_customer, billing_subscription)) =
billing_subscriptions.get(&usage.user_id)
else {
bail!(
"Attempted to sync usage meter for user who is not a Stripe customer: {}",
usage.user_id
);
};
if staff_user_ids.contains(&user_id) {
return anyhow::Ok(());
}
let stripe_customer_id =
StripeCustomerId(billing_customer.stripe_customer_id.clone().into());
let stripe_subscription_id =
StripeSubscriptionId(billing_subscription.stripe_subscription_id.clone().into());
let model = llm_db.model_by_id(usage_meter.model_id)?;
let usage_meters = usage_meters_by_user_id.get(&user_id);
let (price, meter_event_name) = match model.name.as_str() {
"claude-opus-4" => match usage_meter.mode {
CompletionMode::Normal => (&claude_opus_4, "claude_opus_4/requests"),
CompletionMode::Max => (&claude_opus_4_max, "claude_opus_4/requests/max"),
},
"claude-sonnet-4" => match usage_meter.mode {
CompletionMode::Normal => (&claude_sonnet_4, "claude_sonnet_4/requests"),
CompletionMode::Max => (&claude_sonnet_4_max, "claude_sonnet_4/requests/max"),
},
"claude-3-5-sonnet" => (&claude_3_5_sonnet, "claude_3_5_sonnet/requests"),
"claude-3-7-sonnet" => match usage_meter.mode {
CompletionMode::Normal => (&claude_3_7_sonnet, "claude_3_7_sonnet/requests"),
CompletionMode::Max => {
(&claude_3_7_sonnet_max, "claude_3_7_sonnet/requests/max")
for (model, mode) in &model_mode_combinations {
let Ok(model) =
llm_db.model(LanguageModelProvider::Anthropic, model)
else {
log::warn!("Failed to load model for user {user_id}: {model}");
continue;
};
let (price, meter_event_name) = match model.name.as_str() {
"claude-opus-4" => match mode {
CompletionMode::Normal => (&claude_opus_4, "claude_opus_4/requests"),
CompletionMode::Max => (&claude_opus_4_max, "claude_opus_4/requests/max"),
},
"claude-sonnet-4" => match mode {
CompletionMode::Normal => (&claude_sonnet_4, "claude_sonnet_4/requests"),
CompletionMode::Max => {
(&claude_sonnet_4_max, "claude_sonnet_4/requests/max")
}
},
"claude-3-5-sonnet" => (&claude_3_5_sonnet, "claude_3_5_sonnet/requests"),
"claude-3-7-sonnet" => match mode {
CompletionMode::Normal => {
(&claude_3_7_sonnet, "claude_3_7_sonnet/requests")
}
CompletionMode::Max => {
(&claude_3_7_sonnet_max, "claude_3_7_sonnet/requests/max")
}
},
model_name => {
bail!("Attempted to sync usage meter for unsupported model: {model_name:?}")
}
},
model_name => {
bail!("Attempted to sync usage meter for unsupported model: {model_name:?}")
}
};
};
stripe_billing
.subscribe_to_price(&stripe_subscription_id, price)
.await?;
stripe_billing
.bill_model_request_usage(
&stripe_customer_id,
meter_event_name,
usage_meter.requests,
)
.await?;
let model_requests = usage_meters
.and_then(|usage_meters| {
usage_meters
.iter()
.find(|meter| meter.model_id == model.id && meter.mode == *mode)
})
.map(|usage_meter| usage_meter.requests)
.unwrap_or(0);
if model_requests > 0 {
stripe_billing
.subscribe_to_price(&stripe_subscription_id, price)
.await?;
}
stripe_billing
.bill_model_request_usage(&stripe_customer_id, meter_event_name, model_requests)
.await
.with_context(|| {
format!(
"Failed to bill model request usage of {model_requests} for {stripe_customer_id}: {meter_event_name}",
)
})?;
}
Ok(())
})
@@ -1512,7 +1416,7 @@ async fn sync_model_request_usage_with_stripe(
}
log::info!(
"Stripe usage sync: Synced {usage_meter_count} usage meters in {:?}",
"Stripe usage sync: Synced {billing_subscription_count} Zed Pro subscriptions in {}",
Utc::now() - started_at
);

View File

@@ -1,83 +0,0 @@
use serde::Serialize;
/// A number of cents.
#[derive(
Debug,
PartialEq,
Eq,
PartialOrd,
Ord,
Hash,
Clone,
Copy,
derive_more::Add,
derive_more::AddAssign,
derive_more::Sub,
derive_more::SubAssign,
Serialize,
)]
pub struct Cents(pub u32);
impl Cents {
pub const ZERO: Self = Self(0);
pub const fn new(cents: u32) -> Self {
Self(cents)
}
pub const fn from_dollars(dollars: u32) -> Self {
Self(dollars * 100)
}
pub fn saturating_sub(self, other: Cents) -> Self {
Self(self.0.saturating_sub(other.0))
}
}
#[cfg(test)]
mod tests {
use pretty_assertions::assert_eq;
use super::*;
#[test]
fn test_cents_new() {
assert_eq!(Cents::new(50), Cents(50));
}
#[test]
fn test_cents_from_dollars() {
assert_eq!(Cents::from_dollars(1), Cents(100));
assert_eq!(Cents::from_dollars(5), Cents(500));
}
#[test]
fn test_cents_zero() {
assert_eq!(Cents::ZERO, Cents(0));
}
#[test]
fn test_cents_add() {
assert_eq!(Cents(50) + Cents(30), Cents(80));
}
#[test]
fn test_cents_add_assign() {
let mut cents = Cents(50);
cents += Cents(30);
assert_eq!(cents, Cents(80));
}
#[test]
fn test_cents_saturating_sub() {
assert_eq!(Cents(50).saturating_sub(Cents(30)), Cents(20));
assert_eq!(Cents(30).saturating_sub(Cents(50)), Cents(0));
}
#[test]
fn test_cents_ordering() {
assert!(Cents(50) > Cents(30));
assert!(Cents(30) < Cents(50));
assert_eq!(Cents(50), Cents(50));
}
}

View File

@@ -199,6 +199,33 @@ impl Database {
pub async fn get_active_zed_pro_billing_subscriptions(
&self,
) -> Result<HashMap<UserId, (billing_customer::Model, billing_subscription::Model)>> {
self.transaction(|tx| async move {
let mut rows = billing_subscription::Entity::find()
.inner_join(billing_customer::Entity)
.select_also(billing_customer::Entity)
.filter(
billing_subscription::Column::StripeSubscriptionStatus
.eq(StripeSubscriptionStatus::Active),
)
.filter(billing_subscription::Column::Kind.eq(SubscriptionKind::ZedPro))
.order_by_asc(billing_subscription::Column::Id)
.stream(&*tx)
.await?;
let mut subscriptions = HashMap::default();
while let Some(row) = rows.next().await {
if let (subscription, Some(customer)) = row? {
subscriptions.insert(customer.user_id, (customer, subscription));
}
}
Ok(subscriptions)
})
.await
}
pub async fn get_active_zed_pro_billing_subscriptions_for_users(
&self,
user_ids: HashSet<UserId>,
) -> Result<HashMap<UserId, (billing_customer::Model, billing_subscription::Model)>> {
self.transaction(|tx| {

View File

@@ -1,6 +1,5 @@
pub mod api;
pub mod auth;
mod cents;
pub mod db;
pub mod env;
pub mod executor;
@@ -21,7 +20,6 @@ use axum::{
http::{HeaderMap, StatusCode},
response::IntoResponse,
};
pub use cents::*;
use db::{ChannelId, Database};
use executor::Executor;
use llm::db::LlmDatabase;

View File

@@ -1,8 +1,6 @@
pub mod db;
mod token;
use crate::Cents;
pub use token::*;
pub const AGENT_EXTENDED_TRIAL_FEATURE_FLAG: &str = "agent-extended-trial";
@@ -12,9 +10,3 @@ pub const BYPASS_ACCOUNT_AGE_CHECK_FEATURE_FLAG: &str = "bypass-account-age-chec
/// The minimum account age an account must have in order to use the LLM service.
pub const MIN_ACCOUNT_AGE_FOR_LLM_USE: chrono::Duration = chrono::Duration::days(30);
/// The default value to use for maximum spend per month if the user did not
/// explicitly set a maximum spend.
///
/// Used to prevent surprise bills.
pub const DEFAULT_MAX_MONTHLY_SPEND: Cents = Cents::from_dollars(10);

View File

@@ -2246,8 +2246,11 @@ async fn test_lsp_document_color(cx_a: &mut TestAppContext, cx_b: &mut TestAppCo
});
}
#[gpui::test(iterations = 10)]
async fn test_lsp_pull_diagnostics(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
async fn test_lsp_pull_diagnostics(
should_stream_workspace_diagnostic: bool,
cx_a: &mut TestAppContext,
cx_b: &mut TestAppContext,
) {
let mut server = TestServer::start(cx_a.executor()).await;
let executor = cx_a.executor();
let client_a = server.create_client(cx_a, "user_a").await;
@@ -2396,12 +2399,25 @@ async fn test_lsp_pull_diagnostics(cx_a: &mut TestAppContext, cx_b: &mut TestApp
let closure_workspace_diagnostics_pulls_made = workspace_diagnostics_pulls_made.clone();
let closure_workspace_diagnostics_pulls_result_ids =
workspace_diagnostics_pulls_result_ids.clone();
let (workspace_diagnostic_cancel_tx, closure_workspace_diagnostic_cancel_rx) =
smol::channel::bounded::<()>(1);
let (closure_workspace_diagnostic_received_tx, workspace_diagnostic_received_rx) =
smol::channel::bounded::<()>(1);
let expected_workspace_diagnostic_token = lsp::ProgressToken::String(format!(
"workspace/diagnostic-{}-1",
fake_language_server.server.server_id()
));
let closure_expected_workspace_diagnostic_token = expected_workspace_diagnostic_token.clone();
let mut workspace_diagnostics_pulls_handle = fake_language_server
.set_request_handler::<lsp::request::WorkspaceDiagnosticRequest, _, _>(
move |params, _| {
let workspace_requests_made = closure_workspace_diagnostics_pulls_made.clone();
let workspace_diagnostics_pulls_result_ids =
closure_workspace_diagnostics_pulls_result_ids.clone();
let workspace_diagnostic_cancel_rx = closure_workspace_diagnostic_cancel_rx.clone();
let workspace_diagnostic_received_tx = closure_workspace_diagnostic_received_tx.clone();
let expected_workspace_diagnostic_token =
closure_expected_workspace_diagnostic_token.clone();
async move {
let workspace_request_count =
workspace_requests_made.fetch_add(1, atomic::Ordering::Release) + 1;
@@ -2411,6 +2427,21 @@ async fn test_lsp_pull_diagnostics(cx_a: &mut TestAppContext, cx_b: &mut TestApp
.await
.extend(params.previous_result_ids.into_iter().map(|id| id.value));
}
if should_stream_workspace_diagnostic && !workspace_diagnostic_cancel_rx.is_closed()
{
assert_eq!(
params.partial_result_params.partial_result_token,
Some(expected_workspace_diagnostic_token)
);
workspace_diagnostic_received_tx.send(()).await.unwrap();
workspace_diagnostic_cancel_rx.recv().await.unwrap();
workspace_diagnostic_cancel_rx.close();
// https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#partialResults
// > The final response has to be empty in terms of result values.
return Ok(lsp::WorkspaceDiagnosticReportResult::Report(
lsp::WorkspaceDiagnosticReport { items: Vec::new() },
));
}
Ok(lsp::WorkspaceDiagnosticReportResult::Report(
lsp::WorkspaceDiagnosticReport {
items: vec![
@@ -2479,7 +2510,11 @@ async fn test_lsp_pull_diagnostics(cx_a: &mut TestAppContext, cx_b: &mut TestApp
},
);
workspace_diagnostics_pulls_handle.next().await.unwrap();
if should_stream_workspace_diagnostic {
workspace_diagnostic_received_rx.recv().await.unwrap();
} else {
workspace_diagnostics_pulls_handle.next().await.unwrap();
}
assert_eq!(
1,
workspace_diagnostics_pulls_made.load(atomic::Ordering::Acquire),
@@ -2503,10 +2538,10 @@ async fn test_lsp_pull_diagnostics(cx_a: &mut TestAppContext, cx_b: &mut TestApp
"Expected single diagnostic, but got: {all_diagnostics:?}"
);
let diagnostic = &all_diagnostics[0];
let expected_messages = [
expected_workspace_pull_diagnostics_main_message,
expected_pull_diagnostic_main_message,
];
let mut expected_messages = vec![expected_pull_diagnostic_main_message];
if !should_stream_workspace_diagnostic {
expected_messages.push(expected_workspace_pull_diagnostics_main_message);
}
assert!(
expected_messages.contains(&diagnostic.diagnostic.message.as_str()),
"Expected {expected_messages:?} on the host, but got: {}",
@@ -2556,6 +2591,70 @@ async fn test_lsp_pull_diagnostics(cx_a: &mut TestAppContext, cx_b: &mut TestApp
version: None,
},
);
if should_stream_workspace_diagnostic {
fake_language_server.notify::<lsp::notification::Progress>(&lsp::ProgressParams {
token: expected_workspace_diagnostic_token.clone(),
value: lsp::ProgressParamsValue::WorkspaceDiagnostic(
lsp::WorkspaceDiagnosticReportResult::Report(lsp::WorkspaceDiagnosticReport {
items: vec![
lsp::WorkspaceDocumentDiagnosticReport::Full(
lsp::WorkspaceFullDocumentDiagnosticReport {
uri: lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(),
version: None,
full_document_diagnostic_report:
lsp::FullDocumentDiagnosticReport {
result_id: Some(format!(
"workspace_{}",
workspace_diagnostics_pulls_made
.fetch_add(1, atomic::Ordering::Release)
+ 1
)),
items: vec![lsp::Diagnostic {
range: lsp::Range {
start: lsp::Position {
line: 0,
character: 1,
},
end: lsp::Position {
line: 0,
character: 2,
},
},
severity: Some(lsp::DiagnosticSeverity::ERROR),
message:
expected_workspace_pull_diagnostics_main_message
.to_string(),
..lsp::Diagnostic::default()
}],
},
},
),
lsp::WorkspaceDocumentDiagnosticReport::Full(
lsp::WorkspaceFullDocumentDiagnosticReport {
uri: lsp::Url::from_file_path(path!("/a/lib.rs")).unwrap(),
version: None,
full_document_diagnostic_report:
lsp::FullDocumentDiagnosticReport {
result_id: Some(format!(
"workspace_{}",
workspace_diagnostics_pulls_made
.fetch_add(1, atomic::Ordering::Release)
+ 1
)),
items: Vec::new(),
},
},
),
],
}),
),
});
};
let mut workspace_diagnostic_start_count =
workspace_diagnostics_pulls_made.load(atomic::Ordering::Acquire);
executor.run_until_parked();
editor_a_main.update(cx_a, |editor, cx| {
let snapshot = editor.buffer().read(cx).snapshot(cx);
@@ -2599,7 +2698,7 @@ async fn test_lsp_pull_diagnostics(cx_a: &mut TestAppContext, cx_b: &mut TestApp
);
executor.run_until_parked();
assert_eq!(
1,
workspace_diagnostic_start_count,
workspace_diagnostics_pulls_made.load(atomic::Ordering::Acquire),
"Workspace diagnostics should not be changed as the remote client does not initialize the workspace diagnostics pull"
);
@@ -2646,7 +2745,7 @@ async fn test_lsp_pull_diagnostics(cx_a: &mut TestAppContext, cx_b: &mut TestApp
);
executor.run_until_parked();
assert_eq!(
1,
workspace_diagnostic_start_count,
workspace_diagnostics_pulls_made.load(atomic::Ordering::Acquire),
"The remote client still did not anything to trigger the workspace diagnostics pull"
);
@@ -2673,6 +2772,75 @@ async fn test_lsp_pull_diagnostics(cx_a: &mut TestAppContext, cx_b: &mut TestApp
);
}
});
if should_stream_workspace_diagnostic {
fake_language_server.notify::<lsp::notification::Progress>(&lsp::ProgressParams {
token: expected_workspace_diagnostic_token.clone(),
value: lsp::ProgressParamsValue::WorkspaceDiagnostic(
lsp::WorkspaceDiagnosticReportResult::Report(lsp::WorkspaceDiagnosticReport {
items: vec![lsp::WorkspaceDocumentDiagnosticReport::Full(
lsp::WorkspaceFullDocumentDiagnosticReport {
uri: lsp::Url::from_file_path(path!("/a/lib.rs")).unwrap(),
version: None,
full_document_diagnostic_report: lsp::FullDocumentDiagnosticReport {
result_id: Some(format!(
"workspace_{}",
workspace_diagnostics_pulls_made
.fetch_add(1, atomic::Ordering::Release)
+ 1
)),
items: vec![lsp::Diagnostic {
range: lsp::Range {
start: lsp::Position {
line: 0,
character: 1,
},
end: lsp::Position {
line: 0,
character: 2,
},
},
severity: Some(lsp::DiagnosticSeverity::ERROR),
message: expected_workspace_pull_diagnostics_lib_message
.to_string(),
..lsp::Diagnostic::default()
}],
},
},
)],
}),
),
});
workspace_diagnostic_start_count =
workspace_diagnostics_pulls_made.load(atomic::Ordering::Acquire);
workspace_diagnostic_cancel_tx.send(()).await.unwrap();
workspace_diagnostics_pulls_handle.next().await.unwrap();
executor.run_until_parked();
editor_b_lib.update(cx_b, |editor, cx| {
let snapshot = editor.buffer().read(cx).snapshot(cx);
let all_diagnostics = snapshot
.diagnostics_in_range(0..snapshot.len())
.collect::<Vec<_>>();
let expected_messages = [
expected_workspace_pull_diagnostics_lib_message,
// TODO bug: the pushed diagnostics are not being sent to the client when they open the corresponding buffer.
// expected_push_diagnostic_lib_message,
];
assert_eq!(
all_diagnostics.len(),
1,
"Expected pull diagnostics, but got: {all_diagnostics:?}"
);
for diagnostic in all_diagnostics {
assert!(
expected_messages.contains(&diagnostic.diagnostic.message.as_str()),
"The client should get both push and pull messages: {expected_messages:?}, but got: {}",
diagnostic.diagnostic.message
);
}
});
};
{
assert!(
diagnostics_pulls_result_ids.lock().await.len() > 0,
@@ -2701,7 +2869,7 @@ async fn test_lsp_pull_diagnostics(cx_a: &mut TestAppContext, cx_b: &mut TestApp
);
workspace_diagnostics_pulls_handle.next().await.unwrap();
assert_eq!(
2,
workspace_diagnostic_start_count + 1,
workspace_diagnostics_pulls_made.load(atomic::Ordering::Acquire),
"After client lib.rs edits, the workspace diagnostics request should follow"
);
@@ -2720,7 +2888,7 @@ async fn test_lsp_pull_diagnostics(cx_a: &mut TestAppContext, cx_b: &mut TestApp
);
workspace_diagnostics_pulls_handle.next().await.unwrap();
assert_eq!(
3,
workspace_diagnostic_start_count + 2,
workspace_diagnostics_pulls_made.load(atomic::Ordering::Acquire),
"After client main.rs edits, the workspace diagnostics pull should follow"
);
@@ -2739,7 +2907,7 @@ async fn test_lsp_pull_diagnostics(cx_a: &mut TestAppContext, cx_b: &mut TestApp
);
workspace_diagnostics_pulls_handle.next().await.unwrap();
assert_eq!(
4,
workspace_diagnostic_start_count + 3,
workspace_diagnostics_pulls_made.load(atomic::Ordering::Acquire),
"After host main.rs edits, the workspace diagnostics pull should follow"
);
@@ -2769,7 +2937,7 @@ async fn test_lsp_pull_diagnostics(cx_a: &mut TestAppContext, cx_b: &mut TestApp
);
workspace_diagnostics_pulls_handle.next().await.unwrap();
assert_eq!(
5,
workspace_diagnostic_start_count + 4,
workspace_diagnostics_pulls_made.load(atomic::Ordering::Acquire),
"Another workspace diagnostics pull should happen after the diagnostics refresh server request"
);
@@ -2840,6 +3008,19 @@ async fn test_lsp_pull_diagnostics(cx_a: &mut TestAppContext, cx_b: &mut TestApp
});
}
#[gpui::test(iterations = 10)]
async fn test_non_streamed_lsp_pull_diagnostics(
cx_a: &mut TestAppContext,
cx_b: &mut TestAppContext,
) {
test_lsp_pull_diagnostics(false, cx_a, cx_b).await;
}
#[gpui::test(iterations = 10)]
async fn test_streamed_lsp_pull_diagnostics(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
test_lsp_pull_diagnostics(true, cx_a, cx_b).await;
}
#[gpui::test(iterations = 10)]
async fn test_git_blame_is_forwarded(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
let mut server = TestServer::start(cx_a.executor()).await;

View File

@@ -1013,7 +1013,7 @@ async fn test_peers_following_each_other(cx_a: &mut TestAppContext, cx_b: &mut T
// and some of which were originally opened by client B.
workspace_b.update_in(cx_b, |workspace, window, cx| {
workspace.active_pane().update(cx, |pane, cx| {
pane.close_inactive_items(&Default::default(), window, cx)
pane.close_inactive_items(&Default::default(), None, window, cx)
.detach();
});
});

View File

@@ -2,6 +2,7 @@ use crate::tests::TestServer;
use call::ActiveCall;
use collections::{HashMap, HashSet};
use dap::{Capabilities, adapters::DebugTaskDefinition, transport::RequestHandling};
use debugger_ui::debugger_panel::DebugPanel;
use extension::ExtensionHostProxy;
use fs::{FakeFs, Fs as _, RemoveOptions};
@@ -22,6 +23,7 @@ use language::{
use node_runtime::NodeRuntime;
use project::{
ProjectPath,
debugger::session::ThreadId,
lsp_store::{FormatTrigger, LspFormatTarget},
};
use remote::SshRemoteClient;
@@ -29,7 +31,11 @@ use remote_server::{HeadlessAppState, HeadlessProject};
use rpc::proto;
use serde_json::json;
use settings::SettingsStore;
use std::{path::Path, sync::Arc};
use std::{
path::Path,
sync::{Arc, atomic::AtomicUsize},
};
use task::TcpArgumentsTemplate;
use util::path;
#[gpui::test(iterations = 10)]
@@ -688,3 +694,162 @@ async fn test_remote_server_debugger(
shutdown_session.await.unwrap();
}
#[gpui::test]
async fn test_slow_adapter_startup_retries(
cx_a: &mut TestAppContext,
server_cx: &mut TestAppContext,
executor: BackgroundExecutor,
) {
cx_a.update(|cx| {
release_channel::init(SemanticVersion::default(), cx);
command_palette_hooks::init(cx);
zlog::init_test();
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());
remote_fs
.insert_tree(
path!("/code"),
json!({
"lib.rs": "fn one() -> usize { 1 }"
}),
)
.await;
// User A connects to the remote project via SSH.
server_cx.update(HeadlessProject::init);
let remote_http_client = Arc::new(BlockedHttpClient);
let node = NodeRuntime::unavailable();
let languages = Arc::new(LanguageRegistry::new(server_cx.executor()));
let _headless_project = server_cx.new(|cx| {
client::init_settings(cx);
HeadlessProject::new(
HeadlessAppState {
session: server_ssh,
fs: remote_fs.clone(),
http_client: remote_http_client,
node_runtime: node,
languages,
extension_host_proxy: Arc::new(ExtensionHostProxy::new()),
},
cx,
)
});
let client_ssh = SshRemoteClient::fake_client(opts, cx_a).await;
let mut server = TestServer::start(server_cx.executor()).await;
let client_a = server.create_client(cx_a, "user_a").await;
cx_a.update(|cx| {
debugger_ui::init(cx);
command_palette_hooks::init(cx);
});
let (project_a, _) = client_a
.build_ssh_project(path!("/code"), client_ssh.clone(), cx_a)
.await;
let (workspace, cx_a) = client_a.build_workspace(&project_a, cx_a);
let debugger_panel = workspace
.update_in(cx_a, |_workspace, window, cx| {
cx.spawn_in(window, DebugPanel::load)
})
.await
.unwrap();
workspace.update_in(cx_a, |workspace, window, cx| {
workspace.add_panel(debugger_panel, window, cx);
});
cx_a.run_until_parked();
let debug_panel = workspace
.update(cx_a, |workspace, cx| workspace.panel::<DebugPanel>(cx))
.unwrap();
let workspace_window = cx_a
.window_handle()
.downcast::<workspace::Workspace>()
.unwrap();
let count = Arc::new(AtomicUsize::new(0));
let session = debugger_ui::tests::start_debug_session_with(
&workspace_window,
cx_a,
DebugTaskDefinition {
adapter: "fake-adapter".into(),
label: "test".into(),
config: json!({
"request": "launch"
}),
tcp_connection: Some(TcpArgumentsTemplate {
port: None,
host: None,
timeout: None,
}),
},
move |client| {
let count = count.clone();
client.on_request_ext::<dap::requests::Initialize, _>(move |_seq, _request| {
if count.fetch_add(1, std::sync::atomic::Ordering::SeqCst) < 5 {
return RequestHandling::Exit;
}
RequestHandling::Respond(Ok(Capabilities::default()))
});
},
)
.unwrap();
cx_a.run_until_parked();
let client = session.update(cx_a, |session, _| session.adapter_client().unwrap());
client
.fake_event(dap::messages::Events::Stopped(dap::StoppedEvent {
reason: dap::StoppedEventReason::Pause,
description: None,
thread_id: Some(1),
preserve_focus_hint: None,
text: None,
all_threads_stopped: None,
hit_breakpoint_ids: None,
}))
.await;
cx_a.run_until_parked();
let active_session = debug_panel
.update(cx_a, |this, _| this.active_session())
.unwrap();
let running_state = active_session.update(cx_a, |active_session, _| {
active_session.running_state().clone()
});
assert_eq!(
client.id(),
running_state.read_with(cx_a, |running_state, _| running_state.session_id())
);
assert_eq!(
ThreadId(1),
running_state.read_with(cx_a, |running_state, _| running_state
.selected_thread_id()
.unwrap())
);
let shutdown_session = workspace.update(cx_a, |workspace, cx| {
workspace.project().update(cx, |project, cx| {
project.dap_store().update(cx, |dap_store, cx| {
dap_store.shutdown_session(session.read(cx).session_id(), cx)
})
})
});
client_ssh.update(cx_a, |a, _| {
a.shutdown_processes(Some(proto::ShutdownRemoteServer {}), executor)
});
shutdown_session.await.unwrap();
}

View File

@@ -48,20 +48,20 @@ impl RenderOnce for ComponentExample {
)
.child(
div()
.flex()
.w_full()
.rounded_xl()
.min_h(px(100.))
.justify_center()
.w_full()
.p_8()
.flex()
.items_center()
.justify_center()
.rounded_xl()
.border_1()
.border_color(cx.theme().colors().border.opacity(0.5))
.bg(pattern_slash(
cx.theme().colors().surface_background.opacity(0.5),
cx.theme().colors().surface_background.opacity(0.25),
12.0,
12.0,
))
.shadow_xs()
.child(self.element),
)
.into_any_element()

View File

@@ -378,6 +378,14 @@ pub trait DebugAdapter: 'static + Send + Sync {
fn label_for_child_session(&self, _args: &StartDebuggingRequestArguments) -> Option<String> {
None
}
fn compact_child_session(&self) -> bool {
false
}
fn prefer_thread_name(&self) -> bool {
false
}
}
#[cfg(any(test, feature = "test-support"))]
@@ -442,10 +450,18 @@ impl DebugAdapter for FakeAdapter {
_: Option<Vec<String>>,
_: &mut AsyncApp,
) -> Result<DebugAdapterBinary> {
let connection = task_definition
.tcp_connection
.as_ref()
.map(|connection| TcpArguments {
host: connection.host(),
port: connection.port.unwrap_or(17),
timeout: connection.timeout,
});
Ok(DebugAdapterBinary {
command: Some("command".into()),
arguments: vec![],
connection: None,
connection,
envs: HashMap::default(),
cwd: None,
request_args: StartDebuggingRequestArguments {

View File

@@ -2,7 +2,7 @@ use crate::{
adapters::DebugAdapterBinary,
transport::{IoKind, LogKind, TransportDelegate},
};
use anyhow::{Context as _, Result};
use anyhow::Result;
use dap_types::{
messages::{Message, Response},
requests::Request,
@@ -110,9 +110,7 @@ impl DebugAdapterClient {
self.transport_delegate
.pending_requests
.lock()
.as_mut()
.context("client is closed")?
.insert(sequence_id, callback_tx);
.insert(sequence_id, callback_tx)?;
log::debug!(
"Client {} send `{}` request with sequence_id: {}",
@@ -170,6 +168,7 @@ impl DebugAdapterClient {
pub fn kill(&self) {
log::debug!("Killing DAP process");
self.transport_delegate.transport.lock().kill();
self.transport_delegate.pending_requests.lock().shutdown();
}
pub fn has_adapter_logs(&self) -> bool {
@@ -184,11 +183,34 @@ impl DebugAdapterClient {
}
#[cfg(any(test, feature = "test-support"))]
pub fn on_request<R: dap_types::requests::Request, F>(&self, handler: F)
pub fn on_request<R: dap_types::requests::Request, F>(&self, mut handler: F)
where
F: 'static
+ Send
+ FnMut(u64, R::Arguments) -> Result<R::Response, dap_types::ErrorResponse>,
{
use crate::transport::RequestHandling;
self.transport_delegate
.transport
.lock()
.as_fake()
.on_request::<R, _>(move |seq, request| {
RequestHandling::Respond(handler(seq, request))
});
}
#[cfg(any(test, feature = "test-support"))]
pub fn on_request_ext<R: dap_types::requests::Request, F>(&self, handler: F)
where
F: 'static
+ Send
+ FnMut(
u64,
R::Arguments,
) -> crate::transport::RequestHandling<
Result<R::Response, dap_types::ErrorResponse>,
>,
{
self.transport_delegate
.transport

View File

@@ -49,6 +49,12 @@ pub enum IoKind {
StdErr,
}
#[cfg(any(test, feature = "test-support"))]
pub enum RequestHandling<T> {
Respond(T),
Exit,
}
type LogHandlers = Arc<Mutex<SmallVec<[(LogKind, IoHandler); 2]>>>;
pub trait Transport: Send + Sync {
@@ -76,7 +82,11 @@ async fn start(
) -> Result<Box<dyn Transport>> {
#[cfg(any(test, feature = "test-support"))]
if cfg!(any(test, feature = "test-support")) {
return Ok(Box::new(FakeTransport::start(cx).await?));
if let Some(connection) = binary.connection.clone() {
return Ok(Box::new(FakeTransport::start_tcp(connection, cx).await?));
} else {
return Ok(Box::new(FakeTransport::start_stdio(cx).await?));
}
}
if binary.connection.is_some() {
@@ -90,11 +100,57 @@ async fn start(
}
}
pub(crate) struct PendingRequests {
inner: Option<HashMap<u64, oneshot::Sender<Result<Response>>>>,
}
impl PendingRequests {
fn new() -> Self {
Self {
inner: Some(HashMap::default()),
}
}
fn flush(&mut self, e: anyhow::Error) {
let Some(inner) = self.inner.as_mut() else {
return;
};
for (_, sender) in inner.drain() {
sender.send(Err(e.cloned())).ok();
}
}
pub(crate) fn insert(
&mut self,
sequence_id: u64,
callback_tx: oneshot::Sender<Result<Response>>,
) -> anyhow::Result<()> {
let Some(inner) = self.inner.as_mut() else {
bail!("client is closed")
};
inner.insert(sequence_id, callback_tx);
Ok(())
}
pub(crate) fn remove(
&mut self,
sequence_id: u64,
) -> anyhow::Result<Option<oneshot::Sender<Result<Response>>>> {
let Some(inner) = self.inner.as_mut() else {
bail!("client is closed");
};
Ok(inner.remove(&sequence_id))
}
pub(crate) fn shutdown(&mut self) {
self.flush(anyhow!("transport shutdown"));
self.inner = None;
}
}
pub(crate) struct TransportDelegate {
log_handlers: LogHandlers,
// TODO this should really be some kind of associative channel
pub(crate) pending_requests:
Arc<Mutex<Option<HashMap<u64, oneshot::Sender<Result<Response>>>>>>,
pub(crate) pending_requests: Arc<Mutex<PendingRequests>>,
pub(crate) transport: Mutex<Box<dyn Transport>>,
pub(crate) server_tx: smol::lock::Mutex<Option<Sender<Message>>>,
tasks: Mutex<Vec<Task<()>>>,
@@ -108,7 +164,7 @@ impl TransportDelegate {
transport: Mutex::new(transport),
log_handlers,
server_tx: Default::default(),
pending_requests: Arc::new(Mutex::new(Some(HashMap::default()))),
pending_requests: Arc::new(Mutex::new(PendingRequests::new())),
tasks: Default::default(),
})
}
@@ -151,24 +207,10 @@ impl TransportDelegate {
Ok(()) => {
pending_requests
.lock()
.take()
.into_iter()
.flatten()
.for_each(|(_, request)| {
request
.send(Err(anyhow!("debugger shutdown unexpectedly")))
.ok();
});
.flush(anyhow!("debugger shutdown unexpectedly"));
}
Err(e) => {
pending_requests
.lock()
.take()
.into_iter()
.flatten()
.for_each(|(_, request)| {
request.send(Err(e.cloned())).ok();
});
pending_requests.lock().flush(e);
}
}
}));
@@ -286,7 +328,7 @@ impl TransportDelegate {
async fn recv_from_server<Stdout>(
server_stdout: Stdout,
mut message_handler: DapMessageHandler,
pending_requests: Arc<Mutex<Option<HashMap<u64, oneshot::Sender<Result<Response>>>>>>,
pending_requests: Arc<Mutex<PendingRequests>>,
log_handlers: Option<LogHandlers>,
) -> Result<()>
where
@@ -303,14 +345,10 @@ impl TransportDelegate {
ConnectionResult::Timeout => anyhow::bail!("Timed out when connecting to debugger"),
ConnectionResult::ConnectionReset => {
log::info!("Debugger closed the connection");
break Ok(());
return Ok(());
}
ConnectionResult::Result(Ok(Message::Response(res))) => {
let tx = pending_requests
.lock()
.as_mut()
.context("client is closed")?
.remove(&res.request_seq);
let tx = pending_requests.lock().remove(res.request_seq)?;
if let Some(tx) = tx {
if let Err(e) = tx.send(Self::process_response(res)) {
log::trace!("Did not send response `{:?}` for a cancelled", e);
@@ -704,8 +742,7 @@ impl Drop for StdioTransport {
}
#[cfg(any(test, feature = "test-support"))]
type RequestHandler =
Box<dyn Send + FnMut(u64, serde_json::Value) -> dap_types::messages::Response>;
type RequestHandler = Box<dyn Send + FnMut(u64, serde_json::Value) -> RequestHandling<Response>>;
#[cfg(any(test, feature = "test-support"))]
type ResponseHandler = Box<dyn Send + Fn(Response)>;
@@ -716,23 +753,38 @@ pub struct FakeTransport {
request_handlers: Arc<Mutex<HashMap<&'static str, RequestHandler>>>,
// for reverse request responses
response_handlers: Arc<Mutex<HashMap<&'static str, ResponseHandler>>>,
stdin_writer: Option<PipeWriter>,
stdout_reader: Option<PipeReader>,
message_handler: Option<Task<Result<()>>>,
kind: FakeTransportKind,
}
#[cfg(any(test, feature = "test-support"))]
pub enum FakeTransportKind {
Stdio {
stdin_writer: Option<PipeWriter>,
stdout_reader: Option<PipeReader>,
},
Tcp {
connection: TcpArguments,
executor: BackgroundExecutor,
},
}
#[cfg(any(test, feature = "test-support"))]
impl FakeTransport {
pub fn on_request<R: dap_types::requests::Request, F>(&self, mut handler: F)
where
F: 'static + Send + FnMut(u64, R::Arguments) -> Result<R::Response, ErrorResponse>,
F: 'static
+ Send
+ FnMut(u64, R::Arguments) -> RequestHandling<Result<R::Response, ErrorResponse>>,
{
self.request_handlers.lock().insert(
R::COMMAND,
Box::new(move |seq, args| {
let result = handler(seq, serde_json::from_value(args).unwrap());
let response = match result {
let RequestHandling::Respond(response) = result else {
return RequestHandling::Exit;
};
let response = match response {
Ok(response) => Response {
seq: seq + 1,
request_seq: seq,
@@ -750,7 +802,7 @@ impl FakeTransport {
message: None,
},
};
response
RequestHandling::Respond(response)
}),
);
}
@@ -764,86 +816,75 @@ impl FakeTransport {
.insert(R::COMMAND, Box::new(handler));
}
async fn start(cx: &mut AsyncApp) -> Result<Self> {
async fn start_tcp(connection: TcpArguments, cx: &mut AsyncApp) -> Result<Self> {
Ok(Self {
request_handlers: Arc::new(Mutex::new(HashMap::default())),
response_handlers: Arc::new(Mutex::new(HashMap::default())),
message_handler: None,
kind: FakeTransportKind::Tcp {
connection,
executor: cx.background_executor().clone(),
},
})
}
async fn handle_messages(
request_handlers: Arc<Mutex<HashMap<&'static str, RequestHandler>>>,
response_handlers: Arc<Mutex<HashMap<&'static str, ResponseHandler>>>,
stdin_reader: PipeReader,
stdout_writer: PipeWriter,
) -> Result<()> {
use dap_types::requests::{Request, RunInTerminal, StartDebugging};
use serde_json::json;
let (stdin_writer, stdin_reader) = async_pipe::pipe();
let (stdout_writer, stdout_reader) = async_pipe::pipe();
let mut this = Self {
request_handlers: Arc::new(Mutex::new(HashMap::default())),
response_handlers: Arc::new(Mutex::new(HashMap::default())),
stdin_writer: Some(stdin_writer),
stdout_reader: Some(stdout_reader),
message_handler: None,
};
let request_handlers = this.request_handlers.clone();
let response_handlers = this.response_handlers.clone();
let mut reader = BufReader::new(stdin_reader);
let stdout_writer = Arc::new(smol::lock::Mutex::new(stdout_writer));
let mut buffer = String::new();
this.message_handler = Some(cx.background_spawn(async move {
let mut reader = BufReader::new(stdin_reader);
let mut buffer = String::new();
loop {
match TransportDelegate::receive_server_message(&mut reader, &mut buffer, None)
.await
{
ConnectionResult::Timeout => {
anyhow::bail!("Timed out when connecting to debugger");
}
ConnectionResult::ConnectionReset => {
log::info!("Debugger closed the connection");
break Ok(());
}
ConnectionResult::Result(Err(e)) => break Err(e),
ConnectionResult::Result(Ok(message)) => {
match message {
Message::Request(request) => {
// redirect reverse requests to stdout writer/reader
if request.command == RunInTerminal::COMMAND
|| request.command == StartDebugging::COMMAND
{
let message =
serde_json::to_string(&Message::Request(request)).unwrap();
let mut writer = stdout_writer.lock().await;
writer
.write_all(
TransportDelegate::build_rpc_message(message)
.as_bytes(),
)
.await
.unwrap();
writer.flush().await.unwrap();
} else {
let response = if let Some(handle) =
request_handlers.lock().get_mut(request.command.as_str())
{
handle(request.seq, request.arguments.unwrap_or(json!({})))
} else {
panic!("No request handler for {}", request.command);
};
let message =
serde_json::to_string(&Message::Response(response))
.unwrap();
let mut writer = stdout_writer.lock().await;
writer
.write_all(
TransportDelegate::build_rpc_message(message)
.as_bytes(),
)
.await
.unwrap();
writer.flush().await.unwrap();
}
}
Message::Event(event) => {
loop {
match TransportDelegate::receive_server_message(&mut reader, &mut buffer, None).await {
ConnectionResult::Timeout => {
anyhow::bail!("Timed out when connecting to debugger");
}
ConnectionResult::ConnectionReset => {
log::info!("Debugger closed the connection");
break Ok(());
}
ConnectionResult::Result(Err(e)) => break Err(e),
ConnectionResult::Result(Ok(message)) => {
match message {
Message::Request(request) => {
// redirect reverse requests to stdout writer/reader
if request.command == RunInTerminal::COMMAND
|| request.command == StartDebugging::COMMAND
{
let message =
serde_json::to_string(&Message::Event(event)).unwrap();
serde_json::to_string(&Message::Request(request)).unwrap();
let mut writer = stdout_writer.lock().await;
writer
.write_all(
TransportDelegate::build_rpc_message(message).as_bytes(),
)
.await
.unwrap();
writer.flush().await.unwrap();
} else {
let response = if let Some(handle) =
request_handlers.lock().get_mut(request.command.as_str())
{
handle(request.seq, request.arguments.unwrap_or(json!({})))
} else {
panic!("No request handler for {}", request.command);
};
let response = match response {
RequestHandling::Respond(response) => response,
RequestHandling::Exit => {
break Err(anyhow!("exit in response to request"));
}
};
let message =
serde_json::to_string(&Message::Response(response)).unwrap();
let mut writer = stdout_writer.lock().await;
writer
@@ -854,20 +895,56 @@ impl FakeTransport {
.unwrap();
writer.flush().await.unwrap();
}
Message::Response(response) => {
if let Some(handle) =
response_handlers.lock().get(response.command.as_str())
{
handle(response);
} else {
log::error!("No response handler for {}", response.command);
}
}
Message::Event(event) => {
let message = serde_json::to_string(&Message::Event(event)).unwrap();
let mut writer = stdout_writer.lock().await;
writer
.write_all(TransportDelegate::build_rpc_message(message).as_bytes())
.await
.unwrap();
writer.flush().await.unwrap();
}
Message::Response(response) => {
if let Some(handle) =
response_handlers.lock().get(response.command.as_str())
{
handle(response);
} else {
log::error!("No response handler for {}", response.command);
}
}
}
}
}
}));
}
}
async fn start_stdio(cx: &mut AsyncApp) -> Result<Self> {
let (stdin_writer, stdin_reader) = async_pipe::pipe();
let (stdout_writer, stdout_reader) = async_pipe::pipe();
let kind = FakeTransportKind::Stdio {
stdin_writer: Some(stdin_writer),
stdout_reader: Some(stdout_reader),
};
let mut this = Self {
request_handlers: Arc::new(Mutex::new(HashMap::default())),
response_handlers: Arc::new(Mutex::new(HashMap::default())),
message_handler: None,
kind,
};
let request_handlers = this.request_handlers.clone();
let response_handlers = this.response_handlers.clone();
this.message_handler = Some(cx.background_spawn(Self::handle_messages(
request_handlers,
response_handlers,
stdin_reader,
stdout_writer,
)));
Ok(this)
}
@@ -876,7 +953,10 @@ impl FakeTransport {
#[cfg(any(test, feature = "test-support"))]
impl Transport for FakeTransport {
fn tcp_arguments(&self) -> Option<TcpArguments> {
None
match &self.kind {
FakeTransportKind::Stdio { .. } => None,
FakeTransportKind::Tcp { connection, .. } => Some(connection.clone()),
}
}
fn connect(
@@ -887,12 +967,33 @@ impl Transport for FakeTransport {
Box<dyn AsyncRead + Unpin + Send + 'static>,
)>,
> {
let result = util::maybe!({
Ok((
Box::new(self.stdin_writer.take().context("Cannot reconnect")?) as _,
Box::new(self.stdout_reader.take().context("Cannot reconnect")?) as _,
))
});
let result = match &mut self.kind {
FakeTransportKind::Stdio {
stdin_writer,
stdout_reader,
} => util::maybe!({
Ok((
Box::new(stdin_writer.take().context("Cannot reconnect")?) as _,
Box::new(stdout_reader.take().context("Cannot reconnect")?) as _,
))
}),
FakeTransportKind::Tcp { executor, .. } => {
let (stdin_writer, stdin_reader) = async_pipe::pipe();
let (stdout_writer, stdout_reader) = async_pipe::pipe();
let request_handlers = self.request_handlers.clone();
let response_handlers = self.response_handlers.clone();
self.message_handler = Some(executor.spawn(Self::handle_messages(
request_handlers,
response_handlers,
stdin_reader,
stdout_writer,
)));
Ok((Box::new(stdin_writer) as _, Box::new(stdout_reader) as _))
}
};
Task::ready(result)
}

View File

@@ -547,6 +547,7 @@ async fn handle_envs(
}
};
let mut env_vars = HashMap::default();
for path in env_files {
let Some(path) = path
.and_then(|s| PathBuf::from_str(s).ok())
@@ -556,13 +557,33 @@ async fn handle_envs(
};
if let Ok(file) = fs.open_sync(&path).await {
envs.extend(dotenvy::from_read_iter(file).filter_map(Result::ok))
let file_envs: HashMap<String, String> = dotenvy::from_read_iter(file)
.filter_map(Result::ok)
.collect();
envs.extend(file_envs.iter().map(|(k, v)| (k.clone(), v.clone())));
env_vars.extend(file_envs);
} else {
warn!("While starting Go debug session: failed to read env file {path:?}");
};
}
let mut env_obj: serde_json::Map<String, Value> = serde_json::Map::new();
for (k, v) in env_vars {
env_obj.insert(k, Value::String(v));
}
if let Some(existing_env) = config.get("env").and_then(|v| v.as_object()) {
for (k, v) in existing_env {
env_obj.insert(k.clone(), v.clone());
}
}
if !env_obj.is_empty() {
config.insert("env".to_string(), Value::Object(env_obj));
}
// remove envFile now that it's been handled
config.remove("entry");
config.remove("envFile");
Some(())
}

View File

@@ -54,20 +54,6 @@ impl JsDebugAdapter {
user_args: Option<Vec<String>>,
_: &mut AsyncApp,
) -> Result<DebugAdapterBinary> {
let adapter_path = 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.name());
util::fs::find_file_name_in_dir(adapter_path.as_path(), |file_name| {
file_name.starts_with(&file_name_prefix)
})
.await
.context("Couldn't find JavaScript dap directory")?
};
let tcp_connection = task_definition.tcp_connection.clone().unwrap_or_default();
let (host, port, timeout) = crate::configure_tcp_connection(tcp_connection).await?;
@@ -136,21 +122,27 @@ impl JsDebugAdapter {
.or_insert(true.into());
}
let adapter_path = 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.name());
util::fs::find_file_name_in_dir(adapter_path.as_path(), |file_name| {
file_name.starts_with(&file_name_prefix)
})
.await
.context("Couldn't find JavaScript dap directory")?
.join(Self::ADAPTER_PATH)
};
let arguments = if let Some(mut args) = user_args {
args.insert(
0,
adapter_path
.join(Self::ADAPTER_PATH)
.to_string_lossy()
.to_string(),
);
args.insert(0, adapter_path.to_string_lossy().to_string());
args
} else {
vec![
adapter_path
.join(Self::ADAPTER_PATH)
.to_string_lossy()
.to_string(),
adapter_path.to_string_lossy().to_string(),
port.to_string(),
host.to_string(),
]
@@ -534,6 +526,14 @@ impl DebugAdapter for JsDebugAdapter {
.filter(|name| !name.is_empty())?;
Some(label.to_owned())
}
fn compact_child_session(&self) -> bool {
true
}
fn prefer_thread_name(&self) -> bool {
true
}
}
fn normalize_task_type(task_type: &mut Value) {

View File

@@ -40,12 +40,7 @@ impl PythonDebugAdapter {
"Using user-installed debugpy adapter from: {}",
user_installed_path.display()
);
vec![
user_installed_path
.join(Self::ADAPTER_PATH)
.to_string_lossy()
.to_string(),
]
vec![user_installed_path.to_string_lossy().to_string()]
} else if installed_in_venv {
log::debug!("Using venv-installed debugpy");
vec!["-m".to_string(), "debugpy.adapter".to_string()]
@@ -700,7 +695,7 @@ mod tests {
let port = 5678;
// Case 1: User-defined debugpy path (highest precedence)
let user_path = PathBuf::from("/custom/path/to/debugpy");
let user_path = PathBuf::from("/custom/path/to/debugpy/src/debugpy/adapter");
let user_args = PythonDebugAdapter::generate_debugpy_arguments(
&host,
port,
@@ -717,7 +712,7 @@ mod tests {
.await
.unwrap();
assert!(user_args[0].ends_with("src/debugpy/adapter"));
assert_eq!(user_args[0], "/custom/path/to/debugpy/src/debugpy/adapter");
assert_eq!(user_args[1], "--host=127.0.0.1");
assert_eq!(user_args[2], "--port=5678");

View File

@@ -32,12 +32,19 @@ use workspace::{
ui::{Button, Clickable, ContextMenu, Label, LabelCommon, PopoverMenu, h_flex},
};
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
enum View {
AdapterLogs,
RpcMessages,
InitializationSequence,
}
struct DapLogView {
editor: Entity<Editor>,
focus_handle: FocusHandle,
log_store: Entity<LogStore>,
editor_subscriptions: Vec<Subscription>,
current_view: Option<(SessionId, LogKind)>,
current_view: Option<(SessionId, View)>,
project: Entity<Project>,
_subscriptions: Vec<Subscription>,
}
@@ -77,6 +84,7 @@ struct DebugAdapterState {
id: SessionId,
log_messages: VecDeque<SharedString>,
rpc_messages: RpcMessages,
session_label: SharedString,
adapter_name: DebugAdapterName,
has_adapter_logs: bool,
is_terminated: bool,
@@ -121,12 +129,18 @@ impl MessageKind {
}
impl DebugAdapterState {
fn new(id: SessionId, adapter_name: DebugAdapterName, has_adapter_logs: bool) -> Self {
fn new(
id: SessionId,
adapter_name: DebugAdapterName,
session_label: SharedString,
has_adapter_logs: bool,
) -> Self {
Self {
id,
log_messages: VecDeque::new(),
rpc_messages: RpcMessages::new(),
adapter_name,
session_label,
has_adapter_logs,
is_terminated: false,
}
@@ -371,18 +385,22 @@ impl LogStore {
return None;
};
let (adapter_name, has_adapter_logs) = session.read_with(cx, |session, _| {
(
session.adapter(),
session
.adapter_client()
.map_or(false, |client| client.has_adapter_logs()),
)
});
let (adapter_name, session_label, has_adapter_logs) =
session.read_with(cx, |session, _| {
(
session.adapter(),
session.label(),
session
.adapter_client()
.map_or(false, |client| client.has_adapter_logs()),
)
});
state.insert(DebugAdapterState::new(
id.session_id,
adapter_name,
session_label
.unwrap_or_else(|| format!("Session {} (child)", id.session_id.0).into()),
has_adapter_logs,
));
@@ -506,12 +524,13 @@ impl Render for DapLogToolbarItemView {
current_client
.map(|sub_item| {
Cow::Owned(format!(
"{} ({}) - {}",
"{} - {} - {}",
sub_item.adapter_name,
sub_item.session_id.0,
sub_item.session_label,
match sub_item.selected_entry {
LogKind::Adapter => ADAPTER_LOGS,
LogKind::Rpc => RPC_MESSAGES,
View::AdapterLogs => ADAPTER_LOGS,
View::RpcMessages => RPC_MESSAGES,
View::InitializationSequence => INITIALIZATION_SEQUENCE,
}
))
})
@@ -529,8 +548,8 @@ impl Render for DapLogToolbarItemView {
.pl_2()
.child(
Label::new(format!(
"{}. {}",
row.session_id.0, row.adapter_name,
"{} - {}",
row.adapter_name, row.session_label
))
.color(workspace::ui::Color::Muted),
)
@@ -669,9 +688,16 @@ impl DapLogView {
let events_subscriptions = cx.subscribe(&log_store, |log_view, _, event, cx| match event {
Event::NewLogEntry { id, entry, kind } => {
if log_view.current_view == Some((id.session_id, *kind))
&& log_view.project == *id.project
{
let is_current_view = match (log_view.current_view, *kind) {
(Some((i, View::AdapterLogs)), LogKind::Adapter)
| (Some((i, View::RpcMessages)), LogKind::Rpc)
if i == id.session_id =>
{
log_view.project == *id.project
}
_ => false,
};
if is_current_view {
log_view.editor.update(cx, |editor, cx| {
editor.set_read_only(false);
let last_point = editor.buffer().read(cx).len(cx);
@@ -768,10 +794,11 @@ impl DapLogView {
.map(|state| DapMenuItem {
session_id: state.id,
adapter_name: state.adapter_name.clone(),
session_label: state.session_label.clone(),
has_adapter_logs: state.has_adapter_logs,
selected_entry: self
.current_view
.map_or(LogKind::Adapter, |(_, kind)| kind),
.map_or(View::AdapterLogs, |(_, kind)| kind),
})
.collect::<Vec<_>>()
})
@@ -789,7 +816,7 @@ impl DapLogView {
.map(|state| log_contents(state.iter().cloned()))
});
if let Some(rpc_log) = rpc_log {
self.current_view = Some((id.session_id, LogKind::Rpc));
self.current_view = Some((id.session_id, View::RpcMessages));
let (editor, editor_subscriptions) = Self::editor_for_logs(rpc_log, window, cx);
let language = self.project.read(cx).languages().language_for_name("JSON");
editor
@@ -830,7 +857,7 @@ impl DapLogView {
.map(|state| log_contents(state.iter().cloned()))
});
if let Some(message_log) = message_log {
self.current_view = Some((id.session_id, LogKind::Adapter));
self.current_view = Some((id.session_id, View::AdapterLogs));
let (editor, editor_subscriptions) = Self::editor_for_logs(message_log, window, cx);
editor
.read(cx)
@@ -859,7 +886,7 @@ impl DapLogView {
.map(|state| log_contents(state.iter().cloned()))
});
if let Some(rpc_log) = rpc_log {
self.current_view = Some((id.session_id, LogKind::Rpc));
self.current_view = Some((id.session_id, View::InitializationSequence));
let (editor, editor_subscriptions) = Self::editor_for_logs(rpc_log, window, cx);
let language = self.project.read(cx).languages().language_for_name("JSON");
editor
@@ -899,11 +926,12 @@ fn log_contents(lines: impl Iterator<Item = SharedString>) -> String {
}
#[derive(Clone, PartialEq)]
pub(crate) struct DapMenuItem {
pub session_id: SessionId,
pub adapter_name: DebugAdapterName,
pub has_adapter_logs: bool,
pub selected_entry: LogKind,
struct DapMenuItem {
session_id: SessionId,
session_label: SharedString,
adapter_name: DebugAdapterName,
has_adapter_logs: bool,
selected_entry: View,
}
const ADAPTER_LOGS: &str = "Adapter Logs";

View File

@@ -40,12 +40,15 @@ file_icons.workspace = true
futures.workspace = true
fuzzy.workspace = true
gpui.workspace = true
hex.workspace = true
indoc.workspace = true
itertools.workspace = true
language.workspace = true
log.workspace = true
menu.workspace = true
notifications.workspace = true
parking_lot.workspace = true
parse_int.workspace = true
paths.workspace = true
picker.workspace = true
pretty_assertions.workspace = true

View File

@@ -2,6 +2,7 @@ use crate::persistence::DebuggerPaneItem;
use crate::session::DebugSession;
use crate::session::running::RunningState;
use crate::session::running::breakpoint_list::BreakpointList;
use crate::{
ClearAllBreakpoints, Continue, CopyDebugAdapterArguments, Detach, FocusBreakpointList,
FocusConsole, FocusFrames, FocusLoadedSources, FocusModules, FocusTerminal, FocusVariables,
@@ -9,6 +10,7 @@ use crate::{
ToggleExpandItem, ToggleSessionPicker, ToggleThreadPicker, persistence, spawn_task_or_modal,
};
use anyhow::{Context as _, Result, anyhow};
use collections::IndexMap;
use dap::adapters::DebugAdapterName;
use dap::debugger_settings::DebugPanelDockPosition;
use dap::{
@@ -26,7 +28,7 @@ use text::ToPoint as _;
use itertools::Itertools as _;
use language::Buffer;
use project::debugger::session::{Session, SessionStateEvent};
use project::debugger::session::{Session, SessionQuirks, SessionState, SessionStateEvent};
use project::{DebugScenarioContext, Fs, ProjectPath, TaskSourceKind, WorktreeId};
use project::{Project, debugger::session::ThreadStatus};
use rpc::proto::{self};
@@ -35,13 +37,13 @@ use std::sync::{Arc, LazyLock};
use task::{DebugScenario, TaskContext};
use tree_sitter::{Query, StreamingIterator as _};
use ui::{ContextMenu, Divider, PopoverMenuHandle, Tooltip, prelude::*};
use util::{ResultExt, maybe};
use util::{ResultExt, debug_panic, maybe};
use workspace::SplitDirection;
use workspace::item::SaveOptions;
use workspace::{
Item, Pane, Workspace,
dock::{DockPosition, Panel, PanelEvent},
};
use workspace::{OpenInDebugJson, SplitDirection};
use zed_actions::ToggleFocus;
pub enum DebugPanelEvent {
@@ -63,13 +65,14 @@ pub enum DebugPanelEvent {
pub struct DebugPanel {
size: Pixels,
sessions: Vec<Entity<DebugSession>>,
active_session: Option<Entity<DebugSession>>,
project: Entity<Project>,
workspace: WeakEntity<Workspace>,
focus_handle: FocusHandle,
context_menu: Option<(Entity<ContextMenu>, Point<Pixels>, Subscription)>,
debug_scenario_scheduled_last: bool,
pub(crate) sessions_with_children:
IndexMap<Entity<DebugSession>, Vec<WeakEntity<DebugSession>>>,
pub(crate) thread_picker_menu_handle: PopoverMenuHandle<ContextMenu>,
pub(crate) session_picker_menu_handle: PopoverMenuHandle<ContextMenu>,
fs: Arc<dyn Fs>,
@@ -98,28 +101,9 @@ impl DebugPanel {
},
);
if let Some(entity) = workspace.weak_handle().upgrade() {
let edit_scenario_subscription = cx.subscribe_in(
&entity,
window,
move |this, workspace, OpenInDebugJson { scenario, id }, window, cx| {
let task = this.go_to_scenario_definition(
TaskSourceKind::UserInput,
scenario.clone(),
todo!(),
// *id,
window,
cx,
);
cx.spawn(async move |_, cx| task.await)
.detach_and_log_err(cx);
},
);
}
Self {
size: px(300.),
sessions: vec![],
sessions_with_children: Default::default(),
active_session: None,
focus_handle,
breakpoint_list: BreakpointList::new(
@@ -157,8 +141,9 @@ impl DebugPanel {
});
}
pub(crate) fn sessions(&self) -> Vec<Entity<DebugSession>> {
self.sessions.clone()
#[cfg(test)]
pub(crate) fn sessions(&self) -> impl Iterator<Item = Entity<DebugSession>> {
self.sessions_with_children.keys().cloned()
}
pub fn active_session(&self) -> Option<Entity<DebugSession>> {
@@ -204,12 +189,20 @@ impl DebugPanel {
cx: &mut Context<Self>,
) {
let dap_store = self.project.read(cx).dap_store();
let Some(adapter) = DapRegistry::global(cx).adapter(&scenario.adapter) else {
return;
};
let quirks = SessionQuirks {
compact: adapter.compact_child_session(),
prefer_thread_name: adapter.prefer_thread_name(),
};
let session = dap_store.update(cx, |dap_store, cx| {
dap_store.new_session(
scenario.label.clone(),
Some(scenario.label.clone()),
DebugAdapterName(scenario.adapter.clone()),
task_context.clone(),
None,
quirks,
cx,
)
});
@@ -286,22 +279,34 @@ impl DebugPanel {
}
});
cx.spawn(async move |_, cx| {
if let Err(error) = task.await {
log::error!("{error}");
session
.update(cx, |session, cx| {
session
.console_output(cx)
.unbounded_send(format!("error: {}", error))
.ok();
session.shutdown(cx)
})?
.await;
let boot_task = cx.spawn({
let session = session.clone();
async move |_, cx| {
if let Err(error) = task.await {
log::error!("{error}");
session
.update(cx, |session, cx| {
session
.console_output(cx)
.unbounded_send(format!("error: {}", error))
.ok();
session.shutdown(cx)
})?
.await;
}
anyhow::Ok(())
}
anyhow::Ok(())
})
.detach_and_log_err(cx);
});
session.update(cx, |session, _| match &mut session.mode {
SessionState::Building(state_task) => {
*state_task = Some(boot_task);
}
SessionState::Running(_) => {
debug_panic!("Session state should be in building because we are just starting it");
}
});
}
pub(crate) fn rerun_last_session(
@@ -382,14 +387,15 @@ impl DebugPanel {
};
let dap_store_handle = self.project.read(cx).dap_store().clone();
let label = curr_session.read(cx).label().clone();
let label = curr_session.read(cx).label();
let quirks = curr_session.read(cx).quirks();
let adapter = curr_session.read(cx).adapter().clone();
let binary = curr_session.read(cx).binary().cloned().unwrap();
let task_context = curr_session.read(cx).task_context().clone();
let curr_session_id = curr_session.read(cx).session_id();
self.sessions
.retain(|session| session.read(cx).session_id(cx) != curr_session_id);
self.sessions_with_children
.retain(|session, _| session.read(cx).session_id(cx) != curr_session_id);
let task = dap_store_handle.update(cx, |dap_store, cx| {
dap_store.shutdown_session(curr_session_id, cx)
});
@@ -398,7 +404,7 @@ impl DebugPanel {
task.await.log_err();
let (session, task) = dap_store_handle.update(cx, |dap_store, cx| {
let session = dap_store.new_session(label, adapter, task_context, None, cx);
let session = dap_store.new_session(label, adapter, task_context, None, quirks, cx);
let task = session.update(cx, |session, cx| {
session.boot(binary, worktree, dap_store_handle.downgrade(), cx)
@@ -444,6 +450,7 @@ impl DebugPanel {
let dap_store_handle = self.project.read(cx).dap_store().clone();
let label = self.label_for_child_session(&parent_session, request, cx);
let adapter = parent_session.read(cx).adapter().clone();
let quirks = parent_session.read(cx).quirks();
let Some(mut binary) = parent_session.read(cx).binary().cloned() else {
log::error!("Attempted to start a child-session without a binary");
return;
@@ -457,6 +464,7 @@ impl DebugPanel {
adapter,
task_context,
Some(parent_session.clone()),
quirks,
cx,
);
@@ -482,8 +490,8 @@ impl DebugPanel {
cx: &mut Context<Self>,
) {
let Some(session) = self
.sessions
.iter()
.sessions_with_children
.keys()
.find(|other| entity_id == other.entity_id())
.cloned()
else {
@@ -517,15 +525,14 @@ impl DebugPanel {
}
session.update(cx, |session, cx| session.shutdown(cx)).ok();
this.update(cx, |this, cx| {
this.sessions.retain(|other| entity_id != other.entity_id());
this.retain_sessions(|other| entity_id != other.entity_id());
if let Some(active_session_id) = this
.active_session
.as_ref()
.map(|session| session.entity_id())
{
if active_session_id == entity_id {
this.active_session = this.sessions.first().cloned();
this.active_session = this.sessions_with_children.keys().next().cloned();
}
}
cx.notify()
@@ -832,13 +839,24 @@ impl DebugPanel {
.on_click(window.listener_for(
&running_state,
|this, _, _window, cx| {
this.stop_thread(cx);
if this.session().read(cx).is_building() {
this.session().update(cx, |session, cx| {
session.shutdown(cx).detach()
});
} else {
this.stop_thread(cx);
}
},
))
.disabled(active_session.as_ref().is_none_or(
|session| {
session
.read(cx)
.session(cx)
.read(cx)
.is_terminated()
},
))
.disabled(
thread_status != ThreadStatus::Stopped
&& thread_status != ThreadStatus::Running,
)
.tooltip({
let focus_handle = focus_handle.clone();
let label = if capabilities
@@ -995,8 +1013,8 @@ impl DebugPanel {
cx: &mut Context<Self>,
) {
if let Some(session) = self
.sessions
.iter()
.sessions_with_children
.keys()
.find(|session| session.read(cx).session_id(cx) == session_id)
{
self.activate_session(session.clone(), window, cx);
@@ -1009,7 +1027,7 @@ impl DebugPanel {
window: &mut Window,
cx: &mut Context<Self>,
) {
debug_assert!(self.sessions.contains(&session_item));
debug_assert!(self.sessions_with_children.contains_key(&session_item));
session_item.focus_handle(cx).focus(window);
session_item.update(cx, |this, cx| {
this.running_state().update(cx, |this, cx| {
@@ -1280,18 +1298,27 @@ impl DebugPanel {
parent_session: &Entity<Session>,
request: &StartDebuggingRequestArguments,
cx: &mut Context<'_, Self>,
) -> SharedString {
) -> Option<SharedString> {
let adapter = parent_session.read(cx).adapter();
if let Some(adapter) = DapRegistry::global(cx).adapter(&adapter) {
if let Some(label) = adapter.label_for_child_session(request) {
return label.into();
return Some(label.into());
}
}
let mut label = parent_session.read(cx).label().clone();
if !label.ends_with("(child)") {
label = format!("{label} (child)").into();
None
}
fn retain_sessions(&mut self, keep: impl Fn(&Entity<DebugSession>) -> bool) {
self.sessions_with_children
.retain(|session, _| keep(session));
for children in self.sessions_with_children.values_mut() {
children.retain(|child| {
let Some(child) = child.upgrade() else {
return false;
};
keep(&child)
});
}
label
}
}
@@ -1321,11 +1348,11 @@ async fn register_session_inner(
let serialized_layout = persistence::get_serialized_layout(adapter_name).await;
let debug_session = this.update_in(cx, |this, window, cx| {
let parent_session = this
.sessions
.iter()
.sessions_with_children
.keys()
.find(|p| Some(p.read(cx).session_id(cx)) == session.read(cx).parent_id(cx))
.cloned();
this.sessions.retain(|session| {
this.retain_sessions(|session| {
!session
.read(cx)
.running_state()
@@ -1356,13 +1383,23 @@ async fn register_session_inner(
)
.detach();
let insert_position = this
.sessions
.iter()
.sessions_with_children
.keys()
.position(|session| Some(session) == parent_session.as_ref())
.map(|position| position + 1)
.unwrap_or(this.sessions.len());
.unwrap_or(this.sessions_with_children.len());
// Maintain topological sort order of sessions
this.sessions.insert(insert_position, debug_session.clone());
let (_, old) = this.sessions_with_children.insert_before(
insert_position,
debug_session.clone(),
Default::default(),
);
debug_assert!(old.is_none());
if let Some(parent_session) = parent_session {
this.sessions_with_children
.entry(parent_session)
.and_modify(|children| children.push(debug_session.downgrade()));
}
debug_session
})?;
@@ -1402,7 +1439,7 @@ impl Panel for DebugPanel {
cx: &mut Context<Self>,
) {
if position.axis() != self.position(window, cx).axis() {
self.sessions.iter().for_each(|session_item| {
self.sessions_with_children.keys().for_each(|session_item| {
session_item.update(cx, |item, cx| {
item.running_state()
.update(cx, |state, _| state.invert_axies())
@@ -1723,6 +1760,7 @@ impl Render for DebugPanel {
category_filter: Some(
zed_actions::ExtensionCategoryFilter::DebugAdapters,
),
id: None,
}
.boxed_clone(),
cx,
@@ -1768,6 +1806,7 @@ impl Render for DebugPanel {
.child(breakpoint_list)
.child(Divider::vertical())
.child(welcome_experience)
.child(Divider::vertical())
} else {
this.items_end()
.child(welcome_experience)

View File

@@ -83,6 +83,8 @@ actions!(
Rerun,
/// Toggles expansion of the selected item in the debugger UI.
ToggleExpandItem,
/// Set a data breakpoint on the selected variable or memory region.
ToggleDataBreakpoint,
]
);

View File

@@ -1,16 +1,82 @@
use std::time::Duration;
use std::{rc::Rc, time::Duration};
use collections::HashMap;
use gpui::{Animation, AnimationExt as _, Entity, Transformation, percentage};
use gpui::{Animation, AnimationExt as _, Entity, Transformation, WeakEntity, percentage};
use project::debugger::session::{ThreadId, ThreadStatus};
use ui::{ContextMenu, DropdownMenu, DropdownStyle, Indicator, prelude::*};
use util::truncate_and_trailoff;
use util::{maybe, truncate_and_trailoff};
use crate::{
debugger_panel::DebugPanel,
session::{DebugSession, running::RunningState},
};
struct SessionListEntry {
ancestors: Vec<Entity<DebugSession>>,
leaf: Entity<DebugSession>,
}
impl SessionListEntry {
pub(crate) fn label_element(&self, depth: usize, cx: &mut App) -> AnyElement {
const MAX_LABEL_CHARS: usize = 150;
let mut label = String::new();
for ancestor in &self.ancestors {
label.push_str(&ancestor.update(cx, |ancestor, cx| {
ancestor.label(cx).unwrap_or("(child)".into())
}));
label.push_str(" » ");
}
label.push_str(
&self
.leaf
.update(cx, |leaf, cx| leaf.label(cx).unwrap_or("(child)".into())),
);
let label = truncate_and_trailoff(&label, MAX_LABEL_CHARS);
let is_terminated = self
.leaf
.read(cx)
.running_state
.read(cx)
.session()
.read(cx)
.is_terminated();
let icon = {
if is_terminated {
Some(Indicator::dot().color(Color::Error))
} else {
match self
.leaf
.read(cx)
.running_state
.read(cx)
.thread_status(cx)
.unwrap_or_default()
{
project::debugger::session::ThreadStatus::Stopped => {
Some(Indicator::dot().color(Color::Conflict))
}
_ => Some(Indicator::dot().color(Color::Success)),
}
}
};
h_flex()
.id("session-label")
.ml(depth * px(16.0))
.gap_2()
.when_some(icon, |this, indicator| this.child(indicator))
.justify_between()
.child(
Label::new(label)
.size(LabelSize::Small)
.when(is_terminated, |this| this.strikethrough()),
)
.into_any_element()
}
}
impl DebugPanel {
fn dropdown_label(label: impl Into<SharedString>) -> Label {
const MAX_LABEL_CHARS: usize = 50;
@@ -25,145 +91,205 @@ impl DebugPanel {
window: &mut Window,
cx: &mut Context<Self>,
) -> Option<impl IntoElement> {
if let Some(running_state) = running_state {
let sessions = self.sessions().clone();
let weak = cx.weak_entity();
let running_state = running_state.read(cx);
let label = if let Some(active_session) = active_session.clone() {
active_session.read(cx).session(cx).read(cx).label()
} else {
SharedString::new_static("Unknown Session")
};
let running_state = running_state?;
let is_terminated = running_state.session().read(cx).is_terminated();
let is_started = active_session
.is_some_and(|session| session.read(cx).session(cx).read(cx).is_started());
let mut session_entries = Vec::with_capacity(self.sessions_with_children.len() * 3);
let mut sessions_with_children = self.sessions_with_children.iter().peekable();
let session_state_indicator = if is_terminated {
Indicator::dot().color(Color::Error).into_any_element()
} else if !is_started {
Icon::new(IconName::ArrowCircle)
.size(IconSize::Small)
.color(Color::Muted)
.with_animation(
"arrow-circle",
Animation::new(Duration::from_secs(2)).repeat(),
|icon, delta| icon.transform(Transformation::rotate(percentage(delta))),
)
.into_any_element()
while let Some((root, children)) = sessions_with_children.next() {
let root_entry = if let Ok([single_child]) = <&[_; 1]>::try_from(children.as_slice())
&& let Some(single_child) = single_child.upgrade()
&& single_child.read(cx).quirks.compact
{
sessions_with_children.next();
SessionListEntry {
leaf: single_child.clone(),
ancestors: vec![root.clone()],
}
} else {
match running_state.thread_status(cx).unwrap_or_default() {
ThreadStatus::Stopped => {
Indicator::dot().color(Color::Conflict).into_any_element()
}
_ => Indicator::dot().color(Color::Success).into_any_element(),
SessionListEntry {
leaf: root.clone(),
ancestors: Vec::new(),
}
};
session_entries.push(root_entry);
let trigger = h_flex()
.gap_2()
.child(session_state_indicator)
.justify_between()
.child(
DebugPanel::dropdown_label(label)
.when(is_terminated, |this| this.strikethrough()),
)
.into_any_element();
Some(
DropdownMenu::new_with_element(
"debugger-session-list",
trigger,
ContextMenu::build(window, cx, move |mut this, _, cx| {
let context_menu = cx.weak_entity();
let mut session_depths = HashMap::default();
for session in sessions.into_iter() {
let weak_session = session.downgrade();
let weak_session_id = weak_session.entity_id();
let session_id = session.read(cx).session_id(cx);
let parent_depth = session
.read(cx)
.session(cx)
.read(cx)
.parent_id(cx)
.and_then(|parent_id| session_depths.get(&parent_id).cloned());
let self_depth =
*session_depths.entry(session_id).or_insert_with(|| {
parent_depth.map(|depth| depth + 1).unwrap_or(0usize)
});
this = this.custom_entry(
{
let weak = weak.clone();
let context_menu = context_menu.clone();
move |_, cx| {
weak_session
.read_with(cx, |session, cx| {
let context_menu = context_menu.clone();
let id: SharedString =
format!("debug-session-{}", session_id.0)
.into();
h_flex()
.w_full()
.group(id.clone())
.justify_between()
.child(session.label_element(self_depth, cx))
.child(
IconButton::new(
"close-debug-session",
IconName::Close,
)
.visible_on_hover(id.clone())
.icon_size(IconSize::Small)
.on_click({
let weak = weak.clone();
move |_, window, cx| {
weak.update(cx, |panel, cx| {
panel.close_session(
weak_session_id,
window,
cx,
);
})
.ok();
context_menu
.update(cx, |this, cx| {
this.cancel(
&Default::default(),
window,
cx,
);
})
.ok();
}
}),
)
.into_any_element()
})
.unwrap_or_else(|_| div().into_any_element())
}
},
{
let weak = weak.clone();
move |window, cx| {
weak.update(cx, |panel, cx| {
panel.activate_session(session.clone(), window, cx);
})
.ok();
}
},
);
}
this
session_entries.extend(
sessions_with_children
.by_ref()
.take_while(|(session, _)| {
session
.read(cx)
.session(cx)
.read(cx)
.parent_id(cx)
.is_some()
})
.map(|(session, _)| SessionListEntry {
leaf: session.clone(),
ancestors: vec![],
}),
)
.style(DropdownStyle::Ghost)
.handle(self.session_picker_menu_handle.clone()),
)
} else {
None
);
}
let weak = cx.weak_entity();
let trigger_label = if let Some(active_session) = active_session.clone() {
active_session.update(cx, |active_session, cx| {
active_session.label(cx).unwrap_or("(child)".into())
})
} else {
SharedString::new_static("Unknown Session")
};
let running_state = running_state.read(cx);
let is_terminated = running_state.session().read(cx).is_terminated();
let is_started = active_session
.is_some_and(|session| session.read(cx).session(cx).read(cx).is_started());
let session_state_indicator = if is_terminated {
Indicator::dot().color(Color::Error).into_any_element()
} else if !is_started {
Icon::new(IconName::ArrowCircle)
.size(IconSize::Small)
.color(Color::Muted)
.with_animation(
"arrow-circle",
Animation::new(Duration::from_secs(2)).repeat(),
|icon, delta| icon.transform(Transformation::rotate(percentage(delta))),
)
.into_any_element()
} else {
match running_state.thread_status(cx).unwrap_or_default() {
ThreadStatus::Stopped => Indicator::dot().color(Color::Conflict).into_any_element(),
_ => Indicator::dot().color(Color::Success).into_any_element(),
}
};
let trigger = h_flex()
.gap_2()
.child(session_state_indicator)
.justify_between()
.child(
DebugPanel::dropdown_label(trigger_label)
.when(is_terminated, |this| this.strikethrough()),
)
.into_any_element();
let menu = DropdownMenu::new_with_element(
"debugger-session-list",
trigger,
ContextMenu::build(window, cx, move |mut this, _, cx| {
let context_menu = cx.weak_entity();
let mut session_depths = HashMap::default();
for session_entry in session_entries {
let session_id = session_entry.leaf.read(cx).session_id(cx);
let parent_depth = session_entry
.ancestors
.first()
.unwrap_or(&session_entry.leaf)
.read(cx)
.session(cx)
.read(cx)
.parent_id(cx)
.and_then(|parent_id| session_depths.get(&parent_id).cloned());
let self_depth = *session_depths
.entry(session_id)
.or_insert_with(|| parent_depth.map(|depth| depth + 1).unwrap_or(0usize));
this = this.custom_entry(
{
let weak = weak.clone();
let context_menu = context_menu.clone();
let ancestors: Rc<[_]> = session_entry
.ancestors
.iter()
.map(|session| session.downgrade())
.collect();
let leaf = session_entry.leaf.downgrade();
move |window, cx| {
Self::render_session_menu_entry(
weak.clone(),
context_menu.clone(),
ancestors.clone(),
leaf.clone(),
self_depth,
window,
cx,
)
}
},
{
let weak = weak.clone();
let leaf = session_entry.leaf.clone();
move |window, cx| {
weak.update(cx, |panel, cx| {
panel.activate_session(leaf.clone(), window, cx);
})
.ok();
}
},
);
}
this
}),
)
.style(DropdownStyle::Ghost)
.handle(self.session_picker_menu_handle.clone());
Some(menu)
}
fn render_session_menu_entry(
weak: WeakEntity<DebugPanel>,
context_menu: WeakEntity<ContextMenu>,
ancestors: Rc<[WeakEntity<DebugSession>]>,
leaf: WeakEntity<DebugSession>,
self_depth: usize,
_window: &mut Window,
cx: &mut App,
) -> AnyElement {
let Some(session_entry) = maybe!({
let ancestors = ancestors
.iter()
.map(|ancestor| ancestor.upgrade())
.collect::<Option<Vec<_>>>()?;
let leaf = leaf.upgrade()?;
Some(SessionListEntry { ancestors, leaf })
}) else {
return div().into_any_element();
};
let id: SharedString = format!(
"debug-session-{}",
session_entry.leaf.read(cx).session_id(cx).0
)
.into();
let session_entity_id = session_entry.leaf.entity_id();
h_flex()
.w_full()
.group(id.clone())
.justify_between()
.child(session_entry.label_element(self_depth, cx))
.child(
IconButton::new("close-debug-session", IconName::Close)
.visible_on_hover(id.clone())
.icon_size(IconSize::Small)
.on_click({
let weak = weak.clone();
move |_, window, cx| {
weak.update(cx, |panel, cx| {
panel.close_session(session_entity_id, window, cx);
})
.ok();
context_menu
.update(cx, |this, cx| {
this.cancel(&Default::default(), window, cx);
})
.ok();
}
}),
)
.into_any_element()
}
pub(crate) fn render_thread_dropdown(

View File

@@ -343,6 +343,12 @@ impl NewProcessModal {
return;
}
if let NewProcessMode::Launch = &self.mode {
if self.configure_mode.read(cx).save_to_debug_json.selected() {
self.save_debug_scenario(window, cx);
}
}
let Some(debugger) = self.debugger.clone() else {
return;
};
@@ -760,14 +766,7 @@ impl Render for NewProcessModal {
))
.child(
h_flex()
.child(div().child(self.adapter_drop_down_menu(window, cx)))
.child(
Button::new("debugger-spawn", "Start")
.on_click(cx.listener(|this, _, window, cx| {
this.start_new_session(window, cx)
}))
.disabled(disabled),
),
.child(div().child(self.adapter_drop_down_menu(window, cx))),
)
}),
NewProcessMode::Debug => el,
@@ -800,6 +799,7 @@ pub(super) struct ConfigureMode {
program: Entity<Editor>,
cwd: Entity<Editor>,
stop_on_entry: ToggleState,
save_to_debug_json: ToggleState,
}
impl ConfigureMode {
@@ -818,6 +818,7 @@ impl ConfigureMode {
program,
cwd,
stop_on_entry: ToggleState::Unselected,
save_to_debug_json: ToggleState::Unselected,
})
}

View File

@@ -11,7 +11,7 @@ use workspace::{Member, Pane, PaneAxis, Workspace};
use crate::session::running::{
self, DebugTerminal, RunningState, SubView, breakpoint_list::BreakpointList, console::Console,
loaded_source_list::LoadedSourceList, module_list::ModuleList,
loaded_source_list::LoadedSourceList, memory_view::MemoryView, module_list::ModuleList,
stack_frame_list::StackFrameList, variable_list::VariableList,
};
@@ -24,6 +24,7 @@ pub(crate) enum DebuggerPaneItem {
Modules,
LoadedSources,
Terminal,
MemoryView,
}
impl DebuggerPaneItem {
@@ -36,6 +37,7 @@ impl DebuggerPaneItem {
DebuggerPaneItem::Modules,
DebuggerPaneItem::LoadedSources,
DebuggerPaneItem::Terminal,
DebuggerPaneItem::MemoryView,
];
VARIANTS
}
@@ -43,6 +45,9 @@ impl DebuggerPaneItem {
pub(crate) fn is_supported(&self, capabilities: &Capabilities) -> bool {
match self {
DebuggerPaneItem::Modules => capabilities.supports_modules_request.unwrap_or_default(),
DebuggerPaneItem::MemoryView => capabilities
.supports_read_memory_request
.unwrap_or_default(),
DebuggerPaneItem::LoadedSources => capabilities
.supports_loaded_sources_request
.unwrap_or_default(),
@@ -59,6 +64,7 @@ impl DebuggerPaneItem {
DebuggerPaneItem::Modules => SharedString::new_static("Modules"),
DebuggerPaneItem::LoadedSources => SharedString::new_static("Sources"),
DebuggerPaneItem::Terminal => SharedString::new_static("Terminal"),
DebuggerPaneItem::MemoryView => SharedString::new_static("Memory View"),
}
}
pub(crate) fn tab_tooltip(self) -> SharedString {
@@ -80,6 +86,7 @@ impl DebuggerPaneItem {
DebuggerPaneItem::Terminal => {
"Provides an interactive terminal session within the debugging environment."
}
DebuggerPaneItem::MemoryView => "Allows inspection of memory contents.",
};
SharedString::new_static(tooltip)
}
@@ -204,6 +211,7 @@ pub(crate) fn deserialize_pane_layout(
breakpoint_list: &Entity<BreakpointList>,
loaded_sources: &Entity<LoadedSourceList>,
terminal: &Entity<DebugTerminal>,
memory_view: &Entity<MemoryView>,
subscriptions: &mut HashMap<EntityId, Subscription>,
window: &mut Window,
cx: &mut Context<RunningState>,
@@ -228,6 +236,7 @@ pub(crate) fn deserialize_pane_layout(
breakpoint_list,
loaded_sources,
terminal,
memory_view,
subscriptions,
window,
cx,
@@ -298,6 +307,12 @@ pub(crate) fn deserialize_pane_layout(
DebuggerPaneItem::Terminal,
cx,
)),
DebuggerPaneItem::MemoryView => Box::new(SubView::new(
memory_view.focus_handle(cx),
memory_view.clone().into(),
DebuggerPaneItem::MemoryView,
cx,
)),
})
.collect();

View File

@@ -5,14 +5,13 @@ use dap::client::SessionId;
use gpui::{
App, Axis, Entity, EventEmitter, FocusHandle, Focusable, Subscription, Task, WeakEntity,
};
use project::Project;
use project::debugger::session::Session;
use project::worktree_store::WorktreeStore;
use project::{Project, debugger::session::SessionQuirks};
use rpc::proto;
use running::RunningState;
use std::{cell::OnceCell, sync::OnceLock};
use ui::{Indicator, Tooltip, prelude::*};
use util::truncate_and_trailoff;
use std::cell::OnceCell;
use ui::prelude::*;
use workspace::{
CollaboratorId, FollowableItem, ViewId, Workspace,
item::{self, Item},
@@ -20,8 +19,8 @@ use workspace::{
pub struct DebugSession {
remote_id: Option<workspace::ViewId>,
running_state: Entity<RunningState>,
label: OnceLock<SharedString>,
pub(crate) running_state: Entity<RunningState>,
pub(crate) quirks: SessionQuirks,
stack_trace_view: OnceCell<Entity<StackTraceView>>,
_worktree_store: WeakEntity<WorktreeStore>,
workspace: WeakEntity<Workspace>,
@@ -57,6 +56,7 @@ impl DebugSession {
cx,
)
});
let quirks = session.read(cx).quirks();
cx.new(|cx| Self {
_subscriptions: [cx.subscribe(&running_state, |_, _, _, cx| {
@@ -64,7 +64,7 @@ impl DebugSession {
})],
remote_id: None,
running_state,
label: OnceLock::new(),
quirks,
stack_trace_view: OnceCell::new(),
_worktree_store: project.read(cx).worktree_store().downgrade(),
workspace,
@@ -110,65 +110,28 @@ impl DebugSession {
.update(cx, |state, cx| state.shutdown(cx));
}
pub(crate) fn label(&self, cx: &App) -> SharedString {
if let Some(label) = self.label.get() {
return label.clone();
}
let session = self.running_state.read(cx).session();
self.label
.get_or_init(|| session.read(cx).label())
.to_owned()
}
pub(crate) fn running_state(&self) -> &Entity<RunningState> {
&self.running_state
}
pub(crate) fn label_element(&self, depth: usize, cx: &App) -> AnyElement {
const MAX_LABEL_CHARS: usize = 150;
let label = self.label(cx);
let label = truncate_and_trailoff(&label, MAX_LABEL_CHARS);
let is_terminated = self
.running_state
.read(cx)
.session()
.read(cx)
.is_terminated();
let icon = {
if is_terminated {
Some(Indicator::dot().color(Color::Error))
} else {
match self
.running_state
.read(cx)
.thread_status(cx)
.unwrap_or_default()
{
project::debugger::session::ThreadStatus::Stopped => {
Some(Indicator::dot().color(Color::Conflict))
}
_ => Some(Indicator::dot().color(Color::Success)),
pub(crate) fn label(&self, cx: &mut App) -> Option<SharedString> {
let session = self.running_state.read(cx).session().clone();
session.update(cx, |session, cx| {
let session_label = session.label();
let quirks = session.quirks();
let mut single_thread_name = || {
let threads = session.threads(cx);
match threads.as_slice() {
[(thread, _)] => Some(SharedString::from(&thread.name)),
_ => None,
}
};
if quirks.prefer_thread_name {
single_thread_name().or(session_label)
} else {
session_label.or_else(single_thread_name)
}
};
})
}
h_flex()
.id("session-label")
.tooltip(Tooltip::text(format!("Session {}", self.session_id(cx).0,)))
.ml(depth * px(16.0))
.gap_2()
.when_some(icon, |this, indicator| this.child(indicator))
.justify_between()
.child(
Label::new(label)
.size(LabelSize::Small)
.when(is_terminated, |this| this.strikethrough()),
)
.into_any_element()
pub fn running_state(&self) -> &Entity<RunningState> {
&self.running_state
}
}

View File

@@ -1,16 +1,17 @@
pub(crate) mod breakpoint_list;
pub(crate) mod console;
pub(crate) mod loaded_source_list;
pub(crate) mod memory_view;
pub(crate) mod module_list;
pub mod stack_frame_list;
pub mod variable_list;
use std::{any::Any, ops::ControlFlow, path::PathBuf, sync::Arc, time::Duration};
use crate::{
ToggleExpandItem,
new_process_modal::resolve_path,
persistence::{self, DebuggerPaneItem, SerializedLayout},
session::running::memory_view::MemoryView,
};
use super::DebugPanelItemEvent;
@@ -34,7 +35,7 @@ use loaded_source_list::LoadedSourceList;
use module_list::ModuleList;
use project::{
DebugScenarioContext, Project, WorktreeId,
debugger::session::{Session, SessionEvent, ThreadId, ThreadStatus},
debugger::session::{self, Session, SessionEvent, SessionStateEvent, ThreadId, ThreadStatus},
terminals::TerminalKind,
};
use rpc::proto::ViewId;
@@ -81,6 +82,7 @@ pub struct RunningState {
_schedule_serialize: Option<Task<()>>,
pub(crate) scenario: Option<DebugScenario>,
pub(crate) scenario_context: Option<DebugScenarioContext>,
memory_view: Entity<MemoryView>,
}
impl RunningState {
@@ -676,14 +678,36 @@ impl RunningState {
let session_id = session.read(cx).session_id();
let weak_state = cx.weak_entity();
let stack_frame_list = cx.new(|cx| {
StackFrameList::new(workspace.clone(), session.clone(), weak_state, window, cx)
StackFrameList::new(
workspace.clone(),
session.clone(),
weak_state.clone(),
window,
cx,
)
});
let debug_terminal =
parent_terminal.unwrap_or_else(|| cx.new(|cx| DebugTerminal::empty(window, cx)));
let variable_list =
cx.new(|cx| VariableList::new(session.clone(), stack_frame_list.clone(), window, cx));
let memory_view = cx.new(|cx| {
MemoryView::new(
session.clone(),
workspace.clone(),
stack_frame_list.downgrade(),
window,
cx,
)
});
let variable_list = cx.new(|cx| {
VariableList::new(
session.clone(),
stack_frame_list.clone(),
memory_view.clone(),
weak_state.clone(),
window,
cx,
)
});
let module_list = cx.new(|cx| ModuleList::new(session.clone(), workspace.clone(), cx));
@@ -770,6 +794,15 @@ impl RunningState {
cx.on_focus_out(&focus_handle, window, |this, _, window, cx| {
this.serialize_layout(window, cx);
}),
cx.subscribe(
&session,
|this, session, event: &SessionStateEvent, cx| match event {
SessionStateEvent::Shutdown if session.read(cx).is_building() => {
this.shutdown(cx);
}
_ => {}
},
),
];
let mut pane_close_subscriptions = HashMap::default();
@@ -786,6 +819,7 @@ impl RunningState {
&breakpoint_list,
&loaded_source_list,
&debug_terminal,
&memory_view,
&mut pane_close_subscriptions,
window,
cx,
@@ -814,6 +848,7 @@ impl RunningState {
let active_pane = panes.first_pane();
Self {
memory_view,
session,
workspace,
focus_handle,
@@ -884,6 +919,7 @@ impl RunningState {
let weak_project = project.downgrade();
let weak_workspace = workspace.downgrade();
let is_local = project.read(cx).is_local();
cx.spawn_in(window, async move |this, cx| {
let DebugScenario {
adapter,
@@ -1224,6 +1260,12 @@ impl RunningState {
item_kind,
cx,
)),
DebuggerPaneItem::MemoryView => Box::new(SubView::new(
self.memory_view.focus_handle(cx),
self.memory_view.clone().into(),
item_kind,
cx,
)),
}
}
@@ -1408,7 +1450,14 @@ impl RunningState {
&self.module_list
}
pub(crate) fn activate_item(&self, item: DebuggerPaneItem, window: &mut Window, cx: &mut App) {
pub(crate) fn activate_item(
&mut self,
item: DebuggerPaneItem,
window: &mut Window,
cx: &mut Context<Self>,
) {
self.ensure_pane_item(item, window, cx);
let (variable_list_position, pane) = self
.panes
.panes()
@@ -1420,9 +1469,10 @@ impl RunningState {
.map(|view| (view, pane))
})
.unwrap();
pane.update(cx, |this, cx| {
this.activate_item(variable_list_position, true, true, window, cx);
})
});
}
#[cfg(test)]
@@ -1459,7 +1509,7 @@ impl RunningState {
}
}
pub(crate) fn selected_thread_id(&self) -> Option<ThreadId> {
pub fn selected_thread_id(&self) -> Option<ThreadId> {
self.thread_id
}
@@ -1599,9 +1649,21 @@ impl RunningState {
})
.log_err();
self.session.update(cx, |session, cx| {
let is_building = self.session.update(cx, |session, cx| {
session.shutdown(cx).detach();
})
matches!(session.mode, session::SessionState::Building(_))
});
if is_building {
self.debug_terminal.update(cx, |terminal, cx| {
if let Some(view) = terminal.terminal.as_ref() {
view.update(cx, |view, cx| {
view.terminal()
.update(cx, |terminal, _| terminal.kill_active_task())
})
}
})
}
}
pub fn stop_thread(&self, cx: &mut Context<Self>) {

View File

@@ -24,10 +24,10 @@ use project::{
};
use ui::{
ActiveTheme, AnyElement, App, ButtonCommon, Clickable, Color, Context, Disableable, Div,
Divider, FluentBuilder as _, Icon, IconButton, IconName, IconSize, Indicator,
InteractiveElement, IntoElement, Label, LabelCommon, LabelSize, ListItem, ParentElement,
Render, RenderOnce, Scrollbar, ScrollbarState, SharedString, StatefulInteractiveElement,
Styled, Toggleable, Tooltip, Window, div, h_flex, px, v_flex,
Divider, FluentBuilder as _, Icon, IconButton, IconName, IconSize, InteractiveElement,
IntoElement, Label, LabelCommon, LabelSize, ListItem, ParentElement, Render, RenderOnce,
Scrollbar, ScrollbarState, SharedString, StatefulInteractiveElement, Styled, Toggleable,
Tooltip, Window, div, h_flex, px, v_flex,
};
use util::ResultExt;
use workspace::Workspace;
@@ -46,6 +46,7 @@ actions!(
pub(crate) enum SelectedBreakpointKind {
Source,
Exception,
Data,
}
pub(crate) struct BreakpointList {
workspace: WeakEntity<Workspace>,
@@ -188,6 +189,9 @@ impl BreakpointList {
BreakpointEntryKind::ExceptionBreakpoint(bp) => {
(SelectedBreakpointKind::Exception, bp.is_enabled)
}
BreakpointEntryKind::DataBreakpoint(bp) => {
(SelectedBreakpointKind::Data, bp.0.is_enabled)
}
})
})
}
@@ -391,7 +395,8 @@ impl BreakpointList {
let row = line_breakpoint.breakpoint.row;
self.go_to_line_breakpoint(path, row, window, cx);
}
BreakpointEntryKind::ExceptionBreakpoint(_) => {}
BreakpointEntryKind::DataBreakpoint(_)
| BreakpointEntryKind::ExceptionBreakpoint(_) => {}
}
}
@@ -421,6 +426,10 @@ impl BreakpointList {
let id = exception_breakpoint.id.clone();
self.toggle_exception_breakpoint(&id, cx);
}
BreakpointEntryKind::DataBreakpoint(data_breakpoint) => {
let id = data_breakpoint.0.dap.data_id.clone();
self.toggle_data_breakpoint(&id, cx);
}
}
cx.notify();
}
@@ -441,7 +450,7 @@ impl BreakpointList {
let row = line_breakpoint.breakpoint.row;
self.edit_line_breakpoint(path, row, BreakpointEditAction::Toggle, cx);
}
BreakpointEntryKind::ExceptionBreakpoint(_) => {}
_ => {}
}
cx.notify();
}
@@ -490,6 +499,14 @@ impl BreakpointList {
cx.notify();
}
fn toggle_data_breakpoint(&mut self, id: &str, cx: &mut Context<Self>) {
if let Some(session) = &self.session {
session.update(cx, |this, cx| {
this.toggle_data_breakpoint(&id, cx);
});
}
}
fn toggle_exception_breakpoint(&mut self, id: &str, cx: &mut Context<Self>) {
if let Some(session) = &self.session {
session.update(cx, |this, cx| {
@@ -642,6 +659,7 @@ impl BreakpointList {
SelectedBreakpointKind::Exception => {
"Exception Breakpoints cannot be removed from the breakpoint list"
}
SelectedBreakpointKind::Data => "Remove data breakpoint from a breakpoint list",
});
let toggle_label = selection_kind.map(|(_, is_enabled)| {
if is_enabled {
@@ -783,8 +801,20 @@ impl Render for BreakpointList {
weak: weak.clone(),
})
});
self.breakpoints
.extend(breakpoints.chain(exception_breakpoints));
let data_breakpoints = self.session.as_ref().into_iter().flat_map(|session| {
session
.read(cx)
.data_breakpoints()
.map(|state| BreakpointEntry {
kind: BreakpointEntryKind::DataBreakpoint(DataBreakpoint(state.clone())),
weak: weak.clone(),
})
});
self.breakpoints.extend(
breakpoints
.chain(data_breakpoints)
.chain(exception_breakpoints),
);
v_flex()
.id("breakpoint-list")
.key_context("BreakpointList")
@@ -905,7 +935,11 @@ impl LineBreakpoint {
.ok();
}
})
.child(Indicator::icon(Icon::new(icon_name)).color(Color::Debugger))
.child(
Icon::new(icon_name)
.color(Color::Debugger)
.size(IconSize::XSmall),
)
.on_mouse_down(MouseButton::Left, move |_, _, _| {});
ListItem::new(SharedString::from(format!(
@@ -996,6 +1030,103 @@ struct ExceptionBreakpoint {
data: ExceptionBreakpointsFilter,
is_enabled: bool,
}
#[derive(Clone, Debug)]
struct DataBreakpoint(project::debugger::session::DataBreakpointState);
impl DataBreakpoint {
fn render(
&self,
props: SupportedBreakpointProperties,
strip_mode: Option<ActiveBreakpointStripMode>,
ix: usize,
is_selected: bool,
focus_handle: FocusHandle,
list: WeakEntity<BreakpointList>,
) -> ListItem {
let color = if self.0.is_enabled {
Color::Debugger
} else {
Color::Muted
};
let is_enabled = self.0.is_enabled;
let id = self.0.dap.data_id.clone();
ListItem::new(SharedString::from(format!(
"data-breakpoint-ui-item-{}",
self.0.dap.data_id
)))
.rounded()
.start_slot(
div()
.id(SharedString::from(format!(
"data-breakpoint-ui-item-{}-click-handler",
self.0.dap.data_id
)))
.tooltip({
let focus_handle = focus_handle.clone();
move |window, cx| {
Tooltip::for_action_in(
if is_enabled {
"Disable Data Breakpoint"
} else {
"Enable Data Breakpoint"
},
&ToggleEnableBreakpoint,
&focus_handle,
window,
cx,
)
}
})
.on_click({
let list = list.clone();
move |_, _, cx| {
list.update(cx, |this, cx| {
this.toggle_data_breakpoint(&id, cx);
})
.ok();
}
})
.cursor_pointer()
.child(
Icon::new(IconName::Binary)
.color(color)
.size(IconSize::Small),
),
)
.child(
h_flex()
.w_full()
.mr_4()
.py_0p5()
.justify_between()
.child(
v_flex()
.py_1()
.gap_1()
.min_h(px(26.))
.justify_center()
.id(("data-breakpoint-label", ix))
.child(
Label::new(self.0.context.human_readable_label())
.size(LabelSize::Small)
.line_height_style(ui::LineHeightStyle::UiLabel),
),
)
.child(BreakpointOptionsStrip {
props,
breakpoint: BreakpointEntry {
kind: BreakpointEntryKind::DataBreakpoint(self.clone()),
weak: list,
},
is_selected,
focus_handle,
strip_mode,
index: ix,
}),
)
.toggle_state(is_selected)
}
}
impl ExceptionBreakpoint {
fn render(
@@ -1062,7 +1193,11 @@ impl ExceptionBreakpoint {
}
})
.cursor_pointer()
.child(Indicator::icon(Icon::new(IconName::Flame)).color(color)),
.child(
Icon::new(IconName::Flame)
.color(color)
.size(IconSize::Small),
),
)
.child(
h_flex()
@@ -1105,6 +1240,7 @@ impl ExceptionBreakpoint {
enum BreakpointEntryKind {
LineBreakpoint(LineBreakpoint),
ExceptionBreakpoint(ExceptionBreakpoint),
DataBreakpoint(DataBreakpoint),
}
#[derive(Clone, Debug)]
@@ -1140,6 +1276,14 @@ impl BreakpointEntry {
focus_handle,
self.weak.clone(),
),
BreakpointEntryKind::DataBreakpoint(data_breakpoint) => data_breakpoint.render(
props.for_data_breakpoints(),
strip_mode,
ix,
is_selected,
focus_handle,
self.weak.clone(),
),
}
}
@@ -1155,6 +1299,11 @@ impl BreakpointEntry {
exception_breakpoint.id
)
.into(),
BreakpointEntryKind::DataBreakpoint(data_breakpoint) => format!(
"data-breakpoint-control-strip--{}",
data_breakpoint.0.dap.data_id
)
.into(),
}
}
@@ -1172,8 +1321,8 @@ impl BreakpointEntry {
BreakpointEntryKind::LineBreakpoint(line_breakpoint) => {
line_breakpoint.breakpoint.condition.is_some()
}
// We don't support conditions on exception breakpoints
BreakpointEntryKind::ExceptionBreakpoint(_) => false,
// We don't support conditions on exception/data breakpoints
_ => false,
}
}
@@ -1225,6 +1374,10 @@ impl SupportedBreakpointProperties {
// TODO: we don't yet support conditions for exception breakpoints at the data layer, hence all props are disabled here.
Self::empty()
}
fn for_data_breakpoints(self) -> Self {
// TODO: we don't yet support conditions for data breakpoints at the data layer, hence all props are disabled here.
Self::empty()
}
}
#[derive(IntoElement)]
struct BreakpointOptionsStrip {

View File

@@ -12,7 +12,7 @@ use gpui::{
Action as _, AppContext, Context, Corner, Entity, FocusHandle, Focusable, HighlightStyle, Hsla,
Render, Subscription, Task, TextStyle, WeakEntity, actions,
};
use language::{Buffer, CodeLabel, ToOffset};
use language::{Anchor, Buffer, CodeLabel, TextBufferSnapshot, ToOffset};
use menu::{Confirm, SelectNext, SelectPrevious};
use project::{
Completion, CompletionResponse,
@@ -637,27 +637,13 @@ impl ConsoleQueryBarCompletionProvider {
});
let snapshot = buffer.read(cx).text_snapshot();
let query = snapshot.text();
let replace_range = {
let buffer_offset = buffer_position.to_offset(&snapshot);
let reversed_chars = snapshot.reversed_chars_for_range(0..buffer_offset);
let mut word_len = 0;
for ch in reversed_chars {
if ch.is_alphanumeric() || ch == '_' {
word_len += 1;
} else {
break;
}
}
let word_start_offset = buffer_offset - word_len;
let start_anchor = snapshot.anchor_at(word_start_offset, Bias::Left);
start_anchor..buffer_position
};
let buffer_text = snapshot.text();
cx.spawn(async move |_, cx| {
const LIMIT: usize = 10;
let matches = fuzzy::match_strings(
&string_matches,
&query,
&buffer_text,
true,
true,
LIMIT,
@@ -672,7 +658,12 @@ impl ConsoleQueryBarCompletionProvider {
let variable_value = variables.get(&string_match.string)?;
Some(project::Completion {
replace_range: replace_range.clone(),
replace_range: Self::replace_range_for_completion(
&buffer_text,
buffer_position,
string_match.string.as_bytes(),
&snapshot,
),
new_text: string_match.string.clone(),
label: CodeLabel {
filter_range: 0..string_match.string.len(),
@@ -697,6 +688,28 @@ impl ConsoleQueryBarCompletionProvider {
})
}
fn replace_range_for_completion(
buffer_text: &String,
buffer_position: Anchor,
new_bytes: &[u8],
snapshot: &TextBufferSnapshot,
) -> Range<Anchor> {
let buffer_offset = buffer_position.to_offset(&snapshot);
let buffer_bytes = &buffer_text.as_bytes()[0..buffer_offset];
let mut prefix_len = 0;
for i in (0..new_bytes.len()).rev() {
if buffer_bytes.ends_with(&new_bytes[0..i]) {
prefix_len = i;
break;
}
}
let start = snapshot.clip_offset(buffer_offset - prefix_len, Bias::Left);
snapshot.anchor_before(start)..buffer_position
}
const fn completion_type_score(completion_type: CompletionItemType) -> usize {
match completion_type {
CompletionItemType::Field | CompletionItemType::Property => 0,
@@ -744,6 +757,8 @@ impl ConsoleQueryBarCompletionProvider {
cx.background_executor().spawn(async move {
let completions = completion_task.await?;
let buffer_text = snapshot.text();
let completions = completions
.into_iter()
.map(|completion| {
@@ -753,26 +768,14 @@ impl ConsoleQueryBarCompletionProvider {
.as_ref()
.unwrap_or(&completion.label)
.to_owned();
let buffer_text = snapshot.text();
let buffer_bytes = buffer_text.as_bytes();
let new_bytes = new_text.as_bytes();
let mut prefix_len = 0;
for i in (0..new_bytes.len()).rev() {
if buffer_bytes.ends_with(&new_bytes[0..i]) {
prefix_len = i;
break;
}
}
let buffer_offset = buffer_position.to_offset(&snapshot);
let start = buffer_offset - prefix_len;
let start = snapshot.clip_offset(start, Bias::Left);
let start = snapshot.anchor_before(start);
let replace_range = start..buffer_position;
project::Completion {
replace_range,
replace_range: Self::replace_range_for_completion(
&buffer_text,
buffer_position,
new_text.as_bytes(),
&snapshot,
),
new_text,
label: CodeLabel {
filter_range: 0..completion.label.len(),
@@ -944,3 +947,64 @@ fn color_fetcher(color: ansi::Color) -> fn(&Theme) -> Hsla {
};
color_fetcher
}
#[cfg(test)]
mod tests {
use super::*;
use crate::tests::init_test;
use editor::test::editor_test_context::EditorTestContext;
use gpui::TestAppContext;
use language::Point;
#[track_caller]
fn assert_completion_range(
input: &str,
expect: &str,
replacement: &str,
cx: &mut EditorTestContext,
) {
cx.set_state(input);
let buffer_position =
cx.editor(|editor, _, cx| editor.selections.newest::<Point>(cx).start);
let snapshot = &cx.buffer_snapshot();
let replace_range = ConsoleQueryBarCompletionProvider::replace_range_for_completion(
&cx.buffer_text(),
snapshot.anchor_before(buffer_position),
replacement.as_bytes(),
&snapshot,
);
cx.update_editor(|editor, _, cx| {
editor.edit(
vec![(
snapshot.offset_for_anchor(&replace_range.start)
..snapshot.offset_for_anchor(&replace_range.end),
replacement,
)],
cx,
);
});
pretty_assertions::assert_eq!(expect, cx.display_text());
}
#[gpui::test]
async fn test_determine_completion_replace_range(cx: &mut TestAppContext) {
init_test(cx);
let mut cx = EditorTestContext::new(cx).await;
assert_completion_range("resˇ", "result", "result", &mut cx);
assert_completion_range("print(resˇ)", "print(result)", "result", &mut cx);
assert_completion_range("$author->nˇ", "$author->name", "$author->name", &mut cx);
assert_completion_range(
"$author->books[ˇ",
"$author->books[0]",
"$author->books[0]",
&mut cx,
);
}
}

View File

@@ -0,0 +1,984 @@
use std::{
cell::LazyCell,
fmt::Write,
ops::RangeInclusive,
sync::{Arc, LazyLock},
time::Duration,
};
use editor::{Editor, EditorElement, EditorStyle};
use gpui::{
Action, AppContext, DismissEvent, DragMoveEvent, Empty, Entity, FocusHandle, Focusable,
MouseButton, Point, ScrollStrategy, ScrollWheelEvent, Stateful, Subscription, Task, TextStyle,
UniformList, UniformListScrollHandle, WeakEntity, actions, anchored, deferred, point,
uniform_list,
};
use notifications::status_toast::{StatusToast, ToastIcon};
use project::debugger::{MemoryCell, dap_command::DataBreakpointContext, session::Session};
use settings::Settings;
use theme::ThemeSettings;
use ui::{
ActiveTheme, AnyElement, App, Color, Context, ContextMenu, Div, Divider, DropdownMenu, Element,
FluentBuilder, Icon, IconName, InteractiveElement, IntoElement, Label, LabelCommon,
ParentElement, Pixels, PopoverMenuHandle, Render, Scrollbar, ScrollbarState, SharedString,
StatefulInteractiveElement, Styled, TextSize, Tooltip, Window, div, h_flex, px, v_flex,
};
use util::ResultExt;
use workspace::Workspace;
use crate::{ToggleDataBreakpoint, session::running::stack_frame_list::StackFrameList};
actions!(debugger, [GoToSelectedAddress]);
pub(crate) struct MemoryView {
workspace: WeakEntity<Workspace>,
scroll_handle: UniformListScrollHandle,
scroll_state: ScrollbarState,
show_scrollbar: bool,
stack_frame_list: WeakEntity<StackFrameList>,
hide_scrollbar_task: Option<Task<()>>,
focus_handle: FocusHandle,
view_state: ViewState,
query_editor: Entity<Editor>,
session: Entity<Session>,
width_picker_handle: PopoverMenuHandle<ContextMenu>,
is_writing_memory: bool,
open_context_menu: Option<(Entity<ContextMenu>, Point<Pixels>, Subscription)>,
}
impl Focusable for MemoryView {
fn focus_handle(&self, _: &ui::App) -> FocusHandle {
self.focus_handle.clone()
}
}
#[derive(Clone, Debug)]
struct Drag {
start_address: u64,
end_address: u64,
}
impl Drag {
fn contains(&self, address: u64) -> bool {
let range = self.memory_range();
range.contains(&address)
}
fn memory_range(&self) -> RangeInclusive<u64> {
if self.start_address < self.end_address {
self.start_address..=self.end_address
} else {
self.end_address..=self.start_address
}
}
}
#[derive(Clone, Debug)]
enum SelectedMemoryRange {
DragUnderway(Drag),
DragComplete(Drag),
}
impl SelectedMemoryRange {
fn contains(&self, address: u64) -> bool {
match self {
SelectedMemoryRange::DragUnderway(drag) => drag.contains(address),
SelectedMemoryRange::DragComplete(drag) => drag.contains(address),
}
}
fn is_dragging(&self) -> bool {
matches!(self, SelectedMemoryRange::DragUnderway(_))
}
fn drag(&self) -> &Drag {
match self {
SelectedMemoryRange::DragUnderway(drag) => drag,
SelectedMemoryRange::DragComplete(drag) => drag,
}
}
}
#[derive(Clone)]
struct ViewState {
/// Uppermost row index
base_row: u64,
/// How many cells per row do we have?
line_width: ViewWidth,
selection: Option<SelectedMemoryRange>,
}
impl ViewState {
fn new(base_row: u64, line_width: ViewWidth) -> Self {
Self {
base_row,
line_width,
selection: None,
}
}
fn row_count(&self) -> u64 {
// This was picked fully arbitrarily. There's no incentive for us to care about page sizes other than the fact that it seems to be a good
// middle ground for data size.
const PAGE_SIZE: u64 = 4096;
PAGE_SIZE / self.line_width.width as u64
}
fn schedule_scroll_down(&mut self) {
self.base_row = self.base_row.saturating_add(1)
}
fn schedule_scroll_up(&mut self) {
self.base_row = self.base_row.saturating_sub(1);
}
}
struct ScrollbarDragging;
static HEX_BYTES_MEMOIZED: LazyLock<[SharedString; 256]> =
LazyLock::new(|| std::array::from_fn(|byte| SharedString::from(format!("{byte:02X}"))));
static UNKNOWN_BYTE: SharedString = SharedString::new_static("??");
impl MemoryView {
pub(crate) fn new(
session: Entity<Session>,
workspace: WeakEntity<Workspace>,
stack_frame_list: WeakEntity<StackFrameList>,
window: &mut Window,
cx: &mut Context<Self>,
) -> Self {
let view_state = ViewState::new(0, WIDTHS[4].clone());
let scroll_handle = UniformListScrollHandle::default();
let query_editor = cx.new(|cx| Editor::single_line(window, cx));
let scroll_state = ScrollbarState::new(scroll_handle.clone());
let mut this = Self {
workspace,
scroll_state,
scroll_handle,
stack_frame_list,
show_scrollbar: false,
hide_scrollbar_task: None,
focus_handle: cx.focus_handle(),
view_state,
query_editor,
session,
width_picker_handle: Default::default(),
is_writing_memory: true,
open_context_menu: None,
};
this.change_query_bar_mode(false, window, cx);
cx.on_focus_out(&this.focus_handle, window, |this, _, window, cx| {
this.change_query_bar_mode(false, window, cx);
cx.notify();
})
.detach();
this
}
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| {
cx.background_executor()
.timer(SCROLLBAR_SHOW_INTERVAL)
.await;
panel
.update(cx, |panel, cx| {
panel.show_scrollbar = false;
cx.notify();
})
.log_err();
}))
}
fn render_vertical_scrollbar(&self, cx: &mut Context<Self>) -> Option<Stateful<Div>> {
if !(self.show_scrollbar || self.scroll_state.is_dragging()) {
return None;
}
Some(
div()
.occlude()
.id("memory-view-vertical-scrollbar")
.on_drag_move(cx.listener(|this, evt, _, cx| {
let did_handle = this.handle_scroll_drag(evt);
cx.notify();
if did_handle {
cx.stop_propagation()
}
}))
.on_drag(ScrollbarDragging, |_, _, _, cx| cx.new(|_| Empty))
.on_hover(|_, _, cx| {
cx.stop_propagation();
})
.on_any_mouse_down(|_, _, cx| {
cx.stop_propagation();
})
.on_mouse_up(
MouseButton::Left,
cx.listener(|_, _, _, cx| {
cx.stop_propagation();
}),
)
.on_scroll_wheel(cx.listener(|_, _, _, cx| {
cx.notify();
}))
.h_full()
.absolute()
.right_1()
.top_1()
.bottom_0()
.w(px(12.))
.cursor_default()
.children(Scrollbar::vertical(self.scroll_state.clone())),
)
}
fn render_memory(&self, cx: &mut Context<Self>) -> UniformList {
let weak = cx.weak_entity();
let session = self.session.clone();
let view_state = self.view_state.clone();
uniform_list(
"debugger-memory-view",
self.view_state.row_count() as usize,
move |range, _, cx| {
let mut line_buffer = Vec::with_capacity(view_state.line_width.width as usize);
let memory_start =
(view_state.base_row + range.start as u64) * view_state.line_width.width as u64;
let memory_end = (view_state.base_row + range.end as u64)
* view_state.line_width.width as u64
- 1;
let mut memory = session.update(cx, |this, cx| {
this.read_memory(memory_start..=memory_end, cx)
});
let mut rows = Vec::with_capacity(range.end - range.start);
for ix in range {
line_buffer.extend((&mut memory).take(view_state.line_width.width as usize));
rows.push(render_single_memory_view_line(
&line_buffer,
ix as u64,
weak.clone(),
cx,
));
line_buffer.clear();
}
rows
},
)
.track_scroll(self.scroll_handle.clone())
.on_scroll_wheel(cx.listener(|this, evt: &ScrollWheelEvent, window, _| {
let delta = evt.delta.pixel_delta(window.line_height());
let scroll_handle = this.scroll_state.scroll_handle();
let size = scroll_handle.content_size();
let viewport = scroll_handle.viewport();
let current_offset = scroll_handle.offset();
let first_entry_offset_boundary = size.height / this.view_state.row_count() as f32;
let last_entry_offset_boundary = size.height - first_entry_offset_boundary;
if first_entry_offset_boundary + viewport.size.height > current_offset.y.abs() {
// The topmost entry is visible, hence if we're scrolling up, we need to load extra lines.
this.view_state.schedule_scroll_up();
} else if last_entry_offset_boundary < current_offset.y.abs() + viewport.size.height {
this.view_state.schedule_scroll_down();
}
scroll_handle.set_offset(current_offset + point(px(0.), delta.y));
}))
}
fn render_query_bar(&self, cx: &Context<Self>) -> impl IntoElement {
EditorElement::new(
&self.query_editor,
Self::editor_style(&self.query_editor, cx),
)
}
pub(super) fn go_to_memory_reference(
&mut self,
memory_reference: &str,
evaluate_name: Option<&str>,
stack_frame_id: Option<u64>,
cx: &mut Context<Self>,
) {
use parse_int::parse;
let Ok(as_address) = parse::<u64>(&memory_reference) else {
return;
};
let access_size = evaluate_name
.map(|typ| {
self.session.update(cx, |this, cx| {
this.data_access_size(stack_frame_id, typ, cx)
})
})
.unwrap_or_else(|| Task::ready(None));
cx.spawn(async move |this, cx| {
let access_size = access_size.await.unwrap_or(1);
this.update(cx, |this, cx| {
this.view_state.selection = Some(SelectedMemoryRange::DragComplete(Drag {
start_address: as_address,
end_address: as_address + access_size - 1,
}));
this.jump_to_address(as_address, cx);
})
.ok();
})
.detach();
}
fn handle_memory_drag(&mut self, evt: &DragMoveEvent<Drag>) {
if !self
.view_state
.selection
.as_ref()
.is_some_and(|selection| selection.is_dragging())
{
return;
}
let row_count = self.view_state.row_count();
debug_assert!(row_count > 1);
let scroll_handle = self.scroll_state.scroll_handle();
let viewport = scroll_handle.viewport();
if viewport.bottom() < evt.event.position.y {
self.view_state.schedule_scroll_down();
} else if viewport.top() > evt.event.position.y {
self.view_state.schedule_scroll_up();
}
}
fn handle_scroll_drag(&mut self, evt: &DragMoveEvent<ScrollbarDragging>) -> bool {
if !self.scroll_state.is_dragging() {
return false;
}
let row_count = self.view_state.row_count();
debug_assert!(row_count > 1);
let scroll_handle = self.scroll_state.scroll_handle();
let viewport = scroll_handle.viewport();
if viewport.bottom() < evt.event.position.y {
self.view_state.schedule_scroll_down();
true
} else if viewport.top() > evt.event.position.y {
self.view_state.schedule_scroll_up();
true
} else {
false
}
}
fn editor_style(editor: &Entity<Editor>, cx: &Context<Self>) -> EditorStyle {
let is_read_only = editor.read(cx).read_only(cx);
let settings = ThemeSettings::get_global(cx);
let theme = cx.theme();
let text_style = TextStyle {
color: if is_read_only {
theme.colors().text_muted
} else {
theme.colors().text
},
font_family: settings.buffer_font.family.clone(),
font_features: settings.buffer_font.features.clone(),
font_size: TextSize::Small.rems(cx).into(),
font_weight: settings.buffer_font.weight,
..Default::default()
};
EditorStyle {
background: theme.colors().editor_background,
local_player: theme.players().local(),
text: text_style,
..Default::default()
}
}
fn render_width_picker(&self, window: &mut Window, cx: &mut Context<Self>) -> DropdownMenu {
let weak = cx.weak_entity();
let selected_width = self.view_state.line_width.clone();
DropdownMenu::new(
"memory-view-width-picker",
selected_width.label.clone(),
ContextMenu::build(window, cx, |mut this, window, cx| {
for width in &WIDTHS {
let weak = weak.clone();
let width = width.clone();
this = this.entry(width.label.clone(), None, move |_, cx| {
_ = weak.update(cx, |this, _| {
// Convert base ix between 2 line widths to keep the shown memory address roughly the same.
// All widths are powers of 2, so the conversion should be lossless.
match this.view_state.line_width.width.cmp(&width.width) {
std::cmp::Ordering::Less => {
// We're converting up.
let shift = width.width.trailing_zeros()
- this.view_state.line_width.width.trailing_zeros();
this.view_state.base_row >>= shift;
}
std::cmp::Ordering::Greater => {
// We're converting down.
let shift = this.view_state.line_width.width.trailing_zeros()
- width.width.trailing_zeros();
this.view_state.base_row <<= shift;
}
_ => {}
}
this.view_state.line_width = width.clone();
});
});
}
if let Some(ix) = WIDTHS
.iter()
.position(|width| width.width == selected_width.width)
{
for _ in 0..=ix {
this.select_next(&Default::default(), window, cx);
}
}
this
}),
)
.handle(self.width_picker_handle.clone())
}
fn page_down(&mut self, _: &menu::SelectLast, _: &mut Window, cx: &mut Context<Self>) {
self.view_state.base_row = self
.view_state
.base_row
.overflowing_add(self.view_state.row_count())
.0;
cx.notify();
}
fn page_up(&mut self, _: &menu::SelectFirst, _: &mut Window, cx: &mut Context<Self>) {
self.view_state.base_row = self
.view_state
.base_row
.overflowing_sub(self.view_state.row_count())
.0;
cx.notify();
}
fn change_query_bar_mode(
&mut self,
is_writing_memory: bool,
window: &mut Window,
cx: &mut Context<Self>,
) {
if is_writing_memory == self.is_writing_memory {
return;
}
if !self.is_writing_memory {
self.query_editor.update(cx, |this, cx| {
this.clear(window, cx);
this.set_placeholder_text("Write to Selected Memory Range", cx);
});
self.is_writing_memory = true;
self.query_editor.focus_handle(cx).focus(window);
} else {
self.query_editor.update(cx, |this, cx| {
this.clear(window, cx);
this.set_placeholder_text("Go to Memory Address / Expression", cx);
});
self.is_writing_memory = false;
}
}
fn toggle_data_breakpoint(
&mut self,
_: &crate::ToggleDataBreakpoint,
_: &mut Window,
cx: &mut Context<Self>,
) {
let Some(SelectedMemoryRange::DragComplete(selection)) = self.view_state.selection.clone()
else {
return;
};
let range = selection.memory_range();
let context = Arc::new(DataBreakpointContext::Address {
address: range.start().to_string(),
bytes: Some(*range.end() - *range.start()),
});
self.session.update(cx, |this, cx| {
let data_breakpoint_info = this.data_breakpoint_info(context.clone(), None, cx);
cx.spawn(async move |this, cx| {
if let Some(info) = data_breakpoint_info.await {
let Some(data_id) = info.data_id.clone() else {
return;
};
_ = this.update(cx, |this, cx| {
this.create_data_breakpoint(
context,
data_id.clone(),
dap::DataBreakpoint {
data_id,
access_type: None,
condition: None,
hit_condition: None,
},
cx,
);
});
}
})
.detach();
})
}
fn confirm(&mut self, _: &menu::Confirm, window: &mut Window, cx: &mut Context<Self>) {
if let Some(SelectedMemoryRange::DragComplete(drag)) = &self.view_state.selection {
// Go into memory writing mode.
if !self.is_writing_memory {
let should_return = self.session.update(cx, |session, cx| {
if !session
.capabilities()
.supports_write_memory_request
.unwrap_or_default()
{
let adapter_name = session.adapter();
// We cannot write memory with this adapter.
_ = self.workspace.update(cx, |this, cx| {
this.toggle_status_toast(
StatusToast::new(format!(
"Debug Adapter `{adapter_name}` does not support writing to memory"
), cx, |this, cx| {
cx.spawn(async move |this, cx| {
cx.background_executor().timer(Duration::from_secs(2)).await;
_ = this.update(cx, |_, cx| {
cx.emit(DismissEvent)
});
}).detach();
this.icon(ToastIcon::new(IconName::XCircle).color(Color::Error))
}),
cx,
);
});
true
} else {
false
}
});
if should_return {
return;
}
self.change_query_bar_mode(true, window, cx);
} else if self.query_editor.focus_handle(cx).is_focused(window) {
let mut text = self.query_editor.read(cx).text(cx);
if text.chars().any(|c| !c.is_ascii_hexdigit()) {
// Interpret this text as a string and oh-so-conveniently convert it.
text = text.bytes().map(|byte| format!("{:02x}", byte)).collect();
}
self.session.update(cx, |this, cx| {
let range = drag.memory_range();
if let Ok(as_hex) = hex::decode(text) {
this.write_memory(*range.start(), &as_hex, cx);
}
});
self.change_query_bar_mode(false, window, cx);
}
cx.notify();
return;
}
// Just change the currently viewed address.
if !self.query_editor.focus_handle(cx).is_focused(window) {
return;
}
self.jump_to_query_bar_address(cx);
}
fn jump_to_query_bar_address(&mut self, cx: &mut Context<Self>) {
use parse_int::parse;
let text = self.query_editor.read(cx).text(cx);
let Ok(as_address) = parse::<u64>(&text) else {
return self.jump_to_expression(text, cx);
};
self.jump_to_address(as_address, cx);
}
fn jump_to_address(&mut self, address: u64, cx: &mut Context<Self>) {
self.view_state.base_row = (address & !0xfff) / self.view_state.line_width.width as u64;
let line_ix = (address & 0xfff) / self.view_state.line_width.width as u64;
self.scroll_handle
.scroll_to_item(line_ix as usize, ScrollStrategy::Center);
cx.notify();
}
fn jump_to_expression(&mut self, expr: String, cx: &mut Context<Self>) {
let Ok(selected_frame) = self
.stack_frame_list
.update(cx, |this, _| this.opened_stack_frame_id())
else {
return;
};
let expr = format!("?${{{expr}}}");
let reference = self.session.update(cx, |this, cx| {
this.memory_reference_of_expr(selected_frame, expr, cx)
});
cx.spawn(async move |this, cx| {
if let Some((reference, typ)) = reference.await {
_ = this.update(cx, |this, cx| {
let sizeof_expr = if typ.as_ref().is_some_and(|t| {
t.chars()
.all(|c| c.is_whitespace() || c.is_alphabetic() || c == '*')
}) {
typ.as_deref()
} else {
None
};
this.go_to_memory_reference(&reference, sizeof_expr, selected_frame, cx);
});
}
})
.detach();
}
fn cancel(&mut self, _: &menu::Cancel, _: &mut Window, cx: &mut Context<Self>) {
self.view_state.selection = None;
cx.notify();
}
/// Jump to memory pointed to by selected memory range.
fn go_to_address(
&mut self,
_: &GoToSelectedAddress,
window: &mut Window,
cx: &mut Context<Self>,
) {
let Some(SelectedMemoryRange::DragComplete(drag)) = self.view_state.selection.clone()
else {
return;
};
let range = drag.memory_range();
let Some(memory): Option<Vec<u8>> = self.session.update(cx, |this, cx| {
this.read_memory(range, cx).map(|cell| cell.0).collect()
}) else {
return;
};
if memory.len() > 8 {
return;
}
let zeros_to_write = 8 - memory.len();
let mut acc = String::from("0x");
acc.extend(std::iter::repeat("00").take(zeros_to_write));
let as_query = memory.into_iter().rev().fold(acc, |mut acc, byte| {
_ = write!(&mut acc, "{:02x}", byte);
acc
});
self.query_editor.update(cx, |this, cx| {
this.set_text(as_query, window, cx);
});
self.jump_to_query_bar_address(cx);
}
fn deploy_memory_context_menu(
&mut self,
range: RangeInclusive<u64>,
position: Point<Pixels>,
window: &mut Window,
cx: &mut Context<Self>,
) {
let session = self.session.clone();
let context_menu = ContextMenu::build(window, cx, |menu, _, cx| {
let range_too_large = range.end() - range.start() > std::mem::size_of::<u64>() as u64;
let caps = session.read(cx).capabilities();
let supports_data_breakpoints = caps.supports_data_breakpoints.unwrap_or_default()
&& caps.supports_data_breakpoint_bytes.unwrap_or_default();
let memory_unreadable = LazyCell::new(|| {
session.update(cx, |this, cx| {
this.read_memory(range.clone(), cx)
.any(|cell| cell.0.is_none())
})
});
let mut menu = menu.action_disabled_when(
range_too_large || *memory_unreadable,
"Go To Selected Address",
GoToSelectedAddress.boxed_clone(),
);
if supports_data_breakpoints {
menu = menu.action_disabled_when(
*memory_unreadable,
"Set Data Breakpoint",
ToggleDataBreakpoint.boxed_clone(),
);
}
menu.context(self.focus_handle.clone())
});
cx.focus_view(&context_menu, window);
let subscription = cx.subscribe_in(
&context_menu,
window,
|this, _, _: &DismissEvent, window, cx| {
if this.open_context_menu.as_ref().is_some_and(|context_menu| {
context_menu.0.focus_handle(cx).contains_focused(window, cx)
}) {
cx.focus_self(window);
}
this.open_context_menu.take();
cx.notify();
},
);
self.open_context_menu = Some((context_menu, position, subscription));
}
}
#[derive(Clone)]
struct ViewWidth {
width: u8,
label: SharedString,
}
impl ViewWidth {
const fn new(width: u8, label: &'static str) -> Self {
Self {
width,
label: SharedString::new_static(label),
}
}
}
static WIDTHS: [ViewWidth; 7] = [
ViewWidth::new(1, "1 byte"),
ViewWidth::new(2, "2 bytes"),
ViewWidth::new(4, "4 bytes"),
ViewWidth::new(8, "8 bytes"),
ViewWidth::new(16, "16 bytes"),
ViewWidth::new(32, "32 bytes"),
ViewWidth::new(64, "64 bytes"),
];
fn render_single_memory_view_line(
memory: &[MemoryCell],
ix: u64,
weak: gpui::WeakEntity<MemoryView>,
cx: &mut App,
) -> AnyElement {
let Ok(view_state) = weak.update(cx, |this, _| this.view_state.clone()) else {
return div().into_any();
};
let base_address = (view_state.base_row + ix) * view_state.line_width.width as u64;
h_flex()
.id((
"memory-view-row-full",
ix * view_state.line_width.width as u64,
))
.size_full()
.gap_x_2()
.child(
div()
.child(
Label::new(format!("{:016X}", base_address))
.buffer_font(cx)
.size(ui::LabelSize::Small)
.color(Color::Muted),
)
.px_1()
.border_r_1()
.border_color(Color::Muted.color(cx)),
)
.child(
h_flex()
.id((
"memory-view-row-raw-memory",
ix * view_state.line_width.width as u64,
))
.px_1()
.children(memory.iter().enumerate().map(|(cell_ix, cell)| {
let weak = weak.clone();
div()
.id(("memory-view-row-raw-memory-cell", cell_ix as u64))
.px_0p5()
.when_some(view_state.selection.as_ref(), |this, selection| {
this.when(selection.contains(base_address + cell_ix as u64), |this| {
let weak = weak.clone();
this.bg(Color::Selected.color(cx).opacity(0.2)).when(
!selection.is_dragging(),
|this| {
let selection = selection.drag().memory_range();
this.on_mouse_down(
MouseButton::Right,
move |click, window, cx| {
_ = weak.update(cx, |this, cx| {
this.deploy_memory_context_menu(
selection.clone(),
click.position,
window,
cx,
)
});
cx.stop_propagation();
},
)
},
)
})
})
.child(
Label::new(
cell.0
.map(|val| HEX_BYTES_MEMOIZED[val as usize].clone())
.unwrap_or_else(|| UNKNOWN_BYTE.clone()),
)
.buffer_font(cx)
.when(cell.0.is_none(), |this| this.color(Color::Muted))
.size(ui::LabelSize::Small),
)
.on_drag(
Drag {
start_address: base_address + cell_ix as u64,
end_address: base_address + cell_ix as u64,
},
{
let weak = weak.clone();
move |drag, _, _, cx| {
_ = weak.update(cx, |this, _| {
this.view_state.selection =
Some(SelectedMemoryRange::DragUnderway(drag.clone()));
});
cx.new(|_| Empty)
}
},
)
.on_drop({
let weak = weak.clone();
move |drag: &Drag, _, cx| {
_ = weak.update(cx, |this, _| {
this.view_state.selection =
Some(SelectedMemoryRange::DragComplete(Drag {
start_address: drag.start_address,
end_address: base_address + cell_ix as u64,
}));
});
}
})
.drag_over(move |style, drag: &Drag, _, cx| {
_ = weak.update(cx, |this, _| {
this.view_state.selection =
Some(SelectedMemoryRange::DragUnderway(Drag {
start_address: drag.start_address,
end_address: base_address + cell_ix as u64,
}));
});
style
})
})),
)
.child(
h_flex()
.id((
"memory-view-row-ascii-memory",
ix * view_state.line_width.width as u64,
))
.h_full()
.px_1()
.mr_4()
// .gap_x_1p5()
.border_x_1()
.border_color(Color::Muted.color(cx))
.children(memory.iter().enumerate().map(|(ix, cell)| {
let as_character = char::from(cell.0.unwrap_or(0));
let as_visible = if as_character.is_ascii_graphic() {
as_character
} else {
'·'
};
div()
.px_0p5()
.when_some(view_state.selection.as_ref(), |this, selection| {
this.when(selection.contains(base_address + ix as u64), |this| {
this.bg(Color::Selected.color(cx).opacity(0.2))
})
})
.child(
Label::new(format!("{as_visible}"))
.buffer_font(cx)
.when(cell.0.is_none(), |this| this.color(Color::Muted))
.size(ui::LabelSize::Small),
)
})),
)
.into_any()
}
impl Render for MemoryView {
fn render(
&mut self,
window: &mut ui::Window,
cx: &mut ui::Context<Self>,
) -> impl ui::IntoElement {
let (icon, tooltip_text) = if self.is_writing_memory {
(IconName::Pencil, "Edit memory at a selected address")
} else {
(
IconName::LocationEdit,
"Change address of currently viewed memory",
)
};
v_flex()
.id("Memory-view")
.on_action(cx.listener(Self::cancel))
.on_action(cx.listener(Self::go_to_address))
.p_1()
.on_action(cx.listener(Self::confirm))
.on_action(cx.listener(Self::toggle_data_breakpoint))
.on_action(cx.listener(Self::page_down))
.on_action(cx.listener(Self::page_up))
.size_full()
.track_focus(&self.focus_handle)
.on_hover(cx.listener(|this, hovered, window, cx| {
if *hovered {
this.show_scrollbar = true;
this.hide_scrollbar_task.take();
cx.notify();
} else if !this.focus_handle.contains_focused(window, cx) {
this.hide_scrollbar(window, cx);
}
}))
.child(
h_flex()
.w_full()
.mb_0p5()
.gap_1()
.child(
h_flex()
.w_full()
.rounded_md()
.border_1()
.gap_x_2()
.px_2()
.py_0p5()
.mb_0p5()
.bg(cx.theme().colors().editor_background)
.when_else(
self.query_editor
.focus_handle(cx)
.contains_focused(window, cx),
|this| this.border_color(cx.theme().colors().border_focused),
|this| this.border_color(cx.theme().colors().border_transparent),
)
.child(
div()
.id("memory-view-editor-icon")
.child(Icon::new(icon).size(ui::IconSize::XSmall))
.tooltip(Tooltip::text(tooltip_text)),
)
.child(self.render_query_bar(cx)),
)
.child(self.render_width_picker(window, cx)),
)
.child(Divider::horizontal())
.child(
v_flex()
.size_full()
.on_drag_move(cx.listener(|this, evt, _, _| {
this.handle_memory_drag(&evt);
}))
.child(self.render_memory(cx).size_full())
.children(self.open_context_menu.as_ref().map(|(menu, position, _)| {
deferred(
anchored()
.position(*position)
.anchor(gpui::Corner::TopLeft)
.child(menu.clone()),
)
.with_priority(1)
}))
.children(self.render_vertical_scrollbar(cx)),
)
}
}

View File

@@ -1,3 +1,5 @@
use crate::session::running::{RunningState, memory_view::MemoryView};
use super::stack_frame_list::{StackFrameList, StackFrameListEvent};
use dap::{
ScopePresentationHint, StackFrameId, VariablePresentationHint, VariablePresentationHintKind,
@@ -7,13 +9,17 @@ use editor::Editor;
use gpui::{
Action, AnyElement, ClickEvent, ClipboardItem, Context, DismissEvent, Empty, Entity,
FocusHandle, Focusable, Hsla, MouseButton, MouseDownEvent, Point, Stateful, Subscription,
TextStyleRefinement, UniformListScrollHandle, actions, anchored, deferred, uniform_list,
TextStyleRefinement, UniformListScrollHandle, WeakEntity, actions, anchored, deferred,
uniform_list,
};
use menu::{SelectFirst, SelectLast, SelectNext, SelectPrevious};
use project::debugger::session::{Session, SessionEvent, Watcher};
use project::debugger::{
dap_command::DataBreakpointContext,
session::{Session, SessionEvent, Watcher},
};
use std::{collections::HashMap, ops::Range, sync::Arc};
use ui::{ContextMenu, ListItem, ScrollableHandle, Scrollbar, ScrollbarState, Tooltip, prelude::*};
use util::debug_panic;
use util::{debug_panic, maybe};
actions!(
variable_list,
@@ -32,6 +38,8 @@ actions!(
AddWatch,
/// Removes the selected variable from the watch list.
RemoveWatch,
/// Jump to variable's memory location.
GoToMemory,
]
);
@@ -86,30 +94,30 @@ impl EntryPath {
}
#[derive(Debug, Clone, PartialEq)]
enum EntryKind {
enum DapEntry {
Watcher(Watcher),
Variable(dap::Variable),
Scope(dap::Scope),
}
impl EntryKind {
impl DapEntry {
fn as_watcher(&self) -> Option<&Watcher> {
match self {
EntryKind::Watcher(watcher) => Some(watcher),
DapEntry::Watcher(watcher) => Some(watcher),
_ => None,
}
}
fn as_variable(&self) -> Option<&dap::Variable> {
match self {
EntryKind::Variable(dap) => Some(dap),
DapEntry::Variable(dap) => Some(dap),
_ => None,
}
}
fn as_scope(&self) -> Option<&dap::Scope> {
match self {
EntryKind::Scope(dap) => Some(dap),
DapEntry::Scope(dap) => Some(dap),
_ => None,
}
}
@@ -117,38 +125,38 @@ impl EntryKind {
#[cfg(test)]
fn name(&self) -> &str {
match self {
EntryKind::Watcher(watcher) => &watcher.expression,
EntryKind::Variable(dap) => &dap.name,
EntryKind::Scope(dap) => &dap.name,
DapEntry::Watcher(watcher) => &watcher.expression,
DapEntry::Variable(dap) => &dap.name,
DapEntry::Scope(dap) => &dap.name,
}
}
}
#[derive(Debug, Clone, PartialEq)]
struct ListEntry {
dap_kind: EntryKind,
entry: DapEntry,
path: EntryPath,
}
impl ListEntry {
fn as_watcher(&self) -> Option<&Watcher> {
self.dap_kind.as_watcher()
self.entry.as_watcher()
}
fn as_variable(&self) -> Option<&dap::Variable> {
self.dap_kind.as_variable()
self.entry.as_variable()
}
fn as_scope(&self) -> Option<&dap::Scope> {
self.dap_kind.as_scope()
self.entry.as_scope()
}
fn item_id(&self) -> ElementId {
use std::fmt::Write;
let mut id = match &self.dap_kind {
EntryKind::Watcher(watcher) => format!("watcher-{}", watcher.expression),
EntryKind::Variable(dap) => format!("variable-{}", dap.name),
EntryKind::Scope(dap) => format!("scope-{}", dap.name),
let mut id = match &self.entry {
DapEntry::Watcher(watcher) => format!("watcher-{}", watcher.expression),
DapEntry::Variable(dap) => format!("variable-{}", dap.name),
DapEntry::Scope(dap) => format!("scope-{}", dap.name),
};
for name in self.path.indices.iter() {
_ = write!(id, "-{}", name);
@@ -158,10 +166,10 @@ impl ListEntry {
fn item_value_id(&self) -> ElementId {
use std::fmt::Write;
let mut id = match &self.dap_kind {
EntryKind::Watcher(watcher) => format!("watcher-{}", watcher.expression),
EntryKind::Variable(dap) => format!("variable-{}", dap.name),
EntryKind::Scope(dap) => format!("scope-{}", dap.name),
let mut id = match &self.entry {
DapEntry::Watcher(watcher) => format!("watcher-{}", watcher.expression),
DapEntry::Variable(dap) => format!("variable-{}", dap.name),
DapEntry::Scope(dap) => format!("scope-{}", dap.name),
};
for name in self.path.indices.iter() {
_ = write!(id, "-{}", name);
@@ -188,13 +196,17 @@ pub struct VariableList {
focus_handle: FocusHandle,
edited_path: Option<(EntryPath, Entity<Editor>)>,
disabled: bool,
memory_view: Entity<MemoryView>,
weak_running: WeakEntity<RunningState>,
_subscriptions: Vec<Subscription>,
}
impl VariableList {
pub fn new(
pub(crate) fn new(
session: Entity<Session>,
stack_frame_list: Entity<StackFrameList>,
memory_view: Entity<MemoryView>,
weak_running: WeakEntity<RunningState>,
window: &mut Window,
cx: &mut Context<Self>,
) -> Self {
@@ -211,6 +223,7 @@ impl VariableList {
SessionEvent::Variables | SessionEvent::Watchers => {
this.build_entries(cx);
}
_ => {}
}),
cx.on_focus_out(&focus_handle, window, |this, _, _, cx| {
@@ -234,6 +247,8 @@ impl VariableList {
edited_path: None,
entries: Default::default(),
entry_states: Default::default(),
weak_running,
memory_view,
}
}
@@ -284,7 +299,7 @@ impl VariableList {
scope.variables_reference,
scope.variables_reference,
EntryPath::for_scope(&scope.name),
EntryKind::Scope(scope),
DapEntry::Scope(scope),
)
})
.collect::<Vec<_>>();
@@ -298,7 +313,7 @@ impl VariableList {
watcher.variables_reference,
watcher.variables_reference,
EntryPath::for_watcher(watcher.expression.clone()),
EntryKind::Watcher(watcher.clone()),
DapEntry::Watcher(watcher.clone()),
)
})
.collect::<Vec<_>>(),
@@ -309,9 +324,9 @@ impl VariableList {
while let Some((container_reference, variables_reference, mut path, dap_kind)) = stack.pop()
{
match &dap_kind {
EntryKind::Watcher(watcher) => path = path.with_child(watcher.expression.clone()),
EntryKind::Variable(dap) => path = path.with_name(dap.name.clone().into()),
EntryKind::Scope(dap) => path = path.with_child(dap.name.clone().into()),
DapEntry::Watcher(watcher) => path = path.with_child(watcher.expression.clone()),
DapEntry::Variable(dap) => path = path.with_name(dap.name.clone().into()),
DapEntry::Scope(dap) => path = path.with_child(dap.name.clone().into()),
}
let var_state = self
@@ -336,7 +351,7 @@ impl VariableList {
});
entries.push(ListEntry {
dap_kind,
entry: dap_kind,
path: path.clone(),
});
@@ -349,7 +364,7 @@ impl VariableList {
variables_reference,
child.variables_reference,
path.with_child(child.name.clone().into()),
EntryKind::Variable(child),
DapEntry::Variable(child),
)
}));
}
@@ -380,9 +395,9 @@ impl VariableList {
pub fn completion_variables(&self, _cx: &mut Context<Self>) -> Vec<dap::Variable> {
self.entries
.iter()
.filter_map(|entry| match &entry.dap_kind {
EntryKind::Variable(dap) => Some(dap.clone()),
EntryKind::Scope(_) | EntryKind::Watcher { .. } => None,
.filter_map(|entry| match &entry.entry {
DapEntry::Variable(dap) => Some(dap.clone()),
DapEntry::Scope(_) | DapEntry::Watcher { .. } => None,
})
.collect()
}
@@ -400,12 +415,12 @@ impl VariableList {
.get(ix)
.and_then(|entry| Some(entry).zip(self.entry_states.get(&entry.path)))?;
match &entry.dap_kind {
EntryKind::Watcher { .. } => {
match &entry.entry {
DapEntry::Watcher { .. } => {
Some(self.render_watcher(entry, *state, window, cx))
}
EntryKind::Variable(_) => Some(self.render_variable(entry, *state, window, cx)),
EntryKind::Scope(_) => Some(self.render_scope(entry, *state, cx)),
DapEntry::Variable(_) => Some(self.render_variable(entry, *state, window, cx)),
DapEntry::Scope(_) => Some(self.render_scope(entry, *state, cx)),
}
})
.collect()
@@ -562,6 +577,51 @@ impl VariableList {
}
}
fn jump_to_variable_memory(
&mut self,
_: &GoToMemory,
window: &mut Window,
cx: &mut Context<Self>,
) {
_ = maybe!({
let selection = self.selection.as_ref()?;
let entry = self.entries.iter().find(|entry| &entry.path == selection)?;
let var = entry.entry.as_variable()?;
let memory_reference = var.memory_reference.as_deref()?;
let sizeof_expr = if var.type_.as_ref().is_some_and(|t| {
t.chars()
.all(|c| c.is_whitespace() || c.is_alphabetic() || c == '*')
}) {
var.type_.as_deref()
} else {
var.evaluate_name
.as_deref()
.map(|name| name.strip_prefix("/nat ").unwrap_or_else(|| name))
};
self.memory_view.update(cx, |this, cx| {
this.go_to_memory_reference(
memory_reference,
sizeof_expr,
self.selected_stack_frame_id,
cx,
);
});
let weak_panel = self.weak_running.clone();
window.defer(cx, move |window, cx| {
_ = weak_panel.update(cx, |this, cx| {
this.activate_item(
crate::persistence::DebuggerPaneItem::MemoryView,
window,
cx,
);
});
});
Some(())
});
}
fn deploy_list_entry_context_menu(
&mut self,
entry: ListEntry,
@@ -569,49 +629,156 @@ impl VariableList {
window: &mut Window,
cx: &mut Context<Self>,
) {
let supports_set_variable = self
.session
.read(cx)
.capabilities()
.supports_set_variable
.unwrap_or_default();
let (supports_set_variable, supports_data_breakpoints, supports_go_to_memory) =
self.session.read_with(cx, |session, _| {
(
session
.capabilities()
.supports_set_variable
.unwrap_or_default(),
session
.capabilities()
.supports_data_breakpoints
.unwrap_or_default(),
session
.capabilities()
.supports_read_memory_request
.unwrap_or_default(),
)
});
let can_toggle_data_breakpoint = entry
.as_variable()
.filter(|_| supports_data_breakpoints)
.and_then(|variable| {
let variables_reference = self
.entry_states
.get(&entry.path)
.map(|state| state.parent_reference)?;
Some(self.session.update(cx, |session, cx| {
session.data_breakpoint_info(
Arc::new(DataBreakpointContext::Variable {
variables_reference,
name: variable.name.clone(),
bytes: None,
}),
None,
cx,
)
}))
});
let context_menu = ContextMenu::build(window, cx, |menu, _, _| {
menu.when(entry.as_variable().is_some(), |menu| {
menu.action("Copy Name", CopyVariableName.boxed_clone())
.action("Copy Value", CopyVariableValue.boxed_clone())
.when(supports_set_variable, |menu| {
menu.action("Edit Value", EditVariable.boxed_clone())
let focus_handle = self.focus_handle.clone();
cx.spawn_in(window, async move |this, cx| {
let can_toggle_data_breakpoint = if let Some(task) = can_toggle_data_breakpoint {
task.await.is_some()
} else {
true
};
cx.update(|window, cx| {
let context_menu = ContextMenu::build(window, cx, |menu, _, _| {
menu.when_some(entry.as_variable(), |menu, _| {
menu.action("Copy Name", CopyVariableName.boxed_clone())
.action("Copy Value", CopyVariableValue.boxed_clone())
.when(supports_set_variable, |menu| {
menu.action("Edit Value", EditVariable.boxed_clone())
})
.when(supports_go_to_memory, |menu| {
menu.action("Go To Memory", GoToMemory.boxed_clone())
})
.action("Watch Variable", AddWatch.boxed_clone())
.when(can_toggle_data_breakpoint, |menu| {
menu.action(
"Toggle Data Breakpoint",
crate::ToggleDataBreakpoint.boxed_clone(),
)
})
})
.action("Watch Variable", AddWatch.boxed_clone())
})
.when(entry.as_watcher().is_some(), |menu| {
menu.action("Copy Name", CopyVariableName.boxed_clone())
.action("Copy Value", CopyVariableValue.boxed_clone())
.when(supports_set_variable, |menu| {
menu.action("Edit Value", EditVariable.boxed_clone())
.when(entry.as_watcher().is_some(), |menu| {
menu.action("Copy Name", CopyVariableName.boxed_clone())
.action("Copy Value", CopyVariableValue.boxed_clone())
.when(supports_set_variable, |menu| {
menu.action("Edit Value", EditVariable.boxed_clone())
})
.action("Remove Watch", RemoveWatch.boxed_clone())
})
.action("Remove Watch", RemoveWatch.boxed_clone())
.context(focus_handle.clone())
});
_ = this.update(cx, |this, cx| {
cx.focus_view(&context_menu, window);
let subscription = cx.subscribe_in(
&context_menu,
window,
|this, _, _: &DismissEvent, window, cx| {
if this.open_context_menu.as_ref().is_some_and(|context_menu| {
context_menu.0.focus_handle(cx).contains_focused(window, cx)
}) {
cx.focus_self(window);
}
this.open_context_menu.take();
cx.notify();
},
);
this.open_context_menu = Some((context_menu, position, subscription));
});
})
.context(self.focus_handle.clone())
})
.detach();
}
fn toggle_data_breakpoint(
&mut self,
_: &crate::ToggleDataBreakpoint,
_window: &mut Window,
cx: &mut Context<Self>,
) {
let Some(entry) = self
.selection
.as_ref()
.and_then(|selection| self.entries.iter().find(|entry| &entry.path == selection))
else {
return;
};
let Some((name, var_ref)) = entry.as_variable().map(|var| &var.name).zip(
self.entry_states
.get(&entry.path)
.map(|state| state.parent_reference),
) else {
return;
};
let context = Arc::new(DataBreakpointContext::Variable {
variables_reference: var_ref,
name: name.clone(),
bytes: None,
});
let data_breakpoint = self.session.update(cx, |session, cx| {
session.data_breakpoint_info(context.clone(), None, cx)
});
cx.focus_view(&context_menu, window);
let subscription = cx.subscribe_in(
&context_menu,
window,
|this, _, _: &DismissEvent, window, cx| {
if this.open_context_menu.as_ref().is_some_and(|context_menu| {
context_menu.0.focus_handle(cx).contains_focused(window, cx)
}) {
cx.focus_self(window);
}
this.open_context_menu.take();
let session = self.session.downgrade();
cx.spawn(async move |_, cx| {
let Some(data_id) = data_breakpoint.await.and_then(|info| info.data_id) else {
return;
};
_ = session.update(cx, |session, cx| {
session.create_data_breakpoint(
context,
data_id.clone(),
dap::DataBreakpoint {
data_id,
access_type: None,
condition: None,
hit_condition: None,
},
cx,
);
cx.notify();
},
);
self.open_context_menu = Some((context_menu, position, subscription));
});
})
.detach();
}
fn copy_variable_name(
@@ -628,10 +795,10 @@ impl VariableList {
return;
};
let variable_name = match &entry.dap_kind {
EntryKind::Variable(dap) => dap.name.clone(),
EntryKind::Watcher(watcher) => watcher.expression.to_string(),
EntryKind::Scope(_) => return,
let variable_name = match &entry.entry {
DapEntry::Variable(dap) => dap.name.clone(),
DapEntry::Watcher(watcher) => watcher.expression.to_string(),
DapEntry::Scope(_) => return,
};
cx.write_to_clipboard(ClipboardItem::new_string(variable_name));
@@ -651,10 +818,10 @@ impl VariableList {
return;
};
let variable_value = match &entry.dap_kind {
EntryKind::Variable(dap) => dap.value.clone(),
EntryKind::Watcher(watcher) => watcher.value.to_string(),
EntryKind::Scope(_) => return,
let variable_value = match &entry.entry {
DapEntry::Variable(dap) => dap.value.clone(),
DapEntry::Watcher(watcher) => watcher.value.to_string(),
DapEntry::Scope(_) => return,
};
cx.write_to_clipboard(ClipboardItem::new_string(variable_value));
@@ -669,10 +836,10 @@ impl VariableList {
return;
};
let variable_value = match &entry.dap_kind {
EntryKind::Watcher(watcher) => watcher.value.to_string(),
EntryKind::Variable(variable) => variable.value.clone(),
EntryKind::Scope(_) => return,
let variable_value = match &entry.entry {
DapEntry::Watcher(watcher) => watcher.value.to_string(),
DapEntry::Variable(variable) => variable.value.clone(),
DapEntry::Scope(_) => return,
};
let editor = Self::create_variable_editor(&variable_value, window, cx);
@@ -753,7 +920,7 @@ impl VariableList {
"{}{} {}{}",
INDENT.repeat(state.depth - 1),
if state.is_expanded { "v" } else { ">" },
entry.dap_kind.name(),
entry.entry.name(),
if self.selection.as_ref() == Some(&entry.path) {
" <=== selected"
} else {
@@ -770,8 +937,8 @@ impl VariableList {
pub(crate) fn scopes(&self) -> Vec<dap::Scope> {
self.entries
.iter()
.filter_map(|entry| match &entry.dap_kind {
EntryKind::Scope(scope) => Some(scope),
.filter_map(|entry| match &entry.entry {
DapEntry::Scope(scope) => Some(scope),
_ => None,
})
.cloned()
@@ -785,10 +952,10 @@ impl VariableList {
let mut idx = 0;
for entry in self.entries.iter() {
match &entry.dap_kind {
EntryKind::Watcher { .. } => continue,
EntryKind::Variable(dap) => scopes[idx].1.push(dap.clone()),
EntryKind::Scope(scope) => {
match &entry.entry {
DapEntry::Watcher { .. } => continue,
DapEntry::Variable(dap) => scopes[idx].1.push(dap.clone()),
DapEntry::Scope(scope) => {
if scopes.len() > 0 {
idx += 1;
}
@@ -806,8 +973,8 @@ impl VariableList {
pub(crate) fn variables(&self) -> Vec<dap::Variable> {
self.entries
.iter()
.filter_map(|entry| match &entry.dap_kind {
EntryKind::Variable(variable) => Some(variable),
.filter_map(|entry| match &entry.entry {
DapEntry::Variable(variable) => Some(variable),
_ => None,
})
.cloned()
@@ -1358,6 +1525,8 @@ impl Render for VariableList {
.on_action(cx.listener(Self::edit_variable))
.on_action(cx.listener(Self::add_watcher))
.on_action(cx.listener(Self::remove_watcher))
.on_action(cx.listener(Self::toggle_data_breakpoint))
.on_action(cx.listener(Self::jump_to_variable_memory))
.child(
uniform_list(
"variable-list",

View File

@@ -427,7 +427,7 @@ 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()
debug_panel.read(cx).sessions().collect::<Vec<_>>()
})
.unwrap();
assert_eq!(sessions.len(), 1);
@@ -451,7 +451,7 @@ async fn test_handle_start_debugging_request(
.unwrap()
.read(cx)
.session(cx);
let current_sessions = debug_panel.read(cx).sessions();
let current_sessions = debug_panel.read(cx).sessions().collect::<Vec<_>>();
assert_eq!(active_session, current_sessions[1].read(cx).session(cx));
assert_eq!(
active_session.read(cx).parent_session(),
@@ -1796,7 +1796,7 @@ async fn test_debug_adapters_shutdown_on_app_quit(
let panel = workspace.panel::<DebugPanel>(cx).unwrap();
panel.read_with(cx, |panel, _| {
assert!(
!panel.sessions().is_empty(),
panel.sessions().next().is_some(),
"Debug session should be active"
);
});

View File

@@ -2241,3 +2241,34 @@ func main() {
)
.await;
}
#[gpui::test]
async fn test_trim_multi_line_inline_value(executor: BackgroundExecutor, cx: &mut TestAppContext) {
let variables = [("y", "hello\n world")];
let before = r#"
fn main() {
let y = "hello\n world";
}
"#
.unindent();
let after = r#"
fn main() {
let y: hello… = "hello\n world";
}
"#
.unindent();
test_inline_values_util(
&variables,
&[],
&before,
&after,
None,
rust_lang(),
executor,
cx,
)
.await;
}

View File

@@ -111,7 +111,6 @@ async fn test_module_list(executor: BackgroundExecutor, cx: &mut TestAppContext)
});
running_state.update_in(cx, |this, window, cx| {
this.ensure_pane_item(DebuggerPaneItem::Modules, window, cx);
this.activate_item(DebuggerPaneItem::Modules, window, cx);
cx.refresh_windows();
});

View File

@@ -144,7 +144,6 @@ impl editor::DiagnosticRenderer for DiagnosticRenderer {
style: BlockStyle::Flex,
render: Arc::new(move |bcx| block.render_block(editor.clone(), bcx)),
priority: 1,
render_in_minimap: false,
}
})
.collect()

View File

@@ -80,6 +80,7 @@ pub(crate) struct ProjectDiagnosticsEditor {
include_warnings: bool,
update_excerpts_task: Option<Task<Result<()>>>,
cargo_diagnostics_fetch: CargoDiagnosticsFetchState,
diagnostic_summary_update: Task<()>,
_subscription: Subscription,
}
@@ -179,7 +180,16 @@ impl ProjectDiagnosticsEditor {
path,
} => {
this.paths_to_update.insert(path.clone());
this.summary = project.read(cx).diagnostic_summary(false, cx);
let project = project.clone();
this.diagnostic_summary_update = cx.spawn(async move |this, cx| {
cx.background_executor()
.timer(Duration::from_millis(30))
.await;
this.update(cx, |this, cx| {
this.summary = project.read(cx).diagnostic_summary(false, cx);
})
.log_err();
});
cx.emit(EditorEvent::TitleChanged);
if this.editor.focus_handle(cx).contains_focused(window, cx) || this.focus_handle.contains_focused(window, cx) {
@@ -276,6 +286,7 @@ impl ProjectDiagnosticsEditor {
cancel_task: None,
diagnostic_sources: Arc::new(Vec::new()),
},
diagnostic_summary_update: Task::ready(()),
_subscription: project_event_subscription,
};
this.update_all_diagnostics(true, window, cx);
@@ -656,7 +667,6 @@ impl ProjectDiagnosticsEditor {
block.render_block(editor.clone(), bcx)
}),
priority: 1,
render_in_minimap: false,
}
});
let block_ids = this.editor.update(cx, |editor, cx| {

View File

@@ -14,7 +14,10 @@ use indoc::indoc;
use language::{DiagnosticSourceKind, Rope};
use lsp::LanguageServerId;
use pretty_assertions::assert_eq;
use project::FakeFs;
use project::{
FakeFs,
project_settings::{GoToDiagnosticSeverity, GoToDiagnosticSeverityFilter},
};
use rand::{Rng, rngs::StdRng, seq::IteratorRandom as _};
use serde_json::json;
use settings::SettingsStore;
@@ -1005,7 +1008,7 @@ async fn active_diagnostics_dismiss_after_invalidation(cx: &mut TestAppContext)
cx.run_until_parked();
cx.update_editor(|editor, window, cx| {
editor.go_to_diagnostic(&GoToDiagnostic, window, cx);
editor.go_to_diagnostic(&GoToDiagnostic::default(), window, cx);
assert_eq!(
editor
.active_diagnostic_group()
@@ -1047,7 +1050,7 @@ async fn active_diagnostics_dismiss_after_invalidation(cx: &mut TestAppContext)
"});
cx.update_editor(|editor, window, cx| {
editor.go_to_diagnostic(&GoToDiagnostic, window, cx);
editor.go_to_diagnostic(&GoToDiagnostic::default(), window, cx);
assert_eq!(editor.active_diagnostic_group(), None);
});
cx.assert_editor_state(indoc! {"
@@ -1126,7 +1129,7 @@ async fn cycle_through_same_place_diagnostics(cx: &mut TestAppContext) {
// Fourth diagnostic
cx.update_editor(|editor, window, cx| {
editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
});
cx.assert_editor_state(indoc! {"
fn func(abc def: i32) -> ˇu32 {
@@ -1135,7 +1138,7 @@ async fn cycle_through_same_place_diagnostics(cx: &mut TestAppContext) {
// Third diagnostic
cx.update_editor(|editor, window, cx| {
editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
});
cx.assert_editor_state(indoc! {"
fn func(abc ˇdef: i32) -> u32 {
@@ -1144,7 +1147,7 @@ async fn cycle_through_same_place_diagnostics(cx: &mut TestAppContext) {
// Second diagnostic, same place
cx.update_editor(|editor, window, cx| {
editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
});
cx.assert_editor_state(indoc! {"
fn func(abc ˇdef: i32) -> u32 {
@@ -1153,7 +1156,7 @@ async fn cycle_through_same_place_diagnostics(cx: &mut TestAppContext) {
// First diagnostic
cx.update_editor(|editor, window, cx| {
editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
});
cx.assert_editor_state(indoc! {"
fn func(abcˇ def: i32) -> u32 {
@@ -1162,7 +1165,7 @@ async fn cycle_through_same_place_diagnostics(cx: &mut TestAppContext) {
// Wrapped over, fourth diagnostic
cx.update_editor(|editor, window, cx| {
editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
});
cx.assert_editor_state(indoc! {"
fn func(abc def: i32) -> ˇu32 {
@@ -1181,7 +1184,7 @@ async fn cycle_through_same_place_diagnostics(cx: &mut TestAppContext) {
// First diagnostic
cx.update_editor(|editor, window, cx| {
editor.go_to_diagnostic(&GoToDiagnostic, window, cx);
editor.go_to_diagnostic(&GoToDiagnostic::default(), window, cx);
});
cx.assert_editor_state(indoc! {"
fn func(abcˇ def: i32) -> u32 {
@@ -1190,7 +1193,7 @@ async fn cycle_through_same_place_diagnostics(cx: &mut TestAppContext) {
// Second diagnostic
cx.update_editor(|editor, window, cx| {
editor.go_to_diagnostic(&GoToDiagnostic, window, cx);
editor.go_to_diagnostic(&GoToDiagnostic::default(), window, cx);
});
cx.assert_editor_state(indoc! {"
fn func(abc ˇdef: i32) -> u32 {
@@ -1199,7 +1202,7 @@ async fn cycle_through_same_place_diagnostics(cx: &mut TestAppContext) {
// Third diagnostic, same place
cx.update_editor(|editor, window, cx| {
editor.go_to_diagnostic(&GoToDiagnostic, window, cx);
editor.go_to_diagnostic(&GoToDiagnostic::default(), window, cx);
});
cx.assert_editor_state(indoc! {"
fn func(abc ˇdef: i32) -> u32 {
@@ -1208,7 +1211,7 @@ async fn cycle_through_same_place_diagnostics(cx: &mut TestAppContext) {
// Fourth diagnostic
cx.update_editor(|editor, window, cx| {
editor.go_to_diagnostic(&GoToDiagnostic, window, cx);
editor.go_to_diagnostic(&GoToDiagnostic::default(), window, cx);
});
cx.assert_editor_state(indoc! {"
fn func(abc def: i32) -> ˇu32 {
@@ -1217,7 +1220,7 @@ async fn cycle_through_same_place_diagnostics(cx: &mut TestAppContext) {
// Wrapped around, first diagnostic
cx.update_editor(|editor, window, cx| {
editor.go_to_diagnostic(&GoToDiagnostic, window, cx);
editor.go_to_diagnostic(&GoToDiagnostic::default(), window, cx);
});
cx.assert_editor_state(indoc! {"
fn func(abcˇ def: i32) -> u32 {
@@ -1441,6 +1444,128 @@ async fn test_diagnostics_with_code(cx: &mut TestAppContext) {
);
}
#[gpui::test]
async fn go_to_diagnostic_with_severity(cx: &mut TestAppContext) {
init_test(cx);
let mut cx = EditorTestContext::new(cx).await;
let lsp_store =
cx.update_editor(|editor, _, cx| editor.project.as_ref().unwrap().read(cx).lsp_store());
cx.set_state(indoc! {"error warning info hiˇnt"});
cx.update(|_, cx| {
lsp_store.update(cx, |lsp_store, cx| {
lsp_store
.update_diagnostics(
LanguageServerId(0),
lsp::PublishDiagnosticsParams {
uri: lsp::Url::from_file_path(path!("/root/file")).unwrap(),
version: None,
diagnostics: vec![
lsp::Diagnostic {
range: lsp::Range::new(
lsp::Position::new(0, 0),
lsp::Position::new(0, 5),
),
severity: Some(lsp::DiagnosticSeverity::ERROR),
..Default::default()
},
lsp::Diagnostic {
range: lsp::Range::new(
lsp::Position::new(0, 6),
lsp::Position::new(0, 13),
),
severity: Some(lsp::DiagnosticSeverity::WARNING),
..Default::default()
},
lsp::Diagnostic {
range: lsp::Range::new(
lsp::Position::new(0, 14),
lsp::Position::new(0, 18),
),
severity: Some(lsp::DiagnosticSeverity::INFORMATION),
..Default::default()
},
lsp::Diagnostic {
range: lsp::Range::new(
lsp::Position::new(0, 19),
lsp::Position::new(0, 23),
),
severity: Some(lsp::DiagnosticSeverity::HINT),
..Default::default()
},
],
},
None,
DiagnosticSourceKind::Pushed,
&[],
cx,
)
.unwrap()
});
});
cx.run_until_parked();
macro_rules! go {
($severity:expr) => {
cx.update_editor(|editor, window, cx| {
editor.go_to_diagnostic(
&GoToDiagnostic {
severity: $severity,
},
window,
cx,
);
});
};
}
// Default, should cycle through all diagnostics
go!(GoToDiagnosticSeverityFilter::default());
cx.assert_editor_state(indoc! {"ˇerror warning info hint"});
go!(GoToDiagnosticSeverityFilter::default());
cx.assert_editor_state(indoc! {"error ˇwarning info hint"});
go!(GoToDiagnosticSeverityFilter::default());
cx.assert_editor_state(indoc! {"error warning ˇinfo hint"});
go!(GoToDiagnosticSeverityFilter::default());
cx.assert_editor_state(indoc! {"error warning info ˇhint"});
go!(GoToDiagnosticSeverityFilter::default());
cx.assert_editor_state(indoc! {"ˇerror warning info hint"});
let only_info = GoToDiagnosticSeverityFilter::Only(GoToDiagnosticSeverity::Information);
go!(only_info);
cx.assert_editor_state(indoc! {"error warning ˇinfo hint"});
go!(only_info);
cx.assert_editor_state(indoc! {"error warning ˇinfo hint"});
let no_hints = GoToDiagnosticSeverityFilter::Range {
min: GoToDiagnosticSeverity::Information,
max: GoToDiagnosticSeverity::Error,
};
go!(no_hints);
cx.assert_editor_state(indoc! {"ˇerror warning info hint"});
go!(no_hints);
cx.assert_editor_state(indoc! {"error ˇwarning info hint"});
go!(no_hints);
cx.assert_editor_state(indoc! {"error warning ˇinfo hint"});
go!(no_hints);
cx.assert_editor_state(indoc! {"ˇerror warning info hint"});
let warning_info = GoToDiagnosticSeverityFilter::Range {
min: GoToDiagnosticSeverity::Information,
max: GoToDiagnosticSeverity::Warning,
};
go!(warning_info);
cx.assert_editor_state(indoc! {"error ˇwarning info hint"});
go!(warning_info);
cx.assert_editor_state(indoc! {"error warning ˇinfo hint"});
go!(warning_info);
cx.assert_editor_state(indoc! {"error ˇwarning info hint"});
}
fn init_test(cx: &mut TestAppContext) {
cx.update(|cx| {
zlog::init_test();

View File

@@ -6,9 +6,10 @@ use gpui::{
WeakEntity, Window,
};
use language::Diagnostic;
use project::project_settings::ProjectSettings;
use project::project_settings::{GoToDiagnosticSeverityFilter, ProjectSettings};
use settings::Settings;
use ui::{Button, ButtonLike, Color, Icon, IconName, Label, Tooltip, h_flex, prelude::*};
use util::ResultExt;
use workspace::{StatusItemView, ToolbarItemEvent, Workspace, item::ItemHandle};
use crate::{Deploy, IncludeWarnings, ProjectDiagnosticsEditor};
@@ -20,6 +21,7 @@ pub struct DiagnosticIndicator {
current_diagnostic: Option<Diagnostic>,
_observe_active_editor: Option<Subscription>,
diagnostics_update: Task<()>,
diagnostic_summary_update: Task<()>,
}
impl Render for DiagnosticIndicator {
@@ -77,7 +79,7 @@ impl Render for DiagnosticIndicator {
.tooltip(|window, cx| {
Tooltip::for_action(
"Next Diagnostic",
&editor::actions::GoToDiagnostic,
&editor::actions::GoToDiagnostic::default(),
window,
cx,
)
@@ -135,8 +137,16 @@ impl DiagnosticIndicator {
}
project::Event::DiagnosticsUpdated { .. } => {
this.summary = project.read(cx).diagnostic_summary(false, cx);
cx.notify();
this.diagnostic_summary_update = cx.spawn(async move |this, cx| {
cx.background_executor()
.timer(Duration::from_millis(30))
.await;
this.update(cx, |this, cx| {
this.summary = project.read(cx).diagnostic_summary(false, cx);
cx.notify();
})
.log_err();
});
}
_ => {}
@@ -150,13 +160,19 @@ impl DiagnosticIndicator {
current_diagnostic: None,
_observe_active_editor: None,
diagnostics_update: Task::ready(()),
diagnostic_summary_update: Task::ready(()),
}
}
fn go_to_next_diagnostic(&mut self, window: &mut Window, cx: &mut Context<Self>) {
if let Some(editor) = self.active_editor.as_ref().and_then(|e| e.upgrade()) {
editor.update(cx, |editor, cx| {
editor.go_to_diagnostic_impl(editor::Direction::Next, window, cx);
editor.go_to_diagnostic_impl(
editor::Direction::Next,
GoToDiagnosticSeverityFilter::default(),
window,
cx,
);
})
}
}

View File

@@ -243,7 +243,6 @@ struct ActionDef {
fn dump_all_gpui_actions() -> Vec<ActionDef> {
let mut actions = gpui::generate_list_of_all_registered_actions()
.into_iter()
.map(|action| ActionDef {
name: action.name,
human_name: command_palette::humanize_action_name(action.name),

View File

@@ -1,6 +1,7 @@
//! This module contains all actions supported by [`Editor`].
use super::*;
use gpui::{Action, actions};
use project::project_settings::GoToDiagnosticSeverityFilter;
use schemars::JsonSchema;
use util::serde::default_true;
@@ -265,6 +266,24 @@ pub enum UuidVersion {
V7,
}
/// Goes to the next diagnostic in the file.
#[derive(PartialEq, Clone, Default, Debug, Deserialize, JsonSchema, Action)]
#[action(namespace = editor)]
#[serde(deny_unknown_fields)]
pub struct GoToDiagnostic {
#[serde(default)]
pub severity: GoToDiagnosticSeverityFilter,
}
/// Goes to the previous diagnostic in the file.
#[derive(PartialEq, Clone, Default, Debug, Deserialize, JsonSchema, Action)]
#[action(namespace = editor)]
#[serde(deny_unknown_fields)]
pub struct GoToPreviousDiagnostic {
#[serde(default)]
pub severity: GoToDiagnosticSeverityFilter,
}
actions!(
debugger,
[
@@ -406,10 +425,14 @@ actions!(
FoldRecursive,
/// Folds the selected ranges.
FoldSelectedRanges,
/// Toggles focus back to the last active buffer.
ToggleFocus,
/// Toggles folding at the current position.
ToggleFold,
/// Toggles recursive folding at the current position.
ToggleFoldRecursive,
/// Toggles all folds in a buffer or all excerpts in multibuffer.
ToggleFoldAll,
/// Formats the entire document.
Format,
/// Formats only the selected text.
@@ -422,8 +445,6 @@ actions!(
GoToDefinition,
/// Goes to definition in a split pane.
GoToDefinitionSplit,
/// Goes to the next diagnostic in the file.
GoToDiagnostic,
/// Goes to the next diff hunk.
GoToHunk,
/// Goes to the previous diff hunk.
@@ -438,8 +459,6 @@ actions!(
GoToParentModule,
/// Goes to the previous change in the file.
GoToPreviousChange,
/// Goes to the previous diagnostic in the file.
GoToPreviousDiagnostic,
/// Goes to the type definition of the symbol at cursor.
GoToTypeDefinition,
/// Goes to type definition in a split pane.

View File

@@ -1392,7 +1392,6 @@ impl CodeActionsMenu {
) -> AnyElement {
let actions = self.actions.clone();
let selected_item = self.selected_item;
let list = uniform_list(
"code_actions_menu",
self.actions.len(),
@@ -1439,30 +1438,6 @@ impl CodeActionsMenu {
.overflow_hidden()
.child("debug: ")
.child(scenario.label.clone())
.child(
IconButton::new(
SharedString::new(format!("edit-{ix}")),
IconName::Pencil,
)
.on_click(cx.listener({
let scenario = scenario.clone();
move |editor, _, _window, cx| {
if let Some((workspace, Some(id))) =
editor.workspace.as_ref()
{
workspace
.update(cx, |_, cx| {
cx.emit(workspace::OpenInDebugJson {
scenario: scenario.clone(),
id: *id,
});
})
.ok();
}
cx.stop_propagation();
}
})),
)
.when(selected, |this| {
this.text_color(colors.text_accent)
}),

View File

@@ -271,7 +271,6 @@ impl DisplayMap {
height: Some(height),
style,
priority,
render_in_minimap: true,
}
}),
);
@@ -1663,7 +1662,6 @@ pub mod tests {
height: Some(height),
render: Arc::new(|_| div().into_any()),
priority,
render_in_minimap: true,
}
})
.collect::<Vec<_>>();
@@ -2029,7 +2027,6 @@ pub mod tests {
style: BlockStyle::Sticky,
render: Arc::new(|_| div().into_any()),
priority: 0,
render_in_minimap: true,
}],
cx,
);
@@ -2227,7 +2224,6 @@ pub mod tests {
style: BlockStyle::Sticky,
render: Arc::new(|_| div().into_any()),
priority: 0,
render_in_minimap: true,
},
BlockProperties {
placement: BlockPlacement::Below(
@@ -2237,7 +2233,6 @@ pub mod tests {
style: BlockStyle::Sticky,
render: Arc::new(|_| div().into_any()),
priority: 0,
render_in_minimap: true,
},
],
cx,
@@ -2344,7 +2339,6 @@ pub mod tests {
style: BlockStyle::Sticky,
render: Arc::new(|_| div().into_any()),
priority: 0,
render_in_minimap: true,
}],
cx,
)
@@ -2420,7 +2414,6 @@ pub mod tests {
style: BlockStyle::Fixed,
render: Arc::new(|_| div().into_any()),
priority: 0,
render_in_minimap: true,
}],
cx,
);

View File

@@ -193,7 +193,6 @@ pub struct CustomBlock {
style: BlockStyle,
render: Arc<Mutex<RenderBlock>>,
priority: usize,
pub(crate) render_in_minimap: bool,
}
#[derive(Clone)]
@@ -205,7 +204,6 @@ pub struct BlockProperties<P> {
pub style: BlockStyle,
pub render: RenderBlock,
pub priority: usize,
pub render_in_minimap: bool,
}
impl<P: Debug> Debug for BlockProperties<P> {
@@ -1044,7 +1042,6 @@ impl BlockMapWriter<'_> {
render: Arc::new(Mutex::new(block.render)),
style: block.style,
priority: block.priority,
render_in_minimap: block.render_in_minimap,
});
self.0.custom_blocks.insert(block_ix, new_block.clone());
self.0.custom_blocks_by_id.insert(id, new_block);
@@ -1079,7 +1076,6 @@ impl BlockMapWriter<'_> {
style: block.style,
render: block.render.clone(),
priority: block.priority,
render_in_minimap: block.render_in_minimap,
};
let new_block = Arc::new(new_block);
*block = new_block.clone();
@@ -1976,7 +1972,6 @@ mod tests {
height: Some(1),
render: Arc::new(|_| div().into_any()),
priority: 0,
render_in_minimap: true,
},
BlockProperties {
style: BlockStyle::Fixed,
@@ -1984,7 +1979,6 @@ mod tests {
height: Some(2),
render: Arc::new(|_| div().into_any()),
priority: 0,
render_in_minimap: true,
},
BlockProperties {
style: BlockStyle::Fixed,
@@ -1992,7 +1986,6 @@ mod tests {
height: Some(3),
render: Arc::new(|_| div().into_any()),
priority: 0,
render_in_minimap: true,
},
]);
@@ -2217,7 +2210,6 @@ mod tests {
height: Some(1),
render: Arc::new(|_| div().into_any()),
priority: 0,
render_in_minimap: true,
},
BlockProperties {
style: BlockStyle::Fixed,
@@ -2225,7 +2217,6 @@ mod tests {
height: Some(2),
render: Arc::new(|_| div().into_any()),
priority: 0,
render_in_minimap: true,
},
BlockProperties {
style: BlockStyle::Fixed,
@@ -2233,7 +2224,6 @@ mod tests {
height: Some(3),
render: Arc::new(|_| div().into_any()),
priority: 0,
render_in_minimap: true,
},
]);
@@ -2322,7 +2312,6 @@ mod tests {
render: Arc::new(|_| div().into_any()),
height: Some(1),
priority: 0,
render_in_minimap: true,
},
BlockProperties {
style: BlockStyle::Fixed,
@@ -2330,7 +2319,6 @@ mod tests {
render: Arc::new(|_| div().into_any()),
height: Some(1),
priority: 0,
render_in_minimap: true,
},
]);
@@ -2370,7 +2358,6 @@ mod tests {
height: Some(4),
render: Arc::new(|_| div().into_any()),
priority: 0,
render_in_minimap: true,
}])[0];
let blocks_snapshot = block_map.read(wraps_snapshot, Default::default());
@@ -2424,7 +2411,6 @@ mod tests {
height: Some(1),
render: Arc::new(|_| div().into_any()),
priority: 0,
render_in_minimap: true,
},
BlockProperties {
style: BlockStyle::Fixed,
@@ -2432,7 +2418,6 @@ mod tests {
height: Some(1),
render: Arc::new(|_| div().into_any()),
priority: 0,
render_in_minimap: true,
},
BlockProperties {
style: BlockStyle::Fixed,
@@ -2440,7 +2425,6 @@ mod tests {
height: Some(1),
render: Arc::new(|_| div().into_any()),
priority: 0,
render_in_minimap: true,
},
]);
let blocks_snapshot = block_map.read(wraps_snapshot.clone(), Default::default());
@@ -2455,7 +2439,6 @@ mod tests {
height: Some(1),
render: Arc::new(|_| div().into_any()),
priority: 0,
render_in_minimap: true,
},
BlockProperties {
style: BlockStyle::Fixed,
@@ -2463,7 +2446,6 @@ mod tests {
height: Some(1),
render: Arc::new(|_| div().into_any()),
priority: 0,
render_in_minimap: true,
},
BlockProperties {
style: BlockStyle::Fixed,
@@ -2471,7 +2453,6 @@ mod tests {
height: Some(1),
render: Arc::new(|_| div().into_any()),
priority: 0,
render_in_minimap: true,
},
]);
let blocks_snapshot = block_map.read(wraps_snapshot.clone(), Default::default());
@@ -2571,7 +2552,6 @@ mod tests {
height: Some(1),
render: Arc::new(|_| div().into_any()),
priority: 0,
render_in_minimap: true,
},
BlockProperties {
style: BlockStyle::Fixed,
@@ -2579,7 +2559,6 @@ mod tests {
height: Some(1),
render: Arc::new(|_| div().into_any()),
priority: 0,
render_in_minimap: true,
},
BlockProperties {
style: BlockStyle::Fixed,
@@ -2587,7 +2566,6 @@ mod tests {
height: Some(1),
render: Arc::new(|_| div().into_any()),
priority: 0,
render_in_minimap: true,
},
]);
let excerpt_blocks_3 = writer.insert(vec![
@@ -2597,7 +2575,6 @@ mod tests {
height: Some(1),
render: Arc::new(|_| div().into_any()),
priority: 0,
render_in_minimap: true,
},
BlockProperties {
style: BlockStyle::Fixed,
@@ -2605,7 +2582,6 @@ mod tests {
height: Some(1),
render: Arc::new(|_| div().into_any()),
priority: 0,
render_in_minimap: true,
},
]);
@@ -2653,7 +2629,6 @@ mod tests {
height: Some(1),
render: Arc::new(|_| div().into_any()),
priority: 0,
render_in_minimap: true,
}]);
let blocks_snapshot = block_map.read(wrap_snapshot.clone(), Patch::default());
let blocks = blocks_snapshot
@@ -3011,7 +2986,6 @@ mod tests {
height: Some(height),
render: Arc::new(|_| div().into_any()),
priority: 0,
render_in_minimap: true,
}
})
.collect::<Vec<_>>();
@@ -3032,7 +3006,6 @@ mod tests {
style: props.style,
render: Arc::new(|_| div().into_any()),
priority: 0,
render_in_minimap: true,
}));
for (block_properties, block_id) in block_properties.iter().zip(block_ids) {
@@ -3557,7 +3530,6 @@ mod tests {
height: Some(1),
render: Arc::new(|_| div().into_any()),
priority: 0,
render_in_minimap: true,
}])[0];
let blocks_snapshot = block_map.read(wraps_snapshot.clone(), Default::default());

View File

@@ -134,7 +134,7 @@ use project::{
session::{Session, SessionEvent},
},
git_store::{GitStoreEvent, RepositoryEvent},
project_settings::DiagnosticSeverity,
project_settings::{DiagnosticSeverity, GoToDiagnosticSeverityFilter},
};
pub use git::blame::BlameRenderer;
@@ -356,6 +356,7 @@ pub fn init(cx: &mut App) {
workspace.register_action(Editor::new_file_vertical);
workspace.register_action(Editor::new_file_horizontal);
workspace.register_action(Editor::cancel_language_server_work);
workspace.register_action(Editor::toggle_focus);
},
)
.detach();
@@ -482,9 +483,7 @@ pub enum SelectMode {
#[derive(Clone, PartialEq, Eq, Debug)]
pub enum EditorMode {
SingleLine {
auto_width: bool,
},
SingleLine,
AutoHeight {
min_lines: usize,
max_lines: Option<usize>,
@@ -1662,13 +1661,7 @@ impl Editor {
pub fn single_line(window: &mut Window, cx: &mut Context<Self>) -> Self {
let buffer = cx.new(|cx| Buffer::local("", cx));
let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
Self::new(
EditorMode::SingleLine { auto_width: false },
buffer,
None,
window,
cx,
)
Self::new(EditorMode::SingleLine, buffer, None, window, cx)
}
pub fn multi_line(window: &mut Window, cx: &mut Context<Self>) -> Self {
@@ -1677,18 +1670,6 @@ impl Editor {
Self::new(EditorMode::full(), buffer, None, window, cx)
}
pub fn auto_width(window: &mut Window, cx: &mut Context<Self>) -> Self {
let buffer = cx.new(|cx| Buffer::local("", cx));
let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
Self::new(
EditorMode::SingleLine { auto_width: true },
buffer,
None,
window,
cx,
)
}
pub fn auto_height(
min_lines: usize,
max_lines: usize,
@@ -1795,6 +1776,7 @@ impl Editor {
);
let full_mode = mode.is_full();
let is_minimap = mode.is_minimap();
let diagnostics_max_severity = if full_mode {
EditorSettings::get_global(cx)
.diagnostics_max_severity
@@ -1855,13 +1837,19 @@ impl Editor {
let selections = SelectionsCollection::new(display_map.clone(), buffer.clone());
let blink_manager = cx.new(|cx| BlinkManager::new(CURSOR_BLINK_INTERVAL, cx));
let blink_manager = cx.new(|cx| {
let mut blink_manager = BlinkManager::new(CURSOR_BLINK_INTERVAL, cx);
if is_minimap {
blink_manager.disable(cx);
}
blink_manager
});
let soft_wrap_mode_override = matches!(mode, EditorMode::SingleLine { .. })
.then(|| language_settings::SoftWrap::None);
let mut project_subscriptions = Vec::new();
if mode.is_full() {
if full_mode {
if let Some(project) = project.as_ref() {
project_subscriptions.push(cx.subscribe_in(
project,
@@ -1972,18 +1960,23 @@ impl Editor {
let inlay_hint_settings =
inlay_hint_settings(selections.newest_anchor().head(), &buffer_snapshot, cx);
let focus_handle = cx.focus_handle();
cx.on_focus(&focus_handle, window, Self::handle_focus)
.detach();
cx.on_focus_in(&focus_handle, window, Self::handle_focus_in)
.detach();
cx.on_focus_out(&focus_handle, window, Self::handle_focus_out)
.detach();
cx.on_blur(&focus_handle, window, Self::handle_blur)
.detach();
cx.observe_pending_input(window, Self::observe_pending_input)
.detach();
if !is_minimap {
cx.on_focus(&focus_handle, window, Self::handle_focus)
.detach();
cx.on_focus_in(&focus_handle, window, Self::handle_focus_in)
.detach();
cx.on_focus_out(&focus_handle, window, Self::handle_focus_out)
.detach();
cx.on_blur(&focus_handle, window, Self::handle_blur)
.detach();
cx.observe_pending_input(window, Self::observe_pending_input)
.detach();
}
let show_indent_guides = if matches!(mode, EditorMode::SingleLine { .. }) {
let show_indent_guides = if matches!(
mode,
EditorMode::SingleLine { .. } | EditorMode::Minimap { .. }
) {
Some(false)
} else {
None
@@ -2049,10 +2042,10 @@ impl Editor {
minimap_visibility: MinimapVisibility::for_mode(&mode, cx),
offset_content: !matches!(mode, EditorMode::SingleLine { .. }),
show_breadcrumbs: EditorSettings::get_global(cx).toolbar.breadcrumbs,
show_gutter: mode.is_full(),
show_line_numbers: None,
show_gutter: full_mode,
show_line_numbers: (!full_mode).then_some(false),
use_relative_line_numbers: None,
disable_expand_excerpt_buttons: false,
disable_expand_excerpt_buttons: !full_mode,
show_git_diff_gutter: None,
show_code_actions: None,
show_runnables: None,
@@ -2086,7 +2079,7 @@ impl Editor {
document_highlights_task: None,
linked_editing_range_task: None,
pending_rename: None,
searchable: true,
searchable: !is_minimap,
cursor_shape: EditorSettings::get_global(cx)
.cursor_shape
.unwrap_or_default(),
@@ -2094,9 +2087,9 @@ impl Editor {
autoindent_mode: Some(AutoindentMode::EachLine),
collapse_matches: false,
workspace: None,
input_enabled: true,
use_modal_editing: mode.is_full(),
read_only: mode.is_minimap(),
input_enabled: !is_minimap,
use_modal_editing: full_mode,
read_only: is_minimap,
use_autoclose: true,
use_auto_surround: true,
auto_replace_emoji_shortcode: false,
@@ -2112,11 +2105,10 @@ impl Editor {
edit_prediction_preview: EditPredictionPreview::Inactive {
released_too_fast: false,
},
inline_diagnostics_enabled: mode.is_full(),
diagnostics_enabled: mode.is_full(),
inline_diagnostics_enabled: full_mode,
diagnostics_enabled: full_mode,
inline_value_cache: InlineValueCache::new(inlay_hint_settings.show_value_hints),
inlay_hint_cache: InlayHintCache::new(inlay_hint_settings),
gutter_hovered: false,
pixel_position_of_newest_cursor: None,
last_bounds: None,
@@ -2139,9 +2131,10 @@ impl Editor {
show_git_blame_inline: false,
show_selection_menu: None,
show_git_blame_inline_delay_task: None,
git_blame_inline_enabled: ProjectSettings::get_global(cx).git.inline_blame_enabled(),
git_blame_inline_enabled: full_mode
&& ProjectSettings::get_global(cx).git.inline_blame_enabled(),
render_diff_hunk_controls: Arc::new(render_diff_hunk_controls),
serialize_dirty_buffers: !mode.is_minimap()
serialize_dirty_buffers: !is_minimap
&& ProjectSettings::get_global(cx)
.session
.restore_unsaved_buffers,
@@ -2152,27 +2145,31 @@ impl Editor {
breakpoint_store,
gutter_breakpoint_indicator: (None, None),
hovered_diff_hunk_row: None,
_subscriptions: vec![
cx.observe(&buffer, Self::on_buffer_changed),
cx.subscribe_in(&buffer, window, Self::on_buffer_event),
cx.observe_in(&display_map, window, Self::on_display_map_changed),
cx.observe(&blink_manager, |_, _, cx| cx.notify()),
cx.observe_global_in::<SettingsStore>(window, Self::settings_changed),
observe_buffer_font_size_adjustment(cx, |_, cx| cx.notify()),
cx.observe_window_activation(window, |editor, window, cx| {
let active = window.is_window_active();
editor.blink_manager.update(cx, |blink_manager, cx| {
if active {
blink_manager.enable(cx);
} else {
blink_manager.disable(cx);
}
});
if active {
editor.show_mouse_cursor(cx);
}
}),
],
_subscriptions: (!is_minimap)
.then(|| {
vec![
cx.observe(&buffer, Self::on_buffer_changed),
cx.subscribe_in(&buffer, window, Self::on_buffer_event),
cx.observe_in(&display_map, window, Self::on_display_map_changed),
cx.observe(&blink_manager, |_, _, cx| cx.notify()),
cx.observe_global_in::<SettingsStore>(window, Self::settings_changed),
observe_buffer_font_size_adjustment(cx, |_, cx| cx.notify()),
cx.observe_window_activation(window, |editor, window, cx| {
let active = window.is_window_active();
editor.blink_manager.update(cx, |blink_manager, cx| {
if active {
blink_manager.enable(cx);
} else {
blink_manager.disable(cx);
}
});
if active {
editor.show_mouse_cursor(cx);
}
}),
]
})
.unwrap_or_default(),
tasks_update_task: None,
pull_diagnostics_task: Task::ready(()),
colors: None,
@@ -2203,6 +2200,11 @@ impl Editor {
selection_drag_state: SelectionDragState::None,
folding_newlines: Task::ready(()),
};
if is_minimap {
return editor;
}
if let Some(breakpoints) = editor.breakpoint_store.as_ref() {
editor
._subscriptions
@@ -2322,7 +2324,10 @@ impl Editor {
editor.update_lsp_data(false, None, window, cx);
}
editor.report_editor_event("Editor Opened", None, cx);
if editor.mode.is_full() {
editor.report_editor_event("Editor Opened", None, cx);
}
editor
}
@@ -10442,7 +10447,6 @@ impl Editor {
cloned_prompt.clone().into_any_element()
}),
priority: 0,
render_in_minimap: true,
}];
let focus_handle = bp_prompt.focus_handle(cx);
@@ -15063,7 +15067,7 @@ impl Editor {
pub fn go_to_diagnostic(
&mut self,
_: &GoToDiagnostic,
action: &GoToDiagnostic,
window: &mut Window,
cx: &mut Context<Self>,
) {
@@ -15071,12 +15075,12 @@ impl Editor {
return;
}
self.hide_mouse_cursor(HideMouseCursorOrigin::MovementAction, cx);
self.go_to_diagnostic_impl(Direction::Next, window, cx)
self.go_to_diagnostic_impl(Direction::Next, action.severity, window, cx)
}
pub fn go_to_prev_diagnostic(
&mut self,
_: &GoToPreviousDiagnostic,
action: &GoToPreviousDiagnostic,
window: &mut Window,
cx: &mut Context<Self>,
) {
@@ -15084,12 +15088,13 @@ impl Editor {
return;
}
self.hide_mouse_cursor(HideMouseCursorOrigin::MovementAction, cx);
self.go_to_diagnostic_impl(Direction::Prev, window, cx)
self.go_to_diagnostic_impl(Direction::Prev, action.severity, window, cx)
}
pub fn go_to_diagnostic_impl(
&mut self,
direction: Direction,
severity: GoToDiagnosticSeverityFilter,
window: &mut Window,
cx: &mut Context<Self>,
) {
@@ -15105,9 +15110,11 @@ impl Editor {
fn filtered(
snapshot: EditorSnapshot,
severity: GoToDiagnosticSeverityFilter,
diagnostics: impl Iterator<Item = DiagnosticEntry<usize>>,
) -> impl Iterator<Item = DiagnosticEntry<usize>> {
diagnostics
.filter(move |entry| severity.matches(entry.diagnostic.severity))
.filter(|entry| entry.range.start != entry.range.end)
.filter(|entry| !entry.diagnostic.is_unnecessary)
.filter(move |entry| !snapshot.intersects_fold(entry.range.start))
@@ -15116,12 +15123,14 @@ impl Editor {
let snapshot = self.snapshot(window, cx);
let before = filtered(
snapshot.clone(),
severity,
buffer
.diagnostics_in_range(0..selection.start)
.filter(|entry| entry.range.start <= selection.start),
);
let after = filtered(
snapshot,
severity,
buffer
.diagnostics_in_range(selection.start..buffer.len())
.filter(|entry| entry.range.start >= selection.start),
@@ -16138,7 +16147,6 @@ impl Editor {
}
}),
priority: 0,
render_in_minimap: true,
}],
Some(Autoscroll::fit()),
cx,
@@ -16947,6 +16955,18 @@ impl Editor {
cx.notify();
}
pub fn toggle_focus(
workspace: &mut Workspace,
_: &actions::ToggleFocus,
window: &mut Window,
cx: &mut Context<Workspace>,
) {
let Some(item) = workspace.recent_active_item_by_type::<Self>(cx) else {
return;
};
workspace.activate_item(&item, true, true, window, cx);
}
pub fn toggle_fold(
&mut self,
_: &actions::ToggleFold,
@@ -17072,6 +17092,46 @@ impl Editor {
}
}
pub fn toggle_fold_all(
&mut self,
_: &actions::ToggleFoldAll,
window: &mut Window,
cx: &mut Context<Self>,
) {
if self.buffer.read(cx).is_singleton() {
let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
let has_folds = display_map
.folds_in_range(0..display_map.buffer_snapshot.len())
.next()
.is_some();
if has_folds {
self.unfold_all(&actions::UnfoldAll, window, cx);
} else {
self.fold_all(&actions::FoldAll, window, cx);
}
} else {
let buffer_ids = self.buffer.read(cx).excerpt_buffer_ids();
let should_unfold = buffer_ids
.iter()
.any(|buffer_id| self.is_buffer_folded(*buffer_id, cx));
self.toggle_fold_multiple_buffers = cx.spawn_in(window, async move |editor, cx| {
editor
.update_in(cx, |editor, _, cx| {
for buffer_id in buffer_ids {
if should_unfold {
editor.unfold_buffer(buffer_id, cx);
} else {
editor.fold_buffer(buffer_id, cx);
}
}
})
.ok();
});
}
}
fn fold_at_level(
&mut self,
fold_at: &FoldAtLevel,
@@ -18001,7 +18061,7 @@ impl Editor {
parent: cx.weak_entity(),
},
self.buffer.clone(),
self.project.clone(),
None,
Some(self.display_map.clone()),
window,
cx,
@@ -19655,8 +19715,9 @@ impl Editor {
Anchor::in_buffer(excerpt_id, buffer_id, hint.position),
hint.text(),
);
new_inlays.push(inlay);
if !inlay.text.chars().contains(&'\n') {
new_inlays.push(inlay);
}
});
}
@@ -19884,14 +19945,12 @@ impl Editor {
}
fn settings_changed(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let new_severity = if self.diagnostics_enabled() {
EditorSettings::get_global(cx)
if self.diagnostics_enabled() {
let new_severity = EditorSettings::get_global(cx)
.diagnostics_max_severity
.unwrap_or(DiagnosticSeverity::Hint)
} else {
DiagnosticSeverity::Off
};
self.set_max_diagnostics_severity(new_severity, cx);
.unwrap_or(DiagnosticSeverity::Hint);
self.set_max_diagnostics_severity(new_severity, cx);
}
self.tasks_update_task = Some(self.refresh_runnables(window, cx));
self.update_edit_prediction_settings(cx);
self.refresh_inline_completion(true, false, window, cx);
@@ -20503,6 +20562,7 @@ impl Editor {
if event.blurred != self.focus_handle {
self.last_focused_descendant = Some(event.blurred);
}
self.selection_drag_state = SelectionDragState::None;
self.refresh_inlay_hints(InlayHintRefreshReason::ModifiersChanged(false), cx);
}

View File

@@ -5081,7 +5081,6 @@ fn test_move_line_up_down_with_blocks(cx: &mut TestAppContext) {
height: Some(1),
render: Arc::new(|_| div().into_any()),
priority: 0,
render_in_minimap: true,
}],
Some(Autoscroll::fit()),
cx,
@@ -5124,7 +5123,6 @@ async fn test_selections_and_replace_blocks(cx: &mut TestAppContext) {
style: BlockStyle::Sticky,
render: Arc::new(|_| gpui::div().into_any_element()),
priority: 0,
render_in_minimap: true,
}],
None,
cx,
@@ -14736,7 +14734,7 @@ async fn go_to_prev_overlapping_diagnostic(executor: BackgroundExecutor, cx: &mu
executor.run_until_parked();
cx.update_editor(|editor, window, cx| {
editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
});
cx.assert_editor_state(indoc! {"
@@ -14745,7 +14743,7 @@ async fn go_to_prev_overlapping_diagnostic(executor: BackgroundExecutor, cx: &mu
"});
cx.update_editor(|editor, window, cx| {
editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
});
cx.assert_editor_state(indoc! {"
@@ -14754,7 +14752,7 @@ async fn go_to_prev_overlapping_diagnostic(executor: BackgroundExecutor, cx: &mu
"});
cx.update_editor(|editor, window, cx| {
editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
});
cx.assert_editor_state(indoc! {"
@@ -14763,7 +14761,7 @@ async fn go_to_prev_overlapping_diagnostic(executor: BackgroundExecutor, cx: &mu
"});
cx.update_editor(|editor, window, cx| {
editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
});
cx.assert_editor_state(indoc! {"
@@ -21465,7 +21463,7 @@ println!("5");
.unwrap();
pane_1
.update_in(cx, |pane, window, cx| {
pane.close_inactive_items(&CloseInactiveItems::default(), window, cx)
pane.close_inactive_items(&CloseInactiveItems::default(), None, window, cx)
})
.await
.unwrap();
@@ -21501,7 +21499,7 @@ println!("5");
.unwrap();
pane_2
.update_in(cx, |pane, window, cx| {
pane.close_inactive_items(&CloseInactiveItems::default(), window, cx)
pane.close_inactive_items(&CloseInactiveItems::default(), None, window, cx)
})
.await
.unwrap();

View File

@@ -9,7 +9,7 @@ use crate::{
LineUp, MAX_LINE_LEN, MINIMAP_FONT_SIZE, MULTI_BUFFER_EXCERPT_HEADER_HEIGHT, OpenExcerpts,
PageDown, PageUp, PhantomBreakpointIndicator, Point, RowExt, RowRangeExt, SelectPhase,
SelectedTextHighlight, Selection, SelectionDragState, SoftWrap, StickyHeaderExcerpt, ToPoint,
ToggleFold,
ToggleFold, ToggleFoldAll,
code_context_menus::{CodeActionsMenu, MENU_ASIDE_MAX_WIDTH, MENU_ASIDE_MIN_WIDTH, MENU_GAP},
display_map::{
Block, BlockContext, BlockStyle, ChunkRendererId, DisplaySnapshot, EditorMargins,
@@ -416,6 +416,7 @@ impl EditorElement {
register_action(editor, window, Editor::fold_recursive);
register_action(editor, window, Editor::toggle_fold);
register_action(editor, window, Editor::toggle_fold_recursive);
register_action(editor, window, Editor::toggle_fold_all);
register_action(editor, window, Editor::unfold_lines);
register_action(editor, window, Editor::unfold_recursive);
register_action(editor, window, Editor::unfold_all);
@@ -2093,16 +2094,19 @@ impl EditorElement {
window: &mut Window,
cx: &mut App,
) -> HashMap<DisplayRow, AnyElement> {
if self.editor.read(cx).mode().is_minimap() {
return HashMap::default();
}
let max_severity = match ProjectSettings::get_global(cx)
.diagnostics
.inline
.max_severity
.unwrap_or_else(|| self.editor.read(cx).diagnostics_max_severity)
.into_lsp()
let max_severity = match self
.editor
.read(cx)
.inline_diagnostics_enabled()
.then(|| {
ProjectSettings::get_global(cx)
.diagnostics
.inline
.max_severity
.unwrap_or_else(|| self.editor.read(cx).diagnostics_max_severity)
.into_lsp()
})
.flatten()
{
Some(max_severity) => max_severity,
None => return HashMap::default(),
@@ -2618,9 +2622,6 @@ impl EditorElement {
window: &mut Window,
cx: &mut App,
) -> Option<Vec<IndentGuideLayout>> {
if self.editor.read(cx).mode().is_minimap() {
return None;
}
let indent_guides = self.editor.update(cx, |editor, cx| {
editor.indent_guides(visible_buffer_range, snapshot, cx)
})?;
@@ -2836,6 +2837,7 @@ impl EditorElement {
) -> Vec<AnyElement> {
self.editor.update(cx, |editor, cx| {
let active_task_indicator_row =
// TODO: add edit button on the right side of each row in the context menu
if let Some(crate::CodeContextMenu::CodeActions(CodeActionsMenu {
deployed_from,
actions,
@@ -3083,9 +3085,9 @@ impl EditorElement {
window: &mut Window,
cx: &mut App,
) -> Arc<HashMap<MultiBufferRow, LineNumberLayout>> {
let include_line_numbers = snapshot.show_line_numbers.unwrap_or_else(|| {
EditorSettings::get_global(cx).gutter.line_numbers && snapshot.mode.is_full()
});
let include_line_numbers = snapshot
.show_line_numbers
.unwrap_or_else(|| EditorSettings::get_global(cx).gutter.line_numbers);
if !include_line_numbers {
return Arc::default();
}
@@ -3398,22 +3400,18 @@ impl EditorElement {
div()
.size_full()
.children(
(!snapshot.mode.is_minimap() || custom.render_in_minimap).then(|| {
custom.render(&mut BlockContext {
window,
app: cx,
anchor_x,
margins: editor_margins,
line_height,
em_width,
block_id,
selected,
max_width: text_hitbox.size.width.max(*scroll_width),
editor_style: &self.style,
})
}),
)
.child(custom.render(&mut BlockContext {
window,
app: cx,
anchor_x,
margins: editor_margins,
line_height,
em_width,
block_id,
selected,
max_width: text_hitbox.size.width.max(*scroll_width),
editor_style: &self.style,
}))
.into_any()
}
@@ -3619,24 +3617,37 @@ impl EditorElement {
.tooltip({
let focus_handle = focus_handle.clone();
move |window, cx| {
Tooltip::for_action_in(
Tooltip::with_meta_in(
"Toggle Excerpt Fold",
&ToggleFold,
Some(&ToggleFold),
"Alt+click to toggle all",
&focus_handle,
window,
cx,
)
}
})
.on_click(move |_, _, cx| {
if is_folded {
.on_click(move |event, window, cx| {
if event.modifiers().alt {
// Alt+click toggles all buffers
editor.update(cx, |editor, cx| {
editor.unfold_buffer(buffer_id, cx);
editor.toggle_fold_all(
&ToggleFoldAll,
window,
cx,
);
});
} else {
editor.update(cx, |editor, cx| {
editor.fold_buffer(buffer_id, cx);
});
// Regular click toggles single buffer
if is_folded {
editor.update(cx, |editor, cx| {
editor.unfold_buffer(buffer_id, cx);
});
} else {
editor.update(cx, |editor, cx| {
editor.fold_buffer(buffer_id, cx);
});
}
}
}),
),
@@ -6761,7 +6772,7 @@ impl EditorElement {
}
fn paint_mouse_listeners(&mut self, layout: &EditorLayout, window: &mut Window, cx: &mut App) {
if self.editor.read(cx).mode.is_minimap() {
if layout.mode.is_minimap() {
return;
}
@@ -7776,46 +7787,13 @@ impl Element for EditorElement {
editor.set_style(self.style.clone(), window, cx);
let layout_id = match editor.mode {
EditorMode::SingleLine { auto_width } => {
EditorMode::SingleLine => {
let rem_size = window.rem_size();
let height = self.style.text.line_height_in_pixels(rem_size);
if auto_width {
let editor_handle = cx.entity().clone();
let style = self.style.clone();
window.request_measured_layout(
Style::default(),
move |_, _, window, cx| {
let editor_snapshot = editor_handle
.update(cx, |editor, cx| editor.snapshot(window, cx));
let line = Self::layout_lines(
DisplayRow(0)..DisplayRow(1),
&editor_snapshot,
&style,
px(f32::MAX),
|_| false, // Single lines never soft wrap
window,
cx,
)
.pop()
.unwrap();
let font_id =
window.text_system().resolve_font(&style.text.font());
let font_size =
style.text.font_size.to_pixels(window.rem_size());
let em_width =
window.text_system().em_width(font_id, font_size).unwrap();
size(line.width + em_width, height)
},
)
} else {
let mut style = Style::default();
style.size.height = height.into();
style.size.width = relative(1.).into();
window.request_layout(style, None, cx)
}
let mut style = Style::default();
style.size.height = height.into();
style.size.width = relative(1.).into();
window.request_layout(style, None, cx)
}
EditorMode::AutoHeight {
min_lines,
@@ -7888,9 +7866,14 @@ impl Element for EditorElement {
line_height: Some(self.style.text.line_height),
..Default::default()
};
let focus_handle = self.editor.focus_handle(cx);
window.set_view_id(self.editor.entity_id());
window.set_focus_handle(&focus_handle, cx);
let is_minimap = self.editor.read(cx).mode.is_minimap();
if !is_minimap {
let focus_handle = self.editor.focus_handle(cx);
window.set_view_id(self.editor.entity_id());
window.set_focus_handle(&focus_handle, cx);
}
let rem_size = self.rem_size(cx);
window.with_rem_size(rem_size, |window| {
@@ -8034,23 +8017,25 @@ impl Element for EditorElement {
}
};
// TODO: Autoscrolling for both axes
let mut autoscroll_request = None;
let mut autoscroll_containing_element = false;
let mut autoscroll_horizontally = false;
self.editor.update(cx, |editor, cx| {
autoscroll_request = editor.autoscroll_request();
autoscroll_containing_element =
let (
autoscroll_request,
autoscroll_containing_element,
needs_horizontal_autoscroll,
) = self.editor.update(cx, |editor, cx| {
let autoscroll_request = editor.autoscroll_request();
let autoscroll_containing_element =
autoscroll_request.is_some() || editor.has_pending_selection();
// TODO: Is this horizontal or vertical?!
autoscroll_horizontally = editor.autoscroll_vertically(
bounds,
line_height,
max_scroll_top,
window,
cx,
);
snapshot = editor.snapshot(window, cx);
let (needs_horizontal_autoscroll, was_scrolled) = editor
.autoscroll_vertically(bounds, line_height, max_scroll_top, window, cx);
if was_scrolled.0 {
snapshot = editor.snapshot(window, cx);
}
(
autoscroll_request,
autoscroll_containing_element,
needs_horizontal_autoscroll,
)
});
let mut scroll_position = snapshot.scroll_position();
@@ -8326,18 +8311,22 @@ impl Element for EditorElement {
window,
cx,
);
let new_renrerer_widths = line_layouts
.iter()
.flat_map(|layout| &layout.fragments)
.filter_map(|fragment| {
if let LineFragment::Element { id, size, .. } = fragment {
Some((*id, size.width))
} else {
None
}
});
if self.editor.update(cx, |editor, cx| {
editor.update_renderer_widths(new_renrerer_widths, cx)
let new_renderer_widths = (!is_minimap).then(|| {
line_layouts
.iter()
.flat_map(|layout| &layout.fragments)
.filter_map(|fragment| {
if let LineFragment::Element { id, size, .. } = fragment {
Some((*id, size.width))
} else {
None
}
})
});
if new_renderer_widths.is_some_and(|new_renderer_widths| {
self.editor.update(cx, |editor, cx| {
editor.update_renderer_widths(new_renderer_widths, cx)
})
}) {
// If the fold widths have changed, we need to prepaint
// the element again to account for any changes in
@@ -8400,27 +8389,31 @@ impl Element for EditorElement {
let sticky_header_excerpt_id =
sticky_header_excerpt.as_ref().map(|top| top.excerpt.id);
let blocks = window.with_element_namespace("blocks", |window| {
self.render_blocks(
start_row..end_row,
&snapshot,
&hitbox,
&text_hitbox,
editor_width,
&mut scroll_width,
&editor_margins,
em_width,
gutter_dimensions.full_width(),
line_height,
&mut line_layouts,
&local_selections,
&selected_buffer_ids,
is_row_soft_wrapped,
sticky_header_excerpt_id,
window,
cx,
)
});
let blocks = (!is_minimap)
.then(|| {
window.with_element_namespace("blocks", |window| {
self.render_blocks(
start_row..end_row,
&snapshot,
&hitbox,
&text_hitbox,
editor_width,
&mut scroll_width,
&editor_margins,
em_width,
gutter_dimensions.full_width(),
line_height,
&mut line_layouts,
&local_selections,
&selected_buffer_ids,
is_row_soft_wrapped,
sticky_header_excerpt_id,
window,
cx,
)
})
})
.unwrap_or_else(|| Ok((Vec::default(), HashMap::default())));
let (mut blocks, row_block_types) = match blocks {
Ok(blocks) => blocks,
Err(resized_blocks) => {
@@ -8459,10 +8452,12 @@ impl Element for EditorElement {
);
self.editor.update(cx, |editor, cx| {
let clamped = editor.scroll_manager.clamp_scroll_left(scroll_max.x);
if editor.scroll_manager.clamp_scroll_left(scroll_max.x) {
scroll_position.x = scroll_position.x.min(scroll_max.x);
}
let autoscrolled = if autoscroll_horizontally {
editor.autoscroll_horizontally(
if needs_horizontal_autoscroll.0
&& let Some(new_scroll_position) = editor.autoscroll_horizontally(
start_row,
editor_content_width,
scroll_width,
@@ -8471,13 +8466,8 @@ impl Element for EditorElement {
window,
cx,
)
} else {
false
};
if clamped || autoscrolled {
snapshot = editor.snapshot(window, cx);
scroll_position = snapshot.scroll_position();
{
scroll_position = new_scroll_position;
}
});
@@ -8592,7 +8582,9 @@ impl Element for EditorElement {
}
} else {
log::error!(
"bug: line_ix {} is out of bounds - row_infos.len(): {}, line_layouts.len(): {}, crease_trailers.len(): {}",
"bug: line_ix {} is out of bounds - row_infos.len(): {}, \
line_layouts.len(): {}, \
crease_trailers.len(): {}",
line_ix,
row_infos.len(),
line_layouts.len(),
@@ -8838,7 +8830,7 @@ impl Element for EditorElement {
underline: None,
strikethrough: None,
}],
None
None,
);
let space_invisible = window.text_system().shape_line(
"".into(),
@@ -8851,7 +8843,7 @@ impl Element for EditorElement {
underline: None,
strikethrough: None,
}],
None
None,
);
let mode = snapshot.mode.clone();
@@ -8953,19 +8945,21 @@ impl Element for EditorElement {
window: &mut Window,
cx: &mut App,
) {
let focus_handle = self.editor.focus_handle(cx);
let key_context = self
.editor
.update(cx, |editor, cx| editor.key_context(window, cx));
if !layout.mode.is_minimap() {
let focus_handle = self.editor.focus_handle(cx);
let key_context = self
.editor
.update(cx, |editor, cx| editor.key_context(window, cx));
window.set_key_context(key_context);
window.handle_input(
&focus_handle,
ElementInputHandler::new(bounds, self.editor.clone()),
cx,
);
self.register_actions(window, cx);
self.register_key_listeners(window, cx, layout);
window.set_key_context(key_context);
window.handle_input(
&focus_handle,
ElementInputHandler::new(bounds, self.editor.clone()),
cx,
);
self.register_actions(window, cx);
self.register_key_listeners(window, cx, layout);
}
let text_style = TextStyleRefinement {
font_size: Some(self.style.text.font_size),
@@ -10274,7 +10268,6 @@ mod tests {
height: Some(3),
render: Arc::new(|cx| div().h(3. * cx.window.line_height()).into_any()),
priority: 0,
render_in_minimap: true,
}],
None,
cx,
@@ -10364,7 +10357,7 @@ mod tests {
});
for editor_mode_without_invisibles in [
EditorMode::SingleLine { auto_width: false },
EditorMode::SingleLine,
EditorMode::AutoHeight {
min_lines: 1,
max_lines: Some(100),

View File

@@ -813,7 +813,13 @@ impl Item for Editor {
window: &mut Window,
cx: &mut Context<Self>,
) -> Task<Result<()>> {
self.report_editor_event("Editor Saved", None, cx);
// Add meta data tracking # of auto saves
if options.autosave {
self.report_editor_event("Editor Autosaved", None, cx);
} else {
self.report_editor_event("Editor Saved", None, cx);
}
let buffers = self.buffer().clone().read(cx).all_buffers();
let buffers = buffers
.into_iter()
@@ -1220,7 +1226,20 @@ impl SerializableItem for Editor {
abs_path: None,
contents: None,
..
} => Task::ready(Err(anyhow!("No path or contents found for buffer"))),
} => window.spawn(cx, async move |cx| {
let buffer = project
.update(cx, |project, cx| project.create_buffer(cx))?
.await?;
cx.update(|window, cx| {
cx.new(|cx| {
let mut editor = Editor::for_buffer(buffer, Some(project), window, cx);
editor.read_metadata_from_db(item_id, workspace_id, window, cx);
editor
})
})
}),
}
}
@@ -2092,5 +2111,38 @@ mod tests {
assert!(editor.has_conflict(cx)); // The editor should have a conflict
});
}
// Test case 5: Deserialize with no path, no content, no language, and no old mtime (new, empty, unsaved buffer)
{
let project = Project::test(fs.clone(), [path!("/file.rs").as_ref()], cx).await;
let (workspace, cx) =
cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
let workspace_id = workspace::WORKSPACE_DB.next_id().await.unwrap();
let item_id = 10000 as ItemId;
let serialized_editor = SerializedEditor {
abs_path: None,
contents: None,
language: None,
mtime: None,
};
DB.save_serialized_editor(item_id, workspace_id, serialized_editor)
.await
.unwrap();
let deserialized =
deserialize_editor(item_id, workspace_id, workspace, project, cx).await;
deserialized.update(cx, |editor, cx| {
assert_eq!(editor.text(cx), "");
assert!(!editor.is_dirty(cx));
assert!(!editor.has_conflict(cx));
let buffer = editor.buffer().read(cx).as_singleton().unwrap().read(cx);
assert!(buffer.file().is_none());
});
}
}
}

View File

@@ -27,6 +27,8 @@ use workspace::{ItemId, WorkspaceId};
pub const SCROLL_EVENT_SEPARATION: Duration = Duration::from_millis(28);
const SCROLLBAR_SHOW_INTERVAL: Duration = Duration::from_secs(1);
pub struct WasScrolled(pub(crate) bool);
#[derive(Default)]
pub struct ScrollbarAutoHide(pub bool);
@@ -215,87 +217,56 @@ impl ScrollManager {
workspace_id: Option<WorkspaceId>,
window: &mut Window,
cx: &mut Context<Editor>,
) {
let (new_anchor, top_row) = if scroll_position.y <= 0. && scroll_position.x <= 0. {
(
ScrollAnchor {
anchor: Anchor::min(),
offset: scroll_position.max(&gpui::Point::default()),
},
0,
)
} else if scroll_position.y <= 0. {
let buffer_point = map
.clip_point(
DisplayPoint::new(DisplayRow(0), scroll_position.x as u32),
Bias::Left,
)
.to_point(map);
let anchor = map.buffer_snapshot.anchor_at(buffer_point, Bias::Right);
(
ScrollAnchor {
anchor: anchor,
offset: scroll_position.max(&gpui::Point::default()),
},
0,
)
} else {
let scroll_top = scroll_position.y;
let scroll_top = match EditorSettings::get_global(cx).scroll_beyond_last_line {
ScrollBeyondLastLine::OnePage => scroll_top,
ScrollBeyondLastLine::Off => {
if let Some(height_in_lines) = self.visible_line_count {
let max_row = map.max_point().row().0 as f32;
scroll_top.min(max_row - height_in_lines + 1.).max(0.)
} else {
scroll_top
}
) -> WasScrolled {
let scroll_top = scroll_position.y.max(0.);
let scroll_top = match EditorSettings::get_global(cx).scroll_beyond_last_line {
ScrollBeyondLastLine::OnePage => scroll_top,
ScrollBeyondLastLine::Off => {
if let Some(height_in_lines) = self.visible_line_count {
let max_row = map.max_point().row().0 as f32;
scroll_top.min(max_row - height_in_lines + 1.).max(0.)
} else {
scroll_top
}
ScrollBeyondLastLine::VerticalScrollMargin => {
if let Some(height_in_lines) = self.visible_line_count {
let max_row = map.max_point().row().0 as f32;
scroll_top
.min(max_row - height_in_lines + 1. + self.vertical_scroll_margin)
.max(0.)
} else {
scroll_top
}
}
ScrollBeyondLastLine::VerticalScrollMargin => {
if let Some(height_in_lines) = self.visible_line_count {
let max_row = map.max_point().row().0 as f32;
scroll_top
.min(max_row - height_in_lines + 1. + self.vertical_scroll_margin)
.max(0.)
} else {
scroll_top
}
};
let scroll_top_row = DisplayRow(scroll_top as u32);
let scroll_top_buffer_point = map
.clip_point(
DisplayPoint::new(scroll_top_row, scroll_position.x as u32),
Bias::Left,
)
.to_point(map);
let top_anchor = map
.buffer_snapshot
.anchor_at(scroll_top_buffer_point, Bias::Right);
(
ScrollAnchor {
anchor: top_anchor,
offset: point(
scroll_position.x.max(0.),
scroll_top - top_anchor.to_display_point(map).row().as_f32(),
),
},
scroll_top_buffer_point.row,
)
}
};
let scroll_top_row = DisplayRow(scroll_top as u32);
let scroll_top_buffer_point = map
.clip_point(
DisplayPoint::new(scroll_top_row, scroll_position.x as u32),
Bias::Left,
)
.to_point(map);
let top_anchor = map
.buffer_snapshot
.anchor_at(scroll_top_buffer_point, Bias::Right);
self.set_anchor(
new_anchor,
top_row,
ScrollAnchor {
anchor: top_anchor,
offset: point(
scroll_position.x.max(0.),
scroll_top - top_anchor.to_display_point(map).row().as_f32(),
),
},
scroll_top_buffer_point.row,
local,
autoscroll,
workspace_id,
window,
cx,
);
)
}
fn set_anchor(
@@ -307,7 +278,7 @@ impl ScrollManager {
workspace_id: Option<WorkspaceId>,
window: &mut Window,
cx: &mut Context<Editor>,
) {
) -> WasScrolled {
let adjusted_anchor = if self.forbid_vertical_scroll {
ScrollAnchor {
offset: gpui::Point::new(anchor.offset.x, self.anchor.offset.y),
@@ -317,10 +288,14 @@ impl ScrollManager {
anchor
};
self.autoscroll_request.take();
if self.anchor == adjusted_anchor {
return WasScrolled(false);
}
self.anchor = adjusted_anchor;
cx.emit(EditorEvent::ScrollPositionChanged { local, autoscroll });
self.show_scrollbars(window, cx);
self.autoscroll_request.take();
if let Some(workspace_id) = workspace_id {
let item_id = cx.entity().entity_id().as_u64() as ItemId;
@@ -342,6 +317,8 @@ impl ScrollManager {
.detach()
}
cx.notify();
WasScrolled(true)
}
pub fn show_scrollbars(&mut self, window: &mut Window, cx: &mut Context<Editor>) {
@@ -552,13 +529,13 @@ impl Editor {
scroll_position: gpui::Point<f32>,
window: &mut Window,
cx: &mut Context<Self>,
) {
) -> WasScrolled {
let mut position = scroll_position;
if self.scroll_manager.forbid_vertical_scroll {
let current_position = self.scroll_position(cx);
position.y = current_position.y;
}
self.set_scroll_position_internal(position, true, false, window, cx);
self.set_scroll_position_internal(position, true, false, window, cx)
}
/// Scrolls so that `row` is at the top of the editor view.
@@ -590,7 +567,7 @@ impl Editor {
autoscroll: bool,
window: &mut Window,
cx: &mut Context<Self>,
) {
) -> WasScrolled {
let map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
self.set_scroll_position_taking_display_map(
scroll_position,
@@ -599,7 +576,7 @@ impl Editor {
map,
window,
cx,
);
)
}
fn set_scroll_position_taking_display_map(
@@ -610,7 +587,7 @@ impl Editor {
display_map: DisplaySnapshot,
window: &mut Window,
cx: &mut Context<Self>,
) {
) -> WasScrolled {
hide_hover(self, cx);
let workspace_id = self.workspace.as_ref().and_then(|workspace| workspace.1);
@@ -624,7 +601,7 @@ impl Editor {
scroll_position
};
self.scroll_manager.set_scroll_position(
let editor_was_scrolled = self.scroll_manager.set_scroll_position(
adjusted_position,
&display_map,
local,
@@ -636,6 +613,7 @@ impl Editor {
self.refresh_inlay_hints(InlayHintRefreshReason::NewLinesShown, cx);
self.refresh_colors(false, None, window, cx);
editor_was_scrolled
}
pub fn scroll_position(&self, cx: &mut Context<Self>) -> gpui::Point<f32> {

View File

@@ -1,6 +1,6 @@
use crate::{
DisplayRow, Editor, EditorMode, LineWithInvisibles, RowExt, SelectionEffects,
display_map::ToDisplayPoint,
display_map::ToDisplayPoint, scroll::WasScrolled,
};
use gpui::{Bounds, Context, Pixels, Window, px};
use language::Point;
@@ -99,19 +99,21 @@ impl AutoscrollStrategy {
}
}
pub(crate) struct NeedsHorizontalAutoscroll(pub(crate) bool);
impl Editor {
pub fn autoscroll_request(&self) -> Option<Autoscroll> {
self.scroll_manager.autoscroll_request()
}
pub fn autoscroll_vertically(
pub(crate) fn autoscroll_vertically(
&mut self,
bounds: Bounds<Pixels>,
line_height: Pixels,
max_scroll_top: f32,
window: &mut Window,
cx: &mut Context<Editor>,
) -> bool {
) -> (NeedsHorizontalAutoscroll, WasScrolled) {
let viewport_height = bounds.size.height;
let visible_lines = viewport_height / line_height;
let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
@@ -129,12 +131,14 @@ impl Editor {
scroll_position.y = max_scroll_top;
}
if original_y != scroll_position.y {
self.set_scroll_position(scroll_position, window, cx);
}
let editor_was_scrolled = if original_y != scroll_position.y {
self.set_scroll_position(scroll_position, window, cx)
} else {
WasScrolled(false)
};
let Some((autoscroll, local)) = self.scroll_manager.autoscroll_request.take() else {
return false;
return (NeedsHorizontalAutoscroll(false), editor_was_scrolled);
};
let mut target_top;
@@ -212,7 +216,7 @@ impl Editor {
target_bottom = target_top + 1.;
}
match strategy {
let was_autoscrolled = match strategy {
AutoscrollStrategy::Fit | AutoscrollStrategy::Newest => {
let margin = margin.min(self.scroll_manager.vertical_scroll_margin);
let target_top = (target_top - margin).max(0.0);
@@ -225,39 +229,42 @@ impl Editor {
if needs_scroll_up && !needs_scroll_down {
scroll_position.y = target_top;
self.set_scroll_position_internal(scroll_position, local, true, window, cx);
}
if !needs_scroll_up && needs_scroll_down {
} else if !needs_scroll_up && needs_scroll_down {
scroll_position.y = target_bottom - visible_lines;
self.set_scroll_position_internal(scroll_position, local, true, window, cx);
}
if needs_scroll_up ^ needs_scroll_down {
self.set_scroll_position_internal(scroll_position, local, true, window, cx)
} else {
WasScrolled(false)
}
}
AutoscrollStrategy::Center => {
scroll_position.y = (target_top - margin).max(0.0);
self.set_scroll_position_internal(scroll_position, local, true, window, cx);
self.set_scroll_position_internal(scroll_position, local, true, window, cx)
}
AutoscrollStrategy::Focused => {
let margin = margin.min(self.scroll_manager.vertical_scroll_margin);
scroll_position.y = (target_top - margin).max(0.0);
self.set_scroll_position_internal(scroll_position, local, true, window, cx);
self.set_scroll_position_internal(scroll_position, local, true, window, cx)
}
AutoscrollStrategy::Top => {
scroll_position.y = (target_top).max(0.0);
self.set_scroll_position_internal(scroll_position, local, true, window, cx);
self.set_scroll_position_internal(scroll_position, local, true, window, cx)
}
AutoscrollStrategy::Bottom => {
scroll_position.y = (target_bottom - visible_lines).max(0.0);
self.set_scroll_position_internal(scroll_position, local, true, window, cx);
self.set_scroll_position_internal(scroll_position, local, true, window, cx)
}
AutoscrollStrategy::TopRelative(lines) => {
scroll_position.y = target_top - lines as f32;
self.set_scroll_position_internal(scroll_position, local, true, window, cx);
self.set_scroll_position_internal(scroll_position, local, true, window, cx)
}
AutoscrollStrategy::BottomRelative(lines) => {
scroll_position.y = target_bottom + lines as f32;
self.set_scroll_position_internal(scroll_position, local, true, window, cx);
self.set_scroll_position_internal(scroll_position, local, true, window, cx)
}
}
};
self.scroll_manager.last_autoscroll = Some((
self.scroll_manager.anchor.offset,
@@ -266,7 +273,8 @@ impl Editor {
strategy,
));
true
let was_scrolled = WasScrolled(editor_was_scrolled.0 || was_autoscrolled.0);
(NeedsHorizontalAutoscroll(true), was_scrolled)
}
pub(crate) fn autoscroll_horizontally(
@@ -278,7 +286,7 @@ impl Editor {
layouts: &[LineWithInvisibles],
window: &mut Window,
cx: &mut Context<Self>,
) -> bool {
) -> Option<gpui::Point<f32>> {
let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
let selections = self.selections.all::<Point>(cx);
let mut scroll_position = self.scroll_manager.scroll_position(&display_map);
@@ -319,22 +327,26 @@ impl Editor {
target_right = target_right.min(scroll_width);
if target_right - target_left > viewport_width {
return false;
return None;
}
let scroll_left = self.scroll_manager.anchor.offset.x * em_advance;
let scroll_right = scroll_left + viewport_width;
if target_left < scroll_left {
let was_scrolled = if target_left < scroll_left {
scroll_position.x = target_left / em_advance;
self.set_scroll_position_internal(scroll_position, true, true, window, cx);
true
self.set_scroll_position_internal(scroll_position, true, true, window, cx)
} else if target_right > scroll_right {
scroll_position.x = (target_right - viewport_width) / em_advance;
self.set_scroll_position_internal(scroll_position, true, true, window, cx);
true
self.set_scroll_position_internal(scroll_position, true, true, window, cx)
} else {
false
WasScrolled(false)
};
if was_scrolled.0 {
Some(scroll_position)
} else {
None
}
}

View File

@@ -221,9 +221,6 @@ impl ExampleContext {
ThreadEvent::ShowError(thread_error) => {
tx.try_send(Err(anyhow!(thread_error.clone()))).ok();
}
ThreadEvent::RetriesFailed { .. } => {
// Ignore retries failed events
}
ThreadEvent::Stopped(reason) => match reason {
Ok(StopReason::EndTurn) => {
tx.close_channel();

View File

@@ -289,6 +289,24 @@ async fn copy_extension_resources(
}
}
if let Some(snippets_path) = manifest.snippets.as_ref() {
let parent = snippets_path.parent();
if let Some(parent) = parent.filter(|p| p.components().next().is_some()) {
fs::create_dir_all(output_dir.join(parent))?;
}
copy_recursive(
fs.as_ref(),
&extension_path.join(&snippets_path),
&output_dir.join(&snippets_path),
CopyOptions {
overwrite: true,
ignore_if_exists: false,
},
)
.await
.with_context(|| format!("failed to copy snippets from '{}'", snippets_path.display()))?;
}
Ok(())
}

View File

@@ -6,6 +6,7 @@ use std::sync::OnceLock;
use std::time::Duration;
use std::{ops::Range, sync::Arc};
use anyhow::Context as _;
use client::{ExtensionMetadata, ExtensionProvides};
use collections::{BTreeMap, BTreeSet};
use editor::{Editor, EditorElement, EditorStyle};
@@ -23,7 +24,7 @@ use settings::Settings;
use strum::IntoEnumIterator as _;
use theme::ThemeSettings;
use ui::{
CheckboxWithLabel, ContextMenu, PopoverMenu, ScrollableHandle, Scrollbar, ScrollbarState,
CheckboxWithLabel, Chip, ContextMenu, PopoverMenu, ScrollableHandle, Scrollbar, ScrollbarState,
ToggleButton, Tooltip, prelude::*,
};
use vim_mode_setting::VimModeSetting;
@@ -80,16 +81,24 @@ pub fn init(cx: &mut App) {
.find_map(|item| item.downcast::<ExtensionsPage>());
if let Some(existing) = existing {
if provides_filter.is_some() {
existing.update(cx, |extensions_page, cx| {
existing.update(cx, |extensions_page, cx| {
if provides_filter.is_some() {
extensions_page.change_provides_filter(provides_filter, cx);
});
}
}
if let Some(id) = action.id.as_ref() {
extensions_page.focus_extension(id, window, cx);
}
});
workspace.activate_item(&existing, true, true, window, cx);
} else {
let extensions_page =
ExtensionsPage::new(workspace, provides_filter, window, cx);
let extensions_page = ExtensionsPage::new(
workspace,
provides_filter,
action.id.as_deref(),
window,
cx,
);
workspace.add_item_to_active_pane(
Box::new(extensions_page),
None,
@@ -287,6 +296,7 @@ impl ExtensionsPage {
pub fn new(
workspace: &Workspace,
provides_filter: Option<ExtensionProvides>,
focus_extension_id: Option<&str>,
window: &mut Window,
cx: &mut Context<Workspace>,
) -> Entity<Self> {
@@ -317,6 +327,9 @@ impl ExtensionsPage {
let query_editor = cx.new(|cx| {
let mut input = Editor::single_line(window, cx);
input.set_placeholder_text("Search extensions...", cx);
if let Some(id) = focus_extension_id {
input.set_text(format!("id:{id}"), window, cx);
}
input
});
cx.subscribe(&query_editor, Self::on_query_change).detach();
@@ -340,7 +353,7 @@ impl ExtensionsPage {
scrollbar_state: ScrollbarState::new(scroll_handle),
};
this.fetch_extensions(
None,
this.search_query(cx),
Some(BTreeSet::from_iter(this.provides_filter)),
None,
cx,
@@ -464,9 +477,23 @@ impl ExtensionsPage {
.cloned()
.collect::<Vec<_>>();
let remote_extensions = extension_store.update(cx, |store, cx| {
store.fetch_extensions(search.as_deref(), provides_filter.as_ref(), cx)
});
let remote_extensions =
if let Some(id) = search.as_ref().and_then(|s| s.strip_prefix("id:")) {
let versions =
extension_store.update(cx, |store, cx| store.fetch_extension_versions(id, cx));
cx.foreground_executor().spawn(async move {
let versions = versions.await?;
let latest = versions
.into_iter()
.max_by_key(|v| v.published_at)
.context("no extension found")?;
Ok(vec![latest])
})
} else {
extension_store.update(cx, |store, cx| {
store.fetch_extensions(search.as_deref(), provides_filter.as_ref(), cx)
})
};
cx.spawn(async move |this, cx| {
let dev_extensions = if let Some(search) = search {
@@ -732,20 +759,7 @@ impl ExtensionsPage {
_ => {}
}
Some(
div()
.px_1()
.border_1()
.rounded_sm()
.border_color(cx.theme().colors().border)
.bg(cx.theme().colors().element_background)
.child(
Label::new(extension_provides_label(
*provides,
))
.size(LabelSize::XSmall),
),
)
Some(Chip::new(extension_provides_label(*provides)))
})
.collect::<Vec<_>>(),
),
@@ -1165,6 +1179,13 @@ impl ExtensionsPage {
self.refresh_feature_upsells(cx);
}
pub fn focus_extension(&mut self, id: &str, window: &mut Window, cx: &mut Context<Self>) {
self.query_editor.update(cx, |editor, cx| {
editor.set_text(format!("id:{id}"), window, cx)
});
self.refresh_search(cx);
}
pub fn change_provides_filter(
&mut self,
provides_filter: Option<ExtensionProvides>,

View File

@@ -98,17 +98,6 @@ impl FeatureFlag for AcpFeatureFlag {
const NAME: &'static str = "acp";
}
pub struct ZedCloudFeatureFlag {}
impl FeatureFlag for ZedCloudFeatureFlag {
const NAME: &'static str = "zed-cloud";
fn enabled_for_staff() -> bool {
// Require individual opt-in, for now.
false
}
}
pub trait FeatureFlagViewExt<V: 'static> {
fn observe_flag<T: FeatureFlag, F>(&mut self, window: &Window, callback: F) -> Subscription
where

View File

@@ -1,7 +1,7 @@
use crate::FakeFs;
use crate::{FakeFs, Fs};
use anyhow::{Context as _, Result};
use collections::{HashMap, HashSet};
use futures::future::{self, BoxFuture};
use futures::future::{self, BoxFuture, join_all};
use git::{
blame::Blame,
repository::{
@@ -356,18 +356,46 @@ impl GitRepository for FakeGitRepository {
fn stage_paths(
&self,
_paths: Vec<RepoPath>,
paths: Vec<RepoPath>,
_env: Arc<HashMap<String, String>>,
) -> BoxFuture<'_, Result<()>> {
unimplemented!()
Box::pin(async move {
let contents = paths
.into_iter()
.map(|path| {
let abs_path = self.dot_git_path.parent().unwrap().join(&path);
Box::pin(async move { (path.clone(), self.fs.load(&abs_path).await.ok()) })
})
.collect::<Vec<_>>();
let contents = join_all(contents).await;
self.with_state_async(true, move |state| {
for (path, content) in contents {
if let Some(content) = content {
state.index_contents.insert(path, content);
} else {
state.index_contents.remove(&path);
}
}
Ok(())
})
.await
})
}
fn unstage_paths(
&self,
_paths: Vec<RepoPath>,
paths: Vec<RepoPath>,
_env: Arc<HashMap<String, String>>,
) -> BoxFuture<'_, Result<()>> {
unimplemented!()
self.with_state_async(true, move |state| {
for path in paths {
match state.head_contents.get(&path) {
Some(content) => state.index_contents.insert(path, content.clone()),
None => state.index_contents.remove(&path),
};
}
Ok(())
})
}
fn commit(
@@ -375,10 +403,8 @@ impl GitRepository for FakeGitRepository {
_message: gpui::SharedString,
_name_and_email: Option<(gpui::SharedString, gpui::SharedString)>,
_options: CommitOptions,
_ask_pass: AskPassDelegate,
_env: Arc<HashMap<String, String>>,
_cx: AsyncApp,
) -> BoxFuture<'static, Result<()>> {
) -> BoxFuture<'_, Result<()>> {
unimplemented!()
}

View File

@@ -41,9 +41,9 @@ futures.workspace = true
workspace-hack.workspace = true
[dev-dependencies]
gpui = { workspace = true, features = ["test-support"] }
pretty_assertions.workspace = true
serde_json.workspace = true
tempfile.workspace = true
text = { workspace = true, features = ["test-support"] }
unindent.workspace = true
gpui = { workspace = true, features = ["test-support"] }
tempfile.workspace = true

View File

@@ -31,8 +31,10 @@ actions!(
git,
[
// per-hunk
/// Toggles the staged state of the hunk at cursor.
/// Toggles the staged state of the hunk or status entry at cursor.
ToggleStaged,
/// Stage status entries between an anchor entry and the cursor.
StageRange,
/// Stages the current hunk and moves to the next one.
StageAndNext,
/// Unstages the current hunk and moves to the next one.

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