Compare commits

...

264 Commits

Author SHA1 Message Date
Michael Sloan
6b4d9ffd96 Copy some build fixes from post merge branch to the pre merge state 2025-01-19 15:04:08 -07:00
Mikayla
9626de34f7 Fix one of the vim tests 2025-01-14 22:18:39 -08:00
Mikayla
764af2fd87 remove observe_new_window_models 2025-01-14 21:28:02 -08:00
Mikayla
a5fa81586e Fix all warnings 2025-01-14 21:13:47 -08:00
Mikayla
4d4ea61b12 Replace Window::notify with AppContext::notify
Fix several tests

Co-authored-by: Nathan <nathan@zed.dev>
2025-01-13 14:46:06 -08:00
Mikayla
9f56b9208a Rename BlockContext.context to BlockContext.app
Rename `App.windows_accessed` to `AppContext.window_update_stack`

co-authored-by: Nathan <nathan@zed.dev>
2025-01-13 14:12:47 -08:00
Mikayla
1cedbc04db Call Window::refresh() instead of AppContext::refresh_windows()
Rename entities_read to accessed_entities

co-authored-by: Nathan <nathan@zed.dev>
2025-01-13 13:56:11 -08:00
Mikayla
643f2e1e45 WIP 2025-01-13 11:12:40 -08:00
Mikayla
6f6e995603 Remove last TODOs 2025-01-13 01:49:25 -08:00
Mikayla
b43fcb7b09 Fix a bug where cached views would not rebind their window notifies 2025-01-13 00:17:05 -08:00
Mikayla
de6abee965 Implement model creation effect, remove new_view_observers 2025-01-13 00:16:32 -08:00
Mikayla
93ad5c38f0 Remove window_id from focus handle 2025-01-10 14:23:15 -08:00
Mikayla
16e9304af6 Fix some of the warnings and pull out spurious window arguments 2025-01-09 17:07:22 -08:00
Mikayla
71a6e7e677 Begin stripping out the window arguments
co-authored-by: max <max@zed.dev>
2025-01-09 14:03:09 -08:00
Mikayla
f92da24fae Resolve more todos, fix a test
co-authored-by: Joseph <joseph@zed.dev>
2025-01-09 08:29:29 -08:00
Mikayla
4b3ed1152f Delete several commented out methods
Remove the .view() method
Fix a failing test
2025-01-08 15:46:40 -08:00
Mikayla
fc43ec805d Restore minimal notifies 2025-01-08 15:03:40 -08:00
Mikayla
3738d9c34a Fix new view observations
Remove some comments
2025-01-08 14:08:37 -08:00
Mikayla
2776d382c3 Fix a bad naming issue 2025-01-07 14:02:54 -08:00
Mikayla
8e002b1e5a Implement window refreshing when models are read during a frame 2025-01-07 13:47:49 -08:00
Cole Miller
898bc8cb8c examples/image_loading runs 2025-01-07 16:12:05 -05:00
Cole Miller
991345ed4e examples/gradient runs 2025-01-07 16:10:29 -05:00
Cole Miller
55d2e26fa5 examples/gif_viewer runs 2025-01-07 16:07:41 -05:00
Cole Miller
2813cc077f examples/animation runs 2025-01-07 16:00:22 -05:00
Nathan Sobo
696530b1e5 It compiles. Holy shit. 2025-01-03 14:51:16 -07:00
Nathan Sobo
1c6b2b9dd0 everything compiles but zed crate 2025-01-03 14:27:57 -07:00
Nathan Sobo
f4d76abf39 vim compiles 2025-01-03 12:55:35 -07:00
Nathan Sobo
4034691669 theme_selector compiles 2025-01-03 12:34:32 -07:00
Nathan Sobo
904e1ef8f7 welcome compiles 2025-01-03 12:30:52 -07:00
Nathan Sobo
9f73fb6ddc project_symbols compiles 2025-01-03 12:21:46 -07:00
Nathan Sobo
339ed1bad2 tasks_ui compiles 2025-01-03 12:17:05 -07:00
Antonio Scandurra
59fce2924d WIP: midway through new_path_prompt.rs 2025-01-03 19:26:58 +01:00
Antonio Scandurra
8d42bc011d WIP: midway through file_finder_tests.rs 2025-01-03 17:24:22 +01:00
Antonio Scandurra
47f46fe786 extensions_ui compiles 2025-01-03 17:17:47 +01:00
Antonio Scandurra
56d9e60ad7 assistant2 compiles 2025-01-03 17:13:21 +01:00
Antonio Scandurra
b94279da00 assistant compiles 2025-01-03 16:44:33 +01:00
Antonio Scandurra
f971c9eb5f outline_panel compiles 2025-01-03 13:18:35 +01:00
Antonio Scandurra
be4ecd9e21 outline compiles 2025-01-03 13:07:42 +01:00
Antonio Scandurra
6a3834fc91 language_model_selector compiles 2025-01-03 12:56:47 +01:00
Antonio Scandurra
952d10239d command_palette compiles 2025-01-03 12:56:00 +01:00
Antonio Scandurra
8fdc5fa6ad collab_ui compiles 2025-01-03 12:53:52 +01:00
Antonio Scandurra
79ec03cd14 picker compiles, new raft of errors 2025-01-03 12:12:16 +01:00
Antonio Scandurra
df29eaf897 inline_completion_button compiles 2025-01-03 12:09:41 +01:00
Antonio Scandurra
e3bccd7f0d zeta compiles 2025-01-03 12:06:42 +01:00
Antonio Scandurra
4737c81c44 diagnostics compiles 2025-01-03 12:06:05 +01:00
Antonio Scandurra
603b398d32 git_ui compiles 2025-01-03 12:01:46 +01:00
Nathan Sobo
db387de6b3 terminal_view compiles 2025-01-02 14:40:02 -07:00
Nathan Sobo
a5fbde69ac WIP: language_tools compiles 2025-01-02 14:08:53 -07:00
Nathan Sobo
cc5c668744 settings_ui compiles 2025-01-02 14:03:22 -07:00
Nathan Sobo
953586f532 language_models compiles 2025-01-02 14:00:41 -07:00
Nathan Sobo
b1485631cd journal crate compiling 2025-01-02 11:32:31 -07:00
Nathan Sobo
55c46bbddf WIP: title_bar compiles 2025-01-02 10:57:46 -07:00
Nathan Sobo
9f409de5fe WIP 2025-01-02 10:56:04 -07:00
Nathan Sobo
426ae3339a WIP: go_to_line compiles 2025-01-02 10:54:27 -07:00
Nathan Sobo
fb05ab846b WIP: markdown_preview 2025-01-02 10:51:17 -07:00
Nathan Sobo
591352cd9e WIP: image_viewer compiles 2025-01-02 10:47:07 -07:00
Nathan Sobo
54945b922a WIP 2025-01-02 10:44:01 -07:00
Nathan Sobo
16fa413d2d WIP: project_panel compiling 2025-01-02 10:36:49 -07:00
Nathan Sobo
0de4094cc1 WIP: feedback compiles 2025-01-02 10:21:02 -07:00
Nathan Sobo
60ecf8345e WIP 2025-01-02 10:17:44 -07:00
Nathan Sobo
2a3a9d06ad WIP: get ui crate compiling again 2025-01-02 10:11:57 -07:00
Nathan Sobo
2aa3bd7099 WIP: semantic_index compiles 2025-01-02 10:09:18 -07:00
Nathan Sobo
bdc85d780e WIP: copilot compiles 2025-01-02 10:05:56 -07:00
Nathan Sobo
df4c51e9ea WIP: search crate compiles 2025-01-02 10:02:31 -07:00
Nathan Sobo
cb3bd8d442 WIP: editor crate is compiling 2025-01-02 09:38:39 -07:00
Nathan Sobo
d219bfc10f WIP 2025-01-02 09:34:22 -07:00
Nathan Sobo
d768bfdce1 WIP 2025-01-02 09:22:02 -07:00
Nathan Sobo
6c9d544341 WIPY 2025-01-02 08:37:19 -07:00
Nathan Sobo
ab1359b315 WIP 2025-01-02 08:27:28 -07:00
Nathan Sobo
c641bbf0b1 WIP 2025-01-02 07:59:59 -07:00
Nathan Sobo
6864b3e86d WIP 2025-01-02 07:57:09 -07:00
Nathan Sobo
25416ff303 workspace compiling 2025-01-02 07:47:40 -07:00
Nathan Sobo
5e3ec9d81c WIP 2025-01-02 07:45:13 -07:00
Nathan Sobo
5a3b9d8820 WIP 2025-01-02 07:14:07 -07:00
Nathan Sobo
ec72020cb1 WIP 2025-01-02 07:13:40 -07:00
Nathan Sobo
f2d41ca066 WIP 2025-01-02 07:12:09 -07:00
Nathan Sobo
7510afccb8 WIP 2025-01-02 07:11:43 -07:00
Nathan Sobo
ab8f8dde43 WIP 2025-01-02 07:11:15 -07:00
Nathan Sobo
41de86f87f WIP 2025-01-02 07:10:13 -07:00
Nathan Sobo
0ebd08871b WIP 2025-01-02 07:07:14 -07:00
Nathan Sobo
dbb74ca28d WIP 2025-01-02 07:06:30 -07:00
Nathan Sobo
8e987c273f WIP 2025-01-02 07:03:27 -07:00
Nathan Sobo
859da6b13a WIP 2025-01-02 06:27:50 -07:00
Nathan Sobo
54fe194f7b Fix markdown 2025-01-02 06:09:19 -07:00
Nathan Sobo
d07e1d2141 WIP 2025-01-02 05:51:20 -07:00
Nathan Sobo
c9a477350f WIP: 152 errors (but I dont trust it) 2025-01-02 05:34:26 -07:00
Nathan Sobo
c49c33bf61 WIP: Grinding through many remaining errors manually after running the script 2025-01-02 04:36:46 -07:00
Nathan Sobo
772489499c Format 2025-01-02 03:52:47 -07:00
Nathan Sobo
1d73e6646c Run the refactor script as a starting point 2025-01-02 03:45:20 -07:00
Nathan Sobo
efba1cb141 Get gpui examples compiling with new API 2025-01-02 00:48:14 -07:00
Nathan Sobo
19ac46a252 Revert example 2025-01-02 00:38:32 -07:00
Nathan Sobo
cc95b79913 Move impl block for ModelContext 2025-01-01 23:51:15 -07:00
Nathan Sobo
267df69b44 Fix privacy 2025-01-01 23:39:45 -07:00
Nathan Sobo
1de66771d6 Add a temporary view method to ModelContext 2025-01-01 23:12:02 -07:00
Nathan Sobo
d7b266ce62 Merge View into Model 2025-01-01 22:40:52 -07:00
Nathan Sobo
61aa1d8f08 Add Window::notify 2025-01-01 14:12:30 -07:00
Nathan Sobo
d4428c7c4b Restore examples so we can automate them 2025-01-01 13:10:48 -07:00
Nathan Sobo
021e8a17f6 Merge remote-tracking branch 'origin/main' into remove-window-context 2024-12-31 14:55:22 -07:00
Nathan Sobo
296fe144cb Remove WindowContext in GPUI (examples still broken) 2024-12-31 14:27:15 -07:00
Michael Sloan
6ef5d8f748 Improve fuzzy match performance and fix corner case that omits results (#22524)
* Removes `max_results` from the matcher interface as this is better
dealt with in consumers once all results are known. The current
implementation was quite inefficient as it was using binary search to
find insertion points and then doing an insert which copies the entire
suffix each time.

* There was a corner case where if the binary search found a match
candidate with the same score, it was dropped. Now fixed.

* Uses of `util::extend_sorted` when merging results from worker threads
also repeatedly uses binary search and insertion which copies the entire
suffix. A followup will remove that and its usage.

* Adds `util::truncate_to_bottom_n_sorted_by` which uses quickselect +
sort to efficiently get a sorted count limited result.

* Improves interface of Matcher::match_candidates by providing the match
positions to the build function. This allows for removal of the `Match`
trait. It also fixes a bug where the Match's own Ord wasn't being used,
which seems relevant to PathMatch for cases where scores are the same.

Release Notes:

- N/A
2024-12-31 20:56:23 +00:00
Kirill Bulatov
f912c545e7 Deduplicate edits from WorkspaceEdit LSP responses (#22512)
Closes https://github.com/zed-industries/zed/issues/21515

Release Notes:

- Fixed zls renames applying duplicate edits
2024-12-31 00:50:57 +00:00
Kirill Bulatov
ddc469ca3e Ensure zls is used for Zig as a primary language server (#22511)
Part of https://github.com/zed-industries/zed/issues/22415

I've noticed that I cannot work with any Zig projects, as there were no
"go to definition", formatting and inlay hints.

After debugging, I've discovered that `typos` was registered as a first
language server, becoming the "primary" one for Zig.
That one does not have any proper capabilities, hence all corresponding
LSP requests were no-op.

While this solution is not ideal (I wonder, how many other set-ups are
broken due to the same thing?), we'd better fix things for now this way
at least.

Release Notes:

- Fixed `zls` not working properly when `typos` extension is installed
2024-12-30 23:34:25 +00:00
Michael Sloan
e9bd4b2890 Have Zed cli output logs path to stderr (#22509)
When switching from just running the editor directly to using the CLI I
missed the logs in the terminal and started using `--foreground`. I
realized this was because there was a tiny amount of effort involved in
finding out where logs were being written to. Having the cli output it
to stderr helps make this more visible.

Seems like such a minor thing not listing in release notes.

Release Notes:

- N/A
2024-12-30 21:14:08 +00:00
Vitaly Slobodin
a119688a50 ruby: Document use_bundler configuration option (#22498)
Hello,

This PR documents the `use_bundler` configuration option that was added
in https://github.com/zed-extensions/ruby/releases/tag/v0.3.3

**What it does?**

This option allows users to run `solargraph` and `rubocop` within the
context of Bundler, which is the most commonly used scenario in the Ruby
ecosystem. Ruby LSP is not configured to run in the context of Bundler
because it is recommended not to add it to the project dependencies
(https://shopify.github.io/ruby-lsp/composed-bundle.html). However, if
this is the case, the Ruby LSP can still be configured to run in the
context of Bundler via the same configuration option.

This configuration option has the following default values:

- `rubocop` - `use_bundler` is `true`.
- `solargraph` - `use_bundler` is `true`.
- `ruby-lsp` - `use_bundler` is `false`.

Release Notes:

- N/A
2024-12-30 16:56:25 +00:00
Cole Miller
dcbff982ad Decide which panel should be active for a dock based on ordering panels (#22346)
This means that `workspace::ToggleRightDock` will open the assistant if
no right-dock panel has been manually activated, instead of the chat as
before. Also cleans up the `active_panel_index` logic a bit.

cc @nathansobo 

Release Notes:

- Make `workspace::ToggleRightDock` open the assistant panel if no
right-dock panel has yet been activated
2024-12-30 14:43:17 +00:00
Danilo Leal
ad51df7644 Improve multibuffer excerpt affordances (#22167)
Changes:
- [x] Increase expand affordance surface area
- [x] Ensure expand buttons have tooltips with keybindings
- [x] Make line numbers clickable to jump you to location (only in
multibuffers)
- [x] Hide the "Jump To File" element in not-focused excerpts

Before merging it:

- [x] Fix off-by-one header focus styles glitch

Improvements to consider for follow-up PRs:

1. Experiment with increasing the width of the clickable surface area
for line numbers
2. Don't show (or disable) the "expand excerpt" button when at the top
or bottom edge of the file
3. Once you jump to location, centralize the cursor scroll position

Release Notes:

- Improved multibuffer's "expand excerpt" affordance
- Fixed "jump to file/location" and "expand excerpt" keybinding display
- Made clicking on line numbers in multibuffers jump you to cursor
location in file

---------

Co-authored-by: Thorsten Ball <mrnugget@gmail.com>
Co-authored-by: Agus Zubiaga <hi@aguz.me>
Co-authored-by: Kirill Bulatov <kirill@zed.dev>
Co-authored-by: Agus Zubiaga <agus@zed.dev>
2024-12-30 12:23:11 +00:00
Piotr Osiewicz
3f33ca01a8 chore: Remove outline dependency from breadcrumbs (#22504)
This slashes our incremental dev times (touch editor) by 0.6s
(8.1->7.6s) due to unblocking terminal_view build sooner.

Closes #ISSUE

Release Notes:

- N/A
2024-12-30 12:08:26 +00:00
Jason Lee
5f4f3a9c87 chore: Remove duplicate lines in crates/zed/build.rs (#22500)
Release Notes:

- N/A

This may a mistake by merge in PR #21550


344284e013/crates/zed/build.rs (L8-L14)
2024-12-30 11:52:40 +00:00
Nathan Sobo
a7e5d5a298 Move more ViewContext methods to equivalent versions on ModelContext 2024-12-29 16:17:30 -07:00
Kirill Bulatov
344284e013 Invalidate tooltips when mouse leaves element's hitbox (#22488)
Closes https://github.com/zed-industries/zed/issues/21657

In case of the task rerun button tooltip from


f6dabadaf7/crates/terminal_view/src/terminal_view.rs (L1051-L1070)

, the actual button element is not styled as invisible, only its parent.
Zed won't render such element since it's parent is hidden, but will
consider it "visible" all the time its `paint` is called, spawning a
task with the delay, that will create the tooltip:


f6dabadaf7/crates/gpui/src/elements/div.rs (L1949-L1959)

When the parent is hidden, the child won't be painted anymore, and no
mouse listeners will be able to detect this fact and hide the tooltip.

Hence, check such cases separately, during `prepaint`, and invalidate
the tooltips that are not valid anymore.
We cannot use `hitbox.is_hovered(cx)` as it's not really hovered during
prepaint, so a mouse position check is used instead.

Release Notes:

- Fixed tooltips getting stuck
2024-12-29 19:37:04 +00:00
Nathan Sobo
838ebe90cd Add some formerly ViewContext methods to ModelContext (many now take a window) 2024-12-29 10:31:43 -07:00
Nathan Sobo
dcd2d85127 Add ModelContext::subscribe_in 2024-12-29 10:21:43 -07:00
Nathan Sobo
b05f723e95 Move remaining WindowContext methods to Window 2024-12-29 10:10:41 -07:00
Nathan Sobo
30ffadfe32 Move more Window methods 2024-12-29 09:43:58 -07:00
Nathan Sobo
04ffcab87d Move some more WindowContext methods to Window 2024-12-29 09:25:23 -07:00
Nathan Sobo
5a3233ebe4 Add some Window equivalent methods to methods on WindowContext 2024-12-29 09:03:38 -07:00
Jayden Seric
f6dabadaf7 Syntax highlight the JavaScript keyword using (#22479)
The ECMAScript Explicit Resource Management proposal (stage 3) that
specifies the keyword `using`:

https://github.com/tc39/proposal-explicit-resource-management

This has already been done for the TypeScript and TSX languages:

- https://github.com/zed-industries/zed/issues/14762
- https://github.com/zed-industries/zed/pull/14772

Release Notes:

- Syntax highlight the JavaScript keyword `using`
([#22479](https://github.com/zed-industries/zed/pull/22479); thanks
[jaydenseric](https://github.com/jaydenseric)).
2024-12-29 08:17:51 +00:00
Nathan Sobo
a037f71432 Compiling: Remove ViewContext completely 2024-12-29 00:36:17 -07:00
Nathan Sobo
d625a3e6ba Progress toward removing ViewContext
3 compiler errors and some todo!s
2024-12-28 23:28:36 -07:00
Michael Sloan
ca6825066f Fix SHA for zed-patches branch of livekit-rust-sdks (#22478)
Also renamed the repo to `livekit-rustk-sdks` on GitHub for clarity. The
old name will still work for old references.

See [discussion
here](https://github.com/zed-industries/zed/pull/21292#issuecomment-2529573128).
The only changes in the target repo are immaterial:

```
$ git diff 799f10133d93ba2a88642cd480d01ec4da53408c 060964da10574cd9bf06463a53bf6e0769c5c45e
diff --git a/livekit-protocol/src/livekit.rs b/livekit-protocol/src/livekit.rs
index 43f5496..19181c2 100644
--- a/livekit-protocol/src/livekit.rs
+++ b/livekit-protocol/src/livekit.rs
@@ -3837,7 +3837,7 @@ pub struct SendDataRequest {
     #[prost(string, optional, tag="5")]
     pub topic: ::core::option::Option<::prost::alloc::string::String>,
 }
-
+///
 #[allow(clippy::derive_partial_eq_without_eq)]
 #[derive(Clone, PartialEq, ::prost::Message)]
 pub struct SendDataResponse {
diff --git a/livekit-runtime/Cargo.toml b/livekit-runtime/Cargo.toml
index 4d83cdf..7051f30 100644
--- a/livekit-runtime/Cargo.toml
+++ b/livekit-runtime/Cargo.toml
@@ -7,6 +7,7 @@ edition = "2021"
 repository = "https://github.com/livekit/rust-sdks"

 [features]
+tokio = ["dep:tokio", "dep:tokio-stream"]
 async = ["dep:async-std", "dep:futures", "dep:async-io"]
 dispatcher = ["dep:futures", "dep:async-io", "dep:async-std", "dep:async-task"]
```

Release Notes:

- N/A
2024-12-29 00:18:58 +00:00
Michael Sloan
a15360bcc8 Dequalify WindowContext and ViewContext references (#22477)
Release Notes:

- N/A
2024-12-28 23:09:55 +00:00
Michael Sloan
016b5d60e1 Cleanups preparing for WindowContext refactor (#22475)
* Remove unnecessary WindowContext and ViewContext '_ lifetimes

* Removed some cases where WindowContext has a different name than `cx`.

Release Notes:

- N/A
2024-12-28 21:36:14 +00:00
Michael Sloan
9815358bdd Dequalify WindowContext and AsyncWindowContext identifiers (#22444)
This makes a WIP automated refactoring a bit easier to write

Release Notes:

- N/A
2024-12-28 21:21:32 +00:00
Kirill Bulatov
ac60dcd67a Fix Project strong reference leaks (#22470)
Closes https://github.com/zed-industries/zed/issues/21906

* After https://github.com/zed-industries/zed/pull/21238,
`TerminalPanel` and `Project` strong references were moved into
`Pane`-related closures, creating a cycle, that did not allow
registering project release and shutting down corresponding language
servers

* After https://github.com/zed-industries/zed/pull/22329, a special
`Editor` was created with a strong reference to the `Project` which
seemed to do nothing bad in general, but when a working Zed was running
a Zed Dev build, had the same issue with preventing language servers
from shutting down.

The latter is very odd, and seems quite dangerous, as any arbitrary
`Editor` with `Project` in it may do the same, yet it seems that we did
not store them before the way git panel does.

I have tried creating a test, yet seems that we need to initialize a lot
of Zed for it which I failed — all my attempts resulted in a single
language server being present in the `Project`'s statuses.

Release Notes:

- Fixed language servers not being released between project reopens
2024-12-28 17:29:15 +00:00
Kirill Bulatov
3e3a5f04a2 Small fixes after terminal split (#22468)
* removes cache access workarounds as
https://github.com/zed-industries/zed/pull/21165 is supposed to fix this
* use `WeakModel<Project>` inside `Pane` just in case 

Release Notes:

- N/A
2024-12-28 17:29:13 +00:00
Cole Miller
7903f4ea58 Fix the install-linux fix (#22466)
Follow-up fix to #22465, forgot my shell fundamentals!

Release Notes:

- N/A
2024-12-28 16:50:29 +00:00
Cole Miller
a3dec643a1 Fix script/install-linux in non-CI settings (#22465)
Closes #22456 

Release Notes:

- N/A
2024-12-28 15:59:54 +00:00
Dzmitry Malyshau
02cc0b9afa Update Blade with acquire fixes, multisampling (#22409)
Fixes #22406

Picks up:
- https://github.com/kvark/blade/pull/234 and
https://github.com/kvark/blade/pull/237 (`OutOfDate` workaround)
- https://github.com/kvark/blade/pull/229 ("metal-rs" to "objc2"
migration)

Release Notes:

- Improved handling of resizing and multiple monitors in Linux/Windows
2024-12-27 21:29:03 +00:00
Marcel Kersten
787466383e copilot: Update Copilot Chat to o1 GA model version (#22376)
Closes #22375

Release Notes:

- Fixed model version of o1 in GitHub Copilot Chat

---------

Co-authored-by: Peter Tripp <peter@zed.dev>
2024-12-27 18:09:50 +00:00
Danilo Leal
34f8bb246a assistant2: Scroll to the bottom when you submit a new message (#22451)
Up until now, in the assistant 2, if you scrolled up either while a
message was being generated or after it's been generated, then submitted
a new message, you'd keep your scroll position. Now, with this PR, if
your scroll position is somewhere else that's not the bottom, as you
submit a new message, you'll be back at the bottom.


https://github.com/user-attachments/assets/8b111c10-27ff-4d7b-9b10-4c31093c6457

Release Notes:

- N/A

Co-authored-by: Agus Zubiaga <hi@aguz.me>
2024-12-27 16:54:35 +00:00
Danilo Leal
56b425fced assistant2: Show the popover keybinding when context is empty (#22452)
This PR makes it so we always show the "Add Context {keybinding}" text
when there's no context pills attached. Also, while we haven't fully
implemented the mention system (triggered by typing `@`), we removed the
instruction on the message editor placeholder. Once that's fully in
place, we should return with it!

<img width="800" alt="Screenshot 2024-12-27 at 1 35 56 PM"
src="https://github.com/user-attachments/assets/201cf784-e7ac-420a-adf2-51b6e075c2b6"
/>

Release Notes:

- N/A

---------

Co-authored-by: Agus Zubiaga <hi@aguz.me>
2024-12-27 16:54:31 +00:00
Kirill Bulatov
ed61abb8b8 Resolve completion items once exactly (#22448)
Closes https://github.com/zed-industries/zed/issues/19214
Closes https://github.com/zed-industries/zed/pull/22443

Adds `resolved` property into Zed completion item data, to ensure we
resolve every completion item exactly once.

There are 2 paths for singplayer Zed, and corresponding 2 analogues for
multi player code, where resolve may happen:
* completions menu display & selection, that ends up using
`resolve_completions` in `lsp_store.rs`
* applying a completion menu entry, that ends up using
`apply_additional_edits_for_completion` in `lsp_store.rs`

Now, all local counterparts check `enabled` field before resolving and
set it to true afterwards, and reuse the same `resolve_completion_local`
method for resolving the items.

A logic for re-generating docs and item labels was moved out from the
`resolve_completion_local` method into a separate method, as
`apply_additional_edits_for_completion` does not need that, but needs
the rest of the logic for resolving.
During the extraction, I've noted that multiplayer clients are not
getting the item labels, regenerated after the resolve — as the Zed
protocol-based flow is not the exact copy of the local resolving.
To improve that, `resolve_completion_remote` needs to be adjusted, but
this change is omitted to avoid bloating the PR.

Release Notes:

- Fixed autocomplete inserting multiple imports
2024-12-27 16:43:01 +00:00
renovate[bot]
d71180abc2 Update Rust crate async-broadcast to v0.7.2 (#22424)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [async-broadcast](https://redirect.github.com/smol-rs/async-broadcast)
| dependencies | patch | `0.7.1` -> `0.7.2` |

---

### Release Notes

<details>
<summary>smol-rs/async-broadcast (async-broadcast)</summary>

###
[`v0.7.2`](https://redirect.github.com/smol-rs/async-broadcast/blob/HEAD/CHANGELOG.md#Version-072)

[Compare
Source](https://redirect.github.com/smol-rs/async-broadcast/compare/0.7.1...0.7.2)

- Add `Sender::broadcast_blocking` and `Receiver::recv_blocking`.
[#&#8203;41](https://redirect.github.com/smol-rs/async-broadcast/issues/41)
- Use `Mutex` instead of `RwLock` for securing the inner data.
[#&#8203;42](https://redirect.github.com/smol-rs/async-broadcast/issues/42)
-   Many non-user-facing internal improvements and fixes.

</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:eyJjcmVhdGVkSW5WZXIiOiIzOS44MC4wIiwidXBkYXRlZEluVmVyIjoiMzkuODAuMCIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOltdfQ==-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-27 00:11:37 +00:00
renovate[bot]
af2ac1a4a0 Update Rust crate env_logger to v0.11.6 (#22425)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [env_logger](https://redirect.github.com/rust-cli/env_logger) |
workspace.dependencies | patch | `0.11.5` -> `0.11.6` |

---

### Release Notes

<details>
<summary>rust-cli/env_logger (env_logger)</summary>

###
[`v0.11.6`](https://redirect.github.com/rust-cli/env_logger/blob/HEAD/CHANGELOG.md#0116---2024-12-20)

[Compare
Source](https://redirect.github.com/rust-cli/env_logger/compare/v0.11.5...v0.11.6)

##### Features

-   Opt-in file and line rendering

</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:eyJjcmVhdGVkSW5WZXIiOiIzOS44MC4wIiwidXBkYXRlZEluVmVyIjoiMzkuODAuMCIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOltdfQ==-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-26 23:55:28 +00:00
renovate[bot]
c742cda8e8 Update Rust crate libc to v0.2.169 (#22427)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [libc](https://redirect.github.com/rust-lang/libc) |
workspace.dependencies | patch | `0.2.168` -> `0.2.169` |

---

### Release Notes

<details>
<summary>rust-lang/libc (libc)</summary>

###
[`v0.2.169`](https://redirect.github.com/rust-lang/libc/releases/tag/0.2.169)

[Compare
Source](https://redirect.github.com/rust-lang/libc/compare/0.2.168...0.2.169)

##### Added

- FreeBSD: add more socket TCP stack constants
[#&#8203;4193](https://redirect.github.com/rust-lang/libc/pull/4193)
- Fuchsia: add a `sockaddr_vm` definition
[#&#8203;4194](https://redirect.github.com/rust-lang/libc/pull/4194)

##### Fixed

**Breaking**:
[rust-lang/rust#132975](https://redirect.github.com/rust-lang/rust/pull/132975)
corrected the signedness of `core::ffi::c_char` on various Tier 2 and
Tier 3 platforms (mostly Arm and RISC-V) to match Clang. This release
contains the corresponding changes to `libc`, including the following
specific pull requests:

- ESP-IDF: Replace arch-conditional `c_char` with a reexport
[#&#8203;4195](https://redirect.github.com/rust-lang/libc/pull/4195)
- Fix `c_char` on various targets
[#&#8203;4199](https://redirect.github.com/rust-lang/libc/pull/4199)
- Mirror `c_char` configuration from `rust-lang/rust`
[#&#8203;4198](https://redirect.github.com/rust-lang/libc/pull/4198)

##### Cleanup

- Do not re-export `c_void` in target-specific code
[#&#8203;4200](https://redirect.github.com/rust-lang/libc/pull/4200)

</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:eyJjcmVhdGVkSW5WZXIiOiIzOS44MC4wIiwidXBkYXRlZEluVmVyIjoiMzkuODAuMCIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOltdfQ==-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-26 23:55:11 +00:00
sofaspawn
52614482bf docs: Add "bounded" mode to soft_wrap documentation (#22430)
Closes #22272

Release Notes:

- Added documentation for the new "bounded" mode in the soft_wrap
configuration

Co-authored-by: Kirill Bulatov <mail4score@gmail.com>
2024-12-26 19:40:27 +00:00
Nate Butler
8b0a0dfb11 gpui: Update Shadow Example (#22434)
This PR adds a more comprehensive shadow example to gpui:

![CleanShot 2024-12-26 at 13 54
05@2x](https://github.com/user-attachments/assets/452fb8a1-d294-4b56-b0e0-f4e4ca6c29d4)

This is prep to work on the following issues:

- Shadows with a blur of 0 do not render. Expected: A shadow with sharp
edges (#22433)
- Spread does not correctly conform to the shape the shadow is cast from

Release Notes:

- N/A
2024-12-26 19:23:10 +00:00
renovate[bot]
f5935d0cec Pin actions/checkout action to 11bd719 (#22422)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [actions/checkout](https://redirect.github.com/actions/checkout) |
action | pinDigest | -> `11bd719` |

---

### 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:eyJjcmVhdGVkSW5WZXIiOiIzOS44MC4wIiwidXBkYXRlZEluVmVyIjoiMzkuODAuMCIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOltdfQ==-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-26 13:30:18 +00:00
renovate[bot]
d32fe0216f Update Rust crate anyhow to v1.0.95 (#22423)
This PR contains the following updates:

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

---

### Release Notes

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

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

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

- Add
[`Error::from_boxed`](https://docs.rs/anyhow/1/anyhow/struct.Error.html#method.from_boxed)
([#&#8203;401](https://redirect.github.com/dtolnay/anyhow/issues/401),
[#&#8203;402](https://redirect.github.com/dtolnay/anyhow/issues/402))

</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:eyJjcmVhdGVkSW5WZXIiOiIzOS44MC4wIiwidXBkYXRlZEluVmVyIjoiMzkuODAuMCIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOltdfQ==-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-26 13:29:27 +00:00
Danilo Leal
2957263b23 pane: Add keybinding to "Close Left" and "Close Right" actions (#22402)
Was super missing these as I use these actions all the time 😄 

<img width="680" alt="Screenshot 2024-12-24 at 3 18 04 PM"
src="https://github.com/user-attachments/assets/5dbf9ebd-afc5-438c-aad7-c17ca59d9f9b"
/>

Release Notes:

- Add keybinding to "Close Left" and "Close Right" actions
2024-12-26 13:22:08 +00:00
张小白
95911aaa14 windows: Fix crashing when minimizing a window on Windows 11 (#22414)
Closes #22366

This PR fixes the issue of crashing when minimizing a window on Windows
11. And this PR supersedes #22366.

The main change in this PR is to stop rendering the window when it is
minimized. Additionally, I’ve made some modifications to the code in
#21756 to improve clarity (I think...).

cc @mgsloan 

Release Notes:

- N/A
2024-12-25 09:52:19 +00:00
Piotr Osiewicz
1a9f0a647a chore: Remove explicit usages of once_cell in favor of std (#22407)
Closes #ISSUE

Release Notes:

- N/A
2024-12-25 00:33:26 +00:00
Agus Zubiaga
45c714110e Sticky multibuffer headers (#22391)
https://github.com/user-attachments/assets/92cc5ff7-d8be-4e4b-ac6e-68eb310fffce

Release Notes:

- Multibuffer headers will now stick to the top of the viewport as you
scroll
- Added support for expanding diagnostic excerpts

---------

Co-authored-by: Michael <michael@zed.dev>
2024-12-24 17:55:25 +00:00
Marshall Bowers
4c84600630 assistant2: Derive the Context icon at render time instead of storing (#22397)
This PR is a follow up to #22385 that makes it so we treat the icon as
derived state.

Release Notes:

- N/A
2024-12-24 16:13:35 +00:00
Danilo Leal
564936e1fe assistant2: Add stray visual adjusments (#22386)
This PR adds just some tiny visual clean ups to the assistant2 panel.
Nothing major, honestly.

<img width="800" alt="Screenshot 2024-12-24 at 12 19 46 AM"
src="https://github.com/user-attachments/assets/da22aa7f-8a42-4ff0-9e4c-5e8b60b28802"
/>

Release Notes:

- N/A
2024-12-24 04:26:13 +00:00
Danilo Leal
3d4e0780c4 assistant2: Add icons to the context pill (#22385)
This PR adds an icon field to the `ContextKind` enum, which means that
icons will now display on context pills, both on the message editor and
on the active thread.

<img width="800" alt="Screenshot 2024-12-24 at 12 23 17 AM"
src="https://github.com/user-attachments/assets/f00e540b-30fe-49ac-b3df-7c7a5dc86d65"
/>

Release Notes:

- N/A
2024-12-24 04:26:09 +00:00
Danilo Leal
44a46e3713 assistant2: Don't render the context space if there's none (#22383)
Note the extra bottom space on the before version. That was because,
previously, the container that holds the context pills in an active
thread was being rendered even if there was no attached context.

| Before | After |
|--------|--------|
| <img width="1577" alt="Screenshot 2024-12-23 at 8 42 00 PM"
src="https://github.com/user-attachments/assets/b74bdc67-0a08-4d59-b1ec-43a00a59a373"
/> | <img width="1577" alt="Screenshot 2024-12-23 at 8 39 00 PM"
src="https://github.com/user-attachments/assets/1cbc340d-19df-4cce-8c0b-b671703a0ff5"
/> |

Release Notes:

- N/A
2024-12-24 04:26:04 +00:00
Danilo Leal
8a724acc4d Add new CSS logo (#22382)
CSS has a brand new logo :) For context, see:

- https://github.com/CSS-Next/logo.css
- https://github.com/CSS-Next/logo.css/issues/54
- https://github.com/CSS-Next/logo.css/pull/53
- https://github.com/CSS-Next/logo.css/pull/55

<img width="1412" alt="Screenshot 2024-12-23 at 6 17 07 PM"
src="https://github.com/user-attachments/assets/7352a921-06fa-4e50-8aad-44c974c57ed4"
/>

Release Notes:

- Add new CSS logo
2024-12-23 21:42:39 +00:00
Danilo Leal
7595d36943 assistant2: Refine buffer inline assistant styles (#22377)
A lot of spacing tweaks. But, most notably, using the buffer font for
the inline assistant.

<img width="800" alt="Screenshot 2024-12-23 at 12 46 16 PM"
src="https://github.com/user-attachments/assets/ee2908a7-7515-4244-83fc-791172b29364"
/>

Release Notes:

- N/A
2024-12-23 21:29:39 +00:00
Danilo Leal
d25c2ff866 assistant2: Improve markdown rendering design (#22321)
This PR visually balances code blocks within thread messages a bit more.

<img width="800" alt="Screenshot 2024-12-23 at 11 26 14 AM"
src="https://github.com/user-attachments/assets/6d459aac-5d94-4021-8289-0125bc82e77c"
/>

Release Notes:

- N/A
2024-12-23 18:18:20 +00:00
plyght
7f33f31ebe docs: Use higher-quality image in the header (#22195)
Improved image quality of docs site logo at top.

Prior Change: 
<img width="159" alt="SCR-20241218-konx"
src="https://github.com/user-attachments/assets/18c936e8-958d-4970-98e2-8dd1ad3a0a89"
/>
Post Change: 
<img width="177" alt="SCR-20241218-kojm"
src="https://github.com/user-attachments/assets/0b874ac2-7193-44ec-9634-b91a71c53864"
/>

Release Notes:

- N/A

---------

Co-authored-by: Danilo Leal <daniloleal09@gmail.com>
2024-12-23 13:39:28 +00:00
Kirill Bulatov
5df409971c Use more elaborate messages for permission denied errors on startup (#22368)
Also show all paths with issues, grouping them by the issue kind.

Inspired by https://github.com/zed-industries/zed/issues/22365 ,
https://github.com/zed-industries/zed/issues/20162,
https://github.com/zed-industries/zed/issues/13426, ...

Attempts to provide example `chown` and `chmod` commands on this error,
instead of encouraging to submit another issue.
For now, uses a hardcoded command examples, but we might provide a
better workflow later, e.g. try to automatically run those commands.

I seem to be unable to select the size of the modal, but for something
that's supposed to appear only once, it seems to be ok.


![regular](https://github.com/user-attachments/assets/484feb95-b9c5-445c-87f8-cd1db9337529)


![also_regular](https://github.com/user-attachments/assets/680ea95d-91fe-4b16-b3bc-e241ecd841d4)


![many_kinds](https://github.com/user-attachments/assets/7103161f-754d-413a-94df-62a44e353674)


![exaggerated](https://github.com/user-attachments/assets/b6e70be7-9ab4-4861-afae-5285a554fdba)



Release Notes:

- N/A
2024-12-23 12:34:29 +00:00
Cole Miller
8ccc7b81c8 Restore crates/zed dependency on libc (#22354)
This somehow got lost during or after merge of #22202.

Release Notes:

- N/A
2024-12-22 15:53:16 +00:00
teapo
a0e4464a33 Update Luau docs (#22351)
The domain has changed and the old StyLua arguments work again.

Release Notes:

- N/A
2024-12-22 13:54:50 +00:00
tims
204af9cac5 linux: Fix "Failed to start language server" errors when starting Zed from .desktop file (#22335)
Closes #21406

Context:

A few weeks ago on Linux, we resolved an
[issue](https://github.com/zed-industries/zed/issues/20070) where users
could not open more than one file from the file explorer. This was fixed
by replacing `zed-editor` (zed binary in the code) with `zed` (cli
binary in the code) in the `.desktop` file. The reason for this change
was that using the cli to open files is more convenient - it determines
weather to spawn a new Zed instance or use an existing one, if we use
main binary instead it would throw error `Zed is already running`.

You can read the complete PR here: [linux: Fix file not opening from
file explorer](https://github.com/zed-industries/zed/pull/21137).

While this fix resolved the original issue, it introduced a new one.

Problem:

When the cli binary is used, it assumes it is always being invoked from
a terminal and relies on `std::env::vars()` to retrieve the environment
variables needed to spawn Zed. These env vars are then passed to the
worktree, and eventually, languages use the `PATH` from this env to find
binaries. This leads to the "Failed to start language server" error when
the `.desktop` entry is used on Linux.

Solution:

When the `zed-editor` binary is used, it uses some clever Unix-specific
logic to retrieve the default shell (`load_shell_from_passwd`) and then
fetch the env vars from that shell (`load_login_shell_environment`).
This same logic should be used in the cli binary when it is invoked via
a `.desktop` entry rather than from a terminal.

Approach:

I moved these two functions mentioned above to a utils file and reused
them in cli binary to fetch env vars only on Linux when it is not run
from a terminal. This provides missing paths, and fix the issue.

It is also possible to handle this in the `zed-editor` binary by
modifying the logic in `handle_cli_connection`, where `CliRequest::Open`
is processed. There we can discard incoming env, and use our logic. But
discarding incoming envs felt weird, and I thought it's better to handle
this at source.

Release Notes:

- Fixed `Failed to start language server` errors when starting from
dekstop entry on Linux
2024-12-22 10:05:52 +00:00
Cole Miller
a2022d7da3 Improve Linux panic reporting (#22202)
- [x] Upload separate debug symbols for Linux binaries to DigitalOcean
- [x] Send raw offsets with panic report JSON on Linux
- [x] Update `symbolicate` script to handle Linux crashes
- [x] Demangle backtraces 🎉 
- [x] Check that it works
- [x] Improve deduplication (?)
 
Release Notes:

- N/A

---------

Co-authored-by: Conrad <conrad@zed.dev>
2024-12-22 08:20:17 +00:00
Kirill Bulatov
b51a28b75f Return back Windows menu (#22339)
Follow-up of https://github.com/zed-industries/zed/pull/21873

Release Notes:

- N/A
2024-12-21 22:03:17 +00:00
Michael Sloan
dbb76100e5 Improve layout of completions doc popover (#22337)
* Now more often displayed to the right. Resizes docs width if more
space than min width is available.

* If constrained in horizontal space and so displayed above/below resize
docs height to fit.

* Makes space for scrollbar and gap.

Layout is imperfect for viewport sizes smaller than the context menu,
left TODOs in the code for handling this. Wanted to get this change out
for feedback first.

Release Notes:

- N/A
2024-12-21 21:22:15 +00:00
Cole Miller
6b92e0b5da In terminal context, open new terminals with cmd-n instead of new files (#22253)
Release Notes:

- In terminal context, `cmd-n` and `ctrl-n` now open a new terminal
instead of a new file
2024-12-21 19:03:58 +00:00
Kirill Bulatov
2930211af9 Allow disabling editor scrollbars programmatically (#22333)
Disable them in the diff editors

Closes https://github.com/zed-industries/zed/issues/22271

Release Notes:

- N/A
2024-12-21 16:58:26 +00:00
Marshall Bowers
1449377278 assistant2: Fix panics when confirming nonexistent entries in the context picker (#22332)
This PR fixes a panic that could occur when confirming when the context
picker had no matching entries.

Release Notes:

- N/A
2024-12-21 16:11:29 +00:00
Kirill Bulatov
831930aad0 Make git panel entries clickable (#22329)
Makes a first pass over git panel UI, making it more interactive.


![image](https://github.com/user-attachments/assets/4d43b086-4ef2-4913-9783-2b9467d99c9a)


* every item can be selected, the selection is shown in the panel
* every item can be clicked, which changes the selection and
creates/focuses the editor with a project changes multi buffer
* the editor is scrolled so that the clicked item is in the center
* it's possible to nagivate up and down the panel, selecting
next/previous items in it, triggering the editor scroll

Known issues:

* entries are updated oddly sometimes (should become better after
DiffMap improvements land?)
* only unstaged diffs are shown currently (entry status storage should
help with this)
* no deleted files are displayed (the underlying work is done by others
now)
* added files have no diff hunks shown (DiffMap will have it?)
* performance story has not improved (again, DiffMap and status storage
should help with this)

Release Notes:

- N/A
2024-12-21 14:20:08 +00:00
Piotr Osiewicz
bc32b4d016 zeta: Compute diff on background thread (#22328)
@iamnbutler noticed slowness in assistant panel which I've pinned down
to the fact that we're calculating buffer diff on foreground thread.
This PR moves this computation into the background; I don't know much
about Zeta but it seems fine to do, as the call-site is asynchronous
anyways.

Closes #ISSUE

Release Notes:

- N/A
2024-12-21 13:44:18 +00:00
Piotr Osiewicz
fac5118f10 python: Fix decorated test detection (#22327)
Follow-up to #22325, where I missed a couple points in the review that
were actually quite relevant.

Closes #ISSUE

Release Notes:

- N/A
2024-12-21 13:11:52 +00:00
Thomas
7184b15f48 Add decorated function to pytest runnables (#22325)
Fixes: #22324

Closes #ISSUE

Release Notes:

- Fixed pytest decoracted function discovery
2024-12-21 11:31:35 +00:00
Danilo Leal
e82af55d64 assistant2: Adjust role info colors on the thread header (#22318)
<img width="800" alt="Screenshot 2024-12-20 at 9 01 29 PM"
src="https://github.com/user-attachments/assets/5e9e9bf2-c84e-4215-b658-9b668dd151b2"
/>

Release Notes:

- N/A
2024-12-21 00:19:50 +00:00
Danilo Leal
dcd21e6f23 assistant2: Use SwitchWithLabel for the tool toggle (#22317)
<img width="800" alt="Screenshot 2024-12-20 at 8 52 27 PM"
src="https://github.com/user-attachments/assets/4b4f62a3-eb23-489b-a459-2117baaa37b4"
/>

Release Notes:

- N/A
2024-12-21 00:15:47 +00:00
Danilo Leal
9efa13116d assistant2: Use a label to render the thread menu item (#22316)
The thread menu items were rendering with a larger font that the file &
directory counterparts. Now, they're consistent!

| Before | After | Reference |
|--------|--------|--------|
| <img width="800" alt="Screenshot 2024-12-20 at 8 39 47 PM"
src="https://github.com/user-attachments/assets/d196adcb-aea6-4399-8fb1-f5b771e76c1f"
/> | <img width="800" alt="Screenshot 2024-12-20 at 8 35 43 PM"
src="https://github.com/user-attachments/assets/a1e22c13-fd7e-4665-b09b-f3b583e7ce24"
/> | <img width="800" alt="Screenshot 2024-12-20 at 8 40 38 PM"
src="https://github.com/user-attachments/assets/d509ac6f-1251-4240-a369-906faebed8c0"
/> |


Release Notes:

- N/A
2024-12-20 23:53:51 +00:00
Danilo Leal
6dbc12f6af Add the SwitchWithLabel component (#22314)
<img width="800" alt="Screenshot 2024-12-20 at 8 31 14 PM"
src="https://github.com/user-attachments/assets/1d7bd10a-0db3-41e4-9f59-977cc2ab137c"
/>

Release Notes:

- N/A
2024-12-20 23:44:47 +00:00
Marshall Bowers
a8afc63a91 gpui: Remove use of use gpui::* in examples (#22311)
This PR removes the use of `use gpui::*` in the GPUI examples, as this
is not how consumers should be importing GPUI.

Release Notes:

- N/A
2024-12-20 23:09:30 +00:00
Marshall Bowers
7c6feeb3a8 Remove usage of use gpui::* (#22310)
This PR removes usages of `use gpui::*;` in the codebase (excluding
examples).

We should only use glob imports for `gpui::prelude`, and then import
everything else individually.

Release Notes:

- N/A
2024-12-20 22:52:11 +00:00
Danilo Leal
fadf9ff4f4 Make breadcrumb content scrollable (#22309)
Part of https://github.com/zed-industries/zed/issues/5363


https://github.com/user-attachments/assets/66c90cb7-1b36-4436-ad1e-344fbcd4befc

Release Notes:

- N/A
2024-12-20 22:50:41 +00:00
Michael Sloan
9b2bc458e3 Expand diagnostic excerpts using heuristics on syntactic information from TreeSitter (#21942)
This is quite experimental and untested in languages other than Rust.
It's written to attempt to do something sensible in many languages. Due
to its experimental nature, just releasing to staff, and so not
including it in release notes. Future release note might be "Improved
diagnostic excerpts by using syntactic info to determine the context
lines to show."

Release Notes:

- N/A
2024-12-20 22:42:18 +00:00
tims
ca9cee85e1 linux: Fix non-maximized Zed windows growing larger across sessions (#22301)
Closes #17870

Context:

On Linux, when creating a new window, bounds are either pulled from
existing workspace data (serialized in an SQLite DB) or fall back to
some default constants if no data exists.

These bounds include the full dimensions of the window (width and
height), which already account for insets. However, properties like
`inset` (Wayland) or `last_insets` (X11) exist only at the platform
level and are not part of the window bounds themselves.

During rendering, we call `set_client_inset`, which updates the inset
values and also adjusts the window bounds, increasing their dimensions.
In Zed's case, the inset is 10px, which adds 20px to both the width and
height (10px from each side).

Problem:

When quitting, the full window bounds (which already account for inset)
are saved to the DB. On reopening, these saved bounds are used to create
the window. `set_client_inset` runs again and inflates the dimensions
even more.

Solution:

Store window bounds *without* the inset-inflated dimensions. On the next
session, `set_client_inset` will take care of applying the inset,
resulting window dimensions matching the previous session. This fix is
in the PR.

Alternative Solution:

Another option is to save the inset explicitly in the DB and apply it
during window creation. But this means storing more data, and the inset
would need to be platform-agnostic, which adds complexity. Doesn’t seem
worth it for no real gain.

X11 Before:
```sh
saving window bounds with width: 1136, height: 784
tims@lemon ~/w/zed (fix-window-growing-larger)> cargo run
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.37s
     Running `target/debug/zed`
saving window bounds with width: 1156, height: 804   <---- +20px
tims@lemon ~/w/zed (fix-window-growing-larger)> cargo run
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.35s
     Running `target/debug/zed`
saving window bounds with width: 1176, height: 824  <---- +20px
tims@lemon ~/w/zed (fix-window-growing-larger)> cargo run
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.36s
     Running `target/debug/zed`
saving window bounds with width: 1196, height: 844  <---- +20px
```

X11 After:
```sh
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.35s
     Running `target/debug/zed`
saving window bounds with width: 1116, height: 764
tims@lemon ~/w/zed (fix-window-growing-larger)> cargo run
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.35s
     Running `target/debug/zed`
saving window bounds with width: 1116, height: 764     <---- same
tims@lemon ~/w/zed (fix-window-growing-larger)> cargo run
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.35s
     Running `target/debug/zed`
saving window bounds with width: 1116, height: 764   <---- same
```

On Wayland, saving occurs only when you actually resize the window (on
X11, saving happens both on init and while dragging the window). To
trigger saving, I manually resized the window by ~1px to make it print.

Wayland Before:
```sh
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 1m 36s
     Running `target/debug/zed`
saving window bounds with width: 945, height: 577
tims@orange ~/zed (fix-window-growing-larger)> cargo run
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 1.77s
     Running `target/debug/zed`
saving window bounds with width: 966, height: 597    <--- +20px on both  (1px increase in width is me resizing)
tims@orange ~/zed (fix-window-growing-larger)> cargo run
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.87s
     Running `target/debug/zed`
saving window bounds with width: 987, height: 618    <--- +20px on both  (1px increase in width and height is me resizing)
tims@orange ~/zed (fix-window-growing-larger)> cargo run
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.89s
     Running `target/debug/zed`
saving window bounds with width: 1006, height: 638   <--- +20px on both  (1px decrease in width is me resizing)
```

Wayland After:
```sh
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.82s
     Running `target/debug/zed`
saving window bounds with width: 925, height: 558
tims@orange ~/zed (fix-window-growing-larger)> cargo run
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.84s
     Running `target/debug/zed`
saving window bounds with width: 925, height: 557     <--- same (1px decrease in height is me resizing)
saving window bounds with width: 925, height: 558
```

Release Notes:

- Fix non-maximized zed windows growing larger across sessions on Linux

---------

Co-authored-by: mgsloan@gmail.com <michael@zed.dev>
2024-12-20 22:23:25 +00:00
Michael Sloan
c01403b4b1 Reapply completion docs prefetch (#22306)
Leaving release notes as N/A because it had release notes in the past in
#21705

In #21286, documentation resolution was made more efficient by only
resolving the current completion. However, this meant that single line
documentation shown inline in the menu was missing until scrolled
to. This also meant that it would wait for navigation to resolve
completion docs, leading to lag for displaying documentation.

This change resolves this by attempting to fetch all the completions
that will be shown. It also mostly avoids re-resolving completions. It
intentionally re-resolves the current selection on navigation, as some
language servers will respond with more information later on.

Release Notes:

- N/A
2024-12-20 22:16:07 +00:00
Danilo Leal
8ee04bf04a Fix remote dev project name label (#22307)
Closes https://github.com/zed-industries/zed/issues/21877

<img width="800" alt="Screenshot 2024-12-20 at 6 45 50 PM"
src="https://github.com/user-attachments/assets/2069465f-1a00-47de-b691-585bb2603279"
/>

Release Notes:

- N/A
2024-12-20 22:02:12 +00:00
Richard Feldman
4ed0e5160f Inline assistant v2 layout (#22305)
Makes the inline assistant look like @danilo-leal's prototype.

Also fixes the bug with indent guides that @maxdeviant found!

<img width="1059" alt="Screenshot 2024-12-20 at 4 24 56 PM"
src="https://github.com/user-attachments/assets/5e55a3c2-768a-4e9d-bad5-d4ebbe9db9ce"
/>

Release Notes:

- N/A
2024-12-20 22:00:52 +00:00
Kirill Bulatov
72e56eee7a Use stable Alacritty crate version (#22304)
Release Notes:

- N/A
2024-12-20 21:40:22 +00:00
tims
306fc19739 Fix knockout icon background to match hovered entry background in project panel (#22258)
No issue attached, as this is something trivial and should have been
part of my merged PR
[here](https://github.com/zed-industries/zed/pull/22073).

For context, in the above-mentioned PR, I removed the hover color from
selected/marked entries in the Project Panel. The reasoning behind this
change is already explained in that PR, but to reiterate:

> This change was inspired by the behavior in VSCode, the Firefox
sidebar, and Dolphin (KDE File Manager). When an item is selected, it
doesn’t display a separate hover state to avoid confusing users about
whether the item is selected or merely hovered over.

@nilskch mentioned in the comments of the above PR that I should have
also changed the background of the knockout icon, which appears on
entries, to match the entry background color on hover. This PR addresses
that.

Before:

![Screenshot_20241219_230005](https://github.com/user-attachments/assets/fd517e01-1a1e-4e63-9320-ba24bf3d4c64)

After:

![Screenshot_20241219_225853](https://github.com/user-attachments/assets/35fb146f-83e4-409b-8061-0efbdbba5cf9)

Release Notes:

- N/A
2024-12-20 21:24:38 +00:00
Peter Tripp
4fbb568f42 Improve script/bump-zed-minor-version (#22199)
- Follow up to: https://github.com/zed-industries/zed/pull/22170

CC: @mgsloan 

Release Notes:

- N/A

---------

Co-authored-by: Michael Sloan <mgsloan@gmail.com>
2024-12-20 21:02:57 +00:00
Marshall Bowers
7913b6a5a2 assistant2: Restrict directory context picker to development builds (#22302)
This PR temporarily restricts the directory context picker to
development builds until the implementation is finished.

Release Notes:

- N/A
2024-12-20 19:08:29 +00:00
Marshall Bowers
d566792ae1 assistant2: List directories in directory context picker (#22300)
This PR updates the directory context picker in Assistant2 to show the
available directories.

Release Notes:

- N/A
2024-12-20 18:42:36 +00:00
Piotr Osiewicz
62f5ca562e editor: Improve performance of lsp_ext validation (#22299)
We've received a complaint on Discord about bad multicursor performance.
I too run 15k cursor simultaneously. The gist of the issue was in the
lsp_ext; whenever we gather up actions to be registered on a buffer, we
need to know whether a buffer has any of the languages for which we have
LSP extensions. The problem stemed from the fact that we did a two-phase
filtering. For each selection we'd first check whether this selection
lies in a part of a file that is part of a language for which we have
LSP extensions. Then, we'd check whether we're running a language server
of interest for this buffer.

This is not optimal, because it would often do the redundant work:
1. We resolve selections for buffer that are known to not contain a
given language server.
2. We look up language server in the LspStore once per each matching
selection.

In case where the file is not related at all, we end up resolving all of
the selections which is pretty bad. This PR makes us skip buffers which
are known to not match the criteria. It also caches the result of
language server lookup for the buffers.

Closes #ISSUE

Release Notes:

- Improved performance with large quantity of cursors
2024-12-20 18:41:23 +00:00
Piotr Osiewicz
4eb8492308 chore: Remove stray println (#22297)
Follow-up to #22292

Release Notes:

- N/A
2024-12-20 18:18:44 +00:00
Thorsten Ball
d824baeece Fix lang servers status set to Downloading when checking version (#22292)
This message has confused me many times too: we printed the status as
"Downloading" when we were only checking whether we need to install a
given version of a language server.

This fixes the issue for Node-based language servers where we had the
same check in all implementations.

Closes  #22241

Release Notes:

- Fixed some language servers reporting status as "Downloading..." when
only a version check was being done.
2024-12-20 16:59:10 +00:00
Marshall Bowers
8a858fee7c Enable Assistant2 outside of development builds (#22294)
This PR removes the gate that limited Assistant2 to development builds,
so that we can start testing it out in Nightly.

Note that currently this still requires explicit opt-in to the
`assistant2` feature flag.

Release Notes:

- N/A
2024-12-20 16:43:24 +00:00
Alejandro Gómez-Londoño
cbd2e81a7e Add arrow key movements to terminal vi mode (#22103)
Expands #18715

Release Notes:

- Added arrow keys movement to the built-in terminal's [vi
mode](https://github.com/alacritty/alacritty/blob/master/docs/features.md#vi-mode)
(which is using Alacritty under the hood).

Details
--
A minuscule improvement on #18715 to allow user with alternative
keyboard layouts to use the terminal's vi mode with the arrow keys.
2024-12-20 13:49:12 +00:00
Thorsten Ball
b25d8ecb75 vim: Fix deletion with sentence-motion (#22289)
Fixes #22151.

Turns out Vim also has some weird behavior with sentence deletion in
case it's on the first character of a line.

Release Notes:

- vim: Fixed deleting sentence-wise (i.e. `d(` and `d)`), which would
previously delete the whole line instead of just a sentence.
2024-12-20 12:48:38 +00:00
Aaron Feickert
7c03e11cfc Fix blank line cursor width (#22275)
This PR ensures that, for fixed-width fonts, the cursor width is the
same on blank lines as on non-blank lines, as well as at the end of a
line. It does so by using the em advance to define the cursor width
instead of the em width in these cases.

Note that this can look... bizarre on non-fixed-width fonts:
<img width="93" alt="Screenshot 2024-12-19 at 21 43 11"
src="https://github.com/user-attachments/assets/a4c9b26c-98ea-4a1d-947b-51f1acd3c2f8"
/>
However, this is arguably reasonably consistent with how (terminal) Vim
handles it:
<img width="45" alt="Screenshot 2024-12-19 at 21 46 42"
src="https://github.com/user-attachments/assets/ec3ff614-7a15-4cc3-8d14-3d15ce62f2b8"
/>

Closes #22260.

Release Notes:

- N/A
2024-12-20 10:26:27 +00:00
Michael Sloan
f3fc4d6279 Add a CI check for todo! and FIXME comments (#21950)
Motivation for this is to support writing comments that will certainly
be revisited before merge.

Release Notes:

- N/A
2024-12-20 08:38:50 +00:00
tims
e4493d60dc linux: Fix wrong cursor theme for arrow cursor style (#22276)
Closes #22264

On Linux, the arrow cursor style currently used by Zed is `arrow`.
However, this style might not be available in most themes, causing the
cursor to fall back to system default theme. Note cursor style are
platform (X11 and Wayland) agnostic.

Most themes use `left_ptr` as their arrow cursor style instead of
`arrow`. In some cases, `left_ptr` and `arrow` are symlinks pointing to
the `default` style, but the `default` style is not guaranteed to be
available across all themes.

After inspecting the available cursor themes on popular desktop
environments, changing the default from `arrow` to `left_ptr` seems to
be available in all of them. `left_ptr` as default cursor style is also
mentioned in [Arch Wiki: Cursor
themes](https://wiki.archlinux.org/title/Cursor_themes#Change_X_shaped_default_cursor).

KDE:
```sh
tims@lemon /u/s/icons> find . -name "arrow"
./Breeze_Snow/cursors/arrow
./breeze_cursors/cursors/arrow
./Adwaita/cursors/arrow

tims@lemon /u/s/icons> find . -name "default"
./default
./Breeze_Snow/cursors/default
./breeze_cursors/cursors/default
./Adwaita/cursors/default

tims@lemon /u/s/icons> find . -name "left_ptr"
./Oxygen_White/cursors/left_ptr
./KDE_Classic/cursors/left_ptr
./Oxygen_Yellow/cursors/left_ptr
./Oxygen_Blue/cursors/left_ptr
./Oxygen_Black/cursors/left_ptr
./Breeze_Snow/cursors/left_ptr
./breeze_cursors/cursors/left_ptr
./Adwaita/cursors/left_ptr
./Oxygen_Zion/cursors/left_ptr

```

Gnome:
```sh
tims@orange:/usr/share/icons$ find . -name "arrow"
./DMZ-Black/cursors/arrow
./Adwaita/cursors/arrow
./redglass/cursors/arrow
./whiteglass/cursors/arrow
./handhelds/cursors/arrow
./Yaru/cursors/arrow
./DMZ-White/cursors/arrow

tims@orange:/usr/share/icons$ find . -name "default"
./Adwaita/cursors/default
./default
./Yaru/cursors/default

tims@orange:/usr/share/icons$ find . -name "left_ptr"
./DMZ-Black/cursors/left_ptr
./Adwaita/cursors/left_ptr
./redglass/cursors/left_ptr
./whiteglass/cursors/left_ptr
./handhelds/cursors/left_ptr
./Yaru/cursors/left_ptr
./DMZ-White/cursors/left_ptr
```

My theme is set to Oxygen Yellow here.

Before:
<img
src="https://github.com/user-attachments/assets/7485f1e7-5936-45b4-96bd-399525bad95d"
alt="before" width="450px" />

After:
<img
src="https://github.com/user-attachments/assets/56090735-6a1f-4652-ad3e-075ff4c3f9ab"
alt="after" width="450px" />


Release Notes:

- Fixed wrong cursor theme for arrow cursor style on Linux.
2024-12-20 05:57:47 +00:00
Max Brunsfeld
3632b36fde Move multibuffer tests to their own source file (#22270)
Release Notes:

- N/A
2024-12-20 02:02:32 +00:00
renovate[bot]
97e11fd5d2 Update aws-sdk-rust monorepo (#22215)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [aws-config](https://redirect.github.com/smithy-lang/smithy-rs) |
dependencies | patch | `1.5.10` -> `1.5.11` |
| [aws-sdk-kinesis](https://redirect.github.com/awslabs/aws-sdk-rust) |
dependencies | minor | `1.52.0` -> `1.53.0` |
| [aws-sdk-s3](https://redirect.github.com/awslabs/aws-sdk-rust) |
dependencies | minor | `1.65.0` -> `1.66.0` |

---

### 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.

👻 **Immortal**: This PR will be recreated if closed unmerged. Get
[config
help](https://redirect.github.com/renovatebot/renovate/discussions) if
that's undesired.

---

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

---

Release Notes:

- N/A

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

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-20 01:15:25 +00:00
renovate[bot]
150aa03c5f Update Rust crate hyper to v0.14.32 (#22207)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [hyper](https://hyper.rs)
([source](https://redirect.github.com/hyperium/hyper)) |
workspace.dependencies | patch | `0.14.31` -> `0.14.32` |

---

### Release Notes

<details>
<summary>hyperium/hyper (hyper)</summary>

###
[`v0.14.32`](https://redirect.github.com/hyperium/hyper/releases/tag/v0.14.32)

[Compare
Source](https://redirect.github.com/hyperium/hyper/compare/v0.14.31...v0.14.32)

#### Features

- **server:** add `Builder::max_pending_accept_reset_streams(num)`
option
([a24f0c0](a24f0c0af8))

#### Bug Fixes

- **http1:** fix intermittent panic parsing partial headers
([0f274ae](0f274ae653))

#### New Contributors

- [@&#8203;cratelyn](https://redirect.github.com/cratelyn) made their
first contribution in
[https://github.com/hyperium/hyper/pull/3796](https://redirect.github.com/hyperium/hyper/pull/3796)

**Full Changelog**:
https://github.com/hyperium/hyper/compare/v0.14.31...v0.14.32

</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:eyJjcmVhdGVkSW5WZXIiOiIzOS43Mi41IiwidXBkYXRlZEluVmVyIjoiMzkuNzIuNSIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOltdfQ==-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-20 01:14:22 +00:00
renovate[bot]
ebf6115c3c Update Rust crate semver to v1.0.24 (#22211)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [semver](https://redirect.github.com/dtolnay/semver) |
workspace.dependencies | patch | `1.0.23` -> `1.0.24` |

---

### Release Notes

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

###
[`v1.0.24`](https://redirect.github.com/dtolnay/semver/releases/tag/1.0.24)

[Compare
Source](https://redirect.github.com/dtolnay/semver/compare/1.0.23...1.0.24)

- Optimize Ord impls for semver::Prerelease and semver::BuildMetadata
([#&#8203;328](https://redirect.github.com/dtolnay/semver/issues/328),
thanks [@&#8203;Eh2406](https://redirect.github.com/Eh2406))

</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:eyJjcmVhdGVkSW5WZXIiOiIzOS43Mi41IiwidXBkYXRlZEluVmVyIjoiMzkuNzIuNSIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOltdfQ==-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-20 01:14:10 +00:00
renovate[bot]
394af7481d Update Rust crate tree-sitter-c to v0.23.4 (#22212)
This PR contains the following updates:

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

---

### Release Notes

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

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

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

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

</details>

---

### Configuration

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

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

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

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

---

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

---

Release Notes:

- N/A

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

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-20 01:13:51 +00:00
renovate[bot]
7b0d63fffb Update Rust crate tree-sitter-json to 0.24 (#22226)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
|
[tree-sitter-json](https://redirect.github.com/tree-sitter/tree-sitter-json)
| workspace.dependencies | minor | `0.23` -> `0.24` |

---

### Release Notes

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

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

[Compare
Source](https://redirect.github.com/tree-sitter/tree-sitter-json/compare/v0.24.7...v0.24.8)

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

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

[Compare
Source](https://redirect.github.com/tree-sitter/tree-sitter-json/compare/v0.24.5...v0.24.7)

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

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

[Compare
Source](https://redirect.github.com/tree-sitter/tree-sitter-json/compare/v0.24.4...v0.24.5)

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

###
[`v0.24.4`](https://redirect.github.com/tree-sitter/tree-sitter-json/compare/v0.24.3...v0.24.4)

[Compare
Source](https://redirect.github.com/tree-sitter/tree-sitter-json/compare/v0.24.3...v0.24.4)

###
[`v0.24.3`](https://redirect.github.com/tree-sitter/tree-sitter-json/compare/v0.24.2...v0.24.3)

[Compare
Source](https://redirect.github.com/tree-sitter/tree-sitter-json/compare/v0.24.2...v0.24.3)

###
[`v0.24.2`](https://redirect.github.com/tree-sitter/tree-sitter-json/compare/v0.24.1...v0.24.2)

[Compare
Source](https://redirect.github.com/tree-sitter/tree-sitter-json/compare/v0.24.1...v0.24.2)

###
[`v0.24.1`](https://redirect.github.com/tree-sitter/tree-sitter-json/compare/v0.24.0...v0.24.1)

[Compare
Source](https://redirect.github.com/tree-sitter/tree-sitter-json/compare/v0.24.0...v0.24.1)

###
[`v0.24.0`](https://redirect.github.com/tree-sitter/tree-sitter-json/compare/v0.23.0...v0.24.0)

[Compare
Source](https://redirect.github.com/tree-sitter/tree-sitter-json/compare/v0.23.0...v0.24.0)

</details>

---

### Configuration

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

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

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

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

---

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

---

Release Notes:

- N/A

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

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-20 01:13:17 +00:00
tims
f64bfe8c1d linux: Fix saving file with root ownership (#22045)
Closes #13585

Currently, saving files with `root` ownership or `root` as the group
throws a `Permission denied (os error 13). Please try again.` error.
This PR fixes the issue on Linux by prompting the user for a password
and saving the file with elevated privileges.

It uses `pkexec` (Polkit), which is by default available on GNOME, KDE,
and most Linux systems. I haven't implemented this for macOS as I don't
have a device to test it on.

This implementation is similar to how Vscode handles it. Except, they
don't show custom message.

**Working**:

When file saving fails due to a `PermissionDenied` error, we create a
temporary file in the same directory as the target file and writes the
data to this temporary file. After, the contents of this file are copied
to the original file using the `tee` command instead of `cp` or `mv`.
This ensures that the ownership and permissions of the original file are
preserved. This command is executed using `pkexec` which will prompt
user for their password.

**Custom Message**:

The message displayed to the user in the prompt is automatically
retrieved from the `org.zed.app.policy` file, which is located at
`/usr/share/polkit-1/actions/`. This file should be installed during the
setup process. While the policy file is optional, omitting it will cause
the user to see the underlying command being executed rather than a
user-friendly message. Currently, VSCode does not display the
user-friendly message.

The policy file must specify a unique binary, ensuring that only that
binary can use the policy file. It cannot be as generic as a
`/bin/bash`, as any software using bash to prompt will end up showing
Zed’s custom message. To address this, we will create a custom bash
script, as simple as the following, placed in `/usr/bin/zed/elevate.sh`.
The script should have root ownership and should not reside in the home
directory, since the policy file cannot resolve `$HOME`.

```sh
#!/bin/bash
eval "$@"
```

*IMPORTANT NOTE*

Since copying the policy file and our script requires sudo privileges,
the installation script will now prompt for the password at very end.
Only on Linux, if `pexec` is installed.

Screenshots:

KDE with policy file:
![Screenshot from 2024-12-15
22-13-06](https://github.com/user-attachments/assets/b8bb7565-85df-4c95-bb10-82e50acf9b56)

Gnome with policy file:
![Screenshot from 2024-12-15
22-21-48](https://github.com/user-attachments/assets/83d15056-a2bd-41d9-a01d-9b8954260381)

Gnome without policy file:

![image](https://github.com/user-attachments/assets/66c39d02-eed4-4f09-886f-621b6d37ff43)

VSCode:

![image](https://github.com/user-attachments/assets/949dc470-c3df-4e2f-8cc6-31babaee1d18)

User declines the permission request:

![image](https://github.com/user-attachments/assets/c5cbf056-f6f9-43a8-8d88-f2b0597e14d6)

Release Notes:

- Fixed file saving with root ownership on Linux.
2024-12-19 22:16:01 +00:00
Michael Sloan
5b86845605 Fix docs for Bounds::from_corner_and_size (#22265)
(left in a todo!, oops)

Release Notes:

- N/A
2024-12-19 21:50:52 +00:00
Marshall Bowers
9782abf3c5 ci: Put docs-only conditionals on each step (#22261)
This PR is a follow up to #22254 with a different approach.

We need to put the conditional on each step in order to skip them, as I
couldn't see any other way of bailing out of the pipeline early based on
a condition.

Release Notes:

- N/A
2024-12-19 19:46:10 +00:00
Cole Miller
6231072d85 Hide chat panel button when not in a call (#22200)
cc @nathansobo 

Release Notes:

- Hide chat panel button by default when not in a call
2024-12-19 19:32:45 +00:00
Cole Miller
2094d50514 Fix permalink-to-line when Git repo root and worktree dir don't coincide (#22003)
Closes #21505. This should work if the git dir is an ancestor of the
worktree dir or vice versa.

Release Notes:

- Fixed GitHub permalink-to-line actions when worktree dir and Git dir
aren't the same
2024-12-19 19:23:50 +00:00
Ringo De Smet
1e2fa3b022 Update macOS system requirements in docs (#22248)
Update macOS system requirements to include Sequoia (15.x), the latest
major version.

Release Notes:

- N/A

Co-authored-by: Marshall Bowers <elliott.codes@gmail.com>
2024-12-19 17:55:46 +00:00
Piotr Osiewicz
8e81070091 terminal: Clear output after venv is activated (#22256)
The command used to activate the venv can still be accessed/scrolled to
if needed.

Release Notes:

- The Python virtual environment activation command is no longer shown
in the terminal output by default.

Co-authored-by: Peter Tripp <peter@zed.dev>
2024-12-19 17:43:19 +00:00
Thorsten Ball
1071814d41 zeta: Always include current file's outline in telemetry (#22257)
This doesn't include the outline in the prompt yet, but it does send it
up via telemetry so we can use it to see whether it would have improved
generated output.

Release Notes:

- N/A

Co-authored-by: Agus <agus@zed.dev>
2024-12-19 17:17:10 +00:00
Marshall Bowers
3d3d8f20eb ci: Run required status checks for docs-only PRs, but exit early (#22254)
I noticed a problem with the addition of required status checks where
PRs that only touched the docs wouldn't pass the status checks due to
all of the checks being skipped via `paths-ignore`:

<img width="950" alt="Screenshot 2024-12-19 at 11 16 38 AM"
src="https://github.com/user-attachments/assets/a6fa43ee-de63-40a6-a15a-f2f3519e9db8"
/>

This PR aims to address this by making it so required status checks run
for docs-only PRs, but exit early (before doing all of the work).

Release Notes:

- N/A
2024-12-19 17:05:11 +00:00
Aaron Feickert
536a958c58 Fix cursor overlap (#21999)
When the cursor has a block shape and is not on the first line, its name
popup exhibits an overlap:
<img width="171" alt="Screenshot 2024-12-13 at 18 00 54"
src="https://github.com/user-attachments/assets/1dc2ef93-020b-45c4-9fc6-db7d97f65c62"
/>

This occurs because the popup's horizontal alignment is set differently
when the cursor
[is](fff12ec1e5/crates/editor/src/element.rs (L6383))
or
[isn't](fff12ec1e5/crates/editor/src/element.rs (L6385))
on the first line.

This PR makes the horizontal alignment the same in both cases, which
removes the overlap:
<img width="176" alt="Screenshot 2024-12-13 at 17 57 20"
src="https://github.com/user-attachments/assets/a3c10ed5-6a1b-4040-9408-92290e9da30b"
/>

Closes #21887.

Release Notes:

- Fixed an overlap that cuts off user names when a cursor has a block
shape.
2024-12-19 16:24:30 +00:00
Thorsten Ball
96ad022cd7 Fix project environment not working correctly with multiple worktrees (#22246)
Fixes https://github.com/zed-industries/zed/issues/21972

This fixes two bugs:

**Bug 1**: this bug caused us to only ever load a single environment in
a multi-worktree project, thanks to this line:

```rust
if let Some(task) = self.get_environment_task.as_ref()
```

We'd only ever run a single task per project, which is wrong.

What does code does is to cache the tasks per `worktree_id`, which means
we don't even need to cache the environments again, since we can just
cache the `Shared<Task<...>>`.

**Bug 2**: we assumed that every `worktree_abs_path` is a directory,
which lead to `Failed to run direnv` log messages when opening a project
that had a worktree with a single file open (easy to reproduce: open a
normal project, open your settings, close Zed, reopen it — the settings
faile caused environments to not load)

It's fixed by checking whether the `worktree_abs_path` is an absolute
directory. Since this is always running locally, it's fine to use
`smol::fs` here instead of using our `Fs`.

Release Notes:

- Fixed shell environments not being loaded properly to be used by
language servers and terminals in case a project had multiple worktrees.
- Fixed `Failed to run direnv` messages showing up in case Zed restored
a window that contained a worktree with a single file.
https://github.com/zed-industries/zed/issues/21972
2024-12-19 14:50:14 +00:00
Agus Zubiaga
11260e6d37 Match keymap-style action names in command palette (#22149)
For example, `editor::TabPrev` matches "editor: tab prev".

Release Notes:

- Added support for searching command palette using keymap-style action
names.

---------

Co-authored-by: Peter Tripp <peter@zed.dev>
2024-12-19 12:48:54 +00:00
Thorsten Ball
d54662e683 zeta: Do not change user's menu selection if completion is late (#22238)
Release Notes:

- N/A
2024-12-19 09:42:29 +00:00
Thorsten Ball
2a17274ec2 zeta: Always show ghost text, even if showing completion in menu (#22194)
See:
![screenshot-2024-12-18-17 26
22@2x](https://github.com/user-attachments/assets/82bdea76-6c96-4c4d-a08a-1601381a378f)


https://github.com/user-attachments/assets/165d9f0d-8339-4fd9-a796-a67121689af6



Release Notes:

- N/A

Co-authored-by: Danilo <danilo@zed.dev>
2024-12-19 09:00:58 +00:00
Michael Sloan
b93cee8d27 Use static LazyLocks for all constant regexes (#22225)
Release Notes:

- N/A
2024-12-19 02:20:35 +00:00
Peter Tripp
837bbc851f astro: Bump to v0.1.2 (#22220)
Includes:
- https://github.com/zed-industries/zed/pull/20206

Release Notes:

- N/A
2024-12-18 23:22:24 +00:00
Peter Tripp
0fe88a88b1 elixir: Bump to v0.1.2 (#22219)
Includes:
- https://github.com/zed-industries/zed/pull/21666
- https://github.com/zed-industries/zed/pull/20206

Release Notes:

- N/A
2024-12-18 23:22:19 +00:00
renovate[bot]
2b4f0deff5 Update Rust crate async-tungstenite to v0.28.2 (#22206)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
|
[async-tungstenite](https://redirect.github.com/sdroege/async-tungstenite)
| workspace.dependencies | patch | `0.28.1` -> `0.28.2` |

---

### Release Notes

<details>
<summary>sdroege/async-tungstenite (async-tungstenite)</summary>

###
[`v0.28.2`](https://redirect.github.com/sdroege/async-tungstenite/blob/HEAD/CHANGELOG.md#0282---2024-12-15)

[Compare
Source](https://redirect.github.com/sdroege/async-tungstenite/compare/0.28.1...0.28.2)

##### Fixed

- Add `alloc` feature to `futures_task` dependency to make sure
`futures-task::ArcWake` is available.

</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:eyJjcmVhdGVkSW5WZXIiOiIzOS43Mi41IiwidXBkYXRlZEluVmVyIjoiMzkuNzIuNSIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOltdfQ==-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-18 23:05:46 +00:00
Marshall Bowers
4f2ab812fb ci: Enable merge_group for workflows (#22214)
Enable the `merge_group` for CI workflows so that they work with the
merge queue.

Release Notes:

- N/A
2024-12-18 22:59:20 +00:00
Michael Sloan
3f40d76be4 Enable search within diagnostics pane (#22213)
Closes #16033
Addresses one part of #4475

Release Notes:

- Added support for find and replace in diagnostics. Also causes
keybindings that use search to now work (such as `*` and `#` vim
bindings).
2024-12-18 22:52:16 +00:00
Michael Sloan
6bb21b1e5e Fix vim repeat (.) and macro playback insertions in diagnostics and assistant (#22210)
Release Notes:

- Fixed vim repeat (`.`) and macro playback of insertions in diagnostics
and assistant.

Co-authored-by: Conrad <conrad@zed.dev>
2024-12-18 15:30:52 -07:00
tims
f7a7866d4a linux: Implement Menus (#21873)
Closes #19837

This PR implements menus for Linux and Windows, inspired by JetBrains
IDEs. Thanks to @notpeter for the inspiration.


https://github.com/user-attachments/assets/7267fcdf-fec5-442e-a53b-281f89471095

I plan to complete this in multiple parts. While this PR delivers a
fully functional menus, there are many UX improvements that can be done.
So, this is part 1 of 3.

**This PR**:
- [x] Clicking the application menu opens the first menu popup. This
also shows other available menus.
- [x] While a menu is open, hovering over other menus opens them without
needing a click.
- [x] Up/down arrow keys works out of the box. Thanks GPUI. 

**Future - Part 2**:
- Add keybinding support to open specific menus using `Option + first
character of menu item`.
- Add support for left/right arrow keys to move between menus.

**Future - Part 3**:
- Implement nested context menus in GPUI for submenus. (I haven't
checked if this already exists).

---------

Co-authored-by: Mikayla <mikayla@zed.dev>
2024-12-18 17:27:25 -05:00
Dzmitry Malyshau
298b9df589 Switch to a single GPU context in Blade (#20853)
Closes #17005

Release Notes:

- Improved GPU context management: share a single context with multiple
surfaces.

### High Level

Blade got a proper support for Surface objects in
https://github.com/kvark/blade/pull/203.
That was mainly motivated by Zed needing to draw multiple windows. With
the Surface API, Zed is now able to have the GPU context tied to the
"Platform" instead of "Window". Practically speaking, this means:
  - architecture more sound
  - faster to open/close windows
  - less surprises, more robust

### Concerns

1. Zed has been using a temporary workaround for the platform bug on
some Intel+Nvidia machines that makes us unable to present -
https://github.com/kvark/blade/pull/144 . This workaround is no longer
available with the new architecture. I'm looking for ideas on how to
approach this better.
- we are now picking up the change in
https://github.com/kvark/blade/pull/210, which allows forcing a specific
Device ID. This should allow Zed users to work around the issue. We
could help them to automate it, too.
2. ~~Metal-rs dependency is switched to
https://github.com/kvark/metal-rs/tree/blade, since upstream isn't
responsive in merging changes that are required for Blade. Hopefully,
temporary.~~
- ~~we can also hack around it by just transmuting the texture
references, since we know those are unchanged in the branch. That would
allow Blade to use it's own version of Metal, temporarily, if switching
metal-rs in the workspace is a concern.~~
- merged my metal-rs changes and updated Zed to use the upstream github
reference

---------

Co-authored-by: Mikayla Maki <mikayla@zed.dev>
Co-authored-by: Mikayla Maki <mikayla.c.maki@gmail.com>
Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
2024-12-18 14:47:09 -07:00
Agus Zubiaga
56d20fc0a3 Reuse prompt editor across buffer and terminal assist (#22188)
Builds on https://github.com/zed-industries/zed/pull/22160 and extracts
the rest of `PromptEditor` so it can be shared across terminal and
inline assistants. This will help avoid the UI drifting as we have
already observed.

Note: This is mostly a mechanical refactor. I imagine some things could
be factored in a better way by someone with more context, but I think
this is a good start.

Release Notes:

- N/A

---------

Co-authored-by: Richard Feldman <oss@rtfeldman.com>
2024-12-18 16:12:54 -05:00
Marshall Bowers
fc00eaa161 assistant2: Reduce message editor's maximum height (#22205)
This PR reduces the message editor's maximum height to 10 lines, after
which point it will scroll.

Release Notes:

- N/A
2024-12-18 16:11:31 -05:00
renovate[bot]
7414e91a85 Update actions/upload-artifact digest to 6f51ac0 (#22203)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
|
[actions/upload-artifact](https://redirect.github.com/actions/upload-artifact)
| action | digest | `b4b15b8` -> `6f51ac0` |

---

### 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:eyJjcmVhdGVkSW5WZXIiOiIzOS43Mi41IiwidXBkYXRlZEluVmVyIjoiMzkuNzIuNSIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOltdfQ==-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-18 22:59:38 +02:00
renovate[bot]
b51a162d22 Update Rust crate tiktoken-rs to 0.6.0 (#21900)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [tiktoken-rs](https://redirect.github.com/zurawiki/tiktoken-rs) |
workspace.dependencies | minor | `0.5.9` -> `0.6.0` |

---

### Release Notes

<details>
<summary>zurawiki/tiktoken-rs (tiktoken-rs)</summary>

###
[`v0.6.0`](https://redirect.github.com/zurawiki/tiktoken-rs/releases/tag/v0.6.0)

[Compare
Source](https://redirect.github.com/zurawiki/tiktoken-rs/compare/v0.5.9...v0.6.0)

**Minor version release signifies a breaking change in 0.x**

#### What's Changed

- Add support for chatgpt-4o-latest by
[@&#8203;Congyuwang](https://redirect.github.com/Congyuwang) in
[https://github.com/zurawiki/tiktoken-rs/pull/85](https://redirect.github.com/zurawiki/tiktoken-rs/pull/85)
- Refactor internals to make future updates to tiktoken easier to merge
-   Do not expose tiktoken internal modules and functions
-   Update dependencies

#### New Contributors

- [@&#8203;Congyuwang](https://redirect.github.com/Congyuwang) made
their first contribution in
[https://github.com/zurawiki/tiktoken-rs/pull/85](https://redirect.github.com/zurawiki/tiktoken-rs/pull/85)

**Full Changelog**:
https://github.com/zurawiki/tiktoken-rs/compare/v0.5.9...v0.6.0

</details>

---

### Configuration

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

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

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

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

---

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

---

Release Notes:

- N/A

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

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-18 22:24:56 +02:00
renovate[bot]
78dde63337 Update Rust crate heed to 0.21.0 (#21892)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [heed](https://redirect.github.com/Kerollmops/heed) |
workspace.dependencies | minor | `0.20.1` -> `0.21.0` |

---

### 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:eyJjcmVhdGVkSW5WZXIiOiIzOS41OC4xIiwidXBkYXRlZEluVmVyIjoiMzkuNTguMSIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOltdfQ==-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-18 22:24:48 +02:00
renovate[bot]
c0b40d0bd0 Update Rust crate cargo_toml to 0.21 (#21880)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [cargo_toml](https://lib.rs/cargo_toml)
([source](https://gitlab.com/lib.rs/cargo_toml)) |
workspace.dependencies | minor | `0.20` -> `0.21` |

---

### 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:eyJjcmVhdGVkSW5WZXIiOiIzOS41OC4xIiwidXBkYXRlZEluVmVyIjoiMzkuNTguMSIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOltdfQ==-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-18 22:24:30 +02:00
renovate[bot]
0acb743dac Update 2428392/gh-truncate-string-action action to v1.4.1 (#22204)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
|
[2428392/gh-truncate-string-action](https://redirect.github.com/2428392/gh-truncate-string-action)
| action | patch | `v1.4.0` -> `v1.4.1` |

---

### Release Notes

<details>
<summary>2428392/gh-truncate-string-action
(2428392/gh-truncate-string-action)</summary>

###
[`v1.4.1`](https://redirect.github.com/2428392/gh-truncate-string-action/releases/tag/v1.4.1)

[Compare
Source](https://redirect.github.com/2428392/gh-truncate-string-action/compare/v1.4.0...v1.4.1)

#### What's Changed

- update action.yml inputs to include truncationSymbol by
[@&#8203;aballman](https://redirect.github.com/aballman) in
[https://github.com/2428392/gh-truncate-string-action/pull/14](https://redirect.github.com/2428392/gh-truncate-string-action/pull/14)

#### New Contributors

- [@&#8203;aballman](https://redirect.github.com/aballman) made their
first contribution in
[https://github.com/2428392/gh-truncate-string-action/pull/14](https://redirect.github.com/2428392/gh-truncate-string-action/pull/14)

**Full Changelog**:
https://github.com/2428392/gh-truncate-string-action/compare/v1...v1.4.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:eyJjcmVhdGVkSW5WZXIiOiIzOS43Mi41IiwidXBkYXRlZEluVmVyIjoiMzkuNzIuNSIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOltdfQ==-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-18 22:24:02 +02:00
Marshall Bowers
8b2afab0d3 assistant2: Wire up basic @-mention interaction for context (#22197)
This PR adds an initial version of using `@` in the message editor to
add context to the thread.

We don't yet insert any sort of reference to it in the message body
itself.

Release Notes:

- N/A
2024-12-18 13:29:39 -05:00
Peter Tripp
b79117c882 Bump Zed to v0.168.x (#22198) 2024-12-18 12:03:40 -05:00
Conrad Irwin
77abf13f42 Allow telemetry::event! with no properties (#22190)
CC @josephTLyons

Release Notes:

- N/A
2024-12-18 09:41:47 -07:00
Michael Sloan
433cb99170 Remove main push from bump-zed-minor-versions script (#22170)
Motivation for this is to allow the `main` branch to be protected. This will enable use of auto-merge and merge queue.

Co-authored-by: Peter Tripp <peter@zed.dev>
2024-12-18 11:32:58 -05:00
Marshall Bowers
670ade9546 extensions_ui: Don't block scrolling on disabled extension cards (#22189)
This PR fixes an issue where disabled extension cards would block scroll
events when the mouse was over them.

Instead of using `.occlude`—which captures _all_ mouse events—we use
`.block_mouse_down` to just block mouse down events (to prevent clicking
the buttons on the disabled card). We also set the cursor style to the
default, as it was still switching to the pointer when hovered over top
of one of the buttons.

Release Notes:

- Fixed disabled extensions blocking scroll events in the extensions
list.
2024-12-18 11:20:58 -05:00
Conrad Irwin
4a6f071fde vim: Add support for :g/ and :v/ (#22177)
Closes #ISSUE

Still TODO to make this feature good is better command history

Release Notes:

- vim: Add support for `:g/<pattern>/<cmd>` and `:v/<pattern>/<cmd>`
2024-12-18 08:28:42 -07:00
Conrad Irwin
2ecbd97fe8 vim: Support count with [x and ]x (#22176)
Fixes: #21577
Fixes: #17245

Release Notes:

- vim: Add <count> support for [x/]x
2024-12-18 08:28:22 -07:00
Thorsten Ball
a0a095c6a3 zeta: Show deletions when inline completion is shown in menu (#22186)
If an inline completion isn't shown in a menu, we highlight text in the
editor as deleted.

But if it's shown in the menu, we didn't even show deleted text, which
makes it hard to understand what's going on.

This fixes it.
![screenshot-2024-12-18-14 34
55@2x](https://github.com/user-attachments/assets/579639e4-5ed9-4fe6-8e21-65166d192432)


Release Notes:

- N/A
2024-12-18 14:46:51 +01:00
Richard Feldman
4bfc107e3a Fix inconsistencies in "Transform" vs "Generate" tooltips for assistant v2 (#22160)
Also makes the inline assistant and inline terminal assistant share a
bunch more code.

Release Notes:

- N/A

---------

Co-authored-by: Agus <agus@zed.dev>
Co-authored-by: Agus Zubiaga <hi@aguz.me>
2024-12-18 07:10:55 -05:00
Thorsten Ball
6192c83f23 Show inline completions in menu only for supported providers (#22181)
This changes #22093 by making the change only have an effect for the
supported provider: Zeta.

Made the change because the UX is still experimental and I don't want to
break existing workflows for Copilot/Supermaven users.

Even Zeta users can opt-out of it by setting
`"show_inline_completions_in_menu": false` in their settings, in case
they want the old show-inline-completion-or-show-lsp-completion behavior
back.

Release Notes:

- N/A
2024-12-18 12:55:09 +01:00
Hai Zhu
e8c9283700 Fix the illegal JSON in the documentation (#22175)
Release Notes:

- N/A
2024-12-18 11:10:13 +02:00
Thorsten Ball
2469122784 Fix panic when calculating inline completion (#22180)
Possible panic here in case we can't find the excerpt in the
multibuffer.

I thought this could happen when the inline completion references an
excerpt that disappeared from the multibuffer, but looking at the code
I'm not sure anymore - we use the same multibuffer snapshot in this
whole function and the `anchor_in_excerpt` method clips the anchors.

Still, let's be safe here.

Release Notes:

- N/A
2024-12-18 09:53:34 +01:00
Conrad Irwin
6898a31f06 Fix panic in linked editing ranges (#21905)
We were committing the cardinal sin of subtracting one from a column
without clipping

Closes #ISSUE

Release Notes:

- Fixed a panic in linked editing ranges
2024-12-17 22:17:08 -07:00
Conrad Irwin
94bfb93d35 More telemetry events (#22171)
- **Convert more events to telemetry::event**
- **And call events**

Closes #ISSUE

Release Notes:

- N/A
2024-12-17 22:16:59 -07:00
Enrique Kessler Martínez
1b83020dc8 Update collab local development instructions (#22018)
When running the collab server locally on a new machine, I found there
were some instructions missing for setting up the Postgres database.
Namely, the user mentioned on the `.env.toml` is not created by default.

Ref: https://github.com/zed-industries/zed/issues/8260

Release Notes:

- N/A
2024-12-17 20:43:48 -07:00
Cole Miller
613deb6421 Remove panics from X11Window::set_title (#22173)
Release Notes:

- N/A
2024-12-17 22:15:35 -05:00
Cole Miller
672fc76832 Fix panic in activate_pane_in_direction (#21948)
Ah, invalid SecondaryMap key, we meet again...

Release Notes:

- Fixed panic when jumping between panes
2024-12-17 22:10:30 -05:00
Michael Sloan
5d7b6141fd Improve context menu aside layout via custom logic (#22154)
* Presence of the aside no longer affects position or size of the
context menu.

* Prefers to fit to the right, then on same side of line, then other
side of line, within the following preference order:
  - Max possible size within text area.
  - Max possible size within window.
- Actual size within window. This is the only case that could cause it
to jump around with less stability.

A further enhancement atop this might be to dynamically resize aside
height to fit.

Release notes are N/A as they are covered by the notes for #22102.

Closes #8523

Release Notes:

* N/A
2024-12-17 17:01:15 -07:00
Marshall Bowers
6aad616165 assistant2: Fix focus issues with opening history (#22169)
This PR fixes some focus issues with opening the thread history, which
required that the Assistant2 panel be focused in order for the history
to open.

Release Notes:

- N/A
2024-12-17 18:07:28 -05:00
Marshall Bowers
918866b7de assistant2: Allow creating a new thread via the command palette (#22168)
This PR allows a new thread to be created via the command palette when
the Assistant2 panel is not focused.

Release Notes:

- N/A
2024-12-17 17:51:47 -05:00
Peter Tripp
5b2653a1d1 Improve GitHub Issue template for Environment (#22147)
Add support for pre-filling feature request template from zed::RequestFeature action.

Co-authored-by: Agus <agus@zed.dev>
2024-12-17 17:28:47 -05:00
Marshall Bowers
ba44db7f49 gpui: Rename GPUSpecs to GpuSpecs (#22166)
This PR renames the `GPUSpecs` type to `GpuSpecs` to match Rust naming
conventions.

Release Notes:

- N/A
2024-12-17 17:22:53 -05:00
Danilo Leal
ce97e4ddc1 assistant2: Add temporary icon for the status bar item (#22156)
This is temporary and should be removed whenever we fully transition to
Assistant 2.

<img width="800" alt="Screenshot 2024-12-17 at 16 50 48"
src="https://github.com/user-attachments/assets/61137993-ec2c-4754-9f9a-7559d1b21d89"
/>

Release Notes:

- N/A
2024-12-17 16:57:47 -05:00
Marshall Bowers
63d8a43f9d assistant2: Fix file context picker (#22163)
This PR fixes an issue with the Assistant2 file context picker where
files weren't being attached properly.

We needed to retrieve the files from the worktree without the worktree
root name in the file path.

Release Notes:

- N/A

---------

Co-authored-by: Max <max@zed.dev>
2024-12-17 16:53:56 -05:00
Nathan Sobo
81c118d67d Store focus handles in AppContext instead of Window (#22158)
Previously, each window stored its own collection of focus handles. This
meant that to create a focus handle, you needed to have access to a
Window. I'm working on a simplification to gpui's context types that
removes `WindowContext` and `ViewContext` in favor of passing a window
reference explicitly when rendering or handling events. You'll still
need a window to manipulate focus, but it will be helpful to be able to
create focus handles without a window.

cc @mgsloan 

Release Notes:

- N/A
2024-12-17 14:41:00 -07:00
Marshall Bowers
e1ca5ed836 zed: Fix formatting in workspace initialization (#22152)
This PR fixes some formatting issues in the workspace initialization
code that stemmed from certain constructs causing `rustfmt` to bail out
of the formatting.

The bulk of the content of `initialize_workspace` has been factored out
into functions, as having nested closures within closures seems to be
the primary cause of `rustfmt` being unhappy.

Release Notes:

- N/A
2024-12-17 16:34:52 -05:00
Peter Tripp
fa1b1c6aff docs: Fix indentation of JSON example lsp settings (#22162) 2024-12-17 16:09:13 -05:00
Peter Tripp
0511f9268b Add tooltip for Markdown Preview copy code button (#22057) 2024-12-17 14:46:38 -05:00
Marshall Bowers
70f82f84c6 telemetry: Fix license symlink (#22153)
This PR fixes the `LICENSE-GPL` symlink in the `telemetry` crate.

Release Notes:

- N/A
2024-12-17 14:29:04 -05:00
Marshall Bowers
1c4868979d Allow the use of both Assistants when in the assistant2 feature flag (#22150)
This PR makes it so both Assistant panels are visible when in the
`assistant2` feature flag.

This way folks can continue using Assistant1 if Assistant2 isn't meeting
their needs.

Right now they are shown as two different panels shown in the status bar
(although using the same icon), but this is just a temporary state until
we can surface the Assistant1 functionality in Assistant2 somehow.

Note that the inline assist will always use the Assistant2 panel.

Release Notes:

- N/A
2024-12-17 14:23:57 -05:00
Danilo Leal
c86cf2c3e1 zeta: Refine visuals for the completion popover (#22142)
Most notably, trying out a different icon and adding the "Completion"
word to the side of the provider name.

<img width="800" alt="Screenshot 2024-12-17 at 13 04 55"
src="https://github.com/user-attachments/assets/6dcaa699-f358-4242-9812-e1668f426207"
/>

Release Notes:

- N/A
2024-12-17 15:45:58 -03:00
Conrad Irwin
7425d242bc Add telemetry::event! (#22146)
CC @JosephTLyons

Release Notes:

- N/A
2024-12-17 11:39:18 -07:00
Marshall Bowers
b17f2089a2 assistant2: Sketch in directory context picker (#22148)
This PR sketches in the structure for the directory context picker.

Waiting on implementing the actual behavior until we fix the issues with
the file context picker.

Release Notes:

- N/A
2024-12-17 13:02:46 -05:00
Marshall Bowers
68e3d79847 assistant2: Use ContextKind to match on ContextPicker entries (#22143)
This PR updates the `ContextPicker` entries to match on the
`ContextKind` instead of using strings.

Release Notes:

- N/A
2024-12-17 11:43:27 -05:00
Carlos Kieliszewski
ed3e647ed7 editor: Add horizontal scrollbar (#19495)
![editor_scrollbars](https://github.com/user-attachments/assets/76c26776-8fe4-47f8-9c79-9add7d7d2151)

Closes #4427 

Release Notes:

- Added a horizontal scrollbar to the editor panel
- Added `axis` option to `scrollbar` in the Zed configuration, which can
forcefully disable either the horizontal or vertical scrollbar
- Added `horizontal_scroll_margin` equivalent to
`vertical_scroll_margin` in the Zed configuration

Rough Edges:

This feature seems mostly stable from my testing. I've been using a
development build for about a week with no issues. Any feedback would be
appreciated. There are a few things to note as well:

1. Scrolling to the lower right occasionally causes scrollbar clipping
on my end, but it isn't consistent and it isn't major. Some more testing
would definitely be a good idea. [FIXED]
2. Documentation may need to be modified
3. I added an `AxisPair` type to the `editor` crate to manage values
that have a horizontal and vertical variant. I'm not sure if that's the
optimal way to do it, but I didn't see a good alternative. The `Point`
type would technically work, but it may cause confusion.

---------

Co-authored-by: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com>
2024-12-17 17:24:59 +01:00
Marshall Bowers
6fa5a17586 assistant2: Only add context if it is non-empty (#22141)
This PR fixes an issue where we were attaching empty message content
even if there was no context.

Release Notes:

- N/A
2024-12-17 11:09:10 -05:00
Marshall Bowers
a6b717b97b assistant2: Add spacing between historical thread entries (#22140)
This PR adds spacing between entries in the historical threads view:

<img width="1371" alt="Screenshot 2024-12-17 at 10 51 26 AM"
src="https://github.com/user-attachments/assets/f66d69e9-eb59-4c76-b3b5-c4d60190c3cc"
/>

Release Notes:

- N/A
2024-12-17 11:03:09 -05:00
572 changed files with 42719 additions and 29701 deletions

View File

@@ -18,8 +18,11 @@ body:
- type: textarea
id: environment
attributes:
label: Environment
description: Run the `copy system specs into clipboard` command palette action and paste the output in the field below. If you are unable to run the command, please include your Zed version and release channel, operating system and version, RAM amount, and architecture.
label: Zed Version and System Specs
description: Zed version, release channel, architecture (x86_64 or aarch64), OS (macOS version / Linux distro and version) and RAM amount.
placeholder: |
<!-- In Zed run `copy system specs into clipboard` from the Zed command palette and paste here. -->
<!-- Alternatively spawn `request feature` and this field will be autopopulated -->
validations:
required: true
- type: textarea

View File

@@ -20,8 +20,13 @@ body:
- type: textarea
id: environment
attributes:
label: Environment
description: Run the `copy system specs into clipboard` command palette action and paste the output in the field below. If you are unable to run the command, please include your Zed version and release channel, operating system and version, RAM amount, and architecture.
label: Zed Version and System Specs
description: Zed version, release channel, architecture (x86_64 or aarch64), OS (macOS version / Linux distro and version) and RAM amount.
placeholder: |
<!-- In Zed run `copy system specs into clipboard` from the Zed command palette and paste here. -->
<!-- Alternatively spawn `file bug report` and this field will be autopopulated -->
<!-- If Zed won't launch, include the equivalent with other relevant details (e.g. video card driver version for display bugs, etc) -->
<!-- Zed Version: 0.xxx.x; Channel: Stable, OS: macOS xx.xx, RAM: XXGB, Architecture: x86_64"
validations:
required: true
- type: textarea

View File

@@ -7,14 +7,10 @@ on:
- "v[0-9]+.[0-9]+.x"
tags:
- "v*"
paths-ignore:
- "docs/**"
pull_request:
branches:
- "**"
paths-ignore:
- "docs/**/*"
- ".github/workflows/community_*"
merge_group:
concurrency:
# Allow only one workflow per any non-`main` branch.
@@ -28,6 +24,23 @@ env:
RUSTFLAGS: "-D warnings"
jobs:
check_docs_only:
runs-on: ubuntu-latest
outputs:
docs_only: ${{ steps.check_changes.outputs.docs_only }}
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
with:
fetch-depth: 0
- name: Check for non-docs changes
id: check_changes
run: |
if git diff --name-only ${{ github.event.pull_request.base.sha }} ${{ github.sha }} | grep -qvE '^docs/'; then
echo "docs_only=false" >> $GITHUB_OUTPUT
else
echo "docs_only=true" >> $GITHUB_OUTPUT
fi
migration_checks:
name: Check Postgres and Protobuf migrations, mergability
if: github.repository_owner == 'zed-industries'
@@ -81,6 +94,10 @@ jobs:
- name: Checkout repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
# To support writing comments that they will certainly be revisited.
- name: Check for todo! and FIXME comments
run: script/check-todos
- name: Run style checks
uses: ./.github/actions/check_style
@@ -96,6 +113,7 @@ jobs:
runs-on:
- self-hosted
- test
needs: check_docs_only
steps:
- name: Checkout repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
@@ -103,29 +121,35 @@ jobs:
clean: false
- name: cargo clippy
if: needs.check_docs_only.outputs.docs_only == 'false'
run: ./script/clippy
- name: Check unused dependencies
if: needs.check_docs_only.outputs.docs_only == 'false'
uses: bnjbvr/cargo-machete@main
- name: Check licenses
if: needs.check_docs_only.outputs.docs_only == 'false'
run: |
script/check-licenses
script/generate-licenses /tmp/zed_licenses_output
- name: Check for new vulnerable dependencies
if: github.event_name == 'pull_request'
if: github.event_name == 'pull_request' && needs.check_docs_only.outputs.docs_only == 'false'
uses: actions/dependency-review-action@3b139cfc5fae8b618d3eae3675e383bb1769c019 # v4
with:
license-check: false
- name: Run tests
if: needs.check_docs_only.outputs.docs_only == 'false'
uses: ./.github/actions/run_tests
- name: Build collab
if: needs.check_docs_only.outputs.docs_only == 'false'
run: cargo build -p collab
- name: Build other binaries and features
if: needs.check_docs_only.outputs.docs_only == 'false'
run: |
cargo build --workspace --bins --all-features
cargo check -p gpui --features "macos-blade"
@@ -139,6 +163,7 @@ jobs:
if: github.repository_owner == 'zed-industries'
runs-on:
- buildjet-16vcpu-ubuntu-2204
needs: check_docs_only
steps:
- name: Add Rust to the PATH
run: echo "$HOME/.cargo/bin" >> $GITHUB_PATH
@@ -149,21 +174,26 @@ jobs:
clean: false
- name: Cache dependencies
if: needs.check_docs_only.outputs.docs_only == 'false'
uses: swatinem/rust-cache@82a92a6e8fbeee089604da2575dc567ae9ddeaab # v2
with:
save-if: ${{ github.ref == 'refs/heads/main' }}
cache-provider: "buildjet"
- name: Install Linux dependencies
if: needs.check_docs_only.outputs.docs_only == 'false'
run: ./script/linux
- name: cargo clippy
if: needs.check_docs_only.outputs.docs_only == 'false'
run: ./script/clippy
- name: Run tests
if: needs.check_docs_only.outputs.docs_only == 'false'
uses: ./.github/actions/run_tests
- name: Build other binaries and features
if: needs.check_docs_only.outputs.docs_only == 'false'
run: |
cargo build -p zed
cargo check -p workspace
@@ -174,6 +204,7 @@ jobs:
if: github.repository_owner == 'zed-industries'
runs-on:
- buildjet-16vcpu-ubuntu-2204
needs: check_docs_only
steps:
- name: Add Rust to the PATH
run: echo "$HOME/.cargo/bin" >> $GITHUB_PATH
@@ -184,15 +215,18 @@ jobs:
clean: false
- name: Cache dependencies
if: needs.check_docs_only.outputs.docs_only == 'false'
uses: swatinem/rust-cache@82a92a6e8fbeee089604da2575dc567ae9ddeaab # v2
with:
save-if: ${{ github.ref == 'refs/heads/main' }}
cache-provider: "buildjet"
- name: Install Clang & Mold
if: needs.check_docs_only.outputs.docs_only == 'false'
run: ./script/remote-server && ./script/install-mold 2.34.0
- name: Build Remote Server
if: needs.check_docs_only.outputs.docs_only == 'false'
run: cargo build -p remote_server
# todo(windows): Actually run the tests
@@ -201,6 +235,7 @@ jobs:
name: (Windows) Run Clippy and tests
if: github.repository_owner == 'zed-industries'
runs-on: hosted-windows-1
needs: check_docs_only
steps:
# more info here:- https://github.com/rust-lang/cargo/issues/13020
- name: Enable longer pathnames for git
@@ -211,16 +246,19 @@ jobs:
clean: false
- name: Cache dependencies
if: needs.check_docs_only.outputs.docs_only == 'false'
uses: swatinem/rust-cache@82a92a6e8fbeee089604da2575dc567ae9ddeaab # v2
with:
save-if: ${{ github.ref == 'refs/heads/main' }}
cache-provider: "github"
- name: cargo clippy
if: needs.check_docs_only.outputs.docs_only == 'false'
# Windows can't run shell scripts, so we need to use `cargo xtask`.
run: cargo xtask clippy
- name: Build Zed
if: needs.check_docs_only.outputs.docs_only == 'false'
run: cargo build
bundle-mac:
@@ -289,14 +327,14 @@ jobs:
mv target/x86_64-apple-darwin/release/Zed.dmg target/x86_64-apple-darwin/release/Zed-x86_64.dmg
- name: Upload app bundle (aarch64) to workflow run if main branch or specific label
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4
if: ${{ github.ref == 'refs/heads/main' }} || contains(github.event.pull_request.labels.*.name, 'run-bundling') }}
with:
name: Zed_${{ github.event.pull_request.head.sha || github.sha }}-aarch64.dmg
path: target/aarch64-apple-darwin/release/Zed-aarch64.dmg
- name: Upload app bundle (x86_64) to workflow run if main branch or specific label
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4
if: ${{ github.ref == 'refs/heads/main' }} || contains(github.event.pull_request.labels.*.name, 'run-bundling') }}
with:
name: Zed_${{ github.event.pull_request.head.sha || github.sha }}-x86_64.dmg
@@ -326,6 +364,8 @@ jobs:
env:
ZED_CLIENT_CHECKSUM_SEED: ${{ secrets.ZED_CLIENT_CHECKSUM_SEED }}
ZED_CLOUD_PROVIDER_ADDITIONAL_MODELS_JSON: ${{ secrets.ZED_CLOUD_PROVIDER_ADDITIONAL_MODELS_JSON }}
DIGITALOCEAN_SPACES_ACCESS_KEY: ${{ secrets.DIGITALOCEAN_SPACES_ACCESS_KEY }}
DIGITALOCEAN_SPACES_SECRET_KEY: ${{ secrets.DIGITALOCEAN_SPACES_SECRET_KEY }}
steps:
- name: Checkout repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
@@ -345,7 +385,7 @@ jobs:
run: script/bundle-linux
- name: Upload Linux bundle to workflow run if main branch or specific label
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4
if: ${{ github.ref == 'refs/heads/main' }} || contains(github.event.pull_request.labels.*.name, 'run-bundling') }}
with:
name: zed-${{ github.event.pull_request.head.sha || github.sha }}-x86_64-unknown-linux-gnu.tar.gz
@@ -372,6 +412,8 @@ jobs:
env:
ZED_CLIENT_CHECKSUM_SEED: ${{ secrets.ZED_CLIENT_CHECKSUM_SEED }}
ZED_CLOUD_PROVIDER_ADDITIONAL_MODELS_JSON: ${{ secrets.ZED_CLOUD_PROVIDER_ADDITIONAL_MODELS_JSON }}
DIGITALOCEAN_SPACES_ACCESS_KEY: ${{ secrets.DIGITALOCEAN_SPACES_ACCESS_KEY }}
DIGITALOCEAN_SPACES_SECRET_KEY: ${{ secrets.DIGITALOCEAN_SPACES_SECRET_KEY }}
steps:
- name: Checkout repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
@@ -391,7 +433,7 @@ jobs:
run: script/bundle-linux
- name: Upload Linux bundle to workflow run if main branch or specific label
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4
if: ${{ github.ref == 'refs/heads/main' }} || contains(github.event.pull_request.labels.*.name, 'run-bundling') }}
with:
name: zed-${{ github.event.pull_request.head.sha || github.sha }}-aarch64-unknown-linux-gnu.tar.gz

View File

@@ -18,7 +18,7 @@ jobs:
fi
echo "::set-output name=URL::$URL"
- name: Get content
uses: 2428392/gh-truncate-string-action@e6b5885fb83c81ca9a700a91b079baec2133be3e # v1.4.0
uses: 2428392/gh-truncate-string-action@b3ff790d21cf42af3ca7579146eedb93c8fb0757 # v1.4.1
id: get-content
with:
stringToTruncate: |

View File

@@ -7,6 +7,7 @@ on:
push:
branches:
- main
merge_group:
jobs:
check_formatting:

566
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -117,6 +117,7 @@ members = [
"crates/tab_switcher",
"crates/task",
"crates/tasks_ui",
"crates/telemetry",
"crates/telemetry_events",
"crates/terminal",
"crates/terminal_view",
@@ -305,6 +306,7 @@ supermaven_api = { path = "crates/supermaven_api" }
tab_switcher = { path = "crates/tab_switcher" }
task = { path = "crates/task" }
tasks_ui = { path = "crates/tasks_ui" }
telemetry = { path = "crates/telemetry" }
telemetry_events = { path = "crates/telemetry_events" }
terminal = { path = "crates/terminal" }
terminal_view = { path = "crates/terminal_view" }
@@ -335,7 +337,7 @@ zeta = { path = "crates/zeta" }
#
aho-corasick = "1.1"
alacritty_terminal = { git = "https://github.com/alacritty/alacritty", rev = "91d034ff8b53867143c005acfaa14609147c9a2c" }
alacritty_terminal = "0.24"
any_vec = "0.14"
anyhow = "1.0.86"
arrayvec = { version = "0.7.4", features = ["serde"] }
@@ -353,13 +355,13 @@ async-watch = "0.3.1"
async_zip = { version = "0.0.17", features = ["deflate", "deflate64"] }
base64 = "0.22"
bitflags = "2.6.0"
blade-graphics = { git = "https://github.com/kvark/blade", rev = "e142a3a5e678eb6a13e642ad8401b1f3aa38e969" }
blade-macros = { git = "https://github.com/kvark/blade", rev = "e142a3a5e678eb6a13e642ad8401b1f3aa38e969" }
blade-util = { git = "https://github.com/kvark/blade", rev = "e142a3a5e678eb6a13e642ad8401b1f3aa38e969" }
blade-graphics = { git = "https://github.com/kvark/blade", rev = "091a8401033847bb9b6ace3fcf70448d069621c5" }
blade-macros = { git = "https://github.com/kvark/blade", rev = "091a8401033847bb9b6ace3fcf70448d069621c5" }
blade-util = { git = "https://github.com/kvark/blade", rev = "091a8401033847bb9b6ace3fcf70448d069621c5" }
blake3 = "1.5.3"
bytes = "1.0"
cargo_metadata = "0.19"
cargo_toml = "0.20"
cargo_toml = "0.21"
chrono = { version = "0.4", features = ["serde"] }
clap = { version = "4.4", features = ["derive"] }
cocoa = "0.26"
@@ -383,7 +385,7 @@ futures-lite = "1.13"
git2 = { version = "0.19", default-features = false }
globset = "0.4"
handlebars = "4.3"
heed = { version = "0.20.1", features = ["read-txn-no-tls"] }
heed = { version = "0.21.0", features = ["read-txn-no-tls"] }
hex = "0.4.3"
html5ever = "0.27.0"
hyper = "0.14"
@@ -399,14 +401,13 @@ 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 }
livekit = { git = "https://github.com/zed-industries/livekit-rust-sdks", rev="060964da10574cd9bf06463a53bf6e0769c5c45e", 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.9.0" }
nix = "0.29"
num-format = "0.4.4"
once_cell = "1.19.0"
ordered-float = "2.1.1"
palette = { version = "0.7.5", default-features = false, features = ["std"] }
parking_lot = "0.12.1"
@@ -470,7 +471,7 @@ sys-locale = "0.3.1"
sysinfo = "0.31.0"
tempfile = "3.9.0"
thiserror = "1.0.29"
tiktoken-rs = "0.5.9"
tiktoken-rs = "0.6.0"
time = { version = "0.3", features = [
"macros",
"parsing",
@@ -496,7 +497,7 @@ tree-sitter-heex = { git = "https://github.com/zed-industries/tree-sitter-heex",
tree-sitter-diff = "0.1.0"
tree-sitter-html = "0.20"
tree-sitter-jsdoc = "0.23"
tree-sitter-json = "0.23"
tree-sitter-json = "0.24"
tree-sitter-md = { git = "https://github.com/tree-sitter-grammars/tree-sitter-markdown", rev = "9a23c1a96c0513d8fc6520972beedd419a973539" }
tree-sitter-python = "0.23"
tree-sitter-regex = "0.23"
@@ -523,6 +524,7 @@ wasmtime-wasi = "24"
which = "6.0.0"
wit-component = "0.201"
zstd = "0.11"
metal = "0.30"
[workspace.dependencies.async-stripe]
git = "https://github.com/zed-industries/async-stripe"

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="M11.7633 4.2078H4.23674L4.3551 5.5189H10.1429L9.99592 6.87645H6.20408L6.33877 8.16255H9.86939L9.66122 9.92379L8 10.3275L6.3102 9.92021L6.20408 8.86633H4.7102L4.87755 10.7955L8 11.6457L11.0694 10.8812L11.7633 4.2078ZM2 2H14L12.9061 12.7818L7.98775 14L3.09388 12.7818L2 2Z" fill="black"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M12.58 2H2.5V12.08C2.5 12.5892 2.70229 13.0776 3.06235 13.4376C3.42242 13.7977 3.91078 14 4.42 14H12.58C13.0892 14 13.5776 13.7977 13.9376 13.4376C14.2977 13.0776 14.5 12.5892 14.5 12.08V3.92C14.5 3.41078 14.2977 2.92242 13.9376 2.56235C13.5776 2.20229 13.0892 2 12.58 2ZM3.358 11.6285C3.34615 12.6668 3.96437 13.2311 4.96636 13.232H4.96621C6.06429 13.2456 6.70951 12.4798 6.63088 11.3867H5.48085C5.4899 11.601 5.47243 11.8974 5.36026 12.0313C5.27992 12.1441 5.16183 12.2005 5.00645 12.2005C4.67402 12.1952 4.50788 11.9534 4.50788 11.4753V9.19488C4.50788 8.94247 4.54407 8.75168 4.61645 8.62283C4.73423 8.38524 5.17961 8.3584 5.34825 8.58663C5.47804 8.71252 5.48974 9.04683 5.48101 9.26757H6.63104C6.66099 8.70582 6.53494 8.10381 6.20079 7.80913C5.65853 7.23521 4.37403 7.26765 3.82039 7.80102C3.51213 8.07495 3.358 8.47525 3.358 9.00159V11.6285ZM7.04116 11.3867C7.01043 12.4573 7.50713 13.2473 8.61739 13.232L8.61723 13.2317C10.1571 13.2967 10.5874 11.592 9.96023 10.4759C9.74995 10.1097 9.16994 9.80702 8.71379 9.62981C8.36155 9.46772 8.21038 9.3086 8.20711 8.92079C8.20711 8.55559 8.35983 8.37291 8.66543 8.37291C8.83688 8.37291 8.95357 8.42939 9.01519 8.54217C9.10317 8.6754 9.12454 9.0409 9.11565 9.26742H10.1612C10.1866 8.71627 10.0554 8.11739 9.75509 7.81303C9.26822 7.22257 7.99791 7.24909 7.5115 7.82504C7.0109 8.29179 6.97783 9.4437 7.3346 9.96848C7.49278 10.205 7.75143 10.409 8.1107 10.5809C8.15897 10.6051 8.21552 10.6314 8.27633 10.6598C8.53247 10.7792 8.86416 10.9338 8.97119 11.1046C9.16073 11.3241 9.13593 11.8913 9.00333 12.0877C8.9336 12.1952 8.81285 12.2489 8.64141 12.2489C8.25703 12.2785 8.09666 11.8534 8.12677 11.3867H7.04116ZM10.5474 11.3867C10.5167 12.4573 11.0134 13.2473 12.1236 13.232L12.1235 13.2317C13.6634 13.2967 14.0936 11.592 13.4665 10.4759C13.2562 10.1097 12.6762 9.80702 12.2201 9.62981C11.8678 9.46772 11.7166 9.3086 11.7134 8.92079C11.7134 8.55559 11.8661 8.37291 12.1717 8.37291C12.3431 8.37291 12.4598 8.42939 12.5214 8.54217C12.6094 8.6754 12.6308 9.0409 12.6219 9.26742H13.6674C13.6928 8.71627 13.5617 8.11739 13.2614 7.81303C12.7745 7.22257 11.5042 7.24909 11.0178 7.82504C10.5172 8.29179 10.4841 9.4437 10.8409 9.96848C10.999 10.205 11.2577 10.409 11.617 10.5809C11.6652 10.6051 11.7218 10.6314 11.7826 10.6598C12.0387 10.7792 12.3704 10.9338 12.4775 11.1046C12.667 11.3241 12.6422 11.8913 12.5096 12.0877C12.4399 12.1952 12.3191 12.2489 12.1477 12.2489C11.7633 12.2785 11.6029 11.8534 11.633 11.3867H10.5474Z" fill="black"/>
</svg>

Before

Width:  |  Height:  |  Size: 403 B

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

@@ -0,0 +1,5 @@
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M5.27772 1.38585L4.39187 4.07909C4.34653 4.21692 4.26946 4.34219 4.16685 4.44479C4.06425 4.5474 3.93898 4.62447 3.80115 4.66981L1.10791 5.55566L3.80115 6.44151C3.93898 6.48685 4.06425 6.56392 4.16685 6.66653C4.26946 6.76913 4.34653 6.8944 4.39187 7.03223L5.27772 9.72547L6.16357 7.03223C6.20891 6.8944 6.28598 6.76913 6.38859 6.66653C6.49119 6.56392 6.61646 6.48685 6.7543 6.44151L9.44753 5.55566L6.7543 4.66981C6.61646 4.62447 6.49119 4.5474 6.38859 4.44479C6.28598 4.34219 6.20891 4.21692 6.16357 4.07909L5.27772 1.38585Z" fill="black" fill-opacity="0.15" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M8.35938 12.3555C8.35938 12.0664 8.52734 11.8086 9.00781 11.3594L10.2031 10.2344C10.6094 9.85156 10.7891 9.60156 10.7891 9.34375C10.7891 9.05469 10.5781 8.85938 10.2734 8.85938C10.0391 8.85938 9.87109 8.95312 9.66406 9.21094C9.42578 9.50781 9.25391 9.60938 8.99219 9.60938C8.61719 9.60938 8.35156 9.35938 8.35156 9.01172C8.35156 8.25 9.26953 7.57812 10.3594 7.57812C11.4961 7.57812 12.3438 8.26172 12.3438 9.17969C12.3438 9.75391 12.0391 10.3008 11.418 10.8516L10.4961 11.6719V11.7344H11.8047C12.2578 11.7344 12.5391 11.9766 12.5391 12.3711C12.5391 12.7656 12.2656 13 11.8047 13H9.08203C8.65234 13 8.35938 12.7383 8.35938 12.3555Z" fill="black"/>
<path d="M11.0834 1.38585V3.71918M9.91675 2.55248H12.2501" stroke="black" stroke-opacity="0.75" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -1,4 +1,5 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8 8.9V11C5.93097 11 5.06903 11 3 11V10.4L8 5.6V5H3V7.1" stroke="black" stroke-width="1.5"/>
<path d="M11 5L13 8L11 11" stroke="black" stroke-width="1.5"/>
<path d="M7 8.9V11C5.34478 11 4.65522 11 3 11V10.4L7 5.6V5H3V7.1" stroke="black" stroke-width="1.5"/>
<path d="M12 5L14 8L12 11" stroke="black" stroke-width="1.5"/>
<path d="M10 6.5L11 8L10 9.5" stroke="black" stroke-width="1.5"/>
</svg>

Before

Width:  |  Height:  |  Size: 268 B

After

Width:  |  Height:  |  Size: 334 B

View File

@@ -260,6 +260,8 @@
"ctrl-f4": "pane::CloseActiveItem",
"alt-ctrl-t": ["pane::CloseInactiveItems", { "close_pinned": false }],
"alt-ctrl-shift-w": "workspace::CloseInactiveTabsAndPanes",
"ctrl-k e": ["pane::CloseItemsToTheLeft", { "close_pinned": false }],
"ctrl-k t": ["pane::CloseItemsToTheRight", { "close_pinned": false }],
"ctrl-k u": ["pane::CloseCleanItems", { "close_pinned": false }],
"ctrl-k w": ["pane::CloseAllItems", { "close_pinned": false }],
"ctrl-shift-f": "project_search::ToggleFocus",

View File

@@ -327,6 +327,8 @@
"cmd-w": "pane::CloseActiveItem",
"alt-cmd-t": ["pane::CloseInactiveItems", { "close_pinned": false }],
"ctrl-alt-cmd-w": "workspace::CloseInactiveTabsAndPanes",
"cmd-k e": ["pane::CloseItemsToTheLeft", { "close_pinned": false }],
"cmd-k t": ["pane::CloseItemsToTheRight", { "close_pinned": false }],
"cmd-k u": ["pane::CloseCleanItems", { "close_pinned": false }],
"cmd-k cmd-w": ["pane::CloseAllItems", { "close_pinned": false }],
"cmd-f": "project_search::ToggleFocus",
@@ -448,7 +450,6 @@
"cmd-s": "workspace::Save",
"cmd-k s": "workspace::SaveWithoutFormat",
"cmd-shift-s": "workspace::SaveAs",
"cmd-n": "workspace::NewFile",
"cmd-shift-n": "workspace::NewWindow",
"ctrl-`": "terminal_panel::ToggleFocus",
"cmd-1": ["workspace::ActivatePane", 0],
@@ -495,6 +496,7 @@
"context": "Workspace && !Terminal",
"use_key_equivalents": true,
"bindings": {
"cmd-n": "workspace::NewFile",
"cmd-shift-r": "task::Spawn",
"cmd-alt-r": "task::Rerun",
"ctrl-alt-shift-r": ["task::Spawn", { "reveal_target": "center" }]
@@ -761,6 +763,7 @@
"cmd-v": "terminal::Paste",
"cmd-a": "editor::SelectAll",
"cmd-k": "terminal::Clear",
"cmd-n": "workspace::NewTerminal",
"ctrl-enter": "assistant::InlineAssist",
// Some nice conveniences
"cmd-backspace": ["terminal::SendText", "\u0015"],

View File

@@ -230,8 +230,8 @@
"ctrl-pageup": "pane::ActivatePrevItem",
"insert": "vim::InsertBefore",
// tree-sitter related commands
"[ x": "editor::SelectLargerSyntaxNode",
"] x": "editor::SelectSmallerSyntaxNode",
"[ x": "vim::SelectLargerSyntaxNode",
"] x": "vim::SelectSmallerSyntaxNode",
"] d": "editor::GoToDiagnostic",
"[ d": "editor::GoToPrevDiagnostic",
"] c": "editor::GoToHunk",

View File

@@ -160,6 +160,9 @@
/// Whether to show the signature help after completion or a bracket pair inserted.
/// If `auto_signature_help` is enabled, this setting will be treated as enabled also.
"show_signature_help_after_edits": false,
/// Whether to show the inline completions next to the completions provided by a language server.
/// Only has an effect if inline completion provider supports it.
"show_inline_completions_in_menu": true,
// Whether to show wrap guides (vertical rulers) in the editor.
// Setting this to true will show a guide at the 'preferred_line_length' value
// if 'soft_wrap' is set to 'preferred_line_length', and will show any
@@ -254,7 +257,14 @@
// Whether to show selected symbol occurrences in the scrollbar.
"selected_symbol": true,
// Whether to show diagnostic indicators in the scrollbar.
"diagnostics": true
"diagnostics": true,
/// Forcefully enable or disable the scrollbar for each axis
"axes": {
/// When false, forcefully disables the horizontal scrollbar. Otherwise, obey other settings.
"horizontal": true,
/// When false, forcefully disables the vertical scrollbar. Otherwise, obey other settings.
"vertical": true
}
},
// Enable middle-click paste on Linux.
"middle_click_paste": true,
@@ -304,6 +314,8 @@
"vertical_scroll_margin": 3,
// Whether to scroll when clicking near the edge of the visible text area.
"autoscroll_on_clicks": false,
// The number of characters to keep on either side when scrolling with the mouse
"horizontal_scroll_margin": 5,
// Scroll sensitivity multiplier. This multiplier is applied
// to both the horizontal and vertical delta values while scrolling.
"scroll_sensitivity": 1.0,
@@ -469,8 +481,10 @@
"default_width": 240
},
"chat_panel": {
// Whether to show the chat panel button in the status bar.
"button": true,
// When to show the chat panel button in the status bar.
// Can be 'never', 'always', or 'when_in_call',
// or a boolean (interpreted as 'never'/'always').
"button": "when_in_call",
// Where to the chat panel. Can be 'left' or 'right'.
"dock": "right",
// Default width of the chat panel.
@@ -1089,6 +1103,9 @@
"prettier": {
"allowed": true
}
},
"Zig": {
"language_servers": ["zls", "..."]
}
},
// Different settings for specific language models.

View File

@@ -4,8 +4,8 @@ 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 _,
InteractiveElement as _, Model, ModelContext, ParentElement as _, Render, SharedString,
StatefulInteractiveElement, Styled, Transformation, Window,
};
use language::{LanguageRegistry, LanguageServerBinaryStatus, LanguageServerId};
use lsp::LanguageServerName;
@@ -46,22 +46,25 @@ 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, &mut Window, &mut ModelContext<ActivityIndicator>)>,
>,
}
impl ActivityIndicator {
pub fn new(
workspace: &mut Workspace,
languages: Arc<LanguageRegistry>,
cx: &mut ViewContext<Workspace>,
) -> View<ActivityIndicator> {
window: &mut Window,
cx: &mut ModelContext<Workspace>,
) -> 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(|cx| {
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.update(&mut cx, |this: &mut ActivityIndicator, cx| {
this.statuses.retain(|s| s.name != name);
this.statuses.push(LspStatus { name, status });
cx.notify();
@@ -70,6 +73,7 @@ impl ActivityIndicator {
anyhow::Ok(())
})
.detach();
cx.observe(&project, |_, _, cx| cx.notify()).detach();
if let Some(auto_updater) = auto_updater.as_ref() {
@@ -84,13 +88,13 @@ impl ActivityIndicator {
}
});
cx.subscribe(&this, move |_, _, event, cx| match event {
cx.subscribe_in(&this, window, move |_, _, event, window, cx| match event {
Event::ShowError { lsp_name, error } => {
let create_buffer = project.update(cx, |project, cx| project.create_buffer(cx));
let project = project.clone();
let error = error.clone();
let lsp_name = lsp_name.clone();
cx.spawn(|workspace, mut cx| async move {
cx.spawn_in(window, |workspace, mut cx| async move {
let buffer = create_buffer.await?;
buffer.update(&mut cx, |buffer, cx| {
buffer.edit(
@@ -103,13 +107,14 @@ impl ActivityIndicator {
);
buffer.set_capability(language::Capability::ReadOnly, cx);
})?;
workspace.update(&mut cx, |workspace, cx| {
workspace.update_in(&mut cx, |workspace, window, 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(|cx| {
Editor::for_buffer(buffer, Some(project.clone()), window, cx)
})),
None,
true,
window,
cx,
);
})?;
@@ -123,7 +128,12 @@ impl ActivityIndicator {
this
}
fn show_error_message(&mut self, _: &ShowErrorMessage, cx: &mut ViewContext<Self>) {
fn show_error_message(
&mut self,
_: &ShowErrorMessage,
_: &mut Window,
cx: &mut ModelContext<Self>,
) {
self.statuses.retain(|status| {
if let LanguageServerBinaryStatus::Failed { error } = &status.status {
cx.emit(Event::ShowError {
@@ -139,7 +149,12 @@ impl ActivityIndicator {
cx.notify();
}
fn dismiss_error_message(&mut self, _: &DismissErrorMessage, cx: &mut ViewContext<Self>) {
fn dismiss_error_message(
&mut self,
_: &DismissErrorMessage,
_: &mut Window,
cx: &mut ModelContext<Self>,
) {
if let Some(updater) = &self.auto_updater {
updater.update(cx, |updater, cx| {
updater.dismiss_error(cx);
@@ -183,7 +198,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, cx: &mut ModelContext<Self>) -> Option<Content> {
// Show if any direnv calls failed
if let Some((&worktree_id, error)) = self.pending_environment_errors(cx).next() {
return Some(Content {
@@ -193,11 +208,11 @@ impl ActivityIndicator {
.into_any_element(),
),
message: error.0.clone(),
on_click: Some(Arc::new(move |this, cx| {
on_click: Some(Arc::new(move |this, window, cx| {
this.project.update(cx, |project, cx| {
project.remove_environment_error(cx, worktree_id);
});
cx.dispatch_action(Box::new(workspace::OpenLog));
window.dispatch_action(Box::new(workspace::OpenLog), cx);
})),
});
}
@@ -280,10 +295,10 @@ impl ActivityIndicator {
}
)
),
on_click: Some(Arc::new(move |this, cx| {
on_click: Some(Arc::new(move |this, window, cx| {
this.statuses
.retain(|status| !downloading.contains(&status.name));
this.dismiss_error_message(&DismissErrorMessage, cx)
this.dismiss_error_message(&DismissErrorMessage, window, cx)
})),
});
}
@@ -308,10 +323,10 @@ impl ActivityIndicator {
}
),
),
on_click: Some(Arc::new(move |this, cx| {
on_click: Some(Arc::new(move |this, window, cx| {
this.statuses
.retain(|status| !checking_for_update.contains(&status.name));
this.dismiss_error_message(&DismissErrorMessage, cx)
this.dismiss_error_message(&DismissErrorMessage, window, cx)
})),
});
}
@@ -336,8 +351,8 @@ impl ActivityIndicator {
acc
}),
),
on_click: Some(Arc::new(|this, cx| {
this.show_error_message(&Default::default(), cx)
on_click: Some(Arc::new(|this, window, cx| {
this.show_error_message(&Default::default(), window, cx)
})),
});
}
@@ -351,11 +366,11 @@ impl ActivityIndicator {
.into_any_element(),
),
message: format!("Formatting failed: {}. Click to see logs.", failure),
on_click: Some(Arc::new(|indicator, cx| {
on_click: Some(Arc::new(|indicator, window, cx| {
indicator.project.update(cx, |project, cx| {
project.reset_last_formatting_failure(cx);
});
cx.dispatch_action(Box::new(workspace::OpenLog));
window.dispatch_action(Box::new(workspace::OpenLog), cx);
})),
});
}
@@ -370,8 +385,8 @@ impl ActivityIndicator {
.into_any_element(),
),
message: "Checking for Zed updates…".to_string(),
on_click: Some(Arc::new(|this, cx| {
this.dismiss_error_message(&DismissErrorMessage, cx)
on_click: Some(Arc::new(|this, window, cx| {
this.dismiss_error_message(&DismissErrorMessage, window, cx)
})),
}),
AutoUpdateStatus::Downloading => Some(Content {
@@ -381,8 +396,8 @@ impl ActivityIndicator {
.into_any_element(),
),
message: "Downloading Zed update…".to_string(),
on_click: Some(Arc::new(|this, cx| {
this.dismiss_error_message(&DismissErrorMessage, cx)
on_click: Some(Arc::new(|this, window, cx| {
this.dismiss_error_message(&DismissErrorMessage, window, cx)
})),
}),
AutoUpdateStatus::Installing => Some(Content {
@@ -392,8 +407,8 @@ impl ActivityIndicator {
.into_any_element(),
),
message: "Installing Zed update…".to_string(),
on_click: Some(Arc::new(|this, cx| {
this.dismiss_error_message(&DismissErrorMessage, cx)
on_click: Some(Arc::new(|this, window, cx| {
this.dismiss_error_message(&DismissErrorMessage, window, cx)
})),
}),
AutoUpdateStatus::Updated { binary_path } => Some(Content {
@@ -403,7 +418,7 @@ impl ActivityIndicator {
let reload = workspace::Reload {
binary_path: Some(binary_path.clone()),
};
move |_, cx| workspace::reload(&reload, cx)
move |_, _, cx| workspace::reload(&reload, cx)
})),
}),
AutoUpdateStatus::Errored => Some(Content {
@@ -413,8 +428,8 @@ impl ActivityIndicator {
.into_any_element(),
),
message: "Auto update failed".to_string(),
on_click: Some(Arc::new(|this, cx| {
this.dismiss_error_message(&DismissErrorMessage, cx)
on_click: Some(Arc::new(|this, window, cx| {
this.dismiss_error_message(&DismissErrorMessage, window, cx)
})),
}),
AutoUpdateStatus::Idle => None,
@@ -432,8 +447,8 @@ impl ActivityIndicator {
.into_any_element(),
),
message: format!("Updating {extension_id} extension…"),
on_click: Some(Arc::new(|this, cx| {
this.dismiss_error_message(&DismissErrorMessage, cx)
on_click: Some(Arc::new(|this, window, cx| {
this.dismiss_error_message(&DismissErrorMessage, window, cx)
})),
});
}
@@ -442,8 +457,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,
window: &mut Window,
cx: &mut ModelContext<Self>,
) {
self.context_menu_handle.toggle(window, cx);
}
}
@@ -452,7 +471,7 @@ 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, _window: &mut Window, cx: &mut ModelContext<Self>) -> impl IntoElement {
let result = h_flex()
.id("activity-indicator")
.on_action(cx.listener(Self::show_error_message))
@@ -460,7 +479,7 @@ impl Render for ActivityIndicator {
let Some(content) = self.content_to_render(cx) else {
return result;
};
let this = cx.view().downgrade();
let this = cx.model().downgrade();
let truncate_content = content.message.len() > MAX_MESSAGE_LEN;
result.gap_2().child(
PopoverMenu::new("activity-indicator-popover")
@@ -480,24 +499,24 @@ impl Render for ActivityIndicator {
))
.size(LabelSize::Small),
)
.tooltip(move |cx| Tooltip::text(&content.message, cx))
.tooltip(Tooltip::text(content.message))
} 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| {
handler(this, cx);
this.on_click(cx.listener(move |this, _, window, cx| {
handler(this, window, cx);
}))
.cursor(CursorStyle::PointingHand)
}),
),
)
.anchor(gpui::Corner::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, _, cx| {
for work in strong_this.read(cx).pending_language_server_work(cx) {
has_work = true;
let this = this.clone();
@@ -513,7 +532,7 @@ impl Render for ActivityIndicator {
let token = work.progress_token.to_string();
let title = SharedString::from(title);
menu = menu.custom_entry(
move |_| {
move |_, _| {
h_flex()
.w_full()
.justify_between()
@@ -521,7 +540,7 @@ impl Render for ActivityIndicator {
.child(Icon::new(IconName::XCircle))
.into_any_element()
},
move |cx| {
move |_, cx| {
this.update(cx, |this, cx| {
this.project.update(cx, |project, cx| {
project.cancel_language_server_work(
@@ -554,5 +573,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>,
_window: &mut Window,
_: &mut ModelContext<Self>,
) {
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -17,7 +17,7 @@ use futures::{
channel::mpsc,
stream::{self, StreamExt},
};
use gpui::{prelude::*, AppContext, Model, SharedString, Task, TestAppContext, WeakView};
use gpui::{prelude::*, AppContext, Model, SharedString, Task, TestAppContext, WeakModel};
use language::{Buffer, BufferSnapshot, LanguageRegistry, LspAdapterDelegate};
use language_model::{LanguageModelCacheConfiguration, LanguageModelRegistry, Role};
use parking_lot::Mutex;
@@ -35,7 +35,7 @@ use std::{
sync::{atomic::AtomicBool, Arc},
};
use text::{network::Network, OffsetRangeExt as _, ReplicaId, ToOffset};
use ui::{IconName, WindowContext};
use ui::{IconName, Window};
use unindent::Unindent;
use util::{
test::{generate_marked_text, marked_text_ranges},
@@ -1642,8 +1642,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 Window,
_cx: &mut AppContext,
) -> Task<Result<Vec<ArgumentCompletion>>> {
Task::ready(Ok(vec![]))
}
@@ -1657,9 +1658,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 Window,
_cx: &mut AppContext,
) -> Task<SlashCommandResult> {
Task::ready(Ok(SlashCommandOutput {
text: format!("Executed fake command: {}", self.0),

View File

@@ -22,6 +22,7 @@ use paths::contexts_dir;
use project::Project;
use regex::Regex;
use rpc::AnyProtoClient;
use std::sync::LazyLock;
use std::{
cmp::Reverse,
ffi::OsStr,
@@ -319,7 +320,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(&cx.model(), &mut cx.to_async()));
self.advertise_contexts(cx);
} else {
self.client_subscription = None;
@@ -753,8 +754,8 @@ impl ContextStore {
continue;
}
let pattern = r" - \d+.zed.json$";
let re = Regex::new(pattern).unwrap();
static ASSISTANT_CONTEXT_REGEX: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r" - \d+.zed.json$").unwrap());
let metadata = fs.metadata(&path).await?;
if let Some((file_name, metadata)) = path
@@ -763,11 +764,15 @@ impl ContextStore {
.zip(metadata)
{
// This is used to filter out contexts saved by the new assistant.
if !re.is_match(file_name) {
if !ASSISTANT_CONTEXT_REGEX.is_match(file_name) {
continue;
}
if let Some(title) = re.replace(file_name, "").lines().next() {
if let Some(title) = ASSISTANT_CONTEXT_REGEX
.replace(file_name, "")
.lines()
.next()
{
contexts.push(SavedContextMetadata {
title: title.to_string(),
path,

File diff suppressed because it is too large Load Diff

View File

@@ -11,8 +11,8 @@ use futures::{
use fuzzy::StringMatchCandidate;
use gpui::{
actions, point, size, transparent_black, Action, AppContext, BackgroundExecutor, Bounds,
EventEmitter, Global, PromptLevel, ReadGlobal, Subscription, Task, TextStyle, TitlebarOptions,
UpdateGlobal, View, WindowBounds, WindowHandle, WindowOptions,
EventEmitter, Focusable, Global, Model, PromptLevel, ReadGlobal, Subscription, Task, TextStyle,
TitlebarOptions, UpdateGlobal, WindowBounds, WindowHandle, WindowOptions,
};
use heed::{
types::{SerdeBincode, SerdeJson, Str},
@@ -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::*, IconButtonShape, KeyBinding, ListItem, ListItemSpacing, ModelContext,
ParentElement, Render, SharedString, Styled, Tooltip, Window,
};
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, |_, window, _| window.activate_window())
.ok();
Task::ready(Ok(existing_window))
} else {
@@ -109,7 +109,9 @@ pub fn open_prompt_library(
window_bounds: Some(WindowBounds::Windowed(bounds)),
..Default::default()
},
|cx| cx.new_view(|cx| PromptLibrary::new(store, language_registry, cx)),
|window, cx| {
cx.new_model(|cx| PromptLibrary::new(store, language_registry, window, cx))
},
)
})?
})
@@ -121,14 +123,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 +160,7 @@ impl PickerDelegate for PromptPickerDelegate {
self.matches.len()
}
fn no_matches_text(&self, _cx: &mut WindowContext) -> SharedString {
fn no_matches_text(&self, _window: &mut Window, _cx: &mut AppContext) -> SharedString {
if self.store.prompt_count() == 0 {
"No prompts.".into()
} else {
@@ -170,7 +172,12 @@ 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,
_: &mut Window,
cx: &mut ModelContext<Picker<Self>>,
) {
self.selected_index = ix;
if let Some(prompt) = self.matches.get(self.selected_index) {
cx.emit(PromptPickerEvent::Selected {
@@ -179,14 +186,19 @@ impl PickerDelegate for PromptPickerDelegate {
}
}
fn placeholder_text(&self, _cx: &mut WindowContext) -> Arc<str> {
fn placeholder_text(&self, _window: &mut Window, _cx: &mut 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,
window: &mut Window,
cx: &mut ModelContext<Picker<Self>>,
) -> 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 {
cx.spawn_in(window, |this, mut cx| async move {
let (matches, selected_index) = cx
.background_executor()
.spawn(async move {
@@ -201,16 +213,16 @@ impl PickerDelegate for PromptPickerDelegate {
})
.await;
this.update(&mut cx, |this, cx| {
this.update_in(&mut cx, |this, window, cx| {
this.delegate.matches = matches;
this.delegate.set_selected_index(selected_index, cx);
this.delegate.set_selected_index(selected_index, window, cx);
cx.notify();
})
.ok();
})
}
fn confirm(&mut self, _secondary: bool, cx: &mut ViewContext<Picker<Self>>) {
fn confirm(&mut self, _secondary: bool, _: &mut Window, cx: &mut ModelContext<Picker<Self>>) {
if let Some(prompt) = self.matches.get(self.selected_index) {
cx.emit(PromptPickerEvent::Confirmed {
prompt_id: prompt.id,
@@ -218,13 +230,14 @@ impl PickerDelegate for PromptPickerDelegate {
}
}
fn dismissed(&mut self, _cx: &mut ViewContext<Picker<Self>>) {}
fn dismissed(&mut self, _window: &mut Window, _cx: &mut ModelContext<Picker<Self>>) {}
fn render_match(
&self,
ix: usize,
selected: bool,
cx: &mut ViewContext<Picker<Self>>,
_: &mut Window,
cx: &mut ModelContext<Picker<Self>>,
) -> Option<Self::ListItem> {
let prompt = self.matches.get(ix)?;
let default = prompt.default;
@@ -241,8 +254,8 @@ impl PickerDelegate for PromptPickerDelegate {
.toggle_state(true)
.icon_color(Color::Accent)
.shape(IconButtonShape::Square)
.tooltip(move |cx| Tooltip::text("Remove from Default Prompt", cx))
.on_click(cx.listener(move |_, _, cx| {
.tooltip(Tooltip::text("Remove from Default Prompt"))
.on_click(cx.listener(move |_, _, _, cx| {
cx.emit(PromptPickerEvent::ToggledDefault { prompt_id })
}))
}))
@@ -253,11 +266,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,8 +280,8 @@ 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| {
.tooltip(Tooltip::text("Delete Prompt"))
.on_click(cx.listener(move |_, _, _, cx| {
cx.emit(PromptPickerEvent::Deleted { prompt_id })
}))
.into_any_element()
@@ -278,17 +292,12 @@ impl PickerDelegate for PromptPickerDelegate {
.selected_icon(IconName::SparkleFilled)
.icon_color(if default { Color::Accent } else { Color::Muted })
.shape(IconButtonShape::Square)
.tooltip(move |cx| {
Tooltip::text(
if default {
"Remove from Default Prompt"
} else {
"Add to Default Prompt"
},
cx,
)
})
.on_click(cx.listener(move |_, _, cx| {
.tooltip(Tooltip::text(if default {
"Remove from Default Prompt"
} else {
"Add to Default Prompt"
}))
.on_click(cx.listener(move |_, _, _, cx| {
cx.emit(PromptPickerEvent::ToggledDefault { prompt_id })
})),
),
@@ -296,7 +305,12 @@ impl PickerDelegate for PromptPickerDelegate {
Some(element)
}
fn render_editor(&self, editor: &View<Editor>, cx: &mut ViewContext<Picker<Self>>) -> Div {
fn render_editor(
&self,
editor: &Model<Editor>,
_: &mut Window,
cx: &mut ModelContext<Picker<Self>>,
) -> Div {
h_flex()
.bg(cx.theme().colors().editor_background)
.rounded_md()
@@ -313,7 +327,8 @@ impl PromptLibrary {
fn new(
store: Arc<PromptStore>,
language_registry: Arc<LanguageRegistry>,
cx: &mut ViewContext<Self>,
window: &mut Window,
cx: &mut ModelContext<Self>,
) -> Self {
let delegate = PromptPickerDelegate {
store: store.clone(),
@@ -321,11 +336,11 @@ impl PromptLibrary {
matches: Vec::new(),
};
let picker = cx.new_view(|cx| {
let picker = Picker::uniform_list(delegate, cx)
let picker = cx.new_model(|cx| {
let picker = Picker::uniform_list(delegate, window, cx)
.modal(false)
.max_height(None);
picker.focus(cx);
picker.focus(window, cx);
picker
});
Self {
@@ -334,54 +349,63 @@ impl PromptLibrary {
prompt_editors: HashMap::default(),
active_prompt_id: None,
pending_load: Task::ready(()),
_subscriptions: vec![cx.subscribe(&picker, Self::handle_picker_event)],
_subscriptions: vec![cx.subscribe_in(&picker, window, Self::handle_picker_event)],
picker,
}
}
fn handle_picker_event(
&mut self,
_: View<Picker<PromptPickerDelegate>>,
_: &Model<Picker<PromptPickerDelegate>>,
event: &PromptPickerEvent,
cx: &mut ViewContext<Self>,
window: &mut Window,
cx: &mut ModelContext<Self>,
) {
match event {
PromptPickerEvent::Selected { prompt_id } => {
self.load_prompt(*prompt_id, false, cx);
self.load_prompt(*prompt_id, false, window, cx);
}
PromptPickerEvent::Confirmed { prompt_id } => {
self.load_prompt(*prompt_id, true, cx);
self.load_prompt(*prompt_id, true, window, cx);
}
PromptPickerEvent::ToggledDefault { prompt_id } => {
self.toggle_default_for_prompt(*prompt_id, cx);
self.toggle_default_for_prompt(*prompt_id, window, cx);
}
PromptPickerEvent::Deleted { prompt_id } => {
self.delete_prompt(*prompt_id, cx);
self.delete_prompt(*prompt_id, window, cx);
}
}
}
pub fn new_prompt(&mut self, cx: &mut ViewContext<Self>) {
pub fn new_prompt(&mut self, window: &mut Window, cx: &mut ModelContext<Self>) {
// 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, window, 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 {
self.picker
.update(cx, |picker, cx| picker.refresh(window, cx));
cx.spawn_in(window, |this, mut cx| async move {
save.await?;
this.update(&mut cx, |this, cx| this.load_prompt(prompt_id, true, cx))
this.update_in(&mut cx, |this, window, cx| {
this.load_prompt(prompt_id, true, window, 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,
window: &mut Window,
cx: &mut ModelContext<Self>,
) {
const SAVE_THROTTLE: Duration = Duration::from_millis(500);
if prompt_id.is_built_in() {
@@ -407,7 +431,7 @@ 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(cx.spawn_in(window, |this, mut cx| {
async move {
loop {
let title_and_body = this.update(&mut cx, |this, _| {
@@ -427,8 +451,9 @@ 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));
this.update_in(&mut cx, |this, window, cx| {
this.picker
.update(cx, |picker, cx| picker.refresh(window, cx));
cx.notify();
})?;
@@ -449,61 +474,77 @@ impl PromptLibrary {
}
}
pub fn delete_active_prompt(&mut self, cx: &mut ViewContext<Self>) {
pub fn delete_active_prompt(&mut self, window: &mut Window, cx: &mut ModelContext<Self>) {
if let Some(active_prompt_id) = self.active_prompt_id {
self.delete_prompt(active_prompt_id, cx);
self.delete_prompt(active_prompt_id, window, cx);
}
}
pub fn duplicate_active_prompt(&mut self, cx: &mut ViewContext<Self>) {
pub fn duplicate_active_prompt(&mut self, window: &mut Window, cx: &mut ModelContext<Self>) {
if let Some(active_prompt_id) = self.active_prompt_id {
self.duplicate_prompt(active_prompt_id, cx);
self.duplicate_prompt(active_prompt_id, window, cx);
}
}
pub fn toggle_default_for_active_prompt(&mut self, cx: &mut ViewContext<Self>) {
pub fn toggle_default_for_active_prompt(
&mut self,
window: &mut Window,
cx: &mut ModelContext<Self>,
) {
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, window, 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,
window: &mut Window,
cx: &mut ModelContext<Self>,
) {
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));
self.picker
.update(cx, |picker, cx| picker.refresh(window, cx));
cx.notify();
}
}
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,
window: &mut Window,
cx: &mut ModelContext<Self>,
) {
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, cx| window.focus(&editor.focus_handle(cx)));
}
self.set_active_prompt(Some(prompt_id), cx);
self.set_active_prompt(Some(prompt_id), window, 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 = cx.spawn_in(window, |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_in(&mut cx, |this, window, cx| match prompt {
Ok(prompt) => {
let title_editor = cx.new_view(|cx| {
let mut editor = Editor::auto_width(cx);
let title_editor = cx.new_model(|cx| {
let mut editor = Editor::auto_width(window, cx);
editor.set_placeholder_text("Untitled", cx);
editor.set_text(prompt_metadata.title.unwrap_or_default(), cx);
editor.set_text(prompt_metadata.title.unwrap_or_default(), window, 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), window, cx);
}
editor
});
let body_editor = cx.new_view(|cx| {
let body_editor = cx.new_model(|cx| {
let buffer = cx.new_model(|cx| {
let mut buffer = Buffer::local(prompt, cx);
buffer.set_language(markdown.log_err(), cx);
@@ -511,10 +552,10 @@ impl PromptLibrary {
buffer
});
let mut editor = Editor::for_buffer(buffer, None, cx);
let mut editor = Editor::for_buffer(buffer, None, window, 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), window, cx);
}
editor.set_soft_wrap_mode(SoftWrap::EditorWidth, cx);
editor.set_show_gutter(false, cx);
@@ -530,17 +571,29 @@ impl PromptLibrary {
),
)));
if focus {
editor.focus(cx);
window.focus(&editor.focus_handle(cx));
}
editor
});
let _subscriptions = vec![
cx.subscribe(&title_editor, move |this, editor, event, cx| {
this.handle_prompt_title_editor_event(prompt_id, editor, event, cx)
}),
cx.subscribe(&body_editor, move |this, editor, event, cx| {
this.handle_prompt_body_editor_event(prompt_id, editor, event, cx)
}),
cx.subscribe_in(
&title_editor,
window,
move |this, editor, event, window, cx| {
this.handle_prompt_title_editor_event(
prompt_id, editor, event, window, cx,
)
},
),
cx.subscribe_in(
&body_editor,
window,
move |this, editor, event, window, cx| {
this.handle_prompt_body_editor_event(
prompt_id, editor, event, window, cx,
)
},
),
];
this.prompt_editors.insert(
prompt_id,
@@ -554,8 +607,8 @@ impl PromptLibrary {
_subscriptions,
},
);
this.set_active_prompt(Some(prompt_id), cx);
this.count_tokens(prompt_id, cx);
this.set_active_prompt(Some(prompt_id), window, cx);
this.count_tokens(prompt_id, window, cx);
}
Err(error) => {
// TODO: we should show the error in the UI.
@@ -567,7 +620,12 @@ 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>,
window: &mut Window,
cx: &mut ModelContext<Self>,
) {
self.active_prompt_id = prompt_id;
self.picker.update(cx, |picker, cx| {
if let Some(prompt_id) = prompt_id {
@@ -585,19 +643,24 @@ impl PromptLibrary {
.iter()
.position(|mat| mat.id == prompt_id)
{
picker.set_selected_index(ix, true, cx);
picker.set_selected_index(ix, true, window, cx);
}
}
} else {
picker.focus(cx);
picker.focus(window, cx);
}
});
cx.notify();
}
pub fn delete_prompt(&mut self, prompt_id: PromptId, cx: &mut ViewContext<Self>) {
pub fn delete_prompt(
&mut self,
prompt_id: PromptId,
window: &mut Window,
cx: &mut ModelContext<Self>,
) {
if let Some(metadata) = self.store.metadata(prompt_id) {
let confirmation = cx.prompt(
let confirmation = window.prompt(
PromptLevel::Warning,
&format!(
"Are you sure you want to delete {}",
@@ -605,17 +668,19 @@ impl PromptLibrary {
),
None,
&["Delete", "Cancel"],
cx,
);
cx.spawn(|this, mut cx| async move {
cx.spawn_in(window, |this, mut cx| async move {
if confirmation.await.ok() == Some(0) {
this.update(&mut cx, |this, cx| {
this.update_in(&mut cx, |this, window, cx| {
if this.active_prompt_id == Some(prompt_id) {
this.set_active_prompt(None, cx);
this.set_active_prompt(None, window, 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));
this.picker
.update(cx, |picker, cx| picker.refresh(window, cx));
cx.notify();
})?;
}
@@ -625,7 +690,12 @@ impl PromptLibrary {
}
}
pub fn duplicate_prompt(&mut self, prompt_id: PromptId, cx: &mut ViewContext<Self>) {
pub fn duplicate_prompt(
&mut self,
prompt_id: PromptId,
window: &mut Window,
cx: &mut ModelContext<Self>,
) {
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 +725,38 @@ 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 {
self.picker
.update(cx, |picker, cx| picker.refresh(window, cx));
cx.spawn_in(window, |this, mut cx| async move {
save.await?;
this.update(&mut cx, |prompt_library, cx| {
prompt_library.load_prompt(new_id, true, cx)
this.update_in(&mut cx, |prompt_library, window, cx| {
prompt_library.load_prompt(new_id, true, window, cx)
})
})
.detach_and_log_err(cx);
}
}
fn focus_active_prompt(&mut self, _: &Tab, cx: &mut ViewContext<Self>) {
fn focus_active_prompt(&mut self, _: &Tab, window: &mut Window, cx: &mut ModelContext<Self>) {
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, cx| window.focus(&editor.focus_handle(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, window: &mut Window, cx: &mut ModelContext<Self>) {
self.picker
.update(cx, |picker, cx| picker.focus(window, cx));
}
pub fn inline_assist(&mut self, action: &InlineAssist, cx: &mut ViewContext<Self>) {
pub fn inline_assist(
&mut self,
action: &InlineAssist,
window: &mut Window,
cx: &mut ModelContext<Self>,
) {
let Some(active_prompt_id) = self.active_prompt_id else {
cx.propagate();
return;
@@ -693,15 +770,15 @@ 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| {
cx.activate_window();
workspace.focus_panel::<AssistantPanel>(cx)
.update(cx, |workspace, window, cx| {
window.activate_window();
workspace.focus_panel::<AssistantPanel>(window, cx)
})
.ok()
.flatten();
@@ -713,18 +790,28 @@ 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,
window: &mut Window,
cx: &mut ModelContext<Self>,
) {
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);
window.focus(&prompt_editor.body_editor.focus_handle(cx));
}
}
}
fn move_up_from_body(&mut self, _: &editor::actions::MoveUp, cx: &mut ViewContext<Self>) {
fn move_up_from_body(
&mut self,
_: &editor::actions::MoveUp,
window: &mut Window,
cx: &mut ModelContext<Self>,
) {
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);
window.focus(&prompt_editor.title_editor.focus_handle(cx));
}
}
}
@@ -732,18 +819,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>,
window: &mut Window,
cx: &mut ModelContext<Self>,
) {
match event {
EditorEvent::BufferEdited => {
self.save_prompt(prompt_id, cx);
self.count_tokens(prompt_id, cx);
self.save_prompt(prompt_id, window, cx);
self.count_tokens(prompt_id, window, cx);
}
EditorEvent::Blurred => {
title_editor.update(cx, |title_editor, cx| {
title_editor.change_selections(None, cx, |selections| {
title_editor.change_selections(None, window, cx, |selections| {
let cursor = selections.oldest_anchor().head();
selections.select_anchor_ranges([cursor..cursor]);
});
@@ -756,18 +844,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>,
window: &mut Window,
cx: &mut ModelContext<Self>,
) {
match event {
EditorEvent::BufferEdited => {
self.save_prompt(prompt_id, cx);
self.count_tokens(prompt_id, cx);
self.save_prompt(prompt_id, window, cx);
self.count_tokens(prompt_id, window, cx);
}
EditorEvent::Blurred => {
body_editor.update(cx, |body_editor, cx| {
body_editor.change_selections(None, cx, |selections| {
body_editor.change_selections(None, window, cx, |selections| {
let cursor = selections.oldest_anchor().head();
selections.select_anchor_ranges([cursor..cursor]);
});
@@ -777,7 +866,12 @@ impl PromptLibrary {
}
}
fn count_tokens(&mut self, prompt_id: PromptId, cx: &mut ViewContext<Self>) {
fn count_tokens(
&mut self,
prompt_id: PromptId,
window: &mut Window,
cx: &mut ModelContext<Self>,
) {
let Some(model) = LanguageModelRegistry::read_global(cx).active_model() else {
return;
};
@@ -785,13 +879,13 @@ 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 = cx.spawn_in(window, |this, mut cx| {
async move {
const DEBOUNCE_TIMEOUT: Duration = Duration::from_secs(1);
cx.background_executor().timer(DEBOUNCE_TIMEOUT).await;
let token_count = cx
.update(|cx| {
.update(|_, cx| {
model.count_tokens(
LanguageModelRequest {
messages: vec![LanguageModelRequestMessage {
@@ -819,7 +913,7 @@ impl PromptLibrary {
}
}
fn render_prompt_list(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
fn render_prompt_list(&mut self, cx: &mut ModelContext<Self>) -> impl IntoElement {
v_flex()
.id("prompt-list")
.capture_action(cx.listener(Self::focus_active_prompt))
@@ -839,16 +933,21 @@ impl PromptLibrary {
IconButton::new("new-prompt", IconName::Plus)
.style(ButtonStyle::Transparent)
.shape(IconButtonShape::Square)
.tooltip(move |cx| Tooltip::for_action("New Prompt", &NewPrompt, cx))
.on_click(|_, cx| {
cx.dispatch_action(Box::new(NewPrompt));
.tooltip(move |window, cx| {
Tooltip::for_action("New Prompt", &NewPrompt, window, cx)
})
.on_click(|_, window, cx| {
window.dispatch_action(Box::new(NewPrompt), cx);
}),
),
)
.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,
cx: &mut ModelContext<PromptLibrary>,
) -> gpui::Stateful<Div> {
div()
.w_2_3()
.h_full()
@@ -873,8 +972,8 @@ impl PromptLibrary {
.overflow_hidden()
.pl(DynamicSpacing::Base16.rems(cx))
.pt(DynamicSpacing::Base08.rems(cx))
.on_click(cx.listener(move |_, _, cx| {
cx.focus(&focus_handle);
.on_click(cx.listener(move |_, _, window, _| {
window.focus(&focus_handle);
}))
.child(
h_flex()
@@ -957,7 +1056,7 @@ impl PromptLibrary {
h_flex()
.id("token_count")
.tooltip(move |cx| {
.tooltip(move |window, cx| {
let token_count =
token_count.clone();
@@ -976,6 +1075,7 @@ impl PromptLibrary {
.0)
.unwrap_or_default()
),
window,
cx,
)
})
@@ -995,11 +1095,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,
window,
cx,
)
})
@@ -1013,15 +1114,19 @@ impl PromptLibrary {
.style(ButtonStyle::Transparent)
.shape(IconButtonShape::Square)
.size(ButtonSize::Large)
.tooltip(move |cx| {
.tooltip(move |window, cx| {
Tooltip::for_action(
"Delete Prompt",
&DeletePrompt,
window,
cx,
)
})
.on_click(|_, cx| {
cx.dispatch_action(Box::new(DeletePrompt));
.on_click(|_, window, cx| {
window.dispatch_action(
Box::new(DeletePrompt),
cx,
);
})
.into_any_element()
})
@@ -1034,17 +1139,19 @@ impl PromptLibrary {
.style(ButtonStyle::Transparent)
.shape(IconButtonShape::Square)
.size(ButtonSize::Large)
.tooltip(move |cx| {
.tooltip(move |window, cx| {
Tooltip::for_action(
"Duplicate Prompt",
&DuplicatePrompt,
window,
cx,
)
})
.on_click(|_, cx| {
cx.dispatch_action(Box::new(
DuplicatePrompt,
));
.on_click(|_, window, cx| {
window.dispatch_action(
Box::new(DuplicatePrompt),
cx,
);
}),
)
.child(
@@ -1062,20 +1169,18 @@ impl PromptLibrary {
})
.shape(IconButtonShape::Square)
.size(ButtonSize::Large)
.tooltip(move |cx| {
Tooltip::text(
if prompt_metadata.default {
"Remove from Default Prompt"
} else {
"Add to Default Prompt"
},
.tooltip(Tooltip::text(
if prompt_metadata.default {
"Remove from Default Prompt"
} else {
"Add to Default Prompt"
},
))
.on_click(|_, window, cx| {
window.dispatch_action(
Box::new(ToggleDefaultPrompt),
cx,
)
})
.on_click(|_, cx| {
cx.dispatch_action(Box::new(
ToggleDefaultPrompt,
));
);
}),
),
),
@@ -1096,18 +1201,24 @@ 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, window: &mut Window, cx: &mut ModelContext<Self>) -> impl IntoElement {
let ui_font = theme::setup_ui_font(window, cx);
let theme = cx.theme().clone();
h_flex()
.id("prompt-manager")
.key_context("PromptLibrary")
.on_action(cx.listener(|this, &NewPrompt, cx| this.new_prompt(cx)))
.on_action(cx.listener(|this, &DeletePrompt, cx| this.delete_active_prompt(cx)))
.on_action(cx.listener(|this, &DuplicatePrompt, cx| this.duplicate_active_prompt(cx)))
.on_action(cx.listener(|this, &ToggleDefaultPrompt, cx| {
this.toggle_default_for_active_prompt(cx)
.on_action(cx.listener(|this, &NewPrompt, window, cx| this.new_prompt(window, cx)))
.on_action(
cx.listener(|this, &DeletePrompt, window, cx| {
this.delete_active_prompt(window, cx)
}),
)
.on_action(cx.listener(|this, &DuplicatePrompt, window, cx| {
this.duplicate_active_prompt(window, cx)
}))
.on_action(cx.listener(|this, &ToggleDefaultPrompt, window, cx| {
this.toggle_default_for_active_prompt(window, cx)
}))
.size_full()
.overflow_hidden()
@@ -1149,10 +1260,13 @@ impl Render for PromptLibrary {
Button::new("create-prompt", "New Prompt")
.full_width()
.key_binding(KeyBinding::for_action(
&NewPrompt, cx,
&NewPrompt, window,
))
.on_click(|_, cx| {
cx.dispatch_action(NewPrompt.boxed_clone())
.on_click(|_, window, cx| {
window.dispatch_action(
NewPrompt.boxed_clone(),
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, Model, ModelContext, Task, WeakModel, Window};
use language::{Anchor, Buffer, CodeLabel, Documentation, HighlightId, LanguageServerId, ToPoint};
use parking_lot::Mutex;
use project::CompletionIntent;
@@ -43,8 +43,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 {
@@ -57,8 +57,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))),
@@ -73,7 +73,8 @@ impl SlashCommandCompletionProvider {
command_name: &str,
command_range: Range<Anchor>,
name_range: Range<Anchor>,
cx: &mut WindowContext,
window: &mut Window,
cx: &mut AppContext,
) -> Task<Result<Vec<project::Completion>>> {
let slash_commands = self.slash_commands.clone();
let candidates = slash_commands
@@ -85,7 +86,7 @@ impl SlashCommandCompletionProvider {
let command_name = command_name.to_string();
let editor = self.editor.clone();
let workspace = self.workspace.clone();
cx.spawn(|mut cx| async move {
window.spawn(cx, |mut cx| async move {
let matches = match_strings(
&candidates,
&command_name,
@@ -96,7 +97,7 @@ impl SlashCommandCompletionProvider {
)
.await;
cx.update(|cx| {
cx.update(|_, cx| {
matches
.into_iter()
.filter_map(|mat| {
@@ -118,28 +119,31 @@ impl SlashCommandCompletionProvider {
let editor = editor.clone();
let workspace = workspace.clone();
Arc::new(
move |intent: CompletionIntent, cx: &mut WindowContext| {
if !requires_argument
&& (!accepts_arguments || intent.is_complete())
{
editor
.update(cx, |editor, cx| {
editor.run_command(
command_range.clone(),
&command_name,
&[],
true,
workspace.clone(),
cx,
);
})
.ok();
false
} else {
requires_argument || accepts_arguments
}
},
) as Arc<_>
move |intent: CompletionIntent,
window: &mut Window,
cx: &mut AppContext| {
if !requires_argument
&& (!accepts_arguments || intent.is_complete())
{
editor
.update(cx, |editor, cx| {
editor.run_command(
command_range.clone(),
&command_name,
&[],
true,
workspace.clone(),
window,
cx,
);
})
.ok();
false
} else {
requires_argument || accepts_arguments
}
},
) as Arc<_>
});
Some(project::Completion {
old_range: name_range.clone(),
@@ -149,6 +153,7 @@ impl SlashCommandCompletionProvider {
server_id: LanguageServerId(0),
lsp_completion: Default::default(),
confirm,
resolved: true,
})
})
.collect()
@@ -163,7 +168,8 @@ impl SlashCommandCompletionProvider {
command_range: Range<Anchor>,
argument_range: Range<Anchor>,
last_argument_range: Range<Anchor>,
cx: &mut WindowContext,
window: &mut Window,
cx: &mut AppContext,
) -> Task<Result<Vec<project::Completion>>> {
let new_cancel_flag = Arc::new(AtomicBool::new(false));
let mut flag = self.cancel_flag.lock();
@@ -174,6 +180,7 @@ impl SlashCommandCompletionProvider {
arguments,
new_cancel_flag.clone(),
self.workspace.clone(),
window,
cx,
);
let command_name: Arc<str> = command_name.into();
@@ -201,27 +208,30 @@ impl SlashCommandCompletionProvider {
let command_range = command_range.clone();
let command_name = command_name.clone();
move |intent: CompletionIntent, cx: &mut WindowContext| {
if new_argument.after_completion.run()
|| intent.is_complete()
{
editor
.update(cx, |editor, cx| {
editor.run_command(
command_range.clone(),
&command_name,
&completed_arguments,
true,
workspace.clone(),
cx,
);
})
.ok();
false
} else {
!new_argument.after_completion.run()
}
move |intent: CompletionIntent,
window: &mut Window,
cx: &mut AppContext| {
if new_argument.after_completion.run()
|| intent.is_complete()
{
editor
.update(cx, |editor, cx| {
editor.run_command(
command_range.clone(),
&command_name,
&completed_arguments,
true,
workspace.clone(),
window,
cx,
);
})
.ok();
false
} else {
!new_argument.after_completion.run()
}
}
}) as Arc<_>
});
@@ -242,6 +252,7 @@ impl SlashCommandCompletionProvider {
server_id: LanguageServerId(0),
lsp_completion: Default::default(),
confirm,
resolved: true,
}
})
.collect())
@@ -258,7 +269,8 @@ impl CompletionProvider for SlashCommandCompletionProvider {
buffer: &Model<Buffer>,
buffer_position: Anchor,
_: editor::CompletionContext,
cx: &mut ViewContext<Editor>,
window: &mut Window,
cx: &mut ModelContext<Editor>,
) -> Task<Result<Vec<project::Completion>>> {
let Some((name, arguments, command_range, last_argument_range)) =
buffer.update(cx, |buffer, _cx| {
@@ -313,10 +325,11 @@ impl CompletionProvider for SlashCommandCompletionProvider {
command_range,
argument_range,
last_argument_range,
window,
cx,
)
} else {
self.complete_command_name(&name, command_range, last_argument_range, cx)
self.complete_command_name(&name, command_range, last_argument_range, window, cx)
}
}
@@ -325,28 +338,20 @@ impl CompletionProvider for SlashCommandCompletionProvider {
_: Model<Buffer>,
_: Vec<usize>,
_: Rc<RefCell<Box<[project::Completion]>>>,
_: &mut ViewContext<Editor>,
_window: &mut Window,
_: &mut ModelContext<Editor>,
) -> Task<Result<bool>> {
Task::ready(Ok(true))
}
fn apply_additional_edits_for_completion(
&self,
_: Model<Buffer>,
_: project::Completion,
_: bool,
_: &mut ViewContext<Editor>,
) -> Task<Result<Option<language::Transaction>>> {
Task::ready(Ok(None))
}
fn is_completion_trigger(
&self,
buffer: &Model<Buffer>,
position: language::Anchor,
_text: &str,
_trigger_in_words: bool,
cx: &mut ViewContext<Editor>,
_: &mut Window,
cx: &mut ModelContext<Editor>,
) -> bool {
let buffer = buffer.read(cx);
let position = position.to_point(buffer);

View File

@@ -5,7 +5,7 @@ use assistant_slash_command::{
};
use feature_flags::FeatureFlag;
use futures::StreamExt;
use gpui::{AppContext, AsyncAppContext, Task, WeakView};
use gpui::{AppContext, AsyncAppContext, Task, WeakModel, Window};
use language::{CodeLabel, LspAdapterDelegate};
use language_model::{
LanguageModelCompletionEvent, LanguageModelRegistry, LanguageModelRequest,
@@ -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 Window,
cx: &mut 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.
@@ -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 Window,
cx: &mut 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 = window.spawn(cx, |cx| async move {
let summaries = project_index
.read_with(&cx, |project_index, cx| project_index.all_summaries(cx))?
.await?;

View File

@@ -4,7 +4,7 @@ use assistant_slash_command::{
SlashCommandResult,
};
use fs::Fs;
use gpui::{AppContext, Model, Task, WeakView};
use gpui::{AppContext, Model, Task, WeakModel};
use language::{BufferSnapshot, LspAdapterDelegate};
use project::{Project, ProjectPath};
use std::{
@@ -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 Window,
_cx: &mut AppContext,
) -> Task<Result<Vec<ArgumentCompletion>>> {
Task::ready(Err(anyhow!("this command does not require argument")))
}
@@ -122,9 +123,10 @@ 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 Window,
cx: &mut AppContext,
) -> Task<SlashCommandResult> {
let output = workspace.update(cx, |workspace, cx| {
let project = workspace.project().clone();

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, WeakModel, Window};
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 Window,
cx: &mut 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 Window,
cx: &mut AppContext,
) -> Task<SlashCommandResult> {
let server_id = self.server_id.clone();
let prompt_name = self.prompt.name.clone();

View File

@@ -4,7 +4,7 @@ use assistant_slash_command::{
ArgumentCompletion, SlashCommand, SlashCommandOutput, SlashCommandOutputSection,
SlashCommandResult,
};
use gpui::{Task, WeakView};
use gpui::{Task, WeakModel};
use language::{BufferSnapshot, LspAdapterDelegate};
use std::{
fmt::Write,
@@ -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 Window,
_cx: &mut AppContext,
) -> Task<Result<Vec<ArgumentCompletion>>> {
Task::ready(Err(anyhow!("this command does not require argument")))
}
@@ -47,9 +48,10 @@ 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 Window,
cx: &mut 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::{AppContext, Task, WeakModel, Window};
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 Window,
_cx: &mut 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 Window,
cx: &mut 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(),
window,
cx,
));
}

View File

@@ -4,7 +4,7 @@ use assistant_slash_command::{
SlashCommandResult,
};
use fuzzy::{PathMatch, StringMatchCandidate};
use gpui::{AppContext, Model, Task, View, WeakView};
use gpui::{AppContext, Model, Task, WeakModel};
use language::{
Anchor, BufferSnapshot, DiagnosticEntry, DiagnosticSeverity, LspAdapterDelegate,
OffsetRangeExt, ToOffset,
@@ -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,9 @@ impl SlashCommand for DiagnosticsSlashCommand {
self: Arc<Self>,
arguments: &[String],
cancellation_flag: Arc<AtomicBool>,
workspace: Option<WeakView<Workspace>>,
cx: &mut WindowContext,
workspace: Option<WeakModel<Workspace>>,
_: &mut Window,
cx: &mut 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 +173,10 @@ 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 Window,
cx: &mut AppContext,
) -> Task<SlashCommandResult> {
let Some(workspace) = workspace.upgrade() else {
return Task::ready(Err(anyhow!("workspace was dropped")));
@@ -184,7 +186,7 @@ impl SlashCommand for DiagnosticsSlashCommand {
let task = collect_diagnostics(workspace.read(cx).project().clone(), options, cx);
cx.spawn(move |_| async move {
window.spawn(cx, move |_| async move {
task.await?
.map(|output| output.to_event_stream())
.ok_or_else(|| anyhow!("No diagnostics found"))

View File

@@ -8,7 +8,7 @@ use assistant_slash_command::{
ArgumentCompletion, SlashCommand, SlashCommandOutput, SlashCommandOutputSection,
SlashCommandResult,
};
use gpui::{AppContext, BackgroundExecutor, Model, Task, WeakView};
use gpui::{AppContext, BackgroundExecutor, Model, Task, WeakModel};
use indexed_docs::{
DocsDotRsProvider, IndexedDocsRegistry, IndexedDocsStore, LocalRustdocProvider, PackageName,
ProviderId,
@@ -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>>,
_: &mut Window,
cx: &mut 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,
_: &mut Window,
cx: &mut AppContext,
) -> Task<SlashCommandResult> {
if arguments.is_empty() {
return Task::ready(Err(anyhow!("missing an argument")));

View File

@@ -9,7 +9,7 @@ use assistant_slash_command::{
SlashCommandResult,
};
use futures::AsyncReadExt;
use gpui::{Task, WeakView};
use gpui::{Task, WeakModel};
use html_to_markdown::{convert_html_to_markdown, markdown, TagHandler};
use http_client::{AsyncBody, HttpClient, HttpClientWithUrl};
use language::{BufferSnapshot, LspAdapterDelegate};
@@ -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 Window,
_cx: &mut AppContext,
) -> Task<Result<Vec<ArgumentCompletion>>> {
Task::ready(Ok(Vec::new()))
}
@@ -135,9 +136,10 @@ 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,
_: &mut Window,
cx: &mut AppContext,
) -> Task<SlashCommandResult> {
let Some(argument) = arguments.first() else {
return Task::ready(Err(anyhow!("missing URL")));

View File

@@ -6,7 +6,7 @@ use assistant_slash_command::{
use futures::channel::mpsc;
use futures::Stream;
use fuzzy::PathMatch;
use gpui::{AppContext, Model, Task, View, WeakView};
use gpui::{AppContext, Model, Task, WeakModel};
use language::{BufferSnapshot, CodeLabel, HighlightId, LineEnding, LspAdapterDelegate};
use project::{PathMatchCandidateSet, Project};
use serde::{Deserialize, Serialize};
@@ -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,9 @@ impl SlashCommand for FileSlashCommand {
self: Arc<Self>,
arguments: &[String],
cancellation_flag: Arc<AtomicBool>,
workspace: Option<WeakView<Workspace>>,
cx: &mut WindowContext,
workspace: Option<WeakModel<Workspace>>,
_: &mut Window,
cx: &mut 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 +188,10 @@ 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,
_: &mut Window,
cx: &mut AppContext,
) -> Task<SlashCommandResult> {
let Some(workspace) = workspace.upgrade() else {
return Task::ready(Err(anyhow!("workspace was dropped")));

View File

@@ -7,7 +7,7 @@ use assistant_slash_command::{
SlashCommandResult,
};
use chrono::Local;
use gpui::{Task, WeakView};
use gpui::{Task, WeakModel};
use language::{BufferSnapshot, LspAdapterDelegate};
use ui::prelude::*;
use workspace::Workspace;
@@ -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 Window,
_cx: &mut 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 Window,
_cx: &mut 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, WeakModel, Window};
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 Window,
_cx: &mut 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 Window,
cx: &mut AppContext,
) -> Task<SlashCommandResult> {
let model_registry = LanguageModelRegistry::read_global(cx);
let current_model = model_registry.active_model();
@@ -97,7 +99,7 @@ impl SlashCommand for ProjectSlashCommand {
return Task::ready(Err(anyhow::anyhow!("no project indexer")));
};
cx.spawn(|mut cx| async move {
window.spawn(cx, |mut cx| async move {
let current_model = current_model.ok_or_else(|| anyhow!("no model selected"))?;
let prompt =

View File

@@ -4,7 +4,7 @@ use assistant_slash_command::{
ArgumentCompletion, SlashCommand, SlashCommandOutput, SlashCommandOutputSection,
SlashCommandResult,
};
use gpui::{Task, WeakView};
use gpui::{Task, WeakModel};
use language::{BufferSnapshot, LspAdapterDelegate};
use std::sync::{atomic::AtomicBool, Arc};
use ui::prelude::*;
@@ -37,8 +37,9 @@ impl SlashCommand for PromptSlashCommand {
self: Arc<Self>,
arguments: &[String],
_cancellation_flag: Arc<AtomicBool>,
_workspace: Option<WeakView<Workspace>>,
cx: &mut WindowContext,
_workspace: Option<WeakModel<Workspace>>,
_: &mut Window,
cx: &mut AppContext,
) -> Task<Result<Vec<ArgumentCompletion>>> {
let store = PromptStore::global(cx);
let query = arguments.to_owned().join(" ");
@@ -64,9 +65,10 @@ 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,
_: &mut Window,
cx: &mut AppContext,
) -> Task<SlashCommandResult> {
let title = arguments.to_owned().join(" ");
if title.trim().is_empty() {

View File

@@ -4,7 +4,7 @@ use assistant_slash_command::{
SlashCommandResult,
};
use feature_flags::FeatureFlag;
use gpui::{AppContext, Task, WeakView};
use gpui::{AppContext, Task, WeakModel};
use language::{CodeLabel, LspAdapterDelegate};
use semantic_index::{LoadedSearchResult, SemanticDb};
use std::{
@@ -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 Window,
_cx: &mut AppContext,
) -> Task<Result<Vec<ArgumentCompletion>>> {
Task::ready(Ok(Vec::new()))
}
@@ -69,9 +70,10 @@ 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 Window,
cx: &mut AppContext,
) -> Task<SlashCommandResult> {
let Some(workspace) = workspace.upgrade() else {
return Task::ready(Err(anyhow::anyhow!("workspace was dropped")));
@@ -107,7 +109,7 @@ impl SlashCommand for SearchSlashCommand {
return Task::ready(Err(anyhow::anyhow!("no project indexer")));
};
cx.spawn(|cx| async move {
window.spawn(cx, |cx| async move {
let results = project_index
.read_with(&cx, |project_index, cx| {
project_index.search(vec![query.clone()], limit.unwrap_or(5), cx)

View File

@@ -5,11 +5,11 @@ use assistant_slash_command::{
SlashCommandOutputSection, SlashCommandResult,
};
use futures::StreamExt;
use gpui::{AppContext, Task, WeakView};
use gpui::{AppContext, Task, WeakModel};
use language::{BufferSnapshot, CodeLabel, LspAdapterDelegate};
use std::sync::atomic::AtomicBool;
use std::sync::Arc;
use ui::{IconName, SharedString, WindowContext};
use ui::{IconName, SharedString, Window};
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 Window,
_cx: &mut 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,
_: &mut Window,
cx: &mut AppContext,
) -> Task<SlashCommandResult> {
let mut events = vec![];

View File

@@ -9,7 +9,7 @@ use assistant_slash_command::{
};
use feature_flags::FeatureFlag;
use futures::channel::mpsc;
use gpui::{Task, WeakView};
use gpui::{Task, WeakModel};
use language::{BufferSnapshot, LspAdapterDelegate};
use smol::stream::StreamExt;
use smol::Timer;
@@ -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 Window,
_cx: &mut 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,
_: &mut Window,
cx: &mut AppContext,
) -> Task<SlashCommandResult> {
let (events_tx, events_rx) = mpsc::unbounded();
cx.background_executor()

View File

@@ -4,11 +4,11 @@ use assistant_slash_command::{
SlashCommandResult,
};
use editor::Editor;
use gpui::{Task, WeakView};
use gpui::{Task, WeakModel};
use language::{BufferSnapshot, LspAdapterDelegate};
use std::sync::Arc;
use std::{path::Path, sync::atomic::AtomicBool};
use ui::{IconName, WindowContext};
use ui::{AppContext, IconName, Window};
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 Window,
_cx: &mut AppContext,
) -> Task<Result<Vec<ArgumentCompletion>>> {
Task::ready(Err(anyhow!("this command does not require argument")))
}
@@ -49,9 +50,10 @@ 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,
_: &mut Window,
cx: &mut AppContext,
) -> Task<SlashCommandResult> {
let output = workspace.update(cx, |workspace, cx| {
let Some(active_item) = workspace.active_item(cx) else {

View File

@@ -6,13 +6,13 @@ use assistant_slash_command::{
use collections::{HashMap, HashSet};
use editor::Editor;
use futures::future::join_all;
use gpui::{Entity, Task, WeakView};
use gpui::{Entity, Task, WeakModel};
use language::{BufferSnapshot, CodeLabel, HighlightId, LspAdapterDelegate};
use std::{
path::PathBuf,
sync::{atomic::AtomicBool, Arc},
};
use ui::{prelude::*, ActiveTheme, WindowContext};
use ui::{prelude::*, ActiveTheme, AppContext, Window};
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 Window,
cx: &mut AppContext,
) -> Task<Result<Vec<ArgumentCompletion>>> {
let mut has_all_tabs_completion_item = false;
let argument_set = arguments
@@ -82,10 +83,10 @@ 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, window, cx);
let comment_id = cx.theme().syntax().highlight_id("comment").map(HighlightId);
cx.spawn(|_| async move {
window.spawn(cx, |_| async move {
let tab_items = tab_items_search.await?;
let run_command = tab_items.len() == 1;
let tab_completion_items = tab_items.into_iter().filter_map(|(path, ..)| {
@@ -137,15 +138,17 @@ 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 Window,
cx: &mut AppContext,
) -> Task<SlashCommandResult> {
let tab_items_search = tab_items_for_queries(
Some(workspace),
arguments,
Arc::new(AtomicBool::new(false)),
true,
window,
cx,
);
@@ -160,15 +163,16 @@ 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 Window,
cx: &mut 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();
cx.spawn(|mut cx| async move {
window.spawn(cx, |mut cx| async move {
let mut open_buffers =
workspace
.context("no workspace")?
@@ -281,7 +285,7 @@ fn tab_items_for_queries(
fn active_item_buffer(
workspace: &mut Workspace,
cx: &mut ui::ViewContext<Workspace>,
cx: &mut ModelContext<Workspace>,
) -> anyhow::Result<BufferSnapshot> {
let active_editor = workspace
.active_item(cx)

View File

@@ -6,7 +6,7 @@ use assistant_slash_command::{
ArgumentCompletion, SlashCommand, SlashCommandOutput, SlashCommandOutputSection,
SlashCommandResult,
};
use gpui::{AppContext, Task, View, WeakView};
use gpui::{AppContext, Model, Task, WeakModel};
use language::{BufferSnapshot, CodeLabel, LspAdapterDelegate};
use terminal_view::{terminal_panel::TerminalPanel, TerminalView};
use ui::prelude::*;
@@ -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 Window,
_cx: &mut AppContext,
) -> Task<Result<Vec<ArgumentCompletion>>> {
Task::ready(Ok(Vec::new()))
}
@@ -64,9 +65,10 @@ 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,
_: &mut Window,
cx: &mut AppContext,
) -> Task<SlashCommandResult> {
let Some(workspace) = workspace.upgrade() else {
return Task::ready(Err(anyhow::anyhow!("workspace was dropped")));
@@ -107,9 +109,9 @@ impl SlashCommand for TerminalSlashCommand {
}
fn resolve_active_terminal(
workspace: &View<Workspace>,
cx: &WindowContext,
) -> Option<View<TerminalView>> {
workspace: &Model<Workspace>,
cx: &mut AppContext,
) -> Option<Model<TerminalView>> {
if let Some(terminal_view) = workspace
.read(cx)
.active_item(cx)

View File

@@ -1,6 +1,6 @@
use std::sync::Arc;
use gpui::{AnyElement, DismissEvent, SharedString, Task, WeakView};
use gpui::{AnyElement, DismissEvent, SharedString, Task, WeakModel};
use picker::{Picker, PickerDelegate, PickerEditorPosition};
use ui::{prelude::*, ListItem, ListItemSpacing, PopoverMenu, PopoverTrigger, Tooltip};
@@ -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 Window, &mut AppContext) -> AnyElement,
on_confirm: fn(&mut Window, &mut 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,28 @@ 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,
_: &mut Window,
cx: &mut ModelContext<Picker<Self>>,
) {
self.selected_index = ix.min(self.filtered_commands.len().saturating_sub(1));
cx.notify();
}
fn placeholder_text(&self, _cx: &mut WindowContext) -> Arc<str> {
fn placeholder_text(&self, _window: &mut Window, _cx: &mut 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,
window: &mut Window,
cx: &mut ModelContext<Picker<Self>>,
) -> Task<()> {
let all_commands = self.all_commands.clone();
cx.spawn(|this, mut cx| async move {
cx.spawn_in(window, |this, mut cx| async move {
let filtered_commands = cx
.background_executor()
.spawn(async move {
@@ -104,9 +114,9 @@ impl PickerDelegate for SlashCommandDelegate {
})
.await;
this.update(&mut cx, |this, cx| {
this.update_in(&mut cx, |this, window, cx| {
this.delegate.filtered_commands = filtered_commands;
this.delegate.set_selected_index(0, cx);
this.delegate.set_selected_index(0, window, cx);
cx.notify();
})
.ok();
@@ -139,25 +149,30 @@ impl PickerDelegate for SlashCommandDelegate {
ret
}
fn confirm(&mut self, _secondary: bool, cx: &mut ViewContext<Picker<Self>>) {
fn confirm(
&mut self,
_secondary: bool,
window: &mut Window,
cx: &mut ModelContext<Picker<Self>>,
) {
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| {
context_editor.insert_command(&info.name, cx)
context_editor.insert_command(&info.name, window, cx)
})
.ok();
}
SlashCommandEntry::Advert { on_confirm, .. } => {
on_confirm(cx);
on_confirm(window, cx);
}
}
cx.emit(DismissEvent);
}
}
fn dismissed(&mut self, _cx: &mut ViewContext<Picker<Self>>) {}
fn dismissed(&mut self, _window: &mut Window, _cx: &mut ModelContext<Picker<Self>>) {}
fn editor_position(&self) -> PickerEditorPosition {
PickerEditorPosition::End
@@ -167,7 +182,8 @@ impl PickerDelegate for SlashCommandDelegate {
&self,
ix: usize,
selected: bool,
cx: &mut ViewContext<Picker<Self>>,
window: &mut Window,
cx: &mut ModelContext<Picker<Self>>,
) -> Option<Self::ListItem> {
let command_info = self.filtered_commands.get(ix)?;
@@ -179,7 +195,7 @@ impl PickerDelegate for SlashCommandDelegate {
.toggle_state(selected)
.tooltip({
let description = info.description.clone();
move |cx| cx.new_view(|_| Tooltip::new(description.clone())).into()
move |_, cx| cx.new_model(|_| Tooltip::new(description.clone())).into()
})
.child(
v_flex()
@@ -229,14 +245,14 @@ impl PickerDelegate for SlashCommandDelegate {
.inset(true)
.spacing(ListItemSpacing::Dense)
.toggle_state(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 Window, cx: &mut AppContext) -> impl IntoElement {
let all_models = self
.working_set
.featured_command_names(cx)
@@ -259,7 +275,7 @@ impl<T: PopoverTrigger> RenderOnce for SlashCommandSelector<T> {
})
.chain([SlashCommandEntry::Advert {
name: "create-your-command".into(),
renderer: |cx| {
renderer: |_, cx| {
v_flex()
.w_full()
.child(
@@ -293,7 +309,7 @@ impl<T: PopoverTrigger> RenderOnce for SlashCommandSelector<T> {
)
.into_any_element()
},
on_confirm: |cx| cx.open_url("https://zed.dev/docs/extensions/slash-commands"),
on_confirm: |_, cx| cx.open_url("https://zed.dev/docs/extensions/slash-commands"),
}])
.collect::<Vec<_>>();
@@ -304,8 +320,9 @@ 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(|cx| {
let picker =
Picker::uniform_list(delegate, window, cx).max_height(Some(rems(20.).into()));
picker
});
@@ -314,7 +331,7 @@ impl<T: PopoverTrigger> RenderOnce for SlashCommandSelector<T> {
.update(cx, |this, _| this.slash_menu_handle.clone())
.ok();
PopoverMenu::new("model-switcher")
.menu(move |_cx| Some(picker_view.clone()))
.menu(move |_window, _cx| Some(picker_view.clone()))
.trigger(self.trigger)
.attach(gpui::Corner::TopLeft)
.anchor(gpui::Corner::BottomLeft)

View File

@@ -13,8 +13,8 @@ use editor::{
use fs::Fs;
use futures::{channel::mpsc, SinkExt, StreamExt};
use gpui::{
AppContext, Context, EventEmitter, FocusHandle, FocusableView, Global, Model, ModelContext,
Subscription, Task, TextStyle, UpdateGlobal, View, WeakView,
AppContext, Context, EventEmitter, FocusHandle, Focusable, Global, Model, ModelContext,
Subscription, Task, TextStyle, UpdateGlobal, WeakModel,
};
use language::Buffer;
use language_model::{
@@ -87,11 +87,12 @@ impl TerminalInlineAssistant {
pub fn assist(
&mut self,
terminal_view: &View<TerminalView>,
workspace: Option<WeakView<Workspace>>,
assistant_panel: Option<&View<AssistantPanel>>,
terminal_view: &Model<TerminalView>,
workspace: Option<WeakModel<Workspace>>,
assistant_panel: Option<&Model<AssistantPanel>>,
initial_prompt: Option<String>,
cx: &mut WindowContext,
window: &mut Window,
cx: &mut AppContext,
) {
let terminal = terminal_view.read(cx).terminal().clone();
let assist_id = self.next_assist_id.post_inc();
@@ -100,7 +101,7 @@ impl TerminalInlineAssistant {
let prompt_buffer = cx.new_model(|cx| MultiBuffer::singleton(prompt_buffer, cx));
let codegen = cx.new_model(|_| Codegen::new(terminal, self.telemetry.clone()));
let prompt_editor = cx.new_view(|cx| {
let prompt_editor = cx.new_model(|cx| {
PromptEditor::new(
assist_id,
self.prompt_history.clone(),
@@ -109,6 +110,7 @@ impl TerminalInlineAssistant {
assistant_panel,
workspace.clone(),
self.fs.clone(),
window,
cx,
)
});
@@ -118,7 +120,7 @@ impl TerminalInlineAssistant {
render: Box::new(move |_| prompt_editor_render.clone().into_any_element()),
};
terminal_view.update(cx, |terminal_view, cx| {
terminal_view.set_block_below_cursor(block, cx);
terminal_view.set_block_below_cursor(block, window, cx);
});
let terminal_assistant = TerminalInlineAssist::new(
@@ -127,21 +129,27 @@ impl TerminalInlineAssistant {
assistant_panel.is_some(),
prompt_editor,
workspace.clone(),
window,
cx,
);
self.assists.insert(assist_id, terminal_assistant);
self.focus_assist(assist_id, cx);
self.focus_assist(assist_id, window, cx);
}
fn focus_assist(&mut self, assist_id: TerminalInlineAssistId, cx: &mut WindowContext) {
fn focus_assist(
&mut self,
assist_id: TerminalInlineAssistId,
window: &mut Window,
cx: &mut AppContext,
) {
let assist = &self.assists[&assist_id];
if let Some(prompt_editor) = assist.prompt_editor.as_ref() {
prompt_editor.update(cx, |this, cx| {
this.editor.update(cx, |editor, cx| {
editor.focus(cx);
editor.select_all(&SelectAll, cx);
window.focus(&editor.focus_handle(cx));
editor.select_all(&SelectAll, window, cx);
});
});
}
@@ -149,9 +157,10 @@ impl TerminalInlineAssistant {
fn handle_prompt_editor_event(
&mut self,
prompt_editor: View<PromptEditor>,
prompt_editor: Model<PromptEditor>,
event: &PromptEditorEvent,
cx: &mut WindowContext,
window: &mut Window,
cx: &mut AppContext,
) {
let assist_id = prompt_editor.read(cx).id;
match event {
@@ -162,21 +171,21 @@ impl TerminalInlineAssistant {
self.stop_assist(assist_id, cx);
}
PromptEditorEvent::ConfirmRequested { execute } => {
self.finish_assist(assist_id, false, *execute, cx);
self.finish_assist(assist_id, false, *execute, window, cx);
}
PromptEditorEvent::CancelRequested => {
self.finish_assist(assist_id, true, false, cx);
self.finish_assist(assist_id, true, false, window, cx);
}
PromptEditorEvent::DismissRequested => {
self.dismiss_assist(assist_id, cx);
self.dismiss_assist(assist_id, window, cx);
}
PromptEditorEvent::Resized { height_in_lines } => {
self.insert_prompt_editor_into_terminal(assist_id, *height_in_lines, cx);
self.insert_prompt_editor_into_terminal(assist_id, *height_in_lines, window, cx);
}
}
}
fn start_assist(&mut self, assist_id: TerminalInlineAssistId, cx: &mut WindowContext) {
fn start_assist(&mut self, assist_id: TerminalInlineAssistId, cx: &mut AppContext) {
let assist = if let Some(assist) = self.assists.get_mut(&assist_id) {
assist
} else {
@@ -214,7 +223,7 @@ impl TerminalInlineAssistant {
codegen.update(cx, |codegen, cx| codegen.start(request, cx));
}
fn stop_assist(&mut self, assist_id: TerminalInlineAssistId, cx: &mut WindowContext) {
fn stop_assist(&mut self, assist_id: TerminalInlineAssistId, cx: &mut AppContext) {
let assist = if let Some(assist) = self.assists.get_mut(&assist_id) {
assist
} else {
@@ -227,7 +236,7 @@ impl TerminalInlineAssistant {
fn request_for_inline_assist(
&self,
assist_id: TerminalInlineAssistId,
cx: &mut WindowContext,
cx: &mut AppContext,
) -> Result<LanguageModelRequest> {
let assist = self.assists.get(&assist_id).context("invalid assist")?;
@@ -297,16 +306,17 @@ impl TerminalInlineAssistant {
assist_id: TerminalInlineAssistId,
undo: bool,
execute: bool,
cx: &mut WindowContext,
window: &mut Window,
cx: &mut AppContext,
) {
self.dismiss_assist(assist_id, cx);
self.dismiss_assist(assist_id, window, cx);
if let Some(assist) = self.assists.remove(&assist_id) {
assist
.terminal
.update(cx, |this, cx| {
this.clear_block_below_cursor(cx);
this.focus_handle(cx).focus(cx);
this.focus_handle(cx).focus(window);
})
.log_err();
@@ -349,7 +359,8 @@ impl TerminalInlineAssistant {
fn dismiss_assist(
&mut self,
assist_id: TerminalInlineAssistId,
cx: &mut WindowContext,
window: &mut Window,
cx: &mut AppContext,
) -> bool {
let Some(assist) = self.assists.get_mut(&assist_id) else {
return false;
@@ -362,7 +373,7 @@ impl TerminalInlineAssistant {
.terminal
.update(cx, |this, cx| {
this.clear_block_below_cursor(cx);
this.focus_handle(cx).focus(cx);
this.focus_handle(cx).focus(window);
})
.is_ok()
}
@@ -371,7 +382,8 @@ impl TerminalInlineAssistant {
&mut self,
assist_id: TerminalInlineAssistId,
height: u8,
cx: &mut WindowContext,
window: &mut Window,
cx: &mut AppContext,
) {
if let Some(assist) = self.assists.get_mut(&assist_id) {
if let Some(prompt_editor) = assist.prompt_editor.as_ref().cloned() {
@@ -383,7 +395,7 @@ impl TerminalInlineAssistant {
height,
render: Box::new(move |_| prompt_editor.clone().into_any_element()),
};
terminal.set_block_below_cursor(block, cx);
terminal.set_block_below_cursor(block, window, cx);
})
.log_err();
}
@@ -392,10 +404,10 @@ impl TerminalInlineAssistant {
}
struct TerminalInlineAssist {
terminal: WeakView<TerminalView>,
prompt_editor: Option<View<PromptEditor>>,
terminal: WeakModel<TerminalView>,
prompt_editor: Option<Model<PromptEditor>>,
codegen: Model<Codegen>,
workspace: Option<WeakView<Workspace>>,
workspace: Option<WeakModel<Workspace>>,
include_context: bool,
_subscriptions: Vec<Subscription>,
}
@@ -403,11 +415,12 @@ struct TerminalInlineAssist {
impl TerminalInlineAssist {
pub fn new(
assist_id: TerminalInlineAssistId,
terminal: &View<TerminalView>,
terminal: &Model<TerminalView>,
include_context: bool,
prompt_editor: View<PromptEditor>,
workspace: Option<WeakView<Workspace>>,
cx: &mut WindowContext,
prompt_editor: Model<PromptEditor>,
workspace: Option<WeakModel<Workspace>>,
window: &mut Window,
cx: &mut AppContext,
) -> Self {
let codegen = prompt_editor.read(cx).codegen.clone();
Self {
@@ -417,12 +430,12 @@ impl TerminalInlineAssist {
workspace: workspace.clone(),
include_context,
_subscriptions: vec![
cx.subscribe(&prompt_editor, |prompt_editor, event, cx| {
window.subscribe(&prompt_editor, cx, |prompt_editor, event, window, cx| {
TerminalInlineAssistant::update_global(cx, |this, cx| {
this.handle_prompt_editor_event(prompt_editor, event, cx)
this.handle_prompt_editor_event(prompt_editor, event, window, cx)
})
}),
cx.subscribe(&codegen, move |codegen, event, cx| {
window.subscribe(&codegen, cx, move |codegen, event, window, cx| {
TerminalInlineAssistant::update_global(cx, |this, cx| match event {
CodegenEvent::Finished => {
let assist = if let Some(assist) = this.assists.get(&assist_id) {
@@ -455,7 +468,7 @@ impl TerminalInlineAssist {
}
if assist.prompt_editor.is_none() {
this.finish_assist(assist_id, false, false, cx);
this.finish_assist(assist_id, false, false, window, cx);
}
}
})
@@ -477,8 +490,8 @@ enum PromptEditorEvent {
struct PromptEditor {
id: TerminalInlineAssistId,
height_in_lines: u8,
editor: View<Editor>,
language_model_selector: View<LanguageModelSelector>,
editor: Model<Editor>,
language_model_selector: Model<LanguageModelSelector>,
edited_since_done: bool,
prompt_history: VecDeque<String>,
prompt_history_ix: Option<usize>,
@@ -489,13 +502,13 @@ struct PromptEditor {
pending_token_count: Task<Result<()>>,
token_count: Option<usize>,
_token_count_subscriptions: Vec<Subscription>,
workspace: Option<WeakView<Workspace>>,
workspace: Option<WeakModel<Workspace>>,
}
impl EventEmitter<PromptEditorEvent> for PromptEditor {}
impl Render for PromptEditor {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
fn render(&mut self, _: &mut Window, cx: &mut ModelContext<Self>) -> impl IntoElement {
let status = &self.codegen.read(cx).status;
let buttons = match status {
CodegenStatus::Idle => {
@@ -503,16 +516,20 @@ impl Render for PromptEditor {
IconButton::new("cancel", IconName::Close)
.icon_color(Color::Muted)
.shape(IconButtonShape::Square)
.tooltip(|cx| Tooltip::for_action("Cancel Assist", &menu::Cancel, cx))
.tooltip(|window, cx| {
Tooltip::for_action("Cancel Assist", &menu::Cancel, window, cx)
})
.on_click(
cx.listener(|_, _, cx| cx.emit(PromptEditorEvent::CancelRequested)),
cx.listener(|_, _, _, cx| cx.emit(PromptEditorEvent::CancelRequested)),
),
IconButton::new("start", IconName::SparkleAlt)
.icon_color(Color::Muted)
.shape(IconButtonShape::Square)
.tooltip(|cx| Tooltip::for_action("Generate", &menu::Confirm, cx))
.tooltip(|window, cx| {
Tooltip::for_action("Generate", &menu::Confirm, window, cx)
})
.on_click(
cx.listener(|_, _, cx| cx.emit(PromptEditorEvent::StartRequested)),
cx.listener(|_, _, _, cx| cx.emit(PromptEditorEvent::StartRequested)),
),
]
}
@@ -521,23 +538,24 @@ impl Render for PromptEditor {
IconButton::new("cancel", IconName::Close)
.icon_color(Color::Muted)
.shape(IconButtonShape::Square)
.tooltip(|cx| Tooltip::text("Cancel Assist", cx))
.tooltip(Tooltip::text("Cancel Assist"))
.on_click(
cx.listener(|_, _, cx| cx.emit(PromptEditorEvent::CancelRequested)),
cx.listener(|_, _, _, cx| cx.emit(PromptEditorEvent::CancelRequested)),
),
IconButton::new("stop", IconName::Stop)
.icon_color(Color::Error)
.shape(IconButtonShape::Square)
.tooltip(|cx| {
.tooltip(|window, cx| {
Tooltip::with_meta(
"Interrupt Generation",
Some(&menu::Cancel),
"Changes won't be discarded",
window,
cx,
)
})
.on_click(
cx.listener(|_, _, cx| cx.emit(PromptEditorEvent::StopRequested)),
cx.listener(|_, _, _, cx| cx.emit(PromptEditorEvent::StopRequested)),
),
]
}
@@ -545,8 +563,12 @@ impl Render for PromptEditor {
let cancel = IconButton::new("cancel", IconName::Close)
.icon_color(Color::Muted)
.shape(IconButtonShape::Square)
.tooltip(|cx| Tooltip::for_action("Cancel Assist", &menu::Cancel, cx))
.on_click(cx.listener(|_, _, cx| cx.emit(PromptEditorEvent::CancelRequested)));
.tooltip(|window, cx| {
Tooltip::for_action("Cancel Assist", &menu::Cancel, window, cx)
})
.on_click(
cx.listener(|_, _, _, cx| cx.emit(PromptEditorEvent::CancelRequested)),
);
let has_error = matches!(status, CodegenStatus::Error(_));
if has_error || self.edited_since_done {
@@ -555,15 +577,16 @@ impl Render for PromptEditor {
IconButton::new("restart", IconName::RotateCw)
.icon_color(Color::Info)
.shape(IconButtonShape::Square)
.tooltip(|cx| {
.tooltip(|window, cx| {
Tooltip::with_meta(
"Restart Generation",
Some(&menu::Confirm),
"Changes will be discarded",
window,
cx,
)
})
.on_click(cx.listener(|_, _, cx| {
.on_click(cx.listener(|_, _, _, cx| {
cx.emit(PromptEditorEvent::StartRequested);
})),
]
@@ -573,23 +596,29 @@ impl Render for PromptEditor {
IconButton::new("accept", IconName::Check)
.icon_color(Color::Info)
.shape(IconButtonShape::Square)
.tooltip(|cx| {
Tooltip::for_action("Accept Generated Command", &menu::Confirm, cx)
.tooltip(|window, cx| {
Tooltip::for_action(
"Accept Generated Command",
&menu::Confirm,
window,
cx,
)
})
.on_click(cx.listener(|_, _, cx| {
.on_click(cx.listener(|_, _, _, cx| {
cx.emit(PromptEditorEvent::ConfirmRequested { execute: false });
})),
IconButton::new("confirm", IconName::Play)
.icon_color(Color::Info)
.shape(IconButtonShape::Square)
.tooltip(|cx| {
.tooltip(|window, cx| {
Tooltip::for_action(
"Execute Generated Command",
&menu::SecondaryConfirm,
window,
cx,
)
})
.on_click(cx.listener(|_, _, cx| {
.on_click(cx.listener(|_, _, _, cx| {
cx.emit(PromptEditorEvent::ConfirmRequested { execute: true });
})),
]
@@ -620,7 +649,7 @@ impl Render for PromptEditor {
.shape(IconButtonShape::Square)
.icon_size(IconSize::Small)
.icon_color(Color::Muted)
.tooltip(move |cx| {
.tooltip(move |window, cx| {
Tooltip::with_meta(
format!(
"Using {}",
@@ -631,6 +660,7 @@ impl Render for PromptEditor {
),
None,
"Change Model",
window,
cx,
)
}),
@@ -641,7 +671,7 @@ impl Render for PromptEditor {
Some(
div()
.id("error")
.tooltip(move |cx| Tooltip::text(error_message.clone(), cx))
.tooltip(Tooltip::text(error_message))
.child(
Icon::new(IconName::XCircle)
.size(IconSize::Small)
@@ -664,7 +694,7 @@ impl Render for PromptEditor {
}
}
impl FocusableView for PromptEditor {
impl Focusable for PromptEditor {
fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
self.editor.focus_handle(cx)
}
@@ -679,12 +709,13 @@ impl PromptEditor {
prompt_history: VecDeque<String>,
prompt_buffer: Model<MultiBuffer>,
codegen: Model<Codegen>,
assistant_panel: Option<&View<AssistantPanel>>,
workspace: Option<WeakView<Workspace>>,
assistant_panel: Option<&Model<AssistantPanel>>,
workspace: Option<WeakModel<Workspace>>,
fs: Arc<dyn Fs>,
cx: &mut ViewContext<Self>,
window: &mut Window,
cx: &mut ModelContext<Self>,
) -> Self {
let prompt_editor = cx.new_view(|cx| {
let prompt_editor = cx.new_model(|cx| {
let mut editor = Editor::new(
EditorMode::AutoHeight {
max_lines: Self::MAX_LINES as usize,
@@ -692,24 +723,28 @@ impl PromptEditor {
prompt_buffer,
None,
false,
window,
cx,
);
editor.set_soft_wrap_mode(language::language_settings::SoftWrap::EditorWidth, cx);
editor.set_placeholder_text(Self::placeholder_text(cx), cx);
editor.set_placeholder_text(Self::placeholder_text(window), cx);
editor
});
let mut token_count_subscriptions = Vec::new();
if let Some(assistant_panel) = assistant_panel {
token_count_subscriptions
.push(cx.subscribe(assistant_panel, Self::handle_assistant_panel_event));
token_count_subscriptions.push(cx.subscribe_in(
assistant_panel,
window,
Self::handle_assistant_panel_event,
));
}
let mut this = Self {
id,
height_in_lines: 1,
editor: prompt_editor,
language_model_selector: cx.new_view(|cx| {
language_model_selector: cx.new_model(|cx| {
let fs = fs.clone();
LanguageModelSelector::new(
move |model, cx| {
@@ -719,6 +754,7 @@ impl PromptEditor {
move |settings, _| settings.set_model(model.clone()),
);
},
window,
cx,
)
}),
@@ -726,7 +762,7 @@ impl PromptEditor {
prompt_history,
prompt_history_ix: None,
pending_prompt: String::new(),
_codegen_subscription: cx.observe(&codegen, Self::handle_codegen_changed),
_codegen_subscription: cx.observe_in(&codegen, window, Self::handle_codegen_changed),
editor_subscriptions: Vec::new(),
codegen,
pending_token_count: Task::ready(Ok(())),
@@ -740,15 +776,15 @@ impl PromptEditor {
this
}
fn placeholder_text(cx: &WindowContext) -> String {
let context_keybinding = text_for_action(&crate::ToggleFocus, cx)
fn placeholder_text(window: &mut Window) -> String {
let context_keybinding = text_for_action(&crate::ToggleFocus, window)
.map(|keybinding| format!("{keybinding} for context"))
.unwrap_or_default();
format!("Generate…{context_keybinding} • ↓↑ for history")
}
fn subscribe_to_editor(&mut self, cx: &mut ViewContext<Self>) {
fn subscribe_to_editor(&mut self, cx: &mut ModelContext<Self>) {
self.editor_subscriptions.clear();
self.editor_subscriptions
.push(cx.observe(&self.editor, Self::handle_prompt_editor_changed));
@@ -760,7 +796,7 @@ impl PromptEditor {
self.editor.read(cx).text(cx)
}
fn count_lines(&mut self, cx: &mut ViewContext<Self>) {
fn count_lines(&mut self, cx: &mut ModelContext<Self>) {
let height_in_lines = cmp::max(
2, // Make the editor at least two lines tall, to account for padding and buttons.
cmp::min(
@@ -778,15 +814,16 @@ impl PromptEditor {
fn handle_assistant_panel_event(
&mut self,
_: View<AssistantPanel>,
_: &Model<AssistantPanel>,
event: &AssistantPanelEvent,
cx: &mut ViewContext<Self>,
_: &mut Window,
cx: &mut ModelContext<Self>,
) {
let AssistantPanelEvent::ContextEdited { .. } = event;
self.count_tokens(cx);
}
fn count_tokens(&mut self, cx: &mut ViewContext<Self>) {
fn count_tokens(&mut self, cx: &mut ModelContext<Self>) {
let assist_id = self.id;
let Some(model) = LanguageModelRegistry::read_global(cx).active_model() else {
return;
@@ -806,15 +843,15 @@ impl PromptEditor {
})
}
fn handle_prompt_editor_changed(&mut self, _: View<Editor>, cx: &mut ViewContext<Self>) {
fn handle_prompt_editor_changed(&mut self, _: Model<Editor>, cx: &mut ModelContext<Self>) {
self.count_lines(cx);
}
fn handle_prompt_editor_events(
&mut self,
_: View<Editor>,
_: Model<Editor>,
event: &EditorEvent,
cx: &mut ViewContext<Self>,
cx: &mut ModelContext<Self>,
) {
match event {
EditorEvent::Edited { .. } => {
@@ -837,7 +874,12 @@ impl PromptEditor {
}
}
fn handle_codegen_changed(&mut self, _: Model<Codegen>, cx: &mut ViewContext<Self>) {
fn handle_codegen_changed(
&mut self,
_: Model<Codegen>,
_: &mut Window,
cx: &mut ModelContext<Self>,
) {
match &self.codegen.read(cx).status {
CodegenStatus::Idle => {
self.editor
@@ -855,7 +897,7 @@ impl PromptEditor {
}
}
fn cancel(&mut self, _: &editor::actions::Cancel, cx: &mut ViewContext<Self>) {
fn cancel(&mut self, _: &editor::actions::Cancel, _: &mut Window, cx: &mut ModelContext<Self>) {
match &self.codegen.read(cx).status {
CodegenStatus::Idle | CodegenStatus::Done | CodegenStatus::Error(_) => {
cx.emit(PromptEditorEvent::CancelRequested);
@@ -866,7 +908,7 @@ impl PromptEditor {
}
}
fn confirm(&mut self, _: &menu::Confirm, cx: &mut ViewContext<Self>) {
fn confirm(&mut self, _: &menu::Confirm, _: &mut Window, cx: &mut ModelContext<Self>) {
match &self.codegen.read(cx).status {
CodegenStatus::Idle => {
if !self.editor.read(cx).text(cx).trim().is_empty() {
@@ -889,53 +931,58 @@ impl PromptEditor {
}
}
fn secondary_confirm(&mut self, _: &menu::SecondaryConfirm, cx: &mut ViewContext<Self>) {
fn secondary_confirm(
&mut self,
_: &menu::SecondaryConfirm,
_: &mut Window,
cx: &mut ModelContext<Self>,
) {
if matches!(self.codegen.read(cx).status, CodegenStatus::Done) {
cx.emit(PromptEditorEvent::ConfirmRequested { execute: true });
}
}
fn move_up(&mut self, _: &MoveUp, cx: &mut ViewContext<Self>) {
fn move_up(&mut self, _: &MoveUp, window: &mut Window, cx: &mut ModelContext<Self>) {
if let Some(ix) = self.prompt_history_ix {
if ix > 0 {
self.prompt_history_ix = Some(ix - 1);
let prompt = self.prompt_history[ix - 1].as_str();
self.editor.update(cx, |editor, cx| {
editor.set_text(prompt, cx);
editor.move_to_beginning(&Default::default(), cx);
editor.set_text(prompt, window, cx);
editor.move_to_beginning(&Default::default(), window, cx);
});
}
} else if !self.prompt_history.is_empty() {
self.prompt_history_ix = Some(self.prompt_history.len() - 1);
let prompt = self.prompt_history[self.prompt_history.len() - 1].as_str();
self.editor.update(cx, |editor, cx| {
editor.set_text(prompt, cx);
editor.move_to_beginning(&Default::default(), cx);
editor.set_text(prompt, window, cx);
editor.move_to_beginning(&Default::default(), window, cx);
});
}
}
fn move_down(&mut self, _: &MoveDown, cx: &mut ViewContext<Self>) {
fn move_down(&mut self, _: &MoveDown, window: &mut Window, cx: &mut ModelContext<Self>) {
if let Some(ix) = self.prompt_history_ix {
if ix < self.prompt_history.len() - 1 {
self.prompt_history_ix = Some(ix + 1);
let prompt = self.prompt_history[ix + 1].as_str();
self.editor.update(cx, |editor, cx| {
editor.set_text(prompt, cx);
editor.move_to_end(&Default::default(), cx)
editor.set_text(prompt, window, cx);
editor.move_to_end(&Default::default(), window, cx)
});
} else {
self.prompt_history_ix = None;
let prompt = self.pending_prompt.as_str();
self.editor.update(cx, |editor, cx| {
editor.set_text(prompt, cx);
editor.move_to_end(&Default::default(), cx)
editor.set_text(prompt, window, cx);
editor.move_to_end(&Default::default(), window, cx)
});
}
}
}
fn render_token_count(&self, cx: &mut ViewContext<Self>) -> Option<impl IntoElement> {
fn render_token_count(&self, cx: &mut ModelContext<Self>) -> Option<impl IntoElement> {
let model = LanguageModelRegistry::read_global(cx).active_model()?;
let token_count = self.token_count?;
let max_token_count = model.max_token_count();
@@ -965,34 +1012,35 @@ impl PromptEditor {
);
if let Some(workspace) = self.workspace.clone() {
token_count = token_count
.tooltip(|cx| {
.tooltip(|window, cx| {
Tooltip::with_meta(
"Tokens Used by Inline Assistant",
None,
"Click to Open Assistant Panel",
window,
cx,
)
})
.cursor_pointer()
.on_mouse_down(gpui::MouseButton::Left, |_, cx| cx.stop_propagation())
.on_click(move |_, cx| {
.on_mouse_down(gpui::MouseButton::Left, |_, _, cx| cx.stop_propagation())
.on_click(move |_, window, cx| {
cx.stop_propagation();
workspace
.update(cx, |workspace, cx| {
workspace.focus_panel::<AssistantPanel>(cx)
workspace.focus_panel::<AssistantPanel>(window, cx)
})
.ok();
});
} else {
token_count = token_count
.cursor_default()
.tooltip(|cx| Tooltip::text("Tokens Used by Inline Assistant", cx));
.tooltip(Tooltip::text("Tokens Used by Inline Assistant"));
}
Some(token_count)
}
fn render_prompt_editor(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
fn render_prompt_editor(&self, cx: &mut ModelContext<Self>) -> impl IntoElement {
let settings = ThemeSettings::get_global(cx);
let text_style = TextStyle {
color: if self.editor.read(cx).read_only(cx) {

View File

@@ -50,6 +50,7 @@ parking_lot.workspace = true
picker.workspace = true
project.workspace = true
proto.workspace = true
release_channel.workspace = true
rope.workspace = true
schemars.workspace = true
serde.workspace = true

View File

@@ -3,8 +3,9 @@ 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,
list, AbsoluteLength, AnyElement, AppContext, DefiniteLength, EdgesRefinement, Empty, Length,
ListAlignment, ListOffset, ListState, Model, StyleRefinement, Subscription,
TextStyleRefinement, WeakModel,
};
use language::LanguageRegistry;
use language_model::Role;
@@ -18,13 +19,13 @@ use crate::thread::{MessageId, Thread, ThreadError, ThreadEvent};
use crate::ui::ContextPill;
pub struct ActiveThread {
workspace: WeakView<Workspace>,
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, View<Markdown>>,
rendered_messages_by_id: HashMap<MessageId, Model<Markdown>>,
last_error: Option<ThreadError>,
_subscriptions: Vec<Subscription>,
}
@@ -32,14 +33,15 @@ pub struct ActiveThread {
impl ActiveThread {
pub fn new(
thread: Model<Thread>,
workspace: WeakView<Workspace>,
workspace: WeakModel<Workspace>,
language_registry: Arc<LanguageRegistry>,
tools: Arc<ToolWorkingSet>,
cx: &mut ViewContext<Self>,
window: &mut Window,
cx: &mut ModelContext<Self>,
) -> Self {
let subscriptions = vec![
cx.observe(&thread, |_, _, cx| cx.notify()),
cx.subscribe(&thread, Self::handle_thread_event),
cx.subscribe_in(&thread, window, Self::handle_thread_event),
];
let mut this = Self {
@@ -50,8 +52,8 @@ impl ActiveThread {
messages: Vec::new(),
rendered_messages_by_id: HashMap::default(),
list_state: ListState::new(0, ListAlignment::Bottom, px(1024.), {
let this = cx.view().downgrade();
move |ix, cx: &mut WindowContext| {
let this = cx.model().downgrade();
move |ix, _: &mut Window, cx: &mut AppContext| {
this.update(cx, |this, cx| this.render_message(ix, cx))
.unwrap()
}
@@ -61,7 +63,7 @@ impl ActiveThread {
};
for message in thread.read(cx).messages().cloned().collect::<Vec<_>>() {
this.push_message(&message.id, message.text.clone(), cx);
this.push_message(&message.id, message.text.clone(), window, cx);
}
this
@@ -83,16 +85,23 @@ impl ActiveThread {
self.last_error.take();
}
fn push_message(&mut self, id: &MessageId, text: String, cx: &mut ViewContext<Self>) {
fn push_message(
&mut self,
id: &MessageId,
text: String,
window: &mut Window,
cx: &mut ModelContext<Self>,
) {
let old_len = self.messages.len();
self.messages.push(*id);
self.list_state.splice(old_len..old_len, 1);
let theme_settings = ThemeSettings::get_global(cx);
let colors = cx.theme().colors();
let ui_font_size = TextSize::Default.rems(cx);
let buffer_font_size = theme_settings.buffer_font_size;
let buffer_font_size = TextSize::Small.rems(cx);
let mut text_style = window.text_style();
let mut text_style = cx.text_style();
text_style.refine(&TextStyleRefinement {
font_family: Some(theme_settings.ui_font.family.clone()),
font_size: Some(ui_font_size.into()),
@@ -105,6 +114,26 @@ impl ActiveThread {
syntax: cx.theme().syntax().clone(),
selection_background_color: cx.theme().players().local().selection,
code_block: StyleRefinement {
margin: EdgesRefinement {
top: Some(Length::Definite(rems(1.0).into())),
left: Some(Length::Definite(rems(0.).into())),
right: Some(Length::Definite(rems(0.).into())),
bottom: Some(Length::Definite(rems(1.).into())),
},
padding: EdgesRefinement {
top: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(Pixels(8.)))),
left: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(Pixels(8.)))),
right: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(Pixels(8.)))),
bottom: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(Pixels(8.)))),
},
background: Some(colors.editor_foreground.opacity(0.01).into()),
border_color: Some(colors.border_variant.opacity(0.3)),
border_widths: EdgesRefinement {
top: Some(AbsoluteLength::Pixels(Pixels(1.0))),
left: Some(AbsoluteLength::Pixels(Pixels(1.))),
right: Some(AbsoluteLength::Pixels(Pixels(1.))),
bottom: Some(AbsoluteLength::Pixels(Pixels(1.))),
},
text: Some(TextStyleRefinement {
font_family: Some(theme_settings.buffer_font.family.clone()),
font_size: Some(buffer_font_size.into()),
@@ -114,30 +143,36 @@ impl ActiveThread {
},
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),
font_size: Some(buffer_font_size.into()),
background_color: Some(colors.editor_foreground.opacity(0.01)),
..Default::default()
},
..Default::default()
};
let markdown = cx.new_view(|cx| {
let markdown = cx.new_model(|cx| {
Markdown::new(
text,
markdown_style,
Some(self.language_registry.clone()),
None,
window,
cx,
)
});
self.rendered_messages_by_id.insert(*id, markdown);
self.list_state.scroll_to(ListOffset {
item_ix: old_len,
offset_in_item: Pixels(0.0),
});
}
fn handle_thread_event(
&mut self,
_: Model<Thread>,
_: &Model<Thread>,
event: &ThreadEvent,
cx: &mut ViewContext<Self>,
window: &mut Window,
cx: &mut ModelContext<Self>,
) {
match event {
ThreadEvent::ShowError(error) => {
@@ -148,7 +183,7 @@ impl ActiveThread {
ThreadEvent::StreamedAssistantText(message_id, text) => {
if let Some(markdown) = self.rendered_messages_by_id.get_mut(&message_id) {
markdown.update(cx, |markdown, cx| {
markdown.append(text, cx);
markdown.append(text, window, cx);
});
}
}
@@ -159,7 +194,7 @@ impl ActiveThread {
.message(*message_id)
.map(|message| message.text.clone())
{
self.push_message(message_id, message_text, cx);
self.push_message(message_id, message_text, window, cx);
}
cx.notify();
@@ -176,7 +211,7 @@ impl ActiveThread {
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);
let task = tool.run(tool_use.input, self.workspace.clone(), window, cx);
self.thread.update(cx, |thread, cx| {
thread.insert_tool_output(
@@ -193,7 +228,7 @@ impl ActiveThread {
}
}
fn render_message(&self, ix: usize, cx: &mut ViewContext<Self>) -> AnyElement {
fn render_message(&self, ix: usize, cx: &mut ModelContext<Self>) -> AnyElement {
let message_id = self.messages[ix];
let Some(message) = self.thread.read(cx).message(message_id) else {
return Empty.into_any();
@@ -204,11 +239,12 @@ impl ActiveThread {
};
let context = self.thread.read(cx).context_for_message(message_id);
let colors = cx.theme().colors();
let (role_icon, role_name) = match message.role {
Role::User => (IconName::Person, "You"),
Role::Assistant => (IconName::ZedAssistant, "Assistant"),
Role::System => (IconName::Settings, "System"),
let (role_icon, role_name, role_color) = match message.role {
Role::User => (IconName::Person, "You", Color::Muted),
Role::Assistant => (IconName::ZedAssistant, "Assistant", Color::Accent),
Role::System => (IconName::Settings, "System", Color::Default),
};
div()
@@ -218,36 +254,45 @@ impl ActiveThread {
.child(
v_flex()
.border_1()
.border_color(cx.theme().colors().border)
.bg(cx.theme().colors().editor_background)
.border_color(colors.border_variant)
.bg(colors.editor_background)
.rounded_md()
.child(
h_flex()
.justify_between()
.py_1()
.px_2()
.py_1p5()
.px_2p5()
.border_b_1()
.border_color(cx.theme().colors().border_variant)
.border_color(colors.border_variant)
.justify_between()
.child(
h_flex()
.gap_1p5()
.child(
Icon::new(role_icon)
.size(IconSize::XSmall)
.color(Color::Muted),
.color(role_color),
)
.child(Label::new(role_name).size(LabelSize::XSmall)),
.child(
Label::new(role_name)
.size(LabelSize::XSmall)
.color(role_color),
),
),
)
.child(v_flex().px_2().py_1().text_ui(cx).child(markdown.clone()))
.child(div().p_2p5().text_ui(cx).child(markdown.clone()))
.when_some(context, |parent, context| {
parent.child(
h_flex().flex_wrap().gap_2().p_1p5().children(
context
.iter()
.map(|context| ContextPill::new(context.clone())),
),
)
if !context.is_empty() {
parent.child(
h_flex()
.flex_wrap()
.gap_1()
.px_1p5()
.pb_1p5()
.children(context.iter().map(|c| ContextPill::new(c.clone()))),
)
} else {
parent
}
}),
)
.into_any()
@@ -255,7 +300,7 @@ impl ActiveThread {
}
impl Render for ActiveThread {
fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
fn render(&mut self, _window: &mut Window, _cx: &mut ModelContext<Self>) -> impl IntoElement {
list(self.list_state.clone()).flex_1().py_1()
}
}

View File

@@ -1,21 +1,24 @@
mod active_thread;
mod assistant_model_selector;
mod assistant_panel;
mod assistant_settings;
mod buffer_codegen;
mod context;
mod context_picker;
mod context_store;
mod context_strip;
mod inline_assistant;
mod inline_prompt_editor;
mod message_editor;
mod prompts;
mod streaming_diff;
mod terminal_codegen;
mod terminal_inline_assistant;
mod thread;
mod thread_history;
mod thread_store;
mod ui;
use std::any::TypeId;
use std::sync::Arc;
use client::Client;
@@ -79,10 +82,6 @@ pub fn init(fs: Arc<dyn Fs>, client: Arc<Client>, stdout_is_a_pty: bool, cx: &mu
}
fn feature_gate_assistant2_actions(cx: &mut AppContext) {
const ASSISTANT1_NAMESPACE: &str = "assistant";
let inline_assist_actions = [TypeId::of::<zed_actions::InlineAssist>()];
CommandPaletteFilter::update_global(cx, |filter, _cx| {
filter.hide_namespace(NAMESPACE);
});
@@ -91,17 +90,10 @@ fn feature_gate_assistant2_actions(cx: &mut AppContext) {
if is_enabled {
CommandPaletteFilter::update_global(cx, |filter, _cx| {
filter.show_namespace(NAMESPACE);
filter.hide_namespace(ASSISTANT1_NAMESPACE);
// We're hiding all of the `assistant: ` actions, but we want to
// keep the inline assist action around so we can use the same
// one in Assistant2.
filter.show_action_types(inline_assist_actions.iter());
});
} else {
CommandPaletteFilter::update_global(cx, |filter, _cx| {
filter.hide_namespace(NAMESPACE);
filter.show_namespace(ASSISTANT1_NAMESPACE);
});
}
})

View File

@@ -0,0 +1,93 @@
use fs::Fs;
use gpui::{Focusable, Model};
use language_model::LanguageModelRegistry;
use language_model_selector::{LanguageModelSelector, LanguageModelSelectorPopoverMenu};
use settings::update_settings_file;
use std::sync::Arc;
use ui::{prelude::*, ButtonLike, PopoverMenuHandle, Tooltip};
use crate::{assistant_settings::AssistantSettings, ToggleModelSelector};
pub struct AssistantModelSelector {
selector: Model<LanguageModelSelector>,
menu_handle: PopoverMenuHandle<LanguageModelSelector>,
}
impl AssistantModelSelector {
pub(crate) fn new(
fs: Arc<dyn Fs>,
menu_handle: PopoverMenuHandle<LanguageModelSelector>,
window: &mut Window,
cx: &mut AppContext,
) -> Self {
Self {
selector: cx.new_model(|cx| {
let fs = fs.clone();
LanguageModelSelector::new(
move |model, cx| {
update_settings_file::<AssistantSettings>(
fs.clone(),
cx,
move |settings, _cx| settings.set_model(model.clone()),
);
},
window,
cx,
)
}),
menu_handle,
}
}
}
impl Render for AssistantModelSelector {
fn render(&mut self, _window: &mut Window, cx: &mut ModelContext<Self>) -> impl IntoElement {
let active_model = LanguageModelRegistry::read_global(cx).active_model();
let focus_handle = self.selector.focus_handle(cx).clone();
LanguageModelSelectorPopoverMenu::new(
self.selector.clone(),
ButtonLike::new("active-model")
.style(ButtonStyle::Subtle)
.child(
h_flex()
.w_full()
.gap_0p5()
.child(
div()
.overflow_x_hidden()
.flex_grow()
.whitespace_nowrap()
.child(match active_model {
Some(model) => h_flex()
.child(
Label::new(model.name().0)
.size(LabelSize::Small)
.color(Color::Muted),
)
.into_any_element(),
_ => Label::new("No model selected")
.size(LabelSize::Small)
.color(Color::Muted)
.into_any_element(),
}),
)
.child(
Icon::new(IconName::ChevronDown)
.color(Color::Muted)
.size(IconSize::XSmall),
),
)
.tooltip(move |window, cx| {
Tooltip::for_action_in(
"Change Model",
&ToggleModelSelector,
&focus_handle,
window,
cx,
)
}),
)
.with_handle(self.menu_handle.clone())
}
}

View File

@@ -6,8 +6,7 @@ use client::zed_urls;
use fs::Fs;
use gpui::{
prelude::*, px, svg, Action, AnyElement, AppContext, AsyncWindowContext, EventEmitter,
FocusHandle, FocusableView, FontWeight, Model, Pixels, Task, View, ViewContext, WeakView,
WindowContext,
FocusHandle, Focusable, FontWeight, Model, ModelContext, Pixels, Task, WeakModel, Window,
};
use language::LanguageRegistry;
use settings::Settings;
@@ -25,11 +24,24 @@ use crate::thread_store::ThreadStore;
use crate::{NewThread, OpenHistory, ToggleFocus};
pub fn init(cx: &mut AppContext) {
cx.observe_new_views(
|workspace: &mut Workspace, _cx: &mut ViewContext<Workspace>| {
workspace.register_action(|workspace, _: &ToggleFocus, cx| {
workspace.toggle_panel_focus::<AssistantPanel>(cx);
});
cx.observe_new_models(
|workspace: &mut Workspace, _window, _cx: &mut ModelContext<Workspace>| {
workspace
.register_action(|workspace, _: &ToggleFocus, window, cx| {
workspace.toggle_panel_focus::<AssistantPanel>(window, cx);
})
.register_action(|workspace, _: &NewThread, window, cx| {
if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
panel.update(cx, |panel, cx| panel.new_thread(window, cx));
workspace.focus_panel::<AssistantPanel>(window, cx);
}
})
.register_action(|workspace, _: &OpenHistory, window, cx| {
if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
workspace.focus_panel::<AssistantPanel>(window, cx);
panel.update(cx, |panel, cx| panel.open_history(window, cx));
}
});
},
)
.detach();
@@ -41,25 +53,25 @@ enum ActiveView {
}
pub struct AssistantPanel {
workspace: WeakView<Workspace>,
workspace: WeakModel<Workspace>,
fs: Arc<dyn Fs>,
language_registry: Arc<LanguageRegistry>,
thread_store: Model<ThreadStore>,
thread: View<ActiveThread>,
message_editor: View<MessageEditor>,
thread: Model<ActiveThread>,
message_editor: Model<MessageEditor>,
tools: Arc<ToolWorkingSet>,
local_timezone: UtcOffset,
active_view: ActiveView,
history: View<ThreadHistory>,
history: Model<ThreadHistory>,
width: Option<Pixels>,
height: Option<Pixels>,
}
impl AssistantPanel {
pub fn load(
workspace: WeakView<Workspace>,
workspace: WeakModel<Workspace>,
cx: AsyncWindowContext,
) -> Task<Result<View<Self>>> {
) -> Task<Result<Model<Self>>> {
cx.spawn(|mut cx| async move {
let tools = Arc::new(ToolWorkingSet::default());
let thread_store = workspace
@@ -69,8 +81,8 @@ impl AssistantPanel {
})?
.await?;
workspace.update(&mut cx, |workspace, cx| {
cx.new_view(|cx| Self::new(workspace, thread_store, tools, cx))
workspace.update_in(&mut cx, |workspace, window, cx| {
cx.new_model(|cx| Self::new(workspace, thread_store, tools, window, cx))
})
})
}
@@ -79,13 +91,14 @@ impl AssistantPanel {
workspace: &Workspace,
thread_store: Model<ThreadStore>,
tools: Arc<ToolWorkingSet>,
cx: &mut ViewContext<Self>,
window: &mut Window,
cx: &mut ModelContext<Self>,
) -> Self {
let thread = thread_store.update(cx, |this, cx| this.create_thread(cx));
let fs = workspace.app_state().fs.clone();
let language_registry = workspace.project().read(cx).languages().clone();
let workspace = workspace.weak_handle();
let weak_self = cx.view().downgrade();
let weak_self = cx.model().downgrade();
Self {
active_view: ActiveView::Thread,
@@ -93,21 +106,23 @@ impl AssistantPanel {
fs: fs.clone(),
language_registry: language_registry.clone(),
thread_store: thread_store.clone(),
thread: cx.new_view(|cx| {
thread: cx.new_model(|cx| {
ActiveThread::new(
thread.clone(),
workspace.clone(),
language_registry,
tools.clone(),
window,
cx,
)
}),
message_editor: cx.new_view(|cx| {
message_editor: cx.new_model(|cx| {
MessageEditor::new(
fs.clone(),
workspace,
thread_store.downgrade(),
thread.clone(),
window,
cx,
)
}),
@@ -116,7 +131,7 @@ impl AssistantPanel {
chrono::Local::now().offset().local_minus_utc(),
)
.unwrap(),
history: cx.new_view(|cx| ThreadHistory::new(weak_self, thread_store, cx)),
history: cx.new_model(|cx| ThreadHistory::new(weak_self, thread_store, cx)),
width: None,
height: None,
}
@@ -130,34 +145,47 @@ impl AssistantPanel {
&self.thread_store
}
fn new_thread(&mut self, cx: &mut ViewContext<Self>) {
fn new_thread(&mut self, window: &mut Window, cx: &mut ModelContext<Self>) {
let thread = self
.thread_store
.update(cx, |this, cx| this.create_thread(cx));
self.active_view = ActiveView::Thread;
self.thread = cx.new_view(|cx| {
self.thread = cx.new_model(|cx| {
ActiveThread::new(
thread.clone(),
self.workspace.clone(),
self.language_registry.clone(),
self.tools.clone(),
window,
cx,
)
});
self.message_editor = cx.new_view(|cx| {
self.message_editor = cx.new_model(|cx| {
MessageEditor::new(
self.fs.clone(),
self.workspace.clone(),
self.thread_store.downgrade(),
thread,
window,
cx,
)
});
self.message_editor.focus_handle(cx).focus(cx);
self.message_editor.focus_handle(cx).focus(window);
}
pub(crate) fn open_thread(&mut self, thread_id: &ThreadId, cx: &mut ViewContext<Self>) {
fn open_history(&mut self, window: &mut Window, cx: &mut ModelContext<Self>) {
self.active_view = ActiveView::History;
self.history.focus_handle(cx).focus(window);
cx.notify();
}
pub(crate) fn open_thread(
&mut self,
thread_id: &ThreadId,
window: &mut Window,
cx: &mut ModelContext<Self>,
) {
let Some(thread) = self
.thread_store
.update(cx, |this, cx| this.open_thread(thread_id, cx))
@@ -166,34 +194,36 @@ impl AssistantPanel {
};
self.active_view = ActiveView::Thread;
self.thread = cx.new_view(|cx| {
self.thread = cx.new_model(|cx| {
ActiveThread::new(
thread.clone(),
self.workspace.clone(),
self.language_registry.clone(),
self.tools.clone(),
window,
cx,
)
});
self.message_editor = cx.new_view(|cx| {
self.message_editor = cx.new_model(|cx| {
MessageEditor::new(
self.fs.clone(),
self.workspace.clone(),
self.thread_store.downgrade(),
thread,
window,
cx,
)
});
self.message_editor.focus_handle(cx).focus(cx);
self.message_editor.focus_handle(cx).focus(window);
}
pub(crate) fn delete_thread(&mut self, thread_id: &ThreadId, cx: &mut ViewContext<Self>) {
pub(crate) fn delete_thread(&mut self, thread_id: &ThreadId, cx: &mut ModelContext<Self>) {
self.thread_store
.update(cx, |this, cx| this.delete_thread(thread_id, cx));
}
}
impl FocusableView for AssistantPanel {
impl Focusable for AssistantPanel {
fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
match self.active_view {
ActiveView::Thread => self.message_editor.focus_handle(cx),
@@ -209,7 +239,7 @@ impl Panel for AssistantPanel {
"AssistantPanel2"
}
fn position(&self, _cx: &WindowContext) -> DockPosition {
fn position(&self, _window: &Window, _cx: &AppContext) -> DockPosition {
DockPosition::Right
}
@@ -217,7 +247,12 @@ impl Panel for AssistantPanel {
true
}
fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext<Self>) {
fn set_position(
&mut self,
position: DockPosition,
_: &mut Window,
cx: &mut ModelContext<Self>,
) {
settings::update_settings_file::<AssistantSettings>(
self.fs.clone(),
cx,
@@ -232,9 +267,9 @@ impl Panel for AssistantPanel {
);
}
fn size(&self, cx: &WindowContext) -> Pixels {
fn size(&self, window: &Window, cx: &AppContext) -> Pixels {
let settings = AssistantSettings::get_global(cx);
match self.position(cx) {
match self.position(window, cx) {
DockPosition::Left | DockPosition::Right => {
self.width.unwrap_or(settings.default_width)
}
@@ -242,35 +277,39 @@ impl Panel for AssistantPanel {
}
}
fn set_size(&mut self, size: Option<Pixels>, cx: &mut ViewContext<Self>) {
match self.position(cx) {
fn set_size(&mut self, size: Option<Pixels>, window: &mut Window, cx: &mut ModelContext<Self>) {
match self.position(window, cx) {
DockPosition::Left | DockPosition::Right => self.width = size,
DockPosition::Bottom => self.height = size,
}
cx.notify();
}
fn set_active(&mut self, _active: bool, _cx: &mut ViewContext<Self>) {}
fn set_active(&mut self, _active: bool, _window: &mut Window, _cx: &mut ModelContext<Self>) {}
fn remote_id() -> Option<proto::PanelId> {
Some(proto::PanelId::AssistantPanel)
}
fn icon(&self, _cx: &WindowContext) -> Option<IconName> {
Some(IconName::ZedAssistant)
fn icon(&self, _window: &Window, _cx: &AppContext) -> Option<IconName> {
Some(IconName::ZedAssistant2)
}
fn icon_tooltip(&self, _cx: &WindowContext) -> Option<&'static str> {
fn icon_tooltip(&self, _window: &Window, _cx: &AppContext) -> Option<&'static str> {
Some("Assistant Panel")
}
fn toggle_action(&self) -> Box<dyn Action> {
Box::new(ToggleFocus)
}
fn activation_priority(&self) -> u32 {
3
}
}
impl AssistantPanel {
fn render_toolbar(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
fn render_toolbar(&self, cx: &mut ModelContext<Self>) -> impl IntoElement {
let focus_handle = self.focus_handle(cx);
h_flex()
@@ -296,17 +335,18 @@ impl AssistantPanel {
.style(ButtonStyle::Subtle)
.tooltip({
let focus_handle = focus_handle.clone();
move |cx| {
move |window, cx| {
Tooltip::for_action_in(
"New Thread",
&NewThread,
&focus_handle,
window,
cx,
)
}
})
.on_click(move |_event, cx| {
cx.dispatch_action(NewThread.boxed_clone());
.on_click(move |_event, window, cx| {
window.dispatch_action(NewThread.boxed_clone(), cx);
}),
)
.child(
@@ -315,40 +355,51 @@ impl AssistantPanel {
.style(ButtonStyle::Subtle)
.tooltip({
let focus_handle = focus_handle.clone();
move |cx| {
move |window, cx| {
Tooltip::for_action_in(
"Open History",
&OpenHistory,
&focus_handle,
window,
cx,
)
}
})
.on_click(move |_event, cx| {
cx.dispatch_action(OpenHistory.boxed_clone());
.on_click(move |_event, window, cx| {
window.dispatch_action(OpenHistory.boxed_clone(), cx);
}),
)
.child(
IconButton::new("configure-assistant", IconName::Settings)
.icon_size(IconSize::Small)
.style(ButtonStyle::Subtle)
.tooltip(move |cx| Tooltip::text("Configure Assistant", cx))
.on_click(move |_event, _cx| {
.tooltip(Tooltip::text("Configure Assistant"))
.on_click(move |_event, _window, _cx| {
println!("Configure Assistant");
}),
),
)
}
fn render_active_thread_or_empty_state(&self, cx: &mut ViewContext<Self>) -> AnyElement {
fn render_active_thread_or_empty_state(
&self,
window: &mut Window,
cx: &mut ModelContext<Self>,
) -> AnyElement {
if self.thread.read(cx).is_empty() {
return self.render_thread_empty_state(cx).into_any_element();
return self
.render_thread_empty_state(window, cx)
.into_any_element();
}
self.thread.clone().into_any()
self.thread.clone().into_any_element()
}
fn render_thread_empty_state(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
fn render_thread_empty_state(
&self,
window: &mut Window,
cx: &mut ModelContext<Self>,
) -> impl IntoElement {
let recent_threads = self
.thread_store
.update(cx, |this, cx| this.recent_threads(3, cx));
@@ -379,7 +430,7 @@ impl AssistantPanel {
v_flex().mx_auto().w_4_5().gap_2().children(
recent_threads
.into_iter()
.map(|thread| PastThread::new(thread, cx.view().downgrade())),
.map(|thread| PastThread::new(thread, cx.model().downgrade())),
),
)
.child(
@@ -390,17 +441,17 @@ impl AssistantPanel {
.key_binding(KeyBinding::for_action_in(
&OpenHistory,
&self.focus_handle(cx),
cx,
window,
))
.on_click(move |_event, cx| {
cx.dispatch_action(OpenHistory.boxed_clone());
.on_click(move |_event, window, cx| {
window.dispatch_action(OpenHistory.boxed_clone(), cx);
}),
),
)
})
}
fn render_last_error(&self, cx: &mut ViewContext<Self>) -> Option<AnyElement> {
fn render_last_error(&self, cx: &mut ModelContext<Self>) -> Option<AnyElement> {
let last_error = self.thread.read(cx).last_error()?;
Some(
@@ -426,7 +477,7 @@ impl AssistantPanel {
)
}
fn render_payment_required_error(&self, cx: &mut ViewContext<Self>) -> AnyElement {
fn render_payment_required_error(&self, cx: &mut ModelContext<Self>) -> AnyElement {
const ERROR_MESSAGE: &str = "Free tier exceeded. Subscribe and add payment to continue using Zed LLMs. You'll be billed at cost for tokens used.";
v_flex()
@@ -450,7 +501,7 @@ impl AssistantPanel {
.justify_end()
.mt_1()
.child(Button::new("subscribe", "Subscribe").on_click(cx.listener(
|this, _, cx| {
|this, _, _, cx| {
this.thread.update(cx, |this, _cx| {
this.clear_last_error();
});
@@ -460,7 +511,7 @@ impl AssistantPanel {
},
)))
.child(Button::new("dismiss", "Dismiss").on_click(cx.listener(
|this, _, cx| {
|this, _, _, cx| {
this.thread.update(cx, |this, _cx| {
this.clear_last_error();
});
@@ -472,7 +523,7 @@ impl AssistantPanel {
.into_any()
}
fn render_max_monthly_spend_reached_error(&self, cx: &mut ViewContext<Self>) -> AnyElement {
fn render_max_monthly_spend_reached_error(&self, cx: &mut ModelContext<Self>) -> AnyElement {
const ERROR_MESSAGE: &str = "You have reached your maximum monthly spend. Increase your spend limit to continue using Zed LLMs.";
v_flex()
@@ -497,7 +548,7 @@ impl AssistantPanel {
.mt_1()
.child(
Button::new("subscribe", "Update Monthly Spend Limit").on_click(
cx.listener(|this, _, cx| {
cx.listener(|this, _, _, cx| {
this.thread.update(cx, |this, _cx| {
this.clear_last_error();
});
@@ -508,7 +559,7 @@ impl AssistantPanel {
),
)
.child(Button::new("dismiss", "Dismiss").on_click(cx.listener(
|this, _, cx| {
|this, _, _, cx| {
this.thread.update(cx, |this, _cx| {
this.clear_last_error();
});
@@ -523,7 +574,7 @@ impl AssistantPanel {
fn render_error_message(
&self,
error_message: &SharedString,
cx: &mut ViewContext<Self>,
cx: &mut ModelContext<Self>,
) -> AnyElement {
v_flex()
.gap_0p5()
@@ -549,7 +600,7 @@ impl AssistantPanel {
.justify_end()
.mt_1()
.child(Button::new("dismiss", "Dismiss").on_click(cx.listener(
|this, _, cx| {
|this, _, _, cx| {
this.thread.update(cx, |this, _cx| {
this.clear_last_error();
});
@@ -563,23 +614,21 @@ impl AssistantPanel {
}
impl Render for AssistantPanel {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
fn render(&mut self, window: &mut Window, cx: &mut ModelContext<Self>) -> impl IntoElement {
v_flex()
.key_context("AssistantPanel2")
.justify_between()
.size_full()
.on_action(cx.listener(|this, _: &NewThread, cx| {
this.new_thread(cx);
.on_action(cx.listener(|this, _: &NewThread, window, cx| {
this.new_thread(window, cx);
}))
.on_action(cx.listener(|this, _: &OpenHistory, cx| {
this.active_view = ActiveView::History;
this.history.focus_handle(cx).focus(cx);
cx.notify();
.on_action(cx.listener(|this, _: &OpenHistory, window, cx| {
this.open_history(window, cx);
}))
.child(self.render_toolbar(cx))
.map(|parent| match self.active_view {
ActiveView::Thread => parent
.child(self.render_active_thread_or_empty_state(cx))
.child(self.render_active_thread_or_empty_state(window, cx))
.child(
h_flex()
.border_t_1()

File diff suppressed because it is too large Load Diff

View File

@@ -21,9 +21,10 @@ pub struct Context {
pub text: SharedString,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ContextKind {
File,
Directory,
FetchedUrl,
Thread,
}
@@ -33,6 +34,7 @@ pub fn attach_context_to_message(
context: impl IntoIterator<Item = Context>,
) {
let mut file_context = String::new();
let mut directory_context = String::new();
let mut fetch_context = String::new();
let mut thread_context = String::new();
@@ -42,6 +44,10 @@ pub fn attach_context_to_message(
file_context.push_str(&context.text);
file_context.push('\n');
}
ContextKind::Directory => {
directory_context.push_str(&context.text);
directory_context.push('\n');
}
ContextKind::FetchedUrl => {
fetch_context.push_str(&context.name);
fetch_context.push('\n');
@@ -63,6 +69,11 @@ pub fn attach_context_to_message(
context_text.push_str(&file_context);
}
if !directory_context.is_empty() {
context_text.push_str("The following directories are available:\n");
context_text.push_str(&directory_context);
}
if !fetch_context.is_empty() {
context_text.push_str("The following fetched results are available\n");
context_text.push_str(&fetch_context);
@@ -73,5 +84,7 @@ pub fn attach_context_to_message(
context_text.push_str(&thread_context);
}
message.content.push(MessageContent::Text(context_text));
if !context_text.is_empty() {
message.content.push(MessageContent::Text(context_text));
}
}

View File

@@ -1,3 +1,4 @@
mod directory_context_picker;
mod fetch_context_picker;
mod file_context_picker;
mod thread_context_picker;
@@ -5,73 +6,95 @@ mod thread_context_picker;
use std::sync::Arc;
use gpui::{
AppContext, DismissEvent, EventEmitter, FocusHandle, FocusableView, SharedString, Task, View,
WeakModel, WeakView,
AppContext, DismissEvent, EventEmitter, FocusHandle, Focusable, Model, SharedString, Task,
WeakModel,
};
use picker::{Picker, PickerDelegate};
use release_channel::ReleaseChannel;
use ui::{prelude::*, ListItem, ListItemSpacing};
use util::ResultExt;
use workspace::Workspace;
use crate::context::ContextKind;
use crate::context_picker::directory_context_picker::DirectoryContextPicker;
use crate::context_picker::fetch_context_picker::FetchContextPicker;
use crate::context_picker::file_context_picker::FileContextPicker;
use crate::context_picker::thread_context_picker::ThreadContextPicker;
use crate::context_store::ContextStore;
use crate::thread_store::ThreadStore;
#[derive(Debug, Clone, Copy)]
pub enum ConfirmBehavior {
KeepOpen,
Close,
}
#[derive(Debug, Clone)]
enum ContextPickerMode {
Default,
File(View<FileContextPicker>),
Fetch(View<FetchContextPicker>),
Thread(View<ThreadContextPicker>),
File(Model<FileContextPicker>),
Directory(Model<DirectoryContextPicker>),
Fetch(Model<FetchContextPicker>),
Thread(Model<ThreadContextPicker>),
}
pub(super) struct ContextPicker {
mode: ContextPickerMode,
picker: View<Picker<ContextPickerDelegate>>,
picker: Model<Picker<ContextPickerDelegate>>,
}
impl ContextPicker {
pub fn new(
workspace: WeakView<Workspace>,
workspace: WeakModel<Workspace>,
thread_store: Option<WeakModel<ThreadStore>>,
context_store: WeakModel<ContextStore>,
cx: &mut ViewContext<Self>,
confirm_behavior: ConfirmBehavior,
window: &mut Window,
cx: &mut ModelContext<Self>,
) -> Self {
let mut entries = vec![
ContextPickerEntry {
name: "Directory".into(),
let mut entries = Vec::new();
entries.push(ContextPickerEntry {
name: "File".into(),
kind: ContextKind::File,
icon: IconName::File,
});
let release_channel = ReleaseChannel::global(cx);
// The directory context picker isn't fully implemented yet, so limit it
// to development builds.
if release_channel == ReleaseChannel::Dev {
entries.push(ContextPickerEntry {
name: "Folder".into(),
kind: ContextKind::Directory,
icon: IconName::Folder,
},
ContextPickerEntry {
name: "File".into(),
icon: IconName::File,
},
ContextPickerEntry {
name: "Fetch".into(),
icon: IconName::Globe,
},
];
});
}
entries.push(ContextPickerEntry {
name: "Fetch".into(),
kind: ContextKind::FetchedUrl,
icon: IconName::Globe,
});
if thread_store.is_some() {
entries.push(ContextPickerEntry {
name: "Thread".into(),
kind: ContextKind::Thread,
icon: IconName::MessageCircle,
});
}
let delegate = ContextPickerDelegate {
context_picker: cx.view().downgrade(),
context_picker: cx.model().downgrade(),
workspace,
thread_store,
context_store,
confirm_behavior,
entries,
selected_ix: 0,
};
let picker = cx.new_view(|cx| {
Picker::nonsearchable_uniform_list(delegate, cx).max_height(Some(rems(20.).into()))
let picker = cx.new_model(|cx| {
Picker::nonsearchable_uniform_list(delegate, window, cx)
.max_height(Some(rems(20.).into()))
});
ContextPicker {
@@ -87,11 +110,12 @@ impl ContextPicker {
impl EventEmitter<DismissEvent> for ContextPicker {}
impl FocusableView for ContextPicker {
impl Focusable for ContextPicker {
fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
match &self.mode {
ContextPickerMode::Default => self.picker.focus_handle(cx),
ContextPickerMode::File(file_picker) => file_picker.focus_handle(cx),
ContextPickerMode::Directory(directory_picker) => directory_picker.focus_handle(cx),
ContextPickerMode::Fetch(fetch_picker) => fetch_picker.focus_handle(cx),
ContextPickerMode::Thread(thread_picker) => thread_picker.focus_handle(cx),
}
@@ -99,13 +123,16 @@ impl FocusableView for ContextPicker {
}
impl Render for ContextPicker {
fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
fn render(&mut self, _window: &mut Window, _cx: &mut ModelContext<Self>) -> impl IntoElement {
v_flex()
.w(px(400.))
.min_w(px(400.))
.map(|parent| match &self.mode {
ContextPickerMode::Default => parent.child(self.picker.clone()),
ContextPickerMode::File(file_picker) => parent.child(file_picker.clone()),
ContextPickerMode::Directory(directory_picker) => {
parent.child(directory_picker.clone())
}
ContextPickerMode::Fetch(fetch_picker) => parent.child(fetch_picker.clone()),
ContextPickerMode::Thread(thread_picker) => parent.child(thread_picker.clone()),
})
@@ -115,14 +142,16 @@ impl Render for ContextPicker {
#[derive(Clone)]
struct ContextPickerEntry {
name: SharedString,
kind: ContextKind,
icon: IconName,
}
pub(crate) struct ContextPickerDelegate {
context_picker: WeakView<ContextPicker>,
workspace: WeakView<Workspace>,
context_picker: WeakModel<ContextPicker>,
workspace: WeakModel<Workspace>,
thread_store: Option<WeakModel<ThreadStore>>,
context_store: WeakModel<ContextStore>,
confirm_behavior: ConfirmBehavior,
entries: Vec<ContextPickerEntry>,
selected_ix: usize,
}
@@ -138,70 +167,103 @@ impl PickerDelegate for ContextPickerDelegate {
self.selected_ix
}
fn set_selected_index(&mut self, ix: usize, cx: &mut ViewContext<Picker<Self>>) {
fn set_selected_index(
&mut self,
ix: usize,
_: &mut Window,
cx: &mut ModelContext<Picker<Self>>,
) {
self.selected_ix = ix.min(self.entries.len().saturating_sub(1));
cx.notify();
}
fn placeholder_text(&self, _cx: &mut WindowContext) -> Arc<str> {
fn placeholder_text(&self, _window: &mut Window, _cx: &mut AppContext) -> Arc<str> {
"Select a context source…".into()
}
fn update_matches(&mut self, _query: String, _cx: &mut ViewContext<Picker<Self>>) -> Task<()> {
fn update_matches(
&mut self,
_query: String,
_window: &mut Window,
_cx: &mut ModelContext<Picker<Self>>,
) -> Task<()> {
Task::ready(())
}
fn confirm(&mut self, _secondary: bool, cx: &mut ViewContext<Picker<Self>>) {
fn confirm(
&mut self,
_secondary: bool,
window: &mut Window,
cx: &mut ModelContext<Picker<Self>>,
) {
if let Some(entry) = self.entries.get(self.selected_ix) {
self.context_picker
.update(cx, |this, cx| {
match entry.name.to_string().as_str() {
"File" => {
this.mode = ContextPickerMode::File(cx.new_view(|cx| {
match entry.kind {
ContextKind::File => {
this.mode = ContextPickerMode::File(cx.new_model(|cx| {
FileContextPicker::new(
self.context_picker.clone(),
self.workspace.clone(),
self.context_store.clone(),
self.confirm_behavior,
window,
cx,
)
}));
}
"Fetch" => {
this.mode = ContextPickerMode::Fetch(cx.new_view(|cx| {
ContextKind::Directory => {
this.mode = ContextPickerMode::Directory(cx.new_model(|cx| {
DirectoryContextPicker::new(
self.context_picker.clone(),
self.workspace.clone(),
self.context_store.clone(),
self.confirm_behavior,
window,
cx,
)
}));
}
ContextKind::FetchedUrl => {
this.mode = ContextPickerMode::Fetch(cx.new_model(|cx| {
FetchContextPicker::new(
self.context_picker.clone(),
self.workspace.clone(),
self.context_store.clone(),
self.confirm_behavior,
window,
cx,
)
}));
}
"Thread" => {
ContextKind::Thread => {
if let Some(thread_store) = self.thread_store.as_ref() {
this.mode = ContextPickerMode::Thread(cx.new_view(|cx| {
this.mode = ContextPickerMode::Thread(cx.new_model(|cx| {
ThreadContextPicker::new(
thread_store.clone(),
self.context_picker.clone(),
self.context_store.clone(),
self.confirm_behavior,
window,
cx,
)
}));
}
}
_ => {}
}
cx.focus_self();
cx.focus_self(window);
})
.log_err();
}
}
fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>) {
fn dismissed(&mut self, _: &mut Window, cx: &mut ModelContext<Picker<Self>>) {
self.context_picker
.update(cx, |this, cx| match this.mode {
ContextPickerMode::Default => cx.emit(DismissEvent),
ContextPickerMode::File(_)
| ContextPickerMode::Directory(_)
| ContextPickerMode::Fetch(_)
| ContextPickerMode::Thread(_) => {}
})
@@ -212,7 +274,8 @@ impl PickerDelegate for ContextPickerDelegate {
&self,
ix: usize,
selected: bool,
_cx: &mut ViewContext<Picker<Self>>,
_window: &mut Window,
_cx: &mut ModelContext<Picker<Self>>,
) -> Option<Self::ListItem> {
let entry = &self.entries[ix];

View File

@@ -0,0 +1,267 @@
// TODO: Remove this when we finish the implementation.
#![allow(unused)]
use std::path::Path;
use std::sync::atomic::AtomicBool;
use std::sync::Arc;
use fuzzy::PathMatch;
use gpui::{AppContext, DismissEvent, FocusHandle, Focusable, Model, Task, WeakModel};
use picker::{Picker, PickerDelegate};
use project::{PathMatchCandidateSet, WorktreeId};
use ui::{prelude::*, ListItem};
use util::ResultExt as _;
use workspace::Workspace;
use crate::context::ContextKind;
use crate::context_picker::{ConfirmBehavior, ContextPicker};
use crate::context_store::ContextStore;
pub struct DirectoryContextPicker {
picker: Model<Picker<DirectoryContextPickerDelegate>>,
}
impl DirectoryContextPicker {
pub fn new(
context_picker: WeakModel<ContextPicker>,
workspace: WeakModel<Workspace>,
context_store: WeakModel<ContextStore>,
confirm_behavior: ConfirmBehavior,
window: &mut Window,
cx: &mut ModelContext<Self>,
) -> Self {
let delegate = DirectoryContextPickerDelegate::new(
context_picker,
workspace,
context_store,
confirm_behavior,
);
let picker = cx.new_model(|cx| Picker::uniform_list(delegate, window, cx));
Self { picker }
}
}
impl Focusable for DirectoryContextPicker {
fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
self.picker.focus_handle(cx)
}
}
impl Render for DirectoryContextPicker {
fn render(&mut self, _window: &mut Window, _cx: &mut ModelContext<Self>) -> impl IntoElement {
self.picker.clone()
}
}
pub struct DirectoryContextPickerDelegate {
context_picker: WeakModel<ContextPicker>,
workspace: WeakModel<Workspace>,
context_store: WeakModel<ContextStore>,
confirm_behavior: ConfirmBehavior,
matches: Vec<PathMatch>,
selected_index: usize,
}
impl DirectoryContextPickerDelegate {
pub fn new(
context_picker: WeakModel<ContextPicker>,
workspace: WeakModel<Workspace>,
context_store: WeakModel<ContextStore>,
confirm_behavior: ConfirmBehavior,
) -> Self {
Self {
context_picker,
workspace,
context_store,
confirm_behavior,
matches: Vec::new(),
selected_index: 0,
}
}
fn search(
&mut self,
query: String,
cancellation_flag: Arc<AtomicBool>,
workspace: &Model<Workspace>,
window: &mut Window,
cx: &mut ModelContext<Picker<Self>>,
) -> Task<Vec<PathMatch>> {
if query.is_empty() {
let workspace = workspace.read(cx);
let project = workspace.project().read(cx);
let directory_matches = project.worktrees(cx).flat_map(|worktree| {
let worktree = worktree.read(cx);
let path_prefix: Arc<str> = worktree.root_name().into();
worktree.directories(false, 0).map(move |entry| PathMatch {
score: 0.,
positions: Vec::new(),
worktree_id: worktree.id().to_usize(),
path: entry.path.clone(),
path_prefix: path_prefix.clone(),
distance_to_relative_ancestor: 0,
is_dir: true,
})
});
Task::ready(directory_matches.collect())
} else {
let worktrees = workspace.read(cx).visible_worktrees(cx).collect::<Vec<_>>();
let candidate_sets = worktrees
.into_iter()
.map(|worktree| {
let worktree = worktree.read(cx);
PathMatchCandidateSet {
snapshot: worktree.snapshot(),
include_ignored: worktree
.root_entry()
.map_or(false, |entry| entry.is_ignored),
include_root_name: true,
candidates: project::Candidates::Directories,
}
})
.collect::<Vec<_>>();
let executor = cx.background_executor().clone();
cx.foreground_executor().spawn(async move {
fuzzy::match_path_sets(
candidate_sets.as_slice(),
query.as_str(),
None,
false,
100,
&cancellation_flag,
executor,
)
.await
})
}
}
}
impl PickerDelegate for DirectoryContextPickerDelegate {
type ListItem = ListItem;
fn match_count(&self) -> usize {
self.matches.len()
}
fn selected_index(&self) -> usize {
self.selected_index
}
fn set_selected_index(
&mut self,
ix: usize,
_window: &mut Window,
_cx: &mut ModelContext<Picker<Self>>,
) {
self.selected_index = ix;
}
fn placeholder_text(&self, _window: &mut Window, _cx: &mut AppContext) -> Arc<str> {
"Search folders…".into()
}
fn update_matches(
&mut self,
query: String,
window: &mut Window,
cx: &mut ModelContext<Picker<Self>>,
) -> Task<()> {
let Some(workspace) = self.workspace.upgrade() else {
return Task::ready(());
};
let search_task = self.search(query, Arc::<AtomicBool>::default(), &workspace, window, cx);
cx.spawn_in(window, |this, mut cx| async move {
let mut paths = search_task.await;
let empty_path = Path::new("");
paths.retain(|path_match| path_match.path.as_ref() != empty_path);
this.update(&mut cx, |this, _cx| {
this.delegate.matches = paths;
})
.log_err();
})
}
fn confirm(
&mut self,
_secondary: bool,
window: &mut Window,
cx: &mut ModelContext<Picker<Self>>,
) {
let Some(mat) = self.matches.get(self.selected_index) else {
return;
};
let workspace = self.workspace.clone();
let Some(project) = workspace
.upgrade()
.map(|workspace| workspace.read(cx).project().clone())
else {
return;
};
let path = mat.path.clone();
let worktree_id = WorktreeId::from_usize(mat.worktree_id);
let confirm_behavior = self.confirm_behavior;
cx.spawn_in(window, |this, mut cx| async move {
this.update_in(&mut cx, |this, window, cx| {
let mut text = String::new();
// TODO: Add the files from the selected directory.
this.delegate
.context_store
.update(cx, |context_store, cx| {
context_store.insert_context(
ContextKind::Directory,
path.to_string_lossy().to_string(),
text,
);
})?;
match confirm_behavior {
ConfirmBehavior::KeepOpen => {}
ConfirmBehavior::Close => this.delegate.dismissed(window, cx),
}
anyhow::Ok(())
})??;
anyhow::Ok(())
})
.detach_and_log_err(cx)
}
fn dismissed(&mut self, window: &mut Window, cx: &mut ModelContext<Picker<Self>>) {
self.context_picker
.update(cx, |this, cx| {
this.reset_mode();
cx.emit(DismissEvent);
})
.ok();
}
fn render_match(
&self,
ix: usize,
selected: bool,
_window: &mut Window,
_cx: &mut ModelContext<Picker<Self>>,
) -> Option<Self::ListItem> {
let path_match = &self.matches[ix];
let directory_name = path_match.path.to_string_lossy().to_string();
Some(
ListItem::new(ix)
.inset(true)
.toggle_state(selected)
.child(h_flex().gap_2().child(Label::new(directory_name))),
)
}
}

View File

@@ -4,43 +4,50 @@ use std::sync::Arc;
use anyhow::{bail, Context as _, Result};
use futures::AsyncReadExt as _;
use gpui::{AppContext, DismissEvent, FocusHandle, FocusableView, Task, View, WeakModel, WeakView};
use gpui::{AppContext, DismissEvent, FocusHandle, Focusable, Model, Task, WeakModel};
use html_to_markdown::{convert_html_to_markdown, markdown, TagHandler};
use http_client::{AsyncBody, HttpClientWithUrl};
use picker::{Picker, PickerDelegate};
use ui::{prelude::*, ListItem, ViewContext};
use ui::{prelude::*, ListItem, ModelContext, Window};
use workspace::Workspace;
use crate::context::ContextKind;
use crate::context_picker::ContextPicker;
use crate::context_picker::{ConfirmBehavior, ContextPicker};
use crate::context_store::ContextStore;
pub struct FetchContextPicker {
picker: View<Picker<FetchContextPickerDelegate>>,
picker: Model<Picker<FetchContextPickerDelegate>>,
}
impl FetchContextPicker {
pub fn new(
context_picker: WeakView<ContextPicker>,
workspace: WeakView<Workspace>,
context_picker: WeakModel<ContextPicker>,
workspace: WeakModel<Workspace>,
context_store: WeakModel<ContextStore>,
cx: &mut ViewContext<Self>,
confirm_behavior: ConfirmBehavior,
window: &mut Window,
cx: &mut ModelContext<Self>,
) -> Self {
let delegate = FetchContextPickerDelegate::new(context_picker, workspace, context_store);
let picker = cx.new_view(|cx| Picker::uniform_list(delegate, cx));
let delegate = FetchContextPickerDelegate::new(
context_picker,
workspace,
context_store,
confirm_behavior,
);
let picker = cx.new_model(|cx| Picker::uniform_list(delegate, window, cx));
Self { picker }
}
}
impl FocusableView for FetchContextPicker {
impl Focusable for FetchContextPicker {
fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
self.picker.focus_handle(cx)
}
}
impl Render for FetchContextPicker {
fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
fn render(&mut self, _window: &mut Window, _cx: &mut ModelContext<Self>) -> impl IntoElement {
self.picker.clone()
}
}
@@ -53,22 +60,25 @@ enum ContentType {
}
pub struct FetchContextPickerDelegate {
context_picker: WeakView<ContextPicker>,
workspace: WeakView<Workspace>,
context_picker: WeakModel<ContextPicker>,
workspace: WeakModel<Workspace>,
context_store: WeakModel<ContextStore>,
confirm_behavior: ConfirmBehavior,
url: String,
}
impl FetchContextPickerDelegate {
pub fn new(
context_picker: WeakView<ContextPicker>,
workspace: WeakView<Workspace>,
context_picker: WeakModel<ContextPicker>,
workspace: WeakModel<Workspace>,
context_store: WeakModel<ContextStore>,
confirm_behavior: ConfirmBehavior,
) -> Self {
FetchContextPickerDelegate {
context_picker,
workspace,
context_store,
confirm_behavior,
url: String::new(),
}
}
@@ -157,7 +167,7 @@ impl PickerDelegate for FetchContextPickerDelegate {
}
}
fn no_matches_text(&self, _cx: &mut WindowContext) -> SharedString {
fn no_matches_text(&self, _window: &mut Window, _cx: &mut AppContext) -> SharedString {
"Enter the URL that you would like to fetch".into()
}
@@ -165,34 +175,58 @@ impl PickerDelegate for FetchContextPickerDelegate {
0
}
fn set_selected_index(&mut self, _ix: usize, _cx: &mut ViewContext<Picker<Self>>) {}
fn set_selected_index(
&mut self,
_ix: usize,
_window: &mut Window,
_cx: &mut ModelContext<Picker<Self>>,
) {
}
fn placeholder_text(&self, _cx: &mut ui::WindowContext) -> Arc<str> {
fn placeholder_text(&self, _window: &mut Window, _cx: &mut AppContext) -> Arc<str> {
"Enter a URL…".into()
}
fn update_matches(&mut self, query: String, _cx: &mut ViewContext<Picker<Self>>) -> Task<()> {
fn update_matches(
&mut self,
query: String,
_window: &mut Window,
_cx: &mut ModelContext<Picker<Self>>,
) -> Task<()> {
self.url = query;
Task::ready(())
}
fn confirm(&mut self, _secondary: bool, cx: &mut ViewContext<Picker<Self>>) {
fn confirm(
&mut self,
_secondary: bool,
window: &mut Window,
cx: &mut ModelContext<Picker<Self>>,
) {
let Some(workspace) = self.workspace.upgrade() else {
return;
};
let http_client = workspace.read(cx).client().http_client().clone();
let url = self.url.clone();
cx.spawn(|this, mut cx| async move {
let confirm_behavior = self.confirm_behavior;
cx.spawn_in(window, |this, mut cx| async move {
let text = Self::build_message(http_client, &url).await?;
this.update(&mut cx, |this, cx| {
this.update_in(&mut cx, |this, window, cx| {
this.delegate
.context_store
.update(cx, |context_store, _cx| {
context_store.insert_context(ContextKind::FetchedUrl, url, text);
})
})?;
match confirm_behavior {
ConfirmBehavior::KeepOpen => {}
ConfirmBehavior::Close => this.delegate.dismissed(window, cx),
}
anyhow::Ok(())
})??;
anyhow::Ok(())
@@ -200,7 +234,7 @@ impl PickerDelegate for FetchContextPickerDelegate {
.detach_and_log_err(cx);
}
fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>) {
fn dismissed(&mut self, _: &mut Window, cx: &mut ModelContext<Picker<Self>>) {
self.context_picker
.update(cx, |this, cx| {
this.reset_mode();
@@ -213,7 +247,8 @@ impl PickerDelegate for FetchContextPickerDelegate {
&self,
ix: usize,
selected: bool,
_cx: &mut ViewContext<Picker<Self>>,
_window: &mut Window,
_cx: &mut ModelContext<Picker<Self>>,
) -> Option<Self::ListItem> {
Some(
ListItem::new(ix)

View File

@@ -1,11 +1,11 @@
use std::fmt::Write as _;
use std::ops::RangeInclusive;
use std::path::{Path, PathBuf};
use std::path::Path;
use std::sync::atomic::AtomicBool;
use std::sync::Arc;
use fuzzy::PathMatch;
use gpui::{AppContext, DismissEvent, FocusHandle, FocusableView, Task, View, WeakModel, WeakView};
use gpui::{AppContext, DismissEvent, FocusHandle, Focusable, Model, Task, WeakModel};
use picker::{Picker, PickerDelegate};
use project::{PathMatchCandidateSet, WorktreeId};
use ui::{prelude::*, ListItem};
@@ -13,57 +13,67 @@ use util::ResultExt as _;
use workspace::Workspace;
use crate::context::ContextKind;
use crate::context_picker::ContextPicker;
use crate::context_picker::{ConfirmBehavior, ContextPicker};
use crate::context_store::ContextStore;
pub struct FileContextPicker {
picker: View<Picker<FileContextPickerDelegate>>,
picker: Model<Picker<FileContextPickerDelegate>>,
}
impl FileContextPicker {
pub fn new(
context_picker: WeakView<ContextPicker>,
workspace: WeakView<Workspace>,
context_picker: WeakModel<ContextPicker>,
workspace: WeakModel<Workspace>,
context_store: WeakModel<ContextStore>,
cx: &mut ViewContext<Self>,
confirm_behavior: ConfirmBehavior,
window: &mut Window,
cx: &mut ModelContext<Self>,
) -> Self {
let delegate = FileContextPickerDelegate::new(context_picker, workspace, context_store);
let picker = cx.new_view(|cx| Picker::uniform_list(delegate, cx));
let delegate = FileContextPickerDelegate::new(
context_picker,
workspace,
context_store,
confirm_behavior,
);
let picker = cx.new_model(|cx| Picker::uniform_list(delegate, window, cx));
Self { picker }
}
}
impl FocusableView for FileContextPicker {
impl Focusable for FileContextPicker {
fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
self.picker.focus_handle(cx)
}
}
impl Render for FileContextPicker {
fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
fn render(&mut self, _window: &mut Window, _cx: &mut ModelContext<Self>) -> impl IntoElement {
self.picker.clone()
}
}
pub struct FileContextPickerDelegate {
context_picker: WeakView<ContextPicker>,
workspace: WeakView<Workspace>,
context_picker: WeakModel<ContextPicker>,
workspace: WeakModel<Workspace>,
context_store: WeakModel<ContextStore>,
confirm_behavior: ConfirmBehavior,
matches: Vec<PathMatch>,
selected_index: usize,
}
impl FileContextPickerDelegate {
pub fn new(
context_picker: WeakView<ContextPicker>,
workspace: WeakView<Workspace>,
context_picker: WeakModel<ContextPicker>,
workspace: WeakModel<Workspace>,
context_store: WeakModel<ContextStore>,
confirm_behavior: ConfirmBehavior,
) -> Self {
Self {
context_picker,
workspace,
context_store,
confirm_behavior,
matches: Vec::new(),
selected_index: 0,
}
@@ -73,50 +83,44 @@ impl FileContextPickerDelegate {
&mut self,
query: String,
cancellation_flag: Arc<AtomicBool>,
workspace: &View<Workspace>,
cx: &mut ViewContext<Picker<Self>>,
workspace: &Model<Workspace>,
cx: &mut ModelContext<Picker<Self>>,
) -> Task<Vec<PathMatch>> {
if query.is_empty() {
let workspace = workspace.read(cx);
let project = workspace.project().read(cx);
let entries = workspace.recent_navigation_history(Some(10), cx);
let entries = entries
let recent_matches = workspace
.recent_navigation_history(Some(10), cx)
.into_iter()
.map(|entries| entries.0)
.chain(project.worktrees(cx).flat_map(|worktree| {
let worktree = worktree.read(cx);
let id = worktree.id();
worktree
.child_entries(Path::new(""))
.filter(|entry| entry.kind.is_file())
.map(move |entry| project::ProjectPath {
worktree_id: id,
path: entry.path.clone(),
})
}))
.collect::<Vec<_>>();
let path_prefix: Arc<str> = Arc::default();
Task::ready(
entries
.into_iter()
.filter_map(|entry| {
let worktree = project.worktree_for_id(entry.worktree_id, cx)?;
let mut full_path = PathBuf::from(worktree.read(cx).root_name());
full_path.push(&entry.path);
Some(PathMatch {
score: 0.,
positions: Vec::new(),
worktree_id: entry.worktree_id.to_usize(),
path: full_path.into(),
path_prefix: path_prefix.clone(),
distance_to_relative_ancestor: 0,
is_dir: false,
})
.filter_map(|(project_path, _)| {
let worktree = project.worktree_for_id(project_path.worktree_id, cx)?;
Some(PathMatch {
score: 0.,
positions: Vec::new(),
worktree_id: project_path.worktree_id.to_usize(),
path: project_path.path,
path_prefix: worktree.read(cx).root_name().into(),
distance_to_relative_ancestor: 0,
is_dir: false,
})
.collect(),
)
});
let file_matches = project.worktrees(cx).flat_map(|worktree| {
let worktree = worktree.read(cx);
let path_prefix: Arc<str> = worktree.root_name().into();
worktree.files(true, 0).map(move |entry| PathMatch {
score: 0.,
positions: Vec::new(),
worktree_id: worktree.id().to_usize(),
path: entry.path.clone(),
path_prefix: path_prefix.clone(),
distance_to_relative_ancestor: 0,
is_dir: false,
})
});
Task::ready(recent_matches.chain(file_matches).collect())
} else {
let worktrees = workspace.read(cx).visible_worktrees(cx).collect::<Vec<_>>();
let candidate_sets = worktrees
@@ -163,22 +167,32 @@ impl PickerDelegate for FileContextPickerDelegate {
self.selected_index
}
fn set_selected_index(&mut self, ix: usize, _cx: &mut ViewContext<Picker<Self>>) {
fn set_selected_index(
&mut self,
ix: usize,
_window: &mut Window,
_cx: &mut ModelContext<Picker<Self>>,
) {
self.selected_index = ix;
}
fn placeholder_text(&self, _cx: &mut WindowContext) -> Arc<str> {
fn placeholder_text(&self, _window: &mut Window, _cx: &mut AppContext) -> Arc<str> {
"Search files…".into()
}
fn update_matches(&mut self, query: String, cx: &mut ViewContext<Picker<Self>>) -> Task<()> {
fn update_matches(
&mut self,
query: String,
window: &mut Window,
cx: &mut ModelContext<Picker<Self>>,
) -> Task<()> {
let Some(workspace) = self.workspace.upgrade() else {
return Task::ready(());
};
let search_task = self.search(query, Arc::<AtomicBool>::default(), &workspace, cx);
cx.spawn(|this, mut cx| async move {
cx.spawn_in(window, |this, mut cx| async move {
// TODO: This should be probably be run in the background.
let paths = search_task.await;
@@ -189,8 +203,15 @@ impl PickerDelegate for FileContextPickerDelegate {
})
}
fn confirm(&mut self, _secondary: bool, cx: &mut ViewContext<Picker<Self>>) {
let mat = &self.matches[self.selected_index];
fn confirm(
&mut self,
_secondary: bool,
window: &mut Window,
cx: &mut ModelContext<Picker<Self>>,
) {
let Some(mat) = self.matches.get(self.selected_index) else {
return;
};
let workspace = self.workspace.clone();
let Some(project) = workspace
@@ -201,7 +222,8 @@ impl PickerDelegate for FileContextPickerDelegate {
};
let path = mat.path.clone();
let worktree_id = WorktreeId::from_usize(mat.worktree_id);
cx.spawn(|this, mut cx| async move {
let confirm_behavior = self.confirm_behavior;
cx.spawn_in(window, |this, mut cx| async move {
let Some(open_buffer_task) = project
.update(&mut cx, |project, cx| {
project.open_buffer((worktree_id, path.clone()), cx)
@@ -213,23 +235,32 @@ impl PickerDelegate for FileContextPickerDelegate {
let buffer = open_buffer_task.await?;
this.update(&mut cx, |this, cx| {
this.delegate.context_store.update(cx, |context_store, cx| {
let mut text = String::new();
text.push_str(&codeblock_fence_for_path(Some(&path), None));
text.push_str(&buffer.read(cx).text());
if !text.ends_with('\n') {
text.push('\n');
}
this.update_in(&mut cx, |this, window, cx| {
this.delegate
.context_store
.update(cx, |context_store, cx| {
let mut text = String::new();
text.push_str(&codeblock_fence_for_path(Some(&path), None));
text.push_str(&buffer.read(cx).text());
if !text.ends_with('\n') {
text.push('\n');
}
text.push_str("```\n");
text.push_str("```\n");
context_store.insert_context(
ContextKind::File,
path.to_string_lossy().to_string(),
text,
);
})
context_store.insert_context(
ContextKind::File,
path.to_string_lossy().to_string(),
text,
);
})?;
match confirm_behavior {
ConfirmBehavior::KeepOpen => {}
ConfirmBehavior::Close => this.delegate.dismissed(window, cx),
}
anyhow::Ok(())
})??;
anyhow::Ok(())
@@ -237,7 +268,7 @@ impl PickerDelegate for FileContextPickerDelegate {
.detach_and_log_err(cx);
}
fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>) {
fn dismissed(&mut self, _: &mut Window, cx: &mut ModelContext<Picker<Self>>) {
self.context_picker
.update(cx, |this, cx| {
this.reset_mode();
@@ -250,19 +281,34 @@ impl PickerDelegate for FileContextPickerDelegate {
&self,
ix: usize,
selected: bool,
_cx: &mut ViewContext<Picker<Self>>,
_window: &mut Window,
_cx: &mut ModelContext<Picker<Self>>,
) -> Option<Self::ListItem> {
let path_match = &self.matches[ix];
let file_name = path_match
.path
.file_name()
.unwrap_or_default()
.to_string_lossy()
.to_string();
let directory = path_match
.path
.parent()
.map(|directory| format!("{}/", directory.to_string_lossy()));
let (file_name, directory) = if path_match.path.as_ref() == Path::new("") {
(SharedString::from(path_match.path_prefix.clone()), None)
} else {
let file_name = path_match
.path
.file_name()
.unwrap_or_default()
.to_string_lossy()
.to_string()
.into();
let mut directory = format!("{}/", path_match.path_prefix);
if let Some(parent) = path_match
.path
.parent()
.filter(|parent| parent != &Path::new(""))
{
directory.push_str(&parent.to_string_lossy());
directory.push('/');
}
(file_name, Some(directory))
};
Some(
ListItem::new(ix).inset(true).toggle_state(selected).child(

View File

@@ -1,43 +1,49 @@
use std::sync::Arc;
use fuzzy::StringMatchCandidate;
use gpui::{AppContext, DismissEvent, FocusHandle, FocusableView, Task, View, WeakModel, WeakView};
use gpui::{AppContext, DismissEvent, FocusHandle, Focusable, Model, Task, WeakModel};
use picker::{Picker, PickerDelegate};
use ui::{prelude::*, ListItem};
use crate::context::ContextKind;
use crate::context_picker::ContextPicker;
use crate::context_picker::{ConfirmBehavior, ContextPicker};
use crate::context_store;
use crate::thread::ThreadId;
use crate::thread_store::ThreadStore;
pub struct ThreadContextPicker {
picker: View<Picker<ThreadContextPickerDelegate>>,
picker: Model<Picker<ThreadContextPickerDelegate>>,
}
impl ThreadContextPicker {
pub fn new(
thread_store: WeakModel<ThreadStore>,
context_picker: WeakView<ContextPicker>,
context_picker: WeakModel<ContextPicker>,
context_store: WeakModel<context_store::ContextStore>,
cx: &mut ViewContext<Self>,
confirm_behavior: ConfirmBehavior,
window: &mut Window,
cx: &mut ModelContext<Self>,
) -> Self {
let delegate =
ThreadContextPickerDelegate::new(thread_store, context_picker, context_store);
let picker = cx.new_view(|cx| Picker::uniform_list(delegate, cx));
let delegate = ThreadContextPickerDelegate::new(
thread_store,
context_picker,
context_store,
confirm_behavior,
);
let picker = cx.new_model(|cx| Picker::uniform_list(delegate, window, cx));
ThreadContextPicker { picker }
}
}
impl FocusableView for ThreadContextPicker {
impl Focusable for ThreadContextPicker {
fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
self.picker.focus_handle(cx)
}
}
impl Render for ThreadContextPicker {
fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
fn render(&mut self, _window: &mut Window, _cx: &mut ModelContext<Self>) -> impl IntoElement {
self.picker.clone()
}
}
@@ -50,8 +56,9 @@ struct ThreadContextEntry {
pub struct ThreadContextPickerDelegate {
thread_store: WeakModel<ThreadStore>,
context_picker: WeakView<ContextPicker>,
context_picker: WeakModel<ContextPicker>,
context_store: WeakModel<context_store::ContextStore>,
confirm_behavior: ConfirmBehavior,
matches: Vec<ThreadContextEntry>,
selected_index: usize,
}
@@ -59,13 +66,15 @@ pub struct ThreadContextPickerDelegate {
impl ThreadContextPickerDelegate {
pub fn new(
thread_store: WeakModel<ThreadStore>,
context_picker: WeakView<ContextPicker>,
context_picker: WeakModel<ContextPicker>,
context_store: WeakModel<context_store::ContextStore>,
confirm_behavior: ConfirmBehavior,
) -> Self {
ThreadContextPickerDelegate {
thread_store,
context_picker,
context_store,
confirm_behavior,
matches: Vec::new(),
selected_index: 0,
}
@@ -83,15 +92,25 @@ impl PickerDelegate for ThreadContextPickerDelegate {
self.selected_index
}
fn set_selected_index(&mut self, ix: usize, _cx: &mut ViewContext<Picker<Self>>) {
fn set_selected_index(
&mut self,
ix: usize,
_window: &mut Window,
_cx: &mut ModelContext<Picker<Self>>,
) {
self.selected_index = ix;
}
fn placeholder_text(&self, _cx: &mut WindowContext) -> Arc<str> {
fn placeholder_text(&self, _window: &mut Window, _cx: &mut AppContext) -> Arc<str> {
"Search threads…".into()
}
fn update_matches(&mut self, query: String, cx: &mut ViewContext<Picker<Self>>) -> Task<()> {
fn update_matches(
&mut self,
query: String,
window: &mut Window,
cx: &mut ModelContext<Picker<Self>>,
) -> Task<()> {
let Ok(threads) = self.thread_store.update(cx, |this, cx| {
this.threads(cx)
.into_iter()
@@ -134,7 +153,7 @@ impl PickerDelegate for ThreadContextPickerDelegate {
}
});
cx.spawn(|this, mut cx| async move {
cx.spawn_in(window, |this, mut cx| async move {
let matches = search_task.await;
this.update(&mut cx, |this, cx| {
this.delegate.matches = matches;
@@ -145,8 +164,15 @@ impl PickerDelegate for ThreadContextPickerDelegate {
})
}
fn confirm(&mut self, _secondary: bool, cx: &mut ViewContext<Picker<Self>>) {
let entry = &self.matches[self.selected_index];
fn confirm(
&mut self,
_secondary: bool,
window: &mut Window,
cx: &mut ModelContext<Picker<Self>>,
) {
let Some(entry) = self.matches.get(self.selected_index) else {
return;
};
let Some(thread_store) = self.thread_store.upgrade() else {
return;
@@ -180,9 +206,14 @@ impl PickerDelegate for ThreadContextPickerDelegate {
context_store.insert_context(ContextKind::Thread, entry.summary.clone(), text);
})
.ok();
match self.confirm_behavior {
ConfirmBehavior::KeepOpen => {}
ConfirmBehavior::Close => self.dismissed(window, cx),
}
}
fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>) {
fn dismissed(&mut self, _: &mut Window, cx: &mut ModelContext<Picker<Self>>) {
self.context_picker
.update(cx, |this, cx| {
this.reset_mode();
@@ -195,7 +226,8 @@ impl PickerDelegate for ThreadContextPickerDelegate {
&self,
ix: usize,
selected: bool,
_cx: &mut ViewContext<Picker<Self>>,
_window: &mut Window,
_cx: &mut ModelContext<Picker<Self>>,
) -> Option<Self::ListItem> {
let thread = &self.matches[ix];
@@ -203,7 +235,7 @@ impl PickerDelegate for ThreadContextPickerDelegate {
ListItem::new(ix)
.inset(true)
.toggle_state(selected)
.child(thread.summary.clone()),
.child(Label::new(thread.summary.clone())),
)
}
}

View File

@@ -1,18 +1,19 @@
use std::rc::Rc;
use gpui::{FocusHandle, Model, View, WeakModel, WeakView};
use gpui::{FocusHandle, Model, WeakModel};
use ui::{prelude::*, PopoverMenu, PopoverMenuHandle, Tooltip};
use workspace::Workspace;
use crate::context_picker::ContextPicker;
use crate::context_picker::{ConfirmBehavior, ContextPicker};
use crate::context_store::ContextStore;
use crate::thread_store::ThreadStore;
use crate::ui::ContextPill;
use crate::ToggleContextPicker;
use settings::Settings;
pub struct ContextStrip {
context_store: Model<ContextStore>,
context_picker: View<ContextPicker>,
context_picker: Model<ContextPicker>,
context_picker_menu_handle: PopoverMenuHandle<ContextPicker>,
focus_handle: FocusHandle,
}
@@ -20,19 +21,22 @@ pub struct ContextStrip {
impl ContextStrip {
pub fn new(
context_store: Model<ContextStore>,
workspace: WeakView<Workspace>,
workspace: WeakModel<Workspace>,
thread_store: Option<WeakModel<ThreadStore>>,
focus_handle: FocusHandle,
context_picker_menu_handle: PopoverMenuHandle<ContextPicker>,
cx: &mut ViewContext<Self>,
window: &mut Window,
cx: &mut ModelContext<Self>,
) -> Self {
Self {
context_store: context_store.clone(),
context_picker: cx.new_view(|cx| {
context_picker: cx.new_model(|cx| {
ContextPicker::new(
workspace.clone(),
thread_store.clone(),
context_store.downgrade(),
ConfirmBehavior::KeepOpen,
window,
cx,
)
}),
@@ -43,8 +47,8 @@ impl ContextStrip {
}
impl Render for ContextStrip {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let context = self.context_store.read(cx).context();
fn render(&mut self, window: &mut Window, cx: &mut ModelContext<Self>) -> impl IntoElement {
let context = self.context_store.read(cx).context().clone();
let context_picker = self.context_picker.clone();
let focus_handle = self.focus_handle.clone();
@@ -53,16 +57,17 @@ impl Render for ContextStrip {
.gap_1()
.child(
PopoverMenu::new("context-picker")
.menu(move |_cx| Some(context_picker.clone()))
.menu(move |_window, _cx| Some(context_picker.clone()))
.trigger(
IconButton::new("add-context", IconName::Plus)
.icon_size(IconSize::Small)
.style(ui::ButtonStyle::Filled)
.tooltip(move |cx| {
.tooltip(move |window, cx| {
Tooltip::for_action_in(
"Add Context",
&ToggleContextPicker,
&focus_handle,
window,
cx,
)
}),
@@ -75,11 +80,34 @@ impl Render for ContextStrip {
})
.with_handle(self.context_picker_menu_handle.clone()),
)
.when(context.is_empty(), {
|parent| {
parent.child(
h_flex()
.id("no-content-info")
.ml_1p5()
.gap_2()
.font(theme::ThemeSettings::get_global(cx).buffer_font.clone())
.text_size(TextSize::Small.rems(cx))
.text_color(cx.theme().colors().text_muted)
.child("Add Context")
.children(
ui::KeyBinding::for_action_in(
&ToggleContextPicker,
&self.focus_handle,
window,
)
.map(|binding| binding.into_any_element()),
)
.opacity(0.5),
)
}
})
.children(context.iter().map(|context| {
ContextPill::new(context.clone()).on_remove({
let context = context.clone();
let context_store = self.context_store.clone();
Rc::new(cx.listener(move |_this, _event, cx| {
Rc::new(cx.listener(move |_this, _event, _, cx| {
context_store.update(cx, |this, _cx| {
this.remove_context(&context.id);
});
@@ -87,19 +115,21 @@ impl Render for ContextStrip {
}))
})
}))
.when(!context.is_empty(), |parent| {
parent.child(
IconButton::new("remove-all-context", IconName::Eraser)
.icon_size(IconSize::Small)
.tooltip(move |cx| Tooltip::text("Remove All Context", cx))
.on_click({
let context_store = self.context_store.clone();
cx.listener(move |_this, _event, cx| {
context_store.update(cx, |this, _cx| this.clear());
cx.notify();
})
}),
)
.when(!context.is_empty(), {
move |parent| {
parent.child(
IconButton::new("remove-all-context", IconName::Eraser)
.icon_size(IconSize::Small)
.tooltip(Tooltip::text("Remove All Context"))
.on_click({
let context_store = self.context_store.clone();
cx.listener(move |_this, _event, _, cx| {
context_store.update(cx, |this, _cx| this.clear());
cx.notify();
})
}),
)
}
})
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,20 +1,21 @@
use std::sync::Arc;
use editor::{Editor, EditorElement, EditorStyle};
use editor::{Editor, EditorElement, EditorEvent, EditorStyle};
use fs::Fs;
use gpui::{AppContext, FocusableView, Model, TextStyle, View, WeakModel, WeakView};
use gpui::{AppContext, DismissEvent, Focusable, Model, Subscription, TextStyle, WeakModel};
use language_model::{LanguageModelRegistry, LanguageModelRequestTool};
use language_model_selector::{LanguageModelSelector, LanguageModelSelectorPopoverMenu};
use settings::{update_settings_file, Settings};
use language_model_selector::LanguageModelSelector;
use rope::Point;
use settings::Settings;
use theme::ThemeSettings;
use ui::{
prelude::*, ButtonLike, CheckboxWithLabel, ElevationIndex, KeyBinding, PopoverMenuHandle,
Tooltip,
prelude::*, ButtonLike, ElevationIndex, KeyBinding, PopoverMenu, PopoverMenuHandle,
SwitchWithLabel,
};
use workspace::Workspace;
use crate::assistant_settings::AssistantSettings;
use crate::context_picker::ContextPicker;
use crate::assistant_model_selector::AssistantModelSelector;
use crate::context_picker::{ConfirmBehavior, ContextPicker};
use crate::context_store::ContextStore;
use crate::context_strip::ContextStrip;
use crate::thread::{RequestKind, Thread};
@@ -23,83 +24,112 @@ use crate::{Chat, ToggleContextPicker, ToggleModelSelector};
pub struct MessageEditor {
thread: Model<Thread>,
editor: View<Editor>,
editor: Model<Editor>,
context_store: Model<ContextStore>,
context_strip: View<ContextStrip>,
context_strip: Model<ContextStrip>,
context_picker_menu_handle: PopoverMenuHandle<ContextPicker>,
language_model_selector: View<LanguageModelSelector>,
language_model_selector_menu_handle: PopoverMenuHandle<LanguageModelSelector>,
inline_context_picker: Model<ContextPicker>,
inline_context_picker_menu_handle: PopoverMenuHandle<ContextPicker>,
model_selector: Model<AssistantModelSelector>,
model_selector_menu_handle: PopoverMenuHandle<LanguageModelSelector>,
use_tools: bool,
_subscriptions: Vec<Subscription>,
}
impl MessageEditor {
pub fn new(
fs: Arc<dyn Fs>,
workspace: WeakView<Workspace>,
workspace: WeakModel<Workspace>,
thread_store: WeakModel<ThreadStore>,
thread: Model<Thread>,
cx: &mut ViewContext<Self>,
window: &mut Window,
cx: &mut ModelContext<Self>,
) -> Self {
let context_store = cx.new_model(|_cx| ContextStore::new());
let context_picker_menu_handle = PopoverMenuHandle::default();
let inline_context_picker_menu_handle = PopoverMenuHandle::default();
let model_selector_menu_handle = PopoverMenuHandle::default();
let editor = cx.new_view(|cx| {
let mut editor = Editor::auto_height(80, cx);
editor.set_placeholder_text("Ask anything, @ to add context", cx);
let editor = cx.new_model(|cx| {
let mut editor = Editor::auto_height(10, window, cx);
editor.set_placeholder_text("Ask anything", cx);
editor.set_show_indent_guides(false, cx);
editor
});
let inline_context_picker = cx.new_model(|cx| {
ContextPicker::new(
workspace.clone(),
Some(thread_store.clone()),
context_store.downgrade(),
ConfirmBehavior::Close,
window,
cx,
)
});
let subscriptions = vec![
cx.subscribe_in(&editor, window, Self::handle_editor_event),
cx.subscribe_in(
&inline_context_picker,
window,
Self::handle_inline_context_picker_event,
),
];
Self {
thread,
editor: editor.clone(),
context_store: context_store.clone(),
context_strip: cx.new_view(|cx| {
context_strip: cx.new_model(|cx| {
ContextStrip::new(
context_store,
workspace.clone(),
Some(thread_store.clone()),
editor.focus_handle(cx),
context_picker_menu_handle.clone(),
window,
cx,
)
}),
context_picker_menu_handle,
language_model_selector: cx.new_view(|cx| {
let fs = fs.clone();
LanguageModelSelector::new(
move |model, cx| {
update_settings_file::<AssistantSettings>(
fs.clone(),
cx,
move |settings, _cx| settings.set_model(model.clone()),
);
},
cx,
)
inline_context_picker,
inline_context_picker_menu_handle,
model_selector: cx.new_model(|cx| {
AssistantModelSelector::new(fs, model_selector_menu_handle.clone(), window, cx)
}),
language_model_selector_menu_handle: PopoverMenuHandle::default(),
model_selector_menu_handle,
use_tools: false,
_subscriptions: subscriptions,
}
}
fn toggle_model_selector(&mut self, _: &ToggleModelSelector, cx: &mut ViewContext<Self>) {
self.language_model_selector_menu_handle.toggle(cx);
fn toggle_model_selector(
&mut self,
_: &ToggleModelSelector,
window: &mut Window,
cx: &mut ModelContext<Self>,
) {
self.model_selector_menu_handle.toggle(window, cx)
}
fn toggle_context_picker(&mut self, _: &ToggleContextPicker, cx: &mut ViewContext<Self>) {
self.context_picker_menu_handle.toggle(cx);
fn toggle_context_picker(
&mut self,
_: &ToggleContextPicker,
window: &mut Window,
cx: &mut ModelContext<Self>,
) {
self.context_picker_menu_handle.toggle(window, cx);
}
fn chat(&mut self, _: &Chat, cx: &mut ViewContext<Self>) {
self.send_to_model(RequestKind::Chat, cx);
fn chat(&mut self, _: &Chat, window: &mut Window, cx: &mut ModelContext<Self>) {
self.send_to_model(RequestKind::Chat, window, cx);
}
fn send_to_model(
&mut self,
request_kind: RequestKind,
cx: &mut ViewContext<Self>,
window: &mut Window,
cx: &mut ModelContext<Self>,
) -> Option<()> {
let provider = LanguageModelRegistry::read_global(cx).active_provider();
if provider
@@ -115,7 +145,7 @@ impl MessageEditor {
let user_message = self.editor.update(cx, |editor, cx| {
let text = editor.text(cx);
editor.clear(cx);
editor.clear(window, cx);
text
});
let context = self.context_store.update(cx, |this, _cx| this.drain());
@@ -143,62 +173,55 @@ impl MessageEditor {
None
}
fn render_language_model_selector(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let active_model = LanguageModelRegistry::read_global(cx).active_model();
let focus_handle = self.language_model_selector.focus_handle(cx).clone();
fn handle_editor_event(
&mut self,
editor: &Model<Editor>,
event: &EditorEvent,
window: &mut Window,
cx: &mut ModelContext<Self>,
) {
match event {
EditorEvent::SelectionsChanged { .. } => {
editor.update(cx, |editor, cx| {
let snapshot = editor.buffer().read(cx).snapshot(cx);
let newest_cursor = editor.selections.newest::<Point>(cx).head();
if newest_cursor.column > 0 {
let behind_cursor = Point::new(newest_cursor.row, newest_cursor.column - 1);
let char_behind_cursor = snapshot.chars_at(behind_cursor).next();
if char_behind_cursor == Some('@') {
self.inline_context_picker_menu_handle.show(window, cx);
}
}
});
}
_ => {}
}
}
LanguageModelSelectorPopoverMenu::new(
self.language_model_selector.clone(),
ButtonLike::new("active-model")
.style(ButtonStyle::Subtle)
.child(
h_flex()
.w_full()
.gap_0p5()
.child(
div()
.overflow_x_hidden()
.flex_grow()
.whitespace_nowrap()
.child(match active_model {
Some(model) => h_flex()
.child(
Label::new(model.name().0)
.size(LabelSize::Small)
.color(Color::Muted),
)
.into_any_element(),
_ => Label::new("No model selected")
.size(LabelSize::Small)
.color(Color::Muted)
.into_any_element(),
}),
)
.child(
Icon::new(IconName::ChevronDown)
.color(Color::Muted)
.size(IconSize::XSmall),
),
)
.tooltip(move |cx| {
Tooltip::for_action_in("Change Model", &ToggleModelSelector, &focus_handle, cx)
}),
)
.with_handle(self.language_model_selector_menu_handle.clone())
fn handle_inline_context_picker_event(
&mut self,
_inline_context_picker: &Model<ContextPicker>,
_event: &DismissEvent,
window: &mut Window,
cx: &mut ModelContext<Self>,
) {
let editor_focus_handle = self.editor.focus_handle(cx);
window.focus(&editor_focus_handle);
}
}
impl FocusableView for MessageEditor {
impl Focusable for MessageEditor {
fn focus_handle(&self, cx: &AppContext) -> gpui::FocusHandle {
self.editor.focus_handle(cx)
}
}
impl Render for MessageEditor {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
fn render(&mut self, window: &mut Window, cx: &mut ModelContext<Self>) -> impl IntoElement {
let font_size = TextSize::Default.rems(cx);
let line_height = font_size.to_pixels(cx.rem_size()) * 1.5;
let line_height = font_size.to_pixels(window.rem_size()) * 1.5;
let focus_handle = self.editor.focus_handle(cx);
let inline_context_picker = self.inline_context_picker.clone();
let bg_color = cx.theme().colors().editor_background;
v_flex()
@@ -211,58 +234,72 @@ impl Render for MessageEditor {
.p_2()
.bg(bg_color)
.child(self.context_strip.clone())
.child(div().id("thread_editor").overflow_y_scroll().h_12().child({
let settings = ThemeSettings::get_global(cx);
let text_style = TextStyle {
color: cx.theme().colors().editor_foreground,
font_family: settings.ui_font.family.clone(),
font_features: settings.ui_font.features.clone(),
font_size: font_size.into(),
font_weight: settings.ui_font.weight,
line_height: line_height.into(),
..Default::default()
};
EditorElement::new(
&self.editor,
EditorStyle {
background: bg_color,
local_player: cx.theme().players().local(),
text: text_style,
..Default::default()
},
)
}))
.child(
h_flex()
.justify_between()
.child(CheckboxWithLabel::new(
"use-tools",
Label::new("Tools"),
self.use_tools.into(),
cx.listener(|this, selection, _cx| {
this.use_tools = match selection {
ToggleState::Selected => true,
ToggleState::Unselected | ToggleState::Indeterminate => false,
};
}),
))
v_flex()
.gap_4()
.child({
let settings = ThemeSettings::get_global(cx);
let text_style = TextStyle {
color: cx.theme().colors().text,
font_family: settings.ui_font.family.clone(),
font_features: settings.ui_font.features.clone(),
font_size: font_size.into(),
font_weight: settings.ui_font.weight,
line_height: line_height.into(),
..Default::default()
};
EditorElement::new(
&self.editor,
EditorStyle {
background: bg_color,
local_player: cx.theme().players().local(),
text: text_style,
..Default::default()
},
)
})
.child(
PopoverMenu::new("inline-context-picker")
.menu(move |_window, _cx| Some(inline_context_picker.clone()))
.attach(gpui::Corner::TopLeft)
.anchor(gpui::Corner::BottomLeft)
.offset(gpui::Point {
x: px(0.0),
y: px(-16.0),
})
.with_handle(self.inline_context_picker_menu_handle.clone()),
)
.child(
h_flex()
.gap_1()
.child(self.render_language_model_selector(cx))
.justify_between()
.child(SwitchWithLabel::new(
"use-tools",
Label::new("Tools"),
self.use_tools.into(),
cx.listener(|this, selection, _window, _cx| {
this.use_tools = match selection {
ToggleState::Selected => true,
ToggleState::Unselected | ToggleState::Indeterminate => {
false
}
};
}),
))
.child(
ButtonLike::new("chat")
.style(ButtonStyle::Filled)
.layer(ElevationIndex::ModalSurface)
.child(Label::new("Submit"))
.children(
KeyBinding::for_action_in(&Chat, &focus_handle, cx)
.map(|binding| binding.into_any_element()),
)
.on_click(move |_event, cx| {
focus_handle.dispatch_action(&Chat, cx);
}),
h_flex().gap_1().child(self.model_selector.clone()).child(
ButtonLike::new("chat")
.style(ButtonStyle::Filled)
.layer(ElevationIndex::ModalSurface)
.child(Label::new("Submit"))
.children(
KeyBinding::for_action_in(&Chat, &focus_handle, window)
.map(|binding| binding.into_any_element()),
)
.on_click(move |_event, window, cx| {
focus_handle.dispatch_action(&Chat, window, cx);
}),
),
),
),
)

View File

@@ -0,0 +1,192 @@
use crate::inline_prompt_editor::CodegenStatus;
use client::telemetry::Telemetry;
use futures::{channel::mpsc, SinkExt, StreamExt};
use gpui::{AppContext, EventEmitter, Model, ModelContext, Task};
use language_model::{LanguageModelRegistry, LanguageModelRequest};
use language_models::report_assistant_event;
use std::{sync::Arc, time::Instant};
use telemetry_events::{AssistantEvent, AssistantKind, AssistantPhase};
use terminal::Terminal;
pub struct TerminalCodegen {
pub status: CodegenStatus,
pub telemetry: Option<Arc<Telemetry>>,
terminal: Model<Terminal>,
generation: Task<()>,
pub message_id: Option<String>,
transaction: Option<TerminalTransaction>,
}
impl EventEmitter<CodegenEvent> for TerminalCodegen {}
impl TerminalCodegen {
pub fn new(terminal: Model<Terminal>, telemetry: Option<Arc<Telemetry>>) -> Self {
Self {
terminal,
telemetry,
status: CodegenStatus::Idle,
generation: Task::ready(()),
message_id: None,
transaction: None,
}
}
pub fn start(&mut self, prompt: LanguageModelRequest, cx: &mut ModelContext<Self>) {
let Some(model) = LanguageModelRegistry::read_global(cx).active_model() else {
return;
};
let model_api_key = model.api_key(cx);
let http_client = cx.http_client();
let telemetry = self.telemetry.clone();
self.status = CodegenStatus::Pending;
self.transaction = Some(TerminalTransaction::start(self.terminal.clone()));
self.generation = cx.spawn(|this, mut cx| async move {
let model_telemetry_id = model.telemetry_id();
let model_provider_id = model.provider_id();
let response = model.stream_completion_text(prompt, &cx).await;
let generate = async {
let message_id = response
.as_ref()
.ok()
.and_then(|response| response.message_id.clone());
let (mut hunks_tx, mut hunks_rx) = mpsc::channel(1);
let task = cx.background_executor().spawn({
let message_id = message_id.clone();
let executor = cx.background_executor().clone();
async move {
let mut response_latency = None;
let request_start = Instant::now();
let task = async {
let mut chunks = response?.stream;
while let Some(chunk) = chunks.next().await {
if response_latency.is_none() {
response_latency = Some(request_start.elapsed());
}
let chunk = chunk?;
hunks_tx.send(chunk).await?;
}
anyhow::Ok(())
};
let result = task.await;
let error_message = result.as_ref().err().map(|error| error.to_string());
report_assistant_event(
AssistantEvent {
conversation_id: None,
kind: AssistantKind::InlineTerminal,
message_id,
phase: AssistantPhase::Response,
model: model_telemetry_id,
model_provider: model_provider_id.to_string(),
response_latency,
error_message,
language_name: None,
},
telemetry,
http_client,
model_api_key,
&executor,
);
result?;
anyhow::Ok(())
}
});
this.update(&mut cx, |this, _| {
this.message_id = message_id;
})?;
while let Some(hunk) = hunks_rx.next().await {
this.update(&mut cx, |this, cx| {
if let Some(transaction) = &mut this.transaction {
transaction.push(hunk, cx);
cx.notify();
}
})?;
}
task.await?;
anyhow::Ok(())
};
let result = generate.await;
this.update(&mut cx, |this, cx| {
if let Err(error) = result {
this.status = CodegenStatus::Error(error);
} else {
this.status = CodegenStatus::Done;
}
cx.emit(CodegenEvent::Finished);
cx.notify();
})
.ok();
});
cx.notify();
}
pub fn stop(&mut self, cx: &mut ModelContext<Self>) {
self.status = CodegenStatus::Done;
self.generation = Task::ready(());
cx.emit(CodegenEvent::Finished);
cx.notify();
}
pub fn complete(&mut self, cx: &mut ModelContext<Self>) {
if let Some(transaction) = self.transaction.take() {
transaction.complete(cx);
}
}
pub fn undo(&mut self, cx: &mut ModelContext<Self>) {
if let Some(transaction) = self.transaction.take() {
transaction.undo(cx);
}
}
}
#[derive(Copy, Clone, Debug)]
pub enum CodegenEvent {
Finished,
}
pub const CLEAR_INPUT: &str = "\x15";
const CARRIAGE_RETURN: &str = "\x0d";
struct TerminalTransaction {
terminal: Model<Terminal>,
}
impl TerminalTransaction {
pub fn start(terminal: Model<Terminal>) -> Self {
Self { terminal }
}
pub fn push(&mut self, hunk: String, cx: &mut AppContext) {
// Ensure that the assistant cannot accidentally execute commands that are streamed into the terminal
let input = Self::sanitize_input(hunk);
self.terminal
.update(cx, |terminal, _| terminal.input(input));
}
pub fn undo(&self, cx: &mut AppContext) {
self.terminal
.update(cx, |terminal, _| terminal.input(CLEAR_INPUT.to_string()));
}
pub fn complete(&self, cx: &mut AppContext) {
self.terminal.update(cx, |terminal, _| {
terminal.input(CARRIAGE_RETURN.to_string())
});
}
fn sanitize_input(input: String) -> String {
input.replace(['\r', '\n'], "")
}
}

View File

@@ -1,37 +1,26 @@
use crate::assistant_settings::AssistantSettings;
use crate::context::attach_context_to_message;
use crate::context_picker::ContextPicker;
use crate::context_store::ContextStore;
use crate::context_strip::ContextStrip;
use crate::inline_prompt_editor::{
CodegenStatus, PromptEditor, PromptEditorEvent, TerminalInlineAssistId,
};
use crate::prompts::PromptBuilder;
use crate::terminal_codegen::{CodegenEvent, TerminalCodegen, CLEAR_INPUT};
use crate::thread_store::ThreadStore;
use crate::ToggleContextPicker;
use anyhow::{Context as _, Result};
use client::telemetry::Telemetry;
use collections::{HashMap, VecDeque};
use editor::{
actions::{MoveDown, MoveUp, SelectAll},
Editor, EditorElement, EditorEvent, EditorMode, EditorStyle, MultiBuffer,
};
use editor::{actions::SelectAll, MultiBuffer};
use fs::Fs;
use futures::{channel::mpsc, SinkExt, StreamExt};
use gpui::{
AppContext, Context, EventEmitter, FocusHandle, FocusableView, Global, Model, ModelContext,
Subscription, Task, TextStyle, UpdateGlobal, View, WeakModel, WeakView,
};
use gpui::{AppContext, Context, Focusable, Global, Model, Subscription, UpdateGlobal, WeakModel};
use language::Buffer;
use language_model::{
LanguageModelRegistry, LanguageModelRequest, LanguageModelRequestMessage, Role,
};
use language_model_selector::{LanguageModelSelector, LanguageModelSelectorPopoverMenu};
use language_models::report_assistant_event;
use settings::{update_settings_file, Settings};
use std::{cmp, sync::Arc, time::Instant};
use std::sync::Arc;
use telemetry_events::{AssistantEvent, AssistantKind, AssistantPhase};
use terminal::Terminal;
use terminal_view::TerminalView;
use theme::ThemeSettings;
use ui::{prelude::*, text_for_action, IconButtonShape, PopoverMenuHandle, Tooltip};
use ui::prelude::*;
use util::ResultExt;
use workspace::{notifications::NotificationId, Toast, Workspace};
@@ -47,17 +36,6 @@ pub fn init(
const DEFAULT_CONTEXT_LINES: usize = 50;
const PROMPT_HISTORY_MAX_LEN: usize = 20;
#[derive(Copy, Clone, Default, Debug, PartialEq, Eq, Hash)]
struct TerminalInlineAssistId(usize);
impl TerminalInlineAssistId {
fn post_inc(&mut self) -> TerminalInlineAssistId {
let id = *self;
self.0 += 1;
id
}
}
pub struct TerminalInlineAssistant {
next_assist_id: TerminalInlineAssistId,
assists: HashMap<TerminalInlineAssistId, TerminalInlineAssist>,
@@ -87,10 +65,11 @@ impl TerminalInlineAssistant {
pub fn assist(
&mut self,
terminal_view: &View<TerminalView>,
workspace: WeakView<Workspace>,
terminal_view: &Model<TerminalView>,
workspace: WeakModel<Workspace>,
thread_store: Option<WeakModel<ThreadStore>>,
cx: &mut WindowContext,
window: &mut Window,
cx: &mut AppContext,
) {
let terminal = terminal_view.read(cx).terminal().clone();
let assist_id = self.next_assist_id.post_inc();
@@ -98,10 +77,10 @@ impl TerminalInlineAssistant {
MultiBuffer::singleton(cx.new_model(|cx| Buffer::local(String::new(), cx)), cx)
});
let context_store = cx.new_model(|_cx| ContextStore::new());
let codegen = cx.new_model(|_| Codegen::new(terminal, self.telemetry.clone()));
let codegen = cx.new_model(|_| TerminalCodegen::new(terminal, self.telemetry.clone()));
let prompt_editor = cx.new_view(|cx| {
PromptEditor::new(
let prompt_editor = cx.new_model(|cx| {
PromptEditor::new_terminal(
assist_id,
self.prompt_history.clone(),
prompt_buffer.clone(),
@@ -110,6 +89,7 @@ impl TerminalInlineAssistant {
context_store.clone(),
workspace.clone(),
thread_store.clone(),
window,
cx,
)
});
@@ -119,7 +99,7 @@ impl TerminalInlineAssistant {
render: Box::new(move |_| prompt_editor_render.clone().into_any_element()),
};
terminal_view.update(cx, |terminal_view, cx| {
terminal_view.set_block_below_cursor(block, cx);
terminal_view.set_block_below_cursor(block, window, cx);
});
let terminal_assistant = TerminalInlineAssist::new(
@@ -128,21 +108,27 @@ impl TerminalInlineAssistant {
prompt_editor,
workspace.clone(),
context_store,
window,
cx,
);
self.assists.insert(assist_id, terminal_assistant);
self.focus_assist(assist_id, cx);
self.focus_assist(assist_id, window, cx);
}
fn focus_assist(&mut self, assist_id: TerminalInlineAssistId, cx: &mut WindowContext) {
fn focus_assist(
&mut self,
assist_id: TerminalInlineAssistId,
window: &mut Window,
cx: &mut AppContext,
) {
let assist = &self.assists[&assist_id];
if let Some(prompt_editor) = assist.prompt_editor.as_ref() {
prompt_editor.update(cx, |this, cx| {
this.editor.update(cx, |editor, cx| {
editor.focus(cx);
editor.select_all(&SelectAll, cx);
window.focus(&editor.focus_handle(cx));
editor.select_all(&SelectAll, window, cx);
});
});
}
@@ -150,11 +136,12 @@ impl TerminalInlineAssistant {
fn handle_prompt_editor_event(
&mut self,
prompt_editor: View<PromptEditor>,
prompt_editor: Model<PromptEditor<TerminalCodegen>>,
event: &PromptEditorEvent,
cx: &mut WindowContext,
window: &mut Window,
cx: &mut AppContext,
) {
let assist_id = prompt_editor.read(cx).id;
let assist_id = prompt_editor.read(cx).id();
match event {
PromptEditorEvent::StartRequested => {
self.start_assist(assist_id, cx);
@@ -163,21 +150,21 @@ impl TerminalInlineAssistant {
self.stop_assist(assist_id, cx);
}
PromptEditorEvent::ConfirmRequested { execute } => {
self.finish_assist(assist_id, false, *execute, cx);
self.finish_assist(assist_id, false, *execute, window, cx);
}
PromptEditorEvent::CancelRequested => {
self.finish_assist(assist_id, true, false, cx);
self.finish_assist(assist_id, true, false, window, cx);
}
PromptEditorEvent::DismissRequested => {
self.dismiss_assist(assist_id, cx);
self.dismiss_assist(assist_id, window, cx);
}
PromptEditorEvent::Resized { height_in_lines } => {
self.insert_prompt_editor_into_terminal(assist_id, *height_in_lines, cx);
self.insert_prompt_editor_into_terminal(assist_id, *height_in_lines, window, cx);
}
}
}
fn start_assist(&mut self, assist_id: TerminalInlineAssistId, cx: &mut WindowContext) {
fn start_assist(&mut self, assist_id: TerminalInlineAssistId, cx: &mut AppContext) {
let assist = if let Some(assist) = self.assists.get_mut(&assist_id) {
assist
} else {
@@ -215,7 +202,7 @@ impl TerminalInlineAssistant {
codegen.update(cx, |codegen, cx| codegen.start(request, cx));
}
fn stop_assist(&mut self, assist_id: TerminalInlineAssistId, cx: &mut WindowContext) {
fn stop_assist(&mut self, assist_id: TerminalInlineAssistId, cx: &mut AppContext) {
let assist = if let Some(assist) = self.assists.get_mut(&assist_id) {
assist
} else {
@@ -228,7 +215,7 @@ impl TerminalInlineAssistant {
fn request_for_inline_assist(
&self,
assist_id: TerminalInlineAssistId,
cx: &mut WindowContext,
cx: &mut AppContext,
) -> Result<LanguageModelRequest> {
let assist = self.assists.get(&assist_id).context("invalid assist")?;
@@ -284,16 +271,17 @@ impl TerminalInlineAssistant {
assist_id: TerminalInlineAssistId,
undo: bool,
execute: bool,
cx: &mut WindowContext,
window: &mut Window,
cx: &mut AppContext,
) {
self.dismiss_assist(assist_id, cx);
self.dismiss_assist(assist_id, window, cx);
if let Some(assist) = self.assists.remove(&assist_id) {
assist
.terminal
.update(cx, |this, cx| {
this.clear_block_below_cursor(cx);
this.focus_handle(cx).focus(cx);
this.focus_handle(cx).focus(window);
})
.log_err();
@@ -336,7 +324,8 @@ impl TerminalInlineAssistant {
fn dismiss_assist(
&mut self,
assist_id: TerminalInlineAssistId,
cx: &mut WindowContext,
window: &mut Window,
cx: &mut AppContext,
) -> bool {
let Some(assist) = self.assists.get_mut(&assist_id) else {
return false;
@@ -349,7 +338,7 @@ impl TerminalInlineAssistant {
.terminal
.update(cx, |this, cx| {
this.clear_block_below_cursor(cx);
this.focus_handle(cx).focus(cx);
this.focus_handle(cx).focus(window);
})
.is_ok()
}
@@ -358,7 +347,8 @@ impl TerminalInlineAssistant {
&mut self,
assist_id: TerminalInlineAssistId,
height: u8,
cx: &mut WindowContext,
window: &mut Window,
cx: &mut AppContext,
) {
if let Some(assist) = self.assists.get_mut(&assist_id) {
if let Some(prompt_editor) = assist.prompt_editor.as_ref().cloned() {
@@ -370,7 +360,7 @@ impl TerminalInlineAssistant {
height,
render: Box::new(move |_| prompt_editor.clone().into_any_element()),
};
terminal.set_block_below_cursor(block, cx);
terminal.set_block_below_cursor(block, window, cx);
})
.log_err();
}
@@ -379,10 +369,10 @@ impl TerminalInlineAssistant {
}
struct TerminalInlineAssist {
terminal: WeakView<TerminalView>,
prompt_editor: Option<View<PromptEditor>>,
codegen: Model<Codegen>,
workspace: WeakView<Workspace>,
terminal: WeakModel<TerminalView>,
prompt_editor: Option<Model<PromptEditor<TerminalCodegen>>>,
codegen: Model<TerminalCodegen>,
workspace: WeakModel<Workspace>,
context_store: Model<ContextStore>,
_subscriptions: Vec<Subscription>,
}
@@ -390,13 +380,14 @@ struct TerminalInlineAssist {
impl TerminalInlineAssist {
pub fn new(
assist_id: TerminalInlineAssistId,
terminal: &View<TerminalView>,
prompt_editor: View<PromptEditor>,
workspace: WeakView<Workspace>,
terminal: &Model<TerminalView>,
prompt_editor: Model<PromptEditor<TerminalCodegen>>,
workspace: WeakModel<Workspace>,
context_store: Model<ContextStore>,
cx: &mut WindowContext,
window: &mut Window,
cx: &mut AppContext,
) -> Self {
let codegen = prompt_editor.read(cx).codegen.clone();
let codegen = prompt_editor.read(cx).codegen().clone();
Self {
terminal: terminal.downgrade(),
prompt_editor: Some(prompt_editor.clone()),
@@ -404,12 +395,12 @@ impl TerminalInlineAssist {
workspace: workspace.clone(),
context_store,
_subscriptions: vec![
cx.subscribe(&prompt_editor, |prompt_editor, event, cx| {
window.subscribe(&prompt_editor, cx, |prompt_editor, event, window, cx| {
TerminalInlineAssistant::update_global(cx, |this, cx| {
this.handle_prompt_editor_event(prompt_editor, event, cx)
this.handle_prompt_editor_event(prompt_editor, event, window, cx)
})
}),
cx.subscribe(&codegen, move |codegen, event, cx| {
window.subscribe(&codegen, cx, move |codegen, event, window, cx| {
TerminalInlineAssistant::update_global(cx, |this, cx| match event {
CodegenEvent::Finished => {
let assist = if let Some(assist) = this.assists.get(&assist_id) {
@@ -438,7 +429,7 @@ impl TerminalInlineAssist {
}
if assist.prompt_editor.is_none() {
this.finish_assist(assist_id, false, false, cx);
this.finish_assist(assist_id, false, false, window, cx);
}
}
})
@@ -447,660 +438,3 @@ impl TerminalInlineAssist {
}
}
}
enum PromptEditorEvent {
StartRequested,
StopRequested,
ConfirmRequested { execute: bool },
CancelRequested,
DismissRequested,
Resized { height_in_lines: u8 },
}
struct PromptEditor {
id: TerminalInlineAssistId,
height_in_lines: u8,
editor: View<Editor>,
context_strip: View<ContextStrip>,
context_picker_menu_handle: PopoverMenuHandle<ContextPicker>,
language_model_selector: View<LanguageModelSelector>,
edited_since_done: bool,
prompt_history: VecDeque<String>,
prompt_history_ix: Option<usize>,
pending_prompt: String,
codegen: Model<Codegen>,
_codegen_subscription: Subscription,
editor_subscriptions: Vec<Subscription>,
}
impl EventEmitter<PromptEditorEvent> for PromptEditor {}
impl Render for PromptEditor {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let status = &self.codegen.read(cx).status;
let mut buttons = Vec::new();
buttons.extend(match status {
CodegenStatus::Idle => vec![
IconButton::new("cancel", IconName::Close)
.icon_color(Color::Muted)
.shape(IconButtonShape::Square)
.tooltip(|cx| Tooltip::for_action("Cancel Assist", &menu::Cancel, cx))
.on_click(cx.listener(|_, _, cx| cx.emit(PromptEditorEvent::CancelRequested)))
.into_any_element(),
IconButton::new("start", IconName::SparkleAlt)
.icon_color(Color::Muted)
.shape(IconButtonShape::Square)
.tooltip(|cx| Tooltip::for_action("Generate", &menu::Confirm, cx))
.on_click(cx.listener(|_, _, cx| cx.emit(PromptEditorEvent::StartRequested)))
.into_any_element(),
],
CodegenStatus::Pending => vec![
IconButton::new("cancel", IconName::Close)
.icon_color(Color::Muted)
.shape(IconButtonShape::Square)
.tooltip(|cx| Tooltip::text("Cancel Assist", cx))
.on_click(cx.listener(|_, _, cx| cx.emit(PromptEditorEvent::CancelRequested)))
.into_any_element(),
IconButton::new("stop", IconName::Stop)
.icon_color(Color::Error)
.shape(IconButtonShape::Square)
.tooltip(|cx| {
Tooltip::with_meta(
"Interrupt Generation",
Some(&menu::Cancel),
"Changes won't be discarded",
cx,
)
})
.on_click(cx.listener(|_, _, cx| cx.emit(PromptEditorEvent::StopRequested)))
.into_any_element(),
],
CodegenStatus::Error(_) | CodegenStatus::Done => {
let cancel = IconButton::new("cancel", IconName::Close)
.icon_color(Color::Muted)
.shape(IconButtonShape::Square)
.tooltip(|cx| Tooltip::for_action("Cancel Assist", &menu::Cancel, cx))
.on_click(cx.listener(|_, _, cx| cx.emit(PromptEditorEvent::CancelRequested)))
.into_any_element();
let has_error = matches!(status, CodegenStatus::Error(_));
if has_error || self.edited_since_done {
vec![
cancel,
IconButton::new("restart", IconName::RotateCw)
.icon_color(Color::Info)
.shape(IconButtonShape::Square)
.tooltip(|cx| {
Tooltip::with_meta(
"Restart Generation",
Some(&menu::Confirm),
"Changes will be discarded",
cx,
)
})
.on_click(cx.listener(|_, _, cx| {
cx.emit(PromptEditorEvent::StartRequested);
}))
.into_any_element(),
]
} else {
vec![
cancel,
IconButton::new("accept", IconName::Check)
.icon_color(Color::Info)
.shape(IconButtonShape::Square)
.tooltip(|cx| {
Tooltip::for_action("Accept Generated Command", &menu::Confirm, cx)
})
.on_click(cx.listener(|_, _, cx| {
cx.emit(PromptEditorEvent::ConfirmRequested { execute: false });
}))
.into_any_element(),
IconButton::new("confirm", IconName::Play)
.icon_color(Color::Info)
.shape(IconButtonShape::Square)
.tooltip(|cx| {
Tooltip::for_action(
"Execute Generated Command",
&menu::SecondaryConfirm,
cx,
)
})
.on_click(cx.listener(|_, _, cx| {
cx.emit(PromptEditorEvent::ConfirmRequested { execute: true });
}))
.into_any_element(),
]
}
}
});
v_flex()
.border_y_1()
.border_color(cx.theme().status().info_border)
.py_2()
.size_full()
.child(
h_flex()
.key_context("PromptEditor")
.bg(cx.theme().colors().editor_background)
.on_action(cx.listener(Self::toggle_context_picker))
.on_action(cx.listener(Self::confirm))
.on_action(cx.listener(Self::secondary_confirm))
.on_action(cx.listener(Self::cancel))
.on_action(cx.listener(Self::move_up))
.on_action(cx.listener(Self::move_down))
.child(
h_flex()
.w_12()
.justify_center()
.gap_2()
.child(LanguageModelSelectorPopoverMenu::new(
self.language_model_selector.clone(),
IconButton::new("context", IconName::SettingsAlt)
.shape(IconButtonShape::Square)
.icon_size(IconSize::Small)
.icon_color(Color::Muted)
.tooltip(move |cx| {
Tooltip::with_meta(
format!(
"Using {}",
LanguageModelRegistry::read_global(cx)
.active_model()
.map(|model| model.name().0)
.unwrap_or_else(|| "No model selected".into()),
),
None,
"Change Model",
cx,
)
}),
))
.children(
if let CodegenStatus::Error(error) = &self.codegen.read(cx).status {
let error_message = SharedString::from(error.to_string());
Some(
div()
.id("error")
.tooltip(move |cx| {
Tooltip::text(error_message.clone(), cx)
})
.child(
Icon::new(IconName::XCircle)
.size(IconSize::Small)
.color(Color::Error),
),
)
} else {
None
},
),
)
.child(div().flex_1().child(self.render_prompt_editor(cx)))
.child(h_flex().gap_1().pr_4().children(buttons)),
)
.child(h_flex().child(self.context_strip.clone()))
}
}
impl FocusableView for PromptEditor {
fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
self.editor.focus_handle(cx)
}
}
impl PromptEditor {
const MAX_LINES: u8 = 8;
#[allow(clippy::too_many_arguments)]
fn new(
id: TerminalInlineAssistId,
prompt_history: VecDeque<String>,
prompt_buffer: Model<MultiBuffer>,
codegen: Model<Codegen>,
fs: Arc<dyn Fs>,
context_store: Model<ContextStore>,
workspace: WeakView<Workspace>,
thread_store: Option<WeakModel<ThreadStore>>,
cx: &mut ViewContext<Self>,
) -> Self {
let prompt_editor = cx.new_view(|cx| {
let mut editor = Editor::new(
EditorMode::AutoHeight {
max_lines: Self::MAX_LINES as usize,
},
prompt_buffer,
None,
false,
cx,
);
editor.set_soft_wrap_mode(language::language_settings::SoftWrap::EditorWidth, cx);
editor.set_placeholder_text(Self::placeholder_text(cx), cx);
editor
});
let context_picker_menu_handle = PopoverMenuHandle::default();
let mut this = Self {
id,
height_in_lines: 1,
editor: prompt_editor.clone(),
context_strip: cx.new_view(|cx| {
ContextStrip::new(
context_store,
workspace.clone(),
thread_store.clone(),
prompt_editor.focus_handle(cx),
context_picker_menu_handle.clone(),
cx,
)
}),
context_picker_menu_handle,
language_model_selector: cx.new_view(|cx| {
let fs = fs.clone();
LanguageModelSelector::new(
move |model, cx| {
update_settings_file::<AssistantSettings>(
fs.clone(),
cx,
move |settings, _| settings.set_model(model.clone()),
);
},
cx,
)
}),
edited_since_done: false,
prompt_history,
prompt_history_ix: None,
pending_prompt: String::new(),
_codegen_subscription: cx.observe(&codegen, Self::handle_codegen_changed),
editor_subscriptions: Vec::new(),
codegen,
};
this.count_lines(cx);
this.subscribe_to_editor(cx);
this
}
fn placeholder_text(cx: &WindowContext) -> String {
let context_keybinding = text_for_action(&crate::ToggleFocus, cx)
.map(|keybinding| format!("{keybinding} for context"))
.unwrap_or_default();
format!("Generate…{context_keybinding} ↓↑ for history")
}
fn subscribe_to_editor(&mut self, cx: &mut ViewContext<Self>) {
self.editor_subscriptions.clear();
self.editor_subscriptions
.push(cx.observe(&self.editor, Self::handle_prompt_editor_changed));
self.editor_subscriptions
.push(cx.subscribe(&self.editor, Self::handle_prompt_editor_events));
}
fn prompt(&self, cx: &AppContext) -> String {
self.editor.read(cx).text(cx)
}
fn count_lines(&mut self, cx: &mut ViewContext<Self>) {
let height_in_lines = cmp::max(
2, // Make the editor at least two lines tall, to account for padding and buttons.
cmp::min(
self.editor
.update(cx, |editor, cx| editor.max_point(cx).row().0 + 1),
Self::MAX_LINES as u32,
),
) as u8;
if height_in_lines != self.height_in_lines {
self.height_in_lines = height_in_lines;
cx.emit(PromptEditorEvent::Resized { height_in_lines });
}
}
fn handle_prompt_editor_changed(&mut self, _: View<Editor>, cx: &mut ViewContext<Self>) {
self.count_lines(cx);
}
fn handle_prompt_editor_events(
&mut self,
_: View<Editor>,
event: &EditorEvent,
cx: &mut ViewContext<Self>,
) {
match event {
EditorEvent::Edited { .. } => {
let prompt = self.editor.read(cx).text(cx);
if self
.prompt_history_ix
.map_or(true, |ix| self.prompt_history[ix] != prompt)
{
self.prompt_history_ix.take();
self.pending_prompt = prompt;
}
self.edited_since_done = true;
cx.notify();
}
_ => {}
}
}
fn handle_codegen_changed(&mut self, _: Model<Codegen>, cx: &mut ViewContext<Self>) {
match &self.codegen.read(cx).status {
CodegenStatus::Idle => {
self.editor
.update(cx, |editor, _| editor.set_read_only(false));
}
CodegenStatus::Pending => {
self.editor
.update(cx, |editor, _| editor.set_read_only(true));
}
CodegenStatus::Done | CodegenStatus::Error(_) => {
self.edited_since_done = false;
self.editor
.update(cx, |editor, _| editor.set_read_only(false));
}
}
}
fn toggle_context_picker(&mut self, _: &ToggleContextPicker, cx: &mut ViewContext<Self>) {
self.context_picker_menu_handle.toggle(cx);
}
fn cancel(&mut self, _: &editor::actions::Cancel, cx: &mut ViewContext<Self>) {
match &self.codegen.read(cx).status {
CodegenStatus::Idle | CodegenStatus::Done | CodegenStatus::Error(_) => {
cx.emit(PromptEditorEvent::CancelRequested);
}
CodegenStatus::Pending => {
cx.emit(PromptEditorEvent::StopRequested);
}
}
}
fn confirm(&mut self, _: &menu::Confirm, cx: &mut ViewContext<Self>) {
match &self.codegen.read(cx).status {
CodegenStatus::Idle => {
if !self.editor.read(cx).text(cx).trim().is_empty() {
cx.emit(PromptEditorEvent::StartRequested);
}
}
CodegenStatus::Pending => {
cx.emit(PromptEditorEvent::DismissRequested);
}
CodegenStatus::Done => {
if self.edited_since_done {
cx.emit(PromptEditorEvent::StartRequested);
} else {
cx.emit(PromptEditorEvent::ConfirmRequested { execute: false });
}
}
CodegenStatus::Error(_) => {
cx.emit(PromptEditorEvent::StartRequested);
}
}
}
fn secondary_confirm(&mut self, _: &menu::SecondaryConfirm, cx: &mut ViewContext<Self>) {
if matches!(self.codegen.read(cx).status, CodegenStatus::Done) {
cx.emit(PromptEditorEvent::ConfirmRequested { execute: true });
}
}
fn move_up(&mut self, _: &MoveUp, cx: &mut ViewContext<Self>) {
if let Some(ix) = self.prompt_history_ix {
if ix > 0 {
self.prompt_history_ix = Some(ix - 1);
let prompt = self.prompt_history[ix - 1].as_str();
self.editor.update(cx, |editor, cx| {
editor.set_text(prompt, cx);
editor.move_to_beginning(&Default::default(), cx);
});
}
} else if !self.prompt_history.is_empty() {
self.prompt_history_ix = Some(self.prompt_history.len() - 1);
let prompt = self.prompt_history[self.prompt_history.len() - 1].as_str();
self.editor.update(cx, |editor, cx| {
editor.set_text(prompt, cx);
editor.move_to_beginning(&Default::default(), cx);
});
}
}
fn move_down(&mut self, _: &MoveDown, cx: &mut ViewContext<Self>) {
if let Some(ix) = self.prompt_history_ix {
if ix < self.prompt_history.len() - 1 {
self.prompt_history_ix = Some(ix + 1);
let prompt = self.prompt_history[ix + 1].as_str();
self.editor.update(cx, |editor, cx| {
editor.set_text(prompt, cx);
editor.move_to_end(&Default::default(), cx)
});
} else {
self.prompt_history_ix = None;
let prompt = self.pending_prompt.as_str();
self.editor.update(cx, |editor, cx| {
editor.set_text(prompt, cx);
editor.move_to_end(&Default::default(), cx)
});
}
}
}
fn render_prompt_editor(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let settings = ThemeSettings::get_global(cx);
let text_style = TextStyle {
color: if self.editor.read(cx).read_only(cx) {
cx.theme().colors().text_disabled
} else {
cx.theme().colors().text
},
font_family: settings.buffer_font.family.clone(),
font_fallbacks: settings.buffer_font.fallbacks.clone(),
font_size: settings.buffer_font_size.into(),
font_weight: settings.buffer_font.weight,
line_height: relative(settings.buffer_line_height.value()),
..Default::default()
};
EditorElement::new(
&self.editor,
EditorStyle {
background: cx.theme().colors().editor_background,
local_player: cx.theme().players().local(),
text: text_style,
..Default::default()
},
)
}
}
#[derive(Debug)]
pub enum CodegenEvent {
Finished,
}
impl EventEmitter<CodegenEvent> for Codegen {}
const CLEAR_INPUT: &str = "\x15";
const CARRIAGE_RETURN: &str = "\x0d";
struct TerminalTransaction {
terminal: Model<Terminal>,
}
impl TerminalTransaction {
pub fn start(terminal: Model<Terminal>) -> Self {
Self { terminal }
}
pub fn push(&mut self, hunk: String, cx: &mut AppContext) {
// Ensure that the assistant cannot accidentally execute commands that are streamed into the terminal
let input = Self::sanitize_input(hunk);
self.terminal
.update(cx, |terminal, _| terminal.input(input));
}
pub fn undo(&self, cx: &mut AppContext) {
self.terminal
.update(cx, |terminal, _| terminal.input(CLEAR_INPUT.to_string()));
}
pub fn complete(&self, cx: &mut AppContext) {
self.terminal.update(cx, |terminal, _| {
terminal.input(CARRIAGE_RETURN.to_string())
});
}
fn sanitize_input(input: String) -> String {
input.replace(['\r', '\n'], "")
}
}
pub struct Codegen {
status: CodegenStatus,
telemetry: Option<Arc<Telemetry>>,
terminal: Model<Terminal>,
generation: Task<()>,
message_id: Option<String>,
transaction: Option<TerminalTransaction>,
}
impl Codegen {
pub fn new(terminal: Model<Terminal>, telemetry: Option<Arc<Telemetry>>) -> Self {
Self {
terminal,
telemetry,
status: CodegenStatus::Idle,
generation: Task::ready(()),
message_id: None,
transaction: None,
}
}
pub fn start(&mut self, prompt: LanguageModelRequest, cx: &mut ModelContext<Self>) {
let Some(model) = LanguageModelRegistry::read_global(cx).active_model() else {
return;
};
let model_api_key = model.api_key(cx);
let http_client = cx.http_client();
let telemetry = self.telemetry.clone();
self.status = CodegenStatus::Pending;
self.transaction = Some(TerminalTransaction::start(self.terminal.clone()));
self.generation = cx.spawn(|this, mut cx| async move {
let model_telemetry_id = model.telemetry_id();
let model_provider_id = model.provider_id();
let response = model.stream_completion_text(prompt, &cx).await;
let generate = async {
let message_id = response
.as_ref()
.ok()
.and_then(|response| response.message_id.clone());
let (mut hunks_tx, mut hunks_rx) = mpsc::channel(1);
let task = cx.background_executor().spawn({
let message_id = message_id.clone();
let executor = cx.background_executor().clone();
async move {
let mut response_latency = None;
let request_start = Instant::now();
let task = async {
let mut chunks = response?.stream;
while let Some(chunk) = chunks.next().await {
if response_latency.is_none() {
response_latency = Some(request_start.elapsed());
}
let chunk = chunk?;
hunks_tx.send(chunk).await?;
}
anyhow::Ok(())
};
let result = task.await;
let error_message = result.as_ref().err().map(|error| error.to_string());
report_assistant_event(
AssistantEvent {
conversation_id: None,
kind: AssistantKind::InlineTerminal,
message_id,
phase: AssistantPhase::Response,
model: model_telemetry_id,
model_provider: model_provider_id.to_string(),
response_latency,
error_message,
language_name: None,
},
telemetry,
http_client,
model_api_key,
&executor,
);
result?;
anyhow::Ok(())
}
});
this.update(&mut cx, |this, _| {
this.message_id = message_id;
})?;
while let Some(hunk) = hunks_rx.next().await {
this.update(&mut cx, |this, cx| {
if let Some(transaction) = &mut this.transaction {
transaction.push(hunk, cx);
cx.notify();
}
})?;
}
task.await?;
anyhow::Ok(())
};
let result = generate.await;
this.update(&mut cx, |this, cx| {
if let Err(error) = result {
this.status = CodegenStatus::Error(error);
} else {
this.status = CodegenStatus::Done;
}
cx.emit(CodegenEvent::Finished);
cx.notify();
})
.ok();
});
cx.notify();
}
pub fn stop(&mut self, cx: &mut ModelContext<Self>) {
self.status = CodegenStatus::Done;
self.generation = Task::ready(());
cx.emit(CodegenEvent::Finished);
cx.notify();
}
pub fn complete(&mut self, cx: &mut ModelContext<Self>) {
if let Some(transaction) = self.transaction.take() {
transaction.complete(cx);
}
}
pub fn undo(&mut self, cx: &mut ModelContext<Self>) {
if let Some(transaction) = self.transaction.take() {
transaction.undo(cx);
}
}
}
enum CodegenStatus {
Idle,
Pending,
Done,
Error(anyhow::Error),
}

View File

@@ -1,5 +1,5 @@
use gpui::{
uniform_list, AppContext, FocusHandle, FocusableView, Model, UniformListScrollHandle, WeakView,
uniform_list, AppContext, FocusHandle, Focusable, Model, UniformListScrollHandle, WeakModel,
};
use time::{OffsetDateTime, UtcOffset};
use ui::{prelude::*, IconButtonShape, ListItem, ListItemSpacing, Tooltip};
@@ -10,16 +10,17 @@ use crate::AssistantPanel;
pub struct ThreadHistory {
focus_handle: FocusHandle,
assistant_panel: WeakView<AssistantPanel>,
assistant_panel: WeakModel<AssistantPanel>,
thread_store: Model<ThreadStore>,
scroll_handle: UniformListScrollHandle,
}
impl ThreadHistory {
pub(crate) fn new(
assistant_panel: WeakView<AssistantPanel>,
assistant_panel: WeakModel<AssistantPanel>,
thread_store: Model<ThreadStore>,
cx: &mut ViewContext<Self>,
cx: &mut ModelContext<Self>,
) -> Self {
Self {
focus_handle: cx.focus_handle(),
@@ -30,14 +31,14 @@ impl ThreadHistory {
}
}
impl FocusableView for ThreadHistory {
impl Focusable for ThreadHistory {
fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
self.focus_handle.clone()
}
}
impl Render for ThreadHistory {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
fn render(&mut self, _window: &mut Window, cx: &mut ModelContext<Self>) -> impl IntoElement {
let threads = self.thread_store.update(cx, |this, cx| this.threads(cx));
v_flex()
@@ -59,17 +60,17 @@ impl Render for ThreadHistory {
} else {
history.child(
uniform_list(
cx.view().clone(),
cx.model().clone(),
"thread-history",
threads.len(),
move |history, range, _cx| {
move |history, range, _window, _cx| {
threads[range]
.iter()
.map(|thread| {
PastThread::new(
h_flex().w_full().pb_1().child(PastThread::new(
thread.clone(),
history.assistant_panel.clone(),
)
))
})
.collect()
},
@@ -85,11 +86,11 @@ impl Render for ThreadHistory {
#[derive(IntoElement)]
pub struct PastThread {
thread: Model<Thread>,
assistant_panel: WeakView<AssistantPanel>,
assistant_panel: WeakModel<AssistantPanel>,
}
impl PastThread {
pub fn new(thread: Model<Thread>, assistant_panel: WeakView<AssistantPanel>) -> Self {
pub fn new(thread: Model<Thread>, assistant_panel: WeakModel<AssistantPanel>) -> Self {
Self {
thread,
assistant_panel,
@@ -98,7 +99,7 @@ impl PastThread {
}
impl RenderOnce for PastThread {
fn render(self, cx: &mut WindowContext) -> impl IntoElement {
fn render(self, _window: &mut Window, cx: &mut AppContext) -> impl IntoElement {
let (id, summary) = {
const DEFAULT_SUMMARY: SharedString = SharedString::new_static("New Thread");
let thread = self.thread.read(cx);
@@ -139,11 +140,11 @@ impl RenderOnce for PastThread {
IconButton::new("delete", IconName::TrashAlt)
.shape(IconButtonShape::Square)
.icon_size(IconSize::Small)
.tooltip(|cx| Tooltip::text("Delete Thread", cx))
.tooltip(Tooltip::text("Delete Thread"))
.on_click({
let assistant_panel = self.assistant_panel.clone();
let id = id.clone();
move |_event, cx| {
move |_event, _, cx| {
assistant_panel
.update(cx, |this, cx| {
this.delete_thread(&id, cx);
@@ -156,10 +157,10 @@ impl RenderOnce for PastThread {
.on_click({
let assistant_panel = self.assistant_panel.clone();
let id = id.clone();
move |_event, cx| {
move |_event, window, cx| {
assistant_panel
.update(cx, |this, cx| {
this.open_thread(&id, cx);
this.open_thread(&id, window, cx);
})
.ok();
}

View File

@@ -3,12 +3,12 @@ use std::rc::Rc;
use gpui::ClickEvent;
use ui::{prelude::*, IconButtonShape};
use crate::context::Context;
use crate::context::{Context, ContextKind};
#[derive(IntoElement)]
pub struct ContextPill {
context: Context,
on_remove: Option<Rc<dyn Fn(&ClickEvent, &mut WindowContext)>>,
on_remove: Option<Rc<dyn Fn(&ClickEvent, &mut Window, &mut AppContext)>>,
}
impl ContextPill {
@@ -19,23 +19,39 @@ impl ContextPill {
}
}
pub fn on_remove(mut self, on_remove: Rc<dyn Fn(&ClickEvent, &mut WindowContext)>) -> Self {
pub fn on_remove(
mut self,
on_remove: Rc<dyn Fn(&ClickEvent, &mut Window, &mut AppContext)>,
) -> Self {
self.on_remove = Some(on_remove);
self
}
}
impl RenderOnce for ContextPill {
fn render(self, cx: &mut WindowContext) -> impl IntoElement {
fn render(self, _window: &mut Window, cx: &mut AppContext) -> impl IntoElement {
let padding_right = if self.on_remove.is_some() {
px(2.)
} else {
px(4.)
};
let icon = match self.context.kind {
ContextKind::File => IconName::File,
ContextKind::Directory => IconName::Folder,
ContextKind::FetchedUrl => IconName::Globe,
ContextKind::Thread => IconName::MessageCircle,
};
h_flex()
.gap_1()
.pl_1p5()
.pr_0p5()
.pl_1()
.pr(padding_right)
.pb(px(1.))
.border_1()
.border_color(cx.theme().colors().border.opacity(0.5))
.bg(cx.theme().colors().element_background)
.rounded_md()
.child(Icon::new(icon).size(IconSize::XSmall).color(Color::Muted))
.child(Label::new(self.context.name.clone()).size(LabelSize::Small))
.when_some(self.on_remove, |parent, on_remove| {
parent.child(
@@ -44,7 +60,7 @@ impl RenderOnce for ContextPill {
.icon_size(IconSize::XSmall)
.on_click({
let on_remove = on_remove.clone();
move |event, cx| on_remove(event, cx)
move |event, window, cx| on_remove(event, window, cx)
}),
)
})

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, WeakModel, Window};
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 Window,
cx: &mut 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 Window,
cx: &mut 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 Window, &mut AppContext)>,
&mut Window,
&mut 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, WeakModel, Window};
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 Window,
cx: &mut 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 Window,
cx: &mut 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, WeakModel, Window};
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 Window,
cx: &mut 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::{AppContext, Task, WeakModel, Window};
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 Window,
_cx: &mut AppContext,
) -> Task<Result<String>> {
let input: FileToolInput = match serde_json::from_value(input) {
Ok(input) => input,

View File

@@ -4,7 +4,7 @@ use db::kvp::KEY_VALUE_STORE;
use db::RELEASE_CHANNEL;
use gpui::{
actions, AppContext, AsyncAppContext, Context as _, Global, Model, ModelContext,
SemanticVersion, Task, WindowContext,
SemanticVersion, Task, Window,
};
use http_client::{AsyncBody, HttpClient, HttpClientWithUrl};
use paths::remote_servers_dir;
@@ -130,10 +130,10 @@ impl Global for GlobalAutoUpdate {}
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));
cx.observe_new_models(|workspace: &mut Workspace, _window, _cx| {
workspace.register_action(|_, action: &Check, window, cx| check(action, window, cx));
workspace.register_action(|_, action, cx| {
workspace.register_action(|_, action, _, cx| {
view_release_notes(action, cx);
});
})
@@ -172,23 +172,25 @@ 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 Window, cx: &mut AppContext) {
if let Some(message) = option_env!("ZED_UPDATE_EXPLANATION") {
drop(cx.prompt(
drop(window.prompt(
gpui::PromptLevel::Info,
"Zed was installed via a package manager.",
Some(message),
&["Ok"],
cx,
));
return;
}
if let Ok(message) = env::var("ZED_UPDATE_EXPLANATION") {
drop(cx.prompt(
drop(window.prompt(
gpui::PromptLevel::Info,
"Zed was installed via a package manager.",
Some(&message),
&["Ok"],
cx,
));
return;
}
@@ -203,11 +205,12 @@ pub fn check(_: &Check, cx: &mut WindowContext) {
if let Some(updater) = AutoUpdater::get(cx) {
updater.update(cx, |updater, cx| updater.poll(cx));
} else {
drop(cx.prompt(
drop(window.prompt(
gpui::PromptLevel::Info,
"Could not check for updates",
Some("Auto-updates disabled for non-bundled app."),
&["Ok"],
cx,
));
}
}

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, ModelContext, SharedString, Window};
use http_client::HttpClient;
use markdown_preview::markdown_preview_view::{MarkdownPreviewMode, MarkdownPreviewView};
use release_channel::{AppVersion, ReleaseChannel};
@@ -17,9 +17,9 @@ use crate::update_notification::UpdateNotification;
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);
cx.observe_new_models(|workspace: &mut Workspace, _window, _cx| {
workspace.register_action(|workspace, _: &ViewReleaseNotesLocally, window, cx| {
view_release_notes_locally(workspace, window, 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,
window: &mut Window,
cx: &mut ModelContext<Workspace>,
) {
let release_channel = ReleaseChannel::global(cx);
let url = match release_channel {
@@ -60,8 +64,8 @@ fn view_release_notes_locally(workspace: &mut Workspace, cx: &mut ViewContext<Wo
.language_for_name("Markdown");
workspace
.with_local_workspace(cx, move |_, cx| {
cx.spawn(|workspace, mut cx| async move {
.with_local_workspace(window, cx, move |_, window, cx| {
cx.spawn_in(window, |workspace, mut cx| async move {
let markdown = markdown.await.log_err();
let response = client.get(&url, Default::default(), true).await;
let Some(mut response) = response.log_err() else {
@@ -76,7 +80,7 @@ fn view_release_notes_locally(workspace: &mut Workspace, cx: &mut ViewContext<Wo
if let Ok(body) = body {
workspace
.update(&mut cx, |workspace, cx| {
.update_in(&mut cx, |workspace, window, cx| {
let project = workspace.project().clone();
let buffer = project.update(cx, |project, cx| {
project.create_local_buffer("", markdown, cx)
@@ -89,22 +93,24 @@ fn view_release_notes_locally(workspace: &mut Workspace, cx: &mut ViewContext<Wo
let buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer, 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(|cx| {
Editor::for_multibuffer(buffer, Some(project), true, window, 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),
window,
cx,
);
workspace.add_item_to_active_pane(
Box::new(view.clone()),
None,
true,
window,
cx,
);
cx.notify();
@@ -117,12 +123,15 @@ 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(
window: &mut Window,
cx: &mut ModelContext<Workspace>,
) -> 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);
cx.spawn(|workspace, mut cx| async move {
cx.spawn_in(window, |workspace, mut cx| async move {
let should_show_notification = should_show_notification.await?;
if should_show_notification {
workspace.update(&mut cx, |workspace, cx| {
@@ -130,7 +139,7 @@ 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

View File

@@ -1,6 +1,6 @@
use gpui::{
div, DismissEvent, EventEmitter, InteractiveElement, IntoElement, ParentElement, Render,
SemanticVersion, StatefulInteractiveElement, Styled, ViewContext, WeakView,
div, DismissEvent, EventEmitter, InteractiveElement, IntoElement, ModelContext, ParentElement,
Render, SemanticVersion, StatefulInteractiveElement, Styled, WeakModel, Window,
};
use menu::Cancel;
use release_channel::ReleaseChannel;
@@ -12,13 +12,13 @@ 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, _: &mut Window, cx: &mut ModelContext<Self>) -> impl IntoElement {
let app_name = ReleaseChannel::global(cx).display_name();
v_flex()
@@ -37,7 +37,9 @@ 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(cx.listener(|this, _, window, cx| {
this.dismiss(&menu::Cancel, window, cx)
})),
),
)
.child(
@@ -45,24 +47,24 @@ impl Render for UpdateNotification {
.id("notes")
.child(Label::new("View the release notes"))
.cursor_pointer()
.on_click(cx.listener(|this, _, cx| {
.on_click(cx.listener(|this, _, window, cx| {
this.workspace
.update(cx, |workspace, cx| {
crate::view_release_notes_locally(workspace, cx);
crate::view_release_notes_locally(workspace, window, cx);
})
.log_err();
this.dismiss(&menu::Cancel, cx)
this.dismiss(&menu::Cancel, window, cx)
})),
)
}
}
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>) {
pub fn dismiss(&mut self, _: &Cancel, _: &mut Window, cx: &mut ModelContext<Self>) {
cx.emit(DismissEvent);
}
}

View File

@@ -16,10 +16,10 @@ doctest = false
editor.workspace = true
gpui.workspace = true
itertools.workspace = true
outline.workspace = true
theme.workspace = true
ui.workspace = true
workspace.workspace = true
zed_actions.workspace = true
[dev-dependencies]
editor = { workspace = true, features = ["test-support"] }

View File

@@ -1,7 +1,7 @@
use editor::Editor;
use gpui::{
Element, EventEmitter, FocusableView, IntoElement, ParentElement, Render, StyledText,
Subscription, ViewContext,
Element, EventEmitter, Focusable, IntoElement, ModelContext, ParentElement, Render, StyledText,
Subscription, Window,
};
use itertools::Itertools;
use std::cmp;
@@ -37,12 +37,19 @@ 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, window: &mut Window, cx: &mut ModelContext<Self>) -> impl IntoElement {
const MAX_SEGMENTS: usize = 12;
let element = h_flex().text_ui(cx);
let element = h_flex()
.id("breadcrumb-container")
.flex_grow()
.overflow_x_scroll()
.text_ui(cx);
let Some(active_item) = self.active_item.as_ref() else {
return element;
};
let Some(mut segments) = active_item.breadcrumbs(cx.theme(), cx) else {
return element;
};
@@ -52,6 +59,7 @@ impl Render for Breadcrumbs {
prefix_end_ix,
segments.len().saturating_sub(MAX_SEGMENTS / 2),
);
if suffix_start_ix > prefix_end_ix {
segments.splice(
prefix_end_ix..suffix_start_ix,
@@ -64,7 +72,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;
@@ -82,6 +90,7 @@ impl Render for Breadcrumbs {
});
let breadcrumbs_stack = h_flex().gap_1().children(breadcrumbs);
match active_item
.downcast::<Editor>()
.map(|editor| editor.downgrade())
@@ -92,25 +101,30 @@ impl Render for Breadcrumbs {
.style(ButtonStyle::Transparent)
.on_click({
let editor = editor.clone();
move |_, cx| {
if let Some(editor) = editor.upgrade() {
outline::toggle(editor, &editor::actions::ToggleOutline, cx)
move |_, window, cx| {
if let Some((editor, callback)) = editor
.upgrade()
.zip(zed_actions::outline::TOGGLE_OUTLINE.get())
{
callback(editor.to_any(), window, 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,
"Show Symbol Outline",
&zed_actions::outline::ToggleOutline,
&focus_handle,
window,
cx,
)
} else {
Tooltip::for_action(
"Show symbol outline",
&editor::actions::ToggleOutline,
"Show Symbol Outline",
&zed_actions::outline::ToggleOutline,
window,
cx,
)
}
@@ -128,7 +142,8 @@ impl ToolbarItemView for Breadcrumbs {
fn set_active_pane_item(
&mut self,
active_pane_item: Option<&dyn ItemHandle>,
cx: &mut ViewContext<Self>,
window: &mut Window,
cx: &mut ModelContext<Self>,
) -> ToolbarItemLocation {
cx.notify();
self.active_item = None;
@@ -137,10 +152,11 @@ impl ToolbarItemView for Breadcrumbs {
return ToolbarItemLocation::Hidden;
};
let this = cx.view().downgrade();
let this = cx.model().downgrade();
self.subscription = Some(item.subscribe_to_item_events(
window,
cx,
Box::new(move |event, cx| {
Box::new(move |event, _, cx| {
if let ItemEvent::UpdateBreadcrumbs = event {
this.update(cx, |this, cx| {
cx.notify();
@@ -158,7 +174,12 @@ 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,
_window: &mut Window,
_: &mut ModelContext<Self>,
) {
self.pane_focused = pane_focused;
}
}

View File

@@ -40,6 +40,7 @@ schemars.workspace = true
serde.workspace = true
serde_derive.workspace = true
settings.workspace = true
telemetry.workspace = true
util.workspace = true
[target.'cfg(target_os = "macos")'.dependencies]

View File

@@ -250,7 +250,9 @@ impl ActiveCall {
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))?;
this.update(&mut cx, |this, cx| {
this.report_call_event("Participant Invited", cx)
})?;
} else {
//TODO: report collaboration error
log::error!("invite failed: {:?}", result);
@@ -318,7 +320,7 @@ impl ActiveCall {
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)
this.report_call_event("Incoming Call Accepted", cx)
})?;
Ok(())
})
@@ -331,7 +333,7 @@ impl ActiveCall {
.borrow_mut()
.take()
.ok_or_else(|| anyhow!("no incoming call"))?;
report_call_event_for_room("decline incoming", call.room_id, None, &self.client);
telemetry::event!("Incoming Call Declined", room_id = call.room_id);
self.client.send(proto::DeclineCall {
room_id: call.room_id,
})?;
@@ -366,7 +368,7 @@ impl ActiveCall {
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)
this.report_call_event("Channel Joined", cx)
})?;
Ok(room)
})
@@ -374,7 +376,7 @@ impl ActiveCall {
pub fn hang_up(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
cx.notify();
self.report_call_event("hang up", cx);
self.report_call_event("Call Ended", cx);
Audio::end_call(cx);
@@ -393,7 +395,7 @@ impl ActiveCall {
cx: &mut ModelContext<Self>,
) -> Task<Result<u64>> {
if let Some((room, _)) = self.room.as_ref() {
self.report_call_event("share project", cx);
self.report_call_event("Project Shared", cx);
room.update(cx, |room, cx| room.share_project(project, cx))
} else {
Task::ready(Err(anyhow!("no active call")))
@@ -406,7 +408,7 @@ impl ActiveCall {
cx: &mut ModelContext<Self>,
) -> Result<()> {
if let Some((room, _)) = self.room.as_ref() {
self.report_call_event("unshare project", cx);
self.report_call_event("Project Unshared", cx);
room.update(cx, |room, cx| room.unshare_project(project, cx))
} else {
Err(anyhow!("no active call"))
@@ -486,35 +488,15 @@ impl ActiveCall {
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);
telemetry::event!(
operation,
room_id = room.id(),
channel_id = room.channel_id()
)
}
}
}
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;

View File

@@ -243,7 +243,9 @@ impl ActiveCall {
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))?;
this.update(&mut cx, |this, cx| {
this.report_call_event("Participant Invited", cx)
})?;
} else {
//TODO: report collaboration error
log::error!("invite failed: {:?}", result);
@@ -311,7 +313,7 @@ impl ActiveCall {
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)
this.report_call_event("Incoming Call Accepted", cx)
})?;
Ok(())
})
@@ -324,7 +326,7 @@ impl ActiveCall {
.borrow_mut()
.take()
.ok_or_else(|| anyhow!("no incoming call"))?;
report_call_event_for_room("decline incoming", call.room_id, None, &self.client);
telemetry::event!("Incoming Call Declined", room_id = call.room_id);
self.client.send(proto::DeclineCall {
room_id: call.room_id,
})?;
@@ -359,7 +361,7 @@ impl ActiveCall {
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)
this.report_call_event("Channel Joined", cx)
})?;
Ok(room)
})
@@ -367,7 +369,7 @@ impl ActiveCall {
pub fn hang_up(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
cx.notify();
self.report_call_event("hang up", cx);
self.report_call_event("Call Ended", cx);
Audio::end_call(cx);
@@ -386,7 +388,7 @@ impl ActiveCall {
cx: &mut ModelContext<Self>,
) -> Task<Result<u64>> {
if let Some((room, _)) = self.room.as_ref() {
self.report_call_event("share project", cx);
self.report_call_event("Project Shared", cx);
room.update(cx, |room, cx| room.share_project(project, cx))
} else {
Task::ready(Err(anyhow!("no active call")))
@@ -399,7 +401,7 @@ impl ActiveCall {
cx: &mut ModelContext<Self>,
) -> Result<()> {
if let Some((room, _)) = self.room.as_ref() {
self.report_call_event("unshare project", cx);
self.report_call_event("Project Unshared", cx);
room.update(cx, |room, cx| room.unshare_project(project, cx))
} else {
Err(anyhow!("no active call"))
@@ -479,35 +481,15 @@ impl ActiveCall {
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);
telemetry::event!(
operation,
room_id = room.id(),
channel_id = room.channel_id()
);
}
}
}
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;

View File

@@ -81,7 +81,7 @@ 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(&cx.model(), &mut cx.to_async())),
user_store,
channel_store,
};

View File

@@ -134,7 +134,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(&cx.model(), &mut cx.to_async()),
}
})?;
Self::handle_loaded_messages(

View File

@@ -311,7 +311,7 @@ impl ChannelStore {
) -> Task<Result<Model<ChannelBuffer>>> {
let client = self.client.clone();
let user_store = self.user_store.clone();
let channel_store = cx.handle();
let channel_store = cx.model();
self.open_channel_resource(
channel_id,
|this| &mut this.opened_buffers,
@@ -441,7 +441,7 @@ impl ChannelStore {
) -> Task<Result<Model<ChannelChat>>> {
let client = self.client.clone();
let user_store = self.user_store.clone();
let this = cx.handle();
let this = cx.model();
self.open_channel_resource(
channel_id,
|this| &mut this.opened_chats,

View File

@@ -25,7 +25,6 @@ anyhow.workspace = true
clap.workspace = true
collections.workspace = true
ipc-channel = "0.19"
once_cell.workspace = true
parking_lot.workspace = true
paths.workspace = true
release_channel.workspace = true

View File

@@ -18,6 +18,12 @@ use std::{
use tempfile::NamedTempFile;
use util::paths::PathWithPosition;
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
use {
std::io::IsTerminal,
util::{load_login_shell_environment, load_shell_from_passwd, ResultExt},
};
struct Detect;
trait InstalledApp {
@@ -161,7 +167,16 @@ fn main() -> Result<()> {
None
};
// On Linux, desktop entry uses `cli` to spawn `zed`, so we need to load env vars from the shell
// since it doesn't inherit env vars from the terminal.
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
if !std::io::stdout().is_terminal() {
load_shell_from_passwd().log_err();
load_login_shell_environment().log_err();
}
let env = Some(std::env::vars().collect::<HashMap<_, _>>());
let exit_status = Arc::new(Mutex::new(None));
let mut paths = vec![];
let mut urls = vec![];
@@ -242,6 +257,7 @@ fn main() -> Result<()> {
if args.foreground {
app.run_foreground(url)?;
} else {
eprintln!("Logs are written to {:?}", paths::log_file());
app.launch(url)?;
sender.join().unwrap()?;
pipe_handle.join().unwrap()?;
@@ -262,6 +278,7 @@ mod linux {
os::unix::net::{SocketAddr, UnixDatagram},
path::{Path, PathBuf},
process::{self, ExitStatus},
sync::LazyLock,
thread,
time::Duration,
};
@@ -269,12 +286,11 @@ mod linux {
use anyhow::anyhow;
use cli::FORCE_CLI_MODE_ENV_VAR_NAME;
use fork::Fork;
use once_cell::sync::Lazy;
use crate::{Detect, InstalledApp};
static RELEASE_CHANNEL: Lazy<String> =
Lazy::new(|| include_str!("../../zed/RELEASE_CHANNEL").trim().to_string());
static RELEASE_CHANNEL: LazyLock<String> =
LazyLock::new(|| include_str!("../../zed/RELEASE_CHANNEL").trim().to_string());
struct App(PathBuf);

View File

@@ -27,7 +27,6 @@ futures.workspace = true
gpui.workspace = true
http_client.workspace = true
log.workspace = true
once_cell.workspace = true
paths.workspace = true
parking_lot.workspace = true
postage.workspace = true
@@ -51,6 +50,7 @@ tokio-socks = { version = "0.5.2", default-features = false, features = ["future
url.workspace = true
util.workspace = true
worktree.workspace = true
telemetry.workspace = true
[dev-dependencies]
clock = { workspace = true, features = ["test-support"] }

View File

@@ -4,10 +4,10 @@ use crate::{ChannelId, TelemetrySettings};
use anyhow::Result;
use clock::SystemClock;
use collections::{HashMap, HashSet};
use futures::Future;
use futures::channel::mpsc;
use futures::{Future, StreamExt};
use gpui::{AppContext, BackgroundExecutor, Task};
use http_client::{self, AsyncBody, HttpClient, HttpClientWithUrl, Method, Request};
use once_cell::sync::Lazy;
use parking_lot::Mutex;
use release_channel::ReleaseChannel;
use settings::{Settings, SettingsStore};
@@ -15,11 +15,15 @@ use sha2::{Digest, Sha256};
use std::fs::File;
use std::io::Write;
use std::time::Instant;
use std::{env, mem, path::PathBuf, sync::Arc, time::Duration};
use std::{
env, mem,
path::PathBuf,
sync::{Arc, LazyLock},
time::Duration,
};
use telemetry_events::{
ActionEvent, AppEvent, AssistantEvent, CallEvent, EditEvent, EditorEvent, Event,
EventRequestBody, EventWrapper, ExtensionEvent, InlineCompletionEvent, InlineCompletionRating,
InlineCompletionRatingEvent, ReplEvent, SettingEvent,
AppEvent, AssistantEvent, CallEvent, EditEvent, Event, EventRequestBody, EventWrapper,
InlineCompletionEvent,
};
use util::{ResultExt, TryFutureExt};
use worktree::{UpdatedEntriesSet, WorktreeId};
@@ -84,7 +88,7 @@ const FLUSH_INTERVAL: Duration = Duration::from_secs(1);
#[cfg(not(debug_assertions))]
const FLUSH_INTERVAL: Duration = Duration::from_secs(60 * 5);
static ZED_CLIENT_CHECKSUM_SEED: Lazy<Option<Vec<u8>>> = Lazy::new(|| {
static ZED_CLIENT_CHECKSUM_SEED: LazyLock<Option<Vec<u8>>> = LazyLock::new(|| {
option_env!("ZED_CLIENT_CHECKSUM_SEED")
.map(|s| s.as_bytes().into())
.or_else(|| {
@@ -245,7 +249,6 @@ impl Telemetry {
})
.detach();
// TODO: Replace all hardware stuff with nested SystemSpecs json
let this = Arc::new(Self {
clock,
http_client: client,
@@ -253,6 +256,21 @@ impl Telemetry {
state,
});
let (tx, mut rx) = mpsc::unbounded();
::telemetry::init(tx);
cx.background_executor()
.spawn({
let this = Arc::downgrade(&this);
async move {
while let Some(event) = rx.next().await {
let Some(state) = this.upgrade() else { break };
state.report_event(Event::Flexible(event))
}
}
})
.detach();
// We should only ever have one instance of Telemetry, leak the subscription to keep it alive
// rather than store in TelemetryState, complicating spawn as subscriptions are not Send
std::mem::forget(cx.on_app_quit({
@@ -320,27 +338,6 @@ impl Telemetry {
drop(state);
}
pub fn report_editor_event(
self: &Arc<Self>,
file_extension: Option<String>,
vim_mode: bool,
operation: &'static str,
copilot_enabled: bool,
copilot_enabled_for_language: bool,
is_via_ssh: bool,
) {
let event = Event::Editor(EditorEvent {
file_extension,
vim_mode,
operation: operation.into(),
copilot_enabled,
copilot_enabled_for_language,
is_via_ssh,
});
self.report_event(event)
}
pub fn report_inline_completion_event(
self: &Arc<Self>,
provider: String,
@@ -356,24 +353,6 @@ impl Telemetry {
self.report_event(event)
}
pub fn report_inline_completion_rating_event(
self: &Arc<Self>,
rating: InlineCompletionRating,
input_events: Arc<str>,
input_excerpt: Arc<str>,
output_excerpt: Arc<str>,
feedback: String,
) {
let event = Event::InlineCompletionRating(InlineCompletionRatingEvent {
rating,
input_events,
input_excerpt,
output_excerpt,
feedback,
});
self.report_event(event);
}
pub fn report_assistant_event(self: &Arc<Self>, event: AssistantEvent) {
self.report_event(Event::Assistant(event));
}
@@ -401,22 +380,6 @@ impl Telemetry {
event
}
pub fn report_setting_event(self: &Arc<Self>, setting: &'static str, value: String) {
let event = Event::Setting(SettingEvent {
setting: setting.to_string(),
value,
});
self.report_event(event)
}
pub fn report_extension_event(self: &Arc<Self>, extension_id: Arc<str>, version: Arc<str>) {
self.report_event(Event::Extension(ExtensionEvent {
extension_id,
version,
}))
}
pub fn log_edit_event(self: &Arc<Self>, environment: &'static str, is_via_ssh: bool) {
let mut state = self.state.lock();
let period_data = state.event_coalescer.log_event(environment);
@@ -436,15 +399,6 @@ impl Telemetry {
}
}
pub fn report_action_event(self: &Arc<Self>, source: &'static str, action: String) {
let event = Event::Action(ActionEvent {
source: source.to_string(),
action,
});
self.report_event(event)
}
pub fn report_discovered_project_events(
self: &Arc<Self>,
worktree_id: WorktreeId,
@@ -491,21 +445,6 @@ impl Telemetry {
}
}
pub fn report_repl_event(
self: &Arc<Self>,
kernel_language: String,
kernel_status: String,
repl_session_id: String,
) {
let event = Event::Repl(ReplEvent {
kernel_language,
kernel_status,
repl_session_id,
});
self.report_event(event)
}
fn report_event(self: &Arc<Self>, event: Event) {
let mut state = self.state.lock();

View File

@@ -16,7 +16,9 @@ use util::TryFutureExt as _;
pub type UserId = u64;
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]
#[derive(
Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, serde::Serialize, serde::Deserialize,
)]
pub struct ChannelId(pub u64);
impl std::fmt::Display for ChannelId {

View File

@@ -8,7 +8,12 @@ It contains our back-end logic for collaboration, to which we connect from the Z
## Database setup
Before you can run the collab server locally, you'll need to set up a zed Postgres database.
Before you can run the collab server locally, you'll need to set up a zed Postgres database. Follow the steps sequentially:
1. Ensure you have postgres installed. If not, install with `brew install postgresql@15`.
2. Follow the steps on Brew's formula and verify your `$PATH` contains `/opt/homebrew/opt/postgresql@15/bin`.
3. If you hadn't done it before, create the `postgres` user with `createuser -s postgres`.
4. You are now ready to run the `bootstrap` script:
```sh
script/bootstrap

View File

@@ -252,7 +252,7 @@ spec:
value: "${AUTO_JOIN_CHANNEL_ID}"
securityContext:
capabilities:
# FIXME - Switch to the more restrictive `PERFMON` capability.
# TODO - Switch to the more restrictive `PERFMON` capability.
# This capability isn't yet available in a stable version of Debian.
add: ["SYS_ADMIN"]
terminationGracePeriodSeconds: 10

View File

@@ -279,6 +279,7 @@ pub async fn post_panic(
let report: telemetry_events::PanicRequest = serde_json::from_slice(&body)
.map_err(|_| Error::http(StatusCode::BAD_REQUEST, "invalid json".into()))?;
let incident_id = uuid::Uuid::new_v4().to_string();
let panic = report.panic;
if panic.os_name == "Linux" && panic.os_version == Some("1.0.0".to_string()) {
@@ -288,11 +289,37 @@ pub async fn post_panic(
))?;
}
if let Some(blob_store_client) = app.blob_store_client.as_ref() {
let response = blob_store_client
.head_object()
.bucket(CRASH_REPORTS_BUCKET)
.key(incident_id.clone() + ".json")
.send()
.await;
if response.is_ok() {
log::info!("We've already uploaded this crash");
return Ok(());
}
blob_store_client
.put_object()
.bucket(CRASH_REPORTS_BUCKET)
.key(incident_id.clone() + ".json")
.acl(aws_sdk_s3::types::ObjectCannedAcl::PublicRead)
.body(ByteStream::from(body.to_vec()))
.send()
.await
.map_err(|e| log::error!("Failed to upload crash: {}", e))
.ok();
}
tracing::error!(
service = "client",
version = %panic.app_version,
os_name = %panic.os_name,
os_version = %panic.os_version.clone().unwrap_or_default(),
incident_id = %incident_id,
installation_id = %panic.installation_id.clone().unwrap_or_default(),
description = %panic.payload,
backtrace = %panic.backtrace.join("\n"),
@@ -331,10 +358,19 @@ pub async fn post_panic(
panic.app_version
)))
.add_field({
let hostname = app.config.blob_store_url.clone().unwrap_or_default();
let hostname = hostname.strip_prefix("https://").unwrap_or_else(|| {
hostname.strip_prefix("http://").unwrap_or_default()
});
slack::Text::markdown(format!(
"*OS:*\n{} {}",
"*{} {}:*\n<https://{}.{}/{}.json|{}…>",
panic.os_name,
panic.os_version.unwrap_or_default()
panic.os_version.unwrap_or_default(),
CRASH_REPORTS_BUCKET,
hostname,
incident_id,
incident_id.chars().take(8).collect::<String>(),
))
})
})
@@ -361,6 +397,12 @@ pub async fn post_panic(
}
fn report_to_slack(panic: &Panic) -> bool {
// Panics on macOS should make their way to Slack as a crash report,
// so we don't need to send them a second time via this channel.
if panic.os_name == "macOS" {
return false;
}
if panic.payload.contains("ERROR_SURFACE_LOST_KHR") {
return false;
}
@@ -610,6 +652,10 @@ fn for_snowflake(
"Kernel Status Changed".to_string(),
serde_json::to_value(e).unwrap(),
),
Event::Flexible(e) => (
e.event_type.clone(),
serde_json::to_value(&e.event_properties).unwrap(),
),
};
if let serde_json::Value::Object(ref mut map) = event_properties {

View File

@@ -2,7 +2,11 @@ use collab::env::get_dotenv_vars;
fn main() -> anyhow::Result<()> {
for (key, value) in get_dotenv_vars(".")? {
println!("export {}=\"{}\"", key, value);
if option_env!("POWERSHELL").is_some() {
println!("$Env:{} = \"{}\"", key, value);
} else {
println!("export {}=\"{}\"", key, value);
}
}
Ok(())
}

View File

@@ -6,6 +6,7 @@ use axum::{
routing::get,
Extension, Router,
};
use collab::api::billing::sync_llm_usage_with_stripe_periodically;
use collab::api::CloudflareIpCountryHeader;
use collab::llm::{db::LlmDatabase, log_usage_periodically};

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::{BackgroundExecutor, Model, ModelContext, TestAppContext, Window};
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(|window, cx| ChannelView::open(channel_id, None, workspace_a.clone(), window, cx))
.await
.unwrap();
let channel_view_b = cx_b
.update(|cx| ChannelView::open(channel_id, None, workspace_b.clone(), cx))
.update(|window, cx| ChannelView::open(channel_id, None, workspace_b.clone(), window, cx))
.await
.unwrap();
let channel_view_c = cx_c
.update(|cx| ChannelView::open(channel_id, None, workspace_c.clone(), cx))
.update(|window, cx| ChannelView::open(channel_id, None, workspace_c.clone(), window, cx))
.await
.unwrap();
// Clients A, B, and C all insert and select some text
channel_view_a.update(cx_a, |notes, cx| {
channel_view_a.update_in(cx_a, |notes, window, cx| {
notes.editor.update(cx, |editor, cx| {
editor.insert("a", cx);
editor.change_selections(None, cx, |selections| {
editor.insert("a", window, cx);
editor.change_selections(None, window, cx, |selections| {
selections.select_ranges(vec![0..1]);
});
});
});
executor.run_until_parked();
channel_view_b.update(cx_b, |notes, cx| {
channel_view_b.update_in(cx_b, |notes, window, cx| {
notes.editor.update(cx, |editor, cx| {
editor.move_down(&Default::default(), cx);
editor.insert("b", cx);
editor.change_selections(None, cx, |selections| {
editor.move_down(&Default::default(), window, cx);
editor.insert("b", window, cx);
editor.change_selections(None, window, cx, |selections| {
selections.select_ranges(vec![1..2]);
});
});
});
executor.run_until_parked();
channel_view_c.update(cx_c, |notes, cx| {
channel_view_c.update_in(cx_c, |notes, window, cx| {
notes.editor.update(cx, |editor, cx| {
editor.move_down(&Default::default(), cx);
editor.insert("c", cx);
editor.change_selections(None, cx, |selections| {
editor.move_down(&Default::default(), window, cx);
editor.insert("c", window, cx);
editor.change_selections(None, window, cx, |selections| {
selections.select_ranges(vec![2..3]);
});
});
@@ -206,9 +206,9 @@ async fn test_channel_notes_participant_indices(
// Client A sees clients B and C without assigned colors, because they aren't
// in a call together.
executor.run_until_parked();
channel_view_a.update(cx_a, |notes, cx| {
channel_view_a.update_in(cx_a, |notes, window, cx| {
notes.editor.update(cx, |editor, cx| {
assert_remote_selections(editor, &[(None, 1..2), (None, 2..3)], cx);
assert_remote_selections(editor, &[(None, 1..2), (None, 2..3)], window, cx);
});
});
@@ -222,20 +222,22 @@ async fn test_channel_notes_participant_indices(
// Clients A and B see each other with two different assigned colors. Client C
// still doesn't have a color.
executor.run_until_parked();
channel_view_a.update(cx_a, |notes, cx| {
channel_view_a.update_in(cx_a, |notes, window, cx| {
notes.editor.update(cx, |editor, cx| {
assert_remote_selections(
editor,
&[(Some(ParticipantIndex(1)), 1..2), (None, 2..3)],
window,
cx,
);
});
});
channel_view_b.update(cx_b, |notes, cx| {
channel_view_b.update_in(cx_b, |notes, window, cx| {
notes.editor.update(cx, |editor, cx| {
assert_remote_selections(
editor,
&[(Some(ParticipantIndex(0)), 0..1), (None, 2..3)],
window,
cx,
);
});
@@ -252,8 +254,8 @@ async fn test_channel_notes_participant_indices(
// Clients A and B open the same file.
executor.start_waiting();
let editor_a = workspace_a
.update(cx_a, |workspace, cx| {
workspace.open_path((worktree_id_a, "file.txt"), None, true, cx)
.update_in(cx_a, |workspace, window, cx| {
workspace.open_path((worktree_id_a, "file.txt"), None, true, window, cx)
})
.await
.unwrap()
@@ -261,32 +263,32 @@ async fn test_channel_notes_participant_indices(
.unwrap();
executor.start_waiting();
let editor_b = workspace_b
.update(cx_b, |workspace, cx| {
workspace.open_path((worktree_id_a, "file.txt"), None, true, cx)
.update_in(cx_b, |workspace, window, cx| {
workspace.open_path((worktree_id_a, "file.txt"), None, true, window, cx)
})
.await
.unwrap()
.downcast::<Editor>()
.unwrap();
editor_a.update(cx_a, |editor, cx| {
editor.change_selections(None, cx, |selections| {
editor_a.update_in(cx_a, |editor, window, cx| {
editor.change_selections(None, window, cx, |selections| {
selections.select_ranges(vec![0..1]);
});
});
editor_b.update(cx_b, |editor, cx| {
editor.change_selections(None, cx, |selections| {
editor_b.update_in(cx_b, |editor, window, cx| {
editor.change_selections(None, window, cx, |selections| {
selections.select_ranges(vec![2..3]);
});
});
executor.run_until_parked();
// 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);
editor_a.update_in(cx_a, |editor, window, cx| {
assert_remote_selections(editor, &[(Some(ParticipantIndex(1)), 2..3)], window, cx);
});
editor_b.update(cx_b, |editor, cx| {
assert_remote_selections(editor, &[(Some(ParticipantIndex(0)), 0..1)], cx);
editor_b.update_in(cx_b, |editor, window, cx| {
assert_remote_selections(editor, &[(Some(ParticipantIndex(0)), 0..1)], window, 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>,
window: &mut Window,
cx: &mut ModelContext<Editor>,
) {
let snapshot = editor.snapshot(cx);
let snapshot = editor.snapshot(window, cx);
let range = Anchor::min()..Anchor::max();
let remote_selections = snapshot
.remote_selections_in_range(&range, editor.collaboration_hub().unwrap(), cx)
@@ -641,9 +644,9 @@ async fn test_channel_buffer_changes(
});
// Closing the buffer should re-enable change tracking
cx_b.update(|cx| {
cx_b.update(|window, cx| {
workspace_b.update(cx, |workspace, cx| {
workspace.close_all_items_and_panes(&Default::default(), cx)
workspace.close_all_items_and_panes(&Default::default(), window, cx)
});
});
deterministic.run_until_parked();

View File

@@ -107,7 +107,7 @@ 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)));
cx_b.update(|cx_b| {
cx_b.update(|_, cx_b| {
assert!(room_b.read_with(cx_b, |room, cx| !room.can_use_microphone(cx)));
});
assert!(room_b
@@ -135,7 +135,7 @@ 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.
cx_b.update(|cx_b| {
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()));

View File

@@ -356,10 +356,10 @@ async fn test_channel_message_changes(
let project_b = client_b.build_empty_local_project(cx_b);
let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
let chat_panel_b = workspace_b.update(cx_b, ChatPanel::new);
let chat_panel_b = workspace_b.update_in(cx_b, ChatPanel::new);
chat_panel_b
.update(cx_b, |chat_panel, cx| {
chat_panel.set_active(true, cx);
.update_in(cx_b, |chat_panel, window, cx| {
chat_panel.set_active(true, window, cx);
chat_panel.select_channel(channel_id, None, cx)
})
.await
@@ -367,7 +367,7 @@ async fn test_channel_message_changes(
executor.run_until_parked();
let b_has_messages = cx_b.update(|cx| {
let b_has_messages = cx_b.update(|_, cx| {
client_b
.channel_store()
.read(cx)
@@ -384,7 +384,7 @@ async fn test_channel_message_changes(
executor.run_until_parked();
let b_has_messages = cx_b.update(|cx| {
let b_has_messages = cx_b.update(|_, cx| {
client_b
.channel_store()
.read(cx)
@@ -394,8 +394,8 @@ async fn test_channel_message_changes(
assert!(!b_has_messages);
// Sending a message while the chat is closed should change the flag.
chat_panel_b.update(cx_b, |chat_panel, cx| {
chat_panel.set_active(false, cx);
chat_panel_b.update_in(cx_b, |chat_panel, window, cx| {
chat_panel.set_active(false, window, cx);
});
// Sending a message while the chat is open should not change the flag.
@@ -406,7 +406,7 @@ async fn test_channel_message_changes(
executor.run_until_parked();
let b_has_messages = cx_b.update(|cx| {
let b_has_messages = cx_b.update(|_, cx| {
client_b
.channel_store()
.read(cx)
@@ -416,7 +416,7 @@ async fn test_channel_message_changes(
assert!(b_has_messages);
// Closing the chat should re-enable change tracking
cx_b.update(|_| drop(chat_panel_b));
cx_b.update(|_, _| drop(chat_panel_b));
channel_chat_a
.update(cx_a, |c, cx| c.send_message("four".into(), cx).unwrap())
@@ -425,7 +425,7 @@ async fn test_channel_message_changes(
executor.run_until_parked();
let b_has_messages = cx_b.update(|cx| {
let b_has_messages = cx_b.update(|_, cx| {
client_b
.channel_store()
.read(cx)

View File

@@ -82,14 +82,21 @@ 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(|window, cx| {
Workspace::new(
None,
project_b.clone(),
client_b.app_state.clone(),
window,
cx,
)
});
let cx_b = &mut VisualTestContext::from_window(*workspace_b, cx_b);
let workspace_b_view = workspace_b.root_view(cx_b).unwrap();
let workspace_b_view = workspace_b.root_model(cx_b).unwrap();
let editor_b = workspace_b
.update(cx_b, |workspace, cx| {
workspace.open_path((worktree_id, "b.txt"), None, true, cx)
.update(cx_b, |workspace, window, cx| {
workspace.open_path((worktree_id, "b.txt"), None, true, window, cx)
})
.unwrap()
.await
@@ -98,10 +105,10 @@ async fn test_host_disconnect(
.unwrap();
//TODO: focus
assert!(cx_b.update_view(&editor_b, |editor, cx| editor.is_focused(cx)));
editor_b.update(cx_b, |editor, cx| editor.insert("X", cx));
assert!(cx_b.update_window_model(&editor_b, |editor, window, _| editor.is_focused(window)));
editor_b.update_in(cx_b, |editor, window, cx| editor.insert("X", window, cx));
cx_b.update(|cx| {
cx_b.update(|_, cx| {
assert!(workspace_b_view.read(cx).is_edited());
});
@@ -121,7 +128,7 @@ async fn test_host_disconnect(
// Ensure client B's edited state is reset and that the whole window is blurred.
workspace_b
.update(cx_b, |workspace, cx| {
.update(cx_b, |workspace, _, cx| {
assert!(workspace.active_modal::<DisconnectedOverlay>(cx).is_some());
assert!(!workspace.is_edited());
})
@@ -129,8 +136,8 @@ async fn test_host_disconnect(
// Ensure client B is not prompted to save edits when closing window after disconnecting.
let can_close = workspace_b
.update(cx_b, |workspace, cx| {
workspace.prepare_to_close(CloseIntent::Quit, cx)
.update(cx_b, |workspace, window, cx| {
workspace.prepare_to_close(CloseIntent::Quit, window, cx)
})
.unwrap()
.await
@@ -201,11 +208,12 @@ 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_window_model(|window, cx| Editor::for_buffer(buffer_a, Some(project_a), window, cx));
let mut editor_cx_a = EditorTestContext {
cx: cx_a.clone(),
window: cx_a.handle(),
window: cx_a.window_handle(),
editor: editor_a,
assertion_cx: AssertionContextManager::new(),
};
@@ -216,11 +224,12 @@ 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_window_model(|window, cx| Editor::for_buffer(buffer_b, Some(project_b), window, cx));
let mut editor_cx_b = EditorTestContext {
cx: cx_b.clone(),
window: cx_b.handle(),
window: cx_b.window_handle(),
editor: editor_b,
assertion_cx: AssertionContextManager::new(),
};
@@ -232,8 +241,9 @@ async fn test_newline_above_or_below_does_not_move_guest_cursor(
editor_cx_b.set_selections_state(indoc! {"
Some textˇ
"});
editor_cx_a
.update_editor(|editor, cx| editor.newline_above(&editor::actions::NewlineAbove, cx));
editor_cx_a.update_editor(|editor, window, cx| {
editor.newline_above(&editor::actions::NewlineAbove, window, cx)
});
executor.run_until_parked();
editor_cx_a.assert_editor_state(indoc! {"
ˇ
@@ -253,8 +263,9 @@ async fn test_newline_above_or_below_does_not_move_guest_cursor(
Some textˇ
"});
editor_cx_a
.update_editor(|editor, cx| editor.newline_below(&editor::actions::NewlineBelow, cx));
editor_cx_a.update_editor(|editor, window, cx| {
editor.newline_below(&editor::actions::NewlineBelow, window, cx)
});
executor.run_until_parked();
editor_cx_a.assert_editor_state(indoc! {"
@@ -317,8 +328,9 @@ 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_window_model(|window, cx| {
Editor::for_buffer(buffer_b.clone(), Some(project_b.clone()), window, cx)
});
let fake_language_server = fake_language_servers.next().await.unwrap();
cx_a.background_executor.run_until_parked();
@@ -328,11 +340,11 @@ 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.handle_input(".", cx);
editor_b.update_in(cx_b, |editor, window, cx| {
editor.change_selections(None, window, cx, |s| s.select_ranges([13..13]));
editor.handle_input(".", window, cx);
});
cx_b.focus_view(&editor_b);
cx_b.focus(&editor_b);
// Receive a completion request as the host's language server.
// Return some completions from the host's language server.
@@ -394,9 +406,9 @@ async fn test_collaborating_with_completion(cx_a: &mut TestAppContext, cx_b: &mu
});
// Confirm a completion on the guest.
editor_b.update(cx_b, |editor, cx| {
editor_b.update_in(cx_b, |editor, window, cx| {
assert!(editor.context_menu_visible());
editor.confirm_completion(&ConfirmCompletion { item_ix: Some(0) }, cx);
editor.confirm_completion(&ConfirmCompletion { item_ix: Some(0) }, window, cx);
assert_eq!(editor.text(cx), "fn main() { a.first_method() }");
});
@@ -441,10 +453,10 @@ 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.handle_input("; a", cx);
editor.handle_input(".", cx);
editor_b.update_in(cx_b, |editor, window, cx| {
editor.change_selections(None, window, cx, |s| s.select_ranges([46..46]));
editor.handle_input("; a", window, cx);
editor.handle_input(".", window, cx);
});
buffer_b.read_with(cx_b, |buffer, _| {
@@ -508,18 +520,18 @@ async fn test_collaborating_with_completion(cx_a: &mut TestAppContext, cx_b: &mu
completion_response.next().await.unwrap();
editor_b.update(cx_b, |editor, cx| {
editor_b.update_in(cx_b, |editor, window, cx| {
assert!(editor.context_menu_visible());
editor.context_menu_first(&ContextMenuFirst {}, cx);
editor.context_menu_first(&ContextMenuFirst {}, window, cx);
});
resolve_completion_response.next().await.unwrap();
cx_b.executor().run_until_parked();
// When accepting the completion, the snippet is insert.
editor_b.update(cx_b, |editor, cx| {
editor_b.update_in(cx_b, |editor, window, cx| {
assert!(editor.context_menu_visible());
editor.confirm_completion(&ConfirmCompletion { item_ix: Some(0) }, cx);
editor.confirm_completion(&ConfirmCompletion { item_ix: Some(0) }, window, cx);
assert_eq!(
editor.text(cx),
"use d::SomeTrait;\nfn main() { a.first_method(); a.third_method(, , ) }"
@@ -569,8 +581,8 @@ async fn test_collaborating_with_code_actions(
let project_b = client_b.join_remote_project(project_id, cx_b).await;
let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
let editor_b = workspace_b
.update(cx_b, |workspace, cx| {
workspace.open_path((worktree_id, "main.rs"), None, true, cx)
.update_in(cx_b, |workspace, window, cx| {
workspace.open_path((worktree_id, "main.rs"), None, true, window, cx)
})
.await
.unwrap()
@@ -593,12 +605,12 @@ async fn test_collaborating_with_code_actions(
requests.next().await;
// Move cursor to a location that contains code actions.
editor_b.update(cx_b, |editor, cx| {
editor.change_selections(None, cx, |s| {
editor_b.update_in(cx_b, |editor, window, cx| {
editor.change_selections(None, window, cx, |s| {
s.select_ranges([Point::new(1, 31)..Point::new(1, 31)])
});
});
cx_b.focus_view(&editor_b);
cx_b.focus(&editor_b);
let mut requests = fake_language_server
.handle_request::<lsp::request::CodeActionRequest, _, _>(|params, _| async move {
@@ -658,11 +670,12 @@ async fn test_collaborating_with_code_actions(
requests.next().await;
// Toggle code actions and wait for them to display.
editor_b.update(cx_b, |editor, cx| {
editor_b.update_in(cx_b, |editor, window, cx| {
editor.toggle_code_actions(
&ToggleCodeActions {
deployed_from_indicator: None,
},
window,
cx,
);
});
@@ -674,8 +687,8 @@ 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)
.update_in(cx_b, |editor, window, cx| {
Editor::confirm_code_action(editor, &ConfirmCodeAction { item_ix: Some(0) }, window, cx)
})
.unwrap();
fake_language_server.handle_request::<lsp::request::CodeActionResolveRequest, _, _>(
@@ -726,14 +739,14 @@ async fn test_collaborating_with_code_actions(
.downcast::<Editor>()
.unwrap()
});
code_action_editor.update(cx_b, |editor, cx| {
code_action_editor.update_in(cx_b, |editor, window, cx| {
assert_eq!(editor.text(cx), "mod other;\nfn main() { let foo = 4; }\n");
editor.undo(&Undo, cx);
editor.undo(&Undo, window, cx);
assert_eq!(
editor.text(cx),
"mod other;\nfn main() { let foo = other::foo(); }\npub fn foo() -> usize { 4 }"
);
editor.redo(&Redo, cx);
editor.redo(&Redo, window, cx);
assert_eq!(editor.text(cx), "mod other;\nfn main() { let foo = 4; }\n");
});
}
@@ -785,8 +798,8 @@ async fn test_collaborating_with_renames(cx_a: &mut TestAppContext, cx_b: &mut T
let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
let editor_b = workspace_b
.update(cx_b, |workspace, cx| {
workspace.open_path((worktree_id, "one.rs"), None, true, cx)
.update_in(cx_b, |workspace, window, cx| {
workspace.open_path((worktree_id, "one.rs"), None, true, window, cx)
})
.await
.unwrap()
@@ -795,9 +808,9 @@ async fn test_collaborating_with_renames(cx_a: &mut TestAppContext, cx_b: &mut T
let fake_language_server = fake_language_servers.next().await.unwrap();
// 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.rename(&Rename, cx).unwrap()
let prepare_rename = editor_b.update_in(cx_b, |editor, window, cx| {
editor.change_selections(None, window, cx, |s| s.select_ranges([7..7]));
editor.rename(&Rename, window, cx).unwrap()
});
fake_language_server
@@ -835,12 +848,12 @@ async fn test_collaborating_with_renames(cx_a: &mut TestAppContext, cx_b: &mut T
});
// Cancel the rename, and repeat the same, but use selections instead of cursor movement
editor_b.update(cx_b, |editor, cx| {
editor.cancel(&editor::actions::Cancel, cx);
editor_b.update_in(cx_b, |editor, window, cx| {
editor.cancel(&editor::actions::Cancel, window, cx);
});
let prepare_rename = editor_b.update(cx_b, |editor, cx| {
editor.change_selections(None, cx, |s| s.select_ranges([7..8]));
editor.rename(&Rename, cx).unwrap()
let prepare_rename = editor_b.update_in(cx_b, |editor, window, cx| {
editor.change_selections(None, window, cx, |s| s.select_ranges([7..8]));
editor.rename(&Rename, window, cx).unwrap()
});
fake_language_server
@@ -876,8 +889,8 @@ async fn test_collaborating_with_renames(cx_a: &mut TestAppContext, cx_b: &mut T
});
});
let confirm_rename = editor_b.update(cx_b, |editor, cx| {
Editor::confirm_rename(editor, &ConfirmRename, cx).unwrap()
let confirm_rename = editor_b.update_in(cx_b, |editor, window, cx| {
Editor::confirm_rename(editor, &ConfirmRename, window, cx).unwrap()
});
fake_language_server
.handle_request::<lsp::request::Rename, _, _>(|params, _| async move {
@@ -935,17 +948,17 @@ async fn test_collaborating_with_renames(cx_a: &mut TestAppContext, cx_b: &mut T
workspace.active_item_as::<Editor>(cx).unwrap()
});
rename_editor.update(cx_b, |editor, cx| {
rename_editor.update_in(cx_b, |editor, window, cx| {
assert_eq!(
editor.text(cx),
"const THREE: usize = 1;\nconst TWO: usize = one::THREE + one::THREE;"
);
editor.undo(&Undo, cx);
editor.undo(&Undo, window, cx);
assert_eq!(
editor.text(cx),
"const ONE: usize = 1;\nconst TWO: usize = one::ONE + one::ONE;"
);
editor.redo(&Redo, cx);
editor.redo(&Redo, window, cx);
assert_eq!(
editor.text(cx),
"const THREE: usize = 1;\nconst TWO: usize = one::THREE + one::THREE;"
@@ -953,12 +966,12 @@ async fn test_collaborating_with_renames(cx_a: &mut TestAppContext, cx_b: &mut T
});
// Ensure temporary rename edits cannot be undone/redone.
editor_b.update(cx_b, |editor, cx| {
editor.undo(&Undo, cx);
editor_b.update_in(cx_b, |editor, window, cx| {
editor.undo(&Undo, window, cx);
assert_eq!(editor.text(cx), "const ONE: usize = 1;");
editor.undo(&Undo, cx);
editor.undo(&Undo, window, cx);
assert_eq!(editor.text(cx), "const ONE: usize = 1;");
editor.redo(&Redo, cx);
editor.redo(&Redo, window, cx);
assert_eq!(editor.text(cx), "const THREE: usize = 1;");
})
}
@@ -1193,7 +1206,8 @@ 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_window_model(|window, cx| Editor::for_buffer(buffer_b, None, window, cx));
// Client A sees client B's selection
executor.run_until_parked();
@@ -1207,7 +1221,9 @@ async fn test_share_project(
});
// Edit the buffer as client B and see that edit as client A.
editor_b.update(cx_b, |editor, cx| editor.handle_input("ok, ", cx));
editor_b.update_in(cx_b, |editor, window, cx| {
editor.handle_input("ok, ", window, cx)
});
executor.run_until_parked();
buffer_a.read_with(cx_a, |buffer, _| {
@@ -1234,7 +1250,7 @@ async fn test_share_project(
let _project_c = client_c.join_remote_project(initial_project.id, cx_c).await;
// Client B closes the editor, and client A sees client B's selections removed.
cx_b.update(move |_| drop(editor_b));
cx_b.update(move |_, _| drop(editor_b));
executor.run_until_parked();
buffer_a.read_with(cx_a, |buffer, _| {
@@ -1298,7 +1314,9 @@ 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_window_model(|window, cx| {
Editor::for_buffer(buffer_a, Some(project_a.clone()), window, cx)
});
let fake_language_server = fake_language_servers.next().await.unwrap();
executor.run_until_parked();
@@ -1330,10 +1348,10 @@ async fn test_on_input_format_from_host_to_guest(
.unwrap();
// 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.handle_input(">", cx);
cx_a.focus(&editor_a);
editor_a.update_in(cx_a, |editor, window, cx| {
editor.change_selections(None, window, cx, |s| s.select_ranges([13..13]));
editor.handle_input(">", window, cx);
});
executor.run_until_parked();
@@ -1343,9 +1361,9 @@ async fn test_on_input_format_from_host_to_guest(
});
// Undo should remove LSP edits first
editor_a.update(cx_a, |editor, cx| {
editor_a.update_in(cx_a, |editor, window, cx| {
assert_eq!(editor.text(cx), "fn main() { a>~< }");
editor.undo(&Undo, cx);
editor.undo(&Undo, window, cx);
assert_eq!(editor.text(cx), "fn main() { a> }");
});
executor.run_until_parked();
@@ -1354,9 +1372,9 @@ async fn test_on_input_format_from_host_to_guest(
assert_eq!(buffer.text(), "fn main() { a> }")
});
editor_a.update(cx_a, |editor, cx| {
editor_a.update_in(cx_a, |editor, window, cx| {
assert_eq!(editor.text(cx), "fn main() { a> }");
editor.undo(&Undo, cx);
editor.undo(&Undo, window, cx);
assert_eq!(editor.text(cx), "fn main() { a }");
});
executor.run_until_parked();
@@ -1418,16 +1436,18 @@ 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_window_model(|window, cx| {
Editor::for_buffer(buffer_b, Some(project_b.clone()), window, cx)
});
let fake_language_server = fake_language_servers.next().await.unwrap();
executor.run_until_parked();
// 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.handle_input(":", cx);
cx_b.focus(&editor_b);
editor_b.update_in(cx_b, |editor, window, cx| {
editor.change_selections(None, window, cx, |s| s.select_ranges([13..13]));
editor.handle_input(":", window, cx);
});
// Receive an OnTypeFormatting request as the host's language server.
@@ -1466,9 +1486,9 @@ async fn test_on_input_format_from_guest_to_host(
});
// Undo should remove LSP edits first
editor_b.update(cx_b, |editor, cx| {
editor_b.update_in(cx_b, |editor, window, cx| {
assert_eq!(editor.text(cx), "fn main() { a:~: }");
editor.undo(&Undo, cx);
editor.undo(&Undo, window, cx);
assert_eq!(editor.text(cx), "fn main() { a: }");
});
executor.run_until_parked();
@@ -1477,9 +1497,9 @@ async fn test_on_input_format_from_guest_to_host(
assert_eq!(buffer.text(), "fn main() { a: }")
});
editor_b.update(cx_b, |editor, cx| {
editor_b.update_in(cx_b, |editor, window, cx| {
assert_eq!(editor.text(cx), "fn main() { a: }");
editor.undo(&Undo, cx);
editor.undo(&Undo, window, cx);
assert_eq!(editor.text(cx), "fn main() { a }");
});
executor.run_until_parked();
@@ -1590,8 +1610,8 @@ async fn test_mutual_editor_inlay_hint_cache_update(
.await
.unwrap();
let editor_a = workspace_a
.update(cx_a, |workspace, cx| {
workspace.open_path((worktree_id, "main.rs"), None, true, cx)
.update_in(cx_a, |workspace, window, cx| {
workspace.open_path((worktree_id, "main.rs"), None, true, window, cx)
})
.await
.unwrap()
@@ -1646,8 +1666,8 @@ async fn test_mutual_editor_inlay_hint_cache_update(
});
let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
let editor_b = workspace_b
.update(cx_b, |workspace, cx| {
workspace.open_path((worktree_id, "main.rs"), None, true, cx)
.update_in(cx_b, |workspace, window, cx| {
workspace.open_path((worktree_id, "main.rs"), None, true, window, cx)
})
.await
.unwrap()
@@ -1670,11 +1690,11 @@ 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.handle_input(":", cx);
editor_b.update_in(cx_b, |editor, window, cx| {
editor.change_selections(None, window, cx, |s| s.select_ranges([13..13].clone()));
editor.handle_input(":", window, cx);
});
cx_b.focus_view(&editor_b);
cx_b.focus(&editor_b);
executor.run_until_parked();
editor_a.update(cx_a, |editor, _| {
@@ -1695,11 +1715,11 @@ 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.handle_input("a change to increment both buffers' versions", cx);
editor_a.update_in(cx_a, |editor, window, cx| {
editor.change_selections(None, window, cx, |s| s.select_ranges([13..13]));
editor.handle_input("a change to increment both buffers' versions", window, cx);
});
cx_a.focus_view(&editor_a);
cx_a.focus(&editor_a);
executor.run_until_parked();
editor_a.update(cx_a, |editor, _| {
@@ -1848,8 +1868,8 @@ async fn test_inlay_hint_refresh_is_forwarded(
cx_a.background_executor.start_waiting();
let editor_a = workspace_a
.update(cx_a, |workspace, cx| {
workspace.open_path((worktree_id, "main.rs"), None, true, cx)
.update_in(cx_a, |workspace, window, cx| {
workspace.open_path((worktree_id, "main.rs"), None, true, window, cx)
})
.await
.unwrap()
@@ -1857,8 +1877,8 @@ async fn test_inlay_hint_refresh_is_forwarded(
.unwrap();
let editor_b = workspace_b
.update(cx_b, |workspace, cx| {
workspace.open_path((worktree_id, "main.rs"), None, true, cx)
.update_in(cx_b, |workspace, window, cx| {
workspace.open_path((worktree_id, "main.rs"), None, true, window, cx)
})
.await
.unwrap()
@@ -2042,8 +2062,8 @@ async fn test_git_blame_is_forwarded(cx_a: &mut TestAppContext, cx_b: &mut TestA
// Create editor_a
let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a);
let editor_a = workspace_a
.update(cx_a, |workspace, cx| {
workspace.open_path((worktree_id, "file.txt"), None, true, cx)
.update_in(cx_a, |workspace, window, cx| {
workspace.open_path((worktree_id, "file.txt"), None, true, window, cx)
})
.await
.unwrap()
@@ -2054,8 +2074,8 @@ async fn test_git_blame_is_forwarded(cx_a: &mut TestAppContext, cx_b: &mut TestA
let project_b = client_b.join_remote_project(project_id, cx_b).await;
let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
let editor_b = workspace_b
.update(cx_b, |workspace, cx| {
workspace.open_path((worktree_id, "file.txt"), None, true, cx)
.update_in(cx_b, |workspace, window, cx| {
workspace.open_path((worktree_id, "file.txt"), None, true, window, cx)
})
.await
.unwrap()
@@ -2063,9 +2083,9 @@ async fn test_git_blame_is_forwarded(cx_a: &mut TestAppContext, cx_b: &mut TestA
.unwrap();
// client_b now requests git blame for the open buffer
editor_b.update(cx_b, |editor_b, cx| {
editor_b.update_in(cx_b, |editor_b, window, cx| {
assert!(editor_b.blame().is_none());
editor_b.toggle_git_blame(&editor::actions::ToggleGitBlame {}, cx);
editor_b.toggle_git_blame(&editor::actions::ToggleGitBlame {}, window, cx);
});
cx_a.executor().run_until_parked();
@@ -2103,7 +2123,7 @@ async fn test_git_blame_is_forwarded(cx_a: &mut TestAppContext, cx_b: &mut TestA
// editor_b updates the file, which gets sent to client_a, which updates git blame,
// which gets back to client_b.
editor_b.update(cx_b, |editor_b, cx| {
editor_b.update_in(cx_b, |editor_b, _, cx| {
editor_b.edit([(Point::new(0, 3)..Point::new(0, 3), "FOO")], cx);
});
@@ -2130,7 +2150,7 @@ async fn test_git_blame_is_forwarded(cx_a: &mut TestAppContext, cx_b: &mut TestA
});
// Now editor_a also updates the file
editor_a.update(cx_a, |editor_a, cx| {
editor_a.update_in(cx_a, |editor_a, _, cx| {
editor_a.edit([(Point::new(1, 3)..Point::new(1, 3), "FOO")], cx);
});
@@ -2208,19 +2228,21 @@ async fn test_collaborating_with_editorconfig(
.await
.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));
let other_editor_a =
cx_a.new_view(|cx| Editor::for_buffer(other_buffer_a, Some(project_a), cx));
let main_editor_a = cx_a.new_window_model(|window, cx| {
Editor::for_buffer(main_buffer_a, Some(project_a.clone()), window, cx)
});
let other_editor_a = cx_a.new_window_model(|window, cx| {
Editor::for_buffer(other_buffer_a, Some(project_a), window, cx)
});
let mut main_editor_cx_a = EditorTestContext {
cx: cx_a.clone(),
window: cx_a.handle(),
window: cx_a.window_handle(),
editor: main_editor_a,
assertion_cx: AssertionContextManager::new(),
};
let mut other_editor_cx_a = EditorTestContext {
cx: cx_a.clone(),
window: cx_a.handle(),
window: cx_a.window_handle(),
editor: other_editor_a,
assertion_cx: AssertionContextManager::new(),
};
@@ -2240,19 +2262,21 @@ async fn test_collaborating_with_editorconfig(
.await
.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));
let other_editor_b =
cx_b.new_view(|cx| Editor::for_buffer(other_buffer_b, Some(project_b.clone()), cx));
let main_editor_b = cx_b.new_window_model(|window, cx| {
Editor::for_buffer(main_buffer_b, Some(project_b.clone()), window, cx)
});
let other_editor_b = cx_b.new_window_model(|window, cx| {
Editor::for_buffer(other_buffer_b, Some(project_b.clone()), window, cx)
});
let mut main_editor_cx_b = EditorTestContext {
cx: cx_b.clone(),
window: cx_b.handle(),
window: cx_b.window_handle(),
editor: main_editor_b,
assertion_cx: AssertionContextManager::new(),
};
let mut other_editor_cx_b = EditorTestContext {
cx: cx_b.clone(),
window: cx_b.handle(),
window: cx_b.window_handle(),
editor: other_editor_b,
assertion_cx: AssertionContextManager::new(),
};
@@ -2416,12 +2440,12 @@ fn tab_undo_assert(
cx_b.assert_editor_state(expected_initial);
if a_tabs {
cx_a.update_editor(|editor, cx| {
editor.tab(&editor::actions::Tab, cx);
cx_a.update_editor(|editor, window, cx| {
editor.tab(&editor::actions::Tab, window, cx);
});
} else {
cx_b.update_editor(|editor, cx| {
editor.tab(&editor::actions::Tab, cx);
cx_b.update_editor(|editor, window, cx| {
editor.tab(&editor::actions::Tab, window, cx);
});
}
@@ -2432,12 +2456,12 @@ fn tab_undo_assert(
cx_b.assert_editor_state(expected_tabbed);
if a_tabs {
cx_a.update_editor(|editor, cx| {
editor.undo(&editor::actions::Undo, cx);
cx_a.update_editor(|editor, window, cx| {
editor.undo(&editor::actions::Undo, window, cx);
});
} else {
cx_b.update_editor(|editor, cx| {
editor.undo(&editor::actions::Undo, cx);
cx_b.update_editor(|editor, window, cx| {
editor.undo(&editor::actions::Undo, window, cx);
});
}
cx_a.run_until_parked();

View File

@@ -8,8 +8,8 @@ use collab_ui::{
};
use editor::{Editor, ExcerptRange, MultiBuffer};
use gpui::{
point, BackgroundExecutor, BorrowAppContext, Context, Entity, SharedString, TestAppContext,
View, VisualContext, VisualTestContext,
point, BackgroundExecutor, BorrowAppContext, Context, Entity, Model, SharedString,
TestAppContext, VisualTestContext,
};
use language::Capability;
use project::WorktreeSettings;
@@ -77,23 +77,23 @@ async fn test_basic_following(
let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a);
let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
cx_b.update(|cx| {
assert!(cx.is_window_active());
cx_b.update(|window, _| {
assert!(window.is_window_active());
});
// Client A opens some editors.
let pane_a = workspace_a.update(cx_a, |workspace, _| workspace.active_pane().clone());
let editor_a1 = workspace_a
.update(cx_a, |workspace, cx| {
workspace.open_path((worktree_id, "1.txt"), None, true, cx)
.update_in(cx_a, |workspace, window, cx| {
workspace.open_path((worktree_id, "1.txt"), None, true, window, cx)
})
.await
.unwrap()
.downcast::<Editor>()
.unwrap();
let editor_a2 = workspace_a
.update(cx_a, |workspace, cx| {
workspace.open_path((worktree_id, "2.txt"), None, true, cx)
.update_in(cx_a, |workspace, window, cx| {
workspace.open_path((worktree_id, "2.txt"), None, true, window, cx)
})
.await
.unwrap()
@@ -102,8 +102,8 @@ async fn test_basic_following(
// Client B opens an editor.
let editor_b1 = workspace_b
.update(cx_b, |workspace, cx| {
workspace.open_path((worktree_id, "1.txt"), None, true, cx)
.update_in(cx_b, |workspace, window, cx| {
workspace.open_path((worktree_id, "1.txt"), None, true, window, cx)
})
.await
.unwrap()
@@ -116,22 +116,24 @@ async fn test_basic_following(
let peer_id_d = client_d.peer_id().unwrap();
// Client A updates their selections in those editors
editor_a1.update(cx_a, |editor, cx| {
editor.handle_input("a", cx);
editor.handle_input("b", cx);
editor.handle_input("c", cx);
editor.select_left(&Default::default(), cx);
editor_a1.update_in(cx_a, |editor, window, cx| {
editor.handle_input("a", window, cx);
editor.handle_input("b", window, cx);
editor.handle_input("c", window, cx);
editor.select_left(&Default::default(), window, cx);
assert_eq!(editor.selections.ranges(cx), vec![3..2]);
});
editor_a2.update(cx_a, |editor, cx| {
editor.handle_input("d", cx);
editor.handle_input("e", cx);
editor.select_left(&Default::default(), cx);
editor_a2.update_in(cx_a, |editor, window, cx| {
editor.handle_input("d", window, cx);
editor.handle_input("e", window, cx);
editor.select_left(&Default::default(), window, cx);
assert_eq!(editor.selections.ranges(cx), vec![2..1]);
});
// When client B starts following client A, only the active view state is replicated to client B.
workspace_b.update(cx_b, |workspace, cx| workspace.follow(peer_id_a, cx));
workspace_b.update_in(cx_b, |workspace, window, cx| {
workspace.follow(peer_id_a, window, cx)
});
cx_c.executor().run_until_parked();
let editor_b2 = workspace_b.update(cx_b, |workspace, cx| {
@@ -165,7 +167,9 @@ async fn test_basic_following(
drop(project_c);
// Client C also follows client A.
workspace_c.update(cx_c, |workspace, cx| workspace.follow(peer_id_a, cx));
workspace_c.update_in(cx_c, |workspace, window, cx| {
workspace.follow(peer_id_a, window, cx)
});
cx_d.executor().run_until_parked();
let active_call_d = cx_d.read(ActiveCall::global);
@@ -188,8 +192,8 @@ async fn test_basic_following(
}
// Client C unfollows client A.
workspace_c.update(cx_c, |workspace, cx| {
workspace.unfollow(peer_id_a, cx).unwrap();
workspace_c.update_in(cx_c, |workspace, window, cx| {
workspace.unfollow(peer_id_a, window, cx).unwrap();
});
// All clients see that clients B is following client A.
@@ -203,7 +207,9 @@ async fn test_basic_following(
}
// Client C re-follows client A.
workspace_c.update(cx_c, |workspace, cx| workspace.follow(peer_id_a, cx));
workspace_c.update_in(cx_c, |workspace, window, cx| {
workspace.follow(peer_id_a, window, cx)
});
// All clients see that clients B and C are following client A.
cx_c.executor().run_until_parked();
@@ -216,9 +222,13 @@ async fn test_basic_following(
}
// Client D follows client B, then switches to following client C.
workspace_d.update(cx_d, |workspace, cx| workspace.follow(peer_id_b, cx));
workspace_d.update_in(cx_d, |workspace, window, cx| {
workspace.follow(peer_id_b, window, cx)
});
cx_a.executor().run_until_parked();
workspace_d.update(cx_d, |workspace, cx| workspace.follow(peer_id_c, cx));
workspace_d.update_in(cx_d, |workspace, window, cx| {
workspace.follow(peer_id_c, window, cx)
});
// All clients see that D is following C
cx_a.executor().run_until_parked();
@@ -235,8 +245,8 @@ async fn test_basic_following(
// Client C closes the project.
let weak_workspace_c = workspace_c.downgrade();
workspace_c.update(cx_c, |workspace, cx| {
workspace.close_window(&Default::default(), cx);
workspace_c.update_in(cx_c, |workspace, window, cx| {
workspace.close_window(&Default::default(), window, cx);
});
executor.run_until_parked();
// are you sure you want to leave the call?
@@ -260,8 +270,8 @@ async fn test_basic_following(
}
// When client A activates a different editor, client B does so as well.
workspace_a.update(cx_a, |workspace, cx| {
workspace.activate_item(&editor_a1, true, true, cx)
workspace_a.update_in(cx_a, |workspace, window, cx| {
workspace.activate_item(&editor_a1, true, true, window, cx)
});
executor.run_until_parked();
workspace_b.update(cx_b, |workspace, cx| {
@@ -302,11 +312,11 @@ async fn test_basic_following(
);
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 multibuffer_editor_a = workspace_a.update_in(cx_a, |workspace, window, cx| {
let editor = cx.new_model(|cx| {
Editor::for_multibuffer(multibuffer_a, Some(project_a.clone()), true, window, 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, window, cx);
editor
});
executor.run_until_parked();
@@ -324,8 +334,8 @@ async fn test_basic_following(
// When client A navigates back and forth, client B does so as well.
workspace_a
.update(cx_a, |workspace, cx| {
workspace.go_back(workspace.active_pane().downgrade(), cx)
.update_in(cx_a, |workspace, window, cx| {
workspace.go_back(workspace.active_pane().downgrade(), window, cx)
})
.await
.unwrap();
@@ -338,8 +348,8 @@ async fn test_basic_following(
});
workspace_a
.update(cx_a, |workspace, cx| {
workspace.go_back(workspace.active_pane().downgrade(), cx)
.update_in(cx_a, |workspace, window, cx| {
workspace.go_back(workspace.active_pane().downgrade(), window, cx)
})
.await
.unwrap();
@@ -352,8 +362,8 @@ async fn test_basic_following(
});
workspace_a
.update(cx_a, |workspace, cx| {
workspace.go_forward(workspace.active_pane().downgrade(), cx)
.update_in(cx_a, |workspace, window, cx| {
workspace.go_forward(workspace.active_pane().downgrade(), window, cx)
})
.await
.unwrap();
@@ -366,8 +376,8 @@ 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_a1.update_in(cx_a, |editor, window, cx| {
editor.change_selections(None, window, cx, |s| s.select_ranges([1..1, 2..2]));
});
executor.advance_clock(workspace::item::LEADER_UPDATE_THROTTLE);
executor.run_until_parked();
@@ -377,13 +387,15 @@ async fn test_basic_following(
assert_eq!(editor.selections.ranges(cx), &[1..1, 2..2]);
});
editor_a1.update(cx_a, |editor, cx| editor.set_text("TWO", cx));
editor_a1.update_in(cx_a, |editor, window, cx| {
editor.set_text("TWO", window, cx)
});
executor.run_until_parked();
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.set_scroll_position(point(0., 100.), cx);
editor_a1.update_in(cx_a, |editor, window, cx| {
editor.change_selections(None, window, cx, |s| s.select_ranges([3..3]));
editor.set_scroll_position(point(0., 100.), window, cx);
});
executor.advance_clock(workspace::item::LEADER_UPDATE_THROTTLE);
executor.run_until_parked();
@@ -392,11 +404,11 @@ async fn test_basic_following(
});
// After unfollowing, client B stops receiving updates from client A.
workspace_b.update(cx_b, |workspace, cx| {
workspace.unfollow(peer_id_a, cx).unwrap()
workspace_b.update_in(cx_b, |workspace, window, cx| {
workspace.unfollow(peer_id_a, window, cx).unwrap()
});
workspace_a.update(cx_a, |workspace, cx| {
workspace.activate_item(&editor_a2, true, true, cx)
workspace_a.update_in(cx_a, |workspace, window, cx| {
workspace.activate_item(&editor_a2, true, true, window, cx)
});
executor.run_until_parked();
assert_eq!(
@@ -408,14 +420,16 @@ async fn test_basic_following(
);
// Client A starts following client B.
workspace_a.update(cx_a, |workspace, cx| workspace.follow(peer_id_b, cx));
workspace_a.update_in(cx_a, |workspace, window, cx| {
workspace.follow(peer_id_b, window, cx)
});
executor.run_until_parked();
assert_eq!(
workspace_a.update(cx_a, |workspace, _| workspace.leader_for_pane(&pane_a)),
Some(peer_id_b)
);
assert_eq!(
workspace_a.update(cx_a, |workspace, cx| workspace
workspace_a.update_in(cx_a, |workspace, _, cx| workspace
.active_item(cx)
.unwrap()
.item_id()),
@@ -593,19 +607,19 @@ async fn test_following_tab_order(
//Open 1, 3 in that order on client A
workspace_a
.update(cx_a, |workspace, cx| {
workspace.open_path((worktree_id, "1.txt"), None, true, cx)
.update_in(cx_a, |workspace, window, cx| {
workspace.open_path((worktree_id, "1.txt"), None, true, window, cx)
})
.await
.unwrap();
workspace_a
.update(cx_a, |workspace, cx| {
workspace.open_path((worktree_id, "3.txt"), None, true, cx)
.update_in(cx_a, |workspace, window, cx| {
workspace.open_path((worktree_id, "3.txt"), None, true, window, cx)
})
.await
.unwrap();
let pane_paths = |pane: &View<workspace::Pane>, cx: &mut VisualTestContext| {
let pane_paths = |pane: &Model<workspace::Pane>, cx: &mut VisualTestContext| {
pane.update(cx, |pane, cx| {
pane.items()
.map(|item| {
@@ -624,13 +638,15 @@ async fn test_following_tab_order(
assert_eq!(&pane_paths(&pane_a, cx_a), &["1.txt", "3.txt"]);
//Follow client B as client A
workspace_a.update(cx_a, |workspace, cx| workspace.follow(client_b_id, cx));
workspace_a.update_in(cx_a, |workspace, window, cx| {
workspace.follow(client_b_id, window, cx)
});
executor.run_until_parked();
//Open just 2 on client B
workspace_b
.update(cx_b, |workspace, cx| {
workspace.open_path((worktree_id, "2.txt"), None, true, cx)
.update_in(cx_b, |workspace, window, cx| {
workspace.open_path((worktree_id, "2.txt"), None, true, window, cx)
})
.await
.unwrap();
@@ -641,8 +657,8 @@ async fn test_following_tab_order(
//Open just 1 on client B
workspace_b
.update(cx_b, |workspace, cx| {
workspace.open_path((worktree_id, "1.txt"), None, true, cx)
.update_in(cx_b, |workspace, window, cx| {
workspace.open_path((worktree_id, "1.txt"), None, true, window, cx)
})
.await
.unwrap();
@@ -701,8 +717,8 @@ async fn test_peers_following_each_other(cx_a: &mut TestAppContext, cx_b: &mut T
// Client A opens a file.
let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a);
workspace_a
.update(cx_a, |workspace, cx| {
workspace.open_path((worktree_id, "1.txt"), None, true, cx)
.update_in(cx_a, |workspace, window, cx| {
workspace.open_path((worktree_id, "1.txt"), None, true, window, cx)
})
.await
.unwrap()
@@ -712,8 +728,8 @@ async fn test_peers_following_each_other(cx_a: &mut TestAppContext, cx_b: &mut T
// Client B opens a different file.
let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
workspace_b
.update(cx_b, |workspace, cx| {
workspace.open_path((worktree_id, "2.txt"), None, true, cx)
.update_in(cx_b, |workspace, window, cx| {
workspace.open_path((worktree_id, "2.txt"), None, true, window, cx)
})
.await
.unwrap()
@@ -721,24 +737,38 @@ async fn test_peers_following_each_other(cx_a: &mut TestAppContext, cx_b: &mut T
.unwrap();
// Clients A and B follow each other in split panes
workspace_a.update(cx_a, |workspace, cx| {
workspace.split_and_clone(workspace.active_pane().clone(), SplitDirection::Right, cx);
workspace_a.update_in(cx_a, |workspace, window, cx| {
workspace.split_and_clone(
workspace.active_pane().clone(),
SplitDirection::Right,
window,
cx,
);
});
workspace_a.update(cx_a, |workspace, cx| {
workspace.follow(client_b.peer_id().unwrap(), cx)
workspace_a.update_in(cx_a, |workspace, window, cx| {
workspace.follow(client_b.peer_id().unwrap(), window, cx)
});
executor.run_until_parked();
workspace_b.update(cx_b, |workspace, cx| {
workspace.split_and_clone(workspace.active_pane().clone(), SplitDirection::Right, cx);
workspace_b.update_in(cx_b, |workspace, window, cx| {
workspace.split_and_clone(
workspace.active_pane().clone(),
SplitDirection::Right,
window,
cx,
);
});
workspace_b.update(cx_b, |workspace, cx| {
workspace.follow(client_a.peer_id().unwrap(), cx)
workspace_b.update_in(cx_b, |workspace, window, cx| {
workspace.follow(client_a.peer_id().unwrap(), window, cx)
});
executor.run_until_parked();
// Clients A and B return focus to the original files they had open
workspace_a.update(cx_a, |workspace, cx| workspace.activate_next_pane(cx));
workspace_b.update(cx_b, |workspace, cx| workspace.activate_next_pane(cx));
workspace_a.update_in(cx_a, |workspace, window, cx| {
workspace.activate_next_pane(window, cx)
});
workspace_b.update_in(cx_b, |workspace, window, cx| {
workspace.activate_next_pane(window, cx)
});
executor.run_until_parked();
// Both clients see the other client's focused file in their right pane.
@@ -775,15 +805,15 @@ async fn test_peers_following_each_other(cx_a: &mut TestAppContext, cx_b: &mut T
// Clients A and B each open a new file.
workspace_a
.update(cx_a, |workspace, cx| {
workspace.open_path((worktree_id, "3.txt"), None, true, cx)
.update_in(cx_a, |workspace, window, cx| {
workspace.open_path((worktree_id, "3.txt"), None, true, window, cx)
})
.await
.unwrap();
workspace_b
.update(cx_b, |workspace, cx| {
workspace.open_path((worktree_id, "4.txt"), None, true, cx)
.update_in(cx_b, |workspace, window, cx| {
workspace.open_path((worktree_id, "4.txt"), None, true, window, cx)
})
.await
.unwrap();
@@ -831,7 +861,9 @@ async fn test_peers_following_each_other(cx_a: &mut TestAppContext, cx_b: &mut T
);
// Client A focuses their right pane, in which they're following client B.
workspace_a.update(cx_a, |workspace, cx| workspace.activate_next_pane(cx));
workspace_a.update_in(cx_a, |workspace, window, cx| {
workspace.activate_next_pane(window, cx)
});
executor.run_until_parked();
// Client B sees that client A is now looking at the same file as them.
@@ -877,7 +909,9 @@ async fn test_peers_following_each_other(cx_a: &mut TestAppContext, cx_b: &mut T
// Client B focuses their right pane, in which they're following client A,
// who is following them.
workspace_b.update(cx_b, |workspace, cx| workspace.activate_next_pane(cx));
workspace_b.update_in(cx_b, |workspace, window, cx| {
workspace.activate_next_pane(window, cx)
});
executor.run_until_parked();
// Client A sees that client B is now looking at the same file as them.
@@ -923,9 +957,9 @@ 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_b.update_in(cx_b, |workspace, window, cx| {
workspace.active_pane().update(cx, |pane, cx| {
pane.activate_prev_item(true, cx);
pane.activate_prev_item(true, window, cx);
});
});
executor.run_until_parked();
@@ -974,9 +1008,9 @@ 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_b.update_in(cx_b, |workspace, window, cx| {
workspace.active_pane().update(cx, |pane, cx| {
pane.close_inactive_items(&Default::default(), cx)
pane.close_inactive_items(&Default::default(), window, cx)
.unwrap()
.detach();
});
@@ -1022,14 +1056,14 @@ async fn test_peers_following_each_other(cx_a: &mut TestAppContext, cx_b: &mut T
);
// Client B follows client A again.
workspace_b.update(cx_b, |workspace, cx| {
workspace.follow(client_a.peer_id().unwrap(), cx)
workspace_b.update_in(cx_b, |workspace, window, cx| {
workspace.follow(client_a.peer_id().unwrap(), window, cx)
});
executor.run_until_parked();
// Client A cycles through some tabs.
workspace_a.update(cx_a, |workspace, cx| {
workspace_a.update_in(cx_a, |workspace, window, cx| {
workspace.active_pane().update(cx, |pane, cx| {
pane.activate_prev_item(true, cx);
pane.activate_prev_item(true, window, cx);
});
});
executor.run_until_parked();
@@ -1071,9 +1105,9 @@ async fn test_peers_following_each_other(cx_a: &mut TestAppContext, cx_b: &mut T
]
);
workspace_a.update(cx_a, |workspace, cx| {
workspace_a.update_in(cx_a, |workspace, window, cx| {
workspace.active_pane().update(cx, |pane, cx| {
pane.activate_prev_item(true, cx);
pane.activate_prev_item(true, window, cx);
});
});
executor.run_until_parked();
@@ -1118,9 +1152,9 @@ async fn test_peers_following_each_other(cx_a: &mut TestAppContext, cx_b: &mut T
]
);
workspace_a.update(cx_a, |workspace, cx| {
workspace_a.update_in(cx_a, |workspace, window, cx| {
workspace.active_pane().update(cx, |pane, cx| {
pane.activate_prev_item(true, cx);
pane.activate_prev_item(true, window, cx);
});
});
executor.run_until_parked();
@@ -1215,8 +1249,8 @@ async fn test_auto_unfollowing(cx_a: &mut TestAppContext, cx_b: &mut TestAppCont
let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
let _editor_a1 = workspace_a
.update(cx_a, |workspace, cx| {
workspace.open_path((worktree_id, "1.txt"), None, true, cx)
.update_in(cx_a, |workspace, window, cx| {
workspace.open_path((worktree_id, "1.txt"), None, true, window, cx)
})
.await
.unwrap()
@@ -1228,7 +1262,9 @@ async fn test_auto_unfollowing(cx_a: &mut TestAppContext, cx_b: &mut TestAppCont
let leader_id = project_b.update(cx_b, |project, _| {
project.collaborators().values().next().unwrap().peer_id
});
workspace_b.update(cx_b, |workspace, cx| workspace.follow(leader_id, cx));
workspace_b.update_in(cx_b, |workspace, window, cx| {
workspace.follow(leader_id, window, cx)
});
executor.run_until_parked();
assert_eq!(
workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
@@ -1243,15 +1279,17 @@ async fn test_auto_unfollowing(cx_a: &mut TestAppContext, cx_b: &mut TestAppCont
});
// When client B moves, it automatically stops following client A.
editor_b2.update(cx_b, |editor, cx| {
editor.move_right(&editor::actions::MoveRight, cx)
editor_b2.update_in(cx_b, |editor, window, cx| {
editor.move_right(&editor::actions::MoveRight, window, cx)
});
assert_eq!(
workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
None
);
workspace_b.update(cx_b, |workspace, cx| workspace.follow(leader_id, cx));
workspace_b.update_in(cx_b, |workspace, window, cx| {
workspace.follow(leader_id, window, cx)
});
executor.run_until_parked();
assert_eq!(
workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
@@ -1259,13 +1297,15 @@ async fn test_auto_unfollowing(cx_a: &mut TestAppContext, cx_b: &mut TestAppCont
);
// When client B edits, it automatically stops following client A.
editor_b2.update(cx_b, |editor, cx| editor.insert("X", cx));
editor_b2.update_in(cx_b, |editor, window, cx| editor.insert("X", window, cx));
assert_eq!(
workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
workspace_b.update_in(cx_b, |workspace, _, _| workspace.leader_for_pane(&pane_b)),
None
);
workspace_b.update(cx_b, |workspace, cx| workspace.follow(leader_id, cx));
workspace_b.update_in(cx_b, |workspace, window, cx| {
workspace.follow(leader_id, window, cx)
});
executor.run_until_parked();
assert_eq!(
workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
@@ -1273,15 +1313,17 @@ async fn test_auto_unfollowing(cx_a: &mut TestAppContext, cx_b: &mut TestAppCont
);
// When client B scrolls, it automatically stops following client A.
editor_b2.update(cx_b, |editor, cx| {
editor.set_scroll_position(point(0., 3.), cx)
editor_b2.update_in(cx_b, |editor, window, cx| {
editor.set_scroll_position(point(0., 3.), window, cx)
});
assert_eq!(
workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
None
);
workspace_b.update(cx_b, |workspace, cx| workspace.follow(leader_id, cx));
workspace_b.update_in(cx_b, |workspace, window, cx| {
workspace.follow(leader_id, window, cx)
});
executor.run_until_parked();
assert_eq!(
workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
@@ -1289,15 +1331,17 @@ async fn test_auto_unfollowing(cx_a: &mut TestAppContext, cx_b: &mut TestAppCont
);
// When client B activates a different pane, it continues following client A in the original pane.
workspace_b.update(cx_b, |workspace, cx| {
workspace.split_and_clone(pane_b.clone(), SplitDirection::Right, cx)
workspace_b.update_in(cx_b, |workspace, window, cx| {
workspace.split_and_clone(pane_b.clone(), SplitDirection::Right, window, cx)
});
assert_eq!(
workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
Some(leader_id)
);
workspace_b.update(cx_b, |workspace, cx| workspace.activate_next_pane(cx));
workspace_b.update_in(cx_b, |workspace, window, cx| {
workspace.activate_next_pane(window, cx)
});
assert_eq!(
workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
Some(leader_id)
@@ -1305,8 +1349,8 @@ async fn test_auto_unfollowing(cx_a: &mut TestAppContext, cx_b: &mut TestAppCont
// When client B activates a different item in the original pane, it automatically stops following client A.
workspace_b
.update(cx_b, |workspace, cx| {
workspace.open_path((worktree_id, "2.txt"), None, true, cx)
.update_in(cx_b, |workspace, window, cx| {
workspace.open_path((worktree_id, "2.txt"), None, true, window, cx)
})
.await
.unwrap();
@@ -1352,8 +1396,12 @@ async fn test_peers_simultaneously_following_each_other(
project.collaborators().values().next().unwrap().peer_id
});
workspace_a.update(cx_a, |workspace, cx| workspace.follow(client_b_id, cx));
workspace_b.update(cx_b, |workspace, cx| workspace.follow(client_a_id, cx));
workspace_a.update_in(cx_a, |workspace, window, cx| {
workspace.follow(client_b_id, window, cx)
});
workspace_b.update_in(cx_b, |workspace, window, cx| {
workspace.follow(client_a_id, window, cx)
});
executor.run_until_parked();
workspace_a.update(cx_a, |workspace, _| {
@@ -1434,8 +1482,8 @@ async fn test_following_across_workspaces(cx_a: &mut TestAppContext, cx_b: &mut
.unwrap();
workspace_a
.update(cx_a, |workspace, cx| {
workspace.open_path((worktree_id_a, "w.rs"), None, true, cx)
.update_in(cx_a, |workspace, window, cx| {
workspace.open_path((worktree_id_a, "w.rs"), None, true, window, cx)
})
.await
.unwrap();
@@ -1443,8 +1491,8 @@ async fn test_following_across_workspaces(cx_a: &mut TestAppContext, cx_b: &mut
executor.run_until_parked();
assert_eq!(visible_push_notifications(cx_b).len(), 1);
workspace_b.update(cx_b, |workspace, cx| {
workspace.follow(client_a.peer_id().unwrap(), cx)
workspace_b.update_in(cx_b, |workspace, window, cx| {
workspace.follow(client_a.peer_id().unwrap(), window, cx)
});
executor.run_until_parked();
@@ -1490,8 +1538,8 @@ async fn test_following_across_workspaces(cx_a: &mut TestAppContext, cx_b: &mut
// b moves to x.rs in a's project, and a follows
workspace_b_project_a
.update(&mut cx_b2, |workspace, cx| {
workspace.open_path((worktree_id_a, "x.rs"), None, true, cx)
.update_in(&mut cx_b2, |workspace, window, cx| {
workspace.open_path((worktree_id_a, "x.rs"), None, true, window, cx)
})
.await
.unwrap();
@@ -1505,8 +1553,8 @@ async fn test_following_across_workspaces(cx_a: &mut TestAppContext, cx_b: &mut
);
});
workspace_a.update(cx_a, |workspace, cx| {
workspace.follow(client_b.peer_id().unwrap(), cx)
workspace_a.update_in(cx_a, |workspace, window, cx| {
workspace.follow(client_b.peer_id().unwrap(), window, cx)
});
executor.run_until_parked();
@@ -1522,8 +1570,8 @@ async fn test_following_across_workspaces(cx_a: &mut TestAppContext, cx_b: &mut
// b moves to y.rs in b's project, a is still following but can't yet see
workspace_b
.update(cx_b, |workspace, cx| {
workspace.open_path((worktree_id_b, "y.rs"), None, true, cx)
.update_in(cx_b, |workspace, window, cx| {
workspace.open_path((worktree_id_b, "y.rs"), None, true, window, cx)
})
.await
.unwrap();
@@ -1544,7 +1592,7 @@ async fn test_following_across_workspaces(cx_a: &mut TestAppContext, cx_b: &mut
executor.run_until_parked();
assert_eq!(visible_push_notifications(cx_a).len(), 1);
cx_a.update(|cx| {
cx_a.update(|_, cx| {
workspace::join_in_room_project(
project_b_id,
client_b.user_id().unwrap(),
@@ -1607,8 +1655,8 @@ 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_a.update_in(cx_a, |editor, window, cx| {
editor.change_selections(None, window, cx, |s| s.select_ranges([1..1]))
});
cx_a.executor()
.advance_clock(workspace::item::LEADER_UPDATE_THROTTLE);
@@ -1618,7 +1666,7 @@ async fn test_following_stops_on_unshare(cx_a: &mut TestAppContext, cx_b: &mut T
});
// a unshares the project
cx_a.update(|cx| {
cx_a.update(|_, cx| {
let project = workspace_a.read(cx).project().clone();
ActiveCall::global(cx).update(cx, |call, cx| {
call.unshare_project(project, cx).unwrap();
@@ -1627,8 +1675,8 @@ async fn test_following_stops_on_unshare(cx_a: &mut TestAppContext, cx_b: &mut T
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_a.update_in(cx_a, |editor, window, cx| {
editor.change_selections(None, window, cx, |s| s.select_ranges([2..2]))
});
cx_a.executor()
.advance_clock(workspace::item::LEADER_UPDATE_THROTTLE);
@@ -1636,7 +1684,7 @@ async fn test_following_stops_on_unshare(cx_a: &mut TestAppContext, cx_b: &mut T
editor_b.update(cx_b, |editor, cx| {
assert_eq!(editor.selections.ranges(cx), vec![1..1])
});
cx_b.update(|cx| {
cx_b.update(|_, cx| {
let room = ActiveCall::global(cx).read(cx).room().unwrap().read(cx);
let participant = room.remote_participants().get(&client_a.id()).unwrap();
assert_eq!(participant.location, ParticipantLocation::UnsharedProject)
@@ -1703,16 +1751,16 @@ async fn test_following_into_excluded_file(
// Client A opens editors for a regular file and an excluded file.
let editor_for_regular = workspace_a
.update(cx_a, |workspace, cx| {
workspace.open_path((worktree_id, "1.txt"), None, true, cx)
.update_in(cx_a, |workspace, window, cx| {
workspace.open_path((worktree_id, "1.txt"), None, true, window, cx)
})
.await
.unwrap()
.downcast::<Editor>()
.unwrap();
let editor_for_excluded_a = workspace_a
.update(cx_a, |workspace, cx| {
workspace.open_path((worktree_id, ".git/COMMIT_EDITMSG"), None, true, cx)
.update_in(cx_a, |workspace, window, cx| {
workspace.open_path((worktree_id, ".git/COMMIT_EDITMSG"), None, true, window, cx)
})
.await
.unwrap()
@@ -1720,22 +1768,24 @@ async fn test_following_into_excluded_file(
.unwrap();
// Client A updates their selections in those editors
editor_for_regular.update(cx_a, |editor, cx| {
editor.handle_input("a", cx);
editor.handle_input("b", cx);
editor.handle_input("c", cx);
editor.select_left(&Default::default(), cx);
editor_for_regular.update_in(cx_a, |editor, window, cx| {
editor.handle_input("a", window, cx);
editor.handle_input("b", window, cx);
editor.handle_input("c", window, cx);
editor.select_left(&Default::default(), window, cx);
assert_eq!(editor.selections.ranges(cx), vec![3..2]);
});
editor_for_excluded_a.update(cx_a, |editor, cx| {
editor.select_all(&Default::default(), cx);
editor.handle_input("new commit message", cx);
editor.select_left(&Default::default(), cx);
editor_for_excluded_a.update_in(cx_a, |editor, window, cx| {
editor.select_all(&Default::default(), window, cx);
editor.handle_input("new commit message", window, cx);
editor.select_left(&Default::default(), window, cx);
assert_eq!(editor.selections.ranges(cx), vec![18..17]);
});
// When client B starts following client A, currently visible file is replicated
workspace_b.update(cx_b, |workspace, cx| workspace.follow(peer_id_a, cx));
workspace_b.update_in(cx_b, |workspace, window, cx| {
workspace.follow(peer_id_a, window, cx)
});
executor.advance_clock(workspace::item::LEADER_UPDATE_THROTTLE);
executor.run_until_parked();
@@ -1755,15 +1805,15 @@ async fn test_following_into_excluded_file(
vec![18..17]
);
editor_for_excluded_a.update(cx_a, |editor, cx| {
editor.select_right(&Default::default(), cx);
editor_for_excluded_a.update_in(cx_a, |editor, window, cx| {
editor.select_right(&Default::default(), window, cx);
});
executor.advance_clock(workspace::item::LEADER_UPDATE_THROTTLE);
executor.run_until_parked();
// Changes from B to the excluded file are replicated in A's editor
editor_for_excluded_b.update(cx_b, |editor, cx| {
editor.handle_input("\nCo-Authored-By: B <b@b.b>", cx);
editor_for_excluded_b.update_in(cx_b, |editor, window, cx| {
editor.handle_input("\nCo-Authored-By: B <b@b.b>", window, cx);
});
executor.run_until_parked();
editor_for_excluded_a.update(cx_a, |editor, cx| {
@@ -1774,13 +1824,11 @@ async fn test_following_into_excluded_file(
});
}
fn visible_push_notifications(
cx: &mut TestAppContext,
) -> Vec<gpui::View<ProjectSharedNotification>> {
fn visible_push_notifications(cx: &mut TestAppContext) -> Vec<Model<ProjectSharedNotification>> {
let mut ret = Vec::new();
for window in cx.windows() {
window
.update(cx, |window, _| {
.update(cx, |window, _, _| {
if let Ok(handle) = window.downcast::<ProjectSharedNotification>() {
ret.push(handle)
}
@@ -1821,7 +1869,7 @@ fn followers_by_leader(project_id: u64, cx: &TestAppContext) -> Vec<(PeerId, Vec
})
}
fn pane_summaries(workspace: &View<Workspace>, cx: &mut VisualTestContext) -> Vec<PaneSummary> {
fn pane_summaries(workspace: &Model<Workspace>, cx: &mut VisualTestContext) -> Vec<PaneSummary> {
workspace.update(cx, |workspace, cx| {
let active_pane = workspace.active_pane();
workspace
@@ -1924,14 +1972,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(|window, cx| ChannelView::open(channel_1_id, None, workspace_a.clone(), window, cx))
.await
.unwrap();
channel_notes_1_a.update(cx_a, |notes, cx| {
channel_notes_1_a.update_in(cx_a, |notes, window, cx| {
assert_eq!(notes.channel(cx).unwrap().name, "channel-1");
notes.editor.update(cx, |editor, cx| {
editor.insert("Hello from A.", cx);
editor.change_selections(None, cx, |selections| {
editor.insert("Hello from A.", window, cx);
editor.change_selections(None, window, cx, |selections| {
selections.select_ranges(vec![3..4]);
});
});
@@ -1939,9 +1987,9 @@ async fn test_following_to_channel_notes_without_a_shared_project(
// Client B follows client A.
workspace_b
.update(cx_b, |workspace, cx| {
.update_in(cx_b, |workspace, window, cx| {
workspace
.start_following(client_a.peer_id().unwrap(), cx)
.start_following(client_a.peer_id().unwrap(), window, cx)
.unwrap()
})
.await
@@ -1971,7 +2019,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(|window, cx| ChannelView::open(channel_2_id, None, workspace_a.clone(), window, cx))
.await
.unwrap();
channel_notes_2_a.update(cx_a, |notes, cx| {
@@ -1997,8 +2045,8 @@ async fn test_following_to_channel_notes_without_a_shared_project(
// Client A opens a local buffer in their unshared project.
let _unshared_editor_a1 = workspace_a
.update(cx_a, |workspace, cx| {
workspace.open_path((worktree_id, "1.txt"), None, true, cx)
.update_in(cx_a, |workspace, window, cx| {
workspace.open_path((worktree_id, "1.txt"), None, true, window, cx)
})
.await
.unwrap()
@@ -2027,7 +2075,7 @@ 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());
@@ -2069,9 +2117,9 @@ async fn test_following_to_channel_notes_other_workspace(
// a opens a second workspace and the channel notes
let (workspace_a2, cx_a2) = client_a.build_test_workspace(&mut cx_a2).await;
cx_a2.update(|cx| cx.activate_window());
cx_a2.update(|window, _| window.activate_window());
cx_a2
.update(|cx| ChannelView::open(channel, None, workspace_a2, cx))
.update(|window, cx| ChannelView::open(channel, None, workspace_a2, window, cx))
.await
.unwrap();
cx_a2.run_until_parked();
@@ -2083,7 +2131,7 @@ async fn test_following_to_channel_notes_other_workspace(
});
// a returns to the shared project
cx_a.update(|cx| cx.activate_window());
cx_a.update(|window, _| window.activate_window());
cx_a.run_until_parked();
workspace_a.update(cx_a, |workspace, cx| {
@@ -2141,7 +2189,7 @@ async fn test_following_while_deactivated(cx_a: &mut TestAppContext, cx_b: &mut
// a opens a file in a new window
let (_, cx_a2) = client_a.build_test_workspace(&mut cx_a2).await;
cx_a2.update(|cx| cx.activate_window());
cx_a2.update(|window, _| window.activate_window());
cx_a2.simulate_keystrokes("cmd-p");
cx_a2.run_until_parked();
cx_a2.simulate_keystrokes("3 enter");
@@ -2152,7 +2200,7 @@ async fn test_following_while_deactivated(cx_a: &mut TestAppContext, cx_b: &mut
cx_a.run_until_parked();
// a returns to the shared project
cx_a.update(|cx| cx.activate_window());
cx_a.update(|window, _| window.activate_window());
cx_a.run_until_parked();
workspace_a.update(cx_a, |workspace, cx| {

View File

@@ -6174,7 +6174,7 @@ async fn test_right_click_menu_behind_collab_panel(cx: &mut TestAppContext) {
cx.simulate_resize(size(px(300.), px(300.)));
cx.simulate_keystrokes("cmd-n cmd-n cmd-n");
cx.update(|cx| cx.refresh());
cx.update(|window, _cx| window.refresh());
let tab_bounds = cx.debug_bounds("TAB-2").unwrap();
let new_tab_button_bounds = cx.debug_bounds("ICON-Plus").unwrap();
@@ -6273,8 +6273,8 @@ async fn test_preview_tabs(cx: &mut TestAppContext) {
// Opening item 3 as a "permanent" tab
workspace
.update(cx, |workspace, cx| {
workspace.open_path(path_3.clone(), None, false, cx)
.update_in(cx, |workspace, window, cx| {
workspace.open_path(path_3.clone(), None, false, window, cx)
})
.await
.unwrap();
@@ -6290,8 +6290,8 @@ async fn test_preview_tabs(cx: &mut TestAppContext) {
// Open item 1 as preview
workspace
.update(cx, |workspace, cx| {
workspace.open_path_preview(path_1.clone(), None, true, true, cx)
.update_in(cx, |workspace, window, cx| {
workspace.open_path_preview(path_1.clone(), None, true, true, window, cx)
})
.await
.unwrap();
@@ -6311,8 +6311,8 @@ async fn test_preview_tabs(cx: &mut TestAppContext) {
// Open item 2 as preview
workspace
.update(cx, |workspace, cx| {
workspace.open_path_preview(path_2.clone(), None, true, true, cx)
.update_in(cx, |workspace, window, cx| {
workspace.open_path_preview(path_2.clone(), None, true, true, window, cx)
})
.await
.unwrap();
@@ -6332,7 +6332,9 @@ 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_in(cx, |workspace, window, cx| {
workspace.go_back(pane.downgrade(), window, cx)
})
.await
.unwrap();
@@ -6350,10 +6352,11 @@ async fn test_preview_tabs(cx: &mut TestAppContext) {
});
// Closing item 1
pane.update(cx, |pane, cx| {
pane.update_in(cx, |pane, window, cx| {
pane.close_item_by_id(
pane.active_item().unwrap().item_id(),
workspace::SaveIntent::Skip,
window,
cx,
)
})
@@ -6371,7 +6374,9 @@ 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_in(cx, |workspace, window, cx| {
workspace.go_back(pane.downgrade(), window, cx)
})
.await
.unwrap();
@@ -6389,9 +6394,9 @@ async fn test_preview_tabs(cx: &mut TestAppContext) {
});
// Close permanent tab
pane.update(cx, |pane, cx| {
pane.update_in(cx, |pane, window, cx| {
let id = pane.items().next().unwrap().item_id();
pane.close_item_by_id(id, workspace::SaveIntent::Skip, cx)
pane.close_item_by_id(id, workspace::SaveIntent::Skip, window, cx)
})
.await
.unwrap();
@@ -6438,8 +6443,8 @@ async fn test_preview_tabs(cx: &mut TestAppContext) {
// Open item 2 as preview in right pane
workspace
.update(cx, |workspace, cx| {
workspace.open_path_preview(path_2.clone(), None, true, true, cx)
.update_in(cx, |workspace, window, cx| {
workspace.open_path_preview(path_2.clone(), None, true, true, window, cx)
})
.await
.unwrap();
@@ -6470,14 +6475,14 @@ async fn test_preview_tabs(cx: &mut TestAppContext) {
});
// Focus left pane
workspace.update(cx, |workspace, cx| {
workspace.activate_pane_in_direction(workspace::SplitDirection::Left, cx)
workspace.update_in(cx, |workspace, window, cx| {
workspace.activate_pane_in_direction(workspace::SplitDirection::Left, window, cx)
});
// Open item 2 as preview in left pane
workspace
.update(cx, |workspace, cx| {
workspace.open_path_preview(path_2.clone(), None, true, true, cx)
.update_in(cx, |workspace, window, cx| {
workspace.open_path_preview(path_2.clone(), None, true, true, window, cx)
})
.await
.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(&cx.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(&cx.model(), move |_, _, event, _| {
events.lock().push(event.clone());
})
.detach()

View File

@@ -17,7 +17,7 @@ use collections::{HashMap, HashSet};
use fs::FakeFs;
use futures::{channel::oneshot, StreamExt as _};
use git::GitHostingProviderRegistry;
use gpui::{BackgroundExecutor, Context, Model, Task, TestAppContext, View, VisualTestContext};
use gpui::{BackgroundExecutor, Context, Model, Task, TestAppContext, VisualTestContext};
use http_client::FakeHttpClient;
use language::LanguageRegistry;
use node_runtime::NodeRuntime;
@@ -752,17 +752,17 @@ impl TestClient {
pub async fn host_workspace(
&self,
workspace: &View<Workspace>,
workspace: &Model<Workspace>,
channel_id: ChannelId,
cx: &mut VisualTestContext,
) {
cx.update(|cx| {
cx.update(|_, cx| {
let active_call = ActiveCall::global(cx);
active_call.update(cx, |call, cx| call.join_channel(channel_id, cx))
})
.await
.unwrap();
cx.update(|cx| {
cx.update(|_, cx| {
let active_call = ActiveCall::global(cx);
let project = workspace.read(cx).project().clone();
active_call.update(cx, |call, cx| call.share_project(project, cx))
@@ -776,7 +776,7 @@ impl TestClient {
&'a self,
channel_id: ChannelId,
cx: &'a mut TestAppContext,
) -> (View<Workspace>, &'a mut VisualTestContext) {
) -> (Model<Workspace>, &'a mut VisualTestContext) {
cx.update(|cx| workspace::join_channel(channel_id, self.app_state.clone(), None, cx))
.await
.unwrap();
@@ -822,31 +822,31 @@ impl TestClient {
&'a self,
project: &Model<Project>,
cx: &'a mut TestAppContext,
) -> (View<Workspace>, &'a mut VisualTestContext) {
cx.add_window_view(|cx| {
cx.activate_window();
Workspace::new(None, project.clone(), self.app_state.clone(), cx)
) -> (Model<Workspace>, &'a mut VisualTestContext) {
cx.add_window_view(|window, cx| {
window.activate_window();
Workspace::new(None, project.clone(), self.app_state.clone(), window, cx)
})
}
pub async fn build_test_workspace<'a>(
&'a self,
cx: &'a mut TestAppContext,
) -> (View<Workspace>, &'a mut VisualTestContext) {
) -> (Model<Workspace>, &'a mut VisualTestContext) {
let project = self.build_test_project(cx).await;
cx.add_window_view(|cx| {
cx.activate_window();
Workspace::new(None, project.clone(), self.app_state.clone(), cx)
cx.add_window_view(|window, cx| {
window.activate_window();
Workspace::new(None, project.clone(), self.app_state.clone(), window, cx)
})
}
pub fn active_workspace<'a>(
&'a self,
cx: &'a mut TestAppContext,
) -> (View<Workspace>, &'a mut VisualTestContext) {
) -> (Model<Workspace>, &'a mut VisualTestContext) {
let window = cx.update(|cx| cx.active_window().unwrap().downcast::<Workspace>().unwrap());
let view = window.root_view(cx).unwrap();
let view = window.root_model(cx).unwrap();
let cx = VisualTestContext::from_window(*window.deref(), cx).as_mut();
// it might be nice to try and cleanup these at the end of each test.
(view, cx)
@@ -856,11 +856,11 @@ impl TestClient {
pub fn open_channel_notes(
channel_id: ChannelId,
cx: &mut VisualTestContext,
) -> Task<anyhow::Result<View<ChannelView>>> {
let window = cx.update(|cx| cx.active_window().unwrap().downcast::<Workspace>().unwrap());
let view = window.root_view(cx).unwrap();
) -> Task<anyhow::Result<Model<ChannelView>>> {
let window = cx.update(|_, cx| cx.active_window().unwrap().downcast::<Workspace>().unwrap());
let view = window.root_model(cx).unwrap();
cx.update(|cx| ChannelView::open(channel_id, None, view.clone(), cx))
cx.update(|window, cx| ChannelView::open(channel_id, None, view.clone(), window, cx))
}
impl Drop for TestClient {

View File

@@ -56,6 +56,7 @@ serde_json.workspace = true
settings.workspace = true
smallvec.workspace = true
story = { workspace = true, optional = true }
telemetry.workspace = true
theme.workspace = true
time.workspace = true
time_format.workspace = true

View File

@@ -1,5 +1,5 @@
use anyhow::Result;
use call::report_call_event_for_channel;
use call::ActiveCall;
use channel::{Channel, ChannelBuffer, ChannelBufferEvent, ChannelStore};
use client::{
proto::{self, PeerId},
@@ -11,9 +11,8 @@ use editor::{
EditorEvent,
};
use gpui::{
actions, AnyView, AppContext, ClipboardItem, Entity as _, EventEmitter, FocusableView, Model,
Pixels, Point, Render, Subscription, Task, View, ViewContext, VisualContext as _, WeakView,
WindowContext,
actions, AnyView, AppContext, ClipboardItem, Entity as _, EventEmitter, Focusable, Model,
ModelContext, Pixels, Point, Render, Subscription, Task, VisualContext as _, WeakModel, Window,
};
use project::Project;
use rpc::proto::ChannelVisibility;
@@ -38,8 +37,8 @@ pub fn init(cx: &mut AppContext) {
}
pub struct ChannelView {
pub editor: View<Editor>,
workspace: WeakView<Workspace>,
pub editor: Model<Editor>,
workspace: WeakModel<Workspace>,
project: Model<Project>,
channel_store: Model<ChannelStore>,
channel_buffer: Model<ChannelBuffer>,
@@ -52,27 +51,31 @@ impl ChannelView {
pub fn open(
channel_id: ChannelId,
link_position: Option<String>,
workspace: View<Workspace>,
cx: &mut WindowContext,
) -> Task<Result<View<Self>>> {
workspace: Model<Workspace>,
window: &mut Window,
cx: &mut AppContext,
) -> Task<Result<Model<Self>>> {
let pane = workspace.read(cx).active_pane().clone();
let channel_view = Self::open_in_pane(
channel_id,
link_position,
pane.clone(),
workspace.clone(),
window,
cx,
);
cx.spawn(|mut cx| async move {
window.spawn(cx, |mut cx| async move {
let channel_view = channel_view.await?;
pane.update(&mut cx, |pane, cx| {
report_call_event_for_channel(
"open channel notes",
pane.update_in(&mut cx, |pane, window, cx| {
telemetry::event!(
"Channel Notes Opened",
channel_id,
&workspace.read(cx).app_state().client,
cx,
room_id = ActiveCall::global(cx)
.read(cx)
.room()
.map(|r| r.read(cx).id())
);
pane.add_item(Box::new(channel_view.clone()), true, true, None, cx);
pane.add_item(Box::new(channel_view.clone()), true, true, None, window, cx);
})?;
anyhow::Ok(channel_view)
})
@@ -81,15 +84,16 @@ impl ChannelView {
pub fn open_in_pane(
channel_id: ChannelId,
link_position: Option<String>,
pane: View<Pane>,
workspace: View<Workspace>,
cx: &mut WindowContext,
) -> Task<Result<View<Self>>> {
let channel_view = Self::load(channel_id, workspace, cx);
cx.spawn(|mut cx| async move {
pane: Model<Pane>,
workspace: Model<Workspace>,
window: &mut Window,
cx: &mut AppContext,
) -> Task<Result<Model<Self>>> {
let channel_view = Self::load(channel_id, workspace, window, cx);
window.spawn(cx, |mut cx| async move {
let channel_view = channel_view.await?;
pane.update(&mut cx, |pane, cx| {
pane.update_in(&mut cx, |pane, window, cx| {
let buffer_id = channel_view.read(cx).channel_buffer.read(cx).remote_id(cx);
let existing_view = pane
@@ -102,7 +106,12 @@ impl ChannelView {
{
if let Some(link_position) = link_position {
existing_view.update(cx, |channel_view, cx| {
channel_view.focus_position_from_link(link_position, true, cx)
channel_view.focus_position_from_link(
link_position,
true,
window,
cx,
)
});
}
return existing_view;
@@ -113,15 +122,27 @@ impl ChannelView {
// replace that.
if let Some(existing_item) = existing_view {
if let Some(ix) = pane.index_for_item(&existing_item) {
pane.close_item_by_id(existing_item.entity_id(), SaveIntent::Skip, cx)
.detach();
pane.add_item(Box::new(channel_view.clone()), true, true, Some(ix), cx);
pane.close_item_by_id(
existing_item.entity_id(),
SaveIntent::Skip,
window,
cx,
)
.detach();
pane.add_item(
Box::new(channel_view.clone()),
true,
true,
Some(ix),
window,
cx,
);
}
}
if let Some(link_position) = link_position {
channel_view.update(cx, |channel_view, cx| {
channel_view.focus_position_from_link(link_position, true, cx)
channel_view.focus_position_from_link(link_position, true, window, cx)
});
}
@@ -132,9 +153,10 @@ impl ChannelView {
pub fn load(
channel_id: ChannelId,
workspace: View<Workspace>,
cx: &mut WindowContext,
) -> Task<Result<View<Self>>> {
workspace: Model<Workspace>,
window: &mut Window,
cx: &mut AppContext,
) -> Task<Result<Model<Self>>> {
let weak_workspace = workspace.downgrade();
let workspace = workspace.read(cx);
let project = workspace.project().to_owned();
@@ -144,7 +166,7 @@ impl ChannelView {
let channel_buffer =
channel_store.update(cx, |store, cx| store.open_channel_buffer(channel_id, cx));
cx.spawn(|mut cx| async move {
window.spawn(cx, |mut cx| async move {
let channel_buffer = channel_buffer.await?;
let markdown = markdown.await.log_err();
@@ -158,9 +180,15 @@ impl ChannelView {
})
})?;
cx.new_view(|cx| {
let mut this =
Self::new(project, weak_workspace, channel_store, channel_buffer, cx);
cx.new_window_model(|window, cx| {
let mut this = Self::new(
project,
weak_workspace,
channel_store,
channel_buffer,
window,
cx,
);
this.acknowledge_buffer_version(cx);
this
})
@@ -169,24 +197,27 @@ impl ChannelView {
pub fn new(
project: Model<Project>,
workspace: WeakView<Workspace>,
workspace: WeakModel<Workspace>,
channel_store: Model<ChannelStore>,
channel_buffer: Model<ChannelBuffer>,
cx: &mut ViewContext<Self>,
window: &mut Window,
cx: &mut ModelContext<Self>,
) -> Self {
let buffer = channel_buffer.read(cx).buffer();
let this = cx.view().downgrade();
let editor = cx.new_view(|cx| {
let mut editor = Editor::for_buffer(buffer, None, cx);
let this = cx.model().downgrade();
let editor = cx.new_model(|cx| {
let mut editor = Editor::for_buffer(buffer, None, window, cx);
editor.set_collaboration_hub(Box::new(ChannelBufferCollaborationHub(
channel_buffer.clone(),
)));
editor.set_custom_context_menu(move |_, position, cx| {
editor.set_custom_context_menu(move |_, position, window, cx| {
let this = this.clone();
Some(ui::ContextMenu::build(cx, move |menu, _| {
menu.entry("Copy link to section", None, move |cx| {
this.update(cx, |this, cx| this.copy_link_for_position(position, cx))
.ok();
Some(ui::ContextMenu::build(window, cx, move |menu, _, _| {
menu.entry("Copy link to section", None, move |window, cx| {
this.update(cx, |this, cx| {
this.copy_link_for_position(position, window, cx)
})
.ok();
})
}))
});
@@ -195,7 +226,7 @@ impl ChannelView {
let _editor_event_subscription =
cx.subscribe(&editor, |_, _, e: &EditorEvent, cx| cx.emit(e.clone()));
cx.subscribe(&channel_buffer, Self::handle_channel_buffer_event)
cx.subscribe_in(&channel_buffer, window, Self::handle_channel_buffer_event)
.detach();
Self {
@@ -214,10 +245,13 @@ impl ChannelView {
&mut self,
position: String,
first_attempt: bool,
cx: &mut ViewContext<Self>,
window: &mut Window,
cx: &mut ModelContext<Self>,
) {
let position = Channel::slug(&position).to_lowercase();
let snapshot = self.editor.update(cx, |editor, cx| editor.snapshot(cx));
let snapshot = self
.editor
.update(cx, |editor, cx| editor.snapshot(window, cx));
if let Some(outline) = snapshot.buffer_snapshot.outline(None) {
if let Some(item) = outline
@@ -226,7 +260,7 @@ impl ChannelView {
.find(|item| &Channel::slug(&item.text).to_lowercase() == &position)
{
self.editor.update(cx, |editor, cx| {
editor.change_selections(Some(Autoscroll::focused()), cx, |s| {
editor.change_selections(Some(Autoscroll::focused()), window, cx, |s| {
s.replace_cursors_with(|map| vec![item.range.start.to_display_point(map)])
})
});
@@ -237,12 +271,13 @@ impl ChannelView {
if !first_attempt {
return;
}
self._reparse_subscription = Some(cx.subscribe(
self._reparse_subscription = Some(cx.subscribe_in(
&self.editor,
move |this, _, e: &EditorEvent, cx| {
window,
move |this, _, e: &EditorEvent, window, cx| {
match e {
EditorEvent::Reparsed(_) => {
this.focus_position_from_link(position.clone(), false, cx);
this.focus_position_from_link(position.clone(), false, window, cx);
this._reparse_subscription.take();
}
EditorEvent::Edited { .. } | EditorEvent::SelectionsChanged { local: true } => {
@@ -254,15 +289,22 @@ impl ChannelView {
));
}
fn copy_link(&mut self, _: &CopyLink, cx: &mut ViewContext<Self>) {
fn copy_link(&mut self, _: &CopyLink, window: &mut Window, cx: &mut ModelContext<Self>) {
let position = self
.editor
.update(cx, |editor, cx| editor.selections.newest_display(cx).start);
self.copy_link_for_position(position, cx)
self.copy_link_for_position(position, window, cx)
}
fn copy_link_for_position(&self, position: DisplayPoint, cx: &mut ViewContext<Self>) {
let snapshot = self.editor.update(cx, |editor, cx| editor.snapshot(cx));
fn copy_link_for_position(
&self,
position: DisplayPoint,
window: &mut Window,
cx: &mut ModelContext<Self>,
) {
let snapshot = self
.editor
.update(cx, |editor, cx| editor.snapshot(window, cx));
let mut closest_heading = None;
@@ -302,9 +344,10 @@ impl ChannelView {
fn handle_channel_buffer_event(
&mut self,
_: Model<ChannelBuffer>,
_: &Model<ChannelBuffer>,
event: &ChannelBufferEvent,
cx: &mut ViewContext<Self>,
window: &mut Window,
cx: &mut ModelContext<Self>,
) {
match event {
ChannelBufferEvent::Disconnected => self.editor.update(cx, |editor, cx| {
@@ -318,7 +361,7 @@ impl ChannelView {
});
}
ChannelBufferEvent::BufferEdited => {
if self.editor.read(cx).is_focused(cx) {
if self.editor.read(cx).is_focused(window) {
self.acknowledge_buffer_version(cx);
} else {
self.channel_store.update(cx, |store, cx| {
@@ -336,7 +379,7 @@ impl ChannelView {
}
}
fn acknowledge_buffer_version(&mut self, cx: &mut ViewContext<ChannelView>) {
fn acknowledge_buffer_version(&mut self, cx: &mut ModelContext<ChannelView>) {
self.channel_store.update(cx, |store, cx| {
let channel_buffer = self.channel_buffer.read(cx);
store.acknowledge_notes_version(
@@ -355,7 +398,7 @@ impl ChannelView {
impl EventEmitter<EditorEvent> for ChannelView {}
impl Render for ChannelView {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
fn render(&mut self, _: &mut Window, cx: &mut ModelContext<Self>) -> impl IntoElement {
div()
.size_full()
.on_action(cx.listener(Self::copy_link))
@@ -363,7 +406,7 @@ impl Render for ChannelView {
}
}
impl FocusableView for ChannelView {
impl Focusable for ChannelView {
fn focus_handle(&self, cx: &AppContext) -> gpui::FocusHandle {
self.editor.read(cx).focus_handle(cx)
}
@@ -375,7 +418,7 @@ impl Item for ChannelView {
fn act_as_type<'a>(
&'a self,
type_id: TypeId,
self_handle: &'a View<Self>,
self_handle: &'a Model<Self>,
_: &'a AppContext,
) -> Option<AnyView> {
if type_id == TypeId::of::<Self>() {
@@ -387,7 +430,7 @@ impl Item for ChannelView {
}
}
fn tab_icon(&self, cx: &WindowContext) -> Option<Icon> {
fn tab_icon(&self, _: &Window, cx: &AppContext) -> Option<Icon> {
let channel = self.channel(cx)?;
let icon = match channel.visibility {
ChannelVisibility::Public => IconName::Public,
@@ -397,7 +440,12 @@ impl Item for ChannelView {
Some(Icon::new(icon))
}
fn tab_content(&self, params: TabContentParams, cx: &WindowContext) -> gpui::AnyElement {
fn tab_content(
&self,
params: TabContentParams,
_: &Window,
cx: &AppContext,
) -> gpui::AnyElement {
let (channel_name, status) = if let Some(channel) = self.channel(cx) {
let status = match (
self.channel_buffer.read(cx).buffer().read(cx).read_only(),
@@ -437,14 +485,16 @@ impl Item for ChannelView {
fn clone_on_split(
&self,
_: Option<WorkspaceId>,
cx: &mut ViewContext<Self>,
) -> Option<View<Self>> {
Some(cx.new_view(|cx| {
window: &mut Window,
cx: &mut ModelContext<Self>,
) -> Option<Model<Self>> {
Some(cx.new_model(|cx| {
Self::new(
self.project.clone(),
self.workspace.clone(),
self.channel_store.clone(),
self.channel_buffer.clone(),
window,
cx,
)
}))
@@ -454,21 +504,33 @@ impl Item for ChannelView {
false
}
fn navigate(&mut self, data: Box<dyn Any>, cx: &mut ViewContext<Self>) -> bool {
fn navigate(
&mut self,
data: Box<dyn Any>,
window: &mut Window,
cx: &mut ModelContext<Self>,
) -> bool {
self.editor
.update(cx, |editor, cx| editor.navigate(data, cx))
.update(cx, |editor, cx| editor.navigate(data, window, cx))
}
fn deactivated(&mut self, cx: &mut ViewContext<Self>) {
self.editor.update(cx, Item::deactivated)
}
fn set_nav_history(&mut self, history: ItemNavHistory, cx: &mut ViewContext<Self>) {
fn deactivated(&mut self, window: &mut Window, cx: &mut ModelContext<Self>) {
self.editor
.update(cx, |editor, cx| Item::set_nav_history(editor, history, cx))
.update(cx, |item, cx| item.deactivated(window, cx))
}
fn as_searchable(&self, _: &View<Self>) -> Option<Box<dyn SearchableItemHandle>> {
fn set_nav_history(
&mut self,
history: ItemNavHistory,
window: &mut Window,
cx: &mut ModelContext<Self>,
) {
self.editor.update(cx, |editor, cx| {
Item::set_nav_history(editor, history, window, cx)
})
}
fn as_searchable(&self, _: &Model<Self>) -> Option<Box<dyn SearchableItemHandle>> {
Some(Box::new(self.editor.clone()))
}
@@ -490,7 +552,7 @@ impl FollowableItem for ChannelView {
self.remote_id
}
fn to_state_proto(&self, cx: &WindowContext) -> Option<proto::view::Variant> {
fn to_state_proto(&self, window: &Window, cx: &AppContext) -> Option<proto::view::Variant> {
let channel_buffer = self.channel_buffer.read(cx);
if !channel_buffer.is_connected() {
return None;
@@ -500,7 +562,7 @@ impl FollowableItem for ChannelView {
proto::view::ChannelView {
channel_id: channel_buffer.channel_id.0,
editor: if let Some(proto::view::Variant::Editor(proto)) =
self.editor.read(cx).to_state_proto(cx)
self.editor.read(cx).to_state_proto(window, cx)
{
Some(proto)
} else {
@@ -511,11 +573,12 @@ impl FollowableItem for ChannelView {
}
fn from_state_proto(
workspace: View<workspace::Workspace>,
workspace: Model<workspace::Workspace>,
remote_id: workspace::ViewId,
state: &mut Option<proto::view::Variant>,
cx: &mut WindowContext,
) -> Option<gpui::Task<anyhow::Result<View<Self>>>> {
window: &mut Window,
cx: &mut AppContext,
) -> Option<gpui::Task<anyhow::Result<Model<Self>>>> {
let Some(proto::view::Variant::ChannelView(_)) = state else {
return None;
};
@@ -523,12 +586,12 @@ impl FollowableItem for ChannelView {
unreachable!()
};
let open = ChannelView::load(ChannelId(state.channel_id), workspace, cx);
let open = ChannelView::load(ChannelId(state.channel_id), workspace, window, cx);
Some(cx.spawn(|mut cx| async move {
Some(window.spawn(cx, |mut cx| async move {
let this = open.await?;
let task = this.update(&mut cx, |this, cx| {
let task = this.update_in(&mut cx, |this, window, cx| {
this.remote_id = Some(remote_id);
if let Some(state) = state.editor {
@@ -543,6 +606,7 @@ impl FollowableItem for ChannelView {
scroll_y: state.scroll_y,
..Default::default()
}),
window,
cx,
)
}))
@@ -563,31 +627,38 @@ impl FollowableItem for ChannelView {
&self,
event: &EditorEvent,
update: &mut Option<proto::update_view::Variant>,
cx: &WindowContext,
window: &Window,
cx: &AppContext,
) -> bool {
self.editor
.read(cx)
.add_event_to_update_proto(event, update, cx)
.add_event_to_update_proto(event, update, window, cx)
}
fn apply_update_proto(
&mut self,
project: &Model<Project>,
message: proto::update_view::Variant,
cx: &mut ViewContext<Self>,
window: &mut Window,
cx: &mut ModelContext<Self>,
) -> gpui::Task<anyhow::Result<()>> {
self.editor.update(cx, |editor, cx| {
editor.apply_update_proto(project, message, cx)
editor.apply_update_proto(project, message, window, cx)
})
}
fn set_leader_peer_id(&mut self, leader_peer_id: Option<PeerId>, cx: &mut ViewContext<Self>) {
fn set_leader_peer_id(
&mut self,
leader_peer_id: Option<PeerId>,
window: &mut Window,
cx: &mut ModelContext<Self>,
) {
self.editor.update(cx, |editor, cx| {
editor.set_leader_peer_id(leader_peer_id, cx)
editor.set_leader_peer_id(leader_peer_id, window, cx)
})
}
fn is_project_item(&self, _cx: &WindowContext) -> bool {
fn is_project_item(&self, _window: &Window, _cx: &AppContext) -> bool {
false
}
@@ -595,7 +666,7 @@ impl FollowableItem for ChannelView {
Editor::to_follow_event(event)
}
fn dedup(&self, existing: &Self, cx: &WindowContext) -> Option<Dedup> {
fn dedup(&self, existing: &Self, _: &Window, cx: &AppContext) -> Option<Dedup> {
let existing = existing.channel_buffer.read(cx);
if self.channel_buffer.read(cx).channel_id == existing.channel_id {
if existing.is_connected() {

View File

@@ -1,4 +1,4 @@
use crate::{collab_panel, ChatPanelSettings};
use crate::{collab_panel, ChatPanelButton, ChatPanelSettings};
use anyhow::Result;
use call::{room, ActiveCall};
use channel::{ChannelChat, ChannelChatEvent, ChannelMessage, ChannelMessageId, ChannelStore};
@@ -8,9 +8,9 @@ use db::kvp::KEY_VALUE_STORE;
use editor::{actions, Editor};
use gpui::{
actions, div, list, prelude::*, px, Action, AppContext, AsyncWindowContext, ClipboardItem,
CursorStyle, DismissEvent, ElementId, EventEmitter, FocusHandle, FocusableView, FontWeight,
HighlightStyle, ListOffset, ListScrollEvent, ListState, Model, Render, Stateful, Subscription,
Task, View, ViewContext, VisualContext, WeakView,
CursorStyle, DismissEvent, ElementId, EventEmitter, FocusHandle, Focusable, FontWeight,
HighlightStyle, ListOffset, ListScrollEvent, ListState, Model, ModelContext, Render, Stateful,
Subscription, Task, WeakModel, Window,
};
use language::LanguageRegistry;
use menu::Confirm;
@@ -37,9 +37,9 @@ const MESSAGE_LOADING_THRESHOLD: usize = 50;
const CHAT_PANEL_KEY: &str = "ChatPanel";
pub fn init(cx: &mut AppContext) {
cx.observe_new_views(|workspace: &mut Workspace, _| {
workspace.register_action(|workspace, _: &ToggleFocus, cx| {
workspace.toggle_panel_focus::<ChatPanel>(cx);
cx.observe_new_models(|workspace: &mut Workspace, _, _| {
workspace.register_action(|workspace, _: &ToggleFocus, window, cx| {
workspace.toggle_panel_focus::<ChatPanel>(window, cx);
});
})
.detach();
@@ -51,7 +51,7 @@ pub struct ChatPanel {
languages: Arc<LanguageRegistry>,
message_list: ListState,
active_chat: Option<(Model<ChannelChat>, Subscription)>,
message_editor: View<MessageEditor>,
message_editor: Model<MessageEditor>,
local_timezone: UtcOffset,
fs: Arc<dyn Fs>,
width: Option<Pixels>,
@@ -74,37 +74,46 @@ struct SerializedChatPanel {
actions!(chat_panel, [ToggleFocus]);
impl ChatPanel {
pub fn new(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) -> View<Self> {
pub fn new(
workspace: &mut Workspace,
window: &mut Window,
cx: &mut ModelContext<Workspace>,
) -> Model<Self> {
let fs = workspace.app_state().fs.clone();
let client = workspace.app_state().client.clone();
let channel_store = ChannelStore::global(cx);
let user_store = workspace.app_state().user_store.clone();
let languages = workspace.app_state().languages.clone();
let input_editor = cx.new_view(|cx| {
let input_editor = cx.new_model(|cx| {
MessageEditor::new(
languages.clone(),
user_store.clone(),
None,
cx.new_view(|cx| Editor::auto_height(4, cx)),
cx.new_model(|cx| Editor::auto_height(4, window, cx)),
window,
cx,
)
});
cx.new_view(|cx: &mut ViewContext<Self>| {
let view = cx.view().downgrade();
let message_list =
ListState::new(0, gpui::ListAlignment::Bottom, px(1000.), move |ix, cx| {
cx.new_model(|cx| {
let view = cx.model().downgrade();
let message_list = ListState::new(
0,
gpui::ListAlignment::Bottom,
px(1000.),
move |ix, window, cx| {
if let Some(view) = view.upgrade() {
view.update(cx, |view, cx| {
view.render_message(ix, cx).into_any_element()
view.update(cx, |view: &mut Self, cx| {
view.render_message(ix, window, cx).into_any_element()
})
} else {
div().into_any()
}
});
},
);
message_list.set_scroll_handler(cx.listener(|this, event: &ListScrollEvent, cx| {
message_list.set_scroll_handler(cx.listener(|this, event: &ListScrollEvent, _, cx| {
if event.visible_range.start < MESSAGE_LOADING_THRESHOLD {
this.load_more_messages(cx);
}
@@ -187,9 +196,9 @@ impl ChatPanel {
}
pub fn load(
workspace: WeakView<Workspace>,
workspace: WeakModel<Workspace>,
cx: AsyncWindowContext,
) -> Task<Result<View<Self>>> {
) -> Task<Result<Model<Self>>> {
cx.spawn(|mut cx| async move {
let serialized_panel = if let Some(panel) = cx
.background_executor()
@@ -203,8 +212,8 @@ impl ChatPanel {
None
};
workspace.update(&mut cx, |workspace, cx| {
let panel = Self::new(workspace, cx);
workspace.update_in(&mut cx, |workspace, window, cx| {
let panel = Self::new(workspace, window, cx);
if let Some(serialized_panel) = serialized_panel {
panel.update(cx, |panel, cx| {
panel.width = serialized_panel.width.map(|r| r.round());
@@ -216,7 +225,7 @@ impl ChatPanel {
})
}
fn serialize(&mut self, cx: &mut ViewContext<Self>) {
fn serialize(&mut self, cx: &mut ModelContext<Self>) {
let width = self.width;
self.pending_serialization = cx.background_executor().spawn(
async move {
@@ -232,7 +241,7 @@ impl ChatPanel {
);
}
fn set_active_chat(&mut self, chat: Model<ChannelChat>, cx: &mut ViewContext<Self>) {
fn set_active_chat(&mut self, chat: Model<ChannelChat>, cx: &mut ModelContext<Self>) {
if self.active_chat.as_ref().map(|e| &e.0) != Some(&chat) {
self.markdown_data.clear();
self.message_list.reset(chat.read(cx).message_count());
@@ -251,7 +260,7 @@ impl ChatPanel {
&mut self,
_: Model<ChannelChat>,
event: &ChannelChatEvent,
cx: &mut ViewContext<Self>,
cx: &mut ModelContext<Self>,
) {
match event {
ChannelChatEvent::MessagesUpdated {
@@ -284,7 +293,7 @@ impl ChatPanel {
cx.notify();
}
fn acknowledge_last_message(&mut self, cx: &mut ViewContext<Self>) {
fn acknowledge_last_message(&mut self, cx: &mut ModelContext<Self>) {
if self.active && self.is_scrolled_to_bottom {
if let Some((chat, _)) = &self.active_chat {
if let Some(channel_id) = self.channel_id(cx) {
@@ -305,7 +314,7 @@ impl ChatPanel {
&mut self,
message_id: Option<ChannelMessageId>,
reply_to_message: &Option<ChannelMessage>,
cx: &mut ViewContext<Self>,
cx: &mut ModelContext<Self>,
) -> impl IntoElement {
let reply_to_message = match reply_to_message {
None => {
@@ -369,8 +378,8 @@ impl ChatPanel {
),
)
.cursor(CursorStyle::PointingHand)
.tooltip(|cx| Tooltip::text("Go to message", cx))
.on_click(cx.listener(move |chat_panel, _, cx| {
.tooltip(Tooltip::text("Go to message"))
.on_click(cx.listener(move |chat_panel, _, _, cx| {
if let Some(channel_id) = current_channel_id {
chat_panel
.select_channel(channel_id, reply_to_message_id.into(), cx)
@@ -380,7 +389,12 @@ impl ChatPanel {
)
}
fn render_message(&mut self, ix: usize, cx: &mut ViewContext<Self>) -> impl IntoElement {
fn render_message(
&mut self,
ix: usize,
window: &mut Window,
cx: &mut ModelContext<Self>,
) -> impl IntoElement {
let active_chat = &self.active_chat.as_ref().unwrap().0;
let (message, is_continuation_from_previous, is_admin) =
active_chat.update(cx, |active_chat, cx| {
@@ -530,7 +544,7 @@ impl ChatPanel {
.w_full()
.text_ui_sm(cx)
.id(element_id)
.child(text.element("body".into(), cx)),
.child(text.element("body".into(), window, cx)),
)
.when(self.has_open_menu(message_id), |el| {
el.bg(cx.theme().colors().element_selected)
@@ -560,7 +574,7 @@ impl ChatPanel {
},
)
.child(
self.render_popover_buttons(cx, message_id, can_delete_message, can_edit_message)
self.render_popover_buttons(message_id, can_delete_message, can_edit_message, cx)
.mt_neg_2p5(),
)
}
@@ -572,7 +586,7 @@ impl ChatPanel {
}
}
fn render_popover_button(&self, cx: &ViewContext<Self>, child: Stateful<Div>) -> Div {
fn render_popover_button(&self, cx: &mut ModelContext<Self>, child: Stateful<Div>) -> Div {
div()
.w_6()
.bg(cx.theme().colors().element_background)
@@ -582,10 +596,10 @@ impl ChatPanel {
fn render_popover_buttons(
&self,
cx: &ViewContext<Self>,
message_id: Option<u64>,
can_delete_message: bool,
can_edit_message: bool,
cx: &mut ModelContext<Self>,
) -> Div {
h_flex()
.absolute()
@@ -606,16 +620,16 @@ impl ChatPanel {
.id("reply")
.child(
IconButton::new(("reply", message_id), IconName::ReplyArrowRight)
.on_click(cx.listener(move |this, _, cx| {
.on_click(cx.listener(move |this, _, window, cx| {
this.cancel_edit_message(cx);
this.message_editor.update(cx, |editor, cx| {
editor.set_reply_to_message_id(message_id);
editor.focus_handle(cx).focus(cx);
window.focus(&editor.focus_handle(cx));
})
})),
)
.tooltip(|cx| Tooltip::text("Reply", cx)),
.tooltip(Tooltip::text("Reply")),
),
)
})
@@ -628,7 +642,7 @@ impl ChatPanel {
.id("edit")
.child(
IconButton::new(("edit", message_id), IconName::Pencil)
.on_click(cx.listener(move |this, _, cx| {
.on_click(cx.listener(move |this, _, window, cx| {
this.message_editor.update(cx, |editor, cx| {
editor.clear_reply_to_message_id();
@@ -655,18 +669,18 @@ impl ChatPanel {
});
editor.set_edit_message_id(message_id);
editor.focus_handle(cx).focus(cx);
editor.focus_handle(cx).focus(window);
}
})
})),
)
.tooltip(|cx| Tooltip::text("Edit", cx)),
.tooltip(Tooltip::text("Edit")),
),
)
})
})
.when_some(message_id, |el, message_id| {
let this = cx.view().clone();
let this = cx.model().clone();
el.child(
self.render_popover_button(
@@ -678,34 +692,36 @@ impl ChatPanel {
("trigger", message_id),
IconName::Ellipsis,
))
.menu(move |cx| {
.menu(move |window, cx| {
Some(Self::render_message_menu(
&this,
message_id,
can_delete_message,
window,
cx,
))
}),
)
.id("more")
.tooltip(|cx| Tooltip::text("More", cx)),
.tooltip(Tooltip::text("More")),
),
)
})
}
fn render_message_menu(
this: &View<Self>,
this: &Model<Self>,
message_id: u64,
can_delete_message: bool,
cx: &mut WindowContext,
) -> View<ContextMenu> {
window: &mut Window,
cx: &mut AppContext,
) -> Model<ContextMenu> {
let menu = {
ContextMenu::build(cx, move |menu, cx| {
ContextMenu::build(window, cx, move |menu, window, _| {
menu.entry(
"Copy message text",
None,
cx.handler_for(this, move |this, cx| {
window.handler_for(this, move |this, _, cx| {
if let Some(message) = this.active_chat().and_then(|active_chat| {
active_chat.read(cx).find_loaded_message(message_id)
}) {
@@ -718,15 +734,21 @@ impl ChatPanel {
menu.entry(
"Delete message",
None,
cx.handler_for(this, move |this, cx| this.remove_message(message_id, cx)),
window.handler_for(this, move |this, _, cx| {
this.remove_message(message_id, cx)
}),
)
})
})
};
this.update(cx, |this, cx| {
let subscription = cx.subscribe(&menu, |this: &mut Self, _, _: &DismissEvent, _| {
this.open_context_menu = None;
});
let subscription = cx.subscribe_in(
&menu,
window,
|this: &mut Self, _, _: &DismissEvent, _, _| {
this.open_context_menu = None;
},
);
this.open_context_menu = Some((message_id, subscription));
});
menu
@@ -777,19 +799,19 @@ impl ChatPanel {
);
rich_text.custom_ranges.push(range);
rich_text.set_tooltip_builder_for_custom_ranges(move |_, _, cx| {
Some(Tooltip::text(edit_timestamp_text.clone(), cx))
rich_text.set_tooltip_builder_for_custom_ranges(move |_, _, _, cx| {
Some(Tooltip::simple(edit_timestamp_text.clone(), cx))
})
}
}
rich_text
}
fn send(&mut self, _: &Confirm, cx: &mut ViewContext<Self>) {
fn send(&mut self, _: &Confirm, window: &mut Window, cx: &mut ModelContext<Self>) {
if let Some((chat, _)) = self.active_chat.as_ref() {
let message = self
.message_editor
.update(cx, |editor, cx| editor.take_message(cx));
.update(cx, |editor, cx| editor.take_message(window, cx));
if let Some(id) = self.message_editor.read(cx).edit_message_id() {
self.message_editor.update(cx, |editor, _| {
@@ -811,13 +833,13 @@ impl ChatPanel {
}
}
fn remove_message(&mut self, id: u64, cx: &mut ViewContext<Self>) {
fn remove_message(&mut self, id: u64, cx: &mut ModelContext<Self>) {
if let Some((chat, _)) = self.active_chat.as_ref() {
chat.update(cx, |chat, cx| chat.remove_message(id, cx).detach())
}
}
fn load_more_messages(&mut self, cx: &mut ViewContext<Self>) {
fn load_more_messages(&mut self, cx: &mut ModelContext<Self>) {
if let Some((chat, _)) = self.active_chat.as_ref() {
chat.update(cx, |channel, cx| {
if let Some(task) = channel.load_more_messages(cx) {
@@ -831,7 +853,7 @@ impl ChatPanel {
&mut self,
selected_channel_id: ChannelId,
scroll_to_message_id: Option<u64>,
cx: &mut ViewContext<ChatPanel>,
cx: &mut ModelContext<ChatPanel>,
) -> Task<Result<()>> {
let open_chat = self
.active_chat
@@ -857,20 +879,18 @@ impl ChatPanel {
if let Some(message_id) = scroll_to_message_id {
if let Some(item_ix) =
ChannelChat::load_history_since_message(chat.clone(), message_id, (*cx).clone())
ChannelChat::load_history_since_message(chat.clone(), message_id, cx.clone())
.await
{
this.update(&mut cx, |this, cx| {
if let Some(highlight_message_id) = highlight_message_id {
let task = cx.spawn({
|this, mut cx| async move {
cx.background_executor().timer(Duration::from_secs(2)).await;
this.update(&mut cx, |this, cx| {
this.highlighted_message.take();
cx.notify();
})
.ok();
}
let task = cx.spawn(|this, mut cx| async move {
cx.background_executor().timer(Duration::from_secs(2)).await;
this.update(&mut cx, |this, cx| {
this.highlighted_message.take();
cx.notify();
})
.ok();
});
this.highlighted_message = Some((highlight_message_id, task));
@@ -891,12 +911,12 @@ impl ChatPanel {
})
}
fn close_reply_preview(&mut self, cx: &mut ViewContext<Self>) {
fn close_reply_preview(&mut self, cx: &mut ModelContext<Self>) {
self.message_editor
.update(cx, |editor, _| editor.clear_reply_to_message_id());
}
fn cancel_edit_message(&mut self, cx: &mut ViewContext<Self>) {
fn cancel_edit_message(&mut self, cx: &mut ModelContext<Self>) {
self.message_editor.update(cx, |editor, cx| {
// only clear the editor input if we were editing a message
if editor.edit_message_id().is_none() {
@@ -919,7 +939,7 @@ impl ChatPanel {
}
impl Render for ChatPanel {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
fn render(&mut self, window: &mut Window, cx: &mut ModelContext<Self>) -> impl IntoElement {
let channel_id = self
.active_chat
.as_ref()
@@ -971,11 +991,12 @@ impl Render for ChatPanel {
.full_width()
.key_binding(KeyBinding::for_action(
&collab_panel::ToggleFocus,
cx,
window,
))
.on_click(|_, cx| {
cx.dispatch_action(
.on_click(|_, window, cx| {
window.dispatch_action(
collab_panel::ToggleFocus.boxed_clone(),
cx,
)
}),
),
@@ -999,8 +1020,8 @@ impl Render for ChatPanel {
.child(
IconButton::new("cancel-edit-message", IconName::Close)
.shape(ui::IconButtonShape::Square)
.tooltip(|cx| Tooltip::text("Cancel edit message", cx))
.on_click(cx.listener(move |this, _, cx| {
.tooltip(Tooltip::text("Cancel edit message"))
.on_click(cx.listener(move |this, _, _, cx| {
this.cancel_edit_message(cx);
})),
),
@@ -1045,7 +1066,7 @@ impl Render for ChatPanel {
)
.when_some(channel_id, |this, channel_id| {
this.cursor_pointer().on_click(cx.listener(
move |chat_panel, _, cx| {
move |chat_panel, _, _, cx| {
chat_panel
.select_channel(
channel_id,
@@ -1061,8 +1082,8 @@ impl Render for ChatPanel {
.child(
IconButton::new("close-reply-preview", IconName::Close)
.shape(ui::IconButtonShape::Square)
.tooltip(|cx| Tooltip::text("Close reply", cx))
.on_click(cx.listener(move |this, _, cx| {
.tooltip(Tooltip::text("Close reply"))
.on_click(cx.listener(move |this, _, _, cx| {
this.close_reply_preview(cx);
})),
),
@@ -1073,7 +1094,7 @@ impl Render for ChatPanel {
Some(
h_flex()
.p_2()
.on_action(cx.listener(|this, _: &actions::Cancel, cx| {
.on_action(cx.listener(|this, _: &actions::Cancel, _, cx| {
this.cancel_edit_message(cx);
this.close_reply_preview(cx);
}))
@@ -1085,7 +1106,7 @@ impl Render for ChatPanel {
}
}
impl FocusableView for ChatPanel {
impl Focusable for ChatPanel {
fn focus_handle(&self, cx: &AppContext) -> gpui::FocusHandle {
if self.active_chat.is_some() {
self.message_editor.read(cx).focus_handle(cx)
@@ -1096,7 +1117,7 @@ impl FocusableView for ChatPanel {
}
impl Panel for ChatPanel {
fn position(&self, cx: &gpui::WindowContext) -> DockPosition {
fn position(&self, _: &Window, cx: &AppContext) -> DockPosition {
ChatPanelSettings::get_global(cx).dock
}
@@ -1104,7 +1125,12 @@ impl Panel for ChatPanel {
matches!(position, DockPosition::Left | DockPosition::Right)
}
fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext<Self>) {
fn set_position(
&mut self,
position: DockPosition,
_: &mut Window,
cx: &mut ModelContext<Self>,
) {
settings::update_settings_file::<ChatPanelSettings>(
self.fs.clone(),
cx,
@@ -1112,18 +1138,18 @@ impl Panel for ChatPanel {
);
}
fn size(&self, cx: &gpui::WindowContext) -> Pixels {
fn size(&self, _: &Window, cx: &AppContext) -> Pixels {
self.width
.unwrap_or_else(|| ChatPanelSettings::get_global(cx).default_width)
}
fn set_size(&mut self, size: Option<Pixels>, cx: &mut ViewContext<Self>) {
fn set_size(&mut self, size: Option<Pixels>, _: &mut Window, cx: &mut ModelContext<Self>) {
self.width = size;
self.serialize(cx);
cx.notify();
}
fn set_active(&mut self, active: bool, cx: &mut ViewContext<Self>) {
fn set_active(&mut self, active: bool, _: &mut Window, cx: &mut ModelContext<Self>) {
self.active = active;
if active {
self.acknowledge_last_message(cx);
@@ -1134,11 +1160,19 @@ impl Panel for ChatPanel {
"ChatPanel"
}
fn icon(&self, cx: &WindowContext) -> Option<ui::IconName> {
Some(ui::IconName::MessageBubbles).filter(|_| ChatPanelSettings::get_global(cx).button)
fn icon(&self, _: &Window, cx: &AppContext) -> Option<ui::IconName> {
match ChatPanelSettings::get_global(cx).button {
ChatPanelButton::Never => None,
ChatPanelButton::Always => Some(ui::IconName::MessageBubbles),
ChatPanelButton::WhenInCall => ActiveCall::global(cx)
.read(cx)
.room()
.filter(|room| room.read(cx).contains_guests())
.map(|_| ui::IconName::MessageBubbles),
}
}
fn icon_tooltip(&self, _cx: &WindowContext) -> Option<&'static str> {
fn icon_tooltip(&self, _: &Window, _: &AppContext) -> Option<&'static str> {
Some("Chat Panel")
}
@@ -1146,12 +1180,16 @@ impl Panel for ChatPanel {
Box::new(ToggleFocus)
}
fn starts_open(&self, cx: &WindowContext) -> bool {
fn starts_open(&self, _: &Window, cx: &AppContext) -> bool {
ActiveCall::global(cx)
.read(cx)
.room()
.is_some_and(|room| room.read(cx).contains_guests())
}
fn activation_priority(&self) -> u32 {
7
}
}
impl EventEmitter<PanelEvent> for ChatPanel {}

View File

@@ -5,8 +5,8 @@ use collections::HashSet;
use editor::{AnchorRangeExt, CompletionProvider, Editor, EditorElement, EditorStyle};
use fuzzy::{StringMatch, StringMatchCandidate};
use gpui::{
AsyncWindowContext, FocusableView, FontStyle, FontWeight, HighlightStyle, IntoElement, Model,
Render, Task, TextStyle, View, ViewContext, WeakView,
AsyncWindowContext, Focusable, FontStyle, FontWeight, HighlightStyle, IntoElement, Model,
ModelContext, Render, Task, TextStyle, WeakModel, Window,
};
use language::{
language_settings::SoftWrap, Anchor, Buffer, BufferSnapshot, CodeLabel, LanguageRegistry,
@@ -42,7 +42,7 @@ static MENTIONS_SEARCH: LazyLock<SearchQuery> = LazyLock::new(|| {
});
pub struct MessageEditor {
pub editor: View<Editor>,
pub editor: Model<Editor>,
user_store: Model<UserStore>,
channel_chat: Option<Model<ChannelChat>>,
mentions: Vec<UserId>,
@@ -51,7 +51,7 @@ pub struct MessageEditor {
edit_message_id: Option<u64>,
}
struct MessageEditorCompletionProvider(WeakView<MessageEditor>);
struct MessageEditorCompletionProvider(WeakModel<MessageEditor>);
impl CompletionProvider for MessageEditorCompletionProvider {
fn completions(
@@ -59,13 +59,14 @@ impl CompletionProvider for MessageEditorCompletionProvider {
buffer: &Model<Buffer>,
buffer_position: language::Anchor,
_: editor::CompletionContext,
cx: &mut ViewContext<Editor>,
window: &mut Window,
cx: &mut ModelContext<Editor>,
) -> Task<anyhow::Result<Vec<Completion>>> {
let Some(handle) = self.0.upgrade() else {
return Task::ready(Ok(Vec::new()));
};
handle.update(cx, |message_editor, cx| {
message_editor.completions(buffer, buffer_position, cx)
message_editor.completions(buffer, buffer_position, window, cx)
})
}
@@ -74,28 +75,20 @@ impl CompletionProvider for MessageEditorCompletionProvider {
_buffer: Model<Buffer>,
_completion_indices: Vec<usize>,
_completions: Rc<RefCell<Box<[Completion]>>>,
_cx: &mut ViewContext<Editor>,
_window: &mut Window,
_cx: &mut ModelContext<Editor>,
) -> Task<anyhow::Result<bool>> {
Task::ready(Ok(false))
}
fn apply_additional_edits_for_completion(
&self,
_buffer: Model<Buffer>,
_completion: Completion,
_push_to_history: bool,
_cx: &mut ViewContext<Editor>,
) -> Task<Result<Option<language::Transaction>>> {
Task::ready(Ok(None))
}
fn is_completion_trigger(
&self,
_buffer: &Model<Buffer>,
_position: language::Anchor,
text: &str,
_trigger_in_words: bool,
_cx: &mut ViewContext<Editor>,
_window: &mut Window,
_cx: &mut ModelContext<Editor>,
) -> bool {
text == "@"
}
@@ -106,10 +99,11 @@ impl MessageEditor {
language_registry: Arc<LanguageRegistry>,
user_store: Model<UserStore>,
channel_chat: Option<Model<ChannelChat>>,
editor: View<Editor>,
cx: &mut ViewContext<Self>,
editor: Model<Editor>,
window: &mut Window,
cx: &mut ModelContext<Self>,
) -> Self {
let this = cx.view().downgrade();
let this = cx.model().downgrade();
editor.update(cx, |editor, cx| {
editor.set_soft_wrap_mode(SoftWrap::EditorWidth, cx);
editor.set_use_autoclose(false);
@@ -131,7 +125,8 @@ impl MessageEditor {
.as_singleton()
.expect("message editor must be singleton");
cx.subscribe(&buffer, Self::on_buffer_event).detach();
cx.subscribe_in(&buffer, window, Self::on_buffer_event)
.detach();
cx.observe_global::<settings::SettingsStore>(|view, cx| {
view.editor.update(cx, |editor, cx| {
editor.set_auto_replace_emoji_shortcode(
@@ -144,7 +139,7 @@ impl MessageEditor {
.detach();
let markdown = language_registry.language_for_name("Markdown");
cx.spawn(|_, mut cx| async move {
cx.spawn_in(window, |_, mut cx| async move {
let markdown = markdown.await.context("failed to load Markdown language")?;
buffer.update(&mut cx, |buffer, cx| {
buffer.set_language(Some(markdown), cx)
@@ -187,7 +182,7 @@ impl MessageEditor {
self.edit_message_id = None;
}
pub fn set_channel_chat(&mut self, chat: Model<ChannelChat>, cx: &mut ViewContext<Self>) {
pub fn set_channel_chat(&mut self, chat: Model<ChannelChat>, cx: &mut ModelContext<Self>) {
let channel_id = chat.read(cx).channel_id;
self.channel_chat = Some(chat);
let channel_name = ChannelStore::global(cx)
@@ -203,7 +198,11 @@ impl MessageEditor {
});
}
pub fn take_message(&mut self, cx: &mut ViewContext<Self>) -> MessageParams {
pub fn take_message(
&mut self,
window: &mut Window,
cx: &mut ModelContext<Self>,
) -> MessageParams {
self.editor.update(cx, |editor, cx| {
let highlights = editor.text_highlights::<Self>(cx);
let text = editor.text(cx);
@@ -218,7 +217,7 @@ impl MessageEditor {
Vec::new()
};
editor.clear(cx);
editor.clear(window, cx);
self.mentions.clear();
let reply_to_message_id = std::mem::take(&mut self.reply_to_message_id);
@@ -232,13 +231,14 @@ impl MessageEditor {
fn on_buffer_event(
&mut self,
buffer: Model<Buffer>,
buffer: &Model<Buffer>,
event: &language::BufferEvent,
cx: &mut ViewContext<Self>,
window: &mut Window,
cx: &mut ModelContext<Self>,
) {
if let language::BufferEvent::Reparsed | language::BufferEvent::Edited = event {
let buffer = buffer.read(cx).snapshot();
self.mentions_task = Some(cx.spawn(|this, cx| async move {
self.mentions_task = Some(cx.spawn_in(window, |this, cx| async move {
cx.background_executor()
.timer(MENTIONS_DEBOUNCE_INTERVAL)
.await;
@@ -251,13 +251,14 @@ impl MessageEditor {
&mut self,
buffer: &Model<Buffer>,
end_anchor: Anchor,
cx: &mut ViewContext<Self>,
window: &mut Window,
cx: &mut ModelContext<Self>,
) -> Task<Result<Vec<Completion>>> {
if let Some((start_anchor, query, candidates)) =
self.collect_mention_candidates(buffer, end_anchor, cx)
{
if !candidates.is_empty() {
return cx.spawn(|_, cx| async move {
return cx.spawn_in(window, |_, cx| async move {
Ok(Self::resolve_completions_for_candidates(
&cx,
query.as_str(),
@@ -274,7 +275,7 @@ impl MessageEditor {
self.collect_emoji_candidates(buffer, end_anchor, cx)
{
if !candidates.is_empty() {
return cx.spawn(|_, cx| async move {
return cx.spawn_in(window, |_, cx| async move {
Ok(Self::resolve_completions_for_candidates(
&cx,
query.as_str(),
@@ -319,6 +320,7 @@ impl MessageEditor {
server_id: LanguageServerId(0), // TODO: Make this optional or something?
lsp_completion: Default::default(), // TODO: Make this optional or something?
confirm: None,
resolved: true,
}
})
.collect()
@@ -347,7 +349,7 @@ impl MessageEditor {
&mut self,
buffer: &Model<Buffer>,
end_anchor: Anchor,
cx: &mut ViewContext<Self>,
cx: &mut ModelContext<Self>,
) -> Option<(Anchor, String, Vec<StringMatchCandidate>)> {
let end_offset = end_anchor.to_offset(buffer.read(cx));
@@ -396,7 +398,7 @@ impl MessageEditor {
&mut self,
buffer: &Model<Buffer>,
end_anchor: Anchor,
cx: &mut ViewContext<Self>,
cx: &mut ModelContext<Self>,
) -> Option<(Anchor, String, &'static [StringMatchCandidate])> {
static EMOJI_FUZZY_MATCH_CANDIDATES: LazyLock<Vec<StringMatchCandidate>> =
LazyLock::new(|| {
@@ -454,7 +456,7 @@ impl MessageEditor {
}
async fn find_mentions(
this: WeakView<MessageEditor>,
this: WeakModel<MessageEditor>,
buffer: BufferSnapshot,
mut cx: AsyncWindowContext,
) {
@@ -514,7 +516,7 @@ impl MessageEditor {
}
impl Render for MessageEditor {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
fn render(&mut self, _: &mut Window, cx: &mut ModelContext<Self>) -> impl IntoElement {
let settings = ThemeSettings::get_global(cx);
let text_style = TextStyle {
color: if self.editor.read(cx).read_only(cx) {

File diff suppressed because it is too large Load Diff

View File

@@ -6,8 +6,8 @@ use client::{
use fuzzy::{match_strings, StringMatchCandidate};
use gpui::{
actions, anchored, deferred, div, AppContext, ClipboardItem, DismissEvent, EventEmitter,
FocusableView, Model, ParentElement, Render, Styled, Subscription, Task, View, ViewContext,
VisualContext, WeakView,
Focusable, Model, ModelContext, ParentElement, Render, Styled, Subscription, Task, WeakModel,
Window,
};
use picker::{Picker, PickerDelegate};
use std::sync::Arc;
@@ -26,7 +26,7 @@ actions!(
);
pub struct ChannelModal {
picker: View<Picker<ChannelModalDelegate>>,
picker: Model<Picker<ChannelModalDelegate>>,
channel_store: Model<ChannelStore>,
channel_id: ChannelId,
}
@@ -37,11 +37,12 @@ impl ChannelModal {
channel_store: Model<ChannelStore>,
channel_id: ChannelId,
mode: Mode,
cx: &mut ViewContext<Self>,
window: &mut Window,
cx: &mut ModelContext<Self>,
) -> Self {
cx.observe(&channel_store, |_, _, cx| cx.notify()).detach();
let channel_modal = cx.view().downgrade();
let picker = cx.new_view(|cx| {
let channel_modal = cx.model().downgrade();
let picker = cx.new_model(|cx| {
Picker::uniform_list(
ChannelModalDelegate {
channel_modal,
@@ -57,6 +58,7 @@ impl ChannelModal {
has_all_members: false,
mode,
},
window,
cx,
)
.modal(false)
@@ -69,27 +71,32 @@ impl ChannelModal {
}
}
fn toggle_mode(&mut self, _: &ToggleMode, cx: &mut ViewContext<Self>) {
fn toggle_mode(&mut self, _: &ToggleMode, window: &mut Window, cx: &mut ModelContext<Self>) {
let mode = match self.picker.read(cx).delegate.mode {
Mode::ManageMembers => Mode::InviteMembers,
Mode::InviteMembers => Mode::ManageMembers,
};
self.set_mode(mode, cx);
self.set_mode(mode, window, cx);
}
fn set_mode(&mut self, mode: Mode, cx: &mut ViewContext<Self>) {
fn set_mode(&mut self, mode: Mode, window: &mut Window, cx: &mut ModelContext<Self>) {
self.picker.update(cx, |picker, cx| {
let delegate = &mut picker.delegate;
delegate.mode = mode;
delegate.selected_index = 0;
picker.set_query("", cx);
picker.update_matches(picker.query(cx), cx);
picker.set_query("", window, cx);
picker.update_matches(picker.query(cx), window, cx);
cx.notify()
});
cx.notify()
}
fn set_channel_visibility(&mut self, selection: &ToggleState, cx: &mut ViewContext<Self>) {
fn set_channel_visibility(
&mut self,
selection: &ToggleState,
_: &mut Window,
cx: &mut ModelContext<Self>,
) {
self.channel_store.update(cx, |channel_store, cx| {
channel_store
.set_channel_visibility(
@@ -105,7 +112,7 @@ impl ChannelModal {
});
}
fn dismiss(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) {
fn dismiss(&mut self, _: &menu::Cancel, _: &mut Window, cx: &mut ModelContext<Self>) {
cx.emit(DismissEvent);
}
}
@@ -113,14 +120,14 @@ impl ChannelModal {
impl EventEmitter<DismissEvent> for ChannelModal {}
impl ModalView for ChannelModal {}
impl FocusableView for ChannelModal {
impl Focusable for ChannelModal {
fn focus_handle(&self, cx: &AppContext) -> gpui::FocusHandle {
self.picker.focus_handle(cx)
}
}
impl Render for ChannelModal {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
fn render(&mut self, _: &mut Window, cx: &mut ModelContext<Self>) -> impl IntoElement {
let channel_store = self.channel_store.read(cx);
let Some(channel) = channel_store.channel_for_id(self.channel_id) else {
return div();
@@ -169,7 +176,7 @@ impl Render for ChannelModal {
Some(
Button::new("copy-link", "Copy Link")
.label_size(LabelSize::Small)
.on_click(cx.listener(move |this, _, cx| {
.on_click(cx.listener(move |this, _, _, cx| {
if let Some(channel) = this
.channel_store
.read(cx)
@@ -197,8 +204,8 @@ impl Render for ChannelModal {
this.border_color(cx.theme().colors().border)
})
.child(Label::new("Manage Members"))
.on_click(cx.listener(|this, _, cx| {
this.set_mode(Mode::ManageMembers, cx);
.on_click(cx.listener(|this, _, window, cx| {
this.set_mode(Mode::ManageMembers, window, cx);
})),
)
.child(
@@ -212,8 +219,8 @@ impl Render for ChannelModal {
this.border_color(cx.theme().colors().border)
})
.child(Label::new("Invite Members"))
.on_click(cx.listener(|this, _, cx| {
this.set_mode(Mode::InviteMembers, cx);
.on_click(cx.listener(|this, _, window, cx| {
this.set_mode(Mode::InviteMembers, window, cx);
})),
),
),
@@ -229,7 +236,7 @@ pub enum Mode {
}
pub struct ChannelModalDelegate {
channel_modal: WeakView<ChannelModal>,
channel_modal: WeakModel<ChannelModal>,
matching_users: Vec<Arc<User>>,
matching_member_indices: Vec<usize>,
user_store: Model<UserStore>,
@@ -240,13 +247,13 @@ pub struct ChannelModalDelegate {
match_candidates: Vec<StringMatchCandidate>,
members: Vec<ChannelMembership>,
has_all_members: bool,
context_menu: Option<(View<ContextMenu>, Subscription)>,
context_menu: Option<(Model<ContextMenu>, Subscription)>,
}
impl PickerDelegate for ChannelModalDelegate {
type ListItem = ListItem;
fn placeholder_text(&self, _cx: &mut WindowContext) -> Arc<str> {
fn placeholder_text(&self, _window: &mut Window, _cx: &mut AppContext) -> Arc<str> {
"Search collaborator by username...".into()
}
@@ -261,11 +268,21 @@ impl PickerDelegate for ChannelModalDelegate {
self.selected_index
}
fn set_selected_index(&mut self, ix: usize, _: &mut ViewContext<Picker<Self>>) {
fn set_selected_index(
&mut self,
ix: usize,
_window: &mut Window,
_: &mut ModelContext<Picker<Self>>,
) {
self.selected_index = ix;
}
fn update_matches(&mut self, query: String, cx: &mut ViewContext<Picker<Self>>) -> Task<()> {
fn update_matches(
&mut self,
query: String,
window: &mut Window,
cx: &mut ModelContext<Picker<Self>>,
) -> Task<()> {
match self.mode {
Mode::ManageMembers => {
if self.has_all_members {
@@ -284,7 +301,7 @@ impl PickerDelegate for ChannelModalDelegate {
cx.background_executor().clone(),
));
cx.spawn(|picker, mut cx| async move {
cx.spawn_in(window, |picker, mut cx| async move {
picker
.update(&mut cx, |picker, cx| {
let delegate = &mut picker.delegate;
@@ -300,7 +317,7 @@ impl PickerDelegate for ChannelModalDelegate {
let search_members = self.channel_store.update(cx, |store, cx| {
store.fuzzy_search_members(self.channel_id, query.clone(), 100, cx)
});
cx.spawn(|picker, mut cx| async move {
cx.spawn_in(window, |picker, mut cx| async move {
async {
let members = search_members.await?;
picker.update(&mut cx, |picker, cx| {
@@ -322,7 +339,7 @@ impl PickerDelegate for ChannelModalDelegate {
let search_users = self
.user_store
.update(cx, |store, cx| store.fuzzy_search_users(query, cx));
cx.spawn(|picker, mut cx| async move {
cx.spawn_in(window, |picker, mut cx| async move {
async {
let users = search_users.await?;
picker.update(&mut cx, |picker, cx| {
@@ -338,26 +355,26 @@ impl PickerDelegate for ChannelModalDelegate {
}
}
fn confirm(&mut self, _: bool, cx: &mut ViewContext<Picker<Self>>) {
fn confirm(&mut self, _: bool, window: &mut Window, cx: &mut ModelContext<Picker<Self>>) {
if let Some(selected_user) = self.user_at_index(self.selected_index) {
if Some(selected_user.id) == self.user_store.read(cx).current_user().map(|user| user.id)
{
return;
}
match self.mode {
Mode::ManageMembers => self.show_context_menu(self.selected_index, cx),
Mode::ManageMembers => self.show_context_menu(self.selected_index, window, cx),
Mode::InviteMembers => match self.member_status(selected_user.id, cx) {
Some(proto::channel_member::Kind::Invitee) => {
self.remove_member(selected_user.id, cx);
self.remove_member(selected_user.id, window, cx);
}
Some(proto::channel_member::Kind::Member) => {}
None => self.invite_member(selected_user, cx),
None => self.invite_member(selected_user, window, cx),
},
}
}
}
fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>) {
fn dismissed(&mut self, _: &mut Window, cx: &mut ModelContext<Picker<Self>>) {
if self.context_menu.is_none() {
self.channel_modal
.update(cx, |_, cx| {
@@ -371,7 +388,8 @@ impl PickerDelegate for ChannelModalDelegate {
&self,
ix: usize,
selected: bool,
cx: &mut ViewContext<Picker<Self>>,
_: &mut Window,
cx: &mut ModelContext<Picker<Self>>,
) -> Option<Self::ListItem> {
let user = self.user_at_index(ix)?;
let membership = self.member_at_index(ix);
@@ -470,33 +488,39 @@ impl ChannelModalDelegate {
&mut self,
user_id: UserId,
new_role: ChannelRole,
cx: &mut ViewContext<Picker<Self>>,
window: &mut Window,
cx: &mut ModelContext<Picker<Self>>,
) -> Option<()> {
let update = self.channel_store.update(cx, |store, cx| {
store.set_member_role(self.channel_id, user_id, new_role, cx)
});
cx.spawn(|picker, mut cx| async move {
cx.spawn_in(window, |picker, mut cx| async move {
update.await?;
picker.update(&mut cx, |picker, cx| {
picker.update_in(&mut cx, |picker, window, cx| {
let this = &mut picker.delegate;
if let Some(member) = this.members.iter_mut().find(|m| m.user.id == user_id) {
member.role = new_role;
}
cx.focus_self();
cx.focus_self(window);
cx.notify();
})
})
.detach_and_prompt_err("Failed to update role", cx, |_, _| None);
.detach_and_prompt_err("Failed to update role", window, cx, |_, _, _| None);
Some(())
}
fn remove_member(&mut self, user_id: UserId, cx: &mut ViewContext<Picker<Self>>) -> Option<()> {
fn remove_member(
&mut self,
user_id: UserId,
window: &mut Window,
cx: &mut ModelContext<Picker<Self>>,
) -> Option<()> {
let update = self.channel_store.update(cx, |store, cx| {
store.remove_member(self.channel_id, user_id, cx)
});
cx.spawn(|picker, mut cx| async move {
cx.spawn_in(window, |picker, mut cx| async move {
update.await?;
picker.update(&mut cx, |picker, cx| {
picker.update_in(&mut cx, |picker, window, cx| {
let this = &mut picker.delegate;
if let Some(ix) = this.members.iter_mut().position(|m| m.user.id == user_id) {
this.members.remove(ix);
@@ -514,20 +538,25 @@ impl ChannelModalDelegate {
.selected_index
.min(this.matching_member_indices.len().saturating_sub(1));
picker.focus(cx);
picker.focus(window, cx);
cx.notify();
})
})
.detach_and_prompt_err("Failed to remove member", cx, |_, _| None);
.detach_and_prompt_err("Failed to remove member", window, cx, |_, _, _| None);
Some(())
}
fn invite_member(&mut self, user: Arc<User>, cx: &mut ViewContext<Picker<Self>>) {
fn invite_member(
&mut self,
user: Arc<User>,
window: &mut Window,
cx: &mut ModelContext<Picker<Self>>,
) {
let invite_member = self.channel_store.update(cx, |store, cx| {
store.invite_member(self.channel_id, user.id, ChannelRole::Member, cx)
});
cx.spawn(|this, mut cx| async move {
cx.spawn_in(window, |this, mut cx| async move {
invite_member.await?;
this.update(&mut cx, |this, cx| {
@@ -544,25 +573,30 @@ impl ChannelModalDelegate {
cx.notify();
})
})
.detach_and_prompt_err("Failed to invite member", cx, |_, _| None);
.detach_and_prompt_err("Failed to invite member", window, cx, |_, _, _| None);
}
fn show_context_menu(&mut self, ix: usize, cx: &mut ViewContext<Picker<Self>>) {
fn show_context_menu(
&mut self,
ix: usize,
window: &mut Window,
cx: &mut ModelContext<Picker<Self>>,
) {
let Some(membership) = self.member_at_index(ix) else {
return;
};
let user_id = membership.user.id;
let picker = cx.view().clone();
let context_menu = ContextMenu::build(cx, |mut menu, _cx| {
let picker = cx.model().clone();
let context_menu = ContextMenu::build(window, cx, |mut menu, _window, _cx| {
let role = membership.role;
if role == ChannelRole::Admin || role == ChannelRole::Member {
let picker = picker.clone();
menu = menu.entry("Demote to Guest", None, move |cx| {
menu = menu.entry("Demote to Guest", None, move |window, cx| {
picker.update(cx, |picker, cx| {
picker
.delegate
.set_user_role(user_id, ChannelRole::Guest, cx);
.set_user_role(user_id, ChannelRole::Guest, window, cx);
})
});
}
@@ -575,22 +609,22 @@ impl ChannelModalDelegate {
"Demote to Member"
};
menu = menu.entry(label, None, move |cx| {
menu = menu.entry(label, None, move |window, cx| {
picker.update(cx, |picker, cx| {
picker
.delegate
.set_user_role(user_id, ChannelRole::Member, cx);
.set_user_role(user_id, ChannelRole::Member, window, cx);
})
});
}
if role == ChannelRole::Member || role == ChannelRole::Guest {
let picker = picker.clone();
menu = menu.entry("Promote to Admin", None, move |cx| {
menu = menu.entry("Promote to Admin", None, move |window, cx| {
picker.update(cx, |picker, cx| {
picker
.delegate
.set_user_role(user_id, ChannelRole::Admin, cx);
.set_user_role(user_id, ChannelRole::Admin, window, cx);
})
});
};
@@ -598,20 +632,24 @@ impl ChannelModalDelegate {
menu = menu.separator();
menu = menu.entry("Remove from Channel", None, {
let picker = picker.clone();
move |cx| {
move |window, cx| {
picker.update(cx, |picker, cx| {
picker.delegate.remove_member(user_id, cx);
picker.delegate.remove_member(user_id, window, cx);
})
}
});
menu
});
cx.focus_view(&context_menu);
let subscription = cx.subscribe(&context_menu, |picker, _, _: &DismissEvent, cx| {
picker.delegate.context_menu = None;
picker.focus(cx);
cx.notify();
});
window.focus(&context_menu.focus_handle(cx));
let subscription = cx.subscribe_in(
&context_menu,
window,
|picker, _, _: &DismissEvent, window, cx| {
picker.delegate.context_menu = None;
picker.focus(window, cx);
cx.notify();
},
);
self.context_menu = Some((context_menu, subscription));
}
}

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