Compare commits

...

198 Commits

Author SHA1 Message Date
Zed Bot
cfd8d1860e Bump to 0.135.2 for @ConradIrwin 2024-05-10 12:06:22 -07:00
Conrad Irwin
ad5683bcdb Create release archive in the target dir 2024-05-10 13:05:00 -06:00
Kirill Bulatov
a565c3a470 Remove a stray eprintln (#11635)
Release Notes:

- N/A
2024-05-10 02:33:42 +03:00
Kirill Bulatov
3f17540002 Properly calculate expanded git diff hunk highlight ranges (#11632)
Closes https://github.com/zed-industries/zed/issues/11576

Release Notes:

- Fixed expanded diff hunks highlighting an extra row as added
([11576](https://github.com/zed-industries/zed/issues/11576))
2024-05-10 02:05:59 +03:00
Zed Bot
647c31e398 Bump to 0.135.1 for @ConradIrwin 2024-05-08 15:09:27 -07:00
gcp-cherry-pick-bot[bot]
6d927a4991 Don't panic on failure to allocate an AtlasTile (cherry-pick #11579) (#11583)
Cherry-picked Don't panic on failure to allocate an AtlasTile (#11579)

Release Notes:

- Fixed a panic in graphics allocation

Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
2024-05-08 16:00:40 -06:00
gcp-cherry-pick-bot[bot]
3a84d5eaa3 Pass hover position as an anchor (cherry-pick #11578) (#11581)
Cherry-picked Pass hover position as an anchor (#11578)

It's too easily to accidentally pass a point from one snapshot into
another

Release Notes:

- Fixed a panic in show hover

Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
2024-05-08 15:52:14 -06:00
Joseph T. Lyons
3ad3a36643 v0.135.x preview 2024-05-08 12:05:18 -04:00
Kirill Bulatov
ffa2d90dc3 Return prettier entry back to LSP logs (#11563)
Fixes prettier entries disappeared after
https://github.com/zed-industries/zed/pull/10788

Release Notes:

- N/A
2024-05-08 18:21:43 +03:00
Marshall Bowers
5c2ec1705d docs: Fix default value for formatter setting (#11560)
This PR fixes the default value for the `formatter` setting listed in
the docs.

The default value is `"auto"`:
194c43b84b/assets/settings/default.json (L367)

Resolves #11468.

Release Notes:

- N/A
2024-05-08 10:42:41 -04:00
Piotr Osiewicz
2671b9c63c Fix alignment of code actions menu with narrow panes 2024-05-08 16:34:56 +02:00
Piotr Osiewicz
68fe2bb776 Do away with display points in toggle_code_actions 2024-05-08 16:34:56 +02:00
Piotr Osiewicz
65f7238777 editor: Fix task indicator layout for wrapped lines 2024-05-08 16:34:56 +02:00
Thorsten Ball
ca680f07f7 branch picker: Always show HEAD first (#11552)
I think this is a lot less confusing than another branch being selected
by default when there's no query.



Release Notes:

- Changed the branch picker to always show the current branch as the
default selected entry.



https://github.com/zed-industries/zed/assets/1185253/18b50656-f6ac-4138-b4e0-9024072e1555
2024-05-08 15:01:36 +02:00
d1y
9d681bda8d go: support highlight regexp (#11538)
Before:
<img width="521" alt="image"
src="https://github.com/zed-industries/zed/assets/45585937/7b87e552-0cf0-4168-933f-21d1ae84bf64">

After:
<img width="499" alt="image"
src="https://github.com/zed-industries/zed/assets/45585937/f1171375-8c22-4186-a5f1-1bdfa56cf01f">

Release Notes:

- Added go regexp highlighting
2024-05-08 12:52:56 +02:00
Piotr Osiewicz
1669ff80df editor: Fix menu::Confirm falling through to editor when confirming task entry in code actions (#11546)
Release Notes:

- N/A
2024-05-08 12:50:08 +02:00
LoganDark
45aca348b8 Take local project settings into account when launching terminals (#11526)
Fixes #7599

Use project level settings if possible, when creating terminals.

Release Notes:

- Fixed terminals ignoring project-specific settings ([7599](https://github.com/zed-industries/zed/issues/7599))
2024-05-08 13:39:47 +03:00
Piotr Osiewicz
07942bbdfe Editor: Do not display code actions in task gutter menu if they belong to different line (#11506)
This doesn't address the focus issues we saw with @maxbrunsfeld yet.

Release Notes:

- N/A
2024-05-08 12:34:47 +02:00
Owen Law
47c12c6563 Make prefer_line the default soft-wrap behavior. (#11542)
Stops lines from clipping at 1024 by default, returning it to the old
default behaviour where it wraps at 512. The `none` mode is only
supposed to be used for git hunks. See
https://github.com/zed-industries/zed/issues/11518#issuecomment-2099836283.

Release Notes:

- N/A
2024-05-08 13:14:48 +03:00
张小白
63a5f46df4 Remember window restore size (#10429)
Now, regardless of how the Zed window is closed, Zed can remember the
window's restore size.

- [x] Windows implementation
- [x] macOS implementation
- [x] Linux implementation (partial)
- [x] update SQL data base (mark column `fullscreen` as deprecated)

The current implementation on Linux is basic, and I'm not sure if it's
correct.

The variable `fullscreen` in SQL can be removed, but I'm unsure how to
do it.
edit: mark `fullscreen` as deprecated

### Case 1

When the window is closed as maximized, reopening it will open in the
maximized state, and returning from maximized state will restore the
position and size it had when it was maximized.



https://github.com/zed-industries/zed/assets/14981363/7207752e-878a-4d43-93a7-41ad1fdb3a06


### Case 2

When the window is closed as fullscreen, reopening it will open in
fullscreen mode, and toggling fullscreen will restore the position and
size it had when it entered fullscreen (note that the fullscreen
application was not recorded in the video, showing a black screen, but
it had actually entered fullscreen mode).



https://github.com/zed-industries/zed/assets/14981363/ea5aa70d-b296-462a-afb3-4c3372883ea3

### What's more

- As English is not my native language, some variable and struct names
may need to be modified to match their actual meaning.
- I am not familiar with the APIs related to macOS and Linux, so
implementation for these two platforms has not been done for now.
- Any suggestions and ideas are welcome.

Release Notes:

- N/A
2024-05-07 23:29:03 -06:00
Kyle Kelley
6ddcff25e3 Show annotations more like the inline assistant (#11530)
* compute gutter width
* show the AI icon
* borders, background, and text color for annotations

<img width="1840" alt="image"
src="https://github.com/zed-industries/zed/assets/836375/93f2e9b8-d7f7-4c25-b3e2-cf77a0c4ca36">

Release Notes:

- N/A
2024-05-07 19:16:35 -07:00
Kyle Kelley
1cf40d77e2 Supermaven enhanced (#11521)
Fixes #11422 by accepting just the start of the line.

Release Notes:

- N/A

---------

Co-authored-by: max <max@zed.dev>
Co-authored-by: jacob <jacob@supermaven.com>
2024-05-07 15:38:03 -07:00
Marshall Bowers
33a72219c0 assistant2: Add new conversation button, that also saves the current conversation (#11522)
This PR updates the new assistant with a button to start a new
conversation.

Clicking on it will reset the chat and put it into a fresh state.

The current conversation will be serialized and written to
`~/.config/zed/conversations`.

Release Notes:

- N/A
2024-05-07 18:16:48 -04:00
Andrei Zvonimir Crnković
c77dd8b9e0 docs: Add linux build command explanation (#11513)
Just adding a short note about `cargo build --release` and the location
of the compiled binary. It helps a lot to streamline usage of Zed day to
day on Linux.

Release Notes:

- N/A
2024-05-07 14:37:34 -07:00
Kirill Bulatov
3d9f0087ff Do not show diffs for files with \r\n contents (#11519) 2024-05-08 00:37:09 +03:00
Marshall Bowers
6a64b732b6 assistant2: Add general structure for conversation history (#11516)
This PR adds the general structure for conversation history to the new
assistant.

Right now we have a placeholder button in the assistant panel that will
toggle a picker containing some placeholder saved conversations.

Release Notes:

- N/A
2024-05-07 17:03:33 -04:00
Nate Butler
47ca343803 Add DecoratedIcon (#11512)
This allows us to create icons with dynamic decorations drawn on top
like these:


![image](https://github.com/zed-industries/zed/assets/1714999/1d1a22df-8f90-47f2-abbd-ed7afa8fc641)

### Examples:

```rust
div()
    .child(DecoratedIcon::new(
        Icon::new(IconName::Bell).color(Color::Muted),
        IconDecoration::IndicatorDot,
    ))
    .child(
        DecoratedIcon::new(Icon::new(IconName::Bell), IconDecoration::IndicatorDot)
            .decoration_color(Color::Accent),
    )
    .child(DecoratedIcon::new(
        Icon::new(IconName::Bell).color(Color::Muted),
        IconDecoration::Strikethrough,
    ))
    .child(
        DecoratedIcon::new(Icon::new(IconName::Bell), IconDecoration::X)
            .decoration_color(Color::Error),
    )
```

Release Notes:

- N/A
2024-05-07 16:36:13 -04:00
Mikayla Maki
768b63a497 Remove windows dependency from all non-windows platforms (#11510)
Whoops

Release Notes:

- N/A
2024-05-07 12:45:23 -07:00
Kyle Kelley
0d2f65ac13 Language Model Tool for commenting in a multibuffer (#11509)
Language Model can now open multibuffers and insert comments as block
decorations in the editor.


![image](https://github.com/zed-industries/zed/assets/836375/f4456ad0-66e7-4ad6-a2b3-63810a3223a5)


Release Notes:

- N/A

---------

Co-authored-by: max <max@zed.dev>
Co-authored-by: marshall <marshall@zed.dev>
Co-authored-by: nate <nate@zed.dev>
2024-05-07 12:35:37 -07:00
Joseph T. Lyons
953acb0f6d Document configuring pyright for pyproject.toml (#11508)
Release Notes:

- N/A
2024-05-07 14:40:34 -04:00
Marshall Bowers
d64106e01b Add development credentials provider (#11505)
This PR adds a new development credentials provider for the purpose of
streamlining local development against production collab.

## Problem

Today if you want to run a development build of Zed against the
production collab server, you need to either:

1. Enter your keychain password every time in order to retrieve your
saved credentials
2. Re-authenticate with zed.dev every time
    - This can get annoying as you need to pop out into a browser window
- I've also seen cases where if you re-auth too many times in a row
GitHub will make you confirm the authentication, as it looks suspicious

## Solution

This PR decouples the concept of the credentials provider from the
keychain, and adds a new development credentials provider to address
this specific case.

Now when running a development build of Zed and the
`ZED_DEVELOPMENT_AUTH` environment variable is set to a non-empty value,
the credentials will be saved to disk instead of the system keychain.

While this is not as secure as storing them in the system keychain,
since it is only used for development the tradeoff seems acceptable for
the resulting improvement in UX.

Release Notes:

- N/A
2024-05-07 13:59:18 -04:00
张小白
c260f7d5ac Refactor Windows platform implementation (#11393)
This aligns the Windows platform implementation with a code style
similar to macOS platform, eliminating most of the `Cell`s and
`Mutex`es. This adjustment facilitates potential future porting to a
multi-threaded implementation if required.

Overall, this PR made the following changes: it segregated all member
variables in `WindowsPlatform` and `WindowsWindow`, grouping together
variables that remain constant throughout the entire app lifecycle,
while placing variables that may change during app runtime into
`RefCell`.

Edit: 
During the code refactoring process, a bug was also fixed.

**Before**: 
Close window when file has changed, nothing happen:


https://github.com/zed-industries/zed/assets/14981363/0bcda7c1-808c-4b36-8953-a3a3365a314e

**After**:
Now `should_close` callback is properly handled


https://github.com/zed-industries/zed/assets/14981363/c8887b72-9a0b-42ad-b9ab-7d0775d843f5


Release Notes:

- N/A
2024-05-07 09:49:39 -07:00
Kyle Kelley
b038fb3729 rename attachment_store -> attachment_registry (#11501)
Minor touch up from #11471

Release Notes:

- N/A
2024-05-07 09:18:18 -07:00
Thorsten Ball
4eedbdedae linux: Use optional compile-time RELEASE_VERSION variable (#11490)
This implements `app_version` on Linux by using an optional,
compile-time `RELEASE_VERSION` env var that can be set.

We settled on the `RELEASE_VERSION` as the name, since it's similar to
`RELEASE_CHANNEL` which we use in Zed.

cc @ConradIrwin @mikayla-maki 

Release Notes:

- N/A

Co-authored-by: Bennet <bennetbo@gmx.de>
2024-05-07 17:50:19 +02:00
Conrad Irwin
33d4c563fb Add a nohup workaround (#11499)
Release Notes:

- N/A
2024-05-07 09:50:06 -06:00
Conrad Irwin
e2907983d1 don't report hangs on stable (#11494)
Release Notes:

- N/A
2024-05-07 09:35:52 -06:00
Conrad Irwin
ceab446409 Restore vim docs (#11491)
These got reset to the wrong version as part of the docs migration

Release Notes:

- N/A
2024-05-07 09:24:00 -06:00
Kyle Kelley
72c47b7f01 Assistant grouping (#11479)
Groups collections of assistant messages with their tool calls as
children of the assistant message container.


![image](https://github.com/zed-industries/zed/assets/836375/b26b7c90-4c8d-4bbd-972a-1e769d78a455)

Release Notes:

- N/A
2024-05-07 08:21:57 -07:00
Marshall Bowers
c77d2eb73f Increase short SHA length to 7 characters (#11492)
This PR increases the length of a shortened Git SHA from 6 to 7
characters.

This matches what GitHub uses.

I also took the opportunity to factor out a common method for computing
a short SHA so that we have a single source of truth for the length that
we're using.

Release Notes:

- Increased the short commit SHA length used by git blame from 6 to 7
characters.
2024-05-07 11:10:44 -04:00
Conrad Irwin
fc4ea55d3c Update pull_request_template.md
Make it easier to edit to select the N/A option.
2024-05-07 08:51:38 -06:00
Kirill Bulatov
bcf7bc9de8 Do not toggle hunk diffs when resizing the docks (#11489)
Closes https://github.com/zed-industries/zed/issues/11456 

Release Notes:

- N/A

---------

Co-authored-by: Piotr <piotr@zed.dev>
2024-05-07 17:06:12 +03:00
Thorsten Ball
5a7b8f7fe3 linux: Fix restarting by waiting for sockets to be closed (#11488)
This fixes a race-condition that showed up when trying to restart
Nightly/Preview/...

When running with these release channels, Zed tries to ensure that
there's only one instance of Zed running.

It does that by listening on a TCP socket to which other instances can
connect on start. If the other instance receives a message, it knows
that another Zed instance is running and exits.

On Linux, though, we ran into a race condition:

1. `kill -0`, which checks whether a process is still running, returns
an error, signalling that the old Zed process has exited
2. BUT: the process was still listening on the TCP port.

It seems like that on Linux, process resources aren't guaranteed to be
cleaned up as soon as signal handling stops working for a process.

The fix is to wait until the process is no longer listening on any TCP
sockets.

There's a slight downside to this: GPUI processes that never listen on
any TCP sockets now have to pay the cost of an additional `lsof` call
when restarting. We do think that it's a reasonable tradeoff for now
though, since the other options (extending the platform interface to
provide callbacks, sharing the listening port in the framework, ...)
seem wider-reaching only to fix a very local bug.



Release Notes:

- N/A

Co-authored-by: Bennet <bennetbo@gmx.de>
2024-05-07 15:46:41 +02:00
Piotr Osiewicz
0c11d841e8 editor: Move runnables querying to background thread (#11487)
Originally reported by @mrnugget and @bennetbo 
Also, instead of requerying them every frame, we do so whenever buffer
changes.

As a bonus, I modified tree-sitter query for Rust tests.

Release Notes:

- N/A
2024-05-07 15:31:07 +02:00
Marshall Bowers
4eca7875ae gleam: Add runnable tests (#11476)
This PR adds basic runnable tests for Gleam.

Functions with names ending in `_test` will be available for running:


https://github.com/zed-industries/zed/assets/1486634/9f3f81e5-a7fa-425c-a5a2-d615062486bb

Release Notes:

- N/A
2024-05-06 22:26:36 -04:00
CharlesChen0823
843d299d9a Windows: Fix canonicalize return UNC path (#11083)
In Windows platform, using notify to watch file events. 
1. in [notify windows
implement](3df0f65152/notify/src/windows.rs (L344)),
we get the full file path, just with `path.join(file_path)`.
2. In [zed worktree
start_backgroud_scan_tasks](d2569afe66/crates/worktree/src/worktree.rs (L679)),
`abs_path` is not unc path, so we get all file events with not unc path.
3. but in [zed worktree
process_event](d2569afe66/crates/worktree/src/worktree.rs (L3619)),
we `strip_prefix` unc path all times, it will always print annoy error.

@mikayla-maki I can't reopen pre closed pr #10501 .

Release Notes:

- N/A
2024-05-06 18:25:21 -07:00
Marshall Bowers
88c4e0b2d8 Add a registry for GitHostingProviders (#11470)
This PR adds a registry for `GitHostingProvider`s.

The intent here is to help decouple these provider-specific concerns
from the lower-level `git` crate.

Similar to languages, the Git hosting providers live in the new
`git_hosting_providers` crate.

This work also lays the foundation for if we wanted to allow defining a
`GitHostingProvider` from within an extension. This could be useful if
we wanted to extend the support to work with self-hosted Git providers
(like GitHub Enterprise).

I also took the opportunity to move some of the provider-specific code
out of the `util` crate, since it had leaked into there.

Release Notes:

- N/A
2024-05-06 21:24:48 -04:00
Max Brunsfeld
a64e20ed96 Centralize project context provided to the assistant (#11471)
This PR restructures the way that tools and attachments add information
about the current project to a conversation with the assistant. Rather
than each tool call or attachment generating a new tool or system
message containing information about the project, they can all
collectively mutate a new type called a `ProjectContext`, which stores
all of the project data that should be sent to the assistant. That data
is then formatted in a single place, and passed to the assistant in one
system message.

This prevents multiple tools/attachments from including redundant
context.

Release Notes:

- N/A

---------

Co-authored-by: Kyle <kylek@zed.dev>
2024-05-06 17:01:50 -07:00
Nate Butler
f2a415135b Continue Assistant 2 Messages Layout (#11465)
Release Notes:

- N/A

---------

Co-authored-by: Marshall Bowers <1486634+maxdeviant@users.noreply.github.com>
Co-authored-by: Kyle Kelley <rgbkrk@gmail.com>
2024-05-06 15:44:34 -07:00
Conrad Irwin
96a3021b12 vim: Add shift-k as alias for g h (#11463)
Co-Authored-By: Zachiah Sawyer <zachiah@proton.me>

Release Notes:

- vim: Added `shift-k` to show the hover tooltip

Co-authored-by: Zachiah Sawyer <zachiah@proton.me>
2024-05-06 16:05:19 -06:00
jansol
d6a6330419 gpui/blade: add alpha handling for non-rounded quads (#11461)
Fixes
https://github.com/zed-industries/zed/pull/10973#issuecomment-2096586316

Release Notes:

- N/A
2024-05-06 14:38:00 -07:00
Soroush Mirzaei
da6a6ec36b Add col/row resize cursor styles (#11406)
This PR fixes a small issue I noticed with resize cursors. The
column/row resize cursors were missing and in a few places we were using
`ew-resize` and `ns-resize` even though the documentation mentions
`col-resize` and `row-resize`.

Finally updated the panes in the workspace to use the new column/row
resize cursors.

Before:

![Screenshot_20240505_111515](https://github.com/zed-industries/zed/assets/829535/50f28a1b-33e2-431a-8fc8-5048d89c8f7b)

![Screenshot_20240505_111521](https://github.com/zed-industries/zed/assets/829535/45856f7e-4ca9-4b39-9f8c-144934e9d41e)

After:

![Screenshot_20240505_110606](https://github.com/zed-industries/zed/assets/829535/2b247ec1-44ef-4293-87b3-7fda4b2ebf8f)

![Screenshot_20240505_110611](https://github.com/zed-industries/zed/assets/829535/b558e1ce-3e08-4de3-8a11-6a80184d84fe)



Release Notes:

- Added column/row resize cursor styles to GPUI
- Fixed the existing references that were incorrectly using `ew-resize`
for column resize and `ns-resize` for row resize
- Updated panes to use column/row resize cursors instead on `ew-resize`
and `ns-resize`

---------

Co-authored-by: Mikayla <mikayla@zed.dev>
2024-05-06 14:22:56 -07:00
Conrad Irwin
f3fffc25c4 Don't log JSON parse errors with no settings (#11459)
Release Notes:

- Silenced error messages on startup when no settings/keymap files
exist.
2024-05-06 14:55:44 -06:00
moshyfawn
2e6d044bac Fix Collab context menu dismissal (#11414)
Closes: #11413

Release Notes:

- Fixed Collab panel context menu dismissal with `Escape` key
([#11413](https://github.com/zed-industries/zed/issues/11413)).
2024-05-06 13:51:02 -07:00
CharlesChen0823
530bc5c99e windows: Fix crash in vim normal mode when IME key is pressed (#11387)
Fixed crash in vim normal mode when ime key press.

Release Notes:

- N/A
2024-05-06 13:31:49 -07:00
Tim
9edd81c740 Add Windows specific path parsing (#11119)
Since Windows paths are known to be weird and currently not handled at
all (outside of relative paths that just happen to work), I figured I
would add a windows specific implementation for parsing absolute paths.
It should be functionally the same, of course there's always a chance I
missed an edge case though.

This should fix
- #10849

Note that there are still some cases that will probably break the
current implementation, namely local drives that do not have a drive
letter assigned (not sure how to handle those). There's also UNC paths
but I don't know how important those are at the moment (I'll allow
myself to assume not at all)

Release Notes:

- N/A
2024-05-06 13:27:26 -07:00
apricotbucket28
11bc28080f linux: Fix some small issues (#11458)
Fixed various small issues on Linux, mainly on Wayland.

Apart from the first commit (which should be self-describing), the other
commits have a description explaining the issue and what they do.

caadc58bea should fix
https://github.com/zed-industries/zed/issues/11037

Release Notes:

- N/A
2024-05-06 13:23:49 -07:00
Martin Ashby
fd3831861b Add pkgconf to arch linux required dependencies (#11449)
It's needed to build openssl crate

Fixes: #11448

Release Notes:

- N/A
2024-05-06 13:21:54 -07:00
张小白
68a0035264 Remove unused callbacks (#11410)
This PR follows up #11314 (which removes some deprecated `callback`s)
removes the corresponding implements.

Release Notes:

- N/A
2024-05-06 13:21:35 -07:00
Owen Law
9a60c0a059 Replace all X11 mouse events with XI2 equivalents (#11235)
This PR replaces all pointer events on X11 with their XI2 equivalents,
which fixes problems with scroll events not being reported when a mouse
button is down. Additionally it closes #11206 by resetting the tracked
global scroll valulator position with `None` on a leave event to prevent
a large scroll delta if scrolling is done outside the window. Lastly, it
resolves the bad window issue kvark was having.

Release Notes:

- Fixed X11 Scroll snapping (#11206 ).

---------

Co-authored-by: Mikayla Maki <mikayla@zed.dev>
2024-05-06 13:19:28 -07:00
apricotbucket28
5486c3dc93 wayland: Refactor serial usage (#11388)
Adds a `SerialTracker` type which helps simplify serial handling.

Release Notes:

- N/A
2024-05-06 13:15:42 -07:00
Fernando Tagawa
3018a64a1b Wayland: Improve first click detection (#11371)
Release Notes:

- N/A

This changes the first click detection in Wayland by requiring first
click after the keyboard loses focus, and after a `wl_pointer` enters a
window that has keyboard focus
2024-05-06 13:11:58 -07:00
张小白
8633909347 windows: Fix drag drop action (#11332)
The coordinates are screen-based points, converts them to client-based
points then to logical points.

Release Notes:

- N/A
2024-05-06 13:09:28 -07:00
apricotbucket28
091e7cb395 x11: Cursor style support (#11237)
Adds cursor style support to X11

![image](https://github.com/zed-industries/zed/assets/71973804/e5a2414f-4d80-4963-93d2-e4a15878a718)


Release Notes:

- N/A
2024-05-06 13:05:00 -07:00
Marshall Bowers
bb1817ff31 Refactor Git hosting providers (#11457)
This PR refactors the code pertaining to Git hosting providers to make
it more uniform and easy to add support for new providers.

There is now a `GitHostingProvider` trait that contains the
functionality specific to an individual Git hosting provider. Each
provider we support has an implementation of this trait.

Release Notes:

- N/A
2024-05-06 15:44:13 -04:00
Marshall Bowers
8871fec2a8 Adjust names of negated style methods (#11453)
This PR adjusts the names of the negated style methods by moving the
`neg_` to after the property name instead of before.

This will help keep related style methods grouped together in
completions.

It also makes it a bit clearer that the negation applies to the value.

### Before

```rs
div()
    .neg_mx_1()
    .neg_mt_2()
```

### After

```rs
div()
    .mx_neg_1()
    .mt_neg_2()
```

Release Notes:

- N/A
2024-05-06 13:56:25 -04:00
Kyle Kelley
32b59bfa0e Trim index output (#11445)
Trims down the project index output view in assistant2 to just list the
filenames and hideaway the query.

<img width="374" alt="image"
src="https://github.com/zed-industries/zed/assets/836375/8603e3cf-9fdc-4661-bc45-1d87621a006f">

Introduces a way for tools to render running.

Release Notes:

- N/A

---------

Co-authored-by: Max <max@zed.dev>
2024-05-06 10:37:31 -07:00
Marshall Bowers
f658af5903 Make border methods always require an explicit width (#11450)
This PR makes the `border` methods require an explicit width instead of
defaulting to 1px.

This breaks convention with Tailwind, but it makes GPUI more consistent
with itself. We already have an edge case where the parameterized method
had to be named `border_width`, since `border` was taken up by an alias
for the 1px variant.

### Before

```rs
div()
    .border()
    .border_t()
    .border_r()
    .border_b()
    .border_l()
    .border_width(px(7.))
```

### After

```rs
div()
    .border_1()
    .border_t_1()
    .border_r_1()
    .border_b_1()
    .border_l_1()
    .border(px(7.))
```

Release Notes:

- N/A
2024-05-06 13:22:47 -04:00
Mikayla Maki
f99b24acca Update linux script
Added the git submodule initialization to the linux dependency script
2024-05-06 09:56:56 -07:00
Dzmitry Malyshau
e4f13dd561 Blade window transparency (#10973)
Release Notes:

- N/A

Following up to #10880
TODO:
- [x] create window as transparent
  - [x] X11
  - [x] Wayland
  - [ ] Windows
  - [x] MacOS (when used with Blade)  
- [x] enable GPU surface transparency
- [x] adjust the pipeline blend modes
- [x] adjust shader outputs


![transparency2](https://github.com/zed-industries/zed/assets/107301/d554a41b-5d3f-4420-a857-c64c1747c2d5)

Blurred results from @jansol (on Wayland), who contributed to this work:


![zed-blur](https://github.com/zed-industries/zed/assets/107301/a6822171-2dcf-4109-be55-b75557c586de)

---------

Co-authored-by: Jan Solanti <jhs@psonet.com>
2024-05-06 09:53:08 -07:00
Marshall Bowers
056c785f4e zig: Bump to v0.1.2 (#11447)
This PR bumps the Zig extension to v0.1.2.

Changes:

- https://github.com/zed-industries/zed/pull/11409

Release Notes:

- N/A
2024-05-06 12:47:16 -04:00
Marshall Bowers
970a5957cc elixir: Bump to v0.0.4 (#11446)
This PR bumps the Elixir extension to v0.0.4.

Changes:

- https://github.com/zed-industries/zed/pull/11363

Release Notes:

- N/A
2024-05-06 12:41:50 -04:00
vali-pnt
b25eb9afe2 zig: Fix syntax and file types (#11409)
Fixed autoclosing and made it recognize all ZON (Zig Object Notation)
files.

- Fixed single and double quotes not autoclosing for zig
- Fixed ZON file recognition
- Removed angle brackets autoclosing in zig as they are not used

Release Notes:

- N/A
2024-05-06 12:33:44 -04:00
Conrad Irwin
0aab6d8bdc Fix race condition in editor show_hover (#11441)
The DisplayPoint returned from the position map is only valid at the
snapshot in the position map.

Before this change we were erroneously using it to index into the
current version of the buffer.

Release Notes:

- Fixed a panic caused by a race condition in hover.
2024-05-06 09:46:30 -06:00
Marshall Bowers
8caca6db29 docs: Fix some typos (#11443)
This PR fixes some typos in the docs.

Release Notes:

- N/A
2024-05-06 11:45:17 -04:00
Adam
d0c95c2f43 Add Svelte to list of ESLint languages (#11437)
Release Notes:

- Added ESLint as a default language server for Svelte.
2024-05-06 11:38:55 -04:00
Marshall Bowers
910963e5f3 docs: Update README (#11442)
This PR updates the docs README with some notes about how to deal with
images.

Release Notes:

- N/A
2024-05-06 11:33:48 -04:00
Bennet Bo Fenner
237cc9b4a9 remoting: Adjust prompt level for dev server prompts (#11440)
This changes the remoting prompts to use `PromptLevel::Warning` instead
of `PromptLevel::Destructive`.
In #11015 we decided to apply PromptLevel::Destructive to prompts other
than the new path picker. However, we did not notice that this breaks
confirmation with the keyboard, so it should really only be used in
specific situations (e.g. replacing a file with the remote "save as"
picker, because it matches the behavior of the macOS file dialog).

Release Notes:

- N/A
2024-05-06 17:30:06 +02:00
Marshall Bowers
38a50a042a docs: Port over tasks docs from old docs (#11439)
This PR ports over the changes to the "Tasks" page in the docs that were
made in the old docs.

Release Notes:

- N/A
2024-05-06 11:06:56 -04:00
Marshall Bowers
01aa7688c5 docs: Update system requirements to note Linux and Windows can be built from source. (#11438)
This PR updates the system requirements in the docs to note that Linux
and Windows can be built from source.

This matches the messaging we have in place on the [download
page](https://zed.dev/download).

Release Notes:

- N/A
2024-05-06 11:01:00 -04:00
Piotr Osiewicz
d4636481ac tasks: Prefer worktree tasks to global tasks in tag selection (#11427)
Release Notes:

- Added test indicators in Rust files, backed by task system.
2024-05-06 16:53:48 +02:00
Marshall Bowers
29c675ba17 docs: Change path from /docs2 to /docs (#11436)
This PR changes the docs path from `/docs2` to just `/docs` in
preparation for release.

Release Notes:

- N/A
2024-05-06 10:51:37 -04:00
Andrei Zvonimir Crnković
bc5f82d40c elixir: Fix next-ls binary name (#11363)
This is to followup #11318, and the comment from @mhanberg.

Release Notes:

- N/A
2024-05-06 10:41:23 -04:00
Zelaren
80733e919d Fix cfg!(target_os) spelling mistake (#11430)
This PR fixes the spelling mistake cfg!(target_os = "windows")

Release Notes:

- N/A
2024-05-06 16:41:05 +03:00
Piotr Osiewicz
27a9498cb0 editor: Fix up task indicators in multibuffers (#11434)
We were retrieving task context incorrectly with a display point row as
the location argument, and not the actual row in the buffer.



Release Notes:

- N/A
2024-05-06 15:39:49 +02:00
Bennet Bo Fenner
593f0e0c3e remoting: Edit dev server (#11344)
This PR allows configuring existing dev server, right now you can:
- Change the dev servers name
- Generate a new token (and invalidate the old one)

<img width="563" alt="image"
src="https://github.com/zed-industries/zed/assets/53836821/9bc95042-c969-4293-90fd-0848d021b664">


Release Notes:

- N/A
2024-05-06 12:58:11 +02:00
José Olórtegui
6e2be283dd emmet: Support more languages (#10779)
Hey guys! `emmet-language-server` author here. Thank you so much for the
amazing editor!

This PR adds more languages to the list for the `emmet-language-server`
to attach to.

I have a question though, I saw that you guys don't differentiate yet
between `JavaScript` and `JSX` files. I know that the tree-sitter parser
for `js` comes with the ability to parse both but we still need to make
that difference. Is that part of the plan? or do you have a reason for
doing that?

Aside from that, I've still added support for `JavaScript` files since
is important to have emmet completions in `JSX` files, but I would like
to know what are your thoughts on that since doing this may pollute the
completions in `.js` files.

And one last thing, the emmet language server accepts more filetypes
such as `pug`, `sass`, `scss` and `less` files, which are not currently
supported by zed. Should I create some extensions to add grammar support
to those files later? Should those extensions be part of the zed repo?
I'm just thinking that those are sort of core languages.

Aside from that, let me know if there's anything left to do on my side.
Greetings!

Fixes #10654.

Release Notes:

- N/A
2024-05-06 12:09:19 +02:00
Bennet Bo Fenner
cf6c2daaa2 remoting: Register remote modal action when flag is present (#11426)
Fixes #11391

Release Notes:

- N/A
2024-05-06 11:31:30 +02:00
Bennet Bo Fenner
283d424485 remoting: Prevent user from creating multiple dev servers accidentally (#11425)
Fixes #11389 

Release Notes:

- N/A
2024-05-06 11:02:52 +02:00
Conrad Irwin
c68b700312 Fix install.sh to always install to 'zed' (#11370)
This makes our remoting instructions work regardless of which version of
zed is installed.

Release Notes:

- N/A
2024-05-05 20:17:55 -06:00
Nate Butler
08c9157a1e Standardize TabBar start_slot and end_slot elements (#11403)
- Unifies spacing between left and right sides of the tab bar
- Use the default icon color for `end_slot` tools. This should help more
clearly differentiate when forward or backward navigation is disabled
due to the tools on the other side not looking so much like the disabled
navigation arrows.
- Rework the TabBar implementation in `pane.rs` to directly pass in
items to the `start_slot` instead of an unneeded extra horizontal
layout.

Left side:

![CleanShot 2024-05-05 at 11 08
35@2x](https://github.com/zed-industries/zed/assets/1714999/ec80fda5-17ce-4cd4-ae54-8c63dcc79e69)

Right side:

![CleanShot 2024-05-05 at 11 09
04@2x](https://github.com/zed-industries/zed/assets/1714999/0281e462-202f-407b-b6b7-7acbcde9138f)

Release Notes:

- Standardized some Tab Bar UI elements. You many notice some slight
spacing or color changes.
2024-05-05 19:59:18 -04:00
Kirill Bulatov
1e84f01041 Use lowercased language name as language id fallback (#11412) 2024-05-05 22:27:18 +03:00
Piotr Osiewicz
5a71d8c7f1 Add support for detecting tests in source files, and implement it for Rust (#11195)
Continuing work from #10873 

Release Notes:

- N/A

---------

Co-authored-by: Mikayla <mikayla@zed.dev>
2024-05-05 16:32:48 +02:00
Piotr Osiewicz
14c7782ce6 chore: Fix main CI after upgrade to Rust 1.78 (#11402)
The CI was green at the time I've merged Rust 1.78, but a change that
violated clippy::empty_doc has slipped through into main in the
meantime. Mea culpa, I should've reran the CI.



Release Notes:

- N/A
2024-05-05 15:37:53 +02:00
Piotr Osiewicz
1a9b0536a2 Rust 1.78 (#11314)
Notable things I've had to fix due to 1.78:
- Better detection of unused items
- New clippy lint (`assigning_clones`) that points out places where assignment operations with clone rhs could be replaced with more performant `clone_into`
Release Notes:

- N/A
2024-05-05 15:02:50 +02:00
Kirill Bulatov
9ec0927701 Respect LSP completion triggers when copilot suggestion is on (#11401) 2024-05-05 13:01:52 +03:00
Max Brunsfeld
89039f6f34 Restore rendering of assistant tool calls (#11385)
Chat message rendering was restructured in
https://github.com/zed-industries/zed/pull/11327, but it caused tool
calls not to be rendered if the assistant hadn't generated any message
text.

before:

![Screenshot 2024-05-03 at 10 02
57 PM](https://github.com/zed-industries/zed/assets/326587/2b7fd763-0c75-4690-9824-3bd37a3efef2)

after:

<img width="518" alt="Screenshot 2024-05-03 at 11 17 45 PM"
src="https://github.com/zed-industries/zed/assets/326587/34de19ba-daf2-4ac1-9fe0-f51d0ce94872">

Release Notes:

- N/A
2024-05-03 23:33:47 -07:00
Max Brunsfeld
6964302d89 More fixes to the semantic index's chunking (#11376)
This fixes a tricky intermittent issue I was seeing, where failed to
chunk certain files correctly because of the way we reuse Tree-sitter
`Parser` instances across parses.

I've also accounted for leading comments in chunk boundaries, so that
items are grouped with their leading comments whenever possible when
chunking.

Finally, we've changed the `debug project index` action so that it opens
a simple debug view in a pane, instead of printing paths to the console.
This lets you click into a path and see how it was chunked.

Release Notes:

- N/A

---------

Co-authored-by: Marshall <marshall@zed.dev>
2024-05-03 19:00:18 -07:00
Marshall Bowers
335c307b93 Fix README links (#11382)
This PR fixes the links in the README to account for the changes in the
docs structure.

Also, somehow the README had gotten added with `\r\n` newlines, so I
changed it back to `\n` newlines.

Release Notes:

- N/A
2024-05-03 19:53:22 -04:00
Marshall Bowers
02a859fb08 docs: Fix favicon (#11381)
This PR fixes the favicon used in the docs so it matches zed.dev.

Release Notes:

- N/A
2024-05-03 19:20:07 -04:00
Kyle Kelley
f576bd3aaf Clean up some stray todos (#11380)
Quick little todo comment cleanups, either because they aren't needed or
a comment will suffice.

Release Notes:

- N/A
2024-05-03 16:17:56 -07:00
Marshall Bowers
15299dcf80 docs: Update "Themes" page (#11379)
This PR updates the "Themes" page in the docs to remove some outdated
copy.

Release Notes:

- N/A
2024-05-03 19:05:29 -04:00
Marshall Bowers
75a545308d docs: Update inline code style (#11378)
This PR updates the inline code style on the docs to match what we use
on zed.dev.

Release Notes:

- N/A
2024-05-03 19:00:36 -04:00
Marshall Bowers
d62943930b docs: Put redirects underneath /docs2 path 2024-05-03 18:52:39 -04:00
Marshall Bowers
0969f314b9 docs: Update redirects 2024-05-03 18:50:05 -04:00
Marshall Bowers
8d390f986d docs: Remove unneeded frontmatter from "Themes" 2024-05-03 18:37:59 -04:00
Conrad Irwin
b2582a7b1b docs: Force light mode syntax highlighting (#11377)
Release Notes:

- N/A

Co-authored-by: Marshall <marshall@zed.dev>
2024-05-03 18:34:38 -04:00
Conrad Irwin
a497c49fb8 update docs content (#11374)
Move all docs to zed repo

Release Notes:

- N/A

---------

Co-authored-by: Marshall Bowers <elliott.codes@gmail.com>
Co-authored-by: Marshall <marshall@zed.dev>
2024-05-03 16:24:04 -06:00
Kyle Kelley
3e5dcd1bec Attachment store for assistant2 (#11327)
This sets up a way for the user (or Zed) to _push_ context instead of
having the model retrieve it with a function. Our first use is the
contents of the current file.

<img width="399" alt="image"
src="https://github.com/zed-industries/zed/assets/836375/198429a5-82af-4b82-86f6-cb961f10de5c">

<img width="393" alt="image"
src="https://github.com/zed-industries/zed/assets/836375/cfb52444-723b-4fc1-bddc-57e1810c512b">

I heard the asst2 example was deleted in another branch so I deleted
that here too since we wanted the workspace access.

Release Notes:

- N/A

---------

Co-authored-by: Marshall <marshall@zed.dev>
2024-05-03 14:48:00 -07:00
Conrad Irwin
6bdcfad6ad Zeddish docs (#11372)
Co-Authored-By: Marshall <marshall@zed.dev>

Release Notes:

- N/A

---------

Co-authored-by: Marshall <marshall@zed.dev>
Co-authored-by: Marshall Bowers <elliott.codes@gmail.com>
2024-05-03 15:34:44 -06:00
apricotbucket28
86696d88cf wayland: Implement xdg-activation when opening urls (#11368)
Since Wayland doesn't have a way for windows to activate themselves,
currently, when you click on a link in Zed, the browser window opens in
the background.

This PR implements the `xdg-activation` protocol to get an activation
token, which the browser can use to raise its window.


https://github.com/zed-industries/zed/assets/71973804/8b3456c0-89f8-4201-b1cb-633a149796b7

Release Notes:

- N/A
2024-05-03 14:02:39 -07:00
Marshall Bowers
dccf6dae01 Setup docs deployments with mdBook (#11369)
This PR sets up deployments for the docs using mdBook.

Right now the new docs are hosted at
[zed.dev/docs2](https://zed.dev/docs2/).

The docs are deployed to Cloudflare Pages on merges to `main`, and we
have a Cloudflare Worker that routes traffic from `zed.dev/docs2` to the
docs deployment.

We can iterate on the docs for a bit, and then promote them to
`zed.dev/docs` when we're all ready for the switchover.

Release Notes:

- N/A

---------

Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
2024-05-03 15:52:15 -04:00
Kyle Kelley
6563330239 Supermaven (#10788)
Adds a supermaven provider for completions. There are various other
refactors amidst this branch, primarily to make copilot no longer a
dependency of project as well as show LSP Logs for global LSPs like
copilot properly.

This feature is not enabled by default. We're going to seek to refine it
in the coming weeks.

Release Notes:

- N/A

---------

Co-authored-by: Antonio Scandurra <me@as-cii.com>
Co-authored-by: Nathan Sobo <nathan@zed.dev>
Co-authored-by: Max <max@zed.dev>
Co-authored-by: Max Brunsfeld <maxbrunsfeld@gmail.com>
2024-05-03 12:50:42 -07:00
Conrad Irwin
610968815c Fix backwards mouse selection in vim mode (#11329)
Fixes: #8492

Release Notes:

- vim: Fixed last character of reversed mouse selections (#8492)
2024-05-03 10:29:30 -06:00
Marshall Bowers
ff56ca7280 toml: Bump to v0.1.1 (#11359)
This PR bumps the TOML extension to v0.1.1.

Changes:

- https://github.com/zed-industries/zed/pull/11251

Release Notes:

- N/A
2024-05-03 11:29:53 -04:00
Marshall Bowers
899f7113ba elixir: Bump to v0.0.3 (#11358)
This PR bumps the Elixir extension to v0.0.3.

Changes:

- https://github.com/zed-industries/zed/pull/11318

Release Notes:

- N/A
2024-05-03 11:12:35 -04:00
Jason Lee
beee79a9e7 toml: Fix language server installation on Windows (#11251)
Release Notes:

- N/A


---

Follow #11156, to make sure extensions install on window.

https://github.com/tamasfe/taplo/releases/tag/0.8.1

The Taplo have `gz` for windows, so we can just use `gz`.

---------

Co-authored-by: Marshall Bowers <elliott.codes@gmail.com>
2024-05-03 11:11:46 -04:00
Andrei Zvonimir Crnković
b3d969ef3c elixir: Check for next-ls and lexical in path first (#11318)
Since it's not longer possible to setup a local elixir LSP, @maxdeviant
proposed to look for `next-ls` and `lexical` in path first, just like
it's already done for `elixir_ls`.

For context take a look at #11297 (starting from [this
comment](https://github.com/zed-industries/zed/issues/11297#issuecomment-2091095537)).

Release Notes:

- N/A
2024-05-03 11:03:25 -04:00
Thorsten Ball
55555bb41f Enable first version of auto-updates on Linux (#11348)
This downloads Nightly/Preview releases on Linux and copies the contents
the `zed-<channel>.app` to `~/.local`.

What's missing:

- Check if we're not installed in ~/.local and abort
- Update `.desktop` file


Release Notes:

- N/A
2024-05-03 16:43:28 +02:00
Kirill Bulatov
f5e155b5a9 When clicking on a hunk expand it, do not move the caret (#11350)
Release Notes:

- N/A
2024-05-03 16:44:33 +03:00
Bennet Bo Fenner
36055505cd project panel: Allow confirming prompt with keyboard (#11346)
The ability to confirm the file deletion prompt by pressing "Enter" was
broken in #11015

Release Notes:

- Restored the ability to confirm a prompt by pressing "Enter" when
deleting/trashing files
2024-05-03 15:15:39 +02:00
Thorsten Ball
9348e6f7fb lsp: More information in error if server fails to start (#11343)
We do log that information, but we don't put it in the error message
where it's really useful.



Release Notes:

- N/A
2024-05-03 13:23:52 +02:00
José Olórtegui
f987ff05fd Improve JSDoc injection in comments (#10800)
This PR improves JSDoc injection for syntax highlighting. Now we are
only injecting JSDoc in block comments. The regex was mostly adapted
from nvim-treesitter's implementation (lua) to a rust regex.


eb93c3b2fb/queries/ecma/injections.scm (L1-L6)

**Before:**
<img width="441" alt="Screenshot 2024-04-20 at 5 51 04 AM"
src="https://github.com/zed-industries/zed/assets/20072509/8e77851d-22ad-4dc4-8e10-9ac558d3cf40">

**After:**
<img width="441" alt="Screenshot 2024-04-20 at 5 52 05 AM"
src="https://github.com/zed-industries/zed/assets/20072509/a607c219-6973-40c3-958c-44a003d008c3">

Release Notes:

- Changed detection of JSDoc to only do syntax highlighting in block
comments. Improved previous work done in #7826.
2024-05-03 11:43:10 +02:00
adorabilis
2306e3cd50 Add brackets and missing operators to Python grammar (#11180)
Release Notes:

- Fixed #4341 

Before:

![before](https://github.com/zed-industries/zed/assets/16101408/34672e47-5131-481a-803e-064db8126cc9)

After:

![after](https://github.com/zed-industries/zed/assets/16101408/7d2405c6-d04f-4738-ad2e-a9424b1c9d19)
2024-05-03 11:37:44 +02:00
Dom Christie
f252d9cf67 Fix alt-shift-(left|right) behaviour (#11292)
Not sure what the etiquette is here, but in the interest of fixing
#10242, I've re-implemented @jish's PR
https://github.com/zed-industries/zed/pull/10535 and have signed the CLA


Release Notes:

- Fixed `alt-shift-left` and `alt-shift-right` in the Textmate default
keybindings.
([#10242](https://github.com/zed-industries/zed/issues/10242))

TextMate keymap uses default option shift arrow selection
2024-05-03 11:29:47 +02:00
Thorsten Ball
5ce45908b1 install.sh: use per-channel binary names in ~/.local/bin (#11339)
Release Notes:

- N/A
2024-05-03 11:19:11 +02:00
Kirill Bulatov
cd03e473c8 Improve deleted hunk blocks' behavior (#11340)
* clear their selections on focus lost
* allow reverting diff hunks when the caret is inside of the deleted
hunk diff editor block

Release Notes:

- N/A
2024-05-03 12:18:50 +03:00
Thorsten Ball
e69e25c171 linux: Use app_id as filepath for desktop file (#11337)
This undoes the changes from #11333 and uses the path of the `.desktop`
file instead.

According ot the spec
(https://specifications.freedesktop.org/desktop-entry-spec/desktop-entry-spec-latest.html)
the filename and path of the `desktop` file are used to determine the
desktop file ID.

This is enough to match the windows (which have the same WMClass/app-id)
with the desktop entry.

Release Notes:

- N/A
2024-05-03 11:03:55 +02:00
Thorsten Ball
61a60d37a2 Tie the Zed application ID to the release channel (#11335)
Since we do want to have different versions of Zed running on the same
Linux install, we need to give them different application IDs so they're
not grouped together as the same application.

This changes the app_id depending on the releaes channel and, crucially,
it also matches them up with the bundle identifiers that we use on
macOS.

Release Notes:

- N/A
2024-05-03 10:48:35 +02:00
Matthias Grandl
4024b9ac4d gpui: Fix start_display_link leading to resource leak on hidden windows (#11289)
Release Notes:

- N/A

While developing [Loungy](https://loungy.app), I noticed that everytime
I wake my laptop, Loungy starts consuming 100% CPU. I traced it down to
`start_display_link` as there was this error message at the time of wake
up:

```
[2024-05-02T05:02:31Z ERROR util] /Users/matthias/zed/crates/gpui/src/platform/mac/window.rs:420: could not create display link, code: -6661
```

The timeline is this:

1. The application is hidden with `cx.hide()`
2. The system is put to sleep and later woken up
3. `window_did_change_screen` would trigger immediately after wakeup,
calling `start_display_link`
4. `start_display_link` fails catastrophically as `DisplayLink::new`
starts hogging all the CPU for some reason?
5. throws the error message above
6. Once the window is opened, `window_did_change_occlusion_state` it
retriggers `start_display_link` and the CPU issue subsides
2024-05-03 10:35:51 +02:00
Kirill Bulatov
b523ee6980 Use Rope instead of String for buffer diff base (#11300)
As an attempt to do things better when showing diff hunks, store diff
base as Rope, not String, to have cheaper clones when the diff base text
is reused, e.g. creating another buffer with the diff base text for hunk
diff expanding.

Release Notes:

- N/A
2024-05-03 11:18:43 +03:00
Thorsten Ball
5f0046b923 linux: Set StartupWMClass in .desktop file (#11333)
This has to match the WMClass/AppID, which was added here: #10909

Release Notes:

- N/A
2024-05-03 10:17:30 +02:00
Max Linke
155a80c6a5 Rename test (#11317)
This naming makes the purpose of the test clearer to people new to the
project.
2024-05-02 19:56:34 -06:00
Fernando Tagawa
7bc1025d91 Wayland: Fix segfault when exiting with ctrl+q (#11324)
Release Notes:

- N/A

When closing with `ctrl-q`, drop_window is not called and results in a
segfault.
2024-05-02 16:39:08 -07:00
Nathan Sobo
b58bf64f0a Increase rate limits for computing embeddings (#11326)
- Also, remove the rate limit for getting cached embeddings entirely.

Release Notes:

- N/A
2024-05-02 16:36:45 -07:00
Nate Butler
47b38a0428 Tidy Assistant2 composer (#11321)
Release Notes:

- N/A
2024-05-02 17:54:55 -04:00
Kyle Kelley
1915a756a0 Allow codebase search to be turned on or off within the composer for assistant2 (#11315)
![image](https://github.com/zed-industries/zed/assets/836375/e03d2357-e2e4-49f1-86d6-7593bce13618)


![image](https://github.com/zed-industries/zed/assets/836375/3d769622-82e1-4e6f-bdec-4dce81e43423)


![image](https://github.com/zed-industries/zed/assets/836375/bf79a4ec-1660-47b1-8525-e741575dc5d4)

Release Notes:

- N/A
2024-05-02 13:26:46 -07:00
Max Brunsfeld
43ad470e58 Use outline queries to chunk files syntactically (#11283)
This chunking strategy uses the existing `outline` query to chunk files.
We try to find chunk boundaries that are:

* at starts or ends of lines
* nested within as few outline items as possible

Release Notes:

- N/A
2024-05-02 12:28:21 -07:00
Conrad Irwin
1abd58070b Slicker remote project creation (#11309)
Inline the editor into the modal

Release Notes:

- N/A

---------

Co-authored-by: Bennet <bennetbo@gmx.de>
2024-05-02 12:46:52 -06:00
Nate Butler
78a8a58ee2 Iterate on Assistant 2 composer UI (#11306)
- Change project index tool rendering in composer
- Update composer UI style

Release Notes:

- N/A

---------

Co-authored-by: Marshall Bowers <1486634+maxdeviant@users.noreply.github.com>
Co-authored-by: Marshall Bowers <elliott.codes@gmail.com>
2024-05-02 13:01:21 -04:00
Conrad Irwin
9bac64a9c1 Rename RemoteProject -> DevServerProject (#11301)
Co-Authored-By: Mikayla <mikayla@zed.dev>

In a fit of ill-advisedness I called these things remote projects;
forgetting that remote project is also what we call collaboratively
shared projects.

Release Notes:

- N/A

---------

Co-authored-by: Mikayla <mikayla@zed.dev>
Co-authored-by: Bennet <bennetbo@gmx.de>
2024-05-02 11:00:08 -06:00
Marshall Bowers
d61c47d2a9 elixir: Bump to v0.0.2 (#11303)
This PR bumps the Elixir extension to v0.0.2.

Changes:

- https://github.com/zed-industries/zed/pull/11302

Release Notes:

- N/A
2024-05-02 11:26:04 -04:00
Marshall Bowers
8b284331a3 elixir: Respect LSP settings for elixir-ls (#11302)
This PR updates the Elixir extension to respect the LSP settings for
`elixir-ls`:

```json
"lsp": {
  "elixir-ls": {
    "settings": {
      "dialyzerEnabled": false
    }
  }
}
```

```
Received workspace/didChangeConfiguration
Received client configuration via workspace/didChangeConfiguration
%{"dialyzerEnabled" => false}
Loaded DETS databases in 33ms
Loaded DETS databases in 10ms
Compiling 65 files (.ex)
```

Release Notes:

- N/A
2024-05-02 11:16:09 -04:00
Joseph T. Lyons
98ea5e172e Add convert to opposite case command (#11290) 2024-05-02 08:37:13 -04:00
Kirill Bulatov
092869d1fa Use default allocator as a default (#11293) 2024-05-02 14:05:42 +03:00
Kirill Bulatov
aeff468e9c Fix text offset in the expanded deleted hunk (#11295) 2024-05-02 13:38:42 +03:00
Thorsten Ball
23f191450e Do not show inline blame information on empty lines (#11294)
cc @iamnbutler 

Release Notes:

- Changed inline git blame information to not show up on empty lines.

Demo/proof:



https://github.com/zed-industries/zed/assets/1185253/e506cf1f-81b1-407b-8dc7-1666b31ae142
2024-05-02 12:22:51 +02:00
Tarun Verghis
edff78e722 Allow ignoring soft wraps when moving to line ends (#11153)
Release Notes:

- Fixed #10888

This patch addresses behavior of
`Editor::move_to_{beginning|end}_of_line`. It adds a setting,
`stop_at_soft_wraps` when defining a keymap for the
`editor::MoveToBeginningOfLine` and `editor::MoveToEndOfLine` actions.
When `true`, it causes movement to the either end of the line (via, for
example Home or End), to go to the logical end, as opposed to the
nearest soft wrap point in the respective direction.

---------

Co-authored-by: Kirill Bulatov <kirill@zed.dev>
2024-05-02 12:30:45 +03:00
Thorsten Ball
c81230405f typescript: Complete function calls with snippets (#11157)
This allows function call (i.e. snippet) completion with
`typescript-language-server`. So far that didn't work, because
`typescript-language-server` doesn't respond with `insertText` when
getting the completions, but only when then sending
`completionItem/resolve` requests. See:
https://github.com/hrsh7th/nvim-cmp/issues/646#issuecomment-992765479

What this PR does is to support text edits in the response to
`completionItem/resolve`, which means updating the completion item.

It then enables this feature by default for
`typescript-language-server`.


TODOs:

- [x] Make this work over collab
- [x] Test that this doesn't break existing language server support
- [x] Refactor duplicated code

Release Notes:

- Added support for function call completion when using
`typescript-language-server`. This will result in parameters being
added, which can then be changed and navigated with `<tab>`. For this to
work with `typescript-language-server`, the documentation for a given
completion item needs to be resolved, meaning that if one types very
quickly and accepts completion before `typescript-language-server` could
respond with the documentation, no full function completion is used.

Demo:


https://github.com/zed-industries/zed/assets/1185253/c23ebe12-5902-4b50-888c-d9b8cd32965d
2024-05-02 10:50:01 +02:00
Conrad Irwin
d8ca15372c linux preview… (#11286)
Release Notes:

- (preview only) You can now install an (alpha) preview build on linux
2024-05-01 21:51:38 -06:00
Conrad Irwin
b487f2ce6f Don't iterate over all system processes (#11281)
Release Notes:

- Fixed a UI beachball when gathering process information
2024-05-01 20:08:56 -06:00
Conrad Irwin
3b5fd4ea66 Only keep one blame up-to-date (#11274)
I was experiencing hang, and we blamed it on spawning a few hundred git
processes
simultaneously.

cc @MrNugget

Release Notes:

- Fixed slowness with hundreds of buffers open doing git blame.
2024-05-01 16:25:26 -06:00
Conrad Irwin
eb0f1e71f7 Show dev server in the titlebar for remote projects (#11276)
Co-Authored-By: Mikayla <mikayla@zed.dev>

Release Notes:

- Show server name in the titlebar for remote projects

Co-authored-by: Mikayla <mikayla@zed.dev>
2024-05-01 16:24:58 -06:00
Marshall Bowers
74f8ef0364 Clean up Indicator (#11275)
This PR cleans up the `Indicator` component:

- Renamed `IndicatorStyle` to `IndicatorKind` and made it private.
- Fixed `Indicator::bar()` to construct an indicator using the right
`IndicatorKind`.
- Removed the `IndicatorIcon`, since we didn't actually end up using it.

Release Notes:

- N/A
2024-05-01 18:10:35 -04:00
Conrad Irwin
fa0253bc5a Disallow adding folders to projects and opening the terminal in remote projects (#11261)
Release Notes:

- Fixed broken UI in remote projects

---------

Co-authored-by: Mikayla <mikayla@zed.dev>
2024-05-01 16:04:59 -06:00
Conrad Irwin
e5b88ac8c2 Use HIGH priority to wake blocked timers (#11269)
One contributor to some beach-balls was that the main thread was calling
block_with_timeout, but the timeout never fired.

Release Notes:

- Reduced main thread hangs under very high system load
2024-05-01 16:03:27 -06:00
Marshall Bowers
4739797e5d Add the ability to render icons as indicators (#11273)
This PR adds the ability to render `Icon`s as an `Indicator`.

Release Notes:

- N/A

Co-authored-by: Nate Butler <nate@zed.dev>
2024-05-01 17:52:26 -04:00
Marshall Bowers
4f5312804d Replace rem size literals with constant (#11272)
This PR replaces the rem size literals with the `BASE_REM_SIZE_IN_PX`
constant.

This way we can retain our source of truth for the rem size that we're
designing against.

Release Notes:

- N/A
2024-05-01 17:46:43 -04:00
Max Brunsfeld
a194c0aa6e Compile and instantiate wasm modules on a background thread (#11270)
Release Notes:

- Fixed a hang that could occur when loading some extensions, by loading
extensions on a background thread.
2024-05-01 14:34:54 -07:00
Marshall Bowers
fb0d2e948f assistant2: Use new icon in status bar (#11268)
This PR updates the new assistant to use the new (temporary) icon in the
status bar to distinguish it.

We added the icon in #11257, but hadn't wired it up.

Release Notes:

- N/A
2024-05-01 16:52:09 -04:00
Joseph T. Lyons
aa1b4c7868 Add an explore extensions button to welcome screen (#11265)
We are still getting comments from users saying they didn't know we had
extensions until just now. This doesn't fix anything for existing users,
but it helps make new users more aware, earlier on. There's probably
more we need to do for discoverability, but this feels like it should
help.

Release Notes:

- N/A
2024-05-01 16:07:24 -04:00
Max Linke
dad3cbb960 Enable random_channel_buffer_tests to run on Linux (#10863)
With this patch applied I can see the following line in the test output:

test tests::random_channel_buffer_tests::test_random_channel_buffers has
been running for over 60 seconds

Without it I run in a failure with 'Invalid keystroke `cmk-,`



Release Notes:

- N/A

---------

Co-authored-by: Max Linke <kain88-de@users.noreply.github.com>
Co-authored-by: Marshall Bowers <elliott.codes@gmail.com>
2024-05-01 15:49:18 -04:00
Kirill Bulatov
caa0d35b8b Allow to toggle git hunk diffs (#11080)
Part of https://github.com/zed-industries/zed/issues/4523

Added two new actions with the default keybindings

```
"cmd-'": "editor::ToggleHunkDiff",
"cmd-\"": "editor::ExpandAllHunkDiffs",
```

that allow to browse git hunk diffs in Zed:


https://github.com/zed-industries/zed/assets/2690773/9a8a7d10-ed06-4960-b4ee-fe28fc5c4768


The hunks are dynamic and alter on user folds and modifications, or
toggle hidden, if the modifications were not adjacent to the expanded
hunk.


Release Notes:

- Added `editor::ToggleHunkDiff` (`cmd-'`) and
`editor::ExpandAllHunkDiffs` (`cmd-"`) actions to browse git hunk diffs
in Zed
2024-05-01 22:47:36 +03:00
Max Brunsfeld
5831d80f51 Return an error from project index tool when embedding query fails (#11264)
Previously, a failure to embed the search query (due to a rate limit
error) would appear the same as if there were no results.

* Avoid repeatedly embedding the search query for each worktree
* Unify tasks for searching all worktree

Release Notes:

- N/A
2024-05-01 12:15:44 -07:00
Andrew Lygin
4b767697af Fix scrollbar auto-show for cursors out of sight (#11244)
This PR fixes scrollbar auto-show feature when there're not visible
cursors. The intial behavior was broken in #11147.

The problem is that `selections` only contains visible selections, so
the `if` with `non_visible_cursors |= true` is only visited in rare edge
cases when we have at least part of the selection still visible on the
screen. But when we scroll far enough from the cursor,
`non_visible_cursors` keeps its default `false` value, which is
incorrect.

Release Notes:

- N/A
2024-05-01 21:57:10 +03:00
Marshall Bowers
3dc5d48e0c assistant2: Add ChatNotice component (#11262)
This PR adds a new `ChatNotice` component for displaying notices within
the assistant.

We will be able to use this for allowing the user to confirm a project
index request.

<img width="518" alt="Screenshot 2024-05-01 at 2 41 31 PM"
src="https://github.com/zed-industries/zed/assets/1486634/3beaeeaf-6046-4284-ac0a-4248e1f9ac20">

Release Notes:

- N/A
2024-05-01 14:51:28 -04:00
Klaus Hipp
0858e3f0e2 Update Preview release installation instructions (#11263)
This PR updates the Homebrew installation instructions for the Preview
release, which has been migrated to ```homebrew/cask``` as
```zed@preview``` (see
[Commit](bad25d79e1)).

Release Notes:

- N/A
2024-05-01 14:51:11 -04:00
Nate Butler
97512be378 Add wiring for UI density (#11260)
Note: You shouldn't use the `unstable.ui_density` setting – it is only
being added for testing and to enable new UI components to be built with
density in mind. Don't expect this to work well, or at all right now.

Adds some of the basic wiring we'll need to start scaling UI elements
throughout the app based on a desired density setting.

Release Notes:

- N/A
2024-05-01 14:28:52 -04:00
Conrad Irwin
0fce20d8da More element arena (#11258)
Co-Authored-By: Mikayla <mikayla@zed.dev>

My Zed was running out with collab + chat + recent projects + two splits
on a large monitor


Release Notes:

- N/A

Co-authored-by: Mikayla <mikayla@zed.dev>
2024-05-01 12:00:27 -06:00
Nate Butler
779f67506a Assistant 2 temp icon (#11257)
Release Notes:

- N/A
2024-05-01 12:56:27 -04:00
Conrad Irwin
15eebb7fd2 blerg (#11254)
Release Notes:

- N/A
2024-05-01 10:22:44 -06:00
Conrad Irwin
e65a6fd8b6 Revert tyop (#11253)
Follup up from
https://github.com/zed-industries/zed/pull/11229#pullrequestreview-2033556336

Release Notes:

- N/A
2024-05-01 10:15:52 -06:00
Bennet Bo Fenner
8907d111e3 project panel: Adjust formatting of trash/delete action (#11252)
This PR removes a confusing quotation mark when trashing/deleting a
file, introduced in #10875.
Also makes it clear that the file is actually going into trash instead
of being deleted permanently

Before (trash):
<img width="266" alt="image"
src="https://github.com/zed-industries/zed/assets/53836821/f94a6257-62d0-4a3f-bc44-19e36bb28f67">

After (trash):
<img width="285" alt="image"
src="https://github.com/zed-industries/zed/assets/53836821/becb8143-06cf-4b5e-a52e-be2726df5955">

Before (delete):
<img width="263" alt="image"
src="https://github.com/zed-industries/zed/assets/53836821/e109a127-5f9c-4eeb-950e-4c4d5157061b">


After (delete):
<img width="260" alt="image"
src="https://github.com/zed-industries/zed/assets/53836821/246d0d94-e3d2-4279-84e2-74b313fe00a7">



Release Notes:

- Fixed formatting of prompt action when trashing/deleting a file
2024-05-01 18:07:19 +02:00
Joseph T. Lyons
32c9714a63 v0.135.x dev 2024-05-01 11:33:03 -04:00
Marshall Bowers
eb4ac7e5da Suggest the SCSS extension for .scss files (#11249)
This PR adds a suggestion for the new [SCSS
extension](https://github.com/bajrangCoder/zed-scss) when `.scss` files
are opened.

Release Notes:

- Added a suggestion for the SCSS extension when `.scss` files are
opened.
2024-05-01 09:26:22 -04:00
Conrad Irwin
6b55a6f0e1 Retain run loop (#11241)
Contributes: #11168


https://developer.apple.com/documentation/corefoundation/1542428-cfrunloopgetcurrent
implies that we should be `CFRetain`ing the run loop.

Lets do that, and see if it reduces the number of crashes we see.

Release Notes:

- (maybe) Fix a rare crash in watching settings files.
2024-04-30 23:31:59 -06:00
Zorin FOSS
45cf101f6d Add MimeType to open directory with zed (#11238)
Release Notes:

- open directory using context menu (Right click -> open with -> zed).
2024-04-30 21:56:45 -06:00
Hans
f39a24255c Adjust the yss surrounds operator (#11212)
For #11084 In the case of an indentation in front of the current line,
it may also be necessary to deal with the start point of the selected
range


Release Notes:


- N/A

---------

Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
2024-04-30 21:50:04 -06:00
Marshall Bowers
c9778a36ff Use Elixir LS as the default language server for Elixir (#11234)
This PR adds `language_servers` settings for Elixir and HEEX to ensure
they both only use Elixir LS by default.

Eventually we'd like to have these provided by the Elixir extension, but
there are some outstanding questions on the design.

For now we can just use the built-in default settings to ensure a good
out-of-the-box experience for Elixir users.

Release Notes:

- N/A
2024-04-30 19:28:27 -04:00
Nathan Sobo
d1abbb1429 assistant2: Refine editing of previous messages (#11233)
This PR refines the UX of editing a previous message, including the
following:

- Focus the prev message body editor on double-click
- Restore previous body text on cancel
- Cancel pending completion upon submission of previous message
- Drive-by: Remove min height on composer editor

Release Notes:

- N/A

---------

Co-authored-by: Marshall Bowers <marshall@zed.dev>
2024-04-30 16:48:39 -06:00
Conrad Irwin
b18ca1585e Fix merge conflict (#11232)
Release Notes:

- N/A
2024-04-30 16:26:10 -06:00
Conrad Irwin
93fbca4242 Bundle linux preview releases too (#11231)
Release Notes:

- Added preview builds for linux
2024-04-30 16:10:48 -06:00
Conrad Irwin
3752ed294d open new buffer (#11203)
Release Notes:

- Allow creating new untitled buffers in remote projects

TODO:
- Add a Test
- Fix version number check
2024-04-30 16:09:43 -06:00
Conrad Irwin
28bcc95468 installer (#11229)
Release Notes:

- N/A

---------

Co-authored-by: Mikayla <mikayla@zed.dev>
2024-04-30 16:01:07 -06:00
Marshall Bowers
d55b637b7e assistant2: Fix the height of collapsed messages (#11230)
This PR fixes the height of collapsed messages, addressing the
associated TODO comment.

Release Notes:

- N/A
2024-04-30 17:44:51 -04:00
Marshall Bowers
f2a1226e18 assistant2: Setup storybook (#11228)
This PR sets up the `assistant2` crate with the storybook so that UI
elements can be iterated on in isolation.

To start, we have some stories for the `ChatMessage` component:

```sh
cargo run -p storybook -- components/assistant_chat_message
```

<img width="1233" alt="Screenshot 2024-04-30 at 5 20 03 PM"
src="https://github.com/zed-industries/zed/assets/1486634/510967ea-0e9b-4fa9-94fb-421ee74bcc45">

Release Notes:

- N/A
2024-04-30 17:33:32 -04:00
Marshall Bowers
96b1fc4650 assistant2: Align chat messages with composer (#11227)
This PR adds some additional spacing to the composer so it aligns with
the chat messages.

Release Notes:

- N/A
2024-04-30 17:00:23 -04:00
Conrad Irwin
1d4814e5b6 installer (#11224)
Release Notes:

- N/A

---------

Co-authored-by: Mikayla <mikayla@zed.dev>
2024-04-30 14:25:12 -06:00
Conrad Irwin
cb7350174e Don't allow dropping files on remote projects (#11218)
Release Notes:

- Fixed a panic when a remote participant dropped a local file on your
project
2024-04-30 14:24:47 -06:00
Marshall Bowers
f11a7811f3 assistant2: Use composer for editing inline messages (#11222)
This PR updates the assistant to render historical user messages the
same as ones from the assistant.

Double-clicking on a message will open a composer inline for editing.
Pressing `Esc` will cancel the edit.

We don't yet restore the previous state of the message upon canceling.

<img width="401" alt="Screenshot 2024-04-30 at 4 04 01 PM"
src="https://github.com/zed-industries/zed/assets/1486634/5f253fa8-6578-4054-be30-c495e326d700">

<img width="401" alt="Screenshot 2024-04-30 at 4 04 28 PM"
src="https://github.com/zed-industries/zed/assets/1486634/edf25cea-d97e-44d2-8772-3690eac017a4">


Release Notes:

- N/A
2024-04-30 16:12:20 -04:00
Marshall Bowers
e8ee0131f1 Remove unused assistant prompt (#11221)
This PR removes an unused assistant prompt.

Release Notes:

- N/A

Co-authored-by: Nathan <nathan@zed.dev>
2024-04-30 15:32:11 -04:00
Max Brunsfeld
38b9d5cc36 Fix some semantic index issues (#11216)
* [x] Fixed an issue where embeddings would be assigned incorrectly to
files if a subset of embedding batches failed
* [x] Added a command to debug which paths are present in the semantic
index
* [x] Determine why so many paths are often missing from the semantic
index
* we erroring out if an embedding batch contained multiple texts that
were the same, which can happen if a worktree contains multiple copies
of the same text (e.g. a license).

Release Notes:

- N/A

---------

Co-authored-by: Marshall <marshall@zed.dev>
Co-authored-by: Nathan <nathan@zed.dev>
Co-authored-by: Kyle <kylek@zed.dev>
Co-authored-by: Kyle Kelley <rgbkrk@gmail.com>
2024-04-30 10:55:38 -07:00
Marshall Bowers
d01428e69c assistant2: Add create buffer tool (#11219)
This PR adds a new tool to the `assistant2` crate that allows the
assistant to create a new buffer with some content.

Release Notes:

- N/A

---------

Co-authored-by: Nathan <nathan@zed.dev>
2024-04-30 13:43:25 -04:00
Piotr Osiewicz
ada2791fa3 editor: Move code actions menu closer to the indicator when it is deployed via click (#11214)
Before:

![image](https://github.com/zed-industries/zed/assets/24362066/98d633a7-c982-4522-b4dc-b944b70b8081)

After: 

![image](https://github.com/zed-industries/zed/assets/24362066/79931e12-0e6c-4ece-b734-5af7d02f7e50)

Release Notes:

- N/A
2024-04-30 18:51:20 +02:00
Marshall Bowers
713c314d67 Clean up doc comments on LanguageModelTool trait (#11217)
This PR cleans up the doc comments on the `LanguageModelTool` trait to
follow Rust documentation conventions.

Release Notes:

- N/A
2024-04-30 12:32:59 -04:00
Marshall Bowers
e3de440715 assistant2: Restructure tools in preparation for adding more (#11213)
This PR does a slight restructuring of how tools are defined in the
`assistant2` crate to make it more amenable to adding more tools in the
near future.

Release Notes:

- N/A
2024-04-30 11:12:44 -04:00
Thorsten Ball
d743c19fe2 Add script to package Linux binary into archive (#11165)
Release Notes:

- N/A
2024-04-30 14:56:48 +02:00
Thorsten Ball
73d0600ad2 project: Set completion to undocumented if text empty (#11207)
I think the previous code was missing a `return` in there because it
always overwrote the `completion.documentation` field, even if the
`text.is_empty()` is true.

Release Notes:

- N/A
2024-04-30 14:56:22 +02:00
Stanislav Alekseev
f96cab286c Add avatar support for codeberg in git blame (#10991)
Release Notes:

- Added support for avatars in git blame for repositories hosted on
codeberg

<img width="1144" alt="Screenshot 2024-04-25 at 16 45 22"
src="https://github.com/zed-industries/zed/assets/43210583/d44770d8-44ea-4c6b-a1c0-ac2d1d49408f">

Questions:
- Should we move git stuff like `Commit`, `Author`, etc outside of
hosting-specific files (I don't think so, as other hostings can have
different stuff)
- Should we also add support for self hosted forgejo instances or should
it be a different PR?
2024-04-30 12:57:10 +02:00
400 changed files with 23539 additions and 10281 deletions

View File

@@ -6,6 +6,8 @@ Release Notes:
Optionally, include screenshots / media showcasing your addition that can be included in the release notes.
**or**
### Or...
Release Notes:
- N/A

View File

@@ -260,15 +260,13 @@ jobs:
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
bundle-deb:
name: Create a *.deb Linux bundle
bundle-linux:
name: Create a Linux bundle
runs-on: ubuntu-22.04 # keep the version fixed to avoid libc and dynamic linked library issues
if: ${{ startsWith(github.ref, 'refs/tags/v') || contains(github.event.pull_request.labels.*.name, 'run-bundling') }}
needs: [linux_tests]
env:
ZED_CLIENT_CHECKSUM_SEED: ${{ secrets.ZED_CLIENT_CHECKSUM_SEED }}
DIGITALOCEAN_SPACES_ACCESS_KEY: ${{ secrets.DIGITALOCEAN_SPACES_ACCESS_KEY }}
DIGITALOCEAN_SPACES_SECRET_KEY: ${{ secrets.DIGITALOCEAN_SPACES_SECRET_KEY }}
steps:
- name: Checkout repo
uses: actions/checkout@v4
@@ -312,28 +310,26 @@ jobs:
exit 1
fi
# TODO linux : Find a way to add licenses to the final bundle
# - name: Generate license file
# run: script/generate-licenses
- name: Generate license file
run: script/generate-licenses
- name: Create Linux *.deb bundle
- name: Create and upload Linux .tar.gz bundle
run: script/bundle-linux
- name: Upload app bundle to workflow run if main branch or specific label
- name: Upload Linux bundle to workflow run if main branch or specific label
uses: actions/upload-artifact@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 }}.deb
path: target/release/*.deb
name: zed-${{ github.event.pull_request.head.sha || github.sha }}-x86_64-unknown-linux-gnu.tar.gz
path: zed-*.tar.gz
# TODO linux : make it stable enough to be uploaded as a release
# - uses: softprops/action-gh-release@v1
# name: Upload app bundle to release
# if: ${{ env.RELEASE_CHANNEL == 'preview' || env.RELEASE_CHANNEL == 'stable' }}
# with:
# draft: true
# prerelease: ${{ env.RELEASE_CHANNEL == 'preview' }}
# files: target/release/Zed.dmg
# body: ""
# env:
# GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Upload app bundle to release
uses: softprops/action-gh-release@v1
if: ${{ env.RELEASE_CHANNEL == 'preview' }}
with:
draft: true
prerelease: ${{ env.RELEASE_CHANNEL == 'preview' }}
files: target/release/zed-linux-x86_64.tar.gz
body: ""
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

35
.github/workflows/deploy_docs.yml vendored Normal file
View File

@@ -0,0 +1,35 @@
name: Deploy Docs
on:
push:
branches:
- main
jobs:
deploy-docs:
name: Deploy Docs
runs-on: ubuntu-latest
steps:
- name: Checkout repo
uses: actions/checkout@v4
with:
clean: false
- name: Setup mdBook
uses: peaceiris/actions-mdbook@v2
with:
mdbook-version: "0.4.37"
- name: Build book
run: |
set -euo pipefail
mkdir -p target/deploy
mdbook build ./docs --dest-dir=../target/deploy/docs/
- name: Deploy
uses: cloudflare/wrangler-action@v3
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
command: pages deploy target/deploy --project-name=docs

View File

@@ -94,7 +94,7 @@ jobs:
run: script/upload-nightly macos
bundle-deb:
name: Create a *.deb Linux bundle
name: Create a Linux *.tar.gz bundle
if: github.repository_owner == 'zed-industries'
runs-on: ubuntu-22.04 # keep the version fixed to avoid libc and dynamic linked library issues
needs: tests
@@ -125,12 +125,11 @@ jobs:
echo "Publishing version: ${version} on release channel nightly"
echo "nightly" > crates/zed/RELEASE_CHANNEL
# TODO linux : find a way to add licenses to the final bundle
# - name: Generate license file
# run: script/generate-licenses
- name: Generate license file
run: script/generate-licenses
- name: Create Linux *.deb bundle
- name: Create Linux .tar.gz bundle
run: script/bundle-linux
- name: Upload Zed Nightly
run: script/upload-nightly linux-deb
run: script/upload-nightly linux-targz

215
Cargo.lock generated
View File

@@ -16,7 +16,6 @@ dependencies = [
"project",
"smallvec",
"ui",
"util",
"workspace",
]
@@ -385,6 +384,7 @@ dependencies = [
"feature_flags",
"fs",
"futures 0.3.28",
"fuzzy",
"gpui",
"language",
"languages",
@@ -392,6 +392,7 @@ dependencies = [
"nanoid",
"node_runtime",
"open_ai",
"picker",
"project",
"rand 0.8.5",
"release_channel",
@@ -401,6 +402,7 @@ dependencies = [
"serde",
"serde_json",
"settings",
"story",
"theme",
"ui",
"util",
@@ -412,10 +414,17 @@ name = "assistant_tooling"
version = "0.1.0"
dependencies = [
"anyhow",
"collections",
"futures 0.3.28",
"gpui",
"project",
"schemars",
"serde",
"serde_json",
"settings",
"sum_tree",
"unindent",
"util",
]
[[package]]
@@ -1481,7 +1490,7 @@ dependencies = [
[[package]]
name = "blade-graphics"
version = "0.4.0"
source = "git+https://github.com/kvark/blade?rev=e82eec97691c3acdb43494484be60d661edfebf3#e82eec97691c3acdb43494484be60d661edfebf3"
source = "git+https://github.com/kvark/blade?rev=f5766863de9dcc092e90fdbbc5e0007a99e7f9bf#f5766863de9dcc092e90fdbbc5e0007a99e7f9bf"
dependencies = [
"ash",
"ash-window",
@@ -1511,7 +1520,7 @@ dependencies = [
[[package]]
name = "blade-macros"
version = "0.2.1"
source = "git+https://github.com/kvark/blade?rev=e82eec97691c3acdb43494484be60d661edfebf3#e82eec97691c3acdb43494484be60d661edfebf3"
source = "git+https://github.com/kvark/blade?rev=f5766863de9dcc092e90fdbbc5e0007a99e7f9bf#f5766863de9dcc092e90fdbbc5e0007a99e7f9bf"
dependencies = [
"proc-macro2",
"quote",
@@ -2272,6 +2281,7 @@ dependencies = [
"collections",
"ctor",
"dashmap",
"dev_server_projects",
"editor",
"env_logger",
"envy",
@@ -2279,6 +2289,7 @@ dependencies = [
"fs",
"futures 0.3.28",
"git",
"git_hosting_providers",
"google_ai",
"gpui",
"headless",
@@ -2301,7 +2312,6 @@ dependencies = [
"prost",
"rand 0.8.5",
"release_channel",
"remote_projects",
"reqwest",
"rpc",
"rustc-demangle",
@@ -2316,6 +2326,7 @@ dependencies = [
"sha2 0.10.7",
"sqlx",
"subtle",
"supermaven_api",
"telemetry_events",
"text",
"theme",
@@ -2344,6 +2355,7 @@ dependencies = [
"clock",
"collections",
"db",
"dev_server_projects",
"editor",
"emojis",
"extensions_ui",
@@ -2359,6 +2371,7 @@ dependencies = [
"pretty_assertions",
"project",
"recent_projects",
"release_channel",
"rich_text",
"rpc",
"schemars",
@@ -2510,30 +2523,10 @@ dependencies = [
"async-compression",
"async-std",
"async-tar",
"client",
"clock",
"collections",
"command_palette_hooks",
"fs",
"futures 0.3.28",
"gpui",
"language",
"lsp",
"node_runtime",
"parking_lot",
"rpc",
"serde",
"settings",
"smol",
"util",
]
[[package]]
name = "copilot_ui"
version = "0.1.0"
dependencies = [
"anyhow",
"client",
"copilot",
"editor",
"fs",
"futures 0.3.28",
@@ -2542,14 +2535,18 @@ dependencies = [
"language",
"lsp",
"menu",
"node_runtime",
"parking_lot",
"project",
"rpc",
"serde",
"serde_json",
"settings",
"smol",
"theme",
"ui",
"util",
"workspace",
"zed_actions",
]
[[package]]
@@ -3176,6 +3173,18 @@ dependencies = [
"syn 1.0.109",
]
[[package]]
name = "dev_server_projects"
version = "0.1.0"
dependencies = [
"anyhow",
"client",
"gpui",
"rpc",
"serde",
"serde_json",
]
[[package]]
name = "diagnostics"
version = "0.1.0"
@@ -3396,6 +3405,7 @@ dependencies = [
"smol",
"snippet",
"sum_tree",
"task",
"text",
"theme",
"time",
@@ -4105,6 +4115,7 @@ name = "fsevent"
version = "0.1.0"
dependencies = [
"bitflags 2.4.2",
"core-foundation",
"fsevent-sys 3.1.0",
"parking_lot",
"tempfile",
@@ -4384,14 +4395,16 @@ name = "git"
version = "0.1.0"
dependencies = [
"anyhow",
"async-trait",
"clock",
"collections",
"derive_more",
"git2",
"gpui",
"lazy_static",
"log",
"parking_lot",
"pretty_assertions",
"regex",
"rope",
"serde",
"serde_json",
@@ -4418,6 +4431,25 @@ dependencies = [
"url",
]
[[package]]
name = "git_hosting_providers"
version = "0.1.0"
dependencies = [
"anyhow",
"async-trait",
"futures 0.3.28",
"git",
"gpui",
"isahc",
"pretty_assertions",
"regex",
"serde",
"serde_json",
"unindent",
"url",
"util",
]
[[package]]
name = "glob"
version = "0.3.1"
@@ -4466,6 +4498,7 @@ name = "go_to_line"
version = "0.1.0"
dependencies = [
"anyhow",
"collections",
"editor",
"gpui",
"indoc",
@@ -4603,6 +4636,7 @@ dependencies = [
"wayland-client",
"wayland-cursor",
"wayland-protocols",
"wayland-protocols-plasma",
"windows 0.53.0",
"x11rb",
"xkbcommon",
@@ -5127,6 +5161,30 @@ dependencies = [
"syn 2.0.59",
]
[[package]]
name = "inline_completion_button"
version = "0.1.0"
dependencies = [
"anyhow",
"copilot",
"editor",
"fs",
"futures 0.3.28",
"gpui",
"indoc",
"language",
"lsp",
"project",
"serde_json",
"settings",
"supermaven",
"theme",
"ui",
"util",
"workspace",
"zed_actions",
]
[[package]]
name = "inotify"
version = "0.9.6"
@@ -5532,6 +5590,7 @@ dependencies = [
"anyhow",
"client",
"collections",
"copilot",
"editor",
"env_logger",
"futures 0.3.28",
@@ -5675,9 +5734,9 @@ checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058"
[[package]]
name = "libmimalloc-sys"
version = "0.1.35"
version = "0.1.37"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3979b5c37ece694f1f5e51e7ecc871fdb0f517ed04ee45f88d15d6d553cb9664"
checksum = "81eb4061c0582dedea1cbc7aff2240300dd6982e0239d1c99e65c1dbf4a30ba7"
dependencies = [
"cc",
"libc",
@@ -6042,9 +6101,9 @@ dependencies = [
[[package]]
name = "mimalloc"
version = "0.1.39"
version = "0.1.41"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fa01922b5ea280a911e323e4d2fd24b7fe5cc4042e0d2cda3c40775cdc4bdc9c"
checksum = "9f41a2280ded0da56c8cf898babb86e8f10651a34adcfff190ae9a1159c6908d"
dependencies = [
"libmimalloc-sys",
]
@@ -6786,6 +6845,7 @@ dependencies = [
name = "outline"
version = "0.1.0"
dependencies = [
"collections",
"editor",
"fuzzy",
"gpui",
@@ -7405,7 +7465,6 @@ dependencies = [
"client",
"clock",
"collections",
"copilot",
"env_logger",
"fs",
"futures 0.3.28",
@@ -7789,6 +7848,7 @@ name = "recent_projects"
version = "0.1.0"
dependencies = [
"anyhow",
"dev_server_projects",
"editor",
"feature_flags",
"fuzzy",
@@ -7798,7 +7858,6 @@ dependencies = [
"ordered-float 2.10.0",
"picker",
"project",
"remote_projects",
"rpc",
"serde",
"serde_json",
@@ -7933,18 +7992,6 @@ dependencies = [
"once_cell",
]
[[package]]
name = "remote_projects"
version = "0.1.0"
dependencies = [
"anyhow",
"client",
"gpui",
"rpc",
"serde",
"serde_json",
]
[[package]]
name = "rend"
version = "0.4.0"
@@ -8700,7 +8747,12 @@ dependencies = [
"sha2 0.10.7",
"smol",
"tempfile",
"theme",
"tree-sitter",
"ui",
"unindent",
"util",
"workspace",
"worktree",
]
@@ -9498,6 +9550,7 @@ name = "storybook"
version = "0.1.0"
dependencies = [
"anyhow",
"assistant2",
"clap 4.4.4",
"collab_ui",
"ctrlc",
@@ -9586,6 +9639,43 @@ dependencies = [
"rayon",
]
[[package]]
name = "supermaven"
version = "0.1.0"
dependencies = [
"anyhow",
"client",
"collections",
"editor",
"env_logger",
"futures 0.3.28",
"gpui",
"language",
"log",
"postage",
"project",
"serde",
"serde_json",
"settings",
"smol",
"supermaven_api",
"theme",
"ui",
"util",
]
[[package]]
name = "supermaven_api"
version = "0.1.0"
dependencies = [
"anyhow",
"futures 0.3.28",
"serde",
"serde_json",
"smol",
"util",
]
[[package]]
name = "sval"
version = "2.8.0"
@@ -9819,6 +9909,7 @@ dependencies = [
"futures 0.3.28",
"gpui",
"hex",
"parking_lot",
"schemars",
"serde",
"serde_json_lenient",
@@ -9831,7 +9922,6 @@ dependencies = [
name = "tasks_ui"
version = "0.1.0"
dependencies = [
"anyhow",
"editor",
"file_icons",
"fuzzy",
@@ -11723,6 +11813,19 @@ dependencies = [
"wayland-scanner",
]
[[package]]
name = "wayland-protocols-plasma"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "23803551115ff9ea9bce586860c5c5a971e360825a0309264102a9495a5ff479"
dependencies = [
"bitflags 2.4.2",
"wayland-backend",
"wayland-client",
"wayland-protocols",
"wayland-scanner",
]
[[package]]
name = "wayland-protocols-wlr"
version = "0.2.0"
@@ -11790,11 +11893,12 @@ version = "0.1.0"
dependencies = [
"anyhow",
"client",
"copilot_ui",
"db",
"editor",
"extensions_ui",
"fuzzy",
"gpui",
"inline_completion_button",
"install_cli",
"picker",
"project",
@@ -12401,6 +12505,7 @@ dependencies = [
"collections",
"db",
"derive_more",
"dev_server_projects",
"env_logger",
"fs",
"futures 0.3.28",
@@ -12413,7 +12518,6 @@ dependencies = [
"parking_lot",
"postage",
"project",
"remote_projects",
"schemars",
"serde",
"serde_json",
@@ -12652,7 +12756,7 @@ dependencies = [
[[package]]
name = "zed"
version = "0.134.0"
version = "0.135.2"
dependencies = [
"activity_indicator",
"anyhow",
@@ -12674,8 +12778,8 @@ dependencies = [
"collections",
"command_palette",
"copilot",
"copilot_ui",
"db",
"dev_server_projects",
"diagnostics",
"editor",
"env_logger",
@@ -12686,10 +12790,13 @@ dependencies = [
"file_icons",
"fs",
"futures 0.3.28",
"git",
"git_hosting_providers",
"go_to_line",
"gpui",
"headless",
"image_viewer",
"inline_completion_button",
"install_cli",
"isahc",
"journal",
@@ -12713,7 +12820,6 @@ dependencies = [
"quick_action_bar",
"recent_projects",
"release_channel",
"remote_projects",
"rope",
"search",
"serde",
@@ -12721,6 +12827,7 @@ dependencies = [
"settings",
"simplelog",
"smol",
"supermaven",
"tab_switcher",
"task",
"tasks_ui",
@@ -12784,7 +12891,7 @@ dependencies = [
[[package]]
name = "zed_elixir"
version = "0.0.1"
version = "0.0.4"
dependencies = [
"zed_extension_api 0.0.6 (registry+https://github.com/rust-lang/crates.io-index)",
]
@@ -12918,7 +13025,7 @@ dependencies = [
[[package]]
name = "zed_toml"
version = "0.1.0"
version = "0.1.1"
dependencies = [
"zed_extension_api 0.0.6 (registry+https://github.com/rust-lang/crates.io-index)",
]
@@ -12939,7 +13046,7 @@ dependencies = [
[[package]]
name = "zed_zig"
version = "0.1.1"
version = "0.1.2"
dependencies = [
"zed_extension_api 0.0.6 (registry+https://github.com/rust-lang/crates.io-index)",
]

View File

@@ -20,7 +20,6 @@ members = [
"crates/command_palette",
"crates/command_palette_hooks",
"crates/copilot",
"crates/copilot_ui",
"crates/db",
"crates/diagnostics",
"crates/editor",
@@ -36,12 +35,14 @@ members = [
"crates/fsevent",
"crates/fuzzy",
"crates/git",
"crates/git_hosting_providers",
"crates/go_to_line",
"crates/google_ai",
"crates/gpui",
"crates/gpui_macros",
"crates/headless",
"crates/image_viewer",
"crates/inline_completion_button",
"crates/install_cli",
"crates/journal",
"crates/language",
@@ -69,7 +70,7 @@ members = [
"crates/refineable",
"crates/refineable/derive_refineable",
"crates/release_channel",
"crates/remote_projects",
"crates/dev_server_projects",
"crates/rich_text",
"crates/rope",
"crates/rpc",
@@ -86,6 +87,8 @@ members = [
"crates/storybook",
"crates/sum_tree",
"crates/tab_switcher",
"crates/supermaven",
"crates/supermaven_api",
"crates/terminal",
"crates/terminal_view",
"crates/text",
@@ -159,7 +162,6 @@ color = { path = "crates/color" }
command_palette = { path = "crates/command_palette" }
command_palette_hooks = { path = "crates/command_palette_hooks" }
copilot = { path = "crates/copilot" }
copilot_ui = { path = "crates/copilot_ui" }
db = { path = "crates/db" }
diagnostics = { path = "crates/diagnostics" }
editor = { path = "crates/editor" }
@@ -173,6 +175,7 @@ fs = { path = "crates/fs" }
fsevent = { path = "crates/fsevent" }
fuzzy = { path = "crates/fuzzy" }
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" }
@@ -180,6 +183,7 @@ gpui_macros = { path = "crates/gpui_macros" }
headless = { path = "crates/headless" }
install_cli = { path = "crates/install_cli" }
image_viewer = { path = "crates/image_viewer" }
inline_completion_button = { path = "crates/inline_completion_button" }
journal = { path = "crates/journal" }
language = { path = "crates/language" }
language_selector = { path = "crates/language_selector" }
@@ -207,7 +211,7 @@ project_symbols = { path = "crates/project_symbols" }
quick_action_bar = { path = "crates/quick_action_bar" }
recent_projects = { path = "crates/recent_projects" }
release_channel = { path = "crates/release_channel" }
remote_projects = { path = "crates/remote_projects" }
dev_server_projects = { path = "crates/dev_server_projects" }
rich_text = { path = "crates/rich_text" }
rope = { path = "crates/rope" }
rpc = { path = "crates/rpc" }
@@ -220,6 +224,8 @@ settings = { path = "crates/settings" }
snippet = { path = "crates/snippet" }
sqlez = { path = "crates/sqlez" }
sqlez_macros = { path = "crates/sqlez_macros" }
supermaven = { path = "crates/supermaven" }
supermaven_api = { path = "crates/supermaven_api"}
story = { path = "crates/story" }
storybook = { path = "crates/storybook" }
sum_tree = { path = "crates/sum_tree" }
@@ -250,8 +256,8 @@ async-recursion = "1.0.0"
async-tar = "0.4.2"
async-trait = "0.1"
bitflags = "2.4.2"
blade-graphics = { git = "https://github.com/kvark/blade", rev = "e82eec97691c3acdb43494484be60d661edfebf3" }
blade-macros = { git = "https://github.com/kvark/blade", rev = "e82eec97691c3acdb43494484be60d661edfebf3" }
blade-graphics = { git = "https://github.com/kvark/blade", rev = "f5766863de9dcc092e90fdbbc5e0007a99e7f9bf" }
blade-macros = { git = "https://github.com/kvark/blade", rev = "f5766863de9dcc092e90fdbbc5e0007a99e7f9bf" }
cap-std = "3.0"
chrono = { version = "0.4", features = ["serde"] }
clap = { version = "4.4", features = ["derive"] }

View File

@@ -1,6 +1,6 @@
# syntax = docker/dockerfile:1.2
FROM rust:1.77-bookworm as builder
FROM rust:1.78-bookworm as builder
WORKDIR app
COPY . .

101
README.md
View File

@@ -1,51 +1,50 @@
# Zed
[![CI](https://github.com/zed-industries/zed/actions/workflows/ci.yml/badge.svg)](https://github.com/zed-industries/zed/actions/workflows/ci.yml)
Welcome to Zed, a high-performance, multiplayer code editor from the creators of [Atom](https://github.com/atom/atom) and [Tree-sitter](https://github.com/tree-sitter/tree-sitter).
## Installation
You can [download](https://zed.dev/download) Zed today for macOS (v10.15+).
Support for additional platforms is on our [roadmap](https://zed.dev/roadmap):
- Linux ([tracking issue](https://github.com/zed-industries/zed/issues/7015))
- Windows ([tracking issue](https://github.com/zed-industries/zed/issues/5394))
- Web ([tracking issue](https://github.com/zed-industries/zed/issues/5396))
For macOS users, you can also install Zed using [Homebrew](https://brew.sh/):
```sh
brew install --cask zed
```
Alternatively, to install the Preview release:
```sh
brew tap homebrew/cask-versions
brew install zed-preview
```
## Developing Zed
- [Building Zed for macOS](./docs/src/developing_zed__building_zed_macos.md)
- [Building Zed for Linux](./docs/src/developing_zed__building_zed_linux.md)
- [Building Zed for Windows](./docs/src/developing_zed__building_zed_windows.md)
- [Running Collaboration Locally](./docs/src/developing_zed__local_collaboration.md)
## Contributing
See [CONTRIBUTING.md](./CONTRIBUTING.md) for ways you can contribute to Zed.
Also... we're hiring! Check out our [jobs](https://zed.dev/jobs) page for open roles.
## Licensing
License information for third party dependencies must be correctly provided for CI to pass.
We use [`cargo-about`](https://github.com/EmbarkStudios/cargo-about) to automatically comply with open source licenses. If CI is failing, check the following:
- Is it showing a `no license specified` error for a crate you've created? If so, add `publish = false` under `[package]` in your crate's Cargo.toml.
- Is the error `failed to satisfy license requirements` for a dependency? If so, first determine what license the project has and whether this system is sufficient to comply with this license's requirements. If you're unsure, ask a lawyer. Once you've verified that this system is acceptable add the license's SPDX identifier to the `accepted` array in `script/licenses/zed-licenses.toml`.
- Is `cargo-about` unable to find the license for a dependency? If so, add a clarification field at the end of `script/licenses/zed-licenses.toml`, as specified in the [cargo-about book](https://embarkstudios.github.io/cargo-about/cli/generate/config.html#crate-configuration).
# Zed
[![CI](https://github.com/zed-industries/zed/actions/workflows/ci.yml/badge.svg)](https://github.com/zed-industries/zed/actions/workflows/ci.yml)
Welcome to Zed, a high-performance, multiplayer code editor from the creators of [Atom](https://github.com/atom/atom) and [Tree-sitter](https://github.com/tree-sitter/tree-sitter).
## Installation
You can [download](https://zed.dev/download) Zed today for macOS (v10.15+).
Support for additional platforms is on our [roadmap](https://zed.dev/roadmap):
- Linux ([tracking issue](https://github.com/zed-industries/zed/issues/7015))
- Windows ([tracking issue](https://github.com/zed-industries/zed/issues/5394))
- Web ([tracking issue](https://github.com/zed-industries/zed/issues/5396))
For macOS users, you can also install Zed using [Homebrew](https://brew.sh/):
```sh
brew install --cask zed
```
Alternatively, to install the Preview release:
```sh
brew install --cask zed@preview
```
## Developing Zed
- [Building Zed for macOS](./docs/src/development/macos.md)
- [Building Zed for Linux](./docs/src/development/linux.md)
- [Building Zed for Windows](./docs/src/development/windows.md)
- [Running Collaboration Locally](./docs/src/development/local-collaboration.md)
## Contributing
See [CONTRIBUTING.md](./CONTRIBUTING.md) for ways you can contribute to Zed.
Also... we're hiring! Check out our [jobs](https://zed.dev/jobs) page for open roles.
## Licensing
License information for third party dependencies must be correctly provided for CI to pass.
We use [`cargo-about`](https://github.com/EmbarkStudios/cargo-about) to automatically comply with open source licenses. If CI is failing, check the following:
- Is it showing a `no license specified` error for a crate you've created? If so, add `publish = false` under `[package]` in your crate's Cargo.toml.
- Is the error `failed to satisfy license requirements` for a dependency? If so, first determine what license the project has and whether this system is sufficient to comply with this license's requirements. If you're unsure, ask a lawyer. Once you've verified that this system is acceptable add the license's SPDX identifier to the `accepted` array in `script/licenses/zed-licenses.toml`.
- Is `cargo-about` unable to find the license for a dependency? If so, add a clarification field at the end of `script/licenses/zed-licenses.toml`, as specified in the [cargo-about book](https://embarkstudios.github.io/cargo-about/cli/generate/config.html#crate-configuration).

View File

@@ -1,6 +0,0 @@
User input begins on a line starting with /.
Don't apologize ever.
Never say "I apologize".
Use simple language and don't flatter the users.
Keep it short.
Risk being rude.

1
assets/icons/code.svg Normal file
View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-code-xml"><path d="m18 16 4-4-4-4"/><path d="m6 8-4 4 4 4"/><path d="m14.5 4-5 16"/></svg>

After

Width:  |  Height:  |  Size: 293 B

View File

@@ -0,0 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M11.5 15C13.433 15 15 13.433 15 11.5C15 9.567 13.433 8 11.5 8C9.567 8 8 9.567 8 11.5C8 13.433 9.567 15 11.5 15Z" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 240 B

View File

@@ -0,0 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M13.4662 14.9152C13.5801 15.0291 13.7648 15.0291 13.8787 14.9152L14.9145 13.8793C15.0284 13.7654 15.0284 13.5807 14.9145 13.4667L12.9483 11.5004L14.9145 9.53392C15.0285 9.42004 15.0285 9.23533 14.9145 9.12137L13.8787 8.08547C13.7648 7.97154 13.5801 7.97154 13.4662 8.08547L11.5 10.0519L9.53376 8.08545C9.41988 7.97152 9.23517 7.97152 9.12124 8.08545L8.08543 9.12136C7.97152 9.23533 7.97152 9.42004 8.08543 9.53392L10.0517 11.5004L8.08545 13.4667C7.97155 13.5807 7.97155 13.7654 8.08545 13.8793L9.12126 14.9152C9.23517 15.0292 9.41988 15.0292 9.53376 14.9152L11.5 12.9489L13.4662 14.9152Z" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 756 B

13
assets/icons/spinner.svg Normal file
View File

@@ -0,0 +1,13 @@
<svg width="8" height="8" viewBox="0 0 8 8" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_1803_28)">
<path d="M0.5 2C0.5 1.17157 1.17157 0.5 2 0.5V0.5C2.82843 0.5 3.5 1.17157 3.5 2V2C3.5 2.82843 2.82843 3.5 2 3.5V3.5C1.17157 3.5 0.5 2.82843 0.5 2V2Z" fill="black" fill-opacity="0.3"/>
<path d="M7.5 6C7.5 6.82843 6.82843 7.5 6 7.5V7.5C5.17157 7.5 4.5 6.82843 4.5 6V6C4.5 5.17157 5.17157 4.5 6 4.5V4.5C6.82843 4.5 7.5 5.17157 7.5 6V6Z" fill="black" fill-opacity="0.6"/>
<path d="M2 7.5C1.17157 7.5 0.5 6.82843 0.5 6V6C0.5 5.17157 1.17157 4.5 2 4.5V4.5C2.82843 4.5 3.5 5.17157 3.5 6V6C3.5 6.82843 2.82843 7.5 2 7.5V7.5Z" fill="black" fill-opacity="0.8"/>
<path d="M6 0.5C6.82843 0.5 7.5 1.17157 7.5 2V2C7.5 2.82843 6.82843 3.5 6 3.5V3.5C5.17157 3.5 4.5 2.82843 4.5 2V2C4.5 1.17157 5.17157 0.5 6 0.5V0.5Z" fill="black"/>
</g>
<defs>
<clipPath id="clip0_1803_28">
<rect width="8" height="8" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 956 B

View File

@@ -0,0 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3 4L13 12" stroke="black" stroke-width="2" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 181 B

View File

@@ -0,0 +1,8 @@
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3.30859 13.0703C3.80693 13.0703 4.21094 12.6663 4.21094 12.168C4.21094 11.6696 3.80693 11.2656 3.30859 11.2656C2.81025 11.2656 2.40625 11.6696 2.40625 12.168C2.40625 12.6663 2.81025 13.0703 3.30859 13.0703Z" fill="black"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M6.53516 8.03849L4.10799 12.6055L2.51562 11.7584L4.94279 7.19141L6.53516 8.03849Z" fill="black"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M7.38281 2.62443L4.93916 7.19141L3.33594 6.34432L5.77959 1.77734L7.38281 2.62443Z" fill="black"/>
<path d="M6.5625 3.08984C7.06084 3.08984 7.46484 2.68585 7.46484 2.1875C7.46484 1.68915 7.06084 1.28516 6.5625 1.28516C6.06416 1.28516 5.66016 1.68915 5.66016 2.1875C5.66016 2.68585 6.06416 3.08984 6.5625 3.08984Z" fill="black"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M10.882 1.31204C11.2842 1.41224 11.5664 1.7732 11.5664 2.18737V12.168H9.76084V5.8056L8.12938 8.87176L6.53516 8.02471L9.86653 1.76385C10.0611 1.39816 10.4799 1.21184 10.882 1.31204Z" fill="black"/>
<path d="M10.6641 13.0703C11.1624 13.0703 11.5664 12.6663 11.5664 12.168C11.5664 11.6696 11.1624 11.2656 10.6641 11.2656C10.1657 11.2656 9.76172 11.6696 9.76172 12.168C9.76172 12.6663 10.1657 13.0703 10.6641 13.0703Z" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -0,0 +1,15 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<g opacity="0.5">
<path d="M3.78125 14.9375C4.35078 14.9375 4.8125 14.4758 4.8125 13.9062C4.8125 13.3367 4.35078 12.875 3.78125 12.875C3.21172 12.875 2.75 13.3367 2.75 13.9062C2.75 14.4758 3.21172 14.9375 3.78125 14.9375Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M7.46875 9.18684L4.69484 14.4062L2.875 13.4382L5.64891 8.21875L7.46875 9.18684Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M8.4375 2.99935L5.64475 8.21875L3.8125 7.25066L6.60525 2.03125L8.4375 2.99935Z" fill="white"/>
<path d="M7.5 3.53125C8.06953 3.53125 8.53125 3.06954 8.53125 2.5C8.53125 1.93046 8.06953 1.46875 7.5 1.46875C6.93047 1.46875 6.46875 1.93046 6.46875 2.5C6.46875 3.06954 6.93047 3.53125 7.5 3.53125Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M12.4366 1.49947C12.8962 1.61399 13.2188 2.02651 13.2188 2.49985V13.9063H11.1552V6.63497L9.29072 10.1392L7.46875 9.17109L11.276 2.01583C11.4984 1.59789 11.977 1.38496 12.4366 1.49947Z" fill="white"/>
<path d="M12.1875 14.9375C12.757 14.9375 13.2188 14.4758 13.2188 13.9062C13.2188 13.3367 12.757 12.875 12.1875 12.875C11.618 12.875 11.1562 13.3367 11.1562 13.9062C11.1562 14.4758 11.618 14.9375 12.1875 14.9375Z" fill="white"/>
</g>
<g>
<path d="M0.906311 6.42261L1.75155 4.60999L15.3462 10.9493L14.5009 12.7619L0.906311 6.42261Z" fill="white"/>
<circle cx="14.7841" cy="11.7906" r="1" transform="rotate(-65 14.7841 11.7906)" fill="white"/>
<circle cx="1.32893" cy="5.51631" r="1" transform="rotate(-65 1.32893 5.51631)" fill="white"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@@ -0,0 +1,11 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<g opacity="0.5">
<path d="M3.78125 14.9375C4.35078 14.9375 4.8125 14.4758 4.8125 13.9062C4.8125 13.3367 4.35078 12.875 3.78125 12.875C3.21172 12.875 2.75 13.3367 2.75 13.9062C2.75 14.4758 3.21172 14.9375 3.78125 14.9375Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M7.46875 9.18684L4.69484 14.4062L2.875 13.4382L5.64891 8.21875L7.46875 9.18684Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M8.4375 2.99935L5.64475 8.21875L3.8125 7.25066L6.60525 2.03125L8.4375 2.99935Z" fill="white"/>
<path d="M7.5 3.53125C8.06953 3.53125 8.53125 3.06954 8.53125 2.5C8.53125 1.93046 8.06953 1.46875 7.5 1.46875C6.93047 1.46875 6.46875 1.93046 6.46875 2.5C6.46875 3.06954 6.93047 3.53125 7.5 3.53125Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M12.4366 1.49947C12.8962 1.61399 13.2188 2.02651 13.2188 2.49985V13.9063H11.1552V6.63497L9.29072 10.1392L7.46875 9.17109L11.276 2.01583C11.4984 1.59789 11.977 1.38496 12.4366 1.49947Z" fill="white"/>
<path d="M12.1875 14.9375C12.757 14.9375 13.2188 14.4758 13.2188 13.9062C13.2188 13.3367 12.757 12.875 12.1875 12.875C11.618 12.875 11.1562 13.3367 11.1562 13.9062C11.1562 14.4758 11.618 14.9375 12.1875 14.9375Z" fill="white"/>
</g>
<path fill-rule="evenodd" clip-rule="evenodd" d="M14.6847 15.9265C14.7823 16.0241 14.9406 16.0241 15.0382 15.9265L15.9259 15.0387C16.0235 14.9411 16.0235 14.7828 15.9259 14.6851L14.2408 12.9999L15.9259 11.3146C16.0236 11.217 16.0236 11.0587 15.9259 10.961L15.0382 10.0733C14.9406 9.97561 14.7823 9.97561 14.6847 10.0733L12.9996 11.7585L11.3145 10.0732C11.2169 9.97559 11.0586 9.97559 10.9609 10.0732L10.0732 10.961C9.97559 11.0587 9.97559 11.217 10.0732 11.3146L11.7584 12.9999L10.0732 14.6851C9.97562 14.7828 9.97562 14.9411 10.0732 15.0387L10.9609 15.9265C11.0586 16.0242 11.2169 16.0242 11.3145 15.9265L12.9996 14.2413L14.6847 15.9265Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@@ -0,0 +1,11 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<g opacity="0.5">
<path d="M3.78125 14.9375C4.35078 14.9375 4.8125 14.4758 4.8125 13.9062C4.8125 13.3367 4.35078 12.875 3.78125 12.875C3.21172 12.875 2.75 13.3367 2.75 13.9062C2.75 14.4758 3.21172 14.9375 3.78125 14.9375Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M7.46875 9.18684L4.69484 14.4062L2.875 13.4382L5.64891 8.21875L7.46875 9.18684Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M8.4375 2.99935L5.64475 8.21875L3.8125 7.25066L6.60525 2.03125L8.4375 2.99935Z" fill="white"/>
<path d="M7.5 3.53125C8.06953 3.53125 8.53125 3.06954 8.53125 2.5C8.53125 1.93046 8.06953 1.46875 7.5 1.46875C6.93047 1.46875 6.46875 1.93046 6.46875 2.5C6.46875 3.06954 6.93047 3.53125 7.5 3.53125Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M12.4366 1.49947C12.8962 1.61399 13.2188 2.02651 13.2188 2.49985V13.9063H11.1552V6.63497L9.29072 10.1392L7.46875 9.17109L11.276 2.01583C11.4984 1.59789 11.977 1.38496 12.4366 1.49947Z" fill="white"/>
<path d="M12.1875 14.9375C12.757 14.9375 13.2188 14.4758 13.2188 13.9062C13.2188 13.3367 12.757 12.875 12.1875 12.875C11.618 12.875 11.1562 13.3367 11.1562 13.9062C11.1562 14.4758 11.618 14.9375 12.1875 14.9375Z" fill="white"/>
</g>
<circle cx="13" cy="13" r="3" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -0,0 +1,5 @@
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M7 1.75L5.88467 5.14092C5.82759 5.31446 5.73055 5.47218 5.60136 5.60136C5.47218 5.73055 5.31446 5.82759 5.14092 5.88467L1.75 7L5.14092 8.11533C5.31446 8.17241 5.47218 8.26945 5.60136 8.39864C5.73055 8.52782 5.82759 8.68554 5.88467 8.85908L7 12.25L8.11533 8.85908C8.17241 8.68554 8.26945 8.52782 8.39864 8.39864C8.52782 8.26945 8.68554 8.17241 8.85908 8.11533L12.25 7L8.85908 5.88467C8.68554 5.82759 8.52782 5.73055 8.39864 5.60136C8.26945 5.47218 8.17241 5.31446 8.11533 5.14092L7 1.75Z" fill="black" fill-opacity="0.15" stroke="black" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M2.91667 1.75V4.08333M1.75 2.91667H4.08333" stroke="black" stroke-opacity="0.75" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M11.0833 9.91667V12.25M9.91667 11.0833H12.25" stroke="black" stroke-opacity="0.75" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@@ -138,6 +138,8 @@
"ctrl-alt-space": "editor::ShowCharacterPalette",
"ctrl-;": "editor::ToggleLineNumbers",
"ctrl-k ctrl-r": "editor::RevertSelectedHunks",
"ctrl-'": "editor::ToggleHunkDiff",
"ctrl-\"": "editor::ExpandAllHunkDiffs",
"ctrl-alt-g b": "editor::ToggleGitBlame"
}
},

View File

@@ -159,6 +159,8 @@
"ctrl-cmd-space": "editor::ShowCharacterPalette",
"cmd-;": "editor::ToggleLineNumbers",
"cmd-alt-z": "editor::RevertSelectedHunks",
"cmd-'": "editor::ToggleHunkDiff",
"cmd-\"": "editor::ExpandAllHunkDiffs",
"cmd-alt-g b": "editor::ToggleGitBlame"
}
},

View File

@@ -39,13 +39,13 @@
"cmd-shift-left": "editor::SelectToBeginningOfLine",
"cmd-shift-right": "editor::SelectToEndOfLine",
"alt-shift-left": [
"editor::SelectToBeginningOfLine",
"editor::SelectToPreviousWordStart",
{
"stop_at_soft_wraps": true
}
],
"alt-shift-right": [
"editor::SelectToEndOfLine",
"editor::SelectToNextWordEnd",
{
"stop_at_soft_wraps": true
}

View File

@@ -128,6 +128,7 @@
"shift-v": "vim::ToggleVisualLine",
"ctrl-v": "vim::ToggleVisualBlock",
"ctrl-q": "vim::ToggleVisualBlock",
"shift-k": "editor::Hover",
"shift-r": "vim::ToggleReplace",
"0": "vim::StartOfLine", // When no number operator present, use start of line motion
"ctrl-f": "vim::PageDown",

View File

@@ -12,8 +12,8 @@
"base_keymap": "VSCode",
// Features that can be globally enabled or disabled
"features": {
// Show Copilot icon in status bar
"copilot": true
// Which inline completion provider to use.
"inline_completion_provider": "copilot"
},
// The name of a font to use for rendering text in the editor
"buffer_font_family": "Zed Mono",
@@ -299,7 +299,9 @@
// The list of language servers to use (or disable) for all languages.
//
// This is typically customized on a per-language basis.
"language_servers": ["..."],
"language_servers": [
"..."
],
// When to automatically save edited buffers. This setting can
// take four values.
//
@@ -368,11 +370,13 @@
//
// 1. Do not soft wrap.
// "soft_wrap": "none",
// 2. Soft wrap lines that overflow the editor:
// 2. Prefer a single line generally, unless an overly long line is encountered.
// "soft_wrap": "prefer_line",
// 3. Soft wrap lines that overflow the editor:
// "soft_wrap": "editor_width",
// 3. Soft wrap lines at the preferred line length
// 4. Soft wrap lines at the preferred line length
// "soft_wrap": "preferred_line_length",
"soft_wrap": "none",
"soft_wrap": "prefer_line",
// The column at which to soft-wrap lines, for buffers where soft-wrap
// is enabled.
"preferred_line_length": 80,
@@ -428,7 +432,9 @@
"copilot": {
// The set of glob patterns for which copilot should be disabled
// in any matching file.
"disabled_globs": [".env"]
"disabled_globs": [
".env"
]
},
// Settings specific to journaling
"journal": {
@@ -539,7 +545,12 @@
// Default directories to search for virtual environments, relative
// to the current working directory. We recommend overriding this
// in your project's settings, rather than globally.
"directories": [".env", "env", ".venv", "venv"],
"directories": [
".env",
"env",
".venv",
"venv"
],
// Can also be 'csh', 'fish', and `nushell`
"activate_script": "default"
}
@@ -588,6 +599,14 @@
"C": {
"format_on_save": "off"
},
"Elixir": {
"language_servers": [
"elixir-ls",
"!next-ls",
"!lexical",
"..."
]
},
"Gleam": {
"tab_size": 2
},
@@ -596,6 +615,14 @@
"source.organizeImports": true
}
},
"HEEX": {
"language_servers": [
"elixir-ls",
"!next-ls",
"!lexical",
"..."
]
},
"Make": {
"hard_tabs": true
},

View File

@@ -23,7 +23,6 @@ language.workspace = true
project.workspace = true
smallvec.workspace = true
ui.workspace = true
util.workspace = true
workspace.workspace = true
[dev-dependencies]

View File

@@ -12,7 +12,6 @@ use project::{LanguageServerProgress, Project};
use smallvec::SmallVec;
use std::{cmp::Reverse, fmt::Write, sync::Arc};
use ui::prelude::*;
use util::ResultExt;
use workspace::{item::ItemHandle, StatusItemView, Workspace};
actions!(activity_indicator, [ShowErrorMessage]);
@@ -82,27 +81,37 @@ impl ActivityIndicator {
}
});
cx.subscribe(&this, move |workspace, _, event, cx| match event {
cx.subscribe(&this, move |_, _, event, cx| match event {
Event::ShowError { lsp_name, error } => {
if let Some(buffer) = project
.update(cx, |project, cx| project.create_buffer(error, None, cx))
.log_err()
{
buffer.update(cx, |buffer, cx| {
let create_buffer = project.update(cx, |project, cx| project.create_buffer(cx));
let project = project.clone();
let error = error.clone();
let lsp_name = lsp_name.clone();
cx.spawn(|workspace, mut cx| async move {
let buffer = create_buffer.await?;
buffer.update(&mut cx, |buffer, cx| {
buffer.edit(
[(0..0, format!("Language server error: {}\n\n", lsp_name))],
[(
0..0,
format!("Language server error: {}\n\n{}", lsp_name, error),
)],
None,
cx,
);
});
workspace.add_item_to_active_pane(
Box::new(
cx.new_view(|cx| Editor::for_buffer(buffer, Some(project.clone()), cx)),
),
None,
cx,
);
}
})?;
workspace.update(&mut cx, |workspace, cx| {
workspace.add_item_to_active_pane(
Box::new(cx.new_view(|cx| {
Editor::for_buffer(buffer, Some(project.clone()), cx)
})),
None,
cx,
);
})?;
anyhow::Ok(())
})
.detach();
}
})
.detach();

View File

@@ -1,7 +1,7 @@
use anyhow::{anyhow, Result};
use futures::{io::BufReader, stream::BoxStream, AsyncBufReadExt, AsyncReadExt, StreamExt};
use serde::{Deserialize, Serialize};
use std::convert::TryFrom;
use std::{convert::TryFrom, sync::Arc};
use util::http::{AsyncBody, HttpClient, Method, Request as HttpRequest};
#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)]
@@ -141,7 +141,7 @@ pub enum TextDelta {
}
pub async fn stream_completion(
client: &dyn HttpClient,
client: Arc<dyn HttpClient>,
api_url: &str,
api_key: &str,
request: Request,

View File

@@ -106,6 +106,11 @@ impl SavedConversationMetadata {
.and_then(|name| name.to_str())
.zip(metadata)
{
// This is used to filter out conversations saved by the new assistant.
if !re.is_match(file_name) {
continue;
}
let title = re.replace(file_name, "");
conversations.push(Self {
title: title.into_owned(),

View File

@@ -5,9 +5,16 @@ edition = "2021"
publish = false
license = "GPL-3.0-or-later"
[lints]
workspace = true
[lib]
path = "src/assistant2.rs"
[features]
default = []
stories = ["dep:story"]
[dependencies]
anyhow.workspace = true
assistant_tooling.workspace = true
@@ -17,11 +24,13 @@ editor.workspace = true
feature_flags.workspace = true
fs.workspace = true
futures.workspace = true
fuzzy.workspace = true
gpui.workspace = true
language.workspace = true
log.workspace = true
nanoid.workspace = true
open_ai.workspace = true
picker.workspace = true
project.workspace = true
rich_text.workspace = true
schemars.workspace = true
@@ -29,6 +38,7 @@ semantic_index.workspace = true
serde.workspace = true
serde_json.workspace = true
settings.workspace = true
story = { workspace = true, optional = true }
theme.workspace = true
ui.workspace = true
util.workspace = true
@@ -49,6 +59,3 @@ settings = { workspace = true, features = ["test-support"] }
theme = { workspace = true, features = ["test-support"] }
util = { workspace = true, features = ["test-support"] }
workspace = { workspace = true, features = ["test-support"] }
[lints]
workspace = true

View File

@@ -0,0 +1 @@
> Give me a comprehensive list of all the elements defined in my project using the following query: `impl Element for {}, impl<T: 'static> Element for {}, impl IntoElement for {})`

View File

@@ -0,0 +1 @@
> What are all the places we define a new gpui element in my project? (impl Element for {})

View File

@@ -0,0 +1,3 @@
Use tools frequently, especially when referring to files and code. The Zed editor we're working in can show me files directly when you add annotations. Be concise in chat, bountiful in tool calling.
Teach me everything you can about how zed loads settings. Please annotate the code inline.

View File

@@ -0,0 +1 @@
> Can you tell me what the assistant2 crate is for in my project? Tell me in 100 words or less.

View File

@@ -1,370 +0,0 @@
//! This example creates a basic Chat UI with a function for rolling a die.
use anyhow::{Context as _, Result};
use assets::Assets;
use assistant2::AssistantPanel;
use assistant_tooling::{LanguageModelTool, ToolRegistry};
use client::{Client, UserStore};
use fs::Fs;
use futures::StreamExt as _;
use gpui::{actions, AnyElement, App, AppContext, KeyBinding, Model, Task, View, WindowOptions};
use language::LanguageRegistry;
use project::Project;
use rand::Rng;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use settings::{KeymapFile, DEFAULT_KEYMAP_PATH};
use std::{path::PathBuf, sync::Arc};
use theme::LoadThemes;
use ui::{div, prelude::*, Render};
use util::ResultExt as _;
actions!(example, [Quit]);
struct RollDiceTool {}
impl RollDiceTool {
fn new() -> Self {
Self {}
}
}
#[derive(Serialize, Deserialize, JsonSchema, Clone)]
#[serde(rename_all = "snake_case")]
enum Die {
D6 = 6,
D20 = 20,
}
impl Die {
fn into_str(&self) -> &'static str {
match self {
Die::D6 => "d6",
Die::D20 => "d20",
}
}
}
#[derive(Serialize, Deserialize, JsonSchema, Clone)]
struct DiceParams {
/// The number of dice to roll.
num_dice: u8,
/// Which die to roll. Defaults to a d6 if not provided.
die_type: Option<Die>,
}
#[derive(Serialize, Deserialize)]
struct DieRoll {
die: Die,
roll: u8,
}
impl DieRoll {
fn render(&self) -> AnyElement {
match self.die {
Die::D6 => {
let face = match self.roll {
6 => div().child(""),
5 => div().child(""),
4 => div().child(""),
3 => div().child(""),
2 => div().child(""),
1 => div().child(""),
_ => div().child("😅"),
};
face.text_3xl().into_any_element()
}
_ => div()
.child(format!("{}", self.roll))
.text_3xl()
.into_any_element(),
}
}
}
#[derive(Serialize, Deserialize)]
struct DiceRoll {
rolls: Vec<DieRoll>,
}
pub struct DiceView {
result: Result<DiceRoll>,
}
impl Render for DiceView {
fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
let output = match &self.result {
Ok(output) => output,
Err(_) => return "Somehow dice failed 🎲".into_any_element(),
};
h_flex()
.children(
output
.rolls
.iter()
.map(|roll| div().p_2().child(roll.render())),
)
.into_any_element()
}
}
impl LanguageModelTool for RollDiceTool {
type Input = DiceParams;
type Output = DiceRoll;
type View = DiceView;
fn name(&self) -> String {
"roll_dice".to_string()
}
fn description(&self) -> String {
"Rolls N many dice and returns the results.".to_string()
}
fn execute(&self, input: &Self::Input, _cx: &AppContext) -> Task<gpui::Result<Self::Output>> {
let rolls = (0..input.num_dice)
.map(|_| {
let die_type = input.die_type.as_ref().unwrap_or(&Die::D6).clone();
DieRoll {
die: die_type.clone(),
roll: rand::thread_rng().gen_range(1..=die_type as u8),
}
})
.collect();
return Task::ready(Ok(DiceRoll { rolls }));
}
fn output_view(
_tool_call_id: String,
_input: Self::Input,
result: Result<Self::Output>,
cx: &mut WindowContext,
) -> gpui::View<Self::View> {
cx.new_view(|_cx| DiceView { result })
}
fn format(_: &Self::Input, output: &Result<Self::Output>) -> String {
let output = match output {
Ok(output) => output,
Err(_) => return "Somehow dice failed 🎲".to_string(),
};
let mut result = String::new();
for roll in &output.rolls {
let die = &roll.die;
result.push_str(&format!("{}: {}\n", die.into_str(), roll.roll));
}
result
}
}
struct FileBrowserTool {
fs: Arc<dyn Fs>,
root_dir: PathBuf,
}
impl FileBrowserTool {
fn new(fs: Arc<dyn Fs>, root_dir: PathBuf) -> Self {
Self { fs, root_dir }
}
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
struct FileBrowserParams {
command: FileBrowserCommand,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
enum FileBrowserCommand {
Ls { path: PathBuf },
Cat { path: PathBuf },
}
#[derive(Serialize, Deserialize)]
enum FileBrowserOutput {
Ls { entries: Vec<String> },
Cat { content: String },
}
pub struct FileBrowserView {
result: Result<FileBrowserOutput>,
}
impl Render for FileBrowserView {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let Ok(output) = self.result.as_ref() else {
return h_flex().child("Failed to perform operation");
};
match output {
FileBrowserOutput::Ls { entries } => v_flex().children(
entries
.into_iter()
.map(|entry| h_flex().text_ui(cx).child(entry.clone())),
),
FileBrowserOutput::Cat { content } => h_flex().child(content.clone()),
}
}
}
impl LanguageModelTool for FileBrowserTool {
type Input = FileBrowserParams;
type Output = FileBrowserOutput;
type View = FileBrowserView;
fn name(&self) -> String {
"file_browser".to_string()
}
fn description(&self) -> String {
"A tool for browsing the filesystem.".to_string()
}
fn execute(&self, input: &Self::Input, cx: &AppContext) -> Task<gpui::Result<Self::Output>> {
cx.spawn({
let fs = self.fs.clone();
let root_dir = self.root_dir.clone();
let input = input.clone();
|_cx| async move {
match input.command {
FileBrowserCommand::Ls { path } => {
let path = root_dir.join(path);
let mut output = fs.read_dir(&path).await?;
let mut entries = Vec::new();
while let Some(entry) = output.next().await {
let entry = entry?;
entries.push(entry.display().to_string());
}
Ok(FileBrowserOutput::Ls { entries })
}
FileBrowserCommand::Cat { path } => {
let path = root_dir.join(path);
let output = fs.load(&path).await?;
Ok(FileBrowserOutput::Cat { content: output })
}
}
}
})
}
fn output_view(
_tool_call_id: String,
_input: Self::Input,
result: Result<Self::Output>,
cx: &mut WindowContext,
) -> gpui::View<Self::View> {
cx.new_view(|_cx| FileBrowserView { result })
}
fn format(_input: &Self::Input, output: &Result<Self::Output>) -> String {
let Ok(output) = output else {
return "Failed to perform command: {input:?}".to_string();
};
match output {
FileBrowserOutput::Ls { entries } => entries.join("\n"),
FileBrowserOutput::Cat { content } => content.to_owned(),
}
}
}
fn main() {
env_logger::init();
App::new().with_assets(Assets).run(|cx| {
cx.bind_keys(Some(KeyBinding::new("cmd-q", Quit, None)));
cx.on_action(|_: &Quit, cx: &mut AppContext| {
cx.quit();
});
settings::init(cx);
language::init(cx);
Project::init_settings(cx);
editor::init(cx);
theme::init(LoadThemes::JustBase, cx);
Assets.load_fonts(cx).unwrap();
KeymapFile::load_asset(DEFAULT_KEYMAP_PATH, cx).unwrap();
client::init_settings(cx);
release_channel::init("0.130.0", cx);
let client = Client::production(cx);
{
let client = client.clone();
cx.spawn(|cx| async move { client.authenticate_and_connect(false, &cx).await })
.detach_and_log_err(cx);
}
assistant2::init(client.clone(), cx);
let language_registry = Arc::new(LanguageRegistry::new(
Task::ready(()),
cx.background_executor().clone(),
));
let user_store = cx.new_model(|cx| UserStore::new(client.clone(), cx));
let node_runtime = node_runtime::RealNodeRuntime::new(client.http_client());
languages::init(language_registry.clone(), node_runtime, cx);
cx.spawn(|cx| async move {
cx.update(|cx| {
let fs = Arc::new(fs::RealFs::new(None));
let cwd = std::env::current_dir().expect("Failed to get current working directory");
cx.open_window(WindowOptions::default(), |cx| {
let mut tool_registry = ToolRegistry::new();
tool_registry
.register(RollDiceTool::new(), cx)
.context("failed to register DummyTool")
.log_err();
tool_registry
.register(FileBrowserTool::new(fs, cwd), cx)
.context("failed to register FileBrowserTool")
.log_err();
let tool_registry = Arc::new(tool_registry);
println!("Tools registered");
for definition in tool_registry.definitions() {
println!("{}", definition);
}
cx.new_view(|cx| Example::new(language_registry, tool_registry, user_store, cx))
});
cx.activate(true);
})
})
.detach_and_log_err(cx);
})
}
struct Example {
assistant_panel: View<AssistantPanel>,
}
impl Example {
fn new(
language_registry: Arc<LanguageRegistry>,
tool_registry: Arc<ToolRegistry>,
user_store: Model<UserStore>,
cx: &mut ViewContext<Self>,
) -> Self {
Self {
assistant_panel: cx.new_view(|cx| {
AssistantPanel::new(language_registry, tool_registry, user_store, cx)
}),
}
}
}
impl Render for Example {
fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl ui::prelude::IntoElement {
div().size_full().child(self.assistant_panel.clone())
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,3 @@
mod active_file;
pub use active_file::*;

View File

@@ -0,0 +1,112 @@
use anyhow::{anyhow, Result};
use assistant_tooling::{LanguageModelAttachment, ProjectContext, ToolOutput};
use editor::Editor;
use gpui::{Render, Task, View, WeakModel, WeakView};
use language::Buffer;
use project::ProjectPath;
use ui::{prelude::*, ButtonLike, Tooltip, WindowContext};
use util::maybe;
use workspace::Workspace;
pub struct ActiveEditorAttachment {
buffer: WeakModel<Buffer>,
path: Option<ProjectPath>,
}
pub struct FileAttachmentView {
output: Result<ActiveEditorAttachment>,
}
impl Render for FileAttachmentView {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
match &self.output {
Ok(attachment) => {
let filename: SharedString = attachment
.path
.as_ref()
.and_then(|p| p.path.file_name()?.to_str())
.unwrap_or("Untitled")
.to_string()
.into();
// todo!(): make the button link to the actual file to open
ButtonLike::new("file-attachment")
.child(
h_flex()
.gap_1()
.bg(cx.theme().colors().editor_background)
.rounded_md()
.child(ui::Icon::new(IconName::File))
.child(filename.clone()),
)
.tooltip({
move |cx| Tooltip::with_meta("File Attached", None, filename.clone(), cx)
})
.into_any_element()
}
Err(err) => div().child(err.to_string()).into_any_element(),
}
}
}
impl ToolOutput for FileAttachmentView {
fn generate(&self, project: &mut ProjectContext, cx: &mut WindowContext) -> String {
if let Ok(result) = &self.output {
if let Some(path) = &result.path {
project.add_file(path.clone());
return format!("current file: {}", path.path.display());
} else if let Some(buffer) = result.buffer.upgrade() {
return format!("current untitled buffer text:\n{}", buffer.read(cx).text());
}
}
String::new()
}
}
pub struct ActiveEditorAttachmentTool {
workspace: WeakView<Workspace>,
}
impl ActiveEditorAttachmentTool {
pub fn new(workspace: WeakView<Workspace>, _cx: &mut WindowContext) -> Self {
Self { workspace }
}
}
impl LanguageModelAttachment for ActiveEditorAttachmentTool {
type Output = ActiveEditorAttachment;
type View = FileAttachmentView;
fn run(&self, cx: &mut WindowContext) -> Task<Result<ActiveEditorAttachment>> {
Task::ready(maybe!({
let active_buffer = self
.workspace
.update(cx, |workspace, cx| {
workspace
.active_item(cx)
.and_then(|item| Some(item.act_as::<Editor>(cx)?.read(cx).buffer().clone()))
})?
.ok_or_else(|| anyhow!("no active buffer"))?;
let buffer = active_buffer.read(cx);
if let Some(buffer) = buffer.as_singleton() {
let path =
project::File::from_dyn(buffer.read(cx).file()).map(|file| ProjectPath {
worktree_id: file.worktree_id(cx),
path: file.path.clone(),
});
return Ok(ActiveEditorAttachment {
buffer: buffer.downgrade(),
path,
});
} else {
Err(anyhow!("no active buffer"))
}
}))
}
fn view(output: Result<Self::Output>, cx: &mut WindowContext) -> View<Self::View> {
cx.new_view(|_cx| FileAttachmentView { output })
}
}

View File

@@ -33,7 +33,7 @@ impl CompletionProvider {
messages: Vec<CompletionMessage>,
stop: Vec<String>,
temperature: f32,
tools: &[ToolFunctionDefinition],
tools: Vec<ToolFunctionDefinition>,
) -> BoxFuture<'static, Result<BoxStream<'static, Result<proto::LanguageModelResponseMessage>>>>
{
self.0.complete(model, messages, stop, temperature, tools)
@@ -51,7 +51,7 @@ pub trait CompletionProviderBackend: 'static {
messages: Vec<CompletionMessage>,
stop: Vec<String>,
temperature: f32,
tools: &[ToolFunctionDefinition],
tools: Vec<ToolFunctionDefinition>,
) -> BoxFuture<'static, Result<BoxStream<'static, Result<proto::LanguageModelResponseMessage>>>>;
}
@@ -80,7 +80,7 @@ impl CompletionProviderBackend for CloudCompletionProvider {
messages: Vec<CompletionMessage>,
stop: Vec<String>,
temperature: f32,
tools: &[ToolFunctionDefinition],
tools: Vec<ToolFunctionDefinition>,
) -> BoxFuture<'static, Result<BoxStream<'static, Result<proto::LanguageModelResponseMessage>>>>
{
let client = self.client.clone();

View File

@@ -0,0 +1,49 @@
use serde::{Deserialize, Serialize};
use crate::MessageId;
#[derive(Serialize, Deserialize)]
pub struct SavedConversation {
/// The schema version of the conversation.
pub version: String,
/// The title of the conversation, generated by the Assistant.
pub title: String,
pub messages: Vec<SavedMessage>,
}
#[derive(Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum SavedMessageRole {
User,
Assistant,
}
#[derive(Serialize, Deserialize)]
pub struct SavedMessage {
pub id: MessageId,
pub role: SavedMessageRole,
pub text: String,
}
/// Returns a list of placeholder conversations for mocking the UI.
///
/// Once we have real saved conversations to pull from we can use those instead.
pub fn placeholder_conversations() -> Vec<SavedConversation> {
vec![
SavedConversation {
version: "0.3.0".to_string(),
title: "How to get a list of exported functions in an Erlang module".to_string(),
messages: vec![],
},
SavedConversation {
version: "0.3.0".to_string(),
title: "7 wonders of the ancient world".to_string(),
messages: vec![],
},
SavedConversation {
version: "0.3.0".to_string(),
title: "Size difference between u8 and a reference to u8 in Rust".to_string(),
messages: vec![],
},
]
}

View File

@@ -0,0 +1,188 @@
use std::sync::Arc;
use fuzzy::{match_strings, StringMatch, StringMatchCandidate};
use gpui::{AppContext, DismissEvent, EventEmitter, FocusHandle, FocusableView, View, WeakView};
use picker::{Picker, PickerDelegate};
use ui::{prelude::*, HighlightedLabel, ListItem, ListItemSpacing};
use util::ResultExt;
use workspace::{ModalView, Workspace};
use crate::saved_conversation::{self, SavedConversation};
use crate::ToggleSavedConversations;
pub struct SavedConversationPicker {
picker: View<Picker<SavedConversationPickerDelegate>>,
}
impl EventEmitter<DismissEvent> for SavedConversationPicker {}
impl ModalView for SavedConversationPicker {}
impl FocusableView for SavedConversationPicker {
fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
self.picker.focus_handle(cx)
}
}
impl SavedConversationPicker {
pub fn register(workspace: &mut Workspace, _cx: &mut ViewContext<Workspace>) {
workspace.register_action(|workspace, _: &ToggleSavedConversations, cx| {
workspace.toggle_modal(cx, move |cx| {
let delegate = SavedConversationPickerDelegate::new(cx.view().downgrade());
Self::new(delegate, cx)
});
});
}
pub fn new(delegate: SavedConversationPickerDelegate, cx: &mut ViewContext<Self>) -> Self {
let picker = cx.new_view(|cx| Picker::uniform_list(delegate, cx));
Self { picker }
}
}
impl Render for SavedConversationPicker {
fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
v_flex().w(rems(34.)).child(self.picker.clone())
}
}
pub struct SavedConversationPickerDelegate {
view: WeakView<SavedConversationPicker>,
saved_conversations: Vec<SavedConversation>,
selected_index: usize,
matches: Vec<StringMatch>,
}
impl SavedConversationPickerDelegate {
pub fn new(weak_view: WeakView<SavedConversationPicker>) -> Self {
let saved_conversations = saved_conversation::placeholder_conversations();
let matches = saved_conversations
.iter()
.map(|conversation| StringMatch {
candidate_id: 0,
score: 0.0,
positions: Default::default(),
string: conversation.title.clone(),
})
.collect();
Self {
view: weak_view,
saved_conversations,
selected_index: 0,
matches,
}
}
}
impl PickerDelegate for SavedConversationPickerDelegate {
type ListItem = ui::ListItem;
fn placeholder_text(&self, _cx: &mut WindowContext) -> Arc<str> {
"Select saved conversation...".into()
}
fn match_count(&self) -> usize {
self.matches.len()
}
fn selected_index(&self) -> usize {
self.selected_index
}
fn set_selected_index(&mut self, ix: usize, _cx: &mut ViewContext<Picker<Self>>) {
self.selected_index = ix;
}
fn update_matches(
&mut self,
query: String,
cx: &mut ViewContext<Picker<Self>>,
) -> gpui::Task<()> {
let background_executor = cx.background_executor().clone();
let candidates = self
.saved_conversations
.iter()
.enumerate()
.map(|(id, conversation)| {
let text = conversation.title.clone();
StringMatchCandidate {
id,
char_bag: text.as_str().into(),
string: text,
}
})
.collect::<Vec<_>>();
cx.spawn(move |this, mut cx| async move {
let matches = if query.is_empty() {
candidates
.into_iter()
.enumerate()
.map(|(index, candidate)| StringMatch {
candidate_id: index,
string: candidate.string,
positions: Vec::new(),
score: 0.0,
})
.collect()
} else {
match_strings(
&candidates,
&query,
false,
100,
&Default::default(),
background_executor,
)
.await
};
this.update(&mut cx, |this, _cx| {
this.delegate.matches = matches;
this.delegate.selected_index = this
.delegate
.selected_index
.min(this.delegate.matches.len().saturating_sub(1));
})
.log_err();
})
}
fn confirm(&mut self, _secondary: bool, cx: &mut ViewContext<Picker<Self>>) {
if self.matches.is_empty() {
self.dismissed(cx);
return;
}
// TODO: Implement selecting a saved conversation.
}
fn dismissed(&mut self, cx: &mut ui::prelude::ViewContext<Picker<Self>>) {
self.view
.update(cx, |_, cx| cx.emit(DismissEvent))
.log_err();
}
fn render_match(
&self,
ix: usize,
selected: bool,
_cx: &mut ViewContext<Picker<Self>>,
) -> Option<Self::ListItem> {
let conversation_match = &self.matches[ix];
let _conversation = &self.saved_conversations[conversation_match.candidate_id];
Some(
ListItem::new(ix)
.inset(true)
.spacing(ListItemSpacing::Sparse)
.selected(selected)
.child(HighlightedLabel::new(
conversation_match.string.clone(),
conversation_match.positions.clone(),
)),
)
}
}

View File

@@ -1,267 +1,7 @@
use anyhow::Result;
use assistant_tooling::LanguageModelTool;
use gpui::{prelude::*, AnyView, AppContext, Model, Task};
use project::Fs;
use schemars::JsonSchema;
use semantic_index::{ProjectIndex, Status};
use serde::Deserialize;
use std::sync::Arc;
use ui::{
div, prelude::*, CollapsibleContainer, Color, Icon, IconName, Label, SharedString,
WindowContext,
};
use util::ResultExt as _;
mod annotate_code;
mod create_buffer;
mod project_index;
const DEFAULT_SEARCH_LIMIT: usize = 20;
#[derive(Clone)]
pub struct CodebaseExcerpt {
path: SharedString,
text: SharedString,
score: f32,
element_id: ElementId,
expanded: bool,
}
// Note: Comments on a `LanguageModelTool::Input` become descriptions on the generated JSON schema as shown to the language model.
// Any changes or deletions to the `CodebaseQuery` comments will change model behavior.
#[derive(Deserialize, JsonSchema)]
pub struct CodebaseQuery {
/// Semantic search query
query: String,
/// Maximum number of results to return, defaults to 20
limit: Option<usize>,
}
pub struct ProjectIndexView {
input: CodebaseQuery,
output: Result<ProjectIndexOutput>,
}
impl ProjectIndexView {
fn toggle_expanded(&mut self, element_id: ElementId, cx: &mut ViewContext<Self>) {
if let Ok(output) = &mut self.output {
if let Some(excerpt) = output
.excerpts
.iter_mut()
.find(|excerpt| excerpt.element_id == element_id)
{
excerpt.expanded = !excerpt.expanded;
cx.notify();
}
}
}
}
impl Render for ProjectIndexView {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let query = self.input.query.clone();
let result = &self.output;
let output = match result {
Err(err) => {
return div().child(Label::new(format!("Error: {}", err)).color(Color::Error));
}
Ok(output) => output,
};
div()
.v_flex()
.gap_2()
.child(
div()
.p_2()
.rounded_md()
.bg(cx.theme().colors().editor_background)
.child(
h_flex()
.child(Label::new("Query: ").color(Color::Modified))
.child(Label::new(query).color(Color::Muted)),
),
)
.children(output.excerpts.iter().map(|excerpt| {
let element_id = excerpt.element_id.clone();
let expanded = excerpt.expanded;
CollapsibleContainer::new(element_id.clone(), expanded)
.start_slot(
h_flex()
.gap_1()
.child(Icon::new(IconName::File).color(Color::Muted))
.child(Label::new(excerpt.path.clone()).color(Color::Muted)),
)
.on_click(cx.listener(move |this, _, cx| {
this.toggle_expanded(element_id.clone(), cx);
}))
.child(
div()
.p_2()
.rounded_md()
.bg(cx.theme().colors().editor_background)
.child(excerpt.text.clone()),
)
}))
}
}
pub struct ProjectIndexTool {
project_index: Model<ProjectIndex>,
fs: Arc<dyn Fs>,
}
pub struct ProjectIndexOutput {
excerpts: Vec<CodebaseExcerpt>,
status: Status,
}
impl ProjectIndexTool {
pub fn new(project_index: Model<ProjectIndex>, fs: Arc<dyn Fs>) -> Self {
// Listen for project index status and update the ProjectIndexTool directly
// TODO: setup a better description based on the user's current codebase.
Self { project_index, fs }
}
}
impl LanguageModelTool for ProjectIndexTool {
type Input = CodebaseQuery;
type Output = ProjectIndexOutput;
type View = ProjectIndexView;
fn name(&self) -> String {
"query_codebase".to_string()
}
fn description(&self) -> String {
"Semantic search against the user's current codebase, returning excerpts related to the query by computing a dot product against embeddings of chunks and an embedding of the query".to_string()
}
fn execute(&self, query: &Self::Input, cx: &AppContext) -> Task<Result<Self::Output>> {
let project_index = self.project_index.read(cx);
let status = project_index.status();
let results = project_index.search(
query.query.as_str(),
query.limit.unwrap_or(DEFAULT_SEARCH_LIMIT),
cx,
);
let fs = self.fs.clone();
cx.spawn(|cx| async move {
let results = results.await;
let excerpts = results.into_iter().map(|result| {
let abs_path = result
.worktree
.read_with(&cx, |worktree, _| worktree.abs_path().join(&result.path));
let fs = fs.clone();
async move {
let path = result.path.clone();
let text = fs.load(&abs_path?).await?;
let mut start = result.range.start;
let mut end = result.range.end.min(text.len());
while !text.is_char_boundary(start) {
start += 1;
}
while !text.is_char_boundary(end) {
end -= 1;
}
anyhow::Ok(CodebaseExcerpt {
element_id: ElementId::Name(nanoid::nanoid!().into()),
expanded: false,
path: path.to_string_lossy().to_string().into(),
text: SharedString::from(text[start..end].to_string()),
score: result.score,
})
}
});
let excerpts = futures::future::join_all(excerpts)
.await
.into_iter()
.filter_map(|result| result.log_err())
.collect();
anyhow::Ok(ProjectIndexOutput { excerpts, status })
})
}
fn output_view(
_tool_call_id: String,
input: Self::Input,
output: Result<Self::Output>,
cx: &mut WindowContext,
) -> gpui::View<Self::View> {
cx.new_view(|_cx| ProjectIndexView { input, output })
}
fn status_view(&self, cx: &mut WindowContext) -> Option<AnyView> {
Some(
cx.new_view(|cx| ProjectIndexStatusView::new(self.project_index.clone(), cx))
.into(),
)
}
fn format(_input: &Self::Input, output: &Result<Self::Output>) -> String {
match &output {
Ok(output) => {
let mut body = "Semantic search results:\n".to_string();
if output.status != Status::Idle {
body.push_str("Still indexing. Results may be incomplete.\n");
}
if output.excerpts.is_empty() {
body.push_str("No results found");
return body;
}
for excerpt in &output.excerpts {
body.push_str("Excerpt from ");
body.push_str(excerpt.path.as_ref());
body.push_str(", score ");
body.push_str(&excerpt.score.to_string());
body.push_str(":\n");
body.push_str("~~~\n");
body.push_str(excerpt.text.as_ref());
body.push_str("~~~\n");
}
body
}
Err(err) => format!("Error: {}", err),
}
}
}
struct ProjectIndexStatusView {
project_index: Model<ProjectIndex>,
}
impl ProjectIndexStatusView {
pub fn new(project_index: Model<ProjectIndex>, cx: &mut ViewContext<Self>) -> Self {
cx.subscribe(&project_index, |_this, _, _status: &Status, cx| {
cx.notify();
})
.detach();
Self { project_index }
}
}
impl Render for ProjectIndexStatusView {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let status = self.project_index.read(cx).status();
h_flex().gap_2().map(|element| match status {
Status::Idle => element.child(Label::new("Project index ready")),
Status::Loading => element.child(Label::new("Project index loading...")),
Status::Scanning { remaining_count } => element.child(Label::new(format!(
"Project index scanning: {remaining_count} remaining..."
))),
})
}
}
pub use annotate_code::*;
pub use create_buffer::*;
pub use project_index::*;

View File

@@ -0,0 +1,202 @@
use anyhow::Result;
use assistant_tooling::{LanguageModelTool, ProjectContext, ToolOutput};
use editor::{
display_map::{BlockContext, BlockDisposition, BlockProperties, BlockStyle},
Editor, MultiBuffer,
};
use gpui::{prelude::*, AnyElement, Model, Task, View, WeakView};
use language::ToPoint;
use project::{Project, ProjectPath};
use schemars::JsonSchema;
use serde::Deserialize;
use std::path::Path;
use ui::prelude::*;
use util::ResultExt;
use workspace::Workspace;
pub struct AnnotationTool {
workspace: WeakView<Workspace>,
project: Model<Project>,
}
impl AnnotationTool {
pub fn new(workspace: WeakView<Workspace>, project: Model<Project>) -> Self {
Self { workspace, project }
}
}
#[derive(Debug, Deserialize, JsonSchema, Clone)]
pub struct AnnotationInput {
/// Name for this set of annotations
title: String,
annotations: Vec<Annotation>,
}
#[derive(Debug, Deserialize, JsonSchema, Clone)]
struct Annotation {
/// Path to the file
path: String,
/// Name of a symbol in the code
symbol_name: String,
/// Text to display near the symbol definition
text: String,
}
impl LanguageModelTool for AnnotationTool {
type Input = AnnotationInput;
type Output = String;
type View = AnnotationResultView;
fn name(&self) -> String {
"annotate_code".to_string()
}
fn description(&self) -> String {
"Dynamically annotate symbols in the current codebase. Opens a buffer in a panel in their editor, to the side of the conversation. The annotations are shown in the editor as a block decoration.".to_string()
}
fn execute(&self, input: &Self::Input, cx: &mut WindowContext) -> Task<Result<Self::Output>> {
let workspace = self.workspace.clone();
let project = self.project.clone();
let excerpts = input.annotations.clone();
let title = input.title.clone();
let worktree_id = project.update(cx, |project, cx| {
let worktree = project.worktrees().next()?;
let worktree_id = worktree.read(cx).id();
Some(worktree_id)
});
let worktree_id = if let Some(worktree_id) = worktree_id {
worktree_id
} else {
return Task::ready(Err(anyhow::anyhow!("No worktree found")));
};
let buffer_tasks = project.update(cx, |project, cx| {
let excerpts = excerpts.clone();
excerpts
.iter()
.map(|excerpt| {
let project_path = ProjectPath {
worktree_id,
path: Path::new(&excerpt.path).into(),
};
project.open_buffer(project_path.clone(), cx)
})
.collect::<Vec<_>>()
});
cx.spawn(move |mut cx| async move {
let buffers = futures::future::try_join_all(buffer_tasks).await?;
let multibuffer = cx.new_model(|_cx| {
MultiBuffer::new(0, language::Capability::ReadWrite).with_title(title)
})?;
let editor =
cx.new_view(|cx| Editor::for_multibuffer(multibuffer, Some(project), cx))?;
for (excerpt, buffer) in excerpts.iter().zip(buffers.iter()) {
let snapshot = buffer.update(&mut cx, |buffer, _cx| buffer.snapshot())?;
if let Some(outline) = snapshot.outline(None) {
let matches = outline
.search(&excerpt.symbol_name, cx.background_executor().clone())
.await;
if let Some(mat) = matches.first() {
let item = &outline.items[mat.candidate_id];
let start = item.range.start.to_point(&snapshot);
editor.update(&mut cx, |editor, cx| {
let ranges = editor.buffer().update(cx, |multibuffer, cx| {
multibuffer.push_excerpts_with_context_lines(
buffer.clone(),
vec![start..start],
5,
cx,
)
});
let explanation = SharedString::from(excerpt.text.clone());
editor.insert_blocks(
[BlockProperties {
position: ranges[0].start,
height: 2,
style: BlockStyle::Fixed,
render: Box::new(move |cx| {
Self::render_note_block(&explanation, cx)
}),
disposition: BlockDisposition::Above,
}],
None,
cx,
);
})?;
}
}
}
workspace
.update(&mut cx, |workspace, cx| {
workspace.add_item_to_active_pane(Box::new(editor.clone()), None, cx);
})
.log_err();
anyhow::Ok("showed comments to users in a new view".into())
})
}
fn output_view(
_: Self::Input,
output: Result<Self::Output>,
cx: &mut WindowContext,
) -> View<Self::View> {
cx.new_view(|_cx| AnnotationResultView { output })
}
}
impl AnnotationTool {
fn render_note_block(explanation: &SharedString, cx: &mut BlockContext) -> AnyElement {
let anchor_x = cx.anchor_x;
let gutter_width = cx.gutter_dimensions.width;
h_flex()
.w_full()
.py_2()
.border_y_1()
.border_color(cx.theme().colors().border)
.child(
h_flex()
.justify_center()
.w(gutter_width)
.child(Icon::new(IconName::Ai).color(Color::Hint)),
)
.child(
h_flex()
.w_full()
.ml(anchor_x - gutter_width)
.child(explanation.clone()),
)
.into_any_element()
}
}
pub struct AnnotationResultView {
output: Result<String>,
}
impl Render for AnnotationResultView {
fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
match &self.output {
Ok(output) => div().child(output.clone().into_any_element()),
Err(error) => div().child(format!("failed to open path: {:?}", error)),
}
}
}
impl ToolOutput for AnnotationResultView {
fn generate(&self, _: &mut ProjectContext, _: &mut WindowContext) -> String {
match &self.output {
Ok(output) => output.clone(),
Err(err) => format!("Failed to create buffer: {err:?}"),
}
}
}

View File

@@ -0,0 +1,119 @@
use anyhow::Result;
use assistant_tooling::{LanguageModelTool, ProjectContext, ToolOutput};
use editor::Editor;
use gpui::{prelude::*, Model, Task, View, WeakView};
use project::Project;
use schemars::JsonSchema;
use serde::Deserialize;
use ui::prelude::*;
use util::ResultExt;
use workspace::Workspace;
pub struct CreateBufferTool {
workspace: WeakView<Workspace>,
project: Model<Project>,
}
impl CreateBufferTool {
pub fn new(workspace: WeakView<Workspace>, project: Model<Project>) -> Self {
Self { workspace, project }
}
}
#[derive(Debug, Deserialize, JsonSchema)]
pub struct CreateBufferInput {
/// The contents of the buffer.
text: String,
/// The name of the language to use for the buffer.
///
/// This should be a human-readable name, like "Rust", "JavaScript", or "Python".
language: String,
}
impl LanguageModelTool for CreateBufferTool {
type Input = CreateBufferInput;
type Output = ();
type View = CreateBufferView;
fn name(&self) -> String {
"create_buffer".to_string()
}
fn description(&self) -> String {
"Create a new buffer in the current codebase".to_string()
}
fn execute(&self, input: &Self::Input, cx: &mut WindowContext) -> Task<Result<Self::Output>> {
cx.spawn({
let workspace = self.workspace.clone();
let project = self.project.clone();
let text = input.text.clone();
let language_name = input.language.clone();
|mut cx| async move {
let language = cx
.update(|cx| {
project
.read(cx)
.languages()
.language_for_name(&language_name)
})?
.await?;
let buffer = cx
.update(|cx| project.update(cx, |project, cx| project.create_buffer(cx)))?
.await?;
buffer.update(&mut cx, |buffer, cx| {
buffer.edit([(0..0, text)], None, cx);
buffer.set_language(Some(language), cx)
})?;
workspace
.update(&mut cx, |workspace, cx| {
workspace.add_item_to_active_pane(
Box::new(
cx.new_view(|cx| Editor::for_buffer(buffer, Some(project), cx)),
),
None,
cx,
);
})
.log_err();
Ok(())
}
})
}
fn output_view(
input: Self::Input,
output: Result<Self::Output>,
cx: &mut WindowContext,
) -> View<Self::View> {
cx.new_view(|_cx| CreateBufferView {
language: input.language,
output,
})
}
}
pub struct CreateBufferView {
language: String,
output: Result<()>,
}
impl Render for CreateBufferView {
fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
div().child("Opening a buffer")
}
}
impl ToolOutput for CreateBufferView {
fn generate(&self, _: &mut ProjectContext, _: &mut WindowContext) -> String {
match &self.output {
Ok(_) => format!("Created a new {} buffer", self.language),
Err(err) => format!("Failed to create buffer: {err:?}"),
}
}
}

View File

@@ -0,0 +1,209 @@
use anyhow::Result;
use assistant_tooling::{LanguageModelTool, ToolOutput};
use collections::BTreeMap;
use gpui::{prelude::*, Model, Task};
use project::ProjectPath;
use schemars::JsonSchema;
use semantic_index::{ProjectIndex, Status};
use serde::Deserialize;
use std::{fmt::Write as _, ops::Range};
use ui::{div, prelude::*, CollapsibleContainer, Color, Icon, IconName, Label, WindowContext};
const DEFAULT_SEARCH_LIMIT: usize = 20;
pub struct ProjectIndexTool {
project_index: Model<ProjectIndex>,
}
// Note: Comments on a `LanguageModelTool::Input` become descriptions on the generated JSON schema as shown to the language model.
// Any changes or deletions to the `CodebaseQuery` comments will change model behavior.
#[derive(Deserialize, JsonSchema)]
pub struct CodebaseQuery {
/// Semantic search query
query: String,
/// Maximum number of results to return, defaults to 20
limit: Option<usize>,
}
pub struct ProjectIndexView {
input: CodebaseQuery,
output: Result<ProjectIndexOutput>,
element_id: ElementId,
expanded_header: bool,
}
pub struct ProjectIndexOutput {
status: Status,
excerpts: BTreeMap<ProjectPath, Vec<Range<usize>>>,
}
impl ProjectIndexView {
fn new(input: CodebaseQuery, output: Result<ProjectIndexOutput>) -> Self {
let element_id = ElementId::Name(nanoid::nanoid!().into());
Self {
input,
output,
element_id,
expanded_header: false,
}
}
fn toggle_header(&mut self, cx: &mut ViewContext<Self>) {
self.expanded_header = !self.expanded_header;
cx.notify();
}
}
impl Render for ProjectIndexView {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let query = self.input.query.clone();
let result = &self.output;
let output = match result {
Err(err) => {
return div().child(Label::new(format!("Error: {}", err)).color(Color::Error));
}
Ok(output) => output,
};
let file_count = output.excerpts.len();
let header = h_flex()
.gap_2()
.child(Icon::new(IconName::File))
.child(format!(
"Read {} {}",
file_count,
if file_count == 1 { "file" } else { "files" }
));
v_flex().gap_3().child(
CollapsibleContainer::new(self.element_id.clone(), self.expanded_header)
.start_slot(header)
.on_click(cx.listener(move |this, _, cx| {
this.toggle_header(cx);
}))
.child(
v_flex()
.gap_3()
.p_3()
.child(
h_flex()
.gap_2()
.child(Icon::new(IconName::MagnifyingGlass))
.child(Label::new(format!("`{}`", query)).color(Color::Muted)),
)
.child(
v_flex()
.gap_2()
.children(output.excerpts.keys().map(|path| {
h_flex().gap_2().child(Icon::new(IconName::File)).child(
Label::new(path.path.to_string_lossy().to_string())
.color(Color::Muted),
)
})),
),
),
)
}
}
impl ToolOutput for ProjectIndexView {
fn generate(
&self,
context: &mut assistant_tooling::ProjectContext,
_: &mut WindowContext,
) -> String {
match &self.output {
Ok(output) => {
let mut body = "found results in the following paths:\n".to_string();
for (project_path, ranges) in &output.excerpts {
context.add_excerpts(project_path.clone(), ranges);
writeln!(&mut body, "* {}", &project_path.path.display()).unwrap();
}
if output.status != Status::Idle {
body.push_str("Still indexing. Results may be incomplete.\n");
}
body
}
Err(err) => format!("Error: {}", err),
}
}
}
impl ProjectIndexTool {
pub fn new(project_index: Model<ProjectIndex>) -> Self {
Self { project_index }
}
}
impl LanguageModelTool for ProjectIndexTool {
type Input = CodebaseQuery;
type Output = ProjectIndexOutput;
type View = ProjectIndexView;
fn name(&self) -> String {
"query_codebase".to_string()
}
fn description(&self) -> String {
"Semantic search against the user's current codebase, returning excerpts related to the query by computing a dot product against embeddings of code chunks in the code base and an embedding of the query.".to_string()
}
fn execute(&self, query: &Self::Input, cx: &mut WindowContext) -> Task<Result<Self::Output>> {
let project_index = self.project_index.read(cx);
let status = project_index.status();
let search = project_index.search(
query.query.clone(),
query.limit.unwrap_or(DEFAULT_SEARCH_LIMIT),
cx,
);
cx.spawn(|mut cx| async move {
let search_results = search.await?;
cx.update(|cx| {
let mut output = ProjectIndexOutput {
status,
excerpts: Default::default(),
};
for search_result in search_results {
let path = ProjectPath {
worktree_id: search_result.worktree.read(cx).id(),
path: search_result.path.clone(),
};
let excerpts_for_path = output.excerpts.entry(path).or_default();
let ix = match excerpts_for_path
.binary_search_by_key(&search_result.range.start, |r| r.start)
{
Ok(ix) | Err(ix) => ix,
};
excerpts_for_path.insert(ix, search_result.range);
}
output
})
})
}
fn output_view(
input: Self::Input,
output: Result<Self::Output>,
cx: &mut WindowContext,
) -> gpui::View<Self::View> {
cx.new_view(|_cx| ProjectIndexView::new(input, output))
}
fn render_running(_: &mut WindowContext) -> impl IntoElement {
CollapsibleContainer::new(ElementId::Name(nanoid::nanoid!().into()), false)
.start_slot("Searching code base")
}
}

View File

@@ -1,5 +1,17 @@
mod active_file_button;
mod chat_message;
mod chat_notice;
mod composer;
mod project_index_button;
#[cfg(feature = "stories")]
mod stories;
pub use active_file_button::*;
pub use chat_message::*;
pub use chat_notice::*;
pub use composer::*;
pub use project_index_button::*;
#[cfg(feature = "stories")]
pub use stories::*;

View File

@@ -0,0 +1,134 @@
use crate::attachments::ActiveEditorAttachmentTool;
use assistant_tooling::AttachmentRegistry;
use editor::Editor;
use gpui::{prelude::*, Subscription, View};
use std::sync::Arc;
use ui::{prelude::*, ButtonLike, Color, Icon, IconName, Tooltip};
use workspace::Workspace;
#[derive(Clone)]
enum Status {
ActiveFile(String),
#[allow(dead_code)]
NoFile,
}
pub struct ActiveFileButton {
attachment_registry: Arc<AttachmentRegistry>,
status: Status,
#[allow(dead_code)]
workspace_subscription: Subscription,
}
impl ActiveFileButton {
pub fn new(
attachment_registry: Arc<AttachmentRegistry>,
workspace: View<Workspace>,
cx: &mut ViewContext<Self>,
) -> Self {
let workspace_subscription = cx.subscribe(&workspace, Self::handle_workspace_event);
cx.defer(move |this, cx| this.update_active_buffer(workspace.clone(), cx));
Self {
attachment_registry,
status: Status::NoFile,
workspace_subscription,
}
}
pub fn set_enabled(&mut self, enabled: bool) {
self.attachment_registry
.set_attachment_tool_enabled::<ActiveEditorAttachmentTool>(enabled);
}
pub fn update_active_buffer(&mut self, workspace: View<Workspace>, cx: &mut ViewContext<Self>) {
let active_buffer = workspace
.read(cx)
.active_item(cx)
.and_then(|item| Some(item.act_as::<Editor>(cx)?.read(cx).buffer().clone()));
if let Some(buffer) = active_buffer {
let buffer = buffer.read(cx);
if let Some(singleton) = buffer.as_singleton() {
let singleton = singleton.read(cx);
let filename: String = singleton
.file()
.map(|file| file.path().to_string_lossy())
.unwrap_or("Untitled".into())
.into();
self.status = Status::ActiveFile(filename);
}
}
}
fn handle_workspace_event(
&mut self,
workspace: View<Workspace>,
event: &workspace::Event,
cx: &mut ViewContext<Self>,
) {
if let workspace::Event::ActiveItemChanged = event {
self.update_active_buffer(workspace, cx);
}
}
}
impl Render for ActiveFileButton {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let is_enabled = self
.attachment_registry
.is_attachment_tool_enabled::<ActiveEditorAttachmentTool>();
let icon = if is_enabled {
Icon::new(IconName::File)
.size(IconSize::XSmall)
.color(Color::Default)
} else {
Icon::new(IconName::File)
.size(IconSize::XSmall)
.color(Color::Disabled)
};
let indicator = None;
let status = self.status.clone();
ButtonLike::new("active-file-button")
.child(
ui::IconWithIndicator::new(icon, indicator)
.indicator_border_color(Some(gpui::transparent_black())),
)
.tooltip({
move |cx| {
let status = status.clone();
let (tooltip, meta) = match (is_enabled, status) {
(false, _) => (
"Active file disabled".to_string(),
Some("Click to enable".to_string()),
),
(true, Status::ActiveFile(filename)) => (
format!("Active file {filename} enabled"),
Some("Click to disable".to_string()),
),
(true, Status::NoFile) => {
("No file active for conversation".to_string(), None)
}
};
if let Some(meta) = meta {
Tooltip::with_meta(tooltip, None, meta, cx)
} else {
Tooltip::text(tooltip, cx)
}
}
})
.on_click(cx.listener(move |this, _, cx| {
this.set_enabled(!is_enabled);
cx.notify();
}))
}
}

View File

@@ -1,8 +1,8 @@
use std::sync::Arc;
use client::User;
use gpui::{AnyElement, ClickEvent};
use ui::{prelude::*, Avatar};
use gpui::{hsla, AnyElement, ClickEvent};
use ui::{prelude::*, Avatar, Tooltip};
use crate::MessageId;
@@ -15,7 +15,8 @@ pub enum UserOrAssistant {
pub struct ChatMessage {
id: MessageId,
player: UserOrAssistant,
message: AnyElement,
messages: Vec<AnyElement>,
selected: bool,
collapsed: bool,
on_collapse_handle_click: Box<dyn Fn(&ClickEvent, &mut WindowContext) + 'static>,
}
@@ -24,80 +25,44 @@ impl ChatMessage {
pub fn new(
id: MessageId,
player: UserOrAssistant,
message: AnyElement,
messages: Vec<AnyElement>,
collapsed: bool,
on_collapse_handle_click: Box<dyn Fn(&ClickEvent, &mut WindowContext) + 'static>,
) -> Self {
Self {
id,
player,
message,
messages,
selected: false,
collapsed,
on_collapse_handle_click,
}
}
}
impl Selectable for ChatMessage {
fn selected(mut self, selected: bool) -> Self {
self.selected = selected;
self
}
}
impl RenderOnce for ChatMessage {
fn render(self, cx: &mut WindowContext) -> impl IntoElement {
// TODO: This should be top padding + 1.5x line height
// Set the message height to cut off at exactly 1.5 lines when collapsed
let collapsed_height = rems(2.875);
let message_group = SharedString::from(format!("{}_group", self.id.0));
let collapse_handle_id = SharedString::from(format!("{}_collapse_handle", self.id.0));
let collapse_handle = h_flex()
.id(collapse_handle_id.clone())
.group(collapse_handle_id.clone())
.flex_none()
.justify_center()
.w_1()
.mx_2()
.h_full()
.on_click(self.on_collapse_handle_click)
.child(
div()
.w_px()
.h_full()
.rounded_lg()
.overflow_hidden()
.bg(cx.theme().colors().element_background)
.group_hover(collapse_handle_id, |this| {
this.bg(cx.theme().colors().element_hover)
}),
);
let content = div()
.overflow_hidden()
.w_full()
.p_4()
.rounded_lg()
.when(self.collapsed, |this| this.h(collapsed_height))
.bg(cx.theme().colors().surface_background)
.child(self.message);
v_flex()
.gap_1()
.child(ChatMessageHeader::new(self.player))
.child(h_flex().gap_3().child(collapse_handle).child(content))
}
}
let content_padding = Spacing::Small.rems(cx);
// Clamp the message height to exactly 1.5 lines when collapsed.
let collapsed_height = content_padding.to_pixels(cx.rem_size()) + cx.line_height() * 1.5;
#[derive(IntoElement)]
struct ChatMessageHeader {
player: UserOrAssistant,
contexts: Vec<()>,
}
let background_color = if let UserOrAssistant::User(_) = &self.player {
Some(cx.theme().colors().surface_background)
} else {
None
};
impl ChatMessageHeader {
fn new(player: UserOrAssistant) -> Self {
Self {
player,
contexts: Vec::new(),
}
}
}
impl RenderOnce for ChatMessageHeader {
fn render(self, _cx: &mut WindowContext) -> impl IntoElement {
let (username, avatar_uri) = match self.player {
UserOrAssistant::Assistant => (
"Assistant".into(),
@@ -109,23 +74,67 @@ impl RenderOnce for ChatMessageHeader {
UserOrAssistant::User(None) => ("You".into(), None),
};
h_flex()
.justify_between()
v_flex()
.group(message_group.clone())
.gap(Spacing::XSmall.rems(cx))
.p(Spacing::XSmall.rems(cx))
.when(self.selected, |element| {
element.bg(hsla(0.6, 0.67, 0.46, 0.12))
})
.rounded_lg()
.child(
h_flex()
.gap_3()
.map(|this| {
let avatar_size = rems(20.0 / 16.0);
if let Some(avatar_uri) = avatar_uri {
this.child(Avatar::new(avatar_uri).size(avatar_size))
} else {
this.child(div().size(avatar_size))
}
})
.child(Label::new(username).color(Color::Default)),
.justify_between()
.px(content_padding)
.child(
h_flex()
.gap_2()
.map(|this| {
let avatar_size = rems_from_px(20.);
if let Some(avatar_uri) = avatar_uri {
this.child(Avatar::new(avatar_uri).size(avatar_size))
} else {
this.child(div().size(avatar_size))
}
})
.child(Label::new(username).color(Color::Muted)),
)
.child(
h_flex().visible_on_hover(message_group).child(
// temp icons
IconButton::new(
collapse_handle_id.clone(),
if self.collapsed {
IconName::ArrowUp
} else {
IconName::ArrowDown
},
)
.icon_size(IconSize::XSmall)
.icon_color(Color::Muted)
.on_click(self.on_collapse_handle_click)
.tooltip(|cx| Tooltip::text("Collapse Message", cx)),
),
),
)
.child(div().when(!self.contexts.is_empty(), |this| {
this.child(Label::new(self.contexts.len().to_string()).color(Color::Muted))
}))
.when(self.messages.len() > 0, |el| {
el.child(
h_flex().child(
v_flex()
.relative()
.overflow_hidden()
.w_full()
.p(content_padding)
.gap_3()
.text_ui(cx)
.rounded_lg()
.when_some(background_color, |this, background_color| {
this.bg(background_color)
})
.when(self.collapsed, |this| this.h(collapsed_height))
.children(self.messages),
),
)
})
}
}

View File

@@ -0,0 +1,71 @@
use ui::{prelude::*, Avatar, IconButtonShape};
#[derive(IntoElement)]
pub struct ChatNotice {
message: SharedString,
meta: Option<SharedString>,
}
impl ChatNotice {
pub fn new(message: impl Into<SharedString>) -> Self {
Self {
message: message.into(),
meta: None,
}
}
pub fn meta(mut self, meta: impl Into<SharedString>) -> Self {
self.meta = Some(meta.into());
self
}
}
impl RenderOnce for ChatNotice {
fn render(self, _cx: &mut WindowContext) -> impl IntoElement {
h_flex()
.w_full()
.items_start()
.mt_4()
.gap_3()
.child(
// TODO: Replace with question mark.
Avatar::new("https://zed.dev/assistant_avatar.png").size(rems_from_px(20.)),
)
.child(
v_flex()
.size_full()
.gap_1()
.pr_4()
.overflow_hidden()
.child(
h_flex()
.justify_between()
.overflow_hidden()
.child(
h_flex()
.flex_none()
.overflow_hidden()
.child(Label::new(self.message)),
)
.child(
h_flex()
.flex_shrink_0()
.gap_1()
.child(Button::new("allow", "Allow"))
.child(
IconButton::new("deny", IconName::Close)
.shape(IconButtonShape::Square)
.icon_color(Color::Muted)
.size(ButtonSize::None)
.icon_size(IconSize::XSmall),
),
),
)
.children(
self.meta.map(|meta| {
Label::new(meta).size(LabelSize::Small).color(Color::Muted)
}),
),
)
}
}

View File

@@ -1,163 +1,121 @@
use assistant_tooling::ToolRegistry;
use client::User;
use crate::{
ui::{ActiveFileButton, ProjectIndexButton},
AssistantChat, CompletionProvider,
};
use editor::{Editor, EditorElement, EditorStyle};
use gpui::{FontStyle, FontWeight, TextStyle, View, WeakView, WhiteSpace};
use gpui::{AnyElement, FontStyle, FontWeight, TextStyle, View, WeakView, WhiteSpace};
use settings::Settings;
use std::sync::Arc;
use theme::ThemeSettings;
use ui::{popover_menu, prelude::*, Avatar, ButtonLike, ContextMenu, Tooltip};
use crate::{AssistantChat, CompletionProvider, Submit, SubmitMode};
use ui::{popover_menu, prelude::*, ButtonLike, ContextMenu, Divider, TextSize, Tooltip};
#[derive(IntoElement)]
pub struct Composer {
assistant_chat: WeakView<AssistantChat>,
model: String,
editor: View<Editor>,
player: Option<Arc<User>>,
can_submit: bool,
tool_registry: Arc<ToolRegistry>,
project_index_button: View<ProjectIndexButton>,
active_file_button: Option<View<ActiveFileButton>>,
model_selector: AnyElement,
}
impl Composer {
pub fn new(
assistant_chat: WeakView<AssistantChat>,
model: String,
editor: View<Editor>,
player: Option<Arc<User>>,
can_submit: bool,
tool_registry: Arc<ToolRegistry>,
project_index_button: View<ProjectIndexButton>,
active_file_button: Option<View<ActiveFileButton>>,
model_selector: AnyElement,
) -> Self {
Self {
assistant_chat,
model,
editor,
player,
can_submit,
tool_registry,
project_index_button,
active_file_button,
model_selector,
}
}
fn render_tools(&mut self, _cx: &mut WindowContext) -> impl IntoElement {
h_flex().child(self.project_index_button.clone())
}
fn render_attachment_tools(&mut self, _cx: &mut WindowContext) -> impl IntoElement {
h_flex().children(
self.active_file_button
.clone()
.map(|view| view.into_any_element()),
)
}
}
impl RenderOnce for Composer {
fn render(self, cx: &mut WindowContext) -> impl IntoElement {
let mut player_avatar = div().size(rems(20.0 / 16.0)).into_any_element();
if let Some(player) = self.player.clone() {
player_avatar = Avatar::new(player.avatar_uri.clone())
.size(rems(20.0 / 16.0))
.into_any_element();
}
let font_size = rems(0.875);
fn render(mut self, cx: &mut WindowContext) -> impl IntoElement {
let font_size = TextSize::Default.rems(cx);
let line_height = font_size.to_pixels(cx.rem_size()) * 1.3;
h_flex()
.p(Spacing::Small.rems(cx))
.w_full()
.items_start()
.mt_4()
.gap_3()
.child(player_avatar)
.child(
v_flex()
.size_full()
.gap_1()
.child(
v_flex()
.w_full()
.p_4()
.bg(cx.theme().colors().editor_background)
.rounded_lg()
.child(
v_flex()
.justify_between()
.w_full()
.gap_1()
.min_h(line_height * 4 + px(74.0))
.child({
let settings = ThemeSettings::get_global(cx);
let text_style = TextStyle {
color: cx.theme().colors().editor_foreground,
font_family: settings.buffer_font.family.clone(),
font_features: settings.buffer_font.features.clone(),
font_size: font_size.into(),
font_weight: FontWeight::NORMAL,
font_style: FontStyle::Normal,
line_height: line_height.into(),
background_color: None,
underline: None,
strikethrough: None,
white_space: WhiteSpace::Normal,
};
v_flex().size_full().gap_1().child(
v_flex()
.w_full()
.p_3()
.bg(cx.theme().colors().editor_background)
.rounded_lg()
.child(
v_flex()
.justify_between()
.w_full()
.gap_2()
.child({
let settings = ThemeSettings::get_global(cx);
let text_style = TextStyle {
color: cx.theme().colors().editor_foreground,
font_family: settings.buffer_font.family.clone(),
font_features: settings.buffer_font.features.clone(),
font_size: font_size.into(),
font_weight: FontWeight::NORMAL,
font_style: FontStyle::Normal,
line_height: line_height.into(),
background_color: None,
underline: None,
strikethrough: None,
white_space: WhiteSpace::Normal,
};
EditorElement::new(
&self.editor,
EditorStyle {
background: cx.theme().colors().editor_background,
local_player: cx.theme().players().local(),
text: text_style,
..Default::default()
},
)
})
.child(
h_flex()
.flex_none()
.gap_2()
.justify_between()
.w_full()
.child(
h_flex().gap_1().child(
// IconButton/button
// Toggle - if enabled, .selected(true).selected_style(IconButtonStyle::Filled)
//
// match status
// Tooltip::with_meta("some label explaining project index + status", "click to enable")
IconButton::new(
"add-context",
IconName::FileDoc,
)
.icon_color(Color::Muted),
), // .child(
// IconButton::new(
// "add-context",
// IconName::Plus,
// )
// .icon_color(Color::Muted),
// ),
)
.child(
Button::new("send-button", "Send")
.style(ButtonStyle::Filled)
.disabled(!self.can_submit)
.on_click(|_, cx| {
cx.dispatch_action(Box::new(Submit(
SubmitMode::Codebase,
)))
})
.tooltip(|cx| {
Tooltip::for_action(
"Submit message",
&Submit(SubmitMode::Codebase),
cx,
)
}),
EditorElement::new(
&self.editor,
EditorStyle {
background: cx.theme().colors().editor_background,
local_player: cx.theme().players().local(),
text: text_style,
..Default::default()
},
)
})
.child(
h_flex()
.flex_none()
.gap_2()
.justify_between()
.w_full()
.child(
h_flex().gap_1().child(
h_flex()
.gap_2()
.child(self.render_tools(cx))
.child(Divider::vertical())
.child(self.render_attachment_tools(cx)),
),
),
),
)
.child(
h_flex()
.w_full()
.justify_between()
.child(ModelSelector::new(self.assistant_chat, self.model))
.children(self.tool_registry.status_views().iter().cloned()),
),
)
.child(h_flex().gap_1().child(self.model_selector)),
),
),
),
)
}
}
#[derive(IntoElement)]
struct ModelSelector {
pub struct ModelSelector {
assistant_chat: WeakView<AssistantChat>,
model: String,
}
@@ -186,7 +144,7 @@ impl RenderOnce for ModelSelector {
let assistant_chat = self.assistant_chat.clone();
move |cx| {
_ = assistant_chat.update(cx, |assistant_chat, cx| {
assistant_chat.model = model.clone();
assistant_chat.model.clone_from(&model);
cx.notify();
});
}
@@ -208,10 +166,18 @@ impl RenderOnce for ModelSelector {
.overflow_x_hidden()
.flex_grow()
.whitespace_nowrap()
.child(Label::new(self.model)),
.child(
Label::new(self.model)
.size(LabelSize::Small)
.color(Color::Muted),
),
)
.child(
div().child(Icon::new(IconName::ChevronDown).color(Color::Muted)),
div().child(
Icon::new(IconName::ChevronDown)
.color(Color::Muted)
.size(IconSize::XSmall),
),
),
)
.style(ButtonStyle::Subtle)

View File

@@ -0,0 +1,112 @@
use assistant_tooling::ToolRegistry;
use gpui::{percentage, prelude::*, Animation, AnimationExt, Model, Transformation};
use semantic_index::{ProjectIndex, Status};
use std::{sync::Arc, time::Duration};
use ui::{prelude::*, ButtonLike, Color, Icon, IconName, Indicator, Tooltip};
use crate::tools::ProjectIndexTool;
pub struct ProjectIndexButton {
project_index: Model<ProjectIndex>,
tool_registry: Arc<ToolRegistry>,
}
impl ProjectIndexButton {
pub fn new(
project_index: Model<ProjectIndex>,
tool_registry: Arc<ToolRegistry>,
cx: &mut ViewContext<Self>,
) -> Self {
cx.subscribe(&project_index, |_this, _, _status: &Status, cx| {
cx.notify();
})
.detach();
Self {
project_index,
tool_registry,
}
}
pub fn set_enabled(&mut self, enabled: bool) {
self.tool_registry
.set_tool_enabled::<ProjectIndexTool>(enabled);
}
}
impl Render for ProjectIndexButton {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let status = self.project_index.read(cx).status();
let is_enabled = self.tool_registry.is_tool_enabled::<ProjectIndexTool>();
let icon = if is_enabled {
match status {
Status::Idle => Icon::new(IconName::Code)
.size(IconSize::XSmall)
.color(Color::Default),
Status::Loading => Icon::new(IconName::Code)
.size(IconSize::XSmall)
.color(Color::Muted),
Status::Scanning { .. } => Icon::new(IconName::Code)
.size(IconSize::XSmall)
.color(Color::Muted),
}
} else {
Icon::new(IconName::Code)
.size(IconSize::XSmall)
.color(Color::Disabled)
};
let indicator = if is_enabled {
match status {
Status::Idle => Some(Indicator::dot().color(Color::Success)),
Status::Scanning { .. } => Some(Indicator::dot().color(Color::Warning)),
Status::Loading => Some(Indicator::icon(
Icon::new(IconName::Spinner)
.color(Color::Accent)
.with_animation(
"arrow-circle",
Animation::new(Duration::from_secs(2)).repeat(),
|icon, delta| icon.transform(Transformation::rotate(percentage(delta))),
),
)),
}
} else {
None
};
ButtonLike::new("project-index")
.child(
ui::IconWithIndicator::new(icon, indicator)
.indicator_border_color(Some(gpui::transparent_black())),
)
.tooltip({
move |cx| {
let (tooltip, meta) = match (is_enabled, status) {
(false, _) => (
"Project index disabled".to_string(),
Some("Click to enable".to_string()),
),
(_, Status::Idle) => (
"Project index ready".to_string(),
Some("Click to disable".to_string()),
),
(_, Status::Loading) => ("Project index loading...".to_string(), None),
(_, Status::Scanning { remaining_count }) => (
"Project index scanning...".to_string(),
Some(format!("{} remaining...", remaining_count)),
),
};
if let Some(meta) = meta {
Tooltip::with_meta(tooltip, None, meta, cx)
} else {
Tooltip::text(tooltip, cx)
}
}
})
.on_click(cx.listener(move |this, _, cx| {
this.set_enabled(!is_enabled);
cx.notify();
}))
}
}

View File

@@ -0,0 +1,5 @@
mod chat_message;
mod chat_notice;
pub use chat_message::*;
pub use chat_notice::*;

View File

@@ -0,0 +1,101 @@
use std::sync::Arc;
use client::User;
use story::{StoryContainer, StoryItem, StorySection};
use ui::prelude::*;
use crate::ui::{ChatMessage, UserOrAssistant};
use crate::MessageId;
pub struct ChatMessageStory;
impl Render for ChatMessageStory {
fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
let user_1 = Arc::new(User {
id: 12345,
github_login: "iamnbutler".into(),
avatar_uri: "https://avatars.githubusercontent.com/u/1714999?v=4".into(),
});
StoryContainer::new(
"ChatMessage Story",
"crates/assistant2/src/ui/stories/chat_message.rs",
)
.child(
StorySection::new()
.child(StoryItem::new(
"User chat message",
ChatMessage::new(
MessageId(0),
UserOrAssistant::User(Some(user_1.clone())),
vec![div().child("What can I do here?").into_any_element()],
false,
Box::new(|_, _| {}),
),
))
.child(StoryItem::new(
"User chat message (collapsed)",
ChatMessage::new(
MessageId(0),
UserOrAssistant::User(Some(user_1.clone())),
vec![div().child("What can I do here?").into_any_element()],
true,
Box::new(|_, _| {}),
),
)),
)
.child(
StorySection::new()
.child(StoryItem::new(
"Assistant chat message",
ChatMessage::new(
MessageId(0),
UserOrAssistant::Assistant,
vec![div().child("You can talk to me!").into_any_element()],
false,
Box::new(|_, _| {}),
),
))
.child(StoryItem::new(
"Assistant chat message (collapsed)",
ChatMessage::new(
MessageId(0),
UserOrAssistant::Assistant,
vec![div().child(MULTI_LINE_MESSAGE).into_any_element()],
true,
Box::new(|_, _| {}),
),
)),
)
.child(
StorySection::new().child(StoryItem::new(
"Conversation between user and assistant",
v_flex()
.gap_2()
.child(ChatMessage::new(
MessageId(0),
UserOrAssistant::User(Some(user_1.clone())),
vec![div().child("What is Rust??").into_any_element()],
false,
Box::new(|_, _| {}),
))
.child(ChatMessage::new(
MessageId(0),
UserOrAssistant::Assistant,
vec![div().child("Rust is a multi-paradigm programming language focused on performance and safety").into_any_element()],
false,
Box::new(|_, _| {}),
))
.child(ChatMessage::new(
MessageId(0),
UserOrAssistant::User(Some(user_1)),
vec![div().child("Sounds pretty cool!").into_any_element()],
false,
Box::new(|_, _| {}),
)),
)),
)
}
}
const MULTI_LINE_MESSAGE: &str = "In 2010, the movies nominated for the 82nd Academy Awards, for films released in 2009, were as follows. Note that 2010 nominees were announced for the ceremony happening in that year, but they honor movies from the previous year";

View File

@@ -0,0 +1,22 @@
use story::{StoryContainer, StoryItem, StorySection};
use ui::prelude::*;
use crate::ui::ChatNotice;
pub struct ChatNoticeStory;
impl Render for ChatNoticeStory {
fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
StoryContainer::new(
"ChatNotice Story",
"crates/assistant2/src/ui/stories/chat_notice.rs",
)
.child(
StorySection::new().child(StoryItem::new(
"Project index request",
ChatNotice::new("Allow assistant to index your project?")
.meta("Enabling will allow responses more relevant to this project."),
)),
)
}
}

View File

@@ -13,10 +13,18 @@ path = "src/assistant_tooling.rs"
[dependencies]
anyhow.workspace = true
collections.workspace = true
futures.workspace = true
gpui.workspace = true
project.workspace = true
schemars.workspace = true
serde.workspace = true
serde_json.workspace = true
sum_tree.workspace = true
util.workspace = true
[dev-dependencies]
gpui = { workspace = true, features = ["test-support"] }
project = { workspace = true, features = ["test-support"] }
settings = { workspace = true, features = ["test-support"] }
unindent.workspace = true

View File

@@ -1,5 +1,9 @@
pub mod registry;
pub mod tool;
mod attachment_registry;
mod project_context;
mod tool_registry;
pub use crate::registry::ToolRegistry;
pub use crate::tool::{LanguageModelTool, ToolFunctionCall, ToolFunctionDefinition};
pub use attachment_registry::{AttachmentRegistry, LanguageModelAttachment, UserAttachment};
pub use project_context::ProjectContext;
pub use tool_registry::{
LanguageModelTool, ToolFunctionCall, ToolFunctionDefinition, ToolOutput, ToolRegistry,
};

View File

@@ -0,0 +1,148 @@
use crate::{ProjectContext, ToolOutput};
use anyhow::{anyhow, Result};
use collections::HashMap;
use futures::future::join_all;
use gpui::{AnyView, Render, Task, View, WindowContext};
use std::{
any::TypeId,
sync::{
atomic::{AtomicBool, Ordering::SeqCst},
Arc,
},
};
use util::ResultExt as _;
pub struct AttachmentRegistry {
registered_attachments: HashMap<TypeId, RegisteredAttachment>,
}
pub trait LanguageModelAttachment {
type Output: 'static;
type View: Render + ToolOutput;
fn run(&self, cx: &mut WindowContext) -> Task<Result<Self::Output>>;
fn view(output: Result<Self::Output>, cx: &mut WindowContext) -> View<Self::View>;
}
/// A collected attachment from running an attachment tool
pub struct UserAttachment {
pub view: AnyView,
generate_fn: fn(AnyView, &mut ProjectContext, cx: &mut WindowContext) -> String,
}
/// Internal representation of an attachment tool to allow us to treat them dynamically
struct RegisteredAttachment {
enabled: AtomicBool,
call: Box<dyn Fn(&mut WindowContext) -> Task<Result<UserAttachment>>>,
}
impl AttachmentRegistry {
pub fn new() -> Self {
Self {
registered_attachments: HashMap::default(),
}
}
pub fn register<A: LanguageModelAttachment + 'static>(&mut self, attachment: A) {
let call = Box::new(move |cx: &mut WindowContext| {
let result = attachment.run(cx);
cx.spawn(move |mut cx| async move {
let result: Result<A::Output> = result.await;
let view = cx.update(|cx| A::view(result, cx))?;
Ok(UserAttachment {
view: view.into(),
generate_fn: generate::<A>,
})
})
});
self.registered_attachments.insert(
TypeId::of::<A>(),
RegisteredAttachment {
call,
enabled: AtomicBool::new(true),
},
);
return;
fn generate<T: LanguageModelAttachment>(
view: AnyView,
project: &mut ProjectContext,
cx: &mut WindowContext,
) -> String {
view.downcast::<T::View>()
.unwrap()
.update(cx, |view, cx| T::View::generate(view, project, cx))
}
}
pub fn set_attachment_tool_enabled<A: LanguageModelAttachment + 'static>(
&self,
is_enabled: bool,
) {
if let Some(attachment) = self.registered_attachments.get(&TypeId::of::<A>()) {
attachment.enabled.store(is_enabled, SeqCst);
}
}
pub fn is_attachment_tool_enabled<A: LanguageModelAttachment + 'static>(&self) -> bool {
if let Some(attachment) = self.registered_attachments.get(&TypeId::of::<A>()) {
attachment.enabled.load(SeqCst)
} else {
false
}
}
pub fn call<A: LanguageModelAttachment + 'static>(
&self,
cx: &mut WindowContext,
) -> Task<Result<UserAttachment>> {
let Some(attachment) = self.registered_attachments.get(&TypeId::of::<A>()) else {
return Task::ready(Err(anyhow!("no attachment tool")));
};
(attachment.call)(cx)
}
pub fn call_all_attachment_tools(
self: Arc<Self>,
cx: &mut WindowContext<'_>,
) -> Task<Result<Vec<UserAttachment>>> {
let this = self.clone();
cx.spawn(|mut cx| async move {
let attachment_tasks = cx.update(|cx| {
let mut tasks = Vec::new();
for attachment in this
.registered_attachments
.values()
.filter(|attachment| attachment.enabled.load(SeqCst))
{
tasks.push((attachment.call)(cx))
}
tasks
})?;
let attachments = join_all(attachment_tasks.into_iter()).await;
Ok(attachments
.into_iter()
.filter_map(|attachment| attachment.log_err())
.collect())
})
}
}
impl UserAttachment {
pub fn generate(&self, output: &mut ProjectContext, cx: &mut WindowContext) -> Option<String> {
let result = (self.generate_fn)(self.view.clone(), output, cx);
if result.is_empty() {
None
} else {
Some(result)
}
}
}

View File

@@ -0,0 +1,296 @@
use anyhow::{anyhow, Result};
use gpui::{AppContext, Model, Task, WeakModel};
use project::{Fs, Project, ProjectPath, Worktree};
use std::{cmp::Ordering, fmt::Write as _, ops::Range, sync::Arc};
use sum_tree::TreeMap;
pub struct ProjectContext {
files: TreeMap<ProjectPath, PathState>,
project: WeakModel<Project>,
fs: Arc<dyn Fs>,
}
#[derive(Debug, Clone)]
enum PathState {
PathOnly,
EntireFile,
Excerpts { ranges: Vec<Range<usize>> },
}
impl ProjectContext {
pub fn new(project: WeakModel<Project>, fs: Arc<dyn Fs>) -> Self {
Self {
files: TreeMap::default(),
fs,
project,
}
}
pub fn add_path(&mut self, project_path: ProjectPath) {
if self.files.get(&project_path).is_none() {
self.files.insert(project_path, PathState::PathOnly);
}
}
pub fn add_excerpts(&mut self, project_path: ProjectPath, new_ranges: &[Range<usize>]) {
let previous_state = self
.files
.get(&project_path)
.unwrap_or(&PathState::PathOnly);
let mut ranges = match previous_state {
PathState::EntireFile => return,
PathState::PathOnly => Vec::new(),
PathState::Excerpts { ranges } => ranges.to_vec(),
};
for new_range in new_ranges {
let ix = ranges.binary_search_by(|probe| {
if probe.end < new_range.start {
Ordering::Less
} else if probe.start > new_range.end {
Ordering::Greater
} else {
Ordering::Equal
}
});
match ix {
Ok(mut ix) => {
let existing = &mut ranges[ix];
existing.start = existing.start.min(new_range.start);
existing.end = existing.end.max(new_range.end);
while ix + 1 < ranges.len() && ranges[ix + 1].start <= ranges[ix].end {
ranges[ix].end = ranges[ix].end.max(ranges[ix + 1].end);
ranges.remove(ix + 1);
}
while ix > 0 && ranges[ix - 1].end >= ranges[ix].start {
ranges[ix].start = ranges[ix].start.min(ranges[ix - 1].start);
ranges.remove(ix - 1);
ix -= 1;
}
}
Err(ix) => {
ranges.insert(ix, new_range.clone());
}
}
}
self.files
.insert(project_path, PathState::Excerpts { ranges });
}
pub fn add_file(&mut self, project_path: ProjectPath) {
self.files.insert(project_path, PathState::EntireFile);
}
pub fn generate_system_message(&self, cx: &mut AppContext) -> Task<Result<String>> {
let project = self
.project
.upgrade()
.ok_or_else(|| anyhow!("project dropped"));
let files = self.files.clone();
let fs = self.fs.clone();
cx.spawn(|cx| async move {
let project = project?;
let mut result = "project structure:\n".to_string();
let mut last_worktree: Option<Model<Worktree>> = None;
for (project_path, path_state) in files.iter() {
if let Some(worktree) = &last_worktree {
if worktree.read_with(&cx, |tree, _| tree.id())? != project_path.worktree_id {
last_worktree = None;
}
}
let worktree;
if let Some(last_worktree) = &last_worktree {
worktree = last_worktree.clone();
} else if let Some(tree) = project.read_with(&cx, |project, cx| {
project.worktree_for_id(project_path.worktree_id, cx)
})? {
worktree = tree;
last_worktree = Some(worktree.clone());
let worktree_name =
worktree.read_with(&cx, |tree, _cx| tree.root_name().to_string())?;
writeln!(&mut result, "# {}", worktree_name).unwrap();
} else {
continue;
}
let worktree_abs_path = worktree.read_with(&cx, |tree, _cx| tree.abs_path())?;
let path = &project_path.path;
writeln!(&mut result, "## {}", path.display()).unwrap();
match path_state {
PathState::PathOnly => {}
PathState::EntireFile => {
let text = fs.load(&worktree_abs_path.join(&path)).await?;
writeln!(&mut result, "~~~\n{text}\n~~~").unwrap();
}
PathState::Excerpts { ranges } => {
let text = fs.load(&worktree_abs_path.join(&path)).await?;
writeln!(&mut result, "~~~").unwrap();
// Assumption: ranges are in order, not overlapping
let mut prev_range_end = 0;
for range in ranges {
if range.start > prev_range_end {
writeln!(&mut result, "...").unwrap();
prev_range_end = range.end;
}
let mut start = range.start;
let mut end = range.end.min(text.len());
while !text.is_char_boundary(start) {
start += 1;
}
while !text.is_char_boundary(end) {
end -= 1;
}
result.push_str(&text[start..end]);
if !result.ends_with('\n') {
result.push('\n');
}
}
if prev_range_end < text.len() {
writeln!(&mut result, "...").unwrap();
}
writeln!(&mut result, "~~~").unwrap();
}
}
}
Ok(result)
})
}
}
#[cfg(test)]
mod tests {
use std::path::Path;
use super::*;
use gpui::TestAppContext;
use project::FakeFs;
use serde_json::json;
use settings::SettingsStore;
use unindent::Unindent as _;
#[gpui::test]
async fn test_system_message_generation(cx: &mut TestAppContext) {
init_test(cx);
let file_3_contents = r#"
fn test1() {}
fn test2() {}
fn test3() {}
"#
.unindent();
let fs = FakeFs::new(cx.executor());
fs.insert_tree(
"/code",
json!({
"root1": {
"lib": {
"file1.rs": "mod example;",
"file2.rs": "",
},
"test": {
"file3.rs": file_3_contents,
}
},
"root2": {
"src": {
"main.rs": ""
}
}
}),
)
.await;
let project = Project::test(
fs.clone(),
["/code/root1".as_ref(), "/code/root2".as_ref()],
cx,
)
.await;
let worktree_ids = project.read_with(cx, |project, cx| {
project
.worktrees()
.map(|worktree| worktree.read(cx).id())
.collect::<Vec<_>>()
});
let mut ax = ProjectContext::new(project.downgrade(), fs);
ax.add_file(ProjectPath {
worktree_id: worktree_ids[0],
path: Path::new("lib/file1.rs").into(),
});
let message = cx
.update(|cx| ax.generate_system_message(cx))
.await
.unwrap();
assert_eq!(
r#"
project structure:
# root1
## lib/file1.rs
~~~
mod example;
~~~
"#
.unindent(),
message
);
ax.add_excerpts(
ProjectPath {
worktree_id: worktree_ids[0],
path: Path::new("test/file3.rs").into(),
},
&[
file_3_contents.find("fn test2").unwrap()
..file_3_contents.find("fn test3").unwrap(),
],
);
let message = cx
.update(|cx| ax.generate_system_message(cx))
.await
.unwrap();
assert_eq!(
r#"
project structure:
# root1
## lib/file1.rs
~~~
mod example;
~~~
## test/file3.rs
~~~
...
fn test2() {}
...
~~~
"#
.unindent(),
message
);
}
fn init_test(cx: &mut TestAppContext) {
cx.update(|cx| {
let settings_store = SettingsStore::test(cx);
cx.set_global(settings_store);
Project::init_settings(cx);
});
}
}

View File

@@ -1,258 +0,0 @@
use anyhow::{anyhow, Result};
use gpui::{AnyView, Task, WindowContext};
use std::collections::HashMap;
use crate::tool::{
LanguageModelTool, ToolFunctionCall, ToolFunctionCallResult, ToolFunctionDefinition,
};
pub struct ToolRegistry {
tools: HashMap<
String,
Box<dyn Fn(&ToolFunctionCall, &mut WindowContext) -> Task<Result<ToolFunctionCall>>>,
>,
definitions: Vec<ToolFunctionDefinition>,
status_views: Vec<AnyView>,
}
impl ToolRegistry {
pub fn new() -> Self {
Self {
tools: HashMap::new(),
definitions: Vec::new(),
status_views: Vec::new(),
}
}
pub fn definitions(&self) -> &[ToolFunctionDefinition] {
&self.definitions
}
pub fn register<T: 'static + LanguageModelTool>(
&mut self,
tool: T,
cx: &mut WindowContext,
) -> Result<()> {
self.definitions.push(tool.definition());
if let Some(tool_view) = tool.status_view(cx) {
self.status_views.push(tool_view);
}
let name = tool.name();
let previous = self.tools.insert(
name.clone(),
// registry.call(tool_call, cx)
Box::new(
move |tool_call: &ToolFunctionCall, cx: &mut WindowContext| {
let name = tool_call.name.clone();
let arguments = tool_call.arguments.clone();
let id = tool_call.id.clone();
let Ok(input) = serde_json::from_str::<T::Input>(arguments.as_str()) else {
return Task::ready(Ok(ToolFunctionCall {
id,
name: name.clone(),
arguments,
result: Some(ToolFunctionCallResult::ParsingFailed),
}));
};
let result = tool.execute(&input, cx);
cx.spawn(move |mut cx| async move {
let result: Result<T::Output> = result.await;
let for_model = T::format(&input, &result);
let view = cx.update(|cx| T::output_view(id.clone(), input, result, cx))?;
Ok(ToolFunctionCall {
id,
name: name.clone(),
arguments,
result: Some(ToolFunctionCallResult::Finished {
view: view.into(),
for_model,
}),
})
})
},
),
);
if previous.is_some() {
return Err(anyhow!("already registered a tool with name {}", name));
}
Ok(())
}
/// Task yields an error if the window for the given WindowContext is closed before the task completes.
pub fn call(
&self,
tool_call: &ToolFunctionCall,
cx: &mut WindowContext,
) -> Task<Result<ToolFunctionCall>> {
let name = tool_call.name.clone();
let arguments = tool_call.arguments.clone();
let id = tool_call.id.clone();
let tool = match self.tools.get(&name) {
Some(tool) => tool,
None => {
let name = name.clone();
return Task::ready(Ok(ToolFunctionCall {
id,
name: name.clone(),
arguments,
result: Some(ToolFunctionCallResult::NoSuchTool),
}));
}
};
tool(tool_call, cx)
}
pub fn status_views(&self) -> &[AnyView] {
&self.status_views
}
}
#[cfg(test)]
mod test {
use super::*;
use gpui::View;
use gpui::{div, prelude::*, Render, TestAppContext};
use schemars::schema_for;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use serde_json::json;
#[derive(Deserialize, Serialize, JsonSchema)]
struct WeatherQuery {
location: String,
unit: String,
}
struct WeatherTool {
current_weather: WeatherResult,
}
#[derive(Clone, Serialize, Deserialize, PartialEq, Debug)]
struct WeatherResult {
location: String,
temperature: f64,
unit: String,
}
struct WeatherView {
result: WeatherResult,
}
impl Render for WeatherView {
fn render(&mut self, _cx: &mut gpui::ViewContext<Self>) -> impl IntoElement {
div().child(format!("temperature: {}", self.result.temperature))
}
}
impl LanguageModelTool for WeatherTool {
type Input = WeatherQuery;
type Output = WeatherResult;
type View = WeatherView;
fn name(&self) -> String {
"get_current_weather".to_string()
}
fn description(&self) -> String {
"Fetches the current weather for a given location.".to_string()
}
fn execute(
&self,
input: &Self::Input,
_cx: &gpui::AppContext,
) -> Task<Result<Self::Output>> {
let _location = input.location.clone();
let _unit = input.unit.clone();
let weather = self.current_weather.clone();
Task::ready(Ok(weather))
}
fn output_view(
_tool_call_id: String,
_input: Self::Input,
result: Result<Self::Output>,
cx: &mut WindowContext,
) -> View<Self::View> {
cx.new_view(|_cx| {
let result = result.unwrap();
WeatherView { result }
})
}
fn format(_: &Self::Input, output: &Result<Self::Output>) -> String {
serde_json::to_string(&output.as_ref().unwrap()).unwrap()
}
}
#[gpui::test]
async fn test_openai_weather_example(cx: &mut TestAppContext) {
cx.background_executor.run_until_parked();
let tool = WeatherTool {
current_weather: WeatherResult {
location: "San Francisco".to_string(),
temperature: 21.0,
unit: "Celsius".to_string(),
},
};
let tools = vec![tool.definition()];
assert_eq!(tools.len(), 1);
let expected = ToolFunctionDefinition {
name: "get_current_weather".to_string(),
description: "Fetches the current weather for a given location.".to_string(),
parameters: schema_for!(WeatherQuery),
};
assert_eq!(tools[0].name, expected.name);
assert_eq!(tools[0].description, expected.description);
let expected_schema = serde_json::to_value(&tools[0].parameters).unwrap();
assert_eq!(
expected_schema,
json!({
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "WeatherQuery",
"type": "object",
"properties": {
"location": {
"type": "string"
},
"unit": {
"type": "string"
}
},
"required": ["location", "unit"]
})
);
let args = json!({
"location": "San Francisco",
"unit": "Celsius"
});
let query: WeatherQuery = serde_json::from_value(args).unwrap();
let result = cx.update(|cx| tool.execute(&query, cx)).await;
assert!(result.is_ok());
let result = result.unwrap();
assert_eq!(result, tool.current_weather);
}
}

View File

@@ -1,108 +0,0 @@
use anyhow::Result;
use gpui::{AnyElement, AnyView, AppContext, IntoElement as _, Render, Task, View, WindowContext};
use schemars::{schema::RootSchema, schema_for, JsonSchema};
use serde::Deserialize;
use std::fmt::Display;
#[derive(Default, Deserialize)]
pub struct ToolFunctionCall {
pub id: String,
pub name: String,
pub arguments: String,
#[serde(skip)]
pub result: Option<ToolFunctionCallResult>,
}
pub enum ToolFunctionCallResult {
NoSuchTool,
ParsingFailed,
Finished { for_model: String, view: AnyView },
}
impl ToolFunctionCallResult {
pub fn format(&self, name: &String) -> String {
match self {
ToolFunctionCallResult::NoSuchTool => format!("No tool for {name}"),
ToolFunctionCallResult::ParsingFailed => {
format!("Unable to parse arguments for {name}")
}
ToolFunctionCallResult::Finished { for_model, .. } => for_model.clone(),
}
}
pub fn into_any_element(&self, name: &String) -> AnyElement {
match self {
ToolFunctionCallResult::NoSuchTool => {
format!("Language Model attempted to call {name}").into_any_element()
}
ToolFunctionCallResult::ParsingFailed => {
format!("Language Model called {name} with bad arguments").into_any_element()
}
ToolFunctionCallResult::Finished { view, .. } => view.clone().into_any_element(),
}
}
}
#[derive(Clone)]
pub struct ToolFunctionDefinition {
pub name: String,
pub description: String,
pub parameters: RootSchema,
}
impl Display for ToolFunctionDefinition {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let schema = serde_json::to_string(&self.parameters).ok();
let schema = schema.unwrap_or("None".to_string());
write!(f, "Name: {}:\n", self.name)?;
write!(f, "Description: {}\n", self.description)?;
write!(f, "Parameters: {}", schema)
}
}
pub trait LanguageModelTool {
/// The input type that will be passed in to `execute` when the tool is called
/// by the language model.
type Input: for<'de> Deserialize<'de> + JsonSchema;
/// The output returned by executing the tool.
type Output: 'static;
type View: Render;
/// The name of the tool is exposed to the language model to allow
/// the model to pick which tools to use. As this name is used to
/// identify the tool within a tool registry, it should be unique.
fn name(&self) -> String;
/// A description of the tool that can be used to _prompt_ the model
/// as to what the tool does.
fn description(&self) -> String;
/// The OpenAI Function definition for the tool, for direct use with OpenAI's API.
fn definition(&self) -> ToolFunctionDefinition {
let root_schema = schema_for!(Self::Input);
ToolFunctionDefinition {
name: self.name(),
description: self.description(),
parameters: root_schema,
}
}
/// Execute the tool
fn execute(&self, input: &Self::Input, cx: &AppContext) -> Task<Result<Self::Output>>;
fn format(input: &Self::Input, output: &Result<Self::Output>) -> String;
fn output_view(
tool_call_id: String,
input: Self::Input,
output: Result<Self::Output>,
cx: &mut WindowContext,
) -> View<Self::View>;
fn status_view(&self, _cx: &mut WindowContext) -> Option<AnyView> {
None
}
}

View File

@@ -0,0 +1,431 @@
use anyhow::{anyhow, Result};
use gpui::{
div, AnyElement, AnyView, IntoElement, ParentElement, Render, Styled, Task, View, WindowContext,
};
use schemars::{schema::RootSchema, schema_for, JsonSchema};
use serde::Deserialize;
use std::{
any::TypeId,
collections::HashMap,
fmt::Display,
sync::atomic::{AtomicBool, Ordering::SeqCst},
};
use crate::ProjectContext;
pub struct ToolRegistry {
registered_tools: HashMap<String, RegisteredTool>,
}
#[derive(Default, Deserialize)]
pub struct ToolFunctionCall {
pub id: String,
pub name: String,
pub arguments: String,
#[serde(skip)]
pub result: Option<ToolFunctionCallResult>,
}
pub enum ToolFunctionCallResult {
NoSuchTool,
ParsingFailed,
Finished {
view: AnyView,
generate_fn: fn(AnyView, &mut ProjectContext, &mut WindowContext) -> String,
},
}
#[derive(Clone)]
pub struct ToolFunctionDefinition {
pub name: String,
pub description: String,
pub parameters: RootSchema,
}
pub trait LanguageModelTool {
/// The input type that will be passed in to `execute` when the tool is called
/// by the language model.
type Input: for<'de> Deserialize<'de> + JsonSchema;
/// The output returned by executing the tool.
type Output: 'static;
type View: Render + ToolOutput;
/// Returns the name of the tool.
///
/// This name is exposed to the language model to allow the model to pick
/// which tools to use. As this name is used to identify the tool within a
/// tool registry, it should be unique.
fn name(&self) -> String;
/// Returns the description of the tool.
///
/// This can be used to _prompt_ the model as to what the tool does.
fn description(&self) -> String;
/// Returns the OpenAI Function definition for the tool, for direct use with OpenAI's API.
fn definition(&self) -> ToolFunctionDefinition {
let root_schema = schema_for!(Self::Input);
ToolFunctionDefinition {
name: self.name(),
description: self.description(),
parameters: root_schema,
}
}
/// Executes the tool with the given input.
fn execute(&self, input: &Self::Input, cx: &mut WindowContext) -> Task<Result<Self::Output>>;
fn output_view(
input: Self::Input,
output: Result<Self::Output>,
cx: &mut WindowContext,
) -> View<Self::View>;
fn render_running(_cx: &mut WindowContext) -> impl IntoElement {
div()
}
}
pub trait ToolOutput: Sized {
fn generate(&self, project: &mut ProjectContext, cx: &mut WindowContext) -> String;
}
struct RegisteredTool {
enabled: AtomicBool,
type_id: TypeId,
call: Box<dyn Fn(&ToolFunctionCall, &mut WindowContext) -> Task<Result<ToolFunctionCall>>>,
render_running: fn(&mut WindowContext) -> gpui::AnyElement,
definition: ToolFunctionDefinition,
}
impl ToolRegistry {
pub fn new() -> Self {
Self {
registered_tools: HashMap::new(),
}
}
pub fn set_tool_enabled<T: 'static + LanguageModelTool>(&self, is_enabled: bool) {
for tool in self.registered_tools.values() {
if tool.type_id == TypeId::of::<T>() {
tool.enabled.store(is_enabled, SeqCst);
return;
}
}
}
pub fn is_tool_enabled<T: 'static + LanguageModelTool>(&self) -> bool {
for tool in self.registered_tools.values() {
if tool.type_id == TypeId::of::<T>() {
return tool.enabled.load(SeqCst);
}
}
false
}
pub fn definitions(&self) -> Vec<ToolFunctionDefinition> {
self.registered_tools
.values()
.filter(|tool| tool.enabled.load(SeqCst))
.map(|tool| tool.definition.clone())
.collect()
}
pub fn render_tool_call(
&self,
tool_call: &ToolFunctionCall,
cx: &mut WindowContext,
) -> AnyElement {
match &tool_call.result {
Some(result) => div()
.p_2()
.child(result.into_any_element(&tool_call.name))
.into_any_element(),
None => self
.registered_tools
.get(&tool_call.name)
.map(|tool| (tool.render_running)(cx))
.unwrap_or_else(|| div().into_any_element()),
}
}
pub fn register<T: 'static + LanguageModelTool>(
&mut self,
tool: T,
_cx: &mut WindowContext,
) -> Result<()> {
let name = tool.name();
let registered_tool = RegisteredTool {
type_id: TypeId::of::<T>(),
definition: tool.definition(),
enabled: AtomicBool::new(true),
call: Box::new(
move |tool_call: &ToolFunctionCall, cx: &mut WindowContext| {
let name = tool_call.name.clone();
let arguments = tool_call.arguments.clone();
let id = tool_call.id.clone();
let Ok(input) = serde_json::from_str::<T::Input>(arguments.as_str()) else {
return Task::ready(Ok(ToolFunctionCall {
id,
name: name.clone(),
arguments,
result: Some(ToolFunctionCallResult::ParsingFailed),
}));
};
let result = tool.execute(&input, cx);
cx.spawn(move |mut cx| async move {
let result: Result<T::Output> = result.await;
let view = cx.update(|cx| T::output_view(input, result, cx))?;
Ok(ToolFunctionCall {
id,
name: name.clone(),
arguments,
result: Some(ToolFunctionCallResult::Finished {
view: view.into(),
generate_fn: generate::<T>,
}),
})
})
},
),
render_running: render_running::<T>,
};
let previous = self.registered_tools.insert(name.clone(), registered_tool);
if previous.is_some() {
return Err(anyhow!("already registered a tool with name {}", name));
}
return Ok(());
fn render_running<T: LanguageModelTool>(cx: &mut WindowContext) -> AnyElement {
T::render_running(cx).into_any_element()
}
fn generate<T: LanguageModelTool>(
view: AnyView,
project: &mut ProjectContext,
cx: &mut WindowContext,
) -> String {
view.downcast::<T::View>()
.unwrap()
.update(cx, |view, cx| T::View::generate(view, project, cx))
}
}
/// Task yields an error if the window for the given WindowContext is closed before the task completes.
pub fn call(
&self,
tool_call: &ToolFunctionCall,
cx: &mut WindowContext,
) -> Task<Result<ToolFunctionCall>> {
let name = tool_call.name.clone();
let arguments = tool_call.arguments.clone();
let id = tool_call.id.clone();
let tool = match self.registered_tools.get(&name) {
Some(tool) => tool,
None => {
let name = name.clone();
return Task::ready(Ok(ToolFunctionCall {
id,
name: name.clone(),
arguments,
result: Some(ToolFunctionCallResult::NoSuchTool),
}));
}
};
(tool.call)(tool_call, cx)
}
}
impl ToolFunctionCallResult {
pub fn generate(
&self,
name: &String,
project: &mut ProjectContext,
cx: &mut WindowContext,
) -> String {
match self {
ToolFunctionCallResult::NoSuchTool => format!("No tool for {name}"),
ToolFunctionCallResult::ParsingFailed => {
format!("Unable to parse arguments for {name}")
}
ToolFunctionCallResult::Finished { generate_fn, view } => {
(generate_fn)(view.clone(), project, cx)
}
}
}
fn into_any_element(&self, name: &String) -> AnyElement {
match self {
ToolFunctionCallResult::NoSuchTool => {
format!("Language Model attempted to call {name}").into_any_element()
}
ToolFunctionCallResult::ParsingFailed => {
format!("Language Model called {name} with bad arguments").into_any_element()
}
ToolFunctionCallResult::Finished { view, .. } => view.clone().into_any_element(),
}
}
}
impl Display for ToolFunctionDefinition {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let schema = serde_json::to_string(&self.parameters).ok();
let schema = schema.unwrap_or("None".to_string());
write!(f, "Name: {}:\n", self.name)?;
write!(f, "Description: {}\n", self.description)?;
write!(f, "Parameters: {}", schema)
}
}
#[cfg(test)]
mod test {
use super::*;
use gpui::{div, prelude::*, Render, TestAppContext};
use gpui::{EmptyView, View};
use schemars::schema_for;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use serde_json::json;
#[derive(Deserialize, Serialize, JsonSchema)]
struct WeatherQuery {
location: String,
unit: String,
}
struct WeatherTool {
current_weather: WeatherResult,
}
#[derive(Clone, Serialize, Deserialize, PartialEq, Debug)]
struct WeatherResult {
location: String,
temperature: f64,
unit: String,
}
struct WeatherView {
result: WeatherResult,
}
impl Render for WeatherView {
fn render(&mut self, _cx: &mut gpui::ViewContext<Self>) -> impl IntoElement {
div().child(format!("temperature: {}", self.result.temperature))
}
}
impl ToolOutput for WeatherView {
fn generate(&self, _output: &mut ProjectContext, _cx: &mut WindowContext) -> String {
serde_json::to_string(&self.result).unwrap()
}
}
impl LanguageModelTool for WeatherTool {
type Input = WeatherQuery;
type Output = WeatherResult;
type View = WeatherView;
fn name(&self) -> String {
"get_current_weather".to_string()
}
fn description(&self) -> String {
"Fetches the current weather for a given location.".to_string()
}
fn execute(
&self,
input: &Self::Input,
_cx: &mut WindowContext,
) -> Task<Result<Self::Output>> {
let _location = input.location.clone();
let _unit = input.unit.clone();
let weather = self.current_weather.clone();
Task::ready(Ok(weather))
}
fn output_view(
_input: Self::Input,
result: Result<Self::Output>,
cx: &mut WindowContext,
) -> View<Self::View> {
cx.new_view(|_cx| {
let result = result.unwrap();
WeatherView { result }
})
}
}
#[gpui::test]
async fn test_openai_weather_example(cx: &mut TestAppContext) {
cx.background_executor.run_until_parked();
let (_, cx) = cx.add_window_view(|_cx| EmptyView);
let tool = WeatherTool {
current_weather: WeatherResult {
location: "San Francisco".to_string(),
temperature: 21.0,
unit: "Celsius".to_string(),
},
};
let tools = vec![tool.definition()];
assert_eq!(tools.len(), 1);
let expected = ToolFunctionDefinition {
name: "get_current_weather".to_string(),
description: "Fetches the current weather for a given location.".to_string(),
parameters: schema_for!(WeatherQuery),
};
assert_eq!(tools[0].name, expected.name);
assert_eq!(tools[0].description, expected.description);
let expected_schema = serde_json::to_value(&tools[0].parameters).unwrap();
assert_eq!(
expected_schema,
json!({
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "WeatherQuery",
"type": "object",
"properties": {
"location": {
"type": "string"
},
"unit": {
"type": "string"
}
},
"required": ["location", "unit"]
})
);
let args = json!({
"location": "San Francisco",
"unit": "Celsius"
});
let query: WeatherQuery = serde_json::from_value(args).unwrap();
let result = cx.update(|cx| tool.execute(&query, cx)).await;
assert!(result.is_ok());
let result = result.unwrap();
assert_eq!(result, tool.current_weather);
}
}

View File

@@ -15,7 +15,7 @@ use markdown_preview::markdown_preview_view::{MarkdownPreviewMode, MarkdownPrevi
use schemars::JsonSchema;
use serde::Deserialize;
use serde_derive::Serialize;
use smol::io::AsyncReadExt;
use smol::{fs, io::AsyncReadExt};
use settings::{Settings, SettingsSources, SettingsStore};
use smol::{fs::File, process::Command};
@@ -24,6 +24,7 @@ use release_channel::{AppCommitSha, AppVersion, ReleaseChannel};
use std::{
env::consts::{ARCH, OS},
ffi::OsString,
path::PathBuf,
sync::Arc,
time::Duration,
};
@@ -221,9 +222,9 @@ fn view_release_notes_locally(workspace: &mut Workspace, cx: &mut ViewContext<Wo
workspace
.update(&mut cx, |workspace, cx| {
let project = workspace.project().clone();
let buffer = project
.update(cx, |project, cx| project.create_buffer("", markdown, cx))
.expect("creating buffers on a local workspace always succeeds");
let buffer = project.update(cx, |project, cx| {
project.create_local_buffer("", markdown, cx)
});
buffer.update(cx, |buffer, cx| {
buffer.edit([(0..0, body.release_notes)], None, cx)
});
@@ -340,9 +341,15 @@ impl AutoUpdater {
(this.http_client.clone(), this.current_version)
})?;
let asset = match OS {
"linux" => format!("zed-linux-{}.tar.gz", ARCH),
"macos" => "Zed.dmg".into(),
_ => return Err(anyhow!("auto-update not supported for OS {:?}", OS)),
};
let mut url_string = client.build_url(&format!(
"/api/releases/latest?asset=Zed.dmg&os={}&arch={}",
OS, ARCH
"/api/releases/latest?asset={}&os={}&arch={}",
asset, OS, ARCH
));
cx.update(|cx| {
if let Some(param) = ReleaseChannel::try_global(cx)
@@ -361,6 +368,7 @@ impl AutoUpdater {
.read_to_end(&mut body)
.await
.context("error reading release")?;
let release: JsonRelease =
serde_json::from_slice(body.as_slice()).context("error deserializing release")?;
@@ -389,81 +397,18 @@ impl AutoUpdater {
let temp_dir = tempfile::Builder::new()
.prefix("zed-auto-update")
.tempdir()?;
let dmg_path = temp_dir.path().join("Zed.dmg");
let mount_path = temp_dir.path().join("Zed");
let running_app_path = ZED_APP_PATH
.clone()
.map_or_else(|| cx.update(|cx| cx.app_path())?, Ok)?;
let running_app_filename = running_app_path
.file_name()
.ok_or_else(|| anyhow!("invalid running app path"))?;
let mut mounted_app_path: OsString = mount_path.join(running_app_filename).into();
mounted_app_path.push("/");
let mut dmg_file = File::create(&dmg_path).await?;
let (installation_id, release_channel, telemetry) = cx.update(|cx| {
let installation_id = Client::global(cx).telemetry().installation_id();
let release_channel = ReleaseChannel::try_global(cx)
.map(|release_channel| release_channel.display_name());
let telemetry = TelemetrySettings::get_global(cx).metrics;
(installation_id, release_channel, telemetry)
})?;
let request_body = AsyncBody::from(serde_json::to_string(&UpdateRequestBody {
installation_id,
release_channel,
telemetry,
})?);
let mut response = client.get(&release.url, request_body, true).await?;
smol::io::copy(response.body_mut(), &mut dmg_file).await?;
log::info!("downloaded update. path:{:?}", dmg_path);
let downloaded_asset = download_release(&temp_dir, release, &asset, client, &cx).await?;
this.update(&mut cx, |this, cx| {
this.status = AutoUpdateStatus::Installing;
cx.notify();
})?;
let output = Command::new("hdiutil")
.args(&["attach", "-nobrowse"])
.arg(&dmg_path)
.arg("-mountroot")
.arg(&temp_dir.path())
.output()
.await?;
if !output.status.success() {
Err(anyhow!(
"failed to mount: {:?}",
String::from_utf8_lossy(&output.stderr)
))?;
}
let output = Command::new("rsync")
.args(&["-av", "--delete"])
.arg(&mounted_app_path)
.arg(&running_app_path)
.output()
.await?;
if !output.status.success() {
Err(anyhow!(
"failed to copy app: {:?}",
String::from_utf8_lossy(&output.stderr)
))?;
}
let output = Command::new("hdiutil")
.args(&["detach"])
.arg(&mount_path)
.output()
.await?;
if !output.status.success() {
Err(anyhow!(
"failed to unmount: {:?}",
String::from_utf8_lossy(&output.stderr)
))?;
}
match OS {
"macos" => install_release_macos(&temp_dir, downloaded_asset, &cx).await,
"linux" => install_release_linux(&temp_dir, downloaded_asset, &cx).await,
_ => Err(anyhow!("not supported: {:?}", OS)),
}?;
this.update(&mut cx, |this, cx| {
this.set_should_show_update_notification(true, cx)
@@ -471,6 +416,7 @@ impl AutoUpdater {
this.status = AutoUpdateStatus::Updated;
cx.notify();
})?;
Ok(())
}
@@ -504,3 +450,150 @@ impl AutoUpdater {
})
}
}
async fn download_release(
temp_dir: &tempfile::TempDir,
release: JsonRelease,
target_filename: &str,
client: Arc<HttpClientWithUrl>,
cx: &AsyncAppContext,
) -> Result<PathBuf> {
let target_path = temp_dir.path().join(target_filename);
let mut target_file = File::create(&target_path).await?;
let (installation_id, release_channel, telemetry) = cx.update(|cx| {
let installation_id = Client::global(cx).telemetry().installation_id();
let release_channel =
ReleaseChannel::try_global(cx).map(|release_channel| release_channel.display_name());
let telemetry = TelemetrySettings::get_global(cx).metrics;
(installation_id, release_channel, telemetry)
})?;
let request_body = AsyncBody::from(serde_json::to_string(&UpdateRequestBody {
installation_id,
release_channel,
telemetry,
})?);
let mut response = client.get(&release.url, request_body, true).await?;
smol::io::copy(response.body_mut(), &mut target_file).await?;
log::info!("downloaded update. path:{:?}", target_path);
Ok(target_path)
}
async fn install_release_linux(
temp_dir: &tempfile::TempDir,
downloaded_tar_gz: PathBuf,
cx: &AsyncAppContext,
) -> Result<()> {
let channel = cx.update(|cx| ReleaseChannel::global(cx).dev_name())?;
let home_dir = PathBuf::from(std::env::var("HOME").context("no HOME env var set")?);
let extracted = temp_dir.path().join("zed");
fs::create_dir_all(&extracted)
.await
.context("failed to create directory into which to extract update")?;
let output = Command::new("tar")
.arg("-xzf")
.arg(&downloaded_tar_gz)
.arg("-C")
.arg(&extracted)
.output()
.await?;
anyhow::ensure!(
output.status.success(),
"failed to extract {:?} to {:?}: {:?}",
downloaded_tar_gz,
extracted,
String::from_utf8_lossy(&output.stderr)
);
let suffix = if channel != "stable" {
format!("-{}", channel)
} else {
String::default()
};
let app_folder_name = format!("zed{}.app", suffix);
let from = extracted.join(&app_folder_name);
let to = home_dir.join(".local");
let output = Command::new("rsync")
.args(&["-av", "--delete"])
.arg(&from)
.arg(&to)
.output()
.await?;
anyhow::ensure!(
output.status.success(),
"failed to copy Zed update from {:?} to {:?}: {:?}",
from,
to,
String::from_utf8_lossy(&output.stderr)
);
Ok(())
}
async fn install_release_macos(
temp_dir: &tempfile::TempDir,
downloaded_dmg: PathBuf,
cx: &AsyncAppContext,
) -> Result<()> {
let running_app_path = ZED_APP_PATH
.clone()
.map_or_else(|| cx.update(|cx| cx.app_path())?, Ok)?;
let running_app_filename = running_app_path
.file_name()
.ok_or_else(|| anyhow!("invalid running app path"))?;
let mount_path = temp_dir.path().join("Zed");
let mut mounted_app_path: OsString = mount_path.join(running_app_filename).into();
mounted_app_path.push("/");
let output = Command::new("hdiutil")
.args(&["attach", "-nobrowse"])
.arg(&downloaded_dmg)
.arg("-mountroot")
.arg(&temp_dir.path())
.output()
.await?;
anyhow::ensure!(
output.status.success(),
"failed to mount: {:?}",
String::from_utf8_lossy(&output.stderr)
);
let output = Command::new("rsync")
.args(&["-av", "--delete"])
.arg(&mounted_app_path)
.arg(&running_app_path)
.output()
.await?;
anyhow::ensure!(
output.status.success(),
"failed to copy app: {:?}",
String::from_utf8_lossy(&output.stderr)
);
let output = Command::new("hdiutil")
.args(&["detach"])
.arg(&mount_path)
.output()
.await?;
anyhow::ensure!(
output.status.success(),
"failed to unount: {:?}",
String::from_utf8_lossy(&output.stderr)
);
Ok(())
}

View File

@@ -1203,11 +1203,12 @@ impl Room {
project: Model<Project>,
cx: &mut ModelContext<Self>,
) -> Task<Result<u64>> {
let request = if let Some(remote_project_id) = project.read(cx).remote_project_id() {
let request = if let Some(dev_server_project_id) = project.read(cx).dev_server_project_id()
{
self.client.request(proto::ShareProject {
room_id: self.id(),
worktrees: vec![],
remote_project_id: Some(remote_project_id.0),
dev_server_project_id: Some(dev_server_project_id.0),
})
} else {
if let Some(project_id) = project.read(cx).remote_id() {
@@ -1217,7 +1218,7 @@ impl Room {
self.client.request(proto::ShareProject {
room_id: self.id(),
worktrees: project.read(cx).worktree_metadata_protos(cx),
remote_project_id: None,
dev_server_project_id: None,
})
};

View File

@@ -16,39 +16,38 @@ doctest = false
test-support = ["clock/test-support", "collections/test-support", "gpui/test-support", "rpc/test-support"]
[dependencies]
chrono = { workspace = true, features = ["serde"] }
clock.workspace = true
collections.workspace = true
gpui.workspace = true
util.workspace = true
release_channel.workspace = true
rpc.workspace = true
text.workspace = true
settings.workspace = true
feature_flags.workspace = true
anyhow.workspace = true
async-recursion = "0.3"
async-tungstenite = { version = "0.16", features = ["async-std", "async-native-tls"] }
chrono = { workspace = true, features = ["serde"] }
clock.workspace = true
collections.workspace = true
feature_flags.workspace = true
futures.workspace = true
gpui.workspace = true
lazy_static.workspace = true
log.workspace = true
once_cell = "1.19.0"
parking_lot.workspace = true
postage.workspace = true
rand.workspace = true
release_channel.workspace = true
rpc.workspace = true
schemars.workspace = true
serde.workspace = true
serde_json.workspace = true
settings.workspace = true
sha2.workspace = true
smol.workspace = true
sysinfo.workspace = true
telemetry_events.workspace = true
tempfile.workspace = true
text.workspace = true
thiserror.workspace = true
time.workspace = true
tiny_http = "0.8"
url.workspace = true
util.workspace = true
[dev-dependencies]
clock = { workspace = true, features = ["test-support"] }

View File

@@ -30,6 +30,7 @@ use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use settings::{Settings, SettingsSources, SettingsStore};
use std::fmt;
use std::pin::Pin;
use std::{
any::TypeId,
convert::TryFrom,
@@ -65,6 +66,13 @@ impl fmt::Display for DevServerToken {
lazy_static! {
static ref ZED_SERVER_URL: Option<String> = std::env::var("ZED_SERVER_URL").ok();
static ref ZED_RPC_URL: Option<String> = std::env::var("ZED_RPC_URL").ok();
/// An environment variable whose presence indicates that the development auth
/// provider should be used.
///
/// Only works in development. Setting this environment variable in other release
/// channels is a no-op.
pub static ref ZED_DEVELOPMENT_AUTH: bool =
std::env::var("ZED_DEVELOPMENT_AUTH").map_or(false, |value| !value.is_empty());
pub static ref IMPERSONATE_LOGIN: Option<String> = std::env::var("ZED_IMPERSONATE")
.ok()
.and_then(|s| if s.is_empty() { None } else { Some(s) });
@@ -100,7 +108,7 @@ impl Settings for ClientSettings {
fn load(sources: SettingsSources<Self::FileContent>, _: &mut AppContext) -> Result<Self> {
let mut result = sources.json_merge::<Self>()?;
if let Some(server_url) = &*ZED_SERVER_URL {
result.server_url = server_url.clone()
result.server_url.clone_from(&server_url)
}
Ok(result)
}
@@ -161,6 +169,7 @@ pub struct Client {
peer: Arc<Peer>,
http: Arc<HttpClientWithUrl>,
telemetry: Arc<Telemetry>,
credentials_provider: Arc<dyn CredentialsProvider + Send + Sync + 'static>,
state: RwLock<ClientState>,
#[allow(clippy::type_complexity)]
@@ -298,6 +307,32 @@ impl Credentials {
}
}
/// A provider for [`Credentials`].
///
/// Used to abstract over reading and writing credentials to some form of
/// persistence (like the system keychain).
trait CredentialsProvider {
/// Reads the credentials from the provider.
fn read_credentials<'a>(
&'a self,
cx: &'a AsyncAppContext,
) -> Pin<Box<dyn Future<Output = Option<Credentials>> + 'a>>;
/// Writes the credentials to the provider.
fn write_credentials<'a>(
&'a self,
user_id: u64,
access_token: String,
cx: &'a AsyncAppContext,
) -> Pin<Box<dyn Future<Output = Result<()>> + 'a>>;
/// Deletes the credentials from the provider.
fn delete_credentials<'a>(
&'a self,
cx: &'a AsyncAppContext,
) -> Pin<Box<dyn Future<Output = Result<()>> + 'a>>;
}
impl Default for ClientState {
fn default() -> Self {
Self {
@@ -443,11 +478,27 @@ impl Client {
http: Arc<HttpClientWithUrl>,
cx: &mut AppContext,
) -> Arc<Self> {
let use_zed_development_auth = match ReleaseChannel::try_global(cx) {
Some(ReleaseChannel::Dev) => *ZED_DEVELOPMENT_AUTH,
Some(ReleaseChannel::Nightly | ReleaseChannel::Preview | ReleaseChannel::Stable)
| None => false,
};
let credentials_provider: Arc<dyn CredentialsProvider + Send + Sync + 'static> =
if use_zed_development_auth {
Arc::new(DevelopmentCredentialsProvider {
path: util::paths::CONFIG_DIR.join("development_auth"),
})
} else {
Arc::new(KeychainCredentialsProvider)
};
Arc::new(Self {
id: AtomicU64::new(0),
peer: Peer::new(0),
telemetry: Telemetry::new(clock, http.clone(), cx),
http,
credentials_provider,
state: Default::default(),
#[cfg(any(test, feature = "test-support"))]
@@ -763,8 +814,11 @@ impl Client {
}
}
pub async fn has_keychain_credentials(&self, cx: &AsyncAppContext) -> bool {
read_credentials_from_keychain(cx).await.is_some()
pub async fn has_credentials(&self, cx: &AsyncAppContext) -> bool {
self.credentials_provider
.read_credentials(cx)
.await
.is_some()
}
pub fn set_dev_server_token(&self, token: DevServerToken) -> &Self {
@@ -775,7 +829,7 @@ impl Client {
#[async_recursion(?Send)]
pub async fn authenticate_and_connect(
self: &Arc<Self>,
try_keychain: bool,
try_provider: bool,
cx: &AsyncAppContext,
) -> anyhow::Result<()> {
let was_disconnected = match *self.status().borrow() {
@@ -796,12 +850,13 @@ impl Client {
self.set_status(Status::Reauthenticating, cx)
}
let mut read_from_keychain = false;
let mut read_from_provider = false;
let mut credentials = self.state.read().credentials.clone();
if credentials.is_none() && try_keychain {
credentials = read_credentials_from_keychain(cx).await;
read_from_keychain = credentials.is_some();
if credentials.is_none() && try_provider {
credentials = self.credentials_provider.read_credentials(cx).await;
read_from_provider = credentials.is_some();
}
if credentials.is_none() {
let mut status_rx = self.status();
let _ = status_rx.next().await;
@@ -838,9 +893,9 @@ impl Client {
match connection {
Ok(conn) => {
self.state.write().credentials = Some(credentials.clone());
if !read_from_keychain && IMPERSONATE_LOGIN.is_none() {
if !read_from_provider && IMPERSONATE_LOGIN.is_none() {
if let Credentials::User{user_id, access_token} = credentials {
write_credentials_to_keychain(user_id, access_token, cx).await.log_err();
self.credentials_provider.write_credentials(user_id, access_token, cx).await.log_err();
}
}
@@ -854,8 +909,8 @@ impl Client {
}
Err(EstablishConnectionError::Unauthorized) => {
self.state.write().credentials.take();
if read_from_keychain {
delete_credentials_from_keychain(cx).await.log_err();
if read_from_provider {
self.credentials_provider.delete_credentials(cx).await.log_err();
self.set_status(Status::SignedOut, cx);
self.authenticate_and_connect(false, cx).await
} else {
@@ -1264,8 +1319,11 @@ impl Client {
self.state.write().credentials = None;
self.disconnect(&cx);
if self.has_keychain_credentials(cx).await {
delete_credentials_from_keychain(cx).await.log_err();
if self.has_credentials(cx).await {
self.credentials_provider
.delete_credentials(cx)
.await
.log_err();
}
}
@@ -1465,41 +1523,128 @@ impl Client {
}
}
async fn read_credentials_from_keychain(cx: &AsyncAppContext) -> Option<Credentials> {
if IMPERSONATE_LOGIN.is_some() {
return None;
}
let (user_id, access_token) = cx
.update(|cx| cx.read_credentials(&ClientSettings::get_global(cx).server_url))
.log_err()?
.await
.log_err()??;
Some(Credentials::User {
user_id: user_id.parse().ok()?,
access_token: String::from_utf8(access_token).ok()?,
})
}
async fn write_credentials_to_keychain(
#[derive(Serialize, Deserialize)]
struct DevelopmentCredentials {
user_id: u64,
access_token: String,
cx: &AsyncAppContext,
) -> Result<()> {
cx.update(move |cx| {
cx.write_credentials(
&ClientSettings::get_global(cx).server_url,
&user_id.to_string(),
access_token.as_bytes(),
)
})?
.await
}
async fn delete_credentials_from_keychain(cx: &AsyncAppContext) -> Result<()> {
cx.update(move |cx| cx.delete_credentials(&ClientSettings::get_global(cx).server_url))?
.await
/// A credentials provider that stores credentials in a local file.
///
/// This MUST only be used in development, as this is not a secure way of storing
/// credentials on user machines.
///
/// Its existence is purely to work around the annoyance of having to constantly
/// re-allow access to the system keychain when developing Zed.
struct DevelopmentCredentialsProvider {
path: PathBuf,
}
impl CredentialsProvider for DevelopmentCredentialsProvider {
fn read_credentials<'a>(
&'a self,
_cx: &'a AsyncAppContext,
) -> Pin<Box<dyn Future<Output = Option<Credentials>> + 'a>> {
async move {
if IMPERSONATE_LOGIN.is_some() {
return None;
}
let json = std::fs::read(&self.path).log_err()?;
let credentials: DevelopmentCredentials = serde_json::from_slice(&json).log_err()?;
Some(Credentials::User {
user_id: credentials.user_id,
access_token: credentials.access_token,
})
}
.boxed_local()
}
fn write_credentials<'a>(
&'a self,
user_id: u64,
access_token: String,
_cx: &'a AsyncAppContext,
) -> Pin<Box<dyn Future<Output = Result<()>> + 'a>> {
async move {
let json = serde_json::to_string(&DevelopmentCredentials {
user_id,
access_token,
})?;
std::fs::write(&self.path, json)?;
Ok(())
}
.boxed_local()
}
fn delete_credentials<'a>(
&'a self,
_cx: &'a AsyncAppContext,
) -> Pin<Box<dyn Future<Output = Result<()>> + 'a>> {
async move { Ok(std::fs::remove_file(&self.path)?) }.boxed_local()
}
}
/// A credentials provider that stores credentials in the system keychain.
struct KeychainCredentialsProvider;
impl CredentialsProvider for KeychainCredentialsProvider {
fn read_credentials<'a>(
&'a self,
cx: &'a AsyncAppContext,
) -> Pin<Box<dyn Future<Output = Option<Credentials>> + 'a>> {
async move {
if IMPERSONATE_LOGIN.is_some() {
return None;
}
let (user_id, access_token) = cx
.update(|cx| cx.read_credentials(&ClientSettings::get_global(cx).server_url))
.log_err()?
.await
.log_err()??;
Some(Credentials::User {
user_id: user_id.parse().ok()?,
access_token: String::from_utf8(access_token).ok()?,
})
}
.boxed_local()
}
fn write_credentials<'a>(
&'a self,
user_id: u64,
access_token: String,
cx: &'a AsyncAppContext,
) -> Pin<Box<dyn Future<Output = Result<()>> + 'a>> {
async move {
cx.update(move |cx| {
cx.write_credentials(
&ClientSettings::get_global(cx).server_url,
&user_id.to_string(),
access_token.as_bytes(),
)
})?
.await
}
.boxed_local()
}
fn delete_credentials<'a>(
&'a self,
cx: &'a AsyncAppContext,
) -> Pin<Box<dyn Future<Output = Result<()>> + 'a>> {
async move {
cx.update(move |cx| cx.delete_credentials(&ClientSettings::get_global(cx).server_url))?
.await
}
.boxed_local()
}
}
/// prefix for the zed:// url scheme

View File

@@ -12,7 +12,7 @@ use settings::{Settings, SettingsStore};
use sha2::{Digest, Sha256};
use std::io::Write;
use std::{env, mem, path::PathBuf, sync::Arc, time::Duration};
use sysinfo::{CpuRefreshKind, MemoryRefreshKind, Pid, ProcessRefreshKind, RefreshKind, System};
use sysinfo::{CpuRefreshKind, Pid, ProcessRefreshKind, RefreshKind, System};
use telemetry_events::{
ActionEvent, AppEvent, AssistantEvent, AssistantKind, CallEvent, CopilotEvent, CpuEvent,
EditEvent, EditorEvent, Event, EventRequestBody, EventWrapper, ExtensionEvent, MemoryEvent,
@@ -171,40 +171,38 @@ impl Telemetry {
drop(state);
let this = self.clone();
cx.spawn(|_| async move {
// Avoiding calling `System::new_all()`, as there have been crashes related to it
let refresh_kind = RefreshKind::new()
.with_memory(MemoryRefreshKind::everything()) // For memory usage
.with_processes(ProcessRefreshKind::everything()) // For process usage
.with_cpu(CpuRefreshKind::everything()); // For core count
let mut system = System::new_with_specifics(refresh_kind);
// Avoiding calling `refresh_all()`, just update what we need
system.refresh_specifics(refresh_kind);
// Waiting some amount of time before the first query is important to get a reasonable value
// https://docs.rs/sysinfo/0.29.10/sysinfo/trait.ProcessExt.html#tymethod.cpu_usage
const DURATION_BETWEEN_SYSTEM_EVENTS: Duration = Duration::from_secs(4 * 60);
loop {
smol::Timer::after(DURATION_BETWEEN_SYSTEM_EVENTS).await;
system.refresh_specifics(refresh_kind);
cx.background_executor()
.spawn(async move {
let mut system = System::new_with_specifics(
RefreshKind::new().with_cpu(CpuRefreshKind::everything()),
);
let refresh_kind = ProcessRefreshKind::new().with_cpu().with_memory();
let current_process = Pid::from_u32(std::process::id());
let Some(process) = system.processes().get(&current_process) else {
let process = current_process;
log::error!("Failed to find own process {process:?} in system process table");
// TODO: Fire an error telemetry event
return;
};
system.refresh_process_specifics(current_process, refresh_kind);
this.report_memory_event(process.memory(), process.virtual_memory());
this.report_cpu_event(process.cpu_usage(), system.cpus().len() as u32);
}
})
.detach();
// Waiting some amount of time before the first query is important to get a reasonable value
// https://docs.rs/sysinfo/0.29.10/sysinfo/trait.ProcessExt.html#tymethod.cpu_usage
const DURATION_BETWEEN_SYSTEM_EVENTS: Duration = Duration::from_secs(4 * 60);
loop {
smol::Timer::after(DURATION_BETWEEN_SYSTEM_EVENTS).await;
let current_process = Pid::from_u32(std::process::id());
system.refresh_process_specifics(current_process, refresh_kind);
let Some(process) = system.process(current_process) else {
log::error!(
"Failed to find own process {current_process:?} in system process table"
);
// TODO: Fire an error telemetry event
return;
};
this.report_memory_event(process.memory(), process.virtual_memory());
this.report_cpu_event(process.cpu_usage(), system.cpus().len() as u32);
}
})
.detach();
}
pub fn set_authenticated_user_info(
@@ -219,7 +217,7 @@ impl Telemetry {
}
let metrics_id: Option<Arc<str>> = metrics_id.map(|id| id.into());
state.metrics_id = metrics_id.clone();
state.metrics_id.clone_from(&metrics_id);
state.is_staff = Some(is_staff);
drop(state);
}

View File

@@ -33,7 +33,7 @@ pub struct DevServerId(pub u64);
#[derive(
Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, serde::Serialize, serde::Deserialize,
)]
pub struct RemoteProjectId(pub u64);
pub struct DevServerProjectId(pub u64);
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ParticipantIndex(pub u32);

View File

@@ -39,6 +39,7 @@ live_kit_server.workspace = true
log.workspace = true
nanoid.workspace = true
open_ai.workspace = true
supermaven_api.workspace = true
parking_lot.workspace = true
prometheus = "0.13"
prost.workspace = true
@@ -82,6 +83,7 @@ env_logger.workspace = true
file_finder.workspace = true
fs = { workspace = true, features = ["test-support"] }
git = { workspace = true, features = ["test-support"] }
git_hosting_providers.workspace = true
gpui = { workspace = true, features = ["test-support"] }
indoc.workspace = true
language = { workspace = true, features = ["test-support"] }
@@ -93,7 +95,7 @@ notifications = { workspace = true, features = ["test-support"] }
pretty_assertions.workspace = true
project = { workspace = true, features = ["test-support"] }
release_channel.workspace = true
remote_projects.workspace = true
dev_server_projects.workspace = true
rpc = { workspace = true, features = ["test-support"] }
sea-orm = { version = "0.12.x", features = ["sqlx-sqlite"] }
serde_json.workspace = true

View File

@@ -172,6 +172,11 @@ spec:
secretKeyRef:
name: slack
key: panics_webhook
- name: SUPERMAVEN_ADMIN_API_KEY
valueFrom:
secretKeyRef:
name: supermaven
key: api_key
- name: INVITE_LINK_PREFIX
value: ${INVITE_LINK_PREFIX}
- name: RUST_BACKTRACE

View File

@@ -51,7 +51,7 @@ CREATE TABLE "projects" (
"host_connection_server_id" INTEGER REFERENCES servers (id) ON DELETE CASCADE,
"unregistered" BOOLEAN NOT NULL DEFAULT FALSE,
"hosted_project_id" INTEGER REFERENCES hosted_projects (id),
"remote_project_id" INTEGER REFERENCES remote_projects(id)
"dev_server_project_id" INTEGER REFERENCES dev_server_projects(id)
);
CREATE INDEX "index_projects_on_host_connection_server_id" ON "projects" ("host_connection_server_id");
CREATE INDEX "index_projects_on_host_connection_id_and_host_connection_server_id" ON "projects" ("host_connection_id", "host_connection_server_id");
@@ -410,10 +410,8 @@ CREATE TABLE dev_servers (
hashed_token TEXT NOT NULL
);
CREATE TABLE remote_projects (
CREATE TABLE dev_server_projects (
id INTEGER PRIMARY KEY AUTOINCREMENT,
dev_server_id INTEGER NOT NULL REFERENCES dev_servers(id),
path TEXT NOT NULL
);
ALTER TABLE hosted_projects ADD COLUMN remote_project_id INTEGER REFERENCES remote_projects(id);

View File

@@ -0,0 +1,11 @@
CREATE TABLE dev_server_projects (
id INT PRIMARY KEY GENERATED ALWAYS AS IDENTITY (START WITH 100),
dev_server_id INT NOT NULL REFERENCES dev_servers(id) ON DELETE CASCADE,
path TEXT NOT NULL
);
INSERT INTO dev_server_projects OVERRIDING SYSTEM VALUE SELECT * FROM remote_projects;
ALTER TABLE dev_server_projects ADD CONSTRAINT uix_dev_server_projects_dev_server_id_path UNIQUE(dev_server_id, path);
ALTER TABLE projects ADD COLUMN dev_server_project_id INTEGER REFERENCES dev_server_projects(id);
UPDATE projects SET dev_server_project_id = remote_project_id;

View File

@@ -0,0 +1,2 @@
ALTER TABLE projects DROP COLUMN remote_project_id;
DROP TABLE remote_projects;

View File

@@ -116,13 +116,6 @@ struct CreateUserParams {
invite_count: i32,
}
#[derive(Serialize, Debug)]
struct CreateUserResponse {
user: User,
signup_device_id: Option<String>,
metrics_id: String,
}
async fn get_rpc_server_snapshot(
Extension(rpc_server): Extension<Option<Arc<rpc::Server>>>,
) -> Result<ErasedJson> {

View File

@@ -264,7 +264,7 @@ pub async fn post_hang(
Error::Internal(anyhow!(err))
})?;
let mut backtrace = "Possible hang detected on main threadL".to_string();
let mut backtrace = "Possible hang detected on main thread:".to_string();
let unknown = "<unknown>".to_string();
for frame in report.backtrace.iter() {
backtrace.push_str(&format!("\n{}", frame.symbols.first().unwrap_or(&unknown)));

View File

@@ -0,0 +1,2 @@
use anyhow::{anyhow, Result};
use rpc::proto;

View File

@@ -762,7 +762,7 @@ pub struct Project {
pub collaborators: Vec<ProjectCollaborator>,
pub worktrees: BTreeMap<u64, Worktree>,
pub language_servers: Vec<proto::LanguageServer>,
pub remote_project_id: Option<RemoteProjectId>,
pub dev_server_project_id: Option<DevServerProjectId>,
}
pub struct ProjectCollaborator {

View File

@@ -84,7 +84,7 @@ id_type!(NotificationId);
id_type!(NotificationKindId);
id_type!(ProjectCollaboratorId);
id_type!(ProjectId);
id_type!(RemoteProjectId);
id_type!(DevServerProjectId);
id_type!(ReplicaId);
id_type!(RoomId);
id_type!(RoomParticipantId);

View File

@@ -5,6 +5,7 @@ pub mod buffers;
pub mod channels;
pub mod contacts;
pub mod contributors;
pub mod dev_server_projects;
pub mod dev_servers;
pub mod embeddings;
pub mod extensions;
@@ -13,7 +14,6 @@ pub mod messages;
pub mod notifications;
pub mod projects;
pub mod rate_buckets;
pub mod remote_projects;
pub mod rooms;
pub mod servers;
pub mod users;

View File

@@ -0,0 +1,340 @@
use anyhow::anyhow;
use rpc::{
proto::{self},
ConnectionId,
};
use sea_orm::{
ActiveModelTrait, ActiveValue, ColumnTrait, Condition, DatabaseTransaction, EntityTrait,
ModelTrait, QueryFilter,
};
use crate::db::ProjectId;
use super::{
dev_server, dev_server_project, project, project_collaborator, worktree, Database, DevServerId,
DevServerProjectId, RejoinedProject, ResharedProject, ServerId, UserId,
};
impl Database {
pub async fn get_dev_server_project(
&self,
dev_server_project_id: DevServerProjectId,
) -> crate::Result<dev_server_project::Model> {
self.transaction(|tx| async move {
Ok(
dev_server_project::Entity::find_by_id(dev_server_project_id)
.one(&*tx)
.await?
.ok_or_else(|| {
anyhow!("no dev server project with id {}", dev_server_project_id)
})?,
)
})
.await
}
pub async fn get_projects_for_dev_server(
&self,
dev_server_id: DevServerId,
) -> crate::Result<Vec<proto::DevServerProject>> {
self.transaction(|tx| async move {
self.get_projects_for_dev_server_internal(dev_server_id, &tx)
.await
})
.await
}
pub async fn get_projects_for_dev_server_internal(
&self,
dev_server_id: DevServerId,
tx: &DatabaseTransaction,
) -> crate::Result<Vec<proto::DevServerProject>> {
let servers = dev_server_project::Entity::find()
.filter(dev_server_project::Column::DevServerId.eq(dev_server_id))
.find_also_related(project::Entity)
.all(tx)
.await?;
Ok(servers
.into_iter()
.map(|(dev_server_project, project)| proto::DevServerProject {
id: dev_server_project.id.to_proto(),
project_id: project.map(|p| p.id.to_proto()),
dev_server_id: dev_server_project.dev_server_id.to_proto(),
path: dev_server_project.path,
})
.collect())
}
pub async fn dev_server_project_ids_for_user(
&self,
user_id: UserId,
tx: &DatabaseTransaction,
) -> crate::Result<Vec<DevServerProjectId>> {
let dev_servers = dev_server::Entity::find()
.filter(dev_server::Column::UserId.eq(user_id))
.find_with_related(dev_server_project::Entity)
.all(tx)
.await?;
Ok(dev_servers
.into_iter()
.flat_map(|(_, projects)| projects.into_iter().map(|p| p.id))
.collect())
}
pub async fn owner_for_dev_server_project(
&self,
dev_server_project_id: DevServerProjectId,
tx: &DatabaseTransaction,
) -> crate::Result<UserId> {
let dev_server = dev_server_project::Entity::find_by_id(dev_server_project_id)
.find_also_related(dev_server::Entity)
.one(tx)
.await?
.and_then(|(_, dev_server)| dev_server)
.ok_or_else(|| anyhow!("no dev server project"))?;
Ok(dev_server.user_id)
}
pub async fn get_stale_dev_server_projects(
&self,
connection: ConnectionId,
) -> crate::Result<Vec<ProjectId>> {
self.transaction(|tx| async move {
let projects = project::Entity::find()
.filter(
Condition::all()
.add(project::Column::HostConnectionId.eq(connection.id))
.add(project::Column::HostConnectionServerId.eq(connection.owner_id)),
)
.all(&*tx)
.await?;
Ok(projects.into_iter().map(|p| p.id).collect())
})
.await
}
pub async fn create_dev_server_project(
&self,
dev_server_id: DevServerId,
path: &str,
user_id: UserId,
) -> crate::Result<(dev_server_project::Model, proto::DevServerProjectsUpdate)> {
self.transaction(|tx| async move {
let dev_server = dev_server::Entity::find_by_id(dev_server_id)
.one(&*tx)
.await?
.ok_or_else(|| anyhow!("no dev server with id {}", dev_server_id))?;
if dev_server.user_id != user_id {
return Err(anyhow!("not your dev server"))?;
}
let project = dev_server_project::Entity::insert(dev_server_project::ActiveModel {
id: ActiveValue::NotSet,
dev_server_id: ActiveValue::Set(dev_server_id),
path: ActiveValue::Set(path.to_string()),
})
.exec_with_returning(&*tx)
.await?;
let status = self
.dev_server_projects_update_internal(user_id, &tx)
.await?;
Ok((project, status))
})
.await
}
pub async fn delete_dev_server_project(
&self,
dev_server_project_id: DevServerProjectId,
dev_server_id: DevServerId,
user_id: UserId,
) -> crate::Result<(Vec<proto::DevServerProject>, proto::DevServerProjectsUpdate)> {
self.transaction(|tx| async move {
project::Entity::delete_many()
.filter(project::Column::DevServerProjectId.eq(dev_server_project_id))
.exec(&*tx)
.await?;
let result = dev_server_project::Entity::delete_by_id(dev_server_project_id)
.exec(&*tx)
.await?;
if result.rows_affected != 1 {
return Err(anyhow!(
"no dev server project with id {}",
dev_server_project_id
))?;
}
let status = self
.dev_server_projects_update_internal(user_id, &tx)
.await?;
let projects = self
.get_projects_for_dev_server_internal(dev_server_id, &tx)
.await?;
Ok((projects, status))
})
.await
}
pub async fn share_dev_server_project(
&self,
dev_server_project_id: DevServerProjectId,
dev_server_id: DevServerId,
connection: ConnectionId,
worktrees: &[proto::WorktreeMetadata],
) -> crate::Result<(
proto::DevServerProject,
UserId,
proto::DevServerProjectsUpdate,
)> {
self.transaction(|tx| async move {
let dev_server = dev_server::Entity::find_by_id(dev_server_id)
.one(&*tx)
.await?
.ok_or_else(|| anyhow!("no dev server with id {}", dev_server_id))?;
let dev_server_project = dev_server_project::Entity::find_by_id(dev_server_project_id)
.one(&*tx)
.await?
.ok_or_else(|| {
anyhow!("no dev server project with id {}", dev_server_project_id)
})?;
if dev_server_project.dev_server_id != dev_server_id {
return Err(anyhow!("dev server project shared from wrong server"))?;
}
let project = project::ActiveModel {
room_id: ActiveValue::Set(None),
host_user_id: ActiveValue::Set(None),
host_connection_id: ActiveValue::set(Some(connection.id as i32)),
host_connection_server_id: ActiveValue::set(Some(ServerId(
connection.owner_id as i32,
))),
id: ActiveValue::NotSet,
hosted_project_id: ActiveValue::Set(None),
dev_server_project_id: ActiveValue::Set(Some(dev_server_project_id)),
}
.insert(&*tx)
.await?;
if !worktrees.is_empty() {
worktree::Entity::insert_many(worktrees.iter().map(|worktree| {
worktree::ActiveModel {
id: ActiveValue::set(worktree.id as i64),
project_id: ActiveValue::set(project.id),
abs_path: ActiveValue::set(worktree.abs_path.clone()),
root_name: ActiveValue::set(worktree.root_name.clone()),
visible: ActiveValue::set(worktree.visible),
scan_id: ActiveValue::set(0),
completed_scan_id: ActiveValue::set(0),
}
}))
.exec(&*tx)
.await?;
}
let status = self
.dev_server_projects_update_internal(dev_server.user_id, &tx)
.await?;
Ok((
dev_server_project.to_proto(Some(project)),
dev_server.user_id,
status,
))
})
.await
}
pub async fn reshare_dev_server_projects(
&self,
reshared_projects: &Vec<proto::UpdateProject>,
dev_server_id: DevServerId,
connection: ConnectionId,
) -> crate::Result<Vec<ResharedProject>> {
// todo!() project_transaction? (maybe we can make the lock per-dev-server instead of per-project?)
self.transaction(|tx| async move {
let mut ret = Vec::new();
for reshared_project in reshared_projects {
let project_id = ProjectId::from_proto(reshared_project.project_id);
let (project, dev_server_project) = project::Entity::find_by_id(project_id)
.find_also_related(dev_server_project::Entity)
.one(&*tx)
.await?
.ok_or_else(|| anyhow!("project does not exist"))?;
if dev_server_project.map(|rp| rp.dev_server_id) != Some(dev_server_id) {
return Err(anyhow!("dev server project reshared from wrong server"))?;
}
let Ok(old_connection_id) = project.host_connection() else {
return Err(anyhow!("dev server project was not shared"))?;
};
project::Entity::update(project::ActiveModel {
id: ActiveValue::set(project_id),
host_connection_id: ActiveValue::set(Some(connection.id as i32)),
host_connection_server_id: ActiveValue::set(Some(ServerId(
connection.owner_id as i32,
))),
..Default::default()
})
.exec(&*tx)
.await?;
let collaborators = project
.find_related(project_collaborator::Entity)
.all(&*tx)
.await?;
self.update_project_worktrees(project_id, &reshared_project.worktrees, &tx)
.await?;
ret.push(super::ResharedProject {
id: project_id,
old_connection_id,
collaborators: collaborators
.iter()
.map(|collaborator| super::ProjectCollaborator {
connection_id: collaborator.connection(),
user_id: collaborator.user_id,
replica_id: collaborator.replica_id,
is_host: collaborator.is_host,
})
.collect(),
worktrees: reshared_project.worktrees.clone(),
});
}
Ok(ret)
})
.await
}
pub async fn rejoin_dev_server_projects(
&self,
rejoined_projects: &Vec<proto::RejoinProject>,
user_id: UserId,
connection_id: ConnectionId,
) -> crate::Result<Vec<RejoinedProject>> {
// todo!() project_transaction? (maybe we can make the lock per-dev-server instead of per-project?)
self.transaction(|tx| async move {
let mut ret = Vec::new();
for rejoined_project in rejoined_projects {
if let Some(project) = self
.rejoin_project_internal(&tx, rejoined_project, user_id, connection_id)
.await?
{
ret.push(project);
}
}
Ok(ret)
})
.await
}
}

View File

@@ -3,7 +3,7 @@ use sea_orm::{
ActiveValue, ColumnTrait, DatabaseTransaction, EntityTrait, IntoActiveModel, QueryFilter,
};
use super::{dev_server, remote_project, Database, DevServerId, UserId};
use super::{dev_server, dev_server_project, Database, DevServerId, UserId};
impl Database {
pub async fn get_dev_server(
@@ -29,43 +29,43 @@ impl Database {
.await
}
pub async fn remote_projects_update(
pub async fn dev_server_projects_update(
&self,
user_id: UserId,
) -> crate::Result<proto::RemoteProjectsUpdate> {
self.transaction(
|tx| async move { self.remote_projects_update_internal(user_id, &tx).await },
)
) -> crate::Result<proto::DevServerProjectsUpdate> {
self.transaction(|tx| async move {
self.dev_server_projects_update_internal(user_id, &tx).await
})
.await
}
pub async fn remote_projects_update_internal(
pub async fn dev_server_projects_update_internal(
&self,
user_id: UserId,
tx: &DatabaseTransaction,
) -> crate::Result<proto::RemoteProjectsUpdate> {
) -> crate::Result<proto::DevServerProjectsUpdate> {
let dev_servers = dev_server::Entity::find()
.filter(dev_server::Column::UserId.eq(user_id))
.all(tx)
.await?;
let remote_projects = remote_project::Entity::find()
let dev_server_projects = dev_server_project::Entity::find()
.filter(
remote_project::Column::DevServerId
dev_server_project::Column::DevServerId
.is_in(dev_servers.iter().map(|d| d.id).collect::<Vec<_>>()),
)
.find_also_related(super::project::Entity)
.all(tx)
.await?;
Ok(proto::RemoteProjectsUpdate {
Ok(proto::DevServerProjectsUpdate {
dev_servers: dev_servers
.into_iter()
.map(|d| d.to_proto(proto::DevServerStatus::Offline))
.collect(),
remote_projects: remote_projects
dev_server_projects: dev_server_projects
.into_iter()
.map(|(remote_project, project)| remote_project.to_proto(project))
.map(|(dev_server_project, project)| dev_server_project.to_proto(project))
.collect(),
})
}
@@ -75,29 +75,36 @@ impl Database {
name: &str,
hashed_access_token: &str,
user_id: UserId,
) -> crate::Result<(dev_server::Model, proto::RemoteProjectsUpdate)> {
) -> crate::Result<(dev_server::Model, proto::DevServerProjectsUpdate)> {
self.transaction(|tx| async move {
if name.trim().is_empty() {
return Err(anyhow::anyhow!(proto::ErrorCode::Forbidden))?;
}
let dev_server = dev_server::Entity::insert(dev_server::ActiveModel {
id: ActiveValue::NotSet,
hashed_token: ActiveValue::Set(hashed_access_token.to_string()),
name: ActiveValue::Set(name.to_string()),
name: ActiveValue::Set(name.trim().to_string()),
user_id: ActiveValue::Set(user_id),
})
.exec_with_returning(&*tx)
.await?;
let remote_projects = self.remote_projects_update_internal(user_id, &tx).await?;
let dev_server_projects = self
.dev_server_projects_update_internal(user_id, &tx)
.await?;
Ok((dev_server, remote_projects))
Ok((dev_server, dev_server_projects))
})
.await
}
pub async fn delete_dev_server(
pub async fn update_dev_server_token(
&self,
id: DevServerId,
hashed_token: &str,
user_id: UserId,
) -> crate::Result<proto::RemoteProjectsUpdate> {
) -> crate::Result<proto::DevServerProjectsUpdate> {
self.transaction(|tx| async move {
let Some(dev_server) = dev_server::Entity::find_by_id(id).one(&*tx).await? else {
return Err(anyhow::anyhow!("no dev server with id {}", id))?;
@@ -106,8 +113,67 @@ impl Database {
return Err(anyhow::anyhow!(proto::ErrorCode::Forbidden))?;
}
remote_project::Entity::delete_many()
.filter(remote_project::Column::DevServerId.eq(id))
dev_server::Entity::update(dev_server::ActiveModel {
hashed_token: ActiveValue::Set(hashed_token.to_string()),
..dev_server.clone().into_active_model()
})
.exec(&*tx)
.await?;
let dev_server_projects = self
.dev_server_projects_update_internal(user_id, &tx)
.await?;
Ok(dev_server_projects)
})
.await
}
pub async fn rename_dev_server(
&self,
id: DevServerId,
name: &str,
user_id: UserId,
) -> crate::Result<proto::DevServerProjectsUpdate> {
self.transaction(|tx| async move {
let Some(dev_server) = dev_server::Entity::find_by_id(id).one(&*tx).await? else {
return Err(anyhow::anyhow!("no dev server with id {}", id))?;
};
if dev_server.user_id != user_id || name.trim().is_empty() {
return Err(anyhow::anyhow!(proto::ErrorCode::Forbidden))?;
}
dev_server::Entity::update(dev_server::ActiveModel {
name: ActiveValue::Set(name.trim().to_string()),
..dev_server.clone().into_active_model()
})
.exec(&*tx)
.await?;
let dev_server_projects = self
.dev_server_projects_update_internal(user_id, &tx)
.await?;
Ok(dev_server_projects)
})
.await
}
pub async fn delete_dev_server(
&self,
id: DevServerId,
user_id: UserId,
) -> crate::Result<proto::DevServerProjectsUpdate> {
self.transaction(|tx| async move {
let Some(dev_server) = dev_server::Entity::find_by_id(id).one(&*tx).await? else {
return Err(anyhow::anyhow!("no dev server with id {}", id))?;
};
if dev_server.user_id != user_id {
return Err(anyhow::anyhow!(proto::ErrorCode::Forbidden))?;
}
dev_server_project::Entity::delete_many()
.filter(dev_server_project::Column::DevServerId.eq(id))
.exec(&*tx)
.await?;
@@ -115,9 +181,11 @@ impl Database {
.exec(&*tx)
.await?;
let remote_projects = self.remote_projects_update_internal(user_id, &tx).await?;
let dev_server_projects = self
.dev_server_projects_update_internal(user_id, &tx)
.await?;
Ok(remote_projects)
Ok(dev_server_projects)
})
.await
}

View File

@@ -30,7 +30,7 @@ impl Database {
room_id: RoomId,
connection: ConnectionId,
worktrees: &[proto::WorktreeMetadata],
remote_project_id: Option<RemoteProjectId>,
dev_server_project_id: Option<DevServerProjectId>,
) -> Result<TransactionGuard<(ProjectId, proto::Room)>> {
self.room_transaction(room_id, |tx| async move {
let participant = room_participant::Entity::find()
@@ -59,9 +59,9 @@ impl Database {
return Err(anyhow!("guests cannot share projects"))?;
}
if let Some(remote_project_id) = remote_project_id {
if let Some(dev_server_project_id) = dev_server_project_id {
let project = project::Entity::find()
.filter(project::Column::RemoteProjectId.eq(Some(remote_project_id)))
.filter(project::Column::DevServerProjectId.eq(Some(dev_server_project_id)))
.one(&*tx)
.await?
.ok_or_else(|| anyhow!("no remote project"))?;
@@ -78,7 +78,6 @@ impl Database {
.await?;
// todo! check user is a project-collaborator
let room = self.get_room(room_id, &tx).await?;
return Ok((project.id, room));
}
@@ -92,7 +91,7 @@ impl Database {
))),
id: ActiveValue::NotSet,
hosted_project_id: ActiveValue::Set(None),
remote_project_id: ActiveValue::Set(None),
dev_server_project_id: ActiveValue::Set(None),
}
.insert(&*tx)
.await?;
@@ -155,11 +154,11 @@ impl Database {
.await?;
return Ok((room, guest_connection_ids));
}
if let Some(remote_project_id) = project.remote_project_id {
if let Some(dev_server_project_id) = project.dev_server_project_id {
if let Some(user_id) = user_id {
if user_id
!= self
.owner_for_remote_project(remote_project_id, &tx)
.owner_for_dev_server_project(dev_server_project_id, &tx)
.await?
{
Err(anyhow!("cannot unshare a project hosted by another user"))?
@@ -598,6 +597,17 @@ impl Database {
.await
}
pub async fn find_dev_server_project(&self, id: DevServerProjectId) -> Result<project::Model> {
self.transaction(|tx| async move {
Ok(project::Entity::find()
.filter(project::Column::DevServerProjectId.eq(id))
.one(&*tx)
.await?
.ok_or_else(|| anyhow!("no such project"))?)
})
.await
}
/// Adds the given connection to the specified project
/// in the current room.
pub async fn join_project(
@@ -797,7 +807,7 @@ impl Database {
name: language_server.name,
})
.collect(),
remote_project_id: project.remote_project_id,
dev_server_project_id: project.dev_server_project_id,
};
Ok((project, replica_id as ReplicaId))
}
@@ -957,8 +967,8 @@ impl Database {
capability: Capability,
tx: &DatabaseTransaction,
) -> Result<(project::Model, ChannelRole)> {
let (mut project, remote_project) = project::Entity::find_by_id(project_id)
.find_also_related(remote_project::Entity)
let (mut project, dev_server_project) = project::Entity::find_by_id(project_id)
.find_also_related(dev_server_project::Entity)
.one(tx)
.await?
.ok_or_else(|| anyhow!("no such project"))?;
@@ -986,8 +996,8 @@ impl Database {
} else {
None
};
let role_from_remote_project = if let Some(remote_project) = remote_project {
let dev_server = dev_server::Entity::find_by_id(remote_project.dev_server_id)
let role_from_dev_server = if let Some(dev_server_project) = dev_server_project {
let dev_server = dev_server::Entity::find_by_id(dev_server_project.dev_server_id)
.one(tx)
.await?
.ok_or_else(|| anyhow!("no such channel"))?;
@@ -1011,7 +1021,7 @@ impl Database {
None
};
let role = role_from_remote_project
let role = role_from_dev_server
.or(role_from_room)
.unwrap_or(ChannelRole::Banned);

View File

@@ -1,283 +1 @@
use anyhow::anyhow;
use rpc::{proto, ConnectionId};
use sea_orm::{
ActiveModelTrait, ActiveValue, ColumnTrait, Condition, DatabaseTransaction, EntityTrait,
ModelTrait, QueryFilter,
};
use crate::db::ProjectId;
use super::{
dev_server, project, project_collaborator, remote_project, worktree, Database, DevServerId,
RejoinedProject, RemoteProjectId, ResharedProject, ServerId, UserId,
};
impl Database {
pub async fn get_remote_project(
&self,
remote_project_id: RemoteProjectId,
) -> crate::Result<remote_project::Model> {
self.transaction(|tx| async move {
Ok(remote_project::Entity::find_by_id(remote_project_id)
.one(&*tx)
.await?
.ok_or_else(|| anyhow!("no remote project with id {}", remote_project_id))?)
})
.await
}
pub async fn get_remote_projects_for_dev_server(
&self,
dev_server_id: DevServerId,
) -> crate::Result<Vec<proto::RemoteProject>> {
self.transaction(|tx| async move {
let servers = remote_project::Entity::find()
.filter(remote_project::Column::DevServerId.eq(dev_server_id))
.find_also_related(project::Entity)
.all(&*tx)
.await?;
Ok(servers
.into_iter()
.map(|(remote_project, project)| proto::RemoteProject {
id: remote_project.id.to_proto(),
project_id: project.map(|p| p.id.to_proto()),
dev_server_id: remote_project.dev_server_id.to_proto(),
path: remote_project.path,
})
.collect())
})
.await
}
pub async fn remote_project_ids_for_user(
&self,
user_id: UserId,
tx: &DatabaseTransaction,
) -> crate::Result<Vec<RemoteProjectId>> {
let dev_servers = dev_server::Entity::find()
.filter(dev_server::Column::UserId.eq(user_id))
.find_with_related(remote_project::Entity)
.all(tx)
.await?;
Ok(dev_servers
.into_iter()
.flat_map(|(_, projects)| projects.into_iter().map(|p| p.id))
.collect())
}
pub async fn owner_for_remote_project(
&self,
remote_project_id: RemoteProjectId,
tx: &DatabaseTransaction,
) -> crate::Result<UserId> {
let dev_server = remote_project::Entity::find_by_id(remote_project_id)
.find_also_related(dev_server::Entity)
.one(tx)
.await?
.and_then(|(_, dev_server)| dev_server)
.ok_or_else(|| anyhow!("no remote project"))?;
Ok(dev_server.user_id)
}
pub async fn get_stale_dev_server_projects(
&self,
connection: ConnectionId,
) -> crate::Result<Vec<ProjectId>> {
self.transaction(|tx| async move {
let projects = project::Entity::find()
.filter(
Condition::all()
.add(project::Column::HostConnectionId.eq(connection.id))
.add(project::Column::HostConnectionServerId.eq(connection.owner_id)),
)
.all(&*tx)
.await?;
Ok(projects.into_iter().map(|p| p.id).collect())
})
.await
}
pub async fn create_remote_project(
&self,
dev_server_id: DevServerId,
path: &str,
user_id: UserId,
) -> crate::Result<(remote_project::Model, proto::RemoteProjectsUpdate)> {
self.transaction(|tx| async move {
let dev_server = dev_server::Entity::find_by_id(dev_server_id)
.one(&*tx)
.await?
.ok_or_else(|| anyhow!("no dev server with id {}", dev_server_id))?;
if dev_server.user_id != user_id {
return Err(anyhow!("not your dev server"))?;
}
let project = remote_project::Entity::insert(remote_project::ActiveModel {
id: ActiveValue::NotSet,
dev_server_id: ActiveValue::Set(dev_server_id),
path: ActiveValue::Set(path.to_string()),
})
.exec_with_returning(&*tx)
.await?;
let status = self.remote_projects_update_internal(user_id, &tx).await?;
Ok((project, status))
})
.await
}
pub async fn share_remote_project(
&self,
remote_project_id: RemoteProjectId,
dev_server_id: DevServerId,
connection: ConnectionId,
worktrees: &[proto::WorktreeMetadata],
) -> crate::Result<(proto::RemoteProject, UserId, proto::RemoteProjectsUpdate)> {
self.transaction(|tx| async move {
let dev_server = dev_server::Entity::find_by_id(dev_server_id)
.one(&*tx)
.await?
.ok_or_else(|| anyhow!("no dev server with id {}", dev_server_id))?;
let remote_project = remote_project::Entity::find_by_id(remote_project_id)
.one(&*tx)
.await?
.ok_or_else(|| anyhow!("no remote project with id {}", remote_project_id))?;
if remote_project.dev_server_id != dev_server_id {
return Err(anyhow!("remote project shared from wrong server"))?;
}
let project = project::ActiveModel {
room_id: ActiveValue::Set(None),
host_user_id: ActiveValue::Set(None),
host_connection_id: ActiveValue::set(Some(connection.id as i32)),
host_connection_server_id: ActiveValue::set(Some(ServerId(
connection.owner_id as i32,
))),
id: ActiveValue::NotSet,
hosted_project_id: ActiveValue::Set(None),
remote_project_id: ActiveValue::Set(Some(remote_project_id)),
}
.insert(&*tx)
.await?;
if !worktrees.is_empty() {
worktree::Entity::insert_many(worktrees.iter().map(|worktree| {
worktree::ActiveModel {
id: ActiveValue::set(worktree.id as i64),
project_id: ActiveValue::set(project.id),
abs_path: ActiveValue::set(worktree.abs_path.clone()),
root_name: ActiveValue::set(worktree.root_name.clone()),
visible: ActiveValue::set(worktree.visible),
scan_id: ActiveValue::set(0),
completed_scan_id: ActiveValue::set(0),
}
}))
.exec(&*tx)
.await?;
}
let status = self
.remote_projects_update_internal(dev_server.user_id, &tx)
.await?;
Ok((
remote_project.to_proto(Some(project)),
dev_server.user_id,
status,
))
})
.await
}
pub async fn reshare_remote_projects(
&self,
reshared_projects: &Vec<proto::UpdateProject>,
dev_server_id: DevServerId,
connection: ConnectionId,
) -> crate::Result<Vec<ResharedProject>> {
// todo!() project_transaction? (maybe we can make the lock per-dev-server instead of per-project?)
self.transaction(|tx| async move {
let mut ret = Vec::new();
for reshared_project in reshared_projects {
let project_id = ProjectId::from_proto(reshared_project.project_id);
let (project, remote_project) = project::Entity::find_by_id(project_id)
.find_also_related(remote_project::Entity)
.one(&*tx)
.await?
.ok_or_else(|| anyhow!("project does not exist"))?;
if remote_project.map(|rp| rp.dev_server_id) != Some(dev_server_id) {
return Err(anyhow!("remote project reshared from wrong server"))?;
}
let Ok(old_connection_id) = project.host_connection() else {
return Err(anyhow!("remote project was not shared"))?;
};
project::Entity::update(project::ActiveModel {
id: ActiveValue::set(project_id),
host_connection_id: ActiveValue::set(Some(connection.id as i32)),
host_connection_server_id: ActiveValue::set(Some(ServerId(
connection.owner_id as i32,
))),
..Default::default()
})
.exec(&*tx)
.await?;
let collaborators = project
.find_related(project_collaborator::Entity)
.all(&*tx)
.await?;
self.update_project_worktrees(project_id, &reshared_project.worktrees, &tx)
.await?;
ret.push(super::ResharedProject {
id: project_id,
old_connection_id,
collaborators: collaborators
.iter()
.map(|collaborator| super::ProjectCollaborator {
connection_id: collaborator.connection(),
user_id: collaborator.user_id,
replica_id: collaborator.replica_id,
is_host: collaborator.is_host,
})
.collect(),
worktrees: reshared_project.worktrees.clone(),
});
}
Ok(ret)
})
.await
}
pub async fn rejoin_remote_projects(
&self,
rejoined_projects: &Vec<proto::RejoinProject>,
user_id: UserId,
connection_id: ConnectionId,
) -> crate::Result<Vec<RejoinedProject>> {
// todo!() project_transaction? (maybe we can make the lock per-dev-server instead of per-project?)
self.transaction(|tx| async move {
let mut ret = Vec::new();
for rejoined_project in rejoined_projects {
if let Some(project) = self
.rejoin_project_internal(&tx, rejoined_project, user_id, connection_id)
.await?
{
ret.push(project);
}
}
Ok(ret)
})
.await
}
}

View File

@@ -851,17 +851,17 @@ impl Database {
.await?;
// if any project in the room has a remote-project-id that belongs to a dev server that this user owns.
let remote_projects_for_user = self
.remote_project_ids_for_user(leaving_participant.user_id, &tx)
let dev_server_projects_for_user = self
.dev_server_project_ids_for_user(leaving_participant.user_id, &tx)
.await?;
let remote_projects_to_unshare = project::Entity::find()
let dev_server_projects_to_unshare = project::Entity::find()
.filter(
Condition::all()
.add(project::Column::RoomId.eq(room_id))
.add(
project::Column::RemoteProjectId
.is_in(remote_projects_for_user.clone()),
project::Column::DevServerProjectId
.is_in(dev_server_projects_for_user.clone()),
),
)
.all(&*tx)
@@ -892,7 +892,7 @@ impl Database {
}
if (collaborator.is_host && collaborator.connection() == connection)
|| remote_projects_to_unshare.contains(&collaborator.project_id)
|| dev_server_projects_to_unshare.contains(&collaborator.project_id)
{
left_project.should_unshare = true;
}
@@ -936,9 +936,9 @@ impl Database {
.exec(&*tx)
.await?;
if !remote_projects_to_unshare.is_empty() {
if !dev_server_projects_to_unshare.is_empty() {
project::Entity::update_many()
.filter(project::Column::Id.is_in(remote_projects_to_unshare))
.filter(project::Column::Id.is_in(dev_server_projects_to_unshare))
.set(project::ActiveModel {
room_id: ActiveValue::Set(None),
..Default::default()
@@ -1316,8 +1316,10 @@ impl Database {
project.worktree_root_names.push(db_worktree.root_name);
}
}
} else if let Some(remote_project_id) = db_project.remote_project_id {
let host = self.owner_for_remote_project(remote_project_id, tx).await?;
} else if let Some(dev_server_project_id) = db_project.dev_server_project_id {
let host = self
.owner_for_dev_server_project(dev_server_project_id, tx)
.await?;
if let Some((_, participant)) = participants
.iter_mut()
.find(|(_, v)| v.user_id == host.to_proto())

View File

@@ -11,6 +11,7 @@ pub mod channel_message_mention;
pub mod contact;
pub mod contributor;
pub mod dev_server;
pub mod dev_server_project;
pub mod embedding;
pub mod extension;
pub mod extension_version;
@@ -25,7 +26,6 @@ pub mod observed_channel_messages;
pub mod project;
pub mod project_collaborator;
pub mod rate_buckets;
pub mod remote_project;
pub mod room;
pub mod room_participant;
pub mod server;

View File

@@ -16,11 +16,11 @@ impl ActiveModelBehavior for ActiveModel {}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {
#[sea_orm(has_many = "super::remote_project::Entity")]
#[sea_orm(has_many = "super::dev_server_project::Entity")]
RemoteProject,
}
impl Related<super::remote_project::Entity> for Entity {
impl Related<super::dev_server_project::Entity> for Entity {
fn to() -> RelationDef {
Relation::RemoteProject.def()
}

View File

@@ -1,13 +1,13 @@
use super::project;
use crate::db::{DevServerId, RemoteProjectId};
use crate::db::{DevServerId, DevServerProjectId};
use rpc::proto;
use sea_orm::entity::prelude::*;
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
#[sea_orm(table_name = "remote_projects")]
#[sea_orm(table_name = "dev_server_projects")]
pub struct Model {
#[sea_orm(primary_key)]
pub id: RemoteProjectId,
pub id: DevServerProjectId,
pub dev_server_id: DevServerId,
pub path: String,
}
@@ -39,8 +39,8 @@ impl Related<super::dev_server::Entity> for Entity {
}
impl Model {
pub fn to_proto(&self, project: Option<project::Model>) -> proto::RemoteProject {
proto::RemoteProject {
pub fn to_proto(&self, project: Option<project::Model>) -> proto::DevServerProject {
proto::DevServerProject {
id: self.id.to_proto(),
project_id: project.map(|p| p.id.to_proto()),
dev_server_id: self.dev_server_id.to_proto(),

View File

@@ -1,4 +1,4 @@
use crate::db::{HostedProjectId, ProjectId, RemoteProjectId, Result, RoomId, ServerId, UserId};
use crate::db::{DevServerProjectId, HostedProjectId, ProjectId, Result, RoomId, ServerId, UserId};
use anyhow::anyhow;
use rpc::ConnectionId;
use sea_orm::entity::prelude::*;
@@ -13,7 +13,7 @@ pub struct Model {
pub host_connection_id: Option<i32>,
pub host_connection_server_id: Option<ServerId>,
pub hosted_project_id: Option<HostedProjectId>,
pub remote_project_id: Option<RemoteProjectId>,
pub dev_server_project_id: Option<DevServerProjectId>,
}
impl Model {
@@ -58,9 +58,9 @@ pub enum Relation {
)]
HostedProject,
#[sea_orm(
belongs_to = "super::remote_project::Entity",
from = "Column::RemoteProjectId",
to = "super::remote_project::Column::Id"
belongs_to = "super::dev_server_project::Entity",
from = "Column::DevServerProjectId",
to = "super::dev_server_project::Column::Id"
)]
RemoteProject,
}
@@ -101,7 +101,7 @@ impl Related<super::hosted_project::Entity> for Entity {
}
}
impl Related<super::remote_project::Entity> for Entity {
impl Related<super::dev_server_project::Entity> for Entity {
fn to() -> RelationDef {
Relation::RemoteProject.def()
}

View File

@@ -138,6 +138,7 @@ pub struct Config {
pub zed_client_checksum_seed: Option<String>,
pub slack_panics_webhook: Option<String>,
pub auto_join_channel_id: Option<ChannelId>,
pub supermaven_admin_api_key: Option<Arc<str>>,
}
impl Config {

View File

@@ -4,9 +4,9 @@ use crate::{
auth,
db::{
self, dev_server, BufferId, Capability, Channel, ChannelId, ChannelRole, ChannelsForUser,
CreatedChannelMessage, Database, DevServerId, InviteMemberResult, MembershipUpdated,
MessageId, NotificationId, PrincipalId, Project, ProjectId, RejoinedProject,
RemoteProjectId, RemoveChannelMemberResult, ReplicaId, RespondToChannelInvite, RoomId,
CreatedChannelMessage, Database, DevServerId, DevServerProjectId, InviteMemberResult,
MembershipUpdated, MessageId, NotificationId, PrincipalId, Project, ProjectId,
RejoinedProject, RemoveChannelMemberResult, ReplicaId, RespondToChannelInvite, RoomId,
ServerId, UpdatedChannelMessage, User, UserId,
},
executor::Executor,
@@ -34,6 +34,7 @@ pub use connection_pool::{ConnectionPool, ZedVersion};
use core::fmt::{self, Debug, Formatter};
use open_ai::{OpenAiEmbeddingModel, OPEN_AI_API_URL};
use sha2::Digest;
use supermaven_api::{CreateExternalUserRequest, SupermavenAdminApi};
use futures::{
channel::oneshot,
@@ -74,6 +75,8 @@ use tracing::{
};
use util::http::IsahcHttpClient;
use self::connection_pool::VersionedMessage;
pub const RECONNECT_TIMEOUT: Duration = Duration::from_secs(30);
// kubernetes gives terminated pods 10s to shutdown gracefully. After they're gone, we can clean up old resources.
@@ -146,7 +149,8 @@ struct Session {
peer: Arc<Peer>,
connection_pool: Arc<parking_lot::Mutex<ConnectionPool>>,
live_kit_client: Option<Arc<dyn live_kit_server::api::Client>>,
http_client: IsahcHttpClient,
supermaven_client: Option<Arc<SupermavenAdminApi>>,
http_client: Arc<IsahcHttpClient>,
rate_limiter: Arc<RateLimiter>,
_executor: Executor,
}
@@ -187,6 +191,14 @@ impl Session {
}
}
fn is_staff(&self) -> bool {
match &self.principal {
Principal::User(user) => user.admin,
Principal::Impersonated { .. } => true,
Principal::DevServer(_) => false,
}
}
fn dev_server_id(&self) -> Option<DevServerId> {
match &self.principal {
Principal::User(_) | Principal::Impersonated { .. } => None,
@@ -231,6 +243,14 @@ impl UserSession {
pub fn user_id(&self) -> UserId {
self.0.user_id().unwrap()
}
pub fn email(&self) -> Option<String> {
match &self.0.principal {
Principal::User(user) => user.email_address.clone(),
Principal::Impersonated { user, .. } => user.email_address.clone(),
Principal::DevServer(..) => None,
}
}
}
impl Deref for UserSession {
@@ -409,11 +429,14 @@ impl Server {
.add_message_handler(unshare_project)
.add_request_handler(user_handler(join_project))
.add_request_handler(user_handler(join_hosted_project))
.add_request_handler(user_handler(rejoin_remote_projects))
.add_request_handler(user_handler(create_remote_project))
.add_request_handler(user_handler(rejoin_dev_server_projects))
.add_request_handler(user_handler(create_dev_server_project))
.add_request_handler(user_handler(delete_dev_server_project))
.add_request_handler(user_handler(create_dev_server))
.add_request_handler(user_handler(regenerate_dev_server_token))
.add_request_handler(user_handler(rename_dev_server))
.add_request_handler(user_handler(delete_dev_server))
.add_request_handler(dev_server_handler(share_remote_project))
.add_request_handler(dev_server_handler(share_dev_server_project))
.add_request_handler(dev_server_handler(shutdown_dev_server))
.add_request_handler(dev_server_handler(reconnect_dev_server))
.add_message_handler(user_message_handler(leave_project))
@@ -465,6 +488,9 @@ impl Server {
.add_request_handler(user_handler(
forward_mutating_project_request::<proto::ApplyCompletionAdditionalEdits>,
))
.add_request_handler(user_handler(
forward_versioned_mutating_project_request::<proto::OpenNewBuffer>,
))
.add_request_handler(user_handler(
forward_mutating_project_request::<proto::ResolveCompletionDocumentation>,
))
@@ -505,7 +531,7 @@ impl Server {
forward_mutating_project_request::<proto::OnTypeFormatting>,
))
.add_request_handler(user_handler(
forward_mutating_project_request::<proto::SaveBuffer>,
forward_versioned_mutating_project_request::<proto::SaveBuffer>,
))
.add_request_handler(user_handler(
forward_mutating_project_request::<proto::BlameBuffer>,
@@ -555,6 +581,7 @@ impl Server {
.add_request_handler(user_handler(get_private_user_info))
.add_message_handler(user_message_handler(acknowledge_channel_message))
.add_message_handler(user_message_handler(acknowledge_buffer_version))
.add_request_handler(user_handler(get_supermaven_api_key))
.add_streaming_request_handler({
let app_state = app_state.clone();
move |request, response, session| {
@@ -932,13 +959,22 @@ impl Server {
tracing::info!("connection opened");
let http_client = match IsahcHttpClient::new() {
Ok(http_client) => http_client,
Ok(http_client) => Arc::new(http_client),
Err(error) => {
tracing::error!(?error, "failed to create HTTP client");
return;
}
};
let supermaven_client = if let Some(supermaven_admin_api_key) = this.app_state.config.supermaven_admin_api_key.clone() {
Some(Arc::new(SupermavenAdminApi::new(
supermaven_admin_api_key.to_string(),
http_client.clone(),
)))
} else {
None
};
let session = Session {
principal: principal.clone(),
connection_id,
@@ -949,6 +985,7 @@ impl Server {
http_client,
rate_limiter: this.app_state.rate_limiter.clone(),
_executor: executor.clone(),
supermaven_client,
};
if let Err(error) = this.send_initial_client_update(connection_id, &principal, zed_version, send_connection_id, &session).await {
@@ -1062,12 +1099,12 @@ impl Server {
.await?;
}
let (contacts, channels_for_user, channel_invites, remote_projects) =
let (contacts, channels_for_user, channel_invites, dev_server_projects) =
future::try_join4(
self.app_state.db.get_contacts(user.id),
self.app_state.db.get_channels_for_user(user.id),
self.app_state.db.get_channel_invites_for_user(user.id),
self.app_state.db.remote_projects_update(user.id),
self.app_state.db.dev_server_projects_update(user.id),
)
.await?;
@@ -1090,7 +1127,7 @@ impl Server {
build_channels_update(channels_for_user, channel_invites),
)?;
}
send_remote_projects_update(user.id, remote_projects, session).await;
send_dev_server_projects_update(user.id, dev_server_projects, session).await;
if let Some(incoming_call) =
self.app_state.db.incoming_call_for_user(user.id).await?
@@ -1112,7 +1149,7 @@ impl Server {
let projects = self
.app_state
.db
.get_remote_projects_for_dev_server(dev_server.id)
.get_projects_for_dev_server(dev_server.id)
.await?;
self.peer
.send(connection_id, proto::DevServerInstructions { projects })?;
@@ -1120,9 +1157,9 @@ impl Server {
let status = self
.app_state
.db
.remote_projects_update(dev_server.user_id)
.dev_server_projects_update(dev_server.user_id)
.await?;
send_remote_projects_update(dev_server.user_id, status, &session).await;
send_dev_server_projects_update(dev_server.user_id, status, &session).await;
}
}
@@ -1965,8 +2002,8 @@ async fn share_project(
session.connection_id,
&request.worktrees,
request
.remote_project_id
.map(|id| RemoteProjectId::from_proto(id)),
.dev_server_project_id
.map(|id| DevServerProjectId::from_proto(id)),
)
.await?;
response.send(proto::ShareProjectResponse {
@@ -2018,26 +2055,26 @@ async fn unshare_project_internal(
}
/// DevServer makes a project available online
async fn share_remote_project(
request: proto::ShareRemoteProject,
response: Response<proto::ShareRemoteProject>,
async fn share_dev_server_project(
request: proto::ShareDevServerProject,
response: Response<proto::ShareDevServerProject>,
session: DevServerSession,
) -> Result<()> {
let (remote_project, user_id, status) = session
let (dev_server_project, user_id, status) = session
.db()
.await
.share_remote_project(
RemoteProjectId::from_proto(request.remote_project_id),
.share_dev_server_project(
DevServerProjectId::from_proto(request.dev_server_project_id),
session.dev_server_id(),
session.connection_id,
&request.worktrees,
)
.await?;
let Some(project_id) = remote_project.project_id else {
let Some(project_id) = dev_server_project.project_id else {
return Err(anyhow!("failed to share remote project"))?;
};
send_remote_projects_update(user_id, status, &session).await;
send_dev_server_projects_update(user_id, status, &session).await;
response.send(proto::ShareProjectResponse { project_id })?;
@@ -2130,9 +2167,9 @@ fn join_project_internal(
collaborators: collaborators.clone(),
language_servers: project.language_servers.clone(),
role: project.role.into(),
remote_project_id: project
.remote_project_id
.map(|remote_project_id| remote_project_id.0 as u64),
dev_server_project_id: project
.dev_server_project_id
.map(|dev_server_project_id| dev_server_project_id.0 as u64),
})?;
for (worktree_id, worktree) in mem::take(&mut project.worktrees) {
@@ -2244,9 +2281,9 @@ async fn join_hosted_project(
join_project_internal(response, session, &mut project, &replica_id)
}
async fn create_remote_project(
request: proto::CreateRemoteProject,
response: Response<proto::CreateRemoteProject>,
async fn create_dev_server_project(
request: proto::CreateDevServerProject,
response: Response<proto::CreateDevServerProject>,
session: UserSession,
) -> Result<()> {
let dev_server_id = DevServerId(request.dev_server_id as i32);
@@ -2267,14 +2304,14 @@ async fn create_remote_project(
.forward_request(
session.connection_id,
dev_server_connection_id,
proto::ValidateRemoteProjectRequest { path: path.clone() },
proto::ValidateDevServerProjectRequest { path: path.clone() },
)
.await?;
let (remote_project, update) = session
let (dev_server_project, update) = session
.db()
.await
.create_remote_project(
.create_dev_server_project(
DevServerId(request.dev_server_id as i32),
&request.path,
session.user_id(),
@@ -2284,7 +2321,7 @@ async fn create_remote_project(
let projects = session
.db()
.await
.get_remote_projects_for_dev_server(remote_project.dev_server_id)
.get_projects_for_dev_server(dev_server_project.dev_server_id)
.await?;
session.peer.send(
@@ -2292,10 +2329,10 @@ async fn create_remote_project(
proto::DevServerInstructions { projects },
)?;
send_remote_projects_update(session.user_id(), update, &session).await;
send_dev_server_projects_update(session.user_id(), update, &session).await;
response.send(proto::CreateRemoteProjectResponse {
remote_project: Some(remote_project.to_proto(None)),
response.send(proto::CreateDevServerProjectResponse {
dev_server_project: Some(dev_server_project.to_proto(None)),
})?;
Ok(())
}
@@ -2308,13 +2345,19 @@ async fn create_dev_server(
let access_token = auth::random_token();
let hashed_access_token = auth::hash_access_token(&access_token);
if request.name.is_empty() {
return Err(proto::ErrorCode::Forbidden
.message("Dev server name cannot be empty".to_string())
.anyhow())?;
}
let (dev_server, status) = session
.db()
.await
.create_dev_server(&request.name, &hashed_access_token, session.user_id())
.await?;
send_remote_projects_update(session.user_id(), status, &session).await;
send_dev_server_projects_update(session.user_id(), status, &session).await;
response.send(proto::CreateDevServerResponse {
dev_server_id: dev_server.id.0 as u64,
@@ -2324,6 +2367,71 @@ async fn create_dev_server(
Ok(())
}
async fn regenerate_dev_server_token(
request: proto::RegenerateDevServerToken,
response: Response<proto::RegenerateDevServerToken>,
session: UserSession,
) -> Result<()> {
let dev_server_id = DevServerId(request.dev_server_id as i32);
let access_token = auth::random_token();
let hashed_access_token = auth::hash_access_token(&access_token);
let connection_id = session
.connection_pool()
.await
.dev_server_connection_id(dev_server_id);
if let Some(connection_id) = connection_id {
shutdown_dev_server_internal(dev_server_id, connection_id, &session).await?;
session
.peer
.send(connection_id, proto::ShutdownDevServer {})?;
let _ = remove_dev_server_connection(dev_server_id, &session).await;
}
let status = session
.db()
.await
.update_dev_server_token(dev_server_id, &hashed_access_token, session.user_id())
.await?;
send_dev_server_projects_update(session.user_id(), status, &session).await;
response.send(proto::RegenerateDevServerTokenResponse {
dev_server_id: dev_server_id.to_proto(),
access_token: auth::generate_dev_server_token(dev_server_id.0 as usize, access_token),
})?;
Ok(())
}
async fn rename_dev_server(
request: proto::RenameDevServer,
response: Response<proto::RenameDevServer>,
session: UserSession,
) -> Result<()> {
if request.name.trim().is_empty() {
return Err(proto::ErrorCode::Forbidden
.message("Dev server name cannot be empty".to_string())
.anyhow())?;
}
let dev_server_id = DevServerId(request.dev_server_id as i32);
let dev_server = session.db().await.get_dev_server(dev_server_id).await?;
if dev_server.user_id != session.user_id() {
return Err(anyhow!(ErrorCode::Forbidden))?;
}
let status = session
.db()
.await
.rename_dev_server(dev_server_id, &request.name, session.user_id())
.await?;
send_dev_server_projects_update(session.user_id(), status, &session).await;
response.send(proto::Ack {})?;
Ok(())
}
async fn delete_dev_server(
request: proto::DeleteDevServer,
response: Response<proto::DeleteDevServer>,
@@ -2344,6 +2452,7 @@ async fn delete_dev_server(
session
.peer
.send(connection_id, proto::ShutdownDevServer {})?;
let _ = remove_dev_server_connection(dev_server_id, &session).await;
}
let status = session
@@ -2352,20 +2461,82 @@ async fn delete_dev_server(
.delete_dev_server(dev_server_id, session.user_id())
.await?;
send_remote_projects_update(session.user_id(), status, &session).await;
send_dev_server_projects_update(session.user_id(), status, &session).await;
response.send(proto::Ack {})?;
Ok(())
}
async fn rejoin_remote_projects(
async fn delete_dev_server_project(
request: proto::DeleteDevServerProject,
response: Response<proto::DeleteDevServerProject>,
session: UserSession,
) -> Result<()> {
let dev_server_project_id = DevServerProjectId(request.dev_server_project_id as i32);
let dev_server_project = session
.db()
.await
.get_dev_server_project(dev_server_project_id)
.await?;
let dev_server = session
.db()
.await
.get_dev_server(dev_server_project.dev_server_id)
.await?;
if dev_server.user_id != session.user_id() {
return Err(anyhow!(ErrorCode::Forbidden))?;
}
let dev_server_connection_id = session
.connection_pool()
.await
.dev_server_connection_id(dev_server.id);
if let Some(dev_server_connection_id) = dev_server_connection_id {
let project = session
.db()
.await
.find_dev_server_project(dev_server_project_id)
.await;
if let Ok(project) = project {
unshare_project_internal(
project.id,
dev_server_connection_id,
Some(session.user_id()),
&session,
)
.await?;
}
}
let (projects, status) = session
.db()
.await
.delete_dev_server_project(dev_server_project_id, dev_server.id, session.user_id())
.await?;
if let Some(dev_server_connection_id) = dev_server_connection_id {
session.peer.send(
dev_server_connection_id,
proto::DevServerInstructions { projects },
)?;
}
send_dev_server_projects_update(session.user_id(), status, &session).await;
response.send(proto::Ack {})?;
Ok(())
}
async fn rejoin_dev_server_projects(
request: proto::RejoinRemoteProjects,
response: Response<proto::RejoinRemoteProjects>,
session: UserSession,
) -> Result<()> {
let mut rejoined_projects = {
let db = session.db().await;
db.rejoin_remote_projects(
db.rejoin_dev_server_projects(
&request.rejoined_projects,
session.user_id(),
session.0.connection_id,
@@ -2389,7 +2560,7 @@ async fn reconnect_dev_server(
) -> Result<()> {
let reshared_projects = {
let db = session.db().await;
db.reshare_remote_projects(
db.reshare_dev_server_projects(
&request.reshared_projects,
session.dev_server_id(),
session.0.connection_id,
@@ -2454,7 +2625,8 @@ async fn shutdown_dev_server(
session: DevServerSession,
) -> Result<()> {
response.send(proto::Ack {})?;
shutdown_dev_server_internal(session.dev_server_id(), session.connection_id, &session).await
shutdown_dev_server_internal(session.dev_server_id(), session.connection_id, &session).await?;
remove_dev_server_connection(session.dev_server_id(), &session).await
}
async fn shutdown_dev_server_internal(
@@ -2462,14 +2634,14 @@ async fn shutdown_dev_server_internal(
connection_id: ConnectionId,
session: &Session,
) -> Result<()> {
let (remote_projects, dev_server) = {
let (dev_server_projects, dev_server) = {
let db = session.db().await;
let remote_projects = db.get_remote_projects_for_dev_server(dev_server_id).await?;
let dev_server_projects = db.get_projects_for_dev_server(dev_server_id).await?;
let dev_server = db.get_dev_server(dev_server_id).await?;
(remote_projects, dev_server)
(dev_server_projects, dev_server)
};
for project_id in remote_projects.iter().filter_map(|p| p.project_id) {
for project_id in dev_server_projects.iter().filter_map(|p| p.project_id) {
unshare_project_internal(
ProjectId::from_proto(project_id),
connection_id,
@@ -2487,13 +2659,28 @@ async fn shutdown_dev_server_internal(
let status = session
.db()
.await
.remote_projects_update(dev_server.user_id)
.dev_server_projects_update(dev_server.user_id)
.await?;
send_remote_projects_update(dev_server.user_id, status, &session).await;
send_dev_server_projects_update(dev_server.user_id, status, &session).await;
Ok(())
}
async fn remove_dev_server_connection(dev_server_id: DevServerId, session: &Session) -> Result<()> {
let dev_server_connection = session
.connection_pool()
.await
.dev_server_connection_id(dev_server_id);
if let Some(dev_server_connection) = dev_server_connection {
session
.connection_pool()
.await
.remove_connection(dev_server_connection)?;
}
Ok(())
}
/// Updates other participants with changes to the project
async fn update_project(
request: proto::UpdateProject,
@@ -2677,6 +2864,7 @@ where
T: EntityMessage + RequestMessage,
{
let project_id = ProjectId::from_proto(request.remote_entity_id());
let host_connection_id = session
.db()
.await
@@ -2690,6 +2878,45 @@ where
Ok(())
}
/// forward a project request to the host. These requests are disallowed
/// for guests.
async fn forward_versioned_mutating_project_request<T>(
request: T,
response: Response<T>,
session: UserSession,
) -> Result<()>
where
T: EntityMessage + RequestMessage + VersionedMessage,
{
let project_id = ProjectId::from_proto(request.remote_entity_id());
let host_connection_id = session
.db()
.await
.host_for_mutating_project_request(project_id, session.connection_id, session.user_id())
.await?;
if let Some(host_version) = session
.connection_pool()
.await
.connection(host_connection_id)
.map(|c| c.zed_version)
{
if let Some(min_required_version) = request.required_host_version() {
if min_required_version > host_version {
return Err(anyhow!(ErrorCode::RemoteUpgradeRequired
.with_tag("required", &min_required_version.to_string())))?;
}
}
}
let payload = session
.peer
.forward_request(session.connection_id, host_connection_id, request)
.await?;
response.send(payload)?;
Ok(())
}
/// Notify other participants that a new buffer has been created
async fn create_buffer_for_peer(
request: proto::CreateBufferForPeer,
@@ -4102,7 +4329,7 @@ async fn complete_with_open_ai(
api_key: Arc<str>,
) -> Result<()> {
let mut completion_stream = open_ai::stream_completion(
&session.http_client,
session.http_client.as_ref(),
OPEN_AI_API_URL,
&api_key,
crate::ai::language_model_request_to_open_ai(request)?,
@@ -4166,7 +4393,7 @@ async fn complete_with_google_ai(
api_key: Arc<str>,
) -> Result<()> {
let mut stream = google_ai::stream_generate_content(
&session.http_client,
session.http_client.clone(),
google_ai::API_URL,
api_key.as_ref(),
crate::ai::language_model_request_to_google_ai(request)?,
@@ -4250,7 +4477,7 @@ async fn complete_with_anthropic(
.collect();
let mut stream = anthropic::stream_completion(
&session.http_client,
session.http_client.clone(),
"https://api.anthropic.com",
&api_key,
anthropic::Request {
@@ -4374,7 +4601,7 @@ async fn count_tokens_with_language_model(
let api_key = google_ai_api_key
.ok_or_else(|| anyhow!("no Google AI API key configured on the server"))?;
let tokens_response = google_ai::count_tokens(
&session.http_client,
session.http_client.as_ref(),
google_ai::API_URL,
&api_key,
crate::ai::count_tokens_request_to_google_ai(request)?,
@@ -4393,7 +4620,7 @@ impl RateLimit for ComputeEmbeddingsRateLimit {
std::env::var("EMBED_TEXTS_RATE_LIMIT_PER_HOUR")
.ok()
.and_then(|v| v.parse().ok())
.unwrap_or(120) // Picked arbitrarily
.unwrap_or(5000) // Picked arbitrarily
}
fn refill_duration() -> chrono::Duration {
@@ -4422,7 +4649,7 @@ async fn compute_embeddings(
let embeddings = match request.model.as_str() {
"openai/text-embedding-3-small" => {
open_ai::embed(
&session.http_client,
session.http_client.as_ref(),
OPEN_AI_API_URL,
&api_key,
OpenAiEmbeddingModel::TextEmbedding3Small,
@@ -4465,25 +4692,6 @@ async fn compute_embeddings(
Ok(())
}
struct GetCachedEmbeddingsRateLimit;
impl RateLimit for GetCachedEmbeddingsRateLimit {
fn capacity() -> usize {
std::env::var("EMBED_TEXTS_RATE_LIMIT_PER_HOUR")
.ok()
.and_then(|v| v.parse().ok())
.unwrap_or(120) // Picked arbitrarily
}
fn refill_duration() -> chrono::Duration {
chrono::Duration::hours(1)
}
fn db_name() -> &'static str {
"get-cached-embeddings"
}
}
async fn get_cached_embeddings(
request: proto::GetCachedEmbeddings,
response: Response<proto::GetCachedEmbeddings>,
@@ -4491,11 +4699,6 @@ async fn get_cached_embeddings(
) -> Result<()> {
authorize_access_to_language_models(&session).await?;
session
.rate_limiter
.check::<GetCachedEmbeddingsRateLimit>(session.user_id())
.await?;
let db = session.db().await;
let embeddings = db.get_embeddings(&request.model, &request.digests).await?;
@@ -4518,6 +4721,37 @@ async fn authorize_access_to_language_models(session: &UserSession) -> Result<()
}
}
/// Get a Supermaven API key for the user
async fn get_supermaven_api_key(
_request: proto::GetSupermavenApiKey,
response: Response<proto::GetSupermavenApiKey>,
session: UserSession,
) -> Result<()> {
let user_id: String = session.user_id().to_string();
if !session.is_staff() {
return Err(anyhow!("supermaven not enabled for this account"))?;
}
let email = session
.email()
.ok_or_else(|| anyhow!("user must have an email"))?;
let supermaven_admin_api = session
.supermaven_client
.as_ref()
.ok_or_else(|| anyhow!("supermaven not configured"))?;
let result = supermaven_admin_api
.try_get_or_create_user(CreateExternalUserRequest { id: user_id, email })
.await?;
response.send(proto::GetSupermavenApiKeyResponse {
api_key: result.api_key,
})?;
Ok(())
}
/// Start receiving chat updates for a channel
async fn join_channel_chat(
request: proto::JoinChannelChat,
@@ -4863,9 +5097,9 @@ fn channel_updated(
);
}
async fn send_remote_projects_update(
async fn send_dev_server_projects_update(
user_id: UserId,
mut status: proto::RemoteProjectsUpdate,
mut status: proto::DevServerProjectsUpdate,
session: &Session,
) {
let pool = session.connection_pool().await;
@@ -4928,9 +5162,13 @@ async fn lost_dev_server_connection(session: &DevServerSession) -> Result<()> {
}
let user_id = session.dev_server().user_id;
let update = session.db().await.remote_projects_update(user_id).await?;
let update = session
.db()
.await
.dev_server_projects_update(user_id)
.await?;
send_remote_projects_update(user_id, update, session).await;
send_dev_server_projects_update(user_id, update, session).await;
Ok(())
}

View File

@@ -21,7 +21,7 @@ struct ConnectedPrincipal {
connection_ids: HashSet<ConnectionId>,
}
#[derive(Debug, Serialize)]
#[derive(Copy, Clone, Debug, Serialize, PartialOrd, PartialEq, Eq, Ord)]
pub struct ZedVersion(pub SemanticVersion);
impl fmt::Display for ZedVersion {
@@ -34,6 +34,32 @@ impl ZedVersion {
pub fn can_collaborate(&self) -> bool {
self.0 >= SemanticVersion::new(0, 129, 2)
}
pub fn with_save_as() -> ZedVersion {
ZedVersion(SemanticVersion::new(0, 134, 0))
}
}
pub trait VersionedMessage {
fn required_host_version(&self) -> Option<ZedVersion> {
None
}
}
impl VersionedMessage for proto::SaveBuffer {
fn required_host_version(&self) -> Option<ZedVersion> {
if self.new_path.is_some() {
Some(ZedVersion::with_save_as())
} else {
None
}
}
}
impl VersionedMessage for proto::OpenNewBuffer {
fn required_host_version(&self) -> Option<ZedVersion> {
Some(ZedVersion::with_save_as())
}
}
#[derive(Serialize)]
@@ -50,6 +76,10 @@ impl ConnectionPool {
self.channels.clear();
}
pub fn connection(&mut self, connection_id: ConnectionId) -> Option<&Connection> {
self.connections.get(&connection_id)
}
#[instrument(skip(self))]
pub fn add_connection(
&mut self,

View File

@@ -246,7 +246,7 @@ async fn test_channel_notes_participant_indices(
.update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
.await
.unwrap();
let project_b = client_b.build_remote_project(project_id, cx_b).await;
let project_b = client_b.build_dev_server_project(project_id, cx_b).await;
let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
// Clients A and B open the same file.

View File

@@ -16,7 +16,7 @@ use super::TestClient;
async fn test_dev_server(cx: &mut gpui::TestAppContext, cx2: &mut gpui::TestAppContext) {
let (server, client) = TestServer::start1(cx).await;
let store = cx.update(|cx| remote_projects::Store::global(cx).clone());
let store = cx.update(|cx| dev_server_projects::Store::global(cx).clone());
let resp = store
.update(cx, |store, cx| {
@@ -51,7 +51,7 @@ async fn test_dev_server(cx: &mut gpui::TestAppContext, cx2: &mut gpui::TestAppC
store
.update(cx, |store, cx| {
store.create_remote_project(
store.create_dev_server_project(
client::DevServerId(resp.dev_server_id),
"/remote".to_string(),
cx,
@@ -64,10 +64,10 @@ async fn test_dev_server(cx: &mut gpui::TestAppContext, cx2: &mut gpui::TestAppC
let remote_workspace = store
.update(cx, |store, cx| {
let projects = store.remote_projects();
let projects = store.dev_server_projects();
assert_eq!(projects.len(), 1);
assert_eq!(projects[0].path, "/remote");
workspace::join_remote_project(
workspace::join_dev_server_project(
projects[0].project_id.unwrap(),
client.app_state.clone(),
None,
@@ -110,7 +110,7 @@ async fn test_dev_server_env_files(
let (server, client1, client2, channel_id) = TestServer::start2(cx1, cx2).await;
let (_dev_server, remote_workspace) =
create_remote_project(&server, client1.app_state.clone(), cx1, cx3).await;
create_dev_server_project(&server, client1.app_state.clone(), cx1, cx3).await;
cx1.executor().run_until_parked();
@@ -157,13 +157,13 @@ async fn test_dev_server_env_files(
});
}
async fn create_remote_project(
async fn create_dev_server_project(
server: &TestServer,
client_app_state: Arc<AppState>,
cx: &mut TestAppContext,
cx_devserver: &mut TestAppContext,
) -> (TestClient, WindowHandle<Workspace>) {
let store = cx.update(|cx| remote_projects::Store::global(cx).clone());
let store = cx.update(|cx| dev_server_projects::Store::global(cx).clone());
let resp = store
.update(cx, |store, cx| {
@@ -190,7 +190,7 @@ async fn create_remote_project(
store
.update(cx, |store, cx| {
store.create_remote_project(
store.create_dev_server_project(
client::DevServerId(resp.dev_server_id),
"/remote".to_string(),
cx,
@@ -203,10 +203,10 @@ async fn create_remote_project(
let workspace = store
.update(cx, |store, cx| {
let projects = store.remote_projects();
let projects = store.dev_server_projects();
assert_eq!(projects.len(), 1);
assert_eq!(projects[0].path, "/remote");
workspace::join_remote_project(
workspace::join_dev_server_project(
projects[0].project_id.unwrap(),
client_app_state,
None,
@@ -230,7 +230,7 @@ async fn test_dev_server_leave_room(
let (server, client1, client2, channel_id) = TestServer::start2(cx1, cx2).await;
let (_dev_server, remote_workspace) =
create_remote_project(&server, client1.app_state.clone(), cx1, cx3).await;
create_dev_server_project(&server, client1.app_state.clone(), cx1, cx3).await;
cx1.update(|cx| {
workspace::join_channel(
@@ -263,6 +263,191 @@ async fn test_dev_server_leave_room(
cx2.update(|cx| assert!(workspace.read(cx).project().read(cx).is_disconnected()));
}
#[gpui::test]
async fn test_dev_server_delete(
cx1: &mut gpui::TestAppContext,
cx2: &mut gpui::TestAppContext,
cx3: &mut gpui::TestAppContext,
) {
let (server, client1, client2, channel_id) = TestServer::start2(cx1, cx2).await;
let (_dev_server, remote_workspace) =
create_dev_server_project(&server, client1.app_state.clone(), cx1, cx3).await;
cx1.update(|cx| {
workspace::join_channel(
channel_id,
client1.app_state.clone(),
Some(remote_workspace),
cx,
)
})
.await
.unwrap();
cx1.executor().run_until_parked();
remote_workspace
.update(cx1, |ws, cx| {
assert!(ws.project().read(cx).is_shared());
})
.unwrap();
join_channel(channel_id, &client2, cx2).await.unwrap();
cx2.executor().run_until_parked();
cx1.update(|cx| {
dev_server_projects::Store::global(cx).update(cx, |store, cx| {
store.delete_dev_server_project(store.dev_server_projects().first().unwrap().id, cx)
})
})
.await
.unwrap();
cx1.executor().run_until_parked();
let (workspace, cx2) = client2.active_workspace(cx2);
cx2.update(|cx| assert!(workspace.read(cx).project().read(cx).is_disconnected()));
cx1.update(|cx| {
dev_server_projects::Store::global(cx).update(cx, |store, _| {
assert_eq!(store.dev_server_projects().len(), 0);
})
})
}
#[gpui::test]
async fn test_dev_server_rename(
cx1: &mut gpui::TestAppContext,
cx2: &mut gpui::TestAppContext,
cx3: &mut gpui::TestAppContext,
) {
let (server, client1, client2, channel_id) = TestServer::start2(cx1, cx2).await;
let (_dev_server, remote_workspace) =
create_dev_server_project(&server, client1.app_state.clone(), cx1, cx3).await;
cx1.update(|cx| {
workspace::join_channel(
channel_id,
client1.app_state.clone(),
Some(remote_workspace),
cx,
)
})
.await
.unwrap();
cx1.executor().run_until_parked();
remote_workspace
.update(cx1, |ws, cx| {
assert!(ws.project().read(cx).is_shared());
})
.unwrap();
join_channel(channel_id, &client2, cx2).await.unwrap();
cx2.executor().run_until_parked();
cx1.update(|cx| {
dev_server_projects::Store::global(cx).update(cx, |store, cx| {
store.rename_dev_server(
store.dev_servers().first().unwrap().id,
"name-edited".to_string(),
cx,
)
})
})
.await
.unwrap();
cx1.executor().run_until_parked();
cx1.update(|cx| {
dev_server_projects::Store::global(cx).update(cx, |store, _| {
assert_eq!(store.dev_servers().first().unwrap().name, "name-edited");
})
})
}
#[gpui::test]
async fn test_dev_server_refresh_access_token(
cx1: &mut gpui::TestAppContext,
cx2: &mut gpui::TestAppContext,
cx3: &mut gpui::TestAppContext,
cx4: &mut gpui::TestAppContext,
) {
let (server, client1, client2, channel_id) = TestServer::start2(cx1, cx2).await;
let (_dev_server, remote_workspace) =
create_dev_server_project(&server, client1.app_state.clone(), cx1, cx3).await;
cx1.update(|cx| {
workspace::join_channel(
channel_id,
client1.app_state.clone(),
Some(remote_workspace),
cx,
)
})
.await
.unwrap();
cx1.executor().run_until_parked();
remote_workspace
.update(cx1, |ws, cx| {
assert!(ws.project().read(cx).is_shared());
})
.unwrap();
join_channel(channel_id, &client2, cx2).await.unwrap();
cx2.executor().run_until_parked();
// Regenerate the access token
let new_token_response = cx1
.update(|cx| {
dev_server_projects::Store::global(cx).update(cx, |store, cx| {
store.regenerate_dev_server_token(store.dev_servers().first().unwrap().id, cx)
})
})
.await
.unwrap();
cx1.executor().run_until_parked();
// 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()));
// 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());
dev_server_projects::Store::global(cx).update(cx, |store, _| {
assert_eq!(
store.dev_servers().first().unwrap().status,
DevServerStatus::Offline
);
})
});
// Reconnect the dev server with the new token
let _dev_server = server
.create_dev_server(new_token_response.access_token, cx4)
.await;
cx1.executor().run_until_parked();
// Assert that the dev server is online again
cx1.update(|cx| {
dev_server_projects::Store::global(cx).update(cx, |store, _| {
assert_eq!(store.dev_servers().len(), 1);
assert_eq!(
store.dev_servers().first().unwrap().status,
DevServerStatus::Online
);
})
});
}
#[gpui::test]
async fn test_dev_server_reconnect(
cx1: &mut gpui::TestAppContext,
@@ -275,7 +460,7 @@ async fn test_dev_server_reconnect(
.await;
let (_dev_server, remote_workspace) =
create_remote_project(&server, client1.app_state.clone(), cx1, cx3).await;
create_dev_server_project(&server, client1.app_state.clone(), cx1, cx3).await;
cx1.update(|cx| {
workspace::join_channel(
@@ -299,12 +484,12 @@ async fn test_dev_server_reconnect(
let client2 = server.create_client(cx2, "user_a").await;
let store = cx2.update(|cx| remote_projects::Store::global(cx).clone());
let store = cx2.update(|cx| dev_server_projects::Store::global(cx).clone());
store
.update(cx2, |store, cx| {
let projects = store.remote_projects();
workspace::join_remote_project(
let projects = store.dev_server_projects();
workspace::join_dev_server_project(
projects[0].project_id.unwrap(),
client2.app_state.clone(),
None,
@@ -316,7 +501,7 @@ async fn test_dev_server_reconnect(
}
#[gpui::test]
async fn test_create_remote_project_path_validation(
async fn test_create_dev_server_project_path_validation(
cx1: &mut gpui::TestAppContext,
cx2: &mut gpui::TestAppContext,
cx3: &mut gpui::TestAppContext,
@@ -328,11 +513,11 @@ async fn test_create_remote_project_path_validation(
// Creating a project with a path that does exist should not fail
let (_dev_server, _) =
create_remote_project(&server, client1.app_state.clone(), cx1, cx2).await;
create_dev_server_project(&server, client1.app_state.clone(), cx1, cx2).await;
cx1.executor().run_until_parked();
let store = cx1.update(|cx| remote_projects::Store::global(cx).clone());
let store = cx1.update(|cx| dev_server_projects::Store::global(cx).clone());
let resp = store
.update(cx1, |store, cx| {
@@ -350,7 +535,7 @@ async fn test_create_remote_project_path_validation(
// Creating a remote project with a path that does not exist should fail
let result = store
.update(cx1, |store, cx| {
store.create_remote_project(
store.create_dev_server_project(
client::DevServerId(resp.dev_server_id),
"/notfound".to_string(),
cx,
@@ -363,7 +548,7 @@ async fn test_create_remote_project_path_validation(
let error = result.unwrap_err();
assert!(matches!(
error.error_code(),
ErrorCode::RemoteProjectPathDoesNotExist
ErrorCode::DevServerProjectPathDoesNotExist
));
}
@@ -373,7 +558,7 @@ async fn test_save_as_remote(cx1: &mut gpui::TestAppContext, cx2: &mut gpui::Tes
// Creating a project with a path that does exist should not fail
let (dev_server, remote_workspace) =
create_remote_project(&server, client1.app_state.clone(), cx1, cx2).await;
create_dev_server_project(&server, client1.app_state.clone(), cx1, cx2).await;
let mut cx = VisualTestContext::from_window(remote_workspace.into(), cx1);
@@ -398,3 +583,33 @@ async fn test_save_as_remote(cx1: &mut gpui::TestAppContext, cx2: &mut gpui::Tes
"remote\nremote\nremote"
);
}
#[gpui::test]
async fn test_new_file_remote(cx1: &mut gpui::TestAppContext, cx2: &mut gpui::TestAppContext) {
let (server, client1) = TestServer::start1(cx1).await;
// Creating a project with a path that does exist should not fail
let (dev_server, remote_workspace) =
create_dev_server_project(&server, client1.app_state.clone(), cx1, cx2).await;
let mut cx = VisualTestContext::from_window(remote_workspace.into(), cx1);
cx.simulate_keystrokes("cmd-n");
cx.simulate_input("new!");
cx.simulate_keystrokes("cmd-shift-s");
cx.simulate_input("2.txt");
cx.simulate_keystrokes("enter");
cx.executor().run_until_parked();
let title = remote_workspace
.update(&mut cx, |ws, cx| {
ws.active_item(cx).unwrap().tab_description(0, &cx).unwrap()
})
.unwrap();
assert_eq!(title, "2.txt");
let path = Path::new("/remote/2.txt");
assert_eq!(dev_server.fs().load(&path).await.unwrap(), "new!");
}

View File

@@ -6,13 +6,18 @@ use call::ActiveCall;
use collections::HashMap;
use editor::{
actions::{
ConfirmCodeAction, ConfirmCompletion, ConfirmRename, Redo, Rename, RevertSelectedHunks,
ToggleCodeActions, Undo,
ConfirmCodeAction, ConfirmCompletion, ConfirmRename, ContextMenuFirst, Redo, Rename,
RevertSelectedHunks, ToggleCodeActions, Undo,
},
test::{
editor_hunks,
editor_test_context::{AssertionContextManager, EditorTestContext},
expanded_hunks, expanded_hunks_background_highlights,
},
test::editor_test_context::{AssertionContextManager, EditorTestContext},
Editor,
};
use futures::StreamExt;
use git::diff::DiffHunkStatus;
use gpui::{BorrowAppContext, TestAppContext, VisualContext, VisualTestContext};
use indoc::indoc;
use language::{
@@ -73,7 +78,7 @@ async fn test_host_disconnect(
.await
.unwrap();
let project_b = client_b.build_remote_project(project_id, cx_b).await;
let project_b = client_b.build_dev_server_project(project_id, cx_b).await;
cx_a.background_executor.run_until_parked();
assert!(worktree_a.read_with(cx_a, |tree, _| tree.as_local().unwrap().is_shared()));
@@ -194,7 +199,7 @@ async fn test_newline_above_or_below_does_not_move_guest_cursor(
.await
.unwrap();
let project_b = client_b.build_remote_project(project_id, cx_b).await;
let project_b = client_b.build_dev_server_project(project_id, cx_b).await;
// Open a buffer as client A
let buffer_a = project_a
@@ -310,7 +315,7 @@ async fn test_collaborating_with_completion(cx_a: &mut TestAppContext, cx_b: &mu
.update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
.await
.unwrap();
let project_b = client_b.build_remote_project(project_id, cx_b).await;
let project_b = client_b.build_dev_server_project(project_id, cx_b).await;
// Open a file in an editor as the guest.
let buffer_b = project_b
@@ -439,6 +444,93 @@ async fn test_collaborating_with_completion(cx_a: &mut TestAppContext, cx_b: &mu
"use d::SomeTrait;\nfn main() { a.first_method() }"
);
});
// Now we do a second completion, this time to ensure that documentation/snippets are
// resolved
editor_b.update(cx_b, |editor, cx| {
editor.change_selections(None, cx, |s| s.select_ranges([46..46]));
editor.handle_input("; a", cx);
editor.handle_input(".", cx);
});
buffer_b.read_with(cx_b, |buffer, _| {
assert_eq!(
buffer.text(),
"use d::SomeTrait;\nfn main() { a.first_method(); a. }"
);
});
let mut completion_response = fake_language_server
.handle_request::<lsp::request::Completion, _, _>(|params, _| async move {
assert_eq!(
params.text_document_position.text_document.uri,
lsp::Url::from_file_path("/a/main.rs").unwrap(),
);
assert_eq!(
params.text_document_position.position,
lsp::Position::new(1, 32),
);
Ok(Some(lsp::CompletionResponse::Array(vec![
lsp::CompletionItem {
label: "third_method(…)".into(),
detail: Some("fn(&mut self, B, C, D) -> E".into()),
text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
// no snippet placehodlers
new_text: "third_method".to_string(),
range: lsp::Range::new(
lsp::Position::new(1, 32),
lsp::Position::new(1, 32),
),
})),
insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
documentation: None,
..Default::default()
},
])))
});
// The completion now gets a new `text_edit.new_text` when resolving the completion item
let mut resolve_completion_response = fake_language_server
.handle_request::<lsp::request::ResolveCompletionItem, _, _>(|params, _| async move {
assert_eq!(params.label, "third_method(…)");
Ok(lsp::CompletionItem {
label: "third_method(…)".into(),
detail: Some("fn(&mut self, B, C, D) -> E".into()),
text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
// Now it's a snippet
new_text: "third_method($1, $2, $3)".to_string(),
range: lsp::Range::new(lsp::Position::new(1, 32), lsp::Position::new(1, 32)),
})),
insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
documentation: Some(lsp::Documentation::String(
"this is the documentation".into(),
)),
..Default::default()
})
});
cx_b.executor().run_until_parked();
completion_response.next().await.unwrap();
editor_b.update(cx_b, |editor, cx| {
assert!(editor.context_menu_visible());
editor.context_menu_first(&ContextMenuFirst {}, cx);
});
resolve_completion_response.next().await.unwrap();
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);
assert_eq!(
editor.text(cx),
"use d::SomeTrait;\nfn main() { a.first_method(); a.third_method(, , ) }"
);
});
}
#[gpui::test(iterations = 10)]
@@ -480,7 +572,7 @@ async fn test_collaborating_with_code_actions(
.unwrap();
// Join the project as client B.
let project_b = client_b.build_remote_project(project_id, cx_b).await;
let project_b = client_b.build_dev_server_project(project_id, cx_b).await;
let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
let editor_b = workspace_b
.update(cx_b, |workspace, cx| {
@@ -575,7 +667,7 @@ async fn test_collaborating_with_code_actions(
editor_b.update(cx_b, |editor, cx| {
editor.toggle_code_actions(
&ToggleCodeActions {
deployed_from_indicator: false,
deployed_from_indicator: None,
},
cx,
);
@@ -695,7 +787,7 @@ async fn test_collaborating_with_renames(cx_a: &mut TestAppContext, cx_b: &mut T
.update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
.await
.unwrap();
let project_b = client_b.build_remote_project(project_id, cx_b).await;
let project_b = client_b.build_dev_server_project(project_id, cx_b).await;
let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
let editor_b = workspace_b
@@ -944,7 +1036,7 @@ async fn test_language_server_statuses(cx_a: &mut TestAppContext, cx_b: &mut Tes
.await
.unwrap();
executor.run_until_parked();
let project_b = client_b.build_remote_project(project_id, cx_b).await;
let project_b = client_b.build_dev_server_project(project_id, cx_b).await;
project_b.read_with(cx_b, |project, _| {
let status = project.language_server_statuses().next().unwrap();
@@ -1041,7 +1133,7 @@ async fn test_share_project(
.unwrap();
let client_b_peer_id = client_b.peer_id().unwrap();
let project_b = client_b
.build_remote_project(initial_project.id, cx_b)
.build_dev_server_project(initial_project.id, cx_b)
.await;
let replica_id_b = project_b.read_with(cx_b, |project, _| project.replica_id());
@@ -1145,7 +1237,7 @@ async fn test_share_project(
.await
.unwrap();
let _project_c = client_c
.build_remote_project(initial_project.id, cx_c)
.build_dev_server_project(initial_project.id, cx_c)
.await;
// Client B closes the editor, and client A sees client B's selections removed.
@@ -1205,7 +1297,7 @@ async fn test_on_input_format_from_host_to_guest(
.update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
.await
.unwrap();
let project_b = client_b.build_remote_project(project_id, cx_b).await;
let project_b = client_b.build_dev_server_project(project_id, cx_b).await;
// Open a file in an editor as the host.
let buffer_a = project_a
@@ -1325,7 +1417,7 @@ async fn test_on_input_format_from_guest_to_host(
.update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
.await
.unwrap();
let project_b = client_b.build_remote_project(project_id, cx_b).await;
let project_b = client_b.build_dev_server_project(project_id, cx_b).await;
// Open a file in an editor as the guest.
let buffer_b = project_b
@@ -1486,7 +1578,7 @@ async fn test_mutual_editor_inlay_hint_cache_update(
.unwrap();
// Client B joins the project
let project_b = client_b.build_remote_project(project_id, cx_b).await;
let project_b = client_b.build_dev_server_project(project_id, cx_b).await;
active_call_b
.update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
.await
@@ -1746,7 +1838,7 @@ async fn test_inlay_hint_refresh_is_forwarded(
.await
.unwrap();
let project_b = client_b.build_remote_project(project_id, cx_b).await;
let project_b = client_b.build_dev_server_project(project_id, cx_b).await;
active_call_b
.update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
.await
@@ -1875,7 +1967,7 @@ async fn test_inlay_hint_refresh_is_forwarded(
}
#[gpui::test]
async fn test_multiple_types_reverts(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
async fn test_multiple_hunk_types_revert(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
let mut server = TestServer::start(cx_a.executor()).await;
let client_a = server.create_client(cx_a, "user_a").await;
let client_b = server.create_client(cx_b, "user_b").await;
@@ -1922,7 +2014,7 @@ struct Row10;"#};
.await
.unwrap();
let project_b = client_b.build_remote_project(project_id, cx_b).await;
let project_b = client_b.build_dev_server_project(project_id, cx_b).await;
active_call_b
.update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
.await
@@ -1981,7 +2073,7 @@ struct Row10;"#};
.as_singleton()
.unwrap()
.update(cx, |buffer, cx| {
buffer.set_diff_base(Some(base_text.to_string()), cx);
buffer.set_diff_base(Some(base_text.into()), cx);
});
});
editor_cx_b.update_editor(|editor, cx| {
@@ -1991,14 +2083,14 @@ struct Row10;"#};
.as_singleton()
.unwrap()
.update(cx, |buffer, cx| {
buffer.set_diff_base(Some(base_text.to_string()), cx);
buffer.set_diff_base(Some(base_text.into()), cx);
});
});
cx_a.executor().run_until_parked();
cx_b.executor().run_until_parked();
// client, selects a range in the updated buffer, and reverts it
// both host and the client observe the reverted state (with one hunk left, not covered by client's selection)
// the client selects a range in the updated buffer, expands it to see the diff for each hunk in the selection
// the host does not see the diffs toggled
editor_cx_b.set_selections_state(indoc! {r#"«ˇstruct Row;
struct Row0.1;
struct Row0.2;
@@ -2010,11 +2102,100 @@ struct Row10;"#};
struct R»ow9;
struct Row1220;"#});
editor_cx_b
.update_editor(|editor, cx| editor.toggle_hunk_diff(&editor::actions::ToggleHunkDiff, cx));
cx_a.executor().run_until_parked();
cx_b.executor().run_until_parked();
editor_cx_a.update_editor(|editor, cx| {
let snapshot = editor.snapshot(cx);
let all_hunks = editor_hunks(editor, &snapshot, cx);
let all_expanded_hunks = expanded_hunks(&editor, &snapshot, cx);
assert_eq!(expanded_hunks_background_highlights(editor, cx), Vec::new());
assert_eq!(
all_hunks,
vec![
("".to_string(), DiffHunkStatus::Added, 1..3),
("struct Row2;\n".to_string(), DiffHunkStatus::Removed, 4..4),
("struct Row5;\n".to_string(), DiffHunkStatus::Modified, 6..7),
("struct Row8;\n".to_string(), DiffHunkStatus::Removed, 9..9),
(
"struct Row10;".to_string(),
DiffHunkStatus::Modified,
10..10,
),
]
);
assert_eq!(all_expanded_hunks, Vec::new());
});
editor_cx_b.update_editor(|editor, cx| {
let snapshot = editor.snapshot(cx);
let all_hunks = editor_hunks(editor, &snapshot, cx);
let all_expanded_hunks = expanded_hunks(&editor, &snapshot, cx);
assert_eq!(
expanded_hunks_background_highlights(editor, cx),
vec![1..=2, 8..=8],
);
assert_eq!(
all_hunks,
vec![
("".to_string(), DiffHunkStatus::Added, 1..3),
("struct Row2;\n".to_string(), DiffHunkStatus::Removed, 5..5),
("struct Row5;\n".to_string(), DiffHunkStatus::Modified, 8..9),
(
"struct Row8;\n".to_string(),
DiffHunkStatus::Removed,
12..12
),
(
"struct Row10;".to_string(),
DiffHunkStatus::Modified,
13..13,
),
]
);
assert_eq!(all_expanded_hunks, &all_hunks[..all_hunks.len() - 1]);
});
// the client reverts the hunks, removing the expanded diffs too
// both host and the client observe the reverted state (with one hunk left, not covered by client's selection)
editor_cx_b.update_editor(|editor, cx| {
editor.revert_selected_hunks(&RevertSelectedHunks, cx);
});
cx_a.executor().run_until_parked();
cx_b.executor().run_until_parked();
editor_cx_a.update_editor(|editor, cx| {
let snapshot = editor.snapshot(cx);
let all_hunks = editor_hunks(editor, &snapshot, cx);
let all_expanded_hunks = expanded_hunks(&editor, &snapshot, cx);
assert_eq!(expanded_hunks_background_highlights(editor, cx), Vec::new());
assert_eq!(
all_hunks,
vec![(
"struct Row10;".to_string(),
DiffHunkStatus::Modified,
10..10,
)]
);
assert_eq!(all_expanded_hunks, Vec::new());
});
editor_cx_b.update_editor(|editor, cx| {
let snapshot = editor.snapshot(cx);
let all_hunks = editor_hunks(editor, &snapshot, cx);
let all_expanded_hunks = expanded_hunks(&editor, &snapshot, cx);
assert_eq!(
expanded_hunks_background_highlights(editor, cx),
vec![5..=5]
);
assert_eq!(
all_hunks,
vec![(
"struct Row10;".to_string(),
DiffHunkStatus::Modified,
10..10,
)]
);
assert_eq!(all_expanded_hunks, Vec::new());
});
editor_cx_a.assert_editor_state(indoc! {r#"struct Row;
struct Row1;
struct Row2;
@@ -2125,7 +2306,7 @@ async fn test_git_blame_is_forwarded(cx_a: &mut TestAppContext, cx_b: &mut TestA
.unwrap();
// Join the project as client B.
let project_b = client_b.build_remote_project(project_id, cx_b).await;
let project_b = client_b.build_dev_server_project(project_id, cx_b).await;
let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
let editor_b = workspace_b
.update(cx_b, |workspace, cx| {

View File

@@ -73,7 +73,7 @@ async fn test_basic_following(
.update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
.await
.unwrap();
let project_b = client_b.build_remote_project(project_id, cx_b).await;
let project_b = client_b.build_dev_server_project(project_id, cx_b).await;
active_call_b
.update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
.await
@@ -161,7 +161,7 @@ async fn test_basic_following(
executor.run_until_parked();
let active_call_c = cx_c.read(ActiveCall::global);
let project_c = client_c.build_remote_project(project_id, cx_c).await;
let project_c = client_c.build_dev_server_project(project_id, cx_c).await;
let (workspace_c, cx_c) = client_c.build_workspace(&project_c, cx_c);
active_call_c
.update(cx_c, |call, cx| call.set_location(Some(&project_c), cx))
@@ -174,7 +174,7 @@ async fn test_basic_following(
cx_d.executor().run_until_parked();
let active_call_d = cx_d.read(ActiveCall::global);
let project_d = client_d.build_remote_project(project_id, cx_d).await;
let project_d = client_d.build_dev_server_project(project_id, cx_d).await;
let (workspace_d, cx_d) = client_d.build_workspace(&project_d, cx_d);
active_call_d
.update(cx_d, |call, cx| call.set_location(Some(&project_d), cx))
@@ -567,7 +567,7 @@ async fn test_following_tab_order(
.update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
.await
.unwrap();
let project_b = client_b.build_remote_project(project_id, cx_b).await;
let project_b = client_b.build_dev_server_project(project_id, cx_b).await;
active_call_b
.update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
.await
@@ -684,7 +684,7 @@ async fn test_peers_following_each_other(cx_a: &mut TestAppContext, cx_b: &mut T
.unwrap();
// Client B joins the project.
let project_b = client_b.build_remote_project(project_id, cx_b).await;
let project_b = client_b.build_dev_server_project(project_id, cx_b).await;
active_call_b
.update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
.await
@@ -1197,7 +1197,7 @@ async fn test_auto_unfollowing(cx_a: &mut TestAppContext, cx_b: &mut TestAppCont
.update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
.await
.unwrap();
let project_b = client_b.build_remote_project(project_id, cx_b).await;
let project_b = client_b.build_dev_server_project(project_id, cx_b).await;
active_call_b
.update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
.await
@@ -1333,7 +1333,7 @@ async fn test_peers_simultaneously_following_each_other(
.await
.unwrap();
let project_b = client_b.build_remote_project(project_id, cx_b).await;
let project_b = client_b.build_dev_server_project(project_id, cx_b).await;
let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
executor.run_until_parked();
@@ -1683,7 +1683,7 @@ async fn test_following_into_excluded_file(
.update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
.await
.unwrap();
let project_b = client_b.build_remote_project(project_id, cx_b).await;
let project_b = client_b.build_dev_server_project(project_id, cx_b).await;
active_call_b
.update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
.await

View File

@@ -1375,7 +1375,7 @@ async fn test_unshare_project(
.unwrap();
let worktree_a = project_a.read_with(cx_a, |project, _| project.worktrees().next().unwrap());
let project_b = client_b.build_remote_project(project_id, cx_b).await;
let project_b = client_b.build_dev_server_project(project_id, cx_b).await;
executor.run_until_parked();
assert!(worktree_a.read_with(cx_a, |tree, _| tree.as_local().unwrap().is_shared()));
@@ -1395,7 +1395,7 @@ async fn test_unshare_project(
assert!(project_b.read_with(cx_b, |project, _| project.is_disconnected()));
// Client C opens the project.
let project_c = client_c.build_remote_project(project_id, cx_c).await;
let project_c = client_c.build_dev_server_project(project_id, cx_c).await;
// When client A unshares the project, client C's project becomes read-only.
project_a
@@ -1412,7 +1412,7 @@ async fn test_unshare_project(
.update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
.await
.unwrap();
let project_c2 = client_c.build_remote_project(project_id, cx_c).await;
let project_c2 = client_c.build_dev_server_project(project_id, cx_c).await;
executor.run_until_parked();
assert!(worktree_a.read_with(cx_a, |tree, _| tree.as_local().unwrap().is_shared()));
@@ -1516,9 +1516,9 @@ async fn test_project_reconnect(
.await
.unwrap();
let project_b1 = client_b.build_remote_project(project1_id, cx_b).await;
let project_b2 = client_b.build_remote_project(project2_id, cx_b).await;
let project_b3 = client_b.build_remote_project(project3_id, cx_b).await;
let project_b1 = client_b.build_dev_server_project(project1_id, cx_b).await;
let project_b2 = client_b.build_dev_server_project(project2_id, cx_b).await;
let project_b3 = client_b.build_dev_server_project(project3_id, cx_b).await;
executor.run_until_parked();
let worktree1_id = worktree_a1.read_with(cx_a, |worktree, _| {
@@ -2314,8 +2314,8 @@ async fn test_propagate_saves_and_fs_changes(
.unwrap();
// Join that worktree as clients B and C.
let project_b = client_b.build_remote_project(project_id, cx_b).await;
let project_c = client_c.build_remote_project(project_id, cx_c).await;
let project_b = client_b.build_dev_server_project(project_id, cx_b).await;
let project_c = client_c.build_dev_server_project(project_id, cx_c).await;
let worktree_b = project_b.read_with(cx_b, |p, _| p.worktrees().next().unwrap());
@@ -2450,7 +2450,8 @@ async fn test_propagate_saves_and_fs_changes(
});
let new_buffer_a = project_a
.update(cx_a, |p, cx| p.create_buffer("", None, cx))
.update(cx_a, |p, cx| p.create_buffer(cx))
.await
.unwrap();
let new_buffer_id = new_buffer_a.read_with(cx_a, |buffer, _| buffer.remote_id());
@@ -2538,7 +2539,7 @@ async fn test_git_diff_base_change(
.await
.unwrap();
let project_remote = client_b.build_remote_project(project_id, cx_b).await;
let project_remote = client_b.build_dev_server_project(project_id, cx_b).await;
let diff_base = "
one
@@ -2569,7 +2570,10 @@ async fn test_git_diff_base_change(
// Smoke test diffing
buffer_local_a.read_with(cx_a, |buffer, _| {
assert_eq!(buffer.diff_base(), Some(diff_base.as_ref()));
assert_eq!(
buffer.diff_base().map(|rope| rope.to_string()).as_deref(),
Some(diff_base.as_str())
);
git::diff::assert_hunks(
buffer.snapshot().git_diff_hunks_in_row_range(0..4),
&buffer,
@@ -2590,7 +2594,10 @@ async fn test_git_diff_base_change(
// Smoke test diffing
buffer_remote_a.read_with(cx_b, |buffer, _| {
assert_eq!(buffer.diff_base(), Some(diff_base.as_ref()));
assert_eq!(
buffer.diff_base().map(|rope| rope.to_string()).as_deref(),
Some(diff_base.as_str())
);
git::diff::assert_hunks(
buffer.snapshot().git_diff_hunks_in_row_range(0..4),
&buffer,
@@ -2610,7 +2617,10 @@ async fn test_git_diff_base_change(
// Smoke test new diffing
buffer_local_a.read_with(cx_a, |buffer, _| {
assert_eq!(buffer.diff_base(), Some(new_diff_base.as_ref()));
assert_eq!(
buffer.diff_base().map(|rope| rope.to_string()).as_deref(),
Some(new_diff_base.as_str())
);
git::diff::assert_hunks(
buffer.snapshot().git_diff_hunks_in_row_range(0..4),
@@ -2623,7 +2633,10 @@ async fn test_git_diff_base_change(
// Smoke test B
buffer_remote_a.read_with(cx_b, |buffer, _| {
assert_eq!(buffer.diff_base(), Some(new_diff_base.as_ref()));
assert_eq!(
buffer.diff_base().map(|rope| rope.to_string()).as_deref(),
Some(new_diff_base.as_str())
);
git::diff::assert_hunks(
buffer.snapshot().git_diff_hunks_in_row_range(0..4),
&buffer,
@@ -2663,7 +2676,10 @@ async fn test_git_diff_base_change(
// Smoke test diffing
buffer_local_b.read_with(cx_a, |buffer, _| {
assert_eq!(buffer.diff_base(), Some(diff_base.as_ref()));
assert_eq!(
buffer.diff_base().map(|rope| rope.to_string()).as_deref(),
Some(diff_base.as_str())
);
git::diff::assert_hunks(
buffer.snapshot().git_diff_hunks_in_row_range(0..4),
&buffer,
@@ -2684,7 +2700,10 @@ async fn test_git_diff_base_change(
// Smoke test diffing
buffer_remote_b.read_with(cx_b, |buffer, _| {
assert_eq!(buffer.diff_base(), Some(diff_base.as_ref()));
assert_eq!(
buffer.diff_base().map(|rope| rope.to_string()).as_deref(),
Some(diff_base.as_str())
);
git::diff::assert_hunks(
buffer.snapshot().git_diff_hunks_in_row_range(0..4),
&buffer,
@@ -2704,7 +2723,10 @@ async fn test_git_diff_base_change(
// Smoke test new diffing
buffer_local_b.read_with(cx_a, |buffer, _| {
assert_eq!(buffer.diff_base(), Some(new_diff_base.as_ref()));
assert_eq!(
buffer.diff_base().map(|rope| rope.to_string()).as_deref(),
Some(new_diff_base.as_str())
);
println!("{:?}", buffer.as_rope().to_string());
println!("{:?}", buffer.diff_base());
println!(
@@ -2726,7 +2748,10 @@ async fn test_git_diff_base_change(
// Smoke test B
buffer_remote_b.read_with(cx_b, |buffer, _| {
assert_eq!(buffer.diff_base(), Some(new_diff_base.as_ref()));
assert_eq!(
buffer.diff_base().map(|rope| rope.to_string()).as_deref(),
Some(new_diff_base.as_str())
);
git::diff::assert_hunks(
buffer.snapshot().git_diff_hunks_in_row_range(0..4),
&buffer,
@@ -2770,7 +2795,7 @@ async fn test_git_branch_name(
.await
.unwrap();
let project_remote = client_b.build_remote_project(project_id, cx_b).await;
let project_remote = client_b.build_dev_server_project(project_id, cx_b).await;
client_a
.fs()
.set_branch_name(Path::new("/dir/.git"), Some("branch-1"));
@@ -2815,7 +2840,7 @@ async fn test_git_branch_name(
assert_branch(Some("branch-2"), project, cx)
});
let project_remote_c = client_c.build_remote_project(project_id, cx_c).await;
let project_remote_c = client_c.build_dev_server_project(project_id, cx_c).await;
executor.run_until_parked();
project_remote_c.read_with(cx_c, |project, cx| {
@@ -2870,7 +2895,7 @@ async fn test_git_status_sync(
.await
.unwrap();
let project_remote = client_b.build_remote_project(project_id, cx_b).await;
let project_remote = client_b.build_dev_server_project(project_id, cx_b).await;
// Wait for it to catch up to the new status
executor.run_until_parked();
@@ -2946,7 +2971,7 @@ async fn test_git_status_sync(
});
// And synchronization while joining
let project_remote_c = client_c.build_remote_project(project_id, cx_c).await;
let project_remote_c = client_c.build_dev_server_project(project_id, cx_c).await;
executor.run_until_parked();
project_remote_c.read_with(cx_c, |project, cx| {
@@ -2994,7 +3019,7 @@ async fn test_fs_operations(
.update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
.await
.unwrap();
let project_b = client_b.build_remote_project(project_id, cx_b).await;
let project_b = client_b.build_dev_server_project(project_id, cx_b).await;
let worktree_a = project_a.read_with(cx_a, |project, _| project.worktrees().next().unwrap());
@@ -3288,7 +3313,7 @@ async fn test_local_settings(
executor.run_until_parked();
// As client B, join that project and observe the local settings.
let project_b = client_b.build_remote_project(project_id, cx_b).await;
let project_b = client_b.build_dev_server_project(project_id, cx_b).await;
let worktree_b = project_b.read_with(cx_b, |project, _| project.worktrees().next().unwrap());
executor.run_until_parked();
@@ -3411,7 +3436,7 @@ async fn test_buffer_conflict_after_save(
.update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
.await
.unwrap();
let project_b = client_b.build_remote_project(project_id, cx_b).await;
let project_b = client_b.build_dev_server_project(project_id, cx_b).await;
// Open a buffer as client B
let buffer_b = project_b
@@ -3475,7 +3500,7 @@ async fn test_buffer_reloading(
.update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
.await
.unwrap();
let project_b = client_b.build_remote_project(project_id, cx_b).await;
let project_b = client_b.build_dev_server_project(project_id, cx_b).await;
// Open a buffer as client B
let buffer_b = project_b
@@ -3529,7 +3554,7 @@ async fn test_editing_while_guest_opens_buffer(
.update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
.await
.unwrap();
let project_b = client_b.build_remote_project(project_id, cx_b).await;
let project_b = client_b.build_dev_server_project(project_id, cx_b).await;
// Open a buffer as client A
let buffer_a = project_a
@@ -3577,7 +3602,7 @@ async fn test_leaving_worktree_while_opening_buffer(
.update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
.await
.unwrap();
let project_b = client_b.build_remote_project(project_id, cx_b).await;
let project_b = client_b.build_dev_server_project(project_id, cx_b).await;
// See that a guest has joined as client A.
executor.run_until_parked();
@@ -3624,7 +3649,7 @@ async fn test_canceling_buffer_opening(
.update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
.await
.unwrap();
let project_b = client_b.build_remote_project(project_id, cx_b).await;
let project_b = client_b.build_dev_server_project(project_id, cx_b).await;
let buffer_a = project_a
.update(cx_a, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
@@ -3681,8 +3706,8 @@ async fn test_leaving_project(
.update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
.await
.unwrap();
let project_b1 = client_b.build_remote_project(project_id, cx_b).await;
let project_c = client_c.build_remote_project(project_id, cx_c).await;
let project_b1 = client_b.build_dev_server_project(project_id, cx_b).await;
let project_c = client_c.build_dev_server_project(project_id, cx_c).await;
// Client A sees that a guest has joined.
executor.run_until_parked();
@@ -3723,7 +3748,7 @@ async fn test_leaving_project(
});
// Client B re-joins the project and can open buffers as before.
let project_b2 = client_b.build_remote_project(project_id, cx_b).await;
let project_b2 = client_b.build_dev_server_project(project_id, cx_b).await;
executor.run_until_parked();
project_a.read_with(cx_a, |project, _| {
@@ -3899,7 +3924,7 @@ async fn test_collaborating_with_diagnostics(
);
// Join the worktree as client B.
let project_b = client_b.build_remote_project(project_id, cx_b).await;
let project_b = client_b.build_dev_server_project(project_id, cx_b).await;
// Wait for server to see the diagnostics update.
executor.run_until_parked();
@@ -3924,7 +3949,7 @@ async fn test_collaborating_with_diagnostics(
});
// Join project as client C and observe the diagnostics.
let project_c = client_c.build_remote_project(project_id, cx_c).await;
let project_c = client_c.build_dev_server_project(project_id, cx_c).await;
executor.run_until_parked();
let project_c_diagnostic_summaries =
Rc::new(RefCell::new(project_c.read_with(cx_c, |project, cx| {
@@ -4132,7 +4157,7 @@ async fn test_collaborating_with_lsp_progress_updates_and_diagnostics_ordering(
.unwrap();
// Join the project as client B and open all three files.
let project_b = client_b.build_remote_project(project_id, cx_b).await;
let project_b = client_b.build_dev_server_project(project_id, cx_b).await;
let guest_buffers = futures::future::try_join_all(file_names.iter().map(|file_name| {
project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, file_name), cx))
}))
@@ -4238,7 +4263,7 @@ async fn test_reloading_buffer_manually(
.await
.unwrap();
let project_b = client_b.build_remote_project(project_id, cx_b).await;
let project_b = client_b.build_dev_server_project(project_id, cx_b).await;
let open_buffer = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx));
let buffer_b = cx_b.executor().spawn(open_buffer).await.unwrap();
@@ -4336,7 +4361,7 @@ async fn test_formatting_buffer(
.update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
.await
.unwrap();
let project_b = client_b.build_remote_project(project_id, cx_b).await;
let project_b = client_b.build_dev_server_project(project_id, cx_b).await;
let open_buffer = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx));
let buffer_b = cx_b.executor().spawn(open_buffer).await.unwrap();
@@ -4456,7 +4481,7 @@ async fn test_prettier_formatting_buffer(
.update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
.await
.unwrap();
let project_b = client_b.build_remote_project(project_id, cx_b).await;
let project_b = client_b.build_dev_server_project(project_id, cx_b).await;
let open_buffer = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx));
let buffer_b = cx_b.executor().spawn(open_buffer).await.unwrap();
@@ -4559,7 +4584,7 @@ async fn test_definition(
.update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
.await
.unwrap();
let project_b = client_b.build_remote_project(project_id, cx_b).await;
let project_b = client_b.build_dev_server_project(project_id, cx_b).await;
// Open the file on client B.
let open_buffer = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx));
@@ -4704,7 +4729,7 @@ async fn test_references(
.update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
.await
.unwrap();
let project_b = client_b.build_remote_project(project_id, cx_b).await;
let project_b = client_b.build_dev_server_project(project_id, cx_b).await;
// Open the file on client B.
let open_buffer = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "one.rs"), cx));
@@ -4861,7 +4886,7 @@ async fn test_project_search(
.await
.unwrap();
let project_b = client_b.build_remote_project(project_id, cx_b).await;
let project_b = client_b.build_dev_server_project(project_id, cx_b).await;
// Perform a search as the guest.
let mut results = HashMap::default();
@@ -4942,7 +4967,7 @@ async fn test_document_highlights(
.update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
.await
.unwrap();
let project_b = client_b.build_remote_project(project_id, cx_b).await;
let project_b = client_b.build_dev_server_project(project_id, cx_b).await;
// Open the file on client B.
let open_b = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx));
@@ -5064,7 +5089,7 @@ async fn test_lsp_hover(
.update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
.await
.unwrap();
let project_b = client_b.build_remote_project(project_id, cx_b).await;
let project_b = client_b.build_dev_server_project(project_id, cx_b).await;
// Open the file as the guest
let open_buffer = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx));
@@ -5241,7 +5266,7 @@ async fn test_project_symbols(
.update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
.await
.unwrap();
let project_b = client_b.build_remote_project(project_id, cx_b).await;
let project_b = client_b.build_dev_server_project(project_id, cx_b).await;
// Cause the language server to start.
let open_buffer_task =
@@ -5336,7 +5361,7 @@ async fn test_open_buffer_while_getting_definition_pointing_to_it(
.update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
.await
.unwrap();
let project_b = client_b.build_remote_project(project_id, cx_b).await;
let project_b = client_b.build_dev_server_project(project_id, cx_b).await;
let open_buffer_task = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx));
let buffer_b1 = cx_b.executor().spawn(open_buffer_task).await.unwrap();
@@ -6104,7 +6129,7 @@ async fn test_right_click_menu_behind_collab_panel(cx: &mut TestAppContext) {
}
#[gpui::test]
async fn test_cmd_k_left(cx: &mut TestAppContext) {
async fn test_pane_split_left(cx: &mut TestAppContext) {
let (_, client) = TestServer::start1(cx).await;
let (workspace, cx) = client.build_test_workspace(cx).await;

View File

@@ -217,19 +217,20 @@ impl RandomizedTest for ProjectCollaborationTest {
0..=70 => {
// Open a remote project
if let Some(room) = call.read_with(cx, |call, _| call.room().cloned()) {
let existing_remote_project_ids = cx.read(|cx| {
let existing_dev_server_project_ids = cx.read(|cx| {
client
.remote_projects()
.dev_server_projects()
.iter()
.map(|p| p.read(cx).remote_id().unwrap())
.collect::<Vec<_>>()
});
let new_remote_projects = room.read_with(cx, |room, _| {
let new_dev_server_projects = room.read_with(cx, |room, _| {
room.remote_participants()
.values()
.flat_map(|participant| {
participant.projects.iter().filter_map(|project| {
if existing_remote_project_ids.contains(&project.id) {
if existing_dev_server_project_ids.contains(&project.id)
{
None
} else {
Some((
@@ -241,9 +242,9 @@ impl RandomizedTest for ProjectCollaborationTest {
})
.collect::<Vec<_>>()
});
if !new_remote_projects.is_empty() {
if !new_dev_server_projects.is_empty() {
let (host_id, first_root_name) =
new_remote_projects.choose(rng).unwrap().clone();
new_dev_server_projects.choose(rng).unwrap().clone();
break ClientOperation::OpenRemoteProject {
host_id,
first_root_name,
@@ -259,8 +260,8 @@ impl RandomizedTest for ProjectCollaborationTest {
// Close a remote project
71..=80 => {
if !client.remote_projects().is_empty() {
let project = client.remote_projects().choose(rng).unwrap().clone();
if !client.dev_server_projects().is_empty() {
let project = client.dev_server_projects().choose(rng).unwrap().clone();
let first_root_name = root_name_for_project(&project, cx);
break ClientOperation::CloseRemoteProject {
project_root_name: first_root_name,
@@ -595,12 +596,12 @@ impl RandomizedTest for ProjectCollaborationTest {
);
let ix = client
.remote_projects()
.dev_server_projects()
.iter()
.position(|p| p == &project)
.unwrap();
cx.update(|_| {
client.remote_projects_mut().remove(ix);
client.dev_server_projects_mut().remove(ix);
client.buffers().retain(|p, _| *p != project);
drop(project);
});
@@ -642,7 +643,7 @@ impl RandomizedTest for ProjectCollaborationTest {
);
let project = project.await?;
client.remote_projects_mut().push(project.clone());
client.dev_server_projects_mut().push(project.clone());
}
ClientOperation::CreateWorktreeEntry {
@@ -1142,7 +1143,7 @@ impl RandomizedTest for ProjectCollaborationTest {
async fn on_quiesce(_: &mut TestServer, clients: &mut [(Rc<TestClient>, TestAppContext)]) {
for (client, client_cx) in clients.iter() {
for guest_project in client.remote_projects().iter() {
for guest_project in client.dev_server_projects().iter() {
guest_project.read_with(client_cx, |guest_project, cx| {
let host_project = clients.iter().find_map(|(client, cx)| {
let project = client
@@ -1487,8 +1488,9 @@ fn project_for_root_name(
if let Some(ix) = project_ix_for_root_name(client.local_projects().deref(), root_name, cx) {
return Some(client.local_projects()[ix].clone());
}
if let Some(ix) = project_ix_for_root_name(client.remote_projects().deref(), root_name, cx) {
return Some(client.remote_projects()[ix].clone());
if let Some(ix) = project_ix_for_root_name(client.dev_server_projects().deref(), root_name, cx)
{
return Some(client.dev_server_projects()[ix].clone());
}
None
}
@@ -1578,7 +1580,7 @@ fn choose_random_project(client: &TestClient, rng: &mut StdRng) -> Option<Model<
.local_projects()
.deref()
.iter()
.chain(client.remote_projects().iter())
.chain(client.dev_server_projects().iter())
.choose(rng)
.cloned()
}

View File

@@ -533,7 +533,7 @@ impl<T: RandomizedTest> TestPlan<T> {
deterministic.finish_waiting();
server.allow_connections();
for project in client.remote_projects().iter() {
for project in client.dev_server_projects().iter() {
project.read_with(&client_cx, |project, _| {
assert!(
project.is_disconnected(),

View File

@@ -17,6 +17,7 @@ use collab_ui::channel_view::ChannelView;
use collections::{HashMap, HashSet};
use fs::FakeFs;
use futures::{channel::oneshot, StreamExt as _};
use git::GitHostingProviderRegistry;
use gpui::{BackgroundExecutor, Context, Model, Task, TestAppContext, View, VisualTestContext};
use language::LanguageRegistry;
use node_runtime::FakeNodeRuntime;
@@ -64,7 +65,7 @@ pub struct TestClient {
#[derive(Default)]
struct TestClientState {
local_projects: Vec<Model<Project>>,
remote_projects: Vec<Model<Project>>,
dev_server_projects: Vec<Model<Project>>,
buffers: HashMap<Model<Project>, HashSet<Model<language::Buffer>>>,
channel_buffers: HashSet<Model<ChannelBuffer>>,
}
@@ -257,6 +258,11 @@ impl TestServer {
})
});
let git_hosting_provider_registry =
cx.update(|cx| GitHostingProviderRegistry::default_global(cx));
git_hosting_provider_registry
.register_hosting_provider(Arc::new(git_hosting_providers::Github));
let fs = FakeFs::new(cx.executor());
let user_store = cx.new_model(|cx| UserStore::new(client.clone(), cx));
let workspace_store = cx.new_model(|cx| WorkspaceStore::new(client.clone(), cx));
@@ -271,6 +277,12 @@ impl TestServer {
node_runtime: FakeNodeRuntime::new(),
});
let os_keymap = if cfg!(target_os = "linux") {
"keymaps/default-linux.json"
} else {
"keymaps/default-macos.json"
};
cx.update(|cx| {
theme::init(theme::LoadThemes::JustBase, cx);
Project::init(&client, cx);
@@ -284,8 +296,8 @@ impl TestServer {
collab_ui::init(&app_state, cx);
file_finder::init(cx);
menu::init();
remote_projects::init(client.clone(), cx);
settings::KeymapFile::load_asset("keymaps/default-macos.json", cx).unwrap();
dev_server_projects::init(client.clone(), cx);
settings::KeymapFile::load_asset(os_keymap, cx).unwrap();
});
client
@@ -649,6 +661,7 @@ impl TestServer {
auto_join_channel_id: None,
migrations_path: None,
seed_path: None,
supermaven_admin_api_key: None,
},
})
}
@@ -729,16 +742,18 @@ impl TestClient {
Ref::map(self.state.borrow(), |state| &state.local_projects)
}
pub fn remote_projects(&self) -> impl Deref<Target = Vec<Model<Project>>> + '_ {
Ref::map(self.state.borrow(), |state| &state.remote_projects)
pub fn dev_server_projects(&self) -> impl Deref<Target = Vec<Model<Project>>> + '_ {
Ref::map(self.state.borrow(), |state| &state.dev_server_projects)
}
pub fn local_projects_mut(&self) -> impl DerefMut<Target = Vec<Model<Project>>> + '_ {
RefMut::map(self.state.borrow_mut(), |state| &mut state.local_projects)
}
pub fn remote_projects_mut(&self) -> impl DerefMut<Target = Vec<Model<Project>>> + '_ {
RefMut::map(self.state.borrow_mut(), |state| &mut state.remote_projects)
pub fn dev_server_projects_mut(&self) -> impl DerefMut<Target = Vec<Model<Project>>> + '_ {
RefMut::map(self.state.borrow_mut(), |state| {
&mut state.dev_server_projects
})
}
pub fn buffers_for_project<'a>(
@@ -863,7 +878,7 @@ impl TestClient {
})
}
pub async fn build_remote_project(
pub async fn build_dev_server_project(
&self,
host_project_id: u64,
guest_cx: &mut TestAppContext,

View File

@@ -50,6 +50,8 @@ parking_lot.workspace = true
picker.workspace = true
project.workspace = true
recent_projects.workspace = true
dev_server_projects.workspace = true
release_channel.workspace = true
rich_text.workspace = true
rpc.workspace = true
schemars.workspace = true

View File

@@ -572,7 +572,7 @@ impl ChatPanel {
)
.child(
self.render_popover_buttons(&cx, message_id, can_delete_message, can_edit_message)
.neg_mt_2p5(),
.mt_neg_2p5(),
)
}

View File

@@ -59,6 +59,7 @@ impl CompletionProvider for MessageEditorCompletionProvider {
fn resolve_completions(
&self,
_buffer: Model<Buffer>,
_completion_indices: Vec<usize>,
_completions: Arc<RwLock<Box<[Completion]>>>,
_cx: &mut ViewContext<Editor>,

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