Compare commits

...

143 Commits

Author SHA1 Message Date
Peter Tripp
363530ea84 zed 0.158.1 2024-10-16 14:47:11 -04:00
gcp-cherry-pick-bot[bot]
7ce18afabf assistant: Direct user to account page to subscribe for more LLM usage (cherry-pick #19300) (#19301)
Cherry-picked assistant: Direct user to account page to subscribe for
more LLM usage (#19300)

This PR updates the location where we send the user to subscribe for
more LLM usage to the account page.

Release Notes:

- Updated the URL to the account page when subscribing to LLM usage.

Co-authored-by: Marshall Bowers <elliott.codes@gmail.com>
2024-10-16 14:26:52 -04:00
Joseph T. Lyons
f9beb25fb0 v0.158.x preview 2024-10-16 12:47:43 -04:00
Antonio Scandurra
474e670bbd Increase monthly free tier spend from 5 dollars to 10 dollars (#19291)
Release Notes:

- N/A

Co-authored-by: Marshall <marshall@zed.dev>
Co-authored-by: Richard <richard@zed.dev>
2024-10-16 12:22:24 -04:00
Marshall Bowers
fe0bcc063c collab: Add Stripe API key to Kubernetes template (#19292)
This PR adds the Stripe API key to the Kubernetes template.

It's optional right now, so we can set the API key when we're ready.

Release Notes:

- N/A
2024-10-16 12:10:39 -04:00
Thorsten Ball
69abe71bf7 ssh remoting: Treat closed stderr as error (#19289)
Before this change we had a race condition bug: if stderr was closed
before the other two sockets, we wouldn't properly detect when the
server died, and not report or retry anything.

That's because we treated a closed stderr as a non-error.

Technically, it isn't an error (closing a connection is okay!), but
until we have a proper shutdown ceremony between all three processes, we
can treat it as an error, because that lets us to detect when the server
is gone.

On the client-side, we also always react to these errors by
reconnecting. Except when we shutdown: there we do a proper shutdown and
won't error on the proxy exit code.

So, this works, even if I wish there was a better way for the server to
communicate to the proxy that it shutdown properly. But I don't want a
fourth socket.

Release Notes:

- N/A
2024-10-16 18:05:52 +02:00
Marshall Bowers
9c3d80d6e8 collab: Fetch more meters and prices when initializing StripeBilling (#19288)
This PR makes it so we fetch more meters and prices when initializing
`StripeBilling`, as we have more than 10 meters defined.

Release Notes:

- N/A
2024-10-16 11:40:56 -04:00
Kirill Bulatov
834d50f0db Properly open worktrees when cmd-clicking in terminal or on inlay hints (#19280)
* uses the state that's synced, to fetch the language server name
* uses proper, canonicalized path when creating a remote ssh worktree,
otherwise `~/foo/something` stays unexpanded

Release Notes:

- N/A
2024-10-16 18:12:36 +03:00
Kirill Bulatov
bcdb10b3cb Do not attempt to install prettier if the language change is unrelated (#19283)
Release Notes:

- Fix prettier install being attempted too much
2024-10-16 18:10:05 +03:00
Marshall Bowers
598939d186 collab: Refresh the user's LLM token when their subscription changes (#19281)
This PR makes it so collab will trigger a refresh for a user's LLM token
whenever their subscription changes.

This allows us to proactively push down changes to their subscription.

In order to facilitate this, the Stripe event processing has been moved
from the `api` service to the `collab` service in order to access the
RPC server.

Release Notes:

- N/A
2024-10-16 10:58:28 -04:00
Thorsten Ball
9d944d0662 ssh remote: Restore ControlPersist=no (#19277)
This restores the change from #19193 that I erroneously reverted in
#19234.

I think the bug in #19275 got in my way when testing.

With that bug fixed, the changes in here also work fine.


Release Notes:

- N/A
2024-10-16 16:13:31 +02:00
Tilman Roeder
7d2628e805 Make the divider rule color more muted (#19255)
I've been a bit annoyed by the hover divider rule being extremely bright
compared to other divider rules in the UI. This PR updates their color
to use the regular border color from the current theme instead of the
muted (but still pretty bright) text color.

Apologies for the unsolicited PR (and please feel free to close if it
goes against some other plans / designs you already have in place :).

#### Example screenshot before:
<img width="302" alt="Screenshot 2024-10-15 at 23 29 18"
src="https://github.com/user-attachments/assets/7ea22808-8135-4a46-9457-e670225aebaa">

#### Example screenshot after:
<img width="312" alt="Screenshot 2024-10-15 at 23 28 16"
src="https://github.com/user-attachments/assets/63ac0d02-ae6d-4962-84a2-1fdb95519b15">

***

Release Notes:

- Make the divider rule in LSP hovers more muted
2024-10-16 11:00:22 -03:00
Ihnat Aŭtuška
84df3a0cad Allow formatting selections via LSP (#18752)
Release Notes:

- Added a new `editor: format selections` action that allows formatting
only the currently selected text via the primary language server.

---------

Co-authored-by: Thorsten Ball <mrnugget@gmail.com>
2024-10-16 15:58:37 +02:00
Thorsten Ball
eb76065ad3 ssh remoting: Fix hang when activity channel gets dropped (#19275)
When the SSH command dies or the server, the channel gets dropped and
the heartbeat method went into an infinite loop causing a hang.

Oversight from yesterday. Fixed now.

Release Notes:

- N/A
2024-10-16 15:57:58 +02:00
Peter Tripp
84018d7a2d zig: Bump to v0.3.1 (#19252)
Includes:
- https://github.com/zed-industries/zed/pull/18323
- https://github.com/zed-industries/zed/pull/17488
2024-10-16 08:42:45 -04:00
Peter Tripp
57c55b32e1 html: Bump to v0.1.3 (#19251)
Includes:
- https://github.com/zed-industries/zed/pull/18024
2024-10-16 08:42:27 -04:00
Peter Tripp
a4357c429a elixir: Bump to v0.1.0 (#19250)
Includes:
- https://github.com/zed-industries/zed/pull/18024
- https://github.com/zed-industries/zed/pull/17488
- https://github.com/zed-industries/zed/pull/16985
2024-10-16 08:42:07 -04:00
Peter Tripp
103665ee28 astro: Bump to v0.1.1 (#19249)
Includes:
- https://github.com/zed-industries/zed/pull/18024
2024-10-16 08:41:45 -04:00
Thorsten Ball
2f960c4aba project environment: Log when which env is used (#19270)
This adds more logging for debugging purposes.

Release Notes:

- N/A
2024-10-16 14:12:45 +02:00
Piotr Osiewicz
109ebc5f27 ui: Add Scrollbar component (#18927)
Closes #ISSUE

Release Notes:

- N/A
2024-10-16 13:57:28 +02:00
Stanislav Alekseev
eddf70b5c4 Revert "lsp: Do not notify all language servers on file save" (#19183)
Reverts zed-industries/zed#17756. According to the existing
implementations of the LSP specification, namely
[Helix](a7651f5bf0/helix-view/src/document.rs (L1038))
and, if I'm not wrong,
[VSCode](https://github.com/microsoft/vscode-languageserver-node/blob/main/client/src/common/textSynchronization.ts#L580),
`textDocument/didSave` has nothing to do with the watched files and
should be sent to the language servers connected to the buffers even if
the files are not watched by those. As the LSP spec doesn't say anything
about `didSave` being related to the watched files, and the reference
implementation in VSCode seemingly does not filter the notifications
according to those, it seems like this is an incorrect interpretation of
the specification

This also causes issues with language servers. See [Metals
issue](https://github.com/scalameta/metals-zed/issues/28#issuecomment-2410393150)
for example

Closes #18636

Release Notes:

- N/A
2024-10-16 12:41:01 +02:00
Stanislav Alekseev
128619899e Environment loading fixes (#19144)
Closes #19040
Addresses the problem with annoying error messages on windows (see
comment from SomeoneToIgnore on #18567)

Release Notes:

- Fixed the bug where language servers from PATH would sometimes be
prioritised over the ones from `direnv`
- Stopped running environment loading on windows as it didn't work
anyways due to `SHELL` not being set
2024-10-16 12:14:40 +02:00
Axel Carlsson
a77ec94cbc vim: Add support for insert button (#19245)
This commit adds support for using the physical insert-button. First
click toggles insert mode and subsequent clicks toggle back and forth
between replace and insert mode.

Closes #19224

Release Notes:

- Added support for using the insert button for vim_mode.
2024-10-16 12:11:17 +02:00
Kirill Bulatov
a56f946a7d Force astro-language-server to be the primary one for Astro (#19266)
Part of https://github.com/zed-industries/zed/issues/19239

Overall, this hardcoding approach has to stop and Zed better show some
notification/modal that proposes to select a primary language server,
when launching with the language that has no such settings.

Release Notes:

- Fixed Astro LSP interactions
2024-10-16 10:06:45 +03:00
Mikayla Maki
f944ebc4cb Add settings to remote servers, use XDG paths on remote, and enable node LSPs (#19176)
Supersedes https://github.com/zed-industries/zed/pull/19166

TODO:
- [x] Update basic zed paths
- [x] update create_state_directory
- [x] Use this with `NodeRuntime`
- [x] Add server settings
- [x] Add an 'open server settings command'
- [x] Make sure it all works


Release Notes:

- Updated the actions `zed::OpenLocalSettings` and `zed::OpenLocalTasks`
to `zed::OpenProjectSettings` and `zed::OpenProjectTasks`.

---------

Co-authored-by: Conrad <conrad@zed.dev>
Co-authored-by: Richard <richard@zed.dev>
2024-10-15 23:32:44 -07:00
CharlesChen0823
1dda039f38 remote_server: local build also need feature debug-embed (#19265)
it's waste me one more hour. IMO, this also need for `build_local`

Release Notes:

- N/A
2024-10-15 23:08:37 -07:00
Alvaro Gaona
182230a0ba Fix C++ configuration documentation (#19258)
- Update the binary JSON object
- Add .clangd configuration file summary


![image](https://github.com/user-attachments/assets/5c4cfd26-3cd8-4d12-96fc-483596c74287)

Release Notes:

- N/A
2024-10-16 08:45:26 +03:00
Danilo Leal
b64919aa11 ssh: Refine the modal UI (#19256)
This PR refines the SSH modal UI, adjusting spacing and alignment. Via
these changes, I'm also introducing the ability for the `empty_message`
on the `List` component to receive not just a string but any element.
The custom way in which the SSH modal was designed made it feel like
this was needed for proper spacing.

<img width="700" alt="Screenshot 2024-10-16 at 1 20 54 AM"
src="https://github.com/user-attachments/assets/f2e0586b-4c9f-4497-b4cb-e90c8157512b">


Release Notes:

- N/A
2024-10-15 20:39:27 -03:00
Marshall Bowers
b752548742 storybook: Load GPUI with default features (#19253)
This PR makes it so the Storybook loads GPUI with the default features
enabled.

This fixes a panic that would occur when trying to run any of the
stories.

Release Notes:

- N/A
2024-10-15 17:55:58 -04:00
Peter Tripp
d63a49647f Glob documentation (#18789)
Leaving this unlinked for now because I'm not sure where it belongs.
Plan to write up something for regexes too.
2024-10-15 17:21:04 -04:00
Peter Tripp
c00f2d8842 Add Diff language (#19129) 2024-10-15 16:02:12 -04:00
Joseph T. Lyons
973143fa35 Fix variable name typo (#19244)
Release Notes:

- N/A
2024-10-15 16:01:50 -04:00
Joseph T. Lyons
d806df9f16 Ensure issues without core labels have triage labels (#19243)
Sometimes, issues are created outside of issue templates (which we don't
prefer, but we can't prevent). This updates our top-ranking issues
script such that it will add `triage` and `admin read` labels to any
issue that is missing a core label, so that we don't miss the issues
when doing the next triage.

Release Notes:

- N/A
2024-10-15 15:55:18 -04:00
Peter Tripp
b682fc6d1d Use multi-line regex for '\s' (#19241) 2024-10-15 15:47:43 -04:00
Peter Tripp
5445f898e8 ruby: Move Ruby extension to zed-extensions/ruby repo (#19098) 2024-10-15 15:41:20 -04:00
Peter Schilling
56163b1e35 Add ability to reload a file (#18395)
Closes #13212

Release Notes:

- Added reload command
- vim: Added `:e[dit]`, `:e[dit]!` which calls reload
2024-10-15 13:02:18 -06:00
CharlesChen0823
695176898e project_search: Fix message displayed when no results are found (#19108)
when no result found, always display `Search all files`, which is
confused.

Release Notes:

- Fixed an issue where the project search would sometimes show "Search
all files" when there were no results.

---------

Co-authored-by: Marshall Bowers <elliott.codes@gmail.com>
2024-10-15 13:41:51 -04:00
Thorsten Ball
e3c6ba4bd7 ssh remote: Revert #19193 and treat killed proxy as non-zero (#19234)
This does two things.

Important one: it reverts #19193, which lead to our whole process
handling breaking. When the `proxy` process was killed, it apparently
didn't close the stdout/stderr anymore, which meant we would not detect
when it died. (Watching its `status()` in the io loop also didn't work!)

We should figure out how to keep our process handling working before we
make this change in #19193, which sounds reasonable.

Second, less important thing: I think we should treat the process being
killed from a signal as non-zero, as an error.

Release Notes:

- N/A
2024-10-15 19:20:06 +02:00
thataboy
8924b3fb5b Fix block cursor obscuring placeholder text and editor text in some cases (#18114) 2024-10-15 13:10:01 -04:00
Joseph T. Lyons
1732441f48 Use python 3.13 (#19225)
Release Notes:

- N/A
2024-10-15 11:08:02 -04:00
Joseph T. Lyons
096d37a1ed Remove outdated requirements.txt (#19223)
Release Notes:

- N/A
2024-10-15 11:04:54 -04:00
cabrinha
ba69f48ccf docs: Add Helm extension docs (#19095)
Merge after this:
- https://github.com/zed-industries/extensions/pull/746

Release Notes:

- N/A

---------

Co-authored-by: Peter Tripp <peter@zed.dev>
Co-authored-by: Marshall Bowers <elliott.codes@gmail.com>
2024-10-15 10:40:47 -04:00
Bennet Bo Fenner
0eb6bfd323 ssh remoting: Treat other message as heartbeat (#19219)
This improves the heartbeat detection logic. We now treat any other
incoming message from the ssh remote server
as a heartbeat message, meaning that we can detect re-connects earlier.

It also changes the connection handling to await futures detached.

Co-Authored-by: Thorsten <thorsten@zed.dev>

Release Notes:

- N/A

---------

Co-authored-by: Thorsten <thorsten@zed.dev>
Co-authored-by: Antonio <antonio@zed.dev>
2024-10-15 16:37:56 +02:00
Piotr Osiewicz
4fa75a78b9 gpui: Improve performance of laying out long lines (#19215)
TL;DR: Another O(n^2) strikes.

In #19194 we received a report about a 7Mb JSON file that Zed struggles
with. Naturally this file showcased a O(n^2) in line layout; this file
has one long line.

During line layout for Mac we have to convert between UTF-16 and UTF-8
indices in the string, as CoreText works with UTF-16 and Rust strings
are UTF-8. The problem stemmed from the fact that we were re-seeking our
string converter on each glyph, which boils down to: we were reparsing
[0..curr_string_position] bytes up to full length of the string, which
is the O(n^2) in question. This PR changes this behaviour to reuse the
Index Converter if the position we're seeking to is not yet reached.
Basically, we're treating the converter as forward iterator and we try
to seek with the same iterator, if possible.

Where previously you could not even open the file in OP (within
reasonable time frame, I waited for 40 seconds before giving up), now
you can do it in.. slightly over a second. The best part is: the
experience is still not ideal. Typing in the buffer is sluggish. Still,
this is a start.


Release Notes:

- Mac: Improved performance with very long lines
2024-10-15 16:28:47 +02:00
Thorsten Ball
397e4bee0a ssh remoting: Forward LSP logs to client (#19212)
Release Notes:

- N/A

---------

Co-authored-by: Bennet Bo Fenner <bennet@zed.dev>
2024-10-15 16:04:29 +02:00
Piotr Osiewicz
db7417f3b5 Rework file picker for SSH modal (#19020)
This PR changes the SSH modal design so its more keyboard
navigation-friendly and adds the server nickname feature.

Release Notes:

- N/A

---------

Co-authored-by: Danilo <danilo@zed.dev>
Co-authored-by: Danilo Leal <67129314+danilo-leal@users.noreply.github.com>
2024-10-15 12:38:03 +02:00
Thorsten Ball
be7b24fcf7 ssh remote: Shutdown SSH & server process correctly on app quit (#19210)
Release Notes:

- N/A

Co-authored-by: Bennet <bennet@zed.dev>
2024-10-15 11:26:23 +02:00
CharlesChen0823
62de03f286 tab: Fix copy wrong relative path for tab (#19206)
Closes #19204 

Release Notes:

- Fixed relative paths copied incorrectly from tabs
([#19204](https://github.com/zed-industries/zed/issues/19204))
2024-10-15 09:27:34 +03:00
wannacu
41ba4178fc gpui: Fix crash caused by ownership leak (#19185)
- Closes #18811

Release Notes:

- N/A
2024-10-14 12:46:04 -07:00
Antonio Scandurra
6e2869a321 Prevent deadlock when create a new meter/price on Stripe (#19196)
This also puts the entire state of `StripeBilling` behind a `RwLock`.
When fetching the existing prices and meters, or when inserting new
ones, we acquire a write lock and hold it until the Stripe request
completes. This prevents two concurrent calls to `get_or_insert_price`
from inserting the same data twice.

Creating a new meter/price is unusual, so in practice we'll acquire a
read lock most of the time.

/cc @rtfeldman @maxdeviant 

Release Notes:

- N/A
2024-10-14 17:31:51 +02:00
张小白
6986f081d0 supermaven: Fix crash when editing non-ASCII text (#19153)
Closes #19051
Closes #19182


#### How to reproduce this crash:
1. Open any file and input some ASCII characters.
2. Replace these characters with `你好`.
3. Press `backspace`.
4. Crash.



https://github.com/user-attachments/assets/ea5c5340-29a5-42c8-98c5-6e60770445a4



The issue lies with the `prefix_offset` introduced in #18858. After the
buffer is modified, this value is not always valid and may fall within a
`char boundary`, which results in a crash.



Release Notes:

- Fixed Supermaven crashing on deleting non-ASCII text
2024-10-14 18:27:15 +03:00
张小白
3ff52a816e windows: Fix opening wrong path when clicking path in the terminal view (#18726)
Closes #18550

This PR removes the prefix `\\?\`.

https://github.com/user-attachments/assets/f4f4333c-5d87-4f0f-b12c-fb2108703b6a


Release Notes:

- N/A
2024-10-14 18:23:16 +03:00
Shish
7d5fe66b54 remote: Disable ControlPersist for master ssh connection (#19193)
remote: Disable ControlPersist for master ssh connection

`ControlPersist=yes` combined with `ControlMaster=yes` silently forces
`ForkAfterAuthentication=yes` (even when the user has explicitly set it
to `no` - reported upstream in [0]) - and the latter makes the ssh
subprocess disappear, which makes us think that the connection died

(This is only an issue for people who have `ControlPersist=yes` in their
`ssh_config`, and perhaps the answer is "if that option breaks things,
don't use that option?" - but it's an option that makes sense _most_ of
the time, it's just in this edge-case of "creating an ssh connection
with -N and expecting the process to stay in the foreground" where it
_must_ be set to no)

I think the alternative approach is to tell people "if you want to use
persistent connections, have a separate ~/.ssh/config entry for
servername (to ssh into) and servername-no-persist (to zed into)", which
is possible, but ugh. Kind of a messy situation >.<


Tests:

- Before: Connections to my server result in "Failed to connect: ." (The
error message is attempting to show stderr, but stderr is empty)

- After: Connections to my server work reliably


[0] https://bugzilla.mindrot.org/show_bug.cgi?id=3743


Release Notes:

- N/A
2024-10-14 15:36:31 +02:00
Piotr Osiewicz
792f583b97 Revert "chore: Bump taffy to 0.5.2 (#18729)" (#19189)
This reverts commit a99750fd35.

@huacnlee found that commit to have a bad impact on perf and triaged it
for us in
https://github.com/zed-industries/zed/pull/18729#issuecomment-2410445980
Closes #ISSUE

Release Notes:

- N/A
2024-10-14 15:19:10 +02:00
Thorsten Ball
6ec00cdb06 ssh remoting: Restore SSH projects when reopening Zed (#19188)
Release Notes:

- N/A

---------

Co-authored-by: Bennet <bennet@zed.dev>
2024-10-14 14:56:25 +02:00
Thorsten Ball
71a878aa39 remote ssh: Fix asset embedding in cross-compilation (#19180)
This fixes the panic from the settings file not being embedded.


Release Notes:

- N/A

---------

Co-authored-by: Bennet <bennet@zed.dev>
2024-10-14 14:13:06 +02:00
Antonio Scandurra
f2337bbed1 Redirect to checkout page when payment is required (#19179)
Previously, we were redirecting to a non-existant page.

Release Notes:

- N/A
2024-10-14 12:39:20 +02:00
Lilith Iris
fcf9e546da project: Fix content not displaying when selecting a folder in Windows (#18946)
- Closes #16998

This PR resolves issues with the /file and /diagnostics commands in the
assistant panel, which previously failed to display the contents of a
directory when searching for a folder instead of using the arrow button.

- Changed the format in `project.rs` (located at
`crates/project/src/project.rs`) to use `std::path::MAIN_SEPARATOR` for
cross-platform compatibility, which resolves errors encountered on
Windows that originally used the format `format!("{}/", ...)`.


Release Notes:

- N/A
2024-10-14 12:13:53 +03:00
Xavier Lau
7dc069100d Improve macOS build guide (#19172)
- `mold` moved to `sold` long time ago.
  And https://github.com/bluewhalesystems/sold/issues/43...
- And add a step for accepting xcodebuild license

Signed-off-by: Xavier Lau <x@acg.box>
2024-10-14 10:20:36 +02:00
Frank Sheiness
5b207ba238 vim: Add some "z" keybindings for scrolling (#18928)
Release Notes:

- vim: Added a few "z" keybindings for scrolling
2024-10-14 10:00:37 +02:00
Ömer Sinan Ağacan
325f106c8b Add vim::Search command option for non-regex search (#19177)
Similar to e2647025ac, this adds a `regex`
option to `vim::Search` command to allow disabling regex search.

Release Notes:

- Added `regex` option to `vim::Search` command to allow disabling regex
search by default in the keymap. Example usage:
  ```yaml
  {
    "context": "VimControl && !menu",
    "bindings": {
      "/": ["vim::Search", { "regex": false }],
    }
  }
  ```
2024-10-14 09:59:29 +02:00
Kirill Bulatov
ec5d6e96bb Make danger to output less false-positives (#19151) 2024-10-14 01:50:46 +03:00
Bolaji Olajide
54683ff2b9 docs: Fix typo in environment documentation (#19164)
Update incorrect spelling of Raycast in environment.md
2024-10-13 16:47:09 -04:00
Peter Tripp
cdead5760a docs: Formatter arguments, document {buffer_path} usage (#19156) 2024-10-13 10:25:08 -04:00
Kirill Bulatov
39468de8c6 Return back to history-based tabs activation on close (#19150)
Closes https://github.com/zed-industries/zed/issues/19036

Alters https://github.com/zed-industries/zed/pull/18168 and moves its
change behind a settings flag, restoring the previous behavior.

Release Notes:

- Fixed tab closing not respecting history. Use `tabs.activate_on_close
= neighbour` settings to activate near tabs instead.
2024-10-13 14:35:06 +03:00
Kirill Bulatov
6491148196 Fail on warnings during CI builds (#19149)
Forbid things like
https://github.com/zed-industries/zed/pull/19144#issuecomment-2408871788

Release Notes:

- N/A
2024-10-13 13:39:15 +03:00
Peter Tripp
0b10fd5098 cpp: Better icon support (#19146) 2024-10-13 04:09:27 -04:00
Shish
74cc90887a ci: Give names to all github actions (#19080) 2024-10-13 03:01:56 -04:00
Peter Tripp
875c0cb09f Bytes 1.7.2 merge fix (#19145) 2024-10-13 02:56:12 -04:00
Peter Tripp
aefc559f43 Improve auto-detection via shebang of TypeScript, JavaScript and Shell Script (#19114) 2024-10-13 02:35:46 -04:00
Mikayla Maki
bebe24ea77 Add remote server cross compilation (#19136)
This will allow us to compile debug builds of the remote-server for a
different architecture than the one we are developing on.

This also adds a CI step for building our remote server with minimal
dependencies.

Release Notes:

- N/A
2024-10-12 23:23:56 -07:00
renovate[bot]
f73a076a63 Update Rust crate bytes to v1.7.2 (#18656)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [bytes](https://redirect.github.com/tokio-rs/bytes) |
workspace.dependencies | patch | `1.7.1` -> `1.7.2` |

---

### Release Notes

<details>
<summary>tokio-rs/bytes (bytes)</summary>

###
[`v1.7.2`](https://redirect.github.com/tokio-rs/bytes/blob/HEAD/CHANGELOG.md#172-September-17-2024)

[Compare
Source](https://redirect.github.com/tokio-rs/bytes/compare/v1.7.1...v1.7.2)

##### Fixed

- Fix default impl of `Buf::{get_int, get_int_le}`
([#&#8203;732](https://redirect.github.com/tokio-rs/bytes/issues/732))

##### Documented

- Fix double spaces in comments and doc comments
([#&#8203;731](https://redirect.github.com/tokio-rs/bytes/issues/731))

##### Internal changes

- Ensure BytesMut::advance reduces capacity
([#&#8203;728](https://redirect.github.com/tokio-rs/bytes/issues/728))

</details>

---

### Configuration

📅 **Schedule**: Branch creation - "after 3pm on Wednesday" in timezone
America/New_York, Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

Release Notes:

- N/A

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzOC45Ny4wIiwidXBkYXRlZEluVmVyIjoiMzguMTE1LjEiLCJ0YXJnZXRCcmFuY2giOiJtYWluIiwibGFiZWxzIjpbXX0=-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-12 14:54:06 -07:00
Mikayla Maki
b2e844f2ec Fix an issue with using non-reusable body types with redirects (#19134)
Closes #19131
Closes #19039

fixes the broken auto-updater.

I had the bright idea of using streams as the most common unit of data
transfer. Unfortunately, streams are not re-usable. So HTTP redirects
that have a stream body (like our remote server and auto update
downloads), don't redirect, as they can't reuse the stream. This PR
fixes the problem and simplifies the AsyncBody implementation now that
we're not using Isahc.

Release Notes:

- N/A
2024-10-12 13:32:08 -07:00
Lorenzo Cinque
9e14fd915f docs: Fix missing parenthesis in the Terminal: Detect Virtual Environments section of configuring-zed.md (#19127)
Release Notes:

- N/A
2024-10-12 20:18:33 +03:00
Mikayla Maki
c85a3cc117 Switch from OpenSSL to Rustls (#19104)
This PR also includes a downgrade of our async_tungstenite version to
0.24

Release Notes:

- N/A
2024-10-11 18:18:09 -07:00
Mikayla Maki
22ac178f9d Restore HTTP client transition, but use reqwest everywhere (#19055)
Release Notes:

- N/A
2024-10-11 14:58:58 -07:00
Marshall Bowers
c709b66f35 collab: Don't record billing events if billing is not enabled (#19102)
This PR adjusts the billing logic to not write any records to
`billing_events` if:

- The user is staff, as we don't want to bill staff members
- Billing is disabled (we currently enable billing based on the presence
of the Stripe API key)

Release Notes:

- N/A
2024-10-11 17:54:10 -04:00
Peter Tripp
b739cfa73f docs: Link environment.md (#19101) 2024-10-11 17:24:56 -04:00
Peter Tripp
0fc3072362 Document extension extraction process (#19085)
Release Notes:

- N/A
2024-10-11 16:33:40 -04:00
Peter Tripp
3cbaa08d89 Fix script/linux for Linux Mint (#19096)
- Closes: https://github.com/zed-industries/zed/issues/18827

Release Notes:

- N/A
2024-10-11 16:29:35 -04:00
Richard Feldman
12c9f0f723 Test some billing events logic (#19094)
Release Notes:

- N/A

Co-authored-by: Marshall <marshall@zed.dev>
2024-10-11 16:21:21 -04:00
Marshall Bowers
f280b29859 collab: Make the StripeBilling object long-lived (#19090)
This PR makes the `StripeBilling` object long-lived so that we can make
better use of the cached data on it.

We now hold it on the `AppState` and spawn a background task to
initialize the cache on startup.

Release Notes:

- N/A

Co-authored-by: Richard <richard@zed.dev>
2024-10-11 15:15:08 -04:00
Kirill Bulatov
550064f80f Fix ~ expansion in ssh projects' terminals (#19078)
When setting a remote ssh project path starting with ~, Zed would fail
to cd into such project's directory when opening a new terminal.

Release Notes:

- N/A

---------

Co-authored-by: Thorsten Ball <mrnugget@gmail.com>
2024-10-11 21:53:37 +03:00
Marshall Bowers
f33b8abc72 collab: Sort LLM database ID types (#19083)
This PR sorts the order of the LLM database ID type declarations.

Release Notes:

- N/A
2024-10-11 13:57:48 -04:00
Marshall Bowers
22ea7cef7a collab: Add usage-based billing for LLM interactions (#19081)
This PR adds usage-based billing for LLM interactions in the Assistant.

Release Notes:

- N/A

---------

Co-authored-by: Antonio Scandurra <me@as-cii.com>
Co-authored-by: Antonio <antonio@zed.dev>
Co-authored-by: Richard <richard@zed.dev>
Co-authored-by: Richard Feldman <oss@rtfeldman.com>
2024-10-11 13:36:54 -04:00
Shish
f1c45d988e collab: Remove dependency on X11 (#19079)
collab: Remove dependency on X11

I'm not sure if this is the best solution (perhaps pulling
`LanguageName` into a separate `language_types` crate would be
better...?) - but it massively reduces build time / dependencies / size
and means that the collab server no longer requires X11 libraries to be
installed.

tl;dr: `telemetry_events` requires the `language` crate, and the
language crate requires a whole ton of extra stuff. Since
telemetry_events only uses `language` for a single type definition
(`LanguageName`, aka `String`), we can cut all of these out by using the
base `String` type (This doesn't seem too terrible, given that all other
telemetry fields are using basic datatypes like String as opposed to
more strongly-typed variants).


FYI the dependency tree for "why does collab need X11 libraries??" looks
like this:

```
collab
 \- telemetry_events
     \- language
         |- gpui
         |- fuzzy
         |   \- gpui
         |- git
         |   \- gpui
         |- lsp
         |   |- gpui
         |   \- release_channel
         |       \- gpui
         |- settings
         |   |- fs
         |   |   \- gpui
         |   \- gpui
         |- task
         |   \- gpui
         \- theme
             \- gpui
```

Release Notes:

- N/A
2024-10-11 13:28:34 -04:00
Marshall Bowers
84b61c8b1a assistant: Add support for displaying billing-related errors (#19082)
This PR adds support to the assistant for display billing-related
errors.

Pulling this out of #19081 to make it easier to cherry-pick.

Release Notes:

- N/A

Co-authored-by: Antonio <antonio@zed.dev>
Co-authored-by: Richard <richard@zed.dev>
2024-10-11 13:22:45 -04:00
Shish
5cf0217549 terminal: Improve default locale handling (#18967)
terminal: Improve default locale handling

* Use `LANG` instead of `LC_ALL` (`LC_ALL` is the highest priority which
will override any other end-user settings; when that isn't set things
fall back to separate `LC_*` variables; and when those aren't set things
fall back to `LANG`). [0]
* Only set `LANG` for our child if necessary (if it already exists in
the parent, then the child will inherit that, no need for us to do
anything)

[0]
https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap08.html#tag_08_02

Tested cases:

- `unset LANG ; cargo run`: locale inside zed's terminal is set to
`en_US.UTF-8`
- `export LANG=en_GB.UTF-8 ; cargo run`: locale inside zed's terminal is
set to `en_GB.UTF-8`

Release Notes:

- Use the system locale in the terminal instead of forcing `en_US.UTF-8`
2024-10-11 18:09:24 +02:00
Thorsten Ball
c21f26c419 ssh remote: Stream stderr from server via proxy to client (#19073)
Release Notes:

- N/A

---------

Co-authored-by: Bennet <bennet@zed.dev>
2024-10-11 17:11:20 +02:00
Marshall Bowers
d976c5f1b6 gleam: Extract to external repository (#19072)
This PR transfers the Gleam extension over to the @gleam-lang
organization:

https://github.com/gleam-lang/zed-gleam

Release Notes:

- N/A
2024-10-11 10:05:46 -04:00
Kirill Bulatov
79ed217e42 Properly compute depth and path for project panel entries (#19068)
Closes https://github.com/zed-industries/zed/issues/18939

This fixes incorrect width estimates and horizontal scrollbar glitches

Release Notes:

- Fixes horizontal scrollbar not scrolling enough for certain paths
([#18939](https://github.com/zed-industries/zed/issues/18939))

Co-authored-by: Piotr Osiewicz <piotr@zed.dev>
2024-10-11 15:38:12 +03:00
Bennet Bo Fenner
0a7468c89f lsp: Show error message in read only buffer (#19063)
Clicking on

<img width="361" alt="image"
src="https://github.com/user-attachments/assets/b55e2575-b438-4c26-922f-313dc1f41fea">

now opens a read only buffer

<img width="547" alt="image"
src="https://github.com/user-attachments/assets/af82e104-1603-4fe4-9351-635a02cfb4f9">

Previously the buffer would show up as a normal untitled buffer and
would open a prompt when closing the tab.

Co-Authored-by: Thorsten <thorsten@zed.dev>

Release Notes:

- N/A

Co-authored-by: Thorsten <thorsten@zed.dev>
2024-10-11 12:33:08 +02:00
Tim Havlicek
518f8cc5b7 fix: Absolutize path to worktree root in worktree.read_text_file (#19064)
Closes #19050

Release Notes:

- Fixed `worktree.read_text_file` plugin API working incorrectly
([#19050](https://github.com/zed-industries/zed/issues/19050))
2024-10-11 13:26:37 +03:00
Kirill Bulatov
ccaf3268f8 Check paths for FS existence before parsing them as paths with line numbers (#19057)
Closes https://github.com/zed-industries/zed/issues/18268

Release Notes:

- Fixed Zed not being open filenames with special combination of
brackets ([#18268](https://github.com/zed-industries/zed/issues/18268))
2024-10-11 12:58:49 +03:00
Thorsten Ball
1691652948 ssh: Fix abs paths in file history & repeated go-to-def (#19027)
This fixes two things:

- Go-to-def to absolute paths (i.e. opening stdlib files) multiple times
(opening, dropping, and re-opening worktrees)
- Re-opening abs paths from the file picker history that were added
there by go-to-def


Release Notes:

- N/A

---------

Co-authored-by: Bennet <bennet@zed.dev>
2024-10-11 11:21:34 +02:00
Peter Tripp
4726f30bd6 Standardize on CursorShape::Underline not Underscore (#19028)
Currently terminal.cursor_shape uses `underline` and `cursor_shape` uses
`underscore`.
This standardizes them so they use the same settings value.

I think `underline` is the more common term and it matches the
terminology used by VSCode, Alacritty, iTerm, etc.

Note the protobuf enum `CursorShape::CursorUnderscore` remains
unchanged.

See also:
- https://github.com/zed-industries/zed/pull/18530
- https://github.com/zed-industries/zed/pull/17572

Release Notes:

- Settings: rename one `cursor_shape` from `underscore` to `underline`
(breaking change).
2024-10-11 10:44:21 +02:00
Thorsten Ball
36b9e40085 vim: Reset search options whenever / is used (#19058)
This is a bit of a personal thing, but it's been bugging me for a while
now that the search options are sticky whenever I use `/` in Vim mode.

This change makes it so that the options are reset with each new `/`.

That means you can, for example, use `v` to create a visual selection,
then hit `*` to search for that (which activates a bunch of search
options), but then continue with `/` to get a normal search.

Release Notes:

- Changed `/` in Vim mode to always reset the search options in the
search bar back to regex-only. That means using `*` (in normal or visual
mode) still works with its options, but the next `/` will reset the
search options. That makes it much closer to how `/` behaves in Vim.
2024-10-11 10:06:33 +02:00
Kirill Bulatov
e962839d13 Replace rpc with proto dependency for the headless server crate (#19048)
Release Notes:

- N/A
2024-10-11 01:36:40 +03:00
Henry Chu
eea600ecc3 Fix macOS App shortcut (#18921)
- The App Shortcuts in macOS System Settings does not work for Zed since the menu items titles were not set.
- Previously you could set a shortcut for `Zoom`.
- This add support for `Window->Zoom` as well.
2024-10-10 13:08:46 -04:00
Peter Tripp
3c6989323f docs: Add XML (#19026)
Co-authored-by: Marshall Bowers <elliott.codes@gmail.com>
2024-10-10 12:56:39 -04:00
renovate[bot]
596d8b2fe3 Update Rust crate wasmtime to v24.0.1 [SECURITY] (#18944)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [wasmtime](https://redirect.github.com/bytecodealliance/wasmtime) |
workspace.dependencies | patch | `24.0.0` -> `24.0.1` |

### GitHub Vulnerability Alerts

####
[CVE-2024-47763](https://redirect.github.com/bytecodealliance/wasmtime/security/advisories/GHSA-q8hx-mm92-4wvg)

### Impact

Wasmtime's implementation of WebAssembly tail calls combined with stack
traces can result in a runtime crash in certain WebAssembly modules. The
runtime crash may be undefined behavior if Wasmtime was compiled with
Rust 1.80 or prior. The runtime crash is a deterministic process abort
when Wasmtime is compiled with Rust 1.81 and later.

[WebAssembly tail
calls](https://redirect.github.com/webassembly/tail-call) are a proposal
which relatively recently reached stage 4 in the [standardization
process](https://redirect.github.com/WebAssembly/proposals/). Wasmtime
first enabled support for tail calls by default [in Wasmtime
21.0.0](https://redirect.github.com/bytecodealliance/wasmtime/pull/8540),
although that release contained a bug where it was only on-by-default
for some configurations. In [Wasmtime
22.0.0](https://redirect.github.com/bytecodealliance/wasmtime/pull/8682)
tail calls were enabled by default for all configurations.

The specific crash happens when an exported function in a WebAssembly
module (or component) performs a `return_call` (or
`return_call_indirect` or `return_call_ref`) to an imported host
function which captures a stack trace (for example, the host function
raises a trap). In this situation, the stack-walking code previously
assumed there was always at least one WebAssembly frame on the stack but
with tail calls that is no longer true. With the tail-call proposal it's
possible to have an entry trampoline appear as if it directly called the
exit trampoline. This situation triggers an internal assert in the
stack-walking code which raises a Rust `panic!()`.

When Wasmtime is compiled with Rust versions 1.80 and prior this means
that an `extern "C"` function in Rust is raising a `panic!()`. This is
technically undefined behavior and typically manifests as a process
abort when the unwinder fails to unwind Cranelift-generated frames. When
Wasmtime is compiled with Rust versions 1.81 and later this panic
becomes a deterministic process abort.

Overall the impact of this issue is that this is a denial-of-service
vector where a malicious WebAssembly module or component can cause the
host to crash. There is no other impact at this time other than
availability of a service as the result of the crash is always a crash
and no more.

This issue was discovered by routine fuzzing performed by the Wasmtime
project via Google's OSS-Fuzz infrastructure. We have no evidence that
it has ever been exploited by an attacker in the wild.

### Patches

All versions of Wasmtime which have tail calls enabled by default have
been patched:

* 21.0.x - patched in 21.0.2
* 22.0.x - patched in 22.0.1
* 23.0.x - patched in 23.0.3 
* 24.0.x - patched in 24.0.1
* 25.0.x - patched in 25.0.2

Wasmtime versions from 12.0.x (the first release with experimental tail
call support) to 20.0.x (the last release with tail-calls
off-by-default) have support for tail calls but the support is disabled
by default. These versions are not affected in their default
configurations, but users who explicitly enabled tail call support will
need to either disable tail call support or upgrade to a patched version
of Wasmtime.

### Workarounds

The main workaround for this issue is to disable tail support for tail
calls in Wasmtime, for example with
[`Config::wasm_tail_call(false)`](https://docs.rs/wasmtime/latest/wasmtime/struct.Config.html#method.wasm_tail_call).
Users are otherwise encouraged to upgrade to patched versions.

### References

* [Wasmtime's initial implementation of tail
calls](https://redirect.github.com/bytecodealliance/wasmtime/pull/6774)
* [Enabling of tail calls in
21.0.0](https://redirect.github.com/bytecodealliance/wasmtime/pull/8540)
* [Fully enabling tail calls in
22.0.0](https://redirect.github.com/bytecodealliance/wasmtime/pull/8682)
* [The WebAssembly's `tail-call`
proposal](https://redirect.github.com/webassembly/tail-call)

####
[CVE-2024-47813](https://redirect.github.com/bytecodealliance/wasmtime/security/advisories/GHSA-7qmx-3fpx-r45m)

### Impact

Under certain concurrent event orderings, a `wasmtime::Engine`'s
internal type registry was susceptible to double-unregistration bugs due
to a race condition, leading to panics and potentially type registry
corruption. That registry corruption could, following an additional and
particular sequence of concurrent events, lead to violations of
WebAssembly's control-flow integrity (CFI) and type safety. Users that
do not use `wasmtime::Engine` across multiple threads are not affected.
Users that only create new modules across threads over time are
additionally not affected.

Reproducing this bug requires creating and dropping multiple type
instances (such as `wasmtime::FuncType` or `wasmtime::ArrayType`)
concurrently on multiple threads, where all types are associated with
the same `wasmtime::Engine`. **Wasm guests cannot trigger this bug.**
See the "References" section below for a list of Wasmtime types-related
APIs that are affected.

Wasmtime maintains an internal registry of types within a
`wasmtime::Engine` and an engine is shareable across threads. Types can
be created and referenced through creation of a `wasmtime::Module`,
creation of `wasmtime::FuncType`, or a number of other APIs where the
host creates a function (see "References" below). Each of these cases
interacts with an engine to deduplicate type information and manage type
indices that are used to implement type checks in WebAssembly's
`call_indirect` function, for example. This bug is a race condition in
this management where the internal type registry could be corrupted to
trigger an assert or contain invalid state.

Wasmtime's internal representation of a type has individual types (e.g.
one-per-host-function) maintain a registration count of how many time
it's been used. Types additionally have state within an engine behind a
read-write lock such as lookup/deduplication information. The race here
is a time-of-check versus time-of-use (TOCTOU) bug where one thread
atomically decrements a type entry's registration count, observes zero
registrations, and then acquires a lock in order to unregister that
entry. However, between when this first thread observed the
zero-registration count and when it acquires that lock, another thread
could perform the following sequence of events: re-register another copy
of the type, which deduplicates to that same entry, resurrecting it and
incrementing its registration count; then drop the type and decrement
its registration count; observe that the registration count is now zero;
acquire the type registry lock; and finally unregister the type. Now,
when the original thread finally acquires the lock and unregisters the
entry, it is the second time this entry has been unregistered.

| Thread A                          | Thread B                       |
|-----------------------------------|--------------------------------|
| `acquire(type registry lock)`     |                                |
|                                   | `decref(E) --> 0`              |
|                                   | `block_on(type registry lock)` |
| `register(E') == incref(E) --> 1` |                                |
| `release(type registry lock)`     |                                |
| `decref(E) --> 0`                 |                                |
| `acquire(type registry lock)`     |                                |
| `unregister(E)`                   |                                |
| `release(type registry lock)`     |                                |
|                                   | `acquire(type registry lock)`  |
|                                   | `unregister(E)`          |

This double-unregistration could then lead to a WebAssembly CFI
violation under the following conditions: a new WebAssembly module `X`
was loaded into the engine before the second, buggy unregistration
occurs; `X` defined a function type `F` that was allocated in the same
type registry slot where the original entry was allocated; the second,
buggy unregistration incorrectly unregistered `F`; another new
WebAssembly module `Y` was loaded into the engine; `Y` defined a
function type `G`, different from `F`, but which is also allocated in
the same type registry slot; a `funcref` of type `G` is created, either
by the host or by Wasm; that `funcref` is passed to a WebAssembly
instance of module `X`; that instance performs a `call_indirect` to that
`funcref`; the `call_indirect`'s dynamic type check, which preserves
CFI, could incorrectly pass in this case, because `F` and `G` were
assigned the same type registry slot. This would, ultimately, allow
calling a function with too many, too few, or wrongly-typed arguments,
violating CFI and type safety.

We were not able to reproduce this CFI violation in a vanilla Wasmtime
build, although it remains theoretically possible. However, by modifying
Wasmtime's source code to make losing the races described above more
likely (by disabling certain assertions, inserting panic catches, and
adding retry loops in a few places if we did *not* lose the race) we
were able to incorrectly get a `funcref` to pass a type check that it
should have failed, which would allow the CFI violation.

### Patches

This bug was originally introduced in Wasmtime 19's development of the
WebAssembly GC proposal. This bug affects users who are not using the GC
proposal, however, and affects Wasmtime in its default configuration
even when the GC proposal is disabled. Wasmtime users using 19.0.0 and
after are all affected by this issue. We have released the following
Wasmtime versions, all of which have a fix for this bug:

* 21.0.2
* 22.0.1
* 23.0.3
* 24.0.1
* 25.0.2

### Workarounds

If your application creates and drops Wasmtime types on multiple threads
concurrently, there are no known workarounds. Users are encouraged to
upgrade to a patched release.

### References

The following APIs create or drop types, and therefore are affected by
this race condition if performed on multiple threads concurrently and
are all associated with the same `wasmtime::Engine`:

*
[`wasmtime::FuncType::new`](https://docs.rs/wasmtime/latest/wasmtime/struct.FuncType.html#method.new)
* Also reachable from creation of
[`wasmtime::Func`](https://docs.rs/wasmtime/latest/wasmtime/struct.Func.html)
* Also reachable from
[`wasmtime::Linker::func_*`](https://docs.rs/wasmtime/latest/wasmtime/struct.Linker.html#method.func_new)
*
[`wasmtime::ArrayType::new`](https://docs.rs/wasmtime/latest/wasmtime/struct.ArrayType.html#method.new)
*
[`wasmtime::StructType::new`](https://docs.rs/wasmtime/latest/wasmtime/struct.StructType.html#method.new)
*
[`wasmtime::Func::ty`](https://docs.rs/wasmtime/latest/wasmtime/struct.Func.html#method.ty)
*
[`wasmtime::Global::ty`](https://docs.rs/wasmtime/latest/wasmtime/struct.Global.html#method.ty)
*
[`wasmtime::Table::ty`](https://docs.rs/wasmtime/latest/wasmtime/struct.Table.html#method.ty)
*
[`wasmtime::Extern::ty`](https://docs.rs/wasmtime/latest/wasmtime/struct.Extern.html#method.ty)
*
[`wasmtime::Export::ty`](https://docs.rs/wasmtime/latest/wasmtime/struct.Export.html#method.ty)
*
[`wasmtime::UnknownImportError::ty`](https://docs.rs/wasmtime/latest/wasmtime/struct.UnknownImportError.html#method.ty)
*
[`wasmtime::ImportType::ty`](https://docs.rs/wasmtime/latest/wasmtime/struct.ImportType.html#method.ty)
*
[`wasmtime::ExportType::ty`](https://docs.rs/wasmtime/latest/wasmtime/struct.ExportType.html#method.ty)
*
[`wasmtime::Val::ty`](https://docs.rs/wasmtime/latest/wasmtime/struct.Val.html#method.ty)
*
[`wasmtime::Ref::ty`](https://docs.rs/wasmtime/latest/wasmtime/struct.Ref.html#method.ty)
*
[`wasmtime::AnyRef::ty`](https://docs.rs/wasmtime/latest/wasmtime/struct.AnyRef.html#method.ty)
*
[`wasmtime::EqRef::ty`](https://docs.rs/wasmtime/latest/wasmtime/struct.EqRef.html#method.ty)
*
[`wasmtime::ArrayRef::ty`](https://docs.rs/wasmtime/latest/wasmtime/struct.ArrayRef.html#method.ty)
*
[`wasmtime::StructRef::ty`](https://docs.rs/wasmtime/latest/wasmtime/struct.StructRef.html#method.ty)
* Dropping a
[`wasmtime::FuncType`](https://docs.rs/wasmtime/latest/wasmtime/struct.FuncType.html)
* Dropping a
[`wasmtime::ArrayType`](https://docs.rs/wasmtime/latest/wasmtime/struct.ArrayType.html)
* Dropping a
[`wasmtime::StructType`](https://docs.rs/wasmtime/latest/wasmtime/struct.StructType.html)
* Dropping a
[`wasmtime::ExternType`](https://docs.rs/wasmtime/latest/wasmtime/struct.ExternType.html)
* Dropping a
[`wasmtime::GlobalType`](https://docs.rs/wasmtime/latest/wasmtime/struct.GlobalType.html)
* Dropping a
[`wasmtime::TableType`](https://docs.rs/wasmtime/latest/wasmtime/struct.TableType.html)
* Dropping a
[`wasmtime::ValType`](https://docs.rs/wasmtime/latest/wasmtime/struct.ValType.html)
* Dropping a
[`wasmtime::RefType`](https://docs.rs/wasmtime/latest/wasmtime/struct.RefType.html)
* Dropping a
[`wasmtime::HeapType`](https://docs.rs/wasmtime/latest/wasmtime/struct.HeapType.html)
* Dropping a
[`wasmtime::UnknownImportError`](https://docs.rs/wasmtime/latest/wasmtime/struct.UnknownImportError.html)
* Dropping a
[`wasmtime::Linker`](https://docs.rs/wasmtime/latest/wasmtime/struct.Linker.html)

The change which introduced this bug was
[#&#8203;7969](https://redirect.github.com/bytecodealliance/wasmtime/pull/7969)

---

### Release Notes

<details>
<summary>bytecodealliance/wasmtime (wasmtime)</summary>

###
[`v24.0.1`](https://redirect.github.com/bytecodealliance/wasmtime/releases/tag/v24.0.1)

[Compare
Source](https://redirect.github.com/bytecodealliance/wasmtime/compare/v24.0.0...v24.0.1)

#### 24.0.1

Released 2024-10-09.

##### Fixed

- Fix a runtime crash when combining tail-calls with host imports that
capture a
    stack trace or trap.

[GHSA-q8hx-mm92-4wvg](https://redirect.github.com/bytecodealliance/wasmtime/security/advisories/GHSA-q8hx-mm92-4wvg)

- Fix a race condition could lead to WebAssembly control-flow integrity
and type
    safety violations.

[GHSA-7qmx-3fpx-r45m](https://redirect.github.com/bytecodealliance/wasmtime/security/advisories/GHSA-7qmx-3fpx-r45m)

</details>

---

### Configuration

📅 **Schedule**: Branch creation - "" in timezone America/New_York,
Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

Release Notes:

- N/A

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzOC4xMTQuMCIsInVwZGF0ZWRJblZlciI6IjM4LjExNC4wIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6W119-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-10 09:48:02 -04:00
Kirill Bulatov
9bc4e3b4ae Do not resolve more completion fields (#19021)
As Zed instantly shows completion items in the completion menu, and the
resolve will cause the details to appear, flickering.
We can safely resolve the `documentation`, `additionalTextEdits` and
`command` fields, the rest should be resolved eagerly for now.

Release Notes:

- Fixed completion menu rendering
2024-10-10 16:10:18 +03:00
Peter Schilling
972886c29e Automatically indent JSX (#18816)
indents jsx in a way that is [consistent with
html](https://github.com/zed-industries/zed/blob/main/extensions/html/languages/html/indents.scm)

before, no automatic indentation would apply and it would even dedent
you when you add a line above the cursor (shift-o in vim mode)


https://github.com/user-attachments/assets/470fbdb2-3e31-42c4-b535-bb26ae1706ab


after, it applies automatic indentation when you hit return


https://github.com/user-attachments/assets/e86c739d-370d-490d-8c6f-d0190e65f832



Closes #16127

Release Notes:

- Improved automatic indentation behavior in JSX
([#16127](https://github.com/zed-industries/zed/issues/16127))
2024-10-10 15:15:12 +03:00
renovate[bot]
d2b4fa20ef Update actions/upload-artifact digest to 604373d (#18941)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
|
[actions/upload-artifact](https://redirect.github.com/actions/upload-artifact)
| action | digest | `5076954` -> `604373d` |

---

### Configuration

📅 **Schedule**: Branch creation - "after 3pm on Wednesday" in timezone
America/New_York, Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

Release Notes:

- N/A

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzOC4xMTQuMCIsInVwZGF0ZWRJblZlciI6IjM4LjExNC4wIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6W119-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-10 14:50:52 +03:00
Kevin Wang
21c27cecba Track cursor offset before bias in Supermaven completion provider (#18858)
Track the cursor offset before biasing in the Supermaven completion
provider to better determine if the text should be suggested. The
underlying issue here is due to the way anchor biasing works, the
completion provider is not able to determine if a given suggestion's
cursor location no longer exists as it is always coalesced to a correct
location (specifically, the end of the line).

This change updates that logic so the offset is stored independently of
the buffer so it can be used to represent a location that may not exist
in the buffer anymore to represent locations that have been deleted.

The net effect is that suggestions can be backspaced much more cleanly
with Supermaven.


![image](https://github.com/user-attachments/assets/ff61aa09-54ea-4cad-b1ca-633a08bcdd96)


![image](https://github.com/user-attachments/assets/b49e2d6b-f1d3-41a1-9b75-c4bc3ac5f85b)

Release Notes:

- Improves https://github.com/zed-industries/zed/issues/17981 to prevent
suggesting completions based on out-of-date cursor locations.
2024-10-10 14:39:20 +03:00
renovate[bot]
4de05d18ed Update Rust crate ashpd to v0.9.2 (#18950)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [ashpd](https://redirect.github.com/bilelmoussaoui/ashpd) |
workspace.dependencies | patch | `0.9.1` -> `0.9.2` |

---

### Release Notes

<details>
<summary>bilelmoussaoui/ashpd (ashpd)</summary>

###
[`v0.9.2`](https://redirect.github.com/bilelmoussaoui/ashpd/releases/tag/0.9.2)

[Compare
Source](https://redirect.github.com/bilelmoussaoui/ashpd/compare/0.9.1...0.9.2)

#### What's Changed

- [desktop: Make trait SessionPortal
public](0d2dad594e)
- [lib: Add Pid
type](96b27e7069)
- [desktop/game_mode: Use i32 for
pid](336917a4ed)
- [desktop/device: Use Pid type for
pids](c05b3c17f8)
- [flatpak: Use Pid type for
pids](55a6ea0c9d)
- [is_sandboxed: Don't unwrap OnceCell
set](5d3cb41707)

</details>

---

### Configuration

📅 **Schedule**: Branch creation - "after 3pm on Wednesday" in timezone
America/New_York, Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

Release Notes:

- N/A

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzOC4xMTQuMCIsInVwZGF0ZWRJblZlciI6IjM4LjExNC4wIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6W119-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-10 14:15:59 +03:00
renovate[bot]
8c9a05b2a8 Update Rust crate proc-macro2 to v1.0.87 (#18957)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [proc-macro2](https://redirect.github.com/dtolnay/proc-macro2) |
dependencies | patch | `1.0.86` -> `1.0.87` |

---

### Release Notes

<details>
<summary>dtolnay/proc-macro2 (proc-macro2)</summary>

###
[`v1.0.87`](https://redirect.github.com/dtolnay/proc-macro2/releases/tag/1.0.87)

[Compare
Source](https://redirect.github.com/dtolnay/proc-macro2/compare/1.0.86...1.0.87)

- Check valid punctuation character in `Punct::new`
([#&#8203;470](https://redirect.github.com/dtolnay/proc-macro2/issues/470))

</details>

---

### Configuration

📅 **Schedule**: Branch creation - "after 3pm on Wednesday" in timezone
America/New_York, Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

Release Notes:

- N/A

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzOC4xMTQuMCIsInVwZGF0ZWRJblZlciI6IjM4LjExNC4wIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6W119-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-10 14:11:32 +03:00
renovate[bot]
348e317695 Update Rust crate ipc-channel to v0.18.3 (#18663)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [ipc-channel](https://redirect.github.com/servo/ipc-channel) |
dependencies | patch | `0.18.2` -> `0.18.3` |

---

### Configuration

📅 **Schedule**: Branch creation - "after 3pm on Wednesday" in timezone
America/New_York, Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

Release Notes:

- N/A

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzOC45Ny4wIiwidXBkYXRlZEluVmVyIjoiMzguOTcuMCIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOltdfQ==-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-10 14:10:56 +03:00
renovate[bot]
281c60f12d Update Rust crate async-compression to v0.4.13 (#18655)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
|
[async-compression](https://redirect.github.com/Nullus157/async-compression)
| workspace.dependencies | patch | `0.4.12` -> `0.4.13` |

---

### Release Notes

<details>
<summary>Nullus157/async-compression (async-compression)</summary>

###
[`v0.4.13`](https://redirect.github.com/Nullus157/async-compression/blob/HEAD/CHANGELOG.md#0413---2024-10-02)

[Compare
Source](https://redirect.github.com/Nullus157/async-compression/compare/v0.4.12...v0.4.13)

##### Feature

-   Update `brotli` dependency to to `7`.

</details>

---

### Configuration

📅 **Schedule**: Branch creation - "after 3pm on Wednesday" in timezone
America/New_York, Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

Release Notes:

- N/A

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzOC45Ny4wIiwidXBkYXRlZEluVmVyIjoiMzguOTcuMCIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOltdfQ==-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-10 14:10:45 +03:00
renovate[bot]
6859482020 Update Rust crate emojis to v0.6.4 (#18661)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [emojis](https://redirect.github.com/rossmacarthur/emojis) |
workspace.dependencies | patch | `0.6.3` -> `0.6.4` |

---

### Release Notes

<details>
<summary>rossmacarthur/emojis (emojis)</summary>

###
[`v0.6.4`](https://redirect.github.com/rossmacarthur/emojis/compare/0.6.3...0.6.4)

[Compare
Source](https://redirect.github.com/rossmacarthur/emojis/compare/0.6.3...0.6.4)

</details>

---

### Configuration

📅 **Schedule**: Branch creation - "after 3pm on Wednesday" in timezone
America/New_York, Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

Release Notes:

- N/A

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzOC45Ny4wIiwidXBkYXRlZEluVmVyIjoiMzguOTcuMCIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOltdfQ==-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-10 14:10:35 +03:00
张小白
7c306a5a0e gpui: Fix window display on Windows (#18705)
- Closes #18610


This PR addresses the same issue as PR #18578. After a full day of
research and testing, I believe I’ve found the best solution to resolve
this issue. With this PR, the window creation behavior on Windows
becomes more consistent with macOS:

- When `params.show` is `true`: The window is created and immediately
displayed.
- When `params.show` is `false`: The window is created but remains
hidden until the first call to `activate_window`.

As I mentioned in #18578, `winit` creates hidden windows by setting the
window's `exstyle` to `WS_EX_NOACTIVATE | WS_EX_TRANSPARENT |
WS_EX_LAYERED | WS_EX_TOOLWINDOW`, which is different from the method
used in this PR. Here, the window is created with normal parameters, but
we do not call `ShowWindow` so the window is not shown.

I'm not sure why `winit` doesn't use a smilliar approach like this PR to
create hidden windows. My guess is that `winit` is creating this hidden
window to function as a "DispatchWindow" — serving a purpose similar to
`WindowsPlatform` in `zed`. To ensure the window stays hidden even if
`ShowWindow` is called, they use the `exstyle` approach.

With the method used in this PR, my initial tests haven't revealed any
issues.



Release Notes:

- N/A
2024-10-10 14:09:50 +03:00
Thorsten Ball
b75532fad7 ssh remote: Handle disconnect on project and show overlay (#19014)
Demo:



https://github.com/user-attachments/assets/e5edf8f3-8c15-482e-a792-6eb619f83de4


Release Notes:

- N/A

---------

Co-authored-by: Bennet <bennet@zed.dev>
2024-10-10 12:59:09 +02:00
Shish
e3ff2ced79 [terminal] Consider "main.cs(20,5)" to be a single clickable word (#19004)
[terminal] Consider "main.cs(20,5)" to be a single clickable word

First, adding unit tests for the regexes because I'm not certain how
these regexes are _intended_ to work, and unit tests work nicely as
demonstrations of intended behaviour.

The comment string, and the regex itself, seem to imply that
"main.cs(20,5)" is supposed be a single "word" (for the purposes of
being clicked on)... but the regex doesn't actually work like that. This
PR makes it work :)

(I don't know _why_ "word with an optional `(\d+,\d+)` on the end"
doesn't match the full string, while "word with a required `(\d+,\d+)`
on the end" _does_ match the full string - aren't regexes supposed to
match as much as possible, so it should take the optional extra whenever
the extra exists? Either way, "word with a required (\d+,\d+), or word
by itself" has the correct behaviour, as demonstrated by the unit test)

Release Notes:

- N/A
2024-10-10 13:56:48 +03:00
Kirill Bulatov
5841ac406d Fix the completions being too slow (#19013)
Closes https://github.com/zed-industries/zed/issues/19005

Release Notes:

- Fixed completion items inserted with a delay
([#19005](https://github.com/zed-industries/zed/issues/19005))

---------

Co-authored-by: Antonio Scandurra <antonio@zed.dev>
2024-10-10 12:53:02 +03:00
Piotr Osiewicz
f6f5ad138d project panel: Make intermediate folded directories clickable (#18956)
- Closes: https://github.com/zed-industries/zed/issues/18770


Release Notes:

- Intermediate auto-folded project panel entries are now clickable.
2024-10-10 11:15:46 +02:00
Thorsten Ball
db50467bbc remote ssh: Show connection status in tooltip (#19006)
Release Notes:

- N/A

---------

Co-authored-by: Danilo Leal <67129314+danilo-leal@users.noreply.github.com>
2024-10-10 10:49:02 +02:00
Cody
fe1078ef68 Add basic vi motion support for terminal (#18715)
Closes #7417

Release Notes:

- Added basic support for Alacritty's [vi
mode](https://github.com/alacritty/alacritty/blob/master/docs/features.md#vi-mode)
to the built-in terminal (which is using Alacritty under the hood.) The
vi mode can be activated with `ctrl-shift-space` and then supports some
basic motions to navigate through the terminal's scrollback buffer.

## Details

Leverages existing selection functionality from mouse_drag and the
ViMotion API of alacritty to add basic vi motions in the terminal.
Please note, this is only basic functionality (move, select, and yank to
system clipboard) and not a fully functional vim environment (e.g.
search, configurable keybindings, and paste). I figured this would be an
interim solution to the long term, more fleshed out, solution proposed
by @mrnugget.

Ctrl+Shift+Space to enter Vi mode while in the terminal (Same default
binding in alacritty)
2024-10-10 07:50:12 +02:00
Max Brunsfeld
5cf4ac16d6 Don't disable auto-indent when typing in multi buffers (#18984)
Release Notes:

- Fixed a bug where auto-indent was not enabled while typing in
multi-buffers
2024-10-09 20:41:58 -07:00
Joseph T Lyons
05b2010db5 Use uv 2024-10-09 23:02:41 -04:00
Joseph T. Lyons
d8484c57e1 Use uv (#18997)
Release Notes:

- N/A
2024-10-09 22:53:01 -04:00
Joseph T Lyons
fcfd769b39 Delete close_unlabeled_issues.yml
I'm going to write something more robust using PyGitHub.
2024-10-09 22:27:10 -04:00
Joseph T Lyons
285fb51771 Move label data to a data file so multiple scripts can reference them 2024-10-09 22:11:04 -04:00
Joseph T. Lyons
ed484ecf5f Run action to close unlabeled issues every hour (#18995)
Release Notes:

- N/A
2024-10-09 21:39:30 -04:00
Joseph T. Lyons
ab34342664 Close unlabeled issues (#18992)
Release Notes:

- N/A
2024-10-09 21:35:20 -04:00
Max Brunsfeld
53cc82b132 Fix some issues with branch buffers (#18945)
* `Open Excerpts` command always opens the locations in the base buffer
* LSP features like document-highlights, go-to-def, and inlay hints work
correctly in branch buffers
* Other LSP features like completions, code actions, and rename are
disabled in branch buffers

Release Notes:

- N/A
2024-10-09 16:55:25 -07:00
Marshall Bowers
cae548a50d collab: Fix issues with syncing LLM usage to Stripe (#18970)
This PR fixes some issues with our previous approach to synching LLM
usage over to Stripe.

We now have a separate LLM access price in Stripe that is a marker price
to allow us to create the initial subscription with that as its
subscription item

We then dynamically set the LLM usage price during the reconciliation
sync based on the usage for the current month.

Release Notes:

- N/A

---------

Co-authored-by: Antonio <antonio@zed.dev>
Co-authored-by: Richard <richard@zed.dev>
2024-10-09 19:15:38 -04:00
Marshall Bowers
69711660ab collab: Make LLM billing fields required in LlmTokenClaims (#18959)
This PR makes the `has_llm_subscription` and
`max_monthly_spend_in_cents` fields in the `LlmTokenClaims` required.

This change will be safe to deploy in ~45 minutes.

Release Notes:

- N/A
2024-10-09 18:42:22 -04:00
renovate[bot]
b2e1572820 Update actions/checkout digest to eef6144 (#18940)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [actions/checkout](https://redirect.github.com/actions/checkout) |
action | digest | `692973e` -> `eef6144` |

---

### Configuration

📅 **Schedule**: Branch creation - "after 3pm on Wednesday" in timezone
America/New_York, Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

Release Notes:

- N/A

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzOC4xMTQuMCIsInVwZGF0ZWRJblZlciI6IjM4LjExNC4wIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6W119-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-09 18:24:10 -04:00
renovate[bot]
66ea96839a Update Rust crate clap to v4.5.20 (#18953)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [clap](https://redirect.github.com/clap-rs/clap) |
workspace.dependencies | patch | `4.5.19` -> `4.5.20` |

---

### Release Notes

<details>
<summary>clap-rs/clap (clap)</summary>

###
[`v4.5.20`](https://redirect.github.com/clap-rs/clap/blob/HEAD/CHANGELOG.md#4520---2024-10-08)

[Compare
Source](https://redirect.github.com/clap-rs/clap/compare/v4.5.19...v4.5.20)

##### Features

-   *(unstable)* Add `CommandExt`

</details>

---

### Configuration

📅 **Schedule**: Branch creation - "after 3pm on Wednesday" in timezone
America/New_York, Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

Release Notes:

- N/A

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzOC4xMTQuMCIsInVwZGF0ZWRJblZlciI6IjM4LjExNC4wIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6W119-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-09 17:49:21 -04:00
Marshall Bowers
3db789ed90 collab: Include max monthly spend preference in LLM token (#18955)
This PR updates the LLM token claims to include the maximum monthly
spend.

Release Notes:

- N/A
2024-10-09 17:39:34 -04:00
renovate[bot]
99a6a3d5e3 Update cloudflare/wrangler-action digest to 9681c29 (#18949)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
|
[cloudflare/wrangler-action](https://redirect.github.com/cloudflare/wrangler-action)
| action | digest | `168bc28` -> `9681c29` |

---

### Configuration

📅 **Schedule**: Branch creation - "after 3pm on Wednesday" in timezone
America/New_York, Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

Release Notes:

- N/A

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzOC4xMTQuMCIsInVwZGF0ZWRJblZlciI6IjM4LjExNC4wIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6W119-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-09 17:19:11 -04:00
Marshall Bowers
d316577fd5 collab: Add billing preferences for maximum LLM monthly spend (#18948)
This PR adds a new `billing_preferences` table.

Right now there is a single preference: the maximum monthly spend for
LLM usage.

Release Notes:

- N/A

---------

Co-authored-by: Richard <richard@zed.dev>
2024-10-09 16:29:07 -04:00
Kirill Bulatov
711180981b Tone down model summarization logs (#18943)
Release Notes:

- N/A
2024-10-09 22:39:54 +03:00
Kirill Bulatov
49c75eb062 Rework remote task synchronization (#18746)
Reworks the way tasks are stored, accessed and synchronized in the
`project`.
Now both collab and ssh remote projects use the same TaskStorage kind to
get the task context from the remote host, and worktree task templates
are synchronized along with other worktree settings.

Release Notes:

- Adds ssh support to tasks, improves collab-remote projects' tasks sync
2024-10-09 22:28:42 +03:00
Marshall Bowers
f1053ff525 collab: Clarify naming around free tier spending limits (#18936)
This PR renames the `MONTHLY_SPENDING_LIMIT` constant to
`FREE_TIER_MONTHLY_SPENDING_LIMIT` to clarify it.

This will help distinguish it from the user's specified limit on their
paid monthly spending.

Release Notes:

- N/A
2024-10-09 15:05:53 -04:00
Marshall Bowers
817a41c4dc collab: Add a Cents type (#18935)
This PR adds a new `Cents` type that can be used to represent a monetary
value in cents.

This cuts down on the primitive obsession we were using when dealing
with money in the billing code.

Release Notes:

- N/A
2024-10-09 14:22:32 -04:00
Peter Tripp
bc23d1e666 docs: Add gopls install instructions (#18919) 2024-10-09 14:05:35 -04:00
Thorsten Ball
bc4abd2b29 ssh session: Fix hang when doing state update in reconnect (#18934)
This snuck in last-minute.

Release Notes:

- Fixed a potential hang and panic when an SSH project goes through a
slow reconnect.
2024-10-09 19:40:09 +02:00
Ömer Sinan Ağacan
71f4ca67c2 Use WHOLE_WORD search option in vim mode's whole-word search (#18725)
Instead of wrapping the search term with `\<...\>`, enable the
`WHOLE_WORD` search option.

The advantage of the search option is that it can be toggled with one
click/key press (alt+w by default), and it doesn't require regex mode.

Release Notes:

- Vim mode's whole word search now uses the search bar's "Match whole
words" option, instead of wrapping the search term with `\<...\>`. This
allows easier toggling of whole-word search, and it also works without
enabling the regex mode.
2024-10-09 19:26:28 +02:00
狐狸
f05b440572 Improve syntax highlights (#18728)
Closes #18722

- Replace the `@escape` capture name with `@string.escape` for escape
sequences in Go, Python, Regex, Racket, Ruby, and Scheme.
- Rust
  - Add syntax highlighting for escape sequences. Close #18722
- Fix the issue where `@punctuation.delimiter` is being overwritten by
`@operator`.
  - Add the period (".") to `@punctuation.delimiter`.

Release Notes:

- N/A
2024-10-09 19:25:46 +02:00
Joseph T. Lyons
1cbaca667f Remove historical_event column in editor events (#18932)
We have a lot of data in Clickhouse. This column was used when migrating
the events dataset between analytics databases and has no purpose today.

Naive maths: 257,170,993 editor event rows * 1 byte per boolean =
257,170,993 bytes, or ~0.24 GB

I'll drop the column after deploying a new collab.

Going forward, I'd like to remove more data that we never touch, to try
to keep things more focused. We should discuss some TTL at some point.

Release Notes:

- N/A
2024-10-09 13:19:13 -04:00
Kirill Bulatov
8911fd46e1 Do not log errors when no worktree is found for certain assistant panel editors (#18923)
Nothing in the assistant panel needs LSP so far, so the errors are not
useful.

Release Notes:

- N/A
2024-10-09 18:45:22 +03:00
Joseph T Lyons
926e54bd4a v0.158.x dev 2024-10-09 11:32:34 -04:00
318 changed files with 10751 additions and 8109 deletions

View File

@@ -11,7 +11,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
with:
fetch-depth: 0

View File

@@ -18,7 +18,7 @@ jobs:
- buildjet-16vcpu-ubuntu-2204
steps:
- name: Checkout code
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
with:
ref: ${{ github.event.inputs.branch }}
ssh-key: ${{ secrets.ZED_BOT_DEPLOY_KEY }}

View File

@@ -34,7 +34,7 @@ jobs:
- test
steps:
- name: Checkout repo
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
with:
clean: false
fetch-depth: 0
@@ -85,7 +85,7 @@ jobs:
- test
steps:
- name: Checkout repo
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
with:
clean: false
@@ -96,10 +96,13 @@ jobs:
uses: ./.github/actions/run_tests
- name: Build collab
run: cargo build -p collab
run: RUSTFLAGS="-D warnings" cargo build -p collab
- name: Build other binaries and features
run: cargo build --workspace --bins --all-features; cargo check -p gpui --features "macos-blade"
run: |
RUSTFLAGS="-D warnings" cargo build --workspace --bins --all-features
cargo check -p gpui --features "macos-blade"
RUSTFLAGS="-D warnings" cargo build -p remote_server
linux_tests:
timeout-minutes: 60
@@ -111,7 +114,7 @@ jobs:
run: echo "$HOME/.cargo/bin" >> $GITHUB_PATH
- name: Checkout repo
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
with:
clean: false
@@ -131,7 +134,33 @@ jobs:
uses: ./.github/actions/run_tests
- name: Build Zed
run: cargo build -p zed
run: RUSTFLAGS="-D warnings" cargo build -p zed
build_remote_server:
timeout-minutes: 60
name: (Linux) Build Remote Server
runs-on:
- buildjet-16vcpu-ubuntu-2204
steps:
- name: Add Rust to the PATH
run: echo "$HOME/.cargo/bin" >> $GITHUB_PATH
- name: Checkout repo
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
with:
clean: false
- name: Cache dependencies
uses: swatinem/rust-cache@23bce251a8cd2ffc3c1075eaa2367cf899916d84 # v2
with:
save-if: ${{ github.ref == 'refs/heads/main' }}
cache-provider: "buildjet"
- name: Install Clang & Mold
run: ./script/remote-server && ./script/install-mold 2.34.0
- name: Build Remote Server
run: RUSTFLAGS="-D warnings" cargo build -p remote_server
# todo(windows): Actually run the tests
windows_tests:
@@ -140,7 +169,7 @@ jobs:
runs-on: hosted-windows-1
steps:
- name: Checkout repo
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
with:
clean: false
@@ -155,7 +184,7 @@ jobs:
run: cargo xtask clippy
- name: Build Zed
run: cargo build -p zed
run: $env:RUSTFLAGS="-D warnings"; cargo build
bundle-mac:
timeout-minutes: 60
@@ -181,7 +210,7 @@ jobs:
node-version: "18"
- name: Checkout repo
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
with:
# We need to fetch more than one commit so that `script/draft-release-notes`
# is able to diff between the current and previous tag.
@@ -219,20 +248,20 @@ jobs:
mv target/x86_64-apple-darwin/release/Zed.dmg target/x86_64-apple-darwin/release/Zed-x86_64.dmg
- name: Upload app bundle (universal) to workflow run if main branch or specific label
uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4
uses: actions/upload-artifact@604373da6381bf24206979c74d06a550515601b9 # v4
if: ${{ github.ref == 'refs/heads/main' }} || contains(github.event.pull_request.labels.*.name, 'run-bundling') }}
with:
name: Zed_${{ github.event.pull_request.head.sha || github.sha }}.dmg
path: target/release/Zed.dmg
- name: Upload app bundle (aarch64) to workflow run if main branch or specific label
uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4
uses: actions/upload-artifact@604373da6381bf24206979c74d06a550515601b9 # v4
if: ${{ github.ref == 'refs/heads/main' }} || contains(github.event.pull_request.labels.*.name, 'run-bundling') }}
with:
name: Zed_${{ github.event.pull_request.head.sha || github.sha }}-aarch64.dmg
path: target/aarch64-apple-darwin/release/Zed-aarch64.dmg
- name: Upload app bundle (x86_64) to workflow run if main branch or specific label
uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4
uses: actions/upload-artifact@604373da6381bf24206979c74d06a550515601b9 # v4
if: ${{ github.ref == 'refs/heads/main' }} || contains(github.event.pull_request.labels.*.name, 'run-bundling') }}
with:
name: Zed_${{ github.event.pull_request.head.sha || github.sha }}-x86_64.dmg
@@ -266,7 +295,7 @@ jobs:
ZED_CLOUD_PROVIDER_ADDITIONAL_MODELS_JSON: ${{ secrets.ZED_CLOUD_PROVIDER_ADDITIONAL_MODELS_JSON }}
steps:
- name: Checkout repo
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
with:
clean: false
@@ -283,7 +312,7 @@ jobs:
run: script/bundle-linux
- name: Upload Linux bundle to workflow run if main branch or specific label
uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4
uses: actions/upload-artifact@604373da6381bf24206979c74d06a550515601b9 # v4
if: ${{ github.ref == 'refs/heads/main' }} || contains(github.event.pull_request.labels.*.name, 'run-bundling') }}
with:
name: zed-${{ github.event.pull_request.head.sha || github.sha }}-x86_64-unknown-linux-gnu.tar.gz
@@ -313,7 +342,7 @@ jobs:
ZED_CLOUD_PROVIDER_ADDITIONAL_MODELS_JSON: ${{ secrets.ZED_CLOUD_PROVIDER_ADDITIONAL_MODELS_JSON }}
steps:
- name: Checkout repo
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
with:
clean: false
@@ -330,7 +359,7 @@ jobs:
run: script/bundle-linux
- name: Upload Linux bundle to workflow run if main branch or specific label
uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4
uses: actions/upload-artifact@604373da6381bf24206979c74d06a550515601b9 # v4
if: ${{ github.ref == 'refs/heads/main' }} || contains(github.event.pull_request.labels.*.name, 'run-bundling') }}
with:
name: zed-${{ github.event.pull_request.head.sha || github.sha }}-aarch64-unknown-linux-gnu.tar.gz

View File

@@ -14,7 +14,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4
- uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
- uses: pnpm/action-setup@fe02b34f77f8bc703788d5817da081398fad5dd2 # v4.0.0
with:

View File

@@ -12,7 +12,7 @@ jobs:
steps:
- name: Checkout repo
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
with:
clean: false
@@ -36,28 +36,28 @@ jobs:
mdbook build ./docs --dest-dir=../target/deploy/docs/
- name: Deploy Docs
uses: cloudflare/wrangler-action@168bc28b7078db16f6f1ecc26477fc2248592143 # v3
uses: cloudflare/wrangler-action@9681c2997648301493e78cacbfb790a9f19c833f # v3
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
command: pages deploy target/deploy --project-name=docs
- name: Deploy Install
uses: cloudflare/wrangler-action@168bc28b7078db16f6f1ecc26477fc2248592143 # v3
uses: cloudflare/wrangler-action@9681c2997648301493e78cacbfb790a9f19c833f # v3
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
command: r2 object put -f script/install.sh zed-open-source-website-assets/install.sh
- name: Deploy Docs Workers
uses: cloudflare/wrangler-action@168bc28b7078db16f6f1ecc26477fc2248592143 # v3
uses: cloudflare/wrangler-action@9681c2997648301493e78cacbfb790a9f19c833f # v3
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
command: deploy .cloudflare/docs-proxy/src/worker.js
- name: Deploy Install Workers
uses: cloudflare/wrangler-action@168bc28b7078db16f6f1ecc26477fc2248592143 # v3
uses: cloudflare/wrangler-action@9681c2997648301493e78cacbfb790a9f19c833f # v3
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}

View File

@@ -17,7 +17,7 @@ jobs:
- test
steps:
- name: Checkout repo
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
with:
clean: false
fetch-depth: 0
@@ -36,7 +36,7 @@ jobs:
needs: style
steps:
- name: Checkout repo
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
with:
clean: false
fetch-depth: 0
@@ -71,7 +71,7 @@ jobs:
run: doctl registry login
- name: Checkout repo
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
with:
clean: false
@@ -97,7 +97,7 @@ jobs:
steps:
- name: Checkout repo
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
with:
clean: false

View File

@@ -14,7 +14,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4
- uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
- uses: pnpm/action-setup@fe02b34f77f8bc703788d5817da081398fad5dd2 # v4.0.0
with:

View File

@@ -16,7 +16,7 @@ jobs:
- ubuntu-latest
steps:
- name: Checkout repo
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
with:
clean: false

View File

@@ -27,7 +27,7 @@ jobs:
node-version: "18"
- name: Checkout repo
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
with:
clean: false

View File

@@ -1,3 +1,5 @@
name: Release Actions
on:
release:
types: [published]

View File

@@ -23,7 +23,7 @@ jobs:
- test
steps:
- name: Checkout repo
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
with:
clean: false
fetch-depth: 0
@@ -44,7 +44,7 @@ jobs:
needs: style
steps:
- name: Checkout repo
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
with:
clean: false
@@ -75,7 +75,7 @@ jobs:
node-version: "18"
- name: Checkout repo
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
with:
clean: false
@@ -109,7 +109,7 @@ jobs:
ZED_CLOUD_PROVIDER_ADDITIONAL_MODELS_JSON: ${{ secrets.ZED_CLOUD_PROVIDER_ADDITIONAL_MODELS_JSON }}
steps:
- name: Checkout repo
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
with:
clean: false
@@ -149,7 +149,7 @@ jobs:
ZED_CLOUD_PROVIDER_ADDITIONAL_MODELS_JSON: ${{ secrets.ZED_CLOUD_PROVIDER_ADDITIONAL_MODELS_JSON }}
steps:
- name: Checkout repo
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
with:
clean: false
@@ -182,7 +182,7 @@ jobs:
- bundle-linux-arm
steps:
- name: Checkout repo
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
with:
fetch-depth: 0

View File

@@ -1,3 +1,5 @@
name: Update All Top Ranking Issues
on:
schedule:
- cron: "0 */12 * * *"
@@ -8,11 +10,16 @@ jobs:
runs-on: ubuntu-latest
if: github.repository_owner == 'zed-industries'
steps:
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4
- uses: actions/setup-python@f677139bbe7f9c59b41e40162b753c062f5d49a3 # v5
- uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
- name: Set up uv
uses: astral-sh/setup-uv@v3
with:
python-version: "3.11"
architecture: "x64"
cache: "pip"
- run: pip install -r script/update_top_ranking_issues/requirements.txt
- run: python script/update_top_ranking_issues/main.py --github-token ${{ secrets.GITHUB_TOKEN }} --issue-reference-number 5393
version: "latest"
enable-cache: true
cache-dependency-glob: "script/update_top_ranking_issues/pyproject.toml"
- name: Install Python 3.13
run: uv python install 3.13
- name: Install dependencies
run: uv sync --project script/update_top_ranking_issues -p 3.13
- name: Run script
run: uv run --project script/update_top_ranking_issues script/update_top_ranking_issues/main.py --github-token ${{ secrets.GITHUB_TOKEN }} --issue-reference-number 5393

View File

@@ -1,3 +1,5 @@
name: Update Weekly Top Ranking Issues
on:
schedule:
- cron: "0 15 * * *"
@@ -8,11 +10,16 @@ jobs:
runs-on: ubuntu-latest
if: github.repository_owner == 'zed-industries'
steps:
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4
- uses: actions/setup-python@f677139bbe7f9c59b41e40162b753c062f5d49a3 # v5
- uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
- name: Set up uv
uses: astral-sh/setup-uv@v3
with:
python-version: "3.11"
architecture: "x64"
cache: "pip"
- run: pip install -r script/update_top_ranking_issues/requirements.txt
- run: python script/update_top_ranking_issues/main.py --github-token ${{ secrets.GITHUB_TOKEN }} --issue-reference-number 6952 --query-day-interval 7
version: "latest"
enable-cache: true
cache-dependency-glob: "script/update_top_ranking_issues/pyproject.toml"
- name: Install Python 3.13
run: uv python install 3.13
- name: Install dependencies
run: uv sync --project script/update_top_ranking_issues -p 3.13
- name: Run script
run: uv run --project script/update_top_ranking_issues script/update_top_ranking_issues/main.py --github-token ${{ secrets.GITHUB_TOKEN }} --issue-reference-number 6952 --query-day-interval 7

781
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -52,7 +52,6 @@ members = [
"crates/indexed_docs",
"crates/inline_completion_button",
"crates/install_cli",
"crates/isahc_http_client",
"crates/journal",
"crates/language",
"crates/language_model",
@@ -88,6 +87,7 @@ members = [
"crates/remote",
"crates/remote_server",
"crates/repl",
"crates/reqwest_client",
"crates/rich_text",
"crates/rope",
"crates/rpc",
@@ -122,6 +122,7 @@ members = [
"crates/ui",
"crates/ui_input",
"crates/ui_macros",
"crates/reqwest_client",
"crates/util",
"crates/vcs_menu",
"crates/vim",
@@ -144,7 +145,6 @@ members = [
"extensions/elm",
"extensions/emmet",
"extensions/erlang",
"extensions/gleam",
"extensions/glsl",
"extensions/haskell",
"extensions/html",
@@ -156,7 +156,6 @@ members = [
"extensions/proto",
"extensions/purescript",
"extensions/ruff",
"extensions/ruby",
"extensions/slash-commands-example",
"extensions/snippets",
"extensions/svelte",
@@ -220,7 +219,7 @@ git = { path = "crates/git" }
git_hosting_providers = { path = "crates/git_hosting_providers" }
go_to_line = { path = "crates/go_to_line" }
google_ai = { path = "crates/google_ai" }
gpui = { path = "crates/gpui" }
gpui = { path = "crates/gpui", default-features = false, features = ["http_client"]}
gpui_macros = { path = "crates/gpui_macros" }
headless = { path = "crates/headless" }
html_to_markdown = { path = "crates/html_to_markdown" }
@@ -229,7 +228,6 @@ image_viewer = { path = "crates/image_viewer" }
indexed_docs = { path = "crates/indexed_docs" }
inline_completion_button = { path = "crates/inline_completion_button" }
install_cli = { path = "crates/install_cli" }
isahc_http_client = { path = "crates/isahc_http_client" }
journal = { path = "crates/journal" }
language = { path = "crates/language" }
language_model = { path = "crates/language_model" }
@@ -266,6 +264,7 @@ release_channel = { path = "crates/release_channel" }
remote = { path = "crates/remote" }
remote_server = { path = "crates/remote_server" }
repl = { path = "crates/repl" }
reqwest_client = { path = "crates/reqwest_client" }
rich_text = { path = "crates/rich_text" }
rope = { path = "crates/rope" }
rpc = { path = "crates/rpc" }
@@ -327,7 +326,7 @@ async-pipe = { git = "https://github.com/zed-industries/async-pipe-rs", rev = "8
async-recursion = "1.0.0"
async-tar = "0.5.0"
async-trait = "0.1"
async-tungstenite = "0.23"
async-tungstenite = "0.24"
async-watch = "0.3.1"
async_zip = { version = "0.0.17", features = ["deflate", "deflate64"] }
base64 = "0.22"
@@ -336,6 +335,7 @@ blade-graphics = { git = "https://github.com/kvark/blade", rev = "e142a3a5e678eb
blade-macros = { git = "https://github.com/kvark/blade", rev = "e142a3a5e678eb6a13e642ad8401b1f3aa38e969" }
blade-util = { git = "https://github.com/kvark/blade", rev = "e142a3a5e678eb6a13e642ad8401b1f3aa38e969" }
blake3 = "1.5.3"
bytes = "1.0"
cargo_metadata = "0.18"
cargo_toml = "0.20"
chrono = { version = "0.4", features = ["serde"] }
@@ -367,10 +367,6 @@ ignore = "0.4.22"
image = "0.25.1"
indexmap = { version = "1.6.2", features = ["serde"] }
indoc = "2"
# We explicitly disable http2 support in isahc.
isahc = { version = "1.7.2", default-features = false, features = [
"text-decoding",
] }
itertools = "0.13.0"
jsonwebtoken = "9.3"
libc = "0.2"
@@ -395,6 +391,7 @@ pulldown-cmark = { version = "0.12.0", default-features = false }
rand = "0.8.5"
regex = "1.5"
repair_json = "0.1.0"
reqwest = { git = "https://github.com/zed-industries/reqwest.git", rev = "fd110f6998da16bbca97b6dddda9be7827c50e29", default-features = false, features = ["charset", "http2", "macos-system-configuration", "rustls-tls-native-roots", "stream"]}
rsa = "0.9.6"
runtimelib = { version = "0.15", default-features = false, features = [
"async-dispatcher-runtime",
@@ -439,7 +436,7 @@ time = { version = "0.3", features = [
] }
tiny_http = "0.8"
toml = "0.8"
tokio = { version = "1", features = ["full"] }
tokio = { version = "1" }
tower-http = "0.4.4"
tree-sitter = { version = "0.23", features = ["wasm"] }
tree-sitter-bash = "0.23"
@@ -452,6 +449,7 @@ tree-sitter-go = "0.23"
tree-sitter-go-mod = { git = "https://github.com/zed-industries/tree-sitter-go-mod", rev = "a9aea5e358cde4d0f8ff20b7bc4fa311e359c7ca", package = "tree-sitter-gomod" }
tree-sitter-gowork = { git = "https://github.com/zed-industries/tree-sitter-go-work", rev = "acb0617bf7f4fda02c6217676cc64acb89536dc7" }
tree-sitter-heex = { git = "https://github.com/zed-industries/tree-sitter-heex", rev = "1dd45142fbb05562e35b2040c6129c9bca346592" }
tree-sitter-diff = "0.1.0"
tree-sitter-html = "0.20"
tree-sitter-jsdoc = "0.23"
tree-sitter-json = "0.23"
@@ -479,9 +477,11 @@ wasmtime = { version = "24", default-features = false, features = [
wasmtime-wasi = "24"
which = "6.0.0"
wit-component = "0.201"
zstd = "0.11"
[workspace.dependencies.async-stripe]
version = "0.39"
git = "https://github.com/zed-industries/async-stripe"
rev = "3672dd4efb7181aa597bf580bf5a2f5d23db6735"
default-features = false
features = [
"runtime-tokio-hyper-rustls",

2
Cross.toml Normal file
View File

@@ -0,0 +1,2 @@
[build]
dockerfile = "Dockerfile-cross"

View File

@@ -13,30 +13,9 @@ ARG GITHUB_SHA
ENV GITHUB_SHA=$GITHUB_SHA
# At some point in the past 3 weeks, additional dependencies on `xkbcommon` and
# `xkbcommon-x11` were introduced into collab.
#
# A `git bisect` points to this commit as being the culprit: `b8e6098f60e5dabe98fe8281f993858dacc04a55`.
#
# Now when we try to build collab for the Docker image, it fails with the following
# error:
#
# ```
# 985.3 = note: /usr/bin/ld: cannot find -lxkbcommon: No such file or directory
# 985.3 /usr/bin/ld: cannot find -lxkbcommon-x11: No such file or directory
# 985.3 collect2: error: ld returned 1 exit status
# ```
#
# The last successful deploys were at:
# - Staging: `4f408ec65a3867278322a189b4eb20f1ab51f508`
# - Production: `fc4c533d0a8c489e5636a4249d2b52a80039fbd7`
#
# Also add `cmake`, since we need it to build `wasmtime`.
#
# Installing these as a temporary workaround, but I think ideally we'd want to figure
# out what caused them to be included in the first place.
RUN apt-get update; \
apt-get install -y --no-install-recommends libxkbcommon-dev libxkbcommon-x11-dev cmake
apt-get install -y --no-install-recommends cmake
RUN --mount=type=cache,target=./script/node_modules \
--mount=type=cache,target=/usr/local/cargo/registry \

17
Dockerfile-cross Normal file
View File

@@ -0,0 +1,17 @@
# syntax=docker/dockerfile:1
ARG CROSS_BASE_IMAGE
FROM ${CROSS_BASE_IMAGE}
WORKDIR /app
ARG TZ=Etc/UTC \
LANG=C.UTF-8 \
LC_ALL=C.UTF-8 \
DEBIAN_FRONTEND=noninteractive
ENV CARGO_TERM_COLOR=always
COPY script/install-mold script/
RUN ./script/install-mold "2.34.0"
COPY script/remote-server script/
RUN ./script/remote-server
COPY . .

View File

@@ -0,0 +1,16 @@
.git
.github
**/.gitignore
**/.gitkeep
.gitattributes
.mailmap
**/target
zed.xcworkspace
.DS_Store
compose.yml
plugins/bin
script/node_modules
styles/node_modules
crates/collab/static/styles.css
vendor/bin
assets/themes/

View File

@@ -20,6 +20,7 @@
"bashrc": "terminal",
"bmp": "image",
"c": "c",
"c++": "cpp",
"cc": "cpp",
"cjs": "javascript",
"coffee": "coffeescript",
@@ -27,6 +28,7 @@
"cpp": "cpp",
"css": "css",
"csv": "storage",
"cxx": "cpp",
"cts": "typescript",
"dart": "dart",
"dat": "storage",
@@ -66,11 +68,13 @@
"heex": "elixir",
"heic": "image",
"heif": "image",
"hh": "cpp",
"hpp": "cpp",
"hrl": "erlang",
"hs": "haskell",
"htm": "template",
"html": "template",
"hxx": "cpp",
"ib": "storage",
"ico": "image",
"ini": "settings",

View File

@@ -664,7 +664,8 @@
"shift-up": "terminal::ScrollLineUp",
"shift-down": "terminal::ScrollLineDown",
"shift-home": "terminal::ScrollToTop",
"shift-end": "terminal::ScrollToBottom"
"shift-end": "terminal::ScrollToBottom",
"ctrl-shift-space": "terminal::ToggleViMode"
}
},
{

View File

@@ -395,6 +395,7 @@
// Change the default action on `menu::Confirm` by setting the parameter
// "alt-cmd-o": ["projects::OpenRecent", {"create_new_window": true }],
"alt-cmd-o": "projects::OpenRecent",
"ctrl-cmd-o": "projects::OpenRemote",
"alt-cmd-b": "branches::OpenRecent",
"ctrl-~": "workspace::NewTerminal",
"cmd-s": "workspace::Save",
@@ -678,7 +679,8 @@
"cmd-home": "terminal::ScrollToTop",
"cmd-end": "terminal::ScrollToBottom",
"shift-home": "terminal::ScrollToTop",
"shift-end": "terminal::ScrollToBottom"
"shift-end": "terminal::ScrollToBottom",
"ctrl-shift-space": "terminal::ToggleViMode"
}
}
]

View File

@@ -128,6 +128,10 @@
"shift-m": "vim::WindowMiddle",
"shift-l": "vim::WindowBottom",
// z commands
"z enter": ["workspace::SendKeystrokes", "z t ^"],
"z -": ["workspace::SendKeystrokes", "z b ^"],
"z ^": ["workspace::SendKeystrokes", "shift-h k z b ^"],
"z +": ["workspace::SendKeystrokes", "shift-l j z t ^"],
"z t": "editor::ScrollCursorTop",
"z z": "editor::ScrollCursorCenter",
"z .": ["workspace::SendKeystrokes", "z z ^"],
@@ -252,6 +256,7 @@
"@": ["vim::PushOperator", "ReplayRegister"],
"ctrl-pagedown": "pane::ActivateNextItem",
"ctrl-pageup": "pane::ActivatePrevItem",
"insert": "vim::InsertBefore",
// tree-sitter related commands
"[ x": "editor::SelectLargerSyntaxNode",
"] x": "editor::SelectSmallerSyntaxNode",
@@ -334,7 +339,8 @@
"ctrl-t": "vim::Indent",
"ctrl-d": "vim::Outdent",
"ctrl-k": ["vim::PushOperator", { "Digraph": {} }],
"ctrl-r": ["vim::PushOperator", "Register"]
"ctrl-r": ["vim::PushOperator", "Register"],
"insert": "vim::ToggleReplace"
}
},
{
@@ -353,7 +359,8 @@
"ctrl-k": ["vim::PushOperator", { "Digraph": {} }],
"backspace": "vim::UndoReplace",
"tab": "vim::Tab",
"enter": "vim::Enter"
"enter": "vim::Enter",
"insert": "vim::InsertBefore"
}
},
{

View File

@@ -118,8 +118,8 @@
// "bar"
// 2. A block that surrounds the following character
// "block"
// 3. An underline that runs along the following character
// "underscore"
// 3. An underline / underscore that runs along the following character
// "underline"
// 4. A box drawn around the following character
// "hollow"
//
@@ -494,7 +494,14 @@
// Position of the close button on the editor tabs.
"close_position": "right",
// Whether to show the file icon for a tab.
"file_icons": false
"file_icons": false,
// What to do after closing the current tab.
//
// 1. Activate the tab that was open previously (default)
// "History"
// 2. Activate the neighbour tab (prefers the right one, if present)
// "Neighbour"
"activate_on_close": "history"
},
// Settings related to preview tabs.
"preview_tabs": {
@@ -684,8 +691,8 @@
// "block"
// 2. A vertical bar
// "bar"
// 3. An underline that runs along the following character
// "underscore"
// 3. An underline / underscore that runs along the following character
// "underline"
// 4. A box drawn around the following character
// "hollow"
//
@@ -817,6 +824,7 @@
// Different settings for specific languages.
"languages": {
"Astro": {
"language_servers": ["astro-language-server", "..."],
"prettier": {
"allowed": true,
"plugins": ["prettier-plugin-astro"]
@@ -843,6 +851,10 @@
"Dart": {
"tab_size": 2
},
"Diff": {
"remove_trailing_whitespace_on_save": false,
"ensure_final_newline_on_save": false
},
"Elixir": {
"language_servers": ["elixir-ls", "!next-ls", "!lexical", "..."]
},

View File

@@ -0,0 +1,7 @@
// Server-specific settings
//
// For a full list of overridable settings, and general information on settings,
// see the documentation: https://zed.dev/docs/configuring-zed#settings-files
{
"lsp": {}
}

View File

@@ -101,6 +101,7 @@ impl ActivityIndicator {
None,
cx,
);
buffer.set_capability(language::Capability::ReadOnly, cx);
})?;
workspace.update(&mut cx, |workspace, cx| {
workspace.add_item_to_active_pane(

View File

@@ -26,6 +26,3 @@ serde_json.workspace = true
strum.workspace = true
thiserror.workspace = true
util.workspace = true
[dev-dependencies]
tokio.workspace = true

View File

@@ -18,7 +18,7 @@ use crate::{
PendingSlashCommand, PendingSlashCommandStatus, QuoteSelection, RemoteContextMetadata,
SavedContextMetadata, Split, ToggleFocus, ToggleModelSelector, WorkflowStepResolution,
};
use anyhow::{anyhow, Result};
use anyhow::Result;
use assistant_slash_command::{SlashCommand, SlashCommandOutputSection};
use assistant_tool::ToolRegistry;
use client::{proto, Client, Status};
@@ -697,7 +697,9 @@ impl AssistantPanel {
log::error!("no context found with ID: {}", context_id.to_proto());
return;
};
let lsp_adapter_delegate = make_lsp_adapter_delegate(&self.project, cx).log_err();
let lsp_adapter_delegate = make_lsp_adapter_delegate(&self.project, cx)
.log_err()
.flatten();
let assistant_panel = cx.view().downgrade();
let editor = cx.new_view(|cx| {
@@ -971,7 +973,8 @@ impl AssistantPanel {
this.update(&mut cx, |this, cx| {
let workspace = this.workspace.clone();
let project = this.project.clone();
let lsp_adapter_delegate = make_lsp_adapter_delegate(&project, cx).log_err();
let lsp_adapter_delegate =
make_lsp_adapter_delegate(&project, cx).log_err().flatten();
let fs = this.fs.clone();
let project = this.project.clone();
@@ -1001,7 +1004,9 @@ impl AssistantPanel {
None
} else {
let context = self.context_store.update(cx, |store, cx| store.create(cx));
let lsp_adapter_delegate = make_lsp_adapter_delegate(&self.project, cx).log_err();
let lsp_adapter_delegate = make_lsp_adapter_delegate(&self.project, cx)
.log_err()
.flatten();
let assistant_panel = cx.view().downgrade();
let editor = cx.new_view(|cx| {
@@ -1207,7 +1212,7 @@ impl AssistantPanel {
let project = self.project.clone();
let workspace = self.workspace.clone();
let lsp_adapter_delegate = make_lsp_adapter_delegate(&project, cx).log_err();
let lsp_adapter_delegate = make_lsp_adapter_delegate(&project, cx).log_err().flatten();
cx.spawn(|this, mut cx| async move {
let context = context.await?;
@@ -1254,7 +1259,9 @@ impl AssistantPanel {
.update(cx, |store, cx| store.open_remote_context(id, cx));
let fs = self.fs.clone();
let workspace = self.workspace.clone();
let lsp_adapter_delegate = make_lsp_adapter_delegate(&self.project, cx).log_err();
let lsp_adapter_delegate = make_lsp_adapter_delegate(&self.project, cx)
.log_err()
.flatten();
cx.spawn(|this, mut cx| async move {
let context = context.await?;
@@ -1496,6 +1503,13 @@ struct WorkflowAssist {
type MessageHeader = MessageMetadata;
#[derive(Clone)]
enum AssistError {
PaymentRequired,
MaxMonthlySpendReached,
Message(SharedString),
}
pub struct ContextEditor {
context: Model<Context>,
fs: Arc<dyn Fs>,
@@ -1514,7 +1528,7 @@ pub struct ContextEditor {
workflow_steps: HashMap<Range<language::Anchor>, WorkflowStepViewState>,
active_workflow_step: Option<ActiveWorkflowStep>,
assistant_panel: WeakView<AssistantPanel>,
error_message: Option<SharedString>,
last_error: Option<AssistError>,
show_accept_terms: bool,
pub(crate) slash_menu_handle:
PopoverMenuHandle<Picker<slash_command_picker::SlashCommandDelegate>>,
@@ -1553,7 +1567,7 @@ impl ContextEditor {
editor.set_show_runnables(false, cx);
editor.set_show_wrap_guides(false, cx);
editor.set_show_indent_guides(false, cx);
editor.set_completion_provider(Box::new(completion_provider));
editor.set_completion_provider(Some(Box::new(completion_provider)));
editor.set_collaboration_hub(Box::new(project.clone()));
editor
});
@@ -1585,7 +1599,7 @@ impl ContextEditor {
workflow_steps: HashMap::default(),
active_workflow_step: None,
assistant_panel,
error_message: None,
last_error: None,
show_accept_terms: false,
slash_menu_handle: Default::default(),
dragged_file_worktrees: Vec::new(),
@@ -1629,7 +1643,7 @@ impl ContextEditor {
}
if !self.apply_active_workflow_step(cx) {
self.error_message = None;
self.last_error = None;
self.send_to_model(cx);
cx.notify();
}
@@ -1779,7 +1793,7 @@ impl ContextEditor {
}
fn cancel(&mut self, _: &editor::actions::Cancel, cx: &mut ViewContext<Self>) {
self.error_message = None;
self.last_error = None;
if self
.context
@@ -2284,7 +2298,13 @@ impl ContextEditor {
}
ContextEvent::Operation(_) => {}
ContextEvent::ShowAssistError(error_message) => {
self.error_message = Some(error_message.clone());
self.last_error = Some(AssistError::Message(error_message.clone()));
}
ContextEvent::ShowPaymentRequiredError => {
self.last_error = Some(AssistError::PaymentRequired);
}
ContextEvent::ShowMaxMonthlySpendReachedError => {
self.last_error = Some(AssistError::MaxMonthlySpendReached);
}
}
}
@@ -4298,6 +4318,154 @@ impl ContextEditor {
focus_handle.dispatch_action(&Assist, cx);
})
}
fn render_last_error(&self, cx: &mut ViewContext<Self>) -> Option<AnyElement> {
let last_error = self.last_error.as_ref()?;
Some(
div()
.absolute()
.right_3()
.bottom_12()
.max_w_96()
.py_2()
.px_3()
.elevation_2(cx)
.occlude()
.child(match last_error {
AssistError::PaymentRequired => self.render_payment_required_error(cx),
AssistError::MaxMonthlySpendReached => {
self.render_max_monthly_spend_reached_error(cx)
}
AssistError::Message(error_message) => {
self.render_assist_error(error_message, cx)
}
})
.into_any(),
)
}
fn render_payment_required_error(&self, cx: &mut ViewContext<Self>) -> AnyElement {
const ERROR_MESSAGE: &str = "Free tier exceeded. Subscribe and add payment to continue using Zed LLMs. You'll be billed at cost for tokens used.";
const ACCOUNT_URL: &str = "https://zed.dev/account";
v_flex()
.gap_0p5()
.child(
h_flex()
.gap_1p5()
.items_center()
.child(Icon::new(IconName::XCircle).color(Color::Error))
.child(Label::new("Free Usage Exceeded").weight(FontWeight::MEDIUM)),
)
.child(
div()
.id("error-message")
.max_h_24()
.overflow_y_scroll()
.child(Label::new(ERROR_MESSAGE)),
)
.child(
h_flex()
.justify_end()
.mt_1()
.child(Button::new("subscribe", "Subscribe").on_click(cx.listener(
|this, _, cx| {
this.last_error = None;
cx.open_url(ACCOUNT_URL);
cx.notify();
},
)))
.child(Button::new("dismiss", "Dismiss").on_click(cx.listener(
|this, _, cx| {
this.last_error = None;
cx.notify();
},
))),
)
.into_any()
}
fn render_max_monthly_spend_reached_error(&self, cx: &mut ViewContext<Self>) -> AnyElement {
const ERROR_MESSAGE: &str = "You have reached your maximum monthly spend. Increase your spend limit to continue using Zed LLMs.";
const ACCOUNT_URL: &str = "https://zed.dev/account";
v_flex()
.gap_0p5()
.child(
h_flex()
.gap_1p5()
.items_center()
.child(Icon::new(IconName::XCircle).color(Color::Error))
.child(Label::new("Max Monthly Spend Reached").weight(FontWeight::MEDIUM)),
)
.child(
div()
.id("error-message")
.max_h_24()
.overflow_y_scroll()
.child(Label::new(ERROR_MESSAGE)),
)
.child(
h_flex()
.justify_end()
.mt_1()
.child(
Button::new("subscribe", "Update Monthly Spend Limit").on_click(
cx.listener(|this, _, cx| {
this.last_error = None;
cx.open_url(ACCOUNT_URL);
cx.notify();
}),
),
)
.child(Button::new("dismiss", "Dismiss").on_click(cx.listener(
|this, _, cx| {
this.last_error = None;
cx.notify();
},
))),
)
.into_any()
}
fn render_assist_error(
&self,
error_message: &SharedString,
cx: &mut ViewContext<Self>,
) -> AnyElement {
v_flex()
.gap_0p5()
.child(
h_flex()
.gap_1p5()
.items_center()
.child(Icon::new(IconName::XCircle).color(Color::Error))
.child(
Label::new("Error interacting with language model")
.weight(FontWeight::MEDIUM),
),
)
.child(
div()
.id("error-message")
.max_h_24()
.overflow_y_scroll()
.child(Label::new(error_message.clone())),
)
.child(
h_flex()
.justify_end()
.mt_1()
.child(Button::new("dismiss", "Dismiss").on_click(cx.listener(
|this, _, cx| {
this.last_error = None;
cx.notify();
},
))),
)
.into_any()
}
}
/// Returns the contents of the *outermost* fenced code block that contains the given offset.
@@ -4434,48 +4602,7 @@ impl Render for ContextEditor {
.child(element),
)
})
.when_some(self.error_message.clone(), |this, error_message| {
this.child(
div()
.absolute()
.right_3()
.bottom_12()
.max_w_96()
.py_2()
.px_3()
.elevation_2(cx)
.occlude()
.child(
v_flex()
.gap_0p5()
.child(
h_flex()
.gap_1p5()
.items_center()
.child(Icon::new(IconName::XCircle).color(Color::Error))
.child(
Label::new("Error interacting with language model")
.weight(FontWeight::MEDIUM),
),
)
.child(
div()
.id("error-message")
.max_h_24()
.overflow_y_scroll()
.child(Label::new(error_message)),
)
.child(h_flex().justify_end().mt_1().child(
Button::new("dismiss", "Dismiss").on_click(cx.listener(
|this, _, cx| {
this.error_message = None;
cx.notify();
},
)),
)),
),
)
})
.children(self.render_last_error(cx))
.child(
h_flex().w_full().relative().child(
h_flex()
@@ -5505,22 +5632,21 @@ fn render_docs_slash_command_trailer(
fn make_lsp_adapter_delegate(
project: &Model<Project>,
cx: &mut AppContext,
) -> Result<Arc<dyn LspAdapterDelegate>> {
) -> Result<Option<Arc<dyn LspAdapterDelegate>>> {
project.update(cx, |project, cx| {
// TODO: Find the right worktree.
let worktree = project
.worktrees(cx)
.next()
.ok_or_else(|| anyhow!("no worktrees when constructing LocalLspAdapterDelegate"))?;
let Some(worktree) = project.worktrees(cx).next() else {
return Ok(None::<Arc<dyn LspAdapterDelegate>>);
};
let http_client = project.client().http_client().clone();
project.lsp_store().update(cx, |lsp_store, cx| {
Ok(LocalLspAdapterDelegate::new(
Ok(Some(LocalLspAdapterDelegate::new(
lsp_store,
&worktree,
http_client,
project.fs().clone(),
cx,
) as Arc<dyn LspAdapterDelegate>)
) as Arc<dyn LspAdapterDelegate>))
})
})
}

View File

@@ -26,6 +26,7 @@ use gpui::{
use language::{AnchorRangeExt, Bias, Buffer, LanguageRegistry, OffsetRangeExt, Point, ToOffset};
use language_model::{
provider::cloud::{MaxMonthlySpendReachedError, PaymentRequiredError},
LanguageModel, LanguageModelCacheConfiguration, LanguageModelCompletionEvent,
LanguageModelImage, LanguageModelRegistry, LanguageModelRequest, LanguageModelRequestMessage,
LanguageModelRequestTool, LanguageModelToolResult, LanguageModelToolUse, MessageContent, Role,
@@ -294,6 +295,8 @@ impl ContextOperation {
#[derive(Debug, Clone)]
pub enum ContextEvent {
ShowAssistError(SharedString),
ShowPaymentRequiredError,
ShowMaxMonthlySpendReachedError,
MessagesEdited,
SummaryChanged,
StreamedCompletion,
@@ -2112,25 +2115,36 @@ impl Context {
let result = stream_completion.await;
this.update(&mut cx, |this, cx| {
let error_message = result
.as_ref()
.err()
.map(|error| error.to_string().trim().to_string());
if let Some(error_message) = error_message.as_ref() {
cx.emit(ContextEvent::ShowAssistError(SharedString::from(
error_message.clone(),
)));
}
this.update_metadata(assistant_message_id, cx, |metadata| {
if let Some(error_message) = error_message.as_ref() {
metadata.status =
MessageStatus::Error(SharedString::from(error_message.clone()));
let error_message = if let Some(error) = result.as_ref().err() {
if error.is::<PaymentRequiredError>() {
cx.emit(ContextEvent::ShowPaymentRequiredError);
this.update_metadata(assistant_message_id, cx, |metadata| {
metadata.status = MessageStatus::Canceled;
});
Some(error.to_string())
} else if error.is::<MaxMonthlySpendReachedError>() {
cx.emit(ContextEvent::ShowMaxMonthlySpendReachedError);
this.update_metadata(assistant_message_id, cx, |metadata| {
metadata.status = MessageStatus::Canceled;
});
Some(error.to_string())
} else {
metadata.status = MessageStatus::Done;
let error_message = error.to_string().trim().to_string();
cx.emit(ContextEvent::ShowAssistError(SharedString::from(
error_message.clone(),
)));
this.update_metadata(assistant_message_id, cx, |metadata| {
metadata.status =
MessageStatus::Error(SharedString::from(error_message.clone()));
});
Some(error_message)
}
});
} else {
this.update_metadata(assistant_message_id, cx, |metadata| {
metadata.status = MessageStatus::Done;
});
None
};
if let Some(telemetry) = this.telemetry.as_ref() {
let language_name = this
@@ -2146,7 +2160,7 @@ impl Context {
model_provider: model.provider_id().to_string(),
response_latency,
error_message,
language_name,
language_name: language_name.map(|name| name.to_proto()),
});
}

View File

@@ -267,7 +267,7 @@ impl InlineAssistant {
model_provider: model.provider_id().to_string(),
response_latency: None,
error_message: None,
language_name: buffer.language().map(|language| language.name()),
language_name: buffer.language().map(|language| language.name().to_proto()),
});
}
}
@@ -788,7 +788,7 @@ impl InlineAssistant {
model_provider: model.provider_id().to_string(),
response_latency: None,
error_message: None,
language_name,
language_name: language_name.map(|name| name.to_proto()),
});
}
}
@@ -2278,7 +2278,7 @@ impl InlineAssist {
struct InlineAssistantError;
let id =
NotificationId::identified::<InlineAssistantError>(
NotificationId::composite::<InlineAssistantError>(
assist_id.0,
);
@@ -2954,7 +2954,7 @@ impl CodegenAlternative {
model_provider: model_provider_id.to_string(),
response_latency,
error_message,
language_name,
language_name: language_name.map(|name| name.to_proto()),
});
}

View File

@@ -521,9 +521,9 @@ impl PromptLibrary {
editor.set_show_indent_guides(false, cx);
editor.set_use_modal_editing(false);
editor.set_current_line_highlight(Some(CurrentLineHighlight::None));
editor.set_completion_provider(Box::new(
editor.set_completion_provider(Some(Box::new(
SlashCommandCompletionProvider::new(None, None),
));
)));
if focus {
editor.focus(cx);
}

View File

@@ -38,7 +38,10 @@ impl Settings for SlashCommandSettings {
fn load(sources: SettingsSources<Self::FileContent>, _cx: &mut AppContext) -> Result<Self> {
SettingsSources::<Self::FileContent>::json_merge_with(
[sources.default].into_iter().chain(sources.user),
[sources.default]
.into_iter()
.chain(sources.user)
.chain(sources.server),
)
}
}

View File

@@ -414,7 +414,7 @@ impl TerminalInlineAssist {
struct InlineAssistantError;
let id =
NotificationId::identified::<InlineAssistantError>(
NotificationId::composite::<InlineAssistantError>(
assist_id.0,
);

View File

@@ -130,7 +130,7 @@ impl Settings for AutoUpdateSetting {
type FileContent = Option<AutoUpdateSettingContent>;
fn load(sources: SettingsSources<Self::FileContent>, _: &mut AppContext) -> Result<Self> {
let auto_update = [sources.release_channel, sources.user]
let auto_update = [sources.server, sources.release_channel, sources.user]
.into_iter()
.find_map(|value| value.copied().flatten())
.unwrap_or(sources.default.ok_or_else(Self::missing_default)?);
@@ -464,6 +464,7 @@ impl AutoUpdater {
smol::fs::create_dir_all(&platform_dir).await.ok();
let client = this.read_with(cx, |this, _| this.http_client.clone())?;
if smol::fs::metadata(&version_path).await.is_err() {
log::info!("downloading zed-remote-server {os} {arch}");
download_remote_server_binary(&version_path, release, client, cx).await?;

View File

@@ -1178,7 +1178,7 @@ impl Room {
this.update(&mut cx, |this, cx| {
this.joined_projects.retain(|project| {
if let Some(project) = project.upgrade() {
!project.read(cx).is_disconnected()
!project.read(cx).is_disconnected(cx)
} else {
false
}

View File

@@ -58,27 +58,32 @@ struct Args {
dev_server_token: Option<String>,
}
fn parse_path_with_position(argument_str: &str) -> Result<String, std::io::Error> {
let path = PathWithPosition::parse_str(argument_str);
let curdir = env::current_dir()?;
let canonicalized = path.map_path(|path| match fs::canonicalize(&path) {
Ok(path) => Ok(path),
Err(e) => {
if let Some(mut parent) = path.parent() {
if parent == Path::new("") {
parent = &curdir
fn parse_path_with_position(argument_str: &str) -> anyhow::Result<String> {
let canonicalized = match Path::new(argument_str).canonicalize() {
Ok(existing_path) => PathWithPosition::from_path(existing_path),
Err(_) => {
let path = PathWithPosition::parse_str(argument_str);
let curdir = env::current_dir().context("reteiving current directory")?;
path.map_path(|path| match fs::canonicalize(&path) {
Ok(path) => Ok(path),
Err(e) => {
if let Some(mut parent) = path.parent() {
if parent == Path::new("") {
parent = &curdir
}
match fs::canonicalize(parent) {
Ok(parent) => Ok(parent.join(path.file_name().unwrap())),
Err(_) => Err(e),
}
} else {
Err(e)
}
}
match fs::canonicalize(parent) {
Ok(parent) => Ok(parent.join(path.file_name().unwrap())),
Err(_) => Err(e),
}
} else {
Err(e)
}
})
}
})?;
Ok(canonicalized.to_string(|path| path.display().to_string()))
.with_context(|| format!("parsing as path with position {argument_str}"))?,
};
Ok(canonicalized.to_string(|path| path.to_string_lossy().to_string()))
}
fn main() -> Result<()> {

View File

@@ -34,8 +34,8 @@ postage.workspace = true
rand.workspace = true
release_channel.workspace = true
rpc = { workspace = true, features = ["gpui"] }
rustls.workspace = true
rustls-native-certs.workspace = true
rustls.workspace = true
schemars.workspace = true
serde.workspace = true
serde_json.workspace = true

View File

@@ -141,6 +141,7 @@ impl Settings for ProxySettings {
Ok(Self {
proxy: sources
.user
.or(sources.server)
.and_then(|value| value.proxy.clone())
.or(sources.default.proxy.clone()),
})
@@ -472,15 +473,21 @@ impl settings::Settings for TelemetrySettings {
fn load(sources: SettingsSources<Self::FileContent>, _: &mut AppContext) -> Result<Self> {
Ok(Self {
diagnostics: sources.user.as_ref().and_then(|v| v.diagnostics).unwrap_or(
sources
.default
.diagnostics
.ok_or_else(Self::missing_default)?,
),
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)?),
})
@@ -1023,7 +1030,7 @@ impl Client {
&self,
http: Arc<HttpClientWithUrl>,
release_channel: Option<ReleaseChannel>,
) -> impl Future<Output = Result<Url>> {
) -> impl Future<Output = Result<url::Url>> {
#[cfg(any(test, feature = "test-support"))]
let url_override = self.rpc_url.read().clone();
@@ -1117,7 +1124,7 @@ impl Client {
// for us from the RPC URL.
//
// Among other things, it will generate and set a `Sec-WebSocket-Key` header for us.
let mut request = rpc_url.into_client_request()?;
let mut request = IntoClientRequest::into_client_request(rpc_url.as_str())?;
// We then modify the request to add our desired headers.
let request_headers = request.headers_mut();
@@ -1156,6 +1163,7 @@ impl Client {
.with_root_certificates(root_store)
.with_no_client_auth()
};
let (stream, _) =
async_tungstenite::async_tls::client_async_tls_with_connector(
request,

View File

@@ -32,12 +32,12 @@ clickhouse.workspace = true
clock.workspace = true
collections.workspace = true
dashmap.workspace = true
derive_more.workspace = true
envy = "0.4.2"
futures.workspace = true
google_ai.workspace = true
hex.workspace = true
http_client.workspace = true
isahc_http_client.workspace = true
jsonwebtoken.workspace = true
live_kit_server.workspace = true
log.workspace = true
@@ -48,6 +48,7 @@ prometheus = "0.13"
prost.workspace = true
rand.workspace = true
reqwest = { version = "0.11", features = ["json"] }
reqwest_client.workspace = true
rpc.workspace = true
rustc-demangle.workspace = true
scrypt = "0.11"
@@ -66,7 +67,7 @@ telemetry_events.workspace = true
text.workspace = true
thiserror.workspace = true
time.workspace = true
tokio.workspace = true
tokio = { workspace = true, features = ["full"] }
toml.workspace = true
tower = "0.4"
tower-http = { workspace = true, features = ["trace"] }

View File

@@ -199,6 +199,12 @@ spec:
secretKeyRef:
name: slack
key: panics_webhook
- name: STRIPE_API_KEY
valueFrom:
secretKeyRef:
name: stripe
key: api_key
optional: true
- name: COMPLETE_WITH_LANGUAGE_MODEL_RATE_LIMIT_PER_HOUR
value: "1000"
- name: SUPERMAVEN_ADMIN_API_KEY

View File

@@ -422,6 +422,15 @@ CREATE TABLE dev_server_projects (
paths TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS billing_preferences (
id INTEGER PRIMARY KEY AUTOINCREMENT,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
user_id INTEGER NOT NULL REFERENCES users(id),
max_monthly_llm_usage_spending_in_cents INTEGER NOT NULL
);
CREATE UNIQUE INDEX "uix_billing_preferences_on_user_id" ON billing_preferences (user_id);
CREATE TABLE IF NOT EXISTS billing_customers (
id INTEGER PRIMARY KEY AUTOINCREMENT,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,

View File

@@ -0,0 +1,8 @@
create table if not exists billing_preferences (
id serial primary key,
created_at timestamp without time zone not null default now(),
user_id integer not null references users(id) on delete cascade,
max_monthly_llm_usage_spending_in_cents integer not null
);
create unique index "uix_billing_preferences_on_user_id" on billing_preferences (user_id);

View File

@@ -0,0 +1,12 @@
create table billing_events (
id serial primary key,
idempotency_key uuid not null default gen_random_uuid(),
user_id integer not null,
model_id integer not null references models (id) on delete cascade,
input_tokens bigint not null default 0,
input_cache_creation_tokens bigint not null default 0,
input_cache_read_tokens bigint not null default 0,
output_tokens bigint not null default 0
);
create index uix_billing_events_on_user_id_model_id on billing_events (user_id, model_id);

View File

@@ -1,7 +1,3 @@
use std::str::FromStr;
use std::sync::Arc;
use std::time::Duration;
use anyhow::{anyhow, bail, Context};
use axum::{
extract::{self, Query},
@@ -9,32 +5,43 @@ use axum::{
Extension, Json, Router,
};
use chrono::{DateTime, SecondsFormat, Utc};
use collections::HashSet;
use reqwest::StatusCode;
use sea_orm::ActiveValue;
use serde::{Deserialize, Serialize};
use std::{str::FromStr, sync::Arc, time::Duration};
use stripe::{
BillingPortalSession, CheckoutSession, CreateBillingPortalSession,
CreateBillingPortalSessionFlowData, CreateBillingPortalSessionFlowDataAfterCompletion,
BillingPortalSession, CreateBillingPortalSession, CreateBillingPortalSessionFlowData,
CreateBillingPortalSessionFlowDataAfterCompletion,
CreateBillingPortalSessionFlowDataAfterCompletionRedirect,
CreateBillingPortalSessionFlowDataType, CreateCheckoutSession, CreateCheckoutSessionLineItems,
CreateCustomer, Customer, CustomerId, EventObject, EventType, Expandable, ListEvents,
Subscription, SubscriptionId, SubscriptionStatus,
CreateBillingPortalSessionFlowDataType, CreateCustomer, Customer, CustomerId, EventObject,
EventType, Expandable, ListEvents, Subscription, SubscriptionId, SubscriptionStatus,
};
use util::ResultExt;
use crate::db::billing_subscription::{self, StripeSubscriptionStatus};
use crate::db::{
billing_customer, BillingSubscriptionId, CreateBillingCustomerParams,
CreateBillingSubscriptionParams, CreateProcessedStripeEventParams, UpdateBillingCustomerParams,
UpdateBillingSubscriptionParams,
use crate::llm::DEFAULT_MAX_MONTHLY_SPEND;
use crate::rpc::{ResultExt as _, Server};
use crate::{
db::{
billing_customer, BillingSubscriptionId, CreateBillingCustomerParams,
CreateBillingSubscriptionParams, CreateProcessedStripeEventParams,
UpdateBillingCustomerParams, UpdateBillingPreferencesParams,
UpdateBillingSubscriptionParams,
},
stripe_billing::StripeBilling,
};
use crate::{
db::{billing_subscription::StripeSubscriptionStatus, UserId},
llm::db::LlmDatabase,
};
use crate::llm::db::LlmDatabase;
use crate::llm::MONTHLY_SPENDING_LIMIT_IN_CENTS;
use crate::rpc::ResultExt as _;
use crate::{AppState, Error, Result};
pub fn router() -> Router {
Router::new()
.route(
"/billing/preferences",
get(get_billing_preferences).put(update_billing_preferences),
)
.route(
"/billing/subscriptions",
get(list_billing_subscriptions).post(create_billing_subscription),
@@ -45,6 +52,85 @@ pub fn router() -> Router {
)
}
#[derive(Debug, Deserialize)]
struct GetBillingPreferencesParams {
github_user_id: i32,
}
#[derive(Debug, Serialize)]
struct BillingPreferencesResponse {
max_monthly_llm_usage_spending_in_cents: i32,
}
async fn get_billing_preferences(
Extension(app): Extension<Arc<AppState>>,
Query(params): Query<GetBillingPreferencesParams>,
) -> Result<Json<BillingPreferencesResponse>> {
let user = app
.db
.get_user_by_github_user_id(params.github_user_id)
.await?
.ok_or_else(|| anyhow!("user not found"))?;
let preferences = app.db.get_billing_preferences(user.id).await?;
Ok(Json(BillingPreferencesResponse {
max_monthly_llm_usage_spending_in_cents: preferences
.map_or(DEFAULT_MAX_MONTHLY_SPEND.0 as i32, |preferences| {
preferences.max_monthly_llm_usage_spending_in_cents
}),
}))
}
#[derive(Debug, Deserialize)]
struct UpdateBillingPreferencesBody {
github_user_id: i32,
max_monthly_llm_usage_spending_in_cents: i32,
}
async fn update_billing_preferences(
Extension(app): Extension<Arc<AppState>>,
Extension(rpc_server): Extension<Arc<crate::rpc::Server>>,
extract::Json(body): extract::Json<UpdateBillingPreferencesBody>,
) -> Result<Json<BillingPreferencesResponse>> {
let user = app
.db
.get_user_by_github_user_id(body.github_user_id)
.await?
.ok_or_else(|| anyhow!("user not found"))?;
let billing_preferences =
if let Some(_billing_preferences) = app.db.get_billing_preferences(user.id).await? {
app.db
.update_billing_preferences(
user.id,
&UpdateBillingPreferencesParams {
max_monthly_llm_usage_spending_in_cents: ActiveValue::set(
body.max_monthly_llm_usage_spending_in_cents,
),
},
)
.await?
} else {
app.db
.create_billing_preferences(
user.id,
&crate::db::CreateBillingPreferencesParams {
max_monthly_llm_usage_spending_in_cents: body
.max_monthly_llm_usage_spending_in_cents,
},
)
.await?
};
rpc_server.refresh_llm_tokens_for_user(user.id).await;
Ok(Json(BillingPreferencesResponse {
max_monthly_llm_usage_spending_in_cents: billing_preferences
.max_monthly_llm_usage_spending_in_cents,
}))
}
#[derive(Debug, Deserialize)]
struct ListBillingSubscriptionsParams {
github_user_id: i32,
@@ -117,12 +203,22 @@ async fn create_billing_subscription(
.await?
.ok_or_else(|| anyhow!("user not found"))?;
let Some((stripe_client, stripe_price_id)) = app
.stripe_client
.clone()
.zip(app.config.stripe_llm_usage_price_id.clone())
else {
log::error!("failed to retrieve Stripe client or price ID");
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(
StatusCode::NOT_IMPLEMENTED,
"not supported".into(),
))?
};
let Some(llm_db) = app.llm_db.clone() else {
log::error!("failed to retrieve LLM database");
Err(Error::http(
StatusCode::NOT_IMPLEMENTED,
"not supported".into(),
@@ -146,26 +242,14 @@ async fn create_billing_subscription(
customer.id
};
let checkout_session = {
let mut params = CreateCheckoutSession::new();
params.mode = Some(stripe::CheckoutSessionMode::Subscription);
params.customer = Some(customer_id);
params.client_reference_id = Some(user.github_login.as_str());
params.line_items = Some(vec![CreateCheckoutSessionLineItems {
price: Some(stripe_price_id.to_string()),
quantity: Some(0),
..Default::default()
}]);
let success_url = format!("{}/account", app.config.zed_dot_dev_url());
params.success_url = Some(&success_url);
CheckoutSession::create(&stripe_client, params).await?
};
let default_model = llm_db.model(rpc::LanguageModelProvider::Anthropic, "claude-3-5-sonnet")?;
let stripe_model = stripe_billing.register_model(default_model).await?;
let success_url = format!("{}/account", app.config.zed_dot_dev_url());
let checkout_session_url = stripe_billing
.checkout(customer_id, &user.github_login, &stripe_model, &success_url)
.await?;
Ok(Json(CreateBillingSubscriptionResponse {
checkout_session_url: checkout_session
.url
.ok_or_else(|| anyhow!("no checkout session URL"))?,
checkout_session_url,
}))
}
@@ -320,7 +404,7 @@ const NUMBER_OF_ALREADY_PROCESSED_PAGES_BEFORE_WE_STOP: usize = 4;
/// Polls the Stripe events API periodically to reconcile the records in our
/// database with the data in Stripe.
pub fn poll_stripe_events_periodically(app: Arc<AppState>) {
pub fn poll_stripe_events_periodically(app: Arc<AppState>, rpc_server: Arc<Server>) {
let Some(stripe_client) = app.stripe_client.clone() else {
log::warn!("failed to retrieve Stripe client");
return;
@@ -331,7 +415,9 @@ pub fn poll_stripe_events_periodically(app: Arc<AppState>) {
let executor = executor.clone();
async move {
loop {
poll_stripe_events(&app, &stripe_client).await.log_err();
poll_stripe_events(&app, &rpc_server, &stripe_client)
.await
.log_err();
executor.sleep(POLL_EVENTS_INTERVAL).await;
}
@@ -341,6 +427,7 @@ pub fn poll_stripe_events_periodically(app: Arc<AppState>) {
async fn poll_stripe_events(
app: &Arc<AppState>,
rpc_server: &Arc<Server>,
stripe_client: &stripe::Client,
) -> anyhow::Result<()> {
fn event_type_to_string(event_type: EventType) -> String {
@@ -457,7 +544,7 @@ async fn poll_stripe_events(
| EventType::CustomerSubscriptionPaused
| EventType::CustomerSubscriptionResumed
| EventType::CustomerSubscriptionDeleted => {
handle_customer_subscription_event(app, stripe_client, event).await
handle_customer_subscription_event(app, rpc_server, stripe_client, event).await
}
_ => Ok(()),
};
@@ -525,6 +612,7 @@ async fn handle_customer_event(
async fn handle_customer_subscription_event(
app: &Arc<AppState>,
rpc_server: &Arc<Server>,
stripe_client: &stripe::Client,
event: stripe::Event,
) -> anyhow::Result<()> {
@@ -570,6 +658,12 @@ async fn handle_customer_subscription_event(
.await?;
}
// When the user's subscription changes, we want to refresh their LLM tokens
// to either grant/revoke access.
rpc_server
.refresh_llm_tokens_for_user(billing_customer.user_id)
.await;
Ok(())
}
@@ -635,15 +729,15 @@ async fn find_or_create_billing_customer(
Ok(Some(billing_customer))
}
const SYNC_LLM_USAGE_WITH_STRIPE_INTERVAL: Duration = Duration::from_secs(24 * 60 * 60);
const SYNC_LLM_USAGE_WITH_STRIPE_INTERVAL: Duration = Duration::from_secs(60);
pub fn sync_llm_usage_with_stripe_periodically(app: Arc<AppState>, llm_db: LlmDatabase) {
let Some(stripe_client) = app.stripe_client.clone() else {
log::warn!("failed to retrieve Stripe client");
pub fn sync_llm_usage_with_stripe_periodically(app: Arc<AppState>) {
let Some(stripe_billing) = app.stripe_billing.clone() else {
log::warn!("failed to retrieve Stripe billing object");
return;
};
let Some(stripe_llm_usage_price_id) = app.config.stripe_llm_usage_price_id.clone() else {
log::warn!("failed to retrieve Stripe LLM usage price ID");
let Some(llm_db) = app.llm_db.clone() else {
log::warn!("failed to retrieve LLM database");
return;
};
@@ -652,15 +746,9 @@ pub fn sync_llm_usage_with_stripe_periodically(app: Arc<AppState>, llm_db: LlmDa
let executor = executor.clone();
async move {
loop {
sync_with_stripe(
&app,
&llm_db,
&stripe_client,
stripe_llm_usage_price_id.clone(),
)
.await
.trace_err();
sync_with_stripe(&app, &llm_db, &stripe_billing)
.await
.trace_err();
executor.sleep(SYNC_LLM_USAGE_WITH_STRIPE_INTERVAL).await;
}
}
@@ -669,60 +757,44 @@ pub fn sync_llm_usage_with_stripe_periodically(app: Arc<AppState>, llm_db: LlmDa
async fn sync_with_stripe(
app: &Arc<AppState>,
llm_db: &LlmDatabase,
stripe_client: &stripe::Client,
stripe_llm_usage_price_id: Arc<str>,
llm_db: &Arc<LlmDatabase>,
stripe_billing: &Arc<StripeBilling>,
) -> anyhow::Result<()> {
let subscriptions = app.db.get_active_billing_subscriptions().await?;
let events = llm_db.get_billing_events().await?;
let user_ids = events
.iter()
.map(|(event, _)| event.user_id)
.collect::<HashSet<UserId>>();
let stripe_subscriptions = app.db.get_active_billing_subscriptions(user_ids).await?;
for (customer, subscription) in subscriptions {
update_stripe_subscription(
llm_db,
stripe_client,
&stripe_llm_usage_price_id,
customer,
subscription,
)
.await
.log_err();
for (event, model) in events {
let Some((stripe_db_customer, stripe_db_subscription)) =
stripe_subscriptions.get(&event.user_id)
else {
tracing::warn!(
user_id = event.user_id.0,
"Registered billing event for user who is not a Stripe customer. Billing events should only be created for users who are Stripe customers, so this is a mistake on our side."
);
continue;
};
let stripe_subscription_id: stripe::SubscriptionId = stripe_db_subscription
.stripe_subscription_id
.parse()
.context("failed to parse stripe subscription id from db")?;
let stripe_customer_id: stripe::CustomerId = stripe_db_customer
.stripe_customer_id
.parse()
.context("failed to parse stripe customer id from db")?;
let stripe_model = stripe_billing.register_model(&model).await?;
stripe_billing
.subscribe_to_model(&stripe_subscription_id, &stripe_model)
.await?;
stripe_billing
.bill_model_usage(&stripe_customer_id, &stripe_model, &event)
.await?;
llm_db.consume_billing_event(event.id).await?;
}
Ok(())
}
async fn update_stripe_subscription(
llm_db: &LlmDatabase,
stripe_client: &stripe::Client,
stripe_llm_usage_price_id: &Arc<str>,
customer: billing_customer::Model,
subscription: billing_subscription::Model,
) -> Result<(), anyhow::Error> {
let monthly_spending = llm_db
.get_user_spending_for_month(customer.user_id, Utc::now())
.await?;
let subscription_id = SubscriptionId::from_str(&subscription.stripe_subscription_id)
.context("failed to parse subscription ID")?;
let monthly_spending_over_free_tier =
monthly_spending.saturating_sub(MONTHLY_SPENDING_LIMIT_IN_CENTS);
let new_quantity = (monthly_spending_over_free_tier as f32 / 100.).ceil();
Subscription::update(
stripe_client,
&subscription_id,
stripe::UpdateSubscription {
items: Some(vec![stripe::UpdateSubscriptionItems {
// TODO: Do we need to send up the `id` if a subscription item
// with this price already exists, or will Stripe take care of
// it?
id: None,
price: Some(stripe_llm_usage_price_id.to_string()),
quantity: Some(new_quantity as u64),
..Default::default()
}]),
..Default::default()
},
)
.await?;
Ok(())
}

View File

@@ -670,7 +670,6 @@ pub struct EditorEventRow {
time: i64,
copilot_enabled: bool,
copilot_enabled_for_language: bool,
historical_event: bool,
architecture: String,
is_staff: Option<bool>,
major: Option<i32>,
@@ -718,7 +717,6 @@ impl EditorEventRow {
country_code: country_code.unwrap_or("XX".to_string()),
region_code: "".to_string(),
city: "".to_string(),
historical_event: false,
is_via_ssh: event.is_via_ssh,
}
}

View File

@@ -0,0 +1,78 @@
/// A number of cents.
#[derive(
Debug,
PartialEq,
Eq,
PartialOrd,
Ord,
Hash,
Clone,
Copy,
derive_more::Add,
derive_more::AddAssign,
)]
pub struct Cents(pub u32);
impl Cents {
pub const ZERO: Self = Self(0);
pub const fn new(cents: u32) -> Self {
Self(cents)
}
pub const fn from_dollars(dollars: u32) -> Self {
Self(dollars * 100)
}
pub fn saturating_sub(self, other: Cents) -> Self {
Self(self.0.saturating_sub(other.0))
}
}
#[cfg(test)]
mod tests {
use pretty_assertions::assert_eq;
use super::*;
#[test]
fn test_cents_new() {
assert_eq!(Cents::new(50), Cents(50));
}
#[test]
fn test_cents_from_dollars() {
assert_eq!(Cents::from_dollars(1), Cents(100));
assert_eq!(Cents::from_dollars(5), Cents(500));
}
#[test]
fn test_cents_zero() {
assert_eq!(Cents::ZERO, Cents(0));
}
#[test]
fn test_cents_add() {
assert_eq!(Cents(50) + Cents(30), Cents(80));
}
#[test]
fn test_cents_add_assign() {
let mut cents = Cents(50);
cents += Cents(30);
assert_eq!(cents, Cents(80));
}
#[test]
fn test_cents_saturating_sub() {
assert_eq!(Cents(50).saturating_sub(Cents(30)), Cents(20));
assert_eq!(Cents(30).saturating_sub(Cents(50)), Cents(0));
}
#[test]
fn test_cents_ordering() {
assert!(Cents(50) > Cents(30));
assert!(Cents(30) < Cents(50));
assert_eq!(Cents(50), Cents(50));
}
}

View File

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

View File

@@ -72,6 +72,7 @@ macro_rules! id_type {
id_type!(AccessTokenId);
id_type!(BillingCustomerId);
id_type!(BillingSubscriptionId);
id_type!(BillingPreferencesId);
id_type!(BufferId);
id_type!(ChannelBufferCollaboratorId);
id_type!(ChannelChatParticipantId);

View File

@@ -2,6 +2,7 @@ use super::*;
pub mod access_tokens;
pub mod billing_customers;
pub mod billing_preferences;
pub mod billing_subscriptions;
pub mod buffers;
pub mod channels;

View File

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

View File

@@ -114,23 +114,31 @@ impl Database {
pub async fn get_active_billing_subscriptions(
&self,
) -> Result<Vec<(billing_customer::Model, billing_subscription::Model)>> {
self.transaction(|tx| async move {
let mut result = Vec::new();
let mut rows = billing_subscription::Entity::find()
.inner_join(billing_customer::Entity)
.select_also(billing_customer::Entity)
.order_by_asc(billing_subscription::Column::Id)
.stream(&*tx)
.await?;
user_ids: HashSet<UserId>,
) -> Result<HashMap<UserId, (billing_customer::Model, billing_subscription::Model)>> {
self.transaction(|tx| {
let user_ids = user_ids.clone();
async move {
let mut rows = billing_subscription::Entity::find()
.inner_join(billing_customer::Entity)
.select_also(billing_customer::Entity)
.filter(billing_customer::Column::UserId.is_in(user_ids))
.filter(
billing_subscription::Column::StripeSubscriptionStatus
.eq(StripeSubscriptionStatus::Active),
)
.order_by_asc(billing_subscription::Column::Id)
.stream(&*tx)
.await?;
while let Some(row) = rows.next().await {
if let (subscription, Some(customer)) = row? {
result.push((customer, subscription));
let mut subscriptions = HashMap::default();
while let Some(row) = rows.next().await {
if let (subscription, Some(customer)) = row? {
subscriptions.insert(customer.user_id, (customer, subscription));
}
}
Ok(subscriptions)
}
Ok(result)
})
.await
}

View File

@@ -838,6 +838,7 @@ impl Database {
.map(|language_server| proto::LanguageServer {
id: language_server.id as u64,
name: language_server.name,
worktree_id: None,
})
.collect(),
dev_server_project_id: project.dev_server_project_id,

View File

@@ -718,6 +718,7 @@ impl Database {
.map(|language_server| proto::LanguageServer {
id: language_server.id as u64,
name: language_server.name,
worktree_id: None,
})
.collect::<Vec<_>>();

View File

@@ -1,5 +1,6 @@
pub mod access_token;
pub mod billing_customer;
pub mod billing_preference;
pub mod billing_subscription;
pub mod buffer;
pub mod buffer_operation;

View File

@@ -0,0 +1,30 @@
use crate::db::{BillingPreferencesId, UserId};
use sea_orm::entity::prelude::*;
#[derive(Clone, Debug, Default, PartialEq, Eq, DeriveEntityModel)]
#[sea_orm(table_name = "billing_preferences")]
pub struct Model {
#[sea_orm(primary_key)]
pub id: BillingPreferencesId,
pub created_at: DateTime,
pub user_id: UserId,
pub max_monthly_llm_usage_spending_in_cents: i32,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {
#[sea_orm(
belongs_to = "super::user::Entity",
from = "Column::UserId",
to = "super::user::Column::Id"
)]
User,
}
impl Related<super::user::Entity> for Entity {
fn to() -> RelationDef {
Relation::User.def()
}
}
impl ActiveModelBehavior for ActiveModel {}

View File

@@ -1,5 +1,6 @@
pub mod api;
pub mod auth;
mod cents;
pub mod clickhouse;
pub mod db;
pub mod env;
@@ -9,6 +10,7 @@ pub mod migrations;
mod rate_limiter;
pub mod rpc;
pub mod seed;
pub mod stripe_billing;
pub mod user_backfiller;
#[cfg(test)]
@@ -20,13 +22,17 @@ use axum::{
http::{HeaderMap, StatusCode},
response::IntoResponse,
};
pub use cents::*;
use db::{ChannelId, Database};
use executor::Executor;
use llm::db::LlmDatabase;
pub use rate_limiter::*;
use serde::Deserialize;
use std::{path::PathBuf, sync::Arc};
use util::ResultExt;
use crate::stripe_billing::StripeBilling;
pub type Result<T, E = Error> = std::result::Result<T, E>;
pub enum Error {
@@ -174,7 +180,6 @@ pub struct Config {
pub slack_panics_webhook: Option<String>,
pub auto_join_channel_id: Option<ChannelId>,
pub stripe_api_key: Option<String>,
pub stripe_llm_usage_price_id: Option<Arc<str>>,
pub supermaven_admin_api_key: Option<Arc<str>>,
pub user_backfiller_github_access_token: Option<Arc<str>>,
}
@@ -194,7 +199,7 @@ impl Config {
}
pub fn is_llm_billing_enabled(&self) -> bool {
self.stripe_llm_usage_price_id.is_some()
self.stripe_api_key.is_some()
}
#[cfg(test)]
@@ -235,7 +240,6 @@ impl Config {
migrations_path: None,
seed_path: None,
stripe_api_key: None,
stripe_llm_usage_price_id: None,
supermaven_admin_api_key: None,
user_backfiller_github_access_token: None,
}
@@ -268,9 +272,11 @@ impl ServiceMode {
pub struct AppState {
pub db: Arc<Database>,
pub llm_db: Option<Arc<LlmDatabase>>,
pub live_kit_client: Option<Arc<dyn live_kit_server::api::Client>>,
pub blob_store_client: Option<aws_sdk_s3::Client>,
pub stripe_client: Option<Arc<stripe::Client>>,
pub stripe_billing: Option<Arc<StripeBilling>>,
pub rate_limiter: Arc<RateLimiter>,
pub executor: Executor,
pub clickhouse_client: Option<::clickhouse::Client>,
@@ -284,6 +290,20 @@ impl AppState {
let mut db = Database::new(db_options, Executor::Production).await?;
db.initialize_notification_kinds().await?;
let llm_db = if let Some((llm_database_url, llm_database_max_connections)) = config
.llm_database_url
.clone()
.zip(config.llm_database_max_connections)
{
let mut llm_db_options = db::ConnectOptions::new(llm_database_url);
llm_db_options.max_connections(llm_database_max_connections);
let mut llm_db = LlmDatabase::new(llm_db_options, executor.clone()).await?;
llm_db.initialize().await?;
Some(Arc::new(llm_db))
} else {
None
};
let live_kit_client = if let Some(((server, key), secret)) = config
.live_kit_server
.as_ref()
@@ -300,11 +320,16 @@ impl AppState {
};
let db = Arc::new(db);
let stripe_client = build_stripe_client(&config).map(Arc::new).log_err();
let this = Self {
db: db.clone(),
llm_db,
live_kit_client,
blob_store_client: build_blob_store_client(&config).await.log_err(),
stripe_client: build_stripe_client(&config).await.map(Arc::new).log_err(),
stripe_billing: stripe_client
.clone()
.map(|stripe_client| Arc::new(StripeBilling::new(stripe_client))),
stripe_client,
rate_limiter: Arc::new(RateLimiter::new(db)),
executor,
clickhouse_client: config
@@ -317,12 +342,11 @@ impl AppState {
}
}
async fn build_stripe_client(config: &Config) -> anyhow::Result<stripe::Client> {
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"))?;
Ok(stripe::Client::new(api_key))
}

View File

@@ -4,7 +4,7 @@ mod telemetry;
mod token;
use crate::{
api::CloudflareIpCountryHeader, build_clickhouse_client, db::UserId, executor::Executor,
api::CloudflareIpCountryHeader, build_clickhouse_client, db::UserId, executor::Executor, Cents,
Config, Error, Result,
};
use anyhow::{anyhow, Context as _};
@@ -20,13 +20,14 @@ use axum::{
};
use chrono::{DateTime, Duration, Utc};
use collections::HashMap;
use db::TokenUsage;
use db::{usage_measure::UsageMeasure, ActiveUserCount, LlmDatabase};
use futures::{Stream, StreamExt as _};
use isahc_http_client::IsahcHttpClient;
use rpc::ListModelsResponse;
use reqwest_client::ReqwestClient;
use rpc::{
proto::Plan, LanguageModelProvider, PerformCompletionParams, EXPIRED_LLM_TOKEN_HEADER_NAME,
};
use rpc::{ListModelsResponse, MAX_LLM_MONTHLY_SPEND_REACHED_HEADER_NAME};
use std::{
pin::Pin,
sync::Arc,
@@ -43,7 +44,7 @@ pub struct LlmState {
pub config: Config,
pub executor: Executor,
pub db: Arc<LlmDatabase>,
pub http_client: IsahcHttpClient,
pub http_client: ReqwestClient,
pub clickhouse_client: Option<clickhouse::Client>,
active_user_count_by_model:
RwLock<HashMap<(LanguageModelProvider, String), (DateTime<Utc>, ActiveUserCount)>>,
@@ -69,11 +70,8 @@ impl LlmState {
let db = Arc::new(db);
let user_agent = format!("Zed Server/{}", env!("CARGO_PKG_VERSION"));
let http_client = IsahcHttpClient::builder()
.default_header("User-Agent", user_agent)
.build()
.map(IsahcHttpClient::from)
.context("failed to construct http client")?;
let http_client =
ReqwestClient::user_agent(&user_agent).context("failed to construct http client")?;
let this = Self {
executor,
@@ -418,10 +416,7 @@ async fn perform_completion(
claims,
provider: params.provider,
model,
input_tokens: 0,
output_tokens: 0,
cache_creation_input_tokens: 0,
cache_read_input_tokens: 0,
tokens: TokenUsage::default(),
inner_stream: stream,
})))
}
@@ -438,13 +433,18 @@ fn normalize_model_name(known_models: Vec<String>, name: String) -> String {
}
}
/// The maximum monthly spending an individual user can reach before they have to pay.
pub const MONTHLY_SPENDING_LIMIT_IN_CENTS: usize = 5 * 100;
/// The maximum monthly spending an individual user can reach on the free tier
/// before they have to pay.
pub const FREE_TIER_MONTHLY_SPENDING_LIMIT: Cents = Cents::from_dollars(10);
/// The default value to use for maximum spend per month if the user did not
/// explicitly set a maximum spend.
///
/// Used to prevent surprise bills.
pub const DEFAULT_MAX_MONTHLY_SPEND: Cents = Cents::from_dollars(10);
/// The maximum lifetime spending an individual user can reach before being cut off.
///
/// Represented in cents.
const LIFETIME_SPENDING_LIMIT_IN_CENTS: usize = 1_000 * 100;
const LIFETIME_SPENDING_LIMIT: Cents = Cents::from_dollars(1_000);
async fn check_usage_limit(
state: &Arc<LlmState>,
@@ -464,18 +464,31 @@ async fn check_usage_limit(
.await?;
if state.config.is_llm_billing_enabled() {
if usage.spending_this_month >= MONTHLY_SPENDING_LIMIT_IN_CENTS {
if !claims.has_llm_subscription.unwrap_or(false) {
if usage.spending_this_month >= FREE_TIER_MONTHLY_SPENDING_LIMIT {
if !claims.has_llm_subscription {
return Err(Error::http(
StatusCode::PAYMENT_REQUIRED,
"Maximum spending limit reached for this month.".to_string(),
));
}
if usage.spending_this_month >= Cents(claims.max_monthly_spend_in_cents) {
return Err(Error::Http(
StatusCode::FORBIDDEN,
"Maximum spending limit reached for this month.".to_string(),
[(
HeaderName::from_static(MAX_LLM_MONTHLY_SPEND_REACHED_HEADER_NAME),
HeaderValue::from_static("true"),
)]
.into_iter()
.collect(),
));
}
}
}
// TODO: Remove this once we've rolled out monthly spending limits.
if usage.lifetime_spending >= LIFETIME_SPENDING_LIMIT_IN_CENTS {
if usage.lifetime_spending >= LIFETIME_SPENDING_LIMIT {
return Err(Error::http(
StatusCode::FORBIDDEN,
"Maximum spending limit reached.".to_string(),
@@ -593,10 +606,7 @@ struct TokenCountingStream<S> {
claims: LlmTokenClaims,
provider: LanguageModelProvider,
model: String,
input_tokens: usize,
output_tokens: usize,
cache_creation_input_tokens: usize,
cache_read_input_tokens: usize,
tokens: TokenUsage,
inner_stream: S,
}
@@ -610,10 +620,10 @@ where
match Pin::new(&mut self.inner_stream).poll_next(cx) {
Poll::Ready(Some(Ok(mut chunk))) => {
chunk.bytes.push(b'\n');
self.input_tokens += chunk.input_tokens;
self.output_tokens += chunk.output_tokens;
self.cache_creation_input_tokens += chunk.cache_creation_input_tokens;
self.cache_read_input_tokens += chunk.cache_read_input_tokens;
self.tokens.input += chunk.input_tokens;
self.tokens.output += chunk.output_tokens;
self.tokens.input_cache_creation += chunk.cache_creation_input_tokens;
self.tokens.input_cache_read += chunk.cache_read_input_tokens;
Poll::Ready(Some(Ok(chunk.bytes)))
}
Poll::Ready(Some(Err(e))) => Poll::Ready(Some(Err(e))),
@@ -626,13 +636,11 @@ where
impl<S> Drop for TokenCountingStream<S> {
fn drop(&mut self) {
let state = self.state.clone();
let is_llm_billing_enabled = state.config.is_llm_billing_enabled();
let claims = self.claims.clone();
let provider = self.provider;
let model = std::mem::take(&mut self.model);
let input_token_count = self.input_tokens;
let output_token_count = self.output_tokens;
let cache_creation_input_token_count = self.cache_creation_input_tokens;
let cache_read_input_token_count = self.cache_read_input_tokens;
let tokens = self.tokens;
self.state.executor.spawn_detached(async move {
let usage = state
.db
@@ -641,10 +649,16 @@ impl<S> Drop for TokenCountingStream<S> {
claims.is_staff,
provider,
&model,
input_token_count,
cache_creation_input_token_count,
cache_read_input_token_count,
output_token_count,
tokens,
// We're passing `false` here if LLM billing is not enabled
// so that we don't write any records to the
// `billing_events` table until we're ready to bill users.
if is_llm_billing_enabled {
claims.has_llm_subscription
} else {
false
},
Cents(claims.max_monthly_spend_in_cents),
Utc::now(),
)
.await
@@ -674,24 +688,25 @@ impl<S> Drop for TokenCountingStream<S> {
},
model,
provider: provider.to_string(),
input_token_count: input_token_count as u64,
cache_creation_input_token_count: cache_creation_input_token_count
as u64,
cache_read_input_token_count: cache_read_input_token_count as u64,
output_token_count: output_token_count as u64,
input_token_count: tokens.input as u64,
cache_creation_input_token_count: tokens.input_cache_creation as u64,
cache_read_input_token_count: tokens.input_cache_read as u64,
output_token_count: tokens.output as u64,
requests_this_minute: usage.requests_this_minute as u64,
tokens_this_minute: usage.tokens_this_minute as u64,
tokens_this_day: usage.tokens_this_day as u64,
input_tokens_this_month: usage.input_tokens_this_month as u64,
input_tokens_this_month: usage.tokens_this_month.input as u64,
cache_creation_input_tokens_this_month: usage
.cache_creation_input_tokens_this_month
.tokens_this_month
.input_cache_creation
as u64,
cache_read_input_tokens_this_month: usage
.cache_read_input_tokens_this_month
.tokens_this_month
.input_cache_read
as u64,
output_tokens_this_month: usage.output_tokens_this_month as u64,
spending_this_month: usage.spending_this_month as u64,
lifetime_spending: usage.lifetime_spending as u64,
output_tokens_this_month: usage.tokens_this_month.output as u64,
spending_this_month: usage.spending_this_month.0 as u64,
lifetime_spending: usage.lifetime_spending.0 as u64,
},
)
.await

View File

@@ -20,7 +20,7 @@ use std::future::Future;
use std::sync::Arc;
use anyhow::anyhow;
pub use queries::usages::ActiveUserCount;
pub use queries::usages::{ActiveUserCount, TokenUsage};
use sea_orm::prelude::*;
pub use sea_orm::ConnectOptions;
use sea_orm::{

View File

@@ -3,8 +3,9 @@ use serde::{Deserialize, Serialize};
use crate::id_type;
id_type!(BillingEventId);
id_type!(ModelId);
id_type!(ProviderId);
id_type!(RevokedAccessTokenId);
id_type!(UsageId);
id_type!(UsageMeasureId);
id_type!(RevokedAccessTokenId);

View File

@@ -1,5 +1,6 @@
use super::*;
pub mod billing_events;
pub mod providers;
pub mod revoked_access_tokens;
pub mod usages;

View File

@@ -0,0 +1,31 @@
use super::*;
use crate::Result;
use anyhow::Context as _;
impl LlmDatabase {
pub async fn get_billing_events(&self) -> Result<Vec<(billing_event::Model, model::Model)>> {
self.transaction(|tx| async move {
let events_with_models = billing_event::Entity::find()
.find_also_related(model::Entity)
.all(&*tx)
.await?;
events_with_models
.into_iter()
.map(|(event, model)| {
let model =
model.context("could not find model associated with billing event")?;
Ok((event, model))
})
.collect()
})
.await
}
pub async fn consume_billing_event(&self, id: BillingEventId) -> Result<()> {
self.transaction(|tx| async move {
billing_event::Entity::delete_by_id(id).exec(&*tx).await?;
Ok(())
})
.await
}
}

View File

@@ -1,4 +1,5 @@
use crate::db::UserId;
use crate::llm::Cents;
use crate::{db::UserId, llm::FREE_TIER_MONTHLY_SPENDING_LIMIT};
use chrono::{Datelike, Duration};
use futures::StreamExt as _;
use rpc::LanguageModelProvider;
@@ -8,17 +9,28 @@ use strum::IntoEnumIterator as _;
use super::*;
#[derive(Debug, PartialEq, Clone, Copy, Default)]
pub struct TokenUsage {
pub input: usize,
pub input_cache_creation: usize,
pub input_cache_read: usize,
pub output: usize,
}
impl TokenUsage {
pub fn total(&self) -> usize {
self.input + self.input_cache_creation + self.input_cache_read + self.output
}
}
#[derive(Debug, PartialEq, Clone, Copy)]
pub struct Usage {
pub requests_this_minute: usize,
pub tokens_this_minute: usize,
pub tokens_this_day: usize,
pub input_tokens_this_month: usize,
pub cache_creation_input_tokens_this_month: usize,
pub cache_read_input_tokens_this_month: usize,
pub output_tokens_this_month: usize,
pub spending_this_month: usize,
pub lifetime_spending: usize,
pub tokens_this_month: TokenUsage,
pub spending_this_month: Cents,
pub lifetime_spending: Cents,
}
#[derive(Debug, PartialEq, Clone)]
@@ -144,7 +156,7 @@ impl LlmDatabase {
&self,
user_id: UserId,
now: DateTimeUtc,
) -> Result<usize> {
) -> Result<Cents> {
self.transaction(|tx| async move {
let month = now.date_naive().month() as i32;
let year = now.date_naive().year();
@@ -158,7 +170,7 @@ impl LlmDatabase {
)
.stream(&*tx)
.await?;
let mut monthly_spending_in_cents = 0;
let mut monthly_spending = Cents::ZERO;
while let Some(usage) = monthly_usages.next().await {
let usage = usage?;
@@ -166,7 +178,7 @@ impl LlmDatabase {
continue;
};
monthly_spending_in_cents += calculate_spending(
monthly_spending += calculate_spending(
model,
usage.input_tokens as usize,
usage.cache_creation_input_tokens as usize,
@@ -175,7 +187,7 @@ impl LlmDatabase {
);
}
Ok(monthly_spending_in_cents)
Ok(monthly_spending)
})
.await
}
@@ -238,7 +250,7 @@ impl LlmDatabase {
monthly_usage.output_tokens as usize,
)
} else {
0
Cents::ZERO
};
let lifetime_spending = if let Some(lifetime_usage) = &lifetime_usage {
calculate_spending(
@@ -249,25 +261,27 @@ impl LlmDatabase {
lifetime_usage.output_tokens as usize,
)
} else {
0
Cents::ZERO
};
Ok(Usage {
requests_this_minute,
tokens_this_minute,
tokens_this_day,
input_tokens_this_month: monthly_usage
.as_ref()
.map_or(0, |usage| usage.input_tokens as usize),
cache_creation_input_tokens_this_month: monthly_usage
.as_ref()
.map_or(0, |usage| usage.cache_creation_input_tokens as usize),
cache_read_input_tokens_this_month: monthly_usage
.as_ref()
.map_or(0, |usage| usage.cache_read_input_tokens as usize),
output_tokens_this_month: monthly_usage
.as_ref()
.map_or(0, |usage| usage.output_tokens as usize),
tokens_this_month: TokenUsage {
input: monthly_usage
.as_ref()
.map_or(0, |usage| usage.input_tokens as usize),
input_cache_creation: monthly_usage
.as_ref()
.map_or(0, |usage| usage.cache_creation_input_tokens as usize),
input_cache_read: monthly_usage
.as_ref()
.map_or(0, |usage| usage.cache_read_input_tokens as usize),
output: monthly_usage
.as_ref()
.map_or(0, |usage| usage.output_tokens as usize),
},
spending_this_month,
lifetime_spending,
})
@@ -282,10 +296,9 @@ impl LlmDatabase {
is_staff: bool,
provider: LanguageModelProvider,
model_name: &str,
input_token_count: usize,
cache_creation_input_tokens: usize,
cache_read_input_tokens: usize,
output_token_count: usize,
tokens: TokenUsage,
has_llm_subscription: bool,
max_monthly_spend: Cents,
now: DateTimeUtc,
) -> Result<Usage> {
self.transaction(|tx| async move {
@@ -312,10 +325,6 @@ impl LlmDatabase {
&tx,
)
.await?;
let total_token_count = input_token_count
+ cache_read_input_tokens
+ cache_creation_input_tokens
+ output_token_count;
let tokens_this_minute = self
.update_usage_for_measure(
user_id,
@@ -324,7 +333,7 @@ impl LlmDatabase {
&usages,
UsageMeasure::TokensPerMinute,
now,
total_token_count,
tokens.total(),
&tx,
)
.await?;
@@ -336,7 +345,7 @@ impl LlmDatabase {
&usages,
UsageMeasure::TokensPerDay,
now,
total_token_count,
tokens.total(),
&tx,
)
.await?;
@@ -360,18 +369,14 @@ impl LlmDatabase {
Some(usage) => {
monthly_usage::Entity::update(monthly_usage::ActiveModel {
id: ActiveValue::unchanged(usage.id),
input_tokens: ActiveValue::set(
usage.input_tokens + input_token_count as i64,
),
input_tokens: ActiveValue::set(usage.input_tokens + tokens.input as i64),
cache_creation_input_tokens: ActiveValue::set(
usage.cache_creation_input_tokens + cache_creation_input_tokens as i64,
usage.cache_creation_input_tokens + tokens.input_cache_creation as i64,
),
cache_read_input_tokens: ActiveValue::set(
usage.cache_read_input_tokens + cache_read_input_tokens as i64,
),
output_tokens: ActiveValue::set(
usage.output_tokens + output_token_count as i64,
usage.cache_read_input_tokens + tokens.input_cache_read as i64,
),
output_tokens: ActiveValue::set(usage.output_tokens + tokens.output as i64),
..Default::default()
})
.exec(&*tx)
@@ -383,12 +388,12 @@ impl LlmDatabase {
model_id: ActiveValue::set(model.id),
month: ActiveValue::set(month),
year: ActiveValue::set(year),
input_tokens: ActiveValue::set(input_token_count as i64),
input_tokens: ActiveValue::set(tokens.input as i64),
cache_creation_input_tokens: ActiveValue::set(
cache_creation_input_tokens as i64,
tokens.input_cache_creation as i64,
),
cache_read_input_tokens: ActiveValue::set(cache_read_input_tokens as i64),
output_tokens: ActiveValue::set(output_token_count as i64),
cache_read_input_tokens: ActiveValue::set(tokens.input_cache_read as i64),
output_tokens: ActiveValue::set(tokens.output as i64),
..Default::default()
}
.insert(&*tx)
@@ -404,6 +409,27 @@ impl LlmDatabase {
monthly_usage.output_tokens as usize,
);
if !is_staff
&& spending_this_month > FREE_TIER_MONTHLY_SPENDING_LIMIT
&& has_llm_subscription
&& spending_this_month <= max_monthly_spend
{
billing_event::ActiveModel {
id: ActiveValue::not_set(),
idempotency_key: ActiveValue::not_set(),
user_id: ActiveValue::set(user_id),
model_id: ActiveValue::set(model.id),
input_tokens: ActiveValue::set(tokens.input as i64),
input_cache_creation_tokens: ActiveValue::set(
tokens.input_cache_creation as i64,
),
input_cache_read_tokens: ActiveValue::set(tokens.input_cache_read as i64),
output_tokens: ActiveValue::set(tokens.output as i64),
}
.insert(&*tx)
.await?;
}
// Update lifetime usage
let lifetime_usage = lifetime_usage::Entity::find()
.filter(
@@ -418,18 +444,14 @@ impl LlmDatabase {
Some(usage) => {
lifetime_usage::Entity::update(lifetime_usage::ActiveModel {
id: ActiveValue::unchanged(usage.id),
input_tokens: ActiveValue::set(
usage.input_tokens + input_token_count as i64,
),
input_tokens: ActiveValue::set(usage.input_tokens + tokens.input as i64),
cache_creation_input_tokens: ActiveValue::set(
usage.cache_creation_input_tokens + cache_creation_input_tokens as i64,
usage.cache_creation_input_tokens + tokens.input_cache_creation as i64,
),
cache_read_input_tokens: ActiveValue::set(
usage.cache_read_input_tokens + cache_read_input_tokens as i64,
),
output_tokens: ActiveValue::set(
usage.output_tokens + output_token_count as i64,
usage.cache_read_input_tokens + tokens.input_cache_read as i64,
),
output_tokens: ActiveValue::set(usage.output_tokens + tokens.output as i64),
..Default::default()
})
.exec(&*tx)
@@ -439,12 +461,12 @@ impl LlmDatabase {
lifetime_usage::ActiveModel {
user_id: ActiveValue::set(user_id),
model_id: ActiveValue::set(model.id),
input_tokens: ActiveValue::set(input_token_count as i64),
input_tokens: ActiveValue::set(tokens.input as i64),
cache_creation_input_tokens: ActiveValue::set(
cache_creation_input_tokens as i64,
tokens.input_cache_creation as i64,
),
cache_read_input_tokens: ActiveValue::set(cache_read_input_tokens as i64),
output_tokens: ActiveValue::set(output_token_count as i64),
cache_read_input_tokens: ActiveValue::set(tokens.input_cache_read as i64),
output_tokens: ActiveValue::set(tokens.output as i64),
..Default::default()
}
.insert(&*tx)
@@ -464,11 +486,12 @@ impl LlmDatabase {
requests_this_minute,
tokens_this_minute,
tokens_this_day,
input_tokens_this_month: monthly_usage.input_tokens as usize,
cache_creation_input_tokens_this_month: monthly_usage.cache_creation_input_tokens
as usize,
cache_read_input_tokens_this_month: monthly_usage.cache_read_input_tokens as usize,
output_tokens_this_month: monthly_usage.output_tokens as usize,
tokens_this_month: TokenUsage {
input: monthly_usage.input_tokens as usize,
input_cache_creation: monthly_usage.cache_creation_input_tokens as usize,
input_cache_read: monthly_usage.cache_read_input_tokens as usize,
output: monthly_usage.output_tokens as usize,
},
spending_this_month,
lifetime_spending,
})
@@ -637,7 +660,7 @@ fn calculate_spending(
cache_creation_input_tokens_this_month: usize,
cache_read_input_tokens_this_month: usize,
output_tokens_this_month: usize,
) -> usize {
) -> Cents {
let input_token_cost =
input_tokens_this_month * model.price_per_million_input_tokens as usize / 1_000_000;
let cache_creation_input_token_cost = cache_creation_input_tokens_this_month
@@ -648,10 +671,11 @@ fn calculate_spending(
/ 1_000_000;
let output_token_cost =
output_tokens_this_month * model.price_per_million_output_tokens as usize / 1_000_000;
input_token_cost
let spending = input_token_cost
+ cache_creation_input_token_cost
+ cache_read_input_token_cost
+ output_token_cost
+ output_token_cost;
Cents::new(spending as u32)
}
const MINUTE_BUCKET_COUNT: usize = 12;

View File

@@ -1,3 +1,4 @@
pub mod billing_event;
pub mod lifetime_usage;
pub mod model;
pub mod monthly_usage;

View File

@@ -0,0 +1,37 @@
use crate::{
db::UserId,
llm::db::{BillingEventId, ModelId},
};
use sea_orm::entity::prelude::*;
#[derive(Clone, Debug, PartialEq, DeriveEntityModel)]
#[sea_orm(table_name = "billing_events")]
pub struct Model {
#[sea_orm(primary_key)]
pub id: BillingEventId,
pub idempotency_key: Uuid,
pub user_id: UserId,
pub model_id: ModelId,
pub input_tokens: i64,
pub input_cache_creation_tokens: i64,
pub input_cache_read_tokens: i64,
pub output_tokens: i64,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {
#[sea_orm(
belongs_to = "super::model::Entity",
from = "Column::ModelId",
to = "super::model::Column::Id"
)]
Model,
}
impl Related<super::model::Entity> for Entity {
fn to() -> RelationDef {
Relation::Model.def()
}
}
impl ActiveModelBehavior for ActiveModel {}

View File

@@ -29,6 +29,8 @@ pub enum Relation {
Provider,
#[sea_orm(has_many = "super::usage::Entity")]
Usages,
#[sea_orm(has_many = "super::billing_event::Entity")]
BillingEvents,
}
impl Related<super::provider::Entity> for Entity {
@@ -43,4 +45,10 @@ impl Related<super::usage::Entity> for Entity {
}
}
impl Related<super::billing_event::Entity> for Entity {
fn to() -> RelationDef {
Relation::BillingEvents.def()
}
}
impl ActiveModelBehavior for ActiveModel {}

View File

@@ -1,3 +1,4 @@
mod billing_tests;
mod provider_tests;
mod usage_tests;

View File

@@ -0,0 +1,158 @@
use crate::{
db::UserId,
llm::{
db::{
queries::{providers::ModelParams, usages::Usage},
LlmDatabase, TokenUsage,
},
FREE_TIER_MONTHLY_SPENDING_LIMIT,
},
test_llm_db, Cents,
};
use chrono::{DateTime, Utc};
use pretty_assertions::assert_eq;
use rpc::LanguageModelProvider;
test_llm_db!(
test_billing_limit_exceeded,
test_billing_limit_exceeded_postgres
);
async fn test_billing_limit_exceeded(db: &mut LlmDatabase) {
let provider = LanguageModelProvider::Anthropic;
let model = "fake-claude-limerick";
const PRICE_PER_MILLION_INPUT_TOKENS: i32 = 5;
const PRICE_PER_MILLION_OUTPUT_TOKENS: i32 = 5;
// Initialize the database and insert the model
db.initialize().await.unwrap();
db.insert_models(&[ModelParams {
provider,
name: model.to_string(),
max_requests_per_minute: 5,
max_tokens_per_minute: 10_000,
max_tokens_per_day: 50_000,
price_per_million_input_tokens: PRICE_PER_MILLION_INPUT_TOKENS,
price_per_million_output_tokens: PRICE_PER_MILLION_OUTPUT_TOKENS,
}])
.await
.unwrap();
// Set a fixed datetime for consistent testing
let now = DateTime::parse_from_rfc3339("2024-08-08T22:46:33Z")
.unwrap()
.with_timezone(&Utc);
let user_id = UserId::from_proto(123);
let max_monthly_spend = Cents::from_dollars(11);
// Record usage that brings us close to the limit but doesn't exceed it
// Let's say we use $10.50 worth of tokens
let tokens_to_use = 210_000_000; // This will cost $10.50 at $0.05 per 1 million tokens
let usage = TokenUsage {
input: tokens_to_use,
input_cache_creation: 0,
input_cache_read: 0,
output: 0,
};
// Verify that before we record any usage, there are 0 billing events
let billing_events = db.get_billing_events().await.unwrap();
assert_eq!(billing_events.len(), 0);
db.record_usage(
user_id,
false,
provider,
model,
usage,
true,
max_monthly_spend,
now,
)
.await
.unwrap();
// Verify the recorded usage and spending
let recorded_usage = db.get_usage(user_id, provider, model, now).await.unwrap();
// Verify that we exceeded the free tier usage
assert!(
recorded_usage.spending_this_month > FREE_TIER_MONTHLY_SPENDING_LIMIT,
"Expected spending to exceed free tier limit"
);
assert_eq!(
recorded_usage,
Usage {
requests_this_minute: 1,
tokens_this_minute: tokens_to_use,
tokens_this_day: tokens_to_use,
tokens_this_month: TokenUsage {
input: tokens_to_use,
input_cache_creation: 0,
input_cache_read: 0,
output: 0,
},
spending_this_month: Cents::new(1050),
lifetime_spending: Cents::new(1050),
}
);
// Verify that there is one `billing_event` record
let billing_events = db.get_billing_events().await.unwrap();
assert_eq!(billing_events.len(), 1);
let (billing_event, _model) = &billing_events[0];
assert_eq!(billing_event.user_id, user_id);
assert_eq!(billing_event.input_tokens, tokens_to_use as i64);
assert_eq!(billing_event.input_cache_creation_tokens, 0);
assert_eq!(billing_event.input_cache_read_tokens, 0);
assert_eq!(billing_event.output_tokens, 0);
let tokens_to_exceed = 20_000_000; // This will cost $1.00 more, pushing us from $10.50 to $11.50, which is over the $11 monthly maximum limit
let usage_exceeding = TokenUsage {
input: tokens_to_exceed,
input_cache_creation: 0,
input_cache_read: 0,
output: 0,
};
// This should still create a billing event as it's the first request that exceeds the limit
db.record_usage(
user_id,
false,
provider,
model,
usage_exceeding,
true,
max_monthly_spend,
now,
)
.await
.unwrap();
// Verify that there is still one billing record
let billing_events = db.get_billing_events().await.unwrap();
assert_eq!(billing_events.len(), 1);
// Verify the updated usage and spending
let updated_usage = db.get_usage(user_id, provider, model, now).await.unwrap();
assert_eq!(
updated_usage,
Usage {
requests_this_minute: 2,
tokens_this_minute: tokens_to_use + tokens_to_exceed,
tokens_this_day: tokens_to_use + tokens_to_exceed,
tokens_this_month: TokenUsage {
input: tokens_to_use + tokens_to_exceed,
input_cache_creation: 0,
input_cache_read: 0,
output: 0,
},
spending_this_month: Cents::new(1150),
lifetime_spending: Cents::new(1150),
}
);
}

View File

@@ -2,9 +2,9 @@ use crate::{
db::UserId,
llm::db::{
queries::{providers::ModelParams, usages::Usage},
LlmDatabase,
LlmDatabase, TokenUsage,
},
test_llm_db,
test_llm_db, Cents,
};
use chrono::{DateTime, Duration, Utc};
use pretty_assertions::assert_eq;
@@ -36,14 +36,42 @@ async fn test_tracking_usage(db: &mut LlmDatabase) {
let user_id = UserId::from_proto(123);
let now = t0;
db.record_usage(user_id, false, provider, model, 1000, 0, 0, 0, now)
.await
.unwrap();
db.record_usage(
user_id,
false,
provider,
model,
TokenUsage {
input: 1000,
input_cache_creation: 0,
input_cache_read: 0,
output: 0,
},
false,
Cents::ZERO,
now,
)
.await
.unwrap();
let now = t0 + Duration::seconds(10);
db.record_usage(user_id, false, provider, model, 2000, 0, 0, 0, now)
.await
.unwrap();
db.record_usage(
user_id,
false,
provider,
model,
TokenUsage {
input: 2000,
input_cache_creation: 0,
input_cache_read: 0,
output: 0,
},
false,
Cents::ZERO,
now,
)
.await
.unwrap();
let usage = db.get_usage(user_id, provider, model, now).await.unwrap();
assert_eq!(
@@ -52,12 +80,14 @@ async fn test_tracking_usage(db: &mut LlmDatabase) {
requests_this_minute: 2,
tokens_this_minute: 3000,
tokens_this_day: 3000,
input_tokens_this_month: 3000,
cache_creation_input_tokens_this_month: 0,
cache_read_input_tokens_this_month: 0,
output_tokens_this_month: 0,
spending_this_month: 0,
lifetime_spending: 0,
tokens_this_month: TokenUsage {
input: 3000,
input_cache_creation: 0,
input_cache_read: 0,
output: 0,
},
spending_this_month: Cents::ZERO,
lifetime_spending: Cents::ZERO,
}
);
@@ -69,19 +99,35 @@ async fn test_tracking_usage(db: &mut LlmDatabase) {
requests_this_minute: 1,
tokens_this_minute: 2000,
tokens_this_day: 3000,
input_tokens_this_month: 3000,
cache_creation_input_tokens_this_month: 0,
cache_read_input_tokens_this_month: 0,
output_tokens_this_month: 0,
spending_this_month: 0,
lifetime_spending: 0,
tokens_this_month: TokenUsage {
input: 3000,
input_cache_creation: 0,
input_cache_read: 0,
output: 0,
},
spending_this_month: Cents::ZERO,
lifetime_spending: Cents::ZERO,
}
);
let now = t0 + Duration::seconds(60);
db.record_usage(user_id, false, provider, model, 3000, 0, 0, 0, now)
.await
.unwrap();
db.record_usage(
user_id,
false,
provider,
model,
TokenUsage {
input: 3000,
input_cache_creation: 0,
input_cache_read: 0,
output: 0,
},
false,
Cents::ZERO,
now,
)
.await
.unwrap();
let usage = db.get_usage(user_id, provider, model, now).await.unwrap();
assert_eq!(
@@ -90,12 +136,14 @@ async fn test_tracking_usage(db: &mut LlmDatabase) {
requests_this_minute: 2,
tokens_this_minute: 5000,
tokens_this_day: 6000,
input_tokens_this_month: 6000,
cache_creation_input_tokens_this_month: 0,
cache_read_input_tokens_this_month: 0,
output_tokens_this_month: 0,
spending_this_month: 0,
lifetime_spending: 0,
tokens_this_month: TokenUsage {
input: 6000,
input_cache_creation: 0,
input_cache_read: 0,
output: 0,
},
spending_this_month: Cents::ZERO,
lifetime_spending: Cents::ZERO,
}
);
@@ -108,18 +156,34 @@ async fn test_tracking_usage(db: &mut LlmDatabase) {
requests_this_minute: 0,
tokens_this_minute: 0,
tokens_this_day: 5000,
input_tokens_this_month: 6000,
cache_creation_input_tokens_this_month: 0,
cache_read_input_tokens_this_month: 0,
output_tokens_this_month: 0,
spending_this_month: 0,
lifetime_spending: 0,
tokens_this_month: TokenUsage {
input: 6000,
input_cache_creation: 0,
input_cache_read: 0,
output: 0,
},
spending_this_month: Cents::ZERO,
lifetime_spending: Cents::ZERO,
}
);
db.record_usage(user_id, false, provider, model, 4000, 0, 0, 0, now)
.await
.unwrap();
db.record_usage(
user_id,
false,
provider,
model,
TokenUsage {
input: 4000,
input_cache_creation: 0,
input_cache_read: 0,
output: 0,
},
false,
Cents::ZERO,
now,
)
.await
.unwrap();
let usage = db.get_usage(user_id, provider, model, now).await.unwrap();
assert_eq!(
@@ -128,12 +192,14 @@ async fn test_tracking_usage(db: &mut LlmDatabase) {
requests_this_minute: 1,
tokens_this_minute: 4000,
tokens_this_day: 9000,
input_tokens_this_month: 10000,
cache_creation_input_tokens_this_month: 0,
cache_read_input_tokens_this_month: 0,
output_tokens_this_month: 0,
spending_this_month: 0,
lifetime_spending: 0,
tokens_this_month: TokenUsage {
input: 10000,
input_cache_creation: 0,
input_cache_read: 0,
output: 0,
},
spending_this_month: Cents::ZERO,
lifetime_spending: Cents::ZERO,
}
);
@@ -143,9 +209,23 @@ async fn test_tracking_usage(db: &mut LlmDatabase) {
.with_timezone(&Utc);
// Test cache creation input tokens
db.record_usage(user_id, false, provider, model, 1000, 500, 0, 0, now)
.await
.unwrap();
db.record_usage(
user_id,
false,
provider,
model,
TokenUsage {
input: 1000,
input_cache_creation: 500,
input_cache_read: 0,
output: 0,
},
false,
Cents::ZERO,
now,
)
.await
.unwrap();
let usage = db.get_usage(user_id, provider, model, now).await.unwrap();
assert_eq!(
@@ -154,19 +234,35 @@ async fn test_tracking_usage(db: &mut LlmDatabase) {
requests_this_minute: 1,
tokens_this_minute: 1500,
tokens_this_day: 1500,
input_tokens_this_month: 1000,
cache_creation_input_tokens_this_month: 500,
cache_read_input_tokens_this_month: 0,
output_tokens_this_month: 0,
spending_this_month: 0,
lifetime_spending: 0,
tokens_this_month: TokenUsage {
input: 1000,
input_cache_creation: 500,
input_cache_read: 0,
output: 0,
},
spending_this_month: Cents::ZERO,
lifetime_spending: Cents::ZERO,
}
);
// Test cache read input tokens
db.record_usage(user_id, false, provider, model, 1000, 0, 300, 0, now)
.await
.unwrap();
db.record_usage(
user_id,
false,
provider,
model,
TokenUsage {
input: 1000,
input_cache_creation: 0,
input_cache_read: 300,
output: 0,
},
false,
Cents::ZERO,
now,
)
.await
.unwrap();
let usage = db.get_usage(user_id, provider, model, now).await.unwrap();
assert_eq!(
@@ -175,12 +271,14 @@ async fn test_tracking_usage(db: &mut LlmDatabase) {
requests_this_minute: 2,
tokens_this_minute: 2800,
tokens_this_day: 2800,
input_tokens_this_month: 2000,
cache_creation_input_tokens_this_month: 500,
cache_read_input_tokens_this_month: 300,
output_tokens_this_month: 0,
spending_this_month: 0,
lifetime_spending: 0,
tokens_this_month: TokenUsage {
input: 2000,
input_cache_creation: 500,
input_cache_read: 300,
output: 0,
},
spending_this_month: Cents::ZERO,
lifetime_spending: Cents::ZERO,
}
);
}

View File

@@ -1,4 +1,8 @@
use crate::{db::UserId, Config};
use crate::llm::DEFAULT_MAX_MONTHLY_SPEND;
use crate::{
db::{billing_preference, UserId},
Config,
};
use anyhow::{anyhow, Result};
use chrono::Utc;
use jsonwebtoken::{DecodingKey, EncodingKey, Header, Validation};
@@ -16,22 +20,20 @@ pub struct LlmTokenClaims {
pub github_user_login: String,
pub is_staff: bool,
pub has_llm_closed_beta_feature_flag: bool,
// This field is temporarily optional so it can be added
// in a backwards-compatible way. We can make it required
// once all of the LLM tokens have cycled (~1 hour after
// this change has been deployed).
#[serde(default)]
pub has_llm_subscription: Option<bool>,
pub has_llm_subscription: bool,
pub max_monthly_spend_in_cents: u32,
pub plan: rpc::proto::Plan,
}
const LLM_TOKEN_LIFETIME: Duration = Duration::from_secs(60 * 60);
impl LlmTokenClaims {
#[allow(clippy::too_many_arguments)]
pub fn create(
user_id: UserId,
github_user_login: String,
is_staff: bool,
billing_preferences: Option<billing_preference::Model>,
has_llm_closed_beta_feature_flag: bool,
has_llm_subscription: bool,
plan: rpc::proto::Plan,
@@ -51,7 +53,11 @@ impl LlmTokenClaims {
github_user_login,
is_staff,
has_llm_closed_beta_feature_flag,
has_llm_subscription: Some(has_llm_subscription),
has_llm_subscription,
max_monthly_spend_in_cents: billing_preferences
.map_or(DEFAULT_MAX_MONTHLY_SPEND.0, |preferences| {
preferences.max_monthly_llm_usage_spending_in_cents as u32
}),
plan,
};

View File

@@ -111,6 +111,13 @@ async fn main() -> Result<()> {
let state = AppState::new(config, Executor::Production).await?;
if let Some(stripe_billing) = state.stripe_billing.clone() {
let executor = state.executor.clone();
executor.spawn_detached(async move {
stripe_billing.initialize().await.trace_err();
});
}
if mode.is_collab() {
state.db.purge_old_embeddings().await.trace_err();
RateLimiter::save_periodically(
@@ -125,6 +132,8 @@ async fn main() -> Result<()> {
let rpc_server = collab::rpc::Server::new(epoch, state.clone());
rpc_server.start().await?;
poll_stripe_events_periodically(state.clone(), rpc_server.clone());
app = app
.merge(collab::api::routes(rpc_server.clone()))
.merge(collab::rpc::routes(rpc_server.clone()));
@@ -133,7 +142,6 @@ async fn main() -> Result<()> {
}
if mode.is_api() {
poll_stripe_events_periodically(state.clone());
fetch_extensions_from_blob_store_periodically(state.clone());
spawn_user_backfiller(state.clone());
@@ -155,8 +163,9 @@ async fn main() -> Result<()> {
.await
.trace_err();
if let Some(llm_db) = llm_db {
sync_llm_usage_with_stripe_periodically(state.clone(), llm_db);
if let Some(mut llm_db) = llm_db {
llm_db.initialize().await?;
sync_llm_usage_with_stripe_periodically(state.clone());
}
app = app

View File

@@ -36,8 +36,8 @@ use collections::{HashMap, HashSet};
pub use connection_pool::{ConnectionPool, ZedVersion};
use core::fmt::{self, Debug, Formatter};
use http_client::HttpClient;
use isahc_http_client::IsahcHttpClient;
use open_ai::{OpenAiEmbeddingModel, OPEN_AI_API_URL};
use reqwest_client::ReqwestClient;
use sha2::Digest;
use supermaven_api::{CreateExternalUserRequest, SupermavenAdminApi};
@@ -469,9 +469,6 @@ impl Server {
.add_request_handler(user_handler(
forward_project_request_for_owner::<proto::TaskContextForLocation>,
))
.add_request_handler(user_handler(
forward_project_request_for_owner::<proto::TaskTemplates>,
))
.add_request_handler(user_handler(
forward_read_only_project_request::<proto::GetHover>,
))
@@ -964,8 +961,8 @@ impl Server {
tracing::info!("connection opened");
let user_agent = format!("Zed Server/{}", env!("CARGO_PKG_VERSION"));
let http_client = match IsahcHttpClient::builder().default_header("User-Agent", user_agent).build() {
Ok(http_client) => Arc::new(IsahcHttpClient::from(http_client)),
let http_client = match ReqwestClient::user_agent(&user_agent) {
Ok(http_client) => Arc::new(http_client),
Err(error) => {
tracing::error!(?error, "failed to create HTTP client");
return;
@@ -1221,6 +1218,15 @@ impl Server {
Ok(())
}
pub async fn refresh_llm_tokens_for_user(self: &Arc<Self>, user_id: UserId) {
let pool = self.connection_pool.lock();
for connection_id in pool.user_connection_ids(user_id) {
self.peer
.send(connection_id, proto::RefreshLlmToken {})
.trace_err();
}
}
pub async fn snapshot<'a>(self: &'a Arc<Self>) -> ServerSnapshot<'a> {
ServerSnapshot {
connection_pool: ConnectionPoolGuard {
@@ -4920,10 +4926,14 @@ async fn get_llm_api_token(
if Utc::now().naive_utc() - account_created_at < MIN_ACCOUNT_AGE_FOR_LLM_USE {
Err(anyhow!("account too young"))?
}
let billing_preferences = db.get_billing_preferences(user.id).await?;
let token = LlmTokenClaims::create(
user.id,
user.github_login.clone(),
session.is_staff(),
billing_preferences,
has_llm_closed_beta_feature_flag,
session.has_llm_subscription(&db).await?,
session.current_plan(&db).await?,

View File

@@ -0,0 +1,469 @@
use std::sync::Arc;
use crate::{llm, Cents, Result};
use anyhow::Context;
use chrono::Utc;
use collections::HashMap;
use serde::{Deserialize, Serialize};
use tokio::sync::RwLock;
pub struct StripeBilling {
state: RwLock<StripeBillingState>,
client: Arc<stripe::Client>,
}
#[derive(Default)]
struct StripeBillingState {
meters_by_event_name: HashMap<String, StripeMeter>,
price_ids_by_meter_id: HashMap<String, stripe::PriceId>,
}
pub struct StripeModel {
input_tokens_price: StripeBillingPrice,
input_cache_creation_tokens_price: StripeBillingPrice,
input_cache_read_tokens_price: StripeBillingPrice,
output_tokens_price: StripeBillingPrice,
}
struct StripeBillingPrice {
id: stripe::PriceId,
meter_event_name: String,
}
impl StripeBilling {
pub fn new(client: Arc<stripe::Client>) -> Self {
Self {
client,
state: RwLock::default(),
}
}
pub async fn initialize(&self) -> Result<()> {
log::info!("StripeBilling: initializing");
let mut state = self.state.write().await;
let (meters, prices) = futures::try_join!(
StripeMeter::list(&self.client),
stripe::Price::list(
&self.client,
&stripe::ListPrices {
limit: Some(100),
..Default::default()
}
)
)?;
for meter in meters.data {
state
.meters_by_event_name
.insert(meter.event_name.clone(), meter);
}
for price in prices.data {
if let Some(recurring) = price.recurring {
if let Some(meter) = recurring.meter {
state.price_ids_by_meter_id.insert(meter, price.id);
}
}
}
log::info!("StripeBilling: initialized");
Ok(())
}
pub async fn register_model(&self, model: &llm::db::model::Model) -> Result<StripeModel> {
let input_tokens_price = self
.get_or_insert_price(
&format!("model_{}/input_tokens", model.id),
&format!("{} (Input Tokens)", model.name),
Cents::new(model.price_per_million_input_tokens as u32),
)
.await?;
let input_cache_creation_tokens_price = self
.get_or_insert_price(
&format!("model_{}/input_cache_creation_tokens", model.id),
&format!("{} (Input Cache Creation Tokens)", model.name),
Cents::new(model.price_per_million_cache_creation_input_tokens as u32),
)
.await?;
let input_cache_read_tokens_price = self
.get_or_insert_price(
&format!("model_{}/input_cache_read_tokens", model.id),
&format!("{} (Input Cache Read Tokens)", model.name),
Cents::new(model.price_per_million_cache_read_input_tokens as u32),
)
.await?;
let output_tokens_price = self
.get_or_insert_price(
&format!("model_{}/output_tokens", model.id),
&format!("{} (Output Tokens)", model.name),
Cents::new(model.price_per_million_output_tokens as u32),
)
.await?;
Ok(StripeModel {
input_tokens_price,
input_cache_creation_tokens_price,
input_cache_read_tokens_price,
output_tokens_price,
})
}
async fn get_or_insert_price(
&self,
meter_event_name: &str,
price_description: &str,
price_per_million_tokens: Cents,
) -> Result<StripeBillingPrice> {
// Fast code path when the meter and the price already exist.
{
let state = self.state.read().await;
if let Some(meter) = state.meters_by_event_name.get(meter_event_name) {
if let Some(price_id) = state.price_ids_by_meter_id.get(&meter.id) {
return Ok(StripeBillingPrice {
id: price_id.clone(),
meter_event_name: meter_event_name.to_string(),
});
}
}
}
let mut state = self.state.write().await;
let meter = if let Some(meter) = state.meters_by_event_name.get(meter_event_name) {
meter.clone()
} else {
let meter = StripeMeter::create(
&self.client,
StripeCreateMeterParams {
default_aggregation: DefaultAggregation { formula: "sum" },
display_name: price_description.to_string(),
event_name: meter_event_name,
},
)
.await?;
state
.meters_by_event_name
.insert(meter_event_name.to_string(), meter.clone());
meter
};
let price_id = if let Some(price_id) = state.price_ids_by_meter_id.get(&meter.id) {
price_id.clone()
} else {
let price = stripe::Price::create(
&self.client,
stripe::CreatePrice {
active: Some(true),
billing_scheme: Some(stripe::PriceBillingScheme::PerUnit),
currency: stripe::Currency::USD,
currency_options: None,
custom_unit_amount: None,
expand: &[],
lookup_key: None,
metadata: None,
nickname: None,
product: None,
product_data: Some(stripe::CreatePriceProductData {
id: None,
active: Some(true),
metadata: None,
name: price_description.to_string(),
statement_descriptor: None,
tax_code: None,
unit_label: None,
}),
recurring: Some(stripe::CreatePriceRecurring {
aggregate_usage: None,
interval: stripe::CreatePriceRecurringInterval::Month,
interval_count: None,
trial_period_days: None,
usage_type: Some(stripe::CreatePriceRecurringUsageType::Metered),
meter: Some(meter.id.clone()),
}),
tax_behavior: None,
tiers: None,
tiers_mode: None,
transfer_lookup_key: None,
transform_quantity: None,
unit_amount: None,
unit_amount_decimal: Some(&format!(
"{:.12}",
price_per_million_tokens.0 as f64 / 1_000_000f64
)),
},
)
.await?;
state
.price_ids_by_meter_id
.insert(meter.id, price.id.clone());
price.id
};
Ok(StripeBillingPrice {
id: price_id,
meter_event_name: meter_event_name.to_string(),
})
}
pub async fn subscribe_to_model(
&self,
subscription_id: &stripe::SubscriptionId,
model: &StripeModel,
) -> Result<()> {
let subscription =
stripe::Subscription::retrieve(&self.client, &subscription_id, &[]).await?;
let mut items = Vec::new();
if !subscription_contains_price(&subscription, &model.input_tokens_price.id) {
items.push(stripe::UpdateSubscriptionItems {
price: Some(model.input_tokens_price.id.to_string()),
..Default::default()
});
}
if !subscription_contains_price(&subscription, &model.input_cache_creation_tokens_price.id)
{
items.push(stripe::UpdateSubscriptionItems {
price: Some(model.input_cache_creation_tokens_price.id.to_string()),
..Default::default()
});
}
if !subscription_contains_price(&subscription, &model.input_cache_read_tokens_price.id) {
items.push(stripe::UpdateSubscriptionItems {
price: Some(model.input_cache_read_tokens_price.id.to_string()),
..Default::default()
});
}
if !subscription_contains_price(&subscription, &model.output_tokens_price.id) {
items.push(stripe::UpdateSubscriptionItems {
price: Some(model.output_tokens_price.id.to_string()),
..Default::default()
});
}
if !items.is_empty() {
items.extend(subscription.items.data.iter().map(|item| {
stripe::UpdateSubscriptionItems {
id: Some(item.id.to_string()),
..Default::default()
}
}));
stripe::Subscription::update(
&self.client,
subscription_id,
stripe::UpdateSubscription {
items: Some(items),
..Default::default()
},
)
.await?;
}
Ok(())
}
pub async fn bill_model_usage(
&self,
customer_id: &stripe::CustomerId,
model: &StripeModel,
event: &llm::db::billing_event::Model,
) -> Result<()> {
let timestamp = Utc::now().timestamp();
if event.input_tokens > 0 {
StripeMeterEvent::create(
&self.client,
StripeCreateMeterEventParams {
identifier: &format!("input_tokens/{}", event.idempotency_key),
event_name: &model.input_tokens_price.meter_event_name,
payload: StripeCreateMeterEventPayload {
value: event.input_tokens as u64,
stripe_customer_id: customer_id,
},
timestamp: Some(timestamp),
},
)
.await?;
}
if event.input_cache_creation_tokens > 0 {
StripeMeterEvent::create(
&self.client,
StripeCreateMeterEventParams {
identifier: &format!("input_cache_creation_tokens/{}", event.idempotency_key),
event_name: &model.input_cache_creation_tokens_price.meter_event_name,
payload: StripeCreateMeterEventPayload {
value: event.input_cache_creation_tokens as u64,
stripe_customer_id: customer_id,
},
timestamp: Some(timestamp),
},
)
.await?;
}
if event.input_cache_read_tokens > 0 {
StripeMeterEvent::create(
&self.client,
StripeCreateMeterEventParams {
identifier: &format!("input_cache_read_tokens/{}", event.idempotency_key),
event_name: &model.input_cache_read_tokens_price.meter_event_name,
payload: StripeCreateMeterEventPayload {
value: event.input_cache_read_tokens as u64,
stripe_customer_id: customer_id,
},
timestamp: Some(timestamp),
},
)
.await?;
}
if event.output_tokens > 0 {
StripeMeterEvent::create(
&self.client,
StripeCreateMeterEventParams {
identifier: &format!("output_tokens/{}", event.idempotency_key),
event_name: &model.output_tokens_price.meter_event_name,
payload: StripeCreateMeterEventPayload {
value: event.output_tokens as u64,
stripe_customer_id: customer_id,
},
timestamp: Some(timestamp),
},
)
.await?;
}
Ok(())
}
pub async fn checkout(
&self,
customer_id: stripe::CustomerId,
github_login: &str,
model: &StripeModel,
success_url: &str,
) -> Result<String> {
let mut params = stripe::CreateCheckoutSession::new();
params.mode = Some(stripe::CheckoutSessionMode::Subscription);
params.customer = Some(customer_id);
params.client_reference_id = Some(github_login);
params.line_items = Some(
[
&model.input_tokens_price.id,
&model.input_cache_creation_tokens_price.id,
&model.input_cache_read_tokens_price.id,
&model.output_tokens_price.id,
]
.into_iter()
.map(|price_id| stripe::CreateCheckoutSessionLineItems {
price: Some(price_id.to_string()),
..Default::default()
})
.collect(),
);
params.success_url = Some(success_url);
let session = stripe::CheckoutSession::create(&self.client, params).await?;
Ok(session.url.context("no checkout session URL")?)
}
}
#[derive(Serialize)]
struct DefaultAggregation {
formula: &'static str,
}
#[derive(Serialize)]
struct StripeCreateMeterParams<'a> {
default_aggregation: DefaultAggregation,
display_name: String,
event_name: &'a str,
}
#[derive(Clone, Deserialize)]
struct StripeMeter {
id: String,
event_name: String,
}
impl StripeMeter {
pub fn create(
client: &stripe::Client,
params: StripeCreateMeterParams,
) -> stripe::Response<Self> {
client.post_form("/billing/meters", params)
}
pub fn list(client: &stripe::Client) -> stripe::Response<stripe::List<Self>> {
#[derive(Serialize)]
struct Params {
#[serde(skip_serializing_if = "Option::is_none")]
limit: Option<u64>,
}
client.get_query("/billing/meters", Params { limit: Some(100) })
}
}
#[derive(Deserialize)]
struct StripeMeterEvent {
identifier: String,
}
impl StripeMeterEvent {
pub async fn create(
client: &stripe::Client,
params: StripeCreateMeterEventParams<'_>,
) -> Result<Self, stripe::StripeError> {
let identifier = params.identifier;
match client.post_form("/billing/meter_events", params).await {
Ok(event) => Ok(event),
Err(stripe::StripeError::Stripe(error)) => {
if error.http_status == 400
&& error
.message
.as_ref()
.map_or(false, |message| message.contains(identifier))
{
Ok(Self {
identifier: identifier.to_string(),
})
} else {
Err(stripe::StripeError::Stripe(error))
}
}
Err(error) => Err(error),
}
}
}
#[derive(Serialize)]
struct StripeCreateMeterEventParams<'a> {
identifier: &'a str,
event_name: &'a str,
payload: StripeCreateMeterEventPayload<'a>,
timestamp: Option<i64>,
}
#[derive(Serialize)]
struct StripeCreateMeterEventPayload<'a> {
value: u64,
stripe_customer_id: &'a stripe::CustomerId,
}
fn subscription_contains_price(
subscription: &stripe::Subscription,
price_id: &stripe::PriceId,
) -> bool {
subscription.items.data.iter().any(|item| {
item.price
.as_ref()
.map_or(false, |price| price.id == *price_id)
})
}

View File

@@ -50,7 +50,7 @@ async fn test_channel_guests(
project_b.read_with(cx_b, |project, _| project.remote_id()),
Some(project_id),
);
assert!(project_b.read_with(cx_b, |project, _| project.is_read_only()));
assert!(project_b.read_with(cx_b, |project, cx| project.is_read_only(cx)));
assert!(project_b
.update(cx_b, |project, cx| {
let worktree_id = project.worktrees(cx).next().unwrap().read(cx).id();
@@ -103,7 +103,7 @@ async fn test_channel_guest_promotion(cx_a: &mut TestAppContext, cx_b: &mut Test
workspace.active_item_as::<Editor>(cx).unwrap(),
)
});
assert!(project_b.read_with(cx_b, |project, _| project.is_read_only()));
assert!(project_b.read_with(cx_b, |project, cx| project.is_read_only(cx)));
assert!(editor_b.update(cx_b, |e, cx| e.read_only(cx)));
assert!(room_b.read_with(cx_b, |room, _| !room.can_use_microphone()));
assert!(room_b
@@ -127,7 +127,7 @@ async fn test_channel_guest_promotion(cx_a: &mut TestAppContext, cx_b: &mut Test
cx_a.run_until_parked();
// project and buffers are now editable
assert!(project_b.read_with(cx_b, |project, _| !project.is_read_only()));
assert!(project_b.read_with(cx_b, |project, cx| !project.is_read_only(cx)));
assert!(editor_b.update(cx_b, |editor, cx| !editor.read_only(cx)));
// B sees themselves as muted, and can unmute.
@@ -153,7 +153,7 @@ async fn test_channel_guest_promotion(cx_a: &mut TestAppContext, cx_b: &mut Test
cx_a.run_until_parked();
// project and buffers are no longer editable
assert!(project_b.read_with(cx_b, |project, _| project.is_read_only()));
assert!(project_b.read_with(cx_b, |project, cx| project.is_read_only(cx)));
assert!(editor_b.update(cx_b, |editor, cx| editor.read_only(cx)));
assert!(room_b
.update(cx_b, |room, cx| room.share_microphone(cx))

View File

@@ -262,7 +262,7 @@ async fn test_dev_server_leave_room(
cx1.executor().run_until_parked();
let (workspace, cx2) = client2.active_workspace(cx2);
cx2.update(|cx| assert!(workspace.read(cx).project().read(cx).is_disconnected()));
cx2.update(|cx| assert!(workspace.read(cx).project().read(cx).is_disconnected(cx)));
}
#[gpui::test]
@@ -308,7 +308,7 @@ async fn test_dev_server_delete(
cx1.executor().run_until_parked();
let (workspace, cx2) = client2.active_workspace(cx2);
cx2.update(|cx| assert!(workspace.read(cx).project().read(cx).is_disconnected()));
cx2.update(|cx| assert!(workspace.read(cx).project().read(cx).is_disconnected(cx)));
cx1.update(|cx| {
dev_server_projects::Store::global(cx).update(cx, |store, _| {
@@ -418,12 +418,12 @@ async fn test_dev_server_refresh_access_token(
// Assert that the other client was disconnected
let (workspace, cx2) = client2.active_workspace(cx2);
cx2.update(|cx| assert!(workspace.read(cx).project().read(cx).is_disconnected()));
cx2.update(|cx| assert!(workspace.read(cx).project().read(cx).is_disconnected(cx)));
// Assert that the owner of the dev server does not see the dev server as online anymore
let (workspace, cx1) = client1.active_workspace(cx1);
cx1.update(|cx| {
assert!(workspace.read(cx).project().read(cx).is_disconnected());
assert!(workspace.read(cx).project().read(cx).is_disconnected(cx));
dev_server_projects::Store::global(cx).update(cx, |store, _| {
assert_eq!(
store.dev_servers().first().unwrap().status,

View File

@@ -114,7 +114,7 @@ async fn test_host_disconnect(
project_a.read_with(cx_a, |project, _| assert!(!project.is_shared()));
project_b.read_with(cx_b, |project, _| project.is_read_only());
project_b.read_with(cx_b, |project, cx| project.is_read_only(cx));
assert!(worktree_a.read_with(cx_a, |tree, _| !tree.has_update_observer()));
@@ -379,75 +379,51 @@ async fn test_collaborating_with_completion(cx_a: &mut TestAppContext, cx_b: &mu
.next()
.await
.unwrap();
cx_a.executor().finish_waiting();
// Open the buffer on the host.
let buffer_a = project_a
.update(cx_a, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))
.await
.unwrap();
cx_a.executor().run_until_parked();
buffer_a.read_with(cx_a, |buffer, _| {
assert_eq!(buffer.text(), "fn main() { a. }")
});
// Confirm a completion on the guest.
editor_b.update(cx_b, |editor, cx| {
assert!(editor.context_menu_visible());
editor.confirm_completion(&ConfirmCompletion { item_ix: Some(0) }, cx);
assert_eq!(editor.text(cx), "fn main() { a.first_method() }");
});
// Return a resolved completion from the host's language server.
// The resolved completion has an additional text edit.
fake_language_server.handle_request::<lsp::request::ResolveCompletionItem, _, _>(
|params, _| async move {
Ok(match params.label.as_str() {
"first_method(…)" => lsp::CompletionItem {
label: "first_method(…)".into(),
detail: Some("fn(&mut self, B) -> C".into()),
text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
new_text: "first_method($1)".to_string(),
range: lsp::Range::new(
lsp::Position::new(0, 14),
lsp::Position::new(0, 14),
),
})),
additional_text_edits: Some(vec![lsp::TextEdit {
new_text: "use d::SomeTrait;\n".to_string(),
range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
}]),
insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
..Default::default()
},
"second_method(…)" => lsp::CompletionItem {
label: "second_method(…)".into(),
detail: Some("fn(&mut self, C) -> D<E>".into()),
text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
new_text: "second_method()".to_string(),
range: lsp::Range::new(
lsp::Position::new(0, 14),
lsp::Position::new(0, 14),
),
})),
insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
additional_text_edits: Some(vec![lsp::TextEdit {
new_text: "use d::SomeTrait;\n".to_string(),
range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
}]),
..Default::default()
},
_ => panic!("unexpected completion label: {:?}", params.label),
assert_eq!(params.label, "first_method(…)");
Ok(lsp::CompletionItem {
label: "first_method(…)".into(),
detail: Some("fn(&mut self, B) -> C".into()),
text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
new_text: "first_method($1)".to_string(),
range: lsp::Range::new(lsp::Position::new(0, 14), lsp::Position::new(0, 14)),
})),
additional_text_edits: Some(vec![lsp::TextEdit {
new_text: "use d::SomeTrait;\n".to_string(),
range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
}]),
insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
..Default::default()
})
},
);
cx_a.executor().finish_waiting();
cx_a.executor().run_until_parked();
// Confirm a completion on the guest.
editor_b
.update(cx_b, |editor, cx| {
assert!(editor.context_menu_visible());
editor.confirm_completion(&ConfirmCompletion { item_ix: Some(0) }, cx)
})
.unwrap()
.await
.unwrap();
cx_a.executor().run_until_parked();
cx_b.executor().run_until_parked();
// The additional edit is applied.
cx_a.executor().run_until_parked();
buffer_a.read_with(cx_a, |buffer, _| {
assert_eq!(
buffer.text(),
@@ -540,15 +516,9 @@ async fn test_collaborating_with_completion(cx_a: &mut TestAppContext, cx_b: &mu
cx_b.executor().run_until_parked();
// When accepting the completion, the snippet is insert.
editor_b
.update(cx_b, |editor, cx| {
assert!(editor.context_menu_visible());
editor.confirm_completion(&ConfirmCompletion { item_ix: Some(0) }, cx)
})
.unwrap()
.await
.unwrap();
editor_b.update(cx_b, |editor, cx| {
assert!(editor.context_menu_visible());
editor.confirm_completion(&ConfirmCompletion { item_ix: Some(0) }, cx);
assert_eq!(
editor.text(cx),
"use d::SomeTrait;\nfn main() { a.first_method(); a.third_method(, , ) }"

View File

@@ -27,6 +27,7 @@ use language::{
use live_kit_client::MacOSDisplay;
use lsp::LanguageServerId;
use parking_lot::Mutex;
use project::lsp_store::FormatTarget;
use project::{
lsp_store::FormatTrigger, search::SearchQuery, search::SearchResult, DiagnosticSummary,
HoverBlockKind, Project, ProjectPath,
@@ -1389,7 +1390,7 @@ async fn test_unshare_project(
.unwrap();
executor.run_until_parked();
assert!(project_b.read_with(cx_b, |project, _| project.is_disconnected()));
assert!(project_b.read_with(cx_b, |project, cx| project.is_disconnected(cx)));
// Client C opens the project.
let project_c = client_c.join_remote_project(project_id, cx_c).await;
@@ -1402,7 +1403,7 @@ async fn test_unshare_project(
assert!(worktree_a.read_with(cx_a, |tree, _| !tree.has_update_observer()));
assert!(project_c.read_with(cx_c, |project, _| project.is_disconnected()));
assert!(project_c.read_with(cx_c, |project, cx| project.is_disconnected(cx)));
// Client C can open the project again after client A re-shares.
let project_id = active_call_a
@@ -1427,8 +1428,8 @@ async fn test_unshare_project(
project_a.read_with(cx_a, |project, _| assert!(!project.is_shared()));
project_c2.read_with(cx_c, |project, _| {
assert!(project.is_disconnected());
project_c2.read_with(cx_c, |project, cx| {
assert!(project.is_disconnected(cx));
assert!(project.collaborators().is_empty());
});
}
@@ -1560,8 +1561,8 @@ async fn test_project_reconnect(
assert_eq!(project.collaborators().len(), 1);
});
project_b1.read_with(cx_b, |project, _| {
assert!(!project.is_disconnected());
project_b1.read_with(cx_b, |project, cx| {
assert!(!project.is_disconnected(cx));
assert_eq!(project.collaborators().len(), 1);
});
@@ -1661,7 +1662,7 @@ async fn test_project_reconnect(
});
project_b1.read_with(cx_b, |project, cx| {
assert!(!project.is_disconnected());
assert!(!project.is_disconnected(cx));
assert_eq!(
project
.worktree_for_id(worktree1_id, cx)
@@ -1695,9 +1696,9 @@ async fn test_project_reconnect(
);
});
project_b2.read_with(cx_b, |project, _| assert!(project.is_disconnected()));
project_b2.read_with(cx_b, |project, cx| assert!(project.is_disconnected(cx)));
project_b3.read_with(cx_b, |project, _| assert!(!project.is_disconnected()));
project_b3.read_with(cx_b, |project, cx| assert!(!project.is_disconnected(cx)));
buffer_a1.read_with(cx_a, |buffer, _| assert_eq!(buffer.text(), "WaZ"));
@@ -1754,7 +1755,7 @@ async fn test_project_reconnect(
executor.run_until_parked();
project_b1.read_with(cx_b, |project, cx| {
assert!(!project.is_disconnected());
assert!(!project.is_disconnected(cx));
assert_eq!(
project
.worktree_for_id(worktree1_id, cx)
@@ -1788,7 +1789,7 @@ async fn test_project_reconnect(
);
});
project_b3.read_with(cx_b, |project, _| assert!(project.is_disconnected()));
project_b3.read_with(cx_b, |project, cx| assert!(project.is_disconnected(cx)));
buffer_a1.read_with(cx_a, |buffer, _| assert_eq!(buffer.text(), "WXaYZ"));
@@ -3816,8 +3817,8 @@ async fn test_leaving_project(
assert_eq!(project.collaborators().len(), 1);
});
project_b2.read_with(cx_b, |project, _| {
assert!(project.is_disconnected());
project_b2.read_with(cx_b, |project, cx| {
assert!(project.is_disconnected(cx));
});
project_c.read_with(cx_c, |project, _| {
@@ -3849,12 +3850,12 @@ async fn test_leaving_project(
assert_eq!(project.collaborators().len(), 0);
});
project_b2.read_with(cx_b, |project, _| {
assert!(project.is_disconnected());
project_b2.read_with(cx_b, |project, cx| {
assert!(project.is_disconnected(cx));
});
project_c.read_with(cx_c, |project, _| {
assert!(project.is_disconnected());
project_c.read_with(cx_c, |project, cx| {
assert!(project.is_disconnected(cx));
});
}
@@ -4417,6 +4418,7 @@ async fn test_formatting_buffer(
HashSet::from_iter([buffer_b.clone()]),
true,
FormatTrigger::Save,
FormatTarget::Buffer,
cx,
)
})
@@ -4450,6 +4452,7 @@ async fn test_formatting_buffer(
HashSet::from_iter([buffer_b.clone()]),
true,
FormatTrigger::Save,
FormatTarget::Buffer,
cx,
)
})
@@ -4555,6 +4558,7 @@ async fn test_prettier_formatting_buffer(
HashSet::from_iter([buffer_b.clone()]),
true,
FormatTrigger::Save,
FormatTarget::Buffer,
cx,
)
})
@@ -4574,6 +4578,7 @@ async fn test_prettier_formatting_buffer(
HashSet::from_iter([buffer_a.clone()]),
true,
FormatTrigger::Manual,
FormatTarget::Buffer,
cx,
)
})

View File

@@ -1168,7 +1168,7 @@ impl RandomizedTest for ProjectCollaborationTest {
Some((project, cx))
});
if !guest_project.is_disconnected() {
if !guest_project.is_disconnected(cx) {
if let Some((host_project, host_cx)) = host_project {
let host_worktree_snapshots =
host_project.read_with(host_cx, |host_project, cx| {
@@ -1254,8 +1254,8 @@ impl RandomizedTest for ProjectCollaborationTest {
let buffers = client.buffers().clone();
for (guest_project, guest_buffers) in &buffers {
let project_id = if guest_project.read_with(client_cx, |project, _| {
project.is_local() || project.is_disconnected()
let project_id = if guest_project.read_with(client_cx, |project, cx| {
project.is_local() || project.is_disconnected(cx)
}) {
continue;
} else {

View File

@@ -532,9 +532,9 @@ impl<T: RandomizedTest> TestPlan<T> {
server.allow_connections();
for project in client.dev_server_projects().iter() {
project.read_with(&client_cx, |project, _| {
project.read_with(&client_cx, |project, cx| {
assert!(
project.is_disconnected(),
project.is_disconnected(cx),
"project {:?} should be read only",
project.remote_id()
)

View File

@@ -2,10 +2,12 @@ use crate::tests::TestServer;
use call::ActiveCall;
use fs::{FakeFs, Fs as _};
use gpui::{Context as _, TestAppContext};
use language::language_settings::all_language_settings;
use http_client::BlockedHttpClient;
use language::{language_settings::all_language_settings, LanguageRegistry};
use node_runtime::NodeRuntime;
use project::ProjectPath;
use remote::SshRemoteClient;
use remote_server::HeadlessProject;
use remote_server::{HeadlessAppState, HeadlessProject};
use serde_json::json;
use std::{path::Path, sync::Arc};
@@ -48,8 +50,22 @@ async fn test_sharing_an_ssh_remote_project(
// User A connects to the remote project via SSH.
server_cx.update(HeadlessProject::init);
let _headless_project =
server_cx.new_model(|cx| HeadlessProject::new(server_ssh, remote_fs.clone(), cx));
let remote_http_client = Arc::new(BlockedHttpClient);
let node = NodeRuntime::unavailable();
let languages = Arc::new(LanguageRegistry::new(server_cx.executor()));
let _headless_project = server_cx.new_model(|cx| {
client::init_settings(cx);
HeadlessProject::new(
HeadlessAppState {
session: server_ssh,
fs: remote_fs.clone(),
http_client: remote_http_client,
node_runtime: node,
languages,
},
cx,
)
});
let (project_a, worktree_id) = client_a
.build_ssh_project("/code/project1", client_ssh, cx_a)

View File

@@ -635,9 +635,11 @@ impl TestServer {
) -> Arc<AppState> {
Arc::new(AppState {
db: test_db.db().clone(),
llm_db: None,
live_kit_client: Some(Arc::new(live_kit_test_server.create_api_client())),
blob_store_client: None,
stripe_client: None,
stripe_billing: None,
rate_limiter: Arc::new(RateLimiter::new(test_db.db().clone())),
executor,
clickhouse_client: None,
@@ -677,7 +679,6 @@ impl TestServer {
migrations_path: None,
seed_path: None,
stripe_api_key: None,
stripe_llm_usage_price_id: None,
supermaven_admin_api_key: None,
user_backfiller_github_access_token: None,
},

View File

@@ -111,7 +111,7 @@ impl MessageEditor {
editor.set_show_gutter(false, cx);
editor.set_show_wrap_guides(false, cx);
editor.set_show_indent_guides(false, cx);
editor.set_completion_provider(Box::new(MessageEditorCompletionProvider(this)));
editor.set_completion_provider(Some(Box::new(MessageEditorCompletionProvider(this))));
editor.set_auto_replace_emoji_shortcode(
MessageEditorSettings::get_global(cx)
.auto_replace_emoji_shortcode

View File

@@ -363,12 +363,10 @@ mod tests {
// Confirming a completion inserts it and hides the context menu, without showing
// the copilot suggestion afterwards.
editor.confirm_completion(&Default::default(), cx).unwrap()
})
.await
.unwrap();
cx.update_editor(|editor, cx| {
editor
.confirm_completion(&Default::default(), cx)
.unwrap()
.detach();
assert!(!editor.context_menu_visible());
assert!(!editor.has_active_inline_completion(cx));
assert_eq!(editor.text(cx), "one.completion_a\ntwo\nthree\n");

View File

@@ -237,6 +237,7 @@ gpui::actions!(
ToggleFold,
ToggleFoldRecursive,
Format,
FormatSelections,
GoToDeclaration,
GoToDeclarationSplit,
GoToDefinition,
@@ -294,6 +295,7 @@ gpui::actions!(
RevealInFileManager,
ReverseLines,
RevertFile,
ReloadFile,
RevertSelectedHunks,
Rewrap,
ScrollCursorBottom,

View File

@@ -1,4 +1,4 @@
use std::{ops::ControlFlow, time::Duration};
use std::time::Duration;
use futures::{channel::oneshot, FutureExt};
use gpui::{Task, ViewContext};
@@ -7,7 +7,7 @@ use crate::Editor;
pub struct DebouncedDelay {
task: Option<Task<()>>,
cancel_channel: Option<oneshot::Sender<ControlFlow<()>>>,
cancel_channel: Option<oneshot::Sender<()>>,
}
impl DebouncedDelay {
@@ -23,22 +23,17 @@ impl DebouncedDelay {
F: 'static + Send + FnOnce(&mut Editor, &mut ViewContext<Editor>) -> Task<()>,
{
if let Some(channel) = self.cancel_channel.take() {
channel.send(ControlFlow::Break(())).ok();
_ = channel.send(());
}
let (sender, mut receiver) = oneshot::channel::<ControlFlow<()>>();
let (sender, mut receiver) = oneshot::channel::<()>();
self.cancel_channel = Some(sender);
drop(self.task.take());
self.task = Some(cx.spawn(move |model, mut cx| async move {
let mut timer = cx.background_executor().timer(delay).fuse();
futures::select_biased! {
interrupt = receiver => {
match interrupt {
Ok(ControlFlow::Break(())) | Err(_) => return,
Ok(ControlFlow::Continue(())) => {},
}
}
_ = receiver => return,
_ = timer => {}
}
@@ -47,11 +42,4 @@ impl DebouncedDelay {
}
}));
}
pub fn start_now(&mut self) -> Option<Task<()>> {
if let Some(channel) = self.cancel_channel.take() {
channel.send(ControlFlow::Continue(())).ok();
}
self.task.take()
}
}

View File

@@ -96,7 +96,9 @@ use language::{
CursorShape, Diagnostic, Documentation, IndentKind, IndentSize, Language, OffsetRangeExt,
Point, Selection, SelectionGoal, TransactionId,
};
use language::{point_to_lsp, BufferRow, CharClassifier, Runnable, RunnableRange};
use language::{
point_to_lsp, BufferRow, CharClassifier, LanguageServerName, Runnable, RunnableRange,
};
use linked_editing_ranges::refresh_linked_ranges;
pub use proposed_changes_editor::{
ProposedChangesBuffer, ProposedChangesEditor, ProposedChangesEditorToolbar,
@@ -121,10 +123,11 @@ use multi_buffer::{
};
use ordered_float::OrderedFloat;
use parking_lot::{Mutex, RwLock};
use project::project_settings::{GitGutterSetting, ProjectSettings};
use project::{
lsp_store::FormatTrigger, CodeAction, Completion, CompletionIntent, Item, Location, Project,
ProjectPath, ProjectTransaction, TaskSourceKind,
lsp_store::{FormatTarget, FormatTrigger},
project_settings::{GitGutterSetting, ProjectSettings},
CodeAction, Completion, CompletionIntent, DocumentHighlight, InlayHint, Item, Location,
LocationLink, Project, ProjectPath, ProjectTransaction, TaskSourceKind,
};
use rand::prelude::*;
use rpc::{proto::*, ErrorExt};
@@ -160,11 +163,11 @@ use ui::{
};
use util::{defer, maybe, post_inc, RangeExt, ResultExt, TryFutureExt};
use workspace::item::{ItemHandle, PreviewTabsSettings};
use workspace::notifications::{DetachAndPromptErr, NotificationId};
use workspace::notifications::{DetachAndPromptErr, NotificationId, NotifyTaskExt};
use workspace::{
searchable::SearchEvent, ItemNavHistory, SplitDirection, ViewId, Workspace, WorkspaceId,
};
use workspace::{OpenInTerminal, OpenTerminal, TabBarSettings, Toast};
use workspace::{Item as WorkspaceItem, OpenInTerminal, OpenTerminal, TabBarSettings, Toast};
use crate::hover_links::find_url;
use crate::signature_help::{SignatureHelpHiddenBy, SignatureHelpState};
@@ -546,6 +549,7 @@ pub struct Editor {
active_diagnostics: Option<ActiveDiagnosticGroup>,
soft_wrap_mode_override: Option<language_settings::SoftWrap>,
project: Option<Model<Project>>,
semantics_provider: Option<Rc<dyn SemanticsProvider>>,
completion_provider: Option<Box<dyn CompletionProvider>>,
collaboration_hub: Option<Box<dyn CollaborationHub>>,
blink_manager: Model<BlinkManager>,
@@ -884,12 +888,12 @@ enum ContextMenu {
impl ContextMenu {
fn select_first(
&mut self,
project: Option<&Model<Project>>,
provider: Option<&dyn CompletionProvider>,
cx: &mut ViewContext<Editor>,
) -> bool {
if self.visible() {
match self {
ContextMenu::Completions(menu) => menu.select_first(project, cx),
ContextMenu::Completions(menu) => menu.select_first(provider, cx),
ContextMenu::CodeActions(menu) => menu.select_first(cx),
}
true
@@ -900,12 +904,12 @@ impl ContextMenu {
fn select_prev(
&mut self,
project: Option<&Model<Project>>,
provider: Option<&dyn CompletionProvider>,
cx: &mut ViewContext<Editor>,
) -> bool {
if self.visible() {
match self {
ContextMenu::Completions(menu) => menu.select_prev(project, cx),
ContextMenu::Completions(menu) => menu.select_prev(provider, cx),
ContextMenu::CodeActions(menu) => menu.select_prev(cx),
}
true
@@ -916,12 +920,12 @@ impl ContextMenu {
fn select_next(
&mut self,
project: Option<&Model<Project>>,
provider: Option<&dyn CompletionProvider>,
cx: &mut ViewContext<Editor>,
) -> bool {
if self.visible() {
match self {
ContextMenu::Completions(menu) => menu.select_next(project, cx),
ContextMenu::Completions(menu) => menu.select_next(provider, cx),
ContextMenu::CodeActions(menu) => menu.select_next(cx),
}
true
@@ -932,12 +936,12 @@ impl ContextMenu {
fn select_last(
&mut self,
project: Option<&Model<Project>>,
provider: Option<&dyn CompletionProvider>,
cx: &mut ViewContext<Editor>,
) -> bool {
if self.visible() {
match self {
ContextMenu::Completions(menu) => menu.select_last(project, cx),
ContextMenu::Completions(menu) => menu.select_last(provider, cx),
ContextMenu::CodeActions(menu) => menu.select_last(cx),
}
true
@@ -991,39 +995,55 @@ struct CompletionsMenu {
}
impl CompletionsMenu {
fn select_first(&mut self, project: Option<&Model<Project>>, cx: &mut ViewContext<Editor>) {
fn select_first(
&mut self,
provider: Option<&dyn CompletionProvider>,
cx: &mut ViewContext<Editor>,
) {
self.selected_item = 0;
self.scroll_handle.scroll_to_item(self.selected_item);
self.attempt_resolve_selected_completion_documentation(project, cx);
self.attempt_resolve_selected_completion_documentation(provider, cx);
cx.notify();
}
fn select_prev(&mut self, project: Option<&Model<Project>>, cx: &mut ViewContext<Editor>) {
fn select_prev(
&mut self,
provider: Option<&dyn CompletionProvider>,
cx: &mut ViewContext<Editor>,
) {
if self.selected_item > 0 {
self.selected_item -= 1;
} else {
self.selected_item = self.matches.len() - 1;
}
self.scroll_handle.scroll_to_item(self.selected_item);
self.attempt_resolve_selected_completion_documentation(project, cx);
self.attempt_resolve_selected_completion_documentation(provider, cx);
cx.notify();
}
fn select_next(&mut self, project: Option<&Model<Project>>, cx: &mut ViewContext<Editor>) {
fn select_next(
&mut self,
provider: Option<&dyn CompletionProvider>,
cx: &mut ViewContext<Editor>,
) {
if self.selected_item + 1 < self.matches.len() {
self.selected_item += 1;
} else {
self.selected_item = 0;
}
self.scroll_handle.scroll_to_item(self.selected_item);
self.attempt_resolve_selected_completion_documentation(project, cx);
self.attempt_resolve_selected_completion_documentation(provider, cx);
cx.notify();
}
fn select_last(&mut self, project: Option<&Model<Project>>, cx: &mut ViewContext<Editor>) {
fn select_last(
&mut self,
provider: Option<&dyn CompletionProvider>,
cx: &mut ViewContext<Editor>,
) {
self.selected_item = self.matches.len() - 1;
self.scroll_handle.scroll_to_item(self.selected_item);
self.attempt_resolve_selected_completion_documentation(project, cx);
self.attempt_resolve_selected_completion_documentation(provider, cx);
cx.notify();
}
@@ -1059,7 +1079,7 @@ impl CompletionsMenu {
fn attempt_resolve_selected_completion_documentation(
&mut self,
project: Option<&Model<Project>>,
provider: Option<&dyn CompletionProvider>,
cx: &mut ViewContext<Editor>,
) {
let settings = EditorSettings::get_global(cx);
@@ -1068,18 +1088,16 @@ impl CompletionsMenu {
}
let completion_index = self.matches[self.selected_item].candidate_id;
let Some(project) = project else {
let Some(provider) = provider else {
return;
};
let resolve_task = project.update(cx, |project, cx| {
project.resolve_completions(
self.buffer.clone(),
vec![completion_index],
self.completions.clone(),
cx,
)
});
let resolve_task = provider.resolve_completions(
self.buffer.clone(),
vec![completion_index],
self.completions.clone(),
cx,
);
let delay_ms =
EditorSettings::get_global(cx).completion_documentation_secondary_query_debounce;
@@ -1671,7 +1689,7 @@ pub(crate) struct NavigationData {
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum GotoDefinitionKind {
pub enum GotoDefinitionKind {
Symbol,
Declaration,
Type,
@@ -1879,10 +1897,17 @@ impl Editor {
}
}
}));
let task_inventory = project.read(cx).task_inventory().clone();
project_subscriptions.push(cx.observe(&task_inventory, |editor, _, cx| {
editor.tasks_update_task = Some(editor.refresh_runnables(cx));
}));
if let Some(task_inventory) = project
.read(cx)
.task_store()
.read(cx)
.task_inventory()
.cloned()
{
project_subscriptions.push(cx.observe(&task_inventory, |editor, _, cx| {
editor.tasks_update_task = Some(editor.refresh_runnables(cx));
}));
}
}
}
@@ -1930,6 +1955,7 @@ impl Editor {
active_diagnostics: None,
soft_wrap_mode_override,
completion_provider: project.clone().map(|project| Box::new(project) as _),
semantics_provider: project.clone().map(|project| Rc::new(project) as _),
collaboration_hub: project.clone().map(|project| Box::new(project) as _),
project,
blink_manager: blink_manager.clone(),
@@ -2298,8 +2324,16 @@ impl Editor {
self.custom_context_menu = Some(Box::new(f))
}
pub fn set_completion_provider(&mut self, provider: Box<dyn CompletionProvider>) {
self.completion_provider = Some(provider);
pub fn set_completion_provider(&mut self, provider: Option<Box<dyn CompletionProvider>>) {
self.completion_provider = provider;
}
pub fn semantics_provider(&self) -> Option<Rc<dyn SemanticsProvider>> {
self.semantics_provider.clone()
}
pub fn set_semantics_provider(&mut self, provider: Option<Rc<dyn SemanticsProvider>>) {
self.semantics_provider = provider;
}
pub fn set_inline_completion_provider<T>(
@@ -4034,7 +4068,7 @@ impl Editor {
}
fn refresh_inlay_hints(&mut self, reason: InlayHintRefreshReason, cx: &mut ViewContext<Self>) {
if self.project.is_none() || self.mode != EditorMode::Full {
if self.semantics_provider.is_none() || self.mode != EditorMode::Full {
return;
}
@@ -4427,49 +4461,16 @@ impl Editor {
&mut self,
item_ix: Option<usize>,
intent: CompletionIntent,
cx: &mut ViewContext<Self>,
) -> Option<Task<anyhow::Result<()>>> {
cx: &mut ViewContext<Editor>,
) -> Option<Task<std::result::Result<(), anyhow::Error>>> {
use language::ToOffset as _;
let completions_menu = if let ContextMenu::Completions(menu) = self.hide_context_menu(cx)? {
menu
} else {
return None;
};
let mut resolve_task_store = completions_menu
.selected_completion_documentation_resolve_debounce
.lock();
let selected_completion_resolve = resolve_task_store.start_now();
let menu_pre_resolve = self
.completion_documentation_pre_resolve_debounce
.start_now();
drop(resolve_task_store);
Some(cx.spawn(|editor, mut cx| async move {
match (selected_completion_resolve, menu_pre_resolve) {
(None, None) => {}
(Some(resolve), None) | (None, Some(resolve)) => resolve.await,
(Some(resolve_1), Some(resolve_2)) => {
futures::join!(resolve_1, resolve_2);
}
}
if let Some(apply_edits_task) = editor.update(&mut cx, |editor, cx| {
editor.apply_resolved_completion(completions_menu, item_ix, intent, cx)
})? {
apply_edits_task.await?;
}
Ok(())
}))
}
fn apply_resolved_completion(
&mut self,
completions_menu: CompletionsMenu,
item_ix: Option<usize>,
intent: CompletionIntent,
cx: &mut ViewContext<'_, Editor>,
) -> Option<Task<anyhow::Result<Option<language::Transaction>>>> {
use language::ToOffset as _;
let mat = completions_menu
.matches
.get(item_ix.unwrap_or(completions_menu.selected_item))?;
@@ -4628,7 +4629,11 @@ impl Editor {
// so we should automatically call signature_help
self.show_signature_help(&ShowSignatureHelp, cx);
}
Some(apply_edits)
Some(cx.foreground_executor().spawn(async move {
apply_edits.await?;
Ok(())
}))
}
pub fn toggle_code_actions(&mut self, action: &ToggleCodeActions, cx: &mut ViewContext<Self>) {
@@ -4717,11 +4722,13 @@ impl Editor {
);
}
project.update(cx, |project, cx| {
project.task_context_for_location(
captured_task_variables,
location,
cx,
)
project.task_store().update(cx, |task_store, cx| {
task_store.task_context_for_location(
captured_task_variables,
location,
cx,
)
})
})
});
@@ -4933,6 +4940,11 @@ impl Editor {
Ok(())
}
pub fn clear_code_action_providers(&mut self) {
self.code_action_providers.clear();
self.available_code_actions.take();
}
pub fn push_code_action_provider(
&mut self,
provider: Arc<dyn CodeActionProvider>,
@@ -5020,7 +5032,7 @@ impl Editor {
return None;
}
let project = self.project.clone()?;
let provider = self.semantics_provider.clone()?;
let buffer = self.buffer.read(cx);
let newest_selection = self.selections.newest_anchor().clone();
let cursor_position = newest_selection.head();
@@ -5036,11 +5048,12 @@ impl Editor {
.timer(DOCUMENT_HIGHLIGHTS_DEBOUNCE_TIMEOUT)
.await;
let highlights = if let Some(highlights) = project
.update(&mut cx, |project, cx| {
project.document_highlights(&cursor_buffer, cursor_buffer_position, cx)
let highlights = if let Some(highlights) = cx
.update(|cx| {
provider.document_highlights(&cursor_buffer, cursor_buffer_position, cx)
})
.log_err()
.ok()
.flatten()
{
highlights.await.log_err()
} else {
@@ -6230,6 +6243,13 @@ impl Editor {
}
}
pub fn reload_file(&mut self, _: &ReloadFile, cx: &mut ViewContext<Self>) {
let Some(project) = self.project.clone() else {
return;
};
self.reload(project, cx).detach_and_notify_err(cx);
}
pub fn revert_selected_hunks(&mut self, _: &RevertSelectedHunks, cx: &mut ViewContext<Self>) {
let revert_changes = self.gather_revert_changes(&self.selections.disjoint_anchors(), cx);
if !revert_changes.is_empty() {
@@ -7462,7 +7482,7 @@ impl Editor {
.context_menu
.write()
.as_mut()
.map(|menu| menu.select_first(self.project.as_ref(), cx))
.map(|menu| menu.select_first(self.completion_provider.as_deref(), cx))
.unwrap_or(false)
{
return;
@@ -7571,7 +7591,7 @@ impl Editor {
.context_menu
.write()
.as_mut()
.map(|menu| menu.select_last(self.project.as_ref(), cx))
.map(|menu| menu.select_last(self.completion_provider.as_deref(), cx))
.unwrap_or(false)
{
return;
@@ -7623,25 +7643,25 @@ impl Editor {
pub fn context_menu_first(&mut self, _: &ContextMenuFirst, cx: &mut ViewContext<Self>) {
if let Some(context_menu) = self.context_menu.write().as_mut() {
context_menu.select_first(self.project.as_ref(), cx);
context_menu.select_first(self.completion_provider.as_deref(), cx);
}
}
pub fn context_menu_prev(&mut self, _: &ContextMenuPrev, cx: &mut ViewContext<Self>) {
if let Some(context_menu) = self.context_menu.write().as_mut() {
context_menu.select_prev(self.project.as_ref(), cx);
context_menu.select_prev(self.completion_provider.as_deref(), cx);
}
}
pub fn context_menu_next(&mut self, _: &ContextMenuNext, cx: &mut ViewContext<Self>) {
if let Some(context_menu) = self.context_menu.write().as_mut() {
context_menu.select_next(self.project.as_ref(), cx);
context_menu.select_next(self.completion_provider.as_deref(), cx);
}
}
pub fn context_menu_last(&mut self, _: &ContextMenuLast, cx: &mut ViewContext<Self>) {
if let Some(context_menu) = self.context_menu.write().as_mut() {
context_menu.select_last(self.project.as_ref(), cx);
context_menu.select_last(self.completion_provider.as_deref(), cx);
}
}
@@ -9134,23 +9154,29 @@ impl Editor {
.map(|file| (file.worktree_id(cx), file.clone()))
.unzip();
(project.task_inventory().clone(), worktree_id, file)
(
project.task_store().read(cx).task_inventory().cloned(),
worktree_id,
file,
)
});
let inventory = inventory.read(cx);
let tags = mem::take(&mut runnable.tags);
let mut tags: Vec<_> = tags
.into_iter()
.flat_map(|tag| {
let tag = tag.0.clone();
inventory
.list_tasks(
file.clone(),
Some(runnable.language.clone()),
worktree_id,
cx,
)
.as_ref()
.into_iter()
.flat_map(|inventory| {
inventory.read(cx).list_tasks(
file.clone(),
Some(runnable.language.clone()),
worktree_id,
cx,
)
})
.filter(move |(_, template)| {
template.tags.iter().any(|source_tag| source_tag == &tag)
})
@@ -9608,7 +9634,7 @@ impl Editor {
split: bool,
cx: &mut ViewContext<Self>,
) -> Task<Result<Navigated>> {
let Some(workspace) = self.workspace() else {
let Some(provider) = self.semantics_provider.clone() else {
return Task::ready(Ok(Navigated::No));
};
let buffer = self.buffer.read(cx);
@@ -9619,13 +9645,9 @@ impl Editor {
return Task::ready(Ok(Navigated::No));
};
let project = workspace.read(cx).project().clone();
let definitions = project.update(cx, |project, cx| match kind {
GotoDefinitionKind::Symbol => project.definition(&buffer, head, cx),
GotoDefinitionKind::Declaration => project.declaration(&buffer, head, cx),
GotoDefinitionKind::Type => project.type_definition(&buffer, head, cx),
GotoDefinitionKind::Implementation => project.implementation(&buffer, head, cx),
});
let Some(definitions) = provider.definitions(&buffer, head, kind, cx) else {
return Task::ready(Ok(Navigated::No));
};
cx.spawn(|editor, mut cx| async move {
let definitions = definitions.await?;
@@ -9682,9 +9704,7 @@ impl Editor {
return;
};
let Some(project) = self.project.clone() else {
return;
};
let project = self.project.clone();
cx.spawn(|_, mut cx| async move {
let result = find_file(&buffer, project, buffer_position, &mut cx).await;
@@ -9875,21 +9895,19 @@ impl Editor {
&self,
lsp_location: lsp::Location,
server_id: LanguageServerId,
cx: &mut ViewContext<Editor>,
cx: &mut ViewContext<Self>,
) -> Task<anyhow::Result<Option<Location>>> {
let Some(project) = self.project.clone() else {
return Task::Ready(Some(Ok(None)));
};
cx.spawn(move |editor, mut cx| async move {
let location_task = editor.update(&mut cx, |editor, cx| {
let location_task = editor.update(&mut cx, |_, cx| {
project.update(cx, |project, cx| {
let language_server_name =
editor.buffer.read(cx).as_singleton().and_then(|buffer| {
project
.language_server_for_buffer(buffer.read(cx), server_id, cx)
.map(|(lsp_adapter, _)| lsp_adapter.name.clone())
});
let language_server_name = project
.language_server_statuses(cx)
.find(|(id, _)| server_id == *id)
.map(|(_, status)| LanguageServerName::from(status.name.as_str()));
language_server_name.map(|language_server_name| {
project.open_local_buffer_via_lsp(
lsp_location.uri.clone(),
@@ -10086,7 +10104,7 @@ impl Editor {
pub fn rename(&mut self, _: &Rename, cx: &mut ViewContext<Self>) -> Option<Task<Result<()>>> {
use language::ToOffset as _;
let project = self.project.clone()?;
let provider = self.semantics_provider.clone()?;
let selection = self.selections.newest_anchor().clone();
let (cursor_buffer, cursor_buffer_position) = self
.buffer
@@ -10103,9 +10121,9 @@ impl Editor {
let snapshot = cursor_buffer.read(cx).snapshot();
let cursor_buffer_offset = cursor_buffer_position.to_offset(&snapshot);
let cursor_buffer_offset_end = cursor_buffer_position_end.to_offset(&snapshot);
let prepare_rename = project.update(cx, |project, cx| {
project.prepare_rename(cursor_buffer.clone(), cursor_buffer_offset, cx)
});
let prepare_rename = provider
.range_for_rename(&cursor_buffer, cursor_buffer_position, cx)
.unwrap_or_else(|| Task::ready(Ok(None)));
drop(snapshot);
Some(cx.spawn(|this, mut cx| async move {
@@ -10276,32 +10294,28 @@ impl Editor {
cx: &mut ViewContext<Self>,
) -> Option<Task<Result<()>>> {
let rename = self.take_rename(false, cx)?;
let workspace = self.workspace()?;
let (start_buffer, start) = self
let workspace = self.workspace()?.downgrade();
let (buffer, start) = self
.buffer
.read(cx)
.text_anchor_for_position(rename.range.start, cx)?;
let (end_buffer, end) = self
let (end_buffer, _) = self
.buffer
.read(cx)
.text_anchor_for_position(rename.range.end, cx)?;
if start_buffer != end_buffer {
if buffer != end_buffer {
return None;
}
let buffer = start_buffer;
let range = start..end;
let old_name = rename.old_name;
let new_name = rename.editor.read(cx).text(cx);
let rename = workspace
.read(cx)
.project()
.clone()
.update(cx, |project, cx| {
project.perform_rename(buffer.clone(), range.start, new_name.clone(), true, cx)
});
let workspace = workspace.downgrade();
let rename = self.semantics_provider.as_ref()?.perform_rename(
&buffer,
start,
new_name.clone(),
cx,
)?;
Some(cx.spawn(|editor, mut cx| async move {
let project_transaction = rename.await?;
@@ -10372,13 +10386,39 @@ impl Editor {
None => return None,
};
Some(self.perform_format(project, FormatTrigger::Manual, cx))
Some(self.perform_format(project, FormatTrigger::Manual, FormatTarget::Buffer, cx))
}
fn format_selections(
&mut self,
_: &FormatSelections,
cx: &mut ViewContext<Self>,
) -> Option<Task<Result<()>>> {
let project = match &self.project {
Some(project) => project.clone(),
None => return None,
};
let selections = self
.selections
.all_adjusted(cx)
.into_iter()
.filter(|s| !s.is_empty())
.collect_vec();
Some(self.perform_format(
project,
FormatTrigger::Manual,
FormatTarget::Ranges(selections),
cx,
))
}
fn perform_format(
&mut self,
project: Model<Project>,
trigger: FormatTrigger,
target: FormatTarget,
cx: &mut ViewContext<Self>,
) -> Task<Result<()>> {
let buffer = self.buffer().clone();
@@ -10388,7 +10428,9 @@ impl Editor {
}
let mut timeout = cx.background_executor().timer(FORMAT_TIMEOUT).fuse();
let format = project.update(cx, |project, cx| project.format(buffers, true, trigger, cx));
let format = project.update(cx, |project, cx| {
project.format(buffers, true, trigger, target, cx)
});
cx.spawn(|_, mut cx| async move {
let transaction = futures::select_biased! {
@@ -12356,14 +12398,22 @@ impl Editor {
let mut new_selections_by_buffer = HashMap::default();
for selection in self.selections.all::<usize>(cx) {
for (buffer, mut range, _) in
buffer.range_to_buffer_ranges(selection.start..selection.end, cx)
for (mut buffer_handle, mut range, _) in
buffer.range_to_buffer_ranges(selection.range(), cx)
{
// When editing branch buffers, jump to the corresponding location
// in their base buffer.
let buffer = buffer_handle.read(cx);
if let Some(base_buffer) = buffer.diff_base_buffer() {
range = buffer.range_to_version(range, &base_buffer.read(cx).version());
buffer_handle = base_buffer;
}
if selection.reversed {
mem::swap(&mut range.start, &mut range.end);
}
new_selections_by_buffer
.entry(buffer)
.entry(buffer_handle)
.or_insert(Vec::new())
.push(range)
}
@@ -12648,24 +12698,13 @@ impl Editor {
}
pub fn supports_inlay_hints(&self, cx: &AppContext) -> bool {
let Some(project) = self.project.as_ref() else {
let Some(provider) = self.semantics_provider.as_ref() else {
return false;
};
let project = project.read(cx);
let mut supports = false;
self.buffer().read(cx).for_each_buffer(|buffer| {
if !supports {
supports = project
.language_servers_for_buffer(buffer.read(cx), cx)
.any(
|(_, server)| match server.capabilities().inlay_hint_provider {
Some(lsp::OneOf::Left(enabled)) => enabled,
Some(lsp::OneOf::Right(_)) => true,
None => false,
},
)
}
supports |= provider.supports_inlay_hints(buffer, cx);
});
supports
}
@@ -12931,6 +12970,62 @@ impl CollaborationHub for Model<Project> {
}
}
pub trait SemanticsProvider {
fn hover(
&self,
buffer: &Model<Buffer>,
position: text::Anchor,
cx: &mut AppContext,
) -> Option<Task<Vec<project::Hover>>>;
fn inlay_hints(
&self,
buffer_handle: Model<Buffer>,
range: Range<text::Anchor>,
cx: &mut AppContext,
) -> Option<Task<anyhow::Result<Vec<InlayHint>>>>;
fn resolve_inlay_hint(
&self,
hint: InlayHint,
buffer_handle: Model<Buffer>,
server_id: LanguageServerId,
cx: &mut AppContext,
) -> Option<Task<anyhow::Result<InlayHint>>>;
fn supports_inlay_hints(&self, buffer: &Model<Buffer>, cx: &AppContext) -> bool;
fn document_highlights(
&self,
buffer: &Model<Buffer>,
position: text::Anchor,
cx: &mut AppContext,
) -> Option<Task<Result<Vec<DocumentHighlight>>>>;
fn definitions(
&self,
buffer: &Model<Buffer>,
position: text::Anchor,
kind: GotoDefinitionKind,
cx: &mut AppContext,
) -> Option<Task<Result<Vec<LocationLink>>>>;
fn range_for_rename(
&self,
buffer: &Model<Buffer>,
position: text::Anchor,
cx: &mut AppContext,
) -> Option<Task<Result<Option<Range<text::Anchor>>>>>;
fn perform_rename(
&self,
buffer: &Model<Buffer>,
position: text::Anchor,
new_name: String,
cx: &mut AppContext,
) -> Option<Task<Result<ProjectTransaction>>>;
}
pub trait CompletionProvider {
fn completions(
&self,
@@ -13027,18 +13122,11 @@ fn snippet_completions(
return vec![];
}
let snapshot = buffer.read(cx).text_snapshot();
let chunks = snapshot.reversed_chunks_in_range(text::Anchor::MIN..buffer_position);
let mut lines = chunks.lines();
let Some(line_at) = lines.next().filter(|line| !line.is_empty()) else {
return vec![];
};
let chars = snapshot.reversed_chars_for_range(text::Anchor::MIN..buffer_position);
let scope = language.map(|language| language.default_scope());
let classifier = CharClassifier::new(scope).for_completion(true);
let mut last_word = line_at
.chars()
.rev()
let mut last_word = chars
.take_while(|c| classifier.is_word(*c))
.collect::<String>();
last_word = last_word.chars().rev().collect();
@@ -13182,6 +13270,102 @@ impl CompletionProvider for Model<Project> {
}
}
impl SemanticsProvider for Model<Project> {
fn hover(
&self,
buffer: &Model<Buffer>,
position: text::Anchor,
cx: &mut AppContext,
) -> Option<Task<Vec<project::Hover>>> {
Some(self.update(cx, |project, cx| project.hover(buffer, position, cx)))
}
fn document_highlights(
&self,
buffer: &Model<Buffer>,
position: text::Anchor,
cx: &mut AppContext,
) -> Option<Task<Result<Vec<DocumentHighlight>>>> {
Some(self.update(cx, |project, cx| {
project.document_highlights(buffer, position, cx)
}))
}
fn definitions(
&self,
buffer: &Model<Buffer>,
position: text::Anchor,
kind: GotoDefinitionKind,
cx: &mut AppContext,
) -> Option<Task<Result<Vec<LocationLink>>>> {
Some(self.update(cx, |project, cx| match kind {
GotoDefinitionKind::Symbol => project.definition(&buffer, position, cx),
GotoDefinitionKind::Declaration => project.declaration(&buffer, position, cx),
GotoDefinitionKind::Type => project.type_definition(&buffer, position, cx),
GotoDefinitionKind::Implementation => project.implementation(&buffer, position, cx),
}))
}
fn supports_inlay_hints(&self, buffer: &Model<Buffer>, cx: &AppContext) -> bool {
// TODO: make this work for remote projects
self.read(cx)
.language_servers_for_buffer(buffer.read(cx), cx)
.any(
|(_, server)| match server.capabilities().inlay_hint_provider {
Some(lsp::OneOf::Left(enabled)) => enabled,
Some(lsp::OneOf::Right(_)) => true,
None => false,
},
)
}
fn inlay_hints(
&self,
buffer_handle: Model<Buffer>,
range: Range<text::Anchor>,
cx: &mut AppContext,
) -> Option<Task<anyhow::Result<Vec<InlayHint>>>> {
Some(self.update(cx, |project, cx| {
project.inlay_hints(buffer_handle, range, cx)
}))
}
fn resolve_inlay_hint(
&self,
hint: InlayHint,
buffer_handle: Model<Buffer>,
server_id: LanguageServerId,
cx: &mut AppContext,
) -> Option<Task<anyhow::Result<InlayHint>>> {
Some(self.update(cx, |project, cx| {
project.resolve_inlay_hint(hint, buffer_handle, server_id, cx)
}))
}
fn range_for_rename(
&self,
buffer: &Model<Buffer>,
position: text::Anchor,
cx: &mut AppContext,
) -> Option<Task<Result<Option<Range<text::Anchor>>>>> {
Some(self.update(cx, |project, cx| {
project.prepare_rename(buffer.clone(), position, cx)
}))
}
fn perform_rename(
&self,
buffer: &Model<Buffer>,
position: text::Anchor,
new_name: String,
cx: &mut AppContext,
) -> Option<Task<Result<ProjectTransaction>>> {
Some(self.update(cx, |project, cx| {
project.perform_rename(buffer.clone(), position, new_name, cx)
}))
}
}
fn inlay_hint_settings(
location: Anchor,
snapshot: &MultiBufferSnapshot,
@@ -13475,6 +13659,7 @@ pub enum EditorEvent {
TransactionBegun {
transaction_id: clock::Lamport,
},
Reloaded,
CursorShapeChanged,
}

View File

@@ -179,7 +179,7 @@ pub struct EditorSettingsContent {
/// Default: true
pub cursor_blink: Option<bool>,
/// Cursor shape for the default editor.
/// Can be "bar", "block", "underscore", or "hollow".
/// Can be "bar", "block", "underline", or "hollow".
///
/// Default: None
pub cursor_shape: Option<CursorShape>,

View File

@@ -7076,7 +7076,12 @@ async fn test_document_format_manual_trigger(cx: &mut gpui::TestAppContext) {
let format = editor
.update(cx, |editor, cx| {
editor.perform_format(project.clone(), FormatTrigger::Manual, cx)
editor.perform_format(
project.clone(),
FormatTrigger::Manual,
FormatTarget::Buffer,
cx,
)
})
.unwrap();
fake_server
@@ -7112,7 +7117,7 @@ async fn test_document_format_manual_trigger(cx: &mut gpui::TestAppContext) {
});
let format = editor
.update(cx, |editor, cx| {
editor.perform_format(project, FormatTrigger::Manual, cx)
editor.perform_format(project, FormatTrigger::Manual, FormatTarget::Buffer, cx)
})
.unwrap();
cx.executor().advance_clock(super::FORMAT_TIMEOUT);
@@ -7996,7 +8001,7 @@ async fn test_completion(cx: &mut gpui::TestAppContext) {
.unwrap()
});
cx.assert_editor_state(indoc! {"
one.ˇ
one.second_completionˇ
two
three
"});
@@ -8029,9 +8034,9 @@ async fn test_completion(cx: &mut gpui::TestAppContext) {
cx.assert_editor_state(indoc! {"
one.second_completionˇ
two
thoverlapping additional editree
additional edit"});
three
additional edit
"});
cx.set_state(indoc! {"
one.second_completion
@@ -8091,8 +8096,8 @@ async fn test_completion(cx: &mut gpui::TestAppContext) {
});
cx.assert_editor_state(indoc! {"
one.second_completion
two siˇ
three siˇ
two sixth_completionˇ
three sixth_completionˇ
additional edit
"});
@@ -8133,11 +8138,9 @@ async fn test_completion(cx: &mut gpui::TestAppContext) {
.confirm_completion(&ConfirmCompletion::default(), cx)
.unwrap()
});
cx.assert_editor_state("editor.cloˇ");
cx.assert_editor_state("editor.closeˇ");
handle_resolve_completion_request(&mut cx, None).await;
apply_additional_edits.await.unwrap();
cx.assert_editor_state(indoc! {"
editor.closeˇ"});
}
#[gpui::test]
@@ -10142,7 +10145,7 @@ async fn test_completions_with_additional_edits(cx: &mut gpui::TestAppContext) {
.confirm_completion(&ConfirmCompletion::default(), cx)
.unwrap()
});
cx.assert_editor_state(indoc! {"fn main() { let a = 2.ˇ; }"});
cx.assert_editor_state(indoc! {"fn main() { let a = 2.Some(2)ˇ; }"});
cx.handle_request::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
let task_completion_item = completion_item.clone();
@@ -10311,7 +10314,12 @@ async fn test_document_format_with_prettier(cx: &mut gpui::TestAppContext) {
editor
.update(cx, |editor, cx| {
editor.perform_format(project.clone(), FormatTrigger::Manual, cx)
editor.perform_format(
project.clone(),
FormatTrigger::Manual,
FormatTarget::Buffer,
cx,
)
})
.unwrap()
.await;
@@ -10325,7 +10333,12 @@ async fn test_document_format_with_prettier(cx: &mut gpui::TestAppContext) {
settings.defaults.formatter = Some(language_settings::SelectedFormatter::Auto)
});
let format = editor.update(cx, |editor, cx| {
editor.perform_format(project.clone(), FormatTrigger::Manual, cx)
editor.perform_format(
project.clone(),
FormatTrigger::Manual,
FormatTarget::Buffer,
cx,
)
});
format.await.unwrap();
assert_eq!(

View File

@@ -64,7 +64,7 @@ use std::{
sync::Arc,
};
use sum_tree::Bias;
use theme::{ActiveTheme, PlayerColor};
use theme::{ActiveTheme, Appearance, PlayerColor};
use ui::prelude::*;
use ui::{h_flex, ButtonLike, ButtonStyle, ContextMenu, Tooltip};
use util::RangeExt;
@@ -376,6 +376,13 @@ impl EditorElement {
cx.propagate();
}
});
register_action(view, cx, |editor, action, cx| {
if let Some(task) = editor.format_selections(action, cx) {
task.detach_and_log_err(cx);
} else {
cx.propagate();
}
});
register_action(view, cx, Editor::restart_language_server);
register_action(view, cx, Editor::cancel_language_server_work);
register_action(view, cx, Editor::show_character_palette);
@@ -437,7 +444,8 @@ impl EditorElement {
register_action(view, cx, Editor::revert_file);
register_action(view, cx, Editor::revert_selected_hunks);
register_action(view, cx, Editor::apply_selected_diff_hunks);
register_action(view, cx, Editor::open_active_item_in_terminal)
register_action(view, cx, Editor::open_active_item_in_terminal);
register_action(view, cx, Editor::reload_file)
}
fn register_key_listeners(&self, cx: &mut WindowContext, layout: &EditorLayout) {
@@ -1015,8 +1023,20 @@ impl EditorElement {
block_width = em_width;
}
let block_text = if let CursorShape::Block = selection.cursor_shape {
snapshot.display_chars_at(cursor_position).next().and_then(
|(character, _)| {
snapshot
.display_chars_at(cursor_position)
.next()
.or_else(|| {
if cursor_column == 0 {
snapshot
.placeholder_text()
.and_then(|s| s.chars().next())
.map(|c| (c, cursor_position))
} else {
None
}
})
.and_then(|(character, _)| {
let text = if character == '\n' {
SharedString::from(" ")
} else {
@@ -1031,6 +1051,22 @@ impl EditorElement {
})
.unwrap_or(self.style.text.font());
// Invert the text color for the block cursor. Ensure that the text
// color is opaque enough to be visible against the background color.
//
// 0.75 is an arbitrary threshold to determine if the background color is
// opaque enough to use as a text color.
//
// TODO: In the future we should ensure themes have a `text_inverse` color.
let color = if cx.theme().colors().editor_background.a < 0.75 {
match cx.theme().appearance {
Appearance::Dark => Hsla::black(),
Appearance::Light => Hsla::white(),
}
} else {
cx.theme().colors().editor_background
};
cx.text_system()
.shape_line(
text,
@@ -1038,15 +1074,14 @@ impl EditorElement {
&[TextRun {
len,
font,
color: self.style.background,
color,
background_color: None,
strikethrough: None,
underline: None,
}],
)
.log_err()
},
)
})
} else {
None
};
@@ -6060,7 +6095,7 @@ impl CursorLayout {
origin: self.origin + origin,
size: size(self.block_width, self.line_height),
},
CursorShape::Underscore => Bounds {
CursorShape::Underline => Bounds {
origin: self.origin
+ origin
+ gpui::Point::new(Pixels::ZERO, self.line_height - px(2.0)),

View File

@@ -403,7 +403,10 @@ impl GitBlame {
if this.user_triggered {
log::error!("failed to get git blame data: {error:?}");
let notification = format!("{:#}", error).trim().to_string();
cx.emit(project::Event::Notification(notification));
cx.emit(project::Event::Toast {
notification_id: "git-blame".into(),
message: notification,
});
} else {
// If we weren't triggered by a user, we just log errors in the background, instead of sending
// notifications.
@@ -619,9 +622,11 @@ mod tests {
let event = project.next_event(cx).await;
assert_eq!(
event,
project::Event::Notification(
"Failed to blame \"file.txt\": failed to get blame for \"file.txt\"".to_string()
)
project::Event::Toast {
notification_id: "git-blame".into(),
message: "Failed to blame \"file.txt\": failed to get blame for \"file.txt\""
.to_string()
}
);
blame.update(cx, |blame, cx| {

View File

@@ -1,8 +1,8 @@
use crate::{
hover_popover::{self, InlayHover},
scroll::ScrollAmount,
Anchor, Editor, EditorSnapshot, FindAllReferences, GoToDefinition, GoToTypeDefinition, InlayId,
Navigated, PointForPosition, SelectPhase,
Anchor, Editor, EditorSnapshot, FindAllReferences, GoToDefinition, GoToTypeDefinition,
GotoDefinitionKind, InlayId, Navigated, PointForPosition, SelectPhase,
};
use gpui::{px, AppContext, AsyncWindowContext, Model, Modifiers, Task, ViewContext};
use language::{Bias, ToOffset};
@@ -14,12 +14,12 @@ use project::{
};
use std::ops::Range;
use theme::ActiveTheme as _;
use util::{maybe, ResultExt, TryFutureExt};
use util::{maybe, ResultExt, TryFutureExt as _};
#[derive(Debug)]
pub struct HoveredLinkState {
pub last_trigger_point: TriggerPoint,
pub preferred_kind: LinkDefinitionKind,
pub preferred_kind: GotoDefinitionKind,
pub symbol_range: Option<RangeInEditor>,
pub links: Vec<HoverLink>,
pub task: Option<Task<Option<()>>>,
@@ -428,12 +428,6 @@ pub fn update_inlay_link_and_hover_points(
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum LinkDefinitionKind {
Symbol,
Type,
}
pub fn show_link_definition(
shift_held: bool,
editor: &mut Editor,
@@ -442,8 +436,8 @@ pub fn show_link_definition(
cx: &mut ViewContext<Editor>,
) {
let preferred_kind = match trigger_point {
TriggerPoint::Text(_) if !shift_held => LinkDefinitionKind::Symbol,
_ => LinkDefinitionKind::Type,
TriggerPoint::Text(_) if !shift_held => GotoDefinitionKind::Symbol,
_ => GotoDefinitionKind::Type,
};
let (mut hovered_link_state, is_cached) =
@@ -505,6 +499,7 @@ pub fn show_link_definition(
editor.hide_hovered_link(cx)
}
let project = editor.project.clone();
let provider = editor.semantics_provider.clone();
let snapshot = snapshot.buffer_snapshot.clone();
hovered_link_state.task = Some(cx.spawn(|this, mut cx| {
@@ -522,54 +517,40 @@ pub fn show_link_definition(
(range, vec![HoverLink::Url(url)])
})
.ok()
} else if let Some(project) = project {
if let Some((filename_range, filename)) =
find_file(&buffer, project.clone(), buffer_position, &mut cx).await
{
let range = maybe!({
let start =
snapshot.anchor_in_excerpt(excerpt_id, filename_range.start)?;
let end =
snapshot.anchor_in_excerpt(excerpt_id, filename_range.end)?;
Some(RangeInEditor::Text(start..end))
});
} else if let Some((filename_range, filename)) =
find_file(&buffer, project.clone(), buffer_position, &mut cx).await
{
let range = maybe!({
let start =
snapshot.anchor_in_excerpt(excerpt_id, filename_range.start)?;
let end = snapshot.anchor_in_excerpt(excerpt_id, filename_range.end)?;
Some(RangeInEditor::Text(start..end))
});
Some((range, vec![HoverLink::File(filename)]))
Some((range, vec![HoverLink::File(filename)]))
} else if let Some(provider) = provider {
let task = cx.update(|cx| {
provider.definitions(&buffer, buffer_position, preferred_kind, cx)
})?;
if let Some(task) = task {
task.await.ok().map(|definition_result| {
(
definition_result.iter().find_map(|link| {
link.origin.as_ref().and_then(|origin| {
let start = snapshot.anchor_in_excerpt(
excerpt_id,
origin.range.start,
)?;
let end = snapshot
.anchor_in_excerpt(excerpt_id, origin.range.end)?;
Some(RangeInEditor::Text(start..end))
})
}),
definition_result.into_iter().map(HoverLink::Text).collect(),
)
})
} else {
// query the LSP for definition info
project
.update(&mut cx, |project, cx| match preferred_kind {
LinkDefinitionKind::Symbol => {
project.definition(&buffer, buffer_position, cx)
}
LinkDefinitionKind::Type => {
project.type_definition(&buffer, buffer_position, cx)
}
})?
.await
.ok()
.map(|definition_result| {
(
definition_result.iter().find_map(|link| {
link.origin.as_ref().and_then(|origin| {
let start = snapshot.anchor_in_excerpt(
excerpt_id,
origin.range.start,
)?;
let end = snapshot.anchor_in_excerpt(
excerpt_id,
origin.range.end,
)?;
Some(RangeInEditor::Text(start..end))
})
}),
definition_result
.into_iter()
.map(HoverLink::Text)
.collect(),
)
})
None
}
} else {
None
@@ -708,10 +689,11 @@ pub(crate) fn find_url(
pub(crate) async fn find_file(
buffer: &Model<language::Buffer>,
project: Model<Project>,
project: Option<Model<Project>>,
position: text::Anchor,
cx: &mut AsyncWindowContext,
) -> Option<(Range<text::Anchor>, ResolvedPath)> {
let project = project?;
let snapshot = buffer.update(cx, |buffer, _| buffer.snapshot()).ok()?;
let scope = snapshot.language_scope_at(position);
let (range, candidate_file_path) = surrounding_filename(snapshot, position)?;

View File

@@ -195,32 +195,22 @@ fn show_hover(
anchor: Anchor,
ignore_timeout: bool,
cx: &mut ViewContext<Editor>,
) {
) -> Option<()> {
if editor.pending_rename.is_some() {
return;
return None;
}
let snapshot = editor.snapshot(cx);
let (buffer, buffer_position) =
if let Some(output) = editor.buffer.read(cx).text_anchor_for_position(anchor, cx) {
output
} else {
return;
};
let (buffer, buffer_position) = editor
.buffer
.read(cx)
.text_anchor_for_position(anchor, cx)?;
let excerpt_id =
if let Some((excerpt_id, _, _)) = editor.buffer().read(cx).excerpt_containing(anchor, cx) {
excerpt_id
} else {
return;
};
let (excerpt_id, _, _) = editor.buffer().read(cx).excerpt_containing(anchor, cx)?;
let project = if let Some(project) = editor.project.clone() {
project
} else {
return;
};
let language_registry = editor.project.as_ref()?.read(cx).languages().clone();
let provider = editor.semantics_provider.clone()?;
if !ignore_timeout {
if same_info_hover(editor, &snapshot, anchor)
@@ -228,7 +218,7 @@ fn show_hover(
|| editor.hover_state.diagnostic_popover.is_some()
{
// Hover triggered from same location as last time. Don't show again.
return;
return None;
} else {
hide_hover(editor, cx);
}
@@ -240,7 +230,7 @@ fn show_hover(
.cmp(&anchor, &snapshot.buffer_snapshot)
.is_eq()
{
return;
return None;
}
}
@@ -262,12 +252,7 @@ fn show_hover(
total_delay
};
// query the LSP for hover info
let hover_request = cx.update(|cx| {
project.update(cx, |project, cx| {
project.hover(&buffer, buffer_position, cx)
})
})?;
let hover_request = cx.update(|cx| provider.hover(&buffer, buffer_position, cx))?;
if let Some(delay) = delay {
delay.await;
@@ -377,8 +362,11 @@ fn show_hover(
this.hover_state.diagnostic_popover = diagnostic_popover;
})?;
let hovers_response = hover_request.await;
let language_registry = project.update(&mut cx, |p, _| p.languages().clone())?;
let hovers_response = if let Some(hover_request) = hover_request {
hover_request.await
} else {
Vec::new()
};
let snapshot = this.update(&mut cx, |this, cx| this.snapshot(cx))?;
let mut hover_highlights = Vec::with_capacity(hovers_response.len());
let mut info_popovers = Vec::with_capacity(hovers_response.len());
@@ -451,6 +439,7 @@ fn show_hover(
});
editor.hover_state.info_task = Some(task);
None
}
fn same_info_hover(editor: &Editor, snapshot: &EditorSnapshot, anchor: Anchor) -> bool {
@@ -536,7 +525,7 @@ async fn parse_blocks(
font_family: Some(buffer_font_family),
..Default::default()
},
rule_color: Color::Muted.color(cx),
rule_color: cx.theme().colors().border,
block_quote_border_color: Color::Muted.color(cx),
block_quote: TextStyleRefinement {
color: Some(Color::Muted.color(cx)),
@@ -821,7 +810,7 @@ mod tests {
hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
completion_provider: Some(lsp::CompletionOptions {
trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
resolve_provider: Some(false),
resolve_provider: Some(true),
..Default::default()
}),
..Default::default()
@@ -913,15 +902,12 @@ mod tests {
assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
//apply a completion and check it was successfully applied
let () = cx
.update_editor(|editor, cx| {
editor.context_menu_next(&Default::default(), cx);
editor
.confirm_completion(&ConfirmCompletion::default(), cx)
.unwrap()
})
.await
.unwrap();
let _apply_additional_edits = cx.update_editor(|editor, cx| {
editor.context_menu_next(&Default::default(), cx);
editor
.confirm_completion(&ConfirmCompletion::default(), cx)
.unwrap()
});
cx.assert_editor_state(indoc! {"
one.second_completionˇ
two

View File

@@ -591,21 +591,13 @@ impl InlayHintCache {
drop(guard);
cx.spawn(|editor, mut cx| async move {
let resolved_hint_task = editor.update(&mut cx, |editor, cx| {
editor
.buffer()
.read(cx)
.buffer(buffer_id)
.and_then(|buffer| {
let project = editor.project.as_ref()?;
Some(project.update(cx, |project, cx| {
project.resolve_inlay_hint(
hint_to_resolve,
buffer,
server_id,
cx,
)
}))
})
let buffer = editor.buffer().read(cx).buffer(buffer_id)?;
editor.semantics_provider.as_ref()?.resolve_inlay_hint(
hint_to_resolve,
buffer,
server_id,
cx,
)
})?;
if let Some(resolved_hint_task) = resolved_hint_task {
let mut resolved_hint =
@@ -895,11 +887,13 @@ fn fetch_and_update_hints(
) -> Task<anyhow::Result<()>> {
cx.spawn(|editor, mut cx| async move {
let buffer_snapshot = excerpt_buffer.update(&mut cx, |buffer, _| buffer.snapshot())?;
let (lsp_request_limiter, multi_buffer_snapshot) = editor.update(&mut cx, |editor, cx| {
let multi_buffer_snapshot = editor.buffer().update(cx, |buffer, cx| buffer.snapshot(cx));
let lsp_request_limiter = Arc::clone(&editor.inlay_hint_cache.lsp_request_limiter);
(lsp_request_limiter, multi_buffer_snapshot)
})?;
let (lsp_request_limiter, multi_buffer_snapshot) =
editor.update(&mut cx, |editor, cx| {
let multi_buffer_snapshot =
editor.buffer().update(cx, |buffer, cx| buffer.snapshot(cx));
let lsp_request_limiter = Arc::clone(&editor.inlay_hint_cache.lsp_request_limiter);
(lsp_request_limiter, multi_buffer_snapshot)
})?;
let (lsp_request_guard, got_throttled) = if query.invalidate.should_invalidate() {
(None, false)
@@ -909,12 +903,15 @@ fn fetch_and_update_hints(
None => (Some(lsp_request_limiter.acquire().await), true),
}
};
let fetch_range_to_log =
fetch_range.start.to_point(&buffer_snapshot)..fetch_range.end.to_point(&buffer_snapshot);
let fetch_range_to_log = fetch_range.start.to_point(&buffer_snapshot)
..fetch_range.end.to_point(&buffer_snapshot);
let inlay_hints_fetch_task = editor
.update(&mut cx, |editor, cx| {
if got_throttled {
let query_not_around_visible_range = match editor.excerpts_for_inlay_hints_query(None, cx).remove(&query.excerpt_id) {
let query_not_around_visible_range = match editor
.excerpts_for_inlay_hints_query(None, cx)
.remove(&query.excerpt_id)
{
Some((_, _, current_visible_range)) => {
let visible_offset_length = current_visible_range.len();
let double_visible_range = current_visible_range
@@ -928,11 +925,11 @@ fn fetch_and_update_hints(
.contains(&fetch_range.start.to_offset(&buffer_snapshot))
&& !double_visible_range
.contains(&fetch_range.end.to_offset(&buffer_snapshot))
},
}
None => true,
};
if query_not_around_visible_range {
log::trace!("Fetching inlay hints for range {fetch_range_to_log:?} got throttled and fell off the current visible range, skipping.");
// log::trace!("Fetching inlay hints for range {fetch_range_to_log:?} got throttled and fell off the current visible range, skipping.");
if let Some(task_ranges) = editor
.inlay_hint_cache
.update_tasks
@@ -943,16 +940,12 @@ fn fetch_and_update_hints(
return None;
}
}
let buffer = editor.buffer().read(cx).buffer(query.buffer_id)?;
editor
.buffer()
.read(cx)
.buffer(query.buffer_id)
.and_then(|buffer| {
let project = editor.project.as_ref()?;
Some(project.update(cx, |project, cx| {
project.inlay_hints(buffer, fetch_range.clone(), cx)
}))
})
.semantics_provider
.as_ref()?
.inlay_hints(buffer, fetch_range.clone(), cx)
})
.ok()
.flatten();
@@ -1004,12 +997,12 @@ fn fetch_and_update_hints(
})
.await;
if let Some(new_update) = new_update {
log::debug!(
"Applying update for range {fetch_range_to_log:?}: remove from editor: {}, remove from cache: {}, add to cache: {}",
new_update.remove_from_visible.len(),
new_update.remove_from_cache.len(),
new_update.add_to_cache.len()
);
// log::debug!(
// "Applying update for range {fetch_range_to_log:?}: remove from editor: {}, remove from cache: {}, add to cache: {}",
// new_update.remove_from_visible.len(),
// new_update.remove_from_cache.len(),
// new_update.add_to_cache.len()
// );
log::trace!("New update: {new_update:?}");
editor
.update(&mut cx, |editor, cx| {

View File

@@ -27,6 +27,7 @@ use rpc::proto::{self, update_view, PeerId};
use settings::Settings;
use workspace::item::{Dedup, ItemSettings, SerializableItem, TabContentParams};
use project::lsp_store::FormatTarget;
use std::{
any::TypeId,
borrow::Cow,
@@ -722,7 +723,12 @@ impl Item for Editor {
cx.spawn(|this, mut cx| async move {
if format {
this.update(&mut cx, |editor, cx| {
editor.perform_format(project.clone(), FormatTrigger::Save, cx)
editor.perform_format(
project.clone(),
FormatTrigger::Save,
FormatTarget::Buffer,
cx,
)
})?
.await?;
}

View File

@@ -1,5 +1,4 @@
use std::ops::Range;
use crate::actions::FormatSelections;
use crate::{
actions::Format, selections_collection::SelectionsCollection, Copy, CopyPermalinkToLine, Cut,
DisplayPoint, DisplaySnapshot, Editor, EditorMode, FindAllReferences, GoToDeclaration,
@@ -8,6 +7,8 @@ use crate::{
};
use gpui::prelude::FluentBuilder;
use gpui::{DismissEvent, Pixels, Point, Subscription, View, ViewContext};
use std::ops::Range;
use text::PointUtf16;
use workspace::OpenInTerminal;
#[derive(Debug)]
@@ -164,6 +165,12 @@ pub fn deploy_context_menu(
} else {
"Reveal in File Manager"
};
let has_selections = editor
.selections
.all::<PointUtf16>(cx)
.into_iter()
.any(|s| !s.is_empty());
ui::ContextMenu::build(cx, |menu, _cx| {
let builder = menu
.on_blur_subscription(Subscription::new(|| {}))
@@ -175,6 +182,9 @@ pub fn deploy_context_menu(
.separator()
.action("Rename Symbol", Box::new(Rename))
.action("Format Buffer", Box::new(Format))
.when(has_selections, |cx| {
cx.action("Format Selections", Box::new(FormatSelections))
})
.action(
"Code Actions",
Box::new(ToggleCodeActions {

View File

@@ -1,4 +1,4 @@
use crate::{Editor, EditorEvent};
use crate::{Editor, EditorEvent, SemanticsProvider};
use collections::HashSet;
use futures::{channel::mpsc, future::join_all};
use gpui::{AppContext, EventEmitter, FocusableView, Model, Render, Subscription, Task, View};
@@ -6,7 +6,7 @@ use language::{Buffer, BufferEvent, Capability};
use multi_buffer::{ExcerptRange, MultiBuffer};
use project::Project;
use smol::stream::StreamExt;
use std::{any::TypeId, ops::Range, time::Duration};
use std::{any::TypeId, ops::Range, rc::Rc, time::Duration};
use text::ToOffset;
use ui::prelude::*;
use workspace::{
@@ -35,6 +35,12 @@ struct RecalculateDiff {
debounce: bool,
}
/// A provider of code semantics for branch buffers.
///
/// Requests in edited regions will return nothing, but requests in unchanged
/// regions will be translated into the base buffer's coordinates.
struct BranchBufferSemanticsProvider(Rc<dyn SemanticsProvider>);
impl ProposedChangesEditor {
pub fn new<T: ToOffset>(
buffers: Vec<ProposedChangesBuffer<T>>,
@@ -66,6 +72,13 @@ impl ProposedChangesEditor {
editor: cx.new_view(|cx| {
let mut editor = Editor::for_multibuffer(multibuffer.clone(), project, true, cx);
editor.set_expand_all_diff_hunks();
editor.set_completion_provider(None);
editor.clear_code_action_providers();
editor.set_semantics_provider(
editor
.semantics_provider()
.map(|provider| Rc::new(BranchBufferSemanticsProvider(provider)) as _),
);
editor
}),
recalculate_diffs_tx,
@@ -76,7 +89,7 @@ impl ProposedChangesEditor {
while recalculate_diff.debounce {
cx.background_executor()
.timer(Duration::from_millis(250))
.timer(Duration::from_millis(50))
.await;
let mut had_further_changes = false;
while let Ok(next_recalculate_diff) = recalculate_diffs_rx.try_next() {
@@ -245,3 +258,103 @@ impl ToolbarItemView for ProposedChangesEditorToolbar {
self.get_toolbar_item_location()
}
}
impl BranchBufferSemanticsProvider {
fn to_base(
&self,
buffer: &Model<Buffer>,
positions: &[text::Anchor],
cx: &AppContext,
) -> Option<Model<Buffer>> {
let base_buffer = buffer.read(cx).diff_base_buffer()?;
let version = base_buffer.read(cx).version();
if positions
.iter()
.any(|position| !version.observed(position.timestamp))
{
return None;
}
Some(base_buffer)
}
}
impl SemanticsProvider for BranchBufferSemanticsProvider {
fn hover(
&self,
buffer: &Model<Buffer>,
position: text::Anchor,
cx: &mut AppContext,
) -> Option<Task<Vec<project::Hover>>> {
let buffer = self.to_base(buffer, &[position], cx)?;
self.0.hover(&buffer, position, cx)
}
fn inlay_hints(
&self,
buffer: Model<Buffer>,
range: Range<text::Anchor>,
cx: &mut AppContext,
) -> Option<Task<anyhow::Result<Vec<project::InlayHint>>>> {
let buffer = self.to_base(&buffer, &[range.start, range.end], cx)?;
self.0.inlay_hints(buffer, range, cx)
}
fn resolve_inlay_hint(
&self,
hint: project::InlayHint,
buffer: Model<Buffer>,
server_id: lsp::LanguageServerId,
cx: &mut AppContext,
) -> Option<Task<anyhow::Result<project::InlayHint>>> {
let buffer = self.to_base(&buffer, &[], cx)?;
self.0.resolve_inlay_hint(hint, buffer, server_id, cx)
}
fn supports_inlay_hints(&self, buffer: &Model<Buffer>, cx: &AppContext) -> bool {
if let Some(buffer) = self.to_base(&buffer, &[], cx) {
self.0.supports_inlay_hints(&buffer, cx)
} else {
false
}
}
fn document_highlights(
&self,
buffer: &Model<Buffer>,
position: text::Anchor,
cx: &mut AppContext,
) -> Option<Task<gpui::Result<Vec<project::DocumentHighlight>>>> {
let buffer = self.to_base(&buffer, &[position], cx)?;
self.0.document_highlights(&buffer, position, cx)
}
fn definitions(
&self,
buffer: &Model<Buffer>,
position: text::Anchor,
kind: crate::GotoDefinitionKind,
cx: &mut AppContext,
) -> Option<Task<gpui::Result<Vec<project::LocationLink>>>> {
let buffer = self.to_base(&buffer, &[position], cx)?;
self.0.definitions(&buffer, position, kind, cx)
}
fn range_for_rename(
&self,
_: &Model<Buffer>,
_: text::Anchor,
_: &mut AppContext,
) -> Option<Task<gpui::Result<Option<Range<text::Anchor>>>>> {
None
}
fn perform_rename(
&self,
_: &Model<Buffer>,
_: text::Anchor,
_: String,
_: &mut AppContext,
) -> Option<Task<gpui::Result<project::ProjectTransaction>>> {
None
}
}

View File

@@ -67,10 +67,11 @@ fn task_context_with_editor(
variables
};
let context_task = project.update(cx, |project, cx| {
project.task_context_for_location(captured_variables, location.clone(), cx)
});
cx.spawn(|_| context_task)
project.update(cx, |project, cx| {
project.task_store().update(cx, |task_store, cx| {
task_store.task_context_for_location(captured_variables, location, cx)
})
})
}
pub fn task_context(workspace: &Workspace, cx: &mut WindowContext<'_>) -> AsyncTask<TaskContext> {

View File

@@ -13,7 +13,6 @@ use itertools::Itertools;
use language::{Buffer, BufferSnapshot, LanguageRegistry};
use multi_buffer::{ExcerptRange, ToPoint};
use parking_lot::RwLock;
use pretty_assertions::assert_eq;
use project::{FakeFs, Project};
use std::{
any::TypeId,

View File

@@ -25,7 +25,6 @@ fs.workspace = true
git.workspace = true
gpui.workspace = true
http_client.workspace = true
isahc_http_client.workspace = true
language.workspace = true
languages.workspace = true
node_runtime.workspace = true
@@ -36,3 +35,4 @@ serde.workspace = true
serde_json.workspace = true
settings.workspace = true
smol.workspace = true
reqwest_client.workspace = true

View File

@@ -12,6 +12,7 @@ use language::LanguageRegistry;
use node_runtime::NodeRuntime;
use open_ai::OpenAiEmbeddingModel;
use project::Project;
use reqwest_client::ReqwestClient;
use semantic_index::{
EmbeddingProvider, OpenAiEmbeddingProvider, ProjectIndex, SemanticDb, Status,
};
@@ -100,7 +101,7 @@ fn main() -> Result<()> {
gpui::App::headless().run(move |cx| {
let executor = cx.background_executor().clone();
let client = isahc_http_client::IsahcHttpClient::new(None, None);
let client = Arc::new(ReqwestClient::user_agent("Zed LLM evals").unwrap());
cx.set_http_client(client.clone());
match cli.command {
Commands::Fetch {} => {

View File

@@ -56,7 +56,6 @@ wit-component.workspace = true
workspace.workspace = true
[dev-dependencies]
isahc_http_client.workspace = true
ctor.workspace = true
env_logger.workspace = true
fs = { workspace = true, features = ["test-support"] }
@@ -64,5 +63,5 @@ gpui = { workspace = true, features = ["test-support"] }
language = { workspace = true, features = ["test-support"] }
parking_lot.workspace = true
project = { workspace = true, features = ["test-support"] }
tokio.workspace = true
reqwest_client.workspace = true
workspace = { workspace = true, features = ["test-support"] }

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