Compare commits

..

155 Commits

Author SHA1 Message Date
Agus Zubiaga
9da17dd894 move old acp support to its own module
Co-authored-by: Ben Brandt <benjamin.j.brandt@gmail.com>
Co-authored-by: Richard Feldman <oss@rtfeldman.com>
2025-07-24 13:14:16 -03:00
Agus Zubiaga
4605b80b44 Remove deps 2025-07-24 13:06:39 -03:00
Agus Zubiaga
715e96de6f Remove dead code
Co-authored-by: Ben Brandt <benjamin.j.brandt@gmail.com>
Co-authored-by: Richard Feldman <oss@rtfeldman.com>
2025-07-24 13:02:43 -03:00
Agus Zubiaga
0f4e64026b Remove todo
Co-authored-by: Ben Brandt <benjamin.j.brandt@gmail.com>
Co-authored-by: Richard Feldman <oss@rtfeldman.com>
2025-07-24 12:59:56 -03:00
Agus Zubiaga
561a5f1f4f Fix padding
Co-authored-by: Ben Brandt <benjamin.j.brandt@gmail.com> Co-authored-by: Richard
Feldman <oss@rtfeldman.com>

Co-authored-by: Richard Feldman <oss@rtfeldman.com>
2025-07-24 12:50:14 -03:00
Agus Zubiaga
ac3f8f7fa1 Return all diffs
Co-authored-by: Ben Brandt <benjamin.j.brandt@gmail.com>
Co-authored-by: Richard Feldman <oss@rtfeldman.com>
2025-07-24 12:48:26 -03:00
Agus Zubiaga
153ffb2e6b Tests passing
Co-authored-by: Ben Brandt <benjamin.j.brandt@gmail.com>
Co-authored-by: Richard Feldman <oss@rtfeldman.com>
2025-07-24 12:37:06 -03:00
Agus Zubiaga
cdfe91ce9e Move process creation to ClaudeConnection::new_thread
Co-authored-by: Ben Brandt <benjamin.j.brandt@gmail.com>
2025-07-24 11:37:55 -03:00
Ben Brandt
bfa6a2290e Merge branch 'main' into mcp2 2025-07-24 12:48:21 +02:00
Ben Brandt
56ca10dccd Fix authenticate not working without a thread 2025-07-24 12:38:50 +02:00
Ben Brandt
17684703da Move mcp_server back to Claude 2025-07-24 12:14:50 +02:00
Ben Brandt
755cff59a1 Put name on connection as well for Thread Title 2025-07-24 12:06:05 +02:00
Ben Brandt
44cae51ea3 Make new_thread nicer with RC receiver 2025-07-24 11:51:19 +02:00
Ben Brandt
45452a81d4 Cleanup unused fields 2025-07-24 11:41:20 +02:00
Ben Brandt
c03585359a Remove dead code 2025-07-24 11:01:49 +02:00
Ben Brandt
e9a9f6dace Remove unused code 2025-07-24 10:53:54 +02:00
Finn Evers
dd52fb58fe terminal_view: Ensure breadcrumbs are updated on settings change (#35016)
Currently, terminal breadcrumbs are only updated after a settings change
once the terminal view is focused again. This change ensures that the
breadcrumbs are updated instantaneously once the breadcrumb settings
changes.

Release Notes:

- Fixed an issue where terminal breadcrumbs would not react instantly to
settings changes.
2025-07-24 08:51:40 +00:00
Joseph T. Lyons
913b9296d7 Add editor: convert to sentence case (#35015)
This PR adds an `editor: convert to sentence case` action.

I frequently find myself copying branch names and then removing the
hyphens and ensuring the first letter is capitalized, and then using the
result text for the commit message.

For example:

<img width="927" height="482" alt="image"
src="https://github.com/user-attachments/assets/adf14a37-a92e-44df-8c0e-267b5c7677fb"
/>

You can achieve this with a combination of other text manipulation
commands, but this action makes it even easier.

Also, moved `toggle_case` down into the area where all other commands
internally using `manipulate_text` are located.

Release Notes:

- Added `editor: convert to sentence case`
2025-07-24 08:49:04 +00:00
Joseph T. Lyons
5c9363b1c4 Differentiate between file and selection diff events (#35014)
Release Notes:

- N/A
2025-07-24 08:43:28 +00:00
Finn Evers
cd9bcc7f09 agent_ui: Improve wrapping behavior in provider configuration header (#34989)
This ensures that the "Add provider" button does not move offscreen too
fast and ensures the text wraps for smaller panel sizes.

| Before | After |
| --- | --- |
| <img width="413" height="84" alt="image"
src="https://github.com/user-attachments/assets/565f7503-bddb-4b05-83c1-8f8745ac3ce3"
/> | <img width="392" height="84" alt="image"
src="https://github.com/user-attachments/assets/18469e4d-d94c-4641-9081-1af8981bfffd"
/> |

Release Notes:

- N/A
2025-07-24 10:40:36 +02:00
Ben Brandt
0915e4b34b Fix clippy warnings 2025-07-24 10:40:05 +02:00
Jason Lee
65759d4316 gpui: Fix Interactivity prepaint to update scroll_handle bounds (#35013)
It took a long time to check this problem.

Finally, I found that due to a detail missing when changing #34832, the
bounds of `ScrollHandle` was not updated in the Interactivity `prepaint`
phase.

```diff
- scroll_handle_state.padded_content_size = padded_content_size;
+ scroll_handle_state.max_offset = scroll_max;
```

It was correct before the change, because the `padded_content_size`
(including `bounds.size`) was saved before, and the bounds was missing
after changing to `max_offset`, but the bounds were not updated
anywhere.

So when `scroll_handle.bounds()` is obtained outside, it is always 0px
here.

@MrSubidubi

Release Notes:

- N/A
2025-07-24 08:27:29 +00:00
Joseph T. Lyons
ddd50aabba Fix some bugs with editor: diff clipboard with selection (#34999)
Improves testing around `editor: diff clipboard with selection` as well.

Release Notes:

- Fixed some bugs with `editor: diff clipboard with selection`
2025-07-24 02:52:02 -04:00
Conrad Irwin
34bf6ebba6 Disable auto-close in search (#35005)
Currently if you type `\(`, it auto-closes to `\()` which is broken.

It's arguably nice that if you type `(` it auto-closes to `()`, but I am
much more likely to be looking for a function call `name\(` than to be
starting a group in search.

Release Notes:

- search: Regex search will no longer try to close parenthesis
automatically.
2025-07-24 05:45:01 +00:00
Pablo Ramón Guevara
a6956eebcb Improve Helix insert (#34765)
Closes #34763 

Release Notes:

- Improved insert in `helix_mode` when a selection exists to better
match helix's behavior: collapse selection to avoid replacing it
- Improved append (`insert_after`) to better match helix's behavior:
move cursor to end of selection if it exists
2025-07-23 23:27:07 -06:00
AidanV
8b0ec287a5 vim: Add :norm support (#33232)
Closes #21198

Release Notes:

- Adds support for `:norm`
- Allows for vim and zed style modified keys specified in issue
  - Vim style <C-w> and zed style <ctrl-w>
- Differs from vim in how multi-line is handled 
  - vim is sequential
  - zed is combinational (with multi-cursor)
2025-07-23 23:06:05 -06:00
versecafe
c08851a85e ollama: Add Magistral to Ollama (#35000)
See also: #34983

Release Notes:

- Added magistral support to ollama
2025-07-24 00:17:54 -04:00
Agus Zubiaga
8f768439b2 Compiling! 2025-07-24 01:03:37 -03:00
Peter Tripp
b93e1c736b mistral: Add support for magistral-small and magistral-medium (#34983)
Release Notes:

- mistral: Added support for magistral-small and magistral-medium
2025-07-23 23:13:49 -04:00
Agus Zubiaga
2ffc027c26 Fix all agent_servers errors 2025-07-23 23:26:31 -03:00
Agus Zubiaga
158867f42c Moar fixes 2025-07-23 21:25:04 -03:00
Piotr Osiewicz
67027bb241 agent: Fix Zed header in settings view (#34993)
Follow-up to taffy bump (#34939), fixes an issue reported by @MrSubidubi


Release Notes:

- N/A
2025-07-24 00:13:47 +00:00
Smit Barmase
31afda3c0c project_panel: Automatically open project panel when Rename or Duplicate is triggered from workspace (#34988)
In project panel, `rename` and `duplicate` action further needs user
input for editing, so if panel is closed we should open it.

Release Notes:

- Fixed project panel not opening when `project panel: rename` and
`project panel: duplicate` actions are triggered from workspace.
2025-07-24 05:26:12 +05:30
Agus Zubiaga
172d206e02 Replace codex impl with todo! 2025-07-23 20:32:20 -03:00
Marshall Bowers
3d4266bb8f collab: Remove POST /billing/subscriptions/manage endpoint (#34986)
This PR removes the `POST /billing/subscriptions/manage` endpoint, as it
has been moved to `cloud.zed.dev`.

Release Notes:

- N/A
2025-07-23 23:30:00 +00:00
Agus Zubiaga
7f20caf208 Moar progress 2025-07-23 20:11:38 -03:00
Maksim Bondarenkov
4a87397d37 livekit_client: Revert a change that broke MinGW builds (#34977)
the change was made in https://github.com/zed-industries/zed/pull/34223
for unknown reason. it wasn't required actually, and the code can be
safely left as before

update: after this revert Zed compiles with MinGW as before

Release Notes:

- N/A
2025-07-24 01:53:13 +03:00
Piotr Osiewicz
3da23cc65b Re-land taffy 0.8.3 (#34939)
Re #34938
- **chore: Bump taffy to 0.8.3**
- **editor: Fix sticky multi-buffer header not extending to the full
width**


Release Notes:

- N/A
2025-07-24 00:33:43 +02:00
Smit Barmase
b63d820be2 editor: Fix move line up panic when selection is at end of line next to fold marker (#34982)
Closes #34826

In move line up method, make use of `prev_line_boundary` which accounts
for fold map, etc., for selection start row so that we don't incorrectly
calculate row range to move up.

Release Notes:

- Fixed an issue where `editor: move line up` action sometimes crashed
if the cursor was at the end of a line beside a fold marker.
2025-07-24 03:46:29 +05:30
Renato Lochetti
7e9d6cc25c mistral: Add support for Mistral Devstral Medium (#34888)
Mistral released their new DevstralMedium model to be used via API:
https://mistral.ai/news/devstral-2507

Release Notes:

- Add support for Mistral Devstral Medium
2025-07-23 17:27:25 -04:00
Danilo Leal
8bf7dcb613 agent: Fix follow button disabled state (#34978)
Release Notes:

- N/A
2025-07-23 17:09:05 -04:00
Peter Tripp
edceb7284f Redact secrets from environment in LSP Server Info (#34971)
In "Server Info" view of LSP logs:
- Redacts sensitive values from environment
- Sorts environment by name

| Before | After | 
| - | - | 
| <img width="797" height="327" alt="Screenshot 2025-07-23 at 14 10 14"
src="https://github.com/user-attachments/assets/75781f30-9099-4994-9824-94d9c46f63e1"
/> | <img width="972" height="571" alt="image"
src="https://github.com/user-attachments/assets/c5bef744-a1b7-415f-9eb7-8314275c59b9"
/> |


Release Notes:

- Improved display of environment variables in LSP Logs: Server Info
view
2025-07-23 16:55:13 -04:00
Joseph T. Lyons
50985b7d23 Fix telemetry event type names (#34974)
Release Notes:

- N/A
2025-07-23 20:30:21 +00:00
Agus Zubiaga
2d222f7752 Fix tool call confirmation 2025-07-23 16:17:33 -03:00
Nicolas Rodriguez
be0d9eecb7 Add collapse functionality to outline entries (#33490)
partly Closes #23075 

Release Notes:

- Now provides collapse and enables functionality to outline entries
- Add a new expand_outlines_with_depth setting to customize how deep the
tree is expanded by when a file is opened

part 2 is in #34164 

**Visual examples**

![image](https://github.com/user-attachments/assets/5dcdb83b-6e3e-4bfd-8ef4-76ae2ce4d3e6)

![image](https://github.com/user-attachments/assets/7b786a5a-1a8c-4f34-aaa5-4a8d0afa9668)

![image](https://github.com/user-attachments/assets/1817be06-ac71-4480-8f17-0bd862e913c8)
2025-07-23 18:52:44 +00:00
Umesh Yadav
9863c8a44e agent_ui: Show keybindings for NewThread and NewTextThread in new thread button (#34967)
I believe in this PR: #34829 we moved to context menu entry from action
but the side effect of that was we also removed the Keybindings from
showing it in the new thread button dropdown. This PR fixes that. cc
@danilo-leal

| Before | After |
|--------|--------|
| <img width="900" height="1962" alt="CleanShot 2025-07-23 at 23 36
28@2x"
src="https://github.com/user-attachments/assets/760cbe75-09b9-404b-9d33-1db73785234f"
/> | <img width="850" height="1964" alt="CleanShot 2025-07-23 at 23 37
17@2x"
src="https://github.com/user-attachments/assets/24a7e871-aebc-475c-845f-b76f02527b8f"
/> |

Release Notes:

- N/A
2025-07-23 18:28:05 +00:00
Joseph T. Lyons
a48247a313 Bump Zed to v0.198 (#34964)
Release Notes:

-N/A
2025-07-23 18:14:39 +00:00
Julia Ryan
5f0edd38f8 Add TestPanic feature flag (#34963)
Now the `dev: panic` action can be run on all release channels if the
user has the feature flag enabled.

Release Notes:

- N/A
2025-07-23 18:01:16 +00:00
Anthony Eid
56b64b1d3f keymap ui: Improve resize columns on double click (#34961)
This PR splits the resize logic into separate left/right propagation
methods and improve code organization around column width adjustments.
It also allows resize to work for both the left and right sides as well,
instead of only checking the right side for room

Release Notes:

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

---------

Co-authored-by: Ben Kunkle <ben@zed.dev>
2025-07-23 13:45:49 -04:00
Danilo Leal
fdcd86617a ai onboarding: Add telemetry event capturing (#34960)
Release Notes:

- N/A

Co-authored-by: Katie Geer <katie@zed.dev>
Co-authored-by: Joseph T. Lyons <JosephTLyons@gmail.com>
2025-07-23 17:33:53 +00:00
Michael Sloan
3b428e2ecc Remove !menu from j k binding in initial keymap examples (#34959)
See
https://github.com/zed-industries/zed/pull/34912#issuecomment-3108802582

Release Notes:

- N/A
2025-07-23 17:33:40 +00:00
Conrad Irwin
1724bb2084 more wip 2025-07-23 10:59:09 -06:00
Anthony Eid
986b446749 keymap ui: Resizable column follow up (#34955)
I cherry picked a small fix that didn't get into the original column
resizable branch PR because I turned on auto merge.

Release Notes:

- N/A
2025-07-23 12:18:55 -04:00
Finn Evers
8713c556d6 keymap_ui: Dim keybinds that are overridden by other keybinds (#34952)
This change dims rows in the keymap editor for which the corresponding
keybind is overridden by other keybinds coming from higher priority
sources.

Release Notes:

- N/A
2025-07-23 16:03:04 +00:00
Mikayla Maki
326fe05b33 Resizable columns (#34794)
This PR adds resizable columns to the keymap editor and the ability to
double-click on a resizable column to set a column back to its default
size.

The table uses a column's width to calculate what position it should be
laid out at. So `column[i]` x position is calculated by the summation of
`column[..i]`. When resizing `column[i]`, `column[i+1]`’s size is
adjusted to keep all columns’ relative positions the same. If
`column[i+1]` is at its minimum size, we keep seeking to the right to
find a column with space left to take.

An improvement to resizing behavior and double-clicking could be made by
checking both column ranges `0..i-1` and `i+1..COLS`, since only one
range of columns is checked for resize capacity.

Release Notes:

- N/A

---------

Co-authored-by: Anthony <anthony@zed.dev>
Co-authored-by: Remco Smits <djsmits12@gmail.com>
2025-07-23 15:44:45 +00:00
Agus Zubiaga
8affe205bd More progress 2025-07-23 12:17:02 -03:00
claytonrcarter
1f4c9b9427 language: Update block_comment and documentation comment (#34861)
As suggested in https://github.com/zed-industries/zed/pull/34418, this
proposes various changes to language configs to make block comments and
doc-block-style comments more similar. In doing so, it introduces some
breaking changes into the extension schema.

This change is needed to support the changes I'm working on in #34418,
to be able to support `rewrap` in block comments like `/* really long
comment ... */`. As is, we can do this in C-style doc-block comments (eg
`/** ... */`) because of the config in `documentation`, but we can't do
this in regular block comments because we lack the info about what the
line prefix and indentation should be.

And while I was here, I did various other clean-ups, many of which feel
nice but are optional.

I would love special attention on the changes to the schema, version and
related changes; I'm totally unfamiliar with that part of Zed.

**Summary of changes**
- break: changes type of `block_comment` to same type as
`documentation_comment` (**this is the important change**)
- break: rename `documentation` to `documentation_comment` (optional,
but improves consistency w/ `line_comments` and `block_comment`)
- break/refactor?: removes some whitespace in the declaration of
`block_comment` delimiters (optional, may break things, need input; some
langs had no spaces, others did)
- refactor: change `tab_size` from `NonZeroU32` to just a `u32` (some
block comments don't seem to need/want indent past the initial
delimiter, so we need this be 0 sometimes)
- refactor: moves the `documentation_comment` declarations to appear
next to `block_comment`, rearranges the order of the fields in the TOML
for `documentation_comment`, rename backing `struct` (all optional)

**Future scope**
I believe that this will also allow us to extend regular block comments
on newline – as we do doc-block comments – but I haven't looked into
this yet. (eg, in JS try pressing enter in both of these: `/* */` and
`/** */`; the latter should extend w/ a `*` prefixed line, while the
former does not.)

Release Notes:

- BREAKING CHANGE: update extension schema version from 1 to 2, change
format of `block_comment` and rename `documentation_comment`

/cc @smitbarmase
2025-07-23 20:38:52 +05:30
Marshall Bowers
14171e0721 collab: Add POST /users/:id/update_plan endpoint (#34953)
This PR adds a new `POST /users/:id/update_plan` endpoint to Collab to
allow Cloud to push down plan updates to users.

Release Notes:

- N/A
2025-07-23 14:30:08 +00:00
Ben Brandt
ebf94e20c9 wip
Co-authored-by: Agus Zubiaga <agus@zed.dev>
2025-07-23 16:21:20 +02:00
Peter Tripp
326ab5fa3f Improve collab channel organization keybinds (#34821)
Change channel reorganization (move up/down) from `cmd-up/down` (mac) /
`ctrl-up/down` (linux) to `alt-up/down` (both) to match moving lines in
the editor.

Also fix an issue where if you selected channels using down/up in the
filter field, the movement shortcuts would not work (`editing` vs
`not_editing`).

Release Notes:

- N/A
2025-07-23 10:04:53 -04:00
Ben Brandt
edbcc71b87 Merge branch 'main' into mcp2 2025-07-23 14:36:06 +02:00
Ben Brandt
c478cdf30e Use acp_old everywhere 2025-07-23 14:33:26 +02:00
Ben Brandt
af6ab9d4a6 Update plan + read/write tools 2025-07-23 13:29:31 +02:00
Ben Brandt
eff09b0384 Remove ToolCallConfirmation 2025-07-23 13:22:51 +02:00
Ben Brandt
ce5614cc76 Reject pending tool call confirmations 2025-07-23 13:21:33 +02:00
Ben Brandt
cea8e61fd9 Push request and insert further 2025-07-23 13:11:54 +02:00
Ben Brandt
c3e4f3800d Convert new toolcallcontent 2025-07-23 12:31:54 +02:00
Finn Evers
2bc6e18ac9 Ensure disable_ai is properly respected (#34941)
Quick follow up to #34896 which ensures that the Agent Panel cannot be
caught by actions like `workspace: toggle left dock` when `disable_ai`
is set to true.

Also removes a method that was introduced but unused in the workspace
because `first_enabled_panel_idx` already covers all cases this method
could be useful for.

Release Notes:

- N/A
2025-07-23 12:20:09 +02:00
Piotr Osiewicz
c2c2264a60 gpui: Add tree example (#34942)
This commit adds an example with deep children hierarchy.
The depth of a tree can be tweaked with GPUI_TREE_DEPTH env variable.
With depth=100
<img width="301" height="330" alt="image"
src="https://github.com/user-attachments/assets/844cd285-c5f3-4410-a74e-981bf093ba2e"
/>
With this example, I can trigger a stack overflow at depth=633 (and
higher).


Release Notes:

- N/A
2025-07-23 12:17:23 +02:00
Piotr Osiewicz
6cd3726a5a Revert "chore: Bump taffy to 0.8.3" (#34938)
Reverts zed-industries/zed#34876

From our Slack:
<img width="1694" height="1610" alt="image"
src="https://github.com/user-attachments/assets/c7b8f02a-8609-4ed3-9cd5-7d05d152e40e"
/>


https://github.com/user-attachments/assets/828964be-9b6e-4496-9361-9e3a2e9aa208

Release Notes:
- N/A
2025-07-23 10:28:06 +02:00
tidely
7db110f48d sum_tree: Utilize size_hint in TreeSet::extend (#34936)
Collect the iterator instead of manually looping over it to utilize
possible size hints. Zed usually passes in owned `Vec`'s, meaning we get
to reuse memory as well.

Release Notes:

- N/A
2025-07-23 09:57:57 +02:00
maan2003
6122f46095 project: Fix search filter patterns on remote projects (#34748)
we were join(",") and split(",") to serialize the patterns.

This doesn't work when pattern includes a ","
example: *.{ts,tsx} (very common pattern used by agent)


help needed:

how will this work on version mismatch?

Release Notes:

- Fixed search filter patterns on remote projects.
2025-07-23 00:18:45 -06:00
Conrad Irwin
4cd5cceae8 MOAR 2025-07-23 00:12:09 -06:00
Joseph T. Lyons
500ceaabcd Add an editor: diff clipboard with selection action (#33283)
https://github.com/user-attachments/assets/d472fbdd-7736-4bd7-8a90-8cca356b2815

This PR adds `editor: diff clipboard with selection` - good for spotting
the differences in eerily-similar code, which is when refactoring code,
as you need to see what needs to be passed in in order to maintain
previous behavior of both snippets.

1. Copy some text from anywhere
2. Highlight some text in Zed
3. Run `editor: diff clipboard with selection`

Like JetBrains' IDEs and VS Code with the `PartialDiff` package, if the
selection is empty, we take the entire buffer as the selection.

Caveats:

- We do not know the language of the text in the clipboard. I went ahead
and just assumed that in most cases, it will be the same language as the
selected text, which does mean we will highlight the old text
incorrectly if they are copying from a different language, but I think
in most cases, it will be the same, and the alternative of always having
no syntax highlighting is worse. PyCharm seems to do the same thing.

Release Notes:

- Added an `editor: diff clipboard with selection` action

---------

Co-authored-by: Junkui Zhang <364772080@qq.com>
Co-authored-by: Ben Kunkle <ben@zed.dev>
2025-07-23 02:39:32 +00:00
Jason Lee
3e27fa1d92 gpui: Allow Animation to be cloned (#34933)
Release Notes:

- N/A

---

Let `Animation` able to clone, then we can define one to share for
multiple places.

For example:

<img width="914" height="637" alt="image"
src="https://github.com/user-attachments/assets/8eafb318-afba-4399-a975-d83cb7afe74c"
/>
2025-07-23 02:10:40 +00:00
Smit Barmase
11ac83f3d4 workspace: Fix closing remote workspace restores last local workspace on startup (#34931)
Closes #7759

While opening a new SSH project if we are reusing an existing window,
i.e. drop the existing workspace and create a new one, then before
dropping it, we should remove `session_id` from that workspace's
serialized entry. That way:

1. Upon closing (cmd-w) this remote workspace (which also clears
`session_id` for this), and then quitting. No workspace with that
`session_id` is found, and it starts fresh.
2. Upon directly quitting (cmd-q) this remote workspace, only this
workspace exists in db (among two of them) with that `session_id`, and
it restores correctly.
 
Release Notes:

- Fixed an issue while closing remote workspace restores last local
workspace on startup.
2025-07-23 06:48:31 +05:30
Smit Barmase
e90cf0b941 workspace: Fix last removed folder from workspace used to reopen on Zed startup (#34925)
Closes #34924

Now, when `local_paths` are empty, we detach `session_id` from that
workspace serialization item. This way, when we restore it using the
default "last_session", we don't restore this workspace back. This is
same as when we use `cmd-w` to close window, which also sets
`session_id` to `None` before serialization.

Release Notes:

- Fixed an issue where last removed folder from workspace used to reopen
on Zed startup.
2025-07-23 06:46:24 +05:30
Conrad Irwin
c4d920ff5b WIPWIPWIPW 2025-07-22 19:01:29 -06:00
Marshall Bowers
056003860a collab: Remove POST /billing/subscriptions endpoint (#34928)
This PR removes the `POST /billing/subscriptions` endpoint, as it has
been moved to `cloud.zed.dev`.

Release Notes:

- N/A
2025-07-22 18:57:07 -04:00
Agus Zubiaga
5d985fa1d8 Improve MCP server responses (#34927)
Release Notes:

- N/A
2025-07-22 22:14:34 +00:00
Marshall Bowers
7f70325a93 language_models: Rename handler to handle in Bedrock provider (#34923)
This PR renames the `handler` field to `handle` on the
`BedrockLanguageModelProvider` and `BedrockModel` structs.

Release Notes:

- N/A
2025-07-22 20:04:08 +00:00
Conrad Irwin
966d29dcd9 WIp 2025-07-22 13:49:28 -06:00
Umesh Yadav
f3c332d839 Fix new crate license symlink (#34922)
The license file is not properly linked to actual license. This was
casued due to new-crate script linking the license to wrong file. Fixed
both of them.

Reference logs:

```
2025-07-22T17:16:19+05:30 ERROR [worktree] error reading target of symlink "/Users/umesh/code/zed/crates/onboarding/LICENSE-GPL": canonicalizing
```

Release Notes:

- N/A
2025-07-22 15:46:25 -04:00
Remco Smits
446d333515 debugger: Fix debug console persist to history when reusing a previous item (#34893)
Closes #34887

Release Notes:

- Debugger: Fix debug console persist to history when reusing a previous
item
2025-07-22 15:40:11 -04:00
Remco Smits
c0f75e1a17 debugger: Fix built-in JavaScript debug tasks were not working due missing type field value (#34894)
Release Notes:

- Debugger: Fix built-in JavaScript debug tasks were not working due
missing `type` field value
2025-07-22 15:39:52 -04:00
Conrad Irwin
cede9d757a Eliciting better 2025-07-22 13:10:06 -06:00
Piotr Osiewicz
708c2645d1 collab: Tweak screen selector appearance (#34919)
Co-authored-by: Danilo Leal <daniloleal09@gmail.com>


Release Notes:

- N/A

Co-authored-by: Danilo Leal <daniloleal09@gmail.com>
2025-07-22 18:53:57 +00:00
Danilo Leal
4272c1508e ai onboarding: Copyedit the whole flow (#34916)
Release Notes:

- N/A

Co-authored-by: Katie Geer <katie@zed.dev>
2025-07-22 15:41:12 -03:00
Conrad Irwin
c0c698b883 WIP elciication
t
2025-07-22 12:29:27 -06:00
Peter Tripp
99466f4aeb Make zooming from menus not persist (#34910)
Closes: https://github.com/zed-industries/zed/issues/34479
Follow-up to: https://github.com/zed-industries/zed/issues/23505

View->Zoom In / Zoom Out / Reset Zoom were not reverted to match when
the default keybindings were reverted.

Release Notes:

- N/A
2025-07-22 13:57:36 -04:00
Marshall Bowers
9e280d0905 collab: Remove unneeded caching of Stripe price IDs by meter ID (#34915)
This PR removes the caching of Stripe price IDs by meter ID on the
`StripeBilling` object, as we weren't actually reading them anywhere.

Release Notes:

- N/A
2025-07-22 17:42:07 +00:00
Michael Sloan
d81a8178e9 Bind "j k" to NormalBefore in initial keymap examples (#34912)
It looks like typically vim configurations bind "j k" to be the same as
escape, which has the "NormalBefore" behavior positioning the block
cursor on the character before the insertion cursor. The [vim mode
docs](https://zed.dev/docs/vim#useful-contexts-for-vim-mode-key-bindings)
also use NormalBefore here.

Thanks to @omniwrench for mentioning this in
https://github.com/zed-industries/zed/discussions/6661#discussioncomment-13848043
. This was a mistake in #31163.

Release Notes:

- N/A
2025-07-22 11:35:58 -06:00
Ben Kunkle
14cea06f0f keymap_ui: Fix panic in clear keystrokes (#34909)
Closes #ISSUE

Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-07-22 13:18:59 -04:00
Piotr Osiewicz
caa520c499 workspace: Clean up empty panes left over from file opening failures (#34908)
Closes #34583

Release Notes:

- Fixed empty pane being left after a binary file is dropped into a new
pane.s
2025-07-22 16:52:17 +00:00
Piotr Osiewicz
64d0fec699 sum_tree: Store context on cursor (#34904)
This gets rid of the need to pass context to all cursor functions. In
practice context is always immutable when interacting with cursors.

A nicety of this is in the follow-up PR we will be able to implement
Iterator for all Cursors/filter cursors (hell, we may be able to get rid
of filter cursor altogether, as it is just a custom `filter` impl on
iterator trait).
Release Notes:

- N/A
2025-07-22 18:20:48 +02:00
Piotr Osiewicz
fa3e1ccc37 chore: Bump taffy to 0.8.3 (#34876)
That's the latest release. Note that we have an opportunity to simplify
our size types per
https://github.com/DioxusLabs/taffy/blob/main/CHANGELOG.md#highlights-1
(though that's left out of this PR)

<img width="1156" height="603" alt="image"
src="https://github.com/user-attachments/assets/cb9501b9-541a-4080-998a-b6347a0c6887"
/>


Release Notes:

- N/A
2025-07-22 18:19:51 +02:00
tiagoq
56b99f49fd bedrock: Fix remaining streaming delays (#33931)
Closes #26030

*Note: This is my first contribution to Zed*

This addresses a second streaming bottleneck in Bedrock that remained
after the initial fix in #28281 (released in preview 194).

The issue is in the mechanism used to convert Zed's internal `AsyncBody`
into the `SdkBody` expected by the Bedrock language provider. We are
using a non-streaming converter that buffers responses.

**How the fix works:**
The AWS SDK provides streaming-compatible converters to create `SdkBody`
instances, but these require the input body to implement the `Body`
trait from the `http-body` crate.

This PR enables streaming by implementing the required trait and
switching to the streaming-compatible converter.

**Changes (2 commits):**

 * 1st Commit - **Implement http-body Body trait for AsyncBody:**
   - Add `http-body = 1.0` dependency (already an indirect dependency)
   - Implement the `Body` trait for our existing `AsyncBody` type
- Uses `poll_frame` to read data chunks asynchronously, preserving
streaming behavior

 * 2nd Commit - **Use streaming-compatible AWS SDK converter:**
- Create `SdkBody` using `SdkBody::from_body_1_x()` with the new `Body`
trait implementation

**Details/FAQ:**

**Q: Why add another dependency?**
A: We tried to avoid adding a dependency, but the AWS SDK requires the
`Body` trait and `http-body` is where it's defined. The crate is already
an indirect dependency, making this a reasonable solution.

**Q: Why modify the shared `http_client` crate instead of just
`aws_bedrock_client`?**
A: We considered implementing the `Body` trait on a wrapper in
`aws_bedrock_client`, but since `AsyncBody` already uses `http` crate
types, extending support to the companion `http-body` crate seems
reasonable and may benefit other integrations.

**Q: How was this bottleneck discovered?**
A: After @5herlocked's initial streaming fix in #28281, I tested preview
194 and noticed streaming still had issues. I found a way to reproduce
the problem and chatted with @5herlocked about it. He immediately
pinpointed the exact location where the issue was occurring, his
diagnosis made this fix possible.

**Q: How does this relate to the previous fix?**
A: #28281 fixed buffering issues higher in the stack, but unfortunately
there was another bottleneck lower-down in the aws-http-client. This PR
addresses that separate buffering issue.

**Q: Does this use zero-copy or one-copy?**
A: The `Body` implementation includes one copy. Someone more
knowledgeable might be able to achieve a zero-copy approach, but we
opted for a conservative approach. The performance impact should not be
perceptible in typical usage.

**Testing:**
Confirmed that Bedrock streaming now works without buffering delays in a
local build.


Release Notes:

- Improved Bedrock streaming by eliminating response buffering delays

---------

Co-authored-by: Marshall Bowers <git@maxdeviant.com>
2025-07-22 11:55:24 -04:00
Ben Kunkle
2b888e1d30 Fix redo after noop format (#34898)
Closes #31917

Previously, as of #28457 we used a hack, creating an empty transaction
in the history that we then merged formatting changes into in order to
correctly identify concurrent edits to the buffer while formatting was
happening. This caused issues with noop formatting however, as using the
normal API of the buffer history (in an albeit weird way) resulted in
the redo stack being cleared, regardless of whether the formatting
transaction included edits or not, which is the correct behavior in all
other contexts.

This PR fixes the redo issue by codifying the behavior formatting wants,
that being the ability to push an empty transaction to the history with
no other side-effects (i.e. clearing the redo stack) to detect
concurrent edits, with the tradeoff being that it must then manually
remove the transaction later if no changes occurred from the formatting.
The redo stack is still cleared when there are formatting edits, as the
individual format steps use the normal `{start,end}_transaction` methods
which clear the redo stack if the finished transaction isn't empty.

Release Notes:

- Fixed an issue where redo would not work after buffer formatting
(including formatting on save) when the formatting did not result in any
changes
2025-07-22 11:45:42 -04:00
Richard Feldman
96f9942791 Add setting to disable all AI features (#34896)
https://github.com/user-attachments/assets/674bba41-40ac-4a98-99e4-0b47f9097b6a


Release Notes:

- Added setting to disable all AI features
2025-07-22 11:32:39 -04:00
Marshall Bowers
939f9fffa3 collab: Remove unneeded caching of Stripe meters (#34900)
This PR removes the caching of Stripe meters on the `StripeBilling`
object, as we weren't actually reading them anywhere.

Release Notes:

- N/A
2025-07-22 15:27:58 +00:00
Bennet Bo Fenner
230061a6cb Support multiple OpenAI compatible providers (#34212)
TODO
- [x] OpenAI Compatible API Icon
- [x] Docs
- [x] Link to docs in OpenAI provider section about configuring OpenAI
API compatible providers

Closes #33992

Related to #30010

Release Notes:

- agent: Add support for adding multiple OpenAI API compatible providers

---------

Co-authored-by: MrSubidubi <dev@bahn.sh>
Co-authored-by: Danilo Leal <daniloleal09@gmail.com>
2025-07-22 12:20:07 -03:00
Ben Kunkle
1a76a6b0bf gpui: Simplify bindings_for_action API (#34857)
Closes #ISSUE

Simplifies the API to no longer have a variant that returns indices. The
downside is that a few places that used to call
`bindings_for_action_with_indices` now compare `Box<dyn Action>` instead
of indices, however the result is the removal of wrapper code and index
handling that is largely unnecessary

Release Notes:

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

Co-authored-by: Conrad <conrad@zed.dev>
2025-07-22 10:59:51 -04:00
Peter Tripp
2eeab5b0bf textmate: Correct context for 'Editor && mode == full' keybinds (#34895)
Closes https://github.com/zed-industries/zed/issues/34891

Release Notes:

- Fixed textmate keymap misbehaving in certain contexts
2025-07-22 14:52:04 +00:00
Ben Kunkle
30177b87d6 Fix detection of pending bindings when binding in parent context matches (#34856)
Broke in #34664

Release Notes:

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

Co-authored-by: Conrad <conrad@zed.dev>
2025-07-22 13:51:30 +00:00
Ben Brandt
0fa7d58a3e Pass Zed MCP Server as command args 2025-07-22 15:11:48 +02:00
Ben Brandt
9b91445967 Use pathbuf in McpServerConfig 2025-07-22 15:06:39 +02:00
Umesh Yadav
31aab89ab0 ai_onboarding: Fix API key onboarding callout not showing properly (#34880)
The current onboarding callout for ApiKeysWithProviders is broken.

| Before | After |
|--------|--------|
| <img width="822" height="1914" alt="CleanShot 2025-07-22 at 16 21
53@2x"
src="https://github.com/user-attachments/assets/5a611a8c-1ca2-4a13-965e-6fbd7cfe757a"
/> | <img width="814" height="1956" alt="CleanShot 2025-07-22 at 16 22
38@2x"
src="https://github.com/user-attachments/assets/3263b804-671a-4637-b5dc-ee7c87befa48"
/> |

cc @danilo-leal 

Release Notes:

- N/A

---------

Co-authored-by: Danilo Leal <daniloleal09@gmail.com>
2025-07-22 12:54:06 +00:00
Danilo Leal
a3850b3d38 agent: Add use_modifier_to_send section in the settings view (#34866)
This PR also converts all of these switch-based settings to use the new
`SwitchField` component, introduced in
https://github.com/zed-industries/zed/pull/34713.

Release Notes:

- agent: Added the ability to change the `use_modifier_to_send` setting
from the agent panel settings UI.
2025-07-22 09:42:34 -03:00
Ben Brandt
480adade63 Add correct codex path and setup e2e tests (not running yet) 2025-07-22 14:13:05 +02:00
Ben Brandt
47dec0df99 Add local command for codex 2025-07-22 13:56:55 +02:00
Oleksiy Syvokon
c7158f0bd7 Improve distinguishing user from agent edits (#34716)
We no longer rely on the `author` field to tell if a change was made by
the user or the agent. The `author` can be set to `User` in many
situations that are not really user-made edits, such as saving a file,
accepting a change, auto-formatting, and more. I started tracking and
fixing some of these cases, but found that inspecting changes in
`diff_base` is a more reliable method.

Also, we no longer show empty diffs. For example, if the user adds a
line and then removes the same line, the final diff is empty, even
though the buffer is marked as user-changed. Now we won't show such
edit.

There are still some issues to address:

- When a user edits within an unaccepted agent-written block, this
change becomes a part of the agent's edit. Rejecting this block will
lose user edits. It won't be displayed in project notifications, either.

- Accepting an agent block counts as a user-made edit.

- Agent start to call `project_notifications` tool after seeing enough
auto-calls.

Release Notes:

- N/A
2025-07-22 14:23:50 +03:00
Ben Brandt
f20edf1b50 Remove unneeded todo 2025-07-22 12:20:52 +02:00
Ben Brandt
03b94f5831 Merge branch 'main' into mcp-codex 2025-07-22 12:20:13 +02:00
Ben Brandt
3a651c546b context_server: Change command string field to PathBuf (#34873)
Release Notes:

- N/A
2025-07-22 12:12:07 +02:00
Bret Comnes
87014cec71 theme: Add panel.overlay_background and panel.overlay_hover (#34655)
In https://github.com/zed-industries/zed/pull/33994 sticky scroll was
added to project_panel.

I love this feature! 

This introduces a new element layering not seen before. On themes that
use transparency, the overlapping elements can make it difficult to read
project panel entries. This PR introduces a new selector:
~~panel.sticky_entry.background~~ `panel.overlay_background` This
selector lets you set the background of entries when they become sticky.

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

Before:

<img width="373" height="104" alt="Screenshot 2025-07-17 at 10 19 11 AM"
src="https://github.com/user-attachments/assets/d5bab065-53ca-4b27-b5d8-3b3f8d1f7a81"
/>

After:

<img width="292" height="445" alt="Screenshot 2025-07-17 at 11 46 57 AM"
src="https://github.com/user-attachments/assets/4cd2b87b-2989-4489-972f-872d2dc13a33"
/>

<img width="348" height="390" alt="Screenshot 2025-07-17 at 11 39 57 AM"
src="https://github.com/user-attachments/assets/49c0757f-2c50-4e01-92c6-2ae7e4132a53"
/>

<img width="668" height="187" alt="Screenshot 2025-07-17 at 11 39 29 AM"
src="https://github.com/user-attachments/assets/167536c2-5872-4306-90c6-c6b68276b618"
/>

Release Notes:

- Add `panel.sticky_entry.background` theme selector for modifying
project panel entries when they become sticky when scrolling and overlap
with entries below them.

---------

Co-authored-by: Smit Barmase <heysmitbarmase@gmail.com>
2025-07-22 15:20:26 +05:30
Danilo Leal
2b671a46f2 ai onboarding: Don't show API keys section if user is already in Pro (#34867)
Release Notes:

- N/A
2025-07-22 05:39:22 +00:00
Danilo Leal
eaccd542fd Add fast-follows to the AI onboarding flow (#34737)
Follow-up to https://github.com/zed-industries/zed/pull/33738.

Release Notes:

- N/A

---------

Co-authored-by: Bennet Bo Fenner <bennetbo@gmx.de>
2025-07-22 02:09:05 -03:00
Marshall Bowers
5a530ecd39 zed: Add support for zed://agent links (#34862)
This PR adds support for `zed://agent` links for opening the Agent
Panel.

Release Notes:

- N/A
2025-07-22 01:40:33 +00:00
Daste
233e66d35f Add editor::BlameHover action for triggering the blame popover via keyboard (#32096)
Make the git blame popover available via the keymap by making it an
action. The blame popover stays open after being shown via the action,
similar to the `editor::Hover` action.

I added a default vim-mode key binding for `g b`, which goes in hand
with `g h` for hover. I'm not sure what the keybind would be for regular
layouts, if any would be set by default.

I'm opening this as a draft because I coludn't figure out a way to
position the popover correctly above/under the cursor head. I saw some
uses of `content_origin` in other places for calculating absolute pixel
positions, but I'm not sure how to make use of it here without doing a
big refactor of the blame popover code 🤔. I would appreciate some
help/tips with positioning, because it seems like the last thing to
implement here.

Opening as a draft for now because I think without the correct
positioning this feature is not complete.

Closes https://github.com/zed-industries/zed/discussions/26447

Release Notes:

- Added `editor::BlameHover` action for showing the git blame popover
under the cursor. By default bound to `ctrl-k ctrl-b` and to `g h` in
vim mode.
2025-07-21 19:30:23 -06:00
Marshall Bowers
15353630e4 zed: Add OpenRequestKind (#34860)
This PR refactors the `OpenRequest` to introduce an `OpenRequestKind`
enum.

It seems most of the fields on `OpenRequest` are mutually-exclusive, so
it is better to model it as an enum rather than using a bunch of
`Option`s.

There are likely more of the existing fields that can be converted into
`OpenRequestKind` variants, but I'm being conservative for this first
pass.

Release Notes:

- N/A
2025-07-22 01:11:11 +00:00
Marshall Bowers
5289b815fe ai_onboarding: Send users directly into the trial checkout flow when starting the trial (#34859)
This PR makes it so users will be sent immediately into the trial
checkout flow (by hitting zed.dev/account/start-trial) when they click
the "Start Pro Trial" button.

Release Notes:

- N/A
2025-07-21 23:58:16 +00:00
Danilo Leal
8515487bbc agent: Add new thread start buttons to the empty state (#34829)
Release Notes:

- N/A
2025-07-21 20:39:29 -03:00
Agus Zubiaga
e7298c0736 Display tool calls 2025-07-21 20:23:27 -03:00
Sergei Surovtsev
19ab1eb792 Fix an issue where xkb defined hotkeys for arrows would not work (#34823)
Addresses
https://github.com/zed-industries/zed/pull/34053#issuecomment-3096447601
where custom-defined arrows would stop working in Zed.

How to reproduce:

1. Define custom keyboard layout

```bash
cd /usr/share/X11/xkb/symbols/
sudo nano mykbd
```

```
default partial alphanumeric_keys
xkb_symbols "custom" {

    name[Group1]= "Custom Layout";

    key <AD01> { [ q,  Q,  Escape,     Escape      ] };
    key <AD02> { [ w,  W,  Home,       Home        ] };
    key <AD03> { [ e,  E,  Up,         Up          ] };
    key <AD04> { [ r,  R,  End,        End         ] };
    key <AD05> { [ t,  T,  Tab,        Tab         ] };

    key <AC01> { [ a,  A,  Return,     Return      ] };
    key <AC02> { [ s,  S,  Left,       Left        ] };
    key <AC03> { [ d,  D,  Down,       Down        ] };
    key <AC04> { [ f,  F,  Right,      Right       ] };
    key <AC05> { [ g,  G,  BackSpace,  BackSpace   ] };

    // include a base layout to inherit the rest
    include "us(basic)"
};
```

2. Activate custom layout with win-key as AltGr

```bash
setxkbmap mykbd -variant custom -option lv3:win_switch
```

3. Now Win-S should produce left arrow, Win-F right arrow
4. Test whether it works in Zed

Release Notes:

 - linux: xkb-defined hotkeys for arrow keys should behave as expected.

---------

Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
2025-07-21 17:16:44 -06:00
Agus Zubiaga
a822711e99 Stop generation 2025-07-21 19:46:14 -03:00
Agus Zubiaga
4b1ace9a54 Handle Agent Reasoning 2025-07-21 18:29:13 -03:00
Agus Zubiaga
769d6dc632 Stream text 2025-07-21 18:26:28 -03:00
Agus Zubiaga
722a05bc21 Wire up stop button in claude threads (#34839)
Release Notes:

- N/A

---------

Co-authored-by: Ben Brandt <benjamin.j.brandt@gmail.com>
2025-07-21 20:33:59 +00:00
Finn Evers
5b3e371812 gpui: Round scroll_max to two decimal places (#34832)
Follow up to #31836 

After enabling rounding in the Taffy layout engine, we frequently run
into cases where the bounds produced by Taffy and ours slightly differ
after 5 or more decimal places. This leads to cases where containers
become scrollable for less than 0.0000x Pixels. In case this happens for
e.g. hover popovers, we render a scrollbar due to the container being
technically scrollable, even though the scroll amount here will in
practice never be visible.

This change fixes this by rounding the `scroll_max` by which we clamp
the current scroll position to two decimal places. We don't benefit from
the additional floating point precision here at all and it stops such
containers from becoming scrollable altogether. Furthermore, we now
store the `scroll_max` instead of the `padded_content_size` as the
former gives a much better idea on whether the corresponding container
is scrollable or not.

| `main` | After these changes |
| -- | -- |
| <img width="610" height="316" alt="main"
src="https://github.com/user-attachments/assets/ffcc0322-6d6e-4f79-a916-bd3c57fe4211"
/> | <img width="610" height="316" alt="scroll_max_rounded"
src="https://github.com/user-attachments/assets/5fe530f5-2e21-4aaa-81f4-e5c53ab73e4f"
/> |

Release Notes:

- Fixed an issue where scrollbars would appear in containers where no
scrolling was possible.
2025-07-21 22:24:33 +02:00
Peter Tripp
8eca7f32e2 Fix for vim bindings in Pickers on Linux (#34840)
Closes: https://github.com/zed-industries/zed/issues/34780

Also relocated undo/redo selection in the keymap (no-op) as they are
from Sublime, not VSCode.

Release Notes:

- vim: Fixed an issue so `ctrl-w` / `ctrl-h` and `ctrl-u` work in
pickers on Linux when Vim mode is enabled.
2025-07-21 20:09:02 +00:00
Ben Kunkle
1a1715766f Fix enter to select model in agent panel (#34846)
Broken by #34664

Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-07-21 19:16:47 +00:00
Anthony Eid
241acbe4be Stop onboarding page from showing up instead of welcome page (#34845)
This is from PR #34723 where I was working on developing the onboarding
page, but I forgot to switch the first page back to our current version.

Release Notes:

- N/A
2025-07-21 19:02:02 +00:00
Piotr Osiewicz
6ea09beea8 terminal: Handle spaces in cwds of remote terminals (#34844)
Closes #34807

Release Notes:

- Fixed "Open in terminal" action not working with paths that contain
spaces in SSH projects.
2025-07-21 18:51:27 +00:00
Piotr Osiewicz
3e50d997dd agent: Fix double-lease panic when clicking on thread to jump (#34843)
Closes #ISSUE

Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-07-21 18:44:45 +00:00
Richard Feldman
da8bf9ad79 Auto-retry agent errors by default (#34842)
Now we explicitly carve out exceptions for which HTTP responses we do
*not* retry for, and retry at least once on all others.

Release Notes:

- The Agent panel now automatically retries failed requests under more
circumstances.
2025-07-21 14:32:22 -04:00
Agus Zubiaga
f56910556f Connect to Codex MCP server 2025-07-21 15:19:50 -03:00
Bennet Bo Fenner
589af59dfe collab: Refresh the LLM token once the terms of service have been accepted (#34833)
Release Notes:

- N/A
2025-07-21 20:02:28 +02:00
Kirill Bulatov
254c7a330a Regroup LSP context menu items by the worktree name (#34838)
Also 

* remove the feature gate
* open buffers with an error when no logs are present
* adjust the hover text to indicate that difference

<img width="480" height="380" alt="image"
src="https://github.com/user-attachments/assets/6b2350fc-5121-4b1e-bc22-503d964531a2"
/>

Release Notes:

- N/A
2025-07-21 17:48:07 +00:00
Balboa Codes
b6cf398eab docs: Fix PHP docs typo (#34836)
This fixes a minor typo in the PHP docs.

Release Notes:

- N/A
2025-07-21 17:25:59 +00:00
Anthony Eid
bc5c5cf5d6 onboarding: Create basic onboarding page (#34723)
Release Notes:

- N/A

---------

Co-authored-by: Ben Kunkle <ben@zed.dev>
2025-07-21 16:20:38 +00:00
Marshall Bowers
bf8aba566c collab: Remove unused billing preferences queries (#34830)
This PR removes some billing preferences queries that are no longer in
use.

Release Notes:

- N/A
2025-07-21 15:47:40 +00:00
Piotr Osiewicz
e14c9479e4 chore: Pin taffy version (#34827)
Follow-up to 34817

Closes #ISSUE

Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-07-21 15:36:52 +00:00
Marshall Bowers
97af7e1bd9 collab: Remove PUT /billing/preferences endpoint (#34825)
This PR removes the `PUT /billing/preferences` endpoint, as it has been
moved to `cloud.zed.dev`.

Release Notes:

- N/A
2025-07-21 15:29:16 +00:00
Kamal Ahmad
c251f2a2d4 gpui: Throttle interactive resize events on Wayland (#34760)
Wayland compositors can potentially generate thousands of resize
requests when drag-resizing a window, which Zed is unable to keep up
with. This commit changes the behavior to only resize once per vblank
cycle, which helps significantly when resizing the window with a high
poll-rate mouse (1000Hz is common these days)

Here is an example of the behavior pre and post this commit, with some
print-debugging added for tracking resize calls. I have a wireless mouse
with a 2000Hz polling rate and a 165Hz display:

Before: 


https://github.com/user-attachments/assets/4c657f90-5fd2-4809-97ef-363fd48e81b8

After: 


https://github.com/user-attachments/assets/4a0f5fbd-c3c4-40a1-9f71-3b4358c827cf






Closes #20660

Release Notes:

- Improved: Make resizing smoother on Wayland/Linux
2025-07-21 17:48:01 +03:00
Agus Zubiaga
cc56196152 Fix loading agent server settings (#34662)
Release Notes:

- N/A
2025-07-21 14:26:00 +00:00
Agus Zubiaga
405244d422 Display ACP plans (#34816)
Release Notes:

- N/A

---------

Co-authored-by: Danilo Leal <daniloleal09@gmail.com>
2025-07-21 11:11:37 -03:00
Piotr Osiewicz
35b4a918c9 chore: Bump taffy to 0.5.1 (#34817)
We've had problems in the past with bumping past 0.5.2 due to perf
regressions reported by @huacnlee; 0.5.1 was fine though.

Hence, let's bump taffy to 0.5.1 as a safe bet and then try to push past
0.5.2 (after we identify the root cause of regression
Related to #19189
Release Notes:

- N/A
2025-07-21 15:43:51 +02:00
Danilo Leal
56fd950d94 thread view: Add ability to expand message editor and fix scroll (#34766)
Release Notes:

- N/A
2025-07-21 09:26:31 -03:00
Piotr Osiewicz
88af35fe47 collab: Add screen selector (#31506)
Instead of selecting a screen to share arbitrarily, we'll now allow user
to select the screen to share. Note that sharing multiple screens at the
time is still not supported (though prolly not too far-fetched).

Related to #4666

![image](https://github.com/user-attachments/assets/1afb664f-3cdb-4e0a-bb29-9d7093d87fa5)

Release Notes:

- Added screen selector dropdown to screen share button

---------

Co-authored-by: Kirill Bulatov <kirill@zed.dev>
Co-authored-by: Cole Miller <cole@zed.dev>
2025-07-21 13:44:51 +02:00
Ben Brandt
57ab09c2da claude: Don't quote executable path in mcp configuration (#34805)
This was generating an invalid string for the configuration, removing
the extra quotes makes it work. This affected the versions of Zed that
have a space in their name.

Release Notes:

- N/A
2025-07-21 09:53:05 +00:00
Jason Lee
caa4b529e4 gpui: Add tab focus support (#33008)
Release Notes:

- N/A

With a `tab_index` and `tab_stop` option to `FocusHandle` to us can
switch focus by `Tab`, `Shift-Tab`.

The `tab_index` is from
[WinUI](https://learn.microsoft.com/en-us/uwp/api/windows.ui.xaml.controls.control.tabindex?view=winrt-26100)
and [HTML
tabindex](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Global_attributes/tabindex),
only the `tab_stop` is enabled that can be added into the `tab_handles`
list.

- Added `window.focus_next()` and `window.focus_previous()` method to
switch focus.
- Added `tab_index` to `InteractiveElement`.

```bash
cargo run -p gpui --example tab_stop
```


https://github.com/user-attachments/assets/ac4e3e49-8359-436c-9a6e-badba2225211
2025-07-20 16:38:54 -07:00
Michael Sloan
137081f050 Misc code cleanups accumulated while working on other changes (#34787)
Release Notes:

- N/A
2025-07-20 23:22:13 +00:00
Ben Kunkle
7c1040bc93 keymap_ui: Auto complete action arguments (#34785)
Supersedes: #34242

Creates an `ActionArgumentsEditor` that implements the required logic to
have a JSON language server run when editing keybinds so that there is
auto-complete for action arguments.

This is the first time action argument schemas are required by
themselves rather than inlined in the keymap schema. Rather than add all
action schemas to the configuration options we send to the JSON LSP on
startup, this PR implements support for the
`vscode-json-language-server` extension to the LSP whereby the server
will request the client (Zed) to resolve URLs with URI schemes it does
not recognize, in our case `zed://`. This limits the impact on the size
of the configuration options to ~1KB as we send URLs for the language
server to resolve on demand rather than the schema itself. My
understanding is that this is how VSCode handles JSON schemas as well. I
plan to investigate converting the rest of our schema generation logic
to this method in a follow up PR.

Co-Authored-By: Cole <cole@zed.dev>

Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-07-20 16:24:17 -04:00
Michael Sloan
ff79b29f38 Set stripe-mock version to 0.178.0 to match stripe API version used (#34786)
Release Notes:

- N/A
2025-07-20 19:39:04 +00:00
Bennet Bo Fenner
2e41e312ad component preview: Fix Zed AI onboarding young account preview (#34783)
Release Notes:

- N/A
2025-07-20 16:27:18 +00:00
245 changed files with 12728 additions and 5955 deletions

View File

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

70
Cargo.lock generated
View File

@@ -6,6 +6,7 @@ version = 4
name = "acp_thread"
version = "0.1.0"
dependencies = [
"agent-client-protocol",
"agentic-coding-protocol",
"anyhow",
"assistant_tool",
@@ -135,11 +136,21 @@ dependencies = [
"zstd",
]
[[package]]
name = "agent-client-protocol"
version = "0.0.10"
dependencies = [
"schemars",
"serde",
"serde_json",
]
[[package]]
name = "agent_servers"
version = "0.1.0"
dependencies = [
"acp_thread",
"agent-client-protocol",
"agentic-coding-protocol",
"anyhow",
"collections",
@@ -150,7 +161,9 @@ dependencies = [
"indoc",
"itertools 0.14.0",
"language",
"libc",
"log",
"nix 0.29.0",
"paths",
"project",
"schemars",
@@ -162,6 +175,7 @@ dependencies = [
"tempfile",
"ui",
"util",
"uuid",
"watch",
"which 6.0.3",
"workspace-hack",
@@ -192,9 +206,9 @@ version = "0.1.0"
dependencies = [
"acp_thread",
"agent",
"agent-client-protocol",
"agent_servers",
"agent_settings",
"agentic-coding-protocol",
"ai_onboarding",
"anyhow",
"assistant_context",
@@ -207,6 +221,7 @@ dependencies = [
"chrono",
"client",
"collections",
"command_palette_hooks",
"component",
"context_server",
"db",
@@ -228,6 +243,7 @@ dependencies = [
"jsonschema",
"language",
"language_model",
"language_models",
"languages",
"log",
"lsp",
@@ -266,6 +282,7 @@ dependencies = [
"time_format",
"tree-sitter-md",
"ui",
"ui_input",
"unindent",
"urlencoding",
"util",
@@ -279,9 +296,9 @@ dependencies = [
[[package]]
name = "agentic-coding-protocol"
version = "0.0.9"
version = "0.0.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0e276b798eddd02562a339340a96919d90bbfcf78de118fdddc932524646fac7"
checksum = "a3e6ae951b36fa2f8d9dd6e1af6da2fcaba13d7c866cf6a9e65deda9dc6c5fe4"
dependencies = [
"anyhow",
"chrono",
@@ -341,6 +358,7 @@ dependencies = [
"proto",
"serde",
"smallvec",
"telemetry",
"ui",
"workspace-hack",
"zed_actions",
@@ -1866,9 +1884,7 @@ version = "0.1.0"
dependencies = [
"aws-smithy-runtime-api",
"aws-smithy-types",
"futures 0.3.31",
"http_client",
"tokio",
"workspace-hack",
]
@@ -6355,6 +6371,7 @@ dependencies = [
"buffer_diff",
"call",
"chrono",
"client",
"collections",
"command_palette_hooks",
"component",
@@ -7396,9 +7413,9 @@ dependencies = [
[[package]]
name = "grid"
version = "0.13.0"
version = "0.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d196ffc1627db18a531359249b2bf8416178d84b729f3cebeb278f285fb9b58c"
checksum = "71b01d27060ad58be4663b9e4ac9e2d4806918e8876af8912afbddd1a91d5eaa"
[[package]]
name = "group"
@@ -7850,6 +7867,7 @@ dependencies = [
"derive_more 0.99.19",
"futures 0.3.31",
"http 1.3.1",
"http-body 1.0.1",
"log",
"serde",
"serde_json",
@@ -9012,6 +9030,7 @@ dependencies = [
"task",
"text",
"theme",
"toml 0.8.20",
"tree-sitter",
"tree-sitter-elixir",
"tree-sitter-embedded-template",
@@ -9094,11 +9113,11 @@ dependencies = [
"client",
"collections",
"component",
"convert_case 0.8.0",
"copilot",
"credentials_provider",
"deepseek",
"editor",
"fs",
"futures 0.3.31",
"google_ai",
"gpui",
@@ -9165,7 +9184,6 @@ dependencies = [
"collections",
"copilot",
"editor",
"feature_flags",
"futures 0.3.31",
"gpui",
"itertools 0.14.0",
@@ -10983,6 +11001,23 @@ dependencies = [
"workspace-hack",
]
[[package]]
name = "onboarding"
version = "0.1.0"
dependencies = [
"anyhow",
"command_palette_hooks",
"db",
"feature_flags",
"fs",
"gpui",
"settings",
"theme",
"ui",
"workspace",
"workspace-hack",
]
[[package]]
name = "once_cell"
version = "1.21.3"
@@ -14185,7 +14220,7 @@ dependencies = [
[[package]]
name = "scap"
version = "0.0.8"
source = "git+https://github.com/zed-industries/scap?rev=270538dc780f5240723233ff901e1054641ed318#270538dc780f5240723233ff901e1054641ed318"
source = "git+https://github.com/zed-industries/scap?rev=808aa5c45b41e8f44729d02e38fd00a2fe2722e7#808aa5c45b41e8f44729d02e38fd00a2fe2722e7"
dependencies = [
"anyhow",
"cocoa 0.25.0",
@@ -14756,6 +14791,7 @@ dependencies = [
"fs",
"fuzzy",
"gpui",
"itertools 0.14.0",
"language",
"log",
"menu",
@@ -14768,6 +14804,7 @@ dependencies = [
"serde_json",
"settings",
"telemetry",
"tempfile",
"theme",
"tree-sitter-json",
"tree-sitter-rust",
@@ -15935,13 +15972,12 @@ dependencies = [
[[package]]
name = "taffy"
version = "0.4.4"
version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ec17858c2d465b2f734b798b920818a974faf0babb15d7fef81818a4b2d16f1"
checksum = "7aaef0ac998e6527d6d0d5582f7e43953bb17221ac75bb8eb2fcc2db3396db1c"
dependencies = [
"arrayvec",
"grid",
"num-traits",
"serde",
"slotmap",
]
@@ -16483,6 +16519,7 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
name = "title_bar"
version = "0.1.0"
dependencies = [
"anyhow",
"auto_update",
"call",
"chrono",
@@ -18728,8 +18765,7 @@ dependencies = [
[[package]]
name = "windows-capture"
version = "1.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "59d10b4be8b907c7055bc7270dd68d2b920978ffacc1599dcb563a79f0e68d16"
source = "git+https://github.com/zed-industries/windows-capture.git?rev=f0d6c1b6691db75461b732f6d5ff56eed002eeb9#f0d6c1b6691db75461b732f6d5ff56eed002eeb9"
dependencies = [
"clap",
"ctrlc",
@@ -20144,7 +20180,7 @@ dependencies = [
[[package]]
name = "zed"
version = "0.197.0"
version = "0.198.0"
dependencies = [
"activity_indicator",
"agent",
@@ -20185,6 +20221,7 @@ dependencies = [
"extension",
"extension_host",
"extensions_ui",
"feature_flags",
"feedback",
"file_finder",
"fs",
@@ -20222,6 +20259,7 @@ dependencies = [
"nix 0.29.0",
"node_runtime",
"notifications",
"onboarding",
"outline",
"outline_panel",
"parking_lot",

View File

@@ -108,6 +108,7 @@ members = [
"crates/node_runtime",
"crates/notifications",
"crates/ollama",
"crates/onboarding",
"crates/open_ai",
"crates/open_router",
"crates/outline",
@@ -325,6 +326,7 @@ net = { path = "crates/net" }
node_runtime = { path = "crates/node_runtime" }
notifications = { path = "crates/notifications" }
ollama = { path = "crates/ollama" }
onboarding = { path = "crates/onboarding" }
open_ai = { path = "crates/open_ai" }
open_router = { path = "crates/open_router", features = ["schemars"] }
outline = { path = "crates/outline" }
@@ -410,7 +412,8 @@ zlog_settings = { path = "crates/zlog_settings" }
# External crates
#
agentic-coding-protocol = "0.0.9"
agentic-coding-protocol = "0.0.10"
agent-client-protocol = { path = "../agent-client-protocol" }
aho-corasick = "1.1"
alacritty_terminal = { git = "https://github.com/zed-industries/alacritty.git", branch = "add-hush-login-flag" }
any_vec = "0.14"
@@ -480,6 +483,7 @@ heed = { version = "0.21.0", features = ["read-txn-no-tls"] }
hex = "0.4.3"
html5ever = "0.27.0"
http = "1.1"
http-body = "1.0"
hyper = "0.14"
ignore = "0.4.22"
image = "0.25.1"
@@ -553,8 +557,7 @@ rustc-demangle = "0.1.23"
rustc-hash = "2.1.0"
rustls = { version = "0.23.26" }
rustls-platform-verifier = "0.5.0"
# When updating scap rev, also update it in .config/hakari.toml
scap = { git = "https://github.com/zed-industries/scap", rev = "270538dc780f5240723233ff901e1054641ed318", default-features = false }
scap = { git = "https://github.com/zed-industries/scap", rev = "808aa5c45b41e8f44729d02e38fd00a2fe2722e7", default-features = false }
schemars = { version = "1.0", features = ["indexmap2"] }
semver = "1.0"
serde = { version = "1.0", features = ["derive", "rc"] }
@@ -708,6 +711,7 @@ features = [
[patch.crates-io]
notify = { git = "https://github.com/zed-industries/notify.git", rev = "bbb9ea5ae52b253e095737847e367c30653a2e96" }
notify-types = { git = "https://github.com/zed-industries/notify.git", rev = "bbb9ea5ae52b253e095737847e367c30653a2e96" }
windows-capture = { git = "https://github.com/zed-industries/windows-capture.git", rev = "f0d6c1b6691db75461b732f6d5ff56eed002eeb9" }
# Makes the workspace hack crate refer to the local one, but only when you're building locally
workspace-hack = { path = "tooling/workspace-hack" }

View File

@@ -1,3 +1,3 @@
<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M7.8481 26.5925L15.7165 22.1806L15.8481 21.7961L15.7165 21.5836H15.3316L14.0152 21.5027L9.51899 21.3812L5.62025 21.2193L1.84304 21.0169L0.891139 20.8146L0 19.6408L0.0911392 19.0539L0.891139 18.5176L2.03544 18.6188L4.56709 18.7908L8.36456 19.0539L11.119 19.2158L15.2 19.6408H15.8481L15.9392 19.3777L15.7165 19.2158L15.5443 19.0539L11.6152 16.3926L7.36203 13.5796L5.13418 11.9605L3.92911 11.1409L3.32152 10.3719L3.05823 8.69213L4.1519 7.48798L5.62025 7.58917L5.99494 7.69036L7.48354 8.8338L10.6633 11.2927L14.8152 14.3486L15.4228 14.8545L15.6658 14.6825L15.6962 14.5611L15.4228 14.1057L13.1646 10.0278L10.7544 5.87908L9.68101 4.15887L9.39747 3.12674C9.2962 2.70175 9.22532 2.34758 9.22532 1.91247L10.4709 0.222616L11.1595 0L12.8203 0.222616L13.519 0.82975L14.5519 3.18745L16.2228 6.90109L18.8152 11.9504L19.5747 13.448L19.9797 14.8343L20.1316 15.2593H20.3949V15.0164L20.6076 12.173L21.0025 8.68201L21.3873 4.18922L21.519 2.92436L22.1468 1.40653L23.3924 0.586896L24.3646 1.05237L25.1646 2.1958L25.0532 2.93448L24.5772 6.02074L23.6456 10.8576L23.038 14.0956H23.3924L23.7975 13.6909L25.438 11.5153L28.1924 8.07488L29.4076 6.70883L30.8253 5.20111L31.7367 4.48267H33.4582L34.7241 6.36479L34.157 8.30761L32.3848 10.554L30.9165 12.4564L28.8101 15.2897L27.4937 17.5563L27.6152 17.7384L27.9291 17.7081L32.6886 16.6962L35.2608 16.2307L38.3291 15.7045L39.7165 16.3521L39.8684 17.0099L39.3215 18.3557L36.0405 19.1652L32.1924 19.9342L26.4608 21.2902L26.3899 21.3408L26.4709 21.4419L29.0532 21.6848L30.157 21.7455H32.8608L37.8937 22.1199L39.2101 22.9901L40 24.0526L39.8684 24.8621L37.843 25.8943L35.1089 25.2466L28.7291 23.7288L26.5418 23.1824H26.238V23.3645L28.0608 25.1455L31.4025 28.1609L35.5848 32.0465L35.7975 33.0078L35.2608 33.7668L34.6937 33.6858L31.0177 30.9233L29.6 29.6787L26.3899 26.977H26.1772V27.2603L26.9165 28.343L30.8253 34.212L31.0278 36.0132L30.7443 36.6L29.7316 36.9542L28.6177 36.7518L26.3291 33.5441L23.9696 29.9317L22.0658 26.6937L21.8329 26.8252L20.7089 38.9173L20.1823 39.5345L18.9671 40L17.9544 39.231L17.4177 37.9863L17.9544 35.5274L18.6025 32.3198L19.1291 29.7698L19.6051 26.6026L19.8886 25.5502L19.8684 25.4794L19.6354 25.5097L17.2456 28.7883L13.6101 33.6959L10.7342 36.7721L10.0456 37.0453L8.85063 36.428L8.96203 35.3251L9.63038 34.3435L13.6101 29.2841L16.0101 26.1472L17.5595 24.3359L17.5494 24.0729H17.4582L6.88608 30.9335L5.00253 31.1763L4.1924 30.4174L4.29367 29.1728L4.67848 28.768L7.85823 26.5823L7.8481 26.5925Z" fill="black"/>
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M4.35443 9.97775L6.71495 8.65418L6.75443 8.53883L6.71495 8.47508H6.59948L6.20456 8.45081L4.8557 8.41436L3.68608 8.36579L2.55291 8.30507L2.26734 8.24438L2 7.89224L2.02734 7.71617L2.26734 7.55528L2.61063 7.58564L3.37013 7.63724L4.50937 7.71617L5.3357 7.76474L6.56 7.89224H6.75443L6.78176 7.81331L6.71495 7.76474L6.66329 7.71617L5.48456 6.91778L4.20861 6.07388L3.54025 5.58815L3.17873 5.34227L2.99646 5.11157L2.91747 4.60764L3.24557 4.24639L3.68608 4.27675L3.79848 4.30711L4.24506 4.65014L5.19899 5.38781L6.44456 6.30458L6.62684 6.45635L6.69974 6.40475L6.70886 6.36833L6.62684 6.23171L5.94938 5.00834L5.22632 3.76372L4.9043 3.24766L4.81924 2.93802C4.78886 2.81053 4.7676 2.70427 4.7676 2.57374L5.14127 2.06678L5.34785 2L5.84609 2.06678L6.0557 2.24893L6.36557 2.95624L6.86684 4.07033L7.64456 5.58512L7.87241 6.0344L7.99391 6.45029L8.03948 6.57779H8.11847V6.50492L8.18228 5.6519L8.30075 4.6046L8.41619 3.25677L8.4557 2.87731L8.64404 2.42196L9.01772 2.17607L9.30938 2.31571L9.54938 2.65874L9.51596 2.88034L9.37316 3.80622L9.09368 5.25728L8.9114 6.22868H9.01772L9.13925 6.10727L9.6314 5.45459L10.4577 4.42246L10.8223 4.01265L11.2476 3.56033L11.521 3.3448H12.0375L12.4172 3.90944L12.2471 4.49228L11.7154 5.1662L11.275 5.73692L10.643 6.58691L10.2481 7.26689L10.2846 7.32152L10.3787 7.31243L11.8066 7.00886L12.5782 6.86921L13.4987 6.71135L13.915 6.90563L13.9605 7.10297L13.7965 7.50671L12.8122 7.74956L11.6577 7.98026L9.93824 8.38706L9.91697 8.40224L9.94127 8.43257L10.716 8.50544L11.0471 8.52365H11.8582L13.3681 8.63597L13.763 8.89703L14 9.21578L13.9605 9.45863L13.3529 9.76829L12.5327 9.57398L10.6187 9.11864L9.96254 8.95472H9.8714V9.00935L10.4182 9.54365L11.4208 10.4483L12.6754 11.614L12.7393 11.9023L12.5782 12.13L12.4081 12.1057L11.3053 11.277L10.88 10.9036L9.91697 10.0931H9.85316V10.1781L10.075 10.5029L11.2476 12.2636L11.3083 12.804L11.2233 12.98L10.9195 13.0863L10.5853 13.0255L9.89873 12.0632L9.19088 10.9795L8.61974 10.0081L8.54987 10.0476L8.21267 13.6752L8.05469 13.8604L7.69013 14L7.38632 13.7693L7.22531 13.3959L7.38632 12.6582L7.58075 11.6959L7.73873 10.9309L7.88153 9.98078L7.96658 9.66506L7.96052 9.64382L7.89062 9.65291L7.17368 10.6365L6.08303 12.1088L5.22026 13.0316L5.01368 13.1136L4.65519 12.9284L4.68861 12.5975L4.88911 12.303L6.08303 10.7852L6.80303 9.84416L7.26785 9.30077L7.26482 9.22187H7.23746L4.06582 11.2801L3.50076 11.3529L3.25772 11.1252L3.2881 10.7518L3.40354 10.6304L4.35747 9.97469L4.35443 9.97775Z" fill="black"/>
</svg>

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

@@ -1 +1,3 @@
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Google Gemini</title><path d="M11.04 19.32Q12 21.51 12 24q0-2.49.93-4.68.96-2.19 2.58-3.81t3.81-2.55Q21.51 12 24 12q-2.49 0-4.68-.93a12.3 12.3 0 0 1-3.81-2.58 12.3 12.3 0 0 1-2.58-3.81Q12 2.49 12 0q0 2.49-.96 4.68-.93 2.19-2.55 3.81a12.3 12.3 0 0 1-3.81 2.58Q2.49 12 0 12q2.49 0 4.68.96 2.19.93 3.81 2.55t2.55 3.81"/></svg>
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M7.44 12.27C7.81333 13.1217 8 14.0317 8 15C8 14.0317 8.18083 13.1217 8.5425 12.27C8.91583 11.4183 9.4175 10.6775 10.0475 10.0475C10.6775 9.4175 11.4183 8.92167 12.27 8.56C13.1217 8.18667 14.0317 8 15 8C14.0317 8 13.1217 7.81917 12.27 7.4575C11.4411 7.1001 10.6871 6.5895 10.0475 5.9525C9.4105 5.31293 8.8999 4.55891 8.5425 3.73C8.18083 2.87833 8 1.96833 8 1C8 1.96833 7.81333 2.87833 7.44 3.73C7.07833 4.58167 6.5825 5.3225 5.9525 5.9525C5.31293 6.5895 4.55891 7.1001 3.73 7.4575C2.87833 7.81917 1.96833 8 1 8C1.96833 8 2.87833 8.18667 3.73 8.56C4.58167 8.92167 5.3225 9.4175 5.9525 10.0475C6.5825 10.6775 7.07833 11.4183 7.44 12.27Z" fill="black"/>
</svg>

Before

Width:  |  Height:  |  Size: 402 B

After

Width:  |  Height:  |  Size: 762 B

View File

@@ -0,0 +1,4 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M7.25669 0.999943C8.27509 0.993825 9.24655 1.42125 9.9227 2.17279C11.4427 1.85079 12.9991 2.53518 13.7733 3.86518C14.159 4.5149 14.3171 5.26409 14.2372 5.99994H13.2967C13.3789 5.42185 13.265 4.8321 12.9686 4.32514C12.2353 3.06961 10.6088 2.63919 9.33676 3.36322L6.48032 4.98822C6.46926 4.99697 6.46284 5.01135 6.46372 5.02533V6.38568L9.91294 4.42084C10.0565 4.33818 10.2336 4.33823 10.3768 4.42084L13.1502 5.99994H11.2948L9.88364 5.19623C9.87034 5.19054 9.85459 5.19128 9.84262 5.19916L8.64926 5.87983L8.8602 5.99994H7.99985C6.89539 6.00004 5.99988 6.89547 5.99985 7.99994V9.34955L3.90219 8.15522C3.75815 8.07431 3.66897 7.92228 3.66977 7.75873V4.53803C3.66977 4.50828 3.67172 4.4654 3.67172 4.44135C3.08836 4.65262 2.59832 5.0599 2.28794 5.59174C1.55635 6.84647 1.99122 8.44936 3.26059 9.17475L5.99985 10.7363V11.6162C5.87564 11.6568 5.73827 11.6456 5.6229 11.579L2.7977 9.96869C2.77156 9.95382 2.73449 9.9311 2.71372 9.91889C2.60687 10.5231 2.7194 11.1466 3.0311 11.6777C3.6435 12.7209 4.87159 13.1902 5.99985 12.9023V13.8398C4.50443 14.1233 2.98758 13.4424 2.22641 12.1347C1.71174 11.2677 1.60096 10.2237 1.9227 9.27045C0.880739 8.13295 0.703328 6.46023 1.48325 5.13373C1.98739 4.26024 2.84863 3.64401 3.84653 3.44233C4.3245 1.9837 5.70306 0.996447 7.25669 0.999943ZM7.25766 1.91498C5.78932 1.9143 4.59839 3.08914 4.59751 4.53803V7.79193C4.59926 7.80578 4.60735 7.81796 4.61997 7.82416L5.8143 8.50483L5.81626 4.57611C5.81537 4.41216 5.90431 4.2606 6.04868 4.17963L8.87387 2.56928C8.89868 2.55441 8.93612 2.53379 8.95786 2.5224C8.48035 2.13046 7.8788 1.91498 7.25766 1.91498Z" fill="black"/>
<path d="M13.5 6C14.6046 6 15.5 6.89543 15.5 8V13.5C15.5 14.6046 14.6046 15.5 13.5 15.5H8C6.89543 15.5 6 14.6046 6 13.5V8C6 6.89543 6.89543 6 8 6H13.5ZM10.8916 8.02539C10.0563 8.02539 9.33453 8.27982 8.81934 8.76562C8.30213 9.25335 8.02547 9.94371 8.02539 10.748C8.02539 11.557 8.29852 12.2492 8.81543 12.7373C9.33013 13.2232 10.0521 13.4746 10.8916 13.4746C11.9865 13.4745 12.8545 13.1022 13.3076 12.3525C13.3894 12.2176 13.4521 12.0693 13.4521 11.8857C13.4521 11.4795 13.0933 11.2773 12.7842 11.2773C12.6604 11.2774 12.5292 11.3025 12.4072 11.3779C12.2862 11.4529 12.2058 11.5586 12.1494 11.666L12.1475 11.6689C11.9677 12.0213 11.5535 12.246 10.8955 12.2461C10.4219 12.2461 10.0667 12.0932 9.83008 11.8506C9.59255 11.607 9.44141 11.2389 9.44141 10.748C9.44148 10.264 9.59319 9.89628 9.83203 9.65137C10.0702 9.40725 10.4255 9.25391 10.8916 9.25391C11.4912 9.25399 11.9415 9.50614 12.1289 9.8916V9.89062C12.1888 10.0157 12.276 10.1311 12.4023 10.2129C12.5303 10.2956 12.6724 10.3271 12.8115 10.3271C12.9661 10.3271 13.1303 10.2857 13.2627 10.1758C13.4018 10.0603 13.4746 9.89383 13.4746 9.71582C13.4746 9.61857 13.4542 9.52036 13.4199 9.42773L13.3818 9.33691C12.9749 8.49175 11.9927 8.02548 10.8916 8.02539ZM10.3203 8.97852L10.1494 9.03516C10.2095 9.01178 10.2716 8.99089 10.3359 8.97363C10.3307 8.97505 10.3256 8.97706 10.3203 8.97852ZM10.4814 8.94141C10.4969 8.9385 10.5126 8.93616 10.5283 8.93359C10.5126 8.93617 10.4969 8.9385 10.4814 8.94141ZM10.6709 8.91504C10.6819 8.91399 10.693 8.913 10.7041 8.91211C10.693 8.913 10.6819 8.91399 10.6709 8.91504Z" fill="black" fill-opacity="0.95"/>
</svg>

After

Width:  |  Height:  |  Size: 3.2 KiB

View File

@@ -0,0 +1,7 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M14 4H2" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M4.66667 8H2" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M4.66667 12H2" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M8.00016 12C8.41993 12.5597 9.00515 12.9731 9.67294 13.1817C10.3407 13.3903 11.0572 13.3835 11.7209 13.1623C12.3846 12.941 12.9619 12.5166 13.371 11.949C13.78 11.3815 14.0002 10.6996 14.0002 10C14.0002 9.20435 13.6841 8.44129 13.1215 7.87868C12.5589 7.31607 11.7958 7 11.0002 7C10.1135 7 9.30683 7.36 8.72683 7.94L7.3335 9.33333" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M7.3335 6.66669V9.33335H10.0002" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 964 B

View File

@@ -0,0 +1,7 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M7.33333 8H2" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M10.6667 5H2" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M9 11H2" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M12 7V11" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M14 9H10" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 620 B

View File

@@ -0,0 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6.31254 12.549C7.3841 13.0987 8.61676 13.2476 9.78839 12.9688C10.96 12.6901 11.9936 12.0021 12.7028 11.0287C13.412 10.0554 13.7503 8.8607 13.6566 7.66002C13.5629 6.45934 13.0435 5.33159 12.1919 4.48C11.3403 3.62841 10.2126 3.10898 9.01188 3.01531C7.8112 2.92164 6.61655 3.2599 5.64319 3.96912C4.66984 4.67834 3.9818 5.71188 3.70306 6.88351C3.42432 8.05514 3.5732 9.2878 4.12289 10.3594L3 13.6719L6.31254 12.549Z" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 609 B

View File

@@ -0,0 +1,4 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8 13C10.7614 13 13 10.7614 13 8C13 5.23858 10.7614 3 8 3C5.23858 3 3 5.23858 3 8C3 10.7614 5.23858 13 8 13Z" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M6 8L7.33333 9L10 7" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 418 B

View File

@@ -0,0 +1,10 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M7 3C7.66045 3 8.33955 3 9 3" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M11 4C11.3949 4.26602 11.7345 4.60558 12 5" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M13 7C13 7.66045 13 8.33955 13 9" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M12 11C11.734 11.3949 11.3944 11.7345 11 12" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M9 13C8.33954 13 7.66046 13 7 13" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M5 12C4.6051 11.734 4.26554 11.3944 4 11" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M3 9C3 8.33955 3 7.66045 3 7" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M4 5C4.26602 4.6051 4.60558 4.26554 5 4" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -0,0 +1,11 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M7 3C7.66045 3 8.33955 3 9 3" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M11 4C11.3949 4.26602 11.7345 4.60558 12 5" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M13 7C13 7.66045 13 8.33955 13 9" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M12 11C11.734 11.3949 11.3944 11.7345 11 12" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M9 13C8.33954 13 7.66046 13 7 13" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M5 12C4.6051 11.734 4.26554 11.3944 4 11" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M3 9C3 8.33955 3 7.66045 3 7" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M4 5C4.26602 4.6051 4.60558 4.26554 5 4" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M8.00016 8.66665C8.36835 8.66665 8.66683 8.36817 8.66683 7.99998C8.66683 7.63179 8.36835 7.33331 8.00016 7.33331C7.63197 7.33331 7.3335 7.63179 7.3335 7.99998C7.3335 8.36817 7.63197 8.66665 8.00016 8.66665Z" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -277,7 +277,7 @@
}
},
{
"context": "MessageEditor > Editor && !use_modifier_to_send",
"context": "MessageEditor && !Picker > Editor && !use_modifier_to_send",
"bindings": {
"enter": "agent::Chat",
"ctrl-enter": "agent::ChatWithFollow",
@@ -288,7 +288,7 @@
}
},
{
"context": "MessageEditor > Editor && use_modifier_to_send",
"context": "MessageEditor && !Picker > Editor && use_modifier_to_send",
"bindings": {
"ctrl-enter": "agent::Chat",
"enter": "editor::Newline",
@@ -483,9 +483,8 @@
"ctrl-k ctrl-d": ["editor::SelectNext", { "replace_newest": true }], // editor.action.moveSelectionToNextFindMatch / find_under_expand_skip
"ctrl-k ctrl-shift-d": ["editor::SelectPrevious", { "replace_newest": true }], // editor.action.moveSelectionToPreviousFindMatch
"ctrl-k ctrl-i": "editor::Hover",
"ctrl-k ctrl-b": "editor::BlameHover",
"ctrl-/": ["editor::ToggleComments", { "advance_downwards": false }],
"ctrl-u": "editor::UndoSelection",
"ctrl-shift-u": "editor::RedoSelection",
"f8": ["editor::GoToDiagnostic", { "severity": { "min": "hint", "max": "error" } }],
"shift-f8": ["editor::GoToPreviousDiagnostic", { "severity": { "min": "hint", "max": "error" } }],
"f2": "editor::Rename",
@@ -663,6 +662,8 @@
{
"context": "Editor",
"bindings": {
"ctrl-u": "editor::UndoSelection",
"ctrl-shift-u": "editor::RedoSelection",
"ctrl-shift-j": "editor::JoinLines",
"ctrl-alt-backspace": "editor::DeleteToPreviousSubwordStart",
"ctrl-alt-h": "editor::DeleteToPreviousSubwordStart",
@@ -974,9 +975,14 @@
"context": "CollabPanel && not_editing",
"bindings": {
"ctrl-backspace": "collab_panel::Remove",
"space": "menu::Confirm",
"ctrl-up": "collab_panel::MoveChannelUp",
"ctrl-down": "collab_panel::MoveChannelDown"
"space": "menu::Confirm"
}
},
{
"context": "CollabPanel",
"bindings": {
"alt-up": "collab_panel::MoveChannelUp",
"alt-down": "collab_panel::MoveChannelDown"
}
},
{
@@ -1131,7 +1137,10 @@
"alt-ctrl-f": "keymap_editor::ToggleKeystrokeSearch",
"alt-c": "keymap_editor::ToggleConflictFilter",
"enter": "keymap_editor::EditBinding",
"alt-enter": "keymap_editor::CreateBinding"
"alt-enter": "keymap_editor::CreateBinding",
"ctrl-c": "keymap_editor::CopyAction",
"ctrl-shift-c": "keymap_editor::CopyContext",
"ctrl-t": "keymap_editor::ShowMatchingKeybinds"
}
},
{

View File

@@ -318,7 +318,7 @@
}
},
{
"context": "MessageEditor > Editor && !use_modifier_to_send",
"context": "MessageEditor && !Picker > Editor && !use_modifier_to_send",
"use_key_equivalents": true,
"bindings": {
"enter": "agent::Chat",
@@ -330,7 +330,7 @@
}
},
{
"context": "MessageEditor > Editor && use_modifier_to_send",
"context": "MessageEditor && !Picker > Editor && use_modifier_to_send",
"use_key_equivalents": true,
"bindings": {
"cmd-enter": "agent::Chat",
@@ -537,9 +537,8 @@
"ctrl-cmd-d": ["editor::SelectPrevious", { "replace_newest": false }], // editor.action.addSelectionToPreviousFindMatch
"cmd-k ctrl-cmd-d": ["editor::SelectPrevious", { "replace_newest": true }], // editor.action.moveSelectionToPreviousFindMatch
"cmd-k cmd-i": "editor::Hover",
"cmd-k cmd-b": "editor::BlameHover",
"cmd-/": ["editor::ToggleComments", { "advance_downwards": false }],
"cmd-u": "editor::UndoSelection",
"cmd-shift-u": "editor::RedoSelection",
"f8": ["editor::GoToDiagnostic", { "severity": { "min": "hint", "max": "error" } }],
"shift-f8": ["editor::GoToPreviousDiagnostic", { "severity": { "min": "hint", "max": "error" } }],
"f2": "editor::Rename",
@@ -726,6 +725,8 @@
"context": "Editor",
"use_key_equivalents": true,
"bindings": {
"cmd-u": "editor::UndoSelection",
"cmd-shift-u": "editor::RedoSelection",
"ctrl-j": "editor::JoinLines",
"ctrl-alt-backspace": "editor::DeleteToPreviousSubwordStart",
"ctrl-alt-h": "editor::DeleteToPreviousSubwordStart",
@@ -1036,9 +1037,15 @@
"use_key_equivalents": true,
"bindings": {
"ctrl-backspace": "collab_panel::Remove",
"space": "menu::Confirm",
"cmd-up": "collab_panel::MoveChannelUp",
"cmd-down": "collab_panel::MoveChannelDown"
"space": "menu::Confirm"
}
},
{
"context": "CollabPanel",
"use_key_equivalents": true,
"bindings": {
"alt-up": "collab_panel::MoveChannelUp",
"alt-down": "collab_panel::MoveChannelDown"
}
},
{
@@ -1228,10 +1235,14 @@
"context": "KeymapEditor",
"use_key_equivalents": true,
"bindings": {
"cmd-f": "search::FocusSearch",
"cmd-alt-f": "keymap_editor::ToggleKeystrokeSearch",
"cmd-alt-c": "keymap_editor::ToggleConflictFilter",
"enter": "keymap_editor::EditBinding",
"alt-enter": "keymap_editor::CreateBinding"
"alt-enter": "keymap_editor::CreateBinding",
"cmd-c": "keymap_editor::CopyAction",
"cmd-shift-c": "keymap_editor::CopyContext",
"cmd-t": "keymap_editor::ShowMatchingKeybinds"
}
},
{

View File

@@ -13,9 +13,9 @@
}
},
{
"context": "Editor && vim_mode == insert && !menu",
"context": "Editor && vim_mode == insert",
"bindings": {
// "j k": "vim::SwitchToNormalMode"
// "j k": "vim::NormalBefore"
}
}
]

View File

@@ -6,7 +6,7 @@
}
},
{
"context": "Editor",
"context": "Editor && mode == full",
"bindings": {
"cmd-l": "go_to_line::Toggle",
"ctrl-shift-d": "editor::DuplicateLineDown",
@@ -15,7 +15,12 @@
"cmd-enter": "editor::NewlineBelow",
"cmd-alt-enter": "editor::NewlineAbove",
"cmd-shift-l": "editor::SelectLine",
"cmd-shift-t": "outline::Toggle",
"cmd-shift-t": "outline::Toggle"
}
},
{
"context": "Editor",
"bindings": {
"alt-backspace": "editor::DeleteToPreviousWordStart",
"alt-shift-backspace": "editor::DeleteToNextWordEnd",
"alt-delete": "editor::DeleteToNextWordEnd",
@@ -39,10 +44,6 @@
"ctrl-_": "editor::ConvertToSnakeCase"
}
},
{
"context": "Editor && mode == full",
"bindings": {}
},
{
"context": "BufferSearchBar",
"bindings": {

View File

@@ -124,6 +124,7 @@
"g r a": "editor::ToggleCodeActions",
"g g": "vim::StartOfDocument",
"g h": "editor::Hover",
"g B": "editor::BlameHover",
"g t": "pane::ActivateNextItem",
"g shift-t": "pane::ActivatePreviousItem",
"g d": "editor::GoToDefinition",
@@ -219,6 +220,8 @@
{
"context": "vim_mode == normal",
"bindings": {
"i": "vim::InsertBefore",
"a": "vim::InsertAfter",
"ctrl-[": "editor::Cancel",
":": "command_palette::Toggle",
"c": "vim::PushChange",
@@ -352,9 +355,7 @@
"shift-d": "vim::DeleteToEndOfLine",
"shift-j": "vim::JoinLines",
"shift-y": "vim::YankLine",
"i": "vim::InsertBefore",
"shift-i": "vim::InsertFirstNonWhitespace",
"a": "vim::InsertAfter",
"shift-a": "vim::InsertEndOfLine",
"o": "vim::InsertLineBelow",
"shift-o": "vim::InsertLineAbove",
@@ -376,6 +377,8 @@
{
"context": "vim_mode == helix_normal && !menu",
"bindings": {
"i": "vim::HelixInsert",
"a": "vim::HelixAppend",
"ctrl-[": "editor::Cancel",
";": "vim::HelixCollapseSelection",
":": "command_palette::Toggle",
@@ -858,6 +861,14 @@
"shift-n": null
}
},
{
"context": "Picker > Editor",
"bindings": {
"ctrl-h": "editor::Backspace",
"ctrl-u": "editor::DeleteToBeginningOfLine",
"ctrl-w": "editor::DeleteToPreviousWordStart"
}
},
{
"context": "GitCommit > Editor && VimControl && vim_mode == normal",
"bindings": {

View File

@@ -691,7 +691,10 @@
// 5. Never show the scrollbar:
// "never"
"show": null
}
},
// Default depth to expand outline items in the current file.
// Set to 0 to collapse all items that have children, 1 or higher to collapse items at that depth or deeper.
"expand_outlines_with_depth": 100
},
"collaboration_panel": {
// Whether to show the collaboration panel button in the status bar.
@@ -1076,6 +1079,10 @@
// Send anonymized usage data like what languages you're using Zed with.
"metrics": true
},
// Whether to disable all AI features in Zed.
//
// Default: false
"disable_ai": false,
// Automatically update Zed. This setting may be ignored on Linux if
// installed through a package manager.
"auto_update": true,
@@ -1712,6 +1719,7 @@
"openai": {
"api_url": "https://api.openai.com/v1"
},
"openai_compatible": {},
"open_router": {
"api_url": "https://openrouter.ai/api/v1"
},

View File

@@ -15,13 +15,15 @@
"adapter": "JavaScript",
"program": "$ZED_FILE",
"request": "launch",
"cwd": "$ZED_WORKTREE_ROOT"
"cwd": "$ZED_WORKTREE_ROOT",
"type": "pwa-node"
},
{
"label": "JavaScript debug terminal",
"adapter": "JavaScript",
"request": "launch",
"cwd": "$ZED_WORKTREE_ROOT",
"console": "integratedTerminal"
"console": "integratedTerminal",
"type": "pwa-node"
}
]

View File

@@ -60,7 +60,7 @@ services:
- postgres
stripe-mock:
image: stripe/stripe-mock:v0.184.0
image: stripe/stripe-mock:v0.178.0
ports:
- 12111:12111
- 12112:12112

View File

@@ -16,6 +16,7 @@ doctest = false
test-support = ["gpui/test-support", "project/test-support"]
[dependencies]
agent-client-protocol.workspace = true
agentic-coding-protocol.workspace = true
anyhow.workspace = true
assistant_tool.workspace = true

File diff suppressed because it is too large Load Diff

View File

@@ -1,20 +1,26 @@
use agentic_coding_protocol as acp;
use std::{path::Path, rc::Rc};
use agent_client_protocol as acp;
use anyhow::Result;
use futures::future::{FutureExt as _, LocalBoxFuture};
use gpui::{AsyncApp, Entity, Task};
use project::Project;
use ui::App;
use crate::AcpThread;
pub trait AgentConnection {
fn request_any(
&self,
params: acp::AnyAgentRequest,
) -> LocalBoxFuture<'static, Result<acp::AnyAgentResult>>;
}
fn name(&self) -> &'static str;
impl AgentConnection for acp::AgentConnection {
fn request_any(
&self,
params: acp::AnyAgentRequest,
) -> LocalBoxFuture<'static, Result<acp::AnyAgentResult>> {
let task = self.request_any(params);
async move { Ok(task.await?) }.boxed_local()
}
fn new_thread(
self: Rc<Self>,
project: Entity<Project>,
cwd: &Path,
cx: &mut AsyncApp,
) -> Task<Result<Entity<AcpThread>>>;
fn authenticate(&self, cx: &mut App) -> Task<Result<()>>;
fn prompt(&self, params: acp::PromptToolArguments, cx: &mut App) -> Task<Result<()>>;
fn cancel(&self, session_id: &acp::SessionId, cx: &mut App);
}

View File

@@ -0,0 +1,461 @@
///! Translates old acp agents into the new schema
use agent_client_protocol as acp;
use agentic_coding_protocol::{self as acp_old, AgentRequest as _};
use anyhow::{Context as _, Result};
use futures::channel::oneshot;
use gpui::{AppContext as _, AsyncApp, Entity, Task, WeakEntity};
use project::Project;
use std::{cell::RefCell, error::Error, fmt, path::Path, rc::Rc};
use ui::App;
use crate::{AcpThread, AcpThreadEvent, AgentConnection, ToolCallContent, ToolCallStatus};
#[derive(Clone)]
pub struct OldAcpClientDelegate {
thread: Rc<RefCell<WeakEntity<AcpThread>>>,
cx: AsyncApp,
next_tool_call_id: Rc<RefCell<u64>>,
// sent_buffer_versions: HashMap<Entity<Buffer>, HashMap<u64, BufferSnapshot>>,
}
impl OldAcpClientDelegate {
pub fn new(thread: Rc<RefCell<WeakEntity<AcpThread>>>, cx: AsyncApp) -> Self {
Self {
thread,
cx,
next_tool_call_id: Rc::new(RefCell::new(0)),
}
}
}
impl acp_old::Client for OldAcpClientDelegate {
async fn stream_assistant_message_chunk(
&self,
params: acp_old::StreamAssistantMessageChunkParams,
) -> Result<(), acp_old::Error> {
let cx = &mut self.cx.clone();
cx.update(|cx| {
self.thread
.borrow()
.update(cx, |thread, cx| match params.chunk {
acp_old::AssistantMessageChunk::Text { text } => {
thread.push_assistant_chunk(text.into(), false, cx)
}
acp_old::AssistantMessageChunk::Thought { thought } => {
thread.push_assistant_chunk(thought.into(), true, cx)
}
})
.ok();
})?;
Ok(())
}
async fn request_tool_call_confirmation(
&self,
request: acp_old::RequestToolCallConfirmationParams,
) -> Result<acp_old::RequestToolCallConfirmationResponse, acp_old::Error> {
let cx = &mut self.cx.clone();
let old_acp_id = *self.next_tool_call_id.borrow() + 1;
self.next_tool_call_id.replace(old_acp_id);
let tool_call = into_new_tool_call(
acp::ToolCallId(old_acp_id.to_string().into()),
request.tool_call,
);
let mut options = match request.confirmation {
acp_old::ToolCallConfirmation::Edit { .. } => vec![(
acp_old::ToolCallConfirmationOutcome::AlwaysAllow,
acp::PermissionOptionKind::AllowAlways,
"Always Allow Edits".to_string(),
)],
acp_old::ToolCallConfirmation::Execute { root_command, .. } => vec![(
acp_old::ToolCallConfirmationOutcome::AlwaysAllow,
acp::PermissionOptionKind::AllowAlways,
format!("Always Allow {}", root_command),
)],
acp_old::ToolCallConfirmation::Mcp {
server_name,
tool_name,
..
} => vec![
(
acp_old::ToolCallConfirmationOutcome::AlwaysAllowMcpServer,
acp::PermissionOptionKind::AllowAlways,
format!("Always Allow {}", server_name),
),
(
acp_old::ToolCallConfirmationOutcome::AlwaysAllowTool,
acp::PermissionOptionKind::AllowAlways,
format!("Always Allow {}", tool_name),
),
],
acp_old::ToolCallConfirmation::Fetch { .. } => vec![(
acp_old::ToolCallConfirmationOutcome::AlwaysAllow,
acp::PermissionOptionKind::AllowAlways,
"Always Allow".to_string(),
)],
acp_old::ToolCallConfirmation::Other { .. } => vec![(
acp_old::ToolCallConfirmationOutcome::AlwaysAllow,
acp::PermissionOptionKind::AllowAlways,
"Always Allow".to_string(),
)],
};
options.extend([
(
acp_old::ToolCallConfirmationOutcome::Allow,
acp::PermissionOptionKind::AllowOnce,
"Allow".to_string(),
),
(
acp_old::ToolCallConfirmationOutcome::Reject,
acp::PermissionOptionKind::RejectOnce,
"Reject".to_string(),
),
]);
let mut outcomes = Vec::with_capacity(options.len());
let mut acp_options = Vec::with_capacity(options.len());
for (index, (outcome, kind, label)) in options.into_iter().enumerate() {
outcomes.push(outcome);
acp_options.push(acp::PermissionOption {
id: acp::PermissionOptionId(index.to_string().into()),
label,
kind,
})
}
let response = cx
.update(|cx| {
self.thread.borrow().update(cx, |thread, cx| {
thread.request_tool_call_permission(tool_call, acp_options, cx)
})
})?
.context("Failed to update thread")?
.await;
let outcome = match response {
Ok(option_id) => outcomes[option_id.0.parse::<usize>().unwrap_or(0)],
Err(oneshot::Canceled) => acp_old::ToolCallConfirmationOutcome::Cancel,
};
Ok(acp_old::RequestToolCallConfirmationResponse {
id: acp_old::ToolCallId(old_acp_id),
outcome: outcome,
})
}
async fn push_tool_call(
&self,
request: acp_old::PushToolCallParams,
) -> Result<acp_old::PushToolCallResponse, acp_old::Error> {
let cx = &mut self.cx.clone();
let old_acp_id = *self.next_tool_call_id.borrow() + 1;
self.next_tool_call_id.replace(old_acp_id);
cx.update(|cx| {
self.thread.borrow().update(cx, |thread, cx| {
thread.upsert_tool_call(
into_new_tool_call(acp::ToolCallId(old_acp_id.to_string().into()), request),
cx,
)
})
})?
.context("Failed to update thread")?;
Ok(acp_old::PushToolCallResponse {
id: acp_old::ToolCallId(old_acp_id),
})
}
async fn update_tool_call(
&self,
request: acp_old::UpdateToolCallParams,
) -> Result<(), acp_old::Error> {
let cx = &mut self.cx.clone();
cx.update(|cx| {
self.thread.borrow().update(cx, |thread, cx| {
let languages = thread.project.read(cx).languages().clone();
if let Some((ix, tool_call)) = thread
.tool_call_mut(&acp::ToolCallId(request.tool_call_id.0.to_string().into()))
{
tool_call.status = ToolCallStatus::Allowed {
status: into_new_tool_call_status(request.status),
};
tool_call.content = request
.content
.into_iter()
.map(|content| {
ToolCallContent::from_acp(
into_new_tool_call_content(content),
languages.clone(),
cx,
)
})
.collect();
cx.emit(AcpThreadEvent::EntryUpdated(ix));
anyhow::Ok(())
} else {
anyhow::bail!("Tool call not found")
}
})
})?
.context("Failed to update thread")??;
Ok(())
}
async fn update_plan(&self, request: acp_old::UpdatePlanParams) -> Result<(), acp_old::Error> {
let cx = &mut self.cx.clone();
cx.update(|cx| {
self.thread.borrow().update(cx, |thread, cx| {
thread.update_plan(
acp::Plan {
entries: request
.entries
.into_iter()
.map(into_new_plan_entry)
.collect(),
},
cx,
)
})
})?
.context("Failed to update thread")?;
Ok(())
}
async fn read_text_file(
&self,
acp_old::ReadTextFileParams { path, line, limit }: acp_old::ReadTextFileParams,
) -> Result<acp_old::ReadTextFileResponse, acp_old::Error> {
let content = self
.cx
.update(|cx| {
self.thread.borrow().update(cx, |thread, cx| {
thread.read_text_file(path, line, limit, false, cx)
})
})?
.context("Failed to update thread")?
.await?;
Ok(acp_old::ReadTextFileResponse { content })
}
async fn write_text_file(
&self,
acp_old::WriteTextFileParams { path, content }: acp_old::WriteTextFileParams,
) -> Result<(), acp_old::Error> {
self.cx
.update(|cx| {
self.thread
.borrow()
.update(cx, |thread, cx| thread.write_text_file(path, content, cx))
})?
.context("Failed to update thread")?
.await?;
Ok(())
}
}
fn into_new_tool_call(id: acp::ToolCallId, request: acp_old::PushToolCallParams) -> acp::ToolCall {
acp::ToolCall {
id: id,
label: request.label,
kind: acp_kind_from_old_icon(request.icon),
status: acp::ToolCallStatus::InProgress,
content: request
.content
.into_iter()
.map(into_new_tool_call_content)
.collect(),
locations: request
.locations
.into_iter()
.map(into_new_tool_call_location)
.collect(),
}
}
fn acp_kind_from_old_icon(icon: acp_old::Icon) -> acp::ToolKind {
match icon {
acp_old::Icon::FileSearch => acp::ToolKind::Search,
acp_old::Icon::Folder => acp::ToolKind::Search,
acp_old::Icon::Globe => acp::ToolKind::Search,
acp_old::Icon::Hammer => acp::ToolKind::Other,
acp_old::Icon::LightBulb => acp::ToolKind::Think,
acp_old::Icon::Pencil => acp::ToolKind::Edit,
acp_old::Icon::Regex => acp::ToolKind::Search,
acp_old::Icon::Terminal => acp::ToolKind::Execute,
}
}
fn into_new_tool_call_status(status: acp_old::ToolCallStatus) -> acp::ToolCallStatus {
match status {
acp_old::ToolCallStatus::Running => acp::ToolCallStatus::InProgress,
acp_old::ToolCallStatus::Finished => acp::ToolCallStatus::Completed,
acp_old::ToolCallStatus::Error => acp::ToolCallStatus::Failed,
}
}
fn into_new_tool_call_content(content: acp_old::ToolCallContent) -> acp::ToolCallContent {
match content {
acp_old::ToolCallContent::Markdown { markdown } => acp::ToolCallContent::ContentBlock {
content: acp::ContentBlock::Text(acp::TextContent {
annotations: None,
text: markdown,
}),
},
acp_old::ToolCallContent::Diff { diff } => acp::ToolCallContent::Diff {
diff: into_new_diff(diff),
},
}
}
fn into_new_diff(diff: acp_old::Diff) -> acp::Diff {
acp::Diff {
path: diff.path,
old_text: diff.old_text,
new_text: diff.new_text,
}
}
fn into_new_tool_call_location(location: acp_old::ToolCallLocation) -> acp::ToolCallLocation {
acp::ToolCallLocation {
path: location.path,
line: location.line,
}
}
fn into_new_plan_entry(entry: acp_old::PlanEntry) -> acp::PlanEntry {
acp::PlanEntry {
content: entry.content,
priority: into_new_plan_priority(entry.priority),
status: into_new_plan_status(entry.status),
}
}
fn into_new_plan_priority(priority: acp_old::PlanEntryPriority) -> acp::PlanEntryPriority {
match priority {
acp_old::PlanEntryPriority::Low => acp::PlanEntryPriority::Low,
acp_old::PlanEntryPriority::Medium => acp::PlanEntryPriority::Medium,
acp_old::PlanEntryPriority::High => acp::PlanEntryPriority::High,
}
}
fn into_new_plan_status(status: acp_old::PlanEntryStatus) -> acp::PlanEntryStatus {
match status {
acp_old::PlanEntryStatus::Pending => acp::PlanEntryStatus::Pending,
acp_old::PlanEntryStatus::InProgress => acp::PlanEntryStatus::InProgress,
acp_old::PlanEntryStatus::Completed => acp::PlanEntryStatus::Completed,
}
}
#[derive(Debug)]
pub struct Unauthenticated;
impl Error for Unauthenticated {}
impl fmt::Display for Unauthenticated {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "Unauthenticated")
}
}
pub struct OldAcpAgentConnection {
pub name: &'static str,
pub connection: acp_old::AgentConnection,
pub child_status: Task<Result<()>>,
}
impl AgentConnection for OldAcpAgentConnection {
fn name(&self) -> &'static str {
self.name
}
fn new_thread(
self: Rc<Self>,
project: Entity<Project>,
_cwd: &Path,
cx: &mut AsyncApp,
) -> Task<Result<Entity<AcpThread>>> {
let task = self.connection.request_any(
acp_old::InitializeParams {
protocol_version: acp_old::ProtocolVersion::latest(),
}
.into_any(),
);
cx.spawn(async move |cx| {
let result = task.await?;
let result = acp_old::InitializeParams::response_from_any(result)?;
if !result.is_authenticated {
anyhow::bail!(Unauthenticated)
}
cx.update(|cx| {
let thread = cx.new(|cx| {
let session_id = acp::SessionId("acp-old-no-id".into());
AcpThread::new(self.clone(), project, session_id, cx)
});
thread
})
})
}
fn authenticate(&self, cx: &mut App) -> Task<Result<()>> {
let task = self
.connection
.request_any(acp_old::AuthenticateParams.into_any());
cx.foreground_executor().spawn(async move {
task.await?;
Ok(())
})
}
fn prompt(&self, params: acp::PromptToolArguments, cx: &mut App) -> Task<Result<()>> {
let chunks = params
.prompt
.into_iter()
.filter_map(|block| match block {
acp::ContentBlock::Text(text) => {
Some(acp_old::UserMessageChunk::Text { text: text.text })
}
acp::ContentBlock::ResourceLink(link) => Some(acp_old::UserMessageChunk::Path {
path: link.uri.into(),
}),
_ => None,
})
.collect();
let task = self
.connection
.request_any(acp_old::SendUserMessageParams { chunks }.into_any());
cx.foreground_executor().spawn(async move {
task.await?;
anyhow::Ok(())
})
}
fn cancel(&self, _session_id: &acp::SessionId, cx: &mut App) {
let task = self
.connection
.request_any(acp_old::CancelSendMessageParams.into_any());
cx.foreground_executor()
.spawn(async move {
task.await?;
anyhow::Ok(())
})
.detach_and_log_err(cx)
}
}

View File

@@ -231,7 +231,6 @@ impl ActivityIndicator {
status,
} => {
let create_buffer = project.update(cx, |project, cx| project.create_buffer(cx));
let project = project.clone();
let status = status.clone();
let server_name = server_name.clone();
cx.spawn_in(window, async move |workspace, cx| {
@@ -247,8 +246,7 @@ impl ActivityIndicator {
workspace.update_in(cx, |workspace, window, cx| {
workspace.add_item_to_active_pane(
Box::new(cx.new(|cx| {
let mut editor =
Editor::for_buffer(buffer, Some(project.clone()), window, cx);
let mut editor = Editor::for_buffer(buffer, None, window, cx);
editor.set_read_only(true);
editor
})),

View File

@@ -47,11 +47,11 @@ use std::{
time::{Duration, Instant},
};
use thiserror::Error;
use util::{ResultExt as _, debug_panic, post_inc};
use util::{ResultExt as _, post_inc};
use uuid::Uuid;
use zed_llm_client::{CompletionIntent, CompletionRequestStatus, UsageLimit};
const MAX_RETRY_ATTEMPTS: u8 = 3;
const MAX_RETRY_ATTEMPTS: u8 = 4;
const BASE_RETRY_DELAY: Duration = Duration::from_secs(5);
#[derive(Debug, Clone)]
@@ -1582,20 +1582,18 @@ impl Thread {
model: Arc<dyn LanguageModel>,
cx: &mut App,
) -> Option<PendingToolUse> {
let action_log = self.action_log.read(cx);
// Represent notification as a simulated `project_notifications` tool call
let tool_name = Arc::from("project_notifications");
let tool = self.tools.read(cx).tool(&tool_name, cx)?;
if !action_log.has_unnotified_user_edits() {
if !self.profile.is_tool_enabled(tool.source(), tool.name(), cx) {
return None;
}
// Represent notification as a simulated `project_notifications` tool call
let tool_name = Arc::from("project_notifications");
let Some(tool) = self.tools.read(cx).tool(&tool_name, cx) else {
debug_panic!("`project_notifications` tool not found");
return None;
};
if !self.profile.is_tool_enabled(tool.source(), tool.name(), cx) {
if self
.action_log
.update(cx, |log, cx| log.unnotified_user_edits(cx).is_none())
{
return None;
}
@@ -2182,8 +2180,8 @@ impl Thread {
// General strategy here:
// - If retrying won't help (e.g. invalid API key or payload too large), return None so we don't retry at all.
// - If it's a time-based issue (e.g. server overloaded, rate limit exceeded), try multiple times with exponential backoff.
// - If it's an issue that *might* be fixed by retrying (e.g. internal server error), just retry once.
// - If it's a time-based issue (e.g. server overloaded, rate limit exceeded), retry up to 4 times with exponential backoff.
// - If it's an issue that *might* be fixed by retrying (e.g. internal server error), retry up to 3 times.
match error {
HttpResponseError {
status_code: StatusCode::TOO_MANY_REQUESTS,
@@ -2211,8 +2209,8 @@ impl Thread {
}
StatusCode::INTERNAL_SERVER_ERROR => Some(RetryStrategy::Fixed {
delay: retry_after.unwrap_or(BASE_RETRY_DELAY),
// Internal Server Error could be anything, so only retry once.
max_attempts: 1,
// Internal Server Error could be anything, retry up to 3 times.
max_attempts: 3,
}),
status => {
// There is no StatusCode variant for the unofficial HTTP 529 ("The service is overloaded"),
@@ -2223,20 +2221,23 @@ impl Thread {
max_attempts: MAX_RETRY_ATTEMPTS,
})
} else {
None
Some(RetryStrategy::Fixed {
delay: retry_after.unwrap_or(BASE_RETRY_DELAY),
max_attempts: 2,
})
}
}
},
ApiInternalServerError { .. } => Some(RetryStrategy::Fixed {
delay: BASE_RETRY_DELAY,
max_attempts: 1,
max_attempts: 3,
}),
ApiReadResponseError { .. }
| HttpSend { .. }
| DeserializeResponse { .. }
| BadRequestFormat { .. } => Some(RetryStrategy::Fixed {
delay: BASE_RETRY_DELAY,
max_attempts: 1,
max_attempts: 3,
}),
// Retrying these errors definitely shouldn't help.
HttpResponseError {
@@ -2244,24 +2245,31 @@ impl Thread {
StatusCode::PAYLOAD_TOO_LARGE | StatusCode::FORBIDDEN | StatusCode::UNAUTHORIZED,
..
}
| SerializeRequest { .. }
| AuthenticationError { .. }
| PermissionError { .. } => None,
// These errors might be transient, so retry them
SerializeRequest { .. }
| BuildRequestBody { .. }
| PromptTooLarge { .. }
| AuthenticationError { .. }
| PermissionError { .. }
| ApiEndpointNotFound { .. }
| NoApiKey { .. } => None,
| NoApiKey { .. } => Some(RetryStrategy::Fixed {
delay: BASE_RETRY_DELAY,
max_attempts: 2,
}),
// Retry all other 4xx and 5xx errors once.
HttpResponseError { status_code, .. }
if status_code.is_client_error() || status_code.is_server_error() =>
{
Some(RetryStrategy::Fixed {
delay: BASE_RETRY_DELAY,
max_attempts: 1,
max_attempts: 3,
})
}
// Conservatively assume that any other errors are non-retryable
HttpResponseError { .. } | Other(..) => None,
HttpResponseError { .. } | Other(..) => Some(RetryStrategy::Fixed {
delay: BASE_RETRY_DELAY,
max_attempts: 2,
}),
}
}
@@ -4352,7 +4360,7 @@ fn main() {{
let retry_state = thread.retry_state.as_ref().unwrap();
assert_eq!(retry_state.attempt, 1, "Should be first retry attempt");
assert_eq!(
retry_state.max_attempts, 1,
retry_state.max_attempts, 3,
"Should have correct max attempts"
);
});
@@ -4368,8 +4376,9 @@ fn main() {{
if let MessageSegment::Text(text) = seg {
text.contains("internal")
&& text.contains("Fake")
&& text.contains("Retrying in")
&& !text.contains("attempt")
&& text.contains("Retrying")
&& text.contains("attempt 1 of 3")
&& text.contains("seconds")
} else {
false
}
@@ -4464,8 +4473,8 @@ fn main() {{
let retry_state = thread.retry_state.as_ref().unwrap();
assert_eq!(retry_state.attempt, 1, "Should be first retry attempt");
assert_eq!(
retry_state.max_attempts, 1,
"Internal server errors should only retry once"
retry_state.max_attempts, 3,
"Internal server errors should retry up to 3 times"
);
});
@@ -4473,7 +4482,15 @@ fn main() {{
cx.executor().advance_clock(BASE_RETRY_DELAY);
cx.run_until_parked();
// Should have scheduled second retry - count retry messages
// Advance clock for second retry
cx.executor().advance_clock(BASE_RETRY_DELAY);
cx.run_until_parked();
// Advance clock for third retry
cx.executor().advance_clock(BASE_RETRY_DELAY);
cx.run_until_parked();
// Should have completed all retries - count retry messages
let retry_count = thread.update(cx, |thread, _| {
thread
.messages
@@ -4491,24 +4508,24 @@ fn main() {{
.count()
});
assert_eq!(
retry_count, 1,
"Should have only one retry for internal server errors"
retry_count, 3,
"Should have 3 retries for internal server errors"
);
// For internal server errors, we only retry once and then give up
// Check that retry_state is cleared after the single retry
// For internal server errors, we retry 3 times and then give up
// Check that retry_state is cleared after all retries
thread.read_with(cx, |thread, _| {
assert!(
thread.retry_state.is_none(),
"Retry state should be cleared after single retry"
"Retry state should be cleared after all retries"
);
});
// Verify total attempts (1 initial + 1 retry)
// Verify total attempts (1 initial + 3 retries)
assert_eq!(
*completion_count.lock(),
2,
"Should have attempted once plus 1 retry"
4,
"Should have attempted once plus 3 retries"
);
}
@@ -5473,7 +5490,7 @@ fn main() {{
let thread = thread_store.update(cx, |store, cx| store.create_thread(cx));
let context_store = cx.new(|_cx| ContextStore::new(project.downgrade(), None));
let provider = Arc::new(FakeLanguageModelProvider);
let provider = Arc::new(FakeLanguageModelProvider::default());
let model = provider.test_model();
let model: Arc<dyn LanguageModel> = Arc::new(model);

View File

@@ -18,6 +18,7 @@ doctest = false
[dependencies]
acp_thread.workspace = true
agent-client-protocol.workspace = true
agentic-coding-protocol.workspace = true
anyhow.workspace = true
collections.workspace = true
@@ -37,10 +38,15 @@ strum.workspace = true
tempfile.workspace = true
ui.workspace = true
util.workspace = true
uuid.workspace = true
watch.workspace = true
which.workspace = true
workspace-hack.workspace = true
[target.'cfg(unix)'.dependencies]
libc.workspace = true
nix.workspace = true
[dev-dependencies]
env_logger.workspace = true
language.workspace = true

View File

@@ -1,7 +1,6 @@
mod claude;
mod gemini;
mod settings;
mod stdio_agent_server;
#[cfg(test)]
mod e2e_tests;
@@ -9,9 +8,8 @@ mod e2e_tests;
pub use claude::*;
pub use gemini::*;
pub use settings::*;
pub use stdio_agent_server::*;
use acp_thread::AcpThread;
use acp_thread::AgentConnection;
use anyhow::Result;
use collections::HashMap;
use gpui::{App, AsyncApp, Entity, SharedString, Task};
@@ -20,6 +18,7 @@ use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::{
path::{Path, PathBuf},
rc::Rc,
sync::Arc,
};
use util::ResultExt as _;
@@ -33,14 +32,14 @@ pub trait AgentServer: Send {
fn name(&self) -> &'static str;
fn empty_state_headline(&self) -> &'static str;
fn empty_state_message(&self) -> &'static str;
fn supports_always_allow(&self) -> bool;
fn new_thread(
fn connect(
&self,
// these will go away when old_acp is fully removed
root_dir: &Path,
project: &Entity<Project>,
cx: &mut App,
) -> Task<Result<Entity<AcpThread>>>;
) -> Task<Result<Rc<dyn AgentConnection>>>;
}
impl std::fmt::Debug for AgentServerCommand {

View File

@@ -1,21 +1,20 @@
mod mcp_server;
mod tools;
pub mod tools;
use collections::HashMap;
use project::Project;
use settings::SettingsStore;
use smol::process::Child;
use std::cell::RefCell;
use std::fmt::Display;
use std::path::Path;
use std::pin::pin;
use std::rc::Rc;
use uuid::Uuid;
use agentic_coding_protocol::{
self as acp, AnyAgentRequest, AnyAgentResult, Client, ProtocolVersion,
StreamAssistantMessageChunkParams, ToolCallContent, UpdateToolCallParams,
};
use agent_client_protocol as acp;
use anyhow::{Result, anyhow};
use futures::channel::oneshot;
use futures::future::LocalBoxFuture;
use futures::{AsyncBufReadExt, AsyncWriteExt};
use futures::{
AsyncRead, AsyncWrite, FutureExt, StreamExt,
@@ -23,14 +22,14 @@ use futures::{
io::BufReader,
select_biased,
};
use gpui::{App, AppContext, Entity, Task};
use gpui::{App, AppContext, AsyncApp, Entity, Task, WeakEntity};
use serde::{Deserialize, Serialize};
use util::ResultExt;
use crate::claude::mcp_server::ClaudeMcpServer;
use crate::claude::mcp_server::{ClaudeZedMcpServer, McpConfig};
use crate::claude::tools::ClaudeTool;
use crate::{AgentServer, AgentServerCommand, AllAgentServersSettings};
use acp_thread::{AcpClientDelegate, AcpThread, AgentConnection};
use acp_thread::{AcpThread, AgentConnection};
#[derive(Clone)]
pub struct ClaudeCode;
@@ -52,25 +51,52 @@ impl AgentServer for ClaudeCode {
ui::IconName::AiClaude
}
fn supports_always_allow(&self) -> bool {
false
fn connect(
&self,
_root_dir: &Path,
_project: &Entity<Project>,
_cx: &mut App,
) -> Task<Result<Rc<dyn AgentConnection>>> {
let connection = ClaudeAgentConnection {
sessions: Default::default(),
};
Task::ready(Ok(Rc::new(connection) as _))
}
}
#[cfg(unix)]
fn send_interrupt(pid: libc::pid_t) -> anyhow::Result<()> {
let pid = nix::unistd::Pid::from_raw(pid);
nix::sys::signal::kill(pid, nix::sys::signal::SIGINT)
.map_err(|e| anyhow!("Failed to interrupt process: {}", e))
}
#[cfg(windows)]
fn send_interrupt(_pid: i32) -> anyhow::Result<()> {
panic!("Cancel not implemented on Windows")
}
struct ClaudeAgentConnection {
sessions: Rc<RefCell<HashMap<acp::SessionId, ClaudeAgentSession>>>,
}
impl AgentConnection for ClaudeAgentConnection {
fn name(&self) -> &'static str {
ClaudeCode.name()
}
fn new_thread(
&self,
root_dir: &Path,
project: &Entity<Project>,
cx: &mut App,
self: Rc<Self>,
project: Entity<Project>,
cwd: &Path,
cx: &mut AsyncApp,
) -> Task<Result<Entity<AcpThread>>> {
let project = project.clone();
let root_dir = root_dir.to_path_buf();
let title = self.name().into();
let cwd = cwd.to_owned();
cx.spawn(async move |cx| {
let (mut delegate_tx, delegate_rx) = watch::channel(None);
let tool_id_map = Rc::new(RefCell::new(HashMap::default()));
let permission_mcp_server =
ClaudeMcpServer::new(delegate_rx, tool_id_map.clone(), cx).await?;
let (mut thread_tx, thread_rx) = watch::channel(WeakEntity::new_invalid());
let permission_mcp_server = ClaudeZedMcpServer::new(thread_rx.clone(), cx).await?;
let mut mcp_servers = HashMap::default();
mcp_servers.insert(
@@ -98,218 +124,333 @@ impl AgentServer for ClaudeCode {
anyhow::bail!("Failed to find claude binary");
};
let mut child = util::command::new_smol_command(&command.path)
.args(
[
"--input-format",
"stream-json",
"--output-format",
"stream-json",
"--print",
"--verbose",
"--mcp-config",
mcp_config_path.to_string_lossy().as_ref(),
"--permission-prompt-tool",
&format!(
"mcp__{}__{}",
mcp_server::SERVER_NAME,
mcp_server::PERMISSION_TOOL
),
"--allowedTools",
"mcp__zed__Read,mcp__zed__Edit",
"--disallowedTools",
"Read,Edit",
]
.into_iter()
.chain(command.args.iter().map(|arg| arg.as_str())),
)
.current_dir(root_dir)
.stdin(std::process::Stdio::piped())
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::inherit())
.kill_on_drop(true)
.spawn()?;
let stdin = child.stdin.take().unwrap();
let stdout = child.stdout.take().unwrap();
let (incoming_message_tx, mut incoming_message_rx) = mpsc::unbounded();
let (outgoing_tx, outgoing_rx) = mpsc::unbounded();
let (cancel_tx, mut cancel_rx) = mpsc::unbounded::<oneshot::Sender<Result<()>>>();
let io_task =
ClaudeAgentConnection::handle_io(outgoing_rx, incoming_message_tx, stdin, stdout);
cx.background_spawn(async move {
io_task.await.log_err();
drop(mcp_config_path);
drop(child);
let session_id = acp::SessionId(Uuid::new_v4().to_string().into());
log::trace!("Starting session with id: {}", session_id);
cx.background_spawn({
let session_id = session_id.clone();
async move {
let mut outgoing_rx = Some(outgoing_rx);
let mut mode = ClaudeSessionMode::Start;
loop {
let mut child = spawn_claude(
&command,
mode,
session_id.clone(),
&mcp_config_path,
&cwd,
)
.await?;
mode = ClaudeSessionMode::Resume;
let pid = child.id();
log::trace!("Spawned (pid: {})", pid);
let mut io_fut = pin!(
ClaudeAgentSession::handle_io(
outgoing_rx.take().unwrap(),
incoming_message_tx.clone(),
child.stdin.take().unwrap(),
child.stdout.take().unwrap(),
)
.fuse()
);
select_biased! {
done_tx = cancel_rx.next() => {
if let Some(done_tx) = done_tx {
log::trace!("Interrupted (pid: {})", pid);
let result = send_interrupt(pid as i32);
outgoing_rx.replace(io_fut.await?);
done_tx.send(result).log_err();
continue;
}
}
result = io_fut => {
result?;
}
}
log::trace!("Stopped (pid: {})", pid);
break;
}
drop(mcp_config_path);
anyhow::Ok(())
}
})
.detach();
cx.new(|cx| {
let end_turn_tx = Rc::new(RefCell::new(None));
let delegate = AcpClientDelegate::new(cx.entity().downgrade(), cx.to_async());
delegate_tx.send(Some(delegate.clone())).log_err();
let handler_task = cx.foreground_executor().spawn({
let end_turn_tx = end_turn_tx.clone();
let tool_id_map = tool_id_map.clone();
async move {
while let Some(message) = incoming_message_rx.next().await {
ClaudeAgentConnection::handle_message(
delegate.clone(),
message,
end_turn_tx.clone(),
tool_id_map.clone(),
)
.await
}
let end_turn_tx = Rc::new(RefCell::new(None));
let handler_task = cx.spawn({
let end_turn_tx = end_turn_tx.clone();
let thread_rx = thread_rx.clone();
async move |cx| {
while let Some(message) = incoming_message_rx.next().await {
ClaudeAgentSession::handle_message(
thread_rx.clone(),
message,
end_turn_tx.clone(),
cx,
)
.await
}
});
}
});
let mut connection = ClaudeAgentConnection {
outgoing_tx,
end_turn_tx,
_handler_task: handler_task,
_mcp_server: None,
};
let thread =
cx.new(|cx| AcpThread::new(self.clone(), project, session_id.clone(), cx))?;
connection._mcp_server = Some(permission_mcp_server);
acp_thread::AcpThread::new(connection, title, None, project.clone(), cx)
})
thread_tx.send(thread.downgrade())?;
let session = ClaudeAgentSession {
outgoing_tx,
end_turn_tx,
cancel_tx,
_handler_task: handler_task,
_mcp_server: Some(permission_mcp_server),
};
self.sessions.borrow_mut().insert(session_id, session);
Ok(thread)
})
}
}
impl AgentConnection for ClaudeAgentConnection {
/// Send a request to the agent and wait for a response.
fn request_any(
&self,
params: AnyAgentRequest,
) -> LocalBoxFuture<'static, Result<acp::AnyAgentResult>> {
let end_turn_tx = self.end_turn_tx.clone();
let outgoing_tx = self.outgoing_tx.clone();
async move {
match params {
// todo: consider sending an empty request so we get the init response?
AnyAgentRequest::InitializeParams(_) => Ok(AnyAgentResult::InitializeResponse(
acp::InitializeResponse {
is_authenticated: true,
protocol_version: ProtocolVersion::latest(),
},
)),
AnyAgentRequest::AuthenticateParams(_) => {
Err(anyhow!("Authentication not supported"))
fn authenticate(&self, _cx: &mut App) -> Task<Result<()>> {
Task::ready(Err(anyhow!("Authentication not supported")))
}
fn prompt(&self, params: acp::PromptToolArguments, cx: &mut App) -> Task<Result<()>> {
let sessions = self.sessions.borrow();
let Some(session) = sessions.get(&params.session_id) else {
return Task::ready(Err(anyhow!(
"Attempted to send message to nonexistent session {}",
params.session_id
)));
};
let (tx, rx) = oneshot::channel();
session.end_turn_tx.borrow_mut().replace(tx);
let mut content = String::new();
for chunk in params.prompt {
match chunk {
acp::ContentBlock::Text(text_content) => {
content.push_str(&text_content.text);
}
AnyAgentRequest::SendUserMessageParams(message) => {
let (tx, rx) = oneshot::channel();
end_turn_tx.borrow_mut().replace(tx);
let mut content = String::new();
for chunk in message.chunks {
match chunk {
agentic_coding_protocol::UserMessageChunk::Text { text } => {
content.push_str(&text)
}
agentic_coding_protocol::UserMessageChunk::Path { path } => {
content.push_str(&format!("@{path:?}"))
}
}
}
outgoing_tx.unbounded_send(SdkMessage::User {
message: Message {
role: Role::User,
content: Content::UntaggedText(content),
id: None,
model: None,
stop_reason: None,
stop_sequence: None,
usage: None,
},
session_id: None,
})?;
rx.await??;
Ok(AnyAgentResult::SendUserMessageResponse(
acp::SendUserMessageResponse,
))
acp::ContentBlock::ResourceLink(resource_link) => {
content.push_str(&format!("@{}", resource_link.uri));
}
acp::ContentBlock::Audio(_)
| acp::ContentBlock::Image(_)
| acp::ContentBlock::Resource(_) => {
// TODO
}
AnyAgentRequest::CancelSendMessageParams(_) => Ok(
AnyAgentResult::CancelSendMessageResponse(acp::CancelSendMessageResponse),
),
}
}
.boxed_local()
if let Err(err) = session.outgoing_tx.unbounded_send(SdkMessage::User {
message: Message {
role: Role::User,
content: Content::UntaggedText(content),
id: None,
model: None,
stop_reason: None,
stop_sequence: None,
usage: None,
},
session_id: Some(params.session_id.to_string()),
}) {
return Task::ready(Err(anyhow!(err)));
}
cx.foreground_executor().spawn(async move {
rx.await??;
Ok(())
})
}
fn cancel(&self, session_id: &acp::SessionId, cx: &mut App) {
let sessions = self.sessions.borrow();
let Some(session) = sessions.get(&session_id) else {
log::warn!("Attempted to cancel nonexistent session {}", session_id);
return;
};
let (done_tx, done_rx) = oneshot::channel();
if session
.cancel_tx
.unbounded_send(done_tx)
.log_err()
.is_some()
{
let end_turn_tx = session.end_turn_tx.clone();
cx.foreground_executor()
.spawn(async move {
done_rx.await??;
if let Some(end_turn_tx) = end_turn_tx.take() {
end_turn_tx.send(Ok(())).ok();
}
anyhow::Ok(())
})
.detach_and_log_err(cx);
}
}
}
struct ClaudeAgentConnection {
#[derive(Clone, Copy)]
enum ClaudeSessionMode {
Start,
Resume,
}
async fn spawn_claude(
command: &AgentServerCommand,
mode: ClaudeSessionMode,
session_id: acp::SessionId,
mcp_config_path: &Path,
root_dir: &Path,
) -> Result<Child> {
let child = util::command::new_smol_command(&command.path)
.args([
"--input-format",
"stream-json",
"--output-format",
"stream-json",
"--print",
"--verbose",
"--mcp-config",
mcp_config_path.to_string_lossy().as_ref(),
"--permission-prompt-tool",
&format!(
"mcp__{}__{}",
mcp_server::SERVER_NAME,
mcp_server::PERMISSION_TOOL
),
"--allowedTools",
"mcp__zed__Read,mcp__zed__Edit",
"--disallowedTools",
"Read,Edit",
])
.args(match mode {
ClaudeSessionMode::Start => ["--session-id".to_string(), session_id.to_string()],
ClaudeSessionMode::Resume => ["--resume".to_string(), session_id.to_string()],
})
.args(command.args.iter().map(|arg| arg.as_str()))
.current_dir(root_dir)
.stdin(std::process::Stdio::piped())
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::inherit())
.kill_on_drop(true)
.spawn()?;
Ok(child)
}
struct ClaudeAgentSession {
outgoing_tx: UnboundedSender<SdkMessage>,
end_turn_tx: Rc<RefCell<Option<oneshot::Sender<Result<()>>>>>,
_mcp_server: Option<ClaudeMcpServer>,
cancel_tx: UnboundedSender<oneshot::Sender<Result<()>>>,
_mcp_server: Option<ClaudeZedMcpServer>,
_handler_task: Task<()>,
}
impl ClaudeAgentConnection {
impl ClaudeAgentSession {
async fn handle_message(
delegate: AcpClientDelegate,
mut thread_rx: watch::Receiver<WeakEntity<AcpThread>>,
message: SdkMessage,
end_turn_tx: Rc<RefCell<Option<oneshot::Sender<Result<()>>>>>,
tool_id_map: Rc<RefCell<HashMap<String, acp::ToolCallId>>>,
cx: &mut AsyncApp,
) {
match message {
SdkMessage::Assistant { message, .. } | SdkMessage::User { message, .. } => {
SdkMessage::Assistant {
message,
session_id: _,
}
| SdkMessage::User {
message,
session_id: _,
} => {
let Some(thread) = thread_rx
.recv()
.await
.log_err()
.and_then(|entity| entity.upgrade())
else {
log::error!("Received an SDK message but thread is gone");
return;
};
for chunk in message.content.chunks() {
match chunk {
ContentChunk::Text { text } | ContentChunk::UntaggedText(text) => {
delegate
.stream_assistant_message_chunk(StreamAssistantMessageChunkParams {
chunk: acp::AssistantMessageChunk::Text { text },
thread
.update(cx, |thread, cx| {
thread.push_assistant_chunk(text.into(), false, cx)
})
.await
.log_err();
}
ContentChunk::ToolUse { id, name, input } => {
if let Some(resp) = delegate
.push_tool_call(ClaudeTool::infer(&name, input).as_acp())
.await
.log_err()
{
tool_id_map.borrow_mut().insert(id, resp.id);
}
let claude_tool = ClaudeTool::infer(&name, input);
thread
.update(cx, |thread, cx| {
if let ClaudeTool::TodoWrite(Some(params)) = claude_tool {
thread.update_plan(
acp::Plan {
entries: params
.todos
.into_iter()
.map(Into::into)
.collect(),
},
cx,
)
} else {
thread.upsert_tool_call(
claude_tool.as_acp(acp::ToolCallId(id.into())),
cx,
);
}
})
.log_err();
}
ContentChunk::ToolResult {
content,
tool_use_id,
} => {
let id = tool_id_map.borrow_mut().remove(&tool_use_id);
if let Some(id) = id {
let content = content.to_string();
delegate
.update_tool_call(UpdateToolCallParams {
tool_call_id: id,
status: acp::ToolCallStatus::Finished,
// Don't unset existing content
content: (!content.is_empty()).then_some(
ToolCallContent::Markdown {
// For now we only include text content
markdown: content,
},
),
})
.await
.log_err();
}
let content = content.to_string();
thread
.update(cx, |thread, cx| {
thread.update_tool_call(
acp::ToolCallId(tool_use_id.into()),
acp::ToolCallStatus::Completed,
(!content.is_empty()).then(|| vec![content.into()]),
cx,
)
})
.log_err();
}
ContentChunk::Image
| ContentChunk::Document
| ContentChunk::Thinking
| ContentChunk::RedactedThinking
| ContentChunk::WebSearchToolResult => {
delegate
.stream_assistant_message_chunk(StreamAssistantMessageChunkParams {
chunk: acp::AssistantMessageChunk::Text {
text: format!("Unsupported content: {:?}", chunk),
},
thread
.update(cx, |thread, cx| {
thread.push_assistant_chunk(
format!("Unsupported content: {:?}", chunk).into(),
false,
cx,
)
})
.await
.log_err();
}
}
@@ -335,7 +476,7 @@ impl ClaudeAgentConnection {
incoming_tx: UnboundedSender<SdkMessage>,
mut outgoing_bytes: impl Unpin + AsyncWrite,
incoming_bytes: impl Unpin + AsyncRead,
) -> Result<()> {
) -> Result<UnboundedReceiver<SdkMessage>> {
let mut output_reader = BufReader::new(incoming_bytes);
let mut outgoing_line = Vec::new();
let mut incoming_line = String::new();
@@ -369,7 +510,8 @@ impl ClaudeAgentConnection {
}
}
}
Ok(())
Ok(outgoing_rx)
}
}
@@ -561,21 +703,6 @@ enum PermissionMode {
Plan,
}
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct McpConfig {
mcp_servers: HashMap<String, McpServerConfig>,
}
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct McpServerConfig {
command: String,
args: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
env: Option<HashMap<String, String>>,
}
#[cfg(test)]
pub(crate) mod tests {
use super::*;

View File

@@ -1,29 +1,22 @@
use std::{cell::RefCell, rc::Rc};
use std::path::PathBuf;
use acp_thread::AcpClientDelegate;
use agentic_coding_protocol::{self as acp, Client, ReadTextFileParams, WriteTextFileParams};
use acp_thread::AcpThread;
use agent_client_protocol as acp;
use anyhow::{Context, Result};
use collections::HashMap;
use context_server::{
listener::McpServer,
types::{
CallToolParams, CallToolResponse, Implementation, InitializeParams, InitializeResponse,
ListToolsResponse, ProtocolVersion, ServerCapabilities, Tool, ToolAnnotations,
ToolResponseContent, ToolsCapabilities, requests,
},
use context_server::types::{
CallToolParams, CallToolResponse, Implementation, InitializeParams, InitializeResponse,
ListToolsResponse, ProtocolVersion, ServerCapabilities, Tool, ToolAnnotations,
ToolResponseContent, ToolsCapabilities, requests,
};
use gpui::{App, AsyncApp, Task};
use gpui::{App, AsyncApp, Entity, Task, WeakEntity};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use util::debug_panic;
use crate::claude::{
McpServerConfig,
tools::{ClaudeTool, EditToolParams, EditToolResponse, ReadToolParams, ReadToolResponse},
};
use crate::claude::tools::{ClaudeTool, EditToolParams, ReadToolParams};
pub struct ClaudeMcpServer {
server: McpServer,
pub struct ClaudeZedMcpServer {
server: context_server::listener::McpServer,
}
pub const SERVER_NAME: &str = "zed";
@@ -52,30 +45,24 @@ enum PermissionToolBehavior {
Deny,
}
impl ClaudeMcpServer {
impl ClaudeZedMcpServer {
pub async fn new(
delegate: watch::Receiver<Option<AcpClientDelegate>>,
tool_id_map: Rc<RefCell<HashMap<String, acp::ToolCallId>>>,
thread_rx: watch::Receiver<WeakEntity<AcpThread>>,
cx: &AsyncApp,
) -> Result<Self> {
let mut mcp_server = McpServer::new(cx).await?;
let mut mcp_server = context_server::listener::McpServer::new(cx).await?;
mcp_server.handle_request::<requests::Initialize>(Self::handle_initialize);
mcp_server.handle_request::<requests::ListTools>(Self::handle_list_tools);
mcp_server.handle_request::<requests::CallTool>(move |request, cx| {
Self::handle_call_tool(request, delegate.clone(), tool_id_map.clone(), cx)
Self::handle_call_tool(request, thread_rx.clone(), cx)
});
Ok(Self { server: mcp_server })
}
pub fn server_config(&self) -> Result<McpServerConfig> {
#[cfg(not(target_os = "windows"))]
let zed_path = util::get_shell_safe_zed_path()?;
#[cfg(target_os = "windows")]
let zed_path = std::env::current_exe()
.context("finding current executable path for use in mcp_server")?
.to_string_lossy()
.to_string();
.context("finding current executable path for use in mcp_server")?;
Ok(McpServerConfig {
command: zed_path,
@@ -155,22 +142,19 @@ impl ClaudeMcpServer {
fn handle_call_tool(
request: CallToolParams,
mut delegate_watch: watch::Receiver<Option<AcpClientDelegate>>,
tool_id_map: Rc<RefCell<HashMap<String, acp::ToolCallId>>>,
mut thread_rx: watch::Receiver<WeakEntity<AcpThread>>,
cx: &App,
) -> Task<Result<CallToolResponse>> {
cx.spawn(async move |cx| {
let Some(delegate) = delegate_watch.recv().await? else {
debug_panic!("Sent None delegate");
anyhow::bail!("Server not available");
let Some(thread) = thread_rx.recv().await?.upgrade() else {
anyhow::bail!("Thread closed");
};
if request.name.as_str() == PERMISSION_TOOL {
let input =
serde_json::from_value(request.arguments.context("Arguments required")?)?;
let result =
Self::handle_permissions_tool_call(input, delegate, tool_id_map, cx).await?;
let result = Self::handle_permissions_tool_call(input, thread, cx).await?;
Ok(CallToolResponse {
content: vec![ToolResponseContent::Text {
text: serde_json::to_string(&result)?,
@@ -182,11 +166,9 @@ impl ClaudeMcpServer {
let input =
serde_json::from_value(request.arguments.context("Arguments required")?)?;
let result = Self::handle_read_tool_call(input, delegate, cx).await?;
let content = Self::handle_read_tool_call(input, thread, cx).await?;
Ok(CallToolResponse {
content: vec![ToolResponseContent::Text {
text: serde_json::to_string(&result)?,
}],
content,
is_error: None,
meta: None,
})
@@ -194,11 +176,9 @@ impl ClaudeMcpServer {
let input =
serde_json::from_value(request.arguments.context("Arguments required")?)?;
let result = Self::handle_edit_tool_call(input, delegate, cx).await?;
Self::handle_edit_tool_call(input, thread, cx).await?;
Ok(CallToolResponse {
content: vec![ToolResponseContent::Text {
text: serde_json::to_string(&result)?,
}],
content: vec![],
is_error: None,
meta: None,
})
@@ -209,95 +189,113 @@ impl ClaudeMcpServer {
}
fn handle_read_tool_call(
params: ReadToolParams,
delegate: AcpClientDelegate,
ReadToolParams {
abs_path,
offset,
limit,
}: ReadToolParams,
thread: Entity<AcpThread>,
cx: &AsyncApp,
) -> Task<Result<ReadToolResponse>> {
cx.foreground_executor().spawn(async move {
let response = delegate
.read_text_file(ReadTextFileParams {
path: params.abs_path,
line: params.offset,
limit: params.limit,
})
) -> Task<Result<Vec<ToolResponseContent>>> {
cx.spawn(async move |cx| {
let content = thread
.update(cx, |thread, cx| {
thread.read_text_file(abs_path, offset, limit, false, cx)
})?
.await?;
Ok(ReadToolResponse {
content: response.content,
})
Ok(vec![ToolResponseContent::Text { text: content }])
})
}
fn handle_edit_tool_call(
params: EditToolParams,
delegate: AcpClientDelegate,
thread: Entity<AcpThread>,
cx: &AsyncApp,
) -> Task<Result<EditToolResponse>> {
cx.foreground_executor().spawn(async move {
let response = delegate
.read_text_file_reusing_snapshot(ReadTextFileParams {
path: params.abs_path.clone(),
line: None,
limit: None,
})
) -> Task<Result<()>> {
cx.spawn(async move |cx| {
let content = thread
.update(cx, |threads, cx| {
threads.read_text_file(params.abs_path.clone(), None, None, true, cx)
})?
.await?;
let new_content = response.content.replace(&params.old_text, &params.new_text);
if new_content == response.content {
let new_content = content.replace(&params.old_text, &params.new_text);
if new_content == content {
return Err(anyhow::anyhow!("The old_text was not found in the content"));
}
delegate
.write_text_file(WriteTextFileParams {
path: params.abs_path,
content: new_content,
})
thread
.update(cx, |threads, cx| {
threads.write_text_file(params.abs_path, new_content, cx)
})?
.await?;
Ok(EditToolResponse)
Ok(())
})
}
fn handle_permissions_tool_call(
params: PermissionToolParams,
delegate: AcpClientDelegate,
tool_id_map: Rc<RefCell<HashMap<String, acp::ToolCallId>>>,
thread: Entity<AcpThread>,
cx: &AsyncApp,
) -> Task<Result<PermissionToolResponse>> {
cx.foreground_executor().spawn(async move {
cx.spawn(async move |cx| {
let claude_tool = ClaudeTool::infer(&params.tool_name, params.input.clone());
let tool_call_id = match params.tool_use_id {
Some(tool_use_id) => tool_id_map
.borrow()
.get(&tool_use_id)
.cloned()
.context("Tool call ID not found")?,
let tool_call_id =
acp::ToolCallId(params.tool_use_id.context("Tool ID required")?.into());
None => delegate.push_tool_call(claude_tool.as_acp()).await?.id,
};
let allow_option_id = acp::PermissionOptionId("allow".into());
let reject_option_id = acp::PermissionOptionId("reject".into());
let outcome = delegate
.request_existing_tool_call_confirmation(
tool_call_id,
claude_tool.confirmation(None),
)
let chosen_option = thread
.update(cx, |thread, cx| {
thread.request_tool_call_permission(
claude_tool.as_acp(tool_call_id),
vec![
acp::PermissionOption {
id: allow_option_id.clone(),
label: "Allow".into(),
kind: acp::PermissionOptionKind::AllowOnce,
},
acp::PermissionOption {
id: reject_option_id,
label: "Reject".into(),
kind: acp::PermissionOptionKind::RejectOnce,
},
],
cx,
)
})?
.await?;
match outcome {
acp::ToolCallConfirmationOutcome::Allow
| acp::ToolCallConfirmationOutcome::AlwaysAllow
| acp::ToolCallConfirmationOutcome::AlwaysAllowMcpServer
| acp::ToolCallConfirmationOutcome::AlwaysAllowTool => Ok(PermissionToolResponse {
if chosen_option == allow_option_id {
Ok(PermissionToolResponse {
behavior: PermissionToolBehavior::Allow,
updated_input: params.input,
}),
acp::ToolCallConfirmationOutcome::Reject
| acp::ToolCallConfirmationOutcome::Cancel => Ok(PermissionToolResponse {
})
} else {
Ok(PermissionToolResponse {
behavior: PermissionToolBehavior::Deny,
updated_input: params.input,
}),
})
}
})
}
}
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct McpConfig {
pub mcp_servers: HashMap<String, McpServerConfig>,
}
#[derive(Serialize, Clone)]
#[serde(rename_all = "camelCase")]
pub struct McpServerConfig {
pub command: PathBuf,
pub args: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub env: Option<HashMap<String, String>>,
}

View File

@@ -1,6 +1,6 @@
use std::path::PathBuf;
use agentic_coding_protocol::{self as acp, PushToolCallParams, ToolCallLocation};
use agent_client_protocol as acp;
use itertools::Itertools;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
@@ -115,51 +115,36 @@ impl ClaudeTool {
Self::Other { name, .. } => name.clone(),
}
}
pub fn content(&self) -> Option<acp::ToolCallContent> {
pub fn content(&self) -> Vec<acp::ToolCallContent> {
match &self {
Self::Other { input, .. } => Some(acp::ToolCallContent::Markdown {
markdown: format!(
Self::Other { input, .. } => vec![
format!(
"```json\n{}```",
serde_json::to_string_pretty(&input).unwrap_or("{}".to_string())
),
}),
Self::Task(Some(params)) => Some(acp::ToolCallContent::Markdown {
markdown: params.prompt.clone(),
}),
Self::NotebookRead(Some(params)) => Some(acp::ToolCallContent::Markdown {
markdown: params.notebook_path.display().to_string(),
}),
Self::NotebookEdit(Some(params)) => Some(acp::ToolCallContent::Markdown {
markdown: params.new_source.clone(),
}),
Self::Terminal(Some(params)) => Some(acp::ToolCallContent::Markdown {
markdown: format!(
)
.into(),
],
Self::Task(Some(params)) => vec![params.prompt.clone().into()],
Self::NotebookRead(Some(params)) => {
vec![params.notebook_path.display().to_string().into()]
}
Self::NotebookEdit(Some(params)) => vec![params.new_source.clone().into()],
Self::Terminal(Some(params)) => vec![
format!(
"`{}`\n\n{}",
params.command,
params.description.as_deref().unwrap_or_default()
),
}),
Self::ReadFile(Some(params)) => Some(acp::ToolCallContent::Markdown {
markdown: params.abs_path.display().to_string(),
}),
Self::Ls(Some(params)) => Some(acp::ToolCallContent::Markdown {
markdown: params.path.display().to_string(),
}),
Self::Glob(Some(params)) => Some(acp::ToolCallContent::Markdown {
markdown: params.to_string(),
}),
Self::Grep(Some(params)) => Some(acp::ToolCallContent::Markdown {
markdown: format!("`{params}`"),
}),
Self::WebFetch(Some(params)) => Some(acp::ToolCallContent::Markdown {
markdown: params.prompt.clone(),
}),
Self::WebSearch(Some(params)) => Some(acp::ToolCallContent::Markdown {
markdown: params.to_string(),
}),
Self::TodoWrite(Some(params)) => Some(acp::ToolCallContent::Markdown {
markdown: params
)
.into(),
],
Self::ReadFile(Some(params)) => vec![params.abs_path.display().to_string().into()],
Self::Ls(Some(params)) => vec![params.path.display().to_string().into()],
Self::Glob(Some(params)) => vec![params.to_string().into()],
Self::Grep(Some(params)) => vec![format!("`{params}`").into()],
Self::WebFetch(Some(params)) => vec![params.prompt.clone().into()],
Self::WebSearch(Some(params)) => vec![params.to_string().into()],
Self::TodoWrite(Some(params)) => vec![
params
.todos
.iter()
.map(|todo| {
@@ -174,34 +159,39 @@ impl ClaudeTool {
todo.content
)
})
.join("\n"),
}),
Self::ExitPlanMode(Some(params)) => Some(acp::ToolCallContent::Markdown {
markdown: params.plan.clone(),
}),
Self::Edit(Some(params)) => Some(acp::ToolCallContent::Diff {
.join("\n")
.into(),
],
Self::ExitPlanMode(Some(params)) => vec![params.plan.clone().into()],
Self::Edit(Some(params)) => vec![acp::ToolCallContent::Diff {
diff: acp::Diff {
path: params.abs_path.clone(),
old_text: Some(params.old_text.clone()),
new_text: params.new_text.clone(),
},
}),
Self::Write(Some(params)) => Some(acp::ToolCallContent::Diff {
}],
Self::Write(Some(params)) => vec![acp::ToolCallContent::Diff {
diff: acp::Diff {
path: params.file_path.clone(),
old_text: None,
new_text: params.content.clone(),
},
}),
}],
Self::MultiEdit(Some(params)) => {
// todo: show multiple edits in a multibuffer?
params.edits.first().map(|edit| acp::ToolCallContent::Diff {
diff: acp::Diff {
path: params.file_path.clone(),
old_text: Some(edit.old_string.clone()),
new_text: edit.new_string.clone(),
},
})
params
.edits
.first()
.map(|edit| {
vec![acp::ToolCallContent::Diff {
diff: acp::Diff {
path: params.file_path.clone(),
old_text: Some(edit.old_string.clone()),
new_text: edit.new_string.clone(),
},
}]
})
.unwrap_or_default()
}
Self::Task(None)
| Self::NotebookRead(None)
@@ -217,181 +207,80 @@ impl ClaudeTool {
| Self::ExitPlanMode(None)
| Self::Edit(None)
| Self::Write(None)
| Self::MultiEdit(None) => None,
| Self::MultiEdit(None) => vec![],
}
}
pub fn icon(&self) -> acp::Icon {
pub fn kind(&self) -> acp::ToolKind {
match self {
Self::Task(_) => acp::Icon::Hammer,
Self::NotebookRead(_) => acp::Icon::FileSearch,
Self::NotebookEdit(_) => acp::Icon::Pencil,
Self::Edit(_) => acp::Icon::Pencil,
Self::MultiEdit(_) => acp::Icon::Pencil,
Self::Write(_) => acp::Icon::Pencil,
Self::ReadFile(_) => acp::Icon::FileSearch,
Self::Ls(_) => acp::Icon::Folder,
Self::Glob(_) => acp::Icon::FileSearch,
Self::Grep(_) => acp::Icon::Regex,
Self::Terminal(_) => acp::Icon::Terminal,
Self::WebSearch(_) => acp::Icon::Globe,
Self::WebFetch(_) => acp::Icon::Globe,
Self::TodoWrite(_) => acp::Icon::LightBulb,
Self::ExitPlanMode(_) => acp::Icon::Hammer,
Self::Other { .. } => acp::Icon::Hammer,
}
}
pub fn confirmation(&self, description: Option<String>) -> acp::ToolCallConfirmation {
match &self {
Self::Edit(_) | Self::Write(_) | Self::NotebookEdit(_) | Self::MultiEdit(_) => {
acp::ToolCallConfirmation::Edit { description }
}
Self::WebFetch(params) => acp::ToolCallConfirmation::Fetch {
urls: params
.as_ref()
.map(|p| vec![p.url.clone()])
.unwrap_or_default(),
description,
},
Self::Terminal(Some(BashToolParams {
description,
command,
..
})) => acp::ToolCallConfirmation::Execute {
command: command.clone(),
root_command: command.clone(),
description: description.clone(),
},
Self::ExitPlanMode(Some(params)) => acp::ToolCallConfirmation::Other {
description: if let Some(description) = description {
format!("{description} {}", params.plan)
} else {
params.plan.clone()
},
},
Self::Task(Some(params)) => acp::ToolCallConfirmation::Other {
description: if let Some(description) = description {
format!("{description} {}", params.description)
} else {
params.description.clone()
},
},
Self::Ls(Some(LsToolParams { path, .. }))
| Self::ReadFile(Some(ReadToolParams { abs_path: path, .. })) => {
let path = path.display();
acp::ToolCallConfirmation::Other {
description: if let Some(description) = description {
format!("{description} {path}")
} else {
path.to_string()
},
}
}
Self::NotebookRead(Some(NotebookReadToolParams { notebook_path, .. })) => {
let path = notebook_path.display();
acp::ToolCallConfirmation::Other {
description: if let Some(description) = description {
format!("{description} {path}")
} else {
path.to_string()
},
}
}
Self::Glob(Some(params)) => acp::ToolCallConfirmation::Other {
description: if let Some(description) = description {
format!("{description} {params}")
} else {
params.to_string()
},
},
Self::Grep(Some(params)) => acp::ToolCallConfirmation::Other {
description: if let Some(description) = description {
format!("{description} {params}")
} else {
params.to_string()
},
},
Self::WebSearch(Some(params)) => acp::ToolCallConfirmation::Other {
description: if let Some(description) = description {
format!("{description} {params}")
} else {
params.to_string()
},
},
Self::TodoWrite(Some(params)) => {
let params = params.todos.iter().map(|todo| &todo.content).join(", ");
acp::ToolCallConfirmation::Other {
description: if let Some(description) = description {
format!("{description} {params}")
} else {
params
},
}
}
Self::Terminal(None)
| Self::Task(None)
| Self::NotebookRead(None)
| Self::ExitPlanMode(None)
| Self::Ls(None)
| Self::Glob(None)
| Self::Grep(None)
| Self::ReadFile(None)
| Self::WebSearch(None)
| Self::TodoWrite(None)
| Self::Other { .. } => acp::ToolCallConfirmation::Other {
description: description.unwrap_or("".to_string()),
},
Self::Task(_) => acp::ToolKind::Think,
Self::NotebookRead(_) => acp::ToolKind::Read,
Self::NotebookEdit(_) => acp::ToolKind::Edit,
Self::Edit(_) => acp::ToolKind::Edit,
Self::MultiEdit(_) => acp::ToolKind::Edit,
Self::Write(_) => acp::ToolKind::Edit,
Self::ReadFile(_) => acp::ToolKind::Read,
Self::Ls(_) => acp::ToolKind::Search,
Self::Glob(_) => acp::ToolKind::Search,
Self::Grep(_) => acp::ToolKind::Search,
Self::Terminal(_) => acp::ToolKind::Execute,
Self::WebSearch(_) => acp::ToolKind::Search,
Self::WebFetch(_) => acp::ToolKind::Fetch,
Self::TodoWrite(_) => acp::ToolKind::Think,
Self::ExitPlanMode(_) => acp::ToolKind::Think,
Self::Other { .. } => acp::ToolKind::Other,
}
}
pub fn locations(&self) -> Vec<acp::ToolCallLocation> {
match &self {
Self::Edit(Some(EditToolParams { abs_path, .. })) => vec![ToolCallLocation {
Self::Edit(Some(EditToolParams { abs_path, .. })) => vec![acp::ToolCallLocation {
path: abs_path.clone(),
line: None,
}],
Self::MultiEdit(Some(MultiEditToolParams { file_path, .. })) => {
vec![ToolCallLocation {
vec![acp::ToolCallLocation {
path: file_path.clone(),
line: None,
}]
}
Self::Write(Some(WriteToolParams { file_path, .. })) => {
vec![acp::ToolCallLocation {
path: file_path.clone(),
line: None,
}]
}
Self::Write(Some(WriteToolParams { file_path, .. })) => vec![ToolCallLocation {
path: file_path.clone(),
line: None,
}],
Self::ReadFile(Some(ReadToolParams {
abs_path, offset, ..
})) => vec![ToolCallLocation {
})) => vec![acp::ToolCallLocation {
path: abs_path.clone(),
line: *offset,
}],
Self::NotebookRead(Some(NotebookReadToolParams { notebook_path, .. })) => {
vec![ToolCallLocation {
vec![acp::ToolCallLocation {
path: notebook_path.clone(),
line: None,
}]
}
Self::NotebookEdit(Some(NotebookEditToolParams { notebook_path, .. })) => {
vec![ToolCallLocation {
vec![acp::ToolCallLocation {
path: notebook_path.clone(),
line: None,
}]
}
Self::Glob(Some(GlobToolParams {
path: Some(path), ..
})) => vec![ToolCallLocation {
})) => vec![acp::ToolCallLocation {
path: path.clone(),
line: None,
}],
Self::Ls(Some(LsToolParams { path, .. })) => vec![ToolCallLocation {
Self::Ls(Some(LsToolParams { path, .. })) => vec![acp::ToolCallLocation {
path: path.clone(),
line: None,
}],
Self::Grep(Some(GrepToolParams {
path: Some(path), ..
})) => vec![ToolCallLocation {
})) => vec![acp::ToolCallLocation {
path: PathBuf::from(path),
line: None,
}],
@@ -414,11 +303,13 @@ impl ClaudeTool {
}
}
pub fn as_acp(&self) -> PushToolCallParams {
PushToolCallParams {
pub fn as_acp(&self, id: acp::ToolCallId) -> acp::ToolCall {
acp::ToolCall {
id,
kind: self.kind(),
status: acp::ToolCallStatus::InProgress,
label: self.label(),
content: self.content(),
icon: self.icon(),
locations: self.locations(),
}
}
@@ -434,10 +325,6 @@ pub struct EditToolParams {
pub new_text: String,
}
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct EditToolResponse;
#[derive(Deserialize, JsonSchema, Debug)]
pub struct ReadToolParams {
/// The absolute path to the file to read.
@@ -450,12 +337,6 @@ pub struct ReadToolParams {
pub limit: Option<u32>,
}
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ReadToolResponse {
pub content: String,
}
#[derive(Deserialize, JsonSchema, Debug)]
pub struct WriteToolParams {
/// Absolute path for new file
@@ -614,6 +495,16 @@ pub enum TodoPriority {
Low,
}
impl Into<acp::PlanEntryPriority> for TodoPriority {
fn into(self) -> acp::PlanEntryPriority {
match self {
TodoPriority::High => acp::PlanEntryPriority::High,
TodoPriority::Medium => acp::PlanEntryPriority::Medium,
TodoPriority::Low => acp::PlanEntryPriority::Low,
}
}
}
#[derive(Deserialize, Serialize, JsonSchema, Debug)]
#[serde(rename_all = "snake_case")]
pub enum TodoStatus {
@@ -622,6 +513,16 @@ pub enum TodoStatus {
Completed,
}
impl Into<acp::PlanEntryStatus> for TodoStatus {
fn into(self) -> acp::PlanEntryStatus {
match self {
TodoStatus::Pending => acp::PlanEntryStatus::Pending,
TodoStatus::InProgress => acp::PlanEntryStatus::InProgress,
TodoStatus::Completed => acp::PlanEntryStatus::Completed,
}
}
}
#[derive(Deserialize, Serialize, JsonSchema, Debug)]
pub struct Todo {
/// Unique identifier
@@ -634,6 +535,16 @@ pub struct Todo {
pub status: TodoStatus,
}
impl Into<acp::PlanEntry> for Todo {
fn into(self) -> acp::PlanEntry {
acp::PlanEntry {
content: self.content,
priority: self.priority.into(),
status: self.status.into(),
}
}
}
#[derive(Deserialize, JsonSchema, Debug)]
pub struct TodoWriteToolParams {
pub todos: Vec<Todo>,

View File

@@ -1,10 +1,9 @@
use std::{path::Path, sync::Arc, time::Duration};
use crate::{AgentServer, AgentServerSettings, AllAgentServersSettings};
use acp_thread::{
AcpThread, AgentThreadEntry, ToolCall, ToolCallConfirmation, ToolCallContent, ToolCallStatus,
};
use agentic_coding_protocol as acp;
use acp_thread::{AcpThread, AgentThreadEntry, ToolCall, ToolCallStatus};
use agent_client_protocol as acp;
use futures::{FutureExt, StreamExt, channel::mpsc, select};
use gpui::{Entity, TestAppContext};
use indoc::indoc;
@@ -54,19 +53,25 @@ pub async fn test_path_mentions(server: impl AgentServer + 'static, cx: &mut Tes
thread
.update(cx, |thread, cx| {
thread.send(
acp::SendUserMessageParams {
chunks: vec![
acp::UserMessageChunk::Text {
text: "Read the file ".into(),
},
acp::UserMessageChunk::Path {
path: Path::new("foo.rs").into(),
},
acp::UserMessageChunk::Text {
text: " and tell me what the content of the println! is".into(),
},
],
},
vec![
acp::ContentBlock::Text(acp::TextContent {
text: "Read the file ".into(),
annotations: None,
}),
acp::ContentBlock::ResourceLink(acp::ResourceLink {
uri: "foo.rs".into(),
name: "foo.rs".into(),
annotations: None,
description: None,
mime_type: None,
size: None,
title: None,
}),
acp::ContentBlock::Text(acp::TextContent {
text: " and tell me what the content of the println! is".into(),
annotations: None,
}),
],
cx,
)
})
@@ -161,11 +166,8 @@ pub async fn test_tool_call_with_confirmation(
let tool_call_id = thread.read_with(cx, |thread, _cx| {
let AgentThreadEntry::ToolCall(ToolCall {
id,
status:
ToolCallStatus::WaitingForConfirmation {
confirmation: ToolCallConfirmation::Execute { root_command, .. },
..
},
content,
status: ToolCallStatus::WaitingForConfirmation { .. },
..
}) = &thread
.entries()
@@ -176,13 +178,18 @@ pub async fn test_tool_call_with_confirmation(
panic!();
};
assert!(root_command.contains("touch"));
assert!(content.iter().any(|c| c.to_markdown(_cx).contains("touch")));
*id
id.clone()
});
thread.update(cx, |thread, cx| {
thread.authorize_tool_call(tool_call_id, acp::ToolCallConfirmationOutcome::Allow, cx);
thread.authorize_tool_call(
tool_call_id,
acp::PermissionOptionId("0".into()),
acp::PermissionOptionKind::AllowOnce,
cx,
);
assert!(thread.entries().iter().any(|entry| matches!(
entry,
@@ -197,7 +204,7 @@ pub async fn test_tool_call_with_confirmation(
thread.read_with(cx, |thread, cx| {
let AgentThreadEntry::ToolCall(ToolCall {
content: Some(ToolCallContent::Markdown { markdown }),
content,
status: ToolCallStatus::Allowed { .. },
..
}) = thread
@@ -209,13 +216,10 @@ pub async fn test_tool_call_with_confirmation(
panic!();
};
markdown.read_with(cx, |md, _cx| {
assert!(
md.source().contains("Hello"),
r#"Expected '{}' to contain "Hello""#,
md.source()
);
});
assert!(
content.iter().any(|c| c.to_markdown(cx).contains("Hello")),
"Expected content to contain 'Hello'"
);
});
}
@@ -249,26 +253,20 @@ pub async fn test_cancel(server: impl AgentServer + 'static, cx: &mut TestAppCon
thread.read_with(cx, |thread, _cx| {
let AgentThreadEntry::ToolCall(ToolCall {
id,
status:
ToolCallStatus::WaitingForConfirmation {
confirmation: ToolCallConfirmation::Execute { root_command, .. },
..
},
content,
status: ToolCallStatus::WaitingForConfirmation { .. },
..
}) = &thread.entries()[first_tool_call_ix]
else {
panic!("{:?}", thread.entries()[1]);
};
assert!(root_command.contains("touch"));
assert!(content.iter().any(|c| c.to_markdown(_cx).contains("touch")));
*id
id.clone()
});
thread
.update(cx, |thread, cx| thread.cancel(cx))
.await
.unwrap();
let _ = thread.update(cx, |thread, cx| thread.cancel(cx));
full_turn.await.unwrap();
thread.read_with(cx, |thread, _| {
let AgentThreadEntry::ToolCall(ToolCall {
@@ -369,15 +367,16 @@ pub async fn new_test_thread(
current_dir: impl AsRef<Path>,
cx: &mut TestAppContext,
) -> Entity<AcpThread> {
let thread = cx
.update(|cx| server.new_thread(current_dir.as_ref(), &project, cx))
let connection = cx
.update(|cx| server.connect(current_dir.as_ref(), &project, cx))
.await
.unwrap();
thread
.update(cx, |thread, _| thread.initialize())
let thread = connection
.new_thread(project.clone(), current_dir.as_ref(), &mut cx.to_async())
.await
.unwrap();
thread
}

View File

@@ -1,9 +1,17 @@
use crate::stdio_agent_server::StdioAgentServer;
use crate::{AgentServerCommand, AgentServerVersion};
use anyhow::anyhow;
use std::cell::RefCell;
use std::path::Path;
use std::rc::Rc;
use util::ResultExt as _;
use crate::{AgentServer, AgentServerCommand, AgentServerVersion};
use acp_thread::{AgentConnection, LoadError, OldAcpAgentConnection, OldAcpClientDelegate};
use agentic_coding_protocol as acp_old;
use anyhow::{Context as _, Result};
use gpui::{AsyncApp, Entity};
use gpui::{AppContext as _, AsyncApp, Entity, Task, WeakEntity};
use project::Project;
use settings::SettingsStore;
use ui::App;
use crate::AllAgentServersSettings;
@@ -12,7 +20,7 @@ pub struct Gemini;
const ACP_ARG: &str = "--experimental-acp";
impl StdioAgentServer for Gemini {
impl AgentServer for Gemini {
fn name(&self) -> &'static str {
"Gemini"
}
@@ -25,14 +33,88 @@ impl StdioAgentServer for Gemini {
"Ask questions, edit files, run commands.\nBe specific for the best results."
}
fn supports_always_allow(&self) -> bool {
true
}
fn logo(&self) -> ui::IconName {
ui::IconName::AiGemini
}
fn connect(
&self,
root_dir: &Path,
project: &Entity<Project>,
cx: &mut App,
) -> Task<Result<Rc<dyn AgentConnection>>> {
let root_dir = root_dir.to_path_buf();
let project = project.clone();
let this = self.clone();
let name = self.name();
cx.spawn(async move |cx| {
let command = this.command(&project, cx).await?;
let mut child = util::command::new_smol_command(&command.path)
.args(command.args.iter())
.current_dir(root_dir)
.stdin(std::process::Stdio::piped())
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::inherit())
.kill_on_drop(true)
.spawn()?;
let stdin = child.stdin.take().unwrap();
let stdout = child.stdout.take().unwrap();
let foreground_executor = cx.foreground_executor().clone();
let thread_rc = Rc::new(RefCell::new(WeakEntity::new_invalid()));
let (connection, io_fut) = acp_old::AgentConnection::connect_to_agent(
OldAcpClientDelegate::new(thread_rc.clone(), cx.clone()),
stdin,
stdout,
move |fut| foreground_executor.spawn(fut).detach(),
);
let io_task = cx.background_spawn(async move {
io_fut.await.log_err();
});
let child_status = cx.background_spawn(async move {
let result = match child.status().await {
Err(e) => Err(anyhow!(e)),
Ok(result) if result.success() => Ok(()),
Ok(result) => {
if let Some(AgentServerVersion::Unsupported {
error_message,
upgrade_message,
upgrade_command,
}) = this.version(&command).await.log_err()
{
Err(anyhow!(LoadError::Unsupported {
error_message,
upgrade_message,
upgrade_command
}))
} else {
Err(anyhow!(LoadError::Exited(result.code().unwrap_or(-127))))
}
}
};
drop(io_task);
result
});
let connection: Rc<dyn AgentConnection> = Rc::new(OldAcpAgentConnection {
name,
connection,
child_status,
});
Ok(connection)
})
}
}
impl Gemini {
async fn command(
&self,
project: &Entity<Project>,

View File

@@ -29,9 +29,12 @@ impl settings::Settings for AllAgentServersSettings {
fn load(sources: SettingsSources<Self::FileContent>, _: &mut App) -> Result<Self> {
let mut settings = AllAgentServersSettings::default();
for value in sources.defaults_and_customizations() {
if value.gemini.is_some() {
settings.gemini = value.gemini.clone();
for AllAgentServersSettings { gemini, claude } in sources.defaults_and_customizations() {
if gemini.is_some() {
settings.gemini = gemini.clone();
}
if claude.is_some() {
settings.claude = claude.clone();
}
}

View File

@@ -1,119 +0,0 @@
use crate::{AgentServer, AgentServerCommand, AgentServerVersion};
use acp_thread::{AcpClientDelegate, AcpThread, LoadError};
use agentic_coding_protocol as acp;
use anyhow::{Result, anyhow};
use gpui::{App, AsyncApp, Entity, Task, prelude::*};
use project::Project;
use std::path::Path;
use util::ResultExt;
pub trait StdioAgentServer: Send + Clone {
fn logo(&self) -> ui::IconName;
fn name(&self) -> &'static str;
fn empty_state_headline(&self) -> &'static str;
fn empty_state_message(&self) -> &'static str;
fn supports_always_allow(&self) -> bool;
fn command(
&self,
project: &Entity<Project>,
cx: &mut AsyncApp,
) -> impl Future<Output = Result<AgentServerCommand>>;
fn version(
&self,
command: &AgentServerCommand,
) -> impl Future<Output = Result<AgentServerVersion>> + Send;
}
impl<T: StdioAgentServer + 'static> AgentServer for T {
fn name(&self) -> &'static str {
self.name()
}
fn empty_state_headline(&self) -> &'static str {
self.empty_state_headline()
}
fn empty_state_message(&self) -> &'static str {
self.empty_state_message()
}
fn logo(&self) -> ui::IconName {
self.logo()
}
fn supports_always_allow(&self) -> bool {
self.supports_always_allow()
}
fn new_thread(
&self,
root_dir: &Path,
project: &Entity<Project>,
cx: &mut App,
) -> Task<Result<Entity<AcpThread>>> {
let root_dir = root_dir.to_path_buf();
let project = project.clone();
let this = self.clone();
let title = self.name().into();
cx.spawn(async move |cx| {
let command = this.command(&project, cx).await?;
let mut child = util::command::new_smol_command(&command.path)
.args(command.args.iter())
.current_dir(root_dir)
.stdin(std::process::Stdio::piped())
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::inherit())
.kill_on_drop(true)
.spawn()?;
let stdin = child.stdin.take().unwrap();
let stdout = child.stdout.take().unwrap();
cx.new(|cx| {
let foreground_executor = cx.foreground_executor().clone();
let (connection, io_fut) = acp::AgentConnection::connect_to_agent(
AcpClientDelegate::new(cx.entity().downgrade(), cx.to_async()),
stdin,
stdout,
move |fut| foreground_executor.spawn(fut).detach(),
);
let io_task = cx.background_spawn(async move {
io_fut.await.log_err();
});
let child_status = cx.background_spawn(async move {
let result = match child.status().await {
Err(e) => Err(anyhow!(e)),
Ok(result) if result.success() => Ok(()),
Ok(result) => {
if let Some(AgentServerVersion::Unsupported {
error_message,
upgrade_message,
upgrade_command,
}) = this.version(&command).await.log_err()
{
Err(anyhow!(LoadError::Unsupported {
error_message,
upgrade_message,
upgrade_command
}))
} else {
Err(anyhow!(LoadError::Exited(result.code().unwrap_or(-127))))
}
}
};
drop(io_task);
result
});
AcpThread::new(connection, title, Some(child_status), project.clone(), cx)
})
})
}
}

View File

@@ -17,10 +17,10 @@ test-support = ["gpui/test-support", "language/test-support"]
[dependencies]
acp_thread.workspace = true
agent-client-protocol.workspace = true
agent.workspace = true
agentic-coding-protocol.workspace = true
agent_settings.workspace = true
agent_servers.workspace = true
agent_settings.workspace = true
ai_onboarding.workspace = true
anyhow.workspace = true
assistant_context.workspace = true
@@ -32,6 +32,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
db.workspace = true
@@ -53,6 +54,7 @@ itertools.workspace = true
jsonschema.workspace = true
language.workspace = true
language_model.workspace = true
language_models.workspace = true
log.workspace = true
lsp.workspace = true
markdown.workspace = true
@@ -87,6 +89,7 @@ theme.workspace = true
time.workspace = true
time_format.workspace = true
ui.workspace = true
ui_input.workspace = true
urlencoding.workspace = true
util.workspace = true
uuid.workspace = true

File diff suppressed because it is too large Load Diff

View File

@@ -3724,8 +3724,11 @@ pub(crate) fn open_context(
AgentContextHandle::Thread(thread_context) => workspace.update(cx, |workspace, cx| {
if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
panel.update(cx, |panel, cx| {
panel.open_thread(thread_context.thread.clone(), window, cx);
let thread = thread_context.thread.clone();
window.defer(cx, move |window, cx| {
panel.update(cx, |panel, cx| {
panel.open_thread(thread, window, cx);
});
});
}
}),
@@ -3733,8 +3736,11 @@ pub(crate) fn open_context(
AgentContextHandle::TextThread(text_thread_context) => {
workspace.update(cx, |workspace, cx| {
if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
panel.update(cx, |panel, cx| {
panel.open_prompt_editor(text_thread_context.context.clone(), window, cx)
let context = text_thread_context.context.clone();
window.defer(cx, move |window, cx| {
panel.update(cx, |panel, cx| {
panel.open_prompt_editor(context, window, cx)
});
});
}
})
@@ -3889,7 +3895,7 @@ mod tests {
LanguageModelRegistry::global(cx).update(cx, |registry, cx| {
registry.set_default_model(
Some(ConfiguredModel {
provider: Arc::new(FakeLanguageModelProvider),
provider: Arc::new(FakeLanguageModelProvider::default()),
model,
}),
cx,
@@ -3973,7 +3979,7 @@ mod tests {
LanguageModelRegistry::global(cx).update(cx, |registry, cx| {
registry.set_default_model(
Some(ConfiguredModel {
provider: Arc::new(FakeLanguageModelProvider),
provider: Arc::new(FakeLanguageModelProvider::default()),
model: model.clone(),
}),
cx,

View File

@@ -1,3 +1,4 @@
mod add_llm_provider_modal;
mod configure_context_server_modal;
mod manage_profiles_modal;
mod tool_picker;
@@ -28,7 +29,7 @@ use proto::Plan;
use settings::{Settings, update_settings_file};
use ui::{
Chip, ContextMenu, Disclosure, Divider, DividerColor, ElevationIndex, Indicator, PopoverMenu,
Scrollbar, ScrollbarState, Switch, SwitchColor, Tooltip, prelude::*,
Scrollbar, ScrollbarState, Switch, SwitchColor, SwitchField, Tooltip, prelude::*,
};
use util::ResultExt as _;
use workspace::Workspace;
@@ -37,7 +38,10 @@ use zed_actions::ExtensionCategoryFilter;
pub(crate) use configure_context_server_modal::ConfigureContextServerModal;
pub(crate) use manage_profiles_modal::ManageProfilesModal;
use crate::AddContextServer;
use crate::{
AddContextServer,
agent_configuration::add_llm_provider_modal::{AddLlmProviderModal, LlmCompatibleProvider},
};
pub struct AgentConfiguration {
fs: Arc<dyn Fs>,
@@ -182,6 +186,7 @@ impl AgentConfiguration {
};
v_flex()
.w_full()
.when(is_expanded, |this| this.mb_2())
.child(
div()
@@ -212,6 +217,7 @@ impl AgentConfiguration {
.hover(|hover| hover.bg(cx.theme().colors().element_hover))
.child(
h_flex()
.w_full()
.gap_2()
.child(
Icon::new(provider.icon())
@@ -220,6 +226,7 @@ impl AgentConfiguration {
)
.child(
h_flex()
.w_full()
.gap_1()
.child(
Label::new(provider_name.clone())
@@ -303,21 +310,78 @@ impl AgentConfiguration {
let providers = LanguageModelRegistry::read_global(cx).providers();
v_flex()
.w_full()
.child(
v_flex()
h_flex()
.p(DynamicSpacing::Base16.rems(cx))
.pr(DynamicSpacing::Base20.rems(cx))
.pb_0()
.mb_2p5()
.gap_0p5()
.child(Headline::new("LLM Providers"))
.items_start()
.justify_between()
.child(
Label::new("Add at least one provider to use AI-powered features.")
.color(Color::Muted),
v_flex()
.w_full()
.gap_0p5()
.child(
h_flex()
.w_full()
.gap_2()
.justify_between()
.child(Headline::new("LLM Providers"))
.child(
PopoverMenu::new("add-provider-popover")
.trigger(
Button::new("add-provider", "Add Provider")
.icon_position(IconPosition::Start)
.icon(IconName::Plus)
.icon_size(IconSize::Small)
.icon_color(Color::Muted)
.label_size(LabelSize::Small),
)
.anchor(gpui::Corner::TopRight)
.menu({
let workspace = self.workspace.clone();
move |window, cx| {
Some(ContextMenu::build(
window,
cx,
|menu, _window, _cx| {
menu.header("Compatible APIs").entry(
"OpenAI",
None,
{
let workspace =
workspace.clone();
move |window, cx| {
workspace
.update(cx, |workspace, cx| {
AddLlmProviderModal::toggle(
LlmCompatibleProvider::OpenAi,
workspace,
window,
cx,
);
})
.log_err();
}
},
)
},
))
}
}),
),
)
.child(
Label::new("Add at least one provider to use AI-powered features.")
.color(Color::Muted),
),
),
)
.child(
div()
.w_full()
.pl(DynamicSpacing::Base08.rems(cx))
.pr(DynamicSpacing::Base20.rems(cx))
.children(
@@ -330,119 +394,74 @@ impl AgentConfiguration {
fn render_command_permission(&mut self, cx: &mut Context<Self>) -> impl IntoElement {
let always_allow_tool_actions = AgentSettings::get_global(cx).always_allow_tool_actions;
let fs = self.fs.clone();
h_flex()
.gap_4()
.justify_between()
.flex_wrap()
.child(
v_flex()
.gap_0p5()
.max_w_5_6()
.child(Label::new("Allow running editing tools without asking for confirmation"))
.child(
Label::new(
"The agent can perform potentially destructive actions without asking for your confirmation.",
)
.color(Color::Muted),
),
)
.child(
Switch::new(
"always-allow-tool-actions-switch",
always_allow_tool_actions.into(),
)
.color(SwitchColor::Accent)
.on_click({
let fs = self.fs.clone();
move |state, _window, cx| {
let allow = state == &ToggleState::Selected;
update_settings_file::<AgentSettings>(
fs.clone(),
cx,
move |settings, _| {
settings.set_always_allow_tool_actions(allow);
},
);
}
}),
)
SwitchField::new(
"single-file-review",
"Enable single-file agent reviews",
"Agent edits are also displayed in single-file editors for review.",
always_allow_tool_actions,
move |state, _window, cx| {
let allow = state == &ToggleState::Selected;
update_settings_file::<AgentSettings>(fs.clone(), cx, move |settings, _| {
settings.set_always_allow_tool_actions(allow);
});
},
)
}
fn render_single_file_review(&mut self, cx: &mut Context<Self>) -> impl IntoElement {
let single_file_review = AgentSettings::get_global(cx).single_file_review;
let fs = self.fs.clone();
h_flex()
.gap_4()
.justify_between()
.flex_wrap()
.child(
v_flex()
.gap_0p5()
.max_w_5_6()
.child(Label::new("Enable single-file agent reviews"))
.child(
Label::new(
"Agent edits are also displayed in single-file editors for review.",
)
.color(Color::Muted),
),
)
.child(
Switch::new("single-file-review-switch", single_file_review.into())
.color(SwitchColor::Accent)
.on_click({
let fs = self.fs.clone();
move |state, _window, cx| {
let allow = state == &ToggleState::Selected;
update_settings_file::<AgentSettings>(
fs.clone(),
cx,
move |settings, _| {
settings.set_single_file_review(allow);
},
);
}
}),
)
SwitchField::new(
"single-file-review",
"Enable single-file agent reviews",
"Agent edits are also displayed in single-file editors for review.",
single_file_review,
move |state, _window, cx| {
let allow = state == &ToggleState::Selected;
update_settings_file::<AgentSettings>(fs.clone(), cx, move |settings, _| {
settings.set_single_file_review(allow);
});
},
)
}
fn render_sound_notification(&mut self, cx: &mut Context<Self>) -> impl IntoElement {
let play_sound_when_agent_done = AgentSettings::get_global(cx).play_sound_when_agent_done;
let fs = self.fs.clone();
h_flex()
.gap_4()
.justify_between()
.flex_wrap()
.child(
v_flex()
.gap_0p5()
.max_w_5_6()
.child(Label::new("Play sound when finished generating"))
.child(
Label::new(
"Hear a notification sound when the agent is done generating changes or needs your input.",
)
.color(Color::Muted),
),
)
.child(
Switch::new("play-sound-notification-switch", play_sound_when_agent_done.into())
.color(SwitchColor::Accent)
.on_click({
let fs = self.fs.clone();
move |state, _window, cx| {
let allow = state == &ToggleState::Selected;
update_settings_file::<AgentSettings>(
fs.clone(),
cx,
move |settings, _| {
settings.set_play_sound_when_agent_done(allow);
},
);
}
}),
)
SwitchField::new(
"sound-notification",
"Play sound when finished generating",
"Hear a notification sound when the agent is done generating changes or needs your input.",
play_sound_when_agent_done,
move |state, _window, cx| {
let allow = state == &ToggleState::Selected;
update_settings_file::<AgentSettings>(fs.clone(), cx, move |settings, _| {
settings.set_play_sound_when_agent_done(allow);
});
},
)
}
fn render_modifier_to_send(&mut self, cx: &mut Context<Self>) -> impl IntoElement {
let use_modifier_to_send = AgentSettings::get_global(cx).use_modifier_to_send;
let fs = self.fs.clone();
SwitchField::new(
"modifier-send",
"Use modifier to submit a message",
"Make a modifier (cmd-enter on macOS, ctrl-enter on Linux) required to send messages.",
use_modifier_to_send,
move |state, _window, cx| {
let allow = state == &ToggleState::Selected;
update_settings_file::<AgentSettings>(fs.clone(), cx, move |settings, _| {
settings.set_use_modifier_to_send(allow);
});
},
)
}
fn render_general_settings_section(&mut self, cx: &mut Context<Self>) -> impl IntoElement {
@@ -456,6 +475,7 @@ impl AgentConfiguration {
.child(self.render_command_permission(cx))
.child(self.render_single_file_review(cx))
.child(self.render_sound_notification(cx))
.child(self.render_modifier_to_send(cx))
}
fn render_zed_plan_info(&self, plan: Option<Plan>, cx: &mut Context<Self>) -> impl IntoElement {

View File

@@ -0,0 +1,639 @@
use std::sync::Arc;
use anyhow::Result;
use collections::HashSet;
use fs::Fs;
use gpui::{DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, Render, Task};
use language_model::LanguageModelRegistry;
use language_models::{
AllLanguageModelSettings, OpenAiCompatibleSettingsContent,
provider::open_ai_compatible::AvailableModel,
};
use settings::update_settings_file;
use ui::{Banner, KeyBinding, Modal, ModalFooter, ModalHeader, Section, prelude::*};
use ui_input::SingleLineInput;
use workspace::{ModalView, Workspace};
#[derive(Clone, Copy)]
pub enum LlmCompatibleProvider {
OpenAi,
}
impl LlmCompatibleProvider {
fn name(&self) -> &'static str {
match self {
LlmCompatibleProvider::OpenAi => "OpenAI",
}
}
fn api_url(&self) -> &'static str {
match self {
LlmCompatibleProvider::OpenAi => "https://api.openai.com/v1",
}
}
}
struct AddLlmProviderInput {
provider_name: Entity<SingleLineInput>,
api_url: Entity<SingleLineInput>,
api_key: Entity<SingleLineInput>,
models: Vec<ModelInput>,
}
impl AddLlmProviderInput {
fn new(provider: LlmCompatibleProvider, window: &mut Window, cx: &mut App) -> Self {
let provider_name = single_line_input("Provider Name", provider.name(), None, window, cx);
let api_url = single_line_input("API URL", provider.api_url(), None, window, cx);
let api_key = single_line_input(
"API Key",
"000000000000000000000000000000000000000000000000",
None,
window,
cx,
);
Self {
provider_name,
api_url,
api_key,
models: vec![ModelInput::new(window, cx)],
}
}
fn add_model(&mut self, window: &mut Window, cx: &mut App) {
self.models.push(ModelInput::new(window, cx));
}
fn remove_model(&mut self, index: usize) {
self.models.remove(index);
}
}
struct ModelInput {
name: Entity<SingleLineInput>,
max_completion_tokens: Entity<SingleLineInput>,
max_output_tokens: Entity<SingleLineInput>,
max_tokens: Entity<SingleLineInput>,
}
impl ModelInput {
fn new(window: &mut Window, cx: &mut App) -> Self {
let model_name = single_line_input(
"Model Name",
"e.g. gpt-4o, claude-opus-4, gemini-2.5-pro",
None,
window,
cx,
);
let max_completion_tokens = single_line_input(
"Max Completion Tokens",
"200000",
Some("200000"),
window,
cx,
);
let max_output_tokens = single_line_input(
"Max Output Tokens",
"Max Output Tokens",
Some("32000"),
window,
cx,
);
let max_tokens = single_line_input("Max Tokens", "Max Tokens", Some("200000"), window, cx);
Self {
name: model_name,
max_completion_tokens,
max_output_tokens,
max_tokens,
}
}
fn parse(&self, cx: &App) -> Result<AvailableModel, SharedString> {
let name = self.name.read(cx).text(cx);
if name.is_empty() {
return Err(SharedString::from("Model Name cannot be empty"));
}
Ok(AvailableModel {
name,
display_name: None,
max_completion_tokens: Some(
self.max_completion_tokens
.read(cx)
.text(cx)
.parse::<u64>()
.map_err(|_| SharedString::from("Max Completion Tokens must be a number"))?,
),
max_output_tokens: Some(
self.max_output_tokens
.read(cx)
.text(cx)
.parse::<u64>()
.map_err(|_| SharedString::from("Max Output Tokens must be a number"))?,
),
max_tokens: self
.max_tokens
.read(cx)
.text(cx)
.parse::<u64>()
.map_err(|_| SharedString::from("Max Tokens must be a number"))?,
})
}
}
fn single_line_input(
label: impl Into<SharedString>,
placeholder: impl Into<SharedString>,
text: Option<&str>,
window: &mut Window,
cx: &mut App,
) -> Entity<SingleLineInput> {
cx.new(|cx| {
let input = SingleLineInput::new(window, cx, placeholder).label(label);
if let Some(text) = text {
input
.editor()
.update(cx, |editor, cx| editor.set_text(text, window, cx));
}
input
})
}
fn save_provider_to_settings(
input: &AddLlmProviderInput,
cx: &mut App,
) -> Task<Result<(), SharedString>> {
let provider_name: Arc<str> = input.provider_name.read(cx).text(cx).into();
if provider_name.is_empty() {
return Task::ready(Err("Provider Name cannot be empty".into()));
}
if LanguageModelRegistry::read_global(cx)
.providers()
.iter()
.any(|provider| {
provider.id().0.as_ref() == provider_name.as_ref()
|| provider.name().0.as_ref() == provider_name.as_ref()
})
{
return Task::ready(Err(
"Provider Name is already taken by another provider".into()
));
}
let api_url = input.api_url.read(cx).text(cx);
if api_url.is_empty() {
return Task::ready(Err("API URL cannot be empty".into()));
}
let api_key = input.api_key.read(cx).text(cx);
if api_key.is_empty() {
return Task::ready(Err("API Key cannot be empty".into()));
}
let mut models = Vec::new();
let mut model_names: HashSet<String> = HashSet::default();
for model in &input.models {
match model.parse(cx) {
Ok(model) => {
if !model_names.insert(model.name.clone()) {
return Task::ready(Err("Model Names must be unique".into()));
}
models.push(model)
}
Err(err) => return Task::ready(Err(err)),
}
}
let fs = <dyn Fs>::global(cx);
let task = cx.write_credentials(&api_url, "Bearer", api_key.as_bytes());
cx.spawn(async move |cx| {
task.await
.map_err(|_| "Failed to write API key to keychain")?;
cx.update(|cx| {
update_settings_file::<AllLanguageModelSettings>(fs, cx, |settings, _cx| {
settings.openai_compatible.get_or_insert_default().insert(
provider_name,
OpenAiCompatibleSettingsContent {
api_url,
available_models: models,
},
);
});
})
.ok();
Ok(())
})
}
pub struct AddLlmProviderModal {
provider: LlmCompatibleProvider,
input: AddLlmProviderInput,
focus_handle: FocusHandle,
last_error: Option<SharedString>,
}
impl AddLlmProviderModal {
pub fn toggle(
provider: LlmCompatibleProvider,
workspace: &mut Workspace,
window: &mut Window,
cx: &mut Context<Workspace>,
) {
workspace.toggle_modal(window, cx, |window, cx| Self::new(provider, window, cx));
}
fn new(provider: LlmCompatibleProvider, window: &mut Window, cx: &mut Context<Self>) -> Self {
Self {
input: AddLlmProviderInput::new(provider, window, cx),
provider,
last_error: None,
focus_handle: cx.focus_handle(),
}
}
fn confirm(&mut self, _: &menu::Confirm, _: &mut Window, cx: &mut Context<Self>) {
let task = save_provider_to_settings(&self.input, cx);
cx.spawn(async move |this, cx| {
let result = task.await;
this.update(cx, |this, cx| match result {
Ok(_) => {
cx.emit(DismissEvent);
}
Err(error) => {
this.last_error = Some(error);
cx.notify();
}
})
})
.detach_and_log_err(cx);
}
fn cancel(&mut self, _: &menu::Cancel, _: &mut Window, cx: &mut Context<Self>) {
cx.emit(DismissEvent);
}
fn render_section(&self) -> Section {
Section::new()
.child(self.input.provider_name.clone())
.child(self.input.api_url.clone())
.child(self.input.api_key.clone())
}
fn render_model_section(&self, cx: &mut Context<Self>) -> Section {
Section::new().child(
v_flex()
.gap_2()
.child(
h_flex()
.justify_between()
.child(Label::new("Models").size(LabelSize::Small))
.child(
Button::new("add-model", "Add Model")
.icon(IconName::Plus)
.icon_position(IconPosition::Start)
.icon_size(IconSize::XSmall)
.icon_color(Color::Muted)
.label_size(LabelSize::Small)
.on_click(cx.listener(|this, _, window, cx| {
this.input.add_model(window, cx);
cx.notify();
})),
),
)
.children(
self.input
.models
.iter()
.enumerate()
.map(|(ix, _)| self.render_model(ix, cx)),
),
)
}
fn render_model(&self, ix: usize, cx: &mut Context<Self>) -> impl IntoElement + use<> {
let has_more_than_one_model = self.input.models.len() > 1;
let model = &self.input.models[ix];
v_flex()
.p_2()
.gap_2()
.rounded_sm()
.border_1()
.border_dashed()
.border_color(cx.theme().colors().border.opacity(0.6))
.bg(cx.theme().colors().element_active.opacity(0.15))
.child(model.name.clone())
.child(
h_flex()
.gap_2()
.child(model.max_completion_tokens.clone())
.child(model.max_output_tokens.clone()),
)
.child(model.max_tokens.clone())
.when(has_more_than_one_model, |this| {
this.child(
Button::new(("remove-model", ix), "Remove Model")
.icon(IconName::Trash)
.icon_position(IconPosition::Start)
.icon_size(IconSize::XSmall)
.icon_color(Color::Muted)
.label_size(LabelSize::Small)
.style(ButtonStyle::Outlined)
.full_width()
.on_click(cx.listener(move |this, _, _window, cx| {
this.input.remove_model(ix);
cx.notify();
})),
)
})
}
}
impl EventEmitter<DismissEvent> for AddLlmProviderModal {}
impl Focusable for AddLlmProviderModal {
fn focus_handle(&self, _cx: &App) -> FocusHandle {
self.focus_handle.clone()
}
}
impl ModalView for AddLlmProviderModal {}
impl Render for AddLlmProviderModal {
fn render(&mut self, window: &mut ui::Window, cx: &mut ui::Context<Self>) -> impl IntoElement {
let focus_handle = self.focus_handle(cx);
div()
.id("add-llm-provider-modal")
.key_context("AddLlmProviderModal")
.w(rems(34.))
.elevation_3(cx)
.on_action(cx.listener(Self::cancel))
.capture_any_mouse_down(cx.listener(|this, _, window, cx| {
this.focus_handle(cx).focus(window);
}))
.child(
Modal::new("configure-context-server", None)
.header(ModalHeader::new().headline("Add LLM Provider").description(
match self.provider {
LlmCompatibleProvider::OpenAi => {
"This provider will use an OpenAI compatible API."
}
},
))
.when_some(self.last_error.clone(), |this, error| {
this.section(
Section::new().child(
Banner::new()
.severity(ui::Severity::Warning)
.child(div().text_xs().child(error)),
),
)
})
.child(
v_flex()
.id("modal_content")
.max_h_128()
.overflow_y_scroll()
.gap_2()
.child(self.render_section())
.child(self.render_model_section(cx)),
)
.footer(
ModalFooter::new().end_slot(
h_flex()
.gap_1()
.child(
Button::new("cancel", "Cancel")
.key_binding(
KeyBinding::for_action_in(
&menu::Cancel,
&focus_handle,
window,
cx,
)
.map(|kb| kb.size(rems_from_px(12.))),
)
.on_click(cx.listener(|this, _event, window, cx| {
this.cancel(&menu::Cancel, window, cx)
})),
)
.child(
Button::new("save-server", "Save Provider")
.key_binding(
KeyBinding::for_action_in(
&menu::Confirm,
&focus_handle,
window,
cx,
)
.map(|kb| kb.size(rems_from_px(12.))),
)
.on_click(cx.listener(|this, _event, window, cx| {
this.confirm(&menu::Confirm, window, cx)
})),
),
),
),
)
}
}
#[cfg(test)]
mod tests {
use super::*;
use editor::EditorSettings;
use fs::FakeFs;
use gpui::{TestAppContext, VisualTestContext};
use language::language_settings;
use language_model::{
LanguageModelProviderId, LanguageModelProviderName,
fake_provider::FakeLanguageModelProvider,
};
use project::Project;
use settings::{Settings as _, SettingsStore};
use util::path;
#[gpui::test]
async fn test_save_provider_invalid_inputs(cx: &mut TestAppContext) {
let cx = setup_test(cx).await;
assert_eq!(
save_provider_validation_errors("", "someurl", "somekey", vec![], cx,).await,
Some("Provider Name cannot be empty".into())
);
assert_eq!(
save_provider_validation_errors("someprovider", "", "somekey", vec![], cx,).await,
Some("API URL cannot be empty".into())
);
assert_eq!(
save_provider_validation_errors("someprovider", "someurl", "", vec![], cx,).await,
Some("API Key cannot be empty".into())
);
assert_eq!(
save_provider_validation_errors(
"someprovider",
"someurl",
"somekey",
vec![("", "200000", "200000", "32000")],
cx,
)
.await,
Some("Model Name cannot be empty".into())
);
assert_eq!(
save_provider_validation_errors(
"someprovider",
"someurl",
"somekey",
vec![("somemodel", "abc", "200000", "32000")],
cx,
)
.await,
Some("Max Tokens must be a number".into())
);
assert_eq!(
save_provider_validation_errors(
"someprovider",
"someurl",
"somekey",
vec![("somemodel", "200000", "abc", "32000")],
cx,
)
.await,
Some("Max Completion Tokens must be a number".into())
);
assert_eq!(
save_provider_validation_errors(
"someprovider",
"someurl",
"somekey",
vec![("somemodel", "200000", "200000", "abc")],
cx,
)
.await,
Some("Max Output Tokens must be a number".into())
);
assert_eq!(
save_provider_validation_errors(
"someprovider",
"someurl",
"somekey",
vec![
("somemodel", "200000", "200000", "32000"),
("somemodel", "200000", "200000", "32000"),
],
cx,
)
.await,
Some("Model Names must be unique".into())
);
}
#[gpui::test]
async fn test_save_provider_name_conflict(cx: &mut TestAppContext) {
let cx = setup_test(cx).await;
cx.update(|_window, cx| {
LanguageModelRegistry::global(cx).update(cx, |registry, cx| {
registry.register_provider(
FakeLanguageModelProvider::new(
LanguageModelProviderId::new("someprovider"),
LanguageModelProviderName::new("Some Provider"),
),
cx,
);
});
});
assert_eq!(
save_provider_validation_errors(
"someprovider",
"someurl",
"someapikey",
vec![("somemodel", "200000", "200000", "32000")],
cx,
)
.await,
Some("Provider Name is already taken by another provider".into())
);
}
async fn setup_test(cx: &mut TestAppContext) -> &mut VisualTestContext {
cx.update(|cx| {
let store = SettingsStore::test(cx);
cx.set_global(store);
workspace::init_settings(cx);
Project::init_settings(cx);
theme::init(theme::LoadThemes::JustBase, cx);
language_settings::init(cx);
EditorSettings::register(cx);
language_model::init_settings(cx);
language_models::init_settings(cx);
});
let fs = FakeFs::new(cx.executor());
cx.update(|cx| <dyn Fs>::set_global(fs.clone(), cx));
let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
let (_, cx) =
cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
cx
}
async fn save_provider_validation_errors(
provider_name: &str,
api_url: &str,
api_key: &str,
models: Vec<(&str, &str, &str, &str)>,
cx: &mut VisualTestContext,
) -> Option<SharedString> {
fn set_text(
input: &Entity<SingleLineInput>,
text: &str,
window: &mut Window,
cx: &mut App,
) {
input.update(cx, |input, cx| {
input.editor().update(cx, |editor, cx| {
editor.set_text(text, window, cx);
});
});
}
let task = cx.update(|window, cx| {
let mut input = AddLlmProviderInput::new(LlmCompatibleProvider::OpenAi, window, cx);
set_text(&input.provider_name, provider_name, window, cx);
set_text(&input.api_url, api_url, window, cx);
set_text(&input.api_key, api_key, window, cx);
for (i, (name, max_tokens, max_completion_tokens, max_output_tokens)) in
models.iter().enumerate()
{
if i >= input.models.len() {
input.models.push(ModelInput::new(window, cx));
}
let model = &mut input.models[i];
set_text(&model.name, name, window, cx);
set_text(&model.max_tokens, max_tokens, window, cx);
set_text(
&model.max_completion_tokens,
max_completion_tokens,
window,
cx,
);
set_text(&model.max_output_tokens, max_output_tokens, window, cx);
}
save_provider_to_settings(&input, cx)
});
task.await.err()
}
}

View File

@@ -1,4 +1,5 @@
use std::{
path::PathBuf,
sync::{Arc, Mutex},
time::Duration,
};
@@ -188,7 +189,7 @@ fn context_server_input(existing: Option<(ContextServerId, ContextServerCommand)
}
None => (
"some-mcp-server".to_string(),
"".to_string(),
PathBuf::new(),
"[]".to_string(),
"{}".to_string(),
),
@@ -199,13 +200,14 @@ fn context_server_input(existing: Option<(ContextServerId, ContextServerCommand)
/// The name of your MCP server
"{name}": {{
/// The command which runs the MCP server
"command": "{command}",
"command": "{}",
/// The arguments to pass to the MCP server
"args": {args},
/// The environment variables to set
"env": {env}
}}
}}"#
}}"#,
command.display()
)
}

View File

@@ -1506,8 +1506,7 @@ impl AgentDiff {
.read(cx)
.entries()
.last()
.and_then(|entry| entry.diff())
.is_some()
.map_or(false, |entry| entry.diffs().next().is_some())
{
self.update_reviewing_editors(workspace, window, cx);
}
@@ -1517,8 +1516,7 @@ impl AgentDiff {
.read(cx)
.entries()
.get(*ix)
.and_then(|entry| entry.diff())
.is_some()
.map_or(false, |entry| entry.diffs().next().is_some())
{
self.update_reviewing_editors(workspace, window, cx);
}

View File

@@ -1,5 +1,5 @@
use std::cell::RefCell;
use std::ops::Range;
use std::ops::{Not, Range};
use std::path::Path;
use std::rc::Rc;
use std::sync::Arc;
@@ -11,6 +11,8 @@ use serde::{Deserialize, Serialize};
use crate::NewExternalAgentThread;
use crate::agent_diff::AgentDiffThread;
use crate::message_editor::{MAX_EDITOR_LINES, MIN_EDITOR_LINES};
use crate::ui::NewThreadButton;
use crate::{
AddContextServer, AgentDiffPane, ContinueThread, ContinueWithBurnMode,
DeleteRecentlyOpenThread, ExpandMessageEditor, Follow, InlineAssistant, NewTextThread,
@@ -41,7 +43,7 @@ use anyhow::{Result, anyhow};
use assistant_context::{AssistantContext, ContextEvent, ContextSummary};
use assistant_slash_command::SlashCommandWorkingSet;
use assistant_tool::ToolWorkingSet;
use client::{UserStore, zed_urls};
use client::{DisableAiSettings, UserStore, zed_urls};
use editor::{Anchor, AnchorRangeExt as _, Editor, EditorEvent, MultiBuffer};
use feature_flags::{self, FeatureFlagAppExt};
use fs::Fs;
@@ -65,8 +67,8 @@ use theme::ThemeSettings;
use time::UtcOffset;
use ui::utils::WithRemSize;
use ui::{
Banner, Callout, ContextMenu, ElevationIndex, KeyBinding, PopoverMenu, PopoverMenuHandle,
ProgressBar, Tab, Tooltip, prelude::*,
Banner, Callout, ContextMenu, ContextMenuEntry, ElevationIndex, KeyBinding, PopoverMenu,
PopoverMenuHandle, ProgressBar, Tab, Tooltip, prelude::*,
};
use util::ResultExt as _;
use workspace::{
@@ -438,7 +440,7 @@ pub struct AgentPanel {
local_timezone: UtcOffset,
active_view: ActiveView,
acp_message_history:
Rc<RefCell<crate::acp::MessageHistory<agentic_coding_protocol::SendUserMessageParams>>>,
Rc<RefCell<crate::acp::MessageHistory<Vec<agent_client_protocol::ContentBlock>>>>,
previous_view: Option<ActiveView>,
history_store: Entity<HistoryStore>,
history: Entity<ThreadHistory>,
@@ -742,6 +744,7 @@ impl AgentPanel {
if workspace
.panel::<Self>(cx)
.is_some_and(|panel| panel.read(cx).enabled(cx))
&& !DisableAiSettings::get_global(cx).disable_ai
{
workspace.toggle_panel_focus::<Self>(window, cx);
}
@@ -960,6 +963,8 @@ impl AgentPanel {
workspace.clone(),
project,
message_history,
MIN_EDITOR_LINES,
Some(MAX_EDITOR_LINES),
window,
cx,
)
@@ -1677,7 +1682,7 @@ impl Panel for AgentPanel {
}
fn enabled(&self, cx: &App) -> bool {
AgentSettings::get_global(cx).enabled
DisableAiSettings::get_global(cx).disable_ai.not() && AgentSettings::get_global(cx).enabled
}
fn is_zoomed(&self, _window: &Window, _cx: &App) -> bool {
@@ -1896,48 +1901,96 @@ impl AgentPanel {
)
.anchor(Corner::TopRight)
.with_handle(self.new_thread_menu_handle.clone())
.menu(move |window, cx| {
let active_thread = active_thread.clone();
Some(ContextMenu::build(window, cx, |mut menu, _window, cx| {
menu = menu
.when(cx.has_flag::<feature_flags::AcpFeatureFlag>(), |this| {
this.header("Zed Agent")
})
.action("New Thread", NewThread::default().boxed_clone())
.action("New Text Thread", NewTextThread.boxed_clone())
.when_some(active_thread, |this, active_thread| {
let thread = active_thread.read(cx);
if !thread.is_empty() {
this.action(
"New From Summary",
Box::new(NewThread {
from_thread_id: Some(thread.id().clone()),
.menu({
let focus_handle = focus_handle.clone();
move |window, cx| {
let active_thread = active_thread.clone();
Some(ContextMenu::build(window, cx, |mut menu, _window, cx| {
menu = menu
.context(focus_handle.clone())
.when(cx.has_flag::<feature_flags::AcpFeatureFlag>(), |this| {
this.header("Zed Agent")
})
.item(
ContextMenuEntry::new("New Thread")
.icon(IconName::NewThread)
.icon_color(Color::Muted)
.action(NewThread::default().boxed_clone())
.handler(move |window, cx| {
window.dispatch_action(
NewThread::default().boxed_clone(),
cx,
);
}),
)
} else {
this
}
})
.when(cx.has_flag::<feature_flags::AcpFeatureFlag>(), |this| {
this.separator()
.header("External Agents")
.action(
"New Gemini Thread",
NewExternalAgentThread {
agent: Some(crate::ExternalAgent::Gemini),
}
.boxed_clone(),
)
.action(
"New Claude Code Thread",
NewExternalAgentThread {
agent: Some(crate::ExternalAgent::ClaudeCode),
}
.boxed_clone(),
)
});
menu
}))
)
.item(
ContextMenuEntry::new("New Text Thread")
.icon(IconName::NewTextThread)
.icon_color(Color::Muted)
.action(NewTextThread.boxed_clone())
.handler(move |window, cx| {
window.dispatch_action(NewTextThread.boxed_clone(), cx);
}),
)
.when_some(active_thread, |this, active_thread| {
let thread = active_thread.read(cx);
if !thread.is_empty() {
let thread_id = thread.id().clone();
this.item(
ContextMenuEntry::new("New From Summary")
.icon(IconName::NewFromSummary)
.icon_color(Color::Muted)
.handler(move |window, cx| {
window.dispatch_action(
Box::new(NewThread {
from_thread_id: Some(thread_id.clone()),
}),
cx,
);
}),
)
} else {
this
}
})
.when(cx.has_flag::<feature_flags::AcpFeatureFlag>(), |this| {
this.separator()
.header("External Agents")
.item(
ContextMenuEntry::new("New Gemini Thread")
.icon(IconName::AiGemini)
.icon_color(Color::Muted)
.handler(move |window, cx| {
window.dispatch_action(
NewExternalAgentThread {
agent: Some(crate::ExternalAgent::Gemini),
}
.boxed_clone(),
cx,
);
}),
)
.item(
ContextMenuEntry::new("New Claude Code Thread")
.icon(IconName::AiClaude)
.icon_color(Color::Muted)
.handler(move |window, cx| {
window.dispatch_action(
NewExternalAgentThread {
agent: Some(
crate::ExternalAgent::ClaudeCode,
),
}
.boxed_clone(),
cx,
);
}),
)
});
menu
}))
}
});
let agent_panel_menu = PopoverMenu::new("agent-options-menu")
@@ -2259,7 +2312,20 @@ impl AgentPanel {
return None;
}
Some(div().size_full().child(self.onboarding.clone()))
let thread_view = matches!(&self.active_view, ActiveView::Thread { .. });
let text_thread_view = matches!(&self.active_view, ActiveView::TextThread { .. });
Some(
div()
.size_full()
.when(thread_view, |this| {
this.bg(cx.theme().colors().panel_background)
})
.when(text_thread_view, |this| {
this.bg(cx.theme().colors().editor_background)
})
.child(self.onboarding.clone()),
)
}
fn render_trial_end_upsell(
@@ -2282,6 +2348,28 @@ impl AgentPanel {
})))
}
fn render_empty_state_section_header(
&self,
label: impl Into<SharedString>,
action_slot: Option<AnyElement>,
cx: &mut Context<Self>,
) -> impl IntoElement {
h_flex()
.mt_2()
.pl_1p5()
.pb_1()
.w_full()
.justify_between()
.border_b_1()
.border_color(cx.theme().colors().border_variant)
.child(
Label::new(label.into())
.size(LabelSize::Small)
.color(Color::Muted),
)
.children(action_slot)
}
fn render_thread_empty_state(
&self,
window: &mut Window,
@@ -2404,19 +2492,9 @@ impl AgentPanel {
.justify_end()
.gap_1()
.child(
h_flex()
.pl_1p5()
.pb_1()
.w_full()
.justify_between()
.border_b_1()
.border_color(cx.theme().colors().border_variant)
.child(
Label::new("Recent")
.size(LabelSize::Small)
.color(Color::Muted),
)
.child(
self.render_empty_state_section_header(
"Recent",
Some(
Button::new("view-history", "View All")
.style(ButtonStyle::Subtle)
.label_size(LabelSize::Small)
@@ -2431,8 +2509,11 @@ impl AgentPanel {
)
.on_click(move |_event, window, cx| {
window.dispatch_action(OpenHistory.boxed_clone(), cx);
}),
})
.into_any_element(),
),
cx,
),
)
.child(
v_flex()
@@ -2460,6 +2541,113 @@ impl AgentPanel {
},
)),
)
.child(self.render_empty_state_section_header("Start", None, cx))
.child(
v_flex()
.p_1()
.gap_2()
.child(
h_flex()
.w_full()
.gap_2()
.child(
NewThreadButton::new(
"new-thread-btn",
"New Thread",
IconName::NewThread,
)
.keybinding(KeyBinding::for_action_in(
&NewThread::default(),
&self.focus_handle(cx),
window,
cx,
))
.on_click(
|window, cx| {
window.dispatch_action(
NewThread::default().boxed_clone(),
cx,
)
},
),
)
.child(
NewThreadButton::new(
"new-text-thread-btn",
"New Text Thread",
IconName::NewTextThread,
)
.keybinding(KeyBinding::for_action_in(
&NewTextThread,
&self.focus_handle(cx),
window,
cx,
))
.on_click(
|window, cx| {
window.dispatch_action(Box::new(NewTextThread), cx)
},
),
),
)
.when(cx.has_flag::<feature_flags::AcpFeatureFlag>(), |this| {
this.child(
h_flex()
.w_full()
.gap_2()
.child(
NewThreadButton::new(
"new-gemini-thread-btn",
"New Gemini Thread",
IconName::AiGemini,
)
// .keybinding(KeyBinding::for_action_in(
// &OpenHistory,
// &self.focus_handle(cx),
// window,
// cx,
// ))
.on_click(
|window, cx| {
window.dispatch_action(
Box::new(NewExternalAgentThread {
agent: Some(
crate::ExternalAgent::Gemini,
),
}),
cx,
)
},
),
)
.child(
NewThreadButton::new(
"new-claude-thread-btn",
"New Claude Code Thread",
IconName::AiClaude,
)
// .keybinding(KeyBinding::for_action_in(
// &OpenHistory,
// &self.focus_handle(cx),
// window,
// cx,
// ))
.on_click(
|window, cx| {
window.dispatch_action(
Box::new(NewExternalAgentThread {
agent: Some(
crate::ExternalAgent::ClaudeCode,
),
}),
cx,
)
},
),
),
)
}),
)
.when_some(configuration_error.as_ref(), |this, err| {
this.child(self.render_configuration_error(err, &focus_handle, window, cx))
})
@@ -3074,7 +3262,20 @@ impl Render for AgentPanel {
.into_any(),
)
})
.child(h_flex().child(message_editor.clone()))
.child(h_flex().relative().child(message_editor.clone()).when(
!LanguageModelRegistry::read_global(cx).has_authenticated_provider(cx),
|this| {
this.child(
div()
.size_full()
.absolute()
.inset_0()
.bg(cx.theme().colors().panel_background)
.opacity(0.8)
.block_mouse_except_scroll(),
)
},
))
.child(self.render_drag_target(cx)),
ActiveView::ExternalAgentThread { thread_view, .. } => parent
.relative()

View File

@@ -31,7 +31,8 @@ use std::sync::Arc;
use agent::{Thread, ThreadId};
use agent_settings::{AgentProfileId, AgentSettings, LanguageModelSelection};
use assistant_slash_command::SlashCommandRegistry;
use client::Client;
use client::{Client, DisableAiSettings};
use command_palette_hooks::CommandPaletteFilter;
use feature_flags::FeatureFlagAppExt as _;
use fs::Fs;
use gpui::{Action, App, Entity, actions};
@@ -43,6 +44,7 @@ use prompt_store::PromptBuilder;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use settings::{Settings as _, SettingsStore};
use std::any::TypeId;
pub use crate::active_thread::ActiveThread;
use crate::agent_configuration::{ConfigureContextServerModal, ManageProfilesModal};
@@ -52,6 +54,7 @@ use crate::slash_command_settings::SlashCommandSettings;
pub use agent_diff::{AgentDiffPane, AgentDiffToolbar};
pub use text_thread_editor::{AgentPanelDelegate, TextThreadEditor};
pub use ui::preview::{all_agent_previews, get_agent_preview};
use zed_actions;
actions!(
agent,
@@ -241,6 +244,66 @@ pub fn init(
})
.detach();
cx.observe_new(ManageProfilesModal::register).detach();
// Update command palette filter based on AI settings
update_command_palette_filter(cx);
// Watch for settings changes
cx.observe_global::<SettingsStore>(|app_cx| {
// When settings change, update the command palette filter
update_command_palette_filter(app_cx);
})
.detach();
}
fn update_command_palette_filter(cx: &mut App) {
let disable_ai = DisableAiSettings::get_global(cx).disable_ai;
CommandPaletteFilter::update_global(cx, |filter, _| {
if disable_ai {
filter.hide_namespace("agent");
filter.hide_namespace("assistant");
filter.hide_namespace("zed_predict_onboarding");
filter.hide_namespace("edit_prediction");
use editor::actions::{
AcceptEditPrediction, AcceptPartialEditPrediction, NextEditPrediction,
PreviousEditPrediction, ShowEditPrediction, ToggleEditPrediction,
};
let edit_prediction_actions = [
TypeId::of::<AcceptEditPrediction>(),
TypeId::of::<AcceptPartialEditPrediction>(),
TypeId::of::<ShowEditPrediction>(),
TypeId::of::<NextEditPrediction>(),
TypeId::of::<PreviousEditPrediction>(),
TypeId::of::<ToggleEditPrediction>(),
];
filter.hide_action_types(&edit_prediction_actions);
filter.hide_action_types(&[TypeId::of::<zed_actions::OpenZedPredictOnboarding>()]);
} else {
filter.show_namespace("agent");
filter.show_namespace("assistant");
filter.show_namespace("zed_predict_onboarding");
filter.show_namespace("edit_prediction");
use editor::actions::{
AcceptEditPrediction, AcceptPartialEditPrediction, NextEditPrediction,
PreviousEditPrediction, ShowEditPrediction, ToggleEditPrediction,
};
let edit_prediction_actions = [
TypeId::of::<AcceptEditPrediction>(),
TypeId::of::<AcceptPartialEditPrediction>(),
TypeId::of::<ShowEditPrediction>(),
TypeId::of::<NextEditPrediction>(),
TypeId::of::<PreviousEditPrediction>(),
TypeId::of::<ToggleEditPrediction>(),
];
filter.show_action_types(edit_prediction_actions.iter());
filter
.show_action_types([TypeId::of::<zed_actions::OpenZedPredictOnboarding>()].iter());
}
});
}
fn init_language_model_settings(cx: &mut App) {

View File

@@ -16,7 +16,7 @@ use agent::{
};
use agent_settings::AgentSettings;
use anyhow::{Context as _, Result};
use client::telemetry::Telemetry;
use client::{DisableAiSettings, telemetry::Telemetry};
use collections::{HashMap, HashSet, VecDeque, hash_map};
use editor::SelectionEffects;
use editor::{
@@ -57,6 +57,17 @@ pub fn init(
cx: &mut App,
) {
cx.set_global(InlineAssistant::new(fs, prompt_builder, telemetry));
cx.observe_global::<SettingsStore>(|cx| {
if DisableAiSettings::get_global(cx).disable_ai {
// Hide any active inline assist UI when AI is disabled
InlineAssistant::update_global(cx, |assistant, cx| {
assistant.cancel_all_active_completions(cx);
});
}
})
.detach();
cx.observe_new(|_workspace: &mut Workspace, window, cx| {
let Some(window) = window else {
return;
@@ -141,6 +152,26 @@ impl InlineAssistant {
.detach();
}
/// Hides all active inline assists when AI is disabled
pub fn cancel_all_active_completions(&mut self, cx: &mut App) {
// Cancel all active completions in editors
for (editor_handle, _) in self.assists_by_editor.iter() {
if let Some(editor) = editor_handle.upgrade() {
let windows = cx.windows();
if !windows.is_empty() {
let window = windows[0];
let _ = window.update(cx, |_, window, cx| {
editor.update(cx, |editor, cx| {
if editor.has_active_inline_completion() {
editor.cancel(&Default::default(), window, cx);
}
});
});
}
}
}
}
fn handle_workspace_event(
&mut self,
workspace: Entity<Workspace>,
@@ -176,7 +207,7 @@ impl InlineAssistant {
window: &mut Window,
cx: &mut App,
) {
let is_assistant2_enabled = true;
let is_assistant2_enabled = !DisableAiSettings::get_global(cx).disable_ai;
if let Some(editor) = item.act_as::<Editor>(cx) {
editor.update(cx, |editor, cx| {
@@ -199,6 +230,13 @@ impl InlineAssistant {
cx,
);
if DisableAiSettings::get_global(cx).disable_ai {
// Cancel any active completions
if editor.has_active_inline_completion() {
editor.cancel(&Default::default(), window, cx);
}
}
// Remove the Assistant1 code action provider, as it still might be registered.
editor.remove_code_action_provider("assistant".into(), window, cx);
} else {
@@ -219,7 +257,7 @@ impl InlineAssistant {
cx: &mut Context<Workspace>,
) {
let settings = AgentSettings::get_global(cx);
if !settings.enabled {
if !settings.enabled || DisableAiSettings::get_global(cx).disable_ai {
return;
}

View File

@@ -14,6 +14,7 @@ use agent::{
context_store::ContextStoreEvent,
};
use agent_settings::{AgentSettings, CompletionMode};
use ai_onboarding::ApiKeysWithProviders;
use buffer_diff::BufferDiff;
use client::UserStore;
use collections::{HashMap, HashSet};
@@ -33,7 +34,8 @@ use gpui::{
};
use language::{Buffer, Language, Point};
use language_model::{
ConfiguredModel, LanguageModelRequestMessage, MessageContent, ZED_CLOUD_PROVIDER_ID,
ConfiguredModel, LanguageModelRegistry, LanguageModelRequestMessage, MessageContent,
ZED_CLOUD_PROVIDER_ID,
};
use multi_buffer;
use project::Project;
@@ -65,6 +67,9 @@ use agent::{
thread_store::{TextThreadStore, ThreadStore},
};
pub const MIN_EDITOR_LINES: usize = 4;
pub const MAX_EDITOR_LINES: usize = 8;
#[derive(RegisterComponent)]
pub struct MessageEditor {
thread: Entity<Thread>,
@@ -88,9 +93,6 @@ pub struct MessageEditor {
_subscriptions: Vec<Subscription>,
}
const MIN_EDITOR_LINES: usize = 4;
const MAX_EDITOR_LINES: usize = 8;
pub(crate) fn create_editor(
workspace: WeakEntity<Workspace>,
context_store: WeakEntity<ContextStore>,
@@ -623,7 +625,7 @@ impl MessageEditor {
.unwrap_or(false);
IconButton::new("follow-agent", IconName::Crosshair)
.disabled(is_model_selected)
.disabled(!is_model_selected)
.icon_size(IconSize::Small)
.icon_color(Color::Muted)
.toggle_state(following)
@@ -711,11 +713,11 @@ impl MessageEditor {
cx.listener(|this, _: &RejectAll, window, cx| this.handle_reject_all(window, cx)),
)
.capture_action(cx.listener(Self::paste))
.gap_2()
.p_2()
.bg(editor_bg_color)
.gap_2()
.border_t_1()
.border_color(cx.theme().colors().border)
.bg(editor_bg_color)
.child(
h_flex()
.justify_between()
@@ -908,6 +910,10 @@ impl MessageEditor {
.on_click({
let focus_handle = focus_handle.clone();
move |_event, window, cx| {
telemetry::event!(
"Agent Message Sent",
agent = "zed",
);
focus_handle.dispatch_action(
&Chat, window, cx,
);
@@ -1655,9 +1661,34 @@ impl Render for MessageEditor {
let line_height = TextSize::Small.rems(cx).to_pixels(window.rem_size()) * 1.5;
let in_pro_trial = matches!(
self.user_store.read(cx).current_plan(),
Some(proto::Plan::ZedProTrial)
);
let pro_user = matches!(
self.user_store.read(cx).current_plan(),
Some(proto::Plan::ZedPro)
);
let configured_providers: Vec<(IconName, SharedString)> =
LanguageModelRegistry::read_global(cx)
.providers()
.iter()
.filter(|provider| {
provider.is_authenticated(cx) && provider.id() != ZED_CLOUD_PROVIDER_ID
})
.map(|provider| (provider.icon(), provider.name().0.clone()))
.collect();
let has_existing_providers = configured_providers.len() > 0;
v_flex()
.size_full()
.bg(cx.theme().colors().panel_background)
.when(
has_existing_providers && !in_pro_trial && !pro_user,
|this| this.child(cx.new(ApiKeysWithProviders::new)),
)
.when(changed_buffers.len() > 0, |parent| {
parent.child(self.render_edits_bar(&changed_buffers, window, cx))
})

View File

@@ -2,6 +2,7 @@ mod agent_notification;
mod burn_mode_tooltip;
mod context_pill;
mod end_trial_upsell;
mod new_thread_button;
mod onboarding_modal;
pub mod preview;
mod upsell;
@@ -10,4 +11,5 @@ pub use agent_notification::*;
pub use burn_mode_tooltip::*;
pub use context_pill::*;
pub use end_trial_upsell::*;
pub use new_thread_button::*;
pub use onboarding_modal::*;

View File

@@ -0,0 +1,75 @@
use gpui::{ClickEvent, ElementId, IntoElement, ParentElement, Styled};
use ui::prelude::*;
#[derive(IntoElement)]
pub struct NewThreadButton {
id: ElementId,
label: SharedString,
icon: IconName,
keybinding: Option<ui::KeyBinding>,
on_click: Option<Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>>,
}
impl NewThreadButton {
pub fn new(id: impl Into<ElementId>, label: impl Into<SharedString>, icon: IconName) -> Self {
Self {
id: id.into(),
label: label.into(),
icon,
keybinding: None,
on_click: None,
}
}
pub fn keybinding(mut self, keybinding: Option<ui::KeyBinding>) -> Self {
self.keybinding = keybinding;
self
}
pub fn on_click<F>(mut self, handler: F) -> Self
where
F: Fn(&mut Window, &mut App) + 'static,
{
self.on_click = Some(Box::new(
move |_: &ClickEvent, window: &mut Window, cx: &mut App| handler(window, cx),
));
self
}
}
impl RenderOnce for NewThreadButton {
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
h_flex()
.id(self.id)
.w_full()
.py_1p5()
.px_2()
.gap_1()
.justify_between()
.rounded_md()
.border_1()
.border_color(cx.theme().colors().border.opacity(0.4))
.bg(cx.theme().colors().element_active.opacity(0.2))
.hover(|style| {
style
.bg(cx.theme().colors().element_hover)
.border_color(cx.theme().colors().border)
})
.child(
h_flex()
.gap_1p5()
.child(
Icon::new(self.icon)
.size(IconSize::XSmall)
.color(Color::Muted),
)
.child(Label::new(self.label).size(LabelSize::Small)),
)
.when_some(self.keybinding, |this, keybinding| {
this.child(keybinding.size(rems_from_px(10.)))
})
.when_some(self.on_click, |this, on_click| {
this.on_click(move |event, window, cx| on_click(event, window, cx))
})
}
}

View File

@@ -22,6 +22,7 @@ language_model.workspace = true
proto.workspace = true
serde.workspace = true
smallvec.workspace = true
telemetry.workspace = true
ui.workspace = true
workspace-hack.workspace = true
zed_actions.workspace = true

View File

@@ -0,0 +1,146 @@
use gpui::{Action, IntoElement, ParentElement, RenderOnce, point};
use language_model::{LanguageModelRegistry, ZED_CLOUD_PROVIDER_ID};
use ui::{Divider, List, prelude::*};
use crate::BulletItem;
pub struct ApiKeysWithProviders {
configured_providers: Vec<(IconName, SharedString)>,
}
impl ApiKeysWithProviders {
pub fn new(cx: &mut Context<Self>) -> Self {
cx.subscribe(
&LanguageModelRegistry::global(cx),
|this: &mut Self, _registry, event: &language_model::Event, cx| match event {
language_model::Event::ProviderStateChanged
| language_model::Event::AddedProvider(_)
| language_model::Event::RemovedProvider(_) => {
this.configured_providers = Self::compute_configured_providers(cx)
}
_ => {}
},
)
.detach();
Self {
configured_providers: Self::compute_configured_providers(cx),
}
}
fn compute_configured_providers(cx: &App) -> Vec<(IconName, SharedString)> {
LanguageModelRegistry::read_global(cx)
.providers()
.iter()
.filter(|provider| {
provider.is_authenticated(cx) && provider.id() != ZED_CLOUD_PROVIDER_ID
})
.map(|provider| (provider.icon(), provider.name().0.clone()))
.collect()
}
}
impl Render for ApiKeysWithProviders {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let configured_providers_list =
self.configured_providers
.iter()
.cloned()
.map(|(icon, name)| {
h_flex()
.gap_1p5()
.child(Icon::new(icon).size(IconSize::XSmall).color(Color::Muted))
.child(Label::new(name))
});
div()
.mx_2p5()
.p_1()
.pb_0()
.gap_2()
.rounded_t_lg()
.border_t_1()
.border_x_1()
.border_color(cx.theme().colors().border.opacity(0.5))
.bg(cx.theme().colors().background.alpha(0.5))
.shadow(vec![gpui::BoxShadow {
color: gpui::black().opacity(0.15),
offset: point(px(1.), px(-1.)),
blur_radius: px(3.),
spread_radius: px(0.),
}])
.child(
h_flex()
.px_2p5()
.py_1p5()
.gap_2()
.flex_wrap()
.rounded_t(px(5.))
.overflow_hidden()
.border_t_1()
.border_x_1()
.border_color(cx.theme().colors().border)
.bg(cx.theme().colors().panel_background)
.child(
h_flex()
.min_w_0()
.gap_2()
.child(
Icon::new(IconName::Info)
.size(IconSize::XSmall)
.color(Color::Muted)
)
.child(
div()
.w_full()
.child(
Label::new("Start now using API keys from your environment for the following providers:")
.color(Color::Muted)
)
)
)
.children(configured_providers_list)
)
}
}
#[derive(IntoElement)]
pub struct ApiKeysWithoutProviders;
impl ApiKeysWithoutProviders {
pub fn new() -> Self {
Self
}
}
impl RenderOnce for ApiKeysWithoutProviders {
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
v_flex()
.mt_2()
.gap_1()
.child(
h_flex()
.gap_2()
.child(
Label::new("API Keys")
.size(LabelSize::Small)
.color(Color::Muted)
.buffer_font(cx),
)
.child(Divider::horizontal()),
)
.child(List::new().child(BulletItem::new(
"Add your own keys to use AI without signing in.",
)))
.child(
Button::new("configure-providers", "Configure Providers")
.full_width()
.style(ButtonStyle::Outlined)
.on_click(move |_, window, cx| {
window.dispatch_action(
zed_actions::agent::OpenConfiguration.boxed_clone(),
cx,
);
}),
)
}
}

View File

@@ -24,7 +24,7 @@ impl ParentElement for AgentPanelOnboardingCard {
impl RenderOnce for AgentPanelOnboardingCard {
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
div()
.m_4()
.m_2p5()
.p(px(3.))
.elevation_2(cx)
.rounded_lg()
@@ -49,6 +49,7 @@ impl RenderOnce for AgentPanelOnboardingCard {
.right_0()
.w(px(400.))
.h(px(92.))
.rounded_md()
.child(
Vector::new(
VectorName::AiGrid,
@@ -61,11 +62,12 @@ impl RenderOnce for AgentPanelOnboardingCard {
.child(
div()
.absolute()
.top_0()
.right_0()
.top_0p5()
.right_0p5()
.w(px(660.))
.h(px(401.))
.overflow_hidden()
.rounded_md()
.bg(linear_gradient(
75.,
linear_color_stop(

View File

@@ -1,12 +1,11 @@
use std::sync::Arc;
use client::{Client, UserStore};
use gpui::{Action, ClickEvent, Entity, IntoElement, ParentElement};
use gpui::{Entity, IntoElement, ParentElement};
use language_model::{LanguageModelRegistry, ZED_CLOUD_PROVIDER_ID};
use ui::{Divider, List, prelude::*};
use zed_actions::agent::{OpenConfiguration, ToggleModelSelector};
use ui::prelude::*;
use crate::{AgentPanelOnboardingCard, BulletItem, ZedAiOnboarding};
use crate::{AgentPanelOnboardingCard, ApiKeysWithoutProviders, ZedAiOnboarding};
pub struct AgentPanelOnboarding {
user_store: Entity<UserStore>,
@@ -53,93 +52,34 @@ impl AgentPanelOnboarding {
.map(|provider| (provider.icon(), provider.name().0.clone()))
.collect()
}
fn configure_providers(&mut self, _: &ClickEvent, window: &mut Window, cx: &mut Context<Self>) {
window.dispatch_action(OpenConfiguration.boxed_clone(), cx);
cx.notify();
}
fn render_api_keys_section(&mut self, cx: &mut Context<Self>) -> impl IntoElement {
let has_existing_providers = self.configured_providers.len() > 0;
let configure_provider_label = if has_existing_providers {
"Configure Other Provider"
} else {
"Configure Providers"
};
let content = if has_existing_providers {
List::new()
.child(BulletItem::new(
"Or start now using API keys from your environment for the following providers:"
))
.child(
h_flex()
.px_5()
.gap_2()
.flex_wrap()
.children(self.configured_providers.iter().cloned().map(|(icon, name)|
h_flex()
.gap_1p5()
.child(Icon::new(icon).size(IconSize::Small).color(Color::Muted))
.child(Label::new(name))
))
)
.child(BulletItem::new(
"No need for any of the plans or even to sign in",
))
} else {
List::new()
.child(BulletItem::new(
"You can also use AI in Zed by bringing your own API keys",
))
.child(BulletItem::new(
"No need for any of the plans or even to sign in",
))
};
v_flex()
.mt_2()
.gap_1()
.child(
h_flex()
.gap_2()
.child(
Label::new("API Keys")
.size(LabelSize::Small)
.color(Color::Muted)
.buffer_font(cx),
)
.child(Divider::horizontal()),
)
.child(content)
.when(has_existing_providers, |this| {
this.child(
Button::new("pick-model", "Choose Model")
.full_width()
.style(ButtonStyle::Outlined)
.on_click(|_event, window, cx| {
window.dispatch_action(ToggleModelSelector.boxed_clone(), cx)
}),
)
})
.child(
Button::new("configure-providers", configure_provider_label)
.full_width()
.style(ButtonStyle::Outlined)
.on_click(cx.listener(Self::configure_providers)),
)
}
}
impl Render for AgentPanelOnboarding {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let enrolled_in_trial = matches!(
self.user_store.read(cx).current_plan(),
Some(proto::Plan::ZedProTrial)
);
AgentPanelOnboardingCard::new()
.child(ZedAiOnboarding::new(
self.client.clone(),
&self.user_store,
self.continue_with_zed_ai.clone(),
cx,
))
.child(self.render_api_keys_section(cx))
.child(
ZedAiOnboarding::new(
self.client.clone(),
&self.user_store,
self.continue_with_zed_ai.clone(),
cx,
)
.with_dismiss({
let callback = self.continue_with_zed_ai.clone();
move |window, cx| callback(window, cx)
}),
)
.map(|this| {
if enrolled_in_trial || self.configured_providers.len() >= 1 {
this
} else {
this.child(ApiKeysWithoutProviders::new())
}
})
}
}

View File

@@ -1,8 +1,10 @@
mod agent_api_keys_onboarding;
mod agent_panel_onboarding_card;
mod agent_panel_onboarding_content;
mod edit_prediction_onboarding_content;
mod young_account_banner;
pub use agent_api_keys_onboarding::{ApiKeysWithProviders, ApiKeysWithoutProviders};
pub use agent_panel_onboarding_card::AgentPanelOnboardingCard;
pub use agent_panel_onboarding_content::AgentPanelOnboarding;
pub use edit_prediction_onboarding_content::EditPredictionOnboarding;
@@ -12,7 +14,7 @@ use std::sync::Arc;
use client::{Client, UserStore, zed_urls};
use gpui::{AnyElement, Entity, IntoElement, ParentElement, SharedString};
use ui::{Divider, List, ListItem, RegisterComponent, TintColor, prelude::*};
use ui::{Divider, List, ListItem, RegisterComponent, TintColor, Tooltip, prelude::*};
pub struct BulletItem {
label: SharedString,
@@ -69,6 +71,7 @@ pub struct ZedAiOnboarding {
pub continue_with_zed_ai: Arc<dyn Fn(&mut Window, &mut App)>,
pub sign_in: Arc<dyn Fn(&mut Window, &mut App)>,
pub accept_terms_of_service: Arc<dyn Fn(&mut Window, &mut App)>,
pub dismiss_onboarding: Option<Arc<dyn Fn(&mut Window, &mut App)>>,
}
impl ZedAiOnboarding {
@@ -80,6 +83,7 @@ impl ZedAiOnboarding {
) -> Self {
let store = user_store.read(cx);
let status = *client.status().borrow();
Self {
sign_in_status: status.into(),
has_accepted_terms_of_service: store.current_user_has_accepted_terms().unwrap_or(false),
@@ -102,14 +106,22 @@ impl ZedAiOnboarding {
})
.detach();
}),
dismiss_onboarding: None,
}
}
fn render_free_plan_section(&self, cx: &mut App) -> impl IntoElement {
pub fn with_dismiss(
mut self,
dismiss_callback: impl Fn(&mut Window, &mut App) + 'static,
) -> Self {
self.dismiss_onboarding = Some(Arc::new(dismiss_callback));
self
}
fn free_plan_definition(&self, cx: &mut App) -> impl IntoElement {
v_flex()
.mt_2()
.gap_1()
.when(self.account_too_young, |this| this.opacity(0.4))
.child(
h_flex()
.gap_2()
@@ -119,151 +131,205 @@ impl ZedAiOnboarding {
.color(Color::Muted)
.buffer_font(cx),
)
.child(Divider::horizontal()),
)
.child(
List::new()
.child(BulletItem::new(
"50 prompts per month with the Claude models",
))
.child(BulletItem::new(
"2000 accepted edit predictions using our open-source Zeta model",
)),
)
.child(
Button::new("continue", "Continue Free")
.disabled(self.account_too_young)
.full_width()
.style(ButtonStyle::Outlined)
.on_click({
let callback = self.continue_with_zed_ai.clone();
move |_, window, cx| callback(window, cx)
}),
)
}
fn render_pro_plan_section(&self, cx: &mut App) -> impl IntoElement {
let (button_label, button_url) = if self.account_too_young {
("Start with Pro", zed_urls::upgrade_to_zed_pro_url(cx))
} else {
("Start Pro Trial", zed_urls::account_url(cx))
};
v_flex()
.mt_2()
.gap_1()
.child(
h_flex()
.gap_2()
.child(
Label::new("Pro")
Label::new("(Current Plan)")
.size(LabelSize::Small)
.color(Color::Accent)
.color(Color::Custom(cx.theme().colors().text_muted.opacity(0.6)))
.buffer_font(cx),
)
.child(Divider::horizontal()),
)
.child(
List::new()
.child(BulletItem::new("500 prompts per month with Claude models"))
.child(BulletItem::new("Unlimited edit predictions"))
.when(!self.account_too_young, |this| {
this.child(BulletItem::new(
"Try it out for 14 days with no charge, no credit card required",
))
}),
)
.child(
Button::new("pro", button_label)
.full_width()
.style(ButtonStyle::Tinted(ui::TintColor::Accent))
.on_click(move |_, _window, cx| cx.open_url(&button_url)),
.child(BulletItem::new("50 prompts per month with Claude models"))
.child(BulletItem::new(
"2,000 accepted edit predictions with Zeta, our open-source model",
)),
)
}
fn render_accept_terms_of_service(&self) -> Div {
v_flex()
.w_full()
.gap_1()
.child(Headline::new("Before starting…"))
.child(Label::new(
"Make sure you have read and accepted Zed AI's terms of service.",
fn pro_trial_definition(&self) -> impl IntoElement {
List::new()
.child(BulletItem::new("150 prompts with Claude models"))
.child(BulletItem::new(
"Unlimited accepted edit predictions with Zeta, our open-source model",
))
}
fn pro_plan_definition(&self, cx: &mut App) -> impl IntoElement {
v_flex().mt_2().gap_1().map(|this| {
if self.account_too_young {
this.child(
h_flex()
.gap_2()
.child(
Label::new("Pro")
.size(LabelSize::Small)
.color(Color::Accent)
.buffer_font(cx),
)
.child(Divider::horizontal()),
)
.child(
List::new()
.child(BulletItem::new("500 prompts per month with Claude models"))
.child(BulletItem::new(
"Unlimited accepted edit predictions with Zeta, our open-source model",
))
.child(BulletItem::new("$20 USD per month")),
)
.child(
Button::new("pro", "Get Started")
.full_width()
.style(ButtonStyle::Tinted(ui::TintColor::Accent))
.on_click(move |_, _window, cx| {
telemetry::event!("Upgrade To Pro Clicked", state = "young-account");
cx.open_url(&zed_urls::upgrade_to_zed_pro_url(cx))
}),
)
} else {
this.child(
h_flex()
.gap_2()
.child(
Label::new("Pro Trial")
.size(LabelSize::Small)
.color(Color::Accent)
.buffer_font(cx),
)
.child(Divider::horizontal()),
)
.child(
List::new()
.child(self.pro_trial_definition())
.child(BulletItem::new(
"Try it out for 14 days for free, no credit card required",
)),
)
.child(
Button::new("pro", "Start Free Trial")
.full_width()
.style(ButtonStyle::Tinted(ui::TintColor::Accent))
.on_click(move |_, _window, cx| {
telemetry::event!("Start Trial Clicked", state = "post-sign-in");
cx.open_url(&zed_urls::start_trial_url(cx))
}),
)
}
})
}
fn render_accept_terms_of_service(&self) -> AnyElement {
v_flex()
.gap_1()
.w_full()
.child(Headline::new("Accept Terms of Service"))
.child(
Button::new("terms_of_service", "View and Read the Terms of Service")
Label::new("We dont sell your data, track you across the web, or compromise your privacy.")
.color(Color::Muted)
.mb_2(),
)
.child(
Button::new("terms_of_service", "Review Terms of Service")
.full_width()
.style(ButtonStyle::Outlined)
.icon(IconName::ArrowUpRight)
.icon_color(Color::Muted)
.icon_size(IconSize::XSmall)
.on_click(move |_, _window, cx| {
cx.open_url("https://zed.dev/terms-of-service")
telemetry::event!("Review Terms of Service Clicked");
cx.open_url(&zed_urls::terms_of_service(cx))
}),
)
.child(
Button::new("accept_terms", "I've read it and accept it")
Button::new("accept_terms", "Accept")
.full_width()
.style(ButtonStyle::Tinted(TintColor::Accent))
.on_click({
let callback = self.accept_terms_of_service.clone();
move |_, window, cx| (callback)(window, cx)
move |_, window, cx| {
telemetry::event!("Terms of Service Accepted");
(callback)(window, cx)}
}),
)
.into_any_element()
}
fn render_sign_in_disclaimer(&self, _cx: &mut App) -> Div {
const SIGN_IN_DISCLAIMER: &str =
"To start using AI in Zed with our hosted models, sign in and subscribe to a plan.";
fn render_sign_in_disclaimer(&self, _cx: &mut App) -> AnyElement {
let signing_in = matches!(self.sign_in_status, SignInStatus::SigningIn);
v_flex()
.gap_2()
.gap_1()
.child(Headline::new("Welcome to Zed AI"))
.child(div().w_full().child(Label::new(SIGN_IN_DISCLAIMER)))
.child(
Button::new("sign_in", "Sign In with GitHub")
.icon(IconName::Github)
.icon_position(IconPosition::Start)
.icon_size(IconSize::Small)
.icon_color(Color::Muted)
Label::new("Sign in to try Zed Pro for 14 days, no credit card required.")
.color(Color::Muted)
.mb_2(),
)
.child(self.pro_trial_definition())
.child(
Button::new("sign_in", "Try Zed Pro for Free")
.disabled(signing_in)
.full_width()
.style(ButtonStyle::Tinted(ui::TintColor::Accent))
.on_click({
let callback = self.sign_in.clone();
move |_, window, cx| callback(window, cx)
move |_, window, cx| {
telemetry::event!("Start Trial Clicked", state = "pre-sign-in");
callback(window, cx)
}
}),
)
.into_any_element()
}
fn render_free_plan_onboarding(&self, cx: &mut App) -> Div {
const PLANS_DESCRIPTION: &str = "Choose how you want to start.";
fn render_free_plan_state(&self, cx: &mut App) -> AnyElement {
let young_account_banner = YoungAccountBanner;
v_flex()
.relative()
.gap_1()
.child(Headline::new("Welcome to Zed AI"))
.child(
Label::new(PLANS_DESCRIPTION)
.size(LabelSize::Small)
.color(Color::Muted)
.mt_1()
.mb_3(),
)
.when(self.account_too_young, |this| {
this.child(young_account_banner)
.map(|this| {
if self.account_too_young {
this.child(young_account_banner)
} else {
this.child(self.free_plan_definition(cx)).when_some(
self.dismiss_onboarding.as_ref(),
|this, dismiss_callback| {
let callback = dismiss_callback.clone();
this.child(
h_flex().absolute().top_0().right_0().child(
IconButton::new("dismiss_onboarding", IconName::Close)
.icon_size(IconSize::Small)
.tooltip(Tooltip::text("Dismiss"))
.on_click(move |_, window, cx| {
telemetry::event!(
"Banner Dismissed",
source = "AI Onboarding",
);
callback(window, cx)
}),
),
)
},
)
}
})
.child(self.render_free_plan_section(cx))
.child(self.render_pro_plan_section(cx))
.child(self.pro_plan_definition(cx))
.into_any_element()
}
fn render_trial_onboarding(&self, _cx: &mut App) -> Div {
fn render_trial_state(&self, _cx: &mut App) -> AnyElement {
v_flex()
.child(Headline::new("Welcome to the trial of Zed Pro"))
.relative()
.gap_1()
.child(Headline::new("Welcome to the Zed Pro Trial"))
.child(
Label::new("Here's what you get for the next 14 days:")
.size(LabelSize::Small)
.color(Color::Muted)
.mt_1(),
.mb_2(),
)
.child(
List::new()
@@ -272,25 +338,37 @@ impl ZedAiOnboarding {
"Unlimited edit predictions with Zeta, our open-source model",
)),
)
.child(
Button::new("trial", "Start Trial")
.full_width()
.style(ButtonStyle::Outlined)
.on_click({
let callback = self.continue_with_zed_ai.clone();
move |_, window, cx| callback(window, cx)
}),
.when_some(
self.dismiss_onboarding.as_ref(),
|this, dismiss_callback| {
let callback = dismiss_callback.clone();
this.child(
h_flex().absolute().top_0().right_0().child(
IconButton::new("dismiss_onboarding", IconName::Close)
.icon_size(IconSize::Small)
.tooltip(Tooltip::text("Dismiss"))
.on_click(move |_, window, cx| {
telemetry::event!(
"Banner Dismissed",
source = "AI Onboarding",
);
callback(window, cx)
}),
),
)
},
)
.into_any_element()
}
fn render_pro_plan_onboarding(&self, _cx: &mut App) -> Div {
fn render_pro_plan_state(&self, _cx: &mut App) -> AnyElement {
v_flex()
.gap_1()
.child(Headline::new("Welcome to Zed Pro"))
.child(
Label::new("Here's what you get:")
.size(LabelSize::Small)
.color(Color::Muted)
.mt_1(),
.mb_2(),
)
.child(
List::new()
@@ -303,9 +381,13 @@ impl ZedAiOnboarding {
.style(ButtonStyle::Outlined)
.on_click({
let callback = self.continue_with_zed_ai.clone();
move |_, window, cx| callback(window, cx)
move |_, window, cx| {
telemetry::event!("Banner Dismissed", source = "AI Onboarding");
callback(window, cx)
}
}),
)
.into_any_element()
}
}
@@ -314,9 +396,9 @@ impl RenderOnce for ZedAiOnboarding {
if matches!(self.sign_in_status, SignInStatus::SignedIn) {
if self.has_accepted_terms_of_service {
match self.plan {
None | Some(proto::Plan::Free) => self.render_free_plan_onboarding(cx),
Some(proto::Plan::ZedProTrial) => self.render_trial_onboarding(cx),
Some(proto::Plan::ZedPro) => self.render_pro_plan_onboarding(cx),
None | Some(proto::Plan::Free) => self.render_free_plan_state(cx),
Some(proto::Plan::ZedProTrial) => self.render_trial_state(cx),
Some(proto::Plan::ZedPro) => self.render_pro_plan_state(cx),
}
} else {
self.render_accept_terms_of_service()
@@ -347,6 +429,7 @@ impl Component for ZedAiOnboarding {
continue_with_zed_ai: Arc::new(|_, _| {}),
sign_in: Arc::new(|_, _| {}),
accept_terms_of_service: Arc::new(|_, _| {}),
dismiss_onboarding: None,
}
.into_any_element()
}

View File

@@ -6,7 +6,7 @@ pub struct YoungAccountBanner;
impl RenderOnce for YoungAccountBanner {
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
const YOUNG_ACCOUNT_DISCLAIMER: &str = "Given your GitHub account was created less than 30 days ago, we cannot put you in the Free plan or offer you a free trial of the Pro plan. We hope you'll understand, as this is unfortunately required to prevent abuse of our service. To continue, upgrade to Pro or use your own API keys for other providers.";
const YOUNG_ACCOUNT_DISCLAIMER: &str = "To prevent abuse of our service, we cannot offer plans to GitHub accounts created fewer than 30 days ago. To request an exception, reach out to billing-support@zed.dev.";
let label = div()
.w_full()

View File

@@ -1323,7 +1323,7 @@ fn setup_context_editor_with_fake_model(
) -> (Entity<AssistantContext>, Arc<FakeLanguageModel>) {
let registry = Arc::new(LanguageRegistry::test(cx.executor().clone()));
let fake_provider = Arc::new(FakeLanguageModelProvider);
let fake_provider = Arc::new(FakeLanguageModelProvider::default());
let fake_model = Arc::new(fake_provider.test_model());
cx.update(|cx| {

View File

@@ -51,23 +51,13 @@ impl ActionLog {
Some(self.tracked_buffers.get(buffer)?.snapshot.clone())
}
pub fn has_unnotified_user_edits(&self) -> bool {
self.tracked_buffers
.values()
.any(|tracked| tracked.has_unnotified_user_edits)
}
/// Return a unified diff patch with user edits made since last read or notification
pub fn unnotified_user_edits(&self, cx: &Context<Self>) -> Option<String> {
if !self.has_unnotified_user_edits() {
return None;
}
let unified_diff = self
let diffs = self
.tracked_buffers
.values()
.filter_map(|tracked| {
if !tracked.has_unnotified_user_edits {
if !tracked.may_have_unnotified_user_edits {
return None;
}
@@ -95,9 +85,13 @@ impl ActionLog {
Some(result)
})
.collect::<Vec<_>>()
.join("\n\n");
.collect::<Vec<_>>();
if diffs.is_empty() {
return None;
}
let unified_diff = diffs.join("\n\n");
Some(unified_diff)
}
@@ -106,7 +100,7 @@ impl ActionLog {
pub fn flush_unnotified_user_edits(&mut self, cx: &Context<Self>) -> Option<String> {
let patch = self.unnotified_user_edits(cx);
self.tracked_buffers.values_mut().for_each(|tracked| {
tracked.has_unnotified_user_edits = false;
tracked.may_have_unnotified_user_edits = false;
tracked.last_seen_base = tracked.diff_base.clone();
});
patch
@@ -185,7 +179,7 @@ impl ActionLog {
version: buffer.read(cx).version(),
diff,
diff_update: diff_update_tx,
has_unnotified_user_edits: false,
may_have_unnotified_user_edits: false,
_open_lsp_handle: open_lsp_handle,
_maintain_diff: cx.spawn({
let buffer = buffer.clone();
@@ -337,27 +331,34 @@ impl ActionLog {
let new_snapshot = buffer_snapshot.clone();
let unreviewed_edits = tracked_buffer.unreviewed_edits.clone();
let edits = diff_snapshots(&old_snapshot, &new_snapshot);
if let ChangeAuthor::User = author
&& !edits.is_empty()
{
tracked_buffer.has_unnotified_user_edits = true;
}
let mut has_user_changes = false;
async move {
if let ChangeAuthor::User = author {
apply_non_conflicting_edits(
has_user_changes = apply_non_conflicting_edits(
&unreviewed_edits,
edits,
&mut base_text,
new_snapshot.as_rope(),
);
}
(Arc::new(base_text.to_string()), base_text)
(Arc::new(base_text.to_string()), base_text, has_user_changes)
}
});
anyhow::Ok(rebase)
})??;
let (new_base_text, new_diff_base) = rebase.await;
let (new_base_text, new_diff_base, has_user_changes) = rebase.await;
this.update(cx, |this, _| {
let tracked_buffer = this
.tracked_buffers
.get_mut(buffer)
.context("buffer not tracked")
.unwrap();
tracked_buffer.may_have_unnotified_user_edits |= has_user_changes;
})?;
Self::update_diff(
this,
buffer,
@@ -829,11 +830,12 @@ fn apply_non_conflicting_edits(
edits: Vec<Edit<u32>>,
old_text: &mut Rope,
new_text: &Rope,
) {
) -> bool {
let mut old_edits = patch.edits().iter().cloned().peekable();
let mut new_edits = edits.into_iter().peekable();
let mut applied_delta = 0i32;
let mut rebased_delta = 0i32;
let mut has_made_changes = false;
while let Some(mut new_edit) = new_edits.next() {
let mut conflict = false;
@@ -883,8 +885,10 @@ fn apply_non_conflicting_edits(
&new_text.chunks_in_range(new_bytes).collect::<String>(),
);
applied_delta += new_edit.new_len() as i32 - new_edit.old_len() as i32;
has_made_changes = true;
}
}
has_made_changes
}
fn diff_snapshots(
@@ -958,7 +962,7 @@ struct TrackedBuffer {
diff: Entity<BufferDiff>,
snapshot: text::BufferSnapshot,
diff_update: mpsc::UnboundedSender<(ChangeAuthor, text::BufferSnapshot)>,
has_unnotified_user_edits: bool,
may_have_unnotified_user_edits: bool,
_open_lsp_handle: OpenLspBufferHandle,
_maintain_diff: Task<()>,
_subscription: Subscription,

View File

@@ -20,6 +20,7 @@ anyhow.workspace = true
assistant_tool.workspace = true
buffer_diff.workspace = true
chrono.workspace = true
client.workspace = true
collections.workspace = true
component.workspace = true
derive_more.workspace = true

View File

@@ -20,14 +20,13 @@ mod thinking_tool;
mod ui;
mod web_search_tool;
use std::sync::Arc;
use assistant_tool::ToolRegistry;
use copy_path_tool::CopyPathTool;
use gpui::{App, Entity};
use http_client::HttpClientWithUrl;
use language_model::LanguageModelRegistry;
use move_path_tool::MovePathTool;
use std::sync::Arc;
use web_search_tool::WebSearchTool;
pub(crate) use templates::*;

View File

@@ -278,6 +278,9 @@ impl Tool for EditFileTool {
.unwrap_or(false);
if format_on_save_enabled {
action_log.update(cx, |log, cx| {
log.buffer_edited(buffer.clone(), cx);
})?;
let format_task = project.update(cx, |project, cx| {
project.format(
HashSet::from_iter([buffer.clone()]),

View File

@@ -200,7 +200,7 @@ mod tests {
// Run the tool before any changes
let tool = Arc::new(ProjectNotificationsTool);
let provider = Arc::new(FakeLanguageModelProvider);
let provider = Arc::new(FakeLanguageModelProvider::default());
let model: Arc<dyn LanguageModel> = Arc::new(provider.test_model());
let request = Arc::new(LanguageModelRequest::default());
let tool_input = json!({});

View File

@@ -17,7 +17,5 @@ default = []
[dependencies]
aws-smithy-runtime-api.workspace = true
aws-smithy-types.workspace = true
futures.workspace = true
http_client.workspace = true
tokio = { workspace = true, features = ["rt", "rt-multi-thread"] }
workspace-hack.workspace = true

View File

@@ -11,14 +11,11 @@ use aws_smithy_runtime_api::client::result::ConnectorError;
use aws_smithy_runtime_api::client::runtime_components::RuntimeComponents;
use aws_smithy_runtime_api::http::{Headers, StatusCode};
use aws_smithy_types::body::SdkBody;
use futures::AsyncReadExt;
use http_client::{AsyncBody, Inner};
use http_client::AsyncBody;
use http_client::{HttpClient, Request};
use tokio::runtime::Handle;
struct AwsHttpConnector {
client: Arc<dyn HttpClient>,
handle: Handle,
}
impl std::fmt::Debug for AwsHttpConnector {
@@ -42,18 +39,17 @@ impl AwsConnector for AwsHttpConnector {
.client
.send(Request::from_parts(parts, convert_to_async_body(body)));
let handle = self.handle.clone();
HttpConnectorFuture::new(async move {
let response = match response.await {
Ok(response) => response,
Err(err) => return Err(ConnectorError::other(err.into(), None)),
};
let (parts, body) = response.into_parts();
let body = convert_to_sdk_body(body, handle).await;
let mut response =
HttpResponse::new(StatusCode::try_from(parts.status.as_u16()).unwrap(), body);
let mut response = HttpResponse::new(
StatusCode::try_from(parts.status.as_u16()).unwrap(),
convert_to_sdk_body(body),
);
let headers = match Headers::try_from(parts.headers) {
Ok(headers) => headers,
@@ -70,7 +66,6 @@ impl AwsConnector for AwsHttpConnector {
#[derive(Clone)]
pub struct AwsHttpClient {
client: Arc<dyn HttpClient>,
handler: Handle,
}
impl std::fmt::Debug for AwsHttpClient {
@@ -80,11 +75,8 @@ impl std::fmt::Debug for AwsHttpClient {
}
impl AwsHttpClient {
pub fn new(client: Arc<dyn HttpClient>, handle: Handle) -> Self {
Self {
client,
handler: handle,
}
pub fn new(client: Arc<dyn HttpClient>) -> Self {
Self { client }
}
}
@@ -96,25 +88,12 @@ impl AwsClient for AwsHttpClient {
) -> SharedHttpConnector {
SharedHttpConnector::new(AwsHttpConnector {
client: self.client.clone(),
handle: self.handler.clone(),
})
}
}
pub async fn convert_to_sdk_body(body: AsyncBody, handle: Handle) -> SdkBody {
match body.0 {
Inner::Empty => SdkBody::empty(),
Inner::Bytes(bytes) => SdkBody::from(bytes.into_inner()),
Inner::AsyncReader(mut reader) => {
let buffer = handle.spawn(async move {
let mut buffer = Vec::new();
let _ = reader.read_to_end(&mut buffer).await;
buffer
});
SdkBody::from(buffer.await.unwrap_or_default())
}
}
pub fn convert_to_sdk_body(body: AsyncBody) -> SdkBody {
SdkBody::from_body_1_x(body)
}
pub fn convert_to_async_body(body: SdkBody) -> AsyncBody {

View File

@@ -343,8 +343,7 @@ impl BufferDiffInner {
..
} in hunks.iter().cloned()
{
let preceding_pending_hunks =
old_pending_hunks.slice(&buffer_range.start, Bias::Left, buffer);
let preceding_pending_hunks = old_pending_hunks.slice(&buffer_range.start, Bias::Left);
pending_hunks.append(preceding_pending_hunks, buffer);
// Skip all overlapping or adjacent old pending hunks
@@ -355,7 +354,7 @@ impl BufferDiffInner {
.cmp(&buffer_range.end, buffer)
.is_le()
}) {
old_pending_hunks.next(buffer);
old_pending_hunks.next();
}
if (stage && secondary_status == DiffHunkSecondaryStatus::NoSecondaryHunk)
@@ -379,10 +378,10 @@ impl BufferDiffInner {
);
}
// append the remainder
pending_hunks.append(old_pending_hunks.suffix(buffer), buffer);
pending_hunks.append(old_pending_hunks.suffix(), buffer);
let mut unstaged_hunk_cursor = unstaged_diff.hunks.cursor::<DiffHunkSummary>(buffer);
unstaged_hunk_cursor.next(buffer);
unstaged_hunk_cursor.next();
// then, iterate over all pending hunks (both new ones and the existing ones) and compute the edits
let mut prev_unstaged_hunk_buffer_end = 0;
@@ -397,8 +396,7 @@ impl BufferDiffInner {
}) = pending_hunks_iter.next()
{
// Advance unstaged_hunk_cursor to skip unstaged hunks before current hunk
let skipped_unstaged =
unstaged_hunk_cursor.slice(&buffer_range.start, Bias::Left, buffer);
let skipped_unstaged = unstaged_hunk_cursor.slice(&buffer_range.start, Bias::Left);
if let Some(unstaged_hunk) = skipped_unstaged.last() {
prev_unstaged_hunk_base_text_end = unstaged_hunk.diff_base_byte_range.end;
@@ -425,7 +423,7 @@ impl BufferDiffInner {
buffer_offset_range.end =
buffer_offset_range.end.max(unstaged_hunk_offset_range.end);
unstaged_hunk_cursor.next(buffer);
unstaged_hunk_cursor.next();
continue;
}
}
@@ -514,7 +512,7 @@ impl BufferDiffInner {
});
let anchor_iter = iter::from_fn(move || {
cursor.next(buffer);
cursor.next();
cursor.item()
})
.flat_map(move |hunk| {
@@ -531,12 +529,12 @@ impl BufferDiffInner {
});
let mut pending_hunks_cursor = self.pending_hunks.cursor::<DiffHunkSummary>(buffer);
pending_hunks_cursor.next(buffer);
pending_hunks_cursor.next();
let mut secondary_cursor = None;
if let Some(secondary) = secondary.as_ref() {
let mut cursor = secondary.hunks.cursor::<DiffHunkSummary>(buffer);
cursor.next(buffer);
cursor.next();
secondary_cursor = Some(cursor);
}
@@ -564,7 +562,7 @@ impl BufferDiffInner {
.cmp(&pending_hunks_cursor.start().buffer_range.start, buffer)
.is_gt()
{
pending_hunks_cursor.seek_forward(&start_anchor, Bias::Left, buffer);
pending_hunks_cursor.seek_forward(&start_anchor, Bias::Left);
}
if let Some(pending_hunk) = pending_hunks_cursor.item() {
@@ -590,7 +588,7 @@ impl BufferDiffInner {
.cmp(&secondary_cursor.start().buffer_range.start, buffer)
.is_gt()
{
secondary_cursor.seek_forward(&start_anchor, Bias::Left, buffer);
secondary_cursor.seek_forward(&start_anchor, Bias::Left);
}
if let Some(secondary_hunk) = secondary_cursor.item() {
@@ -635,7 +633,7 @@ impl BufferDiffInner {
});
iter::from_fn(move || {
cursor.prev(buffer);
cursor.prev();
let hunk = cursor.item()?;
let range = hunk.buffer_range.to_point(buffer);
@@ -653,8 +651,8 @@ impl BufferDiffInner {
fn compare(&self, old: &Self, new_snapshot: &text::BufferSnapshot) -> Option<Range<Anchor>> {
let mut new_cursor = self.hunks.cursor::<()>(new_snapshot);
let mut old_cursor = old.hunks.cursor::<()>(new_snapshot);
old_cursor.next(new_snapshot);
new_cursor.next(new_snapshot);
old_cursor.next();
new_cursor.next();
let mut start = None;
let mut end = None;
@@ -669,7 +667,7 @@ impl BufferDiffInner {
Ordering::Less => {
start.get_or_insert(new_hunk.buffer_range.start);
end.replace(new_hunk.buffer_range.end);
new_cursor.next(new_snapshot);
new_cursor.next();
}
Ordering::Equal => {
if new_hunk != old_hunk {
@@ -686,25 +684,25 @@ impl BufferDiffInner {
}
}
new_cursor.next(new_snapshot);
old_cursor.next(new_snapshot);
new_cursor.next();
old_cursor.next();
}
Ordering::Greater => {
start.get_or_insert(old_hunk.buffer_range.start);
end.replace(old_hunk.buffer_range.end);
old_cursor.next(new_snapshot);
old_cursor.next();
}
}
}
(Some(new_hunk), None) => {
start.get_or_insert(new_hunk.buffer_range.start);
end.replace(new_hunk.buffer_range.end);
new_cursor.next(new_snapshot);
new_cursor.next();
}
(None, Some(old_hunk)) => {
start.get_or_insert(old_hunk.buffer_range.start);
end.replace(old_hunk.buffer_range.end);
old_cursor.next(new_snapshot);
old_cursor.next();
}
(None, None) => break,
}

View File

@@ -11,15 +11,18 @@ use client::{
use collections::{BTreeMap, HashMap, HashSet};
use fs::Fs;
use futures::{FutureExt, StreamExt};
use gpui::{App, AppContext as _, AsyncApp, Context, Entity, EventEmitter, Task, WeakEntity};
use gpui::{
App, AppContext as _, AsyncApp, Context, Entity, EventEmitter, ScreenCaptureSource,
ScreenCaptureStream, Task, WeakEntity,
};
use gpui_tokio::Tokio;
use language::LanguageRegistry;
use livekit::{LocalTrackPublication, ParticipantIdentity, RoomEvent};
use livekit_client::{self as livekit, TrackSid};
use livekit_client::{self as livekit, AudioStream, TrackSid};
use postage::{sink::Sink, stream::Stream, watch};
use project::Project;
use settings::Settings as _;
use std::{any::Any, future::Future, mem, rc::Rc, sync::Arc, time::Duration};
use std::{future::Future, mem, rc::Rc, sync::Arc, time::Duration};
use util::{ResultExt, TryFutureExt, post_inc};
pub const RECONNECT_TIMEOUT: Duration = Duration::from_secs(30);
@@ -1251,12 +1254,21 @@ impl Room {
})
}
pub fn is_screen_sharing(&self) -> bool {
pub fn is_sharing_screen(&self) -> bool {
self.live_kit.as_ref().map_or(false, |live_kit| {
!matches!(live_kit.screen_track, LocalTrack::None)
})
}
pub fn shared_screen_id(&self) -> Option<u64> {
self.live_kit.as_ref().and_then(|lk| match lk.screen_track {
LocalTrack::Published { ref _stream, .. } => {
_stream.metadata().ok().map(|meta| meta.id)
}
_ => None,
})
}
pub fn is_sharing_mic(&self) -> bool {
self.live_kit.as_ref().map_or(false, |live_kit| {
!matches!(live_kit.microphone_track, LocalTrack::None)
@@ -1369,11 +1381,15 @@ impl Room {
})
}
pub fn share_screen(&mut self, cx: &mut Context<Self>) -> Task<Result<()>> {
pub fn share_screen(
&mut self,
source: Rc<dyn ScreenCaptureSource>,
cx: &mut Context<Self>,
) -> Task<Result<()>> {
if self.status.is_offline() {
return Task::ready(Err(anyhow!("room is offline")));
}
if self.is_screen_sharing() {
if self.is_sharing_screen() {
return Task::ready(Err(anyhow!("screen was already shared")));
}
@@ -1386,20 +1402,8 @@ impl Room {
return Task::ready(Err(anyhow!("live-kit was not initialized")));
};
let sources = cx.screen_capture_sources();
cx.spawn(async move |this, cx| {
let sources = sources
.await
.map_err(|error| error.into())
.and_then(|sources| sources);
let source =
sources.and_then(|sources| sources.into_iter().next().context("no display found"));
let publication = match source {
Ok(source) => participant.publish_screenshare_track(&*source, cx).await,
Err(error) => Err(error),
};
let publication = participant.publish_screenshare_track(&*source, cx).await;
this.update(cx, |this, cx| {
let live_kit = this
@@ -1426,7 +1430,7 @@ impl Room {
} else {
live_kit.screen_track = LocalTrack::Published {
track_publication: publication,
_stream: Box::new(stream),
_stream: stream,
};
cx.notify();
}
@@ -1492,7 +1496,7 @@ impl Room {
}
}
pub fn unshare_screen(&mut self, cx: &mut Context<Self>) -> Result<()> {
pub fn unshare_screen(&mut self, play_sound: bool, cx: &mut Context<Self>) -> Result<()> {
anyhow::ensure!(!self.status.is_offline(), "room is offline");
let live_kit = self
@@ -1516,7 +1520,10 @@ impl Room {
cx.notify();
}
Audio::play_sound(Sound::StopScreenshare, cx);
if play_sound {
Audio::play_sound(Sound::StopScreenshare, cx);
}
Ok(())
}
}
@@ -1624,8 +1631,8 @@ fn spawn_room_connection(
struct LiveKitRoom {
room: Rc<livekit::Room>,
screen_track: LocalTrack,
microphone_track: LocalTrack,
screen_track: LocalTrack<dyn ScreenCaptureStream>,
microphone_track: LocalTrack<AudioStream>,
/// Tracks whether we're currently in a muted state due to auto-mute from deafening or manual mute performed by user.
muted_by_user: bool,
deafened: bool,
@@ -1663,18 +1670,18 @@ impl LiveKitRoom {
}
}
enum LocalTrack {
enum LocalTrack<Stream: ?Sized> {
None,
Pending {
publish_id: usize,
},
Published {
track_publication: LocalTrackPublication,
_stream: Box<dyn Any>,
_stream: Box<Stream>,
},
}
impl Default for LocalTrack {
impl<T: ?Sized> Default for LocalTrack<T> {
fn default() -> Self {
Self::None
}

View File

@@ -333,7 +333,7 @@ impl ChannelChat {
if first_id <= message_id {
let mut cursor = chat.messages.cursor::<(ChannelMessageId, Count)>(&());
let message_id = ChannelMessageId::Saved(message_id);
cursor.seek(&message_id, Bias::Left, &());
cursor.seek(&message_id, Bias::Left);
return ControlFlow::Break(
if cursor
.item()
@@ -499,7 +499,7 @@ impl ChannelChat {
pub fn message(&self, ix: usize) -> &ChannelMessage {
let mut cursor = self.messages.cursor::<Count>(&());
cursor.seek(&Count(ix), Bias::Right, &());
cursor.seek(&Count(ix), Bias::Right);
cursor.item().unwrap()
}
@@ -516,13 +516,13 @@ impl ChannelChat {
pub fn messages_in_range(&self, range: Range<usize>) -> impl Iterator<Item = &ChannelMessage> {
let mut cursor = self.messages.cursor::<Count>(&());
cursor.seek(&Count(range.start), Bias::Right, &());
cursor.seek(&Count(range.start), Bias::Right);
cursor.take(range.len())
}
pub fn pending_messages(&self) -> impl Iterator<Item = &ChannelMessage> {
let mut cursor = self.messages.cursor::<ChannelMessageId>(&());
cursor.seek(&ChannelMessageId::Pending(0), Bias::Left, &());
cursor.seek(&ChannelMessageId::Pending(0), Bias::Left);
cursor
}
@@ -588,9 +588,9 @@ impl ChannelChat {
.collect::<HashSet<_>>();
let mut old_cursor = self.messages.cursor::<(ChannelMessageId, Count)>(&());
let mut new_messages = old_cursor.slice(&first_message.id, Bias::Left, &());
let mut new_messages = old_cursor.slice(&first_message.id, Bias::Left);
let start_ix = old_cursor.start().1.0;
let removed_messages = old_cursor.slice(&last_message.id, Bias::Right, &());
let removed_messages = old_cursor.slice(&last_message.id, Bias::Right);
let removed_count = removed_messages.summary().count;
let new_count = messages.summary().count;
let end_ix = start_ix + removed_count;
@@ -599,10 +599,10 @@ impl ChannelChat {
let mut ranges = Vec::<Range<usize>>::new();
if new_messages.last().unwrap().is_pending() {
new_messages.append(old_cursor.suffix(&()), &());
new_messages.append(old_cursor.suffix(), &());
} else {
new_messages.append(
old_cursor.slice(&ChannelMessageId::Pending(0), Bias::Left, &()),
old_cursor.slice(&ChannelMessageId::Pending(0), Bias::Left),
&(),
);
@@ -617,7 +617,7 @@ impl ChannelChat {
} else {
new_messages.push(message.clone(), &());
}
old_cursor.next(&());
old_cursor.next();
}
}
@@ -641,12 +641,12 @@ impl ChannelChat {
fn message_removed(&mut self, id: u64, cx: &mut Context<Self>) {
let mut cursor = self.messages.cursor::<ChannelMessageId>(&());
let mut messages = cursor.slice(&ChannelMessageId::Saved(id), Bias::Left, &());
let mut messages = cursor.slice(&ChannelMessageId::Saved(id), Bias::Left);
if let Some(item) = cursor.item() {
if item.id == ChannelMessageId::Saved(id) {
let deleted_message_ix = messages.summary().count;
cursor.next(&());
messages.append(cursor.suffix(&()), &());
cursor.next();
messages.append(cursor.suffix(), &());
drop(cursor);
self.messages = messages;
@@ -680,7 +680,7 @@ impl ChannelChat {
cx: &mut Context<Self>,
) {
let mut cursor = self.messages.cursor::<ChannelMessageId>(&());
let mut messages = cursor.slice(&id, Bias::Left, &());
let mut messages = cursor.slice(&id, Bias::Left);
let ix = messages.summary().count;
if let Some(mut message_to_update) = cursor.item().cloned() {
@@ -688,10 +688,10 @@ impl ChannelChat {
message_to_update.mentions = mentions;
message_to_update.edited_at = edited_at;
messages.push(message_to_update, &());
cursor.next(&());
cursor.next();
}
messages.append(cursor.suffix(&()), &());
messages.append(cursor.suffix(), &());
drop(cursor);
self.messages = messages;

View File

@@ -151,6 +151,7 @@ impl Settings for ProxySettings {
pub fn init_settings(cx: &mut App) {
TelemetrySettings::register(cx);
DisableAiSettings::register(cx);
ClientSettings::register(cx);
ProxySettings::register(cx);
}
@@ -548,6 +549,33 @@ impl settings::Settings for TelemetrySettings {
}
}
/// Whether to disable all AI features in Zed.
///
/// Default: false
#[derive(Copy, Clone, Debug)]
pub struct DisableAiSettings {
pub disable_ai: bool,
}
impl settings::Settings for DisableAiSettings {
const KEY: Option<&'static str> = Some("disable_ai");
type FileContent = Option<bool>;
fn load(sources: SettingsSources<Self::FileContent>, _: &mut App) -> Result<Self> {
Ok(Self {
disable_ai: sources
.user
.or(sources.server)
.copied()
.flatten()
.unwrap_or(sources.default.ok_or_else(Self::missing_default)?),
})
}
fn import_from_vscode(_vscode: &settings::VsCodeSettings, _current: &mut Self::FileContent) {}
}
impl Client {
pub fn new(
clock: Arc<dyn SystemClock>,

View File

@@ -18,7 +18,20 @@ pub fn account_url(cx: &App) -> String {
format!("{server_url}/account", server_url = server_url(cx))
}
/// Returns the URL to the start trial page on zed.dev.
pub fn start_trial_url(cx: &App) -> String {
format!(
"{server_url}/account/start-trial",
server_url = server_url(cx)
)
}
/// Returns the URL to the upgrade page on zed.dev.
pub fn upgrade_to_zed_pro_url(cx: &App) -> String {
format!("{server_url}/account/upgrade", server_url = server_url(cx))
}
/// Returns the URL to Zed's terms of service.
pub fn terms_of_service(cx: &App) -> String {
format!("{server_url}/terms-of-service", server_url = server_url(cx))
}

View File

@@ -11,7 +11,9 @@ use crate::{
db::{User, UserId},
rpc,
};
use ::rpc::proto;
use anyhow::Context as _;
use axum::extract;
use axum::{
Extension, Json, Router,
body::Body,
@@ -23,6 +25,7 @@ use axum::{
routing::{get, post},
};
use axum_extra::response::ErasedJson;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::sync::{Arc, OnceLock};
use tower::ServiceBuilder;
@@ -101,6 +104,7 @@ pub fn routes(rpc_server: Arc<rpc::Server>) -> Router<(), Body> {
.route("/users/look_up", get(look_up_user))
.route("/users/:id/access_tokens", post(create_access_token))
.route("/users/:id/refresh_llm_tokens", post(refresh_llm_tokens))
.route("/users/:id/update_plan", post(update_plan))
.route("/rpc_server_snapshot", get(get_rpc_server_snapshot))
.merge(billing::router())
.merge(contributors::router())
@@ -347,3 +351,78 @@ async fn refresh_llm_tokens(
Ok(Json(RefreshLlmTokensResponse {}))
}
#[derive(Debug, Serialize, Deserialize)]
struct UpdatePlanBody {
pub plan: zed_llm_client::Plan,
pub subscription_period: SubscriptionPeriod,
pub usage: zed_llm_client::CurrentUsage,
pub trial_started_at: Option<DateTime<Utc>>,
pub is_usage_based_billing_enabled: bool,
pub is_account_too_young: bool,
pub has_overdue_invoices: bool,
}
#[derive(Debug, PartialEq, Clone, Copy, Serialize, Deserialize)]
struct SubscriptionPeriod {
pub started_at: DateTime<Utc>,
pub ended_at: DateTime<Utc>,
}
#[derive(Serialize)]
struct UpdatePlanResponse {}
async fn update_plan(
Path(user_id): Path<UserId>,
Extension(rpc_server): Extension<Arc<rpc::Server>>,
extract::Json(body): extract::Json<UpdatePlanBody>,
) -> Result<Json<UpdatePlanResponse>> {
let plan = match body.plan {
zed_llm_client::Plan::ZedFree => proto::Plan::Free,
zed_llm_client::Plan::ZedPro => proto::Plan::ZedPro,
zed_llm_client::Plan::ZedProTrial => proto::Plan::ZedProTrial,
};
let update_user_plan = proto::UpdateUserPlan {
plan: plan.into(),
trial_started_at: body
.trial_started_at
.map(|trial_started_at| trial_started_at.timestamp() as u64),
is_usage_based_billing_enabled: Some(body.is_usage_based_billing_enabled),
usage: Some(proto::SubscriptionUsage {
model_requests_usage_amount: body.usage.model_requests.used,
model_requests_usage_limit: Some(usage_limit_to_proto(body.usage.model_requests.limit)),
edit_predictions_usage_amount: body.usage.edit_predictions.used,
edit_predictions_usage_limit: Some(usage_limit_to_proto(
body.usage.edit_predictions.limit,
)),
}),
subscription_period: Some(proto::SubscriptionPeriod {
started_at: body.subscription_period.started_at.timestamp() as u64,
ended_at: body.subscription_period.ended_at.timestamp() as u64,
}),
account_too_young: Some(body.is_account_too_young),
has_overdue_invoices: Some(body.has_overdue_invoices),
};
rpc_server
.update_plan_for_user(user_id, update_user_plan)
.await?;
Ok(Json(UpdatePlanResponse {}))
}
fn usage_limit_to_proto(limit: zed_llm_client::UsageLimit) -> proto::UsageLimit {
proto::UsageLimit {
variant: Some(match limit {
zed_llm_client::UsageLimit::Limited(limit) => {
proto::usage_limit::Variant::Limited(proto::usage_limit::Limited {
limit: limit as u32,
})
}
zed_llm_client::UsageLimit::Unlimited => {
proto::usage_limit::Variant::Unlimited(proto::usage_limit::Unlimited {})
}
}),
}
}

View File

@@ -1,26 +1,15 @@
use anyhow::{Context as _, bail};
use axum::routing::put;
use axum::{Extension, Json, Router, extract, routing::post};
use chrono::{DateTime, SecondsFormat, Utc};
use chrono::{DateTime, Utc};
use collections::{HashMap, HashSet};
use reqwest::StatusCode;
use sea_orm::ActiveValue;
use serde::{Deserialize, Serialize};
use serde_json::json;
use std::{str::FromStr, sync::Arc, time::Duration};
use stripe::{
BillingPortalSession, CancellationDetailsReason, CreateBillingPortalSession,
CreateBillingPortalSessionFlowData, CreateBillingPortalSessionFlowDataAfterCompletion,
CreateBillingPortalSessionFlowDataAfterCompletionRedirect,
CreateBillingPortalSessionFlowDataSubscriptionUpdateConfirm,
CreateBillingPortalSessionFlowDataSubscriptionUpdateConfirmItems,
CreateBillingPortalSessionFlowDataType, CustomerId, EventObject, EventType, ListEvents,
PaymentMethod, Subscription, SubscriptionId, SubscriptionStatus,
};
use std::{sync::Arc, time::Duration};
use stripe::{CancellationDetailsReason, EventObject, EventType, ListEvents, SubscriptionStatus};
use util::{ResultExt, maybe};
use zed_llm_client::LanguageModelProvider;
use crate::api::events::SnowflakeRow;
use crate::db::billing_subscription::{
StripeCancellationReason, StripeSubscriptionStatus, SubscriptionKind,
};
@@ -28,494 +17,24 @@ use crate::llm::db::subscription_usage_meter::{self, CompletionMode};
use crate::rpc::{ResultExt as _, Server};
use crate::stripe_client::{
StripeCancellationDetailsReason, StripeClient, StripeCustomerId, StripeSubscription,
StripeSubscriptionId, UpdateCustomerParams,
StripeSubscriptionId,
};
use crate::{AppState, Error, Result};
use crate::{db::UserId, llm::db::LlmDatabase};
use crate::{
db::{
BillingSubscriptionId, CreateBillingCustomerParams, CreateBillingSubscriptionParams,
CreateBillingCustomerParams, CreateBillingSubscriptionParams,
CreateProcessedStripeEventParams, UpdateBillingCustomerParams,
UpdateBillingPreferencesParams, UpdateBillingSubscriptionParams, billing_customer,
UpdateBillingSubscriptionParams, billing_customer,
},
stripe_billing::StripeBilling,
};
pub fn router() -> Router {
Router::new()
.route("/billing/preferences", put(update_billing_preferences))
.route("/billing/subscriptions", post(create_billing_subscription))
.route(
"/billing/subscriptions/manage",
post(manage_billing_subscription),
)
.route(
"/billing/subscriptions/sync",
post(sync_billing_subscription),
)
}
#[derive(Debug, Serialize)]
struct BillingPreferencesResponse {
trial_started_at: Option<String>,
max_monthly_llm_usage_spending_in_cents: i32,
model_request_overages_enabled: bool,
model_request_overages_spend_limit_in_cents: i32,
}
#[derive(Debug, Deserialize)]
struct UpdateBillingPreferencesBody {
github_user_id: i32,
#[serde(default)]
max_monthly_llm_usage_spending_in_cents: i32,
#[serde(default)]
model_request_overages_enabled: bool,
#[serde(default)]
model_request_overages_spend_limit_in_cents: i32,
}
async fn update_billing_preferences(
Extension(app): Extension<Arc<AppState>>,
Extension(rpc_server): Extension<Arc<crate::rpc::Server>>,
extract::Json(body): extract::Json<UpdateBillingPreferencesBody>,
) -> Result<Json<BillingPreferencesResponse>> {
let user = app
.db
.get_user_by_github_user_id(body.github_user_id)
.await?
.context("user not found")?;
let billing_customer = app.db.get_billing_customer_by_user_id(user.id).await?;
let max_monthly_llm_usage_spending_in_cents =
body.max_monthly_llm_usage_spending_in_cents.max(0);
let model_request_overages_spend_limit_in_cents =
body.model_request_overages_spend_limit_in_cents.max(0);
let billing_preferences =
if let Some(_billing_preferences) = app.db.get_billing_preferences(user.id).await? {
app.db
.update_billing_preferences(
user.id,
&UpdateBillingPreferencesParams {
max_monthly_llm_usage_spending_in_cents: ActiveValue::set(
max_monthly_llm_usage_spending_in_cents,
),
model_request_overages_enabled: ActiveValue::set(
body.model_request_overages_enabled,
),
model_request_overages_spend_limit_in_cents: ActiveValue::set(
model_request_overages_spend_limit_in_cents,
),
},
)
.await?
} else {
app.db
.create_billing_preferences(
user.id,
&crate::db::CreateBillingPreferencesParams {
max_monthly_llm_usage_spending_in_cents,
model_request_overages_enabled: body.model_request_overages_enabled,
model_request_overages_spend_limit_in_cents,
},
)
.await?
};
SnowflakeRow::new(
"Billing Preferences Updated",
Some(user.metrics_id),
user.admin,
None,
json!({
"user_id": user.id,
"model_request_overages_enabled": billing_preferences.model_request_overages_enabled,
"model_request_overages_spend_limit_in_cents": billing_preferences.model_request_overages_spend_limit_in_cents,
"max_monthly_llm_usage_spending_in_cents": billing_preferences.max_monthly_llm_usage_spending_in_cents,
}),
Router::new().route(
"/billing/subscriptions/sync",
post(sync_billing_subscription),
)
.write(&app.kinesis_client, &app.config.kinesis_stream)
.await
.log_err();
rpc_server.refresh_llm_tokens_for_user(user.id).await;
Ok(Json(BillingPreferencesResponse {
trial_started_at: billing_customer
.and_then(|billing_customer| billing_customer.trial_started_at)
.map(|trial_started_at| {
trial_started_at
.and_utc()
.to_rfc3339_opts(SecondsFormat::Millis, true)
}),
max_monthly_llm_usage_spending_in_cents: billing_preferences
.max_monthly_llm_usage_spending_in_cents,
model_request_overages_enabled: billing_preferences.model_request_overages_enabled,
model_request_overages_spend_limit_in_cents: billing_preferences
.model_request_overages_spend_limit_in_cents,
}))
}
#[derive(Debug, PartialEq, Clone, Copy, Deserialize)]
#[serde(rename_all = "snake_case")]
enum ProductCode {
ZedPro,
ZedProTrial,
}
#[derive(Debug, Deserialize)]
struct CreateBillingSubscriptionBody {
github_user_id: i32,
product: ProductCode,
}
#[derive(Debug, Serialize)]
struct CreateBillingSubscriptionResponse {
checkout_session_url: String,
}
/// Initiates a Stripe Checkout session for creating a billing subscription.
async fn create_billing_subscription(
Extension(app): Extension<Arc<AppState>>,
extract::Json(body): extract::Json<CreateBillingSubscriptionBody>,
) -> Result<Json<CreateBillingSubscriptionResponse>> {
let user = app
.db
.get_user_by_github_user_id(body.github_user_id)
.await?
.context("user not found")?;
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(),
))?
};
if let Some(existing_subscription) = app.db.get_active_billing_subscription(user.id).await? {
let is_checkout_allowed = body.product == ProductCode::ZedProTrial
&& existing_subscription.kind == Some(SubscriptionKind::ZedFree);
if !is_checkout_allowed {
return Err(Error::http(
StatusCode::CONFLICT,
"user already has an active subscription".into(),
));
}
}
let existing_billing_customer = app.db.get_billing_customer_by_user_id(user.id).await?;
if let Some(existing_billing_customer) = &existing_billing_customer {
if existing_billing_customer.has_overdue_invoices {
return Err(Error::http(
StatusCode::PAYMENT_REQUIRED,
"user has overdue invoices".into(),
));
}
}
let customer_id = if let Some(existing_customer) = &existing_billing_customer {
let customer_id = StripeCustomerId(existing_customer.stripe_customer_id.clone().into());
if let Some(email) = user.email_address.as_deref() {
stripe_billing
.client()
.update_customer(&customer_id, UpdateCustomerParams { email: Some(email) })
.await
// Update of email address is best-effort - continue checkout even if it fails
.context("error updating stripe customer email address")
.log_err();
}
customer_id
} else {
stripe_billing
.find_or_create_customer_by_email(user.email_address.as_deref())
.await?
};
let success_url = format!(
"{}/account?checkout_complete=1",
app.config.zed_dot_dev_url()
);
let checkout_session_url = match body.product {
ProductCode::ZedPro => {
stripe_billing
.checkout_with_zed_pro(&customer_id, &user.github_login, &success_url)
.await?
}
ProductCode::ZedProTrial => {
if let Some(existing_billing_customer) = &existing_billing_customer {
if existing_billing_customer.trial_started_at.is_some() {
return Err(Error::http(
StatusCode::FORBIDDEN,
"user already used free trial".into(),
));
}
}
let feature_flags = app.db.get_user_flags(user.id).await?;
stripe_billing
.checkout_with_zed_pro_trial(
&customer_id,
&user.github_login,
feature_flags,
&success_url,
)
.await?
}
};
Ok(Json(CreateBillingSubscriptionResponse {
checkout_session_url,
}))
}
#[derive(Debug, PartialEq, Deserialize)]
#[serde(rename_all = "snake_case")]
enum ManageSubscriptionIntent {
/// The user intends to manage their subscription.
///
/// This will open the Stripe billing portal without putting the user in a specific flow.
ManageSubscription,
/// The user intends to update their payment method.
UpdatePaymentMethod,
/// The user intends to upgrade to Zed Pro.
UpgradeToPro,
/// The user intends to cancel their subscription.
Cancel,
/// The user intends to stop the cancellation of their subscription.
StopCancellation,
}
#[derive(Debug, Deserialize)]
struct ManageBillingSubscriptionBody {
github_user_id: i32,
intent: ManageSubscriptionIntent,
/// The ID of the subscription to manage.
subscription_id: BillingSubscriptionId,
redirect_to: Option<String>,
}
#[derive(Debug, Serialize)]
struct ManageBillingSubscriptionResponse {
billing_portal_session_url: Option<String>,
}
/// Initiates a Stripe customer portal session for managing a billing subscription.
async fn manage_billing_subscription(
Extension(app): Extension<Arc<AppState>>,
extract::Json(body): extract::Json<ManageBillingSubscriptionBody>,
) -> Result<Json<ManageBillingSubscriptionResponse>> {
let user = app
.db
.get_user_by_github_user_id(body.github_user_id)
.await?
.context("user not found")?;
let Some(stripe_client) = app.real_stripe_client.clone() else {
log::error!("failed to retrieve Stripe client");
Err(Error::http(
StatusCode::NOT_IMPLEMENTED,
"not supported".into(),
))?
};
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)
.await?
.context("billing customer not found")?;
let customer_id = CustomerId::from_str(&customer.stripe_customer_id)
.context("failed to parse customer ID")?;
let subscription = app
.db
.get_billing_subscription_by_id(body.subscription_id)
.await?
.context("subscription not found")?;
let subscription_id = SubscriptionId::from_str(&subscription.stripe_subscription_id)
.context("failed to parse subscription ID")?;
if body.intent == ManageSubscriptionIntent::StopCancellation {
let updated_stripe_subscription = Subscription::update(
&stripe_client,
&subscription_id,
stripe::UpdateSubscription {
cancel_at_period_end: Some(false),
..Default::default()
},
)
.await?;
app.db
.update_billing_subscription(
subscription.id,
&UpdateBillingSubscriptionParams {
stripe_cancel_at: ActiveValue::set(
updated_stripe_subscription
.cancel_at
.and_then(|cancel_at| DateTime::from_timestamp(cancel_at, 0))
.map(|time| time.naive_utc()),
),
..Default::default()
},
)
.await?;
return Ok(Json(ManageBillingSubscriptionResponse {
billing_portal_session_url: None,
}));
}
let flow = match body.intent {
ManageSubscriptionIntent::ManageSubscription => None,
ManageSubscriptionIntent::UpgradeToPro => {
let zed_pro_price_id: stripe::PriceId =
stripe_billing.zed_pro_price_id().await?.try_into()?;
let zed_free_price_id: stripe::PriceId =
stripe_billing.zed_free_price_id().await?.try_into()?;
let stripe_subscription =
Subscription::retrieve(&stripe_client, &subscription_id, &[]).await?;
let is_on_zed_pro_trial = stripe_subscription.status == SubscriptionStatus::Trialing
&& stripe_subscription.items.data.iter().any(|item| {
item.price
.as_ref()
.map_or(false, |price| price.id == zed_pro_price_id)
});
if is_on_zed_pro_trial {
let payment_methods = PaymentMethod::list(
&stripe_client,
&stripe::ListPaymentMethods {
customer: Some(stripe_subscription.customer.id()),
..Default::default()
},
)
.await?;
let has_payment_method = !payment_methods.data.is_empty();
if !has_payment_method {
return Err(Error::http(
StatusCode::BAD_REQUEST,
"missing payment method".into(),
));
}
// If the user is already on a Zed Pro trial and wants to upgrade to Pro, we just need to end their trial early.
Subscription::update(
&stripe_client,
&stripe_subscription.id,
stripe::UpdateSubscription {
trial_end: Some(stripe::Scheduled::now()),
..Default::default()
},
)
.await?;
return Ok(Json(ManageBillingSubscriptionResponse {
billing_portal_session_url: None,
}));
}
let subscription_item_to_update = stripe_subscription
.items
.data
.iter()
.find_map(|item| {
let price = item.price.as_ref()?;
if price.id == zed_free_price_id {
Some(item.id.clone())
} else {
None
}
})
.context("No subscription item to update")?;
Some(CreateBillingPortalSessionFlowData {
type_: CreateBillingPortalSessionFlowDataType::SubscriptionUpdateConfirm,
subscription_update_confirm: Some(
CreateBillingPortalSessionFlowDataSubscriptionUpdateConfirm {
subscription: subscription.stripe_subscription_id,
items: vec![
CreateBillingPortalSessionFlowDataSubscriptionUpdateConfirmItems {
id: subscription_item_to_update.to_string(),
price: Some(zed_pro_price_id.to_string()),
quantity: Some(1),
},
],
discounts: None,
},
),
..Default::default()
})
}
ManageSubscriptionIntent::UpdatePaymentMethod => Some(CreateBillingPortalSessionFlowData {
type_: CreateBillingPortalSessionFlowDataType::PaymentMethodUpdate,
after_completion: Some(CreateBillingPortalSessionFlowDataAfterCompletion {
type_: stripe::CreateBillingPortalSessionFlowDataAfterCompletionType::Redirect,
redirect: Some(CreateBillingPortalSessionFlowDataAfterCompletionRedirect {
return_url: format!(
"{}{path}",
app.config.zed_dot_dev_url(),
path = body.redirect_to.unwrap_or_else(|| "/account".to_string())
),
}),
..Default::default()
}),
..Default::default()
}),
ManageSubscriptionIntent::Cancel => {
if subscription.kind == Some(SubscriptionKind::ZedFree) {
return Err(Error::http(
StatusCode::BAD_REQUEST,
"free subscription cannot be canceled".into(),
));
}
Some(CreateBillingPortalSessionFlowData {
type_: CreateBillingPortalSessionFlowDataType::SubscriptionCancel,
after_completion: Some(CreateBillingPortalSessionFlowDataAfterCompletion {
type_: stripe::CreateBillingPortalSessionFlowDataAfterCompletionType::Redirect,
redirect: Some(CreateBillingPortalSessionFlowDataAfterCompletionRedirect {
return_url: format!("{}/account", app.config.zed_dot_dev_url()),
}),
..Default::default()
}),
subscription_cancel: Some(
stripe::CreateBillingPortalSessionFlowDataSubscriptionCancel {
subscription: subscription.stripe_subscription_id,
retention: None,
},
),
..Default::default()
})
}
ManageSubscriptionIntent::StopCancellation => unreachable!(),
};
let mut params = CreateBillingPortalSession::new(customer_id);
params.flow_data = flow;
let return_url = format!("{}/account", app.config.zed_dot_dev_url());
params.return_url = Some(&return_url);
let session = BillingPortalSession::create(&stripe_client, params).await?;
Ok(Json(ManageBillingSubscriptionResponse {
billing_portal_session_url: Some(session.url),
}))
}
#[derive(Debug, Deserialize)]
@@ -1008,7 +527,7 @@ async fn handle_customer_subscription_event(
// When the user's subscription changes, push down any changes to their plan.
rpc_server
.update_plan_for_user(billing_customer.user_id)
.update_plan_for_user_legacy(billing_customer.user_id)
.await
.trace_err();

View File

@@ -42,9 +42,6 @@ pub use tests::TestDb;
pub use ids::*;
pub use queries::billing_customers::{CreateBillingCustomerParams, UpdateBillingCustomerParams};
pub use queries::billing_preferences::{
CreateBillingPreferencesParams, UpdateBillingPreferencesParams,
};
pub use queries::billing_subscriptions::{
CreateBillingSubscriptionParams, UpdateBillingSubscriptionParams,
};

View File

@@ -1,21 +1,5 @@
use anyhow::Context as _;
use super::*;
#[derive(Debug)]
pub struct CreateBillingPreferencesParams {
pub max_monthly_llm_usage_spending_in_cents: i32,
pub model_request_overages_enabled: bool,
pub model_request_overages_spend_limit_in_cents: i32,
}
#[derive(Debug, Default)]
pub struct UpdateBillingPreferencesParams {
pub max_monthly_llm_usage_spending_in_cents: ActiveValue<i32>,
pub model_request_overages_enabled: ActiveValue<bool>,
pub model_request_overages_spend_limit_in_cents: ActiveValue<i32>,
}
impl Database {
/// Returns the billing preferences for the given user, if they exist.
pub async fn get_billing_preferences(
@@ -30,62 +14,4 @@ impl Database {
})
.await
}
/// Creates new billing preferences for the given user.
pub async fn create_billing_preferences(
&self,
user_id: UserId,
params: &CreateBillingPreferencesParams,
) -> Result<billing_preference::Model> {
self.transaction(|tx| async move {
let preferences = billing_preference::Entity::insert(billing_preference::ActiveModel {
user_id: ActiveValue::set(user_id),
max_monthly_llm_usage_spending_in_cents: ActiveValue::set(
params.max_monthly_llm_usage_spending_in_cents,
),
model_request_overages_enabled: ActiveValue::set(
params.model_request_overages_enabled,
),
model_request_overages_spend_limit_in_cents: ActiveValue::set(
params.model_request_overages_spend_limit_in_cents,
),
..Default::default()
})
.exec_with_returning(&*tx)
.await?;
Ok(preferences)
})
.await
}
/// Updates the billing preferences for the given user.
pub async fn update_billing_preferences(
&self,
user_id: UserId,
params: &UpdateBillingPreferencesParams,
) -> Result<billing_preference::Model> {
self.transaction(|tx| async move {
let preferences = billing_preference::Entity::update_many()
.set(billing_preference::ActiveModel {
max_monthly_llm_usage_spending_in_cents: params
.max_monthly_llm_usage_spending_in_cents
.clone(),
model_request_overages_enabled: params.model_request_overages_enabled.clone(),
model_request_overages_spend_limit_in_cents: params
.model_request_overages_spend_limit_in_cents
.clone(),
..Default::default()
})
.filter(billing_preference::Column::UserId.eq(user_id))
.exec_with_returning(&*tx)
.await?;
Ok(preferences
.into_iter()
.next()
.context("billing preferences not found")?)
})
.await
}
}

View File

@@ -1002,7 +1002,26 @@ impl Server {
Ok(())
}
pub async fn update_plan_for_user(self: &Arc<Self>, user_id: UserId) -> Result<()> {
pub async fn update_plan_for_user(
self: &Arc<Self>,
user_id: UserId,
update_user_plan: proto::UpdateUserPlan,
) -> Result<()> {
let pool = self.connection_pool.lock();
for connection_id in pool.user_connection_ids(user_id) {
self.peer
.send(connection_id, update_user_plan.clone())
.trace_err();
}
Ok(())
}
/// This is the legacy way of updating the user's plan, where we fetch the data to construct the `UpdateUserPlan`
/// message on the Collab server.
///
/// The new way is to receive the data from Cloud via the `POST /users/:id/update_plan` endpoint.
pub async fn update_plan_for_user_legacy(self: &Arc<Self>, user_id: UserId) -> Result<()> {
let user = self
.app_state
.db
@@ -1018,14 +1037,7 @@ impl Server {
)
.await?;
let pool = self.connection_pool.lock();
for connection_id in pool.user_connection_ids(user_id) {
self.peer
.send(connection_id, update_user_plan.clone())
.trace_err();
}
Ok(())
self.update_plan_for_user(user_id, update_user_plan).await
}
pub async fn refresh_llm_tokens_for_user(self: &Arc<Self>, user_id: UserId) {
@@ -4167,6 +4179,13 @@ async fn accept_terms_of_service(
response.send(proto::AcceptTermsOfServiceResponse {
accepted_tos_at: accepted_tos_at.timestamp() as u64,
})?;
// When the user accepts the terms of service, we want to refresh their LLM
// token to grant access.
session
.peer
.send(session.connection_id, proto::RefreshLlmToken {})?;
Ok(())
}

View File

@@ -1,6 +1,6 @@
use std::sync::Arc;
use anyhow::{Context as _, anyhow};
use anyhow::anyhow;
use chrono::Utc;
use collections::HashMap;
use stripe::SubscriptionStatus;
@@ -9,18 +9,13 @@ use uuid::Uuid;
use crate::Result;
use crate::db::billing_subscription::SubscriptionKind;
use crate::llm::AGENT_EXTENDED_TRIAL_FEATURE_FLAG;
use crate::stripe_client::{
RealStripeClient, StripeAutomaticTax, StripeBillingAddressCollection,
StripeCheckoutSessionMode, StripeCheckoutSessionPaymentMethodCollection, StripeClient,
StripeCreateCheckoutSessionLineItems, StripeCreateCheckoutSessionParams,
StripeCreateCheckoutSessionSubscriptionData, StripeCreateMeterEventParams,
RealStripeClient, StripeAutomaticTax, StripeClient, StripeCreateMeterEventParams,
StripeCreateMeterEventPayload, StripeCreateSubscriptionItems, StripeCreateSubscriptionParams,
StripeCustomerId, StripeCustomerUpdate, StripeCustomerUpdateAddress, StripeCustomerUpdateName,
StripeMeter, StripePrice, StripePriceId, StripeSubscription, StripeSubscriptionId,
StripeCustomerId, StripePrice, StripePriceId, StripeSubscription, StripeSubscriptionId,
StripeSubscriptionTrialSettings, StripeSubscriptionTrialSettingsEndBehavior,
StripeSubscriptionTrialSettingsEndBehaviorMissingPaymentMethod, StripeTaxIdCollection,
UpdateSubscriptionItems, UpdateSubscriptionParams,
StripeSubscriptionTrialSettingsEndBehaviorMissingPaymentMethod, UpdateSubscriptionItems,
UpdateSubscriptionParams,
};
pub struct StripeBilling {
@@ -30,8 +25,6 @@ pub struct StripeBilling {
#[derive(Default)]
struct StripeBillingState {
meters_by_event_name: HashMap<String, StripeMeter>,
price_ids_by_meter_id: HashMap<String, StripePriceId>,
prices_by_lookup_key: HashMap<String, StripePrice>,
}
@@ -60,24 +53,11 @@ impl StripeBilling {
let mut state = self.state.write().await;
let (meters, prices) =
futures::try_join!(self.client.list_meters(), self.client.list_prices())?;
for meter in meters {
state
.meters_by_event_name
.insert(meter.event_name.clone(), meter);
}
let prices = self.client.list_prices().await?;
for price in prices {
if let Some(lookup_key) = price.lookup_key.clone() {
state.prices_by_lookup_key.insert(lookup_key, price.clone());
}
if let Some(recurring) = price.recurring {
if let Some(meter) = recurring.meter {
state.price_ids_by_meter_id.insert(meter, price.id);
}
state.prices_by_lookup_key.insert(lookup_key, price);
}
}
@@ -229,95 +209,6 @@ impl StripeBilling {
Ok(())
}
pub async fn checkout_with_zed_pro(
&self,
customer_id: &StripeCustomerId,
github_login: &str,
success_url: &str,
) -> Result<String> {
let zed_pro_price_id = self.zed_pro_price_id().await?;
let mut params = StripeCreateCheckoutSessionParams::default();
params.mode = Some(StripeCheckoutSessionMode::Subscription);
params.customer = Some(customer_id);
params.client_reference_id = Some(github_login);
params.line_items = Some(vec![StripeCreateCheckoutSessionLineItems {
price: Some(zed_pro_price_id.to_string()),
quantity: Some(1),
}]);
params.success_url = Some(success_url);
params.billing_address_collection = Some(StripeBillingAddressCollection::Required);
params.customer_update = Some(StripeCustomerUpdate {
address: Some(StripeCustomerUpdateAddress::Auto),
name: Some(StripeCustomerUpdateName::Auto),
shipping: None,
});
params.tax_id_collection = Some(StripeTaxIdCollection { enabled: true });
let session = self.client.create_checkout_session(params).await?;
Ok(session.url.context("no checkout session URL")?)
}
pub async fn checkout_with_zed_pro_trial(
&self,
customer_id: &StripeCustomerId,
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);
let trial_period_days = if eligible_for_extended_trial { 60 } else { 14 };
let mut subscription_metadata = std::collections::HashMap::new();
if eligible_for_extended_trial {
subscription_metadata.insert(
"promo_feature_flag".to_string(),
AGENT_EXTENDED_TRIAL_FEATURE_FLAG.to_string(),
);
}
let mut params = StripeCreateCheckoutSessionParams::default();
params.subscription_data = Some(StripeCreateCheckoutSessionSubscriptionData {
trial_period_days: Some(trial_period_days),
trial_settings: Some(StripeSubscriptionTrialSettings {
end_behavior: StripeSubscriptionTrialSettingsEndBehavior {
missing_payment_method:
StripeSubscriptionTrialSettingsEndBehaviorMissingPaymentMethod::Cancel,
},
}),
metadata: if !subscription_metadata.is_empty() {
Some(subscription_metadata)
} else {
None
},
});
params.mode = Some(StripeCheckoutSessionMode::Subscription);
params.payment_method_collection =
Some(StripeCheckoutSessionPaymentMethodCollection::IfRequired);
params.customer = Some(customer_id);
params.client_reference_id = Some(github_login);
params.line_items = Some(vec![StripeCreateCheckoutSessionLineItems {
price: Some(zed_pro_price_id.to_string()),
quantity: Some(1),
}]);
params.success_url = Some(success_url);
params.billing_address_collection = Some(StripeBillingAddressCollection::Required);
params.customer_update = Some(StripeCustomerUpdate {
address: Some(StripeCustomerUpdateAddress::Auto),
name: Some(StripeCustomerUpdateName::Auto),
shipping: None,
});
params.tax_id_collection = Some(StripeTaxIdCollection { enabled: true });
let session = self.client.create_checkout_session(params).await?;
Ok(session.url.context("no checkout session URL")?)
}
pub async fn subscribe_to_zed_free(
&self,
customer_id: StripeCustomerId,

View File

@@ -439,7 +439,7 @@ async fn test_basic_following(
editor_a1.item_id()
);
#[cfg(all(not(target_os = "macos"), not(target_os = "windows")))]
// #[cfg(all(not(target_os = "macos"), not(target_os = "windows")))]
{
use crate::rpc::RECONNECT_TIMEOUT;
use gpui::TestScreenCaptureSource;
@@ -456,11 +456,19 @@ async fn test_basic_following(
.await
.unwrap();
cx_b.set_screen_capture_sources(vec![display]);
let source = cx_b
.read(|cx| cx.screen_capture_sources())
.await
.unwrap()
.unwrap()
.into_iter()
.next()
.unwrap();
active_call_b
.update(cx_b, |call, cx| {
call.room()
.unwrap()
.update(cx, |room, cx| room.share_screen(cx))
.update(cx, |room, cx| room.share_screen(source, cx))
})
.await
.unwrap();

View File

@@ -277,11 +277,19 @@ async fn test_basic_calls(
let events_b = active_call_events(cx_b);
let events_c = active_call_events(cx_c);
cx_a.set_screen_capture_sources(vec![display]);
let screen_a = cx_a
.update(|cx| cx.screen_capture_sources())
.await
.unwrap()
.unwrap()
.into_iter()
.next()
.unwrap();
active_call_a
.update(cx_a, |call, cx| {
call.room()
.unwrap()
.update(cx, |room, cx| room.share_screen(cx))
.update(cx, |room, cx| room.share_screen(screen_a, cx))
})
.await
.unwrap();
@@ -6312,11 +6320,20 @@ async fn test_join_call_after_screen_was_shared(
// User A shares their screen
let display = gpui::TestScreenCaptureSource::new();
cx_a.set_screen_capture_sources(vec![display]);
let screen_a = cx_a
.update(|cx| cx.screen_capture_sources())
.await
.unwrap()
.unwrap()
.into_iter()
.next()
.unwrap();
active_call_a
.update(cx_a, |call, cx| {
call.room()
.unwrap()
.update(cx, |room, cx| room.share_screen(cx))
.update(cx, |room, cx| room.share_screen(screen_a, cx))
})
.await
.unwrap();

View File

@@ -3,17 +3,11 @@ use std::sync::Arc;
use chrono::{Duration, Utc};
use pretty_assertions::assert_eq;
use crate::llm::AGENT_EXTENDED_TRIAL_FEATURE_FLAG;
use crate::stripe_billing::StripeBilling;
use crate::stripe_client::{
FakeStripeClient, StripeBillingAddressCollection, StripeCheckoutSessionMode,
StripeCheckoutSessionPaymentMethodCollection, StripeCreateCheckoutSessionLineItems,
StripeCreateCheckoutSessionSubscriptionData, StripeCustomerId, StripeCustomerUpdate,
StripeCustomerUpdateAddress, StripeCustomerUpdateName, StripeMeter, StripeMeterId, StripePrice,
StripePriceId, StripePriceRecurring, StripeSubscription, StripeSubscriptionId,
StripeSubscriptionItem, StripeSubscriptionItemId, StripeSubscriptionTrialSettings,
StripeSubscriptionTrialSettingsEndBehavior,
StripeSubscriptionTrialSettingsEndBehaviorMissingPaymentMethod, UpdateSubscriptionItems,
FakeStripeClient, StripeCustomerId, StripeMeter, StripeMeterId, StripePrice, StripePriceId,
StripePriceRecurring, StripeSubscription, StripeSubscriptionId, StripeSubscriptionItem,
StripeSubscriptionItemId, UpdateSubscriptionItems,
};
fn make_stripe_billing() -> (StripeBilling, Arc<FakeStripeClient>) {
@@ -364,240 +358,3 @@ async fn test_bill_model_request_usage() {
);
assert_eq!(create_meter_event_calls[0].value, 73);
}
#[gpui::test]
async fn test_checkout_with_zed_pro() {
let (stripe_billing, stripe_client) = make_stripe_billing();
let customer_id = StripeCustomerId("cus_test".into());
let github_login = "zeduser1";
let success_url = "https://example.com/success";
// It returns an error when the Zed Pro price doesn't exist.
{
let result = stripe_billing
.checkout_with_zed_pro(&customer_id, github_login, success_url)
.await;
assert!(result.is_err());
assert_eq!(
result.err().unwrap().to_string(),
r#"no price ID found for "zed-pro""#
);
}
// Successful checkout.
{
let price = StripePrice {
id: StripePriceId("price_1".into()),
unit_amount: Some(2000),
lookup_key: Some("zed-pro".to_string()),
recurring: None,
};
stripe_client
.prices
.lock()
.insert(price.id.clone(), price.clone());
stripe_billing.initialize().await.unwrap();
let checkout_url = stripe_billing
.checkout_with_zed_pro(&customer_id, github_login, success_url)
.await
.unwrap();
assert!(checkout_url.starts_with("https://checkout.stripe.com/c/pay"));
let create_checkout_session_calls = stripe_client
.create_checkout_session_calls
.lock()
.drain(..)
.collect::<Vec<_>>();
assert_eq!(create_checkout_session_calls.len(), 1);
let call = create_checkout_session_calls.into_iter().next().unwrap();
assert_eq!(call.customer, Some(customer_id));
assert_eq!(call.client_reference_id.as_deref(), Some(github_login));
assert_eq!(call.mode, Some(StripeCheckoutSessionMode::Subscription));
assert_eq!(
call.line_items,
Some(vec![StripeCreateCheckoutSessionLineItems {
price: Some(price.id.to_string()),
quantity: Some(1)
}])
);
assert_eq!(call.payment_method_collection, None);
assert_eq!(call.subscription_data, None);
assert_eq!(call.success_url.as_deref(), Some(success_url));
assert_eq!(
call.billing_address_collection,
Some(StripeBillingAddressCollection::Required)
);
assert_eq!(
call.customer_update,
Some(StripeCustomerUpdate {
address: Some(StripeCustomerUpdateAddress::Auto),
name: Some(StripeCustomerUpdateName::Auto),
shipping: None,
})
);
}
}
#[gpui::test]
async fn test_checkout_with_zed_pro_trial() {
let (stripe_billing, stripe_client) = make_stripe_billing();
let customer_id = StripeCustomerId("cus_test".into());
let github_login = "zeduser1";
let success_url = "https://example.com/success";
// It returns an error when the Zed Pro price doesn't exist.
{
let result = stripe_billing
.checkout_with_zed_pro_trial(&customer_id, github_login, Vec::new(), success_url)
.await;
assert!(result.is_err());
assert_eq!(
result.err().unwrap().to_string(),
r#"no price ID found for "zed-pro""#
);
}
let price = StripePrice {
id: StripePriceId("price_1".into()),
unit_amount: Some(2000),
lookup_key: Some("zed-pro".to_string()),
recurring: None,
};
stripe_client
.prices
.lock()
.insert(price.id.clone(), price.clone());
stripe_billing.initialize().await.unwrap();
// Successful checkout.
{
let checkout_url = stripe_billing
.checkout_with_zed_pro_trial(&customer_id, github_login, Vec::new(), success_url)
.await
.unwrap();
assert!(checkout_url.starts_with("https://checkout.stripe.com/c/pay"));
let create_checkout_session_calls = stripe_client
.create_checkout_session_calls
.lock()
.drain(..)
.collect::<Vec<_>>();
assert_eq!(create_checkout_session_calls.len(), 1);
let call = create_checkout_session_calls.into_iter().next().unwrap();
assert_eq!(call.customer.as_ref(), Some(&customer_id));
assert_eq!(call.client_reference_id.as_deref(), Some(github_login));
assert_eq!(call.mode, Some(StripeCheckoutSessionMode::Subscription));
assert_eq!(
call.line_items,
Some(vec![StripeCreateCheckoutSessionLineItems {
price: Some(price.id.to_string()),
quantity: Some(1)
}])
);
assert_eq!(
call.payment_method_collection,
Some(StripeCheckoutSessionPaymentMethodCollection::IfRequired)
);
assert_eq!(
call.subscription_data,
Some(StripeCreateCheckoutSessionSubscriptionData {
trial_period_days: Some(14),
trial_settings: Some(StripeSubscriptionTrialSettings {
end_behavior: StripeSubscriptionTrialSettingsEndBehavior {
missing_payment_method:
StripeSubscriptionTrialSettingsEndBehaviorMissingPaymentMethod::Cancel,
},
}),
metadata: None,
})
);
assert_eq!(call.success_url.as_deref(), Some(success_url));
assert_eq!(
call.billing_address_collection,
Some(StripeBillingAddressCollection::Required)
);
assert_eq!(
call.customer_update,
Some(StripeCustomerUpdate {
address: Some(StripeCustomerUpdateAddress::Auto),
name: Some(StripeCustomerUpdateName::Auto),
shipping: None,
})
);
}
// Successful checkout with extended trial.
{
let checkout_url = stripe_billing
.checkout_with_zed_pro_trial(
&customer_id,
github_login,
vec![AGENT_EXTENDED_TRIAL_FEATURE_FLAG.to_string()],
success_url,
)
.await
.unwrap();
assert!(checkout_url.starts_with("https://checkout.stripe.com/c/pay"));
let create_checkout_session_calls = stripe_client
.create_checkout_session_calls
.lock()
.drain(..)
.collect::<Vec<_>>();
assert_eq!(create_checkout_session_calls.len(), 1);
let call = create_checkout_session_calls.into_iter().next().unwrap();
assert_eq!(call.customer, Some(customer_id));
assert_eq!(call.client_reference_id.as_deref(), Some(github_login));
assert_eq!(call.mode, Some(StripeCheckoutSessionMode::Subscription));
assert_eq!(
call.line_items,
Some(vec![StripeCreateCheckoutSessionLineItems {
price: Some(price.id.to_string()),
quantity: Some(1)
}])
);
assert_eq!(
call.payment_method_collection,
Some(StripeCheckoutSessionPaymentMethodCollection::IfRequired)
);
assert_eq!(
call.subscription_data,
Some(StripeCreateCheckoutSessionSubscriptionData {
trial_period_days: Some(60),
trial_settings: Some(StripeSubscriptionTrialSettings {
end_behavior: StripeSubscriptionTrialSettingsEndBehavior {
missing_payment_method:
StripeSubscriptionTrialSettingsEndBehaviorMissingPaymentMethod::Cancel,
},
}),
metadata: Some(std::collections::HashMap::from_iter([(
"promo_feature_flag".into(),
AGENT_EXTENDED_TRIAL_FEATURE_FLAG.into()
)])),
})
);
assert_eq!(call.success_url.as_deref(), Some(success_url));
assert_eq!(
call.billing_address_collection,
Some(StripeBillingAddressCollection::Required)
);
assert_eq!(
call.customer_update,
Some(StripeCustomerUpdate {
address: Some(StripeCustomerUpdateAddress::Auto),
name: Some(StripeCustomerUpdateName::Auto),
shipping: None,
})
);
}
}

View File

@@ -144,10 +144,22 @@ pub fn init(cx: &mut App) {
if let Some(room) = room {
window.defer(cx, move |_window, cx| {
room.update(cx, |room, cx| {
if room.is_screen_sharing() {
room.unshare_screen(cx).ok();
if room.is_sharing_screen() {
room.unshare_screen(true, cx).ok();
} else {
room.share_screen(cx).detach_and_log_err(cx);
let sources = cx.screen_capture_sources();
cx.spawn(async move |room, cx| {
let sources = sources.await??;
let first = sources.into_iter().next();
if let Some(first) = first {
room.update(cx, |room, cx| room.share_screen(first, cx))?
.await
} else {
Ok(())
}
})
.detach_and_log_err(cx);
};
});
});
@@ -528,10 +540,10 @@ impl CollabPanel {
project_id: project.id,
worktree_root_names: project.worktree_root_names.clone(),
host_user_id: user_id,
is_last: projects.peek().is_none() && !room.is_screen_sharing(),
is_last: projects.peek().is_none() && !room.is_sharing_screen(),
});
}
if room.is_screen_sharing() {
if room.is_sharing_screen() {
self.entries.push(ListEntry::ParticipantScreen {
peer_id: None,
is_last: true,

View File

@@ -242,7 +242,7 @@ impl CommandPaletteDelegate {
self.selected_ix = cmp::min(self.selected_ix, self.matches.len() - 1);
}
}
///
/// Hit count for each command in the palette.
/// We only account for commands triggered directly via command palette and not by e.g. keystrokes because
/// if a user already knows a keystroke for a command, they are unlikely to use a command palette to look for it.

View File

@@ -1,6 +1,6 @@
use anyhow::{Context as _, Result, anyhow};
use collections::HashMap;
use futures::{FutureExt, StreamExt, channel::oneshot, select};
use futures::{FutureExt, StreamExt, channel::oneshot, future, select};
use gpui::{AppContext as _, AsyncApp, BackgroundExecutor, Task};
use parking_lot::Mutex;
use postage::barrier;
@@ -10,15 +10,19 @@ use smol::channel;
use std::{
fmt,
path::PathBuf,
pin::pin,
sync::{
Arc,
atomic::{AtomicI32, Ordering::SeqCst},
},
time::{Duration, Instant},
};
use util::TryFutureExt;
use util::{ResultExt, TryFutureExt};
use crate::transport::{StdioTransport, Transport};
use crate::{
transport::{StdioTransport, Transport},
types::{CancelledParams, ClientNotification, Notification as _, notifications::Cancelled},
};
const JSON_RPC_VERSION: &str = "2.0";
const REQUEST_TIMEOUT: Duration = Duration::from_secs(60);
@@ -32,6 +36,7 @@ pub const INTERNAL_ERROR: i32 = -32603;
type ResponseHandler = Box<dyn Send + FnOnce(Result<String, Error>)>;
type NotificationHandler = Box<dyn Send + FnMut(Value, AsyncApp)>;
type RequestHandler = Box<dyn Send + FnMut(RequestId, &RawValue, AsyncApp)>;
#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)]
#[serde(untagged)]
@@ -78,6 +83,15 @@ pub struct Request<'a, T> {
pub params: T,
}
#[derive(Serialize, Deserialize)]
pub struct AnyRequest<'a> {
pub jsonrpc: &'a str,
pub id: RequestId,
pub method: &'a str,
#[serde(skip_serializing_if = "is_null_value")]
pub params: Option<&'a RawValue>,
}
#[derive(Serialize, Deserialize)]
struct AnyResponse<'a> {
jsonrpc: &'a str,
@@ -176,15 +190,23 @@ impl Client {
Arc::new(Mutex::new(HashMap::<_, NotificationHandler>::default()));
let response_handlers =
Arc::new(Mutex::new(Some(HashMap::<_, ResponseHandler>::default())));
let request_handlers = Arc::new(Mutex::new(HashMap::<_, RequestHandler>::default()));
let receive_input_task = cx.spawn({
let notification_handlers = notification_handlers.clone();
let response_handlers = response_handlers.clone();
let request_handlers = request_handlers.clone();
let transport = transport.clone();
async move |cx| {
Self::handle_input(transport, notification_handlers, response_handlers, cx)
.log_err()
.await
Self::handle_input(
transport,
notification_handlers,
request_handlers,
response_handlers,
cx,
)
.log_err()
.await
}
});
let receive_err_task = cx.spawn({
@@ -230,13 +252,24 @@ impl Client {
async fn handle_input(
transport: Arc<dyn Transport>,
notification_handlers: Arc<Mutex<HashMap<&'static str, NotificationHandler>>>,
request_handlers: Arc<Mutex<HashMap<&'static str, RequestHandler>>>,
response_handlers: Arc<Mutex<Option<HashMap<RequestId, ResponseHandler>>>>,
cx: &mut AsyncApp,
) -> anyhow::Result<()> {
let mut receiver = transport.receive();
while let Some(message) = receiver.next().await {
if let Ok(response) = serde_json::from_str::<AnyResponse>(&message) {
log::trace!("recv: {}", &message);
if let Ok(request) = serde_json::from_str::<AnyRequest>(&message) {
let mut request_handlers = request_handlers.lock();
if let Some(handler) = request_handlers.get_mut(request.method) {
handler(
request.id,
request.params.unwrap_or(RawValue::NULL),
cx.clone(),
);
}
} else if let Ok(response) = serde_json::from_str::<AnyResponse>(&message) {
if let Some(handlers) = response_handlers.lock().as_mut() {
if let Some(handler) = handlers.remove(&response.id) {
handler(Ok(message.to_string()));
@@ -247,6 +280,8 @@ impl Client {
if let Some(handler) = notification_handlers.get_mut(notification.method.as_str()) {
handler(notification.params.unwrap_or(Value::Null), cx.clone());
}
} else {
log::error!("Unhandled JSON from context_server: {}", message);
}
}
@@ -294,6 +329,24 @@ impl Client {
&self,
method: &str,
params: impl Serialize,
) -> Result<T> {
self.request_impl(method, params, None).await
}
pub async fn cancellable_request<T: DeserializeOwned>(
&self,
method: &str,
params: impl Serialize,
cancel_rx: oneshot::Receiver<()>,
) -> Result<T> {
self.request_impl(method, params, Some(cancel_rx)).await
}
pub async fn request_impl<T: DeserializeOwned>(
&self,
method: &str,
params: impl Serialize,
cancel_rx: Option<oneshot::Receiver<()>>,
) -> Result<T> {
let id = self.next_id.fetch_add(1, SeqCst);
let request = serde_json::to_string(&Request {
@@ -330,6 +383,16 @@ impl Client {
send?;
let mut timeout = executor.timer(REQUEST_TIMEOUT).fuse();
let mut cancel_fut = pin!(
match cancel_rx {
Some(rx) => future::Either::Left(async {
rx.await.log_err();
}),
None => future::Either::Right(future::pending()),
}
.fuse()
);
select! {
response = rx.fuse() => {
let elapsed = started.elapsed();
@@ -348,6 +411,16 @@ impl Client {
Err(_) => anyhow::bail!("cancelled")
}
}
_ = cancel_fut => {
self.notify(
Cancelled::METHOD,
ClientNotification::Cancelled(CancelledParams {
request_id: RequestId::Int(id),
reason: None
})
).log_err();
anyhow::bail!("Request cancelled")
}
_ = timeout => {
log::error!("cancelled csp request task for {method:?} id {id} which took over {:?}", REQUEST_TIMEOUT);
anyhow::bail!("Context server request timeout");

View File

@@ -6,9 +6,9 @@ pub mod test;
pub mod transport;
pub mod types;
use std::fmt::Display;
use std::path::Path;
use std::sync::Arc;
use std::{fmt::Display, path::PathBuf};
use anyhow::Result;
use client::Client;
@@ -31,7 +31,7 @@ impl Display for ContextServerId {
#[derive(Deserialize, Serialize, Clone, PartialEq, Eq, JsonSchema)]
pub struct ContextServerCommand {
#[serde(rename = "command")]
pub path: String,
pub path: PathBuf,
pub args: Vec<String>,
pub env: Option<HashMap<String, String>>,
}

View File

@@ -6,6 +6,9 @@
//! of messages.
use anyhow::Result;
use futures::channel::oneshot;
use gpui::AsyncApp;
use serde_json::Value;
use crate::client::Client;
use crate::types::{self, Notification, Request};
@@ -95,7 +98,24 @@ impl InitializedContextServerProtocol {
self.inner.request(T::METHOD, params).await
}
pub async fn cancellable_request<T: Request>(
&self,
params: T::Params,
cancel_rx: oneshot::Receiver<()>,
) -> Result<T::Response> {
self.inner
.cancellable_request(T::METHOD, params, cancel_rx)
.await
}
pub fn notify<T: Notification>(&self, params: T::Params) -> Result<()> {
self.inner.notify(T::METHOD, params)
}
pub fn on_notification<F>(&self, method: &'static str, f: F)
where
F: 'static + Send + FnMut(Value, AsyncApp),
{
self.inner.on_notification(method, f);
}
}

View File

@@ -3,6 +3,8 @@ use serde::de::DeserializeOwned;
use serde::{Deserialize, Serialize};
use url::Url;
use crate::client::RequestId;
pub const LATEST_PROTOCOL_VERSION: &str = "2025-03-26";
pub const VERSION_2024_11_05: &str = "2024-11-05";
@@ -100,6 +102,7 @@ pub mod notifications {
notification!("notifications/initialized", Initialized, ());
notification!("notifications/progress", Progress, ProgressParams);
notification!("notifications/message", Message, MessageParams);
notification!("notifications/cancelled", Cancelled, CancelledParams);
notification!(
"notifications/resources/updated",
ResourcesUpdated,
@@ -617,11 +620,14 @@ pub enum ClientNotification {
Initialized,
Progress(ProgressParams),
RootsListChanged,
Cancelled {
request_id: String,
#[serde(skip_serializing_if = "Option::is_none")]
reason: Option<String>,
},
Cancelled(CancelledParams),
}
#[derive(Debug, Serialize, Deserialize)]
pub struct CancelledParams {
pub request_id: RequestId,
#[serde(skip_serializing_if = "Option::is_none")]
pub reason: Option<String>,
}
#[derive(Debug, Serialize, Deserialize)]

View File

@@ -6,6 +6,7 @@ mod sign_in;
use crate::sign_in::initiate_sign_in_within_workspace;
use ::fs::Fs;
use anyhow::{Context as _, Result, anyhow};
use client::DisableAiSettings;
use collections::{HashMap, HashSet};
use command_palette_hooks::CommandPaletteFilter;
use futures::{Future, FutureExt, TryFutureExt, channel::oneshot, future::Shared};
@@ -25,6 +26,7 @@ use node_runtime::NodeRuntime;
use parking_lot::Mutex;
use request::StatusNotification;
use serde_json::json;
use settings::Settings;
use settings::SettingsStore;
use sign_in::{reinstall_and_sign_in_within_workspace, sign_out_within_workspace};
use std::collections::hash_map::Entry;
@@ -93,26 +95,34 @@ pub fn init(
let copilot_auth_action_types = [TypeId::of::<SignOut>()];
let copilot_no_auth_action_types = [TypeId::of::<SignIn>()];
let status = handle.read(cx).status();
let is_ai_disabled = DisableAiSettings::get_global(cx).disable_ai;
let filter = CommandPaletteFilter::global_mut(cx);
match status {
Status::Disabled => {
filter.hide_action_types(&copilot_action_types);
filter.hide_action_types(&copilot_auth_action_types);
filter.hide_action_types(&copilot_no_auth_action_types);
}
Status::Authorized => {
filter.hide_action_types(&copilot_no_auth_action_types);
filter.show_action_types(
copilot_action_types
.iter()
.chain(&copilot_auth_action_types),
);
}
_ => {
filter.hide_action_types(&copilot_action_types);
filter.hide_action_types(&copilot_auth_action_types);
filter.show_action_types(copilot_no_auth_action_types.iter());
if is_ai_disabled {
filter.hide_action_types(&copilot_action_types);
filter.hide_action_types(&copilot_auth_action_types);
filter.hide_action_types(&copilot_no_auth_action_types);
} else {
match status {
Status::Disabled => {
filter.hide_action_types(&copilot_action_types);
filter.hide_action_types(&copilot_auth_action_types);
filter.hide_action_types(&copilot_no_auth_action_types);
}
Status::Authorized => {
filter.hide_action_types(&copilot_no_auth_action_types);
filter.show_action_types(
copilot_action_types
.iter()
.chain(&copilot_auth_action_types),
);
}
_ => {
filter.hide_action_types(&copilot_action_types);
filter.hide_action_types(&copilot_auth_action_types);
filter.show_action_types(copilot_no_auth_action_types.iter());
}
}
}
})

View File

@@ -46,6 +46,7 @@ impl DapRegistry {
let name = adapter.name();
let _previous_value = self.0.write().adapters.insert(name, adapter);
}
pub fn add_locator(&self, locator: Arc<dyn DapLocator>) {
self.0.write().locators.insert(locator.name(), locator);
}

View File

@@ -259,6 +259,13 @@ pub struct SpawnNearestTask {
pub reveal: task::RevealStrategy,
}
#[derive(Clone, PartialEq, Action)]
#[action(no_json, no_register)]
pub struct DiffClipboardWithSelectionData {
pub clipboard_text: String,
pub editor: Entity<Editor>,
}
#[derive(Debug, PartialEq, Eq, Clone, Copy, Deserialize, Default)]
pub enum UuidVersion {
#[default]
@@ -322,6 +329,8 @@ actions!(
ApplyDiffHunk,
/// Deletes the character before the cursor.
Backspace,
/// Shows git blame information for the current line.
BlameHover,
/// Cancels the current operation.
Cancel,
/// Cancels the running flycheck operation.
@@ -356,6 +365,8 @@ actions!(
ConvertToLowerCase,
/// Toggles the case of selected text.
ConvertToOppositeCase,
/// Converts selected text to sentence case.
ConvertToSentenceCase,
/// Converts selected text to snake_case.
ConvertToSnakeCase,
/// Converts selected text to Title Case.
@@ -396,6 +407,8 @@ actions!(
DeleteToNextSubwordEnd,
/// Deletes to the start of the previous subword.
DeleteToPreviousSubwordStart,
/// Diffs the text stored in the clipboard against the current selection.
DiffClipboardWithSelection,
/// Displays names of all active cursors.
DisplayCursorNames,
/// Duplicates the current line below.

View File

@@ -844,7 +844,7 @@ impl CompletionsMenu {
.with_sizing_behavior(ListSizingBehavior::Infer)
.w(rems(34.));
Popover::new().child(list).into_any_element()
Popover::new().child(div().child(list)).into_any_element()
}
fn render_aside(

View File

@@ -524,10 +524,10 @@ impl BlockMap {
// * Isomorphic transforms that end *at* the start of the edit
// * Below blocks that end at the start of the edit
// However, if we hit a replace block that ends at the start of the edit we want to reconstruct it.
new_transforms.append(cursor.slice(&old_start, Bias::Left, &()), &());
new_transforms.append(cursor.slice(&old_start, Bias::Left), &());
if let Some(transform) = cursor.item() {
if transform.summary.input_rows > 0
&& cursor.end(&()) == old_start
&& cursor.end() == old_start
&& transform
.block
.as_ref()
@@ -535,13 +535,13 @@ impl BlockMap {
{
// Preserve the transform (push and next)
new_transforms.push(transform.clone(), &());
cursor.next(&());
cursor.next();
// Preserve below blocks at end of edit
while let Some(transform) = cursor.item() {
if transform.block.as_ref().map_or(false, |b| b.place_below()) {
new_transforms.push(transform.clone(), &());
cursor.next(&());
cursor.next();
} else {
break;
}
@@ -579,8 +579,8 @@ impl BlockMap {
let mut new_end = WrapRow(edit.new.end);
loop {
// Seek to the transform starting at or after the end of the edit
cursor.seek(&old_end, Bias::Left, &());
cursor.next(&());
cursor.seek(&old_end, Bias::Left);
cursor.next();
// Extend edit to the end of the discarded transform so it is reconstructed in full
let transform_rows_after_edit = cursor.start().0 - old_end.0;
@@ -592,8 +592,8 @@ impl BlockMap {
if next_edit.old.start <= cursor.start().0 {
old_end = WrapRow(next_edit.old.end);
new_end = WrapRow(next_edit.new.end);
cursor.seek(&old_end, Bias::Left, &());
cursor.next(&());
cursor.seek(&old_end, Bias::Left);
cursor.next();
edits.next();
} else {
break;
@@ -608,7 +608,7 @@ impl BlockMap {
// Discard below blocks at the end of the edit. They'll be reconstructed.
while let Some(transform) = cursor.item() {
if transform.block.as_ref().map_or(false, |b| b.place_below()) {
cursor.next(&());
cursor.next();
} else {
break;
}
@@ -720,7 +720,7 @@ impl BlockMap {
push_isomorphic(&mut new_transforms, rows_after_last_block, wrap_snapshot);
}
new_transforms.append(cursor.suffix(&()), &());
new_transforms.append(cursor.suffix(), &());
debug_assert_eq!(
new_transforms.summary().input_rows,
wrap_snapshot.max_point().row() + 1
@@ -971,7 +971,7 @@ impl BlockMapReader<'_> {
);
let mut cursor = self.transforms.cursor::<(WrapRow, BlockRow)>(&());
cursor.seek(&start_wrap_row, Bias::Left, &());
cursor.seek(&start_wrap_row, Bias::Left);
while let Some(transform) = cursor.item() {
if cursor.start().0 > end_wrap_row {
break;
@@ -982,7 +982,7 @@ impl BlockMapReader<'_> {
return Some(cursor.start().1);
}
}
cursor.next(&());
cursor.next();
}
None
@@ -1293,7 +1293,7 @@ impl BlockSnapshot {
let max_output_row = cmp::min(rows.end, self.transforms.summary().output_rows);
let mut cursor = self.transforms.cursor::<(BlockRow, WrapRow)>(&());
cursor.seek(&BlockRow(rows.start), Bias::Right, &());
cursor.seek(&BlockRow(rows.start), Bias::Right);
let transform_output_start = cursor.start().0.0;
let transform_input_start = cursor.start().1.0;
@@ -1325,7 +1325,7 @@ impl BlockSnapshot {
pub(super) fn row_infos(&self, start_row: BlockRow) -> BlockRows<'_> {
let mut cursor = self.transforms.cursor::<(BlockRow, WrapRow)>(&());
cursor.seek(&start_row, Bias::Right, &());
cursor.seek(&start_row, Bias::Right);
let (output_start, input_start) = cursor.start();
let overshoot = if cursor
.item()
@@ -1346,9 +1346,9 @@ impl BlockSnapshot {
pub fn blocks_in_range(&self, rows: Range<u32>) -> impl Iterator<Item = (u32, &Block)> {
let mut cursor = self.transforms.cursor::<BlockRow>(&());
cursor.seek(&BlockRow(rows.start), Bias::Left, &());
while cursor.start().0 < rows.start && cursor.end(&()).0 <= rows.start {
cursor.next(&());
cursor.seek(&BlockRow(rows.start), Bias::Left);
while cursor.start().0 < rows.start && cursor.end().0 <= rows.start {
cursor.next();
}
std::iter::from_fn(move || {
@@ -1364,10 +1364,10 @@ impl BlockSnapshot {
break;
}
if let Some(block) = &transform.block {
cursor.next(&());
cursor.next();
return Some((start_row, block));
} else {
cursor.next(&());
cursor.next();
}
}
None
@@ -1377,7 +1377,7 @@ impl BlockSnapshot {
pub fn sticky_header_excerpt(&self, position: f32) -> Option<StickyHeaderExcerpt<'_>> {
let top_row = position as u32;
let mut cursor = self.transforms.cursor::<BlockRow>(&());
cursor.seek(&BlockRow(top_row), Bias::Right, &());
cursor.seek(&BlockRow(top_row), Bias::Right);
while let Some(transform) = cursor.item() {
match &transform.block {
@@ -1386,7 +1386,7 @@ impl BlockSnapshot {
}
Some(block) if block.is_buffer_header() => return None,
_ => {
cursor.prev(&());
cursor.prev();
continue;
}
}
@@ -1414,7 +1414,7 @@ impl BlockSnapshot {
let wrap_row = WrapRow(wrap_point.row());
let mut cursor = self.transforms.cursor::<WrapRow>(&());
cursor.seek(&wrap_row, Bias::Left, &());
cursor.seek(&wrap_row, Bias::Left);
while let Some(transform) = cursor.item() {
if let Some(block) = transform.block.as_ref() {
@@ -1425,7 +1425,7 @@ impl BlockSnapshot {
break;
}
cursor.next(&());
cursor.next();
}
None
@@ -1442,7 +1442,7 @@ impl BlockSnapshot {
pub fn longest_row_in_range(&self, range: Range<BlockRow>) -> BlockRow {
let mut cursor = self.transforms.cursor::<(BlockRow, WrapRow)>(&());
cursor.seek(&range.start, Bias::Right, &());
cursor.seek(&range.start, Bias::Right);
let mut longest_row = range.start;
let mut longest_row_chars = 0;
@@ -1453,7 +1453,7 @@ impl BlockSnapshot {
let wrap_start_row = input_start.0 + overshoot;
let wrap_end_row = cmp::min(
input_start.0 + (range.end.0 - output_start.0),
cursor.end(&()).1.0,
cursor.end().1.0,
);
let summary = self
.wrap_snapshot
@@ -1461,12 +1461,12 @@ impl BlockSnapshot {
longest_row = BlockRow(range.start.0 + summary.longest_row);
longest_row_chars = summary.longest_row_chars;
}
cursor.next(&());
cursor.next();
}
let cursor_start_row = cursor.start().0;
if range.end > cursor_start_row {
let summary = cursor.summary::<_, TransformSummary>(&range.end, Bias::Right, &());
let summary = cursor.summary::<_, TransformSummary>(&range.end, Bias::Right);
if summary.longest_row_chars > longest_row_chars {
longest_row = BlockRow(cursor_start_row.0 + summary.longest_row);
longest_row_chars = summary.longest_row_chars;
@@ -1493,7 +1493,7 @@ impl BlockSnapshot {
pub(super) fn line_len(&self, row: BlockRow) -> u32 {
let mut cursor = self.transforms.cursor::<(BlockRow, WrapRow)>(&());
cursor.seek(&BlockRow(row.0), Bias::Right, &());
cursor.seek(&BlockRow(row.0), Bias::Right);
if let Some(transform) = cursor.item() {
let (output_start, input_start) = cursor.start();
let overshoot = row.0 - output_start.0;
@@ -1511,13 +1511,13 @@ impl BlockSnapshot {
pub(super) fn is_block_line(&self, row: BlockRow) -> bool {
let mut cursor = self.transforms.cursor::<(BlockRow, WrapRow)>(&());
cursor.seek(&row, Bias::Right, &());
cursor.seek(&row, Bias::Right);
cursor.item().map_or(false, |t| t.block.is_some())
}
pub(super) fn is_folded_buffer_header(&self, row: BlockRow) -> bool {
let mut cursor = self.transforms.cursor::<(BlockRow, WrapRow)>(&());
cursor.seek(&row, Bias::Right, &());
cursor.seek(&row, Bias::Right);
let Some(transform) = cursor.item() else {
return false;
};
@@ -1529,7 +1529,7 @@ impl BlockSnapshot {
.wrap_snapshot
.make_wrap_point(Point::new(row.0, 0), Bias::Left);
let mut cursor = self.transforms.cursor::<(WrapRow, BlockRow)>(&());
cursor.seek(&WrapRow(wrap_point.row()), Bias::Right, &());
cursor.seek(&WrapRow(wrap_point.row()), Bias::Right);
cursor.item().map_or(false, |transform| {
transform
.block
@@ -1540,17 +1540,17 @@ impl BlockSnapshot {
pub fn clip_point(&self, point: BlockPoint, bias: Bias) -> BlockPoint {
let mut cursor = self.transforms.cursor::<(BlockRow, WrapRow)>(&());
cursor.seek(&BlockRow(point.row), Bias::Right, &());
cursor.seek(&BlockRow(point.row), Bias::Right);
let max_input_row = WrapRow(self.transforms.summary().input_rows);
let mut search_left =
(bias == Bias::Left && cursor.start().1.0 > 0) || cursor.end(&()).1 == max_input_row;
(bias == Bias::Left && cursor.start().1.0 > 0) || cursor.end().1 == max_input_row;
let mut reversed = false;
loop {
if let Some(transform) = cursor.item() {
let (output_start_row, input_start_row) = cursor.start();
let (output_end_row, input_end_row) = cursor.end(&());
let (output_end_row, input_end_row) = cursor.end();
let output_start = Point::new(output_start_row.0, 0);
let input_start = Point::new(input_start_row.0, 0);
let input_end = Point::new(input_end_row.0, 0);
@@ -1584,23 +1584,23 @@ impl BlockSnapshot {
}
if search_left {
cursor.prev(&());
cursor.prev();
} else {
cursor.next(&());
cursor.next();
}
} else if reversed {
return self.max_point();
} else {
reversed = true;
search_left = !search_left;
cursor.seek(&BlockRow(point.row), Bias::Right, &());
cursor.seek(&BlockRow(point.row), Bias::Right);
}
}
}
pub fn to_block_point(&self, wrap_point: WrapPoint) -> BlockPoint {
let mut cursor = self.transforms.cursor::<(WrapRow, BlockRow)>(&());
cursor.seek(&WrapRow(wrap_point.row()), Bias::Right, &());
cursor.seek(&WrapRow(wrap_point.row()), Bias::Right);
if let Some(transform) = cursor.item() {
if transform.block.is_some() {
BlockPoint::new(cursor.start().1.0, 0)
@@ -1618,7 +1618,7 @@ impl BlockSnapshot {
pub fn to_wrap_point(&self, block_point: BlockPoint, bias: Bias) -> WrapPoint {
let mut cursor = self.transforms.cursor::<(BlockRow, WrapRow)>(&());
cursor.seek(&BlockRow(block_point.row), Bias::Right, &());
cursor.seek(&BlockRow(block_point.row), Bias::Right);
if let Some(transform) = cursor.item() {
match transform.block.as_ref() {
Some(block) => {
@@ -1630,7 +1630,7 @@ impl BlockSnapshot {
} else if bias == Bias::Left {
WrapPoint::new(cursor.start().1.0, 0)
} else {
let wrap_row = cursor.end(&()).1.0 - 1;
let wrap_row = cursor.end().1.0 - 1;
WrapPoint::new(wrap_row, self.wrap_snapshot.line_len(wrap_row))
}
}
@@ -1650,14 +1650,14 @@ impl BlockChunks<'_> {
/// Go to the next transform
fn advance(&mut self) {
self.input_chunk = Chunk::default();
self.transforms.next(&());
self.transforms.next();
while let Some(transform) = self.transforms.item() {
if transform
.block
.as_ref()
.map_or(false, |block| block.height() == 0)
{
self.transforms.next(&());
self.transforms.next();
} else {
break;
}
@@ -1672,7 +1672,7 @@ impl BlockChunks<'_> {
let start_output_row = self.transforms.start().0.0;
if start_output_row < self.max_output_row {
let end_input_row = cmp::min(
self.transforms.end(&()).1.0,
self.transforms.end().1.0,
start_input_row + (self.max_output_row - start_output_row),
);
self.input_chunks.seek(start_input_row..end_input_row);
@@ -1696,7 +1696,7 @@ impl<'a> Iterator for BlockChunks<'a> {
let transform = self.transforms.item()?;
if transform.block.is_some() {
let block_start = self.transforms.start().0.0;
let mut block_end = self.transforms.end(&()).0.0;
let mut block_end = self.transforms.end().0.0;
self.advance();
if self.transforms.item().is_none() {
block_end -= 1;
@@ -1731,7 +1731,7 @@ impl<'a> Iterator for BlockChunks<'a> {
}
}
let transform_end = self.transforms.end(&()).0.0;
let transform_end = self.transforms.end().0.0;
let (prefix_rows, prefix_bytes) =
offset_for_row(self.input_chunk.text, transform_end - self.output_row);
self.output_row += prefix_rows;
@@ -1770,15 +1770,15 @@ impl Iterator for BlockRows<'_> {
self.started = true;
}
if self.output_row.0 >= self.transforms.end(&()).0.0 {
self.transforms.next(&());
if self.output_row.0 >= self.transforms.end().0.0 {
self.transforms.next();
while let Some(transform) = self.transforms.item() {
if transform
.block
.as_ref()
.map_or(false, |block| block.height() == 0)
{
self.transforms.next(&());
self.transforms.next();
} else {
break;
}

View File

@@ -52,15 +52,15 @@ impl CreaseSnapshot {
) -> Option<&'a Crease<Anchor>> {
let start = snapshot.anchor_before(Point::new(row.0, 0));
let mut cursor = self.creases.cursor::<ItemSummary>(snapshot);
cursor.seek(&start, Bias::Left, snapshot);
cursor.seek(&start, Bias::Left);
while let Some(item) = cursor.item() {
match Ord::cmp(&item.crease.range().start.to_point(snapshot).row, &row.0) {
Ordering::Less => cursor.next(snapshot),
Ordering::Less => cursor.next(),
Ordering::Equal => {
if item.crease.range().start.is_valid(snapshot) {
return Some(&item.crease);
} else {
cursor.next(snapshot);
cursor.next();
}
}
Ordering::Greater => break,
@@ -76,11 +76,11 @@ impl CreaseSnapshot {
) -> impl 'a + Iterator<Item = &'a Crease<Anchor>> {
let start = snapshot.anchor_before(Point::new(range.start.0, 0));
let mut cursor = self.creases.cursor::<ItemSummary>(snapshot);
cursor.seek(&start, Bias::Left, snapshot);
cursor.seek(&start, Bias::Left);
std::iter::from_fn(move || {
while let Some(item) = cursor.item() {
cursor.next(snapshot);
cursor.next();
let crease_range = item.crease.range();
let crease_start = crease_range.start.to_point(snapshot);
let crease_end = crease_range.end.to_point(snapshot);
@@ -102,13 +102,13 @@ impl CreaseSnapshot {
let mut cursor = self.creases.cursor::<ItemSummary>(snapshot);
let mut results = Vec::new();
cursor.next(snapshot);
cursor.next();
while let Some(item) = cursor.item() {
let crease_range = item.crease.range();
let start_point = crease_range.start.to_point(snapshot);
let end_point = crease_range.end.to_point(snapshot);
results.push((item.id, start_point..end_point));
cursor.next(snapshot);
cursor.next();
}
results
@@ -298,7 +298,7 @@ impl CreaseMap {
let mut cursor = self.snapshot.creases.cursor::<ItemSummary>(snapshot);
for crease in creases {
let crease_range = crease.range().clone();
new_creases.append(cursor.slice(&crease_range, Bias::Left, snapshot), snapshot);
new_creases.append(cursor.slice(&crease_range, Bias::Left), snapshot);
let id = self.next_id;
self.next_id.0 += 1;
@@ -306,7 +306,7 @@ impl CreaseMap {
new_creases.push(CreaseItem { crease, id }, snapshot);
new_ids.push(id);
}
new_creases.append(cursor.suffix(snapshot), snapshot);
new_creases.append(cursor.suffix(), snapshot);
new_creases
};
new_ids
@@ -332,9 +332,9 @@ impl CreaseMap {
let mut cursor = self.snapshot.creases.cursor::<ItemSummary>(snapshot);
for (id, range) in &removals {
new_creases.append(cursor.slice(range, Bias::Left, snapshot), snapshot);
new_creases.append(cursor.slice(range, Bias::Left), snapshot);
while let Some(item) = cursor.item() {
cursor.next(snapshot);
cursor.next();
if item.id == *id {
break;
} else {
@@ -343,7 +343,7 @@ impl CreaseMap {
}
}
new_creases.append(cursor.suffix(snapshot), snapshot);
new_creases.append(cursor.suffix(), snapshot);
new_creases
};

View File

@@ -99,7 +99,7 @@ impl FoldPoint {
pub fn to_inlay_point(self, snapshot: &FoldSnapshot) -> InlayPoint {
let mut cursor = snapshot.transforms.cursor::<(FoldPoint, InlayPoint)>(&());
cursor.seek(&self, Bias::Right, &());
cursor.seek(&self, Bias::Right);
let overshoot = self.0 - cursor.start().0.0;
InlayPoint(cursor.start().1.0 + overshoot)
}
@@ -108,7 +108,7 @@ impl FoldPoint {
let mut cursor = snapshot
.transforms
.cursor::<(FoldPoint, TransformSummary)>(&());
cursor.seek(&self, Bias::Right, &());
cursor.seek(&self, Bias::Right);
let overshoot = self.0 - cursor.start().1.output.lines;
let mut offset = cursor.start().1.output.len;
if !overshoot.is_zero() {
@@ -187,10 +187,10 @@ impl FoldMapWriter<'_> {
width: None,
},
);
new_tree.append(cursor.slice(&fold.range, Bias::Right, buffer), buffer);
new_tree.append(cursor.slice(&fold.range, Bias::Right), buffer);
new_tree.push(fold, buffer);
}
new_tree.append(cursor.suffix(buffer), buffer);
new_tree.append(cursor.suffix(), buffer);
new_tree
};
@@ -252,7 +252,7 @@ impl FoldMapWriter<'_> {
fold_ixs_to_delete.push(*folds_cursor.start());
self.0.snapshot.fold_metadata_by_id.remove(&fold.id);
}
folds_cursor.next(buffer);
folds_cursor.next();
}
}
@@ -263,10 +263,10 @@ impl FoldMapWriter<'_> {
let mut cursor = self.0.snapshot.folds.cursor::<usize>(buffer);
let mut folds = SumTree::new(buffer);
for fold_ix in fold_ixs_to_delete {
folds.append(cursor.slice(&fold_ix, Bias::Right, buffer), buffer);
cursor.next(buffer);
folds.append(cursor.slice(&fold_ix, Bias::Right), buffer);
cursor.next();
}
folds.append(cursor.suffix(buffer), buffer);
folds.append(cursor.suffix(), buffer);
folds
};
@@ -412,7 +412,7 @@ impl FoldMap {
let mut new_transforms = SumTree::<Transform>::default();
let mut cursor = self.snapshot.transforms.cursor::<InlayOffset>(&());
cursor.seek(&InlayOffset(0), Bias::Right, &());
cursor.seek(&InlayOffset(0), Bias::Right);
while let Some(mut edit) = inlay_edits_iter.next() {
if let Some(item) = cursor.item() {
@@ -421,19 +421,19 @@ impl FoldMap {
|transform| {
if !transform.is_fold() {
transform.summary.add_summary(&item.summary, &());
cursor.next(&());
cursor.next();
}
},
&(),
);
}
}
new_transforms.append(cursor.slice(&edit.old.start, Bias::Left, &()), &());
new_transforms.append(cursor.slice(&edit.old.start, Bias::Left), &());
edit.new.start -= edit.old.start - *cursor.start();
edit.old.start = *cursor.start();
cursor.seek(&edit.old.end, Bias::Right, &());
cursor.next(&());
cursor.seek(&edit.old.end, Bias::Right);
cursor.next();
let mut delta = edit.new_len().0 as isize - edit.old_len().0 as isize;
loop {
@@ -449,8 +449,8 @@ impl FoldMap {
if next_edit.old.end >= edit.old.end {
edit.old.end = next_edit.old.end;
cursor.seek(&edit.old.end, Bias::Right, &());
cursor.next(&());
cursor.seek(&edit.old.end, Bias::Right);
cursor.next();
}
} else {
break;
@@ -467,11 +467,7 @@ impl FoldMap {
.snapshot
.folds
.cursor::<FoldRange>(&inlay_snapshot.buffer);
folds_cursor.seek(
&FoldRange(anchor..Anchor::max()),
Bias::Left,
&inlay_snapshot.buffer,
);
folds_cursor.seek(&FoldRange(anchor..Anchor::max()), Bias::Left);
let mut folds = iter::from_fn({
let inlay_snapshot = &inlay_snapshot;
@@ -485,7 +481,7 @@ impl FoldMap {
..inlay_snapshot.to_inlay_offset(buffer_end),
)
});
folds_cursor.next(&inlay_snapshot.buffer);
folds_cursor.next();
item
}
})
@@ -558,7 +554,7 @@ impl FoldMap {
}
}
new_transforms.append(cursor.suffix(&()), &());
new_transforms.append(cursor.suffix(), &());
if new_transforms.is_empty() {
let text_summary = inlay_snapshot.text_summary();
push_isomorphic(&mut new_transforms, text_summary);
@@ -575,31 +571,31 @@ impl FoldMap {
let mut new_transforms = new_transforms.cursor::<(InlayOffset, FoldOffset)>(&());
for mut edit in inlay_edits {
old_transforms.seek(&edit.old.start, Bias::Left, &());
old_transforms.seek(&edit.old.start, Bias::Left);
if old_transforms.item().map_or(false, |t| t.is_fold()) {
edit.old.start = old_transforms.start().0;
}
let old_start =
old_transforms.start().1.0 + (edit.old.start - old_transforms.start().0).0;
old_transforms.seek_forward(&edit.old.end, Bias::Right, &());
old_transforms.seek_forward(&edit.old.end, Bias::Right);
if old_transforms.item().map_or(false, |t| t.is_fold()) {
old_transforms.next(&());
old_transforms.next();
edit.old.end = old_transforms.start().0;
}
let old_end =
old_transforms.start().1.0 + (edit.old.end - old_transforms.start().0).0;
new_transforms.seek(&edit.new.start, Bias::Left, &());
new_transforms.seek(&edit.new.start, Bias::Left);
if new_transforms.item().map_or(false, |t| t.is_fold()) {
edit.new.start = new_transforms.start().0;
}
let new_start =
new_transforms.start().1.0 + (edit.new.start - new_transforms.start().0).0;
new_transforms.seek_forward(&edit.new.end, Bias::Right, &());
new_transforms.seek_forward(&edit.new.end, Bias::Right);
if new_transforms.item().map_or(false, |t| t.is_fold()) {
new_transforms.next(&());
new_transforms.next();
edit.new.end = new_transforms.start().0;
}
let new_end =
@@ -656,10 +652,10 @@ impl FoldSnapshot {
let mut summary = TextSummary::default();
let mut cursor = self.transforms.cursor::<(FoldPoint, InlayPoint)>(&());
cursor.seek(&range.start, Bias::Right, &());
cursor.seek(&range.start, Bias::Right);
if let Some(transform) = cursor.item() {
let start_in_transform = range.start.0 - cursor.start().0.0;
let end_in_transform = cmp::min(range.end, cursor.end(&()).0).0 - cursor.start().0.0;
let end_in_transform = cmp::min(range.end, cursor.end().0).0 - cursor.start().0.0;
if let Some(placeholder) = transform.placeholder.as_ref() {
summary = TextSummary::from(
&placeholder.text
@@ -678,10 +674,10 @@ impl FoldSnapshot {
}
}
if range.end > cursor.end(&()).0 {
cursor.next(&());
if range.end > cursor.end().0 {
cursor.next();
summary += &cursor
.summary::<_, TransformSummary>(&range.end, Bias::Right, &())
.summary::<_, TransformSummary>(&range.end, Bias::Right)
.output;
if let Some(transform) = cursor.item() {
let end_in_transform = range.end.0 - cursor.start().0.0;
@@ -705,19 +701,16 @@ impl FoldSnapshot {
pub fn to_fold_point(&self, point: InlayPoint, bias: Bias) -> FoldPoint {
let mut cursor = self.transforms.cursor::<(InlayPoint, FoldPoint)>(&());
cursor.seek(&point, Bias::Right, &());
cursor.seek(&point, Bias::Right);
if cursor.item().map_or(false, |t| t.is_fold()) {
if bias == Bias::Left || point == cursor.start().0 {
cursor.start().1
} else {
cursor.end(&()).1
cursor.end().1
}
} else {
let overshoot = point.0 - cursor.start().0.0;
FoldPoint(cmp::min(
cursor.start().1.0 + overshoot,
cursor.end(&()).1.0,
))
FoldPoint(cmp::min(cursor.start().1.0 + overshoot, cursor.end().1.0))
}
}
@@ -742,7 +735,7 @@ impl FoldSnapshot {
let fold_point = FoldPoint::new(start_row, 0);
let mut cursor = self.transforms.cursor::<(FoldPoint, InlayPoint)>(&());
cursor.seek(&fold_point, Bias::Left, &());
cursor.seek(&fold_point, Bias::Left);
let overshoot = fold_point.0 - cursor.start().0.0;
let inlay_point = InlayPoint(cursor.start().1.0 + overshoot);
@@ -773,7 +766,7 @@ impl FoldSnapshot {
let mut folds = intersecting_folds(&self.inlay_snapshot, &self.folds, range, false);
iter::from_fn(move || {
let item = folds.item();
folds.next(&self.inlay_snapshot.buffer);
folds.next();
item
})
}
@@ -785,7 +778,7 @@ impl FoldSnapshot {
let buffer_offset = offset.to_offset(&self.inlay_snapshot.buffer);
let inlay_offset = self.inlay_snapshot.to_inlay_offset(buffer_offset);
let mut cursor = self.transforms.cursor::<InlayOffset>(&());
cursor.seek(&inlay_offset, Bias::Right, &());
cursor.seek(&inlay_offset, Bias::Right);
cursor.item().map_or(false, |t| t.placeholder.is_some())
}
@@ -794,7 +787,7 @@ impl FoldSnapshot {
.inlay_snapshot
.to_inlay_point(Point::new(buffer_row.0, 0));
let mut cursor = self.transforms.cursor::<InlayPoint>(&());
cursor.seek(&inlay_point, Bias::Right, &());
cursor.seek(&inlay_point, Bias::Right);
loop {
match cursor.item() {
Some(transform) => {
@@ -808,11 +801,11 @@ impl FoldSnapshot {
None => return false,
}
if cursor.end(&()).row() == inlay_point.row() {
cursor.next(&());
if cursor.end().row() == inlay_point.row() {
cursor.next();
} else {
inlay_point.0 += Point::new(1, 0);
cursor.seek(&inlay_point, Bias::Right, &());
cursor.seek(&inlay_point, Bias::Right);
}
}
}
@@ -824,14 +817,14 @@ impl FoldSnapshot {
highlights: Highlights<'a>,
) -> FoldChunks<'a> {
let mut transform_cursor = self.transforms.cursor::<(FoldOffset, InlayOffset)>(&());
transform_cursor.seek(&range.start, Bias::Right, &());
transform_cursor.seek(&range.start, Bias::Right);
let inlay_start = {
let overshoot = range.start.0 - transform_cursor.start().0.0;
transform_cursor.start().1 + InlayOffset(overshoot)
};
let transform_end = transform_cursor.end(&());
let transform_end = transform_cursor.end();
let inlay_end = if transform_cursor
.item()
@@ -879,14 +872,14 @@ impl FoldSnapshot {
pub fn clip_point(&self, point: FoldPoint, bias: Bias) -> FoldPoint {
let mut cursor = self.transforms.cursor::<(FoldPoint, InlayPoint)>(&());
cursor.seek(&point, Bias::Right, &());
cursor.seek(&point, Bias::Right);
if let Some(transform) = cursor.item() {
let transform_start = cursor.start().0.0;
if transform.placeholder.is_some() {
if point.0 == transform_start || matches!(bias, Bias::Left) {
FoldPoint(transform_start)
} else {
FoldPoint(cursor.end(&()).0.0)
FoldPoint(cursor.end().0.0)
}
} else {
let overshoot = InlayPoint(point.0 - transform_start);
@@ -945,7 +938,7 @@ fn intersecting_folds<'a>(
start_cmp == Ordering::Less && end_cmp == Ordering::Greater
}
});
cursor.next(buffer);
cursor.next();
cursor
}
@@ -1211,7 +1204,7 @@ pub struct FoldRows<'a> {
impl FoldRows<'_> {
pub(crate) fn seek(&mut self, row: u32) {
let fold_point = FoldPoint::new(row, 0);
self.cursor.seek(&fold_point, Bias::Left, &());
self.cursor.seek(&fold_point, Bias::Left);
let overshoot = fold_point.0 - self.cursor.start().0.0;
let inlay_point = InlayPoint(self.cursor.start().1.0 + overshoot);
self.input_rows.seek(inlay_point.row());
@@ -1224,8 +1217,8 @@ impl Iterator for FoldRows<'_> {
fn next(&mut self) -> Option<Self::Item> {
let mut traversed_fold = false;
while self.fold_point > self.cursor.end(&()).0 {
self.cursor.next(&());
while self.fold_point > self.cursor.end().0 {
self.cursor.next();
traversed_fold = true;
if self.cursor.item().is_none() {
break;
@@ -1330,14 +1323,14 @@ pub struct FoldChunks<'a> {
impl FoldChunks<'_> {
pub(crate) fn seek(&mut self, range: Range<FoldOffset>) {
self.transform_cursor.seek(&range.start, Bias::Right, &());
self.transform_cursor.seek(&range.start, Bias::Right);
let inlay_start = {
let overshoot = range.start.0 - self.transform_cursor.start().0.0;
self.transform_cursor.start().1 + InlayOffset(overshoot)
};
let transform_end = self.transform_cursor.end(&());
let transform_end = self.transform_cursor.end();
let inlay_end = if self
.transform_cursor
@@ -1376,10 +1369,10 @@ impl<'a> Iterator for FoldChunks<'a> {
self.inlay_chunk.take();
self.inlay_offset += InlayOffset(transform.summary.input.len);
while self.inlay_offset >= self.transform_cursor.end(&()).1
while self.inlay_offset >= self.transform_cursor.end().1
&& self.transform_cursor.item().is_some()
{
self.transform_cursor.next(&());
self.transform_cursor.next();
}
self.output_offset.0 += placeholder.text.len();
@@ -1396,7 +1389,7 @@ impl<'a> Iterator for FoldChunks<'a> {
&& self.inlay_chunks.offset() != self.inlay_offset
{
let transform_start = self.transform_cursor.start();
let transform_end = self.transform_cursor.end(&());
let transform_end = self.transform_cursor.end();
let inlay_end = if self.max_output_offset < transform_end.0 {
let overshoot = self.max_output_offset.0 - transform_start.0.0;
transform_start.1 + InlayOffset(overshoot)
@@ -1417,14 +1410,14 @@ impl<'a> Iterator for FoldChunks<'a> {
if let Some((buffer_chunk_start, mut inlay_chunk)) = self.inlay_chunk.clone() {
let chunk = &mut inlay_chunk.chunk;
let buffer_chunk_end = buffer_chunk_start + InlayOffset(chunk.text.len());
let transform_end = self.transform_cursor.end(&()).1;
let transform_end = self.transform_cursor.end().1;
let chunk_end = buffer_chunk_end.min(transform_end);
chunk.text = &chunk.text
[(self.inlay_offset - buffer_chunk_start).0..(chunk_end - buffer_chunk_start).0];
if chunk_end == transform_end {
self.transform_cursor.next(&());
self.transform_cursor.next();
} else if chunk_end == buffer_chunk_end {
self.inlay_chunk.take();
}
@@ -1456,7 +1449,7 @@ impl FoldOffset {
let mut cursor = snapshot
.transforms
.cursor::<(FoldOffset, TransformSummary)>(&());
cursor.seek(&self, Bias::Right, &());
cursor.seek(&self, Bias::Right);
let overshoot = if cursor.item().map_or(true, |t| t.is_fold()) {
Point::new(0, (self.0 - cursor.start().0.0) as u32)
} else {
@@ -1470,7 +1463,7 @@ impl FoldOffset {
#[cfg(test)]
pub fn to_inlay_offset(self, snapshot: &FoldSnapshot) -> InlayOffset {
let mut cursor = snapshot.transforms.cursor::<(FoldOffset, InlayOffset)>(&());
cursor.seek(&self, Bias::Right, &());
cursor.seek(&self, Bias::Right);
let overshoot = self.0 - cursor.start().0.0;
InlayOffset(cursor.start().1.0 + overshoot)
}

View File

@@ -263,7 +263,7 @@ pub struct InlayChunk<'a> {
impl InlayChunks<'_> {
pub fn seek(&mut self, new_range: Range<InlayOffset>) {
self.transforms.seek(&new_range.start, Bias::Right, &());
self.transforms.seek(&new_range.start, Bias::Right);
let buffer_range = self.snapshot.to_buffer_offset(new_range.start)
..self.snapshot.to_buffer_offset(new_range.end);
@@ -296,12 +296,12 @@ impl<'a> Iterator for InlayChunks<'a> {
*chunk = self.buffer_chunks.next().unwrap();
}
let desired_bytes = self.transforms.end(&()).0.0 - self.output_offset.0;
let desired_bytes = self.transforms.end().0.0 - self.output_offset.0;
// If we're already at the transform boundary, skip to the next transform
if desired_bytes == 0 {
self.inlay_chunks = None;
self.transforms.next(&());
self.transforms.next();
return self.next();
}
@@ -397,7 +397,7 @@ impl<'a> Iterator for InlayChunks<'a> {
let inlay_chunks = self.inlay_chunks.get_or_insert_with(|| {
let start = offset_in_inlay;
let end = cmp::min(self.max_output_offset, self.transforms.end(&()).0)
let end = cmp::min(self.max_output_offset, self.transforms.end().0)
- self.transforms.start().0;
inlay.text.chunks_in_range(start.0..end.0)
});
@@ -441,9 +441,9 @@ impl<'a> Iterator for InlayChunks<'a> {
}
};
if self.output_offset >= self.transforms.end(&()).0 {
if self.output_offset >= self.transforms.end().0 {
self.inlay_chunks = None;
self.transforms.next(&());
self.transforms.next();
}
Some(chunk)
@@ -453,7 +453,7 @@ impl<'a> Iterator for InlayChunks<'a> {
impl InlayBufferRows<'_> {
pub fn seek(&mut self, row: u32) {
let inlay_point = InlayPoint::new(row, 0);
self.transforms.seek(&inlay_point, Bias::Left, &());
self.transforms.seek(&inlay_point, Bias::Left);
let mut buffer_point = self.transforms.start().1;
let buffer_row = MultiBufferRow(if row == 0 {
@@ -487,7 +487,7 @@ impl Iterator for InlayBufferRows<'_> {
self.inlay_row += 1;
self.transforms
.seek_forward(&InlayPoint::new(self.inlay_row, 0), Bias::Left, &());
.seek_forward(&InlayPoint::new(self.inlay_row, 0), Bias::Left);
Some(buffer_row)
}
@@ -556,18 +556,18 @@ impl InlayMap {
let mut cursor = snapshot.transforms.cursor::<(usize, InlayOffset)>(&());
let mut buffer_edits_iter = buffer_edits.iter().peekable();
while let Some(buffer_edit) = buffer_edits_iter.next() {
new_transforms.append(cursor.slice(&buffer_edit.old.start, Bias::Left, &()), &());
new_transforms.append(cursor.slice(&buffer_edit.old.start, Bias::Left), &());
if let Some(Transform::Isomorphic(transform)) = cursor.item() {
if cursor.end(&()).0 == buffer_edit.old.start {
if cursor.end().0 == buffer_edit.old.start {
push_isomorphic(&mut new_transforms, *transform);
cursor.next(&());
cursor.next();
}
}
// Remove all the inlays and transforms contained by the edit.
let old_start =
cursor.start().1 + InlayOffset(buffer_edit.old.start - cursor.start().0);
cursor.seek(&buffer_edit.old.end, Bias::Right, &());
cursor.seek(&buffer_edit.old.end, Bias::Right);
let old_end =
cursor.start().1 + InlayOffset(buffer_edit.old.end - cursor.start().0);
@@ -625,20 +625,20 @@ impl InlayMap {
// we can push its remainder.
if buffer_edits_iter
.peek()
.map_or(true, |edit| edit.old.start >= cursor.end(&()).0)
.map_or(true, |edit| edit.old.start >= cursor.end().0)
{
let transform_start = new_transforms.summary().input.len;
let transform_end =
buffer_edit.new.end + (cursor.end(&()).0 - buffer_edit.old.end);
buffer_edit.new.end + (cursor.end().0 - buffer_edit.old.end);
push_isomorphic(
&mut new_transforms,
buffer_snapshot.text_summary_for_range(transform_start..transform_end),
);
cursor.next(&());
cursor.next();
}
}
new_transforms.append(cursor.suffix(&()), &());
new_transforms.append(cursor.suffix(), &());
if new_transforms.is_empty() {
new_transforms.push(Transform::Isomorphic(Default::default()), &());
}
@@ -773,7 +773,7 @@ impl InlaySnapshot {
let mut cursor = self
.transforms
.cursor::<(InlayOffset, (InlayPoint, usize))>(&());
cursor.seek(&offset, Bias::Right, &());
cursor.seek(&offset, Bias::Right);
let overshoot = offset.0 - cursor.start().0.0;
match cursor.item() {
Some(Transform::Isomorphic(_)) => {
@@ -803,7 +803,7 @@ impl InlaySnapshot {
let mut cursor = self
.transforms
.cursor::<(InlayPoint, (InlayOffset, Point))>(&());
cursor.seek(&point, Bias::Right, &());
cursor.seek(&point, Bias::Right);
let overshoot = point.0 - cursor.start().0.0;
match cursor.item() {
Some(Transform::Isomorphic(_)) => {
@@ -822,7 +822,7 @@ impl InlaySnapshot {
}
pub fn to_buffer_point(&self, point: InlayPoint) -> Point {
let mut cursor = self.transforms.cursor::<(InlayPoint, Point)>(&());
cursor.seek(&point, Bias::Right, &());
cursor.seek(&point, Bias::Right);
match cursor.item() {
Some(Transform::Isomorphic(_)) => {
let overshoot = point.0 - cursor.start().0.0;
@@ -834,7 +834,7 @@ impl InlaySnapshot {
}
pub fn to_buffer_offset(&self, offset: InlayOffset) -> usize {
let mut cursor = self.transforms.cursor::<(InlayOffset, usize)>(&());
cursor.seek(&offset, Bias::Right, &());
cursor.seek(&offset, Bias::Right);
match cursor.item() {
Some(Transform::Isomorphic(_)) => {
let overshoot = offset - cursor.start().0;
@@ -847,19 +847,19 @@ impl InlaySnapshot {
pub fn to_inlay_offset(&self, offset: usize) -> InlayOffset {
let mut cursor = self.transforms.cursor::<(usize, InlayOffset)>(&());
cursor.seek(&offset, Bias::Left, &());
cursor.seek(&offset, Bias::Left);
loop {
match cursor.item() {
Some(Transform::Isomorphic(_)) => {
if offset == cursor.end(&()).0 {
if offset == cursor.end().0 {
while let Some(Transform::Inlay(inlay)) = cursor.next_item() {
if inlay.position.bias() == Bias::Right {
break;
} else {
cursor.next(&());
cursor.next();
}
}
return cursor.end(&()).1;
return cursor.end().1;
} else {
let overshoot = offset - cursor.start().0;
return InlayOffset(cursor.start().1.0 + overshoot);
@@ -867,7 +867,7 @@ impl InlaySnapshot {
}
Some(Transform::Inlay(inlay)) => {
if inlay.position.bias() == Bias::Left {
cursor.next(&());
cursor.next();
} else {
return cursor.start().1;
}
@@ -880,19 +880,19 @@ impl InlaySnapshot {
}
pub fn to_inlay_point(&self, point: Point) -> InlayPoint {
let mut cursor = self.transforms.cursor::<(Point, InlayPoint)>(&());
cursor.seek(&point, Bias::Left, &());
cursor.seek(&point, Bias::Left);
loop {
match cursor.item() {
Some(Transform::Isomorphic(_)) => {
if point == cursor.end(&()).0 {
if point == cursor.end().0 {
while let Some(Transform::Inlay(inlay)) = cursor.next_item() {
if inlay.position.bias() == Bias::Right {
break;
} else {
cursor.next(&());
cursor.next();
}
}
return cursor.end(&()).1;
return cursor.end().1;
} else {
let overshoot = point - cursor.start().0;
return InlayPoint(cursor.start().1.0 + overshoot);
@@ -900,7 +900,7 @@ impl InlaySnapshot {
}
Some(Transform::Inlay(inlay)) => {
if inlay.position.bias() == Bias::Left {
cursor.next(&());
cursor.next();
} else {
return cursor.start().1;
}
@@ -914,7 +914,7 @@ impl InlaySnapshot {
pub fn clip_point(&self, mut point: InlayPoint, mut bias: Bias) -> InlayPoint {
let mut cursor = self.transforms.cursor::<(InlayPoint, Point)>(&());
cursor.seek(&point, Bias::Left, &());
cursor.seek(&point, Bias::Left);
loop {
match cursor.item() {
Some(Transform::Isomorphic(transform)) => {
@@ -923,7 +923,7 @@ impl InlaySnapshot {
if inlay.position.bias() == Bias::Left {
return point;
} else if bias == Bias::Left {
cursor.prev(&());
cursor.prev();
} else if transform.first_line_chars == 0 {
point.0 += Point::new(1, 0);
} else {
@@ -932,12 +932,12 @@ impl InlaySnapshot {
} else {
return point;
}
} else if cursor.end(&()).0 == point {
} else if cursor.end().0 == point {
if let Some(Transform::Inlay(inlay)) = cursor.next_item() {
if inlay.position.bias() == Bias::Right {
return point;
} else if bias == Bias::Right {
cursor.next(&());
cursor.next();
} else if point.0.column == 0 {
point.0.row -= 1;
point.0.column = self.line_len(point.0.row);
@@ -970,7 +970,7 @@ impl InlaySnapshot {
}
_ => return point,
}
} else if point == cursor.end(&()).0 && inlay.position.bias() == Bias::Left {
} else if point == cursor.end().0 && inlay.position.bias() == Bias::Left {
match cursor.next_item() {
Some(Transform::Inlay(inlay)) => {
if inlay.position.bias() == Bias::Right {
@@ -983,9 +983,9 @@ impl InlaySnapshot {
if bias == Bias::Left {
point = cursor.start().0;
cursor.prev(&());
cursor.prev();
} else {
cursor.next(&());
cursor.next();
point = cursor.start().0;
}
}
@@ -993,9 +993,9 @@ impl InlaySnapshot {
bias = bias.invert();
if bias == Bias::Left {
point = cursor.start().0;
cursor.prev(&());
cursor.prev();
} else {
cursor.next(&());
cursor.next();
point = cursor.start().0;
}
}
@@ -1011,7 +1011,7 @@ impl InlaySnapshot {
let mut summary = TextSummary::default();
let mut cursor = self.transforms.cursor::<(InlayOffset, usize)>(&());
cursor.seek(&range.start, Bias::Right, &());
cursor.seek(&range.start, Bias::Right);
let overshoot = range.start.0 - cursor.start().0.0;
match cursor.item() {
@@ -1019,22 +1019,22 @@ impl InlaySnapshot {
let buffer_start = cursor.start().1;
let suffix_start = buffer_start + overshoot;
let suffix_end =
buffer_start + (cmp::min(cursor.end(&()).0, range.end).0 - cursor.start().0.0);
buffer_start + (cmp::min(cursor.end().0, range.end).0 - cursor.start().0.0);
summary = self.buffer.text_summary_for_range(suffix_start..suffix_end);
cursor.next(&());
cursor.next();
}
Some(Transform::Inlay(inlay)) => {
let suffix_start = overshoot;
let suffix_end = cmp::min(cursor.end(&()).0, range.end).0 - cursor.start().0.0;
let suffix_end = cmp::min(cursor.end().0, range.end).0 - cursor.start().0.0;
summary = inlay.text.cursor(suffix_start).summary(suffix_end);
cursor.next(&());
cursor.next();
}
None => {}
}
if range.end > cursor.start().0 {
summary += cursor
.summary::<_, TransformSummary>(&range.end, Bias::Right, &())
.summary::<_, TransformSummary>(&range.end, Bias::Right)
.output;
let overshoot = range.end.0 - cursor.start().0.0;
@@ -1060,7 +1060,7 @@ impl InlaySnapshot {
pub fn row_infos(&self, row: u32) -> InlayBufferRows<'_> {
let mut cursor = self.transforms.cursor::<(InlayPoint, Point)>(&());
let inlay_point = InlayPoint::new(row, 0);
cursor.seek(&inlay_point, Bias::Left, &());
cursor.seek(&inlay_point, Bias::Left);
let max_buffer_row = self.buffer.max_row();
let mut buffer_point = cursor.start().1;
@@ -1101,7 +1101,7 @@ impl InlaySnapshot {
highlights: Highlights<'a>,
) -> InlayChunks<'a> {
let mut cursor = self.transforms.cursor::<(InlayOffset, usize)>(&());
cursor.seek(&range.start, Bias::Right, &());
cursor.seek(&range.start, Bias::Right);
let buffer_range = self.to_buffer_offset(range.start)..self.to_buffer_offset(range.end);
let buffer_chunks = CustomHighlightsChunks::new(

View File

@@ -72,7 +72,7 @@ pub struct WrapRows<'a> {
impl WrapRows<'_> {
pub(crate) fn seek(&mut self, start_row: u32) {
self.transforms
.seek(&WrapPoint::new(start_row, 0), Bias::Left, &());
.seek(&WrapPoint::new(start_row, 0), Bias::Left);
let mut input_row = self.transforms.start().1.row();
if self.transforms.item().map_or(false, |t| t.is_isomorphic()) {
input_row += start_row - self.transforms.start().0.row();
@@ -340,7 +340,7 @@ impl WrapSnapshot {
let mut tab_edits_iter = tab_edits.iter().peekable();
new_transforms =
old_cursor.slice(&tab_edits_iter.peek().unwrap().old.start, Bias::Right, &());
old_cursor.slice(&tab_edits_iter.peek().unwrap().old.start, Bias::Right);
while let Some(edit) = tab_edits_iter.next() {
if edit.new.start > TabPoint::from(new_transforms.summary().input.lines) {
@@ -356,31 +356,29 @@ impl WrapSnapshot {
));
}
old_cursor.seek_forward(&edit.old.end, Bias::Right, &());
old_cursor.seek_forward(&edit.old.end, Bias::Right);
if let Some(next_edit) = tab_edits_iter.peek() {
if next_edit.old.start > old_cursor.end(&()) {
if old_cursor.end(&()) > edit.old.end {
if next_edit.old.start > old_cursor.end() {
if old_cursor.end() > edit.old.end {
let summary = self
.tab_snapshot
.text_summary_for_range(edit.old.end..old_cursor.end(&()));
.text_summary_for_range(edit.old.end..old_cursor.end());
new_transforms.push_or_extend(Transform::isomorphic(summary));
}
old_cursor.next(&());
new_transforms.append(
old_cursor.slice(&next_edit.old.start, Bias::Right, &()),
&(),
);
old_cursor.next();
new_transforms
.append(old_cursor.slice(&next_edit.old.start, Bias::Right), &());
}
} else {
if old_cursor.end(&()) > edit.old.end {
if old_cursor.end() > edit.old.end {
let summary = self
.tab_snapshot
.text_summary_for_range(edit.old.end..old_cursor.end(&()));
.text_summary_for_range(edit.old.end..old_cursor.end());
new_transforms.push_or_extend(Transform::isomorphic(summary));
}
old_cursor.next(&());
new_transforms.append(old_cursor.suffix(&()), &());
old_cursor.next();
new_transforms.append(old_cursor.suffix(), &());
}
}
}
@@ -441,7 +439,6 @@ impl WrapSnapshot {
new_transforms = old_cursor.slice(
&TabPoint::new(row_edits.peek().unwrap().old_rows.start, 0),
Bias::Right,
&(),
);
while let Some(edit) = row_edits.next() {
@@ -516,34 +513,31 @@ impl WrapSnapshot {
}
new_transforms.extend(edit_transforms, &());
old_cursor.seek_forward(&TabPoint::new(edit.old_rows.end, 0), Bias::Right, &());
old_cursor.seek_forward(&TabPoint::new(edit.old_rows.end, 0), Bias::Right);
if let Some(next_edit) = row_edits.peek() {
if next_edit.old_rows.start > old_cursor.end(&()).row() {
if old_cursor.end(&()) > TabPoint::new(edit.old_rows.end, 0) {
if next_edit.old_rows.start > old_cursor.end().row() {
if old_cursor.end() > TabPoint::new(edit.old_rows.end, 0) {
let summary = self.tab_snapshot.text_summary_for_range(
TabPoint::new(edit.old_rows.end, 0)..old_cursor.end(&()),
TabPoint::new(edit.old_rows.end, 0)..old_cursor.end(),
);
new_transforms.push_or_extend(Transform::isomorphic(summary));
}
old_cursor.next(&());
old_cursor.next();
new_transforms.append(
old_cursor.slice(
&TabPoint::new(next_edit.old_rows.start, 0),
Bias::Right,
&(),
),
old_cursor
.slice(&TabPoint::new(next_edit.old_rows.start, 0), Bias::Right),
&(),
);
}
} else {
if old_cursor.end(&()) > TabPoint::new(edit.old_rows.end, 0) {
if old_cursor.end() > TabPoint::new(edit.old_rows.end, 0) {
let summary = self.tab_snapshot.text_summary_for_range(
TabPoint::new(edit.old_rows.end, 0)..old_cursor.end(&()),
TabPoint::new(edit.old_rows.end, 0)..old_cursor.end(),
);
new_transforms.push_or_extend(Transform::isomorphic(summary));
}
old_cursor.next(&());
new_transforms.append(old_cursor.suffix(&()), &());
old_cursor.next();
new_transforms.append(old_cursor.suffix(), &());
}
}
}
@@ -570,19 +564,19 @@ impl WrapSnapshot {
tab_edit.new.start.0.column = 0;
tab_edit.new.end.0 += Point::new(1, 0);
old_cursor.seek(&tab_edit.old.start, Bias::Right, &());
old_cursor.seek(&tab_edit.old.start, Bias::Right);
let mut old_start = old_cursor.start().output.lines;
old_start += tab_edit.old.start.0 - old_cursor.start().input.lines;
old_cursor.seek(&tab_edit.old.end, Bias::Right, &());
old_cursor.seek(&tab_edit.old.end, Bias::Right);
let mut old_end = old_cursor.start().output.lines;
old_end += tab_edit.old.end.0 - old_cursor.start().input.lines;
new_cursor.seek(&tab_edit.new.start, Bias::Right, &());
new_cursor.seek(&tab_edit.new.start, Bias::Right);
let mut new_start = new_cursor.start().output.lines;
new_start += tab_edit.new.start.0 - new_cursor.start().input.lines;
new_cursor.seek(&tab_edit.new.end, Bias::Right, &());
new_cursor.seek(&tab_edit.new.end, Bias::Right);
let mut new_end = new_cursor.start().output.lines;
new_end += tab_edit.new.end.0 - new_cursor.start().input.lines;
@@ -605,7 +599,7 @@ impl WrapSnapshot {
let output_start = WrapPoint::new(rows.start, 0);
let output_end = WrapPoint::new(rows.end, 0);
let mut transforms = self.transforms.cursor::<(WrapPoint, TabPoint)>(&());
transforms.seek(&output_start, Bias::Right, &());
transforms.seek(&output_start, Bias::Right);
let mut input_start = TabPoint(transforms.start().1.0);
if transforms.item().map_or(false, |t| t.is_isomorphic()) {
input_start.0 += output_start.0 - transforms.start().0.0;
@@ -633,7 +627,7 @@ impl WrapSnapshot {
pub fn line_len(&self, row: u32) -> u32 {
let mut cursor = self.transforms.cursor::<(WrapPoint, TabPoint)>(&());
cursor.seek(&WrapPoint::new(row + 1, 0), Bias::Left, &());
cursor.seek(&WrapPoint::new(row + 1, 0), Bias::Left);
if cursor
.item()
.map_or(false, |transform| transform.is_isomorphic())
@@ -658,10 +652,10 @@ impl WrapSnapshot {
let end = WrapPoint::new(rows.end, 0);
let mut cursor = self.transforms.cursor::<(WrapPoint, TabPoint)>(&());
cursor.seek(&start, Bias::Right, &());
cursor.seek(&start, Bias::Right);
if let Some(transform) = cursor.item() {
let start_in_transform = start.0 - cursor.start().0.0;
let end_in_transform = cmp::min(end, cursor.end(&()).0).0 - cursor.start().0.0;
let end_in_transform = cmp::min(end, cursor.end().0).0 - cursor.start().0.0;
if transform.is_isomorphic() {
let tab_start = TabPoint(cursor.start().1.0 + start_in_transform);
let tab_end = TabPoint(cursor.start().1.0 + end_in_transform);
@@ -678,12 +672,12 @@ impl WrapSnapshot {
};
}
cursor.next(&());
cursor.next();
}
if rows.end > cursor.start().0.row() {
summary += &cursor
.summary::<_, TransformSummary>(&WrapPoint::new(rows.end, 0), Bias::Right, &())
.summary::<_, TransformSummary>(&WrapPoint::new(rows.end, 0), Bias::Right)
.output;
if let Some(transform) = cursor.item() {
@@ -712,7 +706,7 @@ impl WrapSnapshot {
pub fn soft_wrap_indent(&self, row: u32) -> Option<u32> {
let mut cursor = self.transforms.cursor::<WrapPoint>(&());
cursor.seek(&WrapPoint::new(row + 1, 0), Bias::Right, &());
cursor.seek(&WrapPoint::new(row + 1, 0), Bias::Right);
cursor.item().and_then(|transform| {
if transform.is_isomorphic() {
None
@@ -728,7 +722,7 @@ impl WrapSnapshot {
pub fn row_infos(&self, start_row: u32) -> WrapRows<'_> {
let mut transforms = self.transforms.cursor::<(WrapPoint, TabPoint)>(&());
transforms.seek(&WrapPoint::new(start_row, 0), Bias::Left, &());
transforms.seek(&WrapPoint::new(start_row, 0), Bias::Left);
let mut input_row = transforms.start().1.row();
if transforms.item().map_or(false, |t| t.is_isomorphic()) {
input_row += start_row - transforms.start().0.row();
@@ -748,7 +742,7 @@ impl WrapSnapshot {
pub fn to_tab_point(&self, point: WrapPoint) -> TabPoint {
let mut cursor = self.transforms.cursor::<(WrapPoint, TabPoint)>(&());
cursor.seek(&point, Bias::Right, &());
cursor.seek(&point, Bias::Right);
let mut tab_point = cursor.start().1.0;
if cursor.item().map_or(false, |t| t.is_isomorphic()) {
tab_point += point.0 - cursor.start().0.0;
@@ -766,14 +760,14 @@ impl WrapSnapshot {
pub fn tab_point_to_wrap_point(&self, point: TabPoint) -> WrapPoint {
let mut cursor = self.transforms.cursor::<(TabPoint, WrapPoint)>(&());
cursor.seek(&point, Bias::Right, &());
cursor.seek(&point, Bias::Right);
WrapPoint(cursor.start().1.0 + (point.0 - cursor.start().0.0))
}
pub fn clip_point(&self, mut point: WrapPoint, bias: Bias) -> WrapPoint {
if bias == Bias::Left {
let mut cursor = self.transforms.cursor::<WrapPoint>(&());
cursor.seek(&point, Bias::Right, &());
cursor.seek(&point, Bias::Right);
if cursor.item().map_or(false, |t| !t.is_isomorphic()) {
point = *cursor.start();
*point.column_mut() -= 1;
@@ -791,16 +785,16 @@ impl WrapSnapshot {
*point.column_mut() = 0;
let mut cursor = self.transforms.cursor::<(WrapPoint, TabPoint)>(&());
cursor.seek(&point, Bias::Right, &());
cursor.seek(&point, Bias::Right);
if cursor.item().is_none() {
cursor.prev(&());
cursor.prev();
}
while let Some(transform) = cursor.item() {
if transform.is_isomorphic() && cursor.start().1.column() == 0 {
return cmp::min(cursor.end(&()).0.row(), point.row());
return cmp::min(cursor.end().0.row(), point.row());
} else {
cursor.prev(&());
cursor.prev();
}
}
@@ -811,12 +805,12 @@ impl WrapSnapshot {
point.0 += Point::new(1, 0);
let mut cursor = self.transforms.cursor::<(WrapPoint, TabPoint)>(&());
cursor.seek(&point, Bias::Right, &());
cursor.seek(&point, Bias::Right);
while let Some(transform) = cursor.item() {
if transform.is_isomorphic() && cursor.start().1.column() == 0 {
return Some(cmp::max(cursor.start().0.row(), point.row()));
} else {
cursor.next(&());
cursor.next();
}
}
@@ -889,7 +883,7 @@ impl WrapChunks<'_> {
pub(crate) fn seek(&mut self, rows: Range<u32>) {
let output_start = WrapPoint::new(rows.start, 0);
let output_end = WrapPoint::new(rows.end, 0);
self.transforms.seek(&output_start, Bias::Right, &());
self.transforms.seek(&output_start, Bias::Right);
let mut input_start = TabPoint(self.transforms.start().1.0);
if self.transforms.item().map_or(false, |t| t.is_isomorphic()) {
input_start.0 += output_start.0 - self.transforms.start().0.0;
@@ -930,7 +924,7 @@ impl<'a> Iterator for WrapChunks<'a> {
}
self.output_position.0 += summary;
self.transforms.next(&());
self.transforms.next();
return Some(Chunk {
text: &display_text[start_ix..end_ix],
..Default::default()
@@ -942,7 +936,7 @@ impl<'a> Iterator for WrapChunks<'a> {
}
let mut input_len = 0;
let transform_end = self.transforms.end(&()).0;
let transform_end = self.transforms.end().0;
for c in self.input_chunk.text.chars() {
let char_len = c.len_utf8();
input_len += char_len;
@@ -954,7 +948,7 @@ impl<'a> Iterator for WrapChunks<'a> {
}
if self.output_position >= transform_end {
self.transforms.next(&());
self.transforms.next();
break;
}
}
@@ -982,7 +976,7 @@ impl Iterator for WrapRows<'_> {
self.output_row += 1;
self.transforms
.seek_forward(&WrapPoint::new(self.output_row, 0), Bias::Left, &());
.seek_forward(&WrapPoint::new(self.output_row, 0), Bias::Left);
if self.transforms.item().map_or(false, |t| t.is_isomorphic()) {
self.input_buffer_row = self.input_buffer_rows.next().unwrap();
self.soft_wrapped = false;

View File

@@ -109,10 +109,10 @@ use inline_completion::{EditPredictionProvider, InlineCompletionProviderHandle};
pub use items::MAX_TAB_TITLE_LEN;
use itertools::Itertools;
use language::{
AutoindentMode, BracketMatch, BracketPair, Buffer, Capability, CharKind, CodeLabel,
CursorShape, DiagnosticEntry, DiffOptions, DocumentationConfig, EditPredictionsMode,
EditPreview, HighlightedText, IndentKind, IndentSize, Language, OffsetRangeExt, Point,
Selection, SelectionGoal, TextObject, TransactionId, TreeSitterOptions, WordsQuery,
AutoindentMode, BlockCommentConfig, BracketMatch, BracketPair, Buffer, Capability, CharKind,
CodeLabel, CursorShape, DiagnosticEntry, DiffOptions, EditPredictionsMode, EditPreview,
HighlightedText, IndentKind, IndentSize, Language, OffsetRangeExt, Point, Selection,
SelectionGoal, TextObject, TransactionId, TreeSitterOptions, WordsQuery,
language_settings::{
self, InlayHintSettings, LspInsertMode, RewrapBehavior, WordsCompletionMode,
all_language_settings, language_settings,
@@ -213,6 +213,7 @@ use workspace::{
notifications::{DetachAndPromptErr, NotificationId, NotifyTaskExt},
searchable::SearchEvent,
};
use zed_actions;
use crate::{
code_context_menus::CompletionsMenuSource,
@@ -950,6 +951,7 @@ struct InlineBlamePopover {
hide_task: Option<Task<()>>,
popover_bounds: Option<Bounds<Pixels>>,
popover_state: InlineBlamePopoverState,
keyboard_grace: bool,
}
enum SelectionDragState {
@@ -4406,7 +4408,9 @@ impl Editor {
})
.max_by_key(|(_, len)| *len)?;
if let Some((block_start, _)) = language.block_comment_delimiters()
if let Some(BlockCommentConfig {
start: block_start, ..
}) = language.block_comment()
{
let block_start_trimmed = block_start.trim_end();
if block_start_trimmed.starts_with(delimiter.trim_end()) {
@@ -4443,13 +4447,12 @@ impl Editor {
return None;
}
let DocumentationConfig {
let BlockCommentConfig {
start: start_tag,
end: end_tag,
prefix: delimiter,
tab_size: len,
} = language.documentation()?;
} = language.documentation_comment()?;
let is_within_block_comment = buffer
.language_scope_at(start_point)
.is_some_and(|scope| scope.override_name() == Some("comment"));
@@ -4519,7 +4522,7 @@ impl Editor {
let cursor_is_at_start_of_end_tag =
column == end_tag_offset;
if cursor_is_at_start_of_end_tag {
indent_on_extra_newline.len = (*len).into();
indent_on_extra_newline.len = *len;
}
}
cursor_is_before_end_tag
@@ -4532,7 +4535,7 @@ impl Editor {
&& cursor_is_before_end_tag_if_exists
{
if cursor_is_after_start_tag {
indent_on_newline.len = (*len).into();
indent_on_newline.len = *len;
}
Some(delimiter.clone())
} else {
@@ -6517,21 +6520,55 @@ impl Editor {
}
}
pub fn blame_hover(&mut self, _: &BlameHover, window: &mut Window, cx: &mut Context<Self>) {
let snapshot = self.snapshot(window, cx);
let cursor = self.selections.newest::<Point>(cx).head();
let Some((buffer, point, _)) = snapshot.buffer_snapshot.point_to_buffer_point(cursor)
else {
return;
};
let Some(blame) = self.blame.as_ref() else {
return;
};
let row_info = RowInfo {
buffer_id: Some(buffer.remote_id()),
buffer_row: Some(point.row),
..Default::default()
};
let Some(blame_entry) = blame
.update(cx, |blame, cx| blame.blame_for_rows(&[row_info], cx).next())
.flatten()
else {
return;
};
let anchor = self.selections.newest_anchor().head();
let position = self.to_pixel_point(anchor, &snapshot, window);
if let (Some(position), Some(last_bounds)) = (position, self.last_bounds) {
self.show_blame_popover(&blame_entry, position + last_bounds.origin, true, cx);
};
}
fn show_blame_popover(
&mut self,
blame_entry: &BlameEntry,
position: gpui::Point<Pixels>,
ignore_timeout: bool,
cx: &mut Context<Self>,
) {
if let Some(state) = &mut self.inline_blame_popover {
state.hide_task.take();
} else {
let delay = EditorSettings::get_global(cx).hover_popover_delay;
let blame_popover_delay = EditorSettings::get_global(cx).hover_popover_delay;
let blame_entry = blame_entry.clone();
let show_task = cx.spawn(async move |editor, cx| {
cx.background_executor()
.timer(std::time::Duration::from_millis(delay))
.await;
if !ignore_timeout {
cx.background_executor()
.timer(std::time::Duration::from_millis(blame_popover_delay))
.await;
}
editor
.update(cx, |editor, cx| {
editor.inline_blame_popover_show_task.take();
@@ -6560,6 +6597,7 @@ impl Editor {
commit_message: details,
markdown,
},
keyboard_grace: ignore_timeout,
});
cx.notify();
})
@@ -10840,17 +10878,6 @@ impl Editor {
});
}
pub fn toggle_case(&mut self, _: &ToggleCase, window: &mut Window, cx: &mut Context<Self>) {
self.manipulate_text(window, cx, |text| {
let has_upper_case_characters = text.chars().any(|c| c.is_uppercase());
if has_upper_case_characters {
text.to_lowercase()
} else {
text.to_uppercase()
}
})
}
fn manipulate_immutable_lines<Fn>(
&mut self,
window: &mut Window,
@@ -11106,6 +11133,26 @@ impl Editor {
})
}
pub fn convert_to_sentence_case(
&mut self,
_: &ConvertToSentenceCase,
window: &mut Window,
cx: &mut Context<Self>,
) {
self.manipulate_text(window, cx, |text| text.to_case(Case::Sentence))
}
pub fn toggle_case(&mut self, _: &ToggleCase, window: &mut Window, cx: &mut Context<Self>) {
self.manipulate_text(window, cx, |text| {
let has_upper_case_characters = text.chars().any(|c| c.is_uppercase());
if has_upper_case_characters {
text.to_lowercase()
} else {
text.to_uppercase()
}
})
}
pub fn convert_to_rot13(
&mut self,
_: &ConvertToRot13,
@@ -12118,6 +12165,41 @@ impl Editor {
});
}
pub fn diff_clipboard_with_selection(
&mut self,
_: &DiffClipboardWithSelection,
window: &mut Window,
cx: &mut Context<Self>,
) {
let selections = self.selections.all::<usize>(cx);
if selections.is_empty() {
log::warn!("There should always be at least one selection in Zed. This is a bug.");
return;
};
let clipboard_text = match cx.read_from_clipboard() {
Some(item) => match item.entries().first() {
Some(ClipboardEntry::String(text)) => Some(text.text().to_string()),
_ => None,
},
None => None,
};
let Some(clipboard_text) = clipboard_text else {
log::warn!("Clipboard doesn't contain text.");
return;
};
window.dispatch_action(
Box::new(DiffClipboardWithSelectionData {
clipboard_text,
editor: cx.entity(),
}),
cx,
);
}
pub fn paste(&mut self, _: &Paste, window: &mut Window, cx: &mut Context<Self>) {
self.hide_mouse_cursor(HideMouseCursorOrigin::TypingAction, cx);
if let Some(item) = cx.read_from_clipboard() {
@@ -14277,8 +14359,11 @@ impl Editor {
(position..position, first_prefix.clone())
}));
}
} else if let Some((full_comment_prefix, comment_suffix)) =
language.block_comment_delimiters()
} else if let Some(BlockCommentConfig {
start: full_comment_prefix,
end: comment_suffix,
..
}) = language.block_comment()
{
let comment_prefix = full_comment_prefix.trim_end_matches(' ');
let comment_prefix_whitespace = &full_comment_prefix[comment_prefix.len()..];
@@ -16892,7 +16977,7 @@ impl Editor {
now: Instant,
window: &mut Window,
cx: &mut Context<Self>,
) {
) -> Option<TransactionId> {
self.end_selection(window, cx);
if let Some(tx_id) = self
.buffer
@@ -16902,7 +16987,10 @@ impl Editor {
.insert_transaction(tx_id, self.selections.disjoint_anchors());
cx.emit(EditorEvent::TransactionBegun {
transaction_id: tx_id,
})
});
Some(tx_id)
} else {
None
}
}
@@ -16930,6 +17018,17 @@ impl Editor {
}
}
pub fn modify_transaction_selection_history(
&mut self,
transaction_id: TransactionId,
modify: impl FnOnce(&mut (Arc<[Selection<Anchor>]>, Option<Arc<[Selection<Anchor>]>>)),
) -> bool {
self.selection_history
.transaction_mut(transaction_id)
.map(modify)
.is_some()
}
pub fn set_mark(&mut self, _: &actions::SetMark, window: &mut Window, cx: &mut Context<Self>) {
if self.selection_mark_mode {
self.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
@@ -22182,7 +22281,7 @@ fn consume_contiguous_rows(
selections: &mut Peekable<std::slice::Iter<Selection<Point>>>,
) -> (MultiBufferRow, MultiBufferRow) {
contiguous_row_selections.push(selection.clone());
let start_row = MultiBufferRow(selection.start.row);
let start_row = starting_row(selection, display_map);
let mut end_row = ending_row(selection, display_map);
while let Some(next_selection) = selections.peek() {
@@ -22196,6 +22295,14 @@ fn consume_contiguous_rows(
(start_row, end_row)
}
fn starting_row(selection: &Selection<Point>, display_map: &DisplaySnapshot) -> MultiBufferRow {
if selection.start.column > 0 {
MultiBufferRow(display_map.prev_line_boundary(selection.start).0.row)
} else {
MultiBufferRow(selection.start.row)
}
}
fn ending_row(next_selection: &Selection<Point>, display_map: &DisplaySnapshot) -> MultiBufferRow {
if next_selection.end.column > 0 || next_selection.is_empty() {
MultiBufferRow(display_map.next_line_boundary(next_selection.end).0.row + 1)

View File

@@ -2875,11 +2875,11 @@ async fn test_newline_documentation_comments(cx: &mut TestAppContext) {
let language = Arc::new(
Language::new(
LanguageConfig {
documentation: Some(language::DocumentationConfig {
documentation_comment: Some(language::BlockCommentConfig {
start: "/**".into(),
end: "*/".into(),
prefix: "* ".into(),
tab_size: NonZeroU32::new(1).unwrap(),
tab_size: 1,
}),
..LanguageConfig::default()
@@ -3089,7 +3089,12 @@ async fn test_newline_comments_with_block_comment(cx: &mut TestAppContext) {
let lua_language = Arc::new(Language::new(
LanguageConfig {
line_comments: vec!["--".into()],
block_comment: Some(("--[[".into(), "]]".into())),
block_comment: Some(language::BlockCommentConfig {
start: "--[[".into(),
prefix: "".into(),
end: "]]".into(),
tab_size: 0,
}),
..LanguageConfig::default()
},
None,
@@ -4719,6 +4724,23 @@ async fn test_toggle_case(cx: &mut TestAppContext) {
"});
}
#[gpui::test]
async fn test_convert_to_sentence_case(cx: &mut TestAppContext) {
init_test(cx, |_| {});
let mut cx = EditorTestContext::new(cx).await;
cx.set_state(indoc! {"
«implement-windows-supportˇ»
"});
cx.update_editor(|e, window, cx| {
e.convert_to_sentence_case(&ConvertToSentenceCase, window, cx)
});
cx.assert_editor_state(indoc! {"
«Implement windows supportˇ»
"});
}
#[gpui::test]
async fn test_manipulate_text(cx: &mut TestAppContext) {
init_test(cx, |_| {});
@@ -5064,6 +5086,33 @@ fn test_move_line_up_down(cx: &mut TestAppContext) {
});
}
#[gpui::test]
fn test_move_line_up_selection_at_end_of_fold(cx: &mut TestAppContext) {
init_test(cx, |_| {});
let editor = cx.add_window(|window, cx| {
let buffer = MultiBuffer::build_simple("\n\n\n\n\n\naaaa\nbbbb\ncccc", cx);
build_editor(buffer, window, cx)
});
_ = editor.update(cx, |editor, window, cx| {
editor.fold_creases(
vec![Crease::simple(
Point::new(6, 4)..Point::new(7, 4),
FoldPlaceholder::test(),
)],
true,
window,
cx,
);
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
s.select_ranges([Point::new(7, 4)..Point::new(7, 4)])
});
assert_eq!(editor.display_text(cx), "\n\n\n\n\n\naaaa⋯\ncccc");
editor.move_line_up(&MoveLineUp, window, cx);
let buffer_text = editor.buffer.read(cx).snapshot(cx).text();
assert_eq!(buffer_text, "\n\n\n\n\naaaa\nbbbb\n\ncccc");
});
}
#[gpui::test]
fn test_move_line_up_down_with_blocks(cx: &mut TestAppContext) {
init_test(cx, |_| {});
@@ -9570,6 +9619,74 @@ async fn test_document_format_during_save(cx: &mut TestAppContext) {
}
}
#[gpui::test]
async fn test_redo_after_noop_format(cx: &mut TestAppContext) {
init_test(cx, |settings| {
settings.defaults.ensure_final_newline_on_save = Some(false);
});
let fs = FakeFs::new(cx.executor());
fs.insert_file(path!("/file.txt"), "foo".into()).await;
let project = Project::test(fs, [path!("/file.txt").as_ref()], cx).await;
let buffer = project
.update(cx, |project, cx| {
project.open_local_buffer(path!("/file.txt"), cx)
})
.await
.unwrap();
let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
let (editor, cx) = cx.add_window_view(|window, cx| {
build_editor_with_project(project.clone(), buffer, window, cx)
});
editor.update_in(cx, |editor, window, cx| {
editor.change_selections(SelectionEffects::default(), window, cx, |s| {
s.select_ranges([0..0])
});
});
assert!(!cx.read(|cx| editor.is_dirty(cx)));
editor.update_in(cx, |editor, window, cx| {
editor.handle_input("\n", window, cx)
});
cx.run_until_parked();
save(&editor, &project, cx).await;
assert_eq!("\nfoo", editor.read_with(cx, |editor, cx| editor.text(cx)));
editor.update_in(cx, |editor, window, cx| {
editor.undo(&Default::default(), window, cx);
});
save(&editor, &project, cx).await;
assert_eq!("foo", editor.read_with(cx, |editor, cx| editor.text(cx)));
editor.update_in(cx, |editor, window, cx| {
editor.redo(&Default::default(), window, cx);
});
cx.run_until_parked();
assert_eq!("\nfoo", editor.read_with(cx, |editor, cx| editor.text(cx)));
async fn save(editor: &Entity<Editor>, project: &Entity<Project>, cx: &mut VisualTestContext) {
let save = editor
.update_in(cx, |editor, window, cx| {
editor.save(
SaveOptions {
format: true,
autosave: false,
},
project.clone(),
window,
cx,
)
})
.unwrap();
cx.executor().start_waiting();
save.await;
assert!(!cx.read(|cx| editor.is_dirty(cx)));
}
}
#[gpui::test]
async fn test_multibuffer_format_during_save(cx: &mut TestAppContext) {
init_test(cx, |_| {});
@@ -13738,7 +13855,12 @@ async fn test_toggle_block_comment(cx: &mut TestAppContext) {
Language::new(
LanguageConfig {
name: "HTML".into(),
block_comment: Some(("<!-- ".into(), " -->".into())),
block_comment: Some(BlockCommentConfig {
start: "<!-- ".into(),
prefix: "".into(),
end: " -->".into(),
tab_size: 0,
}),
..Default::default()
},
Some(tree_sitter_html::LANGUAGE.into()),
@@ -16759,7 +16881,7 @@ async fn test_multibuffer_reverts(cx: &mut TestAppContext) {
}
#[gpui::test]
async fn test_mutlibuffer_in_navigation_history(cx: &mut TestAppContext) {
async fn test_multibuffer_in_navigation_history(cx: &mut TestAppContext) {
init_test(cx, |_| {});
let cols = 4;
@@ -22708,7 +22830,7 @@ pub(crate) fn init_test(cx: &mut TestAppContext, f: fn(&mut AllLanguageSettingsC
workspace::init_settings(cx);
crate::init(cx);
});
zlog::init_test();
update_test_language_settings(cx, f);
}

View File

@@ -216,6 +216,7 @@ impl EditorElement {
register_action(editor, window, Editor::newline_above);
register_action(editor, window, Editor::newline_below);
register_action(editor, window, Editor::backspace);
register_action(editor, window, Editor::blame_hover);
register_action(editor, window, Editor::delete);
register_action(editor, window, Editor::tab);
register_action(editor, window, Editor::backtab);
@@ -229,7 +230,6 @@ impl EditorElement {
register_action(editor, window, Editor::sort_lines_case_insensitive);
register_action(editor, window, Editor::reverse_lines);
register_action(editor, window, Editor::shuffle_lines);
register_action(editor, window, Editor::toggle_case);
register_action(editor, window, Editor::convert_indentation_to_spaces);
register_action(editor, window, Editor::convert_indentation_to_tabs);
register_action(editor, window, Editor::convert_to_upper_case);
@@ -240,6 +240,8 @@ impl EditorElement {
register_action(editor, window, Editor::convert_to_upper_camel_case);
register_action(editor, window, Editor::convert_to_lower_camel_case);
register_action(editor, window, Editor::convert_to_opposite_case);
register_action(editor, window, Editor::convert_to_sentence_case);
register_action(editor, window, Editor::toggle_case);
register_action(editor, window, Editor::convert_to_rot13);
register_action(editor, window, Editor::convert_to_rot47);
register_action(editor, window, Editor::delete_to_previous_word_start);
@@ -261,6 +263,7 @@ impl EditorElement {
register_action(editor, window, Editor::kill_ring_yank);
register_action(editor, window, Editor::copy);
register_action(editor, window, Editor::copy_and_trim);
register_action(editor, window, Editor::diff_clipboard_with_selection);
register_action(editor, window, Editor::paste);
register_action(editor, window, Editor::undo);
register_action(editor, window, Editor::redo);
@@ -1143,10 +1146,14 @@ impl EditorElement {
.as_ref()
.and_then(|state| state.popover_bounds)
.map_or(false, |bounds| bounds.contains(&event.position));
let keyboard_grace = editor
.inline_blame_popover
.as_ref()
.map_or(false, |state| state.keyboard_grace);
if mouse_over_inline_blame || mouse_over_popover {
editor.show_blame_popover(&blame_entry, event.position, cx);
} else {
editor.show_blame_popover(&blame_entry, event.position, false, cx);
} else if !keyboard_grace {
editor.hide_blame_popover(cx);
}
} else {
@@ -4004,6 +4011,7 @@ impl EditorElement {
let available_width = hitbox.bounds.size.width - right_margin;
let mut header = v_flex()
.w_full()
.relative()
.child(
div()

View File

@@ -296,7 +296,7 @@ impl GitBlame {
let row = info
.buffer_row
.filter(|_| info.buffer_id == Some(buffer_id))?;
cursor.seek_forward(&row, Bias::Right, &());
cursor.seek_forward(&row, Bias::Right);
cursor.item()?.blame.clone()
})
}
@@ -389,7 +389,7 @@ impl GitBlame {
}
}
new_entries.append(cursor.slice(&edit.old.start, Bias::Right, &()), &());
new_entries.append(cursor.slice(&edit.old.start, Bias::Right), &());
if edit.new.start > new_entries.summary().rows {
new_entries.push(
@@ -401,7 +401,7 @@ impl GitBlame {
);
}
cursor.seek(&edit.old.end, Bias::Right, &());
cursor.seek(&edit.old.end, Bias::Right);
if !edit.new.is_empty() {
new_entries.push(
GitBlameEntry {
@@ -412,7 +412,7 @@ impl GitBlame {
);
}
let old_end = cursor.end(&());
let old_end = cursor.end();
if row_edits
.peek()
.map_or(true, |next_edit| next_edit.old.start >= old_end)
@@ -421,18 +421,18 @@ impl GitBlame {
if old_end > edit.old.end {
new_entries.push(
GitBlameEntry {
rows: cursor.end(&()) - edit.old.end,
rows: cursor.end() - edit.old.end,
blame: entry.blame.clone(),
},
&(),
);
}
cursor.next(&());
cursor.next();
}
}
}
new_entries.append(cursor.suffix(&()), &());
new_entries.append(cursor.suffix(), &());
drop(cursor);
self.buffer_snapshot = new_snapshot;

View File

@@ -14,7 +14,8 @@ use futures::Future;
use gpui::{Context, Entity, Focusable as _, VisualTestContext, Window};
use indoc::indoc;
use language::{
FakeLspAdapter, Language, LanguageConfig, LanguageMatcher, LanguageQueries, point_to_lsp,
BlockCommentConfig, FakeLspAdapter, Language, LanguageConfig, LanguageMatcher, LanguageQueries,
point_to_lsp,
};
use lsp::{notification, request};
use multi_buffer::ToPointUtf16;
@@ -269,7 +270,12 @@ impl EditorLspTestContext {
path_suffixes: vec!["html".into()],
..Default::default()
},
block_comment: Some(("<!-- ".into(), " -->".into())),
block_comment: Some(BlockCommentConfig {
start: "<!--".into(),
prefix: "".into(),
end: "-->".into(),
tab_size: 0,
}),
completion_query_characters: ['-'].into_iter().collect(),
..Default::default()
},

View File

@@ -3,7 +3,7 @@ mod dap;
mod lsp;
mod slash_command;
use std::ops::Range;
use std::{ops::Range, path::PathBuf};
use util::redact::should_redact;
@@ -18,7 +18,7 @@ pub type EnvVars = Vec<(String, String)>;
/// A command.
pub struct Command {
/// The command to execute.
pub command: String,
pub command: PathBuf,
/// The arguments to pass to the command.
pub args: Vec<String>,
/// The environment variables to set for the command.

View File

@@ -75,7 +75,7 @@ impl From<Range> for std::ops::Range<usize> {
impl From<Command> for extension::Command {
fn from(value: Command) -> Self {
Self {
command: value.command,
command: value.command.into(),
args: value.args,
env: value.env,
}
@@ -958,7 +958,7 @@ impl ExtensionImports for WasmState {
command,
} => Ok(serde_json::to_string(&settings::ContextServerSettings {
command: Some(settings::CommandSettings {
path: Some(command.path),
path: command.path.to_str().map(|path| path.to_string()),
arguments: Some(command.args),
env: command.env.map(|env| env.into_iter().collect()),
}),

View File

@@ -85,6 +85,11 @@ impl FeatureFlag for ThreadAutoCaptureFeatureFlag {
false
}
}
pub struct PanicFeatureFlag;
impl FeatureFlag for PanicFeatureFlag {
const NAME: &'static str = "panic";
}
pub struct JjUiFeatureFlag {}

View File

@@ -23,6 +23,7 @@ askpass.workspace = true
buffer_diff.workspace = true
call.workspace = true
chrono.workspace = true
client.workspace = true
collections.workspace = true
command_palette_hooks.workspace = true
component.workspace = true

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