Compare commits

..

44 Commits

Author SHA1 Message Date
Oleksiy Syvokon
585110b2cb Register agent actions 2025-07-22 17:46:54 +03:00
Oleksiy Syvokon
4059a21422 vim: Use d p and d u for rejecting/keeping agent edits 2025-07-22 13:35:22 +03: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
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
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
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
Peter Tripp
5f92ac25a7 docs: Consolidate backend setup docs into local-collaboration.md (#34653)
Simplify docs for mac/linux/windows by consolidating the backend
dependencies (collaboration) docs into local-collaboration.md. Most
users building zed will not need to do this -- streamline them into
getting setup to build the zed client app first.

Release Notes:

- N/A
2025-07-19 12:01:33 -04:00
Peter Tripp
fb88de9223 Fix error in OpenRouter svg logo (#34764)
Fix a spurious error in Zed logs from the OpenRouter svg Logo introduced
in https://github.com/zed-industries/zed/pull/29496:

```log
WARN  [usvg::parser::svgtree] Failed to parse clip-path value: 'url(#clip0_205_3)'.
```

Release Notes:

- N/A
2025-07-19 16:01:18 +00:00
Oleksandr Mykhailenko
29111304dd agent: Fix Mistral tool use error message (#34692)
Closes #32675

Exactly the same changes as in #33640 by @sviande

The PR has been in WIP state for 3 weeks with no activity, and the issue
basically makes Mistral models unusable. I have tested the changes
locally, and it does indeed work. Full credit goes to @sviande, I just
want this feature to be finished.

Release Notes:

- agent: Fixed an issue with tool calling with the Mistral provider
(thanks [@sviande](https://github.com/sviande) and
[@armyhaylenko](https://github.com/armyhaylenko))

Co-authored-by: sviande <sviande@gmail.com>
2025-07-19 11:59:57 -04:00
Vitaly Slobodin
0ffd93774c Fix Tailwind support for HTML/ERB files (#34743)
Closes #27118
Closes #34165

Fix a small issue after we landed
https://github.com/zed-extensions/ruby/pull/113+ where we introduced
`HTML/ERB` and `YAML/ERB` language IDs to improve user experience. Sorry
about that. Thanks!

Release Notes:

- N/A
2025-07-19 11:06:35 -04:00
Mikayla Maki
2da2ae65a0 gpui: Add use state APIs (#34741)
This PR adds a component level state API to GPUI, as well as a few
utilities for simplified interactions with entities

Release Notes:

- N/A
2025-07-19 01:27:54 +00:00
132 changed files with 4177 additions and 1358 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]

42
Cargo.lock generated
View File

@@ -150,7 +150,9 @@ dependencies = [
"indoc",
"itertools 0.14.0",
"language",
"libc",
"log",
"nix 0.29.0",
"paths",
"project",
"schemars",
@@ -162,6 +164,7 @@ dependencies = [
"tempfile",
"ui",
"util",
"uuid",
"watch",
"which 6.0.3",
"workspace-hack",
@@ -279,9 +282,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",
@@ -7396,9 +7399,9 @@ dependencies = [
[[package]]
name = "grid"
version = "0.13.0"
version = "0.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d196ffc1627db18a531359249b2bf8416178d84b729f3cebeb278f285fb9b58c"
checksum = "be136d9dacc2a13cc70bb6c8f902b414fb2641f8db1314637c6b7933411a8f82"
[[package]]
name = "group"
@@ -9165,7 +9168,6 @@ dependencies = [
"collections",
"copilot",
"editor",
"feature_flags",
"futures 0.3.31",
"gpui",
"itertools 0.14.0",
@@ -10983,6 +10985,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 +14204,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",
@@ -14768,6 +14787,7 @@ dependencies = [
"serde_json",
"settings",
"telemetry",
"tempfile",
"theme",
"tree-sitter-json",
"tree-sitter-rust",
@@ -15935,9 +15955,9 @@ dependencies = [
[[package]]
name = "taffy"
version = "0.4.4"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ec17858c2d465b2f734b798b920818a974faf0babb15d7fef81818a4b2d16f1"
checksum = "e8b61630cba2afd2c851821add2e1bb1b7851a2436e839ab73b56558b009035e"
dependencies = [
"arrayvec",
"grid",
@@ -16483,6 +16503,7 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
name = "title_bar"
version = "0.1.0"
dependencies = [
"anyhow",
"auto_update",
"call",
"chrono",
@@ -17667,6 +17688,7 @@ checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
name = "vim"
version = "0.1.0"
dependencies = [
"agent_ui",
"anyhow",
"assets",
"async-compat",
@@ -18728,8 +18750,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",
@@ -20222,6 +20243,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,7 @@ zlog_settings = { path = "crates/zlog_settings" }
# External crates
#
agentic-coding-protocol = "0.0.9"
agentic-coding-protocol = "0.0.10"
aho-corasick = "1.1"
alacritty_terminal = { git = "https://github.com/zed-industries/alacritty.git", branch = "add-hush-login-flag" }
any_vec = "0.14"
@@ -553,8 +555,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 +709,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

@@ -1,5 +1,5 @@
<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" fill="currentColor" stroke="currentColor">
<g clip-path="url(#clip0_205_3)">
<g>
<path d="M0.094 7.78c0.469 0 2.281 -0.405 3.219 -0.936s0.938 -0.531 2.875 -1.906c2.453 -1.741 4.188 -1.158 7.031 -1.158" stroke-width="2.8125" />
<path d="m15.969 3.797 -4.805 2.774V1.023z" />
<path d="M0 7.781c0.469 0 2.281 0.405 3.219 0.936s0.938 0.531 2.875 1.906C8.547 12.364 10.281 11.781 13.125 11.781" stroke-width="2.8125" />

Before

Width:  |  Height:  |  Size: 575 B

After

Width:  |  Height:  |  Size: 545 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="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",

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",
@@ -1228,6 +1229,7 @@
"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",

View File

@@ -41,9 +41,9 @@
// "ctrl-alt-shift-g": "" // find_under_prev (cancels any selections)
"f9": "editor::SortLinesCaseSensitive",
"ctrl-f9": "editor::SortLinesCaseInsensitive",
"f12": "editor::GoToDefinition", // lsp_symbol_definition
"f12": "editor::GoToDefinition",
"ctrl-f12": "editor::GoToDefinitionSplit",
"shift-f12": "editor::FindAllReferences", // lsp_symbol_references
"shift-f12": "editor::FindAllReferences",
"ctrl-shift-f12": "editor::FindAllReferences",
"ctrl-.": "editor::GoToHunk",
"ctrl-,": "editor::GoToPreviousHunk",
@@ -61,9 +61,7 @@
{
"context": "Editor && mode == full",
"bindings": {
"ctrl-alt-space": "editor::ShowSignatureHelp", // lsp_signature_help_show
"ctrl-shift-r": "project_symbols::Toggle", // lsp_workspace_symbols
"ctrl-r": "outline::Toggle" // lsp_document_symbols
"ctrl-r": "outline::Toggle"
}
},
{
@@ -92,7 +90,6 @@
"context": "Workspace",
"bindings": {
"ctrl-k ctrl-b": "workspace::ToggleLeftDock",
"ctrl-alt-m": "diagnostics::Deploy", // lsp_show_diagnostics_panel
// "ctrl-0": "project_panel::ToggleFocus", // normally resets zoom
"shift-ctrl-r": "project_symbols::Toggle"
}

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",
@@ -578,6 +579,13 @@
"shift-u": "git::UnstageAndNext" // "d shift-u"
}
},
{
"context": "VimControl && (AgentDiff || editor_agent_diff)",
"bindings": {
"d p": "agent::Reject",
"d u": "agent::Keep"
}
},
{
"context": "vim_operator == gu",
"bindings": {
@@ -858,6 +866,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

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

@@ -453,9 +453,69 @@ impl Diff {
}
}
#[derive(Debug, Default)]
pub struct Plan {
pub entries: Vec<PlanEntry>,
}
#[derive(Debug)]
pub struct PlanStats<'a> {
pub in_progress_entry: Option<&'a PlanEntry>,
pub pending: u32,
pub completed: u32,
}
impl Plan {
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
pub fn stats(&self) -> PlanStats<'_> {
let mut stats = PlanStats {
in_progress_entry: None,
pending: 0,
completed: 0,
};
for entry in &self.entries {
match &entry.status {
acp::PlanEntryStatus::Pending => {
stats.pending += 1;
}
acp::PlanEntryStatus::InProgress => {
stats.in_progress_entry = stats.in_progress_entry.or(Some(entry));
}
acp::PlanEntryStatus::Completed => {
stats.completed += 1;
}
}
}
stats
}
}
#[derive(Debug)]
pub struct PlanEntry {
pub content: Entity<Markdown>,
pub priority: acp::PlanEntryPriority,
pub status: acp::PlanEntryStatus,
}
impl PlanEntry {
pub fn from_acp(entry: acp::PlanEntry, cx: &mut App) -> Self {
Self {
content: cx.new(|cx| Markdown::new_text(entry.content.into(), cx)),
priority: entry.priority,
status: entry.status,
}
}
}
pub struct AcpThread {
entries: Vec<AgentThreadEntry>,
title: SharedString,
entries: Vec<AgentThreadEntry>,
plan: Plan,
project: Entity<Project>,
action_log: Entity<ActionLog>,
shared_buffers: HashMap<Entity<Buffer>, BufferSnapshot>,
@@ -515,6 +575,7 @@ impl AcpThread {
action_log,
shared_buffers: Default::default(),
entries: Default::default(),
plan: Default::default(),
title,
project,
send_task: None,
@@ -819,6 +880,29 @@ impl AcpThread {
}
}
pub fn plan(&self) -> &Plan {
&self.plan
}
pub fn update_plan(&mut self, request: acp::UpdatePlanParams, cx: &mut Context<Self>) {
self.plan = Plan {
entries: request
.entries
.into_iter()
.map(|entry| PlanEntry::from_acp(entry, cx))
.collect(),
};
cx.notify();
}
pub fn clear_completed_plan_entries(&mut self, cx: &mut Context<Self>) {
self.plan
.entries
.retain(|entry| !matches!(entry.status, acp::PlanEntryStatus::Completed));
cx.notify();
}
pub fn set_project_location(&self, location: ToolCallLocation, cx: &mut Context<Self>) {
self.project.update(cx, |project, cx| {
let Some(path) = project.project_path_for_absolute_path(&location.path, cx) else {
@@ -1136,6 +1220,17 @@ impl AcpClientDelegate {
Self { thread, cx }
}
pub async fn clear_completed_plan_entries(&self) -> Result<()> {
let cx = &mut self.cx.clone();
cx.update(|cx| {
self.thread
.update(cx, |thread, cx| thread.clear_completed_plan_entries(cx))
})?
.context("Failed to update thread")?;
Ok(())
}
pub async fn request_existing_tool_call_confirmation(
&self,
tool_call_id: ToolCallId,
@@ -1233,6 +1328,18 @@ impl acp::Client for AcpClientDelegate {
Ok(())
}
async fn update_plan(&self, request: acp::UpdatePlanParams) -> Result<(), acp::Error> {
let cx = &mut self.cx.clone();
cx.update(|cx| {
self.thread
.update(cx, |thread, cx| thread.update_plan(request, cx))
})?
.context("Failed to update thread")?;
Ok(())
}
async fn read_text_file(
&self,
request: acp::ReadTextFileParams,

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

@@ -51,7 +51,7 @@ use util::{ResultExt as _, debug_panic, 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)]
@@ -2182,8 +2182,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 +2211,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 +2223,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 +2247,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 +4362,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 +4378,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 +4475,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 +4484,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 +4510,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"
);
}

View File

@@ -37,10 +37,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

@@ -4,10 +4,13 @@ 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,
@@ -16,7 +19,7 @@ use agentic_coding_protocol::{
use anyhow::{Result, anyhow};
use futures::channel::oneshot;
use futures::future::LocalBoxFuture;
use futures::{AsyncBufReadExt, AsyncWriteExt};
use futures::{AsyncBufReadExt, AsyncWriteExt, SinkExt};
use futures::{
AsyncRead, AsyncWrite, FutureExt, StreamExt,
channel::mpsc::{self, UnboundedReceiver, UnboundedSender},
@@ -69,13 +72,12 @@ impl AgentServer for ClaudeCode {
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 mcp_server = ClaudeMcpServer::new(delegate_rx, tool_id_map.clone(), cx).await?;
let mut mcp_servers = HashMap::default();
mcp_servers.insert(
mcp_server::SERVER_NAME.to_string(),
permission_mcp_server.server_config()?,
mcp_server.server_config()?,
);
let mcp_config = McpConfig { mcp_servers };
@@ -98,50 +100,58 @@ 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 session_id = Uuid::new_v4();
log::trace!("Starting session with id: {}", session_id);
let io_task =
ClaudeAgentConnection::handle_io(outgoing_rx, incoming_message_tx, stdin, stdout);
cx.background_spawn(async move {
io_task.await.log_err();
let mut outgoing_rx = Some(outgoing_rx);
let mut mode = ClaudeSessionMode::Start;
loop {
let mut child =
spawn_claude(&command, mode, session_id, &mcp_config_path, &root_dir)
.await?;
mode = ClaudeSessionMode::Resume;
let pid = child.id();
log::trace!("Spawned (pid: {})", pid);
let mut io_fut = pin!(
ClaudeAgentConnection::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);
drop(child);
anyhow::Ok(())
})
.detach();
@@ -153,6 +163,7 @@ impl AgentServer for ClaudeCode {
let handler_task = cx.foreground_executor().spawn({
let end_turn_tx = end_turn_tx.clone();
let tool_id_map = tool_id_map.clone();
let delegate = delegate.clone();
async move {
while let Some(message) = incoming_message_rx.next().await {
ClaudeAgentConnection::handle_message(
@@ -167,27 +178,46 @@ impl AgentServer for ClaudeCode {
});
let mut connection = ClaudeAgentConnection {
delegate,
outgoing_tx,
end_turn_tx,
cancel_tx,
session_id,
_handler_task: handler_task,
_mcp_server: None,
};
connection._mcp_server = Some(permission_mcp_server);
connection._mcp_server = Some(mcp_server);
acp_thread::AcpThread::new(connection, title, None, project.clone(), cx)
})
})
}
}
#[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")
}
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 delegate = self.delegate.clone();
let end_turn_tx = self.end_turn_tx.clone();
let outgoing_tx = self.outgoing_tx.clone();
let mut cancel_tx = self.cancel_tx.clone();
let session_id = self.session_id;
async move {
match params {
// todo: consider sending an empty request so we get the init response?
@@ -201,6 +231,8 @@ impl AgentConnection for ClaudeAgentConnection {
Err(anyhow!("Authentication not supported"))
}
AnyAgentRequest::SendUserMessageParams(message) => {
delegate.clear_completed_plan_entries().await?;
let (tx, rx) = oneshot::channel();
end_turn_tx.borrow_mut().replace(tx);
let mut content = String::new();
@@ -224,25 +256,83 @@ impl AgentConnection for ClaudeAgentConnection {
stop_sequence: None,
usage: None,
},
session_id: None,
session_id: Some(session_id),
})?;
rx.await??;
Ok(AnyAgentResult::SendUserMessageResponse(
acp::SendUserMessageResponse,
))
}
AnyAgentRequest::CancelSendMessageParams(_) => Ok(
AnyAgentResult::CancelSendMessageResponse(acp::CancelSendMessageResponse),
),
AnyAgentRequest::CancelSendMessageParams(_) => {
let (done_tx, done_rx) = oneshot::channel();
cancel_tx.send(done_tx).await?;
done_rx.await??;
Ok(AnyAgentResult::CancelSendMessageResponse(
acp::CancelSendMessageResponse,
))
}
}
}
.boxed_local()
}
}
#[derive(Clone, Copy)]
enum ClaudeSessionMode {
Start,
Resume,
}
async fn spawn_claude(
command: &AgentServerCommand,
mode: ClaudeSessionMode,
session_id: Uuid,
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 ClaudeAgentConnection {
delegate: AcpClientDelegate,
session_id: Uuid,
outgoing_tx: UnboundedSender<SdkMessage>,
end_turn_tx: Rc<RefCell<Option<oneshot::Sender<Result<()>>>>>,
cancel_tx: UnboundedSender<oneshot::Sender<Result<()>>>,
_mcp_server: Option<ClaudeMcpServer>,
_handler_task: Task<()>,
}
@@ -267,8 +357,17 @@ impl ClaudeAgentConnection {
.log_err();
}
ContentChunk::ToolUse { id, name, input } => {
if let Some(resp) = delegate
.push_tool_call(ClaudeTool::infer(&name, input).as_acp())
let claude_tool = ClaudeTool::infer(&name, input);
if let ClaudeTool::TodoWrite(Some(params)) = claude_tool {
delegate
.update_plan(acp::UpdatePlanParams {
entries: params.todos.into_iter().map(Into::into).collect(),
})
.await
.log_err();
} else if let Some(resp) = delegate
.push_tool_call(claude_tool.as_acp())
.await
.log_err()
{
@@ -335,7 +434,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 +468,8 @@ impl ClaudeAgentConnection {
}
}
}
Ok(())
Ok(outgoing_rx)
}
}
@@ -492,14 +592,14 @@ enum SdkMessage {
Assistant {
message: Message, // from Anthropic SDK
#[serde(skip_serializing_if = "Option::is_none")]
session_id: Option<String>,
session_id: Option<Uuid>,
},
// A user message
User {
message: Message, // from Anthropic SDK
#[serde(skip_serializing_if = "Option::is_none")]
session_id: Option<String>,
session_id: Option<Uuid>,
},
// Emitted as the last message in a conversation

View File

@@ -69,9 +69,6 @@ impl ClaudeMcpServer {
}
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()

View File

@@ -614,6 +614,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 +632,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 +654,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

@@ -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,3 +1,4 @@
use acp_thread::Plan;
use agent_servers::AgentServer;
use std::cell::RefCell;
use std::collections::BTreeMap;
@@ -45,7 +46,8 @@ use ::acp_thread::{
use crate::acp::completion_provider::{ContextPickerCompletionProvider, MentionSet};
use crate::acp::message_history::MessageHistory;
use crate::agent_diff::AgentDiff;
use crate::{AgentDiffPane, Follow, KeepAll, OpenAgentDiff, RejectAll};
use crate::message_editor::{MAX_EDITOR_LINES, MIN_EDITOR_LINES};
use crate::{AgentDiffPane, ExpandMessageEditor, Follow, KeepAll, OpenAgentDiff, RejectAll};
const RESPONSE_PADDING_X: Pixels = px(19.);
@@ -65,6 +67,8 @@ pub struct AcpThreadView {
expanded_tool_calls: HashSet<ToolCallId>,
expanded_thinking_blocks: HashSet<(usize, usize)>,
edits_expanded: bool,
plan_expanded: bool,
editor_expanded: bool,
message_history: Rc<RefCell<MessageHistory<acp::SendUserMessageParams>>>,
}
@@ -94,6 +98,8 @@ impl AcpThreadView {
workspace: WeakEntity<Workspace>,
project: Entity<Project>,
message_history: Rc<RefCell<MessageHistory<acp::SendUserMessageParams>>>,
min_lines: usize,
max_lines: Option<usize>,
window: &mut Window,
cx: &mut Context<Self>,
) -> Self {
@@ -113,8 +119,8 @@ impl AcpThreadView {
let mut editor = Editor::new(
editor::EditorMode::AutoHeight {
min_lines: 4,
max_lines: None,
min_lines,
max_lines: max_lines,
},
buffer,
None,
@@ -182,6 +188,8 @@ impl AcpThreadView {
expanded_tool_calls: HashSet::default(),
expanded_thinking_blocks: HashSet::default(),
edits_expanded: false,
plan_expanded: false,
editor_expanded: false,
message_history,
}
}
@@ -321,6 +329,35 @@ impl AcpThreadView {
}
}
pub fn expand_message_editor(
&mut self,
_: &ExpandMessageEditor,
_window: &mut Window,
cx: &mut Context<Self>,
) {
self.set_editor_is_expanded(!self.editor_expanded, cx);
cx.notify();
}
fn set_editor_is_expanded(&mut self, is_expanded: bool, cx: &mut Context<Self>) {
self.editor_expanded = is_expanded;
self.message_editor.update(cx, |editor, _| {
if self.editor_expanded {
editor.set_mode(EditorMode::Full {
scale_ui_elements_with_buffer_font_size: false,
show_active_line_background: false,
sized_by_content: false,
})
} else {
editor.set_mode(EditorMode::AutoHeight {
min_lines: MIN_EDITOR_LINES,
max_lines: Some(MAX_EDITOR_LINES),
})
}
});
cx.notify();
}
fn chat(&mut self, _: &Chat, window: &mut Window, cx: &mut Context<Self>) {
self.last_error.take();
@@ -381,6 +418,7 @@ impl AcpThreadView {
let mention_set = self.mention_set.clone();
self.set_editor_is_expanded(false, cx);
self.message_editor.update(cx, |editor, cx| {
editor.clear(window, cx);
editor.remove_creases(mention_set.lock().drain(), cx)
@@ -1442,7 +1480,7 @@ impl AcpThreadView {
container.into_any()
}
fn render_edits_bar(
fn render_activity_bar(
&self,
thread_entity: &Entity<AcpThread>,
window: &mut Window,
@@ -1451,8 +1489,9 @@ impl AcpThreadView {
let thread = thread_entity.read(cx);
let action_log = thread.action_log();
let changed_buffers = action_log.read(cx).changed_buffers(cx);
let plan = thread.plan();
if changed_buffers.is_empty() {
if changed_buffers.is_empty() && plan.is_empty() {
return None;
}
@@ -1461,7 +1500,6 @@ impl AcpThreadView {
let bg_edit_files_disclosure = editor_bg_color.blend(active_color.opacity(0.3));
let pending_edits = thread.has_pending_edit_tool_calls();
let expanded = self.edits_expanded;
v_flex()
.mt_1()
@@ -1477,27 +1515,165 @@ impl AcpThreadView {
blur_radius: px(3.),
spread_radius: px(0.),
}])
.child(self.render_edits_bar_summary(
action_log,
&changed_buffers,
expanded,
pending_edits,
window,
cx,
))
.when(expanded, |parent| {
parent.child(self.render_edits_bar_files(
action_log,
&changed_buffers,
pending_edits,
cx,
))
.when(!plan.is_empty(), |this| {
this.child(self.render_plan_summary(plan, window, cx))
.when(self.plan_expanded, |parent| {
parent.child(self.render_plan_entries(plan, window, cx))
})
})
.when(!changed_buffers.is_empty(), |this| {
this.child(Divider::horizontal())
.child(self.render_edits_summary(
action_log,
&changed_buffers,
self.edits_expanded,
pending_edits,
window,
cx,
))
.when(self.edits_expanded, |parent| {
parent.child(self.render_edited_files(
action_log,
&changed_buffers,
pending_edits,
cx,
))
})
})
.into_any()
.into()
}
fn render_edits_bar_summary(
fn render_plan_summary(&self, plan: &Plan, window: &mut Window, cx: &Context<Self>) -> Div {
let stats = plan.stats();
let title = if let Some(entry) = stats.in_progress_entry
&& !self.plan_expanded
{
h_flex()
.w_full()
.gap_1()
.text_xs()
.text_color(cx.theme().colors().text_muted)
.justify_between()
.child(
h_flex()
.gap_1()
.child(
Label::new("Current:")
.size(LabelSize::Small)
.color(Color::Muted),
)
.child(MarkdownElement::new(
entry.content.clone(),
plan_label_markdown_style(&entry.status, window, cx),
)),
)
.when(stats.pending > 0, |this| {
this.child(
Label::new(format!("{} left", stats.pending))
.size(LabelSize::Small)
.color(Color::Muted)
.mr_1(),
)
})
} else {
let status_label = if stats.pending == 0 {
"All Done".to_string()
} else if stats.completed == 0 {
format!("{}", plan.entries.len())
} else {
format!("{}/{}", stats.completed, plan.entries.len())
};
h_flex()
.w_full()
.gap_1()
.justify_between()
.child(
Label::new("Plan")
.size(LabelSize::Small)
.color(Color::Muted),
)
.child(
Label::new(status_label)
.size(LabelSize::Small)
.color(Color::Muted)
.mr_1(),
)
};
h_flex()
.p_1()
.justify_between()
.when(self.plan_expanded, |this| {
this.border_b_1().border_color(cx.theme().colors().border)
})
.child(
h_flex()
.id("plan_summary")
.w_full()
.gap_1()
.child(Disclosure::new("plan_disclosure", self.plan_expanded))
.child(title)
.on_click(cx.listener(|this, _, _, cx| {
this.plan_expanded = !this.plan_expanded;
cx.notify();
})),
)
}
fn render_plan_entries(&self, plan: &Plan, window: &mut Window, cx: &Context<Self>) -> Div {
v_flex().children(plan.entries.iter().enumerate().flat_map(|(index, entry)| {
let element = h_flex()
.py_1()
.px_2()
.gap_2()
.justify_between()
.bg(cx.theme().colors().editor_background)
.when(index < plan.entries.len() - 1, |parent| {
parent.border_color(cx.theme().colors().border).border_b_1()
})
.child(
h_flex()
.id(("plan_entry", index))
.gap_1p5()
.max_w_full()
.overflow_x_scroll()
.text_xs()
.text_color(cx.theme().colors().text_muted)
.child(match entry.status {
acp::PlanEntryStatus::Pending => Icon::new(IconName::TodoPending)
.size(IconSize::Small)
.color(Color::Muted)
.into_any_element(),
acp::PlanEntryStatus::InProgress => Icon::new(IconName::TodoProgress)
.size(IconSize::Small)
.color(Color::Accent)
.with_animation(
"running",
Animation::new(Duration::from_secs(2)).repeat(),
|icon, delta| {
icon.transform(Transformation::rotate(percentage(delta)))
},
)
.into_any_element(),
acp::PlanEntryStatus::Completed => Icon::new(IconName::TodoComplete)
.size(IconSize::Small)
.color(Color::Success)
.into_any_element(),
})
.child(MarkdownElement::new(
entry.content.clone(),
plan_label_markdown_style(&entry.status, window, cx),
)),
);
Some(element)
}))
}
fn render_edits_summary(
&self,
action_log: &Entity<ActionLog>,
changed_buffers: &BTreeMap<Entity<Buffer>, Entity<BufferDiff>>,
@@ -1643,7 +1819,7 @@ impl AcpThreadView {
)
}
fn render_edits_bar_files(
fn render_edited_files(
&self,
action_log: &Entity<ActionLog>,
changed_buffers: &BTreeMap<Entity<Buffer>, Entity<BufferDiff>>,
@@ -1793,34 +1969,96 @@ impl AcpThreadView {
))
}
fn render_message_editor(&mut self, cx: &mut Context<Self>) -> AnyElement {
let settings = ThemeSettings::get_global(cx);
let font_size = TextSize::Small
.rems(cx)
.to_pixels(settings.agent_font_size(cx));
let line_height = settings.buffer_line_height.value() * font_size;
let text_style = TextStyle {
color: cx.theme().colors().text,
font_family: settings.buffer_font.family.clone(),
font_fallbacks: settings.buffer_font.fallbacks.clone(),
font_features: settings.buffer_font.features.clone(),
font_size: font_size.into(),
line_height: line_height.into(),
..Default::default()
fn render_message_editor(&mut self, window: &mut Window, cx: &mut Context<Self>) -> AnyElement {
let focus_handle = self.message_editor.focus_handle(cx);
let editor_bg_color = cx.theme().colors().editor_background;
let (expand_icon, expand_tooltip) = if self.editor_expanded {
(IconName::Minimize, "Minimize Message Editor")
} else {
(IconName::Maximize, "Expand Message Editor")
};
EditorElement::new(
&self.message_editor,
EditorStyle {
background: cx.theme().colors().editor_background,
local_player: cx.theme().players().local(),
text: text_style,
syntax: cx.theme().syntax().clone(),
..Default::default()
},
)
.into_any()
v_flex()
.on_action(cx.listener(Self::expand_message_editor))
.p_2()
.gap_2()
.border_t_1()
.border_color(cx.theme().colors().border)
.bg(editor_bg_color)
.when(self.editor_expanded, |this| {
this.h(vh(0.8, window)).size_full().justify_between()
})
.child(
v_flex()
.relative()
.size_full()
.pt_1()
.pr_2p5()
.child(div().flex_1().child({
let settings = ThemeSettings::get_global(cx);
let font_size = TextSize::Small
.rems(cx)
.to_pixels(settings.agent_font_size(cx));
let line_height = settings.buffer_line_height.value() * font_size;
let text_style = TextStyle {
color: cx.theme().colors().text,
font_family: settings.buffer_font.family.clone(),
font_fallbacks: settings.buffer_font.fallbacks.clone(),
font_features: settings.buffer_font.features.clone(),
font_size: font_size.into(),
line_height: line_height.into(),
..Default::default()
};
EditorElement::new(
&self.message_editor,
EditorStyle {
background: editor_bg_color,
local_player: cx.theme().players().local(),
text: text_style,
syntax: cx.theme().syntax().clone(),
..Default::default()
},
)
}))
.child(
h_flex()
.absolute()
.top_0()
.right_0()
.opacity(0.5)
.hover(|this| this.opacity(1.0))
.child(
IconButton::new("toggle-height", expand_icon)
.icon_size(IconSize::XSmall)
.icon_color(Color::Muted)
.tooltip({
let focus_handle = focus_handle.clone();
move |window, cx| {
Tooltip::for_action_in(
expand_tooltip,
&ExpandMessageEditor,
&focus_handle,
window,
cx,
)
}
})
.on_click(cx.listener(|_, _, window, cx| {
window.dispatch_action(Box::new(ExpandMessageEditor), cx);
})),
),
),
)
.child(
h_flex()
.flex_none()
.justify_between()
.child(self.render_follow_toggle(cx))
.child(self.render_send_button(cx)),
)
.into_any()
}
fn render_send_button(&self, cx: &mut Context<Self>) -> AnyElement {
@@ -2132,7 +2370,6 @@ impl Render for AcpThreadView {
.px(RESPONSE_PADDING_X)
.opacity(0.4)
.hover(|style| style.opacity(1.))
.gap_1()
.flex_wrap()
.justify_end()
.child(open_as_markdown)
@@ -2147,7 +2384,7 @@ impl Render for AcpThreadView {
.child(LoadingLabel::new("").size(LabelSize::Small))
.into(),
})
.children(self.render_edits_bar(&thread, window, cx))
.children(self.render_activity_bar(&thread, window, cx))
} else {
this.child(self.render_empty_state(cx))
}
@@ -2166,22 +2403,7 @@ impl Render for AcpThreadView {
),
)
})
.child(
v_flex()
.p_2()
.pt_3()
.gap_1()
.bg(cx.theme().colors().editor_background)
.border_t_1()
.border_color(cx.theme().colors().border)
.child(self.render_message_editor(cx))
.child(
h_flex()
.justify_between()
.child(self.render_follow_toggle(cx))
.child(self.render_send_button(cx)),
),
)
.child(self.render_message_editor(window, cx))
}
}
@@ -2328,3 +2550,27 @@ fn default_markdown_style(buffer_font: bool, window: &Window, cx: &App) -> Markd
..Default::default()
}
}
fn plan_label_markdown_style(
status: &acp::PlanEntryStatus,
window: &Window,
cx: &App,
) -> MarkdownStyle {
let default_md_style = default_markdown_style(false, window, cx);
MarkdownStyle {
base_text_style: TextStyle {
color: cx.theme().colors().text_muted,
strikethrough: if matches!(status, acp::PlanEntryStatus::Completed) {
Some(gpui::StrikethroughStyle {
thickness: px(1.),
color: Some(cx.theme().colors().text_muted.opacity(0.8)),
})
} else {
None
},
..default_md_style.base_text_style
},
..default_md_style
}
}

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)
});
});
}
})

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

@@ -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,
@@ -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::{
@@ -960,6 +962,8 @@ impl AgentPanel {
workspace.clone(),
project,
message_history,
MIN_EDITOR_LINES,
Some(MAX_EDITOR_LINES),
window,
cx,
)
@@ -1903,16 +1907,39 @@ impl AgentPanel {
.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())
.item(
ContextMenuEntry::new("New Thread")
.icon(IconName::NewThread)
.icon_color(Color::Muted)
.handler(move |window, cx| {
window.dispatch_action(NewThread::default().boxed_clone(), cx);
}),
)
.item(
ContextMenuEntry::new("New Text Thread")
.icon(IconName::NewTextThread)
.icon_color(Color::Muted)
.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() {
this.action(
"New From Summary",
Box::new(NewThread {
from_thread_id: Some(thread.id().clone()),
}),
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
@@ -1921,19 +1948,33 @@ impl AgentPanel {
.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(),
.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,
);
}),
)
.action(
"New Claude Code Thread",
NewExternalAgentThread {
agent: Some(crate::ExternalAgent::ClaudeCode),
}
.boxed_clone(),
.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
@@ -2259,7 +2300,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 +2336,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 +2480,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 +2497,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 +2529,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 +3250,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

@@ -197,6 +197,11 @@ impl ModelUsageContext {
}
}
pub fn init_settings(cx: &mut App) {
AgentSettings::register(cx);
SlashCommandSettings::register(cx);
}
/// Initializes the `agent` crate.
pub fn init(
fs: Arc<dyn Fs>,
@@ -206,8 +211,7 @@ pub fn init(
is_eval: bool,
cx: &mut App,
) {
AgentSettings::register(cx);
SlashCommandSettings::register(cx);
init_settings(cx);
assistant_context::init(client.clone(), cx);
rules_library::init(cx);

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>,
@@ -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()
@@ -1655,9 +1657,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

@@ -0,0 +1,135 @@
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()
}
pub fn has_providers(&self) -> bool {
!self.configured_providers.is_empty()
}
}
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::Small).color(Color::Muted))
.child(Label::new(name))
});
h_flex()
.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(Icon::new(IconName::Info).size(IconSize::XSmall).color(Color::Muted))
.child(Label::new("Or 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(
"You can also use AI in Zed by bringing your own API keys",
)))
.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,6 +131,12 @@ impl ZedAiOnboarding {
.color(Color::Muted)
.buffer_font(cx),
)
.child(
Label::new("(Current Plan)")
.size(LabelSize::Small)
.color(Color::Custom(cx.theme().colors().text_muted.opacity(0.6)))
.buffer_font(cx),
)
.child(Divider::horizontal()),
)
.child(
@@ -130,65 +148,89 @@ impl ZedAiOnboarding {
"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")
.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 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)),
)
}
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 per month with the Claude models",
))
.child(BulletItem::new(
"Unlimited accepted edit predictions using our open-source Zeta 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 using our open-source Zeta model",
))
.child(BulletItem::new("USD $20 per month")),
)
.child(
Button::new("pro", "Start with Pro")
.full_width()
.style(ButtonStyle::Tinted(ui::TintColor::Accent))
.on_click(move |_, _window, cx| {
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 with no charge and no credit card required",
)),
)
.child(
Button::new("pro", "Start Pro Trial")
.full_width()
.style(ButtonStyle::Tinted(ui::TintColor::Accent))
.on_click(move |_, _window, cx| {
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("Before starting…"))
.child(
Label::new("Make sure you have read and accepted Zed AI's terms of service.")
.color(Color::Muted)
.mb_2(),
)
.child(
Button::new("terms_of_service", "View and Read the Terms of Service")
.full_width()
@@ -196,9 +238,7 @@ impl ZedAiOnboarding {
.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")
}),
.on_click(move |_, _window, cx| cx.open_url(&zed_urls::terms_of_service(cx))),
)
.child(
Button::new("accept_terms", "I've read it and accept it")
@@ -209,23 +249,23 @@ impl ZedAiOnboarding {
move |_, window, cx| (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 start using AI in Zed with a free trial of the Pro plan, which includes:")
.color(Color::Muted)
.mb_2(),
)
.child(self.pro_trial_definition())
.child(
Button::new("sign_in", "Sign in to Start Trial")
.disabled(signing_in)
.full_width()
.style(ButtonStyle::Tinted(ui::TintColor::Accent))
@@ -234,36 +274,55 @@ impl ZedAiOnboarding {
move |_, window, cx| 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)
Label::new("Choose how you want to start.")
.color(Color::Muted)
.mt_1()
.mb_3(),
.mb_2(),
)
.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| 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 free 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 +331,31 @@ 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| 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()
@@ -306,6 +371,7 @@ impl ZedAiOnboarding {
move |_, window, cx| callback(window, cx)
}),
)
.into_any_element()
}
}
@@ -314,9 +380,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 +413,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@zed.dev.";
let label = div()
.w_full()

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

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

@@ -1,12 +1,10 @@
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,
@@ -20,7 +18,6 @@ use stripe::{
use util::{ResultExt, maybe};
use zed_llm_client::LanguageModelProvider;
use crate::api::events::SnowflakeRow;
use crate::db::billing_subscription::{
StripeCancellationReason, StripeSubscriptionStatus, SubscriptionKind,
};
@@ -36,14 +33,13 @@ use crate::{
db::{
BillingSubscriptionId, 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",
@@ -55,108 +51,6 @@ pub fn router() -> Router {
)
}
#[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,
}),
)
.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 {

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

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

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

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

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

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

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

View File

@@ -950,6 +950,7 @@ struct InlineBlamePopover {
hide_task: Option<Task<()>>,
popover_bounds: Option<Bounds<Pixels>>,
popover_state: InlineBlamePopoverState,
keyboard_grace: bool,
}
enum SelectionDragState {
@@ -6517,21 +6518,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 +6595,7 @@ impl Editor {
commit_message: details,
markdown,
},
keyboard_grace: ignore_timeout,
});
cx.notify();
})

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);
@@ -1143,10 +1144,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 {

View File

@@ -422,6 +422,13 @@ impl AppContext for ExampleContext {
self.app.update_entity(handle, update)
}
fn as_mut<'a, T>(&'a mut self, handle: &Entity<T>) -> Self::Result<gpui::GpuiBorrow<'a, T>>
where
T: 'static,
{
self.app.as_mut(handle)
}
fn read_entity<T, R>(
&self,
handle: &Entity<T>,

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

@@ -121,7 +121,7 @@ smallvec.workspace = true
smol.workspace = true
strum.workspace = true
sum_tree.workspace = true
taffy = "0.4.3"
taffy = "=0.5.1"
thiserror.workspace = true
util.workspace = true
uuid.workspace = true

View File

@@ -34,7 +34,7 @@ fn main() {
});
}
// Associate actions using the `actions!` macro (or `impl_actions!` macro)
// Associate actions using the `actions!` macro (or `Action` derive macro)
actions!(set_menus, [Quit]);
// Define the quit function that is registered with the App

