Compare commits

..

62 Commits

Author SHA1 Message Date
Junkui Zhang
ce9edd7980 test 2025-05-23 21:52:46 +08:00
Junkui Zhang
d2235ae5cd test 2025-05-23 21:43:46 +08:00
Junkui Zhang
aeb2b39e00 test 2025-05-23 21:20:59 +08:00
Junkui Zhang
ed286ed1aa test x11 2025-05-22 20:38:11 +08:00
Junkui Zhang
aa0f53df5c test 2025-05-22 16:42:57 +08:00
Junkui Zhang
da715d3ab1 wtf? 2025-05-21 21:46:02 +08:00
Marshall Bowers
3ee56c196c collab: Add GET /users/look_up endpoint (#31059)
This PR adds a new `GET /users/look_up` endpoint for retrieving users by
various identifiers.

This endpoint can look up users by the following identifiers:

- Zed user ID
- Stripe Customer ID
- Stripe Subscription ID
- Email address
- GitHub login

Release Notes:

- N/A
2025-05-21 01:29:16 +00:00
张小白
3b1f6eaab8 client: Try to re-introduce HTTP/HTTPS proxy (#31002)
When building for the `x86_64-unknown-linux-musl` target, the default
`openssl-dev` is compiled for the GNU toolchain, which causes a build
error due to missing OpenSSL. This PR fixes the issue by avoiding the
use of OpenSSL on non-macOS and non-Windows platforms.


Release Notes:

- N/A
2025-05-21 09:08:32 +08:00
Remco Smits
44fbe27d31 wrap_map: Add capacity to vectors for better performance (#31055)
Release Notes:

- N/A
2025-05-20 23:44:19 +00:00
Remco Smits
a824119367 Fix performance issues in project search related to detecting JSX tag auto-closing (#30842)
This PR changes it so we only create a snapshot and get the syntax tree
for a buffer if we didn't detect that auto_close is enabled.

<img width="1205" alt="Screenshot 2025-05-16 at 21 10 28"
src="https://github.com/user-attachments/assets/1ada445f-77bc-4c7c-bffe-953f34ee5384"
/>


Release Notes:

- Improved project search performance
2025-05-21 02:37:09 +03:00
Kirill Bulatov
16366cf9f2 Use anyhow more idiomatically (#31052)
https://github.com/zed-industries/zed/issues/30972 brought up another
case where our context is not enough to track the actual source of the
issue: we get a general top-level error without inner error.

The reason for this was `.ok_or_else(|| anyhow!("failed to read HEAD
SHA"))?; ` on the top level.

The PR finally reworks the way we use anyhow to reduce such issues (or
at least make it simpler to bubble them up later in a fix).
On top of that, uses a few more anyhow methods for better readability.

* `.ok_or_else(|| anyhow!("..."))`, `map_err` and other similar error
conversion/option reporting cases are replaced with `context` and
`with_context` calls
* in addition to that, various `anyhow!("failed to do ...")` are
stripped with `.context("Doing ...")` messages instead to remove the
parasitic `failed to` text
* `anyhow::ensure!` is used instead of `if ... { return Err(...); }`
calls
* `anyhow::bail!` is used instead of `return Err(anyhow!(...));`

Release Notes:

- N/A
2025-05-20 23:06:07 +00:00
Cole Miller
1e51a7ac44 Don't pass -z flag to git-cat-file (#31053)
Closes #30972 

Release Notes:

- Fixed a bug that prevented the `copy permalink to line` action from
working on systems with older versions of git.
2025-05-20 22:39:41 +00:00
smit
d547a86e31 editor: Hide hover popover when code actions context menu is triggered (#31042)
This PR hides hover info/diagnostic popovers when code action menu is
shown. We already hide hover info/diagnostic popover on code completion
menu trigger (handled on input).

Note: It is still possible to see hover popover if code completion or
code action menu is already open. This is intended behavior.

- [x] Test hover popover hides when code action is triggered

Release Notes:

- Fixed issue where info and diagnostic hover popovers were still
visible when code action menu is triggered.
2025-05-21 03:31:35 +05:30
Richard Feldman
4bb04cef9d Accept wrapped text content from LLM providers (#31048)
Some providers sometimes send `{ "type": "text", "text": ... }` instead
of just the text as a string. Now we accept those instead of erroring.

Release Notes:

- N/A
2025-05-20 20:50:02 +00:00
Peter Tripp
89700c3682 sublime: Don't map editor::FindNextMatch by default (#31029)
Closes: https://github.com/zed-industries/zed/issues/29535

Broken in: https://github.com/zed-industries/zed/pull/28559/files

Removes `editor::FindNextMatch` and `editor::FindPreviousMatch` from the
default sublime mappings. If you would like to use this, you will have
to add them to your user keymap. Reverts the previous behavior where
cmd-g / cmd-shift-g relies on the base keymap.

Linux:
```json
  {
    "context": "Editor && mode == full",
    "bindings": {
      "f3": "editor::FindNextMatch",
      "shift-f3": "editor::FindPreviousMatch"
    }
  }
```

MacOS:
```json
  {
    "context": "Editor && mode == full",
    "bindings": {
      "cmd-g": "editor::FindNextMatch",
      "cmd-shift-g": "editor::FindPreviousMatch"
    }
  },
```


Release Notes:

- Fixed a regression in Sublime Text keymap for find next/previous in
the search bar
2025-05-20 17:52:11 +00:00
Erik Funder Carstensen
7609402200 Remove alt-. keybinding from terminal on macOS (#30827)
Closes: #30730
It conflicts with the `>` key on the Czech keyboard layout  
If you want the previous behavior, add `"alt-.": ["terminal::SendText",
"\u001b."]` to your keymap under the `Terminal` context.

Release Notes: 

- Improved the default terminal keybind to not conflict on Czech
keyboards

Co-authored-by: Peter Tripp <peter@zed.dev>
2025-05-20 13:37:26 -04:00
Andres Suarez
a0ec9cf383 telemetry: Consider the entire chain of config sources when merging (#31039)
Global settings were implemented in #30444, but `Settings`
implementations need to consider that source for it to be useful. This
PR does just that for `TelemetrySettings` so these can be controlled via
global settings.

Release Notes:

- N/A
2025-05-20 10:21:49 -07:00
Ben Kunkle
eb318c1626 Revert "linux(x11): Add support for pasting images from clipboard (#29387)" (#31033)
Closes: #30523

Release Notes:

- linux: Reverted the ability to paste images on X11, as the change
broke pasting from some external applications
2025-05-20 13:05:24 -04:00
Oleksiy Syvokon
5e5a124ae1 evals: Eval for creating an empty file (#31034)
This eval checks that Edit Agent can create an empty file without
writing its thoughts into it. This issue is not specific to empty files,
but it's easier to reproduce with them.

For some mysterious reason, I could easily reproduce this issue roughly
90% of the time in actual Zed. However, once I extract the exact LLM
request before the failure point and generate from that, the
reproduction rate drops to 2%!

Things I've tried to make sure it's not a fluke: disabling prompt
caching, capturing the LLM request via a proxy server, running the
prompt on Claude separately from evals. Every time it was mostly giving
good outcomes, which doesn't match my actual experience in Zed.

At some point I discovered that simply adding one insignificant space or
a newline to the prompt suddenly results in an outcome I tried to
reproduce almost perfectly.

This weirdness happens even outside the Zed code base and even when
using a different subscription. The result is the same: an extra newline
or space changes the model behavior significantly enough, so that the
pass rate drops from 99% to 0-3%

I have no explanation to this.


Release Notes:

- N/A
2025-05-20 20:03:08 +03:00
Jason Lee
65e751ca33 Revert "gpui: Fix shape_text split to support \r\n" (#31031)
Reverts zed-industries/zed#31022

Sorry @mikayla-maki, I found that things are more complicated than I
thought.

The lines returned by shape_text must maintain the same length as all
the original characters, otherwise the subsequent offset needs to always
consider the difference of `\r\n` or `\n` to do the offset.

Before, we only needed to add +1 after each offset after the line, but
now we need to consider +1 or +2, which is much more complicated.
2025-05-20 16:01:47 +00:00
Piotr Osiewicz
17cf04558b debugger: Surface validity of breakpoints (#30380)
We now show on the breakpoint itself whether it can ever be hit.

![image](https://github.com/user-attachments/assets/148d7712-53c9-4a0a-9fc0-4ff80dec5fb1)

Release Notes:

- N/A

---------

Signed-off-by: Umesh Yadav <git@umesh.dev>
Co-authored-by: Anthony <anthony@zed.dev>
Co-authored-by: Cole Miller <cole@zed.dev>
Co-authored-by: Michael Sloan <michael@zed.dev>
Co-authored-by: Marshall Bowers <git@maxdeviant.com>
Co-authored-by: Ben Kunkle <ben@zed.dev>
Co-authored-by: Danilo Leal <67129314+danilo-leal@users.noreply.github.com>
Co-authored-by: Agus Zubiaga <hi@aguz.me>
Co-authored-by: Ben Brandt <benjamin.j.brandt@gmail.com>
Co-authored-by: Agus Zubiaga <agus@zed.dev>
Co-authored-by: Danilo Leal <daniloleal09@gmail.com>
Co-authored-by: Richard Feldman <oss@rtfeldman.com>
Co-authored-by: Max Brunsfeld <maxbrunsfeld@gmail.com>
Co-authored-by: Smit Barmase <heysmitbarmase@gmail.com>
Co-authored-by: peppidesu <bakker.pepijn@gmail.com>
Co-authored-by: Kirill Bulatov <kirill@zed.dev>
Co-authored-by: Ben Kunkle <ben.kunkle@gmail.com>
Co-authored-by: Jens Krause <47693+sectore@users.noreply.github.com>
Co-authored-by: Bennet Bo Fenner <bennet@zed.dev>
Co-authored-by: Max Nordlund <max.nordlund@gmail.com>
Co-authored-by: Finn Evers <dev@bahn.sh>
Co-authored-by: tidely <43219534+tidely@users.noreply.github.com>
Co-authored-by: Sergei Kartsev <kartsevsb@gmail.com>
Co-authored-by: Shardul Vaidya <31039336+5herlocked@users.noreply.github.com>
Co-authored-by: Chris Kelly <amateurhuman@gmail.com>
Co-authored-by: Peter Tripp <peter@zed.dev>
Co-authored-by: Umesh Yadav <23421535+imumesh18@users.noreply.github.com>
Co-authored-by: Julia Ryan <juliaryan3.14@gmail.com>
Co-authored-by: Cole Miller <m@cole-miller.net>
Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
Co-authored-by: william341 <wwokwilliam@gmail.com>
Co-authored-by: Liam <33645555+lj3954@users.noreply.github.com>
Co-authored-by: AidanV <aidanvanduyne@gmail.com>
Co-authored-by: imumesh18 <umesh4257@gmail.com>
Co-authored-by: d1y <chenhonzhou@gmail.com>
Co-authored-by: AidanV <84053180+AidanV@users.noreply.github.com>
Co-authored-by: Anthony Eid <56899983+Anthony-Eid@users.noreply.github.com>
Co-authored-by: 张小白 <364772080@qq.com>
Co-authored-by: THELOSTSOUL <1095533751@qq.com>
Co-authored-by: Ron Harel <55725807+ronharel02@users.noreply.github.com>
Co-authored-by: Tristan Hume <tristan@anthropic.com>
Co-authored-by: Stanislav Alekseev <43210583+WeetHet@users.noreply.github.com>
Co-authored-by: Joseph T. Lyons <JosephTLyons@gmail.com>
Co-authored-by: Remco Smits <djsmits12@gmail.com>
Co-authored-by: Anthony Eid <hello@anthonyeid.me>
Co-authored-by: Oleksiy Syvokon <oleksiy@zed.dev>
Co-authored-by: Thomas David Baker <bakert@gmail.com>
Co-authored-by: Nate Butler <iamnbutler@gmail.com>
Co-authored-by: Mikayla Maki <mikayla@zed.dev>
Co-authored-by: Rob McBroom <github@skurfer.com>
Co-authored-by: CharlesChen0823 <yongchen0823@gmail.com>
2025-05-20 15:56:15 +00:00
Ben Kunkle
36ae564b61 Project Search: Don't prompt to save edited buffers in project search results if buffers open elsewhere (#31026)
Closes #ISSUE

Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-05-20 15:34:42 +00:00
Marshall Bowers
110195cdae collab: Only create a Zed Free subscription if there is no other active subscription (#31023)
This PR makes it so we only create a Zed Free subscription if there is
no other active subscription, rather than just having another Zed Free
subscription.

Release Notes:

- N/A

Co-authored-by: Max Brunsfeld <maxbrunsfeld@gmail.com>
2025-05-20 15:00:10 +00:00
Jason Lee
b7d5e6480a gpui: Fix shape_text split to support \r\n (#31022)
Release Notes:

- N/A

---

Today I check the shape_text result on Windows, I get:

<img width="409" alt="屏幕截图 2025-05-20 222908"
src="https://github.com/user-attachments/assets/3ee93911-3de1-4e01-9433-00c626fc2369"
/>

Here the `shape_text` split logic I think it should use `lines` method,
not `split('\n')`, the newline on Windows is `\r\n`.
2025-05-20 14:53:50 +00:00
Jason Lee
0fa9f05313 gpui: Fix update_window to borrow_mut will crash on Windows (#24545)
Release Notes:

- N/A

---


When we use `window_handle` to draw WebView on Windows, this will crash
by:

This error caused by when used WebView2.

```
thread 'main' panicked at crates\gpui\src\app\async_context.rs:91:28:
already borrowed: BorrowMutError
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
thread 'main' panicked at library\core\src\panicking.rs:221:5:
panic in a function that cannot unwind
```

Try this https://github.com/tauri-apps/wry/pull/1383 on Windows can
replay the crash.

In fact, we had done [a similar fix around August last
year](https://github.com/huacnlee/zed/pull/6), but we used the unsafe
method to avoid crashes in that version, we felt that it was not a good
change, so we do not make PR.

Today @sunli829 thought about it again and changed the method. Now using
`try_borrow_mut` is similar to the previous `borrow_mut`.


691de6b4b3/crates/gpui/src/app.rs (L70-L78)

I have tested to start Zed by those changes, it is looks no problem.

Co-authored-by: Sunli <scott_s829@163.com>
2025-05-20 22:17:16 +08:00
Marshall Bowers
051f49ce9a collab: Cancel trials when they end with a missing payment method (#31018)
This PR makes it so we cancel trials instead of pausing them when they
end with a missing payment method.

Release Notes:

- N/A
2025-05-20 14:01:45 +00:00
Piotr Osiewicz
e5670ba081 extension/dap: Add resolve_tcp_template function (#31010)
Extensions cannot look up available port themselves, hence the new API.
With this I'm able to port our Ruby implementation into an extension.

Release Notes:

- N/A
2025-05-20 15:17:13 +02:00
Kirill Bulatov
e4262f97af Restore the ability to drag and drop images into the editor (#31009)
`ImageItem`'s `file` is returning `""` as its `path` for single-filed
worktrees like the ones are created for the images dropped from the OS.
`ImageItem::load_image_metadata` had used that `path` in FS operations
and the other method tried to use for icon resolving.

Rework the code to use a more specific, `worktree::File` instead and
always use the `abs_path` when dealing with paths from this `file`.

Release Notes:

- Fixed images not opening on drag and drop into the editor
2025-05-20 12:38:24 +00:00
Kirill Bulatov
944a0df436 Revert "Debounce language server file system events (#30773)" (#31008)
Let's keep https://github.com/zed-industries/zed/pull/30773 and its
complexity out of Zed sources if we can:
https://github.com/rust-lang/rust-analyzer/pull/19814 seems to do a
similar thing and might have fixed the root cause.

If not, we can always reapply this later after ensuring.

Release Notes:

- N/A
2025-05-20 12:05:21 +00:00
Piotr Osiewicz
a1be61949d chore: Fix broken CI (#31003)
Closes #ISSUE

Release Notes:

- N/A
2025-05-20 10:24:10 +00:00
Piotr Osiewicz
a092e2dc03 extension: Add debug_adapters to extension manifest (#30676)
Also pass worktree to the get_dap_binary.

Release Notes:

- N/A
2025-05-20 11:01:33 +02:00
Agus Zubiaga
b1c7fa1dac Debounce language server file system events (#30773)
This helps prevent a race condition where the language server would
update in the middle of a `git checkout`

Release Notes:

- N/A
2025-05-20 09:00:28 +00:00
Julia Ryan
df66237428 Add minimap vscode settings import (#30997)
Looks like we missed these when adding the minimap.

Release Notes:

- N/A

Co-authored-by: Kirill Bulatov <mail4score@gmail.com>
2025-05-20 08:35:20 +00:00
Andres Suarez
ca513f52bf title_bar: Fix config merging to respect priority (#30980)
This is a follow-up to #30450 so that _global_ `title_bar` configs
shadow _defaults_. The way `SettingsSources::json_merge` works is by
considering non-json-nulls as values to propagate. So it's important
that configs be `Option<T>` so any intent in overriding values is
captured.

This PR follows the same `*Settings<FileContent = *SettingsContent>`
pattern used throughout to keep the `Option`s in the "settings content"
type with the finalized values in the "settings" type.

Release Notes:

- N/A
2025-05-20 07:56:24 +00:00
Michael Angerman
e9c9a8a269 gpui: Correct the image id in the example image_loading (#30990)
The image id "image-1" already exists so the id should be "image-4"

Release Notes:

- N/A
2025-05-20 10:36:41 +03:00
Mikayla Maki
315321bf8c Add end of service notifications (#30982)
Release Notes:

- N/A

---------

Co-authored-by: Max Brunsfeld <maxbrunsfeld@gmail.com>
Co-authored-by: Marshall Bowers <git@maxdeviant.com>
2025-05-20 00:20:00 +00:00
Max Brunsfeld
c747a57b7e Revert "client: Add support for HTTP/HTTPS proxy" (#30979)
Reverts zed-industries/zed#30812

This PR broke nightly builds on linux by adding an OpenSSL dependency to
the `remote_server` binary, which failed to link when building against
musl.

Release Notes:

- N/A
2025-05-19 20:19:40 -04:00
Marshall Bowers
f73c8e5841 collab: Don't create a Zed Free subscription if one already exists in Stripe (#30983)
This PR adds a check for if a Zed Free subscription already exists in
Stripe before we try to create one.

Release Notes:

- N/A
2025-05-20 00:18:45 +00:00
Marshall Bowers
f7a0834f54 collab: Create Zed Free subscription when issuing an LLM token (#30975)
This PR makes it so we create a Zed Free subscription when issuing an
LLM token, if one does not already exist.

Release Notes:

- N/A

---------

Co-authored-by: Max Brunsfeld <maxbrunsfeld@gmail.com>
2025-05-19 18:45:22 -04:00
Max Brunsfeld
83d513aef4 Continue processing Stripe events after seeing one that's > 1 day old (#30971)
This mostly affects local development. It fixes a bug where we would
only process one Stripe event per polling period (5 seconds) when
hitting old events.

Release Notes:

- N/A

Co-authored-by: Marshall Bowers <git@maxdeviant.com>
2025-05-19 20:17:54 +00:00
Marshall Bowers
b440e1a467 collab: Allow starting a trial from Zed Free (#30970)
This PR makes it so a user can initiate a checkout session for a Zed Pro
trial while on the Zed Free plan.

Release Notes:

- N/A

Co-authored-by: Max Brunsfeld <maxbrunsfeld@gmail.com>
2025-05-19 19:39:01 +00:00
Joseph T. Lyons
5c4f9e57d8 Allow updater to check for updates after downloading one (#30969)
Closes https://github.com/zed-industries/zed/issues/8968

This PR addresses the following scenario:

1. User's Zed polls for an update, finds one, and installs it
2. User doesn't immediately restart Zed, a new update is released, and
the previous version of Zed would stop polling (ignoring the new update)
3. User eventually restarts Zed and is immediately prompted to install
another update

With this change, the auto-updater will continue polling for and
installing new versions even after an initial update is found, reducing
update prompts on restart.

---

This PR does not address the following scenario:

1. User's Zed polls for an update, finds one, and installs it
2. Another update is released before the next scheduled polling interval
3. User restarts Zed and is immediately prompted to install the newer
update

Release Notes:

- Improved the auto-updater to continue checking for updates even after
finding and installing an initial update. This reduces situations where
users are prompted to install another update immediately after
restarting from a previous update.

Co-authored-by: Ben Kunkle <Ben.kunkle@gmail.com>
2025-05-19 18:27:39 +00:00
Marshall Bowers
05f8001ee9 collab: Only subscribe to Zed Free if there isn't an existing active subscription (#30967)
This PR adds a sanity check to ensure that we only subscribe the user to
Zed Free if they don't already have an active subscription.

Release Notes:

- N/A
2025-05-19 18:26:54 +00:00
Marshall Bowers
b93c67438c collab: Require product code for POST /billing/subscriptions (#30968)
This PR makes the `product` field required in the request body for `POST
/billing/subscriptions`.

We were already passing this everywhere, in practice.

Release Notes:

- N/A
2025-05-19 18:11:33 +00:00
Marshall Bowers
fdec966226 collab: Subscribe to Zed Free when a subscription is canceled or paused (#30965)
This PR makes it so that when a Stripe subscription is canceled or
paused we'll subscribe the user to Zed Free.

Release Notes:

- N/A
2025-05-19 17:35:03 +00:00
Cole Miller
9041f734fd git: Save buffer when resolving a conflict from the project diff (#30762)
Closes #30555

Release Notes:

- Changed the project diff to autosave the targeted buffer after
resolving a merge conflict.
2025-05-19 17:32:31 +00:00
Antti Kaihola
844c7ad22e Ctrl/Alt-V to select by page in Emacs keymap (#30858)
Problem: In addition to PgUp/PgDown Emacs also binds `Ctrl-V` to page
down and `Meta-V` to page up. These keys wouldn't extend the selection
in Zed.

Reason: Only PageUp/PageDown were assigned to
`editor::SelectPage{Up|Down}` in the `Editor && selection_mode` context.

Solution: In the `Editor && selection_mode` context, bind `Ctrl-V` to
`editor::SelectPageDown` and `Alt-V` to `editor::SelectPageUp`, both in
the mac and linux keymaps.

Release Notes:

- Added to the Emacs keymap bindings for Ctrl/Alt-V in the selection
mode to extend the selection one page up/down
2025-05-19 13:19:36 -04:00
Umesh Yadav
926f377c6c language_models: Add tool use support for Mistral models (#29994)
Closes https://github.com/zed-industries/zed/issues/29855

Implement tool use handling in Mistral provider, including mapping tool
call events and updating request construction. Add support for
tool_choice and parallel_tool_calls in Mistral API requests.

This works fine with all the existing models. Didn't touched anything
else but for future. Fetching models using their models api, deducting
tool call support, parallel tool calls etc should be done from model
data from api response.

<img width="547" alt="Screenshot 2025-05-06 at 4 52 37 PM"
src="https://github.com/user-attachments/assets/4c08b544-1174-40cc-a40d-522989953448"
/>

Tasks:

- [x] Add tool call support
- [x] Auto Fetch models using mistral api
- [x] Add tests for mistral crates.
- [x] Fix mistral configurations for llm providers.

Release Notes:

- agent: Add tool call support for existing mistral models

---------

Co-authored-by: Peter Tripp <peter@zed.dev>
Co-authored-by: Bennet Bo Fenner <bennet@zed.dev>
2025-05-19 18:36:59 +02:00
Ben Brandt
26a8cac0d8 extension_host: Turn on parallel compilation (#30942)
Precursor to other optimizations, but this already gets us a big
improvement.

Wasm compilation can easily be parallelized, and with all of the cores
on my M4 Max this already gets us an 86% improvement, bringing loading
an extension down to <9ms.

Not all setups will see this much improvement, but it will use the cores
available (it just uses rayon under the hood like we do elsewhere).
Since we load extensions in sequence, this should have a nice impact for
users with a lot of extensions.

#### Before

```
Benchmarking load: Warming up for 3.0000 s
Warning: Unable to complete 100 samples in 5.0s. You may wish to increase target time to 6.5s, or reduce sample count to 70.
load                    time:   [64.859 ms 64.935 ms 65.027 ms]
Found 8 outliers among 100 measurements (8.00%)
  2 (2.00%) low mild
  3 (3.00%) high mild
  3 (3.00%) high severe
```

#### After

```
load                    time:   [8.8685 ms 8.9012 ms 8.9344 ms]
                        change: [-86.347% -86.292% -86.237%] (p = 0.00 < 0.05)
                        Performance has improved.
Found 2 outliers among 100 measurements (2.00%)
  2 (2.00%) high mild
```

Release Notes:

- N/A
2025-05-19 18:06:33 +02:00
smit
c7aae6bd62 zed: Fix no way to open local folder from remote window (#30954)
Closes #27642

Currently, the `Open (cmd-o)` action is used to open a local folder
picker when in a local project, and Zed's remote path modal in the case
of a remote project. While this looks intentional, there is now no way
to open a local project when you are in a remote project window. Neither
by shortcut, nor by UI, as the "Open Local Folder" button uses the same
`Open` action.

The reverse is not true, as we already have an `Open Remote
(ctrl-cmd-o)` action to open the remote modal, where you can select "Add
Folder" which opens the same Zed's remote path modal. This already works
in both local and remote window cases.

This PR makes two changes:
1. It changes `Open (cmd-o)` action such that it should always open the
local file picker regardless of which project is currently open, local
or remote. This way we have two non-ambiguios actions `Open` and `Open
Remote`.
2. It also changes the "Open a project" button (which shows up when no
project is open in the project panel) to open the recent modal (which
contains buttons to open either local or remote) instead of choosing on
behalf of the user.

P.S. If we want to open Zed's remote path modal directly, it should be
different action altogether. Not covered for now.

Release Notes:

- Fixed issue where "Open local folder" was not opening folder picker
when connected to a remote host.
- Added `from_existing_connection` flag to `OpenRemote` action to
directly open path picker for current connection, bypassing the Remote
Projects modal.
2025-05-19 21:26:30 +05:30
Cole Miller
851121ffd4 docs: Document how to load extension grammars from the local FS during development (#30817)
Loading a local grammar could be useful if you're developing the
extension and the grammar in tandem, and a user pointed out that our
docs don't make it obvious that it's possible at all.

Release Notes:

- N/A
2025-05-19 11:46:09 -04:00
Cole Miller
e48daa92c0 debugger: Remember focused item (#30722)
Release Notes:

- Debugger Beta: the `debug panel: toggle focus` action now preserves
the debug panel's focused item.
2025-05-19 15:45:37 +00:00
Marshall Bowers
d9f12879e2 collab: Add POST /billing/subscriptions/sync endpoint (#30956)
This PR adds a new `POST /billing/subscriptions/sync` endpoint that can
be used to sync a user's billing subscriptions from Stripe.

Release Notes:

- N/A
2025-05-19 10:01:53 -04:00
Cole Miller
42dd511fc2 git: Don't filter local upstreams from branch picker (#30557)
Release Notes:

- Fixed local git branches being excluded from the branch selector when
they were set as the upstream of another local branch.
2025-05-19 13:41:58 +00:00
Aleksei Gusev
571c5e7407 Fix ctrl-delete in terminal (#30720)
Closes #30719

Release Notes:

- Fixed `ctrl-delete` in terminal, now it deletes a word forward
2025-05-19 09:33:00 -04:00
Marshall Bowers
c76295251b collab: Factor out subscription kind determination (#30955)
This PR factors out the code that determines the `SubscriptionKind` into
a separate method for reusability purposes.

Release Notes:

- N/A
2025-05-19 13:22:56 +00:00
Ben Kunkle
b057b4697f Simplify docs preprocessing (#30947)
Closes #ISSUE

This was done as part of experimental work towards better validation of
our docs. The validation ended up being not worth it, however, I believe
this refactoring is

Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-05-19 08:16:14 -04:00
Ben Brandt
57424e4743 language_models: Update tiktoken-rs to support newer models (#30951)
I was able to get this fix in upstream, so now we can have simpler code
paths for our model selection.

I also added a test to catch if this would cause a bug again in the
future.

Release Notes:

- N/A
2025-05-19 11:40:36 +00:00
Oleksiy Syvokon
2b6dab9197 agent: Fix OpenAI models not getting first message (#30941)
Closes #30733

Release Notes:

- N/A
2025-05-19 09:09:03 +00:00
laizy
70b0c4d63d gpui: Replace Mutex with RefCell for SubscriberSet (#30907)
`SubscriberSet` is `!Send` and `!Sync` because the `active` field of
`Subscriber` is `Rc`.

Release Notes:

- N/A
2025-05-19 11:08:04 +02:00
Oleksiy Syvokon
875d1ef263 agent: Fix path checks in edit_file (#30909)
- Fixed bug where creating a file failed when the root path wasn't
provided

- Many new checks for the edit_file path

Closes #30706

Release Notes:

- N/A
2025-05-19 11:56:15 +03:00
Danilo Leal
e1a2e8a3aa agent: Adjust codeblock design across edit file tool call card and Markdown (#30931)
This PR makes the edit tool call codeblock cards expanded by default, to
be consistent with https://github.com/zed-industries/zed/pull/30806.
Also, I am removing the collapsing behavior of Markdown codeblocks where
we'd add a gradient while capping the container's height based on an
arbitrary number of lines. Figured if they're all now initially
expanded, we could simplify how the design/code operates here
altogether.

Open for feedback, as I can see an argument where the previous Markdown
codeblock design of "collapsed but not fully; it shows a preview" should
stay as it is useful.

Release Notes:

- N/A
2025-05-19 06:38:12 +00:00
371 changed files with 5574 additions and 4466 deletions

22
Cargo.lock generated
View File

@@ -546,6 +546,7 @@ dependencies = [
"language_model",
"lmstudio",
"log",
"mistral",
"ollama",
"open_ai",
"paths",
@@ -2833,6 +2834,7 @@ dependencies = [
"rand 0.8.5",
"release_channel",
"rpc",
"rustls-pki-types",
"schemars",
"serde",
"serde_json",
@@ -2847,6 +2849,7 @@ dependencies = [
"tiny_http",
"tokio",
"tokio-native-tls",
"tokio-rustls 0.26.2",
"tokio-socks",
"url",
"util",
@@ -4982,6 +4985,7 @@ dependencies = [
"clap",
"client",
"collections",
"debug_adapter_extension",
"dirs 4.0.0",
"dotenv",
"env_logger 0.11.8",
@@ -5137,6 +5141,7 @@ dependencies = [
"async-trait",
"client",
"collections",
"criterion",
"ctor",
"dap",
"env_logger 0.11.8",
@@ -5153,6 +5158,7 @@ dependencies = [
"parking_lot",
"paths",
"project",
"rand 0.8.5",
"release_channel",
"remote",
"reqwest_client",
@@ -7945,6 +7951,7 @@ dependencies = [
"editor",
"file_icons",
"gpui",
"language",
"log",
"project",
"schemars",
@@ -12883,6 +12890,7 @@ dependencies = [
"clock",
"dap",
"dap_adapters",
"debug_adapter_extension",
"env_logger 0.11.8",
"extension",
"extension_host",
@@ -13575,11 +13583,12 @@ dependencies = [
[[package]]
name = "rustls-pki-types"
version = "1.11.0"
version = "1.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "917ce264624a4b4db1c364dcc35bfca9ded014d0a958cd47ad3e960e988ea51c"
checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79"
dependencies = [
"web-time",
"zeroize",
]
[[package]]
@@ -15807,16 +15816,15 @@ dependencies = [
[[package]]
name = "tiktoken-rs"
version = "0.6.0"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "44075987ee2486402f0808505dd65692163d243a337fc54363d49afac41087f6"
checksum = "25563eeba904d770acf527e8b370fe9a5547bacd20ff84a0b6c3bc41288e5625"
dependencies = [
"anyhow",
"base64 0.21.7",
"base64 0.22.1",
"bstr",
"fancy-regex 0.13.0",
"lazy_static",
"parking_lot",
"regex",
"rustc-hash 1.1.0",
]
@@ -17534,6 +17542,7 @@ dependencies = [
"postcard",
"psm",
"pulley-interpreter",
"rayon",
"rustix 0.38.44",
"semver",
"serde",
@@ -19631,6 +19640,7 @@ dependencies = [
"dap",
"dap_adapters",
"db",
"debug_adapter_extension",
"debugger_tools",
"debugger_ui",
"diagnostics",

View File

@@ -430,6 +430,7 @@ convert_case = "0.8.0"
core-foundation = "0.10.0"
core-foundation-sys = "0.8.6"
core-video = { version = "0.4.3", features = ["metal"] }
criterion = { version = "0.5", features = ["html_reports"] }
ctor = "0.4.0"
dap-types = { git = "https://github.com/zed-industries/dap-types", rev = "be69a016ba710191b9fdded28c8b042af4b617f7" }
dashmap = "6.0"
@@ -555,7 +556,7 @@ sysinfo = "0.31.0"
take-until = "0.2.0"
tempfile = "3.20.0"
thiserror = "2.0.12"
tiktoken-rs = "0.6.0"
tiktoken-rs = "0.7.0"
time = { version = "0.3", features = [
"macros",
"parsing",
@@ -608,6 +609,7 @@ wasmtime = { version = "29", default-features = false, features = [
"runtime",
"cranelift",
"component-model",
"parallel-compilation",
] }
wasmtime-wasi = "29"
which = "6.0.0"

View File

@@ -512,6 +512,8 @@
"alt-ctrl-o": "projects::OpenRecent",
"alt-shift-open": "projects::OpenRemote",
"alt-ctrl-shift-o": "projects::OpenRemote",
// Change to open path modal for existing remote connection by setting the parameter
// "alt-ctrl-shift-o": "["projects::OpenRemote", { "from_existing_connection": true }]",
"alt-ctrl-shift-b": "branches::OpenRecent",
"alt-shift-enter": "toast::RunAction",
"ctrl-~": "workspace::NewTerminal",
@@ -928,6 +930,7 @@
"alt-b": ["terminal::SendText", "\u001bb"],
"alt-f": ["terminal::SendText", "\u001bf"],
"alt-.": ["terminal::SendText", "\u001b."],
"ctrl-delete": ["terminal::SendText", "\u001bd"],
// Overrides for conflicting keybindings
"ctrl-b": ["terminal::SendKeystroke", "ctrl-b"],
"ctrl-c": ["terminal::SendKeystroke", "ctrl-c"],

View File

@@ -588,6 +588,7 @@
// "alt-cmd-o": ["projects::OpenRecent", {"create_new_window": true }],
"alt-cmd-o": "projects::OpenRecent",
"ctrl-cmd-o": "projects::OpenRemote",
"ctrl-cmd-shift-o": ["projects::OpenRemote", { "from_existing_connection": true }],
"alt-cmd-b": "branches::OpenRecent",
"ctrl-~": "workspace::NewTerminal",
"cmd-s": "workspace::Save",
@@ -1011,7 +1012,7 @@
"alt-right": ["terminal::SendText", "\u001bf"],
"alt-b": ["terminal::SendText", "\u001bb"],
"alt-f": ["terminal::SendText", "\u001bf"],
"alt-.": ["terminal::SendText", "\u001b."],
"ctrl-delete": ["terminal::SendText", "\u001bd"],
// There are conflicting bindings for these keys in the global context.
// these bindings override them, remove at your own risk:
"up": ["terminal::SendKeystroke", "up"],

View File

@@ -72,7 +72,9 @@
"alt-left": "editor::SelectToPreviousWordStart",
"alt-right": "editor::SelectToNextWordEnd",
"pagedown": "editor::SelectPageDown",
"ctrl-v": "editor::SelectPageDown",
"pageup": "editor::SelectPageUp",
"alt-v": "editor::SelectPageUp",
"ctrl-f": "editor::SelectRight",
"ctrl-b": "editor::SelectLeft",
"ctrl-n": "editor::SelectDown",

View File

@@ -51,9 +51,7 @@
"ctrl-k ctrl-l": "editor::ConvertToLowerCase",
"shift-alt-m": "markdown::OpenPreviewToTheSide",
"ctrl-backspace": "editor::DeleteToPreviousWordStart",
"ctrl-delete": "editor::DeleteToNextWordEnd",
"f3": "editor::FindNextMatch",
"shift-f3": "editor::FindPreviousMatch"
"ctrl-delete": "editor::DeleteToNextWordEnd"
}
},
{

View File

@@ -72,7 +72,9 @@
"alt-left": "editor::SelectToPreviousWordStart",
"alt-right": "editor::SelectToNextWordEnd",
"pagedown": "editor::SelectPageDown",
"ctrl-v": "editor::SelectPageDown",
"pageup": "editor::SelectPageUp",
"alt-v": "editor::SelectPageUp",
"ctrl-f": "editor::SelectRight",
"ctrl-b": "editor::SelectLeft",
"ctrl-n": "editor::SelectDown",

View File

@@ -53,9 +53,7 @@
"cmd-shift-j": "editor::JoinLines",
"shift-alt-m": "markdown::OpenPreviewToTheSide",
"ctrl-backspace": "editor::DeleteToPreviousWordStart",
"ctrl-delete": "editor::DeleteToNextWordEnd",
"cmd-g": "editor::FindNextMatch",
"cmd-shift-g": "editor::FindPreviousMatch"
"ctrl-delete": "editor::DeleteToNextWordEnd"
}
},
{

View File

@@ -485,7 +485,7 @@ impl ActivityIndicator {
this.dismiss_error_message(&DismissErrorMessage, window, cx)
})),
}),
AutoUpdateStatus::Updated { binary_path } => Some(Content {
AutoUpdateStatus::Updated { binary_path, .. } => Some(Content {
icon: None,
message: "Click to restart and update Zed".to_string(),
on_click: Some(Arc::new({

View File

@@ -333,7 +333,6 @@ fn tool_use_markdown_style(window: &Window, cx: &mut App) -> MarkdownStyle {
}
const CODEBLOCK_CONTAINER_GROUP: &str = "codeblock_container";
const MAX_UNCOLLAPSED_LINES_IN_CODE_BLOCK: usize = 10;
fn render_markdown_code_block(
message_id: MessageId,
@@ -346,17 +345,20 @@ fn render_markdown_code_block(
_window: &Window,
cx: &App,
) -> Div {
let label_size = rems(0.8125);
let label = match kind {
CodeBlockKind::Indented => None,
CodeBlockKind::Fenced => Some(
h_flex()
.px_1()
.gap_1()
.child(
Icon::new(IconName::Code)
.color(Color::Muted)
.size(IconSize::XSmall),
)
.child(Label::new("untitled").size(LabelSize::Small))
.child(div().text_size(label_size).child("Plain Text"))
.into_any_element(),
),
CodeBlockKind::FencedLang(raw_language_name) => Some(render_code_language(
@@ -393,7 +395,7 @@ fn render_markdown_code_block(
.id(("code-block-header-label", ix))
.ml_1()
.gap_1()
.child(Label::new(file_name).size(LabelSize::Small))
.child(div().text_size(label_size).child(file_name))
.child(Label::new(path).color(Color::Muted).size(LabelSize::Small))
.tooltip(move |window, cx| {
Tooltip::with_meta(
@@ -406,9 +408,10 @@ fn render_markdown_code_block(
})
.into_any_element()
} else {
Label::new(path_range.path.to_string_lossy().to_string())
.size(LabelSize::Small)
div()
.ml_1()
.text_size(label_size)
.child(path_range.path.to_string_lossy().to_string())
.into_any_element()
};
@@ -456,19 +459,13 @@ fn render_markdown_code_block(
.copied_code_block_ids
.contains(&(message_id, ix));
let can_expand = metadata.line_count >= MAX_UNCOLLAPSED_LINES_IN_CODE_BLOCK;
let is_expanded = if can_expand {
active_thread.read(cx).is_codeblock_expanded(message_id, ix)
} else {
false
};
let is_expanded = active_thread.read(cx).is_codeblock_expanded(message_id, ix);
let codeblock_header_bg = cx
.theme()
.colors()
.element_background
.blend(cx.theme().colors().editor_foreground.opacity(0.01));
.blend(cx.theme().colors().editor_foreground.opacity(0.025));
let control_buttons = h_flex()
.visible_on_hover(CODEBLOCK_CONTAINER_GROUP)
@@ -519,44 +516,48 @@ fn render_markdown_code_block(
}
}),
)
.when(can_expand, |header| {
header.child(
IconButton::new(
("expand-collapse-code", ix),
if is_expanded {
IconName::ChevronUp
} else {
IconName::ChevronDown
},
)
.icon_color(Color::Muted)
.shape(ui::IconButtonShape::Square)
.tooltip(Tooltip::text(if is_expanded {
"Collapse Code"
.child(
IconButton::new(
("expand-collapse-code", ix),
if is_expanded {
IconName::ChevronUp
} else {
"Expand Code"
}))
.on_click({
let active_thread = active_thread.clone();
move |_event, _window, cx| {
active_thread.update(cx, |this, cx| {
this.toggle_codeblock_expanded(message_id, ix);
cx.notify();
});
}
}),
IconName::ChevronDown
},
)
});
.icon_color(Color::Muted)
.shape(ui::IconButtonShape::Square)
.tooltip(Tooltip::text(if is_expanded {
"Collapse Code"
} else {
"Expand Code"
}))
.on_click({
let active_thread = active_thread.clone();
move |_event, _window, cx| {
active_thread.update(cx, |this, cx| {
this.toggle_codeblock_expanded(message_id, ix);
cx.notify();
});
}
}),
);
let codeblock_header = h_flex()
.relative()
.p_1()
.gap_1()
.justify_between()
.border_b_1()
.border_color(cx.theme().colors().border.opacity(0.6))
.bg(codeblock_header_bg)
.rounded_t_md()
.map(|this| {
if !is_expanded {
this.rounded_md()
} else {
this.rounded_t_md()
.border_b_1()
.border_color(cx.theme().colors().border.opacity(0.6))
}
})
.children(label)
.child(control_buttons);
@@ -564,12 +565,12 @@ fn render_markdown_code_block(
.group(CODEBLOCK_CONTAINER_GROUP)
.my_2()
.overflow_hidden()
.rounded_lg()
.rounded_md()
.border_1()
.border_color(cx.theme().colors().border.opacity(0.6))
.bg(cx.theme().colors().editor_background)
.child(codeblock_header)
.when(can_expand && !is_expanded, |this| this.max_h_80())
.when(!is_expanded, |this| this.h(rems_from_px(31.)))
}
fn open_path(
@@ -630,10 +631,13 @@ fn render_code_language(
.map(|language| language.name().into())
.unwrap_or(name_fallback);
let label_size = rems(0.8125);
h_flex()
.gap_1()
.children(icon_path.map(|icon| icon.color(Color::Muted).size(IconSize::Small)))
.child(Label::new(language_label).size(LabelSize::Small))
.px_1()
.gap_1p5()
.children(icon_path.map(|icon| icon.color(Color::Muted).size(IconSize::XSmall)))
.child(div().text_size(label_size).child(language_label))
.into_any_element()
}
@@ -2369,41 +2373,17 @@ impl ActiveThread {
}),
transform: Some(Arc::new({
let active_thread = cx.entity();
let editor_bg = cx.theme().colors().editor_background;
move |el, range, metadata, _, cx| {
let can_expand = metadata.line_count
>= MAX_UNCOLLAPSED_LINES_IN_CODE_BLOCK;
if !can_expand {
return el;
}
move |element, range, _, _, cx| {
let is_expanded = active_thread
.read(cx)
.is_codeblock_expanded(message_id, range.start);
if is_expanded {
return el;
return element;
}
el.child(
div()
.absolute()
.bottom_0()
.left_0()
.w_full()
.h_1_4()
.rounded_b_lg()
.bg(linear_gradient(
0.,
linear_color_stop(editor_bg, 0.),
linear_color_stop(
editor_bg.opacity(0.),
1.,
),
)),
)
element
}
})),
},

View File

@@ -85,6 +85,7 @@ actions!(
KeepAll,
Follow,
ResetTrialUpsell,
ResetTrialEndUpsell,
]
);

View File

@@ -3,7 +3,7 @@ use std::path::Path;
use std::sync::Arc;
use std::time::Duration;
use db::kvp::KEY_VALUE_STORE;
use db::kvp::{Dismissable, KEY_VALUE_STORE};
use markdown::Markdown;
use serde::{Deserialize, Serialize};
@@ -66,8 +66,8 @@ use crate::ui::AgentOnboardingModal;
use crate::{
AddContextServer, AgentDiffPane, ContextStore, DeleteRecentlyOpenThread, ExpandMessageEditor,
Follow, InlineAssistant, NewTextThread, NewThread, OpenActiveThreadAsMarkdown, OpenAgentDiff,
OpenHistory, ResetTrialUpsell, TextThreadStore, ThreadEvent, ToggleContextPicker,
ToggleNavigationMenu, ToggleOptionsMenu,
OpenHistory, ResetTrialEndUpsell, ResetTrialUpsell, TextThreadStore, ThreadEvent,
ToggleContextPicker, ToggleNavigationMenu, ToggleOptionsMenu,
};
const AGENT_PANEL_KEY: &str = "agent_panel";
@@ -157,7 +157,10 @@ pub fn init(cx: &mut App) {
window.refresh();
})
.register_action(|_workspace, _: &ResetTrialUpsell, _window, cx| {
set_trial_upsell_dismissed(false, cx);
TrialUpsell::set_dismissed(false, cx);
})
.register_action(|_workspace, _: &ResetTrialEndUpsell, _window, cx| {
TrialEndUpsell::set_dismissed(false, cx);
});
},
)
@@ -1209,12 +1212,7 @@ impl AgentPanel {
window: &mut Window,
cx: &mut Context<Self>,
) {
let Some(workspace) = self
.workspace
.upgrade()
.ok_or_else(|| anyhow!("workspace dropped"))
.log_err()
else {
let Some(workspace) = self.workspace.upgrade() else {
return;
};
@@ -1932,12 +1930,23 @@ impl AgentPanel {
}
}
fn should_render_trial_end_upsell(&self, cx: &mut Context<Self>) -> bool {
if TrialEndUpsell::dismissed() {
return false;
}
let plan = self.user_store.read(cx).current_plan();
let has_previous_trial = self.user_store.read(cx).trial_started_at().is_some();
matches!(plan, Some(Plan::Free)) && has_previous_trial
}
fn should_render_upsell(&self, cx: &mut Context<Self>) -> bool {
if !matches!(self.active_view, ActiveView::Thread { .. }) {
return false;
}
if self.hide_trial_upsell || dismissed_trial_upsell() {
if self.hide_trial_upsell || TrialUpsell::dismissed() {
return false;
}
@@ -1983,125 +1992,115 @@ impl AgentPanel {
move |toggle_state, _window, cx| {
let toggle_state_bool = toggle_state.selected();
set_trial_upsell_dismissed(toggle_state_bool, cx);
TrialUpsell::set_dismissed(toggle_state_bool, cx);
},
);
Some(
div().p_2().child(
v_flex()
let contents = div()
.size_full()
.gap_2()
.flex()
.flex_col()
.child(Headline::new("Build better with Zed Pro").size(HeadlineSize::Small))
.child(
Label::new("Try Zed Pro for free for 14 days - no credit card required.")
.size(LabelSize::Small),
)
.child(
Label::new(
"Use your own API keys or enable usage-based billing once you hit the cap.",
)
.color(Color::Muted),
)
.child(
h_flex()
.w_full()
.elevation_2(cx)
.rounded(px(8.))
.bg(cx.theme().colors().background.alpha(0.5))
.p(px(3.))
.px_neg_1()
.justify_between()
.items_center()
.child(h_flex().items_center().gap_1().child(checkbox))
.child(
div()
h_flex()
.gap_2()
.flex()
.flex_col()
.size_full()
.border_1()
.rounded(px(5.))
.border_color(cx.theme().colors().text.alpha(0.1))
.overflow_hidden()
.relative()
.bg(cx.theme().colors().panel_background)
.px_4()
.py_3()
.child(
div()
.absolute()
.top_0()
.right(px(-1.0))
.w(px(441.))
.h(px(167.))
.child(
Vector::new(VectorName::Grid, rems_from_px(441.), rems_from_px(167.)).color(ui::Color::Custom(cx.theme().colors().text.alpha(0.1)))
)
Button::new("dismiss-button", "Not Now")
.style(ButtonStyle::Transparent)
.color(Color::Muted)
.on_click({
let agent_panel = cx.entity();
move |_, _, cx| {
agent_panel.update(cx, |this, cx| {
this.hide_trial_upsell = true;
cx.notify();
});
}
}),
)
.child(
div()
.absolute()
.top(px(-8.0))
.right_0()
.w(px(400.))
.h(px(92.))
.child(
Vector::new(VectorName::AiGrid, rems_from_px(400.), rems_from_px(92.)).color(ui::Color::Custom(cx.theme().colors().text.alpha(0.32)))
)
)
// .child(
// div()
// .absolute()
// .top_0()
// .right(px(360.))
// .size(px(401.))
// .overflow_hidden()
// .bg(cx.theme().colors().panel_background)
// )
.child(
div()
.absolute()
.top_0()
.right_0()
.w(px(660.))
.h(px(401.))
.overflow_hidden()
.bg(linear_gradient(
75.,
linear_color_stop(cx.theme().colors().panel_background.alpha(0.01), 1.0),
linear_color_stop(cx.theme().colors().panel_background, 0.45),
))
)
.child(Headline::new("Build better with Zed Pro").size(HeadlineSize::Small))
.child(Label::new("Try Zed Pro for free for 14 days - no credit card required.").size(LabelSize::Small))
.child(Label::new("Use your own API keys or enable usage-based billing once you hit the cap.").color(Color::Muted))
Button::new("cta-button", "Start Trial")
.style(ButtonStyle::Transparent)
.on_click(|_, _, cx| cx.open_url(&zed_urls::account_url(cx))),
),
),
);
Some(self.render_upsell_container(cx, contents))
}
fn render_trial_end_upsell(
&self,
_window: &mut Window,
cx: &mut Context<Self>,
) -> Option<impl IntoElement> {
if !self.should_render_trial_end_upsell(cx) {
return None;
}
Some(
self.render_upsell_container(
cx,
div()
.size_full()
.gap_2()
.flex()
.flex_col()
.child(
Headline::new("Your Zed Pro trial has expired.").size(HeadlineSize::Small),
)
.child(
Label::new("You've been automatically reset to the free plan.")
.size(LabelSize::Small),
)
.child(
h_flex()
.w_full()
.px_neg_1()
.justify_between()
.items_center()
.child(div())
.child(
h_flex()
.w_full()
.px_neg_1()
.justify_between()
.items_center()
.child(h_flex().items_center().gap_1().child(checkbox))
.gap_2()
.child(
h_flex()
.gap_2()
.child(
Button::new("dismiss-button", "Not Now")
.style(ButtonStyle::Transparent)
.color(Color::Muted)
.on_click({
let agent_panel = cx.entity();
move |_, _, cx| {
agent_panel.update(
cx,
|this, cx| {
let hidden =
this.hide_trial_upsell;
println!("hidden: {}", hidden);
this.hide_trial_upsell = true;
let new_hidden =
this.hide_trial_upsell;
println!(
"new_hidden: {}",
new_hidden
);
cx.notify();
},
);
}
}),
)
.child(
Button::new("cta-button", "Start Trial")
.style(ButtonStyle::Transparent)
.on_click(|_, _, cx| {
cx.open_url(&zed_urls::account_url(cx))
}),
),
Button::new("dismiss-button", "Stay on Free")
.style(ButtonStyle::Transparent)
.color(Color::Muted)
.on_click({
let agent_panel = cx.entity();
move |_, _, cx| {
agent_panel.update(cx, |_this, cx| {
TrialEndUpsell::set_dismissed(true, cx);
cx.notify();
});
}
}),
)
.child(
Button::new("cta-button", "Upgrade to Zed Pro")
.style(ButtonStyle::Transparent)
.on_click(|_, _, cx| {
cx.open_url(&zed_urls::account_url(cx))
}),
),
),
),
@@ -2109,6 +2108,91 @@ impl AgentPanel {
)
}
fn render_upsell_container(&self, cx: &mut Context<Self>, content: Div) -> Div {
div().p_2().child(
v_flex()
.w_full()
.elevation_2(cx)
.rounded(px(8.))
.bg(cx.theme().colors().background.alpha(0.5))
.p(px(3.))
.child(
div()
.gap_2()
.flex()
.flex_col()
.size_full()
.border_1()
.rounded(px(5.))
.border_color(cx.theme().colors().text.alpha(0.1))
.overflow_hidden()
.relative()
.bg(cx.theme().colors().panel_background)
.px_4()
.py_3()
.child(
div()
.absolute()
.top_0()
.right(px(-1.0))
.w(px(441.))
.h(px(167.))
.child(
Vector::new(
VectorName::Grid,
rems_from_px(441.),
rems_from_px(167.),
)
.color(ui::Color::Custom(cx.theme().colors().text.alpha(0.1))),
),
)
.child(
div()
.absolute()
.top(px(-8.0))
.right_0()
.w(px(400.))
.h(px(92.))
.child(
Vector::new(
VectorName::AiGrid,
rems_from_px(400.),
rems_from_px(92.),
)
.color(ui::Color::Custom(cx.theme().colors().text.alpha(0.32))),
),
)
// .child(
// div()
// .absolute()
// .top_0()
// .right(px(360.))
// .size(px(401.))
// .overflow_hidden()
// .bg(cx.theme().colors().panel_background)
// )
.child(
div()
.absolute()
.top_0()
.right_0()
.w(px(660.))
.h(px(401.))
.overflow_hidden()
.bg(linear_gradient(
75.,
linear_color_stop(
cx.theme().colors().panel_background.alpha(0.01),
1.0,
),
linear_color_stop(cx.theme().colors().panel_background, 0.45),
)),
)
.child(content),
),
)
}
fn render_active_thread_or_empty_state(
&self,
window: &mut Window,
@@ -2827,6 +2911,7 @@ impl Render for AgentPanel {
.on_action(cx.listener(Self::toggle_zoom))
.child(self.render_toolbar(window, cx))
.children(self.render_trial_upsell(window, cx))
.children(self.render_trial_end_upsell(window, cx))
.map(|parent| match &self.active_view {
ActiveView::Thread { .. } => parent
.relative()
@@ -3014,25 +3099,14 @@ impl AgentPanelDelegate for ConcreteAssistantPanelDelegate {
}
}
const DISMISSED_TRIAL_UPSELL_KEY: &str = "dismissed-trial-upsell";
struct TrialUpsell;
fn dismissed_trial_upsell() -> bool {
db::kvp::KEY_VALUE_STORE
.read_kvp(DISMISSED_TRIAL_UPSELL_KEY)
.log_err()
.map_or(false, |s| s.is_some())
impl Dismissable for TrialUpsell {
const KEY: &'static str = "dismissed-trial-upsell";
}
fn set_trial_upsell_dismissed(is_dismissed: bool, cx: &mut App) {
db::write_and_log(cx, move || async move {
if is_dismissed {
db::kvp::KEY_VALUE_STORE
.write_kvp(DISMISSED_TRIAL_UPSELL_KEY.into(), "1".into())
.await
} else {
db::kvp::KEY_VALUE_STORE
.delete_kvp(DISMISSED_TRIAL_UPSELL_KEY.into())
.await
}
})
struct TrialEndUpsell;
impl Dismissable for TrialEndUpsell {
const KEY: &'static str = "dismissed-trial-end-upsell";
}

View File

@@ -1,7 +1,7 @@
use crate::context::ContextLoadResult;
use crate::inline_prompt_editor::CodegenStatus;
use crate::{context::load_context, context_store::ContextStore};
use anyhow::Result;
use anyhow::{Context as _, Result};
use assistant_settings::AssistantSettings;
use client::telemetry::Telemetry;
use collections::HashSet;
@@ -419,16 +419,16 @@ impl CodegenAlternative {
if start_buffer.remote_id() == end_buffer.remote_id() {
(start_buffer.clone(), start_buffer_offset..end_buffer_offset)
} else {
return Err(anyhow::anyhow!("invalid transformation range"));
anyhow::bail!("invalid transformation range");
}
} else {
return Err(anyhow::anyhow!("invalid transformation range"));
anyhow::bail!("invalid transformation range");
};
let prompt = self
.builder
.generate_inline_transformation_prompt(user_prompt, language_name, buffer, range)
.map_err(|e| anyhow::anyhow!("Failed to generate content prompt: {}", e))?;
.context("generating content prompt")?;
let context_task = self.context_store.as_ref().map(|context_store| {
if let Some(project) = self.project.upgrade() {

View File

@@ -2,7 +2,7 @@ use std::ops::Range;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use anyhow::{Result, anyhow};
use anyhow::{Context as _, Result, anyhow};
use assistant_context_editor::AssistantContext;
use collections::{HashSet, IndexSet};
use futures::{self, FutureExt};
@@ -142,17 +142,12 @@ impl ContextStore {
remove_if_exists: bool,
cx: &mut Context<Self>,
) -> Result<Option<AgentContextHandle>> {
let Some(project) = self.project.upgrade() else {
return Err(anyhow!("failed to read project"));
};
let Some(entry_id) = project
let project = self.project.upgrade().context("failed to read project")?;
let entry_id = project
.read(cx)
.entry_for_path(project_path, cx)
.map(|entry| entry.id)
else {
return Err(anyhow!("no entry found for directory context"));
};
.context("no entry found for directory context")?;
let context_id = self.next_context_id.post_inc();
let context = AgentContextHandle::Directory(DirectoryContextHandle {

View File

@@ -1,6 +1,6 @@
use std::{collections::VecDeque, path::Path, sync::Arc};
use anyhow::{Context as _, anyhow};
use anyhow::Context as _;
use assistant_context_editor::{AssistantContext, SavedContextMetadata};
use chrono::{DateTime, Utc};
use futures::future::{TryFutureExt as _, join_all};
@@ -130,7 +130,10 @@ impl HistoryStore {
.boxed()
})
.unwrap_or_else(|_| {
async { Err(anyhow!("no thread store")) }.boxed()
async {
anyhow::bail!("no thread store");
}
.boxed()
}),
SerializedRecentEntry::Context(id) => context_store
.update(cx, |context_store, cx| {
@@ -140,7 +143,10 @@ impl HistoryStore {
.boxed()
})
.unwrap_or_else(|_| {
async { Err(anyhow!("no context store")) }.boxed()
async {
anyhow::bail!("no context store");
}
.boxed()
}),
});
let entries = join_all(entries)

View File

@@ -11,6 +11,7 @@ use crate::{CycleNextInlineAssist, CyclePreviousInlineAssist};
use crate::{RemoveAllContext, ToggleContextPicker};
use client::ErrorExt;
use collections::VecDeque;
use db::kvp::Dismissable;
use editor::display_map::EditorMargins;
use editor::{
ContextMenuOptions, Editor, EditorElement, EditorEvent, EditorMode, EditorStyle, MultiBuffer,
@@ -33,7 +34,6 @@ use ui::utils::WithRemSize;
use ui::{
CheckboxWithLabel, IconButtonShape, KeyBinding, Popover, PopoverMenuHandle, Tooltip, prelude::*,
};
use util::ResultExt;
use workspace::Workspace;
pub struct PromptEditor<T> {
@@ -722,7 +722,7 @@ impl<T: 'static> PromptEditor<T> {
.child(CheckboxWithLabel::new(
"dont-show-again",
Label::new("Don't show again"),
if dismissed_rate_limit_notice() {
if RateLimitNotice::dismissed() {
ui::ToggleState::Selected
} else {
ui::ToggleState::Unselected
@@ -734,7 +734,7 @@ impl<T: 'static> PromptEditor<T> {
ui::ToggleState::Selected => true,
};
set_rate_limit_notice_dismissed(is_dismissed, cx)
RateLimitNotice::set_dismissed(is_dismissed, cx);
},
))
.child(
@@ -974,7 +974,7 @@ impl PromptEditor<BufferCodegen> {
CodegenStatus::Error(error) => {
if cx.has_flag::<ZedProFeatureFlag>()
&& error.error_code() == proto::ErrorCode::RateLimitExceeded
&& !dismissed_rate_limit_notice()
&& !RateLimitNotice::dismissed()
{
self.show_rate_limit_notice = true;
cx.notify();
@@ -1180,27 +1180,10 @@ impl PromptEditor<TerminalCodegen> {
}
}
const DISMISSED_RATE_LIMIT_NOTICE_KEY: &str = "dismissed-rate-limit-notice";
struct RateLimitNotice;
fn dismissed_rate_limit_notice() -> bool {
db::kvp::KEY_VALUE_STORE
.read_kvp(DISMISSED_RATE_LIMIT_NOTICE_KEY)
.log_err()
.map_or(false, |s| s.is_some())
}
fn set_rate_limit_notice_dismissed(is_dismissed: bool, cx: &mut App) {
db::write_and_log(cx, move || async move {
if is_dismissed {
db::kvp::KEY_VALUE_STORE
.write_kvp(DISMISSED_RATE_LIMIT_NOTICE_KEY.into(), "1".into())
.await
} else {
db::kvp::KEY_VALUE_STORE
.delete_kvp(DISMISSED_RATE_LIMIT_NOTICE_KEY.into())
.await
}
})
impl Dismissable for RateLimitNotice {
const KEY: &'static str = "dismissed-rate-limit-notice";
}
pub enum CodegenStatus {

View File

@@ -24,7 +24,7 @@ use language_model::{
LanguageModelRequestMessage, LanguageModelRequestTool, LanguageModelToolResult,
LanguageModelToolResultContent, LanguageModelToolUseId, MessageContent,
ModelRequestLimitReachedError, PaymentRequiredError, RequestUsage, Role, SelectedModel,
StopReason, TokenUsage,
StopReason, TokenUsage, WrappedTextContent,
};
use postage::stream::Stream as _;
use project::Project;
@@ -881,7 +881,10 @@ impl Thread {
pub fn output_for_tool(&self, id: &LanguageModelToolUseId) -> Option<&Arc<str>> {
match &self.tool_use.tool_result(id)?.content {
LanguageModelToolResultContent::Text(str) => Some(str),
LanguageModelToolResultContent::Text(text)
| LanguageModelToolResultContent::WrappedText(WrappedTextContent { text, .. }) => {
Some(text)
}
LanguageModelToolResultContent::Image(_) => {
// TODO: We should display image
None
@@ -1627,7 +1630,7 @@ impl Thread {
CompletionRequestStatus::Failed {
code, message, request_id
} => {
return Err(anyhow!("completion request failed. request_id: {request_id}, code: {code}, message: {message}"));
anyhow::bail!("completion request failed. request_id: {request_id}, code: {code}, message: {message}");
}
CompletionRequestStatus::UsageUpdated {
amount, limit
@@ -2515,8 +2518,12 @@ impl Thread {
writeln!(markdown, "**\n")?;
match &tool_result.content {
LanguageModelToolResultContent::Text(str) => {
writeln!(markdown, "{}", str)?;
LanguageModelToolResultContent::Text(text)
| LanguageModelToolResultContent::WrappedText(WrappedTextContent {
text,
..
}) => {
writeln!(markdown, "{text}")?;
}
LanguageModelToolResultContent::Image(image) => {
writeln!(markdown, "![Image](data:base64,{})", image.source)?;

View File

@@ -419,7 +419,7 @@ impl ThreadStore {
let thread = database
.try_find_thread(id.clone())
.await?
.ok_or_else(|| anyhow!("no thread found with ID: {id:?}"))?;
.with_context(|| format!("no thread found with ID: {id:?}"))?;
let thread = this.update_in(cx, |this, window, cx| {
cx.new(|cx| {
@@ -699,20 +699,14 @@ impl SerializedThread {
SerializedThread::VERSION => Ok(serde_json::from_value::<SerializedThread>(
saved_thread_json,
)?),
_ => Err(anyhow!(
"unrecognized serialized thread version: {}",
version
)),
_ => anyhow::bail!("unrecognized serialized thread version: {version:?}"),
},
None => {
let saved_thread =
serde_json::from_value::<LegacySerializedThread>(saved_thread_json)?;
Ok(saved_thread.upgrade())
}
version => Err(anyhow!(
"unrecognized serialized thread version: {:?}",
version
)),
version => anyhow::bail!("unrecognized serialized thread version: {version:?}"),
}
}
}

View File

@@ -90,7 +90,7 @@ impl Model {
} else if id.starts_with("claude-3-haiku") {
Ok(Self::Claude3Haiku)
} else {
Err(anyhow!("invalid model id"))
anyhow::bail!("invalid model id {id}");
}
}
@@ -385,10 +385,10 @@ impl RateLimitInfo {
}
}
fn get_header<'a>(key: &str, headers: &'a HeaderMap) -> Result<&'a str, anyhow::Error> {
fn get_header<'a>(key: &str, headers: &'a HeaderMap) -> anyhow::Result<&'a str> {
Ok(headers
.get(key)
.ok_or_else(|| anyhow!("missing header `{key}`"))?
.with_context(|| format!("missing header `{key}`"))?
.to_str()?)
}

View File

@@ -1,6 +1,6 @@
// This crate was essentially pulled out verbatim from main `zed` crate to avoid having to run RustEmbed macro whenever zed has to be rebuilt. It saves a second or two on an incremental build.
use anyhow::anyhow;
use anyhow::Context as _;
use gpui::{App, AssetSource, Result, SharedString};
use rust_embed::RustEmbed;
@@ -21,7 +21,7 @@ impl AssetSource for Assets {
fn load(&self, path: &str) -> Result<Option<std::borrow::Cow<'static, [u8]>>> {
Self::get(path)
.map(|f| Some(f.data))
.ok_or_else(|| anyhow!("could not find asset at path \"{}\"", path))
.with_context(|| format!("loading asset at path {path:?}"))
}
fn list(&self, path: &str) -> Result<Vec<SharedString>> {
@@ -39,7 +39,7 @@ impl AssetSource for Assets {
impl Assets {
/// Populate the [`TextSystem`] of the given [`AppContext`] with all `.ttf` fonts in the `fonts` directory.
pub fn load_fonts(&self, cx: &App) -> gpui::Result<()> {
pub fn load_fonts(&self, cx: &App) -> anyhow::Result<()> {
let font_paths = self.list("fonts")?;
let mut embedded_fonts = Vec::new();
for font_path in font_paths {

View File

@@ -1,7 +1,7 @@
#[cfg(test)]
mod context_tests;
use anyhow::{Context as _, Result, anyhow, bail};
use anyhow::{Context as _, Result, bail};
use assistant_settings::AssistantSettings;
use assistant_slash_command::{
SlashCommandContent, SlashCommandEvent, SlashCommandLine, SlashCommandOutputSection,
@@ -3011,7 +3011,7 @@ impl SavedContext {
let saved_context_json = serde_json::from_str::<serde_json::Value>(json)?;
match saved_context_json
.get("version")
.ok_or_else(|| anyhow!("version not found"))?
.context("version not found")?
{
serde_json::Value::String(version) => match version.as_str() {
SavedContext::VERSION => {
@@ -3032,9 +3032,9 @@ impl SavedContext {
serde_json::from_value::<SavedContextV0_1_0>(saved_context_json)?;
Ok(saved_context.upgrade())
}
_ => Err(anyhow!("unrecognized saved context version: {}", version)),
_ => anyhow::bail!("unrecognized saved context version: {version:?}"),
},
_ => Err(anyhow!("version not found on saved context")),
_ => anyhow::bail!("version not found on saved context"),
}
}

View File

@@ -2,7 +2,7 @@ use crate::{
AssistantContext, ContextEvent, ContextId, ContextOperation, ContextVersion, SavedContext,
SavedContextMetadata,
};
use anyhow::{Context as _, Result, anyhow};
use anyhow::{Context as _, Result};
use assistant_slash_command::{SlashCommandId, SlashCommandWorkingSet};
use client::{Client, TypedEnvelope, proto, telemetry::Telemetry};
use clock::ReplicaId;
@@ -164,16 +164,18 @@ impl ContextStore {
) -> Result<proto::OpenContextResponse> {
let context_id = ContextId::from_proto(envelope.payload.context_id);
let operations = this.update(&mut cx, |this, cx| {
if this.project.read(cx).is_via_collab() {
return Err(anyhow!("only the host contexts can be opened"));
}
anyhow::ensure!(
!this.project.read(cx).is_via_collab(),
"only the host contexts can be opened"
);
let context = this
.loaded_context_for_id(&context_id, cx)
.context("context not found")?;
if context.read(cx).replica_id() != ReplicaId::default() {
return Err(anyhow!("context must be opened via the host"));
}
anyhow::ensure!(
context.read(cx).replica_id() == ReplicaId::default(),
"context must be opened via the host"
);
anyhow::Ok(
context
@@ -193,9 +195,10 @@ impl ContextStore {
mut cx: AsyncApp,
) -> Result<proto::CreateContextResponse> {
let (context_id, operations) = this.update(&mut cx, |this, cx| {
if this.project.read(cx).is_via_collab() {
return Err(anyhow!("can only create contexts as the host"));
}
anyhow::ensure!(
!this.project.read(cx).is_via_collab(),
"can only create contexts as the host"
);
let context = this.create(cx);
let context_id = context.read(cx).id().clone();
@@ -237,9 +240,10 @@ impl ContextStore {
mut cx: AsyncApp,
) -> Result<proto::SynchronizeContextsResponse> {
this.update(&mut cx, |this, cx| {
if this.project.read(cx).is_via_collab() {
return Err(anyhow!("only the host can synchronize contexts"));
}
anyhow::ensure!(
!this.project.read(cx).is_via_collab(),
"only the host can synchronize contexts"
);
let mut local_versions = Vec::new();
for remote_version_proto in envelope.payload.contexts {
@@ -370,7 +374,7 @@ impl ContextStore {
) -> Task<Result<Entity<AssistantContext>>> {
let project = self.project.read(cx);
let Some(project_id) = project.remote_id() else {
return Task::ready(Err(anyhow!("project was not remote")));
return Task::ready(Err(anyhow::anyhow!("project was not remote")));
};
let replica_id = project.replica_id();
@@ -533,7 +537,7 @@ impl ContextStore {
) -> Task<Result<Entity<AssistantContext>>> {
let project = self.project.read(cx);
let Some(project_id) = project.remote_id() else {
return Task::ready(Err(anyhow!("project was not remote")));
return Task::ready(Err(anyhow::anyhow!("project was not remote")));
};
if let Some(context) = self.loaded_context_for_id(&context_id, cx) {

View File

@@ -23,6 +23,7 @@ log.workspace = true
ollama = { workspace = true, features = ["schemars"] }
open_ai = { workspace = true, features = ["schemars"] }
deepseek = { workspace = true, features = ["schemars"] }
mistral = { workspace = true, features = ["schemars"] }
schemars.workspace = true
serde.workspace = true
settings.workspace = true

View File

@@ -10,6 +10,7 @@ use deepseek::Model as DeepseekModel;
use gpui::{App, Pixels, SharedString};
use language_model::{CloudModel, LanguageModel};
use lmstudio::Model as LmStudioModel;
use mistral::Model as MistralModel;
use ollama::Model as OllamaModel;
use schemars::{JsonSchema, schema::Schema};
use serde::{Deserialize, Serialize};
@@ -71,6 +72,11 @@ pub enum AssistantProviderContentV1 {
default_model: Option<DeepseekModel>,
api_url: Option<String>,
},
#[serde(rename = "mistral")]
Mistral {
default_model: Option<MistralModel>,
api_url: Option<String>,
},
}
#[derive(Default, Clone, Debug)]
@@ -249,6 +255,12 @@ impl AssistantSettingsContent {
model: model.id().to_string(),
})
}
AssistantProviderContentV1::Mistral { default_model, .. } => {
default_model.map(|model| LanguageModelSelection {
provider: "mistral".into(),
model: model.id().to_string(),
})
}
}),
inline_assistant_model: None,
commit_message_model: None,
@@ -700,6 +712,7 @@ impl JsonSchema for LanguageModelProviderSetting {
"zed.dev".into(),
"copilot_chat".into(),
"deepseek".into(),
"mistral".into(),
]),
..Default::default()
}

View File

@@ -1,4 +1,4 @@
use anyhow::{Result, anyhow};
use anyhow::{Context as _, Result, anyhow};
use assistant_slash_command::{
AfterCompletion, ArgumentCompletion, SlashCommand, SlashCommandOutput,
SlashCommandOutputSection, SlashCommandResult,
@@ -84,9 +84,7 @@ impl SlashCommand for ContextServerSlashCommand {
if let Some(server) = self.store.read(cx).get_running_server(&server_id) {
cx.foreground_executor().spawn(async move {
let Some(protocol) = server.client() else {
return Err(anyhow!("Context server not initialized"));
};
let protocol = server.client().context("Context server not initialized")?;
let completion_result = protocol
.completion(
@@ -139,21 +137,16 @@ impl SlashCommand for ContextServerSlashCommand {
let store = self.store.read(cx);
if let Some(server) = store.get_running_server(&server_id) {
cx.foreground_executor().spawn(async move {
let Some(protocol) = server.client() else {
return Err(anyhow!("Context server not initialized"));
};
let protocol = server.client().context("Context server not initialized")?;
let result = protocol.run_prompt(&prompt_name, prompt_args).await?;
// Check that there are only user roles
if result
.messages
.iter()
.any(|msg| !matches!(msg.role, context_server::types::Role::User))
{
return Err(anyhow!(
"Prompt contains non-user roles, which is not supported"
));
}
anyhow::ensure!(
result
.messages
.iter()
.all(|msg| matches!(msg.role, context_server::types::Role::User)),
"Prompt contains non-user roles, which is not supported"
);
// Extract text from user messages into a single prompt string
let mut prompt = result
@@ -192,9 +185,7 @@ impl SlashCommand for ContextServerSlashCommand {
}
fn completion_argument(prompt: &Prompt, arguments: &[String]) -> Result<(String, String)> {
if arguments.is_empty() {
return Err(anyhow!("No arguments given"));
}
anyhow::ensure!(!arguments.is_empty(), "No arguments given");
match &prompt.arguments {
Some(args) if args.len() == 1 => {
@@ -202,16 +193,16 @@ fn completion_argument(prompt: &Prompt, arguments: &[String]) -> Result<(String,
let arg_value = arguments.join(" ");
Ok((arg_name, arg_value))
}
Some(_) => Err(anyhow!("Prompt must have exactly one argument")),
None => Err(anyhow!("Prompt has no arguments")),
Some(_) => anyhow::bail!("Prompt must have exactly one argument"),
None => anyhow::bail!("Prompt has no arguments"),
}
}
fn prompt_arguments(prompt: &Prompt, arguments: &[String]) -> Result<HashMap<String, String>> {
match &prompt.arguments {
Some(args) if args.len() > 1 => Err(anyhow!(
"Prompt has more than one argument, which is not supported"
)),
Some(args) if args.len() > 1 => {
anyhow::bail!("Prompt has more than one argument, which is not supported");
}
Some(args) if args.len() == 1 => {
if !arguments.is_empty() {
let mut map = HashMap::default();
@@ -220,15 +211,15 @@ fn prompt_arguments(prompt: &Prompt, arguments: &[String]) -> Result<HashMap<Str
} else if arguments.is_empty() && args[0].required == Some(false) {
Ok(HashMap::default())
} else {
Err(anyhow!("Prompt expects argument but none given"))
anyhow::bail!("Prompt expects argument but none given");
}
}
Some(_) | None => {
if arguments.is_empty() {
Ok(HashMap::default())
} else {
Err(anyhow!("Prompt expects no arguments but some were given"))
}
anyhow::ensure!(
arguments.is_empty(),
"Prompt expects no arguments but some were given"
);
Ok(HashMap::default())
}
}
}

View File

@@ -118,10 +118,7 @@ impl SlashCommand for DeltaSlashCommand {
}
}
if !changes_detected {
return Err(anyhow!("no new changes detected"));
}
anyhow::ensure!(changes_detected, "no new changes detected");
Ok(output.to_event_stream())
})
}

View File

@@ -1,4 +1,4 @@
use anyhow::{Result, anyhow};
use anyhow::{Context as _, Result, anyhow};
use assistant_slash_command::{
ArgumentCompletion, SlashCommand, SlashCommandOutput, SlashCommandOutputSection,
SlashCommandResult,
@@ -189,7 +189,7 @@ impl SlashCommand for DiagnosticsSlashCommand {
window.spawn(cx, async move |_| {
task.await?
.map(|output| output.to_event_stream())
.ok_or_else(|| anyhow!("No diagnostics found"))
.context("No diagnostics found")
})
}
}

View File

@@ -3,7 +3,7 @@ use std::sync::Arc;
use std::sync::atomic::AtomicBool;
use std::time::Duration;
use anyhow::{Result, anyhow, bail};
use anyhow::{Context as _, Result, anyhow, bail};
use assistant_slash_command::{
ArgumentCompletion, SlashCommand, SlashCommandOutput, SlashCommandOutputSection,
SlashCommandResult,
@@ -52,15 +52,16 @@ impl DocsSlashCommand {
.is_none()
{
let index_provider_deps = maybe!({
let workspace = workspace.clone().ok_or_else(|| anyhow!("no workspace"))?;
let workspace = workspace
.as_ref()
.context("no workspace")?
.upgrade()
.ok_or_else(|| anyhow!("workspace was dropped"))?;
.context("workspace dropped")?;
let project = workspace.read(cx).project().clone();
let fs = project.read(cx).fs().clone();
let cargo_workspace_root = Self::path_to_cargo_toml(project, cx)
.and_then(|path| path.parent().map(|path| path.to_path_buf()))
.ok_or_else(|| anyhow!("no Cargo workspace root found"))?;
.context("no Cargo workspace root found")?;
anyhow::Ok((fs, cargo_workspace_root))
});
@@ -78,10 +79,11 @@ impl DocsSlashCommand {
.is_none()
{
let http_client = maybe!({
let workspace = workspace.ok_or_else(|| anyhow!("no workspace"))?;
let workspace = workspace
.as_ref()
.context("no workspace")?
.upgrade()
.ok_or_else(|| anyhow!("workspace was dropped"))?;
.context("workspace was dropped")?;
let project = workspace.read(cx).project().clone();
anyhow::Ok(project.read(cx).client().http_client())
});
@@ -174,7 +176,7 @@ impl SlashCommand for DocsSlashCommand {
let args = DocsSlashCommandArgs::parse(arguments);
let store = args
.provider()
.ok_or_else(|| anyhow!("no docs provider specified"))
.context("no docs provider specified")
.and_then(|provider| IndexedDocsStore::try_global(provider, cx));
cx.background_spawn(async move {
fn build_completions(items: Vec<String>) -> Vec<ArgumentCompletion> {
@@ -287,7 +289,7 @@ impl SlashCommand for DocsSlashCommand {
let task = cx.background_spawn({
let store = args
.provider()
.ok_or_else(|| anyhow!("no docs provider specified"))
.context("no docs provider specified")
.and_then(|provider| IndexedDocsStore::try_global(provider, cx));
async move {
let (provider, key) = match args.clone() {

View File

@@ -3,7 +3,7 @@ use std::rc::Rc;
use std::sync::Arc;
use std::sync::atomic::AtomicBool;
use anyhow::{Context, Result, anyhow, bail};
use anyhow::{Context as _, Result, anyhow, bail};
use assistant_slash_command::{
ArgumentCompletion, SlashCommand, SlashCommandOutput, SlashCommandOutputSection,
SlashCommandResult,

View File

@@ -230,7 +230,10 @@ fn collect_files(
})
.collect::<anyhow::Result<Vec<custom_path_matcher::PathMatcher>>>()
else {
return futures::stream::once(async { Err(anyhow!("invalid path")) }).boxed();
return futures::stream::once(async {
anyhow::bail!("invalid path");
})
.boxed();
};
let project_handle = project.downgrade();

View File

@@ -1,5 +1,5 @@
use crate::ActionLog;
use anyhow::{Result, anyhow};
use anyhow::{Context as _, Result};
use gpui::{AsyncApp, Entity};
use language::{OutlineItem, ParseStatus};
use project::Project;
@@ -22,7 +22,7 @@ pub async fn file_outline(
let project_path = project.read_with(cx, |project, cx| {
project
.find_project_path(&path, cx)
.ok_or_else(|| anyhow!("Path {path} not found in project"))
.with_context(|| format!("Path {path} not found in project"))
})??;
project
@@ -41,9 +41,9 @@ pub async fn file_outline(
}
let snapshot = buffer.read_with(cx, |buffer, _| buffer.snapshot())?;
let Some(outline) = snapshot.outline(None) else {
return Err(anyhow!("No outline information available for this file."));
};
let outline = snapshot
.outline(None)
.context("No outline information available for this file at path {path}")?;
render_outline(
outline

View File

@@ -27,12 +27,10 @@ fn adapt_to_json_schema_subset(json: &mut Value) -> Result<()> {
const UNSUPPORTED_KEYS: [&str; 4] = ["if", "then", "else", "$ref"];
for key in UNSUPPORTED_KEYS {
if obj.contains_key(key) {
return Err(anyhow::anyhow!(
"Schema cannot be made compatible because it contains \"{}\" ",
key
));
}
anyhow::ensure!(
!obj.contains_key(key),
"Schema cannot be made compatible because it contains \"{key}\""
);
}
const KEYS_TO_REMOVE: [&str; 5] = [

View File

@@ -1,5 +1,5 @@
use crate::schema::json_schema_for;
use anyhow::{Result, anyhow};
use anyhow::{Context as _, Result, anyhow};
use assistant_tool::{ActionLog, Tool, ToolResult};
use gpui::AnyWindowHandle;
use gpui::{App, AppContext, Entity, Task};
@@ -107,17 +107,13 @@ impl Tool for CopyPathTool {
});
cx.background_spawn(async move {
match copy_task.await {
Ok(_) => Ok(
format!("Copied {} to {}", input.source_path, input.destination_path).into(),
),
Err(err) => Err(anyhow!(
"Failed to copy {} to {}: {}",
input.source_path,
input.destination_path,
err
)),
}
let _ = copy_task.await.with_context(|| {
format!(
"Copying {} to {}",
input.source_path, input.destination_path
)
})?;
Ok(format!("Copied {} to {}", input.source_path, input.destination_path).into())
})
.into()
}

View File

@@ -1,5 +1,5 @@
use crate::schema::json_schema_for;
use anyhow::{Result, anyhow};
use anyhow::{Context as _, Result, anyhow};
use assistant_tool::{ActionLog, Tool, ToolResult};
use gpui::AnyWindowHandle;
use gpui::{App, Entity, Task};
@@ -86,7 +86,7 @@ impl Tool for CreateDirectoryTool {
project.create_entry(project_path.clone(), true, cx)
})?
.await
.map_err(|err| anyhow!("Unable to create directory {destination_path}: {err}"))?;
.with_context(|| format!("Creating directory {destination_path}"))?;
Ok(format!("Created directory {destination_path}").into())
})

View File

@@ -1,5 +1,5 @@
use crate::schema::json_schema_for;
use anyhow::{Result, anyhow};
use anyhow::{Context as _, Result, anyhow};
use assistant_tool::{ActionLog, Tool, ToolResult};
use futures::{SinkExt, StreamExt, channel::mpsc};
use gpui::{AnyWindowHandle, App, AppContext, Entity, Task};
@@ -122,19 +122,17 @@ impl Tool for DeletePathTool {
}
}
let delete = project.update(cx, |project, cx| {
project.delete_file(project_path, false, cx)
})?;
match delete {
Some(deletion_task) => match deletion_task.await {
Ok(()) => Ok(format!("Deleted {path_str}").into()),
Err(err) => Err(anyhow!("Failed to delete {path_str}: {err}")),
},
None => Err(anyhow!(
"Couldn't delete {path_str} because that path isn't in this project."
)),
}
let deletion_task = project
.update(cx, |project, cx| {
project.delete_file(project_path, false, cx)
})?
.with_context(|| {
format!("Couldn't delete {path_str} because that path isn't in this project.")
})?;
deletion_task
.await
.with_context(|| format!("Deleting {path_str}"))?;
Ok(format!("Deleted {path_str}").into())
})
.into()
}

View File

@@ -24,6 +24,7 @@ use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::{cmp, iter, mem, ops::Range, path::PathBuf, sync::Arc, task::Poll};
use streaming_diff::{CharOperation, StreamingDiff};
use util::debug_panic;
#[derive(Serialize)]
struct CreateFilePromptTemplate {
@@ -543,6 +544,11 @@ impl EditAgent {
if last_message.content.is_empty() {
conversation.messages.pop();
}
} else {
debug_panic!(
"Last message must be an Assistant tool calling! Got {:?}",
last_message.content
);
}
}

View File

@@ -3,9 +3,9 @@ use crate::{
ReadFileToolInput,
edit_file_tool::{EditFileMode, EditFileToolInput},
grep_tool::GrepToolInput,
list_directory_tool::ListDirectoryToolInput,
};
use Role::*;
use anyhow::anyhow;
use assistant_tool::ToolRegistry;
use client::{Client, UserStore};
use collections::HashMap;
@@ -40,8 +40,8 @@ fn eval_extract_handle_command_output() {
eval(
100,
0.95,
EvalInput {
conversation: vec![
EvalInput::from_conversation(
vec![
message(
User,
[text(formatdoc! {"
@@ -81,11 +81,9 @@ fn eval_extract_handle_command_output() {
)],
),
],
input_path: input_file_path.into(),
input_content: Some(input_file_content.into()),
edit_description: edit_description.into(),
assertion: EvalAssertion::assert_eq(output_file_content),
},
Some(input_file_content.into()),
EvalAssertion::assert_eq(output_file_content),
),
);
}
@@ -99,8 +97,8 @@ fn eval_delete_run_git_blame() {
eval(
100,
0.95,
EvalInput {
conversation: vec![
EvalInput::from_conversation(
vec![
message(
User,
[text(formatdoc! {"
@@ -137,11 +135,9 @@ fn eval_delete_run_git_blame() {
)],
),
],
input_path: input_file_path.into(),
input_content: Some(input_file_content.into()),
edit_description: edit_description.into(),
assertion: EvalAssertion::assert_eq(output_file_content),
},
Some(input_file_content.into()),
EvalAssertion::assert_eq(output_file_content),
),
);
}
@@ -154,8 +150,8 @@ fn eval_translate_doc_comments() {
eval(
200,
1.,
EvalInput {
conversation: vec![
EvalInput::from_conversation(
vec![
message(
User,
[text(formatdoc! {"
@@ -192,11 +188,9 @@ fn eval_translate_doc_comments() {
)],
),
],
input_path: input_file_path.into(),
input_content: Some(input_file_content.into()),
edit_description: edit_description.into(),
assertion: EvalAssertion::judge_diff("Doc comments were translated to Italian"),
},
Some(input_file_content.into()),
EvalAssertion::judge_diff("Doc comments were translated to Italian"),
),
);
}
@@ -210,8 +204,8 @@ fn eval_use_wasi_sdk_in_compile_parser_to_wasm() {
eval(
100,
0.95,
EvalInput {
conversation: vec![
EvalInput::from_conversation(
vec![
message(
User,
[text(formatdoc! {"
@@ -307,14 +301,12 @@ fn eval_use_wasi_sdk_in_compile_parser_to_wasm() {
)],
),
],
input_path: input_file_path.into(),
input_content: Some(input_file_content.into()),
edit_description: edit_description.into(),
assertion: EvalAssertion::judge_diff(indoc! {"
Some(input_file_content.into()),
EvalAssertion::judge_diff(indoc! {"
- The compile_parser_to_wasm method has been changed to use wasi-sdk
- ureq is used to download the SDK for current platform and architecture
"}),
},
),
);
}
@@ -325,10 +317,10 @@ fn eval_disable_cursor_blinking() {
let input_file_content = include_str!("evals/fixtures/disable_cursor_blinking/before.rs");
let edit_description = "Comment out the call to `BlinkManager::enable`";
eval(
200,
100,
0.95,
EvalInput {
conversation: vec![
EvalInput::from_conversation(
vec![
message(User, [text("Let's research how to cursor blinking works.")]),
message(
Assistant,
@@ -382,15 +374,13 @@ fn eval_disable_cursor_blinking() {
)],
),
],
input_path: input_file_path.into(),
input_content: Some(input_file_content.into()),
edit_description: edit_description.into(),
assertion: EvalAssertion::judge_diff(indoc! {"
Some(input_file_content.into()),
EvalAssertion::judge_diff(indoc! {"
- Calls to BlinkManager in `observe_window_activation` were commented out
- The call to `blink_manager.enable` above the call to show_cursor_names was commented out
- All the edits have valid indentation
"}),
},
),
);
}
@@ -403,8 +393,8 @@ fn eval_from_pixels_constructor() {
eval(
100,
0.95,
EvalInput {
conversation: vec![
EvalInput::from_conversation(
vec![
message(
User,
[text(indoc! {"
@@ -576,14 +566,12 @@ fn eval_from_pixels_constructor() {
)],
),
],
input_path: input_file_path.into(),
input_content: Some(input_file_content.into()),
edit_description: edit_description.into(),
assertion: EvalAssertion::judge_diff(indoc! {"
- The diff contains a new `from_pixels` constructor
- The diff contains new tests for the `from_pixels` constructor
"}),
},
Some(input_file_content.into()),
EvalAssertion::judge_diff(indoc! {"
- The diff contains a new `from_pixels` constructor
- The diff contains new tests for the `from_pixels` constructor
"}),
),
);
}
@@ -591,12 +579,13 @@ fn eval_from_pixels_constructor() {
#[cfg_attr(not(feature = "eval"), ignore)]
fn eval_zode() {
let input_file_path = "root/zode.py";
let input_content = None;
let edit_description = "Create the main Zode CLI script";
eval(
200,
1.,
EvalInput {
conversation: vec![
EvalInput::from_conversation(
vec![
message(User, [text(include_str!("evals/fixtures/zode/prompt.md"))]),
message(
Assistant,
@@ -654,10 +643,8 @@ fn eval_zode() {
],
),
],
input_path: input_file_path.into(),
input_content: None,
edit_description: edit_description.into(),
assertion: EvalAssertion::new(async move |sample, _, _cx| {
input_content,
EvalAssertion::new(async move |sample, _, _cx| {
let invalid_starts = [' ', '`', '\n'];
let mut message = String::new();
for start in invalid_starts {
@@ -681,7 +668,7 @@ fn eval_zode() {
})
}
}),
},
),
);
}
@@ -694,8 +681,8 @@ fn eval_add_overwrite_test() {
eval(
200,
0.5, // TODO: make this eval better
EvalInput {
conversation: vec![
EvalInput::from_conversation(
vec![
message(
User,
[text(indoc! {"
@@ -899,13 +886,121 @@ fn eval_add_overwrite_test() {
],
),
],
input_path: input_file_path.into(),
input_content: Some(input_file_content.into()),
edit_description: edit_description.into(),
assertion: EvalAssertion::judge_diff(
Some(input_file_content.into()),
EvalAssertion::judge_diff(
"A new test for overwritten files was created, without changing any previous test",
),
},
),
);
}
#[test]
#[ignore] // until we figure out the mystery described in the comments
// #[cfg_attr(not(feature = "eval"), ignore)]
fn eval_create_empty_file() {
// Check that Edit Agent can create a file without writing its
// thoughts into it. This issue is not specific to empty files, but
// it's easier to reproduce with them.
//
// NOTE: For some mysterious reason, I could easily reproduce this
// issue roughly 90% of the time in actual Zed. However, once I
// extract the exact LLM request before the failure point and
// generate from that, the reproduction rate drops to 2%!
//
// Things I've tried to make sure it's not a fluke: disabling prompt
// caching, capturing the LLM request via a proxy server, running the
// prompt on Claude separately from evals. Every time it was mostly
// giving good outcomes, which doesn't match my actual experience in
// Zed.
//
// At some point I discovered that simply adding one insignificant
// space or a newline to the prompt suddenly results in an outcome I
// tried to reproduce almost perfectly.
//
// This weirdness happens even outside of the Zed code base and even
// when using a different subscription. The result is the same: an
// extra newline or space changes the model behavior significantly
// enough, so that the pass rate drops from 99% to 0-3%
//
// I have no explanation to this.
//
//
// Model | Pass rate
// ============================================
//
// --------------------------------------------
// Prompt version: 2025-05-19
// --------------------------------------------
//
// claude-3.7-sonnet | 0.98
// + one extra space in prompt | 0.00
// + original prompt again | 0.99
// + extra newline | 0.03
// gemini-2.5-pro-preview-03-25 | 1.00
// gemini-2.5-flash-preview-04-17 | 1.00
// + one extra space | 1.00
// gpt-4.1 | 1.00
// + one extra space | 1.00
//
//
// TODO: gpt-4.1-mini errored 38 times:
// "data did not match any variant of untagged enum ResponseStreamResult"
//
let input_file_content = None;
let expected_output_content = String::new();
eval(
1,
1.0,
EvalInput::from_conversation(
vec![
message(User, [text("Create a second empty todo file ")]),
message(
Assistant,
[
text(formatdoc! {"
I'll help you create a second empty todo file.
First, let me examine the project structure to see if there's already a todo file, which will help me determine the appropriate name and location for the second one.
"}),
tool_use(
"toolu_01GAF8TtsgpjKxCr8fgQLDgR",
"list_directory",
ListDirectoryToolInput {
path: "root".to_string(),
},
),
],
),
message(
User,
[tool_result(
"toolu_01GAF8TtsgpjKxCr8fgQLDgR",
"list_directory",
"root/TODO\nroot/TODO2\nroot/new.txt\n",
)],
),
message(
Assistant,
[
text(formatdoc! {"
I can see there's already a `TODO` file in the `root` directory. Let me create a second empty todo file called `TODO3` in the same directory:
"}),
tool_use(
"toolu_01Tb3iQ9griqSYMmVuykQPWU",
"edit_file",
EditFileToolInput {
display_description: "Create empty TODO3 file".to_string(),
mode: EditFileMode::Create,
path: "root/TODO3".into(),
},
),
],
),
],
input_file_content,
// Bad behavior is to write something like
// "I'll create an empty TODO3 file as requested."
EvalAssertion::assert_eq(expected_output_content),
),
);
}
@@ -964,12 +1059,46 @@ fn tool_result(
#[derive(Clone)]
struct EvalInput {
conversation: Vec<LanguageModelRequestMessage>,
input_path: PathBuf,
edit_file_input: EditFileToolInput,
input_content: Option<String>,
edit_description: String,
assertion: EvalAssertion,
}
impl EvalInput {
fn from_conversation(
conversation: Vec<LanguageModelRequestMessage>,
input_content: Option<String>,
assertion: EvalAssertion,
) -> Self {
let msg = conversation.last().expect("Conversation must not be empty");
if msg.role != Role::Assistant {
panic!("Conversation must end with an assistant message");
}
let tool_use = msg
.content
.iter()
.flat_map(|content| match content {
MessageContent::ToolUse(tool_use) if tool_use.name == "edit_file".into() => {
Some(tool_use)
}
_ => None,
})
.next()
.expect("Conversation must end with an edit_file tool use")
.clone();
let edit_file_input: EditFileToolInput =
serde_json::from_value(tool_use.input.clone()).unwrap();
EvalInput {
conversation,
edit_file_input,
input_content,
assertion,
}
}
}
#[derive(Clone)]
struct EvalSample {
text: String,
@@ -1077,10 +1206,7 @@ impl EvalAssertion {
}
}
Err(anyhow!(
"No score found in response. Raw output: {}",
output
))
anyhow::bail!("No score found in response. Raw output: {output}");
})
}
@@ -1308,7 +1434,7 @@ impl EditAgentTest {
let path = self
.project
.read_with(cx, |project, cx| {
project.find_project_path(eval.input_path, cx)
project.find_project_path(eval.edit_file_input.path, cx)
})
.unwrap();
let buffer = self
@@ -1336,11 +1462,13 @@ impl EditAgentTest {
}),
..Default::default()
};
let edit_output = if let Some(input_content) = eval.input_content.as_deref() {
buffer.update(cx, |buffer, cx| buffer.set_text(input_content, cx));
let edit_output = if matches!(eval.edit_file_input.mode, EditFileMode::Edit) {
if let Some(input_content) = eval.input_content.as_deref() {
buffer.update(cx, |buffer, cx| buffer.set_text(input_content, cx));
}
let (edit_output, _) = self.agent.edit(
buffer.clone(),
eval.edit_description,
eval.edit_file_input.display_description,
&conversation,
&mut cx.to_async(),
);
@@ -1348,7 +1476,7 @@ impl EditAgentTest {
} else {
let (edit_output, _) = self.agent.overwrite(
buffer.clone(),
eval.edit_description,
eval.edit_file_input.display_description,
&conversation,
&mut cx.to_async(),
);

View File

@@ -98,21 +98,21 @@ impl BlameEntry {
let sha = parts
.next()
.and_then(|line| line.parse::<Oid>().ok())
.ok_or_else(|| anyhow!("failed to parse sha"))?;
.with_context(|| format!("parsing sha from {line}"))?;
let original_line_number = parts
.next()
.and_then(|line| line.parse::<u32>().ok())
.ok_or_else(|| anyhow!("Failed to parse original line number"))?;
.with_context(|| format!("parsing original line number from {line}"))?;
let final_line_number = parts
.next()
.and_then(|line| line.parse::<u32>().ok())
.ok_or_else(|| anyhow!("Failed to parse final line number"))?;
.with_context(|| format!("parsing final line number from {line}"))?;
let line_count = parts
.next()
.and_then(|line| line.parse::<u32>().ok())
.ok_or_else(|| anyhow!("Failed to parse final line number"))?;
.with_context(|| format!("parsing line count from {line}"))?;
let start_line = final_line_number.saturating_sub(1);
let end_line = start_line + line_count;

View File

@@ -80,7 +80,7 @@ async fn run_git_blame(
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.map_err(|e| anyhow!("Failed to start git blame process: {}", e))?;
.context("starting git blame process")?;
let stdin = child
.stdin
@@ -92,10 +92,7 @@ async fn run_git_blame(
}
stdin.flush().await?;
let output = child
.output()
.await
.map_err(|e| anyhow!("Failed to read git blame output: {}", e))?;
let output = child.output().await.context("reading git blame output")?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
@@ -103,7 +100,7 @@ async fn run_git_blame(
if trimmed == GIT_BLAME_NO_COMMIT_ERROR || trimmed.contains(GIT_BLAME_NO_PATH) {
return Ok(String::new());
}
return Err(anyhow!("git blame process failed: {}", stderr));
anyhow::bail!("git blame process failed: {stderr}");
}
Ok(String::from_utf8(output.stdout)?)
@@ -144,21 +141,21 @@ impl BlameEntry {
let sha = parts
.next()
.and_then(|line| line.parse::<Oid>().ok())
.ok_or_else(|| anyhow!("failed to parse sha"))?;
.with_context(|| format!("parsing sha from {line}"))?;
let original_line_number = parts
.next()
.and_then(|line| line.parse::<u32>().ok())
.ok_or_else(|| anyhow!("Failed to parse original line number"))?;
.with_context(|| format!("parsing original line number from {line}"))?;
let final_line_number = parts
.next()
.and_then(|line| line.parse::<u32>().ok())
.ok_or_else(|| anyhow!("Failed to parse final line number"))?;
.with_context(|| format!("parsing final line number from {line}"))?;
let line_count = parts
.next()
.and_then(|line| line.parse::<u32>().ok())
.ok_or_else(|| anyhow!("Failed to parse final line number"))?;
.with_context(|| format!("parsing line count from {line}"))?;
let start_line = final_line_number.saturating_sub(1);
let end_line = start_line + line_count;

View File

@@ -5272,7 +5272,7 @@ impl Editor {
task.await?;
}
Ok::<_, anyhow::Error>(())
anyhow::Ok(())
})
.detach_and_log_err(cx);
}
@@ -10369,8 +10369,8 @@ impl Editor {
.map(|line| {
line.strip_prefix(&line_prefix)
.or_else(|| line.trim_start().strip_prefix(&line_prefix.trim_start()))
.ok_or_else(|| {
anyhow!("line did not start with prefix {line_prefix:?}: {line:?}")
.with_context(|| {
format!("line did not start with prefix {line_prefix:?}: {line:?}")
})
})
.collect::<Result<Vec<_>, _>>()
@@ -16944,7 +16944,7 @@ impl Editor {
Err(err) => {
let message = format!("Failed to copy permalink: {err}");
Err::<(), anyhow::Error>(err).log_err();
anyhow::Result::<()>::Err(err).log_err();
if let Some(workspace) = workspace {
workspace
@@ -16999,7 +16999,7 @@ impl Editor {
Err(err) => {
let message = format!("Failed to open permalink: {err}");
Err::<(), anyhow::Error>(err).log_err();
anyhow::Result::<()>::Err(err).log_err();
if let Some(workspace) = workspace {
workspace

View File

@@ -80,7 +80,7 @@ async fn run_git_blame(
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.map_err(|e| anyhow!("Failed to start git blame process: {}", e))?;
.context("starting git blame process")?;
let stdin = child
.stdin
@@ -92,10 +92,7 @@ async fn run_git_blame(
}
stdin.flush().await?;
let output = child
.output()
.await
.map_err(|e| anyhow!("Failed to read git blame output: {}", e))?;
let output = child.output().await.context("reading git blame output")?;
handle_command_output(output)
}
@@ -107,7 +104,7 @@ fn handle_command_output(output: std::process::Output) -> Result<String> {
if trimmed == GIT_BLAME_NO_COMMIT_ERROR || trimmed.contains(GIT_BLAME_NO_PATH) {
return Ok(String::new());
}
return Err(anyhow!("git blame process failed: {}", stderr));
anyhow::bail!("git blame process failed: {stderr}");
}
Ok(String::from_utf8(output.stdout)?)
@@ -148,21 +145,21 @@ impl BlameEntry {
let sha = parts
.next()
.and_then(|line| line.parse::<Oid>().ok())
.ok_or_else(|| anyhow!("failed to parse sha"))?;
.with_context(|| format!("parsing sha from {line}"))?;
let original_line_number = parts
.next()
.and_then(|line| line.parse::<u32>().ok())
.ok_or_else(|| anyhow!("Failed to parse original line number"))?;
.with_context(|| format!("parsing original line number from {line}"))?;
let final_line_number = parts
.next()
.and_then(|line| line.parse::<u32>().ok())
.ok_or_else(|| anyhow!("Failed to parse final line number"))?;
.with_context(|| format!("parsing final line number from {line}"))?;
let line_count = parts
.next()
.and_then(|line| line.parse::<u32>().ok())
.ok_or_else(|| anyhow!("Failed to parse final line number"))?;
.with_context(|| format!("parsing line count from {line}"))?;
let start_line = final_line_number.saturating_sub(1);
let end_line = start_line + line_count;

View File

@@ -80,7 +80,7 @@ async fn run_git_blame(
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.map_err(|e| anyhow!("Failed to start git blame process: {}", e))?;
.context("starting git blame process")?;
let stdin = child
.stdin
@@ -92,10 +92,7 @@ async fn run_git_blame(
}
stdin.flush().await?;
let output = child
.output()
.await
.map_err(|e| anyhow!("Failed to read git blame output: {}", e))?;
let output = child.output().await.context("reading git blame output")?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
@@ -103,7 +100,7 @@ async fn run_git_blame(
if trimmed == GIT_BLAME_NO_COMMIT_ERROR || trimmed.contains(GIT_BLAME_NO_PATH) {
return Ok(String::new());
}
return Err(anyhow!("git blame process failed: {}", stderr));
anyhow::bail!("git blame process failed: {stderr}");
}
Ok(String::from_utf8(output.stdout)?)
@@ -144,21 +141,21 @@ impl BlameEntry {
let sha = parts
.next()
.and_then(|line| line.parse::<Oid>().ok())
.ok_or_else(|| anyhow!("failed to parse sha"))?;
.with_context(|| format!("parsing sha from {line}"))?;
let original_line_number = parts
.next()
.and_then(|line| line.parse::<u32>().ok())
.ok_or_else(|| anyhow!("Failed to parse original line number"))?;
.with_context(|| format!("parsing original line number from {line}"))?;
let final_line_number = parts
.next()
.and_then(|line| line.parse::<u32>().ok())
.ok_or_else(|| anyhow!("Failed to parse final line number"))?;
.with_context(|| format!("parsing final line number from {line}"))?;
let line_count = parts
.next()
.and_then(|line| line.parse::<u32>().ok())
.ok_or_else(|| anyhow!("Failed to parse final line number"))?;
.with_context(|| format!("parsing line count from {line}"))?;
let start_line = final_line_number.saturating_sub(1);
let end_line = start_line + line_count;

View File

@@ -20,7 +20,7 @@ use std::{
#[cfg(any(feature = "tree-sitter-highlight", feature = "tree-sitter-tags"))]
use anyhow::Error;
use anyhow::{Context, Result, anyhow};
use anyhow::{Context as _, Result, anyhow};
use etcetera::BaseStrategy as _;
use fs4::fs_std::FileExt;
use indoc::indoc;
@@ -875,16 +875,13 @@ impl Loader {
FileExt::unlock(lock_file)?;
fs::remove_file(lock_path)?;
if output.status.success() {
Ok(())
} else {
Err(anyhow!(
"Parser compilation failed.\nStdout: {}\nStderr: {}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
))
}
anyhow::ensure!(
output.status.success(),
"Parser compilation failed.\nStdout: {}\nStderr: {}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
);
Ok(())
}
#[cfg(unix)]
@@ -941,17 +938,13 @@ impl Loader {
.map(|f| format!(" `{f}`"))
.collect::<Vec<_>>()
.join("\n");
anyhow::bail!(format!(indoc! {"
Missing required functions in the external scanner, parsing won't work without these!
return Err(anyhow!(format!(
indoc! {"
Missing required functions in the external scanner, parsing won't work without these!
{missing}
{}
You can read more about this at https://tree-sitter.github.io/tree-sitter/creating-parsers/4-external-scanners
"},
missing,
)));
You can read more about this at https://tree-sitter.github.io/tree-sitter/creating-parsers/4-external-scanners
"}));
}
}
}
@@ -1008,9 +1001,9 @@ impl Loader {
{
EmccSource::Podman
} else {
return Err(anyhow!(
anyhow::bail!(
"You must have either emcc, docker, or podman on your PATH to run this command"
));
);
};
let mut command = match source {
@@ -1103,12 +1096,11 @@ impl Loader {
.spawn()
.with_context(|| "Failed to run emcc command")?
.wait()?;
if !status.success() {
return Err(anyhow!("emcc command failed"));
}
fs::rename(src_path.join(output_name), output_path)
.context("failed to rename wasm output file")?;
anyhow::ensure!(status.success(), "emcc command failed");
let source_path = src_path.join(output_name);
fs::rename(&source_path, &output_path).with_context(|| {
format!("failed to rename wasm output file from {source_path:?} to {output_path:?}")
})?;
Ok(())
}
@@ -1185,11 +1177,8 @@ impl Loader {
.map(|path| {
let path = parser_path.join(path);
// prevent p being above/outside of parser_path
if path.starts_with(parser_path) {
Ok(path)
} else {
Err(anyhow!("External file path {path:?} is outside of parser directory {parser_path:?}"))
}
anyhow::ensure!(path.starts_with(parser_path), "External file path {path:?} is outside of parser directory {parser_path:?}");
Ok(path)
})
.collect::<Result<Vec<_>>>()
}).transpose()?,
@@ -1324,11 +1313,8 @@ impl Loader {
let name = GRAMMAR_NAME_REGEX
.captures(&first_three_lines)
.and_then(|c| c.get(1))
.ok_or_else(|| {
anyhow!(
"Failed to parse the language name from grammar.json at {}",
grammar_path.display()
)
.with_context(|| {
format!("Failed to parse the language name from grammar.json at {grammar_path:?}")
})?;
Ok(name.as_str().to_string())
@@ -1347,7 +1333,7 @@ impl Loader {
{
Ok(config.0)
} else {
Err(anyhow!("Unknown scope '{scope}'"))
anyhow::bail!("Unknown scope '{scope}'")
}
} else if let Some((lang, _)) = self
.language_configuration_for_file_name(path)
@@ -1371,7 +1357,7 @@ impl Loader {
} else if let Some(lang) = self.language_configuration_for_first_line_regex(path)? {
Ok(lang.0)
} else {
Err(anyhow!("No language found"))
anyhow::bail!("No language found");
}
}

View File

@@ -3,7 +3,7 @@ use crate::{
edit_agent::{EditAgent, EditAgentOutput, EditAgentOutputEvent},
schema::json_schema_for,
};
use anyhow::{Result, anyhow};
use anyhow::{Context as _, Result, anyhow};
use assistant_tool::{
ActionLog, AnyToolCard, Tool, ToolCard, ToolResult, ToolResultContent, ToolResultOutput,
ToolUseStatus,
@@ -22,7 +22,7 @@ use language::{
};
use language_model::{LanguageModel, LanguageModelRequest, LanguageModelToolSchemaFormat};
use markdown::{Markdown, MarkdownElement, MarkdownStyle};
use project::Project;
use project::{Project, ProjectPath};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use settings::Settings;
@@ -38,7 +38,7 @@ use workspace::Workspace;
pub struct EditFileTool;
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
pub struct EditFileToolInput {
/// A one-line, user-friendly markdown description of the edit. This will be
/// shown in the UI and also passed to another model to perform the edit.
@@ -86,7 +86,7 @@ pub struct EditFileToolInput {
pub mode: EditFileMode,
}
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "lowercase")]
pub enum EditFileMode {
Edit,
@@ -171,12 +171,9 @@ impl Tool for EditFileTool {
Err(err) => return Task::ready(Err(anyhow!(err))).into(),
};
let Some(project_path) = project.read(cx).find_project_path(&input.path, cx) else {
return Task::ready(Err(anyhow!(
"Path {} not found in project",
input.path.display()
)))
.into();
let project_path = match resolve_path(&input, project.clone(), cx) {
Ok(path) => path,
Err(err) => return Task::ready(Err(anyhow!(err))).into(),
};
let card = window.and_then(|window| {
@@ -199,20 +196,6 @@ impl Tool for EditFileTool {
})?
.await?;
let exists = buffer.read_with(cx, |buffer, _| {
buffer
.file()
.as_ref()
.map_or(false, |file| file.disk_state().exists())
})?;
let create_or_overwrite = match input.mode {
EditFileMode::Create | EditFileMode::Overwrite => true,
_ => false,
};
if !create_or_overwrite && !exists {
return Err(anyhow!("{} not found", input.path.display()));
}
let old_snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot())?;
let old_text = cx
.background_spawn({
@@ -221,15 +204,15 @@ impl Tool for EditFileTool {
})
.await;
let (output, mut events) = if create_or_overwrite {
edit_agent.overwrite(
let (output, mut events) = if matches!(input.mode, EditFileMode::Edit) {
edit_agent.edit(
buffer.clone(),
input.display_description.clone(),
&request,
cx,
)
} else {
edit_agent.edit(
edit_agent.overwrite(
buffer.clone(),
input.display_description.clone(),
&request,
@@ -296,15 +279,15 @@ impl Tool for EditFileTool {
let input_path = input.path.display();
if diff.is_empty() {
if hallucinated_old_text {
Err(anyhow!(formatdoc! {"
Some edits were produced but none of them could be applied.
Read the relevant sections of {input_path} again so that
I can perform the requested edits.
"}))
} else {
Ok("No edits were made.".to_string().into())
}
anyhow::ensure!(
!hallucinated_old_text,
formatdoc! {"
Some edits were produced but none of them could be applied.
Read the relevant sections of {input_path} again so that
I can perform the requested edits.
"}
);
Ok("No edits were made.".to_string().into())
} else {
Ok(ToolResultOutput {
content: ToolResultContent::Text(format!(
@@ -349,6 +332,71 @@ impl Tool for EditFileTool {
}
}
/// Validate that the file path is valid, meaning:
///
/// - For `edit` and `overwrite`, the path must point to an existing file.
/// - For `create`, the file must not already exist, but it's parent dir must exist.
fn resolve_path(
input: &EditFileToolInput,
project: Entity<Project>,
cx: &mut App,
) -> Result<ProjectPath> {
let project = project.read(cx);
match input.mode {
EditFileMode::Edit | EditFileMode::Overwrite => {
let path = project
.find_project_path(&input.path, cx)
.context("Can't edit file: path not found")?;
let entry = project
.entry_for_path(&path, cx)
.context("Can't edit file: path not found")?;
anyhow::ensure!(entry.is_file(), "Can't edit file: path is a directory");
Ok(path)
}
EditFileMode::Create => {
if let Some(path) = project.find_project_path(&input.path, cx) {
anyhow::ensure!(
project.entry_for_path(&path, cx).is_none(),
"Can't create file: file already exists"
);
}
let parent_path = input
.path
.parent()
.context("Can't create file: incorrect path")?;
let parent_project_path = project.find_project_path(&parent_path, cx);
let parent_entry = parent_project_path
.as_ref()
.and_then(|path| project.entry_for_path(&path, cx))
.context("Can't create file: parent directory doesn't exist")?;
anyhow::ensure!(
parent_entry.is_dir(),
"Can't create file: parent is not a directory"
);
let file_name = input
.path
.file_name()
.context("Can't create file: invalid filename")?;
let new_file_path = parent_project_path.map(|parent| ProjectPath {
path: Arc::from(parent.path.join(file_name)),
..parent
});
new_file_path.context("Can't create file")
}
}
}
pub struct EditFileToolCard {
path: PathBuf,
editor: Entity<Editor>,
@@ -400,7 +448,7 @@ impl EditFileToolCard {
diff_task: None,
preview_expanded: true,
error_expanded: None,
full_height_expanded: false,
full_height_expanded: true,
total_lines: None,
}
}
@@ -869,6 +917,7 @@ async fn build_buffer_diff(
#[cfg(test)]
mod tests {
use super::*;
use client::TelemetrySettings;
use fs::FakeFs;
use gpui::TestAppContext;
use language_model::fake_provider::FakeLanguageModel;
@@ -908,10 +957,102 @@ mod tests {
.await;
assert_eq!(
result.unwrap_err().to_string(),
"root/nonexistent_file.txt not found"
"Can't edit file: path not found"
);
}
#[gpui::test]
async fn test_resolve_path_for_creating_file(cx: &mut TestAppContext) {
let mode = &EditFileMode::Create;
let result = test_resolve_path(mode, "root/new.txt", cx);
assert_resolved_path_eq(result.await, "new.txt");
let result = test_resolve_path(mode, "new.txt", cx);
assert_resolved_path_eq(result.await, "new.txt");
let result = test_resolve_path(mode, "dir/new.txt", cx);
assert_resolved_path_eq(result.await, "dir/new.txt");
let result = test_resolve_path(mode, "root/dir/subdir/existing.txt", cx);
assert_eq!(
result.await.unwrap_err().to_string(),
"Can't create file: file already exists"
);
let result = test_resolve_path(mode, "root/dir/nonexistent_dir/new.txt", cx);
assert_eq!(
result.await.unwrap_err().to_string(),
"Can't create file: parent directory doesn't exist"
);
}
#[gpui::test]
async fn test_resolve_path_for_editing_file(cx: &mut TestAppContext) {
let mode = &EditFileMode::Edit;
let path_with_root = "root/dir/subdir/existing.txt";
let path_without_root = "dir/subdir/existing.txt";
let result = test_resolve_path(mode, path_with_root, cx);
assert_resolved_path_eq(result.await, path_without_root);
let result = test_resolve_path(mode, path_without_root, cx);
assert_resolved_path_eq(result.await, path_without_root);
let result = test_resolve_path(mode, "root/nonexistent.txt", cx);
assert_eq!(
result.await.unwrap_err().to_string(),
"Can't edit file: path not found"
);
let result = test_resolve_path(mode, "root/dir", cx);
assert_eq!(
result.await.unwrap_err().to_string(),
"Can't edit file: path is a directory"
);
}
async fn test_resolve_path(
mode: &EditFileMode,
path: &str,
cx: &mut TestAppContext,
) -> anyhow::Result<ProjectPath> {
init_test(cx);
let fs = FakeFs::new(cx.executor());
fs.insert_tree(
"/root",
json!({
"dir": {
"subdir": {
"existing.txt": "hello"
}
}
}),
)
.await;
let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
let input = EditFileToolInput {
display_description: "Some edit".into(),
path: path.into(),
mode: mode.clone(),
};
let result = cx.update(|cx| resolve_path(&input, project, cx));
result
}
fn assert_resolved_path_eq(path: anyhow::Result<ProjectPath>, expected: &str) {
let actual = path
.expect("Should return valid path")
.path
.to_str()
.unwrap()
.replace("\\", "/"); // Naive Windows paths normalization
assert_eq!(actual, expected);
}
#[test]
fn still_streaming_ui_text_with_path() {
let input = json!({
@@ -984,6 +1125,7 @@ mod tests {
let settings_store = SettingsStore::test(cx);
cx.set_global(settings_store);
language::init(cx);
TelemetrySettings::register(cx);
Project::init_settings(cx);
});
}

View File

@@ -109,7 +109,7 @@ impl Tool for GrepTool {
let input = match serde_json::from_value::<GrepToolInput>(input) {
Ok(input) => input,
Err(error) => {
return Task::ready(Err(anyhow!("Failed to parse input: {}", error))).into();
return Task::ready(Err(anyhow!("Failed to parse input: {error}"))).into();
}
};
@@ -122,7 +122,7 @@ impl Tool for GrepTool {
) {
Ok(matcher) => matcher,
Err(error) => {
return Task::ready(Err(anyhow!("invalid include glob pattern: {}", error))).into();
return Task::ready(Err(anyhow!("invalid include glob pattern: {error}"))).into();
}
};

View File

@@ -1,5 +1,5 @@
use crate::schema::json_schema_for;
use anyhow::{Result, anyhow};
use anyhow::{Context as _, Result, anyhow};
use assistant_tool::{ActionLog, Tool, ToolResult};
use gpui::{AnyWindowHandle, App, AppContext, Entity, Task};
use language_model::{LanguageModel, LanguageModelRequest, LanguageModelToolSchemaFormat};
@@ -117,17 +117,10 @@ impl Tool for MovePathTool {
});
cx.background_spawn(async move {
match rename_task.await {
Ok(_) => {
Ok(format!("Moved {} to {}", input.source_path, input.destination_path).into())
}
Err(err) => Err(anyhow!(
"Failed to move {} to {}: {}",
input.source_path,
input.destination_path,
err
)),
}
let _ = rename_task.await.with_context(|| {
format!("Moving {} to {}", input.source_path, input.destination_path)
})?;
Ok(format!("Moved {} to {}", input.source_path, input.destination_path).into())
})
.into()
}

View File

@@ -1,5 +1,5 @@
use crate::schema::json_schema_for;
use anyhow::{Result, anyhow};
use anyhow::{Context as _, Result, anyhow};
use assistant_tool::{ActionLog, Tool, ToolResult};
use assistant_tool::{ToolResultContent, outline};
use gpui::{AnyWindowHandle, App, Entity, Task};
@@ -129,7 +129,7 @@ impl Tool for ReadFileTool {
let language_model_image = cx
.update(|cx| LanguageModelImage::from_image(image, cx))?
.await
.ok_or_else(|| anyhow!("Failed to process image"))?;
.context("processing image")?;
Ok(ToolResultOutput {
content: ToolResultContent::Image(language_model_image),
@@ -152,7 +152,7 @@ impl Tool for ReadFileTool {
.as_ref()
.map_or(true, |file| !file.disk_state().exists())
})? {
return Err(anyhow!("{} not found", file_path));
anyhow::bail!("{file_path} not found");
}
project.update(cx, |project, cx| {

View File

@@ -382,13 +382,11 @@ fn working_dir(
match worktrees.next() {
Some(worktree) => {
if worktrees.next().is_none() {
Ok(Some(worktree.read(cx).abs_path().to_path_buf()))
} else {
Err(anyhow!(
"'.' is ambiguous in multi-root workspaces. Please specify a root directory explicitly.",
))
}
anyhow::ensure!(
worktrees.next().is_none(),
"'.' is ambiguous in multi-root workspaces. Please specify a root directory explicitly.",
);
Ok(Some(worktree.read(cx).abs_path().to_path_buf()))
}
None => Ok(None),
}
@@ -409,9 +407,7 @@ fn working_dir(
}
}
Err(anyhow!(
"`cd` directory {cd:?} was not in any of the project's worktrees."
))
anyhow::bail!("`cd` directory {cd:?} was not in any of the project's worktrees.");
}
}

View File

@@ -1,6 +1,6 @@
use std::{io::Cursor, sync::Arc};
use anyhow::Result;
use anyhow::{Context as _, Result};
use collections::HashMap;
use gpui::{App, AssetSource, Global};
use rodio::{
@@ -44,8 +44,8 @@ impl SoundRegistry {
let bytes = self
.assets
.load(&path)?
.map(Ok)
.unwrap_or_else(|| Err(anyhow::anyhow!("No such asset available")))?
.map(anyhow::Ok)
.with_context(|| format!("No asset available for path {path}"))??
.into_owned();
let cursor = Cursor::new(bytes);
let source = Decoder::new(cursor)?.convert_samples::<f32>().buffered();

View File

@@ -1,4 +1,4 @@
use anyhow::{Context as _, Result, anyhow};
use anyhow::{Context as _, Result};
use client::{Client, TelemetrySettings};
use db::RELEASE_CHANNEL;
use db::kvp::KEY_VALUE_STORE;
@@ -39,13 +39,22 @@ struct UpdateRequestBody {
destination: &'static str,
}
#[derive(Clone, PartialEq, Eq)]
pub enum VersionCheckType {
Sha(String),
Semantic(SemanticVersion),
}
#[derive(Clone, PartialEq, Eq)]
pub enum AutoUpdateStatus {
Idle,
Checking,
Downloading,
Installing,
Updated { binary_path: PathBuf },
Updated {
binary_path: PathBuf,
version: VersionCheckType,
},
Errored,
}
@@ -62,7 +71,7 @@ pub struct AutoUpdater {
pending_poll: Option<Task<Option<()>>>,
}
#[derive(Deserialize, Debug)]
#[derive(Deserialize, Clone, Debug)]
pub struct JsonRelease {
pub version: String,
pub url: String,
@@ -307,7 +316,7 @@ impl AutoUpdater {
}
pub fn poll(&mut self, cx: &mut Context<Self>) {
if self.pending_poll.is_some() || self.status.is_updated() {
if self.pending_poll.is_some() {
return;
}
@@ -358,7 +367,7 @@ impl AutoUpdater {
cx.default_global::<GlobalAutoUpdate>()
.0
.clone()
.ok_or_else(|| anyhow!("auto-update not initialized"))
.context("auto-update not initialized")
})??;
let release = Self::get_release(
@@ -402,7 +411,7 @@ impl AutoUpdater {
cx.default_global::<GlobalAutoUpdate>()
.0
.clone()
.ok_or_else(|| anyhow!("auto-update not initialized"))
.context("auto-update not initialized")
})??;
let release = Self::get_release(
@@ -456,12 +465,11 @@ impl AutoUpdater {
let mut body = Vec::new();
response.body_mut().read_to_end(&mut body).await?;
if !response.status().is_success() {
return Err(anyhow!(
"failed to fetch release: {:?}",
String::from_utf8_lossy(&body),
));
}
anyhow::ensure!(
response.status().is_success(),
"failed to fetch release: {:?}",
String::from_utf8_lossy(&body),
);
serde_json::from_slice(body.as_slice()).with_context(|| {
format!(
@@ -483,36 +491,63 @@ impl AutoUpdater {
Self::get_release(this, asset, os, arch, None, release_channel, cx).await
}
fn installed_update_version(&self) -> Option<VersionCheckType> {
match &self.status {
AutoUpdateStatus::Updated { version, .. } => Some(version.clone()),
_ => None,
}
}
async fn update(this: Entity<Self>, mut cx: AsyncApp) -> Result<()> {
let (client, current_version, release_channel) = this.update(&mut cx, |this, cx| {
this.status = AutoUpdateStatus::Checking;
cx.notify();
(
this.http_client.clone(),
this.current_version,
ReleaseChannel::try_global(cx),
)
})?;
let (client, current_version, installed_update_version, release_channel) =
this.update(&mut cx, |this, cx| {
this.status = AutoUpdateStatus::Checking;
cx.notify();
(
this.http_client.clone(),
this.current_version,
this.installed_update_version(),
ReleaseChannel::try_global(cx),
)
})?;
let release =
Self::get_latest_release(&this, "zed", OS, ARCH, release_channel, &mut cx).await?;
let should_download = match *RELEASE_CHANNEL {
ReleaseChannel::Nightly => cx
.update(|cx| AppCommitSha::try_global(cx).map(|sha| release.version != sha.0))
.ok()
.flatten()
.unwrap_or(true),
_ => release.version.parse::<SemanticVersion>()? > current_version,
let update_version_to_install = match *RELEASE_CHANNEL {
ReleaseChannel::Nightly => {
let should_download = cx
.update(|cx| AppCommitSha::try_global(cx).map(|sha| release.version != sha.0))
.ok()
.flatten()
.unwrap_or(true);
should_download.then(|| VersionCheckType::Sha(release.version.clone()))
}
_ => {
let installed_version =
installed_update_version.unwrap_or(VersionCheckType::Semantic(current_version));
match installed_version {
VersionCheckType::Sha(_) => {
log::warn!("Unexpected SHA-based version in non-nightly build");
Some(installed_version)
}
VersionCheckType::Semantic(semantic_comparison_version) => {
let latest_release_version = release.version.parse::<SemanticVersion>()?;
let should_download = latest_release_version > semantic_comparison_version;
should_download.then(|| VersionCheckType::Semantic(latest_release_version))
}
}
}
};
if !should_download {
let Some(update_version) = update_version_to_install else {
this.update(&mut cx, |this, cx| {
this.status = AutoUpdateStatus::Idle;
cx.notify();
})?;
return Ok(());
}
};
this.update(&mut cx, |this, cx| {
this.status = AutoUpdateStatus::Downloading;
@@ -521,10 +556,10 @@ impl AutoUpdater {
let installer_dir = InstallerDir::new().await?;
let filename = match OS {
"macos" => Ok("Zed.dmg"),
"macos" => anyhow::Ok("Zed.dmg"),
"linux" => Ok("zed.tar.gz"),
"windows" => Ok("ZedUpdateInstaller.exe"),
_ => Err(anyhow!("not supported: {:?}", OS)),
unsupported_os => anyhow::bail!("not supported: {unsupported_os}"),
}?;
#[cfg(not(target_os = "windows"))]
@@ -534,7 +569,7 @@ impl AutoUpdater {
);
let downloaded_asset = installer_dir.path().join(filename);
download_release(&downloaded_asset, release, client, &cx).await?;
download_release(&downloaded_asset, release.clone(), client, &cx).await?;
this.update(&mut cx, |this, cx| {
this.status = AutoUpdateStatus::Installing;
@@ -545,13 +580,16 @@ impl AutoUpdater {
"macos" => install_release_macos(&installer_dir, downloaded_asset, &cx).await,
"linux" => install_release_linux(&installer_dir, downloaded_asset, &cx).await,
"windows" => install_release_windows(downloaded_asset).await,
_ => Err(anyhow!("not supported: {:?}", OS)),
unsupported_os => anyhow::bail!("not supported: {unsupported_os}"),
}?;
this.update(&mut cx, |this, cx| {
this.set_should_show_update_notification(true, cx)
.detach_and_log_err(cx);
this.status = AutoUpdateStatus::Updated { binary_path };
this.status = AutoUpdateStatus::Updated {
binary_path,
version: update_version,
};
cx.notify();
})?;
@@ -601,12 +639,11 @@ async fn download_remote_server_binary(
let request_body = AsyncBody::from(serde_json::to_string(&update_request_body)?);
let mut response = client.get(&release.url, request_body, true).await?;
if !response.status().is_success() {
return Err(anyhow!(
"failed to download remote server release: {:?}",
response.status()
));
}
anyhow::ensure!(
response.status().is_success(),
"failed to download remote server release: {:?}",
response.status()
);
smol::io::copy(response.body_mut(), &mut temp_file).await?;
smol::fs::rename(&temp, &target_path).await?;
@@ -753,7 +790,7 @@ async fn install_release_macos(
let running_app_path = cx.update(|cx| cx.app_path())??;
let running_app_filename = running_app_path
.file_name()
.ok_or_else(|| anyhow!("invalid running app path"))?;
.with_context(|| format!("invalid running app path {running_app_path:?}"))?;
let mount_path = temp_dir.path().join("Zed");
let mut mounted_app_path: OsString = mount_path.join(running_app_filename).into();

View File

@@ -22,7 +22,7 @@ mod windows_impl {
use super::dialog::create_dialog_window;
use super::updater::perform_update;
use anyhow::{Context, Result};
use anyhow::{Context as _, Result};
use windows::{
Win32::{
Foundation::{HWND, LPARAM, WPARAM},

View File

@@ -4,7 +4,7 @@ use std::{
time::{Duration, Instant},
};
use anyhow::{Context, Result};
use anyhow::{Context as _, Result};
use windows::Win32::{
Foundation::{HWND, LPARAM, WPARAM},
System::Threading::CREATE_NEW_PROCESS_GROUP,
@@ -124,9 +124,7 @@ pub(crate) fn perform_update(app_dir: &Path, hwnd: Option<isize>) -> Result<()>
for job in JOBS.iter() {
let start = Instant::now();
loop {
if start.elapsed().as_secs() > 2 {
return Err(anyhow::anyhow!("Timed out"));
}
anyhow::ensure!(start.elapsed().as_secs() <= 2, "Timed out");
match (*job)(app_dir) {
Ok(_) => {
unsafe { PostMessageW(hwnd, WM_JOB_UPDATED, WPARAM(0), LPARAM(0))? };

View File

@@ -3,7 +3,7 @@ mod models;
use std::collections::HashMap;
use std::pin::Pin;
use anyhow::{Error, Result, anyhow};
use anyhow::{Context as _, Error, Result, anyhow};
use aws_sdk_bedrockruntime as bedrock;
pub use aws_sdk_bedrockruntime as bedrock_client;
pub use aws_sdk_bedrockruntime::types::{
@@ -97,7 +97,7 @@ pub async fn stream_completion(
}
})
.await
.map_err(|err| anyhow!("failed to spawn task: {err:?}"))?
.context("spawning a task")?
}
pub fn aws_document_to_value(document: &Document) -> Value {

View File

@@ -1,4 +1,3 @@
use anyhow::anyhow;
use serde::{Deserialize, Serialize};
use strum::EnumIter;
@@ -107,7 +106,7 @@ impl Model {
} else if id.starts_with("claude-3-7-sonnet-thinking") {
Ok(Self::Claude3_7SonnetThinking)
} else {
Err(anyhow!("invalid model id"))
anyhow::bail!("invalid model id {id}");
}
}
@@ -294,7 +293,7 @@ impl Model {
}
}
pub fn cross_region_inference_id(&self, region: &str) -> Result<String, anyhow::Error> {
pub fn cross_region_inference_id(&self, region: &str) -> anyhow::Result<String> {
let region_group = if region.starts_with("us-gov-") {
"us-gov"
} else if region.starts_with("us-") {
@@ -307,8 +306,7 @@ impl Model {
// Canada and South America regions - default to US profiles
"us"
} else {
// Unknown region
return Err(anyhow!("Unsupported Region"));
anyhow::bail!("Unsupported Region {region}");
};
let model_id = self.id();

View File

@@ -2,7 +2,7 @@ pub mod participant;
pub mod room;
use crate::call_settings::CallSettings;
use anyhow::{Result, anyhow};
use anyhow::{Context as _, Result, anyhow};
use audio::Audio;
use client::{ChannelId, Client, TypedEnvelope, User, UserStore, ZED_ALWAYS_ACTIVE, proto};
use collections::HashSet;
@@ -187,7 +187,7 @@ impl ActiveCall {
let invite = if let Some(room) = room {
cx.spawn(async move |_, cx| {
let room = room.await.map_err(|err| anyhow!("{:?}", err))?;
let room = room.await.map_err(|err| anyhow!("{err:?}"))?;
let initial_project_id = if let Some(initial_project) = initial_project {
Some(
@@ -236,7 +236,7 @@ impl ActiveCall {
.shared();
self.pending_room_creation = Some(room.clone());
cx.background_spawn(async move {
room.await.map_err(|err| anyhow!("{:?}", err))?;
room.await.map_err(|err| anyhow!("{err:?}"))?;
anyhow::Ok(())
})
};
@@ -326,7 +326,7 @@ impl ActiveCall {
.0
.borrow_mut()
.take()
.ok_or_else(|| anyhow!("no incoming call"))?;
.context("no incoming call")?;
telemetry::event!("Incoming Call Declined", room_id = call.room_id);
self.client.send(proto::DeclineCall {
room_id: call.room_id,
@@ -399,12 +399,9 @@ impl ActiveCall {
project: Entity<Project>,
cx: &mut Context<Self>,
) -> Result<()> {
if let Some((room, _)) = self.room.as_ref() {
self.report_call_event("Project Unshared", cx);
room.update(cx, |room, cx| room.unshare_project(project, cx))
} else {
Err(anyhow!("no active call"))
}
let (room, _) = self.room.as_ref().context("no active call")?;
self.report_call_event("Project Unshared", cx);
room.update(cx, |room, cx| room.unshare_project(project, cx))
}
pub fn location(&self) -> Option<&WeakEntity<Project>> {

View File

@@ -1,4 +1,4 @@
use anyhow::{Result, anyhow};
use anyhow::{Context as _, Result};
use client::{ParticipantIndex, User, proto};
use collections::HashMap;
use gpui::WeakEntity;
@@ -18,17 +18,17 @@ pub enum ParticipantLocation {
impl ParticipantLocation {
pub fn from_proto(location: Option<proto::ParticipantLocation>) -> Result<Self> {
match location.and_then(|l| l.variant) {
Some(proto::participant_location::Variant::SharedProject(project)) => {
match location
.and_then(|l| l.variant)
.context("participant location was not provided")?
{
proto::participant_location::Variant::SharedProject(project) => {
Ok(Self::SharedProject {
project_id: project.id,
})
}
Some(proto::participant_location::Variant::UnsharedProject(_)) => {
Ok(Self::UnsharedProject)
}
Some(proto::participant_location::Variant::External(_)) => Ok(Self::External),
None => Err(anyhow!("participant location was not provided")),
proto::participant_location::Variant::UnsharedProject(_) => Ok(Self::UnsharedProject),
proto::participant_location::Variant::External(_) => Ok(Self::External),
}
}
}

View File

@@ -2,7 +2,7 @@ use crate::{
call_settings::CallSettings,
participant::{LocalParticipant, ParticipantLocation, RemoteParticipant},
};
use anyhow::{Result, anyhow};
use anyhow::{Context as _, Result, anyhow};
use audio::{Audio, Sound};
use client::{
ChannelId, Client, ParticipantIndex, TypedEnvelope, User, UserStore,
@@ -165,7 +165,7 @@ impl Room {
) -> Task<Result<Entity<Self>>> {
cx.spawn(async move |cx| {
let response = client.request(proto::CreateRoom {}).await?;
let room_proto = response.room.ok_or_else(|| anyhow!("invalid room"))?;
let room_proto = response.room.context("invalid room")?;
let room = cx.new(|cx| {
let mut room = Self::new(
room_proto.id,
@@ -270,7 +270,7 @@ impl Room {
user_store: Entity<UserStore>,
mut cx: AsyncApp,
) -> Result<Entity<Self>> {
let room_proto = response.room.ok_or_else(|| anyhow!("invalid room"))?;
let room_proto = response.room.context("invalid room")?;
let room = cx.new(|cx| {
Self::new(
room_proto.id,
@@ -360,7 +360,7 @@ impl Room {
log::info!("detected client disconnection");
this.upgrade()
.ok_or_else(|| anyhow!("room was dropped"))?
.context("room was dropped")?
.update(cx, |this, cx| {
this.status = RoomStatus::Rejoining;
cx.notify();
@@ -428,9 +428,7 @@ impl Room {
log::info!("reconnection failed, leaving room");
this.update(cx, |this, cx| this.leave(cx))?.await?;
}
Err(anyhow!(
"can't reconnect to room: client failed to re-establish connection"
))
anyhow::bail!("can't reconnect to room: client failed to re-establish connection");
}
fn rejoin(&mut self, cx: &mut Context<Self>) -> Task<Result<()>> {
@@ -494,7 +492,7 @@ impl Room {
let response = response.await?;
let message_id = response.message_id;
let response = response.payload;
let room_proto = response.room.ok_or_else(|| anyhow!("invalid room"))?;
let room_proto = response.room.context("invalid room")?;
this.update(cx, |this, cx| {
this.status = RoomStatus::Online;
this.apply_room_update(room_proto, cx)?;
@@ -645,10 +643,7 @@ impl Room {
envelope: TypedEnvelope<proto::RoomUpdated>,
mut cx: AsyncApp,
) -> Result<()> {
let room = envelope
.payload
.room
.ok_or_else(|| anyhow!("invalid room"))?;
let room = envelope.payload.room.context("invalid room")?;
this.update(&mut cx, |this, cx| this.apply_room_update(room, cx))?
}
@@ -937,12 +932,15 @@ impl Room {
} => {
let user_id = participant.identity().0.parse()?;
let track_id = track.sid();
let participant = self.remote_participants.get_mut(&user_id).ok_or_else(|| {
anyhow!(
"{:?} subscribed to track by unknown participant {user_id}",
self.client.user_id()
)
})?;
let participant =
self.remote_participants
.get_mut(&user_id)
.with_context(|| {
format!(
"{:?} subscribed to track by unknown participant {user_id}",
self.client.user_id()
)
})?;
if self.live_kit.as_ref().map_or(true, |kit| kit.deafened) {
if publication.is_audio() {
publication.set_enabled(false, cx);
@@ -972,12 +970,15 @@ impl Room {
track, participant, ..
} => {
let user_id = participant.identity().0.parse()?;
let participant = self.remote_participants.get_mut(&user_id).ok_or_else(|| {
anyhow!(
"{:?}, unsubscribed from track by unknown participant {user_id}",
self.client.user_id()
)
})?;
let participant =
self.remote_participants
.get_mut(&user_id)
.with_context(|| {
format!(
"{:?}, unsubscribed from track by unknown participant {user_id}",
self.client.user_id()
)
})?;
match track {
livekit_client::RemoteTrack::Audio(track) => {
participant.audio_tracks.remove(&track.sid());
@@ -1324,7 +1325,7 @@ impl Room {
let live_kit = this
.live_kit
.as_mut()
.ok_or_else(|| anyhow!("live-kit was not initialized"))?;
.context("live-kit was not initialized")?;
let canceled = if let LocalTrack::Pending {
publish_id: cur_publish_id,
@@ -1389,7 +1390,7 @@ impl Room {
cx.spawn(async move |this, cx| {
let sources = sources.await??;
let source = sources.first().ok_or_else(|| anyhow!("no display found"))?;
let source = sources.first().context("no display found")?;
let publication = participant.publish_screenshare_track(&**source, cx).await;
@@ -1397,7 +1398,7 @@ impl Room {
let live_kit = this
.live_kit
.as_mut()
.ok_or_else(|| anyhow!("live-kit was not initialized"))?;
.context("live-kit was not initialized")?;
let canceled = if let LocalTrack::Pending {
publish_id: cur_publish_id,
@@ -1485,16 +1486,14 @@ impl Room {
}
pub fn unshare_screen(&mut self, cx: &mut Context<Self>) -> Result<()> {
if self.status.is_offline() {
return Err(anyhow!("room is offline"));
}
anyhow::ensure!(!self.status.is_offline(), "room is offline");
let live_kit = self
.live_kit
.as_mut()
.ok_or_else(|| anyhow!("live-kit was not initialized"))?;
.context("live-kit was not initialized")?;
match mem::take(&mut live_kit.screen_track) {
LocalTrack::None => Err(anyhow!("screen was not shared")),
LocalTrack::None => anyhow::bail!("screen was not shared"),
LocalTrack::Pending { .. } => {
cx.notify();
Ok(())

View File

@@ -1,5 +1,5 @@
use crate::{Channel, ChannelStore};
use anyhow::{Result, anyhow};
use anyhow::{Context as _, Result};
use client::{
ChannelId, Client, Subscription, TypedEnvelope, UserId, proto,
user::{User, UserStore},
@@ -170,15 +170,16 @@ impl ChannelChat {
message: MessageParams,
cx: &mut Context<Self>,
) -> Result<Task<Result<u64>>> {
if message.text.trim().is_empty() {
Err(anyhow!("message body can't be empty"))?;
}
anyhow::ensure!(
!message.text.trim().is_empty(),
"message body can't be empty"
);
let current_user = self
.user_store
.read(cx)
.current_user()
.ok_or_else(|| anyhow!("current_user is not present"))?;
.context("current_user is not present")?;
let channel_id = self.channel_id;
let pending_id = ChannelMessageId::Pending(post_inc(&mut self.next_pending_message_id));
@@ -215,7 +216,7 @@ impl ChannelChat {
});
let response = request.await?;
drop(outgoing_message_guard);
let response = response.message.ok_or_else(|| anyhow!("invalid message"))?;
let response = response.message.context("invalid message")?;
let id = response.id;
let message = ChannelMessage::from_proto(response, &user_store, cx).await?;
this.update(cx, |this, cx| {
@@ -470,7 +471,7 @@ impl ChannelChat {
});
let response = request.await?;
let message = ChannelMessage::from_proto(
response.message.ok_or_else(|| anyhow!("invalid message"))?,
response.message.context("invalid message")?,
&user_store,
cx,
)
@@ -531,10 +532,7 @@ impl ChannelChat {
mut cx: AsyncApp,
) -> Result<()> {
let user_store = this.update(&mut cx, |this, _| this.user_store.clone())?;
let message = message
.payload
.message
.ok_or_else(|| anyhow!("empty message"))?;
let message = message.payload.message.context("empty message")?;
let message_id = message.id;
let message = ChannelMessage::from_proto(message, &user_store, &mut cx).await?;
@@ -566,10 +564,7 @@ impl ChannelChat {
mut cx: AsyncApp,
) -> Result<()> {
let user_store = this.update(&mut cx, |this, _| this.user_store.clone())?;
let message = message
.payload
.message
.ok_or_else(|| anyhow!("empty message"))?;
let message = message.payload.message.context("empty message")?;
let message = ChannelMessage::from_proto(message, &user_store, &mut cx).await?;
@@ -753,10 +748,7 @@ impl ChannelMessage {
.collect(),
timestamp: OffsetDateTime::from_unix_timestamp(message.timestamp as i64)?,
sender,
nonce: message
.nonce
.ok_or_else(|| anyhow!("nonce is required"))?
.into(),
nonce: message.nonce.context("nonce is required")?.into(),
reply_to_message_id: message.reply_to_message_id,
edited_at,
})

View File

@@ -1,7 +1,7 @@
mod channel_index;
use crate::{ChannelMessage, channel_buffer::ChannelBuffer, channel_chat::ChannelChat};
use anyhow::{Result, anyhow};
use anyhow::{Context as _, Result, anyhow};
use channel_index::ChannelIndex;
use client::{ChannelId, Client, ClientSettings, Subscription, User, UserId, UserStore};
use collections::{HashMap, HashSet, hash_map};
@@ -332,9 +332,7 @@ impl ChannelStore {
cx.spawn(async move |this, cx| {
if let Some(request) = request {
let response = request.await?;
let this = this
.upgrade()
.ok_or_else(|| anyhow!("channel store dropped"))?;
let this = this.upgrade().context("channel store dropped")?;
let user_store = this.update(cx, |this, _| this.user_store.clone())?;
ChannelMessage::from_proto_vec(response.messages, &user_store, cx).await
} else {
@@ -482,7 +480,7 @@ impl ChannelStore {
.spawn(async move |this, cx| {
let channel = this.update(cx, |this, _| {
this.channel_for_id(channel_id).cloned().ok_or_else(|| {
Arc::new(anyhow!("no channel for id: {}", channel_id))
Arc::new(anyhow!("no channel for id: {channel_id}"))
})
})??;
@@ -514,7 +512,7 @@ impl ChannelStore {
}
}
};
cx.background_spawn(async move { task.await.map_err(|error| anyhow!("{}", error)) })
cx.background_spawn(async move { task.await.map_err(|error| anyhow!("{error}")) })
}
pub fn is_channel_admin(&self, channel_id: ChannelId) -> bool {
@@ -578,9 +576,7 @@ impl ChannelStore {
})
.await?;
let channel = response
.channel
.ok_or_else(|| anyhow!("missing channel in response"))?;
let channel = response.channel.context("missing channel in response")?;
let channel_id = ChannelId(channel.id);
this.update(cx, |this, cx| {
@@ -752,7 +748,7 @@ impl ChannelStore {
})
.await?
.channel
.ok_or_else(|| anyhow!("missing channel in response"))?;
.context("missing channel in response")?;
this.update(cx, |this, cx| {
let task = this.update_channels(
proto::UpdateChannels {

View File

@@ -169,7 +169,7 @@ fn main() -> Result<()> {
"To retrieve the system specs on the command line, run the following command:",
&format!("{} --system-specs", path.display()),
];
return Err(anyhow::anyhow!(msg.join("\n")));
anyhow::bail!(msg.join("\n"));
}
#[cfg(all(
@@ -255,11 +255,10 @@ fn main() -> Result<()> {
}
}
if let Some(_) = args.dev_server_token {
return Err(anyhow::anyhow!(
"Dev servers were removed in v0.157.x please upgrade to SSH remoting: https://zed.dev/docs/remote-development"
))?;
}
anyhow::ensure!(
args.dev_server_token.is_none(),
"Dev servers were removed in v0.157.x please upgrade to SSH remoting: https://zed.dev/docs/remote-development"
);
let sender: JoinHandle<anyhow::Result<()>> = thread::spawn({
let exit_status = exit_status.clone();
@@ -400,7 +399,7 @@ mod linux {
time::Duration,
};
use anyhow::anyhow;
use anyhow::{Context as _, anyhow};
use cli::FORCE_CLI_MODE_ENV_VAR_NAME;
use fork::Fork;
@@ -417,9 +416,7 @@ mod linux {
path.to_path_buf().canonicalize()?
} else {
let cli = env::current_exe()?;
let dir = cli
.parent()
.ok_or_else(|| anyhow!("no parent path for cli"))?;
let dir = cli.parent().context("no parent path for cli")?;
// libexec is the standard, lib/zed is for Arch (and other non-libexec distros),
// ./zed is for the target directory in development builds.
@@ -428,8 +425,8 @@ mod linux {
possible_locations
.iter()
.find_map(|p| dir.join(p).canonicalize().ok().filter(|path| path != &cli))
.ok_or_else(|| {
anyhow!("could not find any of: {}", possible_locations.join(", "))
.with_context(|| {
format!("could not find any of: {}", possible_locations.join(", "))
})?
};
@@ -759,7 +756,7 @@ mod windows {
#[cfg(target_os = "macos")]
mod mac_os {
use anyhow::{Context as _, Result, anyhow};
use anyhow::{Context as _, Result};
use core_foundation::{
array::{CFArray, CFIndex},
base::TCFType as _,
@@ -800,9 +797,10 @@ mod mac_os {
let cli_path = std::env::current_exe()?.canonicalize()?;
let mut app_path = cli_path.clone();
while app_path.extension() != Some(OsStr::new("app")) {
if !app_path.pop() {
return Err(anyhow!("cannot find app bundle containing {:?}", cli_path));
}
anyhow::ensure!(
app_path.pop(),
"cannot find app bundle containing {cli_path:?}"
);
}
Ok(app_path)
}

View File

@@ -49,7 +49,6 @@ text.workspace = true
thiserror.workspace = true
time.workspace = true
tiny_http = "0.8"
tokio-native-tls = "0.3"
tokio-socks = { version = "0.5.2", default-features = false, features = ["futures-io"] }
url.workspace = true
util.workspace = true
@@ -72,3 +71,10 @@ windows.workspace = true
[target.'cfg(target_os = "macos")'.dependencies]
cocoa.workspace = true
[target.'cfg(any(target_os = "windows", target_os = "macos"))'.dependencies]
tokio-native-tls = "0.3"
[target.'cfg(not(any(target_os = "windows", target_os = "macos")))'.dependencies]
rustls-pki-types = "1.12"
tokio-rustls = { version = "0.26", features = ["tls12", "ring"], default-features = false }

View File

@@ -490,14 +490,14 @@ impl<T: 'static> Drop for PendingEntitySubscription<T> {
}
}
#[derive(Copy, Clone)]
#[derive(Copy, Clone, Deserialize, Debug)]
pub struct TelemetrySettings {
pub diagnostics: bool,
pub metrics: bool,
}
/// Control what info is collected by Zed.
#[derive(Default, Clone, Serialize, Deserialize, JsonSchema)]
#[derive(Default, Clone, Serialize, Deserialize, JsonSchema, Debug)]
pub struct TelemetrySettingsContent {
/// Send debug info like crash reports.
///
@@ -515,25 +515,7 @@ impl settings::Settings for TelemetrySettings {
type FileContent = TelemetrySettingsContent;
fn load(sources: SettingsSources<Self::FileContent>, _: &mut App) -> Result<Self> {
Ok(Self {
diagnostics: sources
.user
.as_ref()
.or(sources.server.as_ref())
.and_then(|v| v.diagnostics)
.unwrap_or(
sources
.default
.diagnostics
.ok_or_else(Self::missing_default)?,
),
metrics: sources
.user
.as_ref()
.or(sources.server.as_ref())
.and_then(|v| v.metrics)
.unwrap_or(sources.default.metrics.ok_or_else(Self::missing_default)?),
})
sources.json_merge()
}
fn import_from_vscode(vscode: &settings::VsCodeSettings, current: &mut Self::FileContent) {
@@ -729,9 +711,10 @@ impl Client {
let id = (TypeId::of::<T>(), remote_id);
let mut state = self.handler_set.lock();
if state.entities_by_type_and_remote_id.contains_key(&id) {
return Err(anyhow!("already subscribed to entity"));
}
anyhow::ensure!(
!state.entities_by_type_and_remote_id.contains_key(&id),
"already subscribed to entity"
);
state
.entities_by_type_and_remote_id
@@ -980,10 +963,7 @@ impl Client {
hello_message_type_name
)
})?;
let peer_id = hello
.payload
.peer_id
.ok_or_else(|| anyhow!("invalid peer id"))?;
let peer_id = hello.payload.peer_id.context("invalid peer id")?;
Ok(peer_id)
};
@@ -1093,22 +1073,19 @@ impl Client {
}
let response = http.get(&url, Default::default(), false).await?;
let collab_url = if response.status().is_redirection() {
response
.headers()
.get("Location")
.ok_or_else(|| anyhow!("missing location header in /rpc response"))?
.to_str()
.map_err(EstablishConnectionError::other)?
.to_string()
} else {
Err(anyhow!(
"unexpected /rpc response status {}",
response.status()
))?
};
Url::parse(&collab_url).context("invalid rpc url")
anyhow::ensure!(
response.status().is_redirection(),
"unexpected /rpc response status {}",
response.status()
);
let collab_url = response
.headers()
.get("Location")
.context("missing location header in /rpc response")?
.to_str()
.map_err(EstablishConnectionError::other)?
.to_string();
Url::parse(&collab_url).with_context(|| format!("parsing colab rpc url {collab_url}"))
}
}
@@ -1150,7 +1127,7 @@ impl Client {
let rpc_host = rpc_url
.host_str()
.zip(rpc_url.port_or_known_default())
.ok_or_else(|| anyhow!("missing host in rpc url"))?;
.context("missing host in rpc url")?;
let stream = {
let handle = cx.update(|cx| gpui_tokio::Tokio::handle(cx)).ok().unwrap();
@@ -1305,16 +1282,13 @@ impl Client {
)
.context("failed to respond to login http request")?;
return Ok((
user_id
.ok_or_else(|| anyhow!("missing user_id parameter"))?,
access_token.ok_or_else(|| {
anyhow!("missing access_token parameter")
})?,
user_id.context("missing user_id parameter")?,
access_token.context("missing access_token parameter")?,
));
}
}
Err(anyhow!("didn't receive login redirect"))
anyhow::bail!("didn't receive login redirect");
})
.await?;
@@ -1432,13 +1406,12 @@ impl Client {
let mut response = http.send(request).await?;
let mut body = String::new();
response.body_mut().read_to_string(&mut body).await?;
if !response.status().is_success() {
Err(anyhow!(
"admin user request failed {} - {}",
response.status().as_u16(),
body,
))?;
}
anyhow::ensure!(
response.status().is_success(),
"admin user request failed {} - {}",
response.status().as_u16(),
body,
);
let response: AuthenticatedUserResponse = serde_json::from_str(&body)?;
// Use the admin API token to authenticate as the impersonated user.
@@ -1475,7 +1448,7 @@ impl Client {
if let Status::Connected { connection_id, .. } = *self.status().borrow() {
Ok(connection_id)
} else {
Err(anyhow!("not connected"))
anyhow::bail!("not connected");
}
}

View File

@@ -3,7 +3,7 @@
mod http_proxy;
mod socks_proxy;
use anyhow::{Context, Result, anyhow};
use anyhow::{Context as _, Result};
use http_client::Url;
use http_proxy::{HttpProxyType, connect_http_proxy_stream, parse_http_proxy};
use socks_proxy::{SocksVersion, connect_socks_proxy_stream, parse_socks_proxy};
@@ -16,7 +16,7 @@ pub(crate) async fn connect_proxy_stream(
// If parsing the proxy URL fails, we must avoid falling back to an insecure connection.
// SOCKS proxies are often used in contexts where security and privacy are critical,
// so any fallback could expose users to significant risks.
return Err(anyhow!("Parsing proxy url failed"));
anyhow::bail!("Parsing proxy url failed");
};
// Connect to proxy and wrap protocol later

View File

@@ -5,7 +5,10 @@ use tokio::{
io::{AsyncBufReadExt, AsyncWriteExt, BufStream},
net::TcpStream,
};
#[cfg(any(target_os = "windows", target_os = "macos"))]
use tokio_native_tls::{TlsConnector, native_tls};
#[cfg(not(any(target_os = "windows", target_os = "macos")))]
use tokio_rustls::TlsConnector;
use url::Url;
use super::AsyncReadWrite;
@@ -61,6 +64,7 @@ where
Ok(Box::new(stream))
}
#[cfg(any(target_os = "windows", target_os = "macos"))]
async fn https_connect<T>(
stream: T,
target: (&str, u16),
@@ -75,6 +79,24 @@ where
http_connect(stream, target, auth).await
}
#[cfg(not(any(target_os = "windows", target_os = "macos")))]
async fn https_connect<T>(
stream: T,
target: (&str, u16),
auth: Option<HttpProxyAuthorization<'_>>,
proxy_domain: &str,
) -> Result<Box<dyn AsyncReadWrite>>
where
T: AsyncReadWrite,
{
let proxy_domain = rustls_pki_types::ServerName::try_from(proxy_domain)
.context("Address resolution failed")?
.to_owned();
let tls_connector = TlsConnector::from(std::sync::Arc::new(http_client_tls::tls_config()));
let stream = tls_connector.connect(proxy_domain, stream).await?;
http_connect(stream, target, auth).await
}
fn make_request(target: (&str, u16), auth: Option<HttpProxyAuthorization<'_>>) -> String {
let (host, port) = target;
let mut request = format!(

View File

@@ -1,9 +1,9 @@
//! socks proxy
use anyhow::{Context, Result};
use anyhow::{Context as _, Result};
use http_client::Url;
use tokio::net::TcpStream;
use tokio_socks::tcp::{Socks4Stream, Socks5Stream};
use url::Url;
use super::AsyncReadWrite;

View File

@@ -1,5 +1,5 @@
use crate::{Client, Connection, Credentials, EstablishConnectionError, UserStore};
use anyhow::{Result, anyhow};
use anyhow::{Context as _, Result, anyhow};
use chrono::Duration;
use futures::{StreamExt, stream::BoxStream};
use gpui::{AppContext as _, BackgroundExecutor, Entity, TestAppContext};
@@ -45,7 +45,7 @@ impl FakeServer {
move |cx| {
let state = state.clone();
cx.spawn(async move |_| {
let state = state.upgrade().ok_or_else(|| anyhow!("server dropped"))?;
let state = state.upgrade().context("server dropped")?;
let mut state = state.lock();
state.auth_count += 1;
let access_token = state.access_token.to_string();
@@ -64,8 +64,8 @@ impl FakeServer {
let state = state.clone();
let credentials = credentials.clone();
cx.spawn(async move |cx| {
let state = state.upgrade().ok_or_else(|| anyhow!("server dropped"))?;
let peer = peer.upgrade().ok_or_else(|| anyhow!("server dropped"))?;
let state = state.upgrade().context("server dropped")?;
let peer = peer.upgrade().context("server dropped")?;
if state.lock().forbid_connections {
Err(EstablishConnectionError::Other(anyhow!(
"server is forbidding connections"
@@ -155,7 +155,7 @@ impl FakeServer {
.expect("not connected")
.next()
.await
.ok_or_else(|| anyhow!("other half hung up"))?;
.context("other half hung up")?;
self.executor.finish_waiting();
let type_name = message.payload_type_name();
let message = message.into_any();

View File

@@ -388,9 +388,7 @@ impl UserStore {
// Users are fetched in parallel above and cached in call to get_users
// No need to parallelize here
let mut updated_contacts = Vec::new();
let this = this
.upgrade()
.ok_or_else(|| anyhow!("can't upgrade user store handle"))?;
let this = this.upgrade().context("can't upgrade user store handle")?;
for contact in message.contacts {
updated_contacts
.push(Arc::new(Contact::from_proto(contact, &this, cx).await?));
@@ -574,7 +572,7 @@ impl UserStore {
let client = self.client.upgrade();
cx.spawn(async move |_, _| {
client
.ok_or_else(|| anyhow!("can't upgrade client reference"))?
.context("can't upgrade client reference")?
.request(proto::RespondToContactRequest {
requester_id,
response: proto::ContactRequestResponse::Dismiss as i32,
@@ -596,7 +594,7 @@ impl UserStore {
cx.spawn(async move |this, cx| {
let response = client
.ok_or_else(|| anyhow!("can't upgrade client reference"))?
.context("can't upgrade client reference")?
.request(request)
.await;
this.update(cx, |this, cx| {
@@ -663,7 +661,7 @@ impl UserStore {
this.users
.get(user_id)
.cloned()
.ok_or_else(|| anyhow!("user {} not found", user_id))
.with_context(|| format!("user {user_id} not found"))
})
.collect()
})?
@@ -703,7 +701,7 @@ impl UserStore {
this.users
.get(&user_id)
.cloned()
.ok_or_else(|| anyhow!("server responded with no users"))
.context("server responded with no users")
})?
})
}
@@ -765,20 +763,17 @@ impl UserStore {
};
let client = self.client.clone();
cx.spawn(async move |this, cx| {
if let Some(client) = client.upgrade() {
let response = client
.request(proto::AcceptTermsOfService {})
.await
.context("error accepting tos")?;
this.update(cx, |this, cx| {
this.set_current_user_accepted_tos_at(Some(response.accepted_tos_at));
cx.emit(Event::PrivateUserInfoUpdated);
})
} else {
Err(anyhow!("client not found"))
}
cx.spawn(async move |this, cx| -> anyhow::Result<()> {
let client = client.upgrade().context("client not found")?;
let response = client
.request(proto::AcceptTermsOfService {})
.await
.context("error accepting tos")?;
this.update(cx, |this, cx| {
this.set_current_user_accepted_tos_at(Some(response.accepted_tos_at));
cx.emit(Event::PrivateUserInfoUpdated);
})?;
Ok(())
})
}
@@ -897,7 +892,7 @@ impl Contact {
impl Collaborator {
pub fn from_proto(message: proto::Collaborator) -> Result<Self> {
Ok(Self {
peer_id: message.peer_id.ok_or_else(|| anyhow!("invalid peer id"))?,
peer_id: message.peer_id.context("invalid peer id")?,
replica_id: message.replica_id as ReplicaId,
user_id: message.user_id as UserId,
is_host: message.is_host,

View File

@@ -5,12 +5,13 @@ pub mod extensions;
pub mod ips_file;
pub mod slack;
use crate::db::Database;
use crate::{
AppState, Error, Result, auth,
db::{User, UserId},
rpc,
};
use anyhow::anyhow;
use anyhow::Context as _;
use axum::{
Extension, Json, Router,
body::Body,
@@ -97,6 +98,7 @@ impl std::fmt::Display for SystemIdHeader {
pub fn routes(rpc_server: Arc<rpc::Server>) -> Router<(), Body> {
Router::new()
.route("/user", get(get_authenticated_user))
.route("/users/look_up", get(look_up_user))
.route("/users/:id/access_tokens", post(create_access_token))
.route("/rpc_server_snapshot", get(get_rpc_server_snapshot))
.merge(billing::router())
@@ -181,6 +183,87 @@ async fn get_authenticated_user(
}))
}
#[derive(Debug, Deserialize)]
struct LookUpUserParams {
identifier: String,
}
#[derive(Debug, Serialize)]
struct LookUpUserResponse {
user: Option<User>,
}
async fn look_up_user(
Query(params): Query<LookUpUserParams>,
Extension(app): Extension<Arc<AppState>>,
) -> Result<Json<LookUpUserResponse>> {
let user = resolve_identifier_to_user(&app.db, &params.identifier).await?;
let user = if let Some(user) = user {
match user {
UserOrId::User(user) => Some(user),
UserOrId::Id(id) => app.db.get_user_by_id(id).await?,
}
} else {
None
};
Ok(Json(LookUpUserResponse { user }))
}
enum UserOrId {
User(User),
Id(UserId),
}
async fn resolve_identifier_to_user(
db: &Arc<Database>,
identifier: &str,
) -> Result<Option<UserOrId>> {
if let Some(identifier) = identifier.parse::<i32>().ok() {
let user = db.get_user_by_id(UserId(identifier)).await?;
return Ok(user.map(UserOrId::User));
}
if identifier.starts_with("cus_") {
let billing_customer = db
.get_billing_customer_by_stripe_customer_id(&identifier)
.await?;
return Ok(billing_customer.map(|billing_customer| UserOrId::Id(billing_customer.user_id)));
}
if identifier.starts_with("sub_") {
let billing_subscription = db
.get_billing_subscription_by_stripe_subscription_id(&identifier)
.await?;
if let Some(billing_subscription) = billing_subscription {
let billing_customer = db
.get_billing_customer_by_id(billing_subscription.billing_customer_id)
.await?;
return Ok(
billing_customer.map(|billing_customer| UserOrId::Id(billing_customer.user_id))
);
} else {
return Ok(None);
}
}
if identifier.contains('@') {
let user = db.get_user_by_email(identifier).await?;
return Ok(user.map(UserOrId::User));
}
if let Some(user) = db.get_user_by_github_login(identifier).await? {
return Ok(Some(UserOrId::User(user)));
}
Ok(None)
}
#[derive(Deserialize, Debug)]
struct CreateUserParams {
github_user_id: i32,
@@ -220,7 +303,7 @@ async fn create_access_token(
.db
.get_user_by_id(user_id)
.await?
.ok_or_else(|| anyhow!("user not found"))?;
.context("user not found")?;
let mut impersonated_user_id = None;
if let Some(impersonate) = params.impersonate {

View File

@@ -1,4 +1,4 @@
use anyhow::{Context, anyhow, bail};
use anyhow::{Context as _, bail};
use axum::{
Extension, Json, Router,
extract::{self, Query},
@@ -17,9 +17,8 @@ use stripe::{
CreateBillingPortalSessionFlowDataAfterCompletionRedirect,
CreateBillingPortalSessionFlowDataSubscriptionUpdateConfirm,
CreateBillingPortalSessionFlowDataSubscriptionUpdateConfirmItems,
CreateBillingPortalSessionFlowDataType, CreateCustomer, Customer, CustomerId, EventObject,
EventType, Expandable, ListEvents, PaymentMethod, Subscription, SubscriptionId,
SubscriptionStatus,
CreateBillingPortalSessionFlowDataType, Customer, CustomerId, EventObject, EventType,
Expandable, ListEvents, PaymentMethod, Subscription, SubscriptionId, SubscriptionStatus,
};
use util::{ResultExt, maybe};
@@ -61,6 +60,10 @@ pub fn router() -> Router {
"/billing/subscriptions/migrate",
post(migrate_to_new_billing),
)
.route(
"/billing/subscriptions/sync",
post(sync_billing_subscription),
)
.route("/billing/monthly_spend", get(get_monthly_spend))
.route("/billing/usage", get(get_current_usage))
}
@@ -86,7 +89,7 @@ async fn get_billing_preferences(
.db
.get_user_by_github_user_id(params.github_user_id)
.await?
.ok_or_else(|| anyhow!("user not found"))?;
.context("user not found")?;
let billing_customer = app.db.get_billing_customer_by_user_id(user.id).await?;
let preferences = app.db.get_billing_preferences(user.id).await?;
@@ -135,7 +138,7 @@ async fn update_billing_preferences(
.db
.get_user_by_github_user_id(body.github_user_id)
.await?
.ok_or_else(|| anyhow!("user not found"))?;
.context("user not found")?;
let billing_customer = app.db.get_billing_customer_by_user_id(user.id).await?;
@@ -238,7 +241,7 @@ async fn list_billing_subscriptions(
.db
.get_user_by_github_user_id(params.github_user_id)
.await?
.ok_or_else(|| anyhow!("user not found"))?;
.context("user not found")?;
let subscriptions = app.db.get_billing_subscriptions(user.id).await?;
@@ -276,7 +279,7 @@ async fn list_billing_subscriptions(
}))
}
#[derive(Debug, Clone, Copy, Deserialize)]
#[derive(Debug, PartialEq, Clone, Copy, Deserialize)]
#[serde(rename_all = "snake_case")]
enum ProductCode {
ZedPro,
@@ -287,7 +290,7 @@ enum ProductCode {
#[derive(Debug, Deserialize)]
struct CreateBillingSubscriptionBody {
github_user_id: i32,
product: Option<ProductCode>,
product: ProductCode,
}
#[derive(Debug, Serialize)]
@@ -304,15 +307,8 @@ async fn create_billing_subscription(
.db
.get_user_by_github_user_id(body.github_user_id)
.await?
.ok_or_else(|| anyhow!("user not found"))?;
.context("user not found")?;
let Some(stripe_client) = app.stripe_client.clone() else {
log::error!("failed to retrieve Stripe client");
Err(Error::http(
StatusCode::NOT_IMPLEMENTED,
"not supported".into(),
))?
};
let Some(stripe_billing) = app.stripe_billing.clone() else {
log::error!("failed to retrieve Stripe billing object");
Err(Error::http(
@@ -321,11 +317,16 @@ async fn create_billing_subscription(
))?
};
if app.db.has_active_billing_subscription(user.id).await? {
return Err(Error::http(
StatusCode::CONFLICT,
"user already has an active subscription".into(),
));
if let Some(existing_subscription) = app.db.get_active_billing_subscription(user.id).await? {
let is_checkout_allowed = body.product == ProductCode::ZedProTrial
&& existing_subscription.kind == Some(SubscriptionKind::ZedFree);
if !is_checkout_allowed {
return Err(Error::http(
StatusCode::CONFLICT,
"user already has an active subscription".into(),
));
}
}
let existing_billing_customer = app.db.get_billing_customer_by_user_id(user.id).await?;
@@ -342,35 +343,9 @@ async fn create_billing_subscription(
CustomerId::from_str(&existing_customer.stripe_customer_id)
.context("failed to parse customer ID")?
} else {
let existing_customer = if let Some(email) = user.email_address.as_deref() {
let customers = Customer::list(
&stripe_client,
&stripe::ListCustomers {
email: Some(email),
..Default::default()
},
)
.await?;
customers.data.first().cloned()
} else {
None
};
if let Some(existing_customer) = existing_customer {
existing_customer.id
} else {
let customer = Customer::create(
&stripe_client,
CreateCustomer {
email: user.email_address.as_deref(),
..Default::default()
},
)
.await?;
customer.id
}
stripe_billing
.find_or_create_customer_by_email(user.email_address.as_deref())
.await?
};
let success_url = format!(
@@ -379,12 +354,12 @@ async fn create_billing_subscription(
);
let checkout_session_url = match body.product {
Some(ProductCode::ZedPro) => {
ProductCode::ZedPro => {
stripe_billing
.checkout_with_zed_pro(customer_id, &user.github_login, &success_url)
.await?
}
Some(ProductCode::ZedProTrial) => {
ProductCode::ZedProTrial => {
if let Some(existing_billing_customer) = &existing_billing_customer {
if existing_billing_customer.trial_started_at.is_some() {
return Err(Error::http(
@@ -405,17 +380,11 @@ async fn create_billing_subscription(
)
.await?
}
Some(ProductCode::ZedFree) => {
ProductCode::ZedFree => {
stripe_billing
.checkout_with_zed_free(customer_id, &user.github_login, &success_url)
.await?
}
None => {
return Err(Error::http(
StatusCode::BAD_REQUEST,
"No product selected".into(),
));
}
};
Ok(Json(CreateBillingSubscriptionResponse {
@@ -463,7 +432,7 @@ async fn manage_billing_subscription(
.db
.get_user_by_github_user_id(body.github_user_id)
.await?
.ok_or_else(|| anyhow!("user not found"))?;
.context("user not found")?;
let Some(stripe_client) = app.stripe_client.clone() else {
log::error!("failed to retrieve Stripe client");
@@ -485,7 +454,7 @@ async fn manage_billing_subscription(
.db
.get_billing_customer_by_user_id(user.id)
.await?
.ok_or_else(|| anyhow!("billing customer not found"))?;
.context("billing customer not found")?;
let customer_id = CustomerId::from_str(&customer.stripe_customer_id)
.context("failed to parse customer ID")?;
@@ -493,7 +462,7 @@ async fn manage_billing_subscription(
.db
.get_billing_subscription_by_id(body.subscription_id)
.await?
.ok_or_else(|| anyhow!("subscription not found"))?;
.context("subscription not found")?;
let subscription_id = SubscriptionId::from_str(&subscription.stripe_subscription_id)
.context("failed to parse subscription ID")?;
@@ -590,7 +559,7 @@ async fn manage_billing_subscription(
None
}
})
.ok_or_else(|| anyhow!("No subscription item to update"))?;
.context("No subscription item to update")?;
Some(CreateBillingPortalSessionFlowData {
type_: CreateBillingPortalSessionFlowDataType::SubscriptionUpdateConfirm,
@@ -684,7 +653,7 @@ async fn migrate_to_new_billing(
.db
.get_user_by_github_user_id(body.github_user_id)
.await?
.ok_or_else(|| anyhow!("user not found"))?;
.context("user not found")?;
let old_billing_subscriptions_by_user = app
.db
@@ -737,6 +706,73 @@ async fn migrate_to_new_billing(
}))
}
#[derive(Debug, Deserialize)]
struct SyncBillingSubscriptionBody {
github_user_id: i32,
}
#[derive(Debug, Serialize)]
struct SyncBillingSubscriptionResponse {
stripe_customer_id: String,
}
async fn sync_billing_subscription(
Extension(app): Extension<Arc<AppState>>,
extract::Json(body): extract::Json<SyncBillingSubscriptionBody>,
) -> Result<Json<SyncBillingSubscriptionResponse>> {
let Some(stripe_client) = app.stripe_client.clone() else {
log::error!("failed to retrieve Stripe client");
Err(Error::http(
StatusCode::NOT_IMPLEMENTED,
"not supported".into(),
))?
};
let user = app
.db
.get_user_by_github_user_id(body.github_user_id)
.await?
.context("user not found")?;
let billing_customer = app
.db
.get_billing_customer_by_user_id(user.id)
.await?
.context("billing customer not found")?;
let stripe_customer_id = billing_customer
.stripe_customer_id
.parse::<stripe::CustomerId>()
.context("failed to parse Stripe customer ID from database")?;
let subscriptions = Subscription::list(
&stripe_client,
&stripe::ListSubscriptions {
customer: Some(stripe_customer_id),
// Sync all non-canceled subscriptions.
status: None,
..Default::default()
},
)
.await?;
for subscription in subscriptions.data {
let subscription_id = subscription.id.clone();
sync_subscription(&app, &stripe_client, subscription)
.await
.with_context(|| {
format!(
"failed to sync subscription {subscription_id} for user {}",
user.id,
)
})?;
}
Ok(Json(SyncBillingSubscriptionResponse {
stripe_customer_id: billing_customer.stripe_customer_id.clone(),
}))
}
/// The amount of time we wait in between each poll of Stripe events.
///
/// This value should strike a balance between:
@@ -901,7 +937,7 @@ async fn poll_stripe_events(
.create_processed_stripe_event(&processed_event_params)
.await?;
return Ok(());
continue;
}
let process_result = match event.type_ {
@@ -979,52 +1015,29 @@ async fn handle_customer_event(
Ok(())
}
async fn handle_customer_subscription_event(
async fn sync_subscription(
app: &Arc<AppState>,
rpc_server: &Arc<Server>,
stripe_client: &stripe::Client,
event: stripe::Event,
) -> anyhow::Result<()> {
let EventObject::Subscription(subscription) = event.data.object else {
bail!("unexpected event payload for {}", event.id);
subscription: stripe::Subscription,
) -> anyhow::Result<billing_customer::Model> {
let subscription_kind = if let Some(stripe_billing) = &app.stripe_billing {
stripe_billing
.determine_subscription_kind(&subscription)
.await
} else {
None
};
log::info!("handling Stripe {} event: {}", event.type_, event.id);
let subscription_kind = maybe!(async {
let stripe_billing = app.stripe_billing.clone()?;
let zed_pro_price_id = stripe_billing.zed_pro_price_id().await.ok()?;
let zed_free_price_id = stripe_billing.zed_free_price_id().await.ok()?;
subscription.items.data.iter().find_map(|item| {
let price = item.price.as_ref()?;
if price.id == zed_pro_price_id {
Some(if subscription.status == SubscriptionStatus::Trialing {
SubscriptionKind::ZedProTrial
} else {
SubscriptionKind::ZedPro
})
} else if price.id == zed_free_price_id {
Some(SubscriptionKind::ZedFree)
} else {
None
}
})
})
.await;
let billing_customer =
find_or_create_billing_customer(app, stripe_client, subscription.customer)
.await?
.ok_or_else(|| anyhow!("billing customer not found"))?;
.context("billing customer not found")?;
if let Some(SubscriptionKind::ZedProTrial) = subscription_kind {
if subscription.status == SubscriptionStatus::Trialing {
let current_period_start =
DateTime::from_timestamp(subscription.current_period_start, 0)
.ok_or_else(|| anyhow!("No trial subscription period start"))?;
.context("No trial subscription period start")?;
app.db
.update_billing_customer(
@@ -1094,31 +1107,51 @@ async fn handle_customer_subscription_event(
)
.await?;
} else {
// If the user already has an active billing subscription, ignore the
// event and return an `Ok` to signal that it was processed
// successfully.
//
// There is the possibility that this could cause us to not create a
// subscription in the following scenario:
//
// 1. User has an active subscription A
// 2. User cancels subscription A
// 3. User creates a new subscription B
// 4. We process the new subscription B before the cancellation of subscription A
// 5. User ends up with no subscriptions
//
// In theory this situation shouldn't arise as we try to process the events in the order they occur.
if app
if let Some(existing_subscription) = app
.db
.has_active_billing_subscription(billing_customer.user_id)
.get_active_billing_subscription(billing_customer.user_id)
.await?
{
log::info!(
"user {user_id} already has an active subscription, skipping creation of subscription {subscription_id}",
user_id = billing_customer.user_id,
subscription_id = subscription.id
);
return Ok(());
if existing_subscription.kind == Some(SubscriptionKind::ZedFree)
&& subscription_kind == Some(SubscriptionKind::ZedProTrial)
{
let stripe_subscription_id = existing_subscription
.stripe_subscription_id
.parse::<stripe::SubscriptionId>()
.context("failed to parse Stripe subscription ID from database")?;
Subscription::cancel(
&stripe_client,
&stripe_subscription_id,
stripe::CancelSubscription {
invoice_now: None,
..Default::default()
},
)
.await?;
} else {
// If the user already has an active billing subscription, ignore the
// event and return an `Ok` to signal that it was processed
// successfully.
//
// There is the possibility that this could cause us to not create a
// subscription in the following scenario:
//
// 1. User has an active subscription A
// 2. User cancels subscription A
// 3. User creates a new subscription B
// 4. We process the new subscription B before the cancellation of subscription A
// 5. User ends up with no subscriptions
//
// In theory this situation shouldn't arise as we try to process the events in the order they occur.
log::info!(
"user {user_id} already has an active subscription, skipping creation of subscription {subscription_id}",
user_id = billing_customer.user_id,
subscription_id = subscription.id
);
return Ok(billing_customer);
}
}
app.db
@@ -1137,6 +1170,44 @@ async fn handle_customer_subscription_event(
.await?;
}
if let Some(stripe_billing) = app.stripe_billing.as_ref() {
if subscription.status == SubscriptionStatus::Canceled
|| subscription.status == SubscriptionStatus::Paused
{
let already_has_active_billing_subscription = app
.db
.has_active_billing_subscription(billing_customer.user_id)
.await?;
if !already_has_active_billing_subscription {
let stripe_customer_id = billing_customer
.stripe_customer_id
.parse::<stripe::CustomerId>()
.context("failed to parse Stripe customer ID from database")?;
stripe_billing
.subscribe_to_zed_free(stripe_customer_id)
.await?;
}
}
}
Ok(billing_customer)
}
async fn handle_customer_subscription_event(
app: &Arc<AppState>,
rpc_server: &Arc<Server>,
stripe_client: &stripe::Client,
event: stripe::Event,
) -> anyhow::Result<()> {
let EventObject::Subscription(subscription) = event.data.object else {
bail!("unexpected event payload for {}", event.id);
};
log::info!("handling Stripe {} event: {}", event.type_, event.id);
let billing_customer = sync_subscription(app, stripe_client, subscription).await?;
// When the user's subscription changes, push down any changes to their plan.
rpc_server
.update_plan_for_user(billing_customer.user_id)
@@ -1172,7 +1243,7 @@ async fn get_monthly_spend(
.db
.get_user_by_github_user_id(params.github_user_id)
.await?
.ok_or_else(|| anyhow!("user not found"))?;
.context("user not found")?;
let Some(llm_db) = app.llm_db.clone() else {
return Err(Error::http(
@@ -1240,7 +1311,7 @@ async fn get_current_usage(
.db
.get_user_by_github_user_id(params.github_user_id)
.await?
.ok_or_else(|| anyhow!("user not found"))?;
.context("user not found")?;
let feature_flags = app.db.get_user_flags(user.id).await?;
let has_extended_trial = feature_flags
@@ -1382,7 +1453,7 @@ impl From<CancellationDetailsReason> for StripeCancellationReason {
}
/// Finds or creates a billing customer using the provided customer.
async fn find_or_create_billing_customer(
pub async fn find_or_create_billing_customer(
app: &Arc<AppState>,
stripe_client: &stripe::Client,
customer_or_id: Expandable<Customer>,

View File

@@ -1,6 +1,5 @@
use std::sync::{Arc, OnceLock};
use anyhow::anyhow;
use axum::{
Extension, Json, Router,
extract::{self, Query},
@@ -39,7 +38,7 @@ impl CheckIsContributorParams {
return Ok(ContributorSelector::GitHubLogin { github_login });
}
Err(anyhow!(
Err(anyhow::anyhow!(
"must be one of `github_user_id` or `github_login`."
))?
}

View File

@@ -1,6 +1,6 @@
use crate::db::ExtensionVersionConstraints;
use crate::{AppState, Error, Result, db::NewExtensionVersion};
use anyhow::{Context as _, anyhow};
use anyhow::Context as _;
use aws_sdk_s3::presigning::PresigningConfig;
use axum::{
Extension, Json, Router,
@@ -181,7 +181,7 @@ async fn download_latest_extension(
.db
.get_extension(&params.extension_id, constraints.as_ref())
.await?
.ok_or_else(|| anyhow!("unknown extension"))?;
.context("unknown extension")?;
download_extension(
Extension(app),
Path(DownloadExtensionParams {
@@ -238,7 +238,7 @@ async fn download_extension(
))
.presigned(PresigningConfig::expires_in(EXTENSION_DOWNLOAD_URL_LIFETIME).unwrap())
.await
.map_err(|e| anyhow!("failed to create presigned extension download url {e}"))?;
.context("creating presigned extension download url")?;
Ok(Redirect::temporary(url.uri()))
}
@@ -374,7 +374,7 @@ async fn fetch_extension_manifest(
blob_store_bucket: &String,
extension_id: &str,
version: &str,
) -> Result<NewExtensionVersion, anyhow::Error> {
) -> anyhow::Result<NewExtensionVersion> {
let object = blob_store_client
.get_object()
.bucket(blob_store_bucket)
@@ -397,8 +397,8 @@ async fn fetch_extension_manifest(
String::from_utf8_lossy(&manifest_bytes)
)
})?;
let published_at = object.last_modified.ok_or_else(|| {
anyhow!("missing last modified timestamp for extension {extension_id} version {version}")
let published_at = object.last_modified.with_context(|| {
format!("missing last modified timestamp for extension {extension_id} version {version}")
})?;
let published_at = time::OffsetDateTime::from_unix_timestamp_nanos(published_at.as_nanos())?;
let published_at = PrimitiveDateTime::new(published_at.date(), published_at.time());

View File

@@ -1,3 +1,4 @@
use anyhow::Context as _;
use collections::HashMap;
use semantic_version::SemanticVersion;
@@ -13,18 +14,12 @@ pub struct IpsFile {
impl IpsFile {
pub fn parse(bytes: &[u8]) -> anyhow::Result<IpsFile> {
let mut split = bytes.splitn(2, |&b| b == b'\n');
let header_bytes = split
.next()
.ok_or_else(|| anyhow::anyhow!("No header found"))?;
let header: Header = serde_json::from_slice(header_bytes)
.map_err(|e| anyhow::anyhow!("Failed to parse header: {}", e))?;
let header_bytes = split.next().context("No header found")?;
let header: Header = serde_json::from_slice(header_bytes).context("parsing header")?;
let body_bytes = split
.next()
.ok_or_else(|| anyhow::anyhow!("No body found"))?;
let body_bytes = split.next().context("No body found")?;
let body: Body = serde_json::from_slice(body_bytes)
.map_err(|e| anyhow::anyhow!("Failed to parse body: {}", e))?;
let body: Body = serde_json::from_slice(body_bytes).context("parsing body")?;
Ok(IpsFile { header, body })
}

View File

@@ -3,7 +3,7 @@ use crate::{
db::{self, AccessTokenId, Database, UserId},
rpc::Principal,
};
use anyhow::{Context as _, anyhow};
use anyhow::Context as _;
use axum::{
http::{self, Request, StatusCode},
middleware::Next,
@@ -85,14 +85,14 @@ pub async fn validate_header<B>(mut req: Request<B>, next: Next<B>) -> impl Into
.db
.get_user_by_id(user_id)
.await?
.ok_or_else(|| anyhow!("user {} not found", user_id))?;
.with_context(|| format!("user {user_id} not found"))?;
if let Some(impersonator_id) = validate_result.impersonator_id {
let admin = state
.db
.get_user_by_id(impersonator_id)
.await?
.ok_or_else(|| anyhow!("user {} not found", impersonator_id))?;
.with_context(|| format!("user {impersonator_id} not found"))?;
req.extensions_mut()
.insert(Principal::Impersonated { user, admin });
} else {
@@ -192,7 +192,7 @@ pub async fn verify_access_token(
let db_token = db.get_access_token(token.id).await?;
let token_user_id = db_token.impersonated_user_id.unwrap_or(db_token.user_id);
if token_user_id != user_id {
return Err(anyhow!("no such access token"))?;
return Err(anyhow::anyhow!("no such access token"))?;
}
let t0 = Instant::now();

View File

@@ -5,7 +5,7 @@ mod tables;
pub mod tests;
use crate::{Error, Result, executor::Executor};
use anyhow::anyhow;
use anyhow::{Context as _, anyhow};
use collections::{BTreeMap, BTreeSet, HashMap, HashSet};
use dashmap::DashMap;
use futures::StreamExt;
@@ -320,11 +320,9 @@ impl Database {
let mut tx = Arc::new(Some(tx));
let result = f(TransactionHandle(tx.clone())).await;
let Some(tx) = Arc::get_mut(&mut tx).and_then(|tx| tx.take()) else {
return Err(anyhow!(
"couldn't complete transaction because it's still in use"
))?;
};
let tx = Arc::get_mut(&mut tx)
.and_then(|tx| tx.take())
.context("couldn't complete transaction because it's still in use")?;
Ok((tx, result))
}
@@ -344,11 +342,9 @@ impl Database {
let mut tx = Arc::new(Some(tx));
let result = f(TransactionHandle(tx.clone())).await;
let Some(tx) = Arc::get_mut(&mut tx).and_then(|tx| tx.take()) else {
return Err(anyhow!(
"couldn't complete transaction because it's still in use"
))?;
};
let tx = Arc::get_mut(&mut tx)
.and_then(|tx| tx.take())
.context("couldn't complete transaction because it's still in use")?;
Ok((tx, result))
}
@@ -853,9 +849,7 @@ fn db_status_to_proto(
)
}
_ => {
return Err(anyhow!(
"Unexpected combination of status fields: {entry:?}"
));
anyhow::bail!("Unexpected combination of status fields: {entry:?}");
}
};
Ok(proto::StatusEntry {

View File

@@ -1,4 +1,5 @@
use super::*;
use anyhow::Context as _;
use sea_orm::sea_query::Query;
impl Database {
@@ -51,7 +52,7 @@ impl Database {
Ok(access_token::Entity::find_by_id(access_token_id)
.one(&*tx)
.await?
.ok_or_else(|| anyhow!("no such access token"))?)
.context("no such access token")?)
})
.await
}

View File

@@ -57,6 +57,19 @@ impl Database {
.await
}
pub async fn get_billing_customer_by_id(
&self,
id: BillingCustomerId,
) -> Result<Option<billing_customer::Model>> {
self.transaction(|tx| async move {
Ok(billing_customer::Entity::find()
.filter(billing_customer::Column::Id.eq(id))
.one(&*tx)
.await?)
})
.await
}
/// Returns the billing customer for the user with the specified ID.
pub async fn get_billing_customer_by_user_id(
&self,

View File

@@ -1,3 +1,5 @@
use anyhow::Context as _;
use super::*;
#[derive(Debug)]
@@ -82,7 +84,7 @@ impl Database {
Ok(preferences
.into_iter()
.next()
.ok_or_else(|| anyhow!("billing preferences not found"))?)
.context("billing preferences not found")?)
})
.await
}

View File

@@ -1,3 +1,5 @@
use anyhow::Context as _;
use crate::db::billing_subscription::{
StripeCancellationReason, StripeSubscriptionStatus, SubscriptionKind,
};
@@ -32,9 +34,9 @@ impl Database {
pub async fn create_billing_subscription(
&self,
params: &CreateBillingSubscriptionParams,
) -> Result<()> {
) -> Result<billing_subscription::Model> {
self.transaction(|tx| async move {
billing_subscription::Entity::insert(billing_subscription::ActiveModel {
let id = billing_subscription::Entity::insert(billing_subscription::ActiveModel {
billing_customer_id: ActiveValue::set(params.billing_customer_id),
kind: ActiveValue::set(params.kind),
stripe_subscription_id: ActiveValue::set(params.stripe_subscription_id.clone()),
@@ -44,10 +46,14 @@ impl Database {
stripe_current_period_end: ActiveValue::set(params.stripe_current_period_end),
..Default::default()
})
.exec_without_returning(&*tx)
.await?;
.exec(&*tx)
.await?
.last_insert_id;
Ok(())
Ok(billing_subscription::Entity::find_by_id(id)
.one(&*tx)
.await?
.context("failed to retrieve inserted billing subscription")?)
})
.await
}
@@ -236,7 +242,9 @@ impl Database {
.filter(
billing_customer::Column::UserId.eq(user_id).and(
billing_subscription::Column::StripeSubscriptionStatus
.eq(StripeSubscriptionStatus::Active),
.eq(StripeSubscriptionStatus::Active)
.or(billing_subscription::Column::StripeSubscriptionStatus
.eq(StripeSubscriptionStatus::Trialing)),
),
)
.count(&*tx)

View File

@@ -1,4 +1,5 @@
use super::*;
use anyhow::Context as _;
use prost::Message;
use text::{EditOperation, UndoOperation};
@@ -467,7 +468,7 @@ impl Database {
.filter(buffer::Column::ChannelId.eq(channel_id))
.one(&*tx)
.await?
.ok_or_else(|| anyhow!("no such buffer"))?;
.context("no such buffer")?;
let serialization_version = self
.get_buffer_operation_serialization_version(buffer.id, buffer.epoch, &tx)
@@ -606,7 +607,7 @@ impl Database {
.into_values::<_, QueryOperationSerializationVersion>()
.one(tx)
.await?
.ok_or_else(|| anyhow!("missing buffer snapshot"))?)
.context("missing buffer snapshot")?)
}
pub async fn get_channel_buffer(
@@ -621,7 +622,7 @@ impl Database {
.find_related(buffer::Entity)
.one(tx)
.await?
.ok_or_else(|| anyhow!("no such buffer"))?)
.context("no such buffer")?)
}
async fn get_buffer_state(
@@ -643,7 +644,7 @@ impl Database {
)
.one(tx)
.await?
.ok_or_else(|| anyhow!("no such snapshot"))?;
.context("no such snapshot")?;
let version = snapshot.operation_serialization_version;
(snapshot.text, version)
@@ -839,7 +840,7 @@ fn operation_from_storage(
_format_version: i32,
) -> Result<proto::operation::Variant, Error> {
let operation =
storage::Operation::decode(row.value.as_slice()).map_err(|error| anyhow!("{}", error))?;
storage::Operation::decode(row.value.as_slice()).map_err(|error| anyhow!("{error}"))?;
let version = version_from_storage(&operation.version);
Ok(if operation.is_undo {
proto::operation::Variant::Undo(proto::operation::Undo {

View File

@@ -1,4 +1,5 @@
use super::*;
use anyhow::Context as _;
use rpc::{
ErrorCode, ErrorCodeExt,
proto::{ChannelBufferVersion, VectorClockEntry, channel_member::Kind},
@@ -647,11 +648,8 @@ impl Database {
.and(channel_member::Column::UserId.eq(for_user)),
)
.one(&*tx)
.await?;
let Some(membership) = membership else {
Err(anyhow!("no such member"))?
};
.await?
.context("no such member")?;
let mut update = membership.into_active_model();
update.role = ActiveValue::Set(role);

View File

@@ -1,3 +1,5 @@
use anyhow::Context as _;
use super::*;
impl Database {
@@ -215,7 +217,7 @@ impl Database {
)
.one(&*tx)
.await?
.ok_or_else(|| anyhow!("no such contact"))?;
.context("no such contact")?;
contact::Entity::delete_by_id(contact.id).exec(&*tx).await?;

View File

@@ -1,5 +1,6 @@
use std::str::FromStr;
use anyhow::Context;
use chrono::Utc;
use sea_orm::sea_query::IntoCondition;
use util::ResultExt;
@@ -166,7 +167,7 @@ impl Database {
.filter(extension::Column::ExternalId.eq(extension_id))
.one(&*tx)
.await?
.ok_or_else(|| anyhow!("no such extension: {extension_id}"))?;
.with_context(|| format!("no such extension: {extension_id}"))?;
let extensions = [extension];
let mut versions = self
@@ -274,7 +275,7 @@ impl Database {
.filter(extension::Column::ExternalId.eq(*external_id))
.one(&*tx)
.await?
.ok_or_else(|| anyhow!("failed to insert extension"))?
.context("failed to insert extension")?
};
extension_version::Entity::insert_many(versions.iter().map(|version| {

View File

@@ -1,4 +1,5 @@
use super::*;
use anyhow::Context as _;
use rpc::Notification;
use sea_orm::{SelectColumns, TryInsertResult};
use time::OffsetDateTime;
@@ -330,7 +331,7 @@ impl Database {
.filter(channel_message::Column::Nonce.eq(Uuid::from_u128(nonce)))
.one(&*tx)
.await?
.ok_or_else(|| anyhow!("failed to insert message"))?
.context("failed to insert message")?
.id;
}
}

View File

@@ -1,4 +1,5 @@
use super::*;
use anyhow::Context as _;
use rpc::Notification;
use util::ResultExt;
@@ -256,7 +257,7 @@ pub fn model_to_proto(this: &Database, row: notification::Model) -> Result<proto
let kind = this
.notification_kinds_by_id
.get(&row.kind)
.ok_or_else(|| anyhow!("Unknown notification kind"))?;
.context("Unknown notification kind")?;
Ok(proto::Notification {
id: row.id.to_proto(),
kind: kind.to_string(),
@@ -276,5 +277,5 @@ fn notification_kind_from_proto(
.notification_kinds_by_name
.get(&proto.kind)
.copied()
.ok_or_else(|| anyhow!("invalid notification kind {:?}", proto.kind))?)
.with_context(|| format!("invalid notification kind {:?}", proto.kind))?)
}

View File

@@ -49,7 +49,7 @@ impl Database {
)
.one(&*tx)
.await?
.ok_or_else(|| anyhow!("could not find participant"))?;
.context("could not find participant")?;
if participant.room_id != room_id {
return Err(anyhow!("shared project on unexpected room"))?;
}
@@ -128,7 +128,7 @@ impl Database {
let project = project::Entity::find_by_id(project_id)
.one(&*tx)
.await?
.ok_or_else(|| anyhow!("project not found"))?;
.context("project not found")?;
let room = if let Some(room_id) = project.room_id {
Some(self.get_room(room_id, &tx).await?)
} else {
@@ -160,7 +160,7 @@ impl Database {
)
.one(&*tx)
.await?
.ok_or_else(|| anyhow!("no such project"))?;
.context("no such project")?;
self.update_project_worktrees(project.id, worktrees, &tx)
.await?;
@@ -242,7 +242,7 @@ impl Database {
)
.one(&*tx)
.await?
.ok_or_else(|| anyhow!("no such project: {project_id}"))?;
.with_context(|| format!("no such project: {project_id}"))?;
// Update metadata.
worktree::Entity::update(worktree::ActiveModel {
@@ -624,16 +624,13 @@ impl Database {
let project_id = ProjectId::from_proto(update.project_id);
let worktree_id = update.worktree_id as i64;
self.project_transaction(project_id, |tx| async move {
let summary = update
.summary
.as_ref()
.ok_or_else(|| anyhow!("invalid summary"))?;
let summary = update.summary.as_ref().context("invalid summary")?;
// Ensure the update comes from the host.
let project = project::Entity::find_by_id(project_id)
.one(&*tx)
.await?
.ok_or_else(|| anyhow!("no such project"))?;
.context("no such project")?;
if project.host_connection()? != connection {
return Err(anyhow!("can't update a project hosted by someone else"))?;
}
@@ -677,16 +674,13 @@ impl Database {
) -> Result<TransactionGuard<Vec<ConnectionId>>> {
let project_id = ProjectId::from_proto(update.project_id);
self.project_transaction(project_id, |tx| async move {
let server = update
.server
.as_ref()
.ok_or_else(|| anyhow!("invalid language server"))?;
let server = update.server.as_ref().context("invalid language server")?;
// Ensure the update comes from the host.
let project = project::Entity::find_by_id(project_id)
.one(&*tx)
.await?
.ok_or_else(|| anyhow!("no such project"))?;
.context("no such project")?;
if project.host_connection()? != connection {
return Err(anyhow!("can't update a project hosted by someone else"))?;
}
@@ -732,7 +726,7 @@ impl Database {
let project = project::Entity::find_by_id(project_id)
.one(&*tx)
.await?
.ok_or_else(|| anyhow!("no such project"))?;
.context("no such project")?;
if project.host_connection()? != connection {
return Err(anyhow!("can't update a project hosted by someone else"))?;
}
@@ -778,7 +772,7 @@ impl Database {
Ok(project::Entity::find_by_id(id)
.one(&*tx)
.await?
.ok_or_else(|| anyhow!("no such project"))?)
.context("no such project")?)
})
.await
}
@@ -1074,7 +1068,7 @@ impl Database {
let project = project::Entity::find_by_id(project_id)
.one(&*tx)
.await?
.ok_or_else(|| anyhow!("no such project"))?;
.context("no such project")?;
let collaborators = project
.find_related(project_collaborator::Entity)
.all(&*tx)
@@ -1143,7 +1137,7 @@ impl Database {
)
.one(&*tx)
.await?
.ok_or_else(|| anyhow!("failed to read project host"))?;
.context("failed to read project host")?;
Ok(())
})
@@ -1162,7 +1156,7 @@ impl Database {
let project = project::Entity::find_by_id(project_id)
.one(tx)
.await?
.ok_or_else(|| anyhow!("no such project"))?;
.context("no such project")?;
let role_from_room = if let Some(room_id) = project.room_id {
room_participant::Entity::find()
@@ -1287,7 +1281,7 @@ impl Database {
let project = project::Entity::find_by_id(project_id)
.one(tx)
.await?
.ok_or_else(|| anyhow!("no such project"))?;
.context("no such project")?;
let mut collaborators = project_collaborator::Entity::find()
.filter(project_collaborator::Column::ProjectId.eq(project_id))

View File

@@ -161,7 +161,7 @@ impl Database {
)
.one(&*tx)
.await?
.ok_or_else(|| anyhow!("user is not in the room"))?;
.context("user is not in the room")?;
let called_user_role = match caller.role.unwrap_or(ChannelRole::Member) {
ChannelRole::Admin | ChannelRole::Member => ChannelRole::Member,
@@ -193,7 +193,7 @@ impl Database {
let room = self.get_room(room_id, &tx).await?;
let incoming_call = Self::build_incoming_call(&room, called_user_id)
.ok_or_else(|| anyhow!("failed to build incoming call"))?;
.context("failed to build incoming call")?;
Ok((room, incoming_call))
})
.await
@@ -279,7 +279,7 @@ impl Database {
)
.one(&*tx)
.await?
.ok_or_else(|| anyhow!("no call to cancel"))?;
.context("no call to cancel")?;
room_participant::Entity::delete(participant.into_active_model())
.exec(&*tx)
@@ -310,7 +310,7 @@ impl Database {
.into_values::<_, QueryChannelId>()
.one(&*tx)
.await?
.ok_or_else(|| anyhow!("no such room"))?;
.context("no such room")?;
if channel_id.is_some() {
Err(anyhow!("tried to join channel call directly"))?
@@ -462,7 +462,7 @@ impl Database {
}
let (channel, room) = self.get_channel_room(room_id, tx).await?;
let channel = channel.ok_or_else(|| anyhow!("no channel for room"))?;
let channel = channel.context("no channel for room")?;
Ok(JoinRoom {
room,
channel: Some(channel),
@@ -505,7 +505,7 @@ impl Database {
let project = project::Entity::find_by_id(project_id)
.one(&*tx)
.await?
.ok_or_else(|| anyhow!("project does not exist"))?;
.context("project does not exist")?;
if project.host_user_id != Some(user_id) {
return Err(anyhow!("no such project"))?;
}
@@ -519,7 +519,7 @@ impl Database {
.position(|collaborator| {
collaborator.user_id == user_id && collaborator.is_host
})
.ok_or_else(|| anyhow!("host not found among collaborators"))?;
.context("host not found among collaborators")?;
let host = collaborators.swap_remove(host_ix);
let old_connection_id = host.connection();
@@ -1051,11 +1051,7 @@ impl Database {
let tx = tx;
let location_kind;
let location_project_id;
match location
.variant
.as_ref()
.ok_or_else(|| anyhow!("invalid location"))?
{
match location.variant.as_ref().context("invalid location")? {
proto::participant_location::Variant::SharedProject(project) => {
location_kind = 0;
location_project_id = Some(ProjectId::from_proto(project.id));
@@ -1119,7 +1115,7 @@ impl Database {
)
.one(&*tx)
.await?
.ok_or_else(|| anyhow!("only admins can set participant role"))?;
.context("only admins can set participant role")?;
if role.requires_cla() {
self.check_user_has_signed_cla(user_id, room_id, &tx)
@@ -1156,7 +1152,7 @@ impl Database {
let channel = room::Entity::find_by_id(room_id)
.one(tx)
.await?
.ok_or_else(|| anyhow!("could not find room"))?
.context("could not find room")?
.find_related(channel::Entity)
.one(tx)
.await?;
@@ -1297,7 +1293,7 @@ impl Database {
let db_room = room::Entity::find_by_id(room_id)
.one(tx)
.await?
.ok_or_else(|| anyhow!("could not find room"))?;
.context("could not find room")?;
let mut db_participants = db_room
.find_related(room_participant::Entity)

View File

@@ -1,3 +1,4 @@
use anyhow::Context as _;
use chrono::NaiveDateTime;
use super::*;
@@ -247,7 +248,7 @@ impl Database {
.into_values::<_, QueryAs>()
.one(&*tx)
.await?
.ok_or_else(|| anyhow!("could not find user"))?;
.context("could not find user")?;
Ok(metrics_id.to_string())
})
.await

View File

@@ -1,5 +1,5 @@
use crate::db::{ProjectId, Result, RoomId, ServerId, UserId};
use anyhow::anyhow;
use anyhow::Context as _;
use rpc::ConnectionId;
use sea_orm::entity::prelude::*;
@@ -18,10 +18,10 @@ impl Model {
pub fn host_connection(&self) -> Result<ConnectionId> {
let host_connection_server_id = self
.host_connection_server_id
.ok_or_else(|| anyhow!("empty host_connection_server_id"))?;
.context("empty host_connection_server_id")?;
let host_connection_id = self
.host_connection_id
.ok_or_else(|| anyhow!("empty host_connection_id"))?;
.context("empty host_connection_id")?;
Ok(ConnectionId {
owner_id: host_connection_server_id.0 as u32,
id: host_connection_id as u32,

View File

@@ -1,4 +1,4 @@
use anyhow::{Result, anyhow};
use anyhow::{Context as _, Result};
use std::fs;
use std::path::Path;
@@ -6,8 +6,8 @@ pub fn get_dotenv_vars(current_dir: impl AsRef<Path>) -> Result<Vec<(String, Str
let current_dir = current_dir.as_ref();
let mut vars = Vec::new();
let env_content = fs::read_to_string(current_dir.join(".env.toml"))
.map_err(|_| anyhow!("no .env.toml file found"))?;
let env_content =
fs::read_to_string(current_dir.join(".env.toml")).context("no .env.toml file found")?;
add_vars(env_content, &mut vars)?;

View File

@@ -14,7 +14,7 @@ pub mod user_backfiller;
#[cfg(test)]
mod tests;
use anyhow::anyhow;
use anyhow::Context as _;
use aws_config::{BehaviorVersion, Region};
use axum::{
http::{HeaderMap, StatusCode},
@@ -339,7 +339,7 @@ fn build_stripe_client(config: &Config) -> anyhow::Result<stripe::Client> {
let api_key = config
.stripe_api_key
.as_ref()
.ok_or_else(|| anyhow!("missing stripe_api_key"))?;
.context("missing stripe_api_key")?;
Ok(stripe::Client::new(api_key))
}
@@ -348,11 +348,11 @@ async fn build_blob_store_client(config: &Config) -> anyhow::Result<aws_sdk_s3::
config
.blob_store_access_key
.clone()
.ok_or_else(|| anyhow!("missing blob_store_access_key"))?,
.context("missing blob_store_access_key")?,
config
.blob_store_secret_key
.clone()
.ok_or_else(|| anyhow!("missing blob_store_secret_key"))?,
.context("missing blob_store_secret_key")?,
None,
None,
"env",
@@ -363,13 +363,13 @@ async fn build_blob_store_client(config: &Config) -> anyhow::Result<aws_sdk_s3::
config
.blob_store_url
.as_ref()
.ok_or_else(|| anyhow!("missing blob_store_url"))?,
.context("missing blob_store_url")?,
)
.region(Region::new(
config
.blob_store_region
.clone()
.ok_or_else(|| anyhow!("missing blob_store_region"))?,
.context("missing blob_store_region")?,
))
.credentials_provider(keys)
.load()
@@ -383,11 +383,11 @@ async fn build_kinesis_client(config: &Config) -> anyhow::Result<aws_sdk_kinesis
config
.kinesis_access_key
.clone()
.ok_or_else(|| anyhow!("missing kinesis_access_key"))?,
.context("missing kinesis_access_key")?,
config
.kinesis_secret_key
.clone()
.ok_or_else(|| anyhow!("missing kinesis_secret_key"))?,
.context("missing kinesis_secret_key")?,
None,
None,
"env",
@@ -398,7 +398,7 @@ async fn build_kinesis_client(config: &Config) -> anyhow::Result<aws_sdk_kinesis
config
.kinesis_region
.clone()
.ok_or_else(|| anyhow!("missing kinesis_region"))?,
.context("missing kinesis_region")?,
))
.credentials_provider(keys)
.load()

View File

@@ -19,7 +19,7 @@ use usage_measure::UsageMeasure;
use std::future::Future;
use std::sync::Arc;
use anyhow::anyhow;
use anyhow::Context;
pub use sea_orm::ConnectOptions;
use sea_orm::prelude::*;
use sea_orm::{
@@ -93,7 +93,7 @@ impl LlmDatabase {
Ok(self
.models
.get(&(provider, name.to_string()))
.ok_or_else(|| anyhow!("unknown model {provider:?}:{name}"))?)
.with_context(|| format!("unknown model {provider:?}:{name}"))?)
}
pub fn model_by_id(&self, id: ModelId) -> Result<&model::Model> {
@@ -101,7 +101,7 @@ impl LlmDatabase {
.models
.values()
.find(|model| model.id == id)
.ok_or_else(|| anyhow!("no model for ID {id:?}"))?)
.with_context(|| format!("no model for ID {id:?}"))?)
}
pub fn options(&self) -> &ConnectOptions {
@@ -142,11 +142,9 @@ impl LlmDatabase {
let mut tx = Arc::new(Some(tx));
let result = f(TransactionHandle(tx.clone())).await;
let Some(tx) = Arc::get_mut(&mut tx).and_then(|tx| tx.take()) else {
return Err(anyhow!(
"couldn't complete transaction because it's still in use"
))?;
};
let tx = Arc::get_mut(&mut tx)
.and_then(|tx| tx.take())
.context("couldn't complete transaction because it's still in use")?;
Ok((tx, result))
}

View File

@@ -2,7 +2,7 @@ use crate::db::billing_subscription::SubscriptionKind;
use crate::db::{billing_subscription, user};
use crate::llm::AGENT_EXTENDED_TRIAL_FEATURE_FLAG;
use crate::{Config, db::billing_preference};
use anyhow::{Result, anyhow};
use anyhow::{Context as _, Result};
use chrono::{NaiveDateTime, Utc};
use jsonwebtoken::{DecodingKey, EncodingKey, Header, Validation};
use serde::{Deserialize, Serialize};
@@ -42,31 +42,28 @@ impl LlmTokenClaims {
is_staff: bool,
billing_preferences: Option<billing_preference::Model>,
feature_flags: &Vec<String>,
subscription: Option<billing_subscription::Model>,
subscription: billing_subscription::Model,
system_id: Option<String>,
config: &Config,
) -> Result<String> {
let secret = config
.llm_api_secret
.as_ref()
.ok_or_else(|| anyhow!("no LLM API secret"))?;
.context("no LLM API secret")?;
let plan = if is_staff {
Plan::ZedPro
} else {
subscription
.as_ref()
.and_then(|subscription| subscription.kind)
.map_or(Plan::ZedFree, |kind| match kind {
SubscriptionKind::ZedFree => Plan::ZedFree,
SubscriptionKind::ZedPro => Plan::ZedPro,
SubscriptionKind::ZedProTrial => Plan::ZedProTrial,
})
subscription.kind.map_or(Plan::ZedFree, |kind| match kind {
SubscriptionKind::ZedFree => Plan::ZedFree,
SubscriptionKind::ZedPro => Plan::ZedPro,
SubscriptionKind::ZedProTrial => Plan::ZedProTrial,
})
};
let subscription_period =
billing_subscription::Model::current_period(subscription, is_staff)
billing_subscription::Model::current_period(Some(subscription), is_staff)
.map(|(start, end)| (start.naive_utc(), end.naive_utc()))
.ok_or_else(|| anyhow!("A plan is required to use Zed's hosted models or edit predictions. Visit https://zed.dev/account to get started."))?;
.context("A plan is required to use Zed's hosted models or edit predictions. Visit https://zed.dev/account to get started.")?;
let now = Utc::now();
let claims = Self {
@@ -115,7 +112,7 @@ impl LlmTokenClaims {
let secret = config
.llm_api_secret
.as_ref()
.ok_or_else(|| anyhow!("no LLM API secret"))?;
.context("no LLM API secret")?;
match jsonwebtoken::decode::<Self>(
token,

View File

@@ -1,4 +1,4 @@
use anyhow::anyhow;
use anyhow::{Context as _, anyhow};
use axum::headers::HeaderMapExt;
use axum::{
Extension, Router,
@@ -138,11 +138,11 @@ async fn main() -> Result<()> {
.config
.llm_database_url
.as_ref()
.ok_or_else(|| anyhow!("missing LLM_DATABASE_URL"))?;
.context("missing LLM_DATABASE_URL")?;
let max_connections = state
.config
.llm_database_max_connections
.ok_or_else(|| anyhow!("missing LLM_DATABASE_MAX_CONNECTIONS"))?;
.context("missing LLM_DATABASE_MAX_CONNECTIONS")?;
let mut db_options = db::ConnectOptions::new(database_url);
db_options.max_connections(max_connections);
@@ -287,7 +287,7 @@ async fn setup_llm_database(config: &Config) -> Result<()> {
let database_url = config
.llm_database_url
.as_ref()
.ok_or_else(|| anyhow!("missing LLM_DATABASE_URL"))?;
.context("missing LLM_DATABASE_URL")?;
let db_options = db::ConnectOptions::new(database_url.clone());
let db = LlmDatabase::new(db_options, Executor::Production).await?;

View File

@@ -30,12 +30,11 @@ pub async fn run_database_migrations(
for migration in migrations {
match applied_migrations.get(&migration.version) {
Some(applied_migration) => {
if migration.checksum != applied_migration.checksum {
Err(anyhow!(
"checksum mismatch for applied migration {}",
migration.description
))?;
}
anyhow::ensure!(
migration.checksum == applied_migration.checksum,
"checksum mismatch for applied migration {}",
migration.description
);
}
None => {
let elapsed = connection.apply(&migration).await?;

View File

@@ -1,5 +1,6 @@
mod connection_pool;
use crate::api::billing::find_or_create_billing_customer;
use crate::api::{CloudflareIpCountryHeader, SystemIdHeader};
use crate::db::billing_subscription::SubscriptionKind;
use crate::llm::db::LlmDatabase;
@@ -663,7 +664,7 @@ impl Server {
Err(error) => {
let proto_err = match &error {
Error::Internal(err) => err.to_proto(),
_ => ErrorCode::Internal.message(format!("{}", error)).to_proto(),
_ => ErrorCode::Internal.message(format!("{error}")).to_proto(),
};
peer.respond_with_error(receipt, proto_err)?;
Err(error)
@@ -937,7 +938,7 @@ impl Server {
.db
.get_user_by_id(user_id)
.await?
.ok_or_else(|| anyhow!("user not found"))?;
.context("user not found")?;
let update_user_plan = make_update_user_plan_message(
&self.app_state.db,
@@ -1168,7 +1169,7 @@ pub async fn handle_metrics(Extension(server): Extension<Arc<Server>>) -> Result
let metric_families = prometheus::gather();
let encoded_metrics = encoder
.encode_to_string(&metric_families)
.map_err(|err| anyhow!("{}", err))?;
.map_err(|err| anyhow!("{err}"))?;
Ok(encoded_metrics)
}
@@ -1684,7 +1685,7 @@ async fn decline_call(message: proto::DeclineCall, session: Session) -> Result<(
.await
.decline_call(Some(room_id), session.user_id())
.await?
.ok_or_else(|| anyhow!("failed to decline call"))?;
.context("declining call")?;
room_updated(&room, &session.peer);
}
@@ -1714,9 +1715,7 @@ async fn update_participant_location(
session: Session,
) -> Result<()> {
let room_id = RoomId::from_proto(request.room_id);
let location = request
.location
.ok_or_else(|| anyhow!("invalid location"))?;
let location = request.location.context("invalid location")?;
let db = session.db().await;
let room = db
@@ -2245,7 +2244,7 @@ async fn create_buffer_for_peer(
session.connection_id,
)
.await?;
let peer_id = request.peer_id.ok_or_else(|| anyhow!("invalid peer id"))?;
let peer_id = request.peer_id.context("invalid peer id")?;
session
.peer
.forward_send(session.connection_id, peer_id.into(), request)?;
@@ -2376,10 +2375,7 @@ async fn follow(
) -> Result<()> {
let room_id = RoomId::from_proto(request.room_id);
let project_id = request.project_id.map(ProjectId::from_proto);
let leader_id = request
.leader_id
.ok_or_else(|| anyhow!("invalid leader id"))?
.into();
let leader_id = request.leader_id.context("invalid leader id")?.into();
let follower_id = session.connection_id;
session
@@ -2410,10 +2406,7 @@ async fn follow(
async fn unfollow(request: proto::Unfollow, session: Session) -> Result<()> {
let room_id = RoomId::from_proto(request.room_id);
let project_id = request.project_id.map(ProjectId::from_proto);
let leader_id = request
.leader_id
.ok_or_else(|| anyhow!("invalid leader id"))?
.into();
let leader_id = request.leader_id.context("invalid leader id")?.into();
let follower_id = session.connection_id;
session
@@ -3357,9 +3350,7 @@ async fn join_channel_internal(
};
channel_updated(
&joined_room
.channel
.ok_or_else(|| anyhow!("channel not returned"))?,
&joined_room.channel.context("channel not returned")?,
&joined_room.room,
&session.peer,
&*session.connection_pool().await,
@@ -3567,9 +3558,7 @@ async fn send_channel_message(
// TODO: adjust mentions if body is trimmed
let timestamp = OffsetDateTime::now_utc();
let nonce = request
.nonce
.ok_or_else(|| anyhow!("nonce can't be blank"))?;
let nonce = request.nonce.context("nonce can't be blank")?;
let channel_id = ChannelId::from_proto(request.channel_id);
let CreatedChannelMessage {
@@ -3709,10 +3698,7 @@ async fn update_channel_message(
)
.await?;
let nonce = request
.nonce
.clone()
.ok_or_else(|| anyhow!("nonce can't be blank"))?;
let nonce = request.nonce.clone().context("nonce can't be blank")?;
let message = proto::ChannelMessage {
sender_id: session.user_id().to_proto(),
@@ -3817,14 +3803,12 @@ async fn get_supermaven_api_key(
return Err(anyhow!("supermaven not enabled for this account"))?;
}
let email = session
.email()
.ok_or_else(|| anyhow!("user must have an email"))?;
let email = session.email().context("user must have an email")?;
let supermaven_admin_api = session
.supermaven_client
.as_ref()
.ok_or_else(|| anyhow!("supermaven not configured"))?;
.context("supermaven not configured")?;
let result = supermaven_admin_api
.try_get_or_create_user(CreateExternalUserRequest { id: user_id, email })
@@ -3972,7 +3956,7 @@ async fn get_private_user_info(
let user = db
.get_user_by_id(session.user_id())
.await?
.ok_or_else(|| anyhow!("user not found"))?;
.context("user not found")?;
let flags = db.get_user_flags(session.user_id()).await?;
response.send(proto::GetPrivateUserInfoResponse {
@@ -4018,13 +4002,66 @@ async fn get_llm_api_token(
let user = db
.get_user_by_id(user_id)
.await?
.ok_or_else(|| anyhow!("user {} not found", user_id))?;
.with_context(|| format!("user {user_id} not found"))?;
if user.accepted_tos_at.is_none() {
Err(anyhow!("terms of service not accepted"))?
}
let billing_subscription = db.get_active_billing_subscription(user.id).await?;
let stripe_client = session
.app_state
.stripe_client
.as_ref()
.context("failed to retrieve Stripe client")?;
let stripe_billing = session
.app_state
.stripe_billing
.as_ref()
.context("failed to retrieve Stripe billing object")?;
let billing_customer =
if let Some(billing_customer) = db.get_billing_customer_by_user_id(user.id).await? {
billing_customer
} else {
let customer_id = stripe_billing
.find_or_create_customer_by_email(user.email_address.as_deref())
.await?;
find_or_create_billing_customer(
&session.app_state,
&stripe_client,
stripe::Expandable::Id(customer_id),
)
.await?
.context("billing customer not found")?
};
let billing_subscription =
if let Some(billing_subscription) = db.get_active_billing_subscription(user.id).await? {
billing_subscription
} else {
let stripe_customer_id = billing_customer
.stripe_customer_id
.parse::<stripe::CustomerId>()
.context("failed to parse Stripe customer ID from database")?;
let stripe_subscription = stripe_billing
.subscribe_to_zed_free(stripe_customer_id)
.await?;
db.create_billing_subscription(&db::CreateBillingSubscriptionParams {
billing_customer_id: billing_customer.id,
kind: Some(SubscriptionKind::ZedFree),
stripe_subscription_id: stripe_subscription.id.to_string(),
stripe_subscription_status: stripe_subscription.status.into(),
stripe_cancellation_reason: None,
stripe_current_period_start: Some(stripe_subscription.current_period_start),
stripe_current_period_end: Some(stripe_subscription.current_period_end),
})
.await?
};
let billing_preferences = db.get_billing_preferences(user.id).await?;
let token = LlmTokenClaims::create(

View File

@@ -1,5 +1,5 @@
use crate::db::{ChannelId, ChannelRole, UserId};
use anyhow::{Result, anyhow};
use anyhow::{Context as _, Result};
use collections::{BTreeMap, HashMap, HashSet};
use rpc::ConnectionId;
use semantic_version::SemanticVersion;
@@ -77,7 +77,7 @@ impl ConnectionPool {
let connection = self
.connections
.get_mut(&connection_id)
.ok_or_else(|| anyhow!("no such connection"))?;
.context("no such connection")?;
let user_id = connection.user_id;

View File

@@ -1,12 +1,13 @@
use std::sync::Arc;
use crate::Result;
use crate::db::billing_subscription::SubscriptionKind;
use crate::llm::AGENT_EXTENDED_TRIAL_FEATURE_FLAG;
use anyhow::{Context as _, anyhow};
use chrono::Utc;
use collections::HashMap;
use serde::{Deserialize, Serialize};
use stripe::PriceId;
use stripe::{CreateCustomer, Customer, CustomerId, PriceId, SubscriptionStatus};
use tokio::sync::RwLock;
use uuid::Uuid;
@@ -97,6 +98,71 @@ impl StripeBilling {
.ok_or_else(|| crate::Error::Internal(anyhow!("no price found for {lookup_key:?}")))
}
pub async fn determine_subscription_kind(
&self,
subscription: &stripe::Subscription,
) -> Option<SubscriptionKind> {
let zed_pro_price_id = self.zed_pro_price_id().await.ok()?;
let zed_free_price_id = self.zed_free_price_id().await.ok()?;
subscription.items.data.iter().find_map(|item| {
let price = item.price.as_ref()?;
if price.id == zed_pro_price_id {
Some(if subscription.status == SubscriptionStatus::Trialing {
SubscriptionKind::ZedProTrial
} else {
SubscriptionKind::ZedPro
})
} else if price.id == zed_free_price_id {
Some(SubscriptionKind::ZedFree)
} else {
None
}
})
}
/// Returns the Stripe customer associated with the provided email address, or creates a new customer, if one does
/// not already exist.
///
/// Always returns a new Stripe customer if the email address is `None`.
pub async fn find_or_create_customer_by_email(
&self,
email_address: Option<&str>,
) -> Result<CustomerId> {
let existing_customer = if let Some(email) = email_address {
let customers = Customer::list(
&self.client,
&stripe::ListCustomers {
email: Some(email),
..Default::default()
},
)
.await?;
customers.data.first().cloned()
} else {
None
};
let customer_id = if let Some(existing_customer) = existing_customer {
existing_customer.id
} else {
let customer = Customer::create(
&self.client,
CreateCustomer {
email: email_address,
..Default::default()
},
)
.await?;
customer.id
};
Ok(customer_id)
}
pub async fn subscribe_to_price(
&self,
subscription_id: &stripe::SubscriptionId,
@@ -212,7 +278,7 @@ impl StripeBilling {
trial_period_days: Some(trial_period_days),
trial_settings: Some(stripe::CreateCheckoutSessionSubscriptionDataTrialSettings {
end_behavior: stripe::CreateCheckoutSessionSubscriptionDataTrialSettingsEndBehavior {
missing_payment_method: stripe::CreateCheckoutSessionSubscriptionDataTrialSettingsEndBehaviorMissingPaymentMethod::Pause,
missing_payment_method: stripe::CreateCheckoutSessionSubscriptionDataTrialSettingsEndBehaviorMissingPaymentMethod::Cancel,
}
}),
metadata: if !subscription_metadata.is_empty() {
@@ -238,6 +304,46 @@ impl StripeBilling {
Ok(session.url.context("no checkout session URL")?)
}
pub async fn subscribe_to_zed_free(
&self,
customer_id: stripe::CustomerId,
) -> Result<stripe::Subscription> {
let zed_free_price_id = self.zed_free_price_id().await?;
let existing_subscriptions = stripe::Subscription::list(
&self.client,
&stripe::ListSubscriptions {
customer: Some(customer_id.clone()),
status: None,
..Default::default()
},
)
.await?;
let existing_active_subscription =
existing_subscriptions
.data
.into_iter()
.find(|subscription| {
subscription.status == SubscriptionStatus::Active
|| subscription.status == SubscriptionStatus::Trialing
});
if let Some(subscription) = existing_active_subscription {
return Ok(subscription);
}
let mut params = stripe::CreateSubscription::new(customer_id);
params.items = Some(vec![stripe::CreateSubscriptionItems {
price: Some(zed_free_price_id.to_string()),
quantity: Some(1),
..Default::default()
}]);
let subscription = stripe::Subscription::create(&self.client, params).await?;
Ok(subscription)
}
pub async fn checkout_with_zed_free(
&self,
customer_id: stripe::CustomerId,

View File

@@ -2517,7 +2517,7 @@ async fn test_add_breakpoints(cx_a: &mut TestAppContext, cx_b: &mut TestAppConte
.clone()
.unwrap()
.read(cx)
.all_breakpoints(cx)
.all_source_breakpoints(cx)
.clone()
});
let breakpoints_b = editor_b.update(cx_b, |editor, cx| {
@@ -2526,7 +2526,7 @@ async fn test_add_breakpoints(cx_a: &mut TestAppContext, cx_b: &mut TestAppConte
.clone()
.unwrap()
.read(cx)
.all_breakpoints(cx)
.all_source_breakpoints(cx)
.clone()
});
@@ -2550,7 +2550,7 @@ async fn test_add_breakpoints(cx_a: &mut TestAppContext, cx_b: &mut TestAppConte
.clone()
.unwrap()
.read(cx)
.all_breakpoints(cx)
.all_source_breakpoints(cx)
.clone()
});
let breakpoints_b = editor_b.update(cx_b, |editor, cx| {
@@ -2559,7 +2559,7 @@ async fn test_add_breakpoints(cx_a: &mut TestAppContext, cx_b: &mut TestAppConte
.clone()
.unwrap()
.read(cx)
.all_breakpoints(cx)
.all_source_breakpoints(cx)
.clone()
});
@@ -2583,7 +2583,7 @@ async fn test_add_breakpoints(cx_a: &mut TestAppContext, cx_b: &mut TestAppConte
.clone()
.unwrap()
.read(cx)
.all_breakpoints(cx)
.all_source_breakpoints(cx)
.clone()
});
let breakpoints_b = editor_b.update(cx_b, |editor, cx| {
@@ -2592,7 +2592,7 @@ async fn test_add_breakpoints(cx_a: &mut TestAppContext, cx_b: &mut TestAppConte
.clone()
.unwrap()
.read(cx)
.all_breakpoints(cx)
.all_source_breakpoints(cx)
.clone()
});
@@ -2616,7 +2616,7 @@ async fn test_add_breakpoints(cx_a: &mut TestAppContext, cx_b: &mut TestAppConte
.clone()
.unwrap()
.read(cx)
.all_breakpoints(cx)
.all_source_breakpoints(cx)
.clone()
});
let breakpoints_b = editor_b.update(cx_b, |editor, cx| {
@@ -2625,7 +2625,7 @@ async fn test_add_breakpoints(cx_a: &mut TestAppContext, cx_b: &mut TestAppConte
.clone()
.unwrap()
.read(cx)
.all_breakpoints(cx)
.all_source_breakpoints(cx)
.clone()
});

View File

@@ -1,6 +1,6 @@
use super::{RandomizedTest, TestClient, TestError, TestServer, UserTestPlan};
use crate::{db::UserId, tests::run_randomized_test};
use anyhow::{Result, anyhow};
use anyhow::{Context as _, Result};
use async_trait::async_trait;
use call::ActiveCall;
use collections::{BTreeMap, HashMap};
@@ -782,8 +782,7 @@ impl RandomizedTest for ProjectCollaborationTest {
let save =
project.update(cx, |project, cx| project.save_buffer(buffer.clone(), cx));
let save = cx.spawn(|cx| async move {
save.await
.map_err(|err| anyhow!("save request failed: {:?}", err))?;
save.await.context("save request failed")?;
assert!(
buffer
.read_with(&cx, |buffer, _| { buffer.saved_version().to_owned() })

View File

@@ -1,6 +1,6 @@
use std::sync::Arc;
use anyhow::{Context as _, Result, anyhow};
use anyhow::{Context as _, Result};
use chrono::{DateTime, Utc};
use util::ResultExt;
@@ -144,12 +144,9 @@ impl UserBackfiller {
}
}
let response = match response.error_for_status() {
Ok(response) => response,
Err(err) => return Err(anyhow!("failed to fetch GitHub user: {err}")),
};
response
.error_for_status()
.context("fetching GitHub user")?
.json()
.await
.with_context(|| format!("failed to deserialize GitHub user from '{url}'"))

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