Compare commits

..

171 Commits

Author SHA1 Message Date
mgsloan@gmail.com
4c43ec918e A little bit more progress 2024-12-13 00:00:44 -07:00
mgsloan@gmail.com
1e61989790 Progress! 2024-12-12 23:21:59 -07:00
Nathan Sobo
c3e5bc767a WIP: 177 errors left in workspace 2024-12-12 16:53:41 -07:00
Nathan Sobo
cf8c356d27 WIP 2024-12-12 16:00:14 -07:00
Nathan Sobo
3da7e7195e WIP 2024-12-12 15:29:33 -07:00
Nathan Sobo
10f88c841c WIP 2024-12-12 14:03:05 -07:00
Nathan Sobo
eb4f81abd8 WIP 2024-12-12 13:50:31 -07:00
Nathan Sobo
cd8fdeb15b WIP 2024-12-12 13:02:27 -07:00
Nathan Sobo
72d264063f WIP 2024-12-12 11:00:50 -07:00
Nathan Sobo
77e2816265 WIP 2024-12-12 09:46:42 -07:00
mgsloan@gmail.com
49cc0b8d68 Progress! 2024-12-12 04:08:08 -07:00
Nathan Sobo
7344dbbe2d Just kidding, workspace isn't compiling 2024-12-12 00:03:31 -07:00
Nathan Sobo
c345532126 WIP 2024-12-11 22:13:15 -07:00
Nathan Sobo
67870c8d53 The workspace crate compiles! 2024-12-11 22:08:01 -07:00
Nathan Sobo
edbb526401 WIP 2024-12-11 21:19:33 -07:00
Nathan Sobo
5987d8b78d WIP 2024-12-11 21:12:43 -07:00
Nathan Sobo
933157bae8 WIP: Now just workspace (more after that I know) 2024-12-11 18:06:23 -07:00
Nathan Sobo
d4dc607ac2 WIP: remote_server compiles 2024-12-11 18:00:03 -07:00
Nathan Sobo
fdb8e92178 WIP: Call crate compiles 2024-12-11 17:38:00 -07:00
Nathan Sobo
8d61c42a8f WIP: project crate compiles 2024-12-11 17:27:28 -07:00
Nathan Sobo
d0984600f7 WIP 2024-12-11 16:06:07 -07:00
Nathan Sobo
be776138b3 WIP 2024-12-11 09:00:41 -07:00
Nathan Sobo
0240f32099 WIP 2024-12-10 16:35:56 -07:00
Nathan Sobo
7ac00f30fa WIP 2024-12-10 09:59:25 -07:00
Nathan Sobo
282ad91336 WIP 2024-12-09 20:20:03 -07:00
Nathan Sobo
857000f535 WIP 2024-12-09 13:07:13 -07:00
Nathan Sobo
6d36e6f27a WIP: Remove ViewContext 2024-12-08 22:08:11 -07:00
Nathan Sobo
33fbfb83e5 WIP: Eliminate ModelContext usages 2024-12-08 21:34:34 -07:00
Nathan Sobo
299852cc7d Not compiling, removed WindowContext references 2024-12-08 21:19:06 -07:00
Nathan Sobo
7db32fbdf4 Merge remote-tracking branch 'origin/main' into gpui4 2024-12-08 13:54:54 -07:00
Nathan Sobo
54d59981da Remove *3 crates 2024-12-08 13:28:22 -07:00
Nathan Sobo
714af18afc Copy 3 versions of crates back to original 2024-12-08 13:24:33 -07:00
Nathan Sobo
906a2d9b9b Eliminate ModelContext 2024-12-08 13:21:42 -07:00
Cole Miller
ac07b9197a gpui: Don't panic on failing to set X11 cursor style (#21689)
One more panic (well, two) that should be a `log_err`.

Release Notes:

- N/A
2024-12-08 13:30:23 -05:00
Michael Sloan
4b93a5ca44 Make completions selector continue to show docs aside if ever shown (#21704)
In #21286, documentation fetch was made more efficient by only
fetching the current completion. This has a side effect of causing the
aside to disappear and reappear when navigating the list. This is
particularly jarring when there isn't enough space for the aside,
causing the completions list to jump to the left.

The solution here is to continue to show the aside even if the current
selection does not yet have docs fetched.

Release Notes:

- N/A
2024-12-08 09:44:48 -07:00
Cole Miller
c5b6d78d5b project_diff: Keep going after failing to rescan a buffer (#21673)
I ran into a case locally where the project diff view was unexpectedly
empty because the first file to be scanned wasn't valid UTF-8, and the
inmost loop in `schedule_worktree_rescan` currently breaks when any
loading task fails. It seems like it might make more sense to continue
with the rest of the buffers in this case and also when
`Project::open_unstaged_changes` fails. I've left the error handling for
`update` as-is.

Release Notes:

- Fix project diff view missing files
2024-12-07 12:56:52 -05:00
Danilo Leal
eb3d3eaebf Adjust diagnostic in tabs behavior (#21671)
Follow up to https://github.com/zed-industries/zed/pull/21637

After discussing about this feature with the team, we've decided that
diagnostic display in tabs should be: 1) turned off by default, and 2)
only shown when there are file icons. The main reason here being to keep
Zed's UI uncluttered.

This means that you can technically have this setting:

```
  "tabs": {
    "show_diagnostics": "all"
  },
```

...and still don't see any diagnostics because you're missing
`file_icons": true`.

| Error with file icons | Error with no file icons |
|--------|--------|
| <img width="800" alt="Screenshot 2024-12-06 at 21 05 13"
src="https://github.com/user-attachments/assets/babf9cc3-b3b0-492e-9748-3e97d96ce90e">
| <img width="800" alt="Screenshot 2024-12-06 at 21 05 24"
src="https://github.com/user-attachments/assets/5247a5f1-55a0-4c56-8aaf-a0cdd115464f">
|


Release Notes:

- N/A
2024-12-07 11:00:31 -03:00
Piotr Osiewicz
fdc7751457 toolchains: Do not use as_json representation for PartialEq (#21682)
Closes #21679

Release Notes:

- N/A
2024-12-07 14:52:55 +01:00
Piotr Osiewicz
f561a91daf lsp: Add support for didRename/willRename LSP messages (#21651)
Closes #21564

Notably, RA will now rename module references if you change the source
file name via our project panel.

This PR is a tad bigger than necessary as I torn out the Model<> from
didSave watchers (I tried to reuse that code for the same purpose).
Release Notes:

- Added support for language server actions being executed on file
rename.
2024-12-07 13:08:18 +01:00
Kirill Bulatov
14ba4a9c94 Fix zoomed terminal pane issues on split (#21668)
Closes https://github.com/zed-industries/zed/issues/21652

* prevents zooming out the panel when any terminal pane is closed
* forces focus on new terminal panes, to prevent the workspace from
getting odd pane events in the background

Release Notes:

- (Preview only) Fixed zoomed terminal pane issues on split
2024-12-07 10:39:01 +02:00
Cole Miller
fa7dddd6b5 gpui: Don't panic when failing to exec system opener (#21674) 2024-12-06 22:11:40 -05:00
Conrad Irwin
4d22a07a1e Remove last few alt- bindings (#21669)
Although I hoped we could keep the non-ascii alt characters, it turns
out this is not the case for all keyboards.

Fixes #21175

Release Notes:

- (breaking change) editor::ShowInlineCompetion is now `option-tab` on
macOS
(not `option-/`). editor::{Next,Previous}Completion are `option-tab` and
  `option-shift-tab` (not `option-[` and `option-]`). This fixes typing
  characters generated by option-{/,[,]} on keyboards like Croatian.
2024-12-06 16:43:12 -07:00
Conrad Irwin
9e287b33e5 Update NorwegianExtended equivalents (#21665)
Release Notes:

- Impoved key equivalents for Norwegian Extended layout
2024-12-06 16:42:58 -07:00
Conrad Irwin
9d44ed0894 Stop overriding cancelOperation (#21667)
This was added before we were handling key equivalents, and is no longer
needed. Furthermore in the gpui2 re-write we stopped sending the correct
modifiers so this hasn't worked for the last year.

Fixes #21520

Release Notes:

- Fixed a bug where cmd-escape could act like .
2024-12-06 16:42:50 -07:00
Matin Aniss
21a6664cf8 gpui: Support animated WebP image (#20778)
Add support for decoding animated WebP images into their individual
frames.

Release Notes:

- N/A
2024-12-06 14:53:27 -08:00
Joseph T. Lyons
e019d1405a Send an event when user changes their max monthly spend limit (#21664)
Release Notes:

- N/A

---------

Co-authored-by: Marshall Bowers <elliott.codes@gmail.com>
2024-12-06 17:35:00 -05:00
feeiyu
e5374f5d7d windows: Ignore WM_SIZE event when minimizing window (#21533)
Closes #21364

Release Notes:

- Fixed minimize window and then reopen cause the layout changed


![layout1204](https://github.com/user-attachments/assets/e823da90-0cc6-4fc9-8b8e-82680357c6fe)
2024-12-06 14:15:04 -08:00
Mikayla Maki
de939e718a Simplify livekit config so that cargo check Just Works (#21661)
Supersedes https://github.com/zed-industries/zed/pull/21653

This enables us to use `cargo test -p workspace` on macOS and Linux.

Note that the line diffs in `shared_screen.rs` are spurious, I just
re-ordered the `macos` and `cross-platform` modules to match the order
in the call crate.

Release Notes:

- N/A
2024-12-06 13:50:59 -08:00
geemili
7d80d1208c vim: Add delete action to HelixNormal mode (#21544)
Related issue: https://github.com/zed-industries/zed/issues/4642

Release-Notes:

* N/A
2024-12-06 14:05:41 -07:00
Conrad Irwin
78ca297282 Make use_key_equivalents opt-in (#21662)
When revamping international keyboard shortcuts I wanted to make the
default to use key equivalents; in hindsight, this is not what people
expect.

Release Notes:

- (Breaking) In keymap.json `"use_layout_keys": true` is now the
default. If you want to opt-out of this behaviour, set
`"use_key_equivalents": true` to have keys mapped for your keyboard. See
[documentation](https://zed.dev/docs/key-bindings#non-qwerty-keyboards)

---------

Co-authored-by: Peter Tripp <peter@zed.dev>
2024-12-06 14:05:03 -07:00
The Bearodactyl
17448f23a6 docs: Add clarification in Windows build instructions (#21659) 2024-12-06 15:19:36 -05:00
Conrad Irwin
e730a9d029 Bump to livekit 1.1.6 (#21660)
Co-Authored-By: Mikayla <mikayla@zed.dev>

This bumps to the latest v1 version of swift SDK. We could bump to 2,
but it
sounds like this will already have some race condition fixes (and a
click
around locally seems less prone to deadlocking so far...)

Release Notes:

- N/A
2024-12-06 12:06:55 -08:00
Marshall Bowers
5142e38d2b editor: Add actions for inserting UUIDs (#21656)
This PR adds two new actions for generating and inserting UUIDs into the
buffer:


https://github.com/user-attachments/assets/a3445a98-07e2-40b8-9773-fd750706cbcc

Release Notes:

- Added `editor: insert uuid v4` and `editor: insert uuid v7` actions
for inserting generated UUIDs into the editor.
2024-12-06 14:32:09 -05:00
Peter Tripp
7a1a7929bd docs: Add x.ai Grok example (#21655)
- Closes https://github.com/zed-industries/zed/issues/21635

<img width="639" alt="Screenshot 2024-12-06 at 13 57 42"
src="https://github.com/user-attachments/assets/a4434edb-3c7d-40c0-9df8-7e928a9307d0">


Release Notes:

- Document support for x.ai Grok
2024-12-06 13:59:40 -05:00
renovate[bot]
0368fff030 Update cloudflare/wrangler-action digest to 6d58852 (#21551)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
|
[cloudflare/wrangler-action](https://redirect.github.com/cloudflare/wrangler-action)
| action | digest | `05f17c4` -> `6d58852` |

---

### 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-06 13:16:53 -05:00
Jax Young
99c31816c9 docs: Correct default values (#20897)
Some default values in the doc are outdated.

Release Notes:

- N/A

---------

Co-authored-by: Marshall Bowers <elliott.codes@gmail.com>
2024-12-06 12:47:05 -05:00
uncenter
feb2d85a13 Add YAML/TOML frontmatter injections for markdown (#21503)
Closes #7938. Adds front-matter injections for TOML/YAML in markdown. 
- See: https://github.com/tree-sitter-grammars/tree-sitter-markdown/blob/split_parser/tree-sitter-markdown/queries/injections.scm.

Co-authored-by: Peter Tripp <peter@zed.dev>
2024-12-06 12:34:15 -05:00
renovate[bot]
d6e11c58db Update Rust crate pathdiff to v0.2.3 (#21568)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [pathdiff](https://redirect.github.com/Manishearth/pathdiff) |
workspace.dependencies | patch | `0.2.2` -> `0.2.3` |

---

### Configuration

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

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

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

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

---

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

---

Release Notes:

- N/A

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

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-06 12:33:33 -05:00
renovate[bot]
8a6c2bb749 Update Rust crate rsa to v0.9.7 (#21570)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [rsa](https://redirect.github.com/RustCrypto/RSA) |
workspace.dependencies | patch | `0.9.6` -> `0.9.7` |

---

### Release Notes

<details>
<summary>RustCrypto/RSA (rsa)</summary>

###
[`v0.9.7`](https://redirect.github.com/RustCrypto/RSA/compare/v0.9.6...v0.9.7)

[Compare
Source](https://redirect.github.com/RustCrypto/RSA/compare/v0.9.6...v0.9.7)

</details>

---

### Configuration

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

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

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

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

---

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

---

Release Notes:

- N/A

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

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-06 12:32:45 -05:00
Bennet Bo Fenner
b4f59284a9 markdown preview: Allow clicking on image to navigate to source location (#21630)
Follow up to #21082

Similar to checkboxes, you can now click on the image to navigate to the
source location, cmd-clicking opens the url in the browser.


https://github.com/user-attachments/assets/edaaa580-9d8f-490b-a4b3-d6ffb21f197c


Release Notes:

- N/A
2024-12-06 18:31:58 +01:00
tims
bffdc55d63 linux: Make prompt detail selectable (#21405)
Closes #21305

As Linux doesn’t have native prompts, Zed uses a custom GPU-based
prompt, like the "About Zed" prompt. Currently, the detail in the prompt
isn’t selectable.

This PR fixes that by using the editor's multi-line selectable
functionality to make the detail selectable (and thus copyable). It
achieves this by disabling editing and setting the cursor to
transparent. The editor also does all the heavy lifting, like
double-clicking to select a word or triple-clicking to select a line,
like what user expects from selectable.

Before/After:

<img
src="https://github.com/user-attachments/assets/2012a6cc-a1ed-4efe-8bfb-440a9259f07a"
alt="before" width="360px" />

<img
src="https://github.com/user-attachments/assets/31922ef5-cb2d-4e90-a1a1-00843e767432"
alt="after" width="360px" />

When detail is `None` or empty string:

<img
src="https://github.com/user-attachments/assets/2be5c921-bda1-4db3-85cd-b4b0e2df86d2"
alt="none" width="360px" />

Release Notes:

- N/A
2024-12-06 09:26:47 -08:00
renovate[bot]
9ca0d99cfd Update Rust crate ctor to v0.2.9 (#21561)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [ctor](https://redirect.github.com/mmastrac/rust-ctor) |
workspace.dependencies | patch | `0.2.8` -> `0.2.9` |

---

### 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-06 12:22:35 -05:00
tims
e5251f4091 Fix incorrect language selected in language selector (#21648)
Due to filtering after enumeration, initial candidate ids are assigned
incorrectly. This later causes the wrong item to be picked up when
accessed via index in the vector.
2024-12-06 12:03:58 -05:00
Cole Miller
304158ed79 Catch panic from oo7 when reading credentials (#21617) 2024-12-06 08:45:03 -05:00
Danilo Leal
e8f0ebc881 Refine diagnostic icons in tabs (#21637)
Follow up to https://github.com/zed-industries/zed/pull/21383

Mostly adjusting the alignment when there are no file icons.

<img width="800" alt="Screenshot 2024-12-06 at 08 35 48"
src="https://github.com/user-attachments/assets/6a4206cc-2af5-4317-a92e-49dffa37de99">

Release Notes:

- N/A
2024-12-06 09:17:48 -03:00
Danilo Leal
7b1d1bf79e Update panel.focused_border token across themes (#21612)
Follow up to https://github.com/zed-industries/zed/pull/21593

This PR updates all built-in themes `panel.focused_border` tokens using
the same HEX code used for `text_accent`.

There shouldn't be any visual change here given the project panel item,
when focused, was using `Color::Selected`, which maps to `text_accent`,
to color its border. In the linked PR above, the project panel item was
updated to use the dedicated token for that. This is good because now
theme markers will be able to customize them separately (e.g., having a
different `text_accent` color than `panel.focused_border`).

Release Notes:

- N/A
2024-12-06 09:17:34 -03:00
Nils Koch
4b16b73f80 Fix panel.background color override (#21559)
Closes #21266

Release Notes:

- Fixes not using the `panel.background` color in the file tree

See comments in https://github.com/zed-industries/zed/issues/21266 for
more details.

Co-authored-by: Danilo Leal <daniloleal09@gmail.com>
2024-12-06 09:17:24 -03:00
Bennet Bo Fenner
7e40addb5f markdown preview: Fix panic when parsing empty image tag (#21616)
Closes #21534

While investigating the panic, I noticed that the code was pretty
complicated and decided to refactor parts of it to reduce redundancy.

Release Notes:

- Fixed an issue where the app could crash when opening the markdown
preview with a malformed image tag
2024-12-06 10:01:57 +01:00
Max Brunsfeld
f6b5e1734e Get unstaged changes when excerpts of new buffers are added (#21619)
This fixes an error on nightly, introduced in
https://github.com/zed-industries/zed/pull/21258, where diffs were not
shown for buffers that were added to multi-buffers after construction.

Release Notes:

- N/A
2024-12-05 16:52:14 -08:00
Mikayla Maki
cf4e847c62 Implement session-global include_warnings in the diagnostic item (#21618)
Release Notes:

- Make the include warnings toggle in the diagnostic tab global for a
zed session.
2024-12-05 16:32:17 -08:00
Nick Breaton
aff17322f3 Detect wider variety of usernames for SSH-based remotes (#21508)
Closes #21507

Release Notes:

- Fixed detection of git remotes when using SSH and username is not
"git".
2024-12-05 15:23:37 -08:00
renovate[bot]
28650b2fac Update Rust crate blake3 to v1.5.5 (#21554)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [blake3](https://redirect.github.com/BLAKE3-team/BLAKE3) |
workspace.dependencies | patch | `1.5.4` -> `1.5.5` |

---

### Release Notes

<details>
<summary>BLAKE3-team/BLAKE3 (blake3)</summary>

###
[`v1.5.5`](https://redirect.github.com/BLAKE3-team/BLAKE3/releases/tag/1.5.5)

[Compare
Source](https://redirect.github.com/BLAKE3-team/BLAKE3/compare/1.5.4...1.5.5)

version 1.5.5

Changes since 1.5.4:

-   `b3sum --check` now supports checkfiles with Windows-style newlines.
    `b3sum` still emits Unix-style newlines, even on Windows, but
    sometimes text editors or version control tools will swap them.
-   The "digest" feature (deleted in v1.5.2) has been added back to the
    `blake3` crate. This is for backwards compatibility only, and it's
    insta-deprecated. All callers should prefer the "traits-preview"
    feature.

</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-05 15:23:08 -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
Nathan Sobo
c244416361 Compiling 2024-12-05 15:51:56 -07: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
Nathan Sobo
5607cf85c5 WIP 2024-12-05 09:58:48 -07: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
Nathan Sobo
b9250d08f9 Compiling with ui3 2024-12-04 19:49:40 -07:00
Nathan Sobo
6a5a9c507c WIP: Converting ui crate. 2 errors left 2024-12-04 19:46:29 -07: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
Nathan Sobo
1a90642088 WIP 2024-12-03 10:09:39 -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
Nathan Sobo
fe59d983eb Cleanup 2024-12-02 09:36:24 -07:00
Nathan Sobo
5ef8df22ac Compiling. 1 warning, no todos 2024-12-01 19:42:24 -07:00
Nathan Sobo
35407dcf8b WIP 2024-12-01 17:04:36 -07:00
Nathan Sobo
1ded8e4b8c WIP
- Updating input handling to use new Model-based architecture
- Refactoring TextInput and InputExample components
2024-12-01 16:59:28 -07:00
Nathan Sobo
dd32313cd0 Implement set_prompt_builder 2024-12-01 15:24:32 -07:00
Nathan Sobo
c55123afa6 Checkpoint 2024-12-01 15:19:41 -07:00
Nathan Sobo
d0cb78d09d Reintroduce Render trait as a requirement for window state 2024-12-01 15:17:24 -07:00
Nathan Sobo
1b82ba050f Reintroduce window state and typed window handles
This time however, the window's state is a model, not a special view.
You provide the window's state and a render function for that state.
2024-12-01 14:51:54 -07:00
Nathan Sobo
864fe2a306 Activate example app 2024-12-01 11:36:59 -07:00
Nathan Sobo
3d53f6da1d Checkpoint: Implement fallback prompt rendering 2024-12-01 09:47:43 -07:00
Nathan Sobo
d7e3b10776 Implement some more todo methods in window 2024-11-30 17:09:47 -07:00
Nathan Sobo
e6eeac8422 Implement some more todo methods in window 2024-11-30 15:22:26 -07:00
Nathan Sobo
6a705fda07 Implement some more todo methods in window 2024-11-30 15:16:54 -07:00
Nathan Sobo
76e6c7f6e3 Implement display 2024-11-30 12:58:45 -07:00
Nathan Sobo
1491ca7dcb Implement appearance_changed 2024-11-30 12:53:07 -07:00
Nathan Sobo
c842988c15 Implement bounds changed 2024-11-30 12:51:35 -07:00
Nathan Sobo
f33adc1a7a Compiling with lots of todos 2024-11-30 12:47:46 -07:00
Nathan Sobo
ad50231829 Accept builder functions in tooltip methods
Before the took render funcitons, but I'm not ready to fight that.
2024-11-30 12:09:26 -07:00
Nathan Sobo
fc152eb459 WIP: Work on rendering tooltips with functions (instead of views) 2024-11-25 19:52:24 -07:00
Nathan Sobo
c404896a32 WIP: Return a render fn from drag listener 2024-11-25 11:41:14 -07:00
Nathan Sobo
34d5f264b4 WIP: Progress toward eliminating window context, view context, views 2024-11-24 23:20:06 -07:00
Nathan Sobo
cf818948dd WIP 2024-11-24 22:41:00 -07:00
Nathan Sobo
1122e106a9 WIP 2024-11-24 22:39:08 -07:00
Nathan Sobo
cb729c4c7f WIP 2024-11-24 22:22:40 -07:00
Nathan Sobo
e28104b2e8 WIP 2024-11-24 22:19:50 -07:00
Nathan Sobo
a7159de184 Checkpoint 2024-11-24 19:43:15 -07:00
Nathan Sobo
3144490fac Change update_window to take a window reference 2024-11-24 19:18:13 -07:00
Nathan Sobo
561660ac07 Compiling with render functions (but lots of todos) 2024-11-24 16:26:35 -07:00
Nathan Sobo
d4d6be3f8d WIP: Give windows a render function instead of a root view 2024-11-24 14:12:23 -07:00
Nathan Sobo
368ff811b9 Clone gpui3 to gpui so I can work on it 2024-11-24 08:38:39 -07:00
661 changed files with 53115 additions and 31166 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,7 +129,9 @@ jobs:
run: |
cargo build --workspace --bins --all-features
cargo check -p gpui --features "macos-blade"
cargo check -p workspace
cargo build -p remote_server
script/check-rust-livekit-macos
linux_tests:
timeout-minutes: 60
@@ -155,8 +163,10 @@ jobs:
- name: Run tests
uses: ./.github/actions/run_tests
- name: Build Zed
run: cargo build -p zed
- name: Build other binaries and features
run: |
cargo build -p zed
cargo check -p workspace
build_remote_server:
timeout-minutes: 60

View File

@@ -37,28 +37,28 @@ jobs:
mdbook build ./docs --dest-dir=../target/deploy/docs/
- name: Deploy Docs
uses: cloudflare/wrangler-action@05f17c4a695b4d94b57b59997562c6a4624c64e4 # v3
uses: cloudflare/wrangler-action@6d58852c35a27e6034745c5d0bc373d739014f7f # v3
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
command: pages deploy target/deploy --project-name=docs
- name: Deploy Install
uses: cloudflare/wrangler-action@05f17c4a695b4d94b57b59997562c6a4624c64e4 # v3
uses: cloudflare/wrangler-action@6d58852c35a27e6034745c5d0bc373d739014f7f # v3
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
command: r2 object put -f script/install.sh zed-open-source-website-assets/install.sh
- name: Deploy Docs Workers
uses: cloudflare/wrangler-action@05f17c4a695b4d94b57b59997562c6a4624c64e4 # v3
uses: cloudflare/wrangler-action@6d58852c35a27e6034745c5d0bc373d739014f7f # v3
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
command: deploy .cloudflare/docs-proxy/src/worker.js
- name: Deploy Install Workers
uses: cloudflare/wrangler-action@05f17c4a695b4d94b57b59997562c6a4624c64e4 # v3
uses: cloudflare/wrangler-action@6d58852c35a27e6034745c5d0bc373d739014f7f # v3
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}

View File

@@ -2,8 +2,7 @@
"languages": {
"Markdown": {
"tab_size": 2,
"formatter": "prettier",
"format_on_save": "on"
"formatter": "prettier"
},
"TOML": {
"formatter": "prettier",

762
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",
@@ -248,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" }
@@ -382,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"
@@ -429,7 +434,7 @@ 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"
@@ -501,7 +506,7 @@ unindent = "0.1.7"
unicode-segmentation = "1.10"
unicode-script = "0.5.7"
url = "2.2"
uuid = { version = "1.1.2", features = ["v4", "v5", "serde"] }
uuid = { version = "1.1.2", features = ["v4", "v5", "v7", "serde"] }
wasmparser = "0.215"
wasm-encoder = "0.215"
wasmtime = { version = "24", default-features = false, features = [
@@ -570,6 +575,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"
@@ -672,6 +681,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,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

@@ -1,3 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8 4L4 12H12L8 4Z" fill="currentColor"/>
<svg width="17" height="16" viewBox="0 0 17 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8.5 3L3 12H14L8.5 3Z" fill="black"/>
</svg>

Before

Width:  |  Height:  |  Size: 155 B

After

Width:  |  Height:  |  Size: 150 B

View File

@@ -1,3 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M4 4L12 12M12 4L4 12" stroke="currentColor" stroke-width="2"/>
<svg width="17" height="16" viewBox="0 0 17 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M5 4.5L12 11.5M12 4.5L5 11.5" stroke="black" stroke-width="2" stroke-linecap="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 177 B

After

Width:  |  Height:  |  Size: 199 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

@@ -1,6 +1,7 @@
[
// Standard macOS bindings
{
"use_key_equivalents": true,
"bindings": {
"up": "menu::SelectPrev",
"shift-tab": "menu::SelectPrev",
@@ -40,6 +41,7 @@
},
{
"context": "Editor",
"use_key_equivalents": true,
"bindings": {
"escape": "editor::Cancel",
"backspace": "editor::Backspace",
@@ -93,8 +95,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",
@@ -133,6 +133,7 @@
},
{
"context": "Editor && mode == full",
"use_key_equivalents": true,
"bindings": {
"enter": "editor::Newline",
"shift-enter": "editor::Newline",
@@ -150,20 +151,23 @@
},
{
"context": "Editor && mode == full && inline_completion",
"use_key_equivalents": true,
"bindings": {
"alt-]": "editor::NextInlineCompletion",
"alt-[": "editor::PreviousInlineCompletion",
"alt-tab": "editor::NextInlineCompletion",
"alt-shift-tab": "editor::PreviousInlineCompletion",
"ctrl-right": "editor::AcceptPartialInlineCompletion"
}
},
{
"context": "Editor && !inline_completion",
"use_key_equivalents": true,
"bindings": {
"alt-\\": "editor::ShowInlineCompletion"
"alt-tab": "editor::ShowInlineCompletion"
}
},
{
"context": "Editor && mode == auto_height",
"use_key_equivalents": true,
"bindings": {
"ctrl-enter": "editor::Newline",
"shift-enter": "editor::Newline",
@@ -172,12 +176,14 @@
},
{
"context": "Markdown",
"use_key_equivalents": true,
"bindings": {
"cmd-c": "markdown::Copy"
}
},
{
"context": "Editor && jupyter && !ContextEditor",
"use_key_equivalents": true,
"bindings": {
"ctrl-shift-enter": "repl::Run",
"ctrl-alt-enter": "repl::RunInPlace"
@@ -185,6 +191,7 @@
},
{
"context": "AssistantPanel",
"use_key_equivalents": true,
"bindings": {
"cmd-k c": "assistant::CopyCode",
"cmd-g": "search::SelectNextMatch",
@@ -197,6 +204,7 @@
},
{
"context": "ContextEditor > Editor",
"use_key_equivalents": true,
"bindings": {
"cmd-enter": "assistant::Assist",
"cmd-shift-enter": "assistant::Edit",
@@ -211,18 +219,22 @@
},
{
"context": "AssistantPanel2",
"use_key_equivalents": true,
"bindings": {
"cmd-n": "assistant2::NewThread"
"cmd-n": "assistant2::NewThread",
"cmd-shift-h": "assistant2::OpenHistory"
}
},
{
"context": "MessageEditor > Editor",
"use_key_equivalents": true,
"bindings": {
"cmd-enter": "assistant2::Chat"
}
},
{
"context": "PromptLibrary",
"use_key_equivalents": true,
"bindings": {
"cmd-n": "prompt_library::NewPrompt",
"cmd-shift-s": "prompt_library::ToggleDefaultPrompt",
@@ -231,6 +243,7 @@
},
{
"context": "BufferSearchBar",
"use_key_equivalents": true,
"bindings": {
"escape": "buffer_search::Dismiss",
"tab": "buffer_search::FocusEditor",
@@ -244,6 +257,7 @@
},
{
"context": "BufferSearchBar && in_replace > Editor",
"use_key_equivalents": true,
"bindings": {
"enter": "search::ReplaceNext",
"cmd-enter": "search::ReplaceAll"
@@ -251,6 +265,7 @@
},
{
"context": "BufferSearchBar && !in_replace > Editor",
"use_key_equivalents": true,
"bindings": {
"up": "search::PreviousHistoryQuery",
"down": "search::NextHistoryQuery"
@@ -258,6 +273,7 @@
},
{
"context": "ProjectSearchBar",
"use_key_equivalents": true,
"bindings": {
"escape": "project_search::ToggleFocus",
"cmd-shift-j": "project_search::ToggleFilters",
@@ -269,6 +285,7 @@
},
{
"context": "ProjectSearchBar > Editor",
"use_key_equivalents": true,
"bindings": {
"up": "search::PreviousHistoryQuery",
"down": "search::NextHistoryQuery"
@@ -276,6 +293,7 @@
},
{
"context": "ProjectSearchBar && in_replace > Editor",
"use_key_equivalents": true,
"bindings": {
"enter": "search::ReplaceNext",
"cmd-enter": "search::ReplaceAll"
@@ -283,6 +301,7 @@
},
{
"context": "ProjectSearchView",
"use_key_equivalents": true,
"bindings": {
"escape": "project_search::ToggleFocus",
"cmd-shift-j": "project_search::ToggleFilters",
@@ -293,6 +312,7 @@
},
{
"context": "Pane",
"use_key_equivalents": true,
"bindings": {
"cmd-{": "pane::ActivatePrevItem",
"cmd-}": "pane::ActivateNextItem",
@@ -321,6 +341,7 @@
// Bindings from VS Code
{
"context": "Editor",
"use_key_equivalents": true,
"bindings": {
"cmd-[": "editor::Outdent",
"cmd-]": "editor::Indent",
@@ -384,6 +405,7 @@
},
{
"context": "Editor && mode == full",
"use_key_equivalents": true,
"bindings": {
"cmd-shift-o": "outline::Toggle",
"ctrl-g": "go_to_line::Toggle"
@@ -391,6 +413,7 @@
},
{
"context": "Pane",
"use_key_equivalents": true,
"bindings": {
"ctrl-1": ["pane::ActivateItem", 0],
"ctrl-2": ["pane::ActivateItem", 1],
@@ -410,6 +433,7 @@
},
{
"context": "Workspace",
"use_key_equivalents": true,
"bindings": {
// Change the default action on `menu::Confirm` by setting the parameter
// "alt-cmd-o": ["projects::OpenRecent", {"create_new_window": true }],
@@ -465,6 +489,7 @@
},
{
"context": "Workspace && !Terminal",
"use_key_equivalents": true,
"bindings": {
"cmd-shift-r": "task::Spawn",
"cmd-alt-r": "task::Rerun",
@@ -475,6 +500,7 @@
// Bindings from Sublime Text
{
"context": "Editor",
"use_key_equivalents": true,
"bindings": {
"ctrl-j": "editor::JoinLines",
"ctrl-alt-backspace": "editor::DeleteToPreviousSubwordStart",
@@ -494,6 +520,7 @@
// Bindings from Atom
{
"context": "Pane",
"use_key_equivalents": true,
"bindings": {
"cmd-k up": "pane::SplitUp",
"cmd-k down": "pane::SplitDown",
@@ -504,12 +531,14 @@
// Bindings that should be unified with bindings for more general actions
{
"context": "Editor && renaming",
"use_key_equivalents": true,
"bindings": {
"enter": "editor::ConfirmRename"
}
},
{
"context": "Editor && showing_completions",
"use_key_equivalents": true,
"bindings": {
"enter": "editor::ConfirmCompletion",
"tab": "editor::ComposeCompletion"
@@ -517,18 +546,21 @@
},
{
"context": "Editor && inline_completion && !showing_completions",
"use_key_equivalents": true,
"bindings": {
"tab": "editor::AcceptInlineCompletion"
}
},
{
"context": "Editor && showing_code_actions",
"use_key_equivalents": true,
"bindings": {
"enter": "editor::ConfirmCodeAction"
}
},
{
"context": "Editor && (showing_code_actions || showing_completions)",
"use_key_equivalents": true,
"bindings": {
"up": "editor::ContextMenuPrev",
"ctrl-p": "editor::ContextMenuPrev",
@@ -540,6 +572,7 @@
},
// Custom bindings
{
"use_key_equivalents": true,
"bindings": {
"ctrl-alt-cmd-f": "workspace::FollowNextCollaborator",
// TODO: Move this to a dock open action
@@ -550,6 +583,7 @@
},
{
"context": "Editor && mode == full",
"use_key_equivalents": true,
"bindings": {
"alt-enter": "editor::OpenExcerpts",
"shift-enter": "editor::ExpandExcerpts",
@@ -561,6 +595,7 @@
},
{
"context": "ProposedChangesEditor",
"use_key_equivalents": true,
"bindings": {
"cmd-shift-y": "editor::ApplyDiffHunk",
"cmd-shift-a": "editor::ApplyAllDiffHunks"
@@ -568,6 +603,7 @@
},
{
"context": "PromptEditor",
"use_key_equivalents": true,
"bindings": {
"ctrl-[": "assistant::CyclePreviousInlineAssist",
"ctrl-]": "assistant::CycleNextInlineAssist"
@@ -575,12 +611,14 @@
},
{
"context": "ProjectSearchBar && !in_replace",
"use_key_equivalents": true,
"bindings": {
"cmd-enter": "project_search::SearchInNew"
}
},
{
"context": "OutlinePanel && not_editing",
"use_key_equivalents": true,
"bindings": {
"escape": "menu::Cancel",
"left": "outline_panel::CollapseSelectedEntry",
@@ -597,6 +635,7 @@
},
{
"context": "ProjectPanel",
"use_key_equivalents": true,
"bindings": {
"left": "project_panel::CollapseSelectedEntry",
"right": "project_panel::ExpandSelectedEntry",
@@ -626,12 +665,14 @@
},
{
"context": "ProjectPanel && not_editing",
"use_key_equivalents": true,
"bindings": {
"space": "project_panel::Open"
}
},
{
"context": "CollabPanel && not_editing",
"use_key_equivalents": true,
"bindings": {
"ctrl-backspace": "collab_panel::Remove",
"space": "menu::Confirm"
@@ -639,18 +680,21 @@
},
{
"context": "(CollabPanel && editing) > Editor",
"use_key_equivalents": true,
"bindings": {
"space": "collab_panel::InsertSpace"
}
},
{
"context": "ChannelModal",
"use_key_equivalents": true,
"bindings": {
"tab": "channel_modal::ToggleMode"
}
},
{
"context": "Picker > Editor",
"use_key_equivalents": true,
"bindings": {
"tab": "picker::ConfirmCompletion",
"alt-enter": ["picker::ConfirmInput", { "secondary": false }],
@@ -659,18 +703,21 @@
},
{
"context": "ChannelModal > Picker > Editor",
"use_key_equivalents": true,
"bindings": {
"tab": "channel_modal::ToggleMode"
}
},
{
"context": "FileFinder",
"use_key_equivalents": true,
"bindings": {
"cmd": "file_finder::ToggleMenu"
}
},
{
"context": "FileFinder && !menu_open",
"use_key_equivalents": true,
"bindings": {
"cmd-shift-p": "file_finder::SelectPrev",
"cmd-j": "pane::SplitDown",
@@ -681,6 +728,7 @@
},
{
"context": "FileFinder && menu_open",
"use_key_equivalents": true,
"bindings": {
"j": "pane::SplitDown",
"k": "pane::SplitUp",
@@ -690,6 +738,7 @@
},
{
"context": "TabSwitcher",
"use_key_equivalents": true,
"bindings": {
"ctrl-up": "menu::SelectPrev",
"ctrl-down": "menu::SelectNext",
@@ -699,6 +748,7 @@
},
{
"context": "Terminal",
"use_key_equivalents": true,
"bindings": {
"ctrl-cmd-space": "terminal::ShowCharacterPalette",
"cmd-c": "terminal::Copy",

View File

@@ -1,7 +1,6 @@
[
{
"context": "VimControl && !menu",
"use_layout_keys": true,
"bindings": {
"i": ["vim::PushOperator", { "Object": { "around": false } }],
"a": ["vim::PushOperator", { "Object": { "around": true } }],
@@ -33,6 +32,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 +66,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 } }],
@@ -176,7 +187,6 @@
},
{
"context": "vim_mode == normal",
"use_layout_keys": true,
"bindings": {
"escape": "editor::Cancel",
"ctrl-[": "editor::Cancel",
@@ -209,6 +219,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"],
@@ -230,7 +241,6 @@
},
{
"context": "VimControl && VimCount",
"use_layout_keys": true,
"bindings": {
"0": ["vim::Number", 0],
":": "vim::CountCommand"
@@ -238,7 +248,6 @@
},
{
"context": "vim_mode == visual",
"use_layout_keys": true,
"bindings": {
":": "vim::VisualCommand",
"u": "vim::ConvertToLowerCase",
@@ -275,6 +284,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",
@@ -287,7 +297,6 @@
},
{
"context": "vim_mode == insert",
"use_layout_keys": true,
"bindings": {
"escape": "vim::NormalBefore",
"ctrl-c": "vim::NormalBefore",
@@ -312,9 +321,25 @@
"ctrl-o": "vim::TemporaryNormal"
}
},
{
"context": "vim_mode == helix_normal",
"bindings": {
"i": "vim::InsertBefore",
"a": "vim::InsertAfter",
"d": "vim::HelixDelete",
"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,
"bindings": {
"ctrl-p": "editor::ShowCompletions",
"ctrl-n": "editor::ShowCompletions"
@@ -322,7 +347,6 @@
},
{
"context": "vim_mode == replace",
"use_layout_keys": true,
"bindings": {
"escape": "vim::NormalBefore",
"ctrl-c": "vim::NormalBefore",
@@ -340,7 +364,6 @@
},
{
"context": "vim_mode == waiting",
"use_layout_keys": true,
"bindings": {
"tab": "vim::Tab",
"enter": "vim::Enter",
@@ -354,16 +377,15 @@
},
{
"context": "vim_mode == operator",
"use_layout_keys": true,
"bindings": {
"escape": "vim::ClearOperators",
"ctrl-c": "vim::ClearOperators",
"ctrl-[": "vim::ClearOperators"
"ctrl-[": "vim::ClearOperators",
"g c": "vim::Comment"
}
},
{
"context": "vim_operator == a || vim_operator == i || vim_operator == cs",
"use_layout_keys": true,
"bindings": {
"w": "vim::Word",
"shift-w": ["vim::Word", { "ignorePunctuation": true }],
@@ -387,12 +409,13 @@
">": "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"
}
},
{
"context": "vim_operator == c",
"use_layout_keys": true,
"bindings": {
"c": "vim::CurrentLine",
"d": "editor::Rename", // zed specific
@@ -401,7 +424,6 @@
},
{
"context": "vim_operator == d",
"use_layout_keys": true,
"bindings": {
"d": "vim::CurrentLine",
"s": ["vim::PushOperator", "DeleteSurrounds"],
@@ -411,7 +433,6 @@
},
{
"context": "vim_operator == gu",
"use_layout_keys": true,
"bindings": {
"g u": "vim::CurrentLine",
"u": "vim::CurrentLine"
@@ -419,7 +440,6 @@
},
{
"context": "vim_operator == gU",
"use_layout_keys": true,
"bindings": {
"g shift-u": "vim::CurrentLine",
"shift-u": "vim::CurrentLine"
@@ -427,7 +447,6 @@
},
{
"context": "vim_operator == g~",
"use_layout_keys": true,
"bindings": {
"g ~": "vim::CurrentLine",
"~": "vim::CurrentLine"
@@ -435,7 +454,6 @@
},
{
"context": "vim_operator == gq",
"use_layout_keys": true,
"bindings": {
"g q": "vim::CurrentLine",
"q": "vim::CurrentLine",
@@ -445,7 +463,6 @@
},
{
"context": "vim_operator == y",
"use_layout_keys": true,
"bindings": {
"y": "vim::CurrentLine",
"s": ["vim::PushOperator", { "AddSurrounds": {} }]
@@ -453,35 +470,36 @@
},
{
"context": "vim_operator == ys",
"use_layout_keys": true,
"bindings": {
"s": "vim::CurrentLine"
}
},
{
"context": "vim_operator == >",
"use_layout_keys": true,
"bindings": {
">": "vim::CurrentLine"
}
},
{
"context": "vim_operator == <",
"use_layout_keys": true,
"bindings": {
"<": "vim::CurrentLine"
}
},
{
"context": "vim_operator == eq",
"bindings": {
"=": "vim::CurrentLine"
}
},
{
"context": "vim_operator == gc",
"use_layout_keys": true,
"bindings": {
"c": "vim::CurrentLine"
}
},
{
"context": "vim_mode == literal",
"use_layout_keys": true,
"bindings": {
"ctrl-@": ["vim::Literal", ["ctrl-@", "\u0000"]],
"ctrl-a": ["vim::Literal", ["ctrl-a", "\u0001"]],
@@ -525,7 +543,6 @@
},
{
"context": "BufferSearchBar && !in_replace",
"use_layout_keys": true,
"bindings": {
"enter": "vim::SearchSubmit",
"escape": "buffer_search::Dismiss"
@@ -533,7 +550,6 @@
},
{
"context": "ProjectPanel || CollabPanel || OutlinePanel || ChatPanel || VimControl || EmptyPane || SharedScreen || MarkdownPreview || KeyContextView",
"use_layout_keys": true,
"bindings": {
// window related commands (ctrl-w X)
"ctrl-w": null,
@@ -590,7 +606,6 @@
},
{
"context": "EmptyPane || SharedScreen || MarkdownPreview || KeyContextView || Welcome",
"use_layout_keys": true,
"bindings": {
":": "command_palette::Toggle",
"g /": "pane::DeploySearch"
@@ -599,7 +614,6 @@
{
// netrw compatibility
"context": "ProjectPanel && not_editing",
"use_layout_keys": true,
"bindings": {
":": "command_palette::Toggle",
"%": "project_panel::NewFile",
@@ -619,6 +633,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",
@@ -627,7 +647,6 @@
},
{
"context": "OutlinePanel && not_editing",
"use_layout_keys": true,
"bindings": {
"j": "menu::SelectNext",
"k": "menu::SelectPrev",

View File

@@ -567,7 +567,18 @@
// "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.
/// Diagnostics are only shown when file icons are also active.
/// This setting only works when 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": "off"
},
// Settings related to preview tabs.
"preview_tabs": {
@@ -1129,6 +1140,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
@@ -1186,6 +1198,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": [
@@ -46,7 +46,7 @@
"tab.active_background": "#1e2025ff",
"search.match_background": "#11a79366",
"panel.background": "#21242bff",
"panel.focused_border": null,
"panel.focused_border": "#10a793ff",
"pane.focused_border": null,
"scrollbar.thumb.background": "#f7f7f84c",
"scrollbar.thumb.hover_background": "#252931ff",

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": [
@@ -46,7 +46,7 @@
"tab.active_background": "#19171cff",
"search.match_background": "#576dda66",
"panel.background": "#221f26ff",
"panel.focused_border": null,
"panel.focused_border": "#566ddaff",
"pane.focused_border": null,
"scrollbar.thumb.background": "#efecf44c",
"scrollbar.thumb.hover_background": "#332f38ff",
@@ -431,7 +431,7 @@
"tab.active_background": "#efecf4ff",
"search.match_background": "#586dda66",
"panel.background": "#e6e3ebff",
"panel.focused_border": null,
"panel.focused_border": "#586cdaff",
"pane.focused_border": null,
"scrollbar.thumb.background": "#19171c4c",
"scrollbar.thumb.hover_background": "#cbc8d1ff",

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": [
@@ -46,7 +46,7 @@
"tab.active_background": "#0d1016ff",
"search.match_background": "#5ac2fe66",
"panel.background": "#1f2127ff",
"panel.focused_border": null,
"panel.focused_border": "#5ac1feff",
"pane.focused_border": null,
"scrollbar.thumb.background": "#bfbdb64c",
"scrollbar.thumb.hover_background": "#2d2f34ff",
@@ -416,7 +416,7 @@
"tab.active_background": "#fcfcfcff",
"search.match_background": "#3b9ee566",
"panel.background": "#ececedff",
"panel.focused_border": null,
"panel.focused_border": "#3b9ee5ff",
"pane.focused_border": null,
"scrollbar.thumb.background": "#5c61664c",
"scrollbar.thumb.hover_background": "#dfe0e1ff",

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": [
@@ -55,7 +55,7 @@
"tab.active_background": "#282828ff",
"search.match_background": "#83a59866",
"panel.background": "#3a3735ff",
"panel.focused_border": null,
"panel.focused_border": "#83a598ff",
"pane.focused_border": null,
"scrollbar.thumb.background": "#fbf1c74c",
"scrollbar.thumb.hover_background": "#494340ff",
@@ -439,7 +439,7 @@
"tab.active_background": "#1d2021ff",
"search.match_background": "#83a59866",
"panel.background": "#393634ff",
"panel.focused_border": null,
"panel.focused_border": "#83a598ff",
"pane.focused_border": null,
"scrollbar.thumb.background": "#fbf1c74c",
"scrollbar.thumb.hover_background": "#494340ff",

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": [
@@ -46,7 +46,7 @@
"tab.active_background": "#191724ff",
"search.match_background": "#57949f66",
"panel.background": "#1c1b2aff",
"panel.focused_border": null,
"panel.focused_border": "#9bced6ff",
"pane.focused_border": null,
"scrollbar.thumb.background": "#e0def44c",
"scrollbar.thumb.hover_background": "#232132ff",
@@ -426,7 +426,7 @@
"tab.active_background": "#faf4edff",
"search.match_background": "#9cced766",
"panel.background": "#fef9f2ff",
"panel.focused_border": null,
"panel.focused_border": "#57949fff",
"pane.focused_border": null,
"scrollbar.thumb.background": "#5752794c",
"scrollbar.thumb.hover_background": "#e5e0dfff",
@@ -806,7 +806,7 @@
"tab.active_background": "#232136ff",
"search.match_background": "#9cced766",
"panel.background": "#28253cff",
"panel.focused_border": null,
"panel.focused_border": "#9bced6ff",
"pane.focused_border": null,
"scrollbar.thumb.background": "#e0def44c",
"scrollbar.thumb.hover_background": "#322f48ff",

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": [
@@ -46,7 +46,7 @@
"tab.active_background": "#282c33ff",
"search.match_background": "#528b8b66",
"panel.background": "#2b3038ff",
"panel.focused_border": null,
"panel.focused_border": "#518b8bff",
"pane.focused_border": null,
"scrollbar.thumb.background": "#fdf4c14c",
"scrollbar.thumb.hover_background": "#313741ff",

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": [
@@ -46,7 +46,7 @@
"tab.active_background": "#002a35ff",
"search.match_background": "#288bd166",
"panel.background": "#04313bff",
"panel.focused_border": null,
"panel.focused_border": "#278ad1ff",
"pane.focused_border": null,
"scrollbar.thumb.background": "#fdf6e34c",
"scrollbar.thumb.hover_background": "#053541ff",
@@ -416,7 +416,7 @@
"tab.active_background": "#fdf6e3ff",
"search.match_background": "#298bd166",
"panel.background": "#f3eddaff",
"panel.focused_border": null,
"panel.focused_border": "#288bd1ff",
"pane.focused_border": null,
"scrollbar.thumb.background": "#002a354c",
"scrollbar.thumb.hover_background": "#dcdacbff",

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": [
@@ -46,7 +46,7 @@
"tab.active_background": "#1b1810ff",
"search.match_background": "#499bef66",
"panel.background": "#231f16ff",
"panel.focused_border": null,
"panel.focused_border": "#499befff",
"pane.focused_border": null,
"scrollbar.thumb.background": "#f8f5de4c",
"scrollbar.thumb.hover_background": "#29251bff",

View File

@@ -3,9 +3,9 @@ use editor::Editor;
use extension_host::ExtensionStore;
use futures::StreamExt;
use gpui::{
actions, percentage, Animation, AnimationExt as _, AppContext, CursorStyle, EventEmitter,
InteractiveElement as _, Model, ParentElement as _, Render, SharedString,
StatefulInteractiveElement, Styled, Transformation, View, ViewContext, VisualContext as _,
actions, percentage, Animation, AnimationExt as _, AppContext, AppContext, CursorStyle,
EventEmitter, InteractiveElement as _, Model, ParentElement as _, Render, SharedString,
StatefulInteractiveElement, Styled, Transformation, View, VisualContext as _,
};
use language::{LanguageRegistry, LanguageServerBinaryStatus, LanguageServerId};
use lsp::LanguageServerName;
@@ -46,34 +46,38 @@ struct PendingWork<'a> {
struct Content {
icon: Option<gpui::AnyElement>,
message: String,
on_click: Option<Arc<dyn Fn(&mut ActivityIndicator, &mut ViewContext<ActivityIndicator>)>>,
on_click:
Option<Arc<dyn Fn(&mut ActivityIndicator, &Model<ActivityIndicator>, &mut AppContext)>>,
}
impl ActivityIndicator {
pub fn new(
workspace: &mut Workspace,
languages: Arc<LanguageRegistry>,
cx: &mut ViewContext<Workspace>,
) -> View<ActivityIndicator> {
model: &Model<Workspace>,
cx: &mut AppContext,
) -> Model<ActivityIndicator> {
let project = workspace.project().clone();
let auto_updater = AutoUpdater::get(cx);
let this = cx.new_view(|cx: &mut ViewContext<Self>| {
let this = cx.new_model(|model: &Model<Self>, cx: &mut AppContext| {
let mut status_events = languages.language_server_binary_statuses();
cx.spawn(|this, mut cx| async move {
while let Some((name, status)) = status_events.next().await {
this.update(&mut cx, |this, cx| {
this.statuses.retain(|s| s.name != name);
this.statuses.push(LspStatus { name, status });
cx.notify();
})?;
}
anyhow::Ok(())
})
.detach();
cx.observe(&project, |_, _, cx| cx.notify()).detach();
model
.spawn(cx, |this, mut cx| async move {
while let Some((name, status)) = status_events.next().await {
this.update(&mut cx, |this, model, cx| {
this.statuses.retain(|s| s.name != name);
this.statuses.push(LspStatus { name, status });
model.notify(cx);
})?;
}
anyhow::Ok(())
})
.detach();
cx.observe(&project, |_, _, cx| model.notify(cx)).detach();
if let Some(auto_updater) = auto_updater.as_ref() {
cx.observe(auto_updater, |_, _, cx| cx.notify()).detach();
cx.observe(auto_updater, |_, _, cx| model.notify(cx))
.detach();
}
Self {
@@ -86,7 +90,8 @@ impl ActivityIndicator {
cx.subscribe(&this, move |_, _, event, cx| match event {
Event::ShowError { lsp_name, error } => {
let create_buffer = project.update(cx, |project, cx| project.create_buffer(cx));
let create_buffer =
project.update(cx, |project, model, cx| project.create_buffer(model, cx));
let project = project.clone();
let error = error.clone();
let lsp_name = lsp_name.clone();
@@ -105,8 +110,8 @@ impl ActivityIndicator {
})?;
workspace.update(&mut cx, |workspace, cx| {
workspace.add_item_to_active_pane(
Box::new(cx.new_view(|cx| {
Editor::for_buffer(buffer, Some(project.clone()), cx)
Box::new(cx.new_model(|model, cx| {
Editor::for_buffer(buffer, Some(project.clone()), model, cx)
})),
None,
true,
@@ -123,29 +128,44 @@ impl ActivityIndicator {
this
}
fn show_error_message(&mut self, _: &ShowErrorMessage, cx: &mut ViewContext<Self>) {
fn show_error_message(
&mut self,
_: &ShowErrorMessage,
model: &Model<Self>,
window: &mut Window,
cx: &mut AppContext,
) {
self.statuses.retain(|status| {
if let LanguageServerBinaryStatus::Failed { error } = &status.status {
cx.emit(Event::ShowError {
lsp_name: status.name.clone(),
error: error.clone(),
});
model.emit(
cx,
Event::ShowError {
lsp_name: status.name.clone(),
error: error.clone(),
},
);
false
} else {
true
}
});
cx.notify();
model.notify(cx);
}
fn dismiss_error_message(&mut self, _: &DismissErrorMessage, cx: &mut ViewContext<Self>) {
fn dismiss_error_message(
&mut self,
_: &DismissErrorMessage,
model: &Model<Self>,
window: &mut Window,
cx: &mut AppContext,
) {
if let Some(updater) = &self.auto_updater {
updater.update(cx, |updater, cx| {
updater.dismiss_error(cx);
updater.update(cx, |updater, model, cx| {
updater.dismiss_error(model, cx);
});
}
cx.notify();
model.notify(cx);
}
fn pending_language_server_work<'a>(
@@ -183,7 +203,7 @@ impl ActivityIndicator {
self.project.read(cx).shell_environment_errors(cx)
}
fn content_to_render(&mut self, cx: &mut ViewContext<Self>) -> Option<Content> {
fn content_to_render(&mut self, model: &Model<Self>, cx: &mut AppContext) -> Option<Content> {
// Show if any direnv calls failed
if let Some((&worktree_id, error)) = self.pending_environment_errors(cx).next() {
return Some(Content {
@@ -194,7 +214,7 @@ impl ActivityIndicator {
),
message: error.0.clone(),
on_click: Some(Arc::new(move |this, cx| {
this.project.update(cx, |project, cx| {
this.project.update(cx, |project, model, cx| {
project.remove_environment_error(cx, worktree_id);
});
cx.dispatch_action(Box::new(workspace::OpenLog));
@@ -352,7 +372,7 @@ impl ActivityIndicator {
),
message: format!("Formatting failed: {}. Click to see logs.", failure),
on_click: Some(Arc::new(|indicator, cx| {
indicator.project.update(cx, |project, cx| {
indicator.project.update(cx, |project, model, cx| {
project.reset_last_formatting_failure(cx);
});
cx.dispatch_action(Box::new(workspace::OpenLog));
@@ -442,8 +462,12 @@ impl ActivityIndicator {
None
}
fn toggle_language_server_work_context_menu(&mut self, cx: &mut ViewContext<Self>) {
self.context_menu_handle.toggle(cx);
fn toggle_language_server_work_context_menu(
&mut self,
model: &Model<Self>,
cx: &mut AppContext,
) {
self.context_menu_handle.toggle(model, cx);
}
}
@@ -452,15 +476,20 @@ impl EventEmitter<Event> for ActivityIndicator {}
const MAX_MESSAGE_LEN: usize = 50;
impl Render for ActivityIndicator {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
fn render(
&mut self,
model: &Model<Self>,
window: &mut gpui::Window,
cx: &mut AppContext,
) -> impl IntoElement {
let result = h_flex()
.id("activity-indicator")
.on_action(cx.listener(Self::show_error_message))
.on_action(cx.listener(Self::dismiss_error_message));
let Some(content) = self.content_to_render(cx) else {
.on_action(model.listener(Self::show_error_message))
.on_action(model.listener(Self::dismiss_error_message));
let Some(content) = self.content_to_render(model, cx) else {
return result;
};
let this = cx.view().downgrade();
let this = model.downgrade();
let truncate_content = content.message.len() > MAX_MESSAGE_LEN;
result.gap_2().child(
PopoverMenu::new("activity-indicator-popover")
@@ -480,13 +509,15 @@ impl Render for ActivityIndicator {
))
.size(LabelSize::Small),
)
.tooltip(move |cx| Tooltip::text(&content.message, cx))
.tooltip(move |window, cx| {
Tooltip::text(&content.message, cx)
})
} else {
button.child(Label::new(content.message).size(LabelSize::Small))
}
})
.when_some(content.on_click, |this, handler| {
this.on_click(cx.listener(move |this, _, cx| {
this.on_click(model.listener(move |this, _, model, window, cx| {
handler(this, cx);
}))
.cursor(CursorStyle::PointingHand)
@@ -494,10 +525,10 @@ impl Render for ActivityIndicator {
),
)
.anchor(gpui::AnchorCorner::BottomLeft)
.menu(move |cx| {
.menu(move |window, cx| {
let strong_this = this.upgrade()?;
let mut has_work = false;
let menu = ContextMenu::build(cx, |mut menu, cx| {
let menu = ContextMenu::build(window, cx, |mut menu, model, window, cx| {
for work in strong_this.read(cx).pending_language_server_work(cx) {
has_work = true;
let this = this.clone();
@@ -522,16 +553,17 @@ impl Render for ActivityIndicator {
.into_any_element()
},
move |cx| {
this.update(cx, |this, cx| {
this.project.update(cx, |project, cx| {
this.update(cx, |this, model, cx| {
this.project.update(cx, |project, model, cx| {
project.cancel_language_server_work(
language_server_id,
Some(token.clone()),
model,
cx,
);
});
this.context_menu_handle.hide(cx);
cx.notify();
model.notify(cx);
})
.ok();
},
@@ -554,5 +586,11 @@ impl Render for ActivityIndicator {
}
impl StatusItemView for ActivityIndicator {
fn set_active_pane_item(&mut self, _: Option<&dyn ItemHandle>, _: &mut ViewContext<Self>) {}
fn set_active_pane_item(
&mut self,
_: Option<&dyn ItemHandle>,
_: &Model<Self>,
_: &mut AppContext,
) {
}
}

View File

@@ -321,9 +321,9 @@ fn update_active_language_model_from_settings(cx: &mut AppContext) {
)
})
.collect::<Vec<_>>();
LanguageModelRegistry::global(cx).update(cx, |registry, cx| {
registry.select_active_model(&provider_name, &model_id, cx);
registry.select_inline_alternative_models(inline_alternatives, cx);
LanguageModelRegistry::global(cx).update(cx, |registry, model, cx| {
registry.select_active_model(&provider_name, &model_id, model, cx);
registry.select_inline_alternative_models(inline_alternatives, model, cx);
});
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -35,7 +35,7 @@ use std::{
sync::{atomic::AtomicBool, Arc},
};
use text::{network::Network, OffsetRangeExt as _, ReplicaId, ToOffset};
use ui::{Context as _, IconName, WindowContext};
use ui::{Context as _, IconName};
use unindent::Unindent;
use util::{
test::{generate_marked_text, marked_text_ranges},
@@ -51,7 +51,7 @@ fn test_inserting_and_removing_messages(cx: &mut AppContext) {
assistant_panel::init(cx);
let registry = Arc::new(LanguageRegistry::test(cx.background_executor().clone()));
let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
let context = cx.new_model(|cx| {
let context = cx.new_model(|model, cx| {
Context::local(
registry,
None,
@@ -59,6 +59,7 @@ fn test_inserting_and_removing_messages(cx: &mut AppContext) {
prompt_builder.clone(),
Arc::new(SlashCommandWorkingSet::default()),
Arc::new(ToolWorkingSet::default()),
model,
cx,
)
});
@@ -70,7 +71,7 @@ fn test_inserting_and_removing_messages(cx: &mut AppContext) {
vec![(message_1.id, Role::User, 0..0)]
);
let message_2 = context.update(cx, |context, cx| {
let message_2 = context.update(cx, |context, model, cx| {
context
.insert_message_after(message_1.id, Role::Assistant, MessageStatus::Done, cx)
.unwrap()
@@ -83,8 +84,8 @@ fn test_inserting_and_removing_messages(cx: &mut AppContext) {
]
);
buffer.update(cx, |buffer, cx| {
buffer.edit([(0..0, "1"), (1..1, "2")], None, cx)
buffer.update(cx, |buffer, model, cx| {
buffer.edit([(0..0, "1"), (1..1, "2")], None, model, cx)
});
assert_eq!(
messages(&context, cx),
@@ -94,7 +95,7 @@ fn test_inserting_and_removing_messages(cx: &mut AppContext) {
]
);
let message_3 = context.update(cx, |context, cx| {
let message_3 = context.update(cx, |context, model, cx| {
context
.insert_message_after(message_2.id, Role::User, MessageStatus::Done, cx)
.unwrap()
@@ -108,7 +109,7 @@ fn test_inserting_and_removing_messages(cx: &mut AppContext) {
]
);
let message_4 = context.update(cx, |context, cx| {
let message_4 = context.update(cx, |context, model, cx| {
context
.insert_message_after(message_2.id, Role::User, MessageStatus::Done, cx)
.unwrap()
@@ -123,8 +124,8 @@ fn test_inserting_and_removing_messages(cx: &mut AppContext) {
]
);
buffer.update(cx, |buffer, cx| {
buffer.edit([(4..4, "C"), (5..5, "D")], None, cx)
buffer.update(cx, |buffer, model, cx| {
buffer.edit([(4..4, "C"), (5..5, "D")], None, model, cx)
});
assert_eq!(
messages(&context, cx),
@@ -137,7 +138,9 @@ fn test_inserting_and_removing_messages(cx: &mut AppContext) {
);
// Deleting across message boundaries merges the messages.
buffer.update(cx, |buffer, cx| buffer.edit([(1..4, "")], None, cx));
buffer.update(cx, |buffer, model, cx| {
buffer.edit([(1..4, "")], None, model, cx)
});
assert_eq!(
messages(&context, cx),
vec![
@@ -147,7 +150,7 @@ fn test_inserting_and_removing_messages(cx: &mut AppContext) {
);
// Undoing the deletion should also undo the merge.
buffer.update(cx, |buffer, cx| buffer.undo(cx));
buffer.update(cx, |buffer, model, cx| buffer.undo(cx));
assert_eq!(
messages(&context, cx),
vec![
@@ -159,7 +162,7 @@ fn test_inserting_and_removing_messages(cx: &mut AppContext) {
);
// Redoing the deletion should also redo the merge.
buffer.update(cx, |buffer, cx| buffer.redo(cx));
buffer.update(cx, |buffer, model, cx| buffer.redo(cx));
assert_eq!(
messages(&context, cx),
vec![
@@ -169,7 +172,7 @@ fn test_inserting_and_removing_messages(cx: &mut AppContext) {
);
// Ensure we can still insert after a merged message.
let message_5 = context.update(cx, |context, cx| {
let message_5 = context.update(cx, |context, model, cx| {
context
.insert_message_after(message_1.id, Role::System, MessageStatus::Done, cx)
.unwrap()
@@ -193,7 +196,7 @@ fn test_message_splitting(cx: &mut AppContext) {
let registry = Arc::new(LanguageRegistry::test(cx.background_executor().clone()));
let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
let context = cx.new_model(|cx| {
let context = cx.new_model(|model, cx| {
Context::local(
registry.clone(),
None,
@@ -201,6 +204,7 @@ fn test_message_splitting(cx: &mut AppContext) {
prompt_builder.clone(),
Arc::new(SlashCommandWorkingSet::default()),
Arc::new(ToolWorkingSet::default()),
model,
cx,
)
});
@@ -212,11 +216,11 @@ fn test_message_splitting(cx: &mut AppContext) {
vec![(message_1.id, Role::User, 0..0)]
);
buffer.update(cx, |buffer, cx| {
buffer.edit([(0..0, "aaa\nbbb\nccc\nddd\n")], None, cx)
buffer.update(cx, |buffer, model, cx| {
buffer.edit([(0..0, "aaa\nbbb\nccc\nddd\n")], None, model, cx)
});
let (_, message_2) = context.update(cx, |context, cx| context.split_message(3..3, cx));
let (_, message_2) = context.update(cx, |context, model, cx| context.split_message(3..3, cx));
let message_2 = message_2.unwrap();
// We recycle newlines in the middle of a split message
@@ -229,7 +233,7 @@ fn test_message_splitting(cx: &mut AppContext) {
]
);
let (_, message_3) = context.update(cx, |context, cx| context.split_message(3..3, cx));
let (_, message_3) = context.update(cx, |context, model, cx| context.split_message(3..3, cx));
let message_3 = message_3.unwrap();
// We don't recycle newlines at the end of a split message
@@ -243,7 +247,7 @@ fn test_message_splitting(cx: &mut AppContext) {
]
);
let (_, message_4) = context.update(cx, |context, cx| context.split_message(9..9, cx));
let (_, message_4) = context.update(cx, |context, model, cx| context.split_message(9..9, cx));
let message_4 = message_4.unwrap();
assert_eq!(buffer.read(cx).text(), "aaa\n\nbbb\nccc\nddd\n");
assert_eq!(
@@ -256,7 +260,7 @@ fn test_message_splitting(cx: &mut AppContext) {
]
);
let (_, message_5) = context.update(cx, |context, cx| context.split_message(9..9, cx));
let (_, message_5) = context.update(cx, |context, model, cx| context.split_message(9..9, cx));
let message_5 = message_5.unwrap();
assert_eq!(buffer.read(cx).text(), "aaa\n\nbbb\n\nccc\nddd\n");
assert_eq!(
@@ -271,7 +275,7 @@ fn test_message_splitting(cx: &mut AppContext) {
);
let (message_6, message_7) =
context.update(cx, |context, cx| context.split_message(14..16, cx));
context.update(cx, |context, model, cx| context.split_message(14..16, cx));
let message_6 = message_6.unwrap();
let message_7 = message_7.unwrap();
assert_eq!(buffer.read(cx).text(), "aaa\n\nbbb\n\nccc\ndd\nd\n");
@@ -297,7 +301,7 @@ fn test_messages_for_offsets(cx: &mut AppContext) {
assistant_panel::init(cx);
let registry = Arc::new(LanguageRegistry::test(cx.background_executor().clone()));
let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
let context = cx.new_model(|cx| {
let context = cx.new_model(|model, cx| {
Context::local(
registry,
None,
@@ -305,6 +309,7 @@ fn test_messages_for_offsets(cx: &mut AppContext) {
prompt_builder.clone(),
Arc::new(SlashCommandWorkingSet::default()),
Arc::new(ToolWorkingSet::default()),
model,
cx,
)
});
@@ -316,20 +321,26 @@ fn test_messages_for_offsets(cx: &mut AppContext) {
vec![(message_1.id, Role::User, 0..0)]
);
buffer.update(cx, |buffer, cx| buffer.edit([(0..0, "aaa")], None, cx));
buffer.update(cx, |buffer, model, cx| {
buffer.edit([(0..0, "aaa")], None, model, cx)
});
let message_2 = context
.update(cx, |context, cx| {
.update(cx, |context, model, cx| {
context.insert_message_after(message_1.id, Role::User, MessageStatus::Done, cx)
})
.unwrap();
buffer.update(cx, |buffer, cx| buffer.edit([(4..4, "bbb")], None, cx));
buffer.update(cx, |buffer, model, cx| {
buffer.edit([(4..4, "bbb")], None, model, cx)
});
let message_3 = context
.update(cx, |context, cx| {
.update(cx, |context, model, cx| {
context.insert_message_after(message_2.id, Role::User, MessageStatus::Done, cx)
})
.unwrap();
buffer.update(cx, |buffer, cx| buffer.edit([(8..8, "ccc")], None, cx));
buffer.update(cx, |buffer, model, cx| {
buffer.edit([(8..8, "ccc")], None, model, cx)
});
assert_eq!(buffer.read(cx).text(), "aaa\nbbb\nccc");
assert_eq!(
@@ -351,7 +362,7 @@ fn test_messages_for_offsets(cx: &mut AppContext) {
);
let message_4 = context
.update(cx, |context, cx| {
.update(cx, |context, model, cx| {
context.insert_message_after(message_3.id, Role::User, MessageStatus::Done, cx)
})
.unwrap();
@@ -412,7 +423,7 @@ async fn test_slash_commands(cx: &mut TestAppContext) {
let registry = Arc::new(LanguageRegistry::test(cx.executor()));
let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
let context = cx.new_model(|cx| {
let context = cx.new_model(|model, cx| {
Context::local(
registry.clone(),
None,
@@ -420,6 +431,7 @@ async fn test_slash_commands(cx: &mut TestAppContext) {
prompt_builder.clone(),
Arc::new(SlashCommandWorkingSet::default()),
Arc::new(ToolWorkingSet::default()),
model,
cx,
)
});
@@ -432,7 +444,7 @@ async fn test_slash_commands(cx: &mut TestAppContext) {
}
let context_ranges = Rc::new(RefCell::new(ContextRanges::default()));
context.update(cx, |_, cx| {
context.update(cx, |_, model, cx| {
cx.subscribe(&context, {
let context_ranges = context_ranges.clone();
move |context, _, event, _| {
@@ -446,7 +458,7 @@ async fn test_slash_commands(cx: &mut TestAppContext) {
}
ContextEvent::ParsedSlashCommandsUpdated { removed, updated } => {
for range in removed {
context_ranges.parsed_commands.remove(range);
context_ranges.parsed_commands.remove(&range);
}
for command in updated {
context_ranges
@@ -467,7 +479,7 @@ async fn test_slash_commands(cx: &mut TestAppContext) {
let buffer = context.read_with(cx, |context, _| context.buffer.clone());
// Insert a slash command
buffer.update(cx, |buffer, cx| {
buffer.update(cx, |buffer, model, cx| {
buffer.edit([(0..0, "/file src/lib.rs")], None, cx);
});
assert_text_and_context_ranges(
@@ -480,7 +492,7 @@ async fn test_slash_commands(cx: &mut TestAppContext) {
);
// Edit the argument of the slash command.
buffer.update(cx, |buffer, cx| {
buffer.update(cx, |buffer, model, cx| {
let edit_offset = buffer.text().find("lib.rs").unwrap();
buffer.edit([(edit_offset..edit_offset + "lib".len(), "main")], None, cx);
});
@@ -494,7 +506,7 @@ async fn test_slash_commands(cx: &mut TestAppContext) {
);
// Edit the name of the slash command, using one that doesn't exist.
buffer.update(cx, |buffer, cx| {
buffer.update(cx, |buffer, model, cx| {
let edit_offset = buffer.text().find("/file").unwrap();
buffer.edit(
[(edit_offset..edit_offset + "/file".len(), "/unknown")],
@@ -512,7 +524,7 @@ async fn test_slash_commands(cx: &mut TestAppContext) {
);
// Undoing the insertion of an non-existent slash command resorts the previous one.
buffer.update(cx, |buffer, cx| buffer.undo(cx));
buffer.update(cx, |buffer, model, cx| buffer.undo(cx));
assert_text_and_context_ranges(
&buffer,
&context_ranges,
@@ -523,7 +535,7 @@ async fn test_slash_commands(cx: &mut TestAppContext) {
);
let (command_output_tx, command_output_rx) = mpsc::unbounded();
context.update(cx, |context, cx| {
context.update(cx, |context, model, cx| {
let command_source_range = context.parsed_slash_commands[0].source_range.clone();
context.insert_command_output(
command_source_range,
@@ -619,7 +631,7 @@ async fn test_slash_commands(cx: &mut TestAppContext) {
cx: &mut TestAppContext,
) {
let mut actual_marked_text = String::new();
buffer.update(cx, |buffer, _| {
buffer.update(cx, |buffer, model, _| {
struct Endpoint {
offset: usize,
marker: char,
@@ -703,7 +715,7 @@ async fn test_workflow_step_parsing(cx: &mut TestAppContext) {
// Create a new context
let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
let context = cx.new_model(|cx| {
let context = cx.new_model(|model, cx| {
Context::local(
registry.clone(),
Some(project),
@@ -711,12 +723,13 @@ async fn test_workflow_step_parsing(cx: &mut TestAppContext) {
prompt_builder.clone(),
Arc::new(SlashCommandWorkingSet::default()),
Arc::new(ToolWorkingSet::default()),
model,
cx,
)
});
// Insert an assistant message to simulate a response.
let assistant_message_id = context.update(cx, |context, cx| {
let assistant_message_id = context.update(cx, |context, model, cx| {
let user_message_id = context.messages(cx).next().unwrap().id;
context
.insert_message_after(user_message_id, Role::Assistant, MessageStatus::Done, cx)
@@ -903,7 +916,7 @@ async fn test_workflow_step_parsing(cx: &mut TestAppContext) {
);
// When setting the message role to User, the steps are cleared.
context.update(cx, |context, cx| {
context.update(cx, |context, model, cx| {
context.cycle_message_roles(HashSet::from_iter([assistant_message_id]), cx);
context.cycle_message_roles(HashSet::from_iter([assistant_message_id]), cx);
});
@@ -932,7 +945,7 @@ async fn test_workflow_step_parsing(cx: &mut TestAppContext) {
);
// When setting the message role back to Assistant, the steps are reparsed.
context.update(cx, |context, cx| {
context.update(cx, |context, model, cx| {
context.cycle_message_roles(HashSet::from_iter([assistant_message_id]), cx);
});
expect_patches(
@@ -968,7 +981,7 @@ async fn test_workflow_step_parsing(cx: &mut TestAppContext) {
// Ensure steps are re-parsed when deserializing.
let serialized_context = context.read_with(cx, |context, cx| context.serialize(cx));
let deserialized_context = cx.new_model(|cx| {
let deserialized_context = cx.new_model(|model, cx| {
Context::deserialize(
serialized_context,
Default::default(),
@@ -978,6 +991,7 @@ async fn test_workflow_step_parsing(cx: &mut TestAppContext) {
Arc::new(ToolWorkingSet::default()),
None,
None,
model,
cx,
)
});
@@ -1013,9 +1027,14 @@ async fn test_workflow_step_parsing(cx: &mut TestAppContext) {
);
fn edit(context: &Model<Context>, new_text_marked_with_edits: &str, cx: &mut TestAppContext) {
context.update(cx, |context, cx| {
context.buffer.update(cx, |buffer, cx| {
buffer.edit_via_marked_text(&new_text_marked_with_edits.unindent(), None, cx);
context.update(cx, |context, model, cx| {
context.buffer.update(cx, |buffer, model, cx| {
buffer.edit_via_marked_text(
&new_text_marked_with_edits.unindent(),
None,
model,
cx,
);
});
});
cx.executor().run_until_parked();
@@ -1031,7 +1050,7 @@ async fn test_workflow_step_parsing(cx: &mut TestAppContext) {
let expected_marked_text = expected_marked_text.unindent();
let (expected_text, _) = marked_text_ranges(&expected_marked_text, false);
let (buffer_text, ranges, patches) = context.update(cx, |context, cx| {
let (buffer_text, ranges, patches) = context.update(cx, |context, model, cx| {
context.buffer.read_with(cx, |buffer, _| {
let ranges = context
.patches
@@ -1084,7 +1103,7 @@ async fn test_serialization(cx: &mut TestAppContext) {
cx.update(assistant_panel::init);
let registry = Arc::new(LanguageRegistry::test(cx.executor()));
let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
let context = cx.new_model(|cx| {
let context = cx.new_model(|model, cx| {
Context::local(
registry.clone(),
None,
@@ -1092,31 +1111,32 @@ async fn test_serialization(cx: &mut TestAppContext) {
prompt_builder.clone(),
Arc::new(SlashCommandWorkingSet::default()),
Arc::new(ToolWorkingSet::default()),
model,
cx,
)
});
let buffer = context.read_with(cx, |context, _| context.buffer.clone());
let message_0 = context.read_with(cx, |context, _| context.message_anchors[0].id);
let message_1 = context.update(cx, |context, cx| {
let message_1 = context.update(cx, |context, model, cx| {
context
.insert_message_after(message_0, Role::Assistant, MessageStatus::Done, cx)
.unwrap()
});
let message_2 = context.update(cx, |context, cx| {
let message_2 = context.update(cx, |context, model, cx| {
context
.insert_message_after(message_1.id, Role::System, MessageStatus::Done, cx)
.unwrap()
});
buffer.update(cx, |buffer, cx| {
buffer.update(cx, |buffer, model, cx| {
buffer.edit([(0..0, "a"), (1..1, "b\nc")], None, cx);
buffer.finalize_last_transaction();
});
let _message_3 = context.update(cx, |context, cx| {
let _message_3 = context.update(cx, |context, model, cx| {
context
.insert_message_after(message_2.id, Role::System, MessageStatus::Done, cx)
.unwrap()
});
buffer.update(cx, |buffer, cx| buffer.undo(cx));
buffer.update(cx, |buffer, model, cx| buffer.undo(cx));
assert_eq!(buffer.read_with(cx, |buffer, _| buffer.text()), "a\nb\nc\n");
assert_eq!(
cx.read(|cx| messages(&context, cx)),
@@ -1128,7 +1148,7 @@ async fn test_serialization(cx: &mut TestAppContext) {
);
let serialized_context = context.read_with(cx, |context, cx| context.serialize(cx));
let deserialized_context = cx.new_model(|cx| {
let deserialized_context = cx.new_model(|model, cx| {
Context::deserialize(
serialized_context,
Default::default(),
@@ -1138,6 +1158,7 @@ async fn test_serialization(cx: &mut TestAppContext) {
Arc::new(ToolWorkingSet::default()),
None,
None,
model,
cx,
)
});
@@ -1187,7 +1208,7 @@ async fn test_random_context_collaboration(cx: &mut TestAppContext, mut rng: Std
let context_id = ContextId::new();
let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
for i in 0..num_peers {
let context = cx.new_model(|cx| {
let context = cx.new_model(|model, cx| {
Context::new(
context_id.clone(),
i as ReplicaId,
@@ -1198,6 +1219,7 @@ async fn test_random_context_collaboration(cx: &mut TestAppContext, mut rng: Std
Arc::new(ToolWorkingSet::default()),
None,
None,
model,
cx,
)
});
@@ -1232,15 +1254,15 @@ async fn test_random_context_collaboration(cx: &mut TestAppContext, mut rng: Std
match rng.gen_range(0..100) {
0..=29 if mutation_count > 0 => {
log::info!("Context {}: edit buffer", context_index);
context.update(cx, |context, cx| {
context
.buffer
.update(cx, |buffer, cx| buffer.randomly_edit(&mut rng, 1, cx));
context.update(cx, |context, model, cx| {
context.buffer.update(cx, |buffer, model, cx| {
buffer.randomly_edit(&mut rng, 1, cx)
});
});
mutation_count -= 1;
}
30..=44 if mutation_count > 0 => {
context.update(cx, |context, cx| {
context.update(cx, |context, model, cx| {
let range = context.buffer.read(cx).random_byte_range(0, &mut rng);
log::info!("Context {}: split message at {:?}", context_index, range);
context.split_message(range, cx);
@@ -1248,7 +1270,7 @@ async fn test_random_context_collaboration(cx: &mut TestAppContext, mut rng: Std
mutation_count -= 1;
}
45..=59 if mutation_count > 0 => {
context.update(cx, |context, cx| {
context.update(cx, |context, model, cx| {
if let Some(message) = context.messages(cx).choose(&mut rng) {
let role = *[Role::User, Role::Assistant, Role::System]
.choose(&mut rng)
@@ -1265,7 +1287,7 @@ async fn test_random_context_collaboration(cx: &mut TestAppContext, mut rng: Std
mutation_count -= 1;
}
60..=74 if mutation_count > 0 => {
context.update(cx, |context, cx| {
context.update(cx, |context, model, cx| {
let command_text = "/".to_string()
+ slash_commands
.command_names()
@@ -1274,7 +1296,7 @@ async fn test_random_context_collaboration(cx: &mut TestAppContext, mut rng: Std
.clone()
.as_ref();
let command_range = context.buffer.update(cx, |buffer, cx| {
let command_range = context.buffer.update(cx, |buffer, model, cx| {
let offset = buffer.random_byte_range(0, &mut rng).start;
buffer.edit(
[(offset..offset, format!("\n{}\n", command_text))],
@@ -1342,7 +1364,7 @@ async fn test_random_context_collaboration(cx: &mut TestAppContext, mut rng: Std
mutation_count -= 1;
}
75..=84 if mutation_count > 0 => {
context.update(cx, |context, cx| {
context.update(cx, |context, model, cx| {
if let Some(message) = context.messages(cx).choose(&mut rng) {
let new_status = match rng.gen_range(0..3) {
0 => MessageStatus::Done,
@@ -1390,7 +1412,9 @@ async fn test_random_context_collaboration(cx: &mut TestAppContext, mut rng: Std
);
network.lock().broadcast(replica_id, ops_to_send);
context.update(cx, |context, cx| context.apply_ops(ops_to_receive, cx));
context.update(cx, |context, model, cx| {
context.apply_ops(ops_to_receive, cx)
});
} else if rng.gen_bool(0.1) && replica_id != 0 {
log::info!("Context {}: disconnecting", context_index);
network.lock().disconnect_peer(replica_id);
@@ -1402,7 +1426,7 @@ async fn test_random_context_collaboration(cx: &mut TestAppContext, mut rng: Std
.map(ContextOperation::from_proto)
.collect::<Result<Vec<_>>>()
.unwrap();
context.update(cx, |context, cx| context.apply_ops(ops, cx));
context.update(cx, |context, model, cx| context.apply_ops(ops, cx));
}
}
}
@@ -1449,7 +1473,7 @@ fn test_mark_cache_anchors(cx: &mut AppContext) {
assistant_panel::init(cx);
let registry = Arc::new(LanguageRegistry::test(cx.background_executor().clone()));
let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
let context = cx.new_model(|cx| {
let context = cx.new_model(|model, cx| {
Context::local(
registry,
None,
@@ -1457,6 +1481,7 @@ fn test_mark_cache_anchors(cx: &mut AppContext) {
prompt_builder.clone(),
Arc::new(SlashCommandWorkingSet::default()),
Arc::new(ToolWorkingSet::default()),
model,
cx,
)
});
@@ -1471,7 +1496,7 @@ fn test_mark_cache_anchors(cx: &mut AppContext) {
let message_1 = context.read(cx).message_anchors[0].clone();
context.update(cx, |context, cx| {
context.update(cx, |context, model, cx| {
context.mark_cache_anchors(cache_configuration, false, cx)
});
@@ -1484,22 +1509,28 @@ fn test_mark_cache_anchors(cx: &mut AppContext) {
"Empty messages should not have any cache anchors."
);
buffer.update(cx, |buffer, cx| buffer.edit([(0..0, "aaa")], None, cx));
buffer.update(cx, |buffer, model, cx| {
buffer.edit([(0..0, "aaa")], None, model, cx)
});
let message_2 = context
.update(cx, |context, cx| {
.update(cx, |context, model, cx| {
context.insert_message_after(message_1.id, Role::User, MessageStatus::Pending, cx)
})
.unwrap();
buffer.update(cx, |buffer, cx| buffer.edit([(4..4, "bbbbbbb")], None, cx));
buffer.update(cx, |buffer, model, cx| {
buffer.edit([(4..4, "bbbbbbb")], None, model, cx)
});
let message_3 = context
.update(cx, |context, cx| {
.update(cx, |context, model, cx| {
context.insert_message_after(message_2.id, Role::User, MessageStatus::Pending, cx)
})
.unwrap();
buffer.update(cx, |buffer, cx| buffer.edit([(12..12, "cccccc")], None, cx));
buffer.update(cx, |buffer, model, cx| {
buffer.edit([(12..12, "cccccc")], None, model, cx)
});
context.update(cx, |context, cx| {
context.update(cx, |context, model, cx| {
context.mark_cache_anchors(cache_configuration, false, cx)
});
assert_eq!(buffer.read(cx).text(), "aaa\nbbbbbbb\ncccccc");
@@ -1511,12 +1542,12 @@ fn test_mark_cache_anchors(cx: &mut AppContext) {
0,
"Messages should not be marked for cache before going over the token minimum."
);
context.update(cx, |context, _| {
context.update(cx, |context, model, _| {
context.token_count = Some(20);
});
context.update(cx, |context, cx| {
context.mark_cache_anchors(cache_configuration, true, cx)
context.update(cx, |context, model, cx| {
context.mark_cache_anchors(cache_configuration, true, model, cx)
});
assert_eq!(
messages_cache(&context, cx)
@@ -1528,12 +1559,12 @@ fn test_mark_cache_anchors(cx: &mut AppContext) {
);
context
.update(cx, |context, cx| {
.update(cx, |context, model, cx| {
context.insert_message_after(message_3.id, Role::Assistant, MessageStatus::Pending, cx)
})
.unwrap();
context.update(cx, |context, cx| {
context.update(cx, |context, model, cx| {
context.mark_cache_anchors(cache_configuration, false, cx)
});
assert_eq!(
@@ -1544,7 +1575,7 @@ fn test_mark_cache_anchors(cx: &mut AppContext) {
vec![false, true, true, false],
"Most recent message should also be cached if not a speculative request."
);
context.update(cx, |context, cx| {
context.update(cx, |context, model, cx| {
context.update_cache_status_for_completion(cx)
});
assert_eq!(
@@ -1563,8 +1594,10 @@ fn test_mark_cache_anchors(cx: &mut AppContext) {
"All user messages prior to anchor should be marked as cached."
);
buffer.update(cx, |buffer, cx| buffer.edit([(14..14, "d")], None, cx));
context.update(cx, |context, cx| {
buffer.update(cx, |buffer, model, cx| {
buffer.edit([(14..14, "d")], None, model, cx)
});
context.update(cx, |context, model, cx| {
context.mark_cache_anchors(cache_configuration, false, cx)
});
assert_eq!(
@@ -1582,8 +1615,10 @@ fn test_mark_cache_anchors(cx: &mut AppContext) {
],
"Modifying a message should invalidate it's cache but leave previous messages."
);
buffer.update(cx, |buffer, cx| buffer.edit([(2..2, "e")], None, cx));
context.update(cx, |context, cx| {
buffer.update(cx, |buffer, model, cx| {
buffer.edit([(2..2, "e")], None, model, cx)
});
context.update(cx, |context, model, cx| {
context.mark_cache_anchors(cache_configuration, false, cx)
});
assert_eq!(
@@ -1642,8 +1677,9 @@ impl SlashCommand for FakeSlashCommand {
self: Arc<Self>,
_arguments: &[String],
_cancel: Arc<AtomicBool>,
_workspace: Option<WeakView<Workspace>>,
_cx: &mut WindowContext,
_workspace: Option<WeakModel<Workspace>>,
_window: &mut gpui::Window,
_cx: &mut gpui::AppContext,
) -> Task<Result<Vec<ArgumentCompletion>>> {
Task::ready(Ok(vec![]))
}
@@ -1657,9 +1693,10 @@ impl SlashCommand for FakeSlashCommand {
_arguments: &[String],
_context_slash_command_output_sections: &[SlashCommandOutputSection<language::Anchor>],
_context_buffer: BufferSnapshot,
_workspace: WeakView<Workspace>,
_workspace: WeakModel<Workspace>,
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
_cx: &mut WindowContext,
_window: &mut gpui::Window,
_cx: &mut gpui::AppContext,
) -> Task<SlashCommandResult> {
Task::ready(Ok(SlashCommandOutput {
text: format!("Executed fake command: {}", self.0),

View File

@@ -14,9 +14,7 @@ use context_server::{ContextServerFactoryRegistry, ContextServerTool};
use fs::Fs;
use futures::StreamExt;
use fuzzy::StringMatchCandidate;
use gpui::{
AppContext, AsyncAppContext, Context as _, EventEmitter, Model, ModelContext, Task, WeakModel,
};
use gpui::{AppContext, AsyncAppContext, Context as _, EventEmitter, Model, Task, WeakModel};
use language::LanguageRegistry;
use paths::contexts_dir;
use project::Project;
@@ -109,11 +107,16 @@ impl ContextStore {
const CONTEXT_WATCH_DURATION: Duration = Duration::from_millis(100);
let (mut events, _) = fs.watch(contexts_dir(), CONTEXT_WATCH_DURATION).await;
let this = cx.new_model(|cx: &mut ModelContext<Self>| {
let this = cx.new_model(|model: &Model<Self>, cx: &mut AppContext| {
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 context_server_manager = cx.new_model(|model, cx| {
ContextServerManager::new(
context_server_factory_registry,
project.clone(),
model,
cx,
)
});
let mut this = Self {
contexts: Vec::new(),
@@ -127,10 +130,10 @@ impl ContextStore {
slash_commands,
tools,
telemetry,
_watch_updates: cx.spawn(|this, mut cx| {
_watch_updates: model.spawn(cx, |this, mut cx| {
async move {
while events.next().await.is_some() {
this.update(&mut cx, |this, cx| this.reload(cx))?
this.update(&mut cx, |this, model, cx| this.reload(model, cx))?
.await
.log_err();
}
@@ -148,12 +151,12 @@ impl ContextStore {
project: project.clone(),
prompt_builder,
};
this.handle_project_changed(project.clone(), cx);
this.synchronize_contexts(cx);
this.register_context_server_handlers(cx);
this.handle_project_changed(project.clone(), model, cx);
this.synchronize_contexts(model, cx);
this.register_context_server_handlers(model, cx);
this
})?;
this.update(&mut cx, |this, cx| this.reload(cx))?
this.update(&mut cx, |this, model, cx| this.reload(model, cx))?
.await
.log_err();
@@ -166,7 +169,7 @@ impl ContextStore {
envelope: TypedEnvelope<proto::AdvertiseContexts>,
mut cx: AsyncAppContext,
) -> Result<()> {
this.update(&mut cx, |this, cx| {
this.update(&mut cx, |this, model, cx| {
this.host_contexts = envelope
.payload
.contexts
@@ -176,7 +179,7 @@ impl ContextStore {
summary: context.summary,
})
.collect();
cx.notify();
model.notify(cx);
})
}
@@ -186,7 +189,7 @@ impl ContextStore {
mut cx: AsyncAppContext,
) -> Result<proto::OpenContextResponse> {
let context_id = ContextId::from_proto(envelope.payload.context_id);
let operations = this.update(&mut cx, |this, cx| {
let operations = this.update(&mut cx, |this, model, cx| {
if this.project.read(cx).is_via_collab() {
return Err(anyhow!("only the host contexts can be opened"));
}
@@ -215,14 +218,14 @@ impl ContextStore {
_: TypedEnvelope<proto::CreateContext>,
mut cx: AsyncAppContext,
) -> Result<proto::CreateContextResponse> {
let (context_id, operations) = this.update(&mut cx, |this, cx| {
let (context_id, operations) = this.update(&mut cx, |this, model, cx| {
if this.project.read(cx).is_via_collab() {
return Err(anyhow!("can only create contexts as the host"));
}
let context = this.create(cx);
let context = this.create(model, cx);
let context_id = context.read(cx).id().clone();
cx.emit(ContextStoreEvent::ContextCreated(context_id.clone()));
model.emit(ContextStoreEvent::ContextCreated(context_id.clone()), cx);
anyhow::Ok((
context_id,
@@ -243,12 +246,14 @@ impl ContextStore {
envelope: TypedEnvelope<proto::UpdateContext>,
mut cx: AsyncAppContext,
) -> Result<()> {
this.update(&mut cx, |this, cx| {
this.update(&mut cx, |this, model, cx| {
let context_id = ContextId::from_proto(envelope.payload.context_id);
if let Some(context) = this.loaded_context_for_id(&context_id, cx) {
let operation_proto = envelope.payload.operation.context("invalid operation")?;
let operation = ContextOperation::from_proto(operation_proto)?;
context.update(cx, |context, cx| context.apply_ops([operation], cx));
context.update(cx, |context, model, cx| {
context.apply_ops([operation], model, cx)
});
}
Ok(())
})?
@@ -259,7 +264,7 @@ impl ContextStore {
envelope: TypedEnvelope<proto::SynchronizeContexts>,
mut cx: AsyncAppContext,
) -> Result<proto::SynchronizeContextsResponse> {
this.update(&mut cx, |this, cx| {
this.update(&mut cx, |this, model, cx| {
if this.project.read(cx).is_via_collab() {
return Err(anyhow!("only the host can synchronize contexts"));
}
@@ -298,7 +303,12 @@ impl ContextStore {
})?
}
fn handle_project_changed(&mut self, _: Model<Project>, cx: &mut ModelContext<Self>) {
fn handle_project_changed(
&mut self,
_: Model<Project>,
model: &Model<Self>,
cx: &mut AppContext,
) {
let is_shared = self.project.read(cx).is_shared();
let was_shared = mem::replace(&mut self.project_is_shared, is_shared);
if is_shared == was_shared {
@@ -319,7 +329,7 @@ impl ContextStore {
.client
.subscribe_to_entity(remote_id)
.log_err()
.map(|subscription| subscription.set_model(&cx.handle(), &mut cx.to_async()));
.map(|subscription| subscription.set_model(model, &mut cx.to_async()));
self.advertise_contexts(cx);
} else {
self.client_subscription = None;
@@ -330,22 +340,23 @@ impl ContextStore {
&mut self,
_: Model<Project>,
event: &project::Event,
cx: &mut ModelContext<Self>,
model: &Model<Self>,
cx: &mut AppContext,
) {
match event {
project::Event::Reshared => {
self.advertise_contexts(cx);
}
project::Event::HostReshared | project::Event::Rejoined => {
self.synchronize_contexts(cx);
self.synchronize_contexts(model, cx);
}
project::Event::DisconnectedFromHost => {
self.contexts.retain_mut(|context| {
if let Some(strong_context) = context.upgrade() {
*context = ContextHandle::Weak(context.downgrade());
strong_context.update(cx, |context, cx| {
strong_context.update(cx, |context, model, cx| {
if context.replica_id() != ReplicaId::default() {
context.set_capability(language::Capability::ReadOnly, cx);
context.set_capability(language::Capability::ReadOnly, model, cx);
}
});
true
@@ -354,14 +365,14 @@ impl ContextStore {
}
});
self.host_contexts.clear();
cx.notify();
model.notify(cx);
}
_ => {}
}
}
pub fn create(&mut self, cx: &mut ModelContext<Self>) -> Model<Context> {
let context = cx.new_model(|cx| {
pub fn create(&mut self, model: &Model<Self>, cx: &mut AppContext) -> Model<Context> {
let context = cx.new_model(|model, cx| {
Context::local(
self.languages.clone(),
Some(self.project.clone()),
@@ -369,16 +380,18 @@ impl ContextStore {
self.prompt_builder.clone(),
self.slash_commands.clone(),
self.tools.clone(),
model,
cx,
)
});
self.register_context(&context, cx);
self.register_context(&context, model, cx);
context
}
pub fn create_remote_context(
&mut self,
cx: &mut ModelContext<Self>,
model: &Model<Self>,
cx: &mut AppContext,
) -> Task<Result<Model<Context>>> {
let project = self.project.read(cx);
let Some(project_id) = project.remote_id() else {
@@ -394,11 +407,11 @@ impl ContextStore {
let slash_commands = self.slash_commands.clone();
let tools = self.tools.clone();
let request = self.client.request(proto::CreateContext { project_id });
cx.spawn(|this, mut cx| async move {
model.spawn(cx, |this, mut cx| async move {
let response = request.await?;
let context_id = ContextId::from_proto(response.context_id);
let context_proto = response.context.context("invalid context")?;
let context = cx.new_model(|cx| {
let context = cx.new_model(|model, cx| {
Context::new(
context_id.clone(),
replica_id,
@@ -409,6 +422,7 @@ impl ContextStore {
tools,
Some(project),
Some(telemetry),
model,
cx,
)
})?;
@@ -423,12 +437,12 @@ impl ContextStore {
})
.await?;
context.update(&mut cx, |context, cx| context.apply_ops(operations, cx))?;
this.update(&mut cx, |this, cx| {
this.update(&mut cx, |this, model, cx| {
if let Some(existing_context) = this.loaded_context_for_id(&context_id, cx) {
existing_context
} else {
this.register_context(&context, cx);
this.synchronize_contexts(cx);
this.register_context(&context, model, cx);
this.synchronize_contexts(model, cx);
context
}
})
@@ -438,7 +452,8 @@ impl ContextStore {
pub fn open_local_context(
&mut self,
path: PathBuf,
cx: &ModelContext<Self>,
model: &Model<Self>,
cx: &AppContext,
) -> Task<Result<Model<Context>>> {
if let Some(existing_context) = self.loaded_context_for_path(&path, cx) {
return Task::ready(Ok(existing_context));
@@ -459,9 +474,9 @@ impl ContextStore {
let slash_commands = self.slash_commands.clone();
let tools = self.tools.clone();
cx.spawn(|this, mut cx| async move {
model.spawn(cx, |this, mut cx| async move {
let saved_context = load.await?;
let context = cx.new_model(|cx| {
let context = cx.new_model(|model, cx| {
Context::deserialize(
saved_context,
path.clone(),
@@ -471,14 +486,15 @@ impl ContextStore {
tools,
Some(project),
Some(telemetry),
model,
cx,
)
})?;
this.update(&mut cx, |this, cx| {
this.update(&mut cx, |this, model, cx| {
if let Some(existing_context) = this.loaded_context_for_path(&path, cx) {
existing_context
} else {
this.register_context(&context, cx);
this.register_context(&context, model, cx);
context
}
})
@@ -514,7 +530,8 @@ impl ContextStore {
pub fn open_remote_context(
&mut self,
context_id: ContextId,
cx: &mut ModelContext<Self>,
model: &Model<Self>,
cx: &mut AppContext,
) -> Task<Result<Model<Context>>> {
let project = self.project.read(cx);
let Some(project_id) = project.remote_id() else {
@@ -537,10 +554,10 @@ impl ContextStore {
let prompt_builder = self.prompt_builder.clone();
let slash_commands = self.slash_commands.clone();
let tools = self.tools.clone();
cx.spawn(|this, mut cx| async move {
model.spawn(cx, |this, mut cx| async move {
let response = request.await?;
let context_proto = response.context.context("invalid context")?;
let context = cx.new_model(|cx| {
let context = cx.new_model(|model, cx| {
Context::new(
context_id.clone(),
replica_id,
@@ -551,6 +568,7 @@ impl ContextStore {
tools,
Some(project),
Some(telemetry),
model,
cx,
)
})?;
@@ -565,19 +583,24 @@ impl ContextStore {
})
.await?;
context.update(&mut cx, |context, cx| context.apply_ops(operations, cx))?;
this.update(&mut cx, |this, cx| {
this.update(&mut cx, |this, model, cx| {
if let Some(existing_context) = this.loaded_context_for_id(&context_id, cx) {
existing_context
} else {
this.register_context(&context, cx);
this.synchronize_contexts(cx);
this.register_context(&context, model, cx);
this.synchronize_contexts(model, cx);
context
}
})
})
}
fn register_context(&mut self, context: &Model<Context>, cx: &mut ModelContext<Self>) {
fn register_context(
&mut self,
context: &Model<Context>,
model: &Model<Self>,
cx: &mut AppContext,
) {
let handle = if self.project_is_shared {
ContextHandle::Strong(context.clone())
} else {
@@ -592,7 +615,8 @@ impl ContextStore {
&mut self,
context: Model<Context>,
event: &ContextEvent,
cx: &mut ModelContext<Self>,
model: &Model<Self>,
cx: &mut AppContext,
) {
let Some(project_id) = self.project.read(cx).remote_id() else {
return;
@@ -651,7 +675,7 @@ impl ContextStore {
.ok();
}
fn synchronize_contexts(&mut self, cx: &mut ModelContext<Self>) {
fn synchronize_contexts(&mut self, model: &Model<Self>, cx: &mut AppContext) {
let Some(project_id) = self.project.read(cx).remote_id() else {
return;
};
@@ -674,36 +698,37 @@ impl ContextStore {
project_id,
contexts,
});
cx.spawn(|this, cx| async move {
let response = request.await?;
model
.spawn(cx, |this, cx| async move {
let response = request.await?;
let mut context_ids = Vec::new();
let mut operations = Vec::new();
this.read_with(&cx, |this, cx| {
for context_version_proto in response.contexts {
let context_version = ContextVersion::from_proto(&context_version_proto);
let context_id = ContextId::from_proto(context_version_proto.context_id);
if let Some(context) = this.loaded_context_for_id(&context_id, cx) {
context_ids.push(context_id);
operations.push(context.read(cx).serialize_ops(&context_version, cx));
let mut context_ids = Vec::new();
let mut operations = Vec::new();
this.read_with(&cx, |this, cx| {
for context_version_proto in response.contexts {
let context_version = ContextVersion::from_proto(&context_version_proto);
let context_id = ContextId::from_proto(context_version_proto.context_id);
if let Some(context) = this.loaded_context_for_id(&context_id, cx) {
context_ids.push(context_id);
operations.push(context.read(cx).serialize_ops(&context_version, cx));
}
}
})?;
let operations = futures::future::join_all(operations).await;
for (context_id, operations) in context_ids.into_iter().zip(operations) {
for operation in operations {
client.send(proto::UpdateContext {
project_id,
context_id: context_id.to_proto(),
operation: Some(operation),
})?;
}
}
})?;
let operations = futures::future::join_all(operations).await;
for (context_id, operations) in context_ids.into_iter().zip(operations) {
for operation in operations {
client.send(proto::UpdateContext {
project_id,
context_id: context_id.to_proto(),
operation: Some(operation),
})?;
}
}
anyhow::Ok(())
})
.detach_and_log_err(cx);
anyhow::Ok(())
})
.detach_and_log_err(cx);
}
pub fn search(&self, query: String, cx: &AppContext) -> Task<Vec<SavedContextMetadata>> {
@@ -740,9 +765,9 @@ impl ContextStore {
&self.host_contexts
}
fn reload(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
fn reload(&mut self, model: &Model<Self>, cx: &mut AppContext) -> Task<Result<()>> {
let fs = self.fs.clone();
cx.spawn(|this, mut cx| async move {
model.spawn(cx, |this, mut cx| async move {
fs.create_dir(contexts_dir()).await?;
let mut paths = fs.read_dir(contexts_dir()).await?;
@@ -778,14 +803,14 @@ impl ContextStore {
}
contexts.sort_unstable_by_key(|context| Reverse(context.mtime));
this.update(&mut cx, |this, cx| {
this.update(&mut cx, |this, model, cx| {
this.contexts_metadata = contexts;
cx.notify();
model.notify(cx);
})
})
}
pub fn restart_context_servers(&mut self, cx: &mut ModelContext<Self>) {
pub fn restart_context_servers(&mut self, model: &Model<Self>, cx: &mut AppContext) {
cx.update_model(
&self.context_server_manager,
|context_server_manager, cx| {
@@ -798,7 +823,7 @@ impl ContextStore {
);
}
fn register_context_server_handlers(&self, cx: &mut ModelContext<Self>) {
fn register_context_server_handlers(&self, model: &Model<Self>, cx: &mut AppContext) {
cx.subscribe(
&self.context_server_manager.clone(),
Self::handle_context_server_event,
@@ -810,7 +835,8 @@ impl ContextStore {
&mut self,
context_server_manager: Model<ContextServerManager>,
event: &context_server::manager::Event,
cx: &mut ModelContext<Self>,
model: &Model<Self>,
cx: &mut AppContext,
) {
let slash_command_working_set = self.slash_commands.clone();
let tool_working_set = self.tools.clone();

File diff suppressed because it is too large Load Diff

View File

@@ -140,7 +140,7 @@ impl ResolvedPatch {
edits.push((suggestion.range.clone(), suggestion.new_text.clone()));
}
}
buffer.update(cx, |buffer, cx| {
buffer.update(cx, |buffer, model, cx| {
buffer.edit(
edits,
Some(AutoindentMode::Block {
@@ -461,7 +461,7 @@ impl AssistantPatch {
// Expand the context ranges of each edit and group edits with overlapping context ranges.
let mut edit_groups_by_buffer = HashMap::default();
for (buffer, edits) in edits_by_buffer {
if let Ok(snapshot) = buffer.update(cx, |buffer, _| buffer.text_snapshot()) {
if let Ok(snapshot) = buffer.update(cx, |buffer, model, _| buffer.text_snapshot()) {
edit_groups_by_buffer.insert(buffer, Self::group_edits(edits, &snapshot));
}
}
@@ -918,7 +918,7 @@ mod tests {
cx: &mut AppContext,
) {
let (text, _) = marked_text_ranges(text_with_expected_range, false);
let buffer = cx.new_model(|cx| Buffer::local(text.clone(), cx));
let buffer = cx.new_model(|model, cx| Buffer::local(text.clone(), model, cx));
let snapshot = buffer.read(cx).snapshot();
let range = AssistantEditKind::resolve_location(&snapshot, query).to_offset(&snapshot);
let text_with_actual_range = generate_marked_text(&text, &[range], false);
@@ -932,8 +932,9 @@ mod tests {
new_text: String,
cx: &mut AppContext,
) {
let buffer =
cx.new_model(|cx| Buffer::local(old_text, cx).with_language(Arc::new(rust_lang()), cx));
let buffer = cx.new_model(|model, cx| {
Buffer::local(old_text, model, cx).with_language(Arc::new(rust_lang()), model, cx)
});
let snapshot = buffer.read(cx).snapshot();
let resolved_edits = edits
.into_iter()

View File

@@ -38,8 +38,8 @@ use std::{
use text::LineEnding;
use theme::ThemeSettings;
use ui::{
div, prelude::*, IconButtonShape, KeyBinding, ListItem, ListItemSpacing, ParentElement, Render,
SharedString, Styled, Tooltip, ViewContext, VisualContext,
div, prelude::*, AppContext, IconButtonShape, KeyBinding, ListItem, ListItemSpacing,
ParentElement, Render, SharedString, Styled, Tooltip, VisualContext,
};
use util::{ResultExt, TryFutureExt};
use uuid::Uuid;
@@ -88,7 +88,7 @@ pub fn open_prompt_library(
.find_map(|window| window.downcast::<PromptLibrary>());
if let Some(existing_window) = existing_window {
existing_window
.update(cx, |_, cx| cx.activate_window())
.update(cx, |_, model, cx| cx.activate_window())
.ok();
Task::ready(Ok(existing_window))
} else {
@@ -109,7 +109,11 @@ pub fn open_prompt_library(
window_bounds: Some(WindowBounds::Windowed(bounds)),
..Default::default()
},
|cx| cx.new_view(|cx| PromptLibrary::new(store, language_registry, cx)),
|cx| {
cx.new_model(|model, cx| {
PromptLibrary::new(store, language_registry, model, cx)
})
},
)
})?
})
@@ -121,14 +125,14 @@ pub struct PromptLibrary {
language_registry: Arc<LanguageRegistry>,
prompt_editors: HashMap<PromptId, PromptEditor>,
active_prompt_id: Option<PromptId>,
picker: View<Picker<PromptPickerDelegate>>,
picker: Model<Picker<PromptPickerDelegate>>,
pending_load: Task<()>,
_subscriptions: Vec<Subscription>,
}
struct PromptEditor {
title_editor: View<Editor>,
body_editor: View<Editor>,
title_editor: Model<Editor>,
body_editor: Model<Editor>,
token_count: Option<usize>,
pending_token_count: Task<Option<()>>,
next_title_and_body_to_save: Option<(String, Rope)>,
@@ -158,7 +162,11 @@ impl PickerDelegate for PromptPickerDelegate {
self.matches.len()
}
fn no_matches_text(&self, _cx: &mut WindowContext) -> SharedString {
fn no_matches_text(
&self,
_window: &mut gpui::Window,
_cx: &mut gpui::AppContext,
) -> SharedString {
if self.store.prompt_count() == 0 {
"No prompts.".into()
} else {
@@ -170,23 +178,31 @@ impl PickerDelegate for PromptPickerDelegate {
self.selected_index
}
fn set_selected_index(&mut self, ix: usize, cx: &mut ViewContext<Picker<Self>>) {
fn set_selected_index(&mut self, ix: usize, model: &Model<Picker>, cx: &mut AppContext) {
self.selected_index = ix;
if let Some(prompt) = self.matches.get(self.selected_index) {
cx.emit(PromptPickerEvent::Selected {
prompt_id: prompt.id,
});
model.emit(
cx,
PromptPickerEvent::Selected {
prompt_id: prompt.id,
},
);
}
}
fn placeholder_text(&self, _cx: &mut WindowContext) -> Arc<str> {
fn placeholder_text(&self, _window: &mut gpui::Window, _cx: &mut gpui::AppContext) -> Arc<str> {
"Search...".into()
}
fn update_matches(&mut self, query: String, cx: &mut ViewContext<Picker<Self>>) -> Task<()> {
fn update_matches(
&mut self,
query: String,
model: &Model<Picker>,
cx: &mut AppContext,
) -> Task<()> {
let search = self.store.search(query);
let prev_prompt_id = self.matches.get(self.selected_index).map(|mat| mat.id);
cx.spawn(|this, mut cx| async move {
model.spawn(cx, |this, mut cx| async move {
let (matches, selected_index) = cx
.background_executor()
.spawn(async move {
@@ -201,30 +217,34 @@ impl PickerDelegate for PromptPickerDelegate {
})
.await;
this.update(&mut cx, |this, cx| {
this.update(&mut cx, |this, model, cx| {
this.delegate.matches = matches;
this.delegate.set_selected_index(selected_index, cx);
cx.notify();
model.notify(cx);
})
.ok();
})
}
fn confirm(&mut self, _secondary: bool, cx: &mut ViewContext<Picker<Self>>) {
fn confirm(&mut self, _secondary: bool, model: &Model<Picker>, cx: &mut AppContext) {
if let Some(prompt) = self.matches.get(self.selected_index) {
cx.emit(PromptPickerEvent::Confirmed {
prompt_id: prompt.id,
});
model.emit(
cx,
PromptPickerEvent::Confirmed {
prompt_id: prompt.id,
},
);
}
}
fn dismissed(&mut self, _cx: &mut ViewContext<Picker<Self>>) {}
fn dismissed(&mut self, model: &Model<Picker>, _cx: &mut AppContext) {}
fn render_match(
&self,
ix: usize,
selected: bool,
cx: &mut ViewContext<Picker<Self>>,
model: &Model<Picker>,
cx: &mut AppContext,
) -> Option<Self::ListItem> {
let prompt = self.matches.get(ix)?;
let default = prompt.default;
@@ -241,9 +261,9 @@ impl PickerDelegate for PromptPickerDelegate {
.selected(true)
.icon_color(Color::Accent)
.shape(IconButtonShape::Square)
.tooltip(move |cx| Tooltip::text("Remove from Default Prompt", cx))
.on_click(cx.listener(move |_, _, cx| {
cx.emit(PromptPickerEvent::ToggledDefault { prompt_id })
.tooltip(move |window, cx| Tooltip::text("Remove from Default Prompt", cx))
.on_click(model.listener(move |_, _, model, window, cx| {
model.emit(PromptPickerEvent::ToggledDefault { prompt_id }, cx)
}))
}))
.end_hover_slot(
@@ -253,11 +273,12 @@ impl PickerDelegate for PromptPickerDelegate {
div()
.id("built-in-prompt")
.child(Icon::new(IconName::FileLock).color(Color::Muted))
.tooltip(move |cx| {
.tooltip(move |window, cx| {
Tooltip::with_meta(
"Built-in prompt",
None,
BUILT_IN_TOOLTIP_TEXT,
window,
cx,
)
})
@@ -266,9 +287,9 @@ impl PickerDelegate for PromptPickerDelegate {
IconButton::new("delete-prompt", IconName::Trash)
.icon_color(Color::Muted)
.shape(IconButtonShape::Square)
.tooltip(move |cx| Tooltip::text("Delete Prompt", cx))
.on_click(cx.listener(move |_, _, cx| {
cx.emit(PromptPickerEvent::Deleted { prompt_id })
.tooltip(move |window, cx| Tooltip::text("Delete Prompt", cx))
.on_click(model.listener(move |_, _, model, window, cx| {
model.emit(PromptPickerEvent::Deleted { prompt_id }, cx)
}))
.into_any_element()
})
@@ -278,7 +299,7 @@ impl PickerDelegate for PromptPickerDelegate {
.selected_icon(IconName::SparkleFilled)
.icon_color(if default { Color::Accent } else { Color::Muted })
.shape(IconButtonShape::Square)
.tooltip(move |cx| {
.tooltip(move |window, cx| {
Tooltip::text(
if default {
"Remove from Default Prompt"
@@ -288,15 +309,20 @@ impl PickerDelegate for PromptPickerDelegate {
cx,
)
})
.on_click(cx.listener(move |_, _, cx| {
cx.emit(PromptPickerEvent::ToggledDefault { prompt_id })
.on_click(model.listener(move |_, _, model, window, cx| {
model.emit(PromptPickerEvent::ToggledDefault { prompt_id }, cx)
})),
),
);
Some(element)
}
fn render_editor(&self, editor: &View<Editor>, cx: &mut ViewContext<Picker<Self>>) -> Div {
fn render_editor(
&self,
editor: &Model<Editor>,
model: &Model<Picker>,
cx: &mut AppContext,
) -> Div {
h_flex()
.bg(cx.theme().colors().editor_background)
.rounded_md()
@@ -313,7 +339,8 @@ impl PromptLibrary {
fn new(
store: Arc<PromptStore>,
language_registry: Arc<LanguageRegistry>,
cx: &mut ViewContext<Self>,
model: &Model<Self>,
cx: &mut AppContext,
) -> Self {
let delegate = PromptPickerDelegate {
store: store.clone(),
@@ -321,11 +348,11 @@ impl PromptLibrary {
matches: Vec::new(),
};
let picker = cx.new_view(|cx| {
let picker = Picker::uniform_list(delegate, cx)
let picker = cx.new_model(|model, cx| {
let picker = Picker::uniform_list(delegate, model, cx)
.modal(false)
.max_height(None);
picker.focus(cx);
picker.focus(window, cx);
picker
});
Self {
@@ -341,47 +368,52 @@ impl PromptLibrary {
fn handle_picker_event(
&mut self,
_: View<Picker<PromptPickerDelegate>>,
_: Model<Picker<PromptPickerDelegate>>,
event: &PromptPickerEvent,
cx: &mut ViewContext<Self>,
model: &Model<Self>,
cx: &mut AppContext,
) {
match event {
PromptPickerEvent::Selected { prompt_id } => {
self.load_prompt(*prompt_id, false, cx);
self.load_prompt(*prompt_id, false, model, cx);
}
PromptPickerEvent::Confirmed { prompt_id } => {
self.load_prompt(*prompt_id, true, cx);
self.load_prompt(*prompt_id, true, model, cx);
}
PromptPickerEvent::ToggledDefault { prompt_id } => {
self.toggle_default_for_prompt(*prompt_id, cx);
self.toggle_default_for_prompt(*prompt_id, model, cx);
}
PromptPickerEvent::Deleted { prompt_id } => {
self.delete_prompt(*prompt_id, cx);
self.delete_prompt(*prompt_id, model, cx);
}
}
}
pub fn new_prompt(&mut self, cx: &mut ViewContext<Self>) {
pub fn new_prompt(&mut self, model: &Model<Self>, cx: &mut AppContext) {
// If we already have an untitled prompt, use that instead
// of creating a new one.
if let Some(metadata) = self.store.first() {
if metadata.title.is_none() {
self.load_prompt(metadata.id, true, cx);
self.load_prompt(metadata.id, true, model, cx);
return;
}
}
let prompt_id = PromptId::new();
let save = self.store.save(prompt_id, None, false, "".into());
self.picker.update(cx, |picker, cx| picker.refresh(cx));
cx.spawn(|this, mut cx| async move {
save.await?;
this.update(&mut cx, |this, cx| this.load_prompt(prompt_id, true, cx))
})
.detach_and_log_err(cx);
self.picker
.update(cx, |picker, model, cx| picker.refresh(cx));
model
.spawn(cx, |this, mut cx| async move {
save.await?;
this.update(&mut cx, |this, model, cx| {
this.load_prompt(prompt_id, true, cx)
})
})
.detach_and_log_err(cx);
}
pub fn save_prompt(&mut self, prompt_id: PromptId, cx: &mut ViewContext<Self>) {
pub fn save_prompt(&mut self, prompt_id: PromptId, model: &Model<Self>, cx: &mut AppContext) {
const SAVE_THROTTLE: Duration = Duration::from_millis(500);
if prompt_id.is_built_in() {
@@ -391,7 +423,7 @@ impl PromptLibrary {
let prompt_metadata = self.store.metadata(prompt_id).unwrap();
let prompt_editor = self.prompt_editors.get_mut(&prompt_id).unwrap();
let title = prompt_editor.title_editor.read(cx).text(cx);
let body = prompt_editor.body_editor.update(cx, |editor, cx| {
let body = prompt_editor.body_editor.update(cx, |editor, model, cx| {
editor
.buffer()
.read(cx)
@@ -407,10 +439,10 @@ impl PromptLibrary {
prompt_editor.next_title_and_body_to_save = Some((title, body));
if prompt_editor.pending_save.is_none() {
prompt_editor.pending_save = Some(cx.spawn(|this, mut cx| {
prompt_editor.pending_save = Some(model.spawn(cx, |this, mut cx| {
async move {
loop {
let title_and_body = this.update(&mut cx, |this, _| {
let title_and_body = this.update(&mut cx, |this, _, _| {
this.prompt_editors
.get_mut(&prompt_id)?
.next_title_and_body_to_save
@@ -427,9 +459,10 @@ impl PromptLibrary {
.save(prompt_id, title, prompt_metadata.default, body)
.await
.log_err();
this.update(&mut cx, |this, cx| {
this.picker.update(cx, |picker, cx| picker.refresh(cx));
cx.notify();
this.update(&mut cx, |this, model, cx| {
this.picker
.update(cx, |picker, model, cx| picker.refresh(cx));
model.notify(cx);
})?;
executor.timer(SAVE_THROTTLE).await;
@@ -449,77 +482,90 @@ impl PromptLibrary {
}
}
pub fn delete_active_prompt(&mut self, cx: &mut ViewContext<Self>) {
pub fn delete_active_prompt(&mut self, model: &Model<Self>, cx: &mut AppContext) {
if let Some(active_prompt_id) = self.active_prompt_id {
self.delete_prompt(active_prompt_id, cx);
self.delete_prompt(active_prompt_id, model, cx);
}
}
pub fn duplicate_active_prompt(&mut self, cx: &mut ViewContext<Self>) {
pub fn duplicate_active_prompt(&mut self, model: &Model<Self>, cx: &mut AppContext) {
if let Some(active_prompt_id) = self.active_prompt_id {
self.duplicate_prompt(active_prompt_id, cx);
self.duplicate_prompt(active_prompt_id, model, cx);
}
}
pub fn toggle_default_for_active_prompt(&mut self, cx: &mut ViewContext<Self>) {
pub fn toggle_default_for_active_prompt(&mut self, model: &Model<Self>, cx: &mut AppContext) {
if let Some(active_prompt_id) = self.active_prompt_id {
self.toggle_default_for_prompt(active_prompt_id, cx);
self.toggle_default_for_prompt(active_prompt_id, model, cx);
}
}
pub fn toggle_default_for_prompt(&mut self, prompt_id: PromptId, cx: &mut ViewContext<Self>) {
pub fn toggle_default_for_prompt(
&mut self,
prompt_id: PromptId,
model: &Model<Self>,
cx: &mut AppContext,
) {
if let Some(prompt_metadata) = self.store.metadata(prompt_id) {
self.store
.save_metadata(prompt_id, prompt_metadata.title, !prompt_metadata.default)
.detach_and_log_err(cx);
self.picker.update(cx, |picker, cx| picker.refresh(cx));
cx.notify();
self.picker
.update(cx, |picker, model, cx| picker.refresh(cx));
model.notify(cx);
}
}
pub fn load_prompt(&mut self, prompt_id: PromptId, focus: bool, cx: &mut ViewContext<Self>) {
pub fn load_prompt(
&mut self,
prompt_id: PromptId,
focus: bool,
model: &Model<Self>,
cx: &mut AppContext,
) {
if let Some(prompt_editor) = self.prompt_editors.get(&prompt_id) {
if focus {
prompt_editor
.body_editor
.update(cx, |editor, cx| editor.focus(cx));
.update(cx, |editor, model, cx| editor.focus(window, cx));
}
self.set_active_prompt(Some(prompt_id), cx);
self.set_active_prompt(Some(prompt_id), model, cx);
} else if let Some(prompt_metadata) = self.store.metadata(prompt_id) {
let language_registry = self.language_registry.clone();
let prompt = self.store.load(prompt_id);
self.pending_load = cx.spawn(|this, mut cx| async move {
self.pending_load = model.spawn(cx, |this, mut cx| async move {
let prompt = prompt.await;
let markdown = language_registry.language_for_name("Markdown").await;
this.update(&mut cx, |this, cx| match prompt {
this.update(&mut cx, |this, model, cx| match prompt {
Ok(prompt) => {
let title_editor = cx.new_view(|cx| {
let mut editor = Editor::auto_width(cx);
editor.set_placeholder_text("Untitled", cx);
editor.set_text(prompt_metadata.title.unwrap_or_default(), cx);
let title_editor = cx.new_model(|model, cx| {
let mut editor = Editor::auto_width(model, cx);
editor.set_placeholder_text("Untitled", model, cx);
editor.set_text(prompt_metadata.title.unwrap_or_default(), model, cx);
if prompt_id.is_built_in() {
editor.set_read_only(true);
editor.set_show_inline_completions(Some(false), cx);
editor.set_show_inline_completions(Some(false), model, cx);
}
editor
});
let body_editor = cx.new_view(|cx| {
let buffer = cx.new_model(|cx| {
let mut buffer = Buffer::local(prompt, cx);
buffer.set_language(markdown.log_err(), cx);
let body_editor = cx.new_model(|model, cx| {
// todo!: Remove this type annotation on cx
let buffer = cx.new_model(|model, cx: &mut AppContext| {
let mut buffer = Buffer::local(prompt, model, cx);
buffer.set_language(markdown.log_err(), model, cx);
buffer.set_language_registry(language_registry);
buffer
});
let mut editor = Editor::for_buffer(buffer, None, cx);
let mut editor = Editor::for_buffer(buffer, None, model, cx);
if prompt_id.is_built_in() {
editor.set_read_only(true);
editor.set_show_inline_completions(Some(false), cx);
editor.set_show_inline_completions(Some(false), model, cx);
}
editor.set_soft_wrap_mode(SoftWrap::EditorWidth, cx);
editor.set_show_gutter(false, cx);
editor.set_show_wrap_guides(false, cx);
editor.set_show_indent_guides(false, cx);
editor.set_soft_wrap_mode(SoftWrap::EditorWidth, model, cx);
editor.set_show_gutter(false, model, cx);
editor.set_show_wrap_guides(false, model, cx);
editor.set_show_indent_guides(false, model, cx);
editor.set_use_modal_editing(false);
editor.set_current_line_highlight(Some(CurrentLineHighlight::None));
editor.set_completion_provider(Some(Box::new(
@@ -530,7 +576,7 @@ impl PromptLibrary {
),
)));
if focus {
editor.focus(cx);
editor.focus(window, cx);
}
editor
});
@@ -567,9 +613,14 @@ impl PromptLibrary {
}
}
fn set_active_prompt(&mut self, prompt_id: Option<PromptId>, cx: &mut ViewContext<Self>) {
fn set_active_prompt(
&mut self,
prompt_id: Option<PromptId>,
model: &Model<Self>,
cx: &mut AppContext,
) {
self.active_prompt_id = prompt_id;
self.picker.update(cx, |picker, cx| {
self.picker.update(cx, |picker, model, cx| {
if let Some(prompt_id) = prompt_id {
if picker
.delegate
@@ -589,13 +640,13 @@ impl PromptLibrary {
}
}
} else {
picker.focus(cx);
picker.focus(window);
}
});
cx.notify();
model.notify(cx);
}
pub fn delete_prompt(&mut self, prompt_id: PromptId, cx: &mut ViewContext<Self>) {
pub fn delete_prompt(&mut self, prompt_id: PromptId, model: &Model<Self>, cx: &mut AppContext) {
if let Some(metadata) = self.store.metadata(prompt_id) {
let confirmation = cx.prompt(
PromptLevel::Warning,
@@ -607,25 +658,32 @@ impl PromptLibrary {
&["Delete", "Cancel"],
);
cx.spawn(|this, mut cx| async move {
if confirmation.await.ok() == Some(0) {
this.update(&mut cx, |this, cx| {
if this.active_prompt_id == Some(prompt_id) {
this.set_active_prompt(None, cx);
}
this.prompt_editors.remove(&prompt_id);
this.store.delete(prompt_id).detach_and_log_err(cx);
this.picker.update(cx, |picker, cx| picker.refresh(cx));
cx.notify();
})?;
}
anyhow::Ok(())
})
.detach_and_log_err(cx);
model
.spawn(cx, |this, mut cx| async move {
if confirmation.await.ok() == Some(0) {
this.update(&mut cx, |this, model, cx| {
if this.active_prompt_id == Some(prompt_id) {
this.set_active_prompt(None, cx);
}
this.prompt_editors.remove(&prompt_id);
this.store.delete(prompt_id).detach_and_log_err(cx);
this.picker
.update(cx, |picker, model, cx| picker.refresh(cx));
model.notify(cx);
})?;
}
anyhow::Ok(())
})
.detach_and_log_err(cx);
}
}
pub fn duplicate_prompt(&mut self, prompt_id: PromptId, cx: &mut ViewContext<Self>) {
pub fn duplicate_prompt(
&mut self,
prompt_id: PromptId,
model: &Model<Self>,
cx: &mut AppContext,
) {
if let Some(prompt) = self.prompt_editors.get(&prompt_id) {
const DUPLICATE_SUFFIX: &str = " copy";
let title_to_duplicate = prompt.title_editor.read(cx).text(cx);
@@ -655,31 +713,39 @@ impl PromptLibrary {
let save = self
.store
.save(new_id, Some(title.into()), false, body.into());
self.picker.update(cx, |picker, cx| picker.refresh(cx));
cx.spawn(|this, mut cx| async move {
save.await?;
this.update(&mut cx, |prompt_library, cx| {
prompt_library.load_prompt(new_id, true, cx)
self.picker
.update(cx, |picker, model, cx| picker.refresh(cx));
model
.spawn(cx, |this, mut cx| async move {
save.await?;
this.update(&mut cx, |prompt_library, cx| {
prompt_library.load_prompt(new_id, true, cx)
})
})
})
.detach_and_log_err(cx);
.detach_and_log_err(cx);
}
}
fn focus_active_prompt(&mut self, _: &Tab, cx: &mut ViewContext<Self>) {
fn focus_active_prompt(&mut self, _: &Tab, model: &Model<Self>, cx: &mut AppContext) {
if let Some(active_prompt) = self.active_prompt_id {
self.prompt_editors[&active_prompt]
.body_editor
.update(cx, |editor, cx| editor.focus(cx));
.update(cx, |editor, model, cx| editor.focus(window, cx));
cx.stop_propagation();
}
}
fn focus_picker(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) {
self.picker.update(cx, |picker, cx| picker.focus(cx));
fn focus_picker(&mut self, _: &menu::Cancel, model: &Model<Self>, cx: &mut AppContext) {
self.picker
.update(cx, |picker, model, cx| picker.focus(window));
}
pub fn inline_assist(&mut self, action: &InlineAssist, cx: &mut ViewContext<Self>) {
pub fn inline_assist(
&mut self,
action: &InlineAssist,
model: &Model<Self>,
cx: &mut AppContext,
) {
let Some(active_prompt_id) = self.active_prompt_id else {
cx.propagate();
return;
@@ -693,13 +759,13 @@ impl PromptLibrary {
let initial_prompt = action.prompt.clone();
if provider.is_authenticated(cx) {
InlineAssistant::update_global(cx, |assistant, cx| {
assistant.assist(&prompt_editor, None, None, initial_prompt, cx)
assistant.assist(&prompt_editor, None, None, initial_prompt, window, cx)
})
} else {
for window in cx.windows() {
if let Some(workspace) = window.downcast::<Workspace>() {
let panel = workspace
.update(cx, |workspace, cx| {
.update(cx, |workspace, model, cx| {
cx.activate_window();
workspace.focus_panel::<AssistantPanel>(cx)
})
@@ -713,7 +779,12 @@ impl PromptLibrary {
}
}
fn move_down_from_title(&mut self, _: &editor::actions::MoveDown, cx: &mut ViewContext<Self>) {
fn move_down_from_title(
&mut self,
_: &editor::actions::MoveDown,
model: &Model<Self>,
cx: &mut AppContext,
) {
if let Some(prompt_id) = self.active_prompt_id {
if let Some(prompt_editor) = self.prompt_editors.get(&prompt_id) {
cx.focus_view(&prompt_editor.body_editor);
@@ -721,7 +792,12 @@ impl PromptLibrary {
}
}
fn move_up_from_body(&mut self, _: &editor::actions::MoveUp, cx: &mut ViewContext<Self>) {
fn move_up_from_body(
&mut self,
_: &editor::actions::MoveUp,
model: &Model<Self>,
cx: &mut AppContext,
) {
if let Some(prompt_id) = self.active_prompt_id {
if let Some(prompt_editor) = self.prompt_editors.get(&prompt_id) {
cx.focus_view(&prompt_editor.title_editor);
@@ -732,18 +808,19 @@ impl PromptLibrary {
fn handle_prompt_title_editor_event(
&mut self,
prompt_id: PromptId,
title_editor: View<Editor>,
title_editor: Model<Editor>,
event: &EditorEvent,
cx: &mut ViewContext<Self>,
model: &Model<Self>,
cx: &mut AppContext,
) {
match event {
EditorEvent::BufferEdited => {
self.save_prompt(prompt_id, cx);
self.count_tokens(prompt_id, cx);
self.save_prompt(prompt_id, model, cx);
self.count_tokens(prompt_id, model, cx);
}
EditorEvent::Blurred => {
title_editor.update(cx, |title_editor, cx| {
title_editor.change_selections(None, cx, |selections| {
title_editor.update(cx, |title_editor, model, cx| {
title_editor.change_selections(None, model, cx, |selections| {
let cursor = selections.oldest_anchor().head();
selections.select_anchor_ranges([cursor..cursor]);
});
@@ -756,18 +833,19 @@ impl PromptLibrary {
fn handle_prompt_body_editor_event(
&mut self,
prompt_id: PromptId,
body_editor: View<Editor>,
body_editor: Model<Editor>,
event: &EditorEvent,
cx: &mut ViewContext<Self>,
model: &Model<Self>,
cx: &mut AppContext,
) {
match event {
EditorEvent::BufferEdited => {
self.save_prompt(prompt_id, cx);
self.count_tokens(prompt_id, cx);
self.save_prompt(prompt_id, model, cx);
self.count_tokens(prompt_id, model, cx);
}
EditorEvent::Blurred => {
body_editor.update(cx, |body_editor, cx| {
body_editor.change_selections(None, cx, |selections| {
body_editor.update(cx, |body_editor, model, cx| {
body_editor.change_selections(None, model, cx, |selections| {
let cursor = selections.oldest_anchor().head();
selections.select_anchor_ranges([cursor..cursor]);
});
@@ -777,7 +855,7 @@ impl PromptLibrary {
}
}
fn count_tokens(&mut self, prompt_id: PromptId, cx: &mut ViewContext<Self>) {
fn count_tokens(&mut self, prompt_id: PromptId, model: &Model<Self>, cx: &mut AppContext) {
let Some(model) = LanguageModelRegistry::read_global(cx).active_model() else {
return;
};
@@ -785,7 +863,7 @@ impl PromptLibrary {
let editor = &prompt.body_editor.read(cx);
let buffer = &editor.buffer().read(cx).as_singleton().unwrap().read(cx);
let body = buffer.as_rope().clone();
prompt.pending_token_count = cx.spawn(|this, mut cx| {
prompt.pending_token_count = model.spawn(cx, |this, mut cx| {
async move {
const DEBOUNCE_TIMEOUT: Duration = Duration::from_secs(1);
@@ -808,10 +886,10 @@ impl PromptLibrary {
})?
.await?;
this.update(&mut cx, |this, cx| {
this.update(&mut cx, |this, model, cx| {
let prompt_editor = this.prompt_editors.get_mut(&prompt_id).unwrap();
prompt_editor.token_count = Some(token_count);
cx.notify();
model.notify(cx);
})
}
.log_err()
@@ -819,7 +897,7 @@ impl PromptLibrary {
}
}
fn render_prompt_list(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
fn render_prompt_list(&mut self, model: &Model<Self>, cx: &mut AppContext) -> impl IntoElement {
v_flex()
.id("prompt-list")
.capture_action(cx.listener(Self::focus_active_prompt))
@@ -839,7 +917,9 @@ impl PromptLibrary {
IconButton::new("new-prompt", IconName::Plus)
.style(ButtonStyle::Transparent)
.shape(IconButtonShape::Square)
.tooltip(move |cx| Tooltip::for_action("New Prompt", &NewPrompt, cx))
.tooltip(move |window, cx| {
Tooltip::for_action("New Prompt", &NewPrompt, window, cx)
})
.on_click(|_, cx| {
cx.dispatch_action(Box::new(NewPrompt));
}),
@@ -848,7 +928,11 @@ impl PromptLibrary {
.child(div().flex_grow().child(self.picker.clone()))
}
fn render_active_prompt(&mut self, cx: &mut ViewContext<PromptLibrary>) -> gpui::Stateful<Div> {
fn render_active_prompt(
&mut self,
model: &Model<PromptLibrary>,
cx: &mut AppContext,
) -> gpui::Stateful<Div> {
div()
.w_2_3()
.h_full()
@@ -873,7 +957,7 @@ impl PromptLibrary {
.overflow_hidden()
.pl(DynamicSpacing::Base16.rems(cx))
.pt(DynamicSpacing::Base08.rems(cx))
.on_click(cx.listener(move |_, _, cx| {
.on_click(model.listener(move |_, _, model, window, cx| {
cx.focus(&focus_handle);
}))
.child(
@@ -927,7 +1011,7 @@ impl PromptLibrary {
syntax: cx.theme().syntax().clone(),
status: cx.theme().status().clone(),
inlay_hints_style:
editor::make_inlay_hints_style(cx),
editor::make_inlay_hints_style(model, cx),
suggestions_style: HighlightStyle {
color: Some(cx.theme().status().predictive),
..HighlightStyle::default()
@@ -959,7 +1043,7 @@ impl PromptLibrary {
h_flex()
.id("token_count")
.tooltip(move |cx| {
.tooltip(move |window, cx| {
let token_count =
token_count.clone();
@@ -978,6 +1062,7 @@ impl PromptLibrary {
.0)
.unwrap_or_default()
),
model,
cx,
)
})
@@ -997,11 +1082,12 @@ impl PromptLibrary {
Icon::new(IconName::FileLock)
.color(Color::Muted),
)
.tooltip(move |cx| {
.tooltip(move |window, cx| {
Tooltip::with_meta(
"Built-in prompt",
None,
BUILT_IN_TOOLTIP_TEXT,
model,
cx,
)
})
@@ -1015,10 +1101,11 @@ impl PromptLibrary {
.style(ButtonStyle::Transparent)
.shape(IconButtonShape::Square)
.size(ButtonSize::Large)
.tooltip(move |cx| {
.tooltip(move |window, cx| {
Tooltip::for_action(
"Delete Prompt",
&DeletePrompt,
model,
cx,
)
})
@@ -1036,10 +1123,11 @@ impl PromptLibrary {
.style(ButtonStyle::Transparent)
.shape(IconButtonShape::Square)
.size(ButtonSize::Large)
.tooltip(move |cx| {
.tooltip(move |window, cx| {
Tooltip::for_action(
"Duplicate Prompt",
&DuplicatePrompt,
model,
cx,
)
})
@@ -1064,7 +1152,7 @@ impl PromptLibrary {
})
.shape(IconButtonShape::Square)
.size(ButtonSize::Large)
.tooltip(move |cx| {
.tooltip(move |window, cx| {
Tooltip::text(
if prompt_metadata.default {
"Remove from Default Prompt"
@@ -1098,8 +1186,13 @@ impl PromptLibrary {
}
impl Render for PromptLibrary {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let ui_font = theme::setup_ui_font(cx);
fn render(
&mut self,
model: &Model<Self>,
window: &mut gpui::Window,
cx: &mut AppContext,
) -> impl IntoElement {
let ui_font = theme::setup_ui_font(window, cx);
let theme = cx.theme().clone();
h_flex()
@@ -1115,7 +1208,7 @@ impl Render for PromptLibrary {
.overflow_hidden()
.font(ui_font)
.text_color(theme.colors().text)
.child(self.render_prompt_list(cx))
.child(self.render_prompt_list(model, cx))
.map(|el| {
if self.store.prompt_count() == 0 {
el.child(
@@ -1151,7 +1244,7 @@ impl Render for PromptLibrary {
Button::new("create-prompt", "New Prompt")
.full_width()
.key_binding(KeyBinding::for_action(
&NewPrompt, cx,
&NewPrompt, window, cx,
))
.on_click(|_, cx| {
cx.dispatch_action(NewPrompt.boxed_clone())
@@ -1162,7 +1255,7 @@ impl Render for PromptLibrary {
),
)
} else {
el.child(self.render_active_prompt(cx))
el.child(self.render_active_prompt(model, cx))
}
})
}

View File

@@ -5,7 +5,7 @@ use assistant_slash_command::AfterCompletion;
pub use assistant_slash_command::{SlashCommand, SlashCommandOutput};
use editor::{CompletionProvider, Editor};
use fuzzy::{match_strings, StringMatchCandidate};
use gpui::{AppContext, Model, Task, ViewContext, WeakView, WindowContext};
use gpui::{AppContext, AppContext, Model, Task, WeakView};
use language::{Anchor, Buffer, CodeLabel, Documentation, HighlightId, LanguageServerId, ToPoint};
use parking_lot::{Mutex, RwLock};
use project::CompletionIntent;
@@ -41,8 +41,8 @@ pub mod terminal_command;
pub(crate) struct SlashCommandCompletionProvider {
cancel_flag: Mutex<Arc<AtomicBool>>,
slash_commands: Arc<SlashCommandWorkingSet>,
editor: Option<WeakView<ContextEditor>>,
workspace: Option<WeakView<Workspace>>,
editor: Option<WeakModel<ContextEditor>>,
workspace: Option<WeakModel<Workspace>>,
}
pub(crate) struct SlashCommandLine {
@@ -55,8 +55,8 @@ pub(crate) struct SlashCommandLine {
impl SlashCommandCompletionProvider {
pub fn new(
slash_commands: Arc<SlashCommandWorkingSet>,
editor: Option<WeakView<ContextEditor>>,
workspace: Option<WeakView<Workspace>>,
editor: Option<WeakModel<ContextEditor>>,
workspace: Option<WeakModel<Workspace>>,
) -> Self {
Self {
cancel_flag: Mutex::new(Arc::new(AtomicBool::new(false))),
@@ -71,7 +71,8 @@ impl SlashCommandCompletionProvider {
command_name: &str,
command_range: Range<Anchor>,
name_range: Range<Anchor>,
cx: &mut WindowContext,
window: &mut gpui::Window,
cx: &mut gpui::AppContext,
) -> Task<Result<Vec<project::Completion>>> {
let slash_commands = self.slash_commands.clone();
let candidates = slash_commands
@@ -120,12 +121,12 @@ impl SlashCommandCompletionProvider {
let editor = editor.clone();
let workspace = workspace.clone();
Arc::new(
move |intent: CompletionIntent, cx: &mut WindowContext| {
move |intent: CompletionIntent, window: &mut gpui::Window, cx: &mut gpui::AppContext| {
if !requires_argument
&& (!accepts_arguments || intent.is_complete())
{
editor
.update(cx, |editor, cx| {
.update(cx, |editor, model, cx| {
editor.run_command(
command_range.clone(),
&command_name,
@@ -165,7 +166,8 @@ impl SlashCommandCompletionProvider {
command_range: Range<Anchor>,
argument_range: Range<Anchor>,
last_argument_range: Range<Anchor>,
cx: &mut WindowContext,
window: &mut gpui::Window,
cx: &mut gpui::AppContext,
) -> Task<Result<Vec<project::Completion>>> {
let new_cancel_flag = Arc::new(AtomicBool::new(false));
let mut flag = self.cancel_flag.lock();
@@ -203,12 +205,12 @@ impl SlashCommandCompletionProvider {
let command_range = command_range.clone();
let command_name = command_name.clone();
move |intent: CompletionIntent, cx: &mut WindowContext| {
move |intent: CompletionIntent, window: &mut gpui::Window, cx: &mut gpui::AppContext| {
if new_argument.after_completion.run()
|| intent.is_complete()
{
editor
.update(cx, |editor, cx| {
.update(cx, |editor, model, cx| {
editor.run_command(
command_range.clone(),
&command_name,
@@ -260,10 +262,11 @@ impl CompletionProvider for SlashCommandCompletionProvider {
buffer: &Model<Buffer>,
buffer_position: Anchor,
_: editor::CompletionContext,
cx: &mut ViewContext<Editor>,
model: &Model<Editor>,
cx: &mut AppContext,
) -> Task<Result<Vec<project::Completion>>> {
let Some((name, arguments, command_range, last_argument_range)) =
buffer.update(cx, |buffer, _cx| {
buffer.update(cx, |buffer, model, _cx| {
let position = buffer_position.to_point(buffer);
let line_start = Point::new(position.row, 0);
let mut lines = buffer.text_for_range(line_start..position).lines();
@@ -318,7 +321,7 @@ impl CompletionProvider for SlashCommandCompletionProvider {
cx,
)
} else {
self.complete_command_name(&name, command_range, last_argument_range, cx)
self.complete_command_name(&name, command_range, last_argument_range, model, cx)
}
}
@@ -327,7 +330,8 @@ impl CompletionProvider for SlashCommandCompletionProvider {
_: Model<Buffer>,
_: Vec<usize>,
_: Arc<RwLock<Box<[project::Completion]>>>,
_: &mut ViewContext<Editor>,
_: &Model<Editor>,
_: &mut AppContext,
) -> Task<Result<bool>> {
Task::ready(Ok(true))
}
@@ -337,7 +341,8 @@ impl CompletionProvider for SlashCommandCompletionProvider {
_: Model<Buffer>,
_: project::Completion,
_: bool,
_: &mut ViewContext<Editor>,
_: &Model<Editor>,
_: &mut AppContext,
) -> Task<Result<Option<language::Transaction>>> {
Task::ready(Ok(None))
}
@@ -348,7 +353,8 @@ impl CompletionProvider for SlashCommandCompletionProvider {
position: language::Anchor,
_text: &str,
_trigger_in_words: bool,
cx: &mut ViewContext<Editor>,
model: &Model<Editor>,
cx: &mut AppContext,
) -> bool {
let buffer = buffer.read(cx);
let position = position.to_point(buffer);

View File

@@ -14,7 +14,7 @@ use language_model::{
use semantic_index::{FileSummary, SemanticDb};
use smol::channel;
use std::sync::{atomic::AtomicBool, Arc};
use ui::{prelude::*, BorrowAppContext, WindowContext};
use ui::{prelude::*, BorrowAppContext};
use util::ResultExt;
use workspace::Workspace;
@@ -53,8 +53,9 @@ impl SlashCommand for AutoCommand {
self: Arc<Self>,
_arguments: &[String],
_cancel: Arc<AtomicBool>,
workspace: Option<WeakView<Workspace>>,
cx: &mut WindowContext,
workspace: Option<WeakModel<Workspace>>,
window: &mut gpui::Window,
cx: &mut gpui::AppContext,
) -> Task<Result<Vec<ArgumentCompletion>>> {
// There's no autocomplete for a prompt, since it's arbitrary text.
// However, we can use this opportunity to kick off a drain of the backlog.
@@ -76,7 +77,7 @@ impl SlashCommand for AutoCommand {
let cx: &mut AppContext = cx;
cx.spawn(|cx: gpui::AsyncAppContext| async move {
cx.spawn(|cx: AsyncAppContext| async move {
let task = project_index.read_with(&cx, |project_index, cx| {
project_index.flush_summary_backlogs(cx)
})?;
@@ -96,9 +97,10 @@ impl SlashCommand for AutoCommand {
arguments: &[String],
_context_slash_command_output_sections: &[SlashCommandOutputSection<language::Anchor>],
_context_buffer: language::BufferSnapshot,
workspace: WeakView<Workspace>,
workspace: WeakModel<Workspace>,
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
cx: &mut WindowContext,
window: &mut gpui::Window,
cx: &mut gpui::AppContext,
) -> Task<SlashCommandResult> {
let Some(workspace) = workspace.upgrade() else {
return Task::ready(Err(anyhow::anyhow!("workspace was dropped")));
@@ -115,7 +117,7 @@ impl SlashCommand for AutoCommand {
return Task::ready(Err(anyhow!("no project indexer")));
};
let task = cx.spawn(|cx: gpui::AsyncWindowContext| async move {
let task = cx.spawn(|cx: AsyncAppContext| async move {
let summaries = project_index
.read_with(&cx, |project_index, cx| project_index.all_summaries(cx))?
.await?;

View File

@@ -107,8 +107,9 @@ impl SlashCommand for CargoWorkspaceSlashCommand {
self: Arc<Self>,
_arguments: &[String],
_cancel: Arc<AtomicBool>,
_workspace: Option<WeakView<Workspace>>,
_cx: &mut WindowContext,
_workspace: Option<WeakModel<Workspace>>,
_window: &mut gpui::Window,
_cx: &mut gpui::AppContext,
) -> Task<Result<Vec<ArgumentCompletion>>> {
Task::ready(Err(anyhow!("this command does not require argument")))
}
@@ -122,11 +123,12 @@ impl SlashCommand for CargoWorkspaceSlashCommand {
_arguments: &[String],
_context_slash_command_output_sections: &[SlashCommandOutputSection<language::Anchor>],
_context_buffer: BufferSnapshot,
workspace: WeakView<Workspace>,
workspace: WeakModel<Workspace>,
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
cx: &mut WindowContext,
window: &mut gpui::Window,
cx: &mut gpui::AppContext,
) -> Task<SlashCommandResult> {
let output = workspace.update(cx, |workspace, cx| {
let output = workspace.update(cx, |workspace, model, cx| {
let project = workspace.project().clone();
let fs = workspace.project().read(cx).fs().clone();
let path = Self::path_to_cargo_toml(project, cx);

View File

@@ -8,7 +8,7 @@ use context_server::{
manager::{ContextServer, ContextServerManager},
types::Prompt,
};
use gpui::{AppContext, Model, Task, WeakView, WindowContext};
use gpui::{AppContext, Model, Task, WeakView};
use language::{BufferSnapshot, CodeLabel, LspAdapterDelegate};
use std::sync::atomic::AtomicBool;
use std::sync::Arc;
@@ -77,8 +77,9 @@ impl SlashCommand for ContextServerSlashCommand {
self: Arc<Self>,
arguments: &[String],
_cancel: Arc<AtomicBool>,
_workspace: Option<WeakView<Workspace>>,
cx: &mut WindowContext,
_workspace: Option<WeakModel<Workspace>>,
window: &mut gpui::Window,
cx: &mut gpui::AppContext,
) -> Task<Result<Vec<ArgumentCompletion>>> {
let Ok((arg_name, arg_value)) = completion_argument(&self.prompt, arguments) else {
return Task::ready(Err(anyhow!("Failed to complete argument")));
@@ -128,9 +129,10 @@ impl SlashCommand for ContextServerSlashCommand {
arguments: &[String],
_context_slash_command_output_sections: &[SlashCommandOutputSection<language::Anchor>],
_context_buffer: BufferSnapshot,
_workspace: WeakView<Workspace>,
_workspace: WeakModel<Workspace>,
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
cx: &mut WindowContext,
window: &mut gpui::Window,
cx: &mut gpui::AppContext,
) -> Task<SlashCommandResult> {
let server_id = self.server_id.clone();
let prompt_name = self.prompt.name.clone();
@@ -164,7 +166,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

@@ -36,8 +36,9 @@ impl SlashCommand for DefaultSlashCommand {
self: Arc<Self>,
_arguments: &[String],
_cancellation_flag: Arc<AtomicBool>,
_workspace: Option<WeakView<Workspace>>,
_cx: &mut WindowContext,
_workspace: Option<WeakModel<Workspace>>,
_window: &mut gpui::Window,
_cx: &mut gpui::AppContext,
) -> Task<Result<Vec<ArgumentCompletion>>> {
Task::ready(Err(anyhow!("this command does not require argument")))
}
@@ -47,9 +48,9 @@ impl SlashCommand for DefaultSlashCommand {
_arguments: &[String],
_context_slash_command_output_sections: &[SlashCommandOutputSection<language::Anchor>],
_context_buffer: BufferSnapshot,
_workspace: WeakView<Workspace>,
_workspace: WeakModel<Workspace>,
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
cx: &mut WindowContext,
window: &mut gpui::Window, cx: &mut gpui::AppContext,
) -> Task<SlashCommandResult> {
let store = PromptStore::global(cx);
cx.background_executor().spawn(async move {

View File

@@ -6,7 +6,7 @@ use assistant_slash_command::{
};
use collections::HashSet;
use futures::future;
use gpui::{Task, WeakView, WindowContext};
use gpui::{Task, WeakView};
use language::{BufferSnapshot, LspAdapterDelegate};
use std::sync::{atomic::AtomicBool, Arc};
use text::OffsetRangeExt;
@@ -40,8 +40,9 @@ impl SlashCommand for DeltaSlashCommand {
self: Arc<Self>,
_arguments: &[String],
_cancellation_flag: Arc<AtomicBool>,
_workspace: Option<WeakView<Workspace>>,
_cx: &mut WindowContext,
_workspace: Option<WeakModel<Workspace>>,
_window: &mut gpui::Window,
_cx: &mut gpui::AppContext,
) -> Task<Result<Vec<ArgumentCompletion>>> {
Task::ready(Err(anyhow!("this command does not require argument")))
}
@@ -51,9 +52,10 @@ impl SlashCommand for DeltaSlashCommand {
_arguments: &[String],
context_slash_command_output_sections: &[SlashCommandOutputSection<language::Anchor>],
context_buffer: BufferSnapshot,
workspace: WeakView<Workspace>,
workspace: WeakModel<Workspace>,
delegate: Option<Arc<dyn LspAdapterDelegate>>,
cx: &mut WindowContext,
window: &mut gpui::Window,
cx: &mut gpui::AppContext,
) -> Task<SlashCommandResult> {
let mut paths = HashSet::default();
let mut file_command_old_outputs = Vec::new();
@@ -77,6 +79,7 @@ impl SlashCommand for DeltaSlashCommand {
context_buffer.clone(),
workspace.clone(),
delegate.clone(),
model,
cx,
));
}

View File

@@ -30,7 +30,7 @@ impl DiagnosticsSlashCommand {
&self,
query: String,
cancellation_flag: Arc<AtomicBool>,
workspace: &View<Workspace>,
workspace: &Model<Workspace>,
cx: &mut AppContext,
) -> Task<Vec<PathMatch>> {
if query.is_empty() {
@@ -118,8 +118,8 @@ impl SlashCommand for DiagnosticsSlashCommand {
self: Arc<Self>,
arguments: &[String],
cancellation_flag: Arc<AtomicBool>,
workspace: Option<WeakView<Workspace>>,
cx: &mut WindowContext,
workspace: Option<WeakModel<Workspace>>,
window: &mut gpui::Window, cx: &mut gpui::AppContext,
) -> Task<Result<Vec<ArgumentCompletion>>> {
let Some(workspace) = workspace.and_then(|workspace| workspace.upgrade()) else {
return Task::ready(Err(anyhow!("workspace was dropped")));
@@ -172,9 +172,9 @@ impl SlashCommand for DiagnosticsSlashCommand {
arguments: &[String],
_context_slash_command_output_sections: &[SlashCommandOutputSection<language::Anchor>],
_context_buffer: BufferSnapshot,
workspace: WeakView<Workspace>,
workspace: WeakModel<Workspace>,
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
cx: &mut WindowContext,
window: &mut gpui::Window, cx: &mut gpui::AppContext,
) -> Task<SlashCommandResult> {
let Some(workspace) = workspace.upgrade() else {
return Task::ready(Err(anyhow!("workspace was dropped")));

View File

@@ -43,7 +43,7 @@ impl DocsSlashCommand {
/// access the workspace so we can read the project.
fn ensure_rust_doc_providers_are_registered(
&self,
workspace: Option<WeakView<Workspace>>,
workspace: Option<WeakModel<Workspace>>,
cx: &mut AppContext,
) {
let indexed_docs_registry = IndexedDocsRegistry::global(cx);
@@ -164,8 +164,9 @@ impl SlashCommand for DocsSlashCommand {
self: Arc<Self>,
arguments: &[String],
_cancel: Arc<AtomicBool>,
workspace: Option<WeakView<Workspace>>,
cx: &mut WindowContext,
workspace: Option<WeakModel<Workspace>>,
window: &mut gpui::Window,
cx: &mut gpui::AppContext,
) -> Task<Result<Vec<ArgumentCompletion>>> {
self.ensure_rust_doc_providers_are_registered(workspace, cx);
@@ -272,9 +273,10 @@ impl SlashCommand for DocsSlashCommand {
arguments: &[String],
_context_slash_command_output_sections: &[SlashCommandOutputSection<language::Anchor>],
_context_buffer: BufferSnapshot,
_workspace: WeakView<Workspace>,
_workspace: WeakModel<Workspace>,
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
cx: &mut WindowContext,
window: &mut gpui::Window,
cx: &mut gpui::AppContext,
) -> Task<SlashCommandResult> {
if arguments.is_empty() {
return Task::ready(Err(anyhow!("missing an argument")));

View File

@@ -124,8 +124,9 @@ impl SlashCommand for FetchSlashCommand {
self: Arc<Self>,
_arguments: &[String],
_cancel: Arc<AtomicBool>,
_workspace: Option<WeakView<Workspace>>,
_cx: &mut WindowContext,
_workspace: Option<WeakModel<Workspace>>,
_window: &mut gpui::Window,
_cx: &mut gpui::AppContext,
) -> Task<Result<Vec<ArgumentCompletion>>> {
Task::ready(Ok(Vec::new()))
}
@@ -135,9 +136,9 @@ impl SlashCommand for FetchSlashCommand {
arguments: &[String],
_context_slash_command_output_sections: &[SlashCommandOutputSection<language::Anchor>],
_context_buffer: BufferSnapshot,
workspace: WeakView<Workspace>,
workspace: WeakModel<Workspace>,
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
cx: &mut WindowContext,
window: &mut gpui::Window, cx: &mut gpui::AppContext,
) -> Task<SlashCommandResult> {
let Some(argument) = arguments.first() else {
return Task::ready(Err(anyhow!("missing URL")));

View File

@@ -28,7 +28,7 @@ impl FileSlashCommand {
&self,
query: String,
cancellation_flag: Arc<AtomicBool>,
workspace: &View<Workspace>,
workspace: &Model<Workspace>,
cx: &mut AppContext,
) -> Task<Vec<PathMatch>> {
if query.is_empty() {
@@ -134,8 +134,8 @@ impl SlashCommand for FileSlashCommand {
self: Arc<Self>,
arguments: &[String],
cancellation_flag: Arc<AtomicBool>,
workspace: Option<WeakView<Workspace>>,
cx: &mut WindowContext,
workspace: Option<WeakModel<Workspace>>,
window: &mut gpui::Window, cx: &mut gpui::AppContext,
) -> Task<Result<Vec<ArgumentCompletion>>> {
let Some(workspace) = workspace.and_then(|workspace| workspace.upgrade()) else {
return Task::ready(Err(anyhow!("workspace was dropped")));
@@ -187,9 +187,9 @@ impl SlashCommand for FileSlashCommand {
arguments: &[String],
_context_slash_command_output_sections: &[SlashCommandOutputSection<language::Anchor>],
_context_buffer: BufferSnapshot,
workspace: WeakView<Workspace>,
workspace: WeakModel<Workspace>,
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
cx: &mut WindowContext,
window: &mut gpui::Window, cx: &mut gpui::AppContext,
) -> Task<SlashCommandResult> {
let Some(workspace) = workspace.upgrade() else {
return Task::ready(Err(anyhow!("workspace was dropped")));

View File

@@ -35,8 +35,9 @@ impl SlashCommand for NowSlashCommand {
self: Arc<Self>,
_arguments: &[String],
_cancel: Arc<AtomicBool>,
_workspace: Option<WeakView<Workspace>>,
_cx: &mut WindowContext,
_workspace: Option<WeakModel<Workspace>>,
_window: &mut gpui::Window,
_cx: &mut gpui::AppContext,
) -> Task<Result<Vec<ArgumentCompletion>>> {
Task::ready(Ok(Vec::new()))
}
@@ -46,9 +47,10 @@ impl SlashCommand for NowSlashCommand {
_arguments: &[String],
_context_slash_command_output_sections: &[SlashCommandOutputSection<language::Anchor>],
_context_buffer: BufferSnapshot,
_workspace: WeakView<Workspace>,
_workspace: WeakModel<Workspace>,
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
_cx: &mut WindowContext,
_window: &mut gpui::Window,
_cx: &mut gpui::AppContext,
) -> Task<SlashCommandResult> {
let now = Local::now();
let text = format!("Today is {now}.", now = now.to_rfc2822());

View File

@@ -6,7 +6,7 @@ use crate::PromptBuilder;
use anyhow::{anyhow, Result};
use assistant_slash_command::{ArgumentCompletion, SlashCommandOutputSection, SlashCommandResult};
use feature_flags::FeatureFlag;
use gpui::{AppContext, Task, WeakView, WindowContext};
use gpui::{AppContext, Task, WeakView};
use language::{Anchor, CodeLabel, LspAdapterDelegate};
use language_model::{LanguageModelRegistry, LanguageModelTool};
use schemars::JsonSchema;
@@ -67,8 +67,9 @@ impl SlashCommand for ProjectSlashCommand {
self: Arc<Self>,
_arguments: &[String],
_cancel: Arc<AtomicBool>,
_workspace: Option<WeakView<Workspace>>,
_cx: &mut WindowContext,
_workspace: Option<WeakModel<Workspace>>,
_window: &mut gpui::Window,
_cx: &mut gpui::AppContext,
) -> Task<Result<Vec<ArgumentCompletion>>> {
Task::ready(Ok(Vec::new()))
}
@@ -78,9 +79,10 @@ impl SlashCommand for ProjectSlashCommand {
_arguments: &[String],
_context_slash_command_output_sections: &[SlashCommandOutputSection<Anchor>],
context_buffer: language::BufferSnapshot,
workspace: WeakView<Workspace>,
workspace: WeakModel<Workspace>,
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
cx: &mut WindowContext,
window: &mut gpui::Window,
cx: &mut gpui::AppContext,
) -> Task<SlashCommandResult> {
let model_registry = LanguageModelRegistry::read_global(cx);
let current_model = model_registry.active_model();

View File

@@ -37,8 +37,8 @@ impl SlashCommand for PromptSlashCommand {
self: Arc<Self>,
arguments: &[String],
_cancellation_flag: Arc<AtomicBool>,
_workspace: Option<WeakView<Workspace>>,
cx: &mut WindowContext,
_workspace: Option<WeakModel<Workspace>>,
window: &mut gpui::Window, cx: &mut gpui::AppContext,
) -> Task<Result<Vec<ArgumentCompletion>>> {
let store = PromptStore::global(cx);
let query = arguments.to_owned().join(" ");
@@ -64,9 +64,9 @@ impl SlashCommand for PromptSlashCommand {
arguments: &[String],
_context_slash_command_output_sections: &[SlashCommandOutputSection<language::Anchor>],
_context_buffer: BufferSnapshot,
_workspace: WeakView<Workspace>,
_workspace: WeakModel<Workspace>,
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
cx: &mut WindowContext,
window: &mut gpui::Window, cx: &mut gpui::AppContext,
) -> Task<SlashCommandResult> {
let title = arguments.to_owned().join(" ");
if title.trim().is_empty() {

View File

@@ -58,8 +58,9 @@ impl SlashCommand for SearchSlashCommand {
self: Arc<Self>,
_arguments: &[String],
_cancel: Arc<AtomicBool>,
_workspace: Option<WeakView<Workspace>>,
_cx: &mut WindowContext,
_workspace: Option<WeakModel<Workspace>>,
_window: &mut gpui::Window,
_cx: &mut gpui::AppContext,
) -> Task<Result<Vec<ArgumentCompletion>>> {
Task::ready(Ok(Vec::new()))
}
@@ -69,9 +70,9 @@ impl SlashCommand for SearchSlashCommand {
arguments: &[String],
_context_slash_command_output_sections: &[SlashCommandOutputSection<language::Anchor>],
_context_buffer: language::BufferSnapshot,
workspace: WeakView<Workspace>,
workspace: WeakModel<Workspace>,
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
cx: &mut WindowContext,
window: &mut gpui::Window, cx: &mut gpui::AppContext,
) -> Task<SlashCommandResult> {
let Some(workspace) = workspace.upgrade() else {
return Task::ready(Err(anyhow::anyhow!("workspace was dropped")));

View File

@@ -9,7 +9,7 @@ use gpui::{AppContext, Task, WeakView};
use language::{BufferSnapshot, CodeLabel, LspAdapterDelegate};
use std::sync::atomic::AtomicBool;
use std::sync::Arc;
use ui::{IconName, SharedString, WindowContext};
use ui::{IconName, SharedString};
use workspace::Workspace;
pub(crate) struct SelectionCommand;
@@ -47,8 +47,9 @@ impl SlashCommand for SelectionCommand {
self: Arc<Self>,
_arguments: &[String],
_cancel: Arc<AtomicBool>,
_workspace: Option<WeakView<Workspace>>,
_cx: &mut WindowContext,
_workspace: Option<WeakModel<Workspace>>,
_window: &mut gpui::Window,
_cx: &mut gpui::AppContext,
) -> Task<Result<Vec<ArgumentCompletion>>> {
Task::ready(Err(anyhow!("this command does not require argument")))
}
@@ -58,9 +59,10 @@ impl SlashCommand for SelectionCommand {
_arguments: &[String],
_context_slash_command_output_sections: &[SlashCommandOutputSection<language::Anchor>],
_context_buffer: BufferSnapshot,
workspace: WeakView<Workspace>,
workspace: WeakModel<Workspace>,
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
cx: &mut WindowContext,
window: &mut gpui::Window,
cx: &mut gpui::AppContext,
) -> Task<SlashCommandResult> {
let mut events = vec![];

View File

@@ -45,8 +45,9 @@ impl SlashCommand for StreamingExampleSlashCommand {
self: Arc<Self>,
_arguments: &[String],
_cancel: Arc<AtomicBool>,
_workspace: Option<WeakView<Workspace>>,
_cx: &mut WindowContext,
_workspace: Option<WeakModel<Workspace>>,
_window: &mut gpui::Window,
_cx: &mut gpui::AppContext,
) -> Task<Result<Vec<ArgumentCompletion>>> {
Task::ready(Ok(Vec::new()))
}
@@ -56,9 +57,10 @@ impl SlashCommand for StreamingExampleSlashCommand {
_arguments: &[String],
_context_slash_command_output_sections: &[SlashCommandOutputSection<language::Anchor>],
_context_buffer: BufferSnapshot,
_workspace: WeakView<Workspace>,
_workspace: WeakModel<Workspace>,
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
cx: &mut WindowContext,
window: &mut gpui::Window,
cx: &mut gpui::AppContext,
) -> Task<SlashCommandResult> {
let (events_tx, events_rx) = mpsc::unbounded();
cx.background_executor()

View File

@@ -8,7 +8,7 @@ use gpui::{Task, WeakView};
use language::{BufferSnapshot, LspAdapterDelegate};
use std::sync::Arc;
use std::{path::Path, sync::atomic::AtomicBool};
use ui::{IconName, WindowContext};
use ui::IconName;
use workspace::Workspace;
pub(crate) struct OutlineSlashCommand;
@@ -34,8 +34,9 @@ impl SlashCommand for OutlineSlashCommand {
self: Arc<Self>,
_arguments: &[String],
_cancel: Arc<AtomicBool>,
_workspace: Option<WeakView<Workspace>>,
_cx: &mut WindowContext,
_workspace: Option<WeakModel<Workspace>>,
_window: &mut gpui::Window,
_cx: &mut gpui::AppContext,
) -> Task<Result<Vec<ArgumentCompletion>>> {
Task::ready(Err(anyhow!("this command does not require argument")))
}
@@ -49,11 +50,12 @@ impl SlashCommand for OutlineSlashCommand {
_arguments: &[String],
_context_slash_command_output_sections: &[SlashCommandOutputSection<language::Anchor>],
_context_buffer: BufferSnapshot,
workspace: WeakView<Workspace>,
workspace: WeakModel<Workspace>,
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
cx: &mut WindowContext,
window: &mut gpui::Window,
cx: &mut gpui::AppContext,
) -> Task<SlashCommandResult> {
let output = workspace.update(cx, |workspace, cx| {
let output = workspace.update(cx, |workspace, model, cx| {
let Some(active_item) = workspace.active_item(cx) else {
return Task::ready(Err(anyhow!("no active tab")));
};

View File

@@ -12,7 +12,7 @@ use std::{
path::PathBuf,
sync::{atomic::AtomicBool, Arc},
};
use ui::{prelude::*, ActiveTheme, WindowContext};
use ui::{prelude::*, ActiveTheme};
use util::ResultExt;
use workspace::Workspace;
@@ -51,8 +51,9 @@ impl SlashCommand for TabSlashCommand {
self: Arc<Self>,
arguments: &[String],
cancel: Arc<AtomicBool>,
workspace: Option<WeakView<Workspace>>,
cx: &mut WindowContext,
workspace: Option<WeakModel<Workspace>>,
window: &mut gpui::Window,
cx: &mut gpui::AppContext,
) -> Task<Result<Vec<ArgumentCompletion>>> {
let mut has_all_tabs_completion_item = false;
let argument_set = arguments
@@ -73,8 +74,8 @@ impl SlashCommand for TabSlashCommand {
let active_item_path = workspace.as_ref().and_then(|workspace| {
workspace
.update(cx, |workspace, cx| {
let snapshot = active_item_buffer(workspace, cx).ok()?;
.update(cx, |workspace, model, cx| {
let snapshot = active_item_buffer(workspace, model, cx).ok()?;
snapshot.resolve_file_path(cx, true)
})
.ok()
@@ -82,7 +83,7 @@ impl SlashCommand for TabSlashCommand {
});
let current_query = arguments.last().cloned().unwrap_or_default();
let tab_items_search =
tab_items_for_queries(workspace, &[current_query], cancel, false, cx);
tab_items_for_queries(workspace, &[current_query], cancel, false, model, cx);
let comment_id = cx.theme().syntax().highlight_id("comment").map(HighlightId);
cx.spawn(|_| async move {
@@ -137,9 +138,10 @@ impl SlashCommand for TabSlashCommand {
arguments: &[String],
_context_slash_command_output_sections: &[SlashCommandOutputSection<language::Anchor>],
_context_buffer: BufferSnapshot,
workspace: WeakView<Workspace>,
workspace: WeakModel<Workspace>,
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
cx: &mut WindowContext,
window: &mut gpui::Window,
cx: &mut gpui::AppContext,
) -> Task<SlashCommandResult> {
let tab_items_search = tab_items_for_queries(
Some(workspace),
@@ -160,11 +162,12 @@ impl SlashCommand for TabSlashCommand {
}
fn tab_items_for_queries(
workspace: Option<WeakView<Workspace>>,
workspace: Option<WeakModel<Workspace>>,
queries: &[String],
cancel: Arc<AtomicBool>,
strict_match: bool,
cx: &mut WindowContext,
window: &mut gpui::Window,
cx: &mut gpui::AppContext,
) -> Task<anyhow::Result<Vec<(Option<PathBuf>, BufferSnapshot, usize)>>> {
let empty_query = queries.is_empty() || queries.iter().all(|query| query.trim().is_empty());
let queries = queries.to_owned();
@@ -174,7 +177,7 @@ fn tab_items_for_queries(
.context("no workspace")?
.update(&mut cx, |workspace, cx| {
if strict_match && empty_query {
let snapshot = active_item_buffer(workspace, cx)?;
let snapshot = active_item_buffer(workspace, model, cx)?;
let full_path = snapshot.resolve_file_path(cx, true);
return anyhow::Ok(vec![(full_path, snapshot, 0)]);
}
@@ -285,7 +288,8 @@ fn tab_items_for_queries(
fn active_item_buffer(
workspace: &mut Workspace,
cx: &mut ui::ViewContext<Workspace>,
model: &Model<Workspace>,
cx: &mut AppContext,
) -> anyhow::Result<BufferSnapshot> {
let active_editor = workspace
.active_item(cx)

View File

@@ -53,8 +53,9 @@ impl SlashCommand for TerminalSlashCommand {
self: Arc<Self>,
_arguments: &[String],
_cancel: Arc<AtomicBool>,
_workspace: Option<WeakView<Workspace>>,
_cx: &mut WindowContext,
_workspace: Option<WeakModel<Workspace>>,
_window: &mut gpui::Window,
_cx: &mut gpui::AppContext,
) -> Task<Result<Vec<ArgumentCompletion>>> {
Task::ready(Ok(Vec::new()))
}
@@ -64,15 +65,16 @@ impl SlashCommand for TerminalSlashCommand {
arguments: &[String],
_context_slash_command_output_sections: &[SlashCommandOutputSection<language::Anchor>],
_context_buffer: BufferSnapshot,
workspace: WeakView<Workspace>,
workspace: WeakModel<Workspace>,
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
cx: &mut WindowContext,
window: &mut gpui::Window,
cx: &mut gpui::AppContext,
) -> Task<SlashCommandResult> {
let Some(workspace) = workspace.upgrade() else {
return Task::ready(Err(anyhow::anyhow!("workspace was dropped")));
};
let Some(active_terminal) = resolve_active_terminal(&workspace, cx) else {
let Some(active_terminal) = resolve_active_terminal(&workspace, model, cx) else {
return Task::ready(Err(anyhow::anyhow!("no active terminal")));
};
@@ -107,9 +109,10 @@ impl SlashCommand for TerminalSlashCommand {
}
fn resolve_active_terminal(
workspace: &View<Workspace>,
cx: &WindowContext,
) -> Option<View<TerminalView>> {
workspace: &Model<Workspace>,
window: &Window,
cx: &AppContext,
) -> Option<Model<TerminalView>> {
if let Some(terminal_view) = workspace
.read(cx)
.active_item(cx)

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;
@@ -10,7 +10,7 @@ use crate::SlashCommandWorkingSet;
#[derive(IntoElement)]
pub(super) struct SlashCommandSelector<T: PopoverTrigger> {
working_set: Arc<SlashCommandWorkingSet>,
active_context_editor: WeakView<ContextEditor>,
active_context_editor: WeakModel<ContextEditor>,
trigger: T,
}
@@ -27,8 +27,8 @@ enum SlashCommandEntry {
Info(SlashCommandInfo),
Advert {
name: SharedString,
renderer: fn(&mut WindowContext<'_>) -> AnyElement,
on_confirm: fn(&mut WindowContext<'_>),
renderer: fn(&mut gpui::Window, &mut gpui::AppContext) -> AnyElement,
on_confirm: fn(&mut gpui::Window, &mut gpui::AppContext),
},
}
@@ -44,14 +44,14 @@ impl AsRef<str> for SlashCommandEntry {
pub(crate) struct SlashCommandDelegate {
all_commands: Vec<SlashCommandEntry>,
filtered_commands: Vec<SlashCommandEntry>,
active_context_editor: WeakView<ContextEditor>,
active_context_editor: WeakModel<ContextEditor>,
selected_index: usize,
}
impl<T: PopoverTrigger> SlashCommandSelector<T> {
pub(crate) fn new(
working_set: Arc<SlashCommandWorkingSet>,
active_context_editor: WeakView<ContextEditor>,
active_context_editor: WeakModel<ContextEditor>,
trigger: T,
) -> Self {
SlashCommandSelector {
@@ -73,18 +73,23 @@ impl PickerDelegate for SlashCommandDelegate {
self.selected_index
}
fn set_selected_index(&mut self, ix: usize, cx: &mut ViewContext<Picker<Self>>) {
fn set_selected_index(&mut self, ix: usize, model: &Model<Picker>, cx: &mut AppContext) {
self.selected_index = ix.min(self.filtered_commands.len().saturating_sub(1));
cx.notify();
model.notify(cx);
}
fn placeholder_text(&self, _cx: &mut WindowContext) -> Arc<str> {
fn placeholder_text(&self, _window: &mut gpui::Window, _cx: &mut gpui::AppContext) -> Arc<str> {
"Select a command...".into()
}
fn update_matches(&mut self, query: String, cx: &mut ViewContext<Picker<Self>>) -> Task<()> {
fn update_matches(
&mut self,
query: String,
model: &Model<Picker>,
cx: &mut AppContext,
) -> Task<()> {
let all_commands = self.all_commands.clone();
cx.spawn(|this, mut cx| async move {
model.spawn(cx, |this, mut cx| async move {
let filtered_commands = cx
.background_executor()
.spawn(async move {
@@ -104,10 +109,10 @@ impl PickerDelegate for SlashCommandDelegate {
})
.await;
this.update(&mut cx, |this, cx| {
this.update(&mut cx, |this, model, cx| {
this.delegate.filtered_commands = filtered_commands;
this.delegate.set_selected_index(0, cx);
cx.notify();
model.notify(cx);
})
.ok();
})
@@ -139,25 +144,31 @@ impl PickerDelegate for SlashCommandDelegate {
ret
}
fn confirm(&mut self, _secondary: bool, cx: &mut ViewContext<Picker<Self>>) {
fn confirm(
&mut self,
_secondary: bool,
model: &Model<Picker>,
window: &mut Window,
cx: &mut AppContext,
) {
if let Some(command) = self.filtered_commands.get(self.selected_index) {
match command {
SlashCommandEntry::Info(info) => {
self.active_context_editor
.update(cx, |context_editor, cx| {
.update(cx, |context_editor, model, cx| {
context_editor.insert_command(&info.name, cx)
})
.ok();
}
SlashCommandEntry::Advert { on_confirm, .. } => {
on_confirm(cx);
on_confirm(window, cx);
}
}
cx.emit(DismissEvent);
model.emit(DismissEvent, cx);
}
}
fn dismissed(&mut self, _cx: &mut ViewContext<Picker<Self>>) {}
fn dismissed(&mut self, model: &Model<Picker>, _cx: &mut AppContext) {}
fn editor_position(&self) -> PickerEditorPosition {
PickerEditorPosition::End
@@ -167,7 +178,8 @@ impl PickerDelegate for SlashCommandDelegate {
&self,
ix: usize,
selected: bool,
cx: &mut ViewContext<Picker<Self>>,
model: &Model<Picker>,
cx: &mut AppContext,
) -> Option<Self::ListItem> {
let command_info = self.filtered_commands.get(ix)?;
@@ -177,11 +189,20 @@ impl PickerDelegate for SlashCommandDelegate {
.inset(true)
.spacing(ListItemSpacing::Dense)
.selected(selected)
.tooltip({
let description = info.description.clone();
move |cx| {
cx.new_model(|_, _| 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 +213,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 +221,7 @@ impl PickerDelegate for SlashCommandDelegate {
.font_buffer(cx)
.child(
Label::new(args)
.single_line()
.size(LabelSize::Small)
.color(Color::Muted),
)
@@ -210,9 +232,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),
),
),
),
),
@@ -221,14 +245,14 @@ impl PickerDelegate for SlashCommandDelegate {
.inset(true)
.spacing(ListItemSpacing::Dense)
.selected(selected)
.child(renderer(cx)),
.child(renderer(window, cx)),
),
}
}
}
impl<T: PopoverTrigger> RenderOnce for SlashCommandSelector<T> {
fn render(self, cx: &mut WindowContext) -> impl IntoElement {
fn render(self, _window: &mut gpui::Window, cx: &mut gpui::AppContext) -> impl IntoElement {
let all_models = self
.working_set
.featured_command_names(cx)
@@ -296,14 +320,15 @@ impl<T: PopoverTrigger> RenderOnce for SlashCommandSelector<T> {
selected_index: 0,
};
let picker_view = cx.new_view(|cx| {
let picker = Picker::uniform_list(delegate, cx).max_height(Some(rems(20.).into()));
let picker_view = cx.new_model(|model, cx| {
let picker =
Picker::uniform_list(delegate, model, cx).max_height(Some(rems(20.).into()));
picker
});
let handle = self
.active_context_editor
.update(cx, |this, _| this.slash_menu_handle.clone())
.update(cx, |this, model, _| this.slash_menu_handle.clone())
.ok();
PopoverMenu::new("model-switcher")
.menu(move |_cx| Some(picker_view.clone()))

File diff suppressed because it is too large Load Diff

View File

@@ -15,6 +15,7 @@ doctest = false
[dependencies]
anyhow.workspace = true
assistant_tool.workspace = true
chrono.workspace = true
client.workspace = true
collections.workspace = true
command_palette_hooks.workspace = true
@@ -23,17 +24,24 @@ 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,258 @@
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: WeakModel<Workspace>,
language_registry: Arc<LanguageRegistry>,
tools: Arc<ToolWorkingSet>,
thread: Model<Thread>,
messages: Vec<MessageId>,
list_state: ListState,
rendered_messages_by_id: HashMap<MessageId, Model<Markdown>>,
last_error: Option<ThreadError>,
_subscriptions: Vec<Subscription>,
}
impl ActiveThread {
pub fn new(
thread: Model<Thread>,
workspace: WeakModel<Workspace>,
language_registry: Arc<LanguageRegistry>,
tools: Arc<ToolWorkingSet>,
model: &Model<Self>,
cx: &mut AppContext,
) -> Self {
let subscriptions = vec![
cx.observe(&thread, |_, _, cx| model.notify(cx)),
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 = model.downgrade();
move |ix, window: &mut gpui::Window, cx: &mut gpui::AppContext| {
this.update(cx, |this, model, cx| this.render_message(ix, model, 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(), model, 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,
model: &Model<Self>,
cx: &mut AppContext,
) {
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 = window.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_model(|model, cx| {
Markdown::new(
text,
markdown_style,
Some(self.language_registry.clone()),
None,
model,
window,
cx,
)
});
self.rendered_messages_by_id.insert(*id, markdown);
}
fn handle_thread_event(
&mut self,
_: Model<Thread>,
event: &ThreadEvent,
model: &Model<Self>,
cx: &mut AppContext,
) {
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, model, cx| {
markdown.append(text, model, 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, model, cx);
}
model.notify(cx);
}
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(), model, cx);
self.thread.update(cx, |thread, model, cx| {
thread.insert_tool_output(
tool_use.assistant_message_id,
tool_use.id.clone(),
task,
model,
cx,
);
});
}
}
}
ThreadEvent::ToolFinished { .. } => {}
}
}
fn render_message(&self, ix: usize, model: &Model<Self>, cx: &mut AppContext) -> 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,
model: &Model<Self>,
_window: &mut gpui::Window,
_cx: &mut AppContext,
) -> impl IntoElement {
list(self.list_state.clone()).flex_1()
}
}

View File

@@ -1,6 +1,9 @@
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;
@@ -11,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

@@ -4,25 +4,28 @@ use anyhow::Result;
use assistant_tool::ToolWorkingSet;
use client::zed_urls;
use gpui::{
prelude::*, px, Action, AnyElement, AppContext, AsyncWindowContext, EventEmitter, FocusHandle,
FocusableView, FontWeight, Model, Pixels, Subscription, Task, View, ViewContext, WeakView,
WindowContext,
prelude::*, px, svg, Action, AnyElement, AppContext, AppContext, EventEmitter, FocusHandle,
FocusableView, FontWeight, Model, Pixels, Task, View, WeakView,
};
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, ThreadError, ThreadEvent};
use crate::thread::{ThreadError, ThreadId};
use crate::thread_history::{PastThread, ThreadHistory};
use crate::thread_store::ThreadStore;
use crate::{NewThread, ToggleFocus, ToggleModelSelector};
use crate::{NewThread, OpenHistory, ToggleFocus, ToggleModelSelector};
pub fn init(cx: &mut AppContext) {
cx.observe_new_views(
|workspace: &mut Workspace, _cx: &mut ViewContext<Workspace>| {
workspace.register_action(|workspace, _: &ToggleFocus, cx| {
|workspace: &mut Workspace, model: &Model<Workspace>, _cx: &mut AppContext| {
workspace.register_action(model, |workspace, _: &ToggleFocus, cx| {
workspace.toggle_panel_focus::<AssistantPanel>(cx);
});
},
@@ -30,22 +33,29 @@ pub fn init(cx: &mut AppContext) {
.detach();
}
enum ActiveView {
Thread,
History,
}
pub struct AssistantPanel {
workspace: WeakView<Workspace>,
#[allow(unused)]
workspace: WeakModel<Workspace>,
language_registry: Arc<LanguageRegistry>,
thread_store: Model<ThreadStore>,
thread: Model<Thread>,
message_editor: View<MessageEditor>,
thread: Model<ActiveThread>,
message_editor: Model<MessageEditor>,
tools: Arc<ToolWorkingSet>,
last_error: Option<ThreadError>,
_subscriptions: Vec<Subscription>,
local_timezone: UtcOffset,
active_view: ActiveView,
history: Model<ThreadHistory>,
}
impl AssistantPanel {
pub fn load(
workspace: WeakView<Workspace>,
cx: AsyncWindowContext,
) -> Task<Result<View<Self>>> {
workspace: WeakModel<Workspace>,
window: AnyWindowHandle,
cx: AsyncAppContext,
) -> Task<Result<Model<Self>>> {
cx.spawn(|mut cx| async move {
let tools = Arc::new(ToolWorkingSet::default());
let thread_store = workspace
@@ -56,7 +66,7 @@ impl AssistantPanel {
.await?;
workspace.update(&mut cx, |workspace, cx| {
cx.new_view(|cx| Self::new(workspace, thread_store, tools, cx))
cx.new_model(|model, cx| Self::new(workspace, thread_store, tools, model, cx))
})
})
}
@@ -65,84 +75,110 @@ impl AssistantPanel {
workspace: &Workspace,
thread_store: Model<ThreadStore>,
tools: Arc<ToolWorkingSet>,
cx: &mut ViewContext<Self>,
model: &Model<Self>,
cx: &mut AppContext,
) -> 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),
];
let thread = thread_store.update(cx, |this, model, cx| this.create_thread(model, cx));
let language_registry = workspace.project().read(cx).languages().clone();
let workspace = workspace.weak_handle();
let weak_self = model.downgrade();
Self {
workspace: workspace.weak_handle(),
thread_store,
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_model(|model, cx| {
ActiveThread::new(
thread.clone(),
workspace,
language_registry,
tools.clone(),
model,
cx,
)
}),
message_editor: cx.new_model(|model, cx| MessageEditor::new(thread.clone(), model, cx)),
tools,
last_error: None,
_subscriptions: subscriptions,
local_timezone: UtcOffset::from_whole_seconds(
chrono::Local::now().offset().local_minus_utc(),
)
.unwrap(),
history: cx
.new_model(|model, cx| ThreadHistory::new(weak_self, thread_store, model, cx)),
}
}
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;
self.message_editor.focus_handle(cx).focus(cx);
pub(crate) fn local_timezone(&self) -> UtcOffset {
self.local_timezone
}
fn handle_thread_event(
fn new_thread(&mut self, model: &Model<Self>, cx: &mut AppContext) {
let thread = self
.thread_store
.update(cx, |this, model, cx| this.create_thread(model, cx));
self.active_view = ActiveView::Thread;
self.thread = cx.new_model(|model, cx| {
ActiveThread::new(
thread.clone(),
self.workspace.clone(),
self.language_registry.clone(),
self.tools.clone(),
model,
cx,
)
});
self.message_editor = cx.new_model(|model, cx| MessageEditor::new(thread, model, cx));
self.message_editor.focus_handle(cx).focus(window);
}
pub(crate) fn open_thread(
&mut self,
_: Model<Thread>,
event: &ThreadEvent,
cx: &mut ViewContext<Self>,
thread_id: &ThreadId,
model: &Model<Self>,
cx: &mut AppContext,
) {
match event {
ThreadEvent::ShowError(error) => {
self.last_error = Some(error.clone());
}
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<_>>();
let Some(thread) = self
.thread_store
.update(cx, |this, model, cx| this.open_thread(thread_id, model, 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_model(|model, cx| {
ActiveThread::new(
thread.clone(),
self.workspace.clone(),
self.language_registry.clone(),
self.tools.clone(),
model,
cx,
)
});
self.message_editor = cx.new_model(|model, cx| MessageEditor::new(thread, model, cx));
self.message_editor.focus_handle(cx).focus(window);
}
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,
model: &Model<Self>,
cx: &mut AppContext,
) {
self.thread_store.update(cx, |this, model, cx| {
this.delete_thread(thread_id, model, 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),
}
}
}
@@ -153,7 +189,7 @@ impl Panel for AssistantPanel {
"AssistantPanel2"
}
fn position(&self, _cx: &WindowContext) -> DockPosition {
fn position(&self, _window: &Window, cx: &AppContext) -> DockPosition {
DockPosition::Right
}
@@ -161,25 +197,26 @@ impl Panel for AssistantPanel {
true
}
fn set_position(&mut self, _position: DockPosition, _cx: &mut ViewContext<Self>) {}
fn set_position(&mut self, _position: DockPosition, model: &Model<Self>, _cx: &mut AppContext) {
}
fn size(&self, _cx: &WindowContext) -> Pixels {
fn size(&self, _window: &Window, cx: &AppContext) -> Pixels {
px(640.)
}
fn set_size(&mut self, _size: Option<Pixels>, _cx: &mut ViewContext<Self>) {}
fn set_size(&mut self, _size: Option<Pixels>, model: &Model<Self>, _cx: &mut AppContext) {}
fn set_active(&mut self, _active: bool, _cx: &mut ViewContext<Self>) {}
fn set_active(&mut self, _active: bool, model: &Model<Self>, _cx: &mut AppContext) {}
fn remote_id() -> Option<proto::PanelId> {
Some(proto::PanelId::AssistantPanel)
}
fn icon(&self, _cx: &WindowContext) -> Option<IconName> {
fn icon(&self, _window: &Window, cx: &AppContext) -> Option<IconName> {
Some(IconName::ZedAssistant)
}
fn icon_tooltip(&self, _cx: &WindowContext) -> Option<&'static str> {
fn icon_tooltip(&self, _window: &Window, cx: &AppContext) -> Option<&'static str> {
Some("Assistant Panel")
}
@@ -189,7 +226,7 @@ impl Panel for AssistantPanel {
}
impl AssistantPanel {
fn render_toolbar(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
fn render_toolbar(&self, model: &Model<Self>, cx: &mut AppContext) -> impl IntoElement {
let focus_handle = self.focus_handle(cx);
h_flex()
@@ -201,11 +238,11 @@ 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))
.child(self.render_language_model_selector(cx))
.child(self.render_language_model_selector(model, cx))
.child(Divider::vertical())
.child(
IconButton::new("new-thread", IconName::Plus)
@@ -219,12 +256,13 @@ impl AssistantPanel {
"New Thread",
&NewThread,
&focus_handle,
window,
cx,
)
}
})
.on_click(move |_event, _cx| {
println!("New Thread");
.on_click(move |_event, cx| {
cx.dispatch_action(NewThread.boxed_clone());
}),
)
.child(
@@ -232,9 +270,20 @@ 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,
window,
cx,
)
}
})
.on_click(move |_event, cx| {
cx.dispatch_action(OpenHistory.boxed_clone());
}),
)
.child(
@@ -242,7 +291,7 @@ impl AssistantPanel {
.shape(IconButtonShape::Square)
.icon_size(IconSize::Small)
.style(ButtonStyle::Subtle)
.tooltip(move |cx| Tooltip::text("Configure Assistant", cx))
.tooltip(move |window, cx| Tooltip::text("Configure Assistant", cx))
.on_click(move |_event, _cx| {
println!("Configure Assistant");
}),
@@ -250,7 +299,11 @@ impl AssistantPanel {
)
}
fn render_language_model_selector(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
fn render_language_model_selector(
&self,
model: &Model<Self>,
cx: &mut AppContext,
) -> impl IntoElement {
let active_provider = LanguageModelRegistry::read_global(cx).active_provider();
let active_model = LanguageModelRegistry::read_global(cx).active_model();
@@ -297,39 +350,123 @@ impl AssistantPanel {
.size(IconSize::XSmall),
),
)
.tooltip(move |cx| Tooltip::for_action("Change Model", &ToggleModelSelector, cx)),
.tooltip(move |window, cx| {
Tooltip::for_action("Change Model", &ToggleModelSelector, model, cx)
}),
)
}
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,
model: &Model<Self>,
cx: &mut AppContext,
) -> AnyElement {
if self.thread.read(cx).is_empty() {
return self.render_thread_empty_state(model, cx).into_any_element();
}
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().child(Label::new(message.text.clone())))
self.thread.clone().into_any()
}
fn render_last_error(&self, cx: &mut ViewContext<Self>) -> Option<AnyElement> {
let last_error = self.last_error.as_ref()?;
fn render_thread_empty_state(
&self,
model: &Model<Self>,
cx: &mut AppContext,
) -> impl IntoElement {
let recent_threads = self
.thread_store
.update(cx, |this, model, cx| this.recent_threads(3, model, cx));
v_flex()
.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()
.w_full()
.justify_center()
.child(Label::new("Context Examples:").size(LabelSize::Small)),
)
.child(
h_flex()
.gap_2()
.justify_center()
.child(
h_flex()
.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)),
),
)
.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, model.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),
model,
cx,
))
.on_click(move |_event, cx| {
cx.dispatch_action(OpenHistory.boxed_clone());
}),
),
)
})
}
fn render_last_error(&self, model: &Model<Self>, cx: &mut AppContext) -> Option<AnyElement> {
let last_error = self.thread.read(cx).last_error()?;
Some(
div()
@@ -342,19 +479,23 @@ impl AssistantPanel {
.elevation_2(cx)
.occlude()
.child(match last_error {
ThreadError::PaymentRequired => self.render_payment_required_error(cx),
ThreadError::PaymentRequired => self.render_payment_required_error(model, cx),
ThreadError::MaxMonthlySpendReached => {
self.render_max_monthly_spend_reached_error(cx)
self.render_max_monthly_spend_reached_error(model, cx)
}
ThreadError::Message(error_message) => {
self.render_error_message(error_message, cx)
self.render_error_message(&error_message, model, cx)
}
})
.into_any(),
)
}
fn render_payment_required_error(&self, cx: &mut ViewContext<Self>) -> AnyElement {
fn render_payment_required_error(
&self,
model: &Model<Self>,
cx: &mut AppContext,
) -> 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()
@@ -379,22 +520,32 @@ impl AssistantPanel {
.mt_1()
.child(Button::new("subscribe", "Subscribe").on_click(cx.listener(
|this, _, cx| {
this.last_error = None;
this.thread.update(cx, |this, model, _cx| {
this.clear_last_error();
});
cx.open_url(&zed_urls::account_url(cx));
cx.notify();
model.notify(cx);
},
)))
.child(Button::new("dismiss", "Dismiss").on_click(cx.listener(
|this, _, cx| {
this.last_error = None;
cx.notify();
this.thread.update(cx, |this, model, _cx| {
this.clear_last_error();
});
model.notify(cx);
},
))),
)
.into_any()
}
fn render_max_monthly_spend_reached_error(&self, cx: &mut ViewContext<Self>) -> AnyElement {
fn render_max_monthly_spend_reached_error(
&self,
model: &Model<Self>,
cx: &mut AppContext,
) -> AnyElement {
const ERROR_MESSAGE: &str = "You have reached your maximum monthly spend. Increase your spend limit to continue using Zed LLMs.";
v_flex()
@@ -419,17 +570,23 @@ impl AssistantPanel {
.mt_1()
.child(
Button::new("subscribe", "Update Monthly Spend Limit").on_click(
cx.listener(|this, _, cx| {
this.last_error = None;
model.listener(|this, model, _, cx| {
this.thread.update(cx, |this, model, _cx| {
this.clear_last_error();
});
cx.open_url(&zed_urls::account_url(cx));
cx.notify();
model.notify(cx);
}),
),
)
.child(Button::new("dismiss", "Dismiss").on_click(cx.listener(
|this, _, cx| {
this.last_error = None;
cx.notify();
this.thread.update(cx, |this, model, _cx| {
this.clear_last_error();
});
model.notify(cx);
},
))),
)
@@ -439,7 +596,8 @@ impl AssistantPanel {
fn render_error_message(
&self,
error_message: &SharedString,
cx: &mut ViewContext<Self>,
model: &Model<Self>,
cx: &mut AppContext,
) -> AnyElement {
v_flex()
.gap_0p5()
@@ -466,8 +624,11 @@ impl AssistantPanel {
.mt_1()
.child(Button::new("dismiss", "Dismiss").on_click(cx.listener(
|this, _, cx| {
this.last_error = None;
cx.notify();
this.thread.update(cx, |this, model, _cx| {
this.clear_last_error();
});
model.notify(cx);
},
))),
)
@@ -476,9 +637,12 @@ impl AssistantPanel {
}
impl Render for AssistantPanel {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let messages = self.thread.read(cx).messages().cloned().collect::<Vec<_>>();
fn render(
&mut self,
model: &Model<Self>,
window: &mut gpui::Window,
cx: &mut AppContext,
) -> impl IntoElement {
v_flex()
.key_context("AssistantPanel2")
.justify_between()
@@ -486,27 +650,23 @@ impl Render for AssistantPanel {
.on_action(cx.listener(|this, _: &NewThread, cx| {
this.new_thread(cx);
}))
.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()),
)
.children(self.render_last_error(cx))
.on_action(cx.listener(|this, _: &OpenHistory, cx| {
this.active_view = ActiveView::History;
this.history.focus_handle(cx).focus(window);
model.notify(cx);
}))
.child(self.render_toolbar(model, cx))
.map(|parent| match self.active_view {
ActiveView::Thread => parent
.child(self.render_active_thread_or_empty_state(model, cx))
.child(
h_flex()
.border_t_1()
.border_color(cx.theme().colors().border_variant)
.child(self.message_editor.clone()),
)
.children(self.render_last_error(model, cx)),
ActiveView::History => parent.child(self.history.clone()),
})
}
}

View File

@@ -0,0 +1,207 @@
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: WeakModel<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: WeakModel<MessageEditor>,
selected_ix: usize,
}
impl<T: PopoverTrigger> ContextPicker<T> {
pub(crate) fn new(message_editor: WeakModel<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, model: &Model<Picker>, cx: &mut AppContext) {
self.selected_ix = ix.min(self.filtered_entries.len().saturating_sub(1));
model.notify(cx);
}
fn placeholder_text(&self, _window: &mut gpui::Window, _cx: &mut gpui::AppContext) -> Arc<str> {
"Select a context source…".into()
}
fn update_matches(
&mut self,
query: String,
model: &Model<Picker>,
cx: &mut AppContext,
) -> Task<()> {
let all_commands = self.all_entries.clone();
model.spawn(cx, |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, model, cx| {
this.delegate.filtered_entries = filtered_commands;
this.delegate.set_selected_index(0, cx);
model.notify(cx);
})
.ok();
})
}
fn confirm(&mut self, _secondary: bool, model: &Model<Picker>, cx: &mut AppContext) {
if let Some(entry) = self.filtered_entries.get(self.selected_ix) {
self.message_editor
.update(cx, |_message_editor, model, _cx| {
println!("Insert context from {}", entry.name);
})
.ok();
model.emit(DismissEvent, cx);
}
}
fn dismissed(&mut self, model: &Model<Picker>, _cx: &mut AppContext) {}
fn editor_position(&self) -> PickerEditorPosition {
PickerEditorPosition::End
}
fn render_match(
&self,
ix: usize,
selected: bool,
model: &Model<Picker>,
_cx: &mut AppContext,
) -> 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_model(|_model, _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, _window: &mut gpui::Window, cx: &mut gpui::AppContext) -> 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_model(|model, cx| {
Picker::uniform_list(delegate, model, cx).max_height(Some(rems(20.).into()))
});
let handle = self
.message_editor
.update(cx, |this, model, _| 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,62 +1,70 @@
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>,
editor: Model<Editor>,
pub(crate) context_picker_handle: PopoverMenuHandle<Picker<ContextPickerDelegate>>,
use_tools: bool,
}
impl MessageEditor {
pub fn new(thread: Model<Thread>, cx: &mut ViewContext<Self>) -> Self {
pub fn new(thread: Model<Thread>, model: &Model<Self>, cx: &mut AppContext) -> Self {
Self {
thread,
editor: cx.new_view(|cx| {
let mut editor = Editor::auto_height(80, cx);
editor.set_placeholder_text("Ask anything…", cx);
editor: cx.new_model(|model, cx| {
let mut editor = Editor::auto_height(80, model, cx);
editor.set_placeholder_text("Ask anything…", model, cx);
editor
}),
context_picker_handle: PopoverMenuHandle::default(),
use_tools: false,
}
}
fn chat(&mut self, _: &Chat, cx: &mut ViewContext<Self>) {
self.send_to_model(RequestKind::Chat, cx);
fn chat(&mut self, _: &Chat, model: &Model<Self>, cx: &mut AppContext) {
self.send_to_model(RequestKind::Chat, model, cx);
}
fn send_to_model(
&mut self,
request_kind: RequestKind,
cx: &mut ViewContext<Self>,
model: &Model<Self>,
cx: &mut AppContext,
) -> Option<()> {
let provider = LanguageModelRegistry::read_global(cx).active_provider();
if provider
.as_ref()
.map_or(false, |provider| provider.must_accept_terms(cx))
{
cx.notify();
model.notify(cx);
return None;
}
let model_registry = LanguageModelRegistry::read_global(cx);
let model = model_registry.active_model()?;
let user_message = self.editor.update(cx, |editor, cx| {
let user_message = self.editor.update(cx, |editor, model, cx| {
let text = editor.text(cx);
editor.clear(cx);
text
});
self.thread.update(cx, |thread, cx| {
thread.insert_user_message(user_message);
self.thread.update(cx, |thread, model, cx| {
thread.insert_user_message(user_message, cx);
let mut request = thread.to_completion_request(request_kind, cx);
if self.use_tools {
@@ -86,7 +94,12 @@ impl FocusableView for MessageEditor {
}
impl Render for MessageEditor {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
fn render(
&mut self,
model: &Model<Self>,
window: &mut gpui::Window,
cx: &mut AppContext,
) -> impl IntoElement {
let font_size = TextSize::Default.rems(cx);
let line_height = font_size.to_pixels(cx.rem_size()) * 1.3;
let focus_handle = self.editor.focus_handle(cx);
@@ -98,6 +111,14 @@ impl Render for MessageEditor {
.gap_2()
.p_2()
.bg(cx.theme().colors().editor_background)
.child(
h_flex().gap_2().child(ContextPicker::new(
model.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 +144,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()
@@ -154,11 +166,11 @@ impl Render for MessageEditor {
.layer(ElevationIndex::ModalSurface)
.child(Label::new("Chat"))
.children(
KeyBinding::for_action_in(&Chat, &focus_handle, cx)
KeyBinding::for_action_in(&Chat, &focus_handle, model, cx)
.map(|binding| binding.into_any_element()),
)
.on_click(move |_event, cx| {
focus_handle.dispatch_action(&Chat, cx);
focus_handle.dispatch_action(&Chat, model, cx);
}),
),
),

View File

@@ -2,24 +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, SharedString, Task};
use gpui::{AppContext, EventEmitter, 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);
@@ -39,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,
@@ -50,8 +71,12 @@ pub struct Thread {
}
impl Thread {
pub fn new(tools: Arc<ToolWorkingSet>, _cx: &mut ModelContext<Self>) -> Self {
pub fn new(tools: Arc<ToolWorkingSet>, model: &Model<Self>, _cx: &mut AppContext) -> 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,
@@ -63,6 +88,40 @@ 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>,
model: &Model<Self>,
cx: &mut AppContext,
) {
self.summary = Some(summary.into());
model.emit(ThreadEvent::SummaryChanged, cx);
}
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()
}
@@ -75,12 +134,30 @@ 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>,
model: &Model<Self>,
cx: &mut AppContext,
) {
self.insert_message(Role::User, text, model, cx)
}
pub fn insert_message(
&mut self,
role: Role,
text: impl Into<String>,
model: &Model<Self>,
cx: &mut AppContext,
) {
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();
model.emit(ThreadEvent::MessageAdded(id), cx);
}
pub fn to_completion_request(
@@ -134,7 +211,8 @@ impl Thread {
&mut self,
request: LanguageModelRequest,
model: Arc<dyn LanguageModel>,
cx: &mut ModelContext<Self>,
model: &Model<Self>,
cx: &mut AppContext,
) {
let pending_completion_id = post_inc(&mut self.completion_count);
@@ -150,11 +228,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;
@@ -163,6 +237,13 @@ impl Thread {
if let Some(last_message) = thread.messages.last_mut() {
if last_message.role == Role::Assistant {
last_message.text.push_str(&chunk);
model.emit(
cx,
ThreadEvent::StreamedAssistantText(
last_message.id,
chunk,
),
);
}
}
}
@@ -192,17 +273,22 @@ impl Thread {
}
}
cx.emit(ThreadEvent::StreamedCompletion);
cx.notify();
thread.touch_updated_at();
model.emit(ThreadEvent::StreamedCompletion, cx);
model.notify(cx);
})?;
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)
@@ -214,25 +300,31 @@ impl Thread {
.update(&mut cx, |_thread, cx| match result.as_ref() {
Ok(stop_reason) => match stop_reason {
StopReason::ToolUse => {
cx.emit(ThreadEvent::UsePendingTools);
model.emit(ThreadEvent::UsePendingTools, cx);
}
StopReason::EndTurn => {}
StopReason::MaxTokens => {}
},
Err(error) => {
if error.is::<PaymentRequiredError>() {
cx.emit(ThreadEvent::ShowError(ThreadError::PaymentRequired));
model.emit(ThreadEvent::ShowError(ThreadError::PaymentRequired), cx);
} else if error.is::<MaxMonthlySpendReachedError>() {
cx.emit(ThreadEvent::ShowError(ThreadError::MaxMonthlySpendReached));
model.emit(
cx,
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()),
)));
model.emit(
cx,
ThreadEvent::ShowError(ThreadError::Message(SharedString::from(
error_message.clone(),
))),
);
}
}
})
@@ -245,12 +337,66 @@ impl Thread {
});
}
pub fn summarize(&mut self, model: &Model<Self>, cx: &mut AppContext) {
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 = model.spawn(cx, |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, model, cx| {
if !new_summary.is_empty() {
this.summary = Some(new_summary.into());
}
model.emit(ThreadEvent::SummaryChanged, cx);
})?;
anyhow::Ok(())
}
.log_err()
});
}
pub fn insert_tool_output(
&mut self,
assistant_message_id: MessageId,
tool_use_id: LanguageModelToolUseId,
output: Task<Result<String>>,
cx: &mut ModelContext<Self>,
model: &Model<Self>,
cx: &mut AppContext,
) {
let insert_output_task = cx.spawn(|thread, mut cx| {
let tool_use_id = tool_use_id.clone();
@@ -276,7 +422,7 @@ impl Thread {
is_error: false,
});
cx.emit(ThreadEvent::ToolFinished { tool_use_id });
model.emit(ThreadEvent::ToolFinished { tool_use_id }, cx);
}
Err(err) => {
tool_results.push(LanguageModelToolResult {
@@ -316,6 +462,9 @@ pub enum ThreadError {
pub enum ThreadEvent {
ShowError(ThreadError),
StreamedCompletion,
StreamedAssistantText(MessageId, String),
MessageAdded(MessageId),
SummaryChanged,
UsePendingTools,
ToolFinished {
#[allow(unused)]

View File

@@ -0,0 +1,164 @@
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: WeakModel<AssistantPanel>,
thread_store: Model<ThreadStore>,
scroll_handle: UniformListScrollHandle,
}
impl ThreadHistory {
pub(crate) fn new(
assistant_panel: WeakModel<AssistantPanel>,
thread_store: Model<ThreadStore>,
model: &Model<Self>,
cx: &mut AppContext,
) -> Self {
Self {
focus_handle: window.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,
model: &Model<Self>,
window: &mut gpui::Window,
cx: &mut AppContext,
) -> impl IntoElement {
let threads = self
.thread_store
.update(cx, |this, model, 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: WeakModel<AssistantPanel>,
}
impl PastThread {
pub fn new(thread: Model<Thread>, assistant_panel: WeakModel<AssistantPanel>) -> Self {
Self {
thread,
assistant_panel,
}
}
}
impl RenderOnce for PastThread {
fn render(self, _window: &mut gpui::Window, cx: &mut gpui::AppContext) -> 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, model, _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, model, 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, model, cx| {
this.open_thread(&id, cx);
})
.ok();
}
})
}
}

View File

@@ -5,16 +5,20 @@ 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 gpui::{prelude::*, AppContext, Model, 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 {
@@ -24,19 +28,26 @@ impl ThreadStore {
cx: &mut AppContext,
) -> Task<Result<Model<Self>>> {
cx.spawn(|mut cx| async move {
let this = cx.new_model(|cx: &mut ModelContext<Self>| {
let this = cx.new_model(|model: &Model<Self>, cx: &mut AppContext| {
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 context_server_manager = cx.new_model(|model, cx| {
ContextServerManager::new(
context_server_factory_registry,
project.clone(),
model,
cx,
)
});
let this = Self {
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
@@ -46,7 +57,49 @@ impl ThreadStore {
})
}
fn register_context_server_handlers(&self, cx: &mut ModelContext<Self>) {
pub fn threads(&self, model: &Model<Self>, cx: &AppContext) -> 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,
model: &Model<Self>,
cx: &AppContext,
) -> Vec<Model<Thread>> {
self.threads(cx).into_iter().take(limit).collect()
}
pub fn create_thread(&mut self, model: &Model<Self>, cx: &mut AppContext) -> Model<Thread> {
let thread = cx.new_model(|model, cx| Thread::new(self.tools.clone(), model, cx));
self.threads.push(thread.clone());
thread
}
pub fn open_thread(
&self,
id: &ThreadId,
model: &Model<Self>,
cx: &mut AppContext,
) -> Option<Model<Thread>> {
self.threads
.iter()
.find(|thread| thread.read(cx).id() == id)
.cloned()
}
pub fn delete_thread(&mut self, id: &ThreadId, model: &Model<Self>, cx: &mut AppContext) {
self.threads.retain(|thread| thread.read(cx).id() != id);
}
fn register_context_server_handlers(&self, model: &Model<Self>, cx: &mut AppContext) {
cx.subscribe(
&self.context_server_manager.clone(),
Self::handle_context_server_event,
@@ -58,7 +111,8 @@ impl ThreadStore {
&mut self,
context_server_manager: Model<ContextServerManager>,
event: &context_server::manager::Event,
cx: &mut ModelContext<Self>,
model: &Model<Self>,
cx: &mut AppContext,
) {
let tool_working_set = self.tools.clone();
match event {
@@ -112,3 +166,93 @@ impl ThreadStore {
}
}
}
impl ThreadStore {
/// Creates some mocked recent threads for testing purposes.
fn mock_recent_threads(&mut self, model: &Model<Self>, cx: &mut AppContext) {
use language_model::Role;
self.threads.push(cx.new_model(|model, cx| {
let mut thread = Thread::new(self.tools.clone(), model, cx);
thread.set_summary("Introduction to quantum computing", model, cx);
thread.insert_user_message("Hello! Can you help me understand quantum computing?", model, 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?", model, cx);
thread.insert_user_message("That's interesting! Can you explain how quantum entanglement is used in quantum computing?", model, 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.", model, cx);
thread
}));
self.threads.push(cx.new_model(|model, cx| {
let mut thread = Thread::new(self.tools.clone(), model, cx);
thread.set_summary("Rust web development and async programming", model, cx);
thread.insert_user_message("Can you show me an example of Rust code for a simple web server?", model, 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(), model, cx);
thread.insert_user_message("That's great! Can you explain more about async functions in Rust?", model, 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(), model, cx);
thread
}));
}
}

View File

@@ -6,7 +6,7 @@ pub use crate::slash_command_registry::*;
use anyhow::Result;
use futures::stream::{self, BoxStream};
use futures::StreamExt;
use gpui::{AnyElement, AppContext, ElementId, SharedString, Task, WeakView, WindowContext};
use gpui::{AnyElement, AppContext, ElementId, SharedString, Task, WeakView};
use language::{BufferSnapshot, CodeLabel, LspAdapterDelegate, OffsetRangeExt};
pub use language_model::Role;
use serde::{Deserialize, Serialize};
@@ -78,8 +78,9 @@ pub trait SlashCommand: 'static + Send + Sync {
self: Arc<Self>,
arguments: &[String],
cancel: Arc<AtomicBool>,
workspace: Option<WeakView<Workspace>>,
cx: &mut WindowContext,
workspace: Option<WeakModel<Workspace>>,
window: &mut gpui::Window,
cx: &mut gpui::AppContext,
) -> Task<Result<Vec<ArgumentCompletion>>>;
fn requires_argument(&self) -> bool;
fn accepts_arguments(&self) -> bool {
@@ -90,21 +91,27 @@ pub trait SlashCommand: 'static + Send + Sync {
arguments: &[String],
context_slash_command_output_sections: &[SlashCommandOutputSection<language::Anchor>],
context_buffer: BufferSnapshot,
workspace: WeakView<Workspace>,
workspace: WeakModel<Workspace>,
// TODO: We're just using the `LspAdapterDelegate` here because that is
// what the extension API is already expecting.
//
// It may be that `LspAdapterDelegate` needs a more general name, or
// perhaps another kind of delegate is needed here.
delegate: Option<Arc<dyn LspAdapterDelegate>>,
cx: &mut WindowContext,
window: &mut gpui::Window,
cx: &mut gpui::AppContext,
) -> Task<SlashCommandResult>;
}
pub type RenderFoldPlaceholder = Arc<
dyn Send
+ Sync
+ Fn(ElementId, Arc<dyn Fn(&mut WindowContext)>, &mut WindowContext) -> AnyElement,
+ Fn(
ElementId,
Arc<dyn Fn(&mut gpui::Window, &mut gpui::AppContext)>,
&mut gpui::Window,
&mut gpui::AppContext,
) -> AnyElement,
>;
#[derive(Debug, PartialEq)]

View File

@@ -4,7 +4,7 @@ use std::sync::{atomic::AtomicBool, Arc};
use anyhow::Result;
use async_trait::async_trait;
use extension::{Extension, ExtensionHostProxy, ExtensionSlashCommandProxy, WorktreeDelegate};
use gpui::{AppContext, Task, WeakView, WindowContext};
use gpui::{AppContext, Task, WeakView};
use language::{BufferSnapshot, LspAdapterDelegate};
use ui::prelude::*;
use workspace::Workspace;
@@ -97,8 +97,9 @@ impl SlashCommand for ExtensionSlashCommand {
self: Arc<Self>,
arguments: &[String],
_cancel: Arc<AtomicBool>,
_workspace: Option<WeakView<Workspace>>,
cx: &mut WindowContext,
_workspace: Option<WeakModel<Workspace>>,
window: &mut gpui::Window,
cx: &mut gpui::AppContext,
) -> Task<Result<Vec<ArgumentCompletion>>> {
let command = self.command.clone();
let arguments = arguments.to_owned();
@@ -127,9 +128,10 @@ impl SlashCommand for ExtensionSlashCommand {
arguments: &[String],
_context_slash_command_output_sections: &[SlashCommandOutputSection<language::Anchor>],
_context_buffer: BufferSnapshot,
_workspace: WeakView<Workspace>,
_workspace: WeakModel<Workspace>,
delegate: Option<Arc<dyn LspAdapterDelegate>>,
cx: &mut WindowContext,
window: &mut gpui::Window,
cx: &mut gpui::AppContext,
) -> Task<SlashCommandResult> {
let command = self.command.clone();
let arguments = arguments.to_owned();

View File

@@ -4,7 +4,7 @@ mod tool_working_set;
use std::sync::Arc;
use anyhow::Result;
use gpui::{AppContext, Task, WeakView, WindowContext};
use gpui::{AppContext, Task, WeakView};
use workspace::Workspace;
pub use crate::tool_registry::*;
@@ -31,7 +31,8 @@ pub trait Tool: 'static + Send + Sync {
fn run(
self: Arc<Self>,
input: serde_json::Value,
workspace: WeakView<Workspace>,
cx: &mut WindowContext,
workspace: WeakModel<Workspace>,
window: &mut gpui::Window,
cx: &mut gpui::AppContext,
) -> Task<Result<String>>;
}

View File

@@ -3,7 +3,7 @@ use std::sync::Arc;
use anyhow::{anyhow, Result};
use assistant_tool::Tool;
use chrono::{Local, Utc};
use gpui::{Task, WeakView, WindowContext};
use gpui::{Task, WeakView};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
@@ -41,8 +41,9 @@ impl Tool for NowTool {
fn run(
self: Arc<Self>,
input: serde_json::Value,
_workspace: WeakView<workspace::Workspace>,
_cx: &mut WindowContext,
_workspace: WeakModel<workspace::Workspace>,
_window: &mut gpui::Window,
_cx: &mut gpui::AppContext,
) -> Task<Result<String>> {
let input: FileToolInput = match serde_json::from_value(input) {
Ok(input) => input,

View File

@@ -3,8 +3,7 @@ use client::{Client, TelemetrySettings};
use db::kvp::KEY_VALUE_STORE;
use db::RELEASE_CHANNEL;
use gpui::{
actions, AppContext, AsyncAppContext, Context as _, Global, Model, ModelContext,
SemanticVersion, Task, WindowContext,
actions, AppContext, AsyncAppContext, Context as _, Global, Model, SemanticVersion, Task,
};
use http_client::{AsyncBody, HttpClient, HttpClientWithUrl};
use paths::remote_servers_dir;
@@ -131,16 +130,16 @@ pub fn init(http_client: Arc<HttpClientWithUrl>, cx: &mut AppContext) {
AutoUpdateSetting::register(cx);
cx.observe_new_views(|workspace: &mut Workspace, _cx| {
workspace.register_action(|_, action: &Check, cx| check(action, cx));
workspace.register_action(model, |_, action: &Check, cx| check(action, model, cx));
workspace.register_action(|_, action, cx| {
workspace.register_action(model, |_, action, cx| {
view_release_notes(action, cx);
});
})
.detach();
let version = release_channel::AppVersion::global(cx);
let auto_updater = cx.new_model(|cx| {
let auto_updater = cx.new_model(|model, cx| {
let updater = AutoUpdater::new(version, http_client);
let poll_for_updates = ReleaseChannel::try_global(cx)
@@ -153,7 +152,7 @@ pub fn init(http_client: Arc<HttpClientWithUrl>, cx: &mut AppContext) {
{
let mut update_subscription = AutoUpdateSetting::get_global(cx)
.0
.then(|| updater.start_polling(cx));
.then(|| updater.start_polling(model, cx));
cx.observe_global::<SettingsStore>(move |updater, cx| {
if AutoUpdateSetting::get_global(cx).0 {
@@ -172,7 +171,7 @@ pub fn init(http_client: Arc<HttpClientWithUrl>, cx: &mut AppContext) {
cx.set_global(GlobalAutoUpdate(Some(auto_updater)));
}
pub fn check(_: &Check, cx: &mut WindowContext) {
pub fn check(_: &Check, window: &mut gpui::Window, cx: &mut gpui::AppContext) {
if let Some(message) = option_env!("ZED_UPDATE_EXPLANATION") {
drop(cx.prompt(
gpui::PromptLevel::Info,
@@ -201,7 +200,7 @@ pub fn check(_: &Check, cx: &mut WindowContext) {
}
if let Some(updater) = AutoUpdater::get(cx) {
updater.update(cx, |updater, cx| updater.poll(cx));
updater.update(cx, |updater, model, cx| updater.poll(model, cx));
} else {
drop(cx.prompt(
gpui::PromptLevel::Info,
@@ -249,30 +248,30 @@ impl AutoUpdater {
}
}
pub fn start_polling(&self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
cx.spawn(|this, mut cx| async move {
pub fn start_polling(&self, model: &Model<Self>, cx: &mut AppContext) -> Task<Result<()>> {
model.spawn(cx, |this, mut cx| async move {
loop {
this.update(&mut cx, |this, cx| this.poll(cx))?;
this.update(&mut cx, |this, model, cx| this.poll(model, cx))?;
cx.background_executor().timer(POLL_INTERVAL).await;
}
})
}
pub fn poll(&mut self, cx: &mut ModelContext<Self>) {
pub fn poll(&mut self, model: &Model<Self>, cx: &mut AppContext) {
if self.pending_poll.is_some() || self.status.is_updated() {
return;
}
cx.notify();
model.notify(cx);
self.pending_poll = Some(cx.spawn(|this, mut cx| async move {
self.pending_poll = Some(model.spawn(cx, |this, mut cx| async move {
let result = Self::update(this.upgrade()?, cx.clone()).await;
this.update(&mut cx, |this, cx| {
this.update(&mut cx, |this, model, cx| {
this.pending_poll = None;
if let Err(error) = result {
log::error!("auto-update failed: error:{:?}", error);
this.status = AutoUpdateStatus::Errored;
cx.notify();
model.notify(cx);
}
})
.ok()
@@ -287,9 +286,9 @@ impl AutoUpdater {
self.status.clone()
}
pub fn dismiss_error(&mut self, cx: &mut ModelContext<Self>) {
pub fn dismiss_error(&mut self, model: &Model<Self>, cx: &mut AppContext) {
self.status = AutoUpdateStatus::Idle;
cx.notify();
model.notify(cx);
}
// If you are packaging Zed and need to override the place it downloads SSH remotes from,
@@ -432,15 +431,16 @@ impl AutoUpdater {
}
async fn update(this: Model<Self>, mut cx: AsyncAppContext) -> Result<()> {
let (client, current_version, release_channel) = this.update(&mut cx, |this, cx| {
this.status = AutoUpdateStatus::Checking;
cx.notify();
(
this.http_client.clone(),
this.current_version,
ReleaseChannel::try_global(cx),
)
})?;
let (client, current_version, release_channel) =
this.update(&mut cx, |this, model, cx| {
this.status = AutoUpdateStatus::Checking;
model.notify(cx);
(
this.http_client.clone(),
this.current_version,
ReleaseChannel::try_global(cx),
)
})?;
let release =
Self::get_latest_release(&this, "zed", OS, ARCH, release_channel, &mut cx).await?;
@@ -455,16 +455,16 @@ impl AutoUpdater {
};
if !should_download {
this.update(&mut cx, |this, cx| {
this.update(&mut cx, |this, model, cx| {
this.status = AutoUpdateStatus::Idle;
cx.notify();
model.notify(cx);
})?;
return Ok(());
}
this.update(&mut cx, |this, cx| {
this.update(&mut cx, |this, model, cx| {
this.status = AutoUpdateStatus::Downloading;
cx.notify();
model.notify(cx);
})?;
let temp_dir = tempfile::Builder::new()
@@ -485,9 +485,9 @@ impl AutoUpdater {
let downloaded_asset = temp_dir.path().join(filename);
download_release(&downloaded_asset, release, client, &cx).await?;
this.update(&mut cx, |this, cx| {
this.update(&mut cx, |this, model, cx| {
this.status = AutoUpdateStatus::Installing;
cx.notify();
model.notify(cx);
})?;
let binary_path = match OS {
@@ -496,11 +496,11 @@ impl AutoUpdater {
_ => Err(anyhow!("not supported: {:?}", OS)),
}?;
this.update(&mut cx, |this, cx| {
this.update(&mut cx, |this, model, cx| {
this.set_should_show_update_notification(true, cx)
.detach_and_log_err(cx);
this.status = AutoUpdateStatus::Updated { binary_path };
cx.notify();
model.notify(cx);
})?;
Ok(())

View File

@@ -2,7 +2,7 @@ mod update_notification;
use auto_update::AutoUpdater;
use editor::{Editor, MultiBuffer};
use gpui::{actions, prelude::*, AppContext, SharedString, View, ViewContext};
use gpui::{actions, prelude::*, AppContext, Model, SharedString, View};
use http_client::HttpClient;
use markdown_preview::markdown_preview_view::{MarkdownPreviewMode, MarkdownPreviewView};
use release_channel::{AppVersion, ReleaseChannel};
@@ -18,8 +18,8 @@ actions!(auto_update, [ViewReleaseNotesLocally]);
pub fn init(cx: &mut AppContext) {
cx.observe_new_views(|workspace: &mut Workspace, _cx| {
workspace.register_action(|workspace, _: &ViewReleaseNotesLocally, cx| {
view_release_notes_locally(workspace, cx);
workspace.register_action(model, |workspace, _: &ViewReleaseNotesLocally, cx| {
view_release_notes_locally(workspace, model, cx);
});
})
.detach();
@@ -31,7 +31,11 @@ struct ReleaseNotesBody {
release_notes: String,
}
fn view_release_notes_locally(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) {
fn view_release_notes_locally(
workspace: &mut Workspace,
model: &Model<Workspace>,
cx: &mut AppContext,
) {
let release_channel = ReleaseChannel::global(cx);
let url = match release_channel {
@@ -60,7 +64,7 @@ fn view_release_notes_locally(workspace: &mut Workspace, cx: &mut ViewContext<Wo
.language_for_name("Markdown");
workspace
.with_local_workspace(cx, move |_, cx| {
.with_local_workspace(model, cx, move |_, model, cx| {
cx.spawn(|workspace, mut cx| async move {
let markdown = markdown.await.log_err();
let response = client.get(&url, Default::default(), true).await;
@@ -78,27 +82,29 @@ fn view_release_notes_locally(workspace: &mut Workspace, cx: &mut ViewContext<Wo
workspace
.update(&mut cx, |workspace, cx| {
let project = workspace.project().clone();
let buffer = project.update(cx, |project, cx| {
let buffer = project.update(cx, |project, model, cx| {
project.create_local_buffer("", markdown, cx)
});
buffer.update(cx, |buffer, cx| {
buffer.update(cx, |buffer, model, cx| {
buffer.edit([(0..0, body.release_notes)], None, cx)
});
let language_registry = project.read(cx).languages().clone();
let buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer, cx));
let buffer =
cx.new_model(|model, cx| MultiBuffer::singleton(buffer, model, cx));
let tab_description = SharedString::from(body.title.to_string());
let editor = cx.new_view(|cx| {
Editor::for_multibuffer(buffer, Some(project), true, cx)
let editor = cx.new_model(|model, cx| {
Editor::for_multibuffer(buffer, Some(project), true, model, cx)
});
let workspace_handle = workspace.weak_handle();
let view: View<MarkdownPreviewView> = MarkdownPreviewView::new(
let view: Model<MarkdownPreviewView> = MarkdownPreviewView::new(
MarkdownPreviewMode::Default,
editor,
workspace_handle,
language_registry,
Some(tab_description),
model,
cx,
);
workspace.add_item_to_active_pane(
@@ -107,7 +113,7 @@ fn view_release_notes_locally(workspace: &mut Workspace, cx: &mut ViewContext<Wo
true,
cx,
);
cx.notify();
model.notify(cx);
})
.log_err();
}
@@ -117,7 +123,7 @@ fn view_release_notes_locally(workspace: &mut Workspace, cx: &mut ViewContext<Wo
.detach();
}
pub fn notify_of_any_new_update(cx: &mut ViewContext<Workspace>) -> Option<()> {
pub fn notify_of_any_new_update(model: &Model<Workspace>, cx: &mut AppContext) -> Option<()> {
let updater = AutoUpdater::get(cx)?;
let version = updater.read(cx).current_version();
let should_show_notification = updater.read(cx).should_show_update_notification(cx);
@@ -130,9 +136,9 @@ pub fn notify_of_any_new_update(cx: &mut ViewContext<Workspace>) -> Option<()> {
workspace.show_notification(
NotificationId::unique::<UpdateNotification>(),
cx,
|cx| cx.new_view(|_| UpdateNotification::new(version, workspace_handle)),
|cx| cx.new_model(|_, _| UpdateNotification::new(version, workspace_handle)),
);
updater.update(cx, |updater, cx| {
updater.update(cx, |updater, model, cx| {
updater
.set_should_show_update_notification(false, cx)
.detach_and_log_err(cx);

View File

@@ -1,6 +1,6 @@
use gpui::{
div, DismissEvent, EventEmitter, InteractiveElement, IntoElement, ParentElement, Render,
SemanticVersion, StatefulInteractiveElement, Styled, ViewContext, WeakView,
div, AppContext, DismissEvent, EventEmitter, InteractiveElement, IntoElement, ParentElement,
Render, SemanticVersion, StatefulInteractiveElement, Styled, WeakView,
};
use menu::Cancel;
use release_channel::ReleaseChannel;
@@ -12,13 +12,18 @@ use workspace::{
pub struct UpdateNotification {
version: SemanticVersion,
workspace: WeakView<Workspace>,
workspace: WeakModel<Workspace>,
}
impl EventEmitter<DismissEvent> for UpdateNotification {}
impl Render for UpdateNotification {
fn render(&mut self, cx: &mut gpui::ViewContext<Self>) -> impl IntoElement {
fn render(
&mut self,
model: &Model<Self>,
window: &mut gpui::Window,
cx: &mut AppContext,
) -> impl IntoElement {
let app_name = ReleaseChannel::global(cx).display_name();
v_flex()
@@ -37,7 +42,10 @@ impl Render for UpdateNotification {
.id("cancel")
.child(Icon::new(IconName::Close))
.cursor_pointer()
.on_click(cx.listener(|this, _, cx| this.dismiss(&menu::Cancel, cx))),
.on_click(
model
.listener(|this, model, _, cx| this.dismiss(&menu::Cancel, cx)),
),
),
)
.child(
@@ -45,10 +53,10 @@ impl Render for UpdateNotification {
.id("notes")
.child(Label::new("View the release notes"))
.cursor_pointer()
.on_click(cx.listener(|this, _, cx| {
.on_click(model.listener(|this, model, _, cx| {
this.workspace
.update(cx, |workspace, cx| {
crate::view_release_notes_locally(workspace, cx);
.update(cx, |workspace, model, cx| {
crate::view_release_notes_locally(workspace, model, cx);
})
.log_err();
this.dismiss(&menu::Cancel, cx)
@@ -58,11 +66,11 @@ impl Render for UpdateNotification {
}
impl UpdateNotification {
pub fn new(version: SemanticVersion, workspace: WeakView<Workspace>) -> Self {
pub fn new(version: SemanticVersion, workspace: WeakModel<Workspace>) -> Self {
Self { version, workspace }
}
pub fn dismiss(&mut self, _: &Cancel, cx: &mut ViewContext<Self>) {
cx.emit(DismissEvent);
pub fn dismiss(&mut self, _: &Cancel, model: &Model<Self>, cx: &mut AppContext) {
model.emit(DismissEvent, cx);
}
}

View File

@@ -1,7 +1,7 @@
use editor::Editor;
use gpui::{
Element, EventEmitter, FocusableView, IntoElement, ParentElement, Render, StyledText,
Subscription, ViewContext,
AppContext, Element, EventEmitter, FocusableView, IntoElement, Model, ParentElement, Render,
StyledText, Subscription,
};
use itertools::Itertools;
use std::cmp;
@@ -37,7 +37,12 @@ impl Breadcrumbs {
impl EventEmitter<ToolbarItemEvent> for Breadcrumbs {}
impl Render for Breadcrumbs {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
fn render(
&mut self,
model: &Model<Self>,
window: &mut gpui::Window,
cx: &mut AppContext,
) -> impl IntoElement {
const MAX_SEGMENTS: usize = 12;
let element = h_flex().text_ui(cx);
let Some(active_item) = self.active_item.as_ref() else {
@@ -64,7 +69,7 @@ impl Render for Breadcrumbs {
}
let highlighted_segments = segments.into_iter().map(|segment| {
let mut text_style = cx.text_style();
let mut text_style = window.text_style();
if let Some(font) = segment.font {
text_style.font_family = font.family;
text_style.font_features = font.features;
@@ -94,23 +99,25 @@ impl Render for Breadcrumbs {
let editor = editor.clone();
move |_, cx| {
if let Some(editor) = editor.upgrade() {
outline::toggle(editor, &editor::actions::ToggleOutline, cx)
outline::toggle(editor, &editor::actions::ToggleOutline, model, cx)
}
}
})
.tooltip(move |cx| {
.tooltip(move |window, cx| {
if let Some(editor) = editor.upgrade() {
let focus_handle = editor.read(cx).focus_handle(cx);
Tooltip::for_action_in(
"Show symbol outline",
&editor::actions::ToggleOutline,
&focus_handle,
window,
cx,
)
} else {
Tooltip::for_action(
"Show symbol outline",
&editor::actions::ToggleOutline,
window,
cx,
)
}
@@ -128,26 +135,30 @@ impl ToolbarItemView for Breadcrumbs {
fn set_active_pane_item(
&mut self,
active_pane_item: Option<&dyn ItemHandle>,
cx: &mut ViewContext<Self>,
model: &Model<Self>,
cx: &mut AppContext,
) -> ToolbarItemLocation {
cx.notify();
model.notify(cx);
self.active_item = None;
let Some(item) = active_pane_item else {
return ToolbarItemLocation::Hidden;
};
let this = cx.view().downgrade();
let this = model.downgrade();
self.subscription = Some(item.subscribe_to_item_events(
cx,
Box::new(move |event, cx| {
if let ItemEvent::UpdateBreadcrumbs = event {
this.update(cx, |this, cx| {
cx.notify();
this.update(cx, |this, model, cx| {
model.notify(cx);
if let Some(active_item) = this.active_item.as_ref() {
cx.emit(ToolbarItemEvent::ChangeLocation(
active_item.breadcrumb_location(cx),
))
model.emit(
cx,
ToolbarItemEvent::ChangeLocation(
active_item.breadcrumb_location(cx),
),
)
}
})
.ok();
@@ -158,7 +169,7 @@ impl ToolbarItemView for Breadcrumbs {
item.breadcrumb_location(cx)
}
fn pane_focus_update(&mut self, pane_focused: bool, _: &mut ViewContext<Self>) {
fn pane_focus_update(&mut self, pane_focused: bool, _: &Model<Self>, _: &mut AppContext) {
self.pane_focused = pane_focused;
}
}

View File

@@ -17,7 +17,7 @@ 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"
]
@@ -27,11 +27,11 @@ 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
@@ -41,13 +41,24 @@ serde_derive.workspace = true
settings.workspace = true
util.workspace = true
[target.'cfg(target_os = "macos")'.dependencies]
livekit_client_macos = { workspace = true }
[target.'cfg(not(target_os = "macos"))'.dependencies]
livekit_client = { workspace = true }
[dev-dependencies]
client = { workspace = true, features = ["test-support"] }
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,13 @@
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(target_os = "macos")]
mod macos;
pub use participant::ParticipantLocation;
pub use room::Room;
#[cfg(target_os = "macos")]
pub use macos::*;
struct GlobalActiveCall(Model<ActiveCall>);
#[cfg(not(target_os = "macos"))]
mod cross_platform;
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);
}
}
#[cfg(not(target_os = "macos"))]
pub use cross_platform::*;

View File

@@ -0,0 +1,584 @@
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, 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(|model, cx| ActiveCall::new(client, user_store, model, 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>,
model: &Model<Self>,
_: &mut AppContext,
) -> 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(model.downgrade(), Self::handle_incoming_call),
client.add_message_handler(model.downgrade(), 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, model, cx| {
user_store.get_users(envelope.payload.participant_user_ids, model, cx)
})?
.await?,
calling_user: user_store
.update(&mut cx, |user_store, model, cx| {
user_store.get_user(envelope.payload.calling_user_id, model, 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>>,
model: &Model<Self>,
cx: &mut AppContext,
) -> Task<Result<()>> {
if !self.pending_invites.insert(called_user_id) {
return Task::ready(Err(anyhow!("user was already invited")));
}
model.notify(cx);
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, model, cx| {
room.share_project(initial_project, model, cx)
})?
.await?,
)
} else {
None
};
room.update(&mut cx, move |room, model, cx| {
room.call(called_user_id, initial_project_id, model, cx)
})?
.await?;
anyhow::Ok(())
})
} else {
let client = self.client.clone();
let user_store = self.user_store.clone();
let room = model
.spawn(cx, 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, model, cx| {
this.set_room(Some(room.clone()), model, 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(())
})
};
model.spawn(cx, move |this, mut cx| async move {
let result = invite.await;
if result.is_ok() {
this.update(&mut cx, |this, _model, cx| {
this.report_call_event("invite", cx)
})?;
} else {
//TODO: report collaboration error
log::error!("invite failed: {:?}", result);
}
this.update(&mut cx, |this, model, cx| {
this.pending_invites.remove(&called_user_id);
model.notify(cx);
})?;
result
})
}
pub fn cancel_invite(
&mut self,
called_user_id: u64,
_: &Model<Self>,
cx: &mut AppContext,
) -> 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,
model: &Model<Self>,
cx: &mut AppContext,
) -> 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));
model.spawn(cx, |this, mut cx| async move {
let room = join.await?;
this.update(&mut cx, |this, model, cx| {
this.set_room(room.clone(), model, cx)
})?
.await?;
this.update(&mut cx, |this, model, cx| {
this.report_call_event("accept incoming", cx)
})?;
Ok(())
})
}
pub fn decline_incoming(&mut self, _: &Model<Self>, _: &mut AppContext) -> 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,
model: &Model<Self>,
cx: &mut AppContext,
) -> 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, model, 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
});
model.spawn(cx, |this, mut cx| async move {
let room = join.await?;
this.update(&mut cx, |this, model, cx| {
this.set_room(room.clone(), model, cx)
})?
.await?;
this.update(&mut cx, |this, model, cx| {
this.report_call_event("join channel", cx)
})?;
Ok(room)
})
}
pub fn hang_up(&mut self, model: &Model<Self>, cx: &mut AppContext) -> Task<Result<()>> {
model.notify(cx);
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() {
model.emit(Event::RoomLeft { channel_id }, cx);
room.update(cx, |room, model, cx| room.leave(model, cx))
} else {
Task::ready(Ok(()))
}
}
pub fn share_project(
&mut self,
project: Model<Project>,
model: &Model<Self>,
cx: &mut AppContext,
) -> Task<Result<u64>> {
if let Some((room, _)) = self.room.as_ref() {
self.report_call_event("share project", cx);
room.update(cx, |room, model, cx| room.share_project(project, model, cx))
} else {
Task::ready(Err(anyhow!("no active call")))
}
}
pub fn unshare_project(
&mut self,
project: Model<Project>,
model: &Model<Self>,
cx: &mut AppContext,
) -> Result<()> {
if let Some((room, _)) = self.room.as_ref() {
self.report_call_event("unshare project", cx);
room.update(cx, |room, model, cx| {
room.unshare_project(project, model, 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>>,
model: &Model<Self>,
cx: &mut AppContext,
) -> 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, model, cx| room.set_location(project, model, cx));
}
}
Task::ready(Ok(()))
}
fn set_room(
&mut self,
room: Option<Model<Room>>,
model: &Model<Self>,
cx: &mut AppContext,
) -> Task<Result<()>> {
if room.as_ref() == self.room.as_ref().map(|room| &room.0) {
Task::ready(Ok(()))
} else {
model.notify(cx);
if let Some(room) = room {
if room.read(cx).status().is_offline() {
self.room = None;
Task::ready(Ok(()))
} else {
let subscriptions = vec![
model.observe(&room, cx, |this, room, model, cx| {
if room.read(cx).status().is_offline() {
this.set_room(None, model, cx).detach_and_log_err(cx)
}
model.notify(cx);
}),
model.subscribe(&room, cx, |_, _, event, model, cx| {
model.emit(event.clone(), cx)
}),
];
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();
model.emit(Event::RoomJoined { channel_id }, cx);
room.update(cx, |room, model, cx| {
room.set_location(location.as_ref(), model, 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,577 @@
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, 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(|model, cx| ActiveCall::new(client, user_store, model, 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>,
model: &Model<Self>,
cx: &mut AppContext,
) -> 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(model.downgrade(), Self::handle_incoming_call),
client.add_message_handler(model.downgrade(), 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, model, cx| {
user_store.get_users(envelope.payload.participant_user_ids, model, cx)
})?
.await?,
calling_user: user_store
.update(&mut cx, |user_store, model, cx| {
user_store.get_user(envelope.payload.calling_user_id, model, 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>>,
model: &Model<Self>,
cx: &mut AppContext,
) -> Task<Result<()>> {
if !self.pending_invites.insert(called_user_id) {
return Task::ready(Err(anyhow!("user was already invited")));
}
model.notify(cx);
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, model, cx| {
room.share_project(initial_project, model, cx)
})?
.await?,
)
} else {
None
};
room.update(&mut cx, move |room, model, cx| {
room.call(called_user_id, initial_project_id, model, cx)
})?
.await?;
anyhow::Ok(())
})
} else {
let client = self.client.clone();
let user_store = self.user_store.clone();
let room = model
.spawn(cx, 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, model, cx| {
this.set_room(Some(room.clone()), model, 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(())
})
};
model.spawn(cx, move |this, mut cx| async move {
let result = invite.await;
if result.is_ok() {
this.update(&mut cx, |this, model, cx| {
this.report_call_event("invite", cx)
})?;
} else {
//TODO: report collaboration error
log::error!("invite failed: {:?}", result);
}
this.update(&mut cx, |this, model, cx| {
this.pending_invites.remove(&called_user_id);
model.notify(cx);
})?;
result
})
}
pub fn cancel_invite(
&mut self,
called_user_id: u64,
model: &Model<Self>,
cx: &mut AppContext,
) -> 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,
model: &Model<Self>,
cx: &mut AppContext,
) -> 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));
model.spawn(cx, |this, mut cx| async move {
let room = join.await?;
this.update(&mut cx, |this, model, cx| {
this.set_room(room.clone(), model, cx)
})?
.await?;
this.update(&mut cx, |this, model, cx| {
this.report_call_event("accept incoming", cx)
})?;
Ok(())
})
}
pub fn decline_incoming(&mut self, _: &Model<Self>, _: &mut AppContext) -> 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,
model: &Model<Self>,
cx: &mut AppContext,
) -> 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, model, 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
});
model.spawn(cx, |this, mut cx| async move {
let room = join.await?;
this.update(&mut cx, |this, model, cx| {
this.set_room(room.clone(), model, cx)
})?
.await?;
this.update(&mut cx, |this, model, cx| {
this.report_call_event("join channel", cx)
})?;
Ok(room)
})
}
pub fn hang_up(&mut self, model: &Model<Self>, cx: &mut AppContext) -> Task<Result<()>> {
model.notify(cx);
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() {
model.emit(Event::RoomLeft { channel_id }, cx);
room.update(cx, |room, model, cx| room.leave(model, cx))
} else {
Task::ready(Ok(()))
}
}
pub fn share_project(
&mut self,
project: Model<Project>,
model: &Model<Self>,
cx: &mut AppContext,
) -> Task<Result<u64>> {
if let Some((room, _)) = self.room.as_ref() {
self.report_call_event("share project", cx);
room.update(cx, |room, model, cx| room.share_project(project, model, cx))
} else {
Task::ready(Err(anyhow!("no active call")))
}
}
pub fn unshare_project(
&mut self,
project: Model<Project>,
model: &Model<Self>,
cx: &mut AppContext,
) -> Result<()> {
if let Some((room, _)) = self.room.as_ref() {
self.report_call_event("unshare project", cx);
room.update(cx, |room, model, cx| {
room.unshare_project(project, model, 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>>,
model: &Model<Self>,
cx: &mut AppContext,
) -> 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, model, cx| room.set_location(project, model, cx));
}
}
Task::ready(Ok(()))
}
fn set_room(
&mut self,
room: Option<Model<Room>>,
model: &Model<Self>,
cx: &mut AppContext,
) -> Task<Result<()>> {
if room.as_ref() == self.room.as_ref().map(|room| &room.0) {
Task::ready(Ok(()))
} else {
model.notify(cx);
if let Some(room) = room {
if room.read(cx).status().is_offline() {
self.room = None;
Task::ready(Ok(()))
} else {
let subscriptions = vec![
model.observe(&room, cx, |this, room, model, cx| {
if room.read(cx).status().is_offline() {
this.set_room(None, model, cx).detach_and_log_err(cx);
}
model.notify(cx);
}),
model.subscribe(&room, cx, |_, _, event, model, cx| {
model.emit(event.clone(), cx)
}),
];
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();
model.emit(Event::RoomJoined { channel_id }, cx);
room.update(cx, |room, model, cx| {
room.set_location(location.as_ref(), model, 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

@@ -11,11 +11,9 @@ use client::{
use collections::{BTreeMap, HashMap, HashSet};
use fs::Fs;
use futures::{FutureExt, StreamExt};
use gpui::{
AppContext, AsyncAppContext, Context, EventEmitter, Model, ModelContext, Task, WeakModel,
};
use gpui::{AppContext, AsyncAppContext, Context, EventEmitter, Model, 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 +95,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
@@ -110,14 +108,15 @@ impl Room {
live_kit_connection_info: Option<proto::LiveKitConnectionInfo>,
client: Arc<Client>,
user_store: Model<UserStore>,
cx: &mut ModelContext<Self>,
model: &Model<Self>,
cx: &mut AppContext,
) -> 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();
let _maintain_room = cx.spawn(|this, mut cx| async move {
let _maintain_room = model.spawn(cx, |this, mut cx| async move {
while let Some(status) = status.next().await {
let this = if let Some(this) = this.upgrade() {
this
@@ -125,15 +124,15 @@ impl Room {
break;
};
if status == live_kit_client::ConnectionState::Disconnected {
this.update(&mut cx, |this, cx| this.leave(cx).log_err())
if status == livekit_client_macos::ConnectionState::Disconnected {
this.update(&mut cx, |this, model, cx| this.leave(model, cx).log_err())
.ok();
break;
}
}
});
let _handle_updates = cx.spawn({
let _handle_updates = model.spawn(cx, {
let room = room.clone();
move |this, mut cx| async move {
let mut updates = room.updates();
@@ -144,8 +143,8 @@ impl Room {
break;
};
this.update(&mut cx, |this, cx| {
this.live_kit_room_updated(update, cx).log_err()
this.update(&mut cx, |this, model, cx| {
this.live_kit_room_updated(update, model, cx).log_err()
})
.ok();
}
@@ -153,21 +152,22 @@ impl Room {
});
let connect = room.connect(&connection_info.server_url, &connection_info.token);
cx.spawn(|this, mut cx| async move {
connect.await?;
this.update(&mut cx, |this, cx| {
if this.can_use_microphone() {
if let Some(live_kit) = &this.live_kit {
if !live_kit.muted_by_user && !live_kit.deafened {
return this.share_microphone(cx);
model
.spawn(cx, |this, mut cx| async move {
connect.await?;
this.update(&mut cx, |this, model, cx| {
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(model, cx);
}
}
}
}
Task::ready(Ok(()))
})?
.await
})
.detach_and_log_err(cx);
Task::ready(Ok(()))
})?
.await
})
.detach_and_log_err(cx);
Some(LiveKitRoom {
room,
@@ -184,7 +184,7 @@ impl Room {
None
};
let maintain_connection = cx.spawn({
let maintain_connection = model.spawn(cx, {
let client = client.clone();
move |this, cx| Self::maintain_connection(this, client.clone(), cx).log_err()
});
@@ -206,11 +206,11 @@ impl Room {
pending_participants: Default::default(),
pending_call_count: 0,
client_subscriptions: vec![
client.add_message_handler(cx.weak_model(), Self::handle_room_updated)
client.add_message_handler(model.downgrade(), Self::handle_room_updated)
],
_subscriptions: vec![
cx.on_release(Self::released),
cx.on_app_quit(Self::app_will_quit),
model.on_release(cx, Self::released),
model.on_app_quit(cx, Self::app_will_quit),
],
leave_when_empty: false,
pending_room_update: None,
@@ -233,13 +233,14 @@ impl Room {
cx.spawn(move |mut cx| async move {
let response = client.request(proto::CreateRoom {}).await?;
let room_proto = response.room.ok_or_else(|| anyhow!("invalid room"))?;
let room = cx.new_model(|cx| {
let room = cx.new_model(|model, cx| {
let mut room = Self::new(
room_proto.id,
None,
response.live_kit_connection_info,
client,
user_store,
model,
cx,
);
if let Some(participant) = room_proto.participants.first() {
@@ -250,8 +251,8 @@ impl Room {
let initial_project_id = if let Some(initial_project) = initial_project {
let initial_project_id = room
.update(&mut cx, |room, cx| {
room.share_project(initial_project.clone(), cx)
.update(&mut cx, |room, model, cx| {
room.share_project(initial_project.clone(), model, cx)
})?
.await?;
Some(initial_project_id)
@@ -260,9 +261,9 @@ impl Room {
};
let did_join = room
.update(&mut cx, |room, cx| {
.update(&mut cx, |room, model, cx| {
room.leave_when_empty = true;
room.call(called_user_id, initial_project_id, cx)
room.call(called_user_id, initial_project_id, model, cx)
})?
.await;
match did_join {
@@ -310,7 +311,11 @@ impl Room {
}
}
fn app_will_quit(&mut self, cx: &mut ModelContext<Self>) -> impl Future<Output = ()> {
fn app_will_quit(
&mut self,
model: &Model<Self>,
cx: &mut AppContext,
) -> impl Future<Output = ()> {
let task = if self.status.is_online() {
let leave = self.leave_internal(cx);
Some(cx.background_executor().spawn(async move {
@@ -338,19 +343,20 @@ impl Room {
mut cx: AsyncAppContext,
) -> Result<Model<Self>> {
let room_proto = response.room.ok_or_else(|| anyhow!("invalid room"))?;
let room = cx.new_model(|cx| {
let room = cx.new_model(|model, cx| {
Self::new(
room_proto.id,
response.channel_id.map(ChannelId),
response.live_kit_connection_info,
client,
user_store,
model,
cx,
)
})?;
room.update(&mut cx, |room, cx| {
room.update(&mut cx, |room, model, cx| {
room.leave_when_empty = room.channel_id.is_none();
room.apply_room_update(room_proto, cx)?;
room.apply_room_update(room_proto, model, cx)?;
anyhow::Ok(())
})??;
Ok(room)
@@ -364,8 +370,8 @@ impl Room {
&& self.pending_call_count == 0
}
pub(crate) fn leave(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
cx.notify();
pub(crate) fn leave(&mut self, model: &Model<Self>, cx: &mut AppContext) -> Task<Result<()>> {
model.notify(cx);
self.leave_internal(cx)
}
@@ -389,16 +395,16 @@ impl Room {
pub(crate) fn clear_state(&mut self, cx: &mut AppContext) {
for project in self.shared_projects.drain() {
if let Some(project) = project.upgrade() {
project.update(cx, |project, cx| {
project.unshare(cx).log_err();
project.update(cx, |project, model, cx| {
project.unshare(model, cx).log_err();
});
}
}
for project in self.joined_projects.drain() {
if let Some(project) = project.upgrade() {
project.update(cx, |project, cx| {
project.disconnected_from_host(cx);
project.close(cx);
project.update(cx, |project, model, cx| {
project.disconnected_from_host(model, cx);
project.close(model, cx);
});
}
}
@@ -428,9 +434,9 @@ impl Room {
this.upgrade()
.ok_or_else(|| anyhow!("room was dropped"))?
.update(&mut cx, |this, cx| {
.update(&mut cx, |this, model, cx| {
this.status = RoomStatus::Rejoining;
cx.notify();
model.notify(cx);
})?;
// Wait for client to re-establish a connection to the server.
@@ -444,7 +450,8 @@ impl Room {
log::info!("client reconnected, attempting to rejoin room");
let Some(this) = this.upgrade() else { break };
match this.update(&mut cx, |this, cx| this.rejoin(cx)) {
match this.update(&mut cx, |this, model, cx| this.rejoin(model, cx))
{
Ok(task) => {
if task.await.log_err().is_some() {
return true;
@@ -493,14 +500,15 @@ impl Room {
// we leave the room and return an error.
if let Some(this) = this.upgrade() {
log::info!("reconnection failed, leaving room");
this.update(&mut cx, |this, cx| this.leave(cx))?.await?;
this.update(&mut cx, |this, model, cx| this.leave(model, cx))?
.await?;
}
Err(anyhow!(
"can't reconnect to room: client failed to re-establish connection"
))
}
fn rejoin(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
fn rejoin(&mut self, model: &Model<Self>, cx: &mut AppContext) -> Task<Result<()>> {
let mut projects = HashMap::default();
let mut reshared_projects = Vec::new();
let mut rejoined_projects = Vec::new();
@@ -548,27 +556,29 @@ impl Room {
rejoined_projects,
});
cx.spawn(|this, mut cx| async move {
model.spawn(cx, |this, mut cx| async move {
let response = response.await?;
let message_id = response.message_id;
let response = response.payload;
let room_proto = response.room.ok_or_else(|| anyhow!("invalid room"))?;
this.update(&mut cx, |this, cx| {
this.update(&mut cx, |this, model, cx| {
this.status = RoomStatus::Online;
this.apply_room_update(room_proto, cx)?;
this.apply_room_update(room_proto, model, cx)?;
for reshared_project in response.reshared_projects {
if let Some(project) = projects.get(&reshared_project.id) {
project.update(cx, |project, cx| {
project.reshared(reshared_project, cx).log_err();
project.update(cx, |project, model, cx| {
project.reshared(reshared_project, model, cx).log_err();
});
}
}
for rejoined_project in response.rejoined_projects {
if let Some(project) = projects.get(&rejoined_project.id) {
project.update(cx, |project, cx| {
project.rejoined(rejoined_project, message_id, cx).log_err();
project.update(cx, |project, model, cx| {
project
.rejoined(rejoined_project, message_id, model, cx)
.log_err();
});
}
}
@@ -626,12 +636,13 @@ impl Room {
&mut self,
user_id: u64,
role: proto::ChannelRole,
cx: &ModelContext<Self>,
model: &Model<Self>,
cx: &AppContext,
) -> Task<Result<()>> {
let client = self.client.clone();
let room_id = self.id;
let role = role.into();
cx.spawn(|_, _| async move {
cx.spawn(|_| async move {
client
.request(proto::SetRoomParticipantRole {
room_id,
@@ -703,13 +714,16 @@ impl Room {
.payload
.room
.ok_or_else(|| anyhow!("invalid room"))?;
this.update(&mut cx, |this, cx| this.apply_room_update(room, cx))?
this.update(&mut cx, |this, model, cx| {
this.apply_room_update(room, model, cx)
})?
}
fn apply_room_update(
&mut self,
mut room: proto::Room,
cx: &mut ModelContext<Self>,
model: &Model<Self>,
cx: &mut AppContext,
) -> Result<()> {
// Filter ourselves out from the room's participants.
let local_participant_ix = room
@@ -731,18 +745,18 @@ impl Room {
.collect::<Vec<_>>();
let (remote_participants, pending_participants) =
self.user_store.update(cx, move |user_store, cx| {
self.user_store.update(cx, move |user_store, model, cx| {
(
user_store.get_users(remote_participant_user_ids, cx),
user_store.get_users(pending_participant_user_ids, cx),
user_store.get_users(remote_participant_user_ids, model, cx),
user_store.get_users(pending_participant_user_ids, model, cx),
)
});
self.pending_room_update = Some(cx.spawn(|this, mut cx| async move {
self.pending_room_update = Some(model.spawn(cx, |this, mut cx| async move {
let (remote_participants, pending_participants) =
futures::join!(remote_participants, pending_participants);
this.update(&mut cx, |this, cx| {
this.update(&mut cx, |this, model, cx| {
this.participant_user_ids.clear();
if let Some(participant) = local_participant {
@@ -754,18 +768,20 @@ impl Room {
if role == proto::ChannelRole::Guest {
for project in mem::take(&mut this.shared_projects) {
if let Some(project) = project.upgrade() {
this.unshare_project(project, cx).log_err();
this.unshare_project(project, model, cx).log_err();
}
}
this.local_participant.projects.clear();
if let Some(live_kit_room) = &mut this.live_kit {
live_kit_room.stop_publishing(cx);
live_kit_room.stop_publishing(model, cx);
}
}
this.joined_projects.retain(|project| {
if let Some(project) = project.upgrade() {
project.update(cx, |project, cx| project.set_role(role, cx));
project.update(cx, |project, model, cx| {
project.set_role(role, model, cx)
});
true
} else {
false
@@ -799,20 +815,23 @@ impl Room {
for project in &participant.projects {
if !old_projects.contains(&project.id) {
cx.emit(Event::RemoteProjectShared {
owner: user.clone(),
project_id: project.id,
worktree_root_names: project.worktree_root_names.clone(),
});
model.emit(
Event::RemoteProjectShared {
owner: user.clone(),
project_id: project.id,
worktree_root_names: project.worktree_root_names.clone(),
},
cx,
);
}
}
for unshared_project_id in old_projects.difference(&new_projects) {
this.joined_projects.retain(|project| {
if let Some(project) = project.upgrade() {
project.update(cx, |project, cx| {
project.update(cx, |project, model, cx| {
if project.remote_id() == Some(*unshared_project_id) {
project.disconnected_from_host(cx);
project.disconnected_from_host(model, cx);
false
} else {
true
@@ -822,9 +841,12 @@ impl Room {
false
}
});
cx.emit(Event::RemoteProjectUnshared {
project_id: *unshared_project_id,
});
model.emit(
Event::RemoteProjectUnshared {
project_id: *unshared_project_id,
},
cx,
);
}
let role = participant.role();
@@ -841,9 +863,12 @@ impl Room {
{
remote_participant.location = location;
remote_participant.role = role;
cx.emit(Event::ParticipantLocationChanged {
participant_id: peer_id,
});
model.emit(
Event::ParticipantLocationChanged {
participant_id: peer_id,
},
cx,
);
}
} else {
this.remote_participants.insert(
@@ -876,6 +901,7 @@ impl Room {
for track in video_tracks {
this.live_kit_room_updated(
RoomUpdate::SubscribedToRemoteVideoTrack(track),
model,
cx,
)
.log_err();
@@ -889,6 +915,7 @@ impl Room {
track.clone(),
publication.clone(),
),
model,
cx,
)
.log_err();
@@ -902,9 +929,12 @@ impl Room {
true
} else {
for project in &participant.projects {
cx.emit(Event::RemoteProjectUnshared {
project_id: project.id,
});
model.emit(
Event::RemoteProjectUnshared {
project_id: project.id,
},
cx,
);
}
false
}
@@ -942,26 +972,26 @@ impl Room {
this.pending_room_update.take();
if this.should_leave() {
log::info!("room is empty, leaving");
this.leave(cx).detach();
this.leave(model, cx).detach();
}
this.user_store.update(cx, |user_store, cx| {
this.user_store.update(cx, |user_store, model, cx| {
let participant_indices_by_user_id = this
.remote_participants
.iter()
.map(|(user_id, participant)| (*user_id, participant.participant_index))
.collect();
user_store.set_participant_indices(participant_indices_by_user_id, cx);
user_store.set_participant_indices(participant_indices_by_user_id, model, cx);
});
this.check_invariants();
this.room_update_completed_tx.try_send(Some(())).ok();
cx.notify();
model.notify(cx);
})
.ok();
}));
cx.notify();
model.notify(cx);
Ok(())
}
@@ -979,7 +1009,8 @@ impl Room {
fn live_kit_room_updated(
&mut self,
update: RoomUpdate,
cx: &mut ModelContext<Self>,
model: &Model<Self>,
cx: &mut AppContext,
) -> Result<()> {
match update {
RoomUpdate::SubscribedToRemoteVideoTrack(track) => {
@@ -990,9 +1021,12 @@ impl Room {
.get_mut(&user_id)
.ok_or_else(|| anyhow!("subscribed to track by unknown participant"))?;
participant.video_tracks.insert(track_id.clone(), track);
cx.emit(Event::RemoteVideoTracksChanged {
participant_id: participant.peer_id,
});
model.emit(
Event::RemoteVideoTracksChanged {
participant_id: participant.peer_id,
},
cx,
);
}
RoomUpdate::UnsubscribedFromRemoteVideoTrack {
@@ -1005,9 +1039,12 @@ impl Room {
.get_mut(&user_id)
.ok_or_else(|| anyhow!("unsubscribed from track by unknown participant"))?;
participant.video_tracks.remove(&track_id);
cx.emit(Event::RemoteVideoTracksChanged {
participant_id: participant.peer_id,
});
model.emit(
Event::RemoteVideoTracksChanged {
participant_id: participant.peer_id,
},
cx,
);
}
RoomUpdate::ActiveSpeakersChanged { speakers } => {
@@ -1061,9 +1098,12 @@ impl Room {
participant.audio_tracks.insert(track_id.clone(), track);
participant.muted = publication.is_muted();
cx.emit(Event::RemoteAudioTracksChanged {
participant_id: participant.peer_id,
});
model.emit(
Event::RemoteAudioTracksChanged {
participant_id: participant.peer_id,
},
cx,
);
}
RoomUpdate::UnsubscribedFromRemoteAudioTrack {
@@ -1076,9 +1116,12 @@ impl Room {
.get_mut(&user_id)
.ok_or_else(|| anyhow!("unsubscribed from track by unknown participant"))?;
participant.audio_tracks.remove(&track_id);
cx.emit(Event::RemoteAudioTracksChanged {
participant_id: participant.peer_id,
});
model.emit(
Event::RemoteAudioTracksChanged {
participant_id: participant.peer_id,
},
cx,
);
}
RoomUpdate::LocalAudioTrackUnpublished { publication } => {
@@ -1104,7 +1147,7 @@ impl Room {
}
}
cx.notify();
model.notify(cx);
Ok(())
}
@@ -1132,17 +1175,18 @@ impl Room {
&mut self,
called_user_id: u64,
initial_project_id: Option<u64>,
cx: &mut ModelContext<Self>,
model: &Model<Self>,
cx: &mut AppContext,
) -> Task<Result<()>> {
if self.status.is_offline() {
return Task::ready(Err(anyhow!("room is offline")));
}
cx.notify();
model.notify(cx);
let client = self.client.clone();
let room_id = self.id;
self.pending_call_count += 1;
cx.spawn(move |this, mut cx| async move {
model.spawn(cx, move |this, mut cx| async move {
let result = client
.request(proto::Call {
room_id,
@@ -1150,10 +1194,10 @@ impl Room {
initial_project_id,
})
.await;
this.update(&mut cx, |this, cx| {
this.update(&mut cx, |this, model, cx| {
this.pending_call_count -= 1;
if this.should_leave() {
this.leave(cx).detach_and_log_err(cx);
this.leave(model, cx).detach_and_log_err(cx);
}
})?;
result?;
@@ -1166,16 +1210,17 @@ impl Room {
id: u64,
language_registry: Arc<LanguageRegistry>,
fs: Arc<dyn Fs>,
cx: &mut ModelContext<Self>,
model: &Model<Self>,
cx: &mut AppContext,
) -> Task<Result<Model<Project>>> {
let client = self.client.clone();
let user_store = self.user_store.clone();
cx.emit(Event::RemoteProjectJoined { project_id: id });
cx.spawn(move |this, mut cx| async move {
model.emit(Event::RemoteProjectJoined { project_id: id }, cx);
model.spawn(cx, move |this, mut cx| async move {
let project =
Project::in_room(id, client, user_store, language_registry, fs, cx.clone()).await?;
this.update(&mut cx, |this, cx| {
this.update(&mut cx, |this, model, cx| {
this.joined_projects.retain(|project| {
if let Some(project) = project.upgrade() {
!project.read(cx).is_disconnected(cx)
@@ -1192,7 +1237,8 @@ impl Room {
pub fn share_project(
&mut self,
project: Model<Project>,
cx: &mut ModelContext<Self>,
model: &Model<Self>,
cx: &mut AppContext,
) -> Task<Result<u64>> {
if let Some(project_id) = project.read(cx).remote_id() {
return Task::ready(Ok(project_id));
@@ -1204,19 +1250,19 @@ impl Room {
is_ssh_project: project.read(cx).is_via_ssh(),
});
cx.spawn(|this, mut cx| async move {
model.spawn(cx, |this, mut cx| async move {
let response = request.await?;
project.update(&mut cx, |project, cx| {
project.shared(response.project_id, cx)
project.update(&mut cx, |project, model, cx| {
project.shared(response.project_id, model, cx)
})??;
// If the user's location is in this project, it changes from UnsharedProject to SharedProject.
this.update(&mut cx, |this, cx| {
this.update(&mut cx, |this, model, cx| {
this.shared_projects.insert(project.downgrade());
let active_project = this.local_participant.active_project.as_ref();
if active_project.map_or(false, |location| *location == project) {
this.set_location(Some(&project), cx)
this.set_location(Some(&project), model, cx)
} else {
Task::ready(Ok(()))
}
@@ -1230,7 +1276,8 @@ impl Room {
pub(crate) fn unshare_project(
&mut self,
project: Model<Project>,
cx: &mut ModelContext<Self>,
model: &Model<Self>,
cx: &mut AppContext,
) -> Result<()> {
let project_id = match project.read(cx).remote_id() {
Some(project_id) => project_id,
@@ -1238,10 +1285,11 @@ impl Room {
};
self.client.send(proto::UnshareProject { project_id })?;
project.update(cx, |this, cx| this.unshare(cx))?;
project.update(cx, |this, model, cx| this.unshare(model, cx))?;
if self.local_participant.active_project == Some(project.downgrade()) {
self.set_location(Some(&project), cx).detach_and_log_err(cx);
self.set_location(Some(&project), model, cx)
.detach_and_log_err(cx);
}
Ok(())
}
@@ -1249,7 +1297,8 @@ impl Room {
pub(crate) fn set_location(
&mut self,
project: Option<&Model<Project>>,
cx: &mut ModelContext<Self>,
model: &Model<Self>,
cx: &mut AppContext,
) -> Task<Result<()>> {
if self.status.is_offline() {
return Task::ready(Err(anyhow!("room is offline")));
@@ -1273,7 +1322,7 @@ impl Room {
proto::participant_location::Variant::External(proto::participant_location::External {})
};
cx.notify();
model.notify(cx);
cx.background_executor().spawn(async move {
client
.request(proto::UpdateParticipantLocation {
@@ -1317,7 +1366,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,
@@ -1334,7 +1383,11 @@ impl Room {
}
#[track_caller]
pub fn share_microphone(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
pub fn share_microphone(
&mut self,
model: &Model<Self>,
cx: &mut AppContext,
) -> Task<Result<()>> {
if self.status.is_offline() {
return Task::ready(Err(anyhow!("room is offline")));
}
@@ -1342,18 +1395,18 @@ impl Room {
let publish_id = if let Some(live_kit) = self.live_kit.as_mut() {
let publish_id = post_inc(&mut live_kit.next_publish_id);
live_kit.microphone_track = LocalTrack::Pending { publish_id };
cx.notify();
model.notify(cx);
publish_id
} else {
return Task::ready(Err(anyhow!("live-kit was not initialized")));
};
cx.spawn(move |this, mut cx| async move {
model.spawn(cx, move |this, mut cx| async move {
let publish_track = async {
let track = LocalAudioTrack::create();
this.upgrade()
.ok_or_else(|| anyhow!("room was dropped"))?
.update(&mut cx, |this, _| {
.update(&mut cx, |this, model, _| {
this.live_kit
.as_ref()
.map(|live_kit| live_kit.room.publish_audio_track(track))
@@ -1364,7 +1417,7 @@ impl Room {
let publication = publish_track.await;
this.upgrade()
.ok_or_else(|| anyhow!("room was dropped"))?
.update(&mut cx, |this, cx| {
.update(&mut cx, |this, model, cx| {
let live_kit = this
.live_kit
.as_mut()
@@ -1392,7 +1445,7 @@ impl Room {
live_kit.microphone_track = LocalTrack::Published {
track_publication: publication,
};
cx.notify();
model.notify(cx);
}
Ok(())
}
@@ -1401,7 +1454,7 @@ impl Room {
Ok(())
} else {
live_kit.microphone_track = LocalTrack::None;
cx.notify();
model.notify(cx);
Err(error)
}
}
@@ -1410,7 +1463,7 @@ impl Room {
})
}
pub fn share_screen(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
pub fn share_screen(&mut self, model: &Model<Self>, cx: &mut AppContext) -> Task<Result<()>> {
if self.status.is_offline() {
return Task::ready(Err(anyhow!("room is offline")));
} else if self.is_screen_sharing() {
@@ -1420,13 +1473,13 @@ impl Room {
let (displays, publish_id) = if let Some(live_kit) = self.live_kit.as_mut() {
let publish_id = post_inc(&mut live_kit.next_publish_id);
live_kit.screen_track = LocalTrack::Pending { publish_id };
cx.notify();
model.notify(cx);
(live_kit.room.display_sources(), publish_id)
} else {
return Task::ready(Err(anyhow!("live-kit was not initialized")));
};
cx.spawn(move |this, mut cx| async move {
model.spawn(cx, move |this, mut cx| async move {
let publish_track = async {
let displays = displays.await?;
let display = displays
@@ -1435,7 +1488,7 @@ impl Room {
let track = LocalVideoTrack::screen_share_for_display(display);
this.upgrade()
.ok_or_else(|| anyhow!("room was dropped"))?
.update(&mut cx, |this, _| {
.update(&mut cx, |this, model, _| {
this.live_kit
.as_ref()
.map(|live_kit| live_kit.room.publish_video_track(track))
@@ -1447,7 +1500,7 @@ impl Room {
let publication = publish_track.await;
this.upgrade()
.ok_or_else(|| anyhow!("room was dropped"))?
.update(&mut cx, |this, cx| {
.update(&mut cx, |this, model, cx| {
let live_kit = this
.live_kit
.as_mut()
@@ -1470,7 +1523,7 @@ impl Room {
live_kit.screen_track = LocalTrack::Published {
track_publication: publication,
};
cx.notify();
model.notify(cx);
}
Audio::play_sound(Sound::StartScreenshare, cx);
@@ -1482,7 +1535,7 @@ impl Room {
Ok(())
} else {
live_kit.screen_track = LocalTrack::None;
cx.notify();
model.notify(cx);
Err(error)
}
}
@@ -1491,7 +1544,7 @@ impl Room {
})
}
pub fn toggle_mute(&mut self, cx: &mut ModelContext<Self>) {
pub fn toggle_mute(&mut self, model: &Model<Self>, cx: &mut AppContext) {
if let Some(live_kit) = self.live_kit.as_mut() {
// When unmuting, undeafen if the user was deafened before.
let was_deafened = live_kit.deafened;
@@ -1507,19 +1560,19 @@ impl Room {
let muted = live_kit.muted_by_user;
let should_undeafen = was_deafened && !live_kit.deafened;
if let Some(task) = self.set_mute(muted, cx) {
if let Some(task) = self.set_mute(muted, model, cx) {
task.detach_and_log_err(cx);
}
if should_undeafen {
if let Some(task) = self.set_deafened(false, cx) {
if let Some(task) = self.set_deafened(false, model, cx) {
task.detach_and_log_err(cx);
}
}
}
}
pub fn toggle_deafen(&mut self, cx: &mut ModelContext<Self>) {
pub fn toggle_deafen(&mut self, model: &Model<Self>, cx: &mut AppContext) {
if let Some(live_kit) = self.live_kit.as_mut() {
// When deafening, mute the microphone if it was not already muted.
// When un-deafening, unmute the microphone, unless it was explicitly muted.
@@ -1527,19 +1580,19 @@ impl Room {
live_kit.deafened = deafened;
let should_change_mute = !live_kit.muted_by_user;
if let Some(task) = self.set_deafened(deafened, cx) {
if let Some(task) = self.set_deafened(deafened, model, cx) {
task.detach_and_log_err(cx);
}
if should_change_mute {
if let Some(task) = self.set_mute(deafened, cx) {
if let Some(task) = self.set_mute(deafened, model, cx) {
task.detach_and_log_err(cx);
}
}
}
}
pub fn unshare_screen(&mut self, cx: &mut ModelContext<Self>) -> Result<()> {
pub fn unshare_screen(&mut self, model: &Model<Self>, cx: &mut AppContext) -> Result<()> {
if self.status.is_offline() {
return Err(anyhow!("room is offline"));
}
@@ -1551,14 +1604,14 @@ impl Room {
match mem::take(&mut live_kit.screen_track) {
LocalTrack::None => Err(anyhow!("screen was not shared")),
LocalTrack::Pending { .. } => {
cx.notify();
model.notify(cx);
Ok(())
}
LocalTrack::Published {
track_publication, ..
} => {
live_kit.room.unpublish_track(track_publication);
cx.notify();
model.notify(cx);
Audio::play_sound(Sound::StopScreenshare, cx);
Ok(())
@@ -1569,10 +1622,11 @@ impl Room {
fn set_deafened(
&mut self,
deafened: bool,
cx: &mut ModelContext<Self>,
model: &Model<Self>,
cx: &mut AppContext,
) -> Option<Task<Result<()>>> {
let live_kit = self.live_kit.as_mut()?;
cx.notify();
model.notify(cx);
let mut track_updates = Vec::new();
for participant in self.remote_participants.values() {
@@ -1603,10 +1657,11 @@ impl Room {
fn set_mute(
&mut self,
should_mute: bool,
cx: &mut ModelContext<Room>,
model: &Model<Room>,
cx: &mut AppContext,
) -> Option<Task<Result<()>>> {
let live_kit = self.live_kit.as_mut()?;
cx.notify();
model.notify(cx);
if should_mute {
Audio::play_sound(Sound::Mute, cx);
@@ -1619,7 +1674,7 @@ impl Room {
if should_mute {
None
} else {
Some(self.share_microphone(cx))
Some(self.share_microphone(model, cx))
}
}
LocalTrack::Pending { .. } => None,
@@ -1631,7 +1686,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 +1696,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.
@@ -1654,13 +1709,13 @@ struct LiveKitRoom {
}
impl LiveKitRoom {
fn stop_publishing(&mut self, cx: &mut ModelContext<Room>) {
fn stop_publishing(&mut self, model: &Model<Room>, cx: &mut AppContext) {
if let LocalTrack::Published {
track_publication, ..
} = mem::replace(&mut self.microphone_track, LocalTrack::None)
{
self.room.unpublish_track(track_publication);
cx.notify();
model.notify(cx);
}
if let LocalTrack::Published {
@@ -1668,7 +1723,7 @@ impl LiveKitRoom {
} = mem::replace(&mut self.screen_track, LocalTrack::None)
{
self.room.unpublish_track(track_publication);
cx.notify();
model.notify(cx);
}
}
}

View File

@@ -2,7 +2,7 @@ use crate::{Channel, ChannelStore};
use anyhow::Result;
use client::{ChannelId, Client, Collaborator, UserStore, ZED_ALWAYS_ACTIVE};
use collections::HashMap;
use gpui::{AppContext, AsyncAppContext, Context, EventEmitter, Model, ModelContext, Task};
use gpui::{AppContext, AsyncAppContext, Context, EventEmitter, Model, Task};
use language::proto::serialize_version;
use rpc::{
proto::{self, PeerId},
@@ -62,17 +62,21 @@ impl ChannelBuffer {
.map(language::proto::deserialize_operation)
.collect::<Result<Vec<_>, _>>()?;
let buffer = cx.new_model(|cx| {
let buffer = cx.new_model(|model, cx| {
let capability = channel_store.read(cx).channel_capability(channel.id);
language::Buffer::remote(buffer_id, response.replica_id as u16, capability, base_text)
})?;
buffer.update(&mut cx, |buffer, cx| buffer.apply_ops(operations, cx))?;
buffer.update(&mut cx, |buffer, model, cx| {
buffer.apply_ops(operations, model, cx)
})?;
let subscription = client.subscribe_to_entity(channel.id.0)?;
anyhow::Ok(cx.new_model(|cx| {
cx.subscribe(&buffer, Self::on_buffer_update).detach();
cx.on_release(Self::release).detach();
anyhow::Ok(cx.new_model(|model, cx| {
model
.subscribe(&buffer, cx, Self::on_buffer_update)
.detach();
model.on_release(cx, Self::release).detach();
let mut this = Self {
buffer,
buffer_epoch: response.epoch,
@@ -81,11 +85,11 @@ impl ChannelBuffer {
collaborators: Default::default(),
acknowledge_task: None,
channel_id: channel.id,
subscription: Some(subscription.set_model(&cx.handle(), &mut cx.to_async())),
subscription: Some(subscription.set_model(model, &mut cx.to_async())),
user_store,
channel_store,
};
this.replace_collaborators(response.collaborators, cx);
this.replace_collaborators(response.collaborators, model, cx);
this
})?)
}
@@ -114,7 +118,8 @@ impl ChannelBuffer {
pub(crate) fn replace_collaborators(
&mut self,
collaborators: Vec<proto::Collaborator>,
cx: &mut ModelContext<Self>,
model: &Model<Self>,
cx: &mut AppContext,
) {
let mut new_collaborators = HashMap::default();
for collaborator in collaborators {
@@ -125,14 +130,14 @@ impl ChannelBuffer {
for (_, old_collaborator) in &self.collaborators {
if !new_collaborators.contains_key(&old_collaborator.peer_id) {
self.buffer.update(cx, |buffer, cx| {
buffer.remove_peer(old_collaborator.replica_id, cx)
self.buffer.update(cx, |buffer, model, cx| {
buffer.remove_peer(old_collaborator.replica_id, model, cx)
});
}
}
self.collaborators = new_collaborators;
cx.emit(ChannelBufferEvent::CollaboratorsChanged);
cx.notify();
model.emit(ChannelBufferEvent::CollaboratorsChanged, cx);
model.notify(cx);
}
async fn handle_update_channel_buffer(
@@ -147,10 +152,10 @@ impl ChannelBuffer {
.map(language::proto::deserialize_operation)
.collect::<Result<Vec<_>, _>>()?;
this.update(&mut cx, |this, cx| {
cx.notify();
this.update(&mut cx, |this, model, cx| {
model.notify(cx);
this.buffer
.update(cx, |buffer, cx| buffer.apply_ops(ops, cx))
.update(cx, |buffer, model, cx| buffer.apply_ops(ops, model, cx))
})?;
Ok(())
@@ -161,10 +166,10 @@ impl ChannelBuffer {
message: TypedEnvelope<proto::UpdateChannelBufferCollaborators>,
mut cx: AsyncAppContext,
) -> Result<()> {
this.update(&mut cx, |this, cx| {
this.replace_collaborators(message.payload.collaborators, cx);
cx.emit(ChannelBufferEvent::CollaboratorsChanged);
cx.notify();
this.update(&mut cx, |this, model, cx| {
this.replace_collaborators(message.payload.collaborators, model, cx);
model.emit(ChannelBufferEvent::CollaboratorsChanged, cx);
model.notify(cx);
})
}
@@ -172,7 +177,8 @@ impl ChannelBuffer {
&mut self,
_: Model<language::Buffer>,
event: &language::BufferEvent,
cx: &mut ModelContext<Self>,
model: &Model<Self>,
cx: &mut AppContext,
) {
match event {
language::BufferEvent::Operation {
@@ -195,20 +201,20 @@ impl ChannelBuffer {
.log_err();
}
language::BufferEvent::Edited => {
cx.emit(ChannelBufferEvent::BufferEdited);
model.emit(ChannelBufferEvent::BufferEdited, cx);
}
_ => {}
}
}
pub fn acknowledge_buffer_version(&mut self, cx: &mut ModelContext<'_, ChannelBuffer>) {
pub fn acknowledge_buffer_version(&mut self, model: &Model<Self>, cx: &mut AppContext) {
let buffer = self.buffer.read(cx);
let version = buffer.version();
let buffer_id = buffer.remote_id().into();
let client = self.client.clone();
let epoch = self.epoch();
self.acknowledge_task = Some(cx.spawn(move |_, cx| async move {
self.acknowledge_task = Some(cx.spawn(move |cx| async move {
cx.background_executor()
.timer(ACKNOWLEDGE_DEBOUNCE_INTERVAL)
.await;
@@ -242,19 +248,19 @@ impl ChannelBuffer {
.cloned()
}
pub(crate) fn disconnect(&mut self, cx: &mut ModelContext<Self>) {
pub(crate) fn disconnect(&mut self, model: &Model<Self>, cx: &mut AppContext) {
log::info!("channel buffer {} disconnected", self.channel_id);
if self.connected {
self.connected = false;
self.subscription.take();
cx.emit(ChannelBufferEvent::Disconnected);
cx.notify()
model.emit(ChannelBufferEvent::Disconnected, cx);
model.notify(cx)
}
}
pub(crate) fn channel_changed(&mut self, cx: &mut ModelContext<Self>) {
cx.emit(ChannelBufferEvent::ChannelChanged);
cx.notify()
pub(crate) fn channel_changed(&mut self, model: &Model<Self>, cx: &mut AppContext) {
model.emit(ChannelBufferEvent::ChannelChanged, cx);
model.notify(cx)
}
pub fn is_connected(&self) -> bool {

View File

@@ -7,9 +7,7 @@ use client::{
};
use collections::HashSet;
use futures::lock::Mutex;
use gpui::{
AppContext, AsyncAppContext, Context, EventEmitter, Model, ModelContext, Task, WeakModel,
};
use gpui::{AppContext, AsyncAppContext, Context, EventEmitter, Model, Task, WeakModel};
use rand::prelude::*;
use rpc::AnyProtoClient;
use std::{
@@ -119,8 +117,8 @@ impl ChannelChat {
})
.await?;
let handle = cx.new_model(|cx| {
cx.on_release(Self::release).detach();
let handle = cx.new_model(|model, cx| {
model.on_release(cx, Self::release).detach();
Self {
channel_id: channel.id,
user_store: user_store.clone(),
@@ -134,7 +132,7 @@ impl ChannelChat {
last_acknowledged_id: None,
rng: StdRng::from_entropy(),
first_loaded_message_id: None,
_subscription: subscription.set_model(&cx.handle(), &mut cx.to_async()),
_subscription: subscription.set_model(model, &mut cx.to_async()),
}
})?;
Self::handle_loaded_messages(
@@ -171,7 +169,8 @@ impl ChannelChat {
pub fn send_message(
&mut self,
message: MessageParams,
cx: &mut ModelContext<Self>,
model: &Model<Self>,
cx: &mut AppContext,
) -> Result<Task<Result<u64>>> {
if message.text.trim().is_empty() {
Err(anyhow!("message body can't be empty"))?;
@@ -200,6 +199,7 @@ impl ChannelChat {
},
&(),
),
model,
cx,
);
let user_store = self.user_store.clone();
@@ -207,7 +207,7 @@ impl ChannelChat {
let outgoing_messages_lock = self.outgoing_messages_lock.clone();
// todo - handle messages that fail to send (e.g. >1024 chars)
Ok(cx.spawn(move |this, mut cx| async move {
Ok(model.spawn(cx, move |this, mut cx| async move {
let outgoing_message_guard = outgoing_messages_lock.lock().await;
let request = rpc.request(proto::SendChannelMessage {
channel_id: channel_id.0,
@@ -221,8 +221,8 @@ impl ChannelChat {
let response = response.message.ok_or_else(|| anyhow!("invalid message"))?;
let id = response.id;
let message = ChannelMessage::from_proto(response, &user_store, &mut cx).await?;
this.update(&mut cx, |this, cx| {
this.insert_messages(SumTree::from_item(message, &()), cx);
this.update(&mut cx, |this, model, cx| {
this.insert_messages(SumTree::from_item(message, &()), model, cx);
if this.first_loaded_message_id.is_none() {
this.first_loaded_message_id = Some(id);
}
@@ -231,15 +231,20 @@ impl ChannelChat {
}))
}
pub fn remove_message(&mut self, id: u64, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
pub fn remove_message(
&mut self,
id: u64,
model: &Model<Self>,
cx: &mut AppContext,
) -> Task<Result<()>> {
let response = self.rpc.request(proto::RemoveChannelMessage {
channel_id: self.channel_id.0,
message_id: id,
});
cx.spawn(move |this, mut cx| async move {
model.spawn(cx, move |this, mut cx| async move {
response.await?;
this.update(&mut cx, |this, cx| {
this.message_removed(id, cx);
this.update(&mut cx, |this, model, cx| {
this.message_removed(id, model, cx);
})?;
Ok(())
})
@@ -249,13 +254,15 @@ impl ChannelChat {
&mut self,
id: u64,
message: MessageParams,
cx: &mut ModelContext<Self>,
model: &Model<Self>,
cx: &mut AppContext,
) -> Result<Task<Result<()>>> {
self.message_update(
ChannelMessageId::Saved(id),
message.text.clone(),
message.mentions.clone(),
Some(OffsetDateTime::now_utc()),
model,
cx,
);
@@ -268,13 +275,17 @@ impl ChannelChat {
nonce: Some(nonce.into()),
mentions: mentions_to_proto(&message.mentions),
});
Ok(cx.spawn(move |_, _| async move {
Ok(cx.spawn(move |_| async move {
request.await?;
Ok(())
}))
}
pub fn load_more_messages(&mut self, cx: &mut ModelContext<Self>) -> Option<Task<Option<()>>> {
pub fn load_more_messages(
&mut self,
model: &Model<Self>,
cx: &mut AppContext,
) -> Option<Task<Option<()>>> {
if self.loaded_all_messages {
return None;
}
@@ -283,7 +294,7 @@ impl ChannelChat {
let user_store = self.user_store.clone();
let channel_id = self.channel_id;
let before_message_id = self.first_loaded_message_id()?;
Some(cx.spawn(move |this, mut cx| {
Some(model.spawn(cx, move |this, mut cx| {
async move {
let response = rpc
.request(proto::GetChannelMessages {
@@ -329,7 +340,7 @@ impl ChannelChat {
) -> Option<usize> {
loop {
let step = chat
.update(&mut cx, |chat, cx| {
.update(&mut cx, |chat, model, cx| {
if let Some(first_id) = chat.first_loaded_message_id() {
if first_id <= message_id {
let mut cursor = chat.messages.cursor::<(ChannelMessageId, Count)>(&());
@@ -347,7 +358,7 @@ impl ChannelChat {
);
}
}
ControlFlow::Continue(chat.load_more_messages(cx))
ControlFlow::Continue(chat.load_more_messages(model, cx))
})
.log_err()?;
match step {
@@ -357,7 +368,7 @@ impl ChannelChat {
}
}
pub fn acknowledge_last_message(&mut self, cx: &mut ModelContext<Self>) {
pub fn acknowledge_last_message(&mut self, model: &Model<Self>, cx: &mut AppContext) {
if let ChannelMessageId::Saved(latest_message_id) = self.messages.summary().max_id {
if self
.last_acknowledged_id
@@ -370,8 +381,8 @@ impl ChannelChat {
})
.ok();
self.last_acknowledged_id = Some(latest_message_id);
self.channel_store.update(cx, |store, cx| {
store.acknowledge_message_id(self.channel_id, latest_message_id, cx);
self.channel_store.update(cx, |store, model, cx| {
store.acknowledge_message_id(self.channel_id, latest_message_id, model, cx);
});
}
}
@@ -388,7 +399,7 @@ impl ChannelChat {
let loaded_messages = messages_from_proto(proto_messages, &user_store, cx).await?;
let first_loaded_message_id = loaded_messages.first().map(|m| m.id);
let loaded_message_ids = this.update(cx, |this, _| {
let loaded_message_ids = this.update(cx, |this, model, _| {
let mut loaded_message_ids: HashSet<u64> = HashSet::default();
for message in loaded_messages.iter() {
if let Some(saved_message_id) = message.id.into() {
@@ -425,68 +436,69 @@ impl ChannelChat {
.await?;
Some(messages_from_proto(response.messages, &user_store, cx).await?)
};
this.update(cx, |this, cx| {
this.update(cx, |this, model, cx| {
this.first_loaded_message_id = first_loaded_message_id.and_then(|msg_id| msg_id.into());
this.loaded_all_messages = loaded_all_messages;
this.insert_messages(loaded_messages, cx);
this.insert_messages(loaded_messages, model, cx);
if let Some(loaded_ancestors) = loaded_ancestors {
this.insert_messages(loaded_ancestors, cx);
this.insert_messages(loaded_ancestors, model, cx);
}
})?;
Ok(())
}
pub fn rejoin(&mut self, cx: &mut ModelContext<Self>) {
pub fn rejoin(&mut self, model: &Model<Self>, cx: &mut AppContext) {
let user_store = self.user_store.clone();
let rpc = self.rpc.clone();
let channel_id = self.channel_id;
cx.spawn(move |this, mut cx| {
async move {
let response = rpc
.request(proto::JoinChannelChat {
channel_id: channel_id.0,
})
.await?;
Self::handle_loaded_messages(
this.clone(),
user_store.clone(),
rpc.clone(),
response.messages,
response.done,
&mut cx,
)
.await?;
let pending_messages = this.update(&mut cx, |this, _| {
this.pending_messages().cloned().collect::<Vec<_>>()
})?;
for pending_message in pending_messages {
let request = rpc.request(proto::SendChannelMessage {
channel_id: channel_id.0,
body: pending_message.body,
mentions: mentions_to_proto(&pending_message.mentions),
nonce: Some(pending_message.nonce.into()),
reply_to_message_id: pending_message.reply_to_message_id,
});
let response = request.await?;
let message = ChannelMessage::from_proto(
response.message.ok_or_else(|| anyhow!("invalid message"))?,
&user_store,
model
.spawn(cx, move |this, mut cx| {
async move {
let response = rpc
.request(proto::JoinChannelChat {
channel_id: channel_id.0,
})
.await?;
Self::handle_loaded_messages(
this.clone(),
user_store.clone(),
rpc.clone(),
response.messages,
response.done,
&mut cx,
)
.await?;
this.update(&mut cx, |this, cx| {
this.insert_messages(SumTree::from_item(message, &()), cx);
})?;
}
anyhow::Ok(())
}
.log_err()
})
.detach();
let pending_messages = this.update(&mut cx, |this, _, _| {
this.pending_messages().cloned().collect::<Vec<_>>()
})?;
for pending_message in pending_messages {
let request = rpc.request(proto::SendChannelMessage {
channel_id: channel_id.0,
body: pending_message.body,
mentions: mentions_to_proto(&pending_message.mentions),
nonce: Some(pending_message.nonce.into()),
reply_to_message_id: pending_message.reply_to_message_id,
});
let response = request.await?;
let message = ChannelMessage::from_proto(
response.message.ok_or_else(|| anyhow!("invalid message"))?,
&user_store,
&mut cx,
)
.await?;
this.update(&mut cx, |this, model, cx| {
this.insert_messages(SumTree::from_item(message, &()), model, cx);
})?;
}
anyhow::Ok(())
}
.log_err()
})
.detach();
}
pub fn message_count(&self) -> usize {
@@ -531,7 +543,7 @@ impl ChannelChat {
message: TypedEnvelope<proto::ChannelMessageSent>,
mut cx: AsyncAppContext,
) -> Result<()> {
let user_store = this.update(&mut cx, |this, _| this.user_store.clone())?;
let user_store = this.update(&mut cx, |this, _, _| this.user_store.clone())?;
let message = message
.payload
.message
@@ -539,12 +551,15 @@ impl ChannelChat {
let message_id = message.id;
let message = ChannelMessage::from_proto(message, &user_store, &mut cx).await?;
this.update(&mut cx, |this, cx| {
this.insert_messages(SumTree::from_item(message, &()), cx);
cx.emit(ChannelChatEvent::NewMessage {
channel_id: this.channel_id,
message_id,
})
this.update(&mut cx, |this, model, cx| {
this.insert_messages(SumTree::from_item(message, &()), model, cx);
model.emit(
ChannelChatEvent::NewMessage {
channel_id: this.channel_id,
message_id,
},
cx,
)
})?;
Ok(())
@@ -555,8 +570,8 @@ impl ChannelChat {
message: TypedEnvelope<proto::RemoveChannelMessage>,
mut cx: AsyncAppContext,
) -> Result<()> {
this.update(&mut cx, |this, cx| {
this.message_removed(message.payload.message_id, cx)
this.update(&mut cx, |this, model, cx| {
this.message_removed(message.payload.message_id, model, cx)
})?;
Ok(())
}
@@ -566,7 +581,7 @@ impl ChannelChat {
message: TypedEnvelope<proto::ChannelMessageUpdate>,
mut cx: AsyncAppContext,
) -> Result<()> {
let user_store = this.update(&mut cx, |this, _| this.user_store.clone())?;
let user_store = this.update(&mut cx, |this, _, _| this.user_store.clone())?;
let message = message
.payload
.message
@@ -574,19 +589,25 @@ impl ChannelChat {
let message = ChannelMessage::from_proto(message, &user_store, &mut cx).await?;
this.update(&mut cx, |this, cx| {
this.update(&mut cx, |this, model, cx| {
this.message_update(
message.id,
message.body,
message.mentions,
message.edited_at,
model,
cx,
)
})?;
Ok(())
}
fn insert_messages(&mut self, messages: SumTree<ChannelMessage>, cx: &mut ModelContext<Self>) {
fn insert_messages(
&mut self,
messages: SumTree<ChannelMessage>,
model: &Model<Self>,
cx: &mut AppContext,
) {
if let Some((first_message, last_message)) = messages.first().zip(messages.last()) {
let nonces = messages
.cursor::<()>(&())
@@ -631,21 +652,27 @@ impl ChannelChat {
self.messages = new_messages;
for range in ranges.into_iter().rev() {
cx.emit(ChannelChatEvent::MessagesUpdated {
old_range: range,
new_count: 0,
});
model.emit(
ChannelChatEvent::MessagesUpdated {
old_range: range,
new_count: 0,
},
cx,
);
}
cx.emit(ChannelChatEvent::MessagesUpdated {
old_range: start_ix..end_ix,
new_count,
});
model.emit(
ChannelChatEvent::MessagesUpdated {
old_range: start_ix..end_ix,
new_count,
},
cx,
);
cx.notify();
model.notify(cx);
}
}
fn message_removed(&mut self, id: u64, cx: &mut ModelContext<Self>) {
fn message_removed(&mut self, id: u64, model: &Model<Self>, cx: &mut AppContext) {
let mut cursor = self.messages.cursor::<ChannelMessageId>(&());
let mut messages = cursor.slice(&ChannelMessageId::Saved(id), Bias::Left, &());
if let Some(item) = cursor.item() {
@@ -658,7 +685,7 @@ impl ChannelChat {
// If the message that was deleted was the last acknowledged message,
// replace the acknowledged message with an earlier one.
self.channel_store.update(cx, |store, _| {
self.channel_store.update(cx, |store, model, _| {
let summary = self.messages.summary();
if summary.count == 0 {
store.set_acknowledged_message_id(self.channel_id, None);
@@ -669,10 +696,13 @@ impl ChannelChat {
}
});
cx.emit(ChannelChatEvent::MessagesUpdated {
old_range: deleted_message_ix..deleted_message_ix + 1,
new_count: 0,
});
model.emit(
ChannelChatEvent::MessagesUpdated {
old_range: deleted_message_ix..deleted_message_ix + 1,
new_count: 0,
},
cx,
);
}
}
}
@@ -683,7 +713,8 @@ impl ChannelChat {
body: String,
mentions: Vec<(Range<usize>, u64)>,
edited_at: Option<OffsetDateTime>,
cx: &mut ModelContext<Self>,
model: &Model<Self>,
cx: &mut AppContext,
) {
let mut cursor = self.messages.cursor::<ChannelMessageId>(&());
let mut messages = cursor.slice(&id, Bias::Left, &());
@@ -701,12 +732,15 @@ impl ChannelChat {
drop(cursor);
self.messages = messages;
cx.emit(ChannelChatEvent::UpdateMessage {
message_ix: ix,
message_id: id,
});
model.emit(
ChannelChatEvent::UpdateMessage {
message_ix: ix,
message_id: id,
},
cx,
);
cx.notify();
model.notify(cx);
}
}
@@ -728,8 +762,8 @@ impl ChannelMessage {
cx: &mut AsyncAppContext,
) -> Result<Self> {
let sender = user_store
.update(cx, |user_store, cx| {
user_store.get_user(message.sender_id, cx)
.update(cx, |user_store, model, cx| {
user_store.get_user(message.sender_id, model, cx)
})?
.await?;
@@ -779,8 +813,8 @@ impl ChannelMessage {
.into_iter()
.collect();
user_store
.update(cx, |user_store, cx| {
user_store.get_users(unique_user_ids, cx)
.update(cx, |user_store, model, cx| {
user_store.get_users(unique_user_ids, model, cx)
})?
.await?;

View File

@@ -7,8 +7,8 @@ use client::{ChannelId, Client, ClientSettings, Subscription, User, UserId, User
use collections::{hash_map, HashMap, HashSet};
use futures::{channel::mpsc, future::Shared, Future, FutureExt, StreamExt};
use gpui::{
AppContext, AsyncAppContext, Context, EventEmitter, Global, Model, ModelContext, SharedString,
Task, WeakModel,
AppContext, AsyncAppContext, Context, EventEmitter, Global, Model, SharedString, Task,
WeakModel,
};
use language::Capability;
use rpc::{
@@ -23,7 +23,7 @@ pub const RECONNECT_TIMEOUT: Duration = Duration::from_secs(30);
pub fn init(client: &Arc<Client>, user_store: Model<UserStore>, cx: &mut AppContext) {
let channel_store =
cx.new_model(|cx| ChannelStore::new(client.clone(), user_store.clone(), cx));
cx.new_model(|model, cx| ChannelStore::new(client.clone(), user_store.clone(), model, cx));
cx.set_global(GlobalChannelStore(channel_store));
}
@@ -160,32 +160,37 @@ impl ChannelStore {
pub fn new(
client: Arc<Client>,
user_store: Model<UserStore>,
cx: &mut ModelContext<Self>,
model: &Model<Self>,
cx: &mut AppContext,
) -> Self {
let rpc_subscriptions = [
client.add_message_handler(cx.weak_model(), Self::handle_update_channels),
client.add_message_handler(cx.weak_model(), Self::handle_update_user_channels),
client.add_message_handler(model.downgrade(), Self::handle_update_channels),
client.add_message_handler(model.downgrade(), Self::handle_update_user_channels),
];
let mut connection_status = client.status();
let (update_channels_tx, mut update_channels_rx) = mpsc::unbounded();
let watch_connection_status = cx.spawn(|this, mut cx| async move {
let watch_connection_status = model.spawn(cx, |this, mut cx| async move {
while let Some(status) = connection_status.next().await {
let this = this.upgrade()?;
match status {
client::Status::Connected { .. } => {
this.update(&mut cx, |this, cx| this.handle_connect(cx))
this.update(&mut cx, |this, model, cx| this.handle_connect(model, cx))
.ok()?
.await
.log_err()?;
}
client::Status::SignedOut | client::Status::UpgradeRequired => {
this.update(&mut cx, |this, cx| this.handle_disconnect(false, cx))
.ok();
this.update(&mut cx, |this, model, cx| {
this.handle_disconnect(false, model, cx)
})
.ok();
}
_ => {
this.update(&mut cx, |this, cx| this.handle_disconnect(true, cx))
.ok();
this.update(&mut cx, |this, model, cx| {
this.handle_disconnect(true, model, cx)
})
.ok();
}
}
}
@@ -205,12 +210,12 @@ impl ChannelStore {
_rpc_subscriptions: rpc_subscriptions,
_watch_connection_status: watch_connection_status,
disconnect_channel_buffers_task: None,
_update_channels: cx.spawn(|this, mut cx| async move {
_update_channels: model.spawn(cx, |this, mut cx| async move {
maybe!(async move {
while let Some(update_channels) = update_channels_rx.next().await {
if let Some(this) = this.upgrade() {
let update_task = this.update(&mut cx, |this, cx| {
this.update_channels(update_channels, cx)
let update_task = this.update(&mut cx, |this, model, cx| {
this.update_channels(update_channels, model, cx)
})?;
if let Some(update_task) = update_task {
update_task.await.log_err();
@@ -307,15 +312,17 @@ impl ChannelStore {
pub fn open_channel_buffer(
&mut self,
channel_id: ChannelId,
cx: &mut ModelContext<Self>,
model: &Model<Self>,
cx: &mut AppContext,
) -> Task<Result<Model<ChannelBuffer>>> {
let client = self.client.clone();
let user_store = self.user_store.clone();
let channel_store = cx.handle();
let channel_store = model.clone();
self.open_channel_resource(
channel_id,
|this| &mut this.opened_buffers,
|channel, cx| ChannelBuffer::new(channel, client, user_store, channel_store, cx),
model,
cx,
)
}
@@ -323,7 +330,8 @@ impl ChannelStore {
pub fn fetch_channel_messages(
&self,
message_ids: Vec<u64>,
cx: &mut ModelContext<Self>,
model: &Model<Self>,
cx: &mut AppContext,
) -> Task<Result<Vec<ChannelMessage>>> {
let request = if message_ids.is_empty() {
None
@@ -333,13 +341,13 @@ impl ChannelStore {
.request(proto::GetChannelMessagesById { message_ids }),
)
};
cx.spawn(|this, mut cx| async move {
model.spawn(cx, |this, mut cx| async move {
if let Some(request) = request {
let response = request.await?;
let this = this
.upgrade()
.ok_or_else(|| anyhow!("channel store dropped"))?;
let user_store = this.update(&mut cx, |this, _| this.user_store.clone())?;
let user_store = this.update(&mut cx, |this, _, _| this.user_store.clone())?;
ChannelMessage::from_proto_vec(response.messages, &user_store, &mut cx).await
} else {
Ok(Vec::new())
@@ -384,26 +392,28 @@ impl ChannelStore {
&mut self,
channel_id: ChannelId,
message_id: u64,
cx: &mut ModelContext<Self>,
model: &Model<Self>,
cx: &mut AppContext,
) {
self.channel_states
.entry(channel_id)
.or_default()
.acknowledge_message_id(message_id);
cx.notify();
model.notify(cx);
}
pub fn update_latest_message_id(
&mut self,
channel_id: ChannelId,
message_id: u64,
cx: &mut ModelContext<Self>,
model: &Model<Self>,
cx: &mut AppContext,
) {
self.channel_states
.entry(channel_id)
.or_default()
.update_latest_message_id(message_id);
cx.notify();
model.notify(cx);
}
pub fn acknowledge_notes_version(
@@ -411,13 +421,14 @@ impl ChannelStore {
channel_id: ChannelId,
epoch: u64,
version: &clock::Global,
cx: &mut ModelContext<Self>,
model: &Model<Self>,
cx: &mut AppContext,
) {
self.channel_states
.entry(channel_id)
.or_default()
.acknowledge_notes_version(epoch, version);
cx.notify()
model.notify(cx)
}
pub fn update_latest_notes_version(
@@ -425,27 +436,30 @@ impl ChannelStore {
channel_id: ChannelId,
epoch: u64,
version: &clock::Global,
cx: &mut ModelContext<Self>,
model: &Model<Self>,
cx: &mut AppContext,
) {
self.channel_states
.entry(channel_id)
.or_default()
.update_latest_notes_version(epoch, version);
cx.notify()
model.notify(cx)
}
pub fn open_channel_chat(
&mut self,
channel_id: ChannelId,
cx: &mut ModelContext<Self>,
model: &Model<Self>,
cx: &mut AppContext,
) -> Task<Result<Model<ChannelChat>>> {
let client = self.client.clone();
let user_store = self.user_store.clone();
let this = cx.handle();
let this = model.clone();
self.open_channel_resource(
channel_id,
|this| &mut this.opened_chats,
|channel, cx| ChannelChat::new(channel, this, user_store, client, cx),
model,
cx,
)
}
@@ -460,7 +474,8 @@ impl ChannelStore {
channel_id: ChannelId,
get_map: fn(&mut Self) -> &mut HashMap<ChannelId, OpenedModelHandle<T>>,
load: F,
cx: &mut ModelContext<Self>,
model: &Model<Self>,
cx: &mut AppContext,
) -> Task<Result<Model<T>>>
where
F: 'static + FnOnce(Arc<Channel>, AsyncAppContext) -> Fut,
@@ -483,9 +498,9 @@ impl ChannelStore {
}
},
hash_map::Entry::Vacant(e) => {
let task = cx
.spawn(move |this, mut cx| async move {
let channel = this.update(&mut cx, |this, _| {
let task = model
.spawn(cx, move |this, mut cx| async move {
let channel = this.update(&mut cx, |this, _, _| {
this.channel_for_id(channel_id).cloned().ok_or_else(|| {
Arc::new(anyhow!("no channel for id: {}", channel_id))
})
@@ -496,25 +511,26 @@ impl ChannelStore {
.shared();
e.insert(OpenedModelHandle::Loading(task.clone()));
cx.spawn({
let task = task.clone();
move |this, mut cx| async move {
let result = task.await;
this.update(&mut cx, |this, _| match result {
Ok(model) => {
get_map(this).insert(
channel_id,
OpenedModelHandle::Open(model.downgrade()),
);
}
Err(_) => {
get_map(this).remove(&channel_id);
}
})
.ok();
}
})
.detach();
model
.spawn(cx, {
let task = task.clone();
move |this, mut cx| async move {
let result = task.await;
this.update(&mut cx, |this, _, _| match result {
Ok(model) => {
get_map(this).insert(
channel_id,
OpenedModelHandle::Open(model.downgrade()),
);
}
Err(_) => {
get_map(this).remove(&channel_id);
}
})
.ok();
}
})
.detach();
break task;
}
}
@@ -572,11 +588,12 @@ impl ChannelStore {
&self,
name: &str,
parent_id: Option<ChannelId>,
cx: &mut ModelContext<Self>,
model: &Model<Self>,
cx: &mut AppContext,
) -> Task<Result<ChannelId>> {
let client = self.client.clone();
let name = name.trim_start_matches('#').to_owned();
cx.spawn(move |this, mut cx| async move {
model.spawn(cx, move |this, mut cx| async move {
let response = client
.request(proto::CreateChannel {
name,
@@ -589,12 +606,13 @@ impl ChannelStore {
.ok_or_else(|| anyhow!("missing channel in response"))?;
let channel_id = ChannelId(channel.id);
this.update(&mut cx, |this, cx| {
this.update(&mut cx, |this, model, cx| {
let task = this.update_channels(
proto::UpdateChannels {
channels: vec![channel],
..Default::default()
},
model,
cx,
);
assert!(task.is_none());
@@ -603,7 +621,7 @@ impl ChannelStore {
// before this frame is rendered. But we can't guarantee that the collab panel's future
// will resolve before this flush_effects finishes. Synchronously emitting this event
// ensures that the collab panel will observe this creation before the frame completes
cx.emit(ChannelEvent::ChannelCreated(channel_id));
model.emit(ChannelEvent::ChannelCreated(channel_id), cx);
})?;
Ok(channel_id)
@@ -614,10 +632,11 @@ impl ChannelStore {
&mut self,
channel_id: ChannelId,
to: ChannelId,
cx: &mut ModelContext<Self>,
model: &Model<Self>,
cx: &mut AppContext,
) -> Task<Result<()>> {
let client = self.client.clone();
cx.spawn(move |_, _| async move {
cx.spawn(move |_| async move {
let _ = client
.request(proto::MoveChannel {
channel_id: channel_id.0,
@@ -633,10 +652,11 @@ impl ChannelStore {
&mut self,
channel_id: ChannelId,
visibility: ChannelVisibility,
cx: &mut ModelContext<Self>,
model: &Model<Self>,
cx: &mut AppContext,
) -> Task<Result<()>> {
let client = self.client.clone();
cx.spawn(move |_, _| async move {
cx.spawn(move |_| async move {
let _ = client
.request(proto::SetChannelVisibility {
channel_id: channel_id.0,
@@ -653,15 +673,16 @@ impl ChannelStore {
channel_id: ChannelId,
user_id: UserId,
role: proto::ChannelRole,
cx: &mut ModelContext<Self>,
model: &Model<Self>,
cx: &mut AppContext,
) -> Task<Result<()>> {
if !self.outgoing_invites.insert((channel_id, user_id)) {
return Task::ready(Err(anyhow!("invite request already in progress")));
}
cx.notify();
model.notify(cx);
let client = self.client.clone();
cx.spawn(move |this, mut cx| async move {
model.spawn(cx, move |this, mut cx| async move {
let result = client
.request(proto::InviteChannelMember {
channel_id: channel_id.0,
@@ -670,9 +691,9 @@ impl ChannelStore {
})
.await;
this.update(&mut cx, |this, cx| {
this.update(&mut cx, |this, model, cx| {
this.outgoing_invites.remove(&(channel_id, user_id));
cx.notify();
model.notify(cx);
})?;
result?;
@@ -685,15 +706,16 @@ impl ChannelStore {
&mut self,
channel_id: ChannelId,
user_id: u64,
cx: &mut ModelContext<Self>,
model: &Model<Self>,
cx: &mut AppContext,
) -> Task<Result<()>> {
if !self.outgoing_invites.insert((channel_id, user_id)) {
return Task::ready(Err(anyhow!("invite request already in progress")));
}
cx.notify();
model.notify(cx);
let client = self.client.clone();
cx.spawn(move |this, mut cx| async move {
model.spawn(cx, move |this, mut cx| async move {
let result = client
.request(proto::RemoveChannelMember {
channel_id: channel_id.0,
@@ -701,9 +723,9 @@ impl ChannelStore {
})
.await;
this.update(&mut cx, |this, cx| {
this.update(&mut cx, |this, model, cx| {
this.outgoing_invites.remove(&(channel_id, user_id));
cx.notify();
model.notify(cx);
})?;
result?;
Ok(())
@@ -715,15 +737,16 @@ impl ChannelStore {
channel_id: ChannelId,
user_id: UserId,
role: proto::ChannelRole,
cx: &mut ModelContext<Self>,
model: &Model<Self>,
cx: &mut AppContext,
) -> Task<Result<()>> {
if !self.outgoing_invites.insert((channel_id, user_id)) {
return Task::ready(Err(anyhow!("member request already in progress")));
}
cx.notify();
model.notify(cx);
let client = self.client.clone();
cx.spawn(move |this, mut cx| async move {
model.spawn(cx, move |this, mut cx| async move {
let result = client
.request(proto::SetChannelMemberRole {
channel_id: channel_id.0,
@@ -732,9 +755,9 @@ impl ChannelStore {
})
.await;
this.update(&mut cx, |this, cx| {
this.update(&mut cx, |this, model, cx| {
this.outgoing_invites.remove(&(channel_id, user_id));
cx.notify();
model.notify(cx);
})?;
result?;
@@ -746,11 +769,12 @@ impl ChannelStore {
&mut self,
channel_id: ChannelId,
new_name: &str,
cx: &mut ModelContext<Self>,
model: &Model<Self>,
cx: &mut AppContext,
) -> Task<Result<()>> {
let client = self.client.clone();
let name = new_name.to_string();
cx.spawn(move |this, mut cx| async move {
model.spawn(cx, move |this, mut cx| async move {
let channel = client
.request(proto::RenameChannel {
channel_id: channel_id.0,
@@ -759,12 +783,13 @@ impl ChannelStore {
.await?
.channel
.ok_or_else(|| anyhow!("missing channel in response"))?;
this.update(&mut cx, |this, cx| {
this.update(&mut cx, |this, model, cx| {
let task = this.update_channels(
proto::UpdateChannels {
channels: vec![channel],
..Default::default()
},
model,
cx,
);
assert!(task.is_none());
@@ -773,7 +798,7 @@ impl ChannelStore {
// before this frame is rendered. But we can't guarantee that the collab panel's future
// will resolve before this flush_effects finishes. Synchronously emitting this event
// ensures that the collab panel will observe this creation before the frame complete
cx.emit(ChannelEvent::ChannelRenamed(channel_id))
model.emit(ChannelEvent::ChannelRenamed(channel_id), cx)
})?;
Ok(())
})
@@ -783,7 +808,8 @@ impl ChannelStore {
&mut self,
channel_id: ChannelId,
accept: bool,
cx: &mut ModelContext<Self>,
model: &Model<Self>,
cx: &mut AppContext,
) -> Task<Result<()>> {
let client = self.client.clone();
cx.background_executor().spawn(async move {
@@ -801,11 +827,12 @@ impl ChannelStore {
channel_id: ChannelId,
query: String,
limit: u16,
cx: &mut ModelContext<Self>,
model: &Model<Self>,
cx: &mut AppContext,
) -> Task<Result<Vec<ChannelMembership>>> {
let client = self.client.clone();
let user_store = self.user_store.downgrade();
cx.spawn(move |_, mut cx| async move {
cx.spawn(move |mut cx| async move {
let response = client
.request(proto::GetChannelMembers {
channel_id: channel_id.0,
@@ -813,7 +840,7 @@ impl ChannelStore {
limit: limit as u64,
})
.await?;
user_store.update(&mut cx, |user_store, _| {
user_store.update(&mut cx, |user_store, model, _| {
user_store.insert(response.users);
response
.members
@@ -855,7 +882,7 @@ impl ChannelStore {
message: TypedEnvelope<proto::UpdateChannels>,
mut cx: AsyncAppContext,
) -> Result<()> {
this.update(&mut cx, |this, _| {
this.update(&mut cx, |this, _, _| {
this.update_channels_tx
.unbounded_send(message.payload)
.unwrap();
@@ -868,13 +895,14 @@ impl ChannelStore {
message: TypedEnvelope<proto::UpdateUserChannels>,
mut cx: AsyncAppContext,
) -> Result<()> {
this.update(&mut cx, |this, cx| {
this.update(&mut cx, |this, model, cx| {
for buffer_version in message.payload.observed_channel_buffer_version {
let version = language::proto::deserialize_version(&buffer_version.version);
this.acknowledge_notes_version(
ChannelId(buffer_version.channel_id),
buffer_version.epoch,
&version,
model,
cx,
);
}
@@ -882,6 +910,7 @@ impl ChannelStore {
this.acknowledge_message_id(
ChannelId(message_id.channel_id),
message_id.message_id,
model,
cx,
);
}
@@ -896,7 +925,7 @@ impl ChannelStore {
})
}
fn handle_connect(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
fn handle_connect(&mut self, model: &Model<Self>, cx: &mut AppContext) -> Task<Result<()>> {
self.channel_index.clear();
self.channel_invitations.clear();
self.channel_participants.clear();
@@ -907,8 +936,8 @@ impl ChannelStore {
for chat in self.opened_chats.values() {
if let OpenedModelHandle::Open(chat) = chat {
if let Some(chat) = chat.upgrade() {
chat.update(cx, |chat, cx| {
chat.rejoin(cx);
chat.update(cx, |chat, model, cx| {
chat.rejoin(model, cx);
});
}
}
@@ -937,17 +966,17 @@ impl ChannelStore {
buffers: buffer_versions,
});
cx.spawn(|this, mut cx| async move {
model.spawn(cx, |this, mut cx| async move {
let mut response = response.await?;
this.update(&mut cx, |this, cx| {
this.update(&mut cx, |this, model, cx| {
this.opened_buffers.retain(|_, buffer| match buffer {
OpenedModelHandle::Open(channel_buffer) => {
let Some(channel_buffer) = channel_buffer.upgrade() else {
return false;
};
channel_buffer.update(cx, |channel_buffer, cx| {
channel_buffer.update(cx, |channel_buffer, model, cx| {
let channel_id = channel_buffer.channel_id;
if let Some(remote_buffer) = response
.buffers
@@ -960,12 +989,13 @@ impl ChannelStore {
channel_buffer.replace_collaborators(
mem::take(&mut remote_buffer.collaborators),
model,
cx,
);
let operations = channel_buffer
.buffer()
.update(cx, |buffer, cx| {
.update(cx, |buffer, model, cx| {
let outgoing_operations =
buffer.serialize_ops(Some(remote_version), cx);
let incoming_operations =
@@ -973,7 +1003,7 @@ impl ChannelStore {
.into_iter()
.map(language::proto::deserialize_operation)
.collect::<Result<Vec<_>>>()?;
buffer.apply_ops(incoming_operations, cx);
buffer.apply_ops(incoming_operations, model, cx);
anyhow::Ok(outgoing_operations)
})
.log_err();
@@ -999,7 +1029,7 @@ impl ChannelStore {
}
}
channel_buffer.disconnect(cx);
channel_buffer.disconnect(model, cx);
false
})
}
@@ -1011,21 +1041,28 @@ impl ChannelStore {
})
}
fn handle_disconnect(&mut self, wait_for_reconnect: bool, cx: &mut ModelContext<Self>) {
cx.notify();
fn handle_disconnect(
&mut self,
wait_for_reconnect: bool,
model: &Model<Self>,
cx: &mut AppContext,
) {
model.notify(cx);
self.did_subscribe = false;
self.disconnect_channel_buffers_task.get_or_insert_with(|| {
cx.spawn(move |this, mut cx| async move {
model.spawn(cx, move |this, mut cx| async move {
if wait_for_reconnect {
cx.background_executor().timer(RECONNECT_TIMEOUT).await;
}
if let Some(this) = this.upgrade() {
this.update(&mut cx, |this, cx| {
this.update(&mut cx, |this, model, cx| {
for (_, buffer) in this.opened_buffers.drain() {
if let OpenedModelHandle::Open(buffer) = buffer {
if let Some(buffer) = buffer.upgrade() {
buffer.update(cx, |buffer, cx| buffer.disconnect(cx));
buffer.update(cx, |buffer, model, cx| {
buffer.disconnect(model, cx)
});
}
}
}
@@ -1039,7 +1076,8 @@ impl ChannelStore {
pub(crate) fn update_channels(
&mut self,
payload: proto::UpdateChannels,
cx: &mut ModelContext<ChannelStore>,
model: &Model<ChannelStore>,
cx: &mut AppContext,
) -> Option<Task<Result<()>>> {
if !payload.remove_channel_invitations.is_empty() {
self.channel_invitations
@@ -1127,7 +1165,7 @@ impl ChannelStore {
}
}
cx.notify();
model.notify(cx);
if payload.channel_participants.is_empty() {
return None;
}
@@ -1142,13 +1180,13 @@ impl ChannelStore {
}
}
let users = self
.user_store
.update(cx, |user_store, cx| user_store.get_users(all_user_ids, cx));
Some(cx.spawn(|this, mut cx| async move {
let users = self.user_store.update(cx, |user_store, model, cx| {
user_store.get_users(all_user_ids, model, cx)
});
Some(model.spawn(cx, |this, mut cx| async move {
let users = users.await?;
this.update(&mut cx, |this, cx| {
this.update(&mut cx, |this, model, cx| {
for entry in &channel_participants {
let mut participants: Vec<_> = entry
.participant_user_ids
@@ -1167,7 +1205,7 @@ impl ChannelStore {
.insert(ChannelId(entry.channel_id), participants);
}
cx.notify();
model.notify(cx);
})
}))
}

View File

@@ -137,7 +137,7 @@ async fn test_channel_messages(cx: &mut TestAppContext) {
let user_id = 5;
let channel_id = 5;
let channel_store = cx.update(init_test);
let client = channel_store.update(cx, |s, _| s.client());
let client = channel_store.update(cx, |s, model, _| s.client());
let server = FakeServer::for_client(user_id, &client, cx).await;
// Get the available channels.
@@ -169,9 +169,9 @@ async fn test_channel_messages(cx: &mut TestAppContext) {
);
// Join a channel and populate its existing messages.
let channel = channel_store.update(cx, |store, cx| {
let channel = channel_store.update(cx, |store, model, cx| {
let channel_id = store.ordered_channels().next().unwrap().1.id;
store.open_channel_chat(channel_id, cx)
store.open_channel_chat(channel_id, model, cx)
});
let join_channel = server.receive::<proto::JoinChannelChat>().await.unwrap();
server.respond(
@@ -221,7 +221,7 @@ async fn test_channel_messages(cx: &mut TestAppContext) {
);
let channel = channel.await.unwrap();
channel.update(cx, |channel, _| {
channel.update(cx, |channel, model, _| {
assert_eq!(
channel
.messages_in_range(0..2)
@@ -270,7 +270,7 @@ async fn test_channel_messages(cx: &mut TestAppContext) {
new_count: 1,
}
);
channel.update(cx, |channel, _| {
channel.update(cx, |channel, model, _| {
assert_eq!(
channel
.messages_in_range(2..3)
@@ -281,8 +281,8 @@ async fn test_channel_messages(cx: &mut TestAppContext) {
});
// Scroll up to view older messages.
channel.update(cx, |channel, cx| {
channel.load_more_messages(cx).unwrap().detach();
channel.update(cx, |channel, model, cx| {
channel.load_more_messages(model, cx).unwrap().detach();
});
let get_messages = server.receive::<proto::GetChannelMessages>().await.unwrap();
assert_eq!(get_messages.payload.channel_id, 5);
@@ -323,7 +323,7 @@ async fn test_channel_messages(cx: &mut TestAppContext) {
new_count: 2,
}
);
channel.update(cx, |channel, _| {
channel.update(cx, |channel, model, _| {
assert_eq!(
channel
.messages_in_range(0..2)
@@ -346,7 +346,7 @@ fn init_test(cx: &mut AppContext) -> Model<ChannelStore> {
let clock = Arc::new(FakeSystemClock::new());
let http = FakeHttpClient::with_404_response();
let client = Client::new(clock, http.clone(), cx);
let user_store = cx.new_model(|cx| UserStore::new(client.clone(), cx));
let user_store = cx.new_model(|model, cx| UserStore::new(client.clone(), model, cx));
client::init(&client, cx);
crate::init(&client, user_store, cx);
@@ -359,7 +359,9 @@ fn update_channels(
message: proto::UpdateChannels,
cx: &mut AppContext,
) {
let task = channel_store.update(cx, |store, cx| store.update_channels(message, cx));
let task = channel_store.update(cx, |store, model, cx| {
store.update_channels(message, model, cx)
});
assert!(task.is_none());
}
@@ -369,7 +371,7 @@ fn assert_channels(
expected_channels: &[(usize, String)],
cx: &mut AppContext,
) {
let actual = channel_store.update(cx, |store, _| {
let actual = channel_store.update(cx, |store, model, _| {
store
.ordered_channels()
.map(|(depth, channel)| (depth, channel.name.to_string()))

View File

@@ -1962,7 +1962,7 @@ mod tests {
let (done_tx2, mut done_rx2) = smol::channel::unbounded();
AnyProtoClient::from(client.clone()).add_model_message_handler(
move |model: Model<TestModel>, _: TypedEnvelope<proto::JoinProject>, mut cx| {
match model.update(&mut cx, |model, _| model.id).unwrap() {
match model.update(&mut cx, |model, _, _| model.id).unwrap() {
1 => done_tx1.try_send(()).unwrap(),
2 => done_tx2.try_send(()).unwrap(),
_ => unreachable!(),
@@ -1970,15 +1970,15 @@ mod tests {
async { Ok(()) }
},
);
let model1 = cx.new_model(|_| TestModel {
let model1 = cx.new_model(|_, _| TestModel {
id: 1,
subscription: None,
});
let model2 = cx.new_model(|_| TestModel {
let model2 = cx.new_model(|_, _| TestModel {
id: 2,
subscription: None,
});
let model3 = cx.new_model(|_| TestModel {
let model3 = cx.new_model(|_, _| TestModel {
id: 3,
subscription: None,
});
@@ -2018,7 +2018,7 @@ mod tests {
});
let server = FakeServer::for_client(user_id, &client, cx).await;
let model = cx.new_model(|_| TestModel::default());
let model = cx.new_model(|_, _| TestModel::default());
let (done_tx1, _done_rx1) = smol::channel::unbounded();
let (done_tx2, mut done_rx2) = smol::channel::unbounded();
let subscription1 = client.add_message_handler(
@@ -2053,20 +2053,20 @@ mod tests {
});
let server = FakeServer::for_client(user_id, &client, cx).await;
let model = cx.new_model(|_| TestModel::default());
let model = cx.new_model(|_, _| TestModel::default());
let (done_tx, mut done_rx) = smol::channel::unbounded();
let subscription = client.add_message_handler(
model.clone().downgrade(),
move |model: Model<TestModel>, _: TypedEnvelope<proto::Ping>, mut cx| {
model
.update(&mut cx, |model, _| model.subscription.take())
.update(&mut cx, |this, model, _| this.subscription.take())
.unwrap();
done_tx.try_send(()).unwrap();
async { Ok(()) }
},
);
model.update(cx, |model, _| {
model.subscription = Some(subscription);
model.update(cx, |this, model, _| {
this.subscription = Some(subscription);
});
server.send(proto::Ping {});
done_rx.next().await.unwrap();

View File

@@ -204,7 +204,7 @@ impl FakeServer {
client: Arc<Client>,
cx: &mut TestAppContext,
) -> Model<UserStore> {
let user_store = cx.new_model(|cx| UserStore::new(client, cx));
let user_store = cx.new_model(|model, cx| UserStore::new(client, model, cx));
assert_eq!(
self.receive::<proto::GetUsers>()
.await

View File

@@ -5,8 +5,7 @@ use collections::{hash_map::Entry, HashMap, HashSet};
use feature_flags::FeatureFlagAppExt;
use futures::{channel::mpsc, Future, StreamExt};
use gpui::{
AppContext, AsyncAppContext, EventEmitter, Model, ModelContext, SharedString, SharedUri, Task,
WeakModel,
AppContext, AsyncAppContext, EventEmitter, Model, SharedString, SharedUri, Task, WeakModel,
};
use postage::{sink::Sink, watch};
use rpc::proto::{RequestMessage, UsersResponse};
@@ -136,14 +135,14 @@ enum UpdateContacts {
}
impl UserStore {
pub fn new(client: Arc<Client>, cx: &ModelContext<Self>) -> Self {
pub fn new(client: Arc<Client>, model: &Model<Self>, cx: &AppContext) -> Self {
let (mut current_user_tx, current_user_rx) = watch::channel();
let (update_contacts_tx, mut update_contacts_rx) = mpsc::unbounded();
let rpc_subscriptions = vec![
client.add_message_handler(cx.weak_model(), Self::handle_update_plan),
client.add_message_handler(cx.weak_model(), Self::handle_update_contacts),
client.add_message_handler(cx.weak_model(), Self::handle_update_invite_info),
client.add_message_handler(cx.weak_model(), Self::handle_show_contacts),
client.add_message_handler(model.downgrade(), Self::handle_update_plan),
client.add_message_handler(model.downgrade(), Self::handle_update_contacts),
client.add_message_handler(model.downgrade(), Self::handle_update_invite_info),
client.add_message_handler(model.downgrade(), Self::handle_show_contacts),
];
Self {
users: Default::default(),
@@ -158,19 +157,19 @@ impl UserStore {
invite_info: None,
client: Arc::downgrade(&client),
update_contacts_tx,
_maintain_contacts: cx.spawn(|this, mut cx| async move {
_maintain_contacts: model.spawn(cx, |this, mut cx| async move {
let _subscriptions = rpc_subscriptions;
while let Some(message) = update_contacts_rx.next().await {
if let Ok(task) =
this.update(&mut cx, |this, cx| this.update_contacts(message, cx))
{
if let Ok(task) = this.update(&mut cx, |this, model, cx| {
this.update_contacts(message, model, cx)
}) {
task.log_err().await;
} else {
break;
}
}
}),
_maintain_current_user: cx.spawn(|this, mut cx| async move {
_maintain_current_user: model.spawn(cx, |this, mut cx| async move {
let mut status = client.status();
let weak = Arc::downgrade(&client);
drop(client);
@@ -183,8 +182,8 @@ impl UserStore {
Status::Connected { .. } => {
if let Some(user_id) = client.user_id() {
let fetch_user = if let Ok(fetch_user) = this
.update(&mut cx, |this, cx| {
this.get_user(user_id, cx).log_err()
.update(&mut cx, |this, model, cx| {
this.get_user(user_id, model, cx).log_err()
}) {
fetch_user
} else {
@@ -206,7 +205,7 @@ impl UserStore {
staff,
);
this.update(cx, |this, _| {
this.update(cx, |this, model, _| {
this.set_current_user_accepted_tos_at(
info.accepted_tos_at,
);
@@ -218,20 +217,20 @@ impl UserStore {
current_user_tx.send(user).await.ok();
this.update(&mut cx, |_, cx| cx.notify())?;
this.update(&mut cx, |_, model, cx| model.notify(cx))?;
}
}
Status::SignedOut => {
current_user_tx.send(None).await.ok();
this.update(&mut cx, |this, cx| {
cx.notify();
this.update(&mut cx, |this, model, cx| {
model.notify(cx);
this.clear_contacts()
})?
.await;
}
Status::ConnectionLost => {
this.update(&mut cx, |this, cx| {
cx.notify();
this.update(&mut cx, |this, model, cx| {
model.notify(cx);
this.clear_contacts()
})?
.await;
@@ -242,7 +241,7 @@ impl UserStore {
Ok(())
}),
pending_contact_requests: Default::default(),
weak_self: cx.weak_model(),
weak_self: model.downgrade(),
}
}
@@ -257,12 +256,12 @@ impl UserStore {
message: TypedEnvelope<proto::UpdateInviteInfo>,
mut cx: AsyncAppContext,
) -> Result<()> {
this.update(&mut cx, |this, cx| {
this.update(&mut cx, |this, model, cx| {
this.invite_info = Some(InviteInfo {
url: Arc::from(message.payload.url),
count: message.payload.count,
});
cx.notify();
model.notify(cx);
})?;
Ok(())
}
@@ -272,7 +271,7 @@ impl UserStore {
_: TypedEnvelope<proto::ShowContacts>,
mut cx: AsyncAppContext,
) -> Result<()> {
this.update(&mut cx, |_, cx| cx.emit(Event::ShowContacts))?;
this.update(&mut cx, |_, model, cx| model.emit(Event::ShowContacts, cx))?;
Ok(())
}
@@ -285,7 +284,7 @@ impl UserStore {
message: TypedEnvelope<proto::UpdateContacts>,
mut cx: AsyncAppContext,
) -> Result<()> {
this.update(&mut cx, |this, _| {
this.update(&mut cx, |this, _, _| {
this.update_contacts_tx
.unbounded_send(UpdateContacts::Update(message.payload))
.unwrap();
@@ -298,9 +297,9 @@ impl UserStore {
message: TypedEnvelope<proto::UpdateUserPlan>,
mut cx: AsyncAppContext,
) -> Result<()> {
this.update(&mut cx, |this, cx| {
this.update(&mut cx, |this, model, cx| {
this.current_plan = Some(message.payload.plan());
cx.notify();
model.notify(cx);
})?;
Ok(())
}
@@ -308,7 +307,8 @@ impl UserStore {
fn update_contacts(
&mut self,
message: UpdateContacts,
cx: &ModelContext<Self>,
model: &Model<Self>,
cx: &AppContext,
) -> Task<Result<()>> {
match message {
UpdateContacts::Wait(barrier) => {
@@ -330,8 +330,8 @@ impl UserStore {
user_ids.extend(message.incoming_requests.iter().map(|req| req.requester_id));
user_ids.extend(message.outgoing_requests.iter());
let load_users = self.get_users(user_ids.into_iter().collect(), cx);
cx.spawn(|this, mut cx| async move {
let load_users = self.get_users(user_ids.into_iter().collect(), model, cx);
model.spawn(cx, |this, mut cx| async move {
load_users.await?;
// Users are fetched in parallel above and cached in call to get_users
@@ -349,8 +349,8 @@ impl UserStore {
let mut incoming_requests = Vec::new();
for request in message.incoming_requests {
incoming_requests.push({
this.update(&mut cx, |this, cx| {
this.get_user(request.requester_id, cx)
this.update(&mut cx, |this, model, cx| {
this.get_user(request.requester_id, model, cx)
})?
.await?
});
@@ -359,8 +359,10 @@ impl UserStore {
let mut outgoing_requests = Vec::new();
for requested_user_id in message.outgoing_requests {
outgoing_requests.push(
this.update(&mut cx, |this, cx| this.get_user(requested_user_id, cx))?
.await?,
this.update(&mut cx, |this, model, cx| {
this.get_user(requested_user_id, model, cx)
})?
.await?,
);
}
@@ -371,7 +373,7 @@ impl UserStore {
let removed_outgoing_requests =
HashSet::<u64>::from_iter(message.remove_outgoing_requests.iter().copied());
this.update(&mut cx, |this, cx| {
this.update(&mut cx, |this, model, cx| {
// Remove contacts
this.contacts
.retain(|contact| !removed_contacts.contains(&contact.user.id));
@@ -389,10 +391,13 @@ impl UserStore {
// Remove incoming contact requests
this.incoming_contact_requests.retain(|user| {
if removed_incoming_requests.contains(&user.id) {
cx.emit(Event::Contact {
user: user.clone(),
kind: ContactEventKind::Cancelled,
});
model.emit(
Event::Contact {
user: user.clone(),
kind: ContactEventKind::Cancelled,
},
cx,
);
false
} else {
true
@@ -425,7 +430,7 @@ impl UserStore {
}
}
cx.notify();
model.notify(cx);
})?;
Ok(())
@@ -483,17 +488,24 @@ impl UserStore {
pub fn request_contact(
&mut self,
responder_id: u64,
cx: &mut ModelContext<Self>,
model: &Model<Self>,
cx: &mut AppContext,
) -> Task<Result<()>> {
self.perform_contact_request(responder_id, proto::RequestContact { responder_id }, cx)
self.perform_contact_request(
responder_id,
proto::RequestContact { responder_id },
model,
cx,
)
}
pub fn remove_contact(
&mut self,
user_id: u64,
cx: &mut ModelContext<Self>,
model: &Model<Self>,
cx: &mut AppContext,
) -> Task<Result<()>> {
self.perform_contact_request(user_id, proto::RemoveContact { user_id }, cx)
self.perform_contact_request(user_id, proto::RemoveContact { user_id }, model, cx)
}
pub fn has_incoming_contact_request(&self, user_id: u64) -> bool {
@@ -506,7 +518,8 @@ impl UserStore {
&mut self,
requester_id: u64,
accept: bool,
cx: &mut ModelContext<Self>,
model: &Model<Self>,
cx: &mut AppContext,
) -> Task<Result<()>> {
self.perform_contact_request(
requester_id,
@@ -518,6 +531,7 @@ impl UserStore {
proto::ContactRequestResponse::Decline
} as i32,
},
model,
cx,
)
}
@@ -525,10 +539,11 @@ impl UserStore {
pub fn dismiss_contact_request(
&self,
requester_id: u64,
cx: &ModelContext<Self>,
model: &Model<Self>,
cx: &AppContext,
) -> Task<Result<()>> {
let client = self.client.upgrade();
cx.spawn(move |_, _| async move {
cx.spawn(move |_| async move {
client
.ok_or_else(|| anyhow!("can't upgrade client reference"))?
.request(proto::RespondToContactRequest {
@@ -544,18 +559,19 @@ impl UserStore {
&mut self,
user_id: u64,
request: T,
cx: &mut ModelContext<Self>,
model: &Model<Self>,
cx: &mut AppContext,
) -> Task<Result<()>> {
let client = self.client.upgrade();
*self.pending_contact_requests.entry(user_id).or_insert(0) += 1;
cx.notify();
model.notify(cx);
cx.spawn(move |this, mut cx| async move {
model.spawn(cx, move |this, mut cx| async move {
let response = client
.ok_or_else(|| anyhow!("can't upgrade client reference"))?
.request(request)
.await;
this.update(&mut cx, |this, cx| {
this.update(&mut cx, |this, model, cx| {
if let Entry::Occupied(mut request_count) =
this.pending_contact_requests.entry(user_id)
{
@@ -564,7 +580,7 @@ impl UserStore {
request_count.remove();
}
}
cx.notify();
model.notify(cx);
})?;
response?;
Ok(())
@@ -594,25 +610,27 @@ impl UserStore {
pub fn get_users(
&self,
user_ids: Vec<u64>,
cx: &ModelContext<Self>,
model: &Model<Self>,
cx: &AppContext,
) -> Task<Result<Vec<Arc<User>>>> {
let mut user_ids_to_fetch = user_ids.clone();
user_ids_to_fetch.retain(|id| !self.users.contains_key(id));
cx.spawn(|this, mut cx| async move {
model.spawn(cx, |this, mut cx| async move {
if !user_ids_to_fetch.is_empty() {
this.update(&mut cx, |this, cx| {
this.update(&mut cx, |this, model, cx| {
this.load_users(
proto::GetUsers {
user_ids: user_ids_to_fetch,
},
model,
cx,
)
})?
.await?;
}
this.update(&mut cx, |this, _| {
this.update(&mut cx, |this, _, _| {
user_ids
.iter()
.map(|user_id| {
@@ -629,33 +647,44 @@ impl UserStore {
pub fn fuzzy_search_users(
&self,
query: String,
cx: &ModelContext<Self>,
model: &Model<Self>,
cx: &AppContext,
) -> Task<Result<Vec<Arc<User>>>> {
self.load_users(proto::FuzzySearchUsers { query }, cx)
self.load_users(proto::FuzzySearchUsers { query }, model, cx)
}
pub fn get_cached_user(&self, user_id: u64) -> Option<Arc<User>> {
self.users.get(&user_id).cloned()
}
pub fn get_user_optimistic(&self, user_id: u64, cx: &ModelContext<Self>) -> Option<Arc<User>> {
pub fn get_user_optimistic(
&self,
user_id: u64,
model: &Model<Self>,
cx: &AppContext,
) -> Option<Arc<User>> {
if let Some(user) = self.users.get(&user_id).cloned() {
return Some(user);
}
self.get_user(user_id, cx).detach_and_log_err(cx);
self.get_user(user_id, model, cx).detach_and_log_err(cx);
None
}
pub fn get_user(&self, user_id: u64, cx: &ModelContext<Self>) -> Task<Result<Arc<User>>> {
pub fn get_user(
&self,
user_id: u64,
model: &Model<Self>,
cx: &AppContext,
) -> Task<Result<Arc<User>>> {
if let Some(user) = self.users.get(&user_id).cloned() {
return Task::ready(Ok(user));
}
let load_users = self.get_users(vec![user_id], cx);
cx.spawn(move |this, mut cx| async move {
let load_users = self.get_users(vec![user_id], model, cx);
model.spawn(cx, move |this, mut cx| async move {
load_users.await?;
this.update(&mut cx, |this, _| {
this.update(&mut cx, |this, _, _| {
this.users
.get(&user_id)
.cloned()
@@ -687,20 +716,24 @@ impl UserStore {
.map(|accepted_tos_at| accepted_tos_at.is_some())
}
pub fn accept_terms_of_service(&self, cx: &ModelContext<Self>) -> Task<Result<()>> {
pub fn accept_terms_of_service(
&self,
model: &Model<Self>,
cx: &AppContext,
) -> Task<Result<()>> {
if self.current_user().is_none() {
return Task::ready(Err(anyhow!("no current user")));
};
let client = self.client.clone();
cx.spawn(move |this, mut cx| async move {
model.spawn(cx, move |this, mut cx| async move {
if let Some(client) = client.upgrade() {
let response = client
.request(proto::AcceptTermsOfService {})
.await
.context("error accepting tos")?;
this.update(&mut cx, |this, _| {
this.update(&mut cx, |this, _, _| {
this.set_current_user_accepted_tos_at(Some(response.accepted_tos_at))
})
} else {
@@ -718,15 +751,16 @@ impl UserStore {
fn load_users(
&self,
request: impl RequestMessage<Response = UsersResponse>,
cx: &ModelContext<Self>,
model: &Model<Self>,
cx: &AppContext,
) -> Task<Result<Vec<Arc<User>>>> {
let client = self.client.clone();
cx.spawn(|this, mut cx| async move {
model.spawn(cx, |this, mut cx| async move {
if let Some(rpc) = client.upgrade() {
let response = rpc.request(request).await.context("error loading users")?;
let users = response.users;
this.update(&mut cx, |this, _| this.insert(users))
this.update(&mut cx, |this, _, _| this.insert(users))
} else {
Ok(Vec::new())
}
@@ -752,11 +786,12 @@ impl UserStore {
pub fn set_participant_indices(
&mut self,
participant_indices: HashMap<u64, ParticipantIndex>,
cx: &mut ModelContext<Self>,
model: &Model<Self>,
cx: &mut AppContext,
) {
if participant_indices != self.participant_indices {
self.participant_indices = participant_indices;
cx.emit(Event::ParticipantIndicesChanged);
model.emit(Event::ParticipantIndicesChanged, cx);
}
}
@@ -781,8 +816,10 @@ impl UserStore {
if !missing_user_ids.is_empty() {
let this = self.weak_self.clone();
cx.spawn(|mut cx| async move {
this.update(&mut cx, |this, cx| this.get_users(missing_user_ids, cx))?
.await
this.update(&mut cx, |this, model, cx| {
this.get_users(missing_user_ids, model, cx)
})?
.await
})
.detach_and_log_err(cx);
}
@@ -807,8 +844,8 @@ impl Contact {
cx: &mut AsyncAppContext,
) -> Result<Self> {
let user = user_store
.update(cx, |user_store, cx| {
user_store.get_user(contact.user_id, cx)
.update(cx, |user_store, model, cx| {
user_store.get_user(contact.user_id, model, cx)
})?
.await?;
Ok(Self {

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

@@ -9,6 +9,7 @@ use collections::HashSet;
use reqwest::StatusCode;
use sea_orm::ActiveValue;
use serde::{Deserialize, Serialize};
use serde_json::json;
use std::{str::FromStr, sync::Arc, time::Duration};
use stripe::{
BillingPortalSession, CreateBillingPortalSession, CreateBillingPortalSessionFlowData,
@@ -19,6 +20,7 @@ use stripe::{
};
use util::ResultExt;
use crate::api::events::SnowflakeRow;
use crate::llm::{DEFAULT_MAX_MONTHLY_SPEND, FREE_TIER_MONTHLY_SPENDING_LIMIT};
use crate::rpc::{ResultExt as _, Server};
use crate::{
@@ -124,6 +126,20 @@ async fn update_billing_preferences(
.await?
};
SnowflakeRow::new(
"Spend Limit Updated",
Some(user.metrics_id),
user.admin,
None,
json!({
"user_id": user.id,
"max_monthly_llm_usage_spending_in_cents": billing_preferences.max_monthly_llm_usage_spending_in_cents,
}),
)
.write(&app.kinesis_client, &app.config.kinesis_stream)
.await
.log_err();
rpc_server.refresh_llm_tokens_for_user(user.id).await;
Ok(Json(BillingPreferencesResponse {

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

@@ -9,7 +9,7 @@ use collab_ui::channel_view::ChannelView;
use collections::HashMap;
use editor::{Anchor, Editor, ToOffset};
use futures::future;
use gpui::{BackgroundExecutor, Model, TestAppContext, ViewContext};
use gpui::{AppContext, BackgroundExecutor, Model, TestAppContext};
use rpc::{proto::PeerId, RECEIVE_TIMEOUT};
use serde_json::json;
use std::ops::Range;
@@ -161,43 +161,43 @@ async fn test_channel_notes_participant_indices(
// Clients A, B, and C open the channel notes
let channel_view_a = cx_a
.update(|cx| ChannelView::open(channel_id, None, workspace_a.clone(), cx))
.update(|cx| ChannelView::open(channel_id, None, workspace_a.clone(), model, cx))
.await
.unwrap();
let channel_view_b = cx_b
.update(|cx| ChannelView::open(channel_id, None, workspace_b.clone(), cx))
.update(|cx| ChannelView::open(channel_id, None, workspace_b.clone(), model, cx))
.await
.unwrap();
let channel_view_c = cx_c
.update(|cx| ChannelView::open(channel_id, None, workspace_c.clone(), cx))
.update(|cx| ChannelView::open(channel_id, None, workspace_c.clone(), model, cx))
.await
.unwrap();
// Clients A, B, and C all insert and select some text
channel_view_a.update(cx_a, |notes, cx| {
notes.editor.update(cx, |editor, cx| {
notes.editor.update(cx, |editor, model, cx| {
editor.insert("a", cx);
editor.change_selections(None, cx, |selections| {
editor.change_selections(None, model, cx, |selections| {
selections.select_ranges(vec![0..1]);
});
});
});
executor.run_until_parked();
channel_view_b.update(cx_b, |notes, cx| {
notes.editor.update(cx, |editor, cx| {
notes.editor.update(cx, |editor, model, cx| {
editor.move_down(&Default::default(), cx);
editor.insert("b", cx);
editor.change_selections(None, cx, |selections| {
editor.change_selections(None, model, cx, |selections| {
selections.select_ranges(vec![1..2]);
});
});
});
executor.run_until_parked();
channel_view_c.update(cx_c, |notes, cx| {
notes.editor.update(cx, |editor, cx| {
notes.editor.update(cx, |editor, model, cx| {
editor.move_down(&Default::default(), cx);
editor.insert("c", cx);
editor.change_selections(None, cx, |selections| {
editor.change_selections(None, model, cx, |selections| {
selections.select_ranges(vec![2..3]);
});
});
@@ -207,8 +207,8 @@ async fn test_channel_notes_participant_indices(
// in a call together.
executor.run_until_parked();
channel_view_a.update(cx_a, |notes, cx| {
notes.editor.update(cx, |editor, cx| {
assert_remote_selections(editor, &[(None, 1..2), (None, 2..3)], cx);
notes.editor.update(cx, |editor, model, cx| {
assert_remote_selections(editor, &[(None, 1..2), (None, 2..3)], model, cx);
});
});
@@ -223,19 +223,21 @@ async fn test_channel_notes_participant_indices(
// still doesn't have a color.
executor.run_until_parked();
channel_view_a.update(cx_a, |notes, cx| {
notes.editor.update(cx, |editor, cx| {
notes.editor.update(cx, |editor, model, cx| {
assert_remote_selections(
editor,
&[(Some(ParticipantIndex(1)), 1..2), (None, 2..3)],
model,
cx,
);
});
});
channel_view_b.update(cx_b, |notes, cx| {
notes.editor.update(cx, |editor, cx| {
notes.editor.update(cx, |editor, model, cx| {
assert_remote_selections(
editor,
&[(Some(ParticipantIndex(0)), 0..1), (None, 2..3)],
model,
cx,
);
});
@@ -270,12 +272,12 @@ async fn test_channel_notes_participant_indices(
.unwrap();
editor_a.update(cx_a, |editor, cx| {
editor.change_selections(None, cx, |selections| {
editor.change_selections(None, model, cx, |selections| {
selections.select_ranges(vec![0..1]);
});
});
editor_b.update(cx_b, |editor, cx| {
editor.change_selections(None, cx, |selections| {
editor.change_selections(None, model, cx, |selections| {
selections.select_ranges(vec![2..3]);
});
});
@@ -283,10 +285,10 @@ async fn test_channel_notes_participant_indices(
// Clients A and B see each other with the same colors as in the channel notes.
editor_a.update(cx_a, |editor, cx| {
assert_remote_selections(editor, &[(Some(ParticipantIndex(1)), 2..3)], cx);
assert_remote_selections(editor, &[(Some(ParticipantIndex(1)), 2..3)], model, cx);
});
editor_b.update(cx_b, |editor, cx| {
assert_remote_selections(editor, &[(Some(ParticipantIndex(0)), 0..1)], cx);
assert_remote_selections(editor, &[(Some(ParticipantIndex(0)), 0..1)], model, cx);
});
}
@@ -294,9 +296,10 @@ async fn test_channel_notes_participant_indices(
fn assert_remote_selections(
editor: &mut Editor,
expected_selections: &[(Option<ParticipantIndex>, Range<usize>)],
cx: &mut ViewContext<Editor>,
model: &Model<Editor>,
cx: &mut AppContext,
) {
let snapshot = editor.snapshot(cx);
let snapshot = editor.snapshot(model, cx);
let range = Anchor::min()..Anchor::max();
let remote_selections = snapshot
.remote_selections_in_range(&range, editor.collaboration_hub().unwrap(), cx)
@@ -344,7 +347,7 @@ async fn test_multiple_handles_to_channel_buffer(
assert_eq!(channel_buffer, channel_buffer_3);
channel_buffer.update(cx_a, |buffer, cx| {
buffer.buffer().update(cx, |buffer, cx| {
buffer.buffer().update(cx, |buffer, model, cx| {
buffer.edit([(0..0, "hello")], None, cx);
})
});
@@ -365,7 +368,7 @@ async fn test_multiple_handles_to_channel_buffer(
.unwrap();
assert_ne!(channel_buffer.entity_id(), channel_buffer_model_id);
channel_buffer.update(cx_a, |buffer, cx| {
buffer.buffer().update(cx, |buffer, _| {
buffer.buffer().update(cx, |buffer, model, _| {
assert_eq!(buffer.text(), "hello");
})
});
@@ -465,7 +468,7 @@ async fn test_rejoin_channel_buffer(
.unwrap();
channel_buffer_a.update(cx_a, |buffer, cx| {
buffer.buffer().update(cx, |buffer, cx| {
buffer.buffer().update(cx, |buffer, model, cx| {
buffer.edit([(0..0, "1")], None, cx);
})
});
@@ -477,12 +480,12 @@ async fn test_rejoin_channel_buffer(
// Both clients make an edit.
channel_buffer_a.update(cx_a, |buffer, cx| {
buffer.buffer().update(cx, |buffer, cx| {
buffer.buffer().update(cx, |buffer, model, cx| {
buffer.edit([(1..1, "2")], None, cx);
})
});
channel_buffer_b.update(cx_b, |buffer, cx| {
buffer.buffer().update(cx, |buffer, cx| {
buffer.buffer().update(cx, |buffer, model, cx| {
buffer.edit([(0..0, "0")], None, cx);
})
});
@@ -552,7 +555,7 @@ async fn test_channel_buffers_and_server_restarts(
.unwrap();
channel_buffer_a.update(cx_a, |buffer, cx| {
buffer.buffer().update(cx, |buffer, cx| {
buffer.buffer().update(cx, |buffer, model, cx| {
buffer.edit([(0..0, "1")], None, cx);
})
});
@@ -567,12 +570,12 @@ async fn test_channel_buffers_and_server_restarts(
// While the server is down, both clients make an edit.
channel_buffer_a.update(cx_a, |buffer, cx| {
buffer.buffer().update(cx, |buffer, cx| {
buffer.buffer().update(cx, |buffer, model, cx| {
buffer.edit([(1..1, "2")], None, cx);
})
});
channel_buffer_b.update(cx_b, |buffer, cx| {
buffer.buffer().update(cx, |buffer, cx| {
buffer.buffer().update(cx, |buffer, model, cx| {
buffer.edit([(0..0, "0")], None, cx);
})
});
@@ -642,8 +645,8 @@ async fn test_channel_buffer_changes(
// Closing the buffer should re-enable change tracking
cx_b.update(|cx| {
workspace_b.update(cx, |workspace, cx| {
workspace.close_all_items_and_panes(&Default::default(), cx)
workspace_b.update(cx, |workspace, model, cx| {
workspace.close_all_items_and_panes(&Default::default(), model, cx)
});
});
deterministic.run_until_parked();

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
@@ -116,7 +118,7 @@ async fn test_channel_guest_promotion(cx_a: &mut TestAppContext, cx_b: &mut Test
// B is promoted
active_call_a
.update(cx_a, |call, cx| {
call.room().unwrap().update(cx, |room, cx| {
call.room().unwrap().update(cx, |room, model, cx| {
room.set_participant_role(
client_b.user_id().unwrap(),
proto::ChannelRole::Member,
@@ -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();
@@ -142,7 +146,7 @@ async fn test_channel_guest_promotion(cx_a: &mut TestAppContext, cx_b: &mut Test
// B is demoted
active_call_a
.update(cx_a, |call, cx| {
call.room().unwrap().update(cx, |room, cx| {
call.room().unwrap().update(cx, |room, model, cx| {
room.set_participant_role(
client_b.user_id().unwrap(),
proto::ChannelRole::Guest,
@@ -226,13 +230,15 @@ 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.
active_call_a
.update(cx_a, |call, cx| {
call.room().unwrap().update(cx, |room, cx| {
call.room().unwrap().update(cx, |room, model, cx| {
room.set_participant_role(
client_b.user_id().unwrap(),
proto::ChannelRole::Member,
@@ -244,13 +250,15 @@ 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.
active_call_a
.update(cx_a, |call, cx| {
call.room().unwrap().update(cx, |room, cx| {
call.room().unwrap().update(cx, |room, model, cx| {
room.set_participant_role(
client_b.user_id().unwrap(),
proto::ChannelRole::Talker,
@@ -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
@@ -275,7 +285,7 @@ async fn test_channel_requires_zed_cla(cx_a: &mut TestAppContext, cx_b: &mut Tes
// A can now grant write access to B.
active_call_a
.update(cx_a, |call, cx| {
call.room().unwrap().update(cx, |room, cx| {
call.room().unwrap().update(cx, |room, model, cx| {
room.set_participant_role(
client_b.user_id().unwrap(),
proto::ChannelRole::Member,
@@ -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

@@ -82,8 +82,15 @@ async fn test_host_disconnect(
assert!(worktree_a.read_with(cx_a, |tree, _| tree.has_update_observer()));
let workspace_b = cx_b
.add_window(|cx| Workspace::new(None, project_b.clone(), client_b.app_state.clone(), cx));
let workspace_b = cx_b.add_window(|cx| {
Workspace::new(
None,
project_b.clone(),
client_b.app_state.clone(),
model,
cx,
)
});
let cx_b = &mut VisualTestContext::from_window(*workspace_b, cx_b);
let workspace_b_view = workspace_b.root_view(cx_b).unwrap();
@@ -98,7 +105,7 @@ async fn test_host_disconnect(
.unwrap();
//TODO: focus
assert!(cx_b.update_view(&editor_b, |editor, cx| editor.is_focused(cx)));
assert!(cx_b.update_view(&editor_b, |editor, cx| editor.is_focused(window)));
editor_b.update(cx_b, |editor, cx| editor.insert("X", cx));
cx_b.update(|cx| {
@@ -201,7 +208,7 @@ async fn test_newline_above_or_below_does_not_move_guest_cursor(
.await
.unwrap();
let cx_a = cx_a.add_empty_window();
let editor_a = cx_a.new_view(|cx| Editor::for_buffer(buffer_a, Some(project_a), cx));
let editor_a = cx_a.new_view(|cx| Editor::for_buffer(buffer_a, Some(project_a), model, cx));
let mut editor_cx_a = EditorTestContext {
cx: cx_a.clone(),
@@ -216,7 +223,7 @@ async fn test_newline_above_or_below_does_not_move_guest_cursor(
.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
.await
.unwrap();
let editor_b = cx_b.new_view(|cx| Editor::for_buffer(buffer_b, Some(project_b), cx));
let editor_b = cx_b.new_view(|cx| Editor::for_buffer(buffer_b, Some(project_b), model, cx));
let mut editor_cx_b = EditorTestContext {
cx: cx_b.clone(),
@@ -317,8 +324,8 @@ async fn test_collaborating_with_completion(cx_a: &mut TestAppContext, cx_b: &mu
.await
.unwrap();
let cx_b = cx_b.add_empty_window();
let editor_b =
cx_b.new_view(|cx| Editor::for_buffer(buffer_b.clone(), Some(project_b.clone()), cx));
let editor_b = cx_b
.new_view(|cx| Editor::for_buffer(buffer_b.clone(), Some(project_b.clone()), model, cx));
let fake_language_server = fake_language_servers.next().await.unwrap();
cx_a.background_executor.run_until_parked();
@@ -329,7 +336,7 @@ async fn test_collaborating_with_completion(cx_a: &mut TestAppContext, cx_b: &mu
// Type a completion trigger character as the guest.
editor_b.update(cx_b, |editor, cx| {
editor.change_selections(None, cx, |s| s.select_ranges([13..13]));
editor.change_selections(None, model, cx, |s| s.select_ranges([13..13]));
editor.handle_input(".", cx);
});
cx_b.focus_view(&editor_b);
@@ -442,7 +449,7 @@ async fn test_collaborating_with_completion(cx_a: &mut TestAppContext, cx_b: &mu
// Now we do a second completion, this time to ensure that documentation/snippets are
// resolved
editor_b.update(cx_b, |editor, cx| {
editor.change_selections(None, cx, |s| s.select_ranges([46..46]));
editor.change_selections(None, model, cx, |s| s.select_ranges([46..46]));
editor.handle_input("; a", cx);
editor.handle_input(".", cx);
});
@@ -594,7 +601,7 @@ async fn test_collaborating_with_code_actions(
// Move cursor to a location that contains code actions.
editor_b.update(cx_b, |editor, cx| {
editor.change_selections(None, cx, |s| {
editor.change_selections(None, model, cx, |s| {
s.select_ranges([Point::new(1, 31)..Point::new(1, 31)])
});
});
@@ -675,7 +682,7 @@ async fn test_collaborating_with_code_actions(
// Confirming the code action will trigger a resolve request.
let confirm_action = editor_b
.update(cx_b, |editor, cx| {
Editor::confirm_code_action(editor, &ConfirmCodeAction { item_ix: Some(0) }, cx)
Editor::confirm_code_action(editor, &ConfirmCodeAction { item_ix: Some(0) }, model, cx)
})
.unwrap();
fake_language_server.handle_request::<lsp::request::CodeActionResolveRequest, _, _>(
@@ -796,7 +803,7 @@ async fn test_collaborating_with_renames(cx_a: &mut TestAppContext, cx_b: &mut T
// Move cursor to a location that can be renamed.
let prepare_rename = editor_b.update(cx_b, |editor, cx| {
editor.change_selections(None, cx, |s| s.select_ranges([7..7]));
editor.change_selections(None, model, cx, |s| s.select_ranges([7..7]));
editor.rename(&Rename, cx).unwrap()
});
@@ -821,14 +828,14 @@ async fn test_collaborating_with_renames(cx_a: &mut TestAppContext, cx_b: &mut T
rename.range.start.to_offset(&buffer)..rename.range.end.to_offset(&buffer),
6..9
);
rename.editor.update(cx, |rename_editor, cx| {
rename.editor.update(cx, |rename_editor, model, cx| {
let rename_selection = rename_editor.selections.newest::<usize>(cx);
assert_eq!(
rename_selection.range(),
0..3,
"Rename that was triggered from zero selection caret, should propose the whole word."
);
rename_editor.buffer().update(cx, |rename_buffer, cx| {
rename_editor.buffer().update(cx, |rename_buffer, model, cx| {
rename_buffer.edit([(0..3, "THREE")], None, cx);
});
});
@@ -839,7 +846,7 @@ async fn test_collaborating_with_renames(cx_a: &mut TestAppContext, cx_b: &mut T
editor.cancel(&editor::actions::Cancel, cx);
});
let prepare_rename = editor_b.update(cx_b, |editor, cx| {
editor.change_selections(None, cx, |s| s.select_ranges([7..8]));
editor.change_selections(None, model, cx, |s| s.select_ranges([7..8]));
editor.rename(&Rename, cx).unwrap()
});
@@ -863,21 +870,21 @@ async fn test_collaborating_with_renames(cx_a: &mut TestAppContext, cx_b: &mut T
let lsp_rename_start = rename.range.start.to_offset(&buffer);
let lsp_rename_end = rename.range.end.to_offset(&buffer);
assert_eq!(lsp_rename_start..lsp_rename_end, 6..9);
rename.editor.update(cx, |rename_editor, cx| {
rename.editor.update(cx, |rename_editor, model, cx| {
let rename_selection = rename_editor.selections.newest::<usize>(cx);
assert_eq!(
rename_selection.range(),
1..2,
"Rename that was triggered from a selection, should have the same selection range in the rename proposal"
);
rename_editor.buffer().update(cx, |rename_buffer, cx| {
rename_editor.buffer().update(cx, |rename_buffer, model, cx| {
rename_buffer.edit([(0..lsp_rename_end - lsp_rename_start, "THREE")], None, cx);
});
});
});
let confirm_rename = editor_b.update(cx_b, |editor, cx| {
Editor::confirm_rename(editor, &ConfirmRename, cx).unwrap()
Editor::confirm_rename(editor, &ConfirmRename, model, cx).unwrap()
});
fake_language_server
.handle_request::<lsp::request::Rename, _, _>(|params, _| async move {
@@ -1191,7 +1198,7 @@ async fn test_share_project(
.await
.unwrap();
let editor_b = cx_b.new_view(|cx| Editor::for_buffer(buffer_b, None, cx));
let editor_b = cx_b.new_view(|cx| Editor::for_buffer(buffer_b, None, model, cx));
// Client A sees client B's selection
executor.run_until_parked();
@@ -1296,7 +1303,8 @@ async fn test_on_input_format_from_host_to_guest(
.await
.unwrap();
let cx_a = cx_a.add_empty_window();
let editor_a = cx_a.new_view(|cx| Editor::for_buffer(buffer_a, Some(project_a.clone()), cx));
let editor_a =
cx_a.new_view(|cx| Editor::for_buffer(buffer_a, Some(project_a.clone()), model, cx));
let fake_language_server = fake_language_servers.next().await.unwrap();
executor.run_until_parked();
@@ -1330,7 +1338,7 @@ async fn test_on_input_format_from_host_to_guest(
// Type a on type formatting trigger character as the guest.
cx_a.focus_view(&editor_a);
editor_a.update(cx_a, |editor, cx| {
editor.change_selections(None, cx, |s| s.select_ranges([13..13]));
editor.change_selections(None, model, cx, |s| s.select_ranges([13..13]));
editor.handle_input(">", cx);
});
@@ -1416,7 +1424,8 @@ async fn test_on_input_format_from_guest_to_host(
.await
.unwrap();
let cx_b = cx_b.add_empty_window();
let editor_b = cx_b.new_view(|cx| Editor::for_buffer(buffer_b, Some(project_b.clone()), cx));
let editor_b =
cx_b.new_view(|cx| Editor::for_buffer(buffer_b, Some(project_b.clone()), model, cx));
let fake_language_server = fake_language_servers.next().await.unwrap();
executor.run_until_parked();
@@ -1424,7 +1433,7 @@ async fn test_on_input_format_from_guest_to_host(
// Type a on type formatting trigger character as the guest.
cx_b.focus_view(&editor_b);
editor_b.update(cx_b, |editor, cx| {
editor.change_selections(None, cx, |s| s.select_ranges([13..13]));
editor.change_selections(None, model, cx, |s| s.select_ranges([13..13]));
editor.handle_input(":", cx);
});
@@ -1668,7 +1677,7 @@ async fn test_mutual_editor_inlay_hint_cache_update(
let after_client_edit = edits_made.fetch_add(1, atomic::Ordering::Release) + 1;
editor_b.update(cx_b, |editor, cx| {
editor.change_selections(None, cx, |s| s.select_ranges([13..13].clone()));
editor.change_selections(None, model, cx, |s| s.select_ranges([13..13].clone()));
editor.handle_input(":", cx);
});
cx_b.focus_view(&editor_b);
@@ -1693,7 +1702,7 @@ async fn test_mutual_editor_inlay_hint_cache_update(
let after_host_edit = edits_made.fetch_add(1, atomic::Ordering::Release) + 1;
editor_a.update(cx_a, |editor, cx| {
editor.change_selections(None, cx, |s| s.select_ranges([13..13]));
editor.change_selections(None, model, cx, |s| s.select_ranges([13..13]));
editor.handle_input("a change to increment both buffers' versions", cx);
});
cx_a.focus_view(&editor_a);
@@ -2070,7 +2079,7 @@ async fn test_git_blame_is_forwarded(cx_a: &mut TestAppContext, cx_b: &mut TestA
editor_b.update(cx_b, |editor_b, cx| {
let blame = editor_b.blame().expect("editor_b should have blame now");
let entries = blame.update(cx, |blame, cx| {
let entries = blame.update(cx, |blame, model, cx| {
blame
.blame_for_rows((0..4).map(MultiBufferRow).map(Some), cx)
.collect::<Vec<_>>()
@@ -2086,7 +2095,7 @@ async fn test_git_blame_is_forwarded(cx_a: &mut TestAppContext, cx_b: &mut TestA
]
);
blame.update(cx, |blame, _| {
blame.update(cx, |blame, model, _| {
for (idx, entry) in entries.iter().flatten().enumerate() {
let details = blame.details_for_entry(entry).unwrap();
assert_eq!(details.message, format!("message for idx-{}", idx));
@@ -2109,7 +2118,7 @@ async fn test_git_blame_is_forwarded(cx_a: &mut TestAppContext, cx_b: &mut TestA
editor_b.update(cx_b, |editor_b, cx| {
let blame = editor_b.blame().expect("editor_b should have blame now");
let entries = blame.update(cx, |blame, cx| {
let entries = blame.update(cx, |blame, model, cx| {
blame
.blame_for_rows((0..4).map(MultiBufferRow).map(Some), cx)
.collect::<Vec<_>>()
@@ -2136,7 +2145,7 @@ async fn test_git_blame_is_forwarded(cx_a: &mut TestAppContext, cx_b: &mut TestA
editor_b.update(cx_b, |editor_b, cx| {
let blame = editor_b.blame().expect("editor_b should have blame now");
let entries = blame.update(cx, |blame, cx| {
let entries = blame.update(cx, |blame, model, cx| {
blame
.blame_for_rows((0..4).map(MultiBufferRow).map(Some), cx)
.collect::<Vec<_>>()
@@ -2206,9 +2215,9 @@ async fn test_collaborating_with_editorconfig(
.unwrap();
let cx_a = cx_a.add_empty_window();
let main_editor_a =
cx_a.new_view(|cx| Editor::for_buffer(main_buffer_a, Some(project_a.clone()), cx));
cx_a.new_view(|cx| Editor::for_buffer(main_buffer_a, Some(project_a.clone()), model, cx));
let other_editor_a =
cx_a.new_view(|cx| Editor::for_buffer(other_buffer_a, Some(project_a), cx));
cx_a.new_view(|cx| Editor::for_buffer(other_buffer_a, Some(project_a), model, cx));
let mut main_editor_cx_a = EditorTestContext {
cx: cx_a.clone(),
window: cx_a.handle(),
@@ -2238,9 +2247,9 @@ async fn test_collaborating_with_editorconfig(
.unwrap();
let cx_b = cx_b.add_empty_window();
let main_editor_b =
cx_b.new_view(|cx| Editor::for_buffer(main_buffer_b, Some(project_b.clone()), cx));
cx_b.new_view(|cx| Editor::for_buffer(main_buffer_b, Some(project_b.clone()), model, cx));
let other_editor_b =
cx_b.new_view(|cx| Editor::for_buffer(other_buffer_b, Some(project_b.clone()), cx));
cx_b.new_view(|cx| Editor::for_buffer(other_buffer_b, Some(project_b.clone()), model, cx));
let mut main_editor_cx_b = EditorTestContext {
cx: cx_b.clone(),
window: cx_b.handle(),

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::{
@@ -8,21 +8,15 @@ use collab_ui::{
};
use editor::{Editor, ExcerptRange, MultiBuffer};
use gpui::{
point, BackgroundExecutor, BorrowAppContext, Context, Entity, SharedString, TestAppContext,
View, VisualContext, VisualTestContext,
point, AppContext, BackgroundExecutor, BorrowAppContext, Context, Entity, SharedString,
TestAppContext, 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;
@@ -279,12 +273,12 @@ async fn test_basic_following(
// When client A opens a multibuffer, client B does so as well.
let multibuffer_a = cx_a.new_model(|cx| {
let buffer_a1 = project_a.update(cx, |project, cx| {
let buffer_a1 = project_a.update(cx, |project, model, cx| {
project
.get_open_buffer(&(worktree_id, "1.txt").into(), cx)
.unwrap()
});
let buffer_a2 = project_a.update(cx, |project, cx| {
let buffer_a2 = project_a.update(cx, |project, model, cx| {
project
.get_open_buffer(&(worktree_id, "2.txt").into(), cx)
.unwrap()
@@ -296,6 +290,7 @@ async fn test_basic_following(
context: 0..3,
primary: None,
}],
model,
cx,
);
result.push_excerpts(
@@ -304,15 +299,16 @@ async fn test_basic_following(
context: 4..7,
primary: None,
}],
model,
cx,
);
result
});
let multibuffer_editor_a = workspace_a.update(cx_a, |workspace, cx| {
let editor = cx.new_view(|cx| {
Editor::for_multibuffer(multibuffer_a, Some(project_a.clone()), true, cx)
let editor = cx.new_model(|model, cx| {
Editor::for_multibuffer(multibuffer_a, Some(project_a.clone()), true, model, cx)
});
workspace.add_item_to_active_pane(Box::new(editor.clone()), None, true, cx);
workspace.add_item_to_active_pane(Box::new(editor.clone()), None, true, model, cx);
editor
});
executor.run_until_parked();
@@ -373,7 +369,7 @@ async fn test_basic_following(
// Changes to client A's editor are reflected on client B.
editor_a1.update(cx_a, |editor, cx| {
editor.change_selections(None, cx, |s| s.select_ranges([1..1, 2..2]));
editor.change_selections(None, model, cx, |s| s.select_ranges([1..1, 2..2]));
});
executor.advance_clock(workspace::item::LEADER_UPDATE_THROTTLE);
executor.run_until_parked();
@@ -388,7 +384,7 @@ async fn test_basic_following(
editor_b1.update(cx_b, |editor, cx| assert_eq!(editor.text(cx), "TWO"));
editor_a1.update(cx_a, |editor, cx| {
editor.change_selections(None, cx, |s| s.select_ranges([3..3]));
editor.change_selections(None, model, cx, |s| s.select_ranges([3..3]));
editor.set_scroll_position(point(0., 100.), cx);
});
executor.advance_clock(workspace::item::LEADER_UPDATE_THROTTLE);
@@ -428,106 +424,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, model, 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, model, 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]
@@ -599,8 +607,8 @@ async fn test_following_tab_order(
.await
.unwrap();
let pane_paths = |pane: &View<workspace::Pane>, cx: &mut VisualTestContext| {
pane.update(cx, |pane, cx| {
let pane_paths = |pane: &Model<workspace::Pane>, cx: &mut VisualTestContext| {
pane.update(cx, |pane, model, cx| {
pane.items()
.map(|item| {
item.project_path(cx)
@@ -918,7 +926,7 @@ async fn test_peers_following_each_other(cx_a: &mut TestAppContext, cx_b: &mut T
// Client B focuses a file that they previously followed A to, breaking
// the follow.
workspace_b.update(cx_b, |workspace, cx| {
workspace.active_pane().update(cx, |pane, cx| {
workspace.active_pane().update(cx, |pane, model, cx| {
pane.activate_prev_item(true, cx);
});
});
@@ -969,7 +977,7 @@ async fn test_peers_following_each_other(cx_a: &mut TestAppContext, cx_b: &mut T
// Client B closes tabs, some of which were originally opened by client A,
// and some of which were originally opened by client B.
workspace_b.update(cx_b, |workspace, cx| {
workspace.active_pane().update(cx, |pane, cx| {
workspace.active_pane().update(cx, |pane, model, cx| {
pane.close_inactive_items(&Default::default(), cx)
.unwrap()
.detach();
@@ -1022,7 +1030,7 @@ async fn test_peers_following_each_other(cx_a: &mut TestAppContext, cx_b: &mut T
executor.run_until_parked();
// Client A cycles through some tabs.
workspace_a.update(cx_a, |workspace, cx| {
workspace.active_pane().update(cx, |pane, cx| {
workspace.active_pane().update(cx, |pane, model, cx| {
pane.activate_prev_item(true, cx);
});
});
@@ -1066,7 +1074,7 @@ async fn test_peers_following_each_other(cx_a: &mut TestAppContext, cx_b: &mut T
);
workspace_a.update(cx_a, |workspace, cx| {
workspace.active_pane().update(cx, |pane, cx| {
workspace.active_pane().update(cx, |pane, model, cx| {
pane.activate_prev_item(true, cx);
});
});
@@ -1113,7 +1121,7 @@ async fn test_peers_following_each_other(cx_a: &mut TestAppContext, cx_b: &mut T
);
workspace_a.update(cx_a, |workspace, cx| {
workspace.active_pane().update(cx, |pane, cx| {
workspace.active_pane().update(cx, |pane, model, cx| {
pane.activate_prev_item(true, cx);
});
});
@@ -1602,7 +1610,7 @@ async fn test_following_stops_on_unshare(cx_a: &mut TestAppContext, cx_b: &mut T
// b should follow a to position 1
editor_a.update(cx_a, |editor, cx| {
editor.change_selections(None, cx, |s| s.select_ranges([1..1]))
editor.change_selections(None, model, cx, |s| s.select_ranges([1..1]))
});
cx_a.executor()
.advance_clock(workspace::item::LEADER_UPDATE_THROTTLE);
@@ -1612,17 +1620,18 @@ async fn test_following_stops_on_unshare(cx_a: &mut TestAppContext, cx_b: &mut T
});
// a unshares the project
cx_a.update(|cx| {
// todo! why do i need the annotation
cx_a.update(|cx: &mut AppContext| {
let project = workspace_a.read(cx).project().clone();
ActiveCall::global(cx).update(cx, |call, cx| {
call.unshare_project(project, cx).unwrap();
ActiveCall::global(cx).update(cx, |call, model, cx| {
call.unshare_project(project, model, cx).unwrap();
})
});
cx_a.run_until_parked();
// b should not follow a to position 2
editor_a.update(cx_a, |editor, cx| {
editor.change_selections(None, cx, |s| s.select_ranges([2..2]))
editor.change_selections(None, model, cx, |s| s.select_ranges([2..2]))
});
cx_a.executor()
.advance_clock(workspace::item::LEADER_UPDATE_THROTTLE);
@@ -1770,11 +1779,11 @@ async fn test_following_into_excluded_file(
fn visible_push_notifications(
cx: &mut TestAppContext,
) -> Vec<gpui::View<ProjectSharedNotification>> {
) -> Vec<gpui::Model<ProjectSharedNotification>> {
let mut ret = Vec::new();
for window in cx.windows() {
window
.update(cx, |window, _| {
.update(cx, |window, model, _| {
if let Ok(handle) = window.downcast::<ProjectSharedNotification>() {
ret.push(handle)
}
@@ -1815,8 +1824,8 @@ fn followers_by_leader(project_id: u64, cx: &TestAppContext) -> Vec<(PeerId, Vec
})
}
fn pane_summaries(workspace: &View<Workspace>, cx: &mut VisualTestContext) -> Vec<PaneSummary> {
workspace.update(cx, |workspace, cx| {
fn pane_summaries(workspace: &Model<Workspace>, cx: &mut VisualTestContext) -> Vec<PaneSummary> {
workspace.update(cx, |workspace, model, cx| {
let active_pane = workspace.active_pane();
workspace
.panes()
@@ -1918,14 +1927,14 @@ async fn test_following_to_channel_notes_without_a_shared_project(
// Client A opens the notes for channel 1.
let channel_notes_1_a = cx_a
.update(|cx| ChannelView::open(channel_1_id, None, workspace_a.clone(), cx))
.update(|cx| ChannelView::open(channel_1_id, None, workspace_a.clone(), model, cx))
.await
.unwrap();
channel_notes_1_a.update(cx_a, |notes, cx| {
assert_eq!(notes.channel(cx).unwrap().name, "channel-1");
notes.editor.update(cx, |editor, cx| {
notes.editor.update(cx, |editor, model, cx| {
editor.insert("Hello from A.", cx);
editor.change_selections(None, cx, |selections| {
editor.change_selections(None, model, cx, |selections| {
selections.select_ranges(vec![3..4]);
});
});
@@ -1957,7 +1966,7 @@ async fn test_following_to_channel_notes_without_a_shared_project(
});
channel_notes_1_b.update(cx_b, |notes, cx| {
assert_eq!(notes.channel(cx).unwrap().name, "channel-1");
notes.editor.update(cx, |editor, cx| {
notes.editor.update(cx, |editor, model, cx| {
assert_eq!(editor.text(cx), "Hello from A.");
assert_eq!(editor.selections.ranges::<usize>(cx), &[3..4]);
})
@@ -1965,7 +1974,7 @@ async fn test_following_to_channel_notes_without_a_shared_project(
// Client A opens the notes for channel 2.
let channel_notes_2_a = cx_a
.update(|cx| ChannelView::open(channel_2_id, None, workspace_a.clone(), cx))
.update(|cx| ChannelView::open(channel_2_id, None, workspace_a.clone(), model, cx))
.await
.unwrap();
channel_notes_2_a.update(cx_a, |notes, cx| {
@@ -2021,12 +2030,12 @@ pub(crate) async fn join_channel(
}
async fn share_workspace(
workspace: &View<Workspace>,
workspace: &Model<Workspace>,
cx: &mut VisualTestContext,
) -> anyhow::Result<u64> {
let project = workspace.update(cx, |workspace, _| workspace.project().clone());
let project = workspace.update(cx, |workspace, model, _| workspace.project().clone());
cx.read(ActiveCall::global)
.update(cx, |call, cx| call.share_project(project, cx))
.update(cx, |call, model, cx| call.share_project(project, model, cx))
.await
}
@@ -2065,7 +2074,7 @@ async fn test_following_to_channel_notes_other_workspace(
let (workspace_a2, cx_a2) = client_a.build_test_workspace(&mut cx_a2).await;
cx_a2.update(|cx| cx.activate_window());
cx_a2
.update(|cx| ChannelView::open(channel, None, workspace_a2, cx))
.update(|cx| ChannelView::open(channel, None, workspace_a2, model, cx))
.await
.unwrap();
cx_a2.run_until_parked();

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, model, 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, model, cx| room.share_screen(cx))
})
.await
.unwrap();
@@ -6159,11 +6173,11 @@ async fn test_pane_split_left(cx: &mut TestAppContext) {
let (workspace, cx) = client.build_test_workspace(cx).await;
cx.simulate_keystrokes("cmd-n");
workspace.update(cx, |workspace, cx| {
workspace.update(cx, |workspace, model, cx| {
assert!(workspace.items(cx).collect::<Vec<_>>().len() == 1);
});
cx.simulate_keystrokes("cmd-k left");
workspace.update(cx, |workspace, cx| {
workspace.update(cx, |workspace, model, cx| {
assert!(workspace.items(cx).collect::<Vec<_>>().len() == 2);
});
cx.simulate_keystrokes("cmd-k");
@@ -6171,7 +6185,7 @@ async fn test_pane_split_left(cx: &mut TestAppContext) {
// to verify that it doesn't fire in this case.
cx.executor().advance_clock(Duration::from_secs(2));
cx.simulate_keystrokes("left");
workspace.update(cx, |workspace, cx| {
workspace.update(cx, |workspace, model, cx| {
assert!(workspace.items(cx).collect::<Vec<_>>().len() == 2);
});
}
@@ -6193,9 +6207,9 @@ async fn test_join_after_restart(cx1: &mut TestAppContext, cx2: &mut TestAppCont
async fn test_preview_tabs(cx: &mut TestAppContext) {
let (_server, client) = TestServer::start1(cx).await;
let (workspace, cx) = client.build_test_workspace(cx).await;
let project = workspace.update(cx, |workspace, _| workspace.project().clone());
let project = workspace.update(cx, |workspace, model, _| workspace.project().clone());
let worktree_id = project.update(cx, |project, cx| {
let worktree_id = project.update(cx, |project, model, cx| {
project.worktrees(cx).next().unwrap().read(cx).id()
});
@@ -6212,7 +6226,7 @@ async fn test_preview_tabs(cx: &mut TestAppContext) {
path: Path::new("3.rs").into(),
};
let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
let pane = workspace.update(cx, |workspace, model, _| workspace.active_pane().clone());
let get_path = |pane: &Pane, idx: usize, cx: &AppContext| {
pane.item_for_index(idx).unwrap().project_path(cx).unwrap()
@@ -6220,13 +6234,13 @@ async fn test_preview_tabs(cx: &mut TestAppContext) {
// Opening item 3 as a "permanent" tab
workspace
.update(cx, |workspace, cx| {
.update(cx, |workspace, model, cx| {
workspace.open_path(path_3.clone(), None, false, cx)
})
.await
.unwrap();
pane.update(cx, |pane, cx| {
pane.update(cx, |pane, model, cx| {
assert_eq!(pane.items_len(), 1);
assert_eq!(get_path(pane, 0, cx), path_3.clone());
assert_eq!(pane.preview_item_id(), None);
@@ -6237,13 +6251,13 @@ async fn test_preview_tabs(cx: &mut TestAppContext) {
// Open item 1 as preview
workspace
.update(cx, |workspace, cx| {
.update(cx, |workspace, model, cx| {
workspace.open_path_preview(path_1.clone(), None, true, true, cx)
})
.await
.unwrap();
pane.update(cx, |pane, cx| {
pane.update(cx, |pane, model, cx| {
assert_eq!(pane.items_len(), 2);
assert_eq!(get_path(pane, 0, cx), path_3.clone());
assert_eq!(get_path(pane, 1, cx), path_1.clone());
@@ -6258,13 +6272,13 @@ async fn test_preview_tabs(cx: &mut TestAppContext) {
// Open item 2 as preview
workspace
.update(cx, |workspace, cx| {
.update(cx, |workspace, model, cx| {
workspace.open_path_preview(path_2.clone(), None, true, true, cx)
})
.await
.unwrap();
pane.update(cx, |pane, cx| {
pane.update(cx, |pane, model, cx| {
assert_eq!(pane.items_len(), 2);
assert_eq!(get_path(pane, 0, cx), path_3.clone());
assert_eq!(get_path(pane, 1, cx), path_2.clone());
@@ -6279,11 +6293,11 @@ async fn test_preview_tabs(cx: &mut TestAppContext) {
// Going back should show item 1 as preview
workspace
.update(cx, |workspace, cx| workspace.go_back(pane.downgrade(), cx))
.update(cx, |workspace, model, cx| workspace.go_back(pane.downgrade(), cx))
.await
.unwrap();
pane.update(cx, |pane, cx| {
pane.update(cx, |pane, model, cx| {
assert_eq!(pane.items_len(), 2);
assert_eq!(get_path(pane, 0, cx), path_3.clone());
assert_eq!(get_path(pane, 1, cx), path_1.clone());
@@ -6297,7 +6311,7 @@ async fn test_preview_tabs(cx: &mut TestAppContext) {
});
// Closing item 1
pane.update(cx, |pane, cx| {
pane.update(cx, |pane, model, cx| {
pane.close_item_by_id(
pane.active_item().unwrap().item_id(),
workspace::SaveIntent::Skip,
@@ -6307,7 +6321,7 @@ async fn test_preview_tabs(cx: &mut TestAppContext) {
.await
.unwrap();
pane.update(cx, |pane, cx| {
pane.update(cx, |pane, model, cx| {
assert_eq!(pane.items_len(), 1);
assert_eq!(get_path(pane, 0, cx), path_3.clone());
assert_eq!(pane.preview_item_id(), None);
@@ -6318,11 +6332,11 @@ async fn test_preview_tabs(cx: &mut TestAppContext) {
// Going back should show item 1 as preview
workspace
.update(cx, |workspace, cx| workspace.go_back(pane.downgrade(), cx))
.update(cx, |workspace, model, cx| workspace.go_back(pane.downgrade(), cx))
.await
.unwrap();
pane.update(cx, |pane, cx| {
pane.update(cx, |pane, model, cx| {
assert_eq!(pane.items_len(), 2);
assert_eq!(get_path(pane, 0, cx), path_3.clone());
assert_eq!(get_path(pane, 1, cx), path_1.clone());
@@ -6336,14 +6350,14 @@ async fn test_preview_tabs(cx: &mut TestAppContext) {
});
// Close permanent tab
pane.update(cx, |pane, cx| {
pane.update(cx, |pane, model, cx| {
let id = pane.items().next().unwrap().item_id();
pane.close_item_by_id(id, workspace::SaveIntent::Skip, cx)
})
.await
.unwrap();
pane.update(cx, |pane, cx| {
pane.update(cx, |pane, model, cx| {
assert_eq!(pane.items_len(), 1);
assert_eq!(get_path(pane, 0, cx), path_1.clone());
assert_eq!(
@@ -6356,13 +6370,13 @@ async fn test_preview_tabs(cx: &mut TestAppContext) {
});
// Split pane to the right
pane.update(cx, |pane, cx| {
pane.update(cx, |pane, model, cx| {
pane.split(workspace::SplitDirection::Right, cx);
});
let right_pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
let right_pane = workspace.update(cx, |workspace, model, _| workspace.active_pane().clone());
pane.update(cx, |pane, cx| {
pane.update(cx, |pane, model, cx| {
assert_eq!(pane.items_len(), 1);
assert_eq!(get_path(pane, 0, cx), path_1.clone());
assert_eq!(
@@ -6374,7 +6388,7 @@ async fn test_preview_tabs(cx: &mut TestAppContext) {
assert!(pane.can_navigate_forward());
});
right_pane.update(cx, |pane, cx| {
right_pane.update(cx, |pane, model, cx| {
assert_eq!(pane.items_len(), 1);
assert_eq!(get_path(pane, 0, cx), path_1.clone());
assert_eq!(pane.preview_item_id(), None);
@@ -6385,13 +6399,13 @@ async fn test_preview_tabs(cx: &mut TestAppContext) {
// Open item 2 as preview in right pane
workspace
.update(cx, |workspace, cx| {
.update(cx, |workspace, model, cx| {
workspace.open_path_preview(path_2.clone(), None, true, true, cx)
})
.await
.unwrap();
pane.update(cx, |pane, cx| {
pane.update(cx, |pane, model, cx| {
assert_eq!(pane.items_len(), 1);
assert_eq!(get_path(pane, 0, cx), path_1.clone());
assert_eq!(
@@ -6403,7 +6417,7 @@ async fn test_preview_tabs(cx: &mut TestAppContext) {
assert!(pane.can_navigate_forward());
});
right_pane.update(cx, |pane, cx| {
right_pane.update(cx, |pane, model, cx| {
assert_eq!(pane.items_len(), 2);
assert_eq!(get_path(pane, 0, cx), path_1.clone());
assert_eq!(get_path(pane, 1, cx), path_2.clone());
@@ -6417,19 +6431,19 @@ async fn test_preview_tabs(cx: &mut TestAppContext) {
});
// Focus left pane
workspace.update(cx, |workspace, cx| {
workspace.update(cx, |workspace, model, cx| {
workspace.activate_pane_in_direction(workspace::SplitDirection::Left, cx)
});
// Open item 2 as preview in left pane
workspace
.update(cx, |workspace, cx| {
.update(cx, |workspace, model, cx| {
workspace.open_path_preview(path_2.clone(), None, true, true, cx)
})
.await
.unwrap();
pane.update(cx, |pane, cx| {
pane.update(cx, |pane, model, cx| {
assert_eq!(pane.items_len(), 1);
assert_eq!(get_path(pane, 0, cx), path_2.clone());
assert_eq!(
@@ -6441,7 +6455,7 @@ async fn test_preview_tabs(cx: &mut TestAppContext) {
assert!(!pane.can_navigate_forward());
});
right_pane.update(cx, |pane, cx| {
right_pane.update(cx, |pane, model, cx| {
assert_eq!(pane.items_len(), 2);
assert_eq!(get_path(pane, 0, cx), path_1.clone());
assert_eq!(get_path(pane, 1, cx), path_2.clone());
@@ -6531,12 +6545,12 @@ async fn test_context_collaboration_with_reconnect(
// Host and guest make changes
context_a.update(cx_a, |context, cx| {
context.buffer().update(cx, |buffer, cx| {
context.buffer().update(cx, |buffer, model, cx| {
buffer.edit([(0..0, "Host change\n")], None, cx)
})
});
context_b.update(cx_b, |context, cx| {
context.buffer().update(cx, |buffer, cx| {
context.buffer().update(cx, |buffer, model, cx| {
buffer.edit([(0..0, "Guest change\n")], None, cx)
})
});
@@ -6554,12 +6568,12 @@ async fn test_context_collaboration_with_reconnect(
server.disconnect_client(client_a.peer_id().unwrap());
server.forbid_connections();
context_a.update(cx_a, |context, cx| {
context.buffer().update(cx, |buffer, cx| {
context.buffer().update(cx, |buffer, model, cx| {
buffer.edit([(0..0, "Host offline change\n")], None, cx)
})
});
context_b.update(cx_b, |context, cx| {
context.buffer().update(cx, |buffer, cx| {
context.buffer().update(cx, |buffer, model, cx| {
buffer.edit([(0..0, "Guest offline change\n")], None, cx)
})
});
@@ -6629,7 +6643,7 @@ async fn test_remote_git_branches(
executor.run_until_parked();
let branches_b = cx_b
.update(|cx| project_b.update(cx, |project, cx| project.branches(root_path.clone(), cx)))
.update(|cx| project_b.update(cx, |project, model, cx| project.branches(root_path.clone(), cx)))
.await
.unwrap();
@@ -6643,7 +6657,7 @@ async fn test_remote_git_branches(
assert_eq!(&branches_b, &branches);
cx_b.update(|cx| {
project_b.update(cx, |project, cx| {
project_b.update(cx, |project, model, cx| {
project.update_or_create_branch(root_path.clone(), new_branch.to_string(), cx)
})
})
@@ -6653,8 +6667,8 @@ async fn test_remote_git_branches(
executor.run_until_parked();
let host_branch = cx_a.update(|cx| {
project_a.update(cx, |project, cx| {
project.worktree_store().update(cx, |worktree_store, cx| {
project_a.update(cx, |project, model, cx| {
project.worktree_store().update(cx, |worktree_store, model, cx| {
worktree_store
.current_branch(root_path.clone(), cx)
.unwrap()
@@ -6666,7 +6680,7 @@ async fn test_remote_git_branches(
// Also try creating a new branch
cx_b.update(|cx| {
project_b.update(cx, |project, cx| {
project_b.update(cx, |project, model, cx| {
project.update_or_create_branch(root_path.clone(), "totally-new-branch".to_string(), cx)
})
})
@@ -6676,8 +6690,8 @@ async fn test_remote_git_branches(
executor.run_until_parked();
let host_branch = cx_a.update(|cx| {
project_a.update(cx, |project, cx| {
project.worktree_store().update(cx, |worktree_store, cx| {
project_a.update(cx, |project, model, cx| {
project.worktree_store().update(cx, |worktree_store, model, cx| {
worktree_store.current_branch(root_path, cx).unwrap()
})
})

View File

@@ -21,14 +21,14 @@ async fn test_notifications(
let notification_events_b = Arc::new(Mutex::new(Vec::new()));
client_a.notification_store().update(cx_a, |_, cx| {
let events = notification_events_a.clone();
cx.subscribe(&cx.handle(), move |_, _, event, _| {
cx.subscribe(model, move |_, _, event, _| {
events.lock().push(event.clone());
})
.detach()
});
client_b.notification_store().update(cx_b, |_, cx| {
let events = notification_events_b.clone();
cx.subscribe(&cx.handle(), move |_, _, event, _| {
cx.subscribe(model, move |_, _, event, _| {
events.lock().push(event.clone());
})
.detach()

View File

@@ -133,7 +133,7 @@ impl RandomizedTest for RandomChannelBufferTest {
) -> Result<(), TestError> {
match operation {
ChannelBufferOperation::JoinChannelNotes { channel_name } => {
let buffer = client.channel_store().update(cx, |store, cx| {
let buffer = client.channel_store().update(cx, |store, model, cx| {
let channel_id = store
.ordered_channels()
.find(|(_, c)| c.name == channel_name)
@@ -198,9 +198,9 @@ impl RandomizedTest for RandomChannelBufferTest {
edits
);
channel_buffer.update(cx, |buffer, cx| {
channel_buffer.update(cx, |buffer, model, cx| {
let buffer = buffer.buffer();
buffer.update(cx, |buffer, cx| {
buffer.update(cx, |buffer, model, cx| {
let snapshot = buffer.snapshot();
buffer.edit(
edits.into_iter().map(|(range, text)| {

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