View File

@@ -0,0 +1,130 @@
use gpui::{
App, Application, Bounds, Context, Div, ElementId, FocusHandle, KeyBinding, SharedString,
Stateful, Window, WindowBounds, WindowOptions, actions, div, prelude::*, px, size,
};
actions!(example, [Tab, TabPrev]);
struct Example {
items: Vec<FocusHandle>,
message: SharedString,
}
impl Example {
fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
let items = vec![
cx.focus_handle().tab_index(1).tab_stop(true),
cx.focus_handle().tab_index(2).tab_stop(true),
cx.focus_handle().tab_index(3).tab_stop(true),
cx.focus_handle(),
cx.focus_handle().tab_index(2).tab_stop(true),
];
window.focus(items.first().unwrap());
Self {
items,
message: SharedString::from("Press `Tab`, `Shift-Tab` to switch focus."),
}
}
fn on_tab(&mut self, _: &Tab, window: &mut Window, _: &mut Context<Self>) {
window.focus_next();
self.message = SharedString::from("You have pressed `Tab`.");
}
fn on_tab_prev(&mut self, _: &TabPrev, window: &mut Window, _: &mut Context<Self>) {
window.focus_prev();
self.message = SharedString::from("You have pressed `Shift-Tab`.");
}
}
impl Render for Example {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
fn button(id: impl Into<ElementId>) -> Stateful<Div> {
div()
.id(id)
.h_10()
.flex_1()
.flex()
.justify_center()
.items_center()
.border_1()
.border_color(gpui::black())
.bg(gpui::black())
.text_color(gpui::white())
.focus(|this| this.border_color(gpui::blue()))
.shadow_sm()
}
div()
.id("app")
.on_action(cx.listener(Self::on_tab))
.on_action(cx.listener(Self::on_tab_prev))
.size_full()
.flex()
.flex_col()
.p_4()
.gap_3()
.bg(gpui::white())
.text_color(gpui::black())
.child(self.message.clone())
.children(
self.items
.clone()
.into_iter()
.enumerate()
.map(|(ix, item_handle)| {
div()
.id(("item", ix))
.track_focus(&item_handle)
.h_10()
.w_full()
.flex()
.justify_center()
.items_center()
.border_1()
.border_color(gpui::black())
.when(
item_handle.tab_stop && item_handle.is_focused(window),
|this| this.border_color(gpui::blue()),
)
.map(|this| match item_handle.tab_stop {
true => this
.hover(|this| this.bg(gpui::black().opacity(0.1)))
.child(format!("tab_index: {}", item_handle.tab_index)),
false => this.opacity(0.4).child("tab_stop: false"),
})
}),
)
.child(
div()
.flex()
.flex_row()
.gap_3()
.items_center()
.child(button("el1").tab_index(4).child("Button 1"))
.child(button("el2").tab_index(5).child("Button 2")),
)
}
}
fn main() {
Application::new().run(|cx: &mut App| {
cx.bind_keys([
KeyBinding::new("tab", Tab, None),
KeyBinding::new("shift-tab", TabPrev, None),
]);
let bounds = Bounds::centered(None, size(px(800.), px(600.0)), cx);
cx.open_window(
WindowOptions {
window_bounds: Some(WindowBounds::Windowed(bounds)),
..Default::default()
},
|window, cx| cx.new(|cx| Example::new(window, cx)),
)
.unwrap();
cx.activate(true);
});
}

