Compare commits

...

220 Commits

Author SHA1 Message Date
Mikayla
e19f6416d4 Merge branch 'main' into tab-bar-settings 2024-04-26 14:32:57 -07:00
Jakob Hellermann
393b16d226 Fix Wayland keyrepeat getting cancelled by unrelated keyup (#11052)
fixes #11048

## Problem
in the situation `press right`, `press left`, `release right` the
following happens right now:

- `keypressed right`, `current_keysym` is set to `right`
- `keypressed left`, `current_keysym` is set to `left`

the repeat timer runs asynchronously and emits keyrepeats since
`current_keysym.is_some()`

- `keyreleased right`, `current_keysym` is set to None

the repeat timer no longer emits keyrepeats

- `keyreleased left`, this is where `current_keysym` should actually be
set to None.

## Solution
Only reset `current_keysym` if the released key matches the last pressed
key.

Release Notes:

- N/A
2024-04-26 14:07:05 -07:00
Akilan Elango
7bd18fa653 Sync maximized state from top-level configure event for a wayland window (#11003)
* Otherwise is_maximized always returns `true`



Release Notes:

- Fixed maximized state. Tested with a dummy maximize/restore button
with the `zoom()` (not implemented yet). Without the right `maximized`,
in toggle zoom function is not possible to call `set_maximized()` or
`unset_maximized()`.

```rust
    fn zoom(&self) {
      if self.is_maximized() {
        self.borrow_mut().toplevel.unset_maximized();
      } else {
        self.borrow_mut().toplevel.set_maximized();
      }
    }
```
2024-04-26 14:03:19 -07:00
张小白
11dc3c2582 windows: Support all OpenType font features (#10756)
Release Notes:

- Added support for all `OpenType` font features to DirectWrite.



https://github.com/zed-industries/zed/assets/14981363/cb2848cd-9178-4d87-881a-54dc646b2b61

---------

Co-authored-by: Mikayla Maki <mikayla@zed.dev>
2024-04-26 13:58:12 -07:00
张小白
268cb948a7 windows: Move manifest file to gpui (#11036)
This is a follow up of #10810 , `embed-resource` crate uses a different
method to link the manifest file, so this makes moving manifest file to
`gpui` possible.

Now, examples can run as expected:
![Screenshot 2024-04-26
111559](https://github.com/zed-industries/zed/assets/14981363/bb040690-8129-490b-83b3-0a7d3cbd4953)

TODO:
- [ ] check if it builds with gnu toolchain

Release Notes:

- N/A
2024-04-26 13:56:48 -07:00
张小白
6a915e349c windows: Fix panicking on startup (#11028)
### Connection:
Closes #10954 

Release Notes:

- N/A
2024-04-26 13:55:41 -07:00
apricotbucket28
70d03e4841 x11: Fix window close (#11008)
Fixes https://github.com/zed-industries/zed/issues/10483 on X11

Also calls the `should_close` callback before closing the window (needed
for the "Do you want to save?" dialog).

Release Notes:

- N/A
2024-04-26 13:53:49 -07:00
DissolveDZ
b1eb0291dc Re-add README.md which might have been deleted by mistake (#11067)
Release Notes:

- N/A
2024-04-26 16:04:25 -04:00
Conrad Irwin
e0644de90e Fix panic in Diagnostics (#11066)
cc @maxbrunsfeld

Release Notes:

- Fixed a panic in populating diagnostics
2024-04-26 14:04:18 -06:00
Bennet Bo Fenner
9329ef1d78 markdown preview: Break up list items into individual blocks (#10852)
Fixes a panic related to rendering checkboxes, see #10824.

Currently we are rendering a list into a single block, meaning the whole
block has to be rendered when it is visible on screen. This would lead
to performance problems when a single list block contained a lot of
items (especially if it contained checkboxes). This PR splits up list
items into separate blocks, meaning only the actual visible list items
on screen get rendered, instead of the whole list.
A nice side effect of the refactoring is, that you can actually click on
individual list items now:


https://github.com/zed-industries/zed/assets/53836821/5ef4200c-bd85-4e96-a8bf-e0c8b452f762

Release Notes:

- Improved rendering performance of list elements inside the markdown
preview

---------

Co-authored-by: Remco <djsmits12@gmail.com>
2024-04-26 21:34:45 +02:00
Conrad Irwin
664f779eb4 new path picker (#11015)
Still TODO:

* Disable the new save-as for local projects
* Wire up sending the new path to the remote server

Release Notes:

- Added the ability to "Save-as" in remote projects

---------

Co-authored-by: Nathan <nathan@zed.dev>
Co-authored-by: Bennet <bennetbo@gmx.de>
2024-04-26 13:25:25 -06:00
Bennet Bo Fenner
314b723292 remote projects: Allow reusing window (#11058)
Release Notes:

- Allow reusing the window when opening a remote project from the recent
projects picker
- Fixed an issue, which would not let you rejoin a remote project after
disconnecting from it for the first time

---------

Co-authored-by: Conrad <conrad@zed.dev>
Co-authored-by: Remco <djsmits12@gmail.com>
2024-04-26 21:04:34 +02:00
Tim Masliuchenko
1af1a9e8b3 Toggle tasks modal in task::Rerun, when no tasks have been scheduled (#11059)
Currently, when no tasks have been scheduled, the `task::Rerun` action
does nothing.
This PR adds a fallback, so when no tasks have been scheduled so far the
`task::Rerun` action toggles the tasks modal

https://github.com/zed-industries/zed/assets/471335/72f7a71e-cfa8-49db-a295-fb05b2e7c905

Release Notes:

- Improved the `task::Rerun` action to toggle the tasks modal when no
tasks have been scheduled so far
2024-04-26 17:56:34 +02:00
Jakob Hellermann
8006f69513 Fix Cargo.toml typo ref -> rev (#11047)
Release Notes:

- N/A
2024-04-26 08:31:04 -04:00
Piotr Osiewicz
bacc92333a tasks: Fix divider position in modal (#11049)
The divider between templates and recent runs is constant, regardless of
the currently used filter string; this can lead to situations where an
user can remove the predefined task, which isn't good at all.

Additionally, in this PR I've made it so that recent runs always show up
before task templates in filtered list.

Release Notes:

- Fixed position of list divider in task modal.
2024-04-26 14:29:16 +02:00
Conrad Irwin
eb7bd0b98a Use fewer fancy cursors even for vim users (#11041)
Release Notes:

- N/A
2024-04-26 09:42:21 +02:00
Conrad Irwin
7f229dc202 Remove unread notes indicator for now (#11035)
I'd like to add something back here, but it's more distracting than
helpful today.

Fixes: #10887

Release Notes:

- Removed channel notes unread indicator

---------

Co-authored-by: Marshall Bowers <elliott.codes@gmail.com>
2024-04-25 23:50:31 -04:00
Conrad Irwin
03d0b68f0c Fix panic in rename selections (#11033)
cc @someonetoignore

Release Notes:

- Fixed a panic when renaming with a selection (preview only)
2024-04-25 21:29:56 -06:00
Hans
5c2f27a501 Fix VIM cw on last character of a word doesn't work as expected: (#10963)
At the moment, using the default expand_selection seems to do the job
well, without the need for some additional logic, which may also make
the code a little clearer, Fix #10945



Release Notes:


- N/A
2024-04-25 21:09:06 -06:00
Conrad Irwin
d9d509a2bb Send installation id with crashes (#11032)
This will let us prioritize crashes that affect many users.

Release Notes:

- N/A
2024-04-25 21:07:06 -06:00
Marshall Bowers
a4ad3bcc08 Hoist nanoid to workspace-level (#11029)
This PR hoists `nanoid` up to a workspace dependency.

Release Notes:

- N/A
2024-04-25 22:37:40 -04:00
Conrad Irwin
6d7332e80c Fix panic in vim search (#11022)
Release Notes:

- vim: Fixed a panic when searching
2024-04-25 20:32:15 -06:00
Marshall Bowers
1b614ef63b Add an Assistant example that can interact with the filesystem (#11027)
This PR adds a new Assistant example that is able to interact with the
filesystem using a tool.

Release Notes:

- N/A
2024-04-25 22:21:18 -04:00
Hendrik Sollich
604857ed2e vim: Increment search right (#10866)
Hi there, nice editor!
Here's my attempt at fixing #10865.

Thanks

Release Notes:

-vim: Fix ctrl+a when cursor is on a decimal point
([#10865](https://github.com/zed-industries/zed/issues/10865)).

---------

Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
2024-04-25 19:47:52 -06:00
DissolveDZ
d9eb3c4b35 vim: Fix hollow cursor being offset when selecting text (#11000)
Fixed the cursor selection being offset, the hollow cursor was being
displayed fine when not having text selected that's why it might not
have been noticed at first.

Release Notes:
- N/A

Improved:
0d6fb08b67
2024-04-25 19:47:12 -06:00
Marshall Bowers
f8beda0704 Rename chat_with_functions to use snake_case (#11020)
This PR renames the `chat-with-functions.rs` example to use snake_case
for the filename, as is convention.

Release Notes:

- N/A
2024-04-25 21:43:02 -04:00
Max Brunsfeld
40fe5275cf Rework project diagnostics to prevent showing inconsistent state (#10922)
For a long time, we've had problems where diagnostics can end up showing
up inconsistently in different views. This PR is my attempt to prevent
that, and to simplify the system in the process. There are some UX
changes.

Diagnostic behaviors that have *not* changed:

* In-buffer diagnostics update immediately when LSPs send diagnostics
updates.
* The diagnostic counts in the status bar indicator also update
immediately.

Diagnostic behaviors that this PR changes:

* [x] The tab title for the project diagnostics view now simply shows
the same counts as the status bar indicator - the project's current
totals. Previously, this tab title showed something slightly different -
the numbers of diagnostics *currently shown* in the diagnostics view's
excerpts. But it was pretty confusing that you could sometimes see two
different diagnostic counts.
* [x] The project diagnostics view **never** updates its excerpts while
the user might be in the middle of typing it that view, unless the user
expressed an intent for the excerpts to update (by e.g. saving the
buffer). This was the behavior we originally implemented, but has
changed a few times since then, in attempts to fix other issues. I've
restored that invariant.

    Times when the excerpts will update:
     * diagnostics are updated while the diagnostics view is not focused
     * the user changes focus away from the diagnostics view
* the language server sends a `work done progress end` message for its
disk-based diagnostics token (i.e. cargo check finishes)
* the user saves a buffer associated with a language server, and then a
debounce timer expires

* [x] The project diagnostics view indicates when its diagnostics are
stale. States:
* when diagnostics have been updated while the diagnostics view was
focused:
        * the indicator shows a 'refresh' icon
        * clicking the indicator updates the excerpts
* when diagnostics have been updated, but a file has been saved, so that
the diagnostics will soon update, the indicator is disabled

With these UX changes, the only 'complex' part of the our diagnostics
presentation is the Project Diagnostics view's excerpt management,
because it needs to implement the deferred updates in order to avoid
disrupting the user while they may be typing. I want to take some steps
to reduce the potential for bugs in this view.

* [x] Reduce the amount of state that the view uses, and simplify its
implementation
* [x] Add a randomized test that checks the invariant that a mutated
diagnostics view matches a freshly computed diagnostics view


##  Release Notes

- Reworked the project diagnostics view:
- Fixed an issue where the project diagnostics view could update its
excerpts while you were typing in it.
- Fixed bugs where the project diagnostics view could show the wrong
excerpts.
- Changed the diagnostics view to always update its excerpts eagerly
when not focused.
- Added an indicator to the project diagnostics view's toolbar, showing
when diagnostics have been changed.

---------

Co-authored-by: Richard Feldman <oss@rtfeldman.com>
2024-04-25 18:12:15 -07:00
Kyle Kelley
cf2272a949 Always submit function definitions in Simple mode too (#11016)
Switches Assistant2 to always provide functions. It's up to the model to
choose to use them. At a later point, the `Submit::Codebase` should
change the `tool_choice` to `query_codebase` rather than `auto`. For
now, I think this will improve the experience for folks testing.

Release Notes:

- N/A
2024-04-25 16:34:07 -07:00
Nate Butler
366d7e7728 Break typography styles out of StyledExt (#11013)
- Centralizes typography-related UI styles and methods in
`styles/typography.rs`
- Breaks the typography-related styles out of `StyledExt`. This means we
add a `StyledTypography` trait – this should more or less be an
invisible change as we publish it in the prelude.
- adds the ability to easily grab the UI or Buffer font sizes
(`ui_font_size`, `buffer_font_size`) with `TextSize::UI`,
`TextSize::Editor`

Release Notes:

- N/A
2024-04-25 17:42:53 -04:00
Michael Angerman
4c780568bc storybook: Fix Backspace in Auto Height Editor and Picker stories (#11011)
Currently in the *Auto Height Editor* story the backspace key is not
working when you type into the Editor.

The same thing is true for the *Picker* story as one is not able to
backspace...

By adding an entry in the keymap file

```rust
assets/keymaps/storybook.json
```

both of these issues are solved...

Release Notes:

- N/A
2024-04-25 17:37:47 -04:00
Marshall Bowers
7af96a15fe Fix typo in comment 2024-04-25 17:30:47 -04:00
Marshall Bowers
3eac581a62 Allow controlling Tailwind via the language_servers setting (#11012)
This PR adds the ability for the Tailwind language server
(`tailwindcss-language-server`) to be controlled by the
`language_servers` setting.

Now in your settings you can indicate that the Tailwind language server
should be used for a given language, even if that language does not have
the Tailwind language server registered for it already:

```json
{
  "languages": {
    "My Language": {
      "language_servers": ["tailwindcss-language-server", "..."]
    }
  }
}
```

Release Notes:

- N/A
2024-04-25 17:29:47 -04:00
Conrad Irwin
c833a7e662 Don't use fancy cursors for non-vim people (#11010)
Release Notes:

- N/A
2024-04-25 14:21:01 -06:00
Kyle Kelley
f176e8f0e4 Accept Views on LanguageModelTools (#10956)
Creates a `ToolView` trait to allow interactivity. This brings expanding
and collapsing to the excerpts from project index searches.

Release Notes:

- N/A

---------

Co-authored-by: Nathan <nathan@zed.dev>
Co-authored-by: Max Brunsfeld <maxbrunsfeld@gmail.com>
2024-04-25 13:03:43 -07:00
Marshall Bowers
7005f0b424 Remove outdated instructions for adding languages (#11005)
This PR removes the outdated comment regarding adding languages to Zed.

New languages should be added as extensions.

Release Notes:

- N/A
2024-04-25 14:30:31 -04:00
James Thurley
d3f6ca7a1e Add @operator, @lifetime and @punctuation.delimiters captures for Rust (#10885)
Adds additional captures for theming rust code.

I'm uncertain about whether `#` belongs in the `@operator` capture, but
I didn't see a more appropriate capture name in my brief hunt in other
files. It is the prefix of an `attribute_item`.. suggestions welcome.

Release Notes:

- Added `@operator`, `@lifetime` and `@punctuation.delimiter` captures
to Rust highlights file.
2024-04-25 14:20:20 -04:00
Marshall Bowers
544bd490ac Extract Elixir extension (#10948)
This PR extracts Elixir support into an extension and removes the
built-in Elixir support from Zed.

As part of this, [Lexical](https://github.com/lexical-lsp/lexical) has
been added as an available language server for Elixir.

Since the Elixir extension provides three different language servers,
you'll need to use the `language_servers` setting to select the one you
want to use:

#### Elixir LS

```json
{
  "languages": {
    "Elixir": {
      "language_servers": [ "elixir-ls", "!next-ls", "!lexical", "..."]
    }
  }
}
```

#### Next LS

```json
{
  "languages": {
    "Elixir": {
      "language_servers": [ "next-ls", "!elixir-ls", "!lexical", "..."]
    }
  }
}
```

#### Lexical

```json
{
  "languages": {
    "Elixir": {
      "language_servers": [ "lexical", "!elixir-ls", "!next-ls", "..."]
    }
  }
}
```

These can either go in your user settings or your project settings.

Release Notes:

- Removed built-in support for Elixir, in favor of making it available
as an extension.
2024-04-25 13:59:14 -04:00
Jason Lee
7065da2b98 Fix project-panel double click file support on Windows (#10917)
Release Notes:

- Fixed project panel double click to force open file on Windows.

Ref issue: #10898 

@bennetbo 

I saw you was also used `event.down.click_count` in Markdown Preview.


7dccbd8e3b (diff-c8d1735cb347ea08d03198df112343ec50a74de8d50414a6f3be6c6d674c6d19R161)

And this also used in other place:

<img width="870" alt="image"
src="https://github.com/zed-industries/zed/assets/5518/b844e700-b95f-4cd2-987f-9e4305ebdd7c">

## Test demo after updated

Looks like it is no side effect

![2024-04-24 10 17
45](https://github.com/zed-industries/zed/assets/5518/0df4cf06-7448-4014-9df2-f2608a5f5314)
2024-04-25 10:01:24 -07:00
Thorsten Ball
0d6fb08b67 vim: set cursor to hollow-block if editor loses focus (#10995)
This has been bugging me for a while now. Finally figured out how to do
it.

Release Notes:

- Fixed cursor in Vim mode not changing into a hollow block when editor
loses focus.


Demo: 


https://github.com/zed-industries/zed/assets/1185253/c7585282-156d-4ab2-b516-eb1940d6d0d3
2024-04-25 17:56:53 +02:00
Marshall Bowers
3ce4ff94ae Update Cargo.lock 2024-04-25 11:17:25 -04:00
Congyu
21022f1644 Fix cmd+click find all references fallback not working in Vim mode (#10684)
Exclude go-to-definition links returned by LSP that points to the
current cursor position. This fixes #10392 . Related PR #9243 .

The previous implementation first performs go-to-definition, and if the
selected text covers the clicked position, it figures out that it is
already clicking on a definition, and should instead look for
references.

However, the selected range does not necessarily cover the clicked
position after clicking on a definition, as in VIM mode.

After the PR, if cmd+click on definitions, the definition links would be
an empty list, so no go-to-definition is performed, and
find-all-references is performed instead.

Release Notes:

- Fixed #10392 , now `cmd+click`ing to find all references works in vim
mode.
2024-04-25 09:07:14 -06:00
Antonio Scandurra
11bcfea6d2 Fix single-line editors not working anymore (#10994)
This was introduced with #10979 and was caused by a missing call to
`cx.set_view_id` in `EditorElement`, which is necessary when rendering
`EditorElement` manually instead of via a view.

Release Notes:

- N/A
2024-04-25 17:04:20 +02:00
Jakob Hellermann
1cd34fdd9c Recognize PKGBUILD as bash script (#10946)
[PKGBUILD] is a file used in the build system of arch linux, and it is
basically just a bash script with special functions.


Release Notes:

- Changed `PKGBUILD` files to be recognized as bash.
2024-04-25 09:44:52 -04:00
Antonio Scandurra
530224527d Allow pressing escape to cancel the current assistant generation (#10987)
If the assistant has already emitted some text, we will leave the
assistant message but maintain the cursor on the previous user message,
so that the user can easily discard the message by submitting again.

If no output was emitted yet, we simply delete the empty assistant
message.

Release Notes:

- N/A
2024-04-25 15:12:58 +02:00
Marshall Bowers
0de2636324 Revert "Changed cmd+w with no open tabs to close window (#10740)" (#10986)
This PR reverts #10740, as it makes it too easy to close Zed
accidentally.

Quitting Zed when you don't mean to is disruptive and can break your
flow. This is even more the case when you're collaborating.

Therefore, we shouldn't make it easy to quit Zed when you don't mean to.

If we want to bring back this behavior it needs to have a corresponding
setting that should, in my opinion, be **off** by default. Additionally,
someone made the good point that this behavior should not be present on
Linux or Windows.

This reverts commit 5102e37a5b.

Release Notes:

- Changed `cmd-w` with no open tabs to no longer close the window
(preview-only).
2024-04-25 09:10:02 -04:00
Thorsten Ball
7ec963664e git blame: Do not try to blame buffer if it has no file (#10985)
Release Notes:

- Fixed error messages being logged due to inline git blame not working
on an empty buffer that hasn't been saved yet.
2024-04-25 15:02:19 +02:00
Thorsten Ball
019821d62c eslint: register as language server for Vue.js (#10983)
This fixes #9934 and does two things:

1. It registers ESLint as a secondary language server for Vue.js files
(`.vue`)
2. It registers ESLint as a _secondary_ (instead of primary) language
server for TypeScript, JavaScript and TSX.

The second point because I don't see any reason why ESLint should be
registered as a primary language server for these languages. I read
through the code in `project.rs` that uses the primary language server
and I don't think there will be any differences to how it previously
worked.

I also manually tested ESLint support in a Vue.js project, a Next.js
project and a plain old JS project — still works in all three.

Release Notes:

- Added ESLint support for Vue.js files by starting it as a language
server on `.vue` files.
([#9934](https://github.com/zed-industries/zed/issues/9934)).
2024-04-25 14:49:07 +02:00
Thorsten Ball
bb213b6e37 Fix keybinding errors on Linux (#10982)
These showed up as error messages. One of them has been removed and the
other two have changed names.



Release Notes:

- N/A
2024-04-25 13:44:24 +02:00
Antonio Scandurra
6a7761e620 Merge ElementContext into WindowContext (#10979)
The new `ElementContext` was originally introduced to ensure the element
APIs could only be used inside of elements. Unfortunately, there were
many places where some of those APIs needed to be used, so
`WindowContext::with_element_context` was introduced, which defeated the
original safety purposes of having a specific context for elements.

This pull request merges `ElementContext` into `WindowContext` and adds
(debug) runtime checks to APIs that can only be used during certain
phases of element drawing.

Release Notes:

- N/A

---------

Co-authored-by: Nathan Sobo <nathan@zed.dev>
2024-04-25 12:54:39 +02:00
Thorsten Ball
031580f4dc git: Fix inline blame moving on horizontal scroll (#10974)
This fixes the behaviour reported by @mikayla-maki.

## Before


https://github.com/zed-industries/zed/assets/1185253/35aa4e6d-295b-4050-b5cc-cab0f91b27e1


## After


https://github.com/zed-industries/zed/assets/1185253/a17cbc9c-fc2c-43d6-918b-1205b327507b

## Release notes

Release Notes:

- Fixed inline git blame information moving when horizontally scrolling.
2024-04-25 11:29:29 +02:00
Hans
1a27016123 Improve logic for obtaining surrounds range in Vim mode (#10938)
now correctly retrieves range in cases where escape characters are
present. Fixed #10827


Release Notes:

- vim: Fix logic for finding surrounding quotes to ignore escaped
characters (#10827)
2024-04-24 21:19:15 -06:00
Conrad Irwin
d1425603f6 Fix misalignment of vim mode indicator (#10962)
Credit-to: @elkowar

New is the top
<img width="220" alt="Screenshot 2024-04-24 at 19 00 48"
src="https://github.com/zed-industries/zed/assets/94272/9d917bf1-e175-494d-9653-757d15584921">

Release Notes:

- N/A
2024-04-24 19:17:10 -06:00
Marshall Bowers
583a662ddc Fix issues with drafting release notes in CI (#10955)
This PR addresses some issues I ran into with the way we draft release
notes in CI when doing builds.

The first issue I encountered was that `script/draft-release-notes` was
failing, seemingly due to CI doing a shallow Git checkout and not having
all of the tags available in order to compare then. This was addressed
by setting the `fetch-depth` during the Git checkout.

The second issue is that (in my opinion) we shouldn't fail the build if
drafting release notes fails. After well, we're doing it as a
convenience to ourselves, and it isn't a mandatory part of the build.
This was addressed by making any failures in
`script/draft-release-notes` not fail the CI step as a whole.

These changes were already applied to the `v0.133.x` branch.

Release Notes:

- N/A
2024-04-24 18:56:36 -04:00
Conrad Irwin
64617a0ede Read settings in headless mode (#10950)
Release Notes:

- N/A
2024-04-24 16:06:36 -06:00
Marshall Bowers
b673494f4d Restore the previous styles for single-line editors (#10951)
This PR fixes a bug introduced in #10870 that caused editors used as
single-line inputs to have the wrong text style.

If this change was intentional for something relating to the new
Assistant panel, we'll need to figure out a way to change it without
breaking these other usages.

### Before

<img width="589" alt="Screenshot 2024-04-24 at 5 35 36 PM"
src="https://github.com/zed-industries/zed/assets/1486634/31624cfd-75d1-4771-9402-c14ef9e9483e">

<img width="326" alt="Screenshot 2024-04-24 at 5 35 46 PM"
src="https://github.com/zed-industries/zed/assets/1486634/1b76a3ef-7205-49ee-b391-7609f90461bd">

### After

<img width="588" alt="Screenshot 2024-04-24 at 5 36 14 PM"
src="https://github.com/zed-industries/zed/assets/1486634/9d550ee2-80c0-4afb-9b45-a2956471c546">

<img width="260" alt="Screenshot 2024-04-24 at 5 36 31 PM"
src="https://github.com/zed-industries/zed/assets/1486634/63240f27-1679-45d5-b39c-016860ff9683">

Release Notes:

- Fixed a bug where some inputs were using the wrong font style
(preview-only).
2024-04-24 17:47:25 -04:00
Dzmitry Malyshau
53f67a8241 Update blade with transparency and exclusive fullscreen fixes (#10880)
Release Notes:

- N/A

Picks up https://github.com/kvark/blade/pull/113 and a bunch of other
fixes.
Should prevent the exclusive full-screen on Vulkan - related to #9728
cc @kazatsuyu 

Note: this PR doesn't enable transparency, this is left to follow-up
2024-04-24 13:02:11 -07:00
张小白
06d2d9da5f windows: Let the OS decide which font to use as the UI font (#10877)
On my computer, I get `Yahei UI`, which makes sense since I'm using a
Chinese operating system, and `Yahei UI` includes Chinese codepoints. On
an English operating system, `Segoe UI` should be used instead.

Edit: I also choose to use the UI font selected by the system as the
fallback font, rather than hard-coding the `Arial` font.

Release Notes:

- N/A
2024-04-24 13:00:25 -07:00
Maksim Bondarenkov
9e88155a48 Use winresource instead of embed-manifest (#10810)
use winresource for crates/zed and crates/storybook. tested on
`x86_64-pc-windows-gnu`. on `x86_64-pc-windows-msvc` I receive a error
message, that looks like a problem with my machine
 
Release Notes:

- N/A
2024-04-24 12:59:18 -07:00
Conrad Irwin
048fc7ad09 Allow cli to accept --dev-server-token (#10944)
Release Notes:

- N/A
2024-04-24 13:15:19 -06:00
Marshall Bowers
bd77232f65 dart: Bump to v0.0.2 (#10940)
This PR bumps the Dart extension to v0.0.2.

Changes:

- https://github.com/zed-industries/zed/pull/8347
- https://github.com/zed-industries/zed/pull/10552

Release Notes:

- N/A
2024-04-24 13:03:56 -04:00
Joseph T. Lyons
facd04c902 v0.134.x dev 2024-04-24 12:46:30 -04:00
Marshall Bowers
d8437136c7 Fix primary language server selection for formatting (#10939)
This PR fixes the way we select the primary language server for use with
formatting.

Previously we were just taking the first one in the list, but this could
be the wrong one in cases where a language server was provided by an
extension in conjunction with a built-in language server (e.g.,
Tailwind).

We now use the `primary_language_server_for_buffer` method to more
accurately identify the primary one.

Fixes https://github.com/zed-industries/zed/issues/10902.

Release Notes:

- Fixed an issue where the wrong language server could be used for
formatting.
2024-04-24 12:42:33 -04:00
Marshall Bowers
d0a5dbd8cb terraform: Sync Cargo.toml version with extension.toml version (#10937)
This PR syncs the `Cargo.toml` version with the `extension.toml`
version.

We should try to keep these in sync.

Release Notes:

- N/A
2024-04-24 11:42:18 -04:00
Marshall Bowers
76ff467965 Log which language servers will be started (#10936)
This PR adds a new log message indicating which language servers will be
started for a given language.

The aim is to make debugging the usage of the new `language_servers`
setting (#10911) easier.

Release Notes:

- N/A
2024-04-24 11:22:42 -04:00
Antonio Scandurra
e1791b7dd0 Autoscroll containing element when editor has a pending selection (#10931)
Release Notes:

- N/A
2024-04-24 14:33:57 +02:00
Antonio Scandurra
25e239d986 Fix autoscroll in the new assistant (#10928)
This removes the manual calls to `scroll_to_reveal_item` in the new
assistant, as they are superseded by the new autoscrolling behavior of
the `List` when the editor requests one.

Release Notes:

- N/A
2024-04-24 14:12:44 +02:00
Thorsten Ball
f7ea1370a4 Update docstring for SumTree (#10927)
Need the updated docstring for the blog post.
Release Notes:

- N/A
2024-04-24 14:01:10 +02:00
Kirill Bulatov
6108140a02 Properly extract package name out of cargo pkgid (#10929)
Fixes https://github.com/zed-industries/zed/issues/10925

Uses correct package name to generate Rust `cargo` tasks.
Also deduplicates lines in task modal item tooltips.

Release Notes:

- Fixed Rust tasks using incorrect package name
([10925](https://github.com/zed-industries/zed/issues/10925))
2024-04-24 14:12:59 +03:00
Hans
135a5f2114 Enable unfocused windows to update their status based on whether they are clickable or not (#10229)
- Fixed #9784 By removing the interception of the MouseMove event, zed
can update the corresponding Hover even when it is inactive
2024-04-24 10:44:00 +02:00
hardlydearly
dfd4d2a437 chore: remove repetitive word (#10923) 2024-04-24 11:34:32 +03:00
Antonio Scandurra
fbc6e930a7 Fix regressions in List (#10924)
Release Notes:

- N/A
2024-04-24 10:18:52 +02:00
Kyle Kelley
af5a9fabc6 Include root schema as parameters for tool calling (#10914)
Allows `LanguageModelTool`s to include nested structures, by exposing
the definitions section of their JSON Schema.

Release Notes:

- N/A
2024-04-23 20:49:29 -07:00
Marshall Bowers
25981550d5 Extract Deno extension (#10912)
This PR extracts Deno support into an extension and removes the built-in
Deno support from Zed.

When using the Deno extension, you'll want to add the following to your
settings to disable the built-in TypeScript and ESLint language servers
so that they don't conflict with Deno's functionality:

```json
{
  "languages": {
    "TypeScript": {
      "language_servers": ["deno", "!typescript-language-server", "!eslint", "..."]
    },
    "TSX": {
      "language_servers": ["deno", "!typescript-language-server", "!eslint", "..."]
    }
  }
}

```

Release Notes:

- Removed built-in support for Deno, in favor of making it available as
an extension.
2024-04-23 20:44:11 -04:00
Marshall Bowers
cf67fc9055 Add language_servers setting for customizing which language servers run (#10911)
This PR adds a new `language_servers` setting underneath the language
settings.


This setting controls which of the available language servers for a
given language will run.

The `language_servers` setting is an array of strings. Each item in the
array must be either:

- A language server ID (e.g., `"rust-analyzer"`,
`"typescript-language-server"`, `"eslint"`, etc.) denoting a language
server that should be enabled.
- A language server ID prefixed with a `!` (e.g., `"!rust-analyzer"`,
`"!typescript-language-server"`, `"!eslint"`, etc.) denoting a language
server that should be disabled.
- A `"..."` placeholder, which will be replaced by the remaining
available language servers that haven't already been mentioned in the
array.

For example, to enable the Biome language server in place of the default
TypeScript language server, you would add the following to your
settings:

```json
{
  "languages": {
    "TypeScript": {
      "language_servers": ["biome", "!typescript-language-server", "..."]
    }
  }
}
```

More details can be found in #10906.

Release Notes:

- Added `language_servers` setting to language settings for customizing
which language server(s) run for a given language.
2024-04-23 19:38:57 -04:00
Kyle Kelley
68a1ad89bb New revision of the Assistant Panel (#10870)
This is a crate only addition of a new version of the AssistantPanel.
We'll be putting this behind a feature flag while we iron out the new
experience.

Release Notes:

- N/A

---------

Co-authored-by: Nathan Sobo <nathan@zed.dev>
Co-authored-by: Antonio Scandurra <me@as-cii.com>
Co-authored-by: Conrad Irwin <conrad@zed.dev>
Co-authored-by: Marshall Bowers <elliott.codes@gmail.com>
Co-authored-by: Antonio Scandurra <antonio@zed.dev>
Co-authored-by: Nate Butler <nate@zed.dev>
Co-authored-by: Nate Butler <iamnbutler@gmail.com>
Co-authored-by: Max Brunsfeld <maxbrunsfeld@gmail.com>
Co-authored-by: Max <max@zed.dev>
2024-04-23 16:23:26 -07:00
Conrad Irwin
e0c83a1d32 remote projects per user (#10594)
Release Notes:

- Made remote projects per-user instead of per-channel. If you'd like to
be part of the remote development alpha, please email hi@zed.dev.

---------

Co-authored-by: Bennet Bo Fenner <53836821+bennetbo@users.noreply.github.com>
Co-authored-by: Bennet <bennetbo@gmx.de>
Co-authored-by: Nate Butler <1714999+iamnbutler@users.noreply.github.com>
Co-authored-by: Nate Butler <iamnbutler@gmail.com>
2024-04-23 15:33:09 -06:00
Michael Angerman
8ae4c3277f storybook: Fix crash in Kitchen Sink and Auto Height Editor stories (#10904)
The *Kitchen Sink* as well as the *Auto Height Editor* story is crashing
for the same reason that the Picker story was crashing...

### Related Topics

- Picker Story PR : #10793 
- Picker Story Issue : #10739 
- Introduced By : #10620 

Release Notes:

- N/A
2024-04-23 16:45:12 -04:00
Joseph T. Lyons
f6eaa8b00f Clean up whitespace (#10755)
I saved the `file_types.json` file and got a diff because it had some
trailing whitespace. I ran
[`lineman`](https://github.com/JosephTLyons/lineman) on the codebase.
I've done this before, but this time, I've added in the following
settings to our `.zed` local settings, to make sure every future save
respects our desire to have consistent whitespace formatting.

```json
"remove_trailing_whitespace_on_save": true,
"ensure_final_newline_on_save": true
```

Release Notes:

- N/A
2024-04-23 13:31:21 -04:00
Thorsten Ball
85b26e9788 Store goldenfiles with trailing newline (#10900)
Release Notes:


- N/A
2024-04-23 19:13:04 +02:00
Piotr Osiewicz
2ee257a562 task_ui: Move status indicator into tab bar of terminal panel (#10846)
I'm not a huge fan of this change (& I expect the placement to change).
The plan is to have the button in a toolbar of terminal panel, but I'm
not sure if occupying a whole line of vertical space for a single button
is worth it; I suppose we might want to put more of tasks ui inside of
that toolbar.
Release Notes:

- Removed task status indicator and added "Spawn task" action to
terminal panel context menu.
2024-04-23 16:27:18 +02:00
Antonio Scandurra
bcbf2f2fd3 Introduce autoscroll support for elements (#10889)
This pull request introduces the new
`ElementContext::request_autoscroll(bounds)` and
`ElementContext::take_autoscroll()` methods in GPUI. These new APIs
enable container elements such as `List` to change their scroll position
if one of their children requested an autoscroll. We plan to use this in
the revamped assistant.

As a drive-by, we also:

- Renamed `Element::before_layout` to `Element::request_layout`
- Renamed `Element::after_layout` to `Element::prepaint`
- Introduced a new `List::splice_focusable` method to splice focusable
elements into the list, which enables rendering offscreen elements that
are focused.

Release Notes:

- N/A

---------

Co-authored-by: Nathan <nathan@zed.dev>
2024-04-23 15:14:22 +02:00
Kyle Kelley
efcd31c254 Update documentation and handling to use a crates/collab/seed.json (#10874)
Updates `collab` to accept a `seed.json` file that allows you to
override the defaults. Updated the `README` in collab to just have
directions inside instead of redirecting the developer to the website.

Release Notes:

- N/A

Co-authored-by: Max <max@zed.dev>
2024-04-23 05:41:31 -07:00
apricotbucket28
ae3c641bbe wayland: File drag and drop (#10817)
Implements file drag and drop on Wayland


https://github.com/zed-industries/zed/assets/71973804/febcfbfe-3a23-4593-8dd3-e85254e58eb5


Release Notes:

- N/A
2024-04-22 16:20:24 -07:00
Marshall Bowers
029eb67043 Add SettingsSources::<T>::json_merge_with function (#10869)
This PR adds a `json_merge_with` function to `SettingsSources::<T>` to
allow JSON merging settings from custom sources.

This should help avoid repeating the actual merging logic when all that
needs to be customized is which sources are being respected.

Release Notes:

- N/A
2024-04-22 18:38:34 -04:00
Marshall Bowers
63c529552c Automatically install the HTML extension (#10867)
This PR makes it so the HTML extension will be installed in Zed by
default.

We feel we should keep HTML available out-of-the-box, but we want to do
so while still keeping it as an extension (as opposed to built-in to Zed
natively). There may be a world where we bundle the extension in with
the Zed binary itself, but installing it on startup gets us 99% of the
way there.

The approach for making HTML available by default is quite general, and
could be applied to any extension that we choose (likely other languages
that we want to come out-of-the-box, but that could then be moved to
extensions).

If you do not want the HTML extension in Zed, you can disable the
auto-installation in your `settings.json` and then uninstall the
extension:

```json
{
  "auto_install_extensions": {
    "html": false
  }
}
```

Release Notes:

- Added auto-installation for the HTML extension on startup.
- This can be disabled by adding `{ "auto_install_extensions": { "html":
false } }` to your settings.
2024-04-22 18:02:22 -04:00
Ben Hamment
c96a96b3ce Add traits in Rust highlights (#10731)
Question: I use type.super here because I made a similar change to the
ruby syntax to apply the same style to superclasses.
With this in mind, should this change be renamed to type.trait or should
it be renamed to something like type.italic so the ruby syntax or any
other language can all use type.italic? or maybe something else
altogether.

<img width="597" alt="image"
src="https://github.com/zed-industries/zed/assets/7274458/9d02dba0-75a4-4439-9f31-fd8aa0873075">

Release Notes:

- Exposed Rust traits as `type.interface` for individual syntax theming.
2024-04-22 16:43:48 -04:00
ElKowar
7f81bfb6b7 Make keymaps reusable across platforms (#10811)
This PR includes two relevant changes:
- Platform binds (super, windows, cmd) will now parse on all platforms,
regardless of which one is being used. While very counter-intuitive
(this means that `cmd-d` will actually be triggered by `win-d` on
windows) this makes it possible to reuse keymap files across platforms
easily
- There is now a KeyContext `os == linux`, `os == macos` or `os ==
windows` available in keymaps. This allows users to specify certain
blocks of keybinds only for one OS, allowing you to minimize the amount
of keymappings that you have to re-configure for each platform.

Release Notes:

- Added `os` KeyContext, set to either `linux`, `macos` or `windows`
- Fixed keymap parsing errors when `cmd` was used on linux, `super` was
used on mac, etc.
2024-04-22 13:24:25 -07:00
Marshall Bowers
33baa377c7 docs: Add note about manually using the bump_patch_version action (#10862)
This PR updates the releases docs to make a note about
`bump_patch_version` action through the GitHub UI.

Not all of us have `gh` (or `brew`) installed.

Release Notes:

- N/A
2024-04-22 15:58:25 -04:00
Marshall Bowers
07f490f9e9 Ensure target directory exists before drafting release notes 2024-04-22 15:56:54 -04:00
ElKowar
b29643168c XDG_BASE_DIR support (linux, windows) (#10808)
This PR adds XDG_BASE_DIR support on linux, and cleans up the path
declarations slightly. Additionally, we move the embeddings and
conversations directly to the SUPPORT_DIR on those platforms.
I _think_ that should also be done on MacOS in the future, but that has
been left out here for now to not break existing users setups.

Additionally, we move the SUPPORT_DIR into LocalAppData on windows for
consistency.

Release Notes:

- Fixed missing support of `XDG_BASE_DIR` on linux
- Fixed improper placement of data in XDG_CONFIG_HOME on linux and
windows (https://github.com/zed-industries/zed/issues/9308,
https://github.com/zed-industries/zed/issues/7155)

---------

Co-authored-by: phisch <philipp.schaffrath@gmail.com>
2024-04-22 12:42:18 -07:00
Max Linke
e9a965fe81 Add missing linux dependencies (#10814)
At least one of the dependencies requires cmake to configure the build
process.

On ubuntu libgit2-dev was missing. Debian and derivates do not install
development headers by default.



Release Notes:

- Improved Linux development setup scripts.

Co-authored-by: Max Linke <max.linke88@gmail.com>
2024-04-22 12:05:12 -07:00
Marshall Bowers
b964fe2ccf Fix reading workspace-level LSP settings in extensions (#10859)
This PR fixes an issue where workspace-level LSP settings could be not
read using `LspSettings::for_worktree` in extensions.

We we erroneously always reading the global settings instead of
respecting the passed-in location.

Release Notes:

- Fixed a bug where workspace LSP settings could not be read by
extensions.
2024-04-22 14:40:23 -04:00
Piotr Osiewicz
1be452744a cli: Use leading dashes in channel designators (#10858)
/cc @maxbrunsfeld 

Release Notes:

- N/A
2024-04-22 20:26:49 +02:00
Nate Butler
d298df823f Minor script fix (#10857)
Fixes a minor error in the analyze highlight script.

Release Notes:

- N/A
2024-04-22 13:05:41 -04:00
Piotr Osiewicz
a111b959d2 cli: Treat first argument as name of release channel to use for the cli (#10856)
With this commit, it is now possible to invoke cli with a release
channel of bundle as an argument. E.g: `zed stable some_arguments` will
find CLI binary of Stable channel installed on your machine and invoke
it with `some_arguments` (so the first argument is essentially omitted).

Fixes #10851

Release Notes:

- CLI now accepts an optional name of release channel as it's first
argument. For example, `zed stable` will always use your Stable
installation's CLI. Trailing args are passed along.
2024-04-22 18:01:06 +02:00
Nate Butler
189cece03e Add analyze highlights script (#10855)
Adds a script to print all unique highlight keys for building syntax
themes.

Usage:

- `python script/analyze_highlights.py` OR
- `python script/analyze_highlights.py -v`
- Using the `-v` or `--verbose` arg will print each language that uses
each key.

Example output:

```
@attribute (6)
@boolean (5)
@charset (1)
@comment (19)
@comment.doc (3)
@comment.unused (2)
@constant (27)
@constant.builtin (15)
@constant.character (1)
@constructor (4)
@embedded (10)
@emphasis (1)
@emphasis.strong (1)
@escape (4)
@function (44)
@function.builtin (2)
@function.definition (2)
@function.method (22)
@function.method.builtin (3)
@function.special (4)
@function.special.definition (1)
@import (1)
@keyframes (1)
@keyword (32)
@label (2)
@link_text (1)
@link_uri (1)
@media (1)
@module (1)
@namespace (1)
@number (16)
@operator (24)
@property (11)
@property.json_key (1)
@punctuation (1)
@punctuation.bracket (28)
@punctuation.delimiter (12)
@punctuation.list_marker (1)
@punctuation.special (17)
@string (23)
@string.doc (1)
@string.escape (5)
@string.regex (7)
@string.special (4)
@string.special.symbol (2)
@supports (1)
@tag (14)
@text.literal (2)
@title (1)
@type (28)
@type.builtin (4)
@type.super (3)
@variable (5)
@variable.member (3)
@variable.parameter (4)
@variable.special (12)

Extension-only:

@tag.delimiter (1)
```

Verbose example output:

```
Shared:

@attribute (6) - [css, heex, javascript, tsx]
@boolean (5) - [javascript, proto, tsx, typescript, yaml]
@charset (1) - [css]
@comment (19) - [bash, c, cpp, css, elixir, erb, go, gomod, gowork, heex, javascript, json, proto, python, ruby, rust, tsx, typescript, yaml]
@comment.doc (3) - [elixir]
@comment.unused (2) - [elixir]
@constant (27) - [bash, c, cpp, elixir, heex, javascript, json, proto, python, ruby, rust, tsx, typescript]
@constant.builtin (15) - [elixir, go, javascript, python, ruby, tsx, typescript, yaml]
@constant.character (1) - [regex]
@constructor (4) - [tsx, typescript]
@embedded (10) - [bash, elixir, javascript, python, ruby, tsx, typescript]
@emphasis (1) - [markdown]
@emphasis.strong (1) - [markdown]
@escape (4) - [go, python, regex, ruby]
@function (44) - [bash, c, cpp, css, elixir, go, heex, javascript, python, rust, tsx, typescript]
@function.builtin (2) - [python]
@function.definition (2) - [rust]
@function.method (22) - [go, javascript, python, ruby, rust, tsx, typescript]
@function.method.builtin (3) - [ruby]
@function.special (4) - [c, cpp, rust]
@function.special.definition (1) - [rust]
@import (1) - [css]
@keyframes (1) - [css]
@keyword (32) - [bash, c, cpp, css, elixir, erb, go, gomod, gowork, heex, javascript, jsdoc, proto, python, ruby, rust, tsx, typescript]
@label (2) - [c, cpp]
@link_text (1) - [markdown]
@link_uri (1) - [markdown]
@media (1) - [css]
@module (1) - [heex]
@namespace (1) - [css]
@number (16) - [bash, c, cpp, css, elixir, go, javascript, json, proto, python, regex, ruby, rust, tsx, typescript, yaml]
@operator (24) - [bash, c, cpp, css, elixir, go, gomod, gowork, heex, javascript, proto, python, regex, ruby, tsx, typescript]
@property (11) - [bash, c, cpp, css, javascript, python, regex, rust, tsx, typescript, yaml]
@property.json_key (1) - [json]
@punctuation (1) - [elixir]
@punctuation.bracket (28) - [c, cpp, elixir, go, heex, javascript, json, proto, regex, ruby, rust, tsx, typescript, yaml]
@punctuation.delimiter (12) - [c, cpp, css, elixir, heex, javascript, proto, regex, ruby, tsx, typescript, yaml]
@punctuation.list_marker (1) - [markdown]
@punctuation.special (17) - [elixir, javascript, python, ruby, tsx, typescript, yaml]
@string (23) - [bash, c, cpp, css, elixir, go, gomod, gowork, heex, javascript, json, proto, python, regex, ruby, rust, tsx, typescript, yaml]
@string.doc (1) - [python]
@string.escape (5) - [elixir, javascript, tsx, typescript, yaml]
@string.regex (7) - [elixir, javascript, ruby, tsx, typescript]
@string.special (4) - [css, elixir]
@string.special.symbol (2) - [elixir, ruby]
@supports (1) - [css]
@tag (14) - [css, heex, javascript, tsx]
@text.literal (2) - [markdown]
@title (1) - [markdown]
@type (28) - [c, cpp, css, elixir, go, javascript, jsdoc, proto, python, ruby, rust, tsx, typescript, yaml]
@type.builtin (4) - [javascript, rust, tsx, typescript]
@type.super (3) - [ruby]
@variable (5) - [c, cpp, javascript, tsx, typescript]
@variable.member (3) - [go, ruby]
@variable.parameter (4) - [ruby]
@variable.special (12) - [cpp, css, javascript, ruby, rust, tsx, typescript]

Extension-only:

@tag.delimiter (1) - [astro]
```

Release Notes:

- N/A

---------

Co-authored-by: Joseph T. Lyons <JosephTLyons@gmail.com>
2024-04-22 11:51:06 -04:00
Marshall Bowers
ee531b6f4d Sort the list of suggested extensions (#10854)
This PR sorts the list of suggested extensions.

Release Notes:

- N/A
2024-04-22 11:41:16 -04:00
Karolis Narkevicius
67e7c33428 Add ReScript suggested extension for res and resi files (#10822)
Release Notes:

- Added ReScript as a suggested extension for .res and .resi files
2024-04-22 11:22:51 -04:00
Piotr Osiewicz
615de381da terminal: hide navigation buttons (#10847)
We were effectively discarding value set by display_nav_history_buttons
once we've updated settings for a pane. This commit adds another bit of
state to display_nav_history_buttons by allowing it to hard-deny setting
updates.

Release Notes:

- Fixed a bug that caused disabled navigation buttons to show up in
terminal panel.
2024-04-22 14:38:16 +02:00
Lachlan Campbell
74241d9f93 Add woff(2) to file type icon list (#10833)
I noticed the sidebar was using the fallback icons for woff/woff2
webfont files, instead of the font icon:

<img width="195" alt="CleanShot 2024-04-22 at 03 01 18@2x"
src="https://github.com/zed-industries/zed/assets/5074763/2e925c33-0be5-4ed9-ae87-ce72f95f8416">

With this PR, I'm hoping all those font files would use the A icon
instead.

Release Notes:

- Updated`.woff` & `.woff2` file types in the sidebar to display the
font icon.
2024-04-22 12:37:05 +03:00
Kirill Bulatov
dd41c10099 Filter out other languages' tasks from the task modal (#10839)
Release Notes:

- Fixed tasks modal showing history from languages, not matching the
currently active buffer's one
2024-04-22 12:36:26 +03:00
Thorsten Ball
a0fa8a489b ruby: Check if solargraph exists in $PATH or is configured (#10835)
This fixes #9811 by checking for the `solargraph` binary in the `$PATH`
as it's setup in the project shell.

It also adds support for configuring the path to `solargraph` manually:

```json
{
  "lsp": {
    "solargraph": {
      "binary": {
        "path": "/Users/thorstenball/bin/solargraph",
        "arguments": ["stdio"]
      }
    }
  }
}
```

## Example

Given the following setup:

- `ruby@3.3.0` used globally, no `solargraph` installed globally
- `ruby@3.2.2` used in a project, `solargraph` installed as binstub in
`$project/bin/solargraph`, `.envrc` to configure `direnv` to add
`$project/bin` to `$PATH

Which looks like this in practice:

```shell
# GLOBAL
~ $ ruby --version
ruby 3.3.0 (2023-12-25 revision 5124f9ac75) [arm64-darwin23]
~ $ which solargraph
solargraph not found

# IN PROJECT
~ $ cd work/projs/rails-proj
direnv: loading ~/work/projs/rails-proj/.envrc
direnv: export ~PATH
~/work/projs/rails-proj $ ruby --version
ruby 3.2.2 (2023-03-30 revision e51014f9c0) [arm64-darwin23]
~/work/projs/rails-proj $ which solargraph
/Users/thorstenball/work/projs/rails-proj/bin/solargraph
```

The expectation is that Zed, when opening `~/work/projs/rails-proj`,
picks up the local `solargraph`.

But with **Zed Stable** that doesn't work, as we can see in the logs:

```
2024-04-22T10:21:37+02:00 [INFO] starting language server. binary path: "solargraph", working directory: "/Users/thorstenball/work/projs/rails-proj", args: ["stdio"]
2024-04-22T10:21:37+02:00 [ERROR] failed to start language server "solargraph": No such file or directory (os error 2)
```

With the change in this PR, it uses `rails/proj/bin/solargraph`:

```
[2024-04-22T10:33:06+02:00 INFO  language] found user-installed language server for Ruby. path: "/Users/thorstenball/work/projs/rails-proj/bin/solargraph", arguments: ["stdio"]
[2024-04-22T10:33:06+02:00 INFO  lsp] starting language server. binary path: "/Users/thorstenball/work/projs/rails-proj/bin/solargraph", working directory: "/Users/thorstenball/work/projs/rails-proj", args: ["stdio"]
```

**NOTE**: depending on whether `mise` (or `rbenv`, `asdf`, `chruby`,
...) or `direnv` come first in the shell-rc file, it picks one or the
other, depending on what puts itself first in `$PATH`.

## Release Notes

Release Notes:

- Added support for finding the Ruby language server `solargraph` in the
user's `$PATH` as it is when `cd`ing into a project's directory.
([#9811](https://github.com/zed-industries/zed/issues/9811))
- Added support for configuring the `path` and `arguments` for
`solargraph` language server manually. Example from settings: `{"lsp":
{"solargraph": {"binary":
{"path":"/Users/thorstenball/bin/solargraph","arguments": ["stdio"]}}}}`
([#9811](https://github.com/zed-industries/zed/issues/9811))
2024-04-22 10:44:05 +02:00
Andrew Lygin
6fb6cd3c5c Merge branch 'main' into tab-bar-settings 2024-04-21 08:15:22 +03:00
Owen Law
e1685deb29 Revert "Use XI2 for Scrolling on X11" (#10818)
Reverts zed-industries/zed#10695

Some users are experiencing broken scrolling due to the changes from
this PR, so it should be reverted while I investigate what causes the
problems.

Release Notes:

- N/A
2024-04-20 20:07:13 -07:00
Michael Angerman
4ab48c689f storybook: Fix crash in Picker story (#10793)
@mikayla-maki approved my suggested change as noted in the issue
below...

Release Notes:

- Fixed: [#10739 ](https://github.com/zed-industries/zed/issues/10739)
2024-04-20 19:41:45 -07:00
d1y
2677ec7568 Suggest .log file (#10796)
Release Notes:

- Suggest .log file
2024-04-21 03:23:31 +03:00
Kyle Kelley
cd6acff635 Remove TODOs from semantic index build-up (#10789)
Quick touch up of some `// todo!()` that got left in `semantic-index`

Release Notes:

- N/A
2024-04-19 18:59:12 -07:00
William Viktorsson
5102e37a5b Changed cmd+w with no open tabs to close window (#10740)
Fixes https://github.com/zed-industries/zed/issues/5322


Release Notes:

- Changed cmd+w with no open tabs to close window
[#5322](https://github.com/zed-industries/zed/issues/5322)



https://github.com/zed-industries/zed/assets/22855292/113b1635-ae30-4718-a3d3-758a4bf53714
2024-04-19 14:42:33 -07:00
张小白
fee2065b64 windows: Fix main thread blocking when resizing or moving window (#10758)
Connection: Fix #10703 


https://github.com/zed-industries/zed/assets/14981363/59abfab7-ebb2-4da7-ad13-0a9e42f9c1d3




Release Notes:

- N/A
2024-04-19 14:40:21 -07:00
张小白
c3bcfb374c windows: Fix wrong bitmap format (#10773)
I accidentally wrote the bitmap format incorrectly during a refactor.

Release Notes:

- N/A
2024-04-19 14:40:06 -07:00
Mikayla Maki
8a02159b82 Add a command to expand the context for a multibuffer (#10300)
This PR adds an action to expand the excerpts lines of context in a
multibuffer.

Release Notes:

- Added an `editor::ExpandExcerpts` action (bound to `shift-enter` by
default), which can expand the excerpt the cursor is currently in by 3
lines. You can customize the number of lines by rebinding this action
like so:

```json5
// In your keybindings array...
  {
    "context": "Editor && mode == full",
    "bindings": {
      "shift-enter": ["editor::ExpandExcerpts", { "lines": 5 }],
    }
  }
```

---------

Co-authored-by: Nathan <nathan@zed.dev>
Co-authored-by: Max <max@zed.dev>
2024-04-19 14:27:56 -07:00
Andrew Lygin
9d9bce08a7 Fix scroll thumb (#10667)
Editor scrollbar has several issues that show up on large files:

- The thumb scrolls beyond the window.
- When dragged, the thumb goes out of sync with the mouse pointer.
- When the scrollbar trunk is clicked, the thumb jumps incorrectly.


https://github.com/zed-industries/zed/assets/2101250/320dba59-a526-4e68-99b3-1186271ba839

The reason is that the scrollbar now has two modes:
1. The "basic mode" for small files, when the thumb height correctly
represents the visible area, i.e. the top of the thumb matches the top
visible row (let's call it top-to-top sync), and the bottom of the thumb
matches the bottom visible row.
2. The "extended mode" for large files, when thumb becomes too small and
we have to impose minimal height to it. In this mode we have a vertical
offset of the first row position inside the scrollbar, we try to
position the thumb center-to-center with the editor.

...and the second mode is not implemented correctly. Also, mouse event
handlers ignore it. It is possible to fix this implementation, but I'm
not sure if it worth doing because it a) leads to some confusing cases
(for instance, in the extended mode the first row marker is not at the
top of the scrollbar), and b) differs from what all other editors do.

Here's a previous mentioning of this problem:
https://github.com/zed-industries/zed/pull/9080#pullrequestreview-1927465293

This PR changes the "extended mode", making it synchronize the thumb
top-to-top with the editor. It solves all the mentioned problems and
makes the scroll thumb work the same whay as in other editors.

But if you want to stick to the idea of the center-to-center sync for
large files, I can do that too.

Release Notes:

- Fixed scroll thumb behaviour.

Optionally, include screenshots / media showcasing your addition that
can be included in the release notes.

- N/A
2024-04-19 23:18:37 +03:00
Max Brunsfeld
247b0317b9 Add bracket queries for HCL, Terraform (#10785)
Release Notes:

- N/A
2024-04-19 10:59:14 -07:00
Thorsten Ball
f082344747 Add pull requests to git blame tooltip (#10784)
Release Notes:

- Added links to GitHub pull requests to the git blame tooltips, if they
are available.

Screenshot:

(Yes, the icon will be resized! cc @iamnbutler)

![screenshot-2024-04-19-18 31
13@2x](https://github.com/zed-industries/zed/assets/1185253/774af0b3-f587-4acc-aa1e-1846c2bec127)
2024-04-19 18:54:20 +02:00
Thorsten Ball
70427daed2 Remove unused field on CommitDetails (#10783)
Release Notes:


- N/A
2024-04-19 18:34:38 +02:00
Kirill Bulatov
13c17267b9 Properly pass nested script arguments for tasks (#10776)
Closes
https://github.com/zed-industries/zed/discussions/10732#discussion-6524347
introduced by https://github.com/zed-industries/zed/pull/10548 while
keeping both Python and Bash run selection capabilities.

Also replaced redundant `SpawnTask` struct with `SpawnInTerminal` that
has identical fields.

Release Notes:

- Fixed incorrect task escaping of nested script arguments

---------

Co-authored-by: Piotr Osiewicz <piotr@zed.dev>
2024-04-19 16:24:35 +03:00
Thorsten Ball
9247da77a3 git blame: Display GitHub avatars in blame tooltips, if available (#10767)
Release Notes:

- Added GitHub avatars to tooltips that appear when hovering over a `git
blame` entry (either inline or in the blame gutter).

Demo:



https://github.com/zed-industries/zed/assets/1185253/295c5aee-3a4e-46aa-812d-495439d8840d
2024-04-19 15:15:19 +02:00
Philipp Schaffrath
37e4f83a78 Fix stutter while dragging something over an element that stops event propagation (#10737)
~~This is extracted from #10643.~~

~~It looks like the editor had a small optimization to drop events when
hovering the gutter. This also happens while dragging a tab over the
gutter, and causes some stuttering. Please correct me if this wasn't
just a small optimization, but I could not derive a different reason for
this code to exist.~~

The window was waiting for event propagation to update any drag. This
change makes sure the drag always gets updated, which makes sure it will
always be fluid, no matter if any element stops event propagation. Ty
@as-cii for pointing me to a better solution!

Release Notes:

- Fixed issue where dragging tab over any element that stops event
propagation would stutter
2024-04-19 13:29:13 +02:00
Piotr Osiewicz
3273f5e404 fs: Move Repository trait into git crate (#10768)
/cc @mrnugget 
Release Notes:

- N/A
2024-04-19 11:57:17 +02:00
Piotr Osiewicz
8513a24dd8 chore: Revert accidental pushes to main (#10769)
This reverts two commits that I've just pushed to main instead of
`move-repository-trait-into-git`. Mea culpa.
Release Notes:

- N/A
2024-04-19 10:56:29 +02:00
Piotr Osiewicz
54699e39e7 fixup! fixup! Always provide default task context (#10764) 2024-04-19 10:54:14 +02:00
Piotr Osiewicz
8fc8309e45 fixup! Always provide default task context (#10764) 2024-04-19 10:53:37 +02:00
Kirill Bulatov
222034cacf Always provide default task context (#10764)
Based on
https://github.com/zed-industries/zed/issues/8324?notification_referrer_id=NT_kwDOACkO1bI5NTk0NjM0NzkyOjI2OTA3NzM&notifications_query=repo%3Azed-industries%2Fzed+is%3Aunread#issuecomment-2065551553

Release Notes:

- Fixed certain files' task modal not showing context-based tasks
2024-04-19 10:51:50 +03:00
张小白
9863b920b0 windows: Fix panic with some unicode characters (#10750)
Fix #10749 

Release Notes:

- N/A
2024-04-18 22:28:39 -07:00
Joseph T. Lyons
ea952b2a95 Remove empty script 2024-04-18 22:24:48 -04:00
Jason Wen
dd7eced2b6 Prevent command prompt from opening when running git blame on windows (#10747)
Release Notes:

- Fixed issue reported in discord where the git blame feature would open
command prompt windows
2024-04-18 16:02:24 -07:00
apricotbucket28
d4922eb10b wayland: Fix window close (#10702)
Partially fixes https://github.com/zed-industries/zed/issues/10483 (X11
still has this issue)

Also adds some missing destroy() calls for some objects.
Thanks @phisch!

Release Notes:

- N/A
2024-04-18 16:02:05 -07:00
Kirill Bulatov
95827d4c49 Fix the typo 2024-04-19 01:52:16 +03:00
Kirill Bulatov
2602fc47bb Match user selection when renaming (#10748)
Initial state:
<img width="337" alt="Screenshot 2024-04-19 at 01 35 34"
src="https://github.com/zed-industries/zed/assets/2690773/1720d06c-54ed-4479-b694-ea478ac5a55a">

Before the fix:
<img width="319" alt="Screenshot 2024-04-19 at 01 35 39"
src="https://github.com/zed-industries/zed/assets/2690773/64429088-e75b-44c3-b5d4-31a841e69a1d">

After:
<img width="336" alt="Screenshot 2024-04-19 at 01 36 43"
src="https://github.com/zed-industries/zed/assets/2690773/c523e549-c546-4a70-aa33-629912598466">
 

Release Notes:

- Improved rename selections to match the user ones
2024-04-19 01:49:14 +03:00
Kirill Bulatov
6d1ea782a4 Show tooltip in task spawn modal (#10744)
Tooltip shows original task template's label, if it differs from the one
displayed in the modal. Also, a resolved command with args will be shown
in the tooltip if different from the modal entry text.

<img width="578" alt="Screenshot 2024-04-19 at 00 40 28"
src="https://github.com/zed-industries/zed/assets/2690773/c89369d6-8ffc-4464-ab3b-ea5e8fb7625a">
<img width="761" alt="Screenshot 2024-04-19 at 00 40 32"
src="https://github.com/zed-industries/zed/assets/2690773/b02f1518-976a-4a9b-ba7c-f88c6e056217">
<img width="738" alt="Screenshot 2024-04-19 at 00 40 56"
src="https://github.com/zed-industries/zed/assets/2690773/be502537-f4bd-4ae0-a5e7-78e37fe8fb00">
<img width="785" alt="Screenshot 2024-04-19 at 00 41 01"
src="https://github.com/zed-industries/zed/assets/2690773/9bedcd21-8729-44c8-9a17-46a5a01c7f26">


Release Notes:

- Added tooltips into task spawn modal
2024-04-19 01:43:52 +03:00
Kirill Bulatov
870a61dd4d Add "Open in Terminal" context menu entries for project panel, editor and tab context menus (#10741)
Closes https://github.com/zed-industries/zed/issues/4566

Pane tabs (does not exist for multibuffer tabs):
<img width="439" alt="Screenshot 2024-04-18 at 23 01 08"
src="https://github.com/zed-industries/zed/assets/2690773/3af79ed8-07ea-4cf2-bcf9-735b1b3be8c4">

Editor context menu:
<img width="404" alt="Screenshot 2024-04-18 at 23 01 14"
src="https://github.com/zed-industries/zed/assets/2690773/38ea7afc-df2b-45ef-8331-eb6a4588af9f">

Project panel context menu (was not shown for file entries before this):
<img width="408" alt="Screenshot 2024-04-18 at 23 01 18"
src="https://github.com/zed-industries/zed/assets/2690773/e336fce1-7da0-4671-b8d2-8d3409c23eb6">

Release Notes:

- (breaking change) Moved `project_panel::OpenInTerminal` into
`workspace::OpenInTerminal` action and add it in editors, tab context
menus and proper panel file entries
([4566](https://github.com/zed-industries/zed/issues/4566))
2024-04-19 01:43:46 +03:00
Kirill Bulatov
250b71fb44 Select buffer search query on follow-up cmd-f (#10745)
https://github.com/zed-industries/zed/assets/2690773/fd754cfc-aca5-4c4d-9b42-53d1c8eca0e9

Unfortunately, the tests did not work for me, as the `FocusSearch`
action handlers were never called for me.

Release Notes:

- Improved buffer search workflow: follow-up cmd-f select query string
2024-04-19 01:43:33 +03:00
Philipp Schaffrath
15c4c4a308 wayland: Fix input_handler out of range access (#10724)
The wayland implementation takes an input handler from the state, but
only puts it back if the event was an IME key. I flipped the logic to
ensure it's always put back.

This should fix both:
- #10344
- #10652

Release Notes:

- Fixed input_handler out of range access
([#10344](https://github.com/zed-industries/zed/issues/10344),
[#10652](https://github.com/zed-industries/zed/issues/10652)).
2024-04-18 15:20:23 -07:00
apricotbucket28
b31df39ab0 linux: Primary clipboard (#10534)
Implements copying from and pasting to the primary selection.

Release Notes:

- N/A
2024-04-18 14:54:18 -07:00
Owen Law
98db7fa61e Use XI2 for Scrolling on X11 (#10695)
Changes the X11 platform code to use the xinput extension which allows
for smooth scrolling and horizontal scrolling.

Release Notes:

- Added smooth scrolling to X11 on Linux
- Added horizontal scrolling to X11 on Linux
2024-04-18 14:44:21 -07:00
Nimit Savant
bd5473a582 docs: Install Windows 10/11 SDK with VS installation (#10550)
This installation is also needed with VS installation. Only then they
would be able to target the WINDOWS SDK

Optionally, include screenshots / media showcasing your addition that
can be included in the release notes.

I was getting link.exe error before this stating that my MSVC was not
installed properly. But MSVC was perfectly installed. I went on
stackoverflow and checked for similar instances with `cargo run` on
windows and found this

https://stackoverflow.com/a/55603112/12859779

where in comment Fasis states that we need to install WINDOWS 10 SDK. I
had Windos 11 so Installed that and it worked :)

Release Notes:

- N/A
2024-04-18 14:42:20 -07:00
Marshall Bowers
1fbc04104c Move lints section to the top of Cargo.toml, to match the others 2024-04-18 15:53:48 -04:00
Philipp Schaffrath
2f892e3523 Improve drop targets (#10643)
This introduces multiple improvements to the drop targets.

## Hitbox shape
Currently, hitboxes are rectangles, where the vertical ones reach all
the way to the ends, which reduces the space for the horizontal ones,
making the hitboxes a bit awkward in the corners. This new approach just
determines the closest side.

Visual representation:
![Frame
3](https://github.com/zed-industries/zed/assets/1282767/1cd2ca31-d9d4-41dd-87fb-1a8fbb8b7fcc)

## Hitbox size
The width of the hitbox was currently always 8 rem all around. In setups
with many columns or rows, or when the font size was very large, this
could potentially overlap the center hitbox, not allowing to drop a tab
without another split. Now the width of the hitboxes are a fraction of
the smaller size of its parents width and height. This makes sure the
hitboxes have the same width all around, but never fully block the
center hitbox.

I've also made this value configurable through the new
`drop_target_size` config which takes a `f32` fraction and is set to 0.2
by default.

Not sure if this is worth mentioning, but this technically allows to
remove the split hitboxes all together by setting it to `0.0`, or
removing the center hitbox by setting it to any value `>=0.5`. Not that
this is necessary, but it would be possible now.

## Larger visualization
The visual overlay when using one of the side hitboxes were also `8em`
wide. Since their logical size now changed, and it can't currently be
represented with GPUI (without abusing the `canvas` element), I made the
visual feedback take half of the width or height of the available space,
just like how other editors do this.

Also, the opacity/alpha value set by a theme is currently ignored. This
change now respects the themes opacity for it!

## Respect alpha value
Currently, the alpha value of `drop_target.background` is ignored. Even
the default themes set a value that is overwritten by a hard coded
value. I have removed this hard coded value and it now respects the
alpha value.

This change affects existing themes, see
https://github.com/zed-industries/zed/pull/10643#issuecomment-2059641528


## ~~No more lag while dragging over gutter~~ Extracted into #10737
~~It looks like the editor had a small optimization to drop events when
hovering the gutter. This also happens while dragging a tab over the
gutter, and causes some stuttering. Please correct me if this wasn't
just a small optimization, but I could not derive a different reason for
this code to exist.~~

Here is a video that tries to show all those changes with a before on
the left, and the after on the right:


https://github.com/zed-industries/zed/assets/1282767/f97f3420-513f-410f-a1c8-7966429ad348


Release Notes:

- Added `drop_target_size` setting. This should be a fractional percent
(e.g., `0.5`).
- Improved the hitboxes for drop targets.
- Updated drop targets to respect the alpha channel of the
`drop_target.background` color.
2024-04-18 15:28:25 -04:00
张小白
5c3e5cc45d windows: Support emoji inputs (#10125)
To work properly, needs #10119 to be merged.



https://github.com/zed-industries/zed/assets/14981363/2bb0c51b-6c70-4b29-8baa-302fb4fb9e89



Release Notes:

- N/A
2024-04-18 11:59:22 -07:00
张小白
11a3d2b04b windows: Introduce Direct Write (#10119)
This PR brings `Direct Write` to Zed. Now, Zed first trys to query
dwrite interface, if not supported, which means runing on Windows below
win10 1703), will choose `cosmic` as a fallback text system.

This direct write text system supports:
- Full font features support
- Emoji support
- Default system fonts as fallback

### Font features


https://github.com/zed-industries/zed/assets/14981363/198eff88-47df-4bc8-a257-e3acf81fd61d

### Emoji

![Screenshot 2024-04-03
211354](https://github.com/zed-industries/zed/assets/14981363/a5bc5845-42e8-4af1-af7e-abba598c1e72)

**Note: input emoji through IME or IMM not working yet, copy paste emoji
works fine (will be fixed by #10125 )**

### Font fallback

I use `Zed mono` which dose not support chinese chars to test font
fallback



https://github.com/zed-industries/zed/assets/14981363/c97d0847-0ac5-47e6-aa00-f3ce6d1e50a5



Release Notes:

- N/A
2024-04-18 11:58:46 -07:00
Marshall Bowers
1127b1a0de glsl: Bump to v0.1.0 (#10734)
This PR bumps the GLSL extension to v0.1.0.

Changes:

- #10694

Release Notes:

- N/A
2024-04-18 11:57:21 -04:00
jansol
c55055599a glsl: Add glsl_analyzer (LSP) (#10694)
<img width="691" alt="image"
src="https://github.com/zed-industries/zed/assets/2588851/c5e02d12-d1e4-4407-971c-72de7e6599f0">

@mikayla-maki the extension lists you as the original author.

Release Notes:

- N/A

---------

Co-authored-by: Marshall Bowers <elliott.codes@gmail.com>
2024-04-18 11:23:11 -04:00
Bennet Bo Fenner
a202499c9a markdown preview: Update channel notes when other collaborator changes buffer (#10718)
https://github.com/zed-industries/zed/assets/53836821/9a57885e-83b0-49fb-b3a8-0a7868566b85


Release Notes:

- Markdown preview now re-renders when another collaborator changes the
content of channel notes
2024-04-18 14:26:32 +02:00
Thorsten Ball
c2428f9f5d git blame: Parse permalinks client side (#10714)
Release Notes:

- N/A
2024-04-18 12:36:22 +02:00
Kirill Bulatov
d5c5394693 Open exactly one terminal on workspace::NewTerminal action (#10721)
Fixes https://github.com/zed-industries/zed/issues/4567

Release Notes:

- Fixed multiple terminals being opened on `workspace::NewTerminal`
calls ([4567](https://github.com/zed-industries/zed/issues/4567))
2024-04-18 12:44:40 +03:00
Shreekar Halvi
bb97432e9a Add minimum column option to git inline blame (#10682)
Release Notes:

- Added a setting to determine the minimum column where the inline blame
information is shown. Example: `{{"git": {"inline_blame": {"min_column":
80}}}` ([#10555](https://github.com/zed-industries/zed/issues/10555)).

Demo Video:


https://github.com/zed-industries/zed/assets/1185253/61343dbe-9002-4bd1-b0d4-403f8da79050

---------

Co-authored-by: Thorsten Ball <mrnugget@gmail.com>
2024-04-18 11:28:47 +02:00
David Baldwin
1b75f9d620 Add a setting to show/hide the project panel button in the status bar (#10707)
### What?

A setting has been added to control the visibility of the Project Panel
button in the status bar.

### Why?

I don't tend to use the Project Panel, but use a keyboard shortcut to
access if needed. Thus, the button in the status bar provides me little
more than visual clutter. Additionally, there is precedent for this
configurability with other panels (collaboration, chat, notification,
terminal, etc).

Release Notes:

- Added a setting to show/hide the Project Panel button in the status
bar. `{"project_panel": {"button": false}}`
2024-04-18 10:14:06 +03:00
Conrad Irwin
4c3178e7a8 Have the CI server draft the release notes (#10700)
While I don't expect these to be useful for our weekly minor releases, I
hope that this will save a step for people doing mid-week patches.

Release Notes:

- N/A
2024-04-17 15:51:45 -06:00
Conrad Irwin
41c8f2caa6 Attempt to fix segfault in window drop (#10690)
By default NSWindow's release themselves when closed, which doesn't
interact well with rust's lifetime system.

Disable that behaviour, and explicitly release the NSWindow when the
window handle is dropped.

Release Notes:

- Fixed a (rare) panic when closing a window.
2024-04-17 15:29:10 -06:00
Piotr Osiewicz
b9e0269991 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.
2024-04-17 22:55:20 +02:00
Piotr Osiewicz
4f2214e1d6 terminal: Treat paths with non-digit col/rows as paths nonetheless (#10691)
This relaxes path parsing to allow paths like ./foo.rs:food or
./food/foo_bar.rs:2:12:food as some tools may add a suffix without
regard for col/row end.

Fixes #10688 

Release Notes:

- Made path parsing in terminal (for directory links) more lenient with
regards to row/column fields.
2024-04-17 22:55:08 +02:00
Andrew Lygin
0875257852 Merge branch 'main' into tab-bar-settings 2024-04-17 21:40:03 +03:00
Marshall Bowers
e25f0dfb0a v0.133.x dev 2024-04-17 13:05:26 -04:00
Marshall Bowers
3c805d4c6b prisma: Bump to v0.0.2 (#10689)
This PR bumps the Prisma extension to v0.02.

Changes:

- The Prisma extension now provides its own `tab_size` setting
([#10296](https://github.com/zed-industries/zed/pull/10296))

Release Notes:

- N/A
2024-04-17 12:40:01 -04:00
Thorsten Ball
4f1861edb6 git blame: ignore uncommitted files or repos without commits (#10685)
This fixes useless error messages popping up in case a file hasn't been
committed yet or the repo doesn't have commits yet.

Release Notes:

- Fixed git blame functionality not handling errors correctly when there
are no commits yet or when file isn't committed yet.
2024-04-17 17:51:26 +02:00
Thorsten Ball
d7becce9aa git: Only show inline git blame when editor is focused (#10680)
Release Notes:

- N/A
2024-04-17 13:28:11 +02:00
Thorsten Ball
62171387f6 Do not show tooltip for editor controls if clicked (#10679)
This avoids the tooltip showing up when the context menu is visible.

It fixes this:

![screenshot-2024-04-17-13 17
41@2x](https://github.com/zed-industries/zed/assets/1185253/373bb70e-9c7f-4b9f-a928-8206697c6039)


Release Notes:

- N/A
2024-04-17 13:20:47 +02:00
Thorsten Ball
47ad010901 Backport documentation for inline git blame (#10677)
Only noticed this when editing zed.dev.

Release Notes:

- N/A
2024-04-17 13:08:04 +02:00
Piotr Osiewicz
06987edadb project panel: Fix alignment of entries overflowing the panel. (#10676)
With file icons turned off, we still reserve space for an icon and make
it invisible. However, that space was marked as flex, which made it
shrink in case subsequent file name could not fit in the current width
of the project panel. Fixes #10622



https://github.com/zed-industries/zed/assets/24362066/d565a03a-3712-49d1-bf52-407e4508a8cf


Release Notes:


- Fixed project panel entries misalignment with narrow panel & file
icons turned off.
2024-04-17 12:54:56 +02:00
Thorsten Ball
1e1a2807db Document inline git blame (#10675)
Release Notes:

- N/A
2024-04-17 12:53:53 +02:00
Bennet Bo Fenner
9782dd342f docs: Sync with zed.dev version (#10674)
This PR brings the docs in line with the version we have on
https://zed.dev

Release Notes:

- N/A
2024-04-17 12:23:30 +02:00
Keith
535bcfad10 Update crates/ui/docs/hello-world.md TODO with explanation of SharedString usage (#10664)
Filled out a comment where there was a TODO to explain SharedString
usage.

Release Notes:

- N/A
2024-04-17 13:04:28 +03:00
Thorsten Ball
c76bacb974 Rename label to toggle inline git blame on/off (#10673)
cc @iamnbutler I think we should differentiate between inline blame and
the gutter blame.

Release Notes:

- N/A
2024-04-17 11:34:34 +02:00
Kirill Bulatov
20554d0296 Fix center element wrapper size (#10672)
Fixes
https://github.com/zed-industries/zed/pull/9754#pullrequestreview-2005401133
Fixes
https://github.com/zed-industries/zed/pull/9754#issuecomment-2060536590
Closes https://github.com/zed-industries/zed/pull/10669

* Updates the docs to use a proper max value for the centered layout
padding (0.4 instead of 0.45)
* Makes the `center` wrapper (`h_flex`) to be of size of the `center`
element always, to ensure terminal lines are displayed correctly

The letter fix is somewhat hacky: while it does the right thing right
now, it does not prevent us from future mistakes like these, and does
not explain why the bottom dock could be of one, smaller, height, and
its contents, the terminal pane/terminal element/something else would
think that it has a larger height, thus breaking the scrolling and
rendering.
cc @alygin if you're interested to solve another layout-related thing.

Release Notes:

- N/A
2024-04-17 12:34:18 +03:00
Thorsten Ball
2c78cf349b Regenerate git blame info when buffer's dirty bit changed (#10670)
This fixes the https://github.com/zed-industries/zed/issues/10583 by
regenerating the blame information after the
buffer is edited. It uses a debounce of 2seconds. Meaning that undone
deletions show up again after 2secs.

Release Notes:

- Fixed `git blame` data not handling the undoing of deletions
correctly.
([#10583](https://github.com/zed-industries/zed/issues/10583)).
2024-04-17 11:25:53 +02:00
Pedro Augusto da Silva Soares
c81eb419d4 Clear credentials state and delete keychain on SignOut request (#10558)
Release Notes:

- Fixed ([#4716](https://github.com/zed-industries/zed/issues/4716)).

---------

Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
2024-04-16 19:57:38 -06:00
Conrad Irwin
c4e446f8a8 ./script/trigger-release (#10589)
Add `./script/trigger-release {nightly|stable|preview}`

This command can be run regardless of the state of your local git
repository, and it
either triggers a workflow run of `bump_patch_version.yml` (for
stable/preview) or
it force pushes the nightly tag.

Also add some docs on releases to explain all of this.

Release Notes:

- N/A
2024-04-16 19:32:51 -06:00
Max Brunsfeld
bc7eaa6cd5 Add links to jobs page in README and in app, under help menu (#10658)
Release Notes:

- N/A

Co-authored-by: Nathan <nathan@zed.dev>
2024-04-16 15:47:23 -07:00
Nate Butler
e93d554725 Add Editor Controls Menu to Tool Bar (#10655)
This PR adds an editor controls menu to the tool bar. This menu will be
used to contain controls that toggle visual features in the editor, like
toggling inlay hints, showing git status or blame, hiding the gutter,
hiding or showing elements in the tool bar, etc.

For the moment, this consolidates the new Inline Git Blame toggle and
the old Inlay Hints toggle. In the future it will contain additional
controls.

Before: 

![CleanShot - 2024-04-16 at 16 38
53@2x](https://github.com/zed-industries/zed/assets/1714999/249e353f-786a-4391-8d49-66dd61feff8a)

After:

![CleanShot - 2024-04-16 at 16 38
43@2x](https://github.com/zed-industries/zed/assets/1714999/5b3cf4d5-855a-4475-ac05-8474b6c94b7b)

---

Release Notes:

- Added an editor controls menu to the tool bar. This will contain
visual, editor-specific options like toggling inlay hints, showing git
status or blame, etc.
- Removed the top level inlay hint toggle from the tool bar due to the
above change.
- Added the ability to toggle inline git blame from the new editor
controls menu.

---------

Co-authored-by: Marshall Bowers <1486634+maxdeviant@users.noreply.github.com>
2024-04-16 18:03:54 -04:00
Mikayla Maki
775539b3fa Fix order of migrations from #9754 (#10657)
This fixes a bug caused by mis-ordered database migration in #9754

Release Notes:

- N/A
2024-04-16 14:29:04 -07:00
Marshall Bowers
545319bced terraform: Bump to v0.0.2 (#10653)
This PR bumps the Terraform extension to v0.0.2.

Changes:

- #10641

Release Notes:

- N/A
2024-04-16 16:08:58 -04:00
Marshall Bowers
0b2de51c37 csharp: Bump to v0.0.2 (#10651)
This PR bumps the C# extension to v0.0.2.

Changes:

- #10638

Release Notes:

- N/A
2024-04-16 15:49:16 -04:00
Marshall Bowers
9a680dafc3 clojure: Bump to v0.0.2 (#10650)
This PR bumps the Clojure extension to v0.0.2.

Changes:

- #10636

Release Notes:

- N/A
2024-04-16 15:37:54 -04:00
Marshall Bowers
4c35cfaa69 gleam: Bump to v0.1.1 (#10648)
This PR bumps the Gleam extension to v0.1.1.

Changes:

- #10635

Release Notes:

- N/A
2024-04-16 15:21:14 -04:00
Kirill Bulatov
be2bf98529 Show task summary in its terminal after it stops running (#10615)
Based on https://github.com/alacritty/alacritty/issues/7795

Unknown error code commands (now includes the interrupted ones):

![image](https://github.com/zed-industries/zed/assets/2690773/801868bc-081c-453c-a353-233d4397bda9)

Successful command:

![image](https://github.com/zed-industries/zed/assets/2690773/874377c7-c967-4a6f-8a89-ec7bf398a8b3)

Unsuccessful command:

![image](https://github.com/zed-industries/zed/assets/2690773/6c99dc5d-d324-41e9-a71b-5d0bf705de27)

The "design", including wordings and special characters, is not final,
suggestions are welcome.
The main idea was to somehow distinguish the appended lines without
occupying extra vertical space.

Release Notes:

- Added task summary output into corresponding terminal tabs
2024-04-16 22:13:35 +03:00
Andrew Lygin
4eb1e65fbb Add centered layout support (#9754)
This PR implements the Centered Layout feature (#4685):
- Added the `toggle centered layout` action.
- The centered layout mode only takes effect when there's a single
central pane.
- The state of the centered layout toggle is saved / restored between
Zed restarts.
- The paddings are controlled by the `centered_layout` setting:

```json
"centered_layout": {
  "left_padding": 0.2,
  "right_padding": 0.2
}
```

This allows us to support both the VSCode-style (equal paddings) and
IntelliJ-style (only left padding in Zen mode).

Release Notes:

- Added support for Centered Layout
([#4685](https://github.com/zed-industries/zed/pull/9754)).


https://github.com/zed-industries/zed/assets/2101250/2d5b2a16-c248-48b5-9e8c-6f1219619398

Related Issues:

- Part of #4382
2024-04-16 12:12:41 -07:00
Marshall Bowers
52591905fb lua: Bump to v0.0.2
The previous v0.0.3 bump was in error, as we hadn't published a v0.0.2 yet.
2024-04-16 14:27:05 -04:00
Marshall Bowers
d2e83cc148 lua: Bump to v0.0.3 (#10646)
This PR bumps the Lua extension to v0.0.3.

Changes:

- #10639
- #10642

Release Notes:

- N/A
2024-04-16 14:12:41 -04:00
Marshall Bowers
f633460a8d zig: Bump to v0.1.1 (#10645)
This PR bumps the Zig extension to v0.1.1.

Changes:

- #10559
- #10634

Release Notes:

- N/A
2024-04-16 14:05:12 -04:00
Peter Tripp
9470a52b5d lua: Fix broken LuaLS download on x64 (#10642)
The changes in #10437 accidentally switched 'x64' to 'x86_64' which
breaks installs on linux x64, macos x64 and windows x64. This yields the
following error:

```
[2024-04-16T12:58:01-04:00 ERROR project] failed to start language server "lua-language-server": no asset found matching "lua-language-server-3.7.4-darwin-x86_64.tar.gz"
[2024-04-16T12:58:01-04:00 ERROR project] server stderr: Some("")
```

It's trying to download: 
`lua-language-server-3.7.4-darwin-x86_64.tar.gz`
which should be
`lua-language-server-3.7.4-darwin-x64.tar.gz`

See [LuaLS release
page](https://github.com/LuaLS/lua-language-server/releases/tag/3.6.25).

CC: @maxbrunsfeld

lua.rs before ef4c70c:

c6028f6651/crates/languages/src/lua.rs (L35)

lua.rs after:

5d7148bde1/extensions/lua/src/lua.rs (L49)

Release Notes:

- N/A
2024-04-16 13:48:45 -04:00
Marshall Bowers
fa0302f156 terraform: Don't cache user-installed terraform-ls (#10641)
This PR updates the Terraform extension to not cache the binary when it
is using the one on the $PATH.

Release Notes:

- N/A
2024-04-16 13:40:13 -04:00
Marshall Bowers
5d7148bde1 lua: Don't cache user-installed lua-language-server (#10639)
This PR updates the Lua extension to not cache the binary when it is
using the one on the $PATH.

Release Notes:

- N/A
2024-04-16 13:22:42 -04:00
Marshall Bowers
58991f332b csharp: Don't cache user-installed OmniSharp (#10638)
This PR updates the C# extension to not cache the binary when it is
using the one on the $PATH.

Release Notes:

- N/A
2024-04-16 13:22:36 -04:00
Marshall Bowers
9c569c8d95 zig: Rename cached_binary to cached_binary_path (#10637)
This PR renames the `cached_binary` field on the `ZigExtension` back to
`cached_binary_path` to make it match the other extensions.

Release Notes:

- N/A
2024-04-16 13:18:21 -04:00
Marshall Bowers
1ba0bf925b clojure: Don't cache user-installed clojure-lsp (#10636)
This PR updates the Clojure extension to not cache the binary when it is
using the one on the $PATH.

Release Notes:

- N/A
2024-04-16 13:12:08 -04:00
Marshall Bowers
53105ddd16 gleam: Don't cache user-installed gleam (#10635)
This PR updates the Gleam extension to not cache the binary when it is
using the one on the $PATH.

Release Notes:

- N/A
2024-04-16 13:11:58 -04:00
Thorsten Ball
210f8ebfed zig: Do not cache user-installed zls (#10634)
This was a bug introduced when moving to extensions: when we find a
binary in the user's project environment, we shouldn't cache that
globally since it might not work for other projects.

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

Release Notes:


- N/A
2024-04-16 19:05:11 +02:00
Thorsten Ball
c015b5c4cd Enable inline-git-blame by default (#10632)
Release Notes:

- N/A
2024-04-16 18:53:57 +02:00
Henrique Ferreiro
c1c8a74c7f Add ability to specify binary path/args for clangd (#10608)
This uses the language server settings added in #9293 to allow users to
specify the binary path and arguments with which to start up `clangd`.

Example user settings for `clangd`:

```json
{
  "lsp": {
    "clangd": {
      "binary": {
        "path": "/usr/bin/clangd",
        "arguments": ["--log=verbose"]
      },
    }
  }
}
```

Constraints:

* Right now this only allows ABSOLUTE paths.

Release Notes:

- Added ability to specify `clangd` binary `path` (must be absolute) and
`arguments` in user settings. Example: `{"lsp": {"clangd": {"binary":
{"path": "/usr/bin/clangd", "arguments": ["--log=verbose"] }}}}`
2024-04-16 18:17:15 +02:00
Peter Tripp
2f00fcbdf6 docs: use_autoclose (#10514)
Add some keywords (bracket, quote, etc)to the comments describing
`use_autoclose` preference in the settings json.

This setting took me a while to find -- so now it'll be more easily
searchable for others.

Release Notes:

- N/A
2024-04-16 18:17:04 +02:00
Thorsten Ball
5c5fb972d0 Handle setting git blame delay to 0 (#10626)
Release Notes:


- N/A
2024-04-16 17:25:24 +02:00
Piotr Osiewicz
7928095951 chore: parse cli args just once in zed crate (#10613)
Release Notes:

- N/A
2024-04-16 16:27:45 +02:00
Thorsten Ball
70c3ca4fdd Fix toggling of inline git blame when it's delayed (#10620)
Release Notes:

- N/A
2024-04-16 16:07:22 +02:00
Kirill Bulatov
d49271a112 Use project search action with the default keybinding in app menus (#10618)
Fixes https://github.com/zed-industries/zed/issues/10611 

Zed has `workspace::NewSearch` (without a default keybinding) and
`workspace::DeploySearch` (with the default keybinding for its
`DeploySearch::find()` form).

Use the one with the keybinding, as it's the whole point of the menu.

Release Notes:

- Fixed "Find In Project" app menu item having no keybinding
([10611](https://github.com/zed-industries/zed/issues/10611))
2024-04-16 16:47:30 +03:00
Piotr Osiewicz
e34c443331 lsp: Do not set error/result fields if they're missing. (#10610)
We were previously not conforming to LSP spec, as we were setting
**both** result field and error field on response fields, which could
confuse servers. Excerpt from the spec:
> * The result of a request. This member is REQUIRED on success.
> * This member MUST NOT exist if there was an error invoking the
method.

Fixes #10595

Release Notes:

- N/A
2024-04-16 14:27:12 +02:00
Kirill Bulatov
263023021d Show Zoom In/Out shortcuts in the labels (#10604)
Based on https://github.com/zed-industries/zed/discussions/10599 
Does the same as the assistant tab with the Zoom In/Out labels.


![image](https://github.com/zed-industries/zed/assets/2690773/afc59a3e-c3df-4fc8-bcaf-1d45a21aecf7)

Release Notes:

- Adjusted Zoom In/Out for Pane and Terminal Pane to show keybinding
labels
2024-04-16 14:00:25 +03:00
Thorsten Ball
7e1a184446 Fix Markdown code rendering in tooltips ignoring languages (#10607)
Some code blocks that are returned in tooltips (returned by language
servers, for example) use the language file extension as the language in
the the triple-backtick code blocks.

Example:

    ```rs
    fn rust_code() {}
    ```

    ```cpp
    fn rust_code() {}
    ```

Before this change we only looked up the language with the
`rs`/`cpp`/... being interpreted as the language name. Now we also treat
it as a possible file extension.



Release Notes:

- Fixed Markdown code blocks in tooltips not having correct language
highlighting.

Before:


![image](https://github.com/zed-industries/zed/assets/1185253/1f3870a6-467c-4e5f-9e49-1ff32240d10f)

After:

![screenshot-2024-04-16-12 43
39@2x](https://github.com/zed-industries/zed/assets/1185253/21a45ed5-825a-412d-9dc0-35a444fc64ba)

Co-authored-by: Bennet <bennetbo@gmx.de>
2024-04-16 12:49:35 +02:00
Thorsten Ball
c834ea75ef Fix --- in Markdown docs not rendered correctly (#10606)
This fixes #10511 by turning off the YAML metadata block rendering in
the Markdown parser.

`clangd` uses `---` as dividers, but our parser interpreted it as a YAML
metadata block, even though it didn't contain any valid YAML.

Example Markdown from `clangd`:

    ### instance-method `format`

    ---
    → `void`
    Parameters:
    - `const int &`
    - `const std::tm &`
    - `int & dest`

    ---
    ```cpp
    // In my_formatter_flag
    public: void format(const int &, const std::tm &, int &dest)
    ```

What's between the two `---` is *not* valid YAML. Neovim, too,
interprets these as dividers and renders them as such.

And since we don't handle any possible metadata anyway, we can turn off
the metadata handling, which causes the parser to interpret the `---` as
dividers.



Release Notes:

- Fixed Markdown returned by `clangd` being rendered the wrong way.
([#10511](https://github.com/zed-industries/zed/issues/10511)).

Before:

![screenshot-2024-04-16-12 32
15@2x](https://github.com/zed-industries/zed/assets/1185253/a268f106-9504-48aa-9744-42a7521de807)

After:

![screenshot-2024-04-16-12 33
02@2x](https://github.com/zed-industries/zed/assets/1185253/dd178a63-a075-48a9-85d9-565157a5b050)

Co-authored-by: Bennet <bennetbo@gmx.de>
2024-04-16 12:40:13 +02:00
Thorsten Ball
4d8cba2add Fix long git author names/emails overflowing blame tooltip (#10605)
This fixes https://github.com/zed-industries/zed/issues/10581.

Release Notes:

- N/A

Co-authored-by: Bennet <bennetbo@gmx.de>
2024-04-16 12:03:45 +02:00
Thorsten Ball
08aef198d5 Fix inline blame annotations handling wrapped lines (#10600)
Fixes inline blame not being displayed correctly for soft-wrapped lines.

(Can't find the ticket)
![screenshot-2024-04-16-10 50
29](https://github.com/zed-industries/zed/assets/1185253/e3ff9018-f796-469a-9d42-5997baf7d2f6)


Release Notes:

- N/A
2024-04-16 11:27:23 +02:00
Hans
2cfb1ffa77 add a setting to control show/hide terminal button for status bar (#10593)
Release Notes:

- Added a setting to show/hide the terminal button in the status bar:
`{"terminal": {"button": false}}` to hide it. (#10513)

---------

Co-authored-by: Thorsten Ball <mrnugget@gmail.com>
2024-04-16 09:40:32 +02:00
Andrew Lygin
f3192b6fa6 Fix scrollbar marker settings (#10530)
Zed displays scrollbar markers of three types: git diffs, background
highlights and diagnostics. At the moment, the "background highlights"
markers are displayed for all the supported highlights:

- Occurences of the symbol under cursor.
- Search results.
- Scope boundaries (only works when a symbol is selected).
- Active hover popover position.

They all use the same color, which leads to confusing results. For
instance, in the following case I expect to see markers for the
`new_anchor` occurences in lines 43 and 47. But besides them, there're
also scope-markers for `ScrollAnchor` initialization block in lines 46
and 49, which makes me think that there're four places where
`new_anchor` appears.

<img width="740" alt="zed-scrollbar-markers"
src="https://github.com/zed-industries/zed/assets/2101250/78700e6b-fdd1-4c2f-beff-e564d8defc13">

Existing settings `selection` and `symbol_selection` in the `scrollbar`
section [don't work as
expected](https://github.com/zed-industries/zed/pull/10080#discussion_r1552325493),
which increases confusion.

This PR only leaves two types of bg-highlight-markers and provides
dedicated settings for them:

- Occurences of the symbol under cursor. Setting: `selected_symbol`,
default is `true`.
- Search results. Setting: `search_results`, default is `true`.

The `selection` and `symbol_selection` settings are not used anymore.

Release Notes:

- Breaking changes. Settings `selection` and `symbol_selection` in the
`scrollbar` section renamed to `search_results` and `selected_symbol`
respectively. Fixed the effect of these settings on which markers are
displayed on the scrollbar.

Optionally, include screenshots / media showcasing your addition that
can be included in the release notes.

- N/A

/cc @mrnugget
2024-04-16 09:21:22 +02:00
Conrad Irwin
33b9aca090 Add a bump patch version workflow (#10588)
I'd like to make it less clunky to release a new patch version of
preview/stable.

Release Notes:

- N/A
2024-04-15 20:31:12 -06:00
Kirill Bulatov
57b087e41e On label conflict, prefer resolved tasks with never templates (#10586)
This way, static (and other) templates that change, e.g. env vars, will
get the new template resolved task in the modal to run.

Release Notes:

- N/A
2024-04-16 03:31:33 +03:00
Kyle Kelley
2a9ce3cec3 Handle no active editor available (#10503)
Cleaned up a stray `.unwrap()`

Release Notes:

- N/A
2024-04-15 16:53:08 -07:00
Kyle Kelley
f5c2483423 Close the backticks in gpui geometry examples (#10579)
I noticed some of the examples in `crates/gpui/src/geometry.rs` were
missing ending triple backticks.

Release Notes:

- N/A
2024-04-15 16:52:19 -07:00
Mikayla Maki
4d314b2dd0 Increase the context in other multibuffers (#10582)
This moves the diagnostics and find all references to be in line with
the search pane. This also centralizes the constant into the editor code
base.

Release Notes:

- Increased diagnostic context to match the project search context.
2024-04-15 14:44:14 -07:00
Abdullah Alsigar
7a112b22ac dart: Use upstream tree-sitter-dart (#10552)
Update `tree-sitter-dart` to the upstream package, there was an issue
building Rust package which is resolved now.


Release Notes:


- N/A
2024-04-15 17:32:35 -04:00
Andrew Lygin
eb97c311c8 Move the placement setting to tab_bar 2024-04-13 18:23:09 +03:00
Andrew Lygin
6422fdea9b Merge branch 'main' into tab-bar-settings 2024-04-13 10:25:23 +03:00
Andrew Lygin
9d684d7856 Resolve conflicts 2024-03-23 16:30:10 +03:00
Andrew Lygin
93978f6017 Fix tests 2024-03-23 16:08:05 +03:00
Andrew Lygin
120ead9429 Fix tests 2024-03-23 16:08:05 +03:00
Andrew Lygin
99a0356a56 Fix tests 2024-03-23 16:08:05 +03:00
Andrew Lygin
94858caff5 Fix typo 2024-03-23 16:08:05 +03:00
Andrew Lygin
ca9645c2bf Move tab bar placement setting to the "tabs" section 2024-03-23 16:08:05 +03:00
Andrew Lygin
b81e8971df Fix default tab bar placement 2024-03-23 15:51:57 +03:00
Andrew Lygin
ed2651d62a Documentation for the editor tab bar settings 2024-03-23 15:51:56 +03:00
Andrew Lygin
b8a6fc316f Support tab bar rendering when it's at the bottom 2024-03-23 15:51:56 +03:00
Andrew Lygin
2fa9220f44 Settings for the editor tab bar placement 2024-03-23 15:46:35 +03:00
406 changed files with 19892 additions and 8579 deletions

View File

@@ -0,0 +1,49 @@
name: bump_patch_version
on:
workflow_dispatch:
inputs:
branch:
description: "Branch name to run on"
required: true
concurrency:
# Allow only one workflow per any non-`main` branch.
group: ${{ github.workflow }}-${{ github.event.input.branch }}
cancel-in-progress: true
jobs:
bump_patch_version:
runs-on:
- self-hosted
- test
steps:
- name: Checkout code
uses: actions/checkout@v2
with:
ref: ${{ github.event.inputs.branch }}
ssh-key: ${{ secrets.ZED_BOT_DEPLOY_KEY }}
- name: Bump Patch Version
run: |
set -eux
channel=$(cat crates/zed/RELEASE_CHANNEL)
tag_suffix=""
case $channel in
stable)
;;
preview)
tag_suffix="-pre"
;;
*)
echo "this must be run on either of stable|preview release branches" >&2
exit 1
;;
esac
which cargo-set-version > /dev/null || cargo install cargo-edit --features vendored-openssl
output=$(cargo set-version -p zed --bump patch 2>&1 | sed 's/.* //')
git commit -am "Bump to $output for @$GITHUB_ACTOR" --author "Zed Bot <hi@zed.dev>"
git tag v${output}${tag_suffix}
git push origin HEAD v${output}${tag_suffix}

View File

@@ -173,6 +173,11 @@ jobs:
- name: Checkout repo
uses: actions/checkout@v4
with:
# We need to fetch more than one commit so that `script/draft-release-notes`
# is able to diff between the current and previous tag.
#
# 25 was chosen arbitrarily.
fetch-depth: 25
clean: false
submodules: "recursive"
@@ -205,6 +210,9 @@ jobs:
echo "invalid release tag ${GITHUB_REF_NAME}. expected ${expected_tag_name}"
exit 1
fi
mkdir -p target/
# Ignore any errors that occur while drafting release notes to not fail the build.
script/draft-release-notes "$version" "$channel" > target/release-notes.md || true
- name: Generate license file
run: script/generate-licenses
@@ -248,7 +256,7 @@ jobs:
target/aarch64-apple-darwin/release/Zed-aarch64.dmg
target/x86_64-apple-darwin/release/Zed-x86_64.dmg
target/release/Zed.dmg
body: ""
body_file: target/release-notes.md
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

2
.gitignore vendored
View File

@@ -6,7 +6,7 @@
/plugins/bin
/script/node_modules
/crates/theme/schemas/theme.json
/crates/collab/.admins.json
/crates/collab/seed.json
/assets/*licenses.md
**/venv
.build

View File

@@ -21,5 +21,7 @@
"formatter": "prettier"
}
},
"formatter": "auto"
"formatter": "auto",
"remove_trailing_whitespace_on_save": true,
"ensure_final_newline_on_save": true
}

View File

@@ -3,5 +3,10 @@
"label": "clippy",
"command": "cargo",
"args": ["xtask", "clippy"]
},
{
"label": "assistant2",
"command": "cargo",
"args": ["run", "-p", "assistant2", "--example", "assistant_example"]
}
]

326
Cargo.lock generated
View File

@@ -284,21 +284,21 @@ checksum = "d92bec98840b8f03a5ff5413de5293bfcd8bf96467cf5452609f939ec6f5de16"
[[package]]
name = "ash"
version = "0.37.3+1.3.251"
version = "0.38.0+1.3.281"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39e9c3835d686b0a6084ab4234fcd1b07dbf6e4767dce60874b12356a25ecd4a"
checksum = "0bb44936d800fea8f016d7f2311c6a4f97aebd5dc86f09906139ec848cf3a46f"
dependencies = [
"libloading 0.7.4",
"libloading 0.8.0",
]
[[package]]
name = "ash-window"
version = "0.12.0"
version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b912285a7c29f3a8f87ca6f55afc48768624e5e33ec17dbd2f2075903f5e35ab"
checksum = "52bca67b61cb81e5553babde81b8211f713cb6db79766f80168f3e5f40ea6c82"
dependencies = [
"ash",
"raw-window-handle 0.5.2",
"raw-window-handle 0.6.0",
"raw-window-metal",
]
@@ -371,6 +371,52 @@ dependencies = [
"workspace",
]
[[package]]
name = "assistant2"
version = "0.1.0"
dependencies = [
"anyhow",
"assets",
"assistant_tooling",
"client",
"editor",
"env_logger",
"feature_flags",
"fs",
"futures 0.3.28",
"gpui",
"language",
"languages",
"log",
"nanoid",
"node_runtime",
"open_ai",
"project",
"rand 0.8.5",
"release_channel",
"rich_text",
"schemars",
"semantic_index",
"serde",
"serde_json",
"settings",
"theme",
"ui",
"util",
"workspace",
]
[[package]]
name = "assistant_tooling"
version = "0.1.0"
dependencies = [
"anyhow",
"gpui",
"schemars",
"serde",
"serde_json",
]
[[package]]
name = "async-broadcast"
version = "0.7.0"
@@ -643,7 +689,7 @@ checksum = "5fd55a5ba1179988837d24ab4c7cc8ed6efdeff578ede0416b4225a5fca35bd0"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.48",
"syn 2.0.59",
]
[[package]]
@@ -710,7 +756,7 @@ checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.48",
"syn 2.0.59",
]
[[package]]
@@ -741,7 +787,7 @@ checksum = "c980ee35e870bd1a4d2c8294d4c04d0499e67bca1e4b5cefcc693c2fa00caea9"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.48",
"syn 2.0.59",
]
[[package]]
@@ -1385,7 +1431,7 @@ dependencies = [
"regex",
"rustc-hash",
"shlex",
"syn 2.0.48",
"syn 2.0.59",
"which 4.4.2",
]
@@ -1434,7 +1480,7 @@ dependencies = [
[[package]]
name = "blade-graphics"
version = "0.4.0"
source = "git+https://github.com/kvark/blade?rev=810ec594358aafea29a4a3d8ab601d25292b2ce4#810ec594358aafea29a4a3d8ab601d25292b2ce4"
source = "git+https://github.com/kvark/blade?rev=e82eec97691c3acdb43494484be60d661edfebf3#e82eec97691c3acdb43494484be60d661edfebf3"
dependencies = [
"ash",
"ash-window",
@@ -1455,7 +1501,7 @@ dependencies = [
"mint",
"naga",
"objc",
"raw-window-handle 0.5.2",
"raw-window-handle 0.6.0",
"slab",
"wasm-bindgen",
"web-sys",
@@ -1464,11 +1510,11 @@ dependencies = [
[[package]]
name = "blade-macros"
version = "0.2.1"
source = "git+https://github.com/kvark/blade?rev=810ec594358aafea29a4a3d8ab601d25292b2ce4#810ec594358aafea29a4a3d8ab601d25292b2ce4"
source = "git+https://github.com/kvark/blade?rev=e82eec97691c3acdb43494484be60d661edfebf3#e82eec97691c3acdb43494484be60d661edfebf3"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.48",
"syn 2.0.59",
]
[[package]]
@@ -1634,7 +1680,7 @@ checksum = "965ab7eb5f8f97d2a083c799f3a1b994fc397b2fe2da5d1da1626ce15a39f2b1"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.48",
"syn 2.0.59",
]
[[package]]
@@ -2019,7 +2065,7 @@ dependencies = [
"heck 0.4.1",
"proc-macro2",
"quote",
"syn 2.0.48",
"syn 2.0.59",
]
[[package]]
@@ -2047,6 +2093,7 @@ dependencies = [
"core-services",
"ipc-channel",
"plist",
"release_channel",
"serde",
"util",
]
@@ -2253,6 +2300,7 @@ dependencies = [
"prost",
"rand 0.8.5",
"release_channel",
"remote_projects",
"reqwest",
"rpc",
"rustc-demangle",
@@ -2298,7 +2346,6 @@ dependencies = [
"editor",
"emojis",
"extensions_ui",
"feature_flags",
"futures 0.3.28",
"fuzzy",
"gpui",
@@ -2958,7 +3005,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "30d2b3721e861707777e3195b0158f950ae6dc4a27e4d02ff9f67e3eb3de199e"
dependencies = [
"quote",
"syn 2.0.48",
"syn 2.0.59",
]
[[package]]
@@ -3135,13 +3182,17 @@ dependencies = [
"anyhow",
"client",
"collections",
"ctor",
"editor",
"env_logger",
"futures 0.3.28",
"gpui",
"language",
"log",
"lsp",
"pretty_assertions",
"project",
"rand 0.8.5",
"schemars",
"serde",
"serde_json",
@@ -3388,10 +3439,18 @@ dependencies = [
]
[[package]]
name = "embed-manifest"
version = "1.4.0"
name = "embed-resource"
version = "2.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41cd446c890d6bed1d8b53acef5f240069ebef91d6fae7c5f52efe61fe8b5eae"
checksum = "c6985554d0688b687c5cb73898a34fbe3ad6c24c58c238a4d91d5e840670ee9d"
dependencies = [
"cc",
"memchr",
"rustc_version",
"toml 0.8.10",
"vswhom",
"winreg 0.52.0",
]
[[package]]
name = "emojis"
@@ -3441,7 +3500,7 @@ checksum = "5c785274071b1b420972453b306eeca06acf4633829db4223b58a2a8c5953bc4"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.48",
"syn 2.0.59",
]
[[package]]
@@ -3770,6 +3829,7 @@ dependencies = [
"ctor",
"editor",
"env_logger",
"futures 0.3.28",
"fuzzy",
"gpui",
"itertools 0.11.0",
@@ -3798,6 +3858,17 @@ dependencies = [
"util",
]
[[package]]
name = "filedescriptor"
version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7199d965852c3bac31f779ef99cbb4537f80e952e2d6aa0ffeb30cce00f4f46e"
dependencies = [
"libc",
"thiserror",
"winapi",
]
[[package]]
name = "filetime"
version = "0.2.22"
@@ -3942,7 +4013,7 @@ checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.48",
"syn 2.0.59",
]
[[package]]
@@ -4002,15 +4073,12 @@ dependencies = [
"gpui",
"lazy_static",
"libc",
"log",
"notify",
"parking_lot",
"rope",
"serde",
"serde_derive",
"serde_json",
"smol",
"sum_tree",
"tempfile",
"text",
"time",
@@ -4185,7 +4253,7 @@ checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.48",
"syn 2.0.59",
]
[[package]]
@@ -4318,7 +4386,10 @@ dependencies = [
"git2",
"lazy_static",
"log",
"parking_lot",
"pretty_assertions",
"regex",
"rope",
"serde",
"serde_json",
"smol",
@@ -4327,6 +4398,8 @@ dependencies = [
"time",
"unindent",
"url",
"util",
"windows 0.53.0",
]
[[package]]
@@ -4433,9 +4506,9 @@ dependencies = [
[[package]]
name = "gpu-alloc-ash"
version = "0.6.0"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d2424bc9be88170e1a56e57c25d3d0e2dfdd22e8f328e892786aeb4da1415732"
checksum = "cbda7a18a29bc98c2e0de0435c347df935bf59489935d0cbd0b73f1679b6f79a"
dependencies = [
"ash",
"gpu-alloc-types",
@@ -4477,8 +4550,10 @@ dependencies = [
"cosmic-text",
"ctor",
"derive_more",
"embed-resource",
"env_logger",
"etagere",
"filedescriptor",
"flume",
"font-kit",
"foreign-types 0.5.0",
@@ -4501,7 +4576,6 @@ dependencies = [
"postage",
"profiling",
"rand 0.8.5",
"raw-window-handle 0.5.2",
"raw-window-handle 0.6.0",
"refineable",
"resvg",
@@ -4664,6 +4738,7 @@ dependencies = [
"project",
"rpc",
"settings",
"shellexpand",
"util",
]
@@ -5046,7 +5121,7 @@ checksum = "ce243b1bfa62ffc028f1cc3b6034ec63d649f3031bc8a4fbbb004e1ac17d1f68"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.48",
"syn 2.0.59",
]
[[package]]
@@ -5395,6 +5470,7 @@ dependencies = [
"globset",
"gpui",
"indoc",
"itertools 0.11.0",
"lazy_static",
"log",
"lsp",
@@ -5491,12 +5567,9 @@ dependencies = [
"regex",
"rope",
"rust-embed",
"schemars",
"serde",
"serde_derive",
"serde_json",
"settings",
"shellexpand",
"smol",
"task",
"text",
@@ -5507,12 +5580,10 @@ dependencies = [
"tree-sitter-c",
"tree-sitter-cpp",
"tree-sitter-css",
"tree-sitter-elixir",
"tree-sitter-embedded-template",
"tree-sitter-go",
"tree-sitter-gomod",
"tree-sitter-gowork",
"tree-sitter-heex",
"tree-sitter-jsdoc",
"tree-sitter-json 0.20.0",
"tree-sitter-markdown",
@@ -5667,7 +5738,7 @@ checksum = "ba125974b109d512fccbc6c0244e7580143e460895dfd6ea7f8bbb692fd94396"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.48",
"syn 2.0.59",
]
[[package]]
@@ -5846,6 +5917,7 @@ version = "0.1.0"
dependencies = [
"anyhow",
"async-recursion 1.0.5",
"collections",
"editor",
"gpui",
"language",
@@ -5902,9 +5974,9 @@ dependencies = [
[[package]]
name = "memchr"
version = "2.6.3"
version = "2.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f232d6ef707e1956a43342693d2a31e72989554d58299d7a88738cc95b0d35c"
checksum = "6c8640c5d730cb13ebd907d8d04b52f55ac9a2eec55b440c8892f40d56c76c1d"
[[package]]
name = "memfd"
@@ -6049,14 +6121,18 @@ dependencies = [
"anyhow",
"clock",
"collections",
"ctor",
"env_logger",
"futures 0.3.28",
"git",
"gpui",
"itertools 0.11.0",
"language",
"log",
"parking_lot",
"rand 0.8.5",
"settings",
"smallvec",
"sum_tree",
"text",
"theme",
@@ -6624,7 +6700,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.48",
"syn 2.0.59",
]
[[package]]
@@ -6700,7 +6776,7 @@ dependencies = [
"proc-macro-error",
"proc-macro2",
"quote",
"syn 2.0.48",
"syn 2.0.59",
]
[[package]]
@@ -6780,7 +6856,7 @@ checksum = "e8890702dbec0bad9116041ae586f84805b13eecd1d8b1df27c29998a9969d6d"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.48",
"syn 2.0.59",
]
[[package]]
@@ -6958,7 +7034,7 @@ dependencies = [
"phf_shared",
"proc-macro2",
"quote",
"syn 2.0.48",
"syn 2.0.59",
]
[[package]]
@@ -7009,7 +7085,7 @@ checksum = "4359fd9c9171ec6e8c62926d6faaf553a8dc3f64e1507e76da7911b4f6a04405"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.48",
"syn 2.0.59",
]
[[package]]
@@ -7233,7 +7309,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae005bd773ab59b4725093fd7df83fd7892f7d8eafb48dbd7de6e024e4215f9d"
dependencies = [
"proc-macro2",
"syn 2.0.48",
"syn 2.0.59",
]
[[package]]
@@ -7290,9 +7366,9 @@ dependencies = [
[[package]]
name = "proc-macro2"
version = "1.0.78"
version = "1.0.81"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae"
checksum = "3d1597b0c024618f09a9c3b8655b7e430397a36d23fdafec26d6965e9eec3eba"
dependencies = [
"unicode-ident",
]
@@ -7313,7 +7389,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8021cf59c8ec9c432cfc2526ac6b8aa508ecaf29cd415f271b8406c1b851c3fd"
dependencies = [
"quote",
"syn 2.0.48",
"syn 2.0.59",
]
[[package]]
@@ -7374,6 +7450,7 @@ dependencies = [
"db",
"editor",
"file_icons",
"git",
"gpui",
"language",
"menu",
@@ -7665,14 +7742,14 @@ checksum = "42a9830a0e1b9fb145ebb365b8bc4ccd75f290f98c0247deafbbe2c75cefb544"
[[package]]
name = "raw-window-metal"
version = "0.3.2"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac4ea493258d54c24cb46aa9345d099e58e2ea3f30dd63667fc54fc892f18e76"
checksum = "76e8caa82e31bb98fee12fa8f051c94a6aa36b07cddb03f0d4fc558988360ff1"
dependencies = [
"cocoa",
"core-graphics",
"objc",
"raw-window-handle 0.5.2",
"raw-window-handle 0.6.0",
]
[[package]]
@@ -7708,7 +7785,9 @@ dependencies = [
name = "recent_projects"
version = "0.1.0"
dependencies = [
"anyhow",
"editor",
"feature_flags",
"fuzzy",
"gpui",
"language",
@@ -7716,10 +7795,15 @@ dependencies = [
"ordered-float 2.10.0",
"picker",
"project",
"remote_projects",
"rpc",
"serde",
"serde_json",
"settings",
"smol",
"theme",
"ui",
"ui_text_field",
"util",
"workspace",
]
@@ -7846,6 +7930,18 @@ dependencies = [
"once_cell",
]
[[package]]
name = "remote_projects"
version = "0.1.0"
dependencies = [
"anyhow",
"client",
"gpui",
"rpc",
"serde",
"serde_json",
]
[[package]]
name = "rend"
version = "0.4.0"
@@ -7889,7 +7985,7 @@ dependencies = [
"wasm-bindgen",
"wasm-bindgen-futures",
"web-sys",
"winreg",
"winreg 0.50.0",
]
[[package]]
@@ -8136,7 +8232,7 @@ dependencies = [
"proc-macro2",
"quote",
"rust-embed-utils",
"syn 2.0.48",
"syn 2.0.59",
"walkdir",
]
@@ -8410,7 +8506,7 @@ dependencies = [
"proc-macro-error",
"proc-macro2",
"quote",
"syn 2.0.48",
"syn 2.0.59",
]
[[package]]
@@ -8451,7 +8547,7 @@ dependencies = [
"proc-macro2",
"quote",
"sea-bae",
"syn 2.0.48",
"syn 2.0.59",
"unicode-ident",
]
@@ -8635,7 +8731,7 @@ checksum = "33c85360c95e7d137454dc81d9a4ed2b8efd8fbe19cee57357b32b9771fccb67"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.48",
"syn 2.0.59",
]
[[package]]
@@ -8700,7 +8796,7 @@ checksum = "8725e1dfadb3a50f7e5ce0b1a540466f6ed3fe7a0fca2ac2b8b831d31316bd00"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.48",
"syn 2.0.59",
]
[[package]]
@@ -9403,7 +9499,6 @@ dependencies = [
"ctrlc",
"dialoguer",
"editor",
"embed-manifest",
"fuzzy",
"gpui",
"indoc",
@@ -9411,6 +9506,7 @@ dependencies = [
"log",
"menu",
"picker",
"project",
"rust-embed",
"settings",
"simplelog",
@@ -9465,7 +9561,7 @@ dependencies = [
"proc-macro2",
"quote",
"rustversion",
"syn 2.0.48",
"syn 2.0.59",
]
[[package]]
@@ -9594,9 +9690,9 @@ dependencies = [
[[package]]
name = "syn"
version = "2.0.48"
version = "2.0.59"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0f3531638e407dfc0814761abb7c00a5b54992b849452a0646b7f65c9f770f3f"
checksum = "4a6531ffc7b071655e4ce2e04bd464c4830bb585a61cabb96cf808f05172615a"
dependencies = [
"proc-macro2",
"quote",
@@ -9736,7 +9832,6 @@ dependencies = [
"file_icons",
"fuzzy",
"gpui",
"itertools 0.11.0",
"language",
"picker",
"project",
@@ -9745,7 +9840,6 @@ dependencies = [
"serde_json",
"settings",
"task",
"terminal",
"tree-sitter-rust",
"tree-sitter-typescript",
"ui",
@@ -9841,9 +9935,9 @@ dependencies = [
"serde_json",
"settings",
"shellexpand",
"shlex",
"smol",
"task",
"tasks_ui",
"terminal",
"theme",
"ui",
@@ -9963,7 +10057,7 @@ checksum = "49922ecae66cc8a249b77e68d1d0623c1b2c514f0060c27cdc68bd62a1219d35"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.48",
"syn 2.0.59",
]
[[package]]
@@ -10142,7 +10236,7 @@ checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.48",
"syn 2.0.59",
]
[[package]]
@@ -10367,7 +10461,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.48",
"syn 2.0.59",
]
[[package]]
@@ -10435,7 +10529,7 @@ dependencies = [
[[package]]
name = "tree-sitter"
version = "0.20.100"
source = "git+https://github.com/tree-sitter/tree-sitter?rev=7f21c3b98c0749ac192da67a0d65dfe3eabc4a63#7f21c3b98c0749ac192da67a0d65dfe3eabc4a63"
source = "git+https://github.com/tree-sitter/tree-sitter?rev=528bcd2274814ca53711a57d71d1e3cf7abd73fe#528bcd2274814ca53711a57d71d1e3cf7abd73fe"
dependencies = [
"cc",
"regex",
@@ -10547,7 +10641,7 @@ dependencies = [
[[package]]
name = "tree-sitter-jsdoc"
version = "0.20.0"
source = "git+https://github.com/tree-sitter/tree-sitter-jsdoc#6a6cf9e7341af32d8e2b2e24a37fbfebefc3dc55"
source = "git+https://github.com/tree-sitter/tree-sitter-jsdoc?rev=6a6cf9e7341af32d8e2b2e24a37fbfebefc3dc55#6a6cf9e7341af32d8e2b2e24a37fbfebefc3dc55"
dependencies = [
"cc",
"tree-sitter",
@@ -10988,8 +11082,8 @@ name = "vcs_menu"
version = "0.1.0"
dependencies = [
"anyhow",
"fs",
"fuzzy",
"git",
"gpui",
"picker",
"ui",
@@ -11017,6 +11111,7 @@ dependencies = [
"futures 0.3.28",
"gpui",
"indoc",
"itertools 0.11.0",
"language",
"log",
"lsp",
@@ -11052,6 +11147,26 @@ version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c3082ca00d5a5ef149bb8b555a72ae84c9c59f7250f013ac822ac2e49b19c64"
[[package]]
name = "vswhom"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "be979b7f07507105799e854203b470ff7c78a1639e330a58f183b5fea574608b"
dependencies = [
"libc",
"vswhom-sys",
]
[[package]]
name = "vswhom-sys"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3b17ae1f6c8a2b28506cd96d412eebf83b4a0ff2cbefeeb952f2f9dfa44ba18"
dependencies = [
"cc",
"libc",
]
[[package]]
name = "vte"
version = "0.13.0"
@@ -11134,7 +11249,7 @@ dependencies = [
"once_cell",
"proc-macro2",
"quote",
"syn 2.0.48",
"syn 2.0.59",
"wasm-bindgen-shared",
]
@@ -11168,7 +11283,7 @@ checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.48",
"syn 2.0.59",
"wasm-bindgen-backend",
"wasm-bindgen-shared",
]
@@ -11305,7 +11420,7 @@ dependencies = [
"anyhow",
"proc-macro2",
"quote",
"syn 2.0.48",
"syn 2.0.59",
"wasmtime-component-util",
"wasmtime-wit-bindgen",
"wit-parser",
@@ -11466,7 +11581,7 @@ checksum = "6d6d967f01032da7d4c6303da32f6a00d5efe1bac124b156e7342d8ace6ffdfc"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.48",
"syn 2.0.59",
]
[[package]]
@@ -11746,7 +11861,7 @@ dependencies = [
"proc-macro2",
"quote",
"shellexpand",
"syn 2.0.48",
"syn 2.0.59",
"witx",
]
@@ -11758,7 +11873,7 @@ checksum = "512d816dbcd0113103b2eb2402ec9018e7f0755202a5b3e67db726f229d8dcae"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.48",
"syn 2.0.59",
"wiggle-generate",
]
@@ -11876,7 +11991,7 @@ checksum = "942ac266be9249c84ca862f0a164a39533dc2f6f33dc98ec89c8da99b82ea0bd"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.48",
"syn 2.0.59",
]
[[package]]
@@ -11887,7 +12002,7 @@ checksum = "da33557140a288fae4e1d5f8873aaf9eb6613a9cf82c3e070223ff177f598b60"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.48",
"syn 2.0.59",
]
[[package]]
@@ -12125,6 +12240,16 @@ dependencies = [
"windows-sys 0.48.0",
]
[[package]]
name = "winreg"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a277a57398d4bfa075df44f501a17cfdf8542d224f0d36095a2adc7aee4ef0a5"
dependencies = [
"cfg-if",
"windows-sys 0.48.0",
]
[[package]]
name = "winresource"
version = "0.1.17"
@@ -12204,7 +12329,7 @@ dependencies = [
"anyhow",
"proc-macro2",
"quote",
"syn 2.0.48",
"syn 2.0.59",
"wit-bindgen-core",
"wit-bindgen-rust",
]
@@ -12284,6 +12409,7 @@ dependencies = [
"parking_lot",
"postage",
"project",
"remote_projects",
"schemars",
"serde",
"serde_json",
@@ -12522,12 +12648,13 @@ dependencies = [
[[package]]
name = "zed"
version = "0.132.0"
version = "0.134.0"
dependencies = [
"activity_indicator",
"anyhow",
"assets",
"assistant",
"assistant2",
"audio",
"auto_update",
"backtrace",
@@ -12547,7 +12674,6 @@ dependencies = [
"db",
"diagnostics",
"editor",
"embed-manifest",
"env_logger",
"extension",
"extensions_ui",
@@ -12582,6 +12708,7 @@ dependencies = [
"quick_action_bar",
"recent_projects",
"release_channel",
"remote_projects",
"rope",
"search",
"serde",
@@ -12623,20 +12750,34 @@ dependencies = [
[[package]]
name = "zed_clojure"
version = "0.0.1"
version = "0.0.2"
dependencies = [
"zed_extension_api 0.0.4",
]
[[package]]
name = "zed_csharp"
version = "0.0.1"
version = "0.0.2"
dependencies = [
"zed_extension_api 0.0.4",
]
[[package]]
name = "zed_dart"
version = "0.0.2"
dependencies = [
"zed_extension_api 0.0.6 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "zed_deno"
version = "0.0.1"
dependencies = [
"zed_extension_api 0.0.6 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "zed_elixir"
version = "0.0.1"
dependencies = [
"zed_extension_api 0.0.6 (registry+https://github.com/rust-lang/crates.io-index)",
@@ -12694,6 +12835,13 @@ dependencies = [
[[package]]
name = "zed_gleam"
version = "0.1.1"
dependencies = [
"zed_extension_api 0.0.6 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "zed_glsl"
version = "0.1.0"
dependencies = [
"zed_extension_api 0.0.6 (registry+https://github.com/rust-lang/crates.io-index)",
@@ -12715,7 +12863,7 @@ dependencies = [
[[package]]
name = "zed_lua"
version = "0.0.1"
version = "0.0.2"
dependencies = [
"zed_extension_api 0.0.6 (registry+https://github.com/rust-lang/crates.io-index)",
]
@@ -12736,7 +12884,7 @@ dependencies = [
[[package]]
name = "zed_prisma"
version = "0.0.1"
version = "0.0.2"
dependencies = [
"zed_extension_api 0.0.4",
]
@@ -12757,7 +12905,7 @@ dependencies = [
[[package]]
name = "zed_terraform"
version = "0.0.1"
version = "0.0.3"
dependencies = [
"zed_extension_api 0.0.6 (registry+https://github.com/rust-lang/crates.io-index)",
]
@@ -12785,7 +12933,7 @@ dependencies = [
[[package]]
name = "zed_zig"
version = "0.1.0"
version = "0.1.1"
dependencies = [
"zed_extension_api 0.0.6 (registry+https://github.com/rust-lang/crates.io-index)",
]
@@ -12813,7 +12961,7 @@ checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.48",
"syn 2.0.59",
]
[[package]]
@@ -12833,7 +12981,7 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.48",
"syn 2.0.59",
]
[[package]]

View File

@@ -4,6 +4,8 @@ members = [
"crates/anthropic",
"crates/assets",
"crates/assistant",
"crates/assistant_tooling",
"crates/assistant2",
"crates/audio",
"crates/auto_update",
"crates/breadcrumbs",
@@ -67,6 +69,7 @@ members = [
"crates/refineable",
"crates/refineable/derive_refineable",
"crates/release_channel",
"crates/remote_projects",
"crates/rich_text",
"crates/rope",
"crates/rpc",
@@ -106,10 +109,13 @@ members = [
"extensions/clojure",
"extensions/csharp",
"extensions/dart",
"extensions/deno",
"extensions/elixir",
"extensions/elm",
"extensions/emmet",
"extensions/erlang",
"extensions/gleam",
"extensions/glsl",
"extensions/haskell",
"extensions/html",
"extensions/lua",
@@ -135,6 +141,8 @@ ai = { path = "crates/ai" }
anthropic = { path = "crates/anthropic" }
assets = { path = "crates/assets" }
assistant = { path = "crates/assistant" }
assistant2 = { path = "crates/assistant2" }
assistant_tooling = { path = "crates/assistant_tooling" }
audio = { path = "crates/audio" }
auto_update = { path = "crates/auto_update" }
base64 = "0.13"
@@ -199,12 +207,14 @@ project_symbols = { path = "crates/project_symbols" }
quick_action_bar = { path = "crates/quick_action_bar" }
recent_projects = { path = "crates/recent_projects" }
release_channel = { path = "crates/release_channel" }
remote_projects = { path = "crates/remote_projects" }
rich_text = { path = "crates/rich_text" }
rope = { path = "crates/rope" }
rpc = { path = "crates/rpc" }
task = { path = "crates/task" }
tasks_ui = { path = "crates/tasks_ui" }
search = { path = "crates/search" }
semantic_index = { path = "crates/semantic_index" }
semantic_version = { path = "crates/semantic_version" }
settings = { path = "crates/settings" }
snippet = { path = "crates/snippet" }
@@ -240,9 +250,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 = "810ec594358aafea29a4a3d8ab601d25292b2ce4" }
blade-macros = { git = "https://github.com/kvark/blade", rev = "810ec594358aafea29a4a3d8ab601d25292b2ce4" }
blade-rwh = { package = "raw-window-handle", version = "0.5" }
blade-graphics = { git = "https://github.com/kvark/blade", rev = "e82eec97691c3acdb43494484be60d661edfebf3" }
blade-macros = { git = "https://github.com/kvark/blade", rev = "e82eec97691c3acdb43494484be60d661edfebf3" }
cap-std = "3.0"
chrono = { version = "0.4", features = ["serde"] }
clap = { version = "4.4", features = ["derive"] }
@@ -259,7 +268,9 @@ futures-batch = "0.6.1"
futures-lite = "1.13"
git2 = { version = "0.18", default-features = false }
globset = "0.4"
heed = { git = "https://github.com/meilisearch/heed", rev = "036ac23f73a021894974b9adc815bc95b3e0482a", features = ["read-txn-no-tls"] }
heed = { git = "https://github.com/meilisearch/heed", rev = "036ac23f73a021894974b9adc815bc95b3e0482a", features = [
"read-txn-no-tls",
] }
hex = "0.4.3"
ignore = "0.4.22"
indoc = "1"
@@ -272,6 +283,7 @@ itertools = "0.11.0"
lazy_static = "1.4.0"
linkify = "0.10.0"
log = { version = "0.4.16", features = ["kv_unstable_serde"] }
nanoid = "0.4"
ordered-float = "2.1.1"
palette = { version = "0.7.5", default-features = false, features = ["std"] }
parking_lot = "0.12.1"
@@ -298,7 +310,6 @@ serde_json_lenient = { version = "0.1", features = [
] }
serde_repr = "0.1"
sha2 = "0.10"
shlex = "1.3"
shellexpand = "2.1.0"
smallvec = { version = "1.6", features = ["union"] }
smol = "1.2"
@@ -331,7 +342,7 @@ tree-sitter-gowork = { git = "https://github.com/d1y/tree-sitter-go-work" }
rustc-demangle = "0.1.23"
tree-sitter-heex = { git = "https://github.com/phoenixframework/tree-sitter-heex", rev = "2e1348c3cf2c9323e87c2744796cf3f3868aa82a" }
tree-sitter-html = "0.19.0"
tree-sitter-jsdoc = { git = "https://github.com/tree-sitter/tree-sitter-jsdoc", ref = "6a6cf9e7341af32d8e2b2e24a37fbfebefc3dc55" }
tree-sitter-jsdoc = { git = "https://github.com/tree-sitter/tree-sitter-jsdoc", rev = "6a6cf9e7341af32d8e2b2e24a37fbfebefc3dc55" }
tree-sitter-json = { git = "https://github.com/tree-sitter/tree-sitter-json", rev = "40a81c01a40ac48744e0c8ccabbaba1920441199" }
tree-sitter-markdown = { git = "https://github.com/MDeiml/tree-sitter-markdown", rev = "330ecab87a3e3a7211ac69bbadc19eabecdb1cca" }
tree-sitter-proto = { git = "https://github.com/rewinfrey/tree-sitter-proto", rev = "36d54f288aee112f13a67b550ad32634d0c2cb52" }
@@ -364,10 +375,16 @@ sys-locale = "0.3.1"
version = "0.53.0"
features = [
"implement",
"Foundation_Numerics",
"Wdk_System_SystemServices",
"Win32_Globalization",
"Win32_Graphics_Direct2D",
"Win32_Graphics_Direct2D_Common",
"Win32_Graphics_DirectWrite",
"Win32_Graphics_Dxgi_Common",
"Win32_Graphics_Gdi",
"Win32_Graphics_Imaging",
"Win32_Graphics_Imaging_D2D",
"Win32_Media",
"Win32_Security",
"Win32_Security_Credentials",
@@ -391,7 +408,7 @@ features = [
]
[patch.crates-io]
tree-sitter = { git = "https://github.com/tree-sitter/tree-sitter", rev = "7f21c3b98c0749ac192da67a0d65dfe3eabc4a63" }
tree-sitter = { git = "https://github.com/tree-sitter/tree-sitter", rev = "528bcd2274814ca53711a57d71d1e3cf7abd73fe" }
# Workaround for a broken nightly build of gpui: See #7644 and revisit once 0.5.3 is released.
pathfinder_simd = { git = "https://github.com/servo/pathfinder.git", rev = "30419d07660dc11a21e42ef4a7fa329600cff152" }

100
README.md
View File

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

9
assets/icons/LICENSES Normal file
View File

@@ -0,0 +1,9 @@
Lucide License
ISC License
Copyright (c) for portions of Lucide are held by Cole Bemis 2013-2022 as part of Feather (MIT). All other copyright (c) for Lucide are held by Lucide Contributors 2022.
Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-unfold-vertical"><path d="M12 22v-6"/><path d="M12 8V2"/><path d="M4 12H2"/><path d="M10 12H8"/><path d="M16 12h-2"/><path d="M22 12h-2"/><path d="m15 19-3 3-3-3"/><path d="m15 5-3-3-3 3"/></svg>

After

Width:  |  Height:  |  Size: 398 B

View File

@@ -161,6 +161,8 @@
"webp": "image",
"wma": "audio",
"wmv": "video",
"woff": "font",
"woff2": "font",
"wv": "audio",
"xls": "document",
"xlsx": "document",
@@ -327,7 +329,7 @@
},
"tcl": {
"icon": "icons/file_icons/tcl.svg"
},
},
"vcs": {
"icon": "icons/file_icons/git.svg"
},

View File

@@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-mail-open"><path d="M21.2 8.4c.5.38.8.97.8 1.6v10a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V10a2 2 0 0 1 .8-1.6l8-6a2 2 0 0 1 2.4 0l8 6Z"/><path d="m22 10-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 10"/></svg>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-mail-open"><path d="M21.2 8.4c.5.38.8.97.8 1.6v10a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V10a2 2 0 0 1 .8-1.6l8-6a2 2 0 0 1 2.4 0l8 6Z"/><path d="m22 10-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 10"/></svg>

Before

Width:  |  Height:  |  Size: 390 B

After

Width:  |  Height:  |  Size: 391 B

3
assets/icons/person.svg Normal file
View File

@@ -0,0 +1,3 @@
<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M7.5 0.875C5.49797 0.875 3.875 2.49797 3.875 4.5C3.875 6.15288 4.98124 7.54738 6.49373 7.98351C5.2997 8.12901 4.27557 8.55134 3.50407 9.31167C2.52216 10.2794 2.02502 11.72 2.02502 13.5999C2.02502 13.8623 2.23769 14.0749 2.50002 14.0749C2.76236 14.0749 2.97502 13.8623 2.97502 13.5999C2.97502 11.8799 3.42786 10.7206 4.17091 9.9883C4.91536 9.25463 6.02674 8.87499 7.49995 8.87499C8.97317 8.87499 10.0846 9.25463 10.8291 9.98831C11.5721 10.7206 12.025 11.8799 12.025 13.5999C12.025 13.8623 12.2376 14.0749 12.5 14.0749C12.7623 14.075 12.975 13.8623 12.975 13.6C12.975 11.72 12.4778 10.2794 11.4959 9.31166C10.7244 8.55135 9.70025 8.12903 8.50625 7.98352C10.0187 7.5474 11.125 6.15289 11.125 4.5C11.125 2.49797 9.50203 0.875 7.5 0.875ZM4.825 4.5C4.825 3.02264 6.02264 1.825 7.5 1.825C8.97736 1.825 10.175 3.02264 10.175 4.5C10.175 5.97736 8.97736 7.175 7.5 7.175C6.02264 7.175 4.825 5.97736 4.825 4.5Z" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-git-pull-request-arrow"><circle cx="5" cy="6" r="3"/><path d="M5 9v12"/><circle cx="19" cy="18" r="3"/><path d="m15 9-3-3 3-3"/><path d="M12 6h5a2 2 0 0 1 2 2v7"/></svg>

After

Width:  |  Height:  |  Size: 372 B

View File

@@ -1,5 +1,16 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M7.99993 6.85713C11.1558 6.85713 13.7142 5.83379 13.7142 4.57142C13.7142 3.30905 11.1558 2.28571 7.99993 2.28571C4.84402 2.28571 2.28564 3.30905 2.28564 4.57142C2.28564 5.83379 4.84402 6.85713 7.99993 6.85713Z" fill="black" stroke="black" stroke-width="1.5"/>
<path d="M13.7142 4.57141V11.4286C13.7142 12.691 11.1558 13.7143 7.99993 13.7143C4.84402 13.7143 2.28564 12.691 2.28564 11.4286V4.57141" stroke="black" stroke-width="1.5"/>
<path d="M13.7142 8C13.7142 9.26237 11.1558 10.2857 7.99993 10.2857C4.84402 10.2857 2.28564 9.26237 2.28564 8" stroke="black" stroke-width="1.5"/>
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<rect width="20" height="8" x="2" y="2" rx="2" ry="2" />
<rect width="20" height="8" x="2" y="14" rx="2" ry="2" />
<line x1="6" x2="6.01" y1="6" y2="6" />
<line x1="6" x2="6.01" y1="18" y2="18" />
</svg>

Before

Width:  |  Height:  |  Size: 692 B

After

Width:  |  Height:  |  Size: 413 B

3
assets/icons/sliders.svg Normal file
View File

@@ -0,0 +1,3 @@
<svg width="17" height="17" viewBox="0 0 17 17" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M6.36667 3.79167C5.53364 3.79167 4.85833 4.46697 4.85833 5.3C4.85833 6.13303 5.53364 6.80833 6.36667 6.80833C7.1997 6.80833 7.875 6.13303 7.875 5.3C7.875 4.46697 7.1997 3.79167 6.36667 3.79167ZM2.1 5.925H3.67944C3.9626 7.14732 5.05824 8.05833 6.36667 8.05833C7.67509 8.05833 8.77073 7.14732 9.05389 5.925H14.9C15.2452 5.925 15.525 5.64518 15.525 5.3C15.525 4.95482 15.2452 4.675 14.9 4.675H9.05389C8.77073 3.45268 7.67509 2.54167 6.36667 2.54167C5.05824 2.54167 3.9626 3.45268 3.67944 4.675H2.1C1.75482 4.675 1.475 4.95482 1.475 5.3C1.475 5.64518 1.75482 5.925 2.1 5.925ZM13.3206 12.325C13.0374 13.5473 11.9418 14.4583 10.6333 14.4583C9.32491 14.4583 8.22927 13.5473 7.94611 12.325H2.1C1.75482 12.325 1.475 12.0452 1.475 11.7C1.475 11.3548 1.75482 11.075 2.1 11.075H7.94611C8.22927 9.85268 9.32491 8.94167 10.6333 8.94167C11.9418 8.94167 13.0374 9.85268 13.3206 11.075H14.9C15.2452 11.075 15.525 11.3548 15.525 11.7C15.525 12.0452 15.2452 12.325 14.9 12.325H13.3206ZM9.125 11.7C9.125 10.867 9.8003 10.1917 10.6333 10.1917C11.4664 10.1917 12.1417 10.867 12.1417 11.7C12.1417 12.533 11.4664 13.2083 10.6333 13.2083C9.8003 13.2083 9.125 12.533 9.125 11.7Z" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-trash-2"><path d="M3 6h18"/><path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6"/><path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2"/><line x1="10" x2="10" y1="11" y2="17"/><line x1="14" x2="14" y1="11" y2="17"/></svg>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-trash-2"><path d="M3 6h18"/><path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6"/><path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2"/><line x1="10" x2="10" y1="11" y2="17"/><line x1="14" x2="14" y1="11" y2="17"/></svg>

Before

Width:  |  Height:  |  Size: 409 B

After

Width:  |  Height:  |  Size: 410 B

View File

@@ -3,4 +3,3 @@
<path fill-rule="evenodd" clip-rule="evenodd" d="M8.64907 9.32382C8.313 9.13287 8.08213 8.81954 7.94725 8.4078C7.8147 8.00318 7.75317 7.44207 7.75317 6.73677C7.75317 6.03845 7.81141 5.48454 7.9369 5.08716L7.93755 5.08512C8.07231 4.67373 8.3034 4.36258 8.64088 4.17794C8.96806 3.99257 9.41119 3.9104 9.9496 3.9104C10.3406 3.9104 10.6632 3.95585 10.8967 4.06485C11.0079 4.11675 11.1099 4.18844 11.2033 4.27745V2.03027H12.4077V9.4856H11.2033V9.18983C11.0945 9.29074 10.98 9.37096 10.8591 9.42752C10.6327 9.53648 10.3335 9.58252 9.97867 9.58252C9.4339 9.58252 8.98592 9.50355 8.65375 9.3264L8.64907 9.32382ZM11.1139 7.85508C11.1841 7.60311 11.2227 7.23354 11.2227 6.73677C11.2227 6.24602 11.1841 5.88331 11.1141 5.63844C11.0457 5.39902 10.9401 5.25863 10.8149 5.18266L10.8077 5.17826C10.6804 5.09342 10.4713 5.03726 10.1531 5.03726C9.80785 5.03726 9.5719 5.09359 9.42256 5.1832L9.41829 5.18576C9.28002 5.26412 9.16722 5.40602 9.09399 5.64263C9.01876 5.88566 8.97694 6.24668 8.97694 6.73677C8.97694 7.23363 9.01882 7.59774 9.09399 7.8406C9.1673 8.07745 9.28097 8.22477 9.42256 8.30972C9.5719 8.39933 9.80785 8.45566 10.1531 8.45566C10.4721 8.45566 10.683 8.40265 10.8114 8.32216C10.9396 8.23944 11.0456 8.09373 11.1139 7.85508Z" fill="#787D87"/>
<rect x="1.14087" y="10.7188" width="11.7183" height="1.26565" rx="0.632824" fill="#787D87"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@@ -297,13 +297,8 @@
"ctrl-shift-k": "editor::DeleteLine",
"alt-up": "editor::MoveLineUp",
"alt-down": "editor::MoveLineDown",
"ctrl-alt-shift-up": [
"editor::DuplicateLine",
{
"move_upwards": true
}
],
"ctrl-alt-shift-down": "editor::DuplicateLine",
"ctrl-alt-shift-up": "editor::DuplicateLineUp",
"ctrl-alt-shift-down": "editor::DuplicateLineDown",
"ctrl-shift-left": "editor::SelectToPreviousWordStart",
"ctrl-shift-right": "editor::SelectToNextWordEnd",
"ctrl-shift-up": "editor::SelectLargerSyntaxNode", //todo(linux) tmp keybinding
@@ -527,6 +522,7 @@
"context": "Editor && mode == full",
"bindings": {
"alt-enter": "editor::OpenExcerpts",
"shift-enter": "editor::ExpandExcerpts",
"ctrl-k enter": "editor::OpenExcerptsSplit",
"ctrl-f8": "editor::GoToHunk",
"ctrl-shift-f8": "editor::GoToPrevHunk",
@@ -592,12 +588,6 @@
"tab": "channel_modal::ToggleMode"
}
},
{
"context": "ChatPanel > MessageEditor",
"bindings": {
"escape": "chat_panel::CloseReplyPreview"
}
},
{
"context": "FileFinder",
"bindings": { "ctrl-shift-p": "file_finder::SelectPrev" }

View File

@@ -209,7 +209,15 @@
}
},
{
"context": "AssistantPanel",
"context": "AssistantChat > Editor", // Used in the assistant2 crate
"bindings": {
"enter": ["assistant2::Submit", "Simple"],
"cmd-enter": ["assistant2::Submit", "Codebase"],
"escape": "assistant2::Cancel"
}
},
{
"context": "AssistantPanel", // Used in the assistant crate, which we're replacing
"bindings": {
"cmd-g": "search::SelectNextMatch",
"cmd-shift-g": "search::SelectPrevMatch"
@@ -541,6 +549,7 @@
"context": "Editor && mode == full",
"bindings": {
"alt-enter": "editor::OpenExcerpts",
"shift-enter": "editor::ExpandExcerpts",
"cmd-k enter": "editor::OpenExcerptsSplit",
"cmd-f8": "editor::GoToHunk",
"cmd-shift-f8": "editor::GoToPrevHunk",

View File

@@ -17,7 +17,11 @@
"cmd-enter": "menu::SecondaryConfirm",
"escape": "menu::Cancel",
"ctrl-c": "menu::Cancel",
"cmd-q": "storybook::Quit"
"cmd-q": "storybook::Quit",
"backspace": "editor::Backspace",
"delete": "editor::Delete",
"left": "editor::MoveLeft",
"right": "editor::MoveRight"
}
}
]

View File

@@ -47,11 +47,20 @@
// The factor to grow the active pane by. Defaults to 1.0
// which gives the same size as all other panes.
"active_pane_magnification": 1.0,
// Centered layout related settings.
"centered_layout": {
// The relative width of the left padding of the central pane from the
// workspace when the centered layout is used.
"left_padding": 0.2,
// The relative width of the right padding of the central pane from the
// workspace when the centered layout is used.
"right_padding": 0.2
},
// The key to use for adding multiple cursors
// Currently "alt" or "cmd_or_ctrl" (also aliased as
// "cmd" and "ctrl") are supported.
"multi_cursor_modifier": "alt",
// Whether to enable vim modes and key bindings
// Whether to enable vim modes and key bindings.
"vim_mode": false,
// Whether to show the informational hover box when moving the mouse
// over symbols in the editor.
@@ -60,6 +69,8 @@
"confirm_quit": false,
// Whether to restore last closed project when fresh Zed instance is opened.
"restore_on_startup": "last_workspace",
// Size of the drop target in the editor.
"drop_target_size": 0.2,
// Whether the cursor blinks in the editor.
"cursor_blink": true,
// Whether to pop the completions menu while typing in an editor without
@@ -92,8 +103,9 @@
// Whether to use additional LSP queries to format (and amend) the code after
// every "trigger" symbol input, defined by LSP server capabilities.
"use_on_type_format": true,
// Whether to automatically type closing characters for you. For example,
// when you type (, Zed will automatically add a closing ) at the correct position.
// Whether to automatically add matching closing characters when typing
// opening parenthesis, bracket, brace, single or double quote characters.
// For example, when you type (, Zed will add a closing ) at the correct position.
"use_autoclose": true,
// Controls how the editor handles the autoclosed characters.
// When set to `false`(default), skipping over and auto-removing of the closing characters
@@ -145,10 +157,10 @@
"show": "auto",
// Whether to show git diff indicators in the scrollbar.
"git_diff": true,
// Whether to show selections in the scrollbar.
"selections": true,
// Whether to show symbols selections in the scrollbar.
"symbols_selections": true,
// Whether to show buffer search results in the scrollbar.
"search_results": true,
// Whether to show selected symbol occurrences in the scrollbar.
"selected_symbol": true,
// Whether to show diagnostic indicators in the scrollbar.
"diagnostics": true
},
@@ -171,7 +183,7 @@
},
// The number of lines to keep above/below the cursor when scrolling.
"vertical_scroll_margin": 3,
// Scroll sensitivity multiplier. This multiplier is applied
// Scroll sensitivity multiplier. This multiplier is applied
// to both the horizontal and vertical delta values while scrolling.
"scroll_sensitivity": 1.0,
"relative_line_numbers": false,
@@ -202,6 +214,8 @@
"scroll_debounce_ms": 50
},
"project_panel": {
// Whether to show the project panel button in the status bar
"button": true,
// Default width of the project panel.
"default_width": 240,
// Where to dock the project panel. Can be 'left' or 'right'.
@@ -280,6 +294,10 @@
"show_call_status_icon": true,
// Whether to use language servers to provide code intelligence.
"enable_language_server": true,
// The list of language servers to use (or disable) for all languages.
//
// This is typically customized on a per-language basis.
"language_servers": ["..."],
// When to automatically save edited buffers. This setting can
// take four values.
//
@@ -294,6 +312,16 @@
"autosave": "off",
// Settings related to the editor's tab bar.
"tab_bar": {
// Where to show the tab bar in the editor.
// This setting can take three values:
//
// 1. Show tab bar at the top of the editor (default):
// "top"
// 2. Show tab bar at the bottom of the editor:
// "bottom"
// 3. Don't show the tab bar:
// "no"
"placement": "top",
// Whether or not to show the navigation history buttons.
"show_nav_history_buttons": true
},
@@ -397,7 +425,7 @@
// Control whether the git blame information is shown inline,
// in the currently focused line.
"inline_blame": {
"enabled": false
"enabled": true
// Sets a delay after which the inline blame information is shown.
// Delay is restarted with every cursor movement.
// "delay_ms": 600
@@ -490,6 +518,8 @@
// Whether or not selecting text in the terminal will automatically
// copy to the system clipboard.
"copy_on_select": false,
// Whether to show the terminal button in the status bar
"button": true,
// Any key-value pairs added to this list will be added to the terminal's
// environment. Use `:` to separate multiple values.
"env": {
@@ -535,31 +565,6 @@
// Existing terminals will not pick up this change until they are recreated.
// "max_scroll_history_lines": 10000,
},
// Settings specific to our elixir integration
"elixir": {
// Change the LSP zed uses for elixir.
// Note that changing this setting requires a restart of Zed
// to take effect.
//
// May take 3 values:
// 1. Use the standard ElixirLS, this is the default
// "lsp": "elixir_ls"
// 2. Use the experimental NextLs
// "lsp": "next_ls",
// 3. Use a language server installed locally on your machine:
// "lsp": {
// "local": {
// "path": "~/next-ls/bin/start",
// "arguments": ["--stdio"]
// }
// },
//
"lsp": "elixir_ls"
},
// Settings specific to our deno integration
"deno": {
"enable": false
},
"code_actions_on_format": {},
// An object whose keys are language names, and whose values
// are arrays of filenames or extensions of files that should
@@ -574,6 +579,13 @@
// }
//
"file_types": {},
// The extensions that Zed should automatically install on startup.
//
// If you don't want any of these extensions, add this field to your settings
// and change the value to `false`.
"auto_install_extensions": {
"html": true
},
// Different settings for specific languages.
"languages": {
"C++": {

View File

@@ -5,6 +5,9 @@ edition = "2021"
publish = false
license = "AGPL-3.0-or-later"
[lints]
workspace = true
[lib]
path = "src/anthropic.rs"
@@ -17,6 +20,3 @@ util.workspace = true
[dev-dependencies]
tokio.workspace = true
[lints]
workspace = true

View File

@@ -5,6 +5,9 @@ edition = "2021"
publish = false
license = "GPL-3.0-or-later"
[lib]
path = "src/assets.rs"
[lints]
workspace = true

View File

@@ -1,7 +1,7 @@
// This crate was essentially pulled out verbatim from main `zed` crate to avoid having to run RustEmbed macro whenever zed has to be rebuilt. It saves a second or two on an incremental build.
use anyhow::anyhow;
use gpui::{AssetSource, Result, SharedString};
use gpui::{AppContext, AssetSource, Result, SharedString};
use rust_embed::RustEmbed;
#[derive(RustEmbed)]
@@ -34,3 +34,19 @@ impl AssetSource for Assets {
.collect())
}
}
impl Assets {
/// Populate the [`TextSystem`] of the given [`AppContext`] with all `.ttf` fonts in the `fonts` directory.
pub fn load_fonts(&self, cx: &AppContext) -> gpui::Result<()> {
let font_paths = self.list("fonts")?;
let mut embedded_fonts = Vec::new();
for font_path in font_paths {
if font_path.ends_with(".ttf") {
let font_bytes = cx.asset_source().load(&font_path)?;
embedded_fonts.push(font_bytes);
}
}
cx.text_system().add_fonts(embedded_fonts)
}
}

View File

@@ -128,6 +128,8 @@ impl LanguageModelRequestMessage {
Role::System => proto::LanguageModelRole::LanguageModelSystem,
} as i32,
content: self.content.clone(),
tool_calls: Vec::new(),
tool_call_id: None,
}
}
}
@@ -147,6 +149,8 @@ impl LanguageModelRequest {
messages: self.messages.iter().map(|m| m.to_proto()).collect(),
stop: self.stop.clone(),
temperature: self.temperature,
tool_choice: None,
tools: Vec::new(),
}
}
}

View File

@@ -1108,7 +1108,7 @@ impl AssistantPanel {
)
.track_scroll(scroll_handle)
.into_any_element();
saved_conversations.layout(
saved_conversations.prepaint_as_root(
bounds.origin,
bounds.size.map(AvailableSpace::Definite),
cx,
@@ -1119,8 +1119,8 @@ impl AssistantPanel {
)
.size_full()
.into_any_element()
} else {
let editor = self.active_conversation_editor().unwrap();
} else if let Some(editor) = self.active_conversation_editor() {
let editor = editor.clone();
let conversation = editor.read(cx).conversation.clone();
div()
.size_full()
@@ -1135,6 +1135,8 @@ impl AssistantPanel {
.children(self.render_remaining_tokens(&conversation, cx)),
)
.into_any_element()
} else {
div().into_any_element()
},
))
}
@@ -2871,7 +2873,7 @@ impl InlineAssistant {
cx.theme().colors().text
},
font_family: settings.ui_font.family.clone(),
font_features: settings.ui_font.features,
font_features: settings.ui_font.features.clone(),
font_size: rems(0.875).into(),
font_weight: FontWeight::NORMAL,
font_style: FontStyle::Normal,

View File

@@ -140,14 +140,24 @@ impl OpenAiCompletionProvider {
messages: request
.messages
.into_iter()
.map(|msg| RequestMessage {
role: msg.role.into(),
content: msg.content,
.map(|msg| match msg.role {
Role::User => RequestMessage::User {
content: msg.content,
},
Role::Assistant => RequestMessage::Assistant {
content: Some(msg.content),
tool_calls: Vec::new(),
},
Role::System => RequestMessage::System {
content: msg.content,
},
})
.collect(),
stream: true,
stop: request.stop,
temperature: request.temperature,
tools: Vec::new(),
tool_choice: None,
}
}
}
@@ -231,7 +241,7 @@ impl AuthenticationPrompt {
let text_style = TextStyle {
color: cx.theme().colors().text,
font_family: settings.ui_font.family.clone(),
font_features: settings.ui_font.features,
font_features: settings.ui_font.features.clone(),
font_size: rems(0.875).into(),
font_weight: FontWeight::NORMAL,
font_style: FontStyle::Normal,

View File

@@ -123,6 +123,8 @@ impl ZedDotDevCompletionProvider {
.collect(),
stop: request.stop,
temperature: request.temperature,
tools: Vec::new(),
tool_choice: None,
};
self.client

View File

@@ -0,0 +1,58 @@
[package]
name = "assistant2"
version = "0.1.0"
edition = "2021"
publish = false
license = "GPL-3.0-or-later"
[lib]
path = "src/assistant2.rs"
[[example]]
name = "assistant_example"
path = "examples/assistant_example.rs"
crate-type = ["bin"]
[dependencies]
anyhow.workspace = true
assistant_tooling.workspace = true
client.workspace = true
editor.workspace = true
feature_flags.workspace = true
fs.workspace = true
futures.workspace = true
gpui.workspace = true
language.workspace = true
log.workspace = true
nanoid.workspace = true
open_ai.workspace = true
project.workspace = true
rich_text.workspace = true
schemars.workspace = true
semantic_index.workspace = true
serde.workspace = true
serde_json.workspace = true
settings.workspace = true
theme.workspace = true
ui.workspace = true
util.workspace = true
workspace.workspace = true
[dev-dependencies]
assets.workspace = true
editor = { workspace = true, features = ["test-support"] }
env_logger.workspace = true
gpui = { workspace = true, features = ["test-support"] }
language = { workspace = true, features = ["test-support"] }
languages.workspace = true
node_runtime.workspace = true
project = { workspace = true, features = ["test-support"] }
rand.workspace = true
release_channel.workspace = true
settings = { workspace = true, features = ["test-support"] }
theme = { workspace = true, features = ["test-support"] }
util = { workspace = true, features = ["test-support"] }
workspace = { workspace = true, features = ["test-support"] }
[lints]
workspace = true

View File

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

View File

@@ -0,0 +1,129 @@
use anyhow::Context as _;
use assets::Assets;
use assistant2::{tools::ProjectIndexTool, AssistantPanel};
use assistant_tooling::ToolRegistry;
use client::Client;
use gpui::{actions, App, AppContext, KeyBinding, Task, View, WindowOptions};
use language::LanguageRegistry;
use project::Project;
use semantic_index::{OpenAiEmbeddingModel, OpenAiEmbeddingProvider, SemanticIndex};
use settings::{KeymapFile, DEFAULT_KEYMAP_PATH};
use std::{
path::{Path, PathBuf},
sync::Arc,
};
use theme::LoadThemes;
use ui::{div, prelude::*, Render};
use util::{http::HttpClientWithUrl, ResultExt as _};
actions!(example, [Quit]);
fn main() {
let args: Vec<String> = std::env::args().collect();
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();
});
if args.len() < 2 {
eprintln!(
"Usage: cargo run --example assistant_example -p assistant2 -- <project_path>"
);
cx.quit();
return;
}
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 node_runtime = node_runtime::RealNodeRuntime::new(client.http_client());
languages::init(language_registry.clone(), node_runtime, cx);
let http = Arc::new(HttpClientWithUrl::new("http://localhost:11434"));
let api_key = std::env::var("OPENAI_API_KEY").expect("OPENAI_API_KEY not set");
let embedding_provider = OpenAiEmbeddingProvider::new(
http.clone(),
OpenAiEmbeddingModel::TextEmbedding3Small,
open_ai::OPEN_AI_API_URL.to_string(),
api_key,
);
cx.spawn(|mut cx| async move {
let mut semantic_index = SemanticIndex::new(
PathBuf::from("/tmp/semantic-index-db.mdb"),
Arc::new(embedding_provider),
&mut cx,
)
.await?;
let project_path = Path::new(&args[1]);
let project = Project::example([project_path], &mut cx).await;
cx.update(|cx| {
let fs = project.read(cx).fs().clone();
let project_index = semantic_index.project_index(project.clone(), cx);
let mut tool_registry = ToolRegistry::new();
tool_registry
.register(ProjectIndexTool::new(project_index.clone(), fs.clone()))
.context("failed to register ProjectIndexTool")
.log_err();
let tool_registry = Arc::new(tool_registry);
cx.open_window(WindowOptions::default(), |cx| {
cx.new_view(|cx| Example::new(language_registry, tool_registry, 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>,
cx: &mut ViewContext<Self>,
) -> Self {
Self {
assistant_panel: cx
.new_view(|cx| AssistantPanel::new(language_registry, tool_registry, 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

@@ -0,0 +1,241 @@
//! 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;
use gpui::{actions, AnyElement, App, AppContext, KeyBinding, 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::sync::Arc;
use theme::LoadThemes;
use ui::{div, prelude::*, Render};
use util::ResultExt as _;
actions!(example, [Quit]);
struct RollDiceTool {}
impl RollDiceTool {
fn new() -> Self {
Self {}
}
}
#[derive(Serialize, Deserialize, JsonSchema, Clone)]
#[serde(rename_all = "snake_case")]
enum Die {
D6 = 6,
D20 = 20,
}
impl Die {
fn into_str(&self) -> &'static str {
match self {
Die::D6 => "d6",
Die::D20 => "d20",
}
}
}
#[derive(Serialize, Deserialize, JsonSchema, Clone)]
struct DiceParams {
/// The number of dice to roll.
num_dice: u8,
/// Which die to roll. Defaults to a d6 if not provided.
die_type: Option<Die>,
}
#[derive(Serialize, Deserialize)]
struct DieRoll {
die: Die,
roll: u8,
}
impl DieRoll {
fn render(&self) -> AnyElement {
match self.die {
Die::D6 => {
let face = match self.roll {
6 => div().child(""),
5 => div().child(""),
4 => div().child(""),
3 => div().child(""),
2 => div().child(""),
1 => div().child(""),
_ => div().child("😅"),
};
face.text_3xl().into_any_element()
}
_ => div()
.child(format!("{}", self.roll))
.text_3xl()
.into_any_element(),
}
}
}
#[derive(Serialize, Deserialize)]
struct DiceRoll {
rolls: Vec<DieRoll>,
}
pub struct DiceView {
result: Result<DiceRoll>,
}
impl Render for DiceView {
fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
let output = match &self.result {
Ok(output) => output,
Err(_) => return "Somehow dice failed 🎲".into_any_element(),
};
h_flex()
.children(
output
.rolls
.iter()
.map(|roll| div().p_2().child(roll.render())),
)
.into_any_element()
}
}
impl LanguageModelTool for RollDiceTool {
type Input = DiceParams;
type Output = DiceRoll;
type View = DiceView;
fn name(&self) -> String {
"roll_dice".to_string()
}
fn description(&self) -> String {
"Rolls N many dice and returns the results.".to_string()
}
fn execute(&self, input: &Self::Input, _cx: &AppContext) -> Task<gpui::Result<Self::Output>> {
let rolls = (0..input.num_dice)
.map(|_| {
let die_type = input.die_type.as_ref().unwrap_or(&Die::D6).clone();
DieRoll {
die: die_type.clone(),
roll: rand::thread_rng().gen_range(1..=die_type as u8),
}
})
.collect();
return Task::ready(Ok(DiceRoll { rolls }));
}
fn new_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
}
}
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 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 mut tool_registry = ToolRegistry::new();
tool_registry
.register(RollDiceTool::new())
.context("failed to register DummyTool")
.log_err();
let tool_registry = Arc::new(tool_registry);
println!("Tools registered");
for definition in tool_registry.definitions() {
println!("{}", definition);
}
cx.open_window(WindowOptions::default(), |cx| {
cx.new_view(|cx| Example::new(language_registry, tool_registry, 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>,
cx: &mut ViewContext<Self>,
) -> Self {
Self {
assistant_panel: cx
.new_view(|cx| AssistantPanel::new(language_registry, tool_registry, 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

@@ -0,0 +1,221 @@
//! This example creates a basic Chat UI for interacting with the filesystem.
use anyhow::{Context as _, Result};
use assets::Assets;
use assistant2::AssistantPanel;
use assistant_tooling::{LanguageModelTool, ToolRegistry};
use client::Client;
use fs::Fs;
use futures::StreamExt;
use gpui::{actions, App, AppContext, KeyBinding, Task, View, WindowOptions};
use language::LanguageRegistry;
use project::Project;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use settings::{KeymapFile, DEFAULT_KEYMAP_PATH};
use std::path::PathBuf;
use std::sync::Arc;
use theme::LoadThemes;
use ui::{div, prelude::*, Render};
use util::ResultExt as _;
actions!(example, [Quit]);
struct FileBrowserTool {
fs: Arc<dyn Fs>,
root_dir: PathBuf,
}
impl FileBrowserTool {
fn new(fs: Arc<dyn Fs>, root_dir: PathBuf) -> Self {
Self { fs, root_dir }
}
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
struct FileBrowserParams {
command: FileBrowserCommand,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
enum FileBrowserCommand {
Ls { path: PathBuf },
Cat { path: PathBuf },
}
#[derive(Serialize, Deserialize)]
enum FileBrowserOutput {
Ls { entries: Vec<String> },
Cat { content: String },
}
pub struct FileBrowserView {
result: Result<FileBrowserOutput>,
}
impl Render for FileBrowserView {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let Ok(output) = self.result.as_ref() else {
return h_flex().child("Failed to perform operation");
};
match output {
FileBrowserOutput::Ls { entries } => v_flex().children(
entries
.into_iter()
.map(|entry| h_flex().text_ui(cx).child(entry.clone())),
),
FileBrowserOutput::Cat { content } => h_flex().child(content.clone()),
}
}
}
impl LanguageModelTool for FileBrowserTool {
type Input = FileBrowserParams;
type Output = FileBrowserOutput;
type View = FileBrowserView;
fn name(&self) -> String {
"file_browser".to_string()
}
fn description(&self) -> String {
"A tool for browsing the filesystem.".to_string()
}
fn execute(&self, input: &Self::Input, cx: &AppContext) -> Task<gpui::Result<Self::Output>> {
cx.spawn({
let fs = self.fs.clone();
let root_dir = self.root_dir.clone();
let input = input.clone();
|_cx| async move {
match input.command {
FileBrowserCommand::Ls { path } => {
let path = root_dir.join(path);
let mut output = fs.read_dir(&path).await?;
let mut entries = Vec::new();
while let Some(entry) = output.next().await {
let entry = entry?;
entries.push(entry.display().to_string());
}
Ok(FileBrowserOutput::Ls { entries })
}
FileBrowserCommand::Cat { path } => {
let path = root_dir.join(path);
let output = fs.load(&path).await?;
Ok(FileBrowserOutput::Cat { content: output })
}
}
}
})
}
fn new_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 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");
let mut tool_registry = ToolRegistry::new();
tool_registry
.register(FileBrowserTool::new(fs, cwd))
.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.open_window(WindowOptions::default(), |cx| {
cx.new_view(|cx| Example::new(language_registry, tool_registry, 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>,
cx: &mut ViewContext<Self>,
) -> Self {
Self {
assistant_panel: cx
.new_view(|cx| AssistantPanel::new(language_registry, tool_registry, 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

@@ -0,0 +1,962 @@
mod assistant_settings;
mod completion_provider;
pub mod tools;
use anyhow::{Context, Result};
use assistant_tooling::{ToolFunctionCall, ToolRegistry};
use client::{proto, Client};
use completion_provider::*;
use editor::Editor;
use feature_flags::FeatureFlagAppExt as _;
use futures::{channel::oneshot, future::join_all, Future, FutureExt, StreamExt};
use gpui::{
list, prelude::*, AnyElement, AppContext, AsyncWindowContext, EventEmitter, FocusHandle,
FocusableView, Global, ListAlignment, ListState, Model, Render, Task, View, WeakView,
};
use language::{language_settings::SoftWrap, LanguageRegistry};
use open_ai::{FunctionContent, ToolCall, ToolCallContent};
use project::Fs;
use rich_text::RichText;
use semantic_index::{CloudEmbeddingProvider, ProjectIndex, SemanticIndex};
use serde::Deserialize;
use settings::Settings;
use std::{cmp, sync::Arc};
use theme::ThemeSettings;
use tools::ProjectIndexTool;
use ui::{popover_menu, prelude::*, ButtonLike, CollapsibleContainer, Color, ContextMenu, Tooltip};
use util::{paths::EMBEDDINGS_DIR, ResultExt};
use workspace::{
dock::{DockPosition, Panel, PanelEvent},
Workspace,
};
pub use assistant_settings::AssistantSettings;
const MAX_COMPLETION_CALLS_PER_SUBMISSION: usize = 5;
#[derive(Eq, PartialEq, Copy, Clone, Deserialize)]
pub struct Submit(SubmitMode);
/// There are multiple different ways to submit a model request, represented by this enum.
#[derive(Eq, PartialEq, Copy, Clone, Deserialize)]
pub enum SubmitMode {
/// Only include the conversation.
Simple,
/// Send the current file as context.
CurrentFile,
/// Search the codebase and send relevant excerpts.
Codebase,
}
gpui::actions!(assistant2, [Cancel, ToggleFocus]);
gpui::impl_actions!(assistant2, [Submit]);
pub fn init(client: Arc<Client>, cx: &mut AppContext) {
AssistantSettings::register(cx);
cx.spawn(|mut cx| {
let client = client.clone();
async move {
let embedding_provider = CloudEmbeddingProvider::new(client.clone());
let semantic_index = SemanticIndex::new(
EMBEDDINGS_DIR.join("semantic-index-db.0.mdb"),
Arc::new(embedding_provider),
&mut cx,
)
.await?;
cx.update(|cx| cx.set_global(semantic_index))
}
})
.detach();
cx.set_global(CompletionProvider::new(CloudCompletionProvider::new(
client,
)));
cx.observe_new_views(
|workspace: &mut Workspace, _cx: &mut ViewContext<Workspace>| {
workspace.register_action(|workspace, _: &ToggleFocus, cx| {
workspace.toggle_panel_focus::<AssistantPanel>(cx);
});
},
)
.detach();
}
pub fn enabled(cx: &AppContext) -> bool {
cx.is_staff()
}
pub struct AssistantPanel {
chat: View<AssistantChat>,
width: Option<Pixels>,
}
impl AssistantPanel {
pub fn load(
workspace: WeakView<Workspace>,
cx: AsyncWindowContext,
) -> Task<Result<View<Self>>> {
cx.spawn(|mut cx| async move {
let (app_state, project) = workspace.update(&mut cx, |workspace, _| {
(workspace.app_state().clone(), workspace.project().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(),
))
.context("failed to register ProjectIndexTool")
.log_err();
let tool_registry = Arc::new(tool_registry);
Self::new(app_state.languages.clone(), tool_registry, cx)
})
})
}
pub fn new(
language_registry: Arc<LanguageRegistry>,
tool_registry: Arc<ToolRegistry>,
cx: &mut ViewContext<Self>,
) -> Self {
let chat = cx.new_view(|cx| {
AssistantChat::new(language_registry.clone(), tool_registry.clone(), cx)
});
Self { width: None, chat }
}
}
impl Render for AssistantPanel {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
div()
.size_full()
.v_flex()
.p_2()
.bg(cx.theme().colors().background)
.child(self.chat.clone())
}
}
impl Panel for AssistantPanel {
fn persistent_name() -> &'static str {
"AssistantPanelv2"
}
fn position(&self, _cx: &WindowContext) -> workspace::dock::DockPosition {
// todo!("Add a setting / use assistant settings")
DockPosition::Right
}
fn position_is_valid(&self, position: workspace::dock::DockPosition) -> bool {
matches!(position, DockPosition::Right)
}
fn set_position(&mut self, _: workspace::dock::DockPosition, _: &mut ViewContext<Self>) {
// Do nothing until we have a setting for this
}
fn size(&self, _cx: &WindowContext) -> Pixels {
self.width.unwrap_or(px(400.))
}
fn set_size(&mut self, size: Option<Pixels>, cx: &mut ViewContext<Self>) {
self.width = size;
cx.notify();
}
fn icon(&self, _cx: &WindowContext) -> Option<ui::IconName> {
Some(IconName::Ai)
}
fn icon_tooltip(&self, _: &WindowContext) -> Option<&'static str> {
Some("Assistant Panel ✨")
}
fn toggle_action(&self) -> Box<dyn gpui::Action> {
Box::new(ToggleFocus)
}
}
impl EventEmitter<PanelEvent> for AssistantPanel {}
impl FocusableView for AssistantPanel {
fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
self.chat
.read(cx)
.messages
.iter()
.rev()
.find_map(|msg| msg.focus_handle(cx))
.expect("no user message in chat")
}
}
struct AssistantChat {
model: String,
messages: Vec<ChatMessage>,
list_state: ListState,
language_registry: Arc<LanguageRegistry>,
next_message_id: MessageId,
pending_completion: Option<Task<()>>,
tool_registry: Arc<ToolRegistry>,
}
impl AssistantChat {
fn new(
language_registry: Arc<LanguageRegistry>,
tool_registry: Arc<ToolRegistry>,
cx: &mut ViewContext<Self>,
) -> Self {
let model = CompletionProvider::get(cx).default_model();
let view = cx.view().downgrade();
let list_state = ListState::new(
0,
ListAlignment::Bottom,
px(1024.),
move |ix, cx: &mut WindowContext| {
view.update(cx, |this, cx| this.render_message(ix, cx))
.unwrap()
},
);
let mut this = Self {
model,
messages: Vec::new(),
list_state,
language_registry,
next_message_id: MessageId(0),
pending_completion: None,
tool_registry,
};
this.push_new_user_message(true, cx);
this
}
fn focused_message_id(&self, cx: &WindowContext) -> Option<MessageId> {
self.messages.iter().find_map(|message| match message {
ChatMessage::User(message) => message
.body
.focus_handle(cx)
.contains_focused(cx)
.then_some(message.id),
ChatMessage::Assistant(_) => None,
})
}
fn cancel(&mut self, _: &Cancel, cx: &mut ViewContext<Self>) {
if self.pending_completion.take().is_none() {
cx.propagate();
return;
}
if let Some(ChatMessage::Assistant(message)) = self.messages.last() {
if message.body.text.is_empty() {
self.pop_message(cx);
} else {
self.push_new_user_message(false, cx);
}
}
}
fn submit(&mut self, Submit(mode): &Submit, cx: &mut ViewContext<Self>) {
let Some(focused_message_id) = self.focused_message_id(cx) else {
log::error!("unexpected state: no user message editor is focused.");
return;
};
self.truncate_messages(focused_message_id, cx);
let mode = *mode;
self.pending_completion = Some(cx.spawn(move |this, mut cx| async move {
Self::request_completion(
this.clone(),
mode,
MAX_COMPLETION_CALLS_PER_SUBMISSION,
&mut cx,
)
.await
.log_err();
this.update(&mut cx, |this, cx| {
let focus = this
.user_message(focused_message_id)
.body
.focus_handle(cx)
.contains_focused(cx);
this.push_new_user_message(focus, cx);
this.pending_completion = None;
})
.context("Failed to push new user message")
.log_err();
}));
}
async fn request_completion(
this: WeakView<Self>,
mode: SubmitMode,
limit: usize,
cx: &mut AsyncWindowContext,
) -> Result<()> {
let mut call_count = 0;
loop {
let complete = async {
let completion = this.update(cx, |this, cx| {
this.push_new_assistant_message(cx);
let definitions = if call_count < limit
&& matches!(mode, SubmitMode::Codebase | SubmitMode::Simple)
{
this.tool_registry.definitions()
} else {
&[]
};
call_count += 1;
let messages = this.completion_messages(cx);
CompletionProvider::get(cx).complete(
this.model.clone(),
messages,
Vec::new(),
1.0,
definitions,
)
});
let mut stream = completion?.await?;
let mut body = String::new();
while let Some(delta) = stream.next().await {
let delta = delta?;
this.update(cx, |this, cx| {
if let Some(ChatMessage::Assistant(AssistantMessage {
body: message_body,
tool_calls: message_tool_calls,
..
})) = this.messages.last_mut()
{
if let Some(content) = &delta.content {
body.push_str(content);
}
for tool_call in delta.tool_calls {
let index = tool_call.index as usize;
if index >= message_tool_calls.len() {
message_tool_calls.resize_with(index + 1, Default::default);
}
let call = &mut message_tool_calls[index];
if let Some(id) = &tool_call.id {
call.id.push_str(id);
}
match tool_call.variant {
Some(proto::tool_call_delta::Variant::Function(tool_call)) => {
if let Some(name) = &tool_call.name {
call.name.push_str(name);
}
if let Some(arguments) = &tool_call.arguments {
call.arguments.push_str(arguments);
}
}
None => {}
}
}
*message_body =
RichText::new(body.clone(), &[], &this.language_registry);
cx.notify();
} else {
unreachable!()
}
})?;
}
anyhow::Ok(())
}
.await;
let mut tool_tasks = Vec::new();
this.update(cx, |this, cx| {
if let Some(ChatMessage::Assistant(AssistantMessage {
error: message_error,
tool_calls,
..
})) = this.messages.last_mut()
{
if let Err(error) = complete {
message_error.replace(SharedString::from(error.to_string()));
cx.notify();
} else {
for tool_call in tool_calls.iter() {
tool_tasks.push(this.tool_registry.call(tool_call, cx));
}
}
}
})?;
if tool_tasks.is_empty() {
return Ok(());
}
let tools = join_all(tool_tasks.into_iter()).await;
// If the WindowContext went away for any tool's view we don't include it
// especially since the below call would fail for the same reason.
let tools = tools.into_iter().filter_map(|tool| tool.ok()).collect();
this.update(cx, |this, cx| {
if let Some(ChatMessage::Assistant(AssistantMessage { tool_calls, .. })) =
this.messages.last_mut()
{
*tool_calls = tools;
cx.notify();
}
})?;
}
}
fn user_message(&mut self, message_id: MessageId) -> &mut UserMessage {
self.messages
.iter_mut()
.find_map(|message| match message {
ChatMessage::User(user_message) if user_message.id == message_id => {
Some(user_message)
}
_ => None,
})
.expect("User message not found")
}
fn push_new_user_message(&mut self, focus: bool, cx: &mut ViewContext<Self>) {
let id = self.next_message_id.post_inc();
let body = cx.new_view(|cx| {
let mut editor = Editor::auto_height(80, cx);
editor.set_soft_wrap_mode(SoftWrap::EditorWidth, cx);
if focus {
cx.focus_self();
}
editor
});
let message = ChatMessage::User(UserMessage {
id,
body,
contexts: Vec::new(),
});
self.push_message(message, cx);
}
fn push_new_assistant_message(&mut self, cx: &mut ViewContext<Self>) {
let message = ChatMessage::Assistant(AssistantMessage {
id: self.next_message_id.post_inc(),
body: RichText::default(),
tool_calls: Vec::new(),
error: None,
});
self.push_message(message, cx);
}
fn push_message(&mut self, message: ChatMessage, cx: &mut ViewContext<Self>) {
let old_len = self.messages.len();
let focus_handle = Some(message.focus_handle(cx));
self.messages.push(message);
self.list_state
.splice_focusable(old_len..old_len, focus_handle);
cx.notify();
}
fn pop_message(&mut self, cx: &mut ViewContext<Self>) {
if self.messages.is_empty() {
return;
}
self.messages.pop();
self.list_state
.splice(self.messages.len()..self.messages.len() + 1, 0);
cx.notify();
}
fn truncate_messages(&mut self, last_message_id: MessageId, cx: &mut ViewContext<Self>) {
if let Some(index) = self.messages.iter().position(|message| match message {
ChatMessage::User(message) => message.id == last_message_id,
ChatMessage::Assistant(message) => message.id == last_message_id,
}) {
self.list_state.splice(index + 1..self.messages.len(), 0);
self.messages.truncate(index + 1);
cx.notify();
}
}
fn render_error(
&self,
error: Option<SharedString>,
_ix: usize,
cx: &mut ViewContext<Self>,
) -> AnyElement {
let theme = cx.theme();
if let Some(error) = error {
div()
.py_1()
.px_2()
.neg_mx_1()
.rounded_md()
.border()
.border_color(theme.status().error_border)
// .bg(theme.status().error_background)
.text_color(theme.status().error)
.child(error.clone())
.into_any_element()
} else {
div().into_any_element()
}
}
fn render_message(&self, ix: usize, cx: &mut ViewContext<Self>) -> AnyElement {
let is_last = ix == self.messages.len() - 1;
match &self.messages[ix] {
ChatMessage::User(UserMessage {
body,
contexts: _contexts,
..
}) => div()
.when(!is_last, |element| element.mb_2())
.child(div().p_2().child(Label::new("You").color(Color::Default)))
.child(
div()
.on_action(cx.listener(Self::submit))
.p_2()
.text_color(cx.theme().colors().editor_foreground)
.font(ThemeSettings::get_global(cx).buffer_font.clone())
.bg(cx.theme().colors().editor_background)
.child(body.clone()), // .children(contexts.iter().map(|context| context.render(cx))),
)
.into_any(),
ChatMessage::Assistant(AssistantMessage {
id,
body,
error,
tool_calls,
..
}) => {
let assistant_body = if body.text.is_empty() && !tool_calls.is_empty() {
div()
} else {
div().p_2().child(body.element(ElementId::from(id.0), cx))
};
div()
.when(!is_last, |element| element.mb_2())
.child(
div()
.p_2()
.child(Label::new("Assistant").color(Color::Modified)),
)
.child(assistant_body)
.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> {
let mut completion_messages = Vec::new();
for message in &self.messages {
match message {
ChatMessage::User(UserMessage { body, contexts, .. }) => {
// setup context for model
contexts.iter().for_each(|context| {
completion_messages.extend(context.completion_messages(cx))
});
// Show user's message last so that the assistant is grounded in the user's request
completion_messages.push(CompletionMessage::User {
content: body.read(cx).text(cx),
});
}
ChatMessage::Assistant(AssistantMessage {
body, tool_calls, ..
}) => {
// In no case do we want to send an empty message. This shouldn't happen, but we might as well
// not break the Chat API if it does.
if body.text.is_empty() && tool_calls.is_empty() {
continue;
}
let tool_calls_from_assistant = tool_calls
.iter()
.map(|tool_call| ToolCall {
content: ToolCallContent::Function {
function: FunctionContent {
name: tool_call.name.clone(),
arguments: tool_call.arguments.clone(),
},
},
id: tool_call.id.clone(),
})
.collect();
completion_messages.push(CompletionMessage::Assistant {
content: Some(body.text.to_string()),
tool_calls: tool_calls_from_assistant,
});
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
let content = match &tool_call.result {
Some(result) => result.format(&tool_call.name),
None => "".to_string(),
};
completion_messages.push(CompletionMessage::Tool {
content,
tool_call_id: tool_call.id.clone(),
});
}
}
}
}
completion_messages
}
fn render_model_dropdown(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let this = cx.view().downgrade();
div().h_flex().justify_end().child(
div().w_32().child(
popover_menu("user-menu")
.menu(move |cx| {
ContextMenu::build(cx, |mut menu, cx| {
for model in CompletionProvider::get(cx).available_models() {
menu = menu.custom_entry(
{
let model = model.clone();
move |_| Label::new(model.clone()).into_any_element()
},
{
let this = this.clone();
move |cx| {
_ = this.update(cx, |this, cx| {
this.model = model.clone();
cx.notify();
});
}
},
);
}
menu
})
.into()
})
.trigger(
ButtonLike::new("active-model")
.child(
h_flex()
.w_full()
.gap_0p5()
.child(
div()
.overflow_x_hidden()
.flex_grow()
.whitespace_nowrap()
.child(Label::new(self.model.clone())),
)
.child(div().child(
Icon::new(IconName::ChevronDown).color(Color::Muted),
)),
)
.style(ButtonStyle::Subtle)
.tooltip(move |cx| Tooltip::text("Change Model", cx)),
)
.anchor(gpui::AnchorCorner::TopRight),
),
)
}
}
impl Render for AssistantChat {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
div()
.relative()
.flex_1()
.v_flex()
.key_context("AssistantChat")
.on_action(cx.listener(Self::cancel))
.text_color(Color::Default.color(cx))
.child(self.render_model_dropdown(cx))
.child(list(self.list_state.clone()).flex_1())
}
}
#[derive(Copy, Clone, Eq, PartialEq)]
struct MessageId(usize);
impl MessageId {
fn post_inc(&mut self) -> Self {
let id = *self;
self.0 += 1;
id
}
}
enum ChatMessage {
User(UserMessage),
Assistant(AssistantMessage),
}
impl ChatMessage {
fn focus_handle(&self, cx: &AppContext) -> Option<FocusHandle> {
match self {
ChatMessage::User(UserMessage { body, .. }) => Some(body.focus_handle(cx)),
ChatMessage::Assistant(_) => None,
}
}
}
struct UserMessage {
id: MessageId,
body: View<Editor>,
contexts: Vec<AssistantContext>,
}
struct AssistantMessage {
id: MessageId,
body: RichText,
tool_calls: Vec<ToolFunctionCall>,
error: Option<SharedString>,
}
// Since we're swapping out for direct query usage, we might not need to use this injected context
// It will be useful though for when the user _definitely_ wants the model to see a specific file,
// query, error, etc.
#[allow(dead_code)]
enum AssistantContext {
Codebase(View<CodebaseContext>),
}
#[allow(dead_code)]
struct CodebaseExcerpt {
element_id: ElementId,
path: SharedString,
text: SharedString,
score: f32,
expanded: bool,
}
impl AssistantContext {
#[allow(dead_code)]
fn render(&self, _cx: &mut ViewContext<AssistantChat>) -> AnyElement {
match self {
AssistantContext::Codebase(context) => context.clone().into_any_element(),
}
}
fn completion_messages(&self, cx: &WindowContext) -> Vec<CompletionMessage> {
match self {
AssistantContext::Codebase(context) => context.read(cx).completion_messages(),
}
}
}
enum CodebaseContext {
Pending { _task: Task<()> },
Done(Result<Vec<CodebaseExcerpt>>),
}
impl CodebaseContext {
fn toggle_expanded(&mut self, element_id: ElementId, cx: &mut ViewContext<Self>) {
if let CodebaseContext::Done(Ok(excerpts)) = self {
if let Some(excerpt) = excerpts
.iter_mut()
.find(|excerpt| excerpt.element_id == element_id)
{
excerpt.expanded = !excerpt.expanded;
cx.notify();
}
}
}
}
impl Render for CodebaseContext {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
match self {
CodebaseContext::Pending { .. } => div()
.h_flex()
.items_center()
.gap_1()
.child(Icon::new(IconName::Ai).color(Color::Muted).into_element())
.child("Searching codebase..."),
CodebaseContext::Done(Ok(excerpts)) => {
div()
.v_flex()
.gap_2()
.children(excerpts.iter().map(|excerpt| {
let expanded = excerpt.expanded;
let element_id = excerpt.element_id.clone();
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(), // todo!(): Show as an editor block
),
)
}))
}
CodebaseContext::Done(Err(error)) => div().child(error.to_string()),
}
}
}
impl CodebaseContext {
#[allow(dead_code)]
fn new(
query: impl 'static + Future<Output = Result<String>>,
populated: oneshot::Sender<bool>,
project_index: Model<ProjectIndex>,
fs: Arc<dyn Fs>,
cx: &mut ViewContext<Self>,
) -> Self {
let query = query.boxed_local();
let _task = cx.spawn(|this, mut cx| async move {
let result = async {
let query = query.await?;
let results = this
.update(&mut cx, |_this, cx| {
project_index.read(cx).search(&query, 16, cx)
})?
.await;
let excerpts = results.into_iter().map(|result| {
let abs_path = result
.worktree
.read_with(&cx, |worktree, _| worktree.abs_path().join(&result.path));
let fs = fs.clone();
async move {
let path = result.path.clone();
let text = fs.load(&abs_path?).await?;
// todo!("what should we do with stale ranges?");
let range = cmp::min(result.range.start, text.len())
..cmp::min(result.range.end, text.len());
let text = SharedString::from(text[range].to_string());
anyhow::Ok(CodebaseExcerpt {
element_id: ElementId::Name(nanoid::nanoid!().into()),
path: path.to_string_lossy().to_string().into(),
text,
score: result.score,
expanded: false,
})
}
});
anyhow::Ok(
futures::future::join_all(excerpts)
.await
.into_iter()
.filter_map(|result| result.log_err())
.collect(),
)
}
.await;
this.update(&mut cx, |this, cx| {
this.populate(result, populated, cx);
})
.ok();
});
Self::Pending { _task }
}
#[allow(dead_code)]
fn populate(
&mut self,
result: Result<Vec<CodebaseExcerpt>>,
populated: oneshot::Sender<bool>,
cx: &mut ViewContext<Self>,
) {
let success = result.is_ok();
*self = Self::Done(result);
populated.send(success).ok();
cx.notify();
}
fn completion_messages(&self) -> Vec<CompletionMessage> {
// One system message for the whole batch of excerpts:
// Semantic search results for user query:
//
// Excerpt from $path:
// ~~~
// `text`
// ~~~
//
// Excerpt from $path:
match self {
CodebaseContext::Done(Ok(excerpts)) => {
if excerpts.is_empty() {
return Vec::new();
}
let mut body = "Semantic search results for user query:\n".to_string();
for excerpt in 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");
}
vec![CompletionMessage::System { content: body }]
}
_ => vec![],
}
}
}

View File

@@ -0,0 +1,26 @@
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use settings::{Settings, SettingsSources};
#[derive(Default, Debug, Deserialize, Serialize, Clone)]
pub struct AssistantSettings {
pub enabled: bool,
}
#[derive(Default, Debug, Deserialize, Serialize, Clone, JsonSchema)]
pub struct AssistantSettingsContent {
pub enabled: Option<bool>,
}
impl Settings for AssistantSettings {
const KEY: Option<&'static str> = Some("assistant_v2");
type FileContent = AssistantSettingsContent;
fn load(
sources: SettingsSources<Self::FileContent>,
_: &mut gpui::AppContext,
) -> anyhow::Result<Self> {
Ok(sources.json_merge().unwrap_or_else(|_| Default::default()))
}
}

View File

@@ -0,0 +1,179 @@
use anyhow::Result;
use assistant_tooling::ToolFunctionDefinition;
use client::{proto, Client};
use futures::{future::BoxFuture, stream::BoxStream, FutureExt, StreamExt};
use gpui::Global;
use std::sync::Arc;
pub use open_ai::RequestMessage as CompletionMessage;
#[derive(Clone)]
pub struct CompletionProvider(Arc<dyn CompletionProviderBackend>);
impl CompletionProvider {
pub fn new(backend: impl CompletionProviderBackend) -> Self {
Self(Arc::new(backend))
}
pub fn default_model(&self) -> String {
self.0.default_model()
}
pub fn available_models(&self) -> Vec<String> {
self.0.available_models()
}
pub fn complete(
&self,
model: String,
messages: Vec<CompletionMessage>,
stop: Vec<String>,
temperature: f32,
tools: &[ToolFunctionDefinition],
) -> BoxFuture<'static, Result<BoxStream<'static, Result<proto::LanguageModelResponseMessage>>>>
{
self.0.complete(model, messages, stop, temperature, tools)
}
}
impl Global for CompletionProvider {}
pub trait CompletionProviderBackend: 'static {
fn default_model(&self) -> String;
fn available_models(&self) -> Vec<String>;
fn complete(
&self,
model: String,
messages: Vec<CompletionMessage>,
stop: Vec<String>,
temperature: f32,
tools: &[ToolFunctionDefinition],
) -> BoxFuture<'static, Result<BoxStream<'static, Result<proto::LanguageModelResponseMessage>>>>;
}
pub struct CloudCompletionProvider {
client: Arc<Client>,
}
impl CloudCompletionProvider {
pub fn new(client: Arc<Client>) -> Self {
Self { client }
}
}
impl CompletionProviderBackend for CloudCompletionProvider {
fn default_model(&self) -> String {
"gpt-4-turbo".into()
}
fn available_models(&self) -> Vec<String> {
vec!["gpt-4-turbo".into(), "gpt-4".into(), "gpt-3.5-turbo".into()]
}
fn complete(
&self,
model: String,
messages: Vec<CompletionMessage>,
stop: Vec<String>,
temperature: f32,
tools: &[ToolFunctionDefinition],
) -> BoxFuture<'static, Result<BoxStream<'static, Result<proto::LanguageModelResponseMessage>>>>
{
let client = self.client.clone();
let tools: Vec<proto::ChatCompletionTool> = tools
.iter()
.filter_map(|tool| {
Some(proto::ChatCompletionTool {
variant: Some(proto::chat_completion_tool::Variant::Function(
proto::chat_completion_tool::FunctionObject {
name: tool.name.clone(),
description: Some(tool.description.clone()),
parameters: Some(serde_json::to_string(&tool.parameters).ok()?),
},
)),
})
})
.collect();
let tool_choice = match tools.is_empty() {
true => None,
false => Some("auto".into()),
};
async move {
let stream = client
.request_stream(proto::CompleteWithLanguageModel {
model,
messages: messages
.into_iter()
.map(|message| match message {
CompletionMessage::Assistant {
content,
tool_calls,
} => proto::LanguageModelRequestMessage {
role: proto::LanguageModelRole::LanguageModelAssistant as i32,
content: content.unwrap_or_default(),
tool_call_id: None,
tool_calls: tool_calls
.into_iter()
.map(|tool_call| match tool_call.content {
open_ai::ToolCallContent::Function { function } => {
proto::ToolCall {
id: tool_call.id,
variant: Some(proto::tool_call::Variant::Function(
proto::tool_call::FunctionCall {
name: function.name,
arguments: function.arguments,
},
)),
}
}
})
.collect(),
},
CompletionMessage::User { content } => {
proto::LanguageModelRequestMessage {
role: proto::LanguageModelRole::LanguageModelUser as i32,
content,
tool_call_id: None,
tool_calls: Vec::new(),
}
}
CompletionMessage::System { content } => {
proto::LanguageModelRequestMessage {
role: proto::LanguageModelRole::LanguageModelSystem as i32,
content,
tool_calls: Vec::new(),
tool_call_id: None,
}
}
CompletionMessage::Tool {
content,
tool_call_id,
} => proto::LanguageModelRequestMessage {
role: proto::LanguageModelRole::LanguageModelTool as i32,
content,
tool_call_id: Some(tool_call_id),
tool_calls: Vec::new(),
},
})
.collect(),
stop,
temperature,
tool_choice,
tools,
})
.await?;
Ok(stream
.filter_map(|response| async move {
match response {
Ok(mut response) => Some(Ok(response.choices.pop()?.delta?)),
Err(error) => Some(Err(error)),
}
})
.boxed())
}
.boxed()
}
}

View File

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

View File

@@ -0,0 +1,22 @@
[package]
name = "assistant_tooling"
version = "0.1.0"
edition = "2021"
publish = false
license = "GPL-3.0-or-later"
[lints]
workspace = true
[lib]
path = "src/assistant_tooling.rs"
[dependencies]
anyhow.workspace = true
gpui.workspace = true
schemars.workspace = true
serde.workspace = true
serde_json.workspace = true
[dev-dependencies]
gpui = { workspace = true, features = ["test-support"] }

View File

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

View File

@@ -0,0 +1,208 @@
# Assistant Tooling
Bringing OpenAI compatible tool calling to GPUI.
This unlocks:
- **Structured Extraction** of model responses
- **Validation** of model inputs
- **Execution** of chosen toolsn
## Overview
Language Models can produce structured outputs that are perfect for calling functions. The most famous of these is OpenAI's tool calling. When make a chat completion you can pass a list of tools available to the model. The model will choose `0..n` tools to help them complete a user's task. It's up to _you_ to create the tools that the model can call.
> **User**: "Hey I need help with implementing a collapsible panel in GPUI"
>
> **Assistant**: "Sure, I can help with that. Let me see what I can find."
>
> `tool_calls: ["name": "query_codebase", arguments: "{ 'query': 'GPUI collapsible panel' }"]`
>
> `result: "['crates/gpui/src/panel.rs:12: impl Panel { ... }', 'crates/gpui/src/panel.rs:20: impl Panel { ... }']"`
>
> **Assistant**: "Here are some excerpts from the GPUI codebase that might help you."
This library is designed to facilitate this interaction mode by allowing you to go from `struct` to `tool` with a simple trait, `LanguageModelTool`.
## Example
Let's expose querying a semantic index directly by the model. First, we'll set up some _necessary_ imports
```rust
use anyhow::Result;
use assistant_tooling::{LanguageModelTool, ToolRegistry};
use gpui::{App, AppContext, Task};
use schemars::JsonSchema;
use serde::Deserialize;
use serde_json::json;
```
Then we'll define the query structure the model must fill in. This _must_ derive `Deserialize` from `serde` and `JsonSchema` from the `schemars` crate.
```rust
#[derive(Deserialize, JsonSchema)]
struct CodebaseQuery {
query: String,
}
```
After that we can define our tool, with the expectation that it will need a `ProjectIndex` to search against. For this example, the index uses the same interface as `semantic_index::ProjectIndex`.
```rust
struct ProjectIndex {}
impl ProjectIndex {
fn new() -> Self {
ProjectIndex {}
}
fn search(&self, _query: &str, _limit: usize, _cx: &AppContext) -> Task<Result<Vec<String>>> {
// Instead of hooking up a real index, we're going to fake it
if _query.contains("gpui") {
return Task::ready(Ok(vec![r#"// crates/gpui/src/gpui.rs
//! # Welcome to GPUI!
//!
//! GPUI is a hybrid immediate and retained mode, GPU accelerated, UI framework
//! for Rust, designed to support a wide variety of applications
"#
.to_string()]));
}
return Task::ready(Ok(vec![]));
}
}
struct ProjectIndexTool {
project_index: ProjectIndex,
}
```
Now we can implement the `LanguageModelTool` trait for our tool by:
- Defining the `Input` from the model, which is `CodebaseQuery`
- Defining the `Output`
- Implementing the `name` and `description` functions to provide the model information when it's choosing a tool
- Implementing the `execute` function to run the tool
```rust
impl LanguageModelTool for ProjectIndexTool {
type Input = CodebaseQuery;
type Output = String;
fn name(&self) -> String {
"query_codebase".to_string()
}
fn description(&self) -> String {
"Executes a query against the codebase, returning excerpts related to the query".to_string()
}
fn execute(&self, query: Self::Input, cx: &AppContext) -> Task<Result<Self::Output>> {
let results = self.project_index.search(query.query.as_str(), 10, cx);
cx.spawn(|_cx| async move {
let results = results.await?;
if !results.is_empty() {
Ok(results.join("\n"))
} else {
Ok("No results".to_string())
}
})
}
}
```
For the sake of this example, let's look at the types that OpenAI will be passing to us
```rust
// OpenAI definitions, shown here for demonstration
#[derive(Deserialize)]
struct FunctionCall {
name: String,
args: String,
}
#[derive(Deserialize, Eq, PartialEq)]
enum ToolCallType {
#[serde(rename = "function")]
Function,
Other,
}
#[derive(Deserialize, Clone, Debug, Eq, PartialEq, Hash, Ord, PartialOrd)]
struct ToolCallId(String);
#[derive(Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
enum ToolCall {
Function {
#[allow(dead_code)]
id: ToolCallId,
function: FunctionCall,
},
Other {
#[allow(dead_code)]
id: ToolCallId,
},
}
#[derive(Deserialize)]
struct AssistantMessage {
role: String,
content: Option<String>,
tool_calls: Option<Vec<ToolCall>>,
}
```
When the model wants to call tools, it will pass a list of `ToolCall`s. When those are `function`s that we can handle, we'll pass them to our `ToolRegistry` to get a future that we can await.
```rust
// Inside `fn main()`
App::new().run(|cx: &mut AppContext| {
let tool = ProjectIndexTool {
project_index: ProjectIndex::new(),
};
let mut registry = ToolRegistry::new();
let registered = registry.register(tool);
assert!(registered.is_ok());
```
Let's pretend the model sent us back a message requesting
```rust
let model_response = json!({
"role": "assistant",
"tool_calls": [
{
"id": "call_1",
"function": {
"name": "query_codebase",
"args": r#"{"query":"GPUI Task background_executor"}"#
},
"type": "function"
}
]
});
let message: AssistantMessage = serde_json::from_value(model_response).unwrap();
// We know there's a tool call, so let's skip straight to it for this example
let tool_calls = message.tool_calls.as_ref().unwrap();
let tool_call = tool_calls.get(0).unwrap();
```
We can now use our registry to call the tool.
```rust
let task = registry.call(
tool_call.name,
tool_call.args,
);
cx.spawn(|_cx| async move {
let result = task.await?;
println!("{}", result.unwrap());
Ok(())
})
```

View File

@@ -0,0 +1,5 @@
pub mod registry;
pub mod tool;
pub use crate::registry::ToolRegistry;
pub use crate::tool::{LanguageModelTool, ToolFunctionCall, ToolFunctionDefinition};

View File

@@ -0,0 +1,283 @@
use anyhow::{anyhow, Result};
use gpui::{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>,
}
impl ToolRegistry {
pub fn new() -> Self {
Self {
tools: HashMap::new(),
definitions: Vec::new(),
}
}
pub fn definitions(&self) -> &[ToolFunctionDefinition] {
&self.definitions
}
pub fn register<T: 'static + LanguageModelTool>(&mut self, tool: T) -> Result<()> {
self.definitions.push(tool.definition());
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::new_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)
}
}
#[cfg(test)]
mod test {
use super::*;
use gpui::View;
use gpui::{div, prelude::*, Render, TestAppContext};
use schemars::schema_for;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use serde_json::json;
#[derive(Deserialize, Serialize, JsonSchema)]
struct WeatherQuery {
location: String,
unit: String,
}
struct WeatherTool {
current_weather: WeatherResult,
}
#[derive(Clone, Serialize, Deserialize, PartialEq, Debug)]
struct WeatherResult {
location: String,
temperature: f64,
unit: String,
}
struct WeatherView {
result: WeatherResult,
}
impl Render for WeatherView {
fn render(&mut self, _cx: &mut gpui::ViewContext<Self>) -> impl IntoElement {
div().child(format!("temperature: {}", self.result.temperature))
}
}
impl LanguageModelTool for WeatherTool {
type Input = WeatherQuery;
type Output = WeatherResult;
type View = WeatherView;
fn name(&self) -> String {
"get_current_weather".to_string()
}
fn description(&self) -> String {
"Fetches the current weather for a given location.".to_string()
}
fn execute(
&self,
input: &Self::Input,
_cx: &gpui::AppContext,
) -> Task<Result<Self::Output>> {
let _location = input.location.clone();
let _unit = input.unit.clone();
let weather = self.current_weather.clone();
Task::ready(Ok(weather))
}
fn new_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_function_registry(cx: &mut TestAppContext) {
cx.background_executor.run_until_parked();
let mut registry = ToolRegistry::new();
let tool = WeatherTool {
current_weather: WeatherResult {
location: "San Francisco".to_string(),
temperature: 21.0,
unit: "Celsius".to_string(),
},
};
registry.register(tool).unwrap();
// let _result = cx
// .update(|cx| {
// registry.call(
// &ToolFunctionCall {
// name: "get_current_weather".to_string(),
// arguments: r#"{ "location": "San Francisco", "unit": "Celsius" }"#
// .to_string(),
// id: "test-123".to_string(),
// result: None,
// },
// cx,
// )
// })
// .await;
// assert!(result.is_ok());
// let result = result.unwrap();
// let expected = r#"{"location":"San Francisco","temperature":21.0,"unit":"Celsius"}"#;
// todo!(): Put this back in after the interface is stabilized
// assert_eq!(result, expected);
}
#[gpui::test]
async fn test_openai_weather_example(cx: &mut TestAppContext) {
cx.background_executor.run_until_parked();
let tool = WeatherTool {
current_weather: WeatherResult {
location: "San Francisco".to_string(),
temperature: 21.0,
unit: "Celsius".to_string(),
},
};
let tools = vec![tool.definition()];
assert_eq!(tools.len(), 1);
let expected = ToolFunctionDefinition {
name: "get_current_weather".to_string(),
description: "Fetches the current weather for a given location.".to_string(),
parameters: schema_for!(WeatherQuery),
};
assert_eq!(tools[0].name, expected.name);
assert_eq!(tools[0].description, expected.description);
let expected_schema = serde_json::to_value(&tools[0].parameters).unwrap();
assert_eq!(
expected_schema,
json!({
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "WeatherQuery",
"type": "object",
"properties": {
"location": {
"type": "string"
},
"unit": {
"type": "string"
}
},
"required": ["location", "unit"]
})
);
let args = json!({
"location": "San Francisco",
"unit": "Celsius"
});
let query: WeatherQuery = serde_json::from_value(args).unwrap();
let result = cx.update(|cx| tool.execute(&query, cx)).await;
assert!(result.is_ok());
let result = result.unwrap();
assert_eq!(result, tool.current_weather);
}
}

View File

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

View File

@@ -33,7 +33,7 @@ impl EventEmitter<ToolbarItemEvent> for Breadcrumbs {}
impl Render for Breadcrumbs {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
const MAX_SEGMENTS: usize = 12;
let element = h_flex().text_ui();
let element = h_flex().text_ui(cx);
let Some(active_item) = self.active_item.as_ref() else {
return element;
};

View File

@@ -1203,14 +1203,24 @@ impl Room {
project: Model<Project>,
cx: &mut ModelContext<Self>,
) -> Task<Result<u64>> {
if let Some(project_id) = project.read(cx).remote_id() {
return Task::ready(Ok(project_id));
}
let request = if let Some(remote_project_id) = project.read(cx).remote_project_id() {
self.client.request(proto::ShareProject {
room_id: self.id(),
worktrees: vec![],
remote_project_id: Some(remote_project_id.0),
})
} else {
if let Some(project_id) = project.read(cx).remote_id() {
return Task::ready(Ok(project_id));
}
self.client.request(proto::ShareProject {
room_id: self.id(),
worktrees: project.read(cx).worktree_metadata_protos(cx),
remote_project_id: None,
})
};
let request = self.client.request(proto::ShareProject {
room_id: self.id(),
worktrees: project.read(cx).worktree_metadata_protos(cx),
});
cx.spawn(|this, mut cx| async move {
let response = request.await?;

View File

@@ -11,9 +11,7 @@ pub use channel_chat::{
mentions_to_proto, ChannelChat, ChannelChatEvent, ChannelMessage, ChannelMessageId,
MessageParams,
};
pub use channel_store::{
Channel, ChannelEvent, ChannelMembership, ChannelStore, DevServer, RemoteProject,
};
pub use channel_store::{Channel, ChannelEvent, ChannelMembership, ChannelStore};
#[cfg(test)]
mod channel_store_tests;

View File

@@ -3,10 +3,7 @@ mod channel_index;
use crate::{channel_buffer::ChannelBuffer, channel_chat::ChannelChat, ChannelMessage};
use anyhow::{anyhow, Result};
use channel_index::ChannelIndex;
use client::{
ChannelId, Client, ClientSettings, DevServerId, ProjectId, RemoteProjectId, Subscription, User,
UserId, UserStore,
};
use client::{ChannelId, Client, ClientSettings, ProjectId, Subscription, User, UserId, UserStore};
use collections::{hash_map, HashMap, HashSet};
use futures::{channel::mpsc, future::Shared, Future, FutureExt, StreamExt};
use gpui::{
@@ -15,7 +12,7 @@ use gpui::{
};
use language::Capability;
use rpc::{
proto::{self, ChannelRole, ChannelVisibility, DevServerStatus},
proto::{self, ChannelRole, ChannelVisibility},
TypedEnvelope,
};
use settings::Settings;
@@ -53,57 +50,12 @@ impl From<proto::HostedProject> for HostedProject {
}
}
}
#[derive(Debug, Clone)]
pub struct RemoteProject {
pub id: RemoteProjectId,
pub project_id: Option<ProjectId>,
pub channel_id: ChannelId,
pub name: SharedString,
pub path: SharedString,
pub dev_server_id: DevServerId,
}
impl From<proto::RemoteProject> for RemoteProject {
fn from(project: proto::RemoteProject) -> Self {
Self {
id: RemoteProjectId(project.id),
project_id: project.project_id.map(|id| ProjectId(id)),
channel_id: ChannelId(project.channel_id),
name: project.name.into(),
path: project.path.into(),
dev_server_id: DevServerId(project.dev_server_id),
}
}
}
#[derive(Debug, Clone)]
pub struct DevServer {
pub id: DevServerId,
pub channel_id: ChannelId,
pub name: SharedString,
pub status: DevServerStatus,
}
impl From<proto::DevServer> for DevServer {
fn from(dev_server: proto::DevServer) -> Self {
Self {
id: DevServerId(dev_server.dev_server_id),
channel_id: ChannelId(dev_server.channel_id),
status: dev_server.status(),
name: dev_server.name.into(),
}
}
}
pub struct ChannelStore {
pub channel_index: ChannelIndex,
channel_invitations: Vec<Arc<Channel>>,
channel_participants: HashMap<ChannelId, Vec<Arc<User>>>,
channel_states: HashMap<ChannelId, ChannelState>,
hosted_projects: HashMap<ProjectId, HostedProject>,
remote_projects: HashMap<RemoteProjectId, RemoteProject>,
dev_servers: HashMap<DevServerId, DevServer>,
outgoing_invites: HashSet<(ChannelId, UserId)>,
update_channels_tx: mpsc::UnboundedSender<proto::UpdateChannels>,
@@ -133,8 +85,6 @@ pub struct ChannelState {
observed_chat_message: Option<u64>,
role: Option<ChannelRole>,
projects: HashSet<ProjectId>,
dev_servers: HashSet<DevServerId>,
remote_projects: HashSet<RemoteProjectId>,
}
impl Channel {
@@ -265,8 +215,6 @@ impl ChannelStore {
channel_index: ChannelIndex::default(),
channel_participants: Default::default(),
hosted_projects: Default::default(),
remote_projects: Default::default(),
dev_servers: Default::default(),
outgoing_invites: Default::default(),
opened_buffers: Default::default(),
opened_chats: Default::default(),
@@ -366,40 +314,6 @@ impl ChannelStore {
projects
}
pub fn dev_servers_for_id(&self, channel_id: ChannelId) -> Vec<DevServer> {
let mut dev_servers: Vec<DevServer> = self
.channel_states
.get(&channel_id)
.map(|state| state.dev_servers.clone())
.unwrap_or_default()
.into_iter()
.flat_map(|id| self.dev_servers.get(&id).cloned())
.collect();
dev_servers.sort_by_key(|s| (s.name.clone(), s.id));
dev_servers
}
pub fn find_dev_server_by_id(&self, id: DevServerId) -> Option<&DevServer> {
self.dev_servers.get(&id)
}
pub fn find_remote_project_by_id(&self, id: RemoteProjectId) -> Option<&RemoteProject> {
self.remote_projects.get(&id)
}
pub fn remote_projects_for_id(&self, channel_id: ChannelId) -> Vec<RemoteProject> {
let mut remote_projects: Vec<RemoteProject> = self
.channel_states
.get(&channel_id)
.map(|state| state.remote_projects.clone())
.unwrap_or_default()
.into_iter()
.flat_map(|id| self.remote_projects.get(&id).cloned())
.collect();
remote_projects.sort_by_key(|p| (p.name.clone(), p.id));
remote_projects
}
pub fn has_open_channel_buffer(&self, channel_id: ChannelId, _cx: &AppContext) -> bool {
if let Some(buffer) = self.opened_buffers.get(&channel_id) {
if let OpenedModelHandle::Open(buffer) = buffer {
@@ -901,46 +815,6 @@ impl ChannelStore {
Ok(())
})
}
pub fn create_remote_project(
&mut self,
channel_id: ChannelId,
dev_server_id: DevServerId,
name: String,
path: String,
cx: &mut ModelContext<Self>,
) -> Task<Result<proto::CreateRemoteProjectResponse>> {
let client = self.client.clone();
cx.background_executor().spawn(async move {
client
.request(proto::CreateRemoteProject {
channel_id: channel_id.0,
dev_server_id: dev_server_id.0,
name,
path,
})
.await
})
}
pub fn create_dev_server(
&mut self,
channel_id: ChannelId,
name: String,
cx: &mut ModelContext<Self>,
) -> Task<Result<proto::CreateDevServerResponse>> {
let client = self.client.clone();
cx.background_executor().spawn(async move {
let result = client
.request(proto::CreateDevServer {
channel_id: channel_id.0,
name,
})
.await?;
Ok(result)
})
}
pub fn get_channel_member_details(
&self,
channel_id: ChannelId,
@@ -1221,11 +1095,7 @@ impl ChannelStore {
|| !payload.latest_channel_message_ids.is_empty()
|| !payload.latest_channel_buffer_versions.is_empty()
|| !payload.hosted_projects.is_empty()
|| !payload.deleted_hosted_projects.is_empty()
|| !payload.dev_servers.is_empty()
|| !payload.deleted_dev_servers.is_empty()
|| !payload.remote_projects.is_empty()
|| !payload.deleted_remote_projects.is_empty();
|| !payload.deleted_hosted_projects.is_empty();
if channels_changed {
if !payload.delete_channels.is_empty() {
@@ -1313,60 +1183,6 @@ impl ChannelStore {
.remove_hosted_project(old_project.project_id);
}
}
for remote_project in payload.remote_projects {
let remote_project: RemoteProject = remote_project.into();
if let Some(old_remote_project) = self
.remote_projects
.insert(remote_project.id, remote_project.clone())
{
self.channel_states
.entry(old_remote_project.channel_id)
.or_default()
.remove_remote_project(old_remote_project.id);
}
self.channel_states
.entry(remote_project.channel_id)
.or_default()
.add_remote_project(remote_project.id);
}
for remote_project_id in payload.deleted_remote_projects {
let remote_project_id = RemoteProjectId(remote_project_id);
if let Some(old_project) = self.remote_projects.remove(&remote_project_id) {
self.channel_states
.entry(old_project.channel_id)
.or_default()
.remove_remote_project(old_project.id);
}
}
for dev_server in payload.dev_servers {
let dev_server: DevServer = dev_server.into();
if let Some(old_server) = self.dev_servers.insert(dev_server.id, dev_server.clone())
{
self.channel_states
.entry(old_server.channel_id)
.or_default()
.remove_dev_server(old_server.id);
}
self.channel_states
.entry(dev_server.channel_id)
.or_default()
.add_dev_server(dev_server.id);
}
for dev_server_id in payload.deleted_dev_servers {
let dev_server_id = DevServerId(dev_server_id);
if let Some(old_server) = self.dev_servers.remove(&dev_server_id) {
self.channel_states
.entry(old_server.channel_id)
.or_default()
.remove_dev_server(old_server.id);
}
}
}
cx.notify();
@@ -1481,20 +1297,4 @@ impl ChannelState {
fn remove_hosted_project(&mut self, project_id: ProjectId) {
self.projects.remove(&project_id);
}
fn add_remote_project(&mut self, remote_project_id: RemoteProjectId) {
self.remote_projects.insert(remote_project_id);
}
fn remove_remote_project(&mut self, remote_project_id: RemoteProjectId) {
self.remote_projects.remove(&remote_project_id);
}
fn add_dev_server(&mut self, dev_server_id: DevServerId) {
self.dev_servers.insert(dev_server_id);
}
fn remove_dev_server(&mut self, dev_server_id: DevServerId) {
self.dev_servers.remove(&dev_server_id);
}
}

View File

@@ -20,6 +20,7 @@ path = "src/main.rs"
anyhow.workspace = true
clap.workspace = true
ipc-channel = "0.18"
release_channel.workspace = true
serde.workspace = true
util.workspace = true

View File

@@ -7,7 +7,7 @@ use serde::Deserialize;
use std::{
env,
ffi::OsStr,
fs::{self},
fs,
path::{Path, PathBuf},
};
use util::paths::PathLikeWithPosition;
@@ -36,6 +36,9 @@ struct Args {
/// Custom Zed.app path
#[arg(short, long)]
bundle_path: Option<PathBuf>,
/// Run zed in dev-server mode
#[arg(long)]
dev_server_token: Option<String>,
}
fn parse_path_with_position(
@@ -53,10 +56,24 @@ struct InfoPlist {
}
fn main() -> Result<()> {
// Intercept version designators
#[cfg(target_os = "macos")]
if let Some(channel) = std::env::args().nth(1).filter(|arg| arg.starts_with("--")) {
// When the first argument is a name of a release channel, we're gonna spawn off a cli of that version, with trailing args passed along.
use std::str::FromStr as _;
if let Ok(channel) = release_channel::ReleaseChannel::from_str(&channel[2..]) {
return mac_os::spawn_channel_cli(channel, std::env::args().skip(2).collect());
}
}
let args = Args::parse();
let bundle = Bundle::detect(args.bundle_path.as_deref()).context("Bundle detection")?;
if let Some(dev_server_token) = args.dev_server_token {
return bundle.spawn(vec!["--dev-server-token".into(), dev_server_token]);
}
if args.version {
println!("{}", bundle.zed_version_string());
return Ok(());
@@ -159,6 +176,10 @@ mod linux {
unimplemented!()
}
pub fn spawn(&self, _args: Vec<String>) -> anyhow::Result<()> {
unimplemented!()
}
pub fn zed_version_string(&self) -> String {
unimplemented!()
}
@@ -192,6 +213,10 @@ mod windows {
unimplemented!()
}
pub fn spawn(&self, _args: Vec<String>) -> anyhow::Result<()> {
unimplemented!()
}
pub fn zed_version_string(&self) -> String {
unimplemented!()
}
@@ -200,14 +225,14 @@ mod windows {
#[cfg(target_os = "macos")]
mod mac_os {
use anyhow::Context;
use anyhow::{Context, Result};
use core_foundation::{
array::{CFArray, CFIndex},
string::kCFStringEncodingUTF8,
url::{CFURLCreateWithBytes, CFURL},
};
use core_services::{kLSLaunchDefaults, LSLaunchURLSpec, LSOpenFromURLSpec, TCFType};
use std::{fs, path::Path, ptr};
use std::{fs, path::Path, process::Command, ptr};
use cli::{CliRequest, CliResponse, IpcHandshake, FORCE_CLI_MODE_ENV_VAR_NAME};
use ipc_channel::ipc::{IpcOneShotServer, IpcReceiver, IpcSender};
@@ -268,6 +293,15 @@ mod mac_os {
}
}
pub fn spawn(&self, args: Vec<String>) -> Result<()> {
let path = match self {
Self::App { app_bundle, .. } => app_bundle.join("Contents/MacOS/zed"),
Self::LocalPath { executable, .. } => executable.clone(),
};
Command::new(path).args(args).status()?;
Ok(())
}
pub fn launch(&self) -> anyhow::Result<(IpcSender<CliRequest>, IpcReceiver<CliResponse>)> {
let (server, server_name) =
IpcOneShotServer::<IpcHandshake>::new().context("Handshake before Zed spawn")?;
@@ -348,4 +382,33 @@ mod mac_os {
)
}
}
pub(super) fn spawn_channel_cli(
channel: release_channel::ReleaseChannel,
leftover_args: Vec<String>,
) -> Result<()> {
use anyhow::bail;
let app_id_prompt = format!("id of app \"{}\"", channel.display_name());
let app_id_output = Command::new("osascript")
.arg("-e")
.arg(&app_id_prompt)
.output()?;
if !app_id_output.status.success() {
bail!("Could not determine app id for {}", channel.display_name());
}
let app_name = String::from_utf8(app_id_output.stdout)?.trim().to_owned();
let app_path_prompt = format!("kMDItemCFBundleIdentifier == '{app_name}'");
let app_path_output = Command::new("mdfind").arg(app_path_prompt).output()?;
if !app_path_output.status.success() {
bail!(
"Could not determine app path for {}",
channel.display_name()
);
}
let app_path = String::from_utf8(app_path_output.stdout)?.trim().to_owned();
let cli_path = format!("{app_path}/Contents/MacOS/cli");
Command::new(cli_path).args(leftover_args).spawn()?;
Ok(())
}
}

View File

@@ -132,7 +132,7 @@ pub fn init(client: &Arc<Client>, cx: &mut AppContext) {
move |_: &SignOut, cx| {
if let Some(client) = client.upgrade() {
cx.spawn(|cx| async move {
client.disconnect(&cx);
client.sign_out(&cx).await;
})
.detach();
}
@@ -457,6 +457,14 @@ impl Client {
})
}
pub fn production(cx: &mut AppContext) -> Arc<Self> {
let clock = Arc::new(clock::RealSystemClock);
let http = Arc::new(HttpClientWithUrl::new(
&ClientSettings::get_global(cx).server_url,
));
Self::new(clock, http.clone(), cx)
}
pub fn id(&self) -> u64 {
self.id.load(Ordering::SeqCst)
}
@@ -1119,6 +1127,8 @@ impl Client {
if let Some((login, token)) =
IMPERSONATE_LOGIN.as_ref().zip(ADMIN_API_TOKEN.as_ref())
{
eprintln!("authenticate as admin {login}, {token}");
return Self::authenticate_as_admin(http, login.clone(), token.clone())
.await;
}
@@ -1250,6 +1260,15 @@ impl Client {
})
}
pub async fn sign_out(self: &Arc<Self>, cx: &AsyncAppContext) {
self.state.write().credentials = None;
self.disconnect(&cx);
if self.has_keychain_credentials(cx).await {
delete_credentials_from_keychain(cx).await.log_err();
}
}
pub fn disconnect(self: &Arc<Self>, cx: &AsyncAppContext) {
self.peer.teardown();
self.set_status(Status::SignedOut, cx);

View File

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

View File

@@ -37,7 +37,7 @@ google_ai.workspace = true
hex.workspace = true
live_kit_server.workspace = true
log.workspace = true
nanoid = "0.4"
nanoid.workspace = true
open_ai.workspace = true
parking_lot.workspace = true
prometheus = "0.13"
@@ -93,6 +93,7 @@ notifications = { workspace = true, features = ["test-support"] }
pretty_assertions.workspace = true
project = { workspace = true, features = ["test-support"] }
release_channel.workspace = true
remote_projects.workspace = true
rpc = { workspace = true, features = ["test-support"] }
sea-orm = { version = "0.12.x", features = ["sqlx-sqlite"] }
serde_json.workspace = true

View File

@@ -6,7 +6,43 @@ It contains our back-end logic for collaboration, to which we connect from the Z
# Local Development
Detailed instructions on getting started are [here](https://zed.dev/docs/local-collaboration).
## Database setup
Before you can run the collab server locally, you'll need to set up a zed Postgres database.
```
script/bootstrap
```
This script will set up the `zed` Postgres database, and populate it with some users. It requires internet access, because it fetches some users from the GitHub API.
The script will create several _admin_ users, who you'll sign in as by default when developing locally. The GitHub logins for the default users are specified in the `seed.default.json` file.
To use a different set of admin users, create `crates/collab/seed.json`.
```json
{
"admins": ["yourgithubhere"],
"channels": ["zed"],
"number_of_users": 20
}
```
## Testing collaborative features locally
In one terminal, run Zed's collaboration server and the livekit dev server:
```
foreman start
```
In a second terminal, run two or more instances of Zed.
```
script/zed-local -2
```
This script starts one to four instances of Zed, depending on the `-2`, `-3` or `-4` flags. Each instance will be connected to the local `collab` server, signed in as a different user from `seed.json` or `seed.default.json`.
# Deployment

View File

@@ -398,26 +398,21 @@ CREATE TABLE hosted_projects (
channel_id INTEGER NOT NULL REFERENCES channels(id),
name TEXT NOT NULL,
visibility TEXT NOT NULL,
deleted_at TIMESTAMP NULL,
dev_server_id INTEGER REFERENCES dev_servers(id),
dev_server_path TEXT
deleted_at TIMESTAMP NULL
);
CREATE INDEX idx_hosted_projects_on_channel_id ON hosted_projects (channel_id);
CREATE UNIQUE INDEX uix_hosted_projects_on_channel_id_and_name ON hosted_projects (channel_id, name) WHERE (deleted_at IS NULL);
CREATE TABLE dev_servers (
id INTEGER PRIMARY KEY AUTOINCREMENT,
channel_id INTEGER NOT NULL REFERENCES channels(id),
user_id INTEGER NOT NULL REFERENCES users(id),
name TEXT NOT NULL,
hashed_token TEXT NOT NULL
);
CREATE INDEX idx_dev_servers_on_channel_id ON dev_servers (channel_id);
CREATE TABLE remote_projects (
id INTEGER PRIMARY KEY AUTOINCREMENT,
channel_id INTEGER NOT NULL REFERENCES channels(id),
dev_server_id INTEGER NOT NULL REFERENCES dev_servers(id),
name TEXT NOT NULL,
path TEXT NOT NULL
);

View File

@@ -0,0 +1,7 @@
DELETE FROM remote_projects;
DELETE FROM dev_servers;
ALTER TABLE dev_servers DROP COLUMN channel_id;
ALTER TABLE dev_servers ADD COLUMN user_id INT NOT NULL REFERENCES users(id);
ALTER TABLE remote_projects DROP COLUMN channel_id;

View File

@@ -0,0 +1,3 @@
ALTER TABLE remote_projects DROP COLUMN name;
ALTER TABLE remote_projects
ADD CONSTRAINT unique_path_constraint UNIQUE(dev_server_id, path);

View File

@@ -5,7 +5,8 @@
"maxbrunsfeld",
"iamnbutler",
"mikayla-maki",
"JosephTLyons"
"JosephTLyons",
"rgbkrk"
],
"channels": ["zed"],
"number_of_users": 100

View File

@@ -1,5 +1,6 @@
use anyhow::{anyhow, Result};
use anyhow::{anyhow, Context as _, Result};
use rpc::proto;
use util::ResultExt as _;
pub fn language_model_request_to_open_ai(
request: proto::CompleteWithLanguageModel,
@@ -9,24 +10,83 @@ pub fn language_model_request_to_open_ai(
messages: request
.messages
.into_iter()
.map(|message| {
.map(|message: proto::LanguageModelRequestMessage| {
let role = proto::LanguageModelRole::from_i32(message.role)
.ok_or_else(|| anyhow!("invalid role {}", message.role))?;
Ok(open_ai::RequestMessage {
role: match role {
proto::LanguageModelRole::LanguageModelUser => open_ai::Role::User,
proto::LanguageModelRole::LanguageModelAssistant => {
open_ai::Role::Assistant
}
proto::LanguageModelRole::LanguageModelSystem => open_ai::Role::System,
let openai_message = match role {
proto::LanguageModelRole::LanguageModelUser => open_ai::RequestMessage::User {
content: message.content,
},
content: message.content,
})
proto::LanguageModelRole::LanguageModelAssistant => {
open_ai::RequestMessage::Assistant {
content: Some(message.content),
tool_calls: message
.tool_calls
.into_iter()
.filter_map(|call| {
Some(open_ai::ToolCall {
id: call.id,
content: match call.variant? {
proto::tool_call::Variant::Function(f) => {
open_ai::ToolCallContent::Function {
function: open_ai::FunctionContent {
name: f.name,
arguments: f.arguments,
},
}
}
},
})
})
.collect(),
}
}
proto::LanguageModelRole::LanguageModelSystem => {
open_ai::RequestMessage::System {
content: message.content,
}
}
proto::LanguageModelRole::LanguageModelTool => open_ai::RequestMessage::Tool {
tool_call_id: message
.tool_call_id
.ok_or_else(|| anyhow!("tool message is missing tool call id"))?,
content: message.content,
},
};
Ok(openai_message)
})
.collect::<Result<Vec<open_ai::RequestMessage>>>()?,
stream: true,
stop: request.stop,
temperature: request.temperature,
tools: request
.tools
.into_iter()
.filter_map(|tool| {
Some(match tool.variant? {
proto::chat_completion_tool::Variant::Function(f) => {
open_ai::ToolDefinition::Function {
function: open_ai::FunctionDefinition {
name: f.name,
description: f.description,
parameters: if let Some(params) = &f.parameters {
Some(
serde_json::from_str(params)
.context("failed to deserialize tool parameters")
.log_err()?,
)
} else {
None
},
},
}
}
})
})
.collect(),
tool_choice: request.tool_choice,
})
}
@@ -58,6 +118,9 @@ pub fn language_model_request_message_to_google_ai(
proto::LanguageModelRole::LanguageModelUser => google_ai::Role::User,
proto::LanguageModelRole::LanguageModelAssistant => google_ai::Role::Model,
proto::LanguageModelRole::LanguageModelSystem => google_ai::Role::User,
proto::LanguageModelRole::LanguageModelTool => {
Err(anyhow!("we don't handle tool calls with google ai yet"))?
}
},
})
}

View File

@@ -136,6 +136,13 @@ pub async fn post_crash(
.get("x-zed-panicked-on")
.and_then(|h| h.to_str().ok())
.and_then(|s| s.parse().ok());
let installation_id = headers
.get("x-zed-installation-id")
.and_then(|h| h.to_str().ok())
.map(|s| s.to_string())
.unwrap_or_default();
let mut recent_panic = None;
if let Some(recent_panic_on) = recent_panic_on {
@@ -160,6 +167,7 @@ pub async fn post_crash(
os_version = %report.header.os_version,
bundle_id = %report.header.bundle_id,
incident_id = %report.header.incident_id,
installation_id = %installation_id,
description = %description,
backtrace = %summary,
"crash report");

View File

@@ -655,8 +655,6 @@ pub struct ChannelsForUser {
pub channel_memberships: Vec<channel_member::Model>,
pub channel_participants: HashMap<ChannelId, Vec<UserId>>,
pub hosted_projects: Vec<proto::HostedProject>,
pub dev_servers: Vec<dev_server::Model>,
pub remote_projects: Vec<proto::RemoteProject>,
pub observed_buffer_versions: Vec<proto::ChannelBufferVersion>,
pub observed_channel_messages: Vec<proto::ChannelMessageId>,
@@ -764,6 +762,7 @@ pub struct Project {
pub collaborators: Vec<ProjectCollaborator>,
pub worktrees: BTreeMap<u64, Worktree>,
pub language_servers: Vec<proto::LanguageServer>,
pub remote_project_id: Option<RemoteProjectId>,
}
pub struct ProjectCollaborator {
@@ -786,8 +785,7 @@ impl ProjectCollaborator {
#[derive(Debug)]
pub struct LeftProject {
pub id: ProjectId,
pub host_user_id: Option<UserId>,
pub host_connection_id: Option<ConnectionId>,
pub should_unshare: bool,
pub connection_ids: Vec<ConnectionId>,
}

View File

@@ -640,15 +640,10 @@ impl Database {
.get_hosted_projects(&channel_ids, &roles_by_channel_id, tx)
.await?;
let dev_servers = self.get_dev_servers(&channel_ids, tx).await?;
let remote_projects = self.get_remote_projects(&channel_ids, tx).await?;
Ok(ChannelsForUser {
channel_memberships,
channels,
hosted_projects,
dev_servers,
remote_projects,
channel_participants,
latest_buffer_versions,
latest_channel_messages,

View File

@@ -1,6 +1,9 @@
use sea_orm::{ActiveValue, ColumnTrait, DatabaseTransaction, EntityTrait, QueryFilter};
use rpc::proto;
use sea_orm::{
ActiveValue, ColumnTrait, DatabaseTransaction, EntityTrait, IntoActiveModel, QueryFilter,
};
use super::{channel, dev_server, ChannelId, Database, DevServerId, UserId};
use super::{dev_server, remote_project, Database, DevServerId, UserId};
impl Database {
pub async fn get_dev_server(
@@ -16,40 +19,105 @@ impl Database {
.await
}
pub async fn get_dev_servers(
pub async fn get_dev_servers(&self, user_id: UserId) -> crate::Result<Vec<dev_server::Model>> {
self.transaction(|tx| async move {
Ok(dev_server::Entity::find()
.filter(dev_server::Column::UserId.eq(user_id))
.all(&*tx)
.await?)
})
.await
}
pub async fn remote_projects_update(
&self,
channel_ids: &Vec<ChannelId>,
user_id: UserId,
) -> crate::Result<proto::RemoteProjectsUpdate> {
self.transaction(
|tx| async move { self.remote_projects_update_internal(user_id, &tx).await },
)
.await
}
pub async fn remote_projects_update_internal(
&self,
user_id: UserId,
tx: &DatabaseTransaction,
) -> crate::Result<Vec<dev_server::Model>> {
let servers = dev_server::Entity::find()
.filter(dev_server::Column::ChannelId.is_in(channel_ids.iter().map(|id| id.0)))
) -> crate::Result<proto::RemoteProjectsUpdate> {
let dev_servers = dev_server::Entity::find()
.filter(dev_server::Column::UserId.eq(user_id))
.all(tx)
.await?;
Ok(servers)
let remote_projects = remote_project::Entity::find()
.filter(
remote_project::Column::DevServerId
.is_in(dev_servers.iter().map(|d| d.id).collect::<Vec<_>>()),
)
.find_also_related(super::project::Entity)
.all(tx)
.await?;
Ok(proto::RemoteProjectsUpdate {
dev_servers: dev_servers
.into_iter()
.map(|d| d.to_proto(proto::DevServerStatus::Offline))
.collect(),
remote_projects: remote_projects
.into_iter()
.map(|(remote_project, project)| remote_project.to_proto(project))
.collect(),
})
}
pub async fn create_dev_server(
&self,
channel_id: ChannelId,
name: &str,
hashed_access_token: &str,
user_id: UserId,
) -> crate::Result<(channel::Model, dev_server::Model)> {
) -> crate::Result<(dev_server::Model, proto::RemoteProjectsUpdate)> {
self.transaction(|tx| async move {
let channel = self.get_channel_internal(channel_id, &tx).await?;
self.check_user_is_channel_admin(&channel, user_id, &tx)
.await?;
let dev_server = dev_server::Entity::insert(dev_server::ActiveModel {
id: ActiveValue::NotSet,
hashed_token: ActiveValue::Set(hashed_access_token.to_string()),
channel_id: ActiveValue::Set(channel_id),
name: ActiveValue::Set(name.to_string()),
user_id: ActiveValue::Set(user_id),
})
.exec_with_returning(&*tx)
.await?;
Ok((channel, dev_server))
let remote_projects = self.remote_projects_update_internal(user_id, &tx).await?;
Ok((dev_server, remote_projects))
})
.await
}
pub async fn delete_dev_server(
&self,
id: DevServerId,
user_id: UserId,
) -> crate::Result<proto::RemoteProjectsUpdate> {
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))?;
}
remote_project::Entity::delete_many()
.filter(remote_project::Column::DevServerId.eq(id))
.exec(&*tx)
.await?;
dev_server::Entity::delete(dev_server.into_active_model())
.exec(&*tx)
.await?;
let remote_projects = self.remote_projects_update_internal(user_id, &tx).await?;
Ok(remote_projects)
})
.await
}

View File

@@ -30,6 +30,7 @@ impl Database {
room_id: RoomId,
connection: ConnectionId,
worktrees: &[proto::WorktreeMetadata],
remote_project_id: Option<RemoteProjectId>,
) -> Result<TransactionGuard<(ProjectId, proto::Room)>> {
self.room_transaction(room_id, |tx| async move {
let participant = room_participant::Entity::find()
@@ -58,6 +59,30 @@ impl Database {
return Err(anyhow!("guests cannot share projects"))?;
}
if let Some(remote_project_id) = remote_project_id {
let project = project::Entity::find()
.filter(project::Column::RemoteProjectId.eq(Some(remote_project_id)))
.one(&*tx)
.await?
.ok_or_else(|| anyhow!("no remote project"))?;
if project.room_id.is_some() {
return Err(anyhow!("project already shared"))?;
};
let project = project::Entity::update(project::ActiveModel {
room_id: ActiveValue::Set(Some(room_id)),
..project.into_active_model()
})
.exec(&*tx)
.await?;
// todo! check user is a project-collaborator
let room = self.get_room(room_id, &tx).await?;
return Ok((project.id, room));
}
let project = project::ActiveModel {
room_id: ActiveValue::set(Some(participant.room_id)),
host_user_id: ActiveValue::set(Some(participant.user_id)),
@@ -111,6 +136,7 @@ impl Database {
&self,
project_id: ProjectId,
connection: ConnectionId,
user_id: Option<UserId>,
) -> Result<TransactionGuard<(Option<proto::Room>, Vec<ConnectionId>)>> {
self.project_transaction(project_id, |tx| async move {
let guest_connection_ids = self.project_guest_connection_ids(project_id, &tx).await?;
@@ -118,19 +144,37 @@ impl Database {
.one(&*tx)
.await?
.ok_or_else(|| anyhow!("project not found"))?;
let room = if let Some(room_id) = project.room_id {
Some(self.get_room(room_id, &tx).await?)
} else {
None
};
if project.host_connection()? == connection {
let room = if let Some(room_id) = project.room_id {
Some(self.get_room(room_id, &tx).await?)
} else {
None
};
project::Entity::delete(project.into_active_model())
.exec(&*tx)
.await?;
Ok((room, guest_connection_ids))
} else {
Err(anyhow!("cannot unshare a project hosted by another user"))?
return Ok((room, guest_connection_ids));
}
if let Some(remote_project_id) = project.remote_project_id {
if let Some(user_id) = user_id {
if user_id
!= self
.owner_for_remote_project(remote_project_id, &tx)
.await?
{
Err(anyhow!("cannot unshare a project hosted by another user"))?
}
project::Entity::update(project::ActiveModel {
room_id: ActiveValue::Set(None),
..project.into_active_model()
})
.exec(&*tx)
.await?;
return Ok((room, guest_connection_ids));
}
}
Err(anyhow!("cannot unshare a project hosted by another user"))?
})
.await
}
@@ -753,6 +797,7 @@ impl Database {
name: language_server.name,
})
.collect(),
remote_project_id: project.remote_project_id,
};
Ok((project, replica_id as ReplicaId))
}
@@ -794,8 +839,7 @@ impl Database {
Ok(LeftProject {
id: project.id,
connection_ids,
host_user_id: None,
host_connection_id: None,
should_unshare: false,
})
})
.await
@@ -832,7 +876,7 @@ impl Database {
.find_related(project_collaborator::Entity)
.all(&*tx)
.await?;
let connection_ids = collaborators
let connection_ids: Vec<ConnectionId> = collaborators
.into_iter()
.map(|collaborator| collaborator.connection())
.collect();
@@ -870,8 +914,7 @@ impl Database {
let left_project = LeftProject {
id: project_id,
host_user_id: project.host_user_id,
host_connection_id: Some(project.host_connection()?),
should_unshare: connection == project.host_connection()?,
connection_ids,
};
Ok((room, left_project))
@@ -914,7 +957,7 @@ impl Database {
capability: Capability,
tx: &DatabaseTransaction,
) -> Result<(project::Model, ChannelRole)> {
let (project, remote_project) = project::Entity::find_by_id(project_id)
let (mut project, remote_project) = project::Entity::find_by_id(project_id)
.find_also_related(remote_project::Entity)
.one(tx)
.await?
@@ -933,27 +976,44 @@ impl Database {
PrincipalId::UserId(user_id) => user_id,
};
let role = if let Some(remote_project) = remote_project {
let channel = channel::Entity::find_by_id(remote_project.channel_id)
.one(tx)
.await?
.ok_or_else(|| anyhow!("no such channel"))?;
self.check_user_is_channel_participant(&channel, user_id, &tx)
.await?
} else if let Some(room_id) = project.room_id {
// what's the users role?
let current_participant = room_participant::Entity::find()
let role_from_room = if let Some(room_id) = project.room_id {
room_participant::Entity::find()
.filter(room_participant::Column::RoomId.eq(room_id))
.filter(room_participant::Column::AnsweringConnectionId.eq(connection_id.id))
.one(tx)
.await?
.ok_or_else(|| anyhow!("no such room"))?;
current_participant.role.unwrap_or(ChannelRole::Guest)
.and_then(|participant| participant.role)
} else {
return Err(anyhow!("not authorized to read projects"))?;
None
};
let role_from_remote_project = if let Some(remote_project) = remote_project {
let dev_server = dev_server::Entity::find_by_id(remote_project.dev_server_id)
.one(tx)
.await?
.ok_or_else(|| anyhow!("no such channel"))?;
if user_id == dev_server.user_id {
// If the user left the room "uncleanly" they may rejoin the
// remote project before leave_room runs. IN that case kick
// the project out of the room pre-emptively.
if role_from_room.is_none() {
project = project::Entity::update(project::ActiveModel {
room_id: ActiveValue::Set(None),
..project.into_active_model()
})
.exec(tx)
.await?;
}
Some(ChannelRole::Admin)
} else {
None
}
} else {
None
};
let role = role_from_remote_project
.or(role_from_room)
.unwrap_or(ChannelRole::Banned);
match capability {
Capability::ReadWrite => {

View File

@@ -8,8 +8,8 @@ use sea_orm::{
use crate::db::ProjectId;
use super::{
channel, project, project_collaborator, remote_project, worktree, ChannelId, Database,
DevServerId, RejoinedProject, RemoteProjectId, ResharedProject, ServerId, UserId,
dev_server, project, project_collaborator, remote_project, worktree, Database, DevServerId,
RejoinedProject, RemoteProjectId, ResharedProject, ServerId, UserId,
};
impl Database {
@@ -26,29 +26,6 @@ impl Database {
.await
}
pub async fn get_remote_projects(
&self,
channel_ids: &Vec<ChannelId>,
tx: &DatabaseTransaction,
) -> crate::Result<Vec<proto::RemoteProject>> {
let servers = remote_project::Entity::find()
.filter(remote_project::Column::ChannelId.is_in(channel_ids.iter().map(|id| id.0)))
.find_also_related(project::Entity)
.all(tx)
.await?;
Ok(servers
.into_iter()
.map(|(remote_project, project)| proto::RemoteProject {
id: remote_project.id.to_proto(),
project_id: project.map(|p| p.id.to_proto()),
channel_id: remote_project.channel_id.to_proto(),
name: remote_project.name,
dev_server_id: remote_project.dev_server_id.to_proto(),
path: remote_project.path,
})
.collect())
}
pub async fn get_remote_projects_for_dev_server(
&self,
dev_server_id: DevServerId,
@@ -64,8 +41,6 @@ impl Database {
.map(|(remote_project, project)| proto::RemoteProject {
id: remote_project.id.to_proto(),
project_id: project.map(|p| p.id.to_proto()),
channel_id: remote_project.channel_id.to_proto(),
name: remote_project.name,
dev_server_id: remote_project.dev_server_id.to_proto(),
path: remote_project.path,
})
@@ -74,6 +49,38 @@ impl Database {
.await
}
pub async fn remote_project_ids_for_user(
&self,
user_id: UserId,
tx: &DatabaseTransaction,
) -> crate::Result<Vec<RemoteProjectId>> {
let dev_servers = dev_server::Entity::find()
.filter(dev_server::Column::UserId.eq(user_id))
.find_with_related(remote_project::Entity)
.all(tx)
.await?;
Ok(dev_servers
.into_iter()
.flat_map(|(_, projects)| projects.into_iter().map(|p| p.id))
.collect())
}
pub async fn owner_for_remote_project(
&self,
remote_project_id: RemoteProjectId,
tx: &DatabaseTransaction,
) -> crate::Result<UserId> {
let dev_server = remote_project::Entity::find_by_id(remote_project_id)
.find_also_related(dev_server::Entity)
.one(tx)
.await?
.and_then(|(_, dev_server)| dev_server)
.ok_or_else(|| anyhow!("no remote project"))?;
Ok(dev_server.user_id)
}
pub async fn get_stale_dev_server_projects(
&self,
connection: ConnectionId,
@@ -95,28 +102,30 @@ impl Database {
pub async fn create_remote_project(
&self,
channel_id: ChannelId,
dev_server_id: DevServerId,
name: &str,
path: &str,
user_id: UserId,
) -> crate::Result<(channel::Model, remote_project::Model)> {
) -> crate::Result<(remote_project::Model, proto::RemoteProjectsUpdate)> {
self.transaction(|tx| async move {
let channel = self.get_channel_internal(channel_id, &tx).await?;
self.check_user_is_channel_admin(&channel, user_id, &tx)
.await?;
let dev_server = dev_server::Entity::find_by_id(dev_server_id)
.one(&*tx)
.await?
.ok_or_else(|| anyhow!("no dev server with id {}", dev_server_id))?;
if dev_server.user_id != user_id {
return Err(anyhow!("not your dev server"))?;
}
let project = remote_project::Entity::insert(remote_project::ActiveModel {
name: ActiveValue::Set(name.to_string()),
id: ActiveValue::NotSet,
channel_id: ActiveValue::Set(channel_id),
dev_server_id: ActiveValue::Set(dev_server_id),
path: ActiveValue::Set(path.to_string()),
})
.exec_with_returning(&*tx)
.await?;
Ok((channel, project))
let status = self.remote_projects_update_internal(user_id, &tx).await?;
Ok((project, status))
})
.await
}
@@ -127,8 +136,13 @@ impl Database {
dev_server_id: DevServerId,
connection: ConnectionId,
worktrees: &[proto::WorktreeMetadata],
) -> crate::Result<proto::RemoteProject> {
) -> crate::Result<(proto::RemoteProject, UserId, proto::RemoteProjectsUpdate)> {
self.transaction(|tx| async move {
let dev_server = dev_server::Entity::find_by_id(dev_server_id)
.one(&*tx)
.await?
.ok_or_else(|| anyhow!("no dev server with id {}", dev_server_id))?;
let remote_project = remote_project::Entity::find_by_id(remote_project_id)
.one(&*tx)
.await?
@@ -168,7 +182,15 @@ impl Database {
.await?;
}
Ok(remote_project.to_proto(Some(project)))
let status = self
.remote_projects_update_internal(dev_server.user_id, &tx)
.await?;
Ok((
remote_project.to_proto(Some(project)),
dev_server.user_id,
status,
))
})
.await
}

View File

@@ -849,11 +849,32 @@ impl Database {
.into_values::<_, QueryProjectIds>()
.all(&*tx)
.await?;
// if any project in the room has a remote-project-id that belongs to a dev server that this user owns.
let remote_projects_for_user = self
.remote_project_ids_for_user(leaving_participant.user_id, &tx)
.await?;
let remote_projects_to_unshare = project::Entity::find()
.filter(
Condition::all()
.add(project::Column::RoomId.eq(room_id))
.add(
project::Column::RemoteProjectId
.is_in(remote_projects_for_user.clone()),
),
)
.all(&*tx)
.await?
.into_iter()
.map(|project| project.id)
.collect::<HashSet<_>>();
let mut left_projects = HashMap::default();
let mut collaborators = project_collaborator::Entity::find()
.filter(project_collaborator::Column::ProjectId.is_in(project_ids))
.stream(&*tx)
.await?;
while let Some(collaborator) = collaborators.next().await {
let collaborator = collaborator?;
let left_project =
@@ -861,9 +882,8 @@ impl Database {
.entry(collaborator.project_id)
.or_insert(LeftProject {
id: collaborator.project_id,
host_user_id: Default::default(),
connection_ids: Default::default(),
host_connection_id: None,
should_unshare: false,
});
let collaborator_connection_id = collaborator.connection();
@@ -871,9 +891,10 @@ impl Database {
left_project.connection_ids.push(collaborator_connection_id);
}
if collaborator.is_host {
left_project.host_user_id = Some(collaborator.user_id);
left_project.host_connection_id = Some(collaborator_connection_id);
if (collaborator.is_host && collaborator.connection() == connection)
|| remote_projects_to_unshare.contains(&collaborator.project_id)
{
left_project.should_unshare = true;
}
}
drop(collaborators);
@@ -915,6 +936,17 @@ impl Database {
.exec(&*tx)
.await?;
if !remote_projects_to_unshare.is_empty() {
project::Entity::update_many()
.filter(project::Column::Id.is_in(remote_projects_to_unshare))
.set(project::ActiveModel {
room_id: ActiveValue::Set(None),
..Default::default()
})
.exec(&*tx)
.await?;
}
let (channel, room) = self.get_channel_room(room_id, &tx).await?;
let deleted = if room.participants.is_empty() {
let result = room::Entity::delete_by_id(room_id).exec(&*tx).await?;
@@ -1264,38 +1296,46 @@ impl Database {
}
drop(db_participants);
let mut db_projects = db_room
let db_projects = db_room
.find_related(project::Entity)
.find_with_related(worktree::Entity)
.stream(tx)
.all(tx)
.await?;
while let Some(row) = db_projects.next().await {
let (db_project, db_worktree) = row?;
for (db_project, db_worktrees) in db_projects {
let host_connection = db_project.host_connection()?;
if let Some(participant) = participants.get_mut(&host_connection) {
let project = if let Some(project) = participant
.projects
.iter_mut()
.find(|project| project.id == db_project.id.to_proto())
{
project
} else {
participant.projects.push(proto::ParticipantProject {
id: db_project.id.to_proto(),
worktree_root_names: Default::default(),
});
participant.projects.last_mut().unwrap()
};
participant.projects.push(proto::ParticipantProject {
id: db_project.id.to_proto(),
worktree_root_names: Default::default(),
});
let project = participant.projects.last_mut().unwrap();
if let Some(db_worktree) = db_worktree {
for db_worktree in db_worktrees {
if db_worktree.visible {
project.worktree_root_names.push(db_worktree.root_name);
}
}
} else if let Some(remote_project_id) = db_project.remote_project_id {
let host = self.owner_for_remote_project(remote_project_id, tx).await?;
if let Some((_, participant)) = participants
.iter_mut()
.find(|(_, v)| v.user_id == host.to_proto())
{
participant.projects.push(proto::ParticipantProject {
id: db_project.id.to_proto(),
worktree_root_names: Default::default(),
});
let project = participant.projects.last_mut().unwrap();
for db_worktree in db_worktrees {
if db_worktree.visible {
project.worktree_root_names.push(db_worktree.root_name);
}
}
}
}
}
drop(db_projects);
let mut db_followers = db_room.find_related(follower::Entity).stream(tx).await?;
let mut followers = Vec::new();

View File

@@ -1,4 +1,4 @@
use crate::db::{ChannelId, DevServerId};
use crate::db::{DevServerId, UserId};
use rpc::proto;
use sea_orm::entity::prelude::*;
@@ -8,20 +8,28 @@ pub struct Model {
#[sea_orm(primary_key)]
pub id: DevServerId,
pub name: String,
pub channel_id: ChannelId,
pub user_id: UserId,
pub hashed_token: String,
}
impl ActiveModelBehavior for ActiveModel {}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {}
pub enum Relation {
#[sea_orm(has_many = "super::remote_project::Entity")]
RemoteProject,
}
impl Related<super::remote_project::Entity> for Entity {
fn to() -> RelationDef {
Relation::RemoteProject.def()
}
}
impl Model {
pub fn to_proto(&self, status: proto::DevServerStatus) -> proto::DevServer {
proto::DevServer {
dev_server_id: self.id.to_proto(),
channel_id: self.channel_id.to_proto(),
name: self.name.clone(),
status: status as i32,
}

View File

@@ -1,5 +1,5 @@
use super::project;
use crate::db::{ChannelId, DevServerId, RemoteProjectId};
use crate::db::{DevServerId, RemoteProjectId};
use rpc::proto;
use sea_orm::entity::prelude::*;
@@ -8,9 +8,7 @@ use sea_orm::entity::prelude::*;
pub struct Model {
#[sea_orm(primary_key)]
pub id: RemoteProjectId,
pub channel_id: ChannelId,
pub dev_server_id: DevServerId,
pub name: String,
pub path: String,
}
@@ -20,6 +18,12 @@ impl ActiveModelBehavior for ActiveModel {}
pub enum Relation {
#[sea_orm(has_one = "super::project::Entity")]
Project,
#[sea_orm(
belongs_to = "super::dev_server::Entity",
from = "Column::DevServerId",
to = "super::dev_server::Column::Id"
)]
DevServer,
}
impl Related<super::project::Entity> for Entity {
@@ -28,14 +32,18 @@ impl Related<super::project::Entity> for Entity {
}
}
impl Related<super::dev_server::Entity> for Entity {
fn to() -> RelationDef {
Relation::DevServer.def()
}
}
impl Model {
pub fn to_proto(&self, project: Option<project::Model>) -> proto::RemoteProject {
proto::RemoteProject {
id: self.id.to_proto(),
project_id: project.map(|p| p.id.to_proto()),
channel_id: self.channel_id.to_proto(),
dev_server_id: self.dev_server_id.to_proto(),
name: self.name.clone(),
path: self.path.clone(),
}
}

View File

@@ -535,18 +535,18 @@ async fn test_project_count(db: &Arc<Database>) {
.unwrap();
assert_eq!(db.project_count_excluding_admins().await.unwrap(), 0);
db.share_project(room_id, ConnectionId { owner_id, id: 1 }, &[])
db.share_project(room_id, ConnectionId { owner_id, id: 1 }, &[], None)
.await
.unwrap();
assert_eq!(db.project_count_excluding_admins().await.unwrap(), 1);
db.share_project(room_id, ConnectionId { owner_id, id: 1 }, &[])
db.share_project(room_id, ConnectionId { owner_id, id: 1 }, &[], None)
.await
.unwrap();
assert_eq!(db.project_count_excluding_admins().await.unwrap(), 2);
// Projects shared by admins aren't counted.
db.share_project(room_id, ConnectionId { owner_id, id: 0 }, &[])
db.share_project(room_id, ConnectionId { owner_id, id: 0 }, &[], None)
.await
.unwrap();
assert_eq!(db.project_count_excluding_admins().await.unwrap(), 2);

View File

@@ -255,6 +255,13 @@ impl DevServerSession {
pub fn dev_server_id(&self) -> DevServerId {
self.0.dev_server_id().unwrap()
}
fn dev_server(&self) -> &dev_server::Model {
match &self.0.principal {
Principal::DevServer(dev_server) => dev_server,
_ => unreachable!(),
}
}
}
impl Deref for DevServerSession {
@@ -405,6 +412,7 @@ impl Server {
.add_request_handler(user_handler(rejoin_remote_projects))
.add_request_handler(user_handler(create_remote_project))
.add_request_handler(user_handler(create_dev_server))
.add_request_handler(user_handler(delete_dev_server))
.add_request_handler(dev_server_handler(share_remote_project))
.add_request_handler(dev_server_handler(shutdown_dev_server))
.add_request_handler(dev_server_handler(reconnect_dev_server))
@@ -767,9 +775,7 @@ impl Server {
Box::new(move |envelope, session| {
let envelope = envelope.into_any().downcast::<TypedEnvelope<M>>().unwrap();
let received_at = envelope.received_at;
tracing::info!(
"message received"
);
tracing::info!("message received");
let start_time = Instant::now();
let future = (handler)(*envelope, session);
async move {
@@ -778,12 +784,24 @@ impl Server {
let processing_duration_ms = start_time.elapsed().as_micros() as f64 / 1000.0;
let queue_duration_ms = total_duration_ms - processing_duration_ms;
let payload_type = M::NAME;
match result {
Err(error) => {
// todo!(), why isn't this logged inside the span?
tracing::error!(%error, total_duration_ms, processing_duration_ms, queue_duration_ms, payload_type, "error handling message")
tracing::error!(
?error,
total_duration_ms,
processing_duration_ms,
queue_duration_ms,
payload_type,
"error handling message"
)
}
Ok(()) => tracing::info!(total_duration_ms, processing_duration_ms, queue_duration_ms, "finished handling message"),
Ok(()) => tracing::info!(
total_duration_ms,
processing_duration_ms,
queue_duration_ms,
"finished handling message"
),
}
}
.boxed()
@@ -1044,12 +1062,14 @@ impl Server {
.await?;
}
let (contacts, channels_for_user, channel_invites) = future::try_join3(
self.app_state.db.get_contacts(user.id),
self.app_state.db.get_channels_for_user(user.id),
self.app_state.db.get_channel_invites_for_user(user.id),
)
.await?;
let (contacts, channels_for_user, channel_invites, remote_projects) =
future::try_join4(
self.app_state.db.get_contacts(user.id),
self.app_state.db.get_channels_for_user(user.id),
self.app_state.db.get_channel_invites_for_user(user.id),
self.app_state.db.remote_projects_update(user.id),
)
.await?;
{
let mut pool = self.connection_pool.lock();
@@ -1067,9 +1087,10 @@ impl Server {
)?;
self.peer.send(
connection_id,
build_channels_update(channels_for_user, channel_invites, &pool),
build_channels_update(channels_for_user, channel_invites),
)?;
}
send_remote_projects_update(user.id, remote_projects, session).await;
if let Some(incoming_call) =
self.app_state.db.incoming_call_for_user(user.id).await?
@@ -1087,9 +1108,6 @@ impl Server {
};
pool.add_dev_server(connection_id, dev_server.id, zed_version);
}
update_dev_server_status(dev_server, proto::DevServerStatus::Online, &session)
.await;
// todo!() allow only one connection.
let projects = self
.app_state
@@ -1098,6 +1116,13 @@ impl Server {
.await?;
self.peer
.send(connection_id, proto::DevServerInstructions { projects })?;
let status = self
.app_state
.db
.remote_projects_update(dev_server.user_id)
.await?;
send_remote_projects_update(dev_server.user_id, status, &session).await;
}
}
@@ -1401,10 +1426,8 @@ async fn connection_lost(
update_user_contacts(session.user_id(), &session).await?;
},
Principal::DevServer(dev_server) => {
lost_dev_server_connection(&session).await?;
update_dev_server_status(&dev_server, proto::DevServerStatus::Offline, &session)
.await;
Principal::DevServer(_) => {
lost_dev_server_connection(&session.for_dev_server().unwrap()).await?;
},
}
},
@@ -1941,6 +1964,9 @@ async fn share_project(
RoomId::from_proto(request.room_id),
session.connection_id,
&request.worktrees,
request
.remote_project_id
.map(|id| RemoteProjectId::from_proto(id)),
)
.await?;
response.send(proto::ShareProjectResponse {
@@ -1954,14 +1980,25 @@ async fn share_project(
/// Unshare a project from the room.
async fn unshare_project(message: proto::UnshareProject, session: Session) -> Result<()> {
let project_id = ProjectId::from_proto(message.project_id);
unshare_project_internal(project_id, &session).await
unshare_project_internal(
project_id,
session.connection_id,
session.user_id(),
&session,
)
.await
}
async fn unshare_project_internal(project_id: ProjectId, session: &Session) -> Result<()> {
async fn unshare_project_internal(
project_id: ProjectId,
connection_id: ConnectionId,
user_id: Option<UserId>,
session: &Session,
) -> Result<()> {
let (room, guest_connection_ids) = &*session
.db()
.await
.unshare_project(project_id, session.connection_id)
.unshare_project(project_id, connection_id, user_id)
.await?;
let message = proto::UnshareProject {
@@ -1969,7 +2006,7 @@ async fn unshare_project_internal(project_id: ProjectId, session: &Session) -> R
};
broadcast(
Some(session.connection_id),
Some(connection_id),
guest_connection_ids.iter().copied(),
|conn_id| session.peer.send(conn_id, message.clone()),
);
@@ -1980,13 +2017,13 @@ async fn unshare_project_internal(project_id: ProjectId, session: &Session) -> R
Ok(())
}
/// Share a project into the room.
/// DevServer makes a project available online
async fn share_remote_project(
request: proto::ShareRemoteProject,
response: Response<proto::ShareRemoteProject>,
session: DevServerSession,
) -> Result<()> {
let remote_project = session
let (remote_project, user_id, status) = session
.db()
.await
.share_remote_project(
@@ -2000,22 +2037,7 @@ async fn share_remote_project(
return Err(anyhow!("failed to share remote project"))?;
};
for (connection_id, _) in session
.connection_pool()
.await
.channel_connection_ids(ChannelId::from_proto(remote_project.channel_id))
{
session
.peer
.send(
connection_id,
proto::UpdateChannels {
remote_projects: vec![remote_project.clone()],
..Default::default()
},
)
.trace_err();
}
send_remote_projects_update(user_id, status, &session).await;
response.send(proto::ShareProjectResponse { project_id })?;
@@ -2081,19 +2103,21 @@ fn join_project_internal(
})
.collect::<Vec<_>>();
let add_project_collaborator = proto::AddProjectCollaborator {
project_id: project_id.to_proto(),
collaborator: Some(proto::Collaborator {
peer_id: Some(session.connection_id.into()),
replica_id: replica_id.0 as u32,
user_id: guest_user_id.to_proto(),
}),
};
for collaborator in &collaborators {
session
.peer
.send(
collaborator.peer_id.unwrap().into(),
proto::AddProjectCollaborator {
project_id: project_id.to_proto(),
collaborator: Some(proto::Collaborator {
peer_id: Some(session.connection_id.into()),
replica_id: replica_id.0 as u32,
user_id: guest_user_id.to_proto(),
}),
},
add_project_collaborator.clone(),
)
.trace_err();
}
@@ -2105,7 +2129,10 @@ fn join_project_internal(
replica_id: replica_id.0 as u32,
collaborators: collaborators.clone(),
language_servers: project.language_servers.clone(),
role: project.role.into(), // todo
role: project.role.into(),
remote_project_id: project
.remote_project_id
.map(|remote_project_id| remote_project_id.0 as u64),
})?;
for (worktree_id, worktree) in mem::take(&mut project.worktrees) {
@@ -2188,8 +2215,6 @@ async fn leave_project(request: proto::LeaveProject, session: UserSession) -> Re
let (room, project) = &*db.leave_project(project_id, sender_id).await?;
tracing::info!(
%project_id,
host_user_id = ?project.host_user_id,
host_connection_id = ?project.host_connection_id,
"leave project"
);
@@ -2224,13 +2249,33 @@ async fn create_remote_project(
response: Response<proto::CreateRemoteProject>,
session: UserSession,
) -> Result<()> {
let (channel, remote_project) = session
let dev_server_id = DevServerId(request.dev_server_id as i32);
let dev_server_connection_id = session
.connection_pool()
.await
.dev_server_connection_id(dev_server_id);
let Some(dev_server_connection_id) = dev_server_connection_id else {
Err(ErrorCode::DevServerOffline
.message("Cannot create a remote project when the dev server is offline".to_string())
.anyhow())?
};
let path = request.path.clone();
//Check that the path exists on the dev server
session
.peer
.forward_request(
session.connection_id,
dev_server_connection_id,
proto::ValidateRemoteProjectRequest { path: path.clone() },
)
.await?;
let (remote_project, update) = session
.db()
.await
.create_remote_project(
ChannelId(request.channel_id as i32),
DevServerId(request.dev_server_id as i32),
&request.name,
&request.path,
session.user_id(),
)
@@ -2242,25 +2287,12 @@ async fn create_remote_project(
.get_remote_projects_for_dev_server(remote_project.dev_server_id)
.await?;
let update = proto::UpdateChannels {
remote_projects: vec![remote_project.to_proto(None)],
..Default::default()
};
let connection_pool = session.connection_pool().await;
for (connection_id, role) in connection_pool.channel_connection_ids(channel.root_id()) {
if role.can_see_all_descendants() {
session.peer.send(connection_id, update.clone())?;
}
}
session.peer.send(
dev_server_connection_id,
proto::DevServerInstructions { projects },
)?;
let dev_server_id = remote_project.dev_server_id;
let dev_server_connection_id = connection_pool.dev_server_connection_id(dev_server_id);
if let Some(dev_server_connection_id) = dev_server_connection_id {
session.peer.send(
dev_server_connection_id,
proto::DevServerInstructions { projects },
)?;
}
send_remote_projects_update(session.user_id(), update, &session).await;
response.send(proto::CreateRemoteProjectResponse {
remote_project: Some(remote_project.to_proto(None)),
@@ -2276,37 +2308,56 @@ async fn create_dev_server(
let access_token = auth::random_token();
let hashed_access_token = auth::hash_access_token(&access_token);
let (channel, dev_server) = session
let (dev_server, status) = session
.db()
.await
.create_dev_server(
ChannelId(request.channel_id as i32),
&request.name,
&hashed_access_token,
session.user_id(),
)
.create_dev_server(&request.name, &hashed_access_token, session.user_id())
.await?;
let update = proto::UpdateChannels {
dev_servers: vec![dev_server.to_proto(proto::DevServerStatus::Offline)],
..Default::default()
};
let connection_pool = session.connection_pool().await;
for (connection_id, role) in connection_pool.channel_connection_ids(channel.root_id()) {
if role.can_see_channel(channel.visibility) {
session.peer.send(connection_id, update.clone())?;
}
}
send_remote_projects_update(session.user_id(), status, &session).await;
response.send(proto::CreateDevServerResponse {
dev_server_id: dev_server.id.0 as u64,
channel_id: request.channel_id,
access_token: auth::generate_dev_server_token(dev_server.id.0 as usize, access_token),
name: request.name.clone(),
})?;
Ok(())
}
async fn delete_dev_server(
request: proto::DeleteDevServer,
response: Response<proto::DeleteDevServer>,
session: UserSession,
) -> Result<()> {
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 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 status = session
.db()
.await
.delete_dev_server(dev_server_id, session.user_id())
.await?;
send_remote_projects_update(session.user_id(), status, &session).await;
response.send(proto::Ack {})?;
Ok(())
}
async fn rejoin_remote_projects(
request: proto::RejoinRemoteProjects,
response: Response<proto::RejoinRemoteProjects>,
@@ -2403,8 +2454,15 @@ 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
}
async fn shutdown_dev_server_internal(
dev_server_id: DevServerId,
connection_id: ConnectionId,
session: &Session,
) -> Result<()> {
let (remote_projects, dev_server) = {
let dev_server_id = session.dev_server_id();
let db = session.db().await;
let remote_projects = db.get_remote_projects_for_dev_server(dev_server_id).await?;
let dev_server = db.get_dev_server(dev_server_id).await?;
@@ -2412,22 +2470,26 @@ async fn shutdown_dev_server(
};
for project_id in remote_projects.iter().filter_map(|p| p.project_id) {
unshare_project_internal(ProjectId::from_proto(project_id), &session.0).await?;
unshare_project_internal(
ProjectId::from_proto(project_id),
connection_id,
None,
session,
)
.await?;
}
let update = proto::UpdateChannels {
remote_projects,
dev_servers: vec![dev_server.to_proto(proto::DevServerStatus::Offline)],
..Default::default()
};
for (connection_id, _) in session
session
.connection_pool()
.await
.channel_connection_ids(dev_server.channel_id)
{
session.peer.send(connection_id, update.clone()).trace_err();
}
.set_dev_server_offline(dev_server_id);
let status = session
.db()
.await
.remote_projects_update(dev_server.user_id)
.await?;
send_remote_projects_update(dev_server.user_id, status, &session).await;
Ok(())
}
@@ -4046,7 +4108,7 @@ async fn complete_with_open_ai(
crate::ai::language_model_request_to_open_ai(request)?,
)
.await
.context("open_ai::stream_completion request failed")?;
.context("open_ai::stream_completion request failed within collab")?;
while let Some(event) = completion_stream.next().await {
let event = event?;
@@ -4061,8 +4123,32 @@ async fn complete_with_open_ai(
open_ai::Role::User => LanguageModelRole::LanguageModelUser,
open_ai::Role::Assistant => LanguageModelRole::LanguageModelAssistant,
open_ai::Role::System => LanguageModelRole::LanguageModelSystem,
open_ai::Role::Tool => LanguageModelRole::LanguageModelTool,
} as i32),
content: choice.delta.content,
tool_calls: choice
.delta
.tool_calls
.into_iter()
.map(|delta| proto::ToolCallDelta {
index: delta.index as u32,
id: delta.id,
variant: match delta.function {
Some(function) => {
let name = function.name;
let arguments = function.arguments;
Some(proto::tool_call_delta::Variant::Function(
proto::tool_call_delta::FunctionCallDelta {
name,
arguments,
},
))
}
None => None,
},
})
.collect(),
}),
finish_reason: choice.finish_reason,
})
@@ -4113,6 +4199,8 @@ async fn complete_with_google_ai(
})
.collect(),
),
// Tool calls are not supported for Google
tool_calls: Vec::new(),
}),
finish_reason: candidate.finish_reason.map(|reason| reason.to_string()),
})
@@ -4135,24 +4223,28 @@ async fn complete_with_anthropic(
let messages = request
.messages
.into_iter()
.filter_map(|message| match message.role() {
LanguageModelRole::LanguageModelUser => Some(anthropic::RequestMessage {
role: anthropic::Role::User,
content: message.content,
}),
LanguageModelRole::LanguageModelAssistant => Some(anthropic::RequestMessage {
role: anthropic::Role::Assistant,
content: message.content,
}),
// Anthropic's API breaks system instructions out as a separate field rather
// than having a system message role.
LanguageModelRole::LanguageModelSystem => {
if !system_message.is_empty() {
system_message.push_str("\n\n");
}
system_message.push_str(&message.content);
.filter_map(|message| {
match message.role() {
LanguageModelRole::LanguageModelUser => Some(anthropic::RequestMessage {
role: anthropic::Role::User,
content: message.content,
}),
LanguageModelRole::LanguageModelAssistant => Some(anthropic::RequestMessage {
role: anthropic::Role::Assistant,
content: message.content,
}),
// Anthropic's API breaks system instructions out as a separate field rather
// than having a system message role.
LanguageModelRole::LanguageModelSystem => {
if !system_message.is_empty() {
system_message.push_str("\n\n");
}
system_message.push_str(&message.content);
None
None
}
// We don't yet support tool calls for Anthropic
LanguageModelRole::LanguageModelTool => None,
}
})
.collect();
@@ -4196,6 +4288,7 @@ async fn complete_with_anthropic(
delta: Some(proto::LanguageModelResponseMessage {
role: Some(current_role as i32),
content: Some(text),
tool_calls: Vec::new(),
}),
finish_reason: None,
}],
@@ -4212,6 +4305,7 @@ async fn complete_with_anthropic(
delta: Some(proto::LanguageModelResponseMessage {
role: Some(current_role as i32),
content: Some(text),
tool_calls: Vec::new(),
}),
finish_reason: None,
}],
@@ -4626,7 +4720,7 @@ fn notify_membership_updated(
..Default::default()
};
let mut update = build_channels_update(result.new_channels, vec![], connection_pool);
let mut update = build_channels_update(result.new_channels, vec![]);
update.delete_channels = result
.removed_channels
.into_iter()
@@ -4659,7 +4753,6 @@ fn build_update_user_channels(channels: &ChannelsForUser) -> proto::UpdateUserCh
fn build_channels_update(
channels: ChannelsForUser,
channel_invites: Vec<db::Channel>,
pool: &ConnectionPool,
) -> proto::UpdateChannels {
let mut update = proto::UpdateChannels::default();
@@ -4684,13 +4777,6 @@ fn build_channels_update(
}
update.hosted_projects = channels.hosted_projects;
update.dev_servers = channels
.dev_servers
.into_iter()
.map(|dev_server| dev_server.to_proto(pool.dev_server_status(dev_server.id)))
.collect();
update.remote_projects = channels.remote_projects;
update
}
@@ -4777,24 +4863,19 @@ fn channel_updated(
);
}
async fn update_dev_server_status(
dev_server: &dev_server::Model,
status: proto::DevServerStatus,
async fn send_remote_projects_update(
user_id: UserId,
mut status: proto::RemoteProjectsUpdate,
session: &Session,
) {
let pool = session.connection_pool().await;
let connections = pool.channel_connection_ids(dev_server.channel_id);
for (connection_id, _) in connections {
session
.peer
.send(
connection_id,
proto::UpdateChannels {
dev_servers: vec![dev_server.to_proto(status)],
..Default::default()
},
)
.trace_err();
for dev_server in &mut status.dev_servers {
dev_server.status =
pool.dev_server_status(DevServerId(dev_server.dev_server_id as i32)) as i32;
}
let connections = pool.user_connection_ids(user_id);
for connection_id in connections {
session.peer.send(connection_id, status.clone()).trace_err();
}
}
@@ -4833,7 +4914,7 @@ async fn update_user_contacts(user_id: UserId, session: &Session) -> Result<()>
Ok(())
}
async fn lost_dev_server_connection(session: &Session) -> Result<()> {
async fn lost_dev_server_connection(session: &DevServerSession) -> Result<()> {
log::info!("lost dev server connection, unsharing projects");
let project_ids = session
.db()
@@ -4843,9 +4924,14 @@ async fn lost_dev_server_connection(session: &Session) -> Result<()> {
for project_id in project_ids {
// not unshare re-checks the connection ids match, so we get away with no transaction
unshare_project_internal(project_id, &session).await?;
unshare_project_internal(project_id, session.connection_id, None, &session).await?;
}
let user_id = session.dev_server().user_id;
let update = session.db().await.remote_projects_update(user_id).await?;
send_remote_projects_update(user_id, update, session).await;
Ok(())
}
@@ -4947,7 +5033,7 @@ async fn leave_channel_buffers_for_session(session: &Session) -> Result<()> {
fn project_left(project: &db::LeftProject, session: &UserSession) {
for connection_id in &project.connection_ids {
if project.host_user_id == Some(session.user_id()) {
if project.should_unshare {
session
.peer
.send(

View File

@@ -13,6 +13,7 @@ pub struct ConnectionPool {
connected_users: BTreeMap<UserId, ConnectedPrincipal>,
connected_dev_servers: BTreeMap<DevServerId, ConnectionId>,
channels: ChannelPool,
offline_dev_servers: HashSet<DevServerId>,
}
#[derive(Default, Serialize)]
@@ -106,12 +107,17 @@ impl ConnectionPool {
}
PrincipalId::DevServerId(dev_server_id) => {
self.connected_dev_servers.remove(&dev_server_id);
self.offline_dev_servers.remove(&dev_server_id);
}
}
self.connections.remove(&connection_id).unwrap();
Ok(())
}
pub fn set_dev_server_offline(&mut self, dev_server_id: DevServerId) {
self.offline_dev_servers.insert(dev_server_id);
}
pub fn connections(&self) -> impl Iterator<Item = &Connection> {
self.connections.values()
}
@@ -137,7 +143,9 @@ impl ConnectionPool {
}
pub fn dev_server_status(&self, dev_server_id: DevServerId) -> proto::DevServerStatus {
if self.dev_server_connection_id(dev_server_id).is_some() {
if self.dev_server_connection_id(dev_server_id).is_some()
&& !self.offline_dev_servers.contains(&dev_server_id)
{
proto::DevServerStatus::Online
} else {
proto::DevServerStatus::Offline

View File

@@ -1023,6 +1023,8 @@ async fn test_channel_link_notifications(
.await
.unwrap();
executor.run_until_parked();
// the new channel shows for b and c
assert_channels_list_shape(
client_a.channel_store(),

View File

@@ -1,45 +1,40 @@
use std::path::Path;
use std::{path::Path, sync::Arc};
use call::ActiveCall;
use editor::Editor;
use fs::Fs;
use gpui::VisualTestContext;
use rpc::proto::DevServerStatus;
use gpui::{TestAppContext, VisualTestContext, WindowHandle};
use rpc::{proto::DevServerStatus, ErrorCode, ErrorExt};
use serde_json::json;
use workspace::{AppState, Workspace};
use crate::tests::TestServer;
use crate::tests::{following_tests::join_channel, TestServer};
use super::TestClient;
#[gpui::test]
async fn test_dev_server(cx: &mut gpui::TestAppContext, cx2: &mut gpui::TestAppContext) {
let (server, client) = TestServer::start1(cx).await;
let channel_id = server
.make_channel("test", None, (&client, cx), &mut [])
.await;
let store = cx.update(|cx| remote_projects::Store::global(cx).clone());
let resp = client
.channel_store()
let resp = store
.update(cx, |store, cx| {
store.create_dev_server(channel_id, "server-1".to_string(), cx)
store.create_dev_server("server-1".to_string(), cx)
})
.await
.unwrap();
client.channel_store().update(cx, |store, _| {
assert_eq!(store.dev_servers_for_id(channel_id).len(), 1);
assert_eq!(store.dev_servers_for_id(channel_id)[0].name, "server-1");
assert_eq!(
store.dev_servers_for_id(channel_id)[0].status,
DevServerStatus::Offline
);
store.update(cx, |store, _| {
assert_eq!(store.dev_servers().len(), 1);
assert_eq!(store.dev_servers()[0].name, "server-1");
assert_eq!(store.dev_servers()[0].status, DevServerStatus::Offline);
});
let dev_server = server.create_dev_server(resp.access_token, cx2).await;
cx.executor().run_until_parked();
client.channel_store().update(cx, |store, _| {
assert_eq!(
store.dev_servers_for_id(channel_id)[0].status,
DevServerStatus::Online
);
store.update(cx, |store, _| {
assert_eq!(store.dev_servers()[0].status, DevServerStatus::Online);
});
dev_server
@@ -54,13 +49,10 @@ async fn test_dev_server(cx: &mut gpui::TestAppContext, cx2: &mut gpui::TestAppC
)
.await;
client
.channel_store()
store
.update(cx, |store, cx| {
store.create_remote_project(
channel_id,
client::DevServerId(resp.dev_server_id),
"project-1".to_string(),
"/remote".to_string(),
cx,
)
@@ -70,15 +62,15 @@ async fn test_dev_server(cx: &mut gpui::TestAppContext, cx2: &mut gpui::TestAppC
cx.executor().run_until_parked();
let remote_workspace = client
.channel_store()
let remote_workspace = store
.update(cx, |store, cx| {
let projects = store.remote_projects_for_id(channel_id);
let projects = store.remote_projects();
assert_eq!(projects.len(), 1);
assert_eq!(projects[0].name, "project-1");
assert_eq!(projects[0].path, "/remote");
workspace::join_remote_project(
projects[0].project_id.unwrap(),
client.app_state.clone(),
None,
cx,
)
})
@@ -87,19 +79,19 @@ async fn test_dev_server(cx: &mut gpui::TestAppContext, cx2: &mut gpui::TestAppC
cx.executor().run_until_parked();
let cx2 = VisualTestContext::from_window(remote_workspace.into(), cx).as_mut();
cx2.simulate_keystrokes("cmd-p 1 enter");
let cx = VisualTestContext::from_window(remote_workspace.into(), cx).as_mut();
cx.simulate_keystrokes("cmd-p 1 enter");
let editor = remote_workspace
.update(cx2, |ws, cx| {
.update(cx, |ws, cx| {
ws.active_item_as::<Editor>(cx).unwrap().clone()
})
.unwrap();
editor.update(cx2, |ed, cx| {
editor.update(cx, |ed, cx| {
assert_eq!(ed.text(cx).to_string(), "remote\nremote\nremote");
});
cx2.simulate_input("wow!");
cx2.simulate_keystrokes("cmd-s");
cx.simulate_input("wow!");
cx.simulate_keystrokes("cmd-s");
let content = dev_server
.fs()
@@ -108,3 +100,301 @@ async fn test_dev_server(cx: &mut gpui::TestAppContext, cx2: &mut gpui::TestAppC
.unwrap();
assert_eq!(content, "wow!remote\nremote\nremote\n");
}
#[gpui::test]
async fn test_dev_server_env_files(
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_remote_project(&server, client1.app_state.clone(), cx1, cx3).await;
cx1.executor().run_until_parked();
let cx1 = VisualTestContext::from_window(remote_workspace.into(), cx1).as_mut();
cx1.simulate_keystrokes("cmd-p . e enter");
let editor = remote_workspace
.update(cx1, |ws, cx| {
ws.active_item_as::<Editor>(cx).unwrap().clone()
})
.unwrap();
editor.update(cx1, |ed, cx| {
assert_eq!(ed.text(cx).to_string(), "SECRET");
});
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();
let (workspace2, cx2) = client2.active_workspace(cx2);
let editor = workspace2.update(cx2, |ws, cx| {
ws.active_item_as::<Editor>(cx).unwrap().clone()
});
// TODO: it'd be nice to hide .env files from other people
editor.update(cx2, |ed, cx| {
assert_eq!(ed.text(cx).to_string(), "SECRET");
});
}
async fn create_remote_project(
server: &TestServer,
client_app_state: Arc<AppState>,
cx: &mut TestAppContext,
cx_devserver: &mut TestAppContext,
) -> (TestClient, WindowHandle<Workspace>) {
let store = cx.update(|cx| remote_projects::Store::global(cx).clone());
let resp = store
.update(cx, |store, cx| {
store.create_dev_server("server-1".to_string(), cx)
})
.await
.unwrap();
let dev_server = server
.create_dev_server(resp.access_token, cx_devserver)
.await;
cx.executor().run_until_parked();
dev_server
.fs()
.insert_tree(
"/remote",
json!({
"1.txt": "remote\nremote\nremote",
".env": "SECRET",
}),
)
.await;
store
.update(cx, |store, cx| {
store.create_remote_project(
client::DevServerId(resp.dev_server_id),
"/remote".to_string(),
cx,
)
})
.await
.unwrap();
cx.executor().run_until_parked();
let workspace = store
.update(cx, |store, cx| {
let projects = store.remote_projects();
assert_eq!(projects.len(), 1);
assert_eq!(projects[0].path, "/remote");
workspace::join_remote_project(
projects[0].project_id.unwrap(),
client_app_state,
None,
cx,
)
})
.await
.unwrap();
cx.executor().run_until_parked();
(dev_server, workspace)
}
#[gpui::test]
async fn test_dev_server_leave_room(
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_remote_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| ActiveCall::global(cx).update(cx, |active_call, cx| active_call.hang_up(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()));
}
#[gpui::test]
async fn test_dev_server_reconnect(
cx1: &mut gpui::TestAppContext,
cx2: &mut gpui::TestAppContext,
cx3: &mut gpui::TestAppContext,
) {
let (mut server, client1) = TestServer::start1(cx1).await;
let channel_id = server
.make_channel("test", None, (&client1, cx1), &mut [])
.await;
let (_dev_server, remote_workspace) =
create_remote_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();
drop(client1);
let client2 = server.create_client(cx2, "user_a").await;
let store = cx2.update(|cx| remote_projects::Store::global(cx).clone());
store
.update(cx2, |store, cx| {
let projects = store.remote_projects();
workspace::join_remote_project(
projects[0].project_id.unwrap(),
client2.app_state.clone(),
None,
cx,
)
})
.await
.unwrap();
}
#[gpui::test]
async fn test_create_remote_project_path_validation(
cx1: &mut gpui::TestAppContext,
cx2: &mut gpui::TestAppContext,
cx3: &mut gpui::TestAppContext,
) {
let (server, client1) = TestServer::start1(cx1).await;
let _channel_id = server
.make_channel("test", None, (&client1, cx1), &mut [])
.await;
// Creating a project with a path that does exist should not fail
let (_dev_server, _) =
create_remote_project(&server, client1.app_state.clone(), cx1, cx2).await;
cx1.executor().run_until_parked();
let store = cx1.update(|cx| remote_projects::Store::global(cx).clone());
let resp = store
.update(cx1, |store, cx| {
store.create_dev_server("server-2".to_string(), cx)
})
.await
.unwrap();
cx1.executor().run_until_parked();
let _dev_server = server.create_dev_server(resp.access_token, cx3).await;
cx1.executor().run_until_parked();
// Creating a remote project with a path that does not exist should fail
let result = store
.update(cx1, |store, cx| {
store.create_remote_project(
client::DevServerId(resp.dev_server_id),
"/notfound".to_string(),
cx,
)
})
.await;
cx1.executor().run_until_parked();
let error = result.unwrap_err();
assert!(matches!(
error.error_code(),
ErrorCode::RemoteProjectPathDoesNotExist
));
}
#[gpui::test]
async fn test_save_as_remote(cx1: &mut gpui::TestAppContext, cx2: &mut gpui::TestAppContext) {
let (server, client1) = TestServer::start1(cx1).await;
// Creating a project with a path that does exist should not fail
let (dev_server, remote_workspace) =
create_remote_project(&server, client1.app_state.clone(), cx1, cx2).await;
let mut cx = VisualTestContext::from_window(remote_workspace.into(), cx1);
cx.simulate_keystrokes("cmd-p 1 enter");
cx.simulate_keystrokes("cmd-shift-s");
cx.simulate_input("2.txt");
cx.simulate_keystrokes("enter");
cx.executor().run_until_parked();
let title = remote_workspace
.update(&mut cx, |ws, cx| {
ws.active_item(cx).unwrap().tab_description(0, &cx).unwrap()
})
.unwrap();
assert_eq!(title, "2.txt");
let path = Path::new("/remote/2.txt");
assert_eq!(
dev_server.fs().load(&path).await.unwrap(),
"remote\nremote\nremote"
);
}

View File

@@ -3,6 +3,7 @@ use crate::{
tests::{rust_lang, TestServer},
};
use call::ActiveCall;
use collections::HashMap;
use editor::{
actions::{
ConfirmCodeAction, ConfirmCompletion, ConfirmRename, Redo, Rename, RevertSelectedHunks,
@@ -18,7 +19,10 @@ use language::{
language_settings::{AllLanguageSettings, InlayHintSettings},
FakeLspAdapter,
};
use project::SERVER_PROGRESS_DEBOUNCE_TIMEOUT;
use project::{
project_settings::{InlineBlameSettings, ProjectSettings},
SERVER_PROGRESS_DEBOUNCE_TIMEOUT,
};
use rpc::RECEIVE_TIMEOUT;
use serde_json::json;
use settings::SettingsStore;
@@ -732,12 +736,60 @@ async fn test_collaborating_with_renames(cx_a: &mut TestAppContext, cx_b: &mut T
6..9
);
rename.editor.update(cx, |rename_editor, cx| {
let rename_selection = rename_editor.selections.newest::<usize>(cx);
assert_eq!(
rename_selection.range(),
0..3,
"Rename that was triggered from zero selection caret, should propose the whole word."
);
rename_editor.buffer().update(cx, |rename_buffer, cx| {
rename_buffer.edit([(0..3, "THREE")], None, cx);
});
});
});
// Cancel the rename, and repeat the same, but use selections instead of cursor movement
editor_b.update(cx_b, |editor, cx| {
editor.cancel(&editor::actions::Cancel, cx);
});
let prepare_rename = editor_b.update(cx_b, |editor, cx| {
editor.change_selections(None, cx, |s| s.select_ranges([7..8]));
editor.rename(&Rename, cx).unwrap()
});
fake_language_server
.handle_request::<lsp::request::PrepareRenameRequest, _, _>(|params, _| async move {
assert_eq!(params.text_document.uri.as_str(), "file:///dir/one.rs");
assert_eq!(params.position, lsp::Position::new(0, 8));
Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range::new(
lsp::Position::new(0, 6),
lsp::Position::new(0, 9),
))))
})
.next()
.await
.unwrap();
prepare_rename.await.unwrap();
editor_b.update(cx_b, |editor, cx| {
use editor::ToOffset;
let rename = editor.pending_rename().unwrap();
let buffer = editor.buffer().read(cx).snapshot(cx);
let lsp_rename_start = rename.range.start.to_offset(&buffer);
let lsp_rename_end = rename.range.end.to_offset(&buffer);
assert_eq!(lsp_rename_start..lsp_rename_end, 6..9);
rename.editor.update(cx, |rename_editor, cx| {
let rename_selection = rename_editor.selections.newest::<usize>(cx);
assert_eq!(
rename_selection.range(),
1..2,
"Rename that was triggered from a selection, should have the same selection range in the rename proposal"
);
rename_editor.buffer().update(cx, |rename_buffer, cx| {
rename_buffer.edit([(0..lsp_rename_end - lsp_rename_start, "THREE")], None, cx);
});
});
});
let confirm_rename = editor_b.update(cx_b, |editor, cx| {
Editor::confirm_rename(editor, &ConfirmRename, cx).unwrap()
});
@@ -1999,6 +2051,26 @@ async fn test_git_blame_is_forwarded(cx_a: &mut TestAppContext, cx_b: &mut TestA
cx_a.update(editor::init);
cx_b.update(editor::init);
// Turn inline-blame-off by default so no state is transferred without us explicitly doing so
let inline_blame_off_settings = Some(InlineBlameSettings {
enabled: false,
delay_ms: None,
min_column: None,
});
cx_a.update(|cx| {
cx.update_global(|store: &mut SettingsStore, cx| {
store.update_user_settings::<ProjectSettings>(cx, |settings| {
settings.git.inline_blame = inline_blame_off_settings;
});
});
});
cx_b.update(|cx| {
cx.update_global(|store: &mut SettingsStore, cx| {
store.update_user_settings::<ProjectSettings>(cx, |settings| {
settings.git.inline_blame = inline_blame_off_settings;
});
});
});
client_a
.fs()
@@ -2018,15 +2090,7 @@ async fn test_git_blame_is_forwarded(cx_a: &mut TestAppContext, cx_b: &mut TestA
blame_entry("3a3a3a", 2..3),
blame_entry("4c4c4c", 3..4),
],
permalinks: [
("1b1b1b", "http://example.com/codehost/idx-0"),
("0d0d0d", "http://example.com/codehost/idx-1"),
("3a3a3a", "http://example.com/codehost/idx-2"),
("4c4c4c", "http://example.com/codehost/idx-3"),
]
.into_iter()
.map(|(sha, url)| (sha.parse().unwrap(), url.parse().unwrap()))
.collect(),
permalinks: HashMap::default(), // This field is deprecrated
messages: [
("1b1b1b", "message for idx-0"),
("0d0d0d", "message for idx-1"),
@@ -2036,6 +2100,7 @@ async fn test_git_blame_is_forwarded(cx_a: &mut TestAppContext, cx_b: &mut TestA
.into_iter()
.map(|(sha, message)| (sha.parse().unwrap(), message.into()))
.collect(),
remote_url: Some("git@github.com:zed-industries/zed.git".to_string()),
};
client_a.fs().set_blame_for_repo(
Path::new("/my-repo/.git"),
@@ -2104,7 +2169,7 @@ async fn test_git_blame_is_forwarded(cx_a: &mut TestAppContext, cx_b: &mut TestA
assert_eq!(details.message, format!("message for idx-{}", idx));
assert_eq!(
details.permalink.unwrap().to_string(),
format!("http://example.com/codehost/idx-{}", idx)
format!("https://github.com/zed-industries/zed/commit/{}", entry.sha)
);
}
});

View File

@@ -9,8 +9,9 @@ use anyhow::{anyhow, Result};
use call::{room, ActiveCall, ParticipantLocation, Room};
use client::{User, RECEIVE_TIMEOUT};
use collections::{HashMap, HashSet};
use fs::{repository::GitFileStatus, FakeFs, Fs as _, RemoveOptions};
use fs::{FakeFs, Fs as _, RemoveOptions};
use futures::{channel::mpsc, StreamExt as _};
use git::repository::GitFileStatus;
use gpui::{
px, size, AppContext, BackgroundExecutor, BorrowAppContext, Model, Modifiers, MouseButton,
MouseDownEvent, TestAppContext,
@@ -2467,7 +2468,12 @@ async fn test_propagate_saves_and_fs_changes(
});
project_a
.update(cx_a, |project, cx| {
project.save_buffer_as(new_buffer_a.clone(), "/a/file3.rs".into(), cx)
let path = ProjectPath {
path: Arc::from(Path::new("file3.rs")),
worktree_id: worktree_a.read(cx).id(),
};
project.save_buffer_as(new_buffer_a.clone(), path, cx)
})
.await
.unwrap();
@@ -3742,6 +3748,10 @@ async fn test_leaving_project(
buffer_b2.read_with(cx_b, |buffer, _| assert_eq!(buffer.text(), "a-contents"));
project_a.read_with(cx_a, |project, _| {
assert_eq!(project.collaborators().len(), 2);
});
// Drop client B's connection and ensure client A and client C observe client B leaving.
client_b.disconnect(&cx_b.to_async());
executor.advance_clock(RECONNECT_TIMEOUT);

View File

@@ -5,8 +5,9 @@ use async_trait::async_trait;
use call::ActiveCall;
use collections::{BTreeMap, HashMap};
use editor::Bias;
use fs::{repository::GitFileStatus, FakeFs, Fs as _};
use fs::{FakeFs, Fs as _};
use futures::StreamExt;
use git::repository::GitFileStatus;
use gpui::{BackgroundExecutor, Model, TestAppContext};
use language::{
range_to_lsp, FakeLspAdapter, Language, LanguageConfig, LanguageMatcher, PointUtf16,

View File

@@ -284,6 +284,7 @@ impl TestServer {
collab_ui::init(&app_state, cx);
file_finder::init(cx);
menu::init();
remote_projects::init(client.clone(), cx);
settings::KeymapFile::load_asset("keymaps/default-macos.json", cx).unwrap();
});

View File

@@ -39,7 +39,6 @@ db.workspace = true
editor.workspace = true
emojis.workspace = true
extensions_ui.workspace = true
feature_flags.workspace = true
futures.workspace = true
fuzzy.workspace = true
gpui.workspace = true

View File

@@ -234,10 +234,11 @@ impl ChatPanel {
let channel_id = chat.read(cx).channel_id;
{
self.markdown_data.clear();
let chat = chat.read(cx);
self.message_list.reset(chat.message_count());
let chat = chat.read(cx);
let channel_name = chat.channel(cx).map(|channel| channel.name.clone());
let message_count = chat.message_count();
self.message_list.reset(message_count);
self.message_editor.update(cx, |editor, cx| {
editor.set_channel(channel_id, channel_name, cx);
editor.clear_reply_to_message_id();
@@ -314,7 +315,7 @@ impl ChatPanel {
None => {
return div().child(
h_flex()
.text_ui_xs()
.text_ui_xs(cx)
.my_0p5()
.px_0p5()
.gap_x_1()
@@ -349,7 +350,7 @@ impl ChatPanel {
div().child(
h_flex()
.id(message_element_id)
.text_ui_xs()
.text_ui_xs(cx)
.my_0p5()
.px_0p5()
.gap_x_1()
@@ -494,7 +495,7 @@ impl ChatPanel {
|this| {
this.child(
h_flex()
.text_ui_sm()
.text_ui_sm(cx)
.child(
div().absolute().child(
Avatar::new(message.sender.avatar_uri.clone())
@@ -538,7 +539,7 @@ impl ChatPanel {
el.child(
v_flex()
.w_full()
.text_ui_sm()
.text_ui_sm(cx)
.id(element_id)
.child(text.element("body".into(), cx)),
)
@@ -561,7 +562,7 @@ impl ChatPanel {
div()
.px_1()
.rounded_md()
.text_ui_xs()
.text_ui_xs(cx)
.bg(cx.theme().colors().background)
.child("New messages"),
)
@@ -766,7 +767,7 @@ impl ChatPanel {
body.push_str(MESSAGE_EDITED);
}
let mut rich_text = rich_text::render_rich_text(body, &mentions, language_registry, None);
let mut rich_text = RichText::new(body, &mentions, language_registry);
if message.edited_at.is_some() {
let range = (rich_text.text.len() - MESSAGE_EDITED.len())..rich_text.text.len();
@@ -1002,7 +1003,7 @@ impl Render for ChatPanel {
el.child(
h_flex()
.px_2()
.text_ui_xs()
.text_ui_xs(cx)
.justify_between()
.border_t_1()
.border_color(cx.theme().colors().border)

View File

@@ -18,7 +18,7 @@ use project::{search::SearchQuery, Completion};
use settings::Settings;
use std::{ops::Range, sync::Arc, time::Duration};
use theme::ThemeSettings;
use ui::{prelude::*, UiTextSize};
use ui::{prelude::*, TextSize};
use crate::panel_settings::MessageEditorSettings;
@@ -522,8 +522,8 @@ impl Render for MessageEditor {
cx.theme().colors().text
},
font_family: settings.ui_font.family.clone(),
font_features: settings.ui_font.features,
font_size: UiTextSize::Small.rems().into(),
font_features: settings.ui_font.features.clone(),
font_size: TextSize::Small.rems(cx).into(),
font_weight: FontWeight::NORMAL,
font_style: FontStyle::Normal,
line_height: relative(1.3),
@@ -630,6 +630,7 @@ mod tests {
let http = FakeHttpClient::with_404_response();
let client = Client::new(clock, http.clone(), cx);
let user_store = cx.new_model(|cx| UserStore::new(client.clone(), cx));
workspace::init_settings(cx);
theme::init(theme::LoadThemes::JustBase, cx);
Project::init_settings(cx);
language::init(cx);

View File

@@ -1,20 +1,17 @@
mod channel_modal;
mod contact_finder;
mod dev_server_modal;
use self::channel_modal::ChannelModal;
use self::dev_server_modal::DevServerModal;
use crate::{
channel_view::ChannelView, chat_panel::ChatPanel, face_pile::FacePile,
CollaborationPanelSettings,
};
use call::ActiveCall;
use channel::{Channel, ChannelEvent, ChannelStore, RemoteProject};
use channel::{Channel, ChannelEvent, ChannelStore};
use client::{ChannelId, Client, Contact, ProjectId, User, UserStore};
use contact_finder::ContactFinder;
use db::kvp::KEY_VALUE_STORE;
use editor::{Editor, EditorElement, EditorStyle};
use feature_flags::{self, FeatureFlagAppExt};
use fuzzy::{match_strings, StringMatchCandidate};
use gpui::{
actions, anchored, canvas, deferred, div, fill, list, point, prelude::*, px, AnyElement,
@@ -27,7 +24,7 @@ use gpui::{
use menu::{Cancel, Confirm, SecondaryConfirm, SelectNext, SelectPrev};
use project::{Fs, Project};
use rpc::{
proto::{self, ChannelVisibility, DevServerStatus, PeerId},
proto::{self, ChannelVisibility, PeerId},
ErrorCode, ErrorExt,
};
use serde_derive::{Deserialize, Serialize};
@@ -191,7 +188,6 @@ enum ListEntry {
id: ProjectId,
name: SharedString,
},
RemoteProject(channel::RemoteProject),
Contact {
contact: Arc<Contact>,
calling: bool,
@@ -282,23 +278,10 @@ impl CollabPanel {
.push(cx.observe(&this.user_store, |this, _, cx| {
this.update_entries(true, cx)
}));
let mut has_opened = false;
this.subscriptions.push(cx.observe(
&this.channel_store,
move |this, channel_store, cx| {
if !has_opened {
if !channel_store
.read(cx)
.dev_servers_for_id(ChannelId(1))
.is_empty()
{
this.manage_remote_projects(ChannelId(1), cx);
has_opened = true;
}
}
this.subscriptions
.push(cx.observe(&this.channel_store, move |this, _, cx| {
this.update_entries(true, cx)
},
));
}));
this.subscriptions
.push(cx.observe(&active_call, |this, _, cx| this.update_entries(true, cx)));
this.subscriptions.push(cx.subscribe(
@@ -586,7 +569,6 @@ impl CollabPanel {
}
let hosted_projects = channel_store.projects_for_id(channel.id);
let remote_projects = channel_store.remote_projects_for_id(channel.id);
let has_children = channel_store
.channel_at_index(mat.candidate_id + 1)
.map_or(false, |next_channel| {
@@ -624,12 +606,6 @@ impl CollabPanel {
for (name, id) in hosted_projects {
self.entries.push(ListEntry::HostedProject { id, name });
}
if cx.has_flag::<feature_flags::Remoting>() {
for remote_project in remote_projects {
self.entries.push(ListEntry::RemoteProject(remote_project));
}
}
}
}
@@ -1089,59 +1065,6 @@ impl CollabPanel {
.tooltip(move |cx| Tooltip::text("Open Project", cx))
}
fn render_remote_project(
&self,
remote_project: &RemoteProject,
is_selected: bool,
cx: &mut ViewContext<Self>,
) -> impl IntoElement {
let id = remote_project.id;
let name = remote_project.name.clone();
let maybe_project_id = remote_project.project_id;
let dev_server = self
.channel_store
.read(cx)
.find_dev_server_by_id(remote_project.dev_server_id);
let tooltip_text = SharedString::from(match dev_server {
Some(dev_server) => format!("Open Remote Project ({})", dev_server.name),
None => "Open Remote Project".to_string(),
});
let dev_server_is_online = dev_server.map(|s| s.status) == Some(DevServerStatus::Online);
let dev_server_text_color = if dev_server_is_online {
Color::Default
} else {
Color::Disabled
};
ListItem::new(ElementId::NamedInteger(
"remote-project".into(),
id.0 as usize,
))
.indent_level(2)
.indent_step_size(px(20.))
.selected(is_selected)
.on_click(cx.listener(move |this, _, cx| {
//TODO display error message if dev server is offline
if dev_server_is_online {
if let Some(project_id) = maybe_project_id {
this.join_remote_project(project_id, cx);
}
}
}))
.start_slot(
h_flex()
.relative()
.gap_1()
.child(IconButton::new(0, IconName::FileTree).icon_color(dev_server_text_color)),
)
.child(Label::new(name.clone()).color(dev_server_text_color))
.tooltip(move |cx| Tooltip::text(tooltip_text.clone(), cx))
}
fn has_subchannels(&self, ix: usize) -> bool {
self.entries.get(ix).map_or(false, |entry| {
if let ListEntry::Channel { has_children, .. } = entry {
@@ -1343,24 +1266,11 @@ impl CollabPanel {
}
if self.channel_store.read(cx).is_root_channel(channel_id) {
context_menu = context_menu
.separator()
.entry(
"Manage Members",
None,
cx.handler_for(&this, move |this, cx| {
this.manage_members(channel_id, cx)
}),
)
.when(cx.has_flag::<feature_flags::Remoting>(), |context_menu| {
context_menu.entry(
"Manage Remote Projects",
None,
cx.handler_for(&this, move |this, cx| {
this.manage_remote_projects(channel_id, cx)
}),
)
})
context_menu = context_menu.separator().entry(
"Manage Members",
None,
cx.handler_for(&this, move |this, cx| this.manage_members(channel_id, cx)),
)
} else {
context_menu = context_menu.entry(
"Move this channel",
@@ -1624,12 +1534,6 @@ impl CollabPanel {
} => {
// todo()
}
ListEntry::RemoteProject(project) => {
if let Some(project_id) = project.project_id {
self.join_remote_project(project_id, cx)
}
}
ListEntry::OutgoingRequest(_) => {}
ListEntry::ChannelEditor { .. } => {}
}
@@ -1801,18 +1705,6 @@ impl CollabPanel {
self.show_channel_modal(channel_id, channel_modal::Mode::ManageMembers, cx);
}
fn manage_remote_projects(&mut self, channel_id: ChannelId, cx: &mut ViewContext<Self>) {
let channel_store = self.channel_store.clone();
let Some(workspace) = self.workspace.upgrade() else {
return;
};
workspace.update(cx, |workspace, cx| {
workspace.toggle_modal(cx, |cx| {
DevServerModal::new(channel_store.clone(), channel_id, cx)
});
});
}
fn remove_selected_channel(&mut self, _: &Remove, cx: &mut ViewContext<Self>) {
if let Some(channel) = self.selected_channel() {
self.remove_channel(channel.id, cx)
@@ -2113,18 +2005,6 @@ impl CollabPanel {
.detach_and_prompt_err("Failed to join channel", cx, |_, _| None)
}
fn join_remote_project(&mut self, project_id: ProjectId, cx: &mut ViewContext<Self>) {
let Some(workspace) = self.workspace.upgrade() else {
return;
};
let app_state = workspace.read(cx).app_state().clone();
workspace::join_remote_project(project_id, app_state, cx).detach_and_prompt_err(
"Failed to join project",
cx,
|_, _| None,
)
}
fn join_channel_chat(&mut self, channel_id: ChannelId, cx: &mut ViewContext<Self>) {
let Some(workspace) = self.workspace.upgrade() else {
return;
@@ -2260,9 +2140,6 @@ impl CollabPanel {
ListEntry::HostedProject { id, name } => self
.render_channel_project(*id, name, is_selected, cx)
.into_any_element(),
ListEntry::RemoteProject(remote_project) => self
.render_remote_project(remote_project, is_selected, cx)
.into_any_element(),
}
}
@@ -2294,7 +2171,7 @@ impl CollabPanel {
cx.theme().colors().text
},
font_family: settings.ui_font.family.clone(),
font_features: settings.ui_font.features,
font_features: settings.ui_font.features.clone(),
font_size: rems(0.875).into(),
font_weight: FontWeight::NORMAL,
font_style: FontStyle::Normal,
@@ -3005,11 +2882,6 @@ impl PartialEq for ListEntry {
return id == other_id;
}
}
ListEntry::RemoteProject(project) => {
if let ListEntry::RemoteProject(other) = other {
return project.id == other.id;
}
}
ListEntry::ChannelNotes { channel_id } => {
if let ListEntry::ChannelNotes {
channel_id: other_id,
@@ -3075,7 +2947,7 @@ impl Render for DraggedChannelView {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl Element {
let ui_font = ThemeSettings::get_global(cx).ui_font.family.clone();
h_flex()
.font(ui_font)
.font_family(ui_font)
.bg(cx.theme().colors().background)
.w(self.width)
.p_1()
@@ -3098,6 +2970,7 @@ impl Render for DraggedChannelView {
struct JoinChannelTooltip {
channel_store: Model<ChannelStore>,
channel_id: ChannelId,
#[allow(unused)]
has_notes_notification: bool,
}
@@ -3111,12 +2984,6 @@ impl Render for JoinChannelTooltip {
container
.child(Label::new("Join channel"))
.children(self.has_notes_notification.then(|| {
h_flex()
.gap_2()
.child(Indicator::dot().color(Color::Info))
.child(Label::new("Unread notes"))
}))
.children(participants.iter().map(|participant| {
h_flex()
.gap_2()

View File

@@ -1,622 +0,0 @@
use channel::{ChannelStore, DevServer, RemoteProject};
use client::{ChannelId, DevServerId, RemoteProjectId};
use editor::Editor;
use gpui::{
AppContext, ClipboardItem, DismissEvent, EventEmitter, FocusHandle, FocusableView, Model,
ScrollHandle, Task, View, ViewContext,
};
use rpc::proto::{self, CreateDevServerResponse, DevServerStatus};
use ui::{prelude::*, Indicator, List, ListHeader, ModalContent, ModalHeader, Tooltip};
use util::ResultExt;
use workspace::ModalView;
pub struct DevServerModal {
mode: Mode,
focus_handle: FocusHandle,
scroll_handle: ScrollHandle,
channel_store: Model<ChannelStore>,
channel_id: ChannelId,
remote_project_name_editor: View<Editor>,
remote_project_path_editor: View<Editor>,
dev_server_name_editor: View<Editor>,
_subscriptions: [gpui::Subscription; 2],
}
#[derive(Default)]
struct CreateDevServer {
creating: Option<Task<()>>,
dev_server: Option<CreateDevServerResponse>,
}
struct CreateRemoteProject {
dev_server_id: DevServerId,
creating: Option<Task<()>>,
remote_project: Option<proto::RemoteProject>,
}
enum Mode {
Default,
CreateRemoteProject(CreateRemoteProject),
CreateDevServer(CreateDevServer),
}
impl DevServerModal {
pub fn new(
channel_store: Model<ChannelStore>,
channel_id: ChannelId,
cx: &mut ViewContext<Self>,
) -> Self {
let name_editor = cx.new_view(|cx| Editor::single_line(cx));
let path_editor = cx.new_view(|cx| Editor::single_line(cx));
let dev_server_name_editor = cx.new_view(|cx| {
let mut editor = Editor::single_line(cx);
editor.set_placeholder_text("Dev server name", cx);
editor
});
let focus_handle = cx.focus_handle();
let subscriptions = [
cx.observe(&channel_store, |_, _, cx| {
cx.notify();
}),
cx.on_focus_out(&focus_handle, |_, _cx| { /* cx.emit(DismissEvent) */ }),
];
Self {
mode: Mode::Default,
focus_handle,
scroll_handle: ScrollHandle::new(),
channel_store,
channel_id,
remote_project_name_editor: name_editor,
remote_project_path_editor: path_editor,
dev_server_name_editor,
_subscriptions: subscriptions,
}
}
pub fn create_remote_project(
&mut self,
dev_server_id: DevServerId,
cx: &mut ViewContext<Self>,
) {
let channel_id = self.channel_id;
let name = self
.remote_project_name_editor
.read(cx)
.text(cx)
.trim()
.to_string();
let path = self
.remote_project_path_editor
.read(cx)
.text(cx)
.trim()
.to_string();
if name == "" {
return;
}
if path == "" {
return;
}
let create = self.channel_store.update(cx, |store, cx| {
store.create_remote_project(channel_id, dev_server_id, name, path, cx)
});
let task = cx.spawn(|this, mut cx| async move {
let result = create.await;
if let Err(e) = &result {
cx.prompt(
gpui::PromptLevel::Critical,
"Failed to create project",
Some(&format!("{:?}. Please try again.", e)),
&["Ok"],
)
.await
.log_err();
}
this.update(&mut cx, |this, _| {
this.mode = Mode::CreateRemoteProject(CreateRemoteProject {
dev_server_id,
creating: None,
remote_project: result.ok().and_then(|r| r.remote_project),
});
})
.log_err();
});
self.mode = Mode::CreateRemoteProject(CreateRemoteProject {
dev_server_id,
creating: Some(task),
remote_project: None,
});
}
pub fn create_dev_server(&mut self, cx: &mut ViewContext<Self>) {
let name = self
.dev_server_name_editor
.read(cx)
.text(cx)
.trim()
.to_string();
if name == "" {
return;
}
let dev_server = self.channel_store.update(cx, |store, cx| {
store.create_dev_server(self.channel_id, name.clone(), cx)
});
let task = cx.spawn(|this, mut cx| async move {
match dev_server.await {
Ok(dev_server) => {
this.update(&mut cx, |this, _| {
this.mode = Mode::CreateDevServer(CreateDevServer {
creating: None,
dev_server: Some(dev_server),
});
})
.log_err();
}
Err(e) => {
cx.prompt(
gpui::PromptLevel::Critical,
"Failed to create server",
Some(&format!("{:?}. Please try again.", e)),
&["Ok"],
)
.await
.log_err();
this.update(&mut cx, |this, _| {
this.mode = Mode::CreateDevServer(Default::default());
})
.log_err();
}
}
});
self.mode = Mode::CreateDevServer(CreateDevServer {
creating: Some(task),
dev_server: None,
});
cx.notify()
}
fn cancel(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) {
match self.mode {
Mode::Default => cx.emit(DismissEvent),
Mode::CreateRemoteProject(_) | Mode::CreateDevServer(_) => {
self.mode = Mode::Default;
cx.notify();
}
}
}
fn render_dev_server(
&mut self,
dev_server: &DevServer,
cx: &mut ViewContext<Self>,
) -> impl IntoElement {
let channel_store = self.channel_store.read(cx);
let dev_server_id = dev_server.id;
let status = dev_server.status;
v_flex()
.w_full()
.child(
h_flex()
.group("dev-server")
.justify_between()
.child(
h_flex()
.gap_2()
.child(
div()
.id(("status", dev_server.id.0))
.relative()
.child(Icon::new(IconName::Server).size(IconSize::Small))
.child(
div().absolute().bottom_0().left(rems_from_px(8.0)).child(
Indicator::dot().color(match status {
DevServerStatus::Online => Color::Created,
DevServerStatus::Offline => Color::Deleted,
}),
),
)
.tooltip(move |cx| {
Tooltip::text(
match status {
DevServerStatus::Online => "Online",
DevServerStatus::Offline => "Offline",
},
cx,
)
}),
)
.child(dev_server.name.clone())
.child(
h_flex()
.visible_on_hover("dev-server")
.gap_1()
.child(
IconButton::new("edit-dev-server", IconName::Pencil)
.disabled(true) //TODO implement this on the collab side
.tooltip(|cx| {
Tooltip::text("Coming Soon - Edit dev server", cx)
}),
)
.child(
IconButton::new("remove-dev-server", IconName::Trash)
.disabled(true) //TODO implement this on the collab side
.tooltip(|cx| {
Tooltip::text("Coming Soon - Remove dev server", cx)
}),
),
),
)
.child(
h_flex().gap_1().child(
IconButton::new("add-remote-project", IconName::Plus)
.tooltip(|cx| Tooltip::text("Add a remote project", cx))
.on_click(cx.listener(move |this, _, cx| {
this.mode = Mode::CreateRemoteProject(CreateRemoteProject {
dev_server_id,
creating: None,
remote_project: None,
});
cx.notify();
})),
),
),
)
.child(
v_flex()
.w_full()
.bg(cx.theme().colors().title_bar_background)
.border()
.border_color(cx.theme().colors().border_variant)
.rounded_md()
.my_1()
.py_0p5()
.px_3()
.child(
List::new().empty_message("No projects.").children(
channel_store
.remote_projects_for_id(dev_server.channel_id)
.iter()
.filter_map(|remote_project| {
if remote_project.dev_server_id == dev_server.id {
Some(self.render_remote_project(remote_project, cx))
} else {
None
}
}),
),
),
)
// .child(div().ml_8().child(
// Button::new(("add-project", dev_server_id.0), "Add Project").on_click(cx.listener(
// move |this, _, cx| {
// this.mode = Mode::CreateRemoteProject(CreateRemoteProject {
// dev_server_id,
// creating: None,
// remote_project: None,
// });
// cx.notify();
// },
// )),
// ))
}
fn render_remote_project(
&mut self,
project: &RemoteProject,
_: &mut ViewContext<Self>,
) -> impl IntoElement {
h_flex()
.gap_2()
.child(Icon::new(IconName::FileTree))
.child(Label::new(project.name.clone()))
.child(Label::new(format!("({})", project.path.clone())).color(Color::Muted))
}
fn render_create_dev_server(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let Mode::CreateDevServer(CreateDevServer {
creating,
dev_server,
}) = &self.mode
else {
unreachable!()
};
self.dev_server_name_editor.update(cx, |editor, _| {
editor.set_read_only(creating.is_some() || dev_server.is_some())
});
v_flex()
.px_1()
.pt_0p5()
.gap_px()
.child(
v_flex().py_0p5().px_1().child(
h_flex()
.px_1()
.py_0p5()
.child(
IconButton::new("back", IconName::ArrowLeft)
.style(ButtonStyle::Transparent)
.on_click(cx.listener(|this, _: &gpui::ClickEvent, cx| {
this.mode = Mode::Default;
cx.notify();
})),
)
.child(Headline::new("Register dev server")),
),
)
.child(
h_flex()
.ml_5()
.gap_2()
.child("Name")
.child(self.dev_server_name_editor.clone())
.on_action(
cx.listener(|this, _: &menu::Confirm, cx| this.create_dev_server(cx)),
)
.when(creating.is_none() && dev_server.is_none(), |div| {
div.child(
Button::new("create-dev-server", "Create").on_click(cx.listener(
move |this, _, cx| {
this.create_dev_server(cx);
},
)),
)
})
.when(creating.is_some() && dev_server.is_none(), |div| {
div.child(Button::new("create-dev-server", "Creating...").disabled(true))
}),
)
.when_some(dev_server.clone(), |div, dev_server| {
let channel_store = self.channel_store.read(cx);
let status = channel_store
.find_dev_server_by_id(DevServerId(dev_server.dev_server_id))
.map(|server| server.status)
.unwrap_or(DevServerStatus::Offline);
let instructions = SharedString::from(format!(
"zed --dev-server-token {}",
dev_server.access_token
));
div.child(
v_flex()
.ml_8()
.gap_2()
.child(Label::new(format!(
"Please log into `{}` and run:",
dev_server.name
)))
.child(instructions.clone())
.child(
IconButton::new("copy-access-token", IconName::Copy)
.on_click(cx.listener(move |_, _, cx| {
cx.write_to_clipboard(ClipboardItem::new(
instructions.to_string(),
))
}))
.icon_size(IconSize::Small)
.tooltip(|cx| Tooltip::text("Copy access token", cx)),
)
.when(status == DevServerStatus::Offline, |this| {
this.child(Label::new("Waiting for connection..."))
})
.when(status == DevServerStatus::Online, |this| {
this.child(Label::new("Connection established! 🎊")).child(
Button::new("done", "Done").on_click(cx.listener(|this, _, cx| {
this.mode = Mode::Default;
cx.notify();
})),
)
}),
)
})
}
fn render_default(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let channel_store = self.channel_store.read(cx);
let dev_servers = channel_store.dev_servers_for_id(self.channel_id);
// let dev_servers = Vec::new();
v_flex()
.id("scroll-container")
.h_full()
.overflow_y_scroll()
.track_scroll(&self.scroll_handle)
.px_1()
.pt_0p5()
.gap_px()
.child(
ModalHeader::new("Manage Remote Project")
.child(Headline::new("Remote Projects").size(HeadlineSize::Small)),
)
.child(
ModalContent::new().child(
List::new()
.empty_message("No dev servers registered.")
.header(Some(
ListHeader::new("Dev Servers").end_slot(
Button::new("register-dev-server-button", "New Server")
.icon(IconName::Plus)
.icon_position(IconPosition::Start)
.tooltip(|cx| Tooltip::text("Register a new dev server", cx))
.on_click(cx.listener(|this, _, cx| {
this.mode = Mode::CreateDevServer(Default::default());
this.dev_server_name_editor
.read(cx)
.focus_handle(cx)
.focus(cx);
cx.notify();
})),
),
))
.children(dev_servers.iter().map(|dev_server| {
self.render_dev_server(dev_server, cx).into_any_element()
})),
),
)
}
fn render_create_project(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let Mode::CreateRemoteProject(CreateRemoteProject {
dev_server_id,
creating,
remote_project,
}) = &self.mode
else {
unreachable!()
};
let channel_store = self.channel_store.read(cx);
let (dev_server_name, dev_server_status) = channel_store
.find_dev_server_by_id(*dev_server_id)
.map(|server| (server.name.clone(), server.status))
.unwrap_or((SharedString::from(""), DevServerStatus::Offline));
v_flex()
.px_1()
.pt_0p5()
.gap_px()
.child(
ModalHeader::new("Manage Remote Project")
.child(Headline::new("Manage Remote Projects")),
)
.child(
h_flex()
.py_0p5()
.px_1()
.child(div().px_1().py_0p5().child(
IconButton::new("back", IconName::ArrowLeft).on_click(cx.listener(
|this, _, cx| {
this.mode = Mode::Default;
cx.notify()
},
)),
))
.child("Add Project..."),
)
.child(
h_flex()
.ml_5()
.gap_2()
.child(
div()
.id(("status", dev_server_id.0))
.relative()
.child(Icon::new(IconName::Server))
.child(div().absolute().bottom_0().left(rems_from_px(12.0)).child(
Indicator::dot().color(match dev_server_status {
DevServerStatus::Online => Color::Created,
DevServerStatus::Offline => Color::Deleted,
}),
))
.tooltip(move |cx| {
Tooltip::text(
match dev_server_status {
DevServerStatus::Online => "Online",
DevServerStatus::Offline => "Offline",
},
cx,
)
}),
)
.child(dev_server_name.clone()),
)
.child(
h_flex()
.ml_5()
.gap_2()
.child("Name")
.child(self.remote_project_name_editor.clone())
.on_action(cx.listener(|this, _: &menu::Confirm, cx| {
cx.focus_view(&this.remote_project_path_editor)
})),
)
.child(
h_flex()
.ml_5()
.gap_2()
.child("Path")
.child(self.remote_project_path_editor.clone())
.on_action(
cx.listener(|this, _: &menu::Confirm, cx| this.create_dev_server(cx)),
)
.when(creating.is_none() && remote_project.is_none(), |div| {
div.child(Button::new("create-remote-server", "Create").on_click({
let dev_server_id = *dev_server_id;
cx.listener(move |this, _, cx| {
this.create_remote_project(dev_server_id, cx)
})
}))
})
.when(creating.is_some(), |div| {
div.child(Button::new("create-dev-server", "Creating...").disabled(true))
}),
)
.when_some(remote_project.clone(), |div, remote_project| {
let channel_store = self.channel_store.read(cx);
let status = channel_store
.find_remote_project_by_id(RemoteProjectId(remote_project.id))
.map(|project| {
if project.project_id.is_some() {
DevServerStatus::Online
} else {
DevServerStatus::Offline
}
})
.unwrap_or(DevServerStatus::Offline);
div.child(
v_flex()
.ml_5()
.ml_8()
.gap_2()
.when(status == DevServerStatus::Offline, |this| {
this.child(Label::new("Waiting for project..."))
})
.when(status == DevServerStatus::Online, |this| {
this.child(Label::new("Project online! 🎊")).child(
Button::new("done", "Done").on_click(cx.listener(|this, _, cx| {
this.mode = Mode::Default;
cx.notify();
})),
)
}),
)
})
}
}
impl ModalView for DevServerModal {}
impl FocusableView for DevServerModal {
fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
self.focus_handle.clone()
}
}
impl EventEmitter<DismissEvent> for DevServerModal {}
impl Render for DevServerModal {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
div()
.track_focus(&self.focus_handle)
.elevation_3(cx)
.key_context("DevServerModal")
.on_action(cx.listener(Self::cancel))
.pb_4()
.w(rems(34.))
.min_h(rems(20.))
.max_h(rems(40.))
.child(match &self.mode {
Mode::Default => self.render_default(cx).into_any_element(),
Mode::CreateRemoteProject(_) => self.render_create_project(cx).into_any_element(),
Mode::CreateDevServer(_) => self.render_create_dev_server(cx).into_any_element(),
})
}
}

View File

@@ -171,44 +171,48 @@ impl Render for CollabTitlebarItem {
let room = room.read(cx);
let project = self.project.read(cx);
let is_local = project.is_local();
let is_shared = is_local && project.is_shared();
let is_remote_project = project.remote_project_id().is_some();
let is_shared = (is_local || is_remote_project) && project.is_shared();
let is_muted = room.is_muted();
let is_deafened = room.is_deafened().unwrap_or(false);
let is_screen_sharing = room.is_screen_sharing();
let can_use_microphone = room.can_use_microphone();
let can_share_projects = room.can_share_projects();
this.when(is_local && can_share_projects, |this| {
this.child(
Button::new(
"toggle_sharing",
if is_shared { "Unshare" } else { "Share" },
)
.tooltip(move |cx| {
Tooltip::text(
if is_shared {
"Stop sharing project with call participants"
} else {
"Share project with call participants"
},
cx,
this.when(
(is_local || is_remote_project) && can_share_projects,
|this| {
this.child(
Button::new(
"toggle_sharing",
if is_shared { "Unshare" } else { "Share" },
)
})
.style(ButtonStyle::Subtle)
.selected_style(ButtonStyle::Tinted(TintColor::Accent))
.selected(is_shared)
.label_size(LabelSize::Small)
.on_click(cx.listener(
move |this, _, cx| {
if is_shared {
this.unshare_project(&Default::default(), cx);
} else {
this.share_project(&Default::default(), cx);
}
},
)),
)
})
.tooltip(move |cx| {
Tooltip::text(
if is_shared {
"Stop sharing project with call participants"
} else {
"Share project with call participants"
},
cx,
)
})
.style(ButtonStyle::Subtle)
.selected_style(ButtonStyle::Tinted(TintColor::Accent))
.selected(is_shared)
.label_size(LabelSize::Small)
.on_click(cx.listener(
move |this, _, cx| {
if is_shared {
this.unshare_project(&Default::default(), cx);
} else {
this.share_project(&Default::default(), cx);
}
},
)),
)
},
)
.child(
div()
.child(
@@ -406,7 +410,7 @@ impl CollabTitlebarItem {
)
}
pub fn render_project_name(&self, cx: &mut ViewContext<Self>) -> impl Element {
pub fn render_project_name(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let name = {
let mut names = self.project.read(cx).visible_worktrees(cx).map(|worktree| {
let worktree = worktree.read(cx);
@@ -423,15 +427,26 @@ impl CollabTitlebarItem {
};
let workspace = self.workspace.clone();
popover_menu("project_name_trigger")
.trigger(
Button::new("project_name_trigger", name)
.when(!is_project_selected, |b| b.color(Color::Muted))
.style(ButtonStyle::Subtle)
.label_size(LabelSize::Small)
.tooltip(move |cx| Tooltip::text("Recent Projects", cx)),
)
.menu(move |cx| Some(Self::render_project_popover(workspace.clone(), cx)))
Button::new("project_name_trigger", name)
.when(!is_project_selected, |b| b.color(Color::Muted))
.style(ButtonStyle::Subtle)
.label_size(LabelSize::Small)
.tooltip(move |cx| {
Tooltip::for_action(
"Recent Projects",
&recent_projects::OpenRecent {
create_new_window: false,
},
cx,
)
})
.on_click(cx.listener(move |_, _, cx| {
if let Some(workspace) = workspace.upgrade() {
workspace.update(cx, |workspace, cx| {
RecentProjects::open(workspace, false, cx);
})
}
}))
}
pub fn render_project_branch(&self, cx: &mut ViewContext<Self>) -> Option<impl Element> {
@@ -607,17 +622,6 @@ impl CollabTitlebarItem {
Some(view)
}
pub fn render_project_popover(
workspace: WeakView<Workspace>,
cx: &mut WindowContext<'_>,
) -> View<RecentProjects> {
let view = RecentProjects::open_popover(workspace, cx);
let focus_handle = view.focus_handle(cx);
cx.focus(&focus_handle);
view
}
fn render_connection_status(
&self,
status: &client::Status,

View File

@@ -34,7 +34,7 @@ impl ParentElement for CollabNotification {
impl RenderOnce for CollabNotification {
fn render(self, cx: &mut WindowContext) -> impl IntoElement {
h_flex()
.text_ui()
.text_ui(cx)
.justify_between()
.size_full()
.overflow_hidden()

View File

@@ -125,7 +125,7 @@ impl Render for IncomingCallNotification {
cx.set_rem_size(ui_font_size);
div().size_full().font(ui_font).child(
div().size_full().font_family(ui_font).child(
CollabNotification::new(
self.state.call.calling_user.avatar_uri.clone(),
Button::new("accept", "Accept").on_click({

View File

@@ -129,7 +129,7 @@ impl Render for ProjectSharedNotification {
cx.set_rem_size(ui_font_size);
div().size_full().font(ui_font).child(
div().size_full().font_family(ui_font).child(
CollabNotification::new(
self.owner.avatar_uri.clone(),
Button::new("open", "Open").on_click(cx.listener(move |this, _event, cx| {

View File

@@ -2,4 +2,4 @@
First, craft your test data. The examples folder shows a template for building a test-db, and can be ran with `cargo run --example [your-example]`.
To actually use and test your queries, import the generated DB file into https://sqliteonline.com/
To actually use and test your queries, import the generated DB file into https://sqliteonline.com/

View File

@@ -15,13 +15,16 @@ doctest = false
[dependencies]
anyhow.workspace = true
collections.workspace = true
ctor.workspace = true
editor.workspace = true
env_logger.workspace = true
futures.workspace = true
gpui.workspace = true
language.workspace = true
log.workspace = true
lsp.workspace = true
project.workspace = true
rand.workspace = true
schemars.workspace = true
serde.workspace = true
settings.workspace = true
@@ -40,3 +43,4 @@ serde_json.workspace = true
theme = { workspace = true, features = ["test-support"] }
unindent.workspace = true
workspace = { workspace = true, features = ["test-support"] }
pretty_assertions.workspace = true

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,13 +1,11 @@
use std::time::Duration;
use collections::HashSet;
use editor::Editor;
use gpui::{
percentage, rems, Animation, AnimationExt, EventEmitter, IntoElement, ParentElement, Render,
Styled, Subscription, Transformation, View, ViewContext, WeakView,
};
use language::Diagnostic;
use lsp::LanguageServerId;
use ui::{h_flex, prelude::*, Button, ButtonLike, Color, Icon, IconName, Label, Tooltip};
use workspace::{item::ItemHandle, StatusItemView, ToolbarItemEvent, Workspace};
@@ -18,7 +16,6 @@ pub struct DiagnosticIndicator {
active_editor: Option<WeakView<Editor>>,
workspace: WeakView<Workspace>,
current_diagnostic: Option<Diagnostic>,
in_progress_checks: HashSet<LanguageServerId>,
_observe_active_editor: Option<Subscription>,
}
@@ -64,7 +61,20 @@ impl Render for DiagnosticIndicator {
.child(Label::new(warning_count.to_string()).size(LabelSize::Small)),
};
let status = if !self.in_progress_checks.is_empty() {
let has_in_progress_checks = self
.workspace
.upgrade()
.and_then(|workspace| {
workspace
.read(cx)
.project()
.read(cx)
.language_servers_running_disk_based_diagnostics()
.next()
})
.is_some();
let status = if has_in_progress_checks {
Some(
h_flex()
.gap_2()
@@ -126,15 +136,13 @@ impl DiagnosticIndicator {
pub fn new(workspace: &Workspace, cx: &mut ViewContext<Self>) -> Self {
let project = workspace.project();
cx.subscribe(project, |this, project, event, cx| match event {
project::Event::DiskBasedDiagnosticsStarted { language_server_id } => {
this.in_progress_checks.insert(*language_server_id);
project::Event::DiskBasedDiagnosticsStarted { .. } => {
cx.notify();
}
project::Event::DiskBasedDiagnosticsFinished { language_server_id }
| project::Event::LanguageServerRemoved(language_server_id) => {
project::Event::DiskBasedDiagnosticsFinished { .. }
| project::Event::LanguageServerRemoved(_) => {
this.summary = project.read(cx).diagnostic_summary(false, cx);
this.in_progress_checks.remove(language_server_id);
cx.notify();
}
@@ -149,10 +157,6 @@ impl DiagnosticIndicator {
Self {
summary: project.read(cx).diagnostic_summary(false, cx),
in_progress_checks: project
.read(cx)
.language_servers_running_disk_based_diagnostics()
.collect(),
active_editor: None,
workspace: workspace.weak_handle(),
current_diagnostic: None,

View File

@@ -1,5 +1,5 @@
use crate::ProjectDiagnosticsEditor;
use gpui::{div, EventEmitter, ParentElement, Render, ViewContext, WeakView};
use gpui::{EventEmitter, ParentElement, Render, ViewContext, WeakView};
use ui::prelude::*;
use ui::{IconButton, IconName, Tooltip};
use workspace::{item::ItemHandle, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView};
@@ -10,12 +10,23 @@ pub struct ToolbarControls {
impl Render for ToolbarControls {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let include_warnings = self
.editor
.as_ref()
.and_then(|editor| editor.upgrade())
.map(|editor| editor.read(cx).include_warnings)
.unwrap_or(false);
let mut include_warnings = false;
let mut has_stale_excerpts = false;
let mut is_updating = false;
if let Some(editor) = self.editor.as_ref().and_then(|editor| editor.upgrade()) {
let editor = editor.read(cx);
include_warnings = editor.include_warnings;
has_stale_excerpts = !editor.paths_to_update.is_empty();
is_updating = editor.update_paths_tx.len() > 0
|| editor
.project
.read(cx)
.language_servers_running_disk_based_diagnostics()
.next()
.is_some();
}
let tooltip = if include_warnings {
"Exclude Warnings"
@@ -23,17 +34,37 @@ impl Render for ToolbarControls {
"Include Warnings"
};
div().child(
IconButton::new("toggle-warnings", IconName::ExclamationTriangle)
.tooltip(move |cx| Tooltip::text(tooltip, cx))
.on_click(cx.listener(|this, _, cx| {
if let Some(editor) = this.editor.as_ref().and_then(|editor| editor.upgrade()) {
editor.update(cx, |editor, cx| {
editor.toggle_warnings(&Default::default(), cx);
});
}
})),
)
h_flex()
.when(has_stale_excerpts, |div| {
div.child(
IconButton::new("update-excerpts", IconName::Update)
.icon_color(Color::Info)
.disabled(is_updating)
.tooltip(move |cx| Tooltip::text("Update excerpts", cx))
.on_click(cx.listener(|this, _, cx| {
if let Some(editor) =
this.editor.as_ref().and_then(|editor| editor.upgrade())
{
editor.update(cx, |editor, _| {
editor.enqueue_update_stale_excerpts(None);
});
}
})),
)
})
.child(
IconButton::new("toggle-warnings", IconName::ExclamationTriangle)
.tooltip(move |cx| Tooltip::text(tooltip, cx))
.on_click(cx.listener(|this, _, cx| {
if let Some(editor) =
this.editor.as_ref().and_then(|editor| editor.upgrade())
{
editor.update(cx, |editor, cx| {
editor.toggle_warnings(&Default::default(), cx);
});
}
})),
)
}
}

View File

@@ -94,12 +94,19 @@ pub struct SelectDownByLines {
pub(super) lines: u32,
}
#[derive(PartialEq, Clone, Deserialize, Default)]
pub struct ExpandExcerpts {
#[serde(default)]
pub(super) lines: u32,
}
impl_actions!(
editor,
[
SelectNext,
SelectPrevious,
SelectToBeginningOfLine,
ExpandExcerpts,
MovePageUp,
MovePageDown,
SelectToEndOfLine,
@@ -254,6 +261,6 @@ gpui::actions!(
UndoSelection,
UnfoldLines,
UniqueLinesCaseSensitive,
UniqueLinesCaseInsensitive
UniqueLinesCaseInsensitive,
]
);

View File

@@ -0,0 +1,277 @@
use futures::Future;
use git::blame::BlameEntry;
use git::Oid;
use gpui::{
Asset, Element, ParentElement, Render, ScrollHandle, StatefulInteractiveElement, WeakView,
WindowContext,
};
use settings::Settings;
use std::hash::Hash;
use theme::{ActiveTheme, ThemeSettings};
use ui::{
div, h_flex, tooltip_container, v_flex, Avatar, Button, ButtonStyle, Clickable as _, Color,
FluentBuilder, Icon, IconName, IconPosition, InteractiveElement as _, IntoElement,
SharedString, Styled as _, ViewContext,
};
use ui::{ButtonCommon, Disableable as _};
use workspace::Workspace;
use crate::git::blame::{CommitDetails, GitRemote};
use crate::EditorStyle;
struct CommitAvatar<'a> {
details: Option<&'a CommitDetails>,
sha: Oid,
}
impl<'a> CommitAvatar<'a> {
fn new(details: Option<&'a CommitDetails>, sha: Oid) -> Self {
Self { details, sha }
}
}
impl<'a> CommitAvatar<'a> {
fn render(&'a self, cx: &mut ViewContext<BlameEntryTooltip>) -> Option<impl IntoElement> {
let remote = self
.details
.and_then(|details| details.remote.as_ref())
.filter(|remote| remote.host_supports_avatars())?;
let avatar_url = CommitAvatarAsset::new(remote.clone(), self.sha);
let element = match cx.use_cached_asset::<CommitAvatarAsset>(&avatar_url) {
// Loading or no avatar found
None | Some(None) => Icon::new(IconName::Person)
.color(Color::Muted)
.into_element()
.into_any(),
// Found
Some(Some(url)) => Avatar::new(url.to_string()).into_element().into_any(),
};
Some(element)
}
}
#[derive(Clone, Debug)]
struct CommitAvatarAsset {
sha: Oid,
remote: GitRemote,
}
impl Hash for CommitAvatarAsset {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.sha.hash(state);
self.remote.host.hash(state);
}
}
impl CommitAvatarAsset {
fn new(remote: GitRemote, sha: Oid) -> Self {
Self { remote, sha }
}
}
impl Asset for CommitAvatarAsset {
type Source = Self;
type Output = Option<SharedString>;
fn load(
source: Self::Source,
cx: &mut WindowContext,
) -> impl Future<Output = Self::Output> + Send + 'static {
let client = cx.http_client();
async move {
source
.remote
.avatar_url(source.sha, client)
.await
.map(|url| SharedString::from(url.to_string()))
}
}
}
pub(crate) struct BlameEntryTooltip {
blame_entry: BlameEntry,
details: Option<CommitDetails>,
editor_style: EditorStyle,
workspace: Option<WeakView<Workspace>>,
scroll_handle: ScrollHandle,
}
impl BlameEntryTooltip {
pub(crate) fn new(
blame_entry: BlameEntry,
details: Option<CommitDetails>,
style: &EditorStyle,
workspace: Option<WeakView<Workspace>>,
) -> Self {
Self {
editor_style: style.clone(),
blame_entry,
details,
workspace,
scroll_handle: ScrollHandle::new(),
}
}
}
impl Render for BlameEntryTooltip {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let avatar = CommitAvatar::new(self.details.as_ref(), self.blame_entry.sha).render(cx);
let author = self
.blame_entry
.author
.clone()
.unwrap_or("<no name>".to_string());
let author_email = self.blame_entry.author_mail.clone();
let pretty_commit_id = format!("{}", self.blame_entry.sha);
let short_commit_id = pretty_commit_id.chars().take(6).collect::<String>();
let absolute_timestamp = blame_entry_absolute_timestamp(&self.blame_entry, cx);
let message = self
.details
.as_ref()
.map(|details| {
crate::render_parsed_markdown(
"blame-message",
&details.parsed_message,
&self.editor_style,
self.workspace.clone(),
cx,
)
.into_any()
})
.unwrap_or("<no commit message>".into_any());
let pull_request = self
.details
.as_ref()
.and_then(|details| details.pull_request.clone());
let ui_font_size = ThemeSettings::get_global(cx).ui_font_size;
let message_max_height = cx.line_height() * 12 + (ui_font_size / 0.4);
tooltip_container(cx, move |this, cx| {
this.occlude()
.on_mouse_move(|_, cx| cx.stop_propagation())
.child(
v_flex()
.w(gpui::rems(30.))
.gap_4()
.child(
h_flex()
.gap_x_2()
.overflow_x_hidden()
.flex_wrap()
.children(avatar)
.child(author)
.when_some(author_email, |this, author_email| {
this.child(
div()
.text_color(cx.theme().colors().text_muted)
.child(author_email),
)
})
.border_b_1()
.border_color(cx.theme().colors().border),
)
.child(
div()
.id("inline-blame-commit-message")
.occlude()
.child(message)
.max_h(message_max_height)
.overflow_y_scroll()
.track_scroll(&self.scroll_handle),
)
.child(
h_flex()
.text_color(cx.theme().colors().text_muted)
.w_full()
.justify_between()
.child(absolute_timestamp)
.child(
h_flex()
.gap_2()
.when_some(pull_request, |this, pr| {
this.child(
Button::new(
"pull-request-button",
format!("#{}", pr.number),
)
.color(Color::Muted)
.icon(IconName::PullRequest)
.icon_color(Color::Muted)
.icon_position(IconPosition::Start)
.style(ButtonStyle::Transparent)
.on_click(move |_, cx| {
cx.stop_propagation();
cx.open_url(pr.url.as_str())
}),
)
})
.child(
Button::new(
"commit-sha-button",
short_commit_id.clone(),
)
.style(ButtonStyle::Transparent)
.color(Color::Muted)
.icon(IconName::FileGit)
.icon_color(Color::Muted)
.icon_position(IconPosition::Start)
.disabled(
self.details.as_ref().map_or(true, |details| {
details.permalink.is_none()
}),
)
.when_some(
self.details
.as_ref()
.and_then(|details| details.permalink.clone()),
|this, url| {
this.on_click(move |_, cx| {
cx.stop_propagation();
cx.open_url(url.as_str())
})
},
),
),
),
),
)
})
}
}
fn blame_entry_timestamp(
blame_entry: &BlameEntry,
format: time_format::TimestampFormat,
cx: &WindowContext,
) -> String {
match blame_entry.author_offset_date_time() {
Ok(timestamp) => time_format::format_localized_timestamp(
timestamp,
time::OffsetDateTime::now_utc(),
cx.local_timezone(),
format,
),
Err(_) => "Error parsing date".to_string(),
}
}
pub fn blame_entry_relative_timestamp(blame_entry: &BlameEntry, cx: &WindowContext) -> String {
blame_entry_timestamp(blame_entry, time_format::TimestampFormat::Relative, cx)
}
fn blame_entry_absolute_timestamp(blame_entry: &BlameEntry, cx: &WindowContext) -> String {
blame_entry_timestamp(
blame_entry,
time_format::TimestampFormat::MediumAbsolute,
cx,
)
}

View File

@@ -1821,6 +1821,7 @@ pub mod tests {
cx.set_global(settings);
language::init(cx);
crate::init(cx);
workspace::init_settings(cx);
Project::init_settings(cx);
theme::init(LoadThemes::JustBase, cx);
cx.update_global::<SettingsStore, _>(|store, cx| {

View File

@@ -4,7 +4,7 @@ use super::{
};
use crate::{EditorStyle, GutterDimensions};
use collections::{Bound, HashMap, HashSet};
use gpui::{AnyElement, ElementContext, Pixels};
use gpui::{AnyElement, Pixels, WindowContext};
use language::{BufferSnapshot, Chunk, Patch, Point};
use multi_buffer::{Anchor, ExcerptId, ExcerptRange, ToPoint as _};
use parking_lot::Mutex;
@@ -82,7 +82,7 @@ pub enum BlockStyle {
}
pub struct BlockContext<'a, 'b> {
pub context: &'b mut ElementContext<'a>,
pub context: &'b mut WindowContext<'a>,
pub anchor_x: Pixels,
pub max_width: Pixels,
pub gutter_dimensions: &'b GutterDimensions,
@@ -934,7 +934,7 @@ impl BlockDisposition {
}
impl<'a> Deref for BlockContext<'a, '_> {
type Target = ElementContext<'a>;
type Target = WindowContext<'a>;
fn deref(&self) -> &Self::Target {
self.context

View File

@@ -13,6 +13,7 @@
//!
//! If you're looking to improve Vim mode, you should check out Vim crate that wraps Editor and overrides its behaviour.
pub mod actions;
mod blame_entry_tooltip;
mod blink_manager;
pub mod display_map;
mod editor_settings;
@@ -60,13 +61,13 @@ use fuzzy::{StringMatch, StringMatchCandidate};
use git::blame::GitBlame;
use git::diff_hunk_to_display;
use gpui::{
div, impl_actions, point, prelude::*, px, relative, rems, size, uniform_list, Action,
AnyElement, AppContext, AsyncWindowContext, AvailableSpace, BackgroundExecutor, Bounds,
ClipboardItem, Context, DispatchPhase, ElementId, EventEmitter, FocusHandle, FocusableView,
FontId, FontStyle, FontWeight, HighlightStyle, Hsla, InteractiveText, KeyContext, Model,
MouseButton, PaintQuad, ParentElement, Pixels, Render, SharedString, Size, StrikethroughStyle,
Styled, StyledText, Subscription, Task, TextStyle, UnderlineStyle, UniformListScrollHandle,
View, ViewContext, ViewInputHandler, VisualContext, WeakView, WhiteSpace, WindowContext,
div, impl_actions, point, prelude::*, px, relative, size, uniform_list, Action, AnyElement,
AppContext, AsyncWindowContext, AvailableSpace, BackgroundExecutor, Bounds, ClipboardItem,
Context, DispatchPhase, ElementId, EventEmitter, FocusHandle, FocusableView, FontId, FontStyle,
FontWeight, HighlightStyle, Hsla, InteractiveText, KeyContext, Model, MouseButton, PaintQuad,
ParentElement, Pixels, Render, SharedString, Size, StrikethroughStyle, Styled, StyledText,
Subscription, Task, TextStyle, UnderlineStyle, UniformListScrollHandle, View, ViewContext,
ViewInputHandler, VisualContext, WeakView, WhiteSpace, WindowContext,
};
use highlight_matching_bracket::refresh_matching_bracket_highlights;
use hover_popover::{hide_hover, HoverState};
@@ -129,15 +130,15 @@ use ui::{
Tooltip,
};
use util::{defer, maybe, post_inc, RangeExt, ResultExt, TryFutureExt};
use workspace::item::ItemHandle;
use workspace::notifications::NotificationId;
use workspace::Toast;
use workspace::{
searchable::SearchEvent, ItemNavHistory, SplitDirection, ViewId, Workspace, WorkspaceId,
item::ItemHandle, notifications::NotificationId, searchable::SearchEvent, ItemNavHistory,
OpenInTerminal, OpenTerminal, SplitDirection, TabBarPlacement, TabBarSettings, Toast, ViewId,
Workspace, WorkspaceId,
};
use crate::hover_links::find_url;
pub const DEFAULT_MULTIBUFFER_CONTEXT: u32 = 2;
const CURSOR_BLINK_INTERVAL: Duration = Duration::from_millis(500);
const MAX_LINE_LEN: usize = 1024;
const MIN_NAVIGATION_HISTORY_ROW_DELTA: i64 = 10;
@@ -417,6 +418,7 @@ pub struct Editor {
hovered_cursors: HashMap<HoveredCursor, Task<()>>,
pub show_local_selections: bool,
mode: EditorMode,
tab_bar_placement: TabBarPlacement,
show_breadcrumbs: bool,
show_gutter: bool,
show_wrap_guides: Option<bool>,
@@ -466,6 +468,7 @@ pub struct Editor {
show_git_blame_gutter: bool,
show_git_blame_inline: bool,
show_git_blame_inline_delay_task: Option<Task<()>>,
git_blame_inline_enabled: bool,
blame: Option<Model<GitBlame>>,
blame_subscription: Option<Subscription>,
custom_context_menu: Option<
@@ -1450,6 +1453,7 @@ impl Editor {
blink_manager: blink_manager.clone(),
show_local_selections: true,
mode,
tab_bar_placement: TabBarSettings::get_global(cx).placement,
show_breadcrumbs: EditorSettings::get_global(cx).toolbar.breadcrumbs,
show_gutter: mode == EditorMode::Full,
show_wrap_guides: None,
@@ -1503,6 +1507,7 @@ impl Editor {
show_git_blame_gutter: false,
show_git_blame_inline: false,
show_git_blame_inline_delay_task: None,
git_blame_inline_enabled: ProjectSettings::get_global(cx).git.inline_blame_enabled(),
blame: None,
blame_subscription: None,
_subscriptions: vec![
@@ -1535,7 +1540,8 @@ impl Editor {
let should_auto_hide_scrollbars = cx.should_auto_hide_scrollbars();
cx.set_global(ScrollbarAutoHide(should_auto_hide_scrollbars));
if ProjectSettings::get_global(cx).git.inline_blame_enabled() {
if this.git_blame_inline_enabled {
this.git_blame_inline_enabled = true;
this.start_git_blame_inline(false, cx);
}
}
@@ -1545,7 +1551,7 @@ impl Editor {
}
fn key_context(&self, cx: &AppContext) -> KeyContext {
let mut key_context = KeyContext::default();
let mut key_context = KeyContext::new_with_defaults();
key_context.add("Editor");
let mode = match self.mode {
EditorMode::SingleLine => "single_line",
@@ -1824,6 +1830,29 @@ impl Editor {
old_cursor_position: &Anchor,
cx: &mut ViewContext<Self>,
) {
// Copy selections to primary selection buffer
#[cfg(target_os = "linux")]
if local {
let selections = self.selections.all::<usize>(cx);
let buffer_handle = self.buffer.read(cx).read(cx);
let mut text = String::new();
for (index, selection) in selections.iter().enumerate() {
let text_for_selection = buffer_handle
.text_for_range(selection.start..selection.end)
.collect::<String>();
text.push_str(&text_for_selection);
if index != selections.len() - 1 {
text.push('\n');
}
}
if !text.is_empty() {
cx.write_to_primary(ClipboardItem::new(text));
}
}
if self.focus_handle.is_focused(cx) && self.leader_peer_id.is_none() {
self.buffer.update(cx, |buffer, cx| {
buffer.set_active_selections(
@@ -1920,7 +1949,9 @@ impl Editor {
self.refresh_document_highlights(cx);
refresh_matching_bracket_highlights(self, cx);
self.discard_inline_completion(cx);
self.start_inline_blame_timer(cx);
if self.git_blame_inline_enabled {
self.start_inline_blame_timer(cx);
}
}
self.blink_manager.update(cx, BlinkManager::pause_blinking);
@@ -3738,7 +3769,7 @@ impl Editor {
buffer
.edited_ranges_for_transaction::<usize>(transaction)
.collect(),
1,
DEFAULT_MULTIBUFFER_CONTEXT,
cx,
),
);
@@ -4914,6 +4945,25 @@ impl Editor {
}
}
pub fn open_active_item_in_terminal(&mut self, _: &OpenInTerminal, cx: &mut ViewContext<Self>) {
if let Some(working_directory) = self.active_excerpt(cx).and_then(|(_, buffer, _)| {
let project_path = buffer.read(cx).project_path(cx)?;
let project = self.project.as_ref()?.read(cx);
let entry = project.entry_for_path(&project_path, cx)?;
let abs_path = project.absolute_path(&project_path, cx)?;
let parent = if entry.is_symlink {
abs_path.canonicalize().ok()?
} else {
abs_path
}
.parent()?
.to_path_buf();
Some(parent)
}) {
cx.dispatch_action(OpenTerminal { working_directory }.boxed_clone());
}
}
fn gather_revert_changes(
&mut self,
selections: &[Selection<Anchor>],
@@ -7413,6 +7463,28 @@ impl Editor {
self.selection_history.mode = SelectionHistoryMode::Normal;
}
pub fn expand_excerpts(&mut self, action: &ExpandExcerpts, cx: &mut ViewContext<Self>) {
let selections = self.selections.disjoint_anchors();
let lines = if action.lines == 0 { 3 } else { action.lines };
self.buffer.update(cx, |buffer, cx| {
buffer.expand_excerpts(
selections
.into_iter()
.map(|selection| selection.head().excerpt_id)
.dedup(),
lines,
cx,
)
})
}
pub fn expand_excerpt(&mut self, excerpt: ExcerptId, cx: &mut ViewContext<Self>) {
self.buffer
.update(cx, |buffer, cx| buffer.expand_excerpts([excerpt], 3, cx))
}
fn go_to_diagnostic(&mut self, _: &GoToDiagnostic, cx: &mut ViewContext<Self>) {
self.go_to_diagnostic_impl(Direction::Next, cx)
}
@@ -7674,7 +7746,13 @@ impl Editor {
.update(&mut cx, |editor, cx| {
editor.navigate_to_hover_links(
Some(kind),
definitions.into_iter().map(HoverLink::Text).collect(),
definitions
.into_iter()
.filter(|location| {
hover_links::exclude_link_to_position(&buffer, &head, location, cx)
})
.map(HoverLink::Text)
.collect::<Vec<_>>(),
split,
cx,
)
@@ -8007,7 +8085,7 @@ impl Editor {
ranges_to_highlight.extend(multibuffer.push_excerpts_with_context_lines(
location.buffer.clone(),
ranges_for_buffer,
1,
DEFAULT_MULTIBUFFER_CONTEXT,
cx,
))
}
@@ -8046,7 +8124,7 @@ impl Editor {
.buffer
.read(cx)
.text_anchor_for_position(selection.head(), cx)?;
let (tail_buffer, _) = self
let (tail_buffer, cursor_buffer_position_end) = self
.buffer
.read(cx)
.text_anchor_for_position(selection.tail(), cx)?;
@@ -8056,6 +8134,7 @@ impl Editor {
let snapshot = cursor_buffer.read(cx).snapshot();
let cursor_buffer_offset = cursor_buffer_position.to_offset(&snapshot);
let cursor_buffer_offset_end = cursor_buffer_position_end.to_offset(&snapshot);
let prepare_rename = project.update(cx, |project, cx| {
project.prepare_rename(cursor_buffer.clone(), cursor_buffer_offset, cx)
});
@@ -8084,6 +8163,8 @@ impl Editor {
let rename_buffer_range = rename_range.to_offset(&snapshot);
let cursor_offset_in_rename_range =
cursor_buffer_offset.saturating_sub(rename_buffer_range.start);
let cursor_offset_in_rename_range_end =
cursor_buffer_offset_end.saturating_sub(rename_buffer_range.start);
this.take_rename(false, cx);
let buffer = this.buffer.read(cx).read(cx);
@@ -8112,7 +8193,27 @@ impl Editor {
editor.buffer.update(cx, |buffer, cx| {
buffer.edit([(0..0, old_name.clone())], None, cx)
});
editor.select_all(&SelectAll, cx);
let rename_selection_range = match cursor_offset_in_rename_range
.cmp(&cursor_offset_in_rename_range_end)
{
Ordering::Equal => {
editor.select_all(&SelectAll, cx);
return editor;
}
Ordering::Less => {
cursor_offset_in_rename_range..cursor_offset_in_rename_range_end
}
Ordering::Greater => {
cursor_offset_in_rename_range_end..cursor_offset_in_rename_range
}
};
if rename_selection_range.end > old_name.len() {
editor.select_all(&SelectAll, cx);
} else {
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
s.select_ranges([rename_selection_range]);
});
}
editor
});
@@ -8795,7 +8896,6 @@ impl Editor {
self.style = Some(style);
}
#[cfg(any(test, feature = "test-support"))]
pub fn style(&self) -> Option<&EditorStyle> {
self.style.as_ref()
}
@@ -8883,12 +8983,20 @@ impl Editor {
cx.notify();
}
pub fn git_blame_inline_enabled(&self) -> bool {
self.git_blame_inline_enabled
}
fn start_git_blame(&mut self, user_triggered: bool, cx: &mut ViewContext<Self>) {
if let Some(project) = self.project.as_ref() {
let Some(buffer) = self.buffer().read(cx).as_singleton() else {
return;
};
if buffer.read(cx).file().is_none() {
return;
}
let project = project.clone();
let blame = cx.new_model(|cx| GitBlame::new(buffer, project, user_triggered, cx));
self.blame_subscription = Some(cx.observe(&blame, |_, _, cx| cx.notify()));
@@ -8901,10 +9009,12 @@ impl Editor {
user_triggered: bool,
cx: &mut ViewContext<Self>,
) {
if self.show_git_blame_inline || self.show_git_blame_inline_delay_task.is_some() {
if self.git_blame_inline_enabled {
self.git_blame_inline_enabled = false;
self.show_git_blame_inline = false;
self.show_git_blame_inline_delay_task.take();
} else {
self.git_blame_inline_enabled = true;
self.start_git_blame_inline(user_triggered, cx);
}
@@ -8912,16 +9022,16 @@ impl Editor {
}
fn start_git_blame_inline(&mut self, user_triggered: bool, cx: &mut ViewContext<Self>) {
if let Some(inline_blame_settings) = ProjectSettings::get_global(cx).git.inline_blame {
if inline_blame_settings.enabled {
self.start_git_blame(user_triggered, cx);
self.start_git_blame(user_triggered, cx);
if inline_blame_settings.delay_ms.is_some() {
self.start_inline_blame_timer(cx);
} else {
self.show_git_blame_inline = true
}
}
if ProjectSettings::get_global(cx)
.git
.inline_blame_delay()
.is_some()
{
self.start_inline_blame_timer(cx);
} else {
self.show_git_blame_inline = true
}
}
@@ -8934,7 +9044,7 @@ impl Editor {
}
pub fn render_git_blame_inline(&mut self, cx: &mut WindowContext) -> bool {
self.show_git_blame_inline && self.has_blame_entries(cx)
self.focus_handle.is_focused(cx) && self.show_git_blame_inline && self.has_blame_entries(cx)
}
fn has_blame_entries(&self, cx: &mut WindowContext) -> bool {
@@ -9511,10 +9621,11 @@ impl Editor {
let editor_settings = EditorSettings::get_global(cx);
self.scroll_manager.vertical_scroll_margin = editor_settings.vertical_scroll_margin;
self.show_breadcrumbs = editor_settings.toolbar.breadcrumbs;
self.tab_bar_placement = TabBarSettings::get_global(cx).placement;
if self.mode == EditorMode::Full {
let inline_blame_enabled = ProjectSettings::get_global(cx).git.inline_blame_enabled();
if self.show_git_blame_inline != inline_blame_enabled {
if self.git_blame_inline_enabled != inline_blame_enabled {
self.toggle_git_blame_inline_internal(false, cx);
}
}
@@ -10226,11 +10337,12 @@ impl FocusableView for Editor {
impl Render for Editor {
fn render<'a>(&mut self, cx: &mut ViewContext<'a, Self>) -> impl IntoElement {
let settings = ThemeSettings::get_global(cx);
let text_style = match self.mode {
EditorMode::SingleLine | EditorMode::AutoHeight { .. } => TextStyle {
color: cx.theme().colors().editor_foreground,
font_family: settings.ui_font.family.clone(),
font_features: settings.ui_font.features,
font_features: settings.ui_font.features.clone(),
font_size: rems(0.875).into(),
font_weight: FontWeight::NORMAL,
font_style: FontStyle::Normal,
@@ -10240,11 +10352,10 @@ impl Render for Editor {
strikethrough: None,
white_space: WhiteSpace::Normal,
},
EditorMode::Full => TextStyle {
color: cx.theme().colors().editor_foreground,
font_family: settings.buffer_font.family.clone(),
font_features: settings.buffer_font.features,
font_features: settings.buffer_font.features.clone(),
font_size: settings.buffer_font_size(cx).into(),
font_weight: FontWeight::NORMAL,
font_style: FontStyle::Normal,
@@ -10669,7 +10780,7 @@ pub fn diagnostic_block_renderer(diagnostic: Diagnostic, _is_valid: bool) -> Ren
let theme_settings = ThemeSettings::get_global(cx);
text_style.font_family = theme_settings.buffer_font.family.clone();
text_style.font_style = theme_settings.buffer_font.style;
text_style.font_features = theme_settings.buffer_font.features;
text_style.font_features = theme_settings.buffer_font.features.clone();
text_style.font_weight = theme_settings.buffer_font.weight;
let multi_line_diagnostic = diagnostic.message.contains('\n');
@@ -10705,7 +10816,7 @@ pub fn diagnostic_block_renderer(diagnostic: Diagnostic, _is_valid: bool) -> Ren
let icon_size = buttons(&diagnostic, cx.block_id)
.into_any_element()
.measure(AvailableSpace::min_size(), cx);
.layout_as_root(AvailableSpace::min_size(), cx);
h_flex()
.id(cx.block_id)

View File

@@ -58,8 +58,8 @@ pub struct Toolbar {
pub struct Scrollbar {
pub show: ShowScrollbar,
pub git_diff: bool,
pub selections: bool,
pub symbols_selections: bool,
pub selected_symbol: bool,
pub search_results: bool,
pub diagnostics: bool,
}
@@ -194,14 +194,14 @@ pub struct ScrollbarContent {
///
/// Default: true
pub git_diff: Option<bool>,
/// Whether to show buffer search result markers in the scrollbar.
/// Whether to show buffer search result indicators in the scrollbar.
///
/// Default: true
pub selections: Option<bool>,
/// Whether to show symbols highlighted markers in the scrollbar.
pub search_results: Option<bool>,
/// Whether to show selected symbol occurrences in the scrollbar.
///
/// Default: true
pub symbols_selections: Option<bool>,
pub selected_symbol: Option<bool>,
/// Whether to show diagnostic indicators in the scrollbar.
///
/// Default: true

File diff suppressed because it is too large Load Diff

View File

@@ -1,9 +1,12 @@
use std::sync::Arc;
use std::{sync::Arc, time::Duration};
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,
};
use gpui::{Model, ModelContext, Subscription, Task};
@@ -12,6 +15,7 @@ use project::{Item, Project};
use smallvec::SmallVec;
use sum_tree::SumTree;
use url::Url;
use util::http::HttpClient;
#[derive(Clone, Debug, Default)]
pub struct GitBlameEntry {
@@ -46,11 +50,34 @@ impl<'a> sum_tree::Dimension<'a, GitBlameEntrySummary> for u32 {
}
}
#[derive(Clone, Debug)]
pub struct GitRemote {
pub host: HostingProvider,
pub owner: String,
pub repo: String,
}
impl GitRemote {
pub fn host_supports_avatars(&self) -> bool {
self.host.supports_avatars()
}
pub async fn avatar_url(&self, commit: Oid, client: Arc<dyn HttpClient>) -> Option<Url> {
self.host
.commit_author_avatar_url(&self.owner, &self.repo, commit, client)
.await
.ok()
.flatten()
}
}
#[derive(Clone, Debug)]
pub struct CommitDetails {
pub message: String,
pub parsed_message: ParsedMarkdown,
pub permalink: Option<Url>,
pub pull_request: Option<PullRequest>,
pub remote: Option<GitRemote>,
}
pub struct GitBlame {
@@ -63,7 +90,8 @@ pub struct GitBlame {
task: Task<Result<()>>,
generated: bool,
user_triggered: bool,
_refresh_subscription: Subscription,
regenerate_on_edit_task: Task<Result<()>>,
_regenerate_subscriptions: Vec<Subscription>,
}
impl GitBlame {
@@ -81,7 +109,19 @@ impl GitBlame {
&(),
);
let refresh_subscription = cx.subscribe(&project, {
let buffer_subscriptions = cx.subscribe(&buffer, |this, buffer, event, cx| match event {
language::Event::DirtyChanged => {
if !buffer.read(cx).is_dirty() {
this.generate(cx);
}
}
language::Event::Edited => {
this.regenerate_on_edit(cx);
}
_ => {}
});
let project_subscription = cx.subscribe(&project, {
let buffer = buffer.clone();
move |this, _, event, cx| match event {
@@ -116,7 +156,8 @@ impl GitBlame {
commit_details: HashMap::default(),
task: Task::ready(Ok(())),
generated: false,
_refresh_subscription: refresh_subscription,
regenerate_on_edit_task: Task::ready(Ok(())),
_regenerate_subscriptions: vec![buffer_subscriptions, project_subscription],
};
this.generate(cx);
this
@@ -272,11 +313,13 @@ impl GitBlame {
entries,
permalinks,
messages,
remote_url,
} = blame.await?;
let entries = build_blame_entry_sum_tree(entries, snapshot.max_point().row);
let commit_details =
parse_commit_messages(messages, &permalinks, &languages).await;
parse_commit_messages(messages, remote_url, &permalinks, &languages)
.await;
anyhow::Ok((entries, commit_details))
}
@@ -310,8 +353,22 @@ impl GitBlame {
})
});
}
fn regenerate_on_edit(&mut self, cx: &mut ModelContext<Self>) {
self.regenerate_on_edit_task = cx.spawn(|this, mut cx| async move {
cx.background_executor()
.timer(REGENERATE_ON_EDIT_DEBOUNCE_INTERVAL)
.await;
this.update(&mut cx, |this, cx| {
this.generate(cx);
})
})
}
}
const REGENERATE_ON_EDIT_DEBOUNCE_INTERVAL: Duration = Duration::from_secs(2);
fn build_blame_entry_sum_tree(entries: Vec<BlameEntry>, max_row: u32) -> SumTree<GitBlameEntry> {
let mut current_row = 0;
let mut entries = SumTree::from_iter(
@@ -351,13 +408,41 @@ fn build_blame_entry_sum_tree(entries: Vec<BlameEntry>, max_row: u32) -> SumTree
async fn parse_commit_messages(
messages: impl IntoIterator<Item = (Oid, String)>,
permalinks: &HashMap<Oid, Url>,
remote_url: Option<String>,
deprecated_permalinks: &HashMap<Oid, Url>,
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);
for (oid, message) in messages {
let parsed_message = parse_markdown(&message, &languages).await;
let permalink = permalinks.get(&oid).cloned();
let permalink = if let Some(git_remote) = parsed_remote_url.as_ref() {
Some(build_commit_permalink(
git::permalink::BuildCommitPermalinkParams {
remote: git_remote,
sha: oid.to_string().as_str(),
},
))
} else {
// DEPRECATED (18 Apr 24): Sending permalinks over the wire is deprecated. Clients
// now do the parsing. This is here for backwards compatibility, so that
// when an old peer sends a client no `parsed_remote_url` but `deprecated_permalinks`,
// we fall back to that.
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 pull_request = parsed_remote_url
.as_ref()
.and_then(|remote| extract_pull_request(remote, &message));
commit_details.insert(
oid,
@@ -365,6 +450,8 @@ async fn parse_commit_messages(
message,
parsed_message,
permalink,
remote,
pull_request,
},
);
}

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