Compare commits

..

100 Commits

Author SHA1 Message Date
Bennet Bo Fenner
f448d7e2e8 remove app_version from gpui platform 2024-05-07 16:28:03 +02:00
Bennet Bo Fenner
4624a0a9f4 allow specifying version for gpui app 2024-05-07 16:27:35 +02: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
293 changed files with 13293 additions and 6507 deletions

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

164
Cargo.lock generated
View File

@@ -382,7 +382,6 @@ dependencies = [
"editor",
"env_logger",
"feature_flags",
"fs",
"futures 0.3.28",
"gpui",
"language",
@@ -412,10 +411,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 +1487,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 +1517,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",
@@ -2280,6 +2286,7 @@ dependencies = [
"fs",
"futures 0.3.28",
"git",
"git_hosting_providers",
"google_ai",
"gpui",
"headless",
@@ -2316,6 +2323,7 @@ dependencies = [
"sha2 0.10.7",
"sqlx",
"subtle",
"supermaven_api",
"telemetry_events",
"text",
"theme",
@@ -2360,6 +2368,7 @@ dependencies = [
"pretty_assertions",
"project",
"recent_projects",
"release_channel",
"rich_text",
"rpc",
"schemars",
@@ -2511,30 +2520,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",
@@ -2543,14 +2532,18 @@ dependencies = [
"language",
"lsp",
"menu",
"node_runtime",
"parking_lot",
"project",
"rpc",
"serde",
"serde_json",
"settings",
"smol",
"theme",
"ui",
"util",
"workspace",
"zed_actions",
]
[[package]]
@@ -3409,6 +3402,7 @@ dependencies = [
"smol",
"snippet",
"sum_tree",
"task",
"text",
"theme",
"time",
@@ -4398,14 +4392,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",
@@ -4432,6 +4428,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"
@@ -4618,6 +4633,7 @@ dependencies = [
"wayland-client",
"wayland-cursor",
"wayland-protocols",
"wayland-protocols-plasma",
"windows 0.53.0",
"x11rb",
"xkbcommon",
@@ -5142,6 +5158,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"
@@ -5547,6 +5587,7 @@ dependencies = [
"anyhow",
"client",
"collections",
"copilot",
"editor",
"env_logger",
"futures 0.3.28",
@@ -7421,7 +7462,6 @@ dependencies = [
"client",
"clock",
"collections",
"copilot",
"env_logger",
"fs",
"futures 0.3.28",
@@ -8704,7 +8744,12 @@ dependencies = [
"sha2 0.10.7",
"smol",
"tempfile",
"theme",
"tree-sitter",
"ui",
"unindent",
"util",
"workspace",
"worktree",
]
@@ -9591,6 +9636,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"
@@ -9824,6 +9906,7 @@ dependencies = [
"futures 0.3.28",
"gpui",
"hex",
"parking_lot",
"schemars",
"serde",
"serde_json_lenient",
@@ -9836,7 +9919,6 @@ dependencies = [
name = "tasks_ui"
version = "0.1.0"
dependencies = [
"anyhow",
"editor",
"file_icons",
"fuzzy",
@@ -11728,6 +11810,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"
@@ -11795,12 +11890,12 @@ version = "0.1.0"
dependencies = [
"anyhow",
"client",
"copilot_ui",
"db",
"editor",
"extensions_ui",
"fuzzy",
"gpui",
"inline_completion_button",
"install_cli",
"picker",
"project",
@@ -12680,7 +12775,6 @@ dependencies = [
"collections",
"command_palette",
"copilot",
"copilot_ui",
"db",
"dev_server_projects",
"diagnostics",
@@ -12693,10 +12787,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",
@@ -12727,6 +12824,7 @@ dependencies = [
"settings",
"simplelog",
"smol",
"supermaven",
"tab_switcher",
"task",
"tasks_ui",
@@ -12790,7 +12888,7 @@ dependencies = [
[[package]]
name = "zed_elixir"
version = "0.0.2"
version = "0.0.4"
dependencies = [
"zed_extension_api 0.0.6 (registry+https://github.com/rust-lang/crates.io-index)",
]
@@ -12924,7 +13022,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)",
]
@@ -12945,7 +13043,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",
@@ -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" }
@@ -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 . .

100
README.md
View File

@@ -1,50 +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 install --cask 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

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

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

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

@@ -22,7 +22,6 @@ client.workspace = true
collections.workspace = true
editor.workspace = true
feature_flags.workspace = true
fs.workspace = true
futures.workspace = true
gpui.workspace = true
language.workspace = true

View File

@@ -0,0 +1 @@
> Give me a comprehensive list of all the elements define in my project (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 @@
> Can you tell me what the assistant2 crate is for in my project? Tell me in 100 words or less.

View File

@@ -1,378 +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: &mut WindowContext,
) -> 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: &mut WindowContext,
) -> 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, None, 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())
}
}

View File

@@ -1,11 +1,19 @@
mod assistant_settings;
mod attachments;
mod completion_provider;
mod tools;
pub mod ui;
use crate::{
attachments::ActiveEditorAttachmentTool,
tools::{CreateBufferTool, ProjectIndexTool},
ui::UserOrAssistant,
};
use ::ui::{div, prelude::*, Color, ViewContext};
use anyhow::{Context, Result};
use assistant_tooling::{ToolFunctionCall, ToolRegistry};
use assistant_tooling::{
AttachmentRegistry, ProjectContext, ToolFunctionCall, ToolRegistry, UserAttachment,
};
use client::{proto, Client, UserStore};
use collections::HashMap;
use completion_provider::*;
@@ -19,12 +27,12 @@ use gpui::{
use language::{language_settings::SoftWrap, LanguageRegistry};
use open_ai::{FunctionContent, ToolCall, ToolCallContent};
use rich_text::RichText;
use semantic_index::{CloudEmbeddingProvider, ProjectIndex, SemanticIndex};
use semantic_index::{CloudEmbeddingProvider, ProjectIndex, ProjectIndexDebugView, SemanticIndex};
use serde::Deserialize;
use settings::Settings;
use std::sync::Arc;
use ui::Composer;
use util::{paths::EMBEDDINGS_DIR, ResultExt};
use ui::{ActiveFileButton, Composer, ProjectIndexButton};
use util::{maybe, paths::EMBEDDINGS_DIR, ResultExt};
use workspace::{
dock::{DockPosition, Panel, PanelEvent},
Workspace,
@@ -32,9 +40,6 @@ use workspace::{
pub use assistant_settings::AssistantSettings;
use crate::tools::{CreateBufferTool, ProjectIndexTool};
use crate::ui::UserOrAssistant;
const MAX_COMPLETION_CALLS_PER_SUBMISSION: usize = 5;
#[derive(Eq, PartialEq, Copy, Clone, Deserialize)]
@@ -81,6 +86,13 @@ pub fn init(client: Arc<Client>, cx: &mut AppContext) {
workspace.register_action(|workspace, _: &ToggleFocus, cx| {
workspace.toggle_panel_focus::<AssistantPanel>(cx);
});
workspace.register_action(|workspace, _: &DebugProjectIndex, cx| {
if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
let index = panel.read(cx).chat.read(cx).project_index.clone();
let view = cx.new_view(|cx| ProjectIndexDebugView::new(index, cx));
workspace.add_item_to_center(Box::new(view), cx);
}
});
},
)
.detach();
@@ -105,20 +117,14 @@ impl AssistantPanel {
(workspace.app_state().clone(), workspace.project().clone())
})?;
let user_store = app_state.user_store.clone();
cx.new_view(|cx| {
// todo!("this will panic if the semantic index failed to load or has not loaded yet")
let project_index = cx.update_global(|semantic_index: &mut SemanticIndex, cx| {
semantic_index.project_index(project.clone(), cx)
});
let mut tool_registry = ToolRegistry::new();
tool_registry
.register(
ProjectIndexTool::new(project_index.clone(), app_state.fs.clone()),
cx,
)
.register(ProjectIndexTool::new(project_index.clone()), cx)
.context("failed to register ProjectIndexTool")
.log_err();
tool_registry
@@ -129,13 +135,16 @@ impl AssistantPanel {
.context("failed to register CreateBufferTool")
.log_err();
let tool_registry = Arc::new(tool_registry);
let mut attachment_store = AttachmentRegistry::new();
attachment_store.register(ActiveEditorAttachmentTool::new(workspace.clone(), cx));
Self::new(
app_state.languages.clone(),
tool_registry,
user_store,
Some(project_index),
Arc::new(tool_registry),
Arc::new(attachment_store),
app_state.user_store.clone(),
project_index,
workspace,
cx,
)
})
@@ -145,16 +154,20 @@ impl AssistantPanel {
pub fn new(
language_registry: Arc<LanguageRegistry>,
tool_registry: Arc<ToolRegistry>,
attachment_store: Arc<AttachmentRegistry>,
user_store: Model<UserStore>,
project_index: Option<Model<ProjectIndex>>,
project_index: Model<ProjectIndex>,
workspace: WeakView<Workspace>,
cx: &mut ViewContext<Self>,
) -> Self {
let chat = cx.new_view(|cx| {
AssistantChat::new(
language_registry.clone(),
language_registry,
tool_registry.clone(),
attachment_store,
user_store,
project_index,
workspace,
cx,
)
});
@@ -168,8 +181,7 @@ impl Render for AssistantPanel {
div()
.size_full()
.v_flex()
.p_2()
.bg(cx.theme().colors().background)
.bg(cx.theme().colors().panel_background)
.child(self.chat.clone())
}
}
@@ -228,13 +240,16 @@ pub struct AssistantChat {
list_state: ListState,
language_registry: Arc<LanguageRegistry>,
composer_editor: View<Editor>,
project_index_button: View<ProjectIndexButton>,
active_file_button: Option<View<ActiveFileButton>>,
user_store: Model<UserStore>,
next_message_id: MessageId,
collapsed_messages: HashMap<MessageId, bool>,
editing_message: Option<EditingMessage>,
pending_completion: Option<Task<()>>,
tool_registry: Arc<ToolRegistry>,
project_index: Option<Model<ProjectIndex>>,
attachment_registry: Arc<AttachmentRegistry>,
project_index: Model<ProjectIndex>,
}
struct EditingMessage {
@@ -247,8 +262,10 @@ impl AssistantChat {
fn new(
language_registry: Arc<LanguageRegistry>,
tool_registry: Arc<ToolRegistry>,
attachment_registry: Arc<AttachmentRegistry>,
user_store: Model<UserStore>,
project_index: Option<Model<ProjectIndex>>,
project_index: Model<ProjectIndex>,
workspace: WeakView<Workspace>,
cx: &mut ViewContext<Self>,
) -> Self {
let model = CompletionProvider::get(cx).default_model();
@@ -263,6 +280,19 @@ impl AssistantChat {
},
);
let project_index_button = cx.new_view(|cx| {
ProjectIndexButton::new(project_index.clone(), tool_registry.clone(), cx)
});
let active_file_button = match workspace.upgrade() {
Some(workspace) => {
Some(cx.new_view(
|cx| ActiveFileButton::new(attachment_registry.clone(), workspace, cx), //
))
}
_ => None,
};
Self {
model,
messages: Vec::new(),
@@ -275,11 +305,14 @@ impl AssistantChat {
list_state,
user_store,
language_registry,
project_index_button,
active_file_button,
project_index,
next_message_id: MessageId(0),
editing_message: None,
collapsed_messages: HashMap::default(),
pending_completion: None,
attachment_registry,
tool_registry,
}
}
@@ -345,7 +378,12 @@ impl AssistantChat {
editor
});
composer_editor.clear(cx);
ChatMessage::User(UserMessage { id, body })
ChatMessage::User(UserMessage {
id,
body,
attachments: Vec::new(),
})
});
self.push_message(message, cx);
} else {
@@ -355,6 +393,29 @@ impl AssistantChat {
let mode = *mode;
self.pending_completion = Some(cx.spawn(move |this, mut cx| async move {
let attachments_task = this.update(&mut cx, |this, cx| {
let attachment_store = this.attachment_registry.clone();
attachment_store.call_all_attachment_tools(cx)
});
let attachments = maybe!(async {
let attachments_task = attachments_task?;
let attachments = attachments_task.await?;
anyhow::Ok(attachments)
})
.await
.log_err()
.unwrap_or_default();
// Set the attachments to the _last_ user message
this.update(&mut cx, |this, _cx| {
if let Some(ChatMessage::User(message)) = this.messages.last_mut() {
message.attachments = attachments;
}
})
.log_err();
Self::request_completion(
this.clone(),
mode,
@@ -372,14 +433,6 @@ impl AssistantChat {
}));
}
fn debug_project_index(&mut self, _: &DebugProjectIndex, cx: &mut ViewContext<Self>) {
if let Some(index) = &self.project_index {
index.update(cx, |project_index, cx| {
project_index.debug(cx).detach_and_log_err(cx)
});
}
}
async fn request_completion(
this: WeakView<Self>,
mode: SubmitMode,
@@ -389,7 +442,7 @@ impl AssistantChat {
let mut call_count = 0;
loop {
let complete = async {
let completion = this.update(cx, |this, cx| {
let (tool_definitions, model_name, messages) = this.update(cx, |this, cx| {
this.push_new_assistant_message(cx);
let definitions = if call_count < limit
@@ -397,18 +450,26 @@ impl AssistantChat {
{
this.tool_registry.definitions()
} else {
&[]
Vec::new()
};
call_count += 1;
let messages = this.completion_messages(cx);
CompletionProvider::get(cx).complete(
(
definitions,
this.model.clone(),
this.completion_messages(cx),
)
})?;
let messages = messages.await?;
let completion = cx.update(|cx| {
CompletionProvider::get(cx).complete(
model_name,
messages,
Vec::new(),
1.0,
definitions,
tool_definitions,
)
});
@@ -565,9 +626,9 @@ impl AssistantChat {
div()
.py_1()
.px_2()
.neg_mx_1()
.mx_neg_1()
.rounded_md()
.border()
.border_1()
.border_color(theme.status().error_border)
// .bg(theme.status().error_background)
.text_color(theme.status().error)
@@ -579,18 +640,27 @@ impl AssistantChat {
}
fn render_message(&self, ix: usize, cx: &mut ViewContext<Self>) -> AnyElement {
let is_first = ix == 0;
let is_last = ix == self.messages.len() - 1;
let padding = Spacing::Large.rems(cx);
// Whenever there's a run of assistant messages, group as one Assistant UI element
match &self.messages[ix] {
ChatMessage::User(UserMessage { id, body }) => div()
ChatMessage::User(UserMessage {
id,
body,
attachments,
}) => div()
.id(SharedString::from(format!("message-{}-container", id.0)))
.when(!is_last, |element| element.mb_2())
.when(is_first, |this| this.pt(padding))
.map(|element| {
if self.editing_message_id() == Some(*id) {
element.child(Composer::new(
body.clone(),
self.user_store.read(cx).current_user(),
self.tool_registry.clone(),
self.project_index_button.clone(),
self.active_file_button.clone(),
crate::ui::ModelSelector::new(
cx.view().downgrade(),
self.model.clone(),
@@ -613,25 +683,39 @@ impl AssistantChat {
}
}
}))
.child(crate::ui::ChatMessage::new(
*id,
UserOrAssistant::User(self.user_store.read(cx).current_user()),
Some(
RichText::new(
body.read(cx).text(cx),
&[],
&self.language_registry,
)
.element(ElementId::from(id.0), cx),
),
self.is_message_collapsed(id),
Box::new(cx.listener({
let id = *id;
move |assistant_chat, _event, _cx| {
assistant_chat.toggle_message_collapsed(id)
}
})),
))
.child(
crate::ui::ChatMessage::new(
*id,
UserOrAssistant::User(self.user_store.read(cx).current_user()),
Some(
RichText::new(
body.read(cx).text(cx),
&[],
&self.language_registry,
)
.element(ElementId::from(id.0), cx),
),
Some(
h_flex()
.gap_2()
.children(
attachments
.iter()
.map(|attachment| attachment.view.clone()),
)
.into_any_element(),
),
self.is_message_collapsed(id),
Box::new(cx.listener({
let id = *id;
move |assistant_chat, _event, _cx| {
assistant_chat.toggle_message_collapsed(id)
}
})),
)
// TODO: Wire up selections.
.selected(is_last),
)
}
})
.into_any(),
@@ -647,57 +731,65 @@ impl AssistantChat {
} else {
Some(
div()
.p_2()
.child(body.element(ElementId::from(id.0), cx))
.into_any_element(),
)
};
let tools = tool_calls
.iter()
.map(|tool_call| self.tool_registry.render_tool_call(tool_call, cx))
.collect::<Vec<AnyElement>>();
let tools_body = if tools.is_empty() {
None
} else {
Some(div().children(tools).into_any_element())
};
div()
.when(!is_last, |element| element.mb_2())
.child(crate::ui::ChatMessage::new(
*id,
UserOrAssistant::Assistant,
assistant_body,
self.is_message_collapsed(id),
Box::new(cx.listener({
let id = *id;
move |assistant_chat, _event, _cx| {
assistant_chat.toggle_message_collapsed(id)
}
})),
))
// TODO: Should the errors and tool calls get passed into `ChatMessage`?
.when(is_first, |this| this.pt(padding))
.child(
crate::ui::ChatMessage::new(
*id,
UserOrAssistant::Assistant,
assistant_body,
tools_body,
self.is_message_collapsed(id),
Box::new(cx.listener({
let id = *id;
move |assistant_chat, _event, _cx| {
assistant_chat.toggle_message_collapsed(id)
}
})),
)
// TODO: Wire up selections.
.selected(is_last),
)
.child(self.render_error(error.clone(), ix, cx))
.children(tool_calls.iter().map(|tool_call| {
let result = &tool_call.result;
let name = tool_call.name.clone();
match result {
Some(result) => {
div().p_2().child(result.into_any_element(&name)).into_any()
}
None => div()
.p_2()
.child(Label::new(name).color(Color::Modified))
.child("Running...")
.into_any(),
}
}))
.into_any()
}
}
}
fn completion_messages(&self, cx: &mut WindowContext) -> Vec<CompletionMessage> {
fn completion_messages(&self, cx: &mut WindowContext) -> Task<Result<Vec<CompletionMessage>>> {
let project_index = self.project_index.read(cx);
let project = project_index.project();
let fs = project_index.fs();
let mut project_context = ProjectContext::new(project, fs);
let mut completion_messages = Vec::new();
for message in &self.messages {
match message {
ChatMessage::User(UserMessage { body, .. }) => {
// When we re-introduce contexts like active file, we'll inject them here instead of relying on the model to request them
// contexts.iter().for_each(|context| {
// completion_messages.extend(context.completion_messages(cx))
// });
ChatMessage::User(UserMessage {
body, attachments, ..
}) => {
for attachment in attachments {
if let Some(content) = attachment.generate(&mut project_context, cx) {
completion_messages.push(CompletionMessage::System { content });
}
}
// Show user's message last so that the assistant is grounded in the user's request
completion_messages.push(CompletionMessage::User {
@@ -732,11 +824,11 @@ impl AssistantChat {
});
for tool_call in tool_calls {
// todo!(): we should not be sending when the tool is still running / has no result
// For now I'm going to have to assume we send an empty string because otherwise
// the Chat API will break -- there is a required message for every tool call by ID
// Every tool call _must_ have a result by ID, otherwise OpenAI will error.
let content = match &tool_call.result {
Some(result) => result.format(&tool_call.name),
Some(result) => {
result.generate(&tool_call.name, &mut project_context, cx)
}
None => "".to_string(),
};
@@ -749,7 +841,13 @@ impl AssistantChat {
}
}
completion_messages
let system_message = project_context.generate_system_message(cx);
cx.background_executor().spawn(async move {
let content = system_message.await?;
completion_messages.insert(0, CompletionMessage::System { content });
Ok(completion_messages)
})
}
}
@@ -762,13 +860,12 @@ impl Render for AssistantChat {
.key_context("AssistantChat")
.on_action(cx.listener(Self::submit))
.on_action(cx.listener(Self::cancel))
.on_action(cx.listener(Self::debug_project_index))
.text_color(Color::Default.color(cx))
.child(list(self.list_state.clone()).flex_1())
.child(Composer::new(
self.composer_editor.clone(),
self.user_store.read(cx).current_user(),
self.tool_registry.clone(),
self.project_index_button.clone(),
self.active_file_button.clone(),
crate::ui::ModelSelector::new(cx.view().downgrade(), self.model.clone())
.into_any_element(),
))
@@ -803,6 +900,7 @@ impl ChatMessage {
struct UserMessage {
id: MessageId,
body: View<Editor>,
attachments: Vec<UserAttachment>,
}
struct AssistantMessage {

View File

@@ -0,0 +1,114 @@
pub mod active_file;
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

@@ -0,0 +1 @@

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

@@ -1,5 +1,5 @@
use anyhow::Result;
use assistant_tooling::LanguageModelTool;
use assistant_tooling::{LanguageModelTool, ProjectContext, ToolOutput};
use editor::Editor;
use gpui::{prelude::*, Model, Task, View, WeakView};
use project::Project;
@@ -31,11 +31,9 @@ pub struct CreateBufferInput {
language: String,
}
pub struct CreateBufferOutput {}
impl LanguageModelTool for CreateBufferTool {
type Input = CreateBufferInput;
type Output = CreateBufferOutput;
type Output = ();
type View = CreateBufferView;
fn name(&self) -> String {
@@ -83,32 +81,39 @@ impl LanguageModelTool for CreateBufferTool {
})
.log_err();
Ok(CreateBufferOutput {})
Ok(())
}
})
}
fn format(input: &Self::Input, output: &Result<Self::Output>) -> String {
match output {
Ok(_) => format!("Created a new {} buffer", input.language),
Err(err) => format!("Failed to create buffer: {err:?}"),
}
}
fn output_view(
_tool_call_id: String,
_input: Self::Input,
_output: Result<Self::Output>,
input: Self::Input,
output: Result<Self::Output>,
cx: &mut WindowContext,
) -> View<Self::View> {
cx.new_view(|_cx| CreateBufferView {})
cx.new_view(|_cx| CreateBufferView {
language: input.language,
output,
})
}
}
pub struct CreateBufferView {}
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

@@ -1,26 +1,18 @@
use anyhow::Result;
use assistant_tooling::LanguageModelTool;
use gpui::{percentage, prelude::*, Animation, AnimationExt, AnyView, Model, Task, Transformation};
use project::Fs;
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::{sync::Arc, time::Duration};
use ui::{
div, prelude::*, ButtonLike, CollapsibleContainer, Color, Icon, IconName, Indicator, Label,
SharedString, Tooltip, WindowContext,
};
use util::ResultExt as _;
use std::{fmt::Write as _, ops::Range};
use ui::{div, prelude::*, CollapsibleContainer, Color, Icon, IconName, Label, WindowContext};
const DEFAULT_SEARCH_LIMIT: usize = 20;
#[derive(Clone)]
pub struct CodebaseExcerpt {
path: SharedString,
text: SharedString,
score: f32,
element_id: ElementId,
expanded: bool,
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.
@@ -37,21 +29,31 @@ pub struct CodebaseQuery {
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 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();
}
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 {
@@ -67,61 +69,77 @@ impl Render for ProjectIndexView {
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;
let file_count = output.excerpts.len();
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()),
)
}))
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),
)
})),
),
),
)
}
}
pub struct ProjectIndexTool {
project_index: Model<ProjectIndex>,
fs: Arc<dyn Fs>,
}
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();
pub struct ProjectIndexOutput {
excerpts: Vec<CodebaseExcerpt>,
status: Status,
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>, 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 }
pub fn new(project_index: Model<ProjectIndex>) -> Self {
Self { project_index }
}
}
@@ -135,183 +153,57 @@ impl LanguageModelTool for ProjectIndexTool {
}
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()
"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 results = project_index.search(
let search = project_index.search(
query.query.clone(),
query.limit.unwrap_or(DEFAULT_SEARCH_LIMIT),
cx,
);
let fs = self.fs.clone();
cx.spawn(|mut cx| async move {
let search_results = search.await?;
cx.spawn(|cx| async move {
let results = results.await?;
cx.update(|cx| {
let mut output = ProjectIndexOutput {
status,
excerpts: Default::default(),
};
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();
for search_result in search_results {
let path = ProjectPath {
worktree_id: search_result.worktree.read(cx).id(),
path: search_result.path.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_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);
}
});
let excerpts = futures::future::join_all(excerpts)
.await
.into_iter()
.filter_map(|result| result.log_err())
.collect();
anyhow::Ok(ProjectIndexOutput { excerpts, status })
output
})
})
}
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 })
cx.new_view(|_cx| ProjectIndexView::new(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();
let is_enabled = match status {
Status::Idle => true,
_ => false,
};
let icon = 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),
};
let indicator = 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))),
),
)),
};
ButtonLike::new("project-index")
.disabled(!is_enabled)
.child(
ui::IconWithIndicator::new(icon, indicator)
.indicator_border_color(Some(gpui::transparent_black())),
)
.tooltip({
move |cx| {
let (tooltip, meta) = match status {
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)
}
}
})
fn render_running(_: &mut WindowContext) -> impl IntoElement {
CollapsibleContainer::new(ElementId::Name(nanoid::nanoid!().into()), false)
.start_slot("Searching code base")
}
}

