Compare commits

...

142 Commits

Author SHA1 Message Date
mgsloan@gmail.com
a03d59be7c Further progress on linux screen capture
In my testing this doesn't work on X11, frames aren't getting
captured. Might work on Wayland
2024-12-07 14:37:25 -07:00
mgsloan@gmail.com
42a125aa19 WIP use of scap for linux screencapture
Co-authored-by: Mikayla <mikayla@zed.dev>
2024-12-06 15:14:35 -07:00
mgsloan@gmail.com
3e0630e227 WIP plumbing preparing for Linux screen capture 2024-12-05 15:11:25 -08:00
Michael Sloan
6a4cd53fd8 Use LiveKit's Rust SDK on Linux while continue using Swift SDK on Mac (#21550)
Similar to #20826 but keeps the Swift implementation. There were quite a
few changes in the `call` crate, and so that code now has two variants.

Closes #13714

Release Notes:

- Added preliminary Linux support for voice chat and viewing
screenshares.

---------

Co-authored-by: Kirill Bulatov <mail4score@gmail.com>
Co-authored-by: Kirill Bulatov <kirill@zed.dev>
Co-authored-by: Mikayla <mikayla@zed.dev>
2024-12-05 15:06:17 -08:00
Danilo Leal
0511768b22 project panel: Use theme token for focused border color (#21593)
Closes https://github.com/zed-industries/zed/issues/12723

This PR makes the border color of a focused project panel item use the
`panel_focused_border` theme token. This allow theme makers to customize
that independently of the `text_accent` color, which was the one being
previously used.

### One Dark

| Before | After |
|--------|--------|
| <img width="800" alt="Screenshot 2024-12-05 at 18 37 00"
src="https://github.com/user-attachments/assets/8b21f1e3-1738-42ab-af30-ad7d589007c1">
| <img width="800" alt="Screenshot 2024-12-05 at 18 39 42"
src="https://github.com/user-attachments/assets/1a424765-a1b6-48eb-ae75-1ffba2b59da3">
|
| <img width="800" alt="Screenshot 2024-12-05 at 18 37 08"
src="https://github.com/user-attachments/assets/d1955cf2-e194-46a5-9518-dc3af7f70cfe">
| <img width="800" alt="Screenshot 2024-12-05 at 18 39 51"
src="https://github.com/user-attachments/assets/99413075-f021-4961-8f03-ad1b40503ea6">
|

### Gruvbox Hard

| Before | After |
|--------|--------|
| <img width="800" alt="Screenshot 2024-12-05 at 18 38 05"
src="https://github.com/user-attachments/assets/cf84ce75-ac8a-4cb6-aaab-81e02bfb4835">
| <img width="800" alt="Screenshot 2024-12-05 at 18 40 15"
src="https://github.com/user-attachments/assets/f62b815b-8bed-41d8-85a1-7091d04bfbd2">
|
| <img width="800" alt="Screenshot 2024-12-05 at 18 38 16"
src="https://github.com/user-attachments/assets/fb458fa2-6ce1-4af0-a7a6-83584f3e5ed0">
| <img width="800" alt="Screenshot 2024-12-05 at 18 39 57"
src="https://github.com/user-attachments/assets/8bf44fe6-7090-4ef0-8b56-b8aa2e1f314d">
|

Release Notes:

- N/A
2024-12-05 19:17:26 -03:00
Marshall Bowers
c8b3c4c6cd assistant2: Add ability to delete past threads (#21607)
This PR adds the ability to delete past threads in Assistant2.

Release Notes:

- N/A
2024-12-05 15:57:35 -05:00
Kirill Bulatov
1efd165ead Restore project diff test (#21606)
Restores a basic project diff test

Release Notes:

- N/A

---------

Co-authored-by: Cole Miller <cole@zed.dev>
2024-12-05 21:48:33 +02:00
Marshall Bowers
787c75cbda assistant2: Add thread history (#21599)
This PR adds support for thread history to the Assistant 2 panel.

We also now generate summaries for the threads.

<img width="986" alt="Screenshot 2024-12-05 at 12 56 53 PM"
src="https://github.com/user-attachments/assets/46cb1309-38a2-4ab9-9fcc-c1275d4b5f2c">

<img width="986" alt="Screenshot 2024-12-05 at 12 56 58 PM"
src="https://github.com/user-attachments/assets/8c91ba57-a6c5-4b88-be05-b22fb615ece5">

Release Notes:

- N/A

---------

Co-authored-by: Piotr <piotr@zed.dev>
2024-12-05 13:22:25 -05:00
Thorsten Ball
2d43ad12e6 git: Make worktrees work for bare git repositories (#21596)
Fixes #21210 by ensuring that Zed can open worktrees of bare git repositories.

Co-authored-by: Peter Tripp <peter@zed.dev>
2024-12-05 12:55:40 -05:00
Nils Koch
6ebd6c2893 Show error and warning indicators in tabs (#21383)
Closes #21179

Release Notes:

- Add setting to display error and warning indicators in tabs.

<img width="454" alt="demo_with_icons"
src="https://github.com/user-attachments/assets/6002b4d4-dca8-4e2a-842d-1df3e281fcd2">
<img width="454" alt="demo_without_icons"
src="https://github.com/user-attachments/assets/df4b67bd-1a6c-4354-847e-d7fea95c1b7e">
2024-12-05 11:43:04 -03:00
Cole Miller
92dea066dd Extend filtering of backtrace frames a bit (#21573)
Both rust_begin_unwind and _rust_begin_unwind appear in practice, not sure why.

Release Notes:

- N/A
2024-12-05 09:33:46 -05:00
Anthony Eid
7335f211fd Add Project Panel navigation actions in netrw mode (#20941)
Release Notes:

- Added "[ c" & "] c" To select prev/next git modified file within the
project panel
- Added "[ d" & "] d" To select prev/next file with diagnostics from an
LSP within the project panel
- Added "{" & "}" To select prev/next directory within the project panel

Note:

I wanted to extend project panel's functionality when netrw is active so
I added some shortcuts that I believe will be helpful for most users. I
tried to keep the default key mappings for the shortcuts inline with
Zed's vim mode.

## Selecting prev/next modified git file

https://github.com/user-attachments/assets/a9c057c7-1015-444f-b273-6d52ac54aa9c


## Selecting prev/next diagnostics 

https://github.com/user-attachments/assets/d1fb04ac-02c6-477c-b751-90a11bb42a78

## Selecting prev/next directories (Only works with visible directoires)

https://github.com/user-attachments/assets/9e96371e-105f-4fe9-bbf7-58f4a529f0dd
2024-12-05 14:07:13 +01:00
Kirill Bulatov
78fea0dd8e Defer is_staff check for the project_diff::Deploy action (#21582)
During workspace registration, it's too early to check for the
`is_staff` flag due to no connection being established yet.
As a compromise, allow the action to appear and be registered, but do
nothing for non-staff users.

Release Notes:

- N/A
2024-12-05 11:55:06 +02:00
tims
9487fffc55 Fix snippet completion will be trigger, when certain symbols are pressed (#21578)
Closes #21576

This issue is caused by the fuzzy matching for snippets I added
[here](https://github.com/zed-industries/zed/pull/21524). When
encountering symbols such as `:`, `(`, `.`, etc., the `last_word`
becomes empty, which results in an empty string being passed to
`fuzzy_match`, leading to the return of all templates.

This fix adds an early return when `last_word` is empty.

Release Notes:

- N/A
2024-12-05 09:01:35 +01:00
Cole Miller
b9c390c22e Revert "Open folds containing selections when jumping from multibuffer (#21433)" (#21566)
This reverts commit dc32ab25a0.

This has been causing panics, backing it out while figuring out what's
up.

Release Notes:

- N/A
2024-12-04 19:26:09 -05:00
renovate[bot]
31c976d8d9 Update Rust crate cargo_metadata to v0.19.1 (#21556)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [cargo_metadata](https://redirect.github.com/oli-obk/cargo_metadata) |
workspace.dependencies | patch | `0.19.0` -> `0.19.1` |

---

### Release Notes

<details>
<summary>oli-obk/cargo_metadata (cargo_metadata)</summary>

###
[`v0.19.1`](https://redirect.github.com/oli-obk/cargo_metadata/compare/0.19.0...0.19.1)

[Compare
Source](https://redirect.github.com/oli-obk/cargo_metadata/compare/0.19.0...0.19.1)

</details>

---

### Configuration

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

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

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

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

---

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

---

Release Notes:

- N/A

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

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-04 18:07:45 -05:00
renovate[bot]
5b169fa535 Update Rust crate anyhow to v1.0.94 (#21552)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [anyhow](https://redirect.github.com/dtolnay/anyhow) |
workspace.dependencies | patch | `1.0.93` -> `1.0.94` |

---

### Release Notes

<details>
<summary>dtolnay/anyhow (anyhow)</summary>

###
[`v1.0.94`](https://redirect.github.com/dtolnay/anyhow/releases/tag/1.0.94)

[Compare
Source](https://redirect.github.com/dtolnay/anyhow/compare/1.0.93...1.0.94)

-   Documentation improvements

</details>

---

### Configuration

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

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

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

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

---

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

---

Release Notes:

- N/A

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

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-04 18:04:24 -05:00
Max Brunsfeld
a2115e7242 Restructure git diff state management to allow viewing buffers with different diff bases (#21258)
This is a pure refactor of our Git diff state management. Buffers are no
longer are associated with one single diff (the unstaged changes).
Instead, there is an explicit project API for retrieving a buffer's
unstaged changes, and the `Editor` view layer is responsible for
choosing what diff to associate with a buffer.

The reason for this change is that we'll soon want to add multiple "git
diff views" to Zed, one of which will show the *uncommitted* changes for
a buffer. But that view will need to co-exist with other views of the
same buffer, which may want to show the unstaged changes.

### Todo

* [x] Get git gutter and git hunks working with new structure
* [x] Update editor tests to use new APIs
* [x] Update buffer tests
* [x] Restructure remoting/collab protocol
* [x] Update assertions about staged text in
`random_project_collaboration_tests`
* [x] Move buffer tests for git diff management to a new spot, using the
new APIs

Release Notes:

- N/A

---------

Co-authored-by: Richard <richard@zed.dev>
Co-authored-by: Cole <cole@zed.dev>
Co-authored-by: Conrad <conrad@zed.dev>
2024-12-04 15:02:33 -08:00
Marshall Bowers
31796171de assistant2: Sketch in context picker (#21560)
This PR sketches in a context picker into the message editor in
Assistant 2. Not functional yet.

<img width="1138" alt="Screenshot 2024-12-04 at 5 45 19 PM"
src="https://github.com/user-attachments/assets/053d6224-de76-4fde-914b-41fe835761eb">

Release Notes:

- N/A
2024-12-04 18:00:28 -05:00
Marshall Bowers
a30ea2fc68 assistant2: Factor out ActiveThread view (#21555)
This PR factors a new `ActiveThread` view out of the `AssistantPanel` to
group together the state that pertains solely to the active view.

There was a bunch of related state on the `AssistantPanel` pertaining to
the active thread that needed to be initialized/reset together and it
makes for a clearer narrative is this state is encapsulated in its own
view.

Release Notes:

- N/A
2024-12-04 16:39:39 -05:00
Kirill Bulatov
55ecb3c51b Regenerate completion labels on resolve (#21521)
Closes https://github.com/zed-industries/zed/issues/21516

Technically, this is an LSP violation from `vtsls`, but seems that it's
not going to be fixed adequately on that side, see
https://github.com/yioneko/vtsls/issues/213 for more context.
So, we have to accommodate at least for now.

Release Notes:

- Fixed completion item labels not being updated after the resolve for
non-LSP compliant servers
2024-12-04 23:37:24 +02:00
Kirill Bulatov
8d18dfa4c1 Add a prototype with a multi buffer having all project git changes (#21543)
Part of https://github.com/zed-industries/zed/issues/20925

This prototype is behind a feature flag and being merged to avoid
conflicts with further git-related resturctures.
To be a proper, public feature, this needs at least:
* showing deleted files
* better performance 
* randomized tests
* `TODO`s in the `project_diff.rs` file fixed

The good thing is, >90% of the changes are in the `project_diff.rs` file
only, have a basic test and already work on simple cases.

Release Notes:

- N/A

---------

Co-authored-by: Thorsten Ball <thorsten@zed.dev>
Co-authored-by: Cole Miller <cole@zed.dev>
2024-12-04 23:36:36 +02:00
Michael Sloan
f0fac41ca4 Add action editor::OpenContextMenu (#21494)
This addresses the editor context menu portion of #17819.

Release Notes:

- Added `editor::OpenContextMenu` action to open context menu at current
cursor position.
2024-12-04 14:13:50 -07:00
Marshall Bowers
0bde0f8e2f assistant2: Add ability to open past threads (#21548)
This PR adds the ability to open past threads in Assistant 2.

There are also some mocked threads in the history for testing purposes.

Release Notes:

- N/A
2024-12-04 14:35:44 -05:00
Conrad Irwin
44264ffedc Revert accidental change to Rust outline files (#21545)
Release Notes:

- Preview only: Fixed impl blocks in the rust outline view
2024-12-04 11:58:56 -07:00
Marshall Bowers
7cfc972df6 assistant2: Add empty state for new threads (#21542)
This PR adds an empty state for new threads in Assistant2:

<img width="1138" alt="Screenshot 2024-12-04 at 12 17 46 PM"
src="https://github.com/user-attachments/assets/ff7b4533-d3b8-4722-bd4b-43fac6d35a77">

This is mostly just a sketch in its current state.

Release Notes:

- N/A
2024-12-04 12:44:03 -05:00
Stanislav Alekseev
fee0624299 Force code actions to be single line (#21409)
Addresses #21403 partially. Is consistent with the behaviour in VSCode

Before:
<img width="332" alt="391571084-1bef4ef9-b8f5-4c8f-9a32-9c0ab6c91af1"
src="https://github.com/user-attachments/assets/d4d83826-23a1-43a1-94f9-feb0b0ddd5ce">

After:
<img width="330" alt="Screenshot 2024-12-02 at 18 35 11"
src="https://github.com/user-attachments/assets/c04f0494-4f34-476a-a090-1443d61851e5">

Release Notes:

- Fixed an issue with multiline code actions' rendering by forcing them
to be single line
2024-12-04 18:39:23 +01:00
Peter Tripp
cf781dff71 v0.166.x dev 2024-12-04 12:01:28 -05:00
Piotr Osiewicz
706372fe4e title_bar: Add show_user_picture setting to let users hide their profile picture (#21526)
Fixes #21464

Closes #21464

Release Notes:

- Added `show_user_picture` setting (default: true) to allow users to
hide their profile picture in titlebar.
2024-12-04 17:59:27 +01:00
Vedant Matanhelia
5948ea217b Configure Highlight settings on yank vim (#21479)
Release Notes:

- Add settings / config variables to control `highlight_on_yank` or
`highlight_on_copy`

---------

Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
2024-12-04 09:23:31 -07:00
Marshall Bowers
207eb51df1 assistant2: Style inline code in Markdown (#21536)
This PR adds some styling for inline code within the messages to
differentiate them from the surrounding text:

<img width="1138" alt="Screenshot 2024-12-04 at 10 58 14 AM"
src="https://github.com/user-attachments/assets/3bb92711-e2f7-454a-b4be-449c6a9bf591">


Release Notes:

- N/A
2024-12-04 11:14:35 -05:00
David Soria Parra
0ee99c6d9c context_server: Add missing types for MCP spec to protocol 2024-11-05 (#21498)
This commit syncs missing types for the mcp spec 2024-11-05.

Release Notes:

- N/A
2024-12-04 10:45:25 -05:00
tims
d8732adfb2 Add fuzzy matching for snippets completions (#21524)
Closes #21439

This PR uses fuzzy matching for snippet completions instead of
fixed-prefix matching. This mimics the behavior of VSCode.

<img
src="https://github.com/user-attachments/assets/68537114-c5cf-4e4d-bc5c-4bb69ce947e5"
alt="fuzzy" width="450px" />

Release Notes:

- Improved suggestions for snippets.
2024-12-04 13:40:53 +01:00
Conrad Irwin
196fd65601 Fix panic folding in multi-buffers (#21511)
Closes #19054

Rename `max_buffer_row()` to `widest_line_number()` to (hopefully)
prevent
people assuming it means the same as `max_point().row`.

Release Notes:

- Fixed a panic when folding in a multibuffer
2024-12-04 00:01:32 -07:00
Conrad Irwin
e231321655 Fix panic in update_ime_position (#21510)
This can call back into the app, so must be done when the platform lock
is not
held.

Release Notes:

- Fixes a (rare) panic when changing tab
2024-12-03 23:20:25 -07:00
Waleed Dahshan
8f08787cf0 Implement Helix Support (WIP) (#19175)
Closes #4642 

- Added the ability to switch to helix normal mode, with an additional
helix visual mode.
- <kbd>ctrl</kbd><kbd>h</kbd> from Insert mode goes to Helix Normal
mode. <kbd> i </kbd> and <kbd> a </kbd> to go back.
- Need to find a way to perform the helix normal mode selection with
<kbd> w </kbd>, <kbd>e </kbd>, <kbd> b </kbd> as a first step. Need to
figure out how the mode will interoperate with the VIM mode as the new
additions are in the same crate.
2024-12-03 23:19:52 -07:00
Cole Miller
c5d15fd065 Add FoldFunctionBodies editor action (#21504)
Related to #19424

This uses the new text object support, so will only work for languages
that have `textobjects.scm`. It does not integrate with
indentation-based folding for now, and the syntax-based folds don't have
matching fold markers in the gutter (unless they are folded).

Release Notes:

- Add an editor action to fold all function bodies

Co-authored-by: Conrad <conrad@zed.dev>
2024-12-03 23:23:16 -05:00
Cole Miller
ce5f492404 Update rustls and sqlx (#21506)
Release Notes:

- N/A
2024-12-03 23:22:26 -05:00
Marshall Bowers
3019960f83 markdown: Make cx the last parameter to Markdown::new_text (#21497)
This PR is a follow-up to
https://github.com/zed-industries/zed/pull/21487 to make sure that the
`cx` is the last parameter to `Markdown::new_text` as well.

Release Notes:

- N/A
2024-12-03 18:39:00 -05:00
Marshall Bowers
9f459ba573 assistant2: Render messages as Markdown (#21496)
This PR updates Assistant 2 to render the messages in the thread as
Markdown:

<img width="1138" alt="Screenshot 2024-12-03 at 6 09 27 PM"
src="https://github.com/user-attachments/assets/c1c44fde-1efb-43cf-b9c9-768e6974c753">

Release Notes:

- N/A
2024-12-03 18:32:13 -05:00
Peter Tripp
ecaf44511c Fix Perplexity extension URL (#21495) 2024-12-03 18:28:59 -05:00
Cole Miller
dc32ab25a0 Open folds containing selections when jumping from multibuffer (#21433)
When searching within a single buffer, activating a search result causes
any fold containing the result to be unfolded. However, this didn't
happen when jumping to a search result from a project-wide search
multibuffer. This PR fixes that.

Release Notes:

- Fixed folds not opening when jumping from search results multibuffer
2024-12-03 17:14:17 -05:00
Marshall Bowers
aca23da971 assistant2: Render messages in the thread using a list (#21491)
This PR updates the rendering of the messages in the current thread to
use a `gpui::list`.

Release Notes:

- N/A
2024-12-03 16:25:09 -05:00
Conrad Irwin
db34f29300 vim: Add == and fix = in the status bar (#21490)
cc @maxbrunsfeld

Release Notes:

- vim: Add ==
2024-12-03 14:18:19 -07:00
Conrad Irwin
1fccda7b8d Add text objects to extensions (#21488)
Release Notes:

- Adds textobject support to erlang, haskell, lua, php, prisma, proto,
toml, and zig
2024-12-03 13:56:25 -07:00
Conrad Irwin
463c99b503 Fix script/get-released-version (#21489)
Release Notes:

- N/A
2024-12-03 13:56:01 -07:00
Marshall Bowers
88b0d3c78e markdown: Make cx the last parameter to the constructor (#21487)
I noticed that `Markdown::new` didn't have the `cx` as the final
parameter, as is conventional.

This PR fixes that.

Release Notes:

- N/A
2024-12-03 15:27:58 -05:00
Peter Tripp
165d50ff5b Add openbsd netcat to script/linux (#21478)
- Follow-up to: https://github.com/zed-industries/zed/pull/20751

openbsd-netcat is required for interactive SSH Remoting prompts
(password, passphrase, 2fa, etc).
2024-12-03 15:27:12 -05:00
Conrad Irwin
731e6d31f6 Revert "macos: Add default keybind for ctrl-home / ctrl-end (#21007)" (#21476)
This reverts commit 614b3b979b.

This conflicts with the macOS `ctrl-fn-left/right` bindings for moving
windows around (new in Sequoia).

If you want these use:
```
  {
    "context": "Editor",
    "bindings": {
      "ctrl-home": "editor::MoveToBeginning",
      "ctrl-end": "editor::MoveToEnd"
    }
  },
```

Release Notes:

- N/A
2024-12-03 13:10:02 -07:00
Conrad Irwin
b28287ce91 Fix panic in remove_item (#21480)
In #20742 we added a call to remove_item that retain an item index over
an
await point. This led to a race condition that could panic if another
tab was
removed during that time. (cc @mgsloan)

This changes the API to make it harder to misuse.

Release Notes:

- Fixed a panic when closing tabs containing new unsaved files
2024-12-03 13:09:53 -07:00
Conrad Irwin
492ca219d3 Fix panic in autoclosing (#21482)
Closes #14961

Release Notes:

- Fixed a panic when backspacing at the start of a buffer with
`always_treat_brackets_as_autoclosed` enabled.
2024-12-03 13:09:44 -07:00
Jason Lee
afb253b406 ui: Ensure Label with single_line set does not wrap (#21444)
Release Notes:

- N/A

---

Split from #21438, this change for make sure the `single_line` mode
Label will not be wrap.

---------

Co-authored-by: Marshall Bowers <elliott.codes@gmail.com>
2024-12-03 13:03:53 -05:00
Peter Tripp
41a973b13f Publish theme json schema v0.2.0 (#21428)
Fix theme json schema so `./script/import-themes print-schema` works again
Update schema to reflect current structs
([diff](https://gist.github.com/notpeter/26e6d0939985f542e8492458442ac62a/revisions?diff=unified&w=))

https://zed.dev/schema/themes/v0.2.0.json
2024-12-03 12:57:39 -05:00
Conrad Irwin
75c9dc179b Add textobjects queries (#20924)
Co-Authored-By: Max <max@zed.dev>

Release Notes:

- vim: Added motions `[[`, `[]`, `]]`, `][` for navigating by section,
`[m`, `]m`, `[M`, `]M` for navigating by method, and `[*`, `]*`, `[/`,
`]/` for comments. These currently only work for languages built in to
Zed, as they are powered by new tree-sitter queries.
- vim: Added new text objects: `ic`, `ac` for inside/around classes,
`if`,`af` for functions/methods, and `g c` for comments. These currently
only work for languages built in to Zed, as they are powered by new
tree-sitter queries.

---------

Co-authored-by: Max <max@zed.dev>
2024-12-03 10:37:01 -07:00
Conrad Irwin
c443307c19 Fix ctrl-alt-X shortcuts (#21473)
The macOS input handler assumes that you want to insert control
sequences when
you type ctrl-alt-X (you probably don't...).

Release Notes:

- (nightly only) fix ctrl-alt-X shortcuts
2024-12-03 10:26:19 -07:00
Peter Tripp
2dd5138988 docs: Add anchor links for language-specific settings (#21469) 2024-12-03 11:54:06 -05:00
Kirill Bulatov
a464474df0 Properly handle opening of file-less excerpts (#21465)
Follow-up of https://github.com/zed-industries/zed/pull/20491 and
https://github.com/zed-industries/zed/pull/20469
Closes https://github.com/zed-industries/zed/issues/21369

Release Notes:

- Fixed file-less excerpts always opening instead of activating
2024-12-03 18:41:36 +02:00
Kirill Bulatov
a0f2c0799e Debounce diagnostics status bar updates (#21463)
Closes https://github.com/zed-industries/zed/pull/20797

Release Notes:

- Fixed diagnostics status bar flashing when typing
2024-12-03 17:27:59 +02:00
Sebastian Nickels
1270ef3ea5 Enable toolchain venv in new terminals (#21388)
Fixes part of issue #7808 

> This venv should be the one we automatically activate when opening new
terminals, if the detect_venv setting is on.

Release Notes:

- Selected Python toolchains (virtual environments) are now automatically activated in new terminals.

---------

Co-authored-by: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com>
2024-12-03 16:24:30 +01:00
Danilo Leal
a76cd778c4 Disable hunk diff arrow buttons when there's only one hunk (#21437)
Closes https://github.com/zed-industries/zed/issues/20817

| One hunk | Multiple hunks |
|--------|--------|
| <img width="800" alt="Screenshot 2024-12-03 at 09 42 49"
src="https://github.com/user-attachments/assets/7c2ff80a-d4d9-4a74-84b8-891fadfd4e6c">
| <img width="800" alt="Screenshot 2024-12-02 at 23 36 38"
src="https://github.com/user-attachments/assets/60ea94b8-0b23-43a2-afad-b816b4645d1f">
|

Release Notes:

- Fixed showing prev/next hunk navigation buttons when there is only one
hunk
2024-12-03 10:07:59 -03:00
Jason Lee
a8c7e61021 Fix AI Context menu text wrapping causing overlap (#21438)
Closes https://github.com/zed-industries/zed/issues/20678

| Before | After |
| --- | --- |
| <img width="672" alt="SCR-20241203-jreb"
src="https://github.com/user-attachments/assets/411ba2a6-712f-4ab7-a320-12ac9a35c1e1">
| <img width="771" alt="SCR-20241203-jwhe"
src="https://github.com/user-attachments/assets/022c8ee9-4089-4c09-aa4b-12a0f5528822">
|

Release Notes:

- Fixed AI Context menu text wrapping causing overlap.

Also cc #21409 @WeetHet @osiewicz to use `Label`, this PR has been fixed
`Label` to ensure `whitespace_nowrap` when use `single_line`.

---------

Co-authored-by: Danilo Leal <daniloleal09@gmail.com>
2024-12-03 01:45:15 -03:00
Danilo Leal
2b143784da Improve audio files icon (#21441)
It took me a couple of minutes of staring at this speaker icon to figure
out it was a speaker! I even researched whether the `.wav` file type had
a specific icon, given I thought it was a specific triangle of sorts 😅
I'm sensing audio waves, at this size, will be easier to parse.

Release Notes:

- N/A
2024-12-03 00:40:46 -03:00
Cole Miller
b53b2c0376 Run dependency review for pull requests only (#21432)
This was an oversight in the original PR, dependency-review-action won't
work properly for `push` events
([example](https://github.com/zed-industries/zed/actions/runs/12130053580/job/33819624076)).

Release Notes:

- N/A
2024-12-02 19:39:18 -05:00
Cole Miller
e1c509e0de Check for vulnerable dependencies in CI (#21424)
This PR adds GitHub's dependency review action to CI, to flag PRs that
introduce new Cargo.lock entries for vulnerable crates according to the
GHSA database.

An alternative would be to run `cargo audit`, which checks against the
RustSec database. The state of synchronization between these two
databases seems a bit messy, but as far as I can tell GHSA has most
recent RustSec advisories on file, while RustSec is missing a larger
number of recent GHSA advisories.

The dependency review action should be smart enough not to flag PRs
because an untouched entry in Cargo.lock has a new advisory.

I've turned off the "license check" functionality since we have a
separate CI step for that.

Release Notes:

- N/A
2024-12-02 18:48:03 -05:00
Michael Sloan
f4dbcb6714 Use explicit sort order instead of comparison impls for gpui prims (#21430)
Found this while looking into adding support for the Surface primitive
on Linux, for rendering video shares. In that case it would be
expensive to compare images for equality. `Eq` and `PartialEq` were
being required but not used here due to use of `Ord` and `PartialOrd`.

Release Notes:

- N/A
2024-12-02 16:27:29 -07:00
Kyle Kelley
579bc8f015 Upgrade repl dependencies (#21431)
Bump dependencies for jupyter packages. cc @maxdeviant 

Release Notes:

- N/A
2024-12-02 15:22:03 -08:00
Max Brunsfeld
7c994cd4a5 Add AutoIndent action and '=' vim operator (#21427)
Release Notes:

- vim: Added the `=` operator, for auto-indent

Co-authored-by: Conrad <conrad@zed.dev>
2024-12-02 15:00:04 -08:00
Marshall Bowers
f3140f54d8 assistant2: Wire up error messages (#21426)
This PR wires up the error messages for Assistant 2 so that they are
shown to the user:

<img width="1138" alt="Screenshot 2024-12-02 at 4 28 02 PM"
src="https://github.com/user-attachments/assets/d8a5b9bd-0cef-4304-b561-b2edadbc70ef">
<img width="1138" alt="Screenshot 2024-12-02 at 4 29 09 PM"
src="https://github.com/user-attachments/assets/0dd70841-0d5a-4de6-bebe-82c563246b65">
<img width="1138" alt="Screenshot 2024-12-02 at 4 32 49 PM"
src="https://github.com/user-attachments/assets/a8838866-fad1-43a9-8935-490dc1936016">

@danilo-leal I kept the existing UX from Assistant 1, as I didn't see
any errors in the design prototype, but we can revisit if another
approach would work better.

Release Notes:

- N/A
2024-12-02 16:54:46 -05:00
yoleuh
72afe684b8 assistant: Use a smaller icon for the "New Chat" button (#21425)
Assistant new chat icon is slightly larger than editor pane new icon.

Changes:
Adds `IconSize::Small` to assistant default size new chat icon, not
really noticeable, but matches the new icon in editor pane, and the
assistant dropdown menu that have icon size small.

|old|new|
|---|---|

|![image](https://github.com/user-attachments/assets/cbef5054-a465-4957-9409-b4a73e703363)|![image](https://github.com/user-attachments/assets/baee66ea-76d6-43b4-a4b9-ead34991ff85)|

Release Notes:

- N/A
2024-12-02 16:48:20 -05:00
Piotr Osiewicz
59dc6cf523 toolchains: Run listing tasks on background thread (#21414)
Potentially fixes #21404

This is a speculative fix, as while I was trying to repro this issue
I've noticed that introducing artificial delays in ToolchainLister::list
could impact apps responsiveness. These delays were essentially there to
stimulate PET taking a while to find venvs.

Release Notes:

- Improved app responsiveness in environments with multiple Python
virtual environments
2024-12-02 21:03:31 +01:00
Marshall Bowers
b88daae67b assistant2: Add support for using tools provided by context servers (#21418)
This PR adds support to Assistant 2 for using tools provided by context
servers.

As part of this I introduced a new `ThreadStore`.

Release Notes:

- N/A

---------

Co-authored-by: Cole <cole@zed.dev>
2024-12-02 15:01:18 -05:00
Piotr Osiewicz
f32ffcf5bb workspace: Sanitize pinned tab count before usage (#21417)
Fixes all sorts of panics around usage of incorrect pinned tab count
that has been fixed in app itself, yet persists in user db.

Closes #ISSUE

Release Notes:

- N/A
2024-12-02 19:56:52 +01:00
Piotr Osiewicz
95a047c11b tasks/rust: Add support for running examples as binary targets (#21412)
Closes #21044

Release Notes:

- Added support for running Rust examples as tasks.
2024-12-02 19:53:51 +01:00
Kirill Bulatov
dbe41823d9 Use proper terminal item for splitting context (#21415)
Closes https://github.com/zed-industries/zed/issues/21411

Release Notes:

- N/A
2024-12-02 20:46:28 +02:00
Conrad Irwin
7c40824783 Fix macOS IME overlay positioning (#21416)
Release Notes:

- Improved positioning of macOS IME overlay

---------

Co-authored-by: Richard Feldman <richard@zed.dev>
2024-12-02 11:46:14 -07:00
Conrad Irwin
4e12f0580a Fix dismissing the IME viewer with escape (#21413)
Co-Authored-By: Richard Feldman <richard@zed.dev>

Closes #21392

Release Notes:

- Fixed dismissing the macOS IME menu with escape when no marked text
was present

---------

Co-authored-by: Richard Feldman <richard@zed.dev>
2024-12-02 11:20:27 -07:00
Danilo Leal
f795ce9623 Add language icons to the language selector (#21298)
Closes https://github.com/zed-industries/zed/issues/21290

This is a first attempt to show the language icons to the selector.
Ideally, I wouldn't like to have yet another place mapping extensions to
icons, as we already have the `file_types.json` file doing that, but I'm
not so sure how to pull from it yet. Maybe in a future pass we'll
improve this and make it more solid.

<img width="700" alt="Screenshot 2024-11-28 at 16 10 27"
src="https://github.com/user-attachments/assets/683c3bef-5389-470f-a41e-3d510b927b61">

Release Notes:

- N/A

---------

Co-authored-by: Kirill Bulatov <kirill@zed.dev>
Co-authored-by: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com>
2024-12-02 15:01:09 -03:00
uncenter
995b40f149 Add "Copy Extension ID" action to extension card dropdown (#21395)
Adds a new "Copy Extension ID" action to the dropdown of remote
extension cards in the extensions list UI. Would have liked for it to be
a context menu where you could click anywhere on the card, but couldn't
figure out how to integrate that with the existing setup.

I've been missing this from VSCode's extension panel, which allows this
on right click:

![CleanShot 2024-12-01 at 22 03
14](https://github.com/user-attachments/assets/64796f96-1a37-4ba2-bfe1-971b939aa50a)

This is useful if you, say, want to add some extensions to
https://zed.dev/docs/configuring-zed#auto-install-extensions, where you
need the IDs.

Release Notes:

- Added "Copy Extension ID" action to extension card dropdown

---------

Co-authored-by: Marshall Bowers <elliott.codes@gmail.com>
2024-12-02 11:19:42 -05:00
Piotr Osiewicz
89e46396f6 workspace: Serialize active panel even if it's not visible (#21408)
Fixes #21285

Closes #21285

Release Notes:

- Fixed workspace serialization of collapsed panels
2024-12-02 17:08:16 +01:00
Finn Evers
3987d0d731 Treat .pcss files as CSS (#21402)
This addresses
https://github.com/zed-industries/zed/pull/19416#discussion_r1865019293
and also follows the [associated PostCSS file extensions for VS
Code](5d003170c5/package.json (L37)).

Release Notes:

- `.pcss` files are now recognized as CSS

---------

Co-authored-by: Marshall Bowers <elliott.codes@gmail.com>
2024-12-02 10:56:47 -05:00
loczek
6cb758a1cd theme_importer: Add more mappings (#21393)
This PR adds `search_match_background` and `editor_document_highlight_bracket_background` color mappings as they appear to be missing.
2024-12-02 09:37:41 -05:00
Delyan Angelov
0cb3a6ed0e Add V file icon (#20017)
Here is a preview of the new `v.svg` in comparison with some of the
existing icons:

![image](https://github.com/user-attachments/assets/451762ff-b13a-42b9-89ac-695f25a43a84)

---------

Co-authored-by: Danilo Leal <daniloleal09@gmail.com>
2024-12-02 10:51:28 -03:00
Danilo Leal
2300f40cd9 Add consistent placeholder text for terminal inline assist (#21398)
Ensuring it is consistent with the buffer inline assistant. Just thought
of not having "Transform" here as that felt it made less sense for
terminal-related prompts, where arguably more frequently, one would be
suggesting for actual commands rather than code transformation.

<img width="700" alt="Screenshot 2024-12-02 at 09 11 00"
src="https://github.com/user-attachments/assets/ad96d03e-0366-46e8-8056-581066712d59">

Release Notes:

- N/A
2024-12-02 09:28:46 -03:00
Danilo Leal
dacd919e27 Add setting for making the tab's close button always visible (#21352)
Closes https://github.com/zed-industries/zed/issues/20422

<img width="700" alt="Screenshot 2024-11-29 at 22 00 20"
src="https://github.com/user-attachments/assets/4a17d00c-d64f-4b33-97a7-a57766ce6d17">

Release Notes:

- N/A
2024-12-02 07:48:10 -03:00
Danilo Leal
740ba7817b Fine-tune terminal tab bar actions spacing (#21391)
Just quickly reducing the spacing between the terminal tab bar actions
so they're tighter and matching other similar components.

| Before | After |
|--------|--------|
| <img width="800" alt="Screenshot 2024-12-01 at 19 20 50"
src="https://github.com/user-attachments/assets/938336df-9ce1-42d3-8f3d-9c26b8e88453">
| <img width="800" alt="Screenshot 2024-12-01 at 19 18 19"
src="https://github.com/user-attachments/assets/0a2b5915-f37c-4b8e-af2c-b8018c4750ab">
|

Release Notes:

- N/A
2024-12-02 07:47:57 -03:00
fred-sch
380679fcc2 Fix: Copilot Chat is logged out (#21360)
Closes #21255

Release Notes:

- Fixed Copilot Chat OAuth Token parsing

---------

Co-authored-by: Bennet Bo Fenner <bennet@zed.dev>
2024-12-02 10:35:29 +01:00
moskirathe
89a56968f6 Fix typos in key-bindings documentation (#21390)
Release Notes:

Fixes two minor typos in the key-bindings documentation.
2024-12-01 19:02:12 -03:00
Kirill Bulatov
5f6b200d8d Do not change selections when opening FS entries (#21382)
Follow-up of https://github.com/zed-industries/zed/pull/21375

When changing selections for FS entries, outline panel will be forced to
change item to the first excerpt which is not what we want.

Release Notes:

- N/A
2024-12-01 14:28:48 +02:00
yoleuh
4d5415273e Docs: Update developing zed docs to match (#21379)
Some changes just so the build docs for the different os matches each
other :)

macos:
- moved `rust wasm toolchain install` up under `rust install` (match
windows docs)
- add instructions to update rust if already installed (match windows
and linux docs)

windows:
- add `(required by a dependency)` to cmake install (match macos docs)

Release Notes:

- N/A
2024-12-01 10:59:29 +02:00
Kirill Bulatov
bf569d720e Always change editor selection when navigating outline panel entries (#21375)
Also scroll to the center when doing so.

This way, related editor's breadcrumbs always update, bringing more
information.

Release Notes:

- Adjust outline panel item opening behavior to always change the editor
selection, and center it
2024-12-01 01:49:41 +02:00
Kirill Bulatov
28849dd2a8 Fix item closing overly triggering save dialogues (#21374)
Closes https://github.com/zed-industries/zed/issues/12029

Allows to introspect project items inside items more deeply, checking
them for being dirty.
For that:
* renames `project::Item` into `project::ProjectItem`
* adds an `is_dirty(&self) -> bool` method to the renamed trait
* changes the closing logic to only care about dirty project items when
checking for save prompts conditions
* save prompts are raised only if the item is singleton without a
project path; or if the item has dirty project items that are not open
elsewhere

Release Notes:

- Fixed item closing overly triggering save dialogues
2024-12-01 01:48:31 +02:00
Agustin Gomes
c2cd84a749 Add musl-gcc as dependency (#21366)
This addition comes after attempting building Zed from source.

As part of the process, one of the components (a crate I presume) called
`ring` failed to compile due to the following sequence of console
messages:

```log
warning: ring@0.17.8: Compiler family detection failed due to error: ToolNotFound: Failed to find tool. Is `musl-gcc` installed?
warning: ring@0.17.8: Compiler family detection failed due to error: ToolNotFound: Failed to find tool. Is `musl-gcc` installed?

error: failed to run custom build command for `ring v0.17.8`
```

Adding this library should help fix the issue on Fedora 41 at least, and
possibly will help fixing it for other RedHat based distributions as
well.

Closes #ISSUE

Release Notes:

- Add musl-gcc as dependency

Signed-off-by: Agustin Gomes <me@agustingomes.com>
2024-11-30 13:20:31 -08:00
tims
d609931e1c linux: Fix mouse cursor size and blur on Wayland (#21373)
Closes #15788, #13258

This is a long-standing issue with a few previous attempts to fix it,
such as [this one](https://github.com/zed-industries/zed/pull/17496).
However, that fix was later reverted because it resolved the blur issue
but caused a size issue. Currently, both blur and size issues persist
when you set a custom cursor size from GNOME Settings and use fractional
scaling.

This PR addresses both issues.

---

### Context

A new Wayland protocol,
[cursor-shape-v1](https://gitlab.freedesktop.org/wayland/wayland-protocols/-/merge_requests/194),
allows the compositor to handle rendering the cursor at the correct size
and shape. This protocol is implemented by KDE, wlroots (Sway-like
environments), etc. Zed supports this protocol, so there are no issues
on these desktop environments.

However, GNOME has not yet
[adopted](https://gitlab.gnome.org/GNOME/gtk/-/merge_requests/6212) this
protocol. As a result, apps must fall back to manually rendering the
cursor by specifying the theme, size, scale, etc., themselves. Zed also
implements this fallback but does not correctly account for the display
scale.

---

### Scale Fix

For example, if your cursor size is `64px` and you’re using fractional
scaling (e.g., `150%`), the display scale reported by the window query
will be an integer value, `2` in this case. Why `2` if the scale is
`150%`? That’s what the new protocol aims to improve. However, since
GNOME Wayland uses this integer scale everywhere, it’s sufficient for
our use case.

To fix the issue, we set the `buffer_scale` to this value. But that
alone doesn’t solve the problem. We also need to generate a matching
theme cursor size for this scaled version. This can be calculated as
`64px` * `2`, resulting in `128px` as the theme cursor size.

---

### Size Fix

The XDG Desktop Portal’s `cursor-size` event fails to read the cursor
size because it expects an `i32` but encounters a type error with `u32`.
Due to this, the cursor size was interpreted as the default `24px`
instead of the actual size set via user.

---

### Tested

This fix has been tested with all possible combinations of the
following:

- [x] GNOME Normal Scale (100%, 200%, etc.)
- [x] GNOME Fractional Scaling (125%, 150%, etc.)
- [x] GNOME Cursor Sizes (**Settings > Accessibility > Seeing**, e.g.,
`24px`, `64px`, etc.)
- [x] GNOME Experimental Feature `scale-monitor-framebuffer` (both
enabled and disabled)
- [x] KDE (`cursor-shape-v1` protocol)

---

**Result:**

64px custom cursor size + 150% Fractional Scale:


https://github.com/user-attachments/assets/cf3b1a0f-9a25-45d0-ab03-75059d3305e7

---

Release Notes:

- Fixed mouse cursor size and blur issues on Wayland
2024-11-30 13:19:44 -08:00
Remco Smits
fd71801346 Improve JavaScript runnable detection followup (#21363)
Followup: https://github.com/zed-industries/zed/pull/21246

**Before**
<img width="545" alt="Screenshot 2024-11-30 at 13 27 15"
src="https://github.com/user-attachments/assets/3346e485-96c8-482d-b5fd-85b86f37d662">
**After**
<img width="537" alt="Screenshot 2024-11-30 at 13 27 36"
src="https://github.com/user-attachments/assets/3cedcaa5-e285-47fb-909d-16d37d9844ca">

We did not need to add the `*` as it was already matching one of them,
we actually need at least one of them, so making it optional was a
mistake.

Don't think we need to add release notes, as the change is only on main
the branch now.

Release Notes:

- N/A
2024-11-30 13:55:14 +01:00
Haru Kim
c1de606581 Fix the autoscroll_on_clicks setting working incorrectly (#21362) 2024-11-30 14:30:27 +02:00
Kirill Bulatov
57a45d80ad Add a keybinding to the Go to Line button (#21350)
Release Notes:

- N/A
2024-11-30 00:50:38 +02:00
tims
5f29f214c3 linux: Fix file not opening from file explorer (#21137)
Closes #20070

Release Notes:

- Fixed issue where files wouldn't open from the file explorer.
- Fixed "Open a new workspace" option on the desktop entry right-click
menu.

Context:

Zed consists of two binaries:

- `zed` (CLI component, located at `crates/cli/main.rs`)
- `zed-editor` (GUI component, located at `crates/zed/main.rs`)

When `zed` is used in the terminal, it checks if an existing instance is
running. If one is found, it sends data via a socket to open the
specified file. Otherwise, it launches a new instance of `zed-editor`.
For more details, see the `detect` and `boot_background` functions in
`crates/cli/main.rs`.

Root Cause:

Install process creates directories like `.local/zed.app` and
`.local/zed-preview.app`, which contain desktop entries for the
corresponding release. For example, `.local/zed.app/share/applications`
contains `zed.desktop`.

This desktop entry includes a generic `Exec` field, which is correct by
default:

```sh
Comment=A high-performance, multiplayer code editor.
TryExec=zed
StartupNotify=true
```

The issue is in the `install.sh` script. This script copies the above
desktop file to the common directory for desktop entries
(.local/share/applications). During this process, it replaces the
`TryExec` value from `zed` with the exact binary path to avoid relying
on the shell's PATH resolution and to make it explicit.

However, replacement incorrectly uses the path for `zed-editor` instead
of the `zed` CLI binary. This results in not opening a file as if you
use `zed-editor` directly to do this it will throw `zed is already
running` error on production and open new instance on dev.


Note: This PR solves it for new users. For existing users, they will
either have to update `.desktop` file manually, or use `install.sh`
script again. I'm not aware of zed auto-update method, if it runs
`install.sh` under the hood.
2024-11-29 23:01:29 +02:00
tims
4bf59393ec linux: Fix Zed not visible in "Open With" list in file manager for Flatpak (#21177)
- Closes #19030

When `%U` is used in desktop entries, file managers pick this and use
it:

- When you right-click a file and choose "Open with..."
- When you drag and drop files onto an application icon

<img
src="https://github.com/user-attachments/assets/ea5aa008-b81c-4f10-9302-b82332f6b174"
width="200px" alt="image">

Adding it to CLI args, changes Flatpak desktop entry `Exec` from:

```diff
- Exec=/usr/bin/flatpak run --branch=master --arch=x86_64 --command=zed dev.zed.ZedDev --foreground
+ Exec=/usr/bin/flatpak run --branch=master --arch=x86_64 --command=zed --file-forwarding dev.zed.ZedDev --foreground @@u %U @@
```

This is Flatpak's way of doing `%U`, by adding `--file-forwarding` and
wrapping arg with `@@u` and `@@`.
Read more below
([source](https://docs.flatpak.org/en/latest/flatpak-command-reference.html)):

> --file-forwarding
>
> If this option is specified, the remaining arguments are scanned, and
all arguments that are enclosed between a pair of '@@' arguments are
interpreted as file paths, exported in the document store, and passed to
the command in the form of the resulting document path. Arguments
between "@@u" and "@@" are considered URIs, and any "file:" URIs are
exported. The exports are non-persistent and with read and write
permissions for the application.

Release Notes:

- Fixed Zed not visible in the "Open with" list in the file manager for
Flatpak.
2024-11-29 22:59:04 +02:00
moshyfawn
aea6fa0c09 Remove project panel trash action for remote projects (#21300)
Closes #20845

I'm uncertain about my placement for the logic to remove actions from
the command palette list. If anyone has insights or alternative
approaches, I'm open to changing the code.

Release Notes:

- Removed project panel `Trash` action for remote projects.

---------

Co-authored-by: Finn Evers <dev@bahn.sh>
2024-11-29 22:37:24 +02:00
Danilo Leal
4137d1adb9 Make project search landing page scrollable if too small (#21338)
Address
https://github.com/zed-industries/zed/issues/21317#issuecomment-2508011556


https://github.com/user-attachments/assets/089844fc-a485-44a6-8e8b-d294f28e9ea2

Release Notes:

- N/A
2024-11-29 12:45:08 -03:00
Danilo Leal
1903a29cca Expose "Column Git Blame" in the editor controls menu (#21336)
Closes https://github.com/zed-industries/zed/issues/10196

I think having this action exposed in the editor controls menu, close to
the inline Git Blame option, makes more sense than a more prominent item
somewhere else in the app. Maybe having it there will increase its
discoverability. I myself didn't know this until a few weeks ago! Next
steps would be ensuring the menu exposes its keybindings.

(Quick note about the menu item name: I think maybe "_Git Blame Column_"
would make more sense and feel grammatically more correct, but then we
would have two Git Blame-related options, one with "Git Blame" at the
start (Inline...) and another with "Git Blame" at the end (... Column).
I guess one had to be sacrificed for the sake of consistency 😅.)

<img width="750" alt="Screenshot 2024-11-29 at 12 01 33"
src="https://github.com/user-attachments/assets/2f3324ec-a2f0-4303-9582-714d0ee6bd31">

Release Notes:

- N/A
2024-11-29 12:38:12 -03:00
Danilo Leal
69c761f5a5 Adjust project search landing page layout (#21332)
Closes https://github.com/zed-industries/zed/issues/21317


https://github.com/user-attachments/assets/a4970c08-9715-4c90-ad48-8f6e80c6fcd0

Release Notes:

- N/A
2024-11-29 11:39:02 -03:00
Kirill Bulatov
0306bdc695 Use a single action for toggling the language (#21331)
Follow-up of https://github.com/zed-industries/zed/pull/21299

Release Notes:

- N/A
2024-11-29 16:02:57 +02:00
yoleuh
de55bd8307 Status bar: Reduce right tools lateral margin (#21329)
Closes #21316

| Before | After |
|--------|-------|
|
![image](https://github.com/user-attachments/assets/525d16b0-c1f0-4d93-9a8e-19112b927e78)|
![image](https://github.com/user-attachments/assets/c6947c3e-6b46-4498-a672-5f418f5faad0)|

Changes:
changed `Base08` to `Base04` in `render_right_tools`

Release Notes:

- N/A
2024-11-29 10:56:32 -03:00
Kirill Bulatov
a593a04da4 Update the lockfile after a recent dependency update (#21328)
Follow-up of https://github.com/zed-industries/zed/pull/21288

Release Notes:

- N/A
2024-11-29 15:39:18 +02:00
наб
74f265e5cf Update to embed-resource 3.0 (fixes build below windows \?\ path) (#21288)
Accd'g to
https://github.com/zed-industries/zed/pull/9009#issuecomment-1983599232
the manifest is required

Followup for
https://github.com/nabijaczleweli/rust-embed-resource/issues/71

Release Notes:
- N/A
2024-11-29 14:43:40 +02:00
Haru Kim
f9d5de834a Disable editor autoscroll on mouse clicks (#20287)
Closes #18148

Release Notes:

- Stop scrolling when clicking to the edges of the visible text area.
Use `autoscroll_on_clicks` to configure this behavior.


https://github.com/user-attachments/assets/3afd5cbb-5957-4e39-94c6-cd2e927038fd

---------

Co-authored-by: Kirill Bulatov <mail4score@gmail.com>
2024-11-29 13:51:36 +02:00
Haru Kim
eadb107339 Allow workspace::ActivatePaneInDirection to navigate out of the terminal panel (#21313)
Enhancement for #21238

Release Notes:

- N/A
2024-11-29 13:04:58 +02:00
Stanislav Alekseev
94faf9dd56 nix: Return to building with crane (#21292)
This removes .envrc, putting it into gitignore as well as building with
crane, as it does not require an up to date hash for a FOD.

Release Notes:

- N/A

cc @mrnugget @jaredramirez
2024-11-29 10:09:33 +01:00
Kirill Bulatov
73f546ea5f Force ashpd crate to not use tokio (#21315)
https://github.com/zed-industries/zed/issues/21304

Fixes a regression after
https://github.com/zed-industries/zed/pull/20939

Release Notes:

- N/A
2024-11-29 11:02:56 +02:00
Danilo Leal
eb2c0b33df Fine-tune status bar left-side spacing (#21306)
Closes https://github.com/zed-industries/zed/issues/21291

This PR also adds a small divider separating the panel-opening controls
from the other items that appear on the left side of the status bar. The
spacing was a bit bigger before because all three items on the left open
panels, whereas each other item does different things (e.g., open the
diagnostics tab, update the app, display language server status, etc.).
Therefore, they needed to be separated somehow to communicate the
difference in behavior. Hopefully, now, the border will help sort of
figuring this out.

| With error | Normal state |
|--------|--------|
| <img width="1179" alt="Screenshot 2024-11-28 at 18 52 58"
src="https://github.com/user-attachments/assets/bf4bad19-5588-481a-9d08-91b2227e44e6">
| <img width="1234" alt="Screenshot 2024-11-28 at 18 53 03"
src="https://github.com/user-attachments/assets/4443a16a-9982-44ce-9005-64d4df46f4f0">
|

Release Notes:

- N/A
2024-11-28 19:15:30 -03:00
Danilo Leal
3458687300 Add keybinding to the language selector tooltip (#21299)
Just making sure sure we're always making keyboard navigation
discoverable.

<img width="700" alt="Screenshot 2024-11-28 at 16 05 40"
src="https://github.com/user-attachments/assets/bd7611f0-190c-4e3b-ad69-9552060e37ea">

Release Notes:

- N/A
2024-11-28 18:28:20 -03:00
Danilo Leal
e76589107d Improve the "go to line" modal (#21301)
Just a small, mostly visual refinement to this component.

<img width="700" alt="Screenshot 2024-11-28 at 16 30 27"
src="https://github.com/user-attachments/assets/d5bbed63-202c-4cd3-b4b0-b7ed23466309">

Release Notes:

- N/A
2024-11-28 18:28:05 -03:00
Danilo Leal
ae85ecba2d Make fetch slash command visible in the command selector (#21302)
The `/fetch` command is naturally already accessible via the completion
menu when you type / in the assistant panel, but it wasn't on the "Add
Context" command selector. I think it should! It's a super nice/powerful
one, and I've seen folks not knowing it existed. Side-note: maybe, in
the near future, it'd be best to rename it to "`/web`, as that's an
easier name to parse and assume what it does.

<img width="700" alt="Screenshot 2024-11-28 at 16 52 07"
src="https://github.com/user-attachments/assets/37134e1c-c788-48ca-81ae-d7416e8c8706">

Release Notes:

- N/A
2024-11-28 18:26:59 -03:00
Kirill Bulatov
0acd98a07e Do not show cursor position for empty files (#21295)
Closes https://github.com/zed-industries/zed/issues/21289

Fixes most of the issues: does not display cursor position in empty
multi buffers and on non-full editors.

Does not fix the startup issue, as it's caused by the AssistantPanel's
`ContextEditor` acting as an `Editor`, so whenever default prompts are
added, those are registered as added editors, and Zed shows some line
numbers for them.

We cannot replace `item.act_as::<Editor>(cx)` with
`item.downcast::<Editor>()` as then multi bufers' navigation will fall
off (arguably, those line numbers do not make that much sense too, but
still seem useful).
This will will fix itself in the future, when assistant panel gets
reworked into readonly view by default, as `assistant2` crate already
shows (there's no `act_as` impl there and nothing cause issue).

Since the remaining issue is minor and will go away on any focus change,
and future changes will alter this, closing the original issue.

Release Notes:

- Improved cursor position display
2024-11-28 20:42:57 +02:00
Matin Aniss
4a96db026c gpui: Implement hover for Windows (#20894) 2024-11-28 18:45:10 +02:00
Danilo Leal
301a8900a5 Add consistency between buffer and project search design (#20754)
Follow up to https://github.com/zed-industries/zed/pull/20242

This PR adds the `SearchInputWidth` util, which sets a threshold
container size in which an input's width stops filling the available
space. In practice, this is in place to make the buffer and project
search input fill the whole container width up to a certain point (where
this point is really an arbitrary number that can be fine-tuned per
taste). For folks using huge monitors, the UX isn't excellent if you
have a gigantic input.

In the future, upon further review, maybe it makes more sense to
reorganize this code better, baking it in as a default behavior of the
input component. Or even exposing this is a function many other
components could use, given we may want to have dynamic width in
different scenarios.

For now, I just wanted to make the design of these search UIs better and
more consistent.

| Buffer Search | Project Search |
|--------|--------|
| <img width="1042" alt="Screenshot 2024-11-15 at 20 39 21"
src="https://github.com/user-attachments/assets/f9cbf0b3-8c58-46d1-8380-e89cd9c89699">
| <img width="1042" alt="Screenshot 2024-11-15 at 20 39 24"
src="https://github.com/user-attachments/assets/ed244a51-ea55-4fe3-a719-a3d9cd119aa9">
|

Release Notes:

- N/A
2024-11-28 13:39:49 -03:00
Kirill Bulatov
f30944543e Do less resolves when showing the completion menu (#21286)
Closes https://github.com/zed-industries/zed/issues/21205

Zed does completion resolve on every menu item selection and when
applying the edit, so resolving all completion menu list is excessive
indeed.

In addition to that, removes the documentation-centric approach of menu
resolves, as we're actually resolving these for more than that, e.g.
additionalTextEdits and have to do that always, even if we do not show
the documentation.

Potentially, we can omit the second resolve too, but that seems
relatively dangerous, and many servers remove the `data` after the first
resolve, so a 2nd one is not that harmful given that we used to do much
more

Release Notes:

- Reduced the amount of `completionItem/resolve` calls done in the
completion menu
2024-11-28 18:16:37 +02:00
Gowtham K
6cba467a4e project-panel: Fix playback GIF images (#21274) 2024-11-28 03:20:10 -08:00
CharlesChen0823
cacec06db6 search: Treat non-word char as whole-char when searching (#19152)
when search somethings like `clone(`, with search options `match case
sensitively` and `match whole words` in zed code base, only `clone(cx)`
hit match, `clone()` will not hit math.

Release Notes:

- Improved buffer search for queries ending with non-letter characters
2024-11-28 11:06:48 +02:00
Zach Bruggeman
3ac119ac4e Fix hovered links underline not showing when using cmd_or_ctrl for multi_cursor_modifier (#20949)
I use `cmd_or_ctrl` for `multi_cursor_modifier`, but noticed that if I
hovered a code reference while holding alt, it wouldn't show the
underline. Instead, it would only show when pressing cmd. Looking at the
code, it seems like this was just a small oversight on always checking
for `modifiers.secondary`, instead of reading from the
`multi_cursor_modifier` setting to determine which button was invoking
link handling.


---

Release Notes:

- Fixed underline when hovering a code link not showing when
`multi_cursor_modifier` is `cmd_or_ctrl`
2024-11-28 11:00:45 +02:00
Jaagup Averin
b12a508ed9 python: Fix highlighting for forward references (#20766)
[PEP484](https://peps.python.org/pep-0484/) defines "Forward references"
for undefined types. This PR treats such annotations as types rather
than strings.
Release Notes:

- Added Python syntax highlighting for forward references.
2024-11-28 09:59:10 +01:00
renovate[bot]
1739de59d4 Update Rust crate proc-macro2 to v1.0.92 (#20967)
This PR contains the following updates:

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

---

### Release Notes

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

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

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

- Improve compiler/fallback mismatch panic message
([#&#8203;487](https://redirect.github.com/dtolnay/proc-macro2/issues/487))

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

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

- Fix panic *"compiler/fallback mismatch 949"* when using
TokenStream::from_str from inside a proc macro to parse a string
containing doc comment
([#&#8203;484](https://redirect.github.com/dtolnay/proc-macro2/issues/484))

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

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

- Improve error recovery in TokenStream's and Literal's FromStr
implementations to work around
[https://github.com/rust-lang/rust/issues/58736](https://redirect.github.com/rust-lang/rust/issues/58736)
such that rustc does not poison compilation on codepaths that should be
recoverable errors
([#&#8203;477](https://redirect.github.com/dtolnay/proc-macro2/issues/477),
[#&#8203;478](https://redirect.github.com/dtolnay/proc-macro2/issues/478),
[#&#8203;479](https://redirect.github.com/dtolnay/proc-macro2/issues/479),
[#&#8203;480](https://redirect.github.com/dtolnay/proc-macro2/issues/480),
[#&#8203;481](https://redirect.github.com/dtolnay/proc-macro2/issues/481),
[#&#8203;482](https://redirect.github.com/dtolnay/proc-macro2/issues/482))

</details>

---

### Configuration

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

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

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

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

---

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

---

Release Notes:

- N/A

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

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-28 10:58:53 +02:00
renovate[bot]
4aa47a9063 Update Rust crate rodio to 0.20.0 (#20955)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [rodio](https://redirect.github.com/RustAudio/rodio) | dependencies |
minor | `0.19.0` -> `0.20.0` |

---

### Release Notes

<details>
<summary>RustAudio/rodio (rodio)</summary>

###
[`v0.20.1`](https://redirect.github.com/RustAudio/rodio/blob/HEAD/CHANGELOG.md#Version-0201-2024-11-08)

[Compare
Source](https://redirect.github.com/RustAudio/rodio/compare/v0.20.0...v0.20.1)

##### Fixed

-   Builds without the `symphonia` feature did not compile

###
[`v0.20.0`](https://redirect.github.com/RustAudio/rodio/blob/HEAD/CHANGELOG.md#Version-0200-2024-11-08)

[Compare
Source](https://redirect.github.com/RustAudio/rodio/compare/v0.19.0...v0.20.0)

##### Added

-   Support for *ALAC/AIFF*
- Add `automatic_gain_control` source for dynamic audio level
adjustment.
-   New test signal generator sources:
- `SignalGenerator` source generates a sine, triangle, square wave or
sawtooth
        of a given frequency and sample rate.
    -   `Chirp` source generates a sine wave with a linearly-increasing
        frequency over a given frequency range and duration.
- `white` and `pink` generate white or pink noise, respectively. These
sources depend on the `rand` crate and are guarded with the "noise"
        feature.
- Documentation for the "noise" feature has been added to `lib.rs`.
-   New Fade and Crossfade sources:
    -   `fade_out` fades an input out using a linear gain fade.
- `linear_gain_ramp` applies a linear gain change to a sound over a
given duration. `fade_out` is implemented as a `linear_gain_ramp` and
        `fade_in` has been refactored to use the `linear_gain_ramp`
        implementation.

##### Fixed

- `Sink.try_seek` now updates `controls.position` before returning.
Calls to `Sink.get_pos`
    done immediately after a seek will now return the correct value.

##### Changed

-   `SamplesBuffer` is now `Clone`

</details>

---

### Configuration

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

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

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

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

---

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

---

Release Notes:

- N/A

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

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-28 10:58:36 +02:00
renovate[bot]
fe30a03921 Update Rust crate ipc-channel to 0.19 (#20951)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [ipc-channel](https://redirect.github.com/servo/ipc-channel) |
dependencies | minor | `0.18` -> `0.19` |

---

### Configuration

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

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

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

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

---

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

---

Release Notes:

- N/A

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

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-28 10:58:10 +02:00
renovate[bot]
38900c2321 Update Rust crate bytemuck to v1.20.0 (#20947)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [bytemuck](https://redirect.github.com/Lokathor/bytemuck) |
dependencies | minor | `1.19.0` -> `1.20.0` |

---

### Release Notes

<details>
<summary>Lokathor/bytemuck (bytemuck)</summary>

###
[`v1.20.0`](https://redirect.github.com/Lokathor/bytemuck/compare/v1.19.0...v1.20.0)

[Compare
Source](https://redirect.github.com/Lokathor/bytemuck/compare/v1.19.0...v1.20.0)

</details>

---

### Configuration

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

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

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

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

---

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

---

Release Notes:

- N/A

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

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-28 10:56:36 +02:00
renovate[bot]
6927512e34 Update Rust crate ashpd to 0.10.0 (#20939)
This PR contains the following updates:

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

---

### Release Notes

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

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

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

-   Add `backend` feature to docs.rs

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

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

#### What's Changed

- desktop/activation-token: Add helper for retriving the token from a
`gtk::Widget` or a `WlSurface`
-   desktop/secret: Close the socket after done reading
-   desktop/input-capture: Fix barrier-id type
-   desktop: Use a Pid alias all over the codebase
-   desktop/notification: Support v2 of the interface
- Introduce backend implementation support, allowing to write a portal
implementation in pure Rust. Currently, we don't support Session based
portals. The backend feature is considered experimental as we might
possibly introduce API breaking changes in the future but it should be
good enough for getting started. Examples of how a portal can be
implemented can be found in
[backend-demo](https://redirect.github.com/bilelmoussaoui/ashpd/tree/master/backend-demo)

**Note**: The 0.10.0 release has been yanked from crates.io as it
contained a build error when the `glib` feature is enabled.

###
[`v0.10.0`](https://redirect.github.com/bilelmoussaoui/ashpd/compare/0.9.2...0.10.0)

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

</details>

---

### Configuration

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

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

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

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

---

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

---

Release Notes:

- N/A

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

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-28 10:56:21 +02:00
renovate[bot]
4342a93d22 Update Rust crate tree-sitter-c to v0.23.2 (#20938)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [tree-sitter-c](https://redirect.github.com/tree-sitter/tree-sitter-c)
| workspace.dependencies | patch | `0.23.1` -> `0.23.2` |

---

### Release Notes

<details>
<summary>tree-sitter/tree-sitter-c (tree-sitter-c)</summary>

###
[`v0.23.2`](https://redirect.github.com/tree-sitter/tree-sitter-c/releases/tag/v0.23.2)

[Compare
Source](https://redirect.github.com/tree-sitter/tree-sitter-c/compare/v0.23.1...v0.23.2)

**NOTE:** Download `tree-sitter-c.tar.xz` for the *complete* source
code.

</details>

---

### Configuration

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

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

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

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

---

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

---

Release Notes:

- N/A

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

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-28 10:55:57 +02:00
renovate[bot]
28640ac076 Update astral-sh/setup-uv digest to caf0cab (#20927)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [astral-sh/setup-uv](https://redirect.github.com/astral-sh/setup-uv) |
action | digest | `2e657c1` -> `caf0cab` |

---

### Configuration

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

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

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

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

---

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

---

Release Notes:

- N/A

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

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-28 10:55:46 +02:00
feeiyu
c2c968f2de Enable clangd's dot-to-arrow feature (#21142)
Closes #20815


![dot2arrow1127](https://github.com/user-attachments/assets/d825f9bf-52ae-47ee-b3a3-5f952b6e8979)

Release Notes:
- Enabled clangd's dot-to-arrow feature
2024-11-28 10:43:25 +02:00
Stanislav Alekseev
a4584c9d13 Add an uninstall script (#21213)
Closes #14306

This looks at what #16660 did and install.sh script as a base for the
uninstall.sh script. The script is bundled with the cli by default
unless the cli/no-bundled-uninstall feature is selected which is done,
so package managers could build zed without bundling a useless feature
and increasing binary size.

I don't have capabilities to test this right now, so any help with that
is appreciated.

Release Notes:

- Added an uninstall script for Zed installations done via zed.dev. To
uninstall zed, run `zed --uninstall` via the CLI binary.
2024-11-28 10:31:12 +02:00
Jason Lee
e9e260776b gpui: Fix default colors blue, red, green to match in CSS default colors (#20851)
Release Notes:

- N/A

---

This change to let the default colors to 100% match with CSS default
colors.

And update the methods to as `const`.

Here is an example:

<img width="338" alt="image"
src="https://github.com/user-attachments/assets/dd17b46a-3ad4-4122-8dca-e800644c75b0">

https://codepen.io/huacnlee/pen/ZEgNXJZ

But the before version for example blue: `h: 0.6 * 360 = 216`, but we
expected `240`, `240 / 360 = 0.666666666`, so the before version are
lose the precision. (Here is a test tool: https://hslpicker.com/#0000FF)

## After Update

```bash
cargo run -p gpui --example hello_world
```

<img width="612" alt="image"
src="https://github.com/user-attachments/assets/97d479d8-9c71-4be3-95e0-09af45fe47e2">
2024-11-28 10:08:07 +02:00
Jared Ramirez
461ab24a06 Update nix cargo hash (#21257)
Closes https://github.com/zed-industries/zed/issues/21256

Release Notes:

- N/A
2024-11-28 08:04:11 +02:00
Remco Smits
04ff9f060c Improve runnable detection for JavaScript files (#21246)
Closes #21242

![Screenshot 2024-11-27 at 18 52
51](https://github.com/user-attachments/assets/d096197c-33d2-41b9-963d-3e1a9bbdc035)
![Screenshot 2024-11-27 at 18 53
08](https://github.com/user-attachments/assets/b3202b00-3f68-4d9d-acc2-1b86c081fc34)

Release Notes:

- Improved runnable detection for JavaScript/Typescript files.
2024-11-28 00:54:01 +01:00
Kirill Bulatov
66ba9d5b4b Use item context for pane tab context menu (#21254)
This allows to show proper override values for terminal tabs in Linux
and Windows.

Release Notes:

- Fixed incorrect "close tab" keybinding shown in context menu of the
terminal panel tabs on Linux and Windows
2024-11-28 00:30:33 +02:00
Kirill Bulatov
e803815b16 Use proper context to show terminal split menu bindings (#21253)
Follow-up of https://github.com/zed-industries/zed/pull/21251

Show proper keybindings on the terminal split button:

<img width="249" alt="image"
src="https://github.com/user-attachments/assets/b51b183f-788a-4e8f-9fec-3ec07f084bd4">

Release Notes:

- N/A
2024-11-28 00:06:23 +02:00
Kirill Bulatov
34ed48e14b Add a split button to terminal panes (#21251)
Follow-up of https://github.com/zed-industries/zed/pull/21238

<img width="873" alt="image"
src="https://github.com/user-attachments/assets/8cf2d8ea-a1df-4a6a-95d6-5867e0ee287d">

Release Notes:

- N/A
2024-11-27 23:17:44 +02:00
Mikayla Maki
0c8e5550e7 Make Markdown images layout vertically instead of horizontally (#21247)
Release Notes:

- Fixed a bug in the Markdown preview where images in the same paragraph
would be rendered next to each other
2024-11-27 10:47:23 -08:00
张小白
cff9ae0bbc Better absolute path handling (#19727)
Closes #19866

This PR supersedes #19228, as #19228 encountered too many merge
conflicts.

After some exploration, I found that for paths with the `\\?\` prefix,
we can safely remove it and consistently use the clean paths in all
cases. Previously, in #19228, I thought we would still need the `\\?\`
prefix for IO operations to handle long paths better. However, this
turns out to be unnecessary because Rust automatically manages this for
us when calling IO-related APIs. For details, refer to Rust's internal
function
[`get_long_path`](017ae1b21f/library/std/src/sys/path/windows.rs (L225-L233)).

Therefore, we can always store and use paths without the `\\?\` prefix.

This PR introduces a `SanitizedPath` structure, which represents a path
stripped of the `\\?\` prefix. To prevent untrimmed paths from being
mistakenly passed into `Worktree`, the type of `Worktree`’s `abs_path`
member variable has been changed to `SanitizedPath`.

Additionally, this PR reverts the changes of #15856 and #18726. After
testing, it appears that the issues those PRs addressed can be resolved
by this PR.

### Existing Issue
To keep the scope of modifications manageable, `Worktree::abs_path` has
retained its current signature as `fn abs_path(&self) -> Arc<Path>`,
rather than returning a `SanitizedPath`. Updating the method to return
`SanitizedPath`—which may better resolve path inconsistencies—would
likely introduce extensive changes similar to those in #19228.

Currently, the limitation is as follows:

```rust
let abs_path: &Arc<Path> = snapshot.abs_path();
let some_non_trimmed_path = Path::new("\\\\?\\C:\\Users\\user\\Desktop\\project"); 
// The caller performs some actions here:
some_non_trimmed_path.strip_prefix(abs_path);  // This fails
some_non_trimmed_path.starts_with(abs_path);   // This fails too
```

The final two lines will fail because `snapshot.abs_path()` returns a
clean path without the `\\?\` prefix. I have identified two relevant
instances that may face this issue:
-
[lsp_store.rs#L3578](0173479d18/crates/project/src/lsp_store.rs (L3578))
-
[worktree.rs#L4338](0173479d18/crates/worktree/src/worktree.rs (L4338))

Switching `Worktree::abs_path` to return `SanitizedPath` would resolve
these issues but would also lead to many code changes.

Any suggestions or feedback on this approach are very welcome.

cc @SomeoneToIgnore 

Release Notes:

- N/A
2024-11-27 20:22:58 +02:00
Kirill Bulatov
d0bafce86b Allow splitting the terminal panel (#21238)
Closes https://github.com/zed-industries/zed/issues/4351


![it_splits](https://github.com/user-attachments/assets/40de03c9-2173-4441-ba96-8e91537956e0)

Applies the same splitting mechanism, as Zed's central pane has, to the
terminal panel.
Similar navigation, splitting and (de)serialization capabilities are
supported.

Notable caveats:
* zooming keeps the terminal splits' ratio, rather expanding the
terminal pane
* on macOs, central panel is split with `cmd-k up/down/etc.` but `cmd-k`
is a "standard" terminal clearing keybinding on macOS, so terminal panel
splitting is done via `ctrl-k up/down/etc.`
* task terminals are "split" into regular terminals, and also not
persisted (same as currently in the terminal)

Seems ok for the initial version, we can revisit and polish things
later.

Release Notes:

- Added the ability to split the terminal panel
2024-11-27 20:22:39 +02:00
Stanislav Alekseev
4564da2875 Improve Nix package and shell (#21075)
With an addition of useFetchCargoVendor, crane becomes less necessary
for our use. This reuses the package from nixpkgs as well as creating a
better devshell that both work on macOS.

I use Xcode's SDKROOT and DEVELOPER_DIR to point the swift in the
livekit client crate to a correct sdk when using a devshell. Devshell
should work without that once apple releases sources for the 15.1 SDK
but for now this is an easy fix

This also replaces fenix with rust-overlay because of issues with the
out-of-sandbox access I've noticed fenix installed toolchains have

Release Notes:

- N/A
2024-11-27 20:22:17 +02:00
Peter Tripp
c021ee60d6 v0.165.x dev 2024-11-27 09:48:40 -05:00
347 changed files with 20075 additions and 5227 deletions

View File

@@ -13,6 +13,12 @@ rustflags = ["-C", "link-arg=-fuse-ld=mold"]
linker = "clang"
rustflags = ["-C", "link-arg=-fuse-ld=mold"]
[target.aarch64-apple-darwin]
rustflags = ["-C", "link-args=-Objc -all_load"]
[target.x86_64-apple-darwin]
rustflags = ["-C", "link-args=-Objc -all_load"]
# This cfg will reduce the size of `windows::core::Error` from 16 bytes to 4 bytes
[target.'cfg(target_os = "windows")']
rustflags = ["--cfg", "windows_slim_errors"]

View File

@@ -113,6 +113,12 @@ jobs:
script/check-licenses
script/generate-licenses /tmp/zed_licenses_output
- name: Check for new vulnerable dependencies
if: github.event_name == 'pull_request'
uses: actions/dependency-review-action@3b139cfc5fae8b618d3eae3675e383bb1769c019 # v4
with:
license-check: false
- name: Run tests
uses: ./.github/actions/run_tests
@@ -123,6 +129,7 @@ jobs:
run: |
cargo build --workspace --bins --all-features
cargo check -p gpui --features "macos-blade"
cargo check -p workspace --features "livekit-cross-platform"
cargo build -p remote_server
linux_tests:

View File

@@ -12,7 +12,7 @@ jobs:
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
- name: Set up uv
uses: astral-sh/setup-uv@2e657c127d5b1635d5a8e3fa40e0ac50a5bf6992 # v3
uses: astral-sh/setup-uv@caf0cab7a618c569241d31dcd442f54681755d39 # v3
with:
version: "latest"
enable-cache: true

View File

@@ -12,7 +12,7 @@ jobs:
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
- name: Set up uv
uses: astral-sh/setup-uv@2e657c127d5b1635d5a8e3fa40e0ac50a5bf6992 # v3
uses: astral-sh/setup-uv@caf0cab7a618c569241d31dcd442f54681755d39 # v3
with:
version: "latest"
enable-cache: true

1
.gitignore vendored
View File

@@ -1,4 +1,5 @@
/.direnv
.envrc
.idea
**/target
**/cargo-target

1184
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -65,8 +65,9 @@ members = [
"crates/language_selector",
"crates/language_tools",
"crates/languages",
"crates/live_kit_client",
"crates/live_kit_server",
"crates/livekit_client",
"crates/livekit_client_macos",
"crates/livekit_server",
"crates/lsp",
"crates/markdown",
"crates/markdown_preview",
@@ -228,7 +229,9 @@ git = { path = "crates/git" }
git_hosting_providers = { path = "crates/git_hosting_providers" }
go_to_line = { path = "crates/go_to_line" }
google_ai = { path = "crates/google_ai" }
gpui = { path = "crates/gpui", default-features = false, features = ["http_client"]}
gpui = { path = "crates/gpui", default-features = false, features = [
"http_client",
] }
gpui_macros = { path = "crates/gpui_macros" }
html_to_markdown = { path = "crates/html_to_markdown" }
http_client = { path = "crates/http_client" }
@@ -246,8 +249,9 @@ language_models = { path = "crates/language_models" }
language_selector = { path = "crates/language_selector" }
language_tools = { path = "crates/language_tools" }
languages = { path = "crates/languages" }
live_kit_client = { path = "crates/live_kit_client" }
live_kit_server = { path = "crates/live_kit_server" }
livekit_client = { path = "crates/livekit_client" }
livekit_client_macos = { path = "crates/livekit_client_macos" }
livekit_server = { path = "crates/livekit_server" }
lsp = { path = "crates/lsp" }
markdown = { path = "crates/markdown" }
markdown_preview = { path = "crates/markdown_preview" }
@@ -331,7 +335,7 @@ alacritty_terminal = { git = "https://github.com/alacritty/alacritty", rev = "91
any_vec = "0.14"
anyhow = "1.0.86"
arrayvec = { version = "0.7.4", features = ["serde"] }
ashpd = "0.9.1"
ashpd = { version = "0.10", default-features = false, features = ["async-std"]}
async-compat = "0.2.1"
async-compression = { version = "0.4", features = ["gzip", "futures-io"] }
async-dispatcher = "0.1"
@@ -380,20 +384,23 @@ heed = { version = "0.20.1", features = ["read-txn-no-tls"] }
hex = "0.4.3"
html5ever = "0.27.0"
hyper = "0.14"
http = "1.1"
ignore = "0.4.22"
image = "0.25.1"
indexmap = { version = "1.6.2", features = ["serde"] }
indoc = "2"
itertools = "0.13.0"
jsonwebtoken = "9.3"
jupyter-protocol = { version = "0.3.0" }
jupyter-websocket-client = { version = "0.5.0" }
jupyter-protocol = { version = "0.5.0" }
jupyter-websocket-client = { version = "0.8.0" }
libc = "0.2"
libsqlite3-sys = { version = "0.30.1", features = ["bundled"] }
linkify = "0.10.0"
livekit = { git = "https://github.com/zed-industries/rust-sdks", rev="799f10133d93ba2a88642cd480d01ec4da53408c", features = ["dispatcher", "services-dispatcher", "rustls-tls-native-roots"], default-features = false }
log = { version = "0.4.16", features = ["kv_unstable_serde", "serde"] }
markup5ever_rcdom = "0.3.0"
nanoid = "0.4"
nbformat = { version = "0.7.0" }
nbformat = { version = "0.9.0" }
nix = "0.29"
num-format = "0.4.4"
once_cell = "1.19.0"
@@ -403,10 +410,10 @@ parking_lot = "0.12.1"
pathdiff = "0.2"
pet = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "ffcbf3f28c46633abd5448a52b1f396c322e0d6c" }
pet-fs = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "ffcbf3f28c46633abd5448a52b1f396c322e0d6c" }
pet-conda = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "ffcbf3f28c46633abd5448a52b1f396c322e0d6c" }
pet-core = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "ffcbf3f28c46633abd5448a52b1f396c322e0d6c" }
pet-poetry = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "ffcbf3f28c46633abd5448a52b1f396c322e0d6c" }
pet-reporter = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "ffcbf3f28c46633abd5448a52b1f396c322e0d6c" }
pet-conda = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "ffcbf3f28c46633abd5448a52b1f396c322e0d6c" }
pet-core = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "ffcbf3f28c46633abd5448a52b1f396c322e0d6c" }
pet-poetry = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "ffcbf3f28c46633abd5448a52b1f396c322e0d6c" }
pet-reporter = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "ffcbf3f28c46633abd5448a52b1f396c322e0d6c" }
postage = { version = "0.5", features = ["futures-traits"] }
pretty_assertions = { version = "1.3.0", features = ["unstable"] }
profiling = "1"
@@ -427,13 +434,14 @@ reqwest = { git = "https://github.com/zed-industries/reqwest.git", rev = "fd110f
"stream",
] }
rsa = "0.9.6"
runtimelib = { version = "0.22.0", default-features = false, features = [
runtimelib = { version = "0.24.0", default-features = false, features = [
"async-dispatcher-runtime",
] }
rustc-demangle = "0.1.23"
rust-embed = { version = "8.4", features = ["include-exclude"] }
rustls = "0.21.12"
rustls-native-certs = "0.8.0"
scap = "0.0.7"
schemars = { version = "0.8", features = ["impl_json_schema"] }
semver = "1.0"
serde = { version = "1.0", features = ["derive", "rc"] }
@@ -568,6 +576,10 @@ features = [
"Win32_UI_WindowsAndMessaging",
]
# TODO livekit https://github.com/RustAudio/cpal/pull/891
[patch.crates-io]
cpal = { git = "https://github.com/zed-industries/cpal", rev = "fd8bc2fd39f1f5fdee5a0690656caff9a26d9d50" }
[profile.dev]
split-debuginfo = "unpacked"
debug = "limited"
@@ -670,6 +682,7 @@ new_ret_no_self = { level = "allow" }
# We have a few `next` functions that differ in lifetimes
# compared to Iterator::next. Yet, clippy complains about those.
should_implement_trait = { level = "allow" }
let_underscore_future = "allow"
[workspace.metadata.cargo-machete]
ignored = ["bindgen", "cbindgen", "prost_build", "serde"]

View File

@@ -1 +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" class="lucide lucide-text-cursor"><path d="M17 22h-1a4 4 0 0 1-4-4V6a4 4 0 0 1 4-4h1"/><path d="M7 22h1a4 4 0 0 0 4-4v-1"/><path d="M7 2h1a4 4 0 0 1 4 4v1"/></svg>
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M17 20H16C14.9391 20 13.9217 19.6629 13.1716 19.0627C12.4214 18.4626 12 17.6487 12 16.8V7.2C12 6.35131 12.4214 5.53737 13.1716 4.93726C13.9217 4.33714 14.9391 4 16 4H17" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M7 20H8C9.06087 20 10.0783 19.5786 10.8284 18.8284C11.5786 18.0783 12 17.0609 12 16V15" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M7 4H8C9.06087 4 10.0783 4.42143 10.8284 5.17157C11.5786 5.92172 12 6.93913 12 8V9" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 345 B

After

Width:  |  Height:  |  Size: 715 B

View File

@@ -1,4 +1,8 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M9.20721 10.8551C7.67694 10.8551 6.33401 11.0424 5.52243 11.1878C5.15088 11.2543 4.808 10.9114 4.87454 10.5399C5.01987 9.72825 5.20721 8.38532 5.20721 6.85505C5.20721 5.69375 5.09932 4.64034 4.98377 3.84516C4.9431 3.56522 5.40267 3.3722 5.5684 3.60145C6.30333 4.61809 7.44022 6.08806 8.70721 7.35505C9.9742 8.62204 11.4442 9.75893 12.4608 10.4939C12.69 10.6596 12.497 11.1192 12.2171 11.0785C11.4219 10.963 10.3685 10.8551 9.20721 10.8551Z" fill="black" stroke="black" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M7.11129 10.6816L5.28324 8.85355C5.08798 8.65829 4.7714 8.65829 4.57613 8.85355L3.35355 10.0761C3.15829 10.2714 3.15829 10.588 3.35355 10.7832L5.1816 12.6113C5.37686 12.8066 5.69345 12.8066 5.88871 12.6113L7.11129 11.3887C7.30655 11.1934 7.30655 10.8769 7.11129 10.6816Z" fill="black"/>
<path d="M3.5 6.66666V8.66666" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M5.5 5V11" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M7.5 3V13" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M9.5 5.33334V10" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M11.5 4V12" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M13.5 6.66666V8.66666" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 946 B

After

Width:  |  Height:  |  Size: 751 B

View File

@@ -0,0 +1,5 @@
<svg width="17" height="16" viewBox="0 0 17 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8.5 3L8.5 10" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M5 6.5H12" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M5 13H12" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 415 B

View File

@@ -34,6 +34,7 @@
"dat": "storage",
"db": "storage",
"dbf": "storage",
"diff": "diff",
"dll": "storage",
"doc": "document",
"docx": "document",
@@ -112,6 +113,7 @@
"mkv": "video",
"ml": "ocaml",
"mli": "ocaml",
"mod": "go",
"mov": "video",
"mp3": "audio",
"mp4": "video",
@@ -127,6 +129,7 @@
"ogg": "audio",
"opus": "audio",
"otf": "font",
"pcss": "css",
"pdb": "storage",
"pdf": "document",
"php": "php",
@@ -173,6 +176,9 @@
"tsx": "react",
"ttf": "font",
"txt": "document",
"v": "v",
"vsh": "v",
"vv": "v",
"vue": "vue",
"wav": "audio",
"webm": "video",
@@ -181,6 +187,7 @@
"wmv": "video",
"woff": "font",
"woff2": "font",
"work": "go",
"wv": "audio",
"xls": "document",
"xlsx": "document",
@@ -235,6 +242,9 @@
"default": {
"icon": "icons/file_icons/file.svg"
},
"diff": {
"icon": "icons/file_icons/diff.svg"
},
"docker": {
"icon": "icons/file_icons/docker.svg"
},
@@ -379,6 +389,9 @@
"typescript": {
"icon": "icons/file_icons/typescript.svg"
},
"v": {
"icon": "icons/file_icons/v.svg"
},
"vcs": {
"icon": "icons/file_icons/git.svg"
},

View File

@@ -0,0 +1,4 @@
<svg width="17" height="16" viewBox="0 0 17 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path opacity="0.5" d="M10.0469 12.8661L13.3884 3.31889C13.4386 3.1754 13.3167 3.03055 13.1667 3.05554L10.7292 3.46179C10.5875 3.48542 10.4693 3.58324 10.4197 3.71807L7.24789 12.3271C7.12763 12.6536 7.36919 13 7.71706 13H9.8581C9.94309 13 10.0188 12.9463 10.0469 12.8661Z" fill="black"/>
<path d="M6.90625 12.7321L3.61161 3.31889C3.56139 3.1754 3.6833 3.03055 3.83326 3.05554L6.27076 3.46179C6.4125 3.48542 6.53067 3.58324 6.58034 3.71807L9.90084 12.7309C9.94895 12.8614 9.85232 13 9.71317 13H7.28379C7.11381 13 6.9624 12.8926 6.90625 12.7321Z" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 663 B

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

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-globe"><circle cx="12" cy="12" r="10"/><path d="M12 2a14.5 14.5 0 0 0 0 20 14.5 14.5 0 0 0 0-20"/><path d="M2 12h20"/></svg>

After

Width:  |  Height:  |  Size: 327 B

View File

@@ -108,7 +108,9 @@
"ctrl-'": "editor::ToggleHunkDiff",
"ctrl-\"": "editor::ExpandAllHunkDiffs",
"ctrl-i": "editor::ShowSignatureHelp",
"alt-g b": "editor::ToggleGitBlame"
"alt-g b": "editor::ToggleGitBlame",
"menu": "editor::OpenContextMenu",
"shift-f10": "editor::OpenContextMenu"
}
},
{

View File

@@ -93,8 +93,6 @@
"ctrl-e": "editor::MoveToEndOfLine",
"cmd-up": "editor::MoveToBeginning",
"cmd-down": "editor::MoveToEnd",
"ctrl-home": "editor::MoveToBeginning",
"ctrl-end": "editor::MoveToEnd",
"shift-up": "editor::SelectUp",
"ctrl-shift-p": "editor::SelectUp",
"shift-down": "editor::SelectDown",
@@ -212,7 +210,8 @@
{
"context": "AssistantPanel2",
"bindings": {
"cmd-n": "assistant2::NewThread"
"cmd-n": "assistant2::NewThread",
"cmd-shift-h": "assistant2::OpenHistory"
}
},
{
@@ -732,7 +731,11 @@
"cmd-end": "terminal::ScrollToBottom",
"shift-home": "terminal::ScrollToTop",
"shift-end": "terminal::ScrollToBottom",
"ctrl-shift-space": "terminal::ToggleViMode"
"ctrl-shift-space": "terminal::ToggleViMode",
"ctrl-k up": "pane::SplitUp",
"ctrl-k down": "pane::SplitDown",
"ctrl-k left": "pane::SplitLeft",
"ctrl-k right": "pane::SplitRight"
}
}
]

View File

@@ -33,6 +33,18 @@
"(": "vim::SentenceBackward",
")": "vim::SentenceForward",
"|": "vim::GoToColumn",
"] ]": "vim::NextSectionStart",
"] [": "vim::NextSectionEnd",
"[ [": "vim::PreviousSectionStart",
"[ ]": "vim::PreviousSectionEnd",
"] m": "vim::NextMethodStart",
"] M": "vim::NextMethodEnd",
"[ m": "vim::PreviousMethodStart",
"[ M": "vim::PreviousMethodEnd",
"[ *": "vim::PreviousComment",
"[ /": "vim::PreviousComment",
"] *": "vim::NextComment",
"] /": "vim::NextComment",
// Word motions
"w": "vim::NextWordStart",
"e": "vim::NextWordEnd",
@@ -55,10 +67,10 @@
"n": "vim::MoveToNextMatch",
"shift-n": "vim::MoveToPrevMatch",
"%": "vim::Matching",
"] }": ["vim::UnmatchedForward", { "char": "}" } ],
"[ {": ["vim::UnmatchedBackward", { "char": "{" } ],
"] )": ["vim::UnmatchedForward", { "char": ")" } ],
"[ (": ["vim::UnmatchedBackward", { "char": "(" } ],
"] }": ["vim::UnmatchedForward", { "char": "}" }],
"[ {": ["vim::UnmatchedBackward", { "char": "{" }],
"] )": ["vim::UnmatchedForward", { "char": ")" }],
"[ (": ["vim::UnmatchedBackward", { "char": "(" }],
"f": ["vim::PushOperator", { "FindForward": { "before": false } }],
"t": ["vim::PushOperator", { "FindForward": { "before": true } }],
"shift-f": ["vim::PushOperator", { "FindBackward": { "after": false } }],
@@ -209,6 +221,7 @@
"shift-s": "vim::SubstituteLine",
">": ["vim::PushOperator", "Indent"],
"<": ["vim::PushOperator", "Outdent"],
"=": ["vim::PushOperator", "AutoIndent"],
"g u": ["vim::PushOperator", "Lowercase"],
"g shift-u": ["vim::PushOperator", "Uppercase"],
"g ~": ["vim::PushOperator", "OppositeCase"],
@@ -275,6 +288,7 @@
"ctrl-[": ["vim::SwitchMode", "Normal"],
">": "vim::Indent",
"<": "vim::Outdent",
"=": "vim::AutoIndent",
"i": ["vim::PushOperator", { "Object": { "around": false } }],
"a": ["vim::PushOperator", { "Object": { "around": true } }],
"g c": "vim::ToggleComments",
@@ -312,6 +326,22 @@
"ctrl-o": "vim::TemporaryNormal"
}
},
{
"context": "vim_mode == helix_normal",
"bindings": {
"i": "vim::InsertBefore",
"a": "vim::InsertAfter",
"w": "vim::NextWordStart",
"e": "vim::NextWordEnd",
"b": "vim::PreviousWordStart",
"h": "vim::Left",
"j": "vim::Down",
"k": "vim::Up",
"l": "vim::Right"
}
},
{
"context": "vim_mode == insert && !(showing_code_actions || showing_completions)",
"use_layout_keys": true,
@@ -358,7 +388,8 @@
"bindings": {
"escape": "vim::ClearOperators",
"ctrl-c": "vim::ClearOperators",
"ctrl-[": "vim::ClearOperators"
"ctrl-[": "vim::ClearOperators",
"g c": "vim::Comment"
}
},
{
@@ -387,7 +418,9 @@
">": "vim::AngleBrackets",
"a": "vim::Argument",
"i": "vim::IndentObj",
"shift-i": ["vim::IndentObj", { "includeBelow": true }]
"shift-i": ["vim::IndentObj", { "includeBelow": true }],
"f": "vim::Method",
"c": "vim::Class"
}
},
{
@@ -472,6 +505,13 @@
"<": "vim::CurrentLine"
}
},
{
"context": "vim_operator == eq",
"use_layout_keys": true,
"bindings": {
"=": "vim::CurrentLine"
}
},
{
"context": "vim_operator == gc",
"use_layout_keys": true,
@@ -619,6 +659,12 @@
"p": "project_panel::Open",
"x": "project_panel::RevealInFileManager",
"s": "project_panel::OpenWithSystem",
"] c": "project_panel::SelectNextGitEntry",
"[ c": "project_panel::SelectPrevGitEntry",
"] d": "project_panel::SelectNextDiagnostic",
"[ d": "project_panel::SelectPrevDiagnostic",
"}": "project_panel::SelectNextDirectory",
"{": "project_panel::SelectPrevDirectory",
"shift-g": "menu::SelectLast",
"g g": "menu::SelectFirst",
"-": "project_panel::SelectParent",

View File

@@ -300,6 +300,8 @@
"scroll_beyond_last_line": "one_page",
// The number of lines to keep above/below the cursor when scrolling.
"vertical_scroll_margin": 3,
// Whether to scroll when clicking near the edge of the visible text area.
"autoscroll_on_clicks": false,
// Scroll sensitivity multiplier. This multiplier is applied
// to both the horizontal and vertical delta values while scrolling.
"scroll_sensitivity": 1.0,
@@ -557,13 +559,25 @@
"close_position": "right",
// Whether to show the file icon for a tab.
"file_icons": false,
// Whether to always show the close button on tabs.
"always_show_close_button": false,
// What to do after closing the current tab.
//
// 1. Activate the tab that was open previously (default)
// "History"
// 2. Activate the neighbour tab (prefers the right one, if present)
// "Neighbour"
"activate_on_close": "history"
"activate_on_close": "history",
/// Which files containing diagnostic errors/warnings to mark in the tabs.
/// This setting can take the following three values:
///
/// 1. Do not mark any files:
/// "off"
/// 2. Only mark files with errors:
/// "errors"
/// 3. Mark files with errors and warnings:
/// "all"
"show_diagnostics": "all"
},
// Settings related to preview tabs.
"preview_tabs": {
@@ -1125,6 +1139,7 @@
"use_system_clipboard": "always",
"use_multiline_find": false,
"use_smartcase_find": false,
"highlight_on_yank_duration": 200,
"custom_digraphs": {}
},
// The server to connect to. If the environment variable
@@ -1182,6 +1197,8 @@
// "W": "workspace::Save"
// }
"command_aliases": {},
// Whether to show user picture in titlebar.
"show_user_picture": true,
// ssh_connections is an array of ssh connections.
// You can configure these from `project: Open Remote` in the command palette.
// Zed's ssh support will pull configuration from your ~/.ssh too.

View File

@@ -1,5 +1,5 @@
{
"$schema": "https://zed.dev/schema/themes/v0.1.0.json",
"$schema": "https://zed.dev/schema/themes/v0.2.0.json",
"name": "Andromeda",
"author": "Zed Industries",
"themes": [

View File

@@ -1,5 +1,5 @@
{
"$schema": "https://zed.dev/schema/themes/v0.1.0.json",
"$schema": "https://zed.dev/schema/themes/v0.2.0.json",
"name": "Atelier",
"author": "Zed Industries",
"themes": [

View File

@@ -1,5 +1,5 @@
{
"$schema": "https://zed.dev/schema/themes/v0.1.0.json",
"$schema": "https://zed.dev/schema/themes/v0.2.0.json",
"name": "Ayu",
"author": "Zed Industries",
"themes": [

View File

@@ -1,5 +1,5 @@
{
"$schema": "https://zed.dev/schema/themes/v0.1.0.json",
"$schema": "https://zed.dev/schema/themes/v0.2.0.json",
"name": "Gruvbox",
"author": "Zed Industries",
"themes": [

View File

@@ -1,5 +1,5 @@
{
"$schema": "https://zed.dev/schema/themes/v0.1.0.json",
"$schema": "https://zed.dev/schema/themes/v0.2.0.json",
"name": "One",
"author": "Zed Industries",
"themes": [

View File

@@ -1,5 +1,5 @@
{
"$schema": "https://zed.dev/schema/themes/v0.1.0.json",
"$schema": "https://zed.dev/schema/themes/v0.2.0.json",
"name": "Rosé Pine",
"author": "Zed Industries",
"themes": [

View File

@@ -1,5 +1,5 @@
{
"$schema": "https://zed.dev/schema/themes/v0.1.0.json",
"$schema": "https://zed.dev/schema/themes/v0.2.0.json",
"name": "Sandcastle",
"author": "Zed Industries",
"themes": [

View File

@@ -1,5 +1,5 @@
{
"$schema": "https://zed.dev/schema/themes/v0.1.0.json",
"$schema": "https://zed.dev/schema/themes/v0.2.0.json",
"name": "Solarized",
"author": "Zed Industries",
"themes": [

View File

@@ -1,5 +1,5 @@
{
"$schema": "https://zed.dev/schema/themes/v0.1.0.json",
"$schema": "https://zed.dev/schema/themes/v0.2.0.json",
"name": "Summercamp",
"author": "Zed Industries",
"themes": [

View File

@@ -342,8 +342,7 @@ fn register_slash_commands(prompt_builder: Option<Arc<PromptBuilder>>, cx: &mut
slash_command_registry.register_command(terminal_command::TerminalSlashCommand, true);
slash_command_registry.register_command(now_command::NowSlashCommand, false);
slash_command_registry.register_command(diagnostics_command::DiagnosticsSlashCommand, true);
slash_command_registry.register_command(fetch_command::FetchSlashCommand, false);
slash_command_registry.register_command(fetch_command::FetchSlashCommand, false);
slash_command_registry.register_command(fetch_command::FetchSlashCommand, true);
if let Some(prompt_builder) = prompt_builder {
cx.observe_flag::<project_command::ProjectSlashCommandFeatureFlag, _>({

View File

@@ -416,7 +416,6 @@ impl AssistantPanel {
ControlFlow::Break(())
});
pane.set_can_split(false, cx);
pane.set_can_navigate(true, cx);
pane.display_nav_history_buttons(None);
pane.set_should_display_tab_bar(|_| true);
@@ -451,6 +450,7 @@ impl AssistantPanel {
.gap(DynamicSpacing::Base02.rems(cx))
.child(
IconButton::new("new-chat", IconName::Plus)
.icon_size(IconSize::Small)
.on_click(
cx.listener(|_, _, cx| {
cx.dispatch_action(NewContext.boxed_clone())

View File

@@ -164,7 +164,7 @@ impl SlashCommand for ContextServerSlashCommand {
.messages
.into_iter()
.filter_map(|msg| match msg.content {
context_server::types::MessageContent::Text { text } => Some(text),
context_server::types::MessageContent::Text { text, .. } => Some(text),
_ => None,
})
.collect::<Vec<String>>()

View File

@@ -108,6 +108,10 @@ impl SlashCommand for FetchSlashCommand {
"Insert fetched URL contents".into()
}
fn icon(&self) -> IconName {
IconName::Globe
}
fn menu_text(&self) -> String {
self.description()
}
@@ -162,7 +166,7 @@ impl SlashCommand for FetchSlashCommand {
text,
sections: vec![SlashCommandOutputSection {
range,
icon: IconName::AtSign,
icon: IconName::Globe,
label: format!("fetch {}", url).into(),
metadata: None,
}],

View File

@@ -2,7 +2,7 @@ use std::sync::Arc;
use gpui::{AnyElement, DismissEvent, SharedString, Task, WeakView};
use picker::{Picker, PickerDelegate, PickerEditorPosition};
use ui::{prelude::*, ListItem, ListItemSpacing, PopoverMenu, PopoverTrigger};
use ui::{prelude::*, ListItem, ListItemSpacing, PopoverMenu, PopoverTrigger, Tooltip};
use crate::assistant_panel::ContextEditor;
use crate::SlashCommandWorkingSet;
@@ -177,11 +177,17 @@ impl PickerDelegate for SlashCommandDelegate {
.inset(true)
.spacing(ListItemSpacing::Dense)
.selected(selected)
.tooltip({
let description = info.description.clone();
move |cx| cx.new_view(|_| Tooltip::new(description.clone())).into()
})
.child(
v_flex()
.group(format!("command-entry-label-{ix}"))
.w_full()
.py_0p5()
.min_w(px(250.))
.max_w(px(400.))
.child(
h_flex()
.gap_1p5()
@@ -192,7 +198,7 @@ impl PickerDelegate for SlashCommandDelegate {
{
label.push_str(&args);
}
Label::new(label).size(LabelSize::Small)
Label::new(label).single_line().size(LabelSize::Small)
}))
.children(info.args.clone().filter(|_| !selected).map(
|args| {
@@ -200,6 +206,7 @@ impl PickerDelegate for SlashCommandDelegate {
.font_buffer(cx)
.child(
Label::new(args)
.single_line()
.size(LabelSize::Small)
.color(Color::Muted),
)
@@ -210,9 +217,11 @@ impl PickerDelegate for SlashCommandDelegate {
)),
)
.child(
Label::new(info.description.clone())
.size(LabelSize::Small)
.color(Color::Muted),
div().overflow_hidden().text_ellipsis().child(
Label::new(info.description.clone())
.size(LabelSize::Small)
.color(Color::Muted),
),
),
),
),

View File

@@ -32,7 +32,7 @@ use telemetry_events::{AssistantEvent, AssistantKind, AssistantPhase};
use terminal::Terminal;
use terminal_view::TerminalView;
use theme::ThemeSettings;
use ui::{prelude::*, IconButtonShape, Tooltip};
use ui::{prelude::*, text_for_action, IconButtonShape, Tooltip};
use util::ResultExt;
use workspace::{notifications::NotificationId, Toast, Workspace};
@@ -704,7 +704,7 @@ impl PromptEditor {
cx,
);
editor.set_soft_wrap_mode(language::language_settings::SoftWrap::EditorWidth, cx);
editor.set_placeholder_text("Add a prompt…", cx);
editor.set_placeholder_text(Self::placeholder_text(cx), cx);
editor
});
@@ -737,6 +737,14 @@ impl PromptEditor {
this
}
fn placeholder_text(cx: &WindowContext) -> String {
let context_keybinding = text_for_action(&crate::ToggleFocus, cx)
.map(|keybinding| format!("{keybinding} for context"))
.unwrap_or_default();
format!("Generate…{context_keybinding} • ↓↑ for history")
}
fn subscribe_to_editor(&mut self, cx: &mut ViewContext<Self>) {
self.editor_subscriptions.clear();
self.editor_subscriptions

View File

@@ -15,20 +15,33 @@ doctest = false
[dependencies]
anyhow.workspace = true
assistant_tool.workspace = true
chrono.workspace = true
client.workspace = true
collections.workspace = true
command_palette_hooks.workspace = true
context_server.workspace = true
editor.workspace = true
feature_flags.workspace = true
futures.workspace = true
gpui.workspace = true
language.workspace = true
language_model.workspace = true
language_model_selector.workspace = true
language_models.workspace = true
log.workspace = true
markdown.workspace = true
picker.workspace = true
project.workspace = true
proto.workspace = true
settings.workspace = true
serde.workspace = true
serde_json.workspace = true
settings.workspace = true
smol.workspace = true
theme.workspace = true
time.workspace = true
time_format.workspace = true
ui.workspace = true
unindent.workspace = true
util.workspace = true
uuid.workspace = true
workspace.workspace = true

View File

@@ -0,0 +1,242 @@
use std::sync::Arc;
use assistant_tool::ToolWorkingSet;
use collections::HashMap;
use gpui::{
list, AnyElement, AppContext, Empty, ListAlignment, ListState, Model, StyleRefinement,
Subscription, TextStyleRefinement, View, WeakView,
};
use language::LanguageRegistry;
use language_model::Role;
use markdown::{Markdown, MarkdownStyle};
use settings::Settings as _;
use theme::ThemeSettings;
use ui::prelude::*;
use workspace::Workspace;
use crate::thread::{MessageId, Thread, ThreadError, ThreadEvent};
pub struct ActiveThread {
workspace: WeakView<Workspace>,
language_registry: Arc<LanguageRegistry>,
tools: Arc<ToolWorkingSet>,
thread: Model<Thread>,
messages: Vec<MessageId>,
list_state: ListState,
rendered_messages_by_id: HashMap<MessageId, View<Markdown>>,
last_error: Option<ThreadError>,
_subscriptions: Vec<Subscription>,
}
impl ActiveThread {
pub fn new(
thread: Model<Thread>,
workspace: WeakView<Workspace>,
language_registry: Arc<LanguageRegistry>,
tools: Arc<ToolWorkingSet>,
cx: &mut ViewContext<Self>,
) -> Self {
let subscriptions = vec![
cx.observe(&thread, |_, _, cx| cx.notify()),
cx.subscribe(&thread, Self::handle_thread_event),
];
let mut this = Self {
workspace,
language_registry,
tools,
thread: thread.clone(),
messages: Vec::new(),
rendered_messages_by_id: HashMap::default(),
list_state: ListState::new(0, ListAlignment::Bottom, px(1024.), {
let this = cx.view().downgrade();
move |ix, cx: &mut WindowContext| {
this.update(cx, |this, cx| this.render_message(ix, cx))
.unwrap()
}
}),
last_error: None,
_subscriptions: subscriptions,
};
for message in thread.read(cx).messages().cloned().collect::<Vec<_>>() {
this.push_message(&message.id, message.text.clone(), cx);
}
this
}
pub fn is_empty(&self) -> bool {
self.messages.is_empty()
}
pub fn summary(&self, cx: &AppContext) -> Option<SharedString> {
self.thread.read(cx).summary()
}
pub fn last_error(&self) -> Option<ThreadError> {
self.last_error.clone()
}
pub fn clear_last_error(&mut self) {
self.last_error.take();
}
fn push_message(&mut self, id: &MessageId, text: String, cx: &mut ViewContext<Self>) {
let old_len = self.messages.len();
self.messages.push(*id);
self.list_state.splice(old_len..old_len, 1);
let theme_settings = ThemeSettings::get_global(cx);
let ui_font_size = TextSize::Default.rems(cx);
let buffer_font_size = theme_settings.buffer_font_size;
let mut text_style = cx.text_style();
text_style.refine(&TextStyleRefinement {
font_family: Some(theme_settings.ui_font.family.clone()),
font_size: Some(ui_font_size.into()),
color: Some(cx.theme().colors().text),
..Default::default()
});
let markdown_style = MarkdownStyle {
base_text_style: text_style,
syntax: cx.theme().syntax().clone(),
selection_background_color: cx.theme().players().local().selection,
code_block: StyleRefinement {
text: Some(TextStyleRefinement {
font_family: Some(theme_settings.buffer_font.family.clone()),
font_size: Some(buffer_font_size.into()),
..Default::default()
}),
..Default::default()
},
inline_code: TextStyleRefinement {
font_family: Some(theme_settings.buffer_font.family.clone()),
font_size: Some(ui_font_size.into()),
background_color: Some(cx.theme().colors().editor_background),
..Default::default()
},
..Default::default()
};
let markdown = cx.new_view(|cx| {
Markdown::new(
text,
markdown_style,
Some(self.language_registry.clone()),
None,
cx,
)
});
self.rendered_messages_by_id.insert(*id, markdown);
}
fn handle_thread_event(
&mut self,
_: Model<Thread>,
event: &ThreadEvent,
cx: &mut ViewContext<Self>,
) {
match event {
ThreadEvent::ShowError(error) => {
self.last_error = Some(error.clone());
}
ThreadEvent::StreamedCompletion => {}
ThreadEvent::SummaryChanged => {}
ThreadEvent::StreamedAssistantText(message_id, text) => {
if let Some(markdown) = self.rendered_messages_by_id.get_mut(&message_id) {
markdown.update(cx, |markdown, cx| {
markdown.append(text, cx);
});
}
}
ThreadEvent::MessageAdded(message_id) => {
if let Some(message_text) = self
.thread
.read(cx)
.message(*message_id)
.map(|message| message.text.clone())
{
self.push_message(message_id, message_text, cx);
}
cx.notify();
}
ThreadEvent::UsePendingTools => {
let pending_tool_uses = self
.thread
.read(cx)
.pending_tool_uses()
.into_iter()
.filter(|tool_use| tool_use.status.is_idle())
.cloned()
.collect::<Vec<_>>();
for tool_use in pending_tool_uses {
if let Some(tool) = self.tools.tool(&tool_use.name, cx) {
let task = tool.run(tool_use.input, self.workspace.clone(), cx);
self.thread.update(cx, |thread, cx| {
thread.insert_tool_output(
tool_use.assistant_message_id,
tool_use.id.clone(),
task,
cx,
);
});
}
}
}
ThreadEvent::ToolFinished { .. } => {}
}
}
fn render_message(&self, ix: usize, cx: &mut ViewContext<Self>) -> AnyElement {
let message_id = self.messages[ix];
let Some(message) = self.thread.read(cx).message(message_id) else {
return Empty.into_any();
};
let Some(markdown) = self.rendered_messages_by_id.get(&message_id) else {
return Empty.into_any();
};
let (role_icon, role_name) = match message.role {
Role::User => (IconName::Person, "You"),
Role::Assistant => (IconName::ZedAssistant, "Assistant"),
Role::System => (IconName::Settings, "System"),
};
div()
.id(("message-container", ix))
.p_2()
.child(
v_flex()
.border_1()
.border_color(cx.theme().colors().border_variant)
.rounded_md()
.child(
h_flex()
.justify_between()
.p_1p5()
.border_b_1()
.border_color(cx.theme().colors().border_variant)
.child(
h_flex()
.gap_2()
.child(Icon::new(role_icon).size(IconSize::Small))
.child(Label::new(role_name).size(LabelSize::Small)),
),
)
.child(v_flex().p_1p5().text_ui(cx).child(markdown.clone())),
)
.into_any()
}
}
impl Render for ActiveThread {
fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
list(self.list_state.clone()).flex_1()
}
}

View File

@@ -1,6 +1,10 @@
mod active_thread;
mod assistant_panel;
mod context_picker;
mod message_editor;
mod thread;
mod thread_history;
mod thread_store;
use command_palette_hooks::CommandPaletteFilter;
use feature_flags::{Assistant2FeatureFlag, FeatureFlagAppExt};
@@ -10,7 +14,13 @@ pub use crate::assistant_panel::AssistantPanel;
actions!(
assistant2,
[ToggleFocus, NewThread, ToggleModelSelector, Chat]
[
ToggleFocus,
NewThread,
ToggleModelSelector,
OpenHistory,
Chat
]
);
const NAMESPACE: &str = "assistant2";

View File

@@ -2,19 +2,26 @@ use std::sync::Arc;
use anyhow::Result;
use assistant_tool::ToolWorkingSet;
use client::zed_urls;
use gpui::{
prelude::*, px, Action, AppContext, AsyncWindowContext, EventEmitter, FocusHandle,
FocusableView, Model, Pixels, Subscription, Task, View, ViewContext, WeakView, WindowContext,
prelude::*, px, svg, Action, AnyElement, AppContext, AsyncWindowContext, EventEmitter,
FocusHandle, FocusableView, FontWeight, Model, Pixels, Task, View, ViewContext, WeakView,
WindowContext,
};
use language_model::{LanguageModelRegistry, Role};
use language::LanguageRegistry;
use language_model::LanguageModelRegistry;
use language_model_selector::LanguageModelSelector;
use ui::{prelude::*, ButtonLike, Divider, IconButtonShape, Tab, Tooltip};
use time::UtcOffset;
use ui::{prelude::*, ButtonLike, Divider, IconButtonShape, KeyBinding, Tab, Tooltip};
use workspace::dock::{DockPosition, Panel, PanelEvent};
use workspace::Workspace;
use crate::active_thread::ActiveThread;
use crate::message_editor::MessageEditor;
use crate::thread::{Message, Thread, ThreadEvent};
use crate::{NewThread, ToggleFocus, ToggleModelSelector};
use crate::thread::{ThreadError, ThreadId};
use crate::thread_history::{PastThread, ThreadHistory};
use crate::thread_store::ThreadStore;
use crate::{NewThread, OpenHistory, ToggleFocus, ToggleModelSelector};
pub fn init(cx: &mut AppContext) {
cx.observe_new_views(
@@ -27,12 +34,21 @@ pub fn init(cx: &mut AppContext) {
.detach();
}
enum ActiveView {
Thread,
History,
}
pub struct AssistantPanel {
workspace: WeakView<Workspace>,
thread: Model<Thread>,
language_registry: Arc<LanguageRegistry>,
thread_store: Model<ThreadStore>,
thread: View<ActiveThread>,
message_editor: View<MessageEditor>,
tools: Arc<ToolWorkingSet>,
_subscriptions: Vec<Subscription>,
local_timezone: UtcOffset,
active_view: ActiveView,
history: View<ThreadHistory>,
}
impl AssistantPanel {
@@ -42,84 +58,111 @@ impl AssistantPanel {
) -> Task<Result<View<Self>>> {
cx.spawn(|mut cx| async move {
let tools = Arc::new(ToolWorkingSet::default());
let thread_store = workspace
.update(&mut cx, |workspace, cx| {
let project = workspace.project().clone();
ThreadStore::new(project, tools.clone(), cx)
})?
.await?;
workspace.update(&mut cx, |workspace, cx| {
cx.new_view(|cx| Self::new(workspace, tools, cx))
cx.new_view(|cx| Self::new(workspace, thread_store, tools, cx))
})
})
}
fn new(workspace: &Workspace, tools: Arc<ToolWorkingSet>, cx: &mut ViewContext<Self>) -> Self {
let thread = cx.new_model(|cx| Thread::new(tools.clone(), cx));
let subscriptions = vec![
cx.observe(&thread, |_, _, cx| cx.notify()),
cx.subscribe(&thread, Self::handle_thread_event),
];
fn new(
workspace: &Workspace,
thread_store: Model<ThreadStore>,
tools: Arc<ToolWorkingSet>,
cx: &mut ViewContext<Self>,
) -> Self {
let thread = thread_store.update(cx, |this, cx| this.create_thread(cx));
let language_registry = workspace.project().read(cx).languages().clone();
let workspace = workspace.weak_handle();
let weak_self = cx.view().downgrade();
Self {
workspace: workspace.weak_handle(),
thread: thread.clone(),
message_editor: cx.new_view(|cx| MessageEditor::new(thread, cx)),
active_view: ActiveView::Thread,
workspace: workspace.clone(),
language_registry: language_registry.clone(),
thread_store: thread_store.clone(),
thread: cx.new_view(|cx| {
ActiveThread::new(
thread.clone(),
workspace,
language_registry,
tools.clone(),
cx,
)
}),
message_editor: cx.new_view(|cx| MessageEditor::new(thread.clone(), cx)),
tools,
_subscriptions: subscriptions,
local_timezone: UtcOffset::from_whole_seconds(
chrono::Local::now().offset().local_minus_utc(),
)
.unwrap(),
history: cx.new_view(|cx| ThreadHistory::new(weak_self, thread_store, cx)),
}
}
pub(crate) fn local_timezone(&self) -> UtcOffset {
self.local_timezone
}
fn new_thread(&mut self, cx: &mut ViewContext<Self>) {
let tools = self.thread.read(cx).tools().clone();
let thread = cx.new_model(|cx| Thread::new(tools, cx));
let subscriptions = vec![
cx.observe(&thread, |_, _, cx| cx.notify()),
cx.subscribe(&thread, Self::handle_thread_event),
];
self.message_editor = cx.new_view(|cx| MessageEditor::new(thread.clone(), cx));
self.thread = thread;
self._subscriptions = subscriptions;
let thread = self
.thread_store
.update(cx, |this, cx| this.create_thread(cx));
self.active_view = ActiveView::Thread;
self.thread = cx.new_view(|cx| {
ActiveThread::new(
thread.clone(),
self.workspace.clone(),
self.language_registry.clone(),
self.tools.clone(),
cx,
)
});
self.message_editor = cx.new_view(|cx| MessageEditor::new(thread, cx));
self.message_editor.focus_handle(cx).focus(cx);
}
fn handle_thread_event(
&mut self,
_: Model<Thread>,
event: &ThreadEvent,
cx: &mut ViewContext<Self>,
) {
match event {
ThreadEvent::StreamedCompletion => {}
ThreadEvent::UsePendingTools => {
let pending_tool_uses = self
.thread
.read(cx)
.pending_tool_uses()
.into_iter()
.filter(|tool_use| tool_use.status.is_idle())
.cloned()
.collect::<Vec<_>>();
pub(crate) fn open_thread(&mut self, thread_id: &ThreadId, cx: &mut ViewContext<Self>) {
let Some(thread) = self
.thread_store
.update(cx, |this, cx| this.open_thread(thread_id, cx))
else {
return;
};
for tool_use in pending_tool_uses {
if let Some(tool) = self.tools.tool(&tool_use.name, cx) {
let task = tool.run(tool_use.input, self.workspace.clone(), cx);
self.active_view = ActiveView::Thread;
self.thread = cx.new_view(|cx| {
ActiveThread::new(
thread.clone(),
self.workspace.clone(),
self.language_registry.clone(),
self.tools.clone(),
cx,
)
});
self.message_editor = cx.new_view(|cx| MessageEditor::new(thread, cx));
self.message_editor.focus_handle(cx).focus(cx);
}
self.thread.update(cx, |thread, cx| {
thread.insert_tool_output(
tool_use.assistant_message_id,
tool_use.id.clone(),
task,
cx,
);
});
}
}
}
ThreadEvent::ToolFinished { .. } => {}
}
pub(crate) fn delete_thread(&mut self, thread_id: &ThreadId, cx: &mut ViewContext<Self>) {
self.thread_store
.update(cx, |this, cx| this.delete_thread(thread_id, cx));
}
}
impl FocusableView for AssistantPanel {
fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
self.message_editor.focus_handle(cx)
match self.active_view {
ActiveView::Thread => self.message_editor.focus_handle(cx),
ActiveView::History => self.history.focus_handle(cx),
}
}
}
@@ -178,7 +221,7 @@ impl AssistantPanel {
.bg(cx.theme().colors().tab_bar_background)
.border_b_1()
.border_color(cx.theme().colors().border_variant)
.child(h_flex().child(Label::new("Thread Title Goes Here")))
.child(h_flex().children(self.thread.read(cx).summary(cx).map(Label::new)))
.child(
h_flex()
.gap(DynamicSpacing::Base08.rems(cx))
@@ -200,8 +243,8 @@ impl AssistantPanel {
)
}
})
.on_click(move |_event, _cx| {
println!("New Thread");
.on_click(move |_event, cx| {
cx.dispatch_action(NewThread.boxed_clone());
}),
)
.child(
@@ -209,9 +252,19 @@ impl AssistantPanel {
.shape(IconButtonShape::Square)
.icon_size(IconSize::Small)
.style(ButtonStyle::Subtle)
.tooltip(move |cx| Tooltip::text("Open History", cx))
.on_click(move |_event, _cx| {
println!("Open History");
.tooltip({
let focus_handle = focus_handle.clone();
move |cx| {
Tooltip::for_action_in(
"Open History",
&OpenHistory,
&focus_handle,
cx,
)
}
})
.on_click(move |_event, cx| {
cx.dispatch_action(OpenHistory.boxed_clone());
}),
)
.child(
@@ -278,38 +331,270 @@ impl AssistantPanel {
)
}
fn render_message(&self, message: Message, cx: &mut ViewContext<Self>) -> impl IntoElement {
let (role_icon, role_name) = match message.role {
Role::User => (IconName::Person, "You"),
Role::Assistant => (IconName::ZedAssistant, "Assistant"),
Role::System => (IconName::Settings, "System"),
};
fn render_active_thread_or_empty_state(&self, cx: &mut ViewContext<Self>) -> AnyElement {
if self.thread.read(cx).is_empty() {
return self.render_thread_empty_state(cx).into_any_element();
}
self.thread.clone().into_any()
}
fn render_thread_empty_state(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let recent_threads = self
.thread_store
.update(cx, |this, cx| this.recent_threads(3, cx));
v_flex()
.border_1()
.border_color(cx.theme().colors().border_variant)
.rounded_md()
.gap_2()
.mx_auto()
.child(
v_flex().w_full().child(
svg()
.path("icons/logo_96.svg")
.text_color(cx.theme().colors().text)
.w(px(40.))
.h(px(40.))
.mx_auto()
.mb_4(),
),
)
.child(v_flex())
.child(
h_flex()
.justify_between()
.p_1p5()
.border_b_1()
.border_color(cx.theme().colors().border_variant)
.w_full()
.justify_center()
.child(Label::new("Context Examples:").size(LabelSize::Small)),
)
.child(
h_flex()
.gap_2()
.justify_center()
.child(
h_flex()
.gap_2()
.child(Icon::new(role_icon).size(IconSize::Small))
.child(Label::new(role_name).size(LabelSize::Small)),
.gap_1()
.p_0p5()
.rounded_md()
.border_1()
.border_color(cx.theme().colors().border_variant)
.child(
Icon::new(IconName::Terminal)
.size(IconSize::Small)
.color(Color::Disabled),
)
.child(Label::new("Terminal").size(LabelSize::Small)),
)
.child(
h_flex()
.gap_1()
.p_0p5()
.rounded_md()
.border_1()
.border_color(cx.theme().colors().border_variant)
.child(
Icon::new(IconName::Folder)
.size(IconSize::Small)
.color(Color::Disabled),
)
.child(Label::new("/src/components").size(LabelSize::Small)),
),
)
.child(v_flex().p_1p5().child(Label::new(message.text.clone())))
.when(!recent_threads.is_empty(), |parent| {
parent
.child(
h_flex()
.w_full()
.justify_center()
.child(Label::new("Recent Threads:").size(LabelSize::Small)),
)
.child(
v_flex().gap_2().children(
recent_threads
.into_iter()
.map(|thread| PastThread::new(thread, cx.view().downgrade())),
),
)
.child(
h_flex().w_full().justify_center().child(
Button::new("view-all-past-threads", "View All Past Threads")
.style(ButtonStyle::Subtle)
.label_size(LabelSize::Small)
.key_binding(KeyBinding::for_action_in(
&OpenHistory,
&self.focus_handle(cx),
cx,
))
.on_click(move |_event, cx| {
cx.dispatch_action(OpenHistory.boxed_clone());
}),
),
)
})
}
fn render_last_error(&self, cx: &mut ViewContext<Self>) -> Option<AnyElement> {
let last_error = self.thread.read(cx).last_error()?;
Some(
div()
.absolute()
.right_3()
.bottom_12()
.max_w_96()
.py_2()
.px_3()
.elevation_2(cx)
.occlude()
.child(match last_error {
ThreadError::PaymentRequired => self.render_payment_required_error(cx),
ThreadError::MaxMonthlySpendReached => {
self.render_max_monthly_spend_reached_error(cx)
}
ThreadError::Message(error_message) => {
self.render_error_message(&error_message, cx)
}
})
.into_any(),
)
}
fn render_payment_required_error(&self, cx: &mut ViewContext<Self>) -> AnyElement {
const ERROR_MESSAGE: &str = "Free tier exceeded. Subscribe and add payment to continue using Zed LLMs. You'll be billed at cost for tokens used.";
v_flex()
.gap_0p5()
.child(
h_flex()
.gap_1p5()
.items_center()
.child(Icon::new(IconName::XCircle).color(Color::Error))
.child(Label::new("Free Usage Exceeded").weight(FontWeight::MEDIUM)),
)
.child(
div()
.id("error-message")
.max_h_24()
.overflow_y_scroll()
.child(Label::new(ERROR_MESSAGE)),
)
.child(
h_flex()
.justify_end()
.mt_1()
.child(Button::new("subscribe", "Subscribe").on_click(cx.listener(
|this, _, cx| {
this.thread.update(cx, |this, _cx| {
this.clear_last_error();
});
cx.open_url(&zed_urls::account_url(cx));
cx.notify();
},
)))
.child(Button::new("dismiss", "Dismiss").on_click(cx.listener(
|this, _, cx| {
this.thread.update(cx, |this, _cx| {
this.clear_last_error();
});
cx.notify();
},
))),
)
.into_any()
}
fn render_max_monthly_spend_reached_error(&self, cx: &mut ViewContext<Self>) -> AnyElement {
const ERROR_MESSAGE: &str = "You have reached your maximum monthly spend. Increase your spend limit to continue using Zed LLMs.";
v_flex()
.gap_0p5()
.child(
h_flex()
.gap_1p5()
.items_center()
.child(Icon::new(IconName::XCircle).color(Color::Error))
.child(Label::new("Max Monthly Spend Reached").weight(FontWeight::MEDIUM)),
)
.child(
div()
.id("error-message")
.max_h_24()
.overflow_y_scroll()
.child(Label::new(ERROR_MESSAGE)),
)
.child(
h_flex()
.justify_end()
.mt_1()
.child(
Button::new("subscribe", "Update Monthly Spend Limit").on_click(
cx.listener(|this, _, cx| {
this.thread.update(cx, |this, _cx| {
this.clear_last_error();
});
cx.open_url(&zed_urls::account_url(cx));
cx.notify();
}),
),
)
.child(Button::new("dismiss", "Dismiss").on_click(cx.listener(
|this, _, cx| {
this.thread.update(cx, |this, _cx| {
this.clear_last_error();
});
cx.notify();
},
))),
)
.into_any()
}
fn render_error_message(
&self,
error_message: &SharedString,
cx: &mut ViewContext<Self>,
) -> AnyElement {
v_flex()
.gap_0p5()
.child(
h_flex()
.gap_1p5()
.items_center()
.child(Icon::new(IconName::XCircle).color(Color::Error))
.child(
Label::new("Error interacting with language model")
.weight(FontWeight::MEDIUM),
),
)
.child(
div()
.id("error-message")
.max_h_32()
.overflow_y_scroll()
.child(Label::new(error_message.clone())),
)
.child(
h_flex()
.justify_end()
.mt_1()
.child(Button::new("dismiss", "Dismiss").on_click(cx.listener(
|this, _, cx| {
this.thread.update(cx, |this, _cx| {
this.clear_last_error();
});
cx.notify();
},
))),
)
.into_any()
}
}
impl Render for AssistantPanel {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let messages = self.thread.read(cx).messages().cloned().collect::<Vec<_>>();
v_flex()
.key_context("AssistantPanel2")
.justify_between()
@@ -317,26 +602,23 @@ impl Render for AssistantPanel {
.on_action(cx.listener(|this, _: &NewThread, cx| {
this.new_thread(cx);
}))
.on_action(cx.listener(|this, _: &OpenHistory, cx| {
this.active_view = ActiveView::History;
this.history.focus_handle(cx).focus(cx);
cx.notify();
}))
.child(self.render_toolbar(cx))
.child(
v_flex()
.id("message-list")
.gap_2()
.size_full()
.p_2()
.overflow_y_scroll()
.bg(cx.theme().colors().panel_background)
.children(
messages
.into_iter()
.map(|message| self.render_message(message, cx)),
),
)
.child(
h_flex()
.border_t_1()
.border_color(cx.theme().colors().border_variant)
.child(self.message_editor.clone()),
)
.map(|parent| match self.active_view {
ActiveView::Thread => parent
.child(self.render_active_thread_or_empty_state(cx))
.child(
h_flex()
.border_t_1()
.border_color(cx.theme().colors().border_variant)
.child(self.message_editor.clone()),
)
.children(self.render_last_error(cx)),
ActiveView::History => parent.child(self.history.clone()),
})
}
}

View File

@@ -0,0 +1,197 @@
use std::sync::Arc;
use gpui::{DismissEvent, SharedString, Task, WeakView};
use picker::{Picker, PickerDelegate, PickerEditorPosition};
use ui::{prelude::*, ListItem, ListItemSpacing, PopoverMenu, PopoverTrigger, Tooltip};
use crate::message_editor::MessageEditor;
#[derive(IntoElement)]
pub(super) struct ContextPicker<T: PopoverTrigger> {
message_editor: WeakView<MessageEditor>,
trigger: T,
}
#[derive(Clone)]
struct ContextPickerEntry {
name: SharedString,
description: SharedString,
icon: IconName,
}
pub(crate) struct ContextPickerDelegate {
all_entries: Vec<ContextPickerEntry>,
filtered_entries: Vec<ContextPickerEntry>,
message_editor: WeakView<MessageEditor>,
selected_ix: usize,
}
impl<T: PopoverTrigger> ContextPicker<T> {
pub(crate) fn new(message_editor: WeakView<MessageEditor>, trigger: T) -> Self {
ContextPicker {
message_editor,
trigger,
}
}
}
impl PickerDelegate for ContextPickerDelegate {
type ListItem = ListItem;
fn match_count(&self) -> usize {
self.filtered_entries.len()
}
fn selected_index(&self) -> usize {
self.selected_ix
}
fn set_selected_index(&mut self, ix: usize, cx: &mut ViewContext<Picker<Self>>) {
self.selected_ix = ix.min(self.filtered_entries.len().saturating_sub(1));
cx.notify();
}
fn placeholder_text(&self, _cx: &mut WindowContext) -> Arc<str> {
"Select a context source…".into()
}
fn update_matches(&mut self, query: String, cx: &mut ViewContext<Picker<Self>>) -> Task<()> {
let all_commands = self.all_entries.clone();
cx.spawn(|this, mut cx| async move {
let filtered_commands = cx
.background_executor()
.spawn(async move {
if query.is_empty() {
all_commands
} else {
all_commands
.into_iter()
.filter(|model_info| {
model_info
.name
.to_lowercase()
.contains(&query.to_lowercase())
})
.collect()
}
})
.await;
this.update(&mut cx, |this, cx| {
this.delegate.filtered_entries = filtered_commands;
this.delegate.set_selected_index(0, cx);
cx.notify();
})
.ok();
})
}
fn confirm(&mut self, _secondary: bool, cx: &mut ViewContext<Picker<Self>>) {
if let Some(entry) = self.filtered_entries.get(self.selected_ix) {
self.message_editor
.update(cx, |_message_editor, _cx| {
println!("Insert context from {}", entry.name);
})
.ok();
cx.emit(DismissEvent);
}
}
fn dismissed(&mut self, _cx: &mut ViewContext<Picker<Self>>) {}
fn editor_position(&self) -> PickerEditorPosition {
PickerEditorPosition::End
}
fn render_match(
&self,
ix: usize,
selected: bool,
_cx: &mut ViewContext<Picker<Self>>,
) -> Option<Self::ListItem> {
let entry = self.filtered_entries.get(ix)?;
Some(
ListItem::new(ix)
.inset(true)
.spacing(ListItemSpacing::Dense)
.selected(selected)
.tooltip({
let description = entry.description.clone();
move |cx| cx.new_view(|_cx| Tooltip::new(description.clone())).into()
})
.child(
v_flex()
.group(format!("context-entry-label-{ix}"))
.w_full()
.py_0p5()
.min_w(px(250.))
.max_w(px(400.))
.child(
h_flex()
.gap_1p5()
.child(Icon::new(entry.icon).size(IconSize::XSmall))
.child(
Label::new(entry.name.clone())
.single_line()
.size(LabelSize::Small),
),
)
.child(
div().overflow_hidden().text_ellipsis().child(
Label::new(entry.description.clone())
.size(LabelSize::Small)
.color(Color::Muted),
),
),
),
)
}
}
impl<T: PopoverTrigger> RenderOnce for ContextPicker<T> {
fn render(self, cx: &mut WindowContext) -> impl IntoElement {
let entries = vec![
ContextPickerEntry {
name: "directory".into(),
description: "Insert any directory".into(),
icon: IconName::Folder,
},
ContextPickerEntry {
name: "file".into(),
description: "Insert any file".into(),
icon: IconName::File,
},
ContextPickerEntry {
name: "web".into(),
description: "Fetch content from URL".into(),
icon: IconName::Globe,
},
];
let delegate = ContextPickerDelegate {
all_entries: entries.clone(),
message_editor: self.message_editor.clone(),
filtered_entries: entries,
selected_ix: 0,
};
let picker =
cx.new_view(|cx| Picker::uniform_list(delegate, cx).max_height(Some(rems(20.).into())));
let handle = self
.message_editor
.update(cx, |this, _| this.context_picker_handle.clone())
.ok();
PopoverMenu::new("context-picker")
.menu(move |_cx| Some(picker.clone()))
.trigger(self.trigger)
.attach(gpui::AnchorCorner::TopLeft)
.anchor(gpui::AnchorCorner::BottomLeft)
.offset(gpui::Point {
x: px(0.0),
y: px(-16.0),
})
.when_some(handle, |this, handle| this.with_handle(handle))
}
}

View File

@@ -1,16 +1,22 @@
use editor::{Editor, EditorElement, EditorStyle};
use gpui::{AppContext, FocusableView, Model, TextStyle, View};
use language_model::{LanguageModelRegistry, LanguageModelRequestTool};
use picker::Picker;
use settings::Settings;
use theme::ThemeSettings;
use ui::{prelude::*, ButtonLike, CheckboxWithLabel, ElevationIndex, KeyBinding};
use ui::{
prelude::*, ButtonLike, CheckboxWithLabel, ElevationIndex, IconButtonShape, KeyBinding,
PopoverMenuHandle,
};
use crate::context_picker::{ContextPicker, ContextPickerDelegate};
use crate::thread::{RequestKind, Thread};
use crate::Chat;
pub struct MessageEditor {
thread: Model<Thread>,
editor: View<Editor>,
pub(crate) context_picker_handle: PopoverMenuHandle<Picker<ContextPickerDelegate>>,
use_tools: bool,
}
@@ -24,6 +30,7 @@ impl MessageEditor {
editor
}),
context_picker_handle: PopoverMenuHandle::default(),
use_tools: false,
}
}
@@ -56,7 +63,7 @@ impl MessageEditor {
});
self.thread.update(cx, |thread, cx| {
thread.insert_user_message(user_message);
thread.insert_user_message(user_message, cx);
let mut request = thread.to_completion_request(request_kind, cx);
if self.use_tools {
@@ -98,6 +105,14 @@ impl Render for MessageEditor {
.gap_2()
.p_2()
.bg(cx.theme().colors().editor_background)
.child(
h_flex().gap_2().child(ContextPicker::new(
cx.view().downgrade(),
IconButton::new("add-context", IconName::Plus)
.shape(IconButtonShape::Square)
.icon_size(IconSize::Small),
)),
)
.child({
let settings = ThemeSettings::get_global(cx);
let text_style = TextStyle {
@@ -123,26 +138,17 @@ impl Render for MessageEditor {
.child(
h_flex()
.justify_between()
.child(
h_flex()
.child(
Button::new("add-context", "Add Context")
.style(ButtonStyle::Filled)
.icon(IconName::Plus)
.icon_position(IconPosition::Start),
)
.child(CheckboxWithLabel::new(
"use-tools",
Label::new("Tools"),
self.use_tools.into(),
cx.listener(|this, selection, _cx| {
this.use_tools = match selection {
Selection::Selected => true,
Selection::Unselected | Selection::Indeterminate => false,
};
}),
)),
)
.child(h_flex().gap_2().child(CheckboxWithLabel::new(
"use-tools",
Label::new("Tools"),
self.use_tools.into(),
cx.listener(|this, selection, _cx| {
this.use_tools = match selection {
Selection::Selected => true,
Selection::Unselected | Selection::Indeterminate => false,
};
}),
)))
.child(
h_flex()
.gap_2()

View File

@@ -2,23 +2,41 @@ use std::sync::Arc;
use anyhow::Result;
use assistant_tool::ToolWorkingSet;
use chrono::{DateTime, Utc};
use collections::HashMap;
use futures::future::Shared;
use futures::{FutureExt as _, StreamExt as _};
use gpui::{AppContext, EventEmitter, ModelContext, Task};
use gpui::{AppContext, EventEmitter, ModelContext, SharedString, Task};
use language_model::{
LanguageModel, LanguageModelCompletionEvent, LanguageModelRequest, LanguageModelRequestMessage,
LanguageModelToolResult, LanguageModelToolUse, LanguageModelToolUseId, MessageContent, Role,
StopReason,
LanguageModel, LanguageModelCompletionEvent, LanguageModelRegistry, LanguageModelRequest,
LanguageModelRequestMessage, LanguageModelToolResult, LanguageModelToolUse,
LanguageModelToolUseId, MessageContent, Role, StopReason,
};
use language_models::provider::cloud::{MaxMonthlySpendReachedError, PaymentRequiredError};
use serde::{Deserialize, Serialize};
use util::post_inc;
use util::{post_inc, TryFutureExt as _};
use uuid::Uuid;
#[derive(Debug, Clone, Copy)]
pub enum RequestKind {
Chat,
}
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Serialize, Deserialize)]
pub struct ThreadId(Arc<str>);
impl ThreadId {
pub fn new() -> Self {
Self(Uuid::new_v4().to_string().into())
}
}
impl std::fmt::Display for ThreadId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, Serialize, Deserialize)]
pub struct MessageId(usize);
@@ -38,6 +56,10 @@ pub struct Message {
/// A thread of conversation with the LLM.
pub struct Thread {
id: ThreadId,
updated_at: DateTime<Utc>,
summary: Option<SharedString>,
pending_summary: Task<Option<()>>,
messages: Vec<Message>,
next_message_id: MessageId,
completion_count: usize,
@@ -51,6 +73,10 @@ pub struct Thread {
impl Thread {
pub fn new(tools: Arc<ToolWorkingSet>, _cx: &mut ModelContext<Self>) -> Self {
Self {
id: ThreadId::new(),
updated_at: Utc::now(),
summary: None,
pending_summary: Task::ready(None),
messages: Vec::new(),
next_message_id: MessageId(0),
completion_count: 0,
@@ -62,6 +88,35 @@ impl Thread {
}
}
pub fn id(&self) -> &ThreadId {
&self.id
}
pub fn is_empty(&self) -> bool {
self.messages.is_empty()
}
pub fn updated_at(&self) -> DateTime<Utc> {
self.updated_at
}
pub fn touch_updated_at(&mut self) {
self.updated_at = Utc::now();
}
pub fn summary(&self) -> Option<SharedString> {
self.summary.clone()
}
pub fn set_summary(&mut self, summary: impl Into<SharedString>, cx: &mut ModelContext<Self>) {
self.summary = Some(summary.into());
cx.emit(ThreadEvent::SummaryChanged);
}
pub fn message(&self, id: MessageId) -> Option<&Message> {
self.messages.iter().find(|message| message.id == id)
}
pub fn messages(&self) -> impl Iterator<Item = &Message> {
self.messages.iter()
}
@@ -74,12 +129,24 @@ impl Thread {
self.pending_tool_uses_by_id.values().collect()
}
pub fn insert_user_message(&mut self, text: impl Into<String>) {
pub fn insert_user_message(&mut self, text: impl Into<String>, cx: &mut ModelContext<Self>) {
self.insert_message(Role::User, text, cx)
}
pub fn insert_message(
&mut self,
role: Role,
text: impl Into<String>,
cx: &mut ModelContext<Self>,
) {
let id = self.next_message_id.post_inc();
self.messages.push(Message {
id: self.next_message_id.post_inc(),
role: Role::User,
id,
role,
text: text.into(),
});
self.touch_updated_at();
cx.emit(ThreadEvent::MessageAdded(id));
}
pub fn to_completion_request(
@@ -149,11 +216,7 @@ impl Thread {
thread.update(&mut cx, |thread, cx| {
match event {
LanguageModelCompletionEvent::StartMessage { .. } => {
thread.messages.push(Message {
id: thread.next_message_id.post_inc(),
role: Role::Assistant,
text: String::new(),
});
thread.insert_message(Role::Assistant, String::new(), cx);
}
LanguageModelCompletionEvent::Stop(reason) => {
stop_reason = reason;
@@ -162,6 +225,10 @@ impl Thread {
if let Some(last_message) = thread.messages.last_mut() {
if last_message.role == Role::Assistant {
last_message.text.push_str(&chunk);
cx.emit(ThreadEvent::StreamedAssistantText(
last_message.id,
chunk,
));
}
}
}
@@ -191,6 +258,7 @@ impl Thread {
}
}
thread.touch_updated_at();
cx.emit(ThreadEvent::StreamedCompletion);
cx.notify();
})?;
@@ -198,10 +266,14 @@ impl Thread {
smol::future::yield_now().await;
}
thread.update(&mut cx, |thread, _cx| {
thread.update(&mut cx, |thread, cx| {
thread
.pending_completions
.retain(|completion| completion.id != pending_completion_id);
if thread.summary.is_none() && thread.messages.len() >= 2 {
thread.summarize(cx);
}
})?;
anyhow::Ok(stop_reason)
@@ -210,29 +282,28 @@ impl Thread {
let result = stream_completion.await;
thread
.update(&mut cx, |_thread, cx| {
let error_message = if let Some(error) = result.as_ref().err() {
let error_message = error
.chain()
.map(|err| err.to_string())
.collect::<Vec<_>>()
.join("\n");
Some(error_message)
} else {
None
};
if let Some(error_message) = error_message {
eprintln!("Completion failed: {error_message:?}");
}
if let Ok(stop_reason) = result {
match stop_reason {
StopReason::ToolUse => {
cx.emit(ThreadEvent::UsePendingTools);
}
StopReason::EndTurn => {}
StopReason::MaxTokens => {}
.update(&mut cx, |_thread, cx| match result.as_ref() {
Ok(stop_reason) => match stop_reason {
StopReason::ToolUse => {
cx.emit(ThreadEvent::UsePendingTools);
}
StopReason::EndTurn => {}
StopReason::MaxTokens => {}
},
Err(error) => {
if error.is::<PaymentRequiredError>() {
cx.emit(ThreadEvent::ShowError(ThreadError::PaymentRequired));
} else if error.is::<MaxMonthlySpendReachedError>() {
cx.emit(ThreadEvent::ShowError(ThreadError::MaxMonthlySpendReached));
} else {
let error_message = error
.chain()
.map(|err| err.to_string())
.collect::<Vec<_>>()
.join("\n");
cx.emit(ThreadEvent::ShowError(ThreadError::Message(
SharedString::from(error_message.clone()),
)));
}
}
})
@@ -245,6 +316,59 @@ impl Thread {
});
}
pub fn summarize(&mut self, cx: &mut ModelContext<Self>) {
let Some(provider) = LanguageModelRegistry::read_global(cx).active_provider() else {
return;
};
let Some(model) = LanguageModelRegistry::read_global(cx).active_model() else {
return;
};
if !provider.is_authenticated(cx) {
return;
}
let mut request = self.to_completion_request(RequestKind::Chat, cx);
request.messages.push(LanguageModelRequestMessage {
role: Role::User,
content: vec![
"Generate a concise 3-7 word title for this conversation, omitting punctuation. Go straight to the title, without any preamble and prefix like `Here's a concise suggestion:...` or `Title:`"
.into(),
],
cache: false,
});
self.pending_summary = cx.spawn(|this, mut cx| {
async move {
let stream = model.stream_completion_text(request, &cx);
let mut messages = stream.await?;
let mut new_summary = String::new();
while let Some(message) = messages.stream.next().await {
let text = message?;
let mut lines = text.lines();
new_summary.extend(lines.next());
// Stop if the LLM generated multiple lines.
if lines.next().is_some() {
break;
}
}
this.update(&mut cx, |this, cx| {
if !new_summary.is_empty() {
this.summary = Some(new_summary.into());
}
cx.emit(ThreadEvent::SummaryChanged);
})?;
anyhow::Ok(())
}
.log_err()
});
}
pub fn insert_tool_output(
&mut self,
assistant_message_id: MessageId,
@@ -305,9 +429,20 @@ impl Thread {
}
}
#[derive(Debug, Clone)]
pub enum ThreadError {
PaymentRequired,
MaxMonthlySpendReached,
Message(SharedString),
}
#[derive(Debug, Clone)]
pub enum ThreadEvent {
ShowError(ThreadError),
StreamedCompletion,
StreamedAssistantText(MessageId, String),
MessageAdded(MessageId),
SummaryChanged,
UsePendingTools,
ToolFinished {
#[allow(unused)]

View File

@@ -0,0 +1,156 @@
use gpui::{
uniform_list, AppContext, FocusHandle, FocusableView, Model, UniformListScrollHandle, WeakView,
};
use time::{OffsetDateTime, UtcOffset};
use ui::{prelude::*, IconButtonShape, ListItem};
use crate::thread::Thread;
use crate::thread_store::ThreadStore;
use crate::AssistantPanel;
pub struct ThreadHistory {
focus_handle: FocusHandle,
assistant_panel: WeakView<AssistantPanel>,
thread_store: Model<ThreadStore>,
scroll_handle: UniformListScrollHandle,
}
impl ThreadHistory {
pub(crate) fn new(
assistant_panel: WeakView<AssistantPanel>,
thread_store: Model<ThreadStore>,
cx: &mut ViewContext<Self>,
) -> Self {
Self {
focus_handle: cx.focus_handle(),
assistant_panel,
thread_store,
scroll_handle: UniformListScrollHandle::default(),
}
}
}
impl FocusableView for ThreadHistory {
fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
self.focus_handle.clone()
}
}
impl Render for ThreadHistory {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let threads = self.thread_store.update(cx, |this, cx| this.threads(cx));
v_flex()
.id("thread-history-container")
.track_focus(&self.focus_handle)
.overflow_y_scroll()
.size_full()
.p_1()
.map(|history| {
if threads.is_empty() {
history
.justify_center()
.child(
h_flex().w_full().justify_center().child(
Label::new("You don't have any past threads yet.")
.size(LabelSize::Small),
),
)
} else {
history.child(
uniform_list(
cx.view().clone(),
"thread-history",
threads.len(),
move |history, range, _cx| {
threads[range]
.iter()
.map(|thread| {
PastThread::new(
thread.clone(),
history.assistant_panel.clone(),
)
})
.collect()
},
)
.track_scroll(self.scroll_handle.clone())
.flex_grow(),
)
}
})
}
}
#[derive(IntoElement)]
pub struct PastThread {
thread: Model<Thread>,
assistant_panel: WeakView<AssistantPanel>,
}
impl PastThread {
pub fn new(thread: Model<Thread>, assistant_panel: WeakView<AssistantPanel>) -> Self {
Self {
thread,
assistant_panel,
}
}
}
impl RenderOnce for PastThread {
fn render(self, cx: &mut WindowContext) -> impl IntoElement {
let (id, summary) = {
const DEFAULT_SUMMARY: SharedString = SharedString::new_static("New Thread");
let thread = self.thread.read(cx);
(
thread.id().clone(),
thread.summary().unwrap_or(DEFAULT_SUMMARY),
)
};
let thread_timestamp = time_format::format_localized_timestamp(
OffsetDateTime::from_unix_timestamp(self.thread.read(cx).updated_at().timestamp())
.unwrap(),
OffsetDateTime::now_utc(),
self.assistant_panel
.update(cx, |this, _cx| this.local_timezone())
.unwrap_or(UtcOffset::UTC),
time_format::TimestampFormat::EnhancedAbsolute,
);
ListItem::new(("past-thread", self.thread.entity_id()))
.start_slot(Icon::new(IconName::MessageBubbles))
.child(Label::new(summary))
.end_slot(
h_flex()
.gap_2()
.child(Label::new(thread_timestamp).color(Color::Disabled))
.child(
IconButton::new("delete", IconName::TrashAlt)
.shape(IconButtonShape::Square)
.icon_size(IconSize::Small)
.on_click({
let assistant_panel = self.assistant_panel.clone();
let id = id.clone();
move |_event, cx| {
assistant_panel
.update(cx, |this, cx| {
this.delete_thread(&id, cx);
})
.ok();
}
}),
),
)
.on_click({
let assistant_panel = self.assistant_panel.clone();
let id = id.clone();
move |_event, cx| {
assistant_panel
.update(cx, |this, cx| {
this.open_thread(&id, cx);
})
.ok();
}
})
}
}

View File

@@ -0,0 +1,242 @@
use std::sync::Arc;
use anyhow::Result;
use assistant_tool::{ToolId, ToolWorkingSet};
use collections::HashMap;
use context_server::manager::ContextServerManager;
use context_server::{ContextServerFactoryRegistry, ContextServerTool};
use gpui::{prelude::*, AppContext, Model, ModelContext, Task};
use project::Project;
use unindent::Unindent;
use util::ResultExt as _;
use crate::thread::{Thread, ThreadId};
pub struct ThreadStore {
#[allow(unused)]
project: Model<Project>,
tools: Arc<ToolWorkingSet>,
context_server_manager: Model<ContextServerManager>,
context_server_tool_ids: HashMap<Arc<str>, Vec<ToolId>>,
threads: Vec<Model<Thread>>,
}
impl ThreadStore {
pub fn new(
project: Model<Project>,
tools: Arc<ToolWorkingSet>,
cx: &mut AppContext,
) -> Task<Result<Model<Self>>> {
cx.spawn(|mut cx| async move {
let this = cx.new_model(|cx: &mut ModelContext<Self>| {
let context_server_factory_registry =
ContextServerFactoryRegistry::default_global(cx);
let context_server_manager = cx.new_model(|cx| {
ContextServerManager::new(context_server_factory_registry, project.clone(), cx)
});
let mut this = Self {
project,
tools,
context_server_manager,
context_server_tool_ids: HashMap::default(),
threads: Vec::new(),
};
this.mock_recent_threads(cx);
this.register_context_server_handlers(cx);
this
})?;
Ok(this)
})
}
pub fn threads(&self, cx: &ModelContext<Self>) -> Vec<Model<Thread>> {
let mut threads = self
.threads
.iter()
.filter(|thread| !thread.read(cx).is_empty())
.cloned()
.collect::<Vec<_>>();
threads.sort_unstable_by_key(|thread| std::cmp::Reverse(thread.read(cx).updated_at()));
threads
}
pub fn recent_threads(&self, limit: usize, cx: &ModelContext<Self>) -> Vec<Model<Thread>> {
self.threads(cx).into_iter().take(limit).collect()
}
pub fn create_thread(&mut self, cx: &mut ModelContext<Self>) -> Model<Thread> {
let thread = cx.new_model(|cx| Thread::new(self.tools.clone(), cx));
self.threads.push(thread.clone());
thread
}
pub fn open_thread(&self, id: &ThreadId, cx: &mut ModelContext<Self>) -> Option<Model<Thread>> {
self.threads
.iter()
.find(|thread| thread.read(cx).id() == id)
.cloned()
}
pub fn delete_thread(&mut self, id: &ThreadId, cx: &mut ModelContext<Self>) {
self.threads.retain(|thread| thread.read(cx).id() != id);
}
fn register_context_server_handlers(&self, cx: &mut ModelContext<Self>) {
cx.subscribe(
&self.context_server_manager.clone(),
Self::handle_context_server_event,
)
.detach();
}
fn handle_context_server_event(
&mut self,
context_server_manager: Model<ContextServerManager>,
event: &context_server::manager::Event,
cx: &mut ModelContext<Self>,
) {
let tool_working_set = self.tools.clone();
match event {
context_server::manager::Event::ServerStarted { server_id } => {
if let Some(server) = context_server_manager.read(cx).get_server(server_id) {
let context_server_manager = context_server_manager.clone();
cx.spawn({
let server = server.clone();
let server_id = server_id.clone();
|this, mut cx| async move {
let Some(protocol) = server.client() else {
return;
};
if protocol.capable(context_server::protocol::ServerCapability::Tools) {
if let Some(tools) = protocol.list_tools().await.log_err() {
let tool_ids = tools
.tools
.into_iter()
.map(|tool| {
log::info!(
"registering context server tool: {:?}",
tool.name
);
tool_working_set.insert(Arc::new(
ContextServerTool::new(
context_server_manager.clone(),
server.id(),
tool,
),
))
})
.collect::<Vec<_>>();
this.update(&mut cx, |this, _cx| {
this.context_server_tool_ids.insert(server_id, tool_ids);
})
.log_err();
}
}
}
})
.detach();
}
}
context_server::manager::Event::ServerStopped { server_id } => {
if let Some(tool_ids) = self.context_server_tool_ids.remove(server_id) {
tool_working_set.remove(&tool_ids);
}
}
}
}
}
impl ThreadStore {
/// Creates some mocked recent threads for testing purposes.
fn mock_recent_threads(&mut self, cx: &mut ModelContext<Self>) {
use language_model::Role;
self.threads.push(cx.new_model(|cx| {
let mut thread = Thread::new(self.tools.clone(), cx);
thread.set_summary("Introduction to quantum computing", cx);
thread.insert_user_message("Hello! Can you help me understand quantum computing?", cx);
thread.insert_message(Role::Assistant, "Of course! I'd be happy to help you understand quantum computing. Quantum computing is a fascinating field that uses the principles of quantum mechanics to process information. Unlike classical computers that use bits (0s and 1s), quantum computers use quantum bits or 'qubits'. These qubits can exist in multiple states simultaneously, a property called superposition. This allows quantum computers to perform certain calculations much faster than classical computers. What specific aspect of quantum computing would you like to know more about?", cx);
thread.insert_user_message("That's interesting! Can you explain how quantum entanglement is used in quantum computing?", cx);
thread.insert_message(Role::Assistant, "Certainly! Quantum entanglement is a key principle used in quantum computing. When two qubits become entangled, the state of one qubit is directly related to the state of the other, regardless of the distance between them. This property is used in quantum computing to create complex quantum states and to perform operations on multiple qubits simultaneously. Entanglement allows quantum computers to process information in ways that classical computers cannot, potentially solving certain problems much more efficiently. For example, it's crucial in quantum error correction and in algorithms like quantum teleportation, which is important for quantum communication.", cx);
thread
}));
self.threads.push(cx.new_model(|cx| {
let mut thread = Thread::new(self.tools.clone(), cx);
thread.set_summary("Rust web development and async programming", cx);
thread.insert_user_message("Can you show me an example of Rust code for a simple web server?", cx);
thread.insert_message(Role::Assistant, "Certainly! Here's an example of a simple web server in Rust using the `actix-web` framework:
```rust
use actix_web::{web, App, HttpResponse, HttpServer, Responder};
async fn hello() -> impl Responder {
HttpResponse::Ok().body(\"Hello, World!\")
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
HttpServer::new(|| {
App::new()
.route(\"/\", web::get().to(hello))
})
.bind(\"127.0.0.1:8080\")?
.run()
.await
}
```
This code creates a basic web server that responds with 'Hello, World!' when you access the root URL. Here's a breakdown of what's happening:
1. We import necessary items from the `actix-web` crate.
2. We define an async `hello` function that returns a simple HTTP response.
3. In the `main` function, we set up the server to listen on `127.0.0.1:8080`.
4. We configure the app to respond to GET requests on the root path with our `hello` function.
To run this, you'd need to add `actix-web` to your `Cargo.toml` dependencies:
```toml
[dependencies]
actix-web = \"4.0\"
```
Then you can run the server with `cargo run` and access it at `http://localhost:8080`.".unindent(), cx);
thread.insert_user_message("That's great! Can you explain more about async functions in Rust?", cx);
thread.insert_message(Role::Assistant, "Certainly! Async functions are a key feature in Rust for writing efficient, non-blocking code, especially for I/O-bound operations. Here's an overview:
1. **Syntax**: Async functions are declared using the `async` keyword:
```rust
async fn my_async_function() -> Result<(), Error> {
// Asynchronous code here
}
```
2. **Futures**: Async functions return a `Future`. A `Future` represents a value that may not be available yet but will be at some point.
3. **Await**: Inside an async function, you can use the `.await` syntax to wait for other async operations to complete:
```rust
async fn fetch_data() -> Result<String, Error> {
let response = make_http_request().await?;
let data = process_response(response).await?;
Ok(data)
}
```
4. **Non-blocking**: Async functions allow the runtime to work on other tasks while waiting for I/O or other operations to complete, making efficient use of system resources.
5. **Runtime**: To execute async code, you need a runtime like `tokio` or `async-std`. Actix-web, which we used in the previous example, includes its own runtime.
6. **Error Handling**: Async functions work well with Rust's `?` operator for error handling.
Async programming in Rust provides a powerful way to write concurrent code that's both safe and efficient. It's particularly useful for servers, network programming, and any application that deals with many concurrent operations.".unindent(), cx);
thread
}));
}
}

View File

@@ -18,5 +18,5 @@ collections.workspace = true
derive_more.workspace = true
gpui.workspace = true
parking_lot.workspace = true
rodio = { version = "0.19.0", default-features = false, features = ["wav"] }
rodio = { version = "0.20.0", default-features = false, features = ["wav"] }
util.workspace = true

View File

@@ -17,21 +17,23 @@ test-support = [
"client/test-support",
"collections/test-support",
"gpui/test-support",
"live_kit_client/test-support",
"livekit_client/test-support",
"project/test-support",
"util/test-support"
]
livekit-macos = ["livekit_client_macos"]
livekit-cross-platform = ["livekit_client"]
[dependencies]
anyhow.workspace = true
audio.workspace = true
client.workspace = true
collections.workspace = true
feature_flags.workspace = true
fs.workspace = true
futures.workspace = true
gpui.workspace = true
language.workspace = true
live_kit_client.workspace = true
log.workspace = true
postage.workspace = true
project.workspace = true
@@ -40,6 +42,8 @@ serde.workspace = true
serde_derive.workspace = true
settings.workspace = true
util.workspace = true
livekit_client_macos = { workspace = true, optional = true }
livekit_client = { workspace = true, optional = true }
[dev-dependencies]
client = { workspace = true, features = ["test-support"] }
@@ -47,7 +51,12 @@ collections = { workspace = true, features = ["test-support"] }
fs = { workspace = true, features = ["test-support"] }
gpui = { workspace = true, features = ["test-support"] }
language = { workspace = true, features = ["test-support"] }
live_kit_client = { workspace = true, features = ["test-support"] }
project = { workspace = true, features = ["test-support"] }
util = { workspace = true, features = ["test-support"] }
http_client = { workspace = true, features = ["test-support"] }
[target.'cfg(target_os = "macos")'.dev-dependencies]
livekit_client_macos = { workspace = true, features = ["test-support"] }
[target.'cfg(not(target_os = "macos"))'.dev-dependencies]
livekit_client = { workspace = true, features = ["test-support"] }

View File

@@ -1,546 +1,41 @@
pub mod call_settings;
pub mod participant;
pub mod room;
use anyhow::{anyhow, Result};
use audio::Audio;
use call_settings::CallSettings;
use client::{proto, ChannelId, Client, TypedEnvelope, User, UserStore, ZED_ALWAYS_ACTIVE};
use collections::HashSet;
use futures::{channel::oneshot, future::Shared, Future, FutureExt};
use gpui::{
AppContext, AsyncAppContext, Context, EventEmitter, Global, Model, ModelContext, Subscription,
Task, WeakModel,
};
use postage::watch;
use project::Project;
use room::Event;
use settings::Settings;
use std::sync::Arc;
#[cfg(any(
all(target_os = "macos", feature = "livekit-macos"),
all(
not(target_os = "macos"),
feature = "livekit-macos",
not(feature = "livekit-cross-platform")
)
))]
mod macos;
pub use participant::ParticipantLocation;
pub use room::Room;
#[cfg(any(
all(target_os = "macos", feature = "livekit-macos"),
all(
not(target_os = "macos"),
feature = "livekit-macos",
not(feature = "livekit-cross-platform")
)
))]
pub use macos::*;
struct GlobalActiveCall(Model<ActiveCall>);
impl Global for GlobalActiveCall {}
pub fn init(client: Arc<Client>, user_store: Model<UserStore>, cx: &mut AppContext) {
CallSettings::register(cx);
let active_call = cx.new_model(|cx| ActiveCall::new(client, user_store, cx));
cx.set_global(GlobalActiveCall(active_call));
}
pub struct OneAtATime {
cancel: Option<oneshot::Sender<()>>,
}
impl OneAtATime {
/// spawn a task in the given context.
/// if another task is spawned before that resolves, or if the OneAtATime itself is dropped, the first task will be cancelled and return Ok(None)
/// otherwise you'll see the result of the task.
fn spawn<F, Fut, R>(&mut self, cx: &mut AppContext, f: F) -> Task<Result<Option<R>>>
where
F: 'static + FnOnce(AsyncAppContext) -> Fut,
Fut: Future<Output = Result<R>>,
R: 'static,
{
let (tx, rx) = oneshot::channel();
self.cancel.replace(tx);
cx.spawn(|cx| async move {
futures::select_biased! {
_ = rx.fuse() => Ok(None),
result = f(cx).fuse() => result.map(Some),
}
})
}
fn running(&self) -> bool {
self.cancel
.as_ref()
.is_some_and(|cancel| !cancel.is_canceled())
}
}
#[derive(Clone)]
pub struct IncomingCall {
pub room_id: u64,
pub calling_user: Arc<User>,
pub participants: Vec<Arc<User>>,
pub initial_project: Option<proto::ParticipantProject>,
}
/// Singleton global maintaining the user's participation in a room across workspaces.
pub struct ActiveCall {
room: Option<(Model<Room>, Vec<Subscription>)>,
pending_room_creation: Option<Shared<Task<Result<Model<Room>, Arc<anyhow::Error>>>>>,
location: Option<WeakModel<Project>>,
_join_debouncer: OneAtATime,
pending_invites: HashSet<u64>,
incoming_call: (
watch::Sender<Option<IncomingCall>>,
watch::Receiver<Option<IncomingCall>>,
#[cfg(any(
all(
target_os = "macos",
feature = "livekit-cross-platform",
not(feature = "livekit-macos"),
),
client: Arc<Client>,
user_store: Model<UserStore>,
_subscriptions: Vec<client::Subscription>,
}
all(not(target_os = "macos"), feature = "livekit-cross-platform"),
))]
mod cross_platform;
impl EventEmitter<Event> for ActiveCall {}
impl ActiveCall {
fn new(client: Arc<Client>, user_store: Model<UserStore>, cx: &mut ModelContext<Self>) -> Self {
Self {
room: None,
pending_room_creation: None,
location: None,
pending_invites: Default::default(),
incoming_call: watch::channel(),
_join_debouncer: OneAtATime { cancel: None },
_subscriptions: vec![
client.add_request_handler(cx.weak_model(), Self::handle_incoming_call),
client.add_message_handler(cx.weak_model(), Self::handle_call_canceled),
],
client,
user_store,
}
}
pub fn channel_id(&self, cx: &AppContext) -> Option<ChannelId> {
self.room()?.read(cx).channel_id()
}
async fn handle_incoming_call(
this: Model<Self>,
envelope: TypedEnvelope<proto::IncomingCall>,
mut cx: AsyncAppContext,
) -> Result<proto::Ack> {
let user_store = this.update(&mut cx, |this, _| this.user_store.clone())?;
let call = IncomingCall {
room_id: envelope.payload.room_id,
participants: user_store
.update(&mut cx, |user_store, cx| {
user_store.get_users(envelope.payload.participant_user_ids, cx)
})?
.await?,
calling_user: user_store
.update(&mut cx, |user_store, cx| {
user_store.get_user(envelope.payload.calling_user_id, cx)
})?
.await?,
initial_project: envelope.payload.initial_project,
};
this.update(&mut cx, |this, _| {
*this.incoming_call.0.borrow_mut() = Some(call);
})?;
Ok(proto::Ack {})
}
async fn handle_call_canceled(
this: Model<Self>,
envelope: TypedEnvelope<proto::CallCanceled>,
mut cx: AsyncAppContext,
) -> Result<()> {
this.update(&mut cx, |this, _| {
let mut incoming_call = this.incoming_call.0.borrow_mut();
if incoming_call
.as_ref()
.map_or(false, |call| call.room_id == envelope.payload.room_id)
{
incoming_call.take();
}
})?;
Ok(())
}
pub fn global(cx: &AppContext) -> Model<Self> {
cx.global::<GlobalActiveCall>().0.clone()
}
pub fn try_global(cx: &AppContext) -> Option<Model<Self>> {
cx.try_global::<GlobalActiveCall>()
.map(|call| call.0.clone())
}
pub fn invite(
&mut self,
called_user_id: u64,
initial_project: Option<Model<Project>>,
cx: &mut ModelContext<Self>,
) -> Task<Result<()>> {
if !self.pending_invites.insert(called_user_id) {
return Task::ready(Err(anyhow!("user was already invited")));
}
cx.notify();
if self._join_debouncer.running() {
return Task::ready(Ok(()));
}
let room = if let Some(room) = self.room().cloned() {
Some(Task::ready(Ok(room)).shared())
} else {
self.pending_room_creation.clone()
};
let invite = if let Some(room) = room {
cx.spawn(move |_, mut cx| async move {
let room = room.await.map_err(|err| anyhow!("{:?}", err))?;
let initial_project_id = if let Some(initial_project) = initial_project {
Some(
room.update(&mut cx, |room, cx| room.share_project(initial_project, cx))?
.await?,
)
} else {
None
};
room.update(&mut cx, move |room, cx| {
room.call(called_user_id, initial_project_id, cx)
})?
.await?;
anyhow::Ok(())
})
} else {
let client = self.client.clone();
let user_store = self.user_store.clone();
let room = cx
.spawn(move |this, mut cx| async move {
let create_room = async {
let room = cx
.update(|cx| {
Room::create(
called_user_id,
initial_project,
client,
user_store,
cx,
)
})?
.await?;
this.update(&mut cx, |this, cx| this.set_room(Some(room.clone()), cx))?
.await?;
anyhow::Ok(room)
};
let room = create_room.await;
this.update(&mut cx, |this, _| this.pending_room_creation = None)?;
room.map_err(Arc::new)
})
.shared();
self.pending_room_creation = Some(room.clone());
cx.background_executor().spawn(async move {
room.await.map_err(|err| anyhow!("{:?}", err))?;
anyhow::Ok(())
})
};
cx.spawn(move |this, mut cx| async move {
let result = invite.await;
if result.is_ok() {
this.update(&mut cx, |this, cx| this.report_call_event("invite", cx))?;
} else {
//TODO: report collaboration error
log::error!("invite failed: {:?}", result);
}
this.update(&mut cx, |this, cx| {
this.pending_invites.remove(&called_user_id);
cx.notify();
})?;
result
})
}
pub fn cancel_invite(
&mut self,
called_user_id: u64,
cx: &mut ModelContext<Self>,
) -> Task<Result<()>> {
let room_id = if let Some(room) = self.room() {
room.read(cx).id()
} else {
return Task::ready(Err(anyhow!("no active call")));
};
let client = self.client.clone();
cx.background_executor().spawn(async move {
client
.request(proto::CancelCall {
room_id,
called_user_id,
})
.await?;
anyhow::Ok(())
})
}
pub fn incoming(&self) -> watch::Receiver<Option<IncomingCall>> {
self.incoming_call.1.clone()
}
pub fn accept_incoming(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
if self.room.is_some() {
return Task::ready(Err(anyhow!("cannot join while on another call")));
}
let call = if let Some(call) = self.incoming_call.0.borrow_mut().take() {
call
} else {
return Task::ready(Err(anyhow!("no incoming call")));
};
if self.pending_room_creation.is_some() {
return Task::ready(Ok(()));
}
let room_id = call.room_id;
let client = self.client.clone();
let user_store = self.user_store.clone();
let join = self
._join_debouncer
.spawn(cx, move |cx| Room::join(room_id, client, user_store, cx));
cx.spawn(|this, mut cx| async move {
let room = join.await?;
this.update(&mut cx, |this, cx| this.set_room(room.clone(), cx))?
.await?;
this.update(&mut cx, |this, cx| {
this.report_call_event("accept incoming", cx)
})?;
Ok(())
})
}
pub fn decline_incoming(&mut self, _: &mut ModelContext<Self>) -> Result<()> {
let call = self
.incoming_call
.0
.borrow_mut()
.take()
.ok_or_else(|| anyhow!("no incoming call"))?;
report_call_event_for_room("decline incoming", call.room_id, None, &self.client);
self.client.send(proto::DeclineCall {
room_id: call.room_id,
})?;
Ok(())
}
pub fn join_channel(
&mut self,
channel_id: ChannelId,
cx: &mut ModelContext<Self>,
) -> Task<Result<Option<Model<Room>>>> {
if let Some(room) = self.room().cloned() {
if room.read(cx).channel_id() == Some(channel_id) {
return Task::ready(Ok(Some(room)));
} else {
room.update(cx, |room, cx| room.clear_state(cx));
}
}
if self.pending_room_creation.is_some() {
return Task::ready(Ok(None));
}
let client = self.client.clone();
let user_store = self.user_store.clone();
let join = self._join_debouncer.spawn(cx, move |cx| async move {
Room::join_channel(channel_id, client, user_store, cx).await
});
cx.spawn(|this, mut cx| async move {
let room = join.await?;
this.update(&mut cx, |this, cx| this.set_room(room.clone(), cx))?
.await?;
this.update(&mut cx, |this, cx| {
this.report_call_event("join channel", cx)
})?;
Ok(room)
})
}
pub fn hang_up(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
cx.notify();
self.report_call_event("hang up", cx);
Audio::end_call(cx);
let channel_id = self.channel_id(cx);
if let Some((room, _)) = self.room.take() {
cx.emit(Event::RoomLeft { channel_id });
room.update(cx, |room, cx| room.leave(cx))
} else {
Task::ready(Ok(()))
}
}
pub fn share_project(
&mut self,
project: Model<Project>,
cx: &mut ModelContext<Self>,
) -> Task<Result<u64>> {
if let Some((room, _)) = self.room.as_ref() {
self.report_call_event("share project", cx);
room.update(cx, |room, cx| room.share_project(project, cx))
} else {
Task::ready(Err(anyhow!("no active call")))
}
}
pub fn unshare_project(
&mut self,
project: Model<Project>,
cx: &mut ModelContext<Self>,
) -> Result<()> {
if let Some((room, _)) = self.room.as_ref() {
self.report_call_event("unshare project", cx);
room.update(cx, |room, cx| room.unshare_project(project, cx))
} else {
Err(anyhow!("no active call"))
}
}
pub fn location(&self) -> Option<&WeakModel<Project>> {
self.location.as_ref()
}
pub fn set_location(
&mut self,
project: Option<&Model<Project>>,
cx: &mut ModelContext<Self>,
) -> Task<Result<()>> {
if project.is_some() || !*ZED_ALWAYS_ACTIVE {
self.location = project.map(|project| project.downgrade());
if let Some((room, _)) = self.room.as_ref() {
return room.update(cx, |room, cx| room.set_location(project, cx));
}
}
Task::ready(Ok(()))
}
fn set_room(
&mut self,
room: Option<Model<Room>>,
cx: &mut ModelContext<Self>,
) -> Task<Result<()>> {
if room.as_ref() == self.room.as_ref().map(|room| &room.0) {
Task::ready(Ok(()))
} else {
cx.notify();
if let Some(room) = room {
if room.read(cx).status().is_offline() {
self.room = None;
Task::ready(Ok(()))
} else {
let subscriptions = vec![
cx.observe(&room, |this, room, cx| {
if room.read(cx).status().is_offline() {
this.set_room(None, cx).detach_and_log_err(cx);
}
cx.notify();
}),
cx.subscribe(&room, |_, _, event, cx| cx.emit(event.clone())),
];
self.room = Some((room.clone(), subscriptions));
let location = self
.location
.as_ref()
.and_then(|location| location.upgrade());
let channel_id = room.read(cx).channel_id();
cx.emit(Event::RoomJoined { channel_id });
room.update(cx, |room, cx| room.set_location(location.as_ref(), cx))
}
} else {
self.room = None;
Task::ready(Ok(()))
}
}
}
pub fn room(&self) -> Option<&Model<Room>> {
self.room.as_ref().map(|(room, _)| room)
}
pub fn client(&self) -> Arc<Client> {
self.client.clone()
}
pub fn pending_invites(&self) -> &HashSet<u64> {
&self.pending_invites
}
pub fn report_call_event(&self, operation: &'static str, cx: &mut AppContext) {
if let Some(room) = self.room() {
let room = room.read(cx);
report_call_event_for_room(operation, room.id(), room.channel_id(), &self.client);
}
}
}
pub fn report_call_event_for_room(
operation: &'static str,
room_id: u64,
channel_id: Option<ChannelId>,
client: &Arc<Client>,
) {
let telemetry = client.telemetry();
telemetry.report_call_event(operation, Some(room_id), channel_id)
}
pub fn report_call_event_for_channel(
operation: &'static str,
channel_id: ChannelId,
client: &Arc<Client>,
cx: &AppContext,
) {
let room = ActiveCall::global(cx).read(cx).room();
let telemetry = client.telemetry();
telemetry.report_call_event(operation, room.map(|r| r.read(cx).id()), Some(channel_id))
}
#[cfg(test)]
mod test {
use gpui::TestAppContext;
use crate::OneAtATime;
#[gpui::test]
async fn test_one_at_a_time(cx: &mut TestAppContext) {
let mut one_at_a_time = OneAtATime { cancel: None };
assert_eq!(
cx.update(|cx| one_at_a_time.spawn(cx, |_| async { Ok(1) }))
.await
.unwrap(),
Some(1)
);
let (a, b) = cx.update(|cx| {
(
one_at_a_time.spawn(cx, |_| async {
panic!("");
}),
one_at_a_time.spawn(cx, |_| async { Ok(3) }),
)
});
assert_eq!(a.await.unwrap(), None::<u32>);
assert_eq!(b.await.unwrap(), Some(3));
let promise = cx.update(|cx| one_at_a_time.spawn(cx, |_| async { Ok(4) }));
drop(one_at_a_time);
assert_eq!(promise.await.unwrap(), None);
}
}
#[cfg(any(
all(
target_os = "macos",
feature = "livekit-cross-platform",
not(feature = "livekit-macos"),
),
all(not(target_os = "macos"), feature = "livekit-cross-platform"),
))]
pub use cross_platform::*;

View File

@@ -0,0 +1,552 @@
pub mod participant;
pub mod room;
use crate::call_settings::CallSettings;
use anyhow::{anyhow, Result};
use audio::Audio;
use client::{proto, ChannelId, Client, TypedEnvelope, User, UserStore, ZED_ALWAYS_ACTIVE};
use collections::HashSet;
use futures::{channel::oneshot, future::Shared, Future, FutureExt};
use gpui::{
AppContext, AsyncAppContext, Context, EventEmitter, Global, Model, ModelContext, Subscription,
Task, WeakModel,
};
use postage::watch;
use project::Project;
use room::Event;
use settings::Settings;
use std::sync::Arc;
pub use livekit_client::{
track::RemoteVideoTrack, RemoteVideoTrackView, RemoteVideoTrackViewEvent,
};
pub use participant::ParticipantLocation;
pub use room::Room;
struct GlobalActiveCall(Model<ActiveCall>);
impl Global for GlobalActiveCall {}
pub fn init(client: Arc<Client>, user_store: Model<UserStore>, cx: &mut AppContext) {
livekit_client::init(
cx.background_executor().dispatcher.clone(),
cx.http_client(),
);
CallSettings::register(cx);
let active_call = cx.new_model(|cx| ActiveCall::new(client, user_store, cx));
cx.set_global(GlobalActiveCall(active_call));
}
pub struct OneAtATime {
cancel: Option<oneshot::Sender<()>>,
}
impl OneAtATime {
/// spawn a task in the given context.
/// if another task is spawned before that resolves, or if the OneAtATime itself is dropped, the first task will be cancelled and return Ok(None)
/// otherwise you'll see the result of the task.
fn spawn<F, Fut, R>(&mut self, cx: &mut AppContext, f: F) -> Task<Result<Option<R>>>
where
F: 'static + FnOnce(AsyncAppContext) -> Fut,
Fut: Future<Output = Result<R>>,
R: 'static,
{
let (tx, rx) = oneshot::channel();
self.cancel.replace(tx);
cx.spawn(|cx| async move {
futures::select_biased! {
_ = rx.fuse() => Ok(None),
result = f(cx).fuse() => result.map(Some),
}
})
}
fn running(&self) -> bool {
self.cancel
.as_ref()
.is_some_and(|cancel| !cancel.is_canceled())
}
}
#[derive(Clone)]
pub struct IncomingCall {
pub room_id: u64,
pub calling_user: Arc<User>,
pub participants: Vec<Arc<User>>,
pub initial_project: Option<proto::ParticipantProject>,
}
/// Singleton global maintaining the user's participation in a room across workspaces.
pub struct ActiveCall {
room: Option<(Model<Room>, Vec<Subscription>)>,
pending_room_creation: Option<Shared<Task<Result<Model<Room>, Arc<anyhow::Error>>>>>,
location: Option<WeakModel<Project>>,
_join_debouncer: OneAtATime,
pending_invites: HashSet<u64>,
incoming_call: (
watch::Sender<Option<IncomingCall>>,
watch::Receiver<Option<IncomingCall>>,
),
client: Arc<Client>,
user_store: Model<UserStore>,
_subscriptions: Vec<client::Subscription>,
}
impl EventEmitter<Event> for ActiveCall {}
impl ActiveCall {
fn new(client: Arc<Client>, user_store: Model<UserStore>, cx: &mut ModelContext<Self>) -> Self {
Self {
room: None,
pending_room_creation: None,
location: None,
pending_invites: Default::default(),
incoming_call: watch::channel(),
_join_debouncer: OneAtATime { cancel: None },
_subscriptions: vec![
client.add_request_handler(cx.weak_model(), Self::handle_incoming_call),
client.add_message_handler(cx.weak_model(), Self::handle_call_canceled),
],
client,
user_store,
}
}
pub fn channel_id(&self, cx: &AppContext) -> Option<ChannelId> {
self.room()?.read(cx).channel_id()
}
async fn handle_incoming_call(
this: Model<Self>,
envelope: TypedEnvelope<proto::IncomingCall>,
mut cx: AsyncAppContext,
) -> Result<proto::Ack> {
let user_store = this.update(&mut cx, |this, _| this.user_store.clone())?;
let call = IncomingCall {
room_id: envelope.payload.room_id,
participants: user_store
.update(&mut cx, |user_store, cx| {
user_store.get_users(envelope.payload.participant_user_ids, cx)
})?
.await?,
calling_user: user_store
.update(&mut cx, |user_store, cx| {
user_store.get_user(envelope.payload.calling_user_id, cx)
})?
.await?,
initial_project: envelope.payload.initial_project,
};
this.update(&mut cx, |this, _| {
*this.incoming_call.0.borrow_mut() = Some(call);
})?;
Ok(proto::Ack {})
}
async fn handle_call_canceled(
this: Model<Self>,
envelope: TypedEnvelope<proto::CallCanceled>,
mut cx: AsyncAppContext,
) -> Result<()> {
this.update(&mut cx, |this, _| {
let mut incoming_call = this.incoming_call.0.borrow_mut();
if incoming_call
.as_ref()
.map_or(false, |call| call.room_id == envelope.payload.room_id)
{
incoming_call.take();
}
})?;
Ok(())
}
pub fn global(cx: &AppContext) -> Model<Self> {
cx.global::<GlobalActiveCall>().0.clone()
}
pub fn try_global(cx: &AppContext) -> Option<Model<Self>> {
cx.try_global::<GlobalActiveCall>()
.map(|call| call.0.clone())
}
pub fn invite(
&mut self,
called_user_id: u64,
initial_project: Option<Model<Project>>,
cx: &mut ModelContext<Self>,
) -> Task<Result<()>> {
if !self.pending_invites.insert(called_user_id) {
return Task::ready(Err(anyhow!("user was already invited")));
}
cx.notify();
if self._join_debouncer.running() {
return Task::ready(Ok(()));
}
let room = if let Some(room) = self.room().cloned() {
Some(Task::ready(Ok(room)).shared())
} else {
self.pending_room_creation.clone()
};
let invite = if let Some(room) = room {
cx.spawn(move |_, mut cx| async move {
let room = room.await.map_err(|err| anyhow!("{:?}", err))?;
let initial_project_id = if let Some(initial_project) = initial_project {
Some(
room.update(&mut cx, |room, cx| room.share_project(initial_project, cx))?
.await?,
)
} else {
None
};
room.update(&mut cx, move |room, cx| {
room.call(called_user_id, initial_project_id, cx)
})?
.await?;
anyhow::Ok(())
})
} else {
let client = self.client.clone();
let user_store = self.user_store.clone();
let room = cx
.spawn(move |this, mut cx| async move {
let create_room = async {
let room = cx
.update(|cx| {
Room::create(
called_user_id,
initial_project,
client,
user_store,
cx,
)
})?
.await?;
this.update(&mut cx, |this, cx| this.set_room(Some(room.clone()), cx))?
.await?;
anyhow::Ok(room)
};
let room = create_room.await;
this.update(&mut cx, |this, _| this.pending_room_creation = None)?;
room.map_err(Arc::new)
})
.shared();
self.pending_room_creation = Some(room.clone());
cx.background_executor().spawn(async move {
room.await.map_err(|err| anyhow!("{:?}", err))?;
anyhow::Ok(())
})
};
cx.spawn(move |this, mut cx| async move {
let result = invite.await;
if result.is_ok() {
this.update(&mut cx, |this, cx| this.report_call_event("invite", cx))?;
} else {
//TODO: report collaboration error
log::error!("invite failed: {:?}", result);
}
this.update(&mut cx, |this, cx| {
this.pending_invites.remove(&called_user_id);
cx.notify();
})?;
result
})
}
pub fn cancel_invite(
&mut self,
called_user_id: u64,
cx: &mut ModelContext<Self>,
) -> Task<Result<()>> {
let room_id = if let Some(room) = self.room() {
room.read(cx).id()
} else {
return Task::ready(Err(anyhow!("no active call")));
};
let client = self.client.clone();
cx.background_executor().spawn(async move {
client
.request(proto::CancelCall {
room_id,
called_user_id,
})
.await?;
anyhow::Ok(())
})
}
pub fn incoming(&self) -> watch::Receiver<Option<IncomingCall>> {
self.incoming_call.1.clone()
}
pub fn accept_incoming(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
if self.room.is_some() {
return Task::ready(Err(anyhow!("cannot join while on another call")));
}
let call = if let Some(call) = self.incoming_call.0.borrow_mut().take() {
call
} else {
return Task::ready(Err(anyhow!("no incoming call")));
};
if self.pending_room_creation.is_some() {
return Task::ready(Ok(()));
}
let room_id = call.room_id;
let client = self.client.clone();
let user_store = self.user_store.clone();
let join = self
._join_debouncer
.spawn(cx, move |cx| Room::join(room_id, client, user_store, cx));
cx.spawn(|this, mut cx| async move {
let room = join.await?;
this.update(&mut cx, |this, cx| this.set_room(room.clone(), cx))?
.await?;
this.update(&mut cx, |this, cx| {
this.report_call_event("accept incoming", cx)
})?;
Ok(())
})
}
pub fn decline_incoming(&mut self, _: &mut ModelContext<Self>) -> Result<()> {
let call = self
.incoming_call
.0
.borrow_mut()
.take()
.ok_or_else(|| anyhow!("no incoming call"))?;
report_call_event_for_room("decline incoming", call.room_id, None, &self.client);
self.client.send(proto::DeclineCall {
room_id: call.room_id,
})?;
Ok(())
}
pub fn join_channel(
&mut self,
channel_id: ChannelId,
cx: &mut ModelContext<Self>,
) -> Task<Result<Option<Model<Room>>>> {
if let Some(room) = self.room().cloned() {
if room.read(cx).channel_id() == Some(channel_id) {
return Task::ready(Ok(Some(room)));
} else {
room.update(cx, |room, cx| room.clear_state(cx));
}
}
if self.pending_room_creation.is_some() {
return Task::ready(Ok(None));
}
let client = self.client.clone();
let user_store = self.user_store.clone();
let join = self._join_debouncer.spawn(cx, move |cx| async move {
Room::join_channel(channel_id, client, user_store, cx).await
});
cx.spawn(|this, mut cx| async move {
let room = join.await?;
this.update(&mut cx, |this, cx| this.set_room(room.clone(), cx))?
.await?;
this.update(&mut cx, |this, cx| {
this.report_call_event("join channel", cx)
})?;
Ok(room)
})
}
pub fn hang_up(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
cx.notify();
self.report_call_event("hang up", cx);
Audio::end_call(cx);
let channel_id = self.channel_id(cx);
if let Some((room, _)) = self.room.take() {
cx.emit(Event::RoomLeft { channel_id });
room.update(cx, |room, cx| room.leave(cx))
} else {
Task::ready(Ok(()))
}
}
pub fn share_project(
&mut self,
project: Model<Project>,
cx: &mut ModelContext<Self>,
) -> Task<Result<u64>> {
if let Some((room, _)) = self.room.as_ref() {
self.report_call_event("share project", cx);
room.update(cx, |room, cx| room.share_project(project, cx))
} else {
Task::ready(Err(anyhow!("no active call")))
}
}
pub fn unshare_project(
&mut self,
project: Model<Project>,
cx: &mut ModelContext<Self>,
) -> Result<()> {
if let Some((room, _)) = self.room.as_ref() {
self.report_call_event("unshare project", cx);
room.update(cx, |room, cx| room.unshare_project(project, cx))
} else {
Err(anyhow!("no active call"))
}
}
pub fn location(&self) -> Option<&WeakModel<Project>> {
self.location.as_ref()
}
pub fn set_location(
&mut self,
project: Option<&Model<Project>>,
cx: &mut ModelContext<Self>,
) -> Task<Result<()>> {
if project.is_some() || !*ZED_ALWAYS_ACTIVE {
self.location = project.map(|project| project.downgrade());
if let Some((room, _)) = self.room.as_ref() {
return room.update(cx, |room, cx| room.set_location(project, cx));
}
}
Task::ready(Ok(()))
}
fn set_room(
&mut self,
room: Option<Model<Room>>,
cx: &mut ModelContext<Self>,
) -> Task<Result<()>> {
if room.as_ref() == self.room.as_ref().map(|room| &room.0) {
Task::ready(Ok(()))
} else {
cx.notify();
if let Some(room) = room {
if room.read(cx).status().is_offline() {
self.room = None;
Task::ready(Ok(()))
} else {
let subscriptions = vec![
cx.observe(&room, |this, room, cx| {
if room.read(cx).status().is_offline() {
this.set_room(None, cx).detach_and_log_err(cx);
}
cx.notify();
}),
cx.subscribe(&room, |_, _, event, cx| cx.emit(event.clone())),
];
self.room = Some((room.clone(), subscriptions));
let location = self
.location
.as_ref()
.and_then(|location| location.upgrade());
let channel_id = room.read(cx).channel_id();
cx.emit(Event::RoomJoined { channel_id });
room.update(cx, |room, cx| room.set_location(location.as_ref(), cx))
}
} else {
self.room = None;
Task::ready(Ok(()))
}
}
}
pub fn room(&self) -> Option<&Model<Room>> {
self.room.as_ref().map(|(room, _)| room)
}
pub fn client(&self) -> Arc<Client> {
self.client.clone()
}
pub fn pending_invites(&self) -> &HashSet<u64> {
&self.pending_invites
}
pub fn report_call_event(&self, operation: &'static str, cx: &mut AppContext) {
if let Some(room) = self.room() {
let room = room.read(cx);
report_call_event_for_room(operation, room.id(), room.channel_id(), &self.client);
}
}
}
pub fn report_call_event_for_room(
operation: &'static str,
room_id: u64,
channel_id: Option<ChannelId>,
client: &Arc<Client>,
) {
let telemetry = client.telemetry();
telemetry.report_call_event(operation, Some(room_id), channel_id)
}
pub fn report_call_event_for_channel(
operation: &'static str,
channel_id: ChannelId,
client: &Arc<Client>,
cx: &AppContext,
) {
let room = ActiveCall::global(cx).read(cx).room();
let telemetry = client.telemetry();
telemetry.report_call_event(operation, room.map(|r| r.read(cx).id()), Some(channel_id))
}
#[cfg(test)]
mod test {
use gpui::TestAppContext;
use crate::OneAtATime;
#[gpui::test]
async fn test_one_at_a_time(cx: &mut TestAppContext) {
let mut one_at_a_time = OneAtATime { cancel: None };
assert_eq!(
cx.update(|cx| one_at_a_time.spawn(cx, |_| async { Ok(1) }))
.await
.unwrap(),
Some(1)
);
let (a, b) = cx.update(|cx| {
(
one_at_a_time.spawn(cx, |_| async {
panic!("");
}),
one_at_a_time.spawn(cx, |_| async { Ok(3) }),
)
});
assert_eq!(a.await.unwrap(), None::<u32>);
assert_eq!(b.await.unwrap(), Some(3));
let promise = cx.update(|cx| one_at_a_time.spawn(cx, |_| async { Ok(4) }));
drop(one_at_a_time);
assert_eq!(promise.await.unwrap(), None);
}
}

View File

@@ -0,0 +1,68 @@
#![cfg_attr(target_os = "windows", allow(unused))]
use anyhow::{anyhow, Result};
use client::{proto, ParticipantIndex, User};
use collections::HashMap;
use gpui::WeakModel;
use livekit_client::AudioStream;
use project::Project;
use std::sync::Arc;
#[cfg(not(target_os = "windows"))]
pub use livekit_client::id::TrackSid;
pub use livekit_client::track::{RemoteAudioTrack, RemoteVideoTrack};
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub enum ParticipantLocation {
SharedProject { project_id: u64 },
UnsharedProject,
External,
}
impl ParticipantLocation {
pub fn from_proto(location: Option<proto::ParticipantLocation>) -> Result<Self> {
match location.and_then(|l| l.variant) {
Some(proto::participant_location::Variant::SharedProject(project)) => {
Ok(Self::SharedProject {
project_id: project.id,
})
}
Some(proto::participant_location::Variant::UnsharedProject(_)) => {
Ok(Self::UnsharedProject)
}
Some(proto::participant_location::Variant::External(_)) => Ok(Self::External),
None => Err(anyhow!("participant location was not provided")),
}
}
}
#[derive(Clone, Default)]
pub struct LocalParticipant {
pub projects: Vec<proto::ParticipantProject>,
pub active_project: Option<WeakModel<Project>>,
pub role: proto::ChannelRole,
}
pub struct RemoteParticipant {
pub user: Arc<User>,
pub peer_id: proto::PeerId,
pub role: proto::ChannelRole,
pub projects: Vec<proto::ParticipantProject>,
pub location: ParticipantLocation,
pub participant_index: ParticipantIndex,
pub muted: bool,
pub speaking: bool,
#[cfg(not(target_os = "windows"))]
pub video_tracks: HashMap<TrackSid, RemoteVideoTrack>,
#[cfg(not(target_os = "windows"))]
pub audio_tracks: HashMap<TrackSid, (RemoteAudioTrack, AudioStream)>,
}
impl RemoteParticipant {
pub fn has_video_tracks(&self) -> bool {
#[cfg(not(target_os = "windows"))]
return !self.video_tracks.is_empty();
#[cfg(target_os = "windows")]
return false;
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,545 @@
pub mod participant;
pub mod room;
use crate::call_settings::CallSettings;
use anyhow::{anyhow, Result};
use audio::Audio;
use client::{proto, ChannelId, Client, TypedEnvelope, User, UserStore, ZED_ALWAYS_ACTIVE};
use collections::HashSet;
use futures::{channel::oneshot, future::Shared, Future, FutureExt};
use gpui::{
AppContext, AsyncAppContext, Context, EventEmitter, Global, Model, ModelContext, Subscription,
Task, WeakModel,
};
use postage::watch;
use project::Project;
use room::Event;
use settings::Settings;
use std::sync::Arc;
pub use participant::ParticipantLocation;
pub use room::Room;
struct GlobalActiveCall(Model<ActiveCall>);
impl Global for GlobalActiveCall {}
pub fn init(client: Arc<Client>, user_store: Model<UserStore>, cx: &mut AppContext) {
CallSettings::register(cx);
let active_call = cx.new_model(|cx| ActiveCall::new(client, user_store, cx));
cx.set_global(GlobalActiveCall(active_call));
}
pub struct OneAtATime {
cancel: Option<oneshot::Sender<()>>,
}
impl OneAtATime {
/// spawn a task in the given context.
/// if another task is spawned before that resolves, or if the OneAtATime itself is dropped, the first task will be cancelled and return Ok(None)
/// otherwise you'll see the result of the task.
fn spawn<F, Fut, R>(&mut self, cx: &mut AppContext, f: F) -> Task<Result<Option<R>>>
where
F: 'static + FnOnce(AsyncAppContext) -> Fut,
Fut: Future<Output = Result<R>>,
R: 'static,
{
let (tx, rx) = oneshot::channel();
self.cancel.replace(tx);
cx.spawn(|cx| async move {
futures::select_biased! {
_ = rx.fuse() => Ok(None),
result = f(cx).fuse() => result.map(Some),
}
})
}
fn running(&self) -> bool {
self.cancel
.as_ref()
.is_some_and(|cancel| !cancel.is_canceled())
}
}
#[derive(Clone)]
pub struct IncomingCall {
pub room_id: u64,
pub calling_user: Arc<User>,
pub participants: Vec<Arc<User>>,
pub initial_project: Option<proto::ParticipantProject>,
}
/// Singleton global maintaining the user's participation in a room across workspaces.
pub struct ActiveCall {
room: Option<(Model<Room>, Vec<Subscription>)>,
pending_room_creation: Option<Shared<Task<Result<Model<Room>, Arc<anyhow::Error>>>>>,
location: Option<WeakModel<Project>>,
_join_debouncer: OneAtATime,
pending_invites: HashSet<u64>,
incoming_call: (
watch::Sender<Option<IncomingCall>>,
watch::Receiver<Option<IncomingCall>>,
),
client: Arc<Client>,
user_store: Model<UserStore>,
_subscriptions: Vec<client::Subscription>,
}
impl EventEmitter<Event> for ActiveCall {}
impl ActiveCall {
fn new(client: Arc<Client>, user_store: Model<UserStore>, cx: &mut ModelContext<Self>) -> Self {
Self {
room: None,
pending_room_creation: None,
location: None,
pending_invites: Default::default(),
incoming_call: watch::channel(),
_join_debouncer: OneAtATime { cancel: None },
_subscriptions: vec![
client.add_request_handler(cx.weak_model(), Self::handle_incoming_call),
client.add_message_handler(cx.weak_model(), Self::handle_call_canceled),
],
client,
user_store,
}
}
pub fn channel_id(&self, cx: &AppContext) -> Option<ChannelId> {
self.room()?.read(cx).channel_id()
}
async fn handle_incoming_call(
this: Model<Self>,
envelope: TypedEnvelope<proto::IncomingCall>,
mut cx: AsyncAppContext,
) -> Result<proto::Ack> {
let user_store = this.update(&mut cx, |this, _| this.user_store.clone())?;
let call = IncomingCall {
room_id: envelope.payload.room_id,
participants: user_store
.update(&mut cx, |user_store, cx| {
user_store.get_users(envelope.payload.participant_user_ids, cx)
})?
.await?,
calling_user: user_store
.update(&mut cx, |user_store, cx| {
user_store.get_user(envelope.payload.calling_user_id, cx)
})?
.await?,
initial_project: envelope.payload.initial_project,
};
this.update(&mut cx, |this, _| {
*this.incoming_call.0.borrow_mut() = Some(call);
})?;
Ok(proto::Ack {})
}
async fn handle_call_canceled(
this: Model<Self>,
envelope: TypedEnvelope<proto::CallCanceled>,
mut cx: AsyncAppContext,
) -> Result<()> {
this.update(&mut cx, |this, _| {
let mut incoming_call = this.incoming_call.0.borrow_mut();
if incoming_call
.as_ref()
.map_or(false, |call| call.room_id == envelope.payload.room_id)
{
incoming_call.take();
}
})?;
Ok(())
}
pub fn global(cx: &AppContext) -> Model<Self> {
cx.global::<GlobalActiveCall>().0.clone()
}
pub fn try_global(cx: &AppContext) -> Option<Model<Self>> {
cx.try_global::<GlobalActiveCall>()
.map(|call| call.0.clone())
}
pub fn invite(
&mut self,
called_user_id: u64,
initial_project: Option<Model<Project>>,
cx: &mut ModelContext<Self>,
) -> Task<Result<()>> {
if !self.pending_invites.insert(called_user_id) {
return Task::ready(Err(anyhow!("user was already invited")));
}
cx.notify();
if self._join_debouncer.running() {
return Task::ready(Ok(()));
}
let room = if let Some(room) = self.room().cloned() {
Some(Task::ready(Ok(room)).shared())
} else {
self.pending_room_creation.clone()
};
let invite = if let Some(room) = room {
cx.spawn(move |_, mut cx| async move {
let room = room.await.map_err(|err| anyhow!("{:?}", err))?;
let initial_project_id = if let Some(initial_project) = initial_project {
Some(
room.update(&mut cx, |room, cx| room.share_project(initial_project, cx))?
.await?,
)
} else {
None
};
room.update(&mut cx, move |room, cx| {
room.call(called_user_id, initial_project_id, cx)
})?
.await?;
anyhow::Ok(())
})
} else {
let client = self.client.clone();
let user_store = self.user_store.clone();
let room = cx
.spawn(move |this, mut cx| async move {
let create_room = async {
let room = cx
.update(|cx| {
Room::create(
called_user_id,
initial_project,
client,
user_store,
cx,
)
})?
.await?;
this.update(&mut cx, |this, cx| this.set_room(Some(room.clone()), cx))?
.await?;
anyhow::Ok(room)
};
let room = create_room.await;
this.update(&mut cx, |this, _| this.pending_room_creation = None)?;
room.map_err(Arc::new)
})
.shared();
self.pending_room_creation = Some(room.clone());
cx.background_executor().spawn(async move {
room.await.map_err(|err| anyhow!("{:?}", err))?;
anyhow::Ok(())
})
};
cx.spawn(move |this, mut cx| async move {
let result = invite.await;
if result.is_ok() {
this.update(&mut cx, |this, cx| this.report_call_event("invite", cx))?;
} else {
//TODO: report collaboration error
log::error!("invite failed: {:?}", result);
}
this.update(&mut cx, |this, cx| {
this.pending_invites.remove(&called_user_id);
cx.notify();
})?;
result
})
}
pub fn cancel_invite(
&mut self,
called_user_id: u64,
cx: &mut ModelContext<Self>,
) -> Task<Result<()>> {
let room_id = if let Some(room) = self.room() {
room.read(cx).id()
} else {
return Task::ready(Err(anyhow!("no active call")));
};
let client = self.client.clone();
cx.background_executor().spawn(async move {
client
.request(proto::CancelCall {
room_id,
called_user_id,
})
.await?;
anyhow::Ok(())
})
}
pub fn incoming(&self) -> watch::Receiver<Option<IncomingCall>> {
self.incoming_call.1.clone()
}
pub fn accept_incoming(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
if self.room.is_some() {
return Task::ready(Err(anyhow!("cannot join while on another call")));
}
let call = if let Some(call) = self.incoming_call.0.borrow_mut().take() {
call
} else {
return Task::ready(Err(anyhow!("no incoming call")));
};
if self.pending_room_creation.is_some() {
return Task::ready(Ok(()));
}
let room_id = call.room_id;
let client = self.client.clone();
let user_store = self.user_store.clone();
let join = self
._join_debouncer
.spawn(cx, move |cx| Room::join(room_id, client, user_store, cx));
cx.spawn(|this, mut cx| async move {
let room = join.await?;
this.update(&mut cx, |this, cx| this.set_room(room.clone(), cx))?
.await?;
this.update(&mut cx, |this, cx| {
this.report_call_event("accept incoming", cx)
})?;
Ok(())
})
}
pub fn decline_incoming(&mut self, _: &mut ModelContext<Self>) -> Result<()> {
let call = self
.incoming_call
.0
.borrow_mut()
.take()
.ok_or_else(|| anyhow!("no incoming call"))?;
report_call_event_for_room("decline incoming", call.room_id, None, &self.client);
self.client.send(proto::DeclineCall {
room_id: call.room_id,
})?;
Ok(())
}
pub fn join_channel(
&mut self,
channel_id: ChannelId,
cx: &mut ModelContext<Self>,
) -> Task<Result<Option<Model<Room>>>> {
if let Some(room) = self.room().cloned() {
if room.read(cx).channel_id() == Some(channel_id) {
return Task::ready(Ok(Some(room)));
} else {
room.update(cx, |room, cx| room.clear_state(cx));
}
}
if self.pending_room_creation.is_some() {
return Task::ready(Ok(None));
}
let client = self.client.clone();
let user_store = self.user_store.clone();
let join = self._join_debouncer.spawn(cx, move |cx| async move {
Room::join_channel(channel_id, client, user_store, cx).await
});
cx.spawn(|this, mut cx| async move {
let room = join.await?;
this.update(&mut cx, |this, cx| this.set_room(room.clone(), cx))?
.await?;
this.update(&mut cx, |this, cx| {
this.report_call_event("join channel", cx)
})?;
Ok(room)
})
}
pub fn hang_up(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
cx.notify();
self.report_call_event("hang up", cx);
Audio::end_call(cx);
let channel_id = self.channel_id(cx);
if let Some((room, _)) = self.room.take() {
cx.emit(Event::RoomLeft { channel_id });
room.update(cx, |room, cx| room.leave(cx))
} else {
Task::ready(Ok(()))
}
}
pub fn share_project(
&mut self,
project: Model<Project>,
cx: &mut ModelContext<Self>,
) -> Task<Result<u64>> {
if let Some((room, _)) = self.room.as_ref() {
self.report_call_event("share project", cx);
room.update(cx, |room, cx| room.share_project(project, cx))
} else {
Task::ready(Err(anyhow!("no active call")))
}
}
pub fn unshare_project(
&mut self,
project: Model<Project>,
cx: &mut ModelContext<Self>,
) -> Result<()> {
if let Some((room, _)) = self.room.as_ref() {
self.report_call_event("unshare project", cx);
room.update(cx, |room, cx| room.unshare_project(project, cx))
} else {
Err(anyhow!("no active call"))
}
}
pub fn location(&self) -> Option<&WeakModel<Project>> {
self.location.as_ref()
}
pub fn set_location(
&mut self,
project: Option<&Model<Project>>,
cx: &mut ModelContext<Self>,
) -> Task<Result<()>> {
if project.is_some() || !*ZED_ALWAYS_ACTIVE {
self.location = project.map(|project| project.downgrade());
if let Some((room, _)) = self.room.as_ref() {
return room.update(cx, |room, cx| room.set_location(project, cx));
}
}
Task::ready(Ok(()))
}
fn set_room(
&mut self,
room: Option<Model<Room>>,
cx: &mut ModelContext<Self>,
) -> Task<Result<()>> {
if room.as_ref() == self.room.as_ref().map(|room| &room.0) {
Task::ready(Ok(()))
} else {
cx.notify();
if let Some(room) = room {
if room.read(cx).status().is_offline() {
self.room = None;
Task::ready(Ok(()))
} else {
let subscriptions = vec![
cx.observe(&room, |this, room, cx| {
if room.read(cx).status().is_offline() {
this.set_room(None, cx).detach_and_log_err(cx);
}
cx.notify();
}),
cx.subscribe(&room, |_, _, event, cx| cx.emit(event.clone())),
];
self.room = Some((room.clone(), subscriptions));
let location = self
.location
.as_ref()
.and_then(|location| location.upgrade());
let channel_id = room.read(cx).channel_id();
cx.emit(Event::RoomJoined { channel_id });
room.update(cx, |room, cx| room.set_location(location.as_ref(), cx))
}
} else {
self.room = None;
Task::ready(Ok(()))
}
}
}
pub fn room(&self) -> Option<&Model<Room>> {
self.room.as_ref().map(|(room, _)| room)
}
pub fn client(&self) -> Arc<Client> {
self.client.clone()
}
pub fn pending_invites(&self) -> &HashSet<u64> {
&self.pending_invites
}
pub fn report_call_event(&self, operation: &'static str, cx: &mut AppContext) {
if let Some(room) = self.room() {
let room = room.read(cx);
report_call_event_for_room(operation, room.id(), room.channel_id(), &self.client);
}
}
}
pub fn report_call_event_for_room(
operation: &'static str,
room_id: u64,
channel_id: Option<ChannelId>,
client: &Arc<Client>,
) {
let telemetry = client.telemetry();
telemetry.report_call_event(operation, Some(room_id), channel_id)
}
pub fn report_call_event_for_channel(
operation: &'static str,
channel_id: ChannelId,
client: &Arc<Client>,
cx: &AppContext,
) {
let room = ActiveCall::global(cx).read(cx).room();
let telemetry = client.telemetry();
telemetry.report_call_event(operation, room.map(|r| r.read(cx).id()), Some(channel_id))
}
#[cfg(test)]
mod test {
use gpui::TestAppContext;
use crate::OneAtATime;
#[gpui::test]
async fn test_one_at_a_time(cx: &mut TestAppContext) {
let mut one_at_a_time = OneAtATime { cancel: None };
assert_eq!(
cx.update(|cx| one_at_a_time.spawn(cx, |_| async { Ok(1) }))
.await
.unwrap(),
Some(1)
);
let (a, b) = cx.update(|cx| {
(
one_at_a_time.spawn(cx, |_| async {
panic!("");
}),
one_at_a_time.spawn(cx, |_| async { Ok(3) }),
)
});
assert_eq!(a.await.unwrap(), None::<u32>);
assert_eq!(b.await.unwrap(), Some(3));
let promise = cx.update(|cx| one_at_a_time.spawn(cx, |_| async { Ok(4) }));
drop(one_at_a_time);
assert_eq!(promise.await.unwrap(), None);
}
}

View File

@@ -3,8 +3,8 @@ use client::ParticipantIndex;
use client::{proto, User};
use collections::HashMap;
use gpui::WeakModel;
pub use live_kit_client::Frame;
pub use live_kit_client::{RemoteAudioTrack, RemoteVideoTrack};
pub use livekit_client_macos::Frame;
pub use livekit_client_macos::{RemoteAudioTrack, RemoteVideoTrack};
use project::Project;
use std::sync::Arc;
@@ -49,6 +49,12 @@ pub struct RemoteParticipant {
pub participant_index: ParticipantIndex,
pub muted: bool,
pub speaking: bool,
pub video_tracks: HashMap<live_kit_client::Sid, Arc<RemoteVideoTrack>>,
pub audio_tracks: HashMap<live_kit_client::Sid, Arc<RemoteAudioTrack>>,
pub video_tracks: HashMap<livekit_client_macos::Sid, Arc<RemoteVideoTrack>>,
pub audio_tracks: HashMap<livekit_client_macos::Sid, Arc<RemoteAudioTrack>>,
}
impl RemoteParticipant {
pub fn has_video_tracks(&self) -> bool {
!self.video_tracks.is_empty()
}
}

View File

@@ -15,7 +15,7 @@ use gpui::{
AppContext, AsyncAppContext, Context, EventEmitter, Model, ModelContext, Task, WeakModel,
};
use language::LanguageRegistry;
use live_kit_client::{LocalAudioTrack, LocalTrackPublication, LocalVideoTrack, RoomUpdate};
use livekit_client_macos::{LocalAudioTrack, LocalTrackPublication, LocalVideoTrack, RoomUpdate};
use postage::{sink::Sink, stream::Stream, watch};
use project::Project;
use settings::Settings as _;
@@ -97,7 +97,7 @@ impl Room {
if let Some(live_kit) = self.live_kit.as_ref() {
matches!(
*live_kit.room.status().borrow(),
live_kit_client::ConnectionState::Connected { .. }
livekit_client_macos::ConnectionState::Connected { .. }
)
} else {
false
@@ -113,7 +113,7 @@ impl Room {
cx: &mut ModelContext<Self>,
) -> Self {
let live_kit_room = if let Some(connection_info) = live_kit_connection_info {
let room = live_kit_client::Room::new();
let room = livekit_client_macos::Room::new();
let mut status = room.status();
// Consume the initial status of the room.
let _ = status.try_recv();
@@ -125,7 +125,7 @@ impl Room {
break;
};
if status == live_kit_client::ConnectionState::Disconnected {
if status == livekit_client_macos::ConnectionState::Disconnected {
this.update(&mut cx, |this, cx| this.leave(cx).log_err())
.ok();
break;
@@ -156,7 +156,7 @@ impl Room {
cx.spawn(|this, mut cx| async move {
connect.await?;
this.update(&mut cx, |this, cx| {
if this.can_use_microphone() {
if this.can_use_microphone(cx) {
if let Some(live_kit) = &this.live_kit {
if !live_kit.muted_by_user && !live_kit.deafened {
return this.share_microphone(cx);
@@ -1317,7 +1317,7 @@ impl Room {
self.live_kit.as_ref().map(|live_kit| live_kit.deafened)
}
pub fn can_use_microphone(&self) -> bool {
pub fn can_use_microphone(&self, _cx: &AppContext) -> bool {
use proto::ChannelRole::*;
match self.local_participant.role {
Admin | Member | Talker => true,
@@ -1631,7 +1631,7 @@ impl Room {
}
#[cfg(any(test, feature = "test-support"))]
pub fn set_display_sources(&self, sources: Vec<live_kit_client::MacOSDisplay>) {
pub fn set_display_sources(&self, sources: Vec<livekit_client_macos::MacOSDisplay>) {
self.live_kit
.as_ref()
.unwrap()
@@ -1641,7 +1641,7 @@ impl Room {
}
struct LiveKitRoom {
room: Arc<live_kit_client::Room>,
room: Arc<livekit_client_macos::Room>,
screen_track: LocalTrack,
microphone_track: LocalTrack,
/// Tracks whether we're currently in a muted state due to auto-mute from deafening or manual mute performed by user.

View File

@@ -16,11 +16,15 @@ doctest = false
name = "cli"
path = "src/main.rs"
[features]
no-bundled-uninstall = []
default = []
[dependencies]
anyhow.workspace = true
clap.workspace = true
collections.workspace = true
ipc-channel = "0.18"
ipc-channel = "0.19"
once_cell.workspace = true
parking_lot.workspace = true
paths.workspace = true

5
crates/cli/build.rs Normal file
View File

@@ -0,0 +1,5 @@
fn main() {
if std::env::var("ZED_UPDATE_EXPLANATION").is_ok() {
println!(r#"cargo:rustc-cfg=feature="no-bundled-uninstall""#);
}
}

View File

@@ -59,6 +59,13 @@ struct Args {
/// Run zed in dev-server mode
#[arg(long)]
dev_server_token: Option<String>,
/// Uninstall Zed from user system
#[cfg(all(
any(target_os = "linux", target_os = "macos"),
not(feature = "no-bundled-uninstall")
))]
#[arg(long)]
uninstall: bool,
}
fn parse_path_with_position(argument_str: &str) -> anyhow::Result<String> {
@@ -119,6 +126,29 @@ fn main() -> Result<()> {
return Ok(());
}
#[cfg(all(
any(target_os = "linux", target_os = "macos"),
not(feature = "no-bundled-uninstall")
))]
if args.uninstall {
static UNINSTALL_SCRIPT: &[u8] = include_bytes!("../../../script/uninstall.sh");
let tmp_dir = tempfile::tempdir()?;
let script_path = tmp_dir.path().join("uninstall.sh");
fs::write(&script_path, UNINSTALL_SCRIPT)?;
use std::os::unix::fs::PermissionsExt as _;
fs::set_permissions(&script_path, fs::Permissions::from_mode(0o755))?;
let status = std::process::Command::new("sh")
.arg(&script_path)
.env("ZED_CHANNEL", &*release_channel::RELEASE_CHANNEL_NAME)
.status()
.context("Failed to execute uninstall script")?;
std::process::exit(status.code().unwrap_or(1));
}
let (server, server_name) =
IpcOneShotServer::<IpcHandshake>::new().context("Handshake before Zed spawn")?;
let url = format!("zed-cli://{server_name}");

View File

@@ -5,9 +5,9 @@ HTTP_PORT = 8080
API_TOKEN = "secret"
INVITE_LINK_PREFIX = "http://localhost:3000/invites/"
ZED_ENVIRONMENT = "development"
LIVE_KIT_SERVER = "http://localhost:7880"
LIVE_KIT_KEY = "devkey"
LIVE_KIT_SECRET = "secret"
LIVEKIT_SERVER = "http://localhost:7880"
LIVEKIT_KEY = "devkey"
LIVEKIT_SECRET = "secret"
BLOB_STORE_ACCESS_KEY = "the-blob-store-access-key"
BLOB_STORE_SECRET_KEY = "the-blob-store-secret-key"
BLOB_STORE_BUCKET = "the-extensions-bucket"

View File

@@ -40,7 +40,7 @@ google_ai.workspace = true
hex.workspace = true
http_client.workspace = true
jsonwebtoken.workspace = true
live_kit_server.workspace = true
livekit_server.workspace = true
log.workspace = true
nanoid.workspace = true
open_ai.workspace = true
@@ -77,6 +77,12 @@ tracing-subscriber = { version = "0.3.18", features = ["env-filter", "json", "re
util.workspace = true
uuid.workspace = true
[target.'cfg(target_os = "macos")'.dependencies]
livekit_client_macos = { workspace = true, features = ["test-support"] }
[target.'cfg(not(target_os = "macos"))'.dependencies]
livekit_client = { workspace = true, features = ["test-support"] }
[dev-dependencies]
assistant = { workspace = true, features = ["test-support"] }
assistant_tool.workspace = true
@@ -101,7 +107,6 @@ hyper.workspace = true
indoc.workspace = true
language = { workspace = true, features = ["test-support"] }
language_model = { workspace = true, features = ["test-support"] }
live_kit_client = { workspace = true, features = ["test-support"] }
lsp = { workspace = true, features = ["test-support"] }
menu.workspace = true
multi_buffer = { workspace = true, features = ["test-support"] }
@@ -125,5 +130,11 @@ util.workspace = true
workspace = { workspace = true, features = ["test-support"] }
worktree = { workspace = true, features = ["test-support"] }
[target.'cfg(target_os = "macos")'.dev-dependencies]
livekit_client_macos = { workspace = true, features = ["test-support"] }
[target.'cfg(not(target_os = "macos"))'.dev-dependencies]
livekit_client = {workspace = true, features = ["test-support"] }
[package.metadata.cargo-machete]
ignored = ["async-stripe"]

View File

@@ -109,17 +109,17 @@ spec:
secretKeyRef:
name: zed-client
key: checksum-seed
- name: LIVE_KIT_SERVER
- name: LIVEKIT_SERVER
valueFrom:
secretKeyRef:
name: livekit
key: server
- name: LIVE_KIT_KEY
- name: LIVEKIT_KEY
valueFrom:
secretKeyRef:
name: livekit
key: key
- name: LIVE_KIT_SECRET
- name: LIVEKIT_SECRET
valueFrom:
secretKeyRef:
name: livekit

View File

@@ -154,9 +154,9 @@ impl Database {
}
let role = role.unwrap();
let live_kit_room = format!("channel-{}", nanoid::nanoid!(30));
let livekit_room = format!("channel-{}", nanoid::nanoid!(30));
let room_id = self
.get_or_create_channel_room(channel_id, &live_kit_room, &tx)
.get_or_create_channel_room(channel_id, &livekit_room, &tx)
.await?;
self.join_channel_room_internal(room_id, user_id, connection, role, &tx)
@@ -896,7 +896,7 @@ impl Database {
pub(crate) async fn get_or_create_channel_room(
&self,
channel_id: ChannelId,
live_kit_room: &str,
livekit_room: &str,
tx: &DatabaseTransaction,
) -> Result<RoomId> {
let room = room::Entity::find()
@@ -909,7 +909,7 @@ impl Database {
} else {
let result = room::Entity::insert(room::ActiveModel {
channel_id: ActiveValue::Set(Some(channel_id)),
live_kit_room: ActiveValue::Set(live_kit_room.to_string()),
live_kit_room: ActiveValue::Set(livekit_room.to_string()),
..Default::default()
})
.exec(tx)

View File

@@ -103,11 +103,11 @@ impl Database {
&self,
user_id: UserId,
connection: ConnectionId,
live_kit_room: &str,
livekit_room: &str,
) -> Result<proto::Room> {
self.transaction(|tx| async move {
let room = room::ActiveModel {
live_kit_room: ActiveValue::set(live_kit_room.into()),
live_kit_room: ActiveValue::set(livekit_room.into()),
..Default::default()
}
.insert(&*tx)
@@ -1316,7 +1316,7 @@ impl Database {
channel,
proto::Room {
id: db_room.id.to_proto(),
live_kit_room: db_room.live_kit_room,
livekit_room: db_room.live_kit_room,
participants: participants.into_values().collect(),
pending_participants,
followers,

View File

@@ -156,9 +156,9 @@ pub struct Config {
pub clickhouse_password: Option<String>,
pub clickhouse_database: Option<String>,
pub invite_link_prefix: String,
pub live_kit_server: Option<String>,
pub live_kit_key: Option<String>,
pub live_kit_secret: Option<String>,
pub livekit_server: Option<String>,
pub livekit_key: Option<String>,
pub livekit_secret: Option<String>,
pub llm_database_url: Option<String>,
pub llm_database_max_connections: Option<u32>,
pub llm_database_migrations_path: Option<PathBuf>,
@@ -210,9 +210,9 @@ impl Config {
database_max_connections: 0,
api_token: "".into(),
invite_link_prefix: "".into(),
live_kit_server: None,
live_kit_key: None,
live_kit_secret: None,
livekit_server: None,
livekit_key: None,
livekit_secret: None,
llm_database_url: None,
llm_database_max_connections: None,
llm_database_migrations_path: None,
@@ -277,7 +277,7 @@ impl ServiceMode {
pub struct AppState {
pub db: Arc<Database>,
pub llm_db: Option<Arc<LlmDatabase>>,
pub live_kit_client: Option<Arc<dyn live_kit_server::api::Client>>,
pub livekit_client: Option<Arc<dyn livekit_server::api::Client>>,
pub blob_store_client: Option<aws_sdk_s3::Client>,
pub stripe_client: Option<Arc<stripe::Client>>,
pub stripe_billing: Option<Arc<StripeBilling>>,
@@ -309,17 +309,17 @@ impl AppState {
None
};
let live_kit_client = if let Some(((server, key), secret)) = config
.live_kit_server
let livekit_client = if let Some(((server, key), secret)) = config
.livekit_server
.as_ref()
.zip(config.live_kit_key.as_ref())
.zip(config.live_kit_secret.as_ref())
.zip(config.livekit_key.as_ref())
.zip(config.livekit_secret.as_ref())
{
Some(Arc::new(live_kit_server::api::LiveKitClient::new(
Some(Arc::new(livekit_server::api::LiveKitClient::new(
server.clone(),
key.clone(),
secret.clone(),
)) as Arc<dyn live_kit_server::api::Client>)
)) as Arc<dyn livekit_server::api::Client>)
} else {
None
};
@@ -329,7 +329,7 @@ impl AppState {
let this = Self {
db: db.clone(),
llm_db,
live_kit_client,
livekit_client,
blob_store_client: build_blob_store_client(&config).await.log_err(),
stripe_billing: stripe_client
.clone()

View File

@@ -309,6 +309,7 @@ impl Server {
.add_request_handler(forward_read_only_project_request::<proto::ResolveInlayHint>)
.add_request_handler(forward_read_only_project_request::<proto::OpenBufferByPath>)
.add_request_handler(forward_read_only_project_request::<proto::GitBranches>)
.add_request_handler(forward_read_only_project_request::<proto::GetStagedText>)
.add_request_handler(forward_mutating_project_request::<proto::UpdateGitBranch>)
.add_request_handler(forward_mutating_project_request::<proto::GetCompletions>)
.add_request_handler(
@@ -418,7 +419,7 @@ impl Server {
let peer = self.peer.clone();
let timeout = self.app_state.executor.sleep(CLEANUP_TIMEOUT);
let pool = self.connection_pool.clone();
let live_kit_client = self.app_state.live_kit_client.clone();
let livekit_client = self.app_state.livekit_client.clone();
let span = info_span!("start server");
self.app_state.executor.spawn_detached(
@@ -463,8 +464,8 @@ impl Server {
for room_id in room_ids {
let mut contacts_to_update = HashSet::default();
let mut canceled_calls_to_user_ids = Vec::new();
let mut live_kit_room = String::new();
let mut delete_live_kit_room = false;
let mut livekit_room = String::new();
let mut delete_livekit_room = false;
if let Some(mut refreshed_room) = app_state
.db
@@ -487,8 +488,8 @@ impl Server {
.extend(refreshed_room.canceled_calls_to_user_ids.iter().copied());
canceled_calls_to_user_ids =
mem::take(&mut refreshed_room.canceled_calls_to_user_ids);
live_kit_room = mem::take(&mut refreshed_room.room.live_kit_room);
delete_live_kit_room = refreshed_room.room.participants.is_empty();
livekit_room = mem::take(&mut refreshed_room.room.livekit_room);
delete_livekit_room = refreshed_room.room.participants.is_empty();
}
{
@@ -539,9 +540,9 @@ impl Server {
}
}
if let Some(live_kit) = live_kit_client.as_ref() {
if delete_live_kit_room {
live_kit.delete_room(live_kit_room).await.trace_err();
if let Some(live_kit) = livekit_client.as_ref() {
if delete_livekit_room {
live_kit.delete_room(livekit_room).await.trace_err();
}
}
}
@@ -1210,15 +1211,15 @@ async fn create_room(
response: Response<proto::CreateRoom>,
session: Session,
) -> Result<()> {
let live_kit_room = nanoid::nanoid!(30);
let livekit_room = nanoid::nanoid!(30);
let live_kit_connection_info = util::maybe!(async {
let live_kit = session.app_state.live_kit_client.as_ref();
let live_kit = session.app_state.livekit_client.as_ref();
let live_kit = live_kit?;
let user_id = session.user_id().to_string();
let token = live_kit
.room_token(&live_kit_room, &user_id.to_string())
.room_token(&livekit_room, &user_id.to_string())
.trace_err()?;
Some(proto::LiveKitConnectionInfo {
@@ -1232,7 +1233,7 @@ async fn create_room(
let room = session
.db()
.await
.create_room(session.user_id(), session.connection_id, &live_kit_room)
.create_room(session.user_id(), session.connection_id, &livekit_room)
.await?;
response.send(proto::CreateRoomResponse {
@@ -1284,22 +1285,22 @@ async fn join_room(
.trace_err();
}
let live_kit_connection_info =
if let Some(live_kit) = session.app_state.live_kit_client.as_ref() {
live_kit
.room_token(
&joined_room.room.live_kit_room,
&session.user_id().to_string(),
)
.trace_err()
.map(|token| proto::LiveKitConnectionInfo {
server_url: live_kit.url().into(),
token,
can_publish: true,
})
} else {
None
};
let live_kit_connection_info = if let Some(live_kit) = session.app_state.livekit_client.as_ref()
{
live_kit
.room_token(
&joined_room.room.livekit_room,
&session.user_id().to_string(),
)
.trace_err()
.map(|token| proto::LiveKitConnectionInfo {
server_url: live_kit.url().into(),
token,
can_publish: true,
})
} else {
None
};
response.send(proto::JoinRoomResponse {
room: Some(joined_room.room),
@@ -1506,7 +1507,7 @@ async fn set_room_participant_role(
let user_id = UserId::from_proto(request.user_id);
let role = ChannelRole::from(request.role());
let (live_kit_room, can_publish) = {
let (livekit_room, can_publish) = {
let room = session
.db()
.await
@@ -1518,18 +1519,18 @@ async fn set_room_participant_role(
)
.await?;
let live_kit_room = room.live_kit_room.clone();
let livekit_room = room.livekit_room.clone();
let can_publish = ChannelRole::from(request.role()).can_use_microphone();
room_updated(&room, &session.peer);
(live_kit_room, can_publish)
(livekit_room, can_publish)
};
if let Some(live_kit) = session.app_state.live_kit_client.as_ref() {
if let Some(live_kit) = session.app_state.livekit_client.as_ref() {
live_kit
.update_participant(
live_kit_room.clone(),
livekit_room.clone(),
request.user_id.to_string(),
live_kit_server::proto::ParticipantPermission {
livekit_server::proto::ParticipantPermission {
can_subscribe: true,
can_publish,
can_publish_data: can_publish,
@@ -3091,7 +3092,7 @@ async fn join_channel_internal(
let live_kit_connection_info =
session
.app_state
.live_kit_client
.livekit_client
.as_ref()
.and_then(|live_kit| {
let (can_publish, token) = if role == ChannelRole::Guest {
@@ -3099,7 +3100,7 @@ async fn join_channel_internal(
false,
live_kit
.guest_token(
&joined_room.room.live_kit_room,
&joined_room.room.livekit_room,
&session.user_id().to_string(),
)
.trace_err()?,
@@ -3109,7 +3110,7 @@ async fn join_channel_internal(
true,
live_kit
.room_token(
&joined_room.room.live_kit_room,
&joined_room.room.livekit_room,
&session.user_id().to_string(),
)
.trace_err()?,
@@ -4313,8 +4314,8 @@ async fn leave_room_for_session(session: &Session, connection_id: ConnectionId)
let room_id;
let canceled_calls_to_user_ids;
let live_kit_room;
let delete_live_kit_room;
let livekit_room;
let delete_livekit_room;
let room;
let channel;
@@ -4327,8 +4328,8 @@ async fn leave_room_for_session(session: &Session, connection_id: ConnectionId)
room_id = RoomId::from_proto(left_room.room.id);
canceled_calls_to_user_ids = mem::take(&mut left_room.canceled_calls_to_user_ids);
live_kit_room = mem::take(&mut left_room.room.live_kit_room);
delete_live_kit_room = left_room.deleted;
livekit_room = mem::take(&mut left_room.room.livekit_room);
delete_livekit_room = left_room.deleted;
room = mem::take(&mut left_room.room);
channel = mem::take(&mut left_room.channel);
@@ -4368,14 +4369,14 @@ async fn leave_room_for_session(session: &Session, connection_id: ConnectionId)
update_user_contacts(contact_user_id, session).await?;
}
if let Some(live_kit) = session.app_state.live_kit_client.as_ref() {
if let Some(live_kit) = session.app_state.livekit_client.as_ref() {
live_kit
.remove_participant(live_kit_room.clone(), session.user_id().to_string())
.remove_participant(livekit_room.clone(), session.user_id().to_string())
.await
.trace_err();
if delete_live_kit_room {
live_kit.delete_room(live_kit_room).await.trace_err();
if delete_livekit_room {
live_kit.delete_room(livekit_room).await.trace_err();
}
}

View File

@@ -1,3 +1,6 @@
// todo(windows): Actually run the tests
#![cfg(not(target_os = "windows"))]
use std::sync::Arc;
use call::Room;

View File

@@ -107,7 +107,9 @@ async fn test_channel_guest_promotion(cx_a: &mut TestAppContext, cx_b: &mut Test
});
assert!(project_b.read_with(cx_b, |project, cx| project.is_read_only(cx)));
assert!(editor_b.update(cx_b, |e, cx| e.read_only(cx)));
assert!(room_b.read_with(cx_b, |room, _| !room.can_use_microphone()));
cx_b.update(|cx_b| {
assert!(room_b.read_with(cx_b, |room, cx| !room.can_use_microphone(cx)));
});
assert!(room_b
.update(cx_b, |room, cx| room.share_microphone(cx))
.await
@@ -133,7 +135,9 @@ async fn test_channel_guest_promotion(cx_a: &mut TestAppContext, cx_b: &mut Test
assert!(editor_b.update(cx_b, |editor, cx| !editor.read_only(cx)));
// B sees themselves as muted, and can unmute.
assert!(room_b.read_with(cx_b, |room, _| room.can_use_microphone()));
cx_b.update(|cx_b| {
assert!(room_b.read_with(cx_b, |room, cx| room.can_use_microphone(cx)));
});
room_b.read_with(cx_b, |room, _| assert!(room.is_muted()));
room_b.update(cx_b, |room, cx| room.toggle_mute(cx));
cx_a.run_until_parked();
@@ -226,7 +230,9 @@ async fn test_channel_requires_zed_cla(cx_a: &mut TestAppContext, cx_b: &mut Tes
let room_b = cx_b
.read(ActiveCall::global)
.update(cx_b, |call, _| call.room().unwrap().clone());
assert!(room_b.read_with(cx_b, |room, _| !room.can_use_microphone()));
cx_b.update(|cx_b| {
assert!(room_b.read_with(cx_b, |room, cx| !room.can_use_microphone(cx)));
});
// A tries to grant write access to B, but cannot because B has not
// yet signed the zed CLA.
@@ -244,7 +250,9 @@ async fn test_channel_requires_zed_cla(cx_a: &mut TestAppContext, cx_b: &mut Tes
.unwrap_err();
cx_a.run_until_parked();
assert!(room_b.read_with(cx_b, |room, _| !room.can_share_projects()));
assert!(room_b.read_with(cx_b, |room, _| !room.can_use_microphone()));
cx_b.update(|cx_b| {
assert!(room_b.read_with(cx_b, |room, cx| !room.can_use_microphone(cx)));
});
// A tries to grant write access to B, but cannot because B has not
// yet signed the zed CLA.
@@ -262,7 +270,9 @@ async fn test_channel_requires_zed_cla(cx_a: &mut TestAppContext, cx_b: &mut Tes
.unwrap();
cx_a.run_until_parked();
assert!(room_b.read_with(cx_b, |room, _| !room.can_share_projects()));
assert!(room_b.read_with(cx_b, |room, _| room.can_use_microphone()));
cx_b.update(|cx_b| {
assert!(room_b.read_with(cx_b, |room, cx| room.can_use_microphone(cx)));
});
// User B signs the zed CLA.
server
@@ -287,5 +297,7 @@ async fn test_channel_requires_zed_cla(cx_a: &mut TestAppContext, cx_b: &mut Tes
.unwrap();
cx_a.run_until_parked();
assert!(room_b.read_with(cx_b, |room, _| room.can_share_projects()));
assert!(room_b.read_with(cx_b, |room, _| room.can_use_microphone()));
cx_b.update(|cx_b| {
assert!(room_b.read_with(cx_b, |room, cx| room.can_use_microphone(cx)));
});
}

View File

@@ -1,5 +1,5 @@
#![allow(clippy::reversed_empty_ranges)]
use crate::{rpc::RECONNECT_TIMEOUT, tests::TestServer};
use crate::tests::TestServer;
use call::{ActiveCall, ParticipantLocation};
use client::ChannelId;
use collab_ui::{
@@ -12,17 +12,11 @@ use gpui::{
View, VisualContext, VisualTestContext,
};
use language::Capability;
use live_kit_client::MacOSDisplay;
use project::WorktreeSettings;
use rpc::proto::PeerId;
use serde_json::json;
use settings::SettingsStore;
use workspace::{
dock::{test::TestPanel, DockPosition},
item::{test::TestItem, ItemHandle as _},
shared_screen::SharedScreen,
SplitDirection, Workspace,
};
use workspace::{item::ItemHandle as _, SplitDirection, Workspace};
use super::TestClient;
@@ -428,106 +422,118 @@ async fn test_basic_following(
editor_a1.item_id()
);
// Client B activates an external window, which causes a new screen-sharing item to be added to the pane.
let display = MacOSDisplay::new();
active_call_b
.update(cx_b, |call, cx| call.set_location(None, cx))
.await
.unwrap();
active_call_b
.update(cx_b, |call, cx| {
call.room().unwrap().update(cx, |room, cx| {
room.set_display_sources(vec![display.clone()]);
room.share_screen(cx)
// TODO: Re-enable this test once we can replace our swift Livekit SDK with the rust SDK
#[cfg(not(target_os = "macos"))]
{
use crate::rpc::RECONNECT_TIMEOUT;
use gpui::TestScreenCaptureSource;
use workspace::{
dock::{test::TestPanel, DockPosition},
item::test::TestItem,
shared_screen::SharedScreen,
};
// Client B activates an external window, which causes a new screen-sharing item to be added to the pane.
let display = TestScreenCaptureSource::new();
active_call_b
.update(cx_b, |call, cx| call.set_location(None, cx))
.await
.unwrap();
cx_b.set_screen_capture_sources(vec![display]);
active_call_b
.update(cx_b, |call, cx| {
call.room()
.unwrap()
.update(cx, |room, cx| room.share_screen(cx))
})
})
.await
.unwrap();
executor.run_until_parked();
let shared_screen = workspace_a.update(cx_a, |workspace, cx| {
workspace
.active_item(cx)
.expect("no active item")
.downcast::<SharedScreen>()
.expect("active item isn't a shared screen")
});
.await
.unwrap(); // This is what breaks
executor.run_until_parked();
let shared_screen = workspace_a.update(cx_a, |workspace, cx| {
workspace
.active_item(cx)
.expect("no active item")
.downcast::<SharedScreen>()
.expect("active item isn't a shared screen")
});
// Client B activates Zed again, which causes the previous editor to become focused again.
active_call_b
.update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
.await
.unwrap();
executor.run_until_parked();
workspace_a.update(cx_a, |workspace, cx| {
// Client B activates Zed again, which causes the previous editor to become focused again.
active_call_b
.update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
.await
.unwrap();
executor.run_until_parked();
workspace_a.update(cx_a, |workspace, cx| {
assert_eq!(
workspace.active_item(cx).unwrap().item_id(),
editor_a1.item_id()
)
});
// Client B activates a multibuffer that was created by following client A. Client A returns to that multibuffer.
workspace_b.update(cx_b, |workspace, cx| {
workspace.activate_item(&multibuffer_editor_b, true, true, cx)
});
executor.run_until_parked();
workspace_a.update(cx_a, |workspace, cx| {
assert_eq!(
workspace.active_item(cx).unwrap().item_id(),
multibuffer_editor_a.item_id()
)
});
// Client B activates a panel, and the previously-opened screen-sharing item gets activated.
let panel = cx_b.new_view(|cx| TestPanel::new(DockPosition::Left, cx));
workspace_b.update(cx_b, |workspace, cx| {
workspace.add_panel(panel, cx);
workspace.toggle_panel_focus::<TestPanel>(cx);
});
executor.run_until_parked();
assert_eq!(
workspace.active_item(cx).unwrap().item_id(),
editor_a1.item_id()
)
});
workspace_a.update(cx_a, |workspace, cx| workspace
.active_item(cx)
.unwrap()
.item_id()),
shared_screen.item_id()
);
// Client B activates a multibuffer that was created by following client A. Client A returns to that multibuffer.
workspace_b.update(cx_b, |workspace, cx| {
workspace.activate_item(&multibuffer_editor_b, true, true, cx)
});
executor.run_until_parked();
workspace_a.update(cx_a, |workspace, cx| {
// Toggling the focus back to the pane causes client A to return to the multibuffer.
workspace_b.update(cx_b, |workspace, cx| {
workspace.toggle_panel_focus::<TestPanel>(cx);
});
executor.run_until_parked();
workspace_a.update(cx_a, |workspace, cx| {
assert_eq!(
workspace.active_item(cx).unwrap().item_id(),
multibuffer_editor_a.item_id()
)
});
// Client B activates an item that doesn't implement following,
// so the previously-opened screen-sharing item gets activated.
let unfollowable_item = cx_b.new_view(TestItem::new);
workspace_b.update(cx_b, |workspace, cx| {
workspace.active_pane().update(cx, |pane, cx| {
pane.add_item(Box::new(unfollowable_item), true, true, None, cx)
})
});
executor.run_until_parked();
assert_eq!(
workspace.active_item(cx).unwrap().item_id(),
multibuffer_editor_a.item_id()
)
});
workspace_a.update(cx_a, |workspace, cx| workspace
.active_item(cx)
.unwrap()
.item_id()),
shared_screen.item_id()
);
// Client B activates a panel, and the previously-opened screen-sharing item gets activated.
let panel = cx_b.new_view(|cx| TestPanel::new(DockPosition::Left, cx));
workspace_b.update(cx_b, |workspace, cx| {
workspace.add_panel(panel, cx);
workspace.toggle_panel_focus::<TestPanel>(cx);
});
executor.run_until_parked();
assert_eq!(
workspace_a.update(cx_a, |workspace, cx| workspace
.active_item(cx)
.unwrap()
.item_id()),
shared_screen.item_id()
);
// Toggling the focus back to the pane causes client A to return to the multibuffer.
workspace_b.update(cx_b, |workspace, cx| {
workspace.toggle_panel_focus::<TestPanel>(cx);
});
executor.run_until_parked();
workspace_a.update(cx_a, |workspace, cx| {
// Following interrupts when client B disconnects.
client_b.disconnect(&cx_b.to_async());
executor.advance_clock(RECONNECT_TIMEOUT);
assert_eq!(
workspace.active_item(cx).unwrap().item_id(),
multibuffer_editor_a.item_id()
)
});
// Client B activates an item that doesn't implement following,
// so the previously-opened screen-sharing item gets activated.
let unfollowable_item = cx_b.new_view(TestItem::new);
workspace_b.update(cx_b, |workspace, cx| {
workspace.active_pane().update(cx, |pane, cx| {
pane.add_item(Box::new(unfollowable_item), true, true, None, cx)
})
});
executor.run_until_parked();
assert_eq!(
workspace_a.update(cx_a, |workspace, cx| workspace
.active_item(cx)
.unwrap()
.item_id()),
shared_screen.item_id()
);
// Following interrupts when client B disconnects.
client_b.disconnect(&cx_b.to_async());
executor.advance_clock(RECONNECT_TIMEOUT);
assert_eq!(
workspace_a.update(cx_a, |workspace, _| workspace.leader_for_pane(&pane_a)),
None
);
workspace_a.update(cx_a, |workspace, _| workspace.leader_for_pane(&pane_a)),
None
);
}
}
#[gpui::test]

View File

@@ -25,7 +25,6 @@ use language::{
tree_sitter_rust, tree_sitter_typescript, Diagnostic, DiagnosticEntry, FakeLspAdapter,
Language, LanguageConfig, LanguageMatcher, LineEnding, OffsetRangeExt, Point, Rope,
};
use live_kit_client::MacOSDisplay;
use lsp::LanguageServerId;
use parking_lot::Mutex;
use project::lsp_store::FormatTarget;
@@ -241,56 +240,60 @@ async fn test_basic_calls(
}
);
// User A shares their screen
let display = MacOSDisplay::new();
let events_b = active_call_events(cx_b);
let events_c = active_call_events(cx_c);
active_call_a
.update(cx_a, |call, cx| {
call.room().unwrap().update(cx, |room, cx| {
room.set_display_sources(vec![display.clone()]);
room.share_screen(cx)
// TODO: Re-enable this test once we can replace our swift Livekit SDK with the rust SDK
#[cfg(not(target_os = "macos"))]
{
// User A shares their screen
let display = gpui::TestScreenCaptureSource::new();
let events_b = active_call_events(cx_b);
let events_c = active_call_events(cx_c);
cx_a.set_screen_capture_sources(vec![display]);
active_call_a
.update(cx_a, |call, cx| {
call.room()
.unwrap()
.update(cx, |room, cx| room.share_screen(cx))
})
})
.await
.unwrap();
.await
.unwrap();
executor.run_until_parked();
executor.run_until_parked();
// User B observes the remote screen sharing track.
assert_eq!(events_b.borrow().len(), 1);
let event_b = events_b.borrow().first().unwrap().clone();
if let call::room::Event::RemoteVideoTracksChanged { participant_id } = event_b {
assert_eq!(participant_id, client_a.peer_id().unwrap());
// User B observes the remote screen sharing track.
assert_eq!(events_b.borrow().len(), 1);
let event_b = events_b.borrow().first().unwrap().clone();
if let call::room::Event::RemoteVideoTracksChanged { participant_id } = event_b {
assert_eq!(participant_id, client_a.peer_id().unwrap());
room_b.read_with(cx_b, |room, _| {
assert_eq!(
room.remote_participants()[&client_a.user_id().unwrap()]
.video_tracks
.len(),
1
);
});
} else {
panic!("unexpected event")
}
room_b.read_with(cx_b, |room, _| {
assert_eq!(
room.remote_participants()[&client_a.user_id().unwrap()]
.video_tracks
.len(),
1
);
});
} else {
panic!("unexpected event")
}
// User C observes the remote screen sharing track.
assert_eq!(events_c.borrow().len(), 1);
let event_c = events_c.borrow().first().unwrap().clone();
if let call::room::Event::RemoteVideoTracksChanged { participant_id } = event_c {
assert_eq!(participant_id, client_a.peer_id().unwrap());
// User C observes the remote screen sharing track.
assert_eq!(events_c.borrow().len(), 1);
let event_c = events_c.borrow().first().unwrap().clone();
if let call::room::Event::RemoteVideoTracksChanged { participant_id } = event_c {
assert_eq!(participant_id, client_a.peer_id().unwrap());
room_c.read_with(cx_c, |room, _| {
assert_eq!(
room.remote_participants()[&client_a.user_id().unwrap()]
.video_tracks
.len(),
1
);
});
} else {
panic!("unexpected event")
room_c.read_with(cx_c, |room, _| {
assert_eq!(
room.remote_participants()[&client_a.user_id().unwrap()]
.video_tracks
.len(),
1
);
});
} else {
panic!("unexpected event")
}
}
// User A leaves the room.
@@ -329,7 +332,7 @@ async fn test_basic_calls(
// to automatically leave the room. User C leaves the room as well because
// nobody else is in there.
server
.test_live_kit_server
.test_livekit_server
.disconnect_client(client_b.user_id().unwrap().to_string())
.await;
executor.run_until_parked();
@@ -844,7 +847,7 @@ async fn test_client_disconnecting_from_room(
// User B gets disconnected from the LiveKit server, which causes it
// to automatically leave the room.
server
.test_live_kit_server
.test_livekit_server
.disconnect_client(client_b.user_id().unwrap().to_string())
.await;
executor.run_until_parked();
@@ -1943,7 +1946,7 @@ async fn test_mute_deafen(
room_a.read_with(cx_a, |room, _| assert!(!room.is_muted()));
room_b.read_with(cx_b, |room, _| assert!(!room.is_muted()));
// Users A and B are both muted.
// Users A and B are both unmuted.
assert_eq!(
participant_audio_state(&room_a, cx_a),
&[ParticipantAudioState {
@@ -2075,7 +2078,17 @@ async fn test_mute_deafen(
audio_tracks_playing: participant
.audio_tracks
.values()
.map(|track| track.is_playing())
.map({
#[cfg(target_os = "macos")]
{
|track| track.is_playing()
}
#[cfg(not(target_os = "macos"))]
{
|(track, _)| track.rtc_track().enabled()
}
})
.collect(),
})
.collect::<Vec<_>>()
@@ -2561,19 +2574,23 @@ async fn test_git_diff_base_change(
.update(cx_a, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
.await
.unwrap();
let change_set_local_a = project_local
.update(cx_a, |p, cx| {
p.open_unstaged_changes(buffer_local_a.clone(), cx)
})
.await
.unwrap();
// Wait for it to catch up to the new diff
executor.run_until_parked();
// Smoke test diffing
buffer_local_a.read_with(cx_a, |buffer, _| {
change_set_local_a.read_with(cx_a, |change_set, cx| {
let buffer = buffer_local_a.read(cx);
assert_eq!(
buffer.diff_base().map(|rope| rope.to_string()).as_deref(),
change_set.base_text_string(cx).as_deref(),
Some(diff_base.as_str())
);
git::diff::assert_hunks(
buffer.snapshot().git_diff_hunks_in_row_range(0..4),
change_set.diff_to_buffer.hunks_in_row_range(0..4, buffer),
buffer,
&diff_base,
&[(1..2, "", "two\n")],
@@ -2585,25 +2602,30 @@ async fn test_git_diff_base_change(
.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
.await
.unwrap();
let change_set_remote_a = project_remote
.update(cx_b, |p, cx| {
p.open_unstaged_changes(buffer_remote_a.clone(), cx)
})
.await
.unwrap();
// Wait remote buffer to catch up to the new diff
executor.run_until_parked();
// Smoke test diffing
buffer_remote_a.read_with(cx_b, |buffer, _| {
change_set_remote_a.read_with(cx_b, |change_set, cx| {
let buffer = buffer_remote_a.read(cx);
assert_eq!(
buffer.diff_base().map(|rope| rope.to_string()).as_deref(),
change_set.base_text_string(cx).as_deref(),
Some(diff_base.as_str())
);
git::diff::assert_hunks(
buffer.snapshot().git_diff_hunks_in_row_range(0..4),
change_set.diff_to_buffer.hunks_in_row_range(0..4, buffer),
buffer,
&diff_base,
&[(1..2, "", "two\n")],
);
});
// Update the staged text of the open buffer
client_a.fs().set_index_for_repo(
Path::new("/dir/.git"),
&[(Path::new("a.txt"), new_diff_base.clone())],
@@ -2611,40 +2633,35 @@ async fn test_git_diff_base_change(
// Wait for buffer_local_a to receive it
executor.run_until_parked();
// Smoke test new diffing
buffer_local_a.read_with(cx_a, |buffer, _| {
change_set_local_a.read_with(cx_a, |change_set, cx| {
let buffer = buffer_local_a.read(cx);
assert_eq!(
buffer.diff_base().map(|rope| rope.to_string()).as_deref(),
change_set.base_text_string(cx).as_deref(),
Some(new_diff_base.as_str())
);
git::diff::assert_hunks(
buffer.snapshot().git_diff_hunks_in_row_range(0..4),
change_set.diff_to_buffer.hunks_in_row_range(0..4, buffer),
buffer,
&diff_base,
&new_diff_base,
&[(2..3, "", "three\n")],
);
});
// Smoke test B
buffer_remote_a.read_with(cx_b, |buffer, _| {
change_set_remote_a.read_with(cx_b, |change_set, cx| {
let buffer = buffer_remote_a.read(cx);
assert_eq!(
buffer.diff_base().map(|rope| rope.to_string()).as_deref(),
change_set.base_text_string(cx).as_deref(),
Some(new_diff_base.as_str())
);
git::diff::assert_hunks(
buffer.snapshot().git_diff_hunks_in_row_range(0..4),
change_set.diff_to_buffer.hunks_in_row_range(0..4, buffer),
buffer,
&diff_base,
&new_diff_base,
&[(2..3, "", "three\n")],
);
});
//Nested git dir
// Nested git dir
let diff_base = "
one
three
@@ -2667,19 +2684,23 @@ async fn test_git_diff_base_change(
.update(cx_a, |p, cx| p.open_buffer((worktree_id, "sub/b.txt"), cx))
.await
.unwrap();
let change_set_local_b = project_local
.update(cx_a, |p, cx| {
p.open_unstaged_changes(buffer_local_b.clone(), cx)
})
.await
.unwrap();
// Wait for it to catch up to the new diff
executor.run_until_parked();
// Smoke test diffing
buffer_local_b.read_with(cx_a, |buffer, _| {
change_set_local_b.read_with(cx_a, |change_set, cx| {
let buffer = buffer_local_b.read(cx);
assert_eq!(
buffer.diff_base().map(|rope| rope.to_string()).as_deref(),
change_set.base_text_string(cx).as_deref(),
Some(diff_base.as_str())
);
git::diff::assert_hunks(
buffer.snapshot().git_diff_hunks_in_row_range(0..4),
change_set.diff_to_buffer.hunks_in_row_range(0..4, buffer),
buffer,
&diff_base,
&[(1..2, "", "two\n")],
@@ -2691,25 +2712,29 @@ async fn test_git_diff_base_change(
.update(cx_b, |p, cx| p.open_buffer((worktree_id, "sub/b.txt"), cx))
.await
.unwrap();
let change_set_remote_b = project_remote
.update(cx_b, |p, cx| {
p.open_unstaged_changes(buffer_remote_b.clone(), cx)
})
.await
.unwrap();
// Wait remote buffer to catch up to the new diff
executor.run_until_parked();
// Smoke test diffing
buffer_remote_b.read_with(cx_b, |buffer, _| {
change_set_remote_b.read_with(cx_b, |change_set, cx| {
let buffer = buffer_remote_b.read(cx);
assert_eq!(
buffer.diff_base().map(|rope| rope.to_string()).as_deref(),
change_set.base_text_string(cx).as_deref(),
Some(diff_base.as_str())
);
git::diff::assert_hunks(
buffer.snapshot().git_diff_hunks_in_row_range(0..4),
change_set.diff_to_buffer.hunks_in_row_range(0..4, buffer),
buffer,
&diff_base,
&[(1..2, "", "two\n")],
);
});
// Update the staged text
client_a.fs().set_index_for_repo(
Path::new("/dir/sub/.git"),
&[(Path::new("b.txt"), new_diff_base.clone())],
@@ -2717,43 +2742,30 @@ async fn test_git_diff_base_change(
// Wait for buffer_local_b to receive it
executor.run_until_parked();
// Smoke test new diffing
buffer_local_b.read_with(cx_a, |buffer, _| {
change_set_local_b.read_with(cx_a, |change_set, cx| {
let buffer = buffer_local_b.read(cx);
assert_eq!(
buffer.diff_base().map(|rope| rope.to_string()).as_deref(),
change_set.base_text_string(cx).as_deref(),
Some(new_diff_base.as_str())
);
println!("{:?}", buffer.as_rope().to_string());
println!("{:?}", buffer.diff_base());
println!(
"{:?}",
buffer
.snapshot()
.git_diff_hunks_in_row_range(0..4)
.collect::<Vec<_>>()
);
git::diff::assert_hunks(
buffer.snapshot().git_diff_hunks_in_row_range(0..4),
change_set.diff_to_buffer.hunks_in_row_range(0..4, buffer),
buffer,
&diff_base,
&new_diff_base,
&[(2..3, "", "three\n")],
);
});
// Smoke test B
buffer_remote_b.read_with(cx_b, |buffer, _| {
change_set_remote_b.read_with(cx_b, |change_set, cx| {
let buffer = buffer_remote_b.read(cx);
assert_eq!(
buffer.diff_base().map(|rope| rope.to_string()).as_deref(),
change_set.base_text_string(cx).as_deref(),
Some(new_diff_base.as_str())
);
git::diff::assert_hunks(
buffer.snapshot().git_diff_hunks_in_row_range(0..4),
change_set.diff_to_buffer.hunks_in_row_range(0..4, buffer),
buffer,
&diff_base,
&new_diff_base,
&[(2..3, "", "three\n")],
);
});
@@ -6016,6 +6028,8 @@ async fn test_contact_requests(
}
}
// TODO: Re-enable this test once we can replace our swift Livekit SDK with the rust SDK
#[cfg(not(target_os = "macos"))]
#[gpui::test(iterations = 10)]
async fn test_join_call_after_screen_was_shared(
executor: BackgroundExecutor,
@@ -6058,13 +6072,13 @@ async fn test_join_call_after_screen_was_shared(
assert_eq!(call_b.calling_user.github_login, "user_a");
// User A shares their screen
let display = MacOSDisplay::new();
let display = gpui::TestScreenCaptureSource::new();
cx_a.set_screen_capture_sources(vec![display]);
active_call_a
.update(cx_a, |call, cx| {
call.room().unwrap().update(cx, |room, cx| {
room.set_display_sources(vec![display.clone()]);
room.share_screen(cx)
})
call.room()
.unwrap()
.update(cx, |room, cx| room.share_screen(cx))
})
.await
.unwrap();

View File

@@ -1336,10 +1336,24 @@ impl RandomizedTest for ProjectCollaborationTest {
(_, None) => panic!("guest's file is None, hosts's isn't"),
}
let host_diff_base = host_buffer
.read_with(host_cx, |b, _| b.diff_base().map(ToString::to_string));
let guest_diff_base = guest_buffer
.read_with(client_cx, |b, _| b.diff_base().map(ToString::to_string));
let host_diff_base = host_project.read_with(host_cx, |project, cx| {
project
.buffer_store()
.read(cx)
.get_unstaged_changes(host_buffer.read(cx).remote_id())
.unwrap()
.read(cx)
.base_text_string(cx)
});
let guest_diff_base = guest_project.read_with(client_cx, |project, cx| {
project
.buffer_store()
.read(cx)
.get_unstaged_changes(guest_buffer.read(cx).remote_id())
.unwrap()
.read(cx)
.base_text_string(cx)
});
assert_eq!(
guest_diff_base, host_diff_base,
"guest {} diff base does not match host's for path {path:?} in project {project_id}",

View File

@@ -45,9 +45,15 @@ use std::{
};
use workspace::{Workspace, WorkspaceStore};
#[cfg(not(target_os = "macos"))]
use livekit_client::test::TestServer as LivekitTestServer;
#[cfg(target_os = "macos")]
use livekit_client_macos::TestServer as LivekitTestServer;
pub struct TestServer {
pub app_state: Arc<AppState>,
pub test_live_kit_server: Arc<live_kit_client::TestServer>,
pub test_livekit_server: Arc<LivekitTestServer>,
server: Arc<Server>,
next_github_user_id: i32,
connection_killers: Arc<Mutex<HashMap<PeerId, Arc<AtomicBool>>>>,
@@ -79,7 +85,7 @@ pub struct ContactsSummary {
impl TestServer {
pub async fn start(deterministic: BackgroundExecutor) -> Self {
static NEXT_LIVE_KIT_SERVER_ID: AtomicUsize = AtomicUsize::new(0);
static NEXT_LIVEKIT_SERVER_ID: AtomicUsize = AtomicUsize::new(0);
let use_postgres = env::var("USE_POSTGRES").ok();
let use_postgres = use_postgres.as_deref();
@@ -88,16 +94,16 @@ impl TestServer {
} else {
TestDb::sqlite(deterministic.clone())
};
let live_kit_server_id = NEXT_LIVE_KIT_SERVER_ID.fetch_add(1, SeqCst);
let live_kit_server = live_kit_client::TestServer::create(
format!("http://livekit.{}.test", live_kit_server_id),
format!("devkey-{}", live_kit_server_id),
format!("secret-{}", live_kit_server_id),
let livekit_server_id = NEXT_LIVEKIT_SERVER_ID.fetch_add(1, SeqCst);
let livekit_server = LivekitTestServer::create(
format!("http://livekit.{}.test", livekit_server_id),
format!("devkey-{}", livekit_server_id),
format!("secret-{}", livekit_server_id),
deterministic.clone(),
)
.unwrap();
let executor = Executor::Deterministic(deterministic.clone());
let app_state = Self::build_app_state(&test_db, &live_kit_server, executor.clone()).await;
let app_state = Self::build_app_state(&test_db, &livekit_server, executor.clone()).await;
let epoch = app_state
.db
.create_server(&app_state.config.zed_environment)
@@ -114,7 +120,7 @@ impl TestServer {
forbid_connections: Default::default(),
next_github_user_id: 0,
_test_db: test_db,
test_live_kit_server: live_kit_server,
test_livekit_server: livekit_server,
}
}
@@ -500,13 +506,13 @@ impl TestServer {
pub async fn build_app_state(
test_db: &TestDb,
live_kit_test_server: &live_kit_client::TestServer,
livekit_test_server: &LivekitTestServer,
executor: Executor,
) -> Arc<AppState> {
Arc::new(AppState {
db: test_db.db().clone(),
llm_db: None,
live_kit_client: Some(Arc::new(live_kit_test_server.create_api_client())),
livekit_client: Some(Arc::new(livekit_test_server.create_api_client())),
blob_store_client: None,
stripe_client: None,
stripe_billing: None,
@@ -520,9 +526,9 @@ impl TestServer {
database_max_connections: 0,
api_token: "".into(),
invite_link_prefix: "".into(),
live_kit_server: None,
live_kit_key: None,
live_kit_secret: None,
livekit_server: None,
livekit_key: None,
livekit_secret: None,
llm_database_url: None,
llm_database_max_connections: None,
llm_database_migrations_path: None,
@@ -572,7 +578,7 @@ impl Deref for TestServer {
impl Drop for TestServer {
fn drop(&mut self) {
self.server.teardown();
self.test_live_kit_server.teardown().unwrap();
self.test_livekit_server.teardown().unwrap();
}
}
@@ -585,7 +591,7 @@ impl Deref for TestClient {
}
impl TestClient {
pub fn fs(&self) -> &FakeFs {
pub fn fs(&self) -> Arc<FakeFs> {
self.app_state.fs.as_fake()
}

View File

@@ -474,11 +474,10 @@ impl CollabPanel {
project_id: project.id,
worktree_root_names: project.worktree_root_names.clone(),
host_user_id: participant.user.id,
is_last: projects.peek().is_none()
&& participant.video_tracks.is_empty(),
is_last: projects.peek().is_none() && !participant.has_video_tracks(),
});
}
if !participant.video_tracks.is_empty() {
if participant.has_video_tracks() {
self.entries.push(ListEntry::ParticipantScreen {
peer_id: Some(participant.peer_id),
is_last: true,

View File

@@ -167,11 +167,18 @@ pub struct InitializeResponse {
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ResourcesReadResponse {
pub contents: Vec<ResourceContents>,
pub contents: Vec<ResourceContentsType>,
#[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
pub meta: Option<HashMap<String, serde_json::Value>>,
}
#[derive(Debug, Deserialize)]
#[serde(untagged)]
pub enum ResourceContentsType {
Text(TextResourceContents),
Blob(BlobResourceContents),
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ResourcesListResponse {
@@ -181,6 +188,7 @@ pub struct ResourcesListResponse {
#[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
pub meta: Option<HashMap<String, serde_json::Value>>,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SamplingMessage {
@@ -188,6 +196,35 @@ pub struct SamplingMessage {
pub content: MessageContent,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct CreateMessageRequest {
pub messages: Vec<SamplingMessage>,
#[serde(skip_serializing_if = "Option::is_none")]
pub model_preferences: Option<ModelPreferences>,
#[serde(skip_serializing_if = "Option::is_none")]
pub system_prompt: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub include_context: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub temperature: Option<f64>,
pub max_tokens: u32,
#[serde(skip_serializing_if = "Option::is_none")]
pub stop_sequences: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub metadata: Option<serde_json::Value>,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CreateMessageResult {
pub role: Role,
pub content: MessageContent,
pub model: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub stop_reason: Option<String>,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PromptMessage {
@@ -206,11 +243,33 @@ pub enum Role {
#[serde(tag = "type")]
pub enum MessageContent {
#[serde(rename = "text")]
Text { text: String },
Text {
text: String,
#[serde(skip_serializing_if = "Option::is_none")]
annotations: Option<MessageAnnotations>,
},
#[serde(rename = "image")]
Image { data: String, mime_type: String },
Image {
data: String,
mime_type: String,
#[serde(skip_serializing_if = "Option::is_none")]
annotations: Option<MessageAnnotations>,
},
#[serde(rename = "resource")]
Resource { resource: ResourceContents },
Resource {
resource: ResourceContents,
#[serde(skip_serializing_if = "Option::is_none")]
annotations: Option<MessageAnnotations>,
},
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct MessageAnnotations {
#[serde(skip_serializing_if = "Option::is_none")]
pub audience: Option<Vec<Role>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub priority: Option<f64>,
}
#[derive(Debug, Deserialize)]
@@ -460,6 +519,11 @@ pub enum ClientNotification {
Initialized,
Progress(ProgressParams),
RootsListChanged,
Cancelled {
request_id: String,
#[serde(skip_serializing_if = "Option::is_none")]
reason: Option<String>,
},
}
#[derive(Debug, Serialize, Deserialize)]
@@ -532,6 +596,16 @@ pub struct ListToolsResponse {
pub meta: Option<HashMap<String, serde_json::Value>>,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ListResourceTemplatesResponse {
pub resource_templates: Vec<ResourceTemplate>,
#[serde(skip_serializing_if = "Option::is_none")]
pub next_cursor: Option<String>,
#[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
pub meta: Option<HashMap<String, serde_json::Value>>,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ListRootsResponse {

View File

@@ -197,7 +197,7 @@ pub fn init(fs: Arc<dyn Fs>, client: Arc<dyn HttpClient>, cx: &mut AppContext) {
cx.set_global(GlobalCopilotChat(copilot_chat));
}
fn copilot_chat_config_path() -> &'static PathBuf {
fn copilot_chat_config_dir() -> &'static PathBuf {
static COPILOT_CHAT_CONFIG_DIR: OnceLock<PathBuf> = OnceLock::new();
COPILOT_CHAT_CONFIG_DIR.get_or_init(|| {
@@ -207,10 +207,14 @@ fn copilot_chat_config_path() -> &'static PathBuf {
home_dir().join(".config")
}
.join("github-copilot")
.join("hosts.json")
})
}
fn copilot_chat_config_paths() -> [PathBuf; 2] {
let base_dir = copilot_chat_config_dir();
[base_dir.join("hosts.json"), base_dir.join("apps.json")]
}
impl CopilotChat {
pub fn global(cx: &AppContext) -> Option<gpui::Model<Self>> {
cx.try_global::<GlobalCopilotChat>()
@@ -218,13 +222,24 @@ impl CopilotChat {
}
pub fn new(fs: Arc<dyn Fs>, client: Arc<dyn HttpClient>, cx: &AppContext) -> Self {
let mut config_file_rx = watch_config_file(
cx.background_executor(),
fs,
copilot_chat_config_path().clone(),
);
let config_paths = copilot_chat_config_paths();
let resolve_config_path = {
let fs = fs.clone();
async move {
for config_path in config_paths.iter() {
if fs.metadata(config_path).await.is_ok_and(|v| v.is_some()) {
return config_path.clone();
}
}
config_paths[0].clone()
}
};
cx.spawn(|cx| async move {
let config_file = resolve_config_path.await;
let mut config_file_rx = watch_config_file(cx.background_executor(), fs, config_file);
while let Some(contents) = config_file_rx.next().await {
let oauth_token = extract_oauth_token(contents);
@@ -318,9 +333,15 @@ async fn request_api_token(oauth_token: &str, client: Arc<dyn HttpClient>) -> Re
fn extract_oauth_token(contents: String) -> Option<String> {
serde_json::from_str::<serde_json::Value>(&contents)
.map(|v| {
v["github.com"]["oauth_token"]
.as_str()
.map(|v| v.to_string())
v.as_object().and_then(|obj| {
obj.iter().find_map(|(key, value)| {
if key.starts_with("github.com") {
value["oauth_token"].as_str().map(|v| v.to_string())
} else {
None
}
})
})
})
.ok()
.flatten()

View File

@@ -716,7 +716,7 @@ impl Item for ProjectDiagnosticsEditor {
fn for_each_project_item(
&self,
cx: &AppContext,
f: &mut dyn FnMut(gpui::EntityId, &dyn project::Item),
f: &mut dyn FnMut(gpui::EntityId, &dyn project::ProjectItem),
) {
self.editor.for_each_project_item(cx, f)
}

View File

@@ -1,6 +1,8 @@
use std::time::Duration;
use editor::Editor;
use gpui::{
rems, EventEmitter, IntoElement, ParentElement, Render, Styled, Subscription, View,
EventEmitter, IntoElement, ParentElement, Render, Styled, Subscription, Task, View,
ViewContext, WeakView,
};
use language::Diagnostic;
@@ -15,6 +17,7 @@ pub struct DiagnosticIndicator {
workspace: WeakView<Workspace>,
current_diagnostic: Option<Diagnostic>,
_observe_active_editor: Option<Subscription>,
diagnostics_update: Task<()>,
}
impl Render for DiagnosticIndicator {
@@ -77,8 +80,10 @@ impl Render for DiagnosticIndicator {
};
h_flex()
.h(rems(1.375))
.gap_2()
.pl_1()
.border_l_1()
.border_color(cx.theme().colors().border)
.child(
ButtonLike::new("diagnostic-indicator")
.child(diagnostic_indicator)
@@ -124,6 +129,7 @@ impl DiagnosticIndicator {
workspace: workspace.weak_handle(),
current_diagnostic: None,
_observe_active_editor: None,
diagnostics_update: Task::ready(()),
}
}
@@ -147,8 +153,17 @@ impl DiagnosticIndicator {
.min_by_key(|entry| (entry.diagnostic.severity, entry.range.len()))
.map(|entry| entry.diagnostic);
if new_diagnostic != self.current_diagnostic {
self.current_diagnostic = new_diagnostic;
cx.notify();
self.diagnostics_update = cx.spawn(|diagnostics_indicator, mut cx| async move {
cx.background_executor()
.timer(Duration::from_millis(50))
.await;
diagnostics_indicator
.update(&mut cx, |diagnostics_indicator, cx| {
diagnostics_indicator.current_diagnostic = new_diagnostic;
cx.notify();
})
.ok();
});
}
}
}

View File

@@ -39,6 +39,7 @@ collections.workspace = true
convert_case.workspace = true
db.workspace = true
emojis.workspace = true
feature_flags.workspace = true
file_icons.workspace = true
futures.workspace = true
fuzzy.workspace = true
@@ -97,6 +98,7 @@ project = { workspace = true, features = ["test-support"] }
release_channel.workspace = true
rand.workspace = true
settings = { workspace = true, features = ["test-support"] }
tempfile.workspace = true
text = { workspace = true, features = ["test-support"] }
theme = { workspace = true, features = ["test-support"] }
tree-sitter-html.workspace = true

View File

@@ -248,6 +248,7 @@ gpui::actions!(
FindAllReferences,
Fold,
FoldAll,
FoldFunctionBodies,
FoldRecursive,
FoldSelectedRanges,
ToggleFold,
@@ -295,6 +296,7 @@ gpui::actions!(
NewlineBelow,
NextInlineCompletion,
NextScreen,
OpenContextMenu,
OpenExcerpts,
OpenExcerptsSplit,
OpenProposedChangesEditor,
@@ -303,6 +305,7 @@ gpui::actions!(
OpenPermalinkToLine,
OpenUrl,
Outdent,
AutoIndent,
PageDown,
PageUp,
Paste,

View File

@@ -684,8 +684,8 @@ impl DisplaySnapshot {
.map(|row| row.map(MultiBufferRow))
}
pub fn max_buffer_row(&self) -> MultiBufferRow {
self.buffer_snapshot.max_buffer_row()
pub fn widest_line_number(&self) -> u32 {
self.buffer_snapshot.widest_line_number()
}
pub fn prev_line_boundary(&self, mut point: MultiBufferPoint) -> (Point, DisplayPoint) {
@@ -726,11 +726,10 @@ impl DisplaySnapshot {
// used by line_mode selections and tries to match vim behavior
pub fn expand_to_line(&self, range: Range<Point>) -> Range<Point> {
let max_row = self.buffer_snapshot.max_row().0;
let new_start = if range.start.row == 0 {
MultiBufferPoint::new(0, 0)
} else if range.start.row == self.max_buffer_row().0
|| (range.end.column > 0 && range.end.row == self.max_buffer_row().0)
{
} else if range.start.row == max_row || (range.end.column > 0 && range.end.row == max_row) {
MultiBufferPoint::new(
range.start.row - 1,
self.buffer_snapshot
@@ -742,7 +741,7 @@ impl DisplaySnapshot {
let new_end = if range.end.column == 0 {
range.end
} else if range.end.row < self.max_buffer_row().0 {
} else if range.end.row < max_row {
self.buffer_snapshot
.clip_point(MultiBufferPoint::new(range.end.row + 1, 0), Bias::Left)
} else {
@@ -1127,7 +1126,7 @@ impl DisplaySnapshot {
}
pub fn starts_indent(&self, buffer_row: MultiBufferRow) -> bool {
let max_row = self.buffer_snapshot.max_buffer_row();
let max_row = self.buffer_snapshot.max_row();
if buffer_row >= max_row {
return false;
}

View File

@@ -1019,7 +1019,7 @@ impl InlaySnapshot {
let inlay_point = InlayPoint::new(row, 0);
cursor.seek(&inlay_point, Bias::Left, &());
let max_buffer_row = MultiBufferRow(self.buffer.max_point().row);
let max_buffer_row = self.buffer.max_row();
let mut buffer_point = cursor.start().1;
let buffer_row = if row == 0 {
MultiBufferRow(0)

File diff suppressed because it is too large Load Diff

View File

@@ -18,6 +18,7 @@ pub struct EditorSettings {
pub gutter: Gutter,
pub scroll_beyond_last_line: ScrollBeyondLastLine,
pub vertical_scroll_margin: f32,
pub autoscroll_on_clicks: bool,
pub scroll_sensitivity: f32,
pub relative_line_numbers: bool,
pub seed_search_query_from_cursor: SeedQuerySetting,
@@ -222,6 +223,10 @@ pub struct EditorSettingsContent {
///
/// Default: 3.
pub vertical_scroll_margin: Option<f32>,
/// Whether to scroll when clicking near the edge of the visible text area.
///
/// Default: false
pub autoscroll_on_clicks: Option<bool>,
/// Scroll sensitivity multiplier. This multiplier is applied
/// to both the horizontal and vertical delta values while scrolling.
///

File diff suppressed because it is too large Load Diff

View File

@@ -169,6 +169,7 @@ impl EditorElement {
crate::rust_analyzer_ext::apply_related_actions(view, cx);
crate::clangd_ext::apply_related_actions(view, cx);
register_action(view, cx, Editor::open_context_menu);
register_action(view, cx, Editor::move_left);
register_action(view, cx, Editor::move_right);
register_action(view, cx, Editor::move_down);
@@ -189,6 +190,7 @@ impl EditorElement {
register_action(view, cx, Editor::tab_prev);
register_action(view, cx, Editor::indent);
register_action(view, cx, Editor::outdent);
register_action(view, cx, Editor::autoindent);
register_action(view, cx, Editor::delete_line);
register_action(view, cx, Editor::join_lines);
register_action(view, cx, Editor::sort_lines_case_sensitive);
@@ -341,6 +343,7 @@ impl EditorElement {
register_action(view, cx, Editor::fold);
register_action(view, cx, Editor::fold_at_level);
register_action(view, cx, Editor::fold_all);
register_action(view, cx, Editor::fold_function_bodies);
register_action(view, cx, Editor::fold_at);
register_action(view, cx, Editor::fold_recursive);
register_action(view, cx, Editor::toggle_fold);
@@ -593,7 +596,7 @@ impl EditorElement {
position_map.point_for_position(text_hitbox.bounds, event.position);
mouse_context_menu::deploy_context_menu(
editor,
event.position,
Some(event.position),
point_for_position.previous_valid,
cx,
);
@@ -1166,7 +1169,7 @@ impl EditorElement {
let editor = self.editor.read(cx);
let is_singleton = editor.is_singleton(cx);
// Git
(is_singleton && scrollbar_settings.git_diff && snapshot.buffer_snapshot.has_git_diffs())
(is_singleton && scrollbar_settings.git_diff && !snapshot.diff_map.is_empty())
||
// Buffer Search Results
(is_singleton && scrollbar_settings.search_results && editor.has_background_highlights::<BufferSearchHighlights>())
@@ -1317,17 +1320,8 @@ impl EditorElement {
cx: &mut WindowContext,
) -> Vec<(DisplayDiffHunk, Option<Hitbox>)> {
let buffer_snapshot = &snapshot.buffer_snapshot;
let buffer_start_row = MultiBufferRow(
DisplayPoint::new(display_rows.start, 0)
.to_point(snapshot)
.row,
);
let buffer_end_row = MultiBufferRow(
DisplayPoint::new(display_rows.end, 0)
.to_point(snapshot)
.row,
);
let buffer_start = DisplayPoint::new(display_rows.start, 0).to_point(snapshot);
let buffer_end = DisplayPoint::new(display_rows.end, 0).to_point(snapshot);
let git_gutter_setting = ProjectSettings::get_global(cx)
.git
@@ -1335,7 +1329,7 @@ impl EditorElement {
.unwrap_or_default();
self.editor.update(cx, |editor, cx| {
let expanded_hunks = &editor.expanded_hunks.hunks;
let expanded_hunks = &editor.diff_map.hunks;
let expanded_hunks_start_ix = expanded_hunks
.binary_search_by(|hunk| {
hunk.hunk_range
@@ -1346,8 +1340,10 @@ impl EditorElement {
.unwrap_err();
let mut expanded_hunks = expanded_hunks[expanded_hunks_start_ix..].iter().peekable();
let display_hunks = buffer_snapshot
.git_diff_hunks_in_range(buffer_start_row..buffer_end_row)
let mut display_hunks: Vec<(DisplayDiffHunk, Option<Hitbox>)> = editor
.diff_map
.snapshot
.diff_hunks_in_range(buffer_start..buffer_end, &buffer_snapshot)
.filter_map(|hunk| {
let display_hunk = diff_hunk_to_display(&hunk, snapshot);
@@ -1390,25 +1386,23 @@ impl EditorElement {
Some(display_hunk)
})
.dedup()
.map(|hunk| match git_gutter_setting {
GitGutterSetting::TrackedFiles => {
let hitbox = match hunk {
DisplayDiffHunk::Unfolded { .. } => {
let hunk_bounds = Self::diff_hunk_bounds(
snapshot,
line_height,
gutter_hitbox.bounds,
&hunk,
);
Some(cx.insert_hitbox(hunk_bounds, true))
}
DisplayDiffHunk::Folded { .. } => None,
};
(hunk, hitbox)
}
GitGutterSetting::Hide => (hunk, None),
})
.map(|hunk| (hunk, None))
.collect();
if let GitGutterSetting::TrackedFiles = git_gutter_setting {
for (hunk, hitbox) in &mut display_hunks {
if let DisplayDiffHunk::Unfolded { .. } = hunk {
let hunk_bounds = Self::diff_hunk_bounds(
snapshot,
line_height,
gutter_hitbox.bounds,
&hunk,
);
*hitbox = Some(cx.insert_hitbox(hunk_bounds, true));
};
}
}
display_hunks
})
}
@@ -2728,6 +2722,7 @@ impl EditorElement {
&self,
editor_snapshot: &EditorSnapshot,
visible_range: Range<DisplayRow>,
content_origin: gpui::Point<Pixels>,
cx: &mut WindowContext,
) -> Option<AnyElement> {
let position = self.editor.update(cx, |editor, cx| {
@@ -2745,16 +2740,11 @@ impl EditorElement {
let mouse_context_menu = editor.mouse_context_menu.as_ref()?;
let (source_display_point, position) = match mouse_context_menu.position {
MenuPosition::PinnedToScreen(point) => (None, point),
MenuPosition::PinnedToEditor {
source,
offset_x,
offset_y,
} => {
MenuPosition::PinnedToEditor { source, offset } => {
let source_display_point = source.to_display_point(editor_snapshot);
let mut source_point = editor.to_pixel_point(source, editor_snapshot, cx)?;
source_point.x += offset_x;
source_point.y += offset_y;
(Some(source_display_point), source_point)
let source_point = editor.to_pixel_point(source, editor_snapshot, cx)?;
let position = content_origin + source_point + offset;
(Some(source_display_point), position)
}
};
@@ -3756,10 +3746,8 @@ impl EditorElement {
let mut marker_quads = Vec::new();
if scrollbar_settings.git_diff {
let marker_row_ranges = snapshot
.buffer_snapshot
.git_diff_hunks_in_range(
MultiBufferRow::MIN..MultiBufferRow::MAX,
)
.diff_map
.diff_hunks(&snapshot.buffer_snapshot)
.map(|hunk| {
let start_display_row =
MultiBufferPoint::new(hunk.row_range.start.0, 0)
@@ -4139,13 +4127,7 @@ impl EditorElement {
}
fn max_line_number_width(&self, snapshot: &EditorSnapshot, cx: &WindowContext) -> Pixels {
let digit_count = snapshot
.max_buffer_row()
.next_row()
.as_f32()
.log10()
.floor() as usize
+ 1;
let digit_count = (snapshot.widest_line_number() as f32).log10().floor() as usize + 1;
self.column_pixels(digit_count, cx)
}
}
@@ -4329,8 +4311,8 @@ fn deploy_blame_entry_context_menu(
});
editor.update(cx, move |editor, cx| {
editor.mouse_context_menu = Some(MouseContextMenu::pinned_to_screen(
position,
editor.mouse_context_menu = Some(MouseContextMenu::new(
MenuPosition::PinnedToScreen(position),
context_menu,
cx,
));
@@ -5447,7 +5429,7 @@ impl Element for EditorElement {
let expanded_add_hunks_by_rows = self.editor.update(cx, |editor, _| {
editor
.expanded_hunks
.diff_map
.hunks(false)
.filter(|hunk| hunk.status == DiffHunkStatus::Added)
.map(|expanded_hunk| {
@@ -5582,8 +5564,12 @@ impl Element for EditorElement {
);
}
let mouse_context_menu =
self.layout_mouse_context_menu(&snapshot, start_row..end_row, cx);
let mouse_context_menu = self.layout_mouse_context_menu(
&snapshot,
start_row..end_row,
content_origin,
cx,
);
cx.with_element_namespace("crease_toggles", |cx| {
self.prepaint_crease_toggles(

View File

@@ -1 +1,2 @@
pub mod blame;
pub mod project_diff;

View File

@@ -10,7 +10,7 @@ use gpui::{Model, ModelContext, Subscription, Task};
use http_client::HttpClient;
use language::{markdown, Bias, Buffer, BufferSnapshot, Edit, LanguageRegistry, ParsedMarkdown};
use multi_buffer::MultiBufferRow;
use project::{Item, Project};
use project::{Project, ProjectItem};
use smallvec::SmallVec;
use sum_tree::SumTree;
use url::Url;
@@ -154,7 +154,7 @@ impl GitBlame {
this.generate(cx);
}
}
project::Event::WorktreeUpdatedGitRepositories => {
project::Event::WorktreeUpdatedGitRepositories(_) => {
log::debug!("Status of git repositories updated. Regenerating blame data...",);
this.generate(cx);
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,8 +1,9 @@
use crate::{
editor_settings::MultiCursorModifier,
hover_popover::{self, InlayHover},
scroll::ScrollAmount,
Anchor, Editor, EditorSnapshot, FindAllReferences, GoToDefinition, GoToTypeDefinition,
GotoDefinitionKind, InlayId, Navigated, PointForPosition, SelectPhase,
Anchor, Editor, EditorSettings, EditorSnapshot, FindAllReferences, GoToDefinition,
GoToTypeDefinition, GotoDefinitionKind, InlayId, Navigated, PointForPosition, SelectPhase,
};
use gpui::{px, AppContext, AsyncWindowContext, Model, Modifiers, Task, ViewContext};
use language::{Bias, ToOffset};
@@ -12,6 +13,7 @@ use project::{
HoverBlock, HoverBlockKind, InlayHintLabelPartTooltip, InlayHintTooltip, LocationLink, Project,
ResolveState, ResolvedPath,
};
use settings::Settings;
use std::ops::Range;
use theme::ActiveTheme as _;
use util::{maybe, ResultExt, TryFutureExt as _};
@@ -117,7 +119,12 @@ impl Editor {
modifiers: Modifiers,
cx: &mut ViewContext<Self>,
) {
if !modifiers.secondary() || self.has_pending_selection() {
let multi_cursor_setting = EditorSettings::get_global(cx).multi_cursor_modifier;
let hovered_link_modifier = match multi_cursor_setting {
MultiCursorModifier::Alt => modifiers.secondary(),
MultiCursorModifier::CmdOrCtrl => modifiers.alt,
};
if !hovered_link_modifier || self.has_pending_selection() {
self.hide_hovered_link(cx);
return;
}
@@ -137,7 +144,7 @@ impl Editor {
snapshot,
point_for_position,
self,
modifiers.secondary(),
hovered_link_modifier,
modifiers.shift,
cx,
);

View File

@@ -378,7 +378,7 @@ fn show_hover(
},
..Default::default()
};
Markdown::new_text(text, markdown_style.clone(), None, cx, None)
Markdown::new_text(text, markdown_style.clone(), None, None, cx)
})
.ok();
@@ -593,8 +593,8 @@ async fn parse_blocks(
combined_text,
markdown_style.clone(),
Some(language_registry.clone()),
cx,
fallback_language_name,
cx,
)
})
.ok();

View File

@@ -1,12 +1,17 @@
use collections::{hash_map, HashMap, HashSet};
use collections::{HashMap, HashSet};
use git::diff::DiffHunkStatus;
use gpui::{Action, AnchorCorner, AppContext, CursorStyle, Hsla, Model, MouseButton, Task, View};
use gpui::{
Action, AnchorCorner, AppContext, CursorStyle, Hsla, Model, MouseButton, Subscription, Task,
View,
};
use language::{Buffer, BufferId, Point};
use multi_buffer::{
Anchor, AnchorRangeExt, ExcerptRange, MultiBuffer, MultiBufferDiffHunk, MultiBufferRow,
MultiBufferSnapshot, ToPoint,
MultiBufferSnapshot, ToOffset, ToPoint,
};
use project::buffer_store::BufferChangeSet;
use std::{ops::Range, sync::Arc};
use sum_tree::TreeMap;
use text::OffsetRangeExt;
use ui::{
prelude::*, ActiveTheme, ContextMenu, IconButtonShape, InteractiveElement, IntoElement,
@@ -29,10 +34,11 @@ pub(super) struct HoveredHunk {
pub diff_base_byte_range: Range<usize>,
}
#[derive(Debug, Default)]
pub(super) struct ExpandedHunks {
#[derive(Default)]
pub(super) struct DiffMap {
pub(crate) hunks: Vec<ExpandedHunk>,
diff_base: HashMap<BufferId, DiffBaseBuffer>,
pub(crate) diff_bases: HashMap<BufferId, DiffBaseState>,
pub(crate) snapshot: DiffMapSnapshot,
hunk_update_tasks: HashMap<Option<BufferId>, Task<()>>,
expand_all: bool,
}
@@ -46,10 +52,13 @@ pub(super) struct ExpandedHunk {
pub folded: bool,
}
#[derive(Debug)]
struct DiffBaseBuffer {
buffer: Model<Buffer>,
diff_base_version: usize,
#[derive(Clone, Debug, Default)]
pub(crate) struct DiffMapSnapshot(TreeMap<BufferId, git::diff::BufferDiff>);
pub(crate) struct DiffBaseState {
pub(crate) change_set: Model<BufferChangeSet>,
pub(crate) last_version: Option<usize>,
_subscription: Subscription,
}
#[derive(Debug, Clone, PartialEq, Eq)]
@@ -66,7 +75,38 @@ pub enum DisplayDiffHunk {
},
}
impl ExpandedHunks {
impl DiffMap {
pub fn snapshot(&self) -> DiffMapSnapshot {
self.snapshot.clone()
}
pub fn add_change_set(
&mut self,
change_set: Model<BufferChangeSet>,
cx: &mut ViewContext<Editor>,
) {
let buffer_id = change_set.read(cx).buffer_id;
self.snapshot
.0
.insert(buffer_id, change_set.read(cx).diff_to_buffer.clone());
Editor::sync_expanded_diff_hunks(self, buffer_id, cx);
self.diff_bases.insert(
buffer_id,
DiffBaseState {
last_version: None,
_subscription: cx.observe(&change_set, move |editor, change_set, cx| {
editor
.diff_map
.snapshot
.0
.insert(buffer_id, change_set.read(cx).diff_to_buffer.clone());
Editor::sync_expanded_diff_hunks(&mut editor.diff_map, buffer_id, cx);
}),
change_set,
},
);
}
pub fn hunks(&self, include_folded: bool) -> impl Iterator<Item = &ExpandedHunk> {
self.hunks
.iter()
@@ -74,9 +114,92 @@ impl ExpandedHunks {
}
}
impl DiffMapSnapshot {
pub fn is_empty(&self) -> bool {
self.0.values().all(|diff| diff.is_empty())
}
pub fn diff_hunks<'a>(
&'a self,
buffer_snapshot: &'a MultiBufferSnapshot,
) -> impl Iterator<Item = MultiBufferDiffHunk> + 'a {
self.diff_hunks_in_range(0..buffer_snapshot.len(), buffer_snapshot)
}
pub fn diff_hunks_in_range<'a, T: ToOffset>(
&'a self,
range: Range<T>,
buffer_snapshot: &'a MultiBufferSnapshot,
) -> impl Iterator<Item = MultiBufferDiffHunk> + 'a {
let range = range.start.to_offset(buffer_snapshot)..range.end.to_offset(buffer_snapshot);
buffer_snapshot
.excerpts_for_range(range.clone())
.filter_map(move |excerpt| {
let buffer = excerpt.buffer();
let buffer_id = buffer.remote_id();
let diff = self.0.get(&buffer_id)?;
let buffer_range = excerpt.map_range_to_buffer(range.clone());
let buffer_range =
buffer.anchor_before(buffer_range.start)..buffer.anchor_after(buffer_range.end);
Some(
diff.hunks_intersecting_range(buffer_range, excerpt.buffer())
.map(move |hunk| {
let start =
excerpt.map_point_from_buffer(Point::new(hunk.row_range.start, 0));
let end =
excerpt.map_point_from_buffer(Point::new(hunk.row_range.end, 0));
MultiBufferDiffHunk {
row_range: MultiBufferRow(start.row)..MultiBufferRow(end.row),
buffer_id,
buffer_range: hunk.buffer_range.clone(),
diff_base_byte_range: hunk.diff_base_byte_range.clone(),
}
}),
)
})
.flatten()
}
pub fn diff_hunks_in_range_rev<'a, T: ToOffset>(
&'a self,
range: Range<T>,
buffer_snapshot: &'a MultiBufferSnapshot,
) -> impl Iterator<Item = MultiBufferDiffHunk> + 'a {
let range = range.start.to_offset(buffer_snapshot)..range.end.to_offset(buffer_snapshot);
buffer_snapshot
.excerpts_for_range_rev(range.clone())
.filter_map(move |excerpt| {
let buffer = excerpt.buffer();
let buffer_id = buffer.remote_id();
let diff = self.0.get(&buffer_id)?;
let buffer_range = excerpt.map_range_to_buffer(range.clone());
let buffer_range =
buffer.anchor_before(buffer_range.start)..buffer.anchor_after(buffer_range.end);
Some(
diff.hunks_intersecting_range_rev(buffer_range, excerpt.buffer())
.map(move |hunk| {
let start_row = excerpt
.map_point_from_buffer(Point::new(hunk.row_range.start, 0))
.row;
let end_row = excerpt
.map_point_from_buffer(Point::new(hunk.row_range.end, 0))
.row;
MultiBufferDiffHunk {
row_range: MultiBufferRow(start_row)..MultiBufferRow(end_row),
buffer_id,
buffer_range: hunk.buffer_range.clone(),
diff_base_byte_range: hunk.diff_base_byte_range.clone(),
}
}),
)
})
.flatten()
}
}
impl Editor {
pub fn set_expand_all_diff_hunks(&mut self) {
self.expanded_hunks.expand_all = true;
self.diff_map.expand_all = true;
}
pub(super) fn toggle_hovered_hunk(
@@ -92,18 +215,15 @@ impl Editor {
}
pub fn toggle_hunk_diff(&mut self, _: &ToggleHunkDiff, cx: &mut ViewContext<Self>) {
let multi_buffer_snapshot = self.buffer().read(cx).snapshot(cx);
let selections = self.selections.disjoint_anchors();
self.toggle_hunks_expanded(
hunks_for_selections(&multi_buffer_snapshot, &selections),
cx,
);
let snapshot = self.snapshot(cx);
let selections = self.selections.all(cx);
self.toggle_hunks_expanded(hunks_for_selections(&snapshot, &selections), cx);
}
pub fn expand_all_hunk_diffs(&mut self, _: &ExpandAllHunkDiffs, cx: &mut ViewContext<Self>) {
let snapshot = self.snapshot(cx);
let display_rows_with_expanded_hunks = self
.expanded_hunks
.diff_map
.hunks(false)
.map(|hunk| &hunk.hunk_range)
.map(|anchor_range| {
@@ -119,10 +239,10 @@ impl Editor {
)
})
.collect::<HashMap<_, _>>();
let hunks = snapshot
.display_snapshot
.buffer_snapshot
.git_diff_hunks_in_range(MultiBufferRow::MIN..MultiBufferRow::MAX)
let hunks = self
.diff_map
.snapshot
.diff_hunks(&snapshot.display_snapshot.buffer_snapshot)
.filter(|hunk| {
let hunk_display_row_range = Point::new(hunk.row_range.start.0, 0)
.to_display_point(&snapshot.display_snapshot)
@@ -140,11 +260,11 @@ impl Editor {
hunks_to_toggle: Vec<MultiBufferDiffHunk>,
cx: &mut ViewContext<Self>,
) {
if self.expanded_hunks.expand_all {
if self.diff_map.expand_all {
return;
}
let previous_toggle_task = self.expanded_hunks.hunk_update_tasks.remove(&None);
let previous_toggle_task = self.diff_map.hunk_update_tasks.remove(&None);
let new_toggle_task = cx.spawn(move |editor, mut cx| async move {
if let Some(task) = previous_toggle_task {
task.await;
@@ -154,11 +274,10 @@ impl Editor {
.update(&mut cx, |editor, cx| {
let snapshot = editor.snapshot(cx);
let mut hunks_to_toggle = hunks_to_toggle.into_iter().fuse().peekable();
let mut highlights_to_remove =
Vec::with_capacity(editor.expanded_hunks.hunks.len());
let mut highlights_to_remove = Vec::with_capacity(editor.diff_map.hunks.len());
let mut blocks_to_remove = HashSet::default();
let mut hunks_to_expand = Vec::new();
editor.expanded_hunks.hunks.retain(|expanded_hunk| {
editor.diff_map.hunks.retain(|expanded_hunk| {
if expanded_hunk.folded {
return true;
}
@@ -238,7 +357,7 @@ impl Editor {
.ok();
});
self.expanded_hunks
self.diff_map
.hunk_update_tasks
.insert(None, cx.background_executor().spawn(new_toggle_task));
}
@@ -252,30 +371,34 @@ impl Editor {
let buffer = self.buffer.clone();
let multi_buffer_snapshot = buffer.read(cx).snapshot(cx);
let hunk_range = hunk.multi_buffer_range.clone();
let (diff_base_buffer, deleted_text_lines) = buffer.update(cx, |buffer, cx| {
let buffer = buffer.buffer(hunk_range.start.buffer_id?)?;
let diff_base_buffer = diff_base_buffer
.or_else(|| self.current_diff_base_buffer(&buffer, cx))
.or_else(|| create_diff_base_buffer(&buffer, cx))?;
let deleted_text_lines = buffer.read(cx).diff_base().map(|diff_base| {
let diff_start_row = diff_base
.offset_to_point(hunk.diff_base_byte_range.start)
.row;
let diff_end_row = diff_base.offset_to_point(hunk.diff_base_byte_range.end).row;
diff_end_row - diff_start_row
})?;
Some((diff_base_buffer, deleted_text_lines))
let buffer_id = hunk_range.start.buffer_id?;
let diff_base_buffer = diff_base_buffer.or_else(|| {
self.diff_map
.diff_bases
.get(&buffer_id)?
.change_set
.read(cx)
.base_text
.clone()
})?;
let block_insert_index = match self.expanded_hunks.hunks.binary_search_by(|probe| {
probe
.hunk_range
.start
.cmp(&hunk_range.start, &multi_buffer_snapshot)
}) {
Ok(_already_present) => return None,
Err(ix) => ix,
};
let diff_base = diff_base_buffer.read(cx);
let diff_start_row = diff_base
.offset_to_point(hunk.diff_base_byte_range.start)
.row;
let diff_end_row = diff_base.offset_to_point(hunk.diff_base_byte_range.end).row;
let deleted_text_lines = diff_end_row - diff_start_row;
let block_insert_index = self
.diff_map
.hunks
.binary_search_by(|probe| {
probe
.hunk_range
.start
.cmp(&hunk_range.start, &multi_buffer_snapshot)
})
.err()?;
let blocks;
match hunk.status {
@@ -315,7 +438,7 @@ impl Editor {
);
}
};
self.expanded_hunks.hunks.insert(
self.diff_map.hunks.insert(
block_insert_index,
ExpandedHunk {
blocks,
@@ -374,8 +497,8 @@ impl Editor {
_: &ApplyDiffHunk,
cx: &mut ViewContext<Self>,
) {
let snapshot = self.buffer.read(cx).snapshot(cx);
let hunks = hunks_for_selections(&snapshot, &self.selections.disjoint_anchors());
let snapshot = self.snapshot(cx);
let hunks = hunks_for_selections(&snapshot, &self.selections.all(cx));
let mut ranges_by_buffer = HashMap::default();
self.transact(cx, |editor, cx| {
for hunk in hunks {
@@ -399,6 +522,12 @@ impl Editor {
}
}
fn has_multiple_hunks(&self, cx: &AppContext) -> bool {
let snapshot = self.buffer.read(cx).snapshot(cx);
let mut hunks = self.diff_map.snapshot.diff_hunks(&snapshot);
hunks.nth(1).is_some()
}
fn hunk_header_block(
&self,
hunk: &HoveredHunk,
@@ -409,7 +538,7 @@ impl Editor {
.read(cx)
.point_to_buffer_offset(hunk.multi_buffer_range.start, cx)
.map_or(false, |(buffer, _, _)| {
buffer.read(cx).diff_base_buffer().is_some()
buffer.read(cx).base_buffer().is_some()
});
let border_color = cx.theme().colors().border_variant;
@@ -428,6 +557,7 @@ impl Editor {
render: Arc::new({
let editor = cx.view().clone();
let hunk = hunk.clone();
let has_multiple_hunks = self.has_multiple_hunks(cx);
move |cx| {
let hunk_controls_menu_handle =
@@ -471,6 +601,7 @@ impl Editor {
IconButton::new("next-hunk", IconName::ArrowDown)
.shape(IconButtonShape::Square)
.icon_size(IconSize::Small)
.disabled(!has_multiple_hunks)
.tooltip({
let focus_handle = editor.focus_handle(cx);
move |cx| {
@@ -499,6 +630,7 @@ impl Editor {
IconButton::new("prev-hunk", IconName::ArrowUp)
.shape(IconButtonShape::Square)
.icon_size(IconSize::Small)
.disabled(!has_multiple_hunks)
.tooltip({
let focus_handle = editor.focus_handle(cx);
move |cx| {
@@ -543,29 +675,9 @@ impl Editor {
let editor = editor.clone();
let hunk = hunk.clone();
move |_event, cx| {
let multi_buffer =
editor.read(cx).buffer().clone();
let multi_buffer_snapshot =
multi_buffer.read(cx).snapshot(cx);
let mut revert_changes = HashMap::default();
if let Some(hunk) =
crate::hunk_diff::to_diff_hunk(
&hunk,
&multi_buffer_snapshot,
)
{
Editor::prepare_revert_change(
&mut revert_changes,
&multi_buffer,
&hunk,
cx,
);
}
if !revert_changes.is_empty() {
editor.update(cx, |editor, cx| {
editor.revert(revert_changes, cx)
});
}
editor.update(cx, |editor, cx| {
editor.revert_hunk(hunk.clone(), cx);
});
}
}),
)
@@ -754,13 +866,13 @@ impl Editor {
}
pub(super) fn clear_expanded_diff_hunks(&mut self, cx: &mut ViewContext<'_, Editor>) -> bool {
if self.expanded_hunks.expand_all {
if self.diff_map.expand_all {
return false;
}
self.expanded_hunks.hunk_update_tasks.clear();
self.diff_map.hunk_update_tasks.clear();
self.clear_row_highlights::<DiffRowHighlight>();
let to_remove = self
.expanded_hunks
.diff_map
.hunks
.drain(..)
.flat_map(|expanded_hunk| expanded_hunk.blocks.into_iter())
@@ -774,48 +886,39 @@ impl Editor {
}
pub(super) fn sync_expanded_diff_hunks(
&mut self,
buffer: Model<Buffer>,
diff_map: &mut DiffMap,
buffer_id: BufferId,
cx: &mut ViewContext<'_, Self>,
) {
let buffer_id = buffer.read(cx).remote_id();
let buffer_diff_base_version = buffer.read(cx).diff_base_version();
self.expanded_hunks
.hunk_update_tasks
.remove(&Some(buffer_id));
let diff_base_buffer = self.current_diff_base_buffer(&buffer, cx);
let diff_base_state = diff_map.diff_bases.get_mut(&buffer_id);
let mut diff_base_buffer = None;
let mut diff_base_buffer_unchanged = true;
if let Some(diff_base_state) = diff_base_state {
diff_base_state.change_set.update(cx, |change_set, _| {
if diff_base_state.last_version != Some(change_set.base_text_version) {
diff_base_state.last_version = Some(change_set.base_text_version);
diff_base_buffer_unchanged = false;
}
diff_base_buffer = change_set.base_text.clone();
})
}
diff_map.hunk_update_tasks.remove(&Some(buffer_id));
let new_sync_task = cx.spawn(move |editor, mut cx| async move {
let diff_base_buffer_unchanged = diff_base_buffer.is_some();
let Ok(diff_base_buffer) =
cx.update(|cx| diff_base_buffer.or_else(|| create_diff_base_buffer(&buffer, cx)))
else {
return;
};
editor
.update(&mut cx, |editor, cx| {
if let Some(diff_base_buffer) = &diff_base_buffer {
editor.expanded_hunks.diff_base.insert(
buffer_id,
DiffBaseBuffer {
buffer: diff_base_buffer.clone(),
diff_base_version: buffer_diff_base_version,
},
);
}
let snapshot = editor.snapshot(cx);
let mut recalculated_hunks = snapshot
.buffer_snapshot
.git_diff_hunks_in_range(MultiBufferRow::MIN..MultiBufferRow::MAX)
.diff_map
.diff_hunks(&snapshot.buffer_snapshot)
.filter(|hunk| hunk.buffer_id == buffer_id)
.fuse()
.peekable();
let mut highlights_to_remove =
Vec::with_capacity(editor.expanded_hunks.hunks.len());
let mut highlights_to_remove = Vec::with_capacity(editor.diff_map.hunks.len());
let mut blocks_to_remove = HashSet::default();
let mut hunks_to_reexpand =
Vec::with_capacity(editor.expanded_hunks.hunks.len());
editor.expanded_hunks.hunks.retain_mut(|expanded_hunk| {
let mut hunks_to_reexpand = Vec::with_capacity(editor.diff_map.hunks.len());
editor.diff_map.hunks.retain_mut(|expanded_hunk| {
if expanded_hunk.hunk_range.start.buffer_id != Some(buffer_id) {
return true;
};
@@ -865,7 +968,7 @@ impl Editor {
> hunk_display_range.end
{
recalculated_hunks.next();
if editor.expanded_hunks.expand_all {
if editor.diff_map.expand_all {
hunks_to_reexpand.push(HoveredHunk {
status,
multi_buffer_range,
@@ -908,7 +1011,7 @@ impl Editor {
retain
});
if editor.expanded_hunks.expand_all {
if editor.diff_map.expand_all {
for hunk in recalculated_hunks {
match diff_hunk_to_display(&hunk, &snapshot) {
DisplayDiffHunk::Folded { .. } => {}
@@ -926,6 +1029,8 @@ impl Editor {
}
}
}
} else {
drop(recalculated_hunks);
}
editor.remove_highlighted_rows::<DiffRowHighlight>(highlights_to_remove, cx);
@@ -940,32 +1045,12 @@ impl Editor {
.ok();
});
self.expanded_hunks.hunk_update_tasks.insert(
diff_map.hunk_update_tasks.insert(
Some(buffer_id),
cx.background_executor().spawn(new_sync_task),
);
}
fn current_diff_base_buffer(
&mut self,
buffer: &Model<Buffer>,
cx: &mut AppContext,
) -> Option<Model<Buffer>> {
buffer.update(cx, |buffer, _| {
match self.expanded_hunks.diff_base.entry(buffer.remote_id()) {
hash_map::Entry::Occupied(o) => {
if o.get().diff_base_version != buffer.diff_base_version() {
o.remove();
None
} else {
Some(o.get().buffer.clone())
}
}
hash_map::Entry::Vacant(_) => None,
}
})
}
fn go_to_subsequent_hunk(&mut self, position: Anchor, cx: &mut ViewContext<Self>) {
let snapshot = self.snapshot(cx);
let position = position.to_point(&snapshot.buffer_snapshot);
@@ -1012,7 +1097,7 @@ impl Editor {
}
}
fn to_diff_hunk(
pub(crate) fn to_diff_hunk(
hovered_hunk: &HoveredHunk,
multi_buffer_snapshot: &MultiBufferSnapshot,
) -> Option<MultiBufferDiffHunk> {
@@ -1034,24 +1119,6 @@ fn to_diff_hunk(
})
}
fn create_diff_base_buffer(buffer: &Model<Buffer>, cx: &mut AppContext) -> Option<Model<Buffer>> {
buffer
.update(cx, |buffer, _| {
let language = buffer.language().cloned();
let diff_base = buffer.diff_base()?.clone();
Some((buffer.line_ending(), diff_base, language))
})
.map(|(line_ending, diff_base, language)| {
cx.new_model(|cx| {
let buffer = Buffer::local_normalized(diff_base, line_ending, cx);
match language {
Some(language) => buffer.with_language(language, cx),
None => buffer,
}
})
})
}
fn added_hunk_color(cx: &AppContext) -> Hsla {
let mut created_color = cx.theme().status().git().created;
created_color.fade_out(0.7);
@@ -1109,51 +1176,27 @@ fn editor_with_deleted_text(
});
})]);
let original_multi_buffer_range = hunk.multi_buffer_range.clone();
let diff_base_range = hunk.diff_base_byte_range.clone();
editor
.register_action::<RevertSelectedHunks>({
let hunk = hunk.clone();
let parent_editor = parent_editor.clone();
move |_, cx| {
parent_editor
.update(cx, |editor, cx| {
let Some((buffer, original_text)) =
editor.buffer().update(cx, |buffer, cx| {
let (_, buffer, _) = buffer.excerpt_containing(
original_multi_buffer_range.start,
cx,
)?;
let original_text =
buffer.read(cx).diff_base()?.slice(diff_base_range.clone());
Some((buffer, Arc::from(original_text.to_string())))
})
else {
return;
};
buffer.update(cx, |buffer, cx| {
buffer.edit(
Some((
original_multi_buffer_range.start.text_anchor
..original_multi_buffer_range.end.text_anchor,
original_text,
)),
None,
cx,
)
});
})
.update(cx, |editor, cx| editor.revert_hunk(hunk.clone(), cx))
.ok();
}
})
.detach();
let hunk = hunk.clone();
editor
.register_action::<ToggleHunkDiff>(move |_, cx| {
parent_editor
.update(cx, |editor, cx| {
editor.toggle_hovered_hunk(&hunk, cx);
})
.ok();
.register_action::<ToggleHunkDiff>({
let hunk = hunk.clone();
move |_, cx| {
parent_editor
.update(cx, |editor, cx| {
editor.toggle_hovered_hunk(&hunk, cx);
})
.ok();
}
})
.detach();
editor
@@ -1263,78 +1306,57 @@ mod tests {
let project = Project::test(fs, [], cx).await;
// buffer has two modified hunks with two rows each
let buffer_1 = project.update(cx, |project, cx| {
project.create_local_buffer(
"
1.zero
1.ONE
1.TWO
1.three
1.FOUR
1.FIVE
1.six
"
.unindent()
.as_str(),
None,
cx,
)
});
buffer_1.update(cx, |buffer, cx| {
buffer.set_diff_base(
Some(
"
1.zero
1.one
1.two
1.three
1.four
1.five
1.six
"
.unindent(),
),
cx,
);
});
let diff_base_1 = "
1.zero
1.one
1.two
1.three
1.four
1.five
1.six
"
.unindent();
let text_1 = "
1.zero
1.ONE
1.TWO
1.three
1.FOUR
1.FIVE
1.six
"
.unindent();
// buffer has a deletion hunk and an insertion hunk
let buffer_2 = project.update(cx, |project, cx| {
project.create_local_buffer(
"
2.zero
2.one
2.two
2.three
2.four
2.five
2.six
"
.unindent()
.as_str(),
None,
cx,
)
});
buffer_2.update(cx, |buffer, cx| {
buffer.set_diff_base(
Some(
"
2.zero
2.one
2.one-and-a-half
2.two
2.three
2.four
2.six
"
.unindent(),
),
cx,
);
});
let diff_base_2 = "
2.zero
2.one
2.one-and-a-half
2.two
2.three
2.four
2.six
"
.unindent();
cx.background_executor.run_until_parked();
let text_2 = "
2.zero
2.one
2.two
2.three
2.four
2.five
2.six
"
.unindent();
let buffer_1 = project.update(cx, |project, cx| {
project.create_local_buffer(text_1.as_str(), None, cx)
});
let buffer_2 = project.update(cx, |project, cx| {
project.create_local_buffer(text_2.as_str(), None, cx)
});
let multibuffer = cx.new_model(|cx| {
let mut multibuffer = MultiBuffer::new(ReadWrite);
@@ -1383,10 +1405,30 @@ mod tests {
multibuffer
});
let snapshot = multibuffer.read_with(cx, |b, cx| b.snapshot(cx));
let editor = cx.add_window(|cx| Editor::for_multibuffer(multibuffer, None, false, cx));
editor
.update(cx, |editor, cx| {
for (buffer, diff_base) in [
(buffer_1.clone(), diff_base_1),
(buffer_2.clone(), diff_base_2),
] {
let change_set = cx.new_model(|cx| {
BufferChangeSet::new_with_base_text(
diff_base.to_string(),
buffer.read(cx).text_snapshot(),
cx,
)
});
editor.diff_map.add_change_set(change_set, cx)
}
})
.unwrap();
cx.background_executor.run_until_parked();
let snapshot = editor.update(cx, |editor, cx| editor.snapshot(cx)).unwrap();
assert_eq!(
snapshot.text(),
snapshot.buffer_snapshot.text(),
"
1.zero
1.ONE
@@ -1429,7 +1471,8 @@ mod tests {
assert_eq!(
snapshot
.git_diff_hunks_in_range(MultiBufferRow(0)..MultiBufferRow(12))
.diff_map
.diff_hunks_in_range(Point::zero()..Point::new(12, 0), &snapshot.buffer_snapshot)
.map(|hunk| (hunk_status(&hunk), hunk.row_range))
.collect::<Vec<_>>(),
&expected,
@@ -1437,7 +1480,11 @@ mod tests {
assert_eq!(
snapshot
.git_diff_hunks_in_range_rev(MultiBufferRow(0)..MultiBufferRow(12))
.diff_map
.diff_hunks_in_range_rev(
Point::zero()..Point::new(12, 0),
&snapshot.buffer_snapshot
)
.map(|hunk| (hunk_status(&hunk), hunk.row_range))
.collect::<Vec<_>>(),
expected

View File

@@ -1258,6 +1258,7 @@ pub mod tests {
use crate::{
scroll::{scroll_amount::ScrollAmount, Autoscroll},
test::editor_lsp_test_context::rust_lang,
ExcerptRange,
};
use futures::StreamExt;
@@ -2274,7 +2275,7 @@ pub mod tests {
let project = Project::test(fs, ["/a".as_ref()], cx).await;
let language_registry = project.read_with(cx, |project, _| project.languages().clone());
language_registry.add(crate::editor_tests::rust_lang());
language_registry.add(rust_lang());
let mut fake_servers = language_registry.register_fake_lsp(
"Rust",
FakeLspAdapter {
@@ -2570,7 +2571,7 @@ pub mod tests {
let project = Project::test(fs, ["/a".as_ref()], cx).await;
let language_registry = project.read_with(cx, |project, _| project.languages().clone());
let language = crate::editor_tests::rust_lang();
let language = rust_lang();
language_registry.add(language);
let mut fake_servers = language_registry.register_fake_lsp(
"Rust",
@@ -2922,7 +2923,7 @@ pub mod tests {
let project = Project::test(fs, ["/a".as_ref()], cx).await;
let language_registry = project.read_with(cx, |project, _| project.languages().clone());
language_registry.add(crate::editor_tests::rust_lang());
language_registry.add(rust_lang());
let mut fake_servers = language_registry.register_fake_lsp(
"Rust",
FakeLspAdapter {
@@ -3153,7 +3154,7 @@ pub mod tests {
let project = Project::test(fs, ["/a".as_ref()], cx).await;
let language_registry = project.read_with(cx, |project, _| project.languages().clone());
language_registry.add(crate::editor_tests::rust_lang());
language_registry.add(rust_lang());
let mut fake_servers = language_registry.register_fake_lsp(
"Rust",
FakeLspAdapter {
@@ -3396,7 +3397,7 @@ pub mod tests {
let project = Project::test(fs, ["/a".as_ref()], cx).await;
let language_registry = project.read_with(cx, |project, _| project.languages().clone());
language_registry.add(crate::editor_tests::rust_lang());
language_registry.add(rust_lang());
let mut fake_servers = language_registry.register_fake_lsp(
"Rust",
FakeLspAdapter {

View File

@@ -22,8 +22,8 @@ use language::{
use lsp::DiagnosticSeverity;
use multi_buffer::AnchorRangeExt;
use project::{
lsp_store::FormatTrigger, project_settings::ProjectSettings, search::SearchQuery, Item as _,
Project, ProjectPath,
lsp_store::FormatTrigger, project_settings::ProjectSettings, search::SearchQuery, Project,
ProjectItem as _, ProjectPath,
};
use rpc::proto::{self, update_view, PeerId};
use settings::Settings;
@@ -47,7 +47,7 @@ use workspace::item::{BreadcrumbText, FollowEvent};
use workspace::{
item::{FollowableItem, Item, ItemEvent, ProjectItem},
searchable::{Direction, SearchEvent, SearchableItem, SearchableItemHandle},
ItemId, ItemNavHistory, Pane, ToolbarItemLocation, ViewId, Workspace, WorkspaceId,
ItemId, ItemNavHistory, ToolbarItemLocation, ViewId, Workspace, WorkspaceId,
};
pub const MAX_TAB_TITLE_LEN: usize = 24;
@@ -665,7 +665,7 @@ impl Item for Editor {
fn for_each_project_item(
&self,
cx: &AppContext,
f: &mut dyn FnMut(EntityId, &dyn project::Item),
f: &mut dyn FnMut(EntityId, &dyn project::ProjectItem),
) {
self.buffer
.read(cx)
@@ -737,7 +737,7 @@ impl Item for Editor {
let buffers = self.buffer().clone().read(cx).all_buffers();
let buffers = buffers
.into_iter()
.map(|handle| handle.read(cx).diff_base_buffer().unwrap_or(handle.clone()))
.map(|handle| handle.read(cx).base_buffer().unwrap_or(handle.clone()))
.collect::<HashSet<_>>();
cx.spawn(|this, mut cx| async move {
if format {
@@ -954,7 +954,7 @@ impl SerializableItem for Editor {
workspace: WeakView<Workspace>,
workspace_id: workspace::WorkspaceId,
item_id: ItemId,
cx: &mut ViewContext<Pane>,
cx: &mut WindowContext,
) -> Task<Result<View<Self>>> {
let serialized_editor = match DB
.get_serialized_editor(item_id, workspace_id)
@@ -989,7 +989,7 @@ impl SerializableItem for Editor {
contents: Some(contents),
language,
..
} => cx.spawn(|pane, mut cx| {
} => cx.spawn(|mut cx| {
let project = project.clone();
async move {
let language = if let Some(language_name) = language {
@@ -1019,7 +1019,7 @@ impl SerializableItem for Editor {
buffer.set_text(contents, cx);
})?;
pane.update(&mut cx, |_, cx| {
cx.update(|cx| {
cx.new_view(|cx| {
let mut editor = Editor::for_buffer(buffer, Some(project), cx);
@@ -1046,7 +1046,7 @@ impl SerializableItem for Editor {
match project_item {
Some(project_item) => {
cx.spawn(|pane, mut cx| async move {
cx.spawn(|mut cx| async move {
let (_, project_item) = project_item.await?;
let buffer = project_item.downcast::<Buffer>().map_err(|_| {
anyhow!("Project item at stored path was not a buffer")
@@ -1073,7 +1073,7 @@ impl SerializableItem for Editor {
})?;
}
pane.update(&mut cx, |_, cx| {
cx.update(|cx| {
cx.new_view(|cx| {
let mut editor = Editor::for_buffer(buffer, Some(project), cx);
@@ -1087,7 +1087,7 @@ impl SerializableItem for Editor {
let open_by_abs_path = workspace.update(cx, |workspace, cx| {
workspace.open_abs_path(abs_path.clone(), false, cx)
});
cx.spawn(|_, mut cx| async move {
cx.spawn(|mut cx| async move {
let editor = open_by_abs_path?.await?.downcast::<Editor>().with_context(|| format!("Failed to downcast to Editor after opening abs path {abs_path:?}"))?;
editor.update(&mut cx, |editor, cx| {
editor.read_scroll_position_from_db(item_id, workspace_id, cx);

View File

@@ -20,8 +20,7 @@ pub enum MenuPosition {
/// Disappears when the position is no longer visible.
PinnedToEditor {
source: multi_buffer::Anchor,
offset_x: Pixels,
offset_y: Pixels,
offset: Point<Pixels>,
},
}
@@ -48,36 +47,22 @@ impl MouseContextMenu {
context_menu: View<ui::ContextMenu>,
cx: &mut ViewContext<Editor>,
) -> Option<Self> {
let context_menu_focus = context_menu.focus_handle(cx);
cx.focus(&context_menu_focus);
let _subscription = cx.subscribe(
&context_menu,
move |editor, _, _event: &DismissEvent, cx| {
editor.mouse_context_menu.take();
if context_menu_focus.contains_focused(cx) {
editor.focus(cx);
}
},
);
let editor_snapshot = editor.snapshot(cx);
let source_point = editor.to_pixel_point(source, &editor_snapshot, cx)?;
let offset = position - source_point;
Some(Self {
position: MenuPosition::PinnedToEditor {
source,
offset_x: offset.x,
offset_y: offset.y,
},
context_menu,
_subscription,
})
let content_origin = editor.last_bounds?.origin
+ Point {
x: editor.gutter_dimensions.width,
y: Pixels(0.0),
};
let source_position = editor.to_pixel_point(source, &editor_snapshot, cx)?;
let menu_position = MenuPosition::PinnedToEditor {
source,
offset: position - (source_position + content_origin),
};
return Some(MouseContextMenu::new(menu_position, context_menu, cx));
}
pub(crate) fn pinned_to_screen(
position: Point<Pixels>,
pub(crate) fn new(
position: MenuPosition,
context_menu: View<ui::ContextMenu>,
cx: &mut ViewContext<Editor>,
) -> Self {
@@ -95,7 +80,7 @@ impl MouseContextMenu {
);
Self {
position: MenuPosition::PinnedToScreen(position),
position,
context_menu,
_subscription,
}
@@ -119,7 +104,7 @@ fn display_ranges<'a>(
pub fn deploy_context_menu(
editor: &mut Editor,
position: Point<Pixels>,
position: Option<Point<Pixels>>,
point: DisplayPoint,
cx: &mut ViewContext<Editor>,
) {
@@ -213,8 +198,18 @@ pub fn deploy_context_menu(
})
};
editor.mouse_context_menu =
MouseContextMenu::pinned_to_editor(editor, source_anchor, position, context_menu, cx);
editor.mouse_context_menu = match position {
Some(position) => {
MouseContextMenu::pinned_to_editor(editor, source_anchor, position, context_menu, cx)
}
None => {
let menu_position = MenuPosition::PinnedToEditor {
source: source_anchor,
offset: editor.character_size(cx),
};
Some(MouseContextMenu::new(menu_position, context_menu, cx))
}
};
cx.notify();
}
@@ -248,7 +243,9 @@ mod tests {
}
"});
cx.editor(|editor, _app| assert!(editor.mouse_context_menu.is_none()));
cx.update_editor(|editor, cx| deploy_context_menu(editor, Default::default(), point, cx));
cx.update_editor(|editor, cx| {
deploy_context_menu(editor, Some(Default::default()), point, cx)
});
cx.assert_editor_state(indoc! {"
fn test() {

View File

@@ -2,7 +2,7 @@
//! in editor given a given motion (e.g. it handles converting a "move left" command into coordinates in editor). It is exposed mostly for use by vim crate.
use super::{Bias, DisplayPoint, DisplaySnapshot, SelectionGoal, ToDisplayPoint};
use crate::{scroll::ScrollAnchor, CharKind, DisplayRow, EditorStyle, RowExt, ToOffset, ToPoint};
use crate::{scroll::ScrollAnchor, CharKind, DisplayRow, EditorStyle, ToOffset, ToPoint};
use gpui::{Pixels, WindowTextSystem};
use language::Point;
use multi_buffer::{MultiBufferRow, MultiBufferSnapshot};
@@ -382,12 +382,12 @@ pub fn end_of_paragraph(
mut count: usize,
) -> DisplayPoint {
let point = display_point.to_point(map);
if point.row == map.max_buffer_row().0 {
if point.row == map.buffer_snapshot.max_row().0 {
return map.max_point();
}
let mut found_non_blank_line = false;
for row in point.row..map.max_buffer_row().next_row().0 {
for row in point.row..=map.buffer_snapshot.max_row().0 {
let blank = map.buffer_snapshot.is_line_blank(MultiBufferRow(row));
if found_non_blank_line && blank {
if count <= 1 {
@@ -488,6 +488,101 @@ pub fn find_boundary_point(
map.clip_point(offset.to_display_point(map), Bias::Right)
}
pub fn find_preceding_boundary_trail(
map: &DisplaySnapshot,
head: DisplayPoint,
mut is_boundary: impl FnMut(char, char) -> bool,
) -> (Option<DisplayPoint>, DisplayPoint) {
let mut offset = head.to_offset(map, Bias::Left);
let mut trail_offset = None;
let mut prev_ch = map.buffer_snapshot.chars_at(offset).next();
let mut forward = map.buffer_snapshot.reversed_chars_at(offset).peekable();
// Skip newlines
while let Some(&ch) = forward.peek() {
if ch == '\n' {
prev_ch = forward.next();
offset -= ch.len_utf8();
trail_offset = Some(offset);
} else {
break;
}
}
// Find the boundary
let start_offset = offset;
for ch in forward {
if let Some(prev_ch) = prev_ch {
if is_boundary(prev_ch, ch) {
if start_offset == offset {
trail_offset = Some(offset);
} else {
break;
}
}
}
offset -= ch.len_utf8();
prev_ch = Some(ch);
}
let trail = trail_offset
.map(|trail_offset: usize| map.clip_point(trail_offset.to_display_point(map), Bias::Left));
(
trail,
map.clip_point(offset.to_display_point(map), Bias::Left),
)
}
/// Finds the location of a boundary
pub fn find_boundary_trail(
map: &DisplaySnapshot,
head: DisplayPoint,
mut is_boundary: impl FnMut(char, char) -> bool,
) -> (Option<DisplayPoint>, DisplayPoint) {
let mut offset = head.to_offset(map, Bias::Right);
let mut trail_offset = None;
let mut prev_ch = map.buffer_snapshot.reversed_chars_at(offset).next();
let mut forward = map.buffer_snapshot.chars_at(offset).peekable();
// Skip newlines
while let Some(&ch) = forward.peek() {
if ch == '\n' {
prev_ch = forward.next();
offset += ch.len_utf8();
trail_offset = Some(offset);
} else {
break;
}
}
// Find the boundary
let start_offset = offset;
for ch in forward {
if let Some(prev_ch) = prev_ch {
if is_boundary(prev_ch, ch) {
if start_offset == offset {
trail_offset = Some(offset);
} else {
break;
}
}
}
offset += ch.len_utf8();
prev_ch = Some(ch);
}
let trail = trail_offset
.map(|trail_offset: usize| map.clip_point(trail_offset.to_display_point(map), Bias::Right));
(
trail,
map.clip_point(offset.to_display_point(map), Bias::Right),
)
}
pub fn find_boundary(
map: &DisplaySnapshot,
from: DisplayPoint,

View File

@@ -4,7 +4,7 @@ use futures::{channel::mpsc, future::join_all};
use gpui::{AppContext, EventEmitter, FocusableView, Model, Render, Subscription, Task, View};
use language::{Buffer, BufferEvent, Capability};
use multi_buffer::{ExcerptRange, MultiBuffer};
use project::Project;
use project::{buffer_store::BufferChangeSet, Project};
use smol::stream::StreamExt;
use std::{any::TypeId, ops::Range, rc::Rc, time::Duration};
use text::ToOffset;
@@ -75,7 +75,7 @@ impl ProposedChangesEditor {
title: title.into(),
buffer_entries: Vec::new(),
recalculate_diffs_tx,
_recalculate_diffs_task: cx.spawn(|_, mut cx| async move {
_recalculate_diffs_task: cx.spawn(|this, mut cx| async move {
let mut buffers_to_diff = HashSet::default();
while let Some(mut recalculate_diff) = recalculate_diffs_rx.next().await {
buffers_to_diff.insert(recalculate_diff.buffer);
@@ -96,12 +96,37 @@ impl ProposedChangesEditor {
}
}
join_all(buffers_to_diff.drain().filter_map(|buffer| {
buffer
.update(&mut cx, |buffer, cx| buffer.recalculate_diff(cx))
.ok()?
}))
.await;
let recalculate_diff_futures = this
.update(&mut cx, |this, cx| {
buffers_to_diff
.drain()
.filter_map(|buffer| {
let buffer = buffer.read(cx);
let base_buffer = buffer.base_buffer()?;
let buffer = buffer.text_snapshot();
let change_set = this.editor.update(cx, |editor, _| {
Some(
editor
.diff_map
.diff_bases
.get(&buffer.remote_id())?
.change_set
.clone(),
)
})?;
Some(change_set.update(cx, |change_set, cx| {
change_set.set_base_text(
base_buffer.read(cx).text(),
buffer,
cx,
)
}))
})
.collect::<Vec<_>>()
})
.ok()?;
join_all(recalculate_diff_futures).await;
}
None
}),
@@ -154,6 +179,7 @@ impl ProposedChangesEditor {
});
let mut buffer_entries = Vec::new();
let mut new_change_sets = Vec::new();
for location in locations {
let branch_buffer;
if let Some(ix) = self
@@ -166,6 +192,15 @@ impl ProposedChangesEditor {
buffer_entries.push(entry);
} else {
branch_buffer = location.buffer.update(cx, |buffer, cx| buffer.branch(cx));
new_change_sets.push(cx.new_model(|cx| {
let mut change_set = BufferChangeSet::new(branch_buffer.read(cx));
let _ = change_set.set_base_text(
location.buffer.read(cx).text(),
branch_buffer.read(cx).text_snapshot(),
cx,
);
change_set
}));
buffer_entries.push(BufferEntry {
branch: branch_buffer.clone(),
base: location.buffer.clone(),
@@ -187,7 +222,10 @@ impl ProposedChangesEditor {
self.buffer_entries = buffer_entries;
self.editor.update(cx, |editor, cx| {
editor.change_selections(None, cx, |selections| selections.refresh())
editor.change_selections(None, cx, |selections| selections.refresh());
for change_set in new_change_sets {
editor.diff_map.add_change_set(change_set, cx)
}
});
}
@@ -217,14 +255,14 @@ impl ProposedChangesEditor {
})
.ok();
}
BufferEvent::DiffBaseChanged => {
self.recalculate_diffs_tx
.unbounded_send(RecalculateDiff {
buffer,
debounce: false,
})
.ok();
}
// BufferEvent::DiffBaseChanged => {
// self.recalculate_diffs_tx
// .unbounded_send(RecalculateDiff {
// buffer,
// debounce: false,
// })
// .ok();
// }
_ => (),
}
}
@@ -373,7 +411,7 @@ impl BranchBufferSemanticsProvider {
positions: &[text::Anchor],
cx: &AppContext,
) -> Option<Model<Buffer>> {
let base_buffer = buffer.read(cx).diff_base_buffer()?;
let base_buffer = buffer.read(cx).base_buffer()?;
let version = base_buffer.read(cx).version();
if positions
.iter()

View File

@@ -31,6 +31,47 @@ pub struct EditorLspTestContext {
pub buffer_lsp_url: lsp::Url,
}
pub(crate) fn rust_lang() -> Arc<Language> {
let language = Language::new(
LanguageConfig {
name: "Rust".into(),
matcher: LanguageMatcher {
path_suffixes: vec!["rs".to_string()],
..Default::default()
},
line_comments: vec!["// ".into(), "/// ".into(), "//! ".into()],
..Default::default()
},
Some(tree_sitter_rust::LANGUAGE.into()),
)
.with_queries(LanguageQueries {
indents: Some(Cow::from(indoc! {r#"
[
((where_clause) _ @end)
(field_expression)
(call_expression)
(assignment_expression)
(let_declaration)
(let_chain)
(await_expression)
] @indent
(_ "[" "]" @end) @indent
(_ "<" ">" @end) @indent
(_ "{" "}" @end) @indent
(_ "(" ")" @end) @indent"#})),
brackets: Some(Cow::from(indoc! {r#"
("(" @open ")" @close)
("[" @open "]" @close)
("{" @open "}" @close)
("<" @open ">" @close)
("\"" @open "\"" @close)
(closure_parameters "|" @open "|" @close)"#})),
..Default::default()
})
.expect("Could not parse queries");
Arc::new(language)
}
impl EditorLspTestContext {
pub async fn new(
language: Language,
@@ -72,7 +113,15 @@ impl EditorLspTestContext {
app_state
.fs
.as_fake()
.insert_tree(root, json!({ "dir": { file_name.clone(): "" }}))
.insert_tree(
root,
json!({
".git": {},
"dir": {
file_name.clone(): ""
}
}),
)
.await;
let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
@@ -119,46 +168,7 @@ impl EditorLspTestContext {
capabilities: lsp::ServerCapabilities,
cx: &mut gpui::TestAppContext,
) -> EditorLspTestContext {
let language = Language::new(
LanguageConfig {
name: "Rust".into(),
matcher: LanguageMatcher {
path_suffixes: vec!["rs".to_string()],
..Default::default()
},
line_comments: vec!["// ".into(), "/// ".into(), "//! ".into()],
..Default::default()
},
Some(tree_sitter_rust::LANGUAGE.into()),
)
.with_queries(LanguageQueries {
indents: Some(Cow::from(indoc! {r#"
[
((where_clause) _ @end)
(field_expression)
(call_expression)
(assignment_expression)
(let_declaration)
(let_chain)
(await_expression)
] @indent
(_ "[" "]" @end) @indent
(_ "<" ">" @end) @indent
(_ "{" "}" @end) @indent
(_ "(" ")" @end) @indent"#})),
brackets: Some(Cow::from(indoc! {r#"
("(" @open ")" @close)
("[" @open "]" @close)
("{" @open "}" @close)
("<" @open ">" @close)
("\"" @open "\"" @close)
(closure_parameters "|" @open "|" @close)"#})),
..Default::default()
})
.expect("Could not parse queries");
Self::new(language, capabilities, cx).await
Self::new(Arc::into_inner(rust_lang()).unwrap(), capabilities, cx).await
}
pub async fn new_typescript(

View File

@@ -42,16 +42,16 @@ pub struct EditorTestContext {
impl EditorTestContext {
pub async fn new(cx: &mut gpui::TestAppContext) -> EditorTestContext {
let fs = FakeFs::new(cx.executor());
// fs.insert_file("/file", "".to_owned()).await;
let root = Self::root_path();
fs.insert_tree(
root,
serde_json::json!({
".git": {},
"file": "",
}),
)
.await;
let project = Project::test(fs, [root], cx).await;
let project = Project::test(fs.clone(), [root], cx).await;
let buffer = project
.update(cx, |project, cx| {
project.open_local_buffer(root.join("file"), cx)
@@ -65,6 +65,8 @@ impl EditorTestContext {
editor
});
let editor_view = editor.root_view(cx).unwrap();
cx.run_until_parked();
Self {
cx: VisualTestContext::from_window(*editor.deref(), cx),
window: editor.into(),
@@ -276,8 +278,16 @@ impl EditorTestContext {
snapshot.anchor_before(ranges[0].start)..snapshot.anchor_after(ranges[0].end)
}
pub fn set_diff_base(&mut self, diff_base: Option<&str>) {
self.update_buffer(|buffer, cx| buffer.set_diff_base(diff_base.map(ToOwned::to_owned), cx));
pub fn set_diff_base(&mut self, diff_base: &str) {
self.cx.run_until_parked();
let fs = self
.update_editor(|editor, cx| editor.project.as_ref().unwrap().read(cx).fs().as_fake());
let path = self.update_buffer(|buffer, _| buffer.file().unwrap().path().clone());
fs.set_index_for_repo(
&Self::root_path().join(".git"),
&[(path.as_ref(), diff_base.to_string())],
);
self.cx.run_until_parked();
}
/// Change the editor's text and selections using a string containing
@@ -319,10 +329,12 @@ impl EditorTestContext {
state_context
}
/// Assert about the text of the editor, the selections, and the expanded
/// diff hunks.
///
/// Diff hunks are indicated by lines starting with `+` and `-`.
#[track_caller]
pub fn assert_diff_hunks(&mut self, expected_diff: String) {
// Normalize the expected diff. If it has no diff markers, then insert blank markers
// before each line. Strip any whitespace-only lines.
pub fn assert_state_with_diff(&mut self, expected_diff: String) {
let has_diff_markers = expected_diff
.lines()
.any(|line| line.starts_with("+") || line.starts_with("-"));
@@ -340,11 +352,14 @@ impl EditorTestContext {
})
.join("\n");
let actual_selections = self.editor_selections();
let actual_marked_text =
generate_marked_text(&self.buffer_text(), &actual_selections, true);
// Read the actual diff from the editor's row highlights and block
// decorations.
let actual_diff = self.editor.update(&mut self.cx, |editor, cx| {
let snapshot = editor.snapshot(cx);
let text = editor.text(cx);
let insertions = editor
.highlighted_rows::<DiffRowHighlight>()
.map(|(range, _)| {
@@ -354,7 +369,7 @@ impl EditorTestContext {
})
.collect::<Vec<_>>();
let deletions = editor
.expanded_hunks
.diff_map
.hunks
.iter()
.filter_map(|hunk| {
@@ -371,10 +386,20 @@ impl EditorTestContext {
.read(cx)
.excerpt_containing(hunk.hunk_range.start, cx)
.expect("no excerpt for expanded buffer's hunk start");
let deleted_text = buffer
.read(cx)
.diff_base()
let buffer_id = buffer.read(cx).remote_id();
let change_set = &editor
.diff_map
.diff_bases
.get(&buffer_id)
.expect("should have a diff base for expanded hunk")
.change_set;
let deleted_text = change_set
.read(cx)
.base_text
.as_ref()
.expect("no base text for expanded hunk")
.read(cx)
.as_rope()
.slice(hunk.diff_base_byte_range.clone())
.to_string();
if let DiffHunkStatus::Modified | DiffHunkStatus::Removed = hunk.status {
@@ -384,7 +409,7 @@ impl EditorTestContext {
}
})
.collect::<Vec<_>>();
format_diff(text, deletions, insertions)
format_diff(actual_marked_text, deletions, insertions)
});
pretty_assertions::assert_eq!(actual_diff, expected_diff_text, "unexpected diff state");

View File

@@ -159,6 +159,7 @@ pub trait ExtensionLanguageProxy: Send + Sync + 'static {
language: LanguageName,
grammar: Option<Arc<str>>,
matcher: LanguageMatcher,
hidden: bool,
load: Arc<dyn Fn() -> Result<LoadedLanguage> + Send + Sync + 'static>,
);
@@ -175,13 +176,14 @@ impl ExtensionLanguageProxy for ExtensionHostProxy {
language: LanguageName,
grammar: Option<Arc<str>>,
matcher: LanguageMatcher,
hidden: bool,
load: Arc<dyn Fn() -> Result<LoadedLanguage> + Send + Sync + 'static>,
) {
let Some(proxy) = self.language_proxy.read().clone() else {
return;
};
proxy.register_language(language, grammar, matcher, load)
proxy.register_language(language, grammar, matcher, hidden, load)
}
fn remove_languages(

View File

@@ -162,6 +162,7 @@ pub struct ExtensionIndexLanguageEntry {
pub extension: Arc<str>,
pub path: PathBuf,
pub matcher: LanguageMatcher,
pub hidden: bool,
pub grammar: Option<Arc<str>>,
}
@@ -1097,6 +1098,7 @@ impl ExtensionStore {
language_name.clone(),
language.grammar.clone(),
language.matcher.clone(),
language.hidden,
Arc::new(move || {
let config = std::fs::read_to_string(language_path.join("config.toml"))?;
let config: LanguageConfig = ::toml::from_str(&config)?;
@@ -1324,6 +1326,7 @@ impl ExtensionStore {
extension: extension_id.clone(),
path: relative_path,
matcher: config.matcher,
hidden: config.hidden,
grammar: config.grammar,
},
);

View File

@@ -203,6 +203,7 @@ async fn test_extension_store(cx: &mut TestAppContext) {
extension: "zed-ruby".into(),
path: "languages/erb".into(),
grammar: Some("embedded_template".into()),
hidden: false,
matcher: LanguageMatcher {
path_suffixes: vec!["erb".into()],
first_line_pattern: None,
@@ -215,6 +216,7 @@ async fn test_extension_store(cx: &mut TestAppContext) {
extension: "zed-ruby".into(),
path: "languages/ruby".into(),
grammar: Some("ruby".into()),
hidden: false,
matcher: LanguageMatcher {
path_suffixes: vec!["rb".into()],
first_line_pattern: None,

View File

@@ -156,6 +156,7 @@ impl HeadlessExtensionStore {
config.name.clone(),
None,
config.matcher.clone(),
config.hidden,
Arc::new(move || {
Ok(LoadedLanguage {
config: config.clone(),

View File

@@ -14,7 +14,7 @@ use editor::{Editor, EditorElement, EditorStyle};
use extension_host::{ExtensionManifest, ExtensionOperation, ExtensionStore};
use fuzzy::{match_strings, StringMatchCandidate};
use gpui::{
actions, uniform_list, Action, AppContext, EventEmitter, Flatten, FocusableView,
actions, uniform_list, Action, AppContext, ClipboardItem, EventEmitter, Flatten, FocusableView,
InteractiveElement, KeyContext, ParentElement, Render, Styled, Task, TextStyle,
UniformListScrollHandle, View, ViewContext, VisualContext, WeakView, WindowContext,
};
@@ -637,13 +637,21 @@ impl ExtensionsPage {
cx: &mut WindowContext,
) -> View<ContextMenu> {
let context_menu = ContextMenu::build(cx, |context_menu, cx| {
context_menu.entry(
"Install Another Version...",
None,
cx.handler_for(this, move |this, cx| {
this.show_extension_version_list(extension_id.clone(), cx)
}),
)
context_menu
.entry(
"Install Another Version...",
None,
cx.handler_for(this, {
let extension_id = extension_id.clone();
move |this, cx| this.show_extension_version_list(extension_id.clone(), cx)
}),
)
.entry("Copy Extension ID", None, {
let extension_id = extension_id.clone();
move |cx| {
cx.write_to_clipboard(ClipboardItem::new_string(extension_id.to_string()));
}
})
});
context_menu

View File

@@ -1,7 +1,7 @@
#[cfg(test)]
mod file_finder_tests;
mod file_finder_settings;
pub mod file_finder_settings;
mod new_path_prompt;
mod open_path_prompt;
@@ -648,7 +648,7 @@ impl FileFinderDelegate {
cx.subscribe(project, |file_finder, _, event, cx| {
match event {
project::Event::WorktreeUpdatedEntries(_, _)
| project::Event::WorktreeAdded
| project::Event::WorktreeAdded(_)
| project::Event::WorktreeRemoved(_) => file_finder
.picker
.update(cx, |picker, cx| picker.refresh(cx)),

View File

@@ -132,7 +132,7 @@ pub trait Fs: Send + Sync {
async fn is_case_sensitive(&self) -> Result<bool>;
#[cfg(any(test, feature = "test-support"))]
fn as_fake(&self) -> &FakeFs {
fn as_fake(&self) -> Arc<FakeFs> {
panic!("called as_fake on a real fs");
}
}
@@ -452,18 +452,16 @@ impl Fs for RealFs {
#[cfg(target_os = "windows")]
async fn trash_file(&self, path: &Path, _options: RemoveOptions) -> Result<()> {
use util::paths::SanitizedPath;
use windows::{
core::HSTRING,
Storage::{StorageDeleteOption, StorageFile},
};
// todo(windows)
// When new version of `windows-rs` release, make this operation `async`
let path = path.canonicalize()?.to_string_lossy().to_string();
let path_str = path.trim_start_matches("\\\\?\\");
if path_str.is_empty() {
anyhow::bail!("File path is empty!");
}
let file = StorageFile::GetFileFromPathAsync(&HSTRING::from(path_str))?.get()?;
let path = SanitizedPath::from(path.canonicalize()?);
let path_string = path.to_string();
let file = StorageFile::GetFileFromPathAsync(&HSTRING::from(path_string))?.get()?;
file.DeleteAsync(StorageDeleteOption::Default)?.get()?;
Ok(())
}
@@ -480,19 +478,17 @@ impl Fs for RealFs {
#[cfg(target_os = "windows")]
async fn trash_dir(&self, path: &Path, _options: RemoveOptions) -> Result<()> {
use util::paths::SanitizedPath;
use windows::{
core::HSTRING,
Storage::{StorageDeleteOption, StorageFolder},
};
let path = path.canonicalize()?.to_string_lossy().to_string();
let path_str = path.trim_start_matches("\\\\?\\");
if path_str.is_empty() {
anyhow::bail!("Folder path is empty!");
}
// todo(windows)
// When new version of `windows-rs` release, make this operation `async`
let folder = StorageFolder::GetFolderFromPathAsync(&HSTRING::from(path_str))?.get()?;
let path = SanitizedPath::from(path.canonicalize()?);
let path_string = path.to_string();
let folder = StorageFolder::GetFolderFromPathAsync(&HSTRING::from(path_string))?.get()?;
folder.DeleteAsync(StorageDeleteOption::Default)?.get()?;
Ok(())
}
@@ -844,6 +840,7 @@ impl Watcher for RealWatcher {
#[cfg(any(test, feature = "test-support"))]
pub struct FakeFs {
this: std::sync::Weak<Self>,
// Use an unfair lock to ensure tests are deterministic.
state: Mutex<FakeFsState>,
executor: gpui::BackgroundExecutor,
@@ -1026,7 +1023,8 @@ impl FakeFs {
pub fn new(executor: gpui::BackgroundExecutor) -> Arc<Self> {
let (tx, mut rx) = smol::channel::bounded::<PathBuf>(10);
let this = Arc::new(Self {
let this = Arc::new_cyclic(|this| Self {
this: this.clone(),
executor: executor.clone(),
state: Mutex::new(FakeFsState {
root: Arc::new(Mutex::new(FakeFsEntry::Dir {
@@ -1478,7 +1476,8 @@ struct FakeHandle {
#[cfg(any(test, feature = "test-support"))]
impl FileHandle for FakeHandle {
fn current_path(&self, fs: &Arc<dyn Fs>) -> Result<PathBuf> {
let state = fs.as_fake().state.lock();
let fs = fs.as_fake();
let state = fs.state.lock();
let Some(target) = state.moves.get(&self.inode) else {
anyhow::bail!("fake fd not moved")
};
@@ -1974,8 +1973,8 @@ impl Fs for FakeFs {
}
#[cfg(any(test, feature = "test-support"))]
fn as_fake(&self) -> &FakeFs {
self
fn as_fake(&self) -> Arc<FakeFs> {
self.this.upgrade().unwrap()
}
}

View File

@@ -14,7 +14,6 @@ path = "src/git.rs"
[dependencies]
anyhow.workspace = true
async-trait.workspace = true
clock.workspace = true
collections.workspace = true
derive_more.workspace = true
git2.workspace = true

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