View File

@@ -448,15 +448,23 @@ impl App {
}
pub(crate) fn update<R>(&mut self, update: impl FnOnce(&mut Self) -> R) -> R {
self.pending_updates += 1;
self.start_update();
let result = update(self);
self.finish_update();
result
}
pub(crate) fn start_update(&mut self) {
self.pending_updates += 1;
}
pub(crate) fn finish_update(&mut self) {
if !self.flushing_effects && self.pending_updates == 1 {
self.flushing_effects = true;
self.flush_effects();
self.flushing_effects = false;
}
self.pending_updates -= 1;
result
}
/// Arrange a callback to be invoked when the given entity calls `notify` on its respective context.
@@ -688,7 +696,7 @@ impl App {
/// Returns a list of available screen capture sources.
pub fn screen_capture_sources(
&self,
) -> oneshot::Receiver<Result<Vec<Box<dyn ScreenCaptureSource>>>> {
) -> oneshot::Receiver<Result<Vec<Rc<dyn ScreenCaptureSource>>>> {
self.platform.screen_capture_sources()
}
@@ -868,7 +876,6 @@ impl App {
loop {
self.release_dropped_entities();
self.release_dropped_focus_handles();
if let Some(effect) = self.pending_effects.pop_front() {
match effect {
Effect::Notify { emitter } => {
@@ -947,8 +954,8 @@ impl App {
self.focus_handles
.clone()
.write()
.retain(|handle_id, count| {
if count.load(SeqCst) == 0 {
.retain(|handle_id, focus| {
if focus.ref_count.load(SeqCst) == 0 {
for window_handle in self.windows() {
window_handle
.update(self, |_, window, _| {
@@ -1363,7 +1370,9 @@ impl App {
self.keymap.clone()
}
/// Register a global listener for actions invoked via the keyboard.
/// Register a global handler for actions invoked via the keyboard. These handlers are run at
/// the end of the bubble phase for actions, and so will only be invoked if there are no other
/// handlers or if they called `cx.propagate()`.
pub fn on_action<A: Action>(&mut self, listener: impl Fn(&A, &mut Self) + 'static) {
self.global_action_listeners
.entry(TypeId::of::<A>())
@@ -1819,6 +1828,13 @@ impl AppContext for App {
})
}
fn as_mut<'a, T>(&'a mut self, handle: &Entity<T>) -> GpuiBorrow<'a, T>
where
T: 'static,
{
GpuiBorrow::new(handle.clone(), self)
}
fn read_entity<T, R>(
&self,
handle: &Entity<T>,
@@ -2015,3 +2031,79 @@ impl HttpClient for NullHttpClient {
type_name::<Self>()
}
}
/// A mutable reference to an entity owned by GPUI
pub struct GpuiBorrow<'a, T> {
inner: Option<Lease<T>>,
app: &'a mut App,
}
impl<'a, T: 'static> GpuiBorrow<'a, T> {
fn new(inner: Entity<T>, app: &'a mut App) -> Self {
app.start_update();
let lease = app.entities.lease(&inner);
Self {
inner: Some(lease),
app,
}
}
}
impl<'a, T: 'static> std::borrow::Borrow<T> for GpuiBorrow<'a, T> {
fn borrow(&self) -> &T {
self.inner.as_ref().unwrap().borrow()
}
}
impl<'a, T: 'static> std::borrow::BorrowMut<T> for GpuiBorrow<'a, T> {
fn borrow_mut(&mut self) -> &mut T {
self.inner.as_mut().unwrap().borrow_mut()
}
}
impl<'a, T> Drop for GpuiBorrow<'a, T> {
fn drop(&mut self) {
let lease = self.inner.take().unwrap();
self.app.notify(lease.id);
self.app.entities.end_lease(lease);
self.app.finish_update();
}
}
#[cfg(test)]
mod test {
use std::{cell::RefCell, rc::Rc};
use crate::{AppContext, TestAppContext};
#[test]
fn test_gpui_borrow() {
let cx = TestAppContext::single();
let observation_count = Rc::new(RefCell::new(0));
let state = cx.update(|cx| {
let state = cx.new(|_| false);
cx.observe(&state, {
let observation_count = observation_count.clone();
move |_, _| {
let mut count = observation_count.borrow_mut();
*count += 1;
}
})
.detach();
state
});
cx.update(|cx| {
// Calling this like this so that we don't clobber the borrow_mut above
*std::borrow::BorrowMut::borrow_mut(&mut state.as_mut(cx)) = true;
});
cx.update(|cx| {
state.write(cx, false);
});
assert_eq!(*observation_count.borrow(), 2);
}
}

View File

@@ -3,7 +3,7 @@ use crate::{
Entity, EventEmitter, Focusable, ForegroundExecutor, Global, PromptButton, PromptLevel, Render,
Reservation, Result, Subscription, Task, VisualContext, Window, WindowHandle,
};
use anyhow::Context as _;
use anyhow::{Context as _, anyhow};
use derive_more::{Deref, DerefMut};
use futures::channel::oneshot;
use std::{future::Future, rc::Weak};
@@ -58,6 +58,15 @@ impl AppContext for AsyncApp {
Ok(app.update_entity(handle, update))
}
fn as_mut<'a, T>(&'a mut self, _handle: &Entity<T>) -> Self::Result<super::GpuiBorrow<'a, T>>
where
T: 'static,
{
Err(anyhow!(
"Cannot as_mut with an async context. Try calling update() first"
))
}
fn read_entity<T, R>(
&self,
handle: &Entity<T>,
@@ -364,6 +373,15 @@ impl AppContext for AsyncWindowContext {
.update(self, |_, _, cx| cx.update_entity(handle, update))
}
fn as_mut<'a, T>(&'a mut self, _: &Entity<T>) -> Self::Result<super::GpuiBorrow<'a, T>>
where
T: 'static,
{
Err(anyhow!(
"Cannot use as_mut() from an async context, call `update`"
))
}
fn read_entity<T, R>(
&self,
handle: &Entity<T>,

View File

@@ -726,6 +726,13 @@ impl<T> AppContext for Context<'_, T> {
self.app.update_entity(handle, update)
}
fn as_mut<'a, E>(&'a mut self, handle: &Entity<E>) -> Self::Result<super::GpuiBorrow<'a, E>>
where
E: 'static,
{
self.app.as_mut(handle)
}
fn read_entity<U, R>(
&self,
handle: &Entity<U>,

View File

@@ -1,4 +1,4 @@
use crate::{App, AppContext, VisualContext, Window, seal::Sealed};
use crate::{App, AppContext, GpuiBorrow, VisualContext, Window, seal::Sealed};
use anyhow::{Context as _, Result};
use collections::FxHashSet;
use derive_more::{Deref, DerefMut};
@@ -105,7 +105,7 @@ impl EntityMap {
/// Move an entity to the stack.
#[track_caller]
pub fn lease<'a, T>(&mut self, pointer: &'a Entity<T>) -> Lease<'a, T> {
pub fn lease<T>(&mut self, pointer: &Entity<T>) -> Lease<T> {
self.assert_valid_context(pointer);
let mut accessed_entities = self.accessed_entities.borrow_mut();
accessed_entities.insert(pointer.entity_id);
@@ -117,15 +117,14 @@ impl EntityMap {
);
Lease {
entity,
pointer,
id: pointer.entity_id,
entity_type: PhantomData,
}
}
/// Returns an entity after moving it to the stack.
pub fn end_lease<T>(&mut self, mut lease: Lease<T>) {
self.entities
.insert(lease.pointer.entity_id, lease.entity.take().unwrap());
self.entities.insert(lease.id, lease.entity.take().unwrap());
}
pub fn read<T: 'static>(&self, entity: &Entity<T>) -> &T {
@@ -187,13 +186,13 @@ fn double_lease_panic<T>(operation: &str) -> ! {
)
}
pub(crate) struct Lease<'a, T> {
pub(crate) struct Lease<T> {
entity: Option<Box<dyn Any>>,
pub pointer: &'a Entity<T>,
pub id: EntityId,
entity_type: PhantomData<T>,
}
impl<T: 'static> core::ops::Deref for Lease<'_, T> {
impl<T: 'static> core::ops::Deref for Lease<T> {
type Target = T;
fn deref(&self) -> &Self::Target {
@@ -201,13 +200,13 @@ impl<T: 'static> core::ops::Deref for Lease<'_, T> {
}
}
impl<T: 'static> core::ops::DerefMut for Lease<'_, T> {
impl<T: 'static> core::ops::DerefMut for Lease<T> {
fn deref_mut(&mut self) -> &mut Self::Target {
self.entity.as_mut().unwrap().downcast_mut().unwrap()
}
}
impl<T> Drop for Lease<'_, T> {
impl<T> Drop for Lease<T> {
fn drop(&mut self) {
if self.entity.is_some() && !panicking() {
panic!("Leases must be ended with EntityMap::end_lease")
@@ -437,6 +436,19 @@ impl<T: 'static> Entity<T> {
cx.update_entity(self, update)
}
/// Updates the entity referenced by this handle with the given function.
pub fn as_mut<'a, C: AppContext>(&self, cx: &'a mut C) -> C::Result<GpuiBorrow<'a, T>> {
cx.as_mut(self)
}
/// Updates the entity referenced by this handle with the given function.
pub fn write<C: AppContext>(&self, cx: &mut C, value: T) -> C::Result<()> {
self.update(cx, |entity, cx| {
*entity = value;
cx.notify();
})
}
/// Updates the entity referenced by this handle with the given function if
/// the referenced entity still exists, within a visual context that has a window.
/// Returns an error if the entity has been released.

View File

@@ -9,6 +9,7 @@ use crate::{
};
use anyhow::{anyhow, bail};
use futures::{Stream, StreamExt, channel::oneshot};
use rand::{SeedableRng, rngs::StdRng};
use std::{cell::RefCell, future::Future, ops::Deref, rc::Rc, sync::Arc, time::Duration};
/// A TestAppContext is provided to tests created with `#[gpui::test]`, it provides
@@ -63,6 +64,13 @@ impl AppContext for TestAppContext {
app.update_entity(handle, update)
}
fn as_mut<'a, T>(&'a mut self, _: &Entity<T>) -> Self::Result<super::GpuiBorrow<'a, T>>
where
T: 'static,
{
panic!("Cannot use as_mut with a test app context. Try calling update() first")
}
fn read_entity<T, R>(
&self,
handle: &Entity<T>,
@@ -134,6 +142,12 @@ impl TestAppContext {
}
}
/// Create a single TestAppContext, for non-multi-client tests
pub fn single() -> Self {
let dispatcher = TestDispatcher::new(StdRng::from_entropy());
Self::build(dispatcher, None)
}
/// The name of the test function that created this `TestAppContext`
pub fn test_function_name(&self) -> Option<&'static str> {
self.fn_name
@@ -914,6 +928,13 @@ impl AppContext for VisualTestContext {
self.cx.update_entity(handle, update)
}
fn as_mut<'a, T>(&'a mut self, handle: &Entity<T>) -> Self::Result<super::GpuiBorrow<'a, T>>
where
T: 'static,
{
self.cx.as_mut(handle)
}
fn read_entity<T, R>(
&self,
handle: &Entity<T>,

View File

@@ -39,7 +39,7 @@ use crate::{
use derive_more::{Deref, DerefMut};
pub(crate) use smallvec::SmallVec;
use std::{
any::Any,
any::{Any, type_name},
fmt::{self, Debug, Display},
mem, panic,
};
@@ -220,14 +220,17 @@ impl<C: RenderOnce> Element for Component<C> {
window: &mut Window,
cx: &mut App,
) -> (LayoutId, Self::RequestLayoutState) {
let mut element = self
.component
.take()
.unwrap()
.render(window, cx)
.into_any_element();
let layout_id = element.request_layout(window, cx);
(layout_id, element)
window.with_global_id(ElementId::Name(type_name::<C>().into()), |_, window| {
let mut element = self
.component
.take()
.unwrap()
.render(window, cx)
.into_any_element();
let layout_id = element.request_layout(window, cx);
(layout_id, element)
})
}
fn prepaint(
@@ -239,7 +242,9 @@ impl<C: RenderOnce> Element for Component<C> {
window: &mut Window,
cx: &mut App,
) {
element.prepaint(window, cx);
window.with_global_id(ElementId::Name(type_name::<C>().into()), |_, window| {
element.prepaint(window, cx);
})
}
fn paint(
@@ -252,7 +257,9 @@ impl<C: RenderOnce> Element for Component<C> {
window: &mut Window,
cx: &mut App,
) {
element.paint(window, cx);
window.with_global_id(ElementId::Name(type_name::<C>().into()), |_, window| {
element.paint(window, cx);
})
}
}

View File

@@ -619,6 +619,13 @@ pub trait InteractiveElement: Sized {
self
}
/// Set index of the tab stop order.
fn tab_index(mut self, index: isize) -> Self {
self.interactivity().focusable = true;
self.interactivity().tab_index = Some(index);
self
}
/// Set the keymap context for this element. This will be used to determine
/// which action to dispatch from the keymap.
fn key_context<C, E>(mut self, key_context: C) -> Self
@@ -1462,6 +1469,7 @@ pub struct Interactivity {
pub(crate) tooltip_builder: Option<TooltipBuilder>,
pub(crate) window_control: Option<WindowControlArea>,
pub(crate) hitbox_behavior: HitboxBehavior,
pub(crate) tab_index: Option<isize>,
#[cfg(any(feature = "inspector", debug_assertions))]
pub(crate) source_location: Option<&'static core::panic::Location<'static>>,
@@ -1521,12 +1529,17 @@ impl Interactivity {
// as frames contain an element with this id.
if self.focusable && self.tracked_focus_handle.is_none() {
if let Some(element_state) = element_state.as_mut() {
self.tracked_focus_handle = Some(
element_state
.focus_handle
.get_or_insert_with(|| cx.focus_handle())
.clone(),
);
let mut handle = element_state
.focus_handle
.get_or_insert_with(|| cx.focus_handle())
.clone()
.tab_stop(false);
if let Some(index) = self.tab_index {
handle = handle.tab_index(index).tab_stop(true);
}
self.tracked_focus_handle = Some(handle);
}
}
@@ -1651,6 +1664,11 @@ impl Interactivity {
window: &mut Window,
_cx: &mut App,
) -> Point<Pixels> {
fn round_to_two_decimals(pixels: Pixels) -> Pixels {
const ROUNDING_FACTOR: f32 = 100.0;
(pixels * ROUNDING_FACTOR).round() / ROUNDING_FACTOR
}
if let Some(scroll_offset) = self.scroll_offset.as_ref() {
let mut scroll_to_bottom = false;
let mut tracked_scroll_handle = self
@@ -1665,8 +1683,16 @@ impl Interactivity {
let rem_size = window.rem_size();
let padding = style.padding.to_pixels(bounds.size.into(), rem_size);
let padding_size = size(padding.left + padding.right, padding.top + padding.bottom);
// The floating point values produced by Taffy and ours often vary
// slightly after ~5 decimal places. This can lead to cases where after
// subtracting these, the container becomes scrollable for less than
// 0.00000x pixels. As we generally don't benefit from a precision that
// high for the maximum scroll, we round the scroll max to 2 decimal
// places here.
let padded_content_size = self.content_size + padding_size;
let scroll_max = (padded_content_size - bounds.size).max(&Size::default());
let scroll_max = (padded_content_size - bounds.size)
.map(round_to_two_decimals)
.max(&Default::default());
// Clamp scroll offset in case scroll max is smaller now (e.g., if children
// were removed or the bounds became larger).
let mut scroll_offset = scroll_offset.borrow_mut();
@@ -1679,7 +1705,7 @@ impl Interactivity {
}
if let Some(mut scroll_handle_state) = tracked_scroll_handle {
scroll_handle_state.padded_content_size = padded_content_size;
scroll_handle_state.max_offset = scroll_max;
}
*scroll_offset
@@ -1729,6 +1755,10 @@ impl Interactivity {
return ((), element_state);
}
if let Some(focus_handle) = &self.tracked_focus_handle {
window.next_frame.tab_handles.insert(focus_handle);
}
window.with_element_opacity(style.opacity, |window| {
style.paint(bounds, window, cx, |window: &mut Window, cx: &mut App| {
window.with_text_style(style.text_style().cloned(), |window| {
@@ -2919,7 +2949,7 @@ impl ScrollAnchor {
struct ScrollHandleState {
offset: Rc<RefCell<Point<Pixels>>>,
bounds: Bounds<Pixels>,
padded_content_size: Size<Pixels>,
max_offset: Size<Pixels>,
child_bounds: Vec<Bounds<Pixels>>,
scroll_to_bottom: bool,
overflow: Point<Overflow>,
@@ -2948,6 +2978,11 @@ impl ScrollHandle {
*self.0.borrow().offset.borrow()
}
/// Get the maximum scroll offset.
pub fn max_offset(&self) -> Size<Pixels> {
self.0.borrow().max_offset
}
/// Get the top child that's scrolled into view.
pub fn top_item(&self) -> usize {
let state = self.0.borrow();
@@ -2982,11 +3017,6 @@ impl ScrollHandle {
self.0.borrow().child_bounds.get(ix).cloned()
}
/// Get the size of the content with padding of the container.
pub fn padded_content_size(&self) -> Size<Pixels> {
self.0.borrow().padded_content_size
}
/// scroll_to_item scrolls the minimal amount to ensure that the child is
/// fully visible
pub fn scroll_to_item(&self, ix: usize) {

View File

@@ -411,9 +411,9 @@ impl ListState {
self.0.borrow_mut().set_offset_from_scrollbar(point);
}
/// Returns the size of items we have measured.
/// Returns the maximum scroll offset according to the items we have measured.
/// This value remains constant while dragging to prevent the scrollbar from moving away unexpectedly.
pub fn content_size_for_scrollbar(&self) -> Size<Pixels> {
pub fn max_offset_for_scrollbar(&self) -> Size<Pixels> {
let state = self.0.borrow();
let bounds = state.last_layout_bounds.unwrap_or_default();
@@ -421,7 +421,7 @@ impl ListState {
.scrollbar_drag_start_height
.unwrap_or_else(|| state.items.summary().height);
Size::new(bounds.size.width, height)
Size::new(Pixels::ZERO, Pixels::ZERO.max(height - bounds.size.height))
}
/// Returns the current scroll offset adjusted for the scrollbar

View File

@@ -95,6 +95,7 @@ mod style;
mod styled;
mod subscription;
mod svg_renderer;
mod tab_stop;
mod taffy;
#[cfg(any(test, feature = "test-support"))]
pub mod test;
@@ -151,6 +152,7 @@ pub use style::*;
pub use styled::*;
pub use subscription::*;
use svg_renderer::*;
pub(crate) use tab_stop::*;
pub use taffy::{AvailableSpace, LayoutId};
#[cfg(any(test, feature = "test-support"))]
pub use test::*;
@@ -197,6 +199,11 @@ pub trait AppContext {
where
T: 'static;
/// Update a entity in the app context.
fn as_mut<'a, T>(&'a mut self, handle: &Entity<T>) -> Self::Result<GpuiBorrow<'a, T>>
where
T: 'static;
/// Read a entity from the app context.
fn read_entity<T, R>(
&self,

View File

@@ -85,7 +85,7 @@ pub(crate) use test::*;
pub(crate) use windows::*;
#[cfg(any(test, feature = "test-support"))]
pub use test::{TestDispatcher, TestScreenCaptureSource};
pub use test::{TestDispatcher, TestScreenCaptureSource, TestScreenCaptureStream};
/// Returns a background executor for the current platform.
pub fn background_executor() -> BackgroundExecutor {
@@ -189,13 +189,12 @@ pub(crate) trait Platform: 'static {
false
}
#[cfg(feature = "screen-capture")]
fn screen_capture_sources(
&self,
) -> oneshot::Receiver<Result<Vec<Box<dyn ScreenCaptureSource>>>>;
fn screen_capture_sources(&self)
-> oneshot::Receiver<Result<Vec<Rc<dyn ScreenCaptureSource>>>>;
#[cfg(not(feature = "screen-capture"))]
fn screen_capture_sources(
&self,
) -> oneshot::Receiver<anyhow::Result<Vec<Box<dyn ScreenCaptureSource>>>> {
) -> oneshot::Receiver<anyhow::Result<Vec<Rc<dyn ScreenCaptureSource>>>> {
let (sources_tx, sources_rx) = oneshot::channel();
sources_tx
.send(Err(anyhow::anyhow!(
@@ -293,10 +292,23 @@ pub trait PlatformDisplay: Send + Sync + Debug {
}
}
/// Metadata for a given [ScreenCaptureSource]
#[derive(Clone)]
pub struct SourceMetadata {
/// Opaque identifier of this screen.
pub id: u64,
/// Human-readable label for this source.
pub label: Option<SharedString>,
/// Whether this source is the main display.
pub is_main: Option<bool>,
/// Video resolution of this source.
pub resolution: Size<DevicePixels>,
}
/// A source of on-screen video content that can be captured.
pub trait ScreenCaptureSource {
/// Returns the video resolution of this source.
fn resolution(&self) -> Result<Size<DevicePixels>>;
/// Returns metadata for this source.
fn metadata(&self) -> Result<SourceMetadata>;
/// Start capture video from this source, invoking the given callback
/// with each frame.
@@ -308,7 +320,10 @@ pub trait ScreenCaptureSource {
}
/// A video stream captured from a screen.
pub trait ScreenCaptureStream {}
pub trait ScreenCaptureStream {
/// Returns metadata for this source.
fn metadata(&self) -> Result<SourceMetadata>;
}
/// A frame of video captured from a screen.
pub struct ScreenCaptureFrame(pub PlatformScreenCaptureFrame);

View File

@@ -73,7 +73,7 @@ impl LinuxClient for HeadlessClient {
#[cfg(feature = "screen-capture")]
fn screen_capture_sources(
&self,
) -> futures::channel::oneshot::Receiver<anyhow::Result<Vec<Box<dyn crate::ScreenCaptureSource>>>>
) -> futures::channel::oneshot::Receiver<anyhow::Result<Vec<Rc<dyn crate::ScreenCaptureSource>>>>
{
let (mut tx, rx) = futures::channel::oneshot::channel();
tx.send(Err(anyhow::anyhow!(

View File

@@ -56,7 +56,7 @@ pub trait LinuxClient {
#[cfg(feature = "screen-capture")]
fn screen_capture_sources(
&self,
) -> oneshot::Receiver<Result<Vec<Box<dyn crate::ScreenCaptureSource>>>>;
) -> oneshot::Receiver<Result<Vec<Rc<dyn crate::ScreenCaptureSource>>>>;
fn open_window(
&self,
@@ -245,7 +245,7 @@ impl<P: LinuxClient + 'static> Platform for P {
#[cfg(feature = "screen-capture")]
fn screen_capture_sources(
&self,
) -> oneshot::Receiver<Result<Vec<Box<dyn crate::ScreenCaptureSource>>>> {
) -> oneshot::Receiver<Result<Vec<Rc<dyn crate::ScreenCaptureSource>>>> {
self.screen_capture_sources()
}
@@ -828,6 +828,13 @@ impl crate::Keystroke {
Keysym::Delete => "delete".to_owned(),
Keysym::Escape => "escape".to_owned(),
Keysym::Left => "left".to_owned(),
Keysym::Right => "right".to_owned(),
Keysym::Up => "up".to_owned(),
Keysym::Down => "down".to_owned(),
Keysym::Home => "home".to_owned(),
Keysym::End => "end".to_owned(),
_ => {
let name = xkb::keysym_get_name(key_sym).to_lowercase();
if key_sym.is_keypad_key() {

View File

@@ -672,7 +672,7 @@ impl LinuxClient for WaylandClient {
#[cfg(feature = "screen-capture")]
fn screen_capture_sources(
&self,
) -> futures::channel::oneshot::Receiver<anyhow::Result<Vec<Box<dyn crate::ScreenCaptureSource>>>>
) -> futures::channel::oneshot::Receiver<anyhow::Result<Vec<Rc<dyn crate::ScreenCaptureSource>>>>
{
// TODO: Get screen capture working on wayland. Be sure to try window resizing as that may
// be tricky.

View File

@@ -76,6 +76,7 @@ struct InProgressConfigure {
size: Option<Size<Pixels>>,
fullscreen: bool,
maximized: bool,
resizing: bool,
tiling: Tiling,
}
@@ -107,6 +108,7 @@ pub struct WaylandWindowState {
active: bool,
hovered: bool,
in_progress_configure: Option<InProgressConfigure>,
resize_throttle: bool,
in_progress_window_controls: Option<WindowControls>,
window_controls: WindowControls,
inset: Option<Pixels>,
@@ -176,6 +178,7 @@ impl WaylandWindowState {
tiling: Tiling::default(),
window_bounds: options.bounds,
in_progress_configure: None,
resize_throttle: false,
client,
appearance,
handle,
@@ -335,6 +338,7 @@ impl WaylandWindowStatePtr {
pub fn frame(&self) {
let mut state = self.state.borrow_mut();
state.surface.frame(&state.globals.qh, state.surface.id());
state.resize_throttle = false;
drop(state);
let mut cb = self.callbacks.borrow_mut();
@@ -366,6 +370,12 @@ impl WaylandWindowStatePtr {
state.fullscreen = configure.fullscreen;
state.maximized = configure.maximized;
state.tiling = configure.tiling;
// Limit interactive resizes to once per vblank
if configure.resizing && state.resize_throttle {
return;
} else if configure.resizing {
state.resize_throttle = true;
}
if !configure.fullscreen && !configure.maximized {
configure.size = if got_unmaximized {
Some(state.window_bounds.size)
@@ -472,6 +482,7 @@ impl WaylandWindowStatePtr {
let mut tiling = Tiling::default();
let mut fullscreen = false;
let mut maximized = false;
let mut resizing = false;
for state in states {
match state {
@@ -481,6 +492,7 @@ impl WaylandWindowStatePtr {
xdg_toplevel::State::Fullscreen => {
fullscreen = true;
}
xdg_toplevel::State::Resizing => resizing = true,
xdg_toplevel::State::TiledTop => {
tiling.top = true;
}
@@ -508,6 +520,7 @@ impl WaylandWindowStatePtr {
size,
fullscreen,
maximized,
resizing,
tiling,
});

View File

@@ -1448,7 +1448,7 @@ impl LinuxClient for X11Client {
#[cfg(feature = "screen-capture")]
fn screen_capture_sources(
&self,
) -> futures::channel::oneshot::Receiver<anyhow::Result<Vec<Box<dyn crate::ScreenCaptureSource>>>>
) -> futures::channel::oneshot::Receiver<anyhow::Result<Vec<Rc<dyn crate::ScreenCaptureSource>>>>
{
crate::platform::scap_screen_capture::scap_screen_sources(
&self.0.borrow().common.foreground_executor,

View File

@@ -583,7 +583,7 @@ impl Platform for MacPlatform {
#[cfg(feature = "screen-capture")]
fn screen_capture_sources(
&self,
) -> oneshot::Receiver<Result<Vec<Box<dyn crate::ScreenCaptureSource>>>> {
) -> oneshot::Receiver<Result<Vec<Rc<dyn crate::ScreenCaptureSource>>>> {
super::screen_capture::get_sources()
}

View File

@@ -1,5 +1,5 @@
use crate::{
DevicePixels, ForegroundExecutor, Size,
DevicePixels, ForegroundExecutor, SharedString, SourceMetadata,
platform::{ScreenCaptureFrame, ScreenCaptureSource, ScreenCaptureStream},
size,
};
@@ -7,8 +7,9 @@ use anyhow::{Result, anyhow};
use block::ConcreteBlock;
use cocoa::{
base::{YES, id, nil},
foundation::NSArray,
foundation::{NSArray, NSString},
};
use collections::HashMap;
use core_foundation::base::TCFType;
use core_graphics::display::{
CGDirectDisplayID, CGDisplayCopyDisplayMode, CGDisplayModeGetPixelHeight,
@@ -32,11 +33,13 @@ use super::NSStringExt;
#[derive(Clone)]
pub struct MacScreenCaptureSource {
sc_display: id,
meta: Option<ScreenMeta>,
}
pub struct MacScreenCaptureStream {
sc_stream: id,
sc_stream_output: id,
meta: SourceMetadata,
}
static mut DELEGATE_CLASS: *const Class = ptr::null();
@@ -47,19 +50,31 @@ const FRAME_CALLBACK_IVAR: &str = "frame_callback";
const SCStreamOutputTypeScreen: NSInteger = 0;
impl ScreenCaptureSource for MacScreenCaptureSource {
fn resolution(&self) -> Result<Size<DevicePixels>> {
unsafe {
fn metadata(&self) -> Result<SourceMetadata> {
let (display_id, size) = unsafe {
let display_id: CGDirectDisplayID = msg_send![self.sc_display, displayID];
let display_mode_ref = CGDisplayCopyDisplayMode(display_id);
let width = CGDisplayModeGetPixelWidth(display_mode_ref);
let height = CGDisplayModeGetPixelHeight(display_mode_ref);
CGDisplayModeRelease(display_mode_ref);
Ok(size(
DevicePixels(width as i32),
DevicePixels(height as i32),
))
}
(
display_id,
size(DevicePixels(width as i32), DevicePixels(height as i32)),
)
};
let (label, is_main) = self
.meta
.clone()
.map(|meta| (meta.label, meta.is_main))
.unzip();
Ok(SourceMetadata {
id: display_id as u64,
label,
is_main,
resolution: size,
})
}
fn stream(
@@ -89,9 +104,9 @@ impl ScreenCaptureSource for MacScreenCaptureSource {
Box::into_raw(Box::new(frame_callback)) as *mut c_void,
);
let resolution = self.resolution().unwrap();
let _: id = msg_send![configuration, setWidth: resolution.width.0 as i64];
let _: id = msg_send![configuration, setHeight: resolution.height.0 as i64];
let meta = self.metadata().unwrap();
let _: id = msg_send![configuration, setWidth: meta.resolution.width.0 as i64];
let _: id = msg_send![configuration, setHeight: meta.resolution.height.0 as i64];
let stream: id = msg_send![stream, initWithFilter:filter configuration:configuration delegate:delegate];
let (mut tx, rx) = oneshot::channel();
@@ -110,6 +125,7 @@ impl ScreenCaptureSource for MacScreenCaptureSource {
move |error: id| {
let result = if error == nil {
let stream = MacScreenCaptureStream {
meta: meta.clone(),
sc_stream: stream,
sc_stream_output: output,
};
@@ -138,7 +154,11 @@ impl Drop for MacScreenCaptureSource {
}
}
impl ScreenCaptureStream for MacScreenCaptureStream {}
impl ScreenCaptureStream for MacScreenCaptureStream {
fn metadata(&self) -> Result<SourceMetadata> {
Ok(self.meta.clone())
}
}
impl Drop for MacScreenCaptureStream {
fn drop(&mut self) {
@@ -164,24 +184,74 @@ impl Drop for MacScreenCaptureStream {
}
}
pub(crate) fn get_sources() -> oneshot::Receiver<Result<Vec<Box<dyn ScreenCaptureSource>>>> {
#[derive(Clone)]
struct ScreenMeta {
label: SharedString,
// Is this the screen with menu bar?
is_main: bool,
}
unsafe fn screen_id_to_human_label() -> HashMap<CGDirectDisplayID, ScreenMeta> {
let screens: id = msg_send![class!(NSScreen), screens];
let count: usize = msg_send![screens, count];
let mut map = HashMap::default();
let screen_number_key = unsafe { NSString::alloc(nil).init_str("NSScreenNumber") };
for i in 0..count {
let screen: id = msg_send![screens, objectAtIndex: i];
let device_desc: id = msg_send![screen, deviceDescription];
if device_desc == nil {
continue;
}
let nsnumber: id = msg_send![device_desc, objectForKey: screen_number_key];
if nsnumber == nil {
continue;
}
let screen_id: u32 = msg_send![nsnumber, unsignedIntValue];
let name: id = msg_send![screen, localizedName];
if name != nil {
let cstr: *const std::os::raw::c_char = msg_send![name, UTF8String];
let rust_str = unsafe {
std::ffi::CStr::from_ptr(cstr)
.to_string_lossy()
.into_owned()
};
map.insert(
screen_id,
ScreenMeta {
label: rust_str.into(),
is_main: i == 0,
},
);
}
}
map
}
pub(crate) fn get_sources() -> oneshot::Receiver<Result<Vec<Rc<dyn ScreenCaptureSource>>>> {
unsafe {
let (mut tx, rx) = oneshot::channel();
let tx = Rc::new(RefCell::new(Some(tx)));
let screen_id_to_label = screen_id_to_human_label();
let block = ConcreteBlock::new(move |shareable_content: id, error: id| {
let Some(mut tx) = tx.borrow_mut().take() else {
return;
};
let result = if error == nil {
let displays: id = msg_send![shareable_content, displays];
let mut result = Vec::new();
for i in 0..displays.count() {
let display = displays.objectAtIndex(i);
let id: CGDirectDisplayID = msg_send![display, displayID];
let meta = screen_id_to_label.get(&id).cloned();
let source = MacScreenCaptureSource {
sc_display: msg_send![display, retain],
meta,
};
result.push(Box::new(source) as Box<dyn ScreenCaptureSource>);
result.push(Rc::new(source) as Rc<dyn ScreenCaptureSource>);
}
Ok(result)
} else {

View File

@@ -1,10 +1,12 @@
//! Screen capture for Linux and Windows
use crate::{
DevicePixels, ForegroundExecutor, ScreenCaptureFrame, ScreenCaptureSource, ScreenCaptureStream,
Size, size,
Size, SourceMetadata, size,
};
use anyhow::{Context as _, Result, anyhow};
use futures::channel::oneshot;
use scap::Target;
use std::rc::Rc;
use std::sync::Arc;
use std::sync::atomic::{self, AtomicBool};
@@ -15,7 +17,7 @@ use std::sync::atomic::{self, AtomicBool};
#[allow(dead_code)]
pub(crate) fn scap_screen_sources(
foreground_executor: &ForegroundExecutor,
) -> oneshot::Receiver<Result<Vec<Box<dyn ScreenCaptureSource>>>> {
) -> oneshot::Receiver<Result<Vec<Rc<dyn ScreenCaptureSource>>>> {
let (sources_tx, sources_rx) = oneshot::channel();
get_screen_targets(sources_tx);
to_dyn_screen_capture_sources(sources_rx, foreground_executor)
@@ -29,14 +31,14 @@ pub(crate) fn scap_screen_sources(
#[allow(dead_code)]
pub(crate) fn start_scap_default_target_source(
foreground_executor: &ForegroundExecutor,
) -> oneshot::Receiver<Result<Vec<Box<dyn ScreenCaptureSource>>>> {
) -> oneshot::Receiver<Result<Vec<Rc<dyn ScreenCaptureSource>>>> {
let (sources_tx, sources_rx) = oneshot::channel();
start_default_target_screen_capture(sources_tx);
to_dyn_screen_capture_sources(sources_rx, foreground_executor)
}
struct ScapCaptureSource {
target: scap::Target,
target: scap::Display,
size: Size<DevicePixels>,
}
@@ -52,7 +54,7 @@ fn get_screen_targets(sources_tx: oneshot::Sender<Result<Vec<ScapCaptureSource>>
}
};
let sources = targets
.iter()
.into_iter()
.filter_map(|target| match target {
scap::Target::Display(display) => {
let size = Size {
@@ -60,7 +62,7 @@ fn get_screen_targets(sources_tx: oneshot::Sender<Result<Vec<ScapCaptureSource>>
height: DevicePixels(display.height as i32),
};
Some(ScapCaptureSource {
target: target.clone(),
target: display,
size,
})
}
@@ -72,8 +74,13 @@ fn get_screen_targets(sources_tx: oneshot::Sender<Result<Vec<ScapCaptureSource>>
}
impl ScreenCaptureSource for ScapCaptureSource {
fn resolution(&self) -> Result<Size<DevicePixels>> {
Ok(self.size)
fn metadata(&self) -> Result<SourceMetadata> {
Ok(SourceMetadata {
resolution: self.size,
label: Some(self.target.title.clone().into()),
is_main: None,
id: self.target.id as u64,
})
}
fn stream(
@@ -85,13 +92,15 @@ impl ScreenCaptureSource for ScapCaptureSource {
let target = self.target.clone();
// Due to use of blocking APIs, a dedicated thread is used.
std::thread::spawn(move || match new_scap_capturer(Some(target)) {
Ok(mut capturer) => {
capturer.start_capture();
run_capture(capturer, frame_callback, stream_tx);
}
Err(e) => {
stream_tx.send(Err(e)).ok();
std::thread::spawn(move || {
match new_scap_capturer(Some(scap::Target::Display(target.clone()))) {
Ok(mut capturer) => {
capturer.start_capture();
run_capture(capturer, target.clone(), frame_callback, stream_tx);
}
Err(e) => {
stream_tx.send(Err(e)).ok();
}
}
});
@@ -107,6 +116,7 @@ struct ScapDefaultTargetCaptureSource {
// Callback for frames.
Box<dyn Fn(ScreenCaptureFrame) + Send>,
)>,
target: scap::Display,
size: Size<DevicePixels>,
}
@@ -123,33 +133,48 @@ fn start_default_target_screen_capture(
.get_next_frame()
.context("Failed to get first frame of screenshare to get the size.")?;
let size = frame_size(&first_frame);
Ok((capturer, size))
let target = capturer
.target()
.context("Unable to determine the target display.")?;
let target = target.clone();
Ok((capturer, size, target))
});
match start_result {
Err(e) => {
sources_tx.send(Err(e)).ok();
}
Ok((capturer, size)) => {
Ok((capturer, size, Target::Display(display))) => {
let (stream_call_tx, stream_rx) = std::sync::mpsc::sync_channel(1);
sources_tx
.send(Ok(vec![ScapDefaultTargetCaptureSource {
stream_call_tx,
size,
target: display.clone(),
}]))
.ok();
let Ok((stream_tx, frame_callback)) = stream_rx.recv() else {
return;
};
run_capture(capturer, frame_callback, stream_tx);
run_capture(capturer, display, frame_callback, stream_tx);
}
Err(e) => {
sources_tx.send(Err(e)).ok();
}
_ => {
sources_tx
.send(Err(anyhow!("The screen capture source is not a display")))
.ok();
}
}
});
}
impl ScreenCaptureSource for ScapDefaultTargetCaptureSource {
fn resolution(&self) -> Result<Size<DevicePixels>> {
Ok(self.size)
fn metadata(&self) -> Result<SourceMetadata> {
Ok(SourceMetadata {
resolution: self.size,
label: None,
is_main: None,
id: self.target.id as u64,
})
}
fn stream(
@@ -189,12 +214,19 @@ fn new_scap_capturer(target: Option<scap::Target>) -> Result<scap::capturer::Cap
fn run_capture(
mut capturer: scap::capturer::Capturer,
display: scap::Display,
frame_callback: Box<dyn Fn(ScreenCaptureFrame) + Send>,
stream_tx: oneshot::Sender<Result<ScapStream>>,
) {
let cancel_stream = Arc::new(AtomicBool::new(false));
let size = Size {
width: DevicePixels(display.width as i32),
height: DevicePixels(display.height as i32),
};
let stream_send_result = stream_tx.send(Ok(ScapStream {
cancel_stream: cancel_stream.clone(),
display,
size,
}));
if let Err(_) = stream_send_result {
return;
@@ -213,9 +245,20 @@ fn run_capture(
struct ScapStream {
cancel_stream: Arc<AtomicBool>,
display: scap::Display,
size: Size<DevicePixels>,
}
impl ScreenCaptureStream for ScapStream {}
impl ScreenCaptureStream for ScapStream {
fn metadata(&self) -> Result<SourceMetadata> {
Ok(SourceMetadata {
resolution: self.size,
label: Some(self.display.title.clone().into()),
is_main: None,
id: self.display.id as u64,
})
}
}
impl Drop for ScapStream {
fn drop(&mut self) {
@@ -237,12 +280,12 @@ fn frame_size(frame: &scap::frame::Frame) -> Size<DevicePixels> {
}
/// This is used by `get_screen_targets` and `start_default_target_screen_capture` to turn their
/// results into `Box<dyn ScreenCaptureSource>`. They need to `Send` their capture source, and so
/// the capture source structs are used as `Box<dyn ScreenCaptureSource>` is not `Send`.
/// results into `Rc<dyn ScreenCaptureSource>`. They need to `Send` their capture source, and so
/// the capture source structs are used as `Rc<dyn ScreenCaptureSource>` is not `Send`.
fn to_dyn_screen_capture_sources<T: ScreenCaptureSource + 'static>(
sources_rx: oneshot::Receiver<Result<Vec<T>>>,
foreground_executor: &ForegroundExecutor,
) -> oneshot::Receiver<Result<Vec<Box<dyn ScreenCaptureSource>>>> {
) -> oneshot::Receiver<Result<Vec<Rc<dyn ScreenCaptureSource>>>> {
let (dyn_sources_tx, dyn_sources_rx) = oneshot::channel();
foreground_executor
.spawn(async move {
@@ -250,7 +293,7 @@ fn to_dyn_screen_capture_sources<T: ScreenCaptureSource + 'static>(
Ok(Ok(results)) => dyn_sources_tx
.send(Ok(results
.into_iter()
.map(|source| Box::new(source) as Box<dyn ScreenCaptureSource>)
.map(|source| Rc::new(source) as Rc<dyn ScreenCaptureSource>)
.collect::<Vec<_>>()))
.ok(),
Ok(Err(err)) => dyn_sources_tx.send(Err(err)).ok(),

View File

@@ -8,4 +8,4 @@ pub(crate) use display::*;
pub(crate) use platform::*;
pub(crate) use window::*;
pub use platform::TestScreenCaptureSource;
pub use platform::{TestScreenCaptureSource, TestScreenCaptureStream};

View File

@@ -2,7 +2,7 @@ use crate::{
AnyWindowHandle, BackgroundExecutor, ClipboardItem, CursorStyle, DevicePixels,
ForegroundExecutor, Keymap, NoopTextSystem, Platform, PlatformDisplay, PlatformKeyboardLayout,
PlatformTextSystem, PromptButton, ScreenCaptureFrame, ScreenCaptureSource, ScreenCaptureStream,
Size, Task, TestDisplay, TestWindow, WindowAppearance, WindowParams, size,
SourceMetadata, Task, TestDisplay, TestWindow, WindowAppearance, WindowParams, size,
};
use anyhow::Result;
use collections::VecDeque;
@@ -44,11 +44,17 @@ pub(crate) struct TestPlatform {
/// A fake screen capture source, used for testing.
pub struct TestScreenCaptureSource {}
/// A fake screen capture stream, used for testing.
pub struct TestScreenCaptureStream {}
impl ScreenCaptureSource for TestScreenCaptureSource {
fn resolution(&self) -> Result<Size<DevicePixels>> {
Ok(size(DevicePixels(1), DevicePixels(1)))
fn metadata(&self) -> Result<SourceMetadata> {
Ok(SourceMetadata {
id: 0,
is_main: None,
label: None,
resolution: size(DevicePixels(1), DevicePixels(1)),
})
}
fn stream(
@@ -64,7 +70,11 @@ impl ScreenCaptureSource for TestScreenCaptureSource {
}
}
impl ScreenCaptureStream for TestScreenCaptureStream {}
impl ScreenCaptureStream for TestScreenCaptureStream {
fn metadata(&self) -> Result<SourceMetadata> {
TestScreenCaptureSource {}.metadata()
}
}
struct TestPrompt {
msg: String,
@@ -271,13 +281,13 @@ impl Platform for TestPlatform {
#[cfg(feature = "screen-capture")]
fn screen_capture_sources(
&self,
) -> oneshot::Receiver<Result<Vec<Box<dyn ScreenCaptureSource>>>> {
) -> oneshot::Receiver<Result<Vec<Rc<dyn ScreenCaptureSource>>>> {
let (mut tx, rx) = oneshot::channel();
tx.send(Ok(self
.screen_capture_sources
.borrow()
.iter()
.map(|source| Box::new(source.clone()) as Box<dyn ScreenCaptureSource>)
.map(|source| Rc::new(source.clone()) as Rc<dyn ScreenCaptureSource>)
.collect()))
.ok();
rx

View File

@@ -440,7 +440,7 @@ impl Platform for WindowsPlatform {
#[cfg(feature = "screen-capture")]
fn screen_capture_sources(
&self,
) -> oneshot::Receiver<Result<Vec<Box<dyn ScreenCaptureSource>>>> {
) -> oneshot::Receiver<Result<Vec<Rc<dyn ScreenCaptureSource>>>> {
crate::platform::scap_screen_capture::scap_screen_sources(&self.foreground_executor)
}

157
crates/gpui/src/tab_stop.rs Normal file
View File

@@ -0,0 +1,157 @@
use crate::{FocusHandle, FocusId};
/// Represents a collection of tab handles.
///
/// Used to manage the `Tab` event to switch between focus handles.
#[derive(Default)]
pub(crate) struct TabHandles {
handles: Vec<FocusHandle>,
}
impl TabHandles {
pub(crate) fn insert(&mut self, focus_handle: &FocusHandle) {
if !focus_handle.tab_stop {
return;
}
let focus_handle = focus_handle.clone();
// Insert handle with same tab_index last
if let Some(ix) = self
.handles
.iter()
.position(|tab| tab.tab_index > focus_handle.tab_index)
{
self.handles.insert(ix, focus_handle);
} else {
self.handles.push(focus_handle);
}
}
pub(crate) fn clear(&mut self) {
self.handles.clear();
}
fn current_index(&self, focused_id: Option<&FocusId>) -> usize {
self.handles
.iter()
.position(|h| Some(&h.id) == focused_id)
.unwrap_or_default()
}
pub(crate) fn next(&self, focused_id: Option<&FocusId>) -> Option<FocusHandle> {
let ix = self.current_index(focused_id);
let mut next_ix = ix + 1;
if next_ix + 1 > self.handles.len() {
next_ix = 0;
}
if let Some(next_handle) = self.handles.get(next_ix) {
Some(next_handle.clone())
} else {
None
}
}
pub(crate) fn prev(&self, focused_id: Option<&FocusId>) -> Option<FocusHandle> {
let ix = self.current_index(focused_id);
let prev_ix;
if ix == 0 {
prev_ix = self.handles.len().saturating_sub(1);
} else {
prev_ix = ix.saturating_sub(1);
}
if let Some(prev_handle) = self.handles.get(prev_ix) {
Some(prev_handle.clone())
} else {
None
}
}
}
#[cfg(test)]
mod tests {
use crate::{FocusHandle, FocusMap, TabHandles};
use std::sync::Arc;
#[test]
fn test_tab_handles() {
let focus_map = Arc::new(FocusMap::default());
let mut tab = TabHandles::default();
let focus_handles = vec![
FocusHandle::new(&focus_map).tab_stop(true).tab_index(0),
FocusHandle::new(&focus_map).tab_stop(true).tab_index(1),
FocusHandle::new(&focus_map).tab_stop(true).tab_index(1),
FocusHandle::new(&focus_map),
FocusHandle::new(&focus_map).tab_index(2),
FocusHandle::new(&focus_map).tab_stop(true).tab_index(0),
FocusHandle::new(&focus_map).tab_stop(true).tab_index(2),
];
for handle in focus_handles.iter() {
tab.insert(&handle);
}
assert_eq!(
tab.handles
.iter()
.map(|handle| handle.id)
.collect::<Vec<_>>(),
vec![
focus_handles[0].id,
focus_handles[5].id,
focus_handles[1].id,
focus_handles[2].id,
focus_handles[6].id,
]
);
// next
assert_eq!(tab.next(None), Some(tab.handles[1].clone()));
assert_eq!(
tab.next(Some(&tab.handles[0].id)),
Some(tab.handles[1].clone())
);
assert_eq!(
tab.next(Some(&tab.handles[1].id)),
Some(tab.handles[2].clone())
);
assert_eq!(
tab.next(Some(&tab.handles[2].id)),
Some(tab.handles[3].clone())
);
assert_eq!(
tab.next(Some(&tab.handles[3].id)),
Some(tab.handles[4].clone())
);
assert_eq!(
tab.next(Some(&tab.handles[4].id)),
Some(tab.handles[0].clone())
);
// prev
assert_eq!(tab.prev(None), Some(tab.handles[4].clone()));
assert_eq!(
tab.prev(Some(&tab.handles[0].id)),
Some(tab.handles[4].clone())
);
assert_eq!(
tab.prev(Some(&tab.handles[1].id)),
Some(tab.handles[0].clone())
);
assert_eq!(
tab.prev(Some(&tab.handles[2].id)),
Some(tab.handles[1].clone())
);
assert_eq!(
tab.prev(Some(&tab.handles[3].id)),
Some(tab.handles[2].clone())
);
assert_eq!(
tab.prev(Some(&tab.handles[4].id)),
Some(tab.handles[3].clone())
);
}
}

View File

@@ -182,7 +182,7 @@ impl TaffyLayoutEngine {
.compute_layout_with_measure(
id.into(),
available_space.into(),
|known_dimensions, available_space, _id, node_context| {
|known_dimensions, available_space, _id, node_context, _style| {
let Some(node_context) = node_context else {
return taffy::geometry::Size::default();
};

View File

@@ -12,10 +12,11 @@ use crate::{
PlatformInputHandler, PlatformWindow, Point, PolychromeSprite, PromptButton, PromptLevel, Quad,
Render, RenderGlyphParams, RenderImage, RenderImageParams, RenderSvgParams, Replay, ResizeEdge,
SMOOTH_SVG_SCALE_FACTOR, SUBPIXEL_VARIANTS, ScaledPixels, Scene, Shadow, SharedString, Size,
StrikethroughStyle, Style, SubscriberSet, Subscription, TaffyLayoutEngine, Task, TextStyle,
TextStyleRefinement, TransformationMatrix, Underline, UnderlineStyle, WindowAppearance,
WindowBackgroundAppearance, WindowBounds, WindowControls, WindowDecorations, WindowOptions,
WindowParams, WindowTextSystem, point, prelude::*, px, rems, size, transparent_black,
StrikethroughStyle, Style, SubscriberSet, Subscription, TabHandles, TaffyLayoutEngine, Task,
TextStyle, TextStyleRefinement, TransformationMatrix, Underline, UnderlineStyle,
WindowAppearance, WindowBackgroundAppearance, WindowBounds, WindowControls, WindowDecorations,
WindowOptions, WindowParams, WindowTextSystem, point, prelude::*, px, rems, size,
transparent_black,
};
use anyhow::{Context as _, Result, anyhow};
use collections::{FxHashMap, FxHashSet};
@@ -222,7 +223,12 @@ impl ArenaClearNeeded {
}
}
pub(crate) type FocusMap = RwLock<SlotMap<FocusId, AtomicUsize>>;
pub(crate) type FocusMap = RwLock<SlotMap<FocusId, FocusRef>>;
pub(crate) struct FocusRef {
pub(crate) ref_count: AtomicUsize,
pub(crate) tab_index: isize,
pub(crate) tab_stop: bool,
}
impl FocusId {
/// Obtains whether the element associated with this handle is currently focused.
@@ -258,6 +264,10 @@ impl FocusId {
pub struct FocusHandle {
pub(crate) id: FocusId,
handles: Arc<FocusMap>,
/// The index of this element in the tab order.
pub tab_index: isize,
/// Whether this element can be focused by tab navigation.
pub tab_stop: bool,
}
impl std::fmt::Debug for FocusHandle {
@@ -268,25 +278,54 @@ impl std::fmt::Debug for FocusHandle {
impl FocusHandle {
pub(crate) fn new(handles: &Arc<FocusMap>) -> Self {
let id = handles.write().insert(AtomicUsize::new(1));
let id = handles.write().insert(FocusRef {
ref_count: AtomicUsize::new(1),
tab_index: 0,
tab_stop: false,
});
Self {
id,
tab_index: 0,
tab_stop: false,
handles: handles.clone(),
}
}
pub(crate) fn for_id(id: FocusId, handles: &Arc<FocusMap>) -> Option<Self> {
let lock = handles.read();
let ref_count = lock.get(id)?;
if atomic_incr_if_not_zero(ref_count) == 0 {
let focus = lock.get(id)?;
if atomic_incr_if_not_zero(&focus.ref_count) == 0 {
return None;
}
Some(Self {
id,
tab_index: focus.tab_index,
tab_stop: focus.tab_stop,
handles: handles.clone(),
})
}
/// Sets the tab index of the element associated with this handle.
pub fn tab_index(mut self, index: isize) -> Self {
self.tab_index = index;
if let Some(focus) = self.handles.write().get_mut(self.id) {
focus.tab_index = index;
}
self
}
/// Sets whether the element associated with this handle is a tab stop.
///
/// When `false`, the element will not be included in the tab order.
pub fn tab_stop(mut self, tab_stop: bool) -> Self {
self.tab_stop = tab_stop;
if let Some(focus) = self.handles.write().get_mut(self.id) {
focus.tab_stop = tab_stop;
}
self
}
/// Converts this focus handle into a weak variant, which does not prevent it from being released.
pub fn downgrade(&self) -> WeakFocusHandle {
WeakFocusHandle {
@@ -354,6 +393,7 @@ impl Drop for FocusHandle {
.read()
.get(self.id)
.unwrap()
.ref_count
.fetch_sub(1, SeqCst);
}
}
@@ -642,6 +682,7 @@ pub(crate) struct Frame {
pub(crate) next_inspector_instance_ids: FxHashMap<Rc<crate::InspectorElementPath>, usize>,
#[cfg(any(feature = "inspector", debug_assertions))]
pub(crate) inspector_hitboxes: FxHashMap<HitboxId, crate::InspectorElementId>,
pub(crate) tab_handles: TabHandles,
}
#[derive(Clone, Default)]
@@ -689,6 +730,7 @@ impl Frame {
#[cfg(any(feature = "inspector", debug_assertions))]
inspector_hitboxes: FxHashMap::default(),
tab_handles: TabHandles::default(),
}
}
@@ -704,6 +746,7 @@ impl Frame {
self.hitboxes.clear();
self.window_control_hitboxes.clear();
self.deferred_draws.clear();
self.tab_handles.clear();
self.focus = None;
#[cfg(any(feature = "inspector", debug_assertions))]
@@ -1289,6 +1332,28 @@ impl Window {
self.focus_enabled = false;
}
/// Move focus to next tab stop.
pub fn focus_next(&mut self) {
if !self.focus_enabled {
return;
}
if let Some(handle) = self.rendered_frame.tab_handles.next(self.focus.as_ref()) {
self.focus(&handle)
}
}
/// Move focus to previous tab stop.
pub fn focus_prev(&mut self) {
if !self.focus_enabled {
return;
}
if let Some(handle) = self.rendered_frame.tab_handles.prev(self.focus.as_ref()) {
self.focus(&handle)
}
}
/// Accessor for the text system.
pub fn text_system(&self) -> &Arc<WindowTextSystem> {
&self.text_system
@@ -2424,6 +2489,53 @@ impl Window {
result
}
/// Use a piece of state that exists as long this element is being rendered in consecutive frames.
pub fn use_keyed_state<S: 'static>(
&mut self,
key: impl Into<ElementId>,
cx: &mut App,
init: impl FnOnce(&mut Self, &mut App) -> S,
) -> Entity<S> {
let current_view = self.current_view();
self.with_global_id(key.into(), |global_id, window| {
window.with_element_state(global_id, |state: Option<Entity<S>>, window| {
if let Some(state) = state {
(state.clone(), state)
} else {
let new_state = cx.new(|cx| init(window, cx));
cx.observe(&new_state, move |_, cx| {
cx.notify(current_view);
})
.detach();
(new_state.clone(), new_state)
}
})
})
}
/// Immediately push an element ID onto the stack. Useful for simplifying IDs in lists
pub fn with_id<R>(&mut self, id: impl Into<ElementId>, f: impl FnOnce(&mut Self) -> R) -> R {
self.with_global_id(id.into(), |_, window| f(window))
}
/// Use a piece of state that exists as long this element is being rendered in consecutive frames, without needing to specify a key
///
/// NOTE: This method uses the location of the caller to generate an ID for this state.
/// If this is not sufficient to identify your state (e.g. you're rendering a list item),
/// you can provide a custom ElementID using the `use_keyed_state` method.
#[track_caller]
pub fn use_state<S: 'static>(
&mut self,
cx: &mut App,
init: impl FnOnce(&mut Self, &mut App) -> S,
) -> Entity<S> {
self.use_keyed_state(
ElementId::CodeLocation(*core::panic::Location::caller()),
cx,
init,
)
}
/// Updates or initializes state for an element with the given id that lives across multiple
/// frames. If an element with this ID existed in the rendered frame, its state will be passed
/// to the given closure. The state returned by the closure will be stored so it can be referenced
@@ -4577,6 +4689,8 @@ pub enum ElementId {
NamedInteger(SharedString, u64),
/// A path.
Path(Arc<std::path::Path>),
/// A code location.
CodeLocation(core::panic::Location<'static>),
}
impl ElementId {
@@ -4596,6 +4710,7 @@ impl Display for ElementId {
ElementId::NamedInteger(s, i) => write!(f, "{}-{}", s, i)?,
ElementId::Uuid(uuid) => write!(f, "{}", uuid)?,
ElementId::Path(path) => write!(f, "{}", path.display())?,
ElementId::CodeLocation(location) => write!(f, "{}", location)?,
}
Ok(())

View File

@@ -53,6 +53,16 @@ pub fn derive_app_context(input: TokenStream) -> TokenStream {
self.#app_variable.update_entity(handle, update)
}
fn as_mut<'y, 'z, T>(
&'y mut self,
handle: &'z gpui::Entity<T>,
) -> Self::Result<gpui::GpuiBorrow<'y, T>>
where
T: 'static,
{
self.#app_variable.as_mut(handle)
}
fn read_entity<T, R>(
&self,
handle: &gpui::Entity<T>,

View File

@@ -181,6 +181,9 @@ pub enum IconName {
MicMute,
Microscope,
Minimize,
NewFromSummary,
NewTextThread,
NewThread,
Option,
PageDown,
PageUp,
@@ -256,6 +259,9 @@ pub enum IconName {
TextSnippet,
ThumbsDown,
ThumbsUp,
TodoComplete,
TodoPending,
TodoProgress,
ToolBulb,
ToolCopy,
ToolDeleteFile,

View File

@@ -206,8 +206,8 @@ impl LanguageModelRegistry {
None
}
/// Check that we have at least one provider that is authenticated.
fn has_authenticated_provider(&self, cx: &App) -> bool {
/// Returns `true` if at least one provider that is authenticated.
pub fn has_authenticated_provider(&self, cx: &App) -> bool {
self.providers.values().any(|p| p.is_authenticated(cx))
}

View File

@@ -1140,19 +1140,19 @@ impl RenderOnce for ZedAiConfiguration {
let is_pro = self.plan == Some(proto::Plan::ZedPro);
let subscription_text = match (self.plan, self.subscription_period) {
(Some(proto::Plan::ZedPro), Some(_)) => {
"You have access to Zed's hosted LLMs through your Pro subscription."
"You have access to Zed's hosted models through your Pro subscription."
}
(Some(proto::Plan::ZedProTrial), Some(_)) => {
"You have access to Zed's hosted LLMs through your Pro trial."
"You have access to Zed's hosted models through your Pro trial."
}
(Some(proto::Plan::Free), Some(_)) => {
"You have basic access to Zed's hosted LLMs through the Free plan."
"You have basic access to Zed's hosted models through the Free plan."
}
_ => {
if self.eligible_for_trial {
"Subscribe for access to Zed's hosted LLMs. Start with a 14 day free trial."
"Subscribe for access to Zed's hosted models. Start with a 14 day free trial."
} else {
"Subscribe for access to Zed's hosted LLMs."
"Subscribe for access to Zed's hosted models."
}
}
};
@@ -1166,7 +1166,7 @@ impl RenderOnce for ZedAiConfiguration {
Button::new("start_trial", "Start 14-day Free Pro Trial")
.style(ui::ButtonStyle::Tinted(ui::TintColor::Accent))
.full_width()
.on_click(|_, _, cx| cx.open_url(&zed_urls::account_url(cx)))
.on_click(|_, _, cx| cx.open_url(&zed_urls::start_trial_url(cx)))
.into_any_element()
} else {
Button::new("upgrade", "Upgrade to Pro")

View File

@@ -410,8 +410,20 @@ pub fn into_mistral(
.push_part(mistral::MessagePart::Text { text: text.clone() });
}
MessageContent::RedactedThinking(_) => {}
MessageContent::ToolUse(_) | MessageContent::ToolResult(_) => {
// Tool content is not supported in User messages for Mistral
MessageContent::ToolUse(_) => {
// Tool use is not supported in User messages for Mistral
}
MessageContent::ToolResult(tool_result) => {
let tool_content = match &tool_result.content {
LanguageModelToolResultContent::Text(text) => text.to_string(),
LanguageModelToolResultContent::Image(_) => {
"[Tool responded with an image, but Zed doesn't support these in Mistral models yet]".to_string()
}
};
messages.push(mistral::RequestMessage::Tool {
content: tool_content,
tool_call_id: tool_result.tool_use_id.to_string(),
});
}
}
}
@@ -482,24 +494,6 @@ pub fn into_mistral(
}
}
for message in &request.messages {
for content in &message.content {
if let MessageContent::ToolResult(tool_result) = content {
let content = match &tool_result.content {
LanguageModelToolResultContent::Text(text) => text.to_string(),
LanguageModelToolResultContent::Image(_) => {
"[Tool responded with an image, but Zed doesn't support these in Mistral models yet]".to_string()
}
};
messages.push(mistral::RequestMessage::Tool {
content,
tool_call_id: tool_result.tool_use_id.to_string(),
});
}
}
}
// The Mistral API requires that tool messages be followed by assistant messages,
// not user messages. When we have a tool->user sequence in the conversation,
// we need to insert a placeholder assistant message to maintain proper conversation

View File

@@ -18,7 +18,6 @@ client.workspace = true
collections.workspace = true
copilot.workspace = true
editor.workspace = true
feature_flags.workspace = true
futures.workspace = true
gpui.workspace = true
itertools.workspace = true

View File

@@ -1,13 +1,17 @@
use std::{collections::hash_map, path::PathBuf, rc::Rc, time::Duration};
use std::{
collections::{BTreeMap, HashMap},
path::{Path, PathBuf},
rc::Rc,
time::Duration,
};
use client::proto;
use collections::{HashMap, HashSet};
use collections::HashSet;
use editor::{Editor, EditorEvent};
use feature_flags::FeatureFlagAppExt as _;
use gpui::{Corner, Entity, Subscription, Task, WeakEntity, actions};
use language::{BinaryStatus, BufferId, LocalFile, ServerHealth};
use language::{BinaryStatus, BufferId, ServerHealth};
use lsp::{LanguageServerId, LanguageServerName, LanguageServerSelector};
use project::{LspStore, LspStoreEvent, project_settings::ProjectSettings};
use project::{LspStore, LspStoreEvent, Worktree, project_settings::ProjectSettings};
use settings::{Settings as _, SettingsStore};
use ui::{
Context, ContextMenu, ContextMenuEntry, ContextMenuItem, DocumentationAside, DocumentationSide,
@@ -36,8 +40,7 @@ pub struct LspTool {
#[derive(Debug)]
struct LanguageServerState {
items: Vec<LspItem>,
other_servers_start_index: Option<usize>,
items: Vec<LspMenuItem>,
workspace: WeakEntity<Workspace>,
lsp_store: WeakEntity<LspStore>,
active_editor: Option<ActiveEditor>,
@@ -63,8 +66,13 @@ impl std::fmt::Debug for ActiveEditor {
struct LanguageServers {
health_statuses: HashMap<LanguageServerId, LanguageServerHealthStatus>,
binary_statuses: HashMap<LanguageServerName, LanguageServerBinaryStatus>,
servers_per_buffer_abs_path:
HashMap<PathBuf, HashMap<LanguageServerId, Option<LanguageServerName>>>,
servers_per_buffer_abs_path: HashMap<PathBuf, ServersForPath>,
}
#[derive(Debug, Clone)]
struct ServersForPath {
servers: HashMap<LanguageServerId, Option<LanguageServerName>>,
worktree: Option<WeakEntity<Worktree>>,
}
#[derive(Debug, Clone)]
@@ -120,8 +128,8 @@ impl LanguageServerState {
};
let mut first_button_encountered = false;
for (i, item) in self.items.iter().enumerate() {
if let LspItem::ToggleServersButton { restart } = item {
for item in &self.items {
if let LspMenuItem::ToggleServersButton { restart } = item {
let label = if *restart {
"Restart All Servers"
} else {
@@ -140,22 +148,19 @@ impl LanguageServerState {
};
let project = workspace.read(cx).project().clone();
let buffer_store = project.read(cx).buffer_store().clone();
let worktree_store = project.read(cx).worktree_store();
let buffers = state
.read(cx)
.language_servers
.servers_per_buffer_abs_path
.keys()
.filter_map(|abs_path| {
worktree_store.read(cx).find_worktree(abs_path, cx)
})
.filter_map(|(worktree, relative_path)| {
let entry =
worktree.read(cx).entry_for_path(&relative_path)?;
project.read(cx).path_for_entry(entry.id, cx)
})
.filter_map(|project_path| {
.iter()
.filter_map(|(abs_path, servers)| {
let worktree =
servers.worktree.as_ref()?.upgrade()?.read(cx);
let relative_path =
abs_path.strip_prefix(&worktree.abs_path()).ok()?;
let entry = worktree.entry_for_path(&relative_path)?;
let project_path =
project.read(cx).path_for_entry(entry.id, cx)?;
buffer_store.read(cx).get_by_path(&project_path)
})
.collect();
@@ -165,13 +170,16 @@ impl LanguageServerState {
.iter()
// Do not try to use IDs as we have stopped all servers already, when allowing to restart them all
.flat_map(|item| match item {
LspItem::ToggleServersButton { .. } => None,
LspItem::WithHealthCheck(_, status, ..) => Some(
LanguageServerSelector::Name(status.name.clone()),
),
LspItem::WithBinaryStatus(_, server_name, ..) => Some(
LanguageServerSelector::Name(server_name.clone()),
LspMenuItem::Header { .. } => None,
LspMenuItem::ToggleServersButton { .. } => None,
LspMenuItem::WithHealthCheck { health, .. } => Some(
LanguageServerSelector::Name(health.name.clone()),
),
LspMenuItem::WithBinaryStatus {
server_name, ..
} => Some(LanguageServerSelector::Name(
server_name.clone(),
)),
})
.collect();
lsp_store.restart_language_servers_for_buffers(
@@ -190,13 +198,17 @@ impl LanguageServerState {
}
menu = menu.item(button);
continue;
};
} else if let LspMenuItem::Header { header, separator } = item {
menu = menu
.when(*separator, |menu| menu.separator())
.when_some(header.as_ref(), |menu, header| menu.header(header));
continue;
}
let Some(server_info) = item.server_info() else {
continue;
};
let workspace = self.workspace.clone();
let server_selector = server_info.server_selector();
// TODO currently, Zed remote does not work well with the LSP logs
// https://github.com/zed-industries/zed/issues/28557
@@ -205,6 +217,7 @@ impl LanguageServerState {
let status_color = server_info
.binary_status
.as_ref()
.and_then(|binary_status| match binary_status.status {
BinaryStatus::None => None,
BinaryStatus::CheckingForUpdate
@@ -223,17 +236,20 @@ impl LanguageServerState {
})
.unwrap_or(Color::Success);
if self
.other_servers_start_index
.is_some_and(|index| index == i)
{
menu = menu.separator().header("Other Buffers");
}
if i == 0 && self.other_servers_start_index.is_some() {
menu = menu.header("Current Buffer");
}
let message = server_info
.message
.as_ref()
.or_else(|| server_info.binary_status.as_ref()?.message.as_ref())
.cloned();
let hover_label = if has_logs {
Some("View Logs")
} else if message.is_some() {
Some("View Message")
} else {
None
};
let server_name = server_info.name.clone();
menu = menu.item(ContextMenuItem::custom_entry(
move |_, _| {
h_flex()
@@ -245,42 +261,99 @@ impl LanguageServerState {
h_flex()
.gap_2()
.child(Indicator::dot().color(status_color))
.child(Label::new(server_info.name.0.clone())),
)
.child(
h_flex()
.visible_on_hover("menu_item")
.child(
Label::new("View Logs")
.size(LabelSize::Small)
.color(Color::Muted),
)
.child(
Icon::new(IconName::ChevronRight)
.size(IconSize::Small)
.color(Color::Muted),
),
.child(Label::new(server_name.0.clone())),
)
.when_some(hover_label, |div, hover_label| {
div.child(
h_flex()
.visible_on_hover("menu_item")
.child(
Label::new(hover_label)
.size(LabelSize::Small)
.color(Color::Muted),
)
.child(
Icon::new(IconName::ChevronRight)
.size(IconSize::Small)
.color(Color::Muted),
),
)
})
.into_any_element()
},
{
let lsp_logs = lsp_logs.clone();
let message = message.clone();
let server_selector = server_selector.clone();
let server_name = server_info.name.clone();
let workspace = self.workspace.clone();
move |window, cx| {
if !has_logs {
if has_logs {
lsp_logs.update(cx, |lsp_logs, cx| {
lsp_logs.open_server_trace(
workspace.clone(),
server_selector.clone(),
window,
cx,
);
});
} else if let Some(message) = &message {
let Some(create_buffer) = workspace
.update(cx, |workspace, cx| {
workspace
.project()
.update(cx, |project, cx| project.create_buffer(cx))
})
.ok()
else {
return;
};
let window = window.window_handle();
let workspace = workspace.clone();
let message = message.clone();
let server_name = server_name.clone();
cx.spawn(async move |cx| {
let buffer = create_buffer.await?;
buffer.update(cx, |buffer, cx| {
buffer.edit(
[(
0..0,
format!("Language server {server_name}:\n\n{message}"),
)],
None,
cx,
);
buffer.set_capability(language::Capability::ReadOnly, cx);
})?;
workspace.update(cx, |workspace, cx| {
window.update(cx, |_, window, cx| {
workspace.add_item_to_active_pane(
Box::new(cx.new(|cx| {
let mut editor =
Editor::for_buffer(buffer, None, window, cx);
editor.set_read_only(true);
editor
})),
None,
true,
window,
cx,
);
})
})??;
anyhow::Ok(())
})
.detach();
} else {
cx.propagate();
return;
}
lsp_logs.update(cx, |lsp_logs, cx| {
lsp_logs.open_server_trace(
workspace.clone(),
server_selector.clone(),
window,
cx,
);
});
}
},
server_info.message.map(|server_message| {
message.map(|server_message| {
DocumentationAside::new(
DocumentationSide::Right,
Rc::new(move |_| Label::new(server_message.clone()).into_any_element()),
@@ -345,81 +418,95 @@ impl LanguageServers {
#[derive(Debug)]
enum ServerData<'a> {
WithHealthCheck(
LanguageServerId,
&'a LanguageServerHealthStatus,
Option<&'a LanguageServerBinaryStatus>,
),
WithBinaryStatus(
Option<LanguageServerId>,
&'a LanguageServerName,
&'a LanguageServerBinaryStatus,
),
}
#[derive(Debug)]
enum LspItem {
WithHealthCheck(
LanguageServerId,
LanguageServerHealthStatus,
Option<LanguageServerBinaryStatus>,
),
WithBinaryStatus(
Option<LanguageServerId>,
LanguageServerName,
LanguageServerBinaryStatus,
),
ToggleServersButton {
restart: bool,
WithHealthCheck {
server_id: LanguageServerId,
health: &'a LanguageServerHealthStatus,
binary_status: Option<&'a LanguageServerBinaryStatus>,
},
WithBinaryStatus {
server_id: Option<LanguageServerId>,
server_name: &'a LanguageServerName,
binary_status: &'a LanguageServerBinaryStatus,
},
}
impl LspItem {
#[derive(Debug)]
enum LspMenuItem {
WithHealthCheck {
server_id: LanguageServerId,
health: LanguageServerHealthStatus,
binary_status: Option<LanguageServerBinaryStatus>,
},
WithBinaryStatus {
server_id: Option<LanguageServerId>,
server_name: LanguageServerName,
binary_status: LanguageServerBinaryStatus,
},
ToggleServersButton {
restart: bool,
},
Header {
header: Option<SharedString>,
separator: bool,
},
}
impl LspMenuItem {
fn server_info(&self) -> Option<ServerInfo> {
match self {
LspItem::ToggleServersButton { .. } => None,
LspItem::WithHealthCheck(
language_server_id,
language_server_health_status,
language_server_binary_status,
) => Some(ServerInfo {
name: language_server_health_status.name.clone(),
id: Some(*language_server_id),
health: language_server_health_status.health(),
binary_status: language_server_binary_status.clone(),
message: language_server_health_status.message(),
}),
LspItem::WithBinaryStatus(
Self::Header { .. } => None,
Self::ToggleServersButton { .. } => None,
Self::WithHealthCheck {
server_id,
language_server_name,
language_server_binary_status,
) => Some(ServerInfo {
name: language_server_name.clone(),
health,
binary_status,
..
} => Some(ServerInfo {
name: health.name.clone(),
id: Some(*server_id),
health: health.health(),
binary_status: binary_status.clone(),
message: health.message(),
}),
Self::WithBinaryStatus {
server_id,
server_name,
binary_status,
..
} => Some(ServerInfo {
name: server_name.clone(),
id: *server_id,
health: None,
binary_status: Some(language_server_binary_status.clone()),
message: language_server_binary_status.message.clone(),
binary_status: Some(binary_status.clone()),
message: binary_status.message.clone(),
}),
}
}
}
impl ServerData<'_> {
fn name(&self) -> &LanguageServerName {
fn into_lsp_item(self) -> LspMenuItem {
match self {
Self::WithHealthCheck(_, state, _) => &state.name,
Self::WithBinaryStatus(_, name, ..) => name,
}
}
fn into_lsp_item(self) -> LspItem {
match self {
Self::WithHealthCheck(id, name, status) => {
LspItem::WithHealthCheck(id, name.clone(), status.cloned())
}
Self::WithBinaryStatus(server_id, name, status) => {
LspItem::WithBinaryStatus(server_id, name.clone(), status.clone())
}
Self::WithHealthCheck {
server_id,
health,
binary_status,
..
} => LspMenuItem::WithHealthCheck {
server_id,
health: health.clone(),
binary_status: binary_status.cloned(),
},
Self::WithBinaryStatus {
server_id,
server_name,
binary_status,
..
} => LspMenuItem::WithBinaryStatus {
server_id,
server_name: server_name.clone(),
binary_status: binary_status.clone(),
},
}
}
}
@@ -452,7 +539,6 @@ impl LspTool {
let state = cx.new(|_| LanguageServerState {
workspace: workspace.weak_handle(),
items: Vec::new(),
other_servers_start_index: None,
lsp_store: lsp_store.downgrade(),
active_editor: None,
language_servers: LanguageServers::default(),
@@ -542,13 +628,28 @@ impl LspTool {
message: proto::update_language_server::Variant::RegisteredForBuffer(update),
..
} => {
self.server_state.update(cx, |state, _| {
state
self.server_state.update(cx, |state, cx| {
let Ok(worktree) = state.workspace.update(cx, |workspace, cx| {
workspace
.project()
.read(cx)
.find_worktree(Path::new(&update.buffer_abs_path), cx)
.map(|(worktree, _)| worktree.downgrade())
}) else {
return;
};
let entry = state
.language_servers
.servers_per_buffer_abs_path
.entry(PathBuf::from(&update.buffer_abs_path))
.or_default()
.insert(*language_server_id, name.clone());
.or_insert_with(|| ServersForPath {
servers: HashMap::default(),
worktree: worktree.clone(),
});
entry.servers.insert(*language_server_id, name.clone());
if worktree.is_some() {
entry.worktree = worktree;
}
});
updated = true;
}
@@ -562,94 +663,95 @@ impl LspTool {
fn regenerate_items(&mut self, cx: &mut App) {
self.server_state.update(cx, |state, cx| {
let editor_buffers = state
let active_worktrees = state
.active_editor
.as_ref()
.map(|active_editor| active_editor.editor_buffers.clone())
.unwrap_or_default();
let editor_buffer_paths = editor_buffers
.iter()
.filter_map(|buffer_id| {
let buffer_path = state
.lsp_store
.update(cx, |lsp_store, cx| {
Some(
project::File::from_dyn(
lsp_store
.buffer_store()
.read(cx)
.get(*buffer_id)?
.read(cx)
.file(),
)?
.abs_path(cx),
)
.into_iter()
.flat_map(|active_editor| {
active_editor
.editor
.upgrade()
.into_iter()
.flat_map(|active_editor| {
active_editor
.read(cx)
.buffer()
.read(cx)
.all_buffers()
.into_iter()
.filter_map(|buffer| {
project::File::from_dyn(buffer.read(cx).file())
})
.map(|buffer_file| buffer_file.worktree.clone())
})
.ok()??;
Some(buffer_path)
})
.collect::<Vec<_>>();
.collect::<HashSet<_>>();
let mut servers_with_health_checks = HashSet::default();
let mut server_ids_with_health_checks = HashSet::default();
let mut buffer_servers =
Vec::with_capacity(state.language_servers.health_statuses.len());
let mut other_servers =
Vec::with_capacity(state.language_servers.health_statuses.len());
let buffer_server_ids = editor_buffer_paths
.iter()
.filter_map(|buffer_path| {
state
.language_servers
.servers_per_buffer_abs_path
.get(buffer_path)
})
.flatten()
.fold(HashMap::default(), |mut acc, (server_id, name)| {
match acc.entry(*server_id) {
hash_map::Entry::Occupied(mut o) => {
let old_name: &mut Option<&LanguageServerName> = o.get_mut();
if old_name.is_none() {
*old_name = name.as_ref();
}
}
hash_map::Entry::Vacant(v) => {
v.insert(name.as_ref());
let mut server_ids_to_worktrees =
HashMap::<LanguageServerId, Entity<Worktree>>::default();
let mut server_names_to_worktrees = HashMap::<
LanguageServerName,
HashSet<(Entity<Worktree>, LanguageServerId)>,
>::default();
for servers_for_path in state.language_servers.servers_per_buffer_abs_path.values() {
if let Some(worktree) = servers_for_path
.worktree
.as_ref()
.and_then(|worktree| worktree.upgrade())
{
for (server_id, server_name) in &servers_for_path.servers {
server_ids_to_worktrees.insert(*server_id, worktree.clone());
if let Some(server_name) = server_name {
server_names_to_worktrees
.entry(server_name.clone())
.or_default()
.insert((worktree.clone(), *server_id));
}
}
acc
}
}
let mut servers_per_worktree = BTreeMap::<SharedString, Vec<ServerData>>::new();
let mut servers_without_worktree = Vec::<ServerData>::new();
let mut servers_with_health_checks = HashSet::default();
for (server_id, health) in &state.language_servers.health_statuses {
let worktree = server_ids_to_worktrees.get(server_id).or_else(|| {
let worktrees = server_names_to_worktrees.get(&health.name)?;
worktrees
.iter()
.find(|(worktree, _)| active_worktrees.contains(worktree))
.or_else(|| worktrees.iter().next())
.map(|(worktree, _)| worktree)
});
for (server_id, server_state) in &state.language_servers.health_statuses {
let binary_status = state
.language_servers
.binary_statuses
.get(&server_state.name);
servers_with_health_checks.insert(&server_state.name);
server_ids_with_health_checks.insert(*server_id);
if buffer_server_ids.contains_key(server_id) {
buffer_servers.push(ServerData::WithHealthCheck(
*server_id,
server_state,
binary_status,
));
} else {
other_servers.push(ServerData::WithHealthCheck(
*server_id,
server_state,
binary_status,
));
servers_with_health_checks.insert(&health.name);
let worktree_name =
worktree.map(|worktree| SharedString::new(worktree.read(cx).root_name()));
let binary_status = state.language_servers.binary_statuses.get(&health.name);
let server_data = ServerData::WithHealthCheck {
server_id: *server_id,
health,
binary_status,
};
match worktree_name {
Some(worktree_name) => servers_per_worktree
.entry(worktree_name.clone())
.or_default()
.push(server_data),
None => servers_without_worktree.push(server_data),
}
}
let mut can_stop_all = !state.language_servers.health_statuses.is_empty();
let mut can_restart_all = state.language_servers.health_statuses.is_empty();
for (server_name, status) in state
for (server_name, binary_status) in state
.language_servers
.binary_statuses
.iter()
.filter(|(name, _)| !servers_with_health_checks.contains(name))
{
match status.status {
match binary_status.status {
BinaryStatus::None => {
can_restart_all = false;
can_stop_all |= true;
@@ -674,52 +776,73 @@ impl LspTool {
BinaryStatus::Failed { .. } => {}
}
let matching_server_id = state
.language_servers
.servers_per_buffer_abs_path
.iter()
.filter(|(path, _)| editor_buffer_paths.contains(path))
.flat_map(|(_, server_associations)| server_associations.iter())
.find_map(|(id, name)| {
if name.as_ref() == Some(server_name) {
Some(*id)
} else {
None
match server_names_to_worktrees.get(server_name) {
Some(worktrees_for_name) => {
match worktrees_for_name
.iter()
.find(|(worktree, _)| active_worktrees.contains(worktree))
.or_else(|| worktrees_for_name.iter().next())
{
Some((worktree, server_id)) => {
let worktree_name =
SharedString::new(worktree.read(cx).root_name());
servers_per_worktree
.entry(worktree_name.clone())
.or_default()
.push(ServerData::WithBinaryStatus {
server_name,
binary_status,
server_id: Some(*server_id),
});
}
None => servers_without_worktree.push(ServerData::WithBinaryStatus {
server_name,
binary_status,
server_id: None,
}),
}
});
if let Some(server_id) = matching_server_id {
buffer_servers.push(ServerData::WithBinaryStatus(
Some(server_id),
}
None => servers_without_worktree.push(ServerData::WithBinaryStatus {
server_name,
status,
));
} else {
other_servers.push(ServerData::WithBinaryStatus(None, server_name, status));
binary_status,
server_id: None,
}),
}
}
buffer_servers.sort_by_key(|data| data.name().clone());
other_servers.sort_by_key(|data| data.name().clone());
let mut other_servers_start_index = None;
let mut new_lsp_items =
Vec::with_capacity(buffer_servers.len() + other_servers.len() + 1);
new_lsp_items.extend(buffer_servers.into_iter().map(ServerData::into_lsp_item));
if !new_lsp_items.is_empty() {
other_servers_start_index = Some(new_lsp_items.len());
Vec::with_capacity(servers_per_worktree.len() + servers_without_worktree.len() + 2);
for (worktree_name, worktree_servers) in servers_per_worktree {
if worktree_servers.is_empty() {
continue;
}
new_lsp_items.push(LspMenuItem::Header {
header: Some(worktree_name),
separator: false,
});
new_lsp_items.extend(worktree_servers.into_iter().map(ServerData::into_lsp_item));
}
if !servers_without_worktree.is_empty() {
new_lsp_items.push(LspMenuItem::Header {
header: Some(SharedString::from("Unknown worktree")),
separator: false,
});
new_lsp_items.extend(
servers_without_worktree
.into_iter()
.map(ServerData::into_lsp_item),
);
}
new_lsp_items.extend(other_servers.into_iter().map(ServerData::into_lsp_item));
if !new_lsp_items.is_empty() {
if can_stop_all {
new_lsp_items.push(LspItem::ToggleServersButton { restart: true });
new_lsp_items.push(LspItem::ToggleServersButton { restart: false });
new_lsp_items.push(LspMenuItem::ToggleServersButton { restart: true });
new_lsp_items.push(LspMenuItem::ToggleServersButton { restart: false });
} else if can_restart_all {
new_lsp_items.push(LspItem::ToggleServersButton { restart: true });
new_lsp_items.push(LspMenuItem::ToggleServersButton { restart: true });
}
}
state.items = new_lsp_items;
state.other_servers_start_index = other_servers_start_index;
});
}
@@ -841,10 +964,7 @@ impl StatusItemView for LspTool {
impl Render for LspTool {
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl ui::IntoElement {
if !cx.is_staff()
|| self.server_state.read(cx).language_servers.is_empty()
|| self.lsp_menu.is_none()
{
if self.server_state.read(cx).language_servers.is_empty() || self.lsp_menu.is_none() {
return div();
}
@@ -852,12 +972,12 @@ impl Render for LspTool {
let mut has_warnings = false;
let mut has_other_notifications = false;
let state = self.server_state.read(cx);
for server in state.language_servers.health_statuses.values() {
if let Some(binary_status) = &state.language_servers.binary_statuses.get(&server.name) {
has_errors |= matches!(binary_status.status, BinaryStatus::Failed { .. });
has_other_notifications |= binary_status.message.is_some();
}
for binary_status in state.language_servers.binary_statuses.values() {
has_errors |= matches!(binary_status.status, BinaryStatus::Failed { .. });
has_other_notifications |= binary_status.message.is_some();
}
for server in state.language_servers.health_statuses.values() {
if let Some((message, health)) = &server.health {
has_other_notifications |= message.is_some();
match health {

View File

@@ -231,6 +231,13 @@ impl JsonLspAdapter {
))
}
schemas
.as_array_mut()
.unwrap()
.extend(cx.all_action_names().into_iter().map(|&name| {
project::lsp_store::json_language_server_ext::url_schema_for_action(name)
}));
// This can be viewed via `dev: open language server logs` -> `json-language-server` ->
// `Server Info`
serde_json::json!({

View File

@@ -273,6 +273,7 @@ pub fn init(languages: Arc<LanguageRegistry>, node: NodeRuntime, cx: &mut App) {
"Astro",
"CSS",
"ERB",
"HTML/ERB",
"HEEX",
"HTML",
"JavaScript",

View File

@@ -179,6 +179,7 @@ impl LspAdapter for TailwindLspAdapter {
("Elixir".to_string(), "phoenix-heex".to_string()),
("HEEX".to_string(), "phoenix-heex".to_string()),
("ERB".to_string(), "erb".to_string()),
("HTML/ERB".to_string(), "erb".to_string()),
("PHP".to_string(), "php".to_string()),
("Vue.js".to_string(), "vue".to_string()),
])

View File

@@ -326,11 +326,11 @@ pub(crate) async fn capture_local_video_track(
capture_source: &dyn ScreenCaptureSource,
cx: &mut gpui::AsyncApp,
) -> Result<(crate::LocalVideoTrack, Box<dyn ScreenCaptureStream>)> {
let resolution = capture_source.resolution()?;
let metadata = capture_source.metadata()?;
let track_source = gpui_tokio::Tokio::spawn(cx, async move {
NativeVideoSource::new(VideoResolution {
width: resolution.width.0 as u32,
height: resolution.height.0 as u32,
width: metadata.resolution.width.0 as u32,
height: metadata.resolution.height.0 as u32,
})
})?
.await?;

View File

@@ -5,7 +5,7 @@ use crate::{
};
use anyhow::Result;
use collections::HashMap;
use gpui::{AsyncApp, ScreenCaptureSource, ScreenCaptureStream};
use gpui::{AsyncApp, ScreenCaptureSource, ScreenCaptureStream, TestScreenCaptureStream};
#[derive(Clone, Debug)]
pub struct LocalParticipant {
@@ -119,7 +119,3 @@ impl RemoteParticipant {
self.identity.clone()
}
}
struct TestScreenCaptureStream;
impl gpui::ScreenCaptureStream for TestScreenCaptureStream {}

View File

@@ -25,7 +25,7 @@ fn replace_string_action(
None
}
/// "ctrl-k ctrl-1": "inline_completion::ToggleMenu" -> "edit_prediction::ToggleMenu"
/// "space": "outline_panel::Open" -> "outline_panel::OpenSelectedEntry"
static STRING_REPLACE: LazyLock<HashMap<&str, &str>> = LazyLock::new(|| {
HashMap::from_iter([("outline_panel::Open", "outline_panel::OpenSelectedEntry")])
});

View File

@@ -0,0 +1,28 @@
[package]
name = "onboarding"
version = "0.1.0"
edition.workspace = true
publish.workspace = true
license = "GPL-3.0-or-later"
[lints]
workspace = true
[lib]
path = "src/onboarding.rs"
[features]
default = []
[dependencies]
anyhow.workspace = true
command_palette_hooks.workspace = true
db.workspace = true
feature_flags.workspace = true
fs.workspace = true
gpui.workspace = true
settings.workspace = true
theme.workspace = true
ui.workspace = true
workspace.workspace = true
workspace-hack.workspace = true

View File

@@ -0,0 +1 @@
../../../LICENSE-GPL

View File

@@ -0,0 +1,352 @@
use command_palette_hooks::CommandPaletteFilter;
use db::kvp::KEY_VALUE_STORE;
use feature_flags::{FeatureFlag, FeatureFlagViewExt as _};
use fs::Fs;
use gpui::{
AnyElement, App, AppContext, Context, Entity, EventEmitter, FocusHandle, Focusable,
IntoElement, Render, SharedString, Subscription, Task, WeakEntity, Window, actions,
};
use settings::{Settings, SettingsStore, update_settings_file};
use std::sync::Arc;
use theme::{ThemeMode, ThemeSettings};
use ui::{
ButtonCommon as _, ButtonSize, ButtonStyle, Clickable as _, Color, Divider, FluentBuilder,
Headline, InteractiveElement, KeyBinding, Label, LabelCommon, ParentElement as _,
StatefulInteractiveElement, Styled, ToggleButton, Toggleable as _, Vector, VectorName, div,
h_flex, rems, v_container, v_flex,
};
use workspace::{
AppState, Workspace, WorkspaceId,
dock::DockPosition,
item::{Item, ItemEvent},
open_new, with_active_or_new_workspace,
};
pub struct OnBoardingFeatureFlag {}
impl FeatureFlag for OnBoardingFeatureFlag {
const NAME: &'static str = "onboarding";
}
pub const FIRST_OPEN: &str = "first_open";
actions!(
zed,
[
/// Opens the onboarding view.
OpenOnboarding
]
);
pub fn init(cx: &mut App) {
cx.on_action(|_: &OpenOnboarding, cx| {
with_active_or_new_workspace(cx, |workspace, window, cx| {
workspace
.with_local_workspace(window, cx, |workspace, window, cx| {
let existing = workspace
.active_pane()
.read(cx)
.items()
.find_map(|item| item.downcast::<Onboarding>());
if let Some(existing) = existing {
workspace.activate_item(&existing, true, true, window, cx);
} else {
let settings_page = Onboarding::new(workspace.weak_handle(), cx);
workspace.add_item_to_active_pane(
Box::new(settings_page),
None,
true,
window,
cx,
)
}
})
.detach();
});
});
cx.observe_new::<Workspace>(|_, window, cx| {
let Some(window) = window else {
return;
};
let onboarding_actions = [std::any::TypeId::of::<OpenOnboarding>()];
CommandPaletteFilter::update_global(cx, |filter, _cx| {
filter.hide_action_types(&onboarding_actions);
});
cx.observe_flag::<OnBoardingFeatureFlag, _>(window, move |is_enabled, _, _, cx| {
if is_enabled {
CommandPaletteFilter::update_global(cx, |filter, _cx| {
filter.show_action_types(onboarding_actions.iter());
});
} else {
CommandPaletteFilter::update_global(cx, |filter, _cx| {
filter.hide_action_types(&onboarding_actions);
});
}
})
.detach();
})
.detach();
}
pub fn show_onboarding_view(app_state: Arc<AppState>, cx: &mut App) -> Task<anyhow::Result<()>> {
open_new(
Default::default(),
app_state,
cx,
|workspace, window, cx| {
{
workspace.toggle_dock(DockPosition::Left, window, cx);
let onboarding_page = Onboarding::new(workspace.weak_handle(), cx);
workspace.add_item_to_center(Box::new(onboarding_page.clone()), window, cx);
window.focus(&onboarding_page.focus_handle(cx));
cx.notify();
};
db::write_and_log(cx, || {
KEY_VALUE_STORE.write_kvp(FIRST_OPEN.to_string(), "false".to_string())
});
},
)
}
fn read_theme_selection(cx: &App) -> ThemeMode {
let settings = ThemeSettings::get_global(cx);
settings
.theme_selection
.as_ref()
.and_then(|selection| selection.mode())
.unwrap_or_default()
}
fn write_theme_selection(theme_mode: ThemeMode, cx: &App) {
let fs = <dyn Fs>::global(cx);
update_settings_file::<ThemeSettings>(fs, cx, move |settings, _| {
settings.set_mode(theme_mode);
});
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum SelectedPage {
Basics,
Editing,
AiSetup,
}
struct Onboarding {
workspace: WeakEntity<Workspace>,
focus_handle: FocusHandle,
selected_page: SelectedPage,
_settings_subscription: Subscription,
}
impl Onboarding {
fn new(workspace: WeakEntity<Workspace>, cx: &mut App) -> Entity<Self> {
cx.new(|cx| Self {
workspace,
focus_handle: cx.focus_handle(),
selected_page: SelectedPage::Basics,
_settings_subscription: cx.observe_global::<SettingsStore>(move |_, cx| cx.notify()),
})
}
fn render_page_nav(
&mut self,
page: SelectedPage,
_: &mut Window,
cx: &mut Context<Self>,
) -> impl IntoElement {
let text = match page {
SelectedPage::Basics => "Basics",
SelectedPage::Editing => "Editing",
SelectedPage::AiSetup => "AI Setup",
};
let binding = match page {
SelectedPage::Basics => {
KeyBinding::new(vec![gpui::Keystroke::parse("cmd-1").unwrap()], cx)
}
SelectedPage::Editing => {
KeyBinding::new(vec![gpui::Keystroke::parse("cmd-2").unwrap()], cx)
}
SelectedPage::AiSetup => {
KeyBinding::new(vec![gpui::Keystroke::parse("cmd-3").unwrap()], cx)
}
};
let selected = self.selected_page == page;
h_flex()
.id(text)
.rounded_sm()
.child(text)
.child(binding)
.h_8()
.gap_2()
.px_2()
.py_0p5()
.w_full()
.justify_between()
.map(|this| {
if selected {
this.bg(Color::Selected.color(cx))
.border_l_1()
.border_color(Color::Accent.color(cx))
} else {
this.text_color(Color::Muted.color(cx))
}
})
.hover(|style| {
if selected {
style.bg(Color::Selected.color(cx).opacity(0.6))
} else {
style.bg(Color::Selected.color(cx).opacity(0.3))
}
})
.on_click(cx.listener(move |this, _, _, cx| {
this.selected_page = page;
cx.notify();
}))
}
fn render_page(&mut self, window: &mut Window, cx: &mut Context<Self>) -> AnyElement {
match self.selected_page {
SelectedPage::Basics => self.render_basics_page(window, cx).into_any_element(),
SelectedPage::Editing => self.render_editing_page(window, cx).into_any_element(),
SelectedPage::AiSetup => self.render_ai_setup_page(window, cx).into_any_element(),
}
}
fn render_basics_page(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let theme_mode = read_theme_selection(cx);
v_container().child(
h_flex()
.items_center()
.justify_between()
.child(Label::new("Theme"))
.child(
h_flex()
.rounded_md()
.child(
ToggleButton::new("light", "Light")
.style(ButtonStyle::Filled)
.size(ButtonSize::Large)
.toggle_state(theme_mode == ThemeMode::Light)
.on_click(|_, _, cx| write_theme_selection(ThemeMode::Light, cx))
.first(),
)
.child(
ToggleButton::new("dark", "Dark")
.style(ButtonStyle::Filled)
.size(ButtonSize::Large)
.toggle_state(theme_mode == ThemeMode::Dark)
.on_click(|_, _, cx| write_theme_selection(ThemeMode::Dark, cx))
.last(),
)
.child(
ToggleButton::new("system", "System")
.style(ButtonStyle::Filled)
.size(ButtonSize::Large)
.toggle_state(theme_mode == ThemeMode::System)
.on_click(|_, _, cx| write_theme_selection(ThemeMode::System, cx))
.middle(),
),
),
)
}
fn render_editing_page(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
// div().child("editing page")
"Right"
}
fn render_ai_setup_page(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
div().child("ai setup page")
}
}
impl Render for Onboarding {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
h_flex()
.image_cache(gpui::retain_all("onboarding-page"))
.key_context("onboarding-page")
.px_24()
.py_12()
.items_start()
.child(
v_flex()
.w_1_3()
.h_full()
.child(
h_flex()
.pt_0p5()
.child(Vector::square(VectorName::ZedLogo, rems(2.)))
.child(
v_flex()
.left_1()
.items_center()
.child(Headline::new("Welcome to Zed"))
.child(
Label::new("The editor for what's next")
.color(Color::Muted)
.italic(),
),
),
)
.p_1()
.child(Divider::horizontal_dashed())
.child(
v_flex().gap_1().children([
self.render_page_nav(SelectedPage::Basics, window, cx)
.into_element(),
self.render_page_nav(SelectedPage::Editing, window, cx)
.into_element(),
self.render_page_nav(SelectedPage::AiSetup, window, cx)
.into_element(),
]),
),
)
// .child(Divider::vertical_dashed())
.child(div().w_2_3().h_full().child(self.render_page(window, cx)))
}
}
impl EventEmitter<ItemEvent> for Onboarding {}
impl Focusable for Onboarding {
fn focus_handle(&self, _: &App) -> gpui::FocusHandle {
self.focus_handle.clone()
}
}
impl Item for Onboarding {
type Event = ItemEvent;
fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
"Onboarding".into()
}
fn telemetry_event_text(&self) -> Option<&'static str> {
Some("Onboarding Page Opened")
}
fn show_toolbar(&self) -> bool {
false
}
fn clone_on_split(
&self,
_workspace_id: Option<WorkspaceId>,
_: &mut Window,
cx: &mut Context<Self>,
) -> Option<Entity<Self>> {
Some(Onboarding::new(self.workspace.clone(), cx))
}
fn to_item_events(event: &Self::Event, mut f: impl FnMut(workspace::item::ItemEvent)) {
f(*event)
}
}

View File

@@ -610,7 +610,7 @@ mod tests {
use context_server::test::create_fake_transport;
use gpui::{AppContext, TestAppContext, UpdateGlobal as _};
use serde_json::json;
use std::{cell::RefCell, rc::Rc};
use std::{cell::RefCell, path::PathBuf, rc::Rc};
use util::path;
#[gpui::test]
@@ -931,7 +931,7 @@ mod tests {
ContextServerSettings::Custom {
enabled: true,
command: ContextServerCommand {
path: "somebinary".to_string(),
path: "somebinary".into(),
args: vec!["arg".to_string()],
env: None,
},
@@ -971,7 +971,7 @@ mod tests {
ContextServerSettings::Custom {
enabled: true,
command: ContextServerCommand {
path: "somebinary".to_string(),
path: "somebinary".into(),
args: vec!["anotherArg".to_string()],
env: None,
},
@@ -1053,7 +1053,7 @@ mod tests {
ContextServerSettings::Custom {
enabled: true,
command: ContextServerCommand {
path: "somebinary".to_string(),
path: "somebinary".into(),
args: vec!["arg".to_string()],
env: None,
},
@@ -1104,7 +1104,7 @@ mod tests {
ContextServerSettings::Custom {
enabled: false,
command: ContextServerCommand {
path: "somebinary".to_string(),
path: "somebinary".into(),
args: vec!["arg".to_string()],
env: None,
},
@@ -1132,7 +1132,7 @@ mod tests {
ContextServerSettings::Custom {
enabled: true,
command: ContextServerCommand {
path: "somebinary".to_string(),
path: "somebinary".into(),
args: vec!["arg".to_string()],
env: None,
},
@@ -1184,7 +1184,7 @@ mod tests {
ContextServerSettings::Custom {
enabled: true,
command: ContextServerCommand {
path: "somebinary".to_string(),
path: "somebinary".into(),
args: vec!["arg".to_string()],
env: None,
},
@@ -1256,11 +1256,11 @@ mod tests {
}
struct FakeContextServerDescriptor {
path: String,
path: PathBuf,
}
impl FakeContextServerDescriptor {
fn new(path: impl Into<String>) -> Self {
fn new(path: impl Into<PathBuf>) -> Self {
Self { path: path.into() }
}
}

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