View File

@@ -1,13 +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_store: 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: attachment_store,
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;
@@ -16,6 +16,8 @@ pub struct ChatMessage {
id: MessageId,
player: UserOrAssistant,
message: Option<AnyElement>,
tools_used: Option<AnyElement>,
selected: bool,
collapsed: bool,
on_collapse_handle_click: Box<dyn Fn(&ClickEvent, &mut WindowContext) + 'static>,
}
@@ -25,6 +27,7 @@ impl ChatMessage {
id: MessageId,
player: UserOrAssistant,
message: Option<AnyElement>,
tools_used: Option<AnyElement>,
collapsed: bool,
on_collapse_handle_click: Box<dyn Fn(&ClickEvent, &mut WindowContext) + 'static>,
) -> Self {
@@ -32,75 +35,37 @@ impl ChatMessage {
id,
player,
message,
tools_used,
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 {
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 message_group = SharedString::from(format!("{}_group", self.id.0));
let content_padding = rems(1.);
let collapse_handle_id = SharedString::from(format!("{}_collapse_handle", self.id.0));
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;
let content = self.message.map(|message| {
div()
.overflow_hidden()
.w_full()
.p(content_padding)
.rounded_lg()
.when(self.collapsed, |this| this.h(collapsed_height))
.bg(cx.theme().colors().surface_background)
.child(message)
});
let background_color = if let UserOrAssistant::User(_) = &self.player {
Some(cx.theme().colors().surface_background)
} else {
None
};
v_flex()
.gap_1()
.child(ChatMessageHeader::new(self.player))
.child(h_flex().gap_3().child(collapse_handle).children(content))
}
}
#[derive(IntoElement)]
struct ChatMessageHeader {
player: UserOrAssistant,
contexts: Vec<()>,
}
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(),
@@ -112,23 +77,77 @@ 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_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::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(
// IconButton::new("copy-message", IconName::Copy)
// .icon_color(Color::Muted)
// .icon_size(IconSize::XSmall),
// )
// .child(
// IconButton::new("menu", IconName::Ellipsis)
// .icon_color(Color::Muted)
// .icon_size(IconSize::XSmall),
// ),
),
)
.child(div().when(!self.contexts.is_empty(), |this| {
this.child(Label::new(self.contexts.len().to_string()).color(Color::Muted))
}))
.when(self.message.is_some() || self.tools_used.is_some(), |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.message)
.when_some(self.tools_used, |this, tools_used| this.child(tools_used)),
),
)
})
}
}

View File

@@ -1,61 +1,63 @@
use assistant_tooling::ToolRegistry;
use client::User;
use crate::{
ui::{ActiveFileButton, ProjectIndexButton},
AssistantChat, CompletionProvider,
};
use editor::{Editor, EditorElement, EditorStyle};
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};
use ui::{popover_menu, prelude::*, ButtonLike, ContextMenu, Divider, TextSize, Tooltip};
#[derive(IntoElement)]
pub struct Composer {
editor: View<Editor>,
player: Option<Arc<User>>,
tool_registry: Arc<ToolRegistry>,
project_index_button: View<ProjectIndexButton>,
active_file_button: Option<View<ActiveFileButton>>,
model_selector: AnyElement,
}
impl Composer {
pub fn new(
editor: View<Editor>,
player: Option<Arc<User>>,
tool_registry: Arc<ToolRegistry>,
project_index_button: View<ProjectIndexButton>,
active_file_button: Option<View<ActiveFileButton>>,
model_selector: AnyElement,
) -> Self {
Self {
editor,
player,
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_from_px(20.)).into_any_element();
if let Some(player) = self.player.clone() {
player_avatar = Avatar::new(player.avatar_uri.clone())
.size(rems_from_px(20.))
.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()
.p_3()
.bg(cx.theme().colors().editor_background)
.rounded_lg()
.child(
@@ -95,9 +97,15 @@ impl RenderOnce for Composer {
.gap_2()
.justify_between()
.w_full()
.child(h_flex().gap_1().children(
self.tool_registry.status_views().iter().cloned(),
))
.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().gap_1().child(self.model_selector)),
),
),
@@ -136,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();
});
}

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

@@ -29,6 +29,7 @@ impl Render for ChatMessageStory {
MessageId(0),
UserOrAssistant::User(Some(user_1.clone())),
Some(div().child("What can I do here?").into_any_element()),
None,
false,
Box::new(|_, _| {}),
),
@@ -39,6 +40,7 @@ impl Render for ChatMessageStory {
MessageId(0),
UserOrAssistant::User(Some(user_1.clone())),
Some(div().child("What can I do here?").into_any_element()),
None,
true,
Box::new(|_, _| {}),
),
@@ -52,6 +54,7 @@ impl Render for ChatMessageStory {
MessageId(0),
UserOrAssistant::Assistant,
Some(div().child("You can talk to me!").into_any_element()),
None,
false,
Box::new(|_, _| {}),
),
@@ -62,6 +65,7 @@ impl Render for ChatMessageStory {
MessageId(0),
UserOrAssistant::Assistant,
Some(div().child(MULTI_LINE_MESSAGE).into_any_element()),
None,
true,
Box::new(|_, _| {}),
),
@@ -76,6 +80,7 @@ impl Render for ChatMessageStory {
MessageId(0),
UserOrAssistant::User(Some(user_1.clone())),
Some(div().child("What is Rust??").into_any_element()),
None,
false,
Box::new(|_, _| {}),
))
@@ -83,6 +88,7 @@ impl Render for ChatMessageStory {
MessageId(0),
UserOrAssistant::Assistant,
Some(div().child("Rust is a multi-paradigm programming language focused on performance and safety").into_any_element()),
None,
false,
Box::new(|_, _| {}),
))
@@ -90,6 +96,7 @@ impl Render for ChatMessageStory {
MessageId(0),
UserOrAssistant::User(Some(user_1)),
Some(div().child("Sounds pretty cool!").into_any_element()),
None,
false,
Box::new(|_, _| {}),
)),

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,259 +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::{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 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(
_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 (_, 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

@@ -1,111 +0,0 @@
use anyhow::Result;
use gpui::{AnyElement, AnyView, 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;
/// 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 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,
};
@@ -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

@@ -100,7 +100,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)
}

View File

@@ -217,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);
}
@@ -445,15 +445,12 @@ impl Telemetry {
installation_id: state.installation_id.as_deref().map(Into::into),
session_id: state.session_id.clone(),
is_staff: state.is_staff,
app_version: state
.app_metadata
.app_version
.unwrap_or_default()
.to_string(),
os_name: state.app_metadata.os_name.to_string(),
app_version: state.app_metadata.version.unwrap_or_default().to_string(),
os_name: state.app_metadata.os.name.to_string(),
os_version: state
.app_metadata
.os_version
.os
.version
.map(|version| version.to_string()),
architecture: state.architecture.to_string(),

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"] }

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

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

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

View File

@@ -1,5 +1,8 @@
use anyhow::anyhow;
use rpc::{proto, ConnectionId};
use rpc::{
proto::{self},
ConnectionId,
};
use sea_orm::{
ActiveModelTrait, ActiveValue, ColumnTrait, Condition, DatabaseTransaction, EntityTrait,
ModelTrait, QueryFilter,
@@ -35,24 +38,33 @@ impl Database {
dev_server_id: DevServerId,
) -> crate::Result<Vec<proto::DevServerProject>> {
self.transaction(|tx| async move {
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())
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,
@@ -136,6 +148,39 @@ impl Database {
.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,

View File

@@ -77,10 +77,14 @@ impl Database {
user_id: UserId,
) -> 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)
@@ -95,6 +99,66 @@ impl Database {
.await
}
pub async fn update_dev_server_token(
&self,
id: DevServerId,
hashed_token: &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 {
return Err(anyhow::anyhow!(proto::ErrorCode::Forbidden))?;
}
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,

View File

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

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

@@ -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,
@@ -148,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,
}
@@ -189,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,
@@ -233,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 {
@@ -413,7 +431,10 @@ impl Server {
.add_request_handler(user_handler(join_hosted_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_dev_server_project))
.add_request_handler(dev_server_handler(shutdown_dev_server))
@@ -560,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| {
@@ -937,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,
@@ -954,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 {
@@ -2313,6 +2345,12 @@ 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
@@ -2329,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>,
@@ -2349,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
@@ -2363,6 +2467,68 @@ async fn delete_dev_server(
Ok(())
}
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>,
@@ -2459,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(
@@ -2499,6 +2666,21 @@ async fn shutdown_dev_server_internal(
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,
@@ -4147,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)?,
@@ -4211,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)?,
@@ -4295,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 {
@@ -4419,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)?,
@@ -4438,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 {
@@ -4467,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,
@@ -4510,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>,
@@ -4536,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?;
@@ -4563,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,

View File

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

View File

@@ -667,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,
);
@@ -2073,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| {
@@ -2083,7 +2083,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);
});
});
cx_a.executor().run_until_parked();

View File

@@ -2570,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,
@@ -2591,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,
@@ -2611,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),
@@ -2624,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,
@@ -2664,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,
@@ -2685,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,
@@ -2705,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!(
@@ -2727,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,
@@ -6105,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

@@ -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;
@@ -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));
@@ -655,6 +661,7 @@ impl TestServer {
auto_join_channel_id: None,
migrations_path: None,
seed_path: None,
supermaven_admin_api_key: None,
},
})
}

View File

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

