Compare commits

..

95 Commits

Author SHA1 Message Date
Piotr Osiewicz
0ae31d0dc7 git: Improve traversal perf with multiple repositories in a single worktree 2025-07-15 15:50:00 +02:00
Piotr Osiewicz
31a4f6d411 git: Improve perf of syncing changes 2025-07-15 15:14:10 +02: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
Agus Zubiaga
d0e01dbd8f Improve thread message history (#34299)
- Keep history across threads
- Reset position when edited

Release Notes:

- N/A

---------

Co-authored-by: Ben Brandt <benjamin.j.brandt@gmail.com>
2025-07-11 16:24:41 +00:00
Danilo Leal
d65855c4a1 git: Change merge conflict button labels (#34297)
Following feedback that "Take Ours" and "Take Theirs" was confusing,
leading to users not knowing what exactly happened with each of these
buttons. It's now "Use HEAD" and "Use Origin", which also match what is
written in Git markers, helping parse them out more easily. Future
improvement is to have the actual branch target name in the "Use Origin"
button.

Release Notes:

- git: Improved merge conflict buttons clarity by changing labels to
"Use HEAD" and "Use Origin".
2025-07-11 13:15:06 -03:00
Alisina Bahadori
70351360d7 Fix bad kerning in integrated terminal (#34292)
Closes #16869

Release Notes:

- (preview only): Fix bad kerning in integrated terminal.
2025-07-11 09:50:48 -06:00
Conrad Irwin
993e0f55ec ACP follow (#34235)
Closes #ISSUE

Release Notes:

- N/A

---------

Co-authored-by: Agus Zubiaga <agus@zed.dev>
Co-authored-by: Anthony Eid <hello@anthonyeid.me>
Co-authored-by: Ben Brandt <benjamin.j.brandt@gmail.com>
2025-07-11 12:38:42 -03:00
Ben Kunkle
496bf0ec43 keymap_ui: Ensure keymap UI opens in local workspace (#34291)
Closes #ISSUE

Use `workspace.with_local_workspace` to ensure the keymap UI is opened
in a local workspace, even in remote. This was tested by removing the
feature flag handling code, as with the feature flag logic the action
does not appear which is likely a bug.

Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-07-11 11:32:59 -04:00
morgankrey
c09f484ec4 collab: Add ability to add tax ID during Stripe Checkout (#34246)
### 1. **Added Tax ID Collection Types**
- Created a new `StripeTaxIdCollection` struct with an `enabled` field
- Added `tax_id_collection` field to `StripeCreateCheckoutSessionParams`

### 2. **Updated the Stripe Client Interface**
- Modified the real Stripe client to handle tax ID collection conversion
- Updated the fake Stripe client for testing purposes
- Added proper imports across all affected files

### 3. **Enabled Tax ID Collection in Checkout Sessions**
- Both `checkout_with_zed_pro` and `checkout_with_zed_pro_trial` methods
now enable tax ID collection
- The implementation correctly sets `tax_id_collection.enabled = true`
for all checkout sessions

### 4. **Key Implementation Details**
- Tax ID collection will be shown to new customers and existing
customers without tax IDs
- Collected tax IDs will be automatically saved to the customer's
`tax_ids` array in Stripe
- Business names will be saved to the customer's `name` property
- The existing `customer_update.name = auto` setting ensures
compatibility with tax ID collection

Release Notes:

- N/A
2025-07-11 11:26:36 -04:00
Finn Evers
a58a75c0f6 keymap_ui: Hide tooltips when context menu is shown (#34286)
This PR ensures tooltips are dismissed/not shown once the context menu
is opened.

It also ensures the context menu is dismissed once the list is scrolled.

Release Notes:

- N/A
2025-07-11 15:17:34 +00:00
Ben Kunkle
d1a6c5d494 keymap_ui: Hover tooltip for context (#34290)
Closes #ISSUE

Ideally the tooltip would only appear if the context was overflowing
it's column, but for now, we just unconditionally show a tooltip so that
long contexts can be seen.

This PR also includes a change to the tooltip element, allowing for
tooltips with non-text contents which is used here for syntax
highlighting

Release Notes:

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

Co-authored-by: Anthony <anthony@zed.dev>
2025-07-11 10:54:08 -04:00
Peter Tripp
10028aaae8 Ensure *.json recognized as JSONC if checkout folder not zed (#34289)
Follow-up to: https://github.com/zed-industries/zed/pull/33410

Release Notes:

- N/A
2025-07-11 14:25:09 +00:00
Ben Kunkle
3b9bb521f4 keymap_ui: Only show conflicts between user bindings (#34284)
Closes #ISSUE

This makes it so conflicts are only shown between user bindings. User
bindings that override bindings in the Vim, Base, and Default keymaps
are not identified as conflicts

Release Notes:

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

Co-authored-by: Anthony <anthony@zed.dev>
2025-07-11 13:51:21 +00:00
Anthony Eid
7eb739d489 Add initial support for search by keystroke to keybinding editor (#34274)
This PR adds preliminary support for searching keybindings by keystrokes
in the keybinding editor.

Release Notes:

- N/A
2025-07-11 09:29:29 -04:00
localcc
b4cbea50bb Fix icon size on Windows (#34277)
Closes #34122

Release Notes:

- N/A
2025-07-11 15:09:10 +02:00
Smit Barmase
153840199e linux: Use randr as fallback for scale factor in X11 (#34265)
Closes #14537

- Adds server-side scale factor detection via `randr` when client-side
detection fails using `xrdb/Xft.dpi`.
- Adds the `GPUI_X11_SCALE_FACTOR` flag to force a scale factor, which
can be a positive number for custom scaling or `randr` for server-side
scale factor detection.

Release Notes:

- Fixed an issue where the scale factor was not detected correctly on
X11 systems when `Xft.dpi` is not defined (mostly in cases involving
window managers).
2025-07-11 14:52:51 +05:30
张小白
8812e7cd14 windows: Fix an issue where dead keys that require holding shift didn’t work properly (#34264)
Closes #34194

Release Notes:

- N/A
2025-07-11 17:19:23 +08:00
Richard Feldman
56d0ae6782 Don't apply contrast adjustment to decorative chars (#34238)
Closes #34234

Release Notes:

- Automatic contrast adjustment in terminal is no longer applied to
decorative characters used in block art.
2025-07-11 02:48:49 +00:00
Danilo Leal
d52f07b77c lsp tool: Make "Restart All Servers" always visible (#34255)
Next step is to have a "Restart Current Buffer Server(s)". 😬 

Release Notes:

- N/A
2025-07-10 22:00:01 -03:00
Max Frai
089ce8f6aa agent: Allow clicking on the read file tool header to jump to the exact file location (#33161)
Release Notes:

- Allow clicking on the header of the read file tool to jump to the
exact file location

When researching code or when the Agent analyzes context by reading
various project files, the read file tool is used. It usually includes
line numbers relevant to the current prompt or task. However, it’s often
frustrating that the read file header isn’t clickable to view the
corresponding code directly. This PR makes the header clickable,
allowing users to jump to the referenced file. If start and end lines
are specified, it will navigate directly to that exact location.


https://github.com/user-attachments/assets/b0125d0b-7166-43dd-924e-dc5585813b0b

Co-authored-by: Danilo Leal <daniloleal09@gmail.com>
2025-07-11 00:43:52 +00:00
Cole Miller
842ac984d5 git: Intercept signing prompt from GPG when committing (#34096)
Closes #30111 

- [x] basic implementation
- [x] implementation for remote projects
- [x] surface error output from GPG if signing fails
- [ ] ~~Windows~~

Release Notes:

- git: Passphrase prompts from GPG to unlock commit signing keys are now
shown in Zed.
2025-07-11 00:38:51 +00:00
Max Brunsfeld
87362c602f Assign checksum seed in windows releases (#34252)
This will allow windows releases to report panics and telemetry.

Release Notes:

- N/A
2025-07-11 00:09:37 +00:00
Michael Sloan
94916cd3b6 Fix screenshare sources error handling, is_sharing_screen() == false on error (#34250)
Release Notes:

- N/A
2025-07-10 23:35:21 +00:00
Ben Kunkle
7915b9f93f keymap_ui: Add ability to delete user created bindings (#34248)
Closes #ISSUE

Adds an action and special handling in `KeymapFile::update_keybinding`
for removals. If the binding being removed is the last in a keymap
section, the keymap section will be removed entirely instead of left
empty.

Still to do is the ability to unbind/remove non-user created bindings
such as those in the default keymap by binding them to `NoAction`,
however, this will be done in a follow up PR.

Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-07-10 23:23:26 +00:00
Max Brunsfeld
33f1ac8b34 Use installed trusted signing (#34245)
Fixes windows nightly build failures

Release Notes:

- N/A
2025-07-10 15:09:31 -07:00
Kirill Bulatov
a1188848ef Make inline and regular diagnostics more related (#34237)
Release Notes:

- N/A
2025-07-10 21:11:49 +00:00
Max Brunsfeld
7588280915 Windows screen sharing (#34223)
Release Notes:

- N/A

---------

Co-authored-by: localcc <work@localcc.cc>
Co-authored-by: Peter Tripp <petertripp@gmail.com>
2025-07-10 14:02:00 -07:00
Peter Tripp
f82fdaa0a4 ci: Skip ci.yml checks for script/update_top_ranking_issues (#34241)
Closes https://github.com/zed-industries/zed/issues/19433
Supercedes: https://github.com/zed-industries/zed/pull/33308

cc: @eshasantosh

Release Notes:

- N/A
2025-07-10 21:01:34 +00:00
Finn Evers
41085f8f55 settings_ui: Inform about keybind conflicts in modal (#34205)
This PR updates the keybinding editor modal so that conflicts are
already shown in the modal itself. Notably, this does not add validation
on every keystroke, the update still has to be confirmed. However, if
only a warning is present, on the second confirm the keybind will
actually be updated.

The change also includes a slight update to the displayment of errors,
since we now differentiate between errors and warnings.

| Error | Warning | 
| --- | --- |
| <img width="543" height="332" alt="warning_keybind"
src="https://github.com/user-attachments/assets/867319be-eeb9-40d7-bf32-fbd44aacf0b5"
/> | <img width="543" height="310" alt="error_keybind"
src="https://github.com/user-attachments/assets/858a6c7c-8c9a-4a90-95af-a5103125676f"
/> |


Release Notes:

- N/A
2025-07-10 15:51:36 -05:00
Julia Ryan
8e1d341d09 nix: Fix CI job (#34231)
Fix regex filter

Release Notes:

- N/A
2025-07-10 20:17:49 +00:00
Danilo Leal
9d2b7c8033 agent: Dismiss the agent panel notification if window is closed (#34230)
Closes https://github.com/zed-industries/zed/issues/32951

Release Notes:

- agent: Fixed an issue where the agent panel notification would linger
on even after you closed the window.
2025-07-10 16:45:17 -03:00
Kirill Bulatov
c6603e4fba Stop extensions' servers and message loops before removing their files (#34208)
Fixes an issue that caused Windows to fail when removing extension's
directories, as Zed had never stop any related processes.

Now:

* Zed shuts down and waits until the end when the language servers are
shut down

* Adds `impl Drop for WasmExtension` where does
`self.tx.close_channel();` to stop a receiver loop that holds the "lock"
on the extension's work dir.
The extension was dropped, but the channel was not closed for some
reason.

* Does more unregistration to ensure `Arc<WasmExtension>` with the `tx`
does not leak further

* Tidies up the related errors which had never reported a problematic
path before

Release Notes:

- N/A

---------

Co-authored-by: Smit Barmase <heysmitbarmase@gmail.com>
Co-authored-by: Smit <smit@zed.dev>
2025-07-10 19:25:10 +00:00
localcc
c549b712fd Just Zed instead of Zed Editor (#34146)
Release Notes:

- N/A
2025-07-10 21:08:43 +02:00
Finn Evers
d6bff274eb settings_ui: Ensure selected keymap entry is properly updated (#34229)
This change ensures that we more reliably deploy the context menu in the
keymap editor as well as highlight the selected row quicker.

Release Notes:

- N/A
2025-07-10 17:49:52 +00:00
Marshall Bowers
cfc9cfa4ab language_models: Refresh the list of models when the LLM token is refreshed (#34222)
This PR makes it so we refresh the list of models whenever the LLM token
is refreshed.

This allows us to add or remove models based on the plan in the new
token.

Release Notes:

- Fixed model list not refreshing when subscribing to Zed Pro.

---------

Co-authored-by: Bennet Bo Fenner <bennetbo@gmx.de>
2025-07-10 17:05:41 +00:00
Michael Sloan
e2e529bd94 Remove redundant autoscroll_horizontally during editor prepaint (#34218)
* Experimentally `scroll_manager.anchor()` appears to be the same before
and after this 2nd call of `autoscroll_horizontally`

* Nothing these depend on seem to be mutated between the calls (and
since this is prepaint, stuff within editor also shouldn't be mutated)

Release Notes:

- N/A

Co-authored-by: Finn <finn@zed.dev>
2025-07-10 11:03:34 -06:00
Joseph T. Lyons
e6c41b577b Add more admin to seed script (#34220)
Release Notes:

- N/A
2025-07-10 16:54:00 +00:00
localcc
f8f827583d Remove default shell breadcrumbs on windows (#34198)
Release Notes:

- N/A
2025-07-10 18:04:13 +02:00
Nathan Brodin
36c325bc60 docs: Add GitHub Copilot Enterprise configuration example (#33902)
### Context

This PR adds documentation for setting up GitHub Copilot Enterprise as
an edit prediction provider in Zed.
There was previously no documentation for this feature, which was
implemented in [PR
#32296](https://github.com/zed-industries/zed/pull/32296).

This follows up on [my
comment](https://github.com/zed-industries/zed/issues/22901#issuecomment-3034817471)
and the response from the[ Zed
team](https://github.com/zed-industries/zed/issues/22901#issuecomment-3034837282),
which clarified the required settings.

### What’s included

- Documents the `enterprise_uri` setting for Copilot Enterprise in
`edit-prediction.md`.
- Explains how to configure the setting and what to expect from the
sign-in flow.

### Notes

- This is a documentation-only change.
- No code or tests are affected.

Release Notes:

- N/A

---------

Co-authored-by: Peter Tripp <peter@zed.dev>
2025-07-10 15:52:31 +00:00
Marshall Bowers
f4106ad404 collab: Send down new usage limits even when the user does not have any usage (#34217)
This PR fixes an issue where the plan usage limits in Zed would not get
updated immediately after the plan has changed.

Previously we were only sending down the usage—which contains the
limits—if there was a usage record in the database. This would be absent
if the user had just changed their plan.

We now always send down the usage in order to update the limits on the
client side.

Release Notes:

- N/A

Co-authored-by: Bennet Bo Fenner <bennetbo@gmx.de>
2025-07-10 15:52:19 +00:00
Finn Evers
1583dd2d6f settings_ui: Ensure context menu is properly populated (#34213)
This change fixes a small issue where the right-click context menu would
not be populdated on the first right click in the keymap editor and the
selection of the corresponding entry would be slightly delayed.

Release Notes:

- N/A
2025-07-10 15:15:29 +00:00
Finn Evers
d7fd9245cd settings_ui: Open keybinding editing modal on mouse double click (#34193)
Whilst working on the keymap editor, I regularly find myself
double-clicking an entry just to find that nothing happens besides
selecting the given entry. This feels really unintuitive to me. I
checked back with VSCode and they also open the modal when
double-clicking an entry in the list.

Thus, this PR enables double-clicking an entry in the list to open the
editing modal.

Release Notes:

- N/A
2025-07-10 17:01:36 +02:00
Justin Su
5f21a9bd32 Uncomment default settings values (#34179)
Closes https://github.com/zed-industries/zed/issues/34178

Release Notes:

- N/A

---------

Co-authored-by: Peter Tripp <peter@zed.dev>
2025-07-10 14:41:10 +00:00
Peter Tripp
c30e28179a Preserve agent message editor unsent text (#34150)
Closes https://github.com/zed-industries/zed/issues/33687

Release Notes:

- agent: Preserve unsent chat message text when creating a new thread
2025-07-10 10:35:16 -04:00
Kirill Bulatov
8bc1396a55 Suggest powershell extension (#34211)
Release Notes:

- N/A
2025-07-10 14:34:59 +00:00
Danilo Leal
51c24e2010 Reorder items in the quick action bar (#34203)
Namely, putting the diagnostics items in their own little section,
divider from the other "inline" and minimap/edit prediction items. I
feel like this is an easier to parse organization, even though all the
"inlines" made sense to be somewhat close together.

Release Notes:

- N/A
2025-07-10 12:49:19 +00:00
张小白
3169f06404 windows: Don't show cmd window when remoting (#34187)
Release Notes:

- N/A
2025-07-10 09:19:12 +00:00
184 changed files with 8914 additions and 3254 deletions

View File

@@ -1,64 +0,0 @@
name: "Trusted Signing on Windows"
description: "Install trusted signing on Windows."
# Modified from https://github.com/Azure/trusted-signing-action
runs:
using: "composite"
steps:
- name: Set variables
id: set-variables
shell: "pwsh"
run: |
$defaultPath = $env:PSModulePath -split ';' | Select-Object -First 1
"PSMODULEPATH=$defaultPath" | Out-File -FilePath $env:GITHUB_OUTPUT -Append
"TRUSTED_SIGNING_MODULE_VERSION=0.5.3" | Out-File -FilePath $env:GITHUB_OUTPUT -Append
"BUILD_TOOLS_NUGET_VERSION=10.0.22621.3233" | Out-File -FilePath $env:GITHUB_OUTPUT -Append
"TRUSTED_SIGNING_NUGET_VERSION=1.0.53" | Out-File -FilePath $env:GITHUB_OUTPUT -Append
"DOTNET_SIGNCLI_NUGET_VERSION=0.9.1-beta.24469.1" | Out-File -FilePath $env:GITHUB_OUTPUT -Append
- name: Cache TrustedSigning PowerShell module
id: cache-module
uses: actions/cache@v4
env:
cache-name: cache-module
with:
path: ${{ steps.set-variables.outputs.PSMODULEPATH }}\TrustedSigning\${{ steps.set-variables.outputs.TRUSTED_SIGNING_MODULE_VERSION }}
key: TrustedSigning-${{ steps.set-variables.outputs.TRUSTED_SIGNING_MODULE_VERSION }}
if: ${{ inputs.cache-dependencies == 'true' }}
- name: Cache Microsoft.Windows.SDK.BuildTools NuGet package
id: cache-buildtools
uses: actions/cache@v4
env:
cache-name: cache-buildtools
with:
path: ~\AppData\Local\TrustedSigning\Microsoft.Windows.SDK.BuildTools\Microsoft.Windows.SDK.BuildTools.${{ steps.set-variables.outputs.BUILD_TOOLS_NUGET_VERSION }}
key: Microsoft.Windows.SDK.BuildTools-${{ steps.set-variables.outputs.BUILD_TOOLS_NUGET_VERSION }}
if: ${{ inputs.cache-dependencies == 'true' }}
- name: Cache Microsoft.Trusted.Signing.Client NuGet package
id: cache-tsclient
uses: actions/cache@v4
env:
cache-name: cache-tsclient
with:
path: ~\AppData\Local\TrustedSigning\Microsoft.Trusted.Signing.Client\Microsoft.Trusted.Signing.Client.${{ steps.set-variables.outputs.TRUSTED_SIGNING_NUGET_VERSION }}
key: Microsoft.Trusted.Signing.Client-${{ steps.set-variables.outputs.TRUSTED_SIGNING_NUGET_VERSION }}
if: ${{ inputs.cache-dependencies == 'true' }}
- name: Cache SignCli NuGet package
id: cache-signcli
uses: actions/cache@v4
env:
cache-name: cache-signcli
with:
path: ~\AppData\Local\TrustedSigning\sign\sign.${{ steps.set-variables.outputs.DOTNET_SIGNCLI_NUGET_VERSION }}
key: SignCli-${{ steps.set-variables.outputs.DOTNET_SIGNCLI_NUGET_VERSION }}
if: ${{ inputs.cache-dependencies == 'true' }}
- name: Install Trusted Signing module
shell: "pwsh"
run: |
Install-Module -Name TrustedSigning -RequiredVersion ${{ steps.set-variables.outputs.TRUSTED_SIGNING_MODULE_VERSION }} -Force -Repository PSGallery
if: ${{ inputs.cache-dependencies != 'true' || steps.cache-module.outputs.cache-hit != 'true' }}

View File

@@ -1,12 +1,6 @@
name: "Run tests"
description: "Runs the tests"
inputs:
use-xvfb:
description: "Whether to run tests with xvfb"
required: false
default: "false"
runs:
using: "composite"
steps:
@@ -26,9 +20,4 @@ runs:
- name: Run tests
shell: bash -euxo pipefail {0}
run: |
if [ "${{ inputs.use-xvfb }}" == "true" ]; then
xvfb-run --auto-servernum --server-args="-screen 0 1024x768x24 -nolisten tcp" cargo nextest run --workspace --no-fail-fast
else
cargo nextest run --workspace --no-fail-fast
fi
run: cargo nextest run --workspace --no-fail-fast

View File

@@ -21,6 +21,9 @@ env:
CARGO_TERM_COLOR: always
CARGO_INCREMENTAL: 0
RUST_BACKTRACE: 1
DIGITALOCEAN_SPACES_ACCESS_KEY: ${{ secrets.DIGITALOCEAN_SPACES_ACCESS_KEY }}
DIGITALOCEAN_SPACES_SECRET_KEY: ${{ secrets.DIGITALOCEAN_SPACES_SECRET_KEY }}
ZED_CLIENT_CHECKSUM_SEED: ${{ secrets.ZED_CLIENT_CHECKSUM_SEED }}
jobs:
job_spec:
@@ -52,9 +55,10 @@ jobs:
fi
# Specify anything which should skip full CI in this regex:
# - docs/
# - script/update_top_ranking_issues/
# - .github/ISSUE_TEMPLATE/
# - .github/workflows/ (except .github/workflows/ci.yml)
SKIP_REGEX='^(docs/|\.github/(ISSUE_TEMPLATE|workflows/(?!ci)))'
SKIP_REGEX='^(docs/|script/update_top_ranking_issues/|\.github/(ISSUE_TEMPLATE|workflows/(?!ci)))'
if [[ $(git diff --name-only $COMPARE_REV ${{ github.sha }} | grep -vP "$SKIP_REGEX") ]]; then
echo "run_tests=true" >> $GITHUB_OUTPUT
else
@@ -71,7 +75,7 @@ jobs:
echo "run_license=false" >> $GITHUB_OUTPUT
fi
NIX_REGEX='^(nix/|flake\.|Cargo\.|rust-toolchain.toml|\.cargo/config.toml)'
if [[ $(git diff --name-only $COMPARE_REV ${{ github.sha }} | grep "$NIX_REGEX") ]]; then
if [[ $(git diff --name-only $COMPARE_REV ${{ github.sha }} | grep -P "$NIX_REGEX") ]]; then
echo "run_nix=true" >> $GITHUB_OUTPUT
else
echo "run_nix=false" >> $GITHUB_OUTPUT
@@ -334,8 +338,6 @@ jobs:
- name: Run tests
uses: ./.github/actions/run_tests
with:
use-xvfb: true
- name: Build other binaries and features
run: |
@@ -494,9 +496,6 @@ jobs:
APPLE_NOTARIZATION_KEY: ${{ secrets.APPLE_NOTARIZATION_KEY }}
APPLE_NOTARIZATION_KEY_ID: ${{ secrets.APPLE_NOTARIZATION_KEY_ID }}
APPLE_NOTARIZATION_ISSUER_ID: ${{ secrets.APPLE_NOTARIZATION_ISSUER_ID }}
ZED_CLIENT_CHECKSUM_SEED: ${{ secrets.ZED_CLIENT_CHECKSUM_SEED }}
DIGITALOCEAN_SPACES_ACCESS_KEY: ${{ secrets.DIGITALOCEAN_SPACES_ACCESS_KEY }}
DIGITALOCEAN_SPACES_SECRET_KEY: ${{ secrets.DIGITALOCEAN_SPACES_SECRET_KEY }}
steps:
- name: Install Node
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
@@ -579,10 +578,6 @@ jobs:
startsWith(github.ref, 'refs/tags/v')
|| contains(github.event.pull_request.labels.*.name, 'run-bundling')
needs: [linux_tests]
env:
ZED_CLIENT_CHECKSUM_SEED: ${{ secrets.ZED_CLIENT_CHECKSUM_SEED }}
DIGITALOCEAN_SPACES_ACCESS_KEY: ${{ secrets.DIGITALOCEAN_SPACES_ACCESS_KEY }}
DIGITALOCEAN_SPACES_SECRET_KEY: ${{ secrets.DIGITALOCEAN_SPACES_SECRET_KEY }}
steps:
- name: Checkout repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
@@ -636,10 +631,6 @@ jobs:
startsWith(github.ref, 'refs/tags/v')
|| contains(github.event.pull_request.labels.*.name, 'run-bundling')
needs: [linux_tests]
env:
ZED_CLIENT_CHECKSUM_SEED: ${{ secrets.ZED_CLIENT_CHECKSUM_SEED }}
DIGITALOCEAN_SPACES_ACCESS_KEY: ${{ secrets.DIGITALOCEAN_SPACES_ACCESS_KEY }}
DIGITALOCEAN_SPACES_SECRET_KEY: ${{ secrets.DIGITALOCEAN_SPACES_SECRET_KEY }}
steps:
- name: Checkout repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
@@ -692,16 +683,12 @@ jobs:
|| contains(github.event.pull_request.labels.*.name, 'run-bundling')
needs: [linux_tests]
name: Build Zed on FreeBSD
# env:
# MYTOKEN : ${{ secrets.MYTOKEN }}
# MYTOKEN2: "value2"
steps:
- uses: actions/checkout@v4
- name: Build FreeBSD remote-server
id: freebsd-build
uses: vmactions/freebsd-vm@c3ae29a132c8ef1924775414107a97cac042aad5 # v1.2.0
with:
# envs: "MYTOKEN MYTOKEN2"
usesh: true
release: 13.5
copyback: true
@@ -768,8 +755,6 @@ jobs:
ACCOUNT_NAME: ${{ vars.AZURE_SIGNING_ACCOUNT_NAME }}
CERT_PROFILE_NAME: ${{ vars.AZURE_SIGNING_CERT_PROFILE_NAME }}
ENDPOINT: ${{ vars.AZURE_SIGNING_ENDPOINT }}
DIGITALOCEAN_SPACES_ACCESS_KEY: ${{ secrets.DIGITALOCEAN_SPACES_ACCESS_KEY }}
DIGITALOCEAN_SPACES_SECRET_KEY: ${{ secrets.DIGITALOCEAN_SPACES_SECRET_KEY }}
FILE_DIGEST: SHA256
TIMESTAMP_DIGEST: SHA256
TIMESTAMP_SERVER: "http://timestamp.acs.microsoft.com"
@@ -786,9 +771,6 @@ jobs:
# This exports RELEASE_CHANNEL into env (GITHUB_ENV)
script/determine-release-channel.ps1
- name: Install trusted signing
uses: ./.github/actions/install_trusted_signing
- name: Build Zed installer
working-directory: ${{ env.ZED_WORKSPACE }}
run: script/bundle-windows.ps1

View File

@@ -12,6 +12,9 @@ env:
CARGO_TERM_COLOR: always
CARGO_INCREMENTAL: 0
RUST_BACKTRACE: 1
ZED_CLIENT_CHECKSUM_SEED: ${{ secrets.ZED_CLIENT_CHECKSUM_SEED }}
DIGITALOCEAN_SPACES_ACCESS_KEY: ${{ secrets.DIGITALOCEAN_SPACES_ACCESS_KEY }}
DIGITALOCEAN_SPACES_SECRET_KEY: ${{ secrets.DIGITALOCEAN_SPACES_SECRET_KEY }}
jobs:
style:
@@ -91,9 +94,6 @@ jobs:
APPLE_NOTARIZATION_KEY: ${{ secrets.APPLE_NOTARIZATION_KEY }}
APPLE_NOTARIZATION_KEY_ID: ${{ secrets.APPLE_NOTARIZATION_KEY_ID }}
APPLE_NOTARIZATION_ISSUER_ID: ${{ secrets.APPLE_NOTARIZATION_ISSUER_ID }}
DIGITALOCEAN_SPACES_ACCESS_KEY: ${{ secrets.DIGITALOCEAN_SPACES_ACCESS_KEY }}
DIGITALOCEAN_SPACES_SECRET_KEY: ${{ secrets.DIGITALOCEAN_SPACES_SECRET_KEY }}
ZED_CLIENT_CHECKSUM_SEED: ${{ secrets.ZED_CLIENT_CHECKSUM_SEED }}
steps:
- name: Install Node
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
@@ -125,10 +125,6 @@ jobs:
runs-on:
- buildjet-16vcpu-ubuntu-2004
needs: tests
env:
DIGITALOCEAN_SPACES_ACCESS_KEY: ${{ secrets.DIGITALOCEAN_SPACES_ACCESS_KEY }}
DIGITALOCEAN_SPACES_SECRET_KEY: ${{ secrets.DIGITALOCEAN_SPACES_SECRET_KEY }}
ZED_CLIENT_CHECKSUM_SEED: ${{ secrets.ZED_CLIENT_CHECKSUM_SEED }}
steps:
- name: Checkout repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
@@ -164,10 +160,6 @@ jobs:
runs-on:
- buildjet-16vcpu-ubuntu-2204-arm
needs: tests
env:
DIGITALOCEAN_SPACES_ACCESS_KEY: ${{ secrets.DIGITALOCEAN_SPACES_ACCESS_KEY }}
DIGITALOCEAN_SPACES_SECRET_KEY: ${{ secrets.DIGITALOCEAN_SPACES_SECRET_KEY }}
ZED_CLIENT_CHECKSUM_SEED: ${{ secrets.ZED_CLIENT_CHECKSUM_SEED }}
steps:
- name: Checkout repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
@@ -198,9 +190,6 @@ jobs:
if: github.repository_owner == 'zed-industries'
runs-on: github-8vcpu-ubuntu-2404
needs: tests
env:
DIGITALOCEAN_SPACES_ACCESS_KEY: ${{ secrets.DIGITALOCEAN_SPACES_ACCESS_KEY }}
DIGITALOCEAN_SPACES_SECRET_KEY: ${{ secrets.DIGITALOCEAN_SPACES_SECRET_KEY }}
name: Build Zed on FreeBSD
# env:
# MYTOKEN : ${{ secrets.MYTOKEN }}
@@ -257,8 +246,6 @@ jobs:
ACCOUNT_NAME: ${{ vars.AZURE_SIGNING_ACCOUNT_NAME }}
CERT_PROFILE_NAME: ${{ vars.AZURE_SIGNING_CERT_PROFILE_NAME }}
ENDPOINT: ${{ vars.AZURE_SIGNING_ENDPOINT }}
DIGITALOCEAN_SPACES_ACCESS_KEY: ${{ secrets.DIGITALOCEAN_SPACES_ACCESS_KEY }}
DIGITALOCEAN_SPACES_SECRET_KEY: ${{ secrets.DIGITALOCEAN_SPACES_SECRET_KEY }}
FILE_DIGEST: SHA256
TIMESTAMP_DIGEST: SHA256
TIMESTAMP_SERVER: "http://timestamp.acs.microsoft.com"
@@ -276,9 +263,6 @@ jobs:
Write-Host "Publishing version: $version on release channel nightly"
"nightly" | Set-Content -Path "crates/zed/RELEASE_CHANNEL"
- name: Install trusted signing
uses: ./.github/actions/install_trusted_signing
- name: Build Zed installer
working-directory: ${{ env.ZED_WORKSPACE }}
run: script/bundle-windows.ps1

View File

@@ -40,7 +40,7 @@
},
"file_types": {
"Dockerfile": ["Dockerfile*[!dockerignore]"],
"JSONC": ["assets/**/*.json", "renovate.json"],
"JSONC": ["**/assets/**/*.json", "renovate.json"],
"Git Ignore": ["dockerignore"]
},
"hard_tabs": false,

32
Cargo.lock generated
View File

@@ -9,6 +9,7 @@ dependencies = [
"agent_servers",
"agentic-coding-protocol",
"anyhow",
"assistant_tool",
"async-pipe",
"buffer_diff",
"editor",
@@ -263,9 +264,9 @@ dependencies = [
[[package]]
name = "agentic-coding-protocol"
version = "0.0.6"
version = "0.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d1ac0351749af7bf53c65042ef69fefb9351aa8b7efa0a813d6281377605c37d"
checksum = "a75f520bcc049ebe40c8c99427aa61b48ad78a01bcc96a13b350b903dcfb9438"
dependencies = [
"anyhow",
"chrono",
@@ -3108,6 +3109,7 @@ dependencies = [
"context_server",
"ctor",
"dap",
"dap-types",
"dap_adapters",
"dashmap 6.1.0",
"debugger_ui",
@@ -4391,12 +4393,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",
@@ -8978,6 +8983,7 @@ dependencies = [
"gpui",
"language",
"lsp",
"project",
"serde",
"serde_json",
"util",
@@ -9030,7 +9036,6 @@ dependencies = [
"credentials_provider",
"deepseek",
"editor",
"feature_flags",
"fs",
"futures 0.3.31",
"google_ai",
@@ -9657,12 +9662,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",
]
@@ -11274,6 +11278,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"
@@ -12317,6 +12330,7 @@ dependencies = [
"anyhow",
"askpass",
"async-trait",
"base64 0.22.1",
"buffer_diff",
"circular-buffer",
"client",
@@ -12362,6 +12376,7 @@ dependencies = [
"sha2",
"shellexpand 2.1.2",
"shlex",
"smallvec",
"smol",
"snippet",
"snippet_provider",
@@ -14096,7 +14111,7 @@ dependencies = [
[[package]]
name = "scap"
version = "0.0.8"
source = "git+https://github.com/zed-industries/scap?rev=08f0a01417505cc0990b9931a37e5120db92e0d0#08f0a01417505cc0990b9931a37e5120db92e0d0"
source = "git+https://github.com/zed-industries/scap?rev=270538dc780f5240723233ff901e1054641ed318#270538dc780f5240723233ff901e1054641ed318"
dependencies = [
"anyhow",
"cocoa 0.25.0",
@@ -14674,6 +14689,7 @@ dependencies = [
"schemars",
"search",
"serde",
"serde_json",
"settings",
"theme",
"tree-sitter-json",
@@ -18373,7 +18389,6 @@ version = "0.1.0"
dependencies = [
"anyhow",
"client",
"feature_flags",
"futures 0.3.31",
"gpui",
"http_client",
@@ -19645,6 +19660,7 @@ dependencies = [
"rustix 1.0.7",
"rustls 0.23.26",
"rustls-webpki 0.103.1",
"scap",
"schemars",
"scopeguard",
"sea-orm",
@@ -19692,7 +19708,9 @@ 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",

View File

@@ -404,7 +404,7 @@ zlog_settings = { path = "crates/zlog_settings" }
# External crates
#
agentic-coding-protocol = "0.0.6"
agentic-coding-protocol = "0.0.7"
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 +492,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 +507,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 +547,7 @@ 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 = "08f0a01417505cc0990b9931a37e5120db92e0d0", default-features = false }
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"] }

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

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

@@ -320,7 +320,8 @@
"bindings": {
"enter": "agent::Chat",
"up": "agent::PreviousHistoryMessage",
"down": "agent::NextHistoryMessage"
"down": "agent::NextHistoryMessage",
"shift-ctrl-r": "agent::OpenAgentDiff"
}
},
{
@@ -855,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",
@@ -997,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"
}
@@ -1112,7 +1115,10 @@
"context": "KeymapEditor",
"use_key_equivalents": true,
"bindings": {
"ctrl-f": "search::FocusSearch"
"ctrl-f": "search::FocusSearch",
"alt-find": "keymap_editor::ToggleKeystrokeSearch",
"alt-ctrl-f": "keymap_editor::ToggleKeystrokeSearch",
"alt-c": "keymap_editor::ToggleConflictFilter"
}
}
]

View File

@@ -371,7 +371,8 @@
"bindings": {
"enter": "agent::Chat",
"up": "agent::PreviousHistoryMessage",
"down": "agent::NextHistoryMessage"
"down": "agent::NextHistoryMessage",
"shift-ctrl-r": "agent::OpenAgentDiff"
}
},
{
@@ -929,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",
@@ -1096,6 +1098,7 @@
"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",
@@ -1211,7 +1214,8 @@
"context": "KeymapEditor",
"use_key_equivalents": true,
"bindings": {
"cmd-f": "search::FocusSearch"
"cmd-alt-f": "keymap_editor::ToggleKeystrokeSearch",
"cmd-alt-c": "keymap_editor::ToggleConflictFilter"
}
}
]

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

@@ -1135,6 +1135,7 @@
"**/.svn",
"**/.hg",
"**/.jj",
"**/.repo",
"**/CVS",
"**/.DS_Store",
"**/Thumbs.db",
@@ -1157,16 +1158,14 @@
// Control whether the git blame information is shown inline,
// in the currently focused line.
"inline_blame": {
"enabled": true
"enabled": true,
// Sets a delay after which the inline blame information is shown.
// Delay is restarted with every cursor movement.
// "delay_ms": 600
//
"delay_ms": 0,
// Whether or not to display the git commit summary on the same line.
// "show_commit_summary": false
//
"show_commit_summary": false,
// The minimum column number to show the inline blame information at
// "min_column": 0
"min_column": 0
},
// How git hunks are displayed visually in the editor.
// This setting can take two values:
@@ -1379,11 +1378,11 @@
// This will be merged with the platform's default font fallbacks
// "font_fallbacks": ["FiraCode Nerd Fonts"],
// The weight of the editor font in standard CSS units from 100 to 900.
// "font_weight": 400
"font_weight": 400,
// Sets the maximum number of lines in the terminal's scrollback buffer.
// Default: 10_000, maximum: 100_000 (all bigger values set will be treated as 100_000), 0 disables the scrolling.
// Existing terminals will not pick up this change until they are recreated.
// "max_scroll_history_lines": 10000,
"max_scroll_history_lines": 10000,
// The minimum APCA perceptual contrast between foreground and background colors.
// APCA (Accessible Perceptual Contrast Algorithm) is more accurate than WCAG 2.x,
// especially for dark mode. Values range from 0 to 106.

View File

@@ -20,6 +20,7 @@ gemini = []
agent_servers.workspace = true
agentic-coding-protocol.workspace = true
anyhow.workspace = true
assistant_tool.workspace = true
buffer_diff.workspace = true
editor.workspace = true
futures.workspace = true

View File

@@ -2,14 +2,19 @@ pub use acp::ToolCallId;
use agent_servers::AgentServer;
use agentic_coding_protocol::{self as acp, UserMessageChunk};
use anyhow::{Context as _, Result, anyhow};
use assistant_tool::ActionLog;
use buffer_diff::BufferDiff;
use editor::{MultiBuffer, PathKey};
use futures::{FutureExt, channel::oneshot, future::BoxFuture};
use gpui::{AppContext, AsyncApp, Context, Entity, EventEmitter, SharedString, Task, WeakEntity};
use itertools::Itertools;
use language::{Anchor, Buffer, Capability, LanguageRegistry, OffsetRangeExt as _};
use language::{
Anchor, Buffer, BufferSnapshot, Capability, LanguageRegistry, OffsetRangeExt as _, Point,
text_diff,
};
use markdown::Markdown;
use project::Project;
use project::{AgentLocation, Project};
use std::collections::HashMap;
use std::error::Error;
use std::fmt::{Formatter, Write};
use std::{
@@ -159,6 +164,26 @@ impl AgentThreadEntry {
Self::ToolCall(too_call) => too_call.to_markdown(cx),
}
}
pub fn diff(&self) -> Option<&Diff> {
if let AgentThreadEntry::ToolCall(ToolCall {
content: Some(ToolCallContent::Diff { diff }),
..
}) = self
{
Some(&diff)
} else {
None
}
}
pub fn locations(&self) -> Option<&[acp::ToolCallLocation]> {
if let AgentThreadEntry::ToolCall(ToolCall { locations, .. }) = self {
Some(locations)
} else {
None
}
}
}
#[derive(Debug)]
@@ -168,6 +193,7 @@ pub struct ToolCall {
pub icon: IconName,
pub content: Option<ToolCallContent>,
pub status: ToolCallStatus,
pub locations: Vec<acp::ToolCallLocation>,
}
impl ToolCall {
@@ -328,6 +354,8 @@ impl ToolCallContent {
pub struct Diff {
pub multibuffer: Entity<MultiBuffer>,
pub path: PathBuf,
pub new_buffer: Entity<Buffer>,
pub old_buffer: Entity<Buffer>,
_task: Task<Result<()>>,
}
@@ -362,6 +390,7 @@ impl Diff {
let task = cx.spawn({
let multibuffer = multibuffer.clone();
let path = path.clone();
let new_buffer = new_buffer.clone();
async move |cx| {
diff_task.await?;
@@ -401,6 +430,8 @@ impl Diff {
Self {
multibuffer,
path,
new_buffer,
old_buffer,
_task: task,
}
}
@@ -421,6 +452,8 @@ pub struct AcpThread {
entries: Vec<AgentThreadEntry>,
title: SharedString,
project: Entity<Project>,
action_log: Entity<ActionLog>,
shared_buffers: HashMap<Entity<Buffer>, BufferSnapshot>,
send_task: Option<Task<()>>,
connection: Arc<acp::AgentConnection>,
child_status: Option<Task<Result<()>>>,
@@ -522,7 +555,11 @@ impl AcpThread {
}
});
let action_log = cx.new(|_| ActionLog::new(project.clone()));
Self {
action_log,
shared_buffers: Default::default(),
entries: Default::default(),
title: "ACP Thread".into(),
project,
@@ -534,6 +571,14 @@ impl AcpThread {
})
}
pub fn action_log(&self) -> &Entity<ActionLog> {
&self.action_log
}
pub fn project(&self) -> &Entity<Project> {
&self.project
}
#[cfg(test)]
pub fn fake(
stdin: async_pipe::PipeWriter,
@@ -558,7 +603,11 @@ impl AcpThread {
}
});
let action_log = cx.new(|_| ActionLog::new(project.clone()));
Self {
action_log,
shared_buffers: Default::default(),
entries: Default::default(),
title: "ACP Thread".into(),
project,
@@ -589,6 +638,26 @@ impl AcpThread {
}
}
pub fn has_pending_edit_tool_calls(&self) -> bool {
for entry in self.entries.iter().rev() {
match entry {
AgentThreadEntry::UserMessage(_) => return false,
AgentThreadEntry::ToolCall(ToolCall {
status:
ToolCallStatus::Allowed {
status: acp::ToolCallStatus::Running,
..
},
content: Some(ToolCallContent::Diff { .. }),
..
}) => return true,
AgentThreadEntry::ToolCall(_) | AgentThreadEntry::AssistantMessage(_) => {}
}
}
false
}
pub fn push_entry(&mut self, entry: AgentThreadEntry, cx: &mut Context<Self>) {
self.entries.push(entry);
cx.emit(AcpThreadEvent::NewEntry);
@@ -644,65 +713,63 @@ impl AcpThread {
pub fn request_tool_call(
&mut self,
label: String,
icon: acp::Icon,
content: Option<acp::ToolCallContent>,
confirmation: acp::ToolCallConfirmation,
tool_call: acp::RequestToolCallConfirmationParams,
cx: &mut Context<Self>,
) -> ToolCallRequest {
let (tx, rx) = oneshot::channel();
let status = ToolCallStatus::WaitingForConfirmation {
confirmation: ToolCallConfirmation::from_acp(
confirmation,
tool_call.confirmation,
self.project.read(cx).languages().clone(),
cx,
),
respond_tx: tx,
};
let id = self.insert_tool_call(label, status, icon, content, cx);
let id = self.insert_tool_call(tool_call.tool_call, status, cx);
ToolCallRequest { id, outcome: rx }
}
pub fn push_tool_call(
&mut self,
label: String,
icon: acp::Icon,
content: Option<acp::ToolCallContent>,
request: acp::PushToolCallParams,
cx: &mut Context<Self>,
) -> acp::ToolCallId {
let status = ToolCallStatus::Allowed {
status: acp::ToolCallStatus::Running,
};
self.insert_tool_call(label, status, icon, content, cx)
self.insert_tool_call(request, status, cx)
}
fn insert_tool_call(
&mut self,
label: String,
tool_call: acp::PushToolCallParams,
status: ToolCallStatus,
icon: acp::Icon,
content: Option<acp::ToolCallContent>,
cx: &mut Context<Self>,
) -> acp::ToolCallId {
let language_registry = self.project.read(cx).languages().clone();
let id = acp::ToolCallId(self.entries.len() as u64);
self.push_entry(
AgentThreadEntry::ToolCall(ToolCall {
id,
label: cx.new(|cx| {
Markdown::new(label.into(), Some(language_registry.clone()), None, cx)
}),
icon: acp_icon_to_ui_icon(icon),
content: content
.map(|content| ToolCallContent::from_acp(content, language_registry, cx)),
status,
let call = ToolCall {
id,
label: cx.new(|cx| {
Markdown::new(
tool_call.label.into(),
Some(language_registry.clone()),
None,
cx,
)
}),
cx,
);
icon: acp_icon_to_ui_icon(tool_call.icon),
content: tool_call
.content
.map(|content| ToolCallContent::from_acp(content, language_registry, cx)),
locations: tool_call.locations,
status,
};
self.push_entry(AgentThreadEntry::ToolCall(call), cx);
id
}
@@ -804,14 +871,16 @@ impl AcpThread {
false
}
pub fn initialize(&self) -> impl use<> + Future<Output = Result<acp::InitializeResponse>> {
pub fn initialize(
&self,
) -> impl use<> + Future<Output = Result<acp::InitializeResponse, acp::Error>> {
let connection = self.connection.clone();
async move { Ok(connection.request(acp::InitializeParams).await?) }
async move { connection.request(acp::InitializeParams).await }
}
pub fn authenticate(&self) -> impl use<> + Future<Output = Result<()>> {
pub fn authenticate(&self) -> impl use<> + Future<Output = Result<(), acp::Error>> {
let connection = self.connection.clone();
async move { Ok(connection.request(acp::AuthenticateParams).await?) }
async move { connection.request(acp::AuthenticateParams).await }
}
#[cfg(test)]
@@ -819,7 +888,7 @@ impl AcpThread {
&mut self,
message: &str,
cx: &mut Context<Self>,
) -> BoxFuture<'static, Result<()>> {
) -> BoxFuture<'static, Result<(), acp::Error>> {
self.send(
acp::SendUserMessageParams {
chunks: vec![acp::UserMessageChunk::Text {
@@ -834,7 +903,7 @@ impl AcpThread {
&mut self,
message: acp::SendUserMessageParams,
cx: &mut Context<Self>,
) -> BoxFuture<'static, Result<()>> {
) -> BoxFuture<'static, Result<(), acp::Error>> {
let agent = self.connection.clone();
self.push_entry(
AgentThreadEntry::UserMessage(UserMessage::from_acp(
@@ -865,7 +934,7 @@ impl AcpThread {
.boxed()
}
pub fn cancel(&mut self, cx: &mut Context<Self>) -> Task<Result<()>> {
pub fn cancel(&mut self, cx: &mut Context<Self>) -> Task<Result<(), acp::Error>> {
let agent = self.connection.clone();
if self.send_task.take().is_some() {
@@ -898,13 +967,123 @@ impl AcpThread {
}
}
}
})
})?;
Ok(())
})
} else {
Task::ready(Ok(()))
}
}
pub fn read_text_file(
&self,
request: acp::ReadTextFileParams,
cx: &mut Context<Self>,
) -> Task<Result<String>> {
let project = self.project.clone();
let action_log = self.action_log.clone();
cx.spawn(async move |this, cx| {
let load = project.update(cx, |project, cx| {
let path = project
.project_path_for_absolute_path(&request.path, cx)
.context("invalid path")?;
anyhow::Ok(project.open_buffer(path, cx))
});
let buffer = load??.await?;
action_log.update(cx, |action_log, cx| {
action_log.buffer_read(buffer.clone(), cx);
})?;
project.update(cx, |project, cx| {
let position = buffer
.read(cx)
.snapshot()
.anchor_before(Point::new(request.line.unwrap_or_default(), 0));
project.set_agent_location(
Some(AgentLocation {
buffer: buffer.downgrade(),
position,
}),
cx,
);
})?;
let snapshot = buffer.update(cx, |buffer, _| buffer.snapshot())?;
this.update(cx, |this, _| {
let text = snapshot.text();
this.shared_buffers.insert(buffer.clone(), snapshot);
text
})
})
}
pub fn write_text_file(
&self,
path: PathBuf,
content: String,
cx: &mut Context<Self>,
) -> Task<Result<()>> {
let project = self.project.clone();
let action_log = self.action_log.clone();
cx.spawn(async move |this, cx| {
let load = project.update(cx, |project, cx| {
let path = project
.project_path_for_absolute_path(&path, cx)
.context("invalid path")?;
anyhow::Ok(project.open_buffer(path, cx))
});
let buffer = load??.await?;
let snapshot = this.update(cx, |this, cx| {
this.shared_buffers
.get(&buffer)
.cloned()
.unwrap_or_else(|| buffer.read(cx).snapshot())
})?;
let edits = cx
.background_executor()
.spawn(async move {
let old_text = snapshot.text();
text_diff(old_text.as_str(), &content)
.into_iter()
.map(|(range, replacement)| {
(
snapshot.anchor_after(range.start)
..snapshot.anchor_before(range.end),
replacement,
)
})
.collect::<Vec<_>>()
})
.await;
cx.update(|cx| {
project.update(cx, |project, cx| {
project.set_agent_location(
Some(AgentLocation {
buffer: buffer.downgrade(),
position: edits
.last()
.map(|(range, _)| range.end)
.unwrap_or(Anchor::MIN),
}),
cx,
);
});
action_log.update(cx, |action_log, cx| {
action_log.buffer_read(buffer.clone(), cx);
});
buffer.update(cx, |buffer, cx| {
buffer.edit(edits, None, cx);
});
action_log.update(cx, |action_log, cx| {
action_log.buffer_edited(buffer.clone(), cx);
});
})?;
project
.update(cx, |project, cx| project.save_buffer(buffer, cx))?
.await
})
}
pub fn child_status(&mut self) -> Option<Task<Result<()>>> {
self.child_status.take()
}
@@ -930,7 +1109,7 @@ impl acp::Client for AcpClientDelegate {
async fn stream_assistant_message_chunk(
&self,
params: acp::StreamAssistantMessageChunkParams,
) -> Result<()> {
) -> Result<(), acp::Error> {
let cx = &mut self.cx.clone();
cx.update(|cx| {
@@ -947,45 +1126,37 @@ impl acp::Client for AcpClientDelegate {
async fn request_tool_call_confirmation(
&self,
request: acp::RequestToolCallConfirmationParams,
) -> Result<acp::RequestToolCallConfirmationResponse> {
) -> Result<acp::RequestToolCallConfirmationResponse, acp::Error> {
let cx = &mut self.cx.clone();
let ToolCallRequest { id, outcome } = cx
.update(|cx| {
self.thread.update(cx, |thread, cx| {
thread.request_tool_call(
request.label,
request.icon,
request.content,
request.confirmation,
cx,
)
})
self.thread
.update(cx, |thread, cx| thread.request_tool_call(request, cx))
})?
.context("Failed to update thread")?;
Ok(acp::RequestToolCallConfirmationResponse {
id,
outcome: outcome.await?,
outcome: outcome.await.map_err(acp::Error::into_internal_error)?,
})
}
async fn push_tool_call(
&self,
request: acp::PushToolCallParams,
) -> Result<acp::PushToolCallResponse> {
) -> Result<acp::PushToolCallResponse, acp::Error> {
let cx = &mut self.cx.clone();
let id = cx
.update(|cx| {
self.thread.update(cx, |thread, cx| {
thread.push_tool_call(request.label, request.icon, request.content, cx)
})
self.thread
.update(cx, |thread, cx| thread.push_tool_call(request, cx))
})?
.context("Failed to update thread")?;
Ok(acp::PushToolCallResponse { id })
}
async fn update_tool_call(&self, request: acp::UpdateToolCallParams) -> Result<()> {
async fn update_tool_call(&self, request: acp::UpdateToolCallParams) -> Result<(), acp::Error> {
let cx = &mut self.cx.clone();
cx.update(|cx| {
@@ -997,6 +1168,34 @@ impl acp::Client for AcpClientDelegate {
Ok(())
}
async fn read_text_file(
&self,
request: acp::ReadTextFileParams,
) -> Result<acp::ReadTextFileResponse, acp::Error> {
let content = self
.cx
.update(|cx| {
self.thread
.update(cx, |thread, cx| thread.read_text_file(request, cx))
})?
.context("Failed to update thread")?
.await?;
Ok(acp::ReadTextFileResponse { content })
}
async fn write_text_file(&self, request: acp::WriteTextFileParams) -> Result<(), acp::Error> {
self.cx
.update(|cx| {
self.thread.update(cx, |thread, cx| {
thread.write_text_file(request.path, request.content, cx)
})
})?
.context("Failed to update thread")?
.await?;
Ok(())
}
}
fn acp_icon_to_ui_icon(icon: acp::Icon) -> IconName {
@@ -1100,6 +1299,80 @@ mod tests {
);
}
#[gpui::test]
async fn test_edits_concurrently_to_user(cx: &mut TestAppContext) {
init_test(cx);
let fs = FakeFs::new(cx.executor());
fs.insert_tree(path!("/tmp"), json!({"foo": "one\ntwo\nthree\n"}))
.await;
let project = Project::test(fs.clone(), [], cx).await;
let (thread, fake_server) = fake_acp_thread(project.clone(), cx);
let (worktree, pathbuf) = project
.update(cx, |project, cx| {
project.find_or_create_worktree(path!("/tmp/foo"), true, cx)
})
.await
.unwrap();
let buffer = project
.update(cx, |project, cx| {
project.open_buffer((worktree.read(cx).id(), pathbuf), cx)
})
.await
.unwrap();
let (read_file_tx, read_file_rx) = oneshot::channel::<()>();
let read_file_tx = Rc::new(RefCell::new(Some(read_file_tx)));
fake_server.update(cx, |fake_server, _| {
fake_server.on_user_message(move |_, server, mut cx| {
let read_file_tx = read_file_tx.clone();
async move {
let content = server
.update(&mut cx, |server, _| {
server.send_to_zed(acp::ReadTextFileParams {
path: path!("/tmp/foo").into(),
line: None,
limit: None,
})
})?
.await
.unwrap();
assert_eq!(content.content, "one\ntwo\nthree\n");
read_file_tx.take().unwrap().send(()).unwrap();
server
.update(&mut cx, |server, _| {
server.send_to_zed(acp::WriteTextFileParams {
path: path!("/tmp/foo").into(),
content: "one\ntwo\nthree\nfour\nfive\n".to_string(),
})
})?
.await
.unwrap();
Ok(())
}
})
});
let request = thread.update(cx, |thread, cx| {
thread.send_raw("Extend the count in /tmp/foo", cx)
});
read_file_rx.await.ok();
buffer.update(cx, |buffer, cx| {
buffer.edit([(0..0, "zero\n".to_string())], None, cx);
});
cx.run_until_parked();
assert_eq!(
buffer.read_with(cx, |buffer, _| buffer.text()),
"zero\none\ntwo\nthree\nfour\nfive\n"
);
assert_eq!(
String::from_utf8(fs.read_file_sync(path!("/tmp/foo")).unwrap()).unwrap(),
"zero\none\ntwo\nthree\nfour\nfive\n"
);
request.await.unwrap();
}
#[gpui::test]
async fn test_succeeding_canceled_toolcall(cx: &mut TestAppContext) {
init_test(cx);
@@ -1124,6 +1397,7 @@ mod tests {
label: "Fetch".to_string(),
icon: acp::Icon::Globe,
content: None,
locations: vec![],
})
})?
.await
@@ -1553,7 +1827,7 @@ mod tests {
acp::SendUserMessageParams,
Entity<FakeAcpServer>,
AsyncApp,
) -> LocalBoxFuture<'static, Result<()>>,
) -> LocalBoxFuture<'static, Result<(), acp::Error>>,
>,
>,
}
@@ -1565,21 +1839,24 @@ mod tests {
}
impl acp::Agent for FakeAgent {
async fn initialize(&self) -> Result<acp::InitializeResponse> {
async fn initialize(&self) -> Result<acp::InitializeResponse, acp::Error> {
Ok(acp::InitializeResponse {
is_authenticated: true,
})
}
async fn authenticate(&self) -> Result<()> {
async fn authenticate(&self) -> Result<(), acp::Error> {
Ok(())
}
async fn cancel_send_message(&self) -> Result<()> {
async fn cancel_send_message(&self) -> Result<(), acp::Error> {
Ok(())
}
async fn send_user_message(&self, request: acp::SendUserMessageParams) -> Result<()> {
async fn send_user_message(
&self,
request: acp::SendUserMessageParams,
) -> Result<(), acp::Error> {
let mut cx = self.cx.clone();
let handler = self
.server
@@ -1589,7 +1866,7 @@ mod tests {
if let Some(handler) = handler {
handler(request, self.server.clone(), self.cx.clone()).await
} else {
anyhow::bail!("No handler for on_user_message")
Err(anyhow::anyhow!("No handler for on_user_message").into())
}
}
}
@@ -1624,7 +1901,7 @@ mod tests {
handler: impl for<'a> Fn(acp::SendUserMessageParams, Entity<FakeAcpServer>, AsyncApp) -> F
+ 'static,
) where
F: Future<Output = Result<()>> + 'static,
F: Future<Output = Result<(), acp::Error>> + 'static,
{
self.on_user_message
.replace(Rc::new(move |request, server, cx| {

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

@@ -2,4 +2,5 @@ mod completion_provider;
mod message_history;
mod thread_view;
pub use message_history::MessageHistory;
pub use thread_view::AcpThreadView;

View File

@@ -3,19 +3,25 @@ pub struct MessageHistory<T> {
current: Option<usize>,
}
impl<T> MessageHistory<T> {
pub fn new() -> Self {
impl<T> Default for MessageHistory<T> {
fn default() -> Self {
MessageHistory {
items: Vec::new(),
current: None,
}
}
}
impl<T> MessageHistory<T> {
pub fn push(&mut self, message: T) {
self.current.take();
self.items.push(message);
}
pub fn reset_position(&mut self) {
self.current.take();
}
pub fn prev(&mut self) -> Option<&T> {
if self.items.is_empty() {
return None;
@@ -46,7 +52,7 @@ mod tests {
#[test]
fn test_prev_next() {
let mut history = MessageHistory::new();
let mut history = MessageHistory::default();
// Test empty history
assert_eq!(history.prev(), None);

View File

@@ -1,33 +1,38 @@
use std::cell::RefCell;
use std::collections::BTreeMap;
use std::path::Path;
use std::rc::Rc;
use std::sync::Arc;
use std::time::Duration;
use agentic_coding_protocol::{self as acp};
use assistant_tool::ActionLog;
use buffer_diff::BufferDiff;
use collections::{HashMap, HashSet};
use editor::{
AnchorRangeExt, ContextMenuOptions, ContextMenuPlacement, Editor, EditorElement, EditorMode,
EditorStyle, MinimapVisibility, MultiBuffer,
EditorStyle, MinimapVisibility, MultiBuffer, PathKey,
};
use file_icons::FileIcons;
use futures::channel::oneshot;
use gpui::{
Animation, AnimationExt, App, BorderStyle, EdgesRefinement, Empty, Entity, EntityId, Focusable,
Hsla, Length, ListOffset, ListState, SharedString, StyleRefinement, Subscription, TextStyle,
TextStyleRefinement, Transformation, UnderlineStyle, WeakEntity, Window, div, list, percentage,
prelude::*, pulsating_between,
Action, Animation, AnimationExt, App, BorderStyle, EdgesRefinement, Empty, Entity, EntityId,
FocusHandle, Focusable, Hsla, Length, ListOffset, ListState, SharedString, StyleRefinement,
Subscription, Task, TextStyle, TextStyleRefinement, Transformation, UnderlineStyle, WeakEntity,
Window, div, linear_color_stop, linear_gradient, list, percentage, point, prelude::*,
pulsating_between,
};
use gpui::{FocusHandle, Task};
use language::language_settings::SoftWrap;
use language::{Buffer, Language};
use markdown::{HeadingLevelStyles, Markdown, MarkdownElement, MarkdownStyle};
use parking_lot::Mutex;
use project::Project;
use settings::Settings as _;
use text::Anchor;
use theme::ThemeSettings;
use ui::{Disclosure, Tooltip, prelude::*};
use ui::{Disclosure, Divider, DividerColor, KeyBinding, Tooltip, prelude::*};
use util::ResultExt;
use workspace::Workspace;
use workspace::{CollaboratorId, Workspace};
use zed_actions::agent::{Chat, NextHistoryMessage, PreviousHistoryMessage};
use ::acp::{
@@ -38,6 +43,8 @@ use ::acp::{
use crate::acp::completion_provider::{ContextPickerCompletionProvider, MentionSet};
use crate::acp::message_history::MessageHistory;
use crate::agent_diff::AgentDiff;
use crate::{AgentDiffPane, Follow, KeepAll, OpenAgentDiff, RejectAll};
const RESPONSE_PADDING_X: Pixels = px(19.);
@@ -47,13 +54,16 @@ pub struct AcpThreadView {
thread_state: ThreadState,
diff_editors: HashMap<EntityId, Entity<Editor>>,
message_editor: Entity<Editor>,
message_set_from_history: bool,
_message_editor_subscription: Subscription,
mention_set: Arc<Mutex<MentionSet>>,
last_error: Option<Entity<Markdown>>,
list_state: ListState,
auth_task: Option<Task<()>>,
expanded_tool_calls: HashSet<ToolCallId>,
expanded_thinking_blocks: HashSet<(usize, usize)>,
message_history: MessageHistory<acp::SendUserMessageParams>,
edits_expanded: bool,
message_history: Rc<RefCell<MessageHistory<acp::SendUserMessageParams>>>,
}
enum ThreadState {
@@ -62,7 +72,7 @@ enum ThreadState {
},
Ready {
thread: Entity<AcpThread>,
_subscription: Subscription,
_subscription: [Subscription; 2],
},
LoadError(LoadError),
Unauthenticated {
@@ -74,6 +84,7 @@ impl AcpThreadView {
pub fn new(
workspace: WeakEntity<Workspace>,
project: Entity<Project>,
message_history: Rc<RefCell<MessageHistory<acp::SendUserMessageParams>>>,
window: &mut Window,
cx: &mut Context<Self>,
) -> Self {
@@ -118,6 +129,17 @@ impl AcpThreadView {
editor
});
let message_editor_subscription = cx.subscribe(&message_editor, |this, _, event, _| {
if let editor::EditorEvent::BufferEdited = &event {
if !this.message_set_from_history {
this.message_history.borrow_mut().reset_position();
}
this.message_set_from_history = false;
}
});
let mention_set = mention_set.clone();
let list_state = ListState::new(
0,
gpui::ListAlignment::Bottom,
@@ -136,10 +158,12 @@ impl AcpThreadView {
);
Self {
workspace,
workspace: workspace.clone(),
project: project.clone(),
thread_state: Self::initial_state(project, window, cx),
thread_state: Self::initial_state(workspace, project, window, cx),
message_editor,
message_set_from_history: false,
_message_editor_subscription: message_editor_subscription,
mention_set,
diff_editors: Default::default(),
list_state: list_state,
@@ -147,11 +171,13 @@ impl AcpThreadView {
auth_task: None,
expanded_tool_calls: HashSet::default(),
expanded_thinking_blocks: HashSet::default(),
message_history: MessageHistory::new(),
edits_expanded: false,
message_history,
}
}
fn initial_state(
workspace: WeakEntity<Workspace>,
project: Entity<Project>,
window: &mut Window,
cx: &mut Context<Self>,
@@ -219,15 +245,23 @@ impl AcpThreadView {
this.update_in(cx, |this, window, cx| {
match result {
Ok(()) => {
let subscription =
let thread_subscription =
cx.subscribe_in(&thread, window, Self::handle_thread_event);
let action_log = thread.read(cx).action_log().clone();
let action_log_subscription =
cx.observe(&action_log, |_, _, cx| cx.notify());
this.list_state
.splice(0..0, thread.read(cx).entries().len());
AgentDiff::set_active_thread(&workspace, thread.clone(), window, cx);
this.thread_state = ThreadState::Ready {
thread,
_subscription: subscription,
_subscription: [thread_subscription, action_log_subscription],
};
cx.notify();
}
Err(err) => {
@@ -250,7 +284,7 @@ impl AcpThreadView {
cx.notify();
}
fn thread(&self) -> Option<&Entity<AcpThread>> {
pub fn thread(&self) -> Option<&Entity<AcpThread>> {
match &self.thread_state {
ThreadState::Ready { thread, .. } | ThreadState::Unauthenticated { thread } => {
Some(thread)
@@ -281,7 +315,6 @@ impl AcpThreadView {
let mut ix = 0;
let mut chunks: Vec<acp::UserMessageChunk> = Vec::new();
let project = self.project.clone();
self.message_editor.update(cx, |editor, cx| {
let text = editor.text(cx);
@@ -342,7 +375,7 @@ impl AcpThreadView {
editor.remove_creases(mention_set.lock().drain(), cx)
});
self.message_history.push(message);
self.message_history.borrow_mut().push(message);
}
fn previous_history_message(
@@ -351,11 +384,11 @@ impl AcpThreadView {
window: &mut Window,
cx: &mut Context<Self>,
) {
Self::set_draft_message(
self.message_set_from_history = Self::set_draft_message(
self.message_editor.clone(),
self.mention_set.clone(),
self.project.clone(),
self.message_history.prev(),
self.message_history.borrow_mut().prev(),
window,
cx,
);
@@ -367,11 +400,11 @@ impl AcpThreadView {
window: &mut Window,
cx: &mut Context<Self>,
) {
Self::set_draft_message(
self.message_set_from_history = Self::set_draft_message(
self.message_editor.clone(),
self.mention_set.clone(),
self.project.clone(),
self.message_history.next(),
self.message_history.borrow_mut().next(),
window,
cx,
);
@@ -384,15 +417,11 @@ impl AcpThreadView {
message: Option<&acp::SendUserMessageParams>,
window: &mut Window,
cx: &mut Context<Self>,
) {
) -> bool {
cx.notify();
let Some(message) = message else {
message_editor.update(cx, |editor, cx| {
editor.clear(window, cx);
editor.remove_creases(mention_set.lock().drain(), cx)
});
return;
return false;
};
let mut text = String::new();
@@ -452,6 +481,35 @@ impl AcpThreadView {
mention_set.lock().insert(crease_id, project_path);
}
}
true
}
fn open_agent_diff(&mut self, _: &OpenAgentDiff, window: &mut Window, cx: &mut Context<Self>) {
if let Some(thread) = self.thread() {
AgentDiffPane::deploy(thread.clone(), self.workspace.clone(), window, cx).log_err();
}
}
fn open_edited_buffer(
&mut self,
buffer: &Entity<Buffer>,
window: &mut Window,
cx: &mut Context<Self>,
) {
let Some(thread) = self.thread() else {
return;
};
let Some(diff) =
AgentDiffPane::deploy(thread.clone(), self.workspace.clone(), window, cx).log_err()
else {
return;
};
diff.update(cx, |diff, cx| {
diff.move_to_path(PathKey::for_buffer(&buffer, cx), window, cx)
})
}
fn handle_thread_event(
@@ -464,7 +522,8 @@ impl AcpThreadView {
let count = self.list_state.item_count();
match event {
AcpThreadEvent::NewEntry => {
self.sync_thread_entry_view(thread.read(cx).entries().len() - 1, window, cx);
let index = thread.read(cx).entries().len() - 1;
self.sync_thread_entry_view(index, window, cx);
self.list_state.splice(count..count, 1);
}
AcpThreadEvent::EntryUpdated(index) => {
@@ -537,15 +596,7 @@ impl AcpThreadView {
fn entry_diff_multibuffer(&self, entry_ix: usize, cx: &App) -> Option<Entity<MultiBuffer>> {
let entry = self.thread()?.read(cx).entries().get(entry_ix)?;
if let AgentThreadEntry::ToolCall(ToolCall {
content: Some(ToolCallContent::Diff { diff }),
..
}) = &entry
{
Some(diff.multibuffer.clone())
} else {
None
}
entry.diff().map(|diff| diff.multibuffer.clone())
}
fn authenticate(&mut self, window: &mut Window, cx: &mut Context<Self>) {
@@ -566,7 +617,8 @@ impl AcpThreadView {
Markdown::new(format!("Error: {err}").into(), None, None, cx)
}))
} else {
this.thread_state = Self::initial_state(project.clone(), window, cx)
this.thread_state =
Self::initial_state(this.workspace.clone(), project.clone(), window, cx)
}
this.auth_task.take()
})
@@ -873,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()
@@ -936,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),
}
}
@@ -1364,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()
@@ -1529,6 +1617,357 @@ impl AcpThreadView {
container.into_any()
}
fn render_edits_bar(
&self,
thread_entity: &Entity<AcpThread>,
window: &mut Window,
cx: &Context<Self>,
) -> Option<AnyElement> {
let thread = thread_entity.read(cx);
let action_log = thread.action_log();
let changed_buffers = action_log.read(cx).changed_buffers(cx);
if changed_buffers.is_empty() {
return None;
}
let editor_bg_color = cx.theme().colors().editor_background;
let active_color = cx.theme().colors().element_selected;
let bg_edit_files_disclosure = editor_bg_color.blend(active_color.opacity(0.3));
let pending_edits = thread.has_pending_edit_tool_calls();
let expanded = self.edits_expanded;
v_flex()
.mt_1()
.mx_2()
.bg(bg_edit_files_disclosure)
.border_1()
.border_b_0()
.border_color(cx.theme().colors().border)
.rounded_t_md()
.shadow(vec![gpui::BoxShadow {
color: gpui::black().opacity(0.15),
offset: point(px(1.), px(-1.)),
blur_radius: px(3.),
spread_radius: px(0.),
}])
.child(self.render_edits_bar_summary(
action_log,
&changed_buffers,
expanded,
pending_edits,
window,
cx,
))
.when(expanded, |parent| {
parent.child(self.render_edits_bar_files(
action_log,
&changed_buffers,
pending_edits,
cx,
))
})
.into_any()
.into()
}
fn render_edits_bar_summary(
&self,
action_log: &Entity<ActionLog>,
changed_buffers: &BTreeMap<Entity<Buffer>, Entity<BufferDiff>>,
expanded: bool,
pending_edits: bool,
window: &mut Window,
cx: &Context<Self>,
) -> Div {
const EDIT_NOT_READY_TOOLTIP_LABEL: &str = "Wait until file edits are complete.";
let focus_handle = self.focus_handle(cx);
h_flex()
.p_1()
.justify_between()
.when(expanded, |this| {
this.border_b_1().border_color(cx.theme().colors().border)
})
.child(
h_flex()
.id("edits-container")
.cursor_pointer()
.w_full()
.gap_1()
.child(Disclosure::new("edits-disclosure", expanded))
.map(|this| {
if pending_edits {
this.child(
Label::new(format!(
"Editing {} {}",
changed_buffers.len(),
if changed_buffers.len() == 1 {
"file"
} else {
"files"
}
))
.color(Color::Muted)
.size(LabelSize::Small)
.with_animation(
"edit-label",
Animation::new(Duration::from_secs(2))
.repeat()
.with_easing(pulsating_between(0.3, 0.7)),
|label, delta| label.alpha(delta),
),
)
} else {
this.child(
Label::new("Edits")
.size(LabelSize::Small)
.color(Color::Muted),
)
.child(Label::new("").size(LabelSize::XSmall).color(Color::Muted))
.child(
Label::new(format!(
"{} {}",
changed_buffers.len(),
if changed_buffers.len() == 1 {
"file"
} else {
"files"
}
))
.size(LabelSize::Small)
.color(Color::Muted),
)
}
})
.on_click(cx.listener(|this, _, _, cx| {
this.edits_expanded = !this.edits_expanded;
cx.notify();
})),
)
.child(
h_flex()
.gap_1()
.child(
IconButton::new("review-changes", IconName::ListTodo)
.icon_size(IconSize::Small)
.tooltip({
let focus_handle = focus_handle.clone();
move |window, cx| {
Tooltip::for_action_in(
"Review Changes",
&OpenAgentDiff,
&focus_handle,
window,
cx,
)
}
})
.on_click(cx.listener(|_, _, window, cx| {
window.dispatch_action(OpenAgentDiff.boxed_clone(), cx);
})),
)
.child(Divider::vertical().color(DividerColor::Border))
.child(
Button::new("reject-all-changes", "Reject All")
.label_size(LabelSize::Small)
.disabled(pending_edits)
.when(pending_edits, |this| {
this.tooltip(Tooltip::text(EDIT_NOT_READY_TOOLTIP_LABEL))
})
.key_binding(
KeyBinding::for_action_in(
&RejectAll,
&focus_handle.clone(),
window,
cx,
)
.map(|kb| kb.size(rems_from_px(10.))),
)
.on_click({
let action_log = action_log.clone();
cx.listener(move |_, _, _, cx| {
action_log.update(cx, |action_log, cx| {
action_log.reject_all_edits(cx).detach();
})
})
}),
)
.child(
Button::new("keep-all-changes", "Keep All")
.label_size(LabelSize::Small)
.disabled(pending_edits)
.when(pending_edits, |this| {
this.tooltip(Tooltip::text(EDIT_NOT_READY_TOOLTIP_LABEL))
})
.key_binding(
KeyBinding::for_action_in(&KeepAll, &focus_handle, window, cx)
.map(|kb| kb.size(rems_from_px(10.))),
)
.on_click({
let action_log = action_log.clone();
cx.listener(move |_, _, _, cx| {
action_log.update(cx, |action_log, cx| {
action_log.keep_all_edits(cx);
})
})
}),
),
)
}
fn render_edits_bar_files(
&self,
action_log: &Entity<ActionLog>,
changed_buffers: &BTreeMap<Entity<Buffer>, Entity<BufferDiff>>,
pending_edits: bool,
cx: &Context<Self>,
) -> Div {
let editor_bg_color = cx.theme().colors().editor_background;
v_flex().children(changed_buffers.into_iter().enumerate().flat_map(
|(index, (buffer, _diff))| {
let file = buffer.read(cx).file()?;
let path = file.path();
let file_path = path.parent().and_then(|parent| {
let parent_str = parent.to_string_lossy();
if parent_str.is_empty() {
None
} else {
Some(
Label::new(format!("/{}{}", parent_str, std::path::MAIN_SEPARATOR_STR))
.color(Color::Muted)
.size(LabelSize::XSmall)
.buffer_font(cx),
)
}
});
let file_name = path.file_name().map(|name| {
Label::new(name.to_string_lossy().to_string())
.size(LabelSize::XSmall)
.buffer_font(cx)
});
let file_icon = FileIcons::get_icon(&path, cx)
.map(Icon::from_path)
.map(|icon| icon.color(Color::Muted).size(IconSize::Small))
.unwrap_or_else(|| {
Icon::new(IconName::File)
.color(Color::Muted)
.size(IconSize::Small)
});
let overlay_gradient = linear_gradient(
90.,
linear_color_stop(editor_bg_color, 1.),
linear_color_stop(editor_bg_color.opacity(0.2), 0.),
);
let element = h_flex()
.group("edited-code")
.id(("file-container", index))
.relative()
.py_1()
.pl_2()
.pr_1()
.gap_2()
.justify_between()
.bg(editor_bg_color)
.when(index < changed_buffers.len() - 1, |parent| {
parent.border_color(cx.theme().colors().border).border_b_1()
})
.child(
h_flex()
.id(("file-name", index))
.pr_8()
.gap_1p5()
.max_w_full()
.overflow_x_scroll()
.child(file_icon)
.child(h_flex().gap_0p5().children(file_name).children(file_path))
.on_click({
let buffer = buffer.clone();
cx.listener(move |this, _, window, cx| {
this.open_edited_buffer(&buffer, window, cx);
})
}),
)
.child(
h_flex()
.gap_1()
.visible_on_hover("edited-code")
.child(
Button::new("review", "Review")
.label_size(LabelSize::Small)
.on_click({
let buffer = buffer.clone();
cx.listener(move |this, _, window, cx| {
this.open_edited_buffer(&buffer, window, cx);
})
}),
)
.child(Divider::vertical().color(DividerColor::BorderVariant))
.child(
Button::new("reject-file", "Reject")
.label_size(LabelSize::Small)
.disabled(pending_edits)
.on_click({
let buffer = buffer.clone();
let action_log = action_log.clone();
move |_, _, cx| {
action_log.update(cx, |action_log, cx| {
action_log
.reject_edits_in_ranges(
buffer.clone(),
vec![Anchor::MIN..Anchor::MAX],
cx,
)
.detach_and_log_err(cx);
})
}
}),
)
.child(
Button::new("keep-file", "Keep")
.label_size(LabelSize::Small)
.disabled(pending_edits)
.on_click({
let buffer = buffer.clone();
let action_log = action_log.clone();
move |_, _, cx| {
action_log.update(cx, |action_log, cx| {
action_log.keep_edits_in_range(
buffer.clone(),
Anchor::MIN..Anchor::MAX,
cx,
);
})
}
}),
),
)
.child(
div()
.id("gradient-overlay")
.absolute()
.h_full()
.w_12()
.top_0()
.bottom_0()
.right(px(152.))
.bg(overlay_gradient),
);
Some(element)
},
))
}
fn render_message_editor(&mut self, cx: &mut Context<Self>) -> AnyElement {
let settings = ThemeSettings::get_global(cx);
let font_size = TextSize::Small
@@ -1559,6 +1998,76 @@ impl AcpThreadView {
.into_any()
}
fn render_send_button(&self, cx: &mut Context<Self>) -> AnyElement {
if self.thread().map_or(true, |thread| {
thread.read(cx).status() == ThreadStatus::Idle
}) {
let is_editor_empty = self.message_editor.read(cx).is_empty(cx);
IconButton::new("send-message", IconName::Send)
.icon_color(Color::Accent)
.style(ButtonStyle::Filled)
.disabled(self.thread().is_none() || is_editor_empty)
.on_click(cx.listener(|this, _, window, cx| {
this.chat(&Chat, window, cx);
}))
.when(!is_editor_empty, |button| {
button.tooltip(move |window, cx| Tooltip::for_action("Send", &Chat, window, cx))
})
.when(is_editor_empty, |button| {
button.tooltip(Tooltip::text("Type a message to submit"))
})
.into_any_element()
} else {
IconButton::new("stop-generation", IconName::StopFilled)
.icon_color(Color::Error)
.style(ButtonStyle::Tinted(ui::TintColor::Error))
.tooltip(move |window, cx| {
Tooltip::for_action("Stop Generation", &editor::actions::Cancel, window, cx)
})
.on_click(cx.listener(|this, _event, _, cx| this.cancel(cx)))
.into_any_element()
}
}
fn render_follow_toggle(&self, cx: &mut Context<Self>) -> impl IntoElement {
let following = self
.workspace
.read_with(cx, |workspace, _| {
workspace.is_being_followed(CollaboratorId::Agent)
})
.unwrap_or(false);
IconButton::new("follow-agent", IconName::Crosshair)
.icon_size(IconSize::Small)
.icon_color(Color::Muted)
.toggle_state(following)
.selected_icon_color(Some(Color::Custom(cx.theme().players().agent().cursor)))
.tooltip(move |window, cx| {
if following {
Tooltip::for_action("Stop Following Agent", &Follow, window, cx)
} else {
Tooltip::with_meta(
"Follow Agent",
Some(&Follow),
"Track the agent's location as it reads and edits files.",
window,
cx,
)
}
})
.on_click(cx.listener(move |this, _, window, cx| {
this.workspace
.update(cx, |workspace, cx| {
if following {
workspace.unfollow(CollaboratorId::Agent, window, cx);
} else {
workspace.follow(CollaboratorId::Agent, window, cx);
}
})
.ok();
}))
}
fn render_markdown(&self, markdown: Entity<Markdown>, style: MarkdownStyle) -> MarkdownElement {
let workspace = self.workspace.clone();
MarkdownElement::new(markdown, style).on_url_click(move |text, window, cx| {
@@ -1603,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>,
@@ -1673,10 +2240,6 @@ impl Focusable for AcpThreadView {
impl Render for AcpThreadView {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let text = self.message_editor.read(cx).text(cx);
let is_editor_empty = text.is_empty();
let focus_handle = self.message_editor.focus_handle(cx);
let open_as_markdown = IconButton::new("open-as-markdown", IconName::DocumentText)
.icon_size(IconSize::XSmall)
.icon_color(Color::Ignored)
@@ -1702,6 +2265,7 @@ impl Render for AcpThreadView {
.on_action(cx.listener(Self::chat))
.on_action(cx.listener(Self::previous_history_message))
.on_action(cx.listener(Self::next_history_message))
.on_action(cx.listener(Self::open_agent_diff))
.child(match &self.thread_state {
ThreadState::Unauthenticated { .. } => v_flex()
.p_2()
@@ -1755,6 +2319,7 @@ impl Render for AcpThreadView {
.child(LoadingLabel::new("").size(LabelSize::Small))
.into(),
})
.children(self.render_edits_bar(&thread, window, cx))
} else {
this.child(self.render_empty_state(false, cx))
}
@@ -1782,47 +2347,12 @@ impl Render for AcpThreadView {
.border_t_1()
.border_color(cx.theme().colors().border)
.child(self.render_message_editor(cx))
.child({
let thread = self.thread();
h_flex().justify_end().child(
if thread.map_or(true, |thread| {
thread.read(cx).status() == ThreadStatus::Idle
}) {
IconButton::new("send-message", IconName::Send)
.icon_color(Color::Accent)
.style(ButtonStyle::Filled)
.disabled(thread.is_none() || is_editor_empty)
.on_click({
let focus_handle = focus_handle.clone();
move |_event, window, cx| {
focus_handle.dispatch_action(&Chat, window, cx);
}
})
.when(!is_editor_empty, |button| {
button.tooltip(move |window, cx| {
Tooltip::for_action("Send", &Chat, window, cx)
})
})
.when(is_editor_empty, |button| {
button.tooltip(Tooltip::text("Type a message to submit"))
})
} else {
IconButton::new("stop-generation", IconName::StopFilled)
.icon_color(Color::Error)
.style(ButtonStyle::Tinted(ui::TintColor::Error))
.tooltip(move |window, cx| {
Tooltip::for_action(
"Stop Generation",
&editor::actions::Cancel,
window,
cx,
)
})
.on_click(cx.listener(|this, _event, _, cx| this.cancel(cx)))
},
)
}),
.child(
h_flex()
.justify_between()
.child(self.render_follow_toggle(cx))
.child(self.render_send_button(cx)),
),
)
}
}

View File

@@ -787,6 +787,15 @@ impl ActiveThread {
.unwrap()
}
});
let workspace_subscription = if let Some(workspace) = workspace.upgrade() {
Some(cx.observe_release(&workspace, |this, _, cx| {
this.dismiss_notifications(cx);
}))
} else {
None
};
let mut this = Self {
language_registry,
thread_store,
@@ -834,6 +843,10 @@ impl ActiveThread {
}
}
if let Some(subscription) = workspace_subscription {
this._subscriptions.push(subscription);
}
this
}

View File

@@ -1,7 +1,9 @@
use crate::{Keep, KeepAll, OpenAgentDiff, Reject, RejectAll};
use agent::{Thread, ThreadEvent};
use acp::{AcpThread, AcpThreadEvent};
use agent::{Thread, ThreadEvent, ThreadSummary};
use agent_settings::AgentSettings;
use anyhow::Result;
use assistant_tool::ActionLog;
use buffer_diff::DiffHunkStatus;
use collections::{HashMap, HashSet};
use editor::{
@@ -41,16 +43,108 @@ use zed_actions::assistant::ToggleFocus;
pub struct AgentDiffPane {
multibuffer: Entity<MultiBuffer>,
editor: Entity<Editor>,
thread: Entity<Thread>,
thread: AgentDiffThread,
focus_handle: FocusHandle,
workspace: WeakEntity<Workspace>,
title: SharedString,
_subscriptions: Vec<Subscription>,
}
#[derive(PartialEq, Eq, Clone)]
pub enum AgentDiffThread {
Native(Entity<Thread>),
AcpThread(Entity<AcpThread>),
}
impl AgentDiffThread {
fn project(&self, cx: &App) -> Entity<Project> {
match self {
AgentDiffThread::Native(thread) => thread.read(cx).project().clone(),
AgentDiffThread::AcpThread(thread) => thread.read(cx).project().clone(),
}
}
fn action_log(&self, cx: &App) -> Entity<ActionLog> {
match self {
AgentDiffThread::Native(thread) => thread.read(cx).action_log().clone(),
AgentDiffThread::AcpThread(thread) => thread.read(cx).action_log().clone(),
}
}
fn summary(&self, cx: &App) -> ThreadSummary {
match self {
AgentDiffThread::Native(thread) => thread.read(cx).summary().clone(),
AgentDiffThread::AcpThread(thread) => ThreadSummary::Ready(thread.read(cx).title()),
}
}
fn is_generating(&self, cx: &App) -> bool {
match self {
AgentDiffThread::Native(thread) => thread.read(cx).is_generating(),
AgentDiffThread::AcpThread(thread) => {
thread.read(cx).status() == acp::ThreadStatus::Generating
}
}
}
fn has_pending_edit_tool_uses(&self, cx: &App) -> bool {
match self {
AgentDiffThread::Native(thread) => thread.read(cx).has_pending_edit_tool_uses(),
AgentDiffThread::AcpThread(thread) => thread.read(cx).has_pending_edit_tool_calls(),
}
}
fn downgrade(&self) -> WeakAgentDiffThread {
match self {
AgentDiffThread::Native(thread) => WeakAgentDiffThread::Native(thread.downgrade()),
AgentDiffThread::AcpThread(thread) => {
WeakAgentDiffThread::AcpThread(thread.downgrade())
}
}
}
}
impl From<Entity<Thread>> for AgentDiffThread {
fn from(entity: Entity<Thread>) -> Self {
AgentDiffThread::Native(entity)
}
}
impl From<Entity<AcpThread>> for AgentDiffThread {
fn from(entity: Entity<AcpThread>) -> Self {
AgentDiffThread::AcpThread(entity)
}
}
#[derive(PartialEq, Eq, Clone)]
pub enum WeakAgentDiffThread {
Native(WeakEntity<Thread>),
AcpThread(WeakEntity<AcpThread>),
}
impl WeakAgentDiffThread {
pub fn upgrade(&self) -> Option<AgentDiffThread> {
match self {
WeakAgentDiffThread::Native(weak) => weak.upgrade().map(AgentDiffThread::Native),
WeakAgentDiffThread::AcpThread(weak) => weak.upgrade().map(AgentDiffThread::AcpThread),
}
}
}
impl From<WeakEntity<Thread>> for WeakAgentDiffThread {
fn from(entity: WeakEntity<Thread>) -> Self {
WeakAgentDiffThread::Native(entity)
}
}
impl From<WeakEntity<AcpThread>> for WeakAgentDiffThread {
fn from(entity: WeakEntity<AcpThread>) -> Self {
WeakAgentDiffThread::AcpThread(entity)
}
}
impl AgentDiffPane {
pub fn deploy(
thread: Entity<Thread>,
thread: impl Into<AgentDiffThread>,
workspace: WeakEntity<Workspace>,
window: &mut Window,
cx: &mut App,
@@ -61,14 +155,16 @@ impl AgentDiffPane {
}
pub fn deploy_in_workspace(
thread: Entity<Thread>,
thread: impl Into<AgentDiffThread>,
workspace: &mut Workspace,
window: &mut Window,
cx: &mut Context<Workspace>,
) -> Entity<Self> {
let thread = thread.into();
let existing_diff = workspace
.items_of_type::<AgentDiffPane>(cx)
.find(|diff| diff.read(cx).thread == thread);
if let Some(existing_diff) = existing_diff {
workspace.activate_item(&existing_diff, true, true, window, cx);
existing_diff
@@ -81,7 +177,7 @@ impl AgentDiffPane {
}
pub fn new(
thread: Entity<Thread>,
thread: AgentDiffThread,
workspace: WeakEntity<Workspace>,
window: &mut Window,
cx: &mut Context<Self>,
@@ -89,7 +185,7 @@ impl AgentDiffPane {
let focus_handle = cx.focus_handle();
let multibuffer = cx.new(|_| MultiBuffer::new(Capability::ReadWrite));
let project = thread.read(cx).project().clone();
let project = thread.project(cx).clone();
let editor = cx.new(|cx| {
let mut editor =
Editor::for_multibuffer(multibuffer.clone(), Some(project.clone()), window, cx);
@@ -100,16 +196,27 @@ impl AgentDiffPane {
editor
});
let action_log = thread.read(cx).action_log().clone();
let action_log = thread.action_log(cx).clone();
let mut this = Self {
_subscriptions: vec![
cx.observe_in(&action_log, window, |this, _action_log, window, cx| {
this.update_excerpts(window, cx)
}),
cx.subscribe(&thread, |this, _thread, event, cx| {
this.handle_thread_event(event, cx)
}),
],
_subscriptions: [
Some(
cx.observe_in(&action_log, window, |this, _action_log, window, cx| {
this.update_excerpts(window, cx)
}),
),
match &thread {
AgentDiffThread::Native(thread) => {
Some(cx.subscribe(&thread, |this, _thread, event, cx| {
this.handle_thread_event(event, cx)
}))
}
AgentDiffThread::AcpThread(_) => None,
},
]
.into_iter()
.flatten()
.collect(),
title: SharedString::default(),
multibuffer,
editor,
@@ -123,8 +230,7 @@ impl AgentDiffPane {
}
fn update_excerpts(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let thread = self.thread.read(cx);
let changed_buffers = thread.action_log().read(cx).changed_buffers(cx);
let changed_buffers = self.thread.action_log(cx).read(cx).changed_buffers(cx);
let mut paths_to_delete = self.multibuffer.read(cx).paths().collect::<HashSet<_>>();
for (buffer, diff_handle) in changed_buffers {
@@ -211,7 +317,7 @@ impl AgentDiffPane {
}
fn update_title(&mut self, cx: &mut Context<Self>) {
let new_title = self.thread.read(cx).summary().unwrap_or("Agent Changes");
let new_title = self.thread.summary(cx).unwrap_or("Agent Changes");
if new_title != self.title {
self.title = new_title;
cx.emit(EditorEvent::TitleChanged);
@@ -275,14 +381,15 @@ impl AgentDiffPane {
fn keep_all(&mut self, _: &KeepAll, _window: &mut Window, cx: &mut Context<Self>) {
self.thread
.update(cx, |thread, cx| thread.keep_all_edits(cx));
.action_log(cx)
.update(cx, |action_log, cx| action_log.keep_all_edits(cx))
}
}
fn keep_edits_in_selection(
editor: &mut Editor,
buffer_snapshot: &MultiBufferSnapshot,
thread: &Entity<Thread>,
thread: &AgentDiffThread,
window: &mut Window,
cx: &mut Context<Editor>,
) {
@@ -297,7 +404,7 @@ fn keep_edits_in_selection(
fn reject_edits_in_selection(
editor: &mut Editor,
buffer_snapshot: &MultiBufferSnapshot,
thread: &Entity<Thread>,
thread: &AgentDiffThread,
window: &mut Window,
cx: &mut Context<Editor>,
) {
@@ -311,7 +418,7 @@ fn reject_edits_in_selection(
fn keep_edits_in_ranges(
editor: &mut Editor,
buffer_snapshot: &MultiBufferSnapshot,
thread: &Entity<Thread>,
thread: &AgentDiffThread,
ranges: Vec<Range<editor::Anchor>>,
window: &mut Window,
cx: &mut Context<Editor>,
@@ -326,8 +433,8 @@ fn keep_edits_in_ranges(
for hunk in &diff_hunks_in_ranges {
let buffer = multibuffer.read(cx).buffer(hunk.buffer_id);
if let Some(buffer) = buffer {
thread.update(cx, |thread, cx| {
thread.keep_edits_in_range(buffer, hunk.buffer_range.clone(), cx)
thread.action_log(cx).update(cx, |action_log, cx| {
action_log.keep_edits_in_range(buffer, hunk.buffer_range.clone(), cx)
});
}
}
@@ -336,7 +443,7 @@ fn keep_edits_in_ranges(
fn reject_edits_in_ranges(
editor: &mut Editor,
buffer_snapshot: &MultiBufferSnapshot,
thread: &Entity<Thread>,
thread: &AgentDiffThread,
ranges: Vec<Range<editor::Anchor>>,
window: &mut Window,
cx: &mut Context<Editor>,
@@ -362,8 +469,9 @@ fn reject_edits_in_ranges(
for (buffer, ranges) in ranges_by_buffer {
thread
.update(cx, |thread, cx| {
thread.reject_edits_in_ranges(buffer, ranges, cx)
.action_log(cx)
.update(cx, |action_log, cx| {
action_log.reject_edits_in_ranges(buffer, ranges, cx)
})
.detach_and_log_err(cx);
}
@@ -461,7 +569,7 @@ impl Item for AgentDiffPane {
}
fn tab_content(&self, params: TabContentParams, _window: &Window, cx: &App) -> AnyElement {
let summary = self.thread.read(cx).summary().unwrap_or("Agent Changes");
let summary = self.thread.summary(cx).unwrap_or("Agent Changes");
Label::new(format!("Review: {}", summary))
.color(if params.selected {
Color::Default
@@ -641,7 +749,7 @@ impl Render for AgentDiffPane {
}
}
fn diff_hunk_controls(thread: &Entity<Thread>) -> editor::RenderDiffHunkControlsFn {
fn diff_hunk_controls(thread: &AgentDiffThread) -> editor::RenderDiffHunkControlsFn {
let thread = thread.clone();
Arc::new(
@@ -676,7 +784,7 @@ fn render_diff_hunk_controls(
hunk_range: Range<editor::Anchor>,
is_created_file: bool,
line_height: Pixels,
thread: &Entity<Thread>,
thread: &AgentDiffThread,
editor: &Entity<Editor>,
window: &mut Window,
cx: &mut App,
@@ -1112,11 +1220,8 @@ impl Render for AgentDiffToolbar {
return Empty.into_any();
};
let has_pending_edit_tool_use = agent_diff
.read(cx)
.thread
.read(cx)
.has_pending_edit_tool_uses();
let has_pending_edit_tool_use =
agent_diff.read(cx).thread.has_pending_edit_tool_uses(cx);
if has_pending_edit_tool_use {
return div().px_2().child(spinner_icon).into_any();
@@ -1187,8 +1292,8 @@ pub enum EditorState {
}
struct WorkspaceThread {
thread: WeakEntity<Thread>,
_thread_subscriptions: [Subscription; 2],
thread: WeakAgentDiffThread,
_thread_subscriptions: (Subscription, Subscription),
singleton_editors: HashMap<WeakEntity<Buffer>, HashMap<WeakEntity<Editor>, Subscription>>,
_settings_subscription: Subscription,
_workspace_subscription: Option<Subscription>,
@@ -1212,23 +1317,23 @@ impl AgentDiff {
pub fn set_active_thread(
workspace: &WeakEntity<Workspace>,
thread: &Entity<Thread>,
thread: impl Into<AgentDiffThread>,
window: &mut Window,
cx: &mut App,
) {
Self::global(cx).update(cx, |this, cx| {
this.register_active_thread_impl(workspace, thread, window, cx);
this.register_active_thread_impl(workspace, thread.into(), window, cx);
});
}
fn register_active_thread_impl(
&mut self,
workspace: &WeakEntity<Workspace>,
thread: &Entity<Thread>,
thread: AgentDiffThread,
window: &mut Window,
cx: &mut Context<Self>,
) {
let action_log = thread.read(cx).action_log().clone();
let action_log = thread.action_log(cx).clone();
let action_log_subscription = cx.observe_in(&action_log, window, {
let workspace = workspace.clone();
@@ -1237,17 +1342,25 @@ impl AgentDiff {
}
});
let thread_subscription = cx.subscribe_in(&thread, window, {
let workspace = workspace.clone();
move |this, _thread, event, window, cx| {
this.handle_thread_event(&workspace, event, window, cx)
}
});
let thread_subscription = match &thread {
AgentDiffThread::Native(thread) => cx.subscribe_in(&thread, window, {
let workspace = workspace.clone();
move |this, _thread, event, window, cx| {
this.handle_native_thread_event(&workspace, event, window, cx)
}
}),
AgentDiffThread::AcpThread(thread) => cx.subscribe_in(&thread, window, {
let workspace = workspace.clone();
move |this, thread, event, window, cx| {
this.handle_acp_thread_event(&workspace, thread, event, window, cx)
}
}),
};
if let Some(workspace_thread) = self.workspace_threads.get_mut(&workspace) {
// replace thread and action log subscription, but keep editors
workspace_thread.thread = thread.downgrade();
workspace_thread._thread_subscriptions = [action_log_subscription, thread_subscription];
workspace_thread._thread_subscriptions = (action_log_subscription, thread_subscription);
self.update_reviewing_editors(&workspace, window, cx);
return;
}
@@ -1272,7 +1385,7 @@ impl AgentDiff {
workspace.clone(),
WorkspaceThread {
thread: thread.downgrade(),
_thread_subscriptions: [action_log_subscription, thread_subscription],
_thread_subscriptions: (action_log_subscription, thread_subscription),
singleton_editors: HashMap::default(),
_settings_subscription: settings_subscription,
_workspace_subscription: workspace_subscription,
@@ -1319,7 +1432,7 @@ impl AgentDiff {
fn register_review_action<T: Action>(
workspace: &mut Workspace,
review: impl Fn(&Entity<Editor>, &Entity<Thread>, &mut Window, &mut App) -> PostReviewState
review: impl Fn(&Entity<Editor>, &AgentDiffThread, &mut Window, &mut App) -> PostReviewState
+ 'static,
this: &Entity<AgentDiff>,
) {
@@ -1338,7 +1451,7 @@ impl AgentDiff {
});
}
fn handle_thread_event(
fn handle_native_thread_event(
&mut self,
workspace: &WeakEntity<Workspace>,
event: &ThreadEvent,
@@ -1380,6 +1493,40 @@ impl AgentDiff {
}
}
fn handle_acp_thread_event(
&mut self,
workspace: &WeakEntity<Workspace>,
thread: &Entity<AcpThread>,
event: &AcpThreadEvent,
window: &mut Window,
cx: &mut Context<Self>,
) {
match event {
AcpThreadEvent::NewEntry => {
if thread
.read(cx)
.entries()
.last()
.and_then(|entry| entry.diff())
.is_some()
{
self.update_reviewing_editors(workspace, window, cx);
}
}
AcpThreadEvent::EntryUpdated(ix) => {
if thread
.read(cx)
.entries()
.get(*ix)
.and_then(|entry| entry.diff())
.is_some()
{
self.update_reviewing_editors(workspace, window, cx);
}
}
}
}
fn handle_workspace_event(
&mut self,
workspace: &Entity<Workspace>,
@@ -1485,7 +1632,7 @@ impl AgentDiff {
return;
};
let action_log = thread.read(cx).action_log();
let action_log = thread.action_log(cx);
let changed_buffers = action_log.read(cx).changed_buffers(cx);
let mut unaffected = self.reviewing_editors.clone();
@@ -1510,7 +1657,7 @@ impl AgentDiff {
multibuffer.add_diff(diff_handle.clone(), cx);
});
let new_state = if thread.read(cx).is_generating() {
let new_state = if thread.is_generating(cx) {
EditorState::Generating
} else {
EditorState::Reviewing
@@ -1606,7 +1753,7 @@ impl AgentDiff {
fn keep_all(
editor: &Entity<Editor>,
thread: &Entity<Thread>,
thread: &AgentDiffThread,
window: &mut Window,
cx: &mut App,
) -> PostReviewState {
@@ -1626,7 +1773,7 @@ impl AgentDiff {
fn reject_all(
editor: &Entity<Editor>,
thread: &Entity<Thread>,
thread: &AgentDiffThread,
window: &mut Window,
cx: &mut App,
) -> PostReviewState {
@@ -1646,7 +1793,7 @@ impl AgentDiff {
fn keep(
editor: &Entity<Editor>,
thread: &Entity<Thread>,
thread: &AgentDiffThread,
window: &mut Window,
cx: &mut App,
) -> PostReviewState {
@@ -1659,7 +1806,7 @@ impl AgentDiff {
fn reject(
editor: &Entity<Editor>,
thread: &Entity<Thread>,
thread: &AgentDiffThread,
window: &mut Window,
cx: &mut App,
) -> PostReviewState {
@@ -1682,7 +1829,7 @@ impl AgentDiff {
fn review_in_active_editor(
&mut self,
workspace: &mut Workspace,
review: impl Fn(&Entity<Editor>, &Entity<Thread>, &mut Window, &mut App) -> PostReviewState,
review: impl Fn(&Entity<Editor>, &AgentDiffThread, &mut Window, &mut App) -> PostReviewState,
window: &mut Window,
cx: &mut Context<Self>,
) -> Option<Task<Result<()>>> {
@@ -1703,7 +1850,7 @@ impl AgentDiff {
if let PostReviewState::AllReviewed = review(&editor, &thread, window, cx) {
if let Some(curr_buffer) = editor.read(cx).buffer().read(cx).as_singleton() {
let changed_buffers = thread.read(cx).action_log().read(cx).changed_buffers(cx);
let changed_buffers = thread.action_log(cx).read(cx).changed_buffers(cx);
let mut keys = changed_buffers.keys().cycle();
keys.find(|k| *k == &curr_buffer);
@@ -1801,8 +1948,9 @@ mod tests {
})
.await
.unwrap();
let thread = thread_store.update(cx, |store, cx| store.create_thread(cx));
let action_log = thread.read_with(cx, |thread, _| thread.action_log().clone());
let thread =
AgentDiffThread::Native(thread_store.update(cx, |store, cx| store.create_thread(cx)));
let action_log = cx.read(|cx| thread.action_log(cx));
let (workspace, cx) =
cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
@@ -1988,8 +2136,9 @@ mod tests {
});
// Set the active thread
let thread = AgentDiffThread::Native(thread);
cx.update(|window, cx| {
AgentDiff::set_active_thread(&workspace.downgrade(), &thread, window, cx)
AgentDiff::set_active_thread(&workspace.downgrade(), thread.clone(), window, cx)
});
let buffer1 = project

View File

@@ -1,3 +1,4 @@
use std::cell::RefCell;
use std::ops::Range;
use std::path::Path;
use std::rc::Rc;
@@ -8,6 +9,7 @@ use db::kvp::{Dismissable, KEY_VALUE_STORE};
use serde::{Deserialize, Serialize};
use crate::NewAcpThread;
use crate::agent_diff::AgentDiffThread;
use crate::language_model_selector::ToggleModelSelector;
use crate::{
AddContextServer, AgentDiffPane, ContinueThread, ContinueWithBurnMode,
@@ -432,6 +434,8 @@ pub struct AgentPanel {
configuration_subscription: Option<Subscription>,
local_timezone: UtcOffset,
active_view: ActiveView,
acp_message_history:
Rc<RefCell<crate::acp::MessageHistory<agentic_coding_protocol::SendUserMessageParams>>>,
previous_view: Option<ActiveView>,
history_store: Entity<HistoryStore>,
history: Entity<ThreadHistory>,
@@ -624,7 +628,7 @@ impl AgentPanel {
}
};
AgentDiff::set_active_thread(&workspace, &thread, window, cx);
AgentDiff::set_active_thread(&workspace, thread.clone(), window, cx);
let weak_panel = weak_self.clone();
@@ -698,6 +702,7 @@ impl AgentPanel {
.unwrap(),
inline_assist_context_store,
previous_view: None,
acp_message_history: Default::default(),
history_store: history_store.clone(),
history: cx.new(|cx| ThreadHistory::new(weak_self, history_store, window, cx)),
hovered_recent_history_item: None,
@@ -770,13 +775,10 @@ impl AgentPanel {
}
fn new_thread(&mut self, action: &NewThread, window: &mut Window, cx: &mut Context<Self>) {
// Preserve chat box text when using creating new thread from summary'
let preserved_text = if action.from_thread_id.is_some() {
self.active_message_editor()
.map(|editor| editor.read(cx).get_text(cx).trim().to_string())
} else {
None
};
// Preserve chat box text when using creating new thread
let preserved_text = self
.active_message_editor()
.map(|editor| editor.read(cx).get_text(cx).trim().to_string());
let thread = self
.thread_store
@@ -848,7 +850,7 @@ impl AgentPanel {
let thread_view = ActiveView::thread(active_thread.clone(), message_editor, window, cx);
self.set_active_view(thread_view, window, cx);
AgentDiff::set_active_thread(&self.workspace, &thread, window, cx);
AgentDiff::set_active_thread(&self.workspace, thread.clone(), window, cx);
}
fn new_prompt_editor(&mut self, window: &mut Window, cx: &mut Context<Self>) {
@@ -890,14 +892,30 @@ impl AgentPanel {
fn new_gemini_thread(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let workspace = self.workspace.clone();
let project = self.project.clone();
let message_history = self.acp_message_history.clone();
cx.spawn_in(window, async move |this, cx| {
let thread_view = cx.new_window_entity(|window, cx| {
crate::acp::AcpThreadView::new(workspace, project, window, cx)
crate::acp::AcpThreadView::new(
workspace.clone(),
project,
message_history,
window,
cx,
)
})?;
this.update_in(cx, |this, window, cx| {
this.set_active_view(ActiveView::AcpThread { thread_view }, window, cx);
this.set_active_view(
ActiveView::AcpThread {
thread_view: thread_view.clone(),
},
window,
cx,
);
})
.log_err();
anyhow::Ok(())
})
.detach();
}
@@ -1053,7 +1071,7 @@ impl AgentPanel {
let thread_view = ActiveView::thread(active_thread.clone(), message_editor, window, cx);
self.set_active_view(thread_view, window, cx);
AgentDiff::set_active_thread(&self.workspace, &thread, window, cx);
AgentDiff::set_active_thread(&self.workspace, thread.clone(), window, cx);
}
pub fn go_back(&mut self, _: &workspace::GoBack, window: &mut Window, cx: &mut Context<Self>) {
@@ -1184,7 +1202,12 @@ impl AgentPanel {
let thread = thread.read(cx).thread().clone();
self.workspace
.update(cx, |workspace, cx| {
AgentDiffPane::deploy_in_workspace(thread, workspace, window, cx)
AgentDiffPane::deploy_in_workspace(
AgentDiffThread::Native(thread),
workspace,
window,
cx,
)
})
.log_err();
}
@@ -1420,6 +1443,8 @@ impl AgentPanel {
self.active_view = new_view;
}
self.acp_message_history.borrow_mut().reset_position();
self.focus_handle(cx).focus(window);
}

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

@@ -2,6 +2,7 @@ use std::collections::BTreeMap;
use std::rc::Rc;
use std::sync::Arc;
use crate::agent_diff::AgentDiffThread;
use crate::agent_model_selector::AgentModelSelector;
use crate::language_model_selector::ToggleModelSelector;
use crate::tool_compatibility::{IncompatibleToolsState, IncompatibleToolsTooltip};
@@ -475,9 +476,12 @@ impl MessageEditor {
window: &mut Window,
cx: &mut Context<Self>,
) {
if let Ok(diff) =
AgentDiffPane::deploy(self.thread.clone(), self.workspace.clone(), window, cx)
{
if let Ok(diff) = AgentDiffPane::deploy(
AgentDiffThread::Native(self.thread.clone()),
self.workspace.clone(),
window,
cx,
) {
let path_key = multi_buffer::PathKey::for_buffer(&buffer, cx);
diff.update(cx, |diff, cx| diff.move_to_path(path_key, window, cx));
}

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

@@ -34,6 +34,11 @@ impl ExtensionSlashCommandProxy for SlashCommandRegistryProxy {
self.slash_command_registry
.register_command(ExtensionSlashCommand::new(extension, command), false)
}
fn unregister_slash_command(&self, command_name: Arc<str>) {
self.slash_command_registry
.unregister_command_by_name(&command_name)
}
}
/// An adapter that allows an [`LspAdapterDelegate`] to be used as a [`WorktreeDelegate`].

View File

@@ -8,7 +8,7 @@ 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;
use util::{RangeExt, ResultExt as _};
/// Tracks actions performed by tools in a thread
pub struct ActionLog {
@@ -47,6 +47,10 @@ impl ActionLog {
self.edited_since_project_diagnostics_check
}
pub fn latest_snapshot(&self, buffer: &Entity<Buffer>) -> Option<text::BufferSnapshot> {
Some(self.tracked_buffers.get(buffer)?.snapshot.clone())
}
fn track_buffer_internal(
&mut self,
buffer: Entity<Buffer>,
@@ -715,6 +719,22 @@ impl ActionLog {
cx.notify();
}
pub fn reject_all_edits(&mut self, cx: &mut Context<Self>) -> Task<()> {
let futures = self.changed_buffers(cx).into_keys().map(|buffer| {
let reject = self.reject_edits_in_ranges(buffer, vec![Anchor::MIN..Anchor::MAX], cx);
async move {
reject.await.log_err();
}
});
let task = futures::future::join_all(futures);
cx.spawn(async move |_, _| {
task.await;
})
}
/// Returns the set of buffers that contain edits that haven't been reviewed by the user.
pub fn changed_buffers(&self, cx: &App) -> BTreeMap<Entity<Buffer>, Entity<BufferDiff>> {
self.tracked_buffers

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

@@ -18,7 +18,6 @@ use serde::{Deserialize, Serialize};
use settings::Settings;
use std::sync::Arc;
use ui::IconName;
use util::markdown::MarkdownInlineCode;
/// If the model requests to read a file whose size exceeds this, then
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
@@ -78,11 +77,21 @@ impl Tool for ReadFileTool {
fn ui_text(&self, input: &serde_json::Value) -> String {
match serde_json::from_value::<ReadFileToolInput>(input.clone()) {
Ok(input) => {
let path = MarkdownInlineCode(&input.path);
let path = &input.path;
match (input.start_line, input.end_line) {
(Some(start), None) => format!("Read file {path} (from line {start})"),
(Some(start), Some(end)) => format!("Read file {path} (lines {start}-{end})"),
_ => format!("Read file {path}"),
(Some(start), Some(end)) => {
format!(
"[Read file `{}` (lines {}-{})](@selection:{}:({}-{}))",
path, start, end, path, start, end
)
}
(Some(start), None) => {
format!(
"[Read file `{}` (from line {})](@selection:{}:({}-{}))",
path, start, path, start, start
)
}
_ => format!("[Read file `{}`](@file:{})", path, path),
}
}
Err(_) => "Read file".to_string(),

View File

@@ -1389,10 +1389,17 @@ impl Room {
let sources = cx.screen_capture_sources();
cx.spawn(async move |this, cx| {
let sources = sources.await??;
let source = sources.first().context("no display found")?;
let sources = sources
.await
.map_err(|error| error.into())
.and_then(|sources| sources);
let source =
sources.and_then(|sources| sources.into_iter().next().context("no display found"));
let publication = participant.publish_screenshare_track(&**source, cx).await;
let publication = match source {
Ok(source) => participant.publish_screenshare_track(&*source, cx).await,
Err(error) => Err(error),
};
this.update(cx, |this, cx| {
let live_kit = this

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

View File

@@ -1,12 +1,33 @@
{
"admins": [
"nathansobo",
"as-cii",
"maxbrunsfeld",
"iamnbutler",
"mikayla-maki",
"as-cii",
"JosephTLyons",
"rgbkrk"
"maxdeviant",
"SomeoneToIgnore",
"mikayla-maki",
"agu-z",
"osiewicz",
"ConradIrwin",
"benbrandt",
"bennetbo",
"smitbarmase",
"notpeter",
"rgbkrk",
"JunkuiZhang",
"Anthony-Eid",
"rtfeldman",
"danilo-leal",
"MrSubidubi",
"cole-miller",
"osyvokon",
"probably-neb",
"mgsloan",
"P1n3appl3",
"mslzed",
"franciskafyi",
"katie-z-geer"
],
"channels": ["zed"]
}

View File

@@ -2836,62 +2836,117 @@ async fn make_update_user_plan_message(
account_too_young: Some(account_too_young),
has_overdue_invoices: billing_customer
.map(|billing_customer| billing_customer.has_overdue_invoices),
usage: usage.map(|usage| {
let plan = match plan {
proto::Plan::Free => zed_llm_client::Plan::ZedFree,
proto::Plan::ZedPro => zed_llm_client::Plan::ZedPro,
proto::Plan::ZedProTrial => zed_llm_client::Plan::ZedProTrial,
};
let model_requests_limit = match plan.model_requests_limit() {
zed_llm_client::UsageLimit::Limited(limit) => {
let limit = if plan == zed_llm_client::Plan::ZedProTrial
&& feature_flags
.iter()
.any(|flag| flag == AGENT_EXTENDED_TRIAL_FEATURE_FLAG)
{
1_000
} else {
limit
};
zed_llm_client::UsageLimit::Limited(limit)
}
zed_llm_client::UsageLimit::Unlimited => zed_llm_client::UsageLimit::Unlimited,
};
proto::SubscriptionUsage {
model_requests_usage_amount: usage.model_requests as u32,
model_requests_usage_limit: Some(proto::UsageLimit {
variant: Some(match model_requests_limit {
zed_llm_client::UsageLimit::Limited(limit) => {
proto::usage_limit::Variant::Limited(proto::usage_limit::Limited {
limit: limit as u32,
})
}
zed_llm_client::UsageLimit::Unlimited => {
proto::usage_limit::Variant::Unlimited(proto::usage_limit::Unlimited {})
}
}),
}),
edit_predictions_usage_amount: usage.edit_predictions as u32,
edit_predictions_usage_limit: Some(proto::UsageLimit {
variant: Some(match plan.edit_predictions_limit() {
zed_llm_client::UsageLimit::Limited(limit) => {
proto::usage_limit::Variant::Limited(proto::usage_limit::Limited {
limit: limit as u32,
})
}
zed_llm_client::UsageLimit::Unlimited => {
proto::usage_limit::Variant::Unlimited(proto::usage_limit::Unlimited {})
}
}),
}),
}
}),
usage: Some(
usage
.map(|usage| subscription_usage_to_proto(plan, usage, &feature_flags))
.unwrap_or_else(|| make_default_subscription_usage(plan, &feature_flags)),
),
})
}
fn model_requests_limit(
plan: zed_llm_client::Plan,
feature_flags: &Vec<String>,
) -> zed_llm_client::UsageLimit {
match plan.model_requests_limit() {
zed_llm_client::UsageLimit::Limited(limit) => {
let limit = if plan == zed_llm_client::Plan::ZedProTrial
&& feature_flags
.iter()
.any(|flag| flag == AGENT_EXTENDED_TRIAL_FEATURE_FLAG)
{
1_000
} else {
limit
};
zed_llm_client::UsageLimit::Limited(limit)
}
zed_llm_client::UsageLimit::Unlimited => zed_llm_client::UsageLimit::Unlimited,
}
}
fn subscription_usage_to_proto(
plan: proto::Plan,
usage: crate::llm::db::subscription_usage::Model,
feature_flags: &Vec<String>,
) -> proto::SubscriptionUsage {
let plan = match plan {
proto::Plan::Free => zed_llm_client::Plan::ZedFree,
proto::Plan::ZedPro => zed_llm_client::Plan::ZedPro,
proto::Plan::ZedProTrial => zed_llm_client::Plan::ZedProTrial,
};
proto::SubscriptionUsage {
model_requests_usage_amount: usage.model_requests as u32,
model_requests_usage_limit: Some(proto::UsageLimit {
variant: Some(match model_requests_limit(plan, feature_flags) {
zed_llm_client::UsageLimit::Limited(limit) => {
proto::usage_limit::Variant::Limited(proto::usage_limit::Limited {
limit: limit as u32,
})
}
zed_llm_client::UsageLimit::Unlimited => {
proto::usage_limit::Variant::Unlimited(proto::usage_limit::Unlimited {})
}
}),
}),
edit_predictions_usage_amount: usage.edit_predictions as u32,
edit_predictions_usage_limit: Some(proto::UsageLimit {
variant: Some(match plan.edit_predictions_limit() {
zed_llm_client::UsageLimit::Limited(limit) => {
proto::usage_limit::Variant::Limited(proto::usage_limit::Limited {
limit: limit as u32,
})
}
zed_llm_client::UsageLimit::Unlimited => {
proto::usage_limit::Variant::Unlimited(proto::usage_limit::Unlimited {})
}
}),
}),
}
}
fn make_default_subscription_usage(
plan: proto::Plan,
feature_flags: &Vec<String>,
) -> proto::SubscriptionUsage {
let plan = match plan {
proto::Plan::Free => zed_llm_client::Plan::ZedFree,
proto::Plan::ZedPro => zed_llm_client::Plan::ZedPro,
proto::Plan::ZedProTrial => zed_llm_client::Plan::ZedProTrial,
};
proto::SubscriptionUsage {
model_requests_usage_amount: 0,
model_requests_usage_limit: Some(proto::UsageLimit {
variant: Some(match model_requests_limit(plan, feature_flags) {
zed_llm_client::UsageLimit::Limited(limit) => {
proto::usage_limit::Variant::Limited(proto::usage_limit::Limited {
limit: limit as u32,
})
}
zed_llm_client::UsageLimit::Unlimited => {
proto::usage_limit::Variant::Unlimited(proto::usage_limit::Unlimited {})
}
}),
}),
edit_predictions_usage_amount: 0,
edit_predictions_usage_limit: Some(proto::UsageLimit {
variant: Some(match plan.edit_predictions_limit() {
zed_llm_client::UsageLimit::Limited(limit) => {
proto::usage_limit::Variant::Limited(proto::usage_limit::Limited {
limit: limit as u32,
})
}
zed_llm_client::UsageLimit::Unlimited => {
proto::usage_limit::Variant::Unlimited(proto::usage_limit::Unlimited {})
}
}),
}),
}
}
async fn update_user_plan(session: &Session) -> Result<()> {
let db = session.db().await;

View File

@@ -19,8 +19,8 @@ use crate::stripe_client::{
StripeCustomerId, StripeCustomerUpdate, StripeCustomerUpdateAddress, StripeCustomerUpdateName,
StripeMeter, StripePrice, StripePriceId, StripeSubscription, StripeSubscriptionId,
StripeSubscriptionTrialSettings, StripeSubscriptionTrialSettingsEndBehavior,
StripeSubscriptionTrialSettingsEndBehaviorMissingPaymentMethod, UpdateSubscriptionItems,
UpdateSubscriptionParams,
StripeSubscriptionTrialSettingsEndBehaviorMissingPaymentMethod, StripeTaxIdCollection,
UpdateSubscriptionItems, UpdateSubscriptionParams,
};
pub struct StripeBilling {
@@ -252,6 +252,7 @@ impl StripeBilling {
name: Some(StripeCustomerUpdateName::Auto),
shipping: None,
});
params.tax_id_collection = Some(StripeTaxIdCollection { enabled: true });
let session = self.client.create_checkout_session(params).await?;
Ok(session.url.context("no checkout session URL")?)
@@ -311,6 +312,7 @@ impl StripeBilling {
name: Some(StripeCustomerUpdateName::Auto),
shipping: None,
});
params.tax_id_collection = Some(StripeTaxIdCollection { enabled: true });
let session = self.client.create_checkout_session(params).await?;
Ok(session.url.context("no checkout session URL")?)

View File

@@ -190,6 +190,7 @@ pub struct StripeCreateCheckoutSessionParams<'a> {
pub success_url: Option<&'a str>,
pub billing_address_collection: Option<StripeBillingAddressCollection>,
pub customer_update: Option<StripeCustomerUpdate>,
pub tax_id_collection: Option<StripeTaxIdCollection>,
}
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
@@ -218,6 +219,11 @@ pub struct StripeCreateCheckoutSessionSubscriptionData {
pub trial_settings: Option<StripeSubscriptionTrialSettings>,
}
#[derive(Debug, PartialEq, Clone)]
pub struct StripeTaxIdCollection {
pub enabled: bool,
}
#[derive(Debug)]
pub struct StripeCheckoutSession {
pub url: Option<String>,

View File

@@ -14,8 +14,8 @@ use crate::stripe_client::{
StripeCreateCheckoutSessionSubscriptionData, StripeCreateMeterEventParams,
StripeCreateSubscriptionParams, StripeCustomer, StripeCustomerId, StripeCustomerUpdate,
StripeMeter, StripeMeterId, StripePrice, StripePriceId, StripeSubscription,
StripeSubscriptionId, StripeSubscriptionItem, StripeSubscriptionItemId, UpdateCustomerParams,
UpdateSubscriptionParams,
StripeSubscriptionId, StripeSubscriptionItem, StripeSubscriptionItemId, StripeTaxIdCollection,
UpdateCustomerParams, UpdateSubscriptionParams,
};
#[derive(Debug, Clone)]
@@ -38,6 +38,7 @@ pub struct StripeCreateCheckoutSessionCall {
pub success_url: Option<String>,
pub billing_address_collection: Option<StripeBillingAddressCollection>,
pub customer_update: Option<StripeCustomerUpdate>,
pub tax_id_collection: Option<StripeTaxIdCollection>,
}
pub struct FakeStripeClient {
@@ -236,6 +237,7 @@ impl StripeClient for FakeStripeClient {
success_url: params.success_url.map(|url| url.to_string()),
billing_address_collection: params.billing_address_collection,
customer_update: params.customer_update,
tax_id_collection: params.tax_id_collection,
});
Ok(StripeCheckoutSession {

View File

@@ -27,8 +27,8 @@ use crate::stripe_client::{
StripeMeter, StripePrice, StripePriceId, StripePriceRecurring, StripeSubscription,
StripeSubscriptionId, StripeSubscriptionItem, StripeSubscriptionItemId,
StripeSubscriptionTrialSettings, StripeSubscriptionTrialSettingsEndBehavior,
StripeSubscriptionTrialSettingsEndBehaviorMissingPaymentMethod, UpdateCustomerParams,
UpdateSubscriptionParams,
StripeSubscriptionTrialSettingsEndBehaviorMissingPaymentMethod, StripeTaxIdCollection,
UpdateCustomerParams, UpdateSubscriptionParams,
};
pub struct RealStripeClient {
@@ -448,6 +448,7 @@ impl<'a> TryFrom<StripeCreateCheckoutSessionParams<'a>> for CreateCheckoutSessio
success_url: value.success_url,
billing_address_collection: value.billing_address_collection.map(Into::into),
customer_update: value.customer_update.map(Into::into),
tax_id_collection: value.tax_id_collection.map(Into::into),
..Default::default()
})
}
@@ -590,3 +591,11 @@ impl From<StripeCustomerUpdate> for stripe::CreateCheckoutSessionCustomerUpdate
}
}
}
impl From<StripeTaxIdCollection> for stripe::CreateCheckoutSessionTaxIdCollection {
fn from(value: StripeTaxIdCollection) -> Self {
stripe::CreateCheckoutSessionTaxIdCollection {
enabled: value.enabled,
}
}
}

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

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

@@ -534,6 +534,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

@@ -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,7 +37,7 @@ 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::{
@@ -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>,
@@ -100,7 +103,7 @@ impl DebugPanel {
Self {
size: px(300.),
sessions: vec![],
sessions_with_children: Default::default(),
active_session: None,
focus_handle,
breakpoint_list: BreakpointList::new(
@@ -138,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>> {
@@ -185,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,
)
});
@@ -267,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(
@@ -363,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)
});
@@ -379,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)
@@ -425,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;
@@ -438,6 +464,7 @@ impl DebugPanel {
adapter,
task_context,
Some(parent_session.clone()),
quirks,
cx,
);
@@ -463,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 {
@@ -498,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()
@@ -813,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
@@ -976,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);
@@ -990,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| {
@@ -1261,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
}
}
@@ -1302,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()
@@ -1337,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
})?;
@@ -1383,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())
@@ -1749,6 +1805,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

@@ -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,963 @@
use std::{
cell::LazyCell,
fmt::Write,
ops::RangeInclusive,
sync::{Arc, LazyLock},
time::Duration,
};
use editor::{Editor, EditorElement, EditorStyle};
use gpui::{
Action, AppContext, DismissEvent, Empty, Entity, FocusHandle, Focusable, MouseButton,
MouseMoveEvent, Point, ScrollStrategy, ScrollWheelEvent, Stateful, Subscription, Task,
TextStyle, UniformList, UniformListScrollHandle, WeakEntity, actions, anchored, bounds,
deferred, point, size, 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);
}
}
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);
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_mouse_move(cx.listener(|this, evt, _, cx| {
this.handle_drag(evt);
cx.notify();
cx.stop_propagation()
}))
.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_drag(&mut self, evt: &MouseMoveEvent) {
if !evt.dragging() {
return;
}
if !self.scroll_state.is_dragging()
&& !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();
let (top_area, bottom_area) = {
let size = size(viewport.size.width, viewport.size.height / 10.);
(
bounds(viewport.origin, size),
bounds(
point(viewport.origin.x, viewport.origin.y + size.height * 2.),
size,
),
)
};
if bottom_area.contains(&evt.position) {
//ix == row_count - 1 {
self.view_state.schedule_scroll_down();
} else if top_area.contains(&evt.position) {
self.view_state.schedule_scroll_up();
}
}
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 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) = reference.await {
_ = this.update(cx, |this, cx| {
let Ok(address) = parse_int::parse::<u64>(&reference) else {
return;
};
this.jump_to_address(address, 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::Accent.color(cx)).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::Accent.color(cx))
})
})
.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_mouse_move(cx.listener(|this, evt: &MouseMoveEvent, _, _| {
this.handle_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

@@ -656,7 +656,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

@@ -410,6 +410,8 @@ actions!(
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.

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

@@ -1795,6 +1795,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 +1856,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 +1979,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 +2061,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 +2098,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 +2106,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 +2124,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 +2150,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 +2164,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 +2219,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 +2343,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 +10466,6 @@ impl Editor {
cloned_prompt.clone().into_any_element()
}),
priority: 0,
render_in_minimap: true,
}];
let focus_handle = bp_prompt.focus_handle(cx);
@@ -16138,7 +16161,6 @@ impl Editor {
}
}),
priority: 0,
render_in_minimap: true,
}],
Some(Autoscroll::fit()),
cx,
@@ -17072,6 +17094,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 +18063,7 @@ impl Editor {
parent: cx.weak_entity(),
},
self.buffer.clone(),
self.project.clone(),
None,
Some(self.display_map.clone()),
window,
cx,
@@ -19655,8 +19717,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 +19947,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);

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,
@@ -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)
})?;
@@ -3084,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();
}
@@ -3399,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()
}
@@ -3620,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);
});
}
}
}),
),
@@ -6762,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;
}
@@ -7889,9 +7899,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| {
@@ -8035,23 +8050,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();
@@ -8327,18 +8344,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
@@ -8401,27 +8422,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) => {
@@ -8460,10 +8485,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,
@@ -8472,13 +8499,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;
}
});
@@ -8593,7 +8615,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(),
@@ -8614,29 +8638,6 @@ impl Element for EditorElement {
cx,
);
self.editor.update(cx, |editor, cx| {
let clamped = editor.scroll_manager.clamp_scroll_left(scroll_max.x);
let autoscrolled = if autoscroll_horizontally {
editor.autoscroll_horizontally(
start_row,
editor_content_width,
scroll_width,
em_advance,
&line_layouts,
window,
cx,
)
} else {
false
};
if clamped || autoscrolled {
snapshot = editor.snapshot(window, cx);
scroll_position = snapshot.scroll_position();
}
});
let line_elements = self.prepaint_lines(
start_row,
&mut line_layouts,
@@ -8862,7 +8863,7 @@ impl Element for EditorElement {
underline: None,
strikethrough: None,
}],
None
None,
);
let space_invisible = window.text_system().shape_line(
"".into(),
@@ -8875,7 +8876,7 @@ impl Element for EditorElement {
underline: None,
strikethrough: None,
}],
None
None,
);
let mode = snapshot.mode.clone();
@@ -8977,19 +8978,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),
@@ -10298,7 +10301,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,

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

@@ -8,6 +8,7 @@ mod tool_metrics;
use assertions::{AssertionsReport, display_error_row};
use instance::{ExampleInstance, JudgeOutput, RunOutput, run_git};
use language_extension::LspAccess;
pub(crate) use tool_metrics::*;
use ::fs::RealFs;
@@ -415,7 +416,11 @@ pub fn init(cx: &mut App) -> Arc<AgentAppState> {
language::init(cx);
debug_adapter_extension::init(extension_host_proxy.clone(), cx);
language_extension::init(extension_host_proxy.clone(), languages.clone());
language_extension::init(
LspAccess::Noop,
extension_host_proxy.clone(),
languages.clone(),
);
language_model::init(client.clone(), cx);
language_models::init(user_store.clone(), client.clone(), cx);
languages::init(languages.clone(), node_runtime.clone(), cx);

View File

@@ -18,7 +18,7 @@
Publisher="CN=Zed Industries Inc, O=Zed Industries Inc, L=Denver, S=Colorado, C=US"
Version="1.0.0.0" />
<Properties>
<DisplayName>Zed Editor Nightly</DisplayName>
<DisplayName>Zed Nightly</DisplayName>
<PublisherDisplayName>Zed Industries</PublisherDisplayName>
<!-- TODO: Use actual icon here. -->
<Logo>resources\logo_150x150.png</Logo>
@@ -45,8 +45,8 @@
<!-- TODO: Use actual icon here. -->
<uap:VisualElements
AppListEntry="none"
DisplayName="Zed Editor Nightly"
Description="Zed Editor Nightly explorer command injector"
DisplayName="Zed Nightly"
Description="Zed Nightly explorer command injector"
BackgroundColor="transparent"
Square150x150Logo="resources\logo_150x150.png"
Square44x44Logo="resources\logo_70x70.png">
@@ -67,7 +67,7 @@
</desktop4:Extension>
<com:Extension Category="windows.comServer">
<com:ComServer>
<com:SurrogateServer DisplayName="Zed Editor Nightly">
<com:SurrogateServer DisplayName="Zed Nightly">
<com:Class Id="266f2cfe-1653-42af-b55c-fe3590c83871" Path="zed_explorer_command_injector.dll" ThreadingModel="STA"/>
</com:SurrogateServer>
</com:ComServer>

View File

@@ -18,7 +18,7 @@
Publisher="CN=Zed Industries Inc, O=Zed Industries Inc, L=Denver, S=Colorado, C=US"
Version="1.0.0.0" />
<Properties>
<DisplayName>Zed Editor Preview</DisplayName>
<DisplayName>Zed Preview</DisplayName>
<PublisherDisplayName>Zed Industries</PublisherDisplayName>
<!-- TODO: Use actual icon here. -->
<Logo>resources\logo_150x150.png</Logo>
@@ -45,8 +45,8 @@
<!-- TODO: Use actual icon here. -->
<uap:VisualElements
AppListEntry="none"
DisplayName="Zed Editor Preview"
Description="Zed Editor Preview explorer command injector"
DisplayName="Zed Preview"
Description="Zed Preview explorer command injector"
BackgroundColor="transparent"
Square150x150Logo="resources\logo_150x150.png"
Square44x44Logo="resources\logo_70x70.png">
@@ -67,7 +67,7 @@
</desktop4:Extension>
<com:Extension Category="windows.comServer">
<com:ComServer>
<com:SurrogateServer DisplayName="Zed Editor Preview">
<com:SurrogateServer DisplayName="Zed Preview">
<com:Class Id="af8e85ea-fb20-4db2-93cf-56513c1ec697" Path="zed_explorer_command_injector.dll" ThreadingModel="STA"/>
</com:SurrogateServer>
</com:ComServer>

View File

@@ -18,7 +18,7 @@
Publisher="CN=Zed Industries Inc, O=Zed Industries Inc, L=Denver, S=Colorado, C=US"
Version="1.0.0.0" />
<Properties>
<DisplayName>Zed Editor</DisplayName>
<DisplayName>Zed</DisplayName>
<PublisherDisplayName>Zed Industries</PublisherDisplayName>
<!-- TODO: Use actual icon here. -->
@@ -46,8 +46,8 @@
<!-- TODO: Use actual icon here. -->
<uap:VisualElements
AppListEntry="none"
DisplayName="Zed Editor"
Description="Zed Editor explorer command injector"
DisplayName="Zed"
Description="Zed explorer command injector"
BackgroundColor="transparent"
Square150x150Logo="resources\logo_150x150.png"
Square44x44Logo="resources\logo_70x70.png">
@@ -68,7 +68,7 @@
</desktop4:Extension>
<com:Extension Category="windows.comServer">
<com:ComServer>
<com:SurrogateServer DisplayName="Zed Editor">
<com:SurrogateServer DisplayName="Zed">
<com:Class Id="6a1f6b13-3b82-48a1-9e06-7bb0a6d0bffd" Path="zed_explorer_command_injector.dll" ThreadingModel="STA"/>
</com:SurrogateServer>
</com:ComServer>

View File

@@ -286,7 +286,8 @@ pub trait ExtensionLanguageServerProxy: Send + Sync + 'static {
&self,
language: &LanguageName,
language_server_id: &LanguageServerName,
);
cx: &mut App,
) -> Task<Result<()>>;
fn update_language_server_status(
&self,
@@ -313,12 +314,13 @@ impl ExtensionLanguageServerProxy for ExtensionHostProxy {
&self,
language: &LanguageName,
language_server_id: &LanguageServerName,
) {
cx: &mut App,
) -> Task<Result<()>> {
let Some(proxy) = self.language_server_proxy.read().clone() else {
return;
return Task::ready(Ok(()));
};
proxy.remove_language_server(language, language_server_id)
proxy.remove_language_server(language, language_server_id, cx)
}
fn update_language_server_status(
@@ -350,6 +352,8 @@ impl ExtensionSnippetProxy for ExtensionHostProxy {
pub trait ExtensionSlashCommandProxy: Send + Sync + 'static {
fn register_slash_command(&self, extension: Arc<dyn Extension>, command: SlashCommand);
fn unregister_slash_command(&self, command_name: Arc<str>);
}
impl ExtensionSlashCommandProxy for ExtensionHostProxy {
@@ -360,6 +364,14 @@ impl ExtensionSlashCommandProxy for ExtensionHostProxy {
proxy.register_slash_command(extension, command)
}
fn unregister_slash_command(&self, command_name: Arc<str>) {
let Some(proxy) = self.slash_command_proxy.read().clone() else {
return;
};
proxy.unregister_slash_command(command_name)
}
}
pub trait ExtensionContextServerProxy: Send + Sync + 'static {
@@ -398,6 +410,8 @@ impl ExtensionContextServerProxy for ExtensionHostProxy {
pub trait ExtensionIndexedDocsProviderProxy: Send + Sync + 'static {
fn register_indexed_docs_provider(&self, extension: Arc<dyn Extension>, provider_id: Arc<str>);
fn unregister_indexed_docs_provider(&self, provider_id: Arc<str>);
}
impl ExtensionIndexedDocsProviderProxy for ExtensionHostProxy {
@@ -408,6 +422,14 @@ impl ExtensionIndexedDocsProviderProxy for ExtensionHostProxy {
proxy.register_indexed_docs_provider(extension, provider_id)
}
fn unregister_indexed_docs_provider(&self, provider_id: Arc<str>) {
let Some(proxy) = self.indexed_docs_provider_proxy.read().clone() else {
return;
};
proxy.unregister_indexed_docs_provider(provider_id)
}
}
pub trait ExtensionDebugAdapterProviderProxy: Send + Sync + 'static {

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

@@ -20,6 +20,7 @@ use extension::{
ExtensionSnippetProxy, ExtensionThemeProxy,
};
use fs::{Fs, RemoveOptions};
use futures::future::join_all;
use futures::{
AsyncReadExt as _, Future, FutureExt as _, StreamExt as _,
channel::{
@@ -860,8 +861,8 @@ impl ExtensionStore {
btree_map::Entry::Vacant(e) => e.insert(ExtensionOperation::Remove),
};
cx.spawn(async move |this, cx| {
let _finish = cx.on_drop(&this, {
cx.spawn(async move |extension_store, cx| {
let _finish = cx.on_drop(&extension_store, {
let extension_id = extension_id.clone();
move |this, cx| {
this.outstanding_operations.remove(extension_id.as_ref());
@@ -876,22 +877,39 @@ impl ExtensionStore {
ignore_if_not_exists: true,
},
)
.await?;
.await
.with_context(|| format!("Removing extension dir {extension_dir:?}"))?;
// todo(windows)
// Stop the server here.
this.update(cx, |this, cx| this.reload(None, cx))?.await;
extension_store
.update(cx, |extension_store, cx| extension_store.reload(None, cx))?
.await;
fs.remove_dir(
&work_dir,
RemoveOptions {
recursive: true,
ignore_if_not_exists: true,
},
)
.await?;
// There's a race between wasm extension fully stopping and the directory removal.
// On Windows, it's impossible to remove a directory that has a process running in it.
for i in 0..3 {
cx.background_executor()
.timer(Duration::from_millis(i * 100))
.await;
let removal_result = fs
.remove_dir(
&work_dir,
RemoveOptions {
recursive: true,
ignore_if_not_exists: true,
},
)
.await;
match removal_result {
Ok(()) => break,
Err(e) => {
if i == 2 {
log::error!("Failed to remove extension work dir {work_dir:?} : {e}");
}
}
}
}
this.update(cx, |_, cx| {
extension_store.update(cx, |_, cx| {
cx.emit(Event::ExtensionUninstalled(extension_id.clone()));
if let Some(events) = ExtensionEvents::try_global(cx) {
if let Some(manifest) = extension_manifest {
@@ -1143,27 +1161,38 @@ impl ExtensionStore {
})
.collect::<Vec<_>>();
let mut grammars_to_remove = Vec::new();
let mut server_removal_tasks = Vec::with_capacity(extensions_to_unload.len());
for extension_id in &extensions_to_unload {
let Some(extension) = old_index.extensions.get(extension_id) else {
continue;
};
grammars_to_remove.extend(extension.manifest.grammars.keys().cloned());
for (language_server_name, config) in extension.manifest.language_servers.iter() {
for (language_server_name, config) in &extension.manifest.language_servers {
for language in config.languages() {
self.proxy
.remove_language_server(&language, language_server_name);
server_removal_tasks.push(self.proxy.remove_language_server(
&language,
language_server_name,
cx,
));
}
}
for (server_id, _) in extension.manifest.context_servers.iter() {
for (server_id, _) in &extension.manifest.context_servers {
self.proxy.unregister_context_server(server_id.clone(), cx);
}
for (adapter, _) in extension.manifest.debug_adapters.iter() {
for (adapter, _) in &extension.manifest.debug_adapters {
self.proxy.unregister_debug_adapter(adapter.clone());
}
for (locator, _) in extension.manifest.debug_locators.iter() {
for (locator, _) in &extension.manifest.debug_locators {
self.proxy.unregister_debug_locator(locator.clone());
}
for (command_name, _) in &extension.manifest.slash_commands {
self.proxy.unregister_slash_command(command_name.clone());
}
for (provider_id, _) in &extension.manifest.indexed_docs_providers {
self.proxy
.unregister_indexed_docs_provider(provider_id.clone());
}
}
self.wasm_extensions
@@ -1268,14 +1297,15 @@ impl ExtensionStore {
cx.background_spawn({
let fs = fs.clone();
async move {
for theme_path in themes_to_add.into_iter() {
let _ = join_all(server_removal_tasks).await;
for theme_path in themes_to_add {
proxy
.load_user_theme(theme_path, fs.clone())
.await
.log_err();
}
for (icon_theme_path, icons_root_path) in icon_themes_to_add.into_iter() {
for (icon_theme_path, icons_root_path) in icon_themes_to_add {
proxy
.load_icon_theme(icon_theme_path, icons_root_path, fs.clone())
.await

View File

@@ -11,6 +11,7 @@ use futures::{AsyncReadExt, StreamExt, io::BufReader};
use gpui::{AppContext as _, SemanticVersion, TestAppContext};
use http_client::{FakeHttpClient, Response};
use language::{BinaryStatus, LanguageMatcher, LanguageRegistry};
use language_extension::LspAccess;
use lsp::LanguageServerName;
use node_runtime::NodeRuntime;
use parking_lot::Mutex;
@@ -271,7 +272,7 @@ async fn test_extension_store(cx: &mut TestAppContext) {
let theme_registry = Arc::new(ThemeRegistry::new(Box::new(())));
theme_extension::init(proxy.clone(), theme_registry.clone(), cx.executor());
let language_registry = Arc::new(LanguageRegistry::test(cx.executor()));
language_extension::init(proxy.clone(), language_registry.clone());
language_extension::init(LspAccess::Noop, proxy.clone(), language_registry.clone());
let node_runtime = NodeRuntime::unavailable();
let store = cx.new(|cx| {
@@ -554,7 +555,11 @@ async fn test_extension_store_with_test_extension(cx: &mut TestAppContext) {
let theme_registry = Arc::new(ThemeRegistry::new(Box::new(())));
theme_extension::init(proxy.clone(), theme_registry.clone(), cx.executor());
let language_registry = project.read_with(cx, |project, _cx| project.languages().clone());
language_extension::init(proxy.clone(), language_registry.clone());
language_extension::init(
LspAccess::ViaLspStore(project.update(cx, |project, _| project.lsp_store())),
proxy.clone(),
language_registry.clone(),
);
let node_runtime = NodeRuntime::unavailable();
let mut status_updates = language_registry.language_server_binary_statuses();
@@ -815,7 +820,6 @@ async fn test_extension_store_with_test_extension(cx: &mut TestAppContext) {
extension_store
.update(cx, |store, cx| store.reload(Some("gleam".into()), cx))
.await;
cx.executor().run_until_parked();
project.update(cx, |project, cx| {
project.restart_language_servers_for_buffers(vec![buffer.clone()], HashSet::default(), cx)

View File

@@ -11,6 +11,7 @@ use extension::{
ExtensionLanguageServerProxy, ExtensionManifest,
};
use fs::{Fs, RemoveOptions, RenameOptions};
use futures::future::join_all;
use gpui::{App, AppContext as _, AsyncApp, Context, Entity, Task, WeakEntity};
use http_client::HttpClient;
use language::{LanguageConfig, LanguageName, LanguageQueries, LoadedLanguage};
@@ -230,18 +231,27 @@ impl HeadlessExtensionStore {
.unwrap_or_default();
self.proxy.remove_languages(&languages_to_remove, &[]);
for (language_server_name, language) in self
let servers_to_remove = self
.loaded_language_servers
.remove(extension_id)
.unwrap_or_default()
{
self.proxy
.remove_language_server(&language, &language_server_name);
}
.unwrap_or_default();
let proxy = self.proxy.clone();
let path = self.extension_dir.join(&extension_id.to_string());
let fs = self.fs.clone();
cx.spawn(async move |_, _| {
cx.spawn(async move |_, cx| {
let mut removal_tasks = Vec::with_capacity(servers_to_remove.len());
cx.update(|cx| {
for (language_server_name, language) in servers_to_remove {
removal_tasks.push(proxy.remove_language_server(
&language,
&language_server_name,
cx,
));
}
})
.ok();
let _ = join_all(removal_tasks).await;
fs.remove_dir(
&path,
RemoveOptions {
@@ -250,6 +260,7 @@ impl HeadlessExtensionStore {
},
)
.await
.with_context(|| format!("Removing directory {path:?}"))
})
}

View File

@@ -54,7 +54,7 @@ pub struct WasmHost {
main_thread_message_tx: mpsc::UnboundedSender<MainThreadCall>,
}
#[derive(Clone)]
#[derive(Clone, Debug)]
pub struct WasmExtension {
tx: UnboundedSender<ExtensionCall>,
pub manifest: Arc<ExtensionManifest>,
@@ -63,6 +63,12 @@ pub struct WasmExtension {
pub zed_api_version: SemanticVersion,
}
impl Drop for WasmExtension {
fn drop(&mut self) {
self.tx.close_channel();
}
}
#[async_trait]
impl extension::Extension for WasmExtension {
fn manifest(&self) -> Arc<ExtensionManifest> {
@@ -742,7 +748,6 @@ impl WasmExtension {
{
let (return_tx, return_rx) = oneshot::channel();
self.tx
.clone()
.unbounded_send(Box::new(move |extension, store| {
async {
let result = f(extension, store).await;

View File

@@ -54,6 +54,7 @@ const SUGGESTIONS_BY_EXTENSION_ID: &[(&str, &[&str])] = &[
("nu", &["nu"]),
("ocaml", &["ml", "mli"]),
("php", &["php"]),
("powershell", &["ps1", "psm1"]),
("prisma", &["prisma"]),
("proto", &["proto"]),
("purescript", &["purs"]),

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(

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.

View File

@@ -11,10 +11,7 @@ use gpui::{
use language::{Anchor, Buffer, BufferId};
use project::{ConflictRegion, ConflictSet, ConflictSetUpdate, ProjectItem as _};
use std::{ops::Range, sync::Arc};
use ui::{
ActiveTheme, AnyElement, Element as _, StatefulInteractiveElement, Styled,
StyledTypography as _, Window, div, h_flex, rems,
};
use ui::{ActiveTheme, Element as _, Styled, Window, prelude::*};
use util::{ResultExt as _, debug_panic, maybe};
pub(crate) struct ConflictAddon {
@@ -300,7 +297,6 @@ fn conflicts_updated(
move |cx| render_conflict_buttons(&conflict, excerpt_id, editor_handle.clone(), cx)
}),
priority: 0,
render_in_minimap: true,
})
}
let new_block_ids = editor.insert_blocks(blocks, None, cx);
@@ -391,20 +387,15 @@ fn render_conflict_buttons(
cx: &mut BlockContext,
) -> AnyElement {
h_flex()
.h(cx.line_height)
.items_end()
.ml(cx.margins.gutter.width)
.id(cx.block_id)
.gap_0p5()
.h(cx.line_height)
.ml(cx.margins.gutter.width)
.items_end()
.gap_1()
.bg(cx.theme().colors().editor_background)
.child(
div()
.id("ours")
.px_1()
.child("Take Ours")
.rounded_t(rems(0.2))
.text_ui_sm(cx)
.hover(|this| this.bg(cx.theme().colors().element_background))
.cursor_pointer()
Button::new("head", "Use HEAD")
.label_size(LabelSize::Small)
.on_click({
let editor = editor.clone();
let conflict = conflict.clone();
@@ -423,14 +414,8 @@ fn render_conflict_buttons(
}),
)
.child(
div()
.id("theirs")
.px_1()
.child("Take Theirs")
.rounded_t(rems(0.2))
.text_ui_sm(cx)
.hover(|this| this.bg(cx.theme().colors().element_background))
.cursor_pointer()
Button::new("origin", "Use Origin")
.label_size(LabelSize::Small)
.on_click({
let editor = editor.clone();
let conflict = conflict.clone();
@@ -449,14 +434,8 @@ fn render_conflict_buttons(
}),
)
.child(
div()
.id("both")
.px_1()
.child("Take Both")
.rounded_t(rems(0.2))
.text_ui_sm(cx)
.hover(|this| this.bg(cx.theme().colors().element_background))
.cursor_pointer()
Button::new("both", "Use Both")
.label_size(LabelSize::Small)
.on_click({
let editor = editor.clone();
let conflict = conflict.clone();

View File

@@ -30,10 +30,9 @@ use git::{ExpandCommitEditor, RestoreTrackedFiles, StageAll, TrashUntrackedFiles
use gpui::{
Action, Animation, AnimationExt as _, AsyncApp, AsyncWindowContext, Axis, ClickEvent, Corner,
DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, KeyContext,
ListHorizontalSizingBehavior, ListSizingBehavior, Modifiers, ModifiersChangedEvent,
MouseButton, MouseDownEvent, Point, PromptLevel, ScrollStrategy, Subscription, Task,
Transformation, UniformListScrollHandle, WeakEntity, actions, anchored, deferred, percentage,
uniform_list,
ListHorizontalSizingBehavior, ListSizingBehavior, MouseButton, MouseDownEvent, Point,
PromptLevel, ScrollStrategy, Subscription, Task, Transformation, UniformListScrollHandle,
WeakEntity, actions, anchored, deferred, percentage, uniform_list,
};
use itertools::Itertools;
use language::{Buffer, File};
@@ -48,7 +47,7 @@ use panel::{
PanelHeader, panel_button, panel_editor_container, panel_editor_style, panel_filled_button,
panel_icon_button,
};
use project::git_store::RepositoryEvent;
use project::git_store::{RepositoryEvent, RepositoryId};
use project::{
Fs, Project, ProjectPath,
git_store::{GitStoreEvent, Repository},
@@ -212,14 +211,14 @@ impl GitHeaderEntry {
#[derive(Debug, PartialEq, Eq, Clone)]
enum GitListEntry {
GitStatusEntry(GitStatusEntry),
Status(GitStatusEntry),
Header(GitHeaderEntry),
}
impl GitListEntry {
fn status_entry(&self) -> Option<&GitStatusEntry> {
match self {
GitListEntry::GitStatusEntry(entry) => Some(entry),
GitListEntry::Status(entry) => Some(entry),
_ => None,
}
}
@@ -323,7 +322,6 @@ pub struct GitPanel {
pub(crate) commit_editor: Entity<Editor>,
conflicted_count: usize,
conflicted_staged_count: usize,
current_modifiers: Modifiers,
add_coauthors: bool,
generate_commit_message_task: Option<Task<Option<()>>>,
entries: Vec<GitListEntry>,
@@ -355,9 +353,16 @@ pub struct GitPanel {
show_placeholders: bool,
local_committer: Option<GitCommitter>,
local_committer_task: Option<Task<()>>,
bulk_staging: Option<BulkStaging>,
_settings_subscription: Subscription,
}
#[derive(Clone, Debug, PartialEq, Eq)]
struct BulkStaging {
repo_id: RepositoryId,
anchor: RepoPath,
}
const MAX_PANEL_EDITOR_LINES: usize = 6;
pub(crate) fn commit_message_editor(
@@ -497,7 +502,6 @@ impl GitPanel {
commit_editor,
conflicted_count: 0,
conflicted_staged_count: 0,
current_modifiers: window.modifiers(),
add_coauthors: true,
generate_commit_message_task: None,
entries: Vec::new(),
@@ -529,6 +533,7 @@ impl GitPanel {
entry_count: 0,
horizontal_scrollbar,
vertical_scrollbar,
bulk_staging: None,
_settings_subscription,
};
@@ -735,16 +740,6 @@ impl GitPanel {
}
}
fn handle_modifiers_changed(
&mut self,
event: &ModifiersChangedEvent,
_: &mut Window,
cx: &mut Context<Self>,
) {
self.current_modifiers = event.modifiers;
cx.notify();
}
fn scroll_to_selected_entry(&mut self, cx: &mut Context<Self>) {
if let Some(selected_entry) = self.selected_entry {
self.scroll_handle
@@ -1265,10 +1260,18 @@ impl GitPanel {
return;
};
let (stage, repo_paths) = match entry {
GitListEntry::GitStatusEntry(status_entry) => {
GitListEntry::Status(status_entry) => {
if status_entry.status.staging().is_fully_staged() {
if let Some(op) = self.bulk_staging.clone()
&& op.anchor == status_entry.repo_path
{
self.bulk_staging = None;
}
(false, vec![status_entry.clone()])
} else {
self.set_bulk_staging_anchor(status_entry.repo_path.clone(), cx);
(true, vec![status_entry.clone()])
}
}
@@ -1383,6 +1386,13 @@ impl GitPanel {
}
}
fn stage_range(&mut self, _: &git::StageRange, _window: &mut Window, cx: &mut Context<Self>) {
let Some(index) = self.selected_entry else {
return;
};
self.stage_bulk(index, cx);
}
fn stage_selected(&mut self, _: &git::StageFile, _window: &mut Window, cx: &mut Context<Self>) {
let Some(selected_entry) = self.get_selected_entry() else {
return;
@@ -2449,6 +2459,11 @@ impl GitPanel {
}
fn update_visible_entries(&mut self, cx: &mut Context<Self>) {
let bulk_staging = self.bulk_staging.take();
let last_staged_path_prev_index = bulk_staging
.as_ref()
.and_then(|op| self.entry_by_path(&op.anchor, cx));
self.entries.clear();
self.single_staged_entry.take();
self.single_tracked_entry.take();
@@ -2465,7 +2480,7 @@ impl GitPanel {
let mut changed_entries = Vec::new();
let mut new_entries = Vec::new();
let mut conflict_entries = Vec::new();
let mut last_staged = None;
let mut single_staged_entry = None;
let mut staged_count = 0;
let mut max_width_item: Option<(RepoPath, usize)> = None;
@@ -2503,7 +2518,7 @@ impl GitPanel {
if staging.has_staged() {
staged_count += 1;
last_staged = Some(entry.clone());
single_staged_entry = Some(entry.clone());
}
let width_estimate = Self::item_width_estimate(
@@ -2534,27 +2549,27 @@ impl GitPanel {
let mut pending_staged_count = 0;
let mut last_pending_staged = None;
let mut pending_status_for_last_staged = None;
let mut pending_status_for_single_staged = None;
for pending in self.pending.iter() {
if pending.target_status == TargetStatus::Staged {
pending_staged_count += pending.entries.len();
last_pending_staged = pending.entries.iter().next().cloned();
}
if let Some(last_staged) = &last_staged {
if let Some(single_staged) = &single_staged_entry {
if pending
.entries
.iter()
.any(|entry| entry.repo_path == last_staged.repo_path)
.any(|entry| entry.repo_path == single_staged.repo_path)
{
pending_status_for_last_staged = Some(pending.target_status);
pending_status_for_single_staged = Some(pending.target_status);
}
}
}
if conflict_entries.len() == 0 && staged_count == 1 && pending_staged_count == 0 {
match pending_status_for_last_staged {
match pending_status_for_single_staged {
Some(TargetStatus::Staged) | None => {
self.single_staged_entry = last_staged;
self.single_staged_entry = single_staged_entry;
}
_ => {}
}
@@ -2570,11 +2585,8 @@ impl GitPanel {
self.entries.push(GitListEntry::Header(GitHeaderEntry {
header: Section::Conflict,
}));
self.entries.extend(
conflict_entries
.into_iter()
.map(GitListEntry::GitStatusEntry),
);
self.entries
.extend(conflict_entries.into_iter().map(GitListEntry::Status));
}
if changed_entries.len() > 0 {
@@ -2583,31 +2595,39 @@ impl GitPanel {
header: Section::Tracked,
}));
}
self.entries.extend(
changed_entries
.into_iter()
.map(GitListEntry::GitStatusEntry),
);
self.entries
.extend(changed_entries.into_iter().map(GitListEntry::Status));
}
if new_entries.len() > 0 {
self.entries.push(GitListEntry::Header(GitHeaderEntry {
header: Section::New,
}));
self.entries
.extend(new_entries.into_iter().map(GitListEntry::GitStatusEntry));
.extend(new_entries.into_iter().map(GitListEntry::Status));
}
if let Some((repo_path, _)) = max_width_item {
self.max_width_item_index = self.entries.iter().position(|entry| match entry {
GitListEntry::GitStatusEntry(git_status_entry) => {
git_status_entry.repo_path == repo_path
}
GitListEntry::Status(git_status_entry) => git_status_entry.repo_path == repo_path,
GitListEntry::Header(_) => false,
});
}
self.update_counts(repo);
let bulk_staging_anchor_new_index = bulk_staging
.as_ref()
.filter(|op| op.repo_id == repo.id)
.and_then(|op| self.entry_by_path(&op.anchor, cx));
if bulk_staging_anchor_new_index == last_staged_path_prev_index
&& let Some(index) = bulk_staging_anchor_new_index
&& let Some(entry) = self.entries.get(index)
&& let Some(entry) = entry.status_entry()
&& self.entry_staging(entry) == StageStatus::Staged
{
self.bulk_staging = bulk_staging;
}
self.select_first_entry_if_none(cx);
let suggested_commit_message = self.suggest_commit_message(cx);
@@ -3743,7 +3763,7 @@ impl GitPanel {
for ix in range {
match &this.entries.get(ix) {
Some(GitListEntry::GitStatusEntry(entry)) => {
Some(GitListEntry::Status(entry)) => {
items.push(this.render_entry(
ix,
entry,
@@ -4000,8 +4020,6 @@ impl GitPanel {
let marked = self.marked_entries.contains(&ix);
let status_style = GitPanelSettings::get_global(cx).status_style;
let status = entry.status;
let modifiers = self.current_modifiers;
let shift_held = modifiers.shift;
let has_conflict = status.is_conflicted();
let is_modified = status.is_modified();
@@ -4120,12 +4138,6 @@ impl GitPanel {
cx.stop_propagation();
},
)
// .on_secondary_mouse_down(cx.listener(
// move |this, event: &MouseDownEvent, window, cx| {
// this.deploy_entry_context_menu(event.position, ix, window, cx);
// cx.stop_propagation();
// },
// ))
.child(
div()
.id(checkbox_wrapper_id)
@@ -4137,46 +4149,35 @@ impl GitPanel {
.disabled(!has_write_access)
.fill()
.elevation(ElevationIndex::Surface)
.on_click({
.on_click_ext({
let entry = entry.clone();
cx.listener(move |this, _, window, cx| {
if !has_write_access {
return;
}
this.toggle_staged_for_entry(
&GitListEntry::GitStatusEntry(entry.clone()),
window,
cx,
);
cx.stop_propagation();
})
let this = cx.weak_entity();
move |_, click, window, cx| {
this.update(cx, |this, cx| {
if !has_write_access {
return;
}
if click.modifiers().shift {
this.stage_bulk(ix, cx);
} else {
this.toggle_staged_for_entry(
&GitListEntry::Status(entry.clone()),
window,
cx,
);
}
cx.stop_propagation();
})
.ok();
}
})
.tooltip(move |window, cx| {
let is_staged = entry_staging.is_fully_staged();
let action = if is_staged { "Unstage" } else { "Stage" };
let tooltip_name = if shift_held {
format!("{} section", action)
} else {
action.to_string()
};
let tooltip_name = action.to_string();
let meta = if shift_held {
format!(
"Release shift to {} single entry",
action.to_lowercase()
)
} else {
format!("Shift click to {} section", action.to_lowercase())
};
Tooltip::with_meta(
tooltip_name,
Some(&ToggleStaged),
meta,
window,
cx,
)
Tooltip::for_action(tooltip_name, &ToggleStaged, window, cx)
}),
),
)
@@ -4242,6 +4243,41 @@ impl GitPanel {
panel
})
}
fn stage_bulk(&mut self, mut index: usize, cx: &mut Context<'_, Self>) {
let Some(op) = self.bulk_staging.as_ref() else {
return;
};
let Some(mut anchor_index) = self.entry_by_path(&op.anchor, cx) else {
return;
};
if let Some(entry) = self.entries.get(index)
&& let Some(entry) = entry.status_entry()
{
self.set_bulk_staging_anchor(entry.repo_path.clone(), cx);
}
if index < anchor_index {
std::mem::swap(&mut index, &mut anchor_index);
}
let entries = self
.entries
.get(anchor_index..=index)
.unwrap_or_default()
.iter()
.filter_map(|entry| entry.status_entry().cloned())
.collect::<Vec<_>>();
self.change_file_stage(true, entries, cx);
}
fn set_bulk_staging_anchor(&mut self, path: RepoPath, cx: &mut Context<'_, GitPanel>) {
let Some(repo) = self.active_repository.as_ref() else {
return;
};
self.bulk_staging = Some(BulkStaging {
repo_id: repo.read(cx).id,
anchor: path,
});
}
}
fn current_language_model(cx: &Context<'_, GitPanel>) -> Option<Arc<dyn LanguageModel>> {
@@ -4279,9 +4315,9 @@ impl Render for GitPanel {
.id("git_panel")
.key_context(self.dispatch_context(window, cx))
.track_focus(&self.focus_handle)
.on_modifiers_changed(cx.listener(Self::handle_modifiers_changed))
.when(has_write_access && !project.is_read_only(cx), |this| {
this.on_action(cx.listener(Self::toggle_staged_for_selected))
.on_action(cx.listener(Self::stage_range))
.on_action(cx.listener(GitPanel::commit))
.on_action(cx.listener(GitPanel::amend))
.on_action(cx.listener(GitPanel::cancel))
@@ -4953,7 +4989,7 @@ impl Component for PanelRepoFooter {
#[cfg(test)]
mod tests {
use git::status::StatusCode;
use git::status::{StatusCode, UnmergedStatus, UnmergedStatusCode};
use gpui::{TestAppContext, VisualTestContext};
use project::{FakeFs, WorktreeSettings};
use serde_json::json;
@@ -5052,13 +5088,13 @@ mod tests {
GitListEntry::Header(GitHeaderEntry {
header: Section::Tracked
}),
GitListEntry::GitStatusEntry(GitStatusEntry {
GitListEntry::Status(GitStatusEntry {
abs_path: path!("/root/zed/crates/gpui/gpui.rs").into(),
repo_path: "crates/gpui/gpui.rs".into(),
status: StatusCode::Modified.worktree(),
staging: StageStatus::Unstaged,
}),
GitListEntry::GitStatusEntry(GitStatusEntry {
GitListEntry::Status(GitStatusEntry {
abs_path: path!("/root/zed/crates/util/util.rs").into(),
repo_path: "crates/util/util.rs".into(),
status: StatusCode::Modified.worktree(),
@@ -5067,54 +5103,6 @@ mod tests {
],
);
// TODO(cole) restore this once repository deduplication is implemented properly.
//cx.update_window_entity(&panel, |panel, window, cx| {
// panel.select_last(&Default::default(), window, cx);
// assert_eq!(panel.selected_entry, Some(2));
// panel.open_diff(&Default::default(), window, cx);
//});
//cx.run_until_parked();
//let worktree_roots = workspace.update(cx, |workspace, cx| {
// workspace
// .worktrees(cx)
// .map(|worktree| worktree.read(cx).abs_path())
// .collect::<Vec<_>>()
//});
//pretty_assertions::assert_eq!(
// worktree_roots,
// vec![
// Path::new(path!("/root/zed/crates/gpui")).into(),
// Path::new(path!("/root/zed/crates/util/util.rs")).into(),
// ]
//);
//project.update(cx, |project, cx| {
// let git_store = project.git_store().read(cx);
// // The repo that comes from the single-file worktree can't be selected through the UI.
// let filtered_entries = filtered_repository_entries(git_store, cx)
// .iter()
// .map(|repo| repo.read(cx).worktree_abs_path.clone())
// .collect::<Vec<_>>();
// assert_eq!(
// filtered_entries,
// [Path::new(path!("/root/zed/crates/gpui")).into()]
// );
// // But we can select it artificially here.
// let repo_from_single_file_worktree = git_store
// .repositories()
// .values()
// .find(|repo| {
// repo.read(cx).worktree_abs_path.as_ref()
// == Path::new(path!("/root/zed/crates/util/util.rs"))
// })
// .unwrap()
// .clone();
// // Paths still make sense when we somehow activate a repo that comes from a single-file worktree.
// repo_from_single_file_worktree.update(cx, |repo, cx| repo.set_as_active_repository(cx));
//});
let handle = cx.update_window_entity(&panel, |panel, _, _| {
std::mem::replace(&mut panel.update_visible_entries_task, Task::ready(()))
});
@@ -5127,13 +5115,13 @@ mod tests {
GitListEntry::Header(GitHeaderEntry {
header: Section::Tracked
}),
GitListEntry::GitStatusEntry(GitStatusEntry {
GitListEntry::Status(GitStatusEntry {
abs_path: path!("/root/zed/crates/gpui/gpui.rs").into(),
repo_path: "crates/gpui/gpui.rs".into(),
status: StatusCode::Modified.worktree(),
staging: StageStatus::Unstaged,
}),
GitListEntry::GitStatusEntry(GitStatusEntry {
GitListEntry::Status(GitStatusEntry {
abs_path: path!("/root/zed/crates/util/util.rs").into(),
repo_path: "crates/util/util.rs".into(),
status: StatusCode::Modified.worktree(),
@@ -5142,4 +5130,196 @@ mod tests {
],
);
}
#[gpui::test]
async fn test_bulk_staging(cx: &mut TestAppContext) {
use GitListEntry::*;
init_test(cx);
let fs = FakeFs::new(cx.background_executor.clone());
fs.insert_tree(
"/root",
json!({
"project": {
".git": {},
"src": {
"main.rs": "fn main() {}",
"lib.rs": "pub fn hello() {}",
"utils.rs": "pub fn util() {}"
},
"tests": {
"test.rs": "fn test() {}"
},
"new_file.txt": "new content",
"another_new.rs": "// new file",
"conflict.txt": "conflicted content"
}
}),
)
.await;
fs.set_status_for_repo(
Path::new(path!("/root/project/.git")),
&[
(Path::new("src/main.rs"), StatusCode::Modified.worktree()),
(Path::new("src/lib.rs"), StatusCode::Modified.worktree()),
(Path::new("tests/test.rs"), StatusCode::Modified.worktree()),
(Path::new("new_file.txt"), FileStatus::Untracked),
(Path::new("another_new.rs"), FileStatus::Untracked),
(Path::new("src/utils.rs"), FileStatus::Untracked),
(
Path::new("conflict.txt"),
UnmergedStatus {
first_head: UnmergedStatusCode::Updated,
second_head: UnmergedStatusCode::Updated,
}
.into(),
),
],
);
let project = Project::test(fs.clone(), [Path::new(path!("/root/project"))], cx).await;
let workspace =
cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
let cx = &mut VisualTestContext::from_window(*workspace, cx);
cx.read(|cx| {
project
.read(cx)
.worktrees(cx)
.nth(0)
.unwrap()
.read(cx)
.as_local()
.unwrap()
.scan_complete()
})
.await;
cx.executor().run_until_parked();
let panel = workspace.update(cx, GitPanel::new).unwrap();
let handle = cx.update_window_entity(&panel, |panel, _, _| {
std::mem::replace(&mut panel.update_visible_entries_task, Task::ready(()))
});
cx.executor().advance_clock(2 * UPDATE_DEBOUNCE);
handle.await;
let entries = panel.read_with(cx, |panel, _| panel.entries.clone());
#[rustfmt::skip]
pretty_assertions::assert_matches!(
entries.as_slice(),
&[
Header(GitHeaderEntry { header: Section::Conflict }),
Status(GitStatusEntry { staging: StageStatus::Unstaged, .. }),
Header(GitHeaderEntry { header: Section::Tracked }),
Status(GitStatusEntry { staging: StageStatus::Unstaged, .. }),
Status(GitStatusEntry { staging: StageStatus::Unstaged, .. }),
Status(GitStatusEntry { staging: StageStatus::Unstaged, .. }),
Header(GitHeaderEntry { header: Section::New }),
Status(GitStatusEntry { staging: StageStatus::Unstaged, .. }),
Status(GitStatusEntry { staging: StageStatus::Unstaged, .. }),
Status(GitStatusEntry { staging: StageStatus::Unstaged, .. }),
],
);
let second_status_entry = entries[3].clone();
panel.update_in(cx, |panel, window, cx| {
panel.toggle_staged_for_entry(&second_status_entry, window, cx);
});
panel.update_in(cx, |panel, window, cx| {
panel.selected_entry = Some(7);
panel.stage_range(&git::StageRange, window, cx);
});
cx.read(|cx| {
project
.read(cx)
.worktrees(cx)
.nth(0)
.unwrap()
.read(cx)
.as_local()
.unwrap()
.scan_complete()
})
.await;
cx.executor().run_until_parked();
let handle = cx.update_window_entity(&panel, |panel, _, _| {
std::mem::replace(&mut panel.update_visible_entries_task, Task::ready(()))
});
cx.executor().advance_clock(2 * UPDATE_DEBOUNCE);
handle.await;
let entries = panel.read_with(cx, |panel, _| panel.entries.clone());
#[rustfmt::skip]
pretty_assertions::assert_matches!(
entries.as_slice(),
&[
Header(GitHeaderEntry { header: Section::Conflict }),
Status(GitStatusEntry { staging: StageStatus::Unstaged, .. }),
Header(GitHeaderEntry { header: Section::Tracked }),
Status(GitStatusEntry { staging: StageStatus::Staged, .. }),
Status(GitStatusEntry { staging: StageStatus::Staged, .. }),
Status(GitStatusEntry { staging: StageStatus::Staged, .. }),
Header(GitHeaderEntry { header: Section::New }),
Status(GitStatusEntry { staging: StageStatus::Staged, .. }),
Status(GitStatusEntry { staging: StageStatus::Unstaged, .. }),
Status(GitStatusEntry { staging: StageStatus::Unstaged, .. }),
],
);
let third_status_entry = entries[4].clone();
panel.update_in(cx, |panel, window, cx| {
panel.toggle_staged_for_entry(&third_status_entry, window, cx);
});
panel.update_in(cx, |panel, window, cx| {
panel.selected_entry = Some(9);
panel.stage_range(&git::StageRange, window, cx);
});
cx.read(|cx| {
project
.read(cx)
.worktrees(cx)
.nth(0)
.unwrap()
.read(cx)
.as_local()
.unwrap()
.scan_complete()
})
.await;
cx.executor().run_until_parked();
let handle = cx.update_window_entity(&panel, |panel, _, _| {
std::mem::replace(&mut panel.update_visible_entries_task, Task::ready(()))
});
cx.executor().advance_clock(2 * UPDATE_DEBOUNCE);
handle.await;
let entries = panel.read_with(cx, |panel, _| panel.entries.clone());
#[rustfmt::skip]
pretty_assertions::assert_matches!(
entries.as_slice(),
&[
Header(GitHeaderEntry { header: Section::Conflict }),
Status(GitStatusEntry { staging: StageStatus::Unstaged, .. }),
Header(GitHeaderEntry { header: Section::Tracked }),
Status(GitStatusEntry { staging: StageStatus::Staged, .. }),
Status(GitStatusEntry { staging: StageStatus::Unstaged, .. }),
Status(GitStatusEntry { staging: StageStatus::Staged, .. }),
Header(GitHeaderEntry { header: Section::New }),
Status(GitStatusEntry { staging: StageStatus::Staged, .. }),
Status(GitStatusEntry { staging: StageStatus::Staged, .. }),
Status(GitStatusEntry { staging: StageStatus::Staged, .. }),
],
);
}
}

View File

@@ -66,6 +66,7 @@ x11 = [
"x11-clipboard",
"filedescriptor",
"open",
"scap?/x11",
]
screen-capture = [
"scap",
@@ -150,6 +151,9 @@ metal.workspace = true
[target.'cfg(any(target_os = "linux", target_os = "freebsd", target_os = "macos"))'.dependencies]
pathfinder_geometry = "0.5"
[target.'cfg(any(target_os = "linux", target_os = "freebsd", target_os = "windows"))'.dependencies]
scap = { workspace = true, optional = true }
[target.'cfg(any(target_os = "linux", target_os = "freebsd"))'.dependencies]
# Always used
flume = "0.11"
@@ -168,7 +172,6 @@ cosmic-text = { version = "0.14.0", optional = true }
font-kit = { git = "https://github.com/zed-industries/font-kit", rev = "5474cfad4b719a72ec8ed2cb7327b2b01fd10568", features = [
"source-fontconfig-dlopen",
], optional = true }
scap = { workspace = true, optional = true }
calloop = { version = "0.13.0" }
filedescriptor = { version = "0.8.2", optional = true }

View File

@@ -903,7 +903,7 @@ pub trait InteractiveElement: Sized {
/// Apply the given style when the given data type is dragged over this element
fn drag_over<S: 'static>(
mut self,
f: impl 'static + Fn(StyleRefinement, &S, &Window, &App) -> StyleRefinement,
f: impl 'static + Fn(StyleRefinement, &S, &mut Window, &mut App) -> StyleRefinement,
) -> Self {
self.interactivity().drag_over_styles.push((
TypeId::of::<S>(),

View File

@@ -26,8 +26,13 @@ mod windows;
#[cfg(all(
feature = "screen-capture",
any(target_os = "linux", target_os = "freebsd"),
any(feature = "wayland", feature = "x11"),
any(
target_os = "windows",
all(
any(target_os = "linux", target_os = "freebsd"),
any(feature = "wayland", feature = "x11"),
)
)
))]
pub(crate) mod scap_screen_capture;

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