Compare commits

...

420 Commits

Author SHA1 Message Date
Cole Miller
4d9de293f9 Merge origin/main 2025-01-27 11:49:23 -05:00
João Marcos
2256c21841 Silence Rust-Analyzer false-positive (#23724)
Release Notes:

- N/A
2025-01-27 16:38:03 +00:00
Marshall Bowers
ad49f71e6e assistant2: Add debug logging for initialization issues (#23722)
This PR adds some logging so we can debug the issues some folks have
been having with Assistant2 not getting initialized properly.

All the logs are prefixed with `[assistant2-debug]` so they're easier to
pick out of the logs, as well as find them later to clean up once we've
diagnosed the issue.

Release Notes:

- N/A
2025-01-27 16:27:59 +00:00
Danilo Leal
98ea0587df assistant: Preserve selection focus in the model selector (#23713)
This PR fixes an incorrect behavior in the model selector where we were
removing the focus from the selected item back to the very first item of
the list. Now, if you click/hit return on an item, focus is preserved
there. In other words, focus is always initially in the selected item.

### Before

https://github.com/user-attachments/assets/62b72b1f-4e32-4b4a-adff-dcf9a2c13a28

### After

https://github.com/user-attachments/assets/a8528933-da01-481a-96f3-0173a39a03c0

Release Notes:

- N/A
2025-01-27 12:29:08 -03:00
Danilo Leal
93b62e0ed4 assistant: Fix model selector label shift (#23717)
This PR caps the width of the model label in the selector trigger to a
certain size. This is fix the behavior of the popover dancing around,
given its popover position is anchored to a certain edge of the trigger,
and if the trigger size increases while you select different models with
different name lengths, the popover dances around.

### Before


https://github.com/user-attachments/assets/0854fa2b-9eb2-45fb-886d-bde1cd644dcf

### After

Note how even though the second item has the largest label, the popover
stays in place.


https://github.com/user-attachments/assets/06b60030-65dc-4f06-b486-3045042bbff0

Fixing that then means truncating the model name to keep it constrained
into a max-width.

<img width="500" alt="Screenshot 2025-01-27 at 11 38 14 AM"
src="https://github.com/user-attachments/assets/94ce9cc6-848c-4dac-86b8-321da75c3af3"
/>

Release Notes:

- N/A
2025-01-27 12:28:58 -03:00
Danilo Leal
91f44725d9 assistant2: Fix model selector position (#23721)
This PR makes the model selector not render on top of its trigger.

<img width="800" alt="Screenshot 2025-01-27 at 12 02 01 PM"
src="https://github.com/user-attachments/assets/76c5ac6f-ae27-4b3d-a80a-3027042c75f8"
/>

Release Notes:

- N/A
2025-01-27 12:28:45 -03:00
Peter Tripp
b58c994706 docs: Example OpenAI model config missing version key (#23720) 2025-01-27 15:17:51 +00:00
Andrew Borg (Kashin)
9f3dbd6fa3 Make ZED_WORKTREE_ROOT always point to a directory or is not set (#23150)
When the out-of-tree file like `tasks.json` is-focus, tasks spawn with `ZED_WORKTREE_ROOT` unset.
2025-01-27 10:10:39 -05:00
Peter Tripp
33921362cf docs: Fix error in OpenAI configuring custom models (#23716) 2025-01-27 14:47:09 +00:00
Peter Tripp
f6d286c7db openai: Add back O1-Preview (#23715)
Follow-up to: https://github.com/zed-industries/zed/pull/23425
2025-01-27 14:44:12 +00:00
Kirill Bulatov
5bde053b0d Avoid panics when normalizing completion label with invalid ranges (#23712)
Dev builds show panics related to completion label normalization

<details>
<summary>Panic</summary>

```
index out of bounds: the len is 103 but the index is 103
zed::reliability::init_panic_hook::{{closure}}::h78130eff43c84f6f+110375521
std::panicking::rust_panic_with_hook::hfe205f6954b2c97b+87457752
std::panicking::begin_panic_handler::{{closure}}::h6cb44b3a50f28c44+87456967
std::sys::backtrace::__rust_end_short_backtrace::hf1c1f2a92799bb0e+87449337
rust_begin_unwind+87456084
core::panicking::panic_fmt::h3d8fc78294164da7+7033011
core::panicking::panic_bounds_check::h9397cb495d89a72d+7033511
project::lsp_store::ensure_uniform_list_compatible_label::haf80316ce11edd67+72663592
project::lsp_store::populate_labels_for_completions::{{closure}}::hc93c3c540ef7d2d6+72642960
project::lsp_store::LspStore::completions::{{closure}}::{{closure}}::hb4b5432e24432ca8+72336627
async_task::raw::RawTask<F,T,S,M>::run::hf444c3dc07dd583b+68504803
<gpui::platform::linux::wayland::client::WaylandClient as gpui::platform::linux::platform::LinuxClient>::run::hbf5a316eb781a10d+50646579
gpui::platform::linux::platform::<impl gpui::platform::Platform for P>::run::hc85518d4552fc4cd+50496669
gpui::app::App::run::hca4e2eaf984ca6f6+109905269
zed::main::h849467ac1a6d32c9+110413414
std::sys::backtrace::__rust_begin_short_backtrace::h81b5ee155a7cf505+110835475
std::rt::lang_start::{{closure}}::h48a83f884cfb6865+110834761
std::rt::lang_start_internal::h5e7c81cecd7f0954+87382485
main+110425932
__libc_start_call_main+22789462491720
__libc_start_main_alias_1+22789462491915
_start+10436606
```
</details>

This can only happen when either `label.runs` or `label.filter_range`
has a range that's larger than the label text, which is an error.
Instead of panicking, log such errors and fall back to last index (which
is not really helpful, but still).

Release Notes:

- N/A
2025-01-27 14:18:39 +00:00
Matin Aniss
06d00b940d gpui: Support windows dark mode title bar (#23700)
This will allow Windows GPUI applications that utilise the native title
bar to support the dark mode variant when the system user has dark mode
enabled in their system settings. [Related Win32
information](https://learn.microsoft.com/en-us/windows/apps/desktop/modernize/ui/apply-windows-themes)

| Before  | After |
| ------------- | ------------- |
|
![image](https://github.com/user-attachments/assets/50f3d131-8f41-4b91-8012-f8120b150033)
|
![image](https://github.com/user-attachments/assets/a36a6c6b-a469-49ba-85a8-9b55db9ea20f)
|

Release Notes:

- N/A

---------

Co-authored-by: 张小白 <364772080@qq.com>
2025-01-27 20:07:28 +08:00
loczek
6293b20fd0 Add auto-completion support for snippet files (#23698)
Release Notes:

- Added auto-completion support for snippet files.


![image](https://github.com/user-attachments/assets/ad165fc7-a6e7-426c-8892-f7004515dfc7)
2025-01-27 12:32:22 +01:00
Michael Sloan
6e9ea47849 Fix completions menu scroll when y_flipped and edit prediction arrives (#23580)
Release Notes:

- N/A
2025-01-27 00:36:21 -07:00
Mikayla Maki
9cae96f82f Remove more references to 'model' in GPUI APIs (#23693)
Release Notes:

- N/A
2025-01-27 04:00:27 +00:00
Mikayla Maki
a6b1514246 Fix missed renames in #22632 (#23688)
Fix a bug where a GPUI macro still used `ModelContext`
Rename `AsyncAppContext` -> `AsyncApp`
Rename update_model, read_model, insert_model, and reserve_model to
update_entity, read_entity, insert_entity, and reserve_entity

Release Notes:

- N/A
2025-01-26 23:37:34 +00:00
Michael Sloan
83141d07e9 Fix escaping of alt-\ in docs for triggering inline completion (#23684)
Release Notes:

- N/A
2025-01-26 20:26:11 +00:00
Kirill Bulatov
7d30dda557 Restore the default release channel (#23671)
Release Notes:

- N/A
2025-01-26 09:19:24 +00:00
Joseph T. Lyons
0c8ee21e22 Clean up duplicate dock-retrieval code (#23670)
Release Notes:

- N/A
2025-01-26 09:11:38 +00:00
Maksim Bondarenkov
64a5153bb5 collab: Make unsupported for MinGW toolchain (#23518)
Closes #23451

reverts #23117 for MinGW. collab can't be compiled for MinGW because
webrtc itself doesn't support MinGW compilers

Release Notes:

- N/A
2025-01-26 11:01:20 +02:00
张小白
5d005a7621 Canonicalize paths when running tests (#23655)
In the Windows test environment, the paths generated by `temp_tree()`
are symlink paths, which causes certain tests to fail.

I later noticed that when opening a project, we seem to always use
`canonicalize` to normalize the paths, as shown here:
https://github.com/zed-industries/zed/pull/21039.

This PR adopts a similar approach for the test environment to address
the issue.

Release Notes:

- N/A
2025-01-26 14:56:07 +08:00
Michael Sloan
0f8e2e3811 Remove scripts used in merging the refactoring (#23669)
Release Notes:

- N/A
2025-01-26 06:33:11 +00:00
Michael Sloan
84b945e89d Revert making keybinding display in Mac menus use standard precedence (#23661)
Closes #23621

Change was in #23378. Also adds a comment to clarify why this is
inconsistent with all other uses of `bindings_for_action`.

Release Notes:

- N/A
2025-01-26 04:35:01 +00:00
Nathan Sobo
6fca1d2b0b Eliminate GPUI View, ViewContext, and WindowContext types (#22632)
There's still a bit more work to do on this, but this PR is compiling
(with warnings) after eliminating the key types. When the tasks below
are complete, this will be the new narrative for GPUI:

- `Entity<T>` - This replaces `View<T>`/`Model<T>`. It represents a unit
of state, and if `T` implements `Render`, then `Entity<T>` implements
`Element`.
- `&mut App` This replaces `AppContext` and represents the app.
- `&mut Context<T>` This replaces `ModelContext` and derefs to `App`. It
is provided by the framework when updating an entity.
- `&mut Window` Broken out of `&mut WindowContext` which no longer
exists. Every method that once took `&mut WindowContext` now takes `&mut
Window, &mut App` and every method that took `&mut ViewContext<T>` now
takes `&mut Window, &mut Context<T>`

Not pictured here are the two other failed attempts. It's been quite a
month!

Tasks:

- [x] Remove `View`, `ViewContext`, `WindowContext` and thread through
`Window`
- [x] [@cole-miller @mikayla-maki] Redraw window when entities change
- [x] [@cole-miller @mikayla-maki] Get examples and Zed running
- [x] [@cole-miller @mikayla-maki] Fix Zed rendering
- [x] [@mikayla-maki] Fix todo! macros and comments
- [x] Fix a bug where the editor would not be redrawn because of view
caching
- [x] remove publicness window.notify() and replace with
`AppContext::notify`
- [x] remove `observe_new_window_models`, replace with
`observe_new_models` with an optional window
- [x] Fix a bug where the project panel would not be redrawn because of
the wrong refresh() call being used
- [x] Fix the tests
- [x] Fix warnings by eliminating `Window` params or using `_`
- [x] Fix conflicts
- [x] Simplify generic code where possible
- [x] Rename types
- [ ] Update docs

### issues post merge

- [x] Issues switching between normal and insert mode
- [x] Assistant re-rendering failure
- [x] Vim test failures
- [x] Mac build issue



Release Notes:

- N/A

---------

Co-authored-by: Antonio Scandurra <me@as-cii.com>
Co-authored-by: Cole Miller <cole@zed.dev>
Co-authored-by: Mikayla <mikayla@zed.dev>
Co-authored-by: Joseph <joseph@zed.dev>
Co-authored-by: max <max@zed.dev>
Co-authored-by: Michael Sloan <michael@zed.dev>
Co-authored-by: Mikayla Maki <mikaylamaki@Mikaylas-MacBook-Pro.local>
Co-authored-by: Mikayla <mikayla.c.maki@gmail.com>
Co-authored-by: joão <joao@zed.dev>
2025-01-26 03:02:45 +00:00
Joseph T. Lyons
21b4a0d50e Adjust editor: open excerpts split key binding (#23646)
The pattern in Zed and in other editors is to use `cmd` to modify some
file-opening action to open it in a split rather than in the current
pane.

- In the project pane, a `click` opens a file, and a `cmd-click` opens
it in a split
- In the file finder, `enter` opens the file, and a `cmd-enter` opens it
in a split

It makes sense to me that if `alt-enter` opens a file from the excerpt,
that `cmd-alt-enter` opens it in a split, following the pattern above.

Note: I'm not auto-merging this, as others might disagree.
Note: I didn't touch the Vim binding.

Release Notes:

- Breaking Change: Changed `editor: open excerpts split` key binding to
`cmd-alt-enter` on macOS and `ctrl-alt-enter` on Linux.
2025-01-25 17:06:53 -05:00
Kirill Bulatov
da2bd4b8e9 Rework go to line infrastructure (#23654)
Closes https://github.com/zed-industries/zed/issues/12024


https://github.com/user-attachments/assets/60ea3dbd-b594-4bf5-a44d-4bff925b815f

* Fixes incorrect line selection for certain corner cases

Before:

<img width="1728" alt="image"
src="https://github.com/user-attachments/assets/35aaee6c-c120-4bf1-9355-448a29d1b9b5"
/>

After:

<img width="1728" alt="image"
src="https://github.com/user-attachments/assets/abd97339-4594-4e8e-8605-50d74581ae86"
/>


* Reworks https://github.com/zed-industries/zed/pull/16420 to display
selection length with less performance overhead.
Improves the performance more, doing a single selections loop instead of
two.

* Fixes incorrect caret position display when text contains UTF-8 chars
with size > 1
Also fixes tooltop values for this case

* Fixes go to line to treat UTF-8 chars with size > 1 properly when
navigating

* Adds a way to fill go to line text editor with its tooltip on `Tab`

Release Notes:

- Fixed incorrect UTF-8 characters handling in `GoToLine` and caret
position
2025-01-25 19:24:19 +00:00
Joseph T. Lyons
7c0a39daa6 Add key binding for editor: open selections in multibuffer (#23648)
Follow-up to: https://github.com/zed-industries/zed/pull/23644

- Existing: `alt-enter` to open files from multi-buffer selections
- New: `alt-enter` to open multi-buffer from file selections

I updated the original PR changelog line.

Release Notes:

- N/A
2025-01-25 10:27:14 +00:00
Kirill Bulatov
392b95b179 Use proper path separator for multi buffer headers (#23635)
Before: 


![bad](https://github.com/user-attachments/assets/ab20836a-b3bb-4b33-8ce4-eecc79d5f02f)


After:


![good](https://github.com/user-attachments/assets/40c328d1-2ef1-4a9b-9de9-9be9bba26756)


Release Notes:

- N/A
2025-01-25 10:35:22 +02:00
Michael Sloan
5f417eda25 Fix repl "plain" terminals enqueing events without ever dequeuing (#23641)
Bringing back part of #23631 which was reverted in #23636. Different but
related memory leak to what's described in #23008

Release Notes:

- N/A
2025-01-25 01:08:34 -07:00
Joseph T. Lyons
75b507d38a Open selections in multi buffer (#23644)
Closes https://github.com/zed-industries/zed/issues/5126

Release Notes:

- Added an `editor: open selections in multibuffer` command.
2025-01-25 07:45:09 +00:00
Kirill Bulatov
f5102838f3 Revert terminal memory leak fixes (#23636)
New mechanism had introduced the following regressions:

* Windows tasks are not registering child exit properly, sometimes
getting stuck in dirty state (even with
https://github.com/zed-industries/zed/pull/23631):


https://github.com/user-attachments/assets/6d406f17-aa76-4012-9c3b-be72d6d5beae

* Overall, the terminal editing started to feel more sluggish, esp. in
regards to deletions (ctrl-w and backspace), tested on macOS:


https://github.com/user-attachments/assets/3a69fe2e-e394-45e8-8f51-0f5ac396cb24


Release Notes:

- N/A
2025-01-24 21:35:24 -07:00
Conrad Irwin
b7c6ffa6c2 Fix panic when multi-cursor edit attempted in deleted hunk (#23633)
Release Notes:

- N/A
2025-01-24 16:14:19 -07:00
Agus Zubiaga
ba16b4eb65 assistant2: Show accept terms UI in thread empty state (#23630)
<img
src="https://github.com/user-attachments/assets/cea93cfb-8a40-48c4-9d90-f1751c79603b"
width=400>



Release Notes:

- N/A

---------

Co-authored-by: Danilo <danilo@zed.dev>
2025-01-24 19:34:46 -03:00
Michael Sloan
ec5d02d5c2 Use send instead of feed on terminal events channel (#23631)
Potentially fixes a bug where tasks are not marked as finished.

Release Notes:

- N/A
2025-01-24 22:32:22 +00:00
Max Brunsfeld
d2c55cbe3d Rework diff rendering to allow putting the cursor into deleted text, soft-wrapping and scrolling deleted text correctly (#22994)
Closes #12553

* [x] Fix `diff_hunk_before`
* [x] Fix failure to show deleted text when expanding hunk w/ cursor on
second line of the hunk
* [x] Failure to expand diff hunk below the cursor.
* [x] Delete the whole file, and expand the diff. Backspace over the
deleted hunk, panic!
* [x] Go-to-line now counts the diff hunks, but it should not
* [x] backspace at the beginning of a deleted hunk deletes too much text
* [x] Indent guides are rendered incorrectly 
* [ ] Fix randomized multi buffer tests

Maybe:
* [ ] Buffer search should include deleted text (in vim mode it turns
out I use `/x` all the time to jump to the next x I can see).
* [ ] vim: should refuse to switch into insert mode if selection is
fully within a diff.
* [ ] vim `o` command when cursor is on last line of deleted hunk.
* [ ] vim `shift-o` on first line of deleted hunk moves cursor but
doesn't insert line
* [x] `enter` at end of diff hunk inserts a new line but doesn't move
cursor
* [x] (`shift-enter` at start of diff hunk does nothing)
* [ ] Inserting a line just before an expanded hunk collapses it

Release Notes:


- Improved diff rendering, allowing you to navigate with your cursor
inside of deleted text in diff hunks.

---------

Co-authored-by: Conrad <conrad@zed.dev>
Co-authored-by: Cole <cole@zed.dev>
Co-authored-by: Mikayla <mikayla@zed.dev>
Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
Co-authored-by: Michael <michael@zed.dev>
Co-authored-by: Agus <agus@zed.dev>
Co-authored-by: João <joao@zed.dev>
2025-01-24 14:18:22 -07:00
renovate[bot]
1fdae4bae0 Update Rust crate tokio to v1.43.0 (#22882)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [tokio](https://tokio.rs)
([source](https://redirect.github.com/tokio-rs/tokio)) | dependencies |
minor | `1.42.0` -> `1.43.0` |
| [tokio](https://tokio.rs)
([source](https://redirect.github.com/tokio-rs/tokio)) |
workspace.dependencies | minor | `1.42.0` -> `1.43.0` |

---

### Release Notes

<details>
<summary>tokio-rs/tokio (tokio)</summary>

###
[`v1.43.0`](https://redirect.github.com/tokio-rs/tokio/releases/tag/tokio-1.43.0):
Tokio v1.43.0

[Compare
Source](https://redirect.github.com/tokio-rs/tokio/compare/tokio-1.42.0...tokio-1.43.0)

### 1.43.0 (Jan 8th, 2025)

##### Added

-   net: add `UdpSocket::peek` methods ([#&#8203;7068])
-   net: add support for Haiku OS ([#&#8203;7042])
-   process: add `Command::into_std()` ([#&#8203;7014])
-   signal: add `SignalKind::info` on illumos ([#&#8203;6995])
-   signal: add support for realtime signals on illumos ([#&#8203;7029])

##### Fixed

- io: don't call `set_len` before initializing vector in `Blocking`
([#&#8203;7054])
- macros: suppress `clippy::needless_return` in `#[tokio::main]`
([#&#8203;6874])
-   runtime: fix thread parking on WebAssembly ([#&#8203;7041])

##### Changes

-   chore: use unsync loads for `unsync_load` ([#&#8203;7073])
-   io: use `Buf::put_bytes` in `Repeat` read impl ([#&#8203;7055])
-   task: drop the join waker of a task eagerly ([#&#8203;6986])

##### Changes to unstable APIs

- metrics: improve flexibility of H2Histogram Configuration
([#&#8203;6963])
-   taskdump: add accessor methods for backtrace ([#&#8203;6975])

##### Documented

- io: clarify `ReadBuf::uninit` allows initialized buffers as well
([#&#8203;7053])
- net: fix ambiguity in `TcpStream::try_write_vectored` docs
([#&#8203;7067])
-   runtime: fix `LocalRuntime` doc links ([#&#8203;7074])
- sync: extend documentation for `watch::Receiver::wait_for`
([#&#8203;7038])
-   sync: fix typos in `OnceCell` docs ([#&#8203;7047])

[#&#8203;6874]: https://redirect.github.com/tokio-rs/tokio/pull/6874

[#&#8203;6963]: https://redirect.github.com/tokio-rs/tokio/pull/6963

[#&#8203;6975]: https://redirect.github.com/tokio-rs/tokio/pull/6975

[#&#8203;6986]: https://redirect.github.com/tokio-rs/tokio/pull/6986

[#&#8203;6995]: https://redirect.github.com/tokio-rs/tokio/pull/6995

[#&#8203;7014]: https://redirect.github.com/tokio-rs/tokio/pull/7014

[#&#8203;7029]: https://redirect.github.com/tokio-rs/tokio/pull/7029

[#&#8203;7038]: https://redirect.github.com/tokio-rs/tokio/pull/7038

[#&#8203;7041]: https://redirect.github.com/tokio-rs/tokio/pull/7041

[#&#8203;7042]: https://redirect.github.com/tokio-rs/tokio/pull/7042

[#&#8203;7047]: https://redirect.github.com/tokio-rs/tokio/pull/7047

[#&#8203;7053]: https://redirect.github.com/tokio-rs/tokio/pull/7053

[#&#8203;7054]: https://redirect.github.com/tokio-rs/tokio/pull/7054

[#&#8203;7055]: https://redirect.github.com/tokio-rs/tokio/pull/7055

[#&#8203;7067]: https://redirect.github.com/tokio-rs/tokio/pull/7067

[#&#8203;7068]: https://redirect.github.com/tokio-rs/tokio/pull/7068

[#&#8203;7073]: https://redirect.github.com/tokio-rs/tokio/pull/7073

[#&#8203;7074]: https://redirect.github.com/tokio-rs/tokio/pull/7074

</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 these
updates again.

---

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

---

Release Notes:

- N/A

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

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-24 22:41:04 +02:00
Cole Miller
9d8d21a4dc git: Disable "stage all" checkbox when no entries (#23608)
Release Notes:

- N/A
2025-01-24 15:29:34 -05:00
Michael Sloan
40c18be541 Make editor autoscroll put cursor to the left of scrollbar not under (#23586)
Closes #19706

Release Notes:

- Improved editor horizontal autoscroll to now place the cursor to the
left of the scrollbar rather than under it.
2025-01-24 12:58:17 -07:00
Peter Tripp
15d041c97c Document elm-language-server not supporting linked_edits correctly (#23616) 2025-01-24 18:30:38 +00:00
Peter Tripp
ed54af337d Support yaml-language-server as formatter without lsp settings (#23612)
- Closes: https://github.com/zed-industries/zed/issues/20183
2025-01-24 12:51:16 -05:00
Nate Butler
d5c7e0b1e3 git_ui: Add keybinding for focusing the git panel (#23613)
Adds a keybinding for opening/toggling focus to the git panel

Release Notes:

- N/A
2025-01-24 17:20:26 +00:00
Piotr Osiewicz
77e9d01b39 task: Always use untruncated label if it is short (#23611)
Also changed rust tasks to be less mouthful.

Release Notes:

- Shortened Rust task labels.
- Task modal will now use full task label when it does not require
truncation.
2025-01-24 16:46:48 +00:00
Danilo Leal
7c2b17540b assistant2: Adjust empty state when there is no provider (#23609)
This PR add a "Configure a Provider" button if the user gets to the
assistant panel with no provider configured. Then, upon configuring it,
they'll see a similar welcome message.

| No provider | Empty state |
|--------|--------|
| <img width="1233" alt="Screenshot 2025-01-24 at 12 25 48 PM"
src="https://github.com/user-attachments/assets/2f3c602f-9e46-4c79-95cd-4bb3717f68a3"
/> | <img width="1233" alt="Screenshot 2025-01-24 at 12 26 01 PM"
src="https://github.com/user-attachments/assets/a4a204dd-9531-45ab-89a2-f1d84f375a7b"
/> |

Release Notes:

- N/A
2025-01-24 12:51:14 -03:00
Danilo Leal
fc3a871264 assistant2: Adjust spacing and icons on the context picker (#23607)
Just fine-tuning spacing, icon size and color, and ensure they're are
consistent throughout.

Release Notes:

- N/A
2025-01-24 12:34:11 -03:00
Piotr Osiewicz
8efed4c449 search: Move invalid UTF-8 errors to debug level (#23602)
Closes #ISSUE

Release Notes:

- N/A
2025-01-24 14:11:45 +01:00
Danilo Leal
7b69c4246a title_bar: Use an IconButton for the user menu (#23601)
That's specifically when we're not rendering the user menu with an
Avatar. We were previously rendering a `ButtonLike` with unnecessary
flex styles there. Just a little fine-tune.

Release Notes:

- N/A
2025-01-24 09:49:50 -03:00
Danilo Leal
ad63bdf65b assistant2: Disable the Submit button when missing requirements (#23598)
This PR disables the Assistant 2 Submit button when either there is no
message written in the editor or there's no model selected. To guide the
user, there will be a tooltip displayed on top of the button to indicate
what to do.

Release Notes:

- N/A
2025-01-24 09:44:20 -03:00
Danilo Leal
802d7421bc assistant: Adjust the ToS acceptance card design (#23599)
Just fine-tuning the copywriting and design here.

| Before | After |
|--------|--------|
| <img width="1233" alt="Screenshot 2025-01-24 at 9 28 30 AM"
src="https://github.com/user-attachments/assets/ca91a985-8a20-4ece-b0e4-3a6779db2fda"
/> | <img width="1233" alt="Screenshot 2025-01-24 at 9 27 49 AM"
src="https://github.com/user-attachments/assets/edc9c2ef-4ae0-4caf-a496-9887748673c9"
/> |

Release Notes:

- N/A
2025-01-24 09:44:09 -03:00
Danilo Leal
f2c2ed0ccd Allow the context menu to take an icon_color (#23600)
Doing this to enable customization in the Assistant 2 context picker.

Release Notes:

- N/A
2025-01-24 09:44:00 -03:00
Michael Sloan
813bbecd5c Fix terminal memory leak by deduping alacritty events on background thread (#23593)
Closes #23008

Release Notes:

- Fixed case where the terminal can leak memory when it produces events
at a faster rate than could be processed.
2025-01-24 10:25:03 +00:00
Shivam Mishra
dd8ee76b2e docs: context_servers json example (#23588) 2025-01-24 08:10:59 +00:00
Marshall Bowers
c55cdd0cb9 assistant2: Add thread persistence (#23582)
This PR adds persistence for threads in Assistant2.

Threads are now persisted to an LMDB database.

Release Notes:

- N/A
2025-01-24 00:09:26 +00:00
Piotr Osiewicz
fb63f61755 search: Add heuristic for discarding matching of binary files (#23581)
Fixes #23398 
Closes #23398

We'll bail on searches of files that we know are binary (thus even if we
were to find a match in them, they'd be thrown away by buffer loader).

Release Notes:

- Improved project search performance in worktrees with binary files
2025-01-23 22:15:58 +00:00
Marshall Bowers
35ddb432b3 assistant_context_editor: Put uses in the right spot (#23579)
This PR cleans up some `use` statements that weren't at the very top of
the module.

Release Notes:

- N/A
2025-01-23 21:58:33 +00:00
Marshall Bowers
ec91a8dc82 assistant2: Expose ActiveThread::thread via a getter (#23577)
This PR exposes the `thread` file on the `ActiveThread` via a getter
rather than exposing the field directly.

Release Notes:

- N/A
2025-01-23 21:54:16 +00:00
Michael Sloan
52494f3fdf Update some editor methods to instead take immutable references (#23578)
Makes the signatures more informative and can be more convenient as
multiple immutable borrows are allowed.

Release Notes:

- N/A
2025-01-23 21:52:10 +00:00
Agus Zubiaga
966533624a Disable zeta predictions in assistant completion menu (#23573)
We don't want the zeta predictions entry to show in the assistant
context editor when completing slash commands. Zeta will still make
suggestions in the rest of the context editor, like the other providers
do.

Release Notes:

- N/A
2025-01-23 18:49:17 -03:00
Joseph T. Lyons
06f0f0747d Autoscroll when running editor: swap selection ends (#23575)
Closes https://github.com/zed-industries/zed/issues/23512

Release Notes:

- Improved `editor: swap selection ends` by always scrolling the cursor
into view
2025-01-23 21:35:24 +00:00
Piotr Osiewicz
d8c9fdd014 project: Revert project tree impl (again) (#23572) 2025-01-23 16:10:38 -05:00
Marshall Bowers
2a2c332584 docs: Document ZED_DEVELOPMENT_AUTH (#23571)
This PR adds documentation for the `ZED_DEVELOPMENT_AUTH` environment
variable, in the hopes that it helps folks find it sooner.

Release Notes:

- N/A
2025-01-23 20:52:14 +00:00
Marshall Bowers
76bf4686ef ui: Don't add an on_click handler for disabled ListItems (#23569)
This PR updates the `ListItem` component to not register an `on_click`
handler for `ListItem`s that are disabled.

When working on #23350 I noticed that even when the context menu entry
was disabled you could still click on the entry to fire the action.

Release Notes:

- Fixed some instances of disabled list items still registering clicks.
2025-01-23 19:58:07 +00:00
Swiftaff
5ef5b75099 Disable Copy Permalink context menu item when not in git repo (#23350)
Closes #13979

Please review this approach to hide the permalink, or alternatively to
disable it instead?

Release Notes:

- The Copy Permalink menu item is now disabled when not in a Git
repository.

---------

Co-authored-by: Marshall Bowers <git@maxdeviant.com>
2025-01-23 19:53:07 +00:00
Conrad Irwin
0ef53bf476 vim: Add support for ctrl-g (#23562)
Co-Authored-By: Jon Walstedt <jon@walstedt.se>

Closes #22094

Release Notes:

- vim: Added support for ctrl-g

Co-authored-by: Jon Walstedt <jon@walstedt.se>
2025-01-23 12:10:08 -07:00
Peter Tripp
f38d0ff069 ollama: Set default max_tokens for llama3.3 (#23558) 2025-01-23 17:38:43 +00:00
Bennet Bo Fenner
3dee32c43d inline completion: Add syntax highlighting for edit prediction (#23361)
Closes #ISSUE

Release Notes:

- N/A

---------

Co-authored-by: Antonio Scandurra <me@as-cii.com>
Co-authored-by: Agus <agus@zed.dev>
2025-01-23 17:32:43 +00:00
Cole Miller
75ae4dada4 Remove unwrap in GitTraversal::synchronize_statuses (#23555)
Release Notes:

- Fixed a potential panic in handling of Git statuses.

Co-authored-by: Marshall <marshall@zed.dev>
2025-01-23 12:13:20 -05:00
Peter Tripp
1ac7da8473 terraform: Bump to v0.1.2 (#23546)
Includes:
- https://github.com/zed-industries/zed/pull/22268
2025-01-23 11:34:40 -05:00
Peter Tripp
469bfa752a scheme: Bump to v0.0.2 (#23545)
Includes:
- https://github.com/zed-industries/zed/pull/18728
- https://github.com/zed-industries/zed/pull/20206
2025-01-23 11:34:29 -05:00
Nate Butler
fb332a0170 git_panel: Toggle stage all when clicking changes label (#23548)
When clicking the checkbox label fire the toggle action.

At first I wasn't sure this is what we wanted, but after looking at a
few existing implementations of checkboxes with labels it seems like
this is reasonably standard.

Eventually this piece of UI will be updated to a CheckboxWithLabel, but
for now it is custom due to some specific style requirements.

Release Notes:

- N/A
2025-01-23 16:18:25 +00:00
Piotr Osiewicz
9e6b10018a project: Bring back language servers in detached worktrees (#23530)
Closes #ISSUE

Release Notes:

- N/A
2025-01-23 17:03:53 +01:00
Peter Tripp
bb937b6cee zig: Bump to v0.3.3 (#23547)
Includes:
- https://github.com/zed-industries/zed/pull/22609
2025-01-23 10:54:50 -05:00
Peter Tripp
ea1a6a68d3 php: Bump to v0.2.4 (#23543)
Includes:
- https://github.com/zed-industries/zed/pull/23532
- https://github.com/zed-industries/zed/pull/22268
2025-01-23 10:48:58 -05:00
Peter Tripp
63c5141365 purescript: Bump to v0.1.0 (#23544)
Includes:
- https://github.com/zed-industries/zed/pull/15181
- https://github.com/zed-industries/zed/pull/16955
- https://github.com/zed-industries/zed/pull/22609
2025-01-23 10:44:22 -05:00
Peter Tripp
26453a0f8f csharp: Bump to v0.1.1 (#23539)
Includes:
- https://github.com/zed-industries/zed/pull/22936
2025-01-23 10:40:54 -05:00
Peter Tripp
a328c81ff6 html: Bump to v0.1.5 (#23542)
Includes:
- https://github.com/zed-industries/zed/pull/20752
- https://github.com/zed-industries/zed/pull/22268
2025-01-23 10:37:14 -05:00
Peter Tripp
9d6d98e94e haskell: Bump to v0.1.3 (#23541)
Includes:
- https://github.com/zed-industries/zed/pull/22609
2025-01-23 10:30:47 -05:00
Peter Tripp
7d7db4debc elixir: Bump to v0.1.4 (#23540)
Includes:
- https://github.com/zed-industries/zed/pull/22055
- https://github.com/zed-industries/zed/pull/22268
2025-01-23 10:27:21 -05:00
Ollie Beckwith
79683ebcaa php: Add nowdoc language injection (#23532)
We already have heredoc injection support, so this just extends it to also cover
[nowdoc](https://www.php.net/manual/en/language.types.string.php#language.types.string.syntax.nowdoc).
2025-01-23 09:51:18 -05:00
Thorsten Ball
0cb41754e2 llm: Sample ~10% of staff members inputs/outputs to LLM (#23537)
Release Notes:

- N/A
2025-01-23 15:32:25 +01:00
Kirill Bulatov
95c045ad93 Fix LSP violation when dismissing server notifications (#23531)
Part of https://github.com/zed-industries/zed/issues/22606
Closes https://github.com/zed-industries/zed/issues/23509

When a user sees an odd notification from the language server like

<img width="508" alt="image"
src="https://github.com/user-attachments/assets/6f5ef1aa-0f09-4705-a02a-aaf81dd8620c"
/>

they usually dismiss that.

Zed uses channels to wait and handle user interactions with such
notifications, and, due to `?`, sends back
```json
{"jsonrpc":"2.0","id":1,"error":{"message":"receiving from an empty and closed channel"}}
```

which is not spec-compliant:
https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#window_showMessageRequest

> Response:
>
> * result: the selected
[MessageActionItem](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#messageActionItem)
| null if none got selected.
> * error: code and message set in case an exception happens during
showing a message.

Unfortunately, vtsls (and, potentially, others) crash if receive such
non-compliant requests, and do not get back.

After the fix, the message is correct:
```json
{"jsonrpc":"2.0","id":1,"result":null}
```


Release Notes:

- Fixed vtsls crashing on notification dismiss

Co-authored-by: Piotr Osiewicz <piotr@zed.dev>
2025-01-23 13:30:35 +00:00
Nate Butler
f9e354ee9b theme: Add version control colors (#23529)
This PR adds version control-specific theme tokens to the them to allow
styling entries in the git ui and elsewhere.

Release Notes:

- N/A
2025-01-23 07:56:49 -05:00
Piotr Osiewicz
828b5ab975 project: Reorder LSP Adapters within LanguageRegistry (#23528)
Closes #ISSUE

Release Notes:

- N/A
2025-01-23 12:03:37 +00:00
Piotr Osiewicz
11bb906520 terminal: Check for script existence properly before activating it (#23476)
We were not covering a variant where the returned metadata was Ok(None)

Closes #ISSUE

Release Notes:

- Fixed venv activation script path showing up in terminal for
non-existant scripts.
2025-01-23 12:32:21 +01:00
Kirill Bulatov
91b0ca0895 Omit tsdk_path from the servers' options if it does not exist (#23525)
Part of https://github.com/zed-industries/zed/issues/22606

Before, `tsdk_path` for vtsls and typescript-language-server
unconditionally set a `tsdk`/`tsserver` property for the corresponding
language server, even if there were no such directory at all.
Instead, make the corresponding code to omit such property if it was not
found on the FS.

Release Notes:

- Fixed "The path /.../tsserver.js doesn't point to a valid tsserver
install. Falling back to bundled TypeScript version." pop-up appearing
2025-01-23 11:17:32 +00:00
Kirill Bulatov
d1be419fff Fix panics on opening repositories with empty remotes (#23520)
Uses a newer `git2` version with
https://github.com/rust-lang/git2-rs/pull/1120 fix

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

Release Notes:

- Fixed panics on opening repositories with empty remotes
2025-01-23 08:20:32 +00:00
renovate[bot]
70f8224a89 Update cloudflare/wrangler-action digest to 7a5f8bb (#23487)
This PR contains the following updates:

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

---

### 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:eyJjcmVhdGVkSW5WZXIiOiIzOS4xMDcuMCIsInVwZGF0ZWRJblZlciI6IjM5LjEwNy4wIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6W119-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-23 08:14:14 +00:00
renovate[bot]
29b57de4ab Update actions/stale digest to 5bef64f (#23486)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [actions/stale](https://redirect.github.com/actions/stale) | action |
digest | `28ca103` -> `5bef64f` |

---

### 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:eyJjcmVhdGVkSW5WZXIiOiIzOS4xMDcuMCIsInVwZGF0ZWRJblZlciI6IjM5LjEwNy4wIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6W119-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-23 09:54:25 +02:00
renovate[bot]
36c62bc2d8 Update Rust crate metal to 0.31 (#23508)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [metal](https://redirect.github.com/gfx-rs/metal-rs) |
workspace.dependencies | minor | `0.30` -> `0.31` |

---

### 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:eyJjcmVhdGVkSW5WZXIiOiIzOS4xMDcuMCIsInVwZGF0ZWRJblZlciI6IjM5LjEwNy4wIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6W119-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-23 09:50:53 +02:00
renovate[bot]
9d7199e403 Update Rust crate convert_case to 0.7.0 (#23504)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [convert_case](https://redirect.github.com/rutrum/convert-case) |
workspace.dependencies | minor | `0.6.0` -> `0.7.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:eyJjcmVhdGVkSW5WZXIiOiIzOS4xMDcuMCIsInVwZGF0ZWRJblZlciI6IjM5LjEwNy4wIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6W119-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-23 09:49:28 +02:00
Cole Miller
caade6af7a WIP 2025-01-22 23:42:47 -05:00
Cole Miller
984eb9b5c3 Work 2025-01-22 23:38:46 -05:00
Marshall Bowers
82cee9e9a4 assistant2: Don't suggest thread context for inline assist without a ThreadStore (#23506)
This PR makes it so we don't suggest threads as context in the inline
assist when we don't have a `ThreadStore` to pull from.

Release Notes:

- N/A
2025-01-22 23:47:56 +00:00
renovate[bot]
ecf70db7f0 Update Rust crate serde_json to v1.0.137 (#23498)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [serde_json](https://redirect.github.com/serde-rs/json) | dependencies
| patch | `1.0.135` -> `1.0.137` |
| [serde_json](https://redirect.github.com/serde-rs/json) |
workspace.dependencies | patch | `1.0.135` -> `1.0.137` |

---

### Release Notes

<details>
<summary>serde-rs/json (serde_json)</summary>

###
[`v1.0.137`](https://redirect.github.com/serde-rs/json/releases/tag/v1.0.137)

[Compare
Source](https://redirect.github.com/serde-rs/json/compare/v1.0.136...v1.0.137)

- Turn on "float_roundtrip" and "unbounded_depth" features for
serde_json in play.rust-lang.org
([#&#8203;1231](https://redirect.github.com/serde-rs/json/issues/1231))

###
[`v1.0.136`](https://redirect.github.com/serde-rs/json/releases/tag/v1.0.136)

[Compare
Source](https://redirect.github.com/serde-rs/json/compare/v1.0.135...v1.0.136)

- Optimize serde_json::value::Serializer::serialize_map by using
Map::with_capacity
([#&#8203;1230](https://redirect.github.com/serde-rs/json/issues/1230),
thanks [@&#8203;goffrie](https://redirect.github.com/goffrie))

</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 these
updates again.

---

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

---

Release Notes:

- N/A

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzOS4xMDcuMCIsInVwZGF0ZWRJblZlciI6IjM5LjEwNy4wIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6W119-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-22 18:12:27 -05:00
renovate[bot]
848f29831e Update Rust crate semver to v1.0.25 (#23497)
This PR contains the following updates:

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

---

### Release Notes

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

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

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

- Enable serde impls on play.rust-lang.org
([#&#8203;330](https://redirect.github.com/dtolnay/semver/issues/330),
thanks [@&#8203;jofas](https://redirect.github.com/jofas))

</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:eyJjcmVhdGVkSW5WZXIiOiIzOS4xMDcuMCIsInVwZGF0ZWRJblZlciI6IjM5LjEwNy4wIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6W119-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-22 18:11:47 -05:00
Cole Miller
78a8c1a68a git: No-op when trying to commit with nothing staged or no commit message (#23491)
The buttons are disabled in this case, but users can still trigger a
commit via the actions directly, and an error toast seems a bit loud for
this.

cc @iamnbutler 

Release Notes:

- N/A
2025-01-22 18:04:14 -05:00
renovate[bot]
db65ee0add Update Rust crate etagere to v0.2.15 (#23493)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [etagere](https://redirect.github.com/nical/etagere) | dependencies |
patch | `0.2.13` -> `0.2.15` |

---

### 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:eyJjcmVhdGVkSW5WZXIiOiIzOS4xMDcuMCIsInVwZGF0ZWRJblZlciI6IjM5LjEwNy4wIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6W119-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-22 18:03:51 -05:00
Marshall Bowers
8ed2a81962 feature_flags: Enable assistant2 for all staff (#23502)
This PR makes it so the `assistant2` feature flag is automatically
enabled for all staff.

Previously all the staff members had been opted in to the feature flag
manually (to allow for opting out), but I think we're ready to
unconditionally ship it to all staff.

Release Notes:

- N/A
2025-01-22 23:02:53 +00:00
Marshall Bowers
01b57b10e7 assistant2: Only show thread context in picker when we have a ThreadStore (#23501)
This PR fixes an issue with the context picker where it would show
thread context as an option even if there was no `ThreadStore`
available.

Release Notes:

- N/A
2025-01-22 23:00:43 +00:00
Marshall Bowers
a1077c6fff assistant2: Add button to open the prompt library (#23500)
This PR adds a button to open the prompt library from the configuration
view in Assistant2.

<img width="1309" alt="Screenshot 2025-01-22 at 5 38 08 PM"
src="https://github.com/user-attachments/assets/d514abca-53bc-4cde-bead-ab68a1994fb5"
/>

Release Notes:

- N/A
2025-01-22 22:53:54 +00:00
Marshall Bowers
51fcb710d7 Dedupe PromptBuilder construction (#23496)
This PR dedupes the construction of the `PromptBuilder`.

Previously this was constructed by both `assistant` and `assistant2`,
but now we construct it outside and pass it in.

Release Notes:

- N/A
2025-01-22 22:17:36 +00:00
renovate[bot]
aad04e078a Update Rust crate indexmap to v2.7.1 (#23494)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [indexmap](https://redirect.github.com/indexmap-rs/indexmap) |
workspace.dependencies | patch | `2.7.0` -> `2.7.1` |

---

### Release Notes

<details>
<summary>indexmap-rs/indexmap (indexmap)</summary>

###
[`v2.7.1`](https://redirect.github.com/indexmap-rs/indexmap/blob/HEAD/RELEASES.md#271-2025-01-19)

[Compare
Source](https://redirect.github.com/indexmap-rs/indexmap/compare/2.7.0...2.7.1)

-   Added `#[track_caller]` to functions that may panic.
-   Improved memory reservation for `insert_entry`.

</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:eyJjcmVhdGVkSW5WZXIiOiIzOS4xMDcuMCIsInVwZGF0ZWRJblZlciI6IjM5LjEwNy4wIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6W119-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-22 16:19:23 -05:00
Piotr Osiewicz
3c0d05e362 lsp: Use replace edits for completions (#23490)
(Late) follow up to #9634.
Fixes #23395

Release Notes:

- Accepting completions while the cursor is in the middle of suggested
completion will now result in smaller edits being applied.
2025-01-22 21:00:04 +00:00
Cole Miller
55d99e0259 git: Handle git status output for deleted-in-index state (#23483)
When a file exists in HEAD, is deleted in the index, and exists again in
the working copy, git produces two lines for it, one reading `D `
(deleted in index, unmodified in working copy), and the other reading
`??` (untracked). Merge these two into the equivalent of `DA`.

Release Notes:

- Improved handling of files that are deleted in the git index but exist
in HEAD and the working copy
2025-01-22 15:28:25 -05:00
Piotr Osiewicz
08b3c03241 project: Allow running multiple instances of a single language server within a single worktree (#23473)
This PR introduces a new entity called Project Tree which is responsible
for finding subprojects within a worktree;
a subproject is a language-specific subset of a worktree which should be
accurately tracked on the language server side. We'll have an ability to
set multiple disjoint workspaceFolders on language server side OR spawn
multiple instances of a single language server (which will be the case
with e.g. Python language servers, as they need to interact with
multiple disjoint virtual environments).
Project Tree assumes that projects of the same LspAdapter kind cannot
overlap. Additionally project nesting is not allowed within the scope of
a single LspAdapter.

Closes https://github.com/zed-industries/zed/issues/5108
Re-lands #22182 which I had to revert due to merging it into todays
Preview.

Release Notes:

- Language servers now track their working directory more accurately.

---------

Co-authored-by: João <joao@zed.dev>
2025-01-22 21:19:02 +01:00
Marshall Bowers
2c2a3ef13d assistant2: Handle LLM providers that do not emit StartMessage events (#23485)
This PR updates Assistant2's response streaming to work with LLM
providers that do not emit `StartMessage` events.

Now if we get a `Text` event without having received a `StartMessage`
event we will still insert an Assistant message so we can stream in the
response from the model.

Release Notes:

- N/A
2025-01-22 20:15:16 +00:00
Cole Miller
00dea4ccc9 Fix 2025-01-22 15:02:10 -05:00
Peter Tripp
6aab82c180 docs: Mention PopOS 24.04 in Linux FAQ for /etc/prime-discrete (#23482) 2025-01-22 19:37:04 +00:00
Cole Miller
abb295ea22 Make the untracked part of the test actually test something 2025-01-22 14:36:51 -05:00
Cole Miller
a811979894 git: Fix the panel's update debouncing (#23462)
This PR replaces the update debouncing code in the git panel with a more
correct and conventional structure (holding a `Task` field instead of
spawning a task that runs a loop). I wrote the code that this replaces
without realizing that it doesn't implement debouncing properly.

Release Notes:

- N/A
2025-01-22 14:33:17 -05:00
Cole Miller
33fa0724cf git: Handle git status output for deleted-in-index state 2025-01-22 14:28:56 -05:00
Marshall Bowers
514d9b4161 assistant2: Add configuration (#23481)
This PR wires up the ability to configure Assistant2.

<img width="1309" alt="Screenshot 2025-01-22 at 1 52 56 PM"
src="https://github.com/user-attachments/assets/3de47797-7959-47af-bd93-51f105e87c28"
/>

Release Notes:

- N/A
2025-01-22 19:07:48 +00:00
Marshall Bowers
f7703973d2 assistant: Extract ConfigurationView to its own module (#23480)
This PR is a small refactoring that extracts the Assistant's
`ConfigurationView` into its own module.

Release Notes:

- N/A
2025-01-22 18:37:00 +00:00
Michael Sloan
09fe1e7f66 Scroll completions menu to new selection on filter, fix corner case (#23478)
In the future if `filter` was used more this would fix other issues. In
the current code paths, this just fixes the particular corner case of
edit prediction arriving async while `y_flipped = true` (in this case it
needs to be scrolled down to show item with index 0).

Release Notes:

- N/A
2025-01-22 17:54:10 +00:00
Marshall Bowers
9f87145af9 title_bar: Simplify git-ui feature flag check (#23475)
This PR is a follow-up to
https://github.com/zed-industries/zed/pull/23470 that simplifies the way
we check the `git-ui` feature flag in the title bar.

Release Notes:

- N/A
2025-01-22 17:28:47 +00:00
Peter Tripp
5930b552b9 Bump Zed to v0.172 (#23474) 2025-01-22 11:57:24 -05:00
Piotr Osiewicz
da406ae07e Revert "project: Allow running multiple instances of a single language server within a single worktree" (#23472)
Reverts zed-industries/zed#22182
I've merged the build too soon as I wanted it to be excluded from todays
Preview.
2025-01-22 11:42:50 -05:00
Thorsten Ball
706e46c3ff vim: Fix % not working for multi-char bracket pairs (#23471)
Closes #23358

Demo/proof:


https://github.com/user-attachments/assets/2036d183-8830-415b-9155-0d3efe3d824c




Release Notes:

- Fixed `%` in Vim mode not working correctly for multi-char brackets,
such as `do/end` in Elixir or `\begin` and `\end` in LaTeX.

Co-authored-by: Conrad <conrad@zed.dev>
2025-01-22 17:39:11 +01:00
Nate Butler
5be2784ddb git_ui: Feature flag repo selector (#23470)
Fixes an issue where the repo selector showed for all users, not just
those in the git_ui feature flag.

This was meant to be included in the `git_ui` feature flag.

Release Notes:

- N/A
2025-01-22 16:37:19 +00:00
Piotr Osiewicz
bed917b0b1 project: Allow running multiple instances of a single language server within a single worktree (#22182)
This PR introduces a new entity called Project Tree which is responsible
for finding subprojects within a worktree;
a subproject is a language-specific subset of a worktree which should be
accurately tracked on the language server side. We'll have an ability to
set multiple disjoint `workspaceFolder`s on language server side OR
spawn multiple instances of a single language server (which will be the
case with e.g. Python language servers, as they need to interact with
multiple disjoint virtual environments).
Project Tree assumes that projects of the same LspAdapter kind cannot
overlap. Additionally **project nesting** is not allowed within the
scope of a single LspAdapter.

Closes #5108

Release Notes:

- Language servers now track their working directory more accurately.

---------

Co-authored-by: João <joao@zed.dev>
2025-01-22 17:31:14 +01:00
Danilo Leal
d85fec5f13 Update "Book Onboarding" destination link (#23464)
This PR updates the Book Onboarding links to point to a new form.

Release Notes:

- N/A
2025-01-22 16:24:39 +00:00
Agus Zubiaga
95cde129af Show "tab Accept" only for zeta (#23463)
#23460 brought up we are showing the new "tab Accept" marker for single
line suggestions for non-zeta providers. We think this might be valid
for any provider, but we only want to enable it for zeta initially so it
doesn't affect an existing user base.

Release Notes:

- N/A
2025-01-22 13:19:18 -03:00
Marshall Bowers
1e88e2924c assistant2: Respect panel dock position (#23465)
This PR fixes an issue where Assistant2 was not respecting the panel
dock position.

Release Notes:

- N/A
2025-01-22 16:05:04 +00:00
Agus Zubiaga
636df12b74 Only show accept callout if suggestion is all insertions/deletions (#23461)
Release Notes:

- N/A
2025-01-22 14:01:25 +00:00
Agus Zubiaga
55721c65d4 Fix single line edit prediction detection (#23456)
#23411 introduced an "Accept" callout for single line edits, but the
logic to detect them was incorrect causing it to trigger for multiline
insertions, this PR fixes that.

Release Notes:

- N/A
2025-01-22 10:00:43 -03:00
Piotr Osiewicz
f0b5b0b4df lsp: Ignore payload of DidChangeConfiguration dynamic registration (#23454)
Fixes #23430

Closes #23430

Release Notes:

- N/A
2025-01-22 11:23:42 +01:00
张小白
c66f611037 windows: Improve foreground task dispatching on Windows (continued) (#23415)
Closes #22653 again

In PR #23283, I thought that every `runnable` dispatched to the main
thread would correspond to an `EVENT_DISPATCHED` message in the message
queue. However, after testing, some `runnable`s occasionally weren’t
executed.

This PR updated the code as follows:  
```rust
if let Ok(runnable) = self.main_receiver.try_recv() {    <-- before
for runnable in self.main_receiver.drain() {            <-- after
    runnable.run();
}
```

This ensures that runnables are handled more proactively on the main
thread, now we handle `runnable`s with a much higher priority.

A big thanks to @MolotovCherry and @ArthurBrussee for their testing
efforts!

Release Notes:

- N/A
2025-01-22 16:37:36 +08:00
Michael Sloan
2f1af2ab69 When completions menu is displayed above cursor, reverse order (#23446)
Considered doing this when previously working on completions menu
layout, as it brings the default selection position next to the cursor
position, and is generally more symmetrical. With #23445 there is now a
more compelling reason, as the "translucent, cropped bottom" display
doesn't make sense when displayed above.

Release Notes:

- N/A
2025-01-21 22:47:56 -07:00
Michael Sloan
1769bc957b Make app notifications appear in new workspaces + dismiss on all workspaces (#23432)
The keymap error notifications got convoluted to support displaying the
notification on startup. This change addresses it systemically for all
future app notifications.

Reverts most of #20531, while keeping the fix to handle keyboard layout
switching. This is a better fix for #20531

Release Notes:

- N/A
2025-01-22 05:39:04 +00:00
Marshall Bowers
1e1997c97b Wire up AssistantPanelDelegate based on assistant2 feature flag (#23444)
This PR adjusts how the `AssistantPanelDelegate` global is set to be
based on the state of the feature flag.

This should prevent `assistant` and `assistant2` from potentially
clobbering each other.

Release Notes:

- N/A
2025-01-22 04:57:51 +00:00
Marshall Bowers
9ee1db3552 assistant2: Insert default prompt into prompt editor (#23443)
This PR fixes an issue where the default prompt was not being inserted
into the prompt editor in Assistant2.

Release Notes:

- N/A
2025-01-22 03:01:34 +00:00
Marshall Bowers
c887bf8e54 Consolidate Assistant panels in assistant2 feature flag (#23442)
This PR consolidates the two Assistant panels into one for users in the
`assistant2` feature flag.

Now that the Assistant1 prompt editor is accessible through the
Assistant2 panel, we no longer have a need to show both panels.

Release Notes:

- N/A
2025-01-22 02:56:42 +00:00
Marshall Bowers
7516b8c8b7 assistant2: Add prompt editor history (#23439)
This PR adds the prompt editor history to Assistant2.

<img width="1309" alt="Screenshot 2025-01-21 at 9 02 07 PM"
src="https://github.com/user-attachments/assets/d79936fe-1c23-425f-b99d-43f85afd0c39"
/>

Release Notes:

- N/A
2025-01-22 02:11:48 +00:00
Marshall Bowers
be407e27f9 Extract ContextHistory to assistant_context_editor (#23437)
This PR extracts the `ContextHistory` to the `assistant_context_editor`
crate.

Release Notes:

- N/A
2025-01-22 01:32:24 +00:00
Marshall Bowers
e59c910845 assistant2: Add prompt editor (#23436)
This PR adds the Assistant1 experience to Assistant2 as a "prompt
editor".

<img width="1309" alt="Screenshot 2025-01-21 at 7 17 26 PM"
src="https://github.com/user-attachments/assets/3ce2f32b-2b1a-48a8-8e56-4c44e3ac4ce5"
/>

Release Notes:

- N/A
2025-01-22 00:36:55 +00:00
Peter Tripp
3d47f32f0c Add support for editor::SwapSelectionEnds everywhere (emacs exchange-point-and-mark) (#23428)
Add `editor:: SwapSelectionEnds ` action which swaps the cursor location from the beginning/end of a given selection.
Renamed from `editor::ExchangeMark` to `editor::SwapSelectionEnds`.
Unbound by default, bound to `ctrl-x ctrl-x` in Emacs keymap.
2025-01-21 18:14:00 -05:00
Cole Miller
31909bf334 git: Implement a basic repository selector (#23419)
This PR adds a rough-and-ready picker for selecting which of the
project's repositories the git panel should display.

Release Notes:

- N/A

---------

Co-authored-by: Nate Butler <iamnbutler@gmail.com>
Co-authored-by: Nate <nate@zed.dev>
2025-01-21 18:11:53 -05:00
Marshall Bowers
417760ade7 Extract ContextEditor to assistant_context_editor (#23433)
This PR extracts the `ContextEditor` to the `assistant_context_editor`
crate.

As part of this, we have decoupled the `ContextEditor` from the
`AssistantPanel`.

There is now an `AssistantPanelDelegate` that the `ContextEditor` uses
when it needs to interface with the Assistant panel.

Release Notes:

- N/A
2025-01-21 23:08:34 +00:00
Marshall Bowers
9a7f1d1de4 Add assistant_context_editor crate (#23429)
This PR adds a new `assistant_context_editor` crate.

This will ultimately house the `ContextEditor` so that it can be
consumed by both `assistant` and `assistant2`.

For the purposes of this PR, we just introduce the crate and move some
supporting constructs to it, such as the `ContextStore`.

Release Notes:

- N/A
2025-01-21 21:22:59 +00:00
Peter Tripp
c450cd51ea open_ai: Move from o1-preview to o1 for OpenAI Assistant provider (#23425)
- Closes: https://github.com/zed-industries/zed/issues/22521
- Follow-up to: https://github.com/zed-industries/zed/pull/22376
2025-01-21 15:05:21 -05:00
k4leg
6eaaced60d Fix button demo in the component preview (#23423)
Problem: If you click on "Tinted Icons", "Icon Color" is also activated.

Release Notes:

- N/A
2025-01-21 19:33:08 +00:00
Marshall Bowers
836b4c1db4 ci: Remove paths-ignore for docs, as it does not work with required status checks (#23424)
This PR removes the `paths-ignore` for docs again, as it causes
docs-only PRs to be unmergable in combination with required status
checks (which we need in order to support merge-when-ready).

We can put these back if and only if we come up with a solution for how
to make it work with required status checks.

Release Notes:

- N/A
2025-01-21 19:28:22 +00:00
Hristo Kanchev
718da3f9f5 docs: Fix broken link for the Odin language (#23421) 2025-01-21 14:20:54 -05:00
Marshall Bowers
51b6cbf9ac assistant: Extract ContextEditor and ContextHistory to their own modules (#23422)
This PR extracts the `ContextEditor` and `ContextHistory`
implementations into their own modules so that it's clearer which parts
depend on other constructs in the `assistant` crate.

Release Notes:

- N/A
2025-01-21 19:10:13 +00:00
Agus Zubiaga
c825bb492d assistant2: Propagate move up action to inline picker from message editor (#23416)
Release Notes:

- N/A

Co-authored-by: Richard <richard@zed.dev>
2025-01-21 16:02:52 -03:00
Agus Zubiaga
86ff88ae1d Show "tab Accept" indicator for single line insertions/deletions (#23411)
https://github.com/user-attachments/assets/655f20a9-f22f-4f91-870e-d40b20c8c994

Release Notes:

- N/A

Co-authored-by: Danilo <danilo@zed.dev>
2025-01-21 16:02:26 -03:00
Peter Tripp
14cd178ab0 ollama: Add deepseek-r1 context size to defaults (#23420) 2025-01-21 18:31:15 +00:00
Cole Miller
aa5fa4b7d4 git: Use a buffer for the panel's commit message (#23308)
This PR changes the `GitPanel` and `GitState` to use a
`language::Buffer` for the commit message. This is a small initial step
toward remote editing and collaboration support.

Release Notes:

- N/A

---------

Co-authored-by: Max <max@zed.dev>
2025-01-21 17:58:18 +00:00
Michael Sloan
64f9acf020 Simplify workspace notification code (#23414)
* Remove `NotificationHandle` trait in favor of just passing `AnyView` -
id field wasn't used.

* Remove `show_notification_once`, doesn't seem to be needed for its
only use.

Release Notes:

- N/A
2025-01-21 17:49:11 +00:00
Enrique Kessler Martínez
8c215d43ad Fix pulling metadata out of broken symlinks (#22396)
## Problem

When developing extensions locally, developers will commonly put their
source code in a specific directory. Zed uses this directory to create a
symlink starting from `$HOME/Library/Application
Support/Zed/extensions/installed` (MacOS path). When a developer then
moves this source code and tries to reinstall the extension, Zed will
fail with an unhelpful message (you can check the #Testing section).

## Change Summary

With this PR, we fix this behaviour by handling broken symlinks
specifically when returning the metadata on `fs::metadata`. Today, we

1. Pull the symlink metadata.
2. Return it if the file was not a symlink OR if it is, pull the
metadata for the pointed file.

After this change gets merged, we return the Symlink metadata if the
symlink is broken. This makes the symlink be recreated since we remove
the symlink either way.

## Risks associated with this change

It's possible changing this behaviour will show additional cases where
we are handling broken symlinks incorrectly. I expect this to be a
better scenario AND backwards compatible. We have the same behaviour we
had for 1. existing symlinks 2. normal files.

## Testing

The way I have been reproducing this is by having a private extension of
my own. I install it using the `zed: install dev extension` command
after running `RUST_LOG=debug RUST_BACKTRACE=1 scripts/zed-local -1`.
Then I move the extension to a different directory.

Zed will now keeps a broken link on its `installed` directory:

```
❯ ll
Permissions Size User    Date Modified Name
lrwxr-xr-x@    - enrikes 24 Dec 12:15  brazil-config-zed-extension -> /Volumes/workplace/BrazilConfigZedExtension
drwxr-xr-x@    - enrikes  5 Dec 14:48  java
drwxr-xr-x@    - enrikes 12 Dec 13:04  kotlin
drwxr-xr-x@    - enrikes 25 Oct 08:13  rose-pine-theme
```

Before the patch, Zed shows on its logs:

```
2024-12-24T16:44:02+01:00 INFO  extension::extension_builder] compiled Rust extension /Users/enrikes/Documents/BrazilConfigZedExtension
[2024-12-24T16:44:02+01:00 INFO  extension::extension_builder] compiling grammar brazil_config for extension /Users/enrikes/Documents/BrazilConfigZedExtension
[2024-12-24T16:44:02+01:00 INFO  extension::extension_builder] checking out brazil_config parser
[2024-12-24T16:44:04+01:00 INFO  extension::extension_builder] compiling brazil_config parser
[2024-12-24T16:44:05+01:00 INFO  extension::extension_builder] compiled grammar brazil_config for extension /Users/enrikes/Documents/BrazilConfigZedExtension
[2024-12-24T16:44:05+01:00 INFO  extension::extension_builder] finished compiling extension /Users/enrikes/Documents/BrazilConfigZedExtension
[2024-12-24T16:44:05+01:00 ERROR extensions_ui] No such file or directory (os error 2)

Stack backtrace:
   0: std::backtrace_rs::backtrace::libunwind::trace
             at /rustc/eeb90cda1969383f56a2637cbd3037bdf598841c/library/std/src/../../backtrace/src/backtrace/libunwind.rs:116:5
   1: std::backtrace_rs::backtrace::trace_unsynchronized
             at /rustc/eeb90cda1969383f56a2637cbd3037bdf598841c/library/std/src/../../backtrace/src/backtrace/mod.rs:66:5
   2: std::backtrace::Backtrace::create
             at /rustc/eeb90cda1969383f56a2637cbd3037bdf598841c/library/std/src/backtrace.rs:331:13
   3: anyhow::error::<impl core::convert::From<E> for anyhow::Error>::from
             at /Users/enrikes/.cargo/registry/src/index.crates.io-6f17d22bba15001f/anyhow-1.0.94/src/backtrace.rs:27:14
   4: <core::result::Result<T,F> as core::ops::try_trait::FromResidual<core::result::Result<core::convert::Infallible,E>>>::from_residual
             at /rustc/eeb90cda1969383f56a2637cbd3037bdf598841c/library/core/src/result.rs:1989:27
   5: <fs::RealFs as fs::Fs>::metadata::{{closure}}
             at ./crates/fs/src/fs.rs:603:13
```

After the patch, the extension is installed and the symlink replaced for
a new one pointing to the user's directory choice.

```
2024-12-24T16:53:33.916022+01:00 [INFO] compiled Rust extension /Users/enrikes/Documents/BrazilConfigZedExtension
2024-12-24T16:53:33.916094+01:00 [INFO] compiling grammar brazil_config for extension /Users/enrikes/Documents/BrazilConfigZedExtension
2024-12-24T16:53:33.916225+01:00 [INFO] checking out brazil_config parser
2024-12-24T16:53:35.481602+01:00 [INFO] compiling brazil_config parser
2024-12-24T16:53:35.964189+01:00 [INFO] compiled grammar brazil_config for extension /Users/enrikes/Documents/BrazilConfigZedExtension
2024-12-24T16:53:35.964319+01:00 [INFO] finished compiling extension /Users/enrikes/Documents/BrazilConfigZedExtension
2024-12-24T16:53:36.213608+01:00 [INFO] rebuilt extension index in 39.108542ms
2024-12-24T16:53:36.213835+01:00 [INFO] extensions updated. loading 0, reloading 1, unloading 0
2024-12-24T16:53:36.375928+01:00 [INFO] rebuilt extension index in 34.478167ms
2024-12-24T16:53:36.376054+01:00 [INFO] extensions updated. loading 0, reloading 1, unloading 0
```

and

```
❯ ll
lrwxr-xr-x@    - enrikes 24 Dec 16:53  brazil-config-zed-extension -> /Users/enrikes/Documents/BrazilConfigZedExtension
drwxr-xr-x@    - enrikes  5 Dec 14:48  java
drwxr-xr-x@    - enrikes 12 Dec 13:04  kotlin
drwxr-xr-x@    - enrikes 25 Oct 08:13  rose-pine-theme
```


Release Notes:

- Fix broken symlinks when installing dev extensions

---------

Co-authored-by: Mikayla Maki <mikayla@zed.dev>
2025-01-21 17:09:16 +00:00
Kirill Bulatov
75c5344754 Fix completion labels becoming overly large due to LSP completion items with newlines (#23407)
Reworks https://github.com/zed-industries/zed/pull/23030 and
https://github.com/zed-industries/zed/pull/15087
Closes https://github.com/zed-industries/zed/issues/23352
Closes https://github.com/zed-industries/zed/issues/23310 

Zed's completion items use `label` from LSP completion items as a base
to show in the list:
d290da7dac/crates/project/src/lsp_store.rs (L4371-L4374)

Besides that, certain language plugins append `detail` or
`label_details.description` as a suffix:
d290da7dac/crates/languages/src/vtsls.rs (L178-L188)

Either of these 3 properties may return `\n` (or multiple) in it,
spoiling Zed's completion menu, which uses `UniformList` to render those
items: a uniform list uses common, minimum possible height for each
element, and `\n` bloats that overly.

Good approach would be to use something else:
https://github.com/zed-industries/zed/issues/21403 but that has its own
drawbacks and relatively hard to use instead (?).

We could follow VSCode's approach and move away all but `label` from
`CodeLabel.text` to the side, where the documentation is, but that does
not solve the issue with `details` having newlines.

So, for now, sanitize all labels and remove any newlines from them. If
newlines are found, also replace whitespace sequences if there's more
than 1 in a row.

Later, this approach can be improved similarly to how Helix and Zed's
inline completions do: rendering a "ghost" text, showing the
completion's edit applied to the editor.

Release Notes:

- Fixed completion labels becoming overly large due to LSP completion
items with newlines
2025-01-21 17:55:41 +02:00
Conrad Irwin
94189e1784 Add emacs mark mode (#23297)
Updates #21927
Replaces https://github.com/zed-industries/zed/pull/22904
Closes #8580

Adds actions (default keybinds with emacs keymap):
- editor::SetMark (`ctrl-space` and `ctrl-@`)
- editor::ExchangeMark (`ctrl-x ctrl-x`)

Co-Authored-By: Peter <peter@zed.dev>

Release Notes:

- Add Emacs mark mode (`ctrl-space` / `ctrl-@` to set mark; `ctrl-x
ctrl-x` to swap mark/cursor)
- Breaking change: `selection` keyboard context has been replaced with
`selection_mode`

---------

Co-authored-by: Peter <peter@zed.dev>
2025-01-21 10:18:25 -05:00
Danilo Leal
c4542ca731 zeta: Make the Jump to Edit callout pop up more (#23404)
| Before | After |
|--------|--------|
| <img width="1328" alt="Screenshot 2025-01-21 at 11 20 49 AM"
src="https://github.com/user-attachments/assets/ad8e3017-122a-4ebd-b1f5-5eb41cc3725a"
/> | <img width="1328" alt="Screenshot 2025-01-21 at 11 19 39 AM"
src="https://github.com/user-attachments/assets/a0dcbd52-6aca-43fa-97ee-6efde15c8bc1"
/> |

Release Notes:

- N/A
2025-01-21 11:40:02 -03:00
Danilo Leal
d011b97830 project_panel: Adjust entry background and border colors (#23403)
Follow up to https://github.com/zed-industries/zed/pull/22658

This PR ensures the background and border color of a project panel entry
is exactly the same with one exception: if the item is focused, active,
and not with mouse down. The point is to not be able to see the border
at all given they're there to act sort of akin to CSS's `outline` (which
doesn't add up to the box model).

Please let me know if there is any edge case I either messed up here or
didn't account for.


https://github.com/user-attachments/assets/29c74f6a-b027-4d19-a7de-b9614f0d7859

Release Notes:

- N/A
2025-01-21 11:13:46 -03:00
Piotr Osiewicz
f33d02c4c1 lsp_store: Do not associate a language server with the language for symbol highlighting (#23401)
This unblocks work on #22182; a single language server might actually be
required by multiple languages (think of e.g. C/C++,
Javascript/Typescript), in which case it doesn't make sense to use a
single grammar. We already use primary language of a buffer for
highlights and this PR makes this the only supported syntax highlighting
flavour for returned symbols.

Closes #ISSUE

Release Notes:

- N/A
2025-01-21 13:35:15 +01:00
Agus Zubiaga
e2c7934783 Show inline completion popover for hard to spot single edits (#23385)
If a suggested edit is a single character insert or a single line
deletion, we'll show the diff popover to make it stand out more.

Release Notes:

- N/A

Co-authored-by: Danilo <danilo@zed.dev>
2025-01-21 09:31:52 -03:00
Andrew Borg (Kashin)
d40177c2ae Fix project entry rename in Remote Development (#23382)
Closes #22883

To fix the problem, we move `handle_rename_project_entry` from
`Worktree` to `LspStore` and register it there. This way it becomes
available both in local and headless projects and this avoids the
duplication.

Release Notes:

- Fixed renaming project entries in Remote Development
2025-01-21 12:48:40 +01:00
Michael Sloan
cc1af7d96b Display keymap errors on initial load (#23394)
Also fixes issue introduced in #23113 where changes to keyboard layout
would not cause reload of keymap configuration.

Closes #20531

Release Notes:

- N/A
2025-01-21 07:14:46 +00:00
张小白
04c04e8406 windows: Fix FS-related issues (#23369)
I've noticed an occasional error: `ignoring event C:\some\path\to\file
outside of root path \\?\C:\some\path`. This happens because UNC paths
always fail to match with non-UNC paths during operations like
`strip_prefix` or `starts_with`. To address this, I changed the types of
some key parameters to `SanitizedPath`. With this adjustment, FS events
are now correctly identified, and under the changes in this PR, the
`test_rescan_and_remote_updates` test also passes successfully on
Windows.

Release Notes:

- N/A
2025-01-21 14:19:23 +08:00
Richard Feldman
8f87b5637a Wire up @mentioned files in assistant2 (#23389)
`@mention`ed files in assistant2 now get replaced by the full path of
the file in what gets sent to the model, while rendering visually as
just the filename (in a crease, so they can only be selected/deleted as
a whole unit, not character by character).


https://github.com/user-attachments/assets/a5867a93-d656-4a17-aced-58424c6e8cf6

Release Notes:

- N/A

---------

Co-authored-by: João Marcos <joao@zed.dev>
Co-authored-by: Conrad <conrad@zed.dev>
2025-01-20 21:33:18 -05:00
Michael Sloan
aacd80ee4a Prefer later bindings in keymap section for display in UI (#23378)
Closes #23015

Release Notes:

- Improved which keybindings are selected for display. Now later entries
within `bindings` will take precedence. The default keymaps have been
updated accordingly.
2025-01-20 23:20:15 +00:00
Agus Zubiaga
919703e6a8 Toggle inline completion menu from keyboard (#23380) 2025-01-20 19:11:13 -03:00
Cherry
3c0acdea5e docs: Add troubleshooting section warning about RUSTFLAGS env var (#23354)
According to #23223, manually setting `RUSTFLAGS` env var overrides
settings in `.cargo/config.toml`. Since users possibly may set their own
`RUSTFLAGS` when building, this creates an avenue where builds may fail
for really strange reasons that are difficult to debug.

This PR adds notes to the troubleshooting section to avoid setting
`RUSTFLAGS`, and offers alternatives which do not conflict.

This problem most recently affected nightly CI builders since we had
been setting `RUSTFLAGS` in our workflows to enable custom things like
gles or compiling with a specific target cpu. PR #23117 caused builds to
fail unless they were compiled with `-C target-feature=+crt-static`,
which due to this issue the `RUSTFLAGS` env var we set overrode the
`config.toml` compile flags, causing our builds to fail.

Release Notes:

- N/A
2025-01-20 15:43:14 -05:00
Agus Zubiaga
36c7b3eb91 Hide "Edit prediction" tooltip when menu is open (#23377)
Release Notes:

- N/A
2025-01-20 17:13:47 -03:00
Jules Bertholet
a22d8ef78f Add support for various action keys to Linux keymap (#22997)
Adds support for Cut, Copy, Paste, Undo, Redo, New, Open, Save, and Find
keys to the default keymap. These keys can be found on old keyboards,
but also custom layouts like
[Extend](https://dreymar.colemak.org/layers-extend.html).

Release Notes:

- Added support for the Cut, Copy, Paste, Undo, Redo, New, Open, Save,
and Find keys to the default keymap.
2025-01-20 12:28:34 -07:00
Michael Sloan
4b4876d48a Document KeymapFile and KeymapSection for display in hovers (#23375)
Release Notes:

- N/A
2025-01-20 19:16:16 +00:00
Cole Miller
fed4b48c57 git: Skip directories in git status output (#23300)
The output of `git status --porcelain=v1` includes untracked
directories, i.e. directories that have no tracked files beneath. Since
we have our own way of computing a "summary" status for each directory
in a repo, this is not helpful for Zed; and it interferes with our
handling of nested repos. So just skip these lines in the output.

Closes #23133 

Release Notes:

- Fix project panel colors when one git repository is nested beneath
another
2025-01-20 13:57:19 -05:00
Peter Tripp
5f59536208 Fix older Anthropic models not supporting -latest tags (#23372)
- Closes: https://github.com/zed-industries/zed/issues/22322
2025-01-20 13:19:15 -05:00
Elisiário Couto
73001a72e3 python: Add capture groups for builtin types, builtin attribute decorators, class inheritance, function arguments and definition keywords (#21454)
Add capture groups for builtin types, builtin attribute decorators,
class inheritance, function arguments and definition keywords.

Related to #14892 

Release Notes:

- Improved syntax highlight for Python: new capture groups for
`@function.arguments`, `@function.kwargs`, `@type.class.inheritance`,
`@keyword.definition`, `@attribute.builtin` and `@type.builtin`.
2025-01-20 19:14:19 +01:00
Marshall Bowers
3282398987 Update .mailmap (#23366)
This PR updates the `.mailmap` file to merge some more commit authors.

Release Notes:

- N/A
2025-01-20 16:32:34 +00:00
Marshall Bowers
8bd49b0966 git_ui: Capitalize co-author prefix (#23365)
This PR capitalizes the co-author prefix, as this is the way GitHub
writes it in their
[docs](https://docs.github.com/en/pull-requests/committing-changes-to-your-project/creating-and-editing-commits/creating-a-commit-with-multiple-authors#creating-co-authored-commits-on-the-command-line).

Release Notes:

- N/A
2025-01-20 16:00:47 +00:00
Danilo Leal
d0db05980e assistant2: Update message editor placeholder (#23363)
To make the mention and keyboard navigability discoverable.

Release Notes:

- N/A
2025-01-20 12:42:16 -03:00
Danilo Leal
40a6b0a0a3 assistant2: Adjust "Recent" header label design in context picker (#23364)
Just a little design fine-tune.

| Before | After |
|--------|--------|
| <img width="454" alt="Screenshot 2025-01-20 at 12 13 10 PM"
src="https://github.com/user-attachments/assets/b27372f2-00f5-40f4-927d-0d831ec4b90d"
/> | <img width="454" alt="Screenshot 2025-01-20 at 12 12 17 PM"
src="https://github.com/user-attachments/assets/207d08da-d75e-4c60-a6eb-cb1549b5925c"
/> |

Release Notes:

- N/A
2025-01-20 12:42:05 -03:00
Agus Zubiaga
919803a4f4 Require accepting ToS when enabling zeta (#23255)
Note: Design hasn't been reviewed yet, but the logic is done

When the user switches the inline completion provider to `zed`, we'll
show a modal prompting them to accept terms if they haven't done so:


https://github.com/user-attachments/assets/3fc6d368-c00a-4dcb-9484-fbbbb5eb859e

If they dismiss the modal, they'll be able to get to it again from the
inline completion button:


https://github.com/user-attachments/assets/cf842778-5538-4e06-9ed8-21579981cc47

This also stops zeta sending requests that will fail immediately when
ToS are not accepted.

Release Notes:

- N/A

---------

Co-authored-by: Richard <richard@zed.dev>
Co-authored-by: Danilo Leal <daniloleal09@gmail.com>
Co-authored-by: Joao <joao@zed.dev>
2025-01-20 11:48:49 -03:00
Thorsten Ball
5bb696e6d7 keybindings: Fix AcceptPartialInlineCompletion on macOS (#23357)
Related issue: https://github.com/zed-industries/zed/issues/20167

Release Notes:

- Changed the default keybinding to accept partial inline completions
from `ctrl-right` to `ctrl-cmd-right` on macOS, because `ctrl-right` is
already bound to jump to the end of the line.

Co-authored-by: Antonio <antonio@zed.dev>
Co-authored-by: Kirill <kirill@zed.dev>
Co-authored-by: Bennet <bennet@zed.dev>
2025-01-20 14:11:46 +01:00
Andrew Borg (Kashin)
571275e6c4 tab_switcher: Preserve selected position on closed tabs (#22861)
When the user closes a tab, the tab switcher will now select the tab at
the same position. This feature is especially relevant for keyboard
users when you want to close multiple consecutive tabs with
`<Ctrl-Backspace>`.

Please see the discussion at
https://github.com/zed-industries/zed/discussions/22791 for full
motivation and the quick demo.

Release Notes:

- tab_switcher: Preserve selected position when tab is closed
2025-01-20 14:15:25 +02:00
tims
938b7bb183 workspace: Add actions for cycling between windows (#23356)
Closes #22740

I haven't assigned any default keybindings to these actions because it
might conflict with existing OS bindings.

Preview:


https://github.com/user-attachments/assets/7c62cb34-2747-4674-a278-f0998e7d17f9

Release Notes:

- Added `workspace::ActivateNextWindow` and
`workspace::ActivatePreviousWindow` actions for cycling between windows.
2025-01-20 11:34:18 +00:00
Dino
0ff803fe10 workspace: Add action to move focused panel to next dock position (#23317)
This Pull Request introduces a new command `workspace: move focused
panel to next position` which finds the currently focused panel, if such
panel exists, and moves it to the next valid dock position, following
the order of `Left → Bottom → Right` and then starting again from the
left position.

In order to achieve this the following changes have been introduced:

* Add a new default implementation for `PanelHandle`, namely
`PanelHandle::move_to_next_position` which leverages
`PanelHandle::position`, `PanelHandle::position_is_valid` and
`PanelHandle::set_position` methods to update the panel's position to
the next valid position.
* Add a new method to the `workspace` module, `
move_focused_panel_to_next_position`, which is responsible for finding
the currently focused panel, if such a panel exists, and calling the
`move_to_next_position` method in the panel's handle.
* Add a new action to the `workspace` module,
`MoveFocusedPanelToNextPosition`, which is handled by the
`move_focused_panel_to_next_position` method.

Tests have also been added to the `workspace` module in order to
guarantee that the action is correctly updating the focused panel's
position.

Here's a quick video of it, in action 🔽 


https://github.com/user-attachments/assets/264d382b-5239-40aa-bc5e-5d569dec0734

Closes #23115 

Release Notes:

- Added new command to move the focused panel to the next valid dock
position – `workspace: move focused panel to next position` .
2025-01-20 13:07:11 +02:00
张小白
7302be8ebd lsp: Use Path instead of String for path handling (#22762)
During my work on PR #22616, while trying to fix the
`test_reporting_fs_changes_to_language_servers` test case, I noticed
that we are currently handling paths using `String` in some places.
However, this approach causes issues on Windows.

This draft PR modifies `rebuild_watched_paths_inner` and
`glob_literal_prefix`. For example, take the `glob_literal_prefix`
function modified in this PR:

```rust
assert_eq!(
    glob_literal_prefix("node_modules/**/*.js"), 
    "node_modules"
);    // This works on Unix, fails on Windows

assert_eq!(
    glob_literal_prefix("node_modules\\**\\*.js"), 
    "node_modules"
);    // This works on Windows

assert_eq!(
    glob_literal_prefix("node_modules\\**/*.js"), 
    "node_modules"
);    // This fails on Windows
```

The current implementation treats path as `String` and relies on `\` as
the path separator on Windows, but on Windows, both `/` and `\` can be
used as separators. This means that `node_modules\**/*.js` is also a
valid path representation.

There are two potential solutions to this issue:

1. **Continue handling paths with `String`**, and on Windows, replace
all `/` with `\`.
2. **Use `Path` for path handling**, which is the solution implemented
in this PR.

### Advantages of Solution 1:
- Simple and direct.

### Advantages of Solution 2:
- More robust, especially in handling `strip_prefix`.

Currently, the logic for removing a path prefix looks like this:

```rust
let path = "/some/path/to/file.rs";
let parent = "/some/path/to";
// remove prefix
let file = path.strip_prefix(parent).unwrap();    // which is `/file.rs`
let file = file.strip_prefix("/").unwrap();
```

However, using `Path` simplifies this process and makes it more robust:

```rust
let path = Path::new("C:/path/to/src/main.rs");
let parent = Path::new("C:/path/to/src"); 
let file = path.strip_prefix(&parent).unwrap(); // which is `main.rs`

let path = Path::new("C:\\path\\to/src/main.rs");
let parent = Path::new("C:/path/to\\src\\"); 
let file = path.strip_prefix(&parent).unwrap(); // which is `main.rs`
```

Release Notes:

- N/A

---------

Co-authored-by: Kirill Bulatov <mail4score@gmail.com>
2025-01-19 21:07:39 +02:00
renovate[bot]
7578834d77 Update actions/upload-artifact digest to 65c4c4a (#23197)
This PR contains the following updates:

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

---

### 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:eyJjcmVhdGVkSW5WZXIiOiIzOS4xMDcuMCIsInVwZGF0ZWRJblZlciI6IjM5LjEwNy4wIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6W119-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-19 08:45:14 -05:00
AidanV
35dad05be9 nix: Update nix flake (#23343)
Closes #23342 

Ran `nix flake update` and did some cleanup in shell.nix to follow nix
[best
practices](https://discourse.nixos.org/t/how-to-solve-libstdc-not-found-in-shell-nix/25458/6).

Prior to running `nix flake update`
`strings "$(echo "$LD_LIBRARY_PATH" | tr : "\n" | grep
"gcc")/libstdc++.so.6" | grep "CXXABI_1.3.15" CXXABI_1.3.15`
Does not find `CXXABI_1.3.15`

After running `nix flake update`

`strings "$(echo "$LD_LIBRARY_PATH" | tr : "\n" | grep
"gcc")/libstdc++.so.6" | grep "CXXABI_1.3.15" CXXABI_1.3.15`
Finds `CXXABI_1.3.15`

Launching Zed 0.168.3 inside Zed's nix development shell now launches
with no errors.

Release Notes:

- N/A
2025-01-19 08:40:55 -05:00
张小白
1d5499bee7 windows: Rename some constants and functions in GPUI (#23348)
This PR renames the constants and functions previously introduced in
PR#23283. Since the changes are within the GPUI crate, I renamed these
from `**_ZED_**` to `**_GPUI_**`.


Release Notes:

- N/A
2025-01-19 15:11:40 +08:00
Michael Sloan
ac8220bb2e Overwrite gzip output without prompting in bundling scripts (#23340)
Release Notes:

- N/A
2025-01-18 22:30:50 +00:00
Michael Sloan
711dc21eb2 Load all key bindings that parse and use markdown in error notifications (#23113)
* Collects and reports all parse errors

* Shares parsed `KeyBindingContextPredicate` among the actions.

* Updates gpui keybinding and action parsing to return structured
errors.

* Renames "block" to "section" to match the docs, as types like
`KeymapSection` are shown in `json-language-server` hovers.

* Removes wrapping of `context` and `use_key_equivalents` fields so that
`json-language-server` auto-inserts `""` and `false` instead of `null`.

* Updates `add_to_cx` to take `&self`, so that the user keymap doesn't
get unnecessarily cloned.

In retrospect I wish I'd just switched to using TreeSitter to do the
parsing and provide proper diagnostics. This is tracked in #23333

Release Notes:

- Improved handling of errors within the user keymap file. Parse errors
within context, keystrokes, or actions no longer prevent loading the key
bindings that do parse.
2025-01-18 22:27:08 +00:00
Bennet Bo Fenner
c929533e00 editor: Hide horizontal scrollbar if not visible (#23337)
This PR fixes two visual issues, that were caused by the fact that we
were always painting the horizontal scrollbar even if there is no
horizontal scrolling possible

Obscuring deleted lines when using the inline assistant:

https://github.com/user-attachments/assets/f8460c3f-403e-40a6-8622-65268ba2d875

Cutting off text even when horizontal scrolling is not possible:

https://github.com/user-attachments/assets/23c909f7-1c23-4693-8edc-40a2f089d4a8

This issue was only present in some themes (e.g. Nord, Catpuccin)


Closes #22716

Release Notes:

- Fixed an issue where horizontal scrollbars of editors would always be
painted (even if there is no horizontal scrolling to be done)
2025-01-18 21:50:07 +00:00
Michael Sloan
8c09a3d5db assistant2: Use notify_async_err for error notifications (#23330)
Release Notes:

- N/A
2025-01-18 21:10:22 +00:00
Kirill Bulatov
0199eca289 Allow filling co-authors in the git panel's commit input (#23329)
https://github.com/user-attachments/assets/78db908e-cfe5-4803-b0dc-4f33bc457840


* starts to extract usernames out of `users/` GitHub API responses, and
pass those along with e-mails in the collab sessions as part of the
`User` data

* adjusts various prefill and seed test methods so that the new data can
be retrieved from GitHub properly

* if there's an active call, where guests have write permissions and
e-mails, allow to trigger `FillCoAuthors` action in the context of the
git panel, that will fill in `co-authored-by:` lines, using e-mail and
names (or GitHub handle names if name is absent)

* the action tries to not duplicate such entries, if any are present
already, and adds those below the rest of the commit input's text

Concerns:

* users with write permissions and no e-mails will be silently omitted
— adding odd entries that try to indicate this or raising pop-ups is
very intrusive (maybe, we can add `#`-prefixed comments?), logging seems
pointless

* it's not clear whether the data prefill will run properly on the
existing users — seems tolerable now, as it seems that we get e-mails
properly already, so we'll see GitHub handles instead of names in the
worst case. This can be prefilled better later.

* e-mails and names for a particular project may be not what the user
wants.
E.g. my `.gitconfig` has
```
[user]
    email = mail4score@gmail.com

# .....snip

[includeif "gitdir:**/work/zed/**/.git"]
    path = ~/.gitconfig.work
```

and that one has

```
[user]
    email = kirill@zed.dev
```

while my GitHub profile is configured so, that `mail4score@gmail.com` is
the public, commit e-mail.

So, when I'm a participant in a Zed session, wrong e-mail will be
picked.
The problem is, it's impossible for a host to get remote's collaborator
git metadata for a particular project, as that might not even exist on
disk for the client.

Seems that we might want to add some "project git URL <-> user name and
email" mapping in the settings(?).
The design of this is not very clear, so the PR concentrates on the
basics for now.

When https://github.com/zed-industries/zed/pull/23308 lands, most of the
issues can be solved by collaborators manually, before committing.

Release Notes:

- N/A
2025-01-18 22:57:17 +02:00
Michael Sloan
ac214c52c9 Delay hiding git blame tooltip (#22644)
It's easy to overshoot the bottom of the tooltip when cursoring to a
button, such as opening the commit from a blame tooltip. Before this
change the tooltip would immediately disappear, and now it sticks around
for a bit.

Also:

* Shares the implementation with `elements/text.rs`. This will
particularly be handy when it makes use of hoverable tooltips.

* Improves the fix to #21657.

- Now the element will no longer think it has an active tooltip that it
registers with the window.

- It will instead display the next available tooltip, whereas I believe
before the next available tooltip would be suppressed.

* Fixes bug where `cx.refresh()` wasn't called when text tooltip is
hidden due to a mouse down event.

* Ports over fix in https://github.com/zed-industries/zed/pull/14832 to
`elements/text.rs`

Release Notes:

- The tooltip for inline git blame now waits a bit before disappearing
when the mouse leaves it.
2025-01-18 20:52:14 +00:00
Michael Sloan
985544ffb9 assistant2: Try again with fix use of rust-analyzer with "workspace": false (#23331)
I thought #23326 did the trick, but it didn't

Release Notes:

- N/A
2025-01-18 20:33:24 +00:00
Michael Sloan
10f358633b lsp: Skip computation of edits_since_save when there are no disk based diagnostics (#23269)
Thought of this improvement while @ConradIrwin and I were looking into
whether this code is misbehaving. It seems not to be.

Release Notes:

- N/A
2025-01-18 13:28:44 -07:00
Michael Sloan
9a7b73b161 assistant2: fix use with rust-analyzer "workspace": false (#23326)
Before this was getting errors about `TestAppContext` not existing.

Release Notes:

- N/A
2025-01-18 18:19:16 +00:00
tims
8c92da45a9 terminal: Add scrollbar (#23256)
Closes #4798

This PR implements a scrollbar for the terminal by turning
`ScrollableHandle` into a trait, allowing us to implement a custom
scroll handle, `TerminalScrollHandle`. It works by converting terminal
lines into pixels that `ScrollableHandle` understands. When
`ScrollableHandle` provides a changed offset (e.g., when you drag the
scrollbar), we convert this pixel offset back into the number of lines
to scroll and update the terminal content accordingly.

While the current version works as expected, I believe the scrollbar's
offset updates could potentially be turned into an event. This event
could then be subscribed to in `TerminalView`, not needing to update the
terminal's offset in the `render` method as it might have performance
implications. Further ideas on this are welcome.

Preview:


https://github.com/user-attachments/assets/560f0aac-4544-4007-8f0b-8833386f608f

Todo:

- [x] Experiment with custom scrollbar responding to terminal mouse
scroll
- [x] Refactor existing scrollbar handle into a trait  
- [x] Update terminal to use the scrollbar trait instead of a custom
scrollbar implementation
- [x] Figure out how scrollbar events like mouse drag should notify the
terminal to update its state
- [x] Code clean up
- [x] Scrollbar hide setting for terminal

Release Notes:

- Added scrollbar to the terminal
2025-01-18 17:36:41 +01:00
张小白
728a874b1e windows: Improve foreground task dispatching on Windows (#23283)
Closes #22653

After some investigation, I found this bug is due to that sometimes
`foreground_task` is not dispatched to the main thread unless there is
user input. The current Windows implementation works as follows: when
the `WindowsDispatcher` receives a `foreground_task`, it adds the task
to a queue and uses `SetEvent(dispatch_event)` to notify the main
thread.

The main thread then listens for notifications using
`MsgWaitForMultipleObjects(&[dispatch_event])`.

Essentially, this is a synchronous method, but it is not robust. For
example, if 100 `foreground_task`s are sent, `dispatch_event` should
theoretically be triggered 100 times, and
`MsgWaitForMultipleObjects(&[dispatch_event])` should receive 100
notifications, causing the main thread to execute all 100 tasks.
However, in practice, some `foreground_task`s may not get a chance to
execute due to certain reasons.

As shown in the attached video, when I don't move the mouse, there are
about 20-30 `foreground_task`s waiting in the queue to be executed. When
I move the mouse, `run_foreground_tasks()` is called, which processes
the tasks in the queue.



https://github.com/user-attachments/assets/83cd09ca-4b17-4a1f-9a2a-5d1569b23483



To address this, this PR adopts an approach similar to `winit`. In
`winit`, an invisible window is created for message passing. In this PR,
we use `PostThreadMessage` to directly send messages to the main thread.

With this implementation, when 100 `foreground_task`s are sent, the
`WindowsDispatcher` uses `PostThreadMessageW(thread_id,
RUNNABLE_DISPATCHED)` to notify the main thread. This approach enqueues
100 `RUNNABLE_DISPATCHED` messages in the main thread's message queue,
ensuring that each `foreground_task` is executed as expected. The main
thread continuously processes these messages, guaranteeing that all 100
tasks are executed.

Release Notes:

- N/A
2025-01-18 23:43:56 +08:00
Antonio Scandurra
5138e6a3c7 Fix accepting partial inline completion (#23312)
Release Notes:

- Fixed a bug that could prevent accepting a partial inline completion.
2025-01-18 10:27:12 +00:00
Michael Sloan
bf0578e32a Remove gap in layout of notifications (#23303)
* Increases width of notification message to remove a gap

* Puts the close button in the top right

Release Notes:

- N/A

Co-authored-by: Nate <nate@zed.dev>
2025-01-17 23:56:45 +00:00
Danilo Leal
e338a177c5 assistant2: Adjust "generating" state design (#23299)
To ensure message readability is not affected in any way.


https://github.com/user-attachments/assets/9a2ad949-1a8a-4c31-ad3c-db70f48e5d98

Release Notes:

- N/A
2025-01-17 20:22:27 -03:00
Peter Tripp
a2385eb0fc issues: Add new core label 'discussion' (#23304)
Release Notes:

- N/A
2025-01-17 17:10:16 -05:00
Conrad Irwin
a247617d6f Revert "lsp: Parse LSP messages on background thread - again (#23122)" (#23301)
This reverts commit 1b3b825c7f.

When debugging git diffs we found that this introduced a re-ordering of
messages sent to the LSP:

* User hits "format"
* Zed adjusts spacing, and sends "spaces changed" to the LSP
* Zed sends "format" to LSP

With the async approach here, the format request can now arrive before
the space changed request.

You can reproduce this with `test_strip_whitespace_and_format_via_lsp`
under some conditions.

Release Notes:

- N/A
2025-01-17 15:06:10 -07:00
Agus Zubiaga
0dda9851b3 zeta: Request completion when jumping to diagnostic (#23292)
Release Notes:

- N/A

Co-authored-by: Antonio <antonio@zed.dev>
2025-01-17 18:41:45 -03:00
Agus Zubiaga
938e28f871 assistant2: Thread history keyboard navigation (#23145)
Open and delete threads via keyboard:


https://github.com/user-attachments/assets/79b402ad-a49d-4c52-9d46-28a7bf32ff1f



Note: this doesn't include navigation in the "recent threads" section of
the empty state

Release Notes:

- N/A
2025-01-17 18:41:17 -03:00
Cole Miller
5da67899b7 git: Implement commit creation (#23263)
- [x] Basic implementation
- [x] Disable commit buttons when committing is not possible (empty
message, no changes)
- [x] Upgrade GitSummary to efficiently figure out whether there are any
staged changes
- [x] Make CommitAll work
- [x] Surface errors with toasts
  - [x] Channel shutdown
  - [x] Empty commit message or no changes
  - [x] Failed git operations
- [x] Fix added files no longer appearing correctly in the project panel
(GitSummary breakage)
- [x] Fix handling of commit message

Release Notes:

- N/A

---------

Co-authored-by: Nate <nate@zed.dev>
2025-01-17 18:51:20 +00:00
Marshall Bowers
3767e7e5f0 html_to_markdown: Restore ability to publish (#23293)
This PR restores the ability to publish the `html_to_markdown` crate
after #23291.

This crate is [published](https://crates.io/crates/html_to_markdown) to
crates.io so that it can be consumed by extensions.

Release Notes:

- N/A
2025-01-17 17:57:13 +00:00
Piotr Osiewicz
c9534e8025 chore: Use workspace fields for edition and publish (#23291)
This prepares us for an upcoming bump to Rust 2024 edition.

Release Notes:

- N/A
2025-01-17 17:39:22 +01:00
Marshall Bowers
cb35b73020 Extract PromptLibrary to prompt_library (#23285)
This PR extracts the `PromptLibrary` out of the `assistant` crate and
moves it to the `prompt_library` crate.

The `PromptLibrary` is now decoupled from the specifics of the
`AssistantPanel` and `InlineAssistant`.

Release Notes:

- N/A
2025-01-17 15:28:27 +00:00
Marshall Bowers
81dd68d696 Update Cargo.lock (#23284)
This PR updates the `Cargo.lock` file, as running `cargo check` was
producing a diff on `main`.

Release Notes:

- N/A
2025-01-17 14:50:28 +00:00
Danilo Leal
2edeb89a84 assistant2: Adjust "you" message block design (#23281)
Just removing the extra indentation margin.

Release Notes:

- N/A
2025-01-17 10:59:03 -03:00
Danilo Leal
da1c3d8b40 Add hover_line_number color token (#23279)
This enables having a dedicated color for the line number hover state.
That's relevant because line numbers can now be clicked to jump to
cursor location in multibuffers.

Release Notes:

- N/A

---------

Co-authored-by: João Marcos <marcospb19@hotmail.com>
2025-01-17 09:24:42 -03:00
Kirill Bulatov
37dcca62b7 Fix outline panel navigation in unnamed files (#23273)
Closes https://github.com/zed-industries/zed/issues/23250

Release Notes:

- Fixed outline panel navigation in unnamed files
2025-01-17 09:32:22 +00:00
renovate[bot]
c6f2326a29 Update aws-sdk-rust monorepo (#23199)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [aws-config](https://redirect.github.com/smithy-lang/smithy-rs) |
dependencies | patch | `1.5.13` -> `1.5.14` |
| [aws-sdk-kinesis](https://redirect.github.com/awslabs/aws-sdk-rust) |
dependencies | minor | `1.55.0` -> `1.56.0` |
| [aws-sdk-s3](https://redirect.github.com/awslabs/aws-sdk-rust) |
dependencies | minor | `1.68.0` -> `1.69.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:eyJjcmVhdGVkSW5WZXIiOiIzOS4xMDcuMCIsInVwZGF0ZWRJblZlciI6IjM5LjEwNy4wIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6W119-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-17 10:41:04 +02:00
Aaron Feickert
4ed3c133cf Remember active panel after closing dock (#23207)
A recent change in #22730 (to reduce workspace serialization) means that
a dock "forgets" its active panel whenever it is closed. When opened
again, the change in #22346 (which establishes a panel activation order)
takes effect, always opening the highest-priority panel for that dock
instead of the panel the user previously manually activated.

The result is that if you have, say, the outline panel active on the
right dock, and toggle the dock closed and then back open again, the
assistant panel will always appear instead.

This PR reverts part of the change in #22730 to ensure a dock remembers
its active panel when it is closed.

Closes #22923.

Release Notes:

- Fixed an issue where docks did not remember the active panel.
2025-01-17 10:01:50 +02:00
Andrew Borg (Kashin)
2ef4883937 terminal: Drain task output on completion (#23085)
Now we ensure that task output is fully drained and printed to Zed
terminal pane on task completion.

This change depends on a recent change to alacritty_terminal crate:
5e78d20c70.

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

Release Notes:

- Fixed missing task terminal output on Linux for short-running commands
2025-01-17 08:00:53 +00:00
faint
d4d36d1adf windows: Fix app icon loading (#22918)
Closes #22602

Release Notes:

- N/A

---------

Co-authored-by: 张小白 <364772080@qq.com>
2025-01-17 07:53:16 +00:00
张小白
70db427fc8 windows: Make collab run on Windows (#23117)
I’ve also updated the documentation in
`development\local-collaboration.md` and
`docs\src\development\windows.md`.

Testing collab on my Windows machine:

![屏幕截图 2025-01-14
162021](https://github.com/user-attachments/assets/28b4a36a-e156-4012-981a-5d0a23dcc613)


Release Notes:

- N/A

---------

Co-authored-by: Marshall Bowers <elliott.codes@gmail.com>
2025-01-17 09:39:13 +02:00
tims
b1375ab946 project_panel: Fix crash when adding a new file or directory to the first folded directory (#23217)
Closes #23216

This crash happens in the `update_visible_entries` function, where we
calculate `ancestors` and `current_ancestor_depth`. `ancestors` is map
storing information about folded ancestors and `current_ancestor_depth`
is basically selected ancestor index in reverse order of visibility.

For example, before adding a new file or directory in `a/b/c`, the
`ancestors` might look like:

```jsonc
{
    "entry_id_of_c": {
            "current_ancestor_depth": 2,  // "a" is selected
            "ancestors": ["entry_id_of_a", "entry_id_of_b", "entry_id_of_c"]
    }
}
```

When new file or directory is added to`a`, ancestors length is reduced,
as `a` now is not part of folded dir due to having multiple children.

But depth still remains the same as while calculating it, we use depth
from `old_ancestors` to preserve selection across renders. This causes
panic.

```jsonc
{
    "entry_id_of_c": {
            "current_ancestor_depth": 2,  // wrong: use of old depth here causes panic
            "ancestors": ["entry_id_of_b", "entry_id_of_c"]  // correct: notice "a" is missing, as "a" now has multiple children
    }
}
```

This PR fixes it by capping depth so it don't exceed `ancestors` array.
This preserves existing depth as well as handles our edge case.

Release Notes:

- Fixed crash when adding a new file or directory to the first folded
directory
2025-01-16 23:34:44 -07:00
Conrad Irwin
f94efb5008 vim: ! support (#23169)
Closes #22885
Closes #12565 

This doesn't yet add history in the command palette, which is painfully
missing.

Release Notes:

- vim: Added `:!`, `:<range>!` and `:r!` support
- vim: Added `!` operator in normal/visual mode
2025-01-16 21:19:15 -07:00
tims
21e7765a48 project_panel: Add directory auto-expand after 500ms hover during dragging (#23080)
This PR resolves one part of issue #14496 

In project panel, when dragging, if you hover over a directory for
~500ms, it now auto-expands so you can drag and drop into nested
directories.

Task cleanup is handled in these cases:  
- Dragged onto a different entry.  
- Dragged anywhere else, and the 500ms timer runs out (for example, out
of the project panel).
- Dropped onto any entry.  

I don’t see any edge cases where task isn’t cleaned up after 500ms.


https://github.com/user-attachments/assets/19da0da1-f9e2-42df-8ee4-fab6dc9a185a

Release Notes:

- Added auto-expand for directories on hover for a while during
dragging.
2025-01-16 20:54:37 -07:00
CharlesChen0823
f0a07b5eff editor: Fix editor: copy path not working in SSH remoting (#23235)
Closes #23135 

Release Notes:

- Fix `editor: copy path` not work in ssh remote
2025-01-16 20:47:47 -07:00
João Marcos
7ee78a4d35 Fix active line number highlight (#23266)
Closes #22734

Release Notes:

- Fixed active line number highlight.

---------

Co-authored-by: Danilo <danilo@zed.dev>
2025-01-17 01:23:28 +00:00
renovate[bot]
b472bd992f Update Rust crate tree-sitter-css to v0.23.2 (#23205)
This PR contains the following updates:

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

---

### Release Notes

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

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

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

**NOTE:** Download `tree-sitter-css.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:eyJjcmVhdGVkSW5WZXIiOiIzOS4xMDcuMCIsInVwZGF0ZWRJblZlciI6IjM5LjEwNy4wIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6W119-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-16 18:38:33 -05:00
Piotr Osiewicz
795376cb07 ui: Track changes to UI font size made via actions with settings (#23265)
Fixes #5380

Closes #5380

Release Notes:

- Font size changes made with actions are now persisted in user settings
2025-01-16 23:28:18 +00:00
Marshall Bowers
24495f09f9 Add streaming_diff crate (#23264)
This PR extracts the streaming diff implementation to its own
`streaming_diff` crate.

It was duplicated between `assistant` and `assistant2`, but their
implementations were exactly the same (and I don't see a reason why they
would need to diverge).

Release Notes:

- N/A
2025-01-16 23:12:46 +00:00
Marshall Bowers
4d22f7e529 assistant: Remove some re-exports (#23262)
This PR removes some re-exports from the `assistant` to make it clearer
what its constituent modules depend on.

Release Notes:

- N/A
2025-01-16 22:49:14 +00:00
Marshall Bowers
8030c0025a Extract slash commands to their own crate (#23261)
This PR extracts the slash command definitions out of the `assistant`
crate and into their own `assistant_slash_commands` crate.

Release Notes:

- N/A
2025-01-16 22:17:07 +00:00
Cole Miller
1a8303b020 git: Migrate some panel code away from visible_entries (#23251)
To prepare for the introduction of folding in the git panel, these
codepaths need to work with the canonical source of all git status
entries, not just the ones that are visible in the panel.

Release Notes:

- N/A

---------

Co-authored-by: Nate <nate@zed.dev>
2025-01-16 16:32:11 -05:00
Marshall Bowers
1b1c2e55f3 Extract PromptStore and PromptBuilder to new prompt_library crate (#23254)
This PR adds a new `prompt_library` crate and extracts the `PromptStore`
and `PromptBuilder` to it.

Eventually we'll want to house the `PromptLibrary` itself in this crate,
but right now that involves untangling a few dependencies.

Release Notes:

- N/A
2025-01-16 20:06:16 +00:00
Marshall Bowers
c9f24c7d45 Move SlashCommandWorkingSet to assistant_slash_command (#23252)
This PR moves the `SlashCommandWorkingSet` out of the `assistant` crate
and into `assistant_slash_command`.

This will unlock moving some things that depend on it out of the
`assistant` crate.

Release Notes:

- N/A
2025-01-16 19:13:30 +00:00
Cole Miller
b7726238ad Move git state to Project (#23208)
This restores its visibility outside of git_ui, which we'll need soon,
while preserving its per-project character.

Release Notes:

- N/A
2025-01-16 13:57:28 -05:00
Mikhail Filippov
614eaec278 Add mold dependency to fix build on Ubuntu 24.10 (#23230)
Co-authored-by: Peter Tripp <peter@zed.dev>
2025-01-16 13:43:47 -05:00
Joseph T. Lyons
415ecaff4a Rename edit prediction provider name and display name (#23249)
Release Notes:

- N/A
2025-01-16 18:27:31 +00:00
Peter Tripp
8e1ad7d475 docs: Add Shell Script language documentation (#23248) 2025-01-16 12:16:38 -05:00
Peter Tripp
ffc6b7b102 Make docs-only PRs skip CI.yml test suite (15 secs instead of 15 minutes) (#23246)
These were previously in place, but removed while we evaluated Merge Queues (since reverted).
2025-01-16 12:09:54 -05:00
renovate[bot]
70547ea5f6 Update Rust crate bitflags to v2.8.0 (#23206)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [bitflags](https://redirect.github.com/bitflags/bitflags) |
workspace.dependencies | minor | `2.6.0` -> `2.8.0` |

---

### Release Notes

<details>
<summary>bitflags/bitflags (bitflags)</summary>

###
[`v2.8.0`](https://redirect.github.com/bitflags/bitflags/blob/HEAD/CHANGELOG.md#280)

[Compare
Source](https://redirect.github.com/bitflags/bitflags/compare/2.7.0...2.8.0)

#### What's Changed

- feat(core): Add bitflags_match macro for bitflag matching by
[@&#8203;YuniqueUnic](https://redirect.github.com/YuniqueUnic) in
[https://github.com/bitflags/bitflags/pull/423](https://redirect.github.com/bitflags/bitflags/pull/423)
- Finalize bitflags_match by
[@&#8203;KodrAus](https://redirect.github.com/KodrAus) in
[https://github.com/bitflags/bitflags/pull/431](https://redirect.github.com/bitflags/bitflags/pull/431)

#### New Contributors

- [@&#8203;YuniqueUnic](https://redirect.github.com/YuniqueUnic) made
their first contribution in
[https://github.com/bitflags/bitflags/pull/423](https://redirect.github.com/bitflags/bitflags/pull/423)

**Full Changelog**:
https://github.com/bitflags/bitflags/compare/2.7.0...2.8.0

###
[`v2.7.0`](https://redirect.github.com/bitflags/bitflags/blob/HEAD/CHANGELOG.md#270)

[Compare
Source](https://redirect.github.com/bitflags/bitflags/compare/2.6.0...2.7.0)

#### What's Changed

- Fix `clippy::doc_lazy_continuation` lints by
[@&#8203;waywardmonkeys](https://redirect.github.com/waywardmonkeys) in
[https://github.com/bitflags/bitflags/pull/414](https://redirect.github.com/bitflags/bitflags/pull/414)
- Run clippy on extra features in CI. by
[@&#8203;waywardmonkeys](https://redirect.github.com/waywardmonkeys) in
[https://github.com/bitflags/bitflags/pull/415](https://redirect.github.com/bitflags/bitflags/pull/415)
- Fix CI: trybuild refresh, allow some clippy restrictions. by
[@&#8203;waywardmonkeys](https://redirect.github.com/waywardmonkeys) in
[https://github.com/bitflags/bitflags/pull/417](https://redirect.github.com/bitflags/bitflags/pull/417)
- Update zerocopy version in example by
[@&#8203;KodrAus](https://redirect.github.com/KodrAus) in
[https://github.com/bitflags/bitflags/pull/422](https://redirect.github.com/bitflags/bitflags/pull/422)
- Add method to check if unknown bits are set by
[@&#8203;wysiwys](https://redirect.github.com/wysiwys) in
[https://github.com/bitflags/bitflags/pull/426](https://redirect.github.com/bitflags/bitflags/pull/426)
- Update error messages by
[@&#8203;KodrAus](https://redirect.github.com/KodrAus) in
[https://github.com/bitflags/bitflags/pull/427](https://redirect.github.com/bitflags/bitflags/pull/427)
- Add `truncate(&mut self)` method to unset unknown bits by
[@&#8203;wysiwys](https://redirect.github.com/wysiwys) in
[https://github.com/bitflags/bitflags/pull/428](https://redirect.github.com/bitflags/bitflags/pull/428)
- Update error messages by
[@&#8203;KodrAus](https://redirect.github.com/KodrAus) in
[https://github.com/bitflags/bitflags/pull/429](https://redirect.github.com/bitflags/bitflags/pull/429)

#### New Contributors

- [@&#8203;wysiwys](https://redirect.github.com/wysiwys) made their
first contribution in
[https://github.com/bitflags/bitflags/pull/426](https://redirect.github.com/bitflags/bitflags/pull/426)

**Full Changelog**:
https://github.com/bitflags/bitflags/compare/2.6.0...2.7.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:eyJjcmVhdGVkSW5WZXIiOiIzOS4xMDcuMCIsInVwZGF0ZWRJblZlciI6IjM5LjEwNy4wIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6W119-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-16 11:02:31 -05:00
Danilo Leal
8a0c22c3bf assistant2: Add ChatMode action (#23243)
This PR makes the assistant 2 panel switch work with the keyboard via
the `cmd-e` keybinding.

Release Notes:

- N/A

---------

Co-authored-by: Agus Zubiaga <hi@aguz.me>
2025-01-16 13:02:22 -03:00
renovate[bot]
9ea8b14ac3 Update Rust crate cbindgen to 0.28.0 (#23209)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [cbindgen](https://redirect.github.com/mozilla/cbindgen) |
build-dependencies | minor | `0.27.0` -> `0.28.0` |

---

### Release Notes

<details>
<summary>mozilla/cbindgen (cbindgen)</summary>

###
[`v0.28.0`](https://redirect.github.com/mozilla/cbindgen/blob/HEAD/CHANGES#0280)

[Compare
Source](https://redirect.github.com/mozilla/cbindgen/compare/v0.27.0...0.28.0)

- Parse unsafe attributes in
[https://github.com/mozilla/cbindgen/pull/1020](https://redirect.github.com/mozilla/cbindgen/pull/1020)
\* Fix local override of enum prefix-with-name by
jsg[https://github.com/mozilla/cbindgen/pull/1006](https://redirect.github.com/mozilla/cbindgen/pull/1006)/1006
\* Add
rename-all=[https://github.com/mozilla/cbindgen/pull/1021](https://redirect.github.com/mozilla/cbindgen/pull/1021)/pull/1021
\* ir: add support for UnsafeCell and SyncUnsafeCell
[https://github.com/mozilla/cbindgen/pull/1003](https://redirect.github.com/mozilla/cbindgen/pull/1003)ndgen/pull/1003
\* Implement
man[https://github.com/mozilla/cbindgen/pull/1022](https://redirect.github.com/mozilla/cbindgen/pull/1022)a/cbindgen/pull/1022
\* Fix: Ignore `CARGO_BUILD_TARGET` in tests by bryango in
[https://github.com/mozilla/cbindgen/pull/1010](https://redirect.github.com/mozilla/cbindgen/pull/1010)
\* Newline for each field for constexpr field constants by
youknowon[https://github.com/mozilla/cbindgen/pull/988](https://redirect.github.com/mozilla/cbindgen/pull/988)l/988
\* Fix clippy warnings by
youk[https://github.com/mozilla/cbindgen/pull/1026](https://redirect.github.com/mozilla/cbindgen/pull/1026)/pull/1026
\* Add aarch64/arm64 to
C[https://github.com/mozilla/cbindgen/pull/1036](https://redirect.github.com/mozilla/cbindgen/pull/1036)ndgen/pull/1036
\* Add `unstable_ir` feature flag that makes the ir pub by heesooy in
[https://github.com/mozilla/cbindgen/pull/1011](https://redirect.github.com/mozilla/cbindgen/pull/1011)
\* Support generated a symbols file by
TheElectronWil[https://github.com/mozilla/cbindgen/pull/916](https://redirect.github.com/mozilla/cbindgen/pull/916)l/916

</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:eyJjcmVhdGVkSW5WZXIiOiIzOS4xMDcuMCIsInVwZGF0ZWRJblZlciI6IjM5LjEwNy4wIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6W119-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-16 11:01:51 -05:00
Peter Tripp
ebb937d88c Move 'pane::ReopenClosedItem' keybinds from Pane to Workspace context (#23242)
Makes pane::ReopenClosedItem (`cmd-shift-t` macos / `ctrl-shift-t`
linux) work in Project Panel and other non-`Pane` Dock contexts too
(Diagnostics, Outline, Git, Collab).
2025-01-16 15:53:35 +00:00
Danilo Leal
9f52683ebc Remove the SwitchWithLabel component (#23240)
This PR removes the `SwitchWithLabel` component because we're adding
`label` as a method to `Switch`. Thus, we no longer need an extra
component just to append a label. Additionally, we're also adding
`keybinding` as a method.

Release Notes:

- N/A

---------

Co-authored-by: Agus Zubiaga <hi@aguz.me>
2025-01-16 12:18:59 -03:00
Peter Tripp
fd40d173f3 macos: Document ctrl-space global shortcut conflict (#23239) 2025-01-16 14:51:59 +00:00
Marshall Bowers
3af09bbae1 ci: Remove zero-width whitespace in job name (#23238)
This PR removes some zero-width whitespace characters from one of the CI
job names.

Release Notes:

- N/A
2025-01-16 14:50:40 +00:00
Danilo Leal
5c5a938ecf Expose a theme preview keybinding function (#23237)
This is useful if we want to pass random strings as keybindings for any
component that takes one, so we can display them on the debug theme
preview pane.

Release Notes:

- N/A

Co-authored-by: Agus Zubiaga <hi@aguz.me>
2025-01-16 11:43:56 -03:00
张小白
92f05d1ab1 Fix configuration issue in CI runners (#23223)
While working on PR #23117, I noticed that the Windows runner in our CI
setup doesn't seem to respect the settings defined in
`.cargo/config.toml`. With @SomeoneToIgnore ’s help, Kirill and I
realized this issue isn’t limited to the Windows runner—all of our
runners disregard the configurations in `.cargo/config.toml`.

Later, @osiewicz suggested an excellent workaround. I conducted some
tests on PR #23117 and found that the solution works as intended.

Personally, I prefer using environment variables for global
configuration. However, according to the documentation
[here](https://doc.rust-lang.org/cargo/reference/config.html), it seems
that environment variables always override the settings in
`.cargo/config.toml`.

Release Notes:

- N/A
2025-01-16 21:01:47 +08:00
Michael Sloan
f51db18b3c Add VIM_KEYMAP_PATH constant (#23228)
Deduplicates 2 occurrences

Release Notes:

- N/A
2025-01-16 11:31:01 +00:00
Michael Sloan
972176a574 Avoid doing string manipulation on render for single line label (#23227)
Release Notes:

- N/A
2025-01-16 11:15:45 +00:00
Michael Sloan
8e6fc3c807 Implement better markdown escaping and inline code escape (#23222)
Motivation for this is using markdown for keymap error notifications in
#23113, but it also benefits the copied text of repl tables.

Release Notes:

- N/A
2025-01-16 11:06:57 +00:00
Michael Sloan
5fdd7edb90 Add support for showing notification to active workspace from AppContext (#23226)
Falls back on notifying all workspaces if there isn't an active one.

This is to support notifying the user about keymap file errors in
#23113. It will also be useful for notifying about settings file errors.

Release Notes:

- N/A
2025-01-16 11:06:08 +00:00
Michael Sloan
55e1e831a1 Make language registry optional in parse_markdown (#23221)
Motivation for this is using markdown for keymap error notifications in
#23113

Release Notes:

- N/A
2025-01-16 03:56:46 -07:00
Michael Sloan
0dbe34d2ae Make TextSize::rems take AppContext instead of WindowContext (#23220)
Motivation for this change is #23113, but this will also be the state of
it after the gpui refactor.

Release Notes:

- N/A
2025-01-16 03:56:36 -07:00
Antonio Scandurra
880f3ff243 Timeout if completion takes longer than 2s (#23215)
Release Notes:

- N/A
2025-01-16 11:13:25 +01:00
Cole Miller
a41d72ee81 Represent git statuses more faithfully (#23082)
First, parse the output of `git status --porcelain=v1` into a
representation that can handle the full "grammar" and doesn't lose
information.

Second, as part of pushing this throughout the codebase, expand the use
of the existing `GitSummary` type to all the places where status
propagation is in play (i.e., anywhere we're dealing with a mix of files
and directories), and get rid of the previous `GitSummary ->
GitFileStatus` conversion.

- [x] Synchronize new representation over collab
  - [x] Update zed.proto
  - [x] Update DB models
- [x] Update `GitSummary` and summarization for the new `FileStatus`
- [x] Fix all tests
  - [x] worktree
  - [x] collab
- [x] Clean up `FILE_*` constants
- [x] New collab tests to exercise syncing of complex statuses
- [x] Run it locally and make sure it looks good

Release Notes:

- N/A

---------

Co-authored-by: Mikayla <mikayla@zed.dev>
Co-authored-by: Conrad <conrad@zed.dev>
2025-01-16 00:01:38 +00:00
renovate[bot]
224f3d4746 Update Rust crate sea-orm to v1.1.4 (#23202)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [sea-orm](https://www.sea-ql.org/SeaORM)
([source](https://redirect.github.com/SeaQL/sea-orm)) | dev-dependencies
| patch | `1.1.3` -> `1.1.4` |
| [sea-orm](https://www.sea-ql.org/SeaORM)
([source](https://redirect.github.com/SeaQL/sea-orm)) | dependencies |
patch | `1.1.3` -> `1.1.4` |

---

### Release Notes

<details>
<summary>SeaQL/sea-orm (sea-orm)</summary>

###
[`v1.1.4`](https://redirect.github.com/SeaQL/sea-orm/blob/HEAD/CHANGELOG.md#114---2025-01-10)

[Compare
Source](https://redirect.github.com/SeaQL/sea-orm/compare/1.1.3...1.1.4)

##### Enhancements

- Allow modifying the connection in migrations
[https://github.com/SeaQL/sea-orm/pull/2397](https://redirect.github.com/SeaQL/sea-orm/pull/2397)
- `DeriveRelatedEntity` proc_macro use `async-graphql` re-exported by
`seaography`
[https://github.com/SeaQL/sea-orm/pull/2469](https://redirect.github.com/SeaQL/sea-orm/pull/2469)

</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 these
updates again.

---

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

---

Release Notes:

- N/A

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzOS4xMDcuMCIsInVwZGF0ZWRJblZlciI6IjM5LjEwNy4wIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6W119-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-15 18:53:11 -05:00
renovate[bot]
be7090c30d Update Rust crate log to v0.4.25 (#23200)
This PR contains the following updates:

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

---

### Release Notes

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

###
[`v0.4.25`](https://redirect.github.com/rust-lang/log/blob/HEAD/CHANGELOG.md#0425---2025-01-14)

[Compare
Source](https://redirect.github.com/rust-lang/log/compare/0.4.24...0.4.25)

###
[`v0.4.24`](https://redirect.github.com/rust-lang/log/blob/HEAD/CHANGELOG.md#0424---2025-01-11)

[Compare
Source](https://redirect.github.com/rust-lang/log/compare/0.4.23...0.4.24)

###
[`v0.4.23`](https://redirect.github.com/rust-lang/log/blob/HEAD/CHANGELOG.md#0423---2025-01-10-yanked)

[Compare
Source](https://redirect.github.com/rust-lang/log/compare/0.4.22...0.4.23)

</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:eyJjcmVhdGVkSW5WZXIiOiIzOS4xMDcuMCIsInVwZGF0ZWRJblZlciI6IjM5LjEwNy4wIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6W119-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-15 18:34:43 -05:00
Marshall Bowers
f53915c711 Add infrastructure for loading icon themes from extensions (#23203)
This PR adds the supporting infrastructure to support loading icon
themes defined by extensions.

Here's an example icon theme:

```json
{
  "name": "My Icon Theme",
  "author": "Me <me@example.com>",
  "themes": [
    {
      "name": "My Icon Theme",
      "appearance": "dark",
      "file_icons": {
        "gleam": { "path": "./icons/file_type_gleam.svg" },
        "toml": { "path": "./icons/file_type_toml.svg" }
      }
    }
  ]
}
```

The icon paths are resolved relative to the root of the extension
directory.

Release Notes:

- N/A
2025-01-15 23:33:47 +00:00
renovate[bot]
3b8a5c9647 Update Rust crate proc-macro2 to v1.0.93 (#23201)
This PR contains the following updates:

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

---

### Release Notes

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

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

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

- Optimize TokenStream's Drop
([#&#8203;489](https://redirect.github.com/dtolnay/proc-macro2/issues/489),
[#&#8203;490](https://redirect.github.com/dtolnay/proc-macro2/issues/490),
thanks [@&#8203;WalkerKnapp](https://redirect.github.com/WalkerKnapp))

</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:eyJjcmVhdGVkSW5WZXIiOiIzOS4xMDcuMCIsInVwZGF0ZWRJblZlciI6IjM5LjEwNy4wIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6W119-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-15 23:25:16 +00:00
Marshall Bowers
a8526d9143 file_icons: Fall back to the default icon theme for icons (#23196)
This PR updates the various `FileIcons` methods to fall back to the
default icon theme if the active icon theme does not have the desired
icon.

Release Notes:

- N/A
2025-01-15 22:06:20 +00:00
Marshall Bowers
904b367882 ui: Add support for rendering Icons from external files (#23195)
This PR adds support for rendering `Icon`s from external files.
Previously this could only be used with icons embedded in the binary.

To achieve this we currently need to use the `img` element until the
`svg` element supports:

1. Loading SVGs from external files
2. Rendering polychrome SVGs

Release Notes:

- N/A
2025-01-15 21:50:16 +00:00
Cole Miller
e265e69429 git: Move all state into the panel (#23185)
This should fix the problem with the panel not updating when switching
projects.

Release Notes:

- N/A
2025-01-15 14:41:21 -05:00
João Marcos
d578f5ac37 Simplify static expression to a constant (#23193)
Release Notes:

- N/A
2025-01-15 19:31:21 +00:00
João Marcos
b3e8bb0ba6 Update docs for running collab locally (#23192)
Mention `cargo run -p collab -- serve all`, which seem to be the easier
way to run it.

Release Notes:

- N/A
2025-01-15 19:27:40 +00:00
Kirill Bulatov
2e959cb6d6 Make Linux audio public (#23191)
Release Notes:

- Added a way to use audio in rooms with Linux builds
2025-01-15 20:54:32 +02:00
Marshall Bowers
e215ca1d99 Dedupe AssistantSettings (#23190)
This PR dedupes the `AssistantSettings` so we can use the same settings
for both Assistant1 and Assistant2.

We originally forked them so we could change the Assistant2 settings
freely, but given our rollout strategy for the new Assistant, I don't
think that makes sense.

This also fixes the issue where the JSON language server would show a
"Matches multiple schemas when only one must validate" warning in
`settings.json`.

Closes #23171.

Release Notes:

- Fixed the "Matches multiple schemas when only one must validate"
warning for the `assistant` setting.
2025-01-15 18:52:54 +00:00
Kirill Bulatov
22f5fd53ca Rework inlay hint cache tests (#23156)
Closes https://github.com/zed-industries/zed/issues/7928

* uncomments and fixes all inlay hint cache tests
* fixes a bug, where invalidated range did not store the new queried
ranges in the cache: this resulted in extra requests in editor that do
not fit into the screen
* comments a peculiarity with the `RefreshInlayHints` event: all editors
react to that when a new language server is inserted, even though
certain editors are not related to the new language server
* fixes handling of inlay hints for the same position: now the same
order is kept, as in the language server's response
(https://github.com/zed-industries/zed/issues/7928)
* queries for hints when on excerpt(s) expansion

Release Notes:

- Fixed inlay hints handling for the same position
2025-01-15 20:13:15 +02:00
Peter Tripp
135e58f1e2 Bump Zed to v0.171 (#23188) 2025-01-15 12:54:47 -05:00
Peter Tripp
88cb9bbc04 Add fn-f keyboard shortcut for Fullscreen (mac-only) (#23184)
- See:
https://github.com/zed-industries/zed/issues/22674#issuecomment-2593133447

Release Notes:

- macos: Added `fn-f` keyboard shortcut for fullscreen toggle.
2025-01-15 12:40:56 -05:00
Peter Tripp
61db8beea4 ci: Cleanup for disabled Merge Queue / merge_group (#23187)
Only run actions dependency-review-action if running in a PR action.

This broke when run as part of action for commit on main and on a
preview branch:
- https://github.com/zed-industries/zed/actions/runs/12793068921/job/35664998296
- https://github.com/zed-industries/zed/actions/runs/12793045639

Originally introduced in:
- https://github.com/zed-industries/zed/pull/21424

But was only tested with `merge_group` which has since been reverted.
2025-01-15 12:27:16 -05:00
Marshall Bowers
72f1b32cc5 project_panel: Remove useless comments (#23186)
This PR removes some comments in the `project_panel` that are just
restating what the code is doing.

Release Notes:

- N/A
2025-01-15 17:20:36 +00:00
Peter Tripp
0150eaf8c7 Revert docs-only test-skipping with Merge Queue (#23180)
These checks were not functioning as intended. Notably tests were
skipped for today's hotfix release of Preview
[v0.169.2-pre](https://github.com/zed-industries/zed/actions/runs/12790602047):

Separately these checks were flawed as they would only be considered
"docs only" if the diff between the PR branch base and main also did not
have any subsequent non-docs changes.

Reverting until we can figure out something better.
2025-01-15 11:47:01 -05:00
Peter Tripp
cc8746a66b Increase timeout for macos release builds (#23183)
Today's Preview hotfix timed out during notarization:
- https://github.com/zed-industries/zed/actions/runs/12790602047/job/35656767355
2025-01-15 11:01:50 -05:00
Conrad Irwin
f50a118e78 Refactor shell wrapping (#23108)
I want to use this to implement ! in vim, so move it from terminal_view
to task, and split windows/non-windows more cleanly.

Release Notes:

- N/A
2025-01-15 08:45:48 -07:00
Conrad Irwin
45198f2af4 Add "tool" support to go.mod (#22995)
Closes #ISSUE

Release Notes:

- Fixed highlighting of ["tool"
directives](https://tip.golang.org/doc/go1.24#tools) in go.mod
2025-01-15 17:44:28 +02:00
Peter Tripp
67525cca71 Add ollama phi4 context size defaults (#23036)
Add `phi4` maximum context length (128K).
By default this clamps to `16384` but if you have enough video memory
you can set it higher or connect to a non-local machine via settings:

```json
"language_models": {
  "ollama": {
    "api_url": "http://localhost:11434",
    "available_models": [
      {
        "name": "phi4",
        "display_name": "Phi4 64K",
         "max_tokens": 65536
      }
    ]
  }
}
```

Release Notes:

- Improve support for Phi4 with ollama.
2025-01-15 17:44:15 +02:00
Kirill Bulatov
0e4a619c9f Revert "Log an error when there are no buffer snapshots for some LSP version (#22934)" (#23179)
https://github.com/zed-industries/zed/pull/22934#issuecomment-2592239448
and myself had noted quite an increase in junk logging after that:


https://github.com/user-attachments/assets/b678d4ec-c301-4d0e-9a12-99aa7f6da0a2


Release Notes:

- N/A
2025-01-15 17:42:41 +02:00
Cole Miller
74620e611e Improve performance of go-to-diagnostic when many diagnostics are present (#23166)
Instead of eagerly calling `to_offset` on the anchor ranges for each
diagnostic in the direction of the search, work lazily in terms of
anchors and convert to offsets at the very end.

Release Notes:

- N/A
2025-01-15 15:02:35 +00:00
Conrad Irwin
9d3a0594f9 Exclude function keys from input handler (#23070)
Fixes #22674

Release Notes:

- Fixed a bug binding to `fn-X` (where X is a printing key) on macOS
2025-01-15 14:33:28 +00:00
Thorsten Ball
b1cfc116d0 edit prediction: Fix width of completion item (#23177)
Release Notes:

- N/A

Co-authored-by: Agus <agus@zed.dev>
Co-authored-by: Bennet <bennet@zed.dev>
Co-authored-by: Antonio <antonio@zed.dev>
2025-01-15 14:15:50 +00:00
Agus Zubiaga
4a7630204a Check for predict-edits feature flag, remove is_staff check (#23165)
Release Notes:

- N/A

---------

Co-authored-by: Thorsten Ball <mrnugget@gmail.com>
2025-01-15 13:52:10 +00:00
Antonio Scandurra
da8e65b3e5 Show loading state for predictions (#23172)
Release Notes:

- N/A

---------

Co-authored-by: Thorsten <thorsten@zed.dev>
Co-authored-by: Danilo Leal <daniloleal09@gmail.com>
Co-authored-by: Agus Zubiaga <hi@aguz.me>
2025-01-15 13:05:18 +00:00
Thorsten Ball
bf75b33464 vim: Fix inline completions not disappearing in normal mode (#23176)
Closes #23042

Release Notes:

- Fixed inline completions (Copilot, Supermaven, ...) still being
visible sometimes after leaving Vim's insert mode.
2025-01-15 12:44:56 +00:00
Bennet Bo Fenner
bd3f64c5a1 zeta: Allow viewing prompt details in rate completion modal (#23142)
Co-Authored-by: Danilo <danilo@zed.dev>

Release Notes:

- N/A

---------

Co-authored-by: Danilo <danilo@zed.dev>
Co-authored-by: Danilo Leal <daniloleal09@gmail.com>
Co-authored-by: Thorsten Ball <mrnugget@gmail.com>
2025-01-15 11:10:46 +00:00
Thorsten Ball
ae746937af settings: Rename 'zeta' to 'zed' (#23174)
Old:

```settings.json
{
  "features": {
    "inline_completion_provider": "zeta"
  }
}
```

New & cool:

```settings.json
{
  "features": {
    "inline_completion_provider": "zed"
  }
}
```

Release Notes:

- N/A
2025-01-15 10:53:30 +00:00
Conrad Irwin
37c2ebed7e Revert "linux: Fix saving file with root ownership (#22045)" (#23162)
Release Notes:

- (temporarily) Removes the linux "save file as root" feature while we
figure out bugs.

Updates https://github.com/zed-industries/zed/pull/22045
2025-01-15 05:17:08 +00:00
Cole Miller
e86fe1d0b9 Fix git commands for staging and unstaging (#23147)
This fixes a bug that prevents unstaging added files.

I've also removed the batching/debouncing logic in the long-running task
that launches the git invocations---I added this originally but I don't
think it's really necessary.

Release Notes:

- N/A
2025-01-15 00:49:07 +00:00
Marshall Bowers
de6216a02b ui: Move IconDecoration and DecoratedIcon to their own modules (#23157)
This PR moves the `IconDecoration` and `DecoratedIcon` components to
their own modules.

Release Notes:

- N/A
2025-01-15 00:27:26 +00:00
Marshall Bowers
167c564509 theme: Pull directory and chevron icons out of IconTheme::file_icons (#23155)
This PR pulls the directory and chevron icons out of the
`IconTheme::file_icons` collection and promotes them to named fields.

This makes things less stringly-typed when looking up these icons.

Release Notes:

- N/A
2025-01-14 23:53:38 +00:00
Marshall Bowers
1178b3e5f2 gpui: Clean up AppContext doc comments (#23154)
This PR cleans up some doc comments for the `AppContext.

Release Notes:

- N/A
2025-01-14 23:24:34 +00:00
Marshall Bowers
88e42cc7aa Refactor file icons to use IconTheme (#23153)
This PR adds the initial concept of an `IconTheme` and refactors
`FileIcons` to use it to resolve the icons.

The `IconTheme` will ultimately be used to allow users to select a
different set of icons to use. Currently, however, this is just laying
the foundation for that work.

The association between file types and icons is now handled by the icon
theme when we resolve file icons. This mapping has been moved out of
`file_types.json` and into `icon_theme.rs`.

Release Notes:

- N/A
2025-01-14 22:49:36 +00:00
Danilo Leal
07d582401a assistant2: Revise thread visual design (#23083)
This PR adjusts the design of the assistant 2 threads with the goal of
reducing visual busyness. My intention is to remove the amount of lines
and borders given it is a relatively tight space. It also refines the
"generating" floating container style, finally leveraging linear
gradients that were recently added to GPUI! Now, we only display headers
for "you" messages. Assistant responses will be rendered right in the
panel; not bounded by a card container.

<img width="800" alt="Screenshot 2025-01-14 at 7 08 39 PM"
src="https://github.com/user-attachments/assets/a8ffa780-0ef2-4d4b-ae19-3f02fd2d63a6"
/>

Release Notes:

- N/A
2025-01-14 22:29:39 +00:00
Joseph T. Lyons
077767a3b0 Migrate more events to telemetry::event (#22178)
Release Notes:

- N/A
2025-01-14 21:00:24 +00:00
Peter Tripp
b7fd5718a3 Revert "Add emacs keybindings for mark emulation" (#23146)
- Reverts zed-industries/zed#22904
- See also: https://github.com/zed-industries/zed/issues/8580

After using it full-time for a day I very much think an implicit "mark
mode" when the emacs base keymap is enabled is the wrong approach.

Release Notes:

- Reverted "Add emacs keybindings for mark emulation" #23146 (main only)
2025-01-14 20:56:04 +00:00
Yagil Burowski
c038696aa8 Add LM Studio support to the Assistant (#23097)
#### Release Notes:

- Added support for [LM Studio](https://lmstudio.ai/) to the Assistant.

#### Quick demo:


https://github.com/user-attachments/assets/af58fc13-1abc-4898-9747-3511016da86a

#### Future enhancements:
- wire up tool calling (new in [LM Studio
0.3.6](https://lmstudio.ai/blog/lmstudio-v0.3.6))

---------

Co-authored-by: Marshall Bowers <elliott.codes@gmail.com>
2025-01-14 20:41:58 +00:00
Kirill Bulatov
4445679f3c Fix a typo in the task example (#23148)
Release Notes:

- N/A
2025-01-14 20:33:28 +00:00
Nate Butler
a3e7444d77 Git panel polish (#23144)
- Clicking checkbox in the header stages or unstages all changes
- Adds tooltips to header checkbox
- Addis the ability for checkboxes to have tooltips
- Ensure an entry in the list is always selected
- Hide revert all button for now

Release Notes:

- N/A
2025-01-14 20:27:05 +00:00
Cole Miller
d13d099675 git: Restore basic jump-to-file functionality (#23140)
This just opens the file for the selected `GitListEntry` right now;
we'll add back integration with the project diff view later.

Release Notes:

- N/A

---------

Co-authored-by: Nate <nate@zed.dev>
2025-01-14 19:29:43 +00:00
Agus Zubiaga
de5f023477 assistant2: Cancel generation button (#23137)
Turns the "esc to cancel" label into a button so it can be dispatched
via click or keyboard. The keybinding isn't hardcoded anymore.

![CleanShot 2025-01-14 at 13 44
22@2x](https://github.com/user-attachments/assets/a947f58b-7de2-400b-b95a-384b78c79697)


Release Notes:

- N/A
2025-01-14 19:22:48 +00:00
Marshall Bowers
4febc7ea49 assistant2: Cancel pending completion when an error occurs (#23143)
This PR makes it so the pending completion is cleared when an error
occurs.

This makes it so `Thread::is_streaming()` will return `false` in the
error case (and thus hide the streaming indicator in the UI).

Release Notes:

- N/A
2025-01-14 19:04:47 +00:00
Thorsten Ball
c33eb012cf Change tooltip to 'Edit Prediction' (#23139)
Release Notes:

- N/A
2025-01-14 17:25:10 +00:00
Bennet Bo Fenner
1ddf754b8b zeta: Rework displaying paths in completion rating modal (#23129)
Two issues i ran into while looking at the completion rating modal
- Single-file worktrees file names are not displayed at all
- Hard to see the filename when the path is long (lots of directories)

This PR fixes this by displaying the filename on the left, followed by
the full path (including the worktree name), similar to how we do it in
the file finder/assistant panel /file command
| Before | After |
|--------|--------|
| <img width="1067" alt="Screenshot 2025-01-14 at 16 09 05"
src="https://github.com/user-attachments/assets/628fde18-da9a-4d98-8ddf-ed0ab0cd8d35"
/> | <img width="1161" alt="Screenshot 2025-01-14 at 16 17 52"
src="https://github.com/user-attachments/assets/80c6a4e1-065d-4b0a-b9c0-5f3391af4557"
/> |





Release Notes:

- N/A
2025-01-14 17:15:24 +00:00
Thorsten Ball
91b36c31e8 environments: Don't load shell environments in non-local worktrees (#23138)
This fixes an error message that has shown up for me when joining collab
projects: "Unable to load shell environment in /<path on another
machine/"

Release Notes:

- Fixed error message about shell environment failing to load when
joining projects in collaboration.
2025-01-14 17:13:55 +00:00
Agus Zubiaga
39ac6e4a75 assistant2: Navigate context strip with keyboard (#23128)
Context pills are now focusable and intractable via the keyboard.

- <kbd>←</kbd> and <kbd>→</kbd> move the focus to the previous or next
item (wrapping if necessary)
- <kbd>↓</kbd> and <kbd>↑</kbd> move the focus vertically
- If the cursor is in the first/last row of the assistant/inline editor,
they will move the focus to the strip
- Inside the strip, they will move the focus to the pill horizontally
overlapping the most
- If already in the first/last row of the strip, they will move to the
first/last pill (like in editors)
- If the first/last pill is focused, they will move the focus back to
the editor
- <kbd>⌫</kbd>  removes the focused pill (unless it's the suggested one)
- <kbd>⏎</kbd> accepts the suggested pill if focused
  


https://github.com/user-attachments/assets/040bc71c-a3ae-4961-9886-2d5c3d290a73



Release Notes:

- N/A
2025-01-14 16:45:11 +00:00
Nate Butler
78fd5b5f02 git_ui: Add Git Panel settings (#23132)
This PR adds settings for the Git Panel.

The new settings include:

| Setting | Description | Default |
|---------|-------------|---------|
| `git_panel.button` | Toggle visibility of the Git Panel button in the
status bar | `true` |
| `git_panel.dock` | Choose where to dock the Git Panel | `"left"` |
| `git_panel.default_width` | Set the default width of the Git Panel in
pixels | `360` |
| `git_panel.status_style` | Select how Git status is displayed |
`"icon"` |
| `git_panel.scrollbar.show` | Configure scrollbar behavior | Inherits
from editor settings |

Example usage:

```json
"git_panel": {
  "button": true,
  "dock": "left",
  "default_width": 360,
  "status_style": "icon",
  "scrollbar": {
    "show": "auto"
  }
}
```

Release Notes:

- N/A
2025-01-14 15:40:45 +00:00
Thorsten Ball
a67709629b zeta: Various product fixes before Preview release (#23125)
Various fixes for Zeta and one fix that's visible to non-Zeta-using
users of inline completions.

Release Notes:

- Changed inline completions (Copilot, Supermaven, ...) to not show up
in empty buffers.

---------

Co-authored-by: Antonio <antonio@zed.dev>
Co-authored-by: Antonio Scandurra <me@as-cii.com>
Co-authored-by: Bennet <bennet@zed.dev>
2025-01-14 14:30:27 +00:00
Piotr Osiewicz
1b3b825c7f lsp: Parse LSP messages on background thread - again (#23122)
This is a follow-up to #12640.
While profiling latency of working with a project with 8192 diagnostics
I've noticed that while we're parsing the LSP messages into a generic
message struct on a background thread, we can still block the main
thread as the conversion between that generic message struct and the
actual LSP message (for use by callback) is still happening on the main
thread.
This PR significantly constrains what a message callback can use, so
that it can be executed on any thread; we also send off message
conversion to the background thread. In practice new callback
constraints were already satisfied by all call sites, so no code outside
of the lsp crate had to be adjusted.

This has improved throughput of my 8192-benchmark from 40s to send out
all diagnostics after saving to ~20s. Now main thread is spending most
of the time updating our diagnostics sets, which can probably be
improved too.

Closes #ISSUE

Release Notes:

- Improved app responsiveness with huge # of diagnostics.
2025-01-14 13:50:54 +00:00
Kirill Bulatov
8e65ec1022 Disable Prettier for C projects by default (#23119)
Follow-up of https://github.com/zed-industries/zed/pull/23112

Same reasoning applies.

Release Notes:

- Changed default formatter for C to be the primary language server, not
Prettier. Format-on-save is still disabled by default for C, but if one
uses the editor: format command now, it will default to the language
server. clangd can format C files, whereas prettier cannot.
2025-01-14 11:47:22 +00:00
Thorsten Ball
fcadd3e1ff cpp: Enable language server as formatter by default (#23112)
As @hferreiro points out in [this

comment](https://github.com/zed-industries/zed/pull/18752#issuecomment-2589340565):
C++ and prettier don't work well together, so let's make the default
formatter for C++ the primary language server. We get that by disabling
prettier.

Release Notes:

- Changed default formatter for C++ to be the primary language server,
not Prettier. Format-on-save is still disabled by default for C++, but
if one uses the `editor: format` command now, it will default to the
language server. `clangd` can format C++ files, whereas prettier cannot.
2025-01-14 09:56:57 +00:00
Michael Sloan
a13e64e0cd Keymap json schema generation improvements intended to be in #23098 (#23114)
Intended to include these in #23098, but seems they didn't push before
merge. Probably didn't use `--force-with-lease`
2025-01-14 09:51:20 +00:00
0x2CA
26be440d99 vim: Add Subword Textobject (#22387)
Closes #22761

[Vim: subword text object?
#22280](https://github.com/zed-industries/zed/discussions/22280)

Release Notes:

- Added Vim SubWord TextObject

---------

Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
2025-01-14 03:34:49 +00:00
0x2CA
03c99e39f9 vim: Fix vim delete to line (#23053)
Closes #23024

Release Notes:

- Fixed Vim `dxG` delete to line

---------

Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
2025-01-14 03:07:47 +00:00
Marshall Bowers
93f117b21a Improve registration for Assistant code action providers (#23099)
This PR is a follow-up to
https://github.com/zed-industries/zed/pull/22911 to further improve the
registration of code action providers for the Assistant in order to
prevent duplicates.

The `CodeActionProvider` trait now has an `id` method that is used to
return a unique ID for a code action provider. We use this to prevent
registering duplicates of the same provider.

The registration of the code action providers for Assistant1 and
Assistant2 have also been reworked. Previously we were not call the
registration function—and thus setting up the subscriptions—until we
resolved the feature flags. However, this could lead to the registration
happening too late for existing workspace items.

We now perform the registration right away and then remove the undesired
code action providers once the feature flags have been resolved.

Release Notes:

- N/A
2025-01-13 22:25:58 +00:00
Marshall Bowers
830f45e56a assistant2: Add floating indicator when a response is streaming (#23096)
This PR adds a separate indicator at the bottom of the thread that shows
when a response is being streamed (as well as how to cancel it):

<img width="1309" alt="Screenshot 2025-01-13 at 4 19 07 PM"
src="https://github.com/user-attachments/assets/b64f785b-d522-458d-b915-3f604890597f"
/>

Release Notes:

- N/A
2025-01-13 22:03:45 +00:00
Michael Sloan
ae103fdf64 Fix confusing keymap json errors and hovers for nonexistent actions (#23098)
Release Notes:

- N/A
2025-01-13 21:53:12 +00:00
Marshall Bowers
c599ba64bc assistant2: Only show the streaming indicator on the last Assistant message (#23090)
This PR is a follow-up to #23078 to ensure that the streaming indicator
only shows up on the last Assistant message.

Release Notes:

- N/A
2025-01-13 21:09:01 +00:00
Piotr Osiewicz
867c069b99 editor: Adjust offset of the opened jump target in the multibuffer (#23091)
This PR fixes an issue with jumping from multi_buffer to a file; namely,
the scroll offset of the opened buffer used to match the position within
the multibuffer, but it broke a while back. This is because we were
opening a buffer without providing the data about the origin scroll
offset.

Closes #ISSUE

Release Notes:

- Fixed a bug where the relative position of an excerpt within the
multibuffer was not accounted for while jumping to the buffer, causing
the clicked line to drastically change position on screen.
2025-01-13 21:08:46 +00:00
Marshall Bowers
ac2d3eec91 Remove commented-out code (#23089)
This PR removes some commented-out code from the codebase.

Release Notes:

- N/A
2025-01-13 21:02:45 +00:00
Agus Zubiaga
4054d4a5b7 assistant2: Fix inline context picker and handle dismiss (#23081)
The new `ContextMenu`-based `ContextPicker` requires initialization when
opened, but we were only doing this for the `ContextStrip` picker, not
the inline one.

Additionally, because we have a wrapper element around ContextMenu, we
need to propagate the `DismissEvent` so that it properly closes when
Escape is pressed.

Release Notes:

- N/A
2025-01-13 21:00:20 +00:00
Michael Sloan
7c2c409f6d Show configuration in language server debug logs (#23084)
Release Notes:

- Added configuration sent on initialization to the `Server Info`
section of the language server logs.
2025-01-13 21:00:03 +00:00
Michael Sloan
d4e91c1898 Add support for namespace changes in action deprecations (#23086)
cc @cole-miller 

Release Notes:

- N/A
2025-01-13 20:56:22 +00:00
Michael Sloan
b633f62aa6 Add test that JSON schema generation works + actions build from no input (#23049)
Release Notes:

- N/A
2025-01-13 20:42:08 +00:00
Joseph T. Lyons
85b727c1a2 Remove inaccurate comments (#23056)
These comments are inaccurate. Even if `convert_case` provided a way to
customize which boundaries were used (which is now does, it 0.7.1), they
would be removed from the string and replaced with the new boundary
character (`-`, `_`, ...), and we'd lose the ability to reconstruct the
text the way the author formatted it. This is not a hack, this is the
way we have to do it.

Release Notes:

- N/A
2025-01-13 20:38:44 +00:00
Cole Miller
bd3c7d6cbf git: Fully implement "all staged" checkbox (#23079)
Also includes some improvements to the "stage/unstage all" actions and
buttons.

Release Notes:

- N/A
2025-01-13 20:13:14 +00:00
Marshall Bowers
2179be1855 assistant2: Add an indicator when a response is streaming in (#23078)
This PR adds an indicator to the Assistant message to indicate that it
is still streaming:

<img width="1310" alt="Screenshot 2025-01-13 at 2 10 33 PM"
src="https://github.com/user-attachments/assets/635ee60d-b5ea-40ac-952a-b7bfa7e04fcc"
/>

Release Notes:

- N/A
2025-01-13 19:29:50 +00:00
Michael Sloan
2f762955cd Take a reference in LSP notify (#23077)
In current code this doesn't have benefit. In preparation for avoiding a
clone of workspace configuration. Having the interface this way may make
opportunities for efficiency clearer in the future

Release Notes:

- N/A
2025-01-13 19:26:28 +00:00
Marshall Bowers
c1c767a5bd assistant2: Make Esc cancel current completion (#23076)
This PR makes it so pressing `Esc` in Assistant2 will cancel the current
completion.

Release Notes:

- N/A
2025-01-13 19:09:27 +00:00
Michael Sloan
b59a9f1f42 Document why rust-analyzer doesn't show action name in action docs (#23072)
rust-analyzer does not support derive_macro expansion in attributes -
https://github.com/rust-lang/rust-analyzer/issues/8092. This could be
worked around via a proc_macro, but I think it'd be best to just require
docs for every action.

Release Notes:

- N/A
2025-01-13 17:48:50 +00:00
Nate Butler
102e70816c git: Git Panel UI, continued (#22960)
TODO:

- [ ] Investigate incorrect hit target for `stage all` button
- [ ] Add top level context menu
- [ ] Add entry context menus
- [x] Show paths in list view
- [ ] For now, `enter` can just open the file
- [ ] 🐞: Hover deadzone in list caused by scrollbar
- [x] 🐞: Incorrect status/nothing shown when multiple worktrees are
added

---

This PR continues work on the feature flagged git panel.

Changes:
- Defines and wires up git panel actions & keybindings
- Re-scopes some actions from `git_ui` -> `git`.
- General git actions (StageAll, CommitChanges, ...) are scoped to
`git`.
- Git panel specific actions (Close, FocusCommitEditor, ...) are scoped
to `git_panel.
- Staging actions & UI are now connected to git!
- Unify more reusable git status into the GitState global over being
tied to the panel directly.
- Uses the new git status codepaths instead of filtering all workspace
entries

Release Notes:

- N/A

---------

Co-authored-by: Cole Miller <53574922+cole-miller@users.noreply.github.com>
Co-authored-by: Cole Miller <cole@zed.dev>
2025-01-13 16:47:09 +00:00
everdrone
1c6dd03e50 Add Diagnostics key context (#23043)
Closes #17337

Release Notes:

- Add `Diagnostics` key context
- Enables users to specify key bindings for that pane

```json
{
    "context": "Diagnostics",
    "bindings": {
        "alt-q": "diagnostics::ToggleWarnings"
    }
}
```
2025-01-13 16:07:04 +00:00
SkywardSyntax
955248fee0 copilot_chat: Rename o1-preview model to o1 (#23038)
https://github.blog/news-insights/openais-o1-model-available-in-copilot-chat-and-github-models/

Release Notes:

- Renamed Github Copilot Chat "o1-preview" model to "o1".

---------

Co-authored-by: Marshall Bowers <elliott.codes@gmail.com>
2025-01-13 15:32:26 +00:00
tims
7ed834bd79 terminal: Fix unresponsive buttons on load until center pane is clicked + Auto-focus docked terminal on load if no other item is focused (#23039)
Closes #23006

This PR should have been split into two, but since the changes are
related, I merged them into one.

1. On load, the title bar actions and bottom bar toggles are
unresponsive until the center pane is clicked. This happens because the
terminal captures focus (even if it's closed) long after the workspace
sets focus to itself during loading.

The issue was in the `focus_view` call used in the `new` method of
`TerminalPanel`. Since new terminal views can be created behind the
scenes (i.e., without the terminal being visible to the user), we
shouldn't handle focus for the terminal in this case. Removing
`focus_view` from the `new` method has no impact on the existing
terminal focusing logic. I've tested scenarios such as creating new
terminals, splitting terminals, zooming, etc., and everything works as
expected.

2. Currently, on load, docked terminals do not automatically focus when
they are only visible item to the user. This PR implements it.

Before/After:

1. When only the dock terminal is visible on load. Terminal is focused.

<img
src="https://github.com/user-attachments/assets/af8848aa-ccb5-4a3b-b2c6-486e8d588f09"
alt="image" height="280px" />

<img
src="https://github.com/user-attachments/assets/8f76ca2e-de29-4cc0-979b-749b50a00bbd"
alt="image" height="280px" />

2. When other items are visible along with the dock terminal on load.
Editor is focused.

<img
src="https://github.com/user-attachments/assets/d3248272-a75d-4763-9e99-defb8a369b68"
alt="image" height="280px" />

<img
src="https://github.com/user-attachments/assets/fba5184e-1ab2-406c-9669-b141aaf1c32f"
alt="image" height="280px" />

3. Multiple tabs along with split panes. Last terminal is focused.

<img
src="https://github.com/user-attachments/assets/7a10c3cf-8bb3-4b88-aacc-732b678bee19"
alt="image" height="270px" />

<img
src="https://github.com/user-attachments/assets/4d16e98f-9d7a-45f6-8701-d6652e411d3b"
alt="image" height="270px" />

Future:

When a docked terminal is in a zoomed state and Zed is loaded, we should
prioritize focusing on the terminal over the active item (e.g., an
editor) behind it. This hasn't been implemented in this PR because the
zoomed state during the load function is stale. The correct state is
received later via the workspace. I'm still investigating where exactly
this should be handled, so this will be a separate PR.

cc: @SomeoneToIgnore 

Release Notes:

- Fixed unresponsive buttons on load until the center pane is clicked.  
- Added auto-focus for the docked terminal on load when no other item is
focused.
2025-01-13 15:11:45 +00:00
Ozan
13405ed4a3 Add emacs keybindings for mark emulation (#22904)
These keybindings extend the already selected text. This allows closer
emacs emulation where subsequent movement commands extend / shrink the
current selection instead of dismissing it.

This is a follow up on 
- #21927

Release Notes:

- Added emacs movement keybindings that extend/shrink the current
selection

---------

Co-authored-by: Peter Tripp <peter@zed.dev>
2025-01-13 14:53:13 +00:00
Antonio Scandurra
c26553de82 Add more metrics for Fireworks Completion Requested (#23062)
Release Notes:

- N/A

Co-authored-by: Thorsten <thorsten@zed.dev>
2025-01-13 12:04:28 +00:00
Antonio Scandurra
f2ab00cec7 Improve prompt caching for edit prediction (#23061)
This is achieved by halving the number of events instead of popping the
front.

Release Notes:

- N/A

Co-authored-by: Thorsten <thorsten@zed.dev>
2025-01-13 10:58:49 +00:00
Michael Sloan
e08484840b Clarify logic for Autoscroll::newest() and Autoscroll::fit() (#23048)
Release Notes:

- N/A
2025-01-13 05:33:24 +00:00
Michael Sloan
6aba3950d2 Improve keymap json schema (#23044)
Also:

* Adds `impl_internal_actions!` for deriving the `Action` trait without
registering.

* Removes some deserializers that immediately fail in favor of
`#[serde(skip)]` on fields where they were used. This also omits them
from the schema.

Release Notes:

- Keymap settings file now has more JSON schema information to inform
`json-language-server` completions and info, particularly for actions
that take input.
2025-01-13 02:34:35 +00:00
Michael Sloan
4c50201036 For informational LSP queries log errors instead of notifying in UI (#23040)
I added these notifies in #23011, but in practive have found them to be
overly disruptive. It would definitely be good to do something better
than logging here, but having a sticky error notification is worse. I
think it is still good to notify on mutation failures, so left those in

In particular with rust-analyzer, "Go to definition" and "Find
references" frequently fail with "Content modified" quite a while after
sending the request. Since users are probably used to these operations
being finicky it doesn't seem useful to have a prominent display of
errors for them.
2025-01-12 21:22:16 +00:00
Kirill Bulatov
fb65044484 Reuse vtsls logic for completion details display (#23030)
Part of https://github.com/zed-industries/zed/issues/22833,
https://github.com/zed-industries/zed/issues/22267,
https://github.com/zed-industries/zed/issues/22503

Before:

![image](https://github.com/user-attachments/assets/b6abd3dc-b5d7-4d6a-91e2-92361a519adb)

![image](https://github.com/user-attachments/assets/e3a9e766-efbe-4f4d-b4f9-e6b019e165a5)

After:

![image](https://github.com/user-attachments/assets/d29414d5-4fcc-4d2f-adb2-48304cbafdf6)

Copies https://github.com/zed-industries/zed/pull/15087 change into
`typescript-language-server`-related label details rendering code.

Release Notes:

- Improved typescript-language-server's completion details rendering
2025-01-12 13:44:24 +00:00
Kirill Bulatov
b6b87405b0 Do not try to activate the terminal panel twice (#23029)
Closes https://github.com/zed-industries/zed/issues/23023

Fixes terminal pane button opening two terminals on click.

The culprit is in

61115bd047/crates/workspace/src/workspace.rs (L2412-L2417)

* We cannot get any panel by index from the Dock, only an active one
* Both `dock.activate_panel(panel_index, cx);` and `dock.set_open(true,
cx);` do `active_panel.panel.set_active(true, cx);`

So, follow other pane's impls that have `active: bool` property for this
case, e.g.
3ec52d8451/crates/assistant/src/inline_assistant.rs (L2687)

Release Notes:

- Fixed terminal pane button opening two terminals on click
2025-01-12 12:56:31 +00:00
Michael Sloan
61115bd047 Fix a completions panic when no fuzzy matches + inline completion (#23019)
My mistake in #22977, in the case where the inline completion was not
selected it set the index to 1 assuming there would be following match
entries.
2025-01-12 02:41:28 +00:00
Michael Sloan
5785266c8c Improve doc comments about keybinding order (#23014)
Release Notes:

- N/A
2025-01-11 22:47:42 +00:00
Michael Sloan
daaa250109 Include display text for LSP commands in errors (#23012)
https://github.com/zed-industries/zed/pull/23011 adds display of errors
in the UI so it's now more important to contextualize these.

Release Notes:

- N/A
2025-01-11 21:59:06 +00:00
Michael Sloan
de2e197ad9 Inline perform_rename_impl as its only used in one spot (#23013)
Also removes a redundant use of `to_point_utf16`.

Release Notes:

- N/A
2025-01-11 21:58:35 +00:00
Michael Sloan
65c38f22f9 Notify user about LSP errors from editor actions (#23011)
Closes #22976

Release Notes:

* Improved visibility of errors from language servers by reporting them
in the UI when the user invokes an LSP action.
2025-01-11 21:48:50 +00:00
Tyler Albee
6bc89eb4b1 docs: Fix "copy" being used instead of "paste" in vim mode documentation (#23010)
It seems the original author intended to write either "`ctrl+c` to copy"
or "`ctrl+v` to paste". Updated to be "`ctrl+v` to paste".

Release Notes:

- N/A

Co-authored-by: Michael Sloan <michael@zed.dev>
2025-01-11 21:45:41 +00:00
Michael Sloan
bda0c67ece Add support for rename with language servers that lack prepareRename (#23000)
This adds support for LSPs that use the old rename flow which does not
first ask the LSP for the rename range and check that it is a valid
range to rename.

Closes #16663

Release Notes:

* Fixed rename symbols action when the language server does not have the
capability to prepare renames - such as `luau-lsp`.
2025-01-11 21:22:17 +00:00
Michael Sloan
b65dc8c566 Fix jank in LSP debug log autoscroll (#22998)
Not sure why scroll was janky with `Autoscroll::newest()`, but this
appears to fix it. Probably better to conditionally do the autoscroll
requests anyway.

Release Notes:

- N/A
2025-01-11 05:59:21 +00:00
Michael Sloan
bbbd1e9902 LSP debug logs: Default to soft wrap + fold long lines + autoscroll (#22996)
Closes #18737

Release notes:

- Improved LSP debug logs by defaulting to soft wrap and folding a
suffix of long lines. Also adds autoscroll, so if the cursor is on the
last line of the logs they will scroll like `tail`.
2025-01-11 04:48:44 +00:00
Marshall Bowers
40ecc38dd2 assistant2: Make ContextStore::insert_* methods private (#22989)
This PR makes the `insert_*` methods on the `ContextStore` private, to
reduce confusion with the public `add_*` methods.

Release Notes:

- N/A
2025-01-10 22:50:33 +00:00
Thorsten Ball
1fcc9b36ba zeta: Report Fireworks request data to Snowflake (#22973)
Release Notes:

- N/A

---------

Co-authored-by: Antonio Scandurra <me@as-cii.com>
Co-authored-by: Conrad <conrad@zed.dev>
2025-01-10 22:40:54 +00:00
Thorsten Ball
3d80b21a91 eslint: Allow configuring workingDirectory (#22972)
This addresses this comment here:
https://github.com/zed-industries/zed/issues/9648#issuecomment-2579246865

Release Notes:

- Added ability to configure `workingDirectory` when using ESLint.
Example: `{"lsp": {"eslint": {"settings": {"workingDirectory": {"mode":
"auto" }}}}}`
2025-01-10 22:21:51 +00:00
Danilo Leal
05b48e8877 zeta: Add tooltip to completion modal list items (#22987)
This is an extra visual aid to make assessing the status of each list
item faster/easier.

<img width="800" alt="Screenshot 2025-01-10 at 7 01 22 PM"
src="https://github.com/user-attachments/assets/4aa712ed-cc70-4ded-afab-e7ceda535ec0"
/>

Release Notes:

- N/A
2025-01-10 22:20:20 +00:00
狐狸
8bd7a048ab Improve TypeScript highlights (#18525)
- Move function queries under constant queries to avoid uppercase
functions highlighted as constants
- Merge keywords and remove duplicates
- Highlights type aliases on import
- Highlights literal built-in types (null, undefined, true, false) as
`@type.builtin`

Confused about case-based queries, should they be rewritten?

Release Notes:

- N/A

---------

Co-authored-by: Marshall Bowers <elliott.codes@gmail.com>
2025-01-10 22:00:28 +00:00
Danilo Leal
1e0ded4feb zeta: Show keybinding in completion rating buttons in review modal (#22985)
This PR also removes the `ThumbsUp` action that wasn't being triggered
correctly. We didn't have it's counterpart `ThumbsDown`, too, so I
mostly assumed it would be harmless to remove `ThumbsUp` as well.

<img width="800" alt="Screenshot 2025-01-10 at 6 18 44 PM"
src="https://github.com/user-attachments/assets/9fd5da9f-9dff-454d-9f31-c02f1370b937"
/>

Release Notes:

- N/A

---------

Co-authored-by: Marshall Bowers <elliott.codes@gmail.com>
2025-01-10 22:00:11 +00:00
Marshall Bowers
dad1a3bd31 assistant2: Inline read calls (#22982)
This PR inlines the `read` calls on models in a few spots.

Release Notes:

- N/A
2025-01-10 21:54:50 +00:00
Marshall Bowers
0f1c2a8d01 ci: Install cargo-nextest with --locked (#22984)
This PR makes it so we install `cargo-nextest` with `cargo install
cargo-nextest --locked` in CI.

According to the
[docs](https://nexte.st/docs/installation/from-source/), this is the
**only** supported way to install `cargo-nextest` when building from
source.

Release Notes:

- N/A
2025-01-10 21:27:28 +00:00
Marshall Bowers
80cc1f174f assistant2: Hide the status bar icon when disabled via the settings (#22981)
This PR makes it so the status bar icon for Assistant2 is hidden when it
is disabled via the settings.

Release Notes:

- N/A
2025-01-10 19:44:57 +00:00
Marshall Bowers
2f07d53cce assistant2: Remove unneeded #[allow(unused)]s (#22979)
This PR removes some unneeded `#[allow(unused)]`s from the context types
in Assistant2.

We're using these fields now, so we no longer need to suppress the
unused lint.

Release Notes:

- N/A
2025-01-10 19:05:08 +00:00
Michael Sloan
fe3d409b17 If completions menu is already displayed, don't select inline completion (#22977)
Before this change, inline completion would displace the user's
selection. Unfortunately this brings less visibility to the inline
completion, I think a good solution to this will be to display a chunk
of the completion inline in the menu, and have a WIP change for that.
Since the current behavior is frustrating, not blocking this improvement
on that

Release Notes:

- N/A
2025-01-10 18:45:55 +00:00
Peter Tripp
c74ad61c0f emacs: Add as Transpose Characters (editor::Transpose) (#22974)
Originally reported here:
-
https://github.com/zed-industries/zed/issues/4856#issuecomment-2578468329

macOS default vscode keymap already has this:

8d42456b8a/assets/keymaps/default-macos.json (L55)
But it's disabled on Linux default vscode keymap as VSCode has this bind
instead:

8d42456b8a/assets/keymaps/default-linux.json (L407)

Explicitly add it to both emacs keymaps so we can keep them identical
between macos/linux as long as possible.

Release Notes:

- emacs: Add support for `ctrl-t` transposing characters on Linux
2025-01-10 17:07:06 +00:00
Finn Evers
c6df23fcb6 csharp: Add brackets.scm (#22936)
This pull request adds the missing `brackets.scm` for the C#-extension.

Release Notes:

- N/A
2025-01-10 16:18:33 +00:00
Michael Sloan
4c7b72bf3c Clarify guests vs collaborators in project sharing docs (#22945)
Release Notes:

- N/A
2025-01-10 15:42:52 +00:00
Peter Tripp
3795963cf5 emacs: Fix emacs in embedded terminal on Linux too (#22969)
- Follow-up to #22779 (accidentially did macos only)
- Follow-up to: https://github.com/zed-industries/zed/pull/22590

Release Notes:

- N/A
2025-01-10 15:32:24 +00:00
Jeremy Cowgar
b74cb92978 docs: Fix missing } in multiple formatters example (#22964)
Add a missing } in the multiple formatters example in the configuring
Zed section of the manual.

Release Notes:

- Fixed a missing } in the multiple formatters doc example
2025-01-10 14:39:34 +00:00
Danilo Leal
cbc403d3f3 assistant2: Change suggested file context pill label (#22967)
Changing it from "Open File" to "Active Tab" instead.

<img width="800" alt="Screenshot 2025-01-10 at 11 09 54 AM"
src="https://github.com/user-attachments/assets/534e94a4-df61-41d4-ad50-514ab9a87e4e"
/>

Release Notes:

- N/A
2025-01-10 14:37:57 +00:00
Danilo Leal
5310e33356 assistant2: Fix context strip context popover position in relation to trigger (#22966)
Little visual adjustment here.

| Before | After |
|--------|--------|
| <img width="1336" alt="Screenshot 2025-01-10 at 11 08 06 AM"
src="https://github.com/user-attachments/assets/268c6df6-fdb2-4a1c-b3b8-d6a39b93b206"
/> | <img width="1336" alt="Screenshot 2025-01-10 at 11 06 17 AM"
src="https://github.com/user-attachments/assets/fb53feef-9ae4-489b-9d12-bd50b349afc1"
/> |

Release Notes:

- N/A
2025-01-10 14:35:09 +00:00
Danilo Leal
9248458928 assistant2: Change model selector keybinding and make it visible (#22965)
We weren't showing the keybinding in none of the places where the model
selector was visible. Also, I took advantage of the opportunity to
change the keybinding for two reasons:

1. `cmd-shift-m` caused conflict if on an editor (inline assistant case)
2. `cmd-opt-/` is the one Cursor uses; so consistency with something
that might be already consolidated sounds like a low-hanging fruit

| Editor Inline Assist | Terminal Inline Assist | Assistant Panel |
|--------|--------|--------|
| <img width="1336" alt="Screenshot 2025-01-10 at 11 01 24 AM"
src="https://github.com/user-attachments/assets/0782f217-025f-4bc0-b2fa-64b3524c968b"
/> | <img width="1336" alt="Screenshot 2025-01-10 at 11 01 29 AM"
src="https://github.com/user-attachments/assets/d05a3b5c-33fd-4593-b1d8-aa9944de816a"
/> | <img width="1336" alt="Screenshot 2025-01-10 at 11 01 33 AM"
src="https://github.com/user-attachments/assets/8cb075e7-ccde-46f5-aa05-d20a9d42b286"
/> |

Release Notes:

- N/A
2025-01-10 14:27:52 +00:00
Agus Zubiaga
a267911e83 assistant2: Suggest recent files and threads as context (#22959)
The context picker will now display up to 6 recent files/threads to add
as a context:

<img
src="https://github.com/user-attachments/assets/80c87bf9-70ad-4e81-ba24-7a624378b991"
width=400>



Note: We decided to use a `ContextMenu` instead of `Picker` for the
initial one since the latter didn't quite fit the design for the
"Recent" section.

Release Notes:

- N/A

---------

Co-authored-by: Danilo <danilo@zed.dev>
Co-authored-by: Piotr <piotr@zed.dev>
Co-authored-by: Nathan <nathan@zed.dev>
2025-01-10 14:26:53 +00:00
Kirill Bulatov
49198a7961 Do not show copy buttons in editor's hover popovers (#22962)
Follow-up of https://github.com/zed-industries/zed/pull/22866

Added a config option to the markdown renderer to omit code copying
buttons, and used those for editor hover popovers.

Such popovers are quite frequent in language servers' hover responses,
e.g. rust-analyzer on `.clone()` hover may respond with
```
{"jsonrpc":"2.0","id":119,"result":{"contents":{"kind":"markdown","value":"\n```rust\nalloc::string::String\n```\n\n```rust\nfn clone(&self) -> Self\n```\n\n---\n\nReturns a copy of the value.\n\n# Examples\n\n```rust\nlet hello = \"Hello\"; // &str implements Clone\n\nassert_eq!(\"Hello\", hello.clone());\n```"},"range":{"start":{"line":518,"character":24},"end":{"line":518,"character":29}}}}
```

(note multiple code blocks sent)


![image](https://github.com/user-attachments/assets/4c40b15e-8f53-4b3d-a809-f1e4d35a00a7)


![image](https://github.com/user-attachments/assets/77b8e13b-b665-42d3-b633-5a0375998f06)

Sounds that editor has either to use a different way to copy popover's
data (so the entire text gets copied, not just its code blocks), or at
least better handle hover popover's hovering to show the button.


Release Notes:

- N/A
2025-01-10 14:16:52 +00:00
Antonio Scandurra
c3301077af Log errors when a prediction fails (#22961)
Release Notes:

- N/A

---------

Co-authored-by: Thorsten <thorsten@zed.dev>
2025-01-10 14:07:17 +00:00
Piotr Osiewicz
9e113bccd0 deps: Bump smol to 2.0 (#22956)
The collateral of this is that code size is increased by ~300kB, but I
think we can stomach it.

Release Notes:

- N/A
2025-01-10 13:38:00 +00:00
AidanV
1f84c1b6c7 nix: Fix webrtc-sys and libstdc++ build errors in development shell (#22938)
Closes #22937

- Added bzip2 package to the build inputs
- Set LD_LIBRARY_PATH environment variable to stdenv.cc.cc.lib

Release Notes:

- N/A
2025-01-10 12:50:33 +00:00
Thorsten Ball
a1cedbece9 zeta: Fix completions not being marked as rated (#22952)
Seems like #22171 accidentally removed this line.

Now it's back and completions are marked as rated again.

![screenshot-2025-01-10-10 56
32@2x](https://github.com/user-attachments/assets/c68bff1b-5b97-493e-9062-390876fd757c)

Release Notes:

- N/A
2025-01-10 10:24:30 +00:00
Michael Sloan
1b44398967 Make SelectionsCollection::disjoint_anchor_ranges return an iterator (#22948)
This helps discourage unnecessary collection to Vec

Release Notes:

- N/A
2025-01-10 09:37:46 +00:00
Michael Sloan
690ad29ba9 assistant2: Small misc efficiency improvements (#22947)
Release Notes:

- N/A
2025-01-10 09:20:15 +00:00
Michael Sloan
767f44bd27 assistant2: Implement refresh of context on message editor send (#22944)
Release Notes:

- N/A
2025-01-10 08:09:47 +00:00
Nico Lehmann
0b105ba8b7 vim: Add sneak motion (#22793)
A (re)continuation of https://github.com/zed-industries/zed/pull/21067. 

This takes the original implementation in
https://github.com/zed-industries/zed/pull/15572 and adds the test in
https://github.com/zed-industries/zed/pull/21067. Then, as requested in
https://github.com/zed-industries/zed/pull/21067#issuecomment-2515469185,
it documents how to map a keybinding instead of having a setting.

Closes #13858

Release Notes:

- Added support for the popular
[vim_sneak](https://github.com/justinmk/vim-sneak) plugin. This is
disabled by default and can be enabled by binding a key to the `Sneak`
and `SneakBackward` operators.

Reference:
https://github.com/justinmk/vim-sneak

---------

Co-authored-by: Kajetan Puchalski <kajetan.puchalski@tuta.io>
Co-authored-by: Aidan Grant <mraidangrant@gmail.com>
Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
2025-01-10 07:07:32 +00:00
Michael Sloan
0d6a549950 assistant2: More improvement to prompt building efficiency (#22941)
Release Notes:

- N/A
2025-01-10 04:40:11 +00:00
Agus Zubiaga
ec4c6744d6 assistant2: Show file icons for context entries (#22928)
https://github.com/user-attachments/assets/d3d6f5f1-23ec-449b-a762-9869b9d4b5a5


Release Notes:

- N/A

---------

Co-authored-by: Nathan <nathan@zed.dev>
Co-authored-by: Michael <michael@zed.dev>
2025-01-10 03:01:42 +00:00
Michael Sloan
c9008fb8c1 Format all selections even if they are cursors (#22933)
Closes #22816

Release Notes:

- Format selections now also applies to cursors.
2025-01-10 01:28:49 +00:00
Michael Sloan
0dd7ea4575 assistant2: Background load of context + prep for refresh + efficiency (#22935)
* Now loads context on background threads.

- For file and directory context, buffer ropes can be shared between
threads as they are immutable. This allows for traversal and
accumulation of buffer text on a background thread.

- For url context, the request, parsing, and rendering is now done on a
background thread.

* Prepares for support of buffer reload by individually storing the text
of directory buffers.

* Avoids some string copying / redundant strings.

- When attaching message context, no longer builds a string for each
context type.

- For directory context, does not build a `SharedString` for the full
text, instead has a slice of `SharedString` chunks which are then
directly appended to the message context.

- Building a fenced codeblock for a buffer now computes a precise
capacity in advance.

Release Notes:

- N/A
2025-01-10 01:26:21 +00:00
Michael Sloan
c41b25cc90 Log an error when there are no buffer snapshots for some LSP version (#22934)
I'm hoping this will bring more visibility to issues related to keeping
track of what version of code the LSP has:

* I've seen diagnostic ranges not appearing in the correct places.

* There have also been reports of edits from language servers
misapplying. This might bring more visibility to the issue - it doesn't
seem good to silently use the current version of the buffer.

Release Notes:

- N/A
2025-01-10 00:35:19 +00:00
Michael Sloan
685dd77d97 Fix handling of selection ranges for format selections in multibuffer (#22929)
Before this change it was using the same multibuffer point ranges in
every buffer, which only worked correctly for singleton buffers.

Release Notes:

- Fixed handling of selection ranges when formatting selections within a
multibuffer.

---------

Co-authored-by: Thorsten Ball <mrnugget@gmail.com>
2025-01-10 00:17:04 +00:00
Kyle Kelley
29aa291d28 Bump repl dependencies (#22921)
Primarily for a `smol` upgrade. cc @osiewicz 

Release Notes:

- N/A
2025-01-09 23:45:18 +00:00
Marshall Bowers
8da58bbe3a story: Use itertools as a workspace dependency (#22919)
This PR makes the `story` crate depend on `itertools` as a workspace
dependency.

Release Notes:

- N/A
2025-01-09 21:19:17 +00:00
Henry Chu
b2eceeb4f2 Enable yaml-language-server lookup in PATH (#22036)
Release Notes:

- Added support for checking for `yaml-language-server` on the`$PATH`.

Co-authored-by: Marshall Bowers <elliott.codes@gmail.com>
2025-01-09 21:06:21 +00:00
Michael Sloan
d3eae024a2 assistant2: Add Linux keybindings following same pattern as macOS (#22874)
Release Notes:

- N/A

---------

Co-authored-by: Marshall Bowers <elliott.codes@gmail.com>
2025-01-09 20:54:11 +00:00
renovate[bot]
cc9b5f1448 Update aws-sdk-rust monorepo (#22868)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [aws-config](https://redirect.github.com/smithy-lang/smithy-rs) |
dependencies | patch | `1.5.11` -> `1.5.13` |
| [aws-sdk-kinesis](https://redirect.github.com/awslabs/aws-sdk-rust) |
dependencies | minor | `1.53.0` -> `1.55.0` |
| [aws-sdk-s3](https://redirect.github.com/awslabs/aws-sdk-rust) |
dependencies | minor | `1.66.0` -> `1.68.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:eyJjcmVhdGVkSW5WZXIiOiIzOS45Mi4wIiwidXBkYXRlZEluVmVyIjoiMzkuOTIuMCIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOltdfQ==-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-09 20:22:01 +00:00
renovate[bot]
1be0ce8be0 Update Rust crate bytemuck to v1.21.0 (#22873)
This PR contains the following updates:

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

---

### Release Notes

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

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

[Compare
Source](https://redirect.github.com/Lokathor/bytemuck/compare/v1.20.0...v1.21.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:eyJjcmVhdGVkSW5WZXIiOiIzOS45Mi4wIiwidXBkYXRlZEluVmVyIjoiMzkuOTIuMCIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOltdfQ==-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-09 20:19:35 +00:00
renovate[bot]
b393d4a1da Update Rust crate tempfile to v3.15.0 (#22881)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [tempfile](https://stebalien.com/projects/tempfile-rs/)
([source](https://redirect.github.com/Stebalien/tempfile)) |
workspace.dependencies | minor | `3.14.0` -> `3.15.0` |

---

### Release Notes

<details>
<summary>Stebalien/tempfile (tempfile)</summary>

###
[`v3.15.0`](https://redirect.github.com/Stebalien/tempfile/blob/HEAD/CHANGELOG.md#3150)

[Compare
Source](https://redirect.github.com/Stebalien/tempfile/compare/v3.14.0...v3.15.0)

Re-seed the per-thread RNG from system randomness when we repeatedly
fail to create temporary files
([#&#8203;314](https://redirect.github.com/Stebalien/tempfile/issues/314)).
This resolves a potential DoS vector
([#&#8203;178](https://redirect.github.com/Stebalien/tempfile/issues/178))
while avoiding `getrandom` in the common case where it's necessary. The
feature is optional but enabled by default via the `getrandom` feature.

For libc-free builds, you'll either need to disable this feature or
opt-in to a different [`getrandom`
backend](https://redirect.github.com/rust-random/getrandom?tab=readme-ov-file#opt-in-backends).

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

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-09 19:40:26 +00:00
renovate[bot]
9aa830d4a2 Update Rust crate async-trait to v0.1.85 (#22859)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [async-trait](https://redirect.github.com/dtolnay/async-trait) |
workspace.dependencies | patch | `0.1.83` -> `0.1.85` |

---

### Release Notes

<details>
<summary>dtolnay/async-trait (async-trait)</summary>

###
[`v0.1.85`](https://redirect.github.com/dtolnay/async-trait/releases/tag/0.1.85)

[Compare
Source](https://redirect.github.com/dtolnay/async-trait/compare/0.1.84...0.1.85)

- Omit `Self: 'async_trait` bound in impl when not needed by signature
([#&#8203;284](https://redirect.github.com/dtolnay/async-trait/issues/284))

###
[`v0.1.84`](https://redirect.github.com/dtolnay/async-trait/releases/tag/0.1.84)

[Compare
Source](https://redirect.github.com/dtolnay/async-trait/compare/0.1.83...0.1.84)

- Support `impl Trait` in return type
([#&#8203;282](https://redirect.github.com/dtolnay/async-trait/issues/282))

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

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-09 19:40:00 +00:00
Marshall Bowers
cb77ee04ec extensions_ui: Show an error toast when a dev extension fails to install (#22914)
This PR adds an error toast that will be displayed when installing a dev
extension fails.

Here's what it looks like:

<img width="1310" alt="Screenshot 2025-01-09 at 11 56 42 AM"
src="https://github.com/user-attachments/assets/b65eb9f9-c559-4b99-b64a-ee301fa9e443"
/>

<img width="1310" alt="Screenshot 2025-01-09 at 12 10 30 PM"
src="https://github.com/user-attachments/assets/f4880221-2ed9-4bb0-9d48-1cb29c2b483f"
/>

I did have to touch the workspace `ErrorMessagePrompt` component to make
it scroll for long messages. I don't anticipate this being a problem for
other classes of errors (if anything, I suspect other long errors will
become more usable now).

Closes #21237.

Release Notes:

- Added an error toast that is shown when a dev extension fails to
install.
2025-01-09 19:38:16 +00:00
Marshall Bowers
2143608b5d Fix duplicated Fix with Assistant code actions (#22911)
This PR fixes the duplicated `Fix with Assistant` code actions that were
being shown in the code actions menu.

This fix isn't 100% ideal, as there is an edge case in buffers that are
already open when the workspace loads, as we may not observe the feature
flags in time to register the code action providers by the time we
receive the event that an item was added to the workspace.

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

Release Notes:

- Fixed duplicate "Fix with Assistant" entries showing in the code
action list.
2025-01-09 19:25:12 +00:00
Aaron Feickert
8b4370f170 Only count existing branches in picker search (#22908)
When displaying the number of matches in the branch picker during a
search, don't count the "create new branch" option as a match, since it
only appears when _no_ existing branches are found.

<img width="530" alt="Screenshot 2025-01-09 at 12 17 30"
src="https://github.com/user-attachments/assets/c4e6ac6f-d842-4b2f-a3af-ec28c9d90f0a"
/>

Closes #22905.

Release Notes:

- Fixed result count in branch picker searches.

---------

Co-authored-by: Marshall Bowers <elliott.codes@gmail.com>
2025-01-09 18:55:19 +00:00
Mike Sun
9ea7ed8e0a Allow configuring spacing of project panel entries (#16255)
Release Notes:

- Added `project_panel.entry_spacing` setting to configure spacing
between entries in the project panel.

### Comfortable (default)
```json
  "project_panel": {
    "entry_spacing": "comfortable",
```
<img width="1582" alt="Screenshot 2024-08-14 at 5 50 41 PM"
src="https://github.com/user-attachments/assets/3411a82e-7517-4095-bf4a-bbf40000a7cb">

### Standard
```json
  "project_panel": {
    "entry_spacing": "standard",
```
<img width="1582" alt="Screenshot 2024-08-14 at 5 50 54 PM"
src="https://github.com/user-attachments/assets/2c13d799-c405-4301-8214-1cb3cc641c92">

---------

Co-authored-by: Marshall Bowers <elliott.codes@gmail.com>
2025-01-09 17:57:52 +00:00
Angelk90
35d3d29bcf Add process ID to terminal tab tooltips (#21955)
Closes #12807

| Before | After |
|--------|--------|
| <img width="1336" alt="Screenshot 2025-01-09 at 2 14 15 PM"
src="https://github.com/user-attachments/assets/8396cf41-74eb-4b5c-89e3-287e4f2ddd1d"
/> | <img width="1336" alt="Screenshot 2025-01-09 at 2 13 34 PM"
src="https://github.com/user-attachments/assets/b39c51e8-fd2c-41fe-9493-396057bd71db"
/> |

Release Notes:

- Added the process ID (PID) to terminal tab tooltips.

---------

Co-authored-by: Marshall Bowers <elliott.codes@gmail.com>
Co-authored-by: Danilo Leal <daniloleal09@gmail.com>
2025-01-09 17:52:06 +00:00
renovate[bot]
9f9f3d215d Update Rust crate itertools to v0.14.0 (#22877)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [itertools](https://redirect.github.com/rust-itertools/itertools) |
dependencies | minor | `0.13` -> `0.14` |
| [itertools](https://redirect.github.com/rust-itertools/itertools) |
workspace.dependencies | minor | `0.13.0` -> `0.14.0` |

---

### Release Notes

<details>
<summary>rust-itertools/itertools (itertools)</summary>

###
[`v0.14.0`](https://redirect.github.com/rust-itertools/itertools/blob/HEAD/CHANGELOG.md#0140)

[Compare
Source](https://redirect.github.com/rust-itertools/itertools/compare/v0.13.0...v0.14.0)

##### Breaking

- Increased MSRV to 1.63.0
([#&#8203;960](https://redirect.github.com/rust-itertools/itertools/issues/960))
- Removed generic parameter from `cons_tuples`
([#&#8203;988](https://redirect.github.com/rust-itertools/itertools/issues/988))

##### Added

- Added `array_combinations`
([#&#8203;991](https://redirect.github.com/rust-itertools/itertools/issues/991))
- Added `k_smallest_relaxed` and variants
([#&#8203;925](https://redirect.github.com/rust-itertools/itertools/issues/925))
- Added `next_array` and `collect_array`
([#&#8203;560](https://redirect.github.com/rust-itertools/itertools/issues/560))
- Implemented `DoubleEndedIterator` for `FilterOk`
([#&#8203;948](https://redirect.github.com/rust-itertools/itertools/issues/948))
- Implemented `DoubleEndedIterator` for `FilterMapOk`
([#&#8203;950](https://redirect.github.com/rust-itertools/itertools/issues/950))

##### Changed

- Allow `Q: ?Sized` in `Itertools::contains`
([#&#8203;971](https://redirect.github.com/rust-itertools/itertools/issues/971))
- Improved hygiene of `chain!`
([#&#8203;943](https://redirect.github.com/rust-itertools/itertools/issues/943))
- Improved `into_group_map_by` documentation
([#&#8203;1000](https://redirect.github.com/rust-itertools/itertools/issues/1000))
- Improved `tree_reduce` documentation
([#&#8203;955](https://redirect.github.com/rust-itertools/itertools/issues/955))
- Improved discoverability of `merge_join_by`
([#&#8203;966](https://redirect.github.com/rust-itertools/itertools/issues/966))
- Improved discoverability of `take_while_inclusive`
([#&#8203;972](https://redirect.github.com/rust-itertools/itertools/issues/972))
- Improved documentation of `find_or_last` and `find_or_first`
([#&#8203;984](https://redirect.github.com/rust-itertools/itertools/issues/984))
- Prevented exponentially large type sizes in `tuple_combinations`
([#&#8203;945](https://redirect.github.com/rust-itertools/itertools/issues/945))
- Added `track_caller` attr for `asser_equal`
([#&#8203;976](https://redirect.github.com/rust-itertools/itertools/issues/976))

##### Notable Internal Changes

- Fixed clippy lints
([#&#8203;956](https://redirect.github.com/rust-itertools/itertools/issues/956),
[#&#8203;987](https://redirect.github.com/rust-itertools/itertools/issues/987),
[#&#8203;1008](https://redirect.github.com/rust-itertools/itertools/issues/1008))
- Addressed warnings within doctests
([#&#8203;964](https://redirect.github.com/rust-itertools/itertools/issues/964))
- CI: Run most tests with miri
([#&#8203;961](https://redirect.github.com/rust-itertools/itertools/issues/961))
- CI: Speed up "cargo-semver-checks" action
([#&#8203;938](https://redirect.github.com/rust-itertools/itertools/issues/938))
- Changed an instance of `default_features` in `Cargo.toml` to
`default-features`
([#&#8203;985](https://redirect.github.com/rust-itertools/itertools/issues/985))

</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 these
updates again.

---

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

---

Release Notes:

- N/A

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

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-09 17:48:25 +00:00
Marshall Bowers
4aa4a40e2f extension: Fix manifest filename in error message (#22906)
This PR fixes the incorrect filename for the extension manifest being
used in an error message.

It should be `extension.toml` and not `extension.json`.

Release Notes:

- N/A
2025-01-09 17:38:46 +00:00
Danilo Leal
5c239be757 pane: Add ability to use custom tooltip content (#22879)
This PR is an alternate version of
https://github.com/zed-industries/zed/pull/22850, but now using a
similar approach to the existing `tab_content` and `tab_content_text`,
where `tab_tooltip_content` refers to the existing `tab_tooltip_text` if
there's no custom tooltip content/trait defined, meaning it will
simplify render the text/string content in this case.

This is all motivated by
https://github.com/zed-industries/zed/pull/21955, as we want to pull off
the ability to add custom content to a terminal tab tooltip.

Release Notes:

- N/A
2025-01-09 15:34:30 +00:00
Antonio Scandurra
e64a56ffad Animate Zeta button while generating completions (#22899)
Release Notes:

- N/A

Co-authored-by: Thorsten <thorsten@zed.dev>
2025-01-09 15:24:35 +00:00
Richard Feldman
7d905d0791 assistant2: Add "Copy code" button to code blocks (#22866)
Here's what it looks like, including the "Copy" hover text in one case:


![screenshot](https://github.com/user-attachments/assets/c8d27205-9650-493d-bd3c-a8c7beb142f9)


Release Notes:

- N/A

---------

Co-authored-by: Conrad <conrad@zed.dev>
Co-authored-by: Danilo Leal <daniloleal09@gmail.com>
2025-01-09 14:32:42 +00:00
Antonio Scandurra
a8ef0f2426 Include outline when predicting edits with Zeta (#22895)
Release Notes:

- N/A

Co-authored-by: Thorsten <thorsten@zed.dev>
2025-01-09 14:26:33 +00:00
Antonio Scandurra
341972c79c Introduce UI affordances to make enabling/disabling inline completions easier (#22894)
Release Notes:

- N/A

---------

Co-authored-by: Thorsten <thorsten@zed.dev>
2025-01-09 13:33:30 +00:00
Thorsten Ball
38fbc73ac4 Improve handling tab when inline completion is visible (#22892)
This changes the behaviour of `<tab>` when inline completion is visible.
When the cursor is before the suggested indentation level, accepting a
completion should just indent.

cc @nathansobo @maxdeviant 

Release Notes:

- Changed the behavior of `<tab>` at start of line when an inline
completion (Copilot, Supermaven, ...) is visible. If the cursor is
before the suggested indentation, `<tab>` now indents the line instead
of accepting the visible completion.

Co-authored-by: Antonio <antonio@zed.dev>
2025-01-09 12:44:52 +00:00
Kirill Bulatov
6c50659c30 Do not serialize workspace for item activations with no focus changes (#22891)
Follow-up of https://github.com/zed-industries/zed/pull/22730

Fixes excessive workspace serialization, when scrolling over outline
items in the outline panel: the panel will move the caret (selection)
over the file, following its outlines, causing the same item to be
re-activated over and over.


7a7cef2dd1/crates/workspace/src/persistence/model.rs (L257-L268)

does not seem to use position within an item, just the fact whether the
item is active or not:


7a7cef2dd1/crates/workspace/src/persistence/model.rs (L511-L517)

so, stop serializing the workspace state if no focus changes were made,
or the pane activated is the same.

Release Notes:

- N/A
2025-01-09 11:58:10 +00:00
Kirill Bulatov
a0284a272b Fix outline items navigation (#22890)
* Follows-up https://github.com/zed-industries/zed/pull/22224 , by
adjusting `impl PartialEq for OutlineEntryOutline` to compare outline
items' values too.
Before that, all outline items from the same excerpt were considered
equal.

Adds a test for this

* Stops re-revealing items in the outline panel, when it's focused: now,
when someone scrolls over outline panel items, there is no extra work
happening: the "revealed" item is the one scrolled to

Release Notes:

- Fixed outline items not scrolling properly
2025-01-09 10:25:02 +00:00
Michael Sloan
af1a3cbaac Make completion menu entries mutable (#22880)
Release Notes:

- N/A
2025-01-09 01:21:56 +00:00
Michael Sloan
05bc6b2abd assistant2: Split out implementation of Context::snapshot (#22878)
Release Notes:

- N/A
2025-01-09 00:25:16 +00:00
Kirill Bulatov
6f2b88239b Use distinct carets for line number hovers (#22836)
Release Notes:

- N/A
2025-01-08 23:51:07 +00:00
Matt Prodani
a9d2628c05 Update suggest_edits prompt to clarify usage of <old_text> when using update/create operations (#22341)
Update `suggest_edits` prompt to clarify usage of `<old_text>` when
using update/create operations using update/create operations.

- Add a mention that `old_text` is required for all but create.
- Change definition of `create` operation to also mean overwrite, as
some models heavily prefer rewrites.
- Remove mention of `If this tag is not specified, then the entire file
will be used as the range.` which is not current behavior.


Closes #22340

Release Notes:

- N/A (not sure if this requires a release note)
2025-01-08 23:45:15 +00:00
renovate[bot]
a038d61940 Update serde monorepo to v1.0.217 (#22872)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [serde](https://serde.rs)
([source](https://redirect.github.com/serde-rs/serde)) | dependencies |
patch | `1.0.216` -> `1.0.217` |
| [serde](https://serde.rs)
([source](https://redirect.github.com/serde-rs/serde)) |
workspace.dependencies | patch | `1.0.216` -> `1.0.217` |
| [serde_derive](https://serde.rs)
([source](https://redirect.github.com/serde-rs/serde)) |
workspace.dependencies | patch | `1.0.216` -> `1.0.217` |

---

### Release Notes

<details>
<summary>serde-rs/serde (serde)</summary>

###
[`v1.0.217`](https://redirect.github.com/serde-rs/serde/releases/tag/v1.0.217)

[Compare
Source](https://redirect.github.com/serde-rs/serde/compare/v1.0.216...v1.0.217)

- Support serializing externally tagged unit variant inside flattened
field
([#&#8203;2786](https://redirect.github.com/serde-rs/serde/issues/2786),
thanks [@&#8203;Mingun](https://redirect.github.com/Mingun))

</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 these
updates again.

---

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

---

Release Notes:

- N/A

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

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-08 23:23:50 +00:00
Cole Miller
1d8bd151b7 Fix double read panic in nav history (#22754)
This one seems to be triggered when the assistant's
`View<ContextEditor>` is leased during the call into
`NavHistory::for_each_entry`, which then tries to read it again through
the `ItemHandle` interface. Fix it by skipping entries that can't be
read in the history iteration.

Release Notes:

- N/A
2025-01-08 23:05:34 +00:00
renovate[bot]
ef583e6b5a Update Rust crate open to v5.3.2 (#22862)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [open](https://redirect.github.com/Byron/open-rs) | dependencies |
patch | `5.3.1` -> `5.3.2` |

---

### Release Notes

<details>
<summary>Byron/open-rs (open)</summary>

###
[`v5.3.2`](https://redirect.github.com/Byron/open-rs/blob/HEAD/changelog.md#532-2025-01-05)

[Compare
Source](https://redirect.github.com/Byron/open-rs/compare/v5.3.1...v5.3.2)

##### Bug Fixes

- <csr-id-c452a8c4e56c3726431d8a4a77ad910bc8ae3ecb/> fix `that_detached`
for UNC path of a directory

##### Commit Statistics

<csr-read-only-do-not-edit/>

- 3 commits contributed to the release over the course of 1 calendar
day.
-   51 days passed between releases.
- 1 commit was understood as
[conventional](https://www.conventionalcommits.org).
-   0 issues like '(#ID)' were seen in commit messages

##### Commit Details

<csr-read-only-do-not-edit/>

<details><summary>view details</summary>

-   **Uncategorized**
- Merge pull request
[#&#8203;107](https://redirect.github.com/Byron/open-rs/issues/107) from
amrbashir/fix/windows/remove-unc-and-fallback-on-error
([`472ce26`](472ce262c8))
- Fix `that_detached` for UNC path of a directory
([`c452a8c`](c452a8c4e5))
- Merge pull request
[#&#8203;79](https://redirect.github.com/Byron/open-rs/issues/79) from
Byron/better-docs
([`2646ff8`](2646ff820c))

</details>

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

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-08 23:00:00 +00:00
Marshall Bowers
a4dd92fe06 collab: Prevent users from creating a new subscription when they have overdue subscriptions (#22870)
This PR adjusts the create billing subscription endpoint to prevent
initiating a checkout flow when a user has existing subscriptions that
are overdue.

A subscription is considered "overdue" when either:

- The status is `past_due`
- The status is `canceled` and the cancellation reason is
`payment_failed`

In Stripe, when a subscription has failed payment a certain number of
times, it is canceled with a reason of `payment_failed`. However, today
there is nothing stopping someone from simply creating a new
subscription without paying the outstanding invoices. With this change a
user will need to reconcile their outstanding invoices before they can
sign up for a new subscription.

Release Notes:

- N/A
2025-01-08 22:50:48 +00:00
Michael Sloan
a0fca24e3f assistant2: Add live context type and use in message editor (#22865)
Release Notes:

- N/A

---------

Co-authored-by: Marshall Bowers <elliott.codes@gmail.com>
Co-authored-by: Marshall <marshall@zed.dev>
2025-01-08 21:47:58 +00:00
renovate[bot]
5d8ef94c86 Update Rust crate serde_json to v1.0.135 (#22863)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [serde_json](https://redirect.github.com/serde-rs/json) | dependencies
| patch | `1.0.134` -> `1.0.135` |
| [serde_json](https://redirect.github.com/serde-rs/json) |
workspace.dependencies | patch | `1.0.134` -> `1.0.135` |

---

### Release Notes

<details>
<summary>serde-rs/json (serde_json)</summary>

###
[`v1.0.135`](https://redirect.github.com/serde-rs/json/releases/tag/v1.0.135)

[Compare
Source](https://redirect.github.com/serde-rs/json/compare/v1.0.134...v1.0.135)

- Add serde_json::Map::into_values method
([#&#8203;1226](https://redirect.github.com/serde-rs/json/issues/1226),
thanks [@&#8203;tisonkun](https://redirect.github.com/tisonkun))

</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 these
updates again.

---

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

---

Release Notes:

- N/A

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

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-08 21:20:47 +00:00
Michael Sloan
fe35695b13 Release syntax aware heuristic expansion of diagnostic excerpts (#22858)
Implementation PR was #21942

Release Notes:

- Improved diagnostic excerpts by using syntactic info to determine the
context lines to show.
2025-01-08 20:53:52 +00:00
Conrad Irwin
9ef454d7eb Add section on how to disable "Verifying..." popup when developing on macOS (#22857)
Release Notes:

- N/A
2025-01-08 20:00:41 +00:00
Marshall Bowers
7e39023ea5 assistant2: Push logic for adding thread context down into the ContextStore (#22855)
This PR takes the logic for adding thread context out of the
`ThreadContextPicker` and pushes it down into the `ContextStore`.

Release Notes:

- N/A
2025-01-08 19:54:54 +00:00
Marshall Bowers
b78396505f collab: Record cancellation reason on billing subscriptions (#22853)
This PR updates the `billing_subscriptions` in the database to record
the cancellation reason from Stripe.

We're primarily interested in this so we can check for subscriptions
that were canceled for being `past_due`.

Release Notes:

- N/A
2025-01-08 19:38:10 +00:00
Marshall Bowers
69dde8e31d assistant2: Push logic for adding directory context down into the ContextStore (#22852)
This PR takes the logic for adding file context out of the
`DirectoryContextPicker` and pushes it down into the `ContextStore`.

Release Notes:

- N/A
2025-01-08 18:43:44 +00:00
Marshall Bowers
86f5bb1cc0 assistant2: Push logic for adding file context down into the ContextStore (#22846)
This PR takes the logic for adding file context out of the
`FileContextPicker` and pushes it down into the `ContextStore`.

Release Notes:

- N/A
2025-01-08 17:46:49 +00:00
Cole Miller
d855eb3acb Update reference to editor::OpenFile in keymap (#22827)
Follow-up to #22494

Release Notes:

- N/A
2025-01-08 17:42:22 +00:00
tims
632372a4f1 linux: Fix issue with project-specific env not being found via .envrc (direnv) (#22803)
Closes #18908

This PR started as a cleanup of redundant logic for setting up envs when
Zed is launched as a desktop entry on Linux. More on this can be read
[here](https://github.com/zed-industries/zed/pull/22335#issuecomment-2574726377).
The TLDR is that desktop entries on Linux sometimes might not have the
correct envs (as they don't `cwd` into your project directory). To
address this, we initially tried to fix it by loading the default shell
and its env vars.

However, a better solution, as recommended by @mrnugget, is to pass
`env` as `None`. Internally, if `env` is `None`, it falls back to the
project's working dir envs. This removes the need to manually load the
envs and is cleaner.

Additionally, it also fixes an issue with Zed not loading
project-specific envs because now we are actually doing so (albeit
unintentionally?).

I don't have macOS to test, but I believe this is not an issue on macOS
since it uses the Zed binary instead of the CLI, which essentially sets
the CLI `env` to `None` automatically.

Before:

Here, I have `/home/tims/go/bin` set up in `.envrc`, which only loads in
that project directory.

When launching Zed via the CLI in the project directory, notice
`/home/tims/go/bin` is in the `PATH`. As a result, we use the
user-installed `gopls` server.

```sh
[INFO] attempting to start language server "gopls", path: "/home/tims/temp/go-proj", id: 1
[INFO] using project environment variables from CLI. PATH="/home/tims/go/bin:/usr/local/go/bin"
[INFO] found user-installed language server for gopls. path: "/home/tims/go/bin/gopls", arguments: ["-mode=stdio"]
[INFO] starting language server process. binary path: "/home/tims/go/bin/gopls", working directory: "/home/tims/temp/go-proj", args: ["-mode=stdio"]
```

However, when using the desktop entry and attempting to load envs from
the default shell, notice `/home/tims/go/bin` is no longer there since
it's not in the project directory. Zed cannot find the user-installed
language server and starts downloading its own `gopls`.

```sh
[INFO] attempting to start language server "gopls", path: "/home/tims/temp/go-proj", id: 1
[INFO] using project environment variables from CLI. PATH="/usr/local/go/bin"
[INFO] fetching latest version of language server "gopls"
[INFO] downloading language server "gopls"
[INFO] starting language server process. binary path: "/home/tims/.local/share/zed/languages/gopls/gopls_0.17.1_go_1.23.4", working directory: "/home/tims/temp/go-proj", args: ["-mode=stdio"]
```

After: 

When using the desktop entry, we pass the CLI env as `None`. For the
language server, it falls back to the project directory envs. Result,
Zed finds the user-installed language server.

```sh
[INFO] attempting to start language server "gopls", path: "/home/tims/temp/go-proj", id: 1
[INFO] using project environment variables shell launched in "/home/tims/temp/go-proj". PATH="/home/tims/go/bin:/usr/local/go/bin"
[INFO] found user-installed language server for gopls. path: "/home/tims/go/bin/gopls", arguments: ["-mode=stdio"]
[INFO] starting language server process. binary path: "/home/tims/go/bin/gopls", working directory: "/home/tims/temp/go-proj", args: ["-mode=stdio"]
```

Release Notes:

- Fixed issue with project-specific env not being found via .envrc
(direnv) on Linux
2025-01-08 16:38:19 +00:00
Thorsten Ball
a248981fca zeta: Validate completion responses for markers (#22840)
Check for markers and how many there are to avoid markers showing up in
completions.

Release Notes:

- N/A
2025-01-08 16:34:05 +00:00
Vladimir Varankin
9850bf8022 Fix extend selection shortcuts in JetBrains keymap on macOS (#22814)
Fixups https://github.com/zed-industries/zed/pull/20199

As mentioned in [the post-merge comment][1], the original change was
wrong. The JetBrains IDEs use <kbd>⌥</kbd> (option) key on macOS for the
shortcuts, which corresponds to the <kbd>alt</kbd> key in the keymap
config.

Release Notes:

- Fixed extend/shrink selection in JetBrains keymap on macOS

[1]:
https://github.com/zed-industries/zed/pull/20199#issuecomment-2468136572
2025-01-08 16:01:21 +00:00
Peter Tripp
83889bb235 Bump Zed to v0.170 (#22838) 2025-01-08 11:02:44 -05:00
1010 changed files with 69866 additions and 47796 deletions

12
.cargo/ci-config.toml Normal file
View File

@@ -0,0 +1,12 @@
# This config is different from config.toml in this directory, as the latter is recognized by Cargo.
# This file is placed in ./../.cargo/config.toml on CI runs. Cargo then merges Zeds .cargo/config.toml with ./../.cargo/config.toml
# with preference for settings from Zeds config.toml.
# TL;DR: If a value is set in both ci-config.toml and config.toml, config.toml value takes precedence.
# Arrays are merged together though. See: https://doc.rust-lang.org/cargo/reference/config.html#hierarchical-structure
# The intent for this file is to configure CI build process with a divergance from Zed developers experience; for example, in this config file
# we use `-D warnings` for rustflags (which makes compilation fail in presence of warnings during build process). Placing that in developers `config.toml`
# would be incovenient.
# The reason for not using the RUSTFLAGS environment variable is that doing so would override all the settings in the config.toml file, even if the contents of the latter are completely nonsensical. See: https://github.com/rust-lang/cargo/issues/5376
# Here, we opted to use `[target.'cfg(all())']` instead of `[build]` because `[target.'**']` is guaranteed to be cumulative.
[target.'cfg(all())']
rustflags = ["-D", "warnings"]

View File

@@ -19,6 +19,10 @@ rustflags = ["-C", "link-args=-Objc -all_load"]
[target.x86_64-apple-darwin]
rustflags = ["-C", "link-args=-Objc -all_load"]
# This cfg will reduce the size of `windows::core::Error` from 16 bytes to 4 bytes
[target.'cfg(target_os = "windows")']
rustflags = ["--cfg", "windows_slim_errors"]
rustflags = [
"--cfg",
"windows_slim_errors", # This cfg will reduce the size of `windows::core::Error` from 16 bytes to 4 bytes
"-C",
"target-feature=+crt-static", # This fixes the linking issue when compiling livekit on Windows
]

View File

@@ -7,7 +7,7 @@ runs:
- name: Install Rust
shell: bash -euxo pipefail {0}
run: |
cargo install cargo-nextest
cargo install cargo-nextest --locked
- name: Install Node
uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4

View File

@@ -7,10 +7,10 @@ on:
- "v[0-9]+.[0-9]+.x"
tags:
- "v*"
pull_request:
branches:
- "**"
merge_group:
concurrency:
# Allow only one workflow per any non-`main` branch.
@@ -21,34 +21,8 @@ env:
CARGO_TERM_COLOR: always
CARGO_INCREMENTAL: 0
RUST_BACKTRACE: 1
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 [ "${{ github.event_name }}" == "merge_group" ]; then
# When we're running in a merge queue, never assume that the changes
# are docs-only, as there could be other PRs in the group that
# contain non-docs changes.
echo "Running in the merge queue"
echo "docs_only=false" >> $GITHUB_OUTPUT
elif git diff --name-only ${{ github.event.pull_request.base.sha }} ${{ github.sha }} | grep -qvE '^docs/'; then
echo "Detected non-docs changes"
echo "docs_only=false" >> $GITHUB_OUTPUT
else
echo "Docs-only change"
echo "docs_only=true" >> $GITHUB_OUTPUT
fi
migration_checks:
name: Check Postgres and Protobuf migrations, mergability
if: github.repository_owner == 'zed-industries'
@@ -103,7 +77,7 @@ jobs:
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
# To support writing comments that they will certainly be revisited.
- name: Check for todo! and FIXME comments
- name: Check for todo! and FIXME comments
run: script/check-todos
- name: Run style checks
@@ -121,43 +95,41 @@ jobs:
runs-on:
- self-hosted
- test
needs: check_docs_only
steps:
- name: Checkout repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
with:
clean: false
- name: Configure CI
run: |
mkdir -p ./../.cargo
cp ./.cargo/ci-config.toml ./../.cargo/config.toml
- 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' && needs.check_docs_only.outputs.docs_only == 'false'
if: github.event_name == 'pull_request'
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"
@@ -165,13 +137,17 @@ jobs:
cargo build -p remote_server
script/check-rust-livekit-macos
# Since the macOS runners are stateful, so we need to remove the config file to prevent potential bug.
- name: Clean CI config file
if: always()
run: rm -rf ./../.cargo
linux_tests:
timeout-minutes: 60
name: (Linux) Run Clippy and tests
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
@@ -182,37 +158,44 @@ jobs:
clean: false
- name: Cache dependencies
if: needs.check_docs_only.outputs.docs_only == 'false'
uses: swatinem/rust-cache@f0deed1e0edfc6a9be95417288c0e1099b1eeec3 # 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: Configure CI
run: |
mkdir -p ./../.cargo
cp ./.cargo/ci-config.toml ./../.cargo/config.toml
- 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
# Even the Linux runner is not stateful, in theory there is no need to do this cleanup.
# But, to avoid potential issues in the future if we choose to use a stateful Linux runner and forget to add code
# to clean up the config file, Ive included the cleanup code here as a precaution.
# While its not strictly necessary at this moment, I believe its better to err on the side of caution.
- name: Clean CI config file
if: always()
run: rm -rf ./../.cargo
build_remote_server:
timeout-minutes: 60
name: (Linux) Build Remote Server
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
@@ -223,27 +206,32 @@ jobs:
clean: false
- name: Cache dependencies
if: needs.check_docs_only.outputs.docs_only == 'false'
uses: swatinem/rust-cache@f0deed1e0edfc6a9be95417288c0e1099b1eeec3 # 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: Configure CI
run: |
mkdir -p ./../.cargo
cp ./.cargo/ci-config.toml ./../.cargo/config.toml
- name: Build Remote Server
if: needs.check_docs_only.outputs.docs_only == 'false'
run: cargo build -p remote_server
- name: Clean CI config file
if: always()
run: rm -rf ./../.cargo
# todo(windows): Actually run the tests
windows_tests:
timeout-minutes: 60
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
@@ -254,23 +242,30 @@ jobs:
clean: false
- name: Cache dependencies
if: needs.check_docs_only.outputs.docs_only == 'false'
uses: swatinem/rust-cache@f0deed1e0edfc6a9be95417288c0e1099b1eeec3 # v2
with:
save-if: ${{ github.ref == 'refs/heads/main' }}
cache-provider: "github"
- name: Configure CI
run: |
mkdir -p ./../.cargo
cp ./.cargo/ci-config.toml ./../.cargo/config.toml
- 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
# Since the Windows runners are stateful, so we need to remove the config file to prevent potential bug.
- name: Clean CI config file
if: always()
run: Remove-Item -Path "./../.cargo" -Recurse -Force
bundle-mac:
timeout-minutes: 60
timeout-minutes: 120
name: Create a macOS bundle
runs-on:
- self-hosted
@@ -332,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@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # 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@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # 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
@@ -359,9 +354,9 @@ jobs:
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
bundle-linux:
bundle-linux-x86_x64:
timeout-minutes: 60
name: Create a Linux bundle
name: Linux x86_x64 release bundle
runs-on:
- buildjet-16vcpu-ubuntu-2004
if: ${{ startsWith(github.ref, 'refs/tags/v') || contains(github.event.pull_request.labels.*.name, 'run-bundling') }}
@@ -390,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@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # 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
@@ -409,7 +404,7 @@ jobs:
bundle-linux-aarch64: # this runs on ubuntu22.04
timeout-minutes: 60
name: Create arm64 Linux bundle
name: Linux arm64 release bundle
runs-on:
- buildjet-16vcpu-ubuntu-2204-arm
if: ${{ startsWith(github.ref, 'refs/tags/v') || contains(github.event.pull_request.labels.*.name, 'run-bundling') }}
@@ -438,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@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # 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
@@ -458,7 +453,7 @@ jobs:
auto-release-preview:
name: Auto release preview
if: ${{ startsWith(github.ref, 'refs/tags/v') && endsWith(github.ref, '-pre') && !endsWith(github.ref, '.0-pre') }}
needs: [bundle-mac, bundle-linux, bundle-linux-aarch64]
needs: [bundle-mac, bundle-linux-x86_x64, bundle-linux-aarch64]
runs-on:
- self-hosted
- bundle

View File

@@ -9,7 +9,7 @@ jobs:
if: github.repository_owner == 'zed-industries'
runs-on: ubuntu-latest
steps:
- uses: actions/stale@28ca1036281a5e5922ead5184a1bbf96e5fc984e # v9
- uses: actions/stale@5bef64f19d7facfb25b37b414482c7164d639639 # v9
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
stale-issue-message: >

View File

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

View File

@@ -44,7 +44,7 @@ jobs:
- name: Install cargo nextest
shell: bash -euxo pipefail {0}
run: |
cargo install cargo-nextest
cargo install cargo-nextest --locked
- name: Limit target directory size
shell: bash -euxo pipefail {0}

View File

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

View File

@@ -9,6 +9,8 @@
# Keep these entries sorted alphabetically.
# In Zed: `editor: sort lines case insensitive`
Agus Zubiaga <agus@zed.dev>
Agus Zubiaga <agus@zed.dev> <hi@aguz.me>
Alex Viscreanu <alexviscreanu@gmail.com>
Alex Viscreanu <alexviscreanu@gmail.com> <alexandru.viscreanu@kiwi.com>
Alexander Mankuta <alex@pointless.one>
@@ -24,6 +26,7 @@ Bennet Bo Fenner <bennet@zed.dev> <53836821+bennetbo@users.noreply.github.com>
Bennet Bo Fenner <bennet@zed.dev> <bennetbo@gmx.de>
Boris Cherny <boris@anthropic.com>
Boris Cherny <boris@anthropic.com> <boris@performancejs.com>
Brian Tan <brian.tan88@gmail.com>
Chris Hayes <chris+git@hayes.software>
Christian Bergschneider <christian.bergschneider@gmx.de>
Christian Bergschneider <christian.bergschneider@gmx.de> <magiclake@gmx.de>
@@ -32,11 +35,16 @@ Conrad Irwin <conrad@zed.dev> <conrad.irwin@gmail.com>
Dairon Medina <dairon.medina@gmail.com>
Danilo Leal <danilo@zed.dev>
Danilo Leal <danilo@zed.dev> <67129314+danilo-leal@users.noreply.github.com>
Edwin Aronsson <75266237+4teapo@users.noreply.github.com>
Evren Sen <nervenes@icloud.com>
Evren Sen <nervenes@icloud.com> <146845123+evrensen467@users.noreply.github.com>
Evren Sen <nervenes@icloud.com> <146845123+evrsen@users.noreply.github.com>
Fernando Tagawa <tagawafernando@gmail.com>
Fernando Tagawa <tagawafernando@gmail.com> <fernando.tagawa.gamail.com@gmail.com>
Finn Evers <dev@bahn.sh>
Finn Evers <dev@bahn.sh> <75036051+MrSubidubi@users.noreply.github.com>
Finn Evers <dev@bahn.sh> <finn.evers@outlook.de>
Gowtham K <73059450+dovakin0007@users.noreply.github.com>
Greg Morenz <greg-morenz@droid.cafe>
Greg Morenz <greg-morenz@droid.cafe> <morenzg@gmail.com>
Ihnat Aŭtuška <autushka.ihnat@gmail.com>
@@ -54,11 +62,14 @@ Kirill Bulatov <kirill@zed.dev>
Kirill Bulatov <kirill@zed.dev> <mail4score@gmail.com>
Kyle Caverly <kylebcaverly@gmail.com>
Kyle Caverly <kylebcaverly@gmail.com> <kyle@zed.dev>
Lilith Iris <itslirissama@gmail.com>
Lilith Iris <itslirissama@gmail.com> <83819417+Irilith@users.noreply.github.com>
LoganDark <contact@logandark.mozmail.com>
LoganDark <contact@logandark.mozmail.com> <git@logandark.mozmail.com>
LoganDark <contact@logandark.mozmail.com> <github@logandark.mozmail.com>
Marshall Bowers <elliott.codes@gmail.com>
Marshall Bowers <elliott.codes@gmail.com> <marshall@zed.dev>
Marshall Bowers <git@maxdeviant.com>
Marshall Bowers <git@maxdeviant.com> <elliott.codes@gmail.com>
Marshall Bowers <git@maxdeviant.com> <marshall@zed.dev>
Matt Fellenz <matt@felle.nz>
Matt Fellenz <matt@felle.nz> <matt+github@felle.nz>
Max Brunsfeld <maxbrunsfeld@gmail.com>
@@ -112,5 +123,7 @@ Uladzislau Kaminski <i@uladkaminski.com>
Uladzislau Kaminski <i@uladkaminski.com> <uladzislau_kaminski@epam.com>
Vitaly Slobodin <vitaliy.slobodin@gmail.com>
Vitaly Slobodin <vitaliy.slobodin@gmail.com> <vitaly_slobodin@fastmail.com>
Will Bradley <williambbradley@gmail.com>
Will Bradley <williambbradley@gmail.com> <will@zed.dev>
WindSoilder <WindSoilder@outlook.com>
张小白 <364772080@qq.com>

1012
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -2,11 +2,15 @@
resolver = "2"
members = [
"crates/activity_indicator",
"crates/zed_predict_tos",
"crates/anthropic",
"crates/assets",
"crates/assistant",
"crates/assistant2",
"crates/assistant_context_editor",
"crates/assistant_settings",
"crates/assistant_slash_command",
"crates/assistant_slash_commands",
"crates/assistant_tool",
"crates/assistant_tools",
"crates/audio",
@@ -40,6 +44,7 @@ members = [
"crates/feedback",
"crates/file_finder",
"crates/file_icons",
"crates/fireworks",
"crates/fs",
"crates/fsevent",
"crates/fuzzy",
@@ -68,6 +73,7 @@ members = [
"crates/livekit_client",
"crates/livekit_client_macos",
"crates/livekit_server",
"crates/lmstudio",
"crates/lsp",
"crates/markdown",
"crates/markdown_preview",
@@ -86,6 +92,7 @@ members = [
"crates/project",
"crates/project_panel",
"crates/project_symbols",
"crates/prompt_library",
"crates/proto",
"crates/recent_projects",
"crates/refineable",
@@ -111,6 +118,7 @@ members = [
"crates/sqlez_macros",
"crates/story",
"crates/storybook",
"crates/streaming_diff",
"crates/sum_tree",
"crates/supermaven",
"crates/supermaven_api",
@@ -180,6 +188,10 @@ members = [
]
default-members = ["crates/zed"]
[workspace.package]
publish = false
edition = "2021"
[workspace.dependencies]
#
@@ -188,11 +200,15 @@ default-members = ["crates/zed"]
activity_indicator = { path = "crates/activity_indicator" }
ai = { path = "crates/ai" }
zed_predict_tos = { path = "crates/zed_predict_tos" }
anthropic = { path = "crates/anthropic" }
assets = { path = "crates/assets" }
assistant = { path = "crates/assistant" }
assistant2 = { path = "crates/assistant2" }
assistant_context_editor = { path = "crates/assistant_context_editor" }
assistant_settings = { path = "crates/assistant_settings" }
assistant_slash_command = { path = "crates/assistant_slash_command" }
assistant_slash_commands = { path = "crates/assistant_slash_commands" }
assistant_tool = { path = "crates/assistant_tool" }
assistant_tools = { path = "crates/assistant_tools" }
audio = { path = "crates/audio" }
@@ -222,6 +238,7 @@ feature_flags = { path = "crates/feature_flags" }
feedback = { path = "crates/feedback" }
file_finder = { path = "crates/file_finder" }
file_icons = { path = "crates/file_icons" }
fireworks = { path = "crates/fireworks" }
fs = { path = "crates/fs" }
fsevent = { path = "crates/fsevent" }
fuzzy = { path = "crates/fuzzy" }
@@ -253,6 +270,7 @@ languages = { path = "crates/languages" }
livekit_client = { path = "crates/livekit_client" }
livekit_client_macos = { path = "crates/livekit_client_macos" }
livekit_server = { path = "crates/livekit_server" }
lmstudio = { path = "crates/lmstudio" }
lsp = { path = "crates/lsp" }
markdown = { path = "crates/markdown" }
markdown_preview = { path = "crates/markdown_preview" }
@@ -273,6 +291,7 @@ prettier = { path = "crates/prettier" }
project = { path = "crates/project" }
project_panel = { path = "crates/project_panel" }
project_symbols = { path = "crates/project_symbols" }
prompt_library = { path = "crates/prompt_library" }
proto = { path = "crates/proto" }
recent_projects = { path = "crates/recent_projects" }
refineable = { path = "crates/refineable" }
@@ -297,6 +316,7 @@ sqlez = { path = "crates/sqlez" }
sqlez_macros = { path = "crates/sqlez_macros" }
story = { path = "crates/story" }
storybook = { path = "crates/storybook" }
streaming_diff = { path = "crates/streaming_diff" }
sum_tree = { path = "crates/sum_tree" }
supermaven = { path = "crates/supermaven" }
supermaven_api = { path = "crates/supermaven_api" }
@@ -334,7 +354,8 @@ zeta = { path = "crates/zeta" }
#
aho-corasick = "1.1"
alacritty_terminal = "0.24"
# TODO(#18342): Update to version 0.25 from crates.io when it is released.
alacritty_terminal = { git = "https://github.com/alacritty/alacritty.git", rev = "5e78d20c709cb1ab8d44ca7a8702cc26d779227c" }
any_vec = "0.14"
anyhow = "1.0.86"
arrayvec = { version = "0.7.4", features = ["serde"] }
@@ -342,7 +363,7 @@ ashpd = { version = "0.10", default-features = false, features = ["async-std"]}
async-compat = "0.2.1"
async-compression = { version = "0.4", features = ["gzip", "futures-io"] }
async-dispatcher = "0.1"
async-fs = "1.6"
async-fs = "2.1"
async-pipe = { git = "https://github.com/zed-industries/async-pipe-rs", rev = "82d00a04211cf4e1236029aa03e6b6ce2a74c553" }
async-recursion = "1.0.0"
async-tar = "0.5.0"
@@ -363,7 +384,7 @@ chrono = { version = "0.4", features = ["serde"] }
clap = { version = "4.4", features = ["derive"] }
cocoa = "0.26"
cocoa-foundation = "0.2.0"
convert_case = "0.6.0"
convert_case = "0.7.0"
core-foundation = "0.9.3"
core-foundation-sys = "0.8.6"
ctor = "0.2.6"
@@ -379,7 +400,8 @@ fork = "0.2.0"
futures = "0.3"
futures-batch = "0.6.1"
futures-lite = "1.13"
git2 = { version = "0.19", default-features = false }
# TODO: get back to regular versions when https://github.com/rust-lang/git2-rs/pull/1120 is released
git2 = { git = "https://github.com/rust-lang/git2-rs", rev = "a3b90cb3756c1bb63e2317bf9cfa57838178de5c", default-features = false }
globset = "0.4"
handlebars = "4.3"
heed = { version = "0.21.0", features = ["read-txn-no-tls"] }
@@ -391,10 +413,10 @@ ignore = "0.4.22"
image = "0.25.1"
indexmap = { version = "2.7.0", features = ["serde"] }
indoc = "2"
itertools = "0.13.0"
itertools = "0.14.0"
jsonwebtoken = "9.3"
jupyter-protocol = { version = "0.5.0" }
jupyter-websocket-client = { version = "0.8.0" }
jupyter-protocol = { version = "0.6.0" }
jupyter-websocket-client = { version = "0.9.0" }
libc = "0.2"
libsqlite3-sys = { version = "0.30.1", features = ["bundled"] }
linkify = "0.10.0"
@@ -402,7 +424,7 @@ livekit = { git = "https://github.com/zed-industries/livekit-rust-sdks", rev="06
log = { version = "0.4.16", features = ["kv_unstable_serde", "serde"] }
markup5ever_rcdom = "0.3.0"
nanoid = "0.4"
nbformat = { version = "0.9.0" }
nbformat = { version = "0.10.0" }
nix = "0.29"
num-format = "0.4.4"
ordered-float = "2.1.1"
@@ -436,7 +458,7 @@ reqwest = { git = "https://github.com/zed-industries/reqwest.git", rev = "fd110f
"stream",
] }
rsa = "0.9.6"
runtimelib = { version = "0.24.0", default-features = false, features = [
runtimelib = { version = "0.25.0", default-features = false, features = [
"async-dispatcher-runtime",
] }
rustc-demangle = "0.1.23"
@@ -461,13 +483,14 @@ signal-hook = "0.3.17"
similar = "1.3"
simplelog = "0.12.2"
smallvec = { version = "1.6", features = ["union"] }
smol = "1.2"
smol = "2.0"
sqlformat = "0.2"
strsim = "0.11"
strum = { version = "0.26.0", features = ["derive"] }
subtle = "2.5.0"
sys-locale = "0.3.1"
sysinfo = "0.31.0"
take-until = "0.2.0"
tempfile = "3.9.0"
thiserror = "1.0.29"
tiktoken-rs = "0.6.0"
@@ -490,7 +513,7 @@ tree-sitter-css = "0.23"
tree-sitter-elixir = "0.3"
tree-sitter-embedded-template = "0.23.0"
tree-sitter-go = "0.23"
tree-sitter-go-mod = { git = "https://github.com/zed-industries/tree-sitter-go-mod", rev = "a9aea5e358cde4d0f8ff20b7bc4fa311e359c7ca", package = "tree-sitter-gomod" }
tree-sitter-go-mod = { git = "https://github.com/camdencheek/tree-sitter-go-mod", rev = "6efb59652d30e0e9cd5f3b3a669afd6f1a926d3c", package = "tree-sitter-gomod" }
tree-sitter-gowork = { git = "https://github.com/zed-industries/tree-sitter-go-work", rev = "acb0617bf7f4fda02c6217676cc64acb89536dc7" }
tree-sitter-heex = { git = "https://github.com/zed-industries/tree-sitter-heex", rev = "1dd45142fbb05562e35b2040c6129c9bca346592" }
tree-sitter-diff = "0.1.0"
@@ -523,7 +546,7 @@ wasmtime-wasi = "24"
which = "6.0.0"
wit-component = "0.201"
zstd = "0.11"
metal = "0.30"
metal = "0.31"
[workspace.dependencies.async-stripe]
git = "https://github.com/zed-industries/async-stripe"
@@ -612,6 +635,7 @@ image_viewer = { codegen-units = 1 }
inline_completion_button = { codegen-units = 1 }
install_cli = { codegen-units = 1 }
journal = { codegen-units = 1 }
lmstudio = { codegen-units = 1 }
menu = { codegen-units = 1 }
notifications = { codegen-units = 1 }
ollama = { codegen-units = 1 }

View File

@@ -0,0 +1,33 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>Artboard</title>
<g id="Artboard" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<rect id="Rectangle" stroke="black" stroke-width="1.26" x="1.22" y="1.22" width="13.56" height="13.56" rx="2.66"></rect>
<g id="Group-7" transform="translate(2.44, 3.03)" fill="black">
<g id="Group" transform="translate(0.37, 0)">
<rect id="Rectangle" opacity="0.487118676" x="1.9" y="0" width="6.28" height="1.43" rx="0.71"></rect>
<rect id="Rectangle" opacity="0.845098586" x="0" y="0" width="6.28" height="1.43" rx="0.71"></rect>
</g>
<g id="Group-2" transform="translate(2.88, 1.7)">
<rect id="Rectangle" opacity="0.487118676" x="1.9" y="0" width="6.28" height="1.43" rx="0.71"></rect>
<rect id="Rectangle" opacity="0.845098586" x="0" y="0" width="6.28" height="1.43" rx="0.71"></rect>
</g>
<g id="Group-3" transform="translate(1.53, 3.38)">
<rect id="Rectangle" opacity="0.487118676" x="1.92" y="0" width="6.28" height="1.43" rx="0.71"></rect>
<rect id="Rectangle" opacity="0.845098586" x="0" y="0" width="6.28" height="1.43" rx="0.71"></rect>
</g>
<g id="Group-4" transform="translate(0, 5.09)">
<rect id="Rectangle" opacity="0.487118676" x="1.9" y="0" width="6.28" height="1.43" rx="0.71"></rect>
<rect id="Rectangle" opacity="0.845098586" x="0" y="0" width="6.28" height="1.43" rx="0.71"></rect>
</g>
<g id="Group-5" transform="translate(1.64, 6.77)">
<rect id="Rectangle" opacity="0.487118676" x="1.94" y="0" width="5.46" height="1.43" rx="0.71"></rect>
<rect id="Rectangle" opacity="0.845098586" x="0" y="0" width="5.46" height="1.43" rx="0.71"></rect>
</g>
<g id="Group-6" transform="translate(4.24, 8.47)">
<rect id="Rectangle" opacity="0.487118676" x="2.11" y="0" width="4.56" height="1.43" rx="0.71"></rect>
<rect id="Rectangle" opacity="0.845098586" x="0" y="0" width="4.56" height="1.43" rx="0.71"></rect>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

@@ -1,8 +1,4 @@
<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M3.5 2C3.22386 2 3 2.22386 3 2.5V12.5C3 12.7761 3.22386 13 3.5 13H11.5C11.7761 13 12 12.7761 12 12.5V6H8.5C8.22386 6 8 5.77614 8 5.5V2H3.5ZM9 2.70711L11.2929 5H9V2.70711ZM2 2.5C2 1.67157 2.67157 1 3.5 1H8.5C8.63261 1 8.75979 1.05268 8.85355 1.14645L12.8536 5.14645C12.9473 5.24021 13 5.36739 13 5.5V12.5C13 13.3284 12.3284 14 11.5 14H3.5C2.67157 14 2 13.3284 2 12.5V2.5Z"
fill="currentColor"
/>
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M10.0001 1.33334H4.00008C3.64646 1.33334 3.30732 1.47382 3.05727 1.72387C2.80722 1.97392 2.66675 2.31305 2.66675 2.66668V13.3333C2.66675 13.687 2.80722 14.0261 3.05727 14.2762C3.30732 14.5262 3.64646 14.6667 4.00008 14.6667H12.0001C12.3537 14.6667 12.6928 14.5262 12.9429 14.2762C13.1929 14.0261 13.3334 13.687 13.3334 13.3333V4.66668L10.0001 1.33334Z" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M9.33325 1.33334V4.00001C9.33325 4.35363 9.47373 4.69277 9.72378 4.94282C9.97383 5.19287 10.313 5.33334 10.6666 5.33334H13.3333" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 567 B

After

Width:  |  Height:  |  Size: 769 B

View File

@@ -210,208 +210,5 @@
"zsh_profile": "terminal",
"zshenv": "terminal",
"zshrc": "terminal"
},
"types": {
"astro": {
"icon": "icons/file_icons/astro.svg"
},
"audio": {
"icon": "icons/file_icons/audio.svg"
},
"bun": {
"icon": "icons/file_icons/bun.svg"
},
"c": {
"icon": "icons/file_icons/c.svg"
},
"code": {
"icon": "icons/file_icons/code.svg"
},
"coffeescript": {
"icon": "icons/file_icons/coffeescript.svg"
},
"collapsed_chevron": {
"icon": "icons/file_icons/chevron_right.svg"
},
"collapsed_folder": {
"icon": "icons/file_icons/folder.svg"
},
"cpp": {
"icon": "icons/file_icons/cpp.svg"
},
"css": {
"icon": "icons/file_icons/css.svg"
},
"dart": {
"icon": "icons/file_icons/dart.svg"
},
"default": {
"icon": "icons/file_icons/file.svg"
},
"diff": {
"icon": "icons/file_icons/diff.svg"
},
"docker": {
"icon": "icons/file_icons/docker.svg"
},
"document": {
"icon": "icons/file_icons/book.svg"
},
"elixir": {
"icon": "icons/file_icons/elixir.svg"
},
"elm": {
"icon": "icons/file_icons/elm.svg"
},
"erlang": {
"icon": "icons/file_icons/erlang.svg"
},
"eslint": {
"icon": "icons/file_icons/eslint.svg"
},
"expanded_chevron": {
"icon": "icons/file_icons/chevron_down.svg"
},
"expanded_folder": {
"icon": "icons/file_icons/folder_open.svg"
},
"font": {
"icon": "icons/file_icons/font.svg"
},
"fsharp": {
"icon": "icons/file_icons/fsharp.svg"
},
"gleam": {
"icon": "icons/file_icons/gleam.svg"
},
"go": {
"icon": "icons/file_icons/go.svg"
},
"graphql": {
"icon": "icons/file_icons/graphql.svg"
},
"haskell": {
"icon": "icons/file_icons/haskell.svg"
},
"hcl": {
"icon": "icons/file_icons/hcl.svg"
},
"heroku": {
"icon": "icons/file_icons/heroku.svg"
},
"image": {
"icon": "icons/file_icons/image.svg"
},
"java": {
"icon": "icons/file_icons/java.svg"
},
"javascript": {
"icon": "icons/file_icons/javascript.svg"
},
"julia": {
"icon": "icons/file_icons/julia.svg"
},
"kotlin": {
"icon": "icons/file_icons/kotlin.svg"
},
"lock": {
"icon": "icons/file_icons/lock.svg"
},
"log": {
"icon": "icons/file_icons/info.svg"
},
"lua": {
"icon": "icons/file_icons/lua.svg"
},
"metal": {
"icon": "icons/file_icons/metal.svg"
},
"nim": {
"icon": "icons/file_icons/nim.svg"
},
"nix": {
"icon": "icons/file_icons/nix.svg"
},
"ocaml": {
"icon": "icons/file_icons/ocaml.svg"
},
"phoenix": {
"icon": "icons/file_icons/phoenix.svg"
},
"php": {
"icon": "icons/file_icons/php.svg"
},
"prettier": {
"icon": "icons/file_icons/prettier.svg"
},
"prisma": {
"icon": "icons/file_icons/prisma.svg"
},
"python": {
"icon": "icons/file_icons/python.svg"
},
"r": {
"icon": "icons/file_icons/r.svg"
},
"react": {
"icon": "icons/file_icons/react.svg"
},
"roc": {
"icon": "icons/file_icons/roc.svg"
},
"ruby": {
"icon": "icons/file_icons/ruby.svg"
},
"rust": {
"icon": "icons/file_icons/rust.svg"
},
"sass": {
"icon": "icons/file_icons/sass.svg"
},
"scala": {
"icon": "icons/file_icons/scala.svg"
},
"settings": {
"icon": "icons/file_icons/settings.svg"
},
"storage": {
"icon": "icons/file_icons/database.svg"
},
"swift": {
"icon": "icons/file_icons/swift.svg"
},
"tcl": {
"icon": "icons/file_icons/tcl.svg"
},
"template": {
"icon": "icons/file_icons/html.svg"
},
"terminal": {
"icon": "icons/file_icons/terminal.svg"
},
"terraform": {
"icon": "icons/file_icons/terraform.svg"
},
"toml": {
"icon": "icons/file_icons/toml.svg"
},
"typescript": {
"icon": "icons/file_icons/typescript.svg"
},
"v": {
"icon": "icons/file_icons/v.svg"
},
"vcs": {
"icon": "icons/file_icons/git.svg"
},
"video": {
"icon": "icons/file_icons/video.svg"
},
"vue": {
"icon": "icons/file_icons/vue.svg"
},
"zig": {
"icon": "icons/file_icons/zig.svg"
}
}
}

View File

@@ -1,3 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8.26046 3.97337C8.3527 4.17617 8.4795 4.47151 8.57375 4.69341C8.65258 4.87898 8.83437 4.99999 9.03599 4.99999H12.5C12.7761 4.99999 13 5.22385 13 5.49999V12.125C13 12.4011 12.7761 12.625 12.5 12.625H3.5C3.22386 12.625 3 12.4011 3 12.125V3.86932C3 3.59318 3.22386 3.36932 3.5 3.36932H7.34219C7.74141 3.36932 8.09483 3.60924 8.26046 3.97337Z" stroke="black" stroke-width="1.5" stroke-linecap="round"/>
<path d="M13.3333 13.3333C13.6869 13.3333 14.026 13.1929 14.2761 12.9428C14.5261 12.6928 14.6666 12.3536 14.6666 12V5.33333C14.6666 4.97971 14.5261 4.64057 14.2761 4.39052C14.026 4.14048 13.6869 4 13.3333 4H8.06659C7.84359 4.00219 7.62362 3.94841 7.42679 3.84359C7.22996 3.73877 7.06256 3.58625 6.93992 3.4L6.39992 2.6C6.27851 2.41565 6.11324 2.26432 5.91892 2.1596C5.7246 2.05488 5.50732 2.00004 5.28659 2H2.66659C2.31296 2 1.97382 2.14048 1.72378 2.39052C1.47373 2.64057 1.33325 2.97971 1.33325 3.33333V12C1.33325 12.3536 1.47373 12.6928 1.72378 12.9428C1.97382 13.1929 2.31296 13.3333 2.66659 13.3333H13.3333Z" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 512 B

After

Width:  |  Height:  |  Size: 800 B

View File

@@ -1 +1,12 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-globe"><circle cx="12" cy="12" r="10"/><path d="M12 2a14.5 14.5 0 0 0 0 20 14.5 14.5 0 0 0 0-20"/><path d="M2 12h20"/></svg>
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_2226_61)">
<path d="M7.99992 14.6667C11.6818 14.6667 14.6666 11.6819 14.6666 8C14.6666 4.3181 11.6818 1.33333 7.99992 1.33333C4.31802 1.33333 1.33325 4.3181 1.33325 8C1.33325 11.6819 4.31802 14.6667 7.99992 14.6667Z" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M7.99992 1.33333C6.28807 3.13076 5.33325 5.51782 5.33325 8C5.33325 10.4822 6.28807 12.8692 7.99992 14.6667C9.71176 12.8692 10.6666 10.4822 10.6666 8C10.6666 5.51782 9.71176 3.13076 7.99992 1.33333Z" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M1.33325 8H14.6666" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</g>
<defs>
<clipPath id="clip0_2226_61">
<rect width="16" height="16" fill="white"/>
</clipPath>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 327 B

After

Width:  |  Height:  |  Size: 937 B

View File

@@ -1,4 +1,6 @@
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M5.46115 8.43419C7.30678 8.43419 8.92229 7.43411 8.92229 5.21171C8.92229 2.98933 7.30678 1.98926 5.46115 1.98926C3.61553 1.98926 2 2.98933 2 5.21171C2 6.028 2.21794 6.67935 2.58519 7.17685C2.7184 7.35732 2.69033 7.77795 2.58387 7.97539C2.32908 8.44793 2.81048 8.9657 3.33372 8.84571C3.72539 8.75597 4.13621 8.63447 4.49574 8.4715C4.62736 8.41181 4.7727 8.38777 4.91631 8.40402C5.09471 8.42416 5.27678 8.43419 5.46115 8.43419Z" fill="black" stroke="black" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M11.3385 6.24835C11.7049 6.74561 11.9224 7.39641 11.9224 8.2117C11.9224 9.02799 11.7044 9.67934 11.3372 10.1768C11.204 10.3573 11.232 10.7779 11.3385 10.9754C11.5933 11.4479 11.1119 11.9657 10.5886 11.8457C10.197 11.756 9.78615 11.6345 9.42662 11.4715C9.295 11.4118 9.14966 11.3878 9.00605 11.404C8.82765 11.4242 8.64558 11.4342 8.46121 11.4342C7.61469 11.4342 6.81658 11.2238 6.20055 10.7816" stroke="black" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M5.26659 13.3333C6.53897 13.986 8.00264 14.1628 9.39384 13.8319C10.785 13.5009 12.0123 12.6839 12.8544 11.5281C13.6966 10.3724 14.0982 8.95381 13.987 7.52811C13.8758 6.10241 13.259 4.76332 12.2478 3.75213C11.2366 2.74095 9.89751 2.12417 8.47181 2.01295C7.04611 1.90173 5.62757 2.30337 4.4718 3.1455C3.31603 3.98764 2.49905 5.21488 2.16807 6.60608C1.83709 7.99728 2.01388 9.46095 2.66659 10.7333L1.33325 14.6667L5.26659 13.3333Z" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M5.33325 8H5.33992" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M8 8H8.00667" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M10.6667 8H10.6734" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 954 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-circle-user"><circle cx="12" cy="12" r="10"/><circle cx="12" cy="10" r="3"/><path d="M7 20.662V19a2 2 0 0 1 2-2h6a2 2 0 0 1 2 2v1.662"/></svg>

After

Width:  |  Height:  |  Size: 345 B

View File

@@ -2,26 +2,27 @@
// Standard Linux bindings
{
"bindings": {
"shift-tab": "menu::SelectPrev",
"home": "menu::SelectFirst",
"pageup": "menu::SelectFirst",
"shift-pageup": "menu::SelectFirst",
"ctrl-p": "menu::SelectPrev",
"tab": "menu::SelectNext",
"pageup": "menu::SelectFirst",
"end": "menu::SelectLast",
"shift-pagedown": "menu::SelectLast",
"pagedown": "menu::SelectLast",
"shift-pagedown": "menu::SelectFirst",
"ctrl-n": "menu::SelectNext",
"tab": "menu::SelectNext",
"ctrl-p": "menu::SelectPrev",
"shift-tab": "menu::SelectPrev",
"enter": "menu::Confirm",
"ctrl-enter": "menu::SecondaryConfirm",
"escape": "menu::Cancel",
"ctrl-escape": "menu::Cancel",
"ctrl-c": "menu::Cancel",
"escape": "menu::Cancel",
"alt-shift-enter": "menu::Restart",
"alt-enter": ["picker::ConfirmInput", { "secondary": false }],
"ctrl-alt-enter": ["picker::ConfirmInput", { "secondary": true }],
"ctrl-shift-w": "workspace::CloseWindow",
"shift-escape": "workspace::ToggleZoom",
"open": "workspace::Open",
"ctrl-o": "workspace::Open",
"ctrl-=": "zed::IncreaseBufferFontSize",
"ctrl-+": "zed::IncreaseBufferFontSize",
@@ -29,7 +30,9 @@
"ctrl-0": "zed::ResetBufferFontSize",
"ctrl-,": "zed::OpenSettings",
"ctrl-q": "zed::Quit",
"f11": "zed::ToggleFullScreen"
"f11": "zed::ToggleFullScreen",
"ctrl-alt-z": "zeta::RateCompletions",
"ctrl-shift-i": "inline_completion::ToggleMenu"
}
},
{
@@ -50,8 +53,8 @@
"context": "Editor",
"bindings": {
"escape": "editor::Cancel",
"backspace": "editor::Backspace",
"shift-backspace": "editor::Backspace",
"backspace": "editor::Backspace",
"delete": "editor::Delete",
"tab": "editor::Tab",
"shift-tab": "editor::TabPrev",
@@ -61,11 +64,19 @@
"ctrl-k q": "editor::Rewrap",
"ctrl-backspace": "editor::DeleteToPreviousWordStart",
"ctrl-delete": "editor::DeleteToNextWordEnd",
"cut": "editor::Cut",
"shift-delete": "editor::Cut",
"ctrl-x": "editor::Cut",
"copy": "editor::Copy",
"ctrl-insert": "editor::Copy",
"ctrl-c": "editor::Copy",
"paste": "editor::Paste",
"shift-insert": "editor::Paste",
"ctrl-y": "editor::Redo",
"ctrl-v": "editor::Paste",
"undo": "editor::Undo",
"ctrl-z": "editor::Undo",
"redo": "editor::Redo",
"ctrl-y": "editor::Redo",
"ctrl-shift-z": "editor::Redo",
"up": "editor::MoveUp",
"ctrl-up": "editor::LineUp",
@@ -97,41 +108,34 @@
"ctrl-l": "editor::SelectLine",
"ctrl-shift-i": "editor::Format",
// "cmd-shift-left": ["editor::SelectToBeginningOfLine", {"stop_at_soft_wraps": true }],
"shift-home": ["editor::SelectToBeginningOfLine", { "stop_at_soft_wraps": true }],
// "ctrl-shift-a": ["editor::SelectToBeginningOfLine", { "stop_at_soft_wraps": true }],
"shift-home": ["editor::SelectToBeginningOfLine", { "stop_at_soft_wraps": true }],
// "cmd-shift-right": ["editor::SelectToEndOfLine", { "stop_at_soft_wraps": true }],
"shift-end": ["editor::SelectToEndOfLine", { "stop_at_soft_wraps": true }],
// "ctrl-shift-e": ["editor::SelectToEndOfLine", { "stop_at_soft_wraps": true }],
"shift-end": ["editor::SelectToEndOfLine", { "stop_at_soft_wraps": true }],
// "alt-v": ["editor::MovePageUp", { "center_cursor": true }],
"ctrl-alt-space": "editor::ShowCharacterPalette",
"ctrl-;": "editor::ToggleLineNumbers",
"ctrl-k ctrl-r": "editor::RevertSelectedHunks",
"ctrl-'": "editor::ToggleHunkDiff",
"ctrl-'": "editor::ToggleSelectedDiffHunks",
"ctrl-\"": "editor::ExpandAllHunkDiffs",
"ctrl-i": "editor::ShowSignatureHelp",
"alt-g b": "editor::ToggleGitBlame",
"menu": "editor::OpenContextMenu",
"shift-f10": "editor::OpenContextMenu"
}
},
{
// Separate block with same context so these display in context menus
"context": "Editor",
"bindings": {
"ctrl-x": "editor::Cut",
"ctrl-c": "editor::Copy",
"ctrl-v": "editor::Paste"
"shift-f10": "editor::OpenContextMenu",
"alt-enter": "editor::OpenSelectionsInMultibuffer"
}
},
{
"context": "Editor && mode == full",
"bindings": {
"enter": "editor::Newline",
"shift-enter": "editor::Newline",
"enter": "editor::Newline",
"ctrl-enter": "editor::NewlineAbove",
"ctrl-shift-enter": "editor::NewlineBelow",
"ctrl-k ctrl-z": "editor::ToggleSoftWrap",
"ctrl-k z": "editor::ToggleSoftWrap",
"find": "buffer_search::Deploy",
"ctrl-f": "buffer_search::Deploy",
"ctrl-h": ["buffer_search::Deploy", { "replace_enabled": true }],
// "cmd-e": ["buffer_search::Deploy", { "focus": false }],
@@ -165,6 +169,7 @@
{
"context": "Markdown",
"bindings": {
"copy": "markdown::Copy",
"ctrl-c": "markdown::Copy"
}
},
@@ -175,15 +180,17 @@
"ctrl-shift-e": "project_panel::ToggleFocus",
"ctrl-g": "search::SelectNextMatch",
"ctrl-shift-g": "search::SelectPrevMatch",
"ctrl-shift-m": "assistant::ToggleModelSelector",
"ctrl-alt-/": "assistant::ToggleModelSelector",
"ctrl-k h": "assistant::DeployHistory",
"ctrl-k l": "assistant::DeployPromptLibrary",
"new": "assistant::NewContext",
"ctrl-n": "assistant::NewContext"
}
},
{
"context": "PromptLibrary",
"bindings": {
"new": "prompt_library::NewPrompt",
"ctrl-n": "prompt_library::NewPrompt",
"ctrl-shift-s": "prompt_library::ToggleDefaultPrompt"
}
@@ -197,6 +204,7 @@
"shift-enter": "search::SelectPrevMatch",
"alt-enter": "search::SelectAllMatches",
"ctrl-f": "search::FocusSearch",
"find": "search::FocusSearch",
"ctrl-h": "search::ToggleReplace",
"ctrl-l": "search::ToggleSelection"
}
@@ -219,6 +227,7 @@
"context": "ProjectSearchBar",
"bindings": {
"escape": "project_search::ToggleFocus",
"shift-find": "search::FocusSearch",
"ctrl-shift-f": "search::FocusSearch",
"ctrl-shift-h": "search::ToggleReplace",
"alt-ctrl-g": "search::ToggleRegex",
@@ -251,32 +260,48 @@
{
"context": "Pane",
"bindings": {
"alt-1": ["pane::ActivateItem", 0],
"alt-2": ["pane::ActivateItem", 1],
"alt-3": ["pane::ActivateItem", 2],
"alt-4": ["pane::ActivateItem", 3],
"alt-5": ["pane::ActivateItem", 4],
"alt-6": ["pane::ActivateItem", 5],
"alt-7": ["pane::ActivateItem", 6],
"alt-8": ["pane::ActivateItem", 7],
"alt-9": ["pane::ActivateItem", 8],
"alt-0": "pane::ActivateLastItem",
"ctrl-pageup": "pane::ActivatePrevItem",
"ctrl-pagedown": "pane::ActivateNextItem",
"ctrl-shift-pageup": "pane::SwapItemLeft",
"ctrl-shift-pagedown": "pane::SwapItemRight",
"back": "pane::GoBack",
"forward": "pane::GoForward",
"ctrl-w": "pane::CloseActiveItem",
"ctrl-f4": "pane::CloseActiveItem",
"ctrl-w": "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": "pane::DeploySearch",
"back": "pane::GoBack",
"ctrl-alt--": "pane::GoBack",
"ctrl-alt-_": "pane::GoForward",
"forward": "pane::GoForward",
"ctrl-alt-g": "search::SelectNextMatch",
"f3": "search::SelectNextMatch",
"ctrl-alt-shift-g": "search::SelectPrevMatch",
"shift-f3": "search::SelectPrevMatch",
"ctrl-shift-f": "project_search::ToggleFocus",
"shift-find": "project_search::ToggleFocus",
"ctrl-alt-shift-h": "search::ToggleReplace",
"ctrl-alt-shift-l": "search::ToggleSelection",
"alt-enter": "search::SelectAllMatches",
"alt-c": "search::ToggleCaseSensitive",
"alt-w": "search::ToggleWholeWord",
"alt-r": "search::ToggleRegex",
"alt-ctrl-f": "project_search::ToggleFilters",
"alt-find": "project_search::ToggleFilters",
"ctrl-alt-shift-r": "search::ToggleRegex",
"ctrl-alt-shift-x": "search::ToggleRegex",
"alt-r": "search::ToggleRegex",
"ctrl-k shift-enter": "pane::TogglePinTab"
}
},
@@ -351,40 +376,25 @@
"ctrl-g": "go_to_line::Toggle"
}
},
{
"context": "Pane",
"bindings": {
"alt-1": ["pane::ActivateItem", 0],
"alt-2": ["pane::ActivateItem", 1],
"alt-3": ["pane::ActivateItem", 2],
"alt-4": ["pane::ActivateItem", 3],
"alt-5": ["pane::ActivateItem", 4],
"alt-6": ["pane::ActivateItem", 5],
"alt-7": ["pane::ActivateItem", 6],
"alt-8": ["pane::ActivateItem", 7],
"alt-9": ["pane::ActivateItem", 8],
"alt-0": "pane::ActivateLastItem",
"ctrl-alt--": "pane::GoBack",
"ctrl-alt-_": "pane::GoForward",
"ctrl-shift-t": "pane::ReopenClosedItem",
"f3": "search::SelectNextMatch",
"shift-f3": "search::SelectPrevMatch",
"ctrl-shift-f": "project_search::ToggleFocus"
}
},
{
"context": "Workspace",
"bindings": {
// Change the default action on `menu::Confirm` by setting the parameter
// "alt-ctrl-o": ["projects::OpenRecent", { "create_new_window": true }],
"alt-open": "projects::OpenRecent",
"alt-ctrl-o": "projects::OpenRecent",
"alt-shift-open": "projects::OpenRemote",
"alt-ctrl-shift-o": "projects::OpenRemote",
"alt-ctrl-shift-b": "branches::OpenRecent",
"ctrl-~": "workspace::NewTerminal",
"save": "workspace::Save",
"ctrl-s": "workspace::Save",
"ctrl-k s": "workspace::SaveWithoutFormat",
"shift-save": "workspace::SaveAs",
"ctrl-shift-s": "workspace::SaveAs",
"new": "workspace::NewFile",
"ctrl-n": "workspace::NewFile",
"shift-new": "workspace::NewWindow",
"ctrl-shift-n": "workspace::NewWindow",
"ctrl-`": "terminal_panel::ToggleFocus",
"alt-1": ["workspace::ActivatePane", 0],
@@ -400,8 +410,10 @@
"ctrl-b": "workspace::ToggleLeftDock",
"ctrl-j": "workspace::ToggleBottomDock",
"ctrl-alt-y": "workspace::CloseAllDocks",
"shift-find": "pane::DeploySearch",
"ctrl-shift-f": "pane::DeploySearch",
"ctrl-shift-h": ["pane::DeploySearch", { "replace_enabled": true }],
"ctrl-shift-t": "pane::ReopenClosedItem",
"ctrl-k ctrl-s": "zed::OpenKeymap",
"ctrl-k ctrl-t": "theme_selector::Toggle",
"ctrl-t": "project_symbols::Toggle",
@@ -409,12 +421,13 @@
"ctrl-tab": "tab_switcher::Toggle",
"ctrl-shift-tab": ["tab_switcher::Toggle", { "select_last": true }],
"ctrl-e": "file_finder::Toggle",
"ctrl-shift-p": "command_palette::Toggle",
"f1": "command_palette::Toggle",
"ctrl-shift-p": "command_palette::Toggle",
"ctrl-shift-m": "diagnostics::Deploy",
"ctrl-shift-e": "project_panel::ToggleFocus",
"ctrl-shift-b": "outline_panel::ToggleFocus",
"ctrl-?": "assistant::ToggleFocus",
"alt-save": "workspace::SaveAll",
"ctrl-alt-s": "workspace::SaveAll",
"ctrl-k m": "language_selector::Toggle",
"escape": "workspace::Unfollow",
@@ -447,7 +460,6 @@
{
"context": "Editor",
"bindings": {
"ctrl-shift-k": "editor::DeleteLine",
"ctrl-shift-d": "editor::DuplicateLineDown",
"ctrl-shift-j": "editor::JoinLines",
"ctrl-alt-backspace": "editor::DeleteToPreviousSubwordStart",
@@ -505,10 +517,10 @@
{
"context": "Editor && (showing_code_actions || showing_completions)",
"bindings": {
"up": "editor::ContextMenuPrev",
"ctrl-p": "editor::ContextMenuPrev",
"down": "editor::ContextMenuNext",
"up": "editor::ContextMenuPrev",
"ctrl-n": "editor::ContextMenuNext",
"down": "editor::ContextMenuNext",
"pageup": "editor::ContextMenuFirst",
"pagedown": "editor::ContextMenuLast"
}
@@ -532,7 +544,7 @@
"bindings": {
"alt-enter": "editor::OpenExcerpts",
"shift-enter": "editor::ExpandExcerpts",
"ctrl-k enter": "editor::OpenExcerptsSplit",
"ctrl-alt-enter": "editor::OpenExcerptsSplit",
"ctrl-shift-e": "pane::RevealInProjectPanel",
"ctrl-f8": "editor::GoToHunk",
"ctrl-shift-f8": "editor::GoToPrevHunk",
@@ -559,6 +571,7 @@
"ctrl-enter": "assistant::Assist",
"ctrl-shift-enter": "assistant::Edit",
"ctrl-s": "workspace::Save",
"save": "workspace::Save",
"ctrl->": "assistant::QuoteSelection",
"ctrl-<": "assistant::InsertIntoEditor",
"shift-enter": "assistant::Split",
@@ -567,11 +580,49 @@
"alt-enter": "editor::Newline"
}
},
{
"context": "AssistantPanel2",
"bindings": {
"ctrl-n": "assistant2::NewThread",
"new": "assistant2::NewThread",
"ctrl-shift-h": "assistant2::OpenHistory",
"ctrl-alt-/": "assistant2::ToggleModelSelector",
"ctrl-shift-a": "assistant2::ToggleContextPicker",
"ctrl-e": "assistant2::ChatMode",
"ctrl-alt-e": "assistant2::RemoveAllContext"
}
},
{
"context": "MessageEditor > Editor",
"use_key_equivalents": true,
"bindings": {
"enter": "assistant2::Chat"
}
},
{
"context": "ContextStrip",
"use_key_equivalents": true,
"bindings": {
"up": "assistant2::FocusUp",
"right": "assistant2::FocusRight",
"left": "assistant2::FocusLeft",
"down": "assistant2::FocusDown",
"backspace": "assistant2::RemoveFocusedContext",
"enter": "assistant2::AcceptSuggestedContext"
}
},
{
"context": "ThreadHistory",
"bindings": {
"backspace": "assistant2::RemoveSelectedThread"
}
},
{
"context": "PromptEditor",
"bindings": {
"ctrl-[": "assistant::CyclePreviousInlineAssist",
"ctrl-]": "assistant::CycleNextInlineAssist"
"ctrl-]": "assistant::CycleNextInlineAssist",
"ctrl-alt-e": "assistant2::RemoveAllContext"
}
},
{
@@ -586,14 +637,16 @@
"escape": "menu::Cancel",
"left": "outline_panel::CollapseSelectedEntry",
"right": "outline_panel::ExpandSelectedEntry",
"alt-copy": "outline_panel::CopyPath",
"ctrl-alt-c": "outline_panel::CopyPath",
"alt-shift-copy": "outline_panel::CopyRelativePath",
"alt-ctrl-shift-c": "outline_panel::CopyRelativePath",
"alt-ctrl-r": "outline_panel::RevealInFileManager",
"space": "outline_panel::Open",
"shift-down": "menu::SelectNext",
"shift-up": "menu::SelectPrev",
"alt-enter": "editor::OpenExcerpts",
"ctrl-k enter": "editor::OpenExcerptsSplit"
"ctrl-alt-enter": "editor::OpenExcerptsSplit"
}
},
{
@@ -601,36 +654,38 @@
"bindings": {
"left": "project_panel::CollapseSelectedEntry",
"right": "project_panel::ExpandSelectedEntry",
"new": "project_panel::NewFile",
"ctrl-n": "project_panel::NewFile",
"alt-new": "project_panel::NewDirectory",
"alt-ctrl-n": "project_panel::NewDirectory",
"cut": "project_panel::Cut",
"ctrl-x": "project_panel::Cut",
"copy": "project_panel::Copy",
"ctrl-insert": "project_panel::Copy",
"ctrl-c": "project_panel::Copy",
"paste": "project_panel::Paste",
"shift-insert": "project_panel::Paste",
"ctrl-v": "project_panel::Paste",
"alt-copy": "project_panel::CopyPath",
"ctrl-alt-c": "project_panel::CopyPath",
"alt-shift-copy": "project_panel::CopyRelativePath",
"alt-ctrl-shift-c": "project_panel::CopyRelativePath",
"enter": "project_panel::Rename",
"f2": "project_panel::Rename",
"backspace": ["project_panel::Trash", { "skip_prompt": false }],
"delete": ["project_panel::Trash", { "skip_prompt": false }],
"shift-delete": ["project_panel::Delete", { "skip_prompt": false }],
"ctrl-backspace": ["project_panel::Delete", { "skip_prompt": false }],
"ctrl-delete": ["project_panel::Delete", { "skip_prompt": false }],
"alt-ctrl-r": "project_panel::RevealInFileManager",
"ctrl-shift-enter": "project_panel::OpenWithSystem",
"ctrl-shift-f": "project_panel::NewSearchInDirectory",
"shift-find": "project_panel::NewSearchInDirectory",
"shift-down": "menu::SelectNext",
"shift-up": "menu::SelectPrev",
"escape": "menu::Cancel"
}
},
{
// Separate block with same context so these display in context menus
"context": "ProjectPanel",
"bindings": {
"f2": "project_panel::Rename",
"ctrl-c": "project_panel::Copy",
"ctrl-x": "project_panel::Cut",
"ctrl-v": "project_panel::Paste",
"delete": ["project_panel::Trash", { "skip_prompt": false }]
}
},
{
"context": "ProjectPanel && not_editing",
"bindings": {
@@ -697,9 +752,9 @@
{
"context": "TabSwitcher",
"bindings": {
"ctrl-shift-tab": "menu::SelectPrev",
"ctrl-up": "menu::SelectPrev",
"ctrl-down": "menu::SelectNext",
"ctrl-shift-tab": "menu::SelectPrev",
"ctrl-backspace": "tab_switcher::CloseSelectedItem"
}
},
@@ -707,12 +762,17 @@
"context": "Terminal",
"bindings": {
"ctrl-alt-space": "terminal::ShowCharacterPalette",
"copy": "terminal::Copy",
"ctrl-insert": "terminal::Copy",
"ctrl-shift-c": "terminal::Copy",
"paste": "terminal::Paste",
"shift-insert": "terminal::Paste",
"ctrl-shift-v": "terminal::Paste",
"ctrl-enter": "assistant::InlineAssist",
// Overrides for conflicting keybindings
"ctrl-w": ["terminal::SendKeystroke", "ctrl-w"],
"ctrl-shift-a": "editor::SelectAll",
"find": "buffer_search::Deploy",
"ctrl-shift-f": "buffer_search::Deploy",
"ctrl-shift-l": "terminal::Clear",
"ctrl-shift-w": "pane::CloseActiveItem",
@@ -732,13 +792,5 @@
"shift-end": "terminal::ScrollToBottom",
"ctrl-shift-space": "terminal::ToggleViMode"
}
},
{
// Separate block with same context so these display in context menus
"context": "Terminal",
"bindings": {
"ctrl-shift-c": "terminal::Copy",
"ctrl-shift-v": "terminal::Paste"
}
}
]

View File

@@ -3,27 +3,27 @@
{
"use_key_equivalents": true,
"bindings": {
"up": "menu::SelectPrev",
"shift-tab": "menu::SelectPrev",
"home": "menu::SelectFirst",
"pageup": "menu::SelectFirst",
"shift-pageup": "menu::SelectFirst",
"ctrl-p": "menu::SelectPrev",
"down": "menu::SelectNext",
"tab": "menu::SelectNext",
"end": "menu::SelectLast",
"pagedown": "menu::SelectLast",
"shift-pagedown": "menu::SelectFirst",
"ctrl-n": "menu::SelectNext",
"pageup": "menu::SelectFirst",
"cmd-up": "menu::SelectFirst",
"end": "menu::SelectLast",
"shift-pagedown": "menu::SelectLast",
"pagedown": "menu::SelectLast",
"cmd-down": "menu::SelectLast",
"tab": "menu::SelectNext",
"ctrl-n": "menu::SelectNext",
"down": "menu::SelectNext",
"shift-tab": "menu::SelectPrev",
"ctrl-p": "menu::SelectPrev",
"up": "menu::SelectPrev",
"enter": "menu::Confirm",
"ctrl-enter": "menu::SecondaryConfirm",
"cmd-enter": "menu::SecondaryConfirm",
"escape": "menu::Cancel",
"cmd-escape": "menu::Cancel",
"ctrl-escape": "menu::Cancel",
"cmd-escape": "menu::Cancel",
"ctrl-c": "menu::Cancel",
"escape": "menu::Cancel",
"alt-shift-enter": "menu::Restart",
"cmd-shift-w": "workspace::CloseWindow",
"shift-escape": "workspace::ToggleZoom",
@@ -37,7 +37,10 @@
"cmd-h": "zed::Hide",
"alt-cmd-h": "zed::HideOthers",
"cmd-m": "zed::Minimize",
"ctrl-cmd-f": "zed::ToggleFullScreen"
"fn-f": "zed::ToggleFullScreen",
"ctrl-cmd-f": "zed::ToggleFullScreen",
"ctrl-shift-z": "zeta::RateCompletions",
"ctrl-shift-i": "inline_completion::ToggleMenu"
}
},
{
@@ -45,18 +48,18 @@
"use_key_equivalents": true,
"bindings": {
"escape": "editor::Cancel",
"backspace": "editor::Backspace",
"shift-backspace": "editor::Backspace",
"ctrl-h": "editor::Backspace",
"delete": "editor::Delete",
"backspace": "editor::Backspace",
"ctrl-d": "editor::Delete",
"delete": "editor::Delete",
"tab": "editor::Tab",
"shift-tab": "editor::TabPrev",
"ctrl-t": "editor::Transpose",
"ctrl-k": "editor::KillRingCut",
"ctrl-y": "editor::KillRingYank",
"cmd-k q": "editor::Rewrap",
"cmd-k cmd-q": "editor::Rewrap",
"cmd-k q": "editor::Rewrap",
"cmd-backspace": "editor::DeleteToBeginningOfLine",
"cmd-delete": "editor::DeleteToEndOfLine",
"alt-backspace": "editor::DeleteToPreviousWordStart",
@@ -67,34 +70,33 @@
"cmd-v": "editor::Paste",
"cmd-z": "editor::Undo",
"cmd-shift-z": "editor::Redo",
"ctrl-shift-z": "zeta::RateCompletions",
"up": "editor::MoveUp",
"ctrl-up": "editor::MoveToStartOfParagraph",
"pageup": "editor::MovePageUp",
"shift-pageup": "editor::SelectPageUp",
"cmd-pageup": "editor::PageUp",
"ctrl-pageup": "editor::LineUp",
"home": "editor::MoveToBeginningOfLine",
"down": "editor::MoveDown",
"ctrl-down": "editor::MoveToEndOfParagraph",
"pagedown": "editor::MovePageDown",
"shift-pagedown": "editor::SelectPageDown",
"cmd-pagedown": "editor::PageDown",
"ctrl-pagedown": "editor::LineDown",
"end": "editor::MoveToEndOfLine",
"left": "editor::MoveLeft",
"right": "editor::MoveRight",
"ctrl-p": "editor::MoveUp",
"ctrl-n": "editor::MoveDown",
"ctrl-b": "editor::MoveLeft",
"left": "editor::MoveLeft",
"ctrl-f": "editor::MoveRight",
"right": "editor::MoveRight",
"ctrl-l": "editor::ScrollCursorCenter",
"alt-left": "editor::MoveToPreviousWordStart",
"alt-right": "editor::MoveToNextWordEnd",
"cmd-left": "editor::MoveToBeginningOfLine",
"ctrl-a": "editor::MoveToBeginningOfLine",
"home": "editor::MoveToBeginningOfLine",
"cmd-right": "editor::MoveToEndOfLine",
"ctrl-e": "editor::MoveToEndOfLine",
"end": "editor::MoveToEndOfLine",
"cmd-up": "editor::MoveToBeginning",
"cmd-down": "editor::MoveToEnd",
"shift-up": "editor::SelectUp",
@@ -125,20 +127,21 @@
"ctrl-cmd-space": "editor::ShowCharacterPalette",
"cmd-;": "editor::ToggleLineNumbers",
"cmd-alt-z": "editor::RevertSelectedHunks",
"cmd-'": "editor::ToggleHunkDiff",
"cmd-'": "editor::ToggleSelectedDiffHunks",
"cmd-\"": "editor::ExpandAllHunkDiffs",
"cmd-alt-g b": "editor::ToggleGitBlame",
"cmd-i": "editor::ShowSignatureHelp",
"ctrl-f12": "editor::GoToDeclaration",
"alt-ctrl-f12": "editor::GoToDeclarationSplit"
"alt-ctrl-f12": "editor::GoToDeclarationSplit",
"alt-enter": "editor::OpenSelectionsInMultibuffer"
}
},
{
"context": "Editor && mode == full",
"use_key_equivalents": true,
"bindings": {
"enter": "editor::Newline",
"shift-enter": "editor::Newline",
"enter": "editor::Newline",
"cmd-enter": "editor::NewlineBelow",
"cmd-shift-enter": "editor::NewlineAbove",
"cmd-k z": "editor::ToggleSoftWrap",
@@ -157,7 +160,7 @@
"bindings": {
"alt-tab": "editor::NextInlineCompletion",
"alt-shift-tab": "editor::PreviousInlineCompletion",
"ctrl-right": "editor::AcceptPartialInlineCompletion"
"ctrl-cmd-right": "editor::AcceptPartialInlineCompletion"
}
},
{
@@ -199,7 +202,7 @@
"cmd-shift-e": "project_panel::ToggleFocus",
"cmd-g": "search::SelectNextMatch",
"cmd-shift-g": "search::SelectPrevMatch",
"cmd-shift-m": "assistant::ToggleModelSelector",
"cmd-alt-/": "assistant::ToggleModelSelector",
"cmd-k h": "assistant::DeployHistory",
"cmd-k l": "assistant::DeployPromptLibrary",
"cmd-n": "assistant::NewContext"
@@ -225,9 +228,11 @@
"use_key_equivalents": true,
"bindings": {
"cmd-n": "assistant2::NewThread",
"cmd-alt-p": "assistant2::NewPromptEditor",
"cmd-shift-h": "assistant2::OpenHistory",
"cmd-shift-m": "assistant2::ToggleModelSelector",
"cmd-alt-/": "assistant2::ToggleModelSelector",
"cmd-shift-a": "assistant2::ToggleContextPicker",
"cmd-e": "assistant2::ChatMode",
"cmd-alt-e": "assistant2::RemoveAllContext"
}
},
@@ -238,6 +243,24 @@
"enter": "assistant2::Chat"
}
},
{
"context": "ContextStrip",
"use_key_equivalents": true,
"bindings": {
"up": "assistant2::FocusUp",
"right": "assistant2::FocusRight",
"left": "assistant2::FocusLeft",
"down": "assistant2::FocusDown",
"backspace": "assistant2::RemoveFocusedContext",
"enter": "assistant2::AcceptSuggestedContext"
}
},
{
"context": "ThreadHistory",
"bindings": {
"backspace": "assistant2::RemoveSelectedThread"
}
},
{
"context": "PromptLibrary",
"use_key_equivalents": true,
@@ -320,10 +343,10 @@
"context": "Pane",
"use_key_equivalents": true,
"bindings": {
"cmd-{": "pane::ActivatePrevItem",
"cmd-}": "pane::ActivateNextItem",
"alt-cmd-left": "pane::ActivatePrevItem",
"cmd-{": "pane::ActivatePrevItem",
"alt-cmd-right": "pane::ActivateNextItem",
"cmd-}": "pane::ActivateNextItem",
"ctrl-shift-pageup": "pane::SwapItemLeft",
"ctrl-shift-pagedown": "pane::SwapItemRight",
"cmd-w": "pane::CloseActiveItem",
@@ -353,10 +376,10 @@
"bindings": {
"cmd-[": "editor::Outdent",
"cmd-]": "editor::Indent",
"cmd-alt-up": "editor::AddSelectionAbove", // Insert cursor above
"cmd-ctrl-p": "editor::AddSelectionAbove",
"cmd-alt-down": "editor::AddSelectionBelow", // Insert cursor below
"cmd-ctrl-n": "editor::AddSelectionBelow",
"cmd-ctrl-p": "editor::AddSelectionAbove", // Insert cursor above
"cmd-alt-up": "editor::AddSelectionAbove",
"cmd-ctrl-n": "editor::AddSelectionBelow", // Insert cursor below
"cmd-alt-down": "editor::AddSelectionBelow",
"cmd-shift-k": "editor::DeleteLine",
"alt-up": "editor::MoveLineUp",
"alt-down": "editor::MoveLineDown",
@@ -383,8 +406,8 @@
"shift-f12": "editor::GoToImplementation",
"alt-cmd-f12": "editor::GoToTypeDefinitionSplit",
"alt-shift-f12": "editor::FindAllReferences",
"ctrl-m": "editor::MoveToEnclosingBracket",
"cmd-|": "editor::MoveToEnclosingBracket",
"ctrl-m": "editor::MoveToEnclosingBracket",
"alt-cmd-[": "editor::Fold",
"alt-cmd-]": "editor::UnfoldLines",
"cmd-k cmd-l": "editor::ToggleFold",
@@ -401,6 +424,8 @@
"cmd-k cmd-9": ["editor::FoldAtLevel", { "level": 9 }],
"cmd-k cmd-0": "editor::FoldAll",
"cmd-k cmd-j": "editor::UnfoldAll",
// Using `ctrl-space` in Zed requires disabling the macOS global shortcut.
// System Preferences->Keyboard->Keyboard Shortcuts->Input Sources->Select the previous input source (uncheck)
"ctrl-space": "editor::ShowCompletions",
"cmd-.": "editor::ToggleCodeActions",
"cmd-k r": "editor::RevealInFileManager",
@@ -435,7 +460,6 @@
"ctrl-0": "pane::ActivateLastItem",
"ctrl--": "pane::GoBack",
"ctrl-shift--": "pane::GoForward",
"cmd-shift-t": "pane::ReopenClosedItem",
"cmd-shift-f": "pane::DeploySearch"
}
},
@@ -469,6 +493,7 @@
"alt-cmd-y": "workspace::CloseAllDocks",
"cmd-shift-f": "pane::DeploySearch",
"cmd-shift-h": ["pane::DeploySearch", { "replace_enabled": true }],
"cmd-shift-t": "pane::ReopenClosedItem",
"cmd-k cmd-s": "zed::OpenKeymap",
"cmd-k cmd-t": "theme_selector::Toggle",
"cmd-t": "project_symbols::Toggle",
@@ -479,6 +504,7 @@
"cmd-shift-m": "diagnostics::Deploy",
"cmd-shift-e": "project_panel::ToggleFocus",
"cmd-shift-b": "outline_panel::ToggleFocus",
"ctrl-shift-g": "git_panel::ToggleFocus",
"cmd-?": "assistant::ToggleFocus",
"cmd-alt-s": "workspace::SaveAll",
"cmd-k m": "language_selector::Toggle",
@@ -503,7 +529,7 @@
"cmd-alt-r": "task::Rerun",
"ctrl-alt-shift-r": ["task::Spawn", { "reveal_target": "center" }]
// also possible to spawn tasks by name:
// "foo-bar": ["task_name::Spawn", { "task_name": "MyTask", "reveal_target": "dock" }]
// "foo-bar": ["task::Spawn", { "task_name": "MyTask", "reveal_target": "dock" }]
}
},
// Bindings from Sublime Text
@@ -596,7 +622,7 @@
"bindings": {
"alt-enter": "editor::OpenExcerpts",
"shift-enter": "editor::ExpandExcerpts",
"cmd-k enter": "editor::OpenExcerptsSplit",
"cmd-alt-enter": "editor::OpenExcerptsSplit",
"cmd-shift-e": "pane::RevealInProjectPanel",
"cmd-f8": "editor::GoToHunk",
"cmd-shift-f8": "editor::GoToPrevHunk",
@@ -616,6 +642,7 @@
"use_key_equivalents": true,
"bindings": {
"cmd-shift-a": "assistant2::ToggleContextPicker",
"cmd-alt-/": "assistant2::ToggleModelSelector",
"cmd-alt-e": "assistant2::RemoveAllContext",
"ctrl-[": "assistant::CyclePreviousInlineAssist",
"ctrl-]": "assistant::CycleNextInlineAssist"
@@ -642,7 +669,7 @@
"shift-down": "menu::SelectNext",
"shift-up": "menu::SelectPrev",
"alt-enter": "editor::OpenExcerpts",
"cmd-k enter": "editor::OpenExcerptsSplit"
"cmd-alt-enter": "editor::OpenExcerptsSplit"
}
},
{
@@ -681,6 +708,38 @@
"space": "project_panel::Open"
}
},
{
"context": "GitPanel && !CommitEditor",
"use_key_equivalents": true,
"bindings": {
"escape": "git_panel::Close"
}
},
{
"context": "GitPanel && ChangesList",
"use_key_equivalents": true,
"bindings": {
"up": "menu::SelectPrev",
"down": "menu::SelectNext",
"cmd-up": "menu::SelectFirst",
"cmd-down": "menu::SelectLast",
"enter": "menu::Confirm",
"space": "git::ToggleStaged",
"cmd-shift-space": "git::StageAll",
"ctrl-shift-space": "git::UnstageAll",
"alt-down": "git_panel::FocusEditor"
}
},
{
"context": "GitPanel && CommitEditor > Editor",
"use_key_equivalents": true,
"bindings": {
"alt-up": "git_panel::FocusChanges",
"escape": "git_panel::FocusChanges",
"cmd-enter": "git::CommitChanges",
"cmd-alt-enter": "git::CommitAllChanges"
}
},
{
"context": "CollabPanel && not_editing",
"use_key_equivalents": true,
@@ -751,9 +810,9 @@
"context": "TabSwitcher",
"use_key_equivalents": true,
"bindings": {
"ctrl-shift-tab": "menu::SelectPrev",
"ctrl-up": "menu::SelectPrev",
"ctrl-down": "menu::SelectNext",
"ctrl-shift-tab": "menu::SelectPrev",
"ctrl-backspace": "tab_switcher::CloseSelectedItem"
}
},
@@ -784,16 +843,16 @@
"escape": ["terminal::SendKeystroke", "escape"],
"enter": ["terminal::SendKeystroke", "enter"],
"ctrl-c": ["terminal::SendKeystroke", "ctrl-c"],
"cmd-up": "terminal::ScrollPageUp",
"cmd-down": "terminal::ScrollPageDown",
"shift-pageup": "terminal::ScrollPageUp",
"cmd-up": "terminal::ScrollPageUp",
"shift-pagedown": "terminal::ScrollPageDown",
"cmd-down": "terminal::ScrollPageDown",
"shift-up": "terminal::ScrollLineUp",
"shift-down": "terminal::ScrollLineDown",
"cmd-home": "terminal::ScrollToTop",
"cmd-end": "terminal::ScrollToBottom",
"shift-home": "terminal::ScrollToTop",
"cmd-home": "terminal::ScrollToTop",
"shift-end": "terminal::ScrollToBottom",
"cmd-end": "terminal::ScrollToBottom",
"ctrl-shift-space": "terminal::ToggleViMode",
"ctrl-k up": "pane::SplitUp",
"ctrl-k down": "pane::SplitDown",
@@ -805,7 +864,8 @@
"context": "RateCompletionModal",
"use_key_equivalents": true,
"bindings": {
"cmd-enter": "zeta::ThumbsUp",
"cmd-shift-enter": "zeta::ThumbsUpActiveCompletion",
"cmd-shift-backspace": "zeta::ThumbsDownActiveCompletion",
"shift-down": "zeta::NextEdit",
"shift-up": "zeta::PreviousEdit",
"right": "zeta::PreviewCompletion"
@@ -819,5 +879,12 @@
"cmd-shift-enter": "zeta::ThumbsUpActiveCompletion",
"cmd-shift-backspace": "zeta::ThumbsDownActiveCompletion"
}
},
{
"context": "ZedPredictTos",
"use_key_equivalents": true,
"bindings": {
"escape": "menu::Cancel"
}
}
]

View File

@@ -15,7 +15,9 @@
"ctrl-x b": "tab_switcher::Toggle", // switch-to-buffer
"alt-g g": "go_to_line::Toggle", // goto-line
"alt-g alt-g": "go_to_line::Toggle", // goto-line
//"ctrl-space": "editor::SetMark",
"ctrl-space": "editor::SetMark", // set-mark
"ctrl-@": "editor::SetMark", // set-mark
"ctrl-x ctrl-x": "editor::SwapSelectionEnds", // exchange-point-and-mark
"ctrl-f": "editor::MoveRight", // forward-char
"ctrl-b": "editor::MoveLeft", // backward-char
"ctrl-n": "editor::MoveDown", // next-line
@@ -24,11 +26,14 @@
"end": ["editor::MoveToEndOfLine", { "stop_at_soft_wraps": false }], // move-end-of-line
"ctrl-a": ["editor::MoveToBeginningOfLine", { "stop_at_soft_wraps": false }], // move-beginning-of-line
"ctrl-e": ["editor::MoveToEndOfLine", { "stop_at_soft_wraps": false }], // move-end-of-line
"shift-home": ["editor::SelectToBeginningOfLine", { "stop_at_soft_wraps": false }], // move-beginning-of-line
"shift-end": ["editor::SelectToEndOfLine", { "stop_at_soft_wraps": false }], // move-end-of-line
"alt-f": "editor::MoveToNextSubwordEnd", // forward-word
"alt-b": "editor::MoveToPreviousSubwordStart", // backward-word
"alt-u": "editor::ConvertToUpperCase", // upcase-word
"alt-l": "editor::ConvertToLowerCase", // downcase-word
"alt-c": "editor::ConvertToUpperCamelCase", // capitalize-word
"ctrl-t": "editor::Transpose", // transpose-chars
"alt-;": ["editor::ToggleComments", { "advance_downwards": false }],
"ctrl-x ctrl-;": "editor::ToggleComments",
"alt-.": "editor::GoToDefinition", // xref-find-definitions
@@ -55,7 +60,33 @@
}
},
{
"context": "Workspace && !Terminal",
"context": "Editor && selection_mode", // region selection
"bindings": {
"right": "editor::SelectRight",
"left": "editor::SelectLeft",
"down": "editor::SelectDown",
"up": "editor::SelectUp",
"alt-left": "editor::SelectToPreviousWordStart",
"alt-right": "editor::SelectToNextWordEnd",
"pagedown": "editor::SelectPageDown",
"pageup": "editor::SelectPageUp",
"ctrl-f": "editor::SelectRight",
"ctrl-b": "editor::SelectLeft",
"ctrl-n": "editor::SelectDown",
"ctrl-p": "editor::SelectUp",
"home": ["editor::SelectToBeginningOfLine", { "stop_at_soft_wraps": false }],
"end": ["editor::SelectToEndOfLine", { "stop_at_soft_wraps": false }],
"ctrl-a": ["editor::SelectToBeginningOfLine", { "stop_at_soft_wraps": false }],
"ctrl-e": ["editor::SelectToEndOfLine", { "stop_at_soft_wraps": false }],
"alt-f": "editor::SelectToNextWordEnd",
"alt-b": "editor::SelectToPreviousSubwordStart",
"alt-<": "editor::SelectToBeginning",
"alt->": "editor::SelectToEnd",
"ctrl-g": "editor::Cancel"
}
},
{
"context": "Workspace",
"bindings": {
"ctrl-x ctrl-c": "zed::Quit", // save-buffers-kill-terminal
"ctrl-x 5 0": "workspace::CloseWindow", // delete-frame
@@ -72,6 +103,18 @@
"ctrl-x s": "workspace::SaveAll" // save-some-buffers
}
},
{
// Workaround to enable using emacs in the Zed terminal.
// Unbind so Zed ignores these keys and lets emacs handle them.
"context": "Terminal",
"bindings": {
"ctrl-x ctrl-c": null, // save-buffers-kill-terminal
"ctrl-x ctrl-f": null, // find-file
"ctrl-x ctrl-s": null, // save-buffer
"ctrl-x ctrl-w": null, // write-file
"ctrl-x s": null // save-some-buffers
}
},
{
"context": "BufferSearchBar > Editor",
"bindings": {

View File

@@ -15,7 +15,9 @@
"ctrl-x b": "tab_switcher::Toggle", // switch-to-buffer
"alt-g g": "go_to_line::Toggle", // goto-line
"alt-g alt-g": "go_to_line::Toggle", // goto-line
//"ctrl-space": "editor::SetMark",
"ctrl-space": "editor::SetMark", // set-mark
"ctrl-@": "editor::SetMark", // set-mark
"ctrl-x ctrl-x": "editor::SwapSelectionEnds", // exchange-point-and-mark
"ctrl-f": "editor::MoveRight", // forward-char
"ctrl-b": "editor::MoveLeft", // backward-char
"ctrl-n": "editor::MoveDown", // next-line
@@ -24,11 +26,14 @@
"end": ["editor::MoveToEndOfLine", { "stop_at_soft_wraps": false }], // move-end-of-line
"ctrl-a": ["editor::MoveToBeginningOfLine", { "stop_at_soft_wraps": false }], // move-beginning-of-line
"ctrl-e": ["editor::MoveToEndOfLine", { "stop_at_soft_wraps": false }], // move-end-of-line
"shift-home": ["editor::SelectToBeginningOfLine", { "stop_at_soft_wraps": false }], // move-beginning-of-line
"shift-end": ["editor::SelectToEndOfLine", { "stop_at_soft_wraps": false }], // move-end-of-line
"alt-f": "editor::MoveToNextSubwordEnd", // forward-word
"alt-b": "editor::MoveToPreviousSubwordStart", // backward-word
"alt-u": "editor::ConvertToUpperCase", // upcase-word
"alt-l": "editor::ConvertToLowerCase", // downcase-word
"alt-c": "editor::ConvertToUpperCamelCase", // capitalize-word
"ctrl-t": "editor::Transpose", // transpose-chars
"alt-;": ["editor::ToggleComments", { "advance_downwards": false }],
"ctrl-x ctrl-;": "editor::ToggleComments",
"alt-.": "editor::GoToDefinition", // xref-find-definitions
@@ -54,6 +59,32 @@
"alt-^": "editor::JoinLines" // join-line
}
},
{
"context": "Editor && selection_mode", // region selection
"bindings": {
"right": "editor::SelectRight",
"left": "editor::SelectLeft",
"down": "editor::SelectDown",
"up": "editor::SelectUp",
"alt-left": "editor::SelectToPreviousWordStart",
"alt-right": "editor::SelectToNextWordEnd",
"pagedown": "editor::SelectPageDown",
"pageup": "editor::SelectPageUp",
"ctrl-f": "editor::SelectRight",
"ctrl-b": "editor::SelectLeft",
"ctrl-n": "editor::SelectDown",
"ctrl-p": "editor::SelectUp",
"home": ["editor::SelectToBeginningOfLine", { "stop_at_soft_wraps": false }],
"end": ["editor::SelectToEndOfLine", { "stop_at_soft_wraps": false }],
"ctrl-a": ["editor::SelectToBeginningOfLine", { "stop_at_soft_wraps": false }],
"ctrl-e": ["editor::SelectToEndOfLine", { "stop_at_soft_wraps": false }],
"alt-f": "editor::SelectToNextWordEnd",
"alt-b": "editor::SelectToPreviousSubwordStart",
"alt-<": "editor::SelectToBeginning",
"alt->": "editor::SelectToEnd",
"ctrl-g": "editor::Cancel"
}
},
{
"context": "Workspace",
"bindings": {

View File

@@ -24,8 +24,8 @@
"ctrl-g": ["editor::SelectNext", { "replace_newest": false }],
"ctrl-cmd-g": ["editor::SelectPrevious", { "replace_newest": false }],
"cmd-/": ["editor::ToggleComments", { "advance_downwards": true }],
"cmd-up": "editor::SelectLargerSyntaxNode",
"cmd-down": "editor::SelectSmallerSyntaxNode",
"alt-up": "editor::SelectLargerSyntaxNode",
"alt-down": "editor::SelectSmallerSyntaxNode",
"shift-alt-up": "editor::MoveLineUp",
"shift-alt-down": "editor::MoveLineDown",
"cmd-alt-l": "editor::Format",

View File

@@ -2,21 +2,27 @@
// Standard macOS bindings
{
"bindings": {
"up": "menu::SelectPrev",
"pageup": "menu::SelectFirst",
"home": "menu::SelectFirst",
"shift-pageup": "menu::SelectFirst",
"ctrl-p": "menu::SelectPrev",
"down": "menu::SelectNext",
"pagedown": "menu::SelectLast",
"shift-pagedown": "menu::SelectFirst",
"ctrl-n": "menu::SelectNext",
"pageup": "menu::SelectFirst",
"cmd-up": "menu::SelectFirst",
"end": "menu::SelectLast",
"shift-pagedown": "menu::SelectLast",
"pagedown": "menu::SelectLast",
"cmd-down": "menu::SelectLast",
"tab": "menu::SelectNext",
"ctrl-n": "menu::SelectNext",
"down": "menu::SelectNext",
"shift-tab": "menu::SelectPrev",
"ctrl-p": "menu::SelectPrev",
"up": "menu::SelectPrev",
"enter": "menu::Confirm",
"ctrl-enter": "menu::SecondaryConfirm",
"cmd-enter": "menu::SecondaryConfirm",
"escape": "menu::Cancel",
"ctrl-escape": "menu::Cancel",
"cmd-escape": "menu::Cancel",
"ctrl-c": "menu::Cancel",
"escape": "menu::Cancel",
"cmd-q": "storybook::Quit",
"backspace": "editor::Backspace",
"delete": "editor::Delete",

View File

@@ -4,25 +4,25 @@
"bindings": {
"i": ["vim::PushOperator", { "Object": { "around": false } }],
"a": ["vim::PushOperator", { "Object": { "around": true } }],
"h": "vim::Left",
"left": "vim::Left",
"h": "vim::Left",
"backspace": "vim::Backspace",
"j": "vim::Down",
"down": "vim::Down",
"ctrl-j": "vim::Down",
"enter": "vim::NextLineStart",
"j": "vim::Down",
"ctrl-m": "vim::NextLineStart",
"+": "vim::NextLineStart",
"enter": "vim::NextLineStart",
"-": "vim::PreviousLineStart",
"tab": "vim::Tab",
"shift-tab": "vim::Tab",
"k": "vim::Up",
"tab": "vim::Tab",
"up": "vim::Up",
"l": "vim::Right",
"k": "vim::Up",
"right": "vim::Right",
"l": "vim::Right",
"space": "vim::Space",
"$": "vim::EndOfLine",
"end": "vim::EndOfLine",
"$": "vim::EndOfLine",
"^": "vim::FirstNonWhitespace",
"_": "vim::StartOfLineDownward",
"g _": "vim::EndOfLineDownward",
@@ -86,6 +86,7 @@
"ctrl-[": ["vim::SwitchMode", "Normal"],
"v": "vim::ToggleVisual",
"shift-v": "vim::ToggleVisualLine",
"ctrl-g": "vim::ShowLocation",
"ctrl-v": "vim::ToggleVisualBlock",
"ctrl-q": "vim::ToggleVisualBlock",
"shift-k": "editor::Hover",
@@ -110,7 +111,7 @@
"g y": "editor::GoToTypeDefinition",
"g shift-i": "editor::GoToImplementation",
"g x": "editor::OpenUrl",
"g f": "editor::OpenFile",
"g f": "editor::OpenSelectedFilename",
"g n": "vim::SelectNextMatch",
"g shift-n": "vim::SelectPreviousMatch",
"g l": "vim::SelectNext",
@@ -188,8 +189,8 @@
{
"context": "vim_mode == normal",
"bindings": {
"escape": "editor::Cancel",
"ctrl-[": "editor::Cancel",
"escape": "editor::Cancel",
":": "command_palette::Toggle",
".": "vim::Repeat",
"c": ["vim::PushOperator", "Change"],
@@ -221,12 +222,13 @@
">": ["vim::PushOperator", "Indent"],
"<": ["vim::PushOperator", "Outdent"],
"=": ["vim::PushOperator", "AutoIndent"],
"!": ["vim::PushOperator", "ShellCommand"],
"g u": ["vim::PushOperator", "Lowercase"],
"g shift-u": ["vim::PushOperator", "Uppercase"],
"g ~": ["vim::PushOperator", "OppositeCase"],
"\"": ["vim::PushOperator", "Register"],
"g q": ["vim::PushOperator", "Rewrap"],
"g w": ["vim::PushOperator", "Rewrap"],
"g q": ["vim::PushOperator", "Rewrap"],
"ctrl-pagedown": "pane::ActivateNextItem",
"ctrl-pageup": "pane::ActivatePrevItem",
"insert": "vim::InsertBefore",
@@ -253,8 +255,8 @@
":": "vim::VisualCommand",
"u": "vim::ConvertToLowerCase",
"shift-u": "vim::ConvertToUpperCase",
"o": "vim::OtherEnd",
"shift-o": "vim::OtherEnd",
"o": "vim::OtherEnd",
"d": "vim::VisualDelete",
"x": "vim::VisualDelete",
"shift-d": "vim::VisualDeleteLine",
@@ -263,10 +265,10 @@
"shift-y": "vim::VisualYankLine",
"p": "vim::Paste",
"shift-p": ["vim::Paste", { "preserveClipboard": true }],
"s": "vim::Substitute",
"shift-s": "vim::SubstituteLine",
"shift-r": "vim::SubstituteLine",
"c": "vim::Substitute",
"s": "vim::Substitute",
"shift-r": "vim::SubstituteLine",
"shift-s": "vim::SubstituteLine",
"~": "vim::ChangeCase",
"*": ["vim::MoveToNext", { "partialWord": true }],
"#": ["vim::MoveToPrev", { "partialWord": true }],
@@ -282,11 +284,12 @@
"g shift-j": "vim::JoinLinesNoWhitespace",
"r": ["vim::PushOperator", "Replace"],
"ctrl-c": ["vim::SwitchMode", "Normal"],
"escape": ["vim::SwitchMode", "Normal"],
"ctrl-[": ["vim::SwitchMode", "Normal"],
"escape": ["vim::SwitchMode", "Normal"],
">": "vim::Indent",
"<": "vim::Outdent",
"=": "vim::AutoIndent",
"!": "vim::ShellCommand",
"i": ["vim::PushOperator", { "Object": { "around": false } }],
"a": ["vim::PushOperator", { "Object": { "around": true } }],
"g c": "vim::ToggleComments",
@@ -300,9 +303,9 @@
{
"context": "vim_mode == insert",
"bindings": {
"escape": "vim::NormalBefore",
"ctrl-c": "vim::NormalBefore",
"ctrl-[": "vim::NormalBefore",
"escape": "vim::NormalBefore",
"ctrl-x": null,
"ctrl-x ctrl-o": "editor::ShowCompletions",
"ctrl-x ctrl-a": "assistant::InlineAssist", // zed specific
@@ -350,9 +353,9 @@
{
"context": "vim_mode == replace",
"bindings": {
"escape": "vim::NormalBefore",
"ctrl-c": "vim::NormalBefore",
"ctrl-[": "vim::NormalBefore",
"escape": "vim::NormalBefore",
"ctrl-k": ["vim::PushOperator", { "Digraph": {} }],
"ctrl-v": ["vim::PushOperator", { "Literal": {} }],
"ctrl-shift-v": "editor::Paste", // note: this is *very* similar to ctrl-v in vim, but ctrl-shift-v on linux is the typical shortcut for paste when ctrl-v is already in use.
@@ -369,9 +372,9 @@
"bindings": {
"tab": "vim::Tab",
"enter": "vim::Enter",
"escape": "vim::ClearOperators",
"ctrl-c": "vim::ClearOperators",
"ctrl-[": "vim::ClearOperators",
"escape": "vim::ClearOperators",
"ctrl-k": ["vim::PushOperator", { "Digraph": {} }],
"ctrl-v": ["vim::PushOperator", { "Literal": {} }],
"ctrl-q": ["vim::PushOperator", { "Literal": {} }]
@@ -380,9 +383,9 @@
{
"context": "vim_mode == operator",
"bindings": {
"escape": "vim::ClearOperators",
"ctrl-c": "vim::ClearOperators",
"ctrl-[": "vim::ClearOperators",
"escape": "vim::ClearOperators",
"g c": "vim::Comment"
}
},
@@ -391,6 +394,9 @@
"bindings": {
"w": "vim::Word",
"shift-w": ["vim::Word", { "ignorePunctuation": true }],
// Subword TextObject
// "w": "vim::Subword",
// "shift-w": ["vim::Subword", { "ignorePunctuation": true }],
"t": "vim::Tag",
"s": "vim::Sentence",
"p": "vim::Paragraph",
@@ -430,7 +436,7 @@
"bindings": {
"d": "vim::CurrentLine",
"s": ["vim::PushOperator", "DeleteSurrounds"],
"o": "editor::ToggleHunkDiff", // "d o"
"o": "editor::ToggleSelectedDiffHunks", // "d o"
"p": "editor::RevertSelectedHunks" // "d p"
}
},
@@ -495,6 +501,12 @@
"=": "vim::CurrentLine"
}
},
{
"context": "vim_operator == sh",
"bindings": {
"!": "vim::CurrentLine"
}
},
{
"context": "vim_operator == gc",
"bindings": {
@@ -560,14 +572,14 @@
"ctrl-w right": ["workspace::ActivatePaneInDirection", "Right"],
"ctrl-w up": ["workspace::ActivatePaneInDirection", "Up"],
"ctrl-w down": ["workspace::ActivatePaneInDirection", "Down"],
"ctrl-w h": ["workspace::ActivatePaneInDirection", "Left"],
"ctrl-w l": ["workspace::ActivatePaneInDirection", "Right"],
"ctrl-w k": ["workspace::ActivatePaneInDirection", "Up"],
"ctrl-w j": ["workspace::ActivatePaneInDirection", "Down"],
"ctrl-w ctrl-h": ["workspace::ActivatePaneInDirection", "Left"],
"ctrl-w ctrl-l": ["workspace::ActivatePaneInDirection", "Right"],
"ctrl-w ctrl-k": ["workspace::ActivatePaneInDirection", "Up"],
"ctrl-w ctrl-j": ["workspace::ActivatePaneInDirection", "Down"],
"ctrl-w h": ["workspace::ActivatePaneInDirection", "Left"],
"ctrl-w l": ["workspace::ActivatePaneInDirection", "Right"],
"ctrl-w k": ["workspace::ActivatePaneInDirection", "Up"],
"ctrl-w j": ["workspace::ActivatePaneInDirection", "Down"],
"ctrl-w shift-left": ["workspace::SwapPaneInDirection", "Left"],
"ctrl-w shift-right": ["workspace::SwapPaneInDirection", "Right"],
"ctrl-w shift-up": ["workspace::SwapPaneInDirection", "Up"],
@@ -592,19 +604,19 @@
"ctrl-w ctrl-p": "workspace::ActivatePreviousPane",
"ctrl-w shift-w": "workspace::ActivatePreviousPane",
"ctrl-w ctrl-shift-w": "workspace::ActivatePreviousPane",
"ctrl-w v": "pane::SplitVertical",
"ctrl-w ctrl-v": "pane::SplitVertical",
"ctrl-w s": "pane::SplitHorizontal",
"ctrl-w v": "pane::SplitVertical",
"ctrl-w shift-s": "pane::SplitHorizontal",
"ctrl-w ctrl-s": "pane::SplitHorizontal",
"ctrl-w c": "pane::CloseAllItems",
"ctrl-w s": "pane::SplitHorizontal",
"ctrl-w ctrl-c": "pane::CloseAllItems",
"ctrl-w q": "pane::CloseAllItems",
"ctrl-w c": "pane::CloseAllItems",
"ctrl-w ctrl-q": "pane::CloseAllItems",
"ctrl-w o": "workspace::CloseInactiveTabsAndPanes",
"ctrl-w q": "pane::CloseAllItems",
"ctrl-w ctrl-o": "workspace::CloseInactiveTabsAndPanes",
"ctrl-w n": "workspace::NewFileSplitHorizontal",
"ctrl-w ctrl-n": "workspace::NewFileSplitHorizontal"
"ctrl-w o": "workspace::CloseInactiveTabsAndPanes",
"ctrl-w ctrl-n": "workspace::NewFileSplitHorizontal",
"ctrl-w n": "workspace::NewFileSplitHorizontal"
}
},
{

View File

@@ -13,15 +13,15 @@ You must describe the change using the following XML structure:
- <description> (optional) - An arbitrarily-long comment that describes the purpose
of this edit.
- <old_text> (optional) - An excerpt from the file's current contents that uniquely
identifies a range within the file where the edit should occur. If this tag is not
specified, then the entire file will be used as the range.
identifies a range within the file where the edit should occur. Required for all operations
except `create`.
- <new_text> (required) - The new text to insert into the file.
- <operation> (required) - The type of change that should occur at the given range
of the file. Must be one of the following values:
- `update`: Replaces the entire range with the new text.
- `insert_before`: Inserts the new text before the range.
- `insert_after`: Inserts new text after the range.
- `create`: Creates a new file with the given path and the new text.
- `create`: Creates or overwrites a file with the given path and the new text.
- `delete`: Deletes the specified range from the file.
<guidelines>

View File

@@ -372,6 +372,8 @@
"default_width": 240,
// Where to dock the project panel. Can be 'left' or 'right'.
"dock": "left",
// Spacing between worktree entries in the project panel. Can be 'comfortable' or 'standard'.
"entry_spacing": "comfortable",
// Whether to show file icons in the project panel.
"file_icons": true,
// Whether to show folder icons or chevrons for directories in the project panel.
@@ -501,7 +503,17 @@
// Where to the git panel. Can be 'left' or 'right'.
"dock": "left",
// Default width of the git panel.
"default_width": 360
"default_width": 360,
// Style of the git status indicator in the panel.
//
// Default: icon
"status_style": "icon",
"scrollbar": {
// When to show the scrollbar in the git panel.
//
// Default: inherits editor scrollbar settings
"show": null
}
},
"message_editor": {
// Whether to automatically replace emoji shortcodes with emoji characters.
@@ -896,6 +908,23 @@
// The shell running in the terminal needs to be configured to emit the title.
// Example: `echo -e "\e]2;New Title\007";`
"breadcrumbs": true
},
/// Scrollbar-related settings
"scrollbar": {
/// When to show the scrollbar in the terminal.
/// This setting can take four values:
///
/// 1. null (default): Inherit editor settings
/// 2. Show the scrollbar if there's important information or
/// follow the system's configured behavior (default):
/// "auto"
/// 3. Match the system's configured behavior:
/// "system"
/// 4. Always show the scrollbar:
/// "always"
/// 5. Never show the scrollbar:
/// "never"
"show": null
}
// Set the terminal's font size. If this option is not included,
// the terminal will default to matching the buffer's font size.
@@ -972,11 +1001,17 @@
},
"C": {
"format_on_save": "off",
"use_on_type_format": false
"use_on_type_format": false,
"prettier": {
"allowed": false
}
},
"C++": {
"format_on_save": "off",
"use_on_type_format": false
"use_on_type_format": false,
"prettier": {
"allowed": false
}
},
"CSS": {
"prettier": {
@@ -1128,6 +1163,9 @@
"openai": {
"version": "1",
"api_url": "https://api.openai.com/v1"
},
"lmstudio": {
"api_url": "http://localhost:1234/api/v0"
}
},
// Zed's Prettier integration settings.

View File

@@ -59,8 +59,9 @@
"editor.subheader.background": "#21242bff",
"editor.active_line.background": "#21242bbf",
"editor.highlighted_line.background": "#21242bff",
"editor.line_number": "#f7f7f859",
"editor.active_line_number": "#f7f7f8ff",
"editor.line_number": "#565960",
"editor.active_line_number": "#f8f8f9",
"editor.hover_line_number": "#cbcdd0",
"editor.invisible": "#64646dff",
"editor.wrap_guide": "#f7f7f80d",
"editor.active_wrap_guide": "#f7f7f81a",

View File

@@ -59,8 +59,9 @@
"editor.subheader.background": "#221f26ff",
"editor.active_line.background": "#221f26bf",
"editor.highlighted_line.background": "#221f26ff",
"editor.line_number": "#efecf459",
"editor.active_line_number": "#efecf4ff",
"editor.line_number": "#656369",
"editor.active_line_number": "#d8d8da",
"editor.hover_line_number": "#b7b5ba",
"editor.invisible": "#726c7aff",
"editor.wrap_guide": "#efecf40d",
"editor.active_wrap_guide": "#efecf41a",
@@ -444,8 +445,9 @@
"editor.subheader.background": "#e6e3ebff",
"editor.active_line.background": "#e6e3ebbf",
"editor.highlighted_line.background": "#e6e3ebff",
"editor.line_number": "#19171c59",
"editor.active_line_number": "#19171cff",
"editor.line_number": "#a4a2a8",
"editor.active_line_number": "#323135",
"editor.hover_line_number": "#58565c",
"editor.invisible": "#726c7aff",
"editor.wrap_guide": "#19171c0d",
"editor.active_wrap_guide": "#19171c1a",
@@ -829,8 +831,9 @@
"editor.subheader.background": "#262622ff",
"editor.active_line.background": "#262622bf",
"editor.highlighted_line.background": "#262622ff",
"editor.line_number": "#fefbec59",
"editor.active_line_number": "#fefbecff",
"editor.line_number": "#6d6c66",
"editor.active_line_number": "#dadad7",
"editor.hover_line_number": "#bab9b5",
"editor.invisible": "#8b8773ff",
"editor.wrap_guide": "#fefbec0d",
"editor.active_wrap_guide": "#fefbec1a",
@@ -1214,8 +1217,9 @@
"editor.subheader.background": "#eeebd7ff",
"editor.active_line.background": "#eeebd7bf",
"editor.highlighted_line.background": "#eeebd7ff",
"editor.line_number": "#20201d59",
"editor.active_line_number": "#20201dff",
"editor.line_number": "#b1afa5",
"editor.active_line_number": "#292824",
"editor.hover_line_number": "#44433b",
"editor.invisible": "#8b8773ff",
"editor.wrap_guide": "#20201d0d",
"editor.active_wrap_guide": "#20201d1a",
@@ -1599,8 +1603,9 @@
"editor.subheader.background": "#2c2b23ff",
"editor.active_line.background": "#2c2b23bf",
"editor.highlighted_line.background": "#2c2b23ff",
"editor.line_number": "#f4f3ec59",
"editor.active_line_number": "#f4f3ecff",
"editor.line_number": "#6b6b65",
"editor.active_line_number": "#e6e6e5",
"editor.hover_line_number": "#babab6",
"editor.invisible": "#7a7867ff",
"editor.wrap_guide": "#f4f3ec0d",
"editor.active_wrap_guide": "#f4f3ec1a",
@@ -1984,8 +1989,9 @@
"editor.subheader.background": "#ebeae3ff",
"editor.active_line.background": "#ebeae3bf",
"editor.highlighted_line.background": "#ebeae3ff",
"editor.line_number": "#22221b59",
"editor.active_line_number": "#22221bff",
"editor.line_number": "#abaaa4",
"editor.active_line_number": "#282725",
"editor.hover_line_number": "#42423d",
"editor.invisible": "#7a7867ff",
"editor.wrap_guide": "#22221b0d",
"editor.active_wrap_guide": "#22221b1a",
@@ -2369,8 +2375,9 @@
"editor.subheader.background": "#27211eff",
"editor.active_line.background": "#27211ebf",
"editor.highlighted_line.background": "#27211eff",
"editor.line_number": "#f0eeed59",
"editor.active_line_number": "#f0eeedff",
"editor.line_number": "#656362k",
"editor.active_line_number": "#e6e5e5",
"editor.hover_line_number": "#b9b7b7",
"editor.invisible": "#89817dff",
"editor.wrap_guide": "#f0eeed0d",
"editor.active_wrap_guide": "#f0eeed1a",
@@ -2754,8 +2761,9 @@
"editor.subheader.background": "#e9e6e4ff",
"editor.active_line.background": "#e9e6e4bf",
"editor.highlighted_line.background": "#e9e6e4ff",
"editor.line_number": "#1b191859",
"editor.active_line_number": "#1b1918ff",
"editor.line_number": "#a3a19f",
"editor.active_line_number": "#272625",
"editor.hover_line_number": "#4e4d4b",
"editor.invisible": "#89817dff",
"editor.wrap_guide": "#1b19180d",
"editor.active_wrap_guide": "#1b19181a",
@@ -3139,8 +3147,9 @@
"editor.subheader.background": "#252025ff",
"editor.active_line.background": "#252025bf",
"editor.highlighted_line.background": "#252025ff",
"editor.line_number": "#f7f3f759",
"editor.active_line_number": "#f7f3f7ff",
"editor.line_number": "#555256",
"editor.active_line_number": "#e6e5e6",
"editor.hover_line_number": "#c0bec1",
"editor.invisible": "#8b7b8bff",
"editor.wrap_guide": "#f7f3f70d",
"editor.active_wrap_guide": "#f7f3f71a",
@@ -3524,8 +3533,9 @@
"editor.subheader.background": "#e0d5e0ff",
"editor.active_line.background": "#e0d5e0bf",
"editor.highlighted_line.background": "#e0d5e0ff",
"editor.line_number": "#1b181b59",
"editor.active_line_number": "#1b181bff",
"editor.line_number": "#a9a7aa",
"editor.active_line_number": "#262627",
"editor.hover_line_number": "#403f41",
"editor.invisible": "#8b7b8bff",
"editor.wrap_guide": "#1b181b0d",
"editor.active_wrap_guide": "#1b181b1a",
@@ -3909,8 +3919,9 @@
"editor.subheader.background": "#1c2529ff",
"editor.active_line.background": "#1c2529bf",
"editor.highlighted_line.background": "#1c2529ff",
"editor.line_number": "#ebf8ff59",
"editor.active_line_number": "#ebf8ffff",
"editor.line_number": "#61686ck",
"editor.active_line_number": "#eaebec",
"editor.hover_line_number": "#bcc0c3",
"editor.invisible": "#66889aff",
"editor.wrap_guide": "#ebf8ff0d",
"editor.active_wrap_guide": "#ebf8ff1a",
@@ -4294,8 +4305,9 @@
"editor.subheader.background": "#cdeaf9ff",
"editor.active_line.background": "#cdeaf9bf",
"editor.highlighted_line.background": "#cdeaf9ff",
"editor.line_number": "#161b1d59",
"editor.active_line_number": "#161b1dff",
"editor.line_number": "#a3abafk",
"editor.active_line_number": "#242729",
"editor.hover_line_number": "#3b4144",
"editor.invisible": "#66889aff",
"editor.wrap_guide": "#161b1d0d",
"editor.active_wrap_guide": "#161b1d1a",
@@ -4679,8 +4691,9 @@
"editor.subheader.background": "#252020ff",
"editor.active_line.background": "#252020bf",
"editor.highlighted_line.background": "#252020ff",
"editor.line_number": "#f4ecec59",
"editor.active_line_number": "#f4ececff",
"editor.line_number": "#666262",
"editor.active_line_number": "#e6e5e5",
"editor.hover_line_number": "#b9b6b6",
"editor.invisible": "#726a6aff",
"editor.wrap_guide": "#f4ecec0d",
"editor.active_wrap_guide": "#f4ecec1a",
@@ -5064,8 +5077,9 @@
"editor.subheader.background": "#ebe3e3ff",
"editor.active_line.background": "#ebe3e3bf",
"editor.highlighted_line.background": "#ebe3e3ff",
"editor.line_number": "#1b181859",
"editor.active_line_number": "#1b1818ff",
"editor.line_number": "#a7a2a2",
"editor.active_line_number": "#272525",
"editor.hover_line_number": "#3f3c3c",
"editor.invisible": "#726a6aff",
"editor.wrap_guide": "#1b18180d",
"editor.active_wrap_guide": "#1b18181a",
@@ -5449,8 +5463,9 @@
"editor.subheader.background": "#1f2621ff",
"editor.active_line.background": "#1f2621bf",
"editor.highlighted_line.background": "#1f2621ff",
"editor.line_number": "#ecf4ee59",
"editor.active_line_number": "#ecf4eeff",
"editor.line_number": "#626763",
"editor.active_line_number": "#e5e6e5",
"editor.hover_line_number": "#b6b9b7",
"editor.invisible": "#6c7a71ff",
"editor.wrap_guide": "#ecf4ee0d",
"editor.active_wrap_guide": "#ecf4ee1a",
@@ -5834,8 +5849,9 @@
"editor.subheader.background": "#e3ebe6ff",
"editor.active_line.background": "#e3ebe6bf",
"editor.highlighted_line.background": "#e3ebe6ff",
"editor.line_number": "#171c1959",
"editor.active_line_number": "#171c19ff",
"editor.line_number": "#a3a9a4",
"editor.active_line_number": "#252825",
"editor.hover_line_number": "#313532",
"editor.invisible": "#6c7a71ff",
"editor.wrap_guide": "#171c190d",
"editor.active_wrap_guide": "#171c191a",
@@ -6219,8 +6235,9 @@
"editor.subheader.background": "#1f231fff",
"editor.active_line.background": "#1f231fbf",
"editor.highlighted_line.background": "#1f231fff",
"editor.line_number": "#f3faf359",
"editor.active_line_number": "#f3faf3ff",
"editor.line_number": "#626561",
"editor.active_line_number": "#e5e6e5",
"editor.hover_line_number": "#b7b9b6",
"editor.invisible": "#738b73ff",
"editor.wrap_guide": "#f3faf30d",
"editor.active_wrap_guide": "#f3faf31a",
@@ -6604,8 +6621,9 @@
"editor.subheader.background": "#daeedaff",
"editor.active_line.background": "#daeedabf",
"editor.highlighted_line.background": "#daeedaff",
"editor.line_number": "#13151359",
"editor.active_line_number": "#131513ff",
"editor.line_number": "#a6aaa5",
"editor.active_line_number": "#262725",
"editor.hover_line_number": "#3f423e",
"editor.invisible": "#738b73ff",
"editor.wrap_guide": "#1315130d",
"editor.active_wrap_guide": "#1315131a",
@@ -6989,8 +7007,9 @@
"editor.subheader.background": "#262f51ff",
"editor.active_line.background": "#262f51bf",
"editor.highlighted_line.background": "#262f51ff",
"editor.line_number": "#f5f7ff59",
"editor.active_line_number": "#f5f7ffff",
"editor.line_number": "#6b6f85",
"editor.active_line_number": "#e3e4e8",
"editor.hover_line_number": "#b8bac6",
"editor.invisible": "#7a819cff",
"editor.wrap_guide": "#f5f7ff0d",
"editor.active_wrap_guide": "#f5f7ff1a",
@@ -7374,8 +7393,9 @@
"editor.subheader.background": "#e5e8f5ff",
"editor.active_line.background": "#e5e8f5bf",
"editor.highlighted_line.background": "#e5e8f5ff",
"editor.line_number": "#20264659",
"editor.active_line_number": "#202646ff",
"editor.line_number": "#abaebd",
"editor.active_line_number": "#22232b",
"editor.hover_line_number": "#434656",
"editor.invisible": "#7a819cff",
"editor.wrap_guide": "#2026460d",
"editor.active_wrap_guide": "#2026461a",

View File

@@ -59,8 +59,9 @@
"editor.subheader.background": "#1f2127ff",
"editor.active_line.background": "#1f2127bf",
"editor.highlighted_line.background": "#1f2127ff",
"editor.line_number": "#bfbdb659",
"editor.active_line_number": "#bfbdb6ff",
"editor.line_number": "#4b4c4e",
"editor.active_line_number": "#cbcccd",
"editor.hover_line_number": "#a1a2a5",
"editor.invisible": "#666767ff",
"editor.wrap_guide": "#bfbdb60d",
"editor.active_wrap_guide": "#bfbdb61a",
@@ -429,8 +430,9 @@
"editor.subheader.background": "#ececedff",
"editor.active_line.background": "#ececedbf",
"editor.highlighted_line.background": "#ececedff",
"editor.line_number": "#5c616659",
"editor.active_line_number": "#5c6166ff",
"editor.line_number": "#b0b3b5",
"editor.active_line_number": "#313435",
"editor.hover_line_number": "#62686a",
"editor.invisible": "#acafb1ff",
"editor.wrap_guide": "#5c61660d",
"editor.active_wrap_guide": "#5c61661a",
@@ -799,8 +801,9 @@
"editor.subheader.background": "#353944ff",
"editor.active_line.background": "#353944bf",
"editor.highlighted_line.background": "#353944ff",
"editor.line_number": "#cccac259",
"editor.active_line_number": "#cccac2ff",
"editor.line_number": "#575c6b",
"editor.active_line_number": "#e1e3ea",
"editor.hover_line_number": "#b2b6c8",
"editor.invisible": "#787a7cff",
"editor.wrap_guide": "#cccac20d",
"editor.active_wrap_guide": "#cccac21a",

View File

@@ -68,8 +68,9 @@
"editor.subheader.background": "#3a3735ff",
"editor.active_line.background": "#3a3735bf",
"editor.highlighted_line.background": "#3a3735ff",
"editor.line_number": "#fbf1c759",
"editor.active_line_number": "#fbf1c7ff",
"editor.line_number": "#6e6b5e",
"editor.active_line_number": "#dedcd3",
"editor.hover_line_number": "#c9c5b6",
"editor.invisible": "#928474ff",
"editor.wrap_guide": "#fbf1c70d",
"editor.active_wrap_guide": "#fbf1c71a",
@@ -452,8 +453,9 @@
"editor.subheader.background": "#393634ff",
"editor.active_line.background": "#393634bf",
"editor.highlighted_line.background": "#393634ff",
"editor.line_number": "#fbf1c759",
"editor.active_line_number": "#fbf1c7ff",
"editor.line_number": "#6e6b5e",
"editor.active_line_number": "#dedcd3",
"editor.hover_line_number": "#c9c5b6",
"editor.invisible": "#928474ff",
"editor.wrap_guide": "#fbf1c70d",
"editor.active_wrap_guide": "#fbf1c71a",
@@ -836,8 +838,9 @@
"editor.subheader.background": "#3b3735ff",
"editor.active_line.background": "#3b3735bf",
"editor.highlighted_line.background": "#3b3735ff",
"editor.line_number": "#fbf1c759",
"editor.active_line_number": "#fbf1c7ff",
"editor.line_number": "#6e6b5e",
"editor.active_line_number": "#dedcd3",
"editor.hover_line_number": "#c9c5b6",
"editor.invisible": "#928474ff",
"editor.wrap_guide": "#fbf1c70d",
"editor.active_wrap_guide": "#fbf1c71a",
@@ -1220,8 +1223,9 @@
"editor.subheader.background": "#ecddb4ff",
"editor.active_line.background": "#ecddb4bf",
"editor.highlighted_line.background": "#ecddb4ff",
"editor.line_number": "#28282859",
"editor.active_line_number": "#282828ff",
"editor.line_number": "#a9a389",
"editor.active_line_number": "#3b382b",
"editor.hover_line_number": "#5e5a45",
"editor.invisible": "#928474ff",
"editor.wrap_guide": "#2828280d",
"editor.active_wrap_guide": "#2828281a",
@@ -1604,8 +1608,9 @@
"editor.subheader.background": "#ecddb5ff",
"editor.active_line.background": "#ecddb5bf",
"editor.highlighted_line.background": "#ecddb5ff",
"editor.line_number": "#28282859",
"editor.active_line_number": "#282828ff",
"editor.line_number": "#a9a389",
"editor.active_line_number": "#3b382b",
"editor.hover_line_number": "#5e5a45",
"editor.invisible": "#928474ff",
"editor.wrap_guide": "#2828280d",
"editor.active_wrap_guide": "#2828281a",
@@ -1988,8 +1993,9 @@
"editor.subheader.background": "#ecdcb3ff",
"editor.active_line.background": "#ecdcb3bf",
"editor.highlighted_line.background": "#ecdcb3ff",
"editor.line_number": "#28282859",
"editor.active_line_number": "#282828ff",
"editor.line_number": "#a9a389",
"editor.active_line_number": "#3b382b",
"editor.hover_line_number": "#5e5a45",
"editor.invisible": "#928474ff",
"editor.wrap_guide": "#2828280d",
"editor.active_wrap_guide": "#2828281a",

View File

@@ -59,8 +59,9 @@
"editor.subheader.background": "#2f343eff",
"editor.active_line.background": "#2f343ebf",
"editor.highlighted_line.background": "#2f343eff",
"editor.line_number": "#c8ccd459",
"editor.active_line_number": "#dce0e5ff",
"editor.line_number": "#4e5a5f",
"editor.active_line_number": "#d0d4da",
"editor.hover_line_number": "#acb0b4",
"editor.invisible": "#878a98ff",
"editor.wrap_guide": "#c8ccd40d",
"editor.active_wrap_guide": "#c8ccd41a",
@@ -434,8 +435,9 @@
"editor.subheader.background": "#ebebecff",
"editor.active_line.background": "#ebebecbf",
"editor.highlighted_line.background": "#ebebecff",
"editor.line_number": "#383a4159",
"editor.active_line_number": "#242529ff",
"editor.line_number": "#b4b4bb",
"editor.active_line_number": "#44454b",
"editor.hover_line_number": "#61616b",
"editor.invisible": "#a3a3a4ff",
"editor.wrap_guide": "#383a410d",
"editor.active_wrap_guide": "#383a411a",

View File

@@ -59,8 +59,9 @@
"editor.subheader.background": "#1c1b2aff",
"editor.active_line.background": "#1c1b2abf",
"editor.highlighted_line.background": "#1c1b2aff",
"editor.line_number": "#e0def459",
"editor.active_line_number": "#e0def4ff",
"editor.line_number": "#605e6e",
"editor.active_line_number": "#c9c8d0",
"editor.hover_line_number": "#aeadb8",
"editor.invisible": "#28253cff",
"editor.wrap_guide": "#e0def40d",
"editor.active_wrap_guide": "#e0def41a",
@@ -439,8 +440,9 @@
"editor.subheader.background": "#fef9f2ff",
"editor.active_line.background": "#fef9f2bf",
"editor.highlighted_line.background": "#fef9f2ff",
"editor.line_number": "#57527959",
"editor.active_line_number": "#575279ff",
"editor.line_number": "#b4adb8",
"editor.active_line_number": "#4e4752",
"editor.hover_line_number": "#685f6d",
"editor.invisible": "#9691a4ff",
"editor.wrap_guide": "#5752790d",
"editor.active_wrap_guide": "#5752791a",
@@ -819,8 +821,9 @@
"editor.subheader.background": "#28253cff",
"editor.active_line.background": "#28253cbf",
"editor.highlighted_line.background": "#28253cff",
"editor.line_number": "#e0def459",
"editor.active_line_number": "#e0def4ff",
"editor.line_number": "#6b697d",
"editor.active_line_number": "#d6d5dc",
"editor.hover_line_number": "#bbbac5",
"editor.invisible": "#595571ff",
"editor.wrap_guide": "#e0def40d",
"editor.active_wrap_guide": "#e0def41a",

View File

@@ -59,8 +59,9 @@
"editor.subheader.background": "#2b3038ff",
"editor.active_line.background": "#2b3038bf",
"editor.highlighted_line.background": "#2b3038ff",
"editor.line_number": "#fdf4c159",
"editor.active_line_number": "#fdf4c1ff",
"editor.line_number": "#6b6b61",
"editor.active_line_number": "#dbdbd7",
"editor.hover_line_number": "#b6b6af",
"editor.invisible": "#7c6f64ff",
"editor.wrap_guide": "#fdf4c10d",
"editor.active_wrap_guide": "#fdf4c11a",

View File

@@ -59,8 +59,9 @@
"editor.subheader.background": "#04313bff",
"editor.active_line.background": "#04313bbf",
"editor.highlighted_line.background": "#04313bff",
"editor.line_number": "#fdf6e359",
"editor.active_line_number": "#fdf6e3ff",
"editor.line_number": "#5a6d6f",
"editor.active_line_number": "#e3e8e8",
"editor.hover_line_number": "#abb9ba",
"editor.invisible": "#6c8287ff",
"editor.wrap_guide": "#fdf6e30d",
"editor.active_wrap_guide": "#fdf6e31a",
@@ -429,8 +430,9 @@
"editor.subheader.background": "#f3eddaff",
"editor.active_line.background": "#f3eddabf",
"editor.highlighted_line.background": "#f3eddaff",
"editor.line_number": "#002a3559",
"editor.active_line_number": "#002a35ff",
"editor.line_number": "#a8ad9f",
"editor.active_line_number": "#272923",
"editor.hover_line_number": "#42453b",
"editor.invisible": "#6c8287ff",
"editor.wrap_guide": "#002a350d",
"editor.active_wrap_guide": "#002a351a",

View File

@@ -59,8 +59,9 @@
"editor.subheader.background": "#231f16ff",
"editor.active_line.background": "#231f16bf",
"editor.highlighted_line.background": "#231f16ff",
"editor.line_number": "#f8f5de59",
"editor.active_line_number": "#f8f5deff",
"editor.line_number": "#676559",
"editor.active_line_number": "#e3e2de",
"editor.hover_line_number": "#b8b6ad",
"editor.invisible": "#494433ff",
"editor.wrap_guide": "#f8f5de0d",
"editor.active_wrap_guide": "#f8f5de1a",

View File

@@ -1,8 +1,8 @@
[package]
name = "activity_indicator"
version = "0.1.0"
edition = "2021"
publish = false
edition.workspace = true
publish.workspace = true
license = "GPL-3.0-or-later"
[lints]

View File

@@ -3,9 +3,9 @@ use editor::Editor;
use extension_host::ExtensionStore;
use futures::StreamExt;
use gpui::{
actions, percentage, Animation, AnimationExt as _, AppContext, CursorStyle, EventEmitter,
InteractiveElement as _, Model, ParentElement as _, Render, SharedString,
StatefulInteractiveElement, Styled, Transformation, View, ViewContext, VisualContext as _,
actions, percentage, Animation, AnimationExt as _, App, Context, CursorStyle, Entity,
EventEmitter, InteractiveElement as _, ParentElement as _, Render, SharedString,
StatefulInteractiveElement, Styled, Transformation, Window,
};
use language::{LanguageRegistry, LanguageServerBinaryStatus, LanguageServerId};
use lsp::LanguageServerName;
@@ -27,8 +27,8 @@ pub enum Event {
pub struct ActivityIndicator {
statuses: Vec<LspStatus>,
project: Model<Project>,
auto_updater: Option<Model<AutoUpdater>>,
project: Entity<Project>,
auto_updater: Option<Entity<AutoUpdater>>,
context_menu_handle: PopoverMenuHandle<ContextMenu>,
}
@@ -46,22 +46,24 @@ 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 Context<ActivityIndicator>)>>,
}
impl ActivityIndicator {
pub fn new(
workspace: &mut Workspace,
languages: Arc<LanguageRegistry>,
cx: &mut ViewContext<Workspace>,
) -> View<ActivityIndicator> {
window: &mut Window,
cx: &mut Context<Workspace>,
) -> Entity<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(|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 +72,7 @@ impl ActivityIndicator {
anyhow::Ok(())
})
.detach();
cx.observe(&project, |_, _, cx| cx.notify()).detach();
if let Some(auto_updater) = auto_updater.as_ref() {
@@ -84,13 +87,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 +106,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(|cx| {
Editor::for_buffer(buffer, Some(project.clone()), window, cx)
})),
None,
true,
window,
cx,
);
})?;
@@ -123,7 +127,7 @@ 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 Context<Self>) {
self.statuses.retain(|status| {
if let LanguageServerBinaryStatus::Failed { error } = &status.status {
cx.emit(Event::ShowError {
@@ -139,7 +143,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 Context<Self>,
) {
if let Some(updater) = &self.auto_updater {
updater.update(cx, |updater, cx| {
updater.dismiss_error(cx);
@@ -150,7 +159,7 @@ impl ActivityIndicator {
fn pending_language_server_work<'a>(
&self,
cx: &'a AppContext,
cx: &'a App,
) -> impl Iterator<Item = PendingWork<'a>> {
self.project
.read(cx)
@@ -178,12 +187,12 @@ impl ActivityIndicator {
fn pending_environment_errors<'a>(
&'a self,
cx: &'a AppContext,
cx: &'a App,
) -> impl Iterator<Item = (&'a WorktreeId, &'a EnvironmentErrorMessage)> {
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 Context<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 +202,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 +289,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 +317,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 +345,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 +360,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 +379,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 +390,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 +401,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 +412,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 +422,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 +441,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 +451,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 Context<Self>,
) {
self.context_menu_handle.toggle(window, cx);
}
}
@@ -452,7 +465,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 Context<Self>) -> impl IntoElement {
let result = h_flex()
.id("activity-indicator")
.on_action(cx.listener(Self::show_error_message))
@@ -460,7 +473,7 @@ impl Render for ActivityIndicator {
let Some(content) = self.content_to_render(cx) else {
return result;
};
let this = cx.view().downgrade();
let this = cx.entity().downgrade();
let truncate_content = content.message.len() > MAX_MESSAGE_LEN;
result.gap_2().child(
PopoverMenu::new("activity-indicator-popover")
@@ -480,24 +493,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 +526,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 +534,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 +567,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 Context<Self>,
) {
}
}

View File

@@ -1,8 +1,8 @@
[package]
name = "anthropic"
version = "0.1.0"
edition = "2021"
publish = false
edition.workspace = true
publish.workspace = true
license = "AGPL-3.0-or-later"
[features]

View File

@@ -2,7 +2,7 @@ mod supported_countries;
use std::{pin::Pin, str::FromStr};
use anyhow::{anyhow, Context, Result};
use anyhow::{anyhow, Context as _, Result};
use chrono::{DateTime, Utc};
use futures::{io::BufReader, stream::BoxStream, AsyncBufReadExt, AsyncReadExt, Stream, StreamExt};
use http_client::http::{HeaderMap, HeaderValue};
@@ -77,8 +77,8 @@ impl Model {
Model::Claude3_5Sonnet => "claude-3-5-sonnet-latest",
Model::Claude3_5Haiku => "claude-3-5-haiku-latest",
Model::Claude3Opus => "claude-3-opus-latest",
Model::Claude3Sonnet => "claude-3-sonnet-latest",
Model::Claude3Haiku => "claude-3-haiku-latest",
Model::Claude3Sonnet => "claude-3-sonnet-20240229",
Model::Claude3Haiku => "claude-3-haiku-20240307",
Self::Custom { name, .. } => name,
}
}

View File

@@ -1,8 +1,8 @@
[package]
name = "assets"
version = "0.1.0"
edition = "2021"
publish = false
edition.workspace = true
publish.workspace = true
license = "GPL-3.0-or-later"
[lib]

View File

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

View File

@@ -1,8 +1,8 @@
[package]
name = "assistant"
version = "0.1.0"
edition = "2021"
publish = false
edition.workspace = true
publish.workspace = true
license = "GPL-3.0-or-later"
[lints]
@@ -21,16 +21,14 @@ test-support = [
]
[dependencies]
anthropic = { workspace = true, features = ["schemars"] }
anyhow.workspace = true
assets.workspace = true
assistant_context_editor.workspace = true
assistant_settings.workspace = true
assistant_slash_command.workspace = true
assistant_slash_commands.workspace = true
assistant_tool.workspace = true
async-watch.workspace = true
cargo_toml.workspace = true
chrono.workspace = true
client.workspace = true
clock.workspace = true
collections.workspace = true
command_palette_hooks.workspace = true
context_server.workspace = true
@@ -39,13 +37,7 @@ editor.workspace = true
feature_flags.workspace = true
fs.workspace = true
futures.workspace = true
fuzzy.workspace = true
globset.workspace = true
gpui.workspace = true
handlebars.workspace = true
heed.workspace = true
html_to_markdown.workspace = true
http_client.workspace = true
indexed_docs.workspace = true
indoc.workspace = true
language.workspace = true
@@ -54,40 +46,30 @@ language_model_selector.workspace = true
language_models.workspace = true
log.workspace = true
lsp.workspace = true
markdown.workspace = true
menu.workspace = true
multi_buffer.workspace = true
ollama = { workspace = true, features = ["schemars"] }
open_ai = { workspace = true, features = ["schemars"] }
ordered-float.workspace = true
parking_lot.workspace = true
paths.workspace = true
picker.workspace = true
project.workspace = true
prompt_library.workspace = true
proto.workspace = true
regex.workspace = true
release_channel.workspace = true
rope.workspace = true
rpc.workspace = true
schemars.workspace = true
search.workspace = true
semantic_index.workspace = true
serde.workspace = true
serde_json.workspace = true
settings.workspace = true
similar.workspace = true
smallvec.workspace = true
smol.workspace = true
strum.workspace = true
streaming_diff.workspace = true
telemetry.workspace = true
telemetry_events.workspace = true
terminal.workspace = true
terminal_view.workspace = true
text.workspace = true
theme.workspace = true
toml.workspace = true
ui.workspace = true
util.workspace = true
uuid.workspace = true
workspace.workspace = true
zed_actions.workspace = true

View File

@@ -1,98 +1,46 @@
#![cfg_attr(target_os = "windows", allow(unused, dead_code))]
mod assistant_configuration;
pub mod assistant_panel;
pub mod assistant_settings;
mod context;
pub mod context_store;
mod inline_assistant;
mod patch;
mod prompt_library;
mod prompts;
mod slash_command;
pub(crate) mod slash_command_picker;
pub mod slash_command_settings;
mod slash_command_working_set;
mod streaming_diff;
mod terminal_inline_assistant;
use crate::slash_command::project_command::ProjectSlashCommandFeatureFlag;
pub use crate::slash_command_working_set::{SlashCommandId, SlashCommandWorkingSet};
pub use assistant_panel::{AssistantPanel, AssistantPanelEvent};
use std::sync::Arc;
use assistant_settings::AssistantSettings;
use assistant_slash_command::SlashCommandRegistry;
use client::{proto, Client};
use assistant_slash_commands::{ProjectSlashCommandFeatureFlag, SearchSlashCommandFeatureFlag};
use client::Client;
use command_palette_hooks::CommandPaletteFilter;
pub use context::*;
pub use context_store::*;
use feature_flags::FeatureFlagAppExt;
use fs::Fs;
use gpui::impl_actions;
use gpui::{actions, AppContext, Global, SharedString, UpdateGlobal};
pub(crate) use inline_assistant::*;
use gpui::{actions, App, Global, UpdateGlobal};
use language_model::{
LanguageModelId, LanguageModelProviderId, LanguageModelRegistry, LanguageModelResponseMessage,
};
pub use patch::*;
pub use prompts::PromptBuilder;
use prompts::PromptLoadingParams;
use prompt_library::PromptBuilder;
use semantic_index::{CloudEmbeddingProvider, SemanticDb};
use serde::{Deserialize, Serialize};
use serde::Deserialize;
use settings::{Settings, SettingsStore};
use slash_command::search_command::SearchSlashCommandFeatureFlag;
use slash_command::{
auto_command, cargo_workspace_command, default_command, delta_command, diagnostics_command,
docs_command, fetch_command, file_command, now_command, project_command, prompt_command,
search_command, selection_command, symbols_command, tab_command, terminal_command,
};
use std::path::PathBuf;
use std::sync::Arc;
pub(crate) use streaming_diff::*;
use util::ResultExt;
use crate::slash_command::streaming_example_command;
pub use crate::assistant_panel::{AssistantPanel, AssistantPanelEvent};
pub(crate) use crate::inline_assistant::*;
use crate::slash_command_settings::SlashCommandSettings;
actions!(
assistant,
[
Assist,
Edit,
Split,
CopyCode,
CycleMessageRole,
QuoteSelection,
InsertIntoEditor,
ToggleFocus,
InsertActivePrompt,
DeployHistory,
DeployPromptLibrary,
ConfirmCommand,
NewContext,
ToggleModelSelector,
CycleNextInlineAssist,
CyclePreviousInlineAssist
]
);
#[derive(PartialEq, Clone, Deserialize)]
pub enum InsertDraggedFiles {
ProjectPaths(Vec<PathBuf>),
ExternalFiles(Vec<PathBuf>),
}
impl_actions!(assistant, [InsertDraggedFiles]);
const DEFAULT_CONTEXT_LINES: usize = 50;
#[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub struct MessageId(clock::Lamport);
impl MessageId {
pub fn as_u64(self) -> u64 {
self.0.as_u64()
}
}
#[derive(Deserialize, Debug)]
pub struct LanguageModelUsage {
pub prompt_tokens: u32,
@@ -107,55 +55,6 @@ pub struct LanguageModelChoiceDelta {
pub finish_reason: Option<String>,
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub enum MessageStatus {
Pending,
Done,
Error(SharedString),
Canceled,
}
impl MessageStatus {
pub fn from_proto(status: proto::ContextMessageStatus) -> MessageStatus {
match status.variant {
Some(proto::context_message_status::Variant::Pending(_)) => MessageStatus::Pending,
Some(proto::context_message_status::Variant::Done(_)) => MessageStatus::Done,
Some(proto::context_message_status::Variant::Error(error)) => {
MessageStatus::Error(error.message.into())
}
Some(proto::context_message_status::Variant::Canceled(_)) => MessageStatus::Canceled,
None => MessageStatus::Pending,
}
}
pub fn to_proto(&self) -> proto::ContextMessageStatus {
match self {
MessageStatus::Pending => proto::ContextMessageStatus {
variant: Some(proto::context_message_status::Variant::Pending(
proto::context_message_status::Pending {},
)),
},
MessageStatus::Done => proto::ContextMessageStatus {
variant: Some(proto::context_message_status::Variant::Done(
proto::context_message_status::Done {},
)),
},
MessageStatus::Error(message) => proto::ContextMessageStatus {
variant: Some(proto::context_message_status::Variant::Error(
proto::context_message_status::Error {
message: message.to_string(),
},
)),
},
MessageStatus::Canceled => proto::ContextMessageStatus {
variant: Some(proto::context_message_status::Variant::Canceled(
proto::context_message_status::Canceled {},
)),
},
}
}
}
/// The state pertaining to the Assistant.
#[derive(Default)]
struct Assistant {
@@ -168,7 +67,7 @@ impl Global for Assistant {}
impl Assistant {
const NAMESPACE: &'static str = "assistant";
fn set_enabled(&mut self, enabled: bool, cx: &mut AppContext) {
fn set_enabled(&mut self, enabled: bool, cx: &mut App) {
if self.enabled == enabled {
return;
}
@@ -192,9 +91,9 @@ impl Assistant {
pub fn init(
fs: Arc<dyn Fs>,
client: Arc<Client>,
stdout_is_a_pty: bool,
cx: &mut AppContext,
) -> Arc<PromptBuilder> {
prompt_builder: Arc<PromptBuilder>,
cx: &mut App,
) {
cx.set_global(Assistant::default());
AssistantSettings::register(cx);
SlashCommandSettings::register(cx);
@@ -226,7 +125,7 @@ pub fn init(
})
.detach();
context_store::init(&client.clone().into());
assistant_context_editor::init(client.clone(), cx);
prompt_library::init(cx);
init_language_model_settings(cx);
assistant_slash_command::init(cx);
@@ -234,16 +133,6 @@ pub fn init(
assistant_panel::init(cx);
context_server::init(cx);
let prompt_builder = prompts::PromptBuilder::new(Some(PromptLoadingParams {
fs: fs.clone(),
repo_path: stdout_is_a_pty
.then(|| std::env::current_dir().log_err())
.flatten(),
cx,
}))
.log_err()
.map(Arc::new)
.unwrap_or_else(|| Arc::new(prompts::PromptBuilder::new(None).unwrap()));
register_slash_commands(Some(prompt_builder.clone()), cx);
inline_assistant::init(
fs.clone(),
@@ -274,11 +163,9 @@ pub fn init(
});
})
.detach();
prompt_builder
}
fn init_language_model_settings(cx: &mut AppContext) {
fn init_language_model_settings(cx: &mut App) {
update_active_language_model_from_settings(cx);
cx.observe_global::<SettingsStore>(update_active_language_model_from_settings)
@@ -297,7 +184,7 @@ fn init_language_model_settings(cx: &mut AppContext) {
.detach();
}
fn update_active_language_model_from_settings(cx: &mut AppContext) {
fn update_active_language_model_from_settings(cx: &mut App) {
let settings = AssistantSettings::get_global(cx);
let provider_name = LanguageModelProviderId::from(settings.default_model.provider.clone());
let model_id = LanguageModelId::from(settings.default_model.model.clone());
@@ -317,30 +204,31 @@ fn update_active_language_model_from_settings(cx: &mut AppContext) {
});
}
fn register_slash_commands(prompt_builder: Option<Arc<PromptBuilder>>, cx: &mut AppContext) {
fn register_slash_commands(prompt_builder: Option<Arc<PromptBuilder>>, cx: &mut App) {
let slash_command_registry = SlashCommandRegistry::global(cx);
slash_command_registry.register_command(file_command::FileSlashCommand, true);
slash_command_registry.register_command(delta_command::DeltaSlashCommand, true);
slash_command_registry.register_command(symbols_command::OutlineSlashCommand, true);
slash_command_registry.register_command(tab_command::TabSlashCommand, true);
slash_command_registry.register_command(assistant_slash_commands::FileSlashCommand, true);
slash_command_registry.register_command(assistant_slash_commands::DeltaSlashCommand, true);
slash_command_registry.register_command(assistant_slash_commands::OutlineSlashCommand, true);
slash_command_registry.register_command(assistant_slash_commands::TabSlashCommand, true);
slash_command_registry
.register_command(cargo_workspace_command::CargoWorkspaceSlashCommand, true);
slash_command_registry.register_command(prompt_command::PromptSlashCommand, true);
slash_command_registry.register_command(selection_command::SelectionCommand, true);
slash_command_registry.register_command(default_command::DefaultSlashCommand, false);
slash_command_registry.register_command(terminal_command::TerminalSlashCommand, true);
slash_command_registry.register_command(now_command::NowSlashCommand, false);
slash_command_registry.register_command(diagnostics_command::DiagnosticsSlashCommand, true);
slash_command_registry.register_command(fetch_command::FetchSlashCommand, true);
.register_command(assistant_slash_commands::CargoWorkspaceSlashCommand, true);
slash_command_registry.register_command(assistant_slash_commands::PromptSlashCommand, true);
slash_command_registry.register_command(assistant_slash_commands::SelectionCommand, true);
slash_command_registry.register_command(assistant_slash_commands::DefaultSlashCommand, false);
slash_command_registry.register_command(assistant_slash_commands::TerminalSlashCommand, true);
slash_command_registry.register_command(assistant_slash_commands::NowSlashCommand, false);
slash_command_registry
.register_command(assistant_slash_commands::DiagnosticsSlashCommand, true);
slash_command_registry.register_command(assistant_slash_commands::FetchSlashCommand, true);
if let Some(prompt_builder) = prompt_builder {
cx.observe_flag::<project_command::ProjectSlashCommandFeatureFlag, _>({
cx.observe_flag::<assistant_slash_commands::ProjectSlashCommandFeatureFlag, _>({
let slash_command_registry = slash_command_registry.clone();
move |is_enabled, _cx| {
if is_enabled {
slash_command_registry.register_command(
project_command::ProjectSlashCommand::new(prompt_builder.clone()),
assistant_slash_commands::ProjectSlashCommand::new(prompt_builder.clone()),
true,
);
}
@@ -349,23 +237,24 @@ fn register_slash_commands(prompt_builder: Option<Arc<PromptBuilder>>, cx: &mut
.detach();
}
cx.observe_flag::<auto_command::AutoSlashCommandFeatureFlag, _>({
cx.observe_flag::<assistant_slash_commands::AutoSlashCommandFeatureFlag, _>({
let slash_command_registry = slash_command_registry.clone();
move |is_enabled, _cx| {
if is_enabled {
// [#auto-staff-ship] TODO remove this when /auto is no longer staff-shipped
slash_command_registry.register_command(auto_command::AutoCommand, true);
slash_command_registry
.register_command(assistant_slash_commands::AutoCommand, true);
}
}
})
.detach();
cx.observe_flag::<streaming_example_command::StreamingExampleSlashCommandFeatureFlag, _>({
cx.observe_flag::<assistant_slash_commands::StreamingExampleSlashCommandFeatureFlag, _>({
let slash_command_registry = slash_command_registry.clone();
move |is_enabled, _cx| {
if is_enabled {
slash_command_registry.register_command(
streaming_example_command::StreamingExampleSlashCommand,
assistant_slash_commands::StreamingExampleSlashCommand,
false,
);
}
@@ -377,51 +266,34 @@ fn register_slash_commands(prompt_builder: Option<Arc<PromptBuilder>>, cx: &mut
cx.observe_global::<SettingsStore>(update_slash_commands_from_settings)
.detach();
cx.observe_flag::<search_command::SearchSlashCommandFeatureFlag, _>({
cx.observe_flag::<assistant_slash_commands::SearchSlashCommandFeatureFlag, _>({
let slash_command_registry = slash_command_registry.clone();
move |is_enabled, _cx| {
if is_enabled {
slash_command_registry.register_command(search_command::SearchSlashCommand, true);
slash_command_registry
.register_command(assistant_slash_commands::SearchSlashCommand, true);
}
}
})
.detach();
}
fn update_slash_commands_from_settings(cx: &mut AppContext) {
fn update_slash_commands_from_settings(cx: &mut App) {
let slash_command_registry = SlashCommandRegistry::global(cx);
let settings = SlashCommandSettings::get_global(cx);
if settings.docs.enabled {
slash_command_registry.register_command(docs_command::DocsSlashCommand, true);
slash_command_registry.register_command(assistant_slash_commands::DocsSlashCommand, true);
} else {
slash_command_registry.unregister_command(docs_command::DocsSlashCommand);
slash_command_registry.unregister_command(assistant_slash_commands::DocsSlashCommand);
}
if settings.cargo_workspace.enabled {
slash_command_registry
.register_command(cargo_workspace_command::CargoWorkspaceSlashCommand, true);
.register_command(assistant_slash_commands::CargoWorkspaceSlashCommand, true);
} else {
slash_command_registry
.unregister_command(cargo_workspace_command::CargoWorkspaceSlashCommand);
}
}
pub fn humanize_token_count(count: usize) -> String {
match count {
0..=999 => count.to_string(),
1000..=9999 => {
let thousands = count / 1000;
let hundreds = (count % 1000 + 50) / 100;
if hundreds == 0 {
format!("{}k", thousands)
} else if hundreds == 10 {
format!("{}k", thousands + 1)
} else {
format!("{}.{}k", thousands, hundreds)
}
}
_ => format!("{}k", (count + 500) / 1000),
.unregister_command(assistant_slash_commands::CargoWorkspaceSlashCommand);
}
}

View File

@@ -0,0 +1,199 @@
use std::sync::Arc;
use collections::HashMap;
use gpui::{canvas, AnyView, App, EventEmitter, FocusHandle, Focusable, Subscription};
use language_model::{LanguageModelProvider, LanguageModelProviderId, LanguageModelRegistry};
use ui::{prelude::*, ElevationIndex};
use workspace::Item;
pub struct ConfigurationView {
focus_handle: FocusHandle,
configuration_views: HashMap<LanguageModelProviderId, AnyView>,
_registry_subscription: Subscription,
}
impl ConfigurationView {
pub fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
let focus_handle = cx.focus_handle();
let registry_subscription = cx.subscribe_in(
&LanguageModelRegistry::global(cx),
window,
|this, _, event: &language_model::Event, window, cx| match event {
language_model::Event::AddedProvider(provider_id) => {
let provider = LanguageModelRegistry::read_global(cx).provider(provider_id);
if let Some(provider) = provider {
this.add_configuration_view(&provider, window, cx);
}
}
language_model::Event::RemovedProvider(provider_id) => {
this.remove_configuration_view(provider_id);
}
_ => {}
},
);
let mut this = Self {
focus_handle,
configuration_views: HashMap::default(),
_registry_subscription: registry_subscription,
};
this.build_configuration_views(window, cx);
this
}
fn build_configuration_views(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let providers = LanguageModelRegistry::read_global(cx).providers();
for provider in providers {
self.add_configuration_view(&provider, window, cx);
}
}
fn remove_configuration_view(&mut self, provider_id: &LanguageModelProviderId) {
self.configuration_views.remove(provider_id);
}
fn add_configuration_view(
&mut self,
provider: &Arc<dyn LanguageModelProvider>,
window: &mut Window,
cx: &mut Context<Self>,
) {
let configuration_view = provider.configuration_view(window, cx);
self.configuration_views
.insert(provider.id(), configuration_view);
}
fn render_provider_view(
&mut self,
provider: &Arc<dyn LanguageModelProvider>,
cx: &mut Context<Self>,
) -> Div {
let provider_id = provider.id().0.clone();
let provider_name = provider.name().0.clone();
let configuration_view = self.configuration_views.get(&provider.id()).cloned();
let open_new_context = cx.listener({
let provider = provider.clone();
move |_, _, _window, cx| {
cx.emit(ConfigurationViewEvent::NewProviderContextEditor(
provider.clone(),
))
}
});
v_flex()
.gap_2()
.child(
h_flex()
.justify_between()
.child(Headline::new(provider_name.clone()).size(HeadlineSize::Small))
.when(provider.is_authenticated(cx), move |this| {
this.child(
h_flex().justify_end().child(
Button::new(
SharedString::from(format!("new-context-{provider_id}")),
"Open New Chat",
)
.icon_position(IconPosition::Start)
.icon(IconName::Plus)
.style(ButtonStyle::Filled)
.layer(ElevationIndex::ModalSurface)
.on_click(open_new_context),
),
)
}),
)
.child(
div()
.p(DynamicSpacing::Base08.rems(cx))
.bg(cx.theme().colors().surface_background)
.border_1()
.border_color(cx.theme().colors().border_variant)
.rounded_md()
.when(configuration_view.is_none(), |this| {
this.child(div().child(Label::new(format!(
"No configuration view for {}",
provider_name
))))
})
.when_some(configuration_view, |this, configuration_view| {
this.child(configuration_view)
}),
)
}
}
impl Render for ConfigurationView {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let providers = LanguageModelRegistry::read_global(cx).providers();
let provider_views = providers
.into_iter()
.map(|provider| self.render_provider_view(&provider, cx))
.collect::<Vec<_>>();
let mut element = v_flex()
.id("assistant-configuration-view")
.track_focus(&self.focus_handle(cx))
.bg(cx.theme().colors().editor_background)
.size_full()
.overflow_y_scroll()
.child(
v_flex()
.p(DynamicSpacing::Base16.rems(cx))
.border_b_1()
.border_color(cx.theme().colors().border)
.gap_1()
.child(Headline::new("Configure your Assistant").size(HeadlineSize::Medium))
.child(
Label::new(
"At least one LLM provider must be configured to use the Assistant.",
)
.color(Color::Muted),
),
)
.child(
v_flex()
.p(DynamicSpacing::Base16.rems(cx))
.mt_1()
.gap_6()
.flex_1()
.children(provider_views),
)
.into_any();
// We use a canvas here to get scrolling to work in the ConfigurationView. It's a workaround
// because we couldn't the element to take up the size of the parent.
canvas(
move |bounds, window, cx| {
element.prepaint_as_root(bounds.origin, bounds.size.into(), window, cx);
element
},
|_, mut element, window, cx| {
element.paint(window, cx);
},
)
.flex_1()
.w_full()
}
}
pub enum ConfigurationViewEvent {
NewProviderContextEditor(Arc<dyn LanguageModelProvider>),
}
impl EventEmitter<ConfigurationViewEvent> for ConfigurationView {}
impl Focusable for ConfigurationView {
fn focus_handle(&self, _: &App) -> FocusHandle {
self.focus_handle.clone()
}
}
impl Item for ConfigurationView {
type Event = ConfigurationViewEvent;
fn tab_content_text(&self, _window: &Window, _cx: &App) -> Option<SharedString> {
Some("Configuration".into())
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,98 +0,0 @@
use crate::assistant_panel::selections_creases;
use anyhow::{anyhow, Result};
use assistant_slash_command::{
ArgumentCompletion, SlashCommand, SlashCommandContent, SlashCommandEvent,
SlashCommandOutputSection, SlashCommandResult,
};
use futures::StreamExt;
use gpui::{AppContext, Task, WeakView};
use language::{BufferSnapshot, CodeLabel, LspAdapterDelegate};
use std::sync::atomic::AtomicBool;
use std::sync::Arc;
use ui::{IconName, SharedString, WindowContext};
use workspace::Workspace;
pub(crate) struct SelectionCommand;
impl SlashCommand for SelectionCommand {
fn name(&self) -> String {
"selection".into()
}
fn label(&self, _cx: &AppContext) -> CodeLabel {
CodeLabel::plain(self.name(), None)
}
fn description(&self) -> String {
"Insert editor selection".into()
}
fn icon(&self) -> IconName {
IconName::Quote
}
fn menu_text(&self) -> String {
self.description()
}
fn requires_argument(&self) -> bool {
false
}
fn accepts_arguments(&self) -> bool {
true
}
fn complete_argument(
self: Arc<Self>,
_arguments: &[String],
_cancel: Arc<AtomicBool>,
_workspace: Option<WeakView<Workspace>>,
_cx: &mut WindowContext,
) -> Task<Result<Vec<ArgumentCompletion>>> {
Task::ready(Err(anyhow!("this command does not require argument")))
}
fn run(
self: Arc<Self>,
_arguments: &[String],
_context_slash_command_output_sections: &[SlashCommandOutputSection<language::Anchor>],
_context_buffer: BufferSnapshot,
workspace: WeakView<Workspace>,
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
cx: &mut WindowContext,
) -> Task<SlashCommandResult> {
let mut events = vec![];
let Some(creases) = workspace
.update(cx, selections_creases)
.unwrap_or_else(|e| {
events.push(Err(e));
None
})
else {
return Task::ready(Err(anyhow!("no active selection")));
};
for (text, title) in creases {
events.push(Ok(SlashCommandEvent::StartSection {
icon: IconName::TextSnippet,
label: SharedString::from(title),
metadata: None,
}));
events.push(Ok(SlashCommandEvent::Content(SlashCommandContent::Text {
text,
run_commands_in_text: false,
})));
events.push(Ok(SlashCommandEvent::EndSection));
events.push(Ok(SlashCommandEvent::Content(SlashCommandContent::Text {
text: "\n".to_string(),
run_commands_in_text: false,
})));
}
let result = futures::stream::iter(events).boxed();
Task::ready(Ok(result))
}
}

View File

@@ -1,5 +1,5 @@
use anyhow::Result;
use gpui::AppContext;
use gpui::App;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use settings::{Settings, SettingsSources};
@@ -36,7 +36,7 @@ impl Settings for SlashCommandSettings {
type FileContent = Self;
fn load(sources: SettingsSources<Self::FileContent>, _cx: &mut AppContext) -> Result<Self> {
fn load(sources: SettingsSources<Self::FileContent>, _cx: &mut App) -> Result<Self> {
SettingsSources::<Self::FileContent>::json_merge_with(
[sources.default]
.into_iter()

View File

@@ -1,9 +1,7 @@
use crate::assistant_settings::AssistantSettings;
use crate::{
humanize_token_count, prompts::PromptBuilder, AssistantPanel, AssistantPanelEvent, RequestType,
DEFAULT_CONTEXT_LINES,
};
use crate::{AssistantPanel, AssistantPanelEvent, DEFAULT_CONTEXT_LINES};
use anyhow::{Context as _, Result};
use assistant_context_editor::{humanize_token_count, RequestType};
use assistant_settings::AssistantSettings;
use client::telemetry::Telemetry;
use collections::{HashMap, VecDeque};
use editor::{
@@ -13,8 +11,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,
App, Context, Entity, EventEmitter, FocusHandle, Focusable, Global, Subscription, Task,
TextStyle, UpdateGlobal, WeakEntity,
};
use language::Buffer;
use language_model::{
@@ -22,6 +20,7 @@ use language_model::{
};
use language_model_selector::{LanguageModelSelector, LanguageModelSelectorPopoverMenu};
use language_models::report_assistant_event;
use prompt_library::PromptBuilder;
use settings::{update_settings_file, Settings};
use std::{
cmp,
@@ -40,7 +39,7 @@ pub fn init(
fs: Arc<dyn Fs>,
prompt_builder: Arc<PromptBuilder>,
telemetry: Arc<Telemetry>,
cx: &mut AppContext,
cx: &mut App,
) {
cx.set_global(TerminalInlineAssistant::new(fs, prompt_builder, telemetry));
}
@@ -87,20 +86,20 @@ impl TerminalInlineAssistant {
pub fn assist(
&mut self,
terminal_view: &View<TerminalView>,
workspace: Option<WeakView<Workspace>>,
assistant_panel: Option<&View<AssistantPanel>>,
terminal_view: &Entity<TerminalView>,
workspace: Option<WeakEntity<Workspace>>,
assistant_panel: Option<&Entity<AssistantPanel>>,
initial_prompt: Option<String>,
cx: &mut WindowContext,
window: &mut Window,
cx: &mut App,
) {
let terminal = terminal_view.read(cx).terminal().clone();
let assist_id = self.next_assist_id.post_inc();
let prompt_buffer =
cx.new_model(|cx| Buffer::local(initial_prompt.unwrap_or_default(), cx));
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_buffer = cx.new(|cx| Buffer::local(initial_prompt.unwrap_or_default(), cx));
let prompt_buffer = cx.new(|cx| MultiBuffer::singleton(prompt_buffer, cx));
let codegen = cx.new(|_| Codegen::new(terminal, self.telemetry.clone()));
let prompt_editor = cx.new_view(|cx| {
let prompt_editor = cx.new(|cx| {
PromptEditor::new(
assist_id,
self.prompt_history.clone(),
@@ -109,6 +108,7 @@ impl TerminalInlineAssistant {
assistant_panel,
workspace.clone(),
self.fs.clone(),
window,
cx,
)
});
@@ -118,7 +118,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 +127,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 App,
) {
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 +155,10 @@ impl TerminalInlineAssistant {
fn handle_prompt_editor_event(
&mut self,
prompt_editor: View<PromptEditor>,
prompt_editor: Entity<PromptEditor>,
event: &PromptEditorEvent,
cx: &mut WindowContext,
window: &mut Window,
cx: &mut App,
) {
let assist_id = prompt_editor.read(cx).id;
match event {
@@ -162,21 +169,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 App) {
let assist = if let Some(assist) = self.assists.get_mut(&assist_id) {
assist
} else {
@@ -214,7 +221,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 App) {
let assist = if let Some(assist) = self.assists.get_mut(&assist_id) {
assist
} else {
@@ -227,7 +234,7 @@ impl TerminalInlineAssistant {
fn request_for_inline_assist(
&self,
assist_id: TerminalInlineAssistId,
cx: &mut WindowContext,
cx: &mut App,
) -> Result<LanguageModelRequest> {
let assist = self.assists.get(&assist_id).context("invalid assist")?;
@@ -235,7 +242,7 @@ impl TerminalInlineAssistant {
let (latest_output, working_directory) = assist
.terminal
.update(cx, |terminal, cx| {
let terminal = terminal.model().read(cx);
let terminal = terminal.entity().read(cx);
let latest_output = terminal.last_n_non_empty_lines(DEFAULT_CONTEXT_LINES);
let working_directory = terminal
.working_directory()
@@ -297,16 +304,17 @@ impl TerminalInlineAssistant {
assist_id: TerminalInlineAssistId,
undo: bool,
execute: bool,
cx: &mut WindowContext,
window: &mut Window,
cx: &mut App,
) {
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 +357,8 @@ impl TerminalInlineAssistant {
fn dismiss_assist(
&mut self,
assist_id: TerminalInlineAssistId,
cx: &mut WindowContext,
window: &mut Window,
cx: &mut App,
) -> bool {
let Some(assist) = self.assists.get_mut(&assist_id) else {
return false;
@@ -362,7 +371,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 +380,8 @@ impl TerminalInlineAssistant {
&mut self,
assist_id: TerminalInlineAssistId,
height: u8,
cx: &mut WindowContext,
window: &mut Window,
cx: &mut App,
) {
if let Some(assist) = self.assists.get_mut(&assist_id) {
if let Some(prompt_editor) = assist.prompt_editor.as_ref().cloned() {
@@ -383,7 +393,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 +402,10 @@ impl TerminalInlineAssistant {
}
struct TerminalInlineAssist {
terminal: WeakView<TerminalView>,
prompt_editor: Option<View<PromptEditor>>,
codegen: Model<Codegen>,
workspace: Option<WeakView<Workspace>>,
terminal: WeakEntity<TerminalView>,
prompt_editor: Option<Entity<PromptEditor>>,
codegen: Entity<Codegen>,
workspace: Option<WeakEntity<Workspace>>,
include_context: bool,
_subscriptions: Vec<Subscription>,
}
@@ -403,11 +413,12 @@ struct TerminalInlineAssist {
impl TerminalInlineAssist {
pub fn new(
assist_id: TerminalInlineAssistId,
terminal: &View<TerminalView>,
terminal: &Entity<TerminalView>,
include_context: bool,
prompt_editor: View<PromptEditor>,
workspace: Option<WeakView<Workspace>>,
cx: &mut WindowContext,
prompt_editor: Entity<PromptEditor>,
workspace: Option<WeakEntity<Workspace>>,
window: &mut Window,
cx: &mut App,
) -> Self {
let codegen = prompt_editor.read(cx).codegen.clone();
Self {
@@ -417,12 +428,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 +466,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,25 +488,25 @@ enum PromptEditorEvent {
struct PromptEditor {
id: TerminalInlineAssistId,
height_in_lines: u8,
editor: View<Editor>,
language_model_selector: View<LanguageModelSelector>,
editor: Entity<Editor>,
language_model_selector: Entity<LanguageModelSelector>,
edited_since_done: bool,
prompt_history: VecDeque<String>,
prompt_history_ix: Option<usize>,
pending_prompt: String,
codegen: Model<Codegen>,
codegen: Entity<Codegen>,
_codegen_subscription: Subscription,
editor_subscriptions: Vec<Subscription>,
pending_token_count: Task<Result<()>>,
token_count: Option<usize>,
_token_count_subscriptions: Vec<Subscription>,
workspace: Option<WeakView<Workspace>>,
workspace: Option<WeakEntity<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 Context<Self>) -> impl IntoElement {
let status = &self.codegen.read(cx).status;
let buttons = match status {
CodegenStatus::Idle => {
@@ -503,16 +514,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 +536,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 +561,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 +575,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 +594,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 +647,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 +658,7 @@ impl Render for PromptEditor {
),
None,
"Change Model",
window,
cx,
)
}),
@@ -641,7 +669,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,8 +692,8 @@ impl Render for PromptEditor {
}
}
impl FocusableView for PromptEditor {
fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
impl Focusable for PromptEditor {
fn focus_handle(&self, cx: &App) -> FocusHandle {
self.editor.focus_handle(cx)
}
}
@@ -677,14 +705,15 @@ impl PromptEditor {
fn new(
id: TerminalInlineAssistId,
prompt_history: VecDeque<String>,
prompt_buffer: Model<MultiBuffer>,
codegen: Model<Codegen>,
assistant_panel: Option<&View<AssistantPanel>>,
workspace: Option<WeakView<Workspace>>,
prompt_buffer: Entity<MultiBuffer>,
codegen: Entity<Codegen>,
assistant_panel: Option<&Entity<AssistantPanel>>,
workspace: Option<WeakEntity<Workspace>>,
fs: Arc<dyn Fs>,
cx: &mut ViewContext<Self>,
window: &mut Window,
cx: &mut Context<Self>,
) -> Self {
let prompt_editor = cx.new_view(|cx| {
let prompt_editor = cx.new(|cx| {
let mut editor = Editor::new(
EditorMode::AutoHeight {
max_lines: Self::MAX_LINES as usize,
@@ -692,24 +721,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(|cx| {
let fs = fs.clone();
LanguageModelSelector::new(
move |model, cx| {
@@ -719,6 +752,7 @@ impl PromptEditor {
move |settings, _| settings.set_model(model.clone()),
);
},
window,
cx,
)
}),
@@ -726,7 +760,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 +774,15 @@ impl PromptEditor {
this
}
fn placeholder_text(cx: &WindowContext) -> String {
let context_keybinding = text_for_action(&crate::ToggleFocus, cx)
fn placeholder_text(window: &Window) -> String {
let context_keybinding = text_for_action(&zed_actions::assistant::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 Context<Self>) {
self.editor_subscriptions.clear();
self.editor_subscriptions
.push(cx.observe(&self.editor, Self::handle_prompt_editor_changed));
@@ -756,11 +790,11 @@ impl PromptEditor {
.push(cx.subscribe(&self.editor, Self::handle_prompt_editor_events));
}
fn prompt(&self, cx: &AppContext) -> String {
fn prompt(&self, cx: &App) -> String {
self.editor.read(cx).text(cx)
}
fn count_lines(&mut self, cx: &mut ViewContext<Self>) {
fn count_lines(&mut self, cx: &mut Context<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 +812,16 @@ impl PromptEditor {
fn handle_assistant_panel_event(
&mut self,
_: View<AssistantPanel>,
_: &Entity<AssistantPanel>,
event: &AssistantPanelEvent,
cx: &mut ViewContext<Self>,
_: &mut Window,
cx: &mut Context<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 Context<Self>) {
let assist_id = self.id;
let Some(model) = LanguageModelRegistry::read_global(cx).active_model() else {
return;
@@ -806,15 +841,15 @@ impl PromptEditor {
})
}
fn handle_prompt_editor_changed(&mut self, _: View<Editor>, cx: &mut ViewContext<Self>) {
fn handle_prompt_editor_changed(&mut self, _: Entity<Editor>, cx: &mut Context<Self>) {
self.count_lines(cx);
}
fn handle_prompt_editor_events(
&mut self,
_: View<Editor>,
_: Entity<Editor>,
event: &EditorEvent,
cx: &mut ViewContext<Self>,
cx: &mut Context<Self>,
) {
match event {
EditorEvent::Edited { .. } => {
@@ -837,7 +872,12 @@ impl PromptEditor {
}
}
fn handle_codegen_changed(&mut self, _: Model<Codegen>, cx: &mut ViewContext<Self>) {
fn handle_codegen_changed(
&mut self,
_: Entity<Codegen>,
_: &mut Window,
cx: &mut Context<Self>,
) {
match &self.codegen.read(cx).status {
CodegenStatus::Idle => {
self.editor
@@ -855,7 +895,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 Context<Self>) {
match &self.codegen.read(cx).status {
CodegenStatus::Idle | CodegenStatus::Done | CodegenStatus::Error(_) => {
cx.emit(PromptEditorEvent::CancelRequested);
@@ -866,7 +906,7 @@ impl PromptEditor {
}
}
fn confirm(&mut self, _: &menu::Confirm, cx: &mut ViewContext<Self>) {
fn confirm(&mut self, _: &menu::Confirm, _: &mut Window, cx: &mut Context<Self>) {
match &self.codegen.read(cx).status {
CodegenStatus::Idle => {
if !self.editor.read(cx).text(cx).trim().is_empty() {
@@ -889,53 +929,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 Context<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 Context<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 Context<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 Context<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 +1010,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 Context<Self>) -> impl IntoElement {
let settings = ThemeSettings::get_global(cx);
let text_style = TextStyle {
color: if self.editor.read(cx).read_only(cx) {
@@ -1030,27 +1076,27 @@ const CLEAR_INPUT: &str = "\x15";
const CARRIAGE_RETURN: &str = "\x0d";
struct TerminalTransaction {
terminal: Model<Terminal>,
terminal: Entity<Terminal>,
}
impl TerminalTransaction {
pub fn start(terminal: Model<Terminal>) -> Self {
pub fn start(terminal: Entity<Terminal>) -> Self {
Self { terminal }
}
pub fn push(&mut self, hunk: String, cx: &mut AppContext) {
pub fn push(&mut self, hunk: String, cx: &mut App) {
// 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) {
pub fn undo(&self, cx: &mut App) {
self.terminal
.update(cx, |terminal, _| terminal.input(CLEAR_INPUT.to_string()));
}
pub fn complete(&self, cx: &mut AppContext) {
pub fn complete(&self, cx: &mut App) {
self.terminal.update(cx, |terminal, _| {
terminal.input(CARRIAGE_RETURN.to_string())
});
@@ -1064,14 +1110,14 @@ impl TerminalTransaction {
pub struct Codegen {
status: CodegenStatus,
telemetry: Option<Arc<Telemetry>>,
terminal: Model<Terminal>,
terminal: Entity<Terminal>,
generation: Task<()>,
message_id: Option<String>,
transaction: Option<TerminalTransaction>,
}
impl Codegen {
pub fn new(terminal: Model<Terminal>, telemetry: Option<Arc<Telemetry>>) -> Self {
pub fn new(terminal: Entity<Terminal>, telemetry: Option<Arc<Telemetry>>) -> Self {
Self {
terminal,
telemetry,
@@ -1082,7 +1128,7 @@ impl Codegen {
}
}
pub fn start(&mut self, prompt: LanguageModelRequest, cx: &mut ModelContext<Self>) {
pub fn start(&mut self, prompt: LanguageModelRequest, cx: &mut Context<Self>) {
let Some(model) = LanguageModelRegistry::read_global(cx).active_model() else {
return;
};
@@ -1182,20 +1228,20 @@ impl Codegen {
cx.notify();
}
pub fn stop(&mut self, cx: &mut ModelContext<Self>) {
pub fn stop(&mut self, cx: &mut Context<Self>) {
self.status = CodegenStatus::Done;
self.generation = Task::ready(());
cx.emit(CodegenEvent::Finished);
cx.notify();
}
pub fn complete(&mut self, cx: &mut ModelContext<Self>) {
pub fn complete(&mut self, cx: &mut Context<Self>) {
if let Some(transaction) = self.transaction.take() {
transaction.complete(cx);
}
}
pub fn undo(&mut self, cx: &mut ModelContext<Self>) {
pub fn undo(&mut self, cx: &mut Context<Self>) {
if let Some(transaction) = self.transaction.take() {
transaction.undo(cx);
}

View File

@@ -1,8 +1,8 @@
[package]
name = "assistant2"
version = "0.1.0"
edition = "2021"
publish = false
edition.workspace = true
publish.workspace = true
license = "GPL-3.0-or-later"
[lints]
@@ -12,27 +12,37 @@ workspace = true
path = "src/assistant.rs"
doctest = false
[features]
test-support = [
"gpui/test-support",
"language/test-support",
]
[dependencies]
anthropic = { workspace = true, features = ["schemars"] }
anyhow.workspace = true
assets.workspace = true
assistant_context_editor.workspace = true
assistant_settings.workspace = true
assistant_slash_command.workspace = true
assistant_tool.workspace = true
async-watch.workspace = true
client.workspace = true
chrono.workspace = true
client.workspace = true
clock.workspace = true
collections.workspace = true
command_palette_hooks.workspace = true
context_server.workspace = true
db.workspace = true
editor.workspace = true
feature_flags.workspace = true
file_icons.workspace = true
fs.workspace = true
futures.workspace = true
fuzzy.workspace = true
gpui.workspace = true
handlebars.workspace = true
heed.workspace = true
html_to_markdown.workspace = true
http_client.workspace = true
itertools.workspace = true
language.workspace = true
language_model.workspace = true
language_model_selector.workspace = true
@@ -42,31 +52,27 @@ lsp.workspace = true
markdown.workspace = true
menu.workspace = true
multi_buffer.workspace = true
ollama = { workspace = true, features = ["schemars"] }
open_ai = { workspace = true, features = ["schemars"] }
ordered-float.workspace = true
paths.workspace = true
parking_lot.workspace = true
paths.workspace = true
picker.workspace = true
project.workspace = true
prompt_library.workspace = true
proto.workspace = true
rope.workspace = true
schemars.workspace = true
serde.workspace = true
serde_json.workspace = true
serde_json_lenient.workspace = true
settings.workspace = true
similar.workspace = true
smol.workspace = true
streaming_diff.workspace = true
telemetry_events.workspace = true
terminal.workspace = true
terminal_view.workspace = true
text.workspace = true
terminal.workspace = true
theme.workspace = true
time.workspace = true
time_format.workspace = true
ui.workspace = true
unindent.workspace = true
util.workspace = true
uuid.workspace = true
workspace.workspace = true

View File

@@ -3,9 +3,9 @@ use std::sync::Arc;
use assistant_tool::ToolWorkingSet;
use collections::HashMap;
use gpui::{
list, AbsoluteLength, AnyElement, AppContext, DefiniteLength, EdgesRefinement, Empty, Length,
ListAlignment, ListOffset, ListState, Model, StyleRefinement, Subscription,
TextStyleRefinement, UnderlineStyle, View, WeakView,
list, AbsoluteLength, AnyElement, App, DefiniteLength, EdgesRefinement, Empty, Entity, Length,
ListAlignment, ListOffset, ListState, StyleRefinement, Subscription, TextStyleRefinement,
UnderlineStyle, WeakEntity,
};
use language::LanguageRegistry;
use language_model::Role;
@@ -16,43 +16,48 @@ use ui::prelude::*;
use workspace::Workspace;
use crate::thread::{MessageId, Thread, ThreadError, ThreadEvent};
use crate::thread_store::ThreadStore;
use crate::ui::ContextPill;
pub struct ActiveThread {
workspace: WeakView<Workspace>,
workspace: WeakEntity<Workspace>,
language_registry: Arc<LanguageRegistry>,
tools: Arc<ToolWorkingSet>,
pub(crate) thread: Model<Thread>,
thread_store: Entity<ThreadStore>,
thread: Entity<Thread>,
messages: Vec<MessageId>,
list_state: ListState,
rendered_messages_by_id: HashMap<MessageId, View<Markdown>>,
rendered_messages_by_id: HashMap<MessageId, Entity<Markdown>>,
last_error: Option<ThreadError>,
_subscriptions: Vec<Subscription>,
}
impl ActiveThread {
pub fn new(
thread: Model<Thread>,
workspace: WeakView<Workspace>,
thread: Entity<Thread>,
thread_store: Entity<ThreadStore>,
workspace: WeakEntity<Workspace>,
language_registry: Arc<LanguageRegistry>,
tools: Arc<ToolWorkingSet>,
cx: &mut ViewContext<Self>,
window: &mut Window,
cx: &mut Context<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 {
workspace,
language_registry,
tools,
thread_store,
thread: thread.clone(),
messages: Vec::new(),
rendered_messages_by_id: HashMap::default(),
list_state: ListState::new(0, ListAlignment::Bottom, px(1024.), {
let this = cx.view().downgrade();
move |ix, cx: &mut WindowContext| {
let this = cx.entity().downgrade();
move |ix, _: &mut Window, cx: &mut App| {
this.update(cx, |this, cx| this.render_message(ix, cx))
.unwrap()
}
@@ -62,20 +67,34 @@ 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
}
pub fn thread(&self) -> &Entity<Thread> {
&self.thread
}
pub fn is_empty(&self) -> bool {
self.messages.is_empty()
}
pub fn summary(&self, cx: &AppContext) -> Option<SharedString> {
pub fn summary(&self, cx: &App) -> Option<SharedString> {
self.thread.read(cx).summary()
}
pub fn summary_or_default(&self, cx: &App) -> SharedString {
self.thread.read(cx).summary_or_default()
}
pub fn cancel_last_completion(&mut self, cx: &mut App) -> bool {
self.last_error.take();
self.thread
.update(cx, |thread, _cx| thread.cancel_last_completion())
}
pub fn last_error(&self) -> Option<ThreadError> {
self.last_error.clone()
}
@@ -84,7 +103,13 @@ 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 Context<Self>,
) {
let old_len = self.messages.len();
self.messages.push(*id);
self.list_state.splice(old_len..old_len, 1);
@@ -93,7 +118,7 @@ impl ActiveThread {
let colors = cx.theme().colors();
let ui_font_size = TextSize::Default.rems(cx);
let buffer_font_size = TextSize::Small.rems(cx);
let mut text_style = cx.text_style();
let mut text_style = window.text_style();
text_style.refine(&TextStyleRefinement {
font_family: Some(theme_settings.ui_font.family.clone()),
@@ -108,10 +133,10 @@ impl ActiveThread {
selection_background_color: cx.theme().players().local().selection,
code_block: StyleRefinement {
margin: EdgesRefinement {
top: Some(Length::Definite(rems(1.0).into())),
top: Some(Length::Definite(rems(0.).into())),
left: Some(Length::Definite(rems(0.).into())),
right: Some(Length::Definite(rems(0.).into())),
bottom: Some(Length::Definite(rems(1.).into())),
bottom: Some(Length::Definite(rems(0.5).into())),
},
padding: EdgesRefinement {
top: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(Pixels(8.)))),
@@ -119,10 +144,10 @@ impl ActiveThread {
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)),
background: Some(colors.editor_background.into()),
border_color: Some(colors.border_variant),
border_widths: EdgesRefinement {
top: Some(AbsoluteLength::Pixels(Pixels(1.0))),
top: Some(AbsoluteLength::Pixels(Pixels(1.))),
left: Some(AbsoluteLength::Pixels(Pixels(1.))),
right: Some(AbsoluteLength::Pixels(Pixels(1.))),
bottom: Some(AbsoluteLength::Pixels(Pixels(1.))),
@@ -152,12 +177,13 @@ impl ActiveThread {
..Default::default()
};
let markdown = cx.new_view(|cx| {
let markdown = cx.new(|cx| {
Markdown::new(
text,
markdown_style,
Some(self.language_registry.clone()),
None,
window,
cx,
)
});
@@ -170,20 +196,26 @@ impl ActiveThread {
fn handle_thread_event(
&mut self,
_: Model<Thread>,
_: &Entity<Thread>,
event: &ThreadEvent,
cx: &mut ViewContext<Self>,
window: &mut Window,
cx: &mut Context<Self>,
) {
match event {
ThreadEvent::ShowError(error) => {
self.last_error = Some(error.clone());
}
ThreadEvent::StreamedCompletion => {}
ThreadEvent::SummaryChanged => {}
ThreadEvent::StreamedCompletion | ThreadEvent::SummaryChanged => {
self.thread_store
.update(cx, |thread_store, cx| {
thread_store.save_thread(&self.thread, cx)
})
.detach_and_log_err(cx);
}
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);
});
}
}
@@ -194,9 +226,15 @@ 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);
}
self.thread_store
.update(cx, |thread_store, cx| {
thread_store.save_thread(&self.thread, cx)
})
.detach_and_log_err(cx);
cx.notify();
}
ThreadEvent::UsePendingTools => {
@@ -211,7 +249,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(
@@ -228,7 +266,7 @@ impl ActiveThread {
}
}
fn render_message(&self, ix: usize, cx: &mut ViewContext<Self>) -> AnyElement {
fn render_message(&self, ix: usize, cx: &mut Context<Self>) -> AnyElement {
let message_id = self.messages[ix];
let Some(message) = self.thread.read(cx).message(message_id) else {
return Empty.into_any();
@@ -241,63 +279,78 @@ impl ActiveThread {
let context = self.thread.read(cx).context_for_message(message_id);
let colors = cx.theme().colors();
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),
};
let message_content = v_flex()
.child(div().p_2p5().text_ui(cx).child(markdown.clone()))
.when_some(context, |parent, context| {
if !context.is_empty() {
parent.child(
h_flex().flex_wrap().gap_1().px_1p5().pb_1p5().children(
context
.into_iter()
.map(|context| ContextPill::added(context, false, false, None)),
),
)
} else {
parent
}
});
div()
.id(("message-container", ix))
.py_1()
.px_2()
.child(
let styled_message = match message.role {
Role::User => v_flex()
.id(("message-container", ix))
.py_1()
.px_2p5()
.child(
v_flex()
.bg(colors.editor_background)
.rounded_lg()
.border_1()
.border_color(colors.border)
.shadow_sm()
.child(
h_flex()
.py_1()
.px_2()
.bg(colors.editor_foreground.opacity(0.05))
.border_b_1()
.border_color(colors.border)
.justify_between()
.rounded_t(px(6.))
.child(
h_flex()
.gap_1p5()
.child(
Icon::new(IconName::PersonCircle)
.size(IconSize::XSmall)
.color(Color::Muted),
)
.child(
Label::new("You")
.size(LabelSize::Small)
.color(Color::Muted),
),
),
)
.child(message_content),
),
Role::Assistant => div().id(("message-container", ix)).child(message_content),
Role::System => div().id(("message-container", ix)).py_1().px_2().child(
v_flex()
.border_1()
.border_color(colors.border_variant)
.bg(colors.editor_background)
.rounded_md()
.child(
h_flex()
.py_1p5()
.px_2p5()
.border_b_1()
.border_color(colors.border_variant)
.justify_between()
.child(
h_flex()
.gap_1p5()
.child(
Icon::new(role_icon)
.size(IconSize::XSmall)
.color(role_color),
)
.child(
Label::new(role_name)
.size(LabelSize::XSmall)
.color(role_color),
),
),
)
.child(div().p_2p5().text_ui(cx).child(markdown.clone()))
.when_some(context, |parent, context| {
if !context.is_empty() {
parent.child(h_flex().flex_wrap().gap_1().px_1p5().pb_1p5().children(
context.iter().map(|context| {
ContextPill::new_added(context.clone(), false, None)
}),
))
} else {
parent
}
}),
)
.into_any()
.child(message_content),
),
};
styled_message.into_any()
}
}
impl Render for ActiveThread {
fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
list(self.list_state.clone()).flex_1().py_1()
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
v_flex()
.size_full()
.pt_1p5()
.child(list(self.list_state.clone()).flex_grow())
}
}

View File

@@ -1,7 +1,7 @@
mod active_thread;
mod assistant_configuration;
mod assistant_model_selector;
mod assistant_panel;
mod assistant_settings;
mod buffer_codegen;
mod context;
mod context_picker;
@@ -10,8 +10,6 @@ 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;
@@ -21,51 +19,55 @@ mod ui;
use std::sync::Arc;
use assistant_settings::AssistantSettings;
use client::Client;
use command_palette_hooks::CommandPaletteFilter;
use feature_flags::{Assistant2FeatureFlag, FeatureFlagAppExt};
use fs::Fs;
use gpui::{actions, AppContext};
use prompts::PromptLoadingParams;
use gpui::{actions, App};
use prompt_library::PromptBuilder;
use settings::Settings as _;
use util::ResultExt;
pub use crate::assistant_panel::AssistantPanel;
use crate::assistant_settings::AssistantSettings;
pub use crate::assistant_panel::{AssistantPanel, ConcreteAssistantPanelDelegate};
pub use crate::inline_assistant::InlineAssistant;
actions!(
assistant2,
[
ToggleFocus,
NewThread,
NewPromptEditor,
ToggleContextPicker,
ToggleModelSelector,
RemoveAllContext,
OpenHistory,
OpenPromptEditorHistory,
OpenConfiguration,
RemoveSelectedThread,
Chat,
ChatMode,
CycleNextInlineAssist,
CyclePreviousInlineAssist
CyclePreviousInlineAssist,
FocusUp,
FocusDown,
FocusLeft,
FocusRight,
RemoveFocusedContext,
AcceptSuggestedContext
]
);
const NAMESPACE: &str = "assistant2";
/// Initializes the `assistant2` crate.
pub fn init(fs: Arc<dyn Fs>, client: Arc<Client>, stdout_is_a_pty: bool, cx: &mut AppContext) {
pub fn init(
fs: Arc<dyn Fs>,
client: Arc<Client>,
prompt_builder: Arc<PromptBuilder>,
cx: &mut App,
) {
AssistantSettings::register(cx);
assistant_panel::init(cx);
let prompt_builder = prompts::PromptBuilder::new(Some(PromptLoadingParams {
fs: fs.clone(),
repo_path: stdout_is_a_pty
.then(|| std::env::current_dir().log_err())
.flatten(),
cx,
}))
.log_err()
.map(Arc::new)
.unwrap_or_else(|| Arc::new(prompts::PromptBuilder::new(None).unwrap()));
inline_assistant::init(
fs.clone(),
prompt_builder.clone(),
@@ -82,7 +84,7 @@ pub fn init(fs: Arc<dyn Fs>, client: Arc<Client>, stdout_is_a_pty: bool, cx: &mu
feature_gate_assistant2_actions(cx);
}
fn feature_gate_assistant2_actions(cx: &mut AppContext) {
fn feature_gate_assistant2_actions(cx: &mut App) {
CommandPaletteFilter::update_global(cx, |filter, _cx| {
filter.hide_namespace(NAMESPACE);
});

View File

@@ -0,0 +1,173 @@
use std::sync::Arc;
use collections::HashMap;
use gpui::{AnyView, App, EventEmitter, FocusHandle, Focusable, Subscription};
use language_model::{LanguageModelProvider, LanguageModelProviderId, LanguageModelRegistry};
use ui::{prelude::*, ElevationIndex};
use zed_actions::assistant::DeployPromptLibrary;
pub struct AssistantConfiguration {
focus_handle: FocusHandle,
configuration_views_by_provider: HashMap<LanguageModelProviderId, AnyView>,
_registry_subscription: Subscription,
}
impl AssistantConfiguration {
pub fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
let focus_handle = cx.focus_handle();
let registry_subscription = cx.subscribe_in(
&LanguageModelRegistry::global(cx),
window,
|this, _, event: &language_model::Event, window, cx| match event {
language_model::Event::AddedProvider(provider_id) => {
let provider = LanguageModelRegistry::read_global(cx).provider(provider_id);
if let Some(provider) = provider {
this.add_provider_configuration_view(&provider, window, cx);
}
}
language_model::Event::RemovedProvider(provider_id) => {
this.remove_provider_configuration_view(provider_id);
}
_ => {}
},
);
let mut this = Self {
focus_handle,
configuration_views_by_provider: HashMap::default(),
_registry_subscription: registry_subscription,
};
this.build_provider_configuration_views(window, cx);
this
}
fn build_provider_configuration_views(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let providers = LanguageModelRegistry::read_global(cx).providers();
for provider in providers {
self.add_provider_configuration_view(&provider, window, cx);
}
}
fn remove_provider_configuration_view(&mut self, provider_id: &LanguageModelProviderId) {
self.configuration_views_by_provider.remove(provider_id);
}
fn add_provider_configuration_view(
&mut self,
provider: &Arc<dyn LanguageModelProvider>,
window: &mut Window,
cx: &mut Context<Self>,
) {
let configuration_view = provider.configuration_view(window, cx);
self.configuration_views_by_provider
.insert(provider.id(), configuration_view);
}
}
impl Focusable for AssistantConfiguration {
fn focus_handle(&self, _: &App) -> FocusHandle {
self.focus_handle.clone()
}
}
pub enum AssistantConfigurationEvent {
NewThread(Arc<dyn LanguageModelProvider>),
}
impl EventEmitter<AssistantConfigurationEvent> for AssistantConfiguration {}
impl AssistantConfiguration {
fn render_provider_configuration(
&mut self,
provider: &Arc<dyn LanguageModelProvider>,
cx: &mut Context<Self>,
) -> impl IntoElement {
let provider_id = provider.id().0.clone();
let provider_name = provider.name().0.clone();
let configuration_view = self
.configuration_views_by_provider
.get(&provider.id())
.cloned();
v_flex()
.gap_2()
.child(
h_flex()
.justify_between()
.child(Headline::new(provider_name.clone()).size(HeadlineSize::Small))
.when(provider.is_authenticated(cx), |parent| {
parent.child(
h_flex().justify_end().child(
Button::new(
SharedString::from(format!("new-thread-{provider_id}")),
"Open New Thread",
)
.icon_position(IconPosition::Start)
.icon(IconName::Plus)
.style(ButtonStyle::Filled)
.layer(ElevationIndex::ModalSurface)
.on_click(cx.listener({
let provider = provider.clone();
move |_this, _event, _window, cx| {
cx.emit(AssistantConfigurationEvent::NewThread(
provider.clone(),
))
}
})),
),
)
}),
)
.child(
div()
.p(DynamicSpacing::Base08.rems(cx))
.bg(cx.theme().colors().surface_background)
.border_1()
.border_color(cx.theme().colors().border_variant)
.rounded_md()
.map(|parent| match configuration_view {
Some(configuration_view) => parent.child(configuration_view),
None => parent.child(div().child(Label::new(format!(
"No configuration view for {provider_name}",
)))),
}),
)
}
}
impl Render for AssistantConfiguration {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let providers = LanguageModelRegistry::read_global(cx).providers();
v_flex()
.id("assistant-configuration")
.track_focus(&self.focus_handle(cx))
.bg(cx.theme().colors().editor_background)
.size_full()
.overflow_y_scroll()
.child(
h_flex().p(DynamicSpacing::Base16.rems(cx)).child(
Button::new("open-prompt-library", "Open Prompt Library")
.style(ButtonStyle::Filled)
.full_width()
.icon(IconName::Book)
.icon_size(IconSize::Small)
.icon_position(IconPosition::Start)
.on_click(|_event, _window, cx| cx.dispatch_action(&DeployPromptLibrary)),
),
)
.child(
v_flex()
.p(DynamicSpacing::Base16.rems(cx))
.mt_1()
.gap_6()
.flex_1()
.children(
providers
.into_iter()
.map(|provider| self.render_provider_configuration(&provider, cx)),
),
)
}
}

View File

@@ -1,26 +1,30 @@
use assistant_settings::AssistantSettings;
use fs::Fs;
use gpui::View;
use gpui::{Entity, FocusHandle, SharedString};
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};
use crate::ToggleModelSelector;
pub struct AssistantModelSelector {
selector: View<LanguageModelSelector>,
selector: Entity<LanguageModelSelector>,
menu_handle: PopoverMenuHandle<LanguageModelSelector>,
focus_handle: FocusHandle,
}
impl AssistantModelSelector {
pub(crate) fn new(
fs: Arc<dyn Fs>,
menu_handle: PopoverMenuHandle<LanguageModelSelector>,
cx: &mut WindowContext,
focus_handle: FocusHandle,
window: &mut Window,
cx: &mut App,
) -> Self {
Self {
selector: cx.new_view(|cx| {
selector: cx.new(|cx| {
let fs = fs.clone();
LanguageModelSelector::new(
move |model, cx| {
@@ -30,18 +34,24 @@ impl AssistantModelSelector {
move |settings, _cx| settings.set_model(model.clone()),
);
},
window,
cx,
)
}),
menu_handle,
focus_handle,
}
}
}
impl Render for AssistantModelSelector {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let active_model = LanguageModelRegistry::read_global(cx).active_model();
let focus_handle = self.selector.focus_handle(cx).clone();
let focus_handle = self.focus_handle.clone();
let model_name = match active_model {
Some(model) => model.name().0,
_ => SharedString::from("No model selected"),
};
LanguageModelSelectorPopoverMenu::new(
self.selector.clone(),
@@ -49,26 +59,15 @@ impl Render for AssistantModelSelector {
.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(),
}),
div().max_w_32().child(
Label::new(model_name)
.size(LabelSize::Small)
.color(Color::Muted)
.text_ellipsis()
.into_any_element(),
),
)
.child(
Icon::new(IconName::ChevronDown)
@@ -76,8 +75,14 @@ impl Render for AssistantModelSelector {
.size(IconSize::XSmall),
),
)
.tooltip(move |cx| {
Tooltip::for_action_in("Change Model", &ToggleModelSelector, &focus_handle, cx)
.tooltip(move |window, cx| {
Tooltip::for_action_in(
"Change Model",
&ToggleModelSelector,
&focus_handle,
window,
cx,
)
}),
)
.with_handle(self.menu_handle.clone())

File diff suppressed because it is too large Load Diff

View File

@@ -1,501 +0,0 @@
use std::sync::Arc;
use ::open_ai::Model as OpenAiModel;
use anthropic::Model as AnthropicModel;
use gpui::Pixels;
use language_model::{CloudModel, LanguageModel};
use ollama::Model as OllamaModel;
use schemars::{schema::Schema, JsonSchema};
use serde::{Deserialize, Serialize};
use settings::{Settings, SettingsSources};
#[derive(Copy, Clone, Default, Debug, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum AssistantDockPosition {
Left,
#[default]
Right,
Bottom,
}
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
#[serde(tag = "name", rename_all = "snake_case")]
pub enum AssistantProviderContentV1 {
#[serde(rename = "zed.dev")]
ZedDotDev { default_model: Option<CloudModel> },
#[serde(rename = "openai")]
OpenAi {
default_model: Option<OpenAiModel>,
api_url: Option<String>,
available_models: Option<Vec<OpenAiModel>>,
},
#[serde(rename = "anthropic")]
Anthropic {
default_model: Option<AnthropicModel>,
api_url: Option<String>,
},
#[serde(rename = "ollama")]
Ollama {
default_model: Option<OllamaModel>,
api_url: Option<String>,
},
}
#[derive(Debug, Default)]
pub struct AssistantSettings {
pub enabled: bool,
pub button: bool,
pub dock: AssistantDockPosition,
pub default_width: Pixels,
pub default_height: Pixels,
pub default_model: LanguageModelSelection,
pub inline_alternatives: Vec<LanguageModelSelection>,
pub using_outdated_settings_version: bool,
pub enable_experimental_live_diffs: bool,
}
/// Assistant panel settings
#[derive(Clone, Serialize, Deserialize, Debug)]
#[serde(untagged)]
pub enum AssistantSettingsContent {
Versioned(VersionedAssistantSettingsContent),
Legacy(LegacyAssistantSettingsContent),
}
impl JsonSchema for AssistantSettingsContent {
fn schema_name() -> String {
VersionedAssistantSettingsContent::schema_name()
}
fn json_schema(gen: &mut schemars::gen::SchemaGenerator) -> Schema {
VersionedAssistantSettingsContent::json_schema(gen)
}
fn is_referenceable() -> bool {
VersionedAssistantSettingsContent::is_referenceable()
}
}
impl Default for AssistantSettingsContent {
fn default() -> Self {
Self::Versioned(VersionedAssistantSettingsContent::default())
}
}
impl AssistantSettingsContent {
pub fn is_version_outdated(&self) -> bool {
match self {
AssistantSettingsContent::Versioned(settings) => match settings {
VersionedAssistantSettingsContent::V1(_) => true,
VersionedAssistantSettingsContent::V2(_) => false,
},
AssistantSettingsContent::Legacy(_) => true,
}
}
fn upgrade(&self) -> AssistantSettingsContentV2 {
match self {
AssistantSettingsContent::Versioned(settings) => match settings {
VersionedAssistantSettingsContent::V1(settings) => AssistantSettingsContentV2 {
enabled: settings.enabled,
button: settings.button,
dock: settings.dock,
default_width: settings.default_width,
default_height: settings.default_width,
default_model: settings
.provider
.clone()
.and_then(|provider| match provider {
AssistantProviderContentV1::ZedDotDev { default_model } => {
default_model.map(|model| LanguageModelSelection {
provider: "zed.dev".to_string(),
model: model.id().to_string(),
})
}
AssistantProviderContentV1::OpenAi { default_model, .. } => {
default_model.map(|model| LanguageModelSelection {
provider: "openai".to_string(),
model: model.id().to_string(),
})
}
AssistantProviderContentV1::Anthropic { default_model, .. } => {
default_model.map(|model| LanguageModelSelection {
provider: "anthropic".to_string(),
model: model.id().to_string(),
})
}
AssistantProviderContentV1::Ollama { default_model, .. } => {
default_model.map(|model| LanguageModelSelection {
provider: "ollama".to_string(),
model: model.id().to_string(),
})
}
}),
inline_alternatives: None,
enable_experimental_live_diffs: None,
},
VersionedAssistantSettingsContent::V2(settings) => settings.clone(),
},
AssistantSettingsContent::Legacy(settings) => AssistantSettingsContentV2 {
enabled: None,
button: settings.button,
dock: settings.dock,
default_width: settings.default_width,
default_height: settings.default_height,
default_model: Some(LanguageModelSelection {
provider: "openai".to_string(),
model: settings
.default_open_ai_model
.clone()
.unwrap_or_default()
.id()
.to_string(),
}),
inline_alternatives: None,
enable_experimental_live_diffs: None,
},
}
}
pub fn set_dock(&mut self, dock: AssistantDockPosition) {
match self {
AssistantSettingsContent::Versioned(settings) => match settings {
VersionedAssistantSettingsContent::V1(settings) => {
settings.dock = Some(dock);
}
VersionedAssistantSettingsContent::V2(settings) => {
settings.dock = Some(dock);
}
},
AssistantSettingsContent::Legacy(settings) => {
settings.dock = Some(dock);
}
}
}
pub fn set_model(&mut self, language_model: Arc<dyn LanguageModel>) {
let model = language_model.id().0.to_string();
let provider = language_model.provider_id().0.to_string();
match self {
AssistantSettingsContent::Versioned(settings) => match settings {
VersionedAssistantSettingsContent::V1(settings) => match provider.as_ref() {
"zed.dev" => {
log::warn!("attempted to set zed.dev model on outdated settings");
}
"anthropic" => {
let api_url = match &settings.provider {
Some(AssistantProviderContentV1::Anthropic { api_url, .. }) => {
api_url.clone()
}
_ => None,
};
settings.provider = Some(AssistantProviderContentV1::Anthropic {
default_model: AnthropicModel::from_id(&model).ok(),
api_url,
});
}
"ollama" => {
let api_url = match &settings.provider {
Some(AssistantProviderContentV1::Ollama { api_url, .. }) => {
api_url.clone()
}
_ => None,
};
settings.provider = Some(AssistantProviderContentV1::Ollama {
default_model: Some(ollama::Model::new(&model, None, None)),
api_url,
});
}
"openai" => {
let (api_url, available_models) = match &settings.provider {
Some(AssistantProviderContentV1::OpenAi {
api_url,
available_models,
..
}) => (api_url.clone(), available_models.clone()),
_ => (None, None),
};
settings.provider = Some(AssistantProviderContentV1::OpenAi {
default_model: OpenAiModel::from_id(&model).ok(),
api_url,
available_models,
});
}
_ => {}
},
VersionedAssistantSettingsContent::V2(settings) => {
settings.default_model = Some(LanguageModelSelection { provider, model });
}
},
AssistantSettingsContent::Legacy(settings) => {
if let Ok(model) = OpenAiModel::from_id(&language_model.id().0) {
settings.default_open_ai_model = Some(model);
}
}
}
}
}
#[derive(Clone, Serialize, Deserialize, JsonSchema, Debug)]
#[serde(tag = "version")]
pub enum VersionedAssistantSettingsContent {
#[serde(rename = "1")]
V1(AssistantSettingsContentV1),
#[serde(rename = "2")]
V2(AssistantSettingsContentV2),
}
impl Default for VersionedAssistantSettingsContent {
fn default() -> Self {
Self::V2(AssistantSettingsContentV2 {
enabled: None,
button: None,
dock: None,
default_width: None,
default_height: None,
default_model: None,
inline_alternatives: None,
enable_experimental_live_diffs: None,
})
}
}
#[derive(Clone, Serialize, Deserialize, JsonSchema, Debug)]
pub struct AssistantSettingsContentV2 {
/// Whether the Assistant is enabled.
///
/// Default: true
enabled: Option<bool>,
/// Whether to show the assistant panel button in the status bar.
///
/// Default: true
button: Option<bool>,
/// Where to dock the assistant.
///
/// Default: right
dock: Option<AssistantDockPosition>,
/// Default width in pixels when the assistant is docked to the left or right.
///
/// Default: 640
default_width: Option<f32>,
/// Default height in pixels when the assistant is docked to the bottom.
///
/// Default: 320
default_height: Option<f32>,
/// The default model to use when creating new chats.
default_model: Option<LanguageModelSelection>,
/// Additional models with which to generate alternatives when performing inline assists.
inline_alternatives: Option<Vec<LanguageModelSelection>>,
/// Enable experimental live diffs in the assistant panel.
///
/// Default: false
enable_experimental_live_diffs: Option<bool>,
}
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
pub struct LanguageModelSelection {
#[schemars(schema_with = "providers_schema")]
pub provider: String,
pub model: String,
}
fn providers_schema(_: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema {
schemars::schema::SchemaObject {
enum_values: Some(vec![
"anthropic".into(),
"google".into(),
"ollama".into(),
"openai".into(),
"zed.dev".into(),
"copilot_chat".into(),
]),
..Default::default()
}
.into()
}
impl Default for LanguageModelSelection {
fn default() -> Self {
Self {
provider: "openai".to_string(),
model: "gpt-4".to_string(),
}
}
}
#[derive(Clone, Serialize, Deserialize, JsonSchema, Debug)]
pub struct AssistantSettingsContentV1 {
/// Whether the Assistant is enabled.
///
/// Default: true
enabled: Option<bool>,
/// Whether to show the assistant panel button in the status bar.
///
/// Default: true
button: Option<bool>,
/// Where to dock the assistant.
///
/// Default: right
dock: Option<AssistantDockPosition>,
/// Default width in pixels when the assistant is docked to the left or right.
///
/// Default: 640
default_width: Option<f32>,
/// Default height in pixels when the assistant is docked to the bottom.
///
/// Default: 320
default_height: Option<f32>,
/// The provider of the assistant service.
///
/// This can be "openai", "anthropic", "ollama", "zed.dev"
/// each with their respective default models and configurations.
provider: Option<AssistantProviderContentV1>,
}
#[derive(Clone, Serialize, Deserialize, JsonSchema, Debug)]
pub struct LegacyAssistantSettingsContent {
/// Whether to show the assistant panel button in the status bar.
///
/// Default: true
pub button: Option<bool>,
/// Where to dock the assistant.
///
/// Default: right
pub dock: Option<AssistantDockPosition>,
/// Default width in pixels when the assistant is docked to the left or right.
///
/// Default: 640
pub default_width: Option<f32>,
/// Default height in pixels when the assistant is docked to the bottom.
///
/// Default: 320
pub default_height: Option<f32>,
/// The default OpenAI model to use when creating new chats.
///
/// Default: gpt-4-1106-preview
pub default_open_ai_model: Option<OpenAiModel>,
/// OpenAI API base URL to use when creating new chats.
///
/// Default: https://api.openai.com/v1
pub openai_api_url: Option<String>,
}
impl Settings for AssistantSettings {
const KEY: Option<&'static str> = Some("assistant");
const PRESERVED_KEYS: Option<&'static [&'static str]> = Some(&["version"]);
type FileContent = AssistantSettingsContent;
fn load(
sources: SettingsSources<Self::FileContent>,
_: &mut gpui::AppContext,
) -> anyhow::Result<Self> {
let mut settings = AssistantSettings::default();
for value in sources.defaults_and_customizations() {
if value.is_version_outdated() {
settings.using_outdated_settings_version = true;
}
let value = value.upgrade();
merge(&mut settings.enabled, value.enabled);
merge(&mut settings.button, value.button);
merge(&mut settings.dock, value.dock);
merge(
&mut settings.default_width,
value.default_width.map(Into::into),
);
merge(
&mut settings.default_height,
value.default_height.map(Into::into),
);
merge(&mut settings.default_model, value.default_model);
merge(&mut settings.inline_alternatives, value.inline_alternatives);
merge(
&mut settings.enable_experimental_live_diffs,
value.enable_experimental_live_diffs,
);
}
Ok(settings)
}
}
fn merge<T>(target: &mut T, value: Option<T>) {
if let Some(value) = value {
*target = value;
}
}
#[cfg(test)]
mod tests {
use fs::Fs;
use gpui::{ReadGlobal, TestAppContext};
use super::*;
#[gpui::test]
async fn test_deserialize_assistant_settings_with_version(cx: &mut TestAppContext) {
let fs = fs::FakeFs::new(cx.executor().clone());
fs.create_dir(paths::settings_file().parent().unwrap())
.await
.unwrap();
cx.update(|cx| {
let test_settings = settings::SettingsStore::test(cx);
cx.set_global(test_settings);
AssistantSettings::register(cx);
});
cx.update(|cx| {
assert!(!AssistantSettings::get_global(cx).using_outdated_settings_version);
assert_eq!(
AssistantSettings::get_global(cx).default_model,
LanguageModelSelection {
provider: "zed.dev".into(),
model: "claude-3-5-sonnet".into(),
}
);
});
cx.update(|cx| {
settings::SettingsStore::global(cx).update_settings_file::<AssistantSettings>(
fs.clone(),
|settings, _| {
*settings = AssistantSettingsContent::Versioned(
VersionedAssistantSettingsContent::V2(AssistantSettingsContentV2 {
default_model: Some(LanguageModelSelection {
provider: "test-provider".into(),
model: "gpt-99".into(),
}),
inline_alternatives: None,
enabled: None,
button: None,
dock: None,
default_width: None,
default_height: None,
enable_experimental_live_diffs: None,
}),
)
},
);
});
cx.run_until_parked();
let raw_settings_value = fs.load(paths::settings_file()).await.unwrap();
assert!(raw_settings_value.contains(r#""version": "2""#));
#[derive(Debug, Deserialize)]
struct AssistantSettingsTest {
assistant: AssistantSettingsContent,
}
let assistant_settings: AssistantSettingsTest =
serde_json_lenient::from_str(&raw_settings_value).unwrap();
assert!(!assistant_settings.assistant.is_version_outdated());
}
}

View File

@@ -1,16 +1,12 @@
use crate::context::attach_context_to_message;
use crate::context_store::ContextStore;
use crate::inline_prompt_editor::CodegenStatus;
use crate::{
prompts::PromptBuilder,
streaming_diff::{CharOperation, LineDiff, LineOperation, StreamingDiff},
};
use anyhow::{Context as _, Result};
use client::telemetry::Telemetry;
use collections::HashSet;
use editor::{Anchor, AnchorRangeExt, MultiBuffer, MultiBufferSnapshot, ToOffset as _, ToPoint};
use futures::{channel::mpsc, future::LocalBoxFuture, join, SinkExt, Stream, StreamExt};
use gpui::{AppContext, Context as _, EventEmitter, Model, ModelContext, Subscription, Task};
use gpui::{App, AppContext as _, Context, Entity, EventEmitter, Subscription, Task};
use language::{Buffer, IndentKind, Point, TransactionId};
use language_model::{
LanguageModel, LanguageModelRegistry, LanguageModelRequest, LanguageModelRequestMessage,
@@ -19,6 +15,7 @@ use language_model::{
use language_models::report_assistant_event;
use multi_buffer::MultiBufferRow;
use parking_lot::Mutex;
use prompt_library::PromptBuilder;
use rope::Rope;
use smol::future::FutureExt;
use std::{
@@ -31,17 +28,18 @@ use std::{
task::{self, Poll},
time::Instant,
};
use streaming_diff::{CharOperation, LineDiff, LineOperation, StreamingDiff};
use telemetry_events::{AssistantEvent, AssistantKind, AssistantPhase};
pub struct BufferCodegen {
alternatives: Vec<Model<CodegenAlternative>>,
alternatives: Vec<Entity<CodegenAlternative>>,
pub active_alternative: usize,
seen_alternatives: HashSet<usize>,
subscriptions: Vec<Subscription>,
buffer: Model<MultiBuffer>,
buffer: Entity<MultiBuffer>,
range: Range<Anchor>,
initial_transaction_id: Option<TransactionId>,
context_store: Model<ContextStore>,
context_store: Entity<ContextStore>,
telemetry: Arc<Telemetry>,
builder: Arc<PromptBuilder>,
pub is_insertion: bool,
@@ -49,15 +47,15 @@ pub struct BufferCodegen {
impl BufferCodegen {
pub fn new(
buffer: Model<MultiBuffer>,
buffer: Entity<MultiBuffer>,
range: Range<Anchor>,
initial_transaction_id: Option<TransactionId>,
context_store: Model<ContextStore>,
context_store: Entity<ContextStore>,
telemetry: Arc<Telemetry>,
builder: Arc<PromptBuilder>,
cx: &mut ModelContext<Self>,
cx: &mut Context<Self>,
) -> Self {
let codegen = cx.new_model(|cx| {
let codegen = cx.new(|cx| {
CodegenAlternative::new(
buffer.clone(),
range.clone(),
@@ -85,7 +83,7 @@ impl BufferCodegen {
this
}
fn subscribe_to_alternative(&mut self, cx: &mut ModelContext<Self>) {
fn subscribe_to_alternative(&mut self, cx: &mut Context<Self>) {
let codegen = self.active_alternative().clone();
self.subscriptions.clear();
self.subscriptions
@@ -94,22 +92,22 @@ impl BufferCodegen {
.push(cx.subscribe(&codegen, |_, _, event, cx| cx.emit(*event)));
}
pub fn active_alternative(&self) -> &Model<CodegenAlternative> {
pub fn active_alternative(&self) -> &Entity<CodegenAlternative> {
&self.alternatives[self.active_alternative]
}
pub fn status<'a>(&self, cx: &'a AppContext) -> &'a CodegenStatus {
pub fn status<'a>(&self, cx: &'a App) -> &'a CodegenStatus {
&self.active_alternative().read(cx).status
}
pub fn alternative_count(&self, cx: &AppContext) -> usize {
pub fn alternative_count(&self, cx: &App) -> usize {
LanguageModelRegistry::read_global(cx)
.inline_alternative_models()
.len()
+ 1
}
pub fn cycle_prev(&mut self, cx: &mut ModelContext<Self>) {
pub fn cycle_prev(&mut self, cx: &mut Context<Self>) {
let next_active_ix = if self.active_alternative == 0 {
self.alternatives.len() - 1
} else {
@@ -118,12 +116,12 @@ impl BufferCodegen {
self.activate(next_active_ix, cx);
}
pub fn cycle_next(&mut self, cx: &mut ModelContext<Self>) {
pub fn cycle_next(&mut self, cx: &mut Context<Self>) {
let next_active_ix = (self.active_alternative + 1) % self.alternatives.len();
self.activate(next_active_ix, cx);
}
fn activate(&mut self, index: usize, cx: &mut ModelContext<Self>) {
fn activate(&mut self, index: usize, cx: &mut Context<Self>) {
self.active_alternative()
.update(cx, |codegen, cx| codegen.set_active(false, cx));
self.seen_alternatives.insert(index);
@@ -134,7 +132,7 @@ impl BufferCodegen {
cx.notify();
}
pub fn start(&mut self, user_prompt: String, cx: &mut ModelContext<Self>) -> Result<()> {
pub fn start(&mut self, user_prompt: String, cx: &mut Context<Self>) -> Result<()> {
let alternative_models = LanguageModelRegistry::read_global(cx)
.inline_alternative_models()
.to_vec();
@@ -145,7 +143,7 @@ impl BufferCodegen {
self.alternatives.truncate(1);
for _ in 0..alternative_models.len() {
self.alternatives.push(cx.new_model(|cx| {
self.alternatives.push(cx.new(|cx| {
CodegenAlternative::new(
self.buffer.clone(),
self.range.clone(),
@@ -174,13 +172,13 @@ impl BufferCodegen {
Ok(())
}
pub fn stop(&mut self, cx: &mut ModelContext<Self>) {
pub fn stop(&mut self, cx: &mut Context<Self>) {
for codegen in &self.alternatives {
codegen.update(cx, |codegen, cx| codegen.stop(cx));
}
}
pub fn undo(&mut self, cx: &mut ModelContext<Self>) {
pub fn undo(&mut self, cx: &mut Context<Self>) {
self.active_alternative()
.update(cx, |codegen, cx| codegen.undo(cx));
@@ -192,27 +190,27 @@ impl BufferCodegen {
});
}
pub fn buffer(&self, cx: &AppContext) -> Model<MultiBuffer> {
pub fn buffer(&self, cx: &App) -> Entity<MultiBuffer> {
self.active_alternative().read(cx).buffer.clone()
}
pub fn old_buffer(&self, cx: &AppContext) -> Model<Buffer> {
pub fn old_buffer(&self, cx: &App) -> Entity<Buffer> {
self.active_alternative().read(cx).old_buffer.clone()
}
pub fn snapshot(&self, cx: &AppContext) -> MultiBufferSnapshot {
pub fn snapshot(&self, cx: &App) -> MultiBufferSnapshot {
self.active_alternative().read(cx).snapshot.clone()
}
pub fn edit_position(&self, cx: &AppContext) -> Option<Anchor> {
pub fn edit_position(&self, cx: &App) -> Option<Anchor> {
self.active_alternative().read(cx).edit_position
}
pub fn diff<'a>(&self, cx: &'a AppContext) -> &'a Diff {
pub fn diff<'a>(&self, cx: &'a App) -> &'a Diff {
&self.active_alternative().read(cx).diff
}
pub fn last_equal_ranges<'a>(&self, cx: &'a AppContext) -> &'a [Range<Anchor>] {
pub fn last_equal_ranges<'a>(&self, cx: &'a App) -> &'a [Range<Anchor>] {
self.active_alternative().read(cx).last_equal_ranges()
}
}
@@ -220,8 +218,8 @@ impl BufferCodegen {
impl EventEmitter<CodegenEvent> for BufferCodegen {}
pub struct CodegenAlternative {
buffer: Model<MultiBuffer>,
old_buffer: Model<Buffer>,
buffer: Entity<MultiBuffer>,
old_buffer: Entity<Buffer>,
snapshot: MultiBufferSnapshot,
edit_position: Option<Anchor>,
range: Range<Anchor>,
@@ -230,7 +228,7 @@ pub struct CodegenAlternative {
status: CodegenStatus,
generation: Task<()>,
diff: Diff,
context_store: Option<Model<ContextStore>>,
context_store: Option<Entity<ContextStore>>,
telemetry: Option<Arc<Telemetry>>,
_subscription: gpui::Subscription,
builder: Arc<PromptBuilder>,
@@ -247,27 +245,27 @@ impl EventEmitter<CodegenEvent> for CodegenAlternative {}
impl CodegenAlternative {
pub fn new(
buffer: Model<MultiBuffer>,
buffer: Entity<MultiBuffer>,
range: Range<Anchor>,
active: bool,
context_store: Option<Model<ContextStore>>,
context_store: Option<Entity<ContextStore>>,
telemetry: Option<Arc<Telemetry>>,
builder: Arc<PromptBuilder>,
cx: &mut ModelContext<Self>,
cx: &mut Context<Self>,
) -> Self {
let snapshot = buffer.read(cx).snapshot(cx);
let (old_excerpt, _) = snapshot
let (old_buffer, _, _) = snapshot
.range_to_buffer_ranges(range.clone())
.pop()
.unwrap();
let old_buffer = cx.new_model(|cx| {
let text = old_excerpt.buffer().as_rope().clone();
let line_ending = old_excerpt.buffer().line_ending();
let language = old_excerpt.buffer().language().cloned();
let old_buffer = cx.new(|cx| {
let text = old_buffer.as_rope().clone();
let line_ending = old_buffer.line_ending();
let language = old_buffer.language().cloned();
let language_registry = buffer
.read(cx)
.buffer(old_excerpt.buffer_id())
.buffer(old_buffer.remote_id())
.unwrap()
.read(cx)
.language_registry();
@@ -305,7 +303,7 @@ impl CodegenAlternative {
}
}
pub fn set_active(&mut self, active: bool, cx: &mut ModelContext<Self>) {
pub fn set_active(&mut self, active: bool, cx: &mut Context<Self>) {
if active != self.active {
self.active = active;
@@ -329,9 +327,9 @@ impl CodegenAlternative {
fn handle_buffer_event(
&mut self,
_buffer: Model<MultiBuffer>,
_buffer: Entity<MultiBuffer>,
event: &multi_buffer::Event,
cx: &mut ModelContext<Self>,
cx: &mut Context<Self>,
) {
if let multi_buffer::Event::TransactionUndone { transaction_id } = event {
if self.transformation_transaction_id == Some(*transaction_id) {
@@ -350,7 +348,7 @@ impl CodegenAlternative {
&mut self,
user_prompt: String,
model: Arc<dyn LanguageModel>,
cx: &mut ModelContext<Self>,
cx: &mut Context<Self>,
) -> Result<()> {
if let Some(transformation_transaction_id) = self.transformation_transaction_id.take() {
self.buffer.update(cx, |buffer, cx| {
@@ -377,11 +375,7 @@ impl CodegenAlternative {
Ok(())
}
fn build_request(
&self,
user_prompt: String,
cx: &mut AppContext,
) -> Result<LanguageModelRequest> {
fn build_request(&self, user_prompt: String, cx: &mut App) -> Result<LanguageModelRequest> {
let buffer = self.buffer.read(cx).snapshot(cx);
let language = buffer.language_at(self.range.start);
let language_name = if let Some(language) = language.as_ref() {
@@ -421,8 +415,7 @@ impl CodegenAlternative {
};
if let Some(context_store) = &self.context_store {
let context = context_store.update(cx, |this, _cx| this.context().clone());
attach_context_to_message(&mut request_message, context);
attach_context_to_message(&mut request_message, context_store.read(cx).snapshot(cx));
}
request_message.content.push(prompt.into());
@@ -441,7 +434,7 @@ impl CodegenAlternative {
model_provider_id: String,
model_api_key: Option<String>,
stream: impl 'static + Future<Output = Result<LanguageModelTextStream>>,
cx: &mut ModelContext<Self>,
cx: &mut Context<Self>,
) {
let start_time = Instant::now();
let snapshot = self.snapshot.clone();
@@ -478,7 +471,7 @@ impl CodegenAlternative {
let ranges = snapshot.range_to_buffer_ranges(self.range.clone());
ranges
.first()
.and_then(|(excerpt, _)| excerpt.buffer().language())
.and_then(|(buffer, _, _)| buffer.language())
.map(|language| language.name())
};
@@ -699,7 +692,7 @@ impl CodegenAlternative {
cx.notify();
}
pub fn stop(&mut self, cx: &mut ModelContext<Self>) {
pub fn stop(&mut self, cx: &mut Context<Self>) {
self.last_equal_ranges.clear();
if self.diff.is_empty() {
self.status = CodegenStatus::Idle;
@@ -711,7 +704,7 @@ impl CodegenAlternative {
cx.notify();
}
pub fn undo(&mut self, cx: &mut ModelContext<Self>) {
pub fn undo(&mut self, cx: &mut Context<Self>) {
self.buffer.update(cx, |buffer, cx| {
if let Some(transaction_id) = self.transformation_transaction_id.take() {
buffer.undo_transaction(transaction_id, cx);
@@ -723,7 +716,7 @@ impl CodegenAlternative {
fn apply_edits(
&mut self,
edits: impl IntoIterator<Item = (Range<Anchor>, String)>,
cx: &mut ModelContext<CodegenAlternative>,
cx: &mut Context<CodegenAlternative>,
) {
let transaction = self.buffer.update(cx, |buffer, cx| {
// Avoid grouping assistant edits with user edits.
@@ -750,7 +743,7 @@ impl CodegenAlternative {
fn reapply_line_based_diff(
&mut self,
line_operations: impl IntoIterator<Item = LineOperation>,
cx: &mut ModelContext<Self>,
cx: &mut Context<Self>,
) {
let old_snapshot = self.snapshot.clone();
let old_range = self.range.to_point(&old_snapshot);
@@ -806,7 +799,7 @@ impl CodegenAlternative {
}
}
fn reapply_batch_diff(&mut self, cx: &mut ModelContext<Self>) -> Task<()> {
fn reapply_batch_diff(&mut self, cx: &mut Context<Self>) -> Task<()> {
let old_snapshot = self.snapshot.clone();
let old_range = self.range.to_point(&old_snapshot);
let new_snapshot = self.buffer.read(cx).snapshot(cx);
@@ -1053,7 +1046,7 @@ mod tests {
stream::{self},
Stream,
};
use gpui::{Context, TestAppContext};
use gpui::TestAppContext;
use indoc::indoc;
use language::{
language_settings, tree_sitter_rust, Buffer, Language, LanguageConfig, LanguageMatcher,
@@ -1084,15 +1077,14 @@ mod tests {
}
}
"};
let buffer =
cx.new_model(|cx| Buffer::local(text, cx).with_language(Arc::new(rust_lang()), cx));
let buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer, cx));
let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(Arc::new(rust_lang()), cx));
let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
let range = buffer.read_with(cx, |buffer, cx| {
let snapshot = buffer.snapshot(cx);
snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_after(Point::new(4, 5))
});
let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
let codegen = cx.new_model(|cx| {
let codegen = cx.new(|cx| {
CodegenAlternative::new(
buffer.clone(),
range.clone(),
@@ -1149,15 +1141,14 @@ mod tests {
le
}
"};
let buffer =
cx.new_model(|cx| Buffer::local(text, cx).with_language(Arc::new(rust_lang()), cx));
let buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer, cx));
let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(Arc::new(rust_lang()), cx));
let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
let range = buffer.read_with(cx, |buffer, cx| {
let snapshot = buffer.snapshot(cx);
snapshot.anchor_before(Point::new(1, 6))..snapshot.anchor_after(Point::new(1, 6))
});
let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
let codegen = cx.new_model(|cx| {
let codegen = cx.new(|cx| {
CodegenAlternative::new(
buffer.clone(),
range.clone(),
@@ -1217,15 +1208,14 @@ mod tests {
" \n",
"}\n" //
);
let buffer =
cx.new_model(|cx| Buffer::local(text, cx).with_language(Arc::new(rust_lang()), cx));
let buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer, cx));
let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(Arc::new(rust_lang()), cx));
let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
let range = buffer.read_with(cx, |buffer, cx| {
let snapshot = buffer.snapshot(cx);
snapshot.anchor_before(Point::new(1, 2))..snapshot.anchor_after(Point::new(1, 2))
});
let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
let codegen = cx.new_model(|cx| {
let codegen = cx.new(|cx| {
CodegenAlternative::new(
buffer.clone(),
range.clone(),
@@ -1285,14 +1275,14 @@ mod tests {
\t}
}
"};
let buffer = cx.new_model(|cx| Buffer::local(text, cx));
let buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer, cx));
let buffer = cx.new(|cx| Buffer::local(text, cx));
let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
let range = buffer.read_with(cx, |buffer, cx| {
let snapshot = buffer.snapshot(cx);
snapshot.anchor_before(Point::new(0, 0))..snapshot.anchor_after(Point::new(4, 2))
});
let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
let codegen = cx.new_model(|cx| {
let codegen = cx.new(|cx| {
CodegenAlternative::new(
buffer.clone(),
range.clone(),
@@ -1340,15 +1330,14 @@ mod tests {
let x = 0;
}
"};
let buffer =
cx.new_model(|cx| Buffer::local(text, cx).with_language(Arc::new(rust_lang()), cx));
let buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer, cx));
let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(Arc::new(rust_lang()), cx));
let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
let range = buffer.read_with(cx, |buffer, cx| {
let snapshot = buffer.snapshot(cx);
snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_after(Point::new(1, 14))
});
let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
let codegen = cx.new_model(|cx| {
let codegen = cx.new(|cx| {
CodegenAlternative::new(
buffer.clone(),
range.clone(),
@@ -1435,7 +1424,7 @@ mod tests {
}
fn simulate_response_stream(
codegen: Model<CodegenAlternative>,
codegen: Entity<CodegenAlternative>,
cx: &mut TestAppContext,
) -> mpsc::UnboundedSender<String> {
let (chunks_tx, chunks_rx) = mpsc::unbounded();

View File

@@ -1,8 +1,17 @@
use gpui::SharedString;
use std::path::Path;
use std::rc::Rc;
use file_icons::FileIcons;
use gpui::{App, Entity, SharedString};
use language::Buffer;
use language_model::{LanguageModelRequestMessage, MessageContent};
use serde::{Deserialize, Serialize};
use text::BufferId;
use ui::IconName;
use util::post_inc;
use crate::{context_store::buffer_path_log_err, thread::Thread};
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, Serialize, Deserialize)]
pub struct ContextId(pub(crate) usize);
@@ -14,16 +23,18 @@ impl ContextId {
/// Some context attached to a message in a thread.
#[derive(Debug, Clone)]
pub struct Context {
pub struct ContextSnapshot {
pub id: ContextId,
pub name: SharedString,
pub parent: Option<SharedString>,
pub tooltip: Option<SharedString>,
pub icon_path: Option<SharedString>,
pub kind: ContextKind,
pub text: SharedString,
/// Joining these strings separated by \n yields text for model. Not refreshed by `snapshot`.
pub text: Box<[SharedString]>,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ContextKind {
File,
Directory,
@@ -31,62 +42,284 @@ pub enum ContextKind {
Thread,
}
impl ContextKind {
pub fn label(&self) -> &'static str {
match self {
ContextKind::File => "File",
ContextKind::Directory => "Folder",
ContextKind::FetchedUrl => "Fetch",
ContextKind::Thread => "Thread",
}
}
pub fn icon(&self) -> IconName {
match self {
ContextKind::File => IconName::File,
ContextKind::Directory => IconName::Folder,
ContextKind::FetchedUrl => IconName::Globe,
ContextKind::Thread => IconName::MessageCircle,
}
}
}
#[derive(Debug)]
pub enum AssistantContext {
File(FileContext),
Directory(DirectoryContext),
FetchedUrl(FetchedUrlContext),
Thread(ThreadContext),
}
impl AssistantContext {
pub fn id(&self) -> ContextId {
match self {
Self::File(file) => file.id,
Self::Directory(directory) => directory.snapshot.id,
Self::FetchedUrl(url) => url.id,
Self::Thread(thread) => thread.id,
}
}
}
#[derive(Debug)]
pub struct FileContext {
pub id: ContextId,
pub context_buffer: ContextBuffer,
}
#[derive(Debug)]
pub struct DirectoryContext {
pub path: Rc<Path>,
pub context_buffers: Vec<ContextBuffer>,
pub snapshot: ContextSnapshot,
}
#[derive(Debug)]
pub struct FetchedUrlContext {
pub id: ContextId,
pub url: SharedString,
pub text: SharedString,
}
// TODO: Model<Thread> holds onto the thread even if the thread is deleted. Can either handle this
// explicitly or have a WeakModel<Thread> and remove during snapshot.
#[derive(Debug)]
pub struct ThreadContext {
pub id: ContextId,
pub thread: Entity<Thread>,
pub text: SharedString,
}
// TODO: Model<Buffer> holds onto the buffer even if the file is deleted and closed. Should remove
// the context from the message editor in this case.
#[derive(Debug, Clone)]
pub struct ContextBuffer {
pub id: BufferId,
pub buffer: Entity<Buffer>,
pub version: clock::Global,
pub text: SharedString,
}
impl AssistantContext {
pub fn snapshot(&self, cx: &App) -> Option<ContextSnapshot> {
match &self {
Self::File(file_context) => file_context.snapshot(cx),
Self::Directory(directory_context) => Some(directory_context.snapshot()),
Self::FetchedUrl(fetched_url_context) => Some(fetched_url_context.snapshot()),
Self::Thread(thread_context) => Some(thread_context.snapshot(cx)),
}
}
}
impl FileContext {
pub fn snapshot(&self, cx: &App) -> Option<ContextSnapshot> {
let buffer = self.context_buffer.buffer.read(cx);
let path = buffer_path_log_err(buffer)?;
let full_path: SharedString = path.to_string_lossy().into_owned().into();
let name = match path.file_name() {
Some(name) => name.to_string_lossy().into_owned().into(),
None => full_path.clone(),
};
let parent = path
.parent()
.and_then(|p| p.file_name())
.map(|p| p.to_string_lossy().into_owned().into());
let icon_path = FileIcons::get_icon(&path, cx);
Some(ContextSnapshot {
id: self.id,
name,
parent,
tooltip: Some(full_path),
icon_path,
kind: ContextKind::File,
text: Box::new([self.context_buffer.text.clone()]),
})
}
}
impl DirectoryContext {
pub fn new(
id: ContextId,
path: &Path,
context_buffers: Vec<ContextBuffer>,
) -> DirectoryContext {
let full_path: SharedString = path.to_string_lossy().into_owned().into();
let name = match path.file_name() {
Some(name) => name.to_string_lossy().into_owned().into(),
None => full_path.clone(),
};
let parent = path
.parent()
.and_then(|p| p.file_name())
.map(|p| p.to_string_lossy().into_owned().into());
// TODO: include directory path in text?
let text = context_buffers
.iter()
.map(|b| b.text.clone())
.collect::<Vec<_>>()
.into();
DirectoryContext {
path: path.into(),
context_buffers,
snapshot: ContextSnapshot {
id,
name,
parent,
tooltip: Some(full_path),
icon_path: None,
kind: ContextKind::Directory,
text,
},
}
}
pub fn snapshot(&self) -> ContextSnapshot {
self.snapshot.clone()
}
}
impl FetchedUrlContext {
pub fn snapshot(&self) -> ContextSnapshot {
ContextSnapshot {
id: self.id,
name: self.url.clone(),
parent: None,
tooltip: None,
icon_path: None,
kind: ContextKind::FetchedUrl,
text: Box::new([self.text.clone()]),
}
}
}
impl ThreadContext {
pub fn snapshot(&self, cx: &App) -> ContextSnapshot {
let thread = self.thread.read(cx);
ContextSnapshot {
id: self.id,
name: thread.summary().unwrap_or("New thread".into()),
parent: None,
tooltip: None,
icon_path: None,
kind: ContextKind::Thread,
text: Box::new([self.text.clone()]),
}
}
}
pub fn attach_context_to_message(
message: &mut LanguageModelRequestMessage,
context: impl IntoIterator<Item = Context>,
contexts: impl Iterator<Item = ContextSnapshot>,
) {
let mut file_context = String::new();
let mut directory_context = String::new();
let mut fetch_context = String::new();
let mut thread_context = String::new();
let mut file_context = Vec::new();
let mut directory_context = Vec::new();
let mut fetch_context = Vec::new();
let mut thread_context = Vec::new();
for context in context.into_iter() {
let mut capacity = 0;
for context in contexts {
capacity += context.text.len();
match context.kind {
ContextKind::File { .. } => {
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');
fetch_context.push_str(&context.text);
fetch_context.push('\n');
}
ContextKind::Thread => {
thread_context.push_str(&context.name);
thread_context.push('\n');
thread_context.push_str(&context.text);
thread_context.push('\n');
ContextKind::File => file_context.push(context),
ContextKind::Directory => directory_context.push(context),
ContextKind::FetchedUrl => fetch_context.push(context),
ContextKind::Thread => thread_context.push(context),
}
}
if !file_context.is_empty() {
capacity += 1;
}
if !directory_context.is_empty() {
capacity += 1;
}
if !fetch_context.is_empty() {
capacity += 1 + fetch_context.len();
}
if !thread_context.is_empty() {
capacity += 1 + thread_context.len();
}
if capacity == 0 {
return;
}
let mut context_chunks = Vec::with_capacity(capacity);
if !file_context.is_empty() {
context_chunks.push("The following files are available:\n");
for context in &file_context {
for chunk in &context.text {
context_chunks.push(&chunk);
}
}
}
let mut context_text = String::new();
if !file_context.is_empty() {
context_text.push_str("The following files are available:\n");
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);
context_chunks.push("The following directories are available:\n");
for context in &directory_context {
for chunk in &context.text {
context_chunks.push(&chunk);
}
}
}
if !fetch_context.is_empty() {
context_text.push_str("The following fetched results are available\n");
context_text.push_str(&fetch_context);
context_chunks.push("The following fetched results are available:\n");
for context in &fetch_context {
context_chunks.push(&context.name);
for chunk in &context.text {
context_chunks.push(&chunk);
}
}
}
if !thread_context.is_empty() {
context_text.push_str("The following previous conversation threads are available\n");
context_text.push_str(&thread_context);
context_chunks.push("The following previous conversation threads are available:\n");
for context in &thread_context {
context_chunks.push(&context.name);
for chunk in &context.text {
context_chunks.push(&chunk);
}
}
}
if !context_text.is_empty() {
message.content.push(MessageContent::Text(context_text));
debug_assert!(
context_chunks.len() == capacity,
"attach_context_message calculated capacity of {}, but length was {}",
capacity,
context_chunks.len()
);
if !context_chunks.is_empty() {
message
.content
.push(MessageContent::Text(context_chunks.join("\n")));
}
}

View File

@@ -3,16 +3,17 @@ mod fetch_context_picker;
mod file_context_picker;
mod thread_context_picker;
use std::path::PathBuf;
use std::sync::Arc;
use gpui::{
AppContext, DismissEvent, EventEmitter, FocusHandle, FocusableView, SharedString, Task, View,
WeakModel, WeakView,
};
use picker::{Picker, PickerDelegate};
use ui::{prelude::*, ListItem, ListItemSpacing};
use util::ResultExt;
use workspace::Workspace;
use anyhow::{anyhow, Result};
use editor::Editor;
use file_context_picker::render_file_context_entry;
use gpui::{App, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, Task, WeakEntity};
use project::ProjectPath;
use thread_context_picker::{render_thread_context_entry, ThreadContextEntry};
use ui::{prelude::*, ContextMenu, ContextMenuEntry, ContextMenuItem};
use workspace::{notifications::NotifyResultExt, Workspace};
use crate::context::ContextKind;
use crate::context_picker::directory_context_picker::DirectoryContextPicker;
@@ -21,6 +22,7 @@ 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;
use crate::AssistantPanel;
#[derive(Debug, Clone, Copy)]
pub enum ConfirmBehavior {
@@ -30,82 +32,367 @@ pub enum ConfirmBehavior {
#[derive(Debug, Clone)]
enum ContextPickerMode {
Default,
File(View<FileContextPicker>),
Directory(View<DirectoryContextPicker>),
Fetch(View<FetchContextPicker>),
Thread(View<ThreadContextPicker>),
Default(Entity<ContextMenu>),
File(Entity<FileContextPicker>),
Directory(Entity<DirectoryContextPicker>),
Fetch(Entity<FetchContextPicker>),
Thread(Entity<ThreadContextPicker>),
}
pub(super) struct ContextPicker {
mode: ContextPickerMode,
picker: View<Picker<ContextPickerDelegate>>,
workspace: WeakEntity<Workspace>,
editor: WeakEntity<Editor>,
context_store: WeakEntity<ContextStore>,
thread_store: Option<WeakEntity<ThreadStore>>,
confirm_behavior: ConfirmBehavior,
}
impl ContextPicker {
pub fn new(
workspace: WeakView<Workspace>,
thread_store: Option<WeakModel<ThreadStore>>,
context_store: WeakModel<ContextStore>,
workspace: WeakEntity<Workspace>,
thread_store: Option<WeakEntity<ThreadStore>>,
context_store: WeakEntity<ContextStore>,
editor: WeakEntity<Editor>,
confirm_behavior: ConfirmBehavior,
cx: &mut ViewContext<Self>,
window: &mut Window,
cx: &mut Context<Self>,
) -> Self {
let mut entries = Vec::new();
entries.push(ContextPickerEntry {
name: "File".into(),
kind: ContextKind::File,
icon: IconName::File,
});
entries.push(ContextPickerEntry {
name: "Folder".into(),
kind: ContextKind::Directory,
icon: IconName::Folder,
});
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(),
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()))
});
ContextPicker {
mode: ContextPickerMode::Default,
picker,
mode: ContextPickerMode::Default(ContextMenu::build(
window,
cx,
|menu, _window, _cx| menu,
)),
workspace,
context_store,
thread_store,
editor,
confirm_behavior,
}
}
pub fn reset_mode(&mut self) {
self.mode = ContextPickerMode::Default;
pub fn init(&mut self, window: &mut Window, cx: &mut Context<Self>) {
self.mode = ContextPickerMode::Default(self.build_menu(window, cx));
cx.notify();
}
fn build_menu(&mut self, window: &mut Window, cx: &mut Context<Self>) -> Entity<ContextMenu> {
let context_picker = cx.entity().clone();
let menu = ContextMenu::build(window, cx, move |menu, _window, cx| {
let recent = self.recent_entries(cx);
let has_recent = !recent.is_empty();
let recent_entries = recent
.into_iter()
.enumerate()
.map(|(ix, entry)| self.recent_menu_item(context_picker.clone(), ix, entry));
let mut context_kinds = vec![
ContextKind::File,
ContextKind::Directory,
ContextKind::FetchedUrl,
];
if self.allow_threads() {
context_kinds.push(ContextKind::Thread);
}
let menu = menu
.when(has_recent, |menu| {
menu.custom_row(|_, _| {
div()
.mb_1()
.child(
Label::new("Recent")
.color(Color::Muted)
.size(LabelSize::Small),
)
.into_any_element()
})
})
.extend(recent_entries)
.when(has_recent, |menu| menu.separator())
.extend(context_kinds.into_iter().map(|kind| {
let context_picker = context_picker.clone();
ContextMenuEntry::new(kind.label())
.icon(kind.icon())
.icon_size(IconSize::XSmall)
.icon_color(Color::Muted)
.handler(move |window, cx| {
context_picker.update(cx, |this, cx| this.select_kind(kind, window, cx))
})
}));
match self.confirm_behavior {
ConfirmBehavior::KeepOpen => menu.keep_open_on_confirm(),
ConfirmBehavior::Close => menu,
}
});
cx.subscribe(&menu, move |_, _, _: &DismissEvent, cx| {
cx.emit(DismissEvent);
})
.detach();
menu
}
/// Whether threads are allowed as context.
pub fn allow_threads(&self) -> bool {
self.thread_store.is_some()
}
fn select_kind(&mut self, kind: ContextKind, window: &mut Window, cx: &mut Context<Self>) {
let context_picker = cx.entity().downgrade();
match kind {
ContextKind::File => {
self.mode = ContextPickerMode::File(cx.new(|cx| {
FileContextPicker::new(
context_picker.clone(),
self.workspace.clone(),
self.editor.clone(),
self.context_store.clone(),
self.confirm_behavior,
window,
cx,
)
}));
}
ContextKind::Directory => {
self.mode = ContextPickerMode::Directory(cx.new(|cx| {
DirectoryContextPicker::new(
context_picker.clone(),
self.workspace.clone(),
self.context_store.clone(),
self.confirm_behavior,
window,
cx,
)
}));
}
ContextKind::FetchedUrl => {
self.mode = ContextPickerMode::Fetch(cx.new(|cx| {
FetchContextPicker::new(
context_picker.clone(),
self.workspace.clone(),
self.context_store.clone(),
self.confirm_behavior,
window,
cx,
)
}));
}
ContextKind::Thread => {
if let Some(thread_store) = self.thread_store.as_ref() {
self.mode = ContextPickerMode::Thread(cx.new(|cx| {
ThreadContextPicker::new(
thread_store.clone(),
context_picker.clone(),
self.context_store.clone(),
self.confirm_behavior,
window,
cx,
)
}));
}
}
}
cx.notify();
cx.focus_self(window);
}
fn recent_menu_item(
&self,
context_picker: Entity<ContextPicker>,
ix: usize,
entry: RecentEntry,
) -> ContextMenuItem {
match entry {
RecentEntry::File {
project_path,
path_prefix,
} => {
let context_store = self.context_store.clone();
let path = project_path.path.clone();
ContextMenuItem::custom_entry(
move |_window, cx| {
render_file_context_entry(
ElementId::NamedInteger("ctx-recent".into(), ix),
&path,
&path_prefix,
context_store.clone(),
cx,
)
.into_any()
},
move |window, cx| {
context_picker.update(cx, |this, cx| {
this.add_recent_file(project_path.clone(), window, cx);
})
},
)
}
RecentEntry::Thread(thread) => {
let context_store = self.context_store.clone();
let view_thread = thread.clone();
ContextMenuItem::custom_entry(
move |_window, cx| {
render_thread_context_entry(&view_thread, context_store.clone(), cx)
.into_any()
},
move |_window, cx| {
context_picker.update(cx, |this, cx| {
this.add_recent_thread(thread.clone(), cx)
.detach_and_log_err(cx);
})
},
)
}
}
}
fn add_recent_file(
&self,
project_path: ProjectPath,
window: &mut Window,
cx: &mut Context<Self>,
) {
let Some(context_store) = self.context_store.upgrade() else {
return;
};
let task = context_store.update(cx, |context_store, cx| {
context_store.add_file_from_path(project_path.clone(), cx)
});
cx.spawn_in(window, |_, mut cx| async move {
task.await.notify_async_err(&mut cx)
})
.detach();
cx.notify();
}
fn add_recent_thread(
&self,
thread: ThreadContextEntry,
cx: &mut Context<Self>,
) -> Task<Result<()>> {
let Some(context_store) = self.context_store.upgrade() else {
return Task::ready(Err(anyhow!("context store not available")));
};
let Some(thread_store) = self
.thread_store
.as_ref()
.and_then(|thread_store| thread_store.upgrade())
else {
return Task::ready(Err(anyhow!("thread store not available")));
};
let open_thread_task = thread_store.update(cx, |this, cx| this.open_thread(&thread.id, cx));
cx.spawn(|this, mut cx| async move {
let thread = open_thread_task.await?;
context_store.update(&mut cx, |context_store, cx| {
context_store.add_thread(thread, cx);
})?;
this.update(&mut cx, |_this, cx| cx.notify())
})
}
fn recent_entries(&self, cx: &mut App) -> Vec<RecentEntry> {
let Some(workspace) = self.workspace.upgrade().map(|w| w.read(cx)) else {
return vec![];
};
let Some(context_store) = self.context_store.upgrade().map(|cs| cs.read(cx)) else {
return vec![];
};
let mut recent = Vec::with_capacity(6);
let mut current_files = context_store.file_paths(cx);
if let Some(active_path) = Self::active_singleton_buffer_path(&workspace, cx) {
current_files.insert(active_path);
}
let project = workspace.project().read(cx);
recent.extend(
workspace
.recent_navigation_history_iter(cx)
.filter(|(path, _)| !current_files.contains(&path.path.to_path_buf()))
.take(4)
.filter_map(|(project_path, _)| {
project
.worktree_for_id(project_path.worktree_id, cx)
.map(|worktree| RecentEntry::File {
project_path,
path_prefix: worktree.read(cx).root_name().into(),
})
}),
);
let mut current_threads = context_store.thread_ids();
if let Some(active_thread) = workspace
.panel::<AssistantPanel>(cx)
.map(|panel| panel.read(cx).active_thread(cx))
{
current_threads.insert(active_thread.read(cx).id().clone());
}
let Some(thread_store) = self
.thread_store
.as_ref()
.and_then(|thread_store| thread_store.upgrade())
else {
return recent;
};
thread_store.update(cx, |thread_store, _cx| {
recent.extend(
thread_store
.threads()
.into_iter()
.filter(|thread| !current_threads.contains(&thread.id))
.take(2)
.map(|thread| {
RecentEntry::Thread(ThreadContextEntry {
id: thread.id,
summary: thread.summary,
})
}),
)
});
recent
}
fn active_singleton_buffer_path(workspace: &Workspace, cx: &App) -> Option<PathBuf> {
let active_item = workspace.active_item(cx)?;
let editor = active_item.to_any().downcast::<Editor>().ok()?.read(cx);
let buffer = editor.buffer().read(cx).as_singleton()?;
let path = buffer.read(cx).file()?.path().to_path_buf();
Some(path)
}
}
impl EventEmitter<DismissEvent> for ContextPicker {}
impl FocusableView for ContextPicker {
fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
impl Focusable for ContextPicker {
fn focus_handle(&self, cx: &App) -> FocusHandle {
match &self.mode {
ContextPickerMode::Default => self.picker.focus_handle(cx),
ContextPickerMode::Default(menu) => menu.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),
@@ -115,12 +402,12 @@ 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 Context<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::Default(menu) => parent.child(menu.clone()),
ContextPickerMode::File(file_picker) => parent.child(file_picker.clone()),
ContextPickerMode::Directory(directory_picker) => {
parent.child(directory_picker.clone())
@@ -130,140 +417,10 @@ 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>,
thread_store: Option<WeakModel<ThreadStore>>,
context_store: WeakModel<ContextStore>,
confirm_behavior: ConfirmBehavior,
entries: Vec<ContextPickerEntry>,
selected_ix: usize,
}
impl PickerDelegate for ContextPickerDelegate {
type ListItem = ListItem;
fn match_count(&self) -> usize {
self.entries.len()
}
fn selected_index(&self) -> usize {
self.selected_ix
}
fn set_selected_index(&mut self, ix: usize, cx: &mut ViewContext<Picker<Self>>) {
self.selected_ix = ix.min(self.entries.len().saturating_sub(1));
cx.notify();
}
fn placeholder_text(&self, _cx: &mut WindowContext) -> Arc<str> {
"Select a context source…".into()
}
fn update_matches(&mut self, _query: String, _cx: &mut ViewContext<Picker<Self>>) -> Task<()> {
Task::ready(())
}
fn confirm(&mut self, _secondary: bool, cx: &mut ViewContext<Picker<Self>>) {
if let Some(entry) = self.entries.get(self.selected_ix) {
self.context_picker
.update(cx, |this, cx| {
match entry.kind {
ContextKind::File => {
this.mode = ContextPickerMode::File(cx.new_view(|cx| {
FileContextPicker::new(
self.context_picker.clone(),
self.workspace.clone(),
self.context_store.clone(),
self.confirm_behavior,
cx,
)
}));
}
ContextKind::Directory => {
this.mode = ContextPickerMode::Directory(cx.new_view(|cx| {
DirectoryContextPicker::new(
self.context_picker.clone(),
self.workspace.clone(),
self.context_store.clone(),
self.confirm_behavior,
cx,
)
}));
}
ContextKind::FetchedUrl => {
this.mode = ContextPickerMode::Fetch(cx.new_view(|cx| {
FetchContextPicker::new(
self.context_picker.clone(),
self.workspace.clone(),
self.context_store.clone(),
self.confirm_behavior,
cx,
)
}));
}
ContextKind::Thread => {
if let Some(thread_store) = self.thread_store.as_ref() {
this.mode = ContextPickerMode::Thread(cx.new_view(|cx| {
ThreadContextPicker::new(
thread_store.clone(),
self.context_picker.clone(),
self.context_store.clone(),
self.confirm_behavior,
cx,
)
}));
}
}
}
cx.focus_self();
})
.log_err();
}
}
fn dismissed(&mut self, cx: &mut ViewContext<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(_) => {}
})
.log_err();
}
fn render_match(
&self,
ix: usize,
selected: bool,
_cx: &mut ViewContext<Picker<Self>>,
) -> Option<Self::ListItem> {
let entry = &self.entries[ix];
Some(
ListItem::new(ix)
.inset(true)
.spacing(ListItemSpacing::Dense)
.toggle_state(selected)
.child(
h_flex()
.min_w(px(250.))
.max_w(px(400.))
.gap_2()
.child(Icon::new(entry.icon).size(IconSize::Small))
.child(Label::new(entry.name.clone()).single_line()),
),
)
}
enum RecentEntry {
File {
project_path: ProjectPath,
path_prefix: Arc<str>,
},
Thread(ThreadContextEntry),
}

View File

@@ -2,29 +2,29 @@ use std::path::Path;
use std::sync::atomic::AtomicBool;
use std::sync::Arc;
use anyhow::anyhow;
use fuzzy::PathMatch;
use gpui::{AppContext, DismissEvent, FocusHandle, FocusableView, Task, View, WeakModel, WeakView};
use gpui::{App, DismissEvent, Entity, FocusHandle, Focusable, Task, WeakEntity};
use picker::{Picker, PickerDelegate};
use project::{PathMatchCandidateSet, ProjectPath, Worktree, WorktreeId};
use project::{PathMatchCandidateSet, ProjectPath, WorktreeId};
use ui::{prelude::*, ListItem};
use util::ResultExt as _;
use workspace::Workspace;
use workspace::{notifications::NotifyResultExt, Workspace};
use crate::context_picker::{ConfirmBehavior, ContextPicker};
use crate::context_store::{push_fenced_codeblock, ContextStore};
use crate::context_store::ContextStore;
pub struct DirectoryContextPicker {
picker: View<Picker<DirectoryContextPickerDelegate>>,
picker: Entity<Picker<DirectoryContextPickerDelegate>>,
}
impl DirectoryContextPicker {
pub fn new(
context_picker: WeakView<ContextPicker>,
workspace: WeakView<Workspace>,
context_store: WeakModel<ContextStore>,
context_picker: WeakEntity<ContextPicker>,
workspace: WeakEntity<Workspace>,
context_store: WeakEntity<ContextStore>,
confirm_behavior: ConfirmBehavior,
cx: &mut ViewContext<Self>,
window: &mut Window,
cx: &mut Context<Self>,
) -> Self {
let delegate = DirectoryContextPickerDelegate::new(
context_picker,
@@ -32,28 +32,28 @@ impl DirectoryContextPicker {
context_store,
confirm_behavior,
);
let picker = cx.new_view(|cx| Picker::uniform_list(delegate, cx));
let picker = cx.new(|cx| Picker::uniform_list(delegate, window, cx));
Self { picker }
}
}
impl FocusableView for DirectoryContextPicker {
fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
impl Focusable for DirectoryContextPicker {
fn focus_handle(&self, cx: &App) -> FocusHandle {
self.picker.focus_handle(cx)
}
}
impl Render for DirectoryContextPicker {
fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
self.picker.clone()
}
}
pub struct DirectoryContextPickerDelegate {
context_picker: WeakView<ContextPicker>,
workspace: WeakView<Workspace>,
context_store: WeakModel<ContextStore>,
context_picker: WeakEntity<ContextPicker>,
workspace: WeakEntity<Workspace>,
context_store: WeakEntity<ContextStore>,
confirm_behavior: ConfirmBehavior,
matches: Vec<PathMatch>,
selected_index: usize,
@@ -61,9 +61,9 @@ pub struct DirectoryContextPickerDelegate {
impl DirectoryContextPickerDelegate {
pub fn new(
context_picker: WeakView<ContextPicker>,
workspace: WeakView<Workspace>,
context_store: WeakModel<ContextStore>,
context_picker: WeakEntity<ContextPicker>,
workspace: WeakEntity<Workspace>,
context_store: WeakEntity<ContextStore>,
confirm_behavior: ConfirmBehavior,
) -> Self {
Self {
@@ -80,8 +80,8 @@ impl DirectoryContextPickerDelegate {
&mut self,
query: String,
cancellation_flag: Arc<AtomicBool>,
workspace: &View<Workspace>,
cx: &mut ViewContext<Picker<Self>>,
workspace: &Entity<Workspace>,
cx: &mut Context<Picker<Self>>,
) -> Task<Vec<PathMatch>> {
if query.is_empty() {
let workspace = workspace.read(cx);
@@ -147,15 +147,25 @@ impl PickerDelegate for DirectoryContextPickerDelegate {
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 Context<Picker<Self>>,
) {
self.selected_index = ix;
}
fn placeholder_text(&self, _cx: &mut WindowContext) -> Arc<str> {
fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> {
"Search folders…".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 Context<Picker<Self>>,
) -> Task<()> {
let Some(workspace) = self.workspace.upgrade() else {
return Task::ready(());
};
@@ -174,118 +184,42 @@ impl PickerDelegate for DirectoryContextPickerDelegate {
})
}
fn confirm(&mut self, _secondary: bool, cx: &mut ViewContext<Picker<Self>>) {
fn confirm(&mut self, _secondary: bool, window: &mut Window, cx: &mut Context<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())
let project_path = ProjectPath {
worktree_id: WorktreeId::from_usize(mat.worktree_id),
path: mat.path.clone(),
};
let Some(task) = self
.context_store
.update(cx, |context_store, cx| {
context_store.add_directory(project_path, cx)
})
.ok()
else {
return;
};
let path = mat.path.clone();
let already_included = self
.context_store
.update(cx, |context_store, _cx| {
if let Some(context_id) = context_store.included_directory(&path) {
context_store.remove_context(&context_id);
true
} else {
false
}
})
.unwrap_or(true);
if already_included {
return;
}
let worktree_id = WorktreeId::from_usize(mat.worktree_id);
let confirm_behavior = self.confirm_behavior;
cx.spawn(|this, mut cx| async move {
let worktree = project.update(&mut cx, |project, cx| {
project
.worktree_for_id(worktree_id, cx)
.ok_or_else(|| anyhow!("no worktree found for {worktree_id:?}"))
})??;
let files = worktree.update(&mut cx, |worktree, _cx| {
collect_files_in_path(worktree, &path)
})?;
let open_buffer_tasks = project.update(&mut cx, |project, cx| {
files
.into_iter()
.map(|file_path| {
project.open_buffer(
ProjectPath {
worktree_id,
path: file_path.clone(),
},
cx,
)
})
.collect::<Vec<_>>()
})?;
let buffers = futures::future::join_all(open_buffer_tasks).await;
this.update(&mut cx, |this, cx| {
let mut text = String::new();
let mut ok_count = 0;
for buffer in buffers.into_iter().flatten() {
let buffer = buffer.read(cx);
let path = buffer.file().map_or(&path, |file| file.path());
push_fenced_codeblock(&path, buffer.text(), &mut text);
ok_count += 1;
}
if ok_count == 0 {
let Some(workspace) = workspace.upgrade() else {
return anyhow::Ok(());
};
workspace.update(cx, |workspace, cx| {
workspace.show_error(
&anyhow::anyhow!(
"Could not read any text files from {}",
path.display()
),
cx,
);
});
return anyhow::Ok(());
}
this.delegate
.context_store
.update(cx, |context_store, _cx| {
context_store.insert_directory(&path, text);
})?;
match confirm_behavior {
cx.spawn_in(window, |this, mut cx| async move {
match task.await.notify_async_err(&mut cx) {
None => anyhow::Ok(()),
Some(()) => this.update_in(&mut cx, |this, window, cx| match confirm_behavior {
ConfirmBehavior::KeepOpen => {}
ConfirmBehavior::Close => this.delegate.dismissed(cx),
}
anyhow::Ok(())
})??;
anyhow::Ok(())
ConfirmBehavior::Close => this.delegate.dismissed(window, cx),
}),
}
})
.detach_and_log_err(cx)
.detach_and_log_err(cx);
}
fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>) {
fn dismissed(&mut self, _window: &mut Window, cx: &mut Context<Picker<Self>>) {
self.context_picker
.update(cx, |this, cx| {
this.reset_mode();
.update(cx, |_, cx| {
cx.emit(DismissEvent);
})
.ok();
@@ -295,7 +229,8 @@ impl PickerDelegate for DirectoryContextPickerDelegate {
&self,
ix: usize,
selected: bool,
cx: &mut ViewContext<Picker<Self>>,
_window: &mut Window,
cx: &mut Context<Picker<Self>>,
) -> Option<Self::ListItem> {
let path_match = &self.matches[ix];
let directory_name = path_match.path.to_string_lossy().to_string();
@@ -303,7 +238,7 @@ impl PickerDelegate for DirectoryContextPickerDelegate {
let added = self.context_store.upgrade().map_or(false, |context_store| {
context_store
.read(cx)
.included_directory(&path_match.path)
.includes_directory(&path_match.path)
.is_some()
});
@@ -311,7 +246,12 @@ impl PickerDelegate for DirectoryContextPickerDelegate {
ListItem::new(ix)
.inset(true)
.toggle_state(selected)
.child(h_flex().gap_2().child(Label::new(directory_name)))
.start_slot(
Icon::new(IconName::Folder)
.size(IconSize::XSmall)
.color(Color::Muted),
)
.child(Label::new(directory_name))
.when(added, |el| {
el.end_slot(
h_flex()
@@ -327,17 +267,3 @@ impl PickerDelegate for DirectoryContextPickerDelegate {
)
}
}
fn collect_files_in_path(worktree: &Worktree, path: &Path) -> Vec<Arc<Path>> {
let mut files = Vec::new();
for entry in worktree.child_entries(path) {
if entry.is_dir() {
files.extend(collect_files_in_path(worktree, &entry.path));
} else if entry.is_file() {
files.push(entry.path.clone());
}
}
files
}

View File

@@ -4,27 +4,28 @@ 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::{App, DismissEvent, Entity, FocusHandle, Focusable, Task, WeakEntity};
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::*, Context, ListItem, Window};
use workspace::Workspace;
use crate::context_picker::{ConfirmBehavior, ContextPicker};
use crate::context_store::ContextStore;
pub struct FetchContextPicker {
picker: View<Picker<FetchContextPickerDelegate>>,
picker: Entity<Picker<FetchContextPickerDelegate>>,
}
impl FetchContextPicker {
pub fn new(
context_picker: WeakView<ContextPicker>,
workspace: WeakView<Workspace>,
context_store: WeakModel<ContextStore>,
context_picker: WeakEntity<ContextPicker>,
workspace: WeakEntity<Workspace>,
context_store: WeakEntity<ContextStore>,
confirm_behavior: ConfirmBehavior,
cx: &mut ViewContext<Self>,
window: &mut Window,
cx: &mut Context<Self>,
) -> Self {
let delegate = FetchContextPickerDelegate::new(
context_picker,
@@ -32,20 +33,20 @@ impl FetchContextPicker {
context_store,
confirm_behavior,
);
let picker = cx.new_view(|cx| Picker::uniform_list(delegate, cx));
let picker = cx.new(|cx| Picker::uniform_list(delegate, window, cx));
Self { picker }
}
}
impl FocusableView for FetchContextPicker {
fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
impl Focusable for FetchContextPicker {
fn focus_handle(&self, cx: &App) -> 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 Context<Self>) -> impl IntoElement {
self.picker.clone()
}
}
@@ -58,18 +59,18 @@ enum ContentType {
}
pub struct FetchContextPickerDelegate {
context_picker: WeakView<ContextPicker>,
workspace: WeakView<Workspace>,
context_store: WeakModel<ContextStore>,
context_picker: WeakEntity<ContextPicker>,
workspace: WeakEntity<Workspace>,
context_store: WeakEntity<ContextStore>,
confirm_behavior: ConfirmBehavior,
url: String,
}
impl FetchContextPickerDelegate {
pub fn new(
context_picker: WeakView<ContextPicker>,
workspace: WeakView<Workspace>,
context_store: WeakModel<ContextStore>,
context_picker: WeakEntity<ContextPicker>,
workspace: WeakEntity<Workspace>,
context_store: WeakEntity<ContextStore>,
confirm_behavior: ConfirmBehavior,
) -> Self {
FetchContextPickerDelegate {
@@ -81,11 +82,12 @@ impl FetchContextPickerDelegate {
}
}
async fn build_message(http_client: Arc<HttpClientWithUrl>, url: &str) -> Result<String> {
let mut url = url.to_owned();
if !url.starts_with("https://") && !url.starts_with("http://") {
url = format!("https://{url}");
}
async fn build_message(http_client: Arc<HttpClientWithUrl>, url: String) -> Result<String> {
let url = if !url.starts_with("https://") && !url.starts_with("http://") {
format!("https://{url}")
} else {
url
};
let mut response = http_client.get(&url, AsyncBody::default(), true).await?;
@@ -165,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 App) -> SharedString {
"Enter the URL that you would like to fetch".into()
}
@@ -173,19 +175,30 @@ 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 Context<Picker<Self>>,
) {
}
fn placeholder_text(&self, _cx: &mut WindowContext) -> Arc<str> {
fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> 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 Context<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 Context<Picker<Self>>) {
let Some(workspace) = self.workspace.upgrade() else {
return;
};
@@ -193,21 +206,22 @@ impl PickerDelegate for FetchContextPickerDelegate {
let http_client = workspace.read(cx).client().http_client().clone();
let url = self.url.clone();
let confirm_behavior = self.confirm_behavior;
cx.spawn(|this, mut cx| async move {
let text = Self::build_message(http_client, &url).await?;
cx.spawn_in(window, |this, mut cx| async move {
let text = cx
.background_executor()
.spawn(Self::build_message(http_client, url.clone()))
.await?;
this.update(&mut cx, |this, cx| {
this.update_in(&mut cx, |this, window, cx| {
this.delegate
.context_store
.update(cx, |context_store, _cx| {
if context_store.included_url(&url).is_none() {
context_store.insert_fetched_url(url, text);
}
context_store.add_fetched_url(url, text);
})?;
match confirm_behavior {
ConfirmBehavior::KeepOpen => {}
ConfirmBehavior::Close => this.delegate.dismissed(cx),
ConfirmBehavior::Close => this.delegate.dismissed(window, cx),
}
anyhow::Ok(())
@@ -218,10 +232,9 @@ impl PickerDelegate for FetchContextPickerDelegate {
.detach_and_log_err(cx);
}
fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>) {
fn dismissed(&mut self, _window: &mut Window, cx: &mut Context<Picker<Self>>) {
self.context_picker
.update(cx, |this, cx| {
this.reset_mode();
.update(cx, |_, cx| {
cx.emit(DismissEvent);
})
.ok();
@@ -231,10 +244,11 @@ impl PickerDelegate for FetchContextPickerDelegate {
&self,
ix: usize,
selected: bool,
cx: &mut ViewContext<Picker<Self>>,
_window: &mut Window,
cx: &mut Context<Picker<Self>>,
) -> Option<Self::ListItem> {
let added = self.context_store.upgrade().map_or(false, |context_store| {
context_store.read(cx).included_url(&self.url).is_some()
context_store.read(cx).includes_url(&self.url).is_some()
});
Some(

View File

@@ -1,58 +1,75 @@
use std::collections::BTreeSet;
use std::ops::Range;
use std::path::Path;
use std::sync::atomic::AtomicBool;
use std::sync::Arc;
use editor::actions::FoldAt;
use editor::display_map::{Crease, FoldId};
use editor::scroll::Autoscroll;
use editor::{Anchor, Editor, FoldPlaceholder, ToPoint};
use file_icons::FileIcons;
use fuzzy::PathMatch;
use gpui::{AppContext, DismissEvent, FocusHandle, FocusableView, Task, View, WeakModel, WeakView};
use gpui::{
AnyElement, App, DismissEvent, Empty, Entity, FocusHandle, Focusable, Stateful, Task,
WeakEntity,
};
use multi_buffer::{MultiBufferPoint, MultiBufferRow};
use picker::{Picker, PickerDelegate};
use project::{PathMatchCandidateSet, ProjectPath, WorktreeId};
use ui::{prelude::*, ListItem, Tooltip};
use rope::Point;
use text::SelectionGoal;
use ui::{prelude::*, ButtonLike, Disclosure, ElevationIndex, ListItem, Tooltip};
use util::ResultExt as _;
use workspace::Workspace;
use workspace::{notifications::NotifyResultExt, Workspace};
use crate::context_picker::{ConfirmBehavior, ContextPicker};
use crate::context_store::{ContextStore, IncludedFile};
use crate::context_store::{ContextStore, FileInclusion};
pub struct FileContextPicker {
picker: View<Picker<FileContextPickerDelegate>>,
picker: Entity<Picker<FileContextPickerDelegate>>,
}
impl FileContextPicker {
pub fn new(
context_picker: WeakView<ContextPicker>,
workspace: WeakView<Workspace>,
context_store: WeakModel<ContextStore>,
context_picker: WeakEntity<ContextPicker>,
workspace: WeakEntity<Workspace>,
editor: WeakEntity<Editor>,
context_store: WeakEntity<ContextStore>,
confirm_behavior: ConfirmBehavior,
cx: &mut ViewContext<Self>,
window: &mut Window,
cx: &mut Context<Self>,
) -> Self {
let delegate = FileContextPickerDelegate::new(
context_picker,
workspace,
editor,
context_store,
confirm_behavior,
);
let picker = cx.new_view(|cx| Picker::uniform_list(delegate, cx));
let picker = cx.new(|cx| Picker::uniform_list(delegate, window, cx));
Self { picker }
}
}
impl FocusableView for FileContextPicker {
fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
impl Focusable for FileContextPicker {
fn focus_handle(&self, cx: &App) -> 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 Context<Self>) -> impl IntoElement {
self.picker.clone()
}
}
pub struct FileContextPickerDelegate {
context_picker: WeakView<ContextPicker>,
workspace: WeakView<Workspace>,
context_store: WeakModel<ContextStore>,
context_picker: WeakEntity<ContextPicker>,
workspace: WeakEntity<Workspace>,
editor: WeakEntity<Editor>,
context_store: WeakEntity<ContextStore>,
confirm_behavior: ConfirmBehavior,
matches: Vec<PathMatch>,
selected_index: usize,
@@ -60,14 +77,16 @@ pub struct FileContextPickerDelegate {
impl FileContextPickerDelegate {
pub fn new(
context_picker: WeakView<ContextPicker>,
workspace: WeakView<Workspace>,
context_store: WeakModel<ContextStore>,
context_picker: WeakEntity<ContextPicker>,
workspace: WeakEntity<Workspace>,
editor: WeakEntity<Editor>,
context_store: WeakEntity<ContextStore>,
confirm_behavior: ConfirmBehavior,
) -> Self {
Self {
context_picker,
workspace,
editor,
context_store,
confirm_behavior,
matches: Vec::new(),
@@ -79,8 +98,9 @@ impl FileContextPickerDelegate {
&mut self,
query: String,
cancellation_flag: Arc<AtomicBool>,
workspace: &View<Workspace>,
cx: &mut ViewContext<Picker<Self>>,
workspace: &Entity<Workspace>,
cx: &mut Context<Picker<Self>>,
) -> Task<Vec<PathMatch>> {
if query.is_empty() {
let workspace = workspace.read(cx);
@@ -162,22 +182,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 Context<Picker<Self>>,
) {
self.selected_index = ix;
}
fn placeholder_text(&self, _cx: &mut WindowContext) -> Arc<str> {
fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> 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 Context<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;
@@ -188,96 +218,134 @@ impl PickerDelegate for FileContextPickerDelegate {
})
}
fn confirm(&mut self, _secondary: bool, cx: &mut ViewContext<Picker<Self>>) {
fn confirm(&mut self, _secondary: bool, window: &mut Window, cx: &mut Context<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())
let Some(file_name) = mat
.path
.file_name()
.map(|os_str| os_str.to_string_lossy().into_owned())
else {
return;
};
let path = mat.path.clone();
let already_included = self
.context_store
.update(cx, |context_store, _cx| {
match context_store.included_file(&path) {
Some(IncludedFile::Direct(context_id)) => {
context_store.remove_context(&context_id);
true
}
Some(IncludedFile::InDirectory(_)) => true,
None => false,
}
})
.unwrap_or(true);
if already_included {
let full_path = mat.path.display().to_string();
let project_path = ProjectPath {
worktree_id: WorktreeId::from_usize(mat.worktree_id),
path: mat.path.clone(),
};
let Some(editor) = self.editor.upgrade() else {
return;
}
};
let worktree_id = WorktreeId::from_usize(mat.worktree_id);
let confirm_behavior = self.confirm_behavior;
cx.spawn(|this, mut cx| async move {
let Some(open_buffer_task) = project
.update(&mut cx, |project, cx| {
let project_path = ProjectPath {
worktree_id,
path: path.clone(),
};
editor.update(cx, |editor, cx| {
editor.transact(window, cx, |editor, window, cx| {
// Move empty selections left by 1 column to select the `@`s, so they get overwritten when we insert.
{
let mut selections = editor.selections.all::<MultiBufferPoint>(cx);
let task = project.open_buffer(project_path, cx);
Some(task)
})
.ok()
.flatten()
else {
return anyhow::Ok(());
};
let result = open_buffer_task.await;
this.update(&mut cx, |this, cx| match result {
Ok(buffer) => {
this.delegate
.context_store
.update(cx, |context_store, cx| {
context_store.insert_file(buffer.read(cx));
})?;
match confirm_behavior {
ConfirmBehavior::KeepOpen => {}
ConfirmBehavior::Close => this.delegate.dismissed(cx),
for selection in selections.iter_mut() {
if selection.is_empty() {
let old_head = selection.head();
let new_head = MultiBufferPoint::new(
old_head.row,
old_head.column.saturating_sub(1),
);
selection.set_head(new_head, SelectionGoal::None);
}
}
anyhow::Ok(())
editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
s.select(selections)
});
}
Err(err) => {
let Some(workspace) = workspace.upgrade() else {
return anyhow::Ok(());
};
workspace.update(cx, |workspace, cx| {
workspace.show_error(&err, cx);
let start_anchors = {
let snapshot = editor.buffer().read(cx).snapshot(cx);
editor
.selections
.all::<Point>(cx)
.into_iter()
.map(|selection| snapshot.anchor_before(selection.start))
.collect::<Vec<_>>()
};
editor.insert(&full_path, window, cx);
let end_anchors = {
let snapshot = editor.buffer().read(cx).snapshot(cx);
editor
.selections
.all::<Point>(cx)
.into_iter()
.map(|selection| snapshot.anchor_after(selection.end))
.collect::<Vec<_>>()
};
editor.insert("\n", window, cx); // Needed to end the fold
let placeholder = FoldPlaceholder {
render: render_fold_icon_button(IconName::File, file_name.into()),
..Default::default()
};
let render_trailer =
move |_row, _unfold, _window: &mut Window, _cx: &mut App| Empty.into_any();
let buffer = editor.buffer().read(cx).snapshot(cx);
let mut rows_to_fold = BTreeSet::new();
let crease_iter = start_anchors
.into_iter()
.zip(end_anchors)
.map(|(start, end)| {
rows_to_fold.insert(MultiBufferRow(start.to_point(&buffer).row));
Crease::inline(
start..end,
placeholder.clone(),
fold_toggle("tool-use"),
render_trailer,
)
});
anyhow::Ok(())
}
})??;
editor.insert_creases(crease_iter, cx);
anyhow::Ok(())
for buffer_row in rows_to_fold {
editor.fold_at(&FoldAt { buffer_row }, window, cx);
}
});
});
let Some(task) = self
.context_store
.update(cx, |context_store, cx| {
context_store.add_file_from_path(project_path, cx)
})
.ok()
else {
return;
};
let confirm_behavior = self.confirm_behavior;
cx.spawn_in(window, |this, mut cx| async move {
match task.await.notify_async_err(&mut cx) {
None => anyhow::Ok(()),
Some(()) => this.update_in(&mut cx, |this, window, cx| match confirm_behavior {
ConfirmBehavior::KeepOpen => {}
ConfirmBehavior::Close => this.delegate.dismissed(window, cx),
}),
}
})
.detach_and_log_err(cx);
}
fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>) {
fn dismissed(&mut self, _: &mut Window, cx: &mut Context<Picker<Self>>) {
self.context_picker
.update(cx, |this, cx| {
this.reset_mode();
.update(cx, |_, cx| {
cx.emit(DismissEvent);
})
.ok();
@@ -287,80 +355,136 @@ impl PickerDelegate for FileContextPickerDelegate {
&self,
ix: usize,
selected: bool,
cx: &mut ViewContext<Picker<Self>>,
_window: &mut Window,
cx: &mut Context<Picker<Self>>,
) -> Option<Self::ListItem> {
let path_match = &self.matches[ix];
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))
};
let added = self
.context_store
.upgrade()
.and_then(|context_store| context_store.read(cx).included_file(&path_match.path));
Some(
ListItem::new(ix)
.inset(true)
.toggle_state(selected)
.child(
h_flex()
.gap_2()
.child(Label::new(file_name))
.children(directory.map(|directory| {
Label::new(directory)
.size(LabelSize::Small)
.color(Color::Muted)
})),
)
.when_some(added, |el, added| match added {
IncludedFile::Direct(_) => el.end_slot(
h_flex()
.gap_1()
.child(
Icon::new(IconName::Check)
.size(IconSize::Small)
.color(Color::Success),
)
.child(Label::new("Added").size(LabelSize::Small)),
),
IncludedFile::InDirectory(dir_name) => {
let dir_name = dir_name.to_string_lossy().into_owned();
el.end_slot(
h_flex()
.gap_1()
.child(
Icon::new(IconName::Check)
.size(IconSize::Small)
.color(Color::Success),
)
.child(Label::new("Included").size(LabelSize::Small)),
)
.tooltip(move |cx| Tooltip::text(format!("in {dir_name}"), cx))
}
}),
.child(render_file_context_entry(
ElementId::NamedInteger("file-ctx-picker".into(), ix),
&path_match.path,
&path_match.path_prefix,
self.context_store.clone(),
cx,
)),
)
}
}
pub fn render_file_context_entry(
id: ElementId,
path: &Path,
path_prefix: &Arc<str>,
context_store: WeakEntity<ContextStore>,
cx: &App,
) -> Stateful<Div> {
let (file_name, directory) = if path == Path::new("") {
(SharedString::from(path_prefix.clone()), None)
} else {
let file_name = path
.file_name()
.unwrap_or_default()
.to_string_lossy()
.to_string()
.into();
let mut directory = format!("{}/", path_prefix);
if let Some(parent) = path.parent().filter(|parent| parent != &Path::new("")) {
directory.push_str(&parent.to_string_lossy());
directory.push('/');
}
(file_name, Some(directory))
};
let added = context_store
.upgrade()
.and_then(|context_store| context_store.read(cx).will_include_file_path(path, cx));
let file_icon = FileIcons::get_icon(&path, cx)
.map(Icon::from_path)
.unwrap_or_else(|| Icon::new(IconName::File));
h_flex()
.id(id)
.gap_1p5()
.w_full()
.child(file_icon.size(IconSize::Small).color(Color::Muted))
.child(
h_flex()
.gap_1()
.child(Label::new(file_name))
.children(directory.map(|directory| {
Label::new(directory)
.size(LabelSize::Small)
.color(Color::Muted)
})),
)
.when_some(added, |el, added| match added {
FileInclusion::Direct(_) => el.child(
h_flex()
.w_full()
.justify_end()
.gap_0p5()
.child(
Icon::new(IconName::Check)
.size(IconSize::Small)
.color(Color::Success),
)
.child(Label::new("Added").size(LabelSize::Small)),
),
FileInclusion::InDirectory(dir_name) => {
let dir_name = dir_name.to_string_lossy().into_owned();
el.child(
h_flex()
.w_full()
.justify_end()
.gap_0p5()
.child(
Icon::new(IconName::Check)
.size(IconSize::Small)
.color(Color::Success),
)
.child(Label::new("Included").size(LabelSize::Small)),
)
.tooltip(Tooltip::text(format!("in {dir_name}")))
}
})
}
fn render_fold_icon_button(
icon: IconName,
label: SharedString,
) -> Arc<dyn Send + Sync + Fn(FoldId, Range<Anchor>, &mut Window, &mut App) -> AnyElement> {
Arc::new(move |fold_id, _fold_range, _window, _cx| {
ButtonLike::new(fold_id)
.style(ButtonStyle::Filled)
.layer(ElevationIndex::ElevatedSurface)
.child(Icon::new(icon))
.child(Label::new(label.clone()).single_line())
.into_any_element()
})
}
fn fold_toggle(
name: &'static str,
) -> impl Fn(
MultiBufferRow,
bool,
Arc<dyn Fn(bool, &mut Window, &mut App) + Send + Sync>,
&mut Window,
&mut App,
) -> AnyElement {
move |row, is_folded, fold, _window, _cx| {
Disclosure::new((name, row.0 as u64), !is_folded)
.toggle_state(is_folded)
.on_click(move |_e, window, cx| fold(!is_folded, window, cx))
.into_any_element()
}
}

View File

@@ -1,26 +1,27 @@
use std::sync::Arc;
use fuzzy::StringMatchCandidate;
use gpui::{AppContext, DismissEvent, FocusHandle, FocusableView, Task, View, WeakModel, WeakView};
use gpui::{App, DismissEvent, Entity, FocusHandle, Focusable, Task, WeakEntity};
use picker::{Picker, PickerDelegate};
use ui::{prelude::*, ListItem};
use crate::context_picker::{ConfirmBehavior, ContextPicker};
use crate::context_store;
use crate::context_store::{self, ContextStore};
use crate::thread::ThreadId;
use crate::thread_store::ThreadStore;
pub struct ThreadContextPicker {
picker: View<Picker<ThreadContextPickerDelegate>>,
picker: Entity<Picker<ThreadContextPickerDelegate>>,
}
impl ThreadContextPicker {
pub fn new(
thread_store: WeakModel<ThreadStore>,
context_picker: WeakView<ContextPicker>,
context_store: WeakModel<context_store::ContextStore>,
thread_store: WeakEntity<ThreadStore>,
context_picker: WeakEntity<ContextPicker>,
context_store: WeakEntity<context_store::ContextStore>,
confirm_behavior: ConfirmBehavior,
cx: &mut ViewContext<Self>,
window: &mut Window,
cx: &mut Context<Self>,
) -> Self {
let delegate = ThreadContextPickerDelegate::new(
thread_store,
@@ -28,34 +29,34 @@ impl ThreadContextPicker {
context_store,
confirm_behavior,
);
let picker = cx.new_view(|cx| Picker::uniform_list(delegate, cx));
let picker = cx.new(|cx| Picker::uniform_list(delegate, window, cx));
ThreadContextPicker { picker }
}
}
impl FocusableView for ThreadContextPicker {
fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
impl Focusable for ThreadContextPicker {
fn focus_handle(&self, cx: &App) -> 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 Context<Self>) -> impl IntoElement {
self.picker.clone()
}
}
#[derive(Debug, Clone)]
struct ThreadContextEntry {
id: ThreadId,
summary: SharedString,
pub struct ThreadContextEntry {
pub id: ThreadId,
pub summary: SharedString,
}
pub struct ThreadContextPickerDelegate {
thread_store: WeakModel<ThreadStore>,
context_picker: WeakView<ContextPicker>,
context_store: WeakModel<context_store::ContextStore>,
thread_store: WeakEntity<ThreadStore>,
context_picker: WeakEntity<ContextPicker>,
context_store: WeakEntity<context_store::ContextStore>,
confirm_behavior: ConfirmBehavior,
matches: Vec<ThreadContextEntry>,
selected_index: usize,
@@ -63,9 +64,9 @@ pub struct ThreadContextPickerDelegate {
impl ThreadContextPickerDelegate {
pub fn new(
thread_store: WeakModel<ThreadStore>,
context_picker: WeakView<ContextPicker>,
context_store: WeakModel<context_store::ContextStore>,
thread_store: WeakEntity<ThreadStore>,
context_picker: WeakEntity<ContextPicker>,
context_store: WeakEntity<context_store::ContextStore>,
confirm_behavior: ConfirmBehavior,
) -> Self {
ThreadContextPickerDelegate {
@@ -90,24 +91,31 @@ 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 Context<Picker<Self>>,
) {
self.selected_index = ix;
}
fn placeholder_text(&self, _cx: &mut WindowContext) -> Arc<str> {
fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> {
"Search threads…".into()
}
fn update_matches(&mut self, query: String, cx: &mut ViewContext<Picker<Self>>) -> Task<()> {
let Ok(threads) = self.thread_store.update(cx, |this, cx| {
this.threads(cx)
fn update_matches(
&mut self,
query: String,
window: &mut Window,
cx: &mut Context<Picker<Self>>,
) -> Task<()> {
let Ok(threads) = self.thread_store.update(cx, |this, _cx| {
this.threads()
.into_iter()
.map(|thread| {
const DEFAULT_SUMMARY: SharedString = SharedString::new_static("New Thread");
let id = thread.read(cx).id().clone();
let summary = thread.read(cx).summary().unwrap_or(DEFAULT_SUMMARY);
ThreadContextEntry { id, summary }
.map(|thread| ThreadContextEntry {
id: thread.id,
summary: thread.summary,
})
.collect::<Vec<_>>()
}) else {
@@ -141,7 +149,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;
@@ -152,7 +160,7 @@ impl PickerDelegate for ThreadContextPickerDelegate {
})
}
fn confirm(&mut self, _secondary: bool, cx: &mut ViewContext<Picker<Self>>) {
fn confirm(&mut self, _secondary: bool, window: &mut Window, cx: &mut Context<Picker<Self>>) {
let Some(entry) = self.matches.get(self.selected_index) else {
return;
};
@@ -161,31 +169,28 @@ impl PickerDelegate for ThreadContextPickerDelegate {
return;
};
let Some(thread) = thread_store.update(cx, |this, cx| this.open_thread(&entry.id, cx))
else {
return;
};
let open_thread_task = thread_store.update(cx, |this, cx| this.open_thread(&entry.id, cx));
self.context_store
.update(cx, |context_store, cx| {
if let Some(context_id) = context_store.included_thread(&entry.id) {
context_store.remove_context(&context_id);
} else {
context_store.insert_thread(thread.read(cx));
cx.spawn_in(window, |this, mut cx| async move {
let thread = open_thread_task.await?;
this.update_in(&mut cx, |this, window, cx| {
this.delegate
.context_store
.update(cx, |context_store, cx| context_store.add_thread(thread, cx))
.ok();
match this.delegate.confirm_behavior {
ConfirmBehavior::KeepOpen => {}
ConfirmBehavior::Close => this.delegate.dismissed(window, cx),
}
})
.ok();
match self.confirm_behavior {
ConfirmBehavior::KeepOpen => {}
ConfirmBehavior::Close => self.dismissed(cx),
}
})
.detach_and_log_err(cx);
}
fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>) {
fn dismissed(&mut self, _window: &mut Window, cx: &mut Context<Picker<Self>>) {
self.context_picker
.update(cx, |this, cx| {
this.reset_mode();
.update(cx, |_, cx| {
cx.emit(DismissEvent);
})
.ok();
@@ -195,31 +200,46 @@ impl PickerDelegate for ThreadContextPickerDelegate {
&self,
ix: usize,
selected: bool,
cx: &mut ViewContext<Picker<Self>>,
_window: &mut Window,
cx: &mut Context<Picker<Self>>,
) -> Option<Self::ListItem> {
let thread = &self.matches[ix];
let added = self.context_store.upgrade().map_or(false, |ctx_store| {
ctx_store.read(cx).included_thread(&thread.id).is_some()
});
Some(
ListItem::new(ix)
.inset(true)
.toggle_state(selected)
.child(Label::new(thread.summary.clone()))
.when(added, |el| {
el.end_slot(
h_flex()
.gap_1()
.child(
Icon::new(IconName::Check)
.size(IconSize::Small)
.color(Color::Success),
)
.child(Label::new("Added").size(LabelSize::Small)),
)
}),
)
Some(ListItem::new(ix).inset(true).toggle_state(selected).child(
render_thread_context_entry(thread, self.context_store.clone(), cx),
))
}
}
pub fn render_thread_context_entry(
thread: &ThreadContextEntry,
context_store: WeakEntity<ContextStore>,
cx: &mut App,
) -> Div {
let added = context_store.upgrade().map_or(false, |ctx_store| {
ctx_store.read(cx).includes_thread(&thread.id).is_some()
});
h_flex()
.gap_1p5()
.w_full()
.child(
Icon::new(IconName::MessageCircle)
.size(IconSize::XSmall)
.color(Color::Muted),
)
.child(Label::new(thread.summary.clone()))
.child(div().w_full())
.when(added, |el| {
el.child(
h_flex()
.gap_1()
.child(
Icon::new(IconName::Check)
.size(IconSize::Small)
.color(Color::Success),
)
.child(Label::new("Added").size(LabelSize::Small)),
)
})
}

View File

@@ -1,38 +1,54 @@
use std::fmt::Write as _;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use collections::{HashMap, HashSet};
use gpui::SharedString;
use anyhow::{anyhow, bail, Result};
use collections::{BTreeMap, HashMap, HashSet};
use futures::{self, future, Future, FutureExt};
use gpui::{App, AsyncApp, Context, Entity, SharedString, Task, WeakEntity};
use language::Buffer;
use project::{ProjectPath, Worktree};
use rope::Rope;
use text::BufferId;
use workspace::Workspace;
use crate::thread::Thread;
use crate::{
context::{Context, ContextId, ContextKind},
thread::ThreadId,
use crate::context::{
AssistantContext, ContextBuffer, ContextId, ContextSnapshot, DirectoryContext,
FetchedUrlContext, FileContext, ThreadContext,
};
use crate::context_strip::SuggestedContext;
use crate::thread::{Thread, ThreadId};
pub struct ContextStore {
context: Vec<Context>,
workspace: WeakEntity<Workspace>,
context: Vec<AssistantContext>,
// TODO: If an EntityId is used for all context types (like BufferId), can remove ContextId.
next_context_id: ContextId,
files: HashMap<PathBuf, ContextId>,
files: BTreeMap<BufferId, ContextId>,
directories: HashMap<PathBuf, ContextId>,
threads: HashMap<ThreadId, ContextId>,
fetched_urls: HashMap<String, ContextId>,
}
impl ContextStore {
pub fn new() -> Self {
pub fn new(workspace: WeakEntity<Workspace>) -> Self {
Self {
workspace,
context: Vec::new(),
next_context_id: ContextId(0),
files: HashMap::default(),
files: BTreeMap::default(),
directories: HashMap::default(),
threads: HashMap::default(),
fetched_urls: HashMap::default(),
}
}
pub fn context(&self) -> &Vec<Context> {
pub fn snapshot<'a>(&'a self, cx: &'a App) -> impl Iterator<Item = ContextSnapshot> + 'a {
self.context()
.iter()
.flat_map(|context| context.snapshot(cx))
}
pub fn context(&self) -> &Vec<AssistantContext> {
&self.context
}
@@ -44,121 +60,310 @@ impl ContextStore {
self.fetched_urls.clear();
}
pub fn insert_file(&mut self, buffer: &Buffer) {
let Some(file) = buffer.file() else {
return;
pub fn add_file_from_path(
&mut self,
project_path: ProjectPath,
cx: &mut Context<Self>,
) -> Task<Result<()>> {
let workspace = self.workspace.clone();
let Some(project) = workspace
.upgrade()
.map(|workspace| workspace.read(cx).project().clone())
else {
return Task::ready(Err(anyhow!("failed to read project")));
};
let path = file.path();
cx.spawn(|this, mut cx| async move {
let open_buffer_task = project.update(&mut cx, |project, cx| {
project.open_buffer(project_path.clone(), cx)
})?;
let id = self.next_context_id.post_inc();
self.files.insert(path.to_path_buf(), id);
let buffer_model = open_buffer_task.await?;
let buffer_id = this.update(&mut cx, |_, cx| buffer_model.read(cx).remote_id())?;
let full_path: SharedString = path.to_string_lossy().into_owned().into();
let already_included = this.update(&mut cx, |this, _cx| {
match this.will_include_buffer(buffer_id, &project_path.path) {
Some(FileInclusion::Direct(context_id)) => {
this.remove_context(context_id);
true
}
Some(FileInclusion::InDirectory(_)) => true,
None => false,
}
})?;
let name = match path.file_name() {
Some(name) => name.to_string_lossy().into_owned().into(),
None => full_path.clone(),
};
if already_included {
return anyhow::Ok(());
}
let parent = path
.parent()
.and_then(|p| p.file_name())
.map(|p| p.to_string_lossy().into_owned().into());
let (buffer_info, text_task) = this.update(&mut cx, |_, cx| {
let buffer = buffer_model.read(cx);
collect_buffer_info_and_text(
project_path.path.clone(),
buffer_model,
buffer,
cx.to_async(),
)
})?;
let mut text = String::new();
push_fenced_codeblock(path, buffer.text(), &mut text);
let text = text_task.await;
self.context.push(Context {
id,
name,
parent,
tooltip: Some(full_path),
kind: ContextKind::File,
text: text.into(),
});
this.update(&mut cx, |this, _cx| {
this.insert_file(make_context_buffer(buffer_info, text));
})?;
anyhow::Ok(())
})
}
pub fn insert_directory(&mut self, path: &Path, text: impl Into<SharedString>) {
pub fn add_file_from_buffer(
&mut self,
buffer_model: Entity<Buffer>,
cx: &mut Context<Self>,
) -> Task<Result<()>> {
cx.spawn(|this, mut cx| async move {
let (buffer_info, text_task) = this.update(&mut cx, |_, cx| {
let buffer = buffer_model.read(cx);
let Some(file) = buffer.file() else {
return Err(anyhow!("Buffer has no path."));
};
Ok(collect_buffer_info_and_text(
file.path().clone(),
buffer_model,
buffer,
cx.to_async(),
))
})??;
let text = text_task.await;
this.update(&mut cx, |this, _cx| {
this.insert_file(make_context_buffer(buffer_info, text))
})?;
anyhow::Ok(())
})
}
fn insert_file(&mut self, context_buffer: ContextBuffer) {
let id = self.next_context_id.post_inc();
self.files.insert(context_buffer.id, id);
self.context
.push(AssistantContext::File(FileContext { id, context_buffer }));
}
pub fn add_directory(
&mut self,
project_path: ProjectPath,
cx: &mut Context<Self>,
) -> Task<Result<()>> {
let workspace = self.workspace.clone();
let Some(project) = workspace
.upgrade()
.map(|workspace| workspace.read(cx).project().clone())
else {
return Task::ready(Err(anyhow!("failed to read project")));
};
let already_included = if let Some(context_id) = self.includes_directory(&project_path.path)
{
self.remove_context(context_id);
true
} else {
false
};
if already_included {
return Task::ready(Ok(()));
}
let worktree_id = project_path.worktree_id;
cx.spawn(|this, mut cx| async move {
let worktree = project.update(&mut cx, |project, cx| {
project
.worktree_for_id(worktree_id, cx)
.ok_or_else(|| anyhow!("no worktree found for {worktree_id:?}"))
})??;
let files = worktree.update(&mut cx, |worktree, _cx| {
collect_files_in_path(worktree, &project_path.path)
})?;
let open_buffers_task = project.update(&mut cx, |project, cx| {
let tasks = files.iter().map(|file_path| {
project.open_buffer(
ProjectPath {
worktree_id,
path: file_path.clone(),
},
cx,
)
});
future::join_all(tasks)
})?;
let buffers = open_buffers_task.await;
let mut buffer_infos = Vec::new();
let mut text_tasks = Vec::new();
this.update(&mut cx, |_, cx| {
for (path, buffer_model) in files.into_iter().zip(buffers) {
let buffer_model = buffer_model?;
let buffer = buffer_model.read(cx);
let (buffer_info, text_task) =
collect_buffer_info_and_text(path, buffer_model, buffer, cx.to_async());
buffer_infos.push(buffer_info);
text_tasks.push(text_task);
}
anyhow::Ok(())
})??;
let buffer_texts = future::join_all(text_tasks).await;
let context_buffers = buffer_infos
.into_iter()
.zip(buffer_texts)
.map(|(info, text)| make_context_buffer(info, text))
.collect::<Vec<_>>();
if context_buffers.is_empty() {
bail!("No text files found in {}", &project_path.path.display());
}
this.update(&mut cx, |this, _| {
this.insert_directory(&project_path.path, context_buffers);
})?;
anyhow::Ok(())
})
}
fn insert_directory(&mut self, path: &Path, context_buffers: Vec<ContextBuffer>) {
let id = self.next_context_id.post_inc();
self.directories.insert(path.to_path_buf(), id);
let full_path: SharedString = path.to_string_lossy().into_owned().into();
let name = match path.file_name() {
Some(name) => name.to_string_lossy().into_owned().into(),
None => full_path.clone(),
};
let parent = path
.parent()
.and_then(|p| p.file_name())
.map(|p| p.to_string_lossy().into_owned().into());
self.context.push(Context {
id,
name,
parent,
tooltip: Some(full_path),
kind: ContextKind::Directory,
text: text.into(),
});
self.context
.push(AssistantContext::Directory(DirectoryContext::new(
id,
path,
context_buffers,
)));
}
pub fn insert_thread(&mut self, thread: &Thread) {
let context_id = self.next_context_id.post_inc();
self.threads.insert(thread.id().clone(), context_id);
self.context.push(Context {
id: context_id,
name: thread.summary().unwrap_or("New thread".into()),
parent: None,
tooltip: None,
kind: ContextKind::Thread,
text: thread.text().into(),
});
pub fn add_thread(&mut self, thread: Entity<Thread>, cx: &mut Context<Self>) {
if let Some(context_id) = self.includes_thread(&thread.read(cx).id()) {
self.remove_context(context_id);
} else {
self.insert_thread(thread, cx);
}
}
pub fn insert_fetched_url(&mut self, url: String, text: impl Into<SharedString>) {
let context_id = self.next_context_id.post_inc();
self.fetched_urls.insert(url.clone(), context_id);
fn insert_thread(&mut self, thread: Entity<Thread>, cx: &App) {
let id = self.next_context_id.post_inc();
let text = thread.read(cx).text().into();
self.context.push(Context {
id: context_id,
name: url.into(),
parent: None,
tooltip: None,
kind: ContextKind::FetchedUrl,
text: text.into(),
});
self.threads.insert(thread.read(cx).id().clone(), id);
self.context
.push(AssistantContext::Thread(ThreadContext { id, thread, text }));
}
pub fn remove_context(&mut self, id: &ContextId) {
let Some(ix) = self.context.iter().position(|context| context.id == *id) else {
pub fn add_fetched_url(&mut self, url: String, text: impl Into<SharedString>) {
if self.includes_url(&url).is_none() {
self.insert_fetched_url(url, text);
}
}
fn insert_fetched_url(&mut self, url: String, text: impl Into<SharedString>) {
let id = self.next_context_id.post_inc();
self.fetched_urls.insert(url.clone(), id);
self.context
.push(AssistantContext::FetchedUrl(FetchedUrlContext {
id,
url: url.into(),
text: text.into(),
}));
}
pub fn accept_suggested_context(
&mut self,
suggested: &SuggestedContext,
cx: &mut Context<ContextStore>,
) -> Task<Result<()>> {
match suggested {
SuggestedContext::File {
buffer,
icon_path: _,
name: _,
} => {
if let Some(buffer) = buffer.upgrade() {
return self.add_file_from_buffer(buffer, cx);
};
}
SuggestedContext::Thread { thread, name: _ } => {
if let Some(thread) = thread.upgrade() {
self.insert_thread(thread, cx);
};
}
}
Task::ready(Ok(()))
}
pub fn remove_context(&mut self, id: ContextId) {
let Some(ix) = self.context.iter().position(|context| context.id() == id) else {
return;
};
match self.context.remove(ix).kind {
ContextKind::File => {
self.files.retain(|_, context_id| context_id != id);
match self.context.remove(ix) {
AssistantContext::File(_) => {
self.files.retain(|_, context_id| *context_id != id);
}
ContextKind::Directory => {
self.directories.retain(|_, context_id| context_id != id);
AssistantContext::Directory(_) => {
self.directories.retain(|_, context_id| *context_id != id);
}
ContextKind::FetchedUrl => {
self.fetched_urls.retain(|_, context_id| context_id != id);
AssistantContext::FetchedUrl(_) => {
self.fetched_urls.retain(|_, context_id| *context_id != id);
}
ContextKind::Thread => {
self.threads.retain(|_, context_id| context_id != id);
AssistantContext::Thread(_) => {
self.threads.retain(|_, context_id| *context_id != id);
}
}
}
pub fn included_file(&self, path: &Path) -> Option<IncludedFile> {
if let Some(id) = self.files.get(path) {
return Some(IncludedFile::Direct(*id));
/// Returns whether the buffer is already included directly in the context, or if it will be
/// included in the context via a directory. Directory inclusion is based on paths rather than
/// buffer IDs as the directory will be re-scanned.
pub fn will_include_buffer(&self, buffer_id: BufferId, path: &Path) -> Option<FileInclusion> {
if let Some(context_id) = self.files.get(&buffer_id) {
return Some(FileInclusion::Direct(*context_id));
}
self.will_include_file_path_via_directory(path)
}
/// Returns whether this file path is already included directly in the context, or if it will be
/// included in the context via a directory.
pub fn will_include_file_path(&self, path: &Path, cx: &App) -> Option<FileInclusion> {
if !self.files.is_empty() {
let found_file_context = self.context.iter().find(|context| match &context {
AssistantContext::File(file_context) => {
let buffer = file_context.context_buffer.buffer.read(cx);
if let Some(file_path) = buffer_path_log_err(buffer) {
*file_path == *path
} else {
false
}
}
_ => false,
});
if let Some(context) = found_file_context {
return Some(FileInclusion::Direct(context.id()));
}
}
self.will_include_file_path_via_directory(path)
}
fn will_include_file_path_via_directory(&self, path: &Path) -> Option<FileInclusion> {
if self.directories.is_empty() {
return None;
}
@@ -167,61 +372,288 @@ impl ContextStore {
while buf.pop() {
if let Some(_) = self.directories.get(&buf) {
return Some(IncludedFile::InDirectory(buf));
return Some(FileInclusion::InDirectory(buf));
}
}
None
}
pub fn included_directory(&self, path: &Path) -> Option<ContextId> {
pub fn includes_directory(&self, path: &Path) -> Option<ContextId> {
self.directories.get(path).copied()
}
pub fn included_thread(&self, thread_id: &ThreadId) -> Option<ContextId> {
pub fn includes_thread(&self, thread_id: &ThreadId) -> Option<ContextId> {
self.threads.get(thread_id).copied()
}
pub fn included_url(&self, url: &str) -> Option<ContextId> {
pub fn includes_url(&self, url: &str) -> Option<ContextId> {
self.fetched_urls.get(url).copied()
}
pub fn duplicated_names(&self) -> HashSet<SharedString> {
let mut seen = HashSet::default();
let mut dupes = HashSet::default();
for context in self.context().iter() {
if !seen.insert(&context.name) {
dupes.insert(context.name.clone());
/// Replaces the context that matches the ID of the new context, if any match.
fn replace_context(&mut self, new_context: AssistantContext) {
let id = new_context.id();
for context in self.context.iter_mut() {
if context.id() == id {
*context = new_context;
break;
}
}
}
dupes
pub fn file_paths(&self, cx: &App) -> HashSet<PathBuf> {
self.context
.iter()
.filter_map(|context| match context {
AssistantContext::File(file) => {
let buffer = file.context_buffer.buffer.read(cx);
buffer_path_log_err(buffer).map(|p| p.to_path_buf())
}
AssistantContext::Directory(_)
| AssistantContext::FetchedUrl(_)
| AssistantContext::Thread(_) => None,
})
.collect()
}
pub fn thread_ids(&self) -> HashSet<ThreadId> {
self.threads.keys().cloned().collect()
}
}
pub enum IncludedFile {
pub enum FileInclusion {
Direct(ContextId),
InDirectory(PathBuf),
}
pub(crate) fn push_fenced_codeblock(path: &Path, content: String, buffer: &mut String) {
buffer.reserve(content.len() + 64);
// ContextBuffer without text.
struct BufferInfo {
buffer_model: Entity<Buffer>,
id: BufferId,
version: clock::Global,
}
write!(buffer, "```").unwrap();
if let Some(extension) = path.extension().and_then(|ext| ext.to_str()) {
write!(buffer, "{} ", extension).unwrap();
fn make_context_buffer(info: BufferInfo, text: SharedString) -> ContextBuffer {
ContextBuffer {
id: info.id,
buffer: info.buffer_model,
version: info.version,
text,
}
}
write!(buffer, "{}", path.display()).unwrap();
fn collect_buffer_info_and_text(
path: Arc<Path>,
buffer_model: Entity<Buffer>,
buffer: &Buffer,
cx: AsyncApp,
) -> (BufferInfo, Task<SharedString>) {
let buffer_info = BufferInfo {
id: buffer.remote_id(),
buffer_model,
version: buffer.version(),
};
// Important to collect version at the same time as content so that staleness logic is correct.
let content = buffer.as_rope().clone();
let text_task = cx
.background_executor()
.spawn(async move { to_fenced_codeblock(&path, content) });
(buffer_info, text_task)
}
pub fn buffer_path_log_err(buffer: &Buffer) -> Option<Arc<Path>> {
if let Some(file) = buffer.file() {
Some(file.path().clone())
} else {
log::error!("Buffer that had a path unexpectedly no longer has a path.");
None
}
}
fn to_fenced_codeblock(path: &Path, content: Rope) -> SharedString {
let path_extension = path.extension().and_then(|ext| ext.to_str());
let path_string = path.to_string_lossy();
let capacity = 3
+ path_extension.map_or(0, |extension| extension.len() + 1)
+ path_string.len()
+ 1
+ content.len()
+ 5;
let mut buffer = String::with_capacity(capacity);
buffer.push_str("```");
if let Some(extension) = path_extension {
buffer.push_str(extension);
buffer.push(' ');
}
buffer.push_str(&path_string);
buffer.push('\n');
buffer.push_str(&content);
for chunk in content.chunks() {
buffer.push_str(&chunk);
}
if !buffer.ends_with('\n') {
buffer.push('\n');
}
buffer.push_str("```\n");
debug_assert!(
buffer.len() == capacity - 1 || buffer.len() == capacity,
"to_fenced_codeblock calculated capacity of {}, but length was {}",
capacity,
buffer.len(),
);
buffer.into()
}
fn collect_files_in_path(worktree: &Worktree, path: &Path) -> Vec<Arc<Path>> {
let mut files = Vec::new();
for entry in worktree.child_entries(path) {
if entry.is_dir() {
files.extend(collect_files_in_path(worktree, &entry.path));
} else if entry.is_file() {
files.push(entry.path.clone());
}
}
files
}
pub fn refresh_context_store_text(
context_store: Entity<ContextStore>,
cx: &App,
) -> impl Future<Output = ()> {
let mut tasks = Vec::new();
for context in &context_store.read(cx).context {
match context {
AssistantContext::File(file_context) => {
let context_store = context_store.clone();
if let Some(task) = refresh_file_text(context_store, file_context, cx) {
tasks.push(task);
}
}
AssistantContext::Directory(directory_context) => {
let context_store = context_store.clone();
if let Some(task) = refresh_directory_text(context_store, directory_context, cx) {
tasks.push(task);
}
}
AssistantContext::Thread(thread_context) => {
let context_store = context_store.clone();
tasks.push(refresh_thread_text(context_store, thread_context, cx));
}
// Intentionally omit refreshing fetched URLs as it doesn't seem all that useful,
// and doing the caching properly could be tricky (unless it's already handled by
// the HttpClient?).
AssistantContext::FetchedUrl(_) => {}
}
}
future::join_all(tasks).map(|_| ())
}
fn refresh_file_text(
context_store: Entity<ContextStore>,
file_context: &FileContext,
cx: &App,
) -> Option<Task<()>> {
let id = file_context.id;
let task = refresh_context_buffer(&file_context.context_buffer, cx);
if let Some(task) = task {
Some(cx.spawn(|mut cx| async move {
let context_buffer = task.await;
context_store
.update(&mut cx, |context_store, _| {
let new_file_context = FileContext { id, context_buffer };
context_store.replace_context(AssistantContext::File(new_file_context));
})
.ok();
}))
} else {
None
}
}
fn refresh_directory_text(
context_store: Entity<ContextStore>,
directory_context: &DirectoryContext,
cx: &App,
) -> Option<Task<()>> {
let mut stale = false;
let futures = directory_context
.context_buffers
.iter()
.map(|context_buffer| {
if let Some(refresh_task) = refresh_context_buffer(context_buffer, cx) {
stale = true;
future::Either::Left(refresh_task)
} else {
future::Either::Right(future::ready((*context_buffer).clone()))
}
})
.collect::<Vec<_>>();
if !stale {
return None;
}
let context_buffers = future::join_all(futures);
let id = directory_context.snapshot.id;
let path = directory_context.path.clone();
Some(cx.spawn(|mut cx| async move {
let context_buffers = context_buffers.await;
context_store
.update(&mut cx, |context_store, _| {
let new_directory_context = DirectoryContext::new(id, &path, context_buffers);
context_store.replace_context(AssistantContext::Directory(new_directory_context));
})
.ok();
}))
}
fn refresh_thread_text(
context_store: Entity<ContextStore>,
thread_context: &ThreadContext,
cx: &App,
) -> Task<()> {
let id = thread_context.id;
let thread = thread_context.thread.clone();
cx.spawn(move |mut cx| async move {
context_store
.update(&mut cx, |context_store, cx| {
let text = thread.read(cx).text().into();
context_store.replace_context(AssistantContext::Thread(ThreadContext {
id,
thread,
text,
}));
})
.ok();
})
}
fn refresh_context_buffer(
context_buffer: &ContextBuffer,
cx: &App,
) -> Option<impl Future<Output = ContextBuffer>> {
let buffer = context_buffer.buffer.read(cx);
let path = buffer_path_log_err(buffer)?;
if buffer.version.changed_since(&context_buffer.version) {
let (buffer_info, text_task) = collect_buffer_info_and_text(
path,
context_buffer.buffer.clone(),
buffer,
cx.to_async(),
);
Some(text_task.map(move |text| make_context_buffer(buffer_info, text)))
} else {
None
}
}

View File

@@ -1,13 +1,16 @@
use std::rc::Rc;
use collections::HashSet;
use editor::Editor;
use file_icons::FileIcons;
use gpui::{
AppContext, DismissEvent, EventEmitter, FocusHandle, Model, Subscription, View, WeakModel,
WeakView,
App, Bounds, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, Subscription,
WeakEntity,
};
use itertools::Itertools;
use language::Buffer;
use ui::{prelude::*, KeyBinding, PopoverMenu, PopoverMenuHandle, Tooltip};
use workspace::Workspace;
use workspace::{notifications::NotifyResultExt, Workspace};
use crate::context::ContextKind;
use crate::context_picker::{ConfirmBehavior, ContextPicker};
@@ -15,40 +18,54 @@ use crate::context_store::ContextStore;
use crate::thread::Thread;
use crate::thread_store::ThreadStore;
use crate::ui::ContextPill;
use crate::{AssistantPanel, RemoveAllContext, ToggleContextPicker};
use crate::{
AcceptSuggestedContext, AssistantPanel, FocusDown, FocusLeft, FocusRight, FocusUp,
RemoveAllContext, RemoveFocusedContext, ToggleContextPicker,
};
pub struct ContextStrip {
context_store: Model<ContextStore>,
context_picker: View<ContextPicker>,
context_store: Entity<ContextStore>,
pub context_picker: Entity<ContextPicker>,
context_picker_menu_handle: PopoverMenuHandle<ContextPicker>,
focus_handle: FocusHandle,
suggest_context_kind: SuggestContextKind,
workspace: WeakView<Workspace>,
_context_picker_subscription: Subscription,
workspace: WeakEntity<Workspace>,
_subscriptions: Vec<Subscription>,
focused_index: Option<usize>,
children_bounds: Option<Vec<Bounds<Pixels>>>,
}
impl ContextStrip {
#[allow(clippy::too_many_arguments)]
pub fn new(
context_store: Model<ContextStore>,
workspace: WeakView<Workspace>,
thread_store: Option<WeakModel<ThreadStore>>,
focus_handle: FocusHandle,
context_store: Entity<ContextStore>,
workspace: WeakEntity<Workspace>,
editor: WeakEntity<Editor>,
thread_store: Option<WeakEntity<ThreadStore>>,
context_picker_menu_handle: PopoverMenuHandle<ContextPicker>,
suggest_context_kind: SuggestContextKind,
cx: &mut ViewContext<Self>,
window: &mut Window,
cx: &mut Context<Self>,
) -> Self {
let context_picker = cx.new_view(|cx| {
let context_picker = cx.new(|cx| {
ContextPicker::new(
workspace.clone(),
thread_store.clone(),
context_store.downgrade(),
editor.clone(),
ConfirmBehavior::KeepOpen,
window,
cx,
)
});
let context_picker_subscription =
cx.subscribe(&context_picker, Self::handle_context_picker_event);
let focus_handle = cx.focus_handle();
let subscriptions = vec![
cx.subscribe_in(&context_picker, window, Self::handle_context_picker_event),
cx.on_focus(&focus_handle, window, Self::handle_focus),
cx.on_blur(&focus_handle, window, Self::handle_blur),
];
Self {
context_store: context_store.clone(),
@@ -57,27 +74,35 @@ impl ContextStrip {
focus_handle,
suggest_context_kind,
workspace,
_context_picker_subscription: context_picker_subscription,
_subscriptions: subscriptions,
focused_index: None,
children_bounds: None,
}
}
fn suggested_context(&self, cx: &ViewContext<Self>) -> Option<SuggestedContext> {
fn suggested_context(&self, cx: &Context<Self>) -> Option<SuggestedContext> {
match self.suggest_context_kind {
SuggestContextKind::File => self.suggested_file(cx),
SuggestContextKind::Thread => self.suggested_thread(cx),
}
}
fn suggested_file(&self, cx: &ViewContext<Self>) -> Option<SuggestedContext> {
fn suggested_file(&self, cx: &Context<Self>) -> Option<SuggestedContext> {
let workspace = self.workspace.upgrade()?;
let active_item = workspace.read(cx).active_item(cx)?;
let editor = active_item.to_any().downcast::<Editor>().ok()?.read(cx);
let active_buffer = editor.buffer().read(cx).as_singleton()?;
let active_buffer_model = editor.buffer().read(cx).as_singleton()?;
let active_buffer = active_buffer_model.read(cx);
let path = active_buffer.read(cx).file()?.path();
let path = active_buffer.file()?.path();
if self.context_store.read(cx).included_file(path).is_some() {
if self
.context_store
.read(cx)
.will_include_buffer(active_buffer.remote_id(), path)
.is_some()
{
return None;
}
@@ -86,13 +111,20 @@ impl ContextStrip {
None => path.to_string_lossy().into_owned().into(),
};
let icon_path = FileIcons::get_icon(path, cx);
Some(SuggestedContext::File {
name,
buffer: active_buffer.downgrade(),
buffer: active_buffer_model.downgrade(),
icon_path,
})
}
fn suggested_thread(&self, cx: &ViewContext<Self>) -> Option<SuggestedContext> {
fn suggested_thread(&self, cx: &Context<Self>) -> Option<SuggestedContext> {
if !self.context_picker.read(cx).allow_threads() {
return None;
}
let workspace = self.workspace.upgrade()?;
let active_thread = workspace
.read(cx)
@@ -106,57 +138,291 @@ impl ContextStrip {
if self
.context_store
.read(cx)
.included_thread(active_thread.id())
.includes_thread(active_thread.id())
.is_some()
{
return None;
}
Some(SuggestedContext::Thread {
name: active_thread.summary().unwrap_or("New Thread".into()),
name: active_thread.summary_or_default(),
thread: weak_active_thread,
})
}
fn handle_context_picker_event(
&mut self,
_picker: View<ContextPicker>,
_picker: &Entity<ContextPicker>,
_event: &DismissEvent,
cx: &mut ViewContext<Self>,
_window: &mut Window,
cx: &mut Context<Self>,
) {
cx.emit(ContextStripEvent::PickerDismissed);
}
fn handle_focus(&mut self, _window: &mut Window, cx: &mut Context<Self>) {
self.focused_index = self.last_pill_index();
cx.notify();
}
fn handle_blur(&mut self, _window: &mut Window, cx: &mut Context<Self>) {
self.focused_index = None;
cx.notify();
}
fn focus_left(&mut self, _: &FocusLeft, _window: &mut Window, cx: &mut Context<Self>) {
self.focused_index = match self.focused_index {
Some(index) if index > 0 => Some(index - 1),
_ => self.last_pill_index(),
};
cx.notify();
}
fn focus_right(&mut self, _: &FocusRight, _window: &mut Window, cx: &mut Context<Self>) {
let Some(last_index) = self.last_pill_index() else {
return;
};
self.focused_index = match self.focused_index {
Some(index) if index < last_index => Some(index + 1),
_ => Some(0),
};
cx.notify();
}
fn focus_up(&mut self, _: &FocusUp, _window: &mut Window, cx: &mut Context<Self>) {
let Some(focused_index) = self.focused_index else {
return;
};
if focused_index == 0 {
return cx.emit(ContextStripEvent::BlurredUp);
}
let Some((focused, pills)) = self.focused_bounds(focused_index) else {
return;
};
let iter = pills[..focused_index].iter().enumerate().rev();
self.focused_index = Self::find_best_horizontal_match(focused, iter).or(Some(0));
cx.notify();
}
fn focus_down(&mut self, _: &FocusDown, _window: &mut Window, cx: &mut Context<Self>) {
let Some(focused_index) = self.focused_index else {
return;
};
let last_index = self.last_pill_index();
if self.focused_index == last_index {
return cx.emit(ContextStripEvent::BlurredDown);
}
let Some((focused, pills)) = self.focused_bounds(focused_index) else {
return;
};
let iter = pills.iter().enumerate().skip(focused_index + 1);
self.focused_index = Self::find_best_horizontal_match(focused, iter).or(last_index);
cx.notify();
}
fn focused_bounds(&self, focused: usize) -> Option<(&Bounds<Pixels>, &[Bounds<Pixels>])> {
let pill_bounds = self.pill_bounds()?;
let focused = pill_bounds.get(focused)?;
Some((focused, pill_bounds))
}
fn pill_bounds(&self) -> Option<&[Bounds<Pixels>]> {
let bounds = self.children_bounds.as_ref()?;
let eraser = if bounds.len() < 3 { 0 } else { 1 };
let pills = &bounds[1..bounds.len() - eraser];
if pills.is_empty() {
None
} else {
Some(pills)
}
}
fn last_pill_index(&self) -> Option<usize> {
Some(self.pill_bounds()?.len() - 1)
}
fn find_best_horizontal_match<'a>(
focused: &'a Bounds<Pixels>,
iter: impl Iterator<Item = (usize, &'a Bounds<Pixels>)>,
) -> Option<usize> {
let mut best = None;
let focused_left = focused.left();
let focused_right = focused.right();
for (index, probe) in iter {
if probe.origin.y == focused.origin.y {
continue;
}
let overlap = probe.right().min(focused_right) - probe.left().max(focused_left);
best = match best {
Some((_, prev_overlap, y)) if probe.origin.y != y || prev_overlap > overlap => {
break;
}
Some(_) | None => Some((index, overlap, probe.origin.y)),
};
}
best.map(|(index, _, _)| index)
}
fn remove_focused_context(
&mut self,
_: &RemoveFocusedContext,
_window: &mut Window,
cx: &mut Context<Self>,
) {
if let Some(index) = self.focused_index {
let mut is_empty = false;
self.context_store.update(cx, |this, _cx| {
if let Some(item) = this.context().get(index) {
this.remove_context(item.id());
}
is_empty = this.context().is_empty();
});
if is_empty {
cx.emit(ContextStripEvent::BlurredEmpty);
} else {
self.focused_index = Some(index.saturating_sub(1));
cx.notify();
}
}
}
fn is_suggested_focused<T>(&self, context: &Vec<T>) -> bool {
// We only suggest one item after the actual context
self.focused_index == Some(context.len())
}
fn accept_suggested_context(
&mut self,
_: &AcceptSuggestedContext,
window: &mut Window,
cx: &mut Context<Self>,
) {
if let Some(suggested) = self.suggested_context(cx) {
let context_store = self.context_store.read(cx);
if self.is_suggested_focused(context_store.context()) {
self.add_suggested_context(&suggested, window, cx);
}
}
}
fn add_suggested_context(
&mut self,
suggested: &SuggestedContext,
window: &mut Window,
cx: &mut Context<Self>,
) {
let task = self.context_store.update(cx, |context_store, cx| {
context_store.accept_suggested_context(&suggested, cx)
});
cx.spawn_in(window, |this, mut cx| async move {
match task.await.notify_async_err(&mut cx) {
None => {}
Some(()) => {
if let Some(this) = this.upgrade() {
this.update(&mut cx, |_, cx| cx.notify())?;
}
}
}
anyhow::Ok(())
})
.detach_and_log_err(cx);
cx.notify();
}
}
impl Focusable for ContextStrip {
fn focus_handle(&self, _cx: &App) -> FocusHandle {
self.focus_handle.clone()
}
}
impl Render for ContextStrip {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let context_store = self.context_store.read(cx);
let context = context_store.context().clone();
let context = context_store
.context()
.iter()
.flat_map(|context| context.snapshot(cx))
.collect::<Vec<_>>();
let context_picker = self.context_picker.clone();
let focus_handle = self.focus_handle.clone();
let suggested_context = self.suggested_context(cx);
let dupe_names = context_store.duplicated_names();
let dupe_names = context
.iter()
.map(|context| context.name.clone())
.sorted()
.tuple_windows()
.filter(|(a, b)| a == b)
.map(|(a, _)| a)
.collect::<HashSet<SharedString>>();
h_flex()
.flex_wrap()
.gap_1()
.track_focus(&focus_handle)
.key_context("ContextStrip")
.on_action(cx.listener(Self::focus_up))
.on_action(cx.listener(Self::focus_right))
.on_action(cx.listener(Self::focus_down))
.on_action(cx.listener(Self::focus_left))
.on_action(cx.listener(Self::remove_focused_context))
.on_action(cx.listener(Self::accept_suggested_context))
.on_children_prepainted({
let model = cx.entity().downgrade();
move |children_bounds, _window, cx| {
model
.update(cx, |this, _| {
this.children_bounds = Some(children_bounds);
})
.ok();
}
})
.child(
PopoverMenu::new("context-picker")
.menu(move |_cx| Some(context_picker.clone()))
.menu(move |window, cx| {
context_picker.update(cx, |this, cx| {
this.init(window, cx);
});
Some(context_picker.clone())
})
.trigger(
IconButton::new("add-context", IconName::Plus)
.icon_size(IconSize::Small)
.style(ui::ButtonStyle::Filled)
.tooltip({
let focus_handle = focus_handle.clone();
move |cx| {
move |window, cx| {
Tooltip::for_action_in(
"Add Context",
&ToggleContextPicker,
&focus_handle,
window,
cx,
)
}
@@ -166,7 +432,7 @@ impl Render for ContextStrip {
.anchor(gpui::Corner::BottomLeft)
.offset(gpui::Point {
x: px(0.0),
y: px(-16.0),
y: px(-2.0),
})
.with_handle(self.context_picker_menu_handle.clone()),
)
@@ -183,43 +449,51 @@ impl Render for ContextStrip {
)
.opacity(0.5)
.children(
KeyBinding::for_action_in(&ToggleContextPicker, &focus_handle, cx)
.map(|binding| binding.into_any_element()),
KeyBinding::for_action_in(
&ToggleContextPicker,
&focus_handle,
window,
)
.map(|binding| binding.into_any_element()),
),
)
}
})
.children(context.iter().map(|context| {
ContextPill::new_added(
.children(context.iter().enumerate().map(|(i, context)| {
ContextPill::added(
context.clone(),
dupe_names.contains(&context.name),
self.focused_index == Some(i),
Some({
let context = context.clone();
let id = context.id;
let context_store = self.context_store.clone();
Rc::new(cx.listener(move |_this, _event, cx| {
Rc::new(cx.listener(move |_this, _event, _window, cx| {
context_store.update(cx, |this, _cx| {
this.remove_context(&context.id);
this.remove_context(id);
});
cx.notify();
}))
}),
)
.on_click(Rc::new(cx.listener(move |this, _, _window, cx| {
this.focused_index = Some(i);
cx.notify();
})))
}))
.when_some(suggested_context, |el, suggested| {
el.child(ContextPill::new_suggested(
suggested.name().clone(),
suggested.kind(),
{
let context_store = self.context_store.clone();
Rc::new(cx.listener(move |_this, _event, cx| {
context_store.update(cx, |context_store, cx| {
suggested.accept(context_store, cx);
});
cx.notify();
}))
},
))
el.child(
ContextPill::suggested(
suggested.name().clone(),
suggested.icon_path(),
suggested.kind(),
self.is_suggested_focused(&context),
)
.on_click(Rc::new(cx.listener(
move |this, _event, window, cx| {
this.add_suggested_context(&suggested, window, cx);
},
))),
)
})
.when(!context.is_empty(), {
move |parent| {
@@ -228,19 +502,20 @@ impl Render for ContextStrip {
.icon_size(IconSize::Small)
.tooltip({
let focus_handle = focus_handle.clone();
move |cx| {
move |window, cx| {
Tooltip::for_action_in(
"Remove All Context",
&RemoveAllContext,
&focus_handle,
window,
cx,
)
}
})
.on_click(cx.listener({
let focus_handle = focus_handle.clone();
move |_this, _event, cx| {
focus_handle.dispatch_action(&RemoveAllContext, cx);
move |_this, _event, window, cx| {
focus_handle.dispatch_action(&RemoveAllContext, window, cx);
}
})),
)
@@ -251,6 +526,9 @@ impl Render for ContextStrip {
pub enum ContextStripEvent {
PickerDismissed,
BlurredEmpty,
BlurredDown,
BlurredUp,
}
impl EventEmitter<ContextStripEvent> for ContextStrip {}
@@ -264,11 +542,12 @@ pub enum SuggestContextKind {
pub enum SuggestedContext {
File {
name: SharedString,
buffer: WeakModel<Buffer>,
icon_path: Option<SharedString>,
buffer: WeakEntity<Buffer>,
},
Thread {
name: SharedString,
thread: WeakModel<Thread>,
thread: WeakEntity<Thread>,
},
}
@@ -280,18 +559,10 @@ impl SuggestedContext {
}
}
pub fn accept(&self, context_store: &mut ContextStore, cx: &mut AppContext) {
pub fn icon_path(&self) -> Option<SharedString> {
match self {
Self::File { buffer, name: _ } => {
if let Some(buffer) = buffer.upgrade() {
context_store.insert_file(buffer.read(cx));
};
}
Self::Thread { thread, name: _ } => {
if let Some(thread) = thread.upgrade() {
context_store.insert_thread(thread.read(cx));
};
}
Self::File { icon_path, .. } => icon_path.clone(),
Self::Thread { .. } => None,
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -16,9 +16,8 @@ use editor::{
use feature_flags::{FeatureFlagAppExt as _, ZedPro};
use fs::Fs;
use gpui::{
anchored, deferred, point, AnyElement, AppContext, ClickEvent, CursorStyle, EventEmitter,
FocusHandle, FocusableView, FontWeight, Model, Subscription, TextStyle, View, ViewContext,
WeakModel, WeakView, WindowContext,
anchored, deferred, point, AnyElement, App, ClickEvent, Context, CursorStyle, Entity,
EventEmitter, FocusHandle, Focusable, FontWeight, Subscription, TextStyle, WeakEntity, Window,
};
use language_model::{LanguageModel, LanguageModelRegistry};
use language_model_selector::LanguageModelSelector;
@@ -35,12 +34,12 @@ use util::ResultExt;
use workspace::Workspace;
pub struct PromptEditor<T> {
pub editor: View<Editor>,
pub editor: Entity<Editor>,
mode: PromptEditorMode,
context_store: Model<ContextStore>,
context_strip: View<ContextStrip>,
context_store: Entity<ContextStore>,
context_strip: Entity<ContextStrip>,
context_picker_menu_handle: PopoverMenuHandle<ContextPicker>,
model_selector: View<AssistantModelSelector>,
model_selector: Entity<AssistantModelSelector>,
model_selector_menu_handle: PopoverMenuHandle<LanguageModelSelector>,
edited_since_done: bool,
prompt_history: VecDeque<String>,
@@ -56,7 +55,7 @@ pub struct PromptEditor<T> {
impl<T: 'static> EventEmitter<PromptEditorEvent> for PromptEditor<T> {}
impl<T: 'static> Render for PromptEditor<T> {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let ui_font_size = ThemeSettings::get_global(cx).ui_font_size;
let mut buttons = Vec::new();
@@ -87,7 +86,7 @@ impl<T: 'static> Render for PromptEditor<T> {
PromptEditorMode::Terminal { .. } => Pixels::from(8.0),
};
buttons.extend(self.render_buttons(cx));
buttons.extend(self.render_buttons(window, cx));
v_flex()
.key_context("PromptEditor")
@@ -163,9 +162,7 @@ impl<T: 'static> Render for PromptEditor<T> {
el.child(
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)
@@ -179,7 +176,7 @@ impl<T: 'static> Render for PromptEditor<T> {
h_flex()
.w_full()
.justify_between()
.child(div().flex_1().child(self.render_editor(cx)))
.child(div().flex_1().child(self.render_editor(window, cx)))
.child(
WithRemSize::new(ui_font_size)
.flex()
@@ -209,8 +206,8 @@ impl<T: 'static> Render for PromptEditor<T> {
}
}
impl<T: 'static> FocusableView for PromptEditor<T> {
fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
impl<T: 'static> Focusable for PromptEditor<T> {
fn focus_handle(&self, cx: &App) -> FocusHandle {
self.editor.focus_handle(cx)
}
}
@@ -218,47 +215,50 @@ impl<T: 'static> FocusableView for PromptEditor<T> {
impl<T: 'static> PromptEditor<T> {
const MAX_LINES: u8 = 8;
fn codegen_status<'a>(&'a self, cx: &'a AppContext) -> &'a CodegenStatus {
fn codegen_status<'a>(&'a self, cx: &'a App) -> &'a CodegenStatus {
match &self.mode {
PromptEditorMode::Buffer { codegen, .. } => codegen.read(cx).status(cx),
PromptEditorMode::Terminal { codegen, .. } => &codegen.read(cx).status,
}
}
fn subscribe_to_editor(&mut self, cx: &mut ViewContext<Self>) {
fn subscribe_to_editor(&mut self, window: &mut Window, cx: &mut Context<Self>) {
self.editor_subscriptions.clear();
self.editor_subscriptions
.push(cx.subscribe(&self.editor, Self::handle_prompt_editor_events));
self.editor_subscriptions.push(cx.subscribe_in(
&self.editor,
window,
Self::handle_prompt_editor_events,
));
}
pub fn set_show_cursor_when_unfocused(
&mut self,
show_cursor_when_unfocused: bool,
cx: &mut ViewContext<Self>,
cx: &mut Context<Self>,
) {
self.editor.update(cx, |editor, cx| {
editor.set_show_cursor_when_unfocused(show_cursor_when_unfocused, cx)
});
}
pub fn unlink(&mut self, cx: &mut ViewContext<Self>) {
pub fn unlink(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let prompt = self.prompt(cx);
let focus = self.editor.focus_handle(cx).contains_focused(cx);
self.editor = cx.new_view(|cx| {
let mut editor = Editor::auto_height(Self::MAX_LINES as usize, cx);
let focus = self.editor.focus_handle(cx).contains_focused(window, cx);
self.editor = cx.new(|cx| {
let mut editor = Editor::auto_height(Self::MAX_LINES as usize, window, cx);
editor.set_soft_wrap_mode(language::language_settings::SoftWrap::EditorWidth, cx);
editor.set_placeholder_text(Self::placeholder_text(&self.mode, cx), cx);
editor.set_placeholder_text(Self::placeholder_text(&self.mode, window, cx), cx);
editor.set_placeholder_text("Add a prompt…", cx);
editor.set_text(prompt, cx);
editor.set_text(prompt, window, cx);
if focus {
editor.focus(cx);
window.focus(&editor.focus_handle(cx));
}
editor
});
self.subscribe_to_editor(cx);
self.subscribe_to_editor(window, cx);
}
pub fn placeholder_text(mode: &PromptEditorMode, cx: &WindowContext) -> String {
pub fn placeholder_text(mode: &PromptEditorMode, window: &mut Window, cx: &mut App) -> String {
let action = match mode {
PromptEditorMode::Buffer { codegen, .. } => {
if codegen.read(cx).is_insertion {
@@ -270,36 +270,43 @@ impl<T: 'static> PromptEditor<T> {
PromptEditorMode::Terminal { .. } => "Generate",
};
let assistant_panel_keybinding = ui::text_for_action(&crate::ToggleFocus, cx)
.map(|keybinding| format!("{keybinding} to chat ― "))
.unwrap_or_default();
let assistant_panel_keybinding =
ui::text_for_action(&zed_actions::assistant::ToggleFocus, window)
.map(|keybinding| format!("{keybinding} to chat ― "))
.unwrap_or_default();
format!("{action}… ({assistant_panel_keybinding}↓↑ for history)")
}
pub fn prompt(&self, cx: &AppContext) -> String {
pub fn prompt(&self, cx: &App) -> String {
self.editor.read(cx).text(cx)
}
fn toggle_rate_limit_notice(&mut self, _: &ClickEvent, cx: &mut ViewContext<Self>) {
fn toggle_rate_limit_notice(
&mut self,
_: &ClickEvent,
window: &mut Window,
cx: &mut Context<Self>,
) {
self.show_rate_limit_notice = !self.show_rate_limit_notice;
if self.show_rate_limit_notice {
cx.focus_view(&self.editor);
window.focus(&self.editor.focus_handle(cx));
}
cx.notify();
}
fn handle_prompt_editor_events(
&mut self,
_: View<Editor>,
_: &Entity<Editor>,
event: &EditorEvent,
cx: &mut ViewContext<Self>,
window: &mut Window,
cx: &mut Context<Self>,
) {
match event {
EditorEvent::Edited { .. } => {
if let Some(workspace) = cx.window_handle().downcast::<Workspace>() {
if let Some(workspace) = window.window_handle().downcast::<Workspace>() {
workspace
.update(cx, |workspace, cx| {
.update(cx, |workspace, _, cx| {
let is_via_ssh = workspace
.project()
.update(cx, |project, _| project.is_via_ssh());
@@ -333,20 +340,40 @@ impl<T: 'static> PromptEditor<T> {
}
}
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 Context<Self>,
) {
self.context_picker_menu_handle.toggle(window, cx);
}
fn toggle_model_selector(&mut self, _: &ToggleModelSelector, cx: &mut ViewContext<Self>) {
self.model_selector_menu_handle.toggle(cx);
fn toggle_model_selector(
&mut self,
_: &ToggleModelSelector,
window: &mut Window,
cx: &mut Context<Self>,
) {
self.model_selector_menu_handle.toggle(window, cx);
}
pub fn remove_all_context(&mut self, _: &RemoveAllContext, cx: &mut ViewContext<Self>) {
pub fn remove_all_context(
&mut self,
_: &RemoveAllContext,
_window: &mut Window,
cx: &mut Context<Self>,
) {
self.context_store.update(cx, |store, _cx| store.clear());
cx.notify();
}
fn cancel(&mut self, _: &editor::actions::Cancel, cx: &mut ViewContext<Self>) {
fn cancel(
&mut self,
_: &editor::actions::Cancel,
_window: &mut Window,
cx: &mut Context<Self>,
) {
match self.codegen_status(cx) {
CodegenStatus::Idle | CodegenStatus::Done | CodegenStatus::Error(_) => {
cx.emit(PromptEditorEvent::CancelRequested);
@@ -357,7 +384,7 @@ impl<T: 'static> PromptEditor<T> {
}
}
fn confirm(&mut self, _: &menu::Confirm, cx: &mut ViewContext<Self>) {
fn confirm(&mut self, _: &menu::Confirm, _window: &mut Window, cx: &mut Context<Self>) {
match self.codegen_status(cx) {
CodegenStatus::Idle => {
cx.emit(PromptEditorEvent::StartRequested);
@@ -378,47 +405,49 @@ impl<T: 'static> PromptEditor<T> {
}
}
fn move_up(&mut self, _: &MoveUp, cx: &mut ViewContext<Self>) {
fn move_up(&mut self, _: &MoveUp, window: &mut Window, cx: &mut Context<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 Context<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)
});
}
} else {
self.context_strip.focus_handle(cx).focus(window);
}
}
fn render_buttons(&self, cx: &mut ViewContext<Self>) -> Vec<AnyElement> {
fn render_buttons(&self, _window: &mut Window, cx: &mut Context<Self>) -> Vec<AnyElement> {
let mode = match &self.mode {
PromptEditorMode::Buffer { codegen, .. } => {
let codegen = codegen.read(cx);
@@ -440,21 +469,22 @@ impl<T: 'static> PromptEditor<T> {
.icon(IconName::Return)
.icon_size(IconSize::XSmall)
.icon_color(Color::Muted)
.on_click(cx.listener(|_, _, cx| cx.emit(PromptEditorEvent::StartRequested)))
.on_click(cx.listener(|_, _, _, cx| cx.emit(PromptEditorEvent::StartRequested)))
.into_any_element()]
}
CodegenStatus::Pending => vec![IconButton::new("stop", IconName::Stop)
.icon_color(Color::Error)
.shape(IconButtonShape::Square)
.tooltip(move |cx| {
.tooltip(move |window, cx| {
Tooltip::with_meta(
mode.tooltip_interrupt(),
Some(&menu::Cancel),
"Changes won't be discarded",
window,
cx,
)
})
.on_click(cx.listener(|_, _, cx| cx.emit(PromptEditorEvent::StopRequested)))
.on_click(cx.listener(|_, _, _, cx| cx.emit(PromptEditorEvent::StopRequested)))
.into_any_element()],
CodegenStatus::Done | CodegenStatus::Error(_) => {
let has_error = matches!(codegen_status, CodegenStatus::Error(_));
@@ -462,15 +492,16 @@ impl<T: 'static> PromptEditor<T> {
vec![IconButton::new("restart", IconName::RotateCw)
.icon_color(Color::Info)
.shape(IconButtonShape::Square)
.tooltip(move |cx| {
.tooltip(move |window, cx| {
Tooltip::with_meta(
mode.tooltip_restart(),
Some(&menu::Confirm),
"Changes will be discarded",
window,
cx,
)
})
.on_click(cx.listener(|_, _, cx| {
.on_click(cx.listener(|_, _, _, cx| {
cx.emit(PromptEditorEvent::StartRequested);
}))
.into_any_element()]
@@ -478,10 +509,10 @@ impl<T: 'static> PromptEditor<T> {
let accept = IconButton::new("accept", IconName::Check)
.icon_color(Color::Info)
.shape(IconButtonShape::Square)
.tooltip(move |cx| {
Tooltip::for_action(mode.tooltip_accept(), &menu::Confirm, cx)
.tooltip(move |window, cx| {
Tooltip::for_action(mode.tooltip_accept(), &menu::Confirm, window, cx)
})
.on_click(cx.listener(|_, _, cx| {
.on_click(cx.listener(|_, _, _, cx| {
cx.emit(PromptEditorEvent::ConfirmRequested { execute: false });
}))
.into_any_element();
@@ -492,14 +523,15 @@ impl<T: 'static> PromptEditor<T> {
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 });
}))
.into_any_element(),
@@ -511,7 +543,12 @@ impl<T: 'static> PromptEditor<T> {
}
}
fn cycle_prev(&mut self, _: &CyclePreviousInlineAssist, cx: &mut ViewContext<Self>) {
fn cycle_prev(
&mut self,
_: &CyclePreviousInlineAssist,
_: &mut Window,
cx: &mut Context<Self>,
) {
match &self.mode {
PromptEditorMode::Buffer { codegen, .. } => {
codegen.update(cx, |codegen, cx| codegen.cycle_prev(cx));
@@ -522,7 +559,7 @@ impl<T: 'static> PromptEditor<T> {
}
}
fn cycle_next(&mut self, _: &CycleNextInlineAssist, cx: &mut ViewContext<Self>) {
fn cycle_next(&mut self, _: &CycleNextInlineAssist, _: &mut Window, cx: &mut Context<Self>) {
match &self.mode {
PromptEditorMode::Buffer { codegen, .. } => {
codegen.update(cx, |codegen, cx| codegen.cycle_next(cx));
@@ -533,16 +570,16 @@ impl<T: 'static> PromptEditor<T> {
}
}
fn render_close_button(&self, cx: &ViewContext<Self>) -> AnyElement {
fn render_close_button(&self, cx: &mut Context<Self>) -> AnyElement {
IconButton::new("cancel", IconName::Close)
.icon_color(Color::Muted)
.shape(IconButtonShape::Square)
.tooltip(|cx| Tooltip::text("Close Assistant", cx))
.on_click(cx.listener(|_, _, cx| cx.emit(PromptEditorEvent::CancelRequested)))
.tooltip(Tooltip::text("Close Assistant"))
.on_click(cx.listener(|_, _, _, cx| cx.emit(PromptEditorEvent::CancelRequested)))
.into_any_element()
}
fn render_cycle_controls(&self, codegen: &BufferCodegen, cx: &ViewContext<Self>) -> AnyElement {
fn render_cycle_controls(&self, codegen: &BufferCodegen, cx: &Context<Self>) -> AnyElement {
let disabled = matches!(codegen.status(cx), CodegenStatus::Idle);
let model_registry = LanguageModelRegistry::read_global(cx);
@@ -582,13 +619,13 @@ impl<T: 'static> PromptEditor<T> {
.shape(IconButtonShape::Square)
.tooltip({
let focus_handle = self.editor.focus_handle(cx);
move |cx| {
cx.new_view(|cx| {
move |window, cx| {
cx.new(|_| {
let mut tooltip = Tooltip::new("Previous Alternative").key_binding(
KeyBinding::for_action_in(
&CyclePreviousInlineAssist,
&focus_handle,
cx,
window,
),
);
if !disabled && current_index != 0 {
@@ -599,8 +636,8 @@ impl<T: 'static> PromptEditor<T> {
.into()
}
})
.on_click(cx.listener(|this, _, cx| {
this.cycle_prev(&CyclePreviousInlineAssist, cx);
.on_click(cx.listener(|this, _, window, cx| {
this.cycle_prev(&CyclePreviousInlineAssist, window, cx);
})),
)
.child(
@@ -623,13 +660,13 @@ impl<T: 'static> PromptEditor<T> {
.shape(IconButtonShape::Square)
.tooltip({
let focus_handle = self.editor.focus_handle(cx);
move |cx| {
cx.new_view(|cx| {
move |window, cx| {
cx.new(|_| {
let mut tooltip = Tooltip::new("Next Alternative").key_binding(
KeyBinding::for_action_in(
&CycleNextInlineAssist,
&focus_handle,
cx,
window,
),
);
if !disabled && current_index != total_models - 1 {
@@ -640,14 +677,14 @@ impl<T: 'static> PromptEditor<T> {
.into()
}
})
.on_click(
cx.listener(|this, _, cx| this.cycle_next(&CycleNextInlineAssist, cx)),
),
.on_click(cx.listener(|this, _, window, cx| {
this.cycle_next(&CycleNextInlineAssist, window, cx)
})),
)
.into_any_element()
}
fn render_rate_limit_notice(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
fn render_rate_limit_notice(&self, cx: &mut Context<Self>) -> impl IntoElement {
Popover::new().child(
v_flex()
.occlude()
@@ -671,7 +708,7 @@ impl<T: 'static> PromptEditor<T> {
} else {
ui::ToggleState::Unselected
},
|selection, cx| {
|selection, _, cx| {
let is_dismissed = match selection {
ui::ToggleState::Unselected => false,
ui::ToggleState::Indeterminate => return,
@@ -690,10 +727,11 @@ impl<T: 'static> PromptEditor<T> {
.on_click(cx.listener(Self::toggle_rate_limit_notice)),
)
.child(Button::new("more-info", "More Info").on_click(
|_event, cx| {
cx.dispatch_action(Box::new(
zed_actions::OpenAccountSettings,
))
|_event, window, cx| {
window.dispatch_action(
Box::new(zed_actions::OpenAccountSettings),
cx,
)
},
)),
),
@@ -701,9 +739,9 @@ impl<T: 'static> PromptEditor<T> {
)
}
fn render_editor(&mut self, cx: &mut ViewContext<Self>) -> AnyElement {
fn render_editor(&mut self, window: &mut Window, cx: &mut Context<Self>) -> AnyElement {
let font_size = TextSize::Default.rems(cx);
let line_height = font_size.to_pixels(cx.rem_size()) * 1.3;
let line_height = font_size.to_pixels(window.rem_size()) * 1.3;
div()
.key_context("InlineAssistEditor")
@@ -737,24 +775,29 @@ impl<T: 'static> PromptEditor<T> {
fn handle_context_strip_event(
&mut self,
_context_strip: View<ContextStrip>,
ContextStripEvent::PickerDismissed: &ContextStripEvent,
cx: &mut ViewContext<Self>,
_context_strip: &Entity<ContextStrip>,
event: &ContextStripEvent,
window: &mut Window,
cx: &mut Context<Self>,
) {
let editor_focus_handle = self.editor.focus_handle(cx);
cx.focus(&editor_focus_handle);
match event {
ContextStripEvent::PickerDismissed
| ContextStripEvent::BlurredEmpty
| ContextStripEvent::BlurredUp => self.editor.focus_handle(cx).focus(window),
ContextStripEvent::BlurredDown => {}
}
}
}
pub enum PromptEditorMode {
Buffer {
id: InlineAssistId,
codegen: Model<BufferCodegen>,
codegen: Entity<BufferCodegen>,
gutter_dimensions: Arc<Mutex<GutterDimensions>>,
},
Terminal {
id: TerminalInlineAssistId,
codegen: Model<TerminalCodegen>,
codegen: Entity<TerminalCodegen>,
height_in_lines: u8,
},
}
@@ -785,13 +828,14 @@ impl PromptEditor<BufferCodegen> {
id: InlineAssistId,
gutter_dimensions: Arc<Mutex<GutterDimensions>>,
prompt_history: VecDeque<String>,
prompt_buffer: Model<MultiBuffer>,
codegen: Model<BufferCodegen>,
prompt_buffer: Entity<MultiBuffer>,
codegen: Entity<BufferCodegen>,
fs: Arc<dyn Fs>,
context_store: Model<ContextStore>,
workspace: WeakView<Workspace>,
thread_store: Option<WeakModel<ThreadStore>>,
cx: &mut ViewContext<PromptEditor<BufferCodegen>>,
context_store: Entity<ContextStore>,
workspace: WeakEntity<Workspace>,
thread_store: Option<WeakEntity<ThreadStore>>,
window: &mut Window,
cx: &mut Context<PromptEditor<BufferCodegen>>,
) -> PromptEditor<BufferCodegen> {
let codegen_subscription = cx.observe(&codegen, Self::handle_codegen_changed);
let mode = PromptEditorMode::Buffer {
@@ -800,7 +844,7 @@ impl PromptEditor<BufferCodegen> {
gutter_dimensions,
};
let prompt_editor = cx.new_view(|cx| {
let prompt_editor = cx.new(|cx| {
let mut editor = Editor::new(
EditorMode::AutoHeight {
max_lines: Self::MAX_LINES as usize,
@@ -808,6 +852,7 @@ impl PromptEditor<BufferCodegen> {
prompt_buffer,
None,
false,
window,
cx,
);
editor.set_soft_wrap_mode(language::language_settings::SoftWrap::EditorWidth, cx);
@@ -815,34 +860,41 @@ impl PromptEditor<BufferCodegen> {
// always show the cursor (even when it isn't focused) because
// typing in one will make what you typed appear in all of them.
editor.set_show_cursor_when_unfocused(true, cx);
editor.set_placeholder_text(Self::placeholder_text(&mode, cx), cx);
editor.set_placeholder_text(Self::placeholder_text(&mode, window, cx), cx);
editor
});
let context_picker_menu_handle = PopoverMenuHandle::default();
let model_selector_menu_handle = PopoverMenuHandle::default();
let context_strip = cx.new_view(|cx| {
let context_strip = cx.new(|cx| {
ContextStrip::new(
context_store.clone(),
workspace.clone(),
prompt_editor.downgrade(),
thread_store.clone(),
prompt_editor.focus_handle(cx),
context_picker_menu_handle.clone(),
SuggestContextKind::Thread,
window,
cx,
)
});
let context_strip_subscription =
cx.subscribe(&context_strip, Self::handle_context_strip_event);
cx.subscribe_in(&context_strip, window, Self::handle_context_strip_event);
let mut this: PromptEditor<BufferCodegen> = PromptEditor {
editor: prompt_editor.clone(),
context_store,
context_strip,
context_picker_menu_handle,
model_selector: cx.new_view(|cx| {
AssistantModelSelector::new(fs, model_selector_menu_handle.clone(), cx)
model_selector: cx.new(|cx| {
AssistantModelSelector::new(
fs,
model_selector_menu_handle.clone(),
prompt_editor.focus_handle(cx),
window,
cx,
)
}),
model_selector_menu_handle,
edited_since_done: false,
@@ -857,14 +909,14 @@ impl PromptEditor<BufferCodegen> {
_phantom: Default::default(),
};
this.subscribe_to_editor(cx);
this.subscribe_to_editor(window, cx);
this
}
fn handle_codegen_changed(
&mut self,
_: Model<BufferCodegen>,
cx: &mut ViewContext<PromptEditor<BufferCodegen>>,
_: Entity<BufferCodegen>,
cx: &mut Context<PromptEditor<BufferCodegen>>,
) {
match self.codegen_status(cx) {
CodegenStatus::Idle => {
@@ -903,7 +955,7 @@ impl PromptEditor<BufferCodegen> {
}
}
pub fn codegen(&self) -> &Model<BufferCodegen> {
pub fn codegen(&self) -> &Entity<BufferCodegen> {
match &self.mode {
PromptEditorMode::Buffer { codegen, .. } => codegen,
PromptEditorMode::Terminal { .. } => unreachable!(),
@@ -936,13 +988,14 @@ impl PromptEditor<TerminalCodegen> {
pub fn new_terminal(
id: TerminalInlineAssistId,
prompt_history: VecDeque<String>,
prompt_buffer: Model<MultiBuffer>,
codegen: Model<TerminalCodegen>,
prompt_buffer: Entity<MultiBuffer>,
codegen: Entity<TerminalCodegen>,
fs: Arc<dyn Fs>,
context_store: Model<ContextStore>,
workspace: WeakView<Workspace>,
thread_store: Option<WeakModel<ThreadStore>>,
cx: &mut ViewContext<Self>,
context_store: Entity<ContextStore>,
workspace: WeakEntity<Workspace>,
thread_store: Option<WeakEntity<ThreadStore>>,
window: &mut Window,
cx: &mut Context<Self>,
) -> Self {
let codegen_subscription = cx.observe(&codegen, Self::handle_codegen_changed);
let mode = PromptEditorMode::Terminal {
@@ -951,7 +1004,7 @@ impl PromptEditor<TerminalCodegen> {
height_in_lines: 1,
};
let prompt_editor = cx.new_view(|cx| {
let prompt_editor = cx.new(|cx| {
let mut editor = Editor::new(
EditorMode::AutoHeight {
max_lines: Self::MAX_LINES as usize,
@@ -959,37 +1012,45 @@ impl PromptEditor<TerminalCodegen> {
prompt_buffer,
None,
false,
window,
cx,
);
editor.set_soft_wrap_mode(language::language_settings::SoftWrap::EditorWidth, cx);
editor.set_placeholder_text(Self::placeholder_text(&mode, cx), cx);
editor.set_placeholder_text(Self::placeholder_text(&mode, window, cx), cx);
editor
});
let context_picker_menu_handle = PopoverMenuHandle::default();
let model_selector_menu_handle = PopoverMenuHandle::default();
let context_strip = cx.new_view(|cx| {
let context_strip = cx.new(|cx| {
ContextStrip::new(
context_store.clone(),
workspace.clone(),
prompt_editor.downgrade(),
thread_store.clone(),
prompt_editor.focus_handle(cx),
context_picker_menu_handle.clone(),
SuggestContextKind::Thread,
window,
cx,
)
});
let context_strip_subscription =
cx.subscribe(&context_strip, Self::handle_context_strip_event);
cx.subscribe_in(&context_strip, window, Self::handle_context_strip_event);
let mut this = Self {
editor: prompt_editor.clone(),
context_store,
context_strip,
context_picker_menu_handle,
model_selector: cx.new_view(|cx| {
AssistantModelSelector::new(fs, model_selector_menu_handle.clone(), cx)
model_selector: cx.new(|cx| {
AssistantModelSelector::new(
fs,
model_selector_menu_handle.clone(),
prompt_editor.focus_handle(cx),
window,
cx,
)
}),
model_selector_menu_handle,
edited_since_done: false,
@@ -1004,11 +1065,11 @@ impl PromptEditor<TerminalCodegen> {
_phantom: Default::default(),
};
this.count_lines(cx);
this.subscribe_to_editor(cx);
this.subscribe_to_editor(window, cx);
this
}
fn count_lines(&mut self, cx: &mut ViewContext<Self>) {
fn count_lines(&mut self, cx: &mut Context<Self>) {
let height_in_lines = cmp::max(
2, // Make the editor at least two lines tall, to account for padding and buttons.
cmp::min(
@@ -1032,7 +1093,7 @@ impl PromptEditor<TerminalCodegen> {
}
}
fn handle_codegen_changed(&mut self, _: Model<TerminalCodegen>, cx: &mut ViewContext<Self>) {
fn handle_codegen_changed(&mut self, _: Entity<TerminalCodegen>, cx: &mut Context<Self>) {
match &self.codegen().read(cx).status {
CodegenStatus::Idle => {
self.editor
@@ -1050,7 +1111,7 @@ impl PromptEditor<TerminalCodegen> {
}
}
pub fn codegen(&self) -> &Model<TerminalCodegen> {
pub fn codegen(&self) -> &Entity<TerminalCodegen> {
match &self.mode {
PromptEditorMode::Buffer { .. } => unreachable!(),
PromptEditorMode::Terminal { codegen, .. } => codegen,
@@ -1074,7 +1135,7 @@ fn dismissed_rate_limit_notice() -> bool {
.map_or(false, |s| s.is_some())
}
fn set_rate_limit_notice_dismissed(is_dismissed: bool, cx: &mut AppContext) {
fn set_rate_limit_notice_dismissed(is_dismissed: bool, cx: &mut App) {
db::write_and_log(cx, move || async move {
if is_dismissed {
db::kvp::KEY_VALUE_STORE

View File

@@ -1,39 +1,40 @@
use std::sync::Arc;
use editor::actions::MoveUp;
use editor::{Editor, EditorElement, EditorEvent, EditorStyle};
use fs::Fs;
use gpui::{
AppContext, DismissEvent, FocusableView, Model, Subscription, TextStyle, View, WeakModel,
WeakView,
pulsating_between, Animation, AnimationExt, App, DismissEvent, Entity, Focusable, Subscription,
TextStyle, WeakEntity,
};
use language_model::{LanguageModelRegistry, LanguageModelRequestTool};
use language_model_selector::LanguageModelSelector;
use rope::Point;
use settings::Settings;
use theme::{get_ui_font_size, ThemeSettings};
use std::time::Duration;
use theme::ThemeSettings;
use ui::{
prelude::*, ButtonLike, ElevationIndex, KeyBinding, PopoverMenu, PopoverMenuHandle,
SwitchWithLabel,
prelude::*, ButtonLike, KeyBinding, PopoverMenu, PopoverMenuHandle, Switch, TintColor, Tooltip,
};
use workspace::Workspace;
use crate::assistant_model_selector::AssistantModelSelector;
use crate::context_picker::{ConfirmBehavior, ContextPicker};
use crate::context_store::ContextStore;
use crate::context_store::{refresh_context_store_text, ContextStore};
use crate::context_strip::{ContextStrip, ContextStripEvent, SuggestContextKind};
use crate::thread::{RequestKind, Thread};
use crate::thread_store::ThreadStore;
use crate::{Chat, RemoveAllContext, ToggleContextPicker, ToggleModelSelector};
use crate::{Chat, ChatMode, RemoveAllContext, ToggleContextPicker, ToggleModelSelector};
pub struct MessageEditor {
thread: Model<Thread>,
editor: View<Editor>,
context_store: Model<ContextStore>,
context_strip: View<ContextStrip>,
thread: Entity<Thread>,
editor: Entity<Editor>,
context_store: Entity<ContextStore>,
context_strip: Entity<ContextStrip>,
context_picker_menu_handle: PopoverMenuHandle<ContextPicker>,
inline_context_picker: View<ContextPicker>,
inline_context_picker: Entity<ContextPicker>,
inline_context_picker_menu_handle: PopoverMenuHandle<ContextPicker>,
model_selector: View<AssistantModelSelector>,
model_selector: Entity<AssistantModelSelector>,
model_selector_menu_handle: PopoverMenuHandle<LanguageModelSelector>,
use_tools: bool,
_subscriptions: Vec<Subscription>,
@@ -42,53 +43,58 @@ pub struct MessageEditor {
impl MessageEditor {
pub fn new(
fs: Arc<dyn Fs>,
workspace: WeakView<Workspace>,
thread_store: WeakModel<ThreadStore>,
thread: Model<Thread>,
cx: &mut ViewContext<Self>,
workspace: WeakEntity<Workspace>,
thread_store: WeakEntity<ThreadStore>,
thread: Entity<Thread>,
window: &mut Window,
cx: &mut Context<Self>,
) -> Self {
let context_store = cx.new_model(|_cx| ContextStore::new());
let context_store = cx.new(|_cx| ContextStore::new(workspace.clone()));
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(10, cx);
editor.set_placeholder_text("Ask anything", cx);
let editor = cx.new(|cx| {
let mut editor = Editor::auto_height(10, window, cx);
editor.set_placeholder_text("Ask anything, @ to mention, ↑ to select", cx);
editor.set_show_indent_guides(false, cx);
editor
});
let inline_context_picker = cx.new_view(|cx| {
let inline_context_picker = cx.new(|cx| {
ContextPicker::new(
workspace.clone(),
Some(thread_store.clone()),
context_store.downgrade(),
editor.downgrade(),
ConfirmBehavior::Close,
window,
cx,
)
});
let context_strip = cx.new_view(|cx| {
let context_strip = cx.new(|cx| {
ContextStrip::new(
context_store.clone(),
workspace.clone(),
editor.downgrade(),
Some(thread_store.clone()),
editor.focus_handle(cx),
context_picker_menu_handle.clone(),
SuggestContextKind::File,
window,
cx,
)
});
let subscriptions = vec![
cx.subscribe(&editor, Self::handle_editor_event),
cx.subscribe(
cx.subscribe_in(&editor, window, Self::handle_editor_event),
cx.subscribe_in(
&inline_context_picker,
window,
Self::handle_inline_context_picker_event,
),
cx.subscribe(&context_strip, Self::handle_context_strip_event),
cx.subscribe_in(&context_strip, window, Self::handle_context_strip_event),
];
Self {
@@ -99,8 +105,14 @@ impl MessageEditor {
context_picker_menu_handle,
inline_context_picker,
inline_context_picker_menu_handle,
model_selector: cx.new_view(|cx| {
AssistantModelSelector::new(fs, model_selector_menu_handle.clone(), cx)
model_selector: cx.new(|cx| {
AssistantModelSelector::new(
fs,
model_selector_menu_handle.clone(),
editor.focus_handle(cx),
window,
cx,
)
}),
model_selector_menu_handle,
use_tools: false,
@@ -108,77 +120,118 @@ impl MessageEditor {
}
}
fn toggle_model_selector(&mut self, _: &ToggleModelSelector, cx: &mut ViewContext<Self>) {
self.model_selector_menu_handle.toggle(cx)
fn toggle_model_selector(
&mut self,
_: &ToggleModelSelector,
window: &mut Window,
cx: &mut Context<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_chat_mode(&mut self, _: &ChatMode, _window: &mut Window, cx: &mut Context<Self>) {
self.use_tools = !self.use_tools;
cx.notify();
}
pub fn remove_all_context(&mut self, _: &RemoveAllContext, cx: &mut ViewContext<Self>) {
fn toggle_context_picker(
&mut self,
_: &ToggleContextPicker,
window: &mut Window,
cx: &mut Context<Self>,
) {
self.context_picker_menu_handle.toggle(window, cx);
}
pub fn remove_all_context(
&mut self,
_: &RemoveAllContext,
_window: &mut Window,
cx: &mut Context<Self>,
) {
self.context_store.update(cx, |store, _cx| store.clear());
cx.notify();
}
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 Context<Self>) {
self.send_to_model(RequestKind::Chat, window, cx);
}
fn is_editor_empty(&self, cx: &App) -> bool {
self.editor.read(cx).text(cx).is_empty()
}
fn is_model_selected(&self, cx: &App) -> bool {
LanguageModelRegistry::read_global(cx)
.active_model()
.is_some()
}
fn send_to_model(
&mut self,
request_kind: RequestKind,
cx: &mut ViewContext<Self>,
) -> Option<()> {
window: &mut Window,
cx: &mut Context<Self>,
) {
let provider = LanguageModelRegistry::read_global(cx).active_provider();
if provider
.as_ref()
.map_or(false, |provider| provider.must_accept_terms(cx))
{
cx.notify();
return None;
return;
}
let model_registry = LanguageModelRegistry::read_global(cx);
let model = model_registry.active_model()?;
let Some(model) = model_registry.active_model() else {
return;
};
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.context().clone());
self.thread.update(cx, |thread, cx| {
thread.insert_user_message(user_message, context, cx);
let mut request = thread.to_completion_request(request_kind, cx);
let refresh_task = refresh_context_store_text(self.context_store.clone(), cx);
if self.use_tools {
request.tools = thread
.tools()
.tools(cx)
.into_iter()
.map(|tool| LanguageModelRequestTool {
name: tool.name(),
description: tool.description(),
input_schema: tool.input_schema(),
})
.collect();
}
let thread = self.thread.clone();
let context_store = self.context_store.clone();
let use_tools = self.use_tools;
cx.spawn(move |_, mut cx| async move {
refresh_task.await;
thread
.update(&mut cx, |thread, cx| {
let context = context_store.read(cx).snapshot(cx).collect::<Vec<_>>();
thread.insert_user_message(user_message, context, cx);
let mut request = thread.to_completion_request(request_kind, cx);
thread.stream_completion(request, model, cx)
});
if use_tools {
request.tools = thread
.tools()
.tools(cx)
.into_iter()
.map(|tool| LanguageModelRequestTool {
name: tool.name(),
description: tool.description(),
input_schema: tool.input_schema(),
})
.collect();
}
None
thread.stream_completion(request, model, cx)
})
.ok();
})
.detach();
}
fn handle_editor_event(
&mut self,
editor: View<Editor>,
editor: &Entity<Editor>,
event: &EditorEvent,
cx: &mut ViewContext<Self>,
window: &mut Window,
cx: &mut Context<Self>,
) {
match event {
EditorEvent::SelectionsChanged { .. } => {
@@ -189,7 +242,7 @@ impl MessageEditor {
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(cx);
self.inline_context_picker_menu_handle.show(window, cx);
}
}
});
@@ -200,38 +253,66 @@ impl MessageEditor {
fn handle_inline_context_picker_event(
&mut self,
_inline_context_picker: View<ContextPicker>,
_inline_context_picker: &Entity<ContextPicker>,
_event: &DismissEvent,
cx: &mut ViewContext<Self>,
window: &mut Window,
cx: &mut Context<Self>,
) {
let editor_focus_handle = self.editor.focus_handle(cx);
cx.focus(&editor_focus_handle);
window.focus(&editor_focus_handle);
}
fn handle_context_strip_event(
&mut self,
_context_strip: View<ContextStrip>,
ContextStripEvent::PickerDismissed: &ContextStripEvent,
cx: &mut ViewContext<Self>,
_context_strip: &Entity<ContextStrip>,
event: &ContextStripEvent,
window: &mut Window,
cx: &mut Context<Self>,
) {
let editor_focus_handle = self.editor.focus_handle(cx);
cx.focus(&editor_focus_handle);
match event {
ContextStripEvent::PickerDismissed
| ContextStripEvent::BlurredEmpty
| ContextStripEvent::BlurredDown => {
let editor_focus_handle = self.editor.focus_handle(cx);
window.focus(&editor_focus_handle);
}
ContextStripEvent::BlurredUp => {}
}
}
fn move_up(&mut self, _: &MoveUp, window: &mut Window, cx: &mut Context<Self>) {
if self.context_picker_menu_handle.is_deployed()
|| self.inline_context_picker_menu_handle.is_deployed()
{
cx.propagate();
} else {
self.context_strip.focus_handle(cx).focus(window);
}
}
}
impl FocusableView for MessageEditor {
fn focus_handle(&self, cx: &AppContext) -> gpui::FocusHandle {
impl Focusable for MessageEditor {
fn focus_handle(&self, cx: &App) -> 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 Context<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;
let is_streaming_completion = self.thread.read(cx).is_streaming();
let button_width = px(64.);
let is_model_selected = self.is_model_selected(cx);
let is_editor_empty = self.is_editor_empty(cx);
let submit_label_color = if is_editor_empty {
Color::Muted
} else {
Color::Default
};
v_flex()
.key_context("MessageEditor")
@@ -239,6 +320,8 @@ impl Render for MessageEditor {
.on_action(cx.listener(Self::toggle_model_selector))
.on_action(cx.listener(Self::toggle_context_picker))
.on_action(cx.listener(Self::remove_all_context))
.on_action(cx.listener(Self::move_up))
.on_action(cx.listener(Self::toggle_chat_mode))
.size_full()
.gap_2()
.p_2()
@@ -271,46 +354,119 @@ impl Render for MessageEditor {
})
.child(
PopoverMenu::new("inline-context-picker")
.menu(move |_cx| Some(inline_context_picker.clone()))
.menu(move |window, cx| {
inline_context_picker.update(cx, |this, cx| {
this.init(window, cx);
});
Some(inline_context_picker.clone())
})
.attach(gpui::Corner::TopLeft)
.anchor(gpui::Corner::BottomLeft)
.offset(gpui::Point {
x: px(0.0),
y: (-get_ui_font_size(cx) * 2) - px(4.0),
y: px(-ThemeSettings::clamp_font_size(
ThemeSettings::get_global(cx).ui_font_size,
)
.0 * 2.0)
- px(4.0),
})
.with_handle(self.inline_context_picker_menu_handle.clone()),
)
.child(
h_flex()
.justify_between()
.child(SwitchWithLabel::new(
"use-tools",
Label::new("Tools").size(LabelSize::Small),
self.use_tools.into(),
cx.listener(|this, selection, _cx| {
this.use_tools = match selection {
ToggleState::Selected => true,
ToggleState::Unselected | ToggleState::Indeterminate => {
false
}
};
}),
))
.child(
h_flex().gap_1().child(self.model_selector.clone()).child(
ButtonLike::new("chat")
.style(ButtonStyle::Filled)
.layer(ElevationIndex::ModalSurface)
.child(Label::new("Submit").size(LabelSize::Small))
.children(
KeyBinding::for_action_in(&Chat, &focus_handle, cx)
.map(|binding| binding.into_any_element()),
Switch::new("use-tools", self.use_tools.into())
.label("Tools")
.on_click(cx.listener(|this, selection, _window, _cx| {
this.use_tools = match selection {
ToggleState::Selected => true,
ToggleState::Unselected
| ToggleState::Indeterminate => false,
};
}))
.key_binding(KeyBinding::for_action_in(
&ChatMode,
&focus_handle,
window,
)),
)
.child(h_flex().gap_1().child(self.model_selector.clone()).child(
if is_streaming_completion {
ButtonLike::new("cancel-generation")
.width(button_width.into())
.style(ButtonStyle::Tinted(TintColor::Accent))
.child(
h_flex()
.w_full()
.justify_between()
.child(
Label::new("Cancel")
.size(LabelSize::Small)
.with_animation(
"pulsating-label",
Animation::new(Duration::from_secs(2))
.repeat()
.with_easing(pulsating_between(
0.4, 0.8,
)),
|label, delta| label.alpha(delta),
),
)
.children(
KeyBinding::for_action_in(
&editor::actions::Cancel,
&focus_handle,
window,
)
.map(|binding| binding.into_any_element()),
),
)
.on_click(move |_event, cx| {
focus_handle.dispatch_action(&Chat, cx);
}),
),
),
.on_click(move |_event, window, cx| {
focus_handle.dispatch_action(
&editor::actions::Cancel,
window,
cx,
);
})
} else {
ButtonLike::new("submit-message")
.width(button_width.into())
.style(ButtonStyle::Filled)
.disabled(is_editor_empty || !is_model_selected)
.child(
h_flex()
.w_full()
.justify_between()
.child(
Label::new("Submit")
.size(LabelSize::Small)
.color(submit_label_color),
)
.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);
})
.when(is_editor_empty, |button| {
button
.tooltip(Tooltip::text("Type a message to submit"))
})
.when(!is_model_selected, |button| {
button.tooltip(Tooltip::text(
"Select a model to continue",
))
})
},
)),
),
)
}

View File

@@ -1,312 +0,0 @@
use anyhow::Result;
use assets::Assets;
use fs::Fs;
use futures::StreamExt;
use gpui::AssetSource;
use handlebars::{Handlebars, RenderError};
use language::{BufferSnapshot, LanguageName, Point};
use parking_lot::Mutex;
use serde::Serialize;
use std::{ops::Range, path::PathBuf, sync::Arc, time::Duration};
use text::LineEnding;
use util::ResultExt;
#[derive(Serialize)]
pub struct ContentPromptDiagnosticContext {
pub line_number: usize,
pub error_message: String,
pub code_content: String,
}
#[derive(Serialize)]
pub struct ContentPromptContext {
pub content_type: String,
pub language_name: Option<String>,
pub is_insert: bool,
pub is_truncated: bool,
pub document_content: String,
pub user_prompt: String,
pub rewrite_section: Option<String>,
pub diagnostic_errors: Vec<ContentPromptDiagnosticContext>,
}
#[derive(Serialize)]
pub struct TerminalAssistantPromptContext {
pub os: String,
pub arch: String,
pub shell: Option<String>,
pub working_directory: Option<String>,
pub latest_output: Vec<String>,
pub user_prompt: String,
}
#[derive(Serialize)]
pub struct ProjectSlashCommandPromptContext {
pub context_buffer: String,
}
pub struct PromptLoadingParams<'a> {
pub fs: Arc<dyn Fs>,
pub repo_path: Option<PathBuf>,
pub cx: &'a gpui::AppContext,
}
pub struct PromptBuilder {
handlebars: Arc<Mutex<Handlebars<'static>>>,
}
impl PromptBuilder {
pub fn new(loading_params: Option<PromptLoadingParams>) -> Result<Self> {
let mut handlebars = Handlebars::new();
Self::register_built_in_templates(&mut handlebars)?;
let handlebars = Arc::new(Mutex::new(handlebars));
if let Some(params) = loading_params {
Self::watch_fs_for_template_overrides(params, handlebars.clone());
}
Ok(Self { handlebars })
}
/// Watches the filesystem for changes to prompt template overrides.
///
/// This function sets up a file watcher on the prompt templates directory. It performs
/// an initial scan of the directory and registers any existing template overrides.
/// Then it continuously monitors for changes, reloading templates as they are
/// modified or added.
///
/// If the templates directory doesn't exist initially, it waits for it to be created.
/// If the directory is removed, it restores the built-in templates and waits for the
/// directory to be recreated.
///
/// # Arguments
///
/// * `params` - A `PromptLoadingParams` struct containing the filesystem, repository path,
/// and application context.
/// * `handlebars` - An `Arc<Mutex<Handlebars>>` for registering and updating templates.
fn watch_fs_for_template_overrides(
params: PromptLoadingParams,
handlebars: Arc<Mutex<Handlebars<'static>>>,
) {
let templates_dir = paths::prompt_overrides_dir(params.repo_path.as_deref());
params.cx.background_executor()
.spawn(async move {
let Some(parent_dir) = templates_dir.parent() else {
return;
};
let mut found_dir_once = false;
loop {
// Check if the templates directory exists and handle its status
// If it exists, log its presence and check if it's a symlink
// If it doesn't exist:
// - Log that we're using built-in prompts
// - Check if it's a broken symlink and log if so
// - Set up a watcher to detect when it's created
// After the first check, set the `found_dir_once` flag
// This allows us to avoid logging when looping back around after deleting the prompt overrides directory.
let dir_status = params.fs.is_dir(&templates_dir).await;
let symlink_status = params.fs.read_link(&templates_dir).await.ok();
if dir_status {
let mut log_message = format!("Prompt template overrides directory found at {}", templates_dir.display());
if let Some(target) = symlink_status {
log_message.push_str(" -> ");
log_message.push_str(&target.display().to_string());
}
log::info!("{}.", log_message);
} else {
if !found_dir_once {
log::info!("No prompt template overrides directory found at {}. Using built-in prompts.", templates_dir.display());
if let Some(target) = symlink_status {
log::info!("Symlink found pointing to {}, but target is invalid.", target.display());
}
}
if params.fs.is_dir(parent_dir).await {
let (mut changes, _watcher) = params.fs.watch(parent_dir, Duration::from_secs(1)).await;
while let Some(changed_paths) = changes.next().await {
if changed_paths.iter().any(|p| &p.path == &templates_dir) {
let mut log_message = format!("Prompt template overrides directory detected at {}", templates_dir.display());
if let Ok(target) = params.fs.read_link(&templates_dir).await {
log_message.push_str(" -> ");
log_message.push_str(&target.display().to_string());
}
log::info!("{}.", log_message);
break;
}
}
} else {
return;
}
}
found_dir_once = true;
// Initial scan of the prompt overrides directory
if let Ok(mut entries) = params.fs.read_dir(&templates_dir).await {
while let Some(Ok(file_path)) = entries.next().await {
if file_path.to_string_lossy().ends_with(".hbs") {
if let Ok(content) = params.fs.load(&file_path).await {
let file_name = file_path.file_stem().unwrap().to_string_lossy();
log::debug!("Registering prompt template override: {}", file_name);
handlebars.lock().register_template_string(&file_name, content).log_err();
}
}
}
}
// Watch both the parent directory and the template overrides directory:
// - Monitor the parent directory to detect if the template overrides directory is deleted.
// - Monitor the template overrides directory to re-register templates when they change.
// Combine both watch streams into a single stream.
let (parent_changes, parent_watcher) = params.fs.watch(parent_dir, Duration::from_secs(1)).await;
let (changes, watcher) = params.fs.watch(&templates_dir, Duration::from_secs(1)).await;
let mut combined_changes = futures::stream::select(changes, parent_changes);
while let Some(changed_paths) = combined_changes.next().await {
if changed_paths.iter().any(|p| &p.path == &templates_dir) {
if !params.fs.is_dir(&templates_dir).await {
log::info!("Prompt template overrides directory removed. Restoring built-in prompt templates.");
Self::register_built_in_templates(&mut handlebars.lock()).log_err();
break;
}
}
for event in changed_paths {
if event.path.starts_with(&templates_dir) && event.path.extension().map_or(false, |ext| ext == "hbs") {
log::info!("Reloading prompt template override: {}", event.path.display());
if let Some(content) = params.fs.load(&event.path).await.log_err() {
let file_name = event.path.file_stem().unwrap().to_string_lossy();
handlebars.lock().register_template_string(&file_name, content).log_err();
}
}
}
}
drop(watcher);
drop(parent_watcher);
}
})
.detach();
}
fn register_built_in_templates(handlebars: &mut Handlebars) -> Result<()> {
for path in Assets.list("prompts")? {
if let Some(id) = path.split('/').last().and_then(|s| s.strip_suffix(".hbs")) {
if let Some(prompt) = Assets.load(path.as_ref()).log_err().flatten() {
log::debug!("Registering built-in prompt template: {}", id);
let prompt = String::from_utf8_lossy(prompt.as_ref());
handlebars.register_template_string(id, LineEnding::normalize_cow(prompt))?
}
}
}
Ok(())
}
pub fn generate_inline_transformation_prompt(
&self,
user_prompt: String,
language_name: Option<&LanguageName>,
buffer: BufferSnapshot,
range: Range<usize>,
) -> Result<String, RenderError> {
let content_type = match language_name.as_ref().map(|l| l.0.as_ref()) {
None | Some("Markdown" | "Plain Text") => "text",
Some(_) => "code",
};
const MAX_CTX: usize = 50000;
let is_insert = range.is_empty();
let mut is_truncated = false;
let before_range = 0..range.start;
let truncated_before = if before_range.len() > MAX_CTX {
is_truncated = true;
let start = buffer.clip_offset(range.start - MAX_CTX, text::Bias::Right);
start..range.start
} else {
before_range
};
let after_range = range.end..buffer.len();
let truncated_after = if after_range.len() > MAX_CTX {
is_truncated = true;
let end = buffer.clip_offset(range.end + MAX_CTX, text::Bias::Left);
range.end..end
} else {
after_range
};
let mut document_content = String::new();
for chunk in buffer.text_for_range(truncated_before) {
document_content.push_str(chunk);
}
if is_insert {
document_content.push_str("<insert_here></insert_here>");
} else {
document_content.push_str("<rewrite_this>\n");
for chunk in buffer.text_for_range(range.clone()) {
document_content.push_str(chunk);
}
document_content.push_str("\n</rewrite_this>");
}
for chunk in buffer.text_for_range(truncated_after) {
document_content.push_str(chunk);
}
let rewrite_section = if !is_insert {
let mut section = String::new();
for chunk in buffer.text_for_range(range.clone()) {
section.push_str(chunk);
}
Some(section)
} else {
None
};
let diagnostics = buffer.diagnostics_in_range::<_, Point>(range, false);
let diagnostic_errors: Vec<ContentPromptDiagnosticContext> = diagnostics
.map(|entry| {
let start = entry.range.start;
ContentPromptDiagnosticContext {
line_number: (start.row + 1) as usize,
error_message: entry.diagnostic.message.clone(),
code_content: buffer.text_for_range(entry.range.clone()).collect(),
}
})
.collect();
let context = ContentPromptContext {
content_type: content_type.to_string(),
language_name: language_name.map(|s| s.to_string()),
is_insert,
is_truncated,
document_content,
user_prompt,
rewrite_section,
diagnostic_errors,
};
self.handlebars.lock().render("content_prompt", &context)
}
pub fn generate_terminal_assistant_prompt(
&self,
user_prompt: &str,
shell: Option<&str>,
working_directory: Option<&str>,
latest_output: &[String],
) -> Result<String, RenderError> {
let context = TerminalAssistantPromptContext {
os: std::env::consts::OS.to_string(),
arch: std::env::consts::ARCH.to_string(),
shell: shell.map(|s| s.to_string()),
working_directory: working_directory.map(|s| s.to_string()),
latest_output: latest_output.to_vec(),
user_prompt: user_prompt.to_string(),
};
self.handlebars
.lock()
.render("terminal_assistant_prompt", &context)
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,7 @@
use crate::inline_prompt_editor::CodegenStatus;
use client::telemetry::Telemetry;
use futures::{channel::mpsc, SinkExt, StreamExt};
use gpui::{AppContext, EventEmitter, Model, ModelContext, Task};
use gpui::{App, Context, Entity, EventEmitter, Task};
use language_model::{LanguageModelRegistry, LanguageModelRequest};
use language_models::report_assistant_event;
use std::{sync::Arc, time::Instant};
@@ -11,7 +11,7 @@ use terminal::Terminal;
pub struct TerminalCodegen {
pub status: CodegenStatus,
pub telemetry: Option<Arc<Telemetry>>,
terminal: Model<Terminal>,
terminal: Entity<Terminal>,
generation: Task<()>,
pub message_id: Option<String>,
transaction: Option<TerminalTransaction>,
@@ -20,7 +20,7 @@ pub struct TerminalCodegen {
impl EventEmitter<CodegenEvent> for TerminalCodegen {}
impl TerminalCodegen {
pub fn new(terminal: Model<Terminal>, telemetry: Option<Arc<Telemetry>>) -> Self {
pub fn new(terminal: Entity<Terminal>, telemetry: Option<Arc<Telemetry>>) -> Self {
Self {
terminal,
telemetry,
@@ -31,7 +31,7 @@ impl TerminalCodegen {
}
}
pub fn start(&mut self, prompt: LanguageModelRequest, cx: &mut ModelContext<Self>) {
pub fn start(&mut self, prompt: LanguageModelRequest, cx: &mut Context<Self>) {
let Some(model) = LanguageModelRegistry::read_global(cx).active_model() else {
return;
};
@@ -131,20 +131,20 @@ impl TerminalCodegen {
cx.notify();
}
pub fn stop(&mut self, cx: &mut ModelContext<Self>) {
pub fn stop(&mut self, cx: &mut Context<Self>) {
self.status = CodegenStatus::Done;
self.generation = Task::ready(());
cx.emit(CodegenEvent::Finished);
cx.notify();
}
pub fn complete(&mut self, cx: &mut ModelContext<Self>) {
pub fn complete(&mut self, cx: &mut Context<Self>) {
if let Some(transaction) = self.transaction.take() {
transaction.complete(cx);
}
}
pub fn undo(&mut self, cx: &mut ModelContext<Self>) {
pub fn undo(&mut self, cx: &mut Context<Self>) {
if let Some(transaction) = self.transaction.take() {
transaction.undo(cx);
}
@@ -160,27 +160,27 @@ pub const CLEAR_INPUT: &str = "\x15";
const CARRIAGE_RETURN: &str = "\x0d";
struct TerminalTransaction {
terminal: Model<Terminal>,
terminal: Entity<Terminal>,
}
impl TerminalTransaction {
pub fn start(terminal: Model<Terminal>) -> Self {
pub fn start(terminal: Entity<Terminal>) -> Self {
Self { terminal }
}
pub fn push(&mut self, hunk: String, cx: &mut AppContext) {
pub fn push(&mut self, hunk: String, cx: &mut App) {
// 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) {
pub fn undo(&self, cx: &mut App) {
self.terminal
.update(cx, |terminal, _| terminal.input(CLEAR_INPUT.to_string()));
}
pub fn complete(&self, cx: &mut AppContext) {
pub fn complete(&self, cx: &mut App) {
self.terminal.update(cx, |terminal, _| {
terminal.input(CARRIAGE_RETURN.to_string())
});

View File

@@ -3,7 +3,6 @@ use crate::context_store::ContextStore;
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 anyhow::{Context as _, Result};
@@ -11,15 +10,13 @@ use client::telemetry::Telemetry;
use collections::{HashMap, VecDeque};
use editor::{actions::SelectAll, MultiBuffer};
use fs::Fs;
use gpui::{
AppContext, Context, FocusableView, Global, Model, Subscription, UpdateGlobal, View, WeakModel,
WeakView,
};
use gpui::{App, Entity, Focusable, Global, Subscription, UpdateGlobal, WeakEntity};
use language::Buffer;
use language_model::{
LanguageModelRegistry, LanguageModelRequest, LanguageModelRequestMessage, Role,
};
use language_models::report_assistant_event;
use prompt_library::PromptBuilder;
use std::sync::Arc;
use telemetry_events::{AssistantEvent, AssistantKind, AssistantPhase};
use terminal_view::TerminalView;
@@ -31,7 +28,7 @@ pub fn init(
fs: Arc<dyn Fs>,
prompt_builder: Arc<PromptBuilder>,
telemetry: Arc<Telemetry>,
cx: &mut AppContext,
cx: &mut App,
) {
cx.set_global(TerminalInlineAssistant::new(fs, prompt_builder, telemetry));
}
@@ -68,20 +65,20 @@ impl TerminalInlineAssistant {
pub fn assist(
&mut self,
terminal_view: &View<TerminalView>,
workspace: WeakView<Workspace>,
thread_store: Option<WeakModel<ThreadStore>>,
cx: &mut WindowContext,
terminal_view: &Entity<TerminalView>,
workspace: WeakEntity<Workspace>,
thread_store: Option<WeakEntity<ThreadStore>>,
window: &mut Window,
cx: &mut App,
) {
let terminal = terminal_view.read(cx).terminal().clone();
let assist_id = self.next_assist_id.post_inc();
let prompt_buffer = cx.new_model(|cx| {
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(|_| TerminalCodegen::new(terminal, self.telemetry.clone()));
let prompt_buffer =
cx.new(|cx| MultiBuffer::singleton(cx.new(|cx| Buffer::local(String::new(), cx)), cx));
let context_store = cx.new(|_cx| ContextStore::new(workspace.clone()));
let codegen = cx.new(|_| TerminalCodegen::new(terminal, self.telemetry.clone()));
let prompt_editor = cx.new_view(|cx| {
let prompt_editor = cx.new(|cx| {
PromptEditor::new_terminal(
assist_id,
self.prompt_history.clone(),
@@ -91,6 +88,7 @@ impl TerminalInlineAssistant {
context_store.clone(),
workspace.clone(),
thread_store.clone(),
window,
cx,
)
});
@@ -100,7 +98,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(
@@ -109,21 +107,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 App,
) {
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);
});
});
}
@@ -131,9 +135,10 @@ impl TerminalInlineAssistant {
fn handle_prompt_editor_event(
&mut self,
prompt_editor: View<PromptEditor<TerminalCodegen>>,
prompt_editor: Entity<PromptEditor<TerminalCodegen>>,
event: &PromptEditorEvent,
cx: &mut WindowContext,
window: &mut Window,
cx: &mut App,
) {
let assist_id = prompt_editor.read(cx).id();
match event {
@@ -144,21 +149,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 App) {
let assist = if let Some(assist) = self.assists.get_mut(&assist_id) {
assist
} else {
@@ -196,7 +201,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 App) {
let assist = if let Some(assist) = self.assists.get_mut(&assist_id) {
assist
} else {
@@ -209,7 +214,7 @@ impl TerminalInlineAssistant {
fn request_for_inline_assist(
&self,
assist_id: TerminalInlineAssistId,
cx: &mut WindowContext,
cx: &mut App,
) -> Result<LanguageModelRequest> {
let assist = self.assists.get(&assist_id).context("invalid assist")?;
@@ -217,7 +222,7 @@ impl TerminalInlineAssistant {
let (latest_output, working_directory) = assist
.terminal
.update(cx, |terminal, cx| {
let terminal = terminal.model().read(cx);
let terminal = terminal.entity().read(cx);
let latest_output = terminal.last_n_non_empty_lines(DEFAULT_CONTEXT_LINES);
let working_directory = terminal
.working_directory()
@@ -245,10 +250,10 @@ impl TerminalInlineAssistant {
cache: false,
};
let context = assist
.context_store
.update(cx, |this, _cx| this.context().clone());
attach_context_to_message(&mut request_message, context);
attach_context_to_message(
&mut request_message,
assist.context_store.read(cx).snapshot(cx),
);
request_message.content.push(prompt.into());
@@ -265,16 +270,17 @@ impl TerminalInlineAssistant {
assist_id: TerminalInlineAssistId,
undo: bool,
execute: bool,
cx: &mut WindowContext,
window: &mut Window,
cx: &mut App,
) {
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();
@@ -317,7 +323,8 @@ impl TerminalInlineAssistant {
fn dismiss_assist(
&mut self,
assist_id: TerminalInlineAssistId,
cx: &mut WindowContext,
window: &mut Window,
cx: &mut App,
) -> bool {
let Some(assist) = self.assists.get_mut(&assist_id) else {
return false;
@@ -330,7 +337,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()
}
@@ -339,7 +346,8 @@ impl TerminalInlineAssistant {
&mut self,
assist_id: TerminalInlineAssistId,
height: u8,
cx: &mut WindowContext,
window: &mut Window,
cx: &mut App,
) {
if let Some(assist) = self.assists.get_mut(&assist_id) {
if let Some(prompt_editor) = assist.prompt_editor.as_ref().cloned() {
@@ -351,7 +359,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();
}
@@ -360,22 +368,23 @@ impl TerminalInlineAssistant {
}
struct TerminalInlineAssist {
terminal: WeakView<TerminalView>,
prompt_editor: Option<View<PromptEditor<TerminalCodegen>>>,
codegen: Model<TerminalCodegen>,
workspace: WeakView<Workspace>,
context_store: Model<ContextStore>,
terminal: WeakEntity<TerminalView>,
prompt_editor: Option<Entity<PromptEditor<TerminalCodegen>>>,
codegen: Entity<TerminalCodegen>,
workspace: WeakEntity<Workspace>,
context_store: Entity<ContextStore>,
_subscriptions: Vec<Subscription>,
}
impl TerminalInlineAssist {
pub fn new(
assist_id: TerminalInlineAssistId,
terminal: &View<TerminalView>,
prompt_editor: View<PromptEditor<TerminalCodegen>>,
workspace: WeakView<Workspace>,
context_store: Model<ContextStore>,
cx: &mut WindowContext,
terminal: &Entity<TerminalView>,
prompt_editor: Entity<PromptEditor<TerminalCodegen>>,
workspace: WeakEntity<Workspace>,
context_store: Entity<ContextStore>,
window: &mut Window,
cx: &mut App,
) -> Self {
let codegen = prompt_editor.read(cx).codegen().clone();
Self {
@@ -385,12 +394,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) {
@@ -419,7 +428,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);
}
}
})

View File

@@ -3,10 +3,10 @@ use std::sync::Arc;
use anyhow::Result;
use assistant_tool::ToolWorkingSet;
use chrono::{DateTime, Utc};
use collections::{HashMap, HashSet};
use collections::{BTreeMap, HashMap, HashSet};
use futures::future::Shared;
use futures::{FutureExt as _, StreamExt as _};
use gpui::{AppContext, EventEmitter, ModelContext, SharedString, Task};
use gpui::{App, Context, EventEmitter, SharedString, Task};
use language_model::{
LanguageModel, LanguageModelCompletionEvent, LanguageModelRegistry, LanguageModelRequest,
LanguageModelRequestMessage, LanguageModelToolResult, LanguageModelToolUse,
@@ -17,7 +17,8 @@ use serde::{Deserialize, Serialize};
use util::{post_inc, TryFutureExt as _};
use uuid::Uuid;
use crate::context::{attach_context_to_message, Context, ContextId};
use crate::context::{attach_context_to_message, ContextId, ContextSnapshot};
use crate::thread_store::SavedThread;
#[derive(Debug, Clone, Copy)]
pub enum RequestKind {
@@ -64,7 +65,7 @@ pub struct Thread {
pending_summary: Task<Option<()>>,
messages: Vec<Message>,
next_message_id: MessageId,
context: HashMap<ContextId, Context>,
context: BTreeMap<ContextId, ContextSnapshot>,
context_by_message: HashMap<MessageId, Vec<ContextId>>,
completion_count: usize,
pending_completions: Vec<PendingCompletion>,
@@ -75,7 +76,7 @@ pub struct Thread {
}
impl Thread {
pub fn new(tools: Arc<ToolWorkingSet>, _cx: &mut ModelContext<Self>) -> Self {
pub fn new(tools: Arc<ToolWorkingSet>, _cx: &mut Context<Self>) -> Self {
Self {
id: ThreadId::new(),
updated_at: Utc::now(),
@@ -83,7 +84,41 @@ impl Thread {
pending_summary: Task::ready(None),
messages: Vec::new(),
next_message_id: MessageId(0),
context: HashMap::default(),
context: BTreeMap::default(),
context_by_message: HashMap::default(),
completion_count: 0,
pending_completions: Vec::new(),
tools,
tool_uses_by_message: HashMap::default(),
tool_results_by_message: HashMap::default(),
pending_tool_uses_by_id: HashMap::default(),
}
}
pub fn from_saved(
id: ThreadId,
saved: SavedThread,
tools: Arc<ToolWorkingSet>,
_cx: &mut Context<Self>,
) -> Self {
let next_message_id = MessageId(saved.messages.len());
Self {
id,
updated_at: saved.updated_at,
summary: Some(saved.summary),
pending_summary: Task::ready(None),
messages: saved
.messages
.into_iter()
.map(|message| Message {
id: message.id,
role: message.role,
text: message.text,
})
.collect(),
next_message_id,
context: BTreeMap::default(),
context_by_message: HashMap::default(),
completion_count: 0,
pending_completions: Vec::new(),
@@ -114,7 +149,12 @@ impl Thread {
self.summary.clone()
}
pub fn set_summary(&mut self, summary: impl Into<SharedString>, cx: &mut ModelContext<Self>) {
pub fn summary_or_default(&self) -> SharedString {
const DEFAULT: SharedString = SharedString::new_static("New Thread");
self.summary.clone().unwrap_or(DEFAULT)
}
pub fn set_summary(&mut self, summary: impl Into<SharedString>, cx: &mut Context<Self>) {
self.summary = Some(summary.into());
cx.emit(ThreadEvent::SummaryChanged);
}
@@ -127,11 +167,15 @@ impl Thread {
self.messages.iter()
}
pub fn is_streaming(&self) -> bool {
!self.pending_completions.is_empty()
}
pub fn tools(&self) -> &Arc<ToolWorkingSet> {
&self.tools
}
pub fn context_for_message(&self, id: MessageId) -> Option<Vec<Context>> {
pub fn context_for_message(&self, id: MessageId) -> Option<Vec<ContextSnapshot>> {
let context = self.context_by_message.get(&id)?;
Some(
context
@@ -149,8 +193,8 @@ impl Thread {
pub fn insert_user_message(
&mut self,
text: impl Into<String>,
context: Vec<Context>,
cx: &mut ModelContext<Self>,
context: Vec<ContextSnapshot>,
cx: &mut Context<Self>,
) {
let message_id = self.insert_message(Role::User, text, cx);
let context_ids = context.iter().map(|context| context.id).collect::<Vec<_>>();
@@ -163,7 +207,7 @@ impl Thread {
&mut self,
role: Role,
text: impl Into<String>,
cx: &mut ModelContext<Self>,
cx: &mut Context<Self>,
) -> MessageId {
let id = self.next_message_id.post_inc();
self.messages.push(Message {
@@ -200,7 +244,7 @@ impl Thread {
pub fn to_completion_request(
&self,
_request_kind: RequestKind,
_cx: &AppContext,
_cx: &App,
) -> LanguageModelRequest {
let mut request = LanguageModelRequest {
messages: vec![],
@@ -270,7 +314,7 @@ impl Thread {
&mut self,
request: LanguageModelRequest,
model: Arc<dyn LanguageModel>,
cx: &mut ModelContext<Self>,
cx: &mut Context<Self>,
) {
let pending_completion_id = post_inc(&mut self.completion_count);
@@ -299,6 +343,13 @@ impl Thread {
last_message.id,
chunk,
));
} else {
// If we won't have an Assistant message yet, assume this chunk marks the beginning
// of a new Assistant response.
//
// Importantly: We do *not* want to emit a `StreamedAssistantText` event here, as it
// will result in duplicating the text of the chunk in the rendered Markdown.
thread.insert_message(Role::Assistant, chunk, cx);
}
}
}
@@ -352,7 +403,7 @@ impl Thread {
let result = stream_completion.await;
thread
.update(&mut cx, |_thread, cx| match result.as_ref() {
.update(&mut cx, |thread, cx| match result.as_ref() {
Ok(stop_reason) => match stop_reason {
StopReason::ToolUse => {
cx.emit(ThreadEvent::UsePendingTools);
@@ -375,6 +426,8 @@ impl Thread {
SharedString::from(error_message.clone()),
)));
}
thread.cancel_last_completion();
}
})
.ok();
@@ -386,7 +439,7 @@ impl Thread {
});
}
pub fn summarize(&mut self, cx: &mut ModelContext<Self>) {
pub fn summarize(&mut self, cx: &mut Context<Self>) {
let Some(provider) = LanguageModelRegistry::read_global(cx).active_provider() else {
return;
};
@@ -444,7 +497,7 @@ impl Thread {
assistant_message_id: MessageId,
tool_use_id: LanguageModelToolUseId,
output: Task<Result<String>>,
cx: &mut ModelContext<Self>,
cx: &mut Context<Self>,
) {
let insert_output_task = cx.spawn(|thread, mut cx| {
let tool_use_id = tool_use_id.clone();
@@ -497,6 +550,17 @@ impl Thread {
};
}
}
/// Cancels the last pending completion, if there are any pending.
///
/// Returns whether a completion was canceled.
pub fn cancel_last_completion(&mut self) -> bool {
if let Some(_last_completion) = self.pending_completions.pop() {
true
} else {
false
}
}
}
#[derive(Debug, Clone)]

View File

@@ -1,51 +1,146 @@
use gpui::{
uniform_list, AppContext, FocusHandle, FocusableView, Model, UniformListScrollHandle, WeakView,
uniform_list, App, Entity, FocusHandle, Focusable, ScrollStrategy, UniformListScrollHandle,
WeakEntity,
};
use time::{OffsetDateTime, UtcOffset};
use ui::{prelude::*, IconButtonShape, ListItem, ListItemSpacing, Tooltip};
use crate::thread::Thread;
use crate::thread_store::ThreadStore;
use crate::AssistantPanel;
use crate::thread_store::{SavedThreadMetadata, ThreadStore};
use crate::{AssistantPanel, RemoveSelectedThread};
pub struct ThreadHistory {
focus_handle: FocusHandle,
assistant_panel: WeakView<AssistantPanel>,
thread_store: Model<ThreadStore>,
assistant_panel: WeakEntity<AssistantPanel>,
thread_store: Entity<ThreadStore>,
scroll_handle: UniformListScrollHandle,
selected_index: usize,
}
impl ThreadHistory {
pub(crate) fn new(
assistant_panel: WeakView<AssistantPanel>,
thread_store: Model<ThreadStore>,
cx: &mut ViewContext<Self>,
assistant_panel: WeakEntity<AssistantPanel>,
thread_store: Entity<ThreadStore>,
cx: &mut Context<Self>,
) -> Self {
Self {
focus_handle: cx.focus_handle(),
assistant_panel,
thread_store,
scroll_handle: UniformListScrollHandle::default(),
selected_index: 0,
}
}
pub fn select_prev(
&mut self,
_: &menu::SelectPrev,
window: &mut Window,
cx: &mut Context<Self>,
) {
let count = self.thread_store.read(cx).thread_count();
if count > 0 {
if self.selected_index == 0 {
self.set_selected_index(count - 1, window, cx);
} else {
self.set_selected_index(self.selected_index - 1, window, cx);
}
}
}
pub fn select_next(
&mut self,
_: &menu::SelectNext,
window: &mut Window,
cx: &mut Context<Self>,
) {
let count = self.thread_store.read(cx).thread_count();
if count > 0 {
if self.selected_index == count - 1 {
self.set_selected_index(0, window, cx);
} else {
self.set_selected_index(self.selected_index + 1, window, cx);
}
}
}
fn select_first(&mut self, _: &menu::SelectFirst, window: &mut Window, cx: &mut Context<Self>) {
let count = self.thread_store.read(cx).thread_count();
if count > 0 {
self.set_selected_index(0, window, cx);
}
}
fn select_last(&mut self, _: &menu::SelectLast, window: &mut Window, cx: &mut Context<Self>) {
let count = self.thread_store.read(cx).thread_count();
if count > 0 {
self.set_selected_index(count - 1, window, cx);
}
}
fn set_selected_index(&mut self, index: usize, _window: &mut Window, cx: &mut Context<Self>) {
self.selected_index = index;
self.scroll_handle
.scroll_to_item(index, ScrollStrategy::Top);
cx.notify();
}
fn confirm(&mut self, _: &menu::Confirm, window: &mut Window, cx: &mut Context<Self>) {
let threads = self.thread_store.update(cx, |this, _cx| this.threads());
if let Some(thread) = threads.get(self.selected_index) {
self.assistant_panel
.update(cx, move |this, cx| this.open_thread(&thread.id, window, cx))
.ok();
cx.notify();
}
}
fn remove_selected_thread(
&mut self,
_: &RemoveSelectedThread,
_window: &mut Window,
cx: &mut Context<Self>,
) {
let threads = self.thread_store.update(cx, |this, _cx| this.threads());
if let Some(thread) = threads.get(self.selected_index) {
self.assistant_panel
.update(cx, |this, cx| {
this.delete_thread(&thread.id, cx);
})
.ok();
cx.notify();
}
}
}
impl FocusableView for ThreadHistory {
fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
impl Focusable for ThreadHistory {
fn focus_handle(&self, _cx: &App) -> FocusHandle {
self.focus_handle.clone()
}
}
impl Render for ThreadHistory {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let threads = self.thread_store.update(cx, |this, cx| this.threads(cx));
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let threads = self.thread_store.update(cx, |this, _cx| this.threads());
let selected_index = self.selected_index;
v_flex()
.id("thread-history-container")
.key_context("ThreadHistory")
.track_focus(&self.focus_handle)
.overflow_y_scroll()
.size_full()
.p_1()
.on_action(cx.listener(Self::select_prev))
.on_action(cx.listener(Self::select_next))
.on_action(cx.listener(Self::select_first))
.on_action(cx.listener(Self::select_last))
.on_action(cx.listener(Self::confirm))
.on_action(cx.listener(Self::remove_selected_thread))
.map(|history| {
if threads.is_empty() {
history
@@ -59,16 +154,18 @@ impl Render for ThreadHistory {
} else {
history.child(
uniform_list(
cx.view().clone(),
cx.entity().clone(),
"thread-history",
threads.len(),
move |history, range, _cx| {
move |history, range, _window, _cx| {
threads[range]
.iter()
.map(|thread| {
.enumerate()
.map(|(index, thread)| {
h_flex().w_full().pb_1().child(PastThread::new(
thread.clone(),
history.assistant_panel.clone(),
selected_index == index,
))
})
.collect()
@@ -84,33 +181,31 @@ impl Render for ThreadHistory {
#[derive(IntoElement)]
pub struct PastThread {
thread: Model<Thread>,
assistant_panel: WeakView<AssistantPanel>,
thread: SavedThreadMetadata,
assistant_panel: WeakEntity<AssistantPanel>,
selected: bool,
}
impl PastThread {
pub fn new(thread: Model<Thread>, assistant_panel: WeakView<AssistantPanel>) -> Self {
pub fn new(
thread: SavedThreadMetadata,
assistant_panel: WeakEntity<AssistantPanel>,
selected: bool,
) -> Self {
Self {
thread,
assistant_panel,
selected,
}
}
}
impl RenderOnce for PastThread {
fn render(self, cx: &mut WindowContext) -> impl IntoElement {
let (id, summary) = {
const DEFAULT_SUMMARY: SharedString = SharedString::new_static("New Thread");
let thread = self.thread.read(cx);
(
thread.id().clone(),
thread.summary().unwrap_or(DEFAULT_SUMMARY),
)
};
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
let summary = self.thread.summary;
let thread_timestamp = time_format::format_localized_timestamp(
OffsetDateTime::from_unix_timestamp(self.thread.read(cx).updated_at().timestamp())
.unwrap(),
OffsetDateTime::from_unix_timestamp(self.thread.updated_at.timestamp()).unwrap(),
OffsetDateTime::now_utc(),
self.assistant_panel
.update(cx, |this, _cx| this.local_timezone())
@@ -118,8 +213,9 @@ impl RenderOnce for PastThread {
time_format::TimestampFormat::EnhancedAbsolute,
);
ListItem::new(("past-thread", self.thread.entity_id()))
ListItem::new(SharedString::from(self.thread.id.to_string()))
.outlined()
.toggle_state(self.selected)
.start_slot(
Icon::new(IconName::MessageCircle)
.size(IconSize::Small)
@@ -139,11 +235,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| {
let id = self.thread.id.clone();
move |_event, _window, cx| {
assistant_panel
.update(cx, |this, cx| {
this.delete_thread(&id, cx);
@@ -155,11 +251,11 @@ impl RenderOnce for PastThread {
)
.on_click({
let assistant_panel = self.assistant_panel.clone();
let id = id.clone();
move |_event, cx| {
let id = self.thread.id.clone();
move |_event, window, cx| {
assistant_panel
.update(cx, |this, cx| {
this.open_thread(&id, cx);
this.open_thread(&id, window, cx).detach_and_log_err(cx);
})
.ok();
}

View File

@@ -1,90 +1,177 @@
use std::path::PathBuf;
use std::sync::Arc;
use anyhow::Result;
use anyhow::{anyhow, Result};
use assistant_tool::{ToolId, ToolWorkingSet};
use chrono::{DateTime, Utc};
use collections::HashMap;
use context_server::manager::ContextServerManager;
use context_server::{ContextServerFactoryRegistry, ContextServerTool};
use gpui::{prelude::*, AppContext, Model, ModelContext, Task};
use futures::future::{self, BoxFuture, Shared};
use futures::FutureExt as _;
use gpui::{prelude::*, App, BackgroundExecutor, Context, Entity, SharedString, Task};
use heed::types::SerdeBincode;
use heed::Database;
use language_model::Role;
use project::Project;
use unindent::Unindent;
use serde::{Deserialize, Serialize};
use util::ResultExt as _;
use crate::thread::{Thread, ThreadId};
use crate::thread::{MessageId, Thread, ThreadId};
pub struct ThreadStore {
#[allow(unused)]
project: Model<Project>,
project: Entity<Project>,
tools: Arc<ToolWorkingSet>,
context_server_manager: Model<ContextServerManager>,
context_server_manager: Entity<ContextServerManager>,
context_server_tool_ids: HashMap<Arc<str>, Vec<ToolId>>,
threads: Vec<Model<Thread>>,
threads: Vec<SavedThreadMetadata>,
database_future: Shared<BoxFuture<'static, Result<Arc<ThreadsDatabase>, Arc<anyhow::Error>>>>,
}
impl ThreadStore {
pub fn new(
project: Model<Project>,
project: Entity<Project>,
tools: Arc<ToolWorkingSet>,
cx: &mut AppContext,
) -> Task<Result<Model<Self>>> {
cx: &mut App,
) -> Task<Result<Entity<Self>>> {
cx.spawn(|mut cx| async move {
let this = cx.new_model(|cx: &mut ModelContext<Self>| {
let this = cx.new(|cx: &mut Context<Self>| {
let context_server_factory_registry =
ContextServerFactoryRegistry::default_global(cx);
let context_server_manager = cx.new_model(|cx| {
let context_server_manager = cx.new(|cx| {
ContextServerManager::new(context_server_factory_registry, project.clone(), cx)
});
let mut this = Self {
let executor = cx.background_executor().clone();
let database_future = executor
.spawn({
let executor = executor.clone();
let database_path = paths::support_dir().join("threads/threads-db.0.mdb");
async move { ThreadsDatabase::new(database_path, executor) }
})
.then(|result| future::ready(result.map(Arc::new).map_err(Arc::new)))
.boxed()
.shared();
let this = Self {
project,
tools,
context_server_manager,
context_server_tool_ids: HashMap::default(),
threads: Vec::new(),
database_future,
};
this.mock_recent_threads(cx);
this.register_context_server_handlers(cx);
this
})?;
log::info!("[assistant2-debug] reloading threads");
this.update(&mut cx, |this, cx| this.reload(cx))?.await?;
log::info!("[assistant2-debug] finished reloading threads");
Ok(this)
})
}
pub fn threads(&self, cx: &ModelContext<Self>) -> Vec<Model<Thread>> {
let mut threads = self
.threads
.iter()
.filter(|thread| !thread.read(cx).is_empty())
.cloned()
.collect::<Vec<_>>();
threads.sort_unstable_by_key(|thread| std::cmp::Reverse(thread.read(cx).updated_at()));
/// Returns the number of threads.
pub fn thread_count(&self) -> usize {
self.threads.len()
}
pub fn threads(&self) -> Vec<SavedThreadMetadata> {
let mut threads = self.threads.iter().cloned().collect::<Vec<_>>();
threads.sort_unstable_by_key(|thread| std::cmp::Reverse(thread.updated_at));
threads
}
pub fn recent_threads(&self, limit: usize, cx: &ModelContext<Self>) -> Vec<Model<Thread>> {
self.threads(cx).into_iter().take(limit).collect()
pub fn recent_threads(&self, limit: usize) -> Vec<SavedThreadMetadata> {
self.threads().into_iter().take(limit).collect()
}
pub fn create_thread(&mut self, cx: &mut ModelContext<Self>) -> Model<Thread> {
let thread = cx.new_model(|cx| Thread::new(self.tools.clone(), cx));
self.threads.push(thread.clone());
thread
pub fn create_thread(&mut self, cx: &mut Context<Self>) -> Entity<Thread> {
cx.new(|cx| Thread::new(self.tools.clone(), cx))
}
pub fn open_thread(&self, id: &ThreadId, cx: &mut ModelContext<Self>) -> Option<Model<Thread>> {
self.threads
.iter()
.find(|thread| thread.read(cx).id() == id)
.cloned()
pub fn open_thread(
&self,
id: &ThreadId,
cx: &mut Context<Self>,
) -> Task<Result<Entity<Thread>>> {
let id = id.clone();
let database_future = self.database_future.clone();
cx.spawn(|this, mut cx| async move {
let database = database_future.await.map_err(|err| anyhow!(err))?;
let thread = database
.try_find_thread(id.clone())
.await?
.ok_or_else(|| anyhow!("no thread found with ID: {id:?}"))?;
this.update(&mut cx, |this, cx| {
cx.new(|cx| Thread::from_saved(id.clone(), thread, this.tools.clone(), cx))
})
})
}
pub fn delete_thread(&mut self, id: &ThreadId, cx: &mut ModelContext<Self>) {
self.threads.retain(|thread| thread.read(cx).id() != id);
pub fn save_thread(&self, thread: &Entity<Thread>, cx: &mut Context<Self>) -> Task<Result<()>> {
let (metadata, thread) = thread.update(cx, |thread, _cx| {
let id = thread.id().clone();
let thread = SavedThread {
summary: thread.summary_or_default(),
updated_at: thread.updated_at(),
messages: thread
.messages()
.map(|message| SavedMessage {
id: message.id,
role: message.role,
text: message.text.clone(),
})
.collect(),
};
(id, thread)
});
let database_future = self.database_future.clone();
cx.spawn(|this, mut cx| async move {
let database = database_future.await.map_err(|err| anyhow!(err))?;
database.save_thread(metadata, thread).await?;
this.update(&mut cx, |this, cx| this.reload(cx))?.await
})
}
fn register_context_server_handlers(&self, cx: &mut ModelContext<Self>) {
pub fn delete_thread(&mut self, id: &ThreadId, cx: &mut Context<Self>) -> Task<Result<()>> {
let id = id.clone();
let database_future = self.database_future.clone();
cx.spawn(|this, mut cx| async move {
let database = database_future.await.map_err(|err| anyhow!(err))?;
database.delete_thread(id.clone()).await?;
this.update(&mut cx, |this, _cx| {
this.threads.retain(|thread| thread.id != id)
})
})
}
fn reload(&self, cx: &mut Context<Self>) -> Task<Result<()>> {
let database_future = self.database_future.clone();
cx.spawn(|this, mut cx| async move {
let threads = database_future
.await
.map_err(|err| anyhow!(err))?
.list_threads()
.await?;
this.update(&mut cx, |this, cx| {
this.threads = threads;
cx.notify();
})
})
}
fn register_context_server_handlers(&self, cx: &mut Context<Self>) {
cx.subscribe(
&self.context_server_manager.clone(),
Self::handle_context_server_event,
@@ -94,9 +181,9 @@ impl ThreadStore {
fn handle_context_server_event(
&mut self,
context_server_manager: Model<ContextServerManager>,
context_server_manager: Entity<ContextServerManager>,
event: &context_server::manager::Event,
cx: &mut ModelContext<Self>,
cx: &mut Context<Self>,
) {
let tool_working_set = self.tools.clone();
match event {
@@ -151,133 +238,108 @@ impl ThreadStore {
}
}
impl ThreadStore {
/// Creates some mocked recent threads for testing purposes.
fn mock_recent_threads(&mut self, cx: &mut ModelContext<Self>) {
use language_model::Role;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SavedThreadMetadata {
pub id: ThreadId,
pub summary: SharedString,
pub updated_at: DateTime<Utc>,
}
self.threads.push(cx.new_model(|cx| {
let mut thread = Thread::new(self.tools.clone(), cx);
thread.set_summary("Introduction to quantum computing", cx);
thread.insert_user_message("Hello! Can you help me understand quantum computing?", Vec::new(), cx);
thread.insert_message(Role::Assistant, "Of course! I'd be happy to help you understand quantum computing. Quantum computing is a fascinating field that uses the principles of quantum mechanics to process information. Unlike classical computers that use bits (0s and 1s), quantum computers use quantum bits or 'qubits'. These qubits can exist in multiple states simultaneously, a property called superposition. This allows quantum computers to perform certain calculations much faster than classical computers. What specific aspect of quantum computing would you like to know more about?", cx);
thread.insert_user_message("That's interesting! Can you explain how quantum entanglement is used in quantum computing?", Vec::new(), cx);
thread.insert_message(Role::Assistant, "Certainly! Quantum entanglement is a key principle used in quantum computing. When two qubits become entangled, the state of one qubit is directly related to the state of the other, regardless of the distance between them. This property is used in quantum computing to create complex quantum states and to perform operations on multiple qubits simultaneously. Entanglement allows quantum computers to process information in ways that classical computers cannot, potentially solving certain problems much more efficiently. For example, it's crucial in quantum error correction and in algorithms like quantum teleportation, which is important for quantum communication.", cx);
thread
}));
#[derive(Serialize, Deserialize)]
pub struct SavedThread {
pub summary: SharedString,
pub updated_at: DateTime<Utc>,
pub messages: Vec<SavedMessage>,
}
self.threads.push(cx.new_model(|cx| {
let mut thread = Thread::new(self.tools.clone(), cx);
thread.set_summary("Rust web development and async programming", cx);
thread.insert_user_message("Can you show me an example of Rust code for a simple web server?", Vec::new(), cx);
thread.insert_message(Role::Assistant, "Certainly! Here's an example of a simple web server in Rust using the `actix-web` framework:
#[derive(Serialize, Deserialize)]
pub struct SavedMessage {
pub id: MessageId,
pub role: Role,
pub text: String,
}
```rust
use actix_web::{web, App, HttpResponse, HttpServer, Responder};
struct ThreadsDatabase {
executor: BackgroundExecutor,
env: heed::Env,
threads: Database<SerdeBincode<ThreadId>, SerdeBincode<SavedThread>>,
}
async fn hello() -> impl Responder {
HttpResponse::Ok().body(\"Hello, World!\")
}
impl ThreadsDatabase {
pub fn new(path: PathBuf, executor: BackgroundExecutor) -> Result<Self> {
std::fs::create_dir_all(&path)?;
#[actix_web::main]
async fn main() -> std::io::Result<()> {
HttpServer::new(|| {
App::new()
.route(\"/\", web::get().to(hello))
})
.bind(\"127.0.0.1:8080\")?
.run()
.await
}
```
const ONE_GB_IN_BYTES: usize = 1024 * 1024 * 1024;
let env = unsafe {
heed::EnvOpenOptions::new()
.map_size(ONE_GB_IN_BYTES)
.max_dbs(1)
.open(path)?
};
This code creates a basic web server that responds with 'Hello, World!' when you access the root URL. Here's a breakdown of what's happening:
let mut txn = env.write_txn()?;
let threads = env.create_database(&mut txn, Some("threads"))?;
txn.commit()?;
1. We import necessary items from the `actix-web` crate.
2. We define an async `hello` function that returns a simple HTTP response.
3. In the `main` function, we set up the server to listen on `127.0.0.1:8080`.
4. We configure the app to respond to GET requests on the root path with our `hello` function.
Ok(Self {
executor,
env,
threads,
})
}
To run this, you'd need to add `actix-web` to your `Cargo.toml` dependencies:
pub fn list_threads(&self) -> Task<Result<Vec<SavedThreadMetadata>>> {
let env = self.env.clone();
let threads = self.threads;
```toml
[dependencies]
actix-web = \"4.0\"
```
Then you can run the server with `cargo run` and access it at `http://localhost:8080`.".unindent(), cx);
thread.insert_user_message("That's great! Can you explain more about async functions in Rust?", Vec::new(), cx);
thread.insert_message(Role::Assistant, "Certainly! Async functions are a key feature in Rust for writing efficient, non-blocking code, especially for I/O-bound operations. Here's an overview:
1. **Syntax**: Async functions are declared using the `async` keyword:
```rust
async fn my_async_function() -> Result<(), Error> {
// Asynchronous code here
}
```
2. **Futures**: Async functions return a `Future`. A `Future` represents a value that may not be available yet but will be at some point.
3. **Await**: Inside an async function, you can use the `.await` syntax to wait for other async operations to complete:
```rust
async fn fetch_data() -> Result<String, Error> {
let response = make_http_request().await?;
let data = process_response(response).await?;
Ok(data)
}
```
4. **Non-blocking**: Async functions allow the runtime to work on other tasks while waiting for I/O or other operations to complete, making efficient use of system resources.
5. **Runtime**: To execute async code, you need a runtime like `tokio` or `async-std`. Actix-web, which we used in the previous example, includes its own runtime.
6. **Error Handling**: Async functions work well with Rust's `?` operator for error handling.
Async programming in Rust provides a powerful way to write concurrent code that's both safe and efficient. It's particularly useful for servers, network programming, and any application that deals with many concurrent operations.".unindent(), cx);
thread
}));
self.threads.push(cx.new_model(|cx| {
let mut thread = Thread::new(self.tools.clone(), cx);
thread.set_summary("Rust code with long lines", cx);
thread.insert_user_message("Could you write me some Rust code with long lines?", Vec::new(), cx);
thread.insert_message(Role::Assistant, r#"Here's some Rust code with some intentionally long lines:
```rust
use std::collections::{HashMap, HashSet};
use std::sync::{Arc, Mutex};
use std::thread;
fn main() {
let very_long_vector = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25];
let complicated_hashmap: HashMap<String, Vec<(i32, f64, String)>> = [("key1".to_string(), vec![(1, 1.1, "value1".to_string()), (2, 2.2, "value2".to_string())]), ("key2".to_string(), vec![(3, 3.3, "value3".to_string()), (4, 4.4, "value4".to_string())])].iter().cloned().collect();
let nested_structure = Arc::new(Mutex::new(HashMap::new()));
let long_closure = |x: i32, y: i32, z: i32| -> i32 { let result = x * y + z; println!("The result of the long closure calculation is: {}", result); result };
let thread_handles: Vec<_> = (0..10).map(|i| {
let nested_structure_clone = Arc::clone(&nested_structure);
thread::spawn(move || {
let mut lock = nested_structure_clone.lock().unwrap();
lock.entry(format!("thread_{}", i)).or_insert_with(|| HashSet::new()).insert(i * i);
})
}).collect();
for handle in thread_handles {
handle.join().unwrap();
}
println!("The final state of the nested structure is: {:?}", nested_structure.lock().unwrap());
let complex_expression = very_long_vector.iter().filter(|&&x| x % 2 == 0).map(|&x| x * x).fold(0, |acc, x| acc + x) + long_closure(5, 10, 15);
println!("The result of the complex expression is: {}", complex_expression);
self.executor.spawn(async move {
let txn = env.read_txn()?;
let mut iter = threads.iter(&txn)?;
let mut threads = Vec::new();
while let Some((key, value)) = iter.next().transpose()? {
threads.push(SavedThreadMetadata {
id: key,
summary: value.summary,
updated_at: value.updated_at,
});
}
```"#.unindent(), cx);
thread
}));
Ok(threads)
})
}
pub fn try_find_thread(&self, id: ThreadId) -> Task<Result<Option<SavedThread>>> {
let env = self.env.clone();
let threads = self.threads;
self.executor.spawn(async move {
let txn = env.read_txn()?;
let thread = threads.get(&txn, &id)?;
Ok(thread)
})
}
pub fn save_thread(&self, id: ThreadId, thread: SavedThread) -> Task<Result<()>> {
let env = self.env.clone();
let threads = self.threads;
self.executor.spawn(async move {
let mut txn = env.write_txn()?;
threads.put(&mut txn, &id, &thread)?;
txn.commit()?;
Ok(())
})
}
pub fn delete_thread(&self, id: ThreadId) -> Task<Result<()>> {
let env = self.env.clone();
let threads = self.threads;
self.executor.spawn(async move {
let mut txn = env.write_txn()?;
threads.delete(&mut txn, &id)?;
txn.commit()?;
Ok(())
})
}
}

View File

@@ -3,41 +3,67 @@ use std::rc::Rc;
use gpui::ClickEvent;
use ui::{prelude::*, IconButtonShape, Tooltip};
use crate::context::{Context, ContextKind};
use crate::context::{ContextKind, ContextSnapshot};
#[derive(IntoElement)]
pub enum ContextPill {
Added {
context: Context,
context: ContextSnapshot,
dupe_name: bool,
on_remove: Option<Rc<dyn Fn(&ClickEvent, &mut WindowContext)>>,
focused: bool,
on_click: Option<Rc<dyn Fn(&ClickEvent, &mut Window, &mut App)>>,
on_remove: Option<Rc<dyn Fn(&ClickEvent, &mut Window, &mut App)>>,
},
Suggested {
name: SharedString,
icon_path: Option<SharedString>,
kind: ContextKind,
on_add: Rc<dyn Fn(&ClickEvent, &mut WindowContext)>,
focused: bool,
on_click: Option<Rc<dyn Fn(&ClickEvent, &mut Window, &mut App)>>,
},
}
impl ContextPill {
pub fn new_added(
context: Context,
pub fn added(
context: ContextSnapshot,
dupe_name: bool,
on_remove: Option<Rc<dyn Fn(&ClickEvent, &mut WindowContext)>>,
focused: bool,
on_remove: Option<Rc<dyn Fn(&ClickEvent, &mut Window, &mut App)>>,
) -> Self {
Self::Added {
context,
dupe_name,
on_remove,
focused,
on_click: None,
}
}
pub fn new_suggested(
pub fn suggested(
name: SharedString,
icon_path: Option<SharedString>,
kind: ContextKind,
on_add: Rc<dyn Fn(&ClickEvent, &mut WindowContext)>,
focused: bool,
) -> Self {
Self::Suggested { name, kind, on_add }
Self::Suggested {
name,
icon_path,
kind,
focused,
on_click: None,
}
}
pub fn on_click(mut self, listener: Rc<dyn Fn(&ClickEvent, &mut Window, &mut App)>) -> Self {
match &mut self {
ContextPill::Added { on_click, .. } => {
*on_click = Some(listener);
}
ContextPill::Suggested { on_click, .. } => {
*on_click = Some(listener);
}
}
self
}
pub fn id(&self) -> ElementId {
@@ -49,23 +75,27 @@ impl ContextPill {
}
}
pub fn kind(&self) -> &ContextKind {
pub fn icon(&self) -> Icon {
match self {
Self::Added { context, .. } => &context.kind,
Self::Suggested { kind, .. } => kind,
Self::Added { context, .. } => match &context.icon_path {
Some(icon_path) => Icon::from_path(icon_path),
None => Icon::new(context.kind.icon()),
},
Self::Suggested {
icon_path: Some(icon_path),
..
} => Icon::from_path(icon_path),
Self::Suggested {
kind,
icon_path: None,
..
} => Icon::new(kind.icon()),
}
}
}
impl RenderOnce for ContextPill {
fn render(self, cx: &mut WindowContext) -> impl IntoElement {
let icon = match &self.kind() {
ContextKind::File => IconName::File,
ContextKind::Directory => IconName::Folder,
ContextKind::FetchedUrl => IconName::Globe,
ContextKind::Thread => IconName::MessageCircle,
};
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
let color = cx.theme().colors();
let base_pill = h_flex()
@@ -75,16 +105,22 @@ impl RenderOnce for ContextPill {
.border_1()
.rounded_md()
.gap_1()
.child(Icon::new(icon).size(IconSize::XSmall).color(Color::Muted));
.child(self.icon().size(IconSize::XSmall).color(Color::Muted));
match &self {
ContextPill::Added {
context,
dupe_name,
on_remove,
focused,
on_click,
} => base_pill
.bg(color.element_background)
.border_color(color.border.opacity(0.5))
.border_color(if *focused {
color.border_focused
} else {
color.border.opacity(0.5)
})
.pr(if on_remove.is_some() { px(2.) } else { px(4.) })
.child(
h_flex()
@@ -103,7 +139,7 @@ impl RenderOnce for ContextPill {
}
})
.when_some(context.tooltip.clone(), |element, tooltip| {
element.tooltip(move |cx| Tooltip::text(tooltip.clone(), cx))
element.tooltip(Tooltip::text(tooltip.clone()))
}),
)
.when_some(on_remove.as_ref(), |element, on_remove| {
@@ -111,17 +147,31 @@ impl RenderOnce for ContextPill {
IconButton::new(("remove", context.id.0), IconName::Close)
.shape(IconButtonShape::Square)
.icon_size(IconSize::XSmall)
.tooltip(|cx| Tooltip::text("Remove Context", cx))
.tooltip(Tooltip::text("Remove Context"))
.on_click({
let on_remove = on_remove.clone();
move |event, cx| on_remove(event, cx)
move |event, window, cx| on_remove(event, window, cx)
}),
)
})
.when_some(on_click.as_ref(), |element, on_click| {
let on_click = on_click.clone();
element.on_click(move |event, window, cx| on_click(event, window, cx))
}),
ContextPill::Suggested { name, kind, on_add } => base_pill
ContextPill::Suggested {
name,
icon_path: _,
kind,
focused,
on_click,
} => base_pill
.cursor_pointer()
.pr_1()
.border_color(color.border_variant.opacity(0.5))
.border_color(if *focused {
color.border_focused
} else {
color.border_variant.opacity(0.5)
})
.hover(|style| style.bg(color.element_hover.opacity(0.5)))
.child(
Label::new(name.clone())
@@ -129,24 +179,28 @@ impl RenderOnce for ContextPill {
.color(Color::Muted),
)
.child(
Label::new(match kind {
ContextKind::File => "Open File",
ContextKind::Thread | ContextKind::Directory | ContextKind::FetchedUrl => {
"Active"
}
})
.size(LabelSize::XSmall)
.color(Color::Muted),
div().px_0p5().child(
Label::new(match kind {
ContextKind::File => "Active Tab",
ContextKind::Thread
| ContextKind::Directory
| ContextKind::FetchedUrl => "Active",
})
.size(LabelSize::XSmall)
.color(Color::Muted),
),
)
.child(
Icon::new(IconName::Plus)
.size(IconSize::XSmall)
.into_any_element(),
)
.tooltip(|cx| Tooltip::with_meta("Suggested Context", None, "Click to add it", cx))
.on_click({
let on_add = on_add.clone();
move |event, cx| on_add(event, cx)
.tooltip(|window, cx| {
Tooltip::with_meta("Suggested Context", None, "Click to add it", window, cx)
})
.when_some(on_click.as_ref(), |element, on_click| {
let on_click = on_click.clone();
element.on_click(move |event, window, cx| on_click(event, window, cx))
}),
}
}

View File

@@ -0,0 +1,68 @@
[package]
name = "assistant_context_editor"
version = "0.1.0"
edition.workspace = true
publish.workspace = true
license = "GPL-3.0-or-later"
[lints]
workspace = true
[lib]
path = "src/assistant_context_editor.rs"
[dependencies]
anyhow.workspace = true
assistant_settings.workspace = true
assistant_slash_command.workspace = true
assistant_slash_commands.workspace = true
assistant_tool.workspace = true
chrono.workspace = true
client.workspace = true
clock.workspace = true
collections.workspace = true
context_server.workspace = true
editor.workspace = true
feature_flags.workspace = true
fs.workspace = true
futures.workspace = true
fuzzy.workspace = true
gpui.workspace = true
indexed_docs.workspace = true
language.workspace = true
language_model.workspace = true
language_model_selector.workspace = true
language_models.workspace = true
log.workspace = true
multi_buffer.workspace = true
open_ai.workspace = true
parking_lot.workspace = true
paths.workspace = true
picker.workspace = true
project.workspace = true
prompt_library.workspace = true
regex.workspace = true
rope.workspace = true
rpc.workspace = true
serde.workspace = true
serde_json.workspace = true
settings.workspace = true
smallvec.workspace = true
smol.workspace = true
strum.workspace = true
telemetry_events.workspace = true
text.workspace = true
theme.workspace = true
ui.workspace = true
util.workspace = true
uuid.workspace = true
workspace.workspace = true
[dev-dependencies]
language_model = { workspace = true, features = ["test-support"] }
languages = { workspace = true, features = ["test-support"] }
pretty_assertions.workspace = true
rand.workspace = true
tree-sitter-md.workspace = true
unindent.workspace = true
workspace = { workspace = true, features = ["test-support"] }

View File

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

View File

@@ -0,0 +1,23 @@
mod context;
mod context_editor;
mod context_history;
mod context_store;
mod patch;
mod slash_command;
mod slash_command_picker;
use std::sync::Arc;
use client::Client;
use gpui::App;
pub use crate::context::*;
pub use crate::context_editor::*;
pub use crate::context_history::*;
pub use crate::context_store::*;
pub use crate::patch::*;
pub use crate::slash_command::*;
pub fn init(client: Arc<Client>, _cx: &mut App) {
context_store::init(&client.into());
}

View File

@@ -1,16 +1,13 @@
#[cfg(test)]
mod context_tests;
use crate::slash_command_working_set::SlashCommandWorkingSet;
use crate::{
prompts::PromptBuilder,
slash_command::{file_command::FileCommandMetadata, SlashCommandLine},
AssistantEdit, AssistantPatch, AssistantPatchStatus, MessageId, MessageStatus,
};
use crate::patch::{AssistantEdit, AssistantPatch, AssistantPatchStatus};
use anyhow::{anyhow, Context as _, Result};
use assistant_slash_command::{
SlashCommandContent, SlashCommandEvent, SlashCommandOutputSection, SlashCommandResult,
SlashCommandContent, SlashCommandEvent, SlashCommandLine, SlashCommandOutputSection,
SlashCommandResult, SlashCommandWorkingSet,
};
use assistant_slash_commands::FileCommandMetadata;
use assistant_tool::ToolWorkingSet;
use client::{self, proto, telemetry::Telemetry};
use clock::ReplicaId;
@@ -19,10 +16,9 @@ use feature_flags::{FeatureFlagAppExt, ToolUseFeatureFlag};
use fs::{Fs, RemoveOptions};
use futures::{future::Shared, FutureExt, StreamExt};
use gpui::{
AppContext, Context as _, EventEmitter, Model, ModelContext, RenderImage, SharedString,
Subscription, Task,
App, AppContext as _, Context, Entity, EventEmitter, RenderImage, SharedString, Subscription,
Task,
};
use language::{AnchorRangeExt, Bias, Buffer, LanguageRegistry, OffsetRangeExt, Point, ToOffset};
use language_model::{
LanguageModel, LanguageModelCacheConfiguration, LanguageModelCompletionEvent,
@@ -37,6 +33,7 @@ use language_models::{
use open_ai::Model as OpenAiModel;
use paths::contexts_dir;
use project::Project;
use prompt_library::PromptBuilder;
use serde::{Deserialize, Serialize};
use smallvec::SmallVec;
use std::{
@@ -51,9 +48,9 @@ use std::{
};
use telemetry_events::{AssistantEvent, AssistantKind, AssistantPhase};
use text::{BufferSnapshot, ToPoint};
use ui::IconName;
use util::{post_inc, ResultExt, TryFutureExt};
use uuid::Uuid;
use workspace::ui::IconName;
#[derive(Clone, Eq, PartialEq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
pub struct ContextId(String);
@@ -72,6 +69,64 @@ impl ContextId {
}
}
#[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub struct MessageId(pub clock::Lamport);
impl MessageId {
pub fn as_u64(self) -> u64 {
self.0.as_u64()
}
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub enum MessageStatus {
Pending,
Done,
Error(SharedString),
Canceled,
}
impl MessageStatus {
pub fn from_proto(status: proto::ContextMessageStatus) -> MessageStatus {
match status.variant {
Some(proto::context_message_status::Variant::Pending(_)) => MessageStatus::Pending,
Some(proto::context_message_status::Variant::Done(_)) => MessageStatus::Done,
Some(proto::context_message_status::Variant::Error(error)) => {
MessageStatus::Error(error.message.into())
}
Some(proto::context_message_status::Variant::Canceled(_)) => MessageStatus::Canceled,
None => MessageStatus::Pending,
}
}
pub fn to_proto(&self) -> proto::ContextMessageStatus {
match self {
MessageStatus::Pending => proto::ContextMessageStatus {
variant: Some(proto::context_message_status::Variant::Pending(
proto::context_message_status::Pending {},
)),
},
MessageStatus::Done => proto::ContextMessageStatus {
variant: Some(proto::context_message_status::Variant::Done(
proto::context_message_status::Done {},
)),
},
MessageStatus::Error(message) => proto::ContextMessageStatus {
variant: Some(proto::context_message_status::Variant::Error(
proto::context_message_status::Error {
message: message.to_string(),
},
)),
},
MessageStatus::Canceled => proto::ContextMessageStatus {
variant: Some(proto::context_message_status::Variant::Canceled(
proto::context_message_status::Canceled {},
)),
},
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum RequestType {
/// Request a normal chat response from the model.
@@ -422,7 +477,7 @@ pub struct MessageCacheMetadata {
pub struct MessageMetadata {
pub role: Role,
pub status: MessageStatus,
pub(crate) timestamp: clock::Lamport,
pub timestamp: clock::Lamport,
#[serde(skip)]
pub cache: Option<MessageCacheMetadata>,
}
@@ -533,18 +588,18 @@ pub enum XmlTagKind {
Operation,
}
pub struct Context {
pub struct AssistantContext {
id: ContextId,
timestamp: clock::Lamport,
version: clock::Global,
pending_ops: Vec<ContextOperation>,
operations: Vec<ContextOperation>,
buffer: Model<Buffer>,
buffer: Entity<Buffer>,
parsed_slash_commands: Vec<ParsedSlashCommand>,
invoked_slash_commands: HashMap<InvokedSlashCommandId, InvokedSlashCommand>,
edits_since_last_parse: language::Subscription,
pub(crate) slash_commands: Arc<SlashCommandWorkingSet>,
pub(crate) tools: Arc<ToolWorkingSet>,
slash_commands: Arc<SlashCommandWorkingSet>,
tools: Arc<ToolWorkingSet>,
slash_command_output_sections: Vec<SlashCommandOutputSection<language::Anchor>>,
pending_tool_uses_by_id: HashMap<LanguageModelToolUseId, PendingToolUse>,
message_anchors: Vec<MessageAnchor>,
@@ -564,7 +619,7 @@ pub struct Context {
language_registry: Arc<LanguageRegistry>,
patches: Vec<AssistantPatch>,
xml_tags: Vec<XmlTag>,
project: Option<Model<Project>>,
project: Option<Entity<Project>>,
prompt_builder: Arc<PromptBuilder>,
}
@@ -590,17 +645,17 @@ impl ContextAnnotation for XmlTag {
}
}
impl EventEmitter<ContextEvent> for Context {}
impl EventEmitter<ContextEvent> for AssistantContext {}
impl Context {
impl AssistantContext {
pub fn local(
language_registry: Arc<LanguageRegistry>,
project: Option<Model<Project>>,
project: Option<Entity<Project>>,
telemetry: Option<Arc<Telemetry>>,
prompt_builder: Arc<PromptBuilder>,
slash_commands: Arc<SlashCommandWorkingSet>,
tools: Arc<ToolWorkingSet>,
cx: &mut ModelContext<Self>,
cx: &mut Context<Self>,
) -> Self {
Self::new(
ContextId::new(),
@@ -625,11 +680,11 @@ impl Context {
prompt_builder: Arc<PromptBuilder>,
slash_commands: Arc<SlashCommandWorkingSet>,
tools: Arc<ToolWorkingSet>,
project: Option<Model<Project>>,
project: Option<Entity<Project>>,
telemetry: Option<Arc<Telemetry>>,
cx: &mut ModelContext<Self>,
cx: &mut Context<Self>,
) -> Self {
let buffer = cx.new_model(|_cx| {
let buffer = cx.new(|_cx| {
let buffer = Buffer::remote(
language::BufferId::new(1).unwrap(),
replica_id,
@@ -700,7 +755,7 @@ impl Context {
this
}
pub(crate) fn serialize(&self, cx: &AppContext) -> SavedContext {
pub(crate) fn serialize(&self, cx: &App) -> SavedContext {
let buffer = self.buffer.read(cx);
SavedContext {
id: Some(self.id.clone()),
@@ -748,9 +803,9 @@ impl Context {
prompt_builder: Arc<PromptBuilder>,
slash_commands: Arc<SlashCommandWorkingSet>,
tools: Arc<ToolWorkingSet>,
project: Option<Model<Project>>,
project: Option<Entity<Project>>,
telemetry: Option<Arc<Telemetry>>,
cx: &mut ModelContext<Self>,
cx: &mut Context<Self>,
) -> Self {
let id = saved_context.id.clone().unwrap_or_else(ContextId::new);
let mut this = Self::new(
@@ -782,18 +837,22 @@ impl Context {
self.timestamp.replica_id
}
pub fn version(&self, cx: &AppContext) -> ContextVersion {
pub fn version(&self, cx: &App) -> ContextVersion {
ContextVersion {
context: self.version.clone(),
buffer: self.buffer.read(cx).version(),
}
}
pub fn set_capability(
&mut self,
capability: language::Capability,
cx: &mut ModelContext<Self>,
) {
pub fn slash_commands(&self) -> &Arc<SlashCommandWorkingSet> {
&self.slash_commands
}
pub fn tools(&self) -> &Arc<ToolWorkingSet> {
&self.tools
}
pub fn set_capability(&mut self, capability: language::Capability, cx: &mut Context<Self>) {
self.buffer
.update(cx, |buffer, cx| buffer.set_capability(capability, cx));
}
@@ -807,7 +866,7 @@ impl Context {
pub fn serialize_ops(
&self,
since: &ContextVersion,
cx: &AppContext,
cx: &App,
) -> Task<Vec<proto::ContextOperation>> {
let buffer_ops = self
.buffer
@@ -842,7 +901,7 @@ impl Context {
pub fn apply_ops(
&mut self,
ops: impl IntoIterator<Item = ContextOperation>,
cx: &mut ModelContext<Self>,
cx: &mut Context<Self>,
) {
let mut buffer_ops = Vec::new();
for op in ops {
@@ -856,7 +915,7 @@ impl Context {
self.flush_ops(cx);
}
fn flush_ops(&mut self, cx: &mut ModelContext<Context>) {
fn flush_ops(&mut self, cx: &mut Context<AssistantContext>) {
let mut changed_messages = HashSet::default();
let mut summary_changed = false;
@@ -975,7 +1034,7 @@ impl Context {
}
}
fn can_apply_op(&self, op: &ContextOperation, cx: &AppContext) -> bool {
fn can_apply_op(&self, op: &ContextOperation, cx: &App) -> bool {
if !self.version.observed_all(op.version()) {
return false;
}
@@ -1006,7 +1065,7 @@ impl Context {
fn has_received_operations_for_anchor_range(
&self,
range: Range<text::Anchor>,
cx: &AppContext,
cx: &App,
) -> bool {
let version = &self.buffer.read(cx).version;
let observed_start = range.start == language::Anchor::MIN
@@ -1018,12 +1077,12 @@ impl Context {
observed_start && observed_end
}
fn push_op(&mut self, op: ContextOperation, cx: &mut ModelContext<Self>) {
fn push_op(&mut self, op: ContextOperation, cx: &mut Context<Self>) {
self.operations.push(op.clone());
cx.emit(ContextEvent::Operation(op));
}
pub fn buffer(&self) -> &Model<Buffer> {
pub fn buffer(&self) -> &Entity<Buffer> {
&self.buffer
}
@@ -1031,7 +1090,7 @@ impl Context {
self.language_registry.clone()
}
pub fn project(&self) -> Option<Model<Project>> {
pub fn project(&self) -> Option<Entity<Project>> {
self.project.clone()
}
@@ -1047,11 +1106,7 @@ impl Context {
self.summary.as_ref()
}
pub(crate) fn patch_containing(
&self,
position: Point,
cx: &AppContext,
) -> Option<&AssistantPatch> {
pub fn patch_containing(&self, position: Point, cx: &App) -> Option<&AssistantPatch> {
let buffer = self.buffer.read(cx);
let index = self.patches.binary_search_by(|patch| {
let patch_range = patch.range.to_point(&buffer);
@@ -1074,10 +1129,10 @@ impl Context {
self.patches.iter().map(|patch| patch.range.clone())
}
pub(crate) fn patch_for_range(
pub fn patch_for_range(
&self,
range: &Range<language::Anchor>,
cx: &AppContext,
cx: &App,
) -> Option<&AssistantPatch> {
let buffer = self.buffer.read(cx);
let index = self.patch_index_for_range(range, buffer).ok()?;
@@ -1108,7 +1163,7 @@ impl Context {
&self.slash_command_output_sections
}
pub fn contains_files(&self, cx: &AppContext) -> bool {
pub fn contains_files(&self, cx: &App) -> bool {
let buffer = self.buffer.read(cx);
self.slash_command_output_sections.iter().any(|section| {
section.is_valid(buffer)
@@ -1130,7 +1185,7 @@ impl Context {
self.pending_tool_uses_by_id.get(id)
}
fn set_language(&mut self, cx: &mut ModelContext<Self>) {
fn set_language(&mut self, cx: &mut Context<Self>) {
let markdown = self.language_registry.language_for_name("Markdown");
cx.spawn(|this, mut cx| async move {
let markdown = markdown.await?;
@@ -1144,9 +1199,9 @@ impl Context {
fn handle_buffer_event(
&mut self,
_: Model<Buffer>,
_: Entity<Buffer>,
event: &language::BufferEvent,
cx: &mut ModelContext<Self>,
cx: &mut Context<Self>,
) {
match event {
language::BufferEvent::Operation {
@@ -1164,11 +1219,11 @@ impl Context {
}
}
pub(crate) fn token_count(&self) -> Option<usize> {
pub fn token_count(&self) -> Option<usize> {
self.token_count
}
pub(crate) fn count_remaining_tokens(&mut self, cx: &mut ModelContext<Self>) {
pub(crate) fn count_remaining_tokens(&mut self, cx: &mut Context<Self>) {
// Assume it will be a Chat request, even though that takes fewer tokens (and risks going over the limit),
// because otherwise you see in the UI that your empty message has a bunch of tokens already used.
let request = self.to_completion_request(RequestType::Chat, cx);
@@ -1196,7 +1251,7 @@ impl Context {
&mut self,
cache_configuration: &Option<LanguageModelCacheConfiguration>,
speculative: bool,
cx: &mut ModelContext<Self>,
cx: &mut Context<Self>,
) -> bool {
let cache_configuration =
cache_configuration
@@ -1298,7 +1353,7 @@ impl Context {
new_anchor_needs_caching
}
fn start_cache_warming(&mut self, model: &Arc<dyn LanguageModel>, cx: &mut ModelContext<Self>) {
fn start_cache_warming(&mut self, model: &Arc<dyn LanguageModel>, cx: &mut Context<Self>) {
let cache_configuration = model.cache_configuration();
if !self.mark_cache_anchors(&cache_configuration, true, cx) {
@@ -1348,7 +1403,7 @@ impl Context {
});
}
pub fn update_cache_status_for_completion(&mut self, cx: &mut ModelContext<Self>) {
pub fn update_cache_status_for_completion(&mut self, cx: &mut Context<Self>) {
let cached_message_ids: Vec<MessageId> = self
.messages_metadata
.iter()
@@ -1373,7 +1428,7 @@ impl Context {
cx.notify();
}
pub fn reparse(&mut self, cx: &mut ModelContext<Self>) {
pub fn reparse(&mut self, cx: &mut Context<Self>) {
let buffer = self.buffer.read(cx).text_snapshot();
let mut row_ranges = self
.edits_since_last_parse
@@ -1446,7 +1501,7 @@ impl Context {
buffer: &BufferSnapshot,
updated: &mut Vec<ParsedSlashCommand>,
removed: &mut Vec<Range<text::Anchor>>,
cx: &AppContext,
cx: &App,
) {
let old_range = self.pending_command_indices_for_range(range.clone(), cx);
@@ -1500,7 +1555,7 @@ impl Context {
fn invalidate_pending_slash_commands(
&mut self,
buffer: &BufferSnapshot,
cx: &mut ModelContext<Self>,
cx: &mut Context<Self>,
) {
let mut invalidated_command_ids = Vec::new();
for (&command_id, command) in self.invoked_slash_commands.iter_mut() {
@@ -1534,7 +1589,7 @@ impl Context {
buffer: &BufferSnapshot,
updated: &mut Vec<Range<text::Anchor>>,
removed: &mut Vec<Range<text::Anchor>>,
cx: &mut ModelContext<Self>,
cx: &mut Context<Self>,
) {
// Rebuild the XML tags in the edited range.
let intersecting_tags_range =
@@ -1577,7 +1632,7 @@ impl Context {
&self,
buffer: &BufferSnapshot,
range: Range<text::Anchor>,
cx: &AppContext,
cx: &App,
) -> Vec<XmlTag> {
let mut messages = self.messages(cx).peekable();
@@ -1634,7 +1689,7 @@ impl Context {
tags_start_ix: usize,
buffer_end: text::Anchor,
buffer: &BufferSnapshot,
cx: &AppContext,
cx: &App,
) -> Vec<AssistantPatch> {
let mut new_patches = Vec::new();
let mut pending_patch = None;
@@ -1792,7 +1847,7 @@ impl Context {
pub fn pending_command_for_position(
&mut self,
position: language::Anchor,
cx: &mut ModelContext<Self>,
cx: &mut Context<Self>,
) -> Option<&mut ParsedSlashCommand> {
let buffer = self.buffer.read(cx);
match self
@@ -1816,7 +1871,7 @@ impl Context {
pub fn pending_commands_for_range(
&self,
range: Range<language::Anchor>,
cx: &AppContext,
cx: &App,
) -> &[ParsedSlashCommand] {
let range = self.pending_command_indices_for_range(range, cx);
&self.parsed_slash_commands[range]
@@ -1825,7 +1880,7 @@ impl Context {
fn pending_command_indices_for_range(
&self,
range: Range<language::Anchor>,
cx: &AppContext,
cx: &App,
) -> Range<usize> {
self.indices_intersecting_buffer_range(&self.parsed_slash_commands, range, cx)
}
@@ -1834,7 +1889,7 @@ impl Context {
&self,
all_annotations: &[T],
range: Range<language::Anchor>,
cx: &AppContext,
cx: &App,
) -> Range<usize> {
let buffer = self.buffer.read(cx);
let start_ix = match all_annotations
@@ -1857,7 +1912,7 @@ impl Context {
name: &str,
output: Task<SlashCommandResult>,
ensure_trailing_newline: bool,
cx: &mut ModelContext<Self>,
cx: &mut Context<Self>,
) {
let version = self.version.clone();
let command_id = InvokedSlashCommandId(self.next_timestamp());
@@ -2125,7 +2180,7 @@ impl Context {
fn insert_slash_command_output_section(
&mut self,
section: SlashCommandOutputSection<language::Anchor>,
cx: &mut ModelContext<Self>,
cx: &mut Context<Self>,
) {
let buffer = self.buffer.read(cx);
let insertion_ix = match self
@@ -2155,7 +2210,7 @@ impl Context {
&mut self,
tool_use_id: LanguageModelToolUseId,
output: Task<Result<String>>,
cx: &mut ModelContext<Self>,
cx: &mut Context<Self>,
) {
let insert_output_task = cx.spawn(|this, mut cx| {
let tool_use_id = tool_use_id.clone();
@@ -2213,11 +2268,11 @@ impl Context {
}
}
pub fn completion_provider_changed(&mut self, cx: &mut ModelContext<Self>) {
pub fn completion_provider_changed(&mut self, cx: &mut Context<Self>) {
self.count_remaining_tokens(cx);
}
fn get_last_valid_message_id(&self, cx: &ModelContext<Self>) -> Option<MessageId> {
fn get_last_valid_message_id(&self, cx: &Context<Self>) -> Option<MessageId> {
self.message_anchors.iter().rev().find_map(|message| {
message
.start
@@ -2229,7 +2284,7 @@ impl Context {
pub fn assist(
&mut self,
request_type: RequestType,
cx: &mut ModelContext<Self>,
cx: &mut Context<Self>,
) -> Option<MessageAnchor> {
let model_registry = LanguageModelRegistry::read_global(cx);
let provider = model_registry.active_provider()?;
@@ -2245,7 +2300,10 @@ impl Context {
let mut request = self.to_completion_request(request_type, cx);
if cx.has_flag::<ToolUseFeatureFlag>() {
// Don't attach tools for now; we'll be removing tool use from
// Assistant1 shortly.
#[allow(clippy::overly_complex_bool_expr)]
if false && cx.has_flag::<ToolUseFeatureFlag>() {
request.tools = self
.tools
.tools(cx)
@@ -2457,7 +2515,7 @@ impl Context {
pub fn to_completion_request(
&self,
request_type: RequestType,
cx: &AppContext,
cx: &App,
) -> LanguageModelRequest {
let buffer = self.buffer.read(cx);
@@ -2569,7 +2627,7 @@ impl Context {
completion_request
}
pub fn cancel_last_assist(&mut self, cx: &mut ModelContext<Self>) -> bool {
pub fn cancel_last_assist(&mut self, cx: &mut Context<Self>) -> bool {
if let Some(pending_completion) = self.pending_completions.pop() {
self.update_metadata(pending_completion.assistant_message_id, cx, |metadata| {
if metadata.status == MessageStatus::Pending {
@@ -2582,7 +2640,7 @@ impl Context {
}
}
pub fn cycle_message_roles(&mut self, ids: HashSet<MessageId>, cx: &mut ModelContext<Self>) {
pub fn cycle_message_roles(&mut self, ids: HashSet<MessageId>, cx: &mut Context<Self>) {
for id in &ids {
if let Some(metadata) = self.messages_metadata.get(id) {
let role = metadata.role.cycle();
@@ -2593,7 +2651,7 @@ impl Context {
self.message_roles_updated(ids, cx);
}
fn message_roles_updated(&mut self, ids: HashSet<MessageId>, cx: &mut ModelContext<Self>) {
fn message_roles_updated(&mut self, ids: HashSet<MessageId>, cx: &mut Context<Self>) {
let mut ranges = Vec::new();
for message in self.messages(cx) {
if ids.contains(&message.id) {
@@ -2616,7 +2674,7 @@ impl Context {
pub fn update_metadata(
&mut self,
id: MessageId,
cx: &mut ModelContext<Self>,
cx: &mut Context<Self>,
f: impl FnOnce(&mut MessageMetadata),
) {
let version = self.version.clone();
@@ -2640,7 +2698,7 @@ impl Context {
message_id: MessageId,
role: Role,
status: MessageStatus,
cx: &mut ModelContext<Self>,
cx: &mut Context<Self>,
) -> Option<MessageAnchor> {
if let Some(prev_message_ix) = self
.message_anchors
@@ -2674,7 +2732,7 @@ impl Context {
offset: usize,
role: Role,
status: MessageStatus,
cx: &mut ModelContext<Self>,
cx: &mut Context<Self>,
) -> MessageAnchor {
let start = self.buffer.update(cx, |buffer, cx| {
buffer.edit([(offset..offset, "\n")], None, cx);
@@ -2704,7 +2762,7 @@ impl Context {
anchor
}
pub fn insert_content(&mut self, content: Content, cx: &mut ModelContext<Self>) {
pub fn insert_content(&mut self, content: Content, cx: &mut Context<Self>) {
let buffer = self.buffer.read(cx);
let insertion_ix = match self
.contents
@@ -2720,7 +2778,7 @@ impl Context {
cx.emit(ContextEvent::MessagesEdited);
}
pub fn contents<'a>(&'a self, cx: &'a AppContext) -> impl 'a + Iterator<Item = Content> {
pub fn contents<'a>(&'a self, cx: &'a App) -> impl 'a + Iterator<Item = Content> {
let buffer = self.buffer.read(cx);
self.contents
.iter()
@@ -2734,7 +2792,7 @@ impl Context {
pub fn split_message(
&mut self,
range: Range<usize>,
cx: &mut ModelContext<Self>,
cx: &mut Context<Self>,
) -> (Option<MessageAnchor>, Option<MessageAnchor>) {
let start_message = self.message_for_offset(range.start, cx);
let end_message = self.message_for_offset(range.end, cx);
@@ -2860,7 +2918,7 @@ impl Context {
&mut self,
new_anchor: MessageAnchor,
new_metadata: MessageMetadata,
cx: &mut ModelContext<Self>,
cx: &mut Context<Self>,
) {
cx.emit(ContextEvent::MessagesEdited);
@@ -2878,7 +2936,7 @@ impl Context {
self.message_anchors.insert(insertion_ix, new_anchor);
}
pub(super) fn summarize(&mut self, replace_old: bool, cx: &mut ModelContext<Self>) {
pub fn summarize(&mut self, replace_old: bool, cx: &mut Context<Self>) {
let Some(provider) = LanguageModelRegistry::read_global(cx).active_provider() else {
return;
};
@@ -2956,14 +3014,14 @@ impl Context {
}
}
fn message_for_offset(&self, offset: usize, cx: &AppContext) -> Option<Message> {
fn message_for_offset(&self, offset: usize, cx: &App) -> Option<Message> {
self.messages_for_offsets([offset], cx).pop()
}
pub fn messages_for_offsets(
&self,
offsets: impl IntoIterator<Item = usize>,
cx: &AppContext,
cx: &App,
) -> Vec<Message> {
let mut result = Vec::new();
@@ -2996,14 +3054,14 @@ impl Context {
fn messages_from_anchors<'a>(
&'a self,
message_anchors: impl Iterator<Item = &'a MessageAnchor> + 'a,
cx: &'a AppContext,
cx: &'a App,
) -> impl 'a + Iterator<Item = Message> {
let buffer = self.buffer.read(cx);
Self::messages_from_iters(buffer, &self.messages_metadata, message_anchors.enumerate())
}
pub fn messages<'a>(&'a self, cx: &'a AppContext) -> impl 'a + Iterator<Item = Message> {
pub fn messages<'a>(&'a self, cx: &'a App) -> impl 'a + Iterator<Item = Message> {
self.messages_from_anchors(self.message_anchors.iter(), cx)
}
@@ -3051,7 +3109,7 @@ impl Context {
&mut self,
debounce: Option<Duration>,
fs: Arc<dyn Fs>,
cx: &mut ModelContext<Context>,
cx: &mut Context<AssistantContext>,
) {
if self.replica_id() != ReplicaId::default() {
// Prevent saving a remote context for now.
@@ -3117,7 +3175,7 @@ impl Context {
});
}
pub(crate) fn custom_summary(&mut self, custom_summary: String, cx: &mut ModelContext<Self>) {
pub fn custom_summary(&mut self, custom_summary: String, cx: &mut Context<Self>) {
let timestamp = self.next_timestamp();
let summary = self.summary.get_or_insert(ContextSummary::default());
summary.timestamp = timestamp;
@@ -3277,8 +3335,8 @@ impl SavedContext {
fn into_ops(
self,
buffer: &Model<Buffer>,
cx: &mut ModelContext<Context>,
buffer: &Entity<Buffer>,
cx: &mut Context<AssistantContext>,
) -> Vec<ContextOperation> {
let mut operations = Vec::new();
let mut version = clock::Global::new();

View File

@@ -1,15 +1,13 @@
use super::{AssistantEdit, MessageCacheMetadata};
use crate::slash_command_working_set::SlashCommandWorkingSet;
use crate::{
assistant_panel, prompt_library, slash_command::file_command, AssistantEditKind, CacheStatus,
Context, ContextEvent, ContextId, ContextOperation, InvokedSlashCommandId, MessageId,
MessageStatus, PromptBuilder,
AssistantContext, AssistantEdit, AssistantEditKind, CacheStatus, ContextEvent, ContextId,
ContextOperation, InvokedSlashCommandId, MessageCacheMetadata, MessageId, MessageStatus,
};
use anyhow::Result;
use assistant_slash_command::{
ArgumentCompletion, SlashCommand, SlashCommandContent, SlashCommandEvent, SlashCommandOutput,
SlashCommandOutputSection, SlashCommandRegistry, SlashCommandResult,
SlashCommandOutputSection, SlashCommandRegistry, SlashCommandResult, SlashCommandWorkingSet,
};
use assistant_slash_commands::FileSlashCommand;
use assistant_tool::ToolWorkingSet;
use collections::{HashMap, HashSet};
use fs::FakeFs;
@@ -17,12 +15,13 @@ use futures::{
channel::mpsc,
stream::{self, StreamExt},
};
use gpui::{prelude::*, AppContext, Model, SharedString, Task, TestAppContext, WeakView};
use gpui::{prelude::*, App, Entity, SharedString, Task, TestAppContext, WeakEntity};
use language::{Buffer, BufferSnapshot, LanguageRegistry, LspAdapterDelegate};
use language_model::{LanguageModelCacheConfiguration, LanguageModelRegistry, Role};
use parking_lot::Mutex;
use pretty_assertions::assert_eq;
use project::Project;
use prompt_library::PromptBuilder;
use rand::prelude::*;
use serde_json::json;
use settings::SettingsStore;
@@ -35,7 +34,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},
@@ -44,15 +43,14 @@ use util::{
use workspace::Workspace;
#[gpui::test]
fn test_inserting_and_removing_messages(cx: &mut AppContext) {
fn test_inserting_and_removing_messages(cx: &mut App) {
let settings_store = SettingsStore::test(cx);
LanguageModelRegistry::test(cx);
cx.set_global(settings_store);
assistant_panel::init(cx);
let registry = Arc::new(LanguageRegistry::test(cx.background_executor().clone()));
let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
let context = cx.new_model(|cx| {
Context::local(
let context = cx.new(|cx| {
AssistantContext::local(
registry,
None,
None,
@@ -185,16 +183,15 @@ fn test_inserting_and_removing_messages(cx: &mut AppContext) {
}
#[gpui::test]
fn test_message_splitting(cx: &mut AppContext) {
fn test_message_splitting(cx: &mut App) {
let settings_store = SettingsStore::test(cx);
cx.set_global(settings_store);
LanguageModelRegistry::test(cx);
assistant_panel::init(cx);
let registry = Arc::new(LanguageRegistry::test(cx.background_executor().clone()));
let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
let context = cx.new_model(|cx| {
Context::local(
let context = cx.new(|cx| {
AssistantContext::local(
registry.clone(),
None,
None,
@@ -290,15 +287,14 @@ fn test_message_splitting(cx: &mut AppContext) {
}
#[gpui::test]
fn test_messages_for_offsets(cx: &mut AppContext) {
fn test_messages_for_offsets(cx: &mut App) {
let settings_store = SettingsStore::test(cx);
LanguageModelRegistry::test(cx);
cx.set_global(settings_store);
assistant_panel::init(cx);
let registry = Arc::new(LanguageRegistry::test(cx.background_executor().clone()));
let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
let context = cx.new_model(|cx| {
Context::local(
let context = cx.new(|cx| {
AssistantContext::local(
registry,
None,
None,
@@ -371,9 +367,9 @@ fn test_messages_for_offsets(cx: &mut AppContext) {
);
fn message_ids_for_offsets(
context: &Model<Context>,
context: &Entity<AssistantContext>,
offsets: &[usize],
cx: &AppContext,
cx: &App,
) -> Vec<MessageId> {
context
.read(cx)
@@ -390,7 +386,6 @@ async fn test_slash_commands(cx: &mut TestAppContext) {
cx.set_global(settings_store);
cx.update(LanguageModelRegistry::test);
cx.update(Project::init_settings);
cx.update(assistant_panel::init);
let fs = FakeFs::new(cx.background_executor.clone());
fs.insert_tree(
@@ -408,12 +403,12 @@ async fn test_slash_commands(cx: &mut TestAppContext) {
.await;
let slash_command_registry = cx.update(SlashCommandRegistry::default_global);
slash_command_registry.register_command(file_command::FileSlashCommand, false);
slash_command_registry.register_command(FileSlashCommand, false);
let registry = Arc::new(LanguageRegistry::test(cx.executor()));
let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
let context = cx.new_model(|cx| {
Context::local(
let context = cx.new(|cx| {
AssistantContext::local(
registry.clone(),
None,
None,
@@ -613,7 +608,7 @@ async fn test_slash_commands(cx: &mut TestAppContext) {
#[track_caller]
fn assert_text_and_context_ranges(
buffer: &Model<Buffer>,
buffer: &Entity<Buffer>,
ranges: &RefCell<ContextRanges>,
expected_marked_text: &str,
cx: &mut TestAppContext,
@@ -698,13 +693,12 @@ async fn test_workflow_step_parsing(cx: &mut TestAppContext) {
let project = Project::test(fs, [Path::new("/root")], cx).await;
cx.update(LanguageModelRegistry::test);
cx.update(assistant_panel::init);
let registry = Arc::new(LanguageRegistry::test(cx.executor()));
// Create a new context
let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
let context = cx.new_model(|cx| {
Context::local(
let context = cx.new(|cx| {
AssistantContext::local(
registry.clone(),
Some(project),
None,
@@ -968,8 +962,8 @@ async fn test_workflow_step_parsing(cx: &mut TestAppContext) {
// Ensure steps are re-parsed when deserializing.
let serialized_context = context.read_with(cx, |context, cx| context.serialize(cx));
let deserialized_context = cx.new_model(|cx| {
Context::deserialize(
let deserialized_context = cx.new(|cx| {
AssistantContext::deserialize(
serialized_context,
Default::default(),
registry.clone(),
@@ -1012,7 +1006,11 @@ async fn test_workflow_step_parsing(cx: &mut TestAppContext) {
cx,
);
fn edit(context: &Model<Context>, new_text_marked_with_edits: &str, cx: &mut TestAppContext) {
fn edit(
context: &Entity<AssistantContext>,
new_text_marked_with_edits: &str,
cx: &mut TestAppContext,
) {
context.update(cx, |context, cx| {
context.buffer.update(cx, |buffer, cx| {
buffer.edit_via_marked_text(&new_text_marked_with_edits.unindent(), None, cx);
@@ -1023,7 +1021,7 @@ async fn test_workflow_step_parsing(cx: &mut TestAppContext) {
#[track_caller]
fn expect_patches(
context: &Model<Context>,
context: &Entity<AssistantContext>,
expected_marked_text: &str,
expected_suggestions: &[&[AssistantEdit]],
cx: &mut TestAppContext,
@@ -1081,11 +1079,10 @@ async fn test_serialization(cx: &mut TestAppContext) {
let settings_store = cx.update(SettingsStore::test);
cx.set_global(settings_store);
cx.update(LanguageModelRegistry::test);
cx.update(assistant_panel::init);
let registry = Arc::new(LanguageRegistry::test(cx.executor()));
let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
let context = cx.new_model(|cx| {
Context::local(
let context = cx.new(|cx| {
AssistantContext::local(
registry.clone(),
None,
None,
@@ -1128,8 +1125,8 @@ async fn test_serialization(cx: &mut TestAppContext) {
);
let serialized_context = context.read_with(cx, |context, cx| context.serialize(cx));
let deserialized_context = cx.new_model(|cx| {
Context::deserialize(
let deserialized_context = cx.new(|cx| {
AssistantContext::deserialize(
serialized_context,
Default::default(),
registry.clone(),
@@ -1173,7 +1170,6 @@ async fn test_random_context_collaboration(cx: &mut TestAppContext, mut rng: Std
cx.set_global(settings_store);
cx.update(LanguageModelRegistry::test);
cx.update(assistant_panel::init);
let slash_commands = cx.update(SlashCommandRegistry::default_global);
slash_commands.register_command(FakeSlashCommand("cmd-1".into()), false);
slash_commands.register_command(FakeSlashCommand("cmd-2".into()), false);
@@ -1187,8 +1183,8 @@ async fn test_random_context_collaboration(cx: &mut TestAppContext, mut rng: Std
let context_id = ContextId::new();
let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
for i in 0..num_peers {
let context = cx.new_model(|cx| {
Context::new(
let context = cx.new(|cx| {
AssistantContext::new(
context_id.clone(),
i as ReplicaId,
language::Capability::ReadWrite,
@@ -1442,15 +1438,14 @@ async fn test_random_context_collaboration(cx: &mut TestAppContext, mut rng: Std
}
#[gpui::test]
fn test_mark_cache_anchors(cx: &mut AppContext) {
fn test_mark_cache_anchors(cx: &mut App) {
let settings_store = SettingsStore::test(cx);
LanguageModelRegistry::test(cx);
cx.set_global(settings_store);
assistant_panel::init(cx);
let registry = Arc::new(LanguageRegistry::test(cx.background_executor().clone()));
let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
let context = cx.new_model(|cx| {
Context::local(
let context = cx.new(|cx| {
AssistantContext::local(
registry,
None,
None,
@@ -1603,7 +1598,7 @@ fn test_mark_cache_anchors(cx: &mut AppContext) {
);
}
fn messages(context: &Model<Context>, cx: &AppContext) -> Vec<(MessageId, Role, Range<usize>)> {
fn messages(context: &Entity<AssistantContext>, cx: &App) -> Vec<(MessageId, Role, Range<usize>)> {
context
.read(cx)
.messages(cx)
@@ -1612,8 +1607,8 @@ fn messages(context: &Model<Context>, cx: &AppContext) -> Vec<(MessageId, Role,
}
fn messages_cache(
context: &Model<Context>,
cx: &AppContext,
context: &Entity<AssistantContext>,
cx: &App,
) -> Vec<(MessageId, Option<MessageCacheMetadata>)> {
context
.read(cx)
@@ -1642,8 +1637,9 @@ impl SlashCommand for FakeSlashCommand {
self: Arc<Self>,
_arguments: &[String],
_cancel: Arc<AtomicBool>,
_workspace: Option<WeakView<Workspace>>,
_cx: &mut WindowContext,
_workspace: Option<WeakEntity<Workspace>>,
_window: &mut Window,
_cx: &mut App,
) -> Task<Result<Vec<ArgumentCompletion>>> {
Task::ready(Ok(vec![]))
}
@@ -1657,9 +1653,10 @@ impl SlashCommand for FakeSlashCommand {
_arguments: &[String],
_context_slash_command_output_sections: &[SlashCommandOutputSection<language::Anchor>],
_context_buffer: BufferSnapshot,
_workspace: WeakView<Workspace>,
_workspace: WeakEntity<Workspace>,
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
_cx: &mut WindowContext,
_window: &mut Window,
_cx: &mut App,
) -> Task<SlashCommandResult> {
Task::ready(Ok(SlashCommandOutput {
text: format!("Executed fake command: {}", self.0),

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,269 @@
use std::sync::Arc;
use gpui::{App, Entity, EventEmitter, FocusHandle, Focusable, Subscription, Task, WeakEntity};
use picker::{Picker, PickerDelegate};
use project::Project;
use ui::utils::{format_distance_from_now, DateTimeType};
use ui::{prelude::*, Avatar, ListItem, ListItemSpacing};
use workspace::{Item, Workspace};
use crate::{
AssistantPanelDelegate, ContextStore, RemoteContextMetadata, SavedContextMetadata,
DEFAULT_TAB_TITLE,
};
#[derive(Clone)]
pub enum ContextMetadata {
Remote(RemoteContextMetadata),
Saved(SavedContextMetadata),
}
enum SavedContextPickerEvent {
Confirmed(ContextMetadata),
}
pub struct ContextHistory {
picker: Entity<Picker<SavedContextPickerDelegate>>,
_subscriptions: Vec<Subscription>,
workspace: WeakEntity<Workspace>,
}
impl ContextHistory {
pub fn new(
project: Entity<Project>,
context_store: Entity<ContextStore>,
workspace: WeakEntity<Workspace>,
window: &mut Window,
cx: &mut Context<Self>,
) -> Self {
let picker = cx.new(|cx| {
Picker::uniform_list(
SavedContextPickerDelegate::new(project, context_store.clone()),
window,
cx,
)
.modal(false)
.max_height(None)
});
let subscriptions = vec![
cx.observe_in(&context_store, window, |this, _, window, cx| {
this.picker
.update(cx, |picker, cx| picker.refresh(window, cx));
}),
cx.subscribe_in(&picker, window, Self::handle_picker_event),
];
Self {
picker,
_subscriptions: subscriptions,
workspace,
}
}
fn handle_picker_event(
&mut self,
_: &Entity<Picker<SavedContextPickerDelegate>>,
event: &SavedContextPickerEvent,
window: &mut Window,
cx: &mut Context<Self>,
) {
let SavedContextPickerEvent::Confirmed(context) = event;
let Some(assistant_panel_delegate) = <dyn AssistantPanelDelegate>::try_global(cx) else {
return;
};
self.workspace
.update(cx, |workspace, cx| match context {
ContextMetadata::Remote(metadata) => {
assistant_panel_delegate
.open_remote_context(workspace, metadata.id.clone(), window, cx)
.detach_and_log_err(cx);
}
ContextMetadata::Saved(metadata) => {
assistant_panel_delegate
.open_saved_context(workspace, metadata.path.clone(), window, cx)
.detach_and_log_err(cx);
}
})
.ok();
}
}
impl Render for ContextHistory {
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
div().size_full().child(self.picker.clone())
}
}
impl Focusable for ContextHistory {
fn focus_handle(&self, cx: &App) -> FocusHandle {
self.picker.focus_handle(cx)
}
}
impl EventEmitter<()> for ContextHistory {}
impl Item for ContextHistory {
type Event = ();
fn tab_content_text(&self, _window: &Window, _cx: &App) -> Option<SharedString> {
Some("History".into())
}
}
struct SavedContextPickerDelegate {
store: Entity<ContextStore>,
project: Entity<Project>,
matches: Vec<ContextMetadata>,
selected_index: usize,
}
impl EventEmitter<SavedContextPickerEvent> for Picker<SavedContextPickerDelegate> {}
impl SavedContextPickerDelegate {
fn new(project: Entity<Project>, store: Entity<ContextStore>) -> Self {
Self {
project,
store,
matches: Vec::new(),
selected_index: 0,
}
}
}
impl PickerDelegate for SavedContextPickerDelegate {
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 Context<Picker<Self>>,
) {
self.selected_index = ix;
}
fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> {
"Search...".into()
}
fn update_matches(
&mut self,
query: String,
_window: &mut Window,
cx: &mut Context<Picker<Self>>,
) -> Task<()> {
let search = self.store.read(cx).search(query, cx);
cx.spawn(|this, mut cx| async move {
let matches = search.await;
this.update(&mut cx, |this, cx| {
let host_contexts = this.delegate.store.read(cx).host_contexts();
this.delegate.matches = host_contexts
.iter()
.cloned()
.map(ContextMetadata::Remote)
.chain(matches.into_iter().map(ContextMetadata::Saved))
.collect();
this.delegate.selected_index = 0;
cx.notify();
})
.ok();
})
}
fn confirm(&mut self, _secondary: bool, _window: &mut Window, cx: &mut Context<Picker<Self>>) {
if let Some(metadata) = self.matches.get(self.selected_index) {
cx.emit(SavedContextPickerEvent::Confirmed(metadata.clone()));
}
}
fn dismissed(&mut self, _window: &mut Window, _cx: &mut Context<Picker<Self>>) {}
fn render_match(
&self,
ix: usize,
selected: bool,
_window: &mut Window,
cx: &mut Context<Picker<Self>>,
) -> Option<Self::ListItem> {
let context = self.matches.get(ix)?;
let item = match context {
ContextMetadata::Remote(context) => {
let host_user = self.project.read(cx).host().and_then(|collaborator| {
self.project
.read(cx)
.user_store()
.read(cx)
.get_cached_user(collaborator.user_id)
});
div()
.flex()
.w_full()
.justify_between()
.gap_2()
.child(
h_flex().flex_1().overflow_x_hidden().child(
Label::new(context.summary.clone().unwrap_or(DEFAULT_TAB_TITLE.into()))
.size(LabelSize::Small),
),
)
.child(
h_flex()
.gap_2()
.children(if let Some(host_user) = host_user {
vec![
Avatar::new(host_user.avatar_uri.clone()).into_any_element(),
Label::new(format!("Shared by @{}", host_user.github_login))
.color(Color::Muted)
.size(LabelSize::Small)
.into_any_element(),
]
} else {
vec![Label::new("Shared by host")
.color(Color::Muted)
.size(LabelSize::Small)
.into_any_element()]
}),
)
}
ContextMetadata::Saved(context) => div()
.flex()
.w_full()
.justify_between()
.gap_2()
.child(
h_flex()
.flex_1()
.child(Label::new(context.title.clone()).size(LabelSize::Small))
.overflow_x_hidden(),
)
.child(
Label::new(format_distance_from_now(
DateTimeType::Local(context.mtime),
false,
true,
true,
))
.color(Color::Muted)
.size(LabelSize::Small),
),
};
Some(
ListItem::new(ix)
.inset(true)
.spacing(ListItemSpacing::Sparse)
.toggle_state(selected)
.child(item),
)
}
}

View File

@@ -1,10 +1,9 @@
use crate::slash_command::context_server_command;
use crate::SlashCommandId;
use crate::{
prompts::PromptBuilder, slash_command_working_set::SlashCommandWorkingSet, Context,
ContextEvent, ContextId, ContextOperation, ContextVersion, SavedContext, SavedContextMetadata,
AssistantContext, ContextEvent, ContextId, ContextOperation, ContextVersion, SavedContext,
SavedContextMetadata,
};
use anyhow::{anyhow, Context as _, Result};
use assistant_slash_command::{SlashCommandId, SlashCommandWorkingSet};
use assistant_tool::{ToolId, ToolWorkingSet};
use client::{proto, telemetry::Telemetry, Client, TypedEnvelope};
use clock::ReplicaId;
@@ -14,12 +13,11 @@ use context_server::{ContextServerFactoryRegistry, ContextServerTool};
use fs::Fs;
use futures::StreamExt;
use fuzzy::StringMatchCandidate;
use gpui::{
AppContext, AsyncAppContext, Context as _, EventEmitter, Model, ModelContext, Task, WeakModel,
};
use gpui::{App, AppContext as _, AsyncApp, Context, Entity, EventEmitter, Task, WeakEntity};
use language::LanguageRegistry;
use paths::contexts_dir;
use project::Project;
use prompt_library::PromptBuilder;
use regex::Regex;
use rpc::AnyProtoClient;
use std::sync::LazyLock;
@@ -33,7 +31,7 @@ use std::{
};
use util::{ResultExt, TryFutureExt};
pub fn init(client: &AnyProtoClient) {
pub(crate) fn init(client: &AnyProtoClient) {
client.add_model_message_handler(ContextStore::handle_advertise_contexts);
client.add_model_request_handler(ContextStore::handle_open_context);
client.add_model_request_handler(ContextStore::handle_create_context);
@@ -50,7 +48,7 @@ pub struct RemoteContextMetadata {
pub struct ContextStore {
contexts: Vec<ContextHandle>,
contexts_metadata: Vec<SavedContextMetadata>,
context_server_manager: Model<ContextServerManager>,
context_server_manager: Entity<ContextServerManager>,
context_server_slash_command_ids: HashMap<Arc<str>, Vec<SlashCommandId>>,
context_server_tool_ids: HashMap<Arc<str>, Vec<ToolId>>,
host_contexts: Vec<RemoteContextMetadata>,
@@ -61,7 +59,7 @@ pub struct ContextStore {
telemetry: Arc<Telemetry>,
_watch_updates: Task<Option<()>>,
client: Arc<Client>,
project: Model<Project>,
project: Entity<Project>,
project_is_shared: bool,
client_subscription: Option<client::Subscription>,
_project_subscriptions: Vec<gpui::Subscription>,
@@ -75,19 +73,19 @@ pub enum ContextStoreEvent {
impl EventEmitter<ContextStoreEvent> for ContextStore {}
enum ContextHandle {
Weak(WeakModel<Context>),
Strong(Model<Context>),
Weak(WeakEntity<AssistantContext>),
Strong(Entity<AssistantContext>),
}
impl ContextHandle {
fn upgrade(&self) -> Option<Model<Context>> {
fn upgrade(&self) -> Option<Entity<AssistantContext>> {
match self {
ContextHandle::Weak(weak) => weak.upgrade(),
ContextHandle::Strong(strong) => Some(strong.clone()),
}
}
fn downgrade(&self) -> WeakModel<Context> {
fn downgrade(&self) -> WeakEntity<AssistantContext> {
match self {
ContextHandle::Weak(weak) => weak.clone(),
ContextHandle::Strong(strong) => strong.downgrade(),
@@ -97,12 +95,12 @@ impl ContextHandle {
impl ContextStore {
pub fn new(
project: Model<Project>,
project: Entity<Project>,
prompt_builder: Arc<PromptBuilder>,
slash_commands: Arc<SlashCommandWorkingSet>,
tools: Arc<ToolWorkingSet>,
cx: &mut AppContext,
) -> Task<Result<Model<Self>>> {
cx: &mut App,
) -> Task<Result<Entity<Self>>> {
let fs = project.read(cx).fs().clone();
let languages = project.read(cx).languages().clone();
let telemetry = project.read(cx).client().telemetry().clone();
@@ -110,10 +108,10 @@ impl ContextStore {
const CONTEXT_WATCH_DURATION: Duration = Duration::from_millis(100);
let (mut events, _) = fs.watch(contexts_dir(), CONTEXT_WATCH_DURATION).await;
let this = cx.new_model(|cx: &mut ModelContext<Self>| {
let this = cx.new(|cx: &mut Context<Self>| {
let context_server_factory_registry =
ContextServerFactoryRegistry::default_global(cx);
let context_server_manager = cx.new_model(|cx| {
let context_server_manager = cx.new(|cx| {
ContextServerManager::new(context_server_factory_registry, project.clone(), cx)
});
let mut this = Self {
@@ -163,9 +161,9 @@ impl ContextStore {
}
async fn handle_advertise_contexts(
this: Model<Self>,
this: Entity<Self>,
envelope: TypedEnvelope<proto::AdvertiseContexts>,
mut cx: AsyncAppContext,
mut cx: AsyncApp,
) -> Result<()> {
this.update(&mut cx, |this, cx| {
this.host_contexts = envelope
@@ -182,9 +180,9 @@ impl ContextStore {
}
async fn handle_open_context(
this: Model<Self>,
this: Entity<Self>,
envelope: TypedEnvelope<proto::OpenContext>,
mut cx: AsyncAppContext,
mut cx: AsyncApp,
) -> Result<proto::OpenContextResponse> {
let context_id = ContextId::from_proto(envelope.payload.context_id);
let operations = this.update(&mut cx, |this, cx| {
@@ -212,9 +210,9 @@ impl ContextStore {
}
async fn handle_create_context(
this: Model<Self>,
this: Entity<Self>,
_: TypedEnvelope<proto::CreateContext>,
mut cx: AsyncAppContext,
mut cx: AsyncApp,
) -> Result<proto::CreateContextResponse> {
let (context_id, operations) = this.update(&mut cx, |this, cx| {
if this.project.read(cx).is_via_collab() {
@@ -240,9 +238,9 @@ impl ContextStore {
}
async fn handle_update_context(
this: Model<Self>,
this: Entity<Self>,
envelope: TypedEnvelope<proto::UpdateContext>,
mut cx: AsyncAppContext,
mut cx: AsyncApp,
) -> Result<()> {
this.update(&mut cx, |this, cx| {
let context_id = ContextId::from_proto(envelope.payload.context_id);
@@ -256,9 +254,9 @@ impl ContextStore {
}
async fn handle_synchronize_contexts(
this: Model<Self>,
this: Entity<Self>,
envelope: TypedEnvelope<proto::SynchronizeContexts>,
mut cx: AsyncAppContext,
mut cx: AsyncApp,
) -> Result<proto::SynchronizeContextsResponse> {
this.update(&mut cx, |this, cx| {
if this.project.read(cx).is_via_collab() {
@@ -299,7 +297,7 @@ impl ContextStore {
})?
}
fn handle_project_changed(&mut self, _: Model<Project>, cx: &mut ModelContext<Self>) {
fn handle_project_changed(&mut self, _: Entity<Project>, cx: &mut Context<Self>) {
let is_shared = self.project.read(cx).is_shared();
let was_shared = mem::replace(&mut self.project_is_shared, is_shared);
if is_shared == was_shared {
@@ -320,7 +318,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.entity(), &mut cx.to_async()));
self.advertise_contexts(cx);
} else {
self.client_subscription = None;
@@ -329,9 +327,9 @@ impl ContextStore {
fn handle_project_event(
&mut self,
_: Model<Project>,
_: Entity<Project>,
event: &project::Event,
cx: &mut ModelContext<Self>,
cx: &mut Context<Self>,
) {
match event {
project::Event::Reshared => {
@@ -361,9 +359,9 @@ impl ContextStore {
}
}
pub fn create(&mut self, cx: &mut ModelContext<Self>) -> Model<Context> {
let context = cx.new_model(|cx| {
Context::local(
pub fn create(&mut self, cx: &mut Context<Self>) -> Entity<AssistantContext> {
let context = cx.new(|cx| {
AssistantContext::local(
self.languages.clone(),
Some(self.project.clone()),
Some(self.telemetry.clone()),
@@ -379,8 +377,8 @@ impl ContextStore {
pub fn create_remote_context(
&mut self,
cx: &mut ModelContext<Self>,
) -> Task<Result<Model<Context>>> {
cx: &mut Context<Self>,
) -> Task<Result<Entity<AssistantContext>>> {
let project = self.project.read(cx);
let Some(project_id) = project.remote_id() else {
return Task::ready(Err(anyhow!("project was not remote")));
@@ -399,8 +397,8 @@ impl ContextStore {
let response = request.await?;
let context_id = ContextId::from_proto(response.context_id);
let context_proto = response.context.context("invalid context")?;
let context = cx.new_model(|cx| {
Context::new(
let context = cx.new(|cx| {
AssistantContext::new(
context_id.clone(),
replica_id,
capability,
@@ -439,8 +437,8 @@ impl ContextStore {
pub fn open_local_context(
&mut self,
path: PathBuf,
cx: &ModelContext<Self>,
) -> Task<Result<Model<Context>>> {
cx: &Context<Self>,
) -> Task<Result<Entity<AssistantContext>>> {
if let Some(existing_context) = self.loaded_context_for_path(&path, cx) {
return Task::ready(Ok(existing_context));
}
@@ -462,8 +460,8 @@ impl ContextStore {
cx.spawn(|this, mut cx| async move {
let saved_context = load.await?;
let context = cx.new_model(|cx| {
Context::deserialize(
let context = cx.new(|cx| {
AssistantContext::deserialize(
saved_context,
path.clone(),
languages,
@@ -486,7 +484,7 @@ impl ContextStore {
})
}
fn loaded_context_for_path(&self, path: &Path, cx: &AppContext) -> Option<Model<Context>> {
fn loaded_context_for_path(&self, path: &Path, cx: &App) -> Option<Entity<AssistantContext>> {
self.contexts.iter().find_map(|context| {
let context = context.upgrade()?;
if context.read(cx).path() == Some(path) {
@@ -497,11 +495,11 @@ impl ContextStore {
})
}
pub(super) fn loaded_context_for_id(
pub fn loaded_context_for_id(
&self,
id: &ContextId,
cx: &AppContext,
) -> Option<Model<Context>> {
cx: &App,
) -> Option<Entity<AssistantContext>> {
self.contexts.iter().find_map(|context| {
let context = context.upgrade()?;
if context.read(cx).id() == id {
@@ -515,8 +513,8 @@ impl ContextStore {
pub fn open_remote_context(
&mut self,
context_id: ContextId,
cx: &mut ModelContext<Self>,
) -> Task<Result<Model<Context>>> {
cx: &mut Context<Self>,
) -> Task<Result<Entity<AssistantContext>>> {
let project = self.project.read(cx);
let Some(project_id) = project.remote_id() else {
return Task::ready(Err(anyhow!("project was not remote")));
@@ -541,8 +539,8 @@ impl ContextStore {
cx.spawn(|this, mut cx| async move {
let response = request.await?;
let context_proto = response.context.context("invalid context")?;
let context = cx.new_model(|cx| {
Context::new(
let context = cx.new(|cx| {
AssistantContext::new(
context_id.clone(),
replica_id,
capability,
@@ -578,7 +576,7 @@ impl ContextStore {
})
}
fn register_context(&mut self, context: &Model<Context>, cx: &mut ModelContext<Self>) {
fn register_context(&mut self, context: &Entity<AssistantContext>, cx: &mut Context<Self>) {
let handle = if self.project_is_shared {
ContextHandle::Strong(context.clone())
} else {
@@ -591,9 +589,9 @@ impl ContextStore {
fn handle_context_event(
&mut self,
context: Model<Context>,
context: Entity<AssistantContext>,
event: &ContextEvent,
cx: &mut ModelContext<Self>,
cx: &mut Context<Self>,
) {
let Some(project_id) = self.project.read(cx).remote_id() else {
return;
@@ -618,7 +616,7 @@ impl ContextStore {
}
}
fn advertise_contexts(&self, cx: &AppContext) {
fn advertise_contexts(&self, cx: &App) {
let Some(project_id) = self.project.read(cx).remote_id() else {
return;
};
@@ -652,7 +650,7 @@ impl ContextStore {
.ok();
}
fn synchronize_contexts(&mut self, cx: &mut ModelContext<Self>) {
fn synchronize_contexts(&mut self, cx: &mut Context<Self>) {
let Some(project_id) = self.project.read(cx).remote_id() else {
return;
};
@@ -707,7 +705,7 @@ impl ContextStore {
.detach_and_log_err(cx);
}
pub fn search(&self, query: String, cx: &AppContext) -> Task<Vec<SavedContextMetadata>> {
pub fn search(&self, query: String, cx: &App) -> Task<Vec<SavedContextMetadata>> {
let metadata = self.contexts_metadata.clone();
let executor = cx.background_executor().clone();
cx.background_executor().spawn(async move {
@@ -741,7 +739,7 @@ impl ContextStore {
&self.host_contexts
}
fn reload(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
fn reload(&mut self, cx: &mut Context<Self>) -> Task<Result<()>> {
let fs = self.fs.clone();
cx.spawn(|this, mut cx| async move {
fs.create_dir(contexts_dir()).await?;
@@ -790,8 +788,8 @@ impl ContextStore {
})
}
pub fn restart_context_servers(&mut self, cx: &mut ModelContext<Self>) {
cx.update_model(
pub fn restart_context_servers(&mut self, cx: &mut Context<Self>) {
cx.update_entity(
&self.context_server_manager,
|context_server_manager, cx| {
for server in context_server_manager.servers() {
@@ -803,7 +801,7 @@ impl ContextStore {
);
}
fn register_context_server_handlers(&self, cx: &mut ModelContext<Self>) {
fn register_context_server_handlers(&self, cx: &mut Context<Self>) {
cx.subscribe(
&self.context_server_manager.clone(),
Self::handle_context_server_event,
@@ -813,9 +811,9 @@ impl ContextStore {
fn handle_context_server_event(
&mut self,
context_server_manager: Model<ContextServerManager>,
context_server_manager: Entity<ContextServerManager>,
event: &context_server::manager::Event,
cx: &mut ModelContext<Self>,
cx: &mut Context<Self>,
) {
let slash_command_working_set = self.slash_commands.clone();
let tool_working_set = self.tools.clone();
@@ -835,14 +833,14 @@ impl ContextStore {
if let Some(prompts) = protocol.list_prompts().await.log_err() {
let slash_command_ids = prompts
.into_iter()
.filter(context_server_command::acceptable_prompt)
.filter(assistant_slash_commands::acceptable_prompt)
.map(|prompt| {
log::info!(
"registering context server command: {:?}",
prompt.name
);
slash_command_working_set.insert(Arc::new(
context_server_command::ContextServerSlashCommand::new(
assistant_slash_commands::ContextServerSlashCommand::new(
context_server_manager.clone(),
&server,
prompt,

View File

@@ -2,14 +2,14 @@ use anyhow::{anyhow, Context as _, Result};
use collections::HashMap;
use editor::ProposedChangesEditor;
use futures::{future, TryFutureExt as _};
use gpui::{AppContext, AsyncAppContext, Model, SharedString};
use gpui::{App, AsyncApp, Entity, SharedString};
use language::{AutoindentMode, Buffer, BufferSnapshot};
use project::{Project, ProjectPath};
use std::{cmp, ops::Range, path::Path, sync::Arc};
use text::{AnchorRangeExt as _, Bias, OffsetRangeExt as _, Point};
#[derive(Clone, Debug)]
pub(crate) struct AssistantPatch {
pub struct AssistantPatch {
pub range: Range<language::Anchor>,
pub title: SharedString,
pub edits: Arc<[Result<AssistantEdit>]>,
@@ -17,13 +17,13 @@ pub(crate) struct AssistantPatch {
}
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub(crate) enum AssistantPatchStatus {
pub enum AssistantPatchStatus {
Pending,
Ready,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) struct AssistantEdit {
pub struct AssistantEdit {
pub path: String,
pub kind: AssistantEditKind,
}
@@ -55,8 +55,8 @@ pub enum AssistantEditKind {
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub(crate) struct ResolvedPatch {
pub edit_groups: HashMap<Model<Buffer>, Vec<ResolvedEditGroup>>,
pub struct ResolvedPatch {
pub edit_groups: HashMap<Entity<Buffer>, Vec<ResolvedEditGroup>>,
pub errors: Vec<AssistantPatchResolutionError>,
}
@@ -74,7 +74,7 @@ pub struct ResolvedEdit {
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub(crate) struct AssistantPatchResolutionError {
pub struct AssistantPatchResolutionError {
pub edit_ix: usize,
pub message: String,
}
@@ -121,7 +121,7 @@ impl SearchMatrix {
}
impl ResolvedPatch {
pub fn apply(&self, editor: &ProposedChangesEditor, cx: &mut AppContext) {
pub fn apply(&self, editor: &ProposedChangesEditor, cx: &mut App) {
for (buffer, groups) in &self.edit_groups {
let branch = editor.branch_buffer_for_base(buffer).unwrap();
Self::apply_edit_groups(groups, &branch, cx);
@@ -129,11 +129,7 @@ impl ResolvedPatch {
editor.recalculate_all_buffer_diffs();
}
fn apply_edit_groups(
groups: &Vec<ResolvedEditGroup>,
buffer: &Model<Buffer>,
cx: &mut AppContext,
) {
fn apply_edit_groups(groups: &Vec<ResolvedEditGroup>, buffer: &Entity<Buffer>, cx: &mut App) {
let mut edits = Vec::new();
for group in groups {
for suggestion in &group.edits {
@@ -232,9 +228,9 @@ impl AssistantEdit {
pub async fn resolve(
&self,
project: Model<Project>,
mut cx: AsyncAppContext,
) -> Result<(Model<Buffer>, ResolvedEdit)> {
project: Entity<Project>,
mut cx: AsyncApp,
) -> Result<(Entity<Buffer>, ResolvedEdit)> {
let path = self.path.clone();
let kind = self.kind.clone();
let buffer = project
@@ -425,11 +421,7 @@ impl AssistantEditKind {
}
impl AssistantPatch {
pub(crate) async fn resolve(
&self,
project: Model<Project>,
cx: &mut AsyncAppContext,
) -> ResolvedPatch {
pub async fn resolve(&self, project: Entity<Project>, cx: &mut AsyncApp) -> ResolvedPatch {
let mut resolve_tasks = Vec::new();
for (ix, edit) in self.edits.iter().enumerate() {
if let Ok(edit) = edit.as_ref() {
@@ -555,7 +547,7 @@ impl Eq for AssistantPatch {}
#[cfg(test)]
mod tests {
use super::*;
use gpui::{AppContext, Context};
use gpui::{App, AppContext as _};
use language::{
language_settings::AllLanguageSettings, Language, LanguageConfig, LanguageMatcher,
};
@@ -565,7 +557,7 @@ mod tests {
use util::test::{generate_marked_text, marked_text_ranges};
#[gpui::test]
fn test_resolve_location(cx: &mut AppContext) {
fn test_resolve_location(cx: &mut App) {
assert_location_resolution(
concat!(
" Lorem\n",
@@ -636,7 +628,7 @@ mod tests {
}
#[gpui::test]
fn test_resolve_edits(cx: &mut AppContext) {
fn test_resolve_edits(cx: &mut App) {
init_test(cx);
assert_edits(
@@ -902,7 +894,7 @@ mod tests {
);
}
fn init_test(cx: &mut AppContext) {
fn init_test(cx: &mut App) {
let settings_store = SettingsStore::test(cx);
cx.set_global(settings_store);
language::init(cx);
@@ -912,13 +904,9 @@ mod tests {
}
#[track_caller]
fn assert_location_resolution(
text_with_expected_range: &str,
query: &str,
cx: &mut AppContext,
) {
fn assert_location_resolution(text_with_expected_range: &str, query: &str, cx: &mut App) {
let (text, _) = marked_text_ranges(text_with_expected_range, false);
let buffer = cx.new_model(|cx| Buffer::local(text.clone(), cx));
let buffer = cx.new(|cx| Buffer::local(text.clone(), cx));
let snapshot = buffer.read(cx).snapshot();
let range = AssistantEditKind::resolve_location(&snapshot, query).to_offset(&snapshot);
let text_with_actual_range = generate_marked_text(&text, &[range], false);
@@ -930,10 +918,10 @@ mod tests {
old_text: String,
edits: Vec<AssistantEditKind>,
new_text: String,
cx: &mut AppContext,
cx: &mut App,
) {
let buffer =
cx.new_model(|cx| Buffer::local(old_text, cx).with_language(Arc::new(rust_lang()), cx));
cx.new(|cx| Buffer::local(old_text, cx).with_language(Arc::new(rust_lang()), cx));
let snapshot = buffer.read(cx).snapshot();
let resolved_edits = edits
.into_iter()

View File

@@ -1,12 +1,11 @@
use crate::assistant_panel::ContextEditor;
use crate::SlashCommandWorkingSet;
use crate::context_editor::ContextEditor;
use anyhow::Result;
use assistant_slash_command::AfterCompletion;
pub use assistant_slash_command::{SlashCommand, SlashCommandOutput};
pub use assistant_slash_command::SlashCommand;
use assistant_slash_command::{AfterCompletion, SlashCommandLine, SlashCommandWorkingSet};
use editor::{CompletionProvider, Editor};
use fuzzy::{match_strings, StringMatchCandidate};
use gpui::{AppContext, Model, Task, ViewContext, WeakView, WindowContext};
use language::{Anchor, Buffer, CodeLabel, Documentation, HighlightId, LanguageServerId, ToPoint};
use gpui::{App, Context, Entity, Task, WeakEntity, Window};
use language::{Anchor, Buffer, Documentation, LanguageServerId, ToPoint};
use parking_lot::Mutex;
use project::CompletionIntent;
use rope::Point;
@@ -19,46 +18,20 @@ use std::{
Arc,
},
};
use ui::ActiveTheme;
use workspace::Workspace;
pub mod auto_command;
pub mod cargo_workspace_command;
pub mod context_server_command;
pub mod default_command;
pub mod delta_command;
pub mod diagnostics_command;
pub mod docs_command;
pub mod fetch_command;
pub mod file_command;
pub mod now_command;
pub mod project_command;
pub mod prompt_command;
pub mod search_command;
pub mod selection_command;
pub mod streaming_example_command;
pub mod symbols_command;
pub mod tab_command;
pub mod terminal_command;
pub(crate) struct SlashCommandCompletionProvider {
pub struct SlashCommandCompletionProvider {
cancel_flag: Mutex<Arc<AtomicBool>>,
slash_commands: Arc<SlashCommandWorkingSet>,
editor: Option<WeakView<ContextEditor>>,
workspace: Option<WeakView<Workspace>>,
}
pub(crate) struct SlashCommandLine {
/// The range within the line containing the command name.
pub name: Range<usize>,
/// Ranges within the line containing the command arguments.
pub arguments: Vec<Range<usize>>,
editor: Option<WeakEntity<ContextEditor>>,
workspace: Option<WeakEntity<Workspace>>,
}
impl SlashCommandCompletionProvider {
pub fn new(
slash_commands: Arc<SlashCommandWorkingSet>,
editor: Option<WeakView<ContextEditor>>,
workspace: Option<WeakView<Workspace>>,
editor: Option<WeakEntity<ContextEditor>>,
workspace: Option<WeakEntity<Workspace>>,
) -> Self {
Self {
cancel_flag: Mutex::new(Arc::new(AtomicBool::new(false))),
@@ -73,7 +46,8 @@ impl SlashCommandCompletionProvider {
command_name: &str,
command_range: Range<Anchor>,
name_range: Range<Anchor>,
cx: &mut WindowContext,
window: &mut Window,
cx: &mut App,
) -> Task<Result<Vec<project::Completion>>> {
let slash_commands = self.slash_commands.clone();
let candidates = slash_commands
@@ -85,7 +59,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 +70,7 @@ impl SlashCommandCompletionProvider {
)
.await;
cx.update(|cx| {
cx.update(|_, cx| {
matches
.into_iter()
.filter_map(|mat| {
@@ -118,28 +92,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 App| {
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(),
@@ -157,6 +134,7 @@ impl SlashCommandCompletionProvider {
})
}
#[allow(clippy::too_many_arguments)]
fn complete_command_argument(
&self,
command_name: &str,
@@ -164,7 +142,8 @@ impl SlashCommandCompletionProvider {
command_range: Range<Anchor>,
argument_range: Range<Anchor>,
last_argument_range: Range<Anchor>,
cx: &mut WindowContext,
window: &mut Window,
cx: &mut App,
) -> Task<Result<Vec<project::Completion>>> {
let new_cancel_flag = Arc::new(AtomicBool::new(false));
let mut flag = self.cancel_flag.lock();
@@ -175,6 +154,7 @@ impl SlashCommandCompletionProvider {
arguments,
new_cancel_flag.clone(),
self.workspace.clone(),
window,
cx,
);
let command_name: Arc<str> = command_name.into();
@@ -202,7 +182,9 @@ impl SlashCommandCompletionProvider {
let command_range = command_range.clone();
let command_name = command_name.clone();
move |intent: CompletionIntent, cx: &mut WindowContext| {
move |intent: CompletionIntent,
window: &mut Window,
cx: &mut App| {
if new_argument.after_completion.run()
|| intent.is_complete()
{
@@ -214,6 +196,7 @@ impl SlashCommandCompletionProvider {
&completed_arguments,
true,
workspace.clone(),
window,
cx,
);
})
@@ -257,10 +240,11 @@ impl SlashCommandCompletionProvider {
impl CompletionProvider for SlashCommandCompletionProvider {
fn completions(
&self,
buffer: &Model<Buffer>,
buffer: &Entity<Buffer>,
buffer_position: Anchor,
_: editor::CompletionContext,
cx: &mut ViewContext<Editor>,
window: &mut Window,
cx: &mut Context<Editor>,
) -> Task<Result<Vec<project::Completion>>> {
let Some((name, arguments, command_range, last_argument_range)) =
buffer.update(cx, |buffer, _cx| {
@@ -315,30 +299,31 @@ 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)
}
}
fn resolve_completions(
&self,
_: Model<Buffer>,
_: Entity<Buffer>,
_: Vec<usize>,
_: Rc<RefCell<Box<[project::Completion]>>>,
_: &mut ViewContext<Editor>,
_: &mut Context<Editor>,
) -> Task<Result<bool>> {
Task::ready(Ok(true))
}
fn is_completion_trigger(
&self,
buffer: &Model<Buffer>,
buffer: &Entity<Buffer>,
position: language::Anchor,
_text: &str,
_trigger_in_words: bool,
cx: &mut ViewContext<Editor>,
cx: &mut Context<Editor>,
) -> bool {
let buffer = buffer.read(cx);
let position = position.to_point(buffer);
@@ -355,73 +340,3 @@ impl CompletionProvider for SlashCommandCompletionProvider {
false
}
}
impl SlashCommandLine {
pub(crate) fn parse(line: &str) -> Option<Self> {
let mut call: Option<Self> = None;
let mut ix = 0;
for c in line.chars() {
let next_ix = ix + c.len_utf8();
if let Some(call) = &mut call {
// The command arguments start at the first non-whitespace character
// after the command name, and continue until the end of the line.
if let Some(argument) = call.arguments.last_mut() {
if c.is_whitespace() {
if (*argument).is_empty() {
argument.start = next_ix;
argument.end = next_ix;
} else {
argument.end = ix;
call.arguments.push(next_ix..next_ix);
}
} else {
argument.end = next_ix;
}
}
// The command name ends at the first whitespace character.
else if !call.name.is_empty() {
if c.is_whitespace() {
call.arguments = vec![next_ix..next_ix];
} else {
call.name.end = next_ix;
}
}
// The command name must begin with a letter.
else if c.is_alphabetic() {
call.name.end = next_ix;
} else {
return None;
}
}
// Commands start with a slash.
else if c == '/' {
call = Some(SlashCommandLine {
name: next_ix..next_ix,
arguments: Vec::new(),
});
}
// The line can't contain anything before the slash except for whitespace.
else if !c.is_whitespace() {
return None;
}
ix = next_ix;
}
call
}
}
pub fn create_label_for_command(
command_name: &str,
arguments: &[&str],
cx: &AppContext,
) -> CodeLabel {
let mut label = CodeLabel::default();
label.push_str(command_name, None);
label.push_str(" ", None);
label.push_str(
&arguments.join(" "),
cx.theme().syntax().highlight_id("comment").map(HighlightId),
);
label.filter_range = 0..command_name.len();
label
}

View File

@@ -1,16 +1,16 @@
use std::sync::Arc;
use gpui::{AnyElement, DismissEvent, SharedString, Task, WeakView};
use assistant_slash_command::SlashCommandWorkingSet;
use gpui::{AnyElement, DismissEvent, SharedString, Task, WeakEntity};
use picker::{Picker, PickerDelegate, PickerEditorPosition};
use ui::{prelude::*, ListItem, ListItemSpacing, PopoverMenu, PopoverTrigger, Tooltip};
use crate::assistant_panel::ContextEditor;
use crate::SlashCommandWorkingSet;
use crate::context_editor::ContextEditor;
#[derive(IntoElement)]
pub(super) struct SlashCommandSelector<T: PopoverTrigger> {
working_set: Arc<SlashCommandWorkingSet>,
active_context_editor: WeakView<ContextEditor>,
active_context_editor: WeakEntity<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 App) -> AnyElement,
on_confirm: fn(&mut Window, &mut App),
},
}
@@ -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: WeakEntity<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: WeakEntity<ContextEditor>,
trigger: T,
) -> Self {
SlashCommandSelector {
@@ -73,18 +73,23 @@ impl PickerDelegate for SlashCommandDelegate {
self.selected_index
}
fn set_selected_index(&mut self, ix: usize, cx: &mut ViewContext<Picker<Self>>) {
fn set_selected_index(&mut self, ix: usize, _: &mut Window, cx: &mut Context<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 App) -> 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 Context<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 +109,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 +144,25 @@ 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 Context<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 Context<Picker<Self>>) {}
fn editor_position(&self) -> PickerEditorPosition {
PickerEditorPosition::End
@@ -167,7 +172,8 @@ impl PickerDelegate for SlashCommandDelegate {
&self,
ix: usize,
selected: bool,
cx: &mut ViewContext<Picker<Self>>,
window: &mut Window,
cx: &mut Context<Picker<Self>>,
) -> Option<Self::ListItem> {
let command_info = self.filtered_commands.get(ix)?;
@@ -179,7 +185,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(|_| Tooltip::new(description.clone())).into()
})
.child(
v_flex()
@@ -229,14 +235,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 App) -> impl IntoElement {
let all_models = self
.working_set
.featured_command_names(cx)
@@ -259,7 +265,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 +299,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 +310,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(|cx| {
let picker =
Picker::uniform_list(delegate, window, cx).max_height(Some(rems(20.).into()));
picker
});
@@ -314,7 +321,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

@@ -0,0 +1,33 @@
[package]
name = "assistant_settings"
version = "0.1.0"
edition.workspace = true
publish.workspace = true
license = "GPL-3.0-or-later"
[lints]
workspace = true
[lib]
path = "src/assistant_settings.rs"
[dependencies]
anthropic = { workspace = true, features = ["schemars"] }
anyhow.workspace = true
feature_flags.workspace = true
gpui.workspace = true
language_model.workspace = true
lmstudio = { workspace = true, features = ["schemars"] }
log.workspace = true
ollama = { workspace = true, features = ["schemars"] }
open_ai = { workspace = true, features = ["schemars"] }
schemars.workspace = true
serde.workspace = true
settings.workspace = true
[dev-dependencies]
fs.workspace = true
gpui = { workspace = true, features = ["test-support"] }
paths.workspace = true
serde_json_lenient.workspace = true
settings = { workspace = true, features = ["test-support"] }

View File

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

View File

@@ -3,8 +3,9 @@ use std::sync::Arc;
use ::open_ai::Model as OpenAiModel;
use anthropic::Model as AnthropicModel;
use feature_flags::FeatureFlagAppExt;
use gpui::{AppContext, Pixels};
use gpui::{App, Pixels};
use language_model::{CloudModel, LanguageModel};
use lmstudio::Model as LmStudioModel;
use ollama::Model as OllamaModel;
use schemars::{schema::Schema, JsonSchema};
use serde::{Deserialize, Serialize};
@@ -40,6 +41,11 @@ pub enum AssistantProviderContentV1 {
default_model: Option<OllamaModel>,
api_url: Option<String>,
},
#[serde(rename = "lmstudio")]
LmStudio {
default_model: Option<LmStudioModel>,
api_url: Option<String>,
},
}
#[derive(Debug, Default)]
@@ -56,7 +62,7 @@ pub struct AssistantSettings {
}
impl AssistantSettings {
pub fn are_live_diffs_enabled(&self, cx: &AppContext) -> bool {
pub fn are_live_diffs_enabled(&self, cx: &App) -> bool {
cx.is_staff() || self.enable_experimental_live_diffs
}
}
@@ -137,6 +143,12 @@ impl AssistantSettingsContent {
model: model.id().to_string(),
})
}
AssistantProviderContentV1::LmStudio { default_model, .. } => {
default_model.map(|model| LanguageModelSelection {
provider: "lmstudio".to_string(),
model: model.id().to_string(),
})
}
}),
inline_alternatives: None,
enable_experimental_live_diffs: None,
@@ -214,6 +226,18 @@ impl AssistantSettingsContent {
api_url,
});
}
"lmstudio" => {
let api_url = match &settings.provider {
Some(AssistantProviderContentV1::LmStudio { api_url, .. }) => {
api_url.clone()
}
_ => None,
};
settings.provider = Some(AssistantProviderContentV1::LmStudio {
default_model: Some(lmstudio::Model::new(&model, None, None)),
api_url,
});
}
"openai" => {
let (api_url, available_models) = match &settings.provider {
Some(AssistantProviderContentV1::OpenAi {
@@ -312,6 +336,7 @@ fn providers_schema(_: &mut schemars::gen::SchemaGenerator) -> schemars::schema:
enum_values: Some(vec![
"anthropic".into(),
"google".into(),
"lmstudio".into(),
"ollama".into(),
"openai".into(),
"zed.dev".into(),
@@ -355,7 +380,7 @@ pub struct AssistantSettingsContentV1 {
default_height: Option<f32>,
/// The provider of the assistant service.
///
/// This can be "openai", "anthropic", "ollama", "zed.dev"
/// This can be "openai", "anthropic", "ollama", "lmstudio", "zed.dev"
/// each with their respective default models and configurations.
provider: Option<AssistantProviderContentV1>,
}
@@ -397,7 +422,7 @@ impl Settings for AssistantSettings {
fn load(
sources: SettingsSources<Self::FileContent>,
_: &mut gpui::AppContext,
_: &mut gpui::App,
) -> anyhow::Result<Self> {
let mut settings = AssistantSettings::default();

View File

@@ -1,8 +1,8 @@
[package]
name = "assistant_slash_command"
version = "0.1.0"
edition = "2021"
publish = false
edition.workspace = true
publish.workspace = true
license = "GPL-3.0-or-later"
[lints]

View File

@@ -1,12 +1,14 @@
mod extension_slash_command;
mod slash_command_registry;
mod slash_command_working_set;
pub use crate::extension_slash_command::*;
pub use crate::slash_command_registry::*;
pub use crate::slash_command_working_set::*;
use anyhow::Result;
use futures::stream::{self, BoxStream};
use futures::StreamExt;
use gpui::{AnyElement, AppContext, ElementId, SharedString, Task, WeakView, WindowContext};
use gpui::{App, SharedString, Task, WeakEntity, Window};
use language::{BufferSnapshot, CodeLabel, LspAdapterDelegate, OffsetRangeExt};
pub use language_model::Role;
use serde::{Deserialize, Serialize};
@@ -16,7 +18,7 @@ use std::{
};
use workspace::{ui::IconName, Workspace};
pub fn init(cx: &mut AppContext) {
pub fn init(cx: &mut App) {
SlashCommandRegistry::default_global(cx);
extension_slash_command::init(cx);
}
@@ -69,7 +71,7 @@ pub trait SlashCommand: 'static + Send + Sync {
fn icon(&self) -> IconName {
IconName::Slash
}
fn label(&self, _cx: &AppContext) -> CodeLabel {
fn label(&self, _cx: &App) -> CodeLabel {
CodeLabel::plain(self.name(), None)
}
fn description(&self) -> String;
@@ -78,35 +80,32 @@ pub trait SlashCommand: 'static + Send + Sync {
self: Arc<Self>,
arguments: &[String],
cancel: Arc<AtomicBool>,
workspace: Option<WeakView<Workspace>>,
cx: &mut WindowContext,
workspace: Option<WeakEntity<Workspace>>,
window: &mut Window,
cx: &mut App,
) -> Task<Result<Vec<ArgumentCompletion>>>;
fn requires_argument(&self) -> bool;
fn accepts_arguments(&self) -> bool {
self.requires_argument()
}
#[allow(clippy::too_many_arguments)]
fn run(
self: Arc<Self>,
arguments: &[String],
context_slash_command_output_sections: &[SlashCommandOutputSection<language::Anchor>],
context_buffer: BufferSnapshot,
workspace: WeakView<Workspace>,
workspace: WeakEntity<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 App,
) -> Task<SlashCommandResult>;
}
pub type RenderFoldPlaceholder = Arc<
dyn Send
+ Sync
+ Fn(ElementId, Arc<dyn Fn(&mut WindowContext)>, &mut WindowContext) -> AnyElement,
>;
#[derive(Debug, PartialEq)]
pub enum SlashCommandContent {
Text {
@@ -266,6 +265,67 @@ impl SlashCommandOutputSection<language::Anchor> {
}
}
pub struct SlashCommandLine {
/// The range within the line containing the command name.
pub name: Range<usize>,
/// Ranges within the line containing the command arguments.
pub arguments: Vec<Range<usize>>,
}
impl SlashCommandLine {
pub fn parse(line: &str) -> Option<Self> {
let mut call: Option<Self> = None;
let mut ix = 0;
for c in line.chars() {
let next_ix = ix + c.len_utf8();
if let Some(call) = &mut call {
// The command arguments start at the first non-whitespace character
// after the command name, and continue until the end of the line.
if let Some(argument) = call.arguments.last_mut() {
if c.is_whitespace() {
if (*argument).is_empty() {
argument.start = next_ix;
argument.end = next_ix;
} else {
argument.end = ix;
call.arguments.push(next_ix..next_ix);
}
} else {
argument.end = next_ix;
}
}
// The command name ends at the first whitespace character.
else if !call.name.is_empty() {
if c.is_whitespace() {
call.arguments = vec![next_ix..next_ix];
} else {
call.name.end = next_ix;
}
}
// The command name must begin with a letter.
else if c.is_alphabetic() {
call.name.end = next_ix;
} else {
return None;
}
}
// Commands start with a slash.
else if c == '/' {
call = Some(SlashCommandLine {
name: next_ix..next_ix,
arguments: Vec::new(),
});
}
// The line can't contain anything before the slash except for whitespace.
else if !c.is_whitespace() {
return None;
}
ix = next_ix;
}
call
}
}
#[cfg(test)]
mod tests {
use pretty_assertions::assert_eq;

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::{App, Task, WeakEntity, Window};
use language::{BufferSnapshot, LspAdapterDelegate};
use ui::prelude::*;
use workspace::Workspace;
@@ -14,7 +14,7 @@ use crate::{
SlashCommandRegistry, SlashCommandResult,
};
pub fn init(cx: &mut AppContext) {
pub fn init(cx: &mut App) {
let proxy = ExtensionHostProxy::default_global(cx);
proxy.register_slash_command_proxy(SlashCommandRegistryProxy {
slash_command_registry: SlashCommandRegistry::global(cx),
@@ -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<WeakEntity<Workspace>>,
_window: &mut Window,
cx: &mut App,
) -> 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: WeakEntity<Workspace>,
delegate: Option<Arc<dyn LspAdapterDelegate>>,
cx: &mut WindowContext,
_window: &mut Window,
cx: &mut App,
) -> Task<SlashCommandResult> {
let command = self.command.clone();
let arguments = arguments.to_owned();

View File

@@ -3,7 +3,7 @@ use std::sync::Arc;
use collections::{BTreeSet, HashMap};
use derive_more::{Deref, DerefMut};
use gpui::Global;
use gpui::{AppContext, ReadGlobal};
use gpui::{App, ReadGlobal};
use parking_lot::RwLock;
use crate::SlashCommand;
@@ -26,14 +26,14 @@ pub struct SlashCommandRegistry {
impl SlashCommandRegistry {
/// Returns the global [`SlashCommandRegistry`].
pub fn global(cx: &AppContext) -> Arc<Self> {
pub fn global(cx: &App) -> Arc<Self> {
GlobalSlashCommandRegistry::global(cx).0.clone()
}
/// Returns the global [`SlashCommandRegistry`].
///
/// Inserts a default [`SlashCommandRegistry`] if one does not yet exist.
pub fn default_global(cx: &mut AppContext) -> Arc<Self> {
pub fn default_global(cx: &mut App) -> Arc<Self> {
cx.default_global::<GlobalSlashCommandRegistry>().0.clone()
}

View File

@@ -1,9 +1,11 @@
use assistant_slash_command::{SlashCommand, SlashCommandRegistry};
use collections::HashMap;
use gpui::AppContext;
use parking_lot::Mutex;
use std::sync::Arc;
use collections::HashMap;
use gpui::App;
use parking_lot::Mutex;
use crate::{SlashCommand, SlashCommandRegistry};
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Default)]
pub struct SlashCommandId(usize);
@@ -21,7 +23,7 @@ struct WorkingSetState {
}
impl SlashCommandWorkingSet {
pub fn command(&self, name: &str, cx: &AppContext) -> Option<Arc<dyn SlashCommand>> {
pub fn command(&self, name: &str, cx: &App) -> Option<Arc<dyn SlashCommand>> {
self.state
.lock()
.context_server_commands_by_name
@@ -30,7 +32,7 @@ impl SlashCommandWorkingSet {
.or_else(|| SlashCommandRegistry::global(cx).command(name))
}
pub fn command_names(&self, cx: &AppContext) -> Vec<Arc<str>> {
pub fn command_names(&self, cx: &App) -> Vec<Arc<str>> {
let mut command_names = SlashCommandRegistry::global(cx).command_names();
command_names.extend(
self.state
@@ -43,7 +45,7 @@ impl SlashCommandWorkingSet {
command_names
}
pub fn featured_command_names(&self, cx: &AppContext) -> Vec<Arc<str>> {
pub fn featured_command_names(&self, cx: &App) -> Vec<Arc<str>> {
SlashCommandRegistry::global(cx).featured_command_names()
}

View File

@@ -0,0 +1,52 @@
[package]
name = "assistant_slash_commands"
version = "0.1.0"
edition.workspace = true
publish.workspace = true
license = "GPL-3.0-or-later"
[lints]
workspace = true
[lib]
path = "src/assistant_slash_commands.rs"
[dependencies]
anyhow.workspace = true
assistant_slash_command.workspace = true
cargo_toml.workspace = true
chrono.workspace = true
collections.workspace = true
context_server.workspace = true
editor.workspace = true
feature_flags.workspace = true
fs.workspace = true
futures.workspace = true
fuzzy.workspace = true
globset.workspace = true
gpui.workspace = true
html_to_markdown.workspace = true
http_client.workspace = true
indexed_docs.workspace = true
language.workspace = true
language_model.workspace = true
log.workspace = true
project.workspace = true
prompt_library.workspace = true
rope.workspace = true
schemars.workspace = true
semantic_index.workspace = true
serde.workspace = true
serde_json.workspace = true
smol.workspace = true
terminal_view.workspace = true
text.workspace = true
toml.workspace = true
ui.workspace = true
util.workspace = true
workspace.workspace = true
[dev-dependencies]
env_logger.workspace = true
pretty_assertions.workspace = true
settings.workspace = true

View File

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

View File

@@ -0,0 +1,53 @@
mod auto_command;
mod cargo_workspace_command;
mod context_server_command;
mod default_command;
mod delta_command;
mod diagnostics_command;
mod docs_command;
mod fetch_command;
mod file_command;
mod now_command;
mod project_command;
mod prompt_command;
mod search_command;
mod selection_command;
mod streaming_example_command;
mod symbols_command;
mod tab_command;
mod terminal_command;
use gpui::App;
use language::{CodeLabel, HighlightId};
use ui::ActiveTheme as _;
pub use crate::auto_command::*;
pub use crate::cargo_workspace_command::*;
pub use crate::context_server_command::*;
pub use crate::default_command::*;
pub use crate::delta_command::*;
pub use crate::diagnostics_command::*;
pub use crate::docs_command::*;
pub use crate::fetch_command::*;
pub use crate::file_command::*;
pub use crate::now_command::*;
pub use crate::project_command::*;
pub use crate::prompt_command::*;
pub use crate::search_command::*;
pub use crate::selection_command::*;
pub use crate::streaming_example_command::*;
pub use crate::symbols_command::*;
pub use crate::tab_command::*;
pub use crate::terminal_command::*;
pub fn create_label_for_command(command_name: &str, arguments: &[&str], cx: &App) -> CodeLabel {
let mut label = CodeLabel::default();
label.push_str(command_name, None);
label.push_str(" ", None);
label.push_str(
&arguments.join(" "),
cx.theme().syntax().highlight_id("comment").map(HighlightId),
);
label.filter_range = 0..command_name.len();
label
}

View File

@@ -5,7 +5,7 @@ use assistant_slash_command::{
};
use feature_flags::FeatureFlag;
use futures::StreamExt;
use gpui::{AppContext, AsyncAppContext, AsyncWindowContext, Task, WeakView, WindowContext};
use gpui::{App, AsyncApp, Task, WeakEntity, Window};
use language::{CodeLabel, LspAdapterDelegate};
use language_model::{
LanguageModelCompletionEvent, LanguageModelRegistry, LanguageModelRequest,
@@ -18,7 +18,7 @@ use ui::{prelude::*, BorrowAppContext};
use util::ResultExt;
use workspace::Workspace;
use crate::slash_command::create_label_for_command;
use crate::create_label_for_command;
pub struct AutoSlashCommandFeatureFlag;
@@ -26,7 +26,7 @@ impl FeatureFlag for AutoSlashCommandFeatureFlag {
const NAME: &'static str = "auto-slash-command";
}
pub(crate) struct AutoCommand;
pub struct AutoCommand;
impl SlashCommand for AutoCommand {
fn name(&self) -> String {
@@ -45,7 +45,7 @@ impl SlashCommand for AutoCommand {
self.description()
}
fn label(&self, cx: &AppContext) -> CodeLabel {
fn label(&self, cx: &App) -> CodeLabel {
create_label_for_command("auto", &["--prompt"], cx)
}
@@ -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<WeakEntity<Workspace>>,
_window: &mut Window,
cx: &mut App,
) -> 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.
@@ -74,9 +75,9 @@ impl SlashCommand for AutoCommand {
return Task::ready(Err(anyhow!("No project indexer, cannot use /auto")));
};
let cx: &mut AppContext = cx;
let cx: &mut App = cx;
cx.spawn(|cx: gpui::AsyncAppContext| async move {
cx.spawn(|cx: gpui::AsyncApp| async move {
let task = project_index.read_with(&cx, |project_index, cx| {
project_index.flush_summary_backlogs(cx)
})?;
@@ -96,9 +97,10 @@ impl SlashCommand for AutoCommand {
arguments: &[String],
_context_slash_command_output_sections: &[SlashCommandOutputSection<language::Anchor>],
_context_buffer: language::BufferSnapshot,
workspace: WeakView<Workspace>,
workspace: WeakEntity<Workspace>,
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
cx: &mut WindowContext,
window: &mut Window,
cx: &mut App,
) -> 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: 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?;
@@ -187,7 +189,7 @@ struct CommandToRun {
async fn commands_for_summaries(
summaries: &[FileSummary],
original_prompt: &str,
cx: &AsyncAppContext,
cx: &AsyncApp,
) -> Result<Vec<CommandToRun>> {
if summaries.is_empty() {
log::warn!("Inferring no context because there were no summaries available.");

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