@@ -1408,6 +1408,11 @@ impl CollabPanel {
});
}
if self.context_menu.is_some() {
self.context_menu.take();
cx.notify();
}
self.update_entries(false, cx);
}
@@ -2149,7 +2154,7 @@ impl CollabPanel {
.child(list(self.list_state.clone()).size_full())
.child(
v_flex()
.child(div().mx_2().border_primary(cx).border_t())
.child(div().mx_2().border_primary(cx).border_t_1())
.child(
v_flex()
.p_2()

View File

@@ -20,6 +20,7 @@ use panel_settings::MessageEditorSettings;
pub use panel_settings::{
ChatPanelSettings, CollaborationPanelSettings, NotificationPanelSettings,
};
use release_channel::ReleaseChannel;
use settings::Settings;
use workspace::{notifications::DetachAndPromptErr, AppState};
@@ -96,6 +97,7 @@ pub fn toggle_deafen(_: &ToggleDeafen, cx: &mut AppContext) {
fn notification_window_options(
screen: Rc<dyn PlatformDisplay>,
window_size: Size<Pixels>,
cx: &AppContext,
) -> WindowOptions {
let notification_margin_width = DevicePixels::from(16);
let notification_margin_height = DevicePixels::from(-0) - DevicePixels::from(48);
@@ -112,6 +114,8 @@ fn notification_window_options(
size: window_size.into(),
};
let app_id = ReleaseChannel::global(cx).app_id();
WindowOptions {
bounds: Some(bounds),
titlebar: None,
@@ -122,6 +126,6 @@ fn notification_window_options(
display_id: Some(screen.id()),
fullscreen: false,
window_background: WindowBackgroundAppearance::default(),
app_id: Some("dev.zed.Zed".to_owned()),
app_id: Some(app_id.to_owned()),
}
}

View File

@@ -31,7 +31,7 @@ impl RenderOnce for FacePile {
.into_iter()
.enumerate()
.rev()
.map(|(ix, player)| div().when(ix > 0, |div| div.neg_ml_1()).child(player)),
.map(|(ix, player)| div().when(ix > 0, |div| div.ml_neg_1()).child(player)),
)
}
}

View File

@@ -32,18 +32,22 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
};
for screen in unique_screens {
let options = notification_window_options(screen, window_size);
let window = cx
.open_window(options, |cx| {
cx.new_view(|_| {
IncomingCallNotification::new(
incoming_call.clone(),
app_state.clone(),
)
if let Some(options) = cx
.update(|cx| notification_window_options(screen, window_size, cx))
.log_err()
{
let window = cx
.open_window(options, |cx| {
cx.new_view(|_| {
IncomingCallNotification::new(
incoming_call.clone(),
app_state.clone(),
)
})
})
})
.unwrap();
notification_windows.push(window);
.unwrap();
notification_windows.push(window);
}
}
}
}
@@ -51,11 +55,6 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
.detach();
}
#[derive(Clone, PartialEq)]
struct RespondToCall {
accept: bool,
}
struct IncomingCallNotificationState {
call: IncomingCall,
app_state: Weak<AppState>,

View File

@@ -26,7 +26,7 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
};
for screen in cx.displays() {
let options = notification_window_options(screen, window_size);
let options = notification_window_options(screen, window_size, cx);
let window = cx.open_window(options, |cx| {
cx.new_view(|_| {
ProjectSharedNotification::new(

View File

@@ -27,28 +27,39 @@ anyhow.workspace = true
async-compression.workspace = true
async-tar.workspace = true
collections.workspace = true
client.workspace = true
command_palette_hooks.workspace = true
editor.workspace = true
futures.workspace = true
gpui.workspace = true
language.workspace = true
lsp.workspace = true
menu.workspace = true
node_runtime.workspace = true
parking_lot.workspace = true
project.workspace = true
serde.workspace = true
settings.workspace = true
smol.workspace = true
ui.workspace = true
util.workspace = true
workspace.workspace = true
[target.'cfg(windows)'.dependencies]
async-std = { version = "1.12.0", features = ["unstable"] }
[dev-dependencies]
clock.workspace = true
indoc.workspace = true
serde_json.workspace = true
collections = { workspace = true, features = ["test-support"] }
editor = { workspace = true, features = ["test-support"] }
fs = { workspace = true, features = ["test-support"] }
gpui = { workspace = true, features = ["test-support"] }
language = { workspace = true, features = ["test-support"] }
lsp = { workspace = true, features = ["test-support"] }
project = { workspace = true, features = ["test-support"] }
rpc = { workspace = true, features = ["test-support"] }
settings = { workspace = true, features = ["test-support"] }
theme = { workspace = true, features = ["test-support"] }
util = { workspace = true, features = ["test-support"] }

View File

@@ -1,4 +1,7 @@
mod copilot_completion_provider;
pub mod request;
mod sign_in;
use anyhow::{anyhow, Context as _, Result};
use async_compression::futures::bufread::GzipDecoder;
use async_tar::Archive;
@@ -10,9 +13,9 @@ use gpui::{
ModelContext, Task, WeakModel,
};
use language::{
language_settings::{all_language_settings, language_settings},
point_from_lsp, point_to_lsp, Anchor, Bias, Buffer, BufferSnapshot, Language,
LanguageServerName, PointUtf16, ToPointUtf16,
language_settings::{all_language_settings, language_settings, InlineCompletionProvider},
point_from_lsp, point_to_lsp, Anchor, Bias, Buffer, BufferSnapshot, Language, PointUtf16,
ToPointUtf16,
};
use lsp::{LanguageServer, LanguageServerBinary, LanguageServerId};
use node_runtime::NodeRuntime;
@@ -32,6 +35,9 @@ use util::{
fs::remove_matching, github::latest_github_release, http::HttpClient, maybe, paths, ResultExt,
};
pub use copilot_completion_provider::CopilotCompletionProvider;
pub use sign_in::CopilotCodeVerification;
actions!(
copilot,
[
@@ -144,7 +150,6 @@ impl CopilotServer {
}
struct RunningCopilotServer {
name: LanguageServerName,
lsp: Arc<LanguageServer>,
sign_in_status: SignInStatus,
registered_buffers: HashMap<EntityId, RegisteredBuffer>,
@@ -354,7 +359,9 @@ impl Copilot {
let server_id = self.server_id;
let http = self.http.clone();
let node_runtime = self.node_runtime.clone();
if all_language_settings(None, cx).copilot_enabled(None, None) {
if all_language_settings(None, cx).inline_completions.provider
== InlineCompletionProvider::Copilot
{
if matches!(self.server, CopilotServer::Disabled) {
let start_task = cx
.spawn(move |this, cx| {
@@ -393,7 +400,6 @@ impl Copilot {
http: http.clone(),
node_runtime,
server: CopilotServer::Running(RunningCopilotServer {
name: LanguageServerName(Arc::from("copilot")),
lsp: Arc::new(server),
sign_in_status: SignInStatus::Authorized,
registered_buffers: Default::default(),
@@ -467,7 +473,6 @@ impl Copilot {
match server {
Ok((server, status)) => {
this.server = CopilotServer::Running(RunningCopilotServer {
name: LanguageServerName(Arc::from("copilot")),
lsp: server,
sign_in_status: SignInStatus::SignedOut,
registered_buffers: Default::default(),
@@ -607,9 +612,9 @@ impl Copilot {
cx.background_executor().spawn(start_task)
}
pub fn language_server(&self) -> Option<(&LanguageServerName, &Arc<LanguageServer>)> {
pub fn language_server(&self) -> Option<&Arc<LanguageServer>> {
if let CopilotServer::Running(server) = &self.server {
Some((&server.name, &server.lsp))
Some(&server.lsp)
} else {
None
}
@@ -943,12 +948,9 @@ impl Copilot {
}
fn id_for_language(language: Option<&Arc<Language>>) -> String {
let language_name = language.map(|language| language.name());
match language_name.as_deref() {
Some("Plain Text") => "plaintext".to_string(),
Some(language_name) => language_name.to_lowercase(),
None => "plaintext".to_string(),
}
language
.map(|language| language.lsp_id())
.unwrap_or_else(|| "plaintext".to_string())
}
fn uri_for_buffer(buffer: &Model<Buffer>, cx: &AppContext) -> lsp::Url {

View File

@@ -1,10 +1,12 @@
use crate::{Completion, Copilot};
use anyhow::Result;
use client::telemetry::Telemetry;
use copilot::Copilot;
use editor::{Direction, InlineCompletionProvider};
use gpui::{AppContext, EntityId, Model, ModelContext, Task};
use language::language_settings::AllLanguageSettings;
use language::{language_settings::all_language_settings, Buffer, OffsetRangeExt, ToOffset};
use language::{
language_settings::{all_language_settings, AllLanguageSettings},
Buffer, OffsetRangeExt, ToOffset,
};
use settings::Settings;
use std::{path::Path, sync::Arc, time::Duration};
@@ -13,7 +15,7 @@ pub const COPILOT_DEBOUNCE_TIMEOUT: Duration = Duration::from_millis(75);
pub struct CopilotCompletionProvider {
cycled: bool,
buffer_id: Option<EntityId>,
completions: Vec<copilot::Completion>,
completions: Vec<Completion>,
active_completion_index: usize,
file_extension: Option<String>,
pending_refresh: Task<Result<()>>,
@@ -42,11 +44,11 @@ impl CopilotCompletionProvider {
self
}
fn active_completion(&self) -> Option<&copilot::Completion> {
fn active_completion(&self) -> Option<&Completion> {
self.completions.get(self.active_completion_index)
}
fn push_completion(&mut self, new_completion: copilot::Completion) {
fn push_completion(&mut self, new_completion: Completion) {
for completion in &self.completions {
if completion.text == new_completion.text && completion.range == new_completion.range {
return;
@@ -71,7 +73,7 @@ impl InlineCompletionProvider for CopilotCompletionProvider {
let file = buffer.file();
let language = buffer.language_at(cursor_position);
let settings = all_language_settings(file, cx);
settings.copilot_enabled(language.as_ref(), file.map(|f| f.path().as_ref()))
settings.inline_completions_enabled(language.as_ref(), file.map(|f| f.path().as_ref()))
}
fn refresh(
@@ -196,7 +198,10 @@ impl InlineCompletionProvider for CopilotCompletionProvider {
fn discard(&mut self, cx: &mut ModelContext<Self>) {
let settings = AllLanguageSettings::get_global(cx);
if !settings.copilot.feature_enabled {
let copilot_enabled = settings.inline_completions_enabled(None, None);
if !copilot_enabled {
return;
}
@@ -298,7 +303,9 @@ mod tests {
)
.await;
let copilot_provider = cx.new_model(|_| CopilotCompletionProvider::new(copilot));
cx.update_editor(|editor, cx| editor.set_inline_completion_provider(copilot_provider, cx));
cx.update_editor(|editor, cx| {
editor.set_inline_completion_provider(Some(copilot_provider), cx)
});
// When inserting, ensure autocompletion is favored over Copilot suggestions.
cx.set_state(indoc! {"
@@ -318,7 +325,7 @@ mod tests {
);
handle_copilot_completion_request(
&copilot_lsp,
vec![copilot::request::Completion {
vec![crate::request::Completion {
text: "one.copilot1".into(),
range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 4)),
..Default::default()
@@ -360,7 +367,7 @@ mod tests {
);
handle_copilot_completion_request(
&copilot_lsp,
vec![copilot::request::Completion {
vec![crate::request::Completion {
text: "one.copilot1".into(),
range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 4)),
..Default::default()
@@ -393,7 +400,7 @@ mod tests {
);
handle_copilot_completion_request(
&copilot_lsp,
vec![copilot::request::Completion {
vec![crate::request::Completion {
text: "one.copilot1".into(),
range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 4)),
..Default::default()
@@ -426,7 +433,7 @@ mod tests {
// After debouncing, new Copilot completions should be requested.
handle_copilot_completion_request(
&copilot_lsp,
vec![copilot::request::Completion {
vec![crate::request::Completion {
text: "one.copilot2".into(),
range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 5)),
..Default::default()
@@ -503,7 +510,7 @@ mod tests {
});
handle_copilot_completion_request(
&copilot_lsp,
vec![copilot::request::Completion {
vec![crate::request::Completion {
text: " let x = 4;".into(),
range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 2)),
..Default::default()
@@ -553,7 +560,9 @@ mod tests {
)
.await;
let copilot_provider = cx.new_model(|_| CopilotCompletionProvider::new(copilot));
cx.update_editor(|editor, cx| editor.set_inline_completion_provider(copilot_provider, cx));
cx.update_editor(|editor, cx| {
editor.set_inline_completion_provider(Some(copilot_provider), cx)
});
// Setup the editor with a completion request.
cx.set_state(indoc! {"
@@ -573,7 +582,7 @@ mod tests {
);
handle_copilot_completion_request(
&copilot_lsp,
vec![copilot::request::Completion {
vec![crate::request::Completion {
text: "one.copilot1".into(),
range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 4)),
..Default::default()
@@ -615,7 +624,7 @@ mod tests {
);
handle_copilot_completion_request(
&copilot_lsp,
vec![copilot::request::Completion {
vec![crate::request::Completion {
text: "one.123. copilot\n 456".into(),
range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 4)),
..Default::default()
@@ -675,7 +684,9 @@ mod tests {
)
.await;
let copilot_provider = cx.new_model(|_| CopilotCompletionProvider::new(copilot));
cx.update_editor(|editor, cx| editor.set_inline_completion_provider(copilot_provider, cx));
cx.update_editor(|editor, cx| {
editor.set_inline_completion_provider(Some(copilot_provider), cx)
});
cx.set_state(indoc! {"
one
@@ -685,7 +696,7 @@ mod tests {
handle_copilot_completion_request(
&copilot_lsp,
vec![copilot::request::Completion {
vec![crate::request::Completion {
text: "two.foo()".into(),
range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 2)),
..Default::default()
@@ -756,13 +767,13 @@ mod tests {
let copilot_provider = cx.new_model(|_| CopilotCompletionProvider::new(copilot));
editor
.update(cx, |editor, cx| {
editor.set_inline_completion_provider(copilot_provider, cx)
editor.set_inline_completion_provider(Some(copilot_provider), cx)
})
.unwrap();
handle_copilot_completion_request(
&copilot_lsp,
vec![copilot::request::Completion {
vec![crate::request::Completion {
text: "b = 2 + a".into(),
range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 5)),
..Default::default()
@@ -788,7 +799,7 @@ mod tests {
handle_copilot_completion_request(
&copilot_lsp,
vec![copilot::request::Completion {
vec![crate::request::Completion {
text: "d = 4 + c".into(),
range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 6)),
..Default::default()
@@ -829,11 +840,129 @@ mod tests {
});
}
#[gpui::test]
async fn test_copilot_does_not_prevent_completion_triggers(
executor: BackgroundExecutor,
cx: &mut TestAppContext,
) {
init_test(cx, |_| {});
let (copilot, copilot_lsp) = Copilot::fake(cx);
let mut cx = EditorLspTestContext::new_rust(
lsp::ServerCapabilities {
completion_provider: Some(lsp::CompletionOptions {
trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
..lsp::CompletionOptions::default()
}),
..lsp::ServerCapabilities::default()
},
cx,
)
.await;
let copilot_provider = cx.new_model(|_| CopilotCompletionProvider::new(copilot));
cx.update_editor(|editor, cx| {
editor.set_inline_completion_provider(Some(copilot_provider), cx)
});
cx.set_state(indoc! {"
one
twˇ
three
"});
let _ = handle_completion_request(
&mut cx,
indoc! {"
one
tw|<>
three
"},
vec!["completion_a", "completion_b"],
);
handle_copilot_completion_request(
&copilot_lsp,
vec![crate::request::Completion {
text: "two.foo()".into(),
range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 2)),
..Default::default()
}],
vec![],
);
cx.update_editor(|editor, cx| editor.next_inline_completion(&Default::default(), cx));
executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
cx.update_editor(|editor, cx| {
assert!(!editor.context_menu_visible(), "Even there are some completions available, those are not triggered when active copilot suggestion is present");
assert!(editor.has_active_inline_completion(cx));
assert_eq!(editor.display_text(cx), "one\ntwo.foo()\nthree\n");
assert_eq!(editor.text(cx), "one\ntw\nthree\n");
});
cx.simulate_keystroke("o");
let _ = handle_completion_request(
&mut cx,
indoc! {"
one
two|<>
three
"},
vec!["completion_a_2", "completion_b_2"],
);
handle_copilot_completion_request(
&copilot_lsp,
vec![crate::request::Completion {
text: "two.foo()".into(),
range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 3)),
..Default::default()
}],
vec![],
);
executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
cx.update_editor(|editor, cx| {
assert!(!editor.context_menu_visible());
assert!(editor.has_active_inline_completion(cx));
assert_eq!(editor.display_text(cx), "one\ntwo.foo()\nthree\n");
assert_eq!(editor.text(cx), "one\ntwo\nthree\n");
});
cx.simulate_keystroke(".");
let _ = handle_completion_request(
&mut cx,
indoc! {"
one
two.|<>
three
"},
vec!["something_else()"],
);
handle_copilot_completion_request(
&copilot_lsp,
vec![crate::request::Completion {
text: "two.foo()".into(),
range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 4)),
..Default::default()
}],
vec![],
);
executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
cx.update_editor(|editor, cx| {
assert!(
editor.context_menu_visible(),
"On completion trigger input, the completions should be fetched and visible"
);
assert!(
!editor.has_active_inline_completion(cx),
"On completion trigger input, copilot suggestion should be dismissed"
);
assert_eq!(editor.display_text(cx), "one\ntwo.\nthree\n");
assert_eq!(editor.text(cx), "one\ntwo.\nthree\n");
});
}
#[gpui::test]
async fn test_copilot_disabled_globs(executor: BackgroundExecutor, cx: &mut TestAppContext) {
init_test(cx, |settings| {
settings
.copilot
.inline_completions
.get_or_insert(Default::default())
.disabled_globs = Some(vec![".env*".to_string()]);
});
@@ -888,15 +1017,15 @@ mod tests {
let copilot_provider = cx.new_model(|_| CopilotCompletionProvider::new(copilot));
editor
.update(cx, |editor, cx| {
editor.set_inline_completion_provider(copilot_provider, cx)
editor.set_inline_completion_provider(Some(copilot_provider), cx)
})
.unwrap();
let mut copilot_requests = copilot_lsp
.handle_request::<copilot::request::GetCompletions, _, _>(
.handle_request::<crate::request::GetCompletions, _, _>(
move |_params, _cx| async move {
Ok(copilot::request::GetCompletionsResult {
completions: vec![copilot::request::Completion {
Ok(crate::request::GetCompletionsResult {
completions: vec![crate::request::Completion {
text: "next line".into(),
range: lsp::Range::new(
lsp::Position::new(1, 0),
@@ -931,21 +1060,21 @@ mod tests {
fn handle_copilot_completion_request(
lsp: &lsp::FakeLanguageServer,
completions: Vec<copilot::request::Completion>,
completions_cycling: Vec<copilot::request::Completion>,
completions: Vec<crate::request::Completion>,
completions_cycling: Vec<crate::request::Completion>,
) {
lsp.handle_request::<copilot::request::GetCompletions, _, _>(move |_params, _cx| {
lsp.handle_request::<crate::request::GetCompletions, _, _>(move |_params, _cx| {
let completions = completions.clone();
async move {
Ok(copilot::request::GetCompletionsResult {
Ok(crate::request::GetCompletionsResult {
completions: completions.clone(),
})
}
});
lsp.handle_request::<copilot::request::GetCompletionsCycling, _, _>(move |_params, _cx| {
lsp.handle_request::<crate::request::GetCompletionsCycling, _, _>(move |_params, _cx| {
let completions_cycling = completions_cycling.clone();
async move {
Ok(copilot::request::GetCompletionsResult {
Ok(crate::request::GetCompletionsResult {
completions: completions_cycling.clone(),
})
}

View File

@@ -1,4 +1,4 @@
use copilot::{request::PromptUserDeviceFlow, Copilot, Status};
use crate::{request::PromptUserDeviceFlow, Copilot, Status};
use gpui::{
div, svg, AppContext, ClipboardItem, DismissEvent, Element, EventEmitter, FocusHandle,
FocusableView, InteractiveElement, IntoElement, Model, MouseDownEvent, ParentElement, Render,
@@ -26,7 +26,7 @@ impl EventEmitter<DismissEvent> for CopilotCodeVerification {}
impl ModalView for CopilotCodeVerification {}
impl CopilotCodeVerification {
pub(crate) fn new(copilot: &Model<Copilot>, cx: &mut ViewContext<Self>) -> Self {
pub fn new(copilot: &Model<Copilot>, cx: &mut ViewContext<Self>) -> Self {
let status = copilot.read(cx).status();
Self {
status,
@@ -60,7 +60,7 @@ impl CopilotCodeVerification {
h_flex()
.w_full()
.p_1()
.border()
.border_1()
.border_muted(cx)
.rounded_md()
.cursor_pointer()

View File

@@ -1,403 +0,0 @@
use crate::sign_in::CopilotCodeVerification;
use anyhow::Result;
use copilot::{Copilot, SignOut, Status};
use editor::{scroll::Autoscroll, Editor};
use fs::Fs;
use gpui::{
div, Action, AnchorCorner, AppContext, AsyncWindowContext, Entity, IntoElement, ParentElement,
Render, Subscription, View, ViewContext, WeakView, WindowContext,
};
use language::{
language_settings::{self, all_language_settings, AllLanguageSettings},
File, Language,
};
use settings::{update_settings_file, Settings, SettingsStore};
use std::{path::Path, sync::Arc};
use util::{paths, ResultExt};
use workspace::notifications::NotificationId;
use workspace::{
create_and_open_local_file,
item::ItemHandle,
ui::{
popover_menu, ButtonCommon, Clickable, ContextMenu, IconButton, IconName, IconSize, Tooltip,
},
StatusItemView, Toast, Workspace,
};
use zed_actions::OpenBrowser;
const COPILOT_SETTINGS_URL: &str = "https://github.com/settings/copilot";
struct CopilotStartingToast;
struct CopilotErrorToast;
pub struct CopilotButton {
editor_subscription: Option<(Subscription, usize)>,
editor_enabled: Option<bool>,
language: Option<Arc<Language>>,
file: Option<Arc<dyn File>>,
fs: Arc<dyn Fs>,
}
impl Render for CopilotButton {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let all_language_settings = all_language_settings(None, cx);
if !all_language_settings.copilot.feature_enabled {
return div();
}
let Some(copilot) = Copilot::global(cx) else {
return div();
};
let status = copilot.read(cx).status();
let enabled = self
.editor_enabled
.unwrap_or_else(|| all_language_settings.copilot_enabled(None, None));
let icon = match status {
Status::Error(_) => IconName::CopilotError,
Status::Authorized => {
if enabled {
IconName::Copilot
} else {
IconName::CopilotDisabled
}
}
_ => IconName::CopilotInit,
};
if let Status::Error(e) = status {
return div().child(
IconButton::new("copilot-error", icon)
.icon_size(IconSize::Small)
.on_click(cx.listener(move |_, _, cx| {
if let Some(workspace) = cx.window_handle().downcast::<Workspace>() {
workspace
.update(cx, |workspace, cx| {
workspace.show_toast(
Toast::new(
NotificationId::unique::<CopilotErrorToast>(),
format!("Copilot can't be started: {}", e),
)
.on_click(
"Reinstall Copilot",
|cx| {
if let Some(copilot) = Copilot::global(cx) {
copilot
.update(cx, |copilot, cx| {
copilot.reinstall(cx)
})
.detach();
}
},
),
cx,
);
})
.ok();
}
}))
.tooltip(|cx| Tooltip::text("GitHub Copilot", cx)),
);
}
let this = cx.view().clone();
div().child(
popover_menu("copilot")
.menu(move |cx| match status {
Status::Authorized => {
Some(this.update(cx, |this, cx| this.build_copilot_menu(cx)))
}
_ => Some(this.update(cx, |this, cx| this.build_copilot_start_menu(cx))),
})
.anchor(AnchorCorner::BottomRight)
.trigger(
IconButton::new("copilot-icon", icon)
.tooltip(|cx| Tooltip::text("GitHub Copilot", cx)),
),
)
}
}
impl CopilotButton {
pub fn new(fs: Arc<dyn Fs>, cx: &mut ViewContext<Self>) -> Self {
if let Some(copilot) = Copilot::global(cx) {
cx.observe(&copilot, |_, _, cx| cx.notify()).detach()
}
cx.observe_global::<SettingsStore>(move |_, cx| cx.notify())
.detach();
Self {
editor_subscription: None,
editor_enabled: None,
language: None,
file: None,
fs,
}
}
pub fn build_copilot_start_menu(&mut self, cx: &mut ViewContext<Self>) -> View<ContextMenu> {
let fs = self.fs.clone();
ContextMenu::build(cx, |menu, _| {
menu.entry("Sign In", None, initiate_sign_in).entry(
"Disable Copilot",
None,
move |cx| hide_copilot(fs.clone(), cx),
)
})
}
pub fn build_copilot_menu(&mut self, cx: &mut ViewContext<Self>) -> View<ContextMenu> {
let fs = self.fs.clone();
ContextMenu::build(cx, move |mut menu, cx| {
if let Some(language) = self.language.clone() {
let fs = fs.clone();
let language_enabled =
language_settings::language_settings(Some(&language), None, cx)
.show_copilot_suggestions;
menu = menu.entry(
format!(
"{} Suggestions for {}",
if language_enabled { "Hide" } else { "Show" },
language.name()
),
None,
move |cx| toggle_copilot_for_language(language.clone(), fs.clone(), cx),
);
}
let settings = AllLanguageSettings::get_global(cx);
if let Some(file) = &self.file {
let path = file.path().clone();
let path_enabled = settings.copilot_enabled_for_path(&path);
menu = menu.entry(
format!(
"{} Suggestions for This Path",
if path_enabled { "Hide" } else { "Show" }
),
None,
move |cx| {
if let Some(workspace) = cx.window_handle().downcast::<Workspace>() {
if let Ok(workspace) = workspace.root_view(cx) {
let workspace = workspace.downgrade();
cx.spawn(|cx| {
configure_disabled_globs(
workspace,
path_enabled.then_some(path.clone()),
cx,
)
})
.detach_and_log_err(cx);
}
}
},
);
}
let globally_enabled = settings.copilot_enabled(None, None);
menu.entry(
if globally_enabled {
"Hide Suggestions for All Files"
} else {
"Show Suggestions for All Files"
},
None,
move |cx| toggle_copilot_globally(fs.clone(), cx),
)
.separator()
.link(
"Copilot Settings",
OpenBrowser {
url: COPILOT_SETTINGS_URL.to_string(),
}
.boxed_clone(),
)
.action("Sign Out", SignOut.boxed_clone())
})
}
pub fn update_enabled(&mut self, editor: View<Editor>, cx: &mut ViewContext<Self>) {
let editor = editor.read(cx);
let snapshot = editor.buffer().read(cx).snapshot(cx);
let suggestion_anchor = editor.selections.newest_anchor().start;
let language = snapshot.language_at(suggestion_anchor);
let file = snapshot.file_at(suggestion_anchor).cloned();
self.editor_enabled = {
let file = file.as_ref();
Some(
file.map(|file| !file.is_private()).unwrap_or(true)
&& all_language_settings(file, cx)
.copilot_enabled(language, file.map(|file| file.path().as_ref())),
)
};
self.language = language.cloned();
self.file = file;
cx.notify()
}
}
impl StatusItemView for CopilotButton {
fn set_active_pane_item(&mut self, item: Option<&dyn ItemHandle>, cx: &mut ViewContext<Self>) {
if let Some(editor) = item.and_then(|item| item.act_as::<Editor>(cx)) {
self.editor_subscription = Some((
cx.observe(&editor, Self::update_enabled),
editor.entity_id().as_u64() as usize,
));
self.update_enabled(editor, cx);
} else {
self.language = None;
self.editor_subscription = None;
self.editor_enabled = None;
}
cx.notify();
}
}
async fn configure_disabled_globs(
workspace: WeakView<Workspace>,
path_to_disable: Option<Arc<Path>>,
mut cx: AsyncWindowContext,
) -> Result<()> {
let settings_editor = workspace
.update(&mut cx, |_, cx| {
create_and_open_local_file(&paths::SETTINGS, cx, || {
settings::initial_user_settings_content().as_ref().into()
})
})?
.await?
.downcast::<Editor>()
.unwrap();
settings_editor.downgrade().update(&mut cx, |item, cx| {
let text = item.buffer().read(cx).snapshot(cx).text();
let settings = cx.global::<SettingsStore>();
let edits = settings.edits_for_update::<AllLanguageSettings>(&text, |file| {
let copilot = file.copilot.get_or_insert_with(Default::default);
let globs = copilot.disabled_globs.get_or_insert_with(|| {
settings
.get::<AllLanguageSettings>(None)
.copilot
.disabled_globs
.iter()
.map(|glob| glob.glob().to_string())
.collect()
});
if let Some(path_to_disable) = &path_to_disable {
globs.push(path_to_disable.to_string_lossy().into_owned());
} else {
globs.clear();
}
});
if !edits.is_empty() {
item.change_selections(Some(Autoscroll::newest()), cx, |selections| {
selections.select_ranges(edits.iter().map(|e| e.0.clone()));
});
// When *enabling* a path, don't actually perform an edit, just select the range.
if path_to_disable.is_some() {
item.edit(edits.iter().cloned(), cx);
}
}
})?;
anyhow::Ok(())
}
fn toggle_copilot_globally(fs: Arc<dyn Fs>, cx: &mut AppContext) {
let show_copilot_suggestions = all_language_settings(None, cx).copilot_enabled(None, None);
update_settings_file::<AllLanguageSettings>(fs, cx, move |file| {
file.defaults.show_copilot_suggestions = Some(!show_copilot_suggestions)
});
}
fn toggle_copilot_for_language(language: Arc<Language>, fs: Arc<dyn Fs>, cx: &mut AppContext) {
let show_copilot_suggestions =
all_language_settings(None, cx).copilot_enabled(Some(&language), None);
update_settings_file::<AllLanguageSettings>(fs, cx, move |file| {
file.languages
.entry(language.name())
.or_default()
.show_copilot_suggestions = Some(!show_copilot_suggestions);
});
}
fn hide_copilot(fs: Arc<dyn Fs>, cx: &mut AppContext) {
update_settings_file::<AllLanguageSettings>(fs, cx, move |file| {
file.features.get_or_insert(Default::default()).copilot = Some(false);
});
}
pub fn initiate_sign_in(cx: &mut WindowContext) {
let Some(copilot) = Copilot::global(cx) else {
return;
};
let status = copilot.read(cx).status();
let Some(workspace) = cx.window_handle().downcast::<Workspace>() else {
return;
};
match status {
Status::Starting { task } => {
let Some(workspace) = cx.window_handle().downcast::<Workspace>() else {
return;
};
let Ok(workspace) = workspace.update(cx, |workspace, cx| {
workspace.show_toast(
Toast::new(
NotificationId::unique::<CopilotStartingToast>(),
"Copilot is starting...",
),
cx,
);
workspace.weak_handle()
}) else {
return;
};
cx.spawn(|mut cx| async move {
task.await;
if let Some(copilot) = cx.update(|cx| Copilot::global(cx)).ok().flatten() {
workspace
.update(&mut cx, |workspace, cx| match copilot.read(cx).status() {
Status::Authorized => workspace.show_toast(
Toast::new(
NotificationId::unique::<CopilotStartingToast>(),
"Copilot has started!",
),
cx,
),
_ => {
workspace.dismiss_toast(
&NotificationId::unique::<CopilotStartingToast>(),
cx,
);
copilot
.update(cx, |copilot, cx| copilot.sign_in(cx))
.detach_and_log_err(cx);
}
})
.log_err();
}
})
.detach();
}
_ => {
copilot.update(cx, |this, cx| this.sign_in(cx)).detach();
workspace
.update(cx, |this, cx| {
this.toggle_modal(cx, |cx| CopilotCodeVerification::new(&copilot, cx));
})
.ok();
}
}
}

View File

@@ -1,7 +0,0 @@
pub mod copilot_button;
mod copilot_completion_provider;
mod sign_in;
pub use copilot_button::*;
pub use copilot_completion_provider::*;
pub use sign_in::*;

View File

@@ -173,6 +173,39 @@ impl Store {
})
}
pub fn rename_dev_server(
&mut self,
dev_server_id: DevServerId,
name: String,
cx: &mut ModelContext<Self>,
) -> Task<Result<()>> {
let client = self.client.clone();
cx.background_executor().spawn(async move {
client
.request(proto::RenameDevServer {
dev_server_id: dev_server_id.0,
name,
})
.await?;
Ok(())
})
}
pub fn regenerate_dev_server_token(
&mut self,
dev_server_id: DevServerId,
cx: &mut ModelContext<Self>,
) -> Task<Result<proto::RegenerateDevServerTokenResponse>> {
let client = self.client.clone();
cx.background_executor().spawn(async move {
client
.request(proto::RegenerateDevServerToken {
dev_server_id: dev_server_id.0,
})
.await
})
}
pub fn delete_dev_server(
&mut self,
id: DevServerId,
@@ -188,4 +221,20 @@ impl Store {
Ok(())
})
}
pub fn delete_dev_server_project(
&mut self,
id: DevServerProjectId,
cx: &mut ModelContext<Self>,
) -> Task<Result<()>> {
let client = self.client.clone();
cx.background_executor().spawn(async move {
client
.request(proto::DeleteDevServerProject {
dev_server_project_id: id.0,
})
.await?;
Ok(())
})
}
}

View File

@@ -60,6 +60,7 @@ smallvec.workspace = true
smol.workspace = true
snippet.workspace = true
sum_tree.workspace = true
task.workspace = true
text.workspace = true
time.workspace = true
time_format.workspace = true

View File

@@ -53,7 +53,13 @@ pub struct SelectToEndOfLine {
#[derive(PartialEq, Clone, Deserialize, Default)]
pub struct ToggleCodeActions {
#[serde(default)]
pub deployed_from_indicator: bool,
pub deployed_from_indicator: Option<u32>,
}
#[derive(PartialEq, Clone, Deserialize, Default)]
pub struct ToggleTestRunner {
#[serde(default)]
pub deployed_from_row: Option<u32>,
}
#[derive(PartialEq, Clone, Deserialize, Default)]

View File

@@ -61,7 +61,7 @@ struct CommitAvatarAsset {
impl Hash for CommitAvatarAsset {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.sha.hash(state);
self.remote.host.hash(state);
self.remote.host.name().hash(state);
}
}

View File

@@ -6,7 +6,6 @@ use gpui::{ElementId, HighlightStyle, Hsla};
use language::{Chunk, Edit, Point, TextSummary};
use multi_buffer::{Anchor, AnchorRangeExt, MultiBufferSnapshot, ToOffset};
use std::{
any::TypeId,
cmp::{self, Ordering},
iter,
ops::{Add, AddAssign, Deref, DerefMut, Range, Sub},
@@ -1066,28 +1065,6 @@ impl<'a> Iterator for FoldChunks<'a> {
}
}
#[derive(Copy, Clone, Eq, PartialEq)]
struct HighlightEndpoint {
offset: InlayOffset,
is_start: bool,
tag: Option<TypeId>,
style: HighlightStyle,
}
impl PartialOrd for HighlightEndpoint {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Ord for HighlightEndpoint {
fn cmp(&self, other: &Self) -> Ordering {
self.offset
.cmp(&other.offset)
.then_with(|| other.is_start.cmp(&self.is_start))
}
}
#[derive(Copy, Clone, Debug, Default, Eq, Ord, PartialOrd, PartialEq)]
pub struct FoldOffset(pub usize);

View File

@@ -34,13 +34,14 @@ mod persistence;
mod rust_analyzer_ext;
pub mod scroll;
mod selections_collection;
pub mod tasks;
#[cfg(test)]
mod editor_tests;
#[cfg(any(test, feature = "test-support"))]
pub mod test;
use ::git::diff::{DiffHunk, DiffHunkStatus};
use ::git::permalink::{build_permalink, BuildPermalinkParams};
use ::git::{parse_git_remote_url, BuildPermalinkParams, GitHostingProviderRegistry};
pub(crate) use actions::*;
use aho_corasick::AhoCorasick;
use anyhow::{anyhow, Context as _, Result};
@@ -78,6 +79,7 @@ use inlay_hint_cache::{InlayHintCache, InlaySplice, InvalidationStrategy};
pub use inline_completion_provider::*;
pub use items::MAX_TAB_TITLE_LEN;
use itertools::Itertools;
use language::Runnable;
use language::{
char_kind,
language_settings::{self, all_language_settings, InlayHintSettings},
@@ -85,6 +87,7 @@ use language::{
CursorShape, Diagnostic, Documentation, IndentKind, IndentSize, Language, OffsetRangeExt,
Point, Selection, SelectionGoal, TransactionId,
};
use task::{ResolvedTask, TaskTemplate};
use hover_links::{HoverLink, HoveredLinkState, InlayHighlight};
use lsp::{DiagnosticSeverity, LanguageServerId};
@@ -99,7 +102,8 @@ use ordered_float::OrderedFloat;
use parking_lot::{Mutex, RwLock};
use project::project_settings::{GitGutterSetting, ProjectSettings};
use project::{
CodeAction, Completion, FormatTrigger, Item, Location, Project, ProjectPath, ProjectTransaction,
CodeAction, Completion, FormatTrigger, Item, Location, Project, ProjectPath,
ProjectTransaction, TaskSourceKind, WorktreeId,
};
use rand::prelude::*;
use rpc::{proto::*, ErrorExt};
@@ -395,6 +399,19 @@ impl Default for ScrollbarMarkerState {
}
}
#[derive(Clone, Debug)]
struct RunnableTasks {
templates: Vec<(TaskSourceKind, TaskTemplate)>,
// We need the column at which the task context evaluation should take place.
column: u32,
}
#[derive(Clone)]
struct ResolvedTasks {
templates: SmallVec<[(TaskSourceKind, ResolvedTask); 1]>,
position: text::Point,
}
/// Zed's primary text input `View`, allowing users to edit a [`MultiBuffer`]
///
/// See the [module level documentation](self) for more information.
@@ -487,6 +504,8 @@ pub struct Editor {
>,
last_bounds: Option<Bounds<Pixels>>,
expect_bounds_change: Option<Bounds<Pixels>>,
tasks: HashMap<u32, RunnableTasks>,
tasks_update_task: Option<Task<()>>,
}
#[derive(Clone)]
@@ -1167,7 +1186,7 @@ impl CompletionsMenu {
for mat in &mut matches {
let completion = &completions[mat.candidate_id];
mat.string = completion.label.text.clone();
mat.string.clone_from(&completion.label.text);
for position in &mut mat.positions {
*position += completion.label.filter_range.start;
}
@@ -1180,12 +1199,106 @@ impl CompletionsMenu {
}
#[derive(Clone)]
struct CodeActionContents {
tasks: Option<Arc<ResolvedTasks>>,
actions: Option<Arc<[CodeAction]>>,
}
impl CodeActionContents {
fn len(&self) -> usize {
match (&self.tasks, &self.actions) {
(Some(tasks), Some(actions)) => actions.len() + tasks.templates.len(),
(Some(tasks), None) => tasks.templates.len(),
(None, Some(actions)) => actions.len(),
(None, None) => 0,
}
}
fn is_empty(&self) -> bool {
match (&self.tasks, &self.actions) {
(Some(tasks), Some(actions)) => actions.is_empty() && tasks.templates.is_empty(),
(Some(tasks), None) => tasks.templates.is_empty(),
(None, Some(actions)) => actions.is_empty(),
(None, None) => true,
}
}
fn iter(&self) -> impl Iterator<Item = CodeActionsItem> + '_ {
self.tasks
.iter()
.flat_map(|tasks| {
tasks
.templates
.iter()
.map(|(kind, task)| CodeActionsItem::Task(kind.clone(), task.clone()))
})
.chain(self.actions.iter().flat_map(|actions| {
actions
.iter()
.map(|action| CodeActionsItem::CodeAction(action.clone()))
}))
}
fn get(&self, index: usize) -> Option<CodeActionsItem> {
match (&self.tasks, &self.actions) {
(Some(tasks), Some(actions)) => {
if index < tasks.templates.len() {
tasks
.templates
.get(index)
.cloned()
.map(|(kind, task)| CodeActionsItem::Task(kind, task))
} else {
actions
.get(index - tasks.templates.len())
.cloned()
.map(CodeActionsItem::CodeAction)
}
}
(Some(tasks), None) => tasks
.templates
.get(index)
.cloned()
.map(|(kind, task)| CodeActionsItem::Task(kind, task)),
(None, Some(actions)) => actions.get(index).cloned().map(CodeActionsItem::CodeAction),
(None, None) => None,
}
}
}
#[allow(clippy::large_enum_variant)]
#[derive(Clone)]
enum CodeActionsItem {
Task(TaskSourceKind, ResolvedTask),
CodeAction(CodeAction),
}
impl CodeActionsItem {
fn as_task(&self) -> Option<&ResolvedTask> {
let Self::Task(_, task) = self else {
return None;
};
Some(task)
}
fn as_code_action(&self) -> Option<&CodeAction> {
let Self::CodeAction(action) = self else {
return None;
};
Some(action)
}
fn label(&self) -> String {
match self {
Self::CodeAction(action) => action.lsp_action.title.clone(),
Self::Task(_, task) => task.resolved_label.clone(),
}
}
}
struct CodeActionsMenu {
actions: Arc<[CodeAction]>,
actions: CodeActionContents,
buffer: Model<Buffer>,
selected_item: usize,
scroll_handle: UniformListScrollHandle,
deployed_from_indicator: bool,
deployed_from_indicator: Option<u32>,
}
impl CodeActionsMenu {
@@ -1234,14 +1347,15 @@ impl CodeActionsMenu {
) -> (ContextMenuOrigin, AnyElement) {
let actions = self.actions.clone();
let selected_item = self.selected_item;
let element = uniform_list(
cx.view().clone(),
"code_actions_menu",
self.actions.len(),
move |_this, range, cx| {
actions[range.clone()]
actions
.iter()
.skip(range.start)
.take(range.end - range.start)
.enumerate()
.map(|(ix, action)| {
let item_ix = range.start + ix;
@@ -1260,23 +1374,42 @@ impl CodeActionsMenu {
.bg(colors.element_hover)
.text_color(colors.text_accent)
})
.on_mouse_down(
MouseButton::Left,
cx.listener(move |editor, _, cx| {
cx.stop_propagation();
if let Some(task) = editor.confirm_code_action(
&ConfirmCodeAction {
item_ix: Some(item_ix),
},
cx,
) {
task.detach_and_log_err(cx)
}
}),
)
.whitespace_nowrap()
// TASK: It would be good to make lsp_action.title a SharedString to avoid allocating here.
.child(SharedString::from(action.lsp_action.title.clone()))
.when_some(action.as_code_action(), |this, action| {
this.on_mouse_down(
MouseButton::Left,
cx.listener(move |editor, _, cx| {
cx.stop_propagation();
if let Some(task) = editor.confirm_code_action(
&ConfirmCodeAction {
item_ix: Some(item_ix),
},
cx,
) {
task.detach_and_log_err(cx)
}
}),
)
// TASK: It would be good to make lsp_action.title a SharedString to avoid allocating here.
.child(SharedString::from(action.lsp_action.title.clone()))
})
.when_some(action.as_task(), |this, task| {
this.on_mouse_down(
MouseButton::Left,
cx.listener(move |editor, _, cx| {
cx.stop_propagation();
if let Some(task) = editor.confirm_code_action(
&ConfirmCodeAction {
item_ix: Some(item_ix),
},
cx,
) {
task.detach_and_log_err(cx)
}
}),
)
.child(SharedString::from(task.resolved_label.clone()))
})
})
.collect()
},
@@ -1291,16 +1424,20 @@ impl CodeActionsMenu {
self.actions
.iter()
.enumerate()
.max_by_key(|(_, action)| action.lsp_action.title.chars().count())
.max_by_key(|(_, action)| match action {
CodeActionsItem::Task(_, task) => task.resolved_label.chars().count(),
CodeActionsItem::CodeAction(action) => action.lsp_action.title.chars().count(),
})
.map(|(ix, _)| ix),
)
.into_any_element();
let cursor_position = if self.deployed_from_indicator {
ContextMenuOrigin::GutterIndicator(cursor_position.row())
let cursor_position = if let Some(row) = self.deployed_from_indicator {
ContextMenuOrigin::GutterIndicator(row)
} else {
ContextMenuOrigin::EditorPoint(cursor_position)
};
(cursor_position, element)
}
}
@@ -1532,6 +1669,7 @@ impl Editor {
git_blame_inline_enabled: ProjectSettings::get_global(cx).git.inline_blame_enabled(),
blame: None,
blame_subscription: None,
tasks: Default::default(),
_subscriptions: vec![
cx.observe(&buffer, Self::on_buffer_changed),
cx.subscribe(&buffer, Self::on_buffer_event),
@@ -1551,8 +1689,9 @@ impl Editor {
});
}),
],
tasks_update_task: None,
};
this.tasks_update_task = Some(this.refresh_runnables(cx));
this._subscriptions.extend(project_subscriptions);
this.end_selection(cx);
@@ -1757,19 +1896,22 @@ impl Editor {
self.completion_provider = Some(hub);
}
pub fn set_inline_completion_provider(
pub fn set_inline_completion_provider<T>(
&mut self,
provider: Model<impl InlineCompletionProvider>,
provider: Option<Model<T>>,
cx: &mut ViewContext<Self>,
) {
self.inline_completion_provider = Some(RegisteredInlineCompletionProvider {
_subscription: cx.observe(&provider, |this, _, cx| {
if this.focus_handle.is_focused(cx) {
this.update_visible_inline_completion(cx);
}
}),
provider: Arc::new(provider),
});
) where
T: InlineCompletionProvider,
{
self.inline_completion_provider =
provider.map(|provider| RegisteredInlineCompletionProvider {
_subscription: cx.observe(&provider, |this, _, cx| {
if this.focus_handle.is_focused(cx) {
this.update_visible_inline_completion(cx);
}
}),
provider: Arc::new(provider),
});
self.refresh_inline_completion(false, cx);
}
@@ -2676,7 +2818,7 @@ impl Editor {
}
drop(snapshot);
let had_active_copilot_completion = this.has_active_inline_completion(cx);
let had_active_inline_completion = this.has_active_inline_completion(cx);
this.change_selections(Some(Autoscroll::fit()), cx, |s| s.select(new_selections));
if brace_inserted {
@@ -2692,15 +2834,9 @@ impl Editor {
}
}
if had_active_copilot_completion {
this.refresh_inline_completion(true, cx);
if !this.has_active_inline_completion(cx) {
this.trigger_completion_on_input(&text, cx);
}
} else {
this.trigger_completion_on_input(&text, cx);
this.refresh_inline_completion(true, cx);
}
let trigger_in_words = !had_active_inline_completion;
this.trigger_completion_on_input(&text, trigger_in_words, cx);
this.refresh_inline_completion(true, cx);
});
}
@@ -2765,7 +2901,7 @@ impl Editor {
indent.len = cmp::min(indent.len, start_point.column);
let start = selection.start;
let end = selection.end;
let is_cursor = start == end;
let selection_is_empty = start == end;
let language_scope = buffer.language_scope_at(start);
let (comment_delimiter, insert_extra_newline) = if let Some(language) =
&language_scope
@@ -2799,13 +2935,18 @@ impl Editor {
pair_start,
)
});
// Comment extension on newline is allowed only for cursor selections
let comment_delimiter = language.line_comment_prefixes().filter(|_| {
let is_comment_extension_enabled =
multi_buffer.settings_at(0, cx).extend_comment_on_newline;
is_cursor && is_comment_extension_enabled
});
let get_comment_delimiter = |delimiters: &[Arc<str>]| {
let comment_delimiter = maybe!({
if !selection_is_empty {
return None;
}
if !multi_buffer.settings_at(0, cx).extend_comment_on_newline {
return None;
}
let delimiters = language.line_comment_prefixes();
let max_len_of_delimiter =
delimiters.iter().map(|delimiter| delimiter.len()).max()?;
let (snapshot, range) =
@@ -2834,12 +2975,7 @@ impl Editor {
} else {
None
}
};
let comment_delimiter = if let Some(delimiters) = comment_delimiter {
get_comment_delimiter(delimiters)
} else {
None
};
});
(comment_delimiter, insert_extra_newline)
} else {
(None, false)
@@ -3053,7 +3189,12 @@ impl Editor {
});
}
fn trigger_completion_on_input(&mut self, text: &str, cx: &mut ViewContext<Self>) {
fn trigger_completion_on_input(
&mut self,
text: &str,
trigger_in_words: bool,
cx: &mut ViewContext<Self>,
) {
if !EditorSettings::get_global(cx).show_completions_on_input {
return;
}
@@ -3062,7 +3203,7 @@ impl Editor {
if self
.buffer
.read(cx)
.is_completion_trigger(selection.head(), text, cx)
.is_completion_trigger(selection.head(), text, trigger_in_words, cx)
{
self.show_completions(&ShowCompletions, cx);
} else {
@@ -3685,38 +3826,130 @@ impl Editor {
pub fn toggle_code_actions(&mut self, action: &ToggleCodeActions, cx: &mut ViewContext<Self>) {
let mut context_menu = self.context_menu.write();
if matches!(context_menu.as_ref(), Some(ContextMenu::CodeActions(_))) {
*context_menu = None;
cx.notify();
return;
if let Some(ContextMenu::CodeActions(code_actions)) = context_menu.as_ref() {
if code_actions.deployed_from_indicator == action.deployed_from_indicator {
// Toggle if we're selecting the same one
*context_menu = None;
cx.notify();
return;
} else {
// Otherwise, clear it and start a new one
*context_menu = None;
cx.notify();
}
}
drop(context_menu);
let deployed_from_indicator = action.deployed_from_indicator;
let mut task = self.code_actions_task.take();
let action = action.clone();
cx.spawn(|this, mut cx| async move {
while let Some(prev_task) = task {
prev_task.await;
task = this.update(&mut cx, |this, _| this.code_actions_task.take())?;
}
this.update(&mut cx, |this, cx| {
let spawned_test_task = this.update(&mut cx, |this, cx| {
if this.focus_handle.is_focused(cx) {
if let Some((buffer, actions)) = this.available_code_actions.clone() {
this.completion_tasks.clear();
this.discard_inline_completion(cx);
*this.context_menu.write() =
Some(ContextMenu::CodeActions(CodeActionsMenu {
buffer,
actions,
selected_item: Default::default(),
scroll_handle: UniformListScrollHandle::default(),
deployed_from_indicator,
}));
cx.notify();
let snapshot = this.snapshot(cx);
let display_row = action.deployed_from_indicator.unwrap_or_else(|| {
this.selections
.newest::<Point>(cx)
.head()
.to_display_point(&snapshot.display_snapshot)
.row()
});
let buffer_point =
DisplayPoint::new(display_row, 0).to_point(&snapshot.display_snapshot);
let buffer_row = snapshot
.buffer_snapshot
.buffer_line_for_row(buffer_point.row)
.map(|(_, Range { start, .. })| start);
let tasks = this.tasks.get(&display_row).map(|t| Arc::new(t.to_owned()));
let (buffer, code_actions) = this.available_code_actions.clone().unzip();
if tasks.is_none() && code_actions.is_none() {
return None;
}
let buffer = buffer.or_else(|| {
let snapshot = this.snapshot(cx);
let (buffer_snapshot, _) =
snapshot.buffer_snapshot.buffer_line_for_row(display_row)?;
let buffer_id = buffer_snapshot.remote_id();
this.buffer().read(cx).buffer(buffer_id)
});
let Some(buffer) = buffer else {
return None;
};
this.completion_tasks.clear();
this.discard_inline_completion(cx);
let task_context = tasks.as_ref().zip(this.workspace.clone()).and_then(
|(tasks, (workspace, _))| {
if let Some(buffer_point) = buffer_row {
let position = Point::new(buffer_point.row, tasks.column);
let range_start = buffer.read(cx).anchor_at(position, Bias::Right);
let location = Location {
buffer: buffer.clone(),
range: range_start..range_start,
};
workspace
.update(cx, |workspace, cx| {
tasks::task_context_for_location(workspace, location, cx)
})
.ok()
.flatten()
} else {
None
}
},
);
let tasks = tasks
.zip(task_context.as_ref())
.map(|(tasks, task_context)| {
Arc::new(ResolvedTasks {
templates: tasks
.templates
.iter()
.filter_map(|(kind, template)| {
template
.resolve_task(&kind.to_id_base(), &task_context)
.map(|task| (kind.clone(), task))
})
.collect(),
position: Point::new(display_row, tasks.column),
})
});
let spawn_straight_away = tasks
.as_ref()
.map_or(false, |tasks| tasks.templates.len() == 1)
&& code_actions
.as_ref()
.map_or(true, |actions| actions.is_empty());
*this.context_menu.write() = Some(ContextMenu::CodeActions(CodeActionsMenu {
buffer,
actions: CodeActionContents {
tasks,
actions: code_actions,
},
selected_item: Default::default(),
scroll_handle: UniformListScrollHandle::default(),
deployed_from_indicator,
}));
if spawn_straight_away {
if let Some(task) =
this.confirm_code_action(&ConfirmCodeAction { item_ix: Some(0) }, cx)
{
cx.notify();
return Some(task);
}
}
cx.notify();
}
Some(Task::ready(Ok(())))
})?;
if let Some(task) = spawned_test_task {
task.await?;
}
Ok::<_, anyhow::Error>(())
})
@@ -3734,23 +3967,47 @@ impl Editor {
return None;
};
let action_ix = action.item_ix.unwrap_or(actions_menu.selected_item);
let action = actions_menu.actions.get(action_ix)?.clone();
let title = action.lsp_action.title.clone();
let action = actions_menu.actions.get(action_ix)?;
let title = action.label();
let buffer = actions_menu.buffer;
let workspace = self.workspace()?;
let apply_code_actions = workspace
.read(cx)
.project()
.clone()
.update(cx, |project, cx| {
project.apply_code_action(buffer, action, true, cx)
});
let workspace = workspace.downgrade();
Some(cx.spawn(|editor, cx| async move {
let project_transaction = apply_code_actions.await?;
Self::open_project_transaction(&editor, workspace, project_transaction, title, cx).await
}))
match action {
CodeActionsItem::Task(task_source_kind, resolved_task) => {
workspace.update(cx, |workspace, cx| {
workspace::tasks::schedule_resolved_task(
workspace,
task_source_kind,
resolved_task,
false,
cx,
);
None
})
}
CodeActionsItem::CodeAction(action) => {
let apply_code_actions = workspace
.read(cx)
.project()
.clone()
.update(cx, |project, cx| {
project.apply_code_action(buffer, action, true, cx)
});
let workspace = workspace.downgrade();
Some(cx.spawn(|editor, cx| async move {
let project_transaction = apply_code_actions.await?;
Self::open_project_transaction(
&editor,
workspace,
project_transaction,
title,
cx,
)
.await
}))
}
}
}
async fn open_project_transaction(
@@ -4005,7 +4262,7 @@ impl Editor {
if !self.show_inline_completions
|| !provider.is_enabled(&buffer, cursor_buffer_position, cx)
{
self.clear_inline_completion(cx);
self.discard_inline_completion(cx);
return None;
}
@@ -4207,20 +4464,14 @@ impl Editor {
self.discard_inline_completion(cx);
}
fn clear_inline_completion(&mut self, cx: &mut ViewContext<Self>) {
if let Some(old_completion) = self.active_inline_completion.take() {
self.splice_inlays(vec![old_completion.id], Vec::new(), cx);
}
self.discard_inline_completion(cx);
}
fn inline_completion_provider(&self) -> Option<Arc<dyn InlineCompletionProviderHandle>> {
Some(self.inline_completion_provider.as_ref()?.provider.clone())
}
pub fn render_code_actions_indicator(
fn render_code_actions_indicator(
&self,
_style: &EditorStyle,
row: u32,
is_active: bool,
cx: &mut ViewContext<Self>,
) -> Option<IconButton> {
@@ -4231,10 +4482,10 @@ impl Editor {
.size(ui::ButtonSize::None)
.icon_color(Color::Muted)
.selected(is_active)
.on_click(cx.listener(|editor, _e, cx| {
.on_click(cx.listener(move |editor, _e, cx| {
editor.toggle_code_actions(
&ToggleCodeActions {
deployed_from_indicator: true,
deployed_from_indicator: Some(row),
},
cx,
);
@@ -4245,6 +4496,39 @@ impl Editor {
}
}
fn clear_tasks(&mut self) {
self.tasks.clear()
}
fn insert_tasks(&mut self, row: u32, tasks: RunnableTasks) {
if let Some(_) = self.tasks.insert(row, tasks) {
// This case should hopefully be rare, but just in case...
log::error!("multiple different run targets found on a single line, only the last target will be rendered")
}
}
fn render_run_indicator(
&self,
_style: &EditorStyle,
is_active: bool,
row: u32,
cx: &mut ViewContext<Self>,
) -> IconButton {
IconButton::new("code_actions_indicator", ui::IconName::Play)
.icon_size(IconSize::XSmall)
.size(ui::ButtonSize::None)
.icon_color(Color::Muted)
.selected(is_active)
.on_click(cx.listener(move |editor, _e, cx| {
editor.toggle_code_actions(
&ToggleCodeActions {
deployed_from_indicator: Some(row),
},
cx,
);
}))
}
pub fn render_fold_indicators(
&mut self,
fold_data: Vec<Option<(FoldStatus, u32, bool)>>,
@@ -4977,10 +5261,16 @@ impl Editor {
if !revert_changes.is_empty() {
self.transact(cx, |editor, cx| {
editor.buffer().update(cx, |multi_buffer, cx| {
for (buffer_id, buffer_revert_ranges) in revert_changes {
for (buffer_id, changes) in revert_changes {
if let Some(buffer) = multi_buffer.buffer(buffer_id) {
buffer.update(cx, |buffer, cx| {
buffer.edit(buffer_revert_ranges, None, cx);
buffer.edit(
changes.into_iter().map(|(range, text)| {
(range, text.to_string().map(Arc::<str>::from))
}),
None,
cx,
);
});
}
}
@@ -5013,7 +5303,7 @@ impl Editor {
&mut self,
selections: &[Selection<Anchor>],
cx: &mut ViewContext<'_, Editor>,
) -> HashMap<BufferId, Vec<(Range<text::Anchor>, Arc<str>)>> {
) -> HashMap<BufferId, Vec<(Range<text::Anchor>, Rope)>> {
let mut revert_changes = HashMap::default();
self.buffer.update(cx, |multi_buffer, cx| {
let multi_buffer_snapshot = multi_buffer.snapshot(cx);
@@ -5025,14 +5315,14 @@ impl Editor {
}
fn prepare_revert_change(
revert_changes: &mut HashMap<BufferId, Vec<(Range<text::Anchor>, Arc<str>)>>,
revert_changes: &mut HashMap<BufferId, Vec<(Range<text::Anchor>, Rope)>>,
multi_buffer: &MultiBuffer,
hunk: &DiffHunk<u32>,
cx: &mut AppContext,
) -> Option<()> {
let buffer = multi_buffer.buffer(hunk.buffer_id)?;
let buffer = buffer.read(cx);
let original_text = buffer.diff_base()?.get(hunk.diff_base_byte_range.clone())?;
let original_text = buffer.diff_base()?.slice(hunk.diff_base_byte_range.clone());
let buffer_snapshot = buffer.snapshot();
let buffer_revert_changes = revert_changes.entry(buffer.remote_id()).or_default();
if let Err(i) = buffer_revert_changes.binary_search_by(|probe| {
@@ -5041,9 +5331,8 @@ impl Editor {
.start
.cmp(&hunk.buffer_range.start, &buffer_snapshot)
.then(probe.0.end.cmp(&hunk.buffer_range.end, &buffer_snapshot))
.then(probe.1.as_ref().cmp(original_text))
}) {
buffer_revert_changes.insert(i, (hunk.buffer_range.clone(), Arc::from(original_text)));
buffer_revert_changes.insert(i, (hunk.buffer_range.clone(), original_text));
Some(())
} else {
None
@@ -7180,10 +7469,8 @@ impl Editor {
}
// If the language has line comments, toggle those.
if let Some(full_comment_prefixes) = language
.line_comment_prefixes()
.filter(|prefixes| !prefixes.is_empty())
{
let full_comment_prefixes = language.line_comment_prefixes();
if !full_comment_prefixes.is_empty() {
let first_prefix = full_comment_prefixes
.first()
.expect("prefixes is non-empty");
@@ -7402,6 +7689,127 @@ impl Editor {
self.select_larger_syntax_node_stack = stack;
}
fn refresh_runnables(&mut self, cx: &mut ViewContext<Self>) -> Task<()> {
let project = self.project.clone();
cx.spawn(|this, mut cx| async move {
let Ok(display_snapshot) = this.update(&mut cx, |this, cx| {
this.display_map.update(cx, |map, cx| map.snapshot(cx))
}) else {
return;
};
let Some(project) = project else {
return;
};
if project
.update(&mut cx, |this, _| this.is_remote())
.unwrap_or(true)
{
// Do not display any test indicators in remote projects.
return;
}
let new_rows =
cx.background_executor()
.spawn({
let snapshot = display_snapshot.clone();
async move {
Self::fetch_runnable_ranges(&snapshot, Anchor::min()..Anchor::max())
}
})
.await;
let rows = Self::refresh_runnable_display_rows(
project,
display_snapshot,
new_rows,
cx.clone(),
);
this.update(&mut cx, |this, _| {
this.clear_tasks();
for (row, tasks) in rows {
this.insert_tasks(row, tasks);
}
})
.ok();
})
}
fn fetch_runnable_ranges(
snapshot: &DisplaySnapshot,
range: Range<Anchor>,
) -> Vec<(Range<usize>, Runnable)> {
snapshot.buffer_snapshot.runnable_ranges(range).collect()
}
fn refresh_runnable_display_rows(
project: Model<Project>,
snapshot: DisplaySnapshot,
runnable_ranges: Vec<(Range<usize>, Runnable)>,
mut cx: AsyncWindowContext,
) -> Vec<(u32, RunnableTasks)> {
runnable_ranges
.into_iter()
.filter_map(|(multi_buffer_range, mut runnable)| {
let (tasks, _) = cx
.update(|cx| Self::resolve_runnable(project.clone(), &mut runnable, cx))
.ok()?;
if tasks.is_empty() {
return None;
}
let point = multi_buffer_range.start.to_display_point(&snapshot);
Some((
point.row(),
RunnableTasks {
templates: tasks,
column: point.column(),
},
))
})
.collect()
}
fn resolve_runnable(
project: Model<Project>,
runnable: &mut Runnable,
cx: &WindowContext<'_>,
) -> (Vec<(TaskSourceKind, TaskTemplate)>, Option<WorktreeId>) {
let (inventory, worktree_id) = project.read_with(cx, |project, cx| {
let worktree_id = project
.buffer_for_id(runnable.buffer)
.and_then(|buffer| buffer.read(cx).file())
.map(|file| WorktreeId::from_usize(file.worktree_id()));
(project.task_inventory().clone(), worktree_id)
});
let inventory = inventory.read(cx);
let tags = mem::take(&mut runnable.tags);
let mut tags: Vec<_> = tags
.into_iter()
.flat_map(|tag| {
let tag = tag.0.clone();
inventory
.list_tasks(Some(runnable.language.clone()), worktree_id)
.into_iter()
.filter(move |(_, template)| {
template.tags.iter().any(|source_tag| source_tag == &tag)
})
})
.sorted_by_key(|(kind, _)| kind.to_owned())
.collect();
if let Some((leading_tag_source, _)) = tags.first() {
// Strongest source wins; if we have worktree tag binding, prefer that to
// global and language bindings;
// if we have a global binding, prefer that to language binding.
let first_mismatch = tags
.iter()
.position(|(tag_source, _)| tag_source != leading_tag_source);
if let Some(index) = first_mismatch {
tags.truncate(index);
}
}
(tags, worktree_id)
}
pub fn move_to_enclosing_bracket(
&mut self,
_: &MoveToEnclosingBracket,
@@ -9182,17 +9590,23 @@ impl Editor {
let selections = self.selections.all::<Point>(cx);
let selection = selections.iter().peekable().next();
build_permalink(BuildPermalinkParams {
remote_url: &origin_url,
sha: &sha,
path: &path,
selection: selection.map(|selection| {
let range = selection.range();
let start = range.start.row;
let end = range.end.row;
start..end
}),
})
let (provider, remote) =
parse_git_remote_url(GitHostingProviderRegistry::default_global(cx), &origin_url)
.ok_or_else(|| anyhow!("failed to parse Git remote URL"))?;
Ok(provider.build_permalink(
remote,
BuildPermalinkParams {
sha: &sha,
path: &path,
selection: selection.map(|selection| {
let range = selection.range();
let start = range.start.row;
let end = range.end.row;
start..end
}),
},
))
}
pub fn copy_permalink_to_line(&mut self, _: &CopyPermalinkToLine, cx: &mut ViewContext<Self>) {
@@ -9698,7 +10112,11 @@ impl Editor {
self.refresh_inlay_hints(InlayHintRefreshReason::ExcerptsRemoved(ids.clone()), cx);
cx.emit(EditorEvent::ExcerptsRemoved { ids: ids.clone() })
}
multi_buffer::Event::Reparsed => cx.emit(EditorEvent::Reparsed),
multi_buffer::Event::Reparsed => {
self.tasks_update_task = Some(self.refresh_runnables(cx));
cx.emit(EditorEvent::Reparsed);
}
multi_buffer::Event::LanguageChanged => {
cx.emit(EditorEvent::Reparsed);
cx.notify();
@@ -9942,12 +10360,14 @@ impl Editor {
.raw_user_settings()
.get("vim_mode")
== Some(&serde_json::Value::Bool(true));
let copilot_enabled = all_language_settings(file, cx).copilot_enabled(None, None);
let copilot_enabled = all_language_settings(file, cx).inline_completions.provider
== language::language_settings::InlineCompletionProvider::Copilot;
let copilot_enabled_for_language = self
.buffer
.read(cx)
.settings_at(0, cx)
.show_copilot_suggestions;
.show_inline_completions;
let telemetry = project.read(cx).client().telemetry().clone();
telemetry.report_editor_event(
@@ -10837,34 +11257,12 @@ impl ViewInputHandler for Editor {
}
trait SelectionExt {
fn offset_range(&self, buffer: &MultiBufferSnapshot) -> Range<usize>;
fn point_range(&self, buffer: &MultiBufferSnapshot) -> Range<Point>;
fn display_range(&self, map: &DisplaySnapshot) -> Range<DisplayPoint>;
fn spanned_rows(&self, include_end_if_at_line_start: bool, map: &DisplaySnapshot)
-> Range<u32>;
}
impl<T: ToPoint + ToOffset> SelectionExt for Selection<T> {
fn point_range(&self, buffer: &MultiBufferSnapshot) -> Range<Point> {
let start = self.start.to_point(buffer);
let end = self.end.to_point(buffer);
if self.reversed {
end..start
} else {
start..end
}
}
fn offset_range(&self, buffer: &MultiBufferSnapshot) -> Range<usize> {
let start = self.start.to_offset(buffer);
let end = self.end.to_offset(buffer);
if self.reversed {
end..start
} else {
start..end
}
}
fn display_range(&self, map: &DisplaySnapshot) -> Range<DisplayPoint> {
let start = self
.start

View File

@@ -9026,7 +9026,7 @@ async fn test_multibuffer_reverts(cx: &mut gpui::TestAppContext) {
.collect::<String>(),
cx,
);
buffer.set_diff_base(Some(sample_text), cx);
buffer.set_diff_base(Some(sample_text.into()), cx);
});
cx.executor().run_until_parked();
}
@@ -10041,17 +10041,17 @@ async fn test_toggle_diff_expand_in_multi_buffer(cx: &mut gpui::TestAppContext)
"vvvv\nwwww\nxxxx\nyyyy\nzzzz\n@@@@\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}";
let buffer_1 = cx.new_model(|cx| {
let mut buffer = Buffer::local(modified_sample_text_1.to_string(), cx);
buffer.set_diff_base(Some(sample_text_1.clone()), cx);
buffer.set_diff_base(Some(sample_text_1.clone().into()), cx);
buffer
});
let buffer_2 = cx.new_model(|cx| {
let mut buffer = Buffer::local(modified_sample_text_2.to_string(), cx);
buffer.set_diff_base(Some(sample_text_2.clone()), cx);
buffer.set_diff_base(Some(sample_text_2.clone().into()), cx);
buffer
});
let buffer_3 = cx.new_model(|cx| {
let mut buffer = Buffer::local(modified_sample_text_3.to_string(), cx);
buffer.set_diff_base(Some(sample_text_3.clone()), cx);
buffer.set_diff_base(Some(sample_text_3.clone().into()), cx);
buffer
});
@@ -11351,7 +11351,7 @@ fn assert_hunk_revert(
.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.executor().run_until_parked();

View File

@@ -12,10 +12,11 @@ use crate::{
items::BufferSearchHighlights,
mouse_context_menu::{self, MouseContextMenu},
scroll::scroll_amount::ScrollAmount,
CursorShape, DisplayPoint, DocumentHighlightRead, DocumentHighlightWrite, Editor, EditorMode,
EditorSettings, EditorSnapshot, EditorStyle, ExpandExcerpts, GutterDimensions, HalfPageDown,
HalfPageUp, HoveredCursor, HunkToExpand, LineDown, LineUp, OpenExcerpts, PageDown, PageUp,
Point, SelectPhase, Selection, SoftWrap, ToPoint, CURSORS_VISIBLE_FOR, MAX_LINE_LEN,
CodeActionsMenu, CursorShape, DisplayPoint, DocumentHighlightRead, DocumentHighlightWrite,
Editor, EditorMode, EditorSettings, EditorSnapshot, EditorStyle, ExpandExcerpts,
GutterDimensions, HalfPageDown, HalfPageUp, HoveredCursor, HunkToExpand, LineDown, LineUp,
OpenExcerpts, PageDown, PageUp, Point, SelectPhase, Selection, SoftWrap, ToPoint,
CURSORS_VISIBLE_FOR, MAX_LINE_LEN,
};
use anyhow::Result;
use client::ParticipantIndex;
@@ -427,10 +428,12 @@ impl EditorElement {
let mut click_count = event.click_count;
let mut modifiers = event.modifiers;
if gutter_hitbox.is_hovered(cx) {
click_count = 3; // Simulate triple-click when clicking the gutter to select lines
} else if let Some(hovered_hunk) = hovered_hunk {
if let Some(hovered_hunk) = hovered_hunk {
editor.expand_diff_hunk(None, hovered_hunk, cx);
cx.notify();
return;
} else if gutter_hitbox.is_hovered(cx) {
click_count = 3; // Simulate triple-click when clicking the gutter to select lines
} else if !text_hitbox.is_hovered(cx) {
return;
}
@@ -635,7 +638,7 @@ impl EditorElement {
editor.update_hovered_link(point_for_position, &position_map.snapshot, modifiers, cx);
if let Some(point) = point_for_position.as_valid() {
hover_at(editor, Some(point), cx);
hover_at(editor, Some((point, &position_map.snapshot)), cx);
Self::update_visible_cursor(editor, point, position_map, cx);
} else {
hover_at(editor, None, cx);
@@ -1372,6 +1375,56 @@ impl EditorElement {
Some(shaped_lines)
}
fn layout_run_indicators(
&self,
line_height: Pixels,
scroll_pixel_position: gpui::Point<Pixels>,
gutter_dimensions: &GutterDimensions,
gutter_hitbox: &Hitbox,
cx: &mut WindowContext,
) -> Vec<AnyElement> {
self.editor.update(cx, |editor, cx| {
let active_task_indicator_row =
if let Some(crate::ContextMenu::CodeActions(CodeActionsMenu {
deployed_from_indicator,
actions,
..
})) = editor.context_menu.read().as_ref()
{
actions
.tasks
.as_ref()
.map(|tasks| tasks.position.row)
.or_else(|| *deployed_from_indicator)
} else {
None
};
editor
.tasks
.keys()
.map(|row| {
let button = editor.render_run_indicator(
&self.style,
Some(*row) == active_task_indicator_row,
*row,
cx,
);
let button = prepaint_gutter_button(
button,
*row,
line_height,
gutter_dimensions,
scroll_pixel_position,
gutter_hitbox,
cx,
);
button
})
.collect_vec()
})
}
fn layout_code_actions_indicator(
&self,
line_height: Pixels,
@@ -1383,35 +1436,28 @@ impl EditorElement {
) -> Option<AnyElement> {
let mut active = false;
let mut button = None;
let row = newest_selection_head.row();
self.editor.update(cx, |editor, cx| {
active = matches!(
editor.context_menu.read().as_ref(),
Some(crate::ContextMenu::CodeActions(_))
);
button = editor.render_code_actions_indicator(&self.style, active, cx);
if let Some(crate::ContextMenu::CodeActions(CodeActionsMenu {
deployed_from_indicator,
..
})) = editor.context_menu.read().as_ref()
{
active = deployed_from_indicator.map_or(true, |indicator_row| indicator_row == row);
};
button = editor.render_code_actions_indicator(&self.style, row, active, cx);
});
let mut button = button?.into_any_element();
let available_space = size(
AvailableSpace::MinContent,
AvailableSpace::Definite(line_height),
let button = prepaint_gutter_button(
button?,
row,
line_height,
gutter_dimensions,
scroll_pixel_position,
gutter_hitbox,
cx,
);
let indicator_size = button.layout_as_root(available_space, cx);
let blame_width = gutter_dimensions
.git_blame_entries_width
.unwrap_or(Pixels::ZERO);
let mut x = blame_width;
let available_width = gutter_dimensions.margin + gutter_dimensions.left_padding
- indicator_size.width
- blame_width;
x += available_width / 2.;
let mut y = newest_selection_head.row() as f32 * line_height - scroll_pixel_position.y;
y += (line_height - indicator_size.height) / 2.;
button.prepaint_as_root(gutter_hitbox.origin + point(x, y), available_space, cx);
Some(button)
}
@@ -1767,7 +1813,7 @@ impl EditorElement {
.pr(gpui::px(8.))
.rounded_md()
.shadow_md()
.border()
.border_1()
.border_color(cx.theme().colors().border)
.bg(cx.theme().colors().editor_subheader_background)
.justify_between()
@@ -2349,6 +2395,10 @@ impl EditorElement {
}
});
for test_indicators in layout.test_indicators.iter_mut() {
test_indicators.paint(cx);
}
if let Some(indicator) = layout.code_actions_indicator.as_mut() {
indicator.paint(cx);
}
@@ -3222,6 +3272,39 @@ impl EditorElement {
}
}
fn prepaint_gutter_button(
button: IconButton,
row: u32,
line_height: Pixels,
gutter_dimensions: &GutterDimensions,
scroll_pixel_position: gpui::Point<Pixels>,
gutter_hitbox: &Hitbox,
cx: &mut WindowContext<'_>,
) -> AnyElement {
let mut button = button.into_any_element();
let available_space = size(
AvailableSpace::MinContent,
AvailableSpace::Definite(line_height),
);
let indicator_size = button.layout_as_root(available_space, cx);
let blame_width = gutter_dimensions
.git_blame_entries_width
.unwrap_or(Pixels::ZERO);
let mut x = blame_width;
let available_width = gutter_dimensions.margin + gutter_dimensions.left_padding
- indicator_size.width
- blame_width;
x += available_width / 2.;
let mut y = row as f32 * line_height - scroll_pixel_position.y;
y += (line_height - indicator_size.height) / 2.;
button.prepaint_as_root(gutter_hitbox.origin + point(x, y), available_space, cx);
button
}
fn render_inline_blame_entry(
blame: &gpui::Model<GitBlame>,
blame_entry: BlameEntry,
@@ -3937,18 +4020,33 @@ impl Element for EditorElement {
cx,
);
if gutter_settings.code_actions {
code_actions_indicator = self.layout_code_actions_indicator(
line_height,
newest_selection_head,
scroll_pixel_position,
&gutter_dimensions,
&gutter_hitbox,
cx,
);
let has_test_indicator = self
.editor
.read(cx)
.tasks
.contains_key(&newest_selection_head.row());
if !has_test_indicator {
code_actions_indicator = self.layout_code_actions_indicator(
line_height,
newest_selection_head,
scroll_pixel_position,
&gutter_dimensions,
&gutter_hitbox,
cx,
);
}
}
}
}
let test_indicators = self.layout_run_indicators(
line_height,
scroll_pixel_position,
&gutter_dimensions,
&gutter_hitbox,
cx,
);
if !context_menu_visible && !cx.has_active_drag() {
self.layout_hover_popovers(
&snapshot,
@@ -4049,6 +4147,7 @@ impl Element for EditorElement {
visible_cursors,
selections,
mouse_context_menu,
test_indicators,
code_actions_indicator,
fold_indicators,
tab_invisible,
@@ -4168,6 +4267,7 @@ pub struct EditorLayout {
selections: Vec<(PlayerColor, Vec<SelectionLayout>)>,
max_row: u32,
code_actions_indicator: Option<AnyElement>,
test_indicators: Vec<AnyElement>,
fold_indicators: Vec<Option<AnyElement>>,
mouse_context_menu: Option<AnyElement>,
tab_invisible: ShapedLine,

View File

@@ -145,7 +145,8 @@ mod tests {
1.five
1.six
"
.unindent(),
.unindent()
.into(),
),
cx,
);
@@ -181,7 +182,8 @@ mod tests {
2.four
2.six
"
.unindent(),
.unindent()
.into(),
),
cx,
);

View File

@@ -4,10 +4,7 @@ use anyhow::Result;
use collections::HashMap;
use git::{
blame::{Blame, BlameEntry},
hosting_provider::HostingProvider,
permalink::{build_commit_permalink, parse_git_remote_url},
pull_request::{extract_pull_request, PullRequest},
Oid,
parse_git_remote_url, GitHostingProvider, GitHostingProviderRegistry, Oid, PullRequest,
};
use gpui::{Model, ModelContext, Subscription, Task};
use language::{markdown, Bias, Buffer, BufferSnapshot, Edit, LanguageRegistry, ParsedMarkdown};
@@ -50,13 +47,23 @@ impl<'a> sum_tree::Dimension<'a, GitBlameEntrySummary> for u32 {
}
}
#[derive(Clone, Debug)]
#[derive(Clone)]
pub struct GitRemote {
pub host: HostingProvider,
pub host: Arc<dyn GitHostingProvider + Send + Sync + 'static>,
pub owner: String,
pub repo: String,
}
impl std::fmt::Debug for GitRemote {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("GitRemote")
.field("host", &self.host.name())
.field("owner", &self.owner)
.field("repo", &self.repo)
.finish()
}
}
impl GitRemote {
pub fn host_supports_avatars(&self) -> bool {
self.host.supports_avatars()
@@ -323,6 +330,7 @@ impl GitBlame {
let snapshot = self.buffer.read(cx).snapshot();
let blame = self.project.read(cx).blame_buffer(&self.buffer, None, cx);
let languages = self.project.read(cx).languages().clone();
let provider_registry = GitHostingProviderRegistry::default_global(cx);
self.task = cx.spawn(|this, mut cx| async move {
let result = cx
@@ -338,9 +346,14 @@ impl GitBlame {
} = blame.await?;
let entries = build_blame_entry_sum_tree(entries, snapshot.max_point().row);
let commit_details =
parse_commit_messages(messages, remote_url, &permalinks, &languages)
.await;
let commit_details = parse_commit_messages(
messages,
remote_url,
&permalinks,
provider_registry,
&languages,
)
.await;
anyhow::Ok((entries, commit_details))
}
@@ -431,19 +444,22 @@ async fn parse_commit_messages(
messages: impl IntoIterator<Item = (Oid, String)>,
remote_url: Option<String>,
deprecated_permalinks: &HashMap<Oid, Url>,
provider_registry: Arc<GitHostingProviderRegistry>,
languages: &Arc<LanguageRegistry>,
) -> HashMap<Oid, CommitDetails> {
let mut commit_details = HashMap::default();
let parsed_remote_url = remote_url.as_deref().and_then(parse_git_remote_url);
let parsed_remote_url = remote_url
.as_deref()
.and_then(|remote_url| parse_git_remote_url(provider_registry, remote_url));
for (oid, message) in messages {
let parsed_message = parse_markdown(&message, &languages).await;
let permalink = if let Some(git_remote) = parsed_remote_url.as_ref() {
Some(build_commit_permalink(
git::permalink::BuildCommitPermalinkParams {
remote: git_remote,
let permalink = if let Some((provider, git_remote)) = parsed_remote_url.as_ref() {
Some(provider.build_commit_permalink(
git_remote,
git::BuildCommitPermalinkParams {
sha: oid.to_string().as_str(),
},
))
@@ -455,15 +471,17 @@ async fn parse_commit_messages(
deprecated_permalinks.get(&oid).cloned()
};
let remote = parsed_remote_url.as_ref().map(|remote| GitRemote {
host: remote.provider.clone(),
owner: remote.owner.to_string(),
repo: remote.repo.to_string(),
});
let remote = parsed_remote_url
.as_ref()
.map(|(provider, remote)| GitRemote {
host: provider.clone(),
owner: remote.owner.to_string(),
repo: remote.repo.to_string(),
});
let pull_request = parsed_remote_url
.as_ref()
.and_then(|remote| extract_pull_request(remote, &message));
.and_then(|(provider, remote)| provider.extract_pull_request(remote, &message));
commit_details.insert(
oid,

View File

@@ -732,7 +732,7 @@ mod tests {
cx.cx
.cx
.simulate_mouse_move(screen_coord.unwrap(), Modifiers::command_shift());
.simulate_mouse_move(screen_coord.unwrap(), None, Modifiers::command_shift());
requests.next().await;
cx.run_until_parked();
@@ -802,7 +802,7 @@ mod tests {
])))
});
cx.simulate_mouse_move(hover_point, Modifiers::secondary_key());
cx.simulate_mouse_move(hover_point, None, Modifiers::secondary_key());
requests.next().await;
cx.background_executor.run_until_parked();
cx.assert_editor_text_highlights::<HoveredLinkState>(indoc! {"
@@ -828,7 +828,7 @@ mod tests {
])))
});
cx.simulate_mouse_move(hover_point, Modifiers::secondary_key());
cx.simulate_mouse_move(hover_point, None, Modifiers::secondary_key());
requests.next().await;
cx.background_executor.run_until_parked();
cx.assert_editor_text_highlights::<HoveredLinkState>(indoc! {"
@@ -847,7 +847,7 @@ mod tests {
// No definitions returned
Ok(Some(lsp::GotoDefinitionResponse::Link(vec![])))
});
cx.simulate_mouse_move(hover_point, Modifiers::secondary_key());
cx.simulate_mouse_move(hover_point, None, Modifiers::secondary_key());
requests.next().await;
cx.background_executor.run_until_parked();
@@ -863,7 +863,7 @@ mod tests {
fn test() { do_work(); }
fn do_work() { teˇst(); }
"});
cx.simulate_mouse_move(hover_point, Modifiers::none());
cx.simulate_mouse_move(hover_point, None, Modifiers::none());
// Assert no link highlights
cx.assert_editor_text_highlights::<HoveredLinkState>(indoc! {"
@@ -907,7 +907,7 @@ mod tests {
fn do_work() { test(); }
"});
cx.simulate_mouse_move(hover_point, Modifiers::secondary_key());
cx.simulate_mouse_move(hover_point, None, Modifiers::secondary_key());
cx.background_executor.run_until_parked();
cx.assert_editor_text_highlights::<HoveredLinkState>(indoc! {"
fn test() { do_work(); }
@@ -919,7 +919,7 @@ mod tests {
fn test() { do_work(); }
fn do_work() { tesˇt(); }
"});
cx.simulate_mouse_move(hover_point, Modifiers::secondary_key());
cx.simulate_mouse_move(hover_point, None, Modifiers::secondary_key());
cx.background_executor.run_until_parked();
cx.assert_editor_text_highlights::<HoveredLinkState>(indoc! {"
fn test() { do_work(); }
@@ -1009,7 +1009,7 @@ mod tests {
s.set_pending_anchor_range(anchor_range, crate::SelectMode::Character)
});
});
cx.simulate_mouse_move(hover_point, Modifiers::secondary_key());
cx.simulate_mouse_move(hover_point, None, Modifiers::secondary_key());
cx.background_executor.run_until_parked();
assert!(requests.try_next().is_err());
cx.assert_editor_text_highlights::<HoveredLinkState>(indoc! {"
@@ -1123,7 +1123,7 @@ mod tests {
});
// Press cmd to trigger highlight
let hover_point = cx.pixel_position_for(midpoint);
cx.simulate_mouse_move(hover_point, Modifiers::secondary_key());
cx.simulate_mouse_move(hover_point, None, Modifiers::secondary_key());
cx.background_executor.run_until_parked();
cx.update_editor(|editor, cx| {
let snapshot = editor.snapshot(cx);
@@ -1142,7 +1142,7 @@ mod tests {
assert_set_eq!(actual_highlights, vec![&expected_highlight]);
});
cx.simulate_mouse_move(hover_point, Modifiers::none());
cx.simulate_mouse_move(hover_point, None, Modifiers::none());
// Assert no link highlights
cx.update_editor(|editor, cx| {
let snapshot = editor.snapshot(cx);
@@ -1186,7 +1186,7 @@ mod tests {
Let's test a [complex](https://zed.dev/channel/had-(ˇoops)) case.
"});
cx.simulate_mouse_move(screen_coord, Modifiers::secondary_key());
cx.simulate_mouse_move(screen_coord, None, Modifiers::secondary_key());
cx.assert_editor_text_highlights::<HoveredLinkState>(indoc! {"
Let's test a [complex](«https://zed.dev/channel/had-(oops)ˇ») case.
"});
@@ -1214,7 +1214,7 @@ mod tests {
let screen_coord =
cx.pixel_position(indoc! {"https://zed.dev/relˇeases is a cool webpage."});
cx.simulate_mouse_move(screen_coord, Modifiers::secondary_key());
cx.simulate_mouse_move(screen_coord, None, Modifiers::secondary_key());
cx.assert_editor_text_highlights::<HoveredLinkState>(
indoc! {"«https://zed.dev/releasesˇ» is a cool webpage."},
);
@@ -1239,7 +1239,7 @@ mod tests {
let screen_coord =
cx.pixel_position(indoc! {"A cool webpage is https://zed.dev/releˇases"});
cx.simulate_mouse_move(screen_coord, Modifiers::secondary_key());
cx.simulate_mouse_move(screen_coord, None, Modifiers::secondary_key());
cx.assert_editor_text_highlights::<HoveredLinkState>(
indoc! {"A cool webpage is «https://zed.dev/releasesˇ»"},
);

View File

@@ -31,15 +31,20 @@ pub const HOVER_POPOVER_GAP: Pixels = px(10.);
/// Bindable action which uses the most recent selection head to trigger a hover
pub fn hover(editor: &mut Editor, _: &Hover, cx: &mut ViewContext<Editor>) {
let head = editor.selections.newest_display(cx).head();
show_hover(editor, head, true, cx);
let snapshot = editor.snapshot(cx);
show_hover(editor, head, &snapshot, true, cx);
}
/// The internal hover action dispatches between `show_hover` or `hide_hover`
/// depending on whether a point to hover over is provided.
pub fn hover_at(editor: &mut Editor, point: Option<DisplayPoint>, cx: &mut ViewContext<Editor>) {
pub fn hover_at(
editor: &mut Editor,
point: Option<(DisplayPoint, &EditorSnapshot)>,
cx: &mut ViewContext<Editor>,
) {
if EditorSettings::get_global(cx).hover_popover_enabled {
if let Some(point) = point {
show_hover(editor, point, false, cx);
if let Some((point, snapshot)) = point {
show_hover(editor, point, snapshot, false, cx);
} else {
hide_hover(editor, cx);
}
@@ -160,6 +165,7 @@ pub fn hide_hover(editor: &mut Editor, cx: &mut ViewContext<Editor>) -> bool {
fn show_hover(
editor: &mut Editor,
point: DisplayPoint,
snapshot: &EditorSnapshot,
ignore_timeout: bool,
cx: &mut ViewContext<Editor>,
) {
@@ -167,7 +173,6 @@ fn show_hover(
return;
}
let snapshot = editor.snapshot(cx);
let multibuffer_offset = point.to_offset(&snapshot.display_snapshot, Bias::Left);
let (buffer, buffer_position) = if let Some(output) = editor
@@ -234,6 +239,7 @@ fn show_hover(
return;
}
}
let snapshot = snapshot.clone();
let task = cx.spawn(|this, mut cx| {
async move {
@@ -659,7 +665,10 @@ mod tests {
fn test() { printˇln!(); }
"});
cx.update_editor(|editor, cx| hover_at(editor, Some(hover_point), cx));
cx.update_editor(|editor, cx| {
let snapshot = editor.snapshot(cx);
hover_at(editor, Some((hover_point, &snapshot)), cx)
});
assert!(!cx.editor(|editor, _| editor.hover_state.visible()));
// After delay, hover should be visible.
@@ -705,7 +714,10 @@ mod tests {
let mut request = cx
.lsp
.handle_request::<lsp::request::HoverRequest, _, _>(|_, _| async move { Ok(None) });
cx.update_editor(|editor, cx| hover_at(editor, Some(hover_point), cx));
cx.update_editor(|editor, cx| {
let snapshot = editor.snapshot(cx);
hover_at(editor, Some((hover_point, &snapshot)), cx)
});
cx.background_executor
.advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100));
request.next().await;

View File

@@ -1,4 +1,4 @@
use std::ops::Range;
use std::{ops::Range, sync::Arc};
use collections::{hash_map, HashMap, HashSet};
use git::diff::{DiffHunk, DiffHunkStatus};
@@ -14,7 +14,8 @@ use util::{debug_panic, RangeExt};
use crate::{
git::{diff_hunk_to_display, DisplayDiffHunk},
hunks_for_selections, BlockDisposition, BlockId, BlockProperties, BlockStyle, DiffRowHighlight,
Editor, ExpandAllHunkDiffs, RangeToAnchorExt, ToDisplayPoint, ToggleHunkDiff,
Editor, ExpandAllHunkDiffs, RangeToAnchorExt, RevertSelectedHunks, ToDisplayPoint,
ToggleHunkDiff,
};
#[derive(Debug, Clone)]
@@ -215,34 +216,29 @@ impl Editor {
let hunk_end = hunk.multi_buffer_range.end;
let buffer = self.buffer().clone();
let (diff_base_buffer, deleted_text_range, deleted_text_lines) =
buffer.update(cx, |buffer, cx| {
let snapshot = buffer.snapshot(cx);
let hunk = buffer_diff_hunk(&snapshot, multi_buffer_row_range.clone())?;
let mut buffer_ranges = buffer.range_to_buffer_ranges(multi_buffer_row_range, cx);
if buffer_ranges.len() == 1 {
let (buffer, _, _) = buffer_ranges.pop()?;
let diff_base_buffer = diff_base_buffer
.or_else(|| self.current_diff_base_buffer(&buffer, cx))
.or_else(|| create_diff_base_buffer(&buffer, cx));
let buffer = buffer.read(cx);
let deleted_text_lines = buffer.diff_base().and_then(|diff_base| {
Some(
diff_base
.get(hunk.diff_base_byte_range.clone())?
.lines()
.count(),
)
});
Some((
diff_base_buffer?,
hunk.diff_base_byte_range,
deleted_text_lines,
))
} else {
None
}
})?;
let (diff_base_buffer, deleted_text_lines) = buffer.update(cx, |buffer, cx| {
let snapshot = buffer.snapshot(cx);
let hunk = buffer_diff_hunk(&snapshot, multi_buffer_row_range.clone())?;
let mut buffer_ranges = buffer.range_to_buffer_ranges(multi_buffer_row_range, cx);
if buffer_ranges.len() == 1 {
let (buffer, _, _) = buffer_ranges.pop()?;
let diff_base_buffer = diff_base_buffer
.or_else(|| self.current_diff_base_buffer(&buffer, cx))
.or_else(|| create_diff_base_buffer(&buffer, cx))?;
let buffer = buffer.read(cx);
let deleted_text_lines = buffer.diff_base().map(|diff_base| {
let diff_start_row = diff_base
.offset_to_point(hunk.diff_base_byte_range.start)
.row;
let diff_end_row = diff_base.offset_to_point(hunk.diff_base_byte_range.end).row;
let line_count = diff_end_row - diff_start_row;
line_count as u8
})?;
Some((diff_base_buffer, deleted_text_lines))
} else {
None
}
})?;
let block_insert_index = match self.expanded_hunks.hunks.binary_search_by(|probe| {
probe
@@ -255,13 +251,9 @@ impl Editor {
};
let block = match hunk.status {
DiffHunkStatus::Removed => self.add_deleted_lines(
deleted_text_lines,
hunk_start,
diff_base_buffer,
deleted_text_range,
cx,
),
DiffHunkStatus::Removed => {
self.insert_deleted_text_block(diff_base_buffer, deleted_text_lines, &hunk, cx)
}
DiffHunkStatus::Added => {
self.highlight_rows::<DiffRowHighlight>(
hunk_start..hunk_end,
@@ -276,13 +268,7 @@ impl Editor {
Some(added_hunk_color(cx)),
cx,
);
self.add_deleted_lines(
deleted_text_lines,
hunk_start,
diff_base_buffer,
deleted_text_range,
cx,
)
self.insert_deleted_text_block(diff_base_buffer, deleted_text_lines, &hunk, cx)
}
};
self.expanded_hunks.hunks.insert(
@@ -299,43 +285,20 @@ impl Editor {
Some(())
}
fn add_deleted_lines(
&mut self,
deleted_text_lines: Option<usize>,
hunk_start: Anchor,
diff_base_buffer: Model<Buffer>,
deleted_text_range: Range<usize>,
cx: &mut ViewContext<'_, Self>,
) -> Option<BlockId> {
if let Some(deleted_text_lines) = deleted_text_lines {
self.insert_deleted_text_block(
hunk_start,
diff_base_buffer,
deleted_text_range,
deleted_text_lines as u8,
cx,
)
} else {
debug_panic!("Found no deleted text for removed hunk on position {hunk_start:?}");
None
}
}
fn insert_deleted_text_block(
&mut self,
position: Anchor,
diff_base_buffer: Model<Buffer>,
deleted_text_range: Range<usize>,
deleted_text_height: u8,
hunk: &HunkToExpand,
cx: &mut ViewContext<'_, Self>,
) -> Option<BlockId> {
let deleted_hunk_color = deleted_hunk_color(cx);
let (editor_height, editor_with_deleted_text) =
editor_with_deleted_text(diff_base_buffer, deleted_text_range, deleted_hunk_color, cx);
editor_with_deleted_text(diff_base_buffer, deleted_hunk_color, hunk, cx);
let parent_gutter_offset = self.gutter_dimensions.width + self.gutter_dimensions.margin;
let mut new_block_ids = self.insert_blocks(
Some(BlockProperties {
position,
position: hunk.multi_buffer_range.start,
height: editor_height.max(deleted_text_height),
style: BlockStyle::Flex,
render: Box::new(move |_| {
@@ -542,12 +505,12 @@ fn create_diff_base_buffer(buffer: &Model<Buffer>, cx: &mut AppContext) -> Optio
buffer
.update(cx, |buffer, _| {
let language = buffer.language().cloned();
let diff_base = buffer.diff_base().map(|s| s.to_owned());
Some((diff_base?, language))
let diff_base = buffer.diff_base()?.clone();
Some((buffer.line_ending(), diff_base, language))
})
.map(|(diff_base, language)| {
.map(|(line_ending, diff_base, language)| {
cx.new_model(|cx| {
let buffer = Buffer::local(diff_base, cx);
let buffer = Buffer::local_normalized(diff_base, line_ending, cx);
match language {
Some(language) => buffer.with_language(language, cx),
None => buffer,
@@ -570,10 +533,11 @@ fn deleted_hunk_color(cx: &AppContext) -> Hsla {
fn editor_with_deleted_text(
diff_base_buffer: Model<Buffer>,
deleted_text_range: Range<usize>,
deleted_color: Hsla,
hunk: &HunkToExpand,
cx: &mut ViewContext<'_, Editor>,
) -> (u8, View<Editor>) {
let parent_editor = cx.view().downgrade();
let editor = cx.new_view(|cx| {
let multi_buffer =
cx.new_model(|_| MultiBuffer::without_headers(0, language::Capability::ReadOnly));
@@ -581,7 +545,7 @@ fn editor_with_deleted_text(
multi_buffer.push_excerpts(
diff_base_buffer,
Some(ExcerptRange {
context: deleted_text_range,
context: hunk.diff_base_byte_range.clone(),
primary: None,
}),
cx,
@@ -602,6 +566,40 @@ fn editor_with_deleted_text(
.anchor_after(editor.buffer.read(cx).len(cx));
editor.highlight_rows::<DiffRowHighlight>(start..end, Some(deleted_color), cx);
let hunk_related_subscription = cx.on_blur(&editor.focus_handle, |editor, cx| {
editor.change_selections(None, cx, |s| {
s.try_cancel();
});
});
editor._subscriptions.push(hunk_related_subscription);
let original_multi_buffer_range = hunk.multi_buffer_range.clone();
let diff_base_range = hunk.diff_base_byte_range.clone();
editor.register_action::<RevertSelectedHunks>(move |_, cx| {
parent_editor
.update(cx, |editor, cx| {
let Some((buffer, original_text)) = editor.buffer().update(cx, |buffer, cx| {
let (_, buffer, _) =
buffer.excerpt_containing(original_multi_buffer_range.start, cx)?;
let original_text =
buffer.read(cx).diff_base()?.slice(diff_base_range.clone());
Some((buffer, Arc::from(original_text.to_string())))
}) else {
return;
};
buffer.update(cx, |buffer, cx| {
buffer.edit(
Some((
original_multi_buffer_range.start.text_anchor
..original_multi_buffer_range.end.text_anchor,
original_text,
)),
None,
cx,
)
});
})
.ok();
});
editor
});

View File

@@ -25,11 +25,11 @@ pub trait InlineCompletionProvider: 'static + Sized {
);
fn accept(&mut self, cx: &mut ModelContext<Self>);
fn discard(&mut self, cx: &mut ModelContext<Self>);
fn active_completion_text(
&self,
fn active_completion_text<'a>(
&'a self,
buffer: &Model<Buffer>,
cursor_position: language::Anchor,
cx: &AppContext,
cx: &'a AppContext,
) -> Option<&str>;
}
@@ -57,7 +57,7 @@ pub trait InlineCompletionProviderHandle {
fn accept(&self, cx: &mut AppContext);
fn discard(&self, cx: &mut AppContext);
fn active_completion_text<'a>(
&self,
&'a self,
buffer: &Model<Buffer>,
cursor_position: language::Anchor,
cx: &'a AppContext,
@@ -110,7 +110,7 @@ where
}
fn active_completion_text<'a>(
&self,
&'a self,
buffer: &Model<Buffer>,
cursor_position: language::Anchor,
cx: &'a AppContext,

View File

@@ -79,7 +79,7 @@ pub fn deploy_context_menu(
.action(
"Code Actions",
Box::new(ToggleCodeActions {
deployed_from_indicator: false,
deployed_from_indicator: None,
}),
)
.separator()

View File

@@ -69,7 +69,7 @@ impl SelectionsCollection {
self.next_selection_id = other.next_selection_id;
self.line_mode = other.line_mode;
self.disjoint = other.disjoint.clone();
self.pending = other.pending.clone();
self.pending.clone_from(&other.pending);
}
pub fn count(&self) -> usize {

118
crates/editor/src/tasks.rs Normal file
View File

@@ -0,0 +1,118 @@
use crate::Editor;
use std::{path::Path, sync::Arc};
use anyhow::Context;
use gpui::WindowContext;
use language::{BasicContextProvider, ContextProvider};
use project::{Location, WorktreeId};
use task::{TaskContext, TaskVariables};
use util::ResultExt;
use workspace::Workspace;
pub(crate) fn task_context_for_location(
workspace: &Workspace,
location: Location,
cx: &mut WindowContext<'_>,
) -> Option<TaskContext> {
let cwd = workspace::tasks::task_cwd(workspace, cx)
.log_err()
.flatten();
let buffer = location.buffer.clone();
let language_context_provider = buffer
.read(cx)
.language()
.and_then(|language| language.context_provider())
.unwrap_or_else(|| Arc::new(BasicContextProvider));
let worktree_abs_path = buffer
.read(cx)
.file()
.map(|file| WorktreeId::from_usize(file.worktree_id()))
.and_then(|worktree_id| {
workspace
.project()
.read(cx)
.worktree_for_id(worktree_id, cx)
.map(|worktree| worktree.read(cx).abs_path())
});
let task_variables = combine_task_variables(
worktree_abs_path.as_deref(),
location,
language_context_provider.as_ref(),
cx,
)
.log_err()?;
Some(TaskContext {
cwd,
task_variables,
})
}
pub(crate) fn task_context_with_editor(
workspace: &Workspace,
editor: &mut Editor,
cx: &mut WindowContext<'_>,
) -> Option<TaskContext> {
let (selection, buffer, editor_snapshot) = {
let selection = editor.selections.newest::<usize>(cx);
let (buffer, _, _) = editor
.buffer()
.read(cx)
.point_to_buffer_offset(selection.start, cx)?;
let snapshot = editor.snapshot(cx);
Some((selection, buffer, snapshot))
}?;
let selection_range = selection.range();
let start = editor_snapshot
.display_snapshot
.buffer_snapshot
.anchor_after(selection_range.start)
.text_anchor;
let end = editor_snapshot
.display_snapshot
.buffer_snapshot
.anchor_after(selection_range.end)
.text_anchor;
let location = Location {
buffer,
range: start..end,
};
task_context_for_location(workspace, location, cx)
}
pub fn task_context(workspace: &Workspace, cx: &mut WindowContext<'_>) -> TaskContext {
let Some(editor) = workspace
.active_item(cx)
.and_then(|item| item.act_as::<Editor>(cx))
else {
return Default::default();
};
editor.update(cx, |editor, cx| {
task_context_with_editor(workspace, editor, cx).unwrap_or_default()
})
}
fn combine_task_variables(
worktree_abs_path: Option<&Path>,
location: Location,
context_provider: &dyn ContextProvider,
cx: &mut WindowContext<'_>,
) -> anyhow::Result<TaskVariables> {
if context_provider.is_basic() {
context_provider
.build_context(worktree_abs_path, &location, cx)
.context("building basic provider context")
} else {
let mut basic_context = BasicContextProvider
.build_context(worktree_abs_path, &location, cx)
.context("building basic default context")?;
basic_context.extend(
context_provider
.build_context(worktree_abs_path, &location, cx)
.context("building provider context ")?,
);
Ok(basic_context)
}
}

View File

@@ -99,12 +99,13 @@ pub fn editor_hunks(
.read(cx)
.excerpt_containing(Point::new(hunk.associated_range.start, 0), cx)
.expect("no excerpt for expanded buffer's hunk start");
let diff_base = &buffer
let diff_base = buffer
.read(cx)
.diff_base()
.expect("should have a diff base for expanded hunk")
[hunk.diff_base_byte_range.clone()];
(diff_base.to_owned(), hunk.status(), display_range)
.slice(hunk.diff_base_byte_range.clone())
.to_string();
(diff_base, hunk.status(), display_range)
})
.collect()
}
@@ -134,16 +135,13 @@ pub fn expanded_hunks(
.read(cx)
.excerpt_containing(expanded_hunk.hunk_range.start, cx)
.expect("no excerpt for expanded buffer's hunk start");
let diff_base = &buffer
let diff_base = buffer
.read(cx)
.diff_base()
.expect("should have a diff base for expanded hunk")
[expanded_hunk.diff_base_byte_range.clone()];
(
diff_base.to_owned(),
expanded_hunk.status,
hunk_display_range,
)
.slice(expanded_hunk.diff_base_byte_range.clone())
.to_string();
(diff_base, expanded_hunk.status, hunk_display_range)
})
.collect()
}

View File

@@ -21,6 +21,7 @@ use std::{
Arc,
},
};
use text::Rope;
use ui::Context;
use util::{
assert_set_eq,
@@ -271,7 +272,7 @@ impl EditorTestContext {
}
pub fn set_diff_base(&mut self, diff_base: Option<&str>) {
let diff_base = diff_base.map(String::from);
let diff_base = diff_base.map(Rope::from);
self.update_buffer(|buffer, cx| buffer.set_diff_base(diff_base, cx));
}

View File

@@ -852,7 +852,7 @@ impl Render for ExtensionsPage {
v_flex()
.gap_4()
.p_4()
.border_b()
.border_b_1()
.border_color(cx.theme().colors().border)
.bg(cx.theme().colors().editor_background)
.child(

View File

@@ -454,7 +454,7 @@ impl Render for FeedbackModal {
.flex_1()
.bg(cx.theme().colors().editor_background)
.p_2()
.border()
.border_1()
.rounded_md()
.border_color(cx.theme().colors().border)
.child(self.feedback_editor.clone()),
@@ -466,7 +466,7 @@ impl Render for FeedbackModal {
h_flex()
.bg(cx.theme().colors().editor_background)
.p_2()
.border()
.border_1()
.rounded_md()
.border_color(if self.valid_email_address() {
cx.theme().colors().border

View File

@@ -20,7 +20,7 @@ impl SystemSpecs {
pub fn new(cx: &AppContext) -> Self {
let app_version = AppVersion::global(cx).to_string();
let release_channel = ReleaseChannel::global(cx);
let os_name = cx.app_metadata().os_name;
let os_name = cx.app_metadata().os.name;
let system = System::new_with_specifics(
RefreshKind::new().with_memory(MemoryRefreshKind::everything()),
);
@@ -28,7 +28,8 @@ impl SystemSpecs {
let architecture = env::consts::ARCH;
let os_version = cx
.app_metadata()
.os_version
.os
.version
.map(|os_version| os_version.to_string());
let commit_sha = match release_channel {
ReleaseChannel::Dev | ReleaseChannel::Nightly => {

View File

@@ -1,4 +1,5 @@
use anyhow::{anyhow, Result};
use git::GitHostingProviderRegistry;
#[cfg(unix)]
use std::os::unix::fs::MetadataExt;
@@ -117,12 +118,19 @@ pub struct Metadata {
#[derive(Default)]
pub struct RealFs {
git_hosting_provider_registry: Arc<GitHostingProviderRegistry>,
git_binary_path: Option<PathBuf>,
}
impl RealFs {
pub fn new(git_binary_path: Option<PathBuf>) -> Self {
Self { git_binary_path }
pub fn new(
git_hosting_provider_registry: Arc<GitHostingProviderRegistry>,
git_binary_path: Option<PathBuf>,
) -> Self {
Self {
git_hosting_provider_registry,
git_binary_path,
}
}
}
@@ -474,6 +482,7 @@ impl Fs for RealFs {
Arc::new(Mutex::new(RealGitRepository::new(
libgit_repository,
self.git_binary_path.clone(),
self.git_hosting_provider_registry.clone(),
)))
})
}

View File

@@ -13,21 +13,23 @@ path = "src/git.rs"
[dependencies]
anyhow.workspace = true
async-trait.workspace = true
clock.workspace = true
collections.workspace = true
derive_more.workspace = true
git2.workspace = true
gpui.workspace = true
lazy_static.workspace = true
log.workspace = true
parking_lot.workspace = true
rope.workspace = true
serde.workspace = true
smol.workspace = true
sum_tree.workspace = true
text.workspace = true
time.workspace = true
url.workspace = true
util.workspace = true
serde.workspace = true
regex.workspace = true
rope.workspace = true
parking_lot.workspace = true
windows.workspace = true
[dev-dependencies]

View File

@@ -1,11 +1,11 @@
use crate::commit::get_messages;
use crate::permalink::{build_commit_permalink, parse_git_remote_url, BuildCommitPermalinkParams};
use crate::Oid;
use crate::{parse_git_remote_url, BuildCommitPermalinkParams, GitHostingProviderRegistry, Oid};
use anyhow::{anyhow, Context, Result};
use collections::{HashMap, HashSet};
use serde::{Deserialize, Serialize};
use std::io::Write;
use std::process::{Command, Stdio};
use std::sync::Arc;
use std::{ops::Range, path::Path};
use text::Rope;
use time;
@@ -34,6 +34,7 @@ impl Blame {
path: &Path,
content: &Rope,
remote_url: Option<String>,
provider_registry: Arc<GitHostingProviderRegistry>,
) -> Result<Self> {
let output = run_git_blame(git_binary, working_directory, path, &content)?;
let mut entries = parse_git_blame(&output)?;
@@ -41,18 +42,22 @@ impl Blame {
let mut permalinks = HashMap::default();
let mut unique_shas = HashSet::default();
let parsed_remote_url = remote_url.as_deref().and_then(parse_git_remote_url);
let parsed_remote_url = remote_url
.as_deref()
.and_then(|remote_url| parse_git_remote_url(provider_registry, remote_url));
for entry in entries.iter_mut() {
unique_shas.insert(entry.sha);
// DEPRECATED (18 Apr 24): Sending permalinks over the wire is deprecated. Clients
// now do the parsing.
if let Some(remote) = parsed_remote_url.as_ref() {
if let Some((provider, remote)) = parsed_remote_url.as_ref() {
permalinks.entry(entry.sha).or_insert_with(|| {
build_commit_permalink(BuildCommitPermalinkParams {
provider.build_commit_permalink(
remote,
sha: entry.sha.to_string().as_str(),
})
BuildCommitPermalinkParams {
sha: entry.sha.to_string().as_str(),
},
)
});
}
}
@@ -255,15 +260,21 @@ fn parse_git_blame(output: &str) -> Result<Vec<BlameEntry>> {
.get(&new_entry.sha)
.and_then(|slot| entries.get(*slot))
{
new_entry.author = existing_entry.author.clone();
new_entry.author_mail = existing_entry.author_mail.clone();
new_entry.author.clone_from(&existing_entry.author);
new_entry
.author_mail
.clone_from(&existing_entry.author_mail);
new_entry.author_time = existing_entry.author_time;
new_entry.author_tz = existing_entry.author_tz.clone();
new_entry.committer = existing_entry.committer.clone();
new_entry.committer_mail = existing_entry.committer_mail.clone();
new_entry.author_tz.clone_from(&existing_entry.author_tz);
new_entry.committer.clone_from(&existing_entry.committer);
new_entry
.committer_mail
.clone_from(&existing_entry.committer_mail);
new_entry.committer_time = existing_entry.committer_time;
new_entry.committer_tz = existing_entry.committer_tz.clone();
new_entry.summary = existing_entry.summary.clone();
new_entry
.committer_tz
.clone_from(&existing_entry.committer_tz);
new_entry.summary.clone_from(&existing_entry.summary);
}
current_entry.replace(new_entry);

View File

@@ -1,3 +1,4 @@
use rope::Rope;
use std::{iter, ops::Range};
use sum_tree::SumTree;
use text::{Anchor, BufferId, BufferSnapshot, OffsetRangeExt, Point};
@@ -179,11 +180,12 @@ impl BufferDiff {
self.tree = SumTree::new();
}
pub async fn update(&mut self, diff_base: &str, buffer: &text::BufferSnapshot) {
pub async fn update(&mut self, diff_base: &Rope, buffer: &text::BufferSnapshot) {
let mut tree = SumTree::new();
let diff_base_text = diff_base.to_string();
let buffer_text = buffer.as_rope().to_string();
let patch = Self::diff(diff_base, &buffer_text);
let patch = Self::diff(&diff_base_text, &buffer_text);
if let Some(patch) = patch {
let mut divergence = 0;
@@ -345,6 +347,7 @@ mod tests {
three
"
.unindent();
let diff_base_rope = Rope::from(diff_base.clone());
let buffer_text = "
one
@@ -355,7 +358,7 @@ mod tests {
let mut buffer = Buffer::new(0, BufferId::new(1).unwrap(), buffer_text);
let mut diff = BufferDiff::new();
smol::block_on(diff.update(&diff_base, &buffer));
smol::block_on(diff.update(&diff_base_rope, &buffer));
assert_hunks(
diff.hunks(&buffer),
&buffer,
@@ -364,7 +367,7 @@ mod tests {
);
buffer.edit([(0..0, "point five\n")]);
smol::block_on(diff.update(&diff_base, &buffer));
smol::block_on(diff.update(&diff_base_rope, &buffer));
assert_hunks(
diff.hunks(&buffer),
&buffer,
@@ -391,6 +394,7 @@ mod tests {
ten
"
.unindent();
let diff_base_rope = Rope::from(diff_base.clone());
let buffer_text = "
A
@@ -415,7 +419,7 @@ mod tests {
let buffer = Buffer::new(0, BufferId::new(1).unwrap(), buffer_text);
let mut diff = BufferDiff::new();
smol::block_on(diff.update(&diff_base, &buffer));
smol::block_on(diff.update(&diff_base_rope, &buffer));
assert_eq!(diff.hunks(&buffer).count(), 8);
assert_hunks(

View File

@@ -1,3 +1,5 @@
mod hosting_provider;
use anyhow::{anyhow, Context, Result};
use serde::{Deserialize, Serialize};
use std::ffi::OsStr;
@@ -7,12 +9,11 @@ use std::str::FromStr;
pub use git2 as libgit;
pub use lazy_static::lazy_static;
pub use crate::hosting_provider::*;
pub mod blame;
pub mod commit;
pub mod diff;
pub mod hosting_provider;
pub mod permalink;
pub mod pull_request;
pub mod repository;
lazy_static! {

View File

@@ -1,110 +1,177 @@
use core::fmt;
use std::{ops::Range, sync::Arc};
use anyhow::Result;
use async_trait::async_trait;
use collections::BTreeMap;
use derive_more::{Deref, DerefMut};
use gpui::{AppContext, Global};
use parking_lot::RwLock;
use url::Url;
use util::{codeberg, github, http::HttpClient};
use util::http::HttpClient;
use crate::Oid;
#[derive(Clone, Debug, Hash)]
pub enum HostingProvider {
Github,
Gitlab,
Gitee,
Bitbucket,
Sourcehut,
Codeberg,
#[derive(Debug, PartialEq, Eq, Clone)]
pub struct PullRequest {
pub number: u32,
pub url: Url,
}
impl HostingProvider {
pub(crate) fn base_url(&self) -> Url {
let base_url = match self {
Self::Github => "https://github.com",
Self::Gitlab => "https://gitlab.com",
Self::Gitee => "https://gitee.com",
Self::Bitbucket => "https://bitbucket.org",
Self::Sourcehut => "https://git.sr.ht",
Self::Codeberg => "https://codeberg.org",
};
pub struct BuildCommitPermalinkParams<'a> {
pub sha: &'a str,
}
Url::parse(&base_url).unwrap()
}
pub struct BuildPermalinkParams<'a> {
pub sha: &'a str,
pub path: &'a str,
pub selection: Option<Range<u32>>,
}
/// Returns the fragment portion of the URL for the selected lines in
/// the representation the [`GitHostingProvider`] expects.
pub(crate) fn line_fragment(&self, selection: &Range<u32>) -> String {
/// A Git hosting provider.
#[async_trait]
pub trait GitHostingProvider {
/// Returns the name of the provider.
fn name(&self) -> String;
/// Returns the base URL of the provider.
fn base_url(&self) -> Url;
/// Returns a permalink to a Git commit on this hosting provider.
fn build_commit_permalink(
&self,
remote: &ParsedGitRemote,
params: BuildCommitPermalinkParams,
) -> Url;
/// Returns a permalink to a file and/or selection on this hosting provider.
fn build_permalink(&self, remote: ParsedGitRemote, params: BuildPermalinkParams) -> Url;
/// Returns whether this provider supports avatars.
fn supports_avatars(&self) -> bool;
/// Returns a URL fragment to the given line selection.
fn line_fragment(&self, selection: &Range<u32>) -> String {
if selection.start == selection.end {
let line = selection.start + 1;
match self {
Self::Github | Self::Gitlab | Self::Gitee | Self::Sourcehut | Self::Codeberg => {
format!("L{}", line)
}
Self::Bitbucket => format!("lines-{}", line),
}
self.format_line_number(line)
} else {
let start_line = selection.start + 1;
let end_line = selection.end + 1;
match self {
Self::Github | Self::Codeberg => format!("L{}-L{}", start_line, end_line),
Self::Gitlab | Self::Gitee | Self::Sourcehut => {
format!("L{}-{}", start_line, end_line)
}
Self::Bitbucket => format!("lines-{}:{}", start_line, end_line),
}
self.format_line_numbers(start_line, end_line)
}
}
pub fn supports_avatars(&self) -> bool {
match self {
HostingProvider::Github | HostingProvider::Codeberg => true,
_ => false,
}
}
/// Returns a formatted line number to be placed in a permalink URL.
fn format_line_number(&self, line: u32) -> String;
pub async fn commit_author_avatar_url(
/// Returns a formatted range of line numbers to be placed in a permalink URL.
fn format_line_numbers(&self, start_line: u32, end_line: u32) -> String;
fn parse_remote_url<'a>(&self, url: &'a str) -> Option<ParsedGitRemote<'a>>;
fn extract_pull_request(
&self,
repo_owner: &str,
repo: &str,
commit: Oid,
client: Arc<dyn HttpClient>,
_remote: &ParsedGitRemote,
_message: &str,
) -> Option<PullRequest> {
None
}
async fn commit_author_avatar_url(
&self,
_repo_owner: &str,
_repo: &str,
_commit: Oid,
_http_client: Arc<dyn HttpClient>,
) -> Result<Option<Url>> {
Ok(match self {
HostingProvider::Github => {
let commit = commit.to_string();
github::fetch_github_commit_author(repo_owner, repo, &commit, &client)
.await?
.map(|author| -> Result<Url, url::ParseError> {
let mut url = Url::parse(&author.avatar_url)?;
url.set_query(Some("size=128"));
Ok(url)
})
.transpose()
}
HostingProvider::Codeberg => {
let commit = commit.to_string();
codeberg::fetch_codeberg_commit_author(repo_owner, repo, &commit, &client)
.await?
.map(|author| Url::parse(&author.avatar_url))
.transpose()
}
_ => Ok(None),
}?)
Ok(None)
}
}
impl fmt::Display for HostingProvider {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let name = match self {
HostingProvider::Github => "GitHub",
HostingProvider::Gitlab => "GitLab",
HostingProvider::Gitee => "Gitee",
HostingProvider::Bitbucket => "Bitbucket",
HostingProvider::Sourcehut => "Sourcehut",
HostingProvider::Codeberg => "Codeberg",
};
write!(f, "{}", name)
#[derive(Default, Deref, DerefMut)]
struct GlobalGitHostingProviderRegistry(Arc<GitHostingProviderRegistry>);
impl Global for GlobalGitHostingProviderRegistry {}
#[derive(Default)]
struct GitHostingProviderRegistryState {
providers: BTreeMap<String, Arc<dyn GitHostingProvider + Send + Sync + 'static>>,
}
#[derive(Default)]
pub struct GitHostingProviderRegistry {
state: RwLock<GitHostingProviderRegistryState>,
}
impl GitHostingProviderRegistry {
/// Returns the global [`GitHostingProviderRegistry`].
pub fn global(cx: &AppContext) -> Arc<Self> {
cx.global::<GlobalGitHostingProviderRegistry>().0.clone()
}
/// Returns the global [`GitHostingProviderRegistry`].
///
/// Inserts a default [`GitHostingProviderRegistry`] if one does not yet exist.
pub fn default_global(cx: &mut AppContext) -> Arc<Self> {
cx.default_global::<GlobalGitHostingProviderRegistry>()
.0
.clone()
}
/// Sets the global [`GitHostingProviderRegistry`].
pub fn set_global(registry: Arc<GitHostingProviderRegistry>, cx: &mut AppContext) {
cx.set_global(GlobalGitHostingProviderRegistry(registry));
}
/// Returns a new [`GitHostingProviderRegistry`].
pub fn new() -> Self {
Self {
state: RwLock::new(GitHostingProviderRegistryState {
providers: BTreeMap::default(),
}),
}
}
/// Returns the list of all [`GitHostingProvider`]s in the registry.
pub fn list_hosting_providers(
&self,
) -> Vec<Arc<dyn GitHostingProvider + Send + Sync + 'static>> {
self.state.read().providers.values().cloned().collect()
}
/// Adds the provided [`GitHostingProvider`] to the registry.
pub fn register_hosting_provider(
&self,
provider: Arc<dyn GitHostingProvider + Send + Sync + 'static>,
) {
self.state
.write()
.providers
.insert(provider.name(), provider);
}
}
#[derive(Debug)]
pub struct ParsedGitRemote<'a> {
pub owner: &'a str,
pub repo: &'a str,
}
pub fn parse_git_remote_url(
provider_registry: Arc<GitHostingProviderRegistry>,
url: &str,
) -> Option<(
Arc<dyn GitHostingProvider + Send + Sync + 'static>,
ParsedGitRemote,
)> {
provider_registry
.list_hosting_providers()
.into_iter()
.find_map(|provider| {
provider
.parse_remote_url(&url)
.map(|parsed_remote| (provider, parsed_remote))
})
}

View File

@@ -1,680 +0,0 @@
use std::ops::Range;
use anyhow::{anyhow, Result};
use url::Url;
use crate::hosting_provider::HostingProvider;
pub struct BuildPermalinkParams<'a> {
pub remote_url: &'a str,
pub sha: &'a str,
pub path: &'a str,
pub selection: Option<Range<u32>>,
}
pub fn build_permalink(params: BuildPermalinkParams) -> Result<Url> {
let BuildPermalinkParams {
remote_url,
sha,
path,
selection,
} = params;
let ParsedGitRemote {
provider,
owner,
repo,
} = parse_git_remote_url(remote_url)
.ok_or_else(|| anyhow!("failed to parse Git remote URL"))?;
let path = match provider {
HostingProvider::Github => format!("{owner}/{repo}/blob/{sha}/{path}"),
HostingProvider::Gitlab => format!("{owner}/{repo}/-/blob/{sha}/{path}"),
HostingProvider::Gitee => format!("{owner}/{repo}/blob/{sha}/{path}"),
HostingProvider::Bitbucket => format!("{owner}/{repo}/src/{sha}/{path}"),
HostingProvider::Sourcehut => format!("~{owner}/{repo}/tree/{sha}/item/{path}"),
HostingProvider::Codeberg => format!("{owner}/{repo}/src/commit/{sha}/{path}"),
};
let line_fragment = selection.map(|selection| provider.line_fragment(&selection));
let mut permalink = provider.base_url().join(&path).unwrap();
permalink.set_fragment(line_fragment.as_deref());
Ok(permalink)
}
#[derive(Debug)]
pub struct ParsedGitRemote<'a> {
pub provider: HostingProvider,
pub owner: &'a str,
pub repo: &'a str,
}
pub struct BuildCommitPermalinkParams<'a> {
pub remote: &'a ParsedGitRemote<'a>,
pub sha: &'a str,
}
pub fn build_commit_permalink(params: BuildCommitPermalinkParams) -> Url {
let BuildCommitPermalinkParams { sha, remote } = params;
let ParsedGitRemote {
provider,
owner,
repo,
} = remote;
let path = match provider {
HostingProvider::Github => format!("{owner}/{repo}/commit/{sha}"),
HostingProvider::Gitlab => format!("{owner}/{repo}/-/commit/{sha}"),
HostingProvider::Gitee => format!("{owner}/{repo}/commit/{sha}"),
HostingProvider::Bitbucket => format!("{owner}/{repo}/commits/{sha}"),
HostingProvider::Sourcehut => format!("~{owner}/{repo}/commit/{sha}"),
HostingProvider::Codeberg => format!("{owner}/{repo}/commit/{sha}"),
};
provider.base_url().join(&path).unwrap()
}
pub fn parse_git_remote_url(url: &str) -> Option<ParsedGitRemote> {
if url.starts_with("git@github.com:") || url.starts_with("https://github.com/") {
let repo_with_owner = url
.trim_start_matches("git@github.com:")
.trim_start_matches("https://github.com/")
.trim_end_matches(".git");
let (owner, repo) = repo_with_owner.split_once('/')?;
return Some(ParsedGitRemote {
provider: HostingProvider::Github,
owner,
repo,
});
}
if url.starts_with("git@gitlab.com:") || url.starts_with("https://gitlab.com/") {
let repo_with_owner = url
.trim_start_matches("git@gitlab.com:")
.trim_start_matches("https://gitlab.com/")
.trim_end_matches(".git");
let (owner, repo) = repo_with_owner.split_once('/')?;
return Some(ParsedGitRemote {
provider: HostingProvider::Gitlab,
owner,
repo,
});
}
if url.starts_with("git@gitee.com:") || url.starts_with("https://gitee.com/") {
let repo_with_owner = url
.trim_start_matches("git@gitee.com:")
.trim_start_matches("https://gitee.com/")
.trim_end_matches(".git");
let (owner, repo) = repo_with_owner.split_once('/')?;
return Some(ParsedGitRemote {
provider: HostingProvider::Gitee,
owner,
repo,
});
}
if url.contains("bitbucket.org") {
let (_, repo_with_owner) = url.trim_end_matches(".git").split_once("bitbucket.org")?;
let (owner, repo) = repo_with_owner
.trim_start_matches('/')
.trim_start_matches(':')
.split_once('/')?;
return Some(ParsedGitRemote {
provider: HostingProvider::Bitbucket,
owner,
repo,
});
}
if url.starts_with("git@git.sr.ht:") || url.starts_with("https://git.sr.ht/") {
// sourcehut indicates a repo with '.git' suffix as a separate repo.
// For example, "git@git.sr.ht:~username/repo" and "git@git.sr.ht:~username/repo.git"
// are two distinct repositories.
let repo_with_owner = url
.trim_start_matches("git@git.sr.ht:~")
.trim_start_matches("https://git.sr.ht/~");
let (owner, repo) = repo_with_owner.split_once('/')?;
return Some(ParsedGitRemote {
provider: HostingProvider::Sourcehut,
owner,
repo,
});
}
if url.starts_with("git@codeberg.org:") || url.starts_with("https://codeberg.org/") {
let repo_with_owner = url
.trim_start_matches("git@codeberg.org:")
.trim_start_matches("https://codeberg.org/")
.trim_end_matches(".git");
let (owner, repo) = repo_with_owner.split_once('/')?;
return Some(ParsedGitRemote {
provider: HostingProvider::Codeberg,
owner,
repo,
});
}
None
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_build_github_permalink_from_ssh_url() {
let permalink = build_permalink(BuildPermalinkParams {
remote_url: "git@github.com:zed-industries/zed.git",
sha: "e6ebe7974deb6bb6cc0e2595c8ec31f0c71084b7",
path: "crates/editor/src/git/permalink.rs",
selection: None,
})
.unwrap();
let expected_url = "https://github.com/zed-industries/zed/blob/e6ebe7974deb6bb6cc0e2595c8ec31f0c71084b7/crates/editor/src/git/permalink.rs";
assert_eq!(permalink.to_string(), expected_url.to_string())
}
#[test]
fn test_build_github_permalink_from_ssh_url_single_line_selection() {
let permalink = build_permalink(BuildPermalinkParams {
remote_url: "git@github.com:zed-industries/zed.git",
sha: "e6ebe7974deb6bb6cc0e2595c8ec31f0c71084b7",
path: "crates/editor/src/git/permalink.rs",
selection: Some(6..6),
})
.unwrap();
let expected_url = "https://github.com/zed-industries/zed/blob/e6ebe7974deb6bb6cc0e2595c8ec31f0c71084b7/crates/editor/src/git/permalink.rs#L7";
assert_eq!(permalink.to_string(), expected_url.to_string())
}
#[test]
fn test_build_github_permalink_from_ssh_url_multi_line_selection() {
let permalink = build_permalink(BuildPermalinkParams {
remote_url: "git@github.com:zed-industries/zed.git",
sha: "e6ebe7974deb6bb6cc0e2595c8ec31f0c71084b7",
path: "crates/editor/src/git/permalink.rs",
selection: Some(23..47),
})
.unwrap();
let expected_url = "https://github.com/zed-industries/zed/blob/e6ebe7974deb6bb6cc0e2595c8ec31f0c71084b7/crates/editor/src/git/permalink.rs#L24-L48";
assert_eq!(permalink.to_string(), expected_url.to_string())
}
#[test]
fn test_build_github_permalink_from_https_url() {
let permalink = build_permalink(BuildPermalinkParams {
remote_url: "https://github.com/zed-industries/zed.git",
sha: "b2efec9824c45fcc90c9a7eb107a50d1772a60aa",
path: "crates/zed/src/main.rs",
selection: None,
})
.unwrap();
let expected_url = "https://github.com/zed-industries/zed/blob/b2efec9824c45fcc90c9a7eb107a50d1772a60aa/crates/zed/src/main.rs";
assert_eq!(permalink.to_string(), expected_url.to_string())
}
#[test]
fn test_build_github_permalink_from_https_url_single_line_selection() {
let permalink = build_permalink(BuildPermalinkParams {
remote_url: "https://github.com/zed-industries/zed.git",
sha: "b2efec9824c45fcc90c9a7eb107a50d1772a60aa",
path: "crates/zed/src/main.rs",
selection: Some(6..6),
})
.unwrap();
let expected_url = "https://github.com/zed-industries/zed/blob/b2efec9824c45fcc90c9a7eb107a50d1772a60aa/crates/zed/src/main.rs#L7";
assert_eq!(permalink.to_string(), expected_url.to_string())
}
#[test]
fn test_build_github_permalink_from_https_url_multi_line_selection() {
let permalink = build_permalink(BuildPermalinkParams {
remote_url: "https://github.com/zed-industries/zed.git",
sha: "b2efec9824c45fcc90c9a7eb107a50d1772a60aa",
path: "crates/zed/src/main.rs",
selection: Some(23..47),
})
.unwrap();
let expected_url = "https://github.com/zed-industries/zed/blob/b2efec9824c45fcc90c9a7eb107a50d1772a60aa/crates/zed/src/main.rs#L24-L48";
assert_eq!(permalink.to_string(), expected_url.to_string())
}
#[test]
fn test_build_gitlab_permalink_from_ssh_url() {
let permalink = build_permalink(BuildPermalinkParams {
remote_url: "git@gitlab.com:zed-industries/zed.git",
sha: "e6ebe7974deb6bb6cc0e2595c8ec31f0c71084b7",
path: "crates/editor/src/git/permalink.rs",
selection: None,
})
.unwrap();
let expected_url = "https://gitlab.com/zed-industries/zed/-/blob/e6ebe7974deb6bb6cc0e2595c8ec31f0c71084b7/crates/editor/src/git/permalink.rs";
assert_eq!(permalink.to_string(), expected_url.to_string())
}
#[test]
fn test_build_gitlab_permalink_from_ssh_url_single_line_selection() {
let permalink = build_permalink(BuildPermalinkParams {
remote_url: "git@gitlab.com:zed-industries/zed.git",
sha: "e6ebe7974deb6bb6cc0e2595c8ec31f0c71084b7",
path: "crates/editor/src/git/permalink.rs",
selection: Some(6..6),
})
.unwrap();
let expected_url = "https://gitlab.com/zed-industries/zed/-/blob/e6ebe7974deb6bb6cc0e2595c8ec31f0c71084b7/crates/editor/src/git/permalink.rs#L7";
assert_eq!(permalink.to_string(), expected_url.to_string())
}
#[test]
fn test_build_gitlab_permalink_from_ssh_url_multi_line_selection() {
let permalink = build_permalink(BuildPermalinkParams {
remote_url: "git@gitlab.com:zed-industries/zed.git",
sha: "e6ebe7974deb6bb6cc0e2595c8ec31f0c71084b7",
path: "crates/editor/src/git/permalink.rs",
selection: Some(23..47),
})
.unwrap();
let expected_url = "https://gitlab.com/zed-industries/zed/-/blob/e6ebe7974deb6bb6cc0e2595c8ec31f0c71084b7/crates/editor/src/git/permalink.rs#L24-48";
assert_eq!(permalink.to_string(), expected_url.to_string())
}
#[test]
fn test_build_gitlab_permalink_from_https_url() {
let permalink = build_permalink(BuildPermalinkParams {
remote_url: "https://gitlab.com/zed-industries/zed.git",
sha: "b2efec9824c45fcc90c9a7eb107a50d1772a60aa",
path: "crates/zed/src/main.rs",
selection: None,
})
.unwrap();
let expected_url = "https://gitlab.com/zed-industries/zed/-/blob/b2efec9824c45fcc90c9a7eb107a50d1772a60aa/crates/zed/src/main.rs";
assert_eq!(permalink.to_string(), expected_url.to_string())
}
#[test]
fn test_build_gitlab_permalink_from_https_url_single_line_selection() {
let permalink = build_permalink(BuildPermalinkParams {
remote_url: "https://gitlab.com/zed-industries/zed.git",
sha: "b2efec9824c45fcc90c9a7eb107a50d1772a60aa",
path: "crates/zed/src/main.rs",
selection: Some(6..6),
})
.unwrap();
let expected_url = "https://gitlab.com/zed-industries/zed/-/blob/b2efec9824c45fcc90c9a7eb107a50d1772a60aa/crates/zed/src/main.rs#L7";
assert_eq!(permalink.to_string(), expected_url.to_string())
}
#[test]
fn test_build_gitlab_permalink_from_https_url_multi_line_selection() {
let permalink = build_permalink(BuildPermalinkParams {
remote_url: "https://gitlab.com/zed-industries/zed.git",
sha: "b2efec9824c45fcc90c9a7eb107a50d1772a60aa",
path: "crates/zed/src/main.rs",
selection: Some(23..47),
})
.unwrap();
let expected_url = "https://gitlab.com/zed-industries/zed/-/blob/b2efec9824c45fcc90c9a7eb107a50d1772a60aa/crates/zed/src/main.rs#L24-48";
assert_eq!(permalink.to_string(), expected_url.to_string())
}
#[test]
fn test_build_gitee_permalink_from_ssh_url() {
let permalink = build_permalink(BuildPermalinkParams {
remote_url: "git@gitee.com:libkitten/zed.git",
sha: "e5fe811d7ad0fc26934edd76f891d20bdc3bb194",
path: "crates/editor/src/git/permalink.rs",
selection: None,
})
.unwrap();
let expected_url = "https://gitee.com/libkitten/zed/blob/e5fe811d7ad0fc26934edd76f891d20bdc3bb194/crates/editor/src/git/permalink.rs";
assert_eq!(permalink.to_string(), expected_url.to_string())
}
#[test]
fn test_build_gitee_permalink_from_ssh_url_single_line_selection() {
let permalink = build_permalink(BuildPermalinkParams {
remote_url: "git@gitee.com:libkitten/zed.git",
sha: "e5fe811d7ad0fc26934edd76f891d20bdc3bb194",
path: "crates/editor/src/git/permalink.rs",
selection: Some(6..6),
})
.unwrap();
let expected_url = "https://gitee.com/libkitten/zed/blob/e5fe811d7ad0fc26934edd76f891d20bdc3bb194/crates/editor/src/git/permalink.rs#L7";
assert_eq!(permalink.to_string(), expected_url.to_string())
}
#[test]
fn test_build_gitee_permalink_from_ssh_url_multi_line_selection() {
let permalink = build_permalink(BuildPermalinkParams {
remote_url: "git@gitee.com:libkitten/zed.git",
sha: "e5fe811d7ad0fc26934edd76f891d20bdc3bb194",
path: "crates/editor/src/git/permalink.rs",
selection: Some(23..47),
})
.unwrap();
let expected_url = "https://gitee.com/libkitten/zed/blob/e5fe811d7ad0fc26934edd76f891d20bdc3bb194/crates/editor/src/git/permalink.rs#L24-48";
assert_eq!(permalink.to_string(), expected_url.to_string())
}
#[test]
fn test_build_gitee_permalink_from_https_url() {
let permalink = build_permalink(BuildPermalinkParams {
remote_url: "https://gitee.com/libkitten/zed.git",
sha: "e5fe811d7ad0fc26934edd76f891d20bdc3bb194",
path: "crates/zed/src/main.rs",
selection: None,
})
.unwrap();
let expected_url = "https://gitee.com/libkitten/zed/blob/e5fe811d7ad0fc26934edd76f891d20bdc3bb194/crates/zed/src/main.rs";
assert_eq!(permalink.to_string(), expected_url.to_string())
}
#[test]
fn test_build_gitee_permalink_from_https_url_single_line_selection() {
let permalink = build_permalink(BuildPermalinkParams {
remote_url: "https://gitee.com/libkitten/zed.git",
sha: "e5fe811d7ad0fc26934edd76f891d20bdc3bb194",
path: "crates/zed/src/main.rs",
selection: Some(6..6),
})
.unwrap();
let expected_url = "https://gitee.com/libkitten/zed/blob/e5fe811d7ad0fc26934edd76f891d20bdc3bb194/crates/zed/src/main.rs#L7";
assert_eq!(permalink.to_string(), expected_url.to_string())
}
#[test]
fn test_build_gitee_permalink_from_https_url_multi_line_selection() {
let permalink = build_permalink(BuildPermalinkParams {
remote_url: "https://gitee.com/libkitten/zed.git",
sha: "e5fe811d7ad0fc26934edd76f891d20bdc3bb194",
path: "crates/zed/src/main.rs",
selection: Some(23..47),
})
.unwrap();
let expected_url = "https://gitee.com/libkitten/zed/blob/e5fe811d7ad0fc26934edd76f891d20bdc3bb194/crates/zed/src/main.rs#L24-48";
assert_eq!(permalink.to_string(), expected_url.to_string())
}
#[test]
fn test_parse_git_remote_url_bitbucket_https_with_username() {
let url = "https://thorstenballzed@bitbucket.org/thorstenzed/testingrepo.git";
let parsed = parse_git_remote_url(url).unwrap();
assert!(matches!(parsed.provider, HostingProvider::Bitbucket));
assert_eq!(parsed.owner, "thorstenzed");
assert_eq!(parsed.repo, "testingrepo");
}
#[test]
fn test_parse_git_remote_url_bitbucket_https_without_username() {
let url = "https://bitbucket.org/thorstenzed/testingrepo.git";
let parsed = parse_git_remote_url(url).unwrap();
assert!(matches!(parsed.provider, HostingProvider::Bitbucket));
assert_eq!(parsed.owner, "thorstenzed");
assert_eq!(parsed.repo, "testingrepo");
}
#[test]
fn test_parse_git_remote_url_bitbucket_git() {
let url = "git@bitbucket.org:thorstenzed/testingrepo.git";
let parsed = parse_git_remote_url(url).unwrap();
assert!(matches!(parsed.provider, HostingProvider::Bitbucket));
assert_eq!(parsed.owner, "thorstenzed");
assert_eq!(parsed.repo, "testingrepo");
}
#[test]
fn test_build_bitbucket_permalink_from_ssh_url() {
let permalink = build_permalink(BuildPermalinkParams {
remote_url: "git@bitbucket.org:thorstenzed/testingrepo.git",
sha: "f00b4r",
path: "main.rs",
selection: None,
})
.unwrap();
let expected_url = "https://bitbucket.org/thorstenzed/testingrepo/src/f00b4r/main.rs";
assert_eq!(permalink.to_string(), expected_url.to_string())
}
#[test]
fn test_build_bitbucket_permalink_from_ssh_url_single_line_selection() {
let permalink = build_permalink(BuildPermalinkParams {
remote_url: "git@bitbucket.org:thorstenzed/testingrepo.git",
sha: "f00b4r",
path: "main.rs",
selection: Some(6..6),
})
.unwrap();
let expected_url =
"https://bitbucket.org/thorstenzed/testingrepo/src/f00b4r/main.rs#lines-7";
assert_eq!(permalink.to_string(), expected_url.to_string())
}
#[test]
fn test_build_bitbucket_permalink_from_ssh_url_multi_line_selection() {
let permalink = build_permalink(BuildPermalinkParams {
remote_url: "git@bitbucket.org:thorstenzed/testingrepo.git",
sha: "f00b4r",
path: "main.rs",
selection: Some(23..47),
})
.unwrap();
let expected_url =
"https://bitbucket.org/thorstenzed/testingrepo/src/f00b4r/main.rs#lines-24:48";
assert_eq!(permalink.to_string(), expected_url.to_string())
}
#[test]
fn test_build_sourcehut_permalink_from_ssh_url() {
let permalink = build_permalink(BuildPermalinkParams {
remote_url: "git@git.sr.ht:~rajveermalviya/zed",
sha: "faa6f979be417239b2e070dbbf6392b909224e0b",
path: "crates/editor/src/git/permalink.rs",
selection: None,
})
.unwrap();
let expected_url = "https://git.sr.ht/~rajveermalviya/zed/tree/faa6f979be417239b2e070dbbf6392b909224e0b/item/crates/editor/src/git/permalink.rs";
assert_eq!(permalink.to_string(), expected_url.to_string())
}
#[test]
fn test_build_sourcehut_permalink_from_ssh_url_with_git_prefix() {
let permalink = build_permalink(BuildPermalinkParams {
remote_url: "git@git.sr.ht:~rajveermalviya/zed.git",
sha: "faa6f979be417239b2e070dbbf6392b909224e0b",
path: "crates/editor/src/git/permalink.rs",
selection: None,
})
.unwrap();
let expected_url = "https://git.sr.ht/~rajveermalviya/zed.git/tree/faa6f979be417239b2e070dbbf6392b909224e0b/item/crates/editor/src/git/permalink.rs";
assert_eq!(permalink.to_string(), expected_url.to_string())
}
#[test]
fn test_build_sourcehut_permalink_from_ssh_url_single_line_selection() {
let permalink = build_permalink(BuildPermalinkParams {
remote_url: "git@git.sr.ht:~rajveermalviya/zed",
sha: "faa6f979be417239b2e070dbbf6392b909224e0b",
path: "crates/editor/src/git/permalink.rs",
selection: Some(6..6),
})
.unwrap();
let expected_url = "https://git.sr.ht/~rajveermalviya/zed/tree/faa6f979be417239b2e070dbbf6392b909224e0b/item/crates/editor/src/git/permalink.rs#L7";
assert_eq!(permalink.to_string(), expected_url.to_string())
}
#[test]
fn test_build_sourcehut_permalink_from_ssh_url_multi_line_selection() {
let permalink = build_permalink(BuildPermalinkParams {
remote_url: "git@git.sr.ht:~rajveermalviya/zed",
sha: "faa6f979be417239b2e070dbbf6392b909224e0b",
path: "crates/editor/src/git/permalink.rs",
selection: Some(23..47),
})
.unwrap();
let expected_url = "https://git.sr.ht/~rajveermalviya/zed/tree/faa6f979be417239b2e070dbbf6392b909224e0b/item/crates/editor/src/git/permalink.rs#L24-48";
assert_eq!(permalink.to_string(), expected_url.to_string())
}
#[test]
fn test_build_sourcehut_permalink_from_https_url() {
let permalink = build_permalink(BuildPermalinkParams {
remote_url: "https://git.sr.ht/~rajveermalviya/zed",
sha: "faa6f979be417239b2e070dbbf6392b909224e0b",
path: "crates/zed/src/main.rs",
selection: None,
})
.unwrap();
let expected_url = "https://git.sr.ht/~rajveermalviya/zed/tree/faa6f979be417239b2e070dbbf6392b909224e0b/item/crates/zed/src/main.rs";
assert_eq!(permalink.to_string(), expected_url.to_string())
}
#[test]
fn test_build_sourcehut_permalink_from_https_url_single_line_selection() {
let permalink = build_permalink(BuildPermalinkParams {
remote_url: "https://git.sr.ht/~rajveermalviya/zed",
sha: "faa6f979be417239b2e070dbbf6392b909224e0b",
path: "crates/zed/src/main.rs",
selection: Some(6..6),
})
.unwrap();
let expected_url = "https://git.sr.ht/~rajveermalviya/zed/tree/faa6f979be417239b2e070dbbf6392b909224e0b/item/crates/zed/src/main.rs#L7";
assert_eq!(permalink.to_string(), expected_url.to_string())
}
#[test]
fn test_build_sourcehut_permalink_from_https_url_multi_line_selection() {
let permalink = build_permalink(BuildPermalinkParams {
remote_url: "https://git.sr.ht/~rajveermalviya/zed",
sha: "faa6f979be417239b2e070dbbf6392b909224e0b",
path: "crates/zed/src/main.rs",
selection: Some(23..47),
})
.unwrap();
let expected_url = "https://git.sr.ht/~rajveermalviya/zed/tree/faa6f979be417239b2e070dbbf6392b909224e0b/item/crates/zed/src/main.rs#L24-48";
assert_eq!(permalink.to_string(), expected_url.to_string())
}
#[test]
fn test_build_codeberg_permalink_from_ssh_url() {
let permalink = build_permalink(BuildPermalinkParams {
remote_url: "git@codeberg.org:rajveermalviya/zed.git",
sha: "faa6f979be417239b2e070dbbf6392b909224e0b",
path: "crates/editor/src/git/permalink.rs",
selection: None,
})
.unwrap();
let expected_url = "https://codeberg.org/rajveermalviya/zed/src/commit/faa6f979be417239b2e070dbbf6392b909224e0b/crates/editor/src/git/permalink.rs";
assert_eq!(permalink.to_string(), expected_url.to_string())
}
#[test]
fn test_build_codeberg_permalink_from_ssh_url_single_line_selection() {
let permalink = build_permalink(BuildPermalinkParams {
remote_url: "git@codeberg.org:rajveermalviya/zed.git",
sha: "faa6f979be417239b2e070dbbf6392b909224e0b",
path: "crates/editor/src/git/permalink.rs",
selection: Some(6..6),
})
.unwrap();
let expected_url = "https://codeberg.org/rajveermalviya/zed/src/commit/faa6f979be417239b2e070dbbf6392b909224e0b/crates/editor/src/git/permalink.rs#L7";
assert_eq!(permalink.to_string(), expected_url.to_string())
}
#[test]
fn test_build_codeberg_permalink_from_ssh_url_multi_line_selection() {
let permalink = build_permalink(BuildPermalinkParams {
remote_url: "git@codeberg.org:rajveermalviya/zed.git",
sha: "faa6f979be417239b2e070dbbf6392b909224e0b",
path: "crates/editor/src/git/permalink.rs",
selection: Some(23..47),
})
.unwrap();
let expected_url = "https://codeberg.org/rajveermalviya/zed/src/commit/faa6f979be417239b2e070dbbf6392b909224e0b/crates/editor/src/git/permalink.rs#L24-L48";
assert_eq!(permalink.to_string(), expected_url.to_string())
}
#[test]
fn test_build_codeberg_permalink_from_https_url() {
let permalink = build_permalink(BuildPermalinkParams {
remote_url: "https://codeberg.org/rajveermalviya/zed.git",
sha: "faa6f979be417239b2e070dbbf6392b909224e0b",
path: "crates/zed/src/main.rs",
selection: None,
})
.unwrap();
let expected_url = "https://codeberg.org/rajveermalviya/zed/src/commit/faa6f979be417239b2e070dbbf6392b909224e0b/crates/zed/src/main.rs";
assert_eq!(permalink.to_string(), expected_url.to_string())
}
#[test]
fn test_build_codeberg_permalink_from_https_url_single_line_selection() {
let permalink = build_permalink(BuildPermalinkParams {
remote_url: "https://codeberg.org/rajveermalviya/zed.git",
sha: "faa6f979be417239b2e070dbbf6392b909224e0b",
path: "crates/zed/src/main.rs",
selection: Some(6..6),
})
.unwrap();
let expected_url = "https://codeberg.org/rajveermalviya/zed/src/commit/faa6f979be417239b2e070dbbf6392b909224e0b/crates/zed/src/main.rs#L7";
assert_eq!(permalink.to_string(), expected_url.to_string())
}
#[test]
fn test_build_codeberg_permalink_from_https_url_multi_line_selection() {
let permalink = build_permalink(BuildPermalinkParams {
remote_url: "https://codeberg.org/rajveermalviya/zed.git",
sha: "faa6f979be417239b2e070dbbf6392b909224e0b",
path: "crates/zed/src/main.rs",
selection: Some(23..47),
})
.unwrap();
let expected_url = "https://codeberg.org/rajveermalviya/zed/src/commit/faa6f979be417239b2e070dbbf6392b909224e0b/crates/zed/src/main.rs#L24-L48";
assert_eq!(permalink.to_string(), expected_url.to_string())
}
}

View File

@@ -1,83 +0,0 @@
use lazy_static::lazy_static;
use url::Url;
use crate::{hosting_provider::HostingProvider, permalink::ParsedGitRemote};
lazy_static! {
static ref GITHUB_PULL_REQUEST_NUMBER: regex::Regex =
regex::Regex::new(r"\(#(\d+)\)$").unwrap();
}
#[derive(Clone, Debug)]
pub struct PullRequest {
pub number: u32,
pub url: Url,
}
pub fn extract_pull_request(remote: &ParsedGitRemote, message: &str) -> Option<PullRequest> {
match remote.provider {
HostingProvider::Github => {
let line = message.lines().next()?;
let capture = GITHUB_PULL_REQUEST_NUMBER.captures(line)?;
let number = capture.get(1)?.as_str().parse::<u32>().ok()?;
let mut url = remote.provider.base_url();
let path = format!("/{}/{}/pull/{}", remote.owner, remote.repo, number);
url.set_path(&path);
Some(PullRequest { number, url })
}
_ => None,
}
}
#[cfg(test)]
mod tests {
use unindent::Unindent;
use crate::{
hosting_provider::HostingProvider, permalink::ParsedGitRemote,
pull_request::extract_pull_request,
};
#[test]
fn test_github_pull_requests() {
let remote = ParsedGitRemote {
provider: HostingProvider::Github,
owner: "zed-industries",
repo: "zed",
};
let message = "This does not contain a pull request";
assert!(extract_pull_request(&remote, message).is_none());
// Pull request number at end of first line
let message = r#"
project panel: do not expand collapsed worktrees on "collapse all entries" (#10687)
Fixes #10597
Release Notes:
- Fixed "project panel: collapse all entries" expanding collapsed worktrees.
"#
.unindent();
assert_eq!(
extract_pull_request(&remote, &message)
.unwrap()
.url
.as_str(),
"https://github.com/zed-industries/zed/pull/10687"
);
// Pull request number in middle of line, which we want to ignore
let message = r#"
Follow-up to #10687 to fix problems
See the original PR, this is a fix.
"#
.unindent();
assert!(extract_pull_request(&remote, &message).is_none());
}
}

View File

@@ -1,4 +1,5 @@
use crate::blame::Blame;
use crate::GitHostingProviderRegistry;
use anyhow::{Context, Result};
use collections::HashMap;
use git2::{BranchType, StatusShow};
@@ -71,13 +72,19 @@ impl std::fmt::Debug for dyn GitRepository {
pub struct RealGitRepository {
pub repository: LibGitRepository,
pub git_binary_path: PathBuf,
hosting_provider_registry: Arc<GitHostingProviderRegistry>,
}
impl RealGitRepository {
pub fn new(repository: LibGitRepository, git_binary_path: Option<PathBuf>) -> Self {
pub fn new(
repository: LibGitRepository,
git_binary_path: Option<PathBuf>,
hosting_provider_registry: Arc<GitHostingProviderRegistry>,
) -> Self {
Self {
repository,
git_binary_path: git_binary_path.unwrap_or_else(|| PathBuf::from("git")),
hosting_provider_registry,
}
}
}
@@ -246,6 +253,7 @@ impl GitRepository for RealGitRepository {
path,
&content,
remote_url,
self.hosting_provider_registry.clone(),
)
}
}

View File

@@ -0,0 +1,30 @@
[package]
name = "git_hosting_providers"
version = "0.1.0"
edition = "2021"
publish = false
license = "GPL-3.0-or-later"
[lints]
workspace = true
[lib]
path = "src/git_hosting_providers.rs"
[dependencies]
anyhow.workspace = true
async-trait.workspace = true
futures.workspace = true
git.workspace = true
gpui.workspace = true
isahc.workspace = true
regex.workspace = true
serde.workspace = true
serde_json.workspace = true
url.workspace = true
util.workspace = true
[dev-dependencies]
unindent.workspace = true
serde_json.workspace = true
pretty_assertions.workspace = true

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