Compare commits

..

200 Commits

Author SHA1 Message Date
Antonio Scandurra
9d3af4d2d2 WIP 2025-05-06 20:25:59 +02:00
Antonio Scandurra
c92b2e31e1 Avoid panicking when edit agent emits an empty old_text tag (#30030)
Release Notes:

- Fixed a panic that could sometimes occur when the agent applies edits.

Co-authored-by: Nathan <nathan@zed.dev>
2025-05-06 18:20:10 +00:00
Piotr Osiewicz
09d3ff9dbe debugger: Rework language association with the debuggers (#29945)
- Languages now define their preferred debuggers in `config.toml`.
- `LanguageRegistry` now exposes language config even for languages that
are not yet loaded. This necessitated extension registry changes (we now
deserialize config.toml of all language entries when loading new
extension index), but it should be backwards compatible with the old
format. /cc @maxdeviant

Release Notes:

- N/A

---------

Co-authored-by: Anthony Eid <hello@anthonyeid.me>
Co-authored-by: Remco Smits <djsmits12@gmail.com>
Co-authored-by: Anthony <anthony@zed.dev>
2025-05-06 20:16:41 +02:00
Marshall Bowers
544e8fc46c agent: Don't render trial upsell when not using the Zed provider (#30029)
This PR makes it so we don't render the trial upsell when not using the
Zed provider.

Release Notes:

- Agent Beta: Changed Zed Pro trial upsell to only be displayed when
using a model through the Zed provider.
2025-05-06 18:07:25 +00:00
Cole Miller
b83d00d69b agent: Fix "tool cancelled" status being overapplied to failed tool calls (#30021)
Release Notes:

- Agent Beta: Fixed a bug that caused past failed tool calls to
incorrectly display as cancelled by the user.
2025-05-06 17:53:00 +00:00
Marshall Bowers
7a9165d5ce agent: Don't render usage callouts when not using the Zed provider (#30025)
This PR makes it so we don't render the usage callouts when not using
the Zed provider.

Release Notes:

- Agent Beta: Changed usage callouts to only be displayed when using a
model through the Zed provider.
2025-05-06 17:50:26 +00:00
Bennet Bo Fenner
80236d0bb9 agent: Handle context servers that do not provide a configuration in MCP setup dialog (#30023)
<img width="674" alt="image"
src="https://github.com/user-attachments/assets/0ccb89e2-1dc1-4caf-88a7-49159f43979f"
/>
<img width="675" alt="image"
src="https://github.com/user-attachments/assets/790e5d45-905e-45da-affa-04ddd1d33c65"
/>

Release Notes:

- N/A
2025-05-06 17:18:49 +00:00
Umesh Yadav
a743035286 lmstudio: Fix streaming not working in v0.3.15 (#30013)
Closes #29781

Tested this with llama3, gemma3 and qwen3.

This is a breaking change, which means after adding this code changes in
future version zed we will require atleast lmstudio >= 0.3.15. For
context why it's breaking changes check out the issue: #29781.

What this doesn't try to solve is:

* Tool calling, thinking text rendering. Will raise a seperate PR for
these as those are not required in this PR to make it work.


https://github.com/user-attachments/assets/945f9c73-6323-4a88-92e2-2219b760a249

Release Notes:

- lmstudio: Fixed Zed support for LMStudio >= v0.3.15 (breaking change -- older versions are no longer supported).

---------

Co-authored-by: Peter Tripp <peter@zed.dev>
2025-05-06 12:59:36 -04:00
Piotr Osiewicz
bbfcd885ab debugger: Allow locators to generate full debug scenarios (#30014)
Closes #ISSUE

Release Notes:

- N/A

---------

Co-authored-by: Anthony <anthony@zed.dev>
Co-authored-by: Remco Smits <djsmits12@gmail.com>
2025-05-06 18:39:49 +02:00
Marshall Bowers
a378b3f300 collab: Treat staff as having usage-based pricing enabled (#30020)
This PR makes it so staff are treated as having opted-in to usage-based
pricing.

Release Notes:

- N/A
2025-05-06 16:06:03 +00:00
Cole Miller
6d2c39c265 Fix checkpoints not being rendered (#30019)
Closes #ISSUE

Release Notes:

- Agent Beta: Fixed a bug causing "Restore Checkpoint" buttons in the
agent panel not to be rendered.

Co-authored-by: Antonio Scandurra <me@as-cii.com>
2025-05-06 15:59:06 +00:00
Cole Miller
1a80103eaf Silence error log when deserializing agent panel navigation history (#30011)
Closes #ISSUE

Release Notes:

- N/A
2025-05-06 11:46:23 -04:00
Marshall Bowers
6cb436565f collab: Disable usage-based billing thresholds (#30016)
This PR disables the usage-based billing thresholds.

Release Notes:

- N/A
2025-05-06 15:21:22 +00:00
Kirill Bulatov
007fd0586a Adds a way to dismiss workspace notifications (#30015)
Closes https://github.com/zed-industries/zed/issues/10140

* On `menu::Cancel` action (`ESC`), close notifications, one by one, if
`Workspace` gets to handle this action.
More specific, focused items contexts (e.g. `Editor`) take priority.

* Allows to temporarily suppress notifications of this kind either by
clicking a corresponding button in the UI, or using
`workspace::SuppressNotification` action.

This might not work well out of the box for all notifications and might
require further improvement.


https://github.com/user-attachments/assets/0ea49ee6-cd21-464f-ba74-fc40f7a8dedf


Release Notes:

- Added a way to dismiss workspace notifications
2025-05-06 18:15:26 +03:00
Cole Miller
7d361ec97e Fall back to old key when loading agent settings (#30001)
This PR updates #29943 to fall back to loading agent panel settings from
the old `assistant` key if the `agent` key is not present. Edits to
these settings will also target `assistant` in this situation instead of
`agent` as before.

Release Notes:

- Agent Beta: Fixed a regression that caused the agent panel not to
load, or buttons in the agent panel not to work.
2025-05-06 14:31:38 +00:00
drathier
a9d5b2064e docs: Add link to formatter settings from configuring-languages (#29981)
Release Notes:

- N/A

---------

Co-authored-by: Peter Tripp <peter@zed.dev>
2025-05-06 10:29:58 -04:00
Antonio Scandurra
0f50e6b1d1 Fix error when requesting completion to Copilot Chat without tools (#30007)
The API will return a Bad Request (with no error message) when tools
were used previously in the conversation but no tools are provided as
part of a new request.

Inserting a dummy tool seems to circumvent this error.

Release Notes:

- Fixed an error that could sometimes occur when editing using Copilot
Chat.

Co-authored-by: Bennet Bo Fenner <bennetbo@gmx.de>
2025-05-06 14:19:59 +00:00
Marshall Bowers
fed5f89f8d agent: Add enabled indicator in Max Mode tooltip (#30008)
This PR adds an enabled indicator in the Max Mode tooltip to show when
it is enabled:

<img width="409" alt="Screenshot 2025-05-06 at 9 49 48 AM"
src="https://github.com/user-attachments/assets/43d3f6dd-5658-467a-9df9-606ce326426a"
/>

Release Notes:

- Agent Beta: Added an indicator in the Max Mode tooltip to show when it
is enabled.

Co-authored-by: Danilo <danilo@zed.dev>
2025-05-06 14:07:31 +00:00
Marshall Bowers
096355915a agent: Add label to Max Mode toggle (#30003)
This PR adds a label to the Max Mode toggle, for increased clarity:

<img width="647" alt="Screenshot 2025-05-06 at 9 16 35 AM"
src="https://github.com/user-attachments/assets/38cd55fb-43ad-430b-8b4c-5adf707317cf"
/>

Release Notes:

- Agent Beta: Added a label to the Max Mode toggle.
2025-05-06 09:40:20 -04:00
Bennet Bo Fenner
e44367c6d0 agent: Disable claude-3-7-sonnet-thinking tool support for Copilot Chat (#29999)
We started getting Bad Requests from the Copilot Chat API.
Seems like Microsoft stopped supporting this:
<img width="331" alt="image"
src="https://github.com/user-attachments/assets/46050063-f031-4836-82ff-219bdd45639a"
/>


Release Notes:

- agent: Disable `claude-3-7-sonnet-thinking` for Copilot Chat Provider
because it is not supported by Copilot Chat

Co-authored-by: Ben Brandt <benjamin.j.brandt@gmail.com>
2025-05-06 12:47:26 +00:00
Antonio Scandurra
07e6e49583 Add new editing eval scenario and improve it substantially (#29997)
This improves the new eval scenario by ~80% (`0.29` vs `0.525`) without
decreasing performance in the other evals.

Release Notes:

- Improved the performance of the `edit_file` tool.
2025-05-06 12:22:42 +00:00
Fernando Tagawa
6e9f8f997e markdown: Ignore html comments (#28318)
Closes #28300

| Before | After |
| ------ | ----- |
|
![Screenshot_20250408_073355](https://github.com/user-attachments/assets/50dcb56d-bc70-4329-94cb-5b848f265c97)
|
![Screenshot_20250408_073322](https://github.com/user-attachments/assets/ba5c519a-bb34-4724-9c14-3278c6c09afd)
|

Release Notes:

- N/A
2025-05-06 14:55:07 +03:00
Ben Brandt
daba603e27 agent: Fix Open Thread as Markdown not working when another panel is focused (#29993)
Release Notes:

- agent: Fix Open Thread as Markdown not working when another panel is
focused

Co-authored-by: Bennet Bo Fenner <bennetbo@gmx.de>
2025-05-06 12:56:01 +02:00
Oleksiy Syvokon
ac007139ab evals: Enable Python LSP (#29987)
We now have one eval that uses a Python repo


Release Notes:

- N/A
2025-05-06 10:28:59 +00:00
Conrad Irwin
68793c0ac2 Debug adapters log to console (#29957)
Closes #ISSUE

Release Notes:

- N/A
2025-05-06 11:21:34 +01:00
Agus Zubiaga
de554589a8 agent: Add date separators to Thread History (#29961)
Adds time-bucket separators to the thread history list:


https://github.com/user-attachments/assets/c9ac3ec4-b632-4ea5-8234-382b48de2bd6

Note: I'm simulating that Today is next Thursday so that I can show the
"This Week" bucket.

Release Notes:

- agent: Add date separators to Thread History
2025-05-06 10:18:48 +00:00
Conrad Irwin
4fdd14c3d8 Remove another unwrap on regex compilation (#29984)
Follow up to #29979

Release Notes:

- Fixed a (hypothetical) panic in terminal search
2025-05-06 11:18:03 +01:00
Murt
848c4f77a6 fix(vim): Store up to the 9th numbered register instead of 7th (#29986)
Release Notes:

- Fixed an issue where we only automatically stored 7 numbered registers
instead of 9
2025-05-06 11:17:45 +01:00
Finn Evers
06794f35bc assistant: Do not create new context on load (#29480)
Closes https://github.com/zed-industries/zed/issues/27673
Closes https://github.com/zed-industries/zed/issues/29344
Closes #29863 

This PR fixes an issue where Zed was showing no language and `4:1` as a
line/column value on startup, as described in the linked issues. You can
actually see in the first issue that the user also experiences the same
issue as described in the second one, as his line/column value is
noticably also `4:1`.


https://github.com/user-attachments/assets/bb60e387-f4b8-4e05-80b3-4dadf1a01262

This issue arises because on assistant panel load, a new context is
created and its editor focused. However, the editor is not visible
despite having focus. The content for the editor for a new context is
`\n\n\n` and the cursor is inserted directly after that - this is where
the line:column position `4:1` comes from. For the assistant panel
editor, the language is intentionally hidden, this is why the language
is not shown on workspace load.
The issue is only present for as long as the user does not focus and
edit another editor, then that instance is focused and everything starts
to work properly again.

As this issue only arises with the old assistant panel, some staff
members were unable to reproduce in the linked issues. Once you set
`export ZED_DISABLE_STAFF=1` in your environment, you should also be
able to reproduce this issue consistently.

--- 

This PR fixes the issue by not creating a new context on assistant panel
load. This should not cause any regressions; every other code path I
checked creates a new context if no context is yet present.
Additionally, this also seems somewhat more reasonable, as users which
have the assistant panel disabled will never need a new context anyway,
so no context should be created.

In the following video, you can see this fixes the issue when the
assistant panel was not open the last time Zed was opened. If the panel
was open before Zed was closed, we will still properly focus the panel
and then the `4:1` will show again, which in that case is correct. The
assistant panel editor is focused and the missing language as well as
the line number then match what the user sees, experiences and expects.


https://github.com/user-attachments/assets/224a786b-52c7-4212-bccb-dff6d9db62c3


Release Notes:

- Fixed an issue where Zed would show no language and an incorrect
line/column value on startup.

Co-authored-by: Peter Tripp <peter@zed.dev>
2025-05-06 11:38:08 +02:00
Conrad Irwin
ef31252ef8 Fix panic in update_selection_occurrence_highlights (#29979)
Closes #ISSUE

Release Notes:

- Fixed a (rare) panic when highlighting text in the editor
2025-05-06 10:23:51 +01:00
Smit Barmase
5640265160 language: Fix larger syntax node when cursor is at end of word or line (#29978)
Closes #28699

Fixes two cases in the `editor::SelectLargerSyntaxNode` action:
1. When cursor is at the end of a word, it now selects that word first
instead of selecting the whole line.
2. When cursor is at the end of a line, it now selects that line first
instead of selecting the whole code block.

Before and After:


https://github.com/user-attachments/assets/233b891e-15f1-4f10-a51f-75693323c2bd

Release Notes:

- Fixed `editor::SelectLargerSyntaxNode` to properly select nodes when
the cursor is positioned at the end of words or lines.
2025-05-06 14:43:28 +05:30
Shashank Verma
9d97e08e4f title_bar: Add icon for project branch trigger button (#29494)
Added icon for branch switcher in title bar

| `main`    | This PR |
| -------- | ------- |
| <img width="196" alt="Screenshot 2025-04-27 at 1 02 47 PM"
src="https://github.com/user-attachments/assets/5625f6c5-7b11-4f3d-bed8-6ea3b74d9416"
/> | <img width="217" alt="Screenshot 2025-04-27 at 1 07 11 PM"
src="https://github.com/user-attachments/assets/6c83daa6-fa71-44a8-8f6b-e33b2217b29e"
/> |

Release Notes:

- Added icon for branch switcher in title bar

---------

Signed-off-by: Shashank Verma <shashank.verma2002@gmail.com>
2025-05-06 08:38:24 +00:00
tidely
6b37646179 client: Implement Socks identification and authorization (#29591)
Closes #28792 

supersedes #28854 

- Adds support for Socks V4 Identification using a userid, and
Authorization using a username and password on Socks V5.
- Added tests for parsing various Socks proxy urls.
- Added a test for making sure a misconfigured socks proxy url doesn't
expose the user by connecting directly as a fallback.

Release Notes:

- Added support for identification and authorization when using a sock
proxy
2025-05-06 08:03:56 +00:00
Aaron Feickert
da3a696a60 editor: Remove extra quotes from outline search term (#29829)
The outline panel includes quotes around search terms. The rendering
makes it somewhat ambiguous whether these quotes are part of the search
term and are unnecessary, especially given other rendering
differentiation. This PR removes them.

Release Notes:

- N/A
2025-05-06 10:50:33 +03:00
Finn Evers
6bacea28bc editor: Do not insert scrollbar hitboxes when scrollbars are never to be shown (#29316)
This PR fixes an issue where scrollbar hitboxes were still inserted for
editors despite scrollbars being programmatically disabled via the
`show_scrollbars`field. This is basically the same fix as in #27467.

The thought process here is that the motivation for `show_scrollbars` is
not to just hide the scrollbars in the editor, but to fully disable
scrollbars for the associated editor. However, this is currently not the
case, as a functioning hitbox for each scrollbar is stil inserted. For
example, the behavior with the old assistant panel can be seen below:


https://github.com/user-attachments/assets/18af6338-dd28-4794-a6a6-5b9691b243f2

Whilst the scrollbar is not visible, there is still a scrollbar hitbox
inserted which triggers hover events and is fully functioning.


This PR fixes this by fully skipping the scrollbar layouting whenever
`show_scrollbars` is set to false, preventing the hitboxes from being
inserted.


https://github.com/user-attachments/assets/b6bb6dc7-902f-4383-bf03-506d0a57ec77


Release Notes:

- N/A
2025-05-06 10:25:01 +03:00
Finn Evers
3b90d62bb2 editor: Implement hover color for scrollbars (#28064)
This PR adds hover colors to the editor scrollbars:


https://github.com/user-attachments/assets/6600810e-7e8e-4dee-9bef-b7be303b5fe0

The color used here is the existing `scrollbar_thumb_hover_background`
color provided by themes.

Looking forward to feedback 😄 

Release Notes:

- Added hover state to editor scrollbars.
2025-05-06 10:17:43 +03:00
Ivan Banov
55a0bb2a91 Add default tab_size for Elm (#29547)
This PR updates the default tab size to 4 spaces, aligning with the
standard adopted by the Elm community and the official language
formatter (elm-format).

Reference: [elm-format tab size
default](https://github.com/avh4/elm-format/blob/main/elm-format-lib/src/Box.hs#L249)
2025-05-06 10:14:02 +03:00
Antonio Scandurra
210c338df4 Restore original file content when rejecting an overwritten file (#29974)
Release Notes:

- Fixed a bug that would cause rejecting a hunk from the agent to delete
the file if the agent had decided to rewrite that file from scratch.
2025-05-06 07:05:55 +00:00
neunato
86cc5c2b55 Apply autoscroll_on_clicks when extending selection (#28235)
Closes https://github.com/zed-industries/zed/issues/22240

Release Notes:

- Fixed `autoscroll_on_clicks` not being applied when expanding
selection

Co-authored-by: Kirill Bulatov <kirill@zed.dev>
2025-05-06 07:05:08 +00:00
Anthony Eid
a0bfe4d293 debugger: Fix debug scenario's defined in debug.json not using passed in build task (#29973)
There were two bugs that caused user-defined debug scenarios from being
able to run a build task.

1. DebugRequest would be deserialized to `Attach` even when `process_id`
wasn't defined in a user's configuration file. This has been fixed by
adding our own deserializer that defaults to None if there are no fields
present instead of `Attach`, and I added tests to prevent regressions.
2. Debug scenario resolve phase never got the active buffer when
spawning a debug session from the new session modal. This has been
worked around by passing in the worktree_id of a debug scenario in the
scenario picker and the active worktree_id otherwise.

Release Notes:

- N/A
2025-05-06 08:54:57 +02:00
neunato
52ea501f4f Fix multicursors not being added when clicking on line numbers (#28263)
Closes https://github.com/zed-industries/zed/issues/21372

Release Notes:

- Fixed multicursors not being added when clicking on line numbers

-----

I tracked this down to
b6ee367ee0/crates/editor/src/element.rs (L591)

being forwarded to `editor.select()` a few lines below
b6ee367ee0/crates/editor/src/element.rs (L667-L675)

resulting in `add == true` and `click_count == 3`, triggering this
b6ee367ee0/crates/editor/src/editor.rs (L2750-L2752)

... and we end up removing the previous selection. 

This was added [in
2021](bfecdb7bc0)
under this reasoning:

> This prevents selections added in earlier clicks from being rendered
under the pending selection.

which no longer seems to be an issue, so removing should be safe?

---------

Co-authored-by: Peter Tripp <peter@zed.dev>
2025-05-06 09:45:43 +03:00
Francisco Fernandes
a07ba3c718 editor: Fix inconsistent SelectPrevious behavior (#27695)
When starting a selection from only carets, the action
`editor::SelectPrevious` behaved in a manner inconsistent with
`editor::SelectNext` as well as equivalent keybinds in editors such as
VSCode, by selecting substrings of whole words matching the initially
selected string on subsequent triggers.

This fix brings the `select_previous` function in line with
`select_next_internal`by calling `select_match_ranges` (previously an
internal function of `select_next_internal`) in the same way it was
previously used in the function that exhibited expected behavior.

Furthermore, the relevant test was adapted to bring it in line with the
equivalent test for the `editor::SelectNext` action

Closes #24346

Release Notes:

- Fixed inconsistent SelectPrevious behavior
2025-05-06 09:37:58 +03:00
Max Brunsfeld
2eb10ab9fb openai: Don't append tool calls to prior assistant messages (#29969)
Closes https://github.com/zed-industries/zed/issues/29821

Release Notes:

- Fixed an issue in the agent panel where OpenAI requests would fail if
the assistant begins its response with a tool call.
2025-05-05 22:04:56 -07:00
Eva Pace
55fd8352e4 assistant_slash_commands: Be more precise in content type matching (#29124)
While investigating https://github.com/zed-industries/zed/issues/28076,
I found out often times the content type header of a website comes with
more data, such as the `charset`. So instead of doing an equal
comparison, I changed to a `starts_with`.

You can see an example here:

```shell
$ curl -sS -D - https://github.com/zed-industries/zed/blob/main/Cargo.toml -o /dev/null | head -n 10
HTTP/2 200
date: Sun, 20 Apr 2025 10:19:52 GMT
content-type: text/html; charset=utf-8
vary: X-PJAX, X-PJAX-Container, Turbo-Visit, Turbo-Frame,Accept-Encoding, Accept, X-Requested-With
etag: W/"92dabf048b34d04a1b1d94e29cae4aca"
cache-control: max-age=0, private, must-revalidate
strict-transport-security: max-age=31536000; includeSubdomains; preload
x-frame-options: deny
x-content-type-options: nosniff
x-xss-protection: 0
```

Release Notes:

- Improved Content Type matching of `/fetch` commands in Assistant

Co-authored-by: Peter Tripp <peter@zed.dev>
2025-05-06 04:38:03 +00:00
Michael Sloan
0b10eb7577 Remove Tree-sitter AST logging from SelectLargerSyntaxNode (#29949)
Release Notes:

- N/A
2025-05-06 04:30:06 +00:00
Jason Lee
3d737fd268 gpui: Update argument name of the from_corners method (#29968)
Release Notes:

- N/A
2025-05-06 07:17:39 +03:00
Nate Butler
c5d8407df4 component: Component crate cleanup (#29967)
This PR further organizes and documents the component crate. It:

- Simplifies the component registry
- Gives access to `ComponentMetadata` sooner
- Enables lookup by id in preview extension implementations
(`ComponentId` -> `ComponentMetadata`)
- Should slightly improve the performance of ComponentPreview

It also brings component statuses to the Component trait:

![CleanShot 2025-05-05 at 23 27
11@2x](https://github.com/user-attachments/assets/dd95ede6-bc90-4de4-90c6-3e5e064fd676)

![CleanShot 2025-05-05 at 23 27
40@2x](https://github.com/user-attachments/assets/9520aece-04c2-418b-95e1-c11aa60a66ca)

![CleanShot 2025-05-05 at 23 27
57@2x](https://github.com/user-attachments/assets/db1713d5-9831-4d00-9b29-1fd51c25fcba)

Release Notes:

- N/A
2025-05-06 03:41:52 +00:00
Danilo Leal
377909a646 Fix toolbar spacing regressions (#29964)
Cleaning up as I introduced a few regressions in this PR:
https://github.com/zed-industries/zed/pull/29866.

Release Notes:

- N/A
2025-05-05 22:28:35 -03:00
Cole Miller
bdd911f89e Update assistant to agent in settings and keymaps (#29943)
Closes #ISSUE

Release Notes:

- Agent Beta: Renamed the top-level `assistant` settings key to `agent`.
A migration for existing settings files is included.
- Agent Beta: Moved the `assistant::ToggleFocus`,
`assistant::ToggleModelSelector`, and `assistant::OpenRulesLibrary`
actions to the `agent` namespace. Existing keymaps that mention these
actions by their old names will continue to work.

---------

Co-authored-by: Max <max@zed.dev>
2025-05-06 01:02:56 +00:00
Max Brunsfeld
34e10e4e56 Honor the prompt field of inline assist action (#29960)
Closes https://github.com/zed-industries/zed/issues/29337

Release Notes:

- Fixed a bug where the `prompt` field was ignored on custom key
bindings for `InlineAssist`

Co-authored-by: Michael Sloan <mgsloan@gmail.com>
2025-05-05 17:24:31 -07:00
Max Brunsfeld
275c808b03 Allow dragging files and tabs into the agent panel (#29959)
Release Notes:

- Added the ability to drag files and tabs onto the new agent panel.

---------

Co-authored-by: Michael Sloan <mgsloan@gmail.com>
2025-05-06 00:21:22 +00:00
Mikayla Maki
b214c9e4a8 Fix profile menu hover flickering due to documentation asides (#29958)
Fixes https://github.com/zed-industries/zed/issues/29909 

🍐'd with @nathansobo 

Release Notes:

- N/A

---------

Co-authored-by: Nathan <nathan@zed.dev>
2025-05-05 23:05:47 +00:00
Bennet Bo Fenner
2aa06d1d0f agent: Switch to new web search provider (#29951)
Release Notes:

- N/A
2025-05-06 00:47:11 +02:00
Nate Butler
9568fa1166 Add Zed Pro Trial Upsell (#29938)
This PR adds an upsell to try Zed Pro

Release Notes:

- N/A

---------

Co-authored-by: Marshall Bowers <git@maxdeviant.com>
2025-05-05 18:46:54 -04:00
xdBronch
b4653c15b8 language: Add fallback for enum member completion highlight (#27929)
i tried to use `variant` but it wasnt giving any color despite my theme
definitely having a color for it, am i doing something wrong? i think
`property` is an alright fallback
before:

![image](https://github.com/user-attachments/assets/e020ec4d-3a85-45fb-9ddb-823c55e0afca)
after:

![image](https://github.com/user-attachments/assets/3c335ed6-746e-4136-858a-8b80e5229f29)


Release Notes:

- N/A
2025-05-05 22:25:09 +00:00
Nathan Sobo
4896e0bc02 Allow the agent panel font size to be customized (#29954)
You can set `agent_font_size` as a top-level settings key. You can also
use `zed::IncreaseBufferFontSize` and `zed::DecreaseBufferFontSize` and
`zed::ResetBufferFontSize` the agent panel is focused via the standard
bindings to adjust the agent font size. In the future, it might make
sense to rename these actions to be more general since "buffer" is now a
bit of a misnomer. 🍐'd with @mikayla-maki

Release Notes:

- N/A

---------

Co-authored-by: Mikayla Maki <mikayla.c.maki@gmail.com>
2025-05-05 16:13:14 -06:00
Noritada Kobayashi
0bf682a0d5 docs: Fix a broken link to the PyRight Settings section (#29283)
This PR fixes a broken link to the PyRight Settings section.
This is a follow-up to 5f390f1bf8.

Release Notes:

- N/A
2025-05-05 18:02:53 -04:00
Max Brunsfeld
3d0c4d716d Use the same context store for all inline assists in a project (#29953)
Release Notes:

- Made context attachments in inline assist prompts persist across
inline assist invocations.

Co-authored-by: Michael Sloan <mgsloan@gmail.com>
2025-05-05 22:01:02 +00:00
Marshall Bowers
b6c7df8183 inline_completion_button: Show the initial usage data from the server (#29952)
This PR updates the usage indicator for edit predictions to show the
initial usage data returned from the server.

Release Notes:

- N/A
2025-05-05 21:39:49 +00:00
Anthony Eid
1aa92d9928 debugger: Enable setting debug panel dock position to the side (#29914)
### Preview
<img width="301" alt="Screenshot 2025-05-05 at 11 08 43 PM"
src="https://github.com/user-attachments/assets/aa445117-1c1c-4d90-a3bb-049f8417eca4"
/>


Setups the ground work to write debug panel persistence tests and allows
users to change the dock position of the debug panel.


Release Notes:

- N/A
2025-05-05 21:27:20 +00:00
Pavel
6e28400e17 gpui: Fix a bug with Japanese romaji typing in input example (#28507)
Steps to reproduce:
* On macOS, run `input` example
* type `aaa|bbb` place caret on the place marked with |
* switch to `japanese romaji`
* press `ko`
* press left arrow

<img width="412" alt="image"
src="https://github.com/user-attachments/assets/d3c02e9b-98f9-420e-a3b7-681ba90829cd"
/>

You will get `aaa` duplicated with every arrow press.

According to [reference
implementation](https://developer.apple.com/library/archive/samplecode/TextInputView/Listings/FadingTextView_m.html#//apple_ref/doc/uid/DTS40008840-FadingTextView_m-DontLinkElementID_6)
we need to unmark text when we get empty line in `setMarkedText `
2025-05-06 00:15:41 +03:00
Lorenzo Lewis
78545a93ea gpui: Fix typo in doc comment (#29950)
Fixes a typo in gpui docs

Release Notes:

- N/A
2025-05-05 17:05:48 -04:00
Max Brunsfeld
dd79c29af9 Allow attaching text threads as context (#29947)
Release Notes:

- N/A

---------

Co-authored-by: Michael Sloan <mgsloan@gmail.com>
2025-05-05 13:59:21 -07:00
chbk
7f868a2eff Improve Rust macro highlighting (#28182)
Release Notes:

  - Improved Rust macro highlighting

| Zed 0.180.2 | With this PR |
| --- | --- |
|
![Image](https://github.com/user-attachments/assets/013c73b1-5eee-45b1-ba37-747563c1bc4b)
|
![Image](https://github.com/user-attachments/assets/57eb97e3-1ccc-4d58-9596-bb3decedc0f4)
|

```rust
macro_rules! square {
  ($e:expr) => { $e * $e };
}
```

- `$var`: `variable`
- `expr`: `type`
2025-05-05 23:50:57 +03:00
Conrad Irwin
6497aa5341 Show request in evaluate selection command (#29621)
Closes #ISSUE

Release Notes:

- N/A
2025-05-05 21:32:00 +01:00
Nathan Sobo
55b908a8bf Allow agent edits to be accepted/rejected before the end the turn (#29941)
Release Notes:

- N/A
2025-05-05 14:25:34 -06:00
Conrad Irwin
ff215b4f11 debugger: Run build in terminal (#29645)
Currently contains the pre-work of making sessions creatable without a
definition, but still need to change the spawn in terminal
to use the running session

Release Notes:

- N/A

---------

Co-authored-by: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com>
2025-05-05 20:08:14 +00:00
Cole Miller
c12e6376b8 Terminal tool improvements (#29924)
WIP

- On macOS/Linux, run the command in bash instead of the user's shell
- Try to prevent the agent from running commands that expect interaction

Release Notes:

- Agent Beta: Switched to using `bash` (if available) instead of the
user's shell when calling the terminal tool.
- Agent Beta: Prevented the agent from hanging when trying to run
interactive commands.

---------

Co-authored-by: WeetHet <stas.ale66@gmail.com>
2025-05-05 15:57:03 -04:00
Bennet Bo Fenner
9cb5ffac25 context_store: Refactor state management (#29910)
Because we instantiated `ContextServerManager` both in `agent` and
`assistant-context-editor`, and these two entities track the running MCP
servers separately, we were effectively running every MCP server twice.

This PR moves the `ContextServerManager` into the project crate (now
called `ContextServerStore`). The store can be accessed via a project
instance. This ensures that we only instantiate one `ContextServerStore`
per project.

Also, this PR adds a bunch of tests to ensure that the
`ContextServerStore` behaves correctly (Previously there were none).

Closes #28714
Closes #29530

Release Notes:

- N/A
2025-05-05 21:36:12 +02:00
Oleksiy Syvokon
8199664a5a agent: Handle attempts to use hallucinated tools (#29946)
This change:

1. Catches attempts to use missing tools. If this happens, we now send
Agent a message listing available tools, after which Agent can
gracefully recover. Prior behavior: thread would stop in a broken state.

Example of a hallucinated call and a message we send back: 

![image](https://github.com/user-attachments/assets/92a8f700-b192-4038-8c7e-0a74ca2e0146)

2. Adds evals for hallucinated tool use and imagined edits
3. Adds ability to configure a profile name in evals.



Release Notes:

- N/A
2025-05-05 19:31:11 +00:00
Danilo Leal
7dfbe0b908 agent: Improve terminal tool card design (#29712)
To-dos:

- [x] Expose the command to defend against cases where that's just super
long
- [x] Tackle the vertical scroll conflict with panel scroll
- [x] Reduce default font-size

Release Notes:

- N/A

---------

Co-authored-by: Ben Brandt <benjamin.j.brandt@gmail.com>
Co-authored-by: Mikayla Maki <mikayla.c.maki@gmail.com>
Co-authored-by: Agus Zubiaga <hi@aguz.me>
2025-05-05 18:50:53 +00:00
Nate Butler
e64f5ff358 agent: Load usage eagerly (#29937)
Release Notes:

- N/A

---------

Co-authored-by: Marshall Bowers <git@maxdeviant.com>
2025-05-05 14:42:21 -04:00
Marshall Bowers
181cd6294f collab: Pass down staff usage in UpdatePlan message (#29939)
This PR fixes an issue where we weren't correctly passing down usage
information in the `UpdatePlan` message for Zed staff.

Release Notes:

- N/A
2025-05-05 18:02:54 +00:00
tidely
769ec59162 ollama: Add tool call support (#29563)
The goal of this PR is to support tool calls using ollama. A lot of the
serialization work was done in
https://github.com/zed-industries/zed/pull/15803 however the abstraction
over language models always disables tools.

## Changelog:

- Use `serde_json::Value` inside `OllamaFunctionCall` just as it's used
in `OllamaFunctionCall`. This fixes deserialization of ollama tool
calls.
- Added deserialization tests using json from official ollama api docs.
- Fetch model capabilities during model enumeration from ollama provider
- Added `supports_tools` setting to manually configure if a model
supports tools

## TODO:

- [x] Fix tool call serialization/deserialization
- [x] Fetch model capabilities from ollama api
- [x] Add tests for parsing model capabilities 
- [ ] Documentation for `supports_tools` field for ollama language model
config
- [ ] Convert between generic language model types
- [x] Pass tools to ollama

Release Notes:

- N/A

---------

Co-authored-by: Antonio Scandurra <me@as-cii.com>
Co-authored-by: Nathan Sobo <nathan@zed.dev>
2025-05-05 17:52:23 +00:00
Nathan Sobo
e9616259d0 Rename Manual profile to Minimal (#29852)
Completely subjective, but I just like it better.

Release Notes:

- N/A

---------

Co-authored-by: Danilo Leal <67129314+danilo-leal@users.noreply.github.com>
Co-authored-by: Danilo Leal <daniloleal09@gmail.com>
2025-05-05 11:48:26 -06:00
Michael Sloan
7164124512 agent panel: Bring back search within text threads (#29934)
Release Notes:

- N/A
2025-05-05 16:26:43 +00:00
Kirill Bulatov
76c0eded0d Add more documentation about ways to configure language servers and rust-analyzer (#29932)
Release Notes:

- N/A
2025-05-05 16:10:10 +00:00
AidanV
c56a1cf2b1 vim: Fix r enter indentation (#29838)
Release Notes:

- `r enter` now maintains indentation, matching vim

Useful info for this implementation can be found here:

c3f48e3a76/src/normal.c (L4865)
2025-05-05 16:57:32 +01:00
Richard Feldman
4b9b908233 Delete obsolete find_replace_tool description (#29928)
The tool has been deleted, but the description remained.

Release Notes:

- N/A
2025-05-05 11:56:13 -04:00
Marshall Bowers
10bdf39497 collab: Pass down billing information in UpdatePlan message (#29929)
This PR updates the `UpdatePlan` message to include some additional
information about the user's billing subscription usage.

Release Notes:

- N/A
2025-05-05 11:48:31 -04:00
Smit Barmase
07b4480396 editor: Handle more completions sort cases in Rust and Python (#29926)
Closes #29725

Adds 3 more tests for Rust `into` and `await` cases, and Python
`__init__` case. Tweaks sort logic to accommodate them.

Release Notes:

- Improved code completion sort order, handling more cases with Rust and
Python.
2025-05-05 20:48:52 +05:30
Antonio Scandurra
b0414df921 Simplify setting font size for EditToolCard (#29925)
Release Notes:

- N/A
2025-05-05 15:04:00 +00:00
Bennet Bo Fenner
0246ec2dab agent: Tweak MCP server configuration dialog (#29878)
Tweaks the MCP configuration dialog a bit:
- Increase width of popover
- Disable soft 
- Clear errors when hitting confirm

Release Notes:

- N/A
2025-05-05 16:31:04 +02:00
Nate Butler
a72ade8762 Show prompt usage in agent overflow menu (#29922)
This PR adds prompt usage information, and easy access to managing your
account, to the agent overflow menu:

![CleanShot 2025-05-05 at 10 04
20@2x](https://github.com/user-attachments/assets/337a1a0b-6f71-49a0-9fe7-4fbf2ec1fc27)

Currently this UI will only show after making a request. We'll work on
eagerly getting the usage info later.

Release Notes:

- Added current prompt usage information to the agent menu (`...`) for
Zed AI users
2025-05-05 14:22:36 +00:00
Dan Bornstein
1c44cabaea bash: Fix bracket autoclose behavior (#29817)
Add `autoclose_before` configuration for Bash.

Closes #23627

Release Notes:

- Bash: Improved bracket autoclose behavior.
2025-05-05 10:02:27 -04:00
Antonio Scandurra
5674b5cd4d Don't show deleted hunks when agent overwrites file (#29918)
Release Notes:

- Improved display of diffs when the agent rewrites a file from scratch.
2025-05-05 13:13:36 +00:00
Smit Barmase
4a7b3aa4b8 zed: Fix migration message sometimes showing up on other tabs (#29917)
<img width="1178" alt="image"
src="https://github.com/user-attachments/assets/6b76fe7d-0621-4d61-936e-bfe4f72cc614"
/>


Release Notes:

- Fixed an issue where the keymap/settings migration message sometimes
showing up on tabs other than `settings.json` and `keymap.json`.
2025-05-05 18:13:26 +05:30
Cole Miller
c765da1c82 lsp: Don't log oneshot channel errors from notify (#29857)
This is kind of noisy and not very informative.

Release Notes:

- N/A
2025-05-05 08:21:45 -04:00
Cole Miller
b404024c7a Get terminal tool working in evals (#29831)
Bypass our terminal subsystem and just run a shell in a pty.

- [x] make sure we use the same working directory
- [x] strip control chars from the pty output (?)
- [x] tests

Release Notes:

- N/A
2025-05-05 08:07:43 -04:00
Ben Brandt
ce053c9bff Stop generating in the Agent panel when the user edits a previous message (#29915)
Otherwise the panel keeps scrolling as the new token comes in and it is
almost impossible to keep the scroll position in the right place.

Also, if the user is editing, it is likely that the current generated
tokens will need to be regenerated anyway, so we may as well stop the
current progress.

Release Notes:

- Agent Beta: Stop generating tokens if previous messages are edited.
2025-05-05 14:06:02 +02:00
Umesh Yadav
251f26d48a copilot: Add support for tool_calls for gpt-4.1, gpt-4o, o4-mini (#29369)
Github Copilot currently supports following models for agent mode with
tool calls. Currently we are only supporting anthropic models and not
openai and gemini. This PR add support for the openai models. I have
tested it and it works for all of them. For gemini models it seems there
is a issues from copilot side so not adding that in this PR as enabling
gemini model breaks it in the ask mode as well.

<img width="392" alt="image"
src="https://github.com/user-attachments/assets/fb7a4148-e48c-45c5-9ff9-c02f71217dfb"
/>


- [x] GPT-4.1

- [x] GPT-4.0

- [x] o4-mini

Release Notes:

- agent: Add tool calling support for gpt-4.1, gpt-4o, o4-mini when
using Copilot Chat as a provider

Signed-off-by: Umesh Yadav <umesh4257@gmail.com>
2025-05-05 13:59:12 +02:00
Kirill Bulatov
7133699335 Suggest nim extension for *.nim files (#29912)
Release Notes:

- N/A
2025-05-05 11:46:42 +00:00
Antonio Scandurra
1adb4ecc95 Polish diff for the edit_file tool (#29911)
I added some padding to the editor, and removed the border around each
hunk as it would overlap in weird ways with the card container.

## Before

<img width="1148" alt="image"
src="https://github.com/user-attachments/assets/2018feaa-c847-4609-bc82-522660714b9a"
/>

## After

One Light:

<img width="1148" alt="image"
src="https://github.com/user-attachments/assets/4da1a4b6-0af2-4479-afcc-02da50178fd6"
/>

One Dark:

<img width="1148" alt="image"
src="https://github.com/user-attachments/assets/0168631d-7b76-4582-8174-c6e9c1297dc8"
/>


Release Notes:

- Improved displaying of diffs when the agent edits files.
2025-05-05 11:17:15 +00:00
Kirill Bulatov
0048e67832 Properly restore window position for SSH projects (#29904)
Release Notes:

- Fixed SSH projects not restoring their window position on reopen
2025-05-05 08:46:49 +00:00
Finn Evers
0119b66426 project_search: Ensure filter row aligns with other search rows (#29886)
Closes #29858 

This PR fixes the alignment-issue for the project saerch for cases where
the horizontally available space is large.

The issue arose because the two smaller editors within one line were
allowed to grow as much as the other editors on separate lines, up to
1200 pixels. However, these two editors should together only take up
1200 pixels at maximum, including the gap between them. To fix this, the
editors now live within one container element that grows at the same
rate as the other editors whilst allowing both editors to flex grow as
needed in the available space.

Current main:


https://github.com/user-attachments/assets/622016dc-70e5-455f-a7ba-5b69405d7e1e

This PR: 


https://github.com/user-attachments/assets/5244abf7-f0c0-4781-acb7-b774638d8a17

Release Notes:

- Improved project search input field alignment.
2025-05-05 09:35:48 +03:00
Marshall Bowers
45fe158bc9 collab: Improve GET /billing/usage endpoint (#29898)
This PR improves the `GET /billing/usage` endpoint.

We now return the usage with the default plan limits when there is no
usage record.

Release Notes:

- N/A
2025-05-05 02:31:02 +00:00
Marshall Bowers
55eb0710ed agent: Update callout URLs (#29897)
This PR updates the Agent callout URLs to go to the account page.

Release Notes:

- N/A
2025-05-05 01:44:09 +00:00
Marshall Bowers
3e2abbf53b ui: Make Callout constructors more flexible (#29895)
This PR updates the `Callout` constructors to be more flexible by
accepting `impl Into<SharedString>`s.

Release Notes:

- N/A
2025-05-05 01:18:48 +00:00
Marshall Bowers
a2fa10f35f agent: Remove UsageBanner (#29896)
This PR removes the `UsageBanner` component, as it was no longer used.

Release Notes:

- N/A
2025-05-05 01:18:36 +00:00
Marshall Bowers
3db4744e18 agent: Remove unneeded tracking of request usage (#29894)
This PR removes some unneeded tracking of the model request usage in the
`ActiveThread` and `ThreadEvent::UsageUpdated` events.

Release Notes:

- N/A
2025-05-05 01:16:53 +00:00
Nate Butler
fe177f5d69 agent: Add UI for upsell scenarios (#29805)
Release Notes:

- N/A

---------

Co-authored-by: Marshall Bowers <git@maxdeviant.com>
2025-05-05 00:48:06 +00:00
Danilo Leal
a19687a815 agent: Sort profiles based on relevance (#29893)
Kinda feel like the way that makes the most sense to sort profiles in
the dropdown is by relevance/impact. "Write" is the default profile and
contains all built-in tools turned on by default, thus it should be the
first. "Ask" contains read-only tools, one step down from Write. And
"Manual" is totally empty, the least "powerful" profile, thus the last.

Release Notes:

- N/A
2025-05-05 00:35:52 +00:00
Nathan Sobo
eb15ed7d60 In the edit tool card, use the UI font size for the editor that we use to render the diff (#29882)
I am currently setting the font size corrrectly by using a custom
EditorStyle and building an element. However I need to use the same
properties as a normal editor for everything but font size.

Release Notes:

- N/A
2025-05-05 00:09:47 +00:00
Danilo Leal
52da375a9d agent: Add design adjustments to message editor (#29891)
- Removed unused `MessageBubbleDashed` icon
- Polished `Crosshair` icon SVG
- Added dropdown toggle keybinding to the profile selector tooltip
- Repositioned buttons at the message editor footer
- Updated buttons to use `Button` instead of `ButtonLike`

Release Notes:

- N/A
2025-05-04 20:21:30 -03:00
Marshall Bowers
3594a52bee collab: Don't try to sync usage to Stripe for staff users (#29892)
This PR makes it so we don't try to sync billing usage to Stripe for
staff users.

Release Notes:

- N/A
2025-05-04 23:14:24 +00:00
Michael Sloan
76ad1a29a5 Add support for getting the token count for all parts of Gemini generation requests (#29630)
* `CountTokensRequest` now takes a full `GenerateContentRequest` instead
of just content.

* Fixes use of `models/` prefix in `model` field of
`GenerateContentRequest`, since that's required for use in
`CountTokensRequest`. This didn't cause issues before because it was
always cleared and used in the path.

Release Notes:

- N/A
2025-05-04 21:32:45 +00:00
Michael Sloan
86484233c0 Replace std::sync::Mutex with parking_lot::Mutex in languages/src/python.rs (#29889)
This appears to be the only place `std::sync::Mutex` is used, Zed always
prefers `parking_lot`.

Release Notes:

- N/A
2025-05-04 21:12:21 +00:00
Michael Sloan
f4e9ea3cd8 In error text of cloud LLM API: completion failed -> request failed (#29888)
This error is used for more requests than completion requests

Release Notes:

- N/A
2025-05-04 21:04:34 +00:00
Marshall Bowers
161f6dfcb6 collab: Set billing-related fields for Zed staff (#29887)
This PR sets the billing-related fields in the LLM token claims for Zed
staff.

Staff members are automatically in the Zed Pro plan with a subscription
periods that spans the entirety of each month.

Release Notes:

- N/A
2025-05-04 21:00:34 +00:00
Michael Sloan
a0895a6ed8 Only send Stop event at end of google completion request (#29885)
I don't think this makes much of a difference in current use, but this
more closely matches other providers and cleans up the "Response"
section of eval markdown output

Release Notes:

- N/A
2025-05-04 20:23:13 +00:00
Michael Sloan
bb82d9ca82 agent eval: Fix --model arg and add --provider (#29883)
Release Notes:

- N/A
2025-05-04 13:43:57 -06:00
ZaraPhu
007685f6d4 docs: Add instructions for uninstalling Zed (#29840) 2025-05-04 17:41:36 +00:00
Max Brunsfeld
c3d9cdecab Change cloud language model provider JSON protocol to surface errors and usage information (#29830)
Release Notes:

- N/A

---------

Co-authored-by: Nathan Sobo <nathan@zed.dev>
Co-authored-by: Marshall Bowers <git@maxdeviant.com>
2025-05-04 17:37:42 +00:00
Bennet Bo Fenner
3984531a45 agent: Rename @rules to @rule (#29881)
This is purely a cosmetic change, renamed `@rules` to `@rule` which
unifies the @mention experience (for files, threads etc. we also use
`@file`, `@thread` not `@files`, `@thread`). Would also make sense to
rename the rules picker to rule picker, but i do not wanna introduce
conflicts just for the purpose of re-naming.

Release Notes:

- N/A
2025-05-04 16:25:44 +00:00
Marshall Bowers
cceb13b7cd collab: Add use_llm_request_queue to LlmTokenClaims (#29877)
This PR adds a `use_llm_request_queue` field to the LLM token claims,
based on the `llm-request-queue` feature flag.

Release Notes:

- N/A
2025-05-04 12:08:43 -04:00
Marshall Bowers
427101b634 collab: Drop legacy subscription usage and meter tables (#29876)
This PR adds a migration to drop the `subscription_usages` and
`subscription_usage_meters` tables from the database.

We're now using `subscription_usages_v2` and
`subscription_usage_meters_v2` everywhere.

Release Notes:

- N/A
2025-05-04 10:42:40 -04:00
Antonio Scandurra
4d51602e7b Encourage editing over re-creating a file from scratch (#29870)
I also introduced a new eval to prove the encouragement actually makes a
difference.

Release Notes:

- Improved agent behavior when streaming edits, encouraging it to
editing files as opposed to creating them from scratch
2025-05-04 13:18:28 +00:00
Marshall Bowers
ca1dc821cf collab: Fix subscription_usage_id column type (#29871)
This PR fixes the type of the `subscription_usage_id` column on the
`SubscriptionUsageMeter` model.

Release Notes:

- N/A
2025-05-04 13:05:26 +00:00
Danilo Leal
2e3baef299 agent: Polish single-file review toolbar controls (#29866) 2025-05-04 07:53:21 -03:00
Antonio Scandurra
545ae27079 Add the ability to follow the agent as it makes edits (#29839)
Nathan here: I also tacked on a bunch of UI refinement.

Release Notes:

- Introduced the ability to follow the agent around as it reads and
edits files.

---------

Co-authored-by: Nathan Sobo <nathan@zed.dev>
Co-authored-by: Max Brunsfeld <maxbrunsfeld@gmail.com>
2025-05-04 08:28:39 +00:00
Danilo Leal
425f32e068 agent: Add the single_file_review setting to the UI (#29859)
Release Notes:

- agent: Add the `single_file_review` setting to the UI
2025-05-03 21:01:44 -03:00
Agus Zubiaga
9c11d24887 Fix hiding editor toolbar and add agent_review setting (#29854)
Closes #29836

The agent diff toolbar item was causing the editor toolbar to show even
when all the other elements were disabled via settings.

This PR fixes this by setting the location to
`ToolbarItemLocation::Hidden` in the states where it shouldn't show.

It also adds a new a `toolbar.agent_review` setting to hide the agent
review buttons altogether. However, if the other toolbar elements are
hidden and the file isn't under review, the editor toolbar will still be
hidden. So you only need to set this to `false` if you don't want them
to show up even under agent review.

Release Notes:

- N/A
2025-05-03 17:43:46 -03:00
Marshall Bowers
1fc57ea9f5 feature_flags: Add a constant to control Agent-related feature flags (#29853)
This PR adds a singular constant that controls the Agent-related feature
flags.

This way we can tweak this one value when we're ready to build the final
build for the launch.

Release Notes:

- N/A
2025-05-03 20:16:25 +00:00
Marshall Bowers
c3d2831d86 collab: Use new subscription usage tables (#29848)
This PR updates Collab to use the new subscription usage tables added in
#29847.

Release Notes:

- N/A
2025-05-03 17:56:43 +00:00
Marshall Bowers
c1247977ed collab: Add new tables for subscription usages and meters (#29847)
This PR adds two new tables:

- `subscription_usages_v2`
- `subscription_usage_meters_v2`

These are the same as the old ones, except using UUIDs as primary keys.

Release Notes:

- N/A
2025-05-03 17:21:22 +00:00
Marshall Bowers
12c26a4fa6 collab: Don't try to transfer usage when a Zed Pro trial is canceled (#29843)
This PR fixes an issue where we would erroneously try to transfer
existing subscription usage when a Zed Pro trial was canceled.

Release Notes:

- N/A
2025-05-03 14:57:54 +00:00
Marshall Bowers
7f8e3fd482 ui: Implement ParentElement for Banner (#29834)
This PR implements the `ParentElement` trait for the `Banner` component
so that it can use the real children APIs instead of a bespoke one.

Release Notes:

- N/A
2025-05-03 02:36:53 +00:00
Marshall Bowers
f0515d1c34 agent: Show a notice when reaching consecutive tool use limits (#29833)
This PR adds a notice when reaching consecutive tool use limits when
using normal mode.

Here's an example with the limit artificially lowered to 2 consecutive
tool uses:


https://github.com/user-attachments/assets/32da8d38-67de-4d6b-8f24-754d2518e5d4

Release Notes:

- agent: Added a notice when reaching consecutive tool use limits when
using a model in normal mode.
2025-05-03 02:09:54 +00:00
Danilo Leal
10a7f2a972 agent: Add several UX improvements (#29828)
Still a work in progress.

Release Notes:

- N/A

---------

Co-authored-by: Bennet Bo Fenner <bennetbo@gmx.de>
Co-authored-by: Nathan Sobo <nathan@zed.dev>
Co-authored-by: Cole Miller <m@cole-miller.net>
Co-authored-by: Nathan Sobo <1789+nathansobo@users.noreply.github.com>
Co-authored-by: Cole Miller <53574922+cole-miller@users.noreply.github.com>
2025-05-02 19:00:55 -06:00
Danilo Leal
5053562e28 agent: Refresh the profile selector and modal design (#29816)
- [x] Separate MCP servers from tools in the profile customization modal
view
- [x] Group MCP tools in the MCP picker and add a heading
- [x] Separate bult-in profiles from custom ones in the dropdown
selector
- [x] Separate bult-in profiles from custom ones in the modal
- [ ] Enable looping through items via keybinding without opening the
dropdown (will be done on a follow-up PR)
- [ ] Stretch: Focus on the currently active item upon opening the
dropdown (will be done on a follow-up PR)

Release Notes:

- N/A

---------

Co-authored-by: Bennet Bo Fenner <53836821+bennetbo@users.noreply.github.com>
Co-authored-by: Bennet Bo Fenner <bennetbo@gmx.de>
Co-authored-by: Bennet Bo Fenner <bennet@zed.dev>
2025-05-02 20:34:36 -03:00
Agus Zubiaga
1877fce609 agent: Fix default cursor position on reviewing editors (#29825)
The cursor wasn't always placed at the first hunk for review editors.

Release Notes:

- N/A
2025-05-02 21:58:00 +00:00
Agus Zubiaga
64316309aa agent: Review edits in single-file editors (#29820)
Enables reviewing agent edits from single-file editors in addition to
the multibuffer experience we already had.


https://github.com/user-attachments/assets/a2c287f0-51d6-43a1-8537-821498b91983


This feature can be turned off by setting `assistant.single_file_review:
false`.

Release Notes:

- agent: Review edits in single-file editors
2025-05-02 17:57:16 -03:00
Max Brunsfeld
04772bf17d Add support for queuing status updates in cloud language model provider (#29818)
This sets us up to display queue position information to the user, once
our language model backend is updated to support request queuing.

The JSON returned by the LLM backend will need to look like this:

```json
{"queue": {"status": "queued", "position": 1}}
{"queue": {"status": "started"}}
{"event": {"THE_UPSTREAM_MODEL_PROVIDER_EVENT": "..."}} 
```

Release Notes:

- N/A

---------

Co-authored-by: Marshall Bowers <git@maxdeviant.com>
2025-05-02 20:36:39 +00:00
Richard Feldman
4d1df7bcd7 Re-enable directory-related tools (#29809)
Also `now` in `write` profile

Release Notes:

- Tools for manipulating directories no longer require confirmation, and
are enabled in the Write profile
- Enabled `now` and `list_directory` tools by default in Write profile

---------

Co-authored-by: Cole Miller <m@cole-miller.net>
Co-authored-by: Cole Miller <cole@zed.dev>
2025-05-02 16:11:16 -04:00
Cole Miller
9547d42b15 Support @-mentions in inline assists and when editing old agent panel messages (#29734)
Closes #ISSUE

Co-authored-by: Bennet <bennet@zed.dev>

Release Notes:

- Added support for context `@mentions` in the inline prompt editor and
when editing past messages in the agent panel.

---------

Co-authored-by: Bennet Bo Fenner <bennet@zed.dev>
Co-authored-by: Ben Brandt <benjamin.j.brandt@gmail.com>
2025-05-02 20:08:53 +00:00
Umesh Yadav
c918f6cde1 agent: Add assistant panel width persistence (#28808)
Previously, the assistant panel width was not persisted across sessions.
This meant that upon restarting the Zed editor, the panel would revert
to its default size, disrupting the user's preferred layout.

This pull request introduces persistence for the assistant panel width.
The width is now saved to the key-value store when the editor is closed
and restored on startup, ensuring a consistent UI experience across
different sessions.

Release Notes:

- agent: Add assistant panel width persistence

---------

Signed-off-by: Umesh Yadav <umesh4257@gmail.com>
2025-05-02 13:05:03 -07:00
Anthony Eid
da98e300cc debugger: Clear active debug line on thread continued (#29811)
I also moved the breakpoint store to session from local mode, because
both remote/local modes will need the ability to remove active debug
lines.

Release Notes:

- N/A
2025-05-02 15:24:28 -04:00
Richard Feldman
e6b0d8e48b Delete obsolete tools (#29808)
Release Notes:

- Removed some obsolete tools: batch_tool, code_actions, code_symbols,
contents, symbol_info, rename

Co-authored-by: Cole Miller <m@cole-miller.net>
2025-05-02 18:52:42 +00:00
Bennet Bo Fenner
9147f89257 zed_extension_api: Release v0.5.0 (#29802)
This PR releases v0.5.0 of the Zed extension API.

Support for this version of the extension API will land in Zed v0.186.x.

Release Notes:

- N/A
2025-05-02 15:58:54 +00:00
Richard Feldman
9efc09c5a6 Add eval for open_tool (#29801)
Also have its description say it should only be used on request

Release Notes:

- N/A
2025-05-02 15:56:07 +00:00
Bennet Bo Fenner
e6f6b351b7 extension_api: Add documentation to context server configuration (#29800)
Release Notes:

- N/A
2025-05-02 15:37:05 +00:00
Bennet Bo Fenner
fde621f0e3 agent: Ensure that web search tool is always available (#29799)
Some changes in the LanguageModelRegistry caused the web search tool not
to show up, because the `DefaultModelChanged` event is not emitted at
startup anymore.

Release Notes:

- agent: Fixed an issue where the web search tool would not be available
after starting Zed (only when using zed.dev as a provider).
2025-05-02 15:34:08 +00:00
Marshall Bowers
c4556e9909 collab: Fix adding users to feature flags when migrating to new billing (#29795)
This PR fixes an issue where users were not being added to the feature
flags when being migrated to the new billing.

Release Notes:

- N/A
2025-05-02 15:07:49 +00:00
Kirill Bulatov
7e2de84155 Properly score fuzzy match queries with multiple chars in lower case (#29794)
Closes https://github.com/zed-industries/zed/issues/29526

Release Notes:

- Fixed file finder crashing for certain file names with multiple chars
in lowercase form
2025-05-02 15:02:53 +00:00
Kirill Bulatov
d1b35be353 Use proper settings in the diagnostics section (#29791)
Follow-up of https://github.com/zed-industries/zed/pull/29706

Release Notes:

- N/A

Co-authored-by: Cole Miller <cole@zed.dev>
2025-05-02 16:48:52 +03:00
Marshall Bowers
49a71ec3b8 collab: Update billing migration endpoint to work for users without active subscriptions (#29792)
This PR updates the billing migration endpoint to work for users who do
not have an active subscription.

This will allow us to use the endpoint to migrate all users.

Release Notes:

- N/A
2025-05-02 13:48:14 +00:00
Nate Butler
3bd7ae6e5b Standardize agent previews (#29790)
This PR makes agent previews render like any other preview in the
component preview list & pages.

Page:

![CleanShot 2025-05-02 at 09 17
12@2x](https://github.com/user-attachments/assets/8b611380-b686-4fd6-9c76-de27e35b0b38)

List:

![CleanShot 2025-05-02 at 09 17
33@2x](https://github.com/user-attachments/assets/ab063649-dc3c-4c95-969b-c3795b2197f2)


Release Notes:

- N/A
2025-05-02 13:32:59 +00:00
Max Brunsfeld
225deb6785 agent: Add animation in the edit file tool card until a diff is assigned (#29773)
This PR prevents this edit card from being shown expanded but empty,
like this:

<img width="590" alt="Screenshot 2025-05-01 at 7 38 47 PM"
src="https://github.com/user-attachments/assets/147d3d73-05b9-4493-8145-0ee915f12cd9"
/>

Now, we will show an animation until it has a diff computed.


https://github.com/user-attachments/assets/52900cdf-ee3d-4c3b-88c7-c53377543bcf

Release Notes:

- N/A

---------

Co-authored-by: Danilo Leal <daniloleal09@gmail.com>
2025-05-02 09:48:40 -03:00
Kirill Bulatov
33011f2eaf Open diagnostics editor faster when fetching cargo diagnostics (#29787)
Follow-up of https://github.com/zed-industries/zed/pull/29706

Release Notes:

- N/A
2025-05-02 12:10:01 +00:00
Kirill Bulatov
e14d078f8a Fix tasks not being stopped on reruns (#29786)
Follow-up of https://github.com/zed-industries/zed/pull/28993

* Tone down tasks' cancellation logging
* Fix task terminals' leak, disallowing to fully cancel the task by
dropping the terminal off the pane:

f619d5f02a/crates/terminal_view/src/terminal_panel.rs (L1464-L1471)

Release Notes:

- Fixed tasks not being stopped on reruns
2025-05-02 11:45:43 +00:00
Stanislav Alekseev
460ac96df4 Use project environment in LSP runnables context (#29761)
Release Notes:

- Fixed the tasks from LSP not inheriting the worktree environment

----

cc @SomeoneToIgnore
2025-05-02 11:01:39 +00:00
Antonio Scandurra
35539847a4 Allow StreamingEditFileTool to also create files (#29785)
Refs #29733 

This pull request introduces a new field to the `StreamingEditFileTool`
that lets the model create or overwrite a file in a streaming way. When
one of the `assistant.stream_edits` setting / `agent-stream-edits`
feature flag is enabled, we are going to disable the `CreateFileTool` so
that the agent model can only use `StreamingEditFileTool` for file
creation.

Release Notes:

- N/A

---------

Co-authored-by: Ben Brandt <benjamin.j.brandt@gmail.com>
Co-authored-by: Oleksiy Syvokon <oleksiy.syvokon@gmail.com>
2025-05-02 09:57:04 +00:00
Anthony Eid
f619d5f02a debugger: Add debug task picker to new session modal (#29702)
## Preview 

![image](https://github.com/user-attachments/assets/203a577f-3b38-4017-9571-de1234415162)


### TODO
- [x] Add scenario picker to new session modal
- [x] Make debugger start action open new session modal instead of task
modal
- [x] Fix `esc` not clearing the cancelling the new session modal while
it's in scenario or attach mode
- [x] Resolve debug scenario's correctly

Release Notes:

- N/A
2025-05-02 08:38:29 +00:00
Kirill Bulatov
ba59305510 Use rust-analyzer's flycheck as source of cargo diagnostics (#29779)
Follow-up of https://github.com/zed-industries/zed/pull/29706

Instead of doing `cargo check` manually, use rust-analyzer's flycheck:
at the cost of more sophisticated check command configuration, we keep
much less code in Zed, and get a proper progress report.

User-facing UI does not change except `diagnostics_fetch_command` and
`env` settings removed from the diagnostics settings.

Release Notes:

- N/A
2025-05-02 10:07:51 +03:00
Nate Butler
672a1dd553 Add Agent Preview trait (#29760)
Like the title says

Release Notes:

- N/A
2025-05-01 23:03:06 -04:00
Marshall Bowers
93cc4946d8 agent: Make thread completion mode non-optional (#29772)
This PR makes the thread completion mode non-optional.

Release Notes:

- N/A
2025-05-02 02:41:54 +00:00
Marshall Bowers
0c0a4ed866 collab: Return increased limit for extended trials from GET /billing/usage (#29771)
This PR updates the `GET /billing/usage` endpoint to return the
increased usage limit for users in the extended trial.

Release Notes:

- N/A
2025-05-02 02:31:30 +00:00
Marshall Bowers
51f1998107 Fix typo in typos.toml (#29770)
This PR fixes a typo in `typos.toml`. How ironic.

Release Notes:

- N/A
2025-05-02 02:01:07 +00:00
Marshall Bowers
1ffedf4a08 collab: Add endpoint for migrating users to new billing (#29769)
This PR adds a new `POST /billing/subscriptions/migrate` endpoint for
migrating users to the new billing system.

When called with a GitHub user ID this endpoint will:

1. Find the active billing subscription for this user (if they have one)
2. Cancel the subscription and send a final invoice
3. Ensure the user is in the `new-billing` and `assistant2` feature
flags

Release Notes:

- N/A
2025-05-02 01:47:09 +00:00
Cole Miller
d25da9728b Run additional checks from script/clippy if local (#29768)
Should cut down on the number of CI cycles if you're forgetful like I
am!

Release Notes:

- N/A
2025-05-02 01:26:12 +00:00
Cole Miller
e1e3f2e423 Improve handling of remote-tracking branches in the picker (#29744)
Release Notes:

- Changed the git branch picker to make remote-tracking branches less
prominent

---------

Co-authored-by: Anthony Eid <hello@anthonyeid.me>
2025-05-01 21:24:26 -04:00
Finn Evers
92b9ecd7d2 agent: Do not render unnecessary lines in edit file tool card (#29766)
This PR prevents any unnecessary lines from being rendered in the edit
file tool card in the case of small diffs.

I think this (hopefully) addresses the last remaining task from
https://github.com/zed-industries/zed/pull/29448.

| `main` | This PR |
| --- | --- |
| <img width="634" alt="main"
src="https://github.com/user-attachments/assets/7c06394e-957a-4d36-a484-5974687041e9"
/> | <img width="634" alt="PR"
src="https://github.com/user-attachments/assets/84206d5a-a93a-4a42-99ca-7cdebb0d91bb"
/> |

(The last empty line in the second image is an empty line present in the
file itself)

---

n the second commit I also preemtively disabled vertical overscrolling
for full mode editors which are sized by content. This is basically the
same fix as in https://github.com/zed-industries/zed/pull/28471.
Strictly speaking, this is not needed for the fix here, but I thought it
might be nice to have for the future to prevent any issues from occuring
due to overscroll.

Release Notes:

- agent: Improved rendering of small diffs for the edit file tool card.
2025-05-01 20:40:12 -03:00
Marshall Bowers
758d260cec collab: Add ability to initiate a checkout session for the Zed Free plan (#29767)
This PR adds the ability to initiate a checkout session for the Zed Free
plan.

Release Notes:

- N/A
2025-05-01 23:35:23 +00:00
Danilo Leal
8d4d3badf3 agent: Add design adjustments to MCP config flow (#29765)
Mostly somewhat small UI tweaks around the MCP extension config flow and
the settings section.

Release Notes:

- N/A
2025-05-01 19:29:59 -03:00
Marshall Bowers
7c23d13773 agent: Render the max mode toggle using a muted color (#29763)
This PR updates the max mode toggle to use the muted color.

This makes it fit in more with the rest of the controls.

<img width="243" alt="Screenshot 2025-05-01 at 5 24 01 PM"
src="https://github.com/user-attachments/assets/57267d29-3c7b-4ea9-b6b9-81c42f6b7e1c"
/>

Release Notes:

- agent: Adjusted the color of the max mode toggle.
2025-05-01 21:40:10 +00:00
Richard Feldman
ad87c545c7 Make context pills clickable while editing (#29740)
Release Notes:

- Fixed a bug where clicking context pills switched into the "editing
message" state instead of clicking the pill.

Co-authored-by: Michael <michael@zed.dev>
Co-authored-by: Ben <ben@zed.dev>
2025-05-01 20:28:54 +00:00
Richard Feldman
23fbab15ee Manual no tool calls (#29745)
Now instead of the model hallucinating tool calls, we get requests for
more context:

<img width="620" alt="Screenshot 2025-05-01 at 12 45 49 PM"
src="https://github.com/user-attachments/assets/847d5c14-82f6-4234-b85a-8cd2bc7ab11d"
/>

It still knows how to answer general questions:
<img width="624" alt="Screenshot 2025-05-01 at 12 47 44 PM"
src="https://github.com/user-attachments/assets/43ab0fc3-4cc8-452f-b26b-474b5d31919f"
/>

Release Notes:

- Fixed the model still trying to do tool calls when no tools selected
(e.g. in `Manual` profile).

---------

Co-authored-by: Ben <ben@zed.dev>
Co-authored-by: Michael <michael@zed.dev>
2025-05-01 16:11:13 -04:00
Richard Feldman
d7e181576e Respect cursor_pointer when a ButtonLike is disabled (#29737)
This is desirable for when we want to use a `ButtonLike` to show a
tooltip over an icon, and we don't want it to show the "not allowed"
cursor on hover.

Release Notes:

- N/A
2025-05-01 15:34:40 -04:00
Eva Pace
9788aff4b1 Fix license symlinks (#29758)
Closes #29527

It looks funny in the diff, but the symlinks are indeed correct:

-
https://github.com/evaporei/zed/blob/fix/license-symlinks/crates/askpass/LICENSE-GPL
-
https://github.com/evaporei/zed/blob/fix/license-symlinks/crates/ui_macros/LICENSE-GPL

I did check all ~170 crates, these were the only inconsistent ones.

Release Notes:

- N/A
2025-05-01 19:24:14 +00:00
Kirill Bulatov
2a319efade Add editor::GoToParentModule for rust-analyzer backed projects (#29755)
Support rust-analyzer's "go to parent module" action


https://rust-analyzer.github.io/book/contributing/lsp-extensions.html#parent-module

Release Notes:

- Added `editor::GoToParentModule` for rust-analyzer backed projects

---------

Co-authored-by: Julia Ryan <juliaryan3.14@gmail.com>
2025-05-01 18:28:05 +00:00
Jonathan LEI
50ec26c163 Fix user rules ignored by agent (#29754)
Closes #29753

The template contains an error: `has_default_user_rules` is always
undefined and should be `has_user_rules` instead.

Release Notes:

- Fixed default user rules ignored during prompt building.
2025-05-01 18:22:48 +00:00
Danilo Leal
39dd133b1c agent: Remove unused agent: chat mode command palette action (#29741)
We weren't using this one anymore. We used to use it for the switch that
toggled tools on, which doesn't exist anymore.

Release Notes:

- N/A

---------

Co-authored-by: Joseph T. Lyons <josephtlyons@gmail.com>
2025-05-01 15:09:14 -03:00
Bennet Bo Fenner
24eb039752 context servers: Show configuration modal when extension is installed (#29309)
WIP

Release Notes:

- N/A

---------

Co-authored-by: Danilo Leal <67129314+danilo-leal@users.noreply.github.com>
Co-authored-by: Danilo Leal <daniloleal09@gmail.com>
Co-authored-by: Marshall Bowers <git@maxdeviant.com>
Co-authored-by: Cole Miller <m@cole-miller.net>
Co-authored-by: Antonio Scandurra <me@as-cii.com>
Co-authored-by: Oleksiy Syvokon <oleksiy.syvokon@gmail.com>
2025-05-01 20:02:14 +02:00
Peter Tripp
bffa53d706 docs: Reorder macOS development documentation (#29751)
Release Notes:

- N/A
2025-05-01 17:34:17 +00:00
Bennet Bo Fenner
0e5e8f9f8d Allow MIT-0 license in checks (#29748)
Part of #29309

The license is on par with other licenses in the list:
https://github.com/aws/mit-0

Release Notes:

- N/A
2025-05-01 17:30:16 +00:00
Danilo Leal
96d785cb45 git: Improve co-author button (#29742)
This PR changes the tooltip label to say "Remove" when you have the
button toggled on and collaborators in the list.

Release Notes:

- N/A

Co-authored-by: Joseph T. Lyons <josephtlyons@gmail.com>
2025-05-01 14:12:52 -03:00
Marshall Bowers
57610c9935 collab: Add billing thresholds to request overage subscription items (#29738)
This PR adds billing thresholds of the unit equivalent of $20 for model
request overages.

Release Notes:

- N/A
2025-05-01 16:10:06 +00:00
Marshall Bowers
5bf1b4f0a8 collab: Add use_new_billing to LlmTokenClaims (#29739)
This PR adds a `use_new_billing` field to the LLM token claims, based on
the `new-billing` feature flag.

Release Notes:

- N/A
2025-05-01 15:43:53 +00:00
Antonio Scandurra
f891dfb358 Introduce a new StreamingEditFileTool (#29733)
This pull request introduces a new tool for streaming edits. The
short-term goal is for this tool to replace the existing `EditFileTool`,
but we want to get this out the door as soon as possible so that we can
start testing it.

`StreamingEditFileTool` is mutually exclusive with `EditFileTool`. It
will be enabled by default for anyone who has the `agent-stream-edits`
feature flag, as well as people that set `assistant.stream_edits` to
`true` in their settings.

### Implementation

Streaming is achieved by requesting a completion while the `edit_file`
tool gets called. We invoke the model by taking the existing
conversation with the agent and appending a prompt specifically tailored
for editing. In that prompt, we ask the model to produce a stream of
`<old_text>`/`<new_text>` tags. As the model streams text in, we
incrementally parse it and start editing as soon as we can.

### Evals

Note that, as part of this pull request, I also defined some new evals
that I used to drive the behavior of the recursive LLM call. To run
them, use this command:

```bash
cargo test --package=assistant_tools --features eval -- eval_extract_handle_command_output
```

Or comment out the `#[cfg_attr(not(feature = "eval"), ignore)]` macro.

I recommend running them one at a time, because right now we don't
really have a way of orchestrating of all these evals. I think we should
invest into that effort once the new agent panel goes live.

Release Notes:

- N/A

---------

Co-authored-by: Nathan Sobo <nathan@zed.dev>
Co-authored-by: Bennet Bo Fenner <bennetbo@gmx.de>
Co-authored-by: Oleksiy Syvokon <oleksiy.syvokon@gmail.com>
2025-05-01 17:37:43 +02:00
Ben Kunkle
e3a2d52472 zlog: Fall back to printing module path instead of *unknown* or just crate name (#29691)
Closes #ISSUE

Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-05-01 10:59:51 -04:00
Danilo Leal
122af4fd53 agent: Show nav dropdown close button only on hover (#29732) 2025-05-01 11:21:57 -03:00
Kirill Bulatov
e07ffe7cf1 Allow to fetch cargo diagnostics separately (#29706)
Adjusts the way `cargo` and `rust-analyzer` diagnostics are fetched into
Zed.

Nothing is changed for defaults: in this mode, Zed does nothing but
reports file updates, which trigger rust-analyzers'
mechanisms:

* generating internal diagnostics, which it is able to produce on the
fly, without blocking cargo lock.
Unfortunately, there are not that many diagnostics in r-a, and some of
them have false-positives compared to rustc ones

* running `cargo check --workspace --all-targets` on each file save,
taking the cargo lock
For large projects like Zed, this might take a while, reducing the
ability to choose how to work with the project: e.g. it's impossible to
save multiple times without long diagnostics refreshes (may happen
automatically on e.g. focus loss), save the project and run it instantly
without waiting for cargo check to finish, etc.

In addition, it's relatively tricky to reconfigure r-a to run a
different command, with different arguments and maybe different env
vars: that would require a language server restart (and a large project
reindex) and fiddling with multiple JSON fields.

The new mode aims to separate out cargo diagnostics into its own loop so
that all Zed diagnostics features are supported still.


For that, an extra mode was introduced:

```jsonc
"rust": {
  // When enabled, Zed runs `cargo check --message-format=json`-based commands and
  // collect cargo diagnostics instead of rust-analyzer.
  "fetch_cargo_diagnostics": false,
  // A command override for fetching the cargo diagnostics.
  // First argument is the command, followed by the arguments.
  "diagnostics_fetch_command": [
    "cargo",
    "check",
    "--quiet",
    "--workspace",
    "--message-format=json",
    "--all-targets",
    "--keep-going"
  ],
  // Extra environment variables to pass to the diagnostics fetch command.
  "env": {}
}
```

which calls to cargo, parses its output and mixes in with the existing
diagnostics:




https://github.com/user-attachments/assets/e986f955-b452-4995-8aac-3049683dd22c




Release Notes:

- Added a way to get diagnostics from cargo and rust-analyzer without
mutually locking each other
- Added `ctrl-r` binding to refresh diagnostics in the project
diagnostics editor context
2025-05-01 11:25:52 +03:00
Finn Evers
5e4be013af zed: Fix application menu capitalization (#29722)
This PR is a quick follow-up to #29717 to ensure that the action within
the app menu has the same capitalization as in the context menu.

Release Notes:

- N/A
2025-05-01 08:19:02 +00:00
Aaron Feickert
f055dca592 editor: Fix context menu capitalization (#29717)
Fixes context menu capitalization for consistency.

Release Notes:

- N/A
2025-05-01 09:58:08 +03:00
Richard Feldman
5872276511 Re-enable open tool (#29707)
Release Notes:

- Added `open` tool for opening files or URLs.
2025-04-30 22:33:52 -04:00
Bennet Bo Fenner
1bf9e15f26 agent: Allow adding/removing context when editing existing messages (#29698)
Release Notes:

- agent: Support adding/removing context when editing existing message

---------

Co-authored-by: Cole Miller <m@cole-miller.net>
Co-authored-by: Cole Miller <cole@zed.dev>
2025-05-01 01:39:34 +00:00
Marshall Bowers
f046d70625 collab: Look up Stripe prices with lookup keys (#29715)
This PR makes it so we look up Stripe prices via lookup keys instead of
passing in the price IDs as environment variables.

Release Notes:

- N/A
2025-05-01 00:26:31 +00:00
Richard Feldman
afeb3d4fd9 Make eval more resilient to bad input from LLM (#29703)
I saw a slice panic (for begin > end) in a debug build of the eval. This
should just be a failed assertion, not a panic that takes out the whole
eval run!

Release Notes:

- N/A
2025-04-30 18:13:45 -04:00
Richard Feldman
92dd6b67c7 Fix potential subtraction overflow (#29697)
I saw this come up in an eval, where the LLM provided a start line of 0.

Release Notes:

- N/A
2025-04-30 18:13:37 -04:00
Cole Miller
38ede4bae3 Fix parsing of author name in git show output (#29704)
Closes #ISSUE

Release Notes:

- Fixed a bug causing incorrect formatting of git commit tooltips
2025-04-30 20:54:53 +00:00
Ben Kunkle
fc920bf63d Improve behavior around word-based brackets in bash (#29700)
Closes #28414

Makes it so that `do`, `then`, `done`, `else`, etc are treated as
brackets in bash. They are not auto-closed *yet* as that requires
additional work to function properly, however they can now be toggled
between using `%` in vim. Additionally, newlines are inserted like they
are with regular brackets (`{}()[]""''`) when hitting enter between
them.

While `if <-> fi` `while/for <-> done` and `case <-> esac` are the
*logical* matching pairs, I've opted to instead match between `then <->
else/elif/fi` `do <-> done` and `in <-> esac` as these are the pairs
that delimit the sub-scope, and are more similar to the `{}` style
bracket pairs than `if <-> }` in a c-like syntax. This does cause some
wierd behavior with `else` in `if` expressions as it matches both with
the previous `then` as well as the following `fi`, so in this case

```bash
if true; then
   foo
else
   bar
f|i
```

after hitting `%` twice times (where cursor is `|`), the cursor will end
up on the `then` instead of back on the `fi` as hitting `%` on the else
will *always* navigate up to the `then`

Release Notes:

- vim: Improved behavior around word-based delimiters in bash (`do <->
done`, `then <-> fi`, etc) so they can be toggled between using `%`
2025-04-30 19:57:29 +00:00
Richard Feldman
04c68dc0cf Make the default repetitions be 8, and concurrency 4 (#29576)
This is based on having observed that there is a lot of variation
between runs on `n=1` and `n=3`.

* With `n=8` two runs on the same branch give answers that seem close
enough to be reasonably consistent.
* With higher concurrency, trying to run this many repetitions seems to
lead language servers to time out a lot, causing evals to fail.

Release Notes:

- N/A
2025-04-30 15:21:19 -04:00
Marshall Bowers
399eced884 collab: Return current usage by model from GET /billing/usage (#29693)
This PR updates the `GET /billing/usage` endpoint to return the number
of requests made to each model and mode.

Release Notes:

- N/A
2025-04-30 19:06:39 +00:00
Richard Feldman
50f705e779 Use outline (#29687)
## Before

![Screenshot 2025-04-30 at 10 56
36 AM](https://github.com/user-attachments/assets/3a435f4c-ad45-4f26-a847-2d5c9d03648e)

## After

![Screenshot 2025-04-30 at 10 55
27 AM](https://github.com/user-attachments/assets/cc3a8144-b6fe-4a15-8a47-b2487ce4f66e)

Release Notes:

- Context picker and `@`-mentions now work with very large files.
2025-04-30 18:00:00 +00:00
Ben Kunkle
8173534ad5 zed: Reinstate default file_scan_exclusions in Zed repo project settings (#29690)
Closes #ISSUE

Re-adds default `file_scan_exclusions` to [project
settings](84e4891d54/.zed/settings.json)
that were overridden in #29106

Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-04-30 17:50:56 +00:00
Nate Butler
8c03934b26 welcome: Theme preview tile (#29689)
![CleanShot 2025-04-30 at 13 26
44@2x](https://github.com/user-attachments/assets/f68fefe2-84a1-48b7-b9a2-47c2547cd06b)


- Adds the ThemePreviewTile component, used for upcoming onboarding UI
- Adds the CornerSolver utility for resolving correct nested corner
radii

Release Notes:

- N/A
2025-04-30 17:46:11 +00:00
Patrick
84e4891d54 file_finder: Add skip_focus_for_active_in_search setting (#27624)
Closes #27073

Currently, when searching for a file with Ctrl+P, and the first file
found is the active one, file_finder skips focus to the second file
automatically. This PR adds a setting to disable this and make the first
file always the focused one.

Default setting is still skipping the active file.

Release Notes: 

- Added the `skip_focus_for_active_in_search` setting for the file
finder, which allows turning off the default behavior of skipping focus
on the active file while searching in the file finder.

---------

Co-authored-by: Smit Barmase <heysmitbarmase@gmail.com>
2025-04-30 22:58:33 +05:30
Ben Kunkle
d03d8ccec1 python: Fix identification of runnable tests within decorated test classes (#29688)
Closes #29486

Release Notes:

- python: Fixed identification of runnable test functions within
decorated pytest classes
2025-04-30 17:26:30 +00:00
Joseph T. Lyons
4d934f2884 Bump Zed to v0.186 (#29680)
Release Notes:

-N/A
2025-04-30 12:52:25 -04:00
Smit Barmase
e697cf9747 editor: Fix edit range for linked edits on do completion (#29650)
Closes #29544

Fixes an issue where accepting an HTML completion would correctly edit
the start tag but incorrectly update the end tag due to incorrect linked
edit ranges.

I want to handle multi cursor case (as it barely works now), but seems
like this should go first. As, it might need whole `do_completions`
overhaul.

Todo:
- [x] Tests for completion aceept on linked edits

Before:


https://github.com/user-attachments/assets/917f8d2a-4a0f-46e8-a004-675fde55fe3d

After:


https://github.com/user-attachments/assets/84b760b6-a5b9-45c4-85d8-b5dccf97775f

Release Notes:

- Fixes an issue where accepting an HTML completion would correctly edit
the start tag but incorrectly update the end tag.
2025-04-30 21:44:20 +05:30
230 changed files with 6970 additions and 3597 deletions

View File

@@ -69,7 +69,7 @@ jobs:
run: cargo build --package=eval
- name: Run eval
run: cargo run --package=eval -- --repetitions=3 --concurrency=1
run: cargo run --package=eval -- --repetitions=8 --concurrency=1
# Even the Linux runner is not stateful, in theory there is no need to do this cleanup.
# But, to avoid potential issues in the future if we choose to use a stateful Linux runner and forget to add code

View File

@@ -46,5 +46,17 @@
"formatter": "auto",
"remove_trailing_whitespace_on_save": true,
"ensure_final_newline_on_save": true,
"file_scan_exclusions": ["crates/eval/worktrees/", "crates/eval/repos/"]
"file_scan_exclusions": [
"crates/eval/worktrees/",
"crates/eval/repos/",
"**/.git",
"**/.svn",
"**/.hg",
"**/.jj",
"**/CVS",
"**/.DS_Store",
"**/Thumbs.db",
"**/.classpath",
"**/.settings"
]
}

19
Cargo.lock generated
View File

@@ -62,6 +62,7 @@ dependencies = [
"chrono",
"client",
"collections",
"command_palette_hooks",
"component",
"context_server",
"convert_case 0.8.0",
@@ -546,6 +547,7 @@ dependencies = [
"collections",
"context_server",
"editor",
"feature_flags",
"fs",
"futures 0.3.31",
"fuzzy",
@@ -593,8 +595,8 @@ version = "0.1.0"
dependencies = [
"anthropic",
"anyhow",
"collections",
"deepseek",
"feature_flags",
"fs",
"gpui",
"indexmap",
@@ -610,7 +612,6 @@ dependencies = [
"serde_json_lenient",
"settings",
"workspace-hack",
"zed_llm_client",
]
[[package]]
@@ -3004,7 +3005,6 @@ dependencies = [
"anyhow",
"assistant",
"assistant_context_editor",
"assistant_settings",
"assistant_slash_command",
"assistant_tool",
"async-stripe",
@@ -3238,6 +3238,7 @@ dependencies = [
"gpui",
"linkme",
"parking_lot",
"strum 0.27.1",
"theme",
"workspace-hack",
]
@@ -4100,6 +4101,7 @@ dependencies = [
"anyhow",
"async-trait",
"dap",
"futures 0.3.31",
"gpui",
"language",
"lsp-types",
@@ -4393,6 +4395,7 @@ dependencies = [
"ctor",
"editor",
"env_logger 0.11.8",
"futures 0.3.31",
"gpui",
"indoc",
"language",
@@ -7981,6 +7984,7 @@ dependencies = [
"log",
"lsp",
"node_runtime",
"parking_lot",
"paths",
"pet",
"pet-conda",
@@ -14744,6 +14748,7 @@ dependencies = [
"log",
"project",
"rand 0.8.5",
"regex",
"schemars",
"search",
"serde",
@@ -15080,6 +15085,7 @@ dependencies = [
"client",
"collections",
"db",
"feature_flags",
"gpui",
"http_client",
"notifications",
@@ -17085,18 +17091,22 @@ version = "0.1.0"
dependencies = [
"anyhow",
"client",
"component",
"db",
"documented",
"editor",
"fuzzy",
"gpui",
"install_cli",
"language",
"linkme",
"picker",
"project",
"schemars",
"serde",
"settings",
"telemetry",
"theme",
"ui",
"util",
"vim_mode_setting",
@@ -18148,6 +18158,7 @@ dependencies = [
"itertools 0.14.0",
"language",
"log",
"menu",
"node_runtime",
"parking_lot",
"postage",
@@ -18683,7 +18694,7 @@ dependencies = [
[[package]]
name = "zed"
version = "0.185.10"
version = "0.186.0"
dependencies = [
"activity_indicator",
"agent",

View File

@@ -433,6 +433,7 @@ dap-types = { git = "https://github.com/zed-industries/dap-types", rev = "be69a0
dashmap = "6.0"
derive_more = "0.99.17"
dirs = "4.0"
documented = "0.9.1"
dotenv = "0.15.0"
ec4rs = "1.1"
emojis = "0.6.1"
@@ -797,5 +798,6 @@ ignored = [
"serde",
"component",
"linkme",
"documented",
"workspace-hack",
]

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-user-round-check-icon lucide-user-round-check"><path d="M2 21a8 8 0 0 1 13.292-6"/><circle cx="10" cy="8" r="5"/><path d="m16 19 2 2 4-4"/></svg>

After

Width:  |  Height:  |  Size: 348 B

View File

@@ -237,14 +237,11 @@
"save": "workspace::Save",
"ctrl->": "assistant::QuoteSelection",
"ctrl-<": "assistant::InsertIntoEditor",
"ctrl-alt-/": "agent::ToggleModelSelector",
"shift-enter": "assistant::Split",
"ctrl-r": "assistant::CycleMessageRole",
"enter": "assistant::ConfirmCommand",
"alt-enter": "editor::Newline",
"ctrl-k c": "assistant::CopyCode",
"ctrl-g": "search::SelectNextMatch",
"ctrl-shift-g": "search::SelectPreviousMatch",
"ctrl-k l": "agent::OpenRulesLibrary"
"alt-enter": "editor::Newline"
}
},
{
@@ -261,8 +258,7 @@
"ctrl-shift-o": "agent::ToggleNavigationMenu",
"ctrl-shift-i": "agent::ToggleOptionsMenu",
"shift-escape": "agent::ExpandMessageEditor",
"ctrl-alt-e": "agent::RemoveAllContext",
"ctrl-shift-e": "project_panel::ToggleFocus"
"ctrl-alt-e": "agent::RemoveAllContext"
}
},
{
@@ -983,5 +979,12 @@
"enter": "editor::Newline",
"ctrl-enter": "menu::Confirm"
}
},
{
"context": "Diagnostics",
"use_key_equivalents": true,
"bindings": {
"ctrl-r": "diagnostics::ToggleDiagnosticsRefresh"
}
}
]

View File

@@ -1091,5 +1091,12 @@
"enter": "editor::Newline",
"cmd-enter": "menu::Confirm"
}
},
{
"context": "Diagnostics",
"use_key_equivalents": true,
"bindings": {
"ctrl-r": "diagnostics::ToggleDiagnosticsRefresh"
}
}
]

View File

@@ -307,6 +307,11 @@
// Whether to show agent review buttons in the editor toolbar.
"agent_review": true
},
// Titlebar related settings
"title_bar": {
// Whether to show the branch icon beside branch switcher in the title bar.
"show_branch_icon": false
},
// Scrollbar related settings
"scrollbar": {
// When to show the scrollbar in the editor.
@@ -605,11 +610,13 @@
//
// Default: main
"fallback_branch_name": "main",
// Whether to sort entries in the panel by path
// or by status (the default).
//
// Default: false
"sort_by_path": false,
"scrollbar": {
// When to show the scrollbar in the git panel.
//
@@ -635,8 +642,6 @@
"version": "2",
// Whether the agent is enabled.
"enabled": true,
/// What completion mode to start new threads in, if available. Can be 'normal' or 'max'.
"preferred_completion_mode": "normal",
// Whether to show the agent panel button in the status bar.
"button": true,
// Where to dock the agent panel. Can be 'left', 'right' or 'bottom'.
@@ -659,28 +664,6 @@
// The model to use.
"model": "claude-3-7-sonnet-latest"
},
// Additional parameters for language model requests. When making a request to a model, parameters will be taken
// from the last entry in this list that matches the model's provider and name. In each entry, both provider
// and model are optional, so that you can specify parameters for either one.
"model_parameters": [
// To set parameters for all requests to OpenAI models:
// {
// "provider": "openai",
// "temperature": 0.5
// }
//
// To set parameters for all requests in general:
// {
// "temperature": 0
// }
//
// To set parameters for a specific provider and model:
// {
// "provider": "zed.dev",
// "model": "claude-3-7-sonnet-latest",
// "temperature": 1.0
// }
],
// When enabled, the agent can run potentially destructive actions without asking for your confirmation.
"always_allow_tool_actions": false,
// When enabled, the agent will stream edits.
@@ -860,7 +843,20 @@
// "modal_max_width": "full"
//
// Default: small
"modal_max_width": "small"
"modal_max_width": "small",
// Determines whether the file finder should skip focus for the active file in search results.
// There are 2 possible values:
//
// 1. true: When searching for files, if the currently active file appears as the first result,
// auto-focus will skip it and focus the second result instead.
// "skip_focus_for_active_in_search": true
//
// 2. false: When searching for files, the first result will always receive focus,
// even if it's the currently active file.
// "skip_focus_for_active_in_search": false
//
// Default: true
"skip_focus_for_active_in_search": true
},
// Whether or not to remove any trailing whitespace from lines of a buffer
// before saving it.
@@ -912,6 +908,8 @@
"hard_tabs": false,
// How many columns a tab should occupy.
"tab_size": 4,
// What debuggers are preferred by default for all languages.
"debuggers": [],
// Control what info is collected by Zed.
"telemetry": {
// Send debug info like crash reports.
@@ -943,6 +941,11 @@
// The minimum severity of the diagnostics to show inline.
// Shows all diagnostics when not specified.
"max_severity": null
},
"cargo": {
// When enabled, Zed disables rust-analyzer's check on save and starts to query
// Cargo diagnostics separately.
"fetch_cargo_diagnostics": false
}
},
// Files or globs of files that will be excluded by Zed entirely. They will be skipped during file
@@ -1326,6 +1329,9 @@
"Elixir": {
"language_servers": ["elixir-ls", "!next-ls", "!lexical", "..."]
},
"Elm": {
"tab_size": 4
},
"Erlang": {
"language_servers": ["erlang-ls", "!elp", "..."]
},

View File

@@ -84,19 +84,6 @@ impl ActivityIndicator {
})
.detach();
let mut status_events = languages.dap_server_binary_statuses();
cx.spawn(async move |this, cx| {
while let Some((name, status)) = status_events.next().await {
this.update(cx, |this, cx| {
this.statuses.retain(|s| s.name != name);
this.statuses.push(ServerStatus { name, status });
cx.notify();
})?;
}
anyhow::Ok(())
})
.detach();
cx.subscribe(
&project.read(cx).lsp_store(),
|_, _, event, cx| match event {

View File

@@ -29,6 +29,7 @@ buffer_diff.workspace = true
chrono.workspace = true
client.workspace = true
collections.workspace = true
command_palette_hooks.workspace = true
component.workspace = true
context_server.workspace = true
convert_case.workspace = true

View File

@@ -1417,10 +1417,7 @@ impl ActiveThread {
messages: vec![request_message],
tools: vec![],
stop: vec![],
temperature: AssistantSettings::temperature_for_model(
&configured_model.model,
cx,
),
temperature: None,
};
Some(configured_model.model.count_tokens(request, cx))
@@ -3466,11 +3463,6 @@ pub(crate) fn open_active_thread_as_markdown(
.unwrap_or_else(|| "Thread".to_string());
let project = workspace.project().clone();
if !project.read(cx).is_local() {
anyhow::bail!("failed to open active thread as markdown in remote project");
}
let buffer = project.update(cx, |project, cx| {
project.create_local_buffer(&markdown, Some(markdown_language), cx)
});

View File

@@ -29,6 +29,8 @@ use std::sync::Arc;
use assistant_settings::{AgentProfileId, AssistantSettings};
use client::Client;
use command_palette_hooks::CommandPaletteFilter;
use feature_flags::{Assistant2FeatureFlag, FeatureFlagAppExt};
use fs::Fs;
use gpui::{App, actions, impl_actions};
use language::LanguageRegistry;
@@ -105,6 +107,8 @@ impl ManageProfiles {
impl_actions!(agent, [NewThread, ManageProfiles]);
const NAMESPACE: &str = "agent";
/// Initializes the `agent` crate.
pub fn init(
fs: Arc<dyn Fs>,
@@ -132,4 +136,25 @@ pub fn init(
);
cx.observe_new(AddContextServerModal::register).detach();
cx.observe_new(ManageProfilesModal::register).detach();
feature_gate_agent_actions(cx);
}
fn feature_gate_agent_actions(cx: &mut App) {
CommandPaletteFilter::update_global(cx, |filter, _cx| {
filter.hide_namespace(NAMESPACE);
});
cx.observe_flag::<Assistant2FeatureFlag, _>(move |is_enabled, cx| {
if is_enabled {
CommandPaletteFilter::update_global(cx, |filter, _cx| {
filter.show_namespace(NAMESPACE);
});
} else {
CommandPaletteFilter::update_global(cx, |filter, _cx| {
filter.hide_namespace(NAMESPACE);
});
}
})
.detach();
}

View File

@@ -46,7 +46,7 @@ use ui::{
use util::{ResultExt as _, maybe};
use workspace::dock::{DockPosition, Panel, PanelEvent};
use workspace::{CollaboratorId, DraggedSelection, DraggedTab, ToolbarItemView, Workspace};
use zed_actions::agent::{OpenConfiguration, OpenOnboardingModal, ResetOnboarding};
use zed_actions::agent::OpenConfiguration;
use zed_actions::assistant::{OpenRulesLibrary, ToggleFocus};
use zed_actions::{DecreaseBufferFontSize, IncreaseBufferFontSize, ResetBufferFontSize};
use zed_llm_client::UsageLimit;
@@ -59,7 +59,6 @@ use crate::message_editor::{MessageEditor, MessageEditorEvent};
use crate::thread::{Thread, ThreadError, ThreadId, TokenUsageRatio};
use crate::thread_history::{EntryTimeFormat, PastContext, PastThread, ThreadHistory};
use crate::thread_store::ThreadStore;
use crate::ui::AgentOnboardingModal;
use crate::{
AddContextServer, AgentDiffPane, ContextStore, DeleteRecentlyOpenThread, ExpandMessageEditor,
Follow, InlineAssistant, NewTextThread, NewThread, OpenActiveThreadAsMarkdown, OpenAgentDiff,
@@ -146,13 +145,6 @@ pub fn init(cx: &mut App) {
});
}
})
.register_action(|workspace, _: &OpenOnboardingModal, window, cx| {
AgentOnboardingModal::toggle(workspace, window, cx)
})
.register_action(|_workspace, _: &ResetOnboarding, window, cx| {
window.dispatch_action(workspace::RestoreBanner.boxed_clone(), cx);
window.refresh();
})
.register_action(|_workspace, _: &ResetTrialUpsell, _window, cx| {
set_trial_upsell_dismissed(false, cx);
});
@@ -1470,7 +1462,6 @@ impl AssistantPanel {
let thread = active_thread.thread().read(cx);
let thread_id = thread.id().clone();
let is_empty = active_thread.is_empty();
let editor_empty = self.message_editor.read(cx).is_editor_fully_empty(cx);
let last_usage = active_thread.thread().read(cx).last_usage().or_else(|| {
maybe!({
let amount = user_store.model_request_usage_amount()?;
@@ -1493,7 +1484,7 @@ impl AssistantPanel {
let account_url = zed_urls::account_url(cx);
let show_token_count = match &self.active_view {
ActiveView::Thread { .. } => !is_empty || !editor_empty,
ActiveView::Thread { .. } => !is_empty,
ActiveView::PromptEditor { .. } => true,
_ => false,
};
@@ -1813,10 +1804,6 @@ impl AssistantPanel {
}
fn should_render_upsell(&self, cx: &mut Context<Self>) -> bool {
if !matches!(self.active_view, ActiveView::Thread { .. }) {
return false;
}
if self.hide_trial_upsell || dismissed_trial_upsell() {
return false;
}

View File

@@ -2,7 +2,6 @@ use crate::context::ContextLoadResult;
use crate::inline_prompt_editor::CodegenStatus;
use crate::{context::load_context, context_store::ContextStore};
use anyhow::Result;
use assistant_settings::AssistantSettings;
use client::telemetry::Telemetry;
use collections::HashSet;
use editor::{Anchor, AnchorRangeExt, MultiBuffer, MultiBufferSnapshot, ToOffset as _, ToPoint};
@@ -384,7 +383,7 @@ impl CodegenAlternative {
if user_prompt.trim().to_lowercase() == "delete" {
async { Ok(LanguageModelTextStream::default()) }.boxed_local()
} else {
let request = self.build_request(&model, user_prompt, cx)?;
let request = self.build_request(user_prompt, cx)?;
cx.spawn(async move |_, cx| model.stream_completion_text(request.await, &cx).await)
.boxed_local()
};
@@ -394,7 +393,6 @@ impl CodegenAlternative {
fn build_request(
&self,
model: &Arc<dyn LanguageModel>,
user_prompt: String,
cx: &mut App,
) -> Result<Task<LanguageModelRequest>> {
@@ -443,8 +441,6 @@ impl CodegenAlternative {
}
});
let temperature = AssistantSettings::temperature_for_model(&model, cx);
Ok(cx.spawn(async move |_cx| {
let mut request_message = LanguageModelRequestMessage {
role: Role::User,
@@ -467,7 +463,7 @@ impl CodegenAlternative {
mode: None,
tools: Vec::new(),
stop: Vec::new(),
temperature,
temperature: None,
messages: vec![request_message],
}
}))

View File

@@ -17,6 +17,7 @@ use editor::{
ToDisplayPoint,
},
};
use feature_flags::{Assistant2FeatureFlag, FeatureFlagViewExt as _};
use fs::Fs;
use gpui::{
App, Context, Entity, Focusable, Global, HighlightStyle, Subscription, Task, UpdateGlobal,
@@ -65,6 +66,15 @@ pub fn init(
InlineAssistant::update_global(cx, |inline_assistant, cx| {
inline_assistant.register_workspace(&workspace, window, cx)
});
cx.observe_flag::<Assistant2FeatureFlag, _>(window, {
|is_assistant2_enabled, _workspace, _window, cx| {
InlineAssistant::update_global(cx, |inline_assistant, _cx| {
inline_assistant.is_assistant2_enabled = is_assistant2_enabled;
});
}
})
.detach();
})
.detach();
}
@@ -87,6 +97,7 @@ pub struct InlineAssistant {
prompt_builder: Arc<PromptBuilder>,
telemetry: Arc<Telemetry>,
fs: Arc<dyn Fs>,
is_assistant2_enabled: bool,
}
impl Global for InlineAssistant {}
@@ -108,6 +119,7 @@ impl InlineAssistant {
prompt_builder,
telemetry,
fs,
is_assistant2_enabled: false,
}
}
@@ -176,7 +188,7 @@ impl InlineAssistant {
window: &mut Window,
cx: &mut App,
) {
let is_assistant2_enabled = true;
let is_assistant2_enabled = self.is_assistant2_enabled;
if let Some(editor) = item.act_as::<Editor>(cx) {
editor.update(cx, |editor, cx| {

View File

@@ -8,7 +8,6 @@ use crate::ui::{
AnimatedLabel, MaxModeTooltip,
preview::{AgentPreview, UsageCallout},
};
use assistant_settings::{AssistantSettings, CompletionMode};
use buffer_diff::BufferDiff;
use client::UserStore;
use collections::{HashMap, HashSet};
@@ -17,6 +16,7 @@ use editor::{
AnchorRangeExt, ContextMenuOptions, ContextMenuPlacement, Editor, EditorElement, EditorEvent,
EditorMode, EditorStyle, MultiBuffer,
};
use feature_flags::{FeatureFlagAppExt, NewBillingFeatureFlag};
use file_icons::FileIcons;
use fs::Fs;
use futures::future::Shared;
@@ -42,6 +42,7 @@ use ui::{Disclosure, DocumentationSide, KeyBinding, PopoverMenuHandle, Tooltip,
use util::{ResultExt as _, maybe};
use workspace::dock::DockPosition;
use workspace::{CollaboratorId, Workspace};
use zed_llm_client::CompletionMode;
use crate::context_picker::{ContextPicker, ContextPickerCompletionProvider, crease_for_mention};
use crate::context_store::ContextStore;
@@ -314,10 +315,6 @@ impl MessageEditor {
self.editor.read(cx).text(cx).trim().is_empty()
}
pub fn is_editor_fully_empty(&self, cx: &App) -> bool {
self.editor.read(cx).is_empty(cx)
}
fn send_to_model(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let Some(ConfiguredModel { model, provider }) = self
.thread
@@ -463,6 +460,10 @@ impl MessageEditor {
}
fn render_max_mode_toggle(&self, cx: &mut Context<Self>) -> Option<AnyElement> {
if !cx.has_flag::<NewBillingFeatureFlag>() {
return None;
}
let thread = self.thread.read(cx);
let model = thread.configured_model();
if !model?.model.supports_max_mode() {
@@ -637,7 +638,7 @@ impl MessageEditor {
this.h(vh(0.8, window)).justify_between()
})
.child(
v_flex()
div()
.min_h_16()
.when(is_editor_expanded, |this| this.h_full())
.child({
@@ -1069,6 +1070,10 @@ impl MessageEditor {
}
fn render_usage_callout(&self, line_height: Pixels, cx: &mut Context<Self>) -> Option<Div> {
if !cx.has_flag::<NewBillingFeatureFlag>() {
return None;
}
let is_using_zed_provider = self
.thread
.read(cx)
@@ -1130,6 +1135,10 @@ impl MessageEditor {
token_usage_ratio: TokenUsageRatio,
cx: &mut Context<Self>,
) -> Option<Div> {
if !cx.has_flag::<NewBillingFeatureFlag>() {
return None;
}
let title = if token_usage_ratio == TokenUsageRatio::Exceeded {
"Thread reached the token limit"
} else {
@@ -1260,7 +1269,7 @@ impl MessageEditor {
messages: vec![request_message],
tools: vec![],
stop: vec![],
temperature: AssistantSettings::temperature_for_model(&model.model, cx),
temperature: None,
};
Some(model.model.count_tokens(request, cx))

View File

@@ -6,7 +6,6 @@ use crate::inline_prompt_editor::{
use crate::terminal_codegen::{CLEAR_INPUT, CodegenEvent, TerminalCodegen};
use crate::thread_store::{TextThreadStore, ThreadStore};
use anyhow::{Context as _, Result};
use assistant_settings::AssistantSettings;
use client::telemetry::Telemetry;
use collections::{HashMap, VecDeque};
use editor::{MultiBuffer, actions::SelectAll};
@@ -267,12 +266,6 @@ impl TerminalInlineAssistant {
load_context(contexts, project, &assist.prompt_store, cx)
})?;
let ConfiguredModel { model, .. } = LanguageModelRegistry::read_global(cx)
.inline_assistant_model()
.context("No inline assistant model")?;
let temperature = AssistantSettings::temperature_for_model(&model, cx);
Ok(cx.background_spawn(async move {
let mut request_message = LanguageModelRequestMessage {
role: Role::User,
@@ -294,7 +287,7 @@ impl TerminalInlineAssistant {
messages: vec![request_message],
tools: Vec::new(),
stop: Vec::new(),
temperature,
temperature: None,
}
}))
}

View File

@@ -5,7 +5,7 @@ use std::sync::Arc;
use std::time::Instant;
use anyhow::{Result, anyhow};
use assistant_settings::{AssistantSettings, CompletionMode};
use assistant_settings::AssistantSettings;
use assistant_tool::{ActionLog, AnyToolCard, Tool, ToolWorkingSet};
use chrono::{DateTime, Utc};
use collections::HashMap;
@@ -37,7 +37,7 @@ use settings::Settings;
use thiserror::Error;
use util::{ResultExt as _, TryFutureExt as _, post_inc};
use uuid::Uuid;
use zed_llm_client::CompletionRequestStatus;
use zed_llm_client::{CompletionMode, CompletionRequestStatus};
use crate::ThreadStore;
use crate::context::{AgentContext, AgentContextHandle, ContextLoadResult, LoadedContext};
@@ -267,7 +267,7 @@ impl DetailedSummaryState {
}
}
#[derive(Default, Debug)]
#[derive(Default)]
pub struct TotalTokenUsage {
pub total: usize,
pub max: usize,
@@ -312,6 +312,14 @@ pub enum TokenUsageRatio {
Exceeded,
}
fn default_completion_mode(cx: &App) -> CompletionMode {
if cx.is_staff() {
CompletionMode::Max
} else {
CompletionMode::Normal
}
}
#[derive(Debug, Clone, Copy)]
pub enum QueueState {
Sending,
@@ -328,7 +336,7 @@ pub struct Thread {
detailed_summary_task: Task<Option<()>>,
detailed_summary_tx: postage::watch::Sender<DetailedSummaryState>,
detailed_summary_rx: postage::watch::Receiver<DetailedSummaryState>,
completion_mode: assistant_settings::CompletionMode,
completion_mode: CompletionMode,
messages: Vec<Message>,
next_message_id: MessageId,
last_prompt_id: PromptId,
@@ -387,7 +395,7 @@ impl Thread {
detailed_summary_task: Task::ready(None),
detailed_summary_tx,
detailed_summary_rx,
completion_mode: AssistantSettings::get_global(cx).preferred_completion_mode,
completion_mode: default_completion_mode(cx),
messages: Vec::new(),
next_message_id: MessageId(0),
last_prompt_id: PromptId::new(),
@@ -456,10 +464,6 @@ impl Thread {
.or_else(|| registry.default_model())
});
let completion_mode = serialized
.completion_mode
.unwrap_or_else(|| AssistantSettings::get_global(cx).preferred_completion_mode);
Self {
id,
updated_at: serialized.updated_at,
@@ -468,7 +472,7 @@ impl Thread {
detailed_summary_task: Task::ready(None),
detailed_summary_tx,
detailed_summary_rx,
completion_mode,
completion_mode: default_completion_mode(cx),
messages: serialized
.messages
.into_iter()
@@ -1091,7 +1095,6 @@ impl Thread {
provider: model.provider.id().0.to_string(),
model: model.model.id().0.to_string(),
}),
completion_mode: Some(this.completion_mode),
})
})
}
@@ -1145,7 +1148,7 @@ impl Thread {
messages: vec![],
tools: Vec::new(),
stop: Vec::new(),
temperature: AssistantSettings::temperature_for_model(&model, cx),
temperature: None,
};
let available_tools = self.available_tools(cx, model.clone());
@@ -1243,20 +1246,15 @@ impl Thread {
request.tools = available_tools;
request.mode = if model.supports_max_mode() {
Some(self.completion_mode.into())
Some(self.completion_mode)
} else {
Some(CompletionMode::Normal.into())
Some(CompletionMode::Normal)
};
request
}
fn to_summarize_request(
&self,
model: &Arc<dyn LanguageModel>,
added_user_message: String,
cx: &App,
) -> LanguageModelRequest {
fn to_summarize_request(&self, added_user_message: String) -> LanguageModelRequest {
let mut request = LanguageModelRequest {
thread_id: None,
prompt_id: None,
@@ -1264,7 +1262,7 @@ impl Thread {
messages: vec![],
tools: Vec::new(),
stop: Vec::new(),
temperature: AssistantSettings::temperature_for_model(model, cx),
temperature: None,
};
for message in &self.messages {
@@ -1701,7 +1699,7 @@ impl Thread {
If the conversation is about a specific subject, include it in the title. \
Be descriptive. DO NOT speak in the first person.";
let request = self.to_summarize_request(&model.model, added_user_message.into(), cx);
let request = self.to_summarize_request(added_user_message.into());
self.pending_summary = cx.spawn(async move |this, cx| {
async move {
@@ -1787,7 +1785,7 @@ impl Thread {
4. Any action items or next steps if any\n\
Format it in Markdown with headings and bullet points.";
let request = self.to_summarize_request(&model, added_user_message.into(), cx);
let request = self.to_summarize_request(added_user_message.into());
*self.detailed_summary_tx.borrow_mut() = DetailedSummaryState::Generating {
message_id: last_message_id,
@@ -2309,7 +2307,7 @@ impl Thread {
.map(|repo| {
repo.update(cx, |repo, _| {
let current_branch =
repo.branch.as_ref().map(|branch| branch.name.to_string());
repo.branch.as_ref().map(|branch| branch.name().to_owned());
repo.send_job(None, |state, _| async move {
let RepositoryState::Local { backend, .. } = state else {
return GitState {
@@ -2660,7 +2658,7 @@ struct PendingCompletion {
mod tests {
use super::*;
use crate::{ThreadStore, context::load_context, context_store::ContextStore, thread_store};
use assistant_settings::{AssistantSettings, LanguageModelParameters};
use assistant_settings::AssistantSettings;
use assistant_tool::ToolRegistry;
use editor::EditorSettings;
use gpui::TestAppContext;
@@ -3071,100 +3069,6 @@ fn main() {{
);
}
#[gpui::test]
async fn test_temperature_setting(cx: &mut TestAppContext) {
init_test_settings(cx);
let project = create_test_project(
cx,
json!({"code.rs": "fn main() {\n println!(\"Hello, world!\");\n}"}),
)
.await;
let (_workspace, _thread_store, thread, _context_store, model) =
setup_test_environment(cx, project.clone()).await;
// Both model and provider
cx.update(|cx| {
AssistantSettings::override_global(
AssistantSettings {
model_parameters: vec![LanguageModelParameters {
provider: Some(model.provider_id().0.to_string().into()),
model: Some(model.id().0.clone()),
temperature: Some(0.66),
}],
..AssistantSettings::get_global(cx).clone()
},
cx,
);
});
let request = thread.update(cx, |thread, cx| {
thread.to_completion_request(model.clone(), cx)
});
assert_eq!(request.temperature, Some(0.66));
// Only model
cx.update(|cx| {
AssistantSettings::override_global(
AssistantSettings {
model_parameters: vec![LanguageModelParameters {
provider: None,
model: Some(model.id().0.clone()),
temperature: Some(0.66),
}],
..AssistantSettings::get_global(cx).clone()
},
cx,
);
});
let request = thread.update(cx, |thread, cx| {
thread.to_completion_request(model.clone(), cx)
});
assert_eq!(request.temperature, Some(0.66));
// Only provider
cx.update(|cx| {
AssistantSettings::override_global(
AssistantSettings {
model_parameters: vec![LanguageModelParameters {
provider: Some(model.provider_id().0.to_string().into()),
model: None,
temperature: Some(0.66),
}],
..AssistantSettings::get_global(cx).clone()
},
cx,
);
});
let request = thread.update(cx, |thread, cx| {
thread.to_completion_request(model.clone(), cx)
});
assert_eq!(request.temperature, Some(0.66));
// Same model name, different provider
cx.update(|cx| {
AssistantSettings::override_global(
AssistantSettings {
model_parameters: vec![LanguageModelParameters {
provider: Some("anthropic".into()),
model: Some(model.id().0.clone()),
temperature: Some(0.66),
}],
..AssistantSettings::get_global(cx).clone()
},
cx,
);
});
let request = thread.update(cx, |thread, cx| {
thread.to_completion_request(model.clone(), cx)
});
assert_eq!(request.temperature, None);
}
fn init_test_settings(cx: &mut TestAppContext) {
cx.update(|cx| {
let settings_store = SettingsStore::test(cx);

View File

@@ -5,7 +5,7 @@ use std::rc::Rc;
use std::sync::Arc;
use anyhow::{Context as _, Result, anyhow};
use assistant_settings::{AgentProfile, AgentProfileId, AssistantSettings, CompletionMode};
use assistant_settings::{AgentProfile, AgentProfileId, AssistantSettings};
use assistant_tool::{ToolId, ToolSource, ToolWorkingSet};
use chrono::{DateTime, Utc};
use collections::HashMap;
@@ -651,8 +651,6 @@ pub struct SerializedThread {
pub exceeded_window_error: Option<ExceededWindowError>,
#[serde(default)]
pub model: Option<SerializedLanguageModel>,
#[serde(default)]
pub completion_mode: Option<CompletionMode>,
}
#[derive(Serialize, Deserialize, Debug)]
@@ -796,7 +794,6 @@ impl LegacySerializedThread {
detailed_summary_state: DetailedSummaryState::default(),
exceeded_window_error: None,
model: None,
completion_mode: None,
}
}
}

View File

@@ -2,7 +2,6 @@ mod agent_notification;
mod animated_label;
mod context_pill;
mod max_mode_tooltip;
mod onboarding_modal;
pub mod preview;
mod upsell;
@@ -10,4 +9,3 @@ pub use agent_notification::*;
pub use animated_label::*;
pub use context_pill::*;
pub use max_mode_tooltip::*;
pub use onboarding_modal::*;

View File

@@ -1,157 +0,0 @@
use gpui::{
ClickEvent, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, MouseDownEvent, Render,
};
use ui::{TintColor, Vector, VectorName, prelude::*};
use workspace::{ModalView, Workspace};
use crate::assistant_panel::AssistantPanel;
macro_rules! agent_onboarding_event {
($name:expr) => {
telemetry::event!($name, source = "Agent Onboarding");
};
($name:expr, $($key:ident $(= $value:expr)?),+ $(,)?) => {
telemetry::event!($name, source = "Agent Onboarding", $($key $(= $value)?),+);
};
}
pub struct AgentOnboardingModal {
focus_handle: FocusHandle,
workspace: Entity<Workspace>,
}
impl AgentOnboardingModal {
pub fn toggle(workspace: &mut Workspace, window: &mut Window, cx: &mut Context<Workspace>) {
let workspace_entity = cx.entity();
workspace.toggle_modal(window, cx, |_window, cx| Self {
workspace: workspace_entity,
focus_handle: cx.focus_handle(),
});
}
fn open_panel(&mut self, _: &ClickEvent, window: &mut Window, cx: &mut Context<Self>) {
self.workspace.update(cx, |workspace, cx| {
workspace.focus_panel::<AssistantPanel>(window, cx);
});
cx.emit(DismissEvent);
agent_onboarding_event!("Open Panel Clicked");
}
fn view_blog(&mut self, _: &ClickEvent, _: &mut Window, cx: &mut Context<Self>) {
cx.open_url("http://zed.dev/blog/fastest-ai-code-editor");
cx.notify();
agent_onboarding_event!("Blog Link Clicked");
}
fn cancel(&mut self, _: &menu::Cancel, _: &mut Window, cx: &mut Context<Self>) {
cx.emit(DismissEvent);
}
}
impl EventEmitter<DismissEvent> for AgentOnboardingModal {}
impl Focusable for AgentOnboardingModal {
fn focus_handle(&self, _cx: &App) -> FocusHandle {
self.focus_handle.clone()
}
}
impl ModalView for AgentOnboardingModal {}
impl Render for AgentOnboardingModal {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let window_height = window.viewport_size().height;
let max_height = window_height - px(200.);
let base = v_flex()
.id("agent-onboarding")
.key_context("AgentOnboardingModal")
.relative()
.w(px(450.))
.h_full()
.max_h(max_height)
.p_4()
.gap_2()
.elevation_3(cx)
.track_focus(&self.focus_handle(cx))
.overflow_hidden()
.on_action(cx.listener(Self::cancel))
.on_action(cx.listener(|_, _: &menu::Cancel, _window, cx| {
agent_onboarding_event!("Canceled", trigger = "Action");
cx.emit(DismissEvent);
}))
.on_any_mouse_down(cx.listener(|this, _: &MouseDownEvent, window, _cx| {
this.focus_handle.focus(window);
}))
.child(
div()
.absolute()
.top_0()
.right(px(-1.0))
.w(px(441.))
.h(px(167.))
.child(
Vector::new(VectorName::Grid, rems_from_px(441.), rems_from_px(167.))
.color(ui::Color::Custom(cx.theme().colors().text.alpha(0.1))),
),
)
.child(
div()
.absolute()
.top(px(-8.0))
.right_0()
.w(px(400.))
.h(px(92.))
.child(
Vector::new(VectorName::AiGrid, rems_from_px(400.), rems_from_px(92.))
.color(ui::Color::Custom(cx.theme().colors().text.alpha(0.32))),
),
)
.child(
v_flex()
.w_full()
.gap_1()
.child(
Label::new("Introducing")
.size(LabelSize::Small)
.color(Color::Muted),
)
.child(Headline::new("Agentic Editing in Zed").size(HeadlineSize::Large)),
)
.child(h_flex().absolute().top_2().right_2().child(
IconButton::new("cancel", IconName::X).on_click(cx.listener(
|_, _: &ClickEvent, _window, cx| {
agent_onboarding_event!("Cancelled", trigger = "X click");
cx.emit(DismissEvent);
},
)),
));
let open_panel_button = Button::new("open-panel", "Get Started with the Agent Panel")
.icon_size(IconSize::Indicator)
.style(ButtonStyle::Tinted(TintColor::Accent))
.full_width()
.on_click(cx.listener(Self::open_panel));
let blog_post_button = Button::new("view-blog", "Check out the Blog Post")
.icon(IconName::ArrowUpRight)
.icon_size(IconSize::Indicator)
.icon_color(Color::Muted)
.full_width()
.on_click(cx.listener(Self::view_blog));
let copy = "Zed now natively supports agentic editing, enabling fluid collaboration between humans and AI.";
base.child(Label::new(copy).color(Color::Muted)).child(
v_flex()
.w_full()
.mt_2()
.gap_2()
.child(open_panel_button)
.child(blog_post_button),
)
}
}

View File

@@ -163,7 +163,7 @@ fn update_active_language_model_from_settings(cx: &mut App) {
fn to_selected_model(selection: &LanguageModelSelection) -> language_model::SelectedModel {
language_model::SelectedModel {
provider: LanguageModelProviderId::from(selection.provider.0.clone()),
provider: LanguageModelProviderId::from(selection.provider.clone()),
model: LanguageModelId::from(selection.model.clone()),
}
}

View File

@@ -17,7 +17,9 @@ use editor::{
ToDisplayPoint,
},
};
use feature_flags::{FeatureFlagAppExt as _, ZedProFeatureFlag};
use feature_flags::{
Assistant2FeatureFlag, FeatureFlagAppExt as _, FeatureFlagViewExt as _, ZedProFeatureFlag,
};
use fs::Fs;
use futures::{
SinkExt, Stream, StreamExt, TryStreamExt as _,
@@ -72,19 +74,25 @@ pub fn init(
cx: &mut App,
) {
cx.set_global(InlineAssistant::new(fs, prompt_builder, telemetry));
// Don't register now that the Agent is released.
if false {
cx.observe_new(|_, window, cx| {
let Some(window) = window else {
return;
};
let workspace = cx.entity().clone();
InlineAssistant::update_global(cx, |inline_assistant, cx| {
inline_assistant.register_workspace(&workspace, window, cx)
});
cx.observe_new(|_, window, cx| {
let Some(window) = window else {
return;
};
let workspace = cx.entity().clone();
InlineAssistant::update_global(cx, |inline_assistant, cx| {
inline_assistant.register_workspace(&workspace, window, cx)
});
cx.observe_flag::<Assistant2FeatureFlag, _>(window, {
|is_assistant2_enabled, _workspace, _window, cx| {
InlineAssistant::update_global(cx, |inline_assistant, _cx| {
inline_assistant.is_assistant2_enabled = is_assistant2_enabled;
});
}
})
.detach();
}
})
.detach();
}
const PROMPT_HISTORY_MAX_LEN: usize = 20;
@@ -100,6 +108,7 @@ pub struct InlineAssistant {
prompt_builder: Arc<PromptBuilder>,
telemetry: Arc<Telemetry>,
fs: Arc<dyn Fs>,
is_assistant2_enabled: bool,
}
impl Global for InlineAssistant {}
@@ -121,6 +130,7 @@ impl InlineAssistant {
prompt_builder,
telemetry,
fs,
is_assistant2_enabled: false,
}
}
@@ -189,7 +199,7 @@ impl InlineAssistant {
window: &mut Window,
cx: &mut App,
) {
let is_assistant2_enabled = true;
let is_assistant2_enabled = self.is_assistant2_enabled;
if let Some(editor) = item.act_as::<Editor>(cx) {
editor.update(cx, |editor, cx| {
@@ -2474,7 +2484,7 @@ impl InlineAssist {
.read(cx)
.active_context(cx)?
.read(cx)
.to_completion_request(None, RequestType::Chat, cx),
.to_completion_request(RequestType::Chat, cx),
)
} else {
None
@@ -2860,8 +2870,7 @@ impl CodegenAlternative {
if let Some(ConfiguredModel { model, .. }) =
LanguageModelRegistry::read_global(cx).inline_assistant_model()
{
let request =
self.build_request(&model, user_prompt, assistant_panel_context.clone(), cx);
let request = self.build_request(user_prompt, assistant_panel_context.clone(), cx);
match request {
Ok(request) => {
let total_count = model.count_tokens(request.clone(), cx);
@@ -2906,8 +2915,7 @@ impl CodegenAlternative {
if user_prompt.trim().to_lowercase() == "delete" {
async { Ok(LanguageModelTextStream::default()) }.boxed_local()
} else {
let request =
self.build_request(&model, user_prompt, assistant_panel_context, cx)?;
let request = self.build_request(user_prompt, assistant_panel_context, cx)?;
self.request = Some(request.clone());
cx.spawn(async move |_, cx| model.stream_completion_text(request, &cx).await)
@@ -2919,7 +2927,6 @@ impl CodegenAlternative {
fn build_request(
&self,
model: &Arc<dyn LanguageModel>,
user_prompt: String,
assistant_panel_context: Option<LanguageModelRequest>,
cx: &App,
@@ -2974,7 +2981,7 @@ impl CodegenAlternative {
messages,
tools: Vec::new(),
stop: Vec::new(),
temperature: AssistantSettings::temperature_for_model(&model, cx),
temperature: None,
})
}

View File

@@ -261,7 +261,7 @@ impl TerminalInlineAssistant {
.read(cx)
.active_context(cx)?
.read(cx)
.to_completion_request(None, RequestType::Chat, cx),
.to_completion_request(RequestType::Chat, cx),
)
})
} else {

View File

@@ -22,6 +22,7 @@ clock.workspace = true
collections.workspace = true
context_server.workspace = true
editor.workspace = true
feature_flags.workspace = true
fs.workspace = true
futures.workspace = true
fuzzy.workspace = true

View File

@@ -3,7 +3,6 @@ mod context_tests;
use crate::patch::{AssistantEdit, AssistantPatch, AssistantPatchStatus};
use anyhow::{Context as _, Result, anyhow};
use assistant_settings::AssistantSettings;
use assistant_slash_command::{
SlashCommandContent, SlashCommandEvent, SlashCommandLine, SlashCommandOutputSection,
SlashCommandResult, SlashCommandWorkingSet,
@@ -1274,10 +1273,10 @@ impl AssistantContext {
pub(crate) fn count_remaining_tokens(&mut self, cx: &mut Context<Self>) {
// Assume it will be a Chat request, even though that takes fewer tokens (and risks going over the limit),
// because otherwise you see in the UI that your empty message has a bunch of tokens already used.
let request = self.to_completion_request(RequestType::Chat, cx);
let Some(model) = LanguageModelRegistry::read_global(cx).default_model() else {
return;
};
let request = self.to_completion_request(Some(&model.model), RequestType::Chat, cx);
let debounce = self.token_count.is_some();
self.pending_token_count = cx.spawn(async move |this, cx| {
async move {
@@ -1423,7 +1422,7 @@ impl AssistantContext {
}
let request = {
let mut req = self.to_completion_request(Some(&model), RequestType::Chat, cx);
let mut req = self.to_completion_request(RequestType::Chat, cx);
// Skip the last message because it's likely to change and
// therefore would be a waste to cache.
req.messages.pop();
@@ -2322,7 +2321,7 @@ impl AssistantContext {
// Compute which messages to cache, including the last one.
self.mark_cache_anchors(&model.cache_configuration(), false, cx);
let request = self.to_completion_request(Some(&model), request_type, cx);
let request = self.to_completion_request(request_type, cx);
let assistant_message = self
.insert_message_after(last_message_id, Role::Assistant, MessageStatus::Pending, cx)
@@ -2562,7 +2561,6 @@ impl AssistantContext {
pub fn to_completion_request(
&self,
model: Option<&Arc<dyn LanguageModel>>,
request_type: RequestType,
cx: &App,
) -> LanguageModelRequest {
@@ -2586,8 +2584,7 @@ impl AssistantContext {
messages: Vec::new(),
tools: Vec::new(),
stop: Vec::new(),
temperature: model
.and_then(|model| AssistantSettings::temperature_for_model(model, cx)),
temperature: None,
};
for message in self.messages(cx) {
if message.status != MessageStatus::Done {
@@ -2984,7 +2981,7 @@ impl AssistantContext {
return;
}
let mut request = self.to_completion_request(Some(&model.model), RequestType::Chat, cx);
let mut request = self.to_completion_request(RequestType::Chat, cx);
request.messages.push(LanguageModelRequestMessage {
role: Role::User,
content: vec![

View File

@@ -43,8 +43,9 @@ use workspace::Workspace;
#[gpui::test]
fn test_inserting_and_removing_messages(cx: &mut App) {
init_test(cx);
let settings_store = SettingsStore::test(cx);
LanguageModelRegistry::test(cx);
cx.set_global(settings_store);
let registry = Arc::new(LanguageRegistry::test(cx.background_executor().clone()));
let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
let context = cx.new(|cx| {
@@ -181,8 +182,9 @@ fn test_inserting_and_removing_messages(cx: &mut App) {
#[gpui::test]
fn test_message_splitting(cx: &mut App) {
init_test(cx);
let settings_store = SettingsStore::test(cx);
cx.set_global(settings_store);
LanguageModelRegistry::test(cx);
let registry = Arc::new(LanguageRegistry::test(cx.background_executor().clone()));
let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
@@ -283,8 +285,9 @@ fn test_message_splitting(cx: &mut App) {
#[gpui::test]
fn test_messages_for_offsets(cx: &mut App) {
init_test(cx);
let settings_store = SettingsStore::test(cx);
LanguageModelRegistry::test(cx);
cx.set_global(settings_store);
let registry = Arc::new(LanguageRegistry::test(cx.background_executor().clone()));
let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
let context = cx.new(|cx| {
@@ -375,8 +378,10 @@ fn test_messages_for_offsets(cx: &mut App) {
#[gpui::test]
async fn test_slash_commands(cx: &mut TestAppContext) {
cx.update(init_test);
let settings_store = cx.update(SettingsStore::test);
cx.set_global(settings_store);
cx.update(LanguageModelRegistry::test);
cx.update(Project::init_settings);
let fs = FakeFs::new(cx.background_executor.clone());
fs.insert_tree(
@@ -666,19 +671,22 @@ async fn test_slash_commands(cx: &mut TestAppContext) {
#[gpui::test]
async fn test_workflow_step_parsing(cx: &mut TestAppContext) {
cx.update(prompt_store::init);
let mut settings_store = cx.update(SettingsStore::test);
cx.update(|cx| {
init_test(cx);
cx.update_global(|settings_store: &mut SettingsStore, cx| {
settings_store
.set_user_settings(
r#"{ "assistant": { "enable_experimental_live_diffs": true } }"#,
cx,
)
.unwrap()
})
settings_store
.set_user_settings(
r#"{ "assistant": { "enable_experimental_live_diffs": true } }"#,
cx,
)
.unwrap()
});
cx.set_global(settings_store);
cx.update(language::init);
cx.update(Project::init_settings);
let fs = FakeFs::new(cx.executor());
let project = Project::test(fs, [Path::new("/root")], cx).await;
cx.update(LanguageModelRegistry::test);
let registry = Arc::new(LanguageRegistry::test(cx.executor()));
@@ -1061,8 +1069,9 @@ async fn test_workflow_step_parsing(cx: &mut TestAppContext) {
#[gpui::test]
async fn test_serialization(cx: &mut TestAppContext) {
cx.update(init_test);
let settings_store = cx.update(SettingsStore::test);
cx.set_global(settings_store);
cx.update(LanguageModelRegistry::test);
let registry = Arc::new(LanguageRegistry::test(cx.executor()));
let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
let context = cx.new(|cx| {
@@ -1138,8 +1147,6 @@ async fn test_serialization(cx: &mut TestAppContext) {
#[gpui::test(iterations = 100)]
async fn test_random_context_collaboration(cx: &mut TestAppContext, mut rng: StdRng) {
cx.update(init_test);
let min_peers = env::var("MIN_PEERS")
.map(|i| i.parse().expect("invalid `MIN_PEERS` variable"))
.unwrap_or(2);
@@ -1150,6 +1157,10 @@ async fn test_random_context_collaboration(cx: &mut TestAppContext, mut rng: Std
.map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
.unwrap_or(50);
let settings_store = cx.update(SettingsStore::test);
cx.set_global(settings_store);
cx.update(LanguageModelRegistry::test);
let slash_commands = cx.update(SlashCommandRegistry::default_global);
slash_commands.register_command(FakeSlashCommand("cmd-1".into()), false);
slash_commands.register_command(FakeSlashCommand("cmd-2".into()), false);
@@ -1418,8 +1429,9 @@ async fn test_random_context_collaboration(cx: &mut TestAppContext, mut rng: Std
#[gpui::test]
fn test_mark_cache_anchors(cx: &mut App) {
init_test(cx);
let settings_store = SettingsStore::test(cx);
LanguageModelRegistry::test(cx);
cx.set_global(settings_store);
let registry = Arc::new(LanguageRegistry::test(cx.background_executor().clone()));
let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
let context = cx.new(|cx| {
@@ -1594,16 +1606,6 @@ fn messages_cache(
.collect()
}
fn init_test(cx: &mut App) {
let settings_store = SettingsStore::test(cx);
prompt_store::init(cx);
LanguageModelRegistry::test(cx);
cx.set_global(settings_store);
language::init(cx);
assistant_settings::init(cx);
Project::init_settings(cx);
}
#[derive(Clone)]
struct FakeSlashCommand(String);

View File

@@ -18,6 +18,7 @@ use editor::{
scroll::Autoscroll,
};
use editor::{FoldPlaceholder, display_map::CreaseId};
use feature_flags::{Assistant2FeatureFlag, FeatureFlagAppExt as _};
use fs::Fs;
use futures::FutureExt;
use gpui::{
@@ -2394,11 +2395,19 @@ impl ContextEditor {
.on_click({
let focus_handle = self.focus_handle(cx).clone();
move |_event, window, cx| {
focus_handle.dispatch_action(
&zed_actions::agent::OpenConfiguration,
window,
cx,
);
if cx.has_flag::<Assistant2FeatureFlag>() {
focus_handle.dispatch_action(
&zed_actions::agent::OpenConfiguration,
window,
cx,
);
} else {
focus_handle.dispatch_action(
&zed_actions::assistant::ShowConfiguration,
window,
cx,
);
};
}
}),
)

View File

@@ -14,7 +14,7 @@ path = "src/assistant_settings.rs"
[dependencies]
anthropic = { workspace = true, features = ["schemars"] }
anyhow.workspace = true
collections.workspace = true
feature_flags.workspace = true
gpui.workspace = true
indexmap.workspace = true
language_model.workspace = true
@@ -27,7 +27,6 @@ schemars.workspace = true
serde.workspace = true
settings.workspace = true
workspace-hack.workspace = true
zed_llm_client.workspace = true
[dev-dependencies]
fs.workspace = true

View File

@@ -1,7 +1,7 @@
use std::sync::Arc;
use collections::IndexMap;
use gpui::SharedString;
use indexmap::IndexMap;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};

View File

@@ -5,9 +5,10 @@ use std::sync::Arc;
use ::open_ai::Model as OpenAiModel;
use anthropic::Model as AnthropicModel;
use anyhow::{Result, bail};
use collections::IndexMap;
use deepseek::Model as DeepseekModel;
use gpui::{App, Pixels, SharedString};
use feature_flags::{AgentStreamEditsFeatureFlag, Assistant2FeatureFlag, FeatureFlagAppExt};
use gpui::{App, Pixels};
use indexmap::IndexMap;
use language_model::{CloudModel, LanguageModel};
use lmstudio::Model as LmStudioModel;
use ollama::Model as OllamaModel;
@@ -17,10 +18,6 @@ use settings::{Settings, SettingsSources};
pub use crate::agent_profile::*;
pub fn init(cx: &mut App) {
AssistantSettings::register(cx);
}
#[derive(Copy, Clone, Default, Debug, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum AssistantDockPosition {
@@ -92,71 +89,31 @@ pub struct AssistantSettings {
pub notify_when_agent_waiting: NotifyWhenAgentWaiting,
pub stream_edits: bool,
pub single_file_review: bool,
pub model_parameters: Vec<LanguageModelParameters>,
pub preferred_completion_mode: CompletionMode,
}
impl AssistantSettings {
pub fn temperature_for_model(model: &Arc<dyn LanguageModel>, cx: &App) -> Option<f32> {
let settings = Self::get_global(cx);
settings
.model_parameters
.iter()
.rfind(|setting| setting.matches(model))
.and_then(|m| m.temperature)
pub fn stream_edits(&self, cx: &App) -> bool {
cx.has_flag::<AgentStreamEditsFeatureFlag>() || self.stream_edits
}
pub fn stream_edits(&self, _cx: &App) -> bool {
// TODO: Remove the `stream_edits` setting.
true
}
pub fn are_live_diffs_enabled(&self, cx: &App) -> bool {
if cx.has_flag::<Assistant2FeatureFlag>() {
return false;
}
pub fn are_live_diffs_enabled(&self, _cx: &App) -> bool {
false
cx.is_staff() || self.enable_experimental_live_diffs
}
pub fn set_inline_assistant_model(&mut self, provider: String, model: String) {
self.inline_assistant_model = Some(LanguageModelSelection {
provider: provider.into(),
model,
});
self.inline_assistant_model = Some(LanguageModelSelection { provider, model });
}
pub fn set_commit_message_model(&mut self, provider: String, model: String) {
self.commit_message_model = Some(LanguageModelSelection {
provider: provider.into(),
model,
});
self.commit_message_model = Some(LanguageModelSelection { provider, model });
}
pub fn set_thread_summary_model(&mut self, provider: String, model: String) {
self.thread_summary_model = Some(LanguageModelSelection {
provider: provider.into(),
model,
});
}
}
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
pub struct LanguageModelParameters {
pub provider: Option<LanguageModelProviderSetting>,
pub model: Option<SharedString>,
pub temperature: Option<f32>,
}
impl LanguageModelParameters {
pub fn matches(&self, model: &Arc<dyn LanguageModel>) -> bool {
if let Some(provider) = &self.provider {
if provider.0 != model.provider_id().0 {
return false;
}
}
if let Some(setting_model) = &self.model {
if *setting_model != model.id().0 {
return false;
}
}
true
self.thread_summary_model = Some(LanguageModelSelection { provider, model });
}
}
@@ -223,37 +180,37 @@ impl AssistantSettingsContent {
.and_then(|provider| match provider {
AssistantProviderContentV1::ZedDotDev { default_model } => {
default_model.map(|model| LanguageModelSelection {
provider: "zed.dev".into(),
provider: "zed.dev".to_string(),
model: model.id().to_string(),
})
}
AssistantProviderContentV1::OpenAi { default_model, .. } => {
default_model.map(|model| LanguageModelSelection {
provider: "openai".into(),
provider: "openai".to_string(),
model: model.id().to_string(),
})
}
AssistantProviderContentV1::Anthropic { default_model, .. } => {
default_model.map(|model| LanguageModelSelection {
provider: "anthropic".into(),
provider: "anthropic".to_string(),
model: model.id().to_string(),
})
}
AssistantProviderContentV1::Ollama { default_model, .. } => {
default_model.map(|model| LanguageModelSelection {
provider: "ollama".into(),
provider: "ollama".to_string(),
model: model.id().to_string(),
})
}
AssistantProviderContentV1::LmStudio { default_model, .. } => {
default_model.map(|model| LanguageModelSelection {
provider: "lmstudio".into(),
provider: "lmstudio".to_string(),
model: model.id().to_string(),
})
}
AssistantProviderContentV1::DeepSeek { default_model, .. } => {
default_model.map(|model| LanguageModelSelection {
provider: "deepseek".into(),
provider: "deepseek".to_string(),
model: model.id().to_string(),
})
}
@@ -269,8 +226,6 @@ impl AssistantSettingsContent {
notify_when_agent_waiting: None,
stream_edits: None,
single_file_review: None,
model_parameters: Vec::new(),
preferred_completion_mode: None,
},
VersionedAssistantSettingsContent::V2(ref settings) => settings.clone(),
},
@@ -281,7 +236,7 @@ impl AssistantSettingsContent {
default_width: settings.default_width,
default_height: settings.default_height,
default_model: Some(LanguageModelSelection {
provider: "openai".into(),
provider: "openai".to_string(),
model: settings
.default_open_ai_model
.clone()
@@ -300,8 +255,6 @@ impl AssistantSettingsContent {
notify_when_agent_waiting: None,
stream_edits: None,
single_file_review: None,
model_parameters: Vec::new(),
preferred_completion_mode: None,
},
None => AssistantSettingsContentV2::default(),
}
@@ -414,10 +367,7 @@ impl AssistantSettingsContent {
}
}
VersionedAssistantSettingsContent::V2(ref mut settings) => {
settings.default_model = Some(LanguageModelSelection {
provider: provider.into(),
model,
});
settings.default_model = Some(LanguageModelSelection { provider, model });
}
},
Some(AssistantSettingsContentInner::Legacy(settings)) => {
@@ -428,10 +378,7 @@ impl AssistantSettingsContent {
None => {
self.inner = Some(AssistantSettingsContentInner::for_v2(
AssistantSettingsContentV2 {
default_model: Some(LanguageModelSelection {
provider: provider.into(),
model,
}),
default_model: Some(LanguageModelSelection { provider, model }),
..Default::default()
},
));
@@ -441,10 +388,7 @@ impl AssistantSettingsContent {
pub fn set_inline_assistant_model(&mut self, provider: String, model: String) {
self.v2_setting(|setting| {
setting.inline_assistant_model = Some(LanguageModelSelection {
provider: provider.into(),
model,
});
setting.inline_assistant_model = Some(LanguageModelSelection { provider, model });
Ok(())
})
.ok();
@@ -452,10 +396,7 @@ impl AssistantSettingsContent {
pub fn set_commit_message_model(&mut self, provider: String, model: String) {
self.v2_setting(|setting| {
setting.commit_message_model = Some(LanguageModelSelection {
provider: provider.into(),
model,
});
setting.commit_message_model = Some(LanguageModelSelection { provider, model });
Ok(())
})
.ok();
@@ -483,10 +424,7 @@ impl AssistantSettingsContent {
pub fn set_thread_summary_model(&mut self, provider: String, model: String) {
self.v2_setting(|setting| {
setting.thread_summary_model = Some(LanguageModelSelection {
provider: provider.into(),
model,
});
setting.thread_summary_model = Some(LanguageModelSelection { provider, model });
Ok(())
})
.ok();
@@ -582,8 +520,6 @@ impl Default for VersionedAssistantSettingsContent {
notify_when_agent_waiting: None,
stream_edits: None,
single_file_review: None,
model_parameters: Vec::new(),
preferred_completion_mode: None,
})
}
}
@@ -647,88 +583,37 @@ pub struct AssistantSettingsContentV2 {
///
/// Default: true
single_file_review: Option<bool>,
/// Additional parameters for language model requests. When making a request
/// to a model, parameters will be taken from the last entry in this list
/// that matches the model's provider and name. In each entry, both provider
/// and model are optional, so that you can specify parameters for either
/// one.
///
/// Default: []
#[serde(default)]
model_parameters: Vec<LanguageModelParameters>,
/// What completion mode to enable for new threads
///
/// Default: normal
preferred_completion_mode: Option<CompletionMode>,
}
#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Default)]
#[serde(rename_all = "snake_case")]
pub enum CompletionMode {
#[default]
Normal,
Max,
}
impl From<CompletionMode> for zed_llm_client::CompletionMode {
fn from(value: CompletionMode) -> Self {
match value {
CompletionMode::Normal => zed_llm_client::CompletionMode::Normal,
CompletionMode::Max => zed_llm_client::CompletionMode::Max,
}
}
}
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
pub struct LanguageModelSelection {
pub provider: LanguageModelProviderSetting,
#[schemars(schema_with = "providers_schema")]
pub provider: String,
pub model: String,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
pub struct LanguageModelProviderSetting(pub String);
impl JsonSchema for LanguageModelProviderSetting {
fn schema_name() -> String {
"LanguageModelProviderSetting".into()
}
fn json_schema(_: &mut schemars::r#gen::SchemaGenerator) -> Schema {
schemars::schema::SchemaObject {
enum_values: Some(vec![
"anthropic".into(),
"bedrock".into(),
"google".into(),
"lmstudio".into(),
"ollama".into(),
"openai".into(),
"zed.dev".into(),
"copilot_chat".into(),
"deepseek".into(),
]),
..Default::default()
}
.into()
}
}
impl From<String> for LanguageModelProviderSetting {
fn from(provider: String) -> Self {
Self(provider)
}
}
impl From<&str> for LanguageModelProviderSetting {
fn from(provider: &str) -> Self {
Self(provider.to_string())
fn providers_schema(_: &mut schemars::r#gen::SchemaGenerator) -> schemars::schema::Schema {
schemars::schema::SchemaObject {
enum_values: Some(vec![
"anthropic".into(),
"bedrock".into(),
"google".into(),
"lmstudio".into(),
"ollama".into(),
"openai".into(),
"zed.dev".into(),
"copilot_chat".into(),
"deepseek".into(),
]),
..Default::default()
}
.into()
}
impl Default for LanguageModelSelection {
fn default() -> Self {
Self {
provider: LanguageModelProviderSetting("openai".to_string()),
provider: "openai".to_string(),
model: "gpt-4".to_string(),
}
}
@@ -865,14 +750,6 @@ impl Settings for AssistantSettings {
merge(&mut settings.stream_edits, value.stream_edits);
merge(&mut settings.single_file_review, value.single_file_review);
merge(&mut settings.default_profile, value.default_profile);
merge(
&mut settings.preferred_completion_mode,
value.preferred_completion_mode,
);
settings
.model_parameters
.extend_from_slice(&value.model_parameters);
if let Some(profiles) = value.profiles {
settings
@@ -1006,8 +883,6 @@ mod tests {
notify_when_agent_waiting: None,
stream_edits: None,
single_file_review: None,
model_parameters: Vec::new(),
preferred_completion_mode: None,
},
)),
}
@@ -1070,7 +945,7 @@ mod tests {
AssistantSettingsContentV2 {
enabled: Some(false),
default_model: Some(LanguageModelSelection {
provider: "xai".to_owned().into(),
provider: "xai".to_owned(),
model: "grok".to_owned(),
}),
..Default::default()

View File

@@ -27,6 +27,7 @@ use std::sync::Arc;
use assistant_settings::AssistantSettings;
use assistant_tool::ToolRegistry;
use copy_path_tool::CopyPathTool;
use feature_flags::{AgentStreamEditsFeatureFlag, FeatureFlagAppExt};
use gpui::{App, Entity};
use http_client::HttpClientWithUrl;
use language_model::LanguageModelRegistry;
@@ -76,6 +77,8 @@ pub fn init(http_client: Arc<HttpClientWithUrl>, cx: &mut App) {
registry.register_tool(FetchTool::new(http_client));
register_edit_file_tool(cx);
cx.observe_flag::<AgentStreamEditsFeatureFlag, _>(|_, cx| register_edit_file_tool(cx))
.detach();
cx.observe_global::<SettingsStore>(register_edit_file_tool)
.detach();

View File

@@ -536,6 +536,7 @@ impl EditAgent {
let request = LanguageModelRequest {
messages,
// temperature: Some(0.5),
..Default::default()
};
Ok(self.model.stream_completion_text(request, cx).await?.stream)

View File

@@ -1033,7 +1033,7 @@ impl EvalAssertion {
fn eval(iterations: usize, expected_pass_ratio: f32, mut eval: EvalInput) {
let mut evaluated_count = 0;
report_progress(evaluated_count, iterations);
report_progress(evaluated_count, 0, iterations);
let (tx, rx) = mpsc::channel();
@@ -1075,7 +1075,7 @@ fn eval(iterations: usize, expected_pass_ratio: f32, mut eval: EvalInput) {
}
evaluated_count += 1;
report_progress(evaluated_count, iterations);
report_progress(evaluated_count, failed_count, iterations);
}
let actual_pass_ratio = (iterations - failed_count) as f32 / iterations as f32;
@@ -1146,8 +1146,13 @@ impl Display for EvalOutput {
}
}
fn report_progress(evaluated_count: usize, iterations: usize) {
print!("\r\x1b[KEvaluated {}/{}", evaluated_count, iterations);
fn report_progress(evaluated_count: usize, failed_count: usize, iterations: usize) {
let failing_rate = failed_count as f32 / evaluated_count as f32;
let passing_rate = ((1. - failing_rate) * 100.).round() as usize;
print!(
"\r\x1b[KEvaluated {}/{} ({}% passing)",
evaluated_count, iterations, passing_rate
);
std::io::stdout().flush().unwrap();
}

View File

@@ -581,18 +581,18 @@ impl ToolCard for EditFileToolCard {
(IconName::ChevronDown, "Expand Code Block")
};
let gradient_overlay =
div()
.absolute()
.bottom_0()
.left_0()
.w_full()
.h_2_5()
.bg(gpui::linear_gradient(
0.,
gpui::linear_color_stop(cx.theme().colors().editor_background, 0.),
gpui::linear_color_stop(cx.theme().colors().editor_background.opacity(0.), 1.),
));
let gradient_overlay = div()
.absolute()
.bottom_0()
.left_0()
.w_full()
.h_2_5()
.rounded_b_lg()
.bg(gpui::linear_gradient(
0.,
gpui::linear_color_stop(cx.theme().colors().editor_background, 0.),
gpui::linear_color_stop(cx.theme().colors().editor_background.opacity(0.), 1.),
));
let border_color = cx.theme().colors().border.opacity(0.6);
@@ -610,9 +610,8 @@ impl ToolCard for EditFileToolCard {
let mut container = v_flex()
.p_3()
.gap_1()
.gap_1p5()
.border_t_1()
.rounded_md()
.border_color(border_color)
.bg(cx.theme().colors().editor_background);
@@ -627,7 +626,7 @@ impl ToolCard for EditFileToolCard {
_ => div().w_1_2(),
}
.id("loading_div")
.h_1()
.h_2()
.rounded_full()
.bg(cx.theme().colors().element_active)
.with_animation(
@@ -649,7 +648,7 @@ impl ToolCard for EditFileToolCard {
.border_1()
.when(failed, |card| card.border_dashed())
.border_color(border_color)
.rounded_md()
.rounded_lg()
.overflow_hidden()
.child(codeblock_header)
.when(failed && self.error_expanded, |card| {
@@ -703,8 +702,8 @@ impl ToolCard for EditFileToolCard {
|editor_container| editor_container.child(gradient_overlay),
),
)
.when(is_collapsible, |card| {
card.child(
.when(is_collapsible, |editor_container| {
editor_container.child(
h_flex()
.id(("expand-button", self.editor_unique_id))
.flex_none()
@@ -712,7 +711,6 @@ impl ToolCard for EditFileToolCard {
.h_5()
.justify_center()
.border_t_1()
.rounded_b_md()
.border_color(border_color)
.bg(cx.theme().colors().editor_background)
.hover(|style| {

View File

@@ -47,6 +47,7 @@ use std::{
};
use telemetry::Telemetry;
use thiserror::Error;
use tokio::net::TcpStream;
use url::Url;
use util::{ResultExt, TryFutureExt};
@@ -1127,7 +1128,10 @@ impl Client {
let stream = {
let handle = cx.update(|cx| gpui_tokio::Tokio::handle(cx)).ok().unwrap();
let _guard = handle.enter();
connect_socks_proxy_stream(proxy.as_ref(), rpc_host).await?
match proxy {
Some(proxy) => connect_socks_proxy_stream(&proxy, rpc_host).await?,
None => Box::new(TcpStream::connect(rpc_host).await?),
}
};
log::info!("connected to rpc endpoint {}", rpc_url);

View File

@@ -1,61 +1,98 @@
//! socks proxy
use anyhow::{Result, anyhow};
use anyhow::{Context, Result, anyhow};
use http_client::Url;
use tokio_socks::tcp::{Socks4Stream, Socks5Stream};
pub(crate) async fn connect_socks_proxy_stream(
proxy: Option<&Url>,
rpc_host: (&str, u16),
) -> Result<Box<dyn AsyncReadWrite>> {
let stream = match parse_socks_proxy(proxy) {
Some((socks_proxy, SocksVersion::V4)) => {
let stream = Socks4Stream::connect_with_socket(
tokio::net::TcpStream::connect(socks_proxy).await?,
rpc_host,
)
.await
.map_err(|err| anyhow!("error connecting to socks {}", err))?;
Box::new(stream) as Box<dyn AsyncReadWrite>
}
Some((socks_proxy, SocksVersion::V5)) => Box::new(
Socks5Stream::connect_with_socket(
tokio::net::TcpStream::connect(socks_proxy).await?,
rpc_host,
)
.await
.map_err(|err| anyhow!("error connecting to socks {}", err))?,
) as Box<dyn AsyncReadWrite>,
None => {
Box::new(tokio::net::TcpStream::connect(rpc_host).await?) as Box<dyn AsyncReadWrite>
}
};
Ok(stream)
/// Identification to a Socks V4 Proxy
struct Socks4Identification<'a> {
user_id: &'a str,
}
fn parse_socks_proxy(proxy: Option<&Url>) -> Option<((String, u16), SocksVersion)> {
let proxy_url = proxy?;
let scheme = proxy_url.scheme();
/// Authorization to a Socks V5 Proxy
struct Socks5Authorization<'a> {
username: &'a str,
password: &'a str,
}
/// Socks Proxy Protocol Version
///
/// V4 allows idenfication using a user_id
/// V5 allows authorization using a username and password
enum SocksVersion<'a> {
V4(Option<Socks4Identification<'a>>),
V5(Option<Socks5Authorization<'a>>),
}
pub(crate) async fn connect_socks_proxy_stream(
proxy: &Url,
rpc_host: (&str, u16),
) -> Result<Box<dyn AsyncReadWrite>> {
let Some((socks_proxy, version)) = parse_socks_proxy(proxy) else {
// If parsing the proxy URL fails, we must avoid falling back to an insecure connection.
// SOCKS proxies are often used in contexts where security and privacy are critical,
// so any fallback could expose users to significant risks.
return Err(anyhow!("Parsing proxy url failed"));
};
// Connect to proxy and wrap protocol later
let stream = tokio::net::TcpStream::connect(socks_proxy)
.await
.context("Failed to connect to socks proxy")?;
let socks: Box<dyn AsyncReadWrite> = match version {
SocksVersion::V4(None) => {
let socks = Socks4Stream::connect_with_socket(stream, rpc_host)
.await
.context("error connecting to socks")?;
Box::new(socks)
}
SocksVersion::V4(Some(Socks4Identification { user_id })) => {
let socks = Socks4Stream::connect_with_userid_and_socket(stream, rpc_host, user_id)
.await
.context("error connecting to socks")?;
Box::new(socks)
}
SocksVersion::V5(None) => {
let socks = Socks5Stream::connect_with_socket(stream, rpc_host)
.await
.context("error connecting to socks")?;
Box::new(socks)
}
SocksVersion::V5(Some(Socks5Authorization { username, password })) => {
let socks = Socks5Stream::connect_with_password_and_socket(
stream, rpc_host, username, password,
)
.await
.context("error connecting to socks")?;
Box::new(socks)
}
};
Ok(socks)
}
fn parse_socks_proxy(proxy: &Url) -> Option<((String, u16), SocksVersion<'_>)> {
let scheme = proxy.scheme();
let socks_version = if scheme.starts_with("socks4") {
// socks4
SocksVersion::V4
let identification = match proxy.username() {
"" => None,
username => Some(Socks4Identification { user_id: username }),
};
SocksVersion::V4(identification)
} else if scheme.starts_with("socks") {
// socks, socks5
SocksVersion::V5
let authorization = proxy.password().map(|password| Socks5Authorization {
username: proxy.username(),
password,
});
SocksVersion::V5(authorization)
} else {
return None;
};
if let Some((host, port)) = proxy_url.host().zip(proxy_url.port_or_known_default()) {
Some(((host.to_string(), port), socks_version))
} else {
None
}
}
// private helper structs and traits
let host = proxy.host()?.to_string();
let port = proxy.port_or_known_default()?;
enum SocksVersion {
V4,
V5,
Some(((host, port), socks_version))
}
pub(crate) trait AsyncReadWrite:
@@ -66,3 +103,74 @@ impl<T: tokio::io::AsyncRead + tokio::io::AsyncWrite + Unpin + Send + 'static> A
for T
{
}
#[cfg(test)]
mod tests {
use url::Url;
use super::*;
#[test]
fn parse_socks4() {
let proxy = Url::parse("socks4://proxy.example.com:1080").unwrap();
let ((host, port), version) = parse_socks_proxy(&proxy).unwrap();
assert_eq!(host, "proxy.example.com");
assert_eq!(port, 1080);
assert!(matches!(version, SocksVersion::V4(None)))
}
#[test]
fn parse_socks4_with_identification() {
let proxy = Url::parse("socks4://userid@proxy.example.com:1080").unwrap();
let ((host, port), version) = parse_socks_proxy(&proxy).unwrap();
assert_eq!(host, "proxy.example.com");
assert_eq!(port, 1080);
assert!(matches!(
version,
SocksVersion::V4(Some(Socks4Identification { user_id: "userid" }))
))
}
#[test]
fn parse_socks5() {
let proxy = Url::parse("socks5://proxy.example.com:1080").unwrap();
let ((host, port), version) = parse_socks_proxy(&proxy).unwrap();
assert_eq!(host, "proxy.example.com");
assert_eq!(port, 1080);
assert!(matches!(version, SocksVersion::V5(None)))
}
#[test]
fn parse_socks5_with_authorization() {
let proxy = Url::parse("socks5://username:password@proxy.example.com:1080").unwrap();
let ((host, port), version) = parse_socks_proxy(&proxy).unwrap();
assert_eq!(host, "proxy.example.com");
assert_eq!(port, 1080);
assert!(matches!(
version,
SocksVersion::V5(Some(Socks5Authorization {
username: "username",
password: "password"
}))
))
}
/// If parsing the proxy URL fails, we must avoid falling back to an insecure connection.
/// SOCKS proxies are often used in contexts where security and privacy are critical,
/// so any fallback could expose users to significant risks.
#[tokio::test]
async fn fails_on_bad_proxy() {
// Should fail connecting because http is not a valid Socks proxy scheme
let proxy = Url::parse("http://localhost:2313").unwrap();
let result = connect_socks_proxy_stream(&proxy, ("test", 1080)).await;
match result {
Err(e) => assert_eq!(e.to_string(), "Parsing proxy url failed"),
Ok(_) => panic!("Connecting on bad proxy should fail"),
};
}
}

View File

@@ -78,7 +78,6 @@ zed_llm_client.workspace = true
[dev-dependencies]
assistant = { workspace = true, features = ["test-support"] }
assistant_context_editor.workspace = true
assistant_settings.workspace = true
assistant_slash_command.workspace = true
assistant_tool.workspace = true
async-trait.workspace = true

View File

@@ -0,0 +1,23 @@
create table subscription_usages_v2 (
id uuid primary key,
user_id integer not null,
period_start_at timestamp without time zone not null,
period_end_at timestamp without time zone not null,
plan text not null,
model_requests int not null default 0,
edit_predictions int not null default 0
);
create unique index uix_subscription_usages_v2_on_user_id_start_at_end_at on subscription_usages_v2 (user_id, period_start_at, period_end_at);
create index ix_subscription_usages_v2_on_plan on subscription_usages_v2 (plan);
create table subscription_usage_meters_v2 (
id uuid primary key,
subscription_usage_id uuid not null references subscription_usages_v2 (id) on delete cascade,
model_id integer not null references models (id) on delete cascade,
mode text not null,
requests integer not null default 0
);
create unique index uix_subscription_usage_meters_v2_on_usage_model_mode on subscription_usage_meters_v2 (subscription_usage_id, model_id, mode);

View File

@@ -0,0 +1,2 @@
drop table subscription_usage_meters;
drop table subscription_usages;

View File

@@ -27,7 +27,9 @@ use crate::db::billing_subscription::{
StripeCancellationReason, StripeSubscriptionStatus, SubscriptionKind,
};
use crate::llm::db::subscription_usage_meter::CompletionMode;
use crate::llm::{DEFAULT_MAX_MONTHLY_SPEND, FREE_TIER_MONTHLY_SPENDING_LIMIT};
use crate::llm::{
AGENT_EXTENDED_TRIAL_FEATURE_FLAG, DEFAULT_MAX_MONTHLY_SPEND, FREE_TIER_MONTHLY_SPENDING_LIMIT,
};
use crate::rpc::{ResultExt as _, Server};
use crate::{AppState, Cents, Error, Result};
use crate::{db::UserId, llm::db::LlmDatabase};
@@ -54,6 +56,10 @@ pub fn router() -> Router {
"/billing/subscriptions/manage",
post(manage_billing_subscription),
)
.route(
"/billing/subscriptions/migrate",
post(migrate_to_new_billing),
)
.route("/billing/monthly_spend", get(get_monthly_spend))
.route("/billing/usage", get(get_current_usage))
}
@@ -256,6 +262,7 @@ async fn list_billing_subscriptions(
enum ProductCode {
ZedPro,
ZedProTrial,
ZedFree,
}
#[derive(Debug, Deserialize)]
@@ -362,12 +369,7 @@ async fn create_billing_subscription(
let checkout_session_url = match body.product {
Some(ProductCode::ZedPro) => {
stripe_billing
.checkout_with_price(
app.config.zed_pro_price_id()?,
customer_id,
&user.github_login,
&success_url,
)
.checkout_with_zed_pro(customer_id, &user.github_login, &success_url)
.await?
}
Some(ProductCode::ZedProTrial) => {
@@ -384,7 +386,6 @@ async fn create_billing_subscription(
stripe_billing
.checkout_with_zed_pro_trial(
app.config.zed_pro_price_id()?,
customer_id,
&user.github_login,
feature_flags,
@@ -392,6 +393,11 @@ async fn create_billing_subscription(
)
.await?
}
Some(ProductCode::ZedFree) => {
stripe_billing
.checkout_with_zed_free(customer_id, &user.github_login, &success_url)
.await?
}
None => {
let default_model = llm_db.model(
zed_llm_client::LanguageModelProvider::Anthropic,
@@ -458,6 +464,14 @@ async fn manage_billing_subscription(
))?
};
let Some(stripe_billing) = app.stripe_billing.clone() else {
log::error!("failed to retrieve Stripe billing object");
Err(Error::http(
StatusCode::NOT_IMPLEMENTED,
"not supported".into(),
))?
};
let customer = app
.db
.get_billing_customer_by_user_id(user.id)
@@ -508,8 +522,8 @@ async fn manage_billing_subscription(
let flow = match body.intent {
ManageSubscriptionIntent::ManageSubscription => None,
ManageSubscriptionIntent::UpgradeToPro => {
let zed_pro_price_id = app.config.zed_pro_price_id()?;
let zed_free_price_id = app.config.zed_free_price_id()?;
let zed_pro_price_id = stripe_billing.zed_pro_price_id().await?;
let zed_free_price_id = stripe_billing.zed_free_price_id().await?;
let stripe_subscription =
Subscription::retrieve(&stripe_client, &subscription_id, &[]).await?;
@@ -602,6 +616,86 @@ async fn manage_billing_subscription(
}))
}
#[derive(Debug, Deserialize)]
struct MigrateToNewBillingBody {
github_user_id: i32,
}
#[derive(Debug, Serialize)]
struct MigrateToNewBillingResponse {
/// The ID of the subscription that was canceled.
canceled_subscription_id: Option<String>,
}
async fn migrate_to_new_billing(
Extension(app): Extension<Arc<AppState>>,
extract::Json(body): extract::Json<MigrateToNewBillingBody>,
) -> Result<Json<MigrateToNewBillingResponse>> {
let Some(stripe_client) = app.stripe_client.clone() else {
log::error!("failed to retrieve Stripe client");
Err(Error::http(
StatusCode::NOT_IMPLEMENTED,
"not supported".into(),
))?
};
let user = app
.db
.get_user_by_github_user_id(body.github_user_id)
.await?
.ok_or_else(|| anyhow!("user not found"))?;
let old_billing_subscriptions_by_user = app
.db
.get_active_billing_subscriptions(HashSet::from_iter([user.id]))
.await?;
let canceled_subscription_id = if let Some((_billing_customer, billing_subscription)) =
old_billing_subscriptions_by_user.get(&user.id)
{
let stripe_subscription_id = billing_subscription
.stripe_subscription_id
.parse::<stripe::SubscriptionId>()
.context("failed to parse Stripe subscription ID from database")?;
Subscription::cancel(
&stripe_client,
&stripe_subscription_id,
stripe::CancelSubscription {
invoice_now: Some(true),
..Default::default()
},
)
.await?;
Some(stripe_subscription_id)
} else {
None
};
let all_feature_flags = app.db.list_feature_flags().await?;
let user_feature_flags = app.db.get_user_flags(user.id).await?;
for feature_flag in ["new-billing", "assistant2"] {
let already_in_feature_flag = user_feature_flags.iter().any(|flag| flag == feature_flag);
if already_in_feature_flag {
continue;
}
let feature_flag = all_feature_flags
.iter()
.find(|flag| flag.flag == feature_flag)
.context("failed to find feature flag: {feature_flag:?}")?;
app.db.add_user_flag(user.id, feature_flag.id).await?;
}
Ok(Json(MigrateToNewBillingResponse {
canceled_subscription_id: canceled_subscription_id
.map(|subscription_id| subscription_id.to_string()),
}))
}
/// The amount of time we wait in between each poll of Stripe events.
///
/// This value should strike a balance between:
@@ -856,9 +950,11 @@ async fn handle_customer_subscription_event(
log::info!("handling Stripe {} event: {}", event.type_, event.id);
let subscription_kind = maybe!({
let zed_pro_price_id = app.config.zed_pro_price_id().ok()?;
let zed_free_price_id = app.config.zed_free_price_id().ok()?;
let subscription_kind = maybe!(async {
let stripe_billing = app.stripe_billing.clone()?;
let zed_pro_price_id = stripe_billing.zed_pro_price_id().await.ok()?;
let zed_free_price_id = stripe_billing.zed_free_price_id().await.ok()?;
subscription.items.data.iter().find_map(|item| {
let price = item.price.as_ref()?;
@@ -875,7 +971,8 @@ async fn handle_customer_subscription_event(
None
}
})
});
})
.await;
let billing_customer =
find_or_create_billing_customer(app, stripe_client, subscription.customer)
@@ -943,6 +1040,7 @@ async fn handle_customer_subscription_event(
billing_customer.user_id,
&existing_subscription,
subscription_kind,
subscription.status.into(),
new_period_start_at,
new_period_end_at,
)
@@ -1091,11 +1189,25 @@ struct UsageCounts {
}
#[derive(Debug, Serialize)]
struct GetCurrentUsageResponse {
struct ModelRequestUsage {
pub model: String,
pub mode: CompletionMode,
pub requests: i32,
}
#[derive(Debug, Serialize)]
struct CurrentUsage {
pub model_requests: UsageCounts,
pub model_request_usage: Vec<ModelRequestUsage>,
pub edit_predictions: UsageCounts,
}
#[derive(Debug, Default, Serialize)]
struct GetCurrentUsageResponse {
pub plan: String,
pub current_usage: Option<CurrentUsage>,
}
async fn get_current_usage(
Extension(app): Extension<Arc<AppState>>,
Query(params): Query<GetCurrentUsageParams>,
@@ -1106,6 +1218,11 @@ async fn get_current_usage(
.await?
.ok_or_else(|| anyhow!("user not found"))?;
let feature_flags = app.db.get_user_flags(user.id).await?;
let has_extended_trial = feature_flags
.iter()
.any(|flag| flag == AGENT_EXTENDED_TRIAL_FEATURE_FLAG);
let Some(llm_db) = app.llm_db.clone() else {
return Err(Error::http(
StatusCode::NOT_IMPLEMENTED,
@@ -1113,21 +1230,8 @@ async fn get_current_usage(
));
};
let empty_usage = GetCurrentUsageResponse {
model_requests: UsageCounts {
used: 0,
limit: Some(0),
remaining: Some(0),
},
edit_predictions: UsageCounts {
used: 0,
limit: Some(0),
remaining: Some(0),
},
};
let Some(subscription) = app.db.get_active_billing_subscription(user.id).await? else {
return Ok(Json(empty_usage));
return Ok(Json(GetCurrentUsageResponse::default()));
};
let subscription_period = maybe!({
@@ -1138,42 +1242,93 @@ async fn get_current_usage(
});
let Some((period_start_at, period_end_at)) = subscription_period else {
return Ok(Json(empty_usage));
return Ok(Json(GetCurrentUsageResponse::default()));
};
let usage = llm_db
.get_subscription_usage_for_period(user.id, period_start_at, period_end_at)
.await?;
let Some(usage) = usage else {
return Ok(Json(empty_usage));
};
let plan = match usage.plan {
SubscriptionKind::ZedPro => zed_llm_client::Plan::ZedPro,
SubscriptionKind::ZedProTrial => zed_llm_client::Plan::ZedProTrial,
SubscriptionKind::ZedFree => zed_llm_client::Plan::Free,
};
let plan = usage
.as_ref()
.map(|usage| usage.plan.into())
.unwrap_or_else(|| {
subscription
.kind
.map(Into::into)
.unwrap_or(zed_llm_client::Plan::Free)
});
let model_requests_limit = match plan.model_requests_limit() {
zed_llm_client::UsageLimit::Limited(limit) => Some(limit),
zed_llm_client::UsageLimit::Limited(limit) => {
let limit = if plan == zed_llm_client::Plan::ZedProTrial && has_extended_trial {
1_000
} else {
limit
};
Some(limit)
}
zed_llm_client::UsageLimit::Unlimited => None,
};
let edit_prediction_limit = match plan.edit_predictions_limit() {
let edit_predictions_limit = match plan.edit_predictions_limit() {
zed_llm_client::UsageLimit::Limited(limit) => Some(limit),
zed_llm_client::UsageLimit::Unlimited => None,
};
let Some(usage) = usage else {
return Ok(Json(GetCurrentUsageResponse {
plan: plan.as_str().to_string(),
current_usage: Some(CurrentUsage {
model_requests: UsageCounts {
used: 0,
limit: model_requests_limit,
remaining: model_requests_limit,
},
model_request_usage: Vec::new(),
edit_predictions: UsageCounts {
used: 0,
limit: edit_predictions_limit,
remaining: edit_predictions_limit,
},
}),
}));
};
let subscription_usage_meters = llm_db
.get_current_subscription_usage_meters_for_user(user.id, Utc::now())
.await?;
let model_request_usage = subscription_usage_meters
.into_iter()
.filter_map(|(usage_meter, _usage)| {
let model = llm_db.model_by_id(usage_meter.model_id).ok()?;
Some(ModelRequestUsage {
model: model.name.clone(),
mode: usage_meter.mode,
requests: usage_meter.requests,
})
})
.collect::<Vec<_>>();
Ok(Json(GetCurrentUsageResponse {
model_requests: UsageCounts {
used: usage.model_requests,
limit: model_requests_limit,
remaining: model_requests_limit.map(|limit| (limit - usage.model_requests).max(0)),
},
edit_predictions: UsageCounts {
used: usage.edit_predictions,
limit: edit_prediction_limit,
remaining: edit_prediction_limit.map(|limit| (limit - usage.edit_predictions).max(0)),
},
plan: plan.as_str().to_string(),
current_usage: Some(CurrentUsage {
model_requests: UsageCounts {
used: usage.model_requests,
limit: model_requests_limit,
remaining: model_requests_limit.map(|limit| (limit - usage.model_requests).max(0)),
},
model_request_usage,
edit_predictions: UsageCounts {
used: usage.edit_predictions,
limit: edit_predictions_limit,
remaining: edit_predictions_limit
.map(|limit| (limit - usage.edit_predictions).max(0)),
},
}),
}))
}
@@ -1358,9 +1513,19 @@ async fn sync_model_request_usage_with_stripe(
llm_db: &Arc<LlmDatabase>,
stripe_billing: &Arc<StripeBilling>,
) -> anyhow::Result<()> {
let staff_users = app.db.get_staff_users().await?;
let staff_user_ids = staff_users
.iter()
.map(|user| user.id)
.collect::<HashSet<UserId>>();
let usage_meters = llm_db
.get_current_subscription_usage_meters(Utc::now())
.await?;
let usage_meters = usage_meters
.into_iter()
.filter(|(_, usage)| !staff_user_ids.contains(&usage.user_id))
.collect::<Vec<_>>();
let user_ids = usage_meters
.iter()
.map(|(_, usage)| usage.user_id)
@@ -1402,12 +1567,12 @@ async fn sync_model_request_usage_with_stripe(
let model = llm_db.model_by_id(usage_meter.model_id)?;
let (price_id, meter_event_name) = match model.name.as_str() {
"claude-3-5-sonnet" => (&claude_3_5_sonnet.id, "claude_3_5_sonnet/requests"),
let (price, meter_event_name) = match model.name.as_str() {
"claude-3-5-sonnet" => (&claude_3_5_sonnet, "claude_3_5_sonnet/requests"),
"claude-3-7-sonnet" => match usage_meter.mode {
CompletionMode::Normal => (&claude_3_7_sonnet.id, "claude_3_7_sonnet/requests"),
CompletionMode::Normal => (&claude_3_7_sonnet, "claude_3_7_sonnet/requests"),
CompletionMode::Max => {
(&claude_3_7_sonnet_max.id, "claude_3_7_sonnet/requests/max")
(&claude_3_7_sonnet_max, "claude_3_7_sonnet/requests/max")
}
},
model_name => {
@@ -1416,7 +1581,7 @@ async fn sync_model_request_usage_with_stripe(
};
stripe_billing
.subscribe_to_price(&stripe_subscription_id, price_id)
.subscribe_to_price(&stripe_subscription_id, price)
.await?;
stripe_billing
.bill_model_request_usage(

View File

@@ -65,6 +65,18 @@ impl Database {
.await
}
/// Returns all users flagged as staff.
pub async fn get_staff_users(&self) -> Result<Vec<user::Model>> {
self.transaction(|tx| async {
let tx = tx;
Ok(user::Entity::find()
.filter(user::Column::Admin.eq(true))
.all(&*tx)
.await?)
})
.await
}
/// Returns a user by email address. There are no access checks here, so this should only be used internally.
pub async fn get_user_by_email(&self, email: &str) -> Result<Option<User>> {
self.transaction(|tx| async move {

View File

@@ -1,4 +1,5 @@
use crate::db::{BillingCustomerId, BillingSubscriptionId};
use chrono::{Datelike as _, NaiveDate, Utc};
use sea_orm::entity::prelude::*;
use serde::Serialize;
@@ -29,6 +30,38 @@ impl Model {
let period_end = self.stripe_current_period_end?;
chrono::DateTime::from_timestamp(period_end, 0)
}
pub fn current_period(
subscription: Option<Self>,
is_staff: bool,
) -> Option<(DateTimeUtc, DateTimeUtc)> {
if is_staff {
let now = Utc::now();
let year = now.year();
let month = now.month();
let first_day_of_this_month =
NaiveDate::from_ymd_opt(year, month, 1)?.and_hms_opt(0, 0, 0)?;
let next_month = if month == 12 { 1 } else { month + 1 };
let next_month_year = if month == 12 { year + 1 } else { year };
let first_day_of_next_month =
NaiveDate::from_ymd_opt(next_month_year, next_month, 1)?.and_hms_opt(23, 59, 59)?;
let last_day_of_this_month = first_day_of_next_month - chrono::Days::new(1);
Some((
first_day_of_this_month.and_utc(),
last_day_of_this_month.and_utc(),
))
} else {
let subscription = subscription?;
let period_start_at = subscription.current_period_start_at()?;
let period_end_at = subscription.current_period_end_at()?;
Some((period_start_at, period_end_at))
}
}
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
@@ -61,6 +94,16 @@ pub enum SubscriptionKind {
ZedFree,
}
impl From<SubscriptionKind> for zed_llm_client::Plan {
fn from(value: SubscriptionKind) -> Self {
match value {
SubscriptionKind::ZedPro => Self::ZedPro,
SubscriptionKind::ZedProTrial => Self::ZedProTrial,
SubscriptionKind::ZedFree => Self::Free,
}
}
}
/// The status of a Stripe subscription.
///
/// [Stripe docs](https://docs.stripe.com/api/subscriptions/object#subscription_object-status)

View File

@@ -180,9 +180,6 @@ pub struct Config {
pub slack_panics_webhook: Option<String>,
pub auto_join_channel_id: Option<ChannelId>,
pub stripe_api_key: Option<String>,
pub stripe_zed_pro_price_id: Option<String>,
pub stripe_zed_pro_trial_price_id: Option<String>,
pub stripe_zed_free_price_id: Option<String>,
pub supermaven_admin_api_key: Option<Arc<str>>,
pub user_backfiller_github_access_token: Option<Arc<str>>,
}
@@ -201,22 +198,6 @@ impl Config {
}
}
pub fn zed_pro_price_id(&self) -> anyhow::Result<stripe::PriceId> {
Self::parse_stripe_price_id("Zed Pro", self.stripe_zed_pro_price_id.as_deref())
}
pub fn zed_free_price_id(&self) -> anyhow::Result<stripe::PriceId> {
Self::parse_stripe_price_id("Zed Free", self.stripe_zed_pro_price_id.as_deref())
}
fn parse_stripe_price_id(name: &str, value: Option<&str>) -> anyhow::Result<stripe::PriceId> {
use std::str::FromStr as _;
let price_id = value.ok_or_else(|| anyhow!("{name} price ID not set"))?;
Ok(stripe::PriceId::from_str(price_id)?)
}
#[cfg(test)]
pub fn test() -> Self {
Self {
@@ -254,9 +235,6 @@ impl Config {
migrations_path: None,
seed_path: None,
stripe_api_key: None,
stripe_zed_pro_price_id: None,
stripe_zed_pro_trial_price_id: None,
stripe_zed_free_price_id: None,
supermaven_admin_api_key: None,
user_backfiller_github_access_token: None,
kinesis_region: None,

View File

@@ -1,3 +1,4 @@
use crate::db::UserId;
use crate::llm::db::queries::subscription_usages::convert_chrono_to_time;
use super::*;
@@ -34,4 +35,38 @@ impl LlmDatabase {
})
.await
}
/// Returns all current subscription usage meters for the given user as of the given timestamp.
pub async fn get_current_subscription_usage_meters_for_user(
&self,
user_id: UserId,
now: DateTimeUtc,
) -> Result<Vec<(subscription_usage_meter::Model, subscription_usage::Model)>> {
let now = convert_chrono_to_time(now)?;
self.transaction(|tx| async move {
let result = subscription_usage_meter::Entity::find()
.inner_join(subscription_usage::Entity)
.filter(subscription_usage::Column::UserId.eq(user_id))
.filter(
subscription_usage::Column::PeriodStartAt
.lte(now)
.and(subscription_usage::Column::PeriodEndAt.gte(now)),
)
.select_also(subscription_usage::Entity)
.all(&*tx)
.await?;
let result = result
.into_iter()
.filter_map(|(meter, usage)| {
let usage = usage?;
Some((meter, usage))
})
.collect();
Ok(result)
})
.await
}
}

View File

@@ -1,7 +1,7 @@
use chrono::Timelike;
use time::PrimitiveDateTime;
use crate::db::billing_subscription::SubscriptionKind;
use crate::db::billing_subscription::{StripeSubscriptionStatus, SubscriptionKind};
use crate::db::{UserId, billing_subscription};
use super::*;
@@ -69,7 +69,7 @@ impl LlmDatabase {
Ok(
subscription_usage::Entity::insert(subscription_usage::ActiveModel {
id: ActiveValue::not_set(),
id: ActiveValue::set(Uuid::now_v7()),
user_id: ActiveValue::set(user_id),
period_start_at: ActiveValue::set(period_start_at),
period_end_at: ActiveValue::set(period_end_at),
@@ -120,12 +120,13 @@ impl LlmDatabase {
user_id: UserId,
existing_subscription: &billing_subscription::Model,
new_subscription_kind: Option<SubscriptionKind>,
new_subscription_status: StripeSubscriptionStatus,
new_period_start_at: DateTimeUtc,
new_period_end_at: DateTimeUtc,
) -> Result<Option<subscription_usage::Model>> {
self.transaction(|tx| async move {
match existing_subscription.kind {
Some(SubscriptionKind::ZedProTrial) => {
match (existing_subscription.kind, new_subscription_status) {
(Some(SubscriptionKind::ZedProTrial), StripeSubscriptionStatus::Active) => {
let trial_period_start_at = existing_subscription
.current_period_start_at()
.ok_or_else(|| anyhow!("No trial subscription period start"))?;

View File

@@ -4,10 +4,10 @@ use sea_orm::entity::prelude::*;
use time::PrimitiveDateTime;
#[derive(Clone, Debug, PartialEq, DeriveEntityModel)]
#[sea_orm(table_name = "subscription_usages")]
#[sea_orm(table_name = "subscription_usages_v2")]
pub struct Model {
#[sea_orm(primary_key)]
pub id: i32,
pub id: Uuid,
pub user_id: UserId,
pub period_start_at: PrimitiveDateTime,
pub period_end_at: PrimitiveDateTime,

View File

@@ -4,11 +4,11 @@ use serde::Serialize;
use crate::llm::db::ModelId;
#[derive(Clone, Debug, PartialEq, DeriveEntityModel)]
#[sea_orm(table_name = "subscription_usage_meters")]
#[sea_orm(table_name = "subscription_usage_meters_v2")]
pub struct Model {
#[sea_orm(primary_key)]
pub id: i32,
pub subscription_usage_id: i32,
pub id: Uuid,
pub subscription_usage_id: Uuid,
pub model_id: ModelId,
pub mode: CompletionMode,
pub requests: i32,

View File

@@ -1,7 +1,7 @@
use chrono::{Duration, Utc};
use pretty_assertions::assert_eq;
use crate::db::billing_subscription::SubscriptionKind;
use crate::db::billing_subscription::{StripeSubscriptionStatus, SubscriptionKind};
use crate::db::{UserId, billing_subscription};
use crate::llm::db::LlmDatabase;
use crate::test_llm_db;
@@ -12,58 +12,108 @@ test_llm_db!(
);
async fn test_transfer_existing_subscription_usage(db: &mut LlmDatabase) {
let user_id = UserId(1);
// Test when an existing Zed Pro trial subscription is upgraded to Zed Pro.
{
let user_id = UserId(1);
let now = Utc::now();
let now = Utc::now();
let trial_period_start_at = now - Duration::days(14);
let trial_period_end_at = now;
let trial_period_start_at = now - Duration::days(14);
let trial_period_end_at = now;
let new_period_start_at = now;
let new_period_end_at = now + Duration::days(30);
let new_period_start_at = now;
let new_period_end_at = now + Duration::days(30);
let existing_subscription = billing_subscription::Model {
kind: Some(SubscriptionKind::ZedProTrial),
stripe_current_period_start: Some(trial_period_start_at.timestamp()),
stripe_current_period_end: Some(trial_period_end_at.timestamp()),
..Default::default()
};
let existing_subscription = billing_subscription::Model {
kind: Some(SubscriptionKind::ZedProTrial),
stripe_current_period_start: Some(trial_period_start_at.timestamp()),
stripe_current_period_end: Some(trial_period_end_at.timestamp()),
..Default::default()
};
let existing_usage = db
.create_subscription_usage(
user_id,
trial_period_start_at,
trial_period_end_at,
SubscriptionKind::ZedProTrial,
25,
1_000,
)
.await
.unwrap();
let existing_usage = db
.create_subscription_usage(
user_id,
trial_period_start_at,
trial_period_end_at,
SubscriptionKind::ZedProTrial,
25,
1_000,
)
.await
.unwrap();
let transferred_usage = db
.transfer_existing_subscription_usage(
user_id,
&existing_subscription,
Some(SubscriptionKind::ZedPro),
new_period_start_at,
new_period_end_at,
)
.await
.unwrap();
let transferred_usage = db
.transfer_existing_subscription_usage(
user_id,
&existing_subscription,
Some(SubscriptionKind::ZedPro),
StripeSubscriptionStatus::Active,
new_period_start_at,
new_period_end_at,
)
.await
.unwrap();
assert!(
transferred_usage.is_some(),
"subscription usage not transferred successfully"
);
let transferred_usage = transferred_usage.unwrap();
assert!(
transferred_usage.is_some(),
"subscription usage not transferred successfully"
);
let transferred_usage = transferred_usage.unwrap();
assert_eq!(
transferred_usage.model_requests,
existing_usage.model_requests
);
assert_eq!(
transferred_usage.edit_predictions,
existing_usage.edit_predictions
);
assert_eq!(
transferred_usage.model_requests,
existing_usage.model_requests
);
assert_eq!(
transferred_usage.edit_predictions,
existing_usage.edit_predictions
);
}
// Test when an existing Zed Pro trial subscription is canceled.
{
let user_id = UserId(2);
let now = Utc::now();
let trial_period_start_at = now - Duration::days(14);
let trial_period_end_at = now;
let existing_subscription = billing_subscription::Model {
kind: Some(SubscriptionKind::ZedProTrial),
stripe_current_period_start: Some(trial_period_start_at.timestamp()),
stripe_current_period_end: Some(trial_period_end_at.timestamp()),
..Default::default()
};
let _existing_usage = db
.create_subscription_usage(
user_id,
trial_period_start_at,
trial_period_end_at,
SubscriptionKind::ZedProTrial,
25,
1_000,
)
.await
.unwrap();
let transferred_usage = db
.transfer_existing_subscription_usage(
user_id,
&existing_subscription,
Some(SubscriptionKind::ZedPro),
StripeSubscriptionStatus::Canceled,
trial_period_start_at,
trial_period_end_at,
)
.await
.unwrap();
assert!(
transferred_usage.is_none(),
"subscription usage was transferred when it should not have been"
);
}
}

View File

@@ -11,7 +11,6 @@ use jsonwebtoken::{DecodingKey, EncodingKey, Header, Validation};
use serde::{Deserialize, Serialize};
use std::time::Duration;
use thiserror::Error;
use util::maybe;
use uuid::Uuid;
use zed_llm_client::Plan;
@@ -30,8 +29,12 @@ pub struct LlmTokenClaims {
pub has_llm_closed_beta_feature_flag: bool,
pub bypass_account_age_check: bool,
pub has_llm_subscription: bool,
#[serde(default)]
pub use_llm_request_queue: bool,
pub max_monthly_spend_in_cents: u32,
pub custom_llm_monthly_allowance_in_cents: Option<u32>,
#[serde(default)]
pub use_new_billing: bool,
pub plan: Plan,
#[serde(default)]
pub has_extended_trial: bool,
@@ -90,24 +93,28 @@ impl LlmTokenClaims {
custom_llm_monthly_allowance_in_cents: user
.custom_llm_monthly_allowance_in_cents
.map(|allowance| allowance as u32),
plan: subscription
.as_ref()
.and_then(|subscription| subscription.kind)
.map_or(Plan::Free, |kind| match kind {
SubscriptionKind::ZedFree => Plan::Free,
SubscriptionKind::ZedPro => Plan::ZedPro,
SubscriptionKind::ZedProTrial => Plan::ZedProTrial,
}),
use_new_billing: feature_flags.iter().any(|flag| flag == "new-billing"),
use_llm_request_queue: feature_flags.iter().any(|flag| flag == "llm-request-queue"),
plan: if is_staff {
Plan::ZedPro
} else {
subscription
.as_ref()
.and_then(|subscription| subscription.kind)
.map_or(Plan::Free, |kind| match kind {
SubscriptionKind::ZedFree => Plan::Free,
SubscriptionKind::ZedPro => Plan::ZedPro,
SubscriptionKind::ZedProTrial => Plan::ZedProTrial,
})
},
has_extended_trial: feature_flags
.iter()
.any(|flag| flag == AGENT_EXTENDED_TRIAL_FEATURE_FLAG),
subscription_period: maybe!({
let subscription = subscription?;
let period_start_at = subscription.current_period_start_at()?;
let period_end_at = subscription.current_period_end_at()?;
Some((period_start_at.naive_utc(), period_end_at.naive_utc()))
}),
subscription_period: billing_subscription::Model::current_period(
subscription,
is_staff,
)
.map(|(start, end)| (start.naive_utc(), end.naive_utc())),
enable_model_request_overages: billing_preferences
.as_ref()
.map_or(false, |preferences| {

View File

@@ -37,7 +37,6 @@ use core::fmt::{self, Debug, Formatter};
use reqwest_client::ReqwestClient;
use rpc::proto::split_repository_update;
use supermaven_api::{CreateExternalUserRequest, SupermavenAdminApi};
use util::maybe;
use futures::{
FutureExt, SinkExt, StreamExt, TryStreamExt, channel::oneshot, future::BoxFuture,
@@ -181,6 +180,10 @@ impl Session {
}
pub async fn current_plan(&self, db: &MutexGuard<'_, DbHandle>) -> anyhow::Result<proto::Plan> {
if self.is_staff() {
return Ok(proto::Plan::ZedPro);
}
let user_id = self.user_id();
let subscription = db.get_active_billing_subscription(user_id).await?;
@@ -328,6 +331,10 @@ impl Server {
.add_request_handler(
forward_read_only_project_request::<proto::LspExtSwitchSourceHeader>,
)
.add_request_handler(forward_read_only_project_request::<proto::LspExtGoToParentModule>)
.add_request_handler(forward_read_only_project_request::<proto::LspExtCancelFlycheck>)
.add_request_handler(forward_read_only_project_request::<proto::LspExtRunFlycheck>)
.add_request_handler(forward_read_only_project_request::<proto::LspExtClearFlycheck>)
.add_request_handler(
forward_read_only_project_request::<proto::LanguageServerIdForName>,
)
@@ -2705,13 +2712,10 @@ async fn update_user_plan(user_id: UserId, session: &Session) -> Result<()> {
let usage = if let Some(llm_db) = session.app_state.llm_db.clone() {
let subscription = db.get_active_billing_subscription(user_id).await?;
let subscription_period = maybe!({
let subscription = subscription?;
let period_start_at = subscription.current_period_start_at()?;
let period_end_at = subscription.current_period_end_at()?;
Some((period_start_at, period_end_at))
});
let subscription_period = crate::db::billing_subscription::Model::current_period(
subscription,
session.is_staff(),
);
if let Some((period_start_at, period_end_at)) = subscription_period {
llm_db
@@ -2733,8 +2737,12 @@ async fn update_user_plan(user_id: UserId, session: &Session) -> Result<()> {
trial_started_at: billing_customer
.and_then(|billing_customer| billing_customer.trial_started_at)
.map(|trial_started_at| trial_started_at.and_utc().timestamp() as u64),
is_usage_based_billing_enabled: billing_preferences
.map(|preferences| preferences.model_request_overages_enabled),
is_usage_based_billing_enabled: if session.is_staff() {
Some(true)
} else {
billing_preferences
.map(|preferences| preferences.model_request_overages_enabled)
},
usage: usage.map(|usage| {
let plan = match plan {
proto::Plan::Free => zed_llm_client::Plan::Free,

View File

@@ -81,6 +81,24 @@ impl StripeBilling {
Ok(())
}
pub async fn zed_pro_price_id(&self) -> Result<PriceId> {
self.find_price_id_by_lookup_key("zed-pro").await
}
pub async fn zed_free_price_id(&self) -> Result<PriceId> {
self.find_price_id_by_lookup_key("zed-free").await
}
pub async fn find_price_id_by_lookup_key(&self, lookup_key: &str) -> Result<PriceId> {
self.state
.read()
.await
.prices_by_lookup_key
.get(lookup_key)
.map(|price| price.id.clone())
.ok_or_else(|| crate::Error::Internal(anyhow!("no price ID found for {lookup_key:?}")))
}
pub async fn find_price_by_lookup_key(&self, lookup_key: &str) -> Result<stripe::Price> {
self.state
.read()
@@ -88,7 +106,7 @@ impl StripeBilling {
.prices_by_lookup_key
.get(lookup_key)
.cloned()
.ok_or_else(|| crate::Error::Internal(anyhow!("no price ID found for {lookup_key:?}")))
.ok_or_else(|| crate::Error::Internal(anyhow!("no price found for {lookup_key:?}")))
}
pub async fn register_model_for_token_based_usage(
@@ -230,21 +248,26 @@ impl StripeBilling {
pub async fn subscribe_to_price(
&self,
subscription_id: &stripe::SubscriptionId,
price_id: &stripe::PriceId,
price: &stripe::Price,
) -> Result<()> {
let subscription =
stripe::Subscription::retrieve(&self.client, &subscription_id, &[]).await?;
if subscription_contains_price(&subscription, price_id) {
if subscription_contains_price(&subscription, &price.id) {
return Ok(());
}
const BILLING_THRESHOLD_IN_CENTS: i64 = 20 * 100;
let price_per_unit = price.unit_amount.unwrap_or_default();
let _units_for_billing_threshold = BILLING_THRESHOLD_IN_CENTS / price_per_unit;
stripe::Subscription::update(
&self.client,
subscription_id,
stripe::UpdateSubscription {
items: Some(vec![stripe::UpdateSubscriptionItems {
price: Some(price_id.to_string()),
price: Some(price.id.to_string()),
..Default::default()
}]),
trial_settings: Some(stripe::UpdateSubscriptionTrialSettings {
@@ -463,19 +486,20 @@ impl StripeBilling {
Ok(session.url.context("no checkout session URL")?)
}
pub async fn checkout_with_price(
pub async fn checkout_with_zed_pro(
&self,
price_id: PriceId,
customer_id: stripe::CustomerId,
github_login: &str,
success_url: &str,
) -> Result<String> {
let zed_pro_price_id = self.zed_pro_price_id().await?;
let mut params = stripe::CreateCheckoutSession::new();
params.mode = Some(stripe::CheckoutSessionMode::Subscription);
params.customer = Some(customer_id);
params.client_reference_id = Some(github_login);
params.line_items = Some(vec![stripe::CreateCheckoutSessionLineItems {
price: Some(price_id.to_string()),
price: Some(zed_pro_price_id.to_string()),
quantity: Some(1),
..Default::default()
}]);
@@ -487,12 +511,13 @@ impl StripeBilling {
pub async fn checkout_with_zed_pro_trial(
&self,
zed_pro_price_id: PriceId,
customer_id: stripe::CustomerId,
github_login: &str,
feature_flags: Vec<String>,
success_url: &str,
) -> Result<String> {
let zed_pro_price_id = self.zed_pro_price_id().await?;
let eligible_for_extended_trial = feature_flags
.iter()
.any(|flag| flag == AGENT_EXTENDED_TRIAL_FEATURE_FLAG);
@@ -537,6 +562,29 @@ impl StripeBilling {
let session = stripe::CheckoutSession::create(&self.client, params).await?;
Ok(session.url.context("no checkout session URL")?)
}
pub async fn checkout_with_zed_free(
&self,
customer_id: stripe::CustomerId,
github_login: &str,
success_url: &str,
) -> Result<String> {
let zed_free_price_id = self.zed_free_price_id().await?;
let mut params = stripe::CreateCheckoutSession::new();
params.mode = Some(stripe::CheckoutSessionMode::Subscription);
params.customer = Some(customer_id);
params.client_reference_id = Some(github_login);
params.line_items = Some(vec![stripe::CreateCheckoutSessionLineItems {
price: Some(zed_free_price_id.to_string()),
quantity: Some(1),
..Default::default()
}]);
params.success_url = Some(success_url);
let session = stripe::CheckoutSession::create(&self.client, params).await?;
Ok(session.url.context("no checkout session URL")?)
}
}
#[derive(Serialize)]

View File

@@ -25,7 +25,7 @@ use language::{
use project::{
ProjectPath, SERVER_PROGRESS_THROTTLE_TIMEOUT,
lsp_store::{
lsp_ext_command::{ExpandedMacro, LspExpandMacro},
lsp_ext_command::{ExpandedMacro, LspExtExpandMacro},
rust_analyzer_ext::RUST_ANALYZER_NAME,
},
project_settings::{InlineBlameSettings, ProjectSettings},
@@ -2704,8 +2704,8 @@ async fn test_client_can_query_lsp_ext(cx_a: &mut TestAppContext, cx_b: &mut Tes
let fake_language_server = fake_language_servers.next().await.unwrap();
// host
let mut expand_request_a =
fake_language_server.set_request_handler::<LspExpandMacro, _, _>(|params, _| async move {
let mut expand_request_a = fake_language_server.set_request_handler::<LspExtExpandMacro, _, _>(
|params, _| async move {
assert_eq!(
params.text_document.uri,
lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(),
@@ -2715,7 +2715,8 @@ async fn test_client_can_query_lsp_ext(cx_a: &mut TestAppContext, cx_b: &mut Tes
name: "test_macro_name".to_string(),
expansion: "test_macro_expansion on the host".to_string(),
}))
});
},
);
editor_a.update_in(cx_a, |editor, window, cx| {
expand_macro_recursively(editor, &ExpandMacroRecursively, window, cx)
@@ -2738,8 +2739,8 @@ async fn test_client_can_query_lsp_ext(cx_a: &mut TestAppContext, cx_b: &mut Tes
});
// client
let mut expand_request_b =
fake_language_server.set_request_handler::<LspExpandMacro, _, _>(|params, _| async move {
let mut expand_request_b = fake_language_server.set_request_handler::<LspExtExpandMacro, _, _>(
|params, _| async move {
assert_eq!(
params.text_document.uri,
lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(),
@@ -2749,7 +2750,8 @@ async fn test_client_can_query_lsp_ext(cx_a: &mut TestAppContext, cx_b: &mut Tes
name: "test_macro_name".to_string(),
expansion: "test_macro_expansion on the client".to_string(),
}))
});
},
);
editor_b.update_in(cx_b, |editor, window, cx| {
expand_macro_recursively(editor, &ExpandMacroRecursively, window, cx)

View File

@@ -2902,7 +2902,7 @@ async fn test_git_branch_name(
.read(cx)
.branch
.as_ref()
.map(|branch| branch.name.to_string()),
.map(|branch| branch.name().to_owned()),
branch_name
)
}
@@ -6862,7 +6862,7 @@ async fn test_remote_git_branches(
let branches_b = branches_b
.into_iter()
.map(|branch| branch.name.to_string())
.map(|branch| branch.name().to_string())
.collect::<HashSet<_>>();
assert_eq!(branches_b, branches_set);
@@ -6893,7 +6893,7 @@ async fn test_remote_git_branches(
})
});
assert_eq!(host_branch.name, branches[2]);
assert_eq!(host_branch.name(), branches[2]);
// Also try creating a new branch
cx_b.update(|cx| {
@@ -6931,5 +6931,5 @@ async fn test_remote_git_branches(
})
});
assert_eq!(host_branch.name, "totally-new-branch");
assert_eq!(host_branch.name(), "totally-new-branch");
}

View File

@@ -293,7 +293,7 @@ async fn test_ssh_collaboration_git_branches(
let branches_b = branches_b
.into_iter()
.map(|branch| branch.name.to_string())
.map(|branch| branch.name().to_string())
.collect::<HashSet<_>>();
assert_eq!(&branches_b, &branches_set);
@@ -326,7 +326,7 @@ async fn test_ssh_collaboration_git_branches(
})
});
assert_eq!(server_branch.name, branches[2]);
assert_eq!(server_branch.name(), branches[2]);
// Also try creating a new branch
cx_b.update(|cx| {
@@ -366,7 +366,7 @@ async fn test_ssh_collaboration_git_branches(
})
});
assert_eq!(server_branch.name, "totally-new-branch");
assert_eq!(server_branch.name(), "totally-new-branch");
// Remove the git repository and check that all participants get the update.
remote_fs

View File

@@ -307,7 +307,6 @@ impl TestServer {
);
language_model::LanguageModelRegistry::test(cx);
assistant_context_editor::init(client.clone(), cx);
assistant_settings::init(cx);
});
client
@@ -555,9 +554,6 @@ impl TestServer {
migrations_path: None,
seed_path: None,
stripe_api_key: None,
stripe_zed_pro_price_id: None,
stripe_zed_pro_trial_price_id: None,
stripe_zed_free_price_id: None,
supermaven_admin_api_key: None,
user_backfiller_github_access_token: None,
kinesis_region: None,

View File

@@ -22,7 +22,9 @@ use ui::{
Avatar, Button, Icon, IconButton, IconName, Label, Tab, Tooltip, h_flex, prelude::*, v_flex,
};
use util::{ResultExt, TryFutureExt};
use workspace::notifications::{Notification as WorkspaceNotification, NotificationId};
use workspace::notifications::{
Notification as WorkspaceNotification, NotificationId, SuppressEvent,
};
use workspace::{
Workspace,
dock::{DockPosition, Panel, PanelEvent},
@@ -823,6 +825,11 @@ impl Render for NotificationToast {
IconButton::new("close", IconName::Close)
.on_click(cx.listener(|_, _, _, cx| cx.emit(DismissEvent))),
)
.child(
IconButton::new("suppress", IconName::XCircle)
.tooltip(Tooltip::text("Do not show until restart"))
.on_click(cx.listener(|_, _, _, cx| cx.emit(SuppressEvent))),
)
.on_click(cx.listener(|this, _, window, cx| {
this.focus_notification_panel(window, cx);
cx.emit(DismissEvent);
@@ -831,3 +838,4 @@ impl Render for NotificationToast {
}
impl EventEmitter<DismissEvent> for NotificationToast {}
impl EventEmitter<SuppressEvent> for NotificationToast {}

View File

@@ -16,6 +16,7 @@ collections.workspace = true
gpui.workspace = true
linkme.workspace = true
parking_lot.workspace = true
strum.workspace = true
theme.workspace = true
workspace-hack.workspace = true

View File

@@ -1,26 +1,197 @@
use std::fmt::Display;
use std::ops::{Deref, DerefMut};
//! # Component
//!
//! This module provides the Component trait, which is used to define
//! components for visual testing and debugging.
//!
//! Additionally, it includes layouts for rendering component examples
//! and example groups, as well as the distributed slice mechanism for
//! registering components.
mod component_layout;
pub use component_layout::*;
use std::sync::LazyLock;
use collections::HashMap;
use gpui::{
AnyElement, App, IntoElement, Pixels, RenderOnce, SharedString, Window, div, pattern_slash,
prelude::*, px, rems,
};
use gpui::{AnyElement, App, SharedString, Window};
use linkme::distributed_slice;
use parking_lot::RwLock;
use theme::ActiveTheme;
use strum::{Display, EnumString};
pub fn components() -> ComponentRegistry {
COMPONENT_DATA.read().clone()
}
pub fn init() {
let component_fns: Vec<_> = __ALL_COMPONENTS.iter().cloned().collect();
for f in component_fns {
f();
}
}
pub fn register_component<T: Component>() {
let id = T::id();
let metadata = ComponentMetadata {
id: id.clone(),
description: T::description().map(Into::into),
name: SharedString::new_static(T::name()),
preview: Some(T::preview),
scope: T::scope(),
sort_name: SharedString::new_static(T::sort_name()),
status: T::status(),
};
let mut data = COMPONENT_DATA.write();
data.components.insert(id, metadata);
}
#[distributed_slice]
pub static __ALL_COMPONENTS: [fn()] = [..];
pub static COMPONENT_DATA: LazyLock<RwLock<ComponentRegistry>> =
LazyLock::new(|| RwLock::new(ComponentRegistry::default()));
#[derive(Default, Clone)]
pub struct ComponentRegistry {
components: HashMap<ComponentId, ComponentMetadata>,
}
impl ComponentRegistry {
pub fn previews(&self) -> Vec<&ComponentMetadata> {
self.components
.values()
.filter(|c| c.preview.is_some())
.collect()
}
pub fn sorted_previews(&self) -> Vec<ComponentMetadata> {
let mut previews: Vec<ComponentMetadata> = self.previews().into_iter().cloned().collect();
previews.sort_by_key(|a| a.name());
previews
}
pub fn components(&self) -> Vec<&ComponentMetadata> {
self.components.values().collect()
}
pub fn sorted_components(&self) -> Vec<ComponentMetadata> {
let mut components: Vec<ComponentMetadata> =
self.components().into_iter().cloned().collect();
components.sort_by_key(|a| a.name());
components
}
pub fn component_map(&self) -> HashMap<ComponentId, ComponentMetadata> {
self.components.clone()
}
pub fn get(&self, id: &ComponentId) -> Option<&ComponentMetadata> {
self.components.get(id)
}
pub fn len(&self) -> usize {
self.components.len()
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct ComponentId(pub &'static str);
#[derive(Clone)]
pub struct ComponentMetadata {
id: ComponentId,
description: Option<SharedString>,
name: SharedString,
preview: Option<fn(&mut Window, &mut App) -> Option<AnyElement>>,
scope: ComponentScope,
sort_name: SharedString,
status: ComponentStatus,
}
impl ComponentMetadata {
pub fn id(&self) -> ComponentId {
self.id.clone()
}
pub fn description(&self) -> Option<SharedString> {
self.description.clone()
}
pub fn name(&self) -> SharedString {
self.name.clone()
}
pub fn preview(&self) -> Option<fn(&mut Window, &mut App) -> Option<AnyElement>> {
self.preview
}
pub fn scope(&self) -> ComponentScope {
self.scope.clone()
}
pub fn sort_name(&self) -> SharedString {
self.sort_name.clone()
}
pub fn scopeless_name(&self) -> SharedString {
self.name
.clone()
.split("::")
.last()
.unwrap_or(&self.name)
.to_string()
.into()
}
pub fn status(&self) -> ComponentStatus {
self.status.clone()
}
}
/// Implement this trait to define a UI component. This will allow you to
/// derive `RegisterComponent` on it, in tutn allowing you to preview the
/// contents of the preview fn in `workspace: open component preview`.
///
/// This can be useful for visual debugging and testing, documenting UI
/// patterns, or simply showing all the variants of a component.
///
/// Generally you will want to implement at least `scope` and `preview`
/// from this trait, so you can preview the component, and it will show up
/// in a section that makes sense.
pub trait Component {
/// The component's unique identifier.
///
/// Used to access previews, or state for more
/// complex, stateful components.
fn id() -> ComponentId {
ComponentId(Self::name())
}
/// Returns the scope of the component.
///
/// This scope is used to determine how components and
/// their previews are displayed and organized.
fn scope() -> ComponentScope {
ComponentScope::None
}
/// The ready status of this component.
///
/// Use this to mark when components are:
/// - `WorkInProgress`: Still being designed or are partially implemented.
/// - `EngineeringReady`: Ready to be implemented.
/// - `Deprecated`: No longer recommended for use.
///
/// Defaults to [`Live`](ComponentStatus::Live).
fn status() -> ComponentStatus {
ComponentStatus::Live
}
/// The name of the component.
///
/// This name is used to identify the component
/// and is usually derived from the component's type.
fn name() -> &'static str {
std::any::type_name::<Self>()
}
fn id() -> ComponentId {
ComponentId(Self::name())
}
/// Returns a name that the component should be sorted by.
///
/// Implement this if the component should be sorted in an alternate order than its name.
@@ -37,408 +208,107 @@ pub trait Component {
fn sort_name() -> &'static str {
Self::name()
}
/// An optional description of the component.
///
/// This will be displayed in the component's preview. To show a
/// component's doc comment as it's description, derive `Documented`.
///
/// Example:
///
/// ```
/// /// This is a doc comment.
/// #[derive(Documented)]
/// struct MyComponent;
///
/// impl MyComponent {
/// fn description() -> Option<&'static str> {
/// Some(Self::DOCS)
/// }
/// }
/// ```
///
/// This will result in "This is a doc comment." being passed
/// to the component's description.
fn description() -> Option<&'static str> {
None
}
/// The component's preview.
///
/// An element returned here will be shown in the component's preview.
///
/// Useful component helpers:
/// - [`component::single_example`]
/// - [`component::component_group`]
/// - [`component::component_group_with_title`]
///
/// Note: Any arbitrary element can be returned here.
///
/// This is useful for displaying related UI to the component you are
/// trying to preview, such as a button that opens a modal or shows a
/// tooltip on hover, or a grid of icons showcasing all the icons available.
fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
None
}
}
#[distributed_slice]
pub static __ALL_COMPONENTS: [fn()] = [..];
pub static COMPONENT_DATA: LazyLock<RwLock<ComponentRegistry>> =
LazyLock::new(|| RwLock::new(ComponentRegistry::new()));
pub struct ComponentRegistry {
components: Vec<(
ComponentScope,
// name
&'static str,
// sort name
&'static str,
// description
Option<&'static str>,
)>,
previews: HashMap<&'static str, fn(&mut Window, &mut App) -> Option<AnyElement>>,
/// The ready status of this component.
///
/// Use this to mark when components are:
/// - `WorkInProgress`: Still being designed or are partially implemented.
/// - `EngineeringReady`: Ready to be implemented.
/// - `Deprecated`: No longer recommended for use.
///
/// Defaults to [`Live`](ComponentStatus::Live).
#[derive(Debug, Clone, PartialEq, Eq, Hash, Display, EnumString)]
pub enum ComponentStatus {
#[strum(serialize = "Work In Progress")]
WorkInProgress,
#[strum(serialize = "Ready To Build")]
EngineeringReady,
Live,
Deprecated,
}
impl ComponentRegistry {
fn new() -> Self {
ComponentRegistry {
components: Vec::new(),
previews: HashMap::default(),
impl ComponentStatus {
pub fn description(&self) -> &str {
match self {
ComponentStatus::WorkInProgress => {
"These components are still being designed or refined. They shouldn't be used in the app yet."
}
ComponentStatus::EngineeringReady => {
"These components are design complete or partially implemented, and are ready for an engineer to complete their implementation."
}
ComponentStatus::Live => "These components are ready for use in the app.",
ComponentStatus::Deprecated => {
"These components are no longer recommended for use in the app, and may be removed in a future release."
}
}
}
}
pub fn init() {
let component_fns: Vec<_> = __ALL_COMPONENTS.iter().cloned().collect();
for f in component_fns {
f();
}
}
pub fn register_component<T: Component>() {
let component_data = (T::scope(), T::name(), T::sort_name(), T::description());
let mut data = COMPONENT_DATA.write();
data.components.push(component_data);
data.previews.insert(T::id().0, T::preview);
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct ComponentId(pub &'static str);
#[derive(Clone)]
pub struct ComponentMetadata {
id: ComponentId,
name: SharedString,
sort_name: SharedString,
scope: ComponentScope,
description: Option<SharedString>,
preview: Option<fn(&mut Window, &mut App) -> Option<AnyElement>>,
}
impl ComponentMetadata {
pub fn id(&self) -> ComponentId {
self.id.clone()
}
pub fn name(&self) -> SharedString {
self.name.clone()
}
pub fn sort_name(&self) -> SharedString {
self.sort_name.clone()
}
pub fn scopeless_name(&self) -> SharedString {
self.name
.clone()
.split("::")
.last()
.unwrap_or(&self.name)
.to_string()
.into()
}
pub fn scope(&self) -> ComponentScope {
self.scope.clone()
}
pub fn description(&self) -> Option<SharedString> {
self.description.clone()
}
pub fn preview(&self) -> Option<fn(&mut Window, &mut App) -> Option<AnyElement>> {
self.preview
}
}
pub struct AllComponents(pub HashMap<ComponentId, ComponentMetadata>);
impl AllComponents {
pub fn new() -> Self {
AllComponents(HashMap::default())
}
pub fn all_previews(&self) -> Vec<&ComponentMetadata> {
self.0.values().filter(|c| c.preview.is_some()).collect()
}
pub fn all_previews_sorted(&self) -> Vec<ComponentMetadata> {
let mut previews: Vec<ComponentMetadata> =
self.all_previews().into_iter().cloned().collect();
previews.sort_by_key(|a| a.name());
previews
}
pub fn all(&self) -> Vec<&ComponentMetadata> {
self.0.values().collect()
}
pub fn all_sorted(&self) -> Vec<ComponentMetadata> {
let mut components: Vec<ComponentMetadata> = self.all().into_iter().cloned().collect();
components.sort_by_key(|a| a.name());
components
}
}
impl Deref for AllComponents {
type Target = HashMap<ComponentId, ComponentMetadata>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl DerefMut for AllComponents {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
pub fn components() -> AllComponents {
let data = COMPONENT_DATA.read();
let mut all_components = AllComponents::new();
for (scope, name, sort_name, description) in &data.components {
let preview = data.previews.get(name).cloned();
let component_name = SharedString::new_static(name);
let sort_name = SharedString::new_static(sort_name);
let id = ComponentId(name);
all_components.insert(
id.clone(),
ComponentMetadata {
id,
name: component_name,
sort_name,
scope: scope.clone(),
description: description.map(Into::into),
preview,
},
);
}
all_components
}
// #[derive(Debug, Clone, PartialEq, Eq, Hash)]
// pub enum ComponentStatus {
// WorkInProgress,
// EngineeringReady,
// Live,
// Deprecated,
// }
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[derive(Debug, Clone, PartialEq, Eq, Hash, Display, EnumString)]
pub enum ComponentScope {
Agent,
Collaboration,
#[strum(serialize = "Data Display")]
DataDisplay,
Editor,
#[strum(serialize = "Images & Icons")]
Images,
#[strum(serialize = "Forms & Input")]
Input,
#[strum(serialize = "Layout & Structure")]
Layout,
#[strum(serialize = "Loading & Progress")]
Loading,
Navigation,
#[strum(serialize = "Unsorted")]
None,
Notification,
#[strum(serialize = "Overlays & Layering")]
Overlays,
Status,
Typography,
#[strum(serialize = "Version Control")]
VersionControl,
}
impl Display for ComponentScope {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ComponentScope::Agent => write!(f, "Agent"),
ComponentScope::Collaboration => write!(f, "Collaboration"),
ComponentScope::DataDisplay => write!(f, "Data Display"),
ComponentScope::Editor => write!(f, "Editor"),
ComponentScope::Images => write!(f, "Images & Icons"),
ComponentScope::Input => write!(f, "Forms & Input"),
ComponentScope::Layout => write!(f, "Layout & Structure"),
ComponentScope::Loading => write!(f, "Loading & Progress"),
ComponentScope::Navigation => write!(f, "Navigation"),
ComponentScope::None => write!(f, "Unsorted"),
ComponentScope::Notification => write!(f, "Notification"),
ComponentScope::Overlays => write!(f, "Overlays & Layering"),
ComponentScope::Status => write!(f, "Status"),
ComponentScope::Typography => write!(f, "Typography"),
ComponentScope::VersionControl => write!(f, "Version Control"),
}
}
}
/// A single example of a component.
#[derive(IntoElement)]
pub struct ComponentExample {
pub variant_name: SharedString,
pub description: Option<SharedString>,
pub element: AnyElement,
pub width: Option<Pixels>,
}
impl RenderOnce for ComponentExample {
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
div()
.pt_2()
.map(|this| {
if let Some(width) = self.width {
this.w(width)
} else {
this.w_full()
}
})
.flex()
.flex_col()
.gap_3()
.child(
div()
.flex()
.flex_col()
.child(
div()
.child(self.variant_name.clone())
.text_size(rems(1.0))
.text_color(cx.theme().colors().text),
)
.when_some(self.description, |this, description| {
this.child(
div()
.text_size(rems(0.875))
.text_color(cx.theme().colors().text_muted)
.child(description.clone()),
)
}),
)
.child(
div()
.flex()
.w_full()
.rounded_xl()
.min_h(px(100.))
.justify_center()
.p_8()
.border_1()
.border_color(cx.theme().colors().border.opacity(0.5))
.bg(pattern_slash(
cx.theme().colors().surface_background.opacity(0.5),
12.0,
12.0,
))
.shadow_sm()
.child(self.element),
)
.into_any_element()
}
}
impl ComponentExample {
pub fn new(variant_name: impl Into<SharedString>, element: AnyElement) -> Self {
Self {
variant_name: variant_name.into(),
element,
description: None,
width: None,
}
}
pub fn description(mut self, description: impl Into<SharedString>) -> Self {
self.description = Some(description.into());
self
}
pub fn width(mut self, width: Pixels) -> Self {
self.width = Some(width);
self
}
}
/// A group of component examples.
#[derive(IntoElement)]
pub struct ComponentExampleGroup {
pub title: Option<SharedString>,
pub examples: Vec<ComponentExample>,
pub width: Option<Pixels>,
pub grow: bool,
pub vertical: bool,
}
impl RenderOnce for ComponentExampleGroup {
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
div()
.flex_col()
.text_sm()
.text_color(cx.theme().colors().text_muted)
.map(|this| {
if let Some(width) = self.width {
this.w(width)
} else {
this.w_full()
}
})
.when_some(self.title, |this, title| {
this.gap_4().child(
div()
.flex()
.items_center()
.gap_3()
.pb_1()
.child(div().h_px().w_4().bg(cx.theme().colors().border))
.child(
div()
.flex_none()
.text_size(px(10.))
.child(title.to_uppercase()),
)
.child(
div()
.h_px()
.w_full()
.flex_1()
.bg(cx.theme().colors().border),
),
)
})
.child(
div()
.flex()
.flex_col()
.items_start()
.w_full()
.gap_6()
.children(self.examples)
.into_any_element(),
)
.into_any_element()
}
}
impl ComponentExampleGroup {
pub fn new(examples: Vec<ComponentExample>) -> Self {
Self {
title: None,
examples,
width: None,
grow: false,
vertical: false,
}
}
pub fn with_title(title: impl Into<SharedString>, examples: Vec<ComponentExample>) -> Self {
Self {
title: Some(title.into()),
examples,
width: None,
grow: false,
vertical: false,
}
}
pub fn width(mut self, width: Pixels) -> Self {
self.width = Some(width);
self
}
pub fn grow(mut self) -> Self {
self.grow = true;
self
}
pub fn vertical(mut self) -> Self {
self.vertical = true;
self
}
}
pub fn single_example(
variant_name: impl Into<SharedString>,
example: AnyElement,
) -> ComponentExample {
ComponentExample::new(variant_name, example)
}
pub fn empty_example(variant_name: impl Into<SharedString>) -> ComponentExample {
ComponentExample::new(variant_name, div().w_full().text_center().items_center().text_xs().opacity(0.4).child("This space is intentionally left blank. It indicates a case that should render nothing.").into_any_element())
}
pub fn example_group(examples: Vec<ComponentExample>) -> ComponentExampleGroup {
ComponentExampleGroup::new(examples)
}
pub fn example_group_with_title(
title: impl Into<SharedString>,
examples: Vec<ComponentExample>,
) -> ComponentExampleGroup {
ComponentExampleGroup::with_title(title, examples)
}

View File

@@ -0,0 +1,205 @@
use gpui::{
AnyElement, App, IntoElement, Pixels, RenderOnce, SharedString, Window, div, pattern_slash,
prelude::*, px, rems,
};
use theme::ActiveTheme;
/// A single example of a component.
#[derive(IntoElement)]
pub struct ComponentExample {
pub variant_name: SharedString,
pub description: Option<SharedString>,
pub element: AnyElement,
pub width: Option<Pixels>,
}
impl RenderOnce for ComponentExample {
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
div()
.pt_2()
.map(|this| {
if let Some(width) = self.width {
this.w(width)
} else {
this.w_full()
}
})
.flex()
.flex_col()
.gap_3()
.child(
div()
.flex()
.flex_col()
.child(
div()
.child(self.variant_name.clone())
.text_size(rems(1.0))
.text_color(cx.theme().colors().text),
)
.when_some(self.description, |this, description| {
this.child(
div()
.text_size(rems(0.875))
.text_color(cx.theme().colors().text_muted)
.child(description.clone()),
)
}),
)
.child(
div()
.flex()
.w_full()
.rounded_xl()
.min_h(px(100.))
.justify_center()
.p_8()
.border_1()
.border_color(cx.theme().colors().border.opacity(0.5))
.bg(pattern_slash(
cx.theme().colors().surface_background.opacity(0.5),
12.0,
12.0,
))
.shadow_sm()
.child(self.element),
)
.into_any_element()
}
}
impl ComponentExample {
pub fn new(variant_name: impl Into<SharedString>, element: AnyElement) -> Self {
Self {
variant_name: variant_name.into(),
element,
description: None,
width: None,
}
}
pub fn description(mut self, description: impl Into<SharedString>) -> Self {
self.description = Some(description.into());
self
}
pub fn width(mut self, width: Pixels) -> Self {
self.width = Some(width);
self
}
}
/// A group of component examples.
#[derive(IntoElement)]
pub struct ComponentExampleGroup {
pub title: Option<SharedString>,
pub examples: Vec<ComponentExample>,
pub width: Option<Pixels>,
pub grow: bool,
pub vertical: bool,
}
impl RenderOnce for ComponentExampleGroup {
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
div()
.flex_col()
.text_sm()
.text_color(cx.theme().colors().text_muted)
.map(|this| {
if let Some(width) = self.width {
this.w(width)
} else {
this.w_full()
}
})
.when_some(self.title, |this, title| {
this.gap_4().child(
div()
.flex()
.items_center()
.gap_3()
.pb_1()
.child(div().h_px().w_4().bg(cx.theme().colors().border))
.child(
div()
.flex_none()
.text_size(px(10.))
.child(title.to_uppercase()),
)
.child(
div()
.h_px()
.w_full()
.flex_1()
.bg(cx.theme().colors().border),
),
)
})
.child(
div()
.flex()
.flex_col()
.items_start()
.w_full()
.gap_6()
.children(self.examples)
.into_any_element(),
)
.into_any_element()
}
}
impl ComponentExampleGroup {
pub fn new(examples: Vec<ComponentExample>) -> Self {
Self {
title: None,
examples,
width: None,
grow: false,
vertical: false,
}
}
pub fn with_title(title: impl Into<SharedString>, examples: Vec<ComponentExample>) -> Self {
Self {
title: Some(title.into()),
examples,
width: None,
grow: false,
vertical: false,
}
}
pub fn width(mut self, width: Pixels) -> Self {
self.width = Some(width);
self
}
pub fn grow(mut self) -> Self {
self.grow = true;
self
}
pub fn vertical(mut self) -> Self {
self.vertical = true;
self
}
}
pub fn single_example(
variant_name: impl Into<SharedString>,
example: AnyElement,
) -> ComponentExample {
ComponentExample::new(variant_name, example)
}
pub fn empty_example(variant_name: impl Into<SharedString>) -> ComponentExample {
ComponentExample::new(variant_name, div().w_full().text_center().items_center().text_xs().opacity(0.4).child("This space is intentionally left blank. It indicates a case that should render nothing.").into_any_element())
}
pub fn example_group(examples: Vec<ComponentExample>) -> ComponentExampleGroup {
ComponentExampleGroup::new(examples)
}
pub fn example_group_with_title(
title: impl Into<SharedString>,
examples: Vec<ComponentExample>,
) -> ComponentExampleGroup {
ComponentExampleGroup::with_title(title, examples)
}

View File

@@ -5,12 +5,13 @@
mod persistence;
mod preview_support;
use std::iter::Iterator;
use std::sync::Arc;
use std::iter::Iterator;
use agent::{ActiveThread, TextThreadStore, ThreadStore};
use client::UserStore;
use component::{ComponentId, ComponentMetadata, components};
use component::{ComponentId, ComponentMetadata, ComponentStatus, components};
use gpui::{
App, Entity, EventEmitter, FocusHandle, Focusable, Task, WeakEntity, Window, list, prelude::*,
};
@@ -25,7 +26,7 @@ use preview_support::active_thread::{
load_preview_text_thread_store, load_preview_thread_store, static_active_thread,
};
use project::Project;
use ui::{Divider, HighlightedLabel, ListItem, ListSubHeader, prelude::*};
use ui::{ButtonLike, Divider, HighlightedLabel, ListItem, ListSubHeader, Tooltip, prelude::*};
use ui_input::SingleLineInput;
use util::ResultExt as _;
use workspace::{AppState, ItemId, SerializableItem, delete_unloaded_items};
@@ -104,26 +105,24 @@ enum PreviewPage {
}
struct ComponentPreview {
workspace_id: Option<WorkspaceId>,
focus_handle: FocusHandle,
_view_scroll_handle: ScrollHandle,
nav_scroll_handle: UniformListScrollHandle,
component_map: HashMap<ComponentId, ComponentMetadata>,
active_page: PreviewPage,
components: Vec<ComponentMetadata>,
active_thread: Option<Entity<ActiveThread>>,
component_list: ListState,
component_map: HashMap<ComponentId, ComponentMetadata>,
components: Vec<ComponentMetadata>,
cursor_index: usize,
language_registry: Arc<LanguageRegistry>,
workspace: WeakEntity<Workspace>,
project: Entity<Project>,
user_store: Entity<UserStore>,
filter_editor: Entity<SingleLineInput>,
filter_text: String,
// preview support
thread_store: Option<Entity<ThreadStore>>,
focus_handle: FocusHandle,
language_registry: Arc<LanguageRegistry>,
nav_scroll_handle: UniformListScrollHandle,
project: Entity<Project>,
text_thread_store: Option<Entity<TextThreadStore>>,
active_thread: Option<Entity<ActiveThread>>,
thread_store: Option<Entity<ThreadStore>>,
user_store: Entity<UserStore>,
workspace: WeakEntity<Workspace>,
workspace_id: Option<WorkspaceId>,
_view_scroll_handle: ScrollHandle,
}
impl ComponentPreview {
@@ -164,7 +163,8 @@ impl ComponentPreview {
})
.detach();
let sorted_components = components().all_sorted();
let component_registry = Arc::new(components());
let sorted_components = component_registry.sorted_components();
let selected_index = selected_index.into().unwrap_or(0);
let active_page = active_page.unwrap_or(PreviewPage::AllComponents);
let filter_editor =
@@ -188,24 +188,24 @@ impl ComponentPreview {
);
let mut component_preview = Self {
workspace_id: None,
focus_handle: cx.focus_handle(),
_view_scroll_handle: ScrollHandle::new(),
nav_scroll_handle: UniformListScrollHandle::new(),
language_registry,
user_store,
workspace,
project,
active_page,
component_map: components().0,
components: sorted_components,
active_thread: None,
component_list,
component_map: component_registry.component_map(),
components: sorted_components,
cursor_index: selected_index,
filter_editor,
filter_text: String::new(),
thread_store: None,
focus_handle: cx.focus_handle(),
language_registry,
nav_scroll_handle: UniformListScrollHandle::new(),
project,
text_thread_store: None,
active_thread: None,
thread_store: None,
user_store,
workspace,
workspace_id: None,
_view_scroll_handle: ScrollHandle::new(),
};
if component_preview.cursor_index > 0 {
@@ -412,6 +412,88 @@ impl ComponentPreview {
entries
}
fn update_component_list(&mut self, cx: &mut Context<Self>) {
let entries = self.scope_ordered_entries();
let new_len = entries.len();
let weak_entity = cx.entity().downgrade();
if new_len > 0 {
self.nav_scroll_handle
.scroll_to_item(0, ScrollStrategy::Top);
}
let filtered_components = self.filtered_components();
if !self.filter_text.is_empty() && !matches!(self.active_page, PreviewPage::AllComponents) {
if let PreviewPage::Component(ref component_id) = self.active_page {
let component_still_visible = filtered_components
.iter()
.any(|component| component.id() == *component_id);
if !component_still_visible {
if !filtered_components.is_empty() {
let first_component = &filtered_components[0];
self.set_active_page(PreviewPage::Component(first_component.id()), cx);
} else {
self.set_active_page(PreviewPage::AllComponents, cx);
}
}
}
}
self.component_list = ListState::new(
filtered_components.len(),
gpui::ListAlignment::Top,
px(1500.0),
{
let components = filtered_components.clone();
let this = cx.entity().downgrade();
move |ix, window: &mut Window, cx: &mut App| {
if ix >= components.len() {
return div().w_full().h_0().into_any_element();
}
this.update(cx, |this, cx| {
let component = &components[ix];
this.render_preview(component, window, cx)
.into_any_element()
})
.unwrap()
}
},
);
let new_list = ListState::new(
new_len,
gpui::ListAlignment::Top,
px(1500.0),
move |ix, window, cx| {
if ix >= entries.len() {
return div().w_full().h_0().into_any_element();
}
let entry = &entries[ix];
weak_entity
.update(cx, |this, cx| match entry {
PreviewEntry::Component(component, _) => this
.render_preview(component, window, cx)
.into_any_element(),
PreviewEntry::SectionHeader(shared_string) => this
.render_scope_header(ix, shared_string.clone(), window, cx)
.into_any_element(),
PreviewEntry::AllComponents => div().w_full().h_0().into_any_element(),
PreviewEntry::ActiveThread => div().w_full().h_0().into_any_element(),
PreviewEntry::Separator => div().w_full().h_0().into_any_element(),
})
.unwrap()
},
);
self.component_list = new_list;
cx.emit(ItemEvent::UpdateTab);
}
fn render_sidebar_entry(
&self,
ix: usize,
@@ -495,88 +577,6 @@ impl ComponentPreview {
}
}
fn update_component_list(&mut self, cx: &mut Context<Self>) {
let entries = self.scope_ordered_entries();
let new_len = entries.len();
let weak_entity = cx.entity().downgrade();
if new_len > 0 {
self.nav_scroll_handle
.scroll_to_item(0, ScrollStrategy::Top);
}
let filtered_components = self.filtered_components();
if !self.filter_text.is_empty() && !matches!(self.active_page, PreviewPage::AllComponents) {
if let PreviewPage::Component(ref component_id) = self.active_page {
let component_still_visible = filtered_components
.iter()
.any(|component| component.id() == *component_id);
if !component_still_visible {
if !filtered_components.is_empty() {
let first_component = &filtered_components[0];
self.set_active_page(PreviewPage::Component(first_component.id()), cx);
} else {
self.set_active_page(PreviewPage::AllComponents, cx);
}
}
}
}
self.component_list = ListState::new(
filtered_components.len(),
gpui::ListAlignment::Top,
px(1500.0),
{
let components = filtered_components.clone();
let this = cx.entity().downgrade();
move |ix, window: &mut Window, cx: &mut App| {
if ix >= components.len() {
return div().w_full().h_0().into_any_element();
}
this.update(cx, |this, cx| {
let component = &components[ix];
this.render_preview(component, window, cx)
.into_any_element()
})
.unwrap()
}
},
);
let new_list = ListState::new(
new_len,
gpui::ListAlignment::Top,
px(1500.0),
move |ix, window, cx| {
if ix >= entries.len() {
return div().w_full().h_0().into_any_element();
}
let entry = &entries[ix];
weak_entity
.update(cx, |this, cx| match entry {
PreviewEntry::Component(component, _) => this
.render_preview(component, window, cx)
.into_any_element(),
PreviewEntry::SectionHeader(shared_string) => this
.render_scope_header(ix, shared_string.clone(), window, cx)
.into_any_element(),
PreviewEntry::AllComponents => div().w_full().h_0().into_any_element(),
PreviewEntry::ActiveThread => div().w_full().h_0().into_any_element(),
PreviewEntry::Separator => div().w_full().h_0().into_any_element(),
})
.unwrap()
},
);
self.component_list = new_list;
cx.emit(ItemEvent::UpdateTab);
}
fn render_scope_header(
&self,
_ix: usize,
@@ -695,7 +695,7 @@ impl ComponentPreview {
if let Some(component) = component {
v_flex()
.id("render-component-page")
.size_full()
.flex_1()
.child(ComponentPreviewPage::new(
component.clone(),
self.workspace.clone(),
@@ -971,7 +971,7 @@ impl SerializableItem for ComponentPreview {
} else {
let component_str = deserialized_active_page.0;
let component_registry = components();
let all_components = component_registry.all();
let all_components = component_registry.components();
let found_component = all_components.iter().find(|c| c.id().0 == component_str);
if let Some(component) = found_component {
@@ -1065,6 +1065,43 @@ impl ComponentPreviewPage {
}
}
/// Renders the component status when it would be useful
///
/// Doesn't render if the component is `ComponentStatus::Live`
/// as that is the default state
fn render_component_status(&self, cx: &App) -> Option<impl IntoElement> {
let status = self.component.status();
let status_description = status.description().to_string();
let color = match status {
ComponentStatus::Deprecated => Color::Error,
ComponentStatus::EngineeringReady => Color::Info,
ComponentStatus::Live => Color::Success,
ComponentStatus::WorkInProgress => Color::Warning,
};
if status != ComponentStatus::Live {
Some(
ButtonLike::new("component_status")
.child(
div()
.px_1p5()
.rounded_sm()
.bg(color.color(cx).alpha(0.12))
.child(
Label::new(status.clone().to_string())
.size(LabelSize::Small)
.color(color),
),
)
.tooltip(Tooltip::text(status_description))
.disabled(true),
)
} else {
None
}
}
fn render_header(&self, _: &Window, cx: &App) -> impl IntoElement {
v_flex()
.px_12()
@@ -1083,7 +1120,14 @@ impl ComponentPreviewPage {
.color(Color::Muted),
)
.child(
Headline::new(self.component.scopeless_name()).size(HeadlineSize::XLarge),
h_flex()
.items_center()
.gap_2()
.child(
Headline::new(self.component.scopeless_name())
.size(HeadlineSize::XLarge),
)
.children(self.render_component_status(cx)),
),
)
.when_some(self.component.description(), |this, description| {

View File

@@ -278,7 +278,7 @@ impl RegisteredBuffer {
content_changes,
},
)
.log_err();
.ok();
}
let _ = done_tx.send((buffer.snapshot_version, buffer.snapshot.clone()));
Some(())
@@ -732,7 +732,7 @@ impl Copilot {
},
},
)
.log_err();
.ok();
RegisteredBuffer {
uri,
@@ -827,7 +827,7 @@ impl Copilot {
text_document: lsp::TextDocumentIdentifier::new(buffer.uri),
},
)
.log_err();
.ok();
}
}
}

View File

@@ -12,10 +12,9 @@ use language::LanguageToolchainStore;
use node_runtime::NodeRuntime;
use serde::{Deserialize, Serialize};
use settings::WorktreeId;
use smol::{self, fs::File, lock::Mutex};
use smol::{self, fs::File};
use std::{
borrow::Borrow,
collections::HashSet,
ffi::OsStr,
fmt::Debug,
net::Ipv4Addr,
@@ -24,7 +23,6 @@ use std::{
sync::Arc,
};
use task::{AttachRequest, DebugRequest, DebugScenario, LaunchRequest, TcpArgumentsTemplate};
use util::ResultExt;
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum DapStatus {
@@ -41,8 +39,7 @@ pub trait DapDelegate {
fn node_runtime(&self) -> NodeRuntime;
fn toolchain_store(&self) -> Arc<dyn LanguageToolchainStore>;
fn fs(&self) -> Arc<dyn Fs>;
fn updated_adapters(&self) -> Arc<Mutex<HashSet<DebugAdapterName>>>;
fn update_status(&self, dap_name: DebugAdapterName, status: DapStatus);
fn output_to_console(&self, msg: String);
fn which(&self, command: &OsStr) -> Option<PathBuf>;
async fn shell_env(&self) -> collections::HashMap<String, String>;
}
@@ -88,7 +85,7 @@ impl<'a> From<&'a str> for DebugAdapterName {
}
}
#[derive(Debug, Clone)]
#[derive(Debug, Clone, PartialEq)]
pub struct TcpArguments {
pub host: Ipv4Addr,
pub port: u16,
@@ -127,7 +124,7 @@ impl TcpArguments {
)]
pub struct DebugTaskDefinition {
pub label: SharedString,
pub adapter: SharedString,
pub adapter: DebugAdapterName,
pub request: DebugRequest,
/// Additional initialization arguments to be sent on DAP initialization
pub initialize_args: Option<serde_json::Value>,
@@ -153,7 +150,7 @@ impl DebugTaskDefinition {
pub fn to_scenario(&self) -> DebugScenario {
DebugScenario {
label: self.label.clone(),
adapter: self.adapter.clone(),
adapter: self.adapter.clone().into(),
build: None,
request: Some(self.request.clone()),
stop_on_entry: self.stop_on_entry,
@@ -207,7 +204,7 @@ impl DebugTaskDefinition {
.map(TcpArgumentsTemplate::from_proto)
.transpose()?,
stop_on_entry: proto.stop_on_entry,
adapter: proto.adapter.into(),
adapter: DebugAdapterName(proto.adapter.into()),
request: match request {
proto::debug_task_definition::Request::DebugAttachRequest(config) => {
DebugRequest::Attach(AttachRequest {
@@ -229,7 +226,7 @@ impl DebugTaskDefinition {
}
/// Created from a [DebugTaskDefinition], this struct describes how to spawn the debugger to create a previously-configured debug session.
#[derive(Debug, Clone)]
#[derive(Debug, Clone, PartialEq)]
pub struct DebugAdapterBinary {
pub command: String,
pub arguments: Vec<String>,
@@ -293,7 +290,7 @@ impl DebugAdapterBinary {
}
}
#[derive(Debug)]
#[derive(Debug, Clone)]
pub struct AdapterVersion {
pub tag_name: String,
pub url: String,
@@ -335,6 +332,7 @@ pub async fn download_adapter_from_github(
adapter_name,
&github_version.url,
);
delegate.output_to_console(format!("Downloading from {}...", github_version.url));
let mut response = delegate
.http_client()
@@ -418,84 +416,6 @@ pub trait DebugAdapter: 'static + Send + Sync {
config: &DebugTaskDefinition,
user_installed_path: Option<PathBuf>,
cx: &mut AsyncApp,
) -> Result<DebugAdapterBinary> {
if delegate
.updated_adapters()
.lock()
.await
.contains(&self.name())
{
log::info!("Using cached debug adapter binary {}", self.name());
if let Some(binary) = self
.get_installed_binary(delegate, &config, user_installed_path.clone(), cx)
.await
.log_err()
{
return Ok(binary);
}
log::info!(
"Cached binary {} is corrupt falling back to install",
self.name()
);
}
log::info!("Getting latest version of debug adapter {}", self.name());
delegate.update_status(self.name(), DapStatus::CheckingForUpdate);
if let Some(version) = self.fetch_latest_adapter_version(delegate).await.log_err() {
log::info!(
"Installiing latest version of debug adapter {}",
self.name()
);
delegate.update_status(self.name(), DapStatus::Downloading);
match self.install_binary(version, delegate).await {
Ok(_) => {
delegate.update_status(self.name(), DapStatus::None);
}
Err(error) => {
delegate.update_status(
self.name(),
DapStatus::Failed {
error: error.to_string(),
},
);
return Err(error);
}
}
delegate
.updated_adapters()
.lock_arc()
.await
.insert(self.name());
}
self.get_installed_binary(delegate, &config, user_installed_path, cx)
.await
}
async fn fetch_latest_adapter_version(
&self,
delegate: &dyn DapDelegate,
) -> Result<AdapterVersion>;
/// Installs the binary for the debug adapter.
/// This method is called when the adapter binary is not found or needs to be updated.
/// It should download and install the necessary files for the debug adapter to function.
async fn install_binary(
&self,
version: AdapterVersion,
delegate: &dyn DapDelegate,
) -> Result<()>;
async fn get_installed_binary(
&self,
delegate: &dyn DapDelegate,
config: &DebugTaskDefinition,
user_installed_path: Option<PathBuf>,
cx: &mut AsyncApp,
) -> Result<DebugAdapterBinary>;
fn inline_value_provider(&self) -> Option<Box<dyn InlineValueProvider>> {
@@ -564,29 +484,4 @@ impl DebugAdapter for FakeAdapter {
request_args: self.request_args(config),
})
}
async fn fetch_latest_adapter_version(
&self,
_delegate: &dyn DapDelegate,
) -> Result<AdapterVersion> {
unimplemented!("fetch latest adapter version");
}
async fn install_binary(
&self,
_version: AdapterVersion,
_delegate: &dyn DapDelegate,
) -> Result<()> {
unimplemented!("install binary");
}
async fn get_installed_binary(
&self,
_: &dyn DapDelegate,
_: &DebugTaskDefinition,
_: Option<PathBuf>,
_: &mut AsyncApp,
) -> Result<DebugAdapterBinary> {
unimplemented!("get installed binary");
}
}

View File

@@ -4,6 +4,14 @@ use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use settings::{Settings, SettingsSources};
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum DebugPanelDockPosition {
Left,
Bottom,
Right,
}
#[derive(Serialize, Deserialize, JsonSchema, Clone, Copy)]
#[serde(default)]
pub struct DebuggerSettings {
@@ -31,6 +39,10 @@ pub struct DebuggerSettings {
///
/// Default: true
pub format_dap_log_messages: bool,
/// The dock position of the debug panel
///
/// Default: Bottom
pub dock: DebugPanelDockPosition,
}
impl Default for DebuggerSettings {
@@ -42,6 +54,7 @@ impl Default for DebuggerSettings {
timeout: 2000,
log_dap_communications: true,
format_dap_log_messages: true,
dock: DebugPanelDockPosition::Bottom,
}
}
}

View File

@@ -1,9 +1,9 @@
use anyhow::Result;
use async_trait::async_trait;
use collections::FxHashMap;
use gpui::{App, Global};
use gpui::{App, Global, SharedString};
use parking_lot::RwLock;
use task::{DebugRequest, SpawnInTerminal};
use task::{DebugRequest, DebugScenario, SpawnInTerminal, TaskTemplate};
use crate::adapters::{DebugAdapter, DebugAdapterName};
use std::{collections::BTreeMap, sync::Arc};
@@ -11,15 +11,17 @@ use std::{collections::BTreeMap, sync::Arc};
/// Given a user build configuration, locator creates a fill-in debug target ([DebugRequest]) on behalf of the user.
#[async_trait]
pub trait DapLocator: Send + Sync {
fn name(&self) -> SharedString;
/// Determines whether this locator can generate debug target for given task.
fn accepts(&self, build_config: &SpawnInTerminal) -> bool;
fn create_scenario(&self, build_config: &TaskTemplate, adapter: &str) -> Option<DebugScenario>;
async fn run(&self, build_config: SpawnInTerminal) -> Result<DebugRequest>;
}
#[derive(Default)]
struct DapRegistryState {
adapters: BTreeMap<DebugAdapterName, Arc<dyn DebugAdapter>>,
locators: FxHashMap<String, Arc<dyn DapLocator>>,
locators: FxHashMap<SharedString, Arc<dyn DapLocator>>,
}
#[derive(Clone, Default)]
@@ -48,15 +50,15 @@ impl DapRegistry {
);
}
pub fn add_locator(&self, name: String, locator: Arc<dyn DapLocator>) {
let _previous_value = self.0.write().locators.insert(name, locator);
pub fn add_locator(&self, locator: Arc<dyn DapLocator>) {
let _previous_value = self.0.write().locators.insert(locator.name(), locator);
debug_assert!(
_previous_value.is_none(),
"Attempted to insert a new debug locator when one is already registered"
);
}
pub fn locators(&self) -> FxHashMap<String, Arc<dyn DapLocator>> {
pub fn locators(&self) -> FxHashMap<SharedString, Arc<dyn DapLocator>> {
self.0.read().locators.clone()
}

View File

@@ -24,6 +24,7 @@ doctest = false
anyhow.workspace = true
async-trait.workspace = true
dap.workspace = true
futures.workspace = true
gpui.workspace = true
language.workspace = true
lsp-types.workspace = true

View File

@@ -1,16 +1,18 @@
use std::{collections::HashMap, path::PathBuf, sync::OnceLock};
use anyhow::{Result, bail};
use anyhow::Result;
use async_trait::async_trait;
use dap::adapters::{DebugTaskDefinition, InlineValueProvider, latest_github_release};
use futures::StreamExt;
use gpui::AsyncApp;
use task::DebugRequest;
use util::fs::remove_matching;
use crate::*;
#[derive(Default)]
pub(crate) struct CodeLldbDebugAdapter {
last_known_version: OnceLock<String>,
path_to_codelldb: OnceLock<String>,
}
impl CodeLldbDebugAdapter {
@@ -54,29 +56,6 @@ impl CodeLldbDebugAdapter {
configuration,
}
}
}
#[async_trait(?Send)]
impl DebugAdapter for CodeLldbDebugAdapter {
fn name(&self) -> DebugAdapterName {
DebugAdapterName(Self::ADAPTER_NAME.into())
}
async fn install_binary(
&self,
version: AdapterVersion,
delegate: &dyn DapDelegate,
) -> Result<()> {
adapters::download_adapter_from_github(
self.name(),
version,
adapters::DownloadedFileType::Vsix,
delegate,
)
.await?;
Ok(())
}
async fn fetch_latest_adapter_version(
&self,
@@ -107,7 +86,6 @@ impl DebugAdapter for CodeLldbDebugAdapter {
}
};
let asset_name = format!("codelldb-{platform}-{arch}.vsix");
let _ = self.last_known_version.set(release.tag_name.clone());
let ret = AdapterVersion {
tag_name: release.tag_name,
url: release
@@ -121,28 +99,56 @@ impl DebugAdapter for CodeLldbDebugAdapter {
Ok(ret)
}
}
async fn get_installed_binary(
#[async_trait(?Send)]
impl DebugAdapter for CodeLldbDebugAdapter {
fn name(&self) -> DebugAdapterName {
DebugAdapterName(Self::ADAPTER_NAME.into())
}
async fn get_binary(
&self,
_: &dyn DapDelegate,
delegate: &dyn DapDelegate,
config: &DebugTaskDefinition,
_: Option<PathBuf>,
user_installed_path: Option<PathBuf>,
_: &mut AsyncApp,
) -> Result<DebugAdapterBinary> {
let Some(version) = self.last_known_version.get() else {
bail!("Could not determine latest CodeLLDB version");
};
let adapter_path = paths::debug_adapters_dir().join(&Self::ADAPTER_NAME);
let version_path = adapter_path.join(format!("{}_{}", Self::ADAPTER_NAME, version));
let mut command = user_installed_path
.map(|p| p.to_string_lossy().to_string())
.or(self.path_to_codelldb.get().cloned());
if command.is_none() {
delegate.output_to_console(format!("Checking latest version of {}...", self.name()));
let adapter_path = paths::debug_adapters_dir().join(&Self::ADAPTER_NAME);
let version_path =
if let Ok(version) = self.fetch_latest_adapter_version(delegate).await {
adapters::download_adapter_from_github(
self.name(),
version.clone(),
adapters::DownloadedFileType::Vsix,
delegate,
)
.await?;
let version_path =
adapter_path.join(format!("{}_{}", Self::ADAPTER_NAME, version.tag_name));
remove_matching(&adapter_path, |entry| entry != version_path).await;
version_path
} else {
let mut paths = delegate.fs().read_dir(&adapter_path).await?;
paths
.next()
.await
.ok_or_else(|| anyhow!("No adapter found"))??
};
let adapter_dir = version_path.join("extension").join("adapter");
let path = adapter_dir.join("codelldb").to_string_lossy().to_string();
self.path_to_codelldb.set(path.clone()).ok();
command = Some(path);
};
let adapter_dir = version_path.join("extension").join("adapter");
let command = adapter_dir.join("codelldb");
let command = command
.to_str()
.map(ToOwned::to_owned)
.ok_or_else(|| anyhow!("Adapter path is expected to be valid UTF-8"))?;
Ok(DebugAdapterBinary {
command,
command: command.unwrap(),
cwd: None,
arguments: vec![
"--settings".into(),

View File

@@ -29,9 +29,9 @@ use task::TcpArgumentsTemplate;
pub fn init(cx: &mut App) {
cx.update_default_global(|registry: &mut DapRegistry, _cx| {
registry.add_adapter(Arc::from(CodeLldbDebugAdapter::default()));
registry.add_adapter(Arc::from(PythonDebugAdapter));
registry.add_adapter(Arc::from(PhpDebugAdapter));
registry.add_adapter(Arc::from(JsDebugAdapter));
registry.add_adapter(Arc::from(PythonDebugAdapter::default()));
registry.add_adapter(Arc::from(PhpDebugAdapter::default()));
registry.add_adapter(Arc::from(JsDebugAdapter::default()));
registry.add_adapter(Arc::from(GoDebugAdapter));
registry.add_adapter(Arc::from(GdbDebugAdapter));
})

View File

@@ -90,26 +90,4 @@ impl DebugAdapter for GdbDebugAdapter {
request_args: self.request_args(config),
})
}
async fn install_binary(
&self,
_version: AdapterVersion,
_delegate: &dyn DapDelegate,
) -> Result<()> {
unimplemented!("GDB debug adapter cannot be installed by Zed (yet)")
}
async fn fetch_latest_adapter_version(&self, _: &dyn DapDelegate) -> Result<AdapterVersion> {
unimplemented!("Fetch latest GDB version not implemented (yet)")
}
async fn get_installed_binary(
&self,
_: &dyn DapDelegate,
_: &DebugTaskDefinition,
_: Option<std::path::PathBuf>,
_: &mut AsyncApp,
) -> Result<DebugAdapterBinary> {
unimplemented!("GDB cannot be installed by Zed (yet)")
}
}

View File

@@ -46,41 +46,8 @@ impl DebugAdapter for GoDebugAdapter {
&self,
delegate: &dyn DapDelegate,
config: &DebugTaskDefinition,
user_installed_path: Option<PathBuf>,
cx: &mut AsyncApp,
) -> Result<DebugAdapterBinary> {
self.get_installed_binary(delegate, config, user_installed_path, cx)
.await
}
async fn fetch_latest_adapter_version(
&self,
_delegate: &dyn DapDelegate,
) -> Result<AdapterVersion> {
unimplemented!("This adapter is used from path for now");
}
async fn install_binary(
&self,
version: AdapterVersion,
delegate: &dyn DapDelegate,
) -> Result<()> {
adapters::download_adapter_from_github(
self.name(),
version,
adapters::DownloadedFileType::Zip,
delegate,
)
.await?;
Ok(())
}
async fn get_installed_binary(
&self,
delegate: &dyn DapDelegate,
config: &DebugTaskDefinition,
_: Option<PathBuf>,
_: &mut AsyncApp,
_user_installed_path: Option<PathBuf>,
_cx: &mut AsyncApp,
) -> Result<DebugAdapterBinary> {
let delve_path = delegate
.which(OsStr::new("dlv"))

View File

@@ -1,13 +1,16 @@
use adapters::latest_github_release;
use dap::{StartDebuggingRequestArguments, adapters::DebugTaskDefinition};
use gpui::AsyncApp;
use std::{collections::HashMap, path::PathBuf};
use std::{collections::HashMap, path::PathBuf, sync::OnceLock};
use task::DebugRequest;
use util::ResultExt;
use crate::*;
#[derive(Debug)]
pub(crate) struct JsDebugAdapter;
#[derive(Debug, Default)]
pub(crate) struct JsDebugAdapter {
checked: OnceLock<()>,
}
impl JsDebugAdapter {
const ADAPTER_NAME: &'static str = "JavaScript";
@@ -47,13 +50,6 @@ impl JsDebugAdapter {
request: config.request.to_dap(),
}
}
}
#[async_trait(?Send)]
impl DebugAdapter for JsDebugAdapter {
fn name(&self) -> DebugAdapterName {
DebugAdapterName(Self::ADAPTER_NAME.into())
}
async fn fetch_latest_adapter_version(
&self,
@@ -130,20 +126,35 @@ impl DebugAdapter for JsDebugAdapter {
request_args: self.request_args(config),
})
}
}
async fn install_binary(
#[async_trait(?Send)]
impl DebugAdapter for JsDebugAdapter {
fn name(&self) -> DebugAdapterName {
DebugAdapterName(Self::ADAPTER_NAME.into())
}
async fn get_binary(
&self,
version: AdapterVersion,
delegate: &dyn DapDelegate,
) -> Result<()> {
adapters::download_adapter_from_github(
self.name(),
version,
adapters::DownloadedFileType::GzipTar,
delegate,
)
.await?;
config: &DebugTaskDefinition,
user_installed_path: Option<PathBuf>,
cx: &mut AsyncApp,
) -> Result<DebugAdapterBinary> {
if self.checked.set(()).is_ok() {
delegate.output_to_console(format!("Checking latest version of {}...", self.name()));
if let Some(version) = self.fetch_latest_adapter_version(delegate).await.log_err() {
adapters::download_adapter_from_github(
self.name(),
version,
adapters::DownloadedFileType::GzipTar,
delegate,
)
.await?;
}
}
return Ok(());
self.get_installed_binary(delegate, &config, user_installed_path, cx)
.await
}
}

View File

@@ -1,12 +1,15 @@
use adapters::latest_github_release;
use dap::adapters::{DebugTaskDefinition, TcpArguments};
use gpui::AsyncApp;
use std::{collections::HashMap, path::PathBuf};
use std::{collections::HashMap, path::PathBuf, sync::OnceLock};
use util::ResultExt;
use crate::*;
#[derive(Default)]
pub(crate) struct PhpDebugAdapter;
pub(crate) struct PhpDebugAdapter {
checked: OnceLock<()>,
}
impl PhpDebugAdapter {
const ADAPTER_NAME: &'static str = "PHP";
@@ -32,13 +35,6 @@ impl PhpDebugAdapter {
}),
}
}
}
#[async_trait(?Send)]
impl DebugAdapter for PhpDebugAdapter {
fn name(&self) -> DebugAdapterName {
DebugAdapterName(Self::ADAPTER_NAME.into())
}
async fn fetch_latest_adapter_version(
&self,
@@ -114,20 +110,35 @@ impl DebugAdapter for PhpDebugAdapter {
request_args: self.request_args(config)?,
})
}
}
async fn install_binary(
#[async_trait(?Send)]
impl DebugAdapter for PhpDebugAdapter {
fn name(&self) -> DebugAdapterName {
DebugAdapterName(Self::ADAPTER_NAME.into())
}
async fn get_binary(
&self,
version: AdapterVersion,
delegate: &dyn DapDelegate,
) -> Result<()> {
adapters::download_adapter_from_github(
self.name(),
version,
adapters::DownloadedFileType::Vsix,
delegate,
)
.await?;
config: &DebugTaskDefinition,
user_installed_path: Option<PathBuf>,
cx: &mut AsyncApp,
) -> Result<DebugAdapterBinary> {
if self.checked.set(()).is_ok() {
delegate.output_to_console(format!("Checking latest version of {}...", self.name()));
if let Some(version) = self.fetch_latest_adapter_version(delegate).await.log_err() {
adapters::download_adapter_from_github(
self.name(),
version,
adapters::DownloadedFileType::Vsix,
delegate,
)
.await?;
}
}
Ok(())
self.get_installed_binary(delegate, &config, user_installed_path, cx)
.await
}
}

View File

@@ -4,10 +4,13 @@ use dap::{
adapters::InlineValueProvider,
};
use gpui::AsyncApp;
use std::{collections::HashMap, ffi::OsStr, path::PathBuf};
use std::{collections::HashMap, ffi::OsStr, path::PathBuf, sync::OnceLock};
use util::ResultExt;
#[derive(Default)]
pub(crate) struct PythonDebugAdapter;
pub(crate) struct PythonDebugAdapter {
checked: OnceLock<()>,
}
impl PythonDebugAdapter {
const ADAPTER_NAME: &'static str = "Debugpy";
@@ -46,14 +49,6 @@ impl PythonDebugAdapter {
request: config.request.to_dap(),
}
}
}
#[async_trait(?Send)]
impl DebugAdapter for PythonDebugAdapter {
fn name(&self) -> DebugAdapterName {
DebugAdapterName(Self::ADAPTER_NAME.into())
}
async fn fetch_latest_adapter_version(
&self,
delegate: &dyn DapDelegate,
@@ -162,6 +157,31 @@ impl DebugAdapter for PythonDebugAdapter {
request_args: self.request_args(config),
})
}
}
#[async_trait(?Send)]
impl DebugAdapter for PythonDebugAdapter {
fn name(&self) -> DebugAdapterName {
DebugAdapterName(Self::ADAPTER_NAME.into())
}
async fn get_binary(
&self,
delegate: &dyn DapDelegate,
config: &DebugTaskDefinition,
user_installed_path: Option<PathBuf>,
cx: &mut AsyncApp,
) -> Result<DebugAdapterBinary> {
if self.checked.set(()).is_ok() {
delegate.output_to_console(format!("Checking latest version of {}...", self.name()));
if let Some(version) = self.fetch_latest_adapter_version(delegate).await.log_err() {
self.install_binary(version, delegate).await?;
}
}
self.get_installed_binary(delegate, &config, user_installed_path, cx)
.await
}
fn inline_value_provider(&self) -> Option<Box<dyn InlineValueProvider>> {
Some(Box::new(PythonInlineValueProvider))

View File

@@ -568,11 +568,11 @@ impl DapLogView {
.sessions()
.filter_map(|session| {
let session = session.read(cx);
session.adapter_name();
session.adapter();
let client = session.adapter_client()?;
Some(DapMenuItem {
client_id: client.id(),
client_name: session.adapter_name().to_string(),
client_name: session.adapter().to_string(),
has_adapter_logs: client.has_adapter_logs(),
selected_entry: self.current_view.map_or(LogKind::Adapter, |(_, kind)| kind),
})

View File

@@ -237,7 +237,7 @@ impl PickerDelegate for AttachModalDelegate {
.flatten();
if let Some(panel) = panel {
panel.update(cx, |panel, cx| {
panel.start_session(scenario, Default::default(), None, window, cx);
panel.start_session(scenario, Default::default(), None, None, window, cx);
});
}

File diff suppressed because it is too large Load Diff

View File

@@ -153,27 +153,63 @@ pub fn init(cx: &mut App) {
let weak_panel = debug_panel.downgrade();
let weak_workspace = cx.weak_entity();
workspace.toggle_modal(window, cx, |window, cx| {
NewSessionModal::new(
debug_panel.read(cx).past_debug_definition.clone(),
weak_panel,
weak_workspace,
window,
cx,
)
});
cx.spawn_in(window, async move |this, cx| {
let task_contexts = this
.update_in(cx, |workspace, window, cx| {
tasks_ui::task_contexts(workspace, window, cx)
})?
.await;
this.update_in(cx, |workspace, window, cx| {
workspace.toggle_modal(window, cx, |window, cx| {
NewSessionModal::new(
debug_panel.read(cx).past_debug_definition.clone(),
weak_panel,
weak_workspace,
None,
task_contexts,
window,
cx,
)
});
})?;
Result::<_, anyhow::Error>::Ok(())
})
.detach();
}
},
)
.register_action(|workspace: &mut Workspace, _: &Start, window, cx| {
tasks_ui::toggle_modal(
workspace,
None,
task::TaskModal::DebugModal,
window,
cx,
)
.detach();
if let Some(debug_panel) = workspace.panel::<DebugPanel>(cx) {
let weak_panel = debug_panel.downgrade();
let weak_workspace = cx.weak_entity();
let task_store = workspace.project().read(cx).task_store().clone();
cx.spawn_in(window, async move |this, cx| {
let task_contexts = this
.update_in(cx, |workspace, window, cx| {
tasks_ui::task_contexts(workspace, window, cx)
})?
.await;
this.update_in(cx, |workspace, window, cx| {
workspace.toggle_modal(window, cx, |window, cx| {
NewSessionModal::new(
debug_panel.read(cx).past_debug_definition.clone(),
weak_panel,
weak_workspace,
Some(task_store),
task_contexts,
window,
cx,
)
});
})?;
anyhow::Ok(())
})
.detach()
}
});
})
})

View File

@@ -1,37 +1,48 @@
use std::{
borrow::Cow,
cmp::Reverse,
ops::Not,
path::{Path, PathBuf},
sync::Arc,
};
use dap::{DapRegistry, DebugRequest, adapters::DebugTaskDefinition};
use editor::{Editor, EditorElement, EditorStyle};
use gpui::{
App, AppContext, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, Render, TextStyle,
WeakEntity,
use collections::{HashMap, HashSet};
use dap::{
DapRegistry, DebugRequest,
adapters::{DebugAdapterName, DebugTaskDefinition},
};
use editor::{Editor, EditorElement, EditorStyle};
use fuzzy::{StringMatch, StringMatchCandidate};
use gpui::{
App, AppContext, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, Render,
Subscription, TextStyle, WeakEntity,
};
use picker::{Picker, PickerDelegate, highlighted_match_with_paths::HighlightedMatch};
use project::{TaskContexts, TaskSourceKind, task_store::TaskStore};
use settings::Settings;
use task::{DebugScenario, LaunchRequest, TaskContext};
use task::{DebugScenario, LaunchRequest};
use theme::ThemeSettings;
use ui::{
ActiveTheme, Button, ButtonCommon, ButtonSize, CheckboxWithLabel, Clickable, Color, Context,
ContextMenu, Disableable, DropdownMenu, FluentBuilder, InteractiveElement, IntoElement, Label,
LabelCommon as _, ParentElement, RenderOnce, SharedString, Styled, StyledExt, ToggleButton,
ToggleState, Toggleable, Window, div, h_flex, relative, rems, v_flex,
ContextMenu, Disableable, DropdownMenu, FluentBuilder, Icon, IconName, InteractiveElement,
IntoElement, Label, LabelCommon as _, ListItem, ListItemSpacing, ParentElement, RenderOnce,
SharedString, Styled, StyledExt, ToggleButton, ToggleState, Toggleable, Window, div, h_flex,
relative, rems, v_flex,
};
use util::ResultExt;
use workspace::{ModalView, Workspace};
use crate::{attach_modal::AttachModal, debugger_panel::DebugPanel};
#[derive(Clone)]
pub(super) struct NewSessionModal {
workspace: WeakEntity<Workspace>,
debug_panel: WeakEntity<DebugPanel>,
mode: NewSessionMode,
stop_on_entry: ToggleState,
initialize_args: Option<serde_json::Value>,
debugger: Option<SharedString>,
debugger: Option<DebugAdapterName>,
last_selected_profile_name: Option<SharedString>,
task_contexts: Arc<TaskContexts>,
}
fn suggested_label(request: &DebugRequest, debugger: &str) -> SharedString {
@@ -57,6 +68,8 @@ impl NewSessionModal {
past_debug_definition: Option<DebugTaskDefinition>,
debug_panel: WeakEntity<DebugPanel>,
workspace: WeakEntity<Workspace>,
task_store: Option<Entity<TaskStore>>,
task_contexts: TaskContexts,
window: &mut Window,
cx: &mut Context<Self>,
) -> Self {
@@ -73,6 +86,18 @@ impl NewSessionModal {
_ => None,
};
if let Some(task_store) = task_store {
cx.defer_in(window, |this, window, cx| {
this.mode = NewSessionMode::scenario(
this.debug_panel.clone(),
this.workspace.clone(),
task_store,
window,
cx,
);
});
};
Self {
workspace: workspace.clone(),
debugger,
@@ -83,13 +108,14 @@ impl NewSessionModal {
.unwrap_or(ToggleState::Unselected),
last_selected_profile_name: None,
initialize_args: None,
task_contexts: Arc::new(task_contexts),
}
}
fn debug_config(&self, cx: &App, debugger: &str) -> DebugScenario {
let request = self.mode.debug_task(cx);
fn debug_config(&self, cx: &App, debugger: &str) -> Option<DebugScenario> {
let request = self.mode.debug_task(cx)?;
let label = suggested_label(&request, debugger);
DebugScenario {
Some(DebugScenario {
adapter: debugger.to_owned().into(),
label,
request: Some(request),
@@ -100,21 +126,35 @@ impl NewSessionModal {
_ => None,
},
build: None,
}
})
}
fn start_new_session(&self, window: &mut Window, cx: &mut Context<Self>) {
let Some(debugger) = self.debugger.as_ref() else {
// todo: show in UI.
// todo(debugger): show in UI.
log::error!("No debugger selected");
return;
};
let config = self.debug_config(cx, debugger);
let debug_panel = self.debug_panel.clone();
if let NewSessionMode::Scenario(picker) = &self.mode {
picker.update(cx, |picker, cx| {
picker.delegate.confirm(false, window, cx);
});
return;
}
let Some(config) = self.debug_config(cx, debugger) else {
log::error!("debug config not found in mode: {}", self.mode);
return;
};
let debug_panel = self.debug_panel.clone();
let task_contexts = self.task_contexts.clone();
cx.spawn_in(window, async move |this, cx| {
let task_context = task_contexts.active_context().cloned().unwrap_or_default();
let worktree_id = task_contexts.worktree();
debug_panel.update_in(cx, |debug_panel, window, cx| {
debug_panel.start_session(config, TaskContext::default(), None, window, cx)
debug_panel.start_session(config, task_context, None, worktree_id, window, cx)
})?;
this.update(cx, |_, cx| {
cx.emit(DismissEvent);
@@ -127,18 +167,17 @@ impl NewSessionModal {
fn update_attach_picker(
attach: &Entity<AttachMode>,
selected_debugger: &str,
adapter: &DebugAdapterName,
window: &mut Window,
cx: &mut App,
) {
attach.update(cx, |this, cx| {
if selected_debugger != this.definition.adapter.as_ref() {
let adapter: SharedString = selected_debugger.to_owned().into();
if adapter != &this.definition.adapter {
this.definition.adapter = adapter.clone();
this.attach_picker.update(cx, |this, cx| {
this.picker.update(cx, |this, cx| {
this.delegate.definition.adapter = adapter;
this.delegate.definition.adapter = adapter.clone();
this.focus(window, cx);
})
});
@@ -151,18 +190,32 @@ impl NewSessionModal {
&self,
window: &mut Window,
cx: &mut Context<Self>,
) -> ui::DropdownMenu {
) -> Option<ui::DropdownMenu> {
let workspace = self.workspace.clone();
let language_registry = self
.workspace
.update(cx, |this, _| this.app_state().languages.clone())
.ok()?;
let weak = cx.weak_entity();
let debugger = self.debugger.clone();
let label = self
.debugger
.as_ref()
.map(|d| d.0.clone())
.unwrap_or_else(|| SELECT_DEBUGGER_LABEL.clone());
let active_buffer_language_name =
self.task_contexts
.active_item_context
.as_ref()
.and_then(|item| {
item.1
.as_ref()
.and_then(|location| location.buffer.read(cx).language()?.name().into())
});
DropdownMenu::new(
"dap-adapter-picker",
debugger
.as_ref()
.unwrap_or_else(|| &SELECT_DEBUGGER_LABEL)
.clone(),
label,
ContextMenu::build(window, cx, move |mut menu, _, cx| {
let setter_for_name = |name: SharedString| {
let setter_for_name = |name: DebugAdapterName| {
let weak = weak.clone();
move |window: &mut Window, cx: &mut App| {
weak.update(cx, |this, cx| {
@@ -176,17 +229,50 @@ impl NewSessionModal {
}
};
let available_adapters = workspace
let available_languages = language_registry.language_names();
let mut debugger_to_languages = HashMap::default();
for language in available_languages {
let Some(language) =
language_registry.available_language_for_name(language.as_str())
else {
continue;
};
language.config().debuggers.iter().for_each(|adapter| {
debugger_to_languages
.entry(adapter.clone())
.or_insert_with(HashSet::default)
.insert(language.name());
});
}
let mut available_adapters = workspace
.update(cx, |_, cx| DapRegistry::global(cx).enumerate_adapters())
.ok()
.unwrap_or_default();
for adapter in available_adapters {
menu = menu.entry(adapter.0.clone(), None, setter_for_name(adapter.0.clone()));
available_adapters.sort_by_key(|name| {
let languages_for_debugger = debugger_to_languages.get(name.as_ref());
let languages_count =
languages_for_debugger.map_or(0, |languages| languages.len());
let contains_language_of_active_buffer = languages_for_debugger
.zip(active_buffer_language_name.as_ref())
.map_or(false, |(languages, active_buffer_language)| {
languages.contains(active_buffer_language)
});
(
Reverse(contains_language_of_active_buffer),
Reverse(languages_count),
)
});
for adapter in available_adapters.into_iter() {
menu = menu.entry(adapter.0.clone(), None, setter_for_name(adapter.clone()));
}
menu
}),
)
.into()
}
fn debug_config_drop_down_menu(
@@ -211,7 +297,7 @@ impl NewSessionModal {
move |window: &mut Window, cx: &mut App| {
weak.update(cx, |this, cx| {
this.last_selected_profile_name = Some(SharedString::from(&task.label));
this.debugger = Some(task.adapter.clone());
this.debugger = Some(DebugAdapterName(task.adapter.clone()));
this.initialize_args = task.initialize_args.clone();
match &task.request {
Some(DebugRequest::Launch(launch_config)) => {
@@ -256,9 +342,14 @@ impl NewSessionModal {
.iter()
.flat_map(|task_inventory| {
task_inventory.read(cx).list_debug_scenarios(
worktree.as_ref().map(|worktree| worktree.read(cx).id()),
worktree
.as_ref()
.map(|worktree| worktree.read(cx).id())
.iter()
.copied(),
)
})
.map(|(_source_kind, scenario)| scenario)
.collect()
})
.ok()
@@ -277,102 +368,22 @@ impl NewSessionModal {
}
}
#[derive(Clone)]
struct LaunchMode {
program: Entity<Editor>,
cwd: Entity<Editor>,
}
impl LaunchMode {
fn new(
past_launch_config: Option<LaunchRequest>,
window: &mut Window,
cx: &mut App,
) -> Entity<Self> {
let (past_program, past_cwd) = past_launch_config
.map(|config| (Some(config.program), config.cwd))
.unwrap_or_else(|| (None, None));
let program = cx.new(|cx| Editor::single_line(window, cx));
program.update(cx, |this, cx| {
this.set_placeholder_text("Program path", cx);
if let Some(past_program) = past_program {
this.set_text(past_program, window, cx);
};
});
let cwd = cx.new(|cx| Editor::single_line(window, cx));
cwd.update(cx, |this, cx| {
this.set_placeholder_text("Working Directory", cx);
if let Some(past_cwd) = past_cwd {
this.set_text(past_cwd.to_string_lossy(), window, cx);
};
});
cx.new(|_| Self { program, cwd })
}
fn debug_task(&self, cx: &App) -> task::LaunchRequest {
let path = self.cwd.read(cx).text(cx);
task::LaunchRequest {
program: self.program.read(cx).text(cx),
cwd: path.is_empty().not().then(|| PathBuf::from(path)),
args: Default::default(),
env: Default::default(),
}
}
}
#[derive(Clone)]
struct AttachMode {
definition: DebugTaskDefinition,
attach_picker: Entity<AttachModal>,
}
impl AttachMode {
fn new(
debugger: Option<SharedString>,
workspace: Entity<Workspace>,
window: &mut Window,
cx: &mut Context<NewSessionModal>,
) -> Entity<Self> {
let definition = DebugTaskDefinition {
adapter: debugger.clone().unwrap_or_default(),
label: "Attach New Session Setup".into(),
request: dap::DebugRequest::Attach(task::AttachRequest { process_id: None }),
initialize_args: None,
tcp_connection: None,
stop_on_entry: Some(false),
};
let attach_picker = cx.new(|cx| {
let modal = AttachModal::new(definition.clone(), workspace, false, window, cx);
window.focus(&modal.focus_handle(cx));
modal
});
cx.new(|_| Self {
definition,
attach_picker,
})
}
fn debug_task(&self) -> task::AttachRequest {
task::AttachRequest { process_id: None }
}
}
static SELECT_DEBUGGER_LABEL: SharedString = SharedString::new_static("Select Debugger");
static SELECT_SCENARIO_LABEL: SharedString = SharedString::new_static("Select Profile");
#[derive(Clone)]
enum NewSessionMode {
Launch(Entity<LaunchMode>),
Scenario(Entity<Picker<DebugScenarioDelegate>>),
Attach(Entity<AttachMode>),
}
impl NewSessionMode {
fn debug_task(&self, cx: &App) -> DebugRequest {
fn debug_task(&self, cx: &App) -> Option<DebugRequest> {
match self {
NewSessionMode::Launch(entity) => entity.read(cx).debug_task(cx).into(),
NewSessionMode::Attach(entity) => entity.read(cx).debug_task().into(),
NewSessionMode::Launch(entity) => Some(entity.read(cx).debug_task(cx).into()),
NewSessionMode::Attach(entity) => Some(entity.read(cx).debug_task().into()),
NewSessionMode::Scenario(_) => None,
}
}
fn as_attach(&self) -> Option<&Entity<AttachMode>> {
@@ -382,6 +393,78 @@ impl NewSessionMode {
None
}
}
fn scenario(
debug_panel: WeakEntity<DebugPanel>,
workspace: WeakEntity<Workspace>,
task_store: Entity<TaskStore>,
window: &mut Window,
cx: &mut Context<NewSessionModal>,
) -> NewSessionMode {
let picker = cx.new(|cx| {
Picker::uniform_list(
DebugScenarioDelegate::new(debug_panel, workspace, task_store),
window,
cx,
)
.modal(false)
});
cx.subscribe(&picker, |_, _, _, cx| {
cx.emit(DismissEvent);
})
.detach();
picker.focus_handle(cx).focus(window);
NewSessionMode::Scenario(picker)
}
fn attach(
debugger: Option<DebugAdapterName>,
workspace: Entity<Workspace>,
window: &mut Window,
cx: &mut Context<NewSessionModal>,
) -> Self {
Self::Attach(AttachMode::new(debugger, workspace, window, cx))
}
fn launch(
past_launch_config: Option<LaunchRequest>,
window: &mut Window,
cx: &mut Context<NewSessionModal>,
) -> Self {
Self::Launch(LaunchMode::new(past_launch_config, window, cx))
}
fn has_match(&self, cx: &App) -> bool {
match self {
NewSessionMode::Scenario(picker) => picker.read(cx).delegate.match_count() > 0,
NewSessionMode::Attach(picker) => {
picker
.read(cx)
.attach_picker
.read(cx)
.picker
.read(cx)
.delegate
.match_count()
> 0
}
_ => false,
}
}
}
impl std::fmt::Display for NewSessionMode {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let mode = match self {
NewSessionMode::Launch(_) => "launch".to_owned(),
NewSessionMode::Attach(_) => "attach".to_owned(),
NewSessionMode::Scenario(_) => "scenario picker".to_owned(),
};
write!(f, "{}", mode)
}
}
impl Focusable for NewSessionMode {
@@ -389,45 +472,11 @@ impl Focusable for NewSessionMode {
match &self {
NewSessionMode::Launch(entity) => entity.read(cx).program.focus_handle(cx),
NewSessionMode::Attach(entity) => entity.read(cx).attach_picker.focus_handle(cx),
NewSessionMode::Scenario(entity) => entity.read(cx).focus_handle(cx),
}
}
}
impl RenderOnce for LaunchMode {
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
v_flex()
.p_2()
.w_full()
.gap_3()
.track_focus(&self.program.focus_handle(cx))
.child(
div().child(
Label::new("Program")
.size(ui::LabelSize::Small)
.color(Color::Muted),
),
)
.child(render_editor(&self.program, window, cx))
.child(
div().child(
Label::new("Working Directory")
.size(ui::LabelSize::Small)
.color(Color::Muted),
),
)
.child(render_editor(&self.cwd, window, cx))
}
}
impl RenderOnce for AttachMode {
fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
v_flex()
.w_full()
.track_focus(&self.attach_picker.focus_handle(cx))
.child(self.attach_picker.clone())
}
}
impl RenderOnce for NewSessionMode {
fn render(self, window: &mut Window, cx: &mut App) -> impl ui::IntoElement {
match self {
@@ -437,27 +486,14 @@ impl RenderOnce for NewSessionMode {
NewSessionMode::Attach(entity) => entity.update(cx, |this, cx| {
this.clone().render(window, cx).into_any_element()
}),
NewSessionMode::Scenario(entity) => v_flex()
.w(rems(34.))
.child(entity.clone())
.into_any_element(),
}
}
}
impl NewSessionMode {
fn attach(
debugger: Option<SharedString>,
workspace: Entity<Workspace>,
window: &mut Window,
cx: &mut Context<NewSessionModal>,
) -> Self {
Self::Attach(AttachMode::new(debugger, workspace, window, cx))
}
fn launch(
past_launch_config: Option<LaunchRequest>,
window: &mut Window,
cx: &mut Context<NewSessionModal>,
) -> Self {
Self::Launch(LaunchMode::new(past_launch_config, window, cx))
}
}
fn render_editor(editor: &Entity<Editor>, window: &mut Window, cx: &App) -> impl IntoElement {
let settings = ThemeSettings::get_global(cx);
let theme = cx.theme();
@@ -519,6 +555,34 @@ impl Render for NewSessionModal {
h_flex()
.justify_start()
.w_full()
.child(
ToggleButton::new("debugger-session-ui-picker-button", "Scenarios")
.size(ButtonSize::Default)
.style(ui::ButtonStyle::Subtle)
.toggle_state(matches!(self.mode, NewSessionMode::Scenario(_)))
.on_click(cx.listener(|this, _, window, cx| {
let Some(task_store) = this
.workspace
.update(cx, |workspace, cx| {
workspace.project().read(cx).task_store().clone()
})
.ok()
else {
return;
};
this.mode = NewSessionMode::scenario(
this.debug_panel.clone(),
this.workspace.clone(),
task_store,
window,
cx,
);
cx.notify();
}))
.first(),
)
.child(
ToggleButton::new(
"debugger-session-ui-launch-button",
@@ -532,7 +596,7 @@ impl Render for NewSessionModal {
this.mode.focus_handle(cx).focus(window);
cx.notify();
}))
.first(),
.middle(),
)
.child(
ToggleButton::new(
@@ -565,7 +629,9 @@ impl Render for NewSessionModal {
),
)
.justify_between()
.child(self.adapter_drop_down_menu(window, cx))
.when(!matches!(self.mode, NewSessionMode::Scenario(_)), |this| {
this.children(self.adapter_drop_down_menu(window, cx))
})
.border_color(cx.theme().colors().border_variant)
.border_b_1(),
)
@@ -601,10 +667,21 @@ impl Render for NewSessionModal {
})
.child(
Button::new("debugger-spawn", "Start")
.on_click(cx.listener(|this, _, window, cx| {
this.start_new_session(window, cx);
.on_click(cx.listener(|this, _, window, cx| match &this.mode {
NewSessionMode::Scenario(picker) => {
picker.update(cx, |picker, cx| {
picker.delegate.confirm(true, window, cx)
})
}
_ => this.start_new_session(window, cx),
}))
.disabled(self.debugger.is_none()),
.disabled(match self.mode {
NewSessionMode::Scenario(_) => !self.mode.has_match(cx),
NewSessionMode::Attach(_) => {
self.debugger.is_none() || !self.mode.has_match(cx)
}
NewSessionMode::Launch(_) => self.debugger.is_none(),
}),
),
),
)
@@ -619,3 +696,351 @@ impl Focusable for NewSessionModal {
}
impl ModalView for NewSessionModal {}
impl RenderOnce for LaunchMode {
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
v_flex()
.p_2()
.w_full()
.gap_3()
.track_focus(&self.program.focus_handle(cx))
.child(
div().child(
Label::new("Program")
.size(ui::LabelSize::Small)
.color(Color::Muted),
),
)
.child(render_editor(&self.program, window, cx))
.child(
div().child(
Label::new("Working Directory")
.size(ui::LabelSize::Small)
.color(Color::Muted),
),
)
.child(render_editor(&self.cwd, window, cx))
}
}
impl RenderOnce for AttachMode {
fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
v_flex()
.w_full()
.track_focus(&self.attach_picker.focus_handle(cx))
.child(self.attach_picker.clone())
}
}
use std::rc::Rc;
#[derive(Clone)]
pub(super) struct LaunchMode {
program: Entity<Editor>,
cwd: Entity<Editor>,
}
impl LaunchMode {
pub(super) fn new(
past_launch_config: Option<LaunchRequest>,
window: &mut Window,
cx: &mut App,
) -> Entity<Self> {
let (past_program, past_cwd) = past_launch_config
.map(|config| (Some(config.program), config.cwd))
.unwrap_or_else(|| (None, None));
let program = cx.new(|cx| Editor::single_line(window, cx));
program.update(cx, |this, cx| {
this.set_placeholder_text("Program path", cx);
if let Some(past_program) = past_program {
this.set_text(past_program, window, cx);
};
});
let cwd = cx.new(|cx| Editor::single_line(window, cx));
cwd.update(cx, |this, cx| {
this.set_placeholder_text("Working Directory", cx);
if let Some(past_cwd) = past_cwd {
this.set_text(past_cwd.to_string_lossy(), window, cx);
};
});
cx.new(|_| Self { program, cwd })
}
pub(super) fn debug_task(&self, cx: &App) -> task::LaunchRequest {
let path = self.cwd.read(cx).text(cx);
task::LaunchRequest {
program: self.program.read(cx).text(cx),
cwd: path.is_empty().not().then(|| PathBuf::from(path)),
args: Default::default(),
env: Default::default(),
}
}
}
#[derive(Clone)]
pub(super) struct AttachMode {
pub(super) definition: DebugTaskDefinition,
pub(super) attach_picker: Entity<AttachModal>,
_subscription: Rc<Subscription>,
}
impl AttachMode {
pub(super) fn new(
debugger: Option<DebugAdapterName>,
workspace: Entity<Workspace>,
window: &mut Window,
cx: &mut Context<NewSessionModal>,
) -> Entity<Self> {
let definition = DebugTaskDefinition {
adapter: debugger.unwrap_or(DebugAdapterName("".into())),
label: "Attach New Session Setup".into(),
request: dap::DebugRequest::Attach(task::AttachRequest { process_id: None }),
initialize_args: None,
tcp_connection: None,
stop_on_entry: Some(false),
};
let attach_picker = cx.new(|cx| {
let modal = AttachModal::new(definition.clone(), workspace, false, window, cx);
window.focus(&modal.focus_handle(cx));
modal
});
let subscription = cx.subscribe(&attach_picker, |_, _, _, cx| {
cx.emit(DismissEvent);
});
cx.new(|_| Self {
definition,
attach_picker,
_subscription: Rc::new(subscription),
})
}
pub(super) fn debug_task(&self) -> task::AttachRequest {
task::AttachRequest { process_id: None }
}
}
pub(super) struct DebugScenarioDelegate {
task_store: Entity<TaskStore>,
candidates: Option<Vec<(TaskSourceKind, DebugScenario)>>,
selected_index: usize,
matches: Vec<StringMatch>,
prompt: String,
debug_panel: WeakEntity<DebugPanel>,
workspace: WeakEntity<Workspace>,
}
impl DebugScenarioDelegate {
pub(super) fn new(
debug_panel: WeakEntity<DebugPanel>,
workspace: WeakEntity<Workspace>,
task_store: Entity<TaskStore>,
) -> Self {
Self {
task_store,
candidates: None,
selected_index: 0,
matches: Vec::new(),
prompt: String::new(),
debug_panel,
workspace,
}
}
}
impl PickerDelegate for DebugScenarioDelegate {
type ListItem = ui::ListItem;
fn match_count(&self) -> usize {
self.matches.len()
}
fn selected_index(&self) -> usize {
self.selected_index
}
fn set_selected_index(
&mut self,
ix: usize,
_window: &mut Window,
_cx: &mut Context<picker::Picker<Self>>,
) {
self.selected_index = ix;
}
fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> std::sync::Arc<str> {
"".into()
}
fn update_matches(
&mut self,
query: String,
window: &mut Window,
cx: &mut Context<picker::Picker<Self>>,
) -> gpui::Task<()> {
let candidates: Vec<_> = match &self.candidates {
Some(candidates) => candidates
.into_iter()
.enumerate()
.map(|(index, (_, candidate))| {
StringMatchCandidate::new(index, candidate.label.as_ref())
})
.collect(),
None => {
let worktree_ids: Vec<_> = self
.workspace
.update(cx, |this, cx| {
this.visible_worktrees(cx)
.map(|tree| tree.read(cx).id())
.collect()
})
.ok()
.unwrap_or_default();
let scenarios: Vec<_> = self
.task_store
.read(cx)
.task_inventory()
.map(|item| item.read(cx).list_debug_scenarios(worktree_ids.into_iter()))
.unwrap_or_default();
self.candidates = Some(scenarios.clone());
scenarios
.into_iter()
.enumerate()
.map(|(index, (_, candidate))| {
StringMatchCandidate::new(index, candidate.label.as_ref())
})
.collect()
}
};
cx.spawn_in(window, async move |picker, cx| {
let matches = fuzzy::match_strings(
&candidates,
&query,
true,
1000,
&Default::default(),
cx.background_executor().clone(),
)
.await;
picker
.update(cx, |picker, _| {
let delegate = &mut picker.delegate;
delegate.matches = matches;
delegate.prompt = query;
if delegate.matches.is_empty() {
delegate.selected_index = 0;
} else {
delegate.selected_index =
delegate.selected_index.min(delegate.matches.len() - 1);
}
})
.log_err();
})
}
fn confirm(&mut self, _: bool, window: &mut Window, cx: &mut Context<picker::Picker<Self>>) {
let debug_scenario = self
.matches
.get(self.selected_index())
.and_then(|match_candidate| {
self.candidates
.as_ref()
.map(|candidates| candidates[match_candidate.candidate_id].clone())
});
let Some((task_source_kind, debug_scenario)) = debug_scenario else {
return;
};
let task_context = if let TaskSourceKind::Worktree {
id: worktree_id,
directory_in_worktree: _,
id_base: _,
} = task_source_kind
{
let workspace = self.workspace.clone();
cx.spawn_in(window, async move |_, cx| {
workspace
.update_in(cx, |workspace, window, cx| {
tasks_ui::task_contexts(workspace, window, cx)
})
.ok()?
.await
.task_context_for_worktree_id(worktree_id)
.cloned()
.map(|context| (context, Some(worktree_id)))
})
} else {
gpui::Task::ready(None)
};
cx.spawn_in(window, async move |this, cx| {
let (task_context, worktree_id) = task_context.await.unwrap_or_default();
this.update_in(cx, |this, window, cx| {
this.delegate
.debug_panel
.update(cx, |panel, cx| {
panel.start_session(
debug_scenario,
task_context,
None,
worktree_id,
window,
cx,
);
})
.ok();
cx.emit(DismissEvent);
})
.ok();
})
.detach();
}
fn dismissed(&mut self, _: &mut Window, cx: &mut Context<picker::Picker<Self>>) {
cx.emit(DismissEvent);
}
fn render_match(
&self,
ix: usize,
selected: bool,
window: &mut Window,
cx: &mut Context<picker::Picker<Self>>,
) -> Option<Self::ListItem> {
let hit = &self.matches[ix];
let highlighted_location = HighlightedMatch {
text: hit.string.clone(),
highlight_positions: hit.positions.clone(),
char_count: hit.string.chars().count(),
color: Color::Default,
};
let icon = Icon::new(IconName::FileTree)
.color(Color::Muted)
.size(ui::IconSize::Small);
Some(
ListItem::new(SharedString::from(format!("debug-scenario-selection-{ix}")))
.inset(true)
.start_slot::<Icon>(icon)
.spacing(ListItemSpacing::Sparse)
.toggle_state(selected)
.child(highlighted_location.render(window, cx)),
)
}
}

View File

@@ -1,5 +1,5 @@
use collections::HashMap;
use dap::Capabilities;
use dap::{Capabilities, adapters::DebugAdapterName};
use db::kvp::KEY_VALUE_STORE;
use gpui::{Axis, Context, Entity, EntityId, Focusable, Subscription, WeakEntity, Window};
use project::Project;
@@ -69,19 +69,22 @@ impl From<DebuggerPaneItem> for SharedString {
}
#[derive(Debug, Serialize, Deserialize)]
pub(crate) struct SerializedAxis(pub Axis);
pub(crate) struct SerializedLayout {
pub(crate) panes: SerializedPaneLayout,
pub(crate) dock_axis: Axis,
}
#[derive(Debug, Serialize, Deserialize)]
#[derive(Debug, Serialize, Deserialize, Clone)]
pub(crate) enum SerializedPaneLayout {
Pane(SerializedPane),
Group {
axis: SerializedAxis,
axis: Axis,
flexes: Option<Vec<f32>>,
children: Vec<SerializedPaneLayout>,
},
}
#[derive(Debug, Serialize, Deserialize)]
#[derive(Debug, Serialize, Deserialize, Clone)]
pub(crate) struct SerializedPane {
pub children: Vec<DebuggerPaneItem>,
pub active_item: Option<DebuggerPaneItem>,
@@ -90,8 +93,8 @@ pub(crate) struct SerializedPane {
const DEBUGGER_PANEL_PREFIX: &str = "debugger_panel_";
pub(crate) async fn serialize_pane_layout(
adapter_name: SharedString,
pane_group: SerializedPaneLayout,
adapter_name: DebugAdapterName,
pane_group: SerializedLayout,
) -> anyhow::Result<()> {
if let Ok(serialized_pane_group) = serde_json::to_string(&pane_group) {
KEY_VALUE_STORE
@@ -107,10 +110,18 @@ pub(crate) async fn serialize_pane_layout(
}
}
pub(crate) fn build_serialized_pane_layout(
pub(crate) fn build_serialized_layout(
pane_group: &Member,
cx: &mut App,
) -> SerializedPaneLayout {
dock_axis: Axis,
cx: &App,
) -> SerializedLayout {
SerializedLayout {
dock_axis,
panes: build_serialized_pane_layout(pane_group, cx),
}
}
pub(crate) fn build_serialized_pane_layout(pane_group: &Member, cx: &App) -> SerializedPaneLayout {
match pane_group {
Member::Axis(PaneAxis {
axis,
@@ -118,7 +129,7 @@ pub(crate) fn build_serialized_pane_layout(
flexes,
bounding_boxes: _,
}) => SerializedPaneLayout::Group {
axis: SerializedAxis(*axis),
axis: *axis,
children: members
.iter()
.map(|member| build_serialized_pane_layout(member, cx))
@@ -129,7 +140,7 @@ pub(crate) fn build_serialized_pane_layout(
}
}
fn serialize_pane(pane: &Entity<Pane>, cx: &mut App) -> SerializedPane {
fn serialize_pane(pane: &Entity<Pane>, cx: &App) -> SerializedPane {
let pane = pane.read(cx);
let children = pane
.items()
@@ -150,20 +161,21 @@ fn serialize_pane(pane: &Entity<Pane>, cx: &mut App) -> SerializedPane {
}
}
pub(crate) async fn get_serialized_pane_layout(
pub(crate) async fn get_serialized_layout(
adapter_name: impl AsRef<str>,
) -> Option<SerializedPaneLayout> {
) -> Option<SerializedLayout> {
let key = format!("{DEBUGGER_PANEL_PREFIX}-{}", adapter_name.as_ref());
KEY_VALUE_STORE
.read_kvp(&key)
.log_err()
.flatten()
.and_then(|value| serde_json::from_str::<SerializedPaneLayout>(&value).ok())
.and_then(|value| serde_json::from_str::<SerializedLayout>(&value).ok())
}
pub(crate) fn deserialize_pane_layout(
serialized: SerializedPaneLayout,
should_invert: bool,
workspace: &WeakEntity<Workspace>,
project: &Entity<Project>,
stack_frame_list: &Entity<StackFrameList>,
@@ -187,6 +199,7 @@ pub(crate) fn deserialize_pane_layout(
for child in children {
if let Some(new_member) = deserialize_pane_layout(
child,
should_invert,
workspace,
project,
stack_frame_list,
@@ -213,7 +226,7 @@ pub(crate) fn deserialize_pane_layout(
}
Some(Member::Axis(PaneAxis::load(
axis.0,
if should_invert { axis.invert() } else { axis },
members,
flexes.clone(),
)))
@@ -307,3 +320,28 @@ pub(crate) fn deserialize_pane_layout(
}
}
}
#[cfg(test)]
impl SerializedPaneLayout {
pub(crate) fn in_order(&self) -> Vec<SerializedPaneLayout> {
let mut panes = vec![];
Self::inner_in_order(&self, &mut panes);
panes
}
fn inner_in_order(&self, panes: &mut Vec<SerializedPaneLayout>) {
match self {
SerializedPaneLayout::Pane(_) => panes.push((*self).clone()),
SerializedPaneLayout::Group {
axis: _,
flexes: _,
children,
} => {
for child in children {
child.inner_in_order(panes);
}
}
}
}
}

View File

@@ -3,7 +3,9 @@ pub mod running;
use std::sync::OnceLock;
use dap::client::SessionId;
use gpui::{App, Entity, EventEmitter, FocusHandle, Focusable, Subscription, Task, WeakEntity};
use gpui::{
App, Axis, Entity, EventEmitter, FocusHandle, Focusable, Subscription, Task, WeakEntity,
};
use project::Project;
use project::debugger::session::Session;
use project::worktree_store::WorktreeStore;
@@ -15,8 +17,7 @@ use workspace::{
item::{self, Item},
};
use crate::debugger_panel::DebugPanel;
use crate::persistence::SerializedPaneLayout;
use crate::{debugger_panel::DebugPanel, persistence::SerializedLayout};
pub struct DebugSession {
remote_id: Option<workspace::ViewId>,
@@ -40,7 +41,8 @@ impl DebugSession {
workspace: WeakEntity<Workspace>,
session: Entity<Session>,
_debug_panel: WeakEntity<DebugPanel>,
serialized_pane_layout: Option<SerializedPaneLayout>,
serialized_layout: Option<SerializedLayout>,
dock_axis: Axis,
window: &mut Window,
cx: &mut App,
) -> Entity<Self> {
@@ -49,7 +51,8 @@ impl DebugSession {
session.clone(),
project.clone(),
workspace.clone(),
serialized_pane_layout,
serialized_layout,
dock_axis,
window,
cx,
)

View File

@@ -7,7 +7,7 @@ pub mod variable_list;
use std::{any::Any, ops::ControlFlow, path::PathBuf, sync::Arc, time::Duration};
use crate::persistence::{self, DebuggerPaneItem, SerializedPaneLayout};
use crate::persistence::{self, DebuggerPaneItem, SerializedLayout};
use super::DebugPanelItemEvent;
use anyhow::{Result, anyhow};
@@ -15,18 +15,21 @@ use breakpoint_list::BreakpointList;
use collections::{HashMap, IndexMap};
use console::Console;
use dap::{
Capabilities, RunInTerminalRequestArguments, Thread, client::SessionId,
Capabilities, RunInTerminalRequestArguments, Thread,
adapters::{DebugAdapterName, DebugTaskDefinition},
client::SessionId,
debugger_settings::DebuggerSettings,
};
use futures::{SinkExt, channel::mpsc};
use gpui::{
Action as _, AnyView, AppContext, Entity, EntityId, EventEmitter, FocusHandle, Focusable,
Action as _, AnyView, AppContext, Axis, Entity, EntityId, EventEmitter, FocusHandle, Focusable,
NoAction, Pixels, Point, Subscription, Task, WeakEntity,
};
use language::Buffer;
use loaded_source_list::LoadedSourceList;
use module_list::ModuleList;
use project::{
Project,
Project, WorktreeId,
debugger::session::{Session, SessionEvent, ThreadId, ThreadStatus},
terminals::TerminalKind,
};
@@ -34,6 +37,10 @@ use rpc::proto::ViewId;
use serde_json::Value;
use settings::Settings;
use stack_frame_list::StackFrameList;
use task::{
BuildTaskDefinition, DebugScenario, LaunchRequest, ShellBuilder, SpawnInTerminal, TaskContext,
substitute_variables_in_map, substitute_variables_in_str,
};
use terminal_view::TerminalView;
use ui::{
ActiveTheme, AnyElement, App, ButtonCommon as _, Clickable as _, Context, ContextMenu,
@@ -66,6 +73,7 @@ pub struct RunningState {
panes: PaneGroup,
active_pane: Option<Entity<Pane>>,
pane_close_subscriptions: HashMap<EntityId, Subscription>,
dock_axis: Axis,
_schedule_serialize: Option<Task<()>>,
}
@@ -188,7 +196,7 @@ impl Render for SubView {
cx.notify();
}))
.size_full()
// Add border uncoditionally to prevent layout shifts on focus changes.
// Add border unconditionally to prevent layout shifts on focus changes.
.border_1()
.when(self.pane_focus_handle.contains_focused(window, cx), |el| {
el.border_color(cx.theme().colors().pane_focused_border)
@@ -503,7 +511,8 @@ impl RunningState {
session: Entity<Session>,
project: Entity<Project>,
workspace: WeakEntity<Workspace>,
serialized_pane_layout: Option<SerializedPaneLayout>,
serialized_pane_layout: Option<SerializedLayout>,
dock_axis: Axis,
window: &mut Window,
cx: &mut Context<Self>,
) -> Self {
@@ -582,7 +591,8 @@ impl RunningState {
let mut pane_close_subscriptions = HashMap::default();
let panes = if let Some(root) = serialized_pane_layout.and_then(|serialized_layout| {
persistence::deserialize_pane_layout(
serialized_layout,
serialized_layout.panes,
dock_axis != serialized_layout.dock_axis,
&workspace,
&project,
&stack_frame_list,
@@ -610,6 +620,7 @@ impl RunningState {
&loaded_source_list,
&console,
&breakpoint_list,
dock_axis,
&mut pane_close_subscriptions,
window,
cx,
@@ -636,6 +647,7 @@ impl RunningState {
loaded_sources_list: loaded_source_list,
pane_close_subscriptions,
debug_terminal,
dock_axis,
_schedule_serialize: None,
}
}
@@ -667,6 +679,196 @@ impl RunningState {
self.panes.pane_at_pixel_position(position).is_some()
}
pub(crate) fn resolve_scenario(
&self,
scenario: DebugScenario,
task_context: TaskContext,
buffer: Option<Entity<Buffer>>,
worktree_id: Option<WorktreeId>,
window: &Window,
cx: &mut Context<Self>,
) -> Task<Result<DebugTaskDefinition>> {
let Some(workspace) = self.workspace.upgrade() else {
return Task::ready(Err(anyhow!("no workspace")));
};
let project = workspace.read(cx).project().clone();
let dap_store = project.read(cx).dap_store().downgrade();
let task_store = project.read(cx).task_store().downgrade();
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,
label,
build,
request,
initialize_args,
tcp_connection,
stop_on_entry,
} = scenario;
let build_output = if let Some(build) = build {
let (task, locator_name) = match build {
BuildTaskDefinition::Template {
task_template,
locator_name,
} => (task_template, locator_name),
BuildTaskDefinition::ByName(ref label) => {
let Some(task) = task_store.update(cx, |this, cx| {
this.task_inventory().and_then(|inventory| {
inventory.read(cx).task_template_by_label(
buffer,
worktree_id,
&label,
cx,
)
})
})?
else {
anyhow::bail!("Couldn't find task template for {:?}", build)
};
(task, None)
}
};
let locator_name = if let Some(locator_name) = locator_name {
debug_assert!(request.is_none());
Some(locator_name)
} else if request.is_none() {
dap_store
.update(cx, |this, cx| {
this.debug_scenario_for_build_task(task.clone(), adapter.clone(), cx)
.and_then(|scenario| match scenario.build {
Some(BuildTaskDefinition::Template {
locator_name, ..
}) => locator_name,
_ => None,
})
})
.ok()
.flatten()
} else {
None
};
let Some(task) = task.resolve_task("debug-build-task", &task_context) else {
anyhow::bail!("Could not resolve task variables within a debug scenario");
};
let builder = ShellBuilder::new(is_local, &task.resolved.shell);
let command_label = builder.command_label(&task.resolved.command_label);
let (command, args) =
builder.build(task.resolved.command.clone(), &task.resolved.args);
let task_with_shell = SpawnInTerminal {
command_label,
command,
args,
..task.resolved.clone()
};
let terminal = project
.update_in(cx, |project, window, cx| {
project.create_terminal(
TerminalKind::Task(task_with_shell.clone()),
window.window_handle(),
cx,
)
})?
.await?;
let terminal_view = cx.new_window_entity(|window, cx| {
TerminalView::new(
terminal.clone(),
weak_workspace,
None,
weak_project,
false,
window,
cx,
)
})?;
this.update_in(cx, |this, window, cx| {
this.ensure_pane_item(DebuggerPaneItem::Terminal, window, cx);
this.debug_terminal.update(cx, |debug_terminal, cx| {
debug_terminal.terminal = Some(terminal_view);
cx.notify();
});
})?;
let exit_status = terminal
.read_with(cx, |terminal, cx| terminal.wait_for_completed_task(cx))?
.await
.ok_or_else(|| anyhow!("Failed to wait for completed task"))?;
if !exit_status.success() {
anyhow::bail!("Build failed");
}
Some((task.resolved.clone(), locator_name))
} else {
None
};
let request = if let Some(request) = request {
request
} else if let Some((task, locator_name)) = build_output {
let locator_name = locator_name
.ok_or_else(|| anyhow!("Could not find a valid locator for a build task"))?;
dap_store
.update(cx, |this, cx| {
this.run_debug_locator(&locator_name, task, cx)
})?
.await?
} else {
return Err(anyhow!("No request or build provided"));
};
let request = match request {
dap::DebugRequest::Launch(launch_request) => {
let cwd = match launch_request.cwd.as_deref().and_then(|path| path.to_str()) {
Some(cwd) => {
let substituted_cwd = substitute_variables_in_str(&cwd, &task_context)
.ok_or_else(|| anyhow!("Failed to substitute variables in cwd"))?;
Some(PathBuf::from(substituted_cwd))
}
None => None,
};
let env = substitute_variables_in_map(
&launch_request.env.into_iter().collect(),
&task_context,
)
.ok_or_else(|| anyhow!("Failed to substitute variables in env"))?
.into_iter()
.collect();
let new_launch_request = LaunchRequest {
program: substitute_variables_in_str(
&launch_request.program,
&task_context,
)
.ok_or_else(|| anyhow!("Failed to substitute variables in program"))?,
args: launch_request
.args
.into_iter()
.map(|arg| substitute_variables_in_str(&arg, &task_context))
.collect::<Option<Vec<_>>>()
.ok_or_else(|| anyhow!("Failed to substitute variables in args"))?,
cwd,
env,
};
dap::DebugRequest::Launch(new_launch_request)
}
request @ dap::DebugRequest::Attach(_) => request,
};
Ok(DebugTaskDefinition {
label,
adapter: DebugAdapterName(adapter),
request,
initialize_args,
stop_on_entry,
tcp_connection,
})
})
}
fn handle_run_in_terminal(
&self,
request: &RunInTerminalRequestArguments,
@@ -912,12 +1114,16 @@ impl RunningState {
.timer(Duration::from_millis(100))
.await;
let Some((adapter_name, pane_group)) = this
.update(cx, |this, cx| {
let adapter_name = this.session.read(cx).adapter_name();
let Some((adapter_name, pane_layout)) = this
.read_with(cx, |this, cx| {
let adapter_name = this.session.read(cx).adapter();
(
adapter_name,
persistence::build_serialized_pane_layout(&this.panes.root, cx),
persistence::build_serialized_layout(
&this.panes.root,
this.dock_axis,
cx,
),
)
})
.ok()
@@ -925,7 +1131,7 @@ impl RunningState {
return;
};
persistence::serialize_pane_layout(adapter_name, pane_group)
persistence::serialize_pane_layout(adapter_name, pane_layout)
.await
.log_err();
@@ -1051,6 +1257,11 @@ impl RunningState {
&self.variable_list
}
#[cfg(test)]
pub(crate) fn serialized_layout(&self, cx: &App) -> SerializedLayout {
persistence::build_serialized_layout(&self.panes.root, self.dock_axis, cx)
}
pub fn capabilities(&self, cx: &App) -> Capabilities {
self.session().read(cx).capabilities().clone()
}
@@ -1264,6 +1475,7 @@ impl RunningState {
loaded_source_list: &Entity<LoadedSourceList>,
console: &Entity<Console>,
breakpoints: &Entity<BreakpointList>,
dock_axis: Axis,
subscriptions: &mut HashMap<EntityId, Subscription>,
window: &mut Window,
cx: &mut Context<'_, RunningState>,
@@ -1384,7 +1596,7 @@ impl RunningState {
);
let group_root = workspace::PaneAxis::new(
gpui::Axis::Horizontal,
dock_axis.invert(),
[leftmost_pane, center_pane, rightmost_pane]
.into_iter()
.map(workspace::Member::Pane)
@@ -1393,6 +1605,11 @@ impl RunningState {
Member::Axis(group_root)
}
pub(crate) fn invert_axies(&mut self) {
self.dock_axis = self.dock_axis.invert();
self.panes.invert_axies();
}
}
impl EventEmitter<DebugPanelItemEvent> for RunningState {}

View File

@@ -148,23 +148,6 @@ impl Console {
expression
});
self.add_messages(
[OutputEvent {
category: None,
output: format!("> {expression}"),
group: None,
variables_reference: None,
source: None,
line: None,
column: None,
data: None,
location_reference: None,
}]
.iter(),
window,
cx,
);
self.session.update(cx, |session, cx| {
session
.evaluate(

View File

@@ -23,6 +23,8 @@ mod debugger_panel;
#[cfg(test)]
mod module_list;
#[cfg(test)]
mod persistence;
#[cfg(test)]
mod stack_frame_list;
#[cfg(test)]
mod variable_list;

View File

@@ -444,11 +444,13 @@ async fn test_handle_start_debugging_request(
.read(cx)
.session(cx);
let parent_session = active_session.read(cx).parent_session().unwrap();
let mut original_binary = parent_session.read(cx).binary().clone();
original_binary.request_args = StartDebuggingRequestArguments {
request: StartDebuggingRequestArgumentsRequest::Launch,
configuration: fake_config.clone(),
};
assert_eq!(
active_session.read(cx).definition(),
parent_session.read(cx).definition()
);
assert_eq!(active_session.read(cx).binary(), &original_binary);
})
.unwrap();
@@ -1663,6 +1665,33 @@ async fn test_active_debug_line_setting(executor: BackgroundExecutor, cx: &mut T
"Second stacktrace request handler was not called"
);
client
.fake_event(dap::messages::Events::Continued(dap::ContinuedEvent {
thread_id: 0,
all_threads_continued: Some(true),
}))
.await;
cx.run_until_parked();
second_editor.update(cx, |editor, _| {
let active_debug_lines: Vec<_> = editor.highlighted_rows::<ActiveDebugLine>().collect();
assert!(
active_debug_lines.is_empty(),
"There shouldn't be any active debug lines"
);
});
main_editor.update(cx, |editor, _| {
let active_debug_lines: Vec<_> = editor.highlighted_rows::<ActiveDebugLine>().collect();
assert!(
active_debug_lines.is_empty(),
"There shouldn't be any active debug lines"
);
});
// Clean up
let shutdown_session = project.update(cx, |project, cx| {
project.dap_store().update(cx, |dap_store, cx| {

View File

@@ -0,0 +1,131 @@
use std::iter::zip;
use crate::{
debugger_panel::DebugPanel,
persistence::SerializedPaneLayout,
tests::{init_test, init_test_workspace, start_debug_session},
};
use dap::{StoppedEvent, StoppedEventReason, messages::Events};
use gpui::{BackgroundExecutor, TestAppContext, VisualTestContext};
use project::{FakeFs, Project};
use serde_json::json;
use util::path;
use workspace::{Panel, dock::DockPosition};
#[gpui::test]
async fn test_invert_axis_on_panel_position_change(
executor: BackgroundExecutor,
cx: &mut TestAppContext,
) {
init_test(cx);
let fs = FakeFs::new(executor.clone());
fs.insert_tree(
path!("/project"),
json!({
"main.rs": "fn main() {\n println!(\"Hello, world!\");\n}",
}),
)
.await;
let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
let workspace = init_test_workspace(&project, cx).await;
let cx = &mut VisualTestContext::from_window(*workspace, cx);
// Start a debug session
let session = start_debug_session(&workspace, cx, |_| {}).unwrap();
let client = session.update(cx, |session, _| session.adapter_client().unwrap());
// Setup thread response
client.on_request::<dap::requests::Threads, _>(move |_, _| {
Ok(dap::ThreadsResponse { threads: vec![] })
});
cx.run_until_parked();
client
.fake_event(Events::Stopped(StoppedEvent {
reason: StoppedEventReason::Pause,
description: None,
thread_id: Some(1),
preserve_focus_hint: None,
text: None,
all_threads_stopped: None,
hit_breakpoint_ids: None,
}))
.await;
cx.run_until_parked();
let (debug_panel, dock_position) = workspace
.update(cx, |workspace, window, cx| {
let debug_panel = workspace.panel::<DebugPanel>(cx).unwrap();
let dock_position = debug_panel.read(cx).position(window, cx);
(debug_panel, dock_position)
})
.unwrap();
assert_eq!(
dock_position,
DockPosition::Bottom,
"Default dock position should be bottom for debug panel"
);
let pre_serialized_layout = debug_panel
.read_with(cx, |panel, cx| {
panel
.active_session()
.unwrap()
.read(cx)
.running_state()
.read(cx)
.serialized_layout(cx)
})
.panes;
let post_serialized_layout = debug_panel
.update_in(cx, |panel, window, cx| {
panel.set_position(DockPosition::Right, window, cx);
panel
.active_session()
.unwrap()
.read(cx)
.running_state()
.read(cx)
.serialized_layout(cx)
})
.panes;
let pre_panes = pre_serialized_layout.in_order();
let post_panes = post_serialized_layout.in_order();
assert_eq!(pre_panes.len(), post_panes.len());
for (pre, post) in zip(pre_panes, post_panes) {
match (pre, post) {
(
SerializedPaneLayout::Group {
axis: pre_axis,
flexes: pre_flexes,
children: _,
},
SerializedPaneLayout::Group {
axis: post_axis,
flexes: post_flexes,
children: _,
},
) => {
assert_ne!(pre_axis, post_axis);
assert_eq!(pre_flexes, post_flexes);
}
(SerializedPaneLayout::Pane(pre_pane), SerializedPaneLayout::Pane(post_pane)) => {
assert_eq!(pre_pane.children, post_pane.children);
assert_eq!(pre_pane.active_item, post_pane.active_item);
}
_ => {
panic!("Variants don't match")
}
}
}
}

View File

@@ -19,6 +19,7 @@ component.workspace = true
ctor.workspace = true
editor.workspace = true
env_logger.workspace = true
futures.workspace = true
gpui.workspace = true
indoc.workspace = true
language.workspace = true
@@ -29,6 +30,7 @@ markdown.workspace = true
project.workspace = true
rand.workspace = true
serde.workspace = true
serde_json.workspace = true
settings.workspace = true
text.workspace = true
theme.workspace = true

View File

@@ -14,6 +14,7 @@ use editor::{
display_map::{BlockPlacement, BlockProperties, BlockStyle, CustomBlockId},
scroll::Autoscroll,
};
use futures::future::join_all;
use gpui::{
AnyElement, AnyView, App, AsyncApp, Context, Entity, EventEmitter, FocusHandle, Focusable,
Global, InteractiveElement, IntoElement, ParentElement, Render, SharedString, Styled,
@@ -23,13 +24,15 @@ use language::{
Bias, Buffer, BufferRow, BufferSnapshot, DiagnosticEntry, Point, ToTreeSitterPoint,
};
use lsp::DiagnosticSeverity;
use project::{DiagnosticSummary, Project, ProjectPath, project_settings::ProjectSettings};
use project::{
DiagnosticSummary, Project, ProjectPath,
lsp_store::rust_analyzer_ext::{cancel_flycheck, run_flycheck},
project_settings::ProjectSettings,
};
use settings::Settings;
use std::{
any::{Any, TypeId},
cmp,
cmp::Ordering,
cmp::{self, Ordering},
ops::{Range, RangeInclusive},
sync::Arc,
time::Duration,
@@ -45,7 +48,10 @@ use workspace::{
searchable::SearchableItemHandle,
};
actions!(diagnostics, [Deploy, ToggleWarnings]);
actions!(
diagnostics,
[Deploy, ToggleWarnings, ToggleDiagnosticsRefresh]
);
#[derive(Default)]
pub(crate) struct IncludeWarnings(bool);
@@ -68,9 +74,16 @@ pub(crate) struct ProjectDiagnosticsEditor {
paths_to_update: BTreeSet<ProjectPath>,
include_warnings: bool,
update_excerpts_task: Option<Task<Result<()>>>,
cargo_diagnostics_fetch: CargoDiagnosticsFetchState,
_subscription: Subscription,
}
struct CargoDiagnosticsFetchState {
fetch_task: Option<Task<()>>,
cancel_task: Option<Task<()>>,
diagnostic_sources: Arc<Vec<ProjectPath>>,
}
impl EventEmitter<EditorEvent> for ProjectDiagnosticsEditor {}
const DIAGNOSTICS_UPDATE_DELAY: Duration = Duration::from_millis(50);
@@ -126,6 +139,7 @@ impl Render for ProjectDiagnosticsEditor {
.track_focus(&self.focus_handle(cx))
.size_full()
.on_action(cx.listener(Self::toggle_warnings))
.on_action(cx.listener(Self::toggle_diagnostics_refresh))
.child(child)
}
}
@@ -212,7 +226,11 @@ impl ProjectDiagnosticsEditor {
cx.observe_global_in::<IncludeWarnings>(window, |this, window, cx| {
this.include_warnings = cx.global::<IncludeWarnings>().0;
this.diagnostics.clear();
this.update_all_excerpts(window, cx);
this.update_all_diagnostics(false, window, cx);
})
.detach();
cx.observe_release(&cx.entity(), |editor, _, cx| {
editor.stop_cargo_diagnostics_fetch(cx);
})
.detach();
@@ -229,9 +247,14 @@ impl ProjectDiagnosticsEditor {
editor,
paths_to_update: Default::default(),
update_excerpts_task: None,
cargo_diagnostics_fetch: CargoDiagnosticsFetchState {
fetch_task: None,
cancel_task: None,
diagnostic_sources: Arc::new(Vec::new()),
},
_subscription: project_event_subscription,
};
this.update_all_excerpts(window, cx);
this.update_all_diagnostics(true, window, cx);
this
}
@@ -239,15 +262,17 @@ impl ProjectDiagnosticsEditor {
if self.update_excerpts_task.is_some() {
return;
}
let project_handle = self.project.clone();
self.update_excerpts_task = Some(cx.spawn_in(window, async move |this, cx| {
cx.background_executor()
.timer(DIAGNOSTICS_UPDATE_DELAY)
.await;
loop {
let Some(path) = this.update(cx, |this, _| {
let Some(path) = this.update(cx, |this, cx| {
let Some(path) = this.paths_to_update.pop_first() else {
this.update_excerpts_task.take();
this.update_excerpts_task = None;
cx.notify();
return None;
};
Some(path)
@@ -307,6 +332,32 @@ impl ProjectDiagnosticsEditor {
cx.set_global(IncludeWarnings(!self.include_warnings));
}
fn toggle_diagnostics_refresh(
&mut self,
_: &ToggleDiagnosticsRefresh,
window: &mut Window,
cx: &mut Context<Self>,
) {
let fetch_cargo_diagnostics = ProjectSettings::get_global(cx)
.diagnostics
.fetch_cargo_diagnostics();
if fetch_cargo_diagnostics {
if self.cargo_diagnostics_fetch.fetch_task.is_some() {
self.stop_cargo_diagnostics_fetch(cx);
} else {
self.update_all_diagnostics(false, window, cx);
}
} else {
if self.update_excerpts_task.is_some() {
self.update_excerpts_task = None;
} else {
self.update_all_diagnostics(false, window, cx);
}
}
cx.notify();
}
fn focus_in(&mut self, window: &mut Window, cx: &mut Context<Self>) {
if self.focus_handle.is_focused(window) && !self.multibuffer.read(cx).is_empty() {
self.editor.focus_handle(cx).focus(window)
@@ -320,6 +371,73 @@ impl ProjectDiagnosticsEditor {
}
}
fn update_all_diagnostics(
&mut self,
first_launch: bool,
window: &mut Window,
cx: &mut Context<Self>,
) {
let cargo_diagnostics_sources = self.cargo_diagnostics_sources(cx);
if cargo_diagnostics_sources.is_empty() {
self.update_all_excerpts(window, cx);
} else if first_launch && !self.summary.is_empty() {
self.update_all_excerpts(window, cx);
} else {
self.fetch_cargo_diagnostics(Arc::new(cargo_diagnostics_sources), cx);
}
}
fn fetch_cargo_diagnostics(
&mut self,
diagnostics_sources: Arc<Vec<ProjectPath>>,
cx: &mut Context<Self>,
) {
let project = self.project.clone();
self.cargo_diagnostics_fetch.cancel_task = None;
self.cargo_diagnostics_fetch.fetch_task = None;
self.cargo_diagnostics_fetch.diagnostic_sources = diagnostics_sources.clone();
if self.cargo_diagnostics_fetch.diagnostic_sources.is_empty() {
return;
}
self.cargo_diagnostics_fetch.fetch_task = Some(cx.spawn(async move |editor, cx| {
let mut fetch_tasks = Vec::new();
for buffer_path in diagnostics_sources.iter().cloned() {
if cx
.update(|cx| {
fetch_tasks.push(run_flycheck(project.clone(), buffer_path, cx));
})
.is_err()
{
break;
}
}
let _ = join_all(fetch_tasks).await;
editor
.update(cx, |editor, _| {
editor.cargo_diagnostics_fetch.fetch_task = None;
})
.ok();
}));
}
fn stop_cargo_diagnostics_fetch(&mut self, cx: &mut App) {
self.cargo_diagnostics_fetch.fetch_task = None;
let mut cancel_gasks = Vec::new();
for buffer_path in std::mem::take(&mut self.cargo_diagnostics_fetch.diagnostic_sources)
.iter()
.cloned()
{
cancel_gasks.push(cancel_flycheck(self.project.clone(), buffer_path, cx));
}
self.cargo_diagnostics_fetch.cancel_task = Some(cx.background_spawn(async move {
let _ = join_all(cancel_gasks).await;
log::info!("Finished fetching cargo diagnostics");
}));
}
/// Enqueue an update of all excerpts. Updates all paths that either
/// currently have diagnostics or are currently present in this view.
fn update_all_excerpts(&mut self, window: &mut Window, cx: &mut Context<Self>) {
@@ -422,20 +540,17 @@ impl ProjectDiagnosticsEditor {
})?;
for item in more {
let insert_pos = blocks
.binary_search_by(|existing| {
match existing.initial_range.start.cmp(&item.initial_range.start) {
Ordering::Equal => item
.initial_range
.end
.cmp(&existing.initial_range.end)
.reverse(),
other => other,
}
let i = blocks
.binary_search_by(|probe| {
probe
.initial_range
.start
.cmp(&item.initial_range.start)
.then(probe.initial_range.end.cmp(&item.initial_range.end))
.then(Ordering::Greater)
})
.unwrap_or_else(|pos| pos);
blocks.insert(insert_pos, item);
.unwrap_or_else(|i| i);
blocks.insert(i, item);
}
}
@@ -448,10 +563,25 @@ impl ProjectDiagnosticsEditor {
&mut cx,
)
.await;
excerpt_ranges.push(ExcerptRange {
context: excerpt_range,
primary: b.initial_range.clone(),
})
let i = excerpt_ranges
.binary_search_by(|probe| {
probe
.context
.start
.cmp(&excerpt_range.start)
.then(probe.context.end.cmp(&excerpt_range.end))
.then(probe.primary.start.cmp(&b.initial_range.start))
.then(probe.primary.end.cmp(&b.initial_range.end))
.then(cmp::Ordering::Greater)
})
.unwrap_or_else(|i| i);
excerpt_ranges.insert(
i,
ExcerptRange {
context: excerpt_range,
primary: b.initial_range.clone(),
},
)
}
this.update_in(cx, |this, window, cx| {
@@ -534,6 +664,30 @@ impl ProjectDiagnosticsEditor {
})
})
}
pub fn cargo_diagnostics_sources(&self, cx: &App) -> Vec<ProjectPath> {
let fetch_cargo_diagnostics = ProjectSettings::get_global(cx)
.diagnostics
.fetch_cargo_diagnostics();
if !fetch_cargo_diagnostics {
return Vec::new();
}
self.project
.read(cx)
.worktrees(cx)
.filter_map(|worktree| {
let _cargo_toml_entry = worktree.read(cx).entry_for_path("Cargo.toml")?;
let rust_file_entry = worktree.read(cx).entries(false, 0).find(|entry| {
entry
.path
.extension()
.and_then(|extension| extension.to_str())
== Some("rs")
})?;
self.project.read(cx).path_for_entry(rust_file_entry.id, cx)
})
.collect()
}
}
impl Focusable for ProjectDiagnosticsEditor {

View File

@@ -1,4 +1,6 @@
use crate::ProjectDiagnosticsEditor;
use std::sync::Arc;
use crate::{ProjectDiagnosticsEditor, ToggleDiagnosticsRefresh};
use gpui::{Context, Entity, EventEmitter, ParentElement, Render, WeakEntity, Window};
use ui::prelude::*;
use ui::{IconButton, IconButtonShape, IconName, Tooltip};
@@ -13,18 +15,26 @@ impl Render for ToolbarControls {
let mut include_warnings = false;
let mut has_stale_excerpts = false;
let mut is_updating = false;
let cargo_diagnostics_sources = Arc::new(self.diagnostics().map_or(Vec::new(), |editor| {
editor.read(cx).cargo_diagnostics_sources(cx)
}));
let fetch_cargo_diagnostics = !cargo_diagnostics_sources.is_empty();
if let Some(editor) = self.diagnostics() {
let diagnostics = editor.read(cx);
include_warnings = diagnostics.include_warnings;
has_stale_excerpts = !diagnostics.paths_to_update.is_empty();
is_updating = diagnostics.update_excerpts_task.is_some()
|| diagnostics
.project
.read(cx)
.language_servers_running_disk_based_diagnostics(cx)
.next()
.is_some();
is_updating = if fetch_cargo_diagnostics {
diagnostics.cargo_diagnostics_fetch.fetch_task.is_some()
} else {
diagnostics.update_excerpts_task.is_some()
|| diagnostics
.project
.read(cx)
.language_servers_running_disk_based_diagnostics(cx)
.next()
.is_some()
};
}
let tooltip = if include_warnings {
@@ -41,21 +51,56 @@ impl Render for ToolbarControls {
h_flex()
.gap_1()
.when(has_stale_excerpts, |div| {
div.child(
IconButton::new("update-excerpts", IconName::Update)
.icon_color(Color::Info)
.shape(IconButtonShape::Square)
.disabled(is_updating)
.tooltip(Tooltip::text("Update excerpts"))
.on_click(cx.listener(|this, _, window, cx| {
if let Some(diagnostics) = this.diagnostics() {
diagnostics.update(cx, |diagnostics, cx| {
diagnostics.update_all_excerpts(window, cx);
});
}
})),
)
.map(|div| {
if is_updating {
div.child(
IconButton::new("stop-updating", IconName::StopFilled)
.icon_color(Color::Info)
.shape(IconButtonShape::Square)
.tooltip(Tooltip::for_action_title(
"Stop diagnostics update",
&ToggleDiagnosticsRefresh,
))
.on_click(cx.listener(move |toolbar_controls, _, _, cx| {
if let Some(diagnostics) = toolbar_controls.diagnostics() {
diagnostics.update(cx, |diagnostics, cx| {
diagnostics.stop_cargo_diagnostics_fetch(cx);
diagnostics.update_excerpts_task = None;
cx.notify();
});
}
})),
)
} else {
div.child(
IconButton::new("refresh-diagnostics", IconName::Update)
.icon_color(Color::Info)
.shape(IconButtonShape::Square)
.disabled(!has_stale_excerpts && !fetch_cargo_diagnostics)
.tooltip(Tooltip::for_action_title(
"Refresh diagnostics",
&ToggleDiagnosticsRefresh,
))
.on_click(cx.listener({
move |toolbar_controls, _, window, cx| {
if let Some(diagnostics) = toolbar_controls.diagnostics() {
let cargo_diagnostics_sources =
Arc::clone(&cargo_diagnostics_sources);
diagnostics.update(cx, move |diagnostics, cx| {
if fetch_cargo_diagnostics {
diagnostics.fetch_cargo_diagnostics(
cargo_diagnostics_sources,
cx,
);
} else {
diagnostics.update_all_excerpts(window, cx);
}
});
}
}
})),
)
}
})
.child(
IconButton::new("toggle-warnings", IconName::Warning)

View File

@@ -249,7 +249,9 @@ actions!(
ApplyDiffHunk,
Backspace,
Cancel,
CancelFlycheck,
CancelLanguageServerWork,
ClearFlycheck,
ConfirmRename,
ConfirmCompletionInsert,
ConfirmCompletionReplace,
@@ -308,6 +310,7 @@ actions!(
GoToImplementation,
GoToImplementationSplit,
GoToNextChange,
GoToParentModule,
GoToPreviousChange,
GoToPreviousDiagnostic,
GoToTypeDefinition,
@@ -371,6 +374,7 @@ actions!(
RevertFile,
ReloadFile,
Rewrap,
RunFlycheck,
ScrollCursorBottom,
ScrollCursorCenter,
ScrollCursorCenterTopBottom,

View File

@@ -2952,7 +2952,9 @@ impl Editor {
_ => {}
}
self.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
let auto_scroll = EditorSettings::get_global(cx).autoscroll_on_clicks;
self.change_selections(auto_scroll.then(Autoscroll::fit), window, cx, |s| {
s.set_pending(pending_selection, pending_mode)
});
}
@@ -2972,7 +2974,6 @@ impl Editor {
let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
let buffer = &display_map.buffer_snapshot;
let newest_selection = self.selections.newest_anchor().clone();
let position = display_map.clip_point(position, Bias::Left);
let start;
@@ -3047,8 +3048,6 @@ impl Editor {
} else {
if !add {
s.clear_disjoint();
} else if click_count > 1 {
s.delete(newest_selection.id)
}
s.set_pending_anchor_range(start..end, mode);
@@ -5207,20 +5206,27 @@ impl Editor {
let dap_store = project.read(cx).dap_store();
let mut scenarios = vec![];
let resolved_tasks = resolved_tasks.as_ref()?;
let debug_adapter: SharedString = buffer
.read(cx)
.language()?
.context_provider()?
.debug_adapter()?
.into();
let buffer = buffer.read(cx);
let language = buffer.language()?;
let file = buffer.file();
let debug_adapter =
language_settings(language.name().into(), file, cx)
.debuggers
.first()
.map(SharedString::from)
.or_else(|| {
language
.config()
.debuggers
.first()
.map(SharedString::from)
})?;
dap_store.update(cx, |this, cx| {
for (_, task) in &resolved_tasks.templates {
if let Some(scenario) = this
.debug_scenario_for_build_task(
task.resolved.clone(),
SharedString::from(
task.original_task().label.clone(),
),
task.original_task().clone(),
debug_adapter.clone(),
cx,
)
@@ -12131,6 +12137,28 @@ impl Editor {
}
}
fn select_match_ranges(
&mut self,
range: Range<usize>,
reversed: bool,
replace_newest: bool,
auto_scroll: Option<Autoscroll>,
window: &mut Window,
cx: &mut Context<Editor>,
) {
self.unfold_ranges(&[range.clone()], false, auto_scroll.is_some(), cx);
self.change_selections(auto_scroll, window, cx, |s| {
if replace_newest {
s.delete(s.newest_anchor().id);
}
if reversed {
s.insert_range(range.end..range.start);
} else {
s.insert_range(range);
}
});
}
pub fn select_next_match_internal(
&mut self,
display_map: &DisplaySnapshot,
@@ -12139,28 +12167,6 @@ impl Editor {
window: &mut Window,
cx: &mut Context<Self>,
) -> Result<()> {
fn select_next_match_ranges(
this: &mut Editor,
range: Range<usize>,
reversed: bool,
replace_newest: bool,
auto_scroll: Option<Autoscroll>,
window: &mut Window,
cx: &mut Context<Editor>,
) {
this.unfold_ranges(&[range.clone()], false, auto_scroll.is_some(), cx);
this.change_selections(auto_scroll, window, cx, |s| {
if replace_newest {
s.delete(s.newest_anchor().id);
}
if reversed {
s.insert_range(range.end..range.start);
} else {
s.insert_range(range);
}
});
}
let buffer = &display_map.buffer_snapshot;
let mut selections = self.selections.all::<usize>(cx);
if let Some(mut select_next_state) = self.select_next_state.take() {
@@ -12205,8 +12211,7 @@ impl Editor {
}
if let Some(next_selected_range) = next_selected_range {
select_next_match_ranges(
self,
self.select_match_ranges(
next_selected_range,
last_selection.reversed,
replace_newest,
@@ -12264,8 +12269,7 @@ impl Editor {
selection.end = word_range.end.to_offset(display_map, Bias::Left);
selection.goal = SelectionGoal::None;
selection.reversed = false;
select_next_match_ranges(
self,
self.select_match_ranges(
selection.start..selection.end,
selection.reversed,
replace_newest,
@@ -12430,17 +12434,14 @@ impl Editor {
}
if let Some(next_selected_range) = next_selected_range {
self.unfold_ranges(&[next_selected_range.clone()], false, true, cx);
self.change_selections(Some(Autoscroll::newest()), window, cx, |s| {
if action.replace_newest {
s.delete(s.newest_anchor().id);
}
if last_selection.reversed {
s.insert_range(next_selected_range.end..next_selected_range.start);
} else {
s.insert_range(next_selected_range);
}
});
self.select_match_ranges(
next_selected_range,
last_selection.reversed,
action.replace_newest,
Some(Autoscroll::newest()),
window,
cx,
);
} else {
select_prev_state.done = true;
}
@@ -12491,6 +12492,14 @@ impl Editor {
selection.end = word_range.end.to_offset(&display_map, Bias::Left);
selection.goal = SelectionGoal::None;
selection.reversed = false;
self.select_match_ranges(
selection.start..selection.end,
selection.reversed,
action.replace_newest,
Some(Autoscroll::newest()),
window,
cx,
);
}
if selections.len() == 1 {
let selection = selections
@@ -12509,16 +12518,6 @@ impl Editor {
} else {
self.select_prev_state = None;
}
self.unfold_ranges(
&selections.iter().map(|s| s.range()).collect::<Vec<_>>(),
false,
true,
cx,
);
self.change_selections(Some(Autoscroll::newest()), window, cx, |s| {
s.select(selections);
});
} else if let Some(selected_text) = selected_text {
self.select_prev_state = Some(SelectNextState {
query: AhoCorasick::new(&[selected_text.chars().rev().collect::<String>()])?,
@@ -13001,10 +13000,9 @@ impl Editor {
}
let mut new_range = old_range.clone();
let mut new_node = None;
while let Some((node, containing_range)) = buffer.syntax_ancestor(new_range.clone())
while let Some((_node, containing_range)) =
buffer.syntax_ancestor(new_range.clone())
{
new_node = Some(node);
new_range = match containing_range {
MultiOrSingleBufferOffsetRange::Single(_) => break,
MultiOrSingleBufferOffsetRange::Multi(range) => range,
@@ -13016,17 +13014,6 @@ impl Editor {
}
}
if let Some(node) = new_node {
// Log the ancestor, to support using this action as a way to explore TreeSitter
// nodes. Parent and grandparent are also logged because this operation will not
// visit nodes that have the same range as their parent.
log::info!("Node: {node:?}");
let parent = node.parent();
log::info!("Parent: {parent:?}");
let grandparent = parent.and_then(|x| x.parent());
log::info!("Grandparent: {grandparent:?}");
}
selected_larger_node |= new_range != old_range;
Selection {
id: selection.id,

View File

@@ -6130,11 +6130,7 @@ async fn test_select_previous_with_single_caret(cx: &mut TestAppContext) {
cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
.unwrap();
cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndef«abcˇ»\n«abcˇ»");
cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
.unwrap();
cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndef«abcˇ»\n«abcˇ»");
cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
}
#[gpui::test]
@@ -6406,6 +6402,68 @@ async fn test_select_larger_smaller_syntax_node(cx: &mut TestAppContext) {
});
}
#[gpui::test]
async fn test_select_larger_syntax_node_for_cursor_at_end(cx: &mut TestAppContext) {
init_test(cx, |_| {});
let language = Arc::new(Language::new(
LanguageConfig::default(),
Some(tree_sitter_rust::LANGUAGE.into()),
));
let text = "let a = 2;";
let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
editor
.condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
.await;
// Test case 1: Cursor at end of word
editor.update_in(cx, |editor, window, cx| {
editor.change_selections(None, window, cx, |s| {
s.select_display_ranges([
DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5)
]);
});
});
editor.update(cx, |editor, cx| {
assert_text_with_selections(editor, "let aˇ = 2;", cx);
});
editor.update_in(cx, |editor, window, cx| {
editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
});
editor.update(cx, |editor, cx| {
assert_text_with_selections(editor, "let «ˇa» = 2;", cx);
});
editor.update_in(cx, |editor, window, cx| {
editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
});
editor.update(cx, |editor, cx| {
assert_text_with_selections(editor, "«ˇlet a = 2;»", cx);
});
// Test case 2: Cursor at end of statement
editor.update_in(cx, |editor, window, cx| {
editor.change_selections(None, window, cx, |s| {
s.select_display_ranges([
DisplayPoint::new(DisplayRow(0), 11)..DisplayPoint::new(DisplayRow(0), 11)
]);
});
});
editor.update(cx, |editor, cx| {
assert_text_with_selections(editor, "let a = 2;ˇ", cx);
});
editor.update_in(cx, |editor, window, cx| {
editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
});
editor.update(cx, |editor, cx| {
assert_text_with_selections(editor, "«ˇlet a = 2;»", cx);
});
}
#[gpui::test]
async fn test_select_larger_smaller_syntax_node_for_string(cx: &mut TestAppContext) {
init_test(cx, |_| {});

View File

@@ -25,7 +25,7 @@ use crate::{
inlay_hint_settings,
items::BufferSearchHighlights,
mouse_context_menu::{self, MenuPosition},
scroll::scroll_amount::ScrollAmount,
scroll::{ActiveScrollbarState, ScrollbarThumbState, scroll_amount::ScrollAmount},
};
use buffer_diff::{DiffHunkStatus, DiffHunkStatusKind};
use collections::{BTreeMap, HashMap};
@@ -1449,7 +1449,7 @@ impl EditorElement {
window: &mut Window,
cx: &mut App,
) -> Option<EditorScrollbars> {
if !snapshot.mode.is_full() {
if !snapshot.mode.is_full() || !self.editor.read(cx).show_scrollbars {
return None;
}
@@ -1457,41 +1457,40 @@ impl EditorElement {
// cancel the scrollbar drag.
if cx.has_active_drag() {
self.editor.update(cx, |editor, cx| {
editor.scroll_manager.reset_scrollbar_dragging_state(cx)
editor.scroll_manager.reset_scrollbar_state(cx)
});
}
let scrollbar_settings = EditorSettings::get_global(cx).scrollbar;
let show_scrollbars = self.editor.read(cx).show_scrollbars
&& match scrollbar_settings.show {
ShowScrollbar::Auto => {
let editor = self.editor.read(cx);
let is_singleton = editor.is_singleton(cx);
// Git
(is_singleton && scrollbar_settings.git_diff && snapshot.buffer_snapshot.has_diff_hunks())
||
// Buffer Search Results
(is_singleton && scrollbar_settings.search_results && editor.has_background_highlights::<BufferSearchHighlights>())
||
// Selected Text Occurrences
(is_singleton && scrollbar_settings.selected_text && editor.has_background_highlights::<SelectedTextHighlight>())
||
// Selected Symbol Occurrences
(is_singleton && scrollbar_settings.selected_symbol && (editor.has_background_highlights::<DocumentHighlightRead>() || editor.has_background_highlights::<DocumentHighlightWrite>()))
||
// Diagnostics
(is_singleton && scrollbar_settings.diagnostics != ScrollbarDiagnostics::None && snapshot.buffer_snapshot.has_diagnostics())
||
// Cursors out of sight
non_visible_cursors
||
// Scrollmanager
editor.scroll_manager.scrollbars_visible()
}
ShowScrollbar::System => self.editor.read(cx).scroll_manager.scrollbars_visible(),
ShowScrollbar::Always => true,
ShowScrollbar::Never => return None,
};
let show_scrollbars = match scrollbar_settings.show {
ShowScrollbar::Auto => {
let editor = self.editor.read(cx);
let is_singleton = editor.is_singleton(cx);
// Git
(is_singleton && scrollbar_settings.git_diff && snapshot.buffer_snapshot.has_diff_hunks())
||
// Buffer Search Results
(is_singleton && scrollbar_settings.search_results && editor.has_background_highlights::<BufferSearchHighlights>())
||
// Selected Text Occurrences
(is_singleton && scrollbar_settings.selected_text && editor.has_background_highlights::<SelectedTextHighlight>())
||
// Selected Symbol Occurrences
(is_singleton && scrollbar_settings.selected_symbol && (editor.has_background_highlights::<DocumentHighlightRead>() || editor.has_background_highlights::<DocumentHighlightWrite>()))
||
// Diagnostics
(is_singleton && scrollbar_settings.diagnostics != ScrollbarDiagnostics::None && snapshot.buffer_snapshot.has_diagnostics())
||
// Cursors out of sight
non_visible_cursors
||
// Scrollmanager
editor.scroll_manager.scrollbars_visible()
}
ShowScrollbar::System => self.editor.read(cx).scroll_manager.scrollbars_visible(),
ShowScrollbar::Always => true,
ShowScrollbar::Never => return None,
};
Some(EditorScrollbars::from_scrollbar_axes(
scrollbar_settings.axes,
@@ -1500,6 +1499,7 @@ impl EditorElement {
scroll_position,
self.style.scrollbar_width,
show_scrollbars,
self.editor.read(cx).scroll_manager.active_scrollbar_state(),
window,
))
}
@@ -2285,6 +2285,9 @@ impl EditorElement {
}
let display_row = multibuffer_point.to_display_point(snapshot).row();
if !range.contains(&display_row) {
return None;
}
if row_infos
.get((display_row - range.start).0 as usize)
.is_some_and(|row_info| row_info.expand_info.is_some())
@@ -5101,7 +5104,7 @@ impl EditorElement {
}
fn paint_scrollbars(&mut self, layout: &mut EditorLayout, window: &mut Window, cx: &mut App) {
let Some(scrollbars_layout) = &layout.scrollbars_layout else {
let Some(scrollbars_layout) = layout.scrollbars_layout.take() else {
return;
};
@@ -5150,10 +5153,16 @@ impl EditorElement {
}
}
let scrollbar_thumb_color = match scrollbar_layout.thumb_state {
ScrollbarThumbState::Dragging | ScrollbarThumbState::Hovered => {
cx.theme().colors().scrollbar_thumb_hover_background
}
ScrollbarThumbState::Idle => cx.theme().colors().scrollbar_thumb_background,
};
window.paint_quad(quad(
thumb_bounds,
Corners::default(),
cx.theme().colors().scrollbar_thumb_background,
scrollbar_thumb_color,
scrollbar_edges,
cx.theme().colors().scrollbar_thumb_border,
BorderStyle::Solid,
@@ -5200,13 +5209,22 @@ impl EditorElement {
});
editor.set_scroll_position(position, window, cx);
}
cx.stop_propagation();
} else {
editor.scroll_manager.reset_scrollbar_dragging_state(cx);
}
if scrollbars_layout.get_hovered_axis(window).is_some() {
editor.scroll_manager.show_scrollbars(window, cx);
cx.stop_propagation();
} else if let Some((layout, axis)) = scrollbars_layout.get_hovered_axis(window)
{
if layout.thumb_bounds().contains(&event.position) {
editor
.scroll_manager
.set_hovered_scroll_thumb_axis(axis, cx);
} else {
editor.scroll_manager.reset_scrollbar_state(cx);
}
editor.scroll_manager.show_scrollbars(window, cx);
} else {
editor.scroll_manager.reset_scrollbar_state(cx);
}
mouse_position = event.position;
@@ -5217,13 +5235,19 @@ impl EditorElement {
if self.editor.read(cx).scroll_manager.any_scrollbar_dragged() {
window.on_mouse_event({
let editor = self.editor.clone();
move |_: &MouseUpEvent, phase, _, cx| {
move |_: &MouseUpEvent, phase, window, cx| {
if phase == DispatchPhase::Capture {
return;
}
editor.update(cx, |editor, cx| {
editor.scroll_manager.reset_scrollbar_dragging_state(cx);
if let Some((_, axis)) = scrollbars_layout.get_hovered_axis(window) {
editor
.scroll_manager
.set_hovered_scroll_thumb_axis(axis, cx);
} else {
editor.scroll_manager.reset_scrollbar_state(cx);
}
cx.stop_propagation();
});
}
@@ -5231,7 +5255,6 @@ impl EditorElement {
} else {
window.on_mouse_event({
let editor = self.editor.clone();
let scrollbars_layout = scrollbars_layout.clone();
move |event: &MouseDownEvent, phase, window, cx| {
if phase == DispatchPhase::Capture {
@@ -5252,7 +5275,9 @@ impl EditorElement {
let thumb_bounds = scrollbar_layout.thumb_bounds();
editor.update(cx, |editor, cx| {
editor.scroll_manager.set_dragged_scrollbar_axis(axis, cx);
editor
.scroll_manager
.set_dragged_scroll_thumb_axis(axis, cx);
let event_position = event.position.along(axis);
@@ -8034,6 +8059,7 @@ impl EditorScrollbars {
scroll_position: gpui::Point<f32>,
scrollbar_width: Pixels,
show_scrollbars: bool,
scrollbar_state: Option<&ActiveScrollbarState>,
window: &mut Window,
) -> Self {
let ScrollbarLayoutInformation {
@@ -8079,6 +8105,10 @@ impl EditorScrollbars {
axis != ScrollbarAxis::Horizontal || editor_content_size < scroll_range
})
.map(|(editor_content_size, scroll_range)| {
let thumb_state = scrollbar_state
.and_then(|state| state.thumb_state_for_axis(axis))
.unwrap_or(ScrollbarThumbState::Idle);
ScrollbarLayout::new(
window.insert_hitbox(scrollbar_bounds_for(axis), false),
editor_content_size,
@@ -8086,6 +8116,7 @@ impl EditorScrollbars {
glyph_grid_cell.along(axis),
content_offset.along(axis),
scroll_position.along(axis),
thumb_state,
axis,
)
})
@@ -8121,6 +8152,7 @@ struct ScrollbarLayout {
text_unit_size: Pixels,
content_offset: Pixels,
thumb_size: Pixels,
thumb_state: ScrollbarThumbState,
axis: ScrollbarAxis,
}
@@ -8137,6 +8169,7 @@ impl ScrollbarLayout {
glyph_space: Pixels,
content_offset: Pixels,
scroll_position: f32,
thumb_state: ScrollbarThumbState,
axis: ScrollbarAxis,
) -> Self {
let track_bounds = scrollbar_track_hitbox.bounds;
@@ -8161,6 +8194,7 @@ impl ScrollbarLayout {
text_unit_size,
content_offset,
thumb_size,
thumb_state,
axis,
}
}

View File

@@ -3,6 +3,7 @@ use std::sync::Arc;
use crate::Editor;
use collections::HashMap;
use futures::stream::FuturesUnordered;
use gpui::AsyncApp;
use gpui::{App, AppContext as _, Entity, Task};
use itertools::Itertools;
use language::Buffer;
@@ -74,6 +75,39 @@ where
})
}
async fn lsp_task_context(
project: &Entity<Project>,
buffer: &Entity<Buffer>,
cx: &mut AsyncApp,
) -> Option<TaskContext> {
let worktree_store = project
.update(cx, |project, _| project.worktree_store())
.ok()?;
let worktree_abs_path = cx
.update(|cx| {
let worktree_id = buffer.read(cx).file().map(|f| f.worktree_id(cx));
worktree_id
.and_then(|worktree_id| worktree_store.read(cx).worktree_for_id(worktree_id, cx))
.and_then(|worktree| worktree.read(cx).root_dir())
})
.ok()?;
let project_env = project
.update(cx, |project, cx| {
project.buffer_environment(&buffer, &worktree_store, cx)
})
.ok()?
.await;
Some(TaskContext {
cwd: worktree_abs_path.map(|p| p.to_path_buf()),
project_env: project_env.unwrap_or_default(),
..TaskContext::default()
})
}
pub fn lsp_tasks(
project: Entity<Project>,
task_sources: &HashMap<LanguageServerName, Vec<BufferId>>,
@@ -97,13 +131,16 @@ pub fn lsp_tasks(
cx.spawn(async move |cx| {
let mut lsp_tasks = Vec::new();
let lsp_task_context = TaskContext::default();
while let Some(server_to_query) = lsp_task_sources.next().await {
if let Some((server_id, buffers)) = server_to_query {
let source_kind = TaskSourceKind::Lsp(server_id);
let id_base = source_kind.to_id_base();
let mut new_lsp_tasks = Vec::new();
for buffer in buffers {
let lsp_buffer_context = lsp_task_context(&project, &buffer, cx)
.await
.unwrap_or_default();
if let Ok(runnables_task) = project.update(cx, |project, cx| {
let buffer_id = buffer.read(cx).remote_id();
project.request_lsp(
@@ -120,7 +157,7 @@ pub fn lsp_tasks(
new_lsp_tasks.extend(new_runnables.runnables.into_iter().filter_map(
|(location, runnable)| {
let resolved_task =
runnable.resolve_task(&id_base, &lsp_task_context)?;
runnable.resolve_task(&id_base, &lsp_buffer_context)?;
Some((location, resolved_task))
},
));

View File

@@ -233,7 +233,7 @@ pub fn deploy_context_menu(
.separator()
.action("Cut", Box::new(Cut))
.action("Copy", Box::new(Copy))
.action("Copy and trim", Box::new(CopyAndTrim))
.action("Copy and Trim", Box::new(CopyAndTrim))
.action("Paste", Box::new(Paste))
.separator()
.map(|builder| {

View File

@@ -4,15 +4,20 @@ use anyhow::Context as _;
use gpui::{App, AppContext as _, Context, Entity, Window};
use language::{Capability, Language, proto::serialize_anchor};
use multi_buffer::MultiBuffer;
use project::lsp_store::{
lsp_ext_command::{DocsUrls, ExpandMacro, ExpandedMacro},
rust_analyzer_ext::RUST_ANALYZER_NAME,
use project::{
ProjectItem,
lsp_command::location_link_from_proto,
lsp_store::{
lsp_ext_command::{DocsUrls, ExpandMacro, ExpandedMacro},
rust_analyzer_ext::{RUST_ANALYZER_NAME, cancel_flycheck, clear_flycheck, run_flycheck},
},
};
use rpc::proto;
use text::ToPointUtf16;
use crate::{
Editor, ExpandMacroRecursively, OpenDocs, element::register_action,
CancelFlycheck, ClearFlycheck, Editor, ExpandMacroRecursively, GoToParentModule,
GotoDefinitionKind, OpenDocs, RunFlycheck, element::register_action, hover_links::HoverLink,
lsp_ext::find_specific_language_server_in_selection,
};
@@ -30,11 +35,97 @@ pub fn apply_related_actions(editor: &Entity<Editor>, window: &mut Window, cx: &
.filter_map(|buffer| buffer.read(cx).language())
.any(|language| is_rust_language(language))
{
register_action(&editor, window, go_to_parent_module);
register_action(&editor, window, expand_macro_recursively);
register_action(&editor, window, open_docs);
register_action(&editor, window, cancel_flycheck_action);
register_action(&editor, window, run_flycheck_action);
register_action(&editor, window, clear_flycheck_action);
}
}
pub fn go_to_parent_module(
editor: &mut Editor,
_: &GoToParentModule,
window: &mut Window,
cx: &mut Context<Editor>,
) {
if editor.selections.count() == 0 {
return;
}
let Some(project) = &editor.project else {
return;
};
let server_lookup = find_specific_language_server_in_selection(
editor,
cx,
is_rust_language,
RUST_ANALYZER_NAME,
);
let project = project.clone();
let lsp_store = project.read(cx).lsp_store();
let upstream_client = lsp_store.read(cx).upstream_client();
cx.spawn_in(window, async move |editor, cx| {
let Some((trigger_anchor, _, server_to_query, buffer)) = server_lookup.await else {
return anyhow::Ok(());
};
let location_links = if let Some((client, project_id)) = upstream_client {
let buffer_id = buffer.update(cx, |buffer, _| buffer.remote_id())?;
let request = proto::LspExtGoToParentModule {
project_id,
buffer_id: buffer_id.to_proto(),
position: Some(serialize_anchor(&trigger_anchor.text_anchor)),
};
let response = client
.request(request)
.await
.context("lsp ext go to parent module proto request")?;
futures::future::join_all(
response
.links
.into_iter()
.map(|link| location_link_from_proto(link, lsp_store.clone(), cx)),
)
.await
.into_iter()
.collect::<anyhow::Result<_>>()
.context("go to parent module via collab")?
} else {
let buffer_snapshot = buffer.update(cx, |buffer, _| buffer.snapshot())?;
let position = trigger_anchor.text_anchor.to_point_utf16(&buffer_snapshot);
project
.update(cx, |project, cx| {
project.request_lsp(
buffer,
project::LanguageServerToQuery::Other(server_to_query),
project::lsp_store::lsp_ext_command::GoToParentModule { position },
cx,
)
})?
.await
.context("go to parent module")?
};
editor
.update_in(cx, |editor, window, cx| {
editor.navigate_to_hover_links(
Some(GotoDefinitionKind::Declaration),
location_links.into_iter().map(HoverLink::Text).collect(),
false,
window,
cx,
)
})?
.await?;
Ok(())
})
.detach_and_log_err(cx);
}
pub fn expand_macro_recursively(
editor: &mut Editor,
_: &ExpandMacroRecursively,
@@ -213,3 +304,87 @@ pub fn open_docs(editor: &mut Editor, _: &OpenDocs, window: &mut Window, cx: &mu
})
.detach_and_log_err(cx);
}
fn cancel_flycheck_action(
editor: &mut Editor,
_: &CancelFlycheck,
_: &mut Window,
cx: &mut Context<Editor>,
) {
let Some(project) = &editor.project else {
return;
};
let Some(buffer_id) = editor
.selections
.disjoint_anchors()
.iter()
.find_map(|selection| {
let buffer_id = selection.start.buffer_id.or(selection.end.buffer_id)?;
let project = project.read(cx);
let entry_id = project
.buffer_for_id(buffer_id, cx)?
.read(cx)
.entry_id(cx)?;
project.path_for_entry(entry_id, cx)
})
else {
return;
};
cancel_flycheck(project.clone(), buffer_id, cx).detach_and_log_err(cx);
}
fn run_flycheck_action(
editor: &mut Editor,
_: &RunFlycheck,
_: &mut Window,
cx: &mut Context<Editor>,
) {
let Some(project) = &editor.project else {
return;
};
let Some(buffer_id) = editor
.selections
.disjoint_anchors()
.iter()
.find_map(|selection| {
let buffer_id = selection.start.buffer_id.or(selection.end.buffer_id)?;
let project = project.read(cx);
let entry_id = project
.buffer_for_id(buffer_id, cx)?
.read(cx)
.entry_id(cx)?;
project.path_for_entry(entry_id, cx)
})
else {
return;
};
run_flycheck(project.clone(), buffer_id, cx).detach_and_log_err(cx);
}
fn clear_flycheck_action(
editor: &mut Editor,
_: &ClearFlycheck,
_: &mut Window,
cx: &mut Context<Editor>,
) {
let Some(project) = &editor.project else {
return;
};
let Some(buffer_id) = editor
.selections
.disjoint_anchors()
.iter()
.find_map(|selection| {
let buffer_id = selection.start.buffer_id.or(selection.end.buffer_id)?;
let project = project.read(cx);
let entry_id = project
.buffer_for_id(buffer_id, cx)?
.read(cx)
.entry_id(cx)?;
project.path_for_entry(entry_id, cx)
})
else {
return;
};
clear_flycheck(project.clone(), buffer_id, cx).detach_and_log_err(cx);
}

View File

@@ -123,6 +123,29 @@ impl OngoingScroll {
}
}
#[derive(Copy, Clone, PartialEq, Eq)]
pub enum ScrollbarThumbState {
Idle,
Hovered,
Dragging,
}
#[derive(PartialEq, Eq)]
pub struct ActiveScrollbarState {
axis: Axis,
thumb_state: ScrollbarThumbState,
}
impl ActiveScrollbarState {
pub fn new(axis: Axis, thumb_state: ScrollbarThumbState) -> Self {
ActiveScrollbarState { axis, thumb_state }
}
pub fn thumb_state_for_axis(&self, axis: Axis) -> Option<ScrollbarThumbState> {
(self.axis == axis).then_some(self.thumb_state)
}
}
pub struct ScrollManager {
pub(crate) vertical_scroll_margin: f32,
anchor: ScrollAnchor,
@@ -131,7 +154,7 @@ pub struct ScrollManager {
last_autoscroll: Option<(gpui::Point<f32>, f32, f32, AutoscrollStrategy)>,
show_scrollbars: bool,
hide_scrollbar_task: Option<Task<()>>,
dragging_scrollbar: Option<Axis>,
active_scrollbar: Option<ActiveScrollbarState>,
visible_line_count: Option<f32>,
forbid_vertical_scroll: bool,
}
@@ -145,7 +168,7 @@ impl ScrollManager {
autoscroll_request: None,
show_scrollbars: true,
hide_scrollbar_task: None,
dragging_scrollbar: None,
active_scrollbar: None,
last_autoscroll: None,
visible_line_count: None,
forbid_vertical_scroll: false,
@@ -322,24 +345,53 @@ impl ScrollManager {
self.autoscroll_request.map(|(autoscroll, _)| autoscroll)
}
pub fn active_scrollbar_state(&self) -> Option<&ActiveScrollbarState> {
self.active_scrollbar.as_ref()
}
pub fn dragging_scrollbar_axis(&self) -> Option<Axis> {
self.dragging_scrollbar
self.active_scrollbar
.as_ref()
.map(|scrollbar| scrollbar.axis)
}
pub fn any_scrollbar_dragged(&self) -> bool {
self.dragging_scrollbar.is_some()
self.active_scrollbar
.as_ref()
.is_some_and(|scrollbar| scrollbar.thumb_state == ScrollbarThumbState::Dragging)
}
pub fn set_dragged_scrollbar_axis(&mut self, axis: Axis, cx: &mut Context<Editor>) {
if self.dragging_scrollbar != Some(axis) {
self.dragging_scrollbar = Some(axis);
cx.notify();
}
pub fn set_hovered_scroll_thumb_axis(&mut self, axis: Axis, cx: &mut Context<Editor>) {
self.update_active_scrollbar_state(
Some(ActiveScrollbarState::new(
axis,
ScrollbarThumbState::Hovered,
)),
cx,
);
}
pub fn reset_scrollbar_dragging_state(&mut self, cx: &mut Context<Editor>) {
if self.dragging_scrollbar.is_some() {
self.dragging_scrollbar = None;
pub fn set_dragged_scroll_thumb_axis(&mut self, axis: Axis, cx: &mut Context<Editor>) {
self.update_active_scrollbar_state(
Some(ActiveScrollbarState::new(
axis,
ScrollbarThumbState::Dragging,
)),
cx,
);
}
pub fn reset_scrollbar_state(&mut self, cx: &mut Context<Editor>) {
self.update_active_scrollbar_state(None, cx);
}
fn update_active_scrollbar_state(
&mut self,
new_state: Option<ActiveScrollbarState>,
cx: &mut Context<Editor>,
) {
if self.active_scrollbar != new_state {
self.active_scrollbar = new_state;
cx.notify();
}
}

View File

@@ -46,16 +46,19 @@ struct Args {
/// Runs all examples and threads that contain these substrings. If unspecified, all examples and threads are run.
#[arg(value_name = "EXAMPLE_SUBSTRING")]
filter: Vec<String>,
/// Model to use (default: "claude-3-7-sonnet-latest")
/// ID of model to use.
#[arg(long, default_value = "claude-3-7-sonnet-latest")]
model: String,
#[arg(long, value_delimiter = ',', default_value = "rs,ts")]
/// Model provider to use.
#[arg(long, default_value = "anthropic")]
provider: String,
#[arg(long, value_delimiter = ',', default_value = "rs,ts,py")]
languages: Vec<String>,
/// How many times to run each example.
#[arg(long, default_value = "1")]
#[arg(long, default_value = "8")]
repetitions: usize,
/// Maximum number of examples to run concurrently.
#[arg(long, default_value = "10")]
#[arg(long, default_value = "4")]
concurrency: usize,
}
@@ -123,7 +126,7 @@ fn main() {
let mut cumulative_tool_metrics = ToolMetrics::default();
let model_registry = LanguageModelRegistry::read_global(cx);
let model = find_model("claude-3-7-sonnet-latest", model_registry, cx).unwrap();
let model = find_model(&args.provider, &args.model, model_registry, cx).unwrap();
let model_provider_id = model.provider_id();
let model_provider = model_registry.provider(&model_provider_id).unwrap();
@@ -451,27 +454,36 @@ pub fn init(cx: &mut App) -> Arc<AgentAppState> {
}
pub fn find_model(
model_name: &str,
provider_id: &str,
model_id: &str,
model_registry: &LanguageModelRegistry,
cx: &App,
) -> anyhow::Result<Arc<dyn LanguageModel>> {
let model = model_registry
let matching_models = model_registry
.available_models(cx)
.find(|model| model.id().0 == model_name);
.filter(|model| model.id().0 == model_id && model.provider_id().0 == provider_id)
.collect::<Vec<_>>();
let Some(model) = model else {
return Err(anyhow!(
"No language model named {} was available. Available models: {}",
model_name,
match matching_models.as_slice() {
[model] => Ok(model.clone()),
[] => Err(anyhow!(
"No language model with ID {} was available. Available models: {}",
model_id,
model_registry
.available_models(cx)
.map(|model| model.id().0.clone())
.collect::<Vec<_>>()
.join(", ")
));
};
Ok(model)
)),
_ => Err(anyhow!(
"Multiple language models with ID {} available - use `--provider` to choose one of: {:?}",
model_id,
matching_models
.iter()
.map(|model| model.provider_id().0)
.collect::<Vec<_>>()
)),
}
}
pub fn commit_sha_for_path(repo_path: &Path) -> String {

View File

@@ -100,54 +100,64 @@ impl Example for CodeBlockCitations {
if let Some(content_len) = content_len {
// + 1 because there's a newline character after the citation.
let content =
&text[(citation.len() + 1)..content_len - (citation.len() + 1)];
let start_index = citation.len() + 1;
let end_index = content_len.saturating_sub(start_index);
// deindent (trim the start of each line) because sometimes the model
// chooses to deindent its code snippets for the sake of readability,
// which in markdown is not only reasonable but usually desirable.
cx.assert(
deindent(&buffer_text)
.trim()
.contains(deindent(&content).trim()),
"Code block content was found in file",
)
.ok();
if let Some(range) = path_range.range {
let start_line_index = range.start.line.saturating_sub(1);
let line_count =
range.end.line.saturating_sub(start_line_index);
let mut snippet = buffer_text
.lines()
.skip(start_line_index as usize)
.take(line_count as usize)
.collect::<Vec<&str>>()
.join("\n");
if let Some(start_col) = range.start.col {
snippet = snippet[start_col as usize..].to_string();
}
if let Some(end_col) = range.end.col {
let last_line = snippet.lines().last().unwrap();
snippet = snippet
[..snippet.len() - last_line.len() + end_col as usize]
.to_string();
}
if cx
.assert(
start_index <= end_index,
"Code block had a valid citation",
)
.is_ok()
{
let content = &text[start_index..end_index];
// deindent (trim the start of each line) because sometimes the model
// chooses to deindent its code snippets for the sake of readability,
// which in markdown is not only reasonable but usually desirable.
cx.assert_eq(
deindent(snippet.as_str()).trim(),
deindent(content).trim(),
format!(
"Code block was at {:?}-{:?}",
range.start, range.end
),
cx.assert(
deindent(&buffer_text)
.trim()
.contains(deindent(&content).trim()),
"Code block content was found in file",
)
.ok();
if let Some(range) = path_range.range {
let start_line_index = range.start.line.saturating_sub(1);
let line_count =
range.end.line.saturating_sub(start_line_index);
let mut snippet = buffer_text
.lines()
.skip(start_line_index as usize)
.take(line_count as usize)
.collect::<Vec<&str>>()
.join("\n");
if let Some(start_col) = range.start.col {
snippet = snippet[start_col as usize..].to_string();
}
if let Some(end_col) = range.end.col {
let last_line = snippet.lines().last().unwrap();
snippet = snippet[..snippet.len() - last_line.len()
+ end_col as usize]
.to_string();
}
// deindent (trim the start of each line) because sometimes the model
// chooses to deindent its code snippets for the sake of readability,
// which in markdown is not only reasonable but usually desirable.
cx.assert_eq(
deindent(snippet.as_str()).trim(),
deindent(content).trim(),
format!(
"Code block was at {:?}-{:?}",
range.start, range.end
),
)
.ok();
}
}
}
}

View File

@@ -4,7 +4,7 @@ use std::sync::Arc;
use anyhow::Result;
use fs::Fs;
use gpui::{App, Global, ReadGlobal, SharedString, Task};
use language::{BinaryStatus, LanguageMatcher, LanguageName, LoadedLanguage};
use language::{BinaryStatus, LanguageConfig, LanguageName, LoadedLanguage};
use lsp::LanguageServerName;
use parking_lot::RwLock;
@@ -224,10 +224,7 @@ impl ExtensionGrammarProxy for ExtensionHostProxy {
pub trait ExtensionLanguageProxy: Send + Sync + 'static {
fn register_language(
&self,
language: LanguageName,
grammar: Option<Arc<str>>,
matcher: LanguageMatcher,
hidden: bool,
config: LanguageConfig,
load: Arc<dyn Fn() -> Result<LoadedLanguage> + Send + Sync + 'static>,
);
@@ -241,17 +238,14 @@ pub trait ExtensionLanguageProxy: Send + Sync + 'static {
impl ExtensionLanguageProxy for ExtensionHostProxy {
fn register_language(
&self,
language: LanguageName,
grammar: Option<Arc<str>>,
matcher: LanguageMatcher,
hidden: bool,
language: LanguageConfig,
load: Arc<dyn Fn() -> Result<LoadedLanguage> + Send + Sync + 'static>,
) {
let Some(proxy) = self.language_proxy.read().clone() else {
return;
};
proxy.register_language(language, grammar, matcher, hidden, load)
proxy.register_language(language, load)
}
fn remove_languages(

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