Compare commits

..

170 Commits

Author SHA1 Message Date
Nate Butler
a3e04867ab Update git_ui.rs 2024-12-16 15:30:27 -05:00
Nate Butler
3abf165c1d Update git_panel.rs 2024-12-16 15:30:25 -05:00
Nate Butler
48eae645f2 Revert "Merge branch 'main' into git-panel-commit-editor"
This reverts commit 78b6cae754, reversing
changes made to 1cac4b6c00.
2024-12-16 15:29:44 -05:00
Nate Butler
78b6cae754 Merge branch 'main' into git-panel-commit-editor 2024-12-16 15:26:45 -05:00
Nate Butler
1cac4b6c00 Revert "wip"
This reverts commit aee641d79d.
2024-12-16 15:12:49 -05:00
Danilo Leal
ec741d61ed assistant2: Adjust thread history list item visuals (#21998)
Most notably, adding the `outlined` property in the `ListItem`
component.

<img width="800" alt="Screenshot 2024-12-13 at 20 35 39"
src="https://github.com/user-attachments/assets/adac4463-66f9-4b5e-b1c0-93c34f068dc4"
/>

Release Notes:

- N/A
2024-12-16 16:42:09 -03:00
Nate Butler
426f94b310 git_ui: Update todos (#22100)
`todo!()` -> `TODO`

Release Notes:

- N/A
2024-12-16 13:39:40 -05:00
Marshall Bowers
eff61ee764 assistant2: Remove WeakView<Workspace> optionality for inline assist (#22099)
This PR removes the optionality for the `WeakView<Workspace>` that we
pass to the inline assist.

This was always `Some` in practice, so it seems we don't need to have it
be an `Option`.

Release Notes:

- N/A
2024-12-16 13:26:11 -05:00
Nate Butler
aee641d79d wip 2024-12-16 13:24:55 -05:00
Marshall Bowers
caefdcd7f1 assistant2: Factor out ContextStrip (#22096)
This PR factors a `ContextStrip` view out of the `MessageEditor` so that
we can use it in other places.

Release Notes:

- N/A
2024-12-16 12:45:01 -05:00
Kirill Bulatov
ff2ad63037 Allow splitting terminal items in the central pane group (#22088)
Follow-up of https://github.com/zed-industries/zed/pull/22004
Closes https://github.com/zed-industries/zed/issues/22078

Release Notes:

- Fixed splitting terminal items in the center
2024-12-16 19:23:01 +02:00
Marshall Bowers
88f7942f11 assistant2: Add support for referencing other threads as context (#22092)
This PR adds the ability to reference other threads as context:

<img width="1159" alt="Screenshot 2024-12-16 at 11 29 54 AM"
src="https://github.com/user-attachments/assets/bb8a24ff-56d3-4406-ab8c-6657e65d8c70"
/>

<img width="1159" alt="Screenshot 2024-12-16 at 11 29 35 AM"
src="https://github.com/user-attachments/assets/7a02ebda-a2f5-40e9-9dd4-1bb029cb1c43"
/>


Release Notes:

- N/A
2024-12-16 11:50:57 -05:00
uncenter
188c55c8a6 docs: Fix context_servers key for example extension manifest (#22079)
Pretty sure this isn't meant to be kebab-case.

Release Notes:

- N/A
2024-12-16 11:00:26 -05:00
Danilo Leal
2562b488b1 Refine interaction in foldable multibuffer header (#22084)
- Ensuring that the fold button is big enough to avoid clicking on the
header as a whole (and then moving to the actual file)
- Adding tooltips to the fold button
- Refining the container structure so that the tooltip for the folder
button and the header click don't overlap
- Adding keybindings to tooltips


https://github.com/user-attachments/assets/82284b59-3025-4d6d-b916-ad4d1ecdb119

Release Notes:

- N/A
2024-12-16 12:54:06 -03:00
Kirill Bulatov
bc113e4b51 Move task centering code closer to user input (#22082)
Follow-up of https://github.com/zed-industries/zed/pull/22004 

* Reuse center terminals for tasks, when requested
* Extend task templates with `RevealTarget`, moving it from
`TaskSpawnTarget` into the core library
* Use `reveal_target` instead of `target` to avoid misinterpretations in
the task template context
* Do not expose `SpawnInTerminal` to user interface, avoid it
implementing `Serialize` and `Deserialize`
* Remove `NewCenterTask` action, extending `task::Spawn` interface
instead
* Do not require any extra unrelated parameters during task resolution,
instead, use task overrides on the resolved tasks on the modal side
* Add keybindings for opening the task modal in the
`RevealTarget::Center` mode

Release Notes:

- N/A
2024-12-16 16:15:58 +02:00
Thorsten Ball
ea012075fc Trigger completions even if inline completion is visible (#22077)
This is related to #22069 and #21858: before both of these PRs, we would
only ever show inline completions OR completions, never both at the same
time.

Now we show both at the same, but we still had this piece of logic here,
that prevented non-inline completions from showing up if there was
already an inline completion.

With this change, it's possible to get LSP completions without having to
dismiss inline completions before.

Release Notes:

- Inline completions (Copilot, Supermaven, ...) don't stop other
completions from showing up anymore. Both can now be visible at the same
time.

---------

Co-authored-by: Bennet <bennet@zed.dev>
2024-12-16 15:10:33 +01:00
Helge Mahrt
ce727fbc07 workspace: Fix doc comments (#22063)
Happened to see that the doc comments here were not correct while
implementing something else.

Release Notes:

- N/A
2024-12-16 08:27:14 -05:00
tims
62b3acee5f Project panel: Deselect entries on remaining blank space click + Remove hover color for selected entries (#22073)
Closes #22072

Clicking on the remaining space now allows a single click to deselect
all selected items. Check the issue for a preview of the current state
and how it works in VSCode.

Bonus: I found the hover color on selected items to be distracting. When
I have many entries selected and hover over them, it becomes hard to
tell if a particular entry is selected while the mouse pointer is on it.
This PR removes hover coloring for selected entries, mimicking how
VSCode handles it.

This PR:
<img
src="https://github.com/user-attachments/assets/9c4b20fc-df93-4868-b7fe-4045433e85b2"
alt="zed" width="450px" />

Release Notes:

- Clicking on empty space in the Project Panel now deselects all
selected items.
2024-12-16 14:05:54 +01:00
Thorsten Ball
38c0aa303e vim: Don't dismiss inline completion when switching to normal mode (#22075)
I'm not sure about this yet.

On one hand: it's nice that the completion doesn't just disappear when I
hit escape because I was typing and in the flow.

On the other hand: no other inline completion provider keeps the
suggestion when leaving insert mode.

I'm going to merge this so we can get it into nightly and try it out for
the next couple of days. cc @ConradIrwin

Release Notes:

- vim: Do not dismiss inline completions when leaving insert/replace
mode with `<esc>`.
2024-12-16 11:23:20 +01:00
Bennet Bo Fenner
040d9ae222 zeta: Prevent diff popover from going offscreen (#22070)
https://github.com/user-attachments/assets/4ce806f1-d790-41d0-9825-e68055281446

Release Notes:

- N/A
2024-12-16 11:22:20 +01:00
Thorsten Ball
d135ec2b73 completions: Restore tab behavior when both visible (#22069)
This reverts part of #21858 by changing how `tab` works again:

- If both, completions and inline completions, are visible, then `tab`
accepts the completion and `shif-tab` the inline completion.
- If only one of them is shown, then `tab` accepts it.

I'm not a fan of this solution, but I think it's a short-term fix that
avoids breaking people's `tab` muscle memory.

Release Notes:

- (These release notes invalidate the release notes contained in:
https://github.com/zed-industries/zed/pull/21858)
- Changed how inline completions (Copilot, Supermaven, ...) and normal
completions (from language servers) interact. Zed will now also show
inline completions when the completion menu is visible. The user can
accept the inline completion with `<shift-tab>` and the active entry in
the completion menu with `<tab>`.
2024-12-16 11:02:54 +01:00
Michael Sloan
a94afbc062 Switch from Arc/RwLock to Rc/RefCell for CodeContextMenu (#22035)
`CodeContextMenu` is always accessed on one thread, so only `Rc`s and
`Rc<RefCell<_>>` are needed. There should be tiny performance benefits
from this. The main benefit of this is that when seeing code accessing a
`RwLock` it would be reasonable to wonder whether it will block. The
only potential downside is the potential for panics due to overlapping
borrows of the RefCells. I think this is an acceptable risk because most
errors of this nature will be local or will be caught by clippy via the
check for holding a RefCell reference over an `await`.

Release Notes:

- N/A
2024-12-16 01:50:21 -07:00
Michael Sloan
7b721efe2c Stop mutating completion match state + reject fuzzy match text change (#22061)
This fixes #21837, where CompletionsMenu fuzzy match positions were
desynchronized from completion label text. The solution is to not mutate
`match_candidates` and instead offset the highlight positions in the
rendering code.

This solution requires that the fuzzy match text not change on
completion resolution. This is a property we want anyway, since fuzzy
match text changing means items unexpectedly changing position in the
menu.

What happened:

* #21521 updated completion resolution to modify labels on resolution.

- This interacted poorly with the code
[here](341e65e122/crates/editor/src/code_context_menus.rs (L604)),
where the fuzzy match results are updated to include the full label, and
the fuzzy match result positions are offset to be in the correct place.
The fuzzy mach positions were now invalid because they were based on the
old text.

* #21705 caused completion resolution to occur more frequently. Before
this only the selected item was being resolved. This caused the panic
due to invalid positions to happen much more frequently.

Closes #21837

Release Notes:

- N/A
2024-12-16 01:21:26 -07:00
Peter Tripp
18b6d142c3 docs: Suggest installing PHP to use PHP (#22058) 2024-12-16 01:21:20 -05:00
Michael Sloan
53c9af3e61 Add and use CodeLabel::filter_text() (#22054)
Release Notes:

- N/A
2024-12-15 22:24:41 -07:00
Kirill Bulatov
af50261ae2 Allow folding buffers inside multi buffers (#22046)
Closes https://github.com/zed-industries/zed/issues/4925


https://github.com/user-attachments/assets/e7b87375-893f-41ae-a2d9-d501499e40d1


Allows to fold any buffer inside multi buffers, either by clicking the
chevron icon on the header, or by using
`editor::Fold`/`editor::UnfoldLines`/`editor::ToggleFold`/`editor::FoldAll`
and `editor::UnfoldAll` actions inside the multi buffer (those were noop
there before).

Every fold has a fake line inside it, so it's possible to navigate into
that via the keyboard and unfold it with the corresponding editor
action.

The state is synchronized with the outline panel state: any fold inside
multi buffer folds the corresponding file entry; any file entry fold
inside the outline panel folds the corresponding buffer inside the multi
buffer, any directory fold inside the outline panel folds the
corresponding buffers inside the multi buffer for each nested file entry
in the panel.


Release Notes:

- Added a possibility to fold buffers inside multi buffers

---------

Co-authored-by: Antonio Scandurra <antonio@zed.dev>
Co-authored-by: Max Brunsfeld <max@zed.dev>
Co-authored-by: Cole Miller <cole@zed.dev>
2024-12-16 00:32:07 +02:00
Michael Sloan
f64fcedabb Fix fuzzy string match invariant check (#22032)
Version in #21983 only handled out of range issues rather than utf-8
boundary issues (thanks to @s3bba for pointing this out)

Release Notes:

- N/A
2024-12-15 01:15:22 -07:00
Michael Sloan
7e6233d70f Remove an unnecessary clone in get_permalink_to_line (#22027)
Release Notes:

- N/A
2024-12-14 15:52:39 -07:00
Michael Sloan
d459f010b6 Rename GitRepository.path() to GitRepository.dot_git_dir() (#22026)
Release Notes:

- N/A
2024-12-14 15:30:56 -07:00
Michael Sloan
25970650a7 Improve StringMatchCandidate::new interface (#22011)
Release Notes:

- N/A
2024-12-14 13:35:36 -07:00
Kirill Bulatov
9daa426e93 Fix terminal pane tabs arrangement and closing (#22013)
* Fixes the inability to drag and drop terminal tabs to reorder them;
fixed incorrect terminal tab move on drag and drop into existing pane
(follow-up of https://github.com/zed-industries/zed/pull/21238)
* Fixes save dialogue appearing when on closing terminal tabs with
running tasks (follow-up of
https://github.com/zed-industries/zed/pull/21374)

Release Notes:

- Fixed terminal pane tabs arrangement and closing
2024-12-14 16:13:04 +02:00
Michael Sloan
6e1cc5dad3 Remove Task::get_ready method I added, which is unusable in practice (#22012)
Does seem like such a mechanism should be possible, but not yet sure how
to define it.

Release Notes:

- N/A
2024-12-14 03:21:41 -07:00
Michael Sloan
c5fe6ef100 Hide the implementation of Task (#22009)
The `Option<T>` within `Ready` is confusing and using `None` for it can
cause crashes. There was actually one instance of this!

Release Notes:

- N/A
2024-12-14 02:52:22 -07:00
Kirill Bulatov
1ac60289fe Fix the compilation (#22010)
https://github.com/zed-industries/zed/pull/21706 was merged after
https://github.com/zed-industries/zed/pull/22004 and the CI missed that.

Release Notes:

- N/A
2024-12-14 10:02:46 +02:00
IViktorov
cbc226597c Prefer project (worktree) tasks to language/global tasks in task::Spawn (#21706)
`Inventory::list_tasks()` in `project` crate now is ordered by task
types. Worktree tasks comes first, language tasks second and global
tasks last.
That leads to `spawn_task_with_name()` from `task_ui` crate will find
worktree task first, so it's possible to override global tasks at
project level.

* `Inventory::templates_from_settings()` splitted to
`Inventory::global_templates_from_settings()` and
`Inventory::worktree_templates_from_settings()`.

* In tests function `list_tasks()` renamed to
`list_tasks_sorted_by_last_used()`, because it call's
`Inventory::used_and_current_resolved_tasks()`. Also added
`list_tasks()` which calls `Inventory::list_tasks()`.

Closes #20987 

Release Notes:

- Fix task::Spawn to search for task name in project tasks first.
2024-12-14 09:30:48 +02:00
Aaron Feickert
ff2d20780f Add setting for hover delay (#22006)
This PR adds a new `hover_popover_delay` setting that allows the user to
specify how long to wait before showing informational hover boxes. It
defaults to the existing delay.

Release Notes:

- Added a setting to control the delay for informational hover boxes
2024-12-13 20:34:16 -08:00
Ulysse Buonomo
cd5d8b4173 settings: Add max tabs option (#18933)
Add a `max_tabs` option to the settings that ensure no more than this
amount of tabs are open in a pane. If set to `null`, there is no limit.

Closes #4784

Release Notes:

- Added a `max_tabs` option to cap the maximum number of open tabs.
2024-12-13 20:32:55 -08:00
Wes Higbee
0be7cf8ea8 Show restart transformation button after successful inline assist (#20439)
When using inline assist, after successfully generating a transformation
it's not possible to generate a new transformation. Currently, you have
to modify the prompt (i.e. add a `<SPACE>` and hit `<ENTER>`) to
regenerate.

So, I changed the restart button to be visible after a successful
transformation. And in that case I map it to a different keyboard
shortcut because `menu::Confirm` is mapped to accept the current
suggestion.

Now, I can invoke a series of transforms back to back until I get what I
want!

It might also be desired to keep the accept button visible after
modifying the prompt (before submitting it). In that case we'll need to
remap accept to an alternate key (perhaps the same alt-shift-enter I am
using for restart. That wouldn't be too insane to remember. But maybe
someone has a better idea.

I don't care what the shortcut is, I just want the ability to regenerate
without adding/deleting spaces.

## Before

**Two choices** after a suggestions is presented. Also, a great example
of why I would want to regenerate the suggestion, it left some tokens
`<rewrite_this>`!

![CleanShot 2024-12-13 at 00 34
09](https://github.com/user-attachments/assets/3c1786ca-8ec5-48e2-b3dd-64de36e61f6a)

## After

**Three choices** after a suggestion is presented, the circular icon is
for regenerate, just like you see if you modify the prompt text.
![CleanShot 2024-12-13 at 00 37
58](https://github.com/user-attachments/assets/ceda300f-0851-48bf-ad3a-be44308c734e)


## Release Notes:

- Added Restart Button to Inline Assistant When Prompt Is Unchanged

---------

Co-authored-by: Danilo Leal <daniloleal09@gmail.com>
2024-12-13 20:31:54 -08:00
Mikayla Maki
4f96706161 Add the ability for tasks to target the center pane (#22004)
Closes #20060
Closes #20720
Closes #19873
Closes #9445

Release Notes:

- Fixed a bug where tasks would be spawned with their working directory
set to a file in some cases
- Added the ability to spawn tasks in the center pane, when spawning
from a keybinding:

```json5
[
  {
    // Assuming you have a task labeled "echo hello"
    "ctrl--": [
      "task::Spawn",
      { "task_name": "echo hello", "target": "center" }
    ]
  }
]
```
2024-12-13 19:39:46 -08:00
Nate Butler
4f439ae35f Update git_panel.rs 2024-12-13 21:19:16 -05:00
Brian Tan
85c3aec6e7 vim: Maintain block cursor for navigating/non-modifying operators (#21502)
The cursor shape now only changes to underline for operators that modify
text (like: delete, change, yank) while maintaining block shape for
navigation operators (like: find, till).

This better matches Vim/Nvim's behavior where the cursor only changes
shape when an operator that will modify text is pending.

Release Notes:

- vim: Improved cursor shape behavior to better match Vim
2024-12-13 19:06:18 -07:00
Nate Butler
c2eea3a474 start on commit editor 2024-12-13 21:06:18 -05:00
Nate Butler
695f06c020 Checkpoint 2024-12-13 20:18:09 -05:00
Marshall Bowers
901dbedf8d assistant2: Refine context pickers (#21996)
This PR adds some visual refinements to the context pickers in
Assistant2.

<img width="1159" alt="Screenshot 2024-12-13 at 5 11 24 PM"
src="https://github.com/user-attachments/assets/f85ce87f-6800-4fc2-8a10-8ec3232d30e9"
/>

<img width="1159" alt="Screenshot 2024-12-13 at 5 11 31 PM"
src="https://github.com/user-attachments/assets/9b13c76d-cb7c-4441-a855-1ec4de685e0c"
/>

Release Notes:

- N/A
2024-12-13 17:26:10 -05:00
Piotr Osiewicz
99dc85e6ec Format code/fix broken CI build (#21997)
In #21981 the CI didn't run for whatever reason. I've merged based off
of CI state alone and that led to CI breaking on main.

Release Notes:

- N/A
2024-12-13 17:25:54 -05:00
Peter Tripp
735849e201 Improve Editor::DuplicateSelection (#21976)
Improves the new `Editor::DuplicateSelection` @CharlesChen0823 added in
https://github.com/zed-industries/zed/pull/21154.

- Merge `duplicate_selection` and `duplicate_line` into single function.
- Add keyboard shortcuts to JetBrains and SublimeText keymaps.
- If the selection is empty (e.g. just a cursor) make
`Editor::DuplicateSelection` fallback to being the same as
`Editor::DuplicateLineDown`.
- Tested with multiple cursors and for multiple selections.

| Editor      | Action              | macOS       | Linux        |
| ----------- | ------------------- | ----------- | ------------ |
| VSCode      | Duplicate Selection |             |              |
| JetBrains   | Duplicate Selection | cmd-d       | ctrl-d       |
| XCode       | Duplicate           | cmd-d       | N/A          |
| SublimeText | duplicate_line      | cmd-shift-d | ctrl-shift-d |

This matches behavior of the `duplicate` functionality in all other
major editors, with one exception: other editors change the selection so
that the newly duplicated object, current Zed behavior leaves the
original selection unchanged (TODO?)
2024-12-13 16:32:33 -05:00
Silvano Cerza
06edcd18be Fix running Python commands that include paths with spaces (#21981)
This PR fixes running Python commands that include paths with spaces by
wrapping python commands and their arguments in quotation marks.

I fixed this only in Python as I noticed this while trying to run
`pytest` in Zed.

Probably this is not the best approach as it doesn't fix other languages
too, though I don't know enough about the codebase to fix it like that.
I'm not even sure if it's actually feasible right now.

I didn't add tests for this either as I couldn't really understand how
to easily to that, I tried to look at other languages but couldn't find
one that tests their `ContextProvider` directly.

Release Notes:

- Fix running Python commands that include paths with spaces

---------

Co-authored-by: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com>
2024-12-13 22:31:08 +01:00
Peter Tripp
e53c1a8ee3 ci: GitHub Actions Runner Cleanup (#21993)
- Only run top issues on zed-industries/zed
- Move Nightly ARM build to BuildJet
2024-12-13 16:30:53 -05:00
Joseph T. Lyons
421974f923 Use consistent casing for provider name in telemetry (#21991)
Release Notes:

- N/A
2024-12-13 16:13:47 -05:00
Marshall Bowers
c57cc35b03 assistant2: Add ability to fetch URLs as context (#21988)
This PR adds the ability to fetch URLs as context in Assistant2.

In the picker we use the search area as an input for the user to enter
the URL they wish to fetch:

<img width="1159" alt="Screenshot 2024-12-13 at 2 45 41 PM"
src="https://github.com/user-attachments/assets/b3b20648-2c22-4509-b592-d0291d25b202"
/>

<img width="1159" alt="Screenshot 2024-12-13 at 2 45 47 PM"
src="https://github.com/user-attachments/assets/7e6bab2d-2731-467f-9781-130c6e4ea5cf"
/>

Release Notes:

- N/A
2024-12-13 15:03:55 -05:00
Nate Butler
19d6e067af Toggle & Switch (#21979)
![CleanShot 2024-12-13 at 11 27
39@2x](https://github.com/user-attachments/assets/7c7828c0-c5c7-4dc6-931e-722366d4f15a)

- Adds the Switch component
- Updates `Selected`, `Selectable` -> `ToggleState`, `Toggleable`
- Adds `checkbox` and `switch` functions to align better with other
elements in our layout system.

We decided not to merge Switch and Checkbox. However, in a followup I'll
introduce a Toggle or AnyToggle enum so we can update
`CheckboxWithLabel` -> `ToggleWithLabel` as this component will work
exactly the same with either a Checkbox or a Switch.

Release Notes:

- N/A
2024-12-13 14:23:02 -05:00
Conrad Irwin
2f2e7f0317 Revert "Resolve documentation for visible completions (#21705)" (#21985)
This reverts commit ab595b0d55.

Release Notes:

- (preview only) Fixed a panic in completions
2024-12-13 12:22:26 -07:00
Michael Sloan
2b699053e6 Log invariant violations in fuzzy string match iterator (#21983)
Seeing frequent inscrutable panics here

Release Notes:

- N/A
2024-12-13 11:16:30 -07:00
Antonio Scandurra
01e5ac0a49 Maintain inline completion order, simplifying how we track pending completions (#21977)
Release Notes:

- N/A
2024-12-13 17:24:07 +01:00
Thorsten Ball
306f1c6838 zeta: Increase context lines to 32 (#21968)
Release Notes:

- N/A

Co-authored-by: Antonio <antonio@zed.dev>
2024-12-13 15:41:43 +01:00
Thorsten Ball
2f722e63a1 Highlight whitespace-only inline completions with background (#21954)
Noticed that whitespace-only insertions are really hard to make out, so
this changes it to make them visible by giving them a green background.

![screenshot-2024-12-13-10 49
09@2x](https://github.com/user-attachments/assets/10d83067-46f2-4cb5-97fa-0f44d254890d)


Release Notes:

- N/A

---------

Co-authored-by: Antonio <antonio@zed.dev>
2024-12-13 13:40:34 +01:00
Jaagup Averin
6838b6203a python: Refine highlighting (#21389)
Fixes:
* Types in binary unions as per [PEP
604](https://peps.python.org/pep-0604/) not highlighted;
   * `except*` keyword not highlighted;
* Classes beginning with `_` not recognized as such, however `_` is a
valid first character for private classes; additionally the regex for
parsing constant/class names appeared inconsistent and incomplete so was
adjusted;
   * Builtin types such as `float`, `dict`, etc not recognized as types;
   * **Update:** decorators with arguments not recognized as decorators;
* **Update:** docstrings after type alias assignments not recognized as
docstrings;
* **Update:** `and/in/is/not/or/is not/not in` not capturable as
keywords;
* **Update:** decorators with "nesting" (@x.y.z) not recognized as
decorators;

Before:

![new_before](https://github.com/user-attachments/assets/6f05262e-be3b-41bf-aee6-26438c2bf254)

After:

![new_after](https://github.com/user-attachments/assets/408c481c-5eb9-40c9-8e18-52ebf5a121d3)

Release Notes:

- N/A

---------

Co-authored-by: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com>
2024-12-13 12:40:16 +01:00
tims
5318f529de Improve editor open URL command to open the selected portion of URL (#21825)
Closes #21718

Just like in Vim, if a URL is selected, it opens exactly that portion of
the URL. Otherwise, if only the cursor is on a URL, it opens the entire
URL.

Zed currently does the latter. This PR also adds support for the former.


https://github.com/user-attachments/assets/8bdd2952-ceec-487c-b27a-5cea4258eb03

Release Notes:

- Updated the `editor: open url` to also handle the selected portion of
a URL.
2024-12-12 22:15:21 -08:00
Danilo Leal
096bbfead5 zeta: Adjust reviewing UI (#21932)
Most notably, adding a title bar-ish in the left column as so to add the
"from most recent to oldest" info, which is supposed to make scanning
the list of completions easier to do (at least it would've helped me
figure out that was sorted that way when I was wondering about it!).

<img width="800" alt="Screenshot 2024-12-12 at 16 24 36"
src="https://github.com/user-attachments/assets/1acc9951-3df0-4cd2-96ff-94ed555ecae5"
/>

Release Notes:

- N/A
2024-12-13 00:52:23 -03:00
Danilo Leal
0b4495a920 zeta: Adjust the "Jump To Edit" button visuals (#21933)
| One Dark | One Light |
|--------|--------|
| <img width="1495" alt="Screenshot 2024-12-12 at 16 27 12"
src="https://github.com/user-attachments/assets/897ee786-a6f7-4d4e-8722-301ac13e6d8c"
/> | <img width="1495" alt="Screenshot 2024-12-12 at 16 27 18"
src="https://github.com/user-attachments/assets/a78aa5e4-f327-41da-bc9c-6e102bc67fe2"
/> |

| One Dark | One Light |
|--------|--------|
| <img width="1495" alt="Screenshot 2024-12-12 at 16 26 54"
src="https://github.com/user-attachments/assets/0357468e-7b5f-4f92-bcdb-5f94e353d8b2"
/> | <img width="1495" alt="Screenshot 2024-12-12 at 16 26 59"
src="https://github.com/user-attachments/assets/20e0f47e-e20f-46a7-b053-8e528b0975d7"
/> |


Release Notes:

- N/A
2024-12-13 00:52:12 -03:00
Bennet Bo Fenner
636c28b652 project panel: Reintroduce project panel knockout color (#21926)
Reintroduces #20760 after it was reverted in #21807

Closes #20572

/cc @danilo-leal 

Release Notes:

- N/A

---------

Co-authored-by: Cole <cole@zed.dev>
Co-authored-by: Danilo Leal <daniloleal09@gmail.com>
2024-12-13 00:52:03 -03:00
Danilo Leal
6ceec5d9a2 Fix project and buffer search input width (#21949)
Closes https://github.com/zed-industries/zed/issues/21922

Now, both the project and buffer search inputs have a min-width set so
that text inside it, as well as the additional controls, are always
visible even at the window's smallest possible size, which looks like
this:

<img width="407" alt="Screenshot 2024-12-13 at 00 35 46"
src="https://github.com/user-attachments/assets/e6e2c4c6-4f75-4663-8c65-590e02141a5d"
/>


Release Notes:

- N/A
2024-12-13 00:51:51 -03:00
Nate Butler
9f0f63f92b Git panel refinements 2 (#21947)
Add entry list, scrollbar

Release Notes:

- N/A
2024-12-12 22:30:00 -05:00
0x2CA
b38e9e44d6 Fix hover popover font fallbacks (#21945)
Closes #21848

Release Notes:

- Fixed Hover Popover Font Callbacks
2024-12-12 18:30:25 -08:00
Wang Can
e0cbbf8d06 Fix opening repos when .git is a soft link (#21153)
Closes #ISSUE

## background
If a project is big, some times it will be splited into many small git
repos.
[google repo](https://gerrit.googlesource.com/git-repo/) is a tool to
manage a group of git repos.

But, any small git repo manged by this tool, have a difference with
normal git repo.
That is , the path `.git` in the root of the git repo, is not a normal
directory, but a soft link to real git bare dir.

### zed can not recognize the `git-repo` managed git repos
you can use the procedure to genreate this problem
```bash
# tested on linux
mkdir -p bad_git_repo_project
cd bad_git_repo_project
git init
echo "hello" > hi.txt
git add .
git commit -m "init commit"
echo "hello world" >> hi.txt

# modify the repo
mv .git ../.real_git_repo
ln -sf ../.real_git_repo .git
```
with vscode, after opening this project, git works well.
but for Zed, git not work(not git status, no git blame)


## how to fix
libgit2 can recognize git repo from the root of the project(dir that
have `.git`).
so, we can recognize the git project by opening from the project root
dir, but not the `.git` dir

This fix also works with normal git project.

### before fix

![image](https://github.com/user-attachments/assets/1fb53ff4-4ab1-402e-9640-608ca79e12a4)


### after fix

![image](https://github.com/user-attachments/assets/6b16bc54-34f0-4436-b642-3c5fa8b669bd)

Release Notes:
- Fix opening repos when .git is a soft link
2024-12-12 18:29:37 -08:00
Mikayla Maki
4eaa1c2514 Only debounce the cursor position in multibuffer excerpts (#21946)
Follow up to: https://github.com/zed-industries/zed/pull/20211

Release Notes:

- Improved the performance of the cursor position indicator in single
buffers
2024-12-12 18:27:06 -08:00
CharlesChen0823
b3de19a6bd editor: Add duplicate selection command (#21154)
Closes #4890 

Release Notes:

- Add duplicate selection command for editor
2024-12-12 17:57:24 -08:00
CharlesChen0823
241b14eeaf project_panel: Create items when the editor is dismissed via the mouse (#21045)
Closes #5036 

Release Notes:

- Created project panel items when the editor is dismissed via the mouse
2024-12-12 17:24:25 -08:00
Ozan
72d8f2e595 editor: Add "selection" key context (#21927)
This change allows defining keybindings that are active when there is a
text selection.

This is especially useful, as an example, for Emacs-like keybindings
where movement keybindings expand the selection.

Here is a snippet from my keymap.json that implements Emacs movements
when selection is active:

```json
{
    "context": "Editor && selection",
    "bindings": {
      "ctrl-f": "editor::SelectRight",
      "ctrl-b": "editor::SelectLeft",
      "ctrl-n": "editor::SelectDown",
      "ctrl-p": "editor::SelectUp",
      "ctrl-a": "editor::SelectToBeginningOfLine",
      "ctrl-e": "editor::SelectToEndOfLine",
      "alt-f": "editor::SelectToNextWordEnd",
      "alt-b": "editor::SelectToPreviousWordStart",
      "alt-<": "editor::SelectToBeginning",
      "alt->": "editor::SelectToEnd"
    }
  }
  ```

What do you think about inclusion of this feature? Should I add more granular `selection=single` `selection=multi`? 

Release Notes:

- Added "selection" context for keybindings that are active when there is a text selection.
2024-12-12 16:56:42 -08:00
Dan Dascalescu
3f6ac53856 Update GitHub bug issue template to refer to bugs instead of features (#21727)
Release Notes:

- N/A
2024-12-12 16:54:37 -08:00
João Otávio Biondo
74d7ce2d2b elixir: Improve ElixirLS LSP autocomplete to show labelDetails information (#21666)
Closes https://github.com/zed-industries/zed/issues/19688

Release Notes:

- Improved ElixirLS LSP autocomplete to show module, function and struct
field details

![image](https://github.com/user-attachments/assets/2f05183f-8f7f-42c3-ba14-28fc58522488)

![image](https://github.com/user-attachments/assets/bfdea373-79ec-4dec-a9c7-5d15ad9403ee)

![image](https://github.com/user-attachments/assets/c0fd66d5-0e01-4e1e-a2d5-0a78d38e0b72)
2024-12-12 16:16:23 -08:00
tims
6a37307302 Add .prettierignore support (#21297)
Closes #11115

**Context**:

Consider a monorepo setup like this: the root has Prettier installed,
but the individual monorepos do not. In this case, only one Prettier
instance is used, with its installation located at the root. The
monorepos also use this same instance for formatting.

However, monorepo can have its own `.prettierignore` file, which will
take precedence over the `.prettierignore` file at the root level (if
one exists) for files in that monorepo.

<img
src="https://github.com/user-attachments/assets/742f16ac-11ad-4d2f-a5a2-696e47a617b9"
alt="prettier" width="200px" />

**Implementation**:

From the context above, we should keep ignore dir decoupled from the
Prettier instance. This means that even if the project has only one
Prettier installation (and thus a single Prettier instance), there can
still be multiple `.prettierignore` in play.

This approach also allows us to respect `.prettierignore` even when the
project does not have Prettier installed locally and instead relies on
the editor’s Prettier instance.

**Tests**:

1. No Prettier in project, using editor Prettier: Ensures
`.prettierignore` is respected even without a local Prettier
installation.
2. Monorepo with root Prettier and child `.prettierignore`: Confirms
that the child project’s ignore file is correctly used.
3. Monorepo with root and child `.prettierignore` files: Verifies the
child ignore file takes precedence over the root’s.

Release Notes:

- Added `.prettierignore` support to the Prettier integration.
2024-12-12 15:45:44 -08:00
xzbdmw
8dd1c23b92 editor: Add debounce setting for triggering DocumentHighlight (#21702)
Closes https://github.com/zed-industries/zed/issues/6843


I don't see where is the logic to remove old document highlight when new
one applies,
ideally, old highlight should be cleared as soon as possible when cursor
moves if the new position does not
sits in old highlight ranges to avoid linger highlights described in
https://github.com/zed-industries/zed/issues/13682#issuecomment-2498368680.

So current solution is still not ideal, because only when lsp responses
highlight ranges (even is a empty set) can we clear the old one.

Release Notes:

- Added a setting `lsp_highlight_debounce` to configure delay for
querying highlights from language server.

---------

Co-authored-by: mgsloan@gmail.com <michael@zed.dev>
2024-12-12 15:37:58 -08:00
Evren Sen
57874717c1 Add metal icon (#21720)
Release Notes:

- Added file icon for metal

---------

Co-authored-by: Danilo Leal <daniloleal09@gmail.com>
2024-12-12 15:23:20 -08:00
Aaron Feickert
bab6a79ab6 Fix audio tooltip logic (#21941)
Earlier work by @osiewicz in #21931 aims to fix audio control tooltips
in the title bar to close #21929. However, its logic is not quite
correct, and does not match the toggle behavior for the controls.

This PR corrects the logic to match the toggle behavior for the
controls. It also updates capitalization and wording for consistency.

Release Notes:

- N/A
2024-12-12 15:20:21 -08:00
uncenter
9a806f98e6 Improve diff syntax highlighting queries (#21740)
Brings over the improvements made for the same grammar:
https://github.com/nvim-treesitter/nvim-treesitter/pull/6619.

Related to #19986 but not really- the problem brought up there is an
issue of themes not supporting the `diff.plus` and `diff.minus` captures
(already used before this PR).

<details><summary>Theme previews (before/after)</summary>

| Before | After |
| --- | --- |
| ![CleanShot 2024-12-09 at 07 33
31](https://github.com/user-attachments/assets/698122df-fb63-4d7c-95aa-9559c7dcc684)
| ![CleanShot 2024-12-09 at 07 31
08](https://github.com/user-attachments/assets/ef927c0d-6c77-4fd4-b513-8359fb2442f7)
|

| Before | After |
| --- | --- |
| ![CleanShot 2024-12-09 at 07 34
15](https://github.com/user-attachments/assets/53b825ec-2987-4122-837d-1ebce334f153)
| ![CleanShot 2024-12-09 at 07 31
36](https://github.com/user-attachments/assets/079f19fb-4cc4-4256-b390-868f33e775c5)
|

| Before | After |
| --- | --- |
| ![CleanShot 2024-12-09 at 07 34
27](https://github.com/user-attachments/assets/4e3a80da-edff-4a53-bbf8-abc17cd49c5e)
| ![CleanShot 2024-12-09 at 07 31
53](https://github.com/user-attachments/assets/c6e12d79-5e59-4ebf-9fb9-ef3b0f8c9a81)
|

| Before | After |
| --- | --- |
| ![CleanShot 2024-12-09 at 07 33
44](https://github.com/user-attachments/assets/a007df22-7012-4de7-a71e-0ce5b523b561)
| ![CleanShot 2024-12-09 at 07 32
13](https://github.com/user-attachments/assets/c8c63292-5a64-4560-ad7c-9235b8b98ca3)
|

| Before | After |
| --- | --- |
| ![CleanShot 2024-12-09 at 07 33
57](https://github.com/user-attachments/assets/1a9c3656-3805-45a6-97af-747ef50e3b6c)
| ![CleanShot 2024-12-09 at 07 32
25](https://github.com/user-attachments/assets/76bac31c-8786-4907-8570-bf3c2888823e)
|

</details>

Release Notes:

- Improved diff syntax highlighting
2024-12-12 15:18:36 -08:00
CharlesChen0823
e778635487 search: Add ToggleRegex for buffer search (#21799)
Closes #21790 

IMO, this is lost

Release Notes:

- Add ToggleRegex for buffer search
2024-12-12 15:16:39 -08:00
5de0bcc990 gpui: Fix for setting window titles on Windows (#21907)
Windows requires `WM_NCCREATE` to be processed by default procedure to
set window title properly.

Release Notes:

- N/A
2024-12-12 14:58:30 -08:00
Marshall Bowers
9143fd2924 language_model_selector: Don't recreate the Picker view each render (#21939)
While working on Assistant2, I noticed that the `LanguageModelSelector`
was recreating its `Picker` view on every single render.

This PR makes it so we create the view once and hold onto it in the
parent view.

Release Notes:

- N/A
2024-12-12 17:08:48 -05:00
Joseph T. Lyons
d7eba54016 Add version control file icon for gitcommit files (#21935)
Closes: https://github.com/zed-industries/zed/issues/21734

<img width="976" alt="SCR-20241212-nlci"
src="https://github.com/user-attachments/assets/d567e2c8-d803-4148-b159-ae781eb59b50"
/>

I added the same file extensions that are used in the `Git Firefly`
extension.


b521b71324/languages/gitcommit/config.toml (L5-L9)

Release Notes:

- Added version control file icon for gitcommit files.
2024-12-12 16:23:17 -05:00
Marshall Bowers
52c0d712a6 assistant2: Add initial support for attaching file context (#21934)
This PR adds the initial support for attaching files as context to a
thread in Assistant2.

Release Notes:

- N/A
2024-12-12 15:30:17 -05:00
Piotr Osiewicz
111e844753 title_bar: Adjust tooltip for mute/deafen buttons (#21931)
Closes #21929 

Release Notes:

- N/A
2024-12-12 20:09:52 +01:00
Kyle Kelley
0eb992219b Set User Agent for Jupyter websocket connections (#21910)
Some VPN configurations require that websockets present a user agent.
This adds it in directly for the repl usage. I wish there was a way to
reuse the user agent from the `cx.http_client`, but I'm not seeing a
simple way to do that for the moment since it's not on the `HttpClient`
trait.

No release notes since this feature hasn't been announced/exposed.

Release Notes:

- N/A
2024-12-12 09:26:16 -08:00
Nate Butler
573e096fc5 More Git panel refinements (#21928)
- Add and wire through git method stubs
- Organize render methods
- Track modifier changes
- Swap commit buttons when `option`/`alt` is held
- More TODOs

Release Notes:

- N/A
2024-12-12 12:21:08 -05:00
Cole Miller
ee6f834028 Fuse LLM completion stream to avoid a panic (#21914)
`LanguageModel::stream_completion_text` can poll the `stream_completion`
stream (ultimately a `futures::Unfold`) after it's returned
`Ready(None)`, which leads to a panic; avoid this by fusing the stream.

Release Notes:

- Fixed a panic when streaming language model completions
2024-12-12 11:39:35 -05:00
Antonio Scandurra
b4c8e04544 Clear completion if model doesn't produce any edit (#21925)
Release Notes:

- N/A
2024-12-12 17:23:22 +01:00
Richard Feldman
bcf8a2f9fc Inline terminal assistant v2 (#21888)
Follow-up to https://github.com/zed-industries/zed/pull/21828 to add it
to the terminal as well.


https://github.com/user-attachments/assets/505d1443-4081-4dd8-9725-17d85532f52d

As with the previous PR, there's plenty of code duplication here; the
plan is to do more code sharing in separate PRs!


Release Notes:

- N/A
2024-12-12 11:06:09 -05:00
Piotr Osiewicz
77d066200a lsp: Fill in a bunch of missing capabilities (#21924)
Also state explicitly that we do support UTF-16 encoding and nothing
else.

See also #19788

Release Notes:

- N/A
2024-12-12 16:39:29 +01:00
Peter Tripp
5d0e75dd73 Improve emacs keybind with better home/end behavior (#21923)
Improve behavior of ctrl-a/ctrl-e home/end in emacs keybind.
Follow up to #21921 to add those to Linux emacs keymap too.

Release Notes:

- emacs: Improved `ctrl-a` / `ctrl-e` / `home` / `end` behavior
- emacs: Added for `ctrl-s` / `ctrl-r` / `ctrl-g` for navigating buffer
search results
2024-12-12 10:37:15 -05:00
Aaron Feickert
181af7804b Fix docstring for CallSettingsContent.share_on_join (#21884) 2024-12-12 10:09:28 -05:00
Antonio Scandurra
ad4c4aff13 Always let two completions race with each other (#21919)
When a user types, chances are the model will anticipate what they are
about to do. Previously, we would continuously cancel the pending
completion until the user stopped typing. With this commit, we allow at
most two completions to race with each other (the first and the last
one):

- If the completion that was requested first completes first, we will
show it (assuming we can interpolate it) but avoid canceling the last
one.
- When the completion that was requested last completes, we will cancel
the first one if it's pending.

In both cases, if a completion is already on-screen we have a special
case for when the completions are just insertions and the new completion
is a superset of the existing one. In this case, we will replace the
existing completion with the new one. Otherwise we will keep showing the
old one to avoid thrashing the UI.

This should make latency a lot better. Note that I also reduced the
debounce timeout to 8ms.

Release Notes:

- N/A
2024-12-12 16:01:05 +01:00
Peter Tripp
91b02a6259 Add emacs keybinds for previous/next/cancel in search (#21921) 2024-12-12 09:50:54 -05:00
xuoe
1f296d8f3b docs: Include restore_on_startup (#21918)
Signed-off-by: xuoe <xuoe@pm.me>
2024-12-12 09:21:27 -05:00
Danilo Leal
c204b0d01a zeta: Add adjustments to the review modal UI (#21920)
Most notably, adding a current iteration of a possible logo to feel it
out! :) Also, I'm hiding the input and instructions container after the
review has been sent. In the future, if we allow changing an already
sent review, we can change this behavior.

<img width="800" alt="Screenshot 2024-12-12 at 10 42 44"
src="https://github.com/user-attachments/assets/37e63d0d-d847-445e-bdf8-bf5c97d0fe4c"
/>

Release Notes:

- N/A
2024-12-12 11:17:08 -03:00
Nate Butler
8e0ae441f3 Initial git panel refinements (#21912)
- Wire up settings
- Update static Panel impl
- Tidy up renders

Release Notes:

- N/A
2024-12-12 09:13:40 -05:00
Raphael Kieling
02fbad18ce toolbar: Add gap between the Kernel and REPL button (#21871)
Before:

![image](https://github.com/user-attachments/assets/dbc382a8-2ba5-4639-964f-35c934875e88)

After:

![image](https://github.com/user-attachments/assets/5faf2144-63c3-41d4-b1b8-fcd6f6fd7b7e)

Also works with dark themes:

![image](https://github.com/user-attachments/assets/1f3e9bfb-94f8-44f2-9727-e46fddccb153)

Release Notes:

- N/A

Co-authored-by: raphael.kieling <raphael.kieling-ext@ab-inbev.com>
2024-12-12 09:49:17 -03:00
Thorsten Ball
227f21f035 zeta: Show timestamps and latency in rating modal (#21863)
Release Notes:

- N/A

---------

Co-authored-by: Antonio <antonio@zed.dev>
Co-authored-by: Bennet <bennet@zed.dev>
Co-authored-by: Cole <cole@zed.dev>
2024-12-12 09:30:20 +01:00
Cole Miller
543a3ef5e4 linux: Don't watch parent directory when target path already exists (#21854)
The Linux watcher was unconditionally watching the parent directory of
every watched path. This is needed in the case of config files that may
not exist when the watch is set up, but not in other cases. Scoping the
parent watch more narrowly cuts down on the amount of error logging from
irrelevant file change notifications being sent to Zed (in my case it
was picking up changes to a random file in `$HOME`).

Release Notes:

- N/A
2024-12-12 01:54:14 -05:00
Cole Miller
cc97e682d5 worktree: Fix privacy check for singleton files (#21861)
Closes #20676

Release Notes:

- Fixed private files not being redacted when not part of a larger
worktree
2024-12-12 01:53:00 -05:00
Nate Butler
59afc27f03 Add placeholder git panel (#21894)
Adds a simple git placeholder panel for us to iterate from. Also
includes a number of assets from the git prototyping branch that we will
use.

Note: This panel is staff flagged for now.

Release Notes:

- N/A
2024-12-11 22:13:52 -05:00
Peter Tripp
611abcadc0 Add schema to .github/ISSUE_TEMPLATE/config.yml (#21836)
Workaround for upstream issue where yaml-language-server
2024-12-11 17:16:21 -05:00
Peter Tripp
fff12ec1e5 Mention Lllama 3.3 in Ollama config panel (#21866)
Trivial, but makes us not look outdated.
2024-12-11 16:38:03 -05:00
Conrad Irwin
13a81e454a Start to split out initialization and registration (#21787)
Still TODO:

* [x] Factor out `start_language_server` so we can call it on register
(instead of on detect language)
* [x] Only call register in singleton editors (or when
editing/go-to-definition etc. in a multibuffer?)
* [x] Refcount on register so we can unregister when no buffer remain
* [ ] (maybe) Stop language servers that are no longer needed after some
time

Release Notes:

- Fixed language servers starting when doing project search
- Fixed high CPU usage when ignoring warnings in the diagnostics view

---------

Co-authored-by: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com>
Co-authored-by: Cole <cole@zed.dev>
2024-12-11 14:05:10 -07:00
Jason Lee
de89f8cf83 gpui: Add linear gradient support to fill background (#20812)
Release Notes:

- gpui: Add linear gradient support to fill background

Run example:

```
cargo run -p gpui --example gradient
cargo run -p gpui --example gradient --features macos-blade
```

## Demo

In GPUI (sRGB):

<img width="761" alt="image"
src="https://github.com/user-attachments/assets/568c02e8-3065-43c2-b5c2-5618d553dd6e">

In GPUI (Oklab):

<img width="761" alt="image"
src="https://github.com/user-attachments/assets/b008b0de-2705-4f99-831d-998ce48eed42">

In CSS (sRGB): 

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

<img width="505" alt="image"
src="https://github.com/user-attachments/assets/239f4b65-24b3-4797-9491-a13eea420158">

In CSS (Oklab):

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

<img width="658" alt="image"
src="https://github.com/user-attachments/assets/56fdd55f-d219-45de-922f-7227f535b210">


---

Currently only support 2 color stops with linear-gradient. I think this
is we first introduce the gradient feature in GPUI, and the
linear-gradient is most popular for use. So we can just add this first
and then to add more other supports.
2024-12-11 21:52:52 +02:00
Richard Feldman
c594ccb0af Inline assistant v2 (#21828)
This is behind the Assistant v2 feature flag. As @maxdeviant and I
discussed, the state is currently decoupled from the Assistant Panel's
state, although in the future we plan to introduce a way to refer to
conversations from the panel. Also, we're intentionally duplicating some
code with the v2 panel right now; the plan is to do a future PR to make
them share code more.


https://github.com/user-attachments/assets/bb163bd3-a02d-4a91-8f8f-2a8e60acbc34

It doesn't include the terminal inline assistant, which will be in a
separate PR.

Release Notes:

- N/A
2024-12-11 14:32:30 -05:00
Marshall Bowers
937186da12 gpui: Don't export named Context from prelude (#21869)
This PR updates the `gpui::prelude` to not export the `Context` trait
named.

This prevents some naming clashes in downstream consumers.

Release Notes:

- N/A
2024-12-11 13:21:40 -05:00
Marshall Bowers
b3ffbea376 assistant2: Allow removing individual context (#21868)
This PR adds the ability to remove individual pieces of context from the
message editor:

<img width="1159" alt="Screenshot 2024-12-11 at 12 38 45 PM"
src="https://github.com/user-attachments/assets/77d04272-f667-4ebb-a567-84b382afef3d"
/>

Release Notes:

- N/A
2024-12-11 12:51:05 -05:00
Thorsten Ball
124e63d07c Show inline completions when completion menu is visible (#21858)
This changes the behavior of how we display inline completions and
non-inline completions (i.e. completion menu).

Previously we would never show inline completions if a completion menu
was visible, meaning that we'd never show Copilot/Supermaven/...
suggestions if the language server had a suggestion.

With this change, we now display the inline completions even if there is
a completion menu visible.

In that case `<tab>` then accepts the inline completion and `<enter>`
accepts the selected entry in the completion menu.

Release Notes:

- Changed how inline completions (Copilot, Supermaven, ...) and normal
completions (from language servers) interact. Zed will now also show
inline completions when the completion menu is visible. The user can
accept the inline completion with `<tab>` and the active entry in the
completion menu with `<enter>`. Previously, `<tab>` would also select
the active entry in the completion menu.

---------

Co-authored-by: Antonio <antonio@zed.dev>
2024-12-11 17:13:22 +01:00
Antonio Scandurra
dd66a20d78 Move prediction diff computation to background thread (#21862)
Release Notes:

- N/A

Co-authored-by: Thorsten <thorsten@zed.dev>
Co-authored-by: Cole <cole@zed.dev>
Co-authored-by: Bennet <bennet@zed.dev>
2024-12-11 17:12:58 +01:00
Joseph T. Lyons
e8c72d91c3 v0.167.x dev 2024-12-11 11:00:35 -05:00
Danilo Leal
dfe455b054 zeta: Improve UI for feedback instructions (#21857)
If the instructions are added as the input placeholder, when in a
smaller window size (like the one from the screenshot), scrolling is
needed to see them all. So, thought of extracting it out of there. Also
thought it looked more refined this way!

<img width="800" alt="Screenshot 2024-12-11 at 11 48 17"
src="https://github.com/user-attachments/assets/46974b94-6365-4a59-bf71-a6c0863aac68"
/>

Release Notes:

- N/A
2024-12-11 12:07:41 -03:00
Danilo Leal
db7e38464a zeta: Show keybinding on rating buttons (#21853)
<img width="800" alt="Screenshot 2024-12-11 at 10 57 00"
src="https://github.com/user-attachments/assets/6055639c-5b38-444d-b76d-bf7584a82efc"
/>

Release Notes:

- N/A
2024-12-11 11:54:39 -03:00
Kyle Kelley
f8b6d71670 Optimize REPL kernel spec refresh (#21844)
Python kernelspec refresh now only performed on (known) python files. 

Release Notes:

- N/A
2024-12-11 06:20:44 -08:00
Thorsten Ball
ae351298b4 zeta: Fixes to completion-rating modal (#21852)
Release Notes:

- N/A

Co-authored-by: Antonio <antonio@zed.dev>
2024-12-11 15:00:27 +01:00
Thorsten Ball
664468d468 zeta: Invalidate completion in different buffers (#21850)
Release Notes:

- N/A

Co-authored-by: Antonio <antonio@zed.dev>
2024-12-11 14:37:53 +01:00
Piotr Osiewicz
714f183ede multi_buffer: optimize runnables layout (#21849)
Related to #21481 ; it fixes a bunch of hotspots I saw while looking at
the provided profiles. MultiBuffer still takes up 100% CPU on the
foreground thread for me - this time around it's on selection updates
(when dragging the selected text towards an edge of a screen).

Release Notes:

- N/A
2024-12-11 13:46:08 +01:00
Mikayla Maki
b36dcf3b92 Improve Zeta rating ergonomics (#21845)
This PR adds keyboard shortcuts to common interactions you might want to
do in the Zeta rating panel.

This PR also adds a way to fake inline completion requests, as well as
the test data used to create this PR, to make it easier to adjust the UI
in the future.

It also changes the status bar from the text "Zeta" to "ζ", because I
thought it looked cool.

Release Notes:

- N/A
2024-12-11 01:57:46 -08:00
Danilo Leal
63e1bf01a4 zeta: Improve reviewing UI (#21838)
Starting to fine-tune it.

| No edits scenario | Rated edits scenario |
|--------|--------|
| <img width="1577" alt="Screenshot 2024-12-11 at 01 57 46"
src="https://github.com/user-attachments/assets/42926e84-7a7f-4692-af44-672b52d3d377">
| <img width="1577" alt="Screenshot 2024-12-11 at 01 58 37"
src="https://github.com/user-attachments/assets/ee8ab0ef-75af-424c-b916-9f1ce8b5264d">

Release Notes:

- N/A
2024-12-11 02:19:57 -03:00
Connor Tsui
62a6a755ec Add musl package for Arch Linux (#21830)
It seems like `musl` is required to build on Arch Linux, but it is not included in the dependencies list.
2024-12-10 21:05:53 -05:00
Ethan Budd
28faba12a2 Recognize .C and .H as supported cpp extensions (#21647)
Co-authored-by: Peter Tripp <peter@zed.dev>
2024-12-10 19:55:21 -05:00
Marshall Bowers
c255e55599 assistant2: Sketch in sending file context to model (#21829)
This PR sketches in support for sending file context attached to a
message to the model.

Right now the context is just mocked.

<img width="1159" alt="Screenshot 2024-12-10 at 4 18 41 PM"
src="https://github.com/user-attachments/assets/3ee4e86a-7893-42dc-98f9-982aa202d310">

<img width="1159" alt="Screenshot 2024-12-10 at 4 18 53 PM"
src="https://github.com/user-attachments/assets/8a3c2dd7-a466-4dbf-83ec-1c7d969c1a4b">

Release Notes:

- N/A
2024-12-10 16:35:53 -05:00
Joseph T. Lyons
f80eb73213 Update event type to conform to standard (#21827)
Release Notes:

- N/A
2024-12-10 16:14:31 -05:00
strowk
faf79e52fe zed_extension_api: Add a short explanation of repo format (#21824)
Improved extension api documentation for latest_github_release function

Release Notes:

- N/A

---------

Co-authored-by: Marshall Bowers <elliott.codes@gmail.com>
2024-12-10 15:04:47 -05:00
Michael Sloan
ab595b0d55 Resolve documentation for visible completions (#21705)
Release Notes:

- Improved LSP resolution of documentation for completions. It now
queries documentation for visible completions and avoids doing too many
redundant queries.

---

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

This change resolves this by attempting to fetch all the completions
that will be shown. It also mostly avoids re-resolving completions. It
intentionally re-resolves the current selection on navigation, as some
language servers will respond with more information later on.
2024-12-10 12:25:30 -07:00
Michael Sloan
ab1e9bf270 On windows, recreate renderer swap chain on restore from minimized (#21756)
Closes #21688

Release Notes:

- Windows: Fix freeze after window minimize and maximize
2024-12-10 11:59:44 -07:00
Minqi Pan
adc66473e7 gpui: Add cursor style methods of nesw nwse resize (#21801)
Release Notes:

- N/A

---

This change adds two new methods to the cursor_style_methods function in
the gpui_macros crate (according to the Tailwind CSS documentation
https://tailwindcss.com/docs/cursor):
1. `cursor_nesw_resize`: Sets the cursor style to nesw-resize when
hovering over an element. This is useful for indicating resizing
diagonally from top-right to bottom-left.
2. `cursor_nwse_resize`: Sets the cursor style to nwse-resize when
hovering over an element. This is used for resizing diagonally from
top-left to bottom-right.
2024-12-10 11:54:26 -07:00
Marshall Bowers
119b5de384 assistant2: Change chat keybinding to just Enter (#21819)
This PR changes the Assistant2 chat keybinding from `Cmd-Enter` to just
`Enter`.

Release Notes:

- N/A
2024-12-10 12:34:54 -05:00
Marshall Bowers
c80ea60860 assistant2: Update to match latest designs (#21818)
This PR updates the Assistant2 panel to match the latest designs.

<img width="1159" alt="Screenshot 2024-12-10 at 11 49 14 AM"
src="https://github.com/user-attachments/assets/53739709-e7b9-4e35-8a5d-97b6560623ed">

Release Notes:

- N/A
2024-12-10 12:05:30 -05:00
Peter Tripp
bac6896786 Add Dart docs for line length (#21815) 2024-12-10 11:22:17 -05:00
Bennet Bo Fenner
c6932d1f51 zeta: Add action to clear edit history (#21813)
Co-Authored-by: Antonio <antonio@zed.dev>

Release Notes:

- N/A

---------

Co-authored-by: Antonio <antonio@zed.dev>
2024-12-10 16:57:24 +01:00
Conrad Irwin
03efd0d1d9 Stop sending data to Clickhouse (#21763)
Release Notes:

- N/A
2024-12-10 08:47:29 -07:00
Thorsten Ball
43ba0c9fa6 zeta: Extend text in popover until EOL (#21811)
Release Notes:

- N/A

Co-authored-by: Antonio <antonio@zed.dev>
Co-authored-by: Bennet <bennet@zed.dev>
2024-12-10 16:21:45 +01:00
Thorsten Ball
4300ef840b zeta: Use word-wise diff when computing edits (#21810)
Release Notes:

- N/A

Co-authored-by: Antonio <antonio@zed.dev>
Co-authored-by: Bennet <bennet@zed.dev>
2024-12-10 16:05:34 +01:00
Bennet Bo Fenner
e0f4c01794 Revert "Improve project_panel diagnostic icon knockout colors (#20760)" (#21807)
This reverts commit 571c7d4f66.

Manually tracking the hovered entities causes issues with hightlighting:


https://github.com/user-attachments/assets/932dc022-a0ad-485c-a9db-ef03d7b86032

cc @danilo-leal @nilskch 

Release Notes:

- Fixed an issue where hovering over project panel would not update the
background correctly
2024-12-10 15:37:33 +01:00
Bennet Bo Fenner
58f9301253 image viewer: Allow dropping images on pane (#21803)
Partially addresses #21484


https://github.com/user-attachments/assets/777da5de-15c3-4af3-a597-1835c0155326

Release Notes:

- Support opening images by dropping them onto a pane
2024-12-10 15:01:14 +01:00
Thorsten Ball
96499b7b25 zeta: Refresh LLM token in case it expired (#21796)
Release Notes:

- N/A

---------

Co-authored-by: Antonio <antonio@zed.dev>
Co-authored-by: Bennet <bennet@zed.dev>
2024-12-10 14:12:49 +01:00
Finn Evers
09006aaee9 Add option to activate left neighbour tab on tab close (#21800)
Closes #21738

Release Notes:

- Added `left_neighbour` option to the `tabs.activate_on_close` setting
to activate the left adjacent tab on tab close.
2024-12-10 15:05:36 +02:00
Kirill Bulatov
2ca3b440a9 Fix a panic when drop-splitting the terminal panel (#21795)
Closes https://github.com/zed-industries/zed/issues/21792

Release Notes:

- (Preview only) Fixed a panic when drop-splitting the terminal panel
2024-12-10 13:50:19 +02:00
Piotr Osiewicz
9219b05c85 chore: Move more local code into LocalLspStore (#21794)
Closes #ISSUE

Release Notes:

- N/A
2024-12-10 12:48:44 +01:00
Nils Koch
bd2087675b Fix git colors in image tabs (#21773)
Note that the git coloring of the icons got removed in
https://github.com/zed-industries/zed/pull/21383

Closes #21772

Release Notes:

- N/A
2024-12-10 01:40:25 -07:00
Jason Lee
44164dbbb8 gpui: Update Bounds, Point, and Axis to be serializable (#21783)
Makes `Bounds`, `Point`, and `Axis` be serializable, for dumping to JSON without conversion.

Release Notes:

- N/A
2024-12-10 00:43:55 -07:00
Conrad Irwin
3c053c7bc4 LspStore: move language_server_ids to local state too (#21786)
Attempt to further clarify what state is present in which mode

Release Notes:

- N/A
2024-12-10 00:15:06 -07:00
Conrad Irwin
48eed7499f Move diagnostics to the LocalLspStore (#21782)
This should be a no-op, but clarifies that some fields of the LspStore
were never actually used in the remote case.

Release Notes:

- N/A
2024-12-09 22:47:13 -07:00
Conrad Irwin
a35ef5b79f Fix diagnostics randomized tests (#21775)
These were silently passing after the delay in updating diagnostics was
added.

Co-Authored-By: Max <max@zed.dev>

cc @someonetoignore

Release Notes:

- N/A

---------

Co-authored-by: Max <max@zed.dev>
2024-12-09 21:56:43 -07:00
Marshall Bowers
8a85d6ef96 collab: Make metrics_id required in LlmTokenClaims (#21771)
This PR makes the `metrics_id` field on the `LlmTokenClaims` required,
as we always have one in practice.

Release Notes:

- N/A
2024-12-09 17:58:14 -05:00
Marshall Bowers
158cdc33ba collab: Attach additional properties to Language Model Used event (#21770)
This PR attaches two new properties to the `Language Model Used` event:

- `has_llm_subscription` - This will tell us if a user is a paid
subscriber.
- `max_monthly_spend_in_cents` - This will indicate what their maximum
monthly spend is set to.

Release Notes:

- N/A
2024-12-09 17:13:41 -05:00
Marshall Bowers
bdeac79d48 collab: Prevent max_monthly_llm_usage_spending_in_cents from being negative (#21768)
This PR fixes an issue where the
`max_monthly_llm_usage_spending_in_cents` preference could be set to a
negative value.

Release Notes:

- N/A
2024-12-09 16:55:26 -05:00
Mikayla Maki
73e0d816c4 Move ContextMenu out of editor.rs and rename ContextMenu to CodeContextMenu (#21766)
This is a no-functionality refactor of where the `ContextMenu` type is
defined. Just the type definition and implementation is up to almost
1,000 lines; so I've moved it to it's own file and renamed the type to
`CodeContextMenu`

Release Notes:

- N/A
2024-12-09 13:31:20 -08:00
Conrad Irwin
6538227f07 Revert "Avoid endless loop of the diagnostic updates (#21209)" (#21764)
This reverts commit 9999c31859.

Release Notes:

- Fixes diagnostics not updating in some circumstances
2024-12-09 14:15:23 -07:00
Marshall Bowers
ef45eca88e extension_host: Fix uploading dev extensions to the remote server (#21761)
This PR fixes an issue where dev extensions were not working when
uploaded to the remote server.

The `extension.toml` for dev extensions may not contain all of the
information (such as the list of languages), as this is something that
we derive from the filesystem at packaging time. This meant that
uploading a dev extension that contained languages could have them
absent from the uploaded `extension.toml`.

For dev extensions we now upload a serialized version of the in-memory
extension manifest, which should have all of the information present.

Release Notes:

- SSH Remoting: Fixed an issue where some dev extensions would not work
after being uploaded to the remote server.

---------

Co-authored-by: Conrad <conrad@zed.dev>
2024-12-09 15:23:28 -05:00
Michael Sloan
803855e7b1 Add async_task::spawn_local variant that includes caller in panics (#21758)
For debugging #21020. Copy-modified [from async_task
here](ca9dbe1db9/src/runnable.rs (L432))

Release Notes:

- N/A
2024-12-09 12:45:37 -07:00
Kirill Bulatov
25a5ad54ae Sync newly added diff hunks (#21759)
Fixed project diff multi buffer not expanding its diff until edited

Release Notes:

- N/A
2024-12-09 21:43:25 +02:00
Michael Sloan
a5355e92e3 Add per-language settings show_completions_on_input and show_completion_documentation (#21722)
Release Notes:

- Added `show_completions_on_input` and `show_completion_documentation`
per-language settings. These settings were available before, but were
not configurable per-language.
2024-12-09 11:53:50 -07:00
Piotr Osiewicz
b7edf31170 lsp: Disable usage of follow-up completion invokes (#21755)
Some of our users ran into a peculiar bug: autoimports with vtsls were
leaving behind an extra curly brace. I think we were slightly incorrect
in always requesting a follow-up completion without regard for last
result of completion request (whether it was incomplete or not).
Specifically, we're falling into this branch in current form:
037c2b615b/packages/service/src/service/completion.ts (L121)
which then leads to incorrect edits being returned from vtsls.

Release Notes:

- Fixed an edge case with appliance of autocompletions in VTSLS that
could result in incorrect edits being applied.
2024-12-09 19:10:34 +01:00
Michael Sloan
7bd69130f8 Make space for documentation aside during followup completion select (#21716)
The goal of #7115 appears to be to limit the disruptiveness of
completion documentation load causing the completion selector to move
around. The approach was to debounce load of documentation via a setting
`completion_documentation_secondary_query_debounce`. This particularly
had a nonideal interaction with #21286, where now this debounce interval
was used between the documentation fetches of every individual
completion item.

I think a better solution is to continue making space for documentation
to be shown as soon as any documentation is shown. #21704 implemented
part of this, but it did not persist across followup completions.

Release Notes:

- Fixed completion list moving around on load of documentation. The
previous approach to mitigating this was to rate-limit the fetch of
docs, configured by a
`completion_documentation_secondary_query_debounce` setting, which is
now deprecated.
2024-12-09 10:47:14 -07:00
Alexandre Hamez
2af9fa7785 docs: Add missing ':' (#21751)
Release Notes:

- N/A
2024-12-09 12:22:19 -05:00
Michael Sloan
16ecbafa7a Skip spawning task for background_executor.timer(Duration::ZERO) (#21729)
Release Notes:

- N/A
2024-12-09 10:18:18 -07:00
Travis Stevens
e5f3a683f0 Fixing Missing comma (#21749)
Fix a missing comma in the docs

Release Notes:

- N/A
2024-12-09 18:49:40 +02:00
Marshall Bowers
8c91eecb67 call: Add test-support feature for livekit_client_macos (#21748)
This PR updates the `call` crate to include the `test-support` feature
for `livekit_client_macos` when `call` is used with `test-support`.

This fixes running `cargo test -p copilot` and `cargo test -p editor`
(and perhaps some other crates).

Release Notes:

- N/A
2024-12-09 11:21:02 -05:00
Thorsten Ball
8fcaf8b870 collab: Fix compilation error by removing dependency on livekit_client (#21744)
This fixes collab not being able to compile anymore for Linux:


https://github.com/zed-industries/zed/actions/runs/12236650046/job/34130962682

Release Notes:

- N/A

Co-authored-by: Antonio <antonio@zed.dev>
2024-12-09 15:14:46 +01:00
Antonio Scandurra
77b8296fbb Introduce staff-only inline completion provider (#21739)
Release Notes:

- N/A

---------

Co-authored-by: Thorsten Ball <mrnugget@gmail.com>
Co-authored-by: Bennet <bennet@zed.dev>
Co-authored-by: Thorsten <thorsten@zed.dev>
2024-12-09 14:26:36 +01:00
Piotr Osiewicz
39e8944dcc language_tools: Split LSP log view selector into two (#21742)
This should make it easier to interact with LSP log view when there are
multiple language servers. I often find the current UI clunky when I
have over 5 servers running (which isn't uncommon with multiple projects
open)


https://github.com/user-attachments/assets/2ecaf17f-4b40-4c8f-aa6f-03b437a3d979


Closes #ISSUE

Release Notes:

- N/A
2024-12-09 14:10:11 +01:00
Danilo Leal
a7d12eea39 Enhance the Vim Mode toggle discoverability (#21589)
Closes https://github.com/zed-industries/zed/issues/21522

This PR adds an info tooltip on the Welcome screen, informing users how
Vim Mode can be toggled on and off. It also adds the Vim Mode toggle in
the Editor Controls menu. This is all so that folks who accidentally
turn it on better know how to turn it off. We're of course already able
to toggle this setting via the command palette, but that may be harder
to reach for beginners. So, maybe that's enough to close the linked
issue? Open to feedback.

(Note: I also added a max-width to the tooltip's label in this PR. I'm
confident that this won't make any tooltip look weird/broken, but if it
does, it may be because of this new property).

| Welcome Page | Editor Controls |
|--------|--------|
| <img width="800" alt="Screenshot 2024-12-05 at 11 20 04"
src="https://github.com/user-attachments/assets/1229f866-6be5-45cd-a6b8-c805f72144a6">
| <img width="800" alt="Screenshot 2024-12-05 at 11 12 15"
src="https://github.com/user-attachments/assets/f082d7f9-7d56-41d1-bc86-c333ad6264c7">
|

Release Notes:

- N/A

---------

Co-authored-by: Thorsten Ball <mrnugget@gmail.com>
2024-12-09 09:28:40 -03:00
Nils Koch
ce9e4629be Add go version to gopls cache key (#20922)
Closes #8071

Release Notes:

- Changed the Go integration to check whether an existing `gopls` was compiled for the current `go` version.

Previously we cached gopls (the go language server) as a file called
`gopls_{GOPLS_VERSION}`. The go version that gopls was built with is
crucial, so we need to cache the go version as well.

It's actually super interesting and very clever; gopls uses go to parse
the AST and do all the analyzation etc. Go exposes its internals in its
standard lib (`go/parser`, `go/types`, ...), which gopls uses to analyze
the user code. So if there is a new go release that contains new
syntax/features/etc. (the libraries `go/parser`, `go/types`, ...
change), we can rebuild the same version of `gopls` with the new version
of go (with the updated `go/xxx` libraries) to support the new language
features.

We had some issues around that (e.g., range over integers introduced in
go1.22, or custom iterators in go1.23) where we never updated gopls,
because we were on the latest gopls version, but built with an old go
version.

After this PR gopls will be cached under the name
`gopls_{GOPLS_VERSION}_go_{GO_VERSION}`.

Most users do not see this issue anymore, because after
https://github.com/zed-industries/zed/pull/8188 we first check if we can
find gopls in the PATH before downloading and caching gopls, but the
issue still exists.
2024-12-09 12:56:01 +01:00
Remco Smits
e58cdca044 Added JavaScript runnable detection for context and suite methods (#21719)
Fixes
https://github.com/zed-industries/zed/pull/21246#issuecomment-2525578141

<img width="545" alt="Screenshot 2024-12-08 at 22 58 33"
src="https://github.com/user-attachments/assets/2f303bfe-9718-4aa9-910e-613feca15ea8">
<img width="409" alt="Screenshot 2024-12-08 at 22 58 44"
src="https://github.com/user-attachments/assets/c4576cf7-fd71-44d2-911e-3ed944c9b794">

Release Notes:

- Added JavaScript runnable detection for `context` and `suite` methods
for mochajs framework
2024-12-09 13:17:51 +02:00
mgsloan@gmail.com
4564273322 Add comment explaining project panel behavior on right-click outside selection 2024-12-08 21:21:16 -07:00
João Marcos
55ee72d84a Simplify TextHighlights map (#21724)
Remove unnecessary `Option` wrapping.
2024-12-08 20:27:54 -07:00
tims
2ce01ead93 Fix right click selection behavior in project panel (#21707)
Closes #21605

Consider you have set of entries selected or even a single entry
selected, and you right click some other entry which is **not** part of
your selected set. This doesn't not clear existing entries selection
(which it should clear, as how file manager right-click logic works, see
more below).

This issue might lead unexpected operation like deletion applied on
those existing selected entries. This PR fixes it.

Release Notes:

- Fix right click selection behavior in project panel
2024-12-08 19:13:12 -07:00
Hendrik
bf1525588d Add .jj to default file exclusion (#21708)
Relates to #21538

Release Notes:

- Added `**/.jj` to the default file exclusion list.
2024-12-08 18:44:46 -07:00
Michael Sloan
d0e99f6496 Bump x11rb version to v0.13.1 (#21723)
From diff looks like no material differences. With a local checkout of
`v0.13.0` I get build errors due to warning checking when I use a `path
= ...` dependency, but it is fixed with `v0.13.1`.

I see mention of this in the [renovate configuration
PR](https://github.com/zed-industries/zed/pull/15132) but doesn't seem
like that initial batch of renovation happened.

Release Notes:

- N/A
2024-12-08 18:42:44 -07:00
605 changed files with 50732 additions and 46830 deletions

View File

@@ -26,8 +26,8 @@ body:
required: true
- type: textarea
attributes:
label: If applicable, add mockups / screenshots to help explain present your vision of the feature
description: Drag issues into the text input below
label: If applicable, add screenshots or screencasts of the incorrect state / behavior
description: Drag images / videos into the text input below
validations:
required: false
- type: textarea

View File

@@ -1,3 +1,4 @@
# yaml-language-server: $schema=https://json.schemastore.org/github-issue-config.json
blank_issues_enabled: false
contact_links:
- name: Language Request

View File

@@ -8,7 +8,7 @@ on:
jobs:
update_top_ranking_issues:
runs-on: ubuntu-latest
if: github.repository_owner == 'zed-industries'
if: github.repository == 'zed-industries/zed'
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
- name: Set up uv

View File

@@ -8,7 +8,7 @@ on:
jobs:
update_top_ranking_issues:
runs-on: ubuntu-latest
if: github.repository_owner == 'zed-industries'
if: github.repository == 'zed-industries/zed'
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
- name: Set up uv

View File

@@ -140,7 +140,7 @@ jobs:
name: Create a Linux *.tar.gz bundle for ARM
if: github.repository_owner == 'zed-industries'
runs-on:
- hosted-linux-arm-1
- buildjet-16vcpu-ubuntu-2204-arm
needs: tests
env:
DIGITALOCEAN_SPACES_ACCESS_KEY: ${{ secrets.DIGITALOCEAN_SPACES_ACCESS_KEY }}

1357
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -141,6 +141,8 @@ members = [
"crates/worktree",
"crates/zed",
"crates/zed_actions",
"crates/zeta",
"crates/git_ui",
#
# Extensions
@@ -226,6 +228,7 @@ fs = { path = "crates/fs" }
fsevent = { path = "crates/fsevent" }
fuzzy = { path = "crates/fuzzy" }
git = { path = "crates/git" }
git_ui = { path = "crates/git_ui" }
git_hosting_providers = { path = "crates/git_hosting_providers" }
go_to_line = { path = "crates/go_to_line" }
google_ai = { path = "crates/google_ai" }
@@ -325,6 +328,7 @@ workspace = { path = "crates/workspace" }
worktree = { path = "crates/worktree" }
zed = { path = "crates/zed" }
zed_actions = { path = "crates/zed_actions" }
zeta = { path = "crates/zeta" }
#
# External crates
@@ -358,7 +362,6 @@ cargo_metadata = "0.19"
cargo_toml = "0.20"
chrono = { version = "0.4", features = ["serde"] }
clap = { version = "4.4", features = ["derive"] }
clickhouse = "0.11.6"
cocoa = "0.26"
cocoa-foundation = "0.2.0"
convert_case = "0.6.0"

4
assets/icons/eraser.svg Normal file
View File

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-eraser">
<path d="m7 21-4.3-4.3c-1-1-1-2.5 0-3.4l9.6-9.6c1-1 2.5-1 3.4 0l5.6 5.6c1 1 1 2.5 0 3.4L13 21"/>
<path d="M22 21H7"/><path d="m5 11 9 9"/>
</svg>

After

Width:  |  Height:  |  Size: 365 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-file-diff"><path d="M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z"/><path d="M9 10h6"/><path d="M12 13V7"/><path d="M9 17h6"/></svg>

After

Width:  |  Height:  |  Size: 348 B

View File

@@ -59,6 +59,11 @@
"gitignore": "vcs",
"gitkeep": "vcs",
"gitmodules": "vcs",
"TAG_EDITMSG": "vcs",
"MERGE_MSG": "vcs",
"COMMIT_EDITMSG": "vcs",
"NOTES_EDITMSG": "vcs",
"EDIT_DESCRIPTION": "vcs",
"gleam": "gleam",
"go": "go",
"gql": "graphql",
@@ -108,6 +113,7 @@
"mdf": "storage",
"mdx": "document",
"metadata": "code",
"metal": "metal",
"mjs": "javascript",
"mka": "audio",
"mkv": "video",
@@ -317,6 +323,9 @@
"lua": {
"icon": "icons/file_icons/lua.svg"
},
"metal": {
"icon": "icons/file_icons/metal.svg"
},
"nim": {
"icon": "icons/file_icons/nim.svg"
},

View File

@@ -0,0 +1 @@
<svg width="16" height="16" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M4.56 4.502 3.25 3.027V11.5h1.5V6.973l2.69 3.025 1.31 1.475V7.918l3.306 3.582h2.042L8.55 5.491 7.25 4.081V7.528L4.56 4.502Z" fill="#000"/></svg>

After

Width:  |  Height:  |  Size: 269 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-git-branch"><line x1="6" x2="6" y1="3" y2="15"/><circle cx="18" cy="6" r="3"/><circle cx="6" cy="18" r="3"/><path d="M18 9a9 9 0 0 1-9 9"/></svg>

After

Width:  |  Height:  |  Size: 348 B

12
assets/icons/info.svg Normal file
View File

@@ -0,0 +1,12 @@
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_2131_1193)">
<circle cx="7" cy="7" r="6" stroke="black" stroke-width="1.5"/>
<path d="M6 10H7M8 10H7M7 10V7.1C7 7.04477 6.95523 7 6.9 7H6" stroke="black" stroke-width="1.5" stroke-linecap="round"/>
<circle cx="7" cy="4.5" r="1" fill="black"/>
</g>
<defs>
<clipPath id="clip0_2131_1193">
<rect width="14" height="14" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 479 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-panel-left"><rect width="18" height="18" x="3" y="3" rx="2"/><path d="M9 3v18"/></svg>

After

Width:  |  Height:  |  Size: 289 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-panel-right"><rect width="18" height="18" x="3" y="3" rx="2"/><path d="M15 3v18"/></svg>

After

Width:  |  Height:  |  Size: 291 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-square-dot"><rect width="18" height="18" x="3" y="3" rx="2"/><circle cx="12" cy="12" r="1"/></svg>

After

Width:  |  Height:  |  Size: 301 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-square-minus"><rect width="18" height="18" x="3" y="3" rx="2"/><path d="M8 12h8"/></svg>

After

Width:  |  Height:  |  Size: 291 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-square-plus"><rect width="18" height="18" x="3" y="3" rx="2"/><path d="M8 12h8"/><path d="M12 8v8"/></svg>

After

Width:  |  Height:  |  Size: 309 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-thumbs-down"><path d="M17 14V2"/><path d="M9 18.12 10 14H4.17a2 2 0 0 1-1.92-2.56l2.33-8A2 2 0 0 1 6.5 2H20a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2h-2.76a2 2 0 0 0-1.79 1.11L12 22a3.13 3.13 0 0 1-3-3.88Z"/></svg>

After

Width:  |  Height:  |  Size: 405 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-thumbs-up"><path d="M7 10v12"/><path d="M15 5.88 14 10h5.83a2 2 0 0 1 1.92 2.56l-2.33 8A2 2 0 0 1 17.5 22H4a2 2 0 0 1-2-2v-8a2 2 0 0 1 2-2h2.76a2 2 0 0 0 1.79-1.11L12 2a3.13 3.13 0 0 1 3 3.88Z"/></svg>

After

Width:  |  Height:  |  Size: 404 B

View File

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

After

Width:  |  Height:  |  Size: 268 B

View File

@@ -468,13 +468,21 @@
},
{
"context": "Editor && showing_completions",
"use_key_equivalents": true,
"bindings": {
"enter": "editor::ConfirmCompletion"
}
},
{
"context": "Editor && !inline_completion && showing_completions",
"use_key_equivalents": true,
"bindings": {
"enter": "editor::ConfirmCompletion",
"tab": "editor::ComposeCompletion"
}
},
{
"context": "Editor && inline_completion && !showing_completions",
"context": "Editor && inline_completion",
"use_key_equivalents": true,
"bindings": {
"tab": "editor::AcceptInlineCompletion"
}

View File

@@ -66,6 +66,7 @@
"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",
@@ -229,7 +230,7 @@
"context": "MessageEditor > Editor",
"use_key_equivalents": true,
"bindings": {
"cmd-enter": "assistant2::Chat"
"enter": "assistant2::Chat"
}
},
{
@@ -540,12 +541,18 @@
"context": "Editor && showing_completions",
"use_key_equivalents": true,
"bindings": {
"enter": "editor::ConfirmCompletion",
"enter": "editor::ConfirmCompletion"
}
},
{
"context": "Editor && !inline_completion && showing_completions",
"use_key_equivalents": true,
"bindings": {
"tab": "editor::ComposeCompletion"
}
},
{
"context": "Editor && inline_completion && !showing_completions",
"context": "Editor && inline_completion",
"use_key_equivalents": true,
"bindings": {
"tab": "editor::AcceptInlineCompletion"
@@ -788,5 +795,24 @@
"ctrl-k left": "pane::SplitLeft",
"ctrl-k right": "pane::SplitRight"
}
},
{
"context": "RateCompletionModal",
"use_key_equivalents": true,
"bindings": {
"cmd-enter": "zeta::ThumbsUp",
"shift-down": "zeta::NextEdit",
"shift-up": "zeta::PreviousEdit",
"right": "zeta::PreviewCompletion"
}
},
{
"context": "RateCompletionModal > Editor",
"use_key_equivalents": true,
"bindings": {
"escape": "zeta::FocusCompletions",
"cmd-shift-enter": "zeta::ThumbsUpActiveCompletion",
"cmd-shift-backspace": "zeta::ThumbsDownActiveCompletion"
}
}
]

View File

@@ -15,8 +15,10 @@
"ctrl-b": "editor::MoveLeft",
"ctrl-n": "editor::MoveDown",
"ctrl-p": "editor::MoveUp",
"ctrl-a": "editor::MoveToBeginningOfLine",
"ctrl-e": "editor::MoveToEndOfLine",
"home": ["editor::MoveToBeginningOfLine", { "stop_at_soft_wraps": false }],
"end": ["editor::MoveToEndOfLine", { "stop_at_soft_wraps": false }],
"ctrl-a": ["editor::MoveToBeginningOfLine", { "stop_at_soft_wraps": false }],
"ctrl-e": ["editor::MoveToEndOfLine", { "stop_at_soft_wraps": false }],
"alt-f": "editor::MoveToNextSubwordEnd",
"alt-b": "editor::MoveToPreviousSubwordStart",
"ctrl-d": "editor::Delete",
@@ -53,6 +55,14 @@
"shift shift": "file_finder::Toggle"
}
},
{
"context": "BufferSearchBar > Editor",
"bindings": {
"ctrl-s": "search::SelectNextMatch",
"ctrl-r": "search::SelectPrevMatch",
"ctrl-g": "buffer_search::Dismiss"
}
},
{
"context": "Pane",
"bindings": {

View File

@@ -12,7 +12,7 @@
"ctrl->": "zed::IncreaseBufferFontSize",
"ctrl-<": "zed::DecreaseBufferFontSize",
"ctrl-shift-j": "editor::JoinLines",
"ctrl-d": "editor::DuplicateLineDown",
"ctrl-d": "editor::DuplicateSelection",
"ctrl-y": "editor::DeleteLine",
"ctrl-m": "editor::ScrollCursorCenter",
"ctrl-pagedown": "editor::MovePageDown",

View File

@@ -15,7 +15,7 @@
"ctrl-shift-m": "editor::SelectLargerSyntaxNode",
"ctrl-shift-l": "editor::SplitSelectionIntoLines",
"ctrl-shift-a": "editor::SelectLargerSyntaxNode",
"ctrl-shift-d": "editor::DuplicateLineDown",
"ctrl-shift-d": "editor::DuplicateSelection",
"alt-f3": "editor::SelectAllMatches", // find_all_under
"f12": "editor::GoToDefinition",
"ctrl-f12": "editor::GoToDefinitionSplit",

View File

@@ -15,8 +15,10 @@
"ctrl-b": "editor::MoveLeft",
"ctrl-n": "editor::MoveDown",
"ctrl-p": "editor::MoveUp",
"ctrl-a": "editor::MoveToBeginningOfLine",
"ctrl-e": "editor::MoveToEndOfLine",
"home": ["editor::MoveToBeginningOfLine", { "stop_at_soft_wraps": false }],
"end": ["editor::MoveToEndOfLine", { "stop_at_soft_wraps": false }],
"ctrl-a": ["editor::MoveToBeginningOfLine", { "stop_at_soft_wraps": false }],
"ctrl-e": ["editor::MoveToEndOfLine", { "stop_at_soft_wraps": false }],
"alt-f": "editor::MoveToNextSubwordEnd",
"alt-b": "editor::MoveToPreviousSubwordStart",
"ctrl-d": "editor::Delete",
@@ -53,6 +55,14 @@
"shift shift": "file_finder::Toggle"
}
},
{
"context": "BufferSearchBar > Editor",
"bindings": {
"ctrl-s": "search::SelectNextMatch",
"ctrl-r": "search::SelectPrevMatch",
"ctrl-g": "buffer_search::Dismiss"
}
},
{
"context": "Pane",
"bindings": {

View File

@@ -11,7 +11,7 @@
"ctrl->": "zed::IncreaseBufferFontSize",
"ctrl-<": "zed::DecreaseBufferFontSize",
"ctrl-shift-j": "editor::JoinLines",
"cmd-d": "editor::DuplicateLineDown",
"cmd-d": "editor::DuplicateSelection",
"cmd-backspace": "editor::DeleteLine",
"cmd-pagedown": "editor::MovePageDown",
"cmd-pageup": "editor::MovePageUp",

View File

@@ -18,7 +18,7 @@
"ctrl-shift-m": "editor::SelectLargerSyntaxNode",
"cmd-shift-l": "editor::SplitSelectionIntoLines",
"cmd-shift-a": "editor::SelectLargerSyntaxNode",
"cmd-shift-d": "editor::DuplicateLineDown",
"cmd-shift-d": "editor::DuplicateSelection",
"ctrl-cmd-g": "editor::SelectAllMatches", // find_all_under
"shift-f12": "editor::FindAllReferences",
"alt-cmd-down": "editor::GoToDefinition",

View File

@@ -144,15 +144,15 @@
// 4. Highlight the full line (default):
// "all"
"current_line_highlight": "all",
// The debounce delay before querying highlights from the language
// server based on the current cursor location.
"lsp_highlight_debounce": 75,
// Whether to pop the completions menu while typing in an editor without
// explicitly requesting it.
"show_completions_on_input": true,
// Whether to display inline and alongside documentation for items in the
// completions menu
"show_completion_documentation": true,
// The debounce delay before re-querying the language server for completion
// documentation when not included in original completion list.
"completion_documentation_secondary_query_debounce": 300,
// Show method signatures in the editor, when inside parentheses.
"auto_signature_help": false,
/// Whether to show the signature help after completion or a bracket pair inserted.
@@ -474,6 +474,14 @@
// Default width of the chat panel.
"default_width": 240
},
"git_panel": {
// Whether to show the git panel button in the status bar.
"button": true,
// Where to the git panel. Can be 'left' or 'right'.
"dock": "left",
// Default width of the git panel.
"default_width": 360
},
"message_editor": {
// Whether to automatically replace emoji shortcodes with emoji characters.
// For example: typing `:wave:` gets replaced with `👋`.
@@ -564,9 +572,11 @@
// What to do after closing the current tab.
//
// 1. Activate the tab that was open previously (default)
// "History"
// 2. Activate the neighbour tab (prefers the right one, if present)
// "Neighbour"
// "history"
// 2. Activate the right neighbour tab if present
// "neighbour"
// 3. Activate the left neighbour tab if present
// "left_neighbour"
"activate_on_close": "history",
/// Which files containing diagnostic errors/warnings to mark in the tabs.
/// Diagnostics are only shown when file icons are also active.
@@ -685,6 +695,7 @@
"**/.git",
"**/.svn",
"**/.hg",
"**/.jj",
"**/CVS",
"**/.DS_Store",
"**/Thumbs.db",

View File

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

View File

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

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

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

View File

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

View File

@@ -14,7 +14,7 @@ use language_model::{
use semantic_index::{FileSummary, SemanticDb};
use smol::channel;
use std::sync::{atomic::AtomicBool, Arc};
use ui::{prelude::*, BorrowAppContext};
use ui::{prelude::*, BorrowAppContext, WindowContext};
use util::ResultExt;
use workspace::Workspace;
@@ -53,9 +53,8 @@ impl SlashCommand for AutoCommand {
self: Arc<Self>,
_arguments: &[String],
_cancel: Arc<AtomicBool>,
workspace: Option<WeakModel<Workspace>>,
window: &mut gpui::Window,
cx: &mut gpui::AppContext,
workspace: Option<WeakView<Workspace>>,
cx: &mut WindowContext,
) -> 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.
@@ -77,7 +76,7 @@ impl SlashCommand for AutoCommand {
let cx: &mut AppContext = cx;
cx.spawn(|cx: AsyncAppContext| async move {
cx.spawn(|cx: gpui::AsyncAppContext| async move {
let task = project_index.read_with(&cx, |project_index, cx| {
project_index.flush_summary_backlogs(cx)
})?;
@@ -97,10 +96,9 @@ impl SlashCommand for AutoCommand {
arguments: &[String],
_context_slash_command_output_sections: &[SlashCommandOutputSection<language::Anchor>],
_context_buffer: language::BufferSnapshot,
workspace: WeakModel<Workspace>,
workspace: WeakView<Workspace>,
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
window: &mut gpui::Window,
cx: &mut gpui::AppContext,
cx: &mut WindowContext,
) -> Task<SlashCommandResult> {
let Some(workspace) = workspace.upgrade() else {
return Task::ready(Err(anyhow::anyhow!("workspace was dropped")));
@@ -117,7 +115,7 @@ impl SlashCommand for AutoCommand {
return Task::ready(Err(anyhow!("no project indexer")));
};
let task = cx.spawn(|cx: AsyncAppContext| async move {
let task = cx.spawn(|cx: gpui::AsyncWindowContext| async move {
let summaries = project_index
.read_with(&cx, |project_index, cx| project_index.all_summaries(cx))?
.await?;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -13,30 +13,55 @@ path = "src/assistant.rs"
doctest = false
[dependencies]
anthropic = { workspace = true, features = ["schemars"] }
anyhow.workspace = true
assets.workspace = true
assistant_tool.workspace = true
chrono.workspace = true
async-watch.workspace = true
client.workspace = true
chrono.workspace = true
collections.workspace = true
command_palette_hooks.workspace = true
context_server.workspace = true
db.workspace = true
editor.workspace = true
feature_flags.workspace = true
fs.workspace = true
futures.workspace = true
fuzzy.workspace = true
gpui.workspace = true
handlebars.workspace = true
html_to_markdown.workspace = true
http_client.workspace = true
language.workspace = true
language_model.workspace = true
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
paths.workspace = true
parking_lot.workspace = true
picker.workspace = true
project.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
telemetry_events.workspace = true
terminal_view.workspace = true
text.workspace = true
terminal.workspace = true
theme.workspace = true
time.workspace = true
time_format.workspace = true
@@ -45,3 +70,8 @@ unindent.workspace = true
util.workspace = true
uuid.workspace = true
workspace.workspace = true
zed_actions.workspace = true
[dev-dependencies]
rand.workspace = true
indoc.workspace = true

View File

@@ -15,15 +15,16 @@ use ui::prelude::*;
use workspace::Workspace;
use crate::thread::{MessageId, Thread, ThreadError, ThreadEvent};
use crate::ui::ContextPill;
pub struct ActiveThread {
workspace: WeakModel<Workspace>,
workspace: WeakView<Workspace>,
language_registry: Arc<LanguageRegistry>,
tools: Arc<ToolWorkingSet>,
thread: Model<Thread>,
messages: Vec<MessageId>,
list_state: ListState,
rendered_messages_by_id: HashMap<MessageId, Model<Markdown>>,
rendered_messages_by_id: HashMap<MessageId, View<Markdown>>,
last_error: Option<ThreadError>,
_subscriptions: Vec<Subscription>,
}
@@ -31,14 +32,13 @@ pub struct ActiveThread {
impl ActiveThread {
pub fn new(
thread: Model<Thread>,
workspace: WeakModel<Workspace>,
workspace: WeakView<Workspace>,
language_registry: Arc<LanguageRegistry>,
tools: Arc<ToolWorkingSet>,
model: &Model<Self>,
cx: &mut AppContext,
cx: &mut ViewContext<Self>,
) -> Self {
let subscriptions = vec![
cx.observe(&thread, |_, _, cx| model.notify(cx)),
cx.observe(&thread, |_, _, cx| cx.notify()),
cx.subscribe(&thread, Self::handle_thread_event),
];
@@ -50,9 +50,9 @@ impl ActiveThread {
messages: Vec::new(),
rendered_messages_by_id: HashMap::default(),
list_state: ListState::new(0, ListAlignment::Bottom, px(1024.), {
let this = model.downgrade();
move |ix, window: &mut gpui::Window, cx: &mut gpui::AppContext| {
this.update(cx, |this, model, cx| this.render_message(ix, model, cx))
let this = cx.view().downgrade();
move |ix, cx: &mut WindowContext| {
this.update(cx, |this, cx| this.render_message(ix, cx))
.unwrap()
}
}),
@@ -61,7 +61,7 @@ impl ActiveThread {
};
for message in thread.read(cx).messages().cloned().collect::<Vec<_>>() {
this.push_message(&message.id, message.text.clone(), model, cx);
this.push_message(&message.id, message.text.clone(), cx);
}
this
@@ -83,13 +83,7 @@ impl ActiveThread {
self.last_error.take();
}
fn push_message(
&mut self,
id: &MessageId,
text: String,
model: &Model<Self>,
cx: &mut AppContext,
) {
fn push_message(&mut self, id: &MessageId, text: String, cx: &mut ViewContext<Self>) {
let old_len = self.messages.len();
self.messages.push(*id);
self.list_state.splice(old_len..old_len, 1);
@@ -98,7 +92,7 @@ impl ActiveThread {
let ui_font_size = TextSize::Default.rems(cx);
let buffer_font_size = theme_settings.buffer_font_size;
let mut text_style = window.text_style();
let mut text_style = cx.text_style();
text_style.refine(&TextStyleRefinement {
font_family: Some(theme_settings.ui_font.family.clone()),
font_size: Some(ui_font_size.into()),
@@ -127,14 +121,12 @@ impl ActiveThread {
..Default::default()
};
let markdown = cx.new_model(|model, cx| {
let markdown = cx.new_view(|cx| {
Markdown::new(
text,
markdown_style,
Some(self.language_registry.clone()),
None,
model,
window,
cx,
)
});
@@ -145,8 +137,7 @@ impl ActiveThread {
&mut self,
_: Model<Thread>,
event: &ThreadEvent,
model: &Model<Self>,
cx: &mut AppContext,
cx: &mut ViewContext<Self>,
) {
match event {
ThreadEvent::ShowError(error) => {
@@ -156,8 +147,8 @@ impl ActiveThread {
ThreadEvent::SummaryChanged => {}
ThreadEvent::StreamedAssistantText(message_id, text) => {
if let Some(markdown) = self.rendered_messages_by_id.get_mut(&message_id) {
markdown.update(cx, |markdown, model, cx| {
markdown.append(text, model, cx);
markdown.update(cx, |markdown, cx| {
markdown.append(text, cx);
});
}
}
@@ -168,10 +159,10 @@ impl ActiveThread {
.message(*message_id)
.map(|message| message.text.clone())
{
self.push_message(message_id, message_text, model, cx);
self.push_message(message_id, message_text, cx);
}
model.notify(cx);
cx.notify();
}
ThreadEvent::UsePendingTools => {
let pending_tool_uses = self
@@ -185,14 +176,13 @@ 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(), model, cx);
let task = tool.run(tool_use.input, self.workspace.clone(), cx);
self.thread.update(cx, |thread, model, cx| {
self.thread.update(cx, |thread, cx| {
thread.insert_tool_output(
tool_use.assistant_message_id,
tool_use.id.clone(),
task,
model,
cx,
);
});
@@ -203,7 +193,7 @@ impl ActiveThread {
}
}
fn render_message(&self, ix: usize, model: &Model<Self>, cx: &mut AppContext) -> AnyElement {
fn render_message(&self, ix: usize, cx: &mut ViewContext<Self>) -> AnyElement {
let message_id = self.messages[ix];
let Some(message) = self.thread.read(cx).message(message_id) else {
return Empty.into_any();
@@ -213,6 +203,8 @@ impl ActiveThread {
return Empty.into_any();
};
let context = self.thread.read(cx).context_for_message(message_id);
let (role_icon, role_name) = match message.role {
Role::User => (IconName::Person, "You"),
Role::Assistant => (IconName::ZedAssistant, "Assistant"),
@@ -240,19 +232,23 @@ impl ActiveThread {
.child(Label::new(role_name).size(LabelSize::Small)),
),
)
.child(v_flex().p_1p5().text_ui(cx).child(markdown.clone())),
.child(v_flex().p_1p5().text_ui(cx).child(markdown.clone()))
.when_some(context, |parent, context| {
parent.child(
h_flex().flex_wrap().gap_2().p_1p5().children(
context
.iter()
.map(|context| ContextPill::new(context.clone())),
),
)
}),
)
.into_any()
}
}
impl Render for ActiveThread {
fn render(
&mut self,
model: &Model<Self>,
_window: &mut gpui::Window,
_cx: &mut AppContext,
) -> impl IntoElement {
fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
list(self.list_state.clone()).flex_1()
}
}

View File

@@ -1,14 +1,29 @@
mod active_thread;
mod assistant_panel;
mod assistant_settings;
mod context;
mod context_picker;
mod inline_assistant;
mod message_editor;
mod prompts;
mod streaming_diff;
mod terminal_inline_assistant;
mod thread;
mod thread_history;
mod thread_store;
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 settings::Settings as _;
use util::ResultExt;
pub use crate::assistant_panel::AssistantPanel;
@@ -19,15 +34,43 @@ actions!(
NewThread,
ToggleModelSelector,
OpenHistory,
Chat
Chat,
ToggleInlineAssist,
CycleNextInlineAssist,
CyclePreviousInlineAssist
]
);
const NAMESPACE: &str = "assistant2";
/// Initializes the `assistant2` crate.
pub fn init(cx: &mut AppContext) {
pub fn init(fs: Arc<dyn Fs>, client: Arc<Client>, stdout_is_a_pty: bool, cx: &mut AppContext) {
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(),
client.telemetry().clone(),
cx,
);
terminal_inline_assistant::init(
fs.clone(),
prompt_builder.clone(),
client.telemetry().clone(),
cx,
);
feature_gate_assistant2_actions(cx);
}

View File

@@ -4,14 +4,13 @@ use anyhow::Result;
use assistant_tool::ToolWorkingSet;
use client::zed_urls;
use gpui::{
prelude::*, px, svg, Action, AnyElement, AppContext, AppContext, EventEmitter, FocusHandle,
FocusableView, FontWeight, Model, Pixels, Task, View, WeakView,
prelude::*, px, svg, Action, AnyElement, AppContext, AsyncWindowContext, EventEmitter,
FocusHandle, FocusableView, FontWeight, Model, Pixels, Task, View, ViewContext, WeakView,
WindowContext,
};
use language::LanguageRegistry;
use language_model::LanguageModelRegistry;
use language_model_selector::LanguageModelSelector;
use time::UtcOffset;
use ui::{prelude::*, ButtonLike, Divider, IconButtonShape, KeyBinding, Tab, Tooltip};
use ui::{prelude::*, Divider, IconButtonShape, KeyBinding, Tab, Tooltip};
use workspace::dock::{DockPosition, Panel, PanelEvent};
use workspace::Workspace;
@@ -20,12 +19,12 @@ use crate::message_editor::MessageEditor;
use crate::thread::{ThreadError, ThreadId};
use crate::thread_history::{PastThread, ThreadHistory};
use crate::thread_store::ThreadStore;
use crate::{NewThread, OpenHistory, ToggleFocus, ToggleModelSelector};
use crate::{NewThread, OpenHistory, ToggleFocus};
pub fn init(cx: &mut AppContext) {
cx.observe_new_views(
|workspace: &mut Workspace, model: &Model<Workspace>, _cx: &mut AppContext| {
workspace.register_action(model, |workspace, _: &ToggleFocus, cx| {
|workspace: &mut Workspace, _cx: &mut ViewContext<Workspace>| {
workspace.register_action(|workspace, _: &ToggleFocus, cx| {
workspace.toggle_panel_focus::<AssistantPanel>(cx);
});
},
@@ -39,23 +38,22 @@ enum ActiveView {
}
pub struct AssistantPanel {
workspace: WeakModel<Workspace>,
workspace: WeakView<Workspace>,
language_registry: Arc<LanguageRegistry>,
thread_store: Model<ThreadStore>,
thread: Model<ActiveThread>,
message_editor: Model<MessageEditor>,
thread: View<ActiveThread>,
message_editor: View<MessageEditor>,
tools: Arc<ToolWorkingSet>,
local_timezone: UtcOffset,
active_view: ActiveView,
history: Model<ThreadHistory>,
history: View<ThreadHistory>,
}
impl AssistantPanel {
pub fn load(
workspace: WeakModel<Workspace>,
window: AnyWindowHandle,
cx: AsyncAppContext,
) -> Task<Result<Model<Self>>> {
workspace: WeakView<Workspace>,
cx: AsyncWindowContext,
) -> Task<Result<View<Self>>> {
cx.spawn(|mut cx| async move {
let tools = Arc::new(ToolWorkingSet::default());
let thread_store = workspace
@@ -66,7 +64,7 @@ impl AssistantPanel {
.await?;
workspace.update(&mut cx, |workspace, cx| {
cx.new_model(|model, cx| Self::new(workspace, thread_store, tools, model, cx))
cx.new_view(|cx| Self::new(workspace, thread_store, tools, cx))
})
})
}
@@ -75,37 +73,34 @@ impl AssistantPanel {
workspace: &Workspace,
thread_store: Model<ThreadStore>,
tools: Arc<ToolWorkingSet>,
model: &Model<Self>,
cx: &mut AppContext,
cx: &mut ViewContext<Self>,
) -> Self {
let thread = thread_store.update(cx, |this, model, cx| this.create_thread(model, cx));
let thread = thread_store.update(cx, |this, cx| this.create_thread(cx));
let language_registry = workspace.project().read(cx).languages().clone();
let workspace = workspace.weak_handle();
let weak_self = model.downgrade();
let weak_self = cx.view().downgrade();
Self {
active_view: ActiveView::Thread,
workspace: workspace.clone(),
language_registry: language_registry.clone(),
thread_store: thread_store.clone(),
thread: cx.new_model(|model, cx| {
thread: cx.new_view(|cx| {
ActiveThread::new(
thread.clone(),
workspace,
workspace.clone(),
language_registry,
tools.clone(),
model,
cx,
)
}),
message_editor: cx.new_model(|model, cx| MessageEditor::new(thread.clone(), model, cx)),
message_editor: cx.new_view(|cx| MessageEditor::new(workspace, thread.clone(), cx)),
tools,
local_timezone: UtcOffset::from_whole_seconds(
chrono::Local::now().offset().local_minus_utc(),
)
.unwrap(),
history: cx
.new_model(|model, cx| ThreadHistory::new(weak_self, thread_store, model, cx)),
history: cx.new_view(|cx| ThreadHistory::new(weak_self, thread_store, cx)),
}
}
@@ -113,63 +108,52 @@ impl AssistantPanel {
self.local_timezone
}
fn new_thread(&mut self, model: &Model<Self>, cx: &mut AppContext) {
fn new_thread(&mut self, cx: &mut ViewContext<Self>) {
let thread = self
.thread_store
.update(cx, |this, model, cx| this.create_thread(model, cx));
.update(cx, |this, cx| this.create_thread(cx));
self.active_view = ActiveView::Thread;
self.thread = cx.new_model(|model, cx| {
self.thread = cx.new_view(|cx| {
ActiveThread::new(
thread.clone(),
self.workspace.clone(),
self.language_registry.clone(),
self.tools.clone(),
model,
cx,
)
});
self.message_editor = cx.new_model(|model, cx| MessageEditor::new(thread, model, cx));
self.message_editor.focus_handle(cx).focus(window);
self.message_editor =
cx.new_view(|cx| MessageEditor::new(self.workspace.clone(), thread, cx));
self.message_editor.focus_handle(cx).focus(cx);
}
pub(crate) fn open_thread(
&mut self,
thread_id: &ThreadId,
model: &Model<Self>,
cx: &mut AppContext,
) {
pub(crate) fn open_thread(&mut self, thread_id: &ThreadId, cx: &mut ViewContext<Self>) {
let Some(thread) = self
.thread_store
.update(cx, |this, model, cx| this.open_thread(thread_id, model, cx))
.update(cx, |this, cx| this.open_thread(thread_id, cx))
else {
return;
};
self.active_view = ActiveView::Thread;
self.thread = cx.new_model(|model, cx| {
self.thread = cx.new_view(|cx| {
ActiveThread::new(
thread.clone(),
self.workspace.clone(),
self.language_registry.clone(),
self.tools.clone(),
model,
cx,
)
});
self.message_editor = cx.new_model(|model, cx| MessageEditor::new(thread, model, cx));
self.message_editor.focus_handle(cx).focus(window);
self.message_editor =
cx.new_view(|cx| MessageEditor::new(self.workspace.clone(), thread, cx));
self.message_editor.focus_handle(cx).focus(cx);
}
pub(crate) fn delete_thread(
&mut self,
thread_id: &ThreadId,
model: &Model<Self>,
cx: &mut AppContext,
) {
self.thread_store.update(cx, |this, model, cx| {
this.delete_thread(thread_id, model, cx)
});
pub(crate) fn delete_thread(&mut self, thread_id: &ThreadId, cx: &mut ViewContext<Self>) {
self.thread_store
.update(cx, |this, cx| this.delete_thread(thread_id, cx));
}
}
@@ -189,7 +173,7 @@ impl Panel for AssistantPanel {
"AssistantPanel2"
}
fn position(&self, _window: &Window, cx: &AppContext) -> DockPosition {
fn position(&self, _cx: &WindowContext) -> DockPosition {
DockPosition::Right
}
@@ -197,26 +181,25 @@ impl Panel for AssistantPanel {
true
}
fn set_position(&mut self, _position: DockPosition, model: &Model<Self>, _cx: &mut AppContext) {
}
fn set_position(&mut self, _position: DockPosition, _cx: &mut ViewContext<Self>) {}
fn size(&self, _window: &Window, cx: &AppContext) -> Pixels {
fn size(&self, _cx: &WindowContext) -> Pixels {
px(640.)
}
fn set_size(&mut self, _size: Option<Pixels>, model: &Model<Self>, _cx: &mut AppContext) {}
fn set_size(&mut self, _size: Option<Pixels>, _cx: &mut ViewContext<Self>) {}
fn set_active(&mut self, _active: bool, model: &Model<Self>, _cx: &mut AppContext) {}
fn set_active(&mut self, _active: bool, _cx: &mut ViewContext<Self>) {}
fn remote_id() -> Option<proto::PanelId> {
Some(proto::PanelId::AssistantPanel)
}
fn icon(&self, _window: &Window, cx: &AppContext) -> Option<IconName> {
fn icon(&self, _cx: &WindowContext) -> Option<IconName> {
Some(IconName::ZedAssistant)
}
fn icon_tooltip(&self, _window: &Window, cx: &AppContext) -> Option<&'static str> {
fn icon_tooltip(&self, _cx: &WindowContext) -> Option<&'static str> {
Some("Assistant Panel")
}
@@ -226,7 +209,7 @@ impl Panel for AssistantPanel {
}
impl AssistantPanel {
fn render_toolbar(&self, model: &Model<Self>, cx: &mut AppContext) -> impl IntoElement {
fn render_toolbar(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let focus_handle = self.focus_handle(cx);
h_flex()
@@ -242,7 +225,6 @@ impl AssistantPanel {
.child(
h_flex()
.gap(DynamicSpacing::Base08.rems(cx))
.child(self.render_language_model_selector(model, cx))
.child(Divider::vertical())
.child(
IconButton::new("new-thread", IconName::Plus)
@@ -256,7 +238,6 @@ impl AssistantPanel {
"New Thread",
&NewThread,
&focus_handle,
window,
cx,
)
}
@@ -277,7 +258,6 @@ impl AssistantPanel {
"Open History",
&OpenHistory,
&focus_handle,
window,
cx,
)
}
@@ -291,7 +271,7 @@ impl AssistantPanel {
.shape(IconButtonShape::Square)
.icon_size(IconSize::Small)
.style(ButtonStyle::Subtle)
.tooltip(move |window, cx| Tooltip::text("Configure Assistant", cx))
.tooltip(move |cx| Tooltip::text("Configure Assistant", cx))
.on_click(move |_event, _cx| {
println!("Configure Assistant");
}),
@@ -299,83 +279,18 @@ impl AssistantPanel {
)
}
fn render_language_model_selector(
&self,
model: &Model<Self>,
cx: &mut AppContext,
) -> impl IntoElement {
let active_provider = LanguageModelRegistry::read_global(cx).active_provider();
let active_model = LanguageModelRegistry::read_global(cx).active_model();
LanguageModelSelector::new(
|model, _cx| {
println!("Selected {:?}", model.name());
},
ButtonLike::new("active-model")
.style(ButtonStyle::Subtle)
.child(
h_flex()
.w_full()
.gap_0p5()
.child(
div()
.overflow_x_hidden()
.flex_grow()
.whitespace_nowrap()
.child(match (active_provider, active_model) {
(Some(provider), Some(model)) => h_flex()
.gap_1()
.child(
Icon::new(
model.icon().unwrap_or_else(|| provider.icon()),
)
.color(Color::Muted)
.size(IconSize::XSmall),
)
.child(
Label::new(model.name().0)
.size(LabelSize::Small)
.color(Color::Muted),
)
.into_any_element(),
_ => Label::new("No model selected")
.size(LabelSize::Small)
.color(Color::Muted)
.into_any_element(),
}),
)
.child(
Icon::new(IconName::ChevronDown)
.color(Color::Muted)
.size(IconSize::XSmall),
),
)
.tooltip(move |window, cx| {
Tooltip::for_action("Change Model", &ToggleModelSelector, model, cx)
}),
)
}
fn render_active_thread_or_empty_state(
&self,
model: &Model<Self>,
cx: &mut AppContext,
) -> AnyElement {
fn render_active_thread_or_empty_state(&self, cx: &mut ViewContext<Self>) -> AnyElement {
if self.thread.read(cx).is_empty() {
return self.render_thread_empty_state(model, cx).into_any_element();
return self.render_thread_empty_state(cx).into_any_element();
}
self.thread.clone().into_any()
}
fn render_thread_empty_state(
&self,
model: &Model<Self>,
cx: &mut AppContext,
) -> impl IntoElement {
fn render_thread_empty_state(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let recent_threads = self
.thread_store
.update(cx, |this, model, cx| this.recent_threads(3, model, cx));
.update(cx, |this, cx| this.recent_threads(3, cx));
v_flex()
.gap_2()
@@ -391,46 +306,6 @@ impl AssistantPanel {
.mb_4(),
),
)
.child(v_flex())
.child(
h_flex()
.w_full()
.justify_center()
.child(Label::new("Context Examples:").size(LabelSize::Small)),
)
.child(
h_flex()
.gap_2()
.justify_center()
.child(
h_flex()
.gap_1()
.p_0p5()
.rounded_md()
.border_1()
.border_color(cx.theme().colors().border_variant)
.child(
Icon::new(IconName::Terminal)
.size(IconSize::Small)
.color(Color::Disabled),
)
.child(Label::new("Terminal").size(LabelSize::Small)),
)
.child(
h_flex()
.gap_1()
.p_0p5()
.rounded_md()
.border_1()
.border_color(cx.theme().colors().border_variant)
.child(
Icon::new(IconName::Folder)
.size(IconSize::Small)
.color(Color::Disabled),
)
.child(Label::new("/src/components").size(LabelSize::Small)),
),
)
.when(!recent_threads.is_empty(), |parent| {
parent
.child(
@@ -443,7 +318,7 @@ impl AssistantPanel {
v_flex().gap_2().children(
recent_threads
.into_iter()
.map(|thread| PastThread::new(thread, model.downgrade())),
.map(|thread| PastThread::new(thread, cx.view().downgrade())),
),
)
.child(
@@ -454,7 +329,6 @@ impl AssistantPanel {
.key_binding(KeyBinding::for_action_in(
&OpenHistory,
&self.focus_handle(cx),
model,
cx,
))
.on_click(move |_event, cx| {
@@ -465,7 +339,7 @@ impl AssistantPanel {
})
}
fn render_last_error(&self, model: &Model<Self>, cx: &mut AppContext) -> Option<AnyElement> {
fn render_last_error(&self, cx: &mut ViewContext<Self>) -> Option<AnyElement> {
let last_error = self.thread.read(cx).last_error()?;
Some(
@@ -479,23 +353,19 @@ impl AssistantPanel {
.elevation_2(cx)
.occlude()
.child(match last_error {
ThreadError::PaymentRequired => self.render_payment_required_error(model, cx),
ThreadError::PaymentRequired => self.render_payment_required_error(cx),
ThreadError::MaxMonthlySpendReached => {
self.render_max_monthly_spend_reached_error(model, cx)
self.render_max_monthly_spend_reached_error(cx)
}
ThreadError::Message(error_message) => {
self.render_error_message(&error_message, model, cx)
self.render_error_message(&error_message, cx)
}
})
.into_any(),
)
}
fn render_payment_required_error(
&self,
model: &Model<Self>,
cx: &mut AppContext,
) -> AnyElement {
fn render_payment_required_error(&self, cx: &mut ViewContext<Self>) -> AnyElement {
const ERROR_MESSAGE: &str = "Free tier exceeded. Subscribe and add payment to continue using Zed LLMs. You'll be billed at cost for tokens used.";
v_flex()
@@ -520,32 +390,28 @@ impl AssistantPanel {
.mt_1()
.child(Button::new("subscribe", "Subscribe").on_click(cx.listener(
|this, _, cx| {
this.thread.update(cx, |this, model, _cx| {
this.thread.update(cx, |this, _cx| {
this.clear_last_error();
});
cx.open_url(&zed_urls::account_url(cx));
model.notify(cx);
cx.notify();
},
)))
.child(Button::new("dismiss", "Dismiss").on_click(cx.listener(
|this, _, cx| {
this.thread.update(cx, |this, model, _cx| {
this.thread.update(cx, |this, _cx| {
this.clear_last_error();
});
model.notify(cx);
cx.notify();
},
))),
)
.into_any()
}
fn render_max_monthly_spend_reached_error(
&self,
model: &Model<Self>,
cx: &mut AppContext,
) -> AnyElement {
fn render_max_monthly_spend_reached_error(&self, cx: &mut ViewContext<Self>) -> AnyElement {
const ERROR_MESSAGE: &str = "You have reached your maximum monthly spend. Increase your spend limit to continue using Zed LLMs.";
v_flex()
@@ -570,23 +436,23 @@ impl AssistantPanel {
.mt_1()
.child(
Button::new("subscribe", "Update Monthly Spend Limit").on_click(
model.listener(|this, model, _, cx| {
this.thread.update(cx, |this, model, _cx| {
cx.listener(|this, _, cx| {
this.thread.update(cx, |this, _cx| {
this.clear_last_error();
});
cx.open_url(&zed_urls::account_url(cx));
model.notify(cx);
cx.notify();
}),
),
)
.child(Button::new("dismiss", "Dismiss").on_click(cx.listener(
|this, _, cx| {
this.thread.update(cx, |this, model, _cx| {
this.thread.update(cx, |this, _cx| {
this.clear_last_error();
});
model.notify(cx);
cx.notify();
},
))),
)
@@ -596,8 +462,7 @@ impl AssistantPanel {
fn render_error_message(
&self,
error_message: &SharedString,
model: &Model<Self>,
cx: &mut AppContext,
cx: &mut ViewContext<Self>,
) -> AnyElement {
v_flex()
.gap_0p5()
@@ -624,11 +489,11 @@ impl AssistantPanel {
.mt_1()
.child(Button::new("dismiss", "Dismiss").on_click(cx.listener(
|this, _, cx| {
this.thread.update(cx, |this, model, _cx| {
this.thread.update(cx, |this, _cx| {
this.clear_last_error();
});
model.notify(cx);
cx.notify();
},
))),
)
@@ -637,12 +502,7 @@ impl AssistantPanel {
}
impl Render for AssistantPanel {
fn render(
&mut self,
model: &Model<Self>,
window: &mut gpui::Window,
cx: &mut AppContext,
) -> impl IntoElement {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
v_flex()
.key_context("AssistantPanel2")
.justify_between()
@@ -652,20 +512,20 @@ impl Render for AssistantPanel {
}))
.on_action(cx.listener(|this, _: &OpenHistory, cx| {
this.active_view = ActiveView::History;
this.history.focus_handle(cx).focus(window);
model.notify(cx);
this.history.focus_handle(cx).focus(cx);
cx.notify();
}))
.child(self.render_toolbar(model, cx))
.child(self.render_toolbar(cx))
.map(|parent| match self.active_view {
ActiveView::Thread => parent
.child(self.render_active_thread_or_empty_state(model, cx))
.child(self.render_active_thread_or_empty_state(cx))
.child(
h_flex()
.border_t_1()
.border_color(cx.theme().colors().border_variant)
.child(self.message_editor.clone()),
)
.children(self.render_last_error(model, cx)),
.children(self.render_last_error(cx)),
ActiveView::History => parent.child(self.history.clone()),
})
}

View File

@@ -0,0 +1,485 @@
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_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

@@ -0,0 +1,27 @@
use gpui::SharedString;
use serde::{Deserialize, Serialize};
use util::post_inc;
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, Serialize, Deserialize)]
pub struct ContextId(pub(crate) usize);
impl ContextId {
pub fn post_inc(&mut self) -> Self {
Self(post_inc(&mut self.0))
}
}
/// Some context attached to a message in a thread.
#[derive(Debug, Clone)]
pub struct Context {
pub id: ContextId,
pub name: SharedString,
pub kind: ContextKind,
pub text: SharedString,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum ContextKind {
File,
FetchedUrl,
}

View File

@@ -1,15 +1,101 @@
mod fetch_context_picker;
mod file_context_picker;
use std::sync::Arc;
use gpui::{DismissEvent, SharedString, Task, WeakView};
use picker::{Picker, PickerDelegate, PickerEditorPosition};
use ui::{prelude::*, ListItem, ListItemSpacing, PopoverMenu, PopoverTrigger, Tooltip};
use gpui::{
AppContext, DismissEvent, EventEmitter, FocusHandle, FocusableView, SharedString, Task, View,
WeakView,
};
use picker::{Picker, PickerDelegate};
use ui::{prelude::*, ListItem, ListItemSpacing, Tooltip};
use util::ResultExt;
use workspace::Workspace;
use crate::context_picker::fetch_context_picker::FetchContextPicker;
use crate::context_picker::file_context_picker::FileContextPicker;
use crate::message_editor::MessageEditor;
#[derive(IntoElement)]
pub(super) struct ContextPicker<T: PopoverTrigger> {
message_editor: WeakModel<MessageEditor>,
trigger: T,
#[derive(Debug, Clone)]
enum ContextPickerMode {
Default,
File(View<FileContextPicker>),
Fetch(View<FetchContextPicker>),
}
pub(super) struct ContextPicker {
mode: ContextPickerMode,
picker: View<Picker<ContextPickerDelegate>>,
}
impl ContextPicker {
pub fn new(
workspace: WeakView<Workspace>,
message_editor: WeakView<MessageEditor>,
cx: &mut ViewContext<Self>,
) -> Self {
let delegate = ContextPickerDelegate {
context_picker: cx.view().downgrade(),
workspace: workspace.clone(),
message_editor: message_editor.clone(),
entries: vec![
ContextPickerEntry {
name: "directory".into(),
description: "Insert any directory".into(),
icon: IconName::Folder,
},
ContextPickerEntry {
name: "file".into(),
description: "Insert any file".into(),
icon: IconName::File,
},
ContextPickerEntry {
name: "fetch".into(),
description: "Fetch content from URL".into(),
icon: IconName::Globe,
},
],
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,
}
}
pub fn reset_mode(&mut self) {
self.mode = ContextPickerMode::Default;
}
}
impl EventEmitter<DismissEvent> for ContextPicker {}
impl FocusableView for ContextPicker {
fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
match &self.mode {
ContextPickerMode::Default => self.picker.focus_handle(cx),
ContextPickerMode::File(file_picker) => file_picker.focus_handle(cx),
ContextPickerMode::Fetch(fetch_picker) => fetch_picker.focus_handle(cx),
}
}
}
impl Render for ContextPicker {
fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
v_flex()
.w(px(400.))
.min_w(px(400.))
.map(|parent| match &self.mode {
ContextPickerMode::Default => parent.child(self.picker.clone()),
ContextPickerMode::File(file_picker) => parent.child(file_picker.clone()),
ContextPickerMode::Fetch(fetch_picker) => parent.child(fetch_picker.clone()),
})
}
}
#[derive(Clone)]
@@ -20,114 +106,96 @@ struct ContextPickerEntry {
}
pub(crate) struct ContextPickerDelegate {
all_entries: Vec<ContextPickerEntry>,
filtered_entries: Vec<ContextPickerEntry>,
message_editor: WeakModel<MessageEditor>,
context_picker: WeakView<ContextPicker>,
workspace: WeakView<Workspace>,
message_editor: WeakView<MessageEditor>,
entries: Vec<ContextPickerEntry>,
selected_ix: usize,
}
impl<T: PopoverTrigger> ContextPicker<T> {
pub(crate) fn new(message_editor: WeakModel<MessageEditor>, trigger: T) -> Self {
ContextPicker {
message_editor,
trigger,
}
}
}
impl PickerDelegate for ContextPickerDelegate {
type ListItem = ListItem;
fn match_count(&self) -> usize {
self.filtered_entries.len()
self.entries.len()
}
fn selected_index(&self) -> usize {
self.selected_ix
}
fn set_selected_index(&mut self, ix: usize, model: &Model<Picker>, cx: &mut AppContext) {
self.selected_ix = ix.min(self.filtered_entries.len().saturating_sub(1));
model.notify(cx);
fn 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, _window: &mut gpui::Window, _cx: &mut gpui::AppContext) -> Arc<str> {
fn placeholder_text(&self, _cx: &mut WindowContext) -> Arc<str> {
"Select a context source…".into()
}
fn update_matches(
&mut self,
query: String,
model: &Model<Picker>,
cx: &mut AppContext,
) -> Task<()> {
let all_commands = self.all_entries.clone();
model.spawn(cx, |this, mut cx| async move {
let filtered_commands = cx
.background_executor()
.spawn(async move {
if query.is_empty() {
all_commands
} else {
all_commands
.into_iter()
.filter(|model_info| {
model_info
.name
.to_lowercase()
.contains(&query.to_lowercase())
})
.collect()
}
})
.await;
this.update(&mut cx, |this, model, cx| {
this.delegate.filtered_entries = filtered_commands;
this.delegate.set_selected_index(0, cx);
model.notify(cx);
})
.ok();
})
fn update_matches(&mut self, _query: String, _cx: &mut ViewContext<Picker<Self>>) -> Task<()> {
Task::ready(())
}
fn confirm(&mut self, _secondary: bool, model: &Model<Picker>, cx: &mut AppContext) {
if let Some(entry) = self.filtered_entries.get(self.selected_ix) {
self.message_editor
.update(cx, |_message_editor, model, _cx| {
println!("Insert context from {}", entry.name);
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.name.to_string().as_str() {
"file" => {
this.mode = ContextPickerMode::File(cx.new_view(|cx| {
FileContextPicker::new(
self.context_picker.clone(),
self.workspace.clone(),
self.message_editor.clone(),
cx,
)
}));
}
"fetch" => {
this.mode = ContextPickerMode::Fetch(cx.new_view(|cx| {
FetchContextPicker::new(
self.context_picker.clone(),
self.workspace.clone(),
self.message_editor.clone(),
cx,
)
}));
}
_ => {}
}
cx.focus_self();
})
.ok();
model.emit(DismissEvent, cx);
.log_err();
}
}
fn dismissed(&mut self, model: &Model<Picker>, _cx: &mut AppContext) {}
fn editor_position(&self) -> PickerEditorPosition {
PickerEditorPosition::End
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::Fetch(_) => {}
})
.log_err();
}
fn render_match(
&self,
ix: usize,
selected: bool,
model: &Model<Picker>,
_cx: &mut AppContext,
_cx: &mut ViewContext<Picker<Self>>,
) -> Option<Self::ListItem> {
let entry = self.filtered_entries.get(ix)?;
let entry = &self.entries[ix];
Some(
ListItem::new(ix)
.inset(true)
.spacing(ListItemSpacing::Dense)
.selected(selected)
.toggle_state(selected)
.tooltip({
let description = entry.description.clone();
move |cx| {
cx.new_model(|_model, _cx| Tooltip::new(description.clone()))
.into()
}
move |cx| cx.new_view(|_cx| Tooltip::new(description.clone())).into()
})
.child(
v_flex()
@@ -157,51 +225,3 @@ impl PickerDelegate for ContextPickerDelegate {
)
}
}
impl<T: PopoverTrigger> RenderOnce for ContextPicker<T> {
fn render(self, _window: &mut gpui::Window, cx: &mut gpui::AppContext) -> impl IntoElement {
let entries = vec![
ContextPickerEntry {
name: "directory".into(),
description: "Insert any directory".into(),
icon: IconName::Folder,
},
ContextPickerEntry {
name: "file".into(),
description: "Insert any file".into(),
icon: IconName::File,
},
ContextPickerEntry {
name: "web".into(),
description: "Fetch content from URL".into(),
icon: IconName::Globe,
},
];
let delegate = ContextPickerDelegate {
all_entries: entries.clone(),
message_editor: self.message_editor.clone(),
filtered_entries: entries,
selected_ix: 0,
};
let picker = cx.new_model(|model, cx| {
Picker::uniform_list(delegate, model, cx).max_height(Some(rems(20.).into()))
});
let handle = self
.message_editor
.update(cx, |this, model, _| this.context_picker_handle.clone())
.ok();
PopoverMenu::new("context-picker")
.menu(move |_cx| Some(picker.clone()))
.trigger(self.trigger)
.attach(gpui::AnchorCorner::TopLeft)
.anchor(gpui::AnchorCorner::BottomLeft)
.offset(gpui::Point {
x: px(0.0),
y: px(-16.0),
})
.when_some(handle, |this, handle| this.with_handle(handle))
}
}

View File

@@ -0,0 +1,218 @@
use std::cell::RefCell;
use std::rc::Rc;
use std::sync::Arc;
use anyhow::{bail, Context as _, Result};
use futures::AsyncReadExt as _;
use gpui::{AppContext, DismissEvent, FocusHandle, FocusableView, Task, View, WeakView};
use html_to_markdown::{convert_html_to_markdown, markdown, TagHandler};
use http_client::{AsyncBody, HttpClientWithUrl};
use picker::{Picker, PickerDelegate};
use ui::{prelude::*, ListItem, ListItemSpacing, ViewContext};
use workspace::Workspace;
use crate::context::ContextKind;
use crate::context_picker::ContextPicker;
use crate::message_editor::MessageEditor;
pub struct FetchContextPicker {
picker: View<Picker<FetchContextPickerDelegate>>,
}
impl FetchContextPicker {
pub fn new(
context_picker: WeakView<ContextPicker>,
workspace: WeakView<Workspace>,
message_editor: WeakView<MessageEditor>,
cx: &mut ViewContext<Self>,
) -> Self {
let delegate = FetchContextPickerDelegate::new(context_picker, workspace, message_editor);
let picker = cx.new_view(|cx| Picker::uniform_list(delegate, cx));
Self { picker }
}
}
impl FocusableView for FetchContextPicker {
fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
self.picker.focus_handle(cx)
}
}
impl Render for FetchContextPicker {
fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
self.picker.clone()
}
}
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]
enum ContentType {
Html,
Plaintext,
Json,
}
pub struct FetchContextPickerDelegate {
context_picker: WeakView<ContextPicker>,
workspace: WeakView<Workspace>,
message_editor: WeakView<MessageEditor>,
url: String,
}
impl FetchContextPickerDelegate {
pub fn new(
context_picker: WeakView<ContextPicker>,
workspace: WeakView<Workspace>,
message_editor: WeakView<MessageEditor>,
) -> Self {
FetchContextPickerDelegate {
context_picker,
workspace,
message_editor,
url: String::new(),
}
}
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}");
}
let mut response = http_client.get(&url, AsyncBody::default(), true).await?;
let mut body = Vec::new();
response
.body_mut()
.read_to_end(&mut body)
.await
.context("error reading response body")?;
if response.status().is_client_error() {
let text = String::from_utf8_lossy(body.as_slice());
bail!(
"status error {}, response: {text:?}",
response.status().as_u16()
);
}
let Some(content_type) = response.headers().get("content-type") else {
bail!("missing Content-Type header");
};
let content_type = content_type
.to_str()
.context("invalid Content-Type header")?;
let content_type = match content_type {
"text/html" => ContentType::Html,
"text/plain" => ContentType::Plaintext,
"application/json" => ContentType::Json,
_ => ContentType::Html,
};
match content_type {
ContentType::Html => {
let mut handlers: Vec<TagHandler> = vec![
Rc::new(RefCell::new(markdown::WebpageChromeRemover)),
Rc::new(RefCell::new(markdown::ParagraphHandler)),
Rc::new(RefCell::new(markdown::HeadingHandler)),
Rc::new(RefCell::new(markdown::ListHandler)),
Rc::new(RefCell::new(markdown::TableHandler::new())),
Rc::new(RefCell::new(markdown::StyledTextHandler)),
];
if url.contains("wikipedia.org") {
use html_to_markdown::structure::wikipedia;
handlers.push(Rc::new(RefCell::new(wikipedia::WikipediaChromeRemover)));
handlers.push(Rc::new(RefCell::new(wikipedia::WikipediaInfoboxHandler)));
handlers.push(Rc::new(
RefCell::new(wikipedia::WikipediaCodeHandler::new()),
));
} else {
handlers.push(Rc::new(RefCell::new(markdown::CodeHandler)));
}
convert_html_to_markdown(&body[..], &mut handlers)
}
ContentType::Plaintext => Ok(std::str::from_utf8(&body)?.to_owned()),
ContentType::Json => {
let json: serde_json::Value = serde_json::from_slice(&body)?;
Ok(format!(
"```json\n{}\n```",
serde_json::to_string_pretty(&json)?
))
}
}
}
}
impl PickerDelegate for FetchContextPickerDelegate {
type ListItem = ListItem;
fn match_count(&self) -> usize {
1
}
fn selected_index(&self) -> usize {
0
}
fn set_selected_index(&mut self, _ix: usize, _cx: &mut ViewContext<Picker<Self>>) {}
fn placeholder_text(&self, _cx: &mut ui::WindowContext) -> Arc<str> {
"Enter a URL…".into()
}
fn update_matches(&mut self, query: String, _cx: &mut ViewContext<Picker<Self>>) -> Task<()> {
self.url = query;
Task::ready(())
}
fn confirm(&mut self, _secondary: bool, cx: &mut ViewContext<Picker<Self>>) {
let Some(workspace) = self.workspace.upgrade() else {
return;
};
let http_client = workspace.read(cx).client().http_client().clone();
let url = self.url.clone();
cx.spawn(|this, mut cx| async move {
let text = Self::build_message(http_client, &url).await?;
this.update(&mut cx, |this, cx| {
this.delegate
.message_editor
.update(cx, |message_editor, _cx| {
message_editor.insert_context(ContextKind::FetchedUrl, url, text);
})
})??;
anyhow::Ok(())
})
.detach_and_log_err(cx);
}
fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>) {
self.context_picker
.update(cx, |this, cx| {
this.reset_mode();
cx.emit(DismissEvent);
})
.ok();
}
fn render_match(
&self,
ix: usize,
selected: bool,
_cx: &mut ViewContext<Picker<Self>>,
) -> Option<Self::ListItem> {
Some(
ListItem::new(ix)
.inset(true)
.spacing(ListItemSpacing::Sparse)
.toggle_state(selected)
.child(self.url.clone()),
)
}
}

View File

@@ -0,0 +1,289 @@
use std::fmt::Write as _;
use std::ops::RangeInclusive;
use std::path::{Path, PathBuf};
use std::sync::atomic::AtomicBool;
use std::sync::Arc;
use fuzzy::PathMatch;
use gpui::{AppContext, DismissEvent, FocusHandle, FocusableView, Task, View, WeakView};
use picker::{Picker, PickerDelegate};
use project::{PathMatchCandidateSet, WorktreeId};
use ui::{prelude::*, ListItem, ListItemSpacing};
use util::ResultExt as _;
use workspace::Workspace;
use crate::context::ContextKind;
use crate::context_picker::ContextPicker;
use crate::message_editor::MessageEditor;
pub struct FileContextPicker {
picker: View<Picker<FileContextPickerDelegate>>,
}
impl FileContextPicker {
pub fn new(
context_picker: WeakView<ContextPicker>,
workspace: WeakView<Workspace>,
message_editor: WeakView<MessageEditor>,
cx: &mut ViewContext<Self>,
) -> Self {
let delegate = FileContextPickerDelegate::new(context_picker, workspace, message_editor);
let picker = cx.new_view(|cx| Picker::uniform_list(delegate, cx));
Self { picker }
}
}
impl FocusableView for FileContextPicker {
fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
self.picker.focus_handle(cx)
}
}
impl Render for FileContextPicker {
fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
self.picker.clone()
}
}
pub struct FileContextPickerDelegate {
context_picker: WeakView<ContextPicker>,
workspace: WeakView<Workspace>,
message_editor: WeakView<MessageEditor>,
matches: Vec<PathMatch>,
selected_index: usize,
}
impl FileContextPickerDelegate {
pub fn new(
context_picker: WeakView<ContextPicker>,
workspace: WeakView<Workspace>,
message_editor: WeakView<MessageEditor>,
) -> Self {
Self {
context_picker,
workspace,
message_editor,
matches: Vec::new(),
selected_index: 0,
}
}
fn search(
&mut self,
query: String,
cancellation_flag: Arc<AtomicBool>,
workspace: &View<Workspace>,
cx: &mut ViewContext<Picker<Self>>,
) -> Task<Vec<PathMatch>> {
if query.is_empty() {
let workspace = workspace.read(cx);
let project = workspace.project().read(cx);
let entries = workspace.recent_navigation_history(Some(10), cx);
let entries = entries
.into_iter()
.map(|entries| entries.0)
.chain(project.worktrees(cx).flat_map(|worktree| {
let worktree = worktree.read(cx);
let id = worktree.id();
worktree
.child_entries(Path::new(""))
.filter(|entry| entry.kind.is_file())
.map(move |entry| project::ProjectPath {
worktree_id: id,
path: entry.path.clone(),
})
}))
.collect::<Vec<_>>();
let path_prefix: Arc<str> = Arc::default();
Task::ready(
entries
.into_iter()
.filter_map(|entry| {
let worktree = project.worktree_for_id(entry.worktree_id, cx)?;
let mut full_path = PathBuf::from(worktree.read(cx).root_name());
full_path.push(&entry.path);
Some(PathMatch {
score: 0.,
positions: Vec::new(),
worktree_id: entry.worktree_id.to_usize(),
path: full_path.into(),
path_prefix: path_prefix.clone(),
distance_to_relative_ancestor: 0,
is_dir: false,
})
})
.collect(),
)
} else {
let worktrees = workspace.read(cx).visible_worktrees(cx).collect::<Vec<_>>();
let candidate_sets = worktrees
.into_iter()
.map(|worktree| {
let worktree = worktree.read(cx);
PathMatchCandidateSet {
snapshot: worktree.snapshot(),
include_ignored: worktree
.root_entry()
.map_or(false, |entry| entry.is_ignored),
include_root_name: true,
candidates: project::Candidates::Files,
}
})
.collect::<Vec<_>>();
let executor = cx.background_executor().clone();
cx.foreground_executor().spawn(async move {
fuzzy::match_path_sets(
candidate_sets.as_slice(),
query.as_str(),
None,
false,
100,
&cancellation_flag,
executor,
)
.await
})
}
}
}
impl PickerDelegate for FileContextPickerDelegate {
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, _cx: &mut ViewContext<Picker<Self>>) {
self.selected_index = ix;
}
fn placeholder_text(&self, _cx: &mut WindowContext) -> Arc<str> {
"Search files…".into()
}
fn update_matches(&mut self, query: String, cx: &mut ViewContext<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 {
// TODO: This should be probably be run in the background.
let paths = search_task.await;
this.update(&mut cx, |this, _cx| {
this.delegate.matches = paths;
})
.log_err();
})
}
fn confirm(&mut self, _secondary: bool, cx: &mut ViewContext<Picker<Self>>) {
let mat = &self.matches[self.selected_index];
let workspace = self.workspace.clone();
let Some(project) = workspace
.upgrade()
.map(|workspace| workspace.read(cx).project().clone())
else {
return;
};
let path = mat.path.clone();
let worktree_id = WorktreeId::from_usize(mat.worktree_id);
cx.spawn(|this, mut cx| async move {
let Some(open_buffer_task) = project
.update(&mut cx, |project, cx| {
project.open_buffer((worktree_id, path.clone()), cx)
})
.ok()
else {
return anyhow::Ok(());
};
let buffer = open_buffer_task.await?;
this.update(&mut cx, |this, cx| {
this.delegate
.message_editor
.update(cx, |message_editor, cx| {
let mut text = String::new();
text.push_str(&codeblock_fence_for_path(Some(&path), None));
text.push_str(&buffer.read(cx).text());
if !text.ends_with('\n') {
text.push('\n');
}
text.push_str("```\n");
message_editor.insert_context(
ContextKind::File,
path.to_string_lossy().to_string(),
text,
);
})
})??;
anyhow::Ok(())
})
.detach_and_log_err(cx);
}
fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>) {
self.context_picker
.update(cx, |this, cx| {
this.reset_mode();
cx.emit(DismissEvent);
})
.ok();
}
fn render_match(
&self,
ix: usize,
selected: bool,
_cx: &mut ViewContext<Picker<Self>>,
) -> Option<Self::ListItem> {
let mat = &self.matches[ix];
Some(
ListItem::new(ix)
.inset(true)
.spacing(ListItemSpacing::Sparse)
.toggle_state(selected)
.child(mat.path.to_string_lossy().to_string()),
)
}
}
fn codeblock_fence_for_path(path: Option<&Path>, row_range: Option<RangeInclusive<u32>>) -> String {
let mut text = String::new();
write!(text, "```").unwrap();
if let Some(path) = path {
if let Some(extension) = path.extension().and_then(|ext| ext.to_str()) {
write!(text, "{} ", extension).unwrap();
}
write!(text, "{}", path.display()).unwrap();
} else {
write!(text, "untitled").unwrap();
}
if let Some(row_range) = row_range {
write!(text, ":{}-{}", row_range.start() + 1, row_range.end() + 1).unwrap();
}
text.push('\n');
text
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,70 +1,109 @@
use std::rc::Rc;
use editor::{Editor, EditorElement, EditorStyle};
use gpui::{AppContext, FocusableView, Model, TextStyle, View};
use gpui::{AppContext, FocusableView, Model, TextStyle, View, WeakView};
use language_model::{LanguageModelRegistry, LanguageModelRequestTool};
use picker::Picker;
use language_model_selector::{LanguageModelSelector, LanguageModelSelectorPopoverMenu};
use settings::Settings;
use theme::ThemeSettings;
use ui::{
prelude::*, ButtonLike, CheckboxWithLabel, ElevationIndex, IconButtonShape, KeyBinding,
PopoverMenuHandle,
PopoverMenu, PopoverMenuHandle, Tooltip,
};
use workspace::Workspace;
use crate::context_picker::{ContextPicker, ContextPickerDelegate};
use crate::context::{Context, ContextId, ContextKind};
use crate::context_picker::ContextPicker;
use crate::thread::{RequestKind, Thread};
use crate::Chat;
use crate::ui::ContextPill;
use crate::{Chat, ToggleModelSelector};
pub struct MessageEditor {
thread: Model<Thread>,
editor: Model<Editor>,
pub(crate) context_picker_handle: PopoverMenuHandle<Picker<ContextPickerDelegate>>,
editor: View<Editor>,
context: Vec<Context>,
next_context_id: ContextId,
context_picker: View<ContextPicker>,
pub(crate) context_picker_handle: PopoverMenuHandle<ContextPicker>,
language_model_selector: View<LanguageModelSelector>,
use_tools: bool,
}
impl MessageEditor {
pub fn new(thread: Model<Thread>, model: &Model<Self>, cx: &mut AppContext) -> Self {
pub fn new(
workspace: WeakView<Workspace>,
thread: Model<Thread>,
cx: &mut ViewContext<Self>,
) -> Self {
let weak_self = cx.view().downgrade();
Self {
thread,
editor: cx.new_model(|model, cx| {
let mut editor = Editor::auto_height(80, model, cx);
editor.set_placeholder_text("Ask anything", model, cx);
editor: cx.new_view(|cx| {
let mut editor = Editor::auto_height(80, cx);
editor.set_placeholder_text("Ask anything or type @ to add context", cx);
editor
}),
context: Vec::new(),
next_context_id: ContextId(0),
context_picker: cx.new_view(|cx| ContextPicker::new(workspace.clone(), weak_self, cx)),
context_picker_handle: PopoverMenuHandle::default(),
language_model_selector: cx.new_view(|cx| {
LanguageModelSelector::new(
|model, _cx| {
println!("Selected {:?}", model.name());
},
cx,
)
}),
use_tools: false,
}
}
fn chat(&mut self, _: &Chat, model: &Model<Self>, cx: &mut AppContext) {
self.send_to_model(RequestKind::Chat, model, cx);
pub fn insert_context(
&mut self,
kind: ContextKind,
name: impl Into<SharedString>,
text: impl Into<SharedString>,
) {
self.context.push(Context {
id: self.next_context_id.post_inc(),
name: name.into(),
kind,
text: text.into(),
});
}
fn chat(&mut self, _: &Chat, cx: &mut ViewContext<Self>) {
self.send_to_model(RequestKind::Chat, cx);
}
fn send_to_model(
&mut self,
request_kind: RequestKind,
model: &Model<Self>,
cx: &mut AppContext,
cx: &mut ViewContext<Self>,
) -> Option<()> {
let provider = LanguageModelRegistry::read_global(cx).active_provider();
if provider
.as_ref()
.map_or(false, |provider| provider.must_accept_terms(cx))
{
model.notify(cx);
cx.notify();
return None;
}
let model_registry = LanguageModelRegistry::read_global(cx);
let model = model_registry.active_model()?;
let user_message = self.editor.update(cx, |editor, model, cx| {
let user_message = self.editor.update(cx, |editor, cx| {
let text = editor.text(cx);
editor.clear(cx);
text
});
let context = self.context.drain(..).collect::<Vec<_>>();
self.thread.update(cx, |thread, model, cx| {
thread.insert_user_message(user_message, cx);
self.thread.update(cx, |thread, cx| {
thread.insert_user_message(user_message, context, cx);
let mut request = thread.to_completion_request(request_kind, cx);
if self.use_tools {
@@ -85,6 +124,55 @@ impl MessageEditor {
None
}
fn render_language_model_selector(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let active_provider = LanguageModelRegistry::read_global(cx).active_provider();
let active_model = LanguageModelRegistry::read_global(cx).active_model();
LanguageModelSelectorPopoverMenu::new(
self.language_model_selector.clone(),
ButtonLike::new("active-model")
.style(ButtonStyle::Subtle)
.child(
h_flex()
.w_full()
.gap_0p5()
.child(
div()
.overflow_x_hidden()
.flex_grow()
.whitespace_nowrap()
.child(match (active_provider, active_model) {
(Some(provider), Some(model)) => h_flex()
.gap_1()
.child(
Icon::new(
model.icon().unwrap_or_else(|| provider.icon()),
)
.color(Color::Muted)
.size(IconSize::XSmall),
)
.child(
Label::new(model.name().0)
.size(LabelSize::Small)
.color(Color::Muted),
)
.into_any_element(),
_ => Label::new("No model selected")
.size(LabelSize::Small)
.color(Color::Muted)
.into_any_element(),
}),
)
.child(
Icon::new(IconName::ChevronDown)
.color(Color::Muted)
.size(IconSize::XSmall),
),
)
.tooltip(move |cx| Tooltip::for_action("Change Model", &ToggleModelSelector, cx)),
)
}
}
impl FocusableView for MessageEditor {
@@ -94,15 +182,11 @@ impl FocusableView for MessageEditor {
}
impl Render for MessageEditor {
fn render(
&mut self,
model: &Model<Self>,
window: &mut gpui::Window,
cx: &mut AppContext,
) -> impl IntoElement {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let font_size = TextSize::Default.rems(cx);
let line_height = font_size.to_pixels(cx.rem_size()) * 1.3;
let focus_handle = self.editor.focus_handle(cx);
let context_picker = self.context_picker.clone();
v_flex()
.key_context("MessageEditor")
@@ -112,12 +196,46 @@ impl Render for MessageEditor {
.p_2()
.bg(cx.theme().colors().editor_background)
.child(
h_flex().gap_2().child(ContextPicker::new(
model.downgrade(),
IconButton::new("add-context", IconName::Plus)
.shape(IconButtonShape::Square)
.icon_size(IconSize::Small),
)),
h_flex()
.flex_wrap()
.gap_2()
.child(
PopoverMenu::new("context-picker")
.menu(move |_cx| Some(context_picker.clone()))
.trigger(
IconButton::new("add-context", IconName::Plus)
.shape(IconButtonShape::Square)
.icon_size(IconSize::Small),
)
.attach(gpui::AnchorCorner::TopLeft)
.anchor(gpui::AnchorCorner::BottomLeft)
.offset(gpui::Point {
x: px(0.0),
y: px(-16.0),
})
.with_handle(self.context_picker_handle.clone()),
)
.children(self.context.iter().map(|context| {
ContextPill::new(context.clone()).on_remove({
let context = context.clone();
Rc::new(cx.listener(move |this, _event, cx| {
this.context.retain(|other| other.id != context.id);
cx.notify();
}))
})
}))
.when(!self.context.is_empty(), |parent| {
parent.child(
IconButton::new("remove-all-context", IconName::Eraser)
.shape(IconButtonShape::Square)
.icon_size(IconSize::Small)
.tooltip(move |cx| Tooltip::text("Remove All Context", cx))
.on_click(cx.listener(|this, _event, cx| {
this.context.clear();
cx.notify();
})),
)
}),
)
.child({
let settings = ThemeSettings::get_global(cx);
@@ -150,27 +268,26 @@ impl Render for MessageEditor {
self.use_tools.into(),
cx.listener(|this, selection, _cx| {
this.use_tools = match selection {
Selection::Selected => true,
Selection::Unselected | Selection::Indeterminate => false,
ToggleState::Selected => true,
ToggleState::Unselected | ToggleState::Indeterminate => false,
};
}),
)))
.child(
h_flex()
.gap_2()
.child(Button::new("codebase", "Codebase").style(ButtonStyle::Filled))
.child(Label::new("or"))
.child(self.render_language_model_selector(cx))
.child(
ButtonLike::new("chat")
.style(ButtonStyle::Filled)
.layer(ElevationIndex::ModalSurface)
.child(Label::new("Chat"))
.child(Label::new("Submit"))
.children(
KeyBinding::for_action_in(&Chat, &focus_handle, model, cx)
KeyBinding::for_action_in(&Chat, &focus_handle, cx)
.map(|binding| binding.into_any_element()),
)
.on_click(move |_event, cx| {
focus_handle.dispatch_action(&Chat, model, cx);
focus_handle.dispatch_action(&Chat, cx);
}),
),
),

View File

@@ -0,0 +1,312 @@
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

File diff suppressed because it is too large Load Diff

View File

@@ -6,7 +6,7 @@ use chrono::{DateTime, Utc};
use collections::HashMap;
use futures::future::Shared;
use futures::{FutureExt as _, StreamExt as _};
use gpui::{AppContext, EventEmitter, SharedString, Task};
use gpui::{AppContext, EventEmitter, ModelContext, SharedString, Task};
use language_model::{
LanguageModel, LanguageModelCompletionEvent, LanguageModelRegistry, LanguageModelRequest,
LanguageModelRequestMessage, LanguageModelToolResult, LanguageModelToolUse,
@@ -17,6 +17,8 @@ use serde::{Deserialize, Serialize};
use util::{post_inc, TryFutureExt as _};
use uuid::Uuid;
use crate::context::{Context, ContextKind};
#[derive(Debug, Clone, Copy)]
pub enum RequestKind {
Chat,
@@ -62,6 +64,7 @@ pub struct Thread {
pending_summary: Task<Option<()>>,
messages: Vec<Message>,
next_message_id: MessageId,
context_by_message: HashMap<MessageId, Vec<Context>>,
completion_count: usize,
pending_completions: Vec<PendingCompletion>,
tools: Arc<ToolWorkingSet>,
@@ -71,7 +74,7 @@ pub struct Thread {
}
impl Thread {
pub fn new(tools: Arc<ToolWorkingSet>, model: &Model<Self>, _cx: &mut AppContext) -> Self {
pub fn new(tools: Arc<ToolWorkingSet>, _cx: &mut ModelContext<Self>) -> Self {
Self {
id: ThreadId::new(),
updated_at: Utc::now(),
@@ -79,6 +82,7 @@ impl Thread {
pending_summary: Task::ready(None),
messages: Vec::new(),
next_message_id: MessageId(0),
context_by_message: HashMap::default(),
completion_count: 0,
pending_completions: Vec::new(),
tools,
@@ -108,14 +112,9 @@ impl Thread {
self.summary.clone()
}
pub fn set_summary(
&mut self,
summary: impl Into<SharedString>,
model: &Model<Self>,
cx: &mut AppContext,
) {
pub fn set_summary(&mut self, summary: impl Into<SharedString>, cx: &mut ModelContext<Self>) {
self.summary = Some(summary.into());
model.emit(ThreadEvent::SummaryChanged, cx);
cx.emit(ThreadEvent::SummaryChanged);
}
pub fn message(&self, id: MessageId) -> Option<&Message> {
@@ -130,6 +129,10 @@ impl Thread {
&self.tools
}
pub fn context_for_message(&self, id: MessageId) -> Option<&Vec<Context>> {
self.context_by_message.get(&id)
}
pub fn pending_tool_uses(&self) -> Vec<&PendingToolUse> {
self.pending_tool_uses_by_id.values().collect()
}
@@ -137,19 +140,19 @@ impl Thread {
pub fn insert_user_message(
&mut self,
text: impl Into<String>,
model: &Model<Self>,
cx: &mut AppContext,
context: Vec<Context>,
cx: &mut ModelContext<Self>,
) {
self.insert_message(Role::User, text, model, cx)
let message_id = self.insert_message(Role::User, text, cx);
self.context_by_message.insert(message_id, context);
}
pub fn insert_message(
&mut self,
role: Role,
text: impl Into<String>,
model: &Model<Self>,
cx: &mut AppContext,
) {
cx: &mut ModelContext<Self>,
) -> MessageId {
let id = self.next_message_id.post_inc();
self.messages.push(Message {
id,
@@ -157,7 +160,8 @@ impl Thread {
text: text.into(),
});
self.touch_updated_at();
model.emit(ThreadEvent::MessageAdded(id), cx);
cx.emit(ThreadEvent::MessageAdded(id));
id
}
pub fn to_completion_request(
@@ -187,6 +191,41 @@ impl Thread {
}
}
if let Some(context) = self.context_for_message(message.id) {
let mut file_context = String::new();
let mut fetch_context = String::new();
for context in context.iter() {
match context.kind {
ContextKind::File => {
file_context.push_str(&context.text);
file_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');
}
}
}
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 !fetch_context.is_empty() {
context_text.push_str("The following fetched results are available\n");
context_text.push_str(&fetch_context);
}
request_message
.content
.push(MessageContent::Text(context_text))
}
if !message.text.is_empty() {
request_message
.content
@@ -211,8 +250,7 @@ impl Thread {
&mut self,
request: LanguageModelRequest,
model: Arc<dyn LanguageModel>,
model: &Model<Self>,
cx: &mut AppContext,
cx: &mut ModelContext<Self>,
) {
let pending_completion_id = post_inc(&mut self.completion_count);
@@ -237,13 +275,10 @@ impl Thread {
if let Some(last_message) = thread.messages.last_mut() {
if last_message.role == Role::Assistant {
last_message.text.push_str(&chunk);
model.emit(
cx,
ThreadEvent::StreamedAssistantText(
last_message.id,
chunk,
),
);
cx.emit(ThreadEvent::StreamedAssistantText(
last_message.id,
chunk,
));
}
}
}
@@ -274,8 +309,8 @@ impl Thread {
}
thread.touch_updated_at();
model.emit(ThreadEvent::StreamedCompletion, cx);
model.notify(cx);
cx.emit(ThreadEvent::StreamedCompletion);
cx.notify();
})?;
smol::future::yield_now().await;
@@ -300,31 +335,25 @@ impl Thread {
.update(&mut cx, |_thread, cx| match result.as_ref() {
Ok(stop_reason) => match stop_reason {
StopReason::ToolUse => {
model.emit(ThreadEvent::UsePendingTools, cx);
cx.emit(ThreadEvent::UsePendingTools);
}
StopReason::EndTurn => {}
StopReason::MaxTokens => {}
},
Err(error) => {
if error.is::<PaymentRequiredError>() {
model.emit(ThreadEvent::ShowError(ThreadError::PaymentRequired), cx);
cx.emit(ThreadEvent::ShowError(ThreadError::PaymentRequired));
} else if error.is::<MaxMonthlySpendReachedError>() {
model.emit(
cx,
ThreadEvent::ShowError(ThreadError::MaxMonthlySpendReached),
);
cx.emit(ThreadEvent::ShowError(ThreadError::MaxMonthlySpendReached));
} else {
let error_message = error
.chain()
.map(|err| err.to_string())
.collect::<Vec<_>>()
.join("\n");
model.emit(
cx,
ThreadEvent::ShowError(ThreadError::Message(SharedString::from(
error_message.clone(),
))),
);
cx.emit(ThreadEvent::ShowError(ThreadError::Message(
SharedString::from(error_message.clone()),
)));
}
}
})
@@ -337,7 +366,7 @@ impl Thread {
});
}
pub fn summarize(&mut self, model: &Model<Self>, cx: &mut AppContext) {
pub fn summarize(&mut self, cx: &mut ModelContext<Self>) {
let Some(provider) = LanguageModelRegistry::read_global(cx).active_provider() else {
return;
};
@@ -359,7 +388,7 @@ impl Thread {
cache: false,
});
self.pending_summary = model.spawn(cx, |this, mut cx| {
self.pending_summary = cx.spawn(|this, mut cx| {
async move {
let stream = model.stream_completion_text(request, &cx);
let mut messages = stream.await?;
@@ -376,12 +405,12 @@ impl Thread {
}
}
this.update(&mut cx, |this, model, cx| {
this.update(&mut cx, |this, cx| {
if !new_summary.is_empty() {
this.summary = Some(new_summary.into());
}
model.emit(ThreadEvent::SummaryChanged, cx);
cx.emit(ThreadEvent::SummaryChanged);
})?;
anyhow::Ok(())
@@ -395,8 +424,7 @@ impl Thread {
assistant_message_id: MessageId,
tool_use_id: LanguageModelToolUseId,
output: Task<Result<String>>,
model: &Model<Self>,
cx: &mut AppContext,
cx: &mut ModelContext<Self>,
) {
let insert_output_task = cx.spawn(|thread, mut cx| {
let tool_use_id = tool_use_id.clone();
@@ -422,7 +450,7 @@ impl Thread {
is_error: false,
});
model.emit(ThreadEvent::ToolFinished { tool_use_id }, cx);
cx.emit(ThreadEvent::ToolFinished { tool_use_id });
}
Err(err) => {
tool_results.push(LanguageModelToolResult {

View File

@@ -10,20 +10,19 @@ use crate::AssistantPanel;
pub struct ThreadHistory {
focus_handle: FocusHandle,
assistant_panel: WeakModel<AssistantPanel>,
assistant_panel: WeakView<AssistantPanel>,
thread_store: Model<ThreadStore>,
scroll_handle: UniformListScrollHandle,
}
impl ThreadHistory {
pub(crate) fn new(
assistant_panel: WeakModel<AssistantPanel>,
assistant_panel: WeakView<AssistantPanel>,
thread_store: Model<ThreadStore>,
model: &Model<Self>,
cx: &mut AppContext,
cx: &mut ViewContext<Self>,
) -> Self {
Self {
focus_handle: window.focus_handle(),
focus_handle: cx.focus_handle(),
assistant_panel,
thread_store,
scroll_handle: UniformListScrollHandle::default(),
@@ -38,15 +37,8 @@ impl FocusableView for ThreadHistory {
}
impl Render for ThreadHistory {
fn render(
&mut self,
model: &Model<Self>,
window: &mut gpui::Window,
cx: &mut AppContext,
) -> impl IntoElement {
let threads = self
.thread_store
.update(cx, |this, model, cx| this.threads(cx));
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let threads = self.thread_store.update(cx, |this, cx| this.threads(cx));
v_flex()
.id("thread-history-container")
@@ -93,11 +85,11 @@ impl Render for ThreadHistory {
#[derive(IntoElement)]
pub struct PastThread {
thread: Model<Thread>,
assistant_panel: WeakModel<AssistantPanel>,
assistant_panel: WeakView<AssistantPanel>,
}
impl PastThread {
pub fn new(thread: Model<Thread>, assistant_panel: WeakModel<AssistantPanel>) -> Self {
pub fn new(thread: Model<Thread>, assistant_panel: WeakView<AssistantPanel>) -> Self {
Self {
thread,
assistant_panel,
@@ -106,7 +98,7 @@ impl PastThread {
}
impl RenderOnce for PastThread {
fn render(self, _window: &mut gpui::Window, cx: &mut gpui::AppContext) -> impl IntoElement {
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);
@@ -121,7 +113,7 @@ impl RenderOnce for PastThread {
.unwrap(),
OffsetDateTime::now_utc(),
self.assistant_panel
.update(cx, |this, model, _cx| this.local_timezone())
.update(cx, |this, _cx| this.local_timezone())
.unwrap_or(UtcOffset::UTC),
time_format::TimestampFormat::EnhancedAbsolute,
);
@@ -141,7 +133,7 @@ impl RenderOnce for PastThread {
let id = id.clone();
move |_event, cx| {
assistant_panel
.update(cx, |this, model, cx| {
.update(cx, |this, cx| {
this.delete_thread(&id, cx);
})
.ok();
@@ -154,7 +146,7 @@ impl RenderOnce for PastThread {
let id = id.clone();
move |_event, cx| {
assistant_panel
.update(cx, |this, model, cx| {
.update(cx, |this, cx| {
this.open_thread(&id, cx);
})
.ok();

View File

@@ -5,7 +5,7 @@ use assistant_tool::{ToolId, ToolWorkingSet};
use collections::HashMap;
use context_server::manager::ContextServerManager;
use context_server::{ContextServerFactoryRegistry, ContextServerTool};
use gpui::{prelude::*, AppContext, Model, Task};
use gpui::{prelude::*, AppContext, Model, ModelContext, Task};
use project::Project;
use unindent::Unindent;
use util::ResultExt as _;
@@ -28,16 +28,11 @@ impl ThreadStore {
cx: &mut AppContext,
) -> Task<Result<Model<Self>>> {
cx.spawn(|mut cx| async move {
let this = cx.new_model(|model: &Model<Self>, cx: &mut AppContext| {
let this = cx.new_model(|cx: &mut ModelContext<Self>| {
let context_server_factory_registry =
ContextServerFactoryRegistry::default_global(cx);
let context_server_manager = cx.new_model(|model, cx| {
ContextServerManager::new(
context_server_factory_registry,
project.clone(),
model,
cx,
)
let context_server_manager = cx.new_model(|cx| {
ContextServerManager::new(context_server_factory_registry, project.clone(), cx)
});
let mut this = Self {
@@ -57,7 +52,7 @@ impl ThreadStore {
})
}
pub fn threads(&self, model: &Model<Self>, cx: &AppContext) -> Vec<Model<Thread>> {
pub fn threads(&self, cx: &ModelContext<Self>) -> Vec<Model<Thread>> {
let mut threads = self
.threads
.iter()
@@ -68,38 +63,28 @@ impl ThreadStore {
threads
}
pub fn recent_threads(
&self,
limit: usize,
model: &Model<Self>,
cx: &AppContext,
) -> Vec<Model<Thread>> {
pub fn recent_threads(&self, limit: usize, cx: &ModelContext<Self>) -> Vec<Model<Thread>> {
self.threads(cx).into_iter().take(limit).collect()
}
pub fn create_thread(&mut self, model: &Model<Self>, cx: &mut AppContext) -> Model<Thread> {
let thread = cx.new_model(|model, cx| Thread::new(self.tools.clone(), model, cx));
pub fn create_thread(&mut self, cx: &mut ModelContext<Self>) -> Model<Thread> {
let thread = cx.new_model(|cx| Thread::new(self.tools.clone(), cx));
self.threads.push(thread.clone());
thread
}
pub fn open_thread(
&self,
id: &ThreadId,
model: &Model<Self>,
cx: &mut AppContext,
) -> Option<Model<Thread>> {
pub fn open_thread(&self, id: &ThreadId, cx: &mut ModelContext<Self>) -> Option<Model<Thread>> {
self.threads
.iter()
.find(|thread| thread.read(cx).id() == id)
.cloned()
}
pub fn delete_thread(&mut self, id: &ThreadId, model: &Model<Self>, cx: &mut AppContext) {
pub fn delete_thread(&mut self, id: &ThreadId, cx: &mut ModelContext<Self>) {
self.threads.retain(|thread| thread.read(cx).id() != id);
}
fn register_context_server_handlers(&self, model: &Model<Self>, cx: &mut AppContext) {
fn register_context_server_handlers(&self, cx: &mut ModelContext<Self>) {
cx.subscribe(
&self.context_server_manager.clone(),
Self::handle_context_server_event,
@@ -111,8 +96,7 @@ impl ThreadStore {
&mut self,
context_server_manager: Model<ContextServerManager>,
event: &context_server::manager::Event,
model: &Model<Self>,
cx: &mut AppContext,
cx: &mut ModelContext<Self>,
) {
let tool_working_set = self.tools.clone();
match event {
@@ -169,23 +153,23 @@ impl ThreadStore {
impl ThreadStore {
/// Creates some mocked recent threads for testing purposes.
fn mock_recent_threads(&mut self, model: &Model<Self>, cx: &mut AppContext) {
fn mock_recent_threads(&mut self, cx: &mut ModelContext<Self>) {
use language_model::Role;
self.threads.push(cx.new_model(|model, cx| {
let mut thread = Thread::new(self.tools.clone(), model, cx);
thread.set_summary("Introduction to quantum computing", model, cx);
thread.insert_user_message("Hello! Can you help me understand quantum computing?", model, cx);
thread.insert_message(Role::Assistant, "Of course! I'd be happy to help you understand quantum computing. Quantum computing is a fascinating field that uses the principles of quantum mechanics to process information. Unlike classical computers that use bits (0s and 1s), quantum computers use quantum bits or 'qubits'. These qubits can exist in multiple states simultaneously, a property called superposition. This allows quantum computers to perform certain calculations much faster than classical computers. What specific aspect of quantum computing would you like to know more about?", model, cx);
thread.insert_user_message("That's interesting! Can you explain how quantum entanglement is used in quantum computing?", model, cx);
thread.insert_message(Role::Assistant, "Certainly! Quantum entanglement is a key principle used in quantum computing. When two qubits become entangled, the state of one qubit is directly related to the state of the other, regardless of the distance between them. This property is used in quantum computing to create complex quantum states and to perform operations on multiple qubits simultaneously. Entanglement allows quantum computers to process information in ways that classical computers cannot, potentially solving certain problems much more efficiently. For example, it's crucial in quantum error correction and in algorithms like quantum teleportation, which is important for quantum communication.", model, cx);
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
}));
self.threads.push(cx.new_model(|model, cx| {
let mut thread = Thread::new(self.tools.clone(), model, cx);
thread.set_summary("Rust web development and async programming", model, cx);
thread.insert_user_message("Can you show me an example of Rust code for a simple web server?", model, cx);
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:
```rust
@@ -221,8 +205,8 @@ impl ThreadStore {
actix-web = \"4.0\"
```
Then you can run the server with `cargo run` and access it at `http://localhost:8080`.".unindent(), model, cx);
thread.insert_user_message("That's great! Can you explain more about async functions in Rust?", model, cx);
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:
@@ -251,7 +235,7 @@ impl ThreadStore {
6. **Error Handling**: Async functions work well with Rust's `?` operator for error handling.
Async programming in Rust provides a powerful way to write concurrent code that's both safe and efficient. It's particularly useful for servers, network programming, and any application that deals with many concurrent operations.".unindent(), model, cx);
Async programming in Rust provides a powerful way to write concurrent code that's both safe and efficient. It's particularly useful for servers, network programming, and any application that deals with many concurrent operations.".unindent(), cx);
thread
}));
}

View File

@@ -0,0 +1,3 @@
mod context_pill;
pub use context_pill::*;

View File

@@ -0,0 +1,49 @@
use std::rc::Rc;
use gpui::ClickEvent;
use ui::{prelude::*, IconButtonShape};
use crate::context::Context;
#[derive(IntoElement)]
pub struct ContextPill {
context: Context,
on_remove: Option<Rc<dyn Fn(&ClickEvent, &mut WindowContext)>>,
}
impl ContextPill {
pub fn new(context: Context) -> Self {
Self {
context,
on_remove: None,
}
}
pub fn on_remove(mut self, on_remove: Rc<dyn Fn(&ClickEvent, &mut WindowContext)>) -> Self {
self.on_remove = Some(on_remove);
self
}
}
impl RenderOnce for ContextPill {
fn render(self, cx: &mut WindowContext) -> impl IntoElement {
h_flex()
.gap_1()
.px_1()
.border_1()
.border_color(cx.theme().colors().border)
.rounded_md()
.child(Label::new(self.context.name.clone()).size(LabelSize::Small))
.when_some(self.on_remove, |parent, on_remove| {
parent.child(
IconButton::new("remove", IconName::Close)
.shape(IconButtonShape::Square)
.icon_size(IconSize::XSmall)
.on_click({
let on_remove = on_remove.clone();
move |event, cx| on_remove(event, cx)
}),
)
})
}
}

View File

@@ -6,7 +6,7 @@ pub use crate::slash_command_registry::*;
use anyhow::Result;
use futures::stream::{self, BoxStream};
use futures::StreamExt;
use gpui::{AnyElement, AppContext, ElementId, SharedString, Task, WeakView};
use gpui::{AnyElement, AppContext, ElementId, SharedString, Task, WeakView, WindowContext};
use language::{BufferSnapshot, CodeLabel, LspAdapterDelegate, OffsetRangeExt};
pub use language_model::Role;
use serde::{Deserialize, Serialize};
@@ -78,9 +78,8 @@ pub trait SlashCommand: 'static + Send + Sync {
self: Arc<Self>,
arguments: &[String],
cancel: Arc<AtomicBool>,
workspace: Option<WeakModel<Workspace>>,
window: &mut gpui::Window,
cx: &mut gpui::AppContext,
workspace: Option<WeakView<Workspace>>,
cx: &mut WindowContext,
) -> Task<Result<Vec<ArgumentCompletion>>>;
fn requires_argument(&self) -> bool;
fn accepts_arguments(&self) -> bool {
@@ -91,27 +90,21 @@ pub trait SlashCommand: 'static + Send + Sync {
arguments: &[String],
context_slash_command_output_sections: &[SlashCommandOutputSection<language::Anchor>],
context_buffer: BufferSnapshot,
workspace: WeakModel<Workspace>,
workspace: WeakView<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>>,
window: &mut gpui::Window,
cx: &mut gpui::AppContext,
cx: &mut WindowContext,
) -> Task<SlashCommandResult>;
}
pub type RenderFoldPlaceholder = Arc<
dyn Send
+ Sync
+ Fn(
ElementId,
Arc<dyn Fn(&mut gpui::Window, &mut gpui::AppContext)>,
&mut gpui::Window,
&mut gpui::AppContext,
) -> AnyElement,
+ Fn(ElementId, Arc<dyn Fn(&mut WindowContext)>, &mut WindowContext) -> AnyElement,
>;
#[derive(Debug, PartialEq)]

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -18,6 +18,7 @@ test-support = [
"collections/test-support",
"gpui/test-support",
"livekit_client/test-support",
"livekit_client_macos/test-support",
"project/test-support",
"util/test-support"
]

View File

@@ -20,7 +20,7 @@ pub struct CallSettingsContent {
/// Whether your current project should be shared when joining an empty channel.
///
/// Default: true
/// Default: false
pub share_on_join: Option<bool>,
}

View File

@@ -8,8 +8,8 @@ use client::{proto, ChannelId, Client, TypedEnvelope, User, UserStore, ZED_ALWAY
use collections::HashSet;
use futures::{channel::oneshot, future::Shared, Future, FutureExt};
use gpui::{
AppContext, AsyncAppContext, Context, EventEmitter, Global, Model, Subscription, Task,
WeakModel,
AppContext, AsyncAppContext, Context, EventEmitter, Global, Model, ModelContext, Subscription,
Task, WeakModel,
};
use postage::watch;
use project::Project;
@@ -34,7 +34,7 @@ pub fn init(client: Arc<Client>, user_store: Model<UserStore>, cx: &mut AppConte
);
CallSettings::register(cx);
let active_call = cx.new_model(|model, cx| ActiveCall::new(client, user_store, model, cx));
let active_call = cx.new_model(|cx| ActiveCall::new(client, user_store, cx));
cx.set_global(GlobalActiveCall(active_call));
}
@@ -96,12 +96,7 @@ pub struct ActiveCall {
impl EventEmitter<Event> for ActiveCall {}
impl ActiveCall {
fn new(
client: Arc<Client>,
user_store: Model<UserStore>,
model: &Model<Self>,
_: &mut AppContext,
) -> Self {
fn new(client: Arc<Client>, user_store: Model<UserStore>, cx: &mut ModelContext<Self>) -> Self {
Self {
room: None,
pending_room_creation: None,
@@ -110,8 +105,8 @@ impl ActiveCall {
incoming_call: watch::channel(),
_join_debouncer: OneAtATime { cancel: None },
_subscriptions: vec![
client.add_request_handler(model.downgrade(), Self::handle_incoming_call),
client.add_message_handler(model.downgrade(), Self::handle_call_canceled),
client.add_request_handler(cx.weak_model(), Self::handle_incoming_call),
client.add_message_handler(cx.weak_model(), Self::handle_call_canceled),
],
client,
user_store,
@@ -127,22 +122,22 @@ impl ActiveCall {
envelope: TypedEnvelope<proto::IncomingCall>,
mut cx: AsyncAppContext,
) -> Result<proto::Ack> {
let user_store = this.update(&mut cx, |this, _, _| this.user_store.clone())?;
let user_store = this.update(&mut cx, |this, _| this.user_store.clone())?;
let call = IncomingCall {
room_id: envelope.payload.room_id,
participants: user_store
.update(&mut cx, |user_store, model, cx| {
user_store.get_users(envelope.payload.participant_user_ids, model, cx)
.update(&mut cx, |user_store, cx| {
user_store.get_users(envelope.payload.participant_user_ids, cx)
})?
.await?,
calling_user: user_store
.update(&mut cx, |user_store, model, cx| {
user_store.get_user(envelope.payload.calling_user_id, model, cx)
.update(&mut cx, |user_store, cx| {
user_store.get_user(envelope.payload.calling_user_id, cx)
})?
.await?,
initial_project: envelope.payload.initial_project,
};
this.update(&mut cx, |this, _, _| {
this.update(&mut cx, |this, _| {
*this.incoming_call.0.borrow_mut() = Some(call);
})?;
@@ -154,7 +149,7 @@ impl ActiveCall {
envelope: TypedEnvelope<proto::CallCanceled>,
mut cx: AsyncAppContext,
) -> Result<()> {
this.update(&mut cx, |this, _, _| {
this.update(&mut cx, |this, _| {
let mut incoming_call = this.incoming_call.0.borrow_mut();
if incoming_call
.as_ref()
@@ -179,13 +174,12 @@ impl ActiveCall {
&mut self,
called_user_id: u64,
initial_project: Option<Model<Project>>,
model: &Model<Self>,
cx: &mut AppContext,
cx: &mut ModelContext<Self>,
) -> Task<Result<()>> {
if !self.pending_invites.insert(called_user_id) {
return Task::ready(Err(anyhow!("user was already invited")));
}
model.notify(cx);
cx.notify();
if self._join_debouncer.running() {
return Task::ready(Ok(()));
@@ -198,22 +192,20 @@ impl ActiveCall {
};
let invite = if let Some(room) = room {
cx.spawn(move |mut cx| async move {
cx.spawn(move |_, mut cx| async move {
let room = room.await.map_err(|err| anyhow!("{:?}", err))?;
let initial_project_id = if let Some(initial_project) = initial_project {
Some(
room.update(&mut cx, |room, model, cx| {
room.share_project(initial_project, model, cx)
})?
.await?,
room.update(&mut cx, |room, cx| room.share_project(initial_project, cx))?
.await?,
)
} else {
None
};
room.update(&mut cx, move |room, model, cx| {
room.call(called_user_id, initial_project_id, model, cx)
room.update(&mut cx, move |room, cx| {
room.call(called_user_id, initial_project_id, cx)
})?
.await?;
@@ -222,8 +214,8 @@ impl ActiveCall {
} else {
let client = self.client.clone();
let user_store = self.user_store.clone();
let room = model
.spawn(cx, move |this, mut cx| async move {
let room = cx
.spawn(move |this, mut cx| async move {
let create_room = async {
let room = cx
.update(|cx| {
@@ -237,16 +229,14 @@ impl ActiveCall {
})?
.await?;
this.update(&mut cx, |this, model, cx| {
this.set_room(Some(room.clone()), model, cx)
})?
.await?;
this.update(&mut cx, |this, cx| this.set_room(Some(room.clone()), cx))?
.await?;
anyhow::Ok(room)
};
let room = create_room.await;
this.update(&mut cx, |this, _, _| this.pending_room_creation = None)?;
this.update(&mut cx, |this, _| this.pending_room_creation = None)?;
room.map_err(Arc::new)
})
.shared();
@@ -257,20 +247,18 @@ impl ActiveCall {
})
};
model.spawn(cx, move |this, mut cx| async move {
cx.spawn(move |this, mut cx| async move {
let result = invite.await;
if result.is_ok() {
this.update(&mut cx, |this, _model, cx| {
this.report_call_event("invite", cx)
})?;
this.update(&mut cx, |this, cx| this.report_call_event("invite", cx))?;
} else {
//TODO: report collaboration error
log::error!("invite failed: {:?}", result);
}
this.update(&mut cx, |this, model, cx| {
this.update(&mut cx, |this, cx| {
this.pending_invites.remove(&called_user_id);
model.notify(cx);
cx.notify();
})?;
result
})
@@ -279,8 +267,7 @@ impl ActiveCall {
pub fn cancel_invite(
&mut self,
called_user_id: u64,
_: &Model<Self>,
cx: &mut AppContext,
cx: &mut ModelContext<Self>,
) -> Task<Result<()>> {
let room_id = if let Some(room) = self.room() {
room.read(cx).id()
@@ -304,11 +291,7 @@ impl ActiveCall {
self.incoming_call.1.clone()
}
pub fn accept_incoming(
&mut self,
model: &Model<Self>,
cx: &mut AppContext,
) -> Task<Result<()>> {
pub fn accept_incoming(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
if self.room.is_some() {
return Task::ready(Err(anyhow!("cannot join while on another call")));
}
@@ -330,20 +313,18 @@ impl ActiveCall {
._join_debouncer
.spawn(cx, move |cx| Room::join(room_id, client, user_store, cx));
model.spawn(cx, |this, mut cx| async move {
cx.spawn(|this, mut cx| async move {
let room = join.await?;
this.update(&mut cx, |this, model, cx| {
this.set_room(room.clone(), model, cx)
})?
.await?;
this.update(&mut cx, |this, model, cx| {
this.update(&mut cx, |this, cx| this.set_room(room.clone(), cx))?
.await?;
this.update(&mut cx, |this, cx| {
this.report_call_event("accept incoming", cx)
})?;
Ok(())
})
}
pub fn decline_incoming(&mut self, _: &Model<Self>, _: &mut AppContext) -> Result<()> {
pub fn decline_incoming(&mut self, _: &mut ModelContext<Self>) -> Result<()> {
let call = self
.incoming_call
.0
@@ -360,14 +341,13 @@ impl ActiveCall {
pub fn join_channel(
&mut self,
channel_id: ChannelId,
model: &Model<Self>,
cx: &mut AppContext,
cx: &mut ModelContext<Self>,
) -> Task<Result<Option<Model<Room>>>> {
if let Some(room) = self.room().cloned() {
if room.read(cx).channel_id() == Some(channel_id) {
return Task::ready(Ok(Some(room)));
} else {
room.update(cx, |room, model, cx| room.clear_state(cx));
room.update(cx, |room, cx| room.clear_state(cx));
}
}
@@ -381,29 +361,27 @@ impl ActiveCall {
Room::join_channel(channel_id, client, user_store, cx).await
});
model.spawn(cx, |this, mut cx| async move {
cx.spawn(|this, mut cx| async move {
let room = join.await?;
this.update(&mut cx, |this, model, cx| {
this.set_room(room.clone(), model, cx)
})?
.await?;
this.update(&mut cx, |this, model, cx| {
this.update(&mut cx, |this, cx| this.set_room(room.clone(), cx))?
.await?;
this.update(&mut cx, |this, cx| {
this.report_call_event("join channel", cx)
})?;
Ok(room)
})
}
pub fn hang_up(&mut self, model: &Model<Self>, cx: &mut AppContext) -> Task<Result<()>> {
model.notify(cx);
pub fn hang_up(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
cx.notify();
self.report_call_event("hang up", cx);
Audio::end_call(cx);
let channel_id = self.channel_id(cx);
if let Some((room, _)) = self.room.take() {
model.emit(Event::RoomLeft { channel_id }, cx);
room.update(cx, |room, model, cx| room.leave(model, cx))
cx.emit(Event::RoomLeft { channel_id });
room.update(cx, |room, cx| room.leave(cx))
} else {
Task::ready(Ok(()))
}
@@ -412,12 +390,11 @@ impl ActiveCall {
pub fn share_project(
&mut self,
project: Model<Project>,
model: &Model<Self>,
cx: &mut AppContext,
cx: &mut ModelContext<Self>,
) -> Task<Result<u64>> {
if let Some((room, _)) = self.room.as_ref() {
self.report_call_event("share project", cx);
room.update(cx, |room, model, cx| room.share_project(project, model, cx))
room.update(cx, |room, cx| room.share_project(project, cx))
} else {
Task::ready(Err(anyhow!("no active call")))
}
@@ -426,14 +403,11 @@ impl ActiveCall {
pub fn unshare_project(
&mut self,
project: Model<Project>,
model: &Model<Self>,
cx: &mut AppContext,
cx: &mut ModelContext<Self>,
) -> Result<()> {
if let Some((room, _)) = self.room.as_ref() {
self.report_call_event("unshare project", cx);
room.update(cx, |room, model, cx| {
room.unshare_project(project, model, cx)
})
room.update(cx, |room, cx| room.unshare_project(project, cx))
} else {
Err(anyhow!("no active call"))
}
@@ -446,13 +420,12 @@ impl ActiveCall {
pub fn set_location(
&mut self,
project: Option<&Model<Project>>,
model: &Model<Self>,
cx: &mut AppContext,
cx: &mut ModelContext<Self>,
) -> Task<Result<()>> {
if project.is_some() || !*ZED_ALWAYS_ACTIVE {
self.location = project.map(|project| project.downgrade());
if let Some((room, _)) = self.room.as_ref() {
return room.update(cx, |room, model, cx| room.set_location(project, model, cx));
return room.update(cx, |room, cx| room.set_location(project, cx));
}
}
Task::ready(Ok(()))
@@ -461,29 +434,26 @@ impl ActiveCall {
fn set_room(
&mut self,
room: Option<Model<Room>>,
model: &Model<Self>,
cx: &mut AppContext,
cx: &mut ModelContext<Self>,
) -> Task<Result<()>> {
if room.as_ref() == self.room.as_ref().map(|room| &room.0) {
Task::ready(Ok(()))
} else {
model.notify(cx);
cx.notify();
if let Some(room) = room {
if room.read(cx).status().is_offline() {
self.room = None;
Task::ready(Ok(()))
} else {
let subscriptions = vec![
model.observe(&room, cx, |this, room, model, cx| {
cx.observe(&room, |this, room, cx| {
if room.read(cx).status().is_offline() {
this.set_room(None, model, cx).detach_and_log_err(cx)
this.set_room(None, cx).detach_and_log_err(cx);
}
model.notify(cx);
}),
model.subscribe(&room, cx, |_, _, event, model, cx| {
model.emit(event.clone(), cx)
cx.notify();
}),
cx.subscribe(&room, |_, _, event, cx| cx.emit(event.clone())),
];
self.room = Some((room.clone(), subscriptions));
let location = self
@@ -491,10 +461,8 @@ impl ActiveCall {
.as_ref()
.and_then(|location| location.upgrade());
let channel_id = room.read(cx).channel_id();
model.emit(Event::RoomJoined { channel_id }, cx);
room.update(cx, |room, model, cx| {
room.set_location(location.as_ref(), model, cx)
})
cx.emit(Event::RoomJoined { channel_id });
room.update(cx, |room, cx| room.set_location(location.as_ref(), cx))
}
} else {
self.room = None;

View File

@@ -13,7 +13,9 @@ use client::{
use collections::{BTreeMap, HashMap, HashSet};
use fs::Fs;
use futures::{FutureExt, StreamExt};
use gpui::{AppContext, AsyncAppContext, Context, EventEmitter, Model, Task, WeakModel};
use gpui::{
AppContext, AsyncAppContext, Context, EventEmitter, Model, ModelContext, Task, WeakModel,
};
use language::LanguageRegistry;
#[cfg(not(target_os = "windows"))]
use livekit::{
@@ -119,12 +121,11 @@ impl Room {
livekit_connection_info: Option<proto::LiveKitConnectionInfo>,
client: Arc<Client>,
user_store: Model<UserStore>,
model: &Model<Self>,
cx: &mut AppContext,
cx: &mut ModelContext<Self>,
) -> Self {
spawn_room_connection(livekit_connection_info, model, cx);
spawn_room_connection(livekit_connection_info, cx);
let maintain_connection = model.spawn(cx, {
let maintain_connection = cx.spawn({
let client = client.clone();
move |this, cx| Self::maintain_connection(this, client.clone(), cx).log_err()
});
@@ -146,11 +147,11 @@ impl Room {
pending_participants: Default::default(),
pending_call_count: 0,
client_subscriptions: vec![
client.add_message_handler(model.downgrade(), Self::handle_room_updated)
client.add_message_handler(cx.weak_model(), Self::handle_room_updated)
],
_subscriptions: vec![
model.on_release(cx, Self::released),
model.on_app_quit(cx, Self::app_will_quit),
cx.on_release(Self::released),
cx.on_app_quit(Self::app_will_quit),
],
leave_when_empty: false,
pending_room_update: None,
@@ -173,14 +174,13 @@ impl Room {
cx.spawn(move |mut cx| async move {
let response = client.request(proto::CreateRoom {}).await?;
let room_proto = response.room.ok_or_else(|| anyhow!("invalid room"))?;
let room = cx.new_model(|model, cx| {
let room = cx.new_model(|cx| {
let mut room = Self::new(
room_proto.id,
None,
response.live_kit_connection_info,
client,
user_store,
model,
cx,
);
if let Some(participant) = room_proto.participants.first() {
@@ -191,8 +191,8 @@ impl Room {
let initial_project_id = if let Some(initial_project) = initial_project {
let initial_project_id = room
.update(&mut cx, |room, model, cx| {
room.share_project(initial_project.clone(), model, cx)
.update(&mut cx, |room, cx| {
room.share_project(initial_project.clone(), cx)
})?
.await?;
Some(initial_project_id)
@@ -201,9 +201,9 @@ impl Room {
};
let did_join = room
.update(&mut cx, |room, model, cx| {
.update(&mut cx, |room, cx| {
room.leave_when_empty = true;
room.call(called_user_id, initial_project_id, model, cx)
room.call(called_user_id, initial_project_id, cx)
})?
.await;
match did_join {
@@ -251,11 +251,7 @@ impl Room {
}
}
fn app_will_quit(
&mut self,
model: &Model<Self>,
cx: &mut AppContext,
) -> impl Future<Output = ()> {
fn app_will_quit(&mut self, cx: &mut ModelContext<Self>) -> impl Future<Output = ()> {
let task = if self.status.is_online() {
let leave = self.leave_internal(cx);
Some(cx.background_executor().spawn(async move {
@@ -283,20 +279,19 @@ impl Room {
mut cx: AsyncAppContext,
) -> Result<Model<Self>> {
let room_proto = response.room.ok_or_else(|| anyhow!("invalid room"))?;
let room = cx.new_model(|model, cx| {
let room = cx.new_model(|cx| {
Self::new(
room_proto.id,
response.channel_id.map(ChannelId),
response.live_kit_connection_info,
client,
user_store,
model,
cx,
)
})?;
room.update(&mut cx, |room, model, cx| {
room.update(&mut cx, |room, cx| {
room.leave_when_empty = room.channel_id.is_none();
room.apply_room_update(room_proto, model, cx)?;
room.apply_room_update(room_proto, cx)?;
anyhow::Ok(())
})??;
Ok(room)
@@ -310,8 +305,8 @@ impl Room {
&& self.pending_call_count == 0
}
pub(crate) fn leave(&mut self, model: &Model<Self>, cx: &mut AppContext) -> Task<Result<()>> {
model.notify(cx);
pub(crate) fn leave(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
cx.notify();
self.leave_internal(cx)
}
@@ -335,16 +330,16 @@ impl Room {
pub(crate) fn clear_state(&mut self, cx: &mut AppContext) {
for project in self.shared_projects.drain() {
if let Some(project) = project.upgrade() {
project.update(cx, |project, model, cx| {
project.unshare(model, cx).log_err();
project.update(cx, |project, cx| {
project.unshare(cx).log_err();
});
}
}
for project in self.joined_projects.drain() {
if let Some(project) = project.upgrade() {
project.update(cx, |project, model, cx| {
project.disconnected_from_host(model, cx);
project.close(model, cx);
project.update(cx, |project, cx| {
project.disconnected_from_host(cx);
project.close(cx);
});
}
}
@@ -374,9 +369,9 @@ impl Room {
this.upgrade()
.ok_or_else(|| anyhow!("room was dropped"))?
.update(&mut cx, |this, model, cx| {
.update(&mut cx, |this, cx| {
this.status = RoomStatus::Rejoining;
model.notify(cx);
cx.notify();
})?;
// Wait for client to re-establish a connection to the server.
@@ -390,8 +385,7 @@ impl Room {
log::info!("client reconnected, attempting to rejoin room");
let Some(this) = this.upgrade() else { break };
match this.update(&mut cx, |this, model, cx| this.rejoin(model, cx))
{
match this.update(&mut cx, |this, cx| this.rejoin(cx)) {
Ok(task) => {
if task.await.log_err().is_some() {
return true;
@@ -440,15 +434,14 @@ impl Room {
// we leave the room and return an error.
if let Some(this) = this.upgrade() {
log::info!("reconnection failed, leaving room");
this.update(&mut cx, |this, model, cx| this.leave(model, cx))?
.await?;
this.update(&mut cx, |this, cx| this.leave(cx))?.await?;
}
Err(anyhow!(
"can't reconnect to room: client failed to re-establish connection"
))
}
fn rejoin(&mut self, model: &Model<Self>, cx: &mut AppContext) -> Task<Result<()>> {
fn rejoin(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
let mut projects = HashMap::default();
let mut reshared_projects = Vec::new();
let mut rejoined_projects = Vec::new();
@@ -496,29 +489,27 @@ impl Room {
rejoined_projects,
});
model.spawn(cx, |this, mut cx| async move {
cx.spawn(|this, mut cx| async move {
let response = response.await?;
let message_id = response.message_id;
let response = response.payload;
let room_proto = response.room.ok_or_else(|| anyhow!("invalid room"))?;
this.update(&mut cx, |this, model, cx| {
this.update(&mut cx, |this, cx| {
this.status = RoomStatus::Online;
this.apply_room_update(room_proto, model, cx)?;
this.apply_room_update(room_proto, cx)?;
for reshared_project in response.reshared_projects {
if let Some(project) = projects.get(&reshared_project.id) {
project.update(cx, |project, model, cx| {
project.reshared(reshared_project, model, cx).log_err();
project.update(cx, |project, cx| {
project.reshared(reshared_project, cx).log_err();
});
}
}
for rejoined_project in response.rejoined_projects {
if let Some(project) = projects.get(&rejoined_project.id) {
project.update(cx, |project, model, cx| {
project
.rejoined(rejoined_project, message_id, model, cx)
.log_err();
project.update(cx, |project, cx| {
project.rejoined(rejoined_project, message_id, cx).log_err();
});
}
}
@@ -576,13 +567,12 @@ impl Room {
&mut self,
user_id: u64,
role: proto::ChannelRole,
model: &Model<Self>,
cx: &AppContext,
cx: &ModelContext<Self>,
) -> Task<Result<()>> {
let client = self.client.clone();
let room_id = self.id;
let role = role.into();
cx.spawn(|_| async move {
cx.spawn(|_, _| async move {
client
.request(proto::SetRoomParticipantRole {
room_id,
@@ -654,26 +644,19 @@ impl Room {
.payload
.room
.ok_or_else(|| anyhow!("invalid room"))?;
this.update(&mut cx, |this, model, cx| {
this.apply_room_update(room, model, cx)
})?
this.update(&mut cx, |this, cx| this.apply_room_update(room, cx))?
}
fn apply_room_update(
&mut self,
room: proto::Room,
model: &Model<Self>,
cx: &mut AppContext,
) -> Result<()> {
fn apply_room_update(&mut self, room: proto::Room, cx: &mut ModelContext<Self>) -> Result<()> {
log::trace!(
"client {:?}. room update: {:?}",
self.client.user_id(),
&room
);
self.pending_room_update = Some(self.start_room_connection(room, model, cx));
self.pending_room_update = Some(self.start_room_connection(room, cx));
model.notify(cx);
cx.notify();
Ok(())
}
@@ -692,8 +675,7 @@ impl Room {
fn start_room_connection(
&self,
mut room: proto::Room,
model: &Model<Self>,
cx: &mut AppContext,
cx: &mut ModelContext<Self>,
) -> Task<()> {
Task::ready(())
}
@@ -702,8 +684,7 @@ impl Room {
fn start_room_connection(
&self,
mut room: proto::Room,
model: &Model<Self>,
cx: &mut AppContext,
cx: &mut ModelContext<Self>,
) -> Task<()> {
// Filter ourselves out from the room's participants.
let local_participant_ix = room
@@ -725,17 +706,17 @@ impl Room {
.collect::<Vec<_>>();
let (remote_participants, pending_participants) =
self.user_store.update(cx, move |user_store, model, cx| {
self.user_store.update(cx, move |user_store, cx| {
(
user_store.get_users(remote_participant_user_ids, model, cx),
user_store.get_users(pending_participant_user_ids, model, cx),
user_store.get_users(remote_participant_user_ids, cx),
user_store.get_users(pending_participant_user_ids, cx),
)
});
model.spawn(cx, |this, mut cx| async move {
cx.spawn(|this, mut cx| async move {
let (remote_participants, pending_participants) =
futures::join!(remote_participants, pending_participants);
this.update(&mut cx, |this, model, cx| {
this.update(&mut cx, |this, cx| {
this.participant_user_ids.clear();
if let Some(participant) = local_participant {
@@ -747,20 +728,18 @@ impl Room {
if role == proto::ChannelRole::Guest {
for project in mem::take(&mut this.shared_projects) {
if let Some(project) = project.upgrade() {
this.unshare_project(project, model, cx).log_err();
this.unshare_project(project, cx).log_err();
}
}
this.local_participant.projects.clear();
if let Some(livekit_room) = &mut this.live_kit {
livekit_room.stop_publishing(model, cx);
livekit_room.stop_publishing(cx);
}
}
this.joined_projects.retain(|project| {
if let Some(project) = project.upgrade() {
project.update(cx, |project, model, cx| {
project.set_role(role, model, cx)
});
project.update(cx, |project, cx| project.set_role(role, cx));
true
} else {
false
@@ -799,23 +778,20 @@ impl Room {
for project in &participant.projects {
if !old_projects.contains(&project.id) {
model.emit(
Event::RemoteProjectShared {
owner: user.clone(),
project_id: project.id,
worktree_root_names: project.worktree_root_names.clone(),
},
cx,
);
cx.emit(Event::RemoteProjectShared {
owner: user.clone(),
project_id: project.id,
worktree_root_names: project.worktree_root_names.clone(),
});
}
}
for unshared_project_id in old_projects.difference(&new_projects) {
this.joined_projects.retain(|project| {
if let Some(project) = project.upgrade() {
project.update(cx, |project, model, cx| {
project.update(cx, |project, cx| {
if project.remote_id() == Some(*unshared_project_id) {
project.disconnected_from_host(model, cx);
project.disconnected_from_host(cx);
false
} else {
true
@@ -825,12 +801,9 @@ impl Room {
false
}
});
model.emit(
Event::RemoteProjectUnshared {
project_id: *unshared_project_id,
},
cx,
);
cx.emit(Event::RemoteProjectUnshared {
project_id: *unshared_project_id,
});
}
let role = participant.role();
@@ -847,12 +820,9 @@ impl Room {
{
remote_participant.location = location;
remote_participant.role = role;
model.emit(
Event::ParticipantLocationChanged {
participant_id: peer_id,
},
cx,
);
cx.emit(Event::ParticipantLocationChanged {
participant_id: peer_id,
});
}
} else {
this.remote_participants.insert(
@@ -887,7 +857,6 @@ impl Room {
publication,
participant: livekit_participant.clone(),
},
model,
cx,
)
.warn_on_err();
@@ -903,12 +872,9 @@ impl Room {
true
} else {
for project in &participant.projects {
model.emit(
Event::RemoteProjectUnshared {
project_id: project.id,
},
cx,
);
cx.emit(Event::RemoteProjectUnshared {
project_id: project.id,
});
}
false
}
@@ -946,21 +912,21 @@ impl Room {
this.pending_room_update.take();
if this.should_leave() {
log::info!("room is empty, leaving");
this.leave(model, cx).detach();
this.leave(cx).detach();
}
this.user_store.update(cx, |user_store, model, cx| {
this.user_store.update(cx, |user_store, cx| {
let participant_indices_by_user_id = this
.remote_participants
.iter()
.map(|(user_id, participant)| (*user_id, participant.participant_index))
.collect();
user_store.set_participant_indices(participant_indices_by_user_id, model, cx);
user_store.set_participant_indices(participant_indices_by_user_id, cx);
});
this.check_invariants();
this.room_update_completed_tx.try_send(Some(())).ok();
model.notify(cx);
cx.notify();
})
.ok();
})
@@ -969,8 +935,7 @@ impl Room {
fn livekit_room_updated(
&mut self,
event: RoomEvent,
model: &Model<Self>,
cx: &mut AppContext,
cx: &mut ModelContext<Self>,
) -> Result<()> {
log::trace!(
"client {:?}. livekit event: {:?}",
@@ -998,23 +963,17 @@ impl Room {
}
match track {
livekit::track::RemoteTrack::Audio(track) => {
model.emit(
Event::RemoteAudioTracksChanged {
participant_id: participant.peer_id,
},
cx,
);
cx.emit(Event::RemoteAudioTracksChanged {
participant_id: participant.peer_id,
});
let stream = play_remote_audio_track(&track, cx.background_executor())?;
participant.audio_tracks.insert(track_id, (track, stream));
participant.muted = publication.is_muted();
}
livekit::track::RemoteTrack::Video(track) => {
model.emit(
Event::RemoteVideoTracksChanged {
participant_id: participant.peer_id,
},
cx,
);
cx.emit(Event::RemoteVideoTracksChanged {
participant_id: participant.peer_id,
});
participant.video_tracks.insert(track_id, track);
}
}
@@ -1035,21 +994,15 @@ impl Room {
livekit::track::RemoteTrack::Audio(track) => {
participant.audio_tracks.remove(&track.sid());
participant.muted = true;
model.emit(
Event::RemoteAudioTracksChanged {
participant_id: participant.peer_id,
},
cx,
);
cx.emit(Event::RemoteAudioTracksChanged {
participant_id: participant.peer_id,
});
}
livekit::track::RemoteTrack::Video(track) => {
participant.video_tracks.remove(&track.sid());
model.emit(
Event::RemoteVideoTracksChanged {
participant_id: participant.peer_id,
},
cx,
);
cx.emit(Event::RemoteVideoTracksChanged {
participant_id: participant.peer_id,
});
}
}
}
@@ -1127,12 +1080,12 @@ impl Room {
#[cfg(not(target_os = "windows"))]
RoomEvent::Disconnected { reason } => {
log::info!("disconnected from room: {reason:?}");
self.leave(model, cx).detach_and_log_err(cx);
self.leave(cx).detach_and_log_err(cx);
}
_ => {}
}
model.notify(cx);
cx.notify();
Ok(())
}
@@ -1160,18 +1113,17 @@ impl Room {
&mut self,
called_user_id: u64,
initial_project_id: Option<u64>,
model: &Model<Self>,
cx: &mut AppContext,
cx: &mut ModelContext<Self>,
) -> Task<Result<()>> {
if self.status.is_offline() {
return Task::ready(Err(anyhow!("room is offline")));
}
model.notify(cx);
cx.notify();
let client = self.client.clone();
let room_id = self.id;
self.pending_call_count += 1;
model.spawn(cx, move |this, mut cx| async move {
cx.spawn(move |this, mut cx| async move {
let result = client
.request(proto::Call {
room_id,
@@ -1179,10 +1131,10 @@ impl Room {
initial_project_id,
})
.await;
this.update(&mut cx, |this, model, cx| {
this.update(&mut cx, |this, cx| {
this.pending_call_count -= 1;
if this.should_leave() {
this.leave(model, cx).detach_and_log_err(cx);
this.leave(cx).detach_and_log_err(cx);
}
})?;
result?;
@@ -1195,17 +1147,16 @@ impl Room {
id: u64,
language_registry: Arc<LanguageRegistry>,
fs: Arc<dyn Fs>,
model: &Model<Self>,
cx: &mut AppContext,
cx: &mut ModelContext<Self>,
) -> Task<Result<Model<Project>>> {
let client = self.client.clone();
let user_store = self.user_store.clone();
model.emit(Event::RemoteProjectJoined { project_id: id }, cx);
model.spawn(cx, move |this, mut cx| async move {
cx.emit(Event::RemoteProjectJoined { project_id: id });
cx.spawn(move |this, mut cx| async move {
let project =
Project::in_room(id, client, user_store, language_registry, fs, cx.clone()).await?;
this.update(&mut cx, |this, _, cx| {
this.update(&mut cx, |this, cx| {
this.joined_projects.retain(|project| {
if let Some(project) = project.upgrade() {
!project.read(cx).is_disconnected(cx)
@@ -1222,8 +1173,7 @@ impl Room {
pub fn share_project(
&mut self,
project: Model<Project>,
model: &Model<Self>,
cx: &mut AppContext,
cx: &mut ModelContext<Self>,
) -> Task<Result<u64>> {
if let Some(project_id) = project.read(cx).remote_id() {
return Task::ready(Ok(project_id));
@@ -1235,19 +1185,19 @@ impl Room {
is_ssh_project: project.read(cx).is_via_ssh(),
});
model.spawn(cx, |this, mut cx| async move {
cx.spawn(|this, mut cx| async move {
let response = request.await?;
project.update(&mut cx, |project, model, cx| {
project.shared(response.project_id, model, cx)
project.update(&mut cx, |project, cx| {
project.shared(response.project_id, cx)
})??;
// If the user's location is in this project, it changes from UnsharedProject to SharedProject.
this.update(&mut cx, |this, model, cx| {
this.update(&mut cx, |this, cx| {
this.shared_projects.insert(project.downgrade());
let active_project = this.local_participant.active_project.as_ref();
if active_project.map_or(false, |location| *location == project) {
this.set_location(Some(&project), model, cx)
this.set_location(Some(&project), cx)
} else {
Task::ready(Ok(()))
}
@@ -1261,8 +1211,7 @@ impl Room {
pub(crate) fn unshare_project(
&mut self,
project: Model<Project>,
model: &Model<Self>,
cx: &mut AppContext,
cx: &mut ModelContext<Self>,
) -> Result<()> {
let project_id = match project.read(cx).remote_id() {
Some(project_id) => project_id,
@@ -1270,11 +1219,10 @@ impl Room {
};
self.client.send(proto::UnshareProject { project_id })?;
project.update(cx, |this, model, cx| this.unshare(model, cx))?;
project.update(cx, |this, cx| this.unshare(cx))?;
if self.local_participant.active_project == Some(project.downgrade()) {
self.set_location(Some(&project), model, cx)
.detach_and_log_err(cx);
self.set_location(Some(&project), cx).detach_and_log_err(cx);
}
Ok(())
}
@@ -1282,8 +1230,7 @@ impl Room {
pub(crate) fn set_location(
&mut self,
project: Option<&Model<Project>>,
model: &Model<Self>,
cx: &mut AppContext,
cx: &mut ModelContext<Self>,
) -> Task<Result<()>> {
if self.status.is_offline() {
return Task::ready(Err(anyhow!("room is offline")));
@@ -1307,7 +1254,7 @@ impl Room {
proto::participant_location::Variant::External(proto::participant_location::External {})
};
model.notify(cx);
cx.notify();
cx.background_executor().spawn(async move {
client
.request(proto::UpdateParticipantLocation {
@@ -1341,6 +1288,12 @@ impl Room {
})
}
pub fn muted_by_user(&self) -> bool {
self.live_kit
.as_ref()
.map_or(false, |live_kit| live_kit.muted_by_user)
}
pub fn is_speaking(&self) -> bool {
self.live_kit
.as_ref()
@@ -1377,21 +1330,13 @@ impl Room {
}
#[cfg(target_os = "windows")]
pub fn share_microphone(
&mut self,
model: &Model<Self>,
cx: &mut AppContext,
) -> Task<Result<()>> {
pub fn share_microphone(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
Task::ready(Err(anyhow!("Windows is not supported yet")))
}
#[cfg(not(target_os = "windows"))]
#[track_caller]
pub fn share_microphone(
&mut self,
model: &Model<Self>,
cx: &mut AppContext,
) -> Task<Result<()>> {
pub fn share_microphone(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
if self.status.is_offline() {
return Task::ready(Err(anyhow!("room is offline")));
}
@@ -1399,13 +1344,13 @@ impl Room {
let (participant, publish_id) = if let Some(live_kit) = self.live_kit.as_mut() {
let publish_id = post_inc(&mut live_kit.next_publish_id);
live_kit.microphone_track = LocalTrack::Pending { publish_id };
model.notify(cx);
cx.notify();
(live_kit.room.local_participant(), publish_id)
} else {
return Task::ready(Err(anyhow!("live-kit was not initialized")));
};
model.spawn(cx, move |this, mut cx| async move {
cx.spawn(move |this, mut cx| async move {
let (track, stream) = capture_local_audio_track(cx.background_executor())?.await;
let publication = participant
@@ -1418,7 +1363,7 @@ impl Room {
)
.await
.map_err(|error| anyhow!("failed to publish track: {error}"));
this.update(&mut cx, |this, model, cx| {
this.update(&mut cx, |this, cx| {
let live_kit = this
.live_kit
.as_mut()
@@ -1449,7 +1394,7 @@ impl Room {
track_publication: publication,
_stream: Box::new(stream),
};
model.notify(cx);
cx.notify();
}
Ok(())
}
@@ -1458,7 +1403,7 @@ impl Room {
Ok(())
} else {
live_kit.microphone_track = LocalTrack::None;
model.notify(cx);
cx.notify();
Err(error)
}
}
@@ -1468,12 +1413,12 @@ impl Room {
}
#[cfg(target_os = "windows")]
pub fn share_screen(&mut self, model: &Model<Self>, cx: &mut AppContext) -> Task<Result<()>> {
pub fn share_screen(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
Task::ready(Err(anyhow!("Windows is not supported yet")))
}
#[cfg(not(target_os = "windows"))]
pub fn share_screen(&mut self, model: &Model<Self>, cx: &mut AppContext) -> Task<Result<()>> {
pub fn share_screen(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
if self.status.is_offline() {
return Task::ready(Err(anyhow!("room is offline")));
}
@@ -1484,7 +1429,7 @@ impl Room {
let (participant, publish_id) = if let Some(live_kit) = self.live_kit.as_mut() {
let publish_id = post_inc(&mut live_kit.next_publish_id);
live_kit.screen_track = LocalTrack::Pending { publish_id };
model.notify(cx);
cx.notify();
(live_kit.room.local_participant(), publish_id)
} else {
return Task::ready(Err(anyhow!("live-kit was not initialized")));
@@ -1492,7 +1437,7 @@ impl Room {
let sources = cx.screen_capture_sources();
model.spawn(cx, move |this, mut cx| async move {
cx.spawn(move |this, mut cx| async move {
let sources = sources.await??;
let source = sources.first().ok_or_else(|| anyhow!("no display found"))?;
@@ -1510,7 +1455,7 @@ impl Room {
.await
.map_err(|error| anyhow!("error publishing screen track {error:?}"));
this.update(&mut cx, |this, model, cx| {
this.update(&mut cx, |this, cx| {
let live_kit = this
.live_kit
.as_mut()
@@ -1538,7 +1483,7 @@ impl Room {
track_publication: publication,
_stream: Box::new(stream),
};
model.notify(cx);
cx.notify();
}
Audio::play_sound(Sound::StartScreenshare, cx);
@@ -1549,7 +1494,7 @@ impl Room {
Ok(())
} else {
live_kit.screen_track = LocalTrack::None;
model.notify(cx);
cx.notify();
Err(error)
}
}
@@ -1558,7 +1503,7 @@ impl Room {
})
}
pub fn toggle_mute(&mut self, model: &Model<Self>, cx: &mut AppContext) {
pub fn toggle_mute(&mut self, cx: &mut ModelContext<Self>) {
if let Some(live_kit) = self.live_kit.as_mut() {
// When unmuting, undeafen if the user was deafened before.
let was_deafened = live_kit.deafened;
@@ -1574,17 +1519,17 @@ impl Room {
let muted = live_kit.muted_by_user;
let should_undeafen = was_deafened && !live_kit.deafened;
if let Some(task) = self.set_mute(muted, model, cx) {
if let Some(task) = self.set_mute(muted, cx) {
task.detach_and_log_err(cx);
}
if should_undeafen {
self.set_deafened(false, model, cx);
self.set_deafened(false, cx);
}
}
}
pub fn toggle_deafen(&mut self, model: &Model<Self>, cx: &mut AppContext) {
pub fn toggle_deafen(&mut self, cx: &mut ModelContext<Self>) {
if let Some(live_kit) = self.live_kit.as_mut() {
// When deafening, mute the microphone if it was not already muted.
// When un-deafening, unmute the microphone, unless it was explicitly muted.
@@ -1592,17 +1537,17 @@ impl Room {
live_kit.deafened = deafened;
let should_change_mute = !live_kit.muted_by_user;
self.set_deafened(deafened, model, cx);
self.set_deafened(deafened, cx);
if should_change_mute {
if let Some(task) = self.set_mute(deafened, model, cx) {
if let Some(task) = self.set_mute(deafened, cx) {
task.detach_and_log_err(cx);
}
}
}
}
pub fn unshare_screen(&mut self, model: &Model<Self>, cx: &mut AppContext) -> Result<()> {
pub fn unshare_screen(&mut self, cx: &mut ModelContext<Self>) -> Result<()> {
if self.status.is_offline() {
return Err(anyhow!("room is offline"));
}
@@ -1614,7 +1559,7 @@ impl Room {
match mem::take(&mut live_kit.screen_track) {
LocalTrack::None => Err(anyhow!("screen was not shared")),
LocalTrack::Pending { .. } => {
model.notify(cx);
cx.notify();
Ok(())
}
LocalTrack::Published {
@@ -1627,7 +1572,7 @@ impl Room {
cx.background_executor()
.spawn(async move { local_participant.unpublish_track(&sid).await })
.detach_and_log_err(cx);
model.notify(cx);
cx.notify();
}
Audio::play_sound(Sound::StopScreenshare, cx);
Ok(())
@@ -1635,16 +1580,11 @@ impl Room {
}
}
fn set_deafened(
&mut self,
deafened: bool,
model: &Model<Self>,
cx: &mut AppContext,
) -> Option<()> {
fn set_deafened(&mut self, deafened: bool, cx: &mut ModelContext<Self>) -> Option<()> {
#[cfg(not(target_os = "windows"))]
{
let live_kit = self.live_kit.as_mut()?;
model.notify(cx);
cx.notify();
for (_, participant) in live_kit.room.remote_participants() {
for (_, publication) in participant.track_publications() {
if publication.kind() == TrackKind::Audio {
@@ -1660,11 +1600,10 @@ impl Room {
fn set_mute(
&mut self,
should_mute: bool,
model: &Model<Room>,
cx: &mut AppContext,
cx: &mut ModelContext<Room>,
) -> Option<Task<Result<()>>> {
let live_kit = self.live_kit.as_mut()?;
model.notify(cx);
cx.notify();
if should_mute {
Audio::play_sound(Sound::Mute, cx);
@@ -1677,7 +1616,7 @@ impl Room {
if should_mute {
None
} else {
Some(self.share_microphone(model, cx))
Some(self.share_microphone(cx))
}
}
LocalTrack::Pending { .. } => None,
@@ -1701,62 +1640,59 @@ impl Room {
#[cfg(target_os = "windows")]
fn spawn_room_connection(
livekit_connection_info: Option<proto::LiveKitConnectionInfo>,
model: &Model<Room>,
cx: &mut AppContext,
cx: &mut ModelContext<'_, Room>,
) {
}
#[cfg(not(target_os = "windows"))]
fn spawn_room_connection(
livekit_connection_info: Option<proto::LiveKitConnectionInfo>,
model: &Model<Room>,
cx: &mut AppContext,
cx: &mut ModelContext<'_, Room>,
) {
if let Some(connection_info) = livekit_connection_info {
model
.spawn(cx, |this, mut cx| async move {
let (room, mut events) = livekit::Room::connect(
&connection_info.server_url,
&connection_info.token,
RoomOptions::default(),
)
.await?;
cx.spawn(|this, mut cx| async move {
let (room, mut events) = livekit::Room::connect(
&connection_info.server_url,
&connection_info.token,
RoomOptions::default(),
)
.await?;
this.update(&mut cx, |this, model, cx| {
let _handle_updates = model.spawn(cx, |this, mut cx| async move {
while let Some(event) = events.recv().await {
if this
.update(&mut cx, |this, model, cx| {
this.livekit_room_updated(event, model, cx).warn_on_err();
})
.is_err()
{
break;
}
this.update(&mut cx, |this, cx| {
let _handle_updates = cx.spawn(|this, mut cx| async move {
while let Some(event) = events.recv().await {
if this
.update(&mut cx, |this, cx| {
this.livekit_room_updated(event, cx).warn_on_err();
})
.is_err()
{
break;
}
});
let muted_by_user = Room::mute_on_join(cx);
this.live_kit = Some(LiveKitRoom {
room: Arc::new(room),
screen_track: LocalTrack::None,
microphone_track: LocalTrack::None,
next_publish_id: 0,
muted_by_user,
deafened: false,
speaking: false,
_handle_updates,
});
if !muted_by_user && this.can_use_microphone(cx) {
this.share_microphone(model, cx)
} else {
Task::ready(Ok(()))
}
})?
.await
})
.detach_and_log_err(cx);
});
let muted_by_user = Room::mute_on_join(cx);
this.live_kit = Some(LiveKitRoom {
room: Arc::new(room),
screen_track: LocalTrack::None,
microphone_track: LocalTrack::None,
next_publish_id: 0,
muted_by_user,
deafened: false,
speaking: false,
_handle_updates,
});
if !muted_by_user && this.can_use_microphone(cx) {
this.share_microphone(cx)
} else {
Task::ready(Ok(()))
}
})?
.await
})
.detach_and_log_err(cx);
}
}
@@ -1774,17 +1710,17 @@ struct LiveKitRoom {
impl LiveKitRoom {
#[cfg(target_os = "windows")]
fn stop_publishing(&mut self, model: &Model<Room>, _cx: &mut AppContext) {}
fn stop_publishing(&mut self, _cx: &mut ModelContext<Room>) {}
#[cfg(not(target_os = "windows"))]
fn stop_publishing(&mut self, model: &Model<Room>, cx: &mut AppContext) {
fn stop_publishing(&mut self, cx: &mut ModelContext<Room>) {
let mut tracks_to_unpublish = Vec::new();
if let LocalTrack::Published {
track_publication, ..
} = mem::replace(&mut self.microphone_track, LocalTrack::None)
{
tracks_to_unpublish.push(track_publication.sid());
model.notify(cx);
cx.notify();
}
if let LocalTrack::Published {
@@ -1792,7 +1728,7 @@ impl LiveKitRoom {
} = mem::replace(&mut self.screen_track, LocalTrack::None)
{
tracks_to_unpublish.push(track_publication.sid());
model.notify(cx);
cx.notify();
}
let participant = self.room.local_participant();

View File

@@ -8,8 +8,8 @@ use client::{proto, ChannelId, Client, TypedEnvelope, User, UserStore, ZED_ALWAY
use collections::HashSet;
use futures::{channel::oneshot, future::Shared, Future, FutureExt};
use gpui::{
AppContext, AsyncAppContext, Context, EventEmitter, Global, Model, Subscription, Task,
WeakModel,
AppContext, AsyncAppContext, Context, EventEmitter, Global, Model, ModelContext, Subscription,
Task, WeakModel,
};
use postage::watch;
use project::Project;
@@ -27,7 +27,7 @@ impl Global for GlobalActiveCall {}
pub fn init(client: Arc<Client>, user_store: Model<UserStore>, cx: &mut AppContext) {
CallSettings::register(cx);
let active_call = cx.new_model(|model, cx| ActiveCall::new(client, user_store, model, cx));
let active_call = cx.new_model(|cx| ActiveCall::new(client, user_store, cx));
cx.set_global(GlobalActiveCall(active_call));
}
@@ -89,12 +89,7 @@ pub struct ActiveCall {
impl EventEmitter<Event> for ActiveCall {}
impl ActiveCall {
fn new(
client: Arc<Client>,
user_store: Model<UserStore>,
model: &Model<Self>,
cx: &mut AppContext,
) -> Self {
fn new(client: Arc<Client>, user_store: Model<UserStore>, cx: &mut ModelContext<Self>) -> Self {
Self {
room: None,
pending_room_creation: None,
@@ -103,8 +98,8 @@ impl ActiveCall {
incoming_call: watch::channel(),
_join_debouncer: OneAtATime { cancel: None },
_subscriptions: vec![
client.add_request_handler(model.downgrade(), Self::handle_incoming_call),
client.add_message_handler(model.downgrade(), Self::handle_call_canceled),
client.add_request_handler(cx.weak_model(), Self::handle_incoming_call),
client.add_message_handler(cx.weak_model(), Self::handle_call_canceled),
],
client,
user_store,
@@ -120,22 +115,22 @@ impl ActiveCall {
envelope: TypedEnvelope<proto::IncomingCall>,
mut cx: AsyncAppContext,
) -> Result<proto::Ack> {
let user_store = this.update(&mut cx, |this, _, _| this.user_store.clone())?;
let user_store = this.update(&mut cx, |this, _| this.user_store.clone())?;
let call = IncomingCall {
room_id: envelope.payload.room_id,
participants: user_store
.update(&mut cx, |user_store, model, cx| {
user_store.get_users(envelope.payload.participant_user_ids, model, cx)
.update(&mut cx, |user_store, cx| {
user_store.get_users(envelope.payload.participant_user_ids, cx)
})?
.await?,
calling_user: user_store
.update(&mut cx, |user_store, model, cx| {
user_store.get_user(envelope.payload.calling_user_id, model, cx)
.update(&mut cx, |user_store, cx| {
user_store.get_user(envelope.payload.calling_user_id, cx)
})?
.await?,
initial_project: envelope.payload.initial_project,
};
this.update(&mut cx, |this, _, _| {
this.update(&mut cx, |this, _| {
*this.incoming_call.0.borrow_mut() = Some(call);
})?;
@@ -147,7 +142,7 @@ impl ActiveCall {
envelope: TypedEnvelope<proto::CallCanceled>,
mut cx: AsyncAppContext,
) -> Result<()> {
this.update(&mut cx, |this, _, _| {
this.update(&mut cx, |this, _| {
let mut incoming_call = this.incoming_call.0.borrow_mut();
if incoming_call
.as_ref()
@@ -172,13 +167,12 @@ impl ActiveCall {
&mut self,
called_user_id: u64,
initial_project: Option<Model<Project>>,
model: &Model<Self>,
cx: &mut AppContext,
cx: &mut ModelContext<Self>,
) -> Task<Result<()>> {
if !self.pending_invites.insert(called_user_id) {
return Task::ready(Err(anyhow!("user was already invited")));
}
model.notify(cx);
cx.notify();
if self._join_debouncer.running() {
return Task::ready(Ok(()));
@@ -191,22 +185,20 @@ impl ActiveCall {
};
let invite = if let Some(room) = room {
cx.spawn(move |mut cx| async move {
cx.spawn(move |_, mut cx| async move {
let room = room.await.map_err(|err| anyhow!("{:?}", err))?;
let initial_project_id = if let Some(initial_project) = initial_project {
Some(
room.update(&mut cx, |room, model, cx| {
room.share_project(initial_project, model, cx)
})?
.await?,
room.update(&mut cx, |room, cx| room.share_project(initial_project, cx))?
.await?,
)
} else {
None
};
room.update(&mut cx, move |room, model, cx| {
room.call(called_user_id, initial_project_id, model, cx)
room.update(&mut cx, move |room, cx| {
room.call(called_user_id, initial_project_id, cx)
})?
.await?;
@@ -215,8 +207,8 @@ impl ActiveCall {
} else {
let client = self.client.clone();
let user_store = self.user_store.clone();
let room = model
.spawn(cx, move |this, mut cx| async move {
let room = cx
.spawn(move |this, mut cx| async move {
let create_room = async {
let room = cx
.update(|cx| {
@@ -230,16 +222,14 @@ impl ActiveCall {
})?
.await?;
this.update(&mut cx, |this, model, cx| {
this.set_room(Some(room.clone()), model, cx)
})?
.await?;
this.update(&mut cx, |this, cx| this.set_room(Some(room.clone()), cx))?
.await?;
anyhow::Ok(room)
};
let room = create_room.await;
this.update(&mut cx, |this, _, _| this.pending_room_creation = None)?;
this.update(&mut cx, |this, _| this.pending_room_creation = None)?;
room.map_err(Arc::new)
})
.shared();
@@ -250,20 +240,18 @@ impl ActiveCall {
})
};
model.spawn(cx, move |this, mut cx| async move {
cx.spawn(move |this, mut cx| async move {
let result = invite.await;
if result.is_ok() {
this.update(&mut cx, |this, model, cx| {
this.report_call_event("invite", cx)
})?;
this.update(&mut cx, |this, cx| this.report_call_event("invite", cx))?;
} else {
//TODO: report collaboration error
log::error!("invite failed: {:?}", result);
}
this.update(&mut cx, |this, model, cx| {
this.update(&mut cx, |this, cx| {
this.pending_invites.remove(&called_user_id);
model.notify(cx);
cx.notify();
})?;
result
})
@@ -272,8 +260,7 @@ impl ActiveCall {
pub fn cancel_invite(
&mut self,
called_user_id: u64,
model: &Model<Self>,
cx: &mut AppContext,
cx: &mut ModelContext<Self>,
) -> Task<Result<()>> {
let room_id = if let Some(room) = self.room() {
room.read(cx).id()
@@ -297,11 +284,7 @@ impl ActiveCall {
self.incoming_call.1.clone()
}
pub fn accept_incoming(
&mut self,
model: &Model<Self>,
cx: &mut AppContext,
) -> Task<Result<()>> {
pub fn accept_incoming(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
if self.room.is_some() {
return Task::ready(Err(anyhow!("cannot join while on another call")));
}
@@ -323,20 +306,18 @@ impl ActiveCall {
._join_debouncer
.spawn(cx, move |cx| Room::join(room_id, client, user_store, cx));
model.spawn(cx, |this, mut cx| async move {
cx.spawn(|this, mut cx| async move {
let room = join.await?;
this.update(&mut cx, |this, model, cx| {
this.set_room(room.clone(), model, cx)
})?
.await?;
this.update(&mut cx, |this, model, cx| {
this.update(&mut cx, |this, cx| this.set_room(room.clone(), cx))?
.await?;
this.update(&mut cx, |this, cx| {
this.report_call_event("accept incoming", cx)
})?;
Ok(())
})
}
pub fn decline_incoming(&mut self, _: &Model<Self>, _: &mut AppContext) -> Result<()> {
pub fn decline_incoming(&mut self, _: &mut ModelContext<Self>) -> Result<()> {
let call = self
.incoming_call
.0
@@ -353,14 +334,13 @@ impl ActiveCall {
pub fn join_channel(
&mut self,
channel_id: ChannelId,
model: &Model<Self>,
cx: &mut AppContext,
cx: &mut ModelContext<Self>,
) -> Task<Result<Option<Model<Room>>>> {
if let Some(room) = self.room().cloned() {
if room.read(cx).channel_id() == Some(channel_id) {
return Task::ready(Ok(Some(room)));
} else {
room.update(cx, |room, model, cx| room.clear_state(cx));
room.update(cx, |room, cx| room.clear_state(cx));
}
}
@@ -374,29 +354,27 @@ impl ActiveCall {
Room::join_channel(channel_id, client, user_store, cx).await
});
model.spawn(cx, |this, mut cx| async move {
cx.spawn(|this, mut cx| async move {
let room = join.await?;
this.update(&mut cx, |this, model, cx| {
this.set_room(room.clone(), model, cx)
})?
.await?;
this.update(&mut cx, |this, model, cx| {
this.update(&mut cx, |this, cx| this.set_room(room.clone(), cx))?
.await?;
this.update(&mut cx, |this, cx| {
this.report_call_event("join channel", cx)
})?;
Ok(room)
})
}
pub fn hang_up(&mut self, model: &Model<Self>, cx: &mut AppContext) -> Task<Result<()>> {
model.notify(cx);
pub fn hang_up(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
cx.notify();
self.report_call_event("hang up", cx);
Audio::end_call(cx);
let channel_id = self.channel_id(cx);
if let Some((room, _)) = self.room.take() {
model.emit(Event::RoomLeft { channel_id }, cx);
room.update(cx, |room, model, cx| room.leave(model, cx))
cx.emit(Event::RoomLeft { channel_id });
room.update(cx, |room, cx| room.leave(cx))
} else {
Task::ready(Ok(()))
}
@@ -405,12 +383,11 @@ impl ActiveCall {
pub fn share_project(
&mut self,
project: Model<Project>,
model: &Model<Self>,
cx: &mut AppContext,
cx: &mut ModelContext<Self>,
) -> Task<Result<u64>> {
if let Some((room, _)) = self.room.as_ref() {
self.report_call_event("share project", cx);
room.update(cx, |room, model, cx| room.share_project(project, model, cx))
room.update(cx, |room, cx| room.share_project(project, cx))
} else {
Task::ready(Err(anyhow!("no active call")))
}
@@ -419,14 +396,11 @@ impl ActiveCall {
pub fn unshare_project(
&mut self,
project: Model<Project>,
model: &Model<Self>,
cx: &mut AppContext,
cx: &mut ModelContext<Self>,
) -> Result<()> {
if let Some((room, _)) = self.room.as_ref() {
self.report_call_event("unshare project", cx);
room.update(cx, |room, model, cx| {
room.unshare_project(project, model, cx)
})
room.update(cx, |room, cx| room.unshare_project(project, cx))
} else {
Err(anyhow!("no active call"))
}
@@ -439,13 +413,12 @@ impl ActiveCall {
pub fn set_location(
&mut self,
project: Option<&Model<Project>>,
model: &Model<Self>,
cx: &mut AppContext,
cx: &mut ModelContext<Self>,
) -> Task<Result<()>> {
if project.is_some() || !*ZED_ALWAYS_ACTIVE {
self.location = project.map(|project| project.downgrade());
if let Some((room, _)) = self.room.as_ref() {
return room.update(cx, |room, model, cx| room.set_location(project, model, cx));
return room.update(cx, |room, cx| room.set_location(project, cx));
}
}
Task::ready(Ok(()))
@@ -454,29 +427,26 @@ impl ActiveCall {
fn set_room(
&mut self,
room: Option<Model<Room>>,
model: &Model<Self>,
cx: &mut AppContext,
cx: &mut ModelContext<Self>,
) -> Task<Result<()>> {
if room.as_ref() == self.room.as_ref().map(|room| &room.0) {
Task::ready(Ok(()))
} else {
model.notify(cx);
cx.notify();
if let Some(room) = room {
if room.read(cx).status().is_offline() {
self.room = None;
Task::ready(Ok(()))
} else {
let subscriptions = vec![
model.observe(&room, cx, |this, room, model, cx| {
cx.observe(&room, |this, room, cx| {
if room.read(cx).status().is_offline() {
this.set_room(None, model, cx).detach_and_log_err(cx);
this.set_room(None, cx).detach_and_log_err(cx);
}
model.notify(cx);
}),
model.subscribe(&room, cx, |_, _, event, model, cx| {
model.emit(event.clone(), cx)
cx.notify();
}),
cx.subscribe(&room, |_, _, event, cx| cx.emit(event.clone())),
];
self.room = Some((room.clone(), subscriptions));
let location = self
@@ -484,10 +454,8 @@ impl ActiveCall {
.as_ref()
.and_then(|location| location.upgrade());
let channel_id = room.read(cx).channel_id();
model.emit(Event::RoomJoined { channel_id }, cx);
room.update(cx, |room, model, cx| {
room.set_location(location.as_ref(), model, cx)
})
cx.emit(Event::RoomJoined { channel_id });
room.update(cx, |room, cx| room.set_location(location.as_ref(), cx))
}
} else {
self.room = None;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -18,7 +18,8 @@ use std::time::Instant;
use std::{env, mem, path::PathBuf, sync::Arc, time::Duration};
use telemetry_events::{
ActionEvent, AppEvent, AssistantEvent, CallEvent, EditEvent, EditorEvent, Event,
EventRequestBody, EventWrapper, ExtensionEvent, InlineCompletionEvent, ReplEvent, SettingEvent,
EventRequestBody, EventWrapper, ExtensionEvent, InlineCompletionEvent, InlineCompletionRating,
InlineCompletionRatingEvent, ReplEvent, SettingEvent,
};
use util::{ResultExt, TryFutureExt};
use worktree::{UpdatedEntriesSet, WorktreeId};
@@ -355,6 +356,24 @@ impl Telemetry {
self.report_event(event)
}
pub fn report_inline_completion_rating_event(
self: &Arc<Self>,
rating: InlineCompletionRating,
input_events: Arc<str>,
input_excerpt: Arc<str>,
output_excerpt: Arc<str>,
feedback: String,
) {
let event = Event::InlineCompletionRating(InlineCompletionRatingEvent {
rating,
input_events,
input_excerpt,
output_excerpt,
feedback,
});
self.report_event(event);
}
pub fn report_assistant_event(self: &Arc<Self>, event: AssistantEvent) {
self.report_event(Event::Assistant(event));
}

View File

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

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