Compare commits

...

93 Commits

Author SHA1 Message Date
Piotr Osiewicz
8f44ff7d31 Cargo fmt 2025-03-31 15:31:27 +02:00
Piotr Osiewicz
e85ec8ffb7 chore: Prepare for Rust 2024
This is just a result of `cargo fix --edition`, without actually bumping the edition just yet.
2025-03-31 15:29:44 +02:00
Richard Feldman
9b40770e9f Add Code Symbols tool (#27733)
Lets you get all the code symbols in the project (like the Code Symbols
panel) or in a particular file (like the Outline panel), optionally
paginated and filtering results by regex. The tool gives the files,
lines, and numbers of all of these, which means they can be used in
conjunction with the read file tool to read subsets of large files
without having to open the entire large file and poke around in it.

<img width="621" alt="Screenshot 2025-03-29 at 12 00 21 PM"
src="https://github.com/user-attachments/assets/d78259d7-2746-44c0-ac18-2e21f2505c0a"
/>

Release Notes:

- N/A
2025-03-31 05:13:13 +00:00
Richard Feldman
5b2adfbb50 Add symbol-info tool to default tools (#27766)
Follow-up to https://github.com/zed-industries/zed/pull/27733

Release Notes:

- N/A
2025-03-31 00:56:22 -04:00
Richard Feldman
078b241223 Add symbol info tool (#27742)
Does various readonly LSP operations: get definition, get declaration,
get implementation, get type definition, and find all references.

<img width="635" alt="Screenshot 2025-03-30 at 1 24 11 AM"
src="https://github.com/user-attachments/assets/87eae2b0-9791-4e7f-b91f-79dfc2b746cc"
/>

Release Notes:

- N/A
2025-03-31 00:23:03 -04:00
Finn Evers
e42406f9d5 python: Fix incorrect highlighting of function parameters (#26815)
This PR addresses the highlighting of function parameters in Python.

#21454 added various improvements to Python highlighting. However, some
of the capture groups are missing corresponding colors in themes, which
was also [noted on the
PR](https://github.com/zed-industries/zed/pull/21454#pullrequestreview-2537510964).

Currently, this is especially bad for function parameters, which are not
only missing corresponding colors, but are also captured incorrectly as
`arguments` instead of `parameters`. Additionally, as not one theme
defines `function.arguments` (I cheked this with the [extension
surveyor](https://github.com/zed-industries/extension-surveyor), we
instead always fall back to `function` here. Thus, parameters are always
highlighted the same as functions, resulting in incorrect and inproper
highlighting.

This PR resolves this issue by instead capturing parameters as
`variable.parameter`, which has not perfect, but much better coverage
among existing themes.

| `main` | <img width="670" alt="main"
src="https://github.com/user-attachments/assets/6942b494-fe0f-4537-8503-8de4e2c5a30e"
/> |
| --- | --- |
| This PR | <img width="670" alt="PR"
src="https://github.com/user-attachments/assets/f0d1d22a-c5f4-46b8-a22b-f18e0e55fa47"
/> |

--- 

Following [this
comment](7d9dbbe5fe/extensions/test-extension/languages/gleam/highlights.scm (L77-L78))
and [the note on the other
PR](https://github.com/zed-industries/zed/pull/21454#discussion_r1907012758),
I also updated the last two matchs in the file to instead use `any-of`
in the second commit (GitHub falsely shows `id` being removed despite it
still being present). Should that not be wanted, I can revert this
change.

Release Notes:

- Fixed improper highlighting of function parameters in Python.
2025-03-31 01:23:03 +02:00
Danilo Leal
4ee20dda23 assistant2: Adjust edit files design (#27762)
This PR includes design tweaks to elements involved on the "edit files"
flow: the bar that appears above the message editor, buttons on the
multibuffer hunks, adding keybindings to the "Review Changes" button,
etc.

<img
src="https://github.com/user-attachments/assets/4bff883a-c5c4-443e-8bf5-d98f535c83ce"
width="750" />

Release Notes:

- N/A
2025-03-30 18:52:48 -03:00
Danilo Leal
74dd32d52c assistant2: Fix overflowing notification title (#27763)
<img
src="https://github.com/user-attachments/assets/5a22caba-2535-4f45-96a1-6ee9a552266f"
width="500"/>

Release Notes:

- N/A
2025-03-30 18:31:56 -03:00
Richard Feldman
342acdd080 Show notifications on primary screen by default (#27665)
By default, agent notifications now display only on your primary screen.
You can optionally configure them to display on all screens (or not to
display at all).

Release Notes:

- N/A
2025-03-30 14:33:20 -04:00
Cole Miller
9f8776d1af Fix stale git statuses (#27735)
Display of git statuses in the git panel, project panel, and tabs
regressed in #27391, causing us to frequently see stale statuses. This
turns out to be because we were not emitting the
`WorktreeUpdatedGitRepositories` event in cases where we should be,
which in turn is because of bumping the `LocalRepositoryEntry`'s
`status_scan_id` too early, so that a later comparison of two
`status_scan_id` values wasn't detecting a change that we're expecting
it to detect.

Release Notes:

- N/A (problematic behavior didn't make it into stable or preview)
2025-03-29 22:50:09 -04:00
Ben Kunkle
548a8d75e6 proto: Create indents.scm (#27730)
Closes #27676

Release Notes:

- N/A
2025-03-29 15:24:05 +00:00
Conrad Irwin
73f77a7fbb Actually run git commands if no GIT_ASKPASS is set (#27729)
Follow up to #27681

Release Notes:

- N/A
2025-03-29 15:19:19 +00:00
Smit Barmase
4970fe2d56 editor: Hide mouse cursor also for movements and selections (#27677)
This enables hiding mouse cursor even on cursor movements like up, down,
etc. or selections made using keyboard, etc.

Renamed existing boolean setting "hide_mouse_while_typing" to
"hide_mouse". It can have three values: `on_typing_and_movement`,
`on_typing`, `never`.

Release Notes:

- Now mouse cursor hides even when you navigate, or make selections
using keyboard in editor. This behavior can be changed by setting
`hide_mouse` to `on_typing_and_movement`, `on_typing` or `never`.
2025-03-29 19:23:36 +05:30
Antonio Scandurra
7fe6188f8e Introduce "Keep All" and "Reject All" buttons when reviewing assistant edits (#27724)
Release Notes:

- N/A
2025-03-29 09:28:11 +00:00
Anthony Eid
8add90d7cb Set up Rust debugger code runner tasks (#27571)
## Summary 
This PR starts the process of adding debug task locators to Zed's
debugger system. A task locator is a secondary resolution phase that
allows a debug task to run a command before starting a debug session and
then uses the output of the run command to configure itself.

Locators are most applicable when debugging a compiled language but will
be helpful for any language as well.

## Architecture

At a high level, this works by adding a debug task queue to `Workspace`.
Which add's a debug configuration associated with a `TaskId` whenever a
resolved task with a debug config is added to `TaskInventory`'s queue.
Then, when the `SpawnInTerminal` task finishes running, it emits its
task_id and the result of the ran task.

When a ran task exits successfully, `Workspace` tells `Project` to start
a debug session using its stored debug config, then `DapStore` queries
the `LocatorStore` to configure the debug configuration if it has a
valid locator argument.

Release Notes:

- N/A
2025-03-29 02:10:40 -04:00
Conrad Irwin
141a6c3915 Revert "terminal: Make IME work with tab and enter keys (#27572)" (#27719)
This reverts commit be657aefa3. (#27572)

Unfortunately this change broke other bindings in the terminal like
`cmd-left`
and `cmd-right`.

We do need to redo the terminal IME handling at some point, but we'll
need a
bit more thought to find an approach that works.

Release Notes:

- N/A
2025-03-29 03:28:14 +00:00
Floyd Wang
b4254a33e0 gpui: Support window resizing for PlatformWindow (#27477)
Support resizing windows to a specified size.

## macOS

https://github.com/user-attachments/assets/8c639bc2-ee5f-4adc-a850-576dac939574


## Wayland

[wayland.webm](https://github.com/user-attachments/assets/3d593604-83b4-488f-8f63-1cf4c0c0cb9a)

## X11

[x11.webm](https://github.com/user-attachments/assets/ce8fa62e-fb74-4641-abe8-70574011e630)

## Windows

https://github.com/user-attachments/assets/abb03e48-f82a-4d62-90b3-2598a4866c3f

Release Notes:

- N/A
2025-03-28 20:02:15 -07:00
Piotr Osiewicz
f86977e2a7 debugger: Touchups to log breakpoints (#27675)
This is a slight refactor that flattens Breakpoint struct in
anticipation of condition/hit breakpoints. It also adds a slight delay
before breakpoints are shown on gutter hover to make breakpoints less
attention grabbing.
Release Notes:

- N/A
2025-03-29 02:16:44 +01:00
Marshall Bowers
8ecf553279 assistant2: Add a way to quickly configure tools for the current profile (#27714)
This PR adds a new entry to the profile selector to quickly access tool
customization for the current profile:

<img width="228" alt="Screenshot 2025-03-28 at 7 08 51 PM"
src="https://github.com/user-attachments/assets/929ae5e7-5a16-4bf2-8043-6c09b621fc61"
/>

Release Notes:

- N/A
2025-03-28 23:25:39 +00:00
Marshall Bowers
e171d16ae3 assistant2: Fix incorrect action when clicking on a profile (#27710)
This PR fixes an issue where clicking on a profile entry would fork it
instead of viewing it.

Release Notes:

- N/A
2025-03-28 23:22:56 +00:00
Danilo Leal
35da9c0cdc assistant2: Move prompt editor item into dropdown menu (#27708)
This PR makes the plus icon button not a dropdown anymore, freeing it up
to be always the new thread action. In consequence, I'm moving all of
the other items into another dropdown, which now houses "new prompt
editor", history, and settings, all of which there are keybindings for.

<img
src="https://github.com/user-attachments/assets/1d0d43da-9447-4218-8b9b-e692c0b74f61"
width="700"/>
 
Release Notes:

- N/A
2025-03-28 19:44:18 -03:00
Danilo Leal
8b3eb98d86 assistant2: Adjust elements in the message editor (#27696)
Most notable change in this PR is the changing the default profiles'
names to just "Write" and "Ask". Everything else is mostly
design-related. Here's how it looks like:

<img
src="https://github.com/user-attachments/assets/791948c9-2d63-4523-9d54-08b63a00be6a"
width="600" />

Release Notes:

- N/A
2025-03-28 19:34:16 -03:00
Kirill Bulatov
d912b0dd36 Remove unused tasks-related config (#27707)
Release Notes:

- N/A
2025-03-28 22:34:02 +00:00
Danilo Leal
044508ef77 assistant2: Visually de-emphasize read-only tool calls (#27702)
<img
src="https://github.com/user-attachments/assets/03961518-ae40-47d8-b84c-974c9b897eb3"
width="500"/>

Release Notes:

- N/A

---------

Co-authored-by: Agus Zubiaga <hi@aguz.me>
2025-03-28 19:33:56 -03:00
Danilo Leal
d63658cee2 Remove duplicate message bubble icon (#27699)
Release Notes:

- N/A
2025-03-28 19:33:43 -03:00
Marshall Bowers
790b56f52c livekit_client: Sort dependencies in Cargo.toml (#27705)
This PR sorts the dependencies in the `Cargo.toml` for `livekit_client`.

Release Notes:

- N/A
2025-03-28 22:32:18 +00:00
Marshall Bowers
43142000a6 Switch back to upstream cargo_metadata (#27706)
This PR switches us back to the upstream `cargo_metadata`.

We had switched to a fork in #27126, but this shouldn't be necessary
after #27117.

Release Notes:

- N/A
2025-03-28 18:25:10 -04:00
Marshall Bowers
3e400a4969 zed: Fix package.metadata.bundle-dev key (#27704)
This PR fixes the `package.metadata.bundle-dev` key in the `zed` crate's
`Cargo.toml`.

It seems this was inadvertently changed in
https://github.com/zed-industries/zed/pull/27126.

Nightly builds are currently failing with:

```
error: `cargo metadata` exited with an error: error: invalid table header
duplicate key `bundle` in table `package.metadata`
   --> Cargo.toml:173:1
    |
173 | [package.metadata.bundle]
    | ^
    |

   0: backtrace::capture::Backtrace::create
   1: error_chain::backtrace::imp::InternalBacktrace::new
   2: <error_chain::State as core::default::Default>::default
   3: cargo_bundle::bundle::settings::load_metadata
   4: cargo_bundle::bundle::settings::Settings::new
   5: cargo_bundle::main
   6: std::sys::backtrace::__rust_begin_short_backtrace
   7: std::rt::lang_start::{{closure}}
   8: std::rt::lang_start_internal
   9: _main
```

Release Notes:

- N/A
2025-03-28 22:15:11 +00:00
Kirill Bulatov
e11e7df724 Restore editor state on reopen (#27672)
Closes https://github.com/zed-industries/zed/issues/11626
Part of https://github.com/zed-industries/zed/issues/12853

`"restore_on_file_reopen": true` in workspace settings can now be used
to enable and disable editor data between file reopens in the same pane:


https://github.com/user-attachments/assets/8d938ee1-d854-42a8-bbc3-2a4e4d7d5933

The settings are generic and panes' data store can be extended for
further entities, beyond editors.

---------------
Impl details: 

Currently, the project entry IDs seem to be stable across file reopens,
unlike BufferIds, so those were used.
Originally, the DB data was considered over in-memory one as editors
serialize their state anyway, but managing and exposing PaneIds out of
the DB is quite tedious and joining the DB data otherwise is not
possible.


Release Notes:

- Started to restore editor state on reopen
2025-03-28 22:04:16 +00:00
Felix Packard
bbd1e628f0 Fix GPUI keyup events not firing on Windows and macOS (#27290)
While building my own application using GPUI, I found that the `key_up`
event doesn't fire on Windows or macOS, with each platform failing for
different reasons. These events aren't used anywhere in Zed yet, so it
makes sense that the issue hasn't already been caught.

I don't have a Linux machine set up right now, so I don't know if these
events fire correctly on Linux or not.

---

Without this fix, a simple layout like the following:

```rust
div()
    .on_key_down(cx.listener(|_, event, _, _| println!("Key down: {:?}", event)))
    .on_key_up(cx.listener(|_, event, _, _| println!("Key up: {:?}", event)));
```

...would result in the following logs if the 'a' key was pressed:

```text
Key down: KeyDownEvent { keystroke: Keystroke { modifiers: Modifiers { control: false, alt: false, shift: false, platform: false, function: false }, key: "a", key_char: Some("a") }, is_held: false }
<eof>
```

With this fix, the `key_up` event fires correctly, resulting in the
following logs:

```text
Key down: KeyDownEvent { keystroke: Keystroke { modifiers: Modifiers { control: false, alt: false, shift: false, platform: false, function: false }, key: "a", key_char: Some("a") }, is_held: false }
Key up: KeyUpEvent { keystroke: Keystroke { modifiers: Modifiers { control: false, alt: false, shift: false, platform: false, function: false }, key: "a", key_char: None } }
<eof>
```

---

I've made the assumption that the `key_char` field shouldn't be set on
the `key_up` event since, unlike the `key_down` event, it's not an event
that may produce a character.

Happy to make any changes to this PR as required. If it would be
preferable to test this on Linux as well before it's merged, let me know
and I'll sort something out.

Hopefully this makes the experience of building new applications on GPUI
smoother, and potentially saves the Zed team some time if this event is
ever used in the future.

Release Notes:

- N/A
2025-03-28 14:39:15 -07:00
tidely
01b400ea29 gpui: Implement From trait for Clipboard related structs (#27585)
Implement the From trait for some simple conversations between Clipboard
related structs.

This PR only adds the From trait implementations and doesn't touch any
code. In a future PR we can simplify usage throughout the codebase, such
as:

```rust
// impl ClipboardString
fn new(text: String) -> Self {
    Self::from(text)
}
```

Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-03-28 14:38:05 -07:00
Max Brunsfeld
9445005bff Don't consider empty deleted files to be dirty or conflicting (#27701)
When a file is deleted outside of Zed, but it doesn't have any unsaved
changes, it shouldn't be considered "dirty" (prompting you before you
close it).

Release Notes:

- Fixed an bug where unchanged buffers were marked as conflicting if
their files were deleted outside of Zed.

---------

Co-authored-by: Antonio <antonio@zed.dev>
2025-03-28 14:17:16 -07:00
Peter Finn
5c0adde7bb Correct other end visual block functionality (#27678)
Closes #27385

Builds on #27604 so that `vim::OtherEnd` works in visual block mode.
This is accomplished by reversing the order of active selections in the
buffer when the user hit `o`, so that the cursor moves diagonally across
the selection. The current behavior is preserved for `shift-o`, which is
how the cursors behave in vim.

We'll close #27604 since this encapsulates that change, but if you'd
prefer to take only the visual block motion component, we'll keep the
branch for #27604 open.

Test case: growing a box down and to the right, other ending, followed
by growing and shrinking the box:


https://github.com/user-attachments/assets/1df544e1-efce-4354-b354-bbfec007a7df

Test case: growing a box up and to the left, other ending, followed by
growing and shrinking the box:


https://github.com/user-attachments/assets/2f6d7729-c63a-4486-960b-23474c2e507a



Release Notes:
- Improved visual block mode when cursor is at beginning of selection
- Improved visual block mode so that `o` and `shift-o` reach parity with
vim

---------

Co-authored-by: KyleBarton <kjbarton4@gmail.com>
2025-03-28 20:52:38 +00:00
Conrad Irwin
4a5c492188 If GIT_ASKPASS is already set, assume it will do the right thing (#27681)
Fixes running git push on a coder instance.

Closes #ISSUE

Release Notes:

- Zed will now use `GIT_ASKPASS` if you already have one set instead of
overriding with our own. Fixes `git push` in Coder.
2025-03-28 14:50:05 -06:00
Conrad Irwin
4da987dad4 Delete test file (#27697)
Closes #ISSUE

Release Notes:

- N/A
2025-03-28 14:49:50 -06:00
Ishige
be657aefa3 terminal: Make IME work with tab and enter keys (#27572)
… in the terminal.

Closes #23003

Release Notes:

- N/A

## Before fix:


https://github.com/user-attachments/assets/249ec62d-1461-4551-87b2-4259dba171f2


## After fix:



https://github.com/user-attachments/assets/2db624a0-8035-4260-9b2e-0cee83662b84
2025-03-28 20:44:02 +00:00
Marshall Bowers
d0ae604eda collab: Switch to new encryption format for access tokens (#27691)
This PR switches collab over to start minting access tokens using the
new OAEP-based encryption format.

This is a follow-up to #15058 where we added support for this new
encryption format.

Clients that are newer than 8 months ago should be able to decrypt the
new access tokens. It is only clients older than 8 months ago that will
no longer be supported.

Release Notes:

- N/A
2025-03-28 16:19:25 -04:00
Conrad Irwin
08bb17a7eb Allow Trash to fallback to Delete on failure (#27682)
This fixes trashing files from the git panel on SSH remotes that don't
run a Desktop environment.

Release Notes:

- Fix trash to work on remotes with no desktop environment configured
2025-03-28 14:14:29 -06:00
Anthony Eid
55c1f9d26c Debugger: Switch Breakpoint Anchor from left to right (#27688)
This fixes an edge case where some breakpoints would not render
correctly when expanding a conflict git hunk.

## Before 


https://github.com/user-attachments/assets/4fd75ef6-8381-4f9e-9765-5eeb3a734df0

## After


https://github.com/user-attachments/assets/b2b49894-2dc2-42ba-8038-504a1b4c2665


Release Notes:

- N/A

Co-authored-by: Conrad <conrad@zed.dev>
2025-03-28 20:04:44 +00:00
Anthony Eid
28f0ba3381 Debugger: Basic breakpoint improvements (#27687)
This PR does three things

- Right clicking within the gutter outside of the gutter fold area
bounds opens a breakpoint context menu
- Disabled breakpoints are now outline with the debugger accent color
instead of being fully colored at half opacity
- Clicking a breakpoint acts differently now
- Clicking a breakpoint while holding the platform modifier key will
disable/enable it
- Clicking a breakpoint hint while holding the platform modifier key
will set a disabled breakpoint
- Clicking a disabled breakpoint will enable it instead of deleting it

Release Notes:

- N/A
2025-03-28 19:55:09 +00:00
Marshall Bowers
b5dc09c0ca Remove unneeded anonymous lifetimes from gpui::Context (#27686)
This PR removes a number of unneeded anonymous lifetimes from usages of
`gpui::Context`.

Release Notes:

- N/A
2025-03-28 19:26:30 +00:00
Marshall Bowers
e90411efa2 gpui: Remove unneeded anonymous lifetime from Render::render (#27684)
This PR removes an unneeded anonymous lifetime from the `cx` parameter
to `Render::render`.

This makes it so the anonymous lifetime doesn't show up when
implementing the `Render` trait via a code action:

#### Before

```rs
struct Foo;

impl Render for Foo {
    fn render(&mut self, window: &mut Window, cx: &mut Context<'_, Self>) -> impl IntoElement {
        todo!()
    }
}
```

#### After

```rs
struct Foo;

impl Render for Foo {
    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
        todo!()
    }
}
```

Release Notes:

- N/A
2025-03-28 19:19:20 +00:00
Bennet Bo Fenner
fcadcbb510 assistant2: Make context pills clickable (#27680)
Release Notes:

- N/A
2025-03-28 19:05:30 +00:00
Antonio Scandurra
94ed0b7767 Allow reviewing of agent changes without Git (#27668)
Release Notes:

- N/A
2025-03-28 18:58:53 +00:00
Mikayla Maki
8a307e7b89 Switch fully to Rust Livekit (redux) (#27126)
Swift bindings BEGONE

Release Notes:

- Switched from using the Swift LiveKit bindings, to the Rust bindings,
fixing https://github.com/zed-industries/zed/issues/9396, a crash when
leaving a collaboration session, and making Zed easier to build.

---------

Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
Co-authored-by: Michael Sloan <michael@zed.dev>
2025-03-28 17:58:23 +00:00
Marshall Bowers
c8fb95cd1b assistant2: Add keybinding for profile selector (#27674)
This PR adds a keybinding to toggle the profile selector.

Defaults to `Cmd-I` on macOS and `ctrl-I` on Linux/Windows.

Release Notes:

- N/A
2025-03-28 17:19:37 +00:00
Richard Feldman
55d934a3be Show tool name for Unknown tool (#27667)
Right now we can't see what tool name the model was trying to run.

Release Notes:

- N/A
2025-03-28 13:13:15 -04:00
Bennet Bo Fenner
a916bbf00c assistant2: Add support for referencing symbols as context (#27513)
TODO

Release Notes:

- N/A
2025-03-28 16:56:14 +00:00
Agus Zubiaga
da47013e56 assistant2: Agent notification improvements (#27638)
- Show thread's summary in notification title
- Improve thread's summary prompt so it's more descriptive
- Make whole notification clickable


![image](https://github.com/user-attachments/assets/f29da109-f16e-40af-bb43-0882403535c5)

Release Notes:

- N/A
2025-03-28 16:30:48 +00:00
Joseph T. Lyons
82ce187bfc Preserve newlines in release notes email (#27670)
Release Notes:

- N/A
2025-03-28 11:42:37 -04:00
Marshall Bowers
e843790490 assistant2: Rework profile list (#27669)
This PR reworks the profile list to make it match the designs more
closely:


https://github.com/user-attachments/assets/3cd9cad4-771c-4231-ba9b-ddca72ff617c

We're no longer using a `Picker` and are instead using a custom
navigable list.

Also added an option to add a new profile.

Release Notes:

- N/A
2025-03-28 15:37:36 +00:00
Marshall Bowers
83f3fbfef5 assistant2: Add header to fork profile modal (#27666)
This PR adds a header to the fork profile view in the profile management
modal:

<img width="575" alt="Screenshot 2025-03-28 at 10 29 10 AM"
src="https://github.com/user-attachments/assets/8c0012e5-3297-4ba6-9603-0364f5b6f3b7"
/>

Release Notes:

- N/A
2025-03-28 14:44:08 +00:00
Richard Feldman
60a7455f12 assistant2: Don't scroll down if user has scrolled up (#27614)
This caused undesirable and unnecessary scrolling, in that if you scroll
up, then as new messages stream into the panel, they jump the scroll bar
back down.

@agu-z and I paired on this and we think this is unnecessary now that we
don't see the Edit button in the UI, but @bennetbo we still see code for
the button in there, so...maybe we do still want this? (If so, we can
revert the second commit and go back to a more conditional way of
scrolling. 😄)

Release Notes:

- N/A

---------

Co-authored-by: Agus <agus@zed.dev>
2025-03-28 10:32:31 -04:00
Marshall Bowers
4315b2fc8a assistant2: Reload profile when making changes to the active profile's tools (#27664)
This PR makes it so the profile is reloaded when making changes to the
active profile's tools.

Release Notes:

- N/A
2025-03-28 14:08:42 +00:00
Marshall Bowers
68d453da52 assistant: Remove /search (#27661)
This PR removes the `/search` command.

This was feature-flagged and was never released to the general public.

Release Notes:

- N/A
2025-03-28 13:40:31 +00:00
Marshall Bowers
20eab9038f assistant: Remove /project (#27660)
This PR removes the `/project` command.

This was feature-flagged and was never released to the general public.

Release Notes:

- N/A
2025-03-28 13:08:16 +00:00
loczek
a201263448 storybook: Fix auto_height_editor story (#27653)
This is a fix for this error when trying to run `auto_height_editor`
story:

```sh
thread 'main' panicked at C:\Users\x\dev\zed\crates\settings\src\settings_store.rs:363:32:
unregistered setting type workspace::workspace_settings::WorkspaceSettings
```

Release Notes:

- N/A
2025-03-28 08:42:44 -04:00
Marshall Bowers
1a4ba59d4e docs: Fix #windows Discord channel name (#27659)
This PR fixes the name of the `#windows` Discord channel to match what
the channel is actually called.

Release Notes:

- N/A
2025-03-28 08:41:07 -04:00
Piotr Osiewicz
24ad97008b language server: Fix restarts sometimes not working for buffers open in go-to-definition view (#27655)
Closes #ISSUE

Release Notes:

- Fixed language server restarts sometimes not restarting a language
server.
2025-03-28 11:46:46 +00:00
Bennet Bo Fenner
f6c81a0595 assistant2: Unify path rendering for file context (#27537)
This ensures that we render path matches the same in both the file
context picker and the messag editor completion provider.

Release Notes:

- N/A
2025-03-28 11:26:54 +01:00
Smit Barmase
7ac51e4c82 project: Add tests for more cases in LSP completions (#27650)
This PR adds two more cases to existing LSP completion cases.

- When text_edit exists:  (New test)
   1. we use text_edit, over insert_text and label

- When edit range exists (and text_edit is None):  (New test)
   1. insert_text is used over label if exists
   2. label is used otherwise

- When not edit range exists (and text_edit is None):  (Existing test)
   1. insert_text is used over label if exists
   2. label is used otherwise

Release Notes:

- N/A
2025-03-28 15:43:18 +05:30
mslzed
c8105863c8 Update docs to include Windows JD (#27649)
Closes #ISSUE

Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-03-28 02:57:07 -07:00
张小白
9f72e05c40 windows: Fix extension uninstall (#18467)
Closes #18153


When calling `uninstall_extension`, the `work_dir` associated with this
`extension` doesn't have its corresponding `FileHandle` properly closed,
preventing the deletion of the `work_dir`. As seen in the image below,
after installing the `toml` extension, `zed.exe` holds two `Handle`s for
the folder `C:\Users\36477\AppData\Local\Zed\extensions\work\toml`.

![Screenshot 2024-09-27
171149](https://github.com/user-attachments/assets/f75f3f6f-9a62-43b5-9450-73ee1ed8e7f9)


Therefore, after deleting `extension_dir` and then calling
`this.update(...)`, `zed.exe` releases these two `Handles`, and only
then can the folder
`C:\Users\36477\AppData\Local\Zed\extensions\work\toml` be deleted. See
the corresponding file handles are closed after calling
`this.update(...)`:

![Screenshot 2024-09-28
132823](https://github.com/user-attachments/assets/476e0494-850a-4af5-b351-899e60ae98f7)

However, if there is a running server of the extension, the error will
persist. At this point, I haven’t found a direct way to terminate all
running servers of the extension. Since this feature might affect the
`LspStore` structure, I paused my work here.


See when `toml` extension is running, we can not delete
`C:\Users\36477\AppData\Local\Zed\extensions\work\toml` since
`C:\Users\36477\AppData\Local\Zed\extensions\work\toml\taplo.exe` is
still running:

![Screenshot 2024-09-28
134709](https://github.com/user-attachments/assets/6801d6e2-2a44-4103-8570-467c507e6e20)



cc @ConradIrwin You're the expert in this area—what are your thoughts?



Release Notes:

- N/A
2025-03-28 13:52:48 +08:00
Marshall Bowers
bb15f4c493 assistant2: Fix tool picker appearance inside modal (#27635)
This PR fixes the tool picker appearance now that there is a header.

Release Notes:

- N/A
2025-03-28 00:10:32 +00:00
Shardul Vaidya
dc6004066d bedrock: Support DeepSeek r1 (#27495)
Release Notes:

- Added support for DeepSeek R1 hosted on AWS Bedrock.

---------

Co-authored-by: Marshall Bowers <git@maxdeviant.com>
2025-03-27 19:30:30 -04:00
Marshall Bowers
2863bd1836 icons: Remove ZedAssistant2 icon (#27634)
This PR removes the `ZedAssistant2` icon.

I went to use it as a placeholder icon, but noticed that the icon wasn't
loaded properly due to a name mismatch.

However, since we aren't using it anywhere I'm opting to remove it.

Release Notes:

- N/A
2025-03-27 23:10:42 +00:00
Marshall Bowers
f1ce83b533 context_server: Only look for context server project settings in visible worktrees (#27633)
This PR fixes an issue where we were looking for context server project
settings in _all_ worktrees, not just visible ones.

This meant that if you had a single file worktree open (e.g.,
`settings.json`) this could impact whether context servers defined in
project settings would load.

Release Notes:

- Fixed an issue where context servers defined in project settings would
not be respected in some scenarios.

Co-authored-by: Wilhelm Klopp <wil.klopp@gmail.com>
2025-03-27 22:58:29 +00:00
Richard Feldman
edc7d73643 Set cache breakpoint on second-to-last message (#27632)
Here's a sample `dbg!` of token usage after this change, for a small
agent thread:

```
[crates/assistant2/src/thread.rs:1092:25] &usage = TokenUsage {
    input_tokens: 5354,
    output_tokens: 184,
    cache_creation_input_tokens: 0,
    cache_read_input_tokens: 0,
}
[crates/assistant2/src/thread.rs:1092:25] &usage = TokenUsage {
    input_tokens: 54,
    output_tokens: 132,
    cache_creation_input_tokens: 5518,
    cache_read_input_tokens: 0,
}
[crates/assistant2/src/thread.rs:1092:25] &usage = TokenUsage {
    input_tokens: 54,
    output_tokens: 113,
    cache_creation_input_tokens: 166,
    cache_read_input_tokens: 5518,
}
[crates/assistant2/src/thread.rs:1092:25] &usage = TokenUsage {
    input_tokens: 291,
    output_tokens: 181,
    cache_creation_input_tokens: 147,
    cache_read_input_tokens: 5684,
}
```

Release Notes:

- N/A
2025-03-27 22:32:50 +00:00
Piotr Osiewicz
4839195003 debugger: Remove fake adapter and un-gate GDB (#27557)
This is a clean-up PR in anticipation of introduction of Debugger
Registry. I wanna get rid of DebugAdapterKind (or rather, it being an
enum).
Release Notes:

- N/A

---------

Co-authored-by: Anthony Eid <hello@anthonyeid.me>
Co-authored-by: Anthony <anthony@zed.dev>
2025-03-27 22:31:58 +00:00
Richard Feldman
56eb650f09 Add Batch tool call for calling multiple tools (#27621)
<img width="620" alt="Screenshot 2025-03-27 at 2 29 13 PM"
src="https://github.com/user-attachments/assets/dd023507-61bc-4722-a095-f65f4b6c746a"
/>

We'll iterate on the UI, but first the goal is to just get it to work at
all so we can see if it's useful in terms of getting correct output
faster.

Release Notes:

- N/A

---------

Co-authored-by: Agus Zubiaga <hi@aguz.me>
2025-03-27 18:21:26 -04:00
Richard Feldman
61be869352 Add Open Tool (#27499)
I've seen models try to run `open` in Bash. This is a cross-platform
version of that.

<img width="634" alt="Screenshot 2025-03-26 at 10 27 40 AM"
src="https://github.com/user-attachments/assets/b18cb50f-6e2f-4770-b15c-1040916a420a"
/>

Release Notes:

- N/A
2025-03-27 18:20:59 -04:00
Richard Feldman
7537f0557f Automatically hide "View Panel" notification after refocusing Zed (#27512)
Now if you refocus Zed manually (e.g. cmd-tab), we hide the "View Panel"
notification automatically.

This also fixes a related subscription leak.

Release Notes:

- N/A
2025-03-27 18:20:45 -04:00
Richard Feldman
85740ddaa4 Make serialization backwards-compatible for collab server (#27626)
Sets up the collab server to accept the format of system message that
we'll introduce later for [prompt
caching](https://docs.anthropic.com/en/docs/build-with-claude/prompt-caching).

Release Notes:

- N/A
2025-03-27 18:20:10 -04:00
Finn Evers
6550a96e15 editor: Remove gap between gutter and horizontal scrollbar track (#24887)
Longer write-up, sorry if this got a bit too long.

This PR removes a small gap between the editor gutter and the horizontal
scrollbar, if present, by stretching the scrollbar track the entire
witdth of the editor.


https://github.com/user-attachments/assets/d5c18b03-d1ff-4d48-a3da-5d0fb80ee967

This gap which can be seen in the bottom left of the video can cause
bugs when interacting with it using the cursor, as accidentally clicking
on it would trigger a vertical scroll instead of dragging the horizontal
scroll. Also for cases where themes provide a non-transparent scrollbar
track background, which can be seen in the video, the small gap is
visible whilst scrolling horizontally.

This gap is present because the horizontal editor scrollbar is layouted
based upon the `content_origin`, which offsets the whole layout by the
horizontal gutter margin to the right. However, the scrollbar should be
layouted based upon the editor text bounds to be properly painted over
the entire editor text hitbox.

Here are some comparison images with `scrollbar.track.background` and
`gutter.background` set to red for visibility.

| | Current `main` | With this change | 
| - | - | - |
| Default position / Fully scrolled to the left | <img width="842"
alt="left_main"
src="https://github.com/user-attachments/assets/8b053fc8-5271-4b58-8404-dcabf49bf702"
/> | <img width="842" alt="left_fix"
src="https://github.com/user-attachments/assets/459df723-05d5-4813-a6a4-038f7d662495"
/> |
| Scrolled to the right | <img width="216" alt="scroll_main"
src="https://github.com/user-attachments/assets/9c1fcc0d-fbb4-49af-9645-f258f5a7217b"
/> | <img width="216" alt="scroll_fix"
src="https://github.com/user-attachments/assets/8dd2e585-7802-415b-a05a-fb40a882323e"
/> |

---

#### Small downsight of this approach

Currently, the scrollbar thumb aligns with the indent guides if the
editor is fully scrolled to the left and the track background is
transparent. This is because the indent guides are layouted according to
the content margin.
With this change, however, the scrollbar thumb will shift a few pixels
to the left and will overlap the indent guides if present.

| Current `main` | With this change |
| - | - |
| <img width="295" alt="cur_indent"
src="https://github.com/user-attachments/assets/92753951-6f35-4c39-94eb-21c445f8d2f5"
/> | <img width="381" alt="fix_indent"
src="https://github.com/user-attachments/assets/899d945c-49f8-4117-bc48-52501d55cc33"
/> |

To circumvent this, the scrollbar thumb could be layouted with a small
offset so that the thumb aligns properly with the indent guides whilst
the scrollbar track spans the whole editor width. This would lead to
some questions on how to account for the gap during layouting and
dragging of the thumb though, but might work for a gap that small. Happy
to implement this fix, should that be preferred 😄

(VSCode does not have the indent guide issue, as they do not layout the
text in the editor with any offset unlike Zed does)

Release Notes:

- Removed a small gap between the editor gutter and horizontal
scrollbar.
2025-03-28 03:36:49 +05:30
Peter Tripp
93c0056065 git: Display author not committer in git popover (#27628)
Display Author name/email instead of Committer name/email in git
popover.

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

Prior to v0.173.x, Zed displayed Author name. Regression introduced in:
- https://github.com/zed-industries/zed/pull/24593

Release Notes:

- git: Switch to displaying Git author instead of Git committer in Git
Blame popover.
2025-03-27 17:35:27 -04:00
Smit Barmase
84dd2366bc project: Fix LSP completion to use insertText when constructing default edits (#27630)
Closes #25761 #21603

When `text_edit` is not available we directly fallback to to `label`.
That means, when we have range to replace, we never use `insertText` and
only use it when we haven't found any range.

This PR fixes, this and uses `insertText` as fallback first, and then
`label`.

Release Notes:

- Fixed an issue where accepting LSP snippet completion would insert the
label instead of expanding the snippet.
2025-03-28 03:00:48 +05:30
Ben Kunkle
8e12eb0ab1 keymap: Detect and report errors for uppercase keybindings (#27558)
Closes #25353

Detect keybindings using upper case instead of lowercase, and report an
error

Release Notes:

- N/A
2025-03-27 21:17:43 +00:00
Marshall Bowers
3b158461be assistant2: Add support for forking existing profiles (#27627)
This PR adds support for forking existing profiles from the manage
profiles modal.


https://github.com/user-attachments/assets/5fa9b76c-fafe-4c72-8843-576c4b5ca2f2

Release Notes:

- N/A
2025-03-27 20:17:42 +00:00
Peter Tripp
12a8b850ef Fix Terminal theming issue with background/foreground text (#27617)
- Closes: https://github.com/zed-industries/zed/issues/27427

Release Notes:

- terminal: Fixed an issue where editor theme colors (`text`,
`background`) were incorrectly being uses instead of terminal theme
colors (`terminal.{foreground,background}`)
2025-03-27 16:12:19 -04:00
Michael Sloan
d35f5a4197 Opt-in to markdown parser options and check all options were considered (#27623)
Release Notes:

- N/A
2025-03-27 20:10:06 +00:00
Danilo Leal
1f5d57bece assistant2: Add docs to the notify_when_agent_waiting setting (#27622)
Release Notes:

- N/A
2025-03-27 17:00:59 -03:00
Finn Evers
ca9fb2399e Prevent toggle_dock from opening assistant panel when it is disabled via settings (#27215)
Part of #27171

Follows-up the change in
https://github.com/zed-industries/zed/pull/22346 to consider the case
where the assistant-panel is disabled via settings (as also noted in
[this
comment](https://github.com/zed-industries/zed/pull/22346#issuecomment-2558372412),
Notably, only the explicit case is considered here. Can extend this
change to also cover the implicit case where the button is disabled if
requested.).

Currently, if the user toggles the right dock, the assistant panel will
be shown even if it is disabled via settings, because it has the highest
priority (see
https://github.com/zed-industries/zed/pull/22346#issuecomment-2564890493).
With this change, the assistant panel is no longer activated when
disabled and the dock with the next highest activation order is
activated instead.

I did not opt in to make the priority configurabe, as I agree with
https://github.com/zed-industries/zed/pull/22346#issuecomment-2564890493
that this will most likely rarely be used (the active panel is only none
on the first toggle of the dock, afterwards it remains set for the
remainder of the session).

Release Notes:

- `workspace::ToggleRightDock` will no longer open the assistant panel
when it is disabled via settings.
2025-03-27 19:19:37 +00:00
Ben Kunkle
a360365410 tsx: Insert newline between open and close tags on enter (#27618)
Release Notes:

- Added support for automatically inserting a newline when hitting enter
between opening and closing tags in JSX/TSX
2025-03-27 19:15:12 +00:00
Danilo Leal
df6ee1fc4a assistant2: Add adjustments to OS notification (#27615)
This PR hardcodes the font size for the OS notification and adjusts the
copywriting on the `DoneStreaming` scenario.

1. Reason for the former change is because notifications always have a
fixed width and height, so any responsive design strategy here wouldn't
fully work.
2. Reason for the latter is because when the assistant response is done
streaming, that _can_ mean "changes have been applied" (previous label)
but it can also not mean that. So, I'm making it more generic now.

Release Notes:

- N/A
2025-03-27 15:36:02 -03:00
Richard Feldman
fc99557952 Explicitly prefer find-replace over edit-files (#27503)
`edit-files` is still enabled for now, but this makes it less likely to
be used.

Release Notes:

- N/A
2025-03-27 14:27:28 -04:00
Richard Feldman
52c1e0021c Allow Bash tool to Just Work with more cd inputs (#27501)
Release Notes:

- N/A
2025-03-27 14:27:18 -04:00
Marshall Bowers
6ead57d5ed assistant2: Fix navigation between states in manage profiles modal (#27613)
This PR fixes the navigation between states in the manage profiles
modal, specifically around dismissal.

Release Notes:

- N/A
2025-03-27 17:43:25 +00:00
Marshall Bowers
4a10a0ca77 feature_flags: Remove predict-edits-non-eager-mode feature flag (#27610)
This PR removes the `predict-edits-non-eager-mode` feature flag.

The feature is shipped, and we aren't referencing the flag anywhere
anymore.

Release Notes:

- N/A
2025-03-27 17:25:01 +00:00
Marshall Bowers
cc6d4e3c62 assistant: Remove /auto (#27608)
This PR removes the `/auto` command.

This was feature-flagged and was never released to the general public.

Release Notes:

- N/A
2025-03-27 17:23:32 +00:00
Danilo Leal
3f7c8c97c2 docs: Add improvements to the Completions page (#27612)
Release Notes:

- N/A
2025-03-27 14:18:29 -03:00
Marshall Bowers
ab5ba66b94 feature_flags: Remove predict-edits feature flag (#27605)
This PR removes the `predict-edits` feature flag.

The feature is shipped, and we aren't referencing the flag anywhere
anymore.

Release Notes:

- N/A
2025-03-27 17:01:27 +00:00
Marshall Bowers
a20a534ecf assistant2: Allow dismissing the tool list with the keyboard (#27603)
This PR adds the ability to dismiss the tool list in the profile
configuration modal using the keyboard.

Release Notes:

- N/A
2025-03-27 16:53:59 +00:00
Danilo Leal
5bb979820b docs: Fix link to the subtle mode heading (#27606)
Release Notes:

- N/A
2025-03-27 13:51:32 -03:00
532 changed files with 24142 additions and 22574 deletions

View File

@@ -209,7 +209,6 @@ jobs:
cargo check -p workspace
cargo build -p remote_server
cargo check -p gpui --examples
script/check-rust-livekit-macos
# Since the macOS runners are stateful, so we need to remove the config file to prevent potential bug.
- name: Clean CI config file

View File

@@ -59,7 +59,9 @@ jobs:
if: steps.check-promotion-from-preview.outputs.was_promoted_from_preview == 'true'
run: |
TAG="${{ github.event.release.tag_name }}"
echo \"${{ toJSON(github.event.release.body) }}\" > release_body.txt
cat << 'EOF' > release_body.txt
${{ github.event.release.body }}
EOF
jq -n --arg tag "$TAG" --rawfile body release_body.txt '{version: $tag, markdown_body: $body}' \
> release_data.json
curl -X POST "https://zed.dev/api/send_release_notes_email" \

283
Cargo.lock generated
View File

@@ -413,7 +413,6 @@ dependencies = [
"menu",
"multi_buffer",
"parking_lot",
"paths",
"pretty_assertions",
"project",
"prompt_library",
@@ -423,7 +422,6 @@ dependencies = [
"rope",
"schemars",
"search",
"semantic_index",
"serde",
"serde_json_lenient",
"settings",
@@ -453,12 +451,14 @@ dependencies = [
"assistant_slash_command",
"assistant_tool",
"async-watch",
"buffer_diff",
"chrono",
"client",
"clock",
"collections",
"command_palette_hooks",
"context_server",
"convert_case 0.8.0",
"db",
"editor",
"feature_flags",
@@ -467,7 +467,6 @@ dependencies = [
"futures 0.3.31",
"fuzzy",
"git",
"git_ui",
"gpui",
"heed",
"html_to_markdown",
@@ -483,6 +482,7 @@ dependencies = [
"markdown",
"menu",
"multi_buffer",
"ordered-float 2.10.1",
"parking_lot",
"paths",
"picker",
@@ -493,6 +493,7 @@ dependencies = [
"rand 0.8.5",
"release_channel",
"rope",
"schemars",
"serde",
"serde_json",
"settings",
@@ -580,6 +581,7 @@ dependencies = [
"client",
"collections",
"context_server",
"dap",
"env_logger 0.11.7",
"fs",
"futures 0.3.31",
@@ -669,14 +671,10 @@ dependencies = [
"http_client",
"indexed_docs",
"language",
"language_model",
"log",
"pretty_assertions",
"project",
"prompt_store",
"rope",
"schemars",
"semantic_index",
"serde",
"serde_json",
"settings",
@@ -695,6 +693,8 @@ name = "assistant_tool"
version = "0.1.0"
dependencies = [
"anyhow",
"async-watch",
"buffer_diff",
"clock",
"collections",
"derive_more",
@@ -706,6 +706,9 @@ dependencies = [
"project",
"serde",
"serde_json",
"settings",
"text",
"util",
]
[[package]]
@@ -715,6 +718,7 @@ dependencies = [
"anyhow",
"assistant_tool",
"chrono",
"clock",
"collections",
"feature_flags",
"futures 0.3.31",
@@ -724,8 +728,12 @@ dependencies = [
"itertools 0.14.0",
"language",
"language_model",
"log",
"lsp",
"open",
"project",
"rand 0.8.5",
"regex",
"release_channel",
"schemars",
"serde",
@@ -1883,7 +1891,7 @@ dependencies = [
"bitflags 2.9.0",
"cexpr",
"clang-sys",
"itertools 0.10.5",
"itertools 0.12.1",
"lazy_static",
"lazycell",
"log",
@@ -1906,13 +1914,31 @@ dependencies = [
"bitflags 2.9.0",
"cexpr",
"clang-sys",
"itertools 0.10.5",
"itertools 0.12.1",
"proc-macro2",
"quote",
"regex",
"rustc-hash 1.1.0",
"shlex",
"syn 2.0.100",
]
[[package]]
name = "bindgen"
version = "0.71.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f58bf3d7db68cfbac37cfc485a8d711e87e064c3d0fe0435b92f7a407f9d6b3"
dependencies = [
"bitflags 2.9.0",
"cexpr",
"clang-sys",
"itertools 0.12.1",
"log",
"prettyplease",
"proc-macro2",
"quote",
"regex",
"rustc-hash 1.1.0",
"rustc-hash 2.1.1",
"shlex",
"syn 2.0.100",
]
@@ -2301,10 +2327,10 @@ dependencies = [
"fs",
"futures 0.3.31",
"gpui",
"gpui_tokio",
"http_client",
"language",
"livekit_client",
"livekit_client_macos",
"log",
"postage",
"project",
@@ -2567,6 +2593,15 @@ version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
[[package]]
name = "cgl"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ced0551234e87afee12411d535648dd89d2e7f34c78b753395567aff3d447ff"
dependencies = [
"libc",
]
[[package]]
name = "channel"
version = "0.1.0"
@@ -2723,7 +2758,7 @@ dependencies = [
"anyhow",
"clap",
"collections",
"core-foundation 0.9.4",
"core-foundation 0.10.0",
"core-services",
"exec",
"fork",
@@ -2923,6 +2958,7 @@ dependencies = [
"git_ui",
"google_ai",
"gpui",
"gpui_tokio",
"hex",
"http_client",
"hyper 0.14.32",
@@ -2932,7 +2968,6 @@ dependencies = [
"language_model",
"livekit_api",
"livekit_client",
"livekit_client_macos",
"log",
"lsp",
"menu",
@@ -3356,6 +3391,19 @@ dependencies = [
"libc",
]
[[package]]
name = "core-graphics2"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7e4583956b9806b69f73fcb23aee05eb3620efc282972f08f6a6db7504f8334d"
dependencies = [
"bitflags 2.9.0",
"block",
"cfg-if",
"core-foundation 0.10.0",
"libc",
]
[[package]]
name = "core-services"
version = "0.2.1"
@@ -3367,16 +3415,30 @@ dependencies = [
[[package]]
name = "core-text"
version = "20.1.0"
version = "21.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c9d2790b5c08465d49f8dc05c8bcae9fea467855947db39b0f8145c091aaced5"
checksum = "a593227b66cbd4007b2a050dfdd9e1d1318311409c8d600dc82ba1b15ca9c130"
dependencies = [
"core-foundation 0.9.4",
"core-graphics 0.23.2",
"core-foundation 0.10.0",
"core-graphics 0.24.0",
"foreign-types 0.5.0",
"libc",
]
[[package]]
name = "core-video"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d45e71d5be22206bed53c3c3cb99315fc4c3d31b8963808c6bc4538168c4f8ef"
dependencies = [
"block",
"core-foundation 0.10.0",
"core-graphics2",
"io-surface",
"libc",
"metal",
]
[[package]]
name = "core_maths"
version = "0.1.1"
@@ -3874,6 +3936,7 @@ dependencies = [
"node_runtime",
"parking_lot",
"paths",
"regex",
"schemars",
"serde",
"serde_json",
@@ -3907,7 +3970,6 @@ dependencies = [
"regex",
"serde",
"serde_json",
"sysinfo",
"task",
"util",
]
@@ -4641,6 +4703,7 @@ dependencies = [
"client",
"clock",
"collections",
"dap",
"env_logger 0.11.7",
"feature_flags",
"fs",
@@ -5074,12 +5137,12 @@ checksum = "f81ec6369c545a7d40e4589b5597581fa1c441fe1cce96dd1de43159910a36a2"
[[package]]
name = "font-kit"
version = "0.14.1"
source = "git+https://github.com/zed-industries/font-kit?rev=40391b7#40391b7c0041d8a8572af2afa3de32ae088f0120"
source = "git+https://github.com/zed-industries/font-kit?rev=5474cfad4b719a72ec8ed2cb7327b2b01fd10568#5474cfad4b719a72ec8ed2cb7327b2b01fd10568"
dependencies = [
"bitflags 2.9.0",
"byteorder",
"core-foundation 0.9.4",
"core-graphics 0.23.2",
"core-foundation 0.10.0",
"core-graphics 0.24.0",
"core-text",
"dirs 5.0.1",
"dwrote",
@@ -5277,7 +5340,7 @@ name = "fsevent"
version = "0.1.0"
dependencies = [
"bitflags 2.9.0",
"core-foundation 0.9.4",
"core-foundation 0.10.0",
"fsevent-sys 3.1.0",
"parking_lot",
"tempfile",
@@ -5803,7 +5866,7 @@ dependencies = [
"ashpd",
"async-task",
"backtrace",
"bindgen 0.70.1",
"bindgen 0.71.1",
"blade-graphics",
"blade-macros",
"blade-util",
@@ -5814,10 +5877,11 @@ dependencies = [
"cbindgen 0.28.0",
"cocoa 0.26.0",
"collections",
"core-foundation 0.9.4",
"core-foundation 0.10.0",
"core-foundation-sys",
"core-graphics 0.23.2",
"core-graphics 0.24.0",
"core-text",
"core-video",
"cosmic-text",
"ctor",
"derive_more",
@@ -6919,6 +6983,19 @@ version = "2.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06432fb54d3be7964ecd3649233cddf80db2832f47fec34c01f65b3d9d774983"
[[package]]
name = "io-surface"
version = "0.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8283575d5f0b2e7447ec0840363879d71c0fa325d4c699d5b45208ea4a51f45e"
dependencies = [
"cgl",
"core-foundation 0.10.0",
"core-foundation-sys",
"leaky-cow",
"libc",
]
[[package]]
name = "iovec"
version = "0.1.4"
@@ -7499,6 +7576,21 @@ version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55"
[[package]]
name = "leak"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bd100e01f1154f2908dfa7d02219aeab25d0b9c7fa955164192e3245255a0c73"
[[package]]
name = "leaky-cow"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "40a8225d44241fd324a8af2806ba635fc7c8a7e9a7de4d5cf3ef54e71f5926fc"
dependencies = [
"leak",
]
[[package]]
name = "leb128"
version = "0.2.5"
@@ -7556,7 +7648,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc2f4eb4bc735547cfed7c0a4922cbd04a4655978c09b54f1f7b228750664c34"
dependencies = [
"cfg-if",
"windows-targets 0.48.5",
"windows-targets 0.52.6",
]
[[package]]
@@ -7599,8 +7691,8 @@ dependencies = [
[[package]]
name = "libwebrtc"
version = "0.3.7"
source = "git+https://github.com/zed-industries/livekit-rust-sdks?rev=811ceae29fabee455f110c56cd66b3f49a7e5003#811ceae29fabee455f110c56cd66b3f49a7e5003"
version = "0.3.10"
source = "git+https://github.com/zed-industries/livekit-rust-sdks?rev=102ebbb1ccfbdbcb7332d86dc30b1b1c8c01e4f8#102ebbb1ccfbdbcb7332d86dc30b1b1c8c01e4f8"
dependencies = [
"cxx",
"jni",
@@ -7684,12 +7776,13 @@ checksum = "4ee93343901ab17bd981295f2cf0026d4ad018c7c31ba84549a4ddbb47a45104"
[[package]]
name = "livekit"
version = "0.7.0"
source = "git+https://github.com/zed-industries/livekit-rust-sdks?rev=811ceae29fabee455f110c56cd66b3f49a7e5003#811ceae29fabee455f110c56cd66b3f49a7e5003"
version = "0.7.8"
source = "git+https://github.com/zed-industries/livekit-rust-sdks?rev=102ebbb1ccfbdbcb7332d86dc30b1b1c8c01e4f8#102ebbb1ccfbdbcb7332d86dc30b1b1c8c01e4f8"
dependencies = [
"chrono",
"futures-util",
"lazy_static",
"libloading",
"libwebrtc",
"livekit-api",
"livekit-protocol",
@@ -7706,10 +7799,10 @@ dependencies = [
[[package]]
name = "livekit-api"
version = "0.4.1"
source = "git+https://github.com/zed-industries/livekit-rust-sdks?rev=811ceae29fabee455f110c56cd66b3f49a7e5003#811ceae29fabee455f110c56cd66b3f49a7e5003"
version = "0.4.2"
source = "git+https://github.com/zed-industries/livekit-rust-sdks?rev=102ebbb1ccfbdbcb7332d86dc30b1b1c8c01e4f8#102ebbb1ccfbdbcb7332d86dc30b1b1c8c01e4f8"
dependencies = [
"async-tungstenite",
"base64 0.21.7",
"futures-util",
"http 0.2.12",
"jsonwebtoken",
@@ -7717,7 +7810,9 @@ dependencies = [
"livekit-runtime",
"log",
"parking_lot",
"pbjson-types",
"prost 0.12.6",
"rand 0.9.0",
"reqwest 0.11.27",
"scopeguard",
"serde",
@@ -7725,14 +7820,14 @@ dependencies = [
"sha2",
"thiserror 1.0.69",
"tokio",
"tokio-tungstenite 0.20.1",
"tokio-tungstenite 0.26.2",
"url",
]
[[package]]
name = "livekit-protocol"
version = "0.3.6"
source = "git+https://github.com/zed-industries/livekit-rust-sdks?rev=811ceae29fabee455f110c56cd66b3f49a7e5003#811ceae29fabee455f110c56cd66b3f49a7e5003"
version = "0.3.9"
source = "git+https://github.com/zed-industries/livekit-rust-sdks?rev=102ebbb1ccfbdbcb7332d86dc30b1b1c8c01e4f8#102ebbb1ccfbdbcb7332d86dc30b1b1c8c01e4f8"
dependencies = [
"futures-util",
"livekit-runtime",
@@ -7748,13 +7843,11 @@ dependencies = [
[[package]]
name = "livekit-runtime"
version = "0.3.1"
source = "git+https://github.com/zed-industries/livekit-rust-sdks?rev=811ceae29fabee455f110c56cd66b3f49a7e5003#811ceae29fabee455f110c56cd66b3f49a7e5003"
version = "0.4.0"
source = "git+https://github.com/zed-industries/livekit-rust-sdks?rev=102ebbb1ccfbdbcb7332d86dc30b1b1c8c01e4f8#102ebbb1ccfbdbcb7332d86dc30b1b1c8c01e4f8"
dependencies = [
"async-io",
"async-std",
"async-task",
"futures 0.3.31",
"tokio",
"tokio-stream",
]
[[package]]
@@ -7779,19 +7872,21 @@ dependencies = [
"anyhow",
"async-trait",
"collections",
"core-foundation 0.9.4",
"core-foundation 0.10.0",
"core-video",
"coreaudio-rs 0.12.1",
"cpal",
"futures 0.3.31",
"gpui",
"http 0.2.12",
"http_client",
"gpui_tokio",
"http_client_tls",
"image",
"libwebrtc",
"livekit",
"livekit_api",
"log",
"media",
"nanoid",
"objc",
"parking_lot",
"postage",
"serde",
@@ -7799,32 +7894,10 @@ dependencies = [
"sha2",
"simplelog",
"smallvec",
"tokio-tungstenite 0.26.2",
"util",
]
[[package]]
name = "livekit_client_macos"
version = "0.1.0"
dependencies = [
"anyhow",
"async-broadcast",
"async-trait",
"collections",
"core-foundation 0.9.4",
"futures 0.3.31",
"gpui",
"livekit_api",
"log",
"media",
"nanoid",
"parking_lot",
"postage",
"serde",
"serde_json",
"sha2",
"simplelog",
]
[[package]]
name = "lmdb-master-sys"
version = "0.2.4"
@@ -8165,8 +8238,9 @@ name = "media"
version = "0.1.0"
dependencies = [
"anyhow",
"bindgen 0.70.1",
"core-foundation 0.9.4",
"bindgen 0.71.1",
"core-foundation 0.10.0",
"core-video",
"ctor",
"foreign-types 0.5.0",
"metal",
@@ -8216,9 +8290,9 @@ dependencies = [
[[package]]
name = "metal"
version = "0.31.0"
version = "0.29.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f569fb946490b5743ad69813cb19629130ce9374034abe31614a36402d18f99e"
checksum = "7ecfd3296f8c56b7c1f6fbac3c71cefa9d78ce009850c45000015f206dc7fa21"
dependencies = [
"bitflags 2.9.0",
"block",
@@ -10711,8 +10785,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22505a5c94da8e3b7c2996394d1c933236c4d743e81a410bcca4e6989fc066a4"
dependencies = [
"bytes 1.10.1",
"heck 0.4.1",
"itertools 0.10.5",
"heck 0.5.0",
"itertools 0.12.1",
"log",
"multimap 0.10.0",
"once_cell",
@@ -10745,7 +10819,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "81bddcdb20abf9501610992b6759a4c888aef7d1a7247ef75e2404275ac24af1"
dependencies = [
"anyhow",
"itertools 0.10.5",
"itertools 0.12.1",
"proc-macro2",
"quote",
"syn 2.0.100",
@@ -11348,6 +11422,7 @@ dependencies = [
"clap",
"client",
"clock",
"dap",
"env_logger 0.11.7",
"extension",
"extension_host",
@@ -13235,6 +13310,7 @@ dependencies = [
"theme",
"title_bar",
"ui",
"workspace",
]
[[package]]
@@ -13740,10 +13816,8 @@ dependencies = [
"menu",
"picker",
"project",
"schemars",
"serde",
"serde_json",
"settings",
"task",
"tree-sitter-rust",
"tree-sitter-typescript",
@@ -14092,7 +14166,7 @@ dependencies = [
name = "time_format"
version = "0.1.0"
dependencies = [
"core-foundation 0.9.4",
"core-foundation 0.10.0",
"core-foundation-sys",
"sys-locale",
"time",
@@ -14316,10 +14390,7 @@ checksum = "212d5dcb2a1ce06d81107c3d0ffa3121fe974b73f068c8282cb1c32328113b6c"
dependencies = [
"futures-util",
"log",
"rustls 0.21.12",
"rustls-native-certs 0.6.3",
"tokio",
"tokio-rustls 0.24.1",
"tungstenite 0.20.1",
]
@@ -14335,6 +14406,21 @@ dependencies = [
"tungstenite 0.21.0",
]
[[package]]
name = "tokio-tungstenite"
version = "0.26.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a9daff607c6d2bf6c16fd681ccb7eecc83e4e2cdc1ca067ffaadfca5de7f084"
dependencies = [
"futures-util",
"log",
"rustls 0.23.25",
"rustls-pki-types",
"tokio",
"tokio-rustls 0.26.1",
"tungstenite 0.26.2",
]
[[package]]
name = "tokio-util"
version = "0.7.13"
@@ -14839,7 +14925,6 @@ dependencies = [
"httparse",
"log",
"rand 0.8.5",
"rustls 0.21.12",
"sha1",
"thiserror 1.0.69",
"url",
@@ -14883,6 +14968,25 @@ dependencies = [
"utf-8",
]
[[package]]
name = "tungstenite"
version = "0.26.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4793cb5e56680ecbb1d843515b23b6de9a75eb04b66643e256a396d43be33c13"
dependencies = [
"bytes 1.10.1",
"data-encoding",
"http 1.2.0",
"httparse",
"log",
"rand 0.9.0",
"rustls 0.23.25",
"rustls-pki-types",
"sha1",
"thiserror 2.0.12",
"utf-8",
]
[[package]]
name = "typeid"
version = "1.0.2"
@@ -16019,8 +16123,8 @@ dependencies = [
[[package]]
name = "webrtc-sys"
version = "0.3.5"
source = "git+https://github.com/zed-industries/livekit-rust-sdks?rev=811ceae29fabee455f110c56cd66b3f49a7e5003#811ceae29fabee455f110c56cd66b3f49a7e5003"
version = "0.3.7"
source = "git+https://github.com/zed-industries/livekit-rust-sdks?rev=102ebbb1ccfbdbcb7332d86dc30b1b1c8c01e4f8#102ebbb1ccfbdbcb7332d86dc30b1b1c8c01e4f8"
dependencies = [
"cc",
"cxx",
@@ -16032,8 +16136,8 @@ dependencies = [
[[package]]
name = "webrtc-sys-build"
version = "0.3.5"
source = "git+https://github.com/zed-industries/livekit-rust-sdks?rev=811ceae29fabee455f110c56cd66b3f49a7e5003#811ceae29fabee455f110c56cd66b3f49a7e5003"
version = "0.3.6"
source = "git+https://github.com/zed-industries/livekit-rust-sdks?rev=102ebbb1ccfbdbcb7332d86dc30b1b1c8c01e4f8#102ebbb1ccfbdbcb7332d86dc30b1b1c8c01e4f8"
dependencies = [
"fs2",
"regex",
@@ -16172,7 +16276,7 @@ version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
dependencies = [
"windows-sys 0.48.0",
"windows-sys 0.59.0",
]
[[package]]
@@ -16907,6 +17011,7 @@ dependencies = [
"clock",
"collections",
"component",
"dap",
"db",
"derive_more",
"env_logger 0.11.7",
@@ -17283,6 +17388,8 @@ dependencies = [
"command_palette_hooks",
"component_preview",
"copilot",
"dap",
"dap_adapters",
"db",
"debugger_tools",
"debugger_ui",

View File

@@ -87,7 +87,6 @@ members = [
"crates/languages",
"crates/livekit_api",
"crates/livekit_client",
"crates/livekit_client_macos",
"crates/lmstudio",
"crates/lsp",
"crates/markdown",
@@ -292,7 +291,6 @@ language_tools = { path = "crates/language_tools" }
languages = { path = "crates/languages" }
livekit_api = { path = "crates/livekit_api" }
livekit_client = { path = "crates/livekit_client" }
livekit_client_macos = { path = "crates/livekit_client_macos" }
lmstudio = { path = "crates/lmstudio" }
lsp = { path = "crates/lsp" }
markdown = { path = "crates/markdown" }
@@ -420,8 +418,9 @@ circular-buffer = "1.0"
clap = { version = "4.4", features = ["derive"] }
cocoa = "0.26"
cocoa-foundation = "0.2.0"
core-video = { version = "0.4.3", features = ["metal"] }
convert_case = "0.8.0"
core-foundation = "0.9.3"
core-foundation = "0.10.0"
core-foundation-sys = "0.8.6"
ctor = "0.4.0"
dashmap = "6.0"
@@ -459,17 +458,13 @@ libc = "0.2"
libsqlite3-sys = { version = "0.30.1", features = ["bundled"] }
linkify = "0.10.0"
linkme = "0.3.31"
livekit = { git = "https://github.com/zed-industries/livekit-rust-sdks", rev = "811ceae29fabee455f110c56cd66b3f49a7e5003", features = [
"dispatcher",
"services-dispatcher",
"rustls-tls-native-roots",
], default-features = false }
log = { version = "0.4.16", features = ["kv_unstable_serde", "serde"] }
markup5ever_rcdom = "0.3.0"
mlua = { version = "0.10", features = ["lua54", "vendored", "async", "send"] }
nanoid = "0.4"
nbformat = { version = "0.10.0" }
nix = "0.29"
open = "5.0.0"
num-format = "0.4.4"
ordered-float = "2.1.1"
palette = { version = "0.7.5", default-features = false, features = ["std"] }
@@ -551,6 +546,7 @@ time = { version = "0.3", features = [
tiny_http = "0.8"
toml = "0.8"
tokio = { version = "1" }
tokio-tungstenite = { version = "0.26", features = ["__rustls-tls"]}
tower-http = "0.4.4"
tree-sitter = { version = "0.25.3", features = ["wasm"] }
tree-sitter-bash = "0.23"
@@ -596,7 +592,7 @@ which = "6.0.0"
wit-component = "0.221"
zed_llm_client = "0.4"
zstd = "0.11"
metal = "0.31"
metal = "0.29"
[workspace.dependencies.async-stripe]
git = "https://github.com/zed-industries/async-stripe"

View File

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

After

Width:  |  Height:  |  Size: 249 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-message-circle"><path d="M7.9 20A9 9 0 1 0 4 16.1L2 22Z"/></svg>

After

Width:  |  Height:  |  Size: 267 B

View File

@@ -1 +0,0 @@
<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-message-circle-more"><path d="M7.9 20A9 9 0 1 0 4 16.1L2 22Z"/><path d="M8 12h.01"/><path d="M12 12h.01"/><path d="M16 12h.01"/></svg>

Before

Width:  |  Height:  |  Size: 337 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-user-round-pen-icon lucide-user-round-pen"><path d="M2 21a8 8 0 0 1 10.821-7.487"/><path d="M21.378 16.626a1 1 0 0 0-3.004-3.004l-4.01 4.012a2 2 0 0 0-.506.854l-.837 2.87a.5.5 0 0 0 .62.62l2.87-.837a2 2 0 0 0 .854-.506z"/><circle cx="10" cy="8" r="5"/></svg>

After

Width:  |  Height:  |  Size: 461 B

View File

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

Before

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -126,7 +126,6 @@
// "alt-v": ["editor::MovePageUp", { "center_cursor": true }],
"ctrl-alt-space": "editor::ShowCharacterPalette",
"ctrl-;": "editor::ToggleLineNumbers",
"ctrl-k ctrl-r": "git::Restore",
"ctrl-'": "editor::ToggleSelectedDiffHunks",
"ctrl-\"": "editor::ExpandAllDiffHunks",
"ctrl-i": "editor::ShowSignatureHelp",
@@ -138,6 +137,22 @@
"shift-f9": "editor::EditLogBreakpoint"
}
},
{
"context": "Editor && !assistant_diff",
"bindings": {
"ctrl-k ctrl-r": "git::Restore",
"ctrl-alt-y": "git::ToggleStaged",
"alt-y": "git::StageAndNext",
"alt-shift-y": "git::UnstageAndNext"
}
},
{
"context": "AssistantDiff",
"bindings": {
"ctrl-y": "assistant2::ToggleKeep",
"ctrl-k ctrl-r": "assistant2::Reject"
}
},
{
"context": "Editor && mode == full",
"bindings": {
@@ -382,9 +397,6 @@
"ctrl-k v": "markdown::OpenPreviewToTheSide",
"ctrl-shift-v": "markdown::OpenPreview",
"ctrl-alt-shift-c": "editor::DisplayCursorNames",
"ctrl-alt-y": "git::ToggleStaged",
"alt-y": "git::StageAndNext",
"alt-shift-y": "git::UnstageAndNext",
"alt-.": "editor::GoToHunk",
"alt-,": "editor::GoToPreviousHunk"
}
@@ -617,7 +629,10 @@
"bindings": {
"ctrl-n": "assistant2::NewThread",
"new": "assistant2::NewThread",
"ctrl-alt-n": "assistant2::NewPromptEditor",
"ctrl-shift-h": "assistant2::OpenHistory",
"ctrl-alt-c": "assistant2::OpenConfiguration",
"ctrl-i": "assistant2::ToggleProfileSelector",
"ctrl-alt-/": "assistant::ToggleModelSelector",
"ctrl-shift-a": "assistant2::ToggleContextPicker",
"ctrl-e": "assistant2::ChatMode",
@@ -635,7 +650,9 @@
{
"context": "MessageEditor > Editor",
"bindings": {
"enter": "assistant2::Chat"
"enter": "assistant2::Chat",
"ctrl-i": "assistant2::ToggleProfileSelector",
"shift-ctrl-r": "assistant2::OpenAssistantDiff"
}
},
{

View File

@@ -147,10 +147,6 @@
"ctrl-shift-v": ["editor::MovePageUp", { "center_cursor": true }],
"ctrl-cmd-space": "editor::ShowCharacterPalette",
"cmd-;": "editor::ToggleLineNumbers",
"cmd-alt-z": "git::Restore",
"cmd-alt-y": "git::ToggleStaged",
"cmd-y": "git::StageAndNext",
"cmd-shift-y": "git::UnstageAndNext",
"cmd-'": "editor::ToggleSelectedDiffHunks",
"cmd-\"": "editor::ExpandAllDiffHunks",
"cmd-alt-g b": "editor::ToggleGitBlame",
@@ -231,6 +227,24 @@
"ctrl-alt-enter": "repl::RunInPlace"
}
},
{
"context": "Editor && !assistant_diff",
"use_key_equivalents": true,
"bindings": {
"cmd-alt-z": "git::Restore",
"cmd-alt-y": "git::ToggleStaged",
"cmd-y": "git::StageAndNext",
"cmd-shift-y": "git::UnstageAndNext"
}
},
{
"context": "AssistantDiff",
"use_key_equivalents": true,
"bindings": {
"cmd-y": "assistant2::ToggleKeep",
"cmd-alt-z": "assistant2::Reject"
}
},
{
"context": "AssistantPanel",
"use_key_equivalents": true,
@@ -267,8 +281,10 @@
"use_key_equivalents": true,
"bindings": {
"cmd-n": "assistant2::NewThread",
"cmd-alt-p": "assistant2::NewPromptEditor",
"cmd-alt-n": "assistant2::NewPromptEditor",
"cmd-shift-h": "assistant2::OpenHistory",
"cmd-alt-c": "assistant2::OpenConfiguration",
"cmd-i": "assistant2::ToggleProfileSelector",
"cmd-alt-/": "assistant::ToggleModelSelector",
"cmd-shift-a": "assistant2::ToggleContextPicker",
"cmd-e": "assistant2::ChatMode",
@@ -288,8 +304,8 @@
"use_key_equivalents": true,
"bindings": {
"enter": "assistant2::Chat",
"cmd-g d": "git::Diff",
"shift-escape": "git::ExpandCommitEditor"
"cmd-i": "assistant2::ToggleProfileSelector",
"shift-ctrl-r": "assistant2::OpenAssistantDiff"
}
},
{

View File

@@ -258,7 +258,7 @@
"u": "vim::ConvertToLowerCase",
"shift-u": "vim::ConvertToUpperCase",
"shift-o": "vim::OtherEnd",
"o": "vim::OtherEnd",
"o": "vim::OtherEndRowAware",
"d": "vim::VisualDelete",
"x": "vim::VisualDelete",
"shift-d": "vim::VisualDeleteLine",

View File

@@ -115,6 +115,15 @@
"confirm_quit": false,
// Whether to restore last closed project when fresh Zed instance is opened.
"restore_on_startup": "last_session",
// Whether to attempt to restore previous file's state when opening it again.
// The state is stored per pane.
// When disabled, defaults are applied instead of the state restoration.
//
// E.g. for editors, selections, folds and scroll positions are restored, if the same file is closed and, later, opened again in the same pane.
// When disabled, a single selection in the very beginning of the file, zero scroll position and no folds state is used as a default.
//
// Default: true
"restore_on_file_reopen": true,
// Size of the drop target in the editor.
"drop_target_size": 0.2,
// Whether the window should be closed when using 'close active item' on a window with no tabs.
@@ -155,8 +164,8 @@
//
// Default: not set, defaults to "bar"
"cursor_shape": null,
// Determines whether the mouse cursor is hidden when typing in an editor or input box.
"hide_mouse_while_typing": true,
// Determines when the mouse cursor should be hidden in an editor or input box.
"hide_mouse": "on_typing_and_movement",
// How to highlight the current line in the editor.
//
// 1. Don't highlight the current line:
@@ -624,10 +633,10 @@
// The model to use.
"model": "claude-3-5-sonnet-latest"
},
"default_profile": "code-writer",
"default_profile": "write",
"profiles": {
"read-only": {
"name": "Read-only",
"ask": {
"name": "Ask",
"tools": {
"diagnostics": true,
"fetch": true,
@@ -639,10 +648,12 @@
"thinking": true
}
},
"code-writer": {
"name": "Code Writer",
"write": {
"name": "Write",
"tools": {
"bash": true,
"batch-tool": true,
"code-symbols": true,
"copy-path": true,
"create-file": true,
"delete-path": true,
@@ -656,11 +667,18 @@
"path-search": true,
"read-file": true,
"regex-search": true,
"symbol-info": true,
"thinking": true
}
}
},
"notify_when_agent_waiting": true
// Where to show notifications when an agent has either completed
// its response, or else needs confirmation before it can run a
// tool action.
// "primary_screen" - Show the notification only on your primary screen (default)
// "all_screens" - Show these notifications on all screens
// "never" - Never show these notifications
"notify_when_agent_waiting": "primary_screen"
},
// The settings for slash commands.
"slash_commands": {
@@ -1464,11 +1482,6 @@
"dev": {
// "theme": "Andromeda"
},
// Task-related settings.
"task": {
// Whether to show task status indicator in the status bar. Default: true
"show_status_indicator": true
},
// Whether to show full labels in line indicator or short ones
//
// Values:

View File

@@ -190,7 +190,7 @@ impl ActivityIndicator {
fn pending_language_server_work<'a>(
&self,
cx: &'a App,
) -> impl Iterator<Item = PendingWork<'a>> {
) -> impl Iterator<Item = PendingWork<'a>> + use<'a> {
self.project
.read(cx)
.language_server_statuses(cx)

View File

@@ -585,6 +585,13 @@ pub enum Thinking {
Enabled { budget_tokens: Option<u32> },
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(untagged)]
pub enum StringOrContents {
String(String),
Content(Vec<RequestContent>),
}
#[derive(Debug, Serialize, Deserialize)]
pub struct Request {
pub model: String,
@@ -597,7 +604,7 @@ pub struct Request {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub tool_choice: Option<ToolChoice>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub system: Option<String>,
pub system: Option<StringOrContents>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub metadata: Option<Metadata>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]

View File

@@ -89,22 +89,25 @@ impl AskPassSession {
buffer.clear();
}
let prompt = String::from_utf8_lossy(&buffer);
if let Some(password) = delegate
match delegate
.ask_password(prompt.to_string())
.await
.context("failed to get askpass password")
.log_err()
{
stream.write_all(password.as_bytes()).await.log_err();
} else {
if let Some(kill_tx) = kill_tx.take() {
kill_tx.send(()).log_err();
Some(password) => {
stream.write_all(password.as_bytes()).await.log_err();
}
_ => {
if let Some(kill_tx) = kill_tx.take() {
kill_tx.send(()).log_err();
}
// note: we expect the caller to drop this task when it's done.
// We need to keep the stream open until the caller is done to avoid
// spurious errors from ssh.
std::future::pending::<()>().await;
drop(stream);
}
// note: we expect the caller to drop this task when it's done.
// We need to keep the stream open until the caller is done to avoid
// spurious errors from ssh.
std::future::pending::<()>().await;
drop(stream);
}
}
drop(temp_dir)

View File

@@ -48,7 +48,6 @@ lsp.workspace = true
menu.workspace = true
multi_buffer.workspace = true
parking_lot.workspace = true
paths.workspace = true
project.workspace = true
prompt_library.workspace = true
prompt_store.workspace = true
@@ -56,7 +55,6 @@ proto.workspace = true
rope.workspace = true
schemars.workspace = true
search.workspace = true
semantic_index.workspace = true
serde.workspace = true
settings.workspace = true
smol.workspace = true

View File

@@ -10,17 +10,15 @@ use std::sync::Arc;
use assistant_settings::AssistantSettings;
use assistant_slash_command::SlashCommandRegistry;
use assistant_slash_commands::{ProjectSlashCommandFeatureFlag, SearchSlashCommandFeatureFlag};
use client::Client;
use command_palette_hooks::CommandPaletteFilter;
use feature_flags::FeatureFlagAppExt;
use fs::Fs;
use gpui::{actions, App, Global, UpdateGlobal};
use gpui::{actions, App, Global, ReadGlobal, UpdateGlobal};
use language_model::{
LanguageModelId, LanguageModelProviderId, LanguageModelRegistry, LanguageModelResponseMessage,
};
use prompt_store::PromptBuilder;
use semantic_index::{CloudEmbeddingProvider, SemanticDb};
use serde::Deserialize;
use settings::{Settings, SettingsStore};
@@ -86,6 +84,10 @@ impl Assistant {
filter.show_namespace(Self::NAMESPACE);
});
}
pub fn enabled(cx: &App) -> bool {
Self::global(cx).enabled
}
}
pub fn init(
@@ -98,33 +100,6 @@ pub fn init(
AssistantSettings::register(cx);
SlashCommandSettings::register(cx);
cx.spawn({
let client = client.clone();
async move |cx| {
let is_search_slash_command_enabled = cx
.update(|cx| cx.wait_for_flag::<SearchSlashCommandFeatureFlag>())?
.await;
let is_project_slash_command_enabled = cx
.update(|cx| cx.wait_for_flag::<ProjectSlashCommandFeatureFlag>())?
.await;
if !is_search_slash_command_enabled && !is_project_slash_command_enabled {
return Ok(());
}
let embedding_provider = CloudEmbeddingProvider::new(client.clone());
let semantic_index = SemanticDb::new(
paths::embeddings_dir().join("semantic-index-db.0.mdb"),
Arc::new(embedding_provider),
cx,
)
.await?;
cx.update(|cx| cx.set_global(semantic_index))
}
})
.detach();
assistant_context_editor::init(client.clone(), cx);
prompt_library::init(cx);
init_language_model_settings(cx);
@@ -133,7 +108,7 @@ pub fn init(
assistant_panel::init(cx);
context_server::init(cx);
register_slash_commands(Some(prompt_builder.clone()), cx);
register_slash_commands(cx);
inline_assistant::init(
fs.clone(),
prompt_builder.clone(),
@@ -209,7 +184,7 @@ fn update_active_language_model_from_settings(cx: &mut App) {
});
}
fn register_slash_commands(prompt_builder: Option<Arc<PromptBuilder>>, cx: &mut App) {
fn register_slash_commands(cx: &mut App) {
let slash_command_registry = SlashCommandRegistry::global(cx);
slash_command_registry.register_command(assistant_slash_commands::FileSlashCommand, true);
@@ -227,33 +202,6 @@ fn register_slash_commands(prompt_builder: Option<Arc<PromptBuilder>>, cx: &mut
.register_command(assistant_slash_commands::DiagnosticsSlashCommand, true);
slash_command_registry.register_command(assistant_slash_commands::FetchSlashCommand, true);
if let Some(prompt_builder) = prompt_builder {
cx.observe_flag::<assistant_slash_commands::ProjectSlashCommandFeatureFlag, _>({
let slash_command_registry = slash_command_registry.clone();
move |is_enabled, _cx| {
if is_enabled {
slash_command_registry.register_command(
assistant_slash_commands::ProjectSlashCommand::new(prompt_builder.clone()),
true,
);
}
}
})
.detach();
}
cx.observe_flag::<assistant_slash_commands::AutoSlashCommandFeatureFlag, _>({
let slash_command_registry = slash_command_registry.clone();
move |is_enabled, _cx| {
if is_enabled {
// [#auto-staff-ship] TODO remove this when /auto is no longer staff-shipped
slash_command_registry
.register_command(assistant_slash_commands::AutoCommand, true);
}
}
})
.detach();
cx.observe_flag::<assistant_slash_commands::StreamingExampleSlashCommandFeatureFlag, _>({
let slash_command_registry = slash_command_registry.clone();
move |is_enabled, _cx| {
@@ -270,17 +218,6 @@ fn register_slash_commands(prompt_builder: Option<Arc<PromptBuilder>>, cx: &mut
update_slash_commands_from_settings(cx);
cx.observe_global::<SettingsStore>(update_slash_commands_from_settings)
.detach();
cx.observe_flag::<assistant_slash_commands::SearchSlashCommandFeatureFlag, _>({
let slash_command_registry = slash_command_registry.clone();
move |is_enabled, _cx| {
if is_enabled {
slash_command_registry
.register_command(assistant_slash_commands::SearchSlashCommand, true);
}
}
})
.detach();
}
fn update_slash_commands_from_settings(cx: &mut App) {

View File

@@ -1,4 +1,5 @@
use crate::assistant_configuration::{ConfigurationView, ConfigurationViewEvent};
use crate::Assistant;
use crate::{
terminal_inline_assistant::TerminalInlineAssistant, DeployHistory, InlineAssistant, NewChat,
};
@@ -58,8 +59,7 @@ pub fn init(cx: &mut App) {
cx.observe_new(
|terminal_panel: &mut TerminalPanel, _, cx: &mut Context<TerminalPanel>| {
let settings = AssistantSettings::get_global(cx);
terminal_panel.set_assistant_enabled(settings.enabled, cx);
terminal_panel.set_assistant_enabled(Assistant::enabled(cx), cx);
},
)
.detach();
@@ -342,12 +342,12 @@ impl AssistantPanel {
window: &mut Window,
cx: &mut Context<Workspace>,
) {
let settings = AssistantSettings::get_global(cx);
if !settings.enabled {
return;
if workspace
.panel::<Self>(cx)
.is_some_and(|panel| panel.read(cx).enabled(cx))
{
workspace.toggle_panel_focus::<Self>(window, cx);
}
workspace.toggle_panel_focus::<Self>(window, cx);
}
fn watch_client_status(
@@ -595,12 +595,10 @@ impl AssistantPanel {
window: &mut Window,
cx: &mut Context<Workspace>,
) {
let settings = AssistantSettings::get_global(cx);
if !settings.enabled {
return;
}
let Some(assistant_panel) = workspace.panel::<AssistantPanel>(cx) else {
let Some(assistant_panel) = workspace
.panel::<AssistantPanel>(cx)
.filter(|panel| panel.read(cx).enabled(cx))
else {
return;
};
@@ -743,20 +741,29 @@ impl AssistantPanel {
}
});
if let Some(context_editor) = context_editor {
Some(InlineAssistTarget::Editor(context_editor, false))
} else if let Some(workspace_editor) = workspace
.active_item(cx)
.and_then(|item| item.act_as::<Editor>(cx))
{
Some(InlineAssistTarget::Editor(workspace_editor, true))
} else if let Some(terminal_view) = workspace
.active_item(cx)
.and_then(|item| item.act_as::<TerminalView>(cx))
{
Some(InlineAssistTarget::Terminal(terminal_view))
} else {
None
match context_editor {
Some(context_editor) => Some(InlineAssistTarget::Editor(context_editor, false)),
_ => {
match workspace
.active_item(cx)
.and_then(|item| item.act_as::<Editor>(cx))
{
Some(workspace_editor) => {
Some(InlineAssistTarget::Editor(workspace_editor, true))
}
_ => {
match workspace
.active_item(cx)
.and_then(|item| item.act_as::<TerminalView>(cx))
{
Some(terminal_view) => {
Some(InlineAssistTarget::Terminal(terminal_view))
}
_ => None,
}
}
}
}
}
}
@@ -1298,12 +1305,8 @@ impl Panel for AssistantPanel {
}
fn icon(&self, _: &Window, cx: &App) -> Option<IconName> {
let settings = AssistantSettings::get_global(cx);
if !settings.enabled || !settings.button {
return None;
}
Some(IconName::ZedAssistant)
(self.enabled(cx) && AssistantSettings::get_global(cx).button)
.then_some(IconName::ZedAssistant)
}
fn icon_tooltip(&self, _: &Window, _: &App) -> Option<&'static str> {
@@ -1317,6 +1320,10 @@ impl Panel for AssistantPanel {
fn activation_priority(&self) -> u32 {
4
}
fn enabled(&self, cx: &App) -> bool {
Assistant::enabled(cx)
}
}
impl EventEmitter<PanelEvent> for AssistantPanel {}

View File

@@ -1,5 +1,6 @@
use crate::{
AssistantPanel, AssistantPanelEvent, CycleNextInlineAssist, CyclePreviousInlineAssist,
Assistant, AssistantPanel, AssistantPanelEvent, CycleNextInlineAssist,
CyclePreviousInlineAssist,
};
use anyhow::{anyhow, Context as _, Result};
use assistant_context_editor::{humanize_token_count, RequestType};
@@ -249,33 +250,35 @@ impl InlineAssistant {
selection.end.column = snapshot
.buffer_snapshot
.line_len(MultiBufferRow(selection.end.row));
} else if let Some(fold) =
snapshot.crease_for_buffer_row(MultiBufferRow(selection.end.row))
{
selection.start = fold.range().start;
selection.end = fold.range().end;
if MultiBufferRow(selection.end.row) < snapshot.buffer_snapshot.max_row() {
let chars = snapshot
.buffer_snapshot
.chars_at(Point::new(selection.end.row + 1, 0));
for c in chars {
if c == '\n' {
break;
}
if c.is_whitespace() {
continue;
}
if snapshot
.language_at(selection.end)
.is_some_and(|language| language.config().brackets.is_closing_brace(c))
{
selection.end.row += 1;
selection.end.column = snapshot
} else {
match snapshot.crease_for_buffer_row(MultiBufferRow(selection.end.row)) {
Some(fold) => {
selection.start = fold.range().start;
selection.end = fold.range().end;
if MultiBufferRow(selection.end.row) < snapshot.buffer_snapshot.max_row() {
let chars = snapshot
.buffer_snapshot
.line_len(MultiBufferRow(selection.end.row));
.chars_at(Point::new(selection.end.row + 1, 0));
for c in chars {
if c == '\n' {
break;
}
if c.is_whitespace() {
continue;
}
if snapshot.language_at(selection.end).is_some_and(|language| {
language.config().brackets.is_closing_brace(c)
}) {
selection.end.row += 1;
selection.end.column = snapshot
.buffer_snapshot
.line_len(MultiBufferRow(selection.end.row));
}
}
}
}
_ => {}
}
}
@@ -1030,24 +1033,27 @@ impl InlineAssistant {
let mut scroll_target_top;
let mut scroll_target_bottom;
if let Some(decorations) = assist.decorations.as_ref() {
scroll_target_top = editor
.row_for_block(decorations.prompt_block_id, cx)
.unwrap()
.0 as f32;
scroll_target_bottom = editor
.row_for_block(decorations.end_block_id, cx)
.unwrap()
.0 as f32;
} else {
let snapshot = editor.snapshot(window, cx);
let start_row = assist
.range
.start
.to_display_point(&snapshot.display_snapshot)
.row();
scroll_target_top = start_row.0 as f32;
scroll_target_bottom = scroll_target_top + 1.;
match assist.decorations.as_ref() {
Some(decorations) => {
scroll_target_top = editor
.row_for_block(decorations.prompt_block_id, cx)
.unwrap()
.0 as f32;
scroll_target_bottom = editor
.row_for_block(decorations.end_block_id, cx)
.unwrap()
.0 as f32;
}
_ => {
let snapshot = editor.snapshot(window, cx);
let start_row = assist
.range
.start
.to_display_point(&snapshot.display_snapshot)
.row();
scroll_target_top = start_row.0 as f32;
scroll_target_bottom = scroll_target_top + 1.;
}
}
scroll_target_top -= editor.vertical_scroll_margin() as f32;
scroll_target_bottom += editor.vertical_scroll_margin() as f32;
@@ -1092,10 +1098,11 @@ impl InlineAssistant {
}
pub fn start_assist(&mut self, assist_id: InlineAssistId, window: &mut Window, cx: &mut App) {
let assist = if let Some(assist) = self.assists.get_mut(&assist_id) {
assist
} else {
return;
let assist = match self.assists.get_mut(&assist_id) {
Some(assist) => assist,
_ => {
return;
}
};
let assist_group_id = assist.group_id;
@@ -1127,10 +1134,11 @@ impl InlineAssistant {
}
pub fn stop_assist(&mut self, assist_id: InlineAssistId, cx: &mut App) {
let assist = if let Some(assist) = self.assists.get_mut(&assist_id) {
assist
} else {
return;
let assist = match self.assists.get_mut(&assist_id) {
Some(assist) => assist,
_ => {
return;
}
};
assist.codegen.update(cx, |codegen, cx| codegen.stop(cx));
@@ -2183,7 +2191,7 @@ impl PromptEditor {
.into_any_element()
}
fn render_token_count(&self, cx: &mut Context<Self>) -> Option<impl IntoElement> {
fn render_token_count(&self, cx: &mut Context<Self>) -> Option<impl IntoElement + use<>> {
let model = LanguageModelRegistry::read_global(cx).active_model()?;
let token_counts = self.token_counts?;
let max_token_count = model.max_token_count();
@@ -2211,40 +2219,43 @@ impl PromptEditor {
.size(LabelSize::Small)
.color(Color::Muted),
);
if let Some(workspace) = self.workspace.clone() {
token_count = token_count
.tooltip(move |window, cx| {
Tooltip::with_meta(
format!(
"Tokens Used ({} from the Assistant Panel)",
humanize_token_count(token_counts.assistant_panel)
),
None,
"Click to open the Assistant Panel",
window,
cx,
)
})
.cursor_pointer()
.on_mouse_down(gpui::MouseButton::Left, |_, _, cx| cx.stop_propagation())
.on_click(move |_, window, cx| {
cx.stop_propagation();
workspace
.update(cx, |workspace, cx| {
workspace.focus_panel::<AssistantPanel>(window, cx)
})
.ok();
});
} else {
token_count = token_count
.cursor_default()
.tooltip(Tooltip::text("Tokens used"));
match self.workspace.clone() {
Some(workspace) => {
token_count = token_count
.tooltip(move |window, cx| {
Tooltip::with_meta(
format!(
"Tokens Used ({} from the Assistant Panel)",
humanize_token_count(token_counts.assistant_panel)
),
None,
"Click to open the Assistant Panel",
window,
cx,
)
})
.cursor_pointer()
.on_mouse_down(gpui::MouseButton::Left, |_, _, cx| cx.stop_propagation())
.on_click(move |_, window, cx| {
cx.stop_propagation();
workspace
.update(cx, |workspace, cx| {
workspace.focus_panel::<AssistantPanel>(window, cx)
})
.ok();
});
}
_ => {
token_count = token_count
.cursor_default()
.tooltip(Tooltip::text("Tokens used"));
}
}
Some(token_count)
}
fn render_prompt_editor(&self, cx: &mut Context<Self>) -> impl IntoElement {
fn render_prompt_editor(&self, cx: &mut Context<Self>) -> impl IntoElement + use<> {
let settings = ThemeSettings::get_global(cx);
let text_style = TextStyle {
color: if self.editor.read(cx).read_only(cx) {
@@ -2270,7 +2281,7 @@ impl PromptEditor {
)
}
fn render_rate_limit_notice(&self, cx: &mut Context<Self>) -> impl IntoElement {
fn render_rate_limit_notice(&self, cx: &mut Context<Self>) -> impl IntoElement + use<> {
Popover::new().child(
v_flex()
.occlude()
@@ -2429,10 +2440,11 @@ impl InlineAssist {
InlineAssistant::update_global(cx, |this, cx| match event {
CodegenEvent::Undone => this.finish_assist(assist_id, false, window, cx),
CodegenEvent::Finished => {
let assist = if let Some(assist) = this.assists.get(&assist_id) {
assist
} else {
return;
let assist = match this.assists.get(&assist_id) {
Some(assist) => assist,
_ => {
return;
}
};
if let CodegenStatus::Error(error) = codegen.read(cx).status(cx) {
@@ -2864,27 +2876,28 @@ impl CodegenAlternative {
assistant_panel_context: Option<LanguageModelRequest>,
cx: &App,
) -> BoxFuture<'static, Result<TokenCounts>> {
if let Some(model) = LanguageModelRegistry::read_global(cx).active_model() {
let request = self.build_request(user_prompt, assistant_panel_context.clone(), cx);
match request {
Ok(request) => {
let total_count = model.count_tokens(request.clone(), cx);
let assistant_panel_count = assistant_panel_context
.map(|context| model.count_tokens(context, cx))
.unwrap_or_else(|| future::ready(Ok(0)).boxed());
match LanguageModelRegistry::read_global(cx).active_model() {
Some(model) => {
let request = self.build_request(user_prompt, assistant_panel_context.clone(), cx);
match request {
Ok(request) => {
let total_count = model.count_tokens(request.clone(), cx);
let assistant_panel_count = assistant_panel_context
.map(|context| model.count_tokens(context, cx))
.unwrap_or_else(|| future::ready(Ok(0)).boxed());
async move {
Ok(TokenCounts {
total: total_count.await?,
assistant_panel: assistant_panel_count.await?,
})
async move {
Ok(TokenCounts {
total: total_count.await?,
assistant_panel: assistant_panel_count.await?,
})
}
.boxed()
}
.boxed()
Err(error) => futures::future::ready(Err(error)).boxed(),
}
Err(error) => futures::future::ready(Err(error)).boxed(),
}
} else {
future::ready(Err(anyhow!("no active model"))).boxed()
_ => future::ready(Err(anyhow!("no active model"))).boxed(),
}
}
@@ -3220,10 +3233,13 @@ impl CodegenAlternative {
.update(cx, |this, cx| {
this.message_id = message_id;
this.last_equal_ranges.clear();
if let Err(error) = result {
this.status = CodegenStatus::Error(error);
} else {
this.status = CodegenStatus::Done;
match result {
Err(error) => {
this.status = CodegenStatus::Error(error);
}
_ => {
this.status = CodegenStatus::Done;
}
}
this.elapsed_time = Some(elapsed_time);
this.completion = Some(completion.lock().clone());
@@ -3555,7 +3571,7 @@ impl CodeActionProvider for AssistantCodeActionProvider {
_: &mut Window,
cx: &mut App,
) -> Task<Result<Vec<CodeAction>>> {
if !AssistantSettings::get_global(cx).enabled {
if !Assistant::enabled(cx) {
return Task::ready(Ok(Vec::new()));
}

View File

@@ -184,10 +184,11 @@ impl TerminalInlineAssistant {
}
fn start_assist(&mut self, assist_id: TerminalInlineAssistId, cx: &mut App) {
let assist = if let Some(assist) = self.assists.get_mut(&assist_id) {
assist
} else {
return;
let assist = match self.assists.get_mut(&assist_id) {
Some(assist) => assist,
_ => {
return;
}
};
let Some(user_prompt) = assist
@@ -222,10 +223,11 @@ impl TerminalInlineAssistant {
}
fn stop_assist(&mut self, assist_id: TerminalInlineAssistId, cx: &mut App) {
let assist = if let Some(assist) = self.assists.get_mut(&assist_id) {
assist
} else {
return;
let assist = match self.assists.get_mut(&assist_id) {
Some(assist) => assist,
_ => {
return;
}
};
assist.codegen.update(cx, |codegen, cx| codegen.stop(cx));
@@ -436,10 +438,11 @@ impl TerminalInlineAssist {
window.subscribe(&codegen, cx, move |codegen, event, window, cx| {
TerminalInlineAssistant::update_global(cx, |this, cx| match event {
CodegenEvent::Finished => {
let assist = if let Some(assist) = this.assists.get(&assist_id) {
assist
} else {
return;
let assist = match this.assists.get(&assist_id) {
Some(assist) => assist,
_ => {
return;
}
};
if let CodegenStatus::Error(error) = &codegen.read(cx).status {
@@ -664,8 +667,8 @@ impl Render for PromptEditor {
},
gpui::Corner::TopRight,
))
.children(
if let CodegenStatus::Error(error) = &self.codegen.read(cx).status {
.children(match &self.codegen.read(cx).status {
CodegenStatus::Error(error) => {
let error_message = SharedString::from(error.to_string());
Some(
div()
@@ -677,10 +680,9 @@ impl Render for PromptEditor {
.color(Color::Error),
),
)
} else {
None
},
),
}
_ => None,
}),
)
.child(div().flex_1().child(self.render_prompt_editor(cx)))
.child(
@@ -979,7 +981,7 @@ impl PromptEditor {
}
}
fn render_token_count(&self, cx: &mut Context<Self>) -> Option<impl IntoElement> {
fn render_token_count(&self, cx: &mut Context<Self>) -> Option<impl IntoElement + use<>> {
let model = LanguageModelRegistry::read_global(cx).active_model()?;
let token_count = self.token_count?;
let max_token_count = model.max_token_count();
@@ -1007,37 +1009,40 @@ impl PromptEditor {
.size(LabelSize::Small)
.color(Color::Muted),
);
if let Some(workspace) = self.workspace.clone() {
token_count = token_count
.tooltip(|window, cx| {
Tooltip::with_meta(
"Tokens Used by Inline Assistant",
None,
"Click to Open Assistant Panel",
window,
cx,
)
})
.cursor_pointer()
.on_mouse_down(gpui::MouseButton::Left, |_, _, cx| cx.stop_propagation())
.on_click(move |_, window, cx| {
cx.stop_propagation();
workspace
.update(cx, |workspace, cx| {
workspace.focus_panel::<AssistantPanel>(window, cx)
})
.ok();
});
} else {
token_count = token_count
.cursor_default()
.tooltip(Tooltip::text("Tokens Used by Inline Assistant"));
match self.workspace.clone() {
Some(workspace) => {
token_count = token_count
.tooltip(|window, cx| {
Tooltip::with_meta(
"Tokens Used by Inline Assistant",
None,
"Click to Open Assistant Panel",
window,
cx,
)
})
.cursor_pointer()
.on_mouse_down(gpui::MouseButton::Left, |_, _, cx| cx.stop_propagation())
.on_click(move |_, window, cx| {
cx.stop_propagation();
workspace
.update(cx, |workspace, cx| {
workspace.focus_panel::<AssistantPanel>(window, cx)
})
.ok();
});
}
_ => {
token_count = token_count
.cursor_default()
.tooltip(Tooltip::text("Tokens Used by Inline Assistant"));
}
}
Some(token_count)
}
fn render_prompt_editor(&self, cx: &mut Context<Self>) -> impl IntoElement {
fn render_prompt_editor(&self, cx: &mut Context<Self>) -> impl IntoElement + use<> {
let settings = ThemeSettings::get_global(cx);
let text_style = TextStyle {
color: if self.editor.read(cx).read_only(cx) {
@@ -1217,10 +1222,13 @@ impl Codegen {
let result = generate.await;
this.update(cx, |this, cx| {
if let Err(error) = result {
this.status = CodegenStatus::Error(error);
} else {
this.status = CodegenStatus::Done;
match result {
Err(error) => {
this.status = CodegenStatus::Error(error);
}
_ => {
this.status = CodegenStatus::Done;
}
}
cx.emit(CodegenEvent::Finished);
cx.notify();

View File

@@ -25,12 +25,14 @@ assistant_settings.workspace = true
assistant_slash_command.workspace = true
assistant_tool.workspace = true
async-watch.workspace = true
buffer_diff.workspace = true
chrono.workspace = true
client.workspace = true
clock.workspace = true
collections.workspace = true
command_palette_hooks.workspace = true
context_server.workspace = true
convert_case.workspace = true
db.workspace = true
editor.workspace = true
feature_flags.workspace = true
@@ -39,7 +41,6 @@ fs.workspace = true
futures.workspace = true
fuzzy.workspace = true
git.workspace = true
git_ui.workspace = true
gpui.workspace = true
heed.workspace = true
html_to_markdown.workspace = true
@@ -54,6 +55,7 @@ lsp.workspace = true
markdown.workspace = true
menu.workspace = true
multi_buffer.workspace = true
ordered-float.workspace = true
parking_lot.workspace = true
paths.workspace = true
picker.workspace = true
@@ -63,6 +65,7 @@ prompt_store.workspace = true
proto.workspace = true
release_channel.workspace = true
rope.workspace = true
schemars.workspace = true
serde.workspace = true
serde_json.workspace = true
settings.workspace = true
@@ -85,6 +88,7 @@ workspace.workspace = true
zed_actions.workspace = true
[dev-dependencies]
buffer_diff = { workspace = true, features = ["test-support"] }
editor = { workspace = true, features = ["test-support"] }
gpui = { workspace = true, "features" = ["test-support"] }
indoc.workspace = true

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,6 @@
mod active_thread;
mod assistant_configuration;
mod assistant_diff;
mod assistant_model_selector;
mod assistant_panel;
mod buffer_codegen;
@@ -27,8 +28,10 @@ use client::Client;
use command_palette_hooks::CommandPaletteFilter;
use feature_flags::{Assistant2FeatureFlag, FeatureFlagAppExt};
use fs::Fs;
use gpui::{actions, App};
use gpui::{actions, impl_actions, App};
use prompt_store::PromptBuilder;
use schemars::JsonSchema;
use serde::Deserialize;
use settings::Settings as _;
pub use crate::active_thread::ActiveThread;
@@ -37,6 +40,7 @@ pub use crate::assistant_panel::{AssistantPanel, ConcreteAssistantPanelDelegate}
pub use crate::inline_assistant::InlineAssistant;
pub use crate::thread::{Message, RequestKind, Thread, ThreadEvent};
pub use crate::thread_store::ThreadStore;
pub use assistant_diff::{AssistantDiff, AssistantDiffToolbar};
actions!(
assistant2,
@@ -44,10 +48,10 @@ actions!(
NewThread,
NewPromptEditor,
ToggleContextPicker,
ToggleProfileSelector,
RemoveAllContext,
OpenHistory,
OpenConfiguration,
ManageProfiles,
AddContextServer,
RemoveSelectedThread,
Chat,
@@ -60,10 +64,31 @@ actions!(
FocusRight,
RemoveFocusedContext,
AcceptSuggestedContext,
OpenActiveThreadAsMarkdown
OpenActiveThreadAsMarkdown,
OpenAssistantDiff,
ToggleKeep,
Reject,
RejectAll,
KeepAll
]
);
#[derive(PartialEq, Clone, Default, Debug, Deserialize, JsonSchema)]
pub struct ManageProfiles {
#[serde(default)]
pub customize_tools: Option<Arc<str>>,
}
impl ManageProfiles {
pub fn customize_tools(profile_id: Arc<str>) -> Self {
Self {
customize_tools: Some(profile_id),
}
}
}
impl_actions!(assistant, [ManageProfiles]);
const NAMESPACE: &str = "assistant2";
/// Initializes the `assistant2` crate.

View File

@@ -1,6 +1,5 @@
mod add_context_server_modal;
mod manage_profiles_modal;
mod profile_picker;
mod tool_picker;
use std::sync::Arc;
@@ -106,7 +105,7 @@ impl AssistantConfiguration {
&mut self,
provider: &Arc<dyn LanguageModelProvider>,
cx: &mut Context<Self>,
) -> impl IntoElement {
) -> impl IntoElement + use<> {
let provider_id = provider.id().0.clone();
let provider_name = provider.name().0.clone();
let configuration_view = self
@@ -168,7 +167,10 @@ impl AssistantConfiguration {
)
}
fn render_context_servers_section(&mut self, cx: &mut Context<Self>) -> impl IntoElement {
fn render_context_servers_section(
&mut self,
cx: &mut Context<Self>,
) -> impl IntoElement + use<> {
let context_servers = self.context_server_manager.read(cx).all_servers().clone();
let tools_by_source = self.tools.tools_by_source(cx);
let empty = Vec::new();

View File

@@ -1,32 +1,93 @@
mod profile_modal_header;
use std::sync::Arc;
use assistant_settings::AssistantSettings;
use assistant_settings::{
AgentProfile, AgentProfileContent, AssistantSettings, AssistantSettingsContent,
ContextServerPresetContent, VersionedAssistantSettingsContent,
};
use assistant_tool::ToolWorkingSet;
use convert_case::{Case, Casing as _};
use editor::Editor;
use fs::Fs;
use gpui::{prelude::*, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable};
use settings::Settings as _;
use ui::{prelude::*, ListItem, ListItemSpacing, Navigable, NavigableEntry};
use gpui::{
prelude::*, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, Subscription,
WeakEntity,
};
use settings::{update_settings_file, Settings as _};
use ui::{
prelude::*, KeyBinding, ListItem, ListItemSpacing, ListSeparator, Navigable, NavigableEntry,
};
use workspace::{ModalView, Workspace};
use crate::assistant_configuration::profile_picker::{ProfilePicker, ProfilePickerDelegate};
use crate::assistant_configuration::manage_profiles_modal::profile_modal_header::ProfileModalHeader;
use crate::assistant_configuration::tool_picker::{ToolPicker, ToolPickerDelegate};
use crate::{AssistantPanel, ManageProfiles};
use crate::{AssistantPanel, ManageProfiles, ThreadStore};
enum Mode {
ChooseProfile(Entity<ProfilePicker>),
ChooseProfile(ChooseProfileMode),
NewProfile(NewProfileMode),
ViewProfile(ViewProfileMode),
ConfigureTools(Entity<ToolPicker>),
ConfigureTools {
profile_id: Arc<str>,
tool_picker: Entity<ToolPicker>,
_subscription: Subscription,
},
}
impl Mode {
pub fn choose_profile(_window: &mut Window, cx: &mut Context<ManageProfilesModal>) -> Self {
let settings = AssistantSettings::get_global(cx);
let mut profiles = settings.profiles.clone();
profiles.sort_unstable_by(|_, a, _, b| a.name.cmp(&b.name));
let profiles = profiles
.into_iter()
.map(|(id, profile)| ProfileEntry {
id,
name: profile.name,
navigation: NavigableEntry::focusable(cx),
})
.collect::<Vec<_>>();
Self::ChooseProfile(ChooseProfileMode {
profiles,
add_new_profile: NavigableEntry::focusable(cx),
})
}
}
#[derive(Clone)]
struct ProfileEntry {
pub id: Arc<str>,
pub name: SharedString,
pub navigation: NavigableEntry,
}
#[derive(Clone)]
pub struct ChooseProfileMode {
profiles: Vec<ProfileEntry>,
add_new_profile: NavigableEntry,
}
#[derive(Clone)]
pub struct ViewProfileMode {
profile_id: Arc<str>,
fork_profile: NavigableEntry,
configure_tools: NavigableEntry,
}
#[derive(Clone)]
pub struct NewProfileMode {
name_editor: Entity<Editor>,
base_profile_id: Option<Arc<str>>,
}
pub struct ManageProfilesModal {
fs: Arc<dyn Fs>,
tools: Arc<ToolWorkingSet>,
thread_store: WeakEntity<ThreadStore>,
focus_handle: FocusHandle,
mode: Mode,
}
@@ -37,12 +98,21 @@ impl ManageProfilesModal {
_window: Option<&mut Window>,
_cx: &mut Context<Workspace>,
) {
workspace.register_action(|workspace, _: &ManageProfiles, window, cx| {
workspace.register_action(|workspace, action: &ManageProfiles, window, cx| {
if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
let fs = workspace.app_state().fs.clone();
let thread_store = panel.read(cx).thread_store().read(cx);
let tools = thread_store.tools();
workspace.toggle_modal(window, cx, |window, cx| Self::new(fs, tools, window, cx))
let thread_store = panel.read(cx).thread_store();
let tools = thread_store.read(cx).tools();
let thread_store = thread_store.downgrade();
workspace.toggle_modal(window, cx, |window, cx| {
let mut this = Self::new(fs, tools, thread_store, window, cx);
if let Some(profile_id) = action.customize_tools.clone() {
this.configure_tools(profile_id, window, cx);
}
this
})
}
});
}
@@ -50,30 +120,44 @@ impl ManageProfilesModal {
pub fn new(
fs: Arc<dyn Fs>,
tools: Arc<ToolWorkingSet>,
thread_store: WeakEntity<ThreadStore>,
window: &mut Window,
cx: &mut Context<Self>,
) -> Self {
let focus_handle = cx.focus_handle();
let handle = cx.entity();
Self {
fs,
tools,
thread_store,
focus_handle,
mode: Mode::ChooseProfile(cx.new(|cx| {
let delegate = ProfilePickerDelegate::new(
move |profile_id, window, cx| {
handle.update(cx, |this, cx| {
this.view_profile(profile_id.clone(), window, cx);
})
},
cx,
);
ProfilePicker::new(delegate, window, cx)
})),
mode: Mode::choose_profile(window, cx),
}
}
fn choose_profile(&mut self, window: &mut Window, cx: &mut Context<Self>) {
self.mode = Mode::choose_profile(window, cx);
self.focus_handle(cx).focus(window);
}
fn new_profile(
&mut self,
base_profile_id: Option<Arc<str>>,
window: &mut Window,
cx: &mut Context<Self>,
) {
let name_editor = cx.new(|cx| Editor::single_line(window, cx));
name_editor.update(cx, |editor, cx| {
editor.set_placeholder_text("Profile name", cx);
});
self.mode = Mode::NewProfile(NewProfileMode {
name_editor,
base_profile_id,
});
self.focus_handle(cx).focus(window);
}
pub fn view_profile(
&mut self,
profile_id: Arc<str>,
@@ -82,6 +166,7 @@ impl ManageProfilesModal {
) {
self.mode = Mode::ViewProfile(ViewProfileMode {
profile_id,
fork_profile: NavigableEntry::focusable(cx),
configure_tools: NavigableEntry::focusable(cx),
});
self.focus_handle(cx).focus(window);
@@ -98,22 +183,118 @@ impl ManageProfilesModal {
return;
};
self.mode = Mode::ConfigureTools(cx.new(|cx| {
let tool_picker = cx.new(|cx| {
let delegate = ToolPickerDelegate::new(
self.fs.clone(),
self.tools.clone(),
profile_id,
self.thread_store.clone(),
profile_id.clone(),
profile,
cx,
);
ToolPicker::new(delegate, window, cx)
}));
});
let dismiss_subscription = cx.subscribe_in(&tool_picker, window, {
let profile_id = profile_id.clone();
move |this, _tool_picker, _: &DismissEvent, window, cx| {
this.view_profile(profile_id.clone(), window, cx);
}
});
self.mode = Mode::ConfigureTools {
profile_id,
tool_picker,
_subscription: dismiss_subscription,
};
self.focus_handle(cx).focus(window);
}
fn confirm(&mut self, _window: &mut Window, _cx: &mut Context<Self>) {}
fn confirm(&mut self, window: &mut Window, cx: &mut Context<Self>) {
match &self.mode {
Mode::ChooseProfile { .. } => {}
Mode::NewProfile(mode) => {
let settings = AssistantSettings::get_global(cx);
fn cancel(&mut self, _window: &mut Window, _cx: &mut Context<Self>) {}
let base_profile = mode
.base_profile_id
.as_ref()
.and_then(|profile_id| settings.profiles.get(profile_id).cloned());
let name = mode.name_editor.read(cx).text(cx);
let profile_id: Arc<str> = name.to_case(Case::Kebab).into();
let profile = AgentProfile {
name: name.into(),
tools: base_profile
.as_ref()
.map(|profile| profile.tools.clone())
.unwrap_or_default(),
context_servers: base_profile
.map(|profile| profile.context_servers)
.unwrap_or_default(),
};
self.create_profile(profile_id.clone(), profile, cx);
self.view_profile(profile_id, window, cx);
}
Mode::ViewProfile(_) => {}
Mode::ConfigureTools { .. } => {}
}
}
fn cancel(&mut self, window: &mut Window, cx: &mut Context<Self>) {
match &self.mode {
Mode::ChooseProfile { .. } => {
cx.emit(DismissEvent);
}
Mode::NewProfile(mode) => {
if let Some(profile_id) = mode.base_profile_id.clone() {
self.view_profile(profile_id, window, cx);
} else {
self.choose_profile(window, cx);
}
}
Mode::ViewProfile(_) => self.choose_profile(window, cx),
Mode::ConfigureTools { .. } => {}
}
}
fn create_profile(&self, profile_id: Arc<str>, profile: AgentProfile, cx: &mut Context<Self>) {
update_settings_file::<AssistantSettings>(self.fs.clone(), cx, {
move |settings, _cx| match settings {
AssistantSettingsContent::Versioned(VersionedAssistantSettingsContent::V2(
settings,
)) => {
let profiles = settings.profiles.get_or_insert_default();
if profiles.contains_key(&profile_id) {
log::error!("profile with ID '{profile_id}' already exists");
return;
}
profiles.insert(
profile_id,
AgentProfileContent {
name: profile.name.into(),
tools: profile.tools,
context_servers: profile
.context_servers
.into_iter()
.map(|(server_id, preset)| {
(
server_id,
ContextServerPresetContent {
tools: preset.tools,
},
)
})
.collect(),
},
);
}
_ => {}
}
});
}
}
impl ModalView for ManageProfilesModal {}
@@ -121,9 +302,10 @@ impl ModalView for ManageProfilesModal {}
impl Focusable for ManageProfilesModal {
fn focus_handle(&self, cx: &App) -> FocusHandle {
match &self.mode {
Mode::ChooseProfile(profile_picker) => profile_picker.focus_handle(cx),
Mode::ConfigureTools(tool_picker) => tool_picker.focus_handle(cx),
Mode::ChooseProfile(_) => self.focus_handle.clone(),
Mode::NewProfile(mode) => mode.name_editor.focus_handle(cx),
Mode::ViewProfile(_) => self.focus_handle.clone(),
Mode::ConfigureTools { tool_picker, .. } => tool_picker.focus_handle(cx),
}
}
}
@@ -131,55 +313,240 @@ impl Focusable for ManageProfilesModal {
impl EventEmitter<DismissEvent> for ManageProfilesModal {}
impl ManageProfilesModal {
fn render_view_profile(
fn render_choose_profile(
&mut self,
mode: ViewProfileMode,
mode: ChooseProfileMode,
window: &mut Window,
cx: &mut Context<Self>,
) -> impl IntoElement {
) -> impl IntoElement + use<> {
Navigable::new(
div()
.track_focus(&self.focus_handle(cx))
.size_full()
.child(ProfileModalHeader::new(
"Agent Profiles",
IconName::ZedAssistant,
))
.child(
v_flex().child(
div()
.id("configure-tools")
.track_focus(&mode.configure_tools.focus_handle)
.on_action({
let profile_id = mode.profile_id.clone();
cx.listener(move |this, _: &menu::Confirm, window, cx| {
this.configure_tools(profile_id.clone(), window, cx);
v_flex()
.pb_1()
.child(ListSeparator)
.children(mode.profiles.iter().map(|profile| {
div()
.id(SharedString::from(format!("profile-{}", profile.id)))
.track_focus(&profile.navigation.focus_handle)
.on_action({
let profile_id = profile.id.clone();
cx.listener(move |this, _: &menu::Confirm, window, cx| {
this.view_profile(profile_id.clone(), window, cx);
})
})
})
.child(
ListItem::new("configure-tools")
.child(
ListItem::new(SharedString::from(format!(
"profile-{}",
profile.id
)))
.toggle_state(
mode.configure_tools
profile
.navigation
.focus_handle
.contains_focused(window, cx),
)
.inset(true)
.spacing(ListItemSpacing::Sparse)
.start_slot(Icon::new(IconName::Cog))
.child(Label::new("Configure Tools"))
.child(Label::new(profile.name.clone()))
.end_slot(
h_flex()
.gap_1()
.child(Label::new("Customize").size(LabelSize::Small))
.children(KeyBinding::for_action_in(
&menu::Confirm,
&self.focus_handle,
window,
cx,
)),
)
.on_click({
let profile_id = mode.profile_id.clone();
let profile_id = profile.id.clone();
cx.listener(move |this, _, window, cx| {
this.configure_tools(profile_id.clone(), window, cx);
this.view_profile(profile_id.clone(), window, cx);
})
}),
),
),
)
}))
.child(ListSeparator)
.child(
div()
.id("new-profile")
.track_focus(&mode.add_new_profile.focus_handle)
.on_action(cx.listener(|this, _: &menu::Confirm, window, cx| {
this.new_profile(None, window, cx);
}))
.child(
ListItem::new("new-profile")
.toggle_state(
mode.add_new_profile
.focus_handle
.contains_focused(window, cx),
)
.inset(true)
.spacing(ListItemSpacing::Sparse)
.start_slot(Icon::new(IconName::Plus))
.child(Label::new("Add New Profile"))
.on_click({
cx.listener(move |this, _, window, cx| {
this.new_profile(None, window, cx);
})
}),
),
),
)
.into_any_element(),
)
.map(|mut navigable| {
for profile in mode.profiles {
navigable = navigable.entry(profile.navigation);
}
navigable
})
.entry(mode.add_new_profile)
}
fn render_new_profile(
&mut self,
mode: NewProfileMode,
_window: &mut Window,
cx: &mut Context<Self>,
) -> impl IntoElement + use<> {
let settings = AssistantSettings::get_global(cx);
let base_profile_name = mode.base_profile_id.as_ref().map(|base_profile_id| {
settings
.profiles
.get(base_profile_id)
.map(|profile| profile.name.clone())
.unwrap_or_else(|| "Unknown".into())
});
v_flex()
.id("new-profile")
.track_focus(&self.focus_handle(cx))
.child(ProfileModalHeader::new(
match base_profile_name {
Some(base_profile) => format!("Fork {base_profile}"),
None => "New Profile".into(),
},
IconName::Plus,
))
.child(ListSeparator)
.child(h_flex().p_2().child(mode.name_editor.clone()))
}
fn render_view_profile(
&mut self,
mode: ViewProfileMode,
window: &mut Window,
cx: &mut Context<Self>,
) -> impl IntoElement + use<> {
let settings = AssistantSettings::get_global(cx);
let profile_name = settings
.profiles
.get(&mode.profile_id)
.map(|profile| profile.name.clone())
.unwrap_or_else(|| "Unknown".into());
Navigable::new(
div()
.track_focus(&self.focus_handle(cx))
.size_full()
.child(ProfileModalHeader::new(
profile_name,
IconName::ZedAssistant,
))
.child(
v_flex()
.pb_1()
.child(ListSeparator)
.child(
div()
.id("fork-profile")
.track_focus(&mode.fork_profile.focus_handle)
.on_action({
let profile_id = mode.profile_id.clone();
cx.listener(move |this, _: &menu::Confirm, window, cx| {
this.new_profile(Some(profile_id.clone()), window, cx);
})
})
.child(
ListItem::new("fork-profile")
.toggle_state(
mode.fork_profile
.focus_handle
.contains_focused(window, cx),
)
.inset(true)
.spacing(ListItemSpacing::Sparse)
.start_slot(Icon::new(IconName::GitBranch))
.child(Label::new("Fork Profile"))
.on_click({
let profile_id = mode.profile_id.clone();
cx.listener(move |this, _, window, cx| {
this.new_profile(
Some(profile_id.clone()),
window,
cx,
);
})
}),
),
)
.child(
div()
.id("configure-tools")
.track_focus(&mode.configure_tools.focus_handle)
.on_action({
let profile_id = mode.profile_id.clone();
cx.listener(move |this, _: &menu::Confirm, window, cx| {
this.configure_tools(profile_id.clone(), window, cx);
})
})
.child(
ListItem::new("configure-tools")
.toggle_state(
mode.configure_tools
.focus_handle
.contains_focused(window, cx),
)
.inset(true)
.spacing(ListItemSpacing::Sparse)
.start_slot(Icon::new(IconName::Cog))
.child(Label::new("Configure Tools"))
.on_click({
let profile_id = mode.profile_id.clone();
cx.listener(move |this, _, window, cx| {
this.configure_tools(
profile_id.clone(),
window,
cx,
);
})
}),
),
),
)
.into_any_element(),
)
.entry(mode.fork_profile)
.entry(mode.configure_tools)
}
}
impl Render for ManageProfilesModal {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let settings = AssistantSettings::get_global(cx);
div()
.elevation_3(cx)
.w(rems(34.))
@@ -191,11 +558,35 @@ impl Render for ManageProfilesModal {
}))
.on_mouse_down_out(cx.listener(|_this, _, _, cx| cx.emit(DismissEvent)))
.child(match &self.mode {
Mode::ChooseProfile(profile_picker) => profile_picker.clone().into_any_element(),
Mode::ChooseProfile(mode) => self
.render_choose_profile(mode.clone(), window, cx)
.into_any_element(),
Mode::NewProfile(mode) => self
.render_new_profile(mode.clone(), window, cx)
.into_any_element(),
Mode::ViewProfile(mode) => self
.render_view_profile(mode.clone(), window, cx)
.into_any_element(),
Mode::ConfigureTools(tool_picker) => tool_picker.clone().into_any_element(),
Mode::ConfigureTools {
profile_id,
tool_picker,
..
} => {
let profile_name = settings
.profiles
.get(profile_id)
.map(|profile| profile.name.clone())
.unwrap_or_else(|| "Unknown".into());
div()
.child(ProfileModalHeader::new(
format!("{profile_name}: Configure Tools"),
IconName::Cog,
))
.child(ListSeparator)
.child(tool_picker.clone())
.into_any_element()
}
})
}
}

View File

@@ -0,0 +1,38 @@
use ui::prelude::*;
#[derive(IntoElement)]
pub struct ProfileModalHeader {
label: SharedString,
icon: IconName,
}
impl ProfileModalHeader {
pub fn new(label: impl Into<SharedString>, icon: IconName) -> Self {
Self {
label: label.into(),
icon,
}
}
}
impl RenderOnce for ProfileModalHeader {
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
h_flex()
.w_full()
.px(DynamicSpacing::Base12.rems(cx))
.pt(DynamicSpacing::Base08.rems(cx))
.pb(DynamicSpacing::Base04.rems(cx))
.rounded_t_sm()
.gap_1p5()
.child(Icon::new(self.icon).size(IconSize::XSmall))
.child(
h_flex().gap_1().overflow_x_hidden().child(
div()
.max_w_96()
.overflow_x_hidden()
.text_ellipsis()
.child(Headline::new(self.label).size(HeadlineSize::XSmall)),
),
)
}
}

View File

@@ -1,194 +0,0 @@
use std::sync::Arc;
use assistant_settings::AssistantSettings;
use fuzzy::{match_strings, StringMatch, StringMatchCandidate};
use gpui::{
App, Context, DismissEvent, Entity, EventEmitter, Focusable, SharedString, Task, WeakEntity,
Window,
};
use picker::{Picker, PickerDelegate};
use settings::Settings;
use ui::{prelude::*, HighlightedLabel, ListItem, ListItemSpacing};
use util::ResultExt as _;
pub struct ProfilePicker {
picker: Entity<Picker<ProfilePickerDelegate>>,
}
impl ProfilePicker {
pub fn new(
delegate: ProfilePickerDelegate,
window: &mut Window,
cx: &mut Context<Self>,
) -> Self {
let picker = cx.new(|cx| Picker::uniform_list(delegate, window, cx));
Self { picker }
}
}
impl EventEmitter<DismissEvent> for ProfilePicker {}
impl Focusable for ProfilePicker {
fn focus_handle(&self, cx: &App) -> gpui::FocusHandle {
self.picker.focus_handle(cx)
}
}
impl Render for ProfilePicker {
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
v_flex().w(rems(34.)).child(self.picker.clone())
}
}
#[derive(Debug)]
pub struct ProfileEntry {
pub id: Arc<str>,
pub name: SharedString,
}
pub struct ProfilePickerDelegate {
profile_picker: WeakEntity<ProfilePicker>,
profiles: Vec<ProfileEntry>,
matches: Vec<StringMatch>,
selected_index: usize,
on_confirm: Arc<dyn Fn(&Arc<str>, &mut Window, &mut App) + 'static>,
}
impl ProfilePickerDelegate {
pub fn new(
on_confirm: impl Fn(&Arc<str>, &mut Window, &mut App) + 'static,
cx: &mut Context<ProfilePicker>,
) -> Self {
let settings = AssistantSettings::get_global(cx);
let profiles = settings
.profiles
.iter()
.map(|(id, profile)| ProfileEntry {
id: id.clone(),
name: profile.name.clone(),
})
.collect::<Vec<_>>();
Self {
profile_picker: cx.entity().downgrade(),
profiles,
matches: Vec::new(),
selected_index: 0,
on_confirm: Arc::new(on_confirm),
}
}
}
impl PickerDelegate for ProfilePickerDelegate {
type ListItem = ListItem;
fn match_count(&self) -> usize {
self.matches.len()
}
fn selected_index(&self) -> usize {
self.selected_index
}
fn set_selected_index(
&mut self,
ix: usize,
_window: &mut Window,
_cx: &mut Context<Picker<Self>>,
) {
self.selected_index = ix;
}
fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> {
"Search profiles…".into()
}
fn update_matches(
&mut self,
query: String,
window: &mut Window,
cx: &mut Context<Picker<Self>>,
) -> Task<()> {
let background = cx.background_executor().clone();
let candidates = self
.profiles
.iter()
.enumerate()
.map(|(id, profile)| StringMatchCandidate::new(id, profile.name.as_ref()))
.collect::<Vec<_>>();
cx.spawn_in(window, async move |this, cx| {
let matches = if query.is_empty() {
candidates
.into_iter()
.enumerate()
.map(|(index, candidate)| StringMatch {
candidate_id: index,
string: candidate.string,
positions: Vec::new(),
score: 0.,
})
.collect()
} else {
match_strings(
&candidates,
&query,
false,
100,
&Default::default(),
background,
)
.await
};
this.update(cx, |this, _cx| {
this.delegate.matches = matches;
this.delegate.selected_index = this
.delegate
.selected_index
.min(this.delegate.matches.len().saturating_sub(1));
})
.log_err();
})
}
fn confirm(&mut self, _secondary: bool, window: &mut Window, cx: &mut Context<Picker<Self>>) {
if self.matches.is_empty() {
self.dismissed(window, cx);
return;
}
let candidate_id = self.matches[self.selected_index].candidate_id;
let profile = &self.profiles[candidate_id];
(self.on_confirm)(&profile.id, window, cx);
}
fn dismissed(&mut self, _window: &mut Window, cx: &mut Context<Picker<Self>>) {
self.profile_picker
.update(cx, |_this, cx| cx.emit(DismissEvent))
.log_err();
}
fn render_match(
&self,
ix: usize,
selected: bool,
_window: &mut Window,
_cx: &mut Context<Picker<Self>>,
) -> Option<Self::ListItem> {
let profile_match = &self.matches[ix];
Some(
ListItem::new(ix)
.inset(true)
.spacing(ListItemSpacing::Sparse)
.toggle_state(selected)
.child(HighlightedLabel::new(
profile_match.string.clone(),
profile_match.positions.clone(),
)),
)
}
}

View File

@@ -9,17 +9,19 @@ use fs::Fs;
use fuzzy::{match_strings, StringMatch, StringMatchCandidate};
use gpui::{App, Context, DismissEvent, Entity, EventEmitter, Focusable, Task, WeakEntity, Window};
use picker::{Picker, PickerDelegate};
use settings::update_settings_file;
use settings::{update_settings_file, Settings as _};
use ui::{prelude::*, HighlightedLabel, ListItem, ListItemSpacing};
use util::ResultExt as _;
use crate::ThreadStore;
pub struct ToolPicker {
picker: Entity<Picker<ToolPickerDelegate>>,
}
impl ToolPicker {
pub fn new(delegate: ToolPickerDelegate, window: &mut Window, cx: &mut Context<Self>) -> Self {
let picker = cx.new(|cx| Picker::uniform_list(delegate, window, cx));
let picker = cx.new(|cx| Picker::uniform_list(delegate, window, cx).modal(false));
Self { picker }
}
}
@@ -46,6 +48,7 @@ pub struct ToolEntry {
pub struct ToolPickerDelegate {
tool_picker: WeakEntity<ToolPicker>,
thread_store: WeakEntity<ThreadStore>,
fs: Arc<dyn Fs>,
tools: Vec<ToolEntry>,
profile_id: Arc<str>,
@@ -58,6 +61,7 @@ impl ToolPickerDelegate {
pub fn new(
fs: Arc<dyn Fs>,
tool_set: Arc<ToolWorkingSet>,
thread_store: WeakEntity<ThreadStore>,
profile_id: Arc<str>,
profile: AgentProfile,
cx: &mut Context<ToolPicker>,
@@ -73,6 +77,7 @@ impl ToolPickerDelegate {
Self {
tool_picker: cx.entity().downgrade(),
thread_store,
fs,
tools: tool_entries,
profile_id,
@@ -183,6 +188,15 @@ impl PickerDelegate for ToolPickerDelegate {
}
};
let active_profile_id = &AssistantSettings::get_global(cx).default_profile;
if active_profile_id == &self.profile_id {
self.thread_store
.update(cx, |this, _cx| {
this.load_profile(&self.profile);
})
.log_err();
}
update_settings_file::<AssistantSettings>(self.fs.clone(), cx, {
let profile_id = self.profile_id.clone();
let default_profile = self.profile.clone();

View File

@@ -0,0 +1,771 @@
use crate::{Thread, ThreadEvent, ToggleKeep};
use anyhow::Result;
use buffer_diff::DiffHunkStatus;
use collections::HashSet;
use editor::{
actions::{GoToHunk, GoToPreviousHunk},
Direction, Editor, EditorEvent, MultiBuffer, ToPoint,
};
use gpui::{
prelude::*, Action, AnyElement, AnyView, App, Entity, EventEmitter, FocusHandle, Focusable,
SharedString, Subscription, Task, WeakEntity, Window,
};
use language::{Capability, DiskState, OffsetRangeExt, Point};
use multi_buffer::PathKey;
use project::{Project, ProjectPath};
use std::{
any::{Any, TypeId},
ops::Range,
sync::Arc,
};
use ui::{prelude::*, IconButtonShape, KeyBinding, Tooltip};
use workspace::{
item::{BreadcrumbText, ItemEvent, TabContentParams},
searchable::SearchableItemHandle,
Item, ItemHandle, ItemNavHistory, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView,
Workspace,
};
pub struct AssistantDiff {
multibuffer: Entity<MultiBuffer>,
editor: Entity<Editor>,
thread: Entity<Thread>,
focus_handle: FocusHandle,
workspace: WeakEntity<Workspace>,
title: SharedString,
_subscriptions: Vec<Subscription>,
}
impl AssistantDiff {
pub fn deploy(
thread: Entity<Thread>,
workspace: WeakEntity<Workspace>,
window: &mut Window,
cx: &mut App,
) -> Result<()> {
let existing_diff = workspace.update(cx, |workspace, cx| {
workspace
.items_of_type::<AssistantDiff>(cx)
.find(|diff| diff.read(cx).thread == thread)
})?;
match existing_diff {
Some(existing_diff) => workspace.update(cx, |workspace, cx| {
workspace.activate_item(&existing_diff, true, true, window, cx);
}),
_ => {
let assistant_diff =
cx.new(|cx| AssistantDiff::new(thread.clone(), workspace.clone(), window, cx));
workspace.update(cx, |workspace, cx| {
workspace.add_item_to_center(Box::new(assistant_diff), window, cx);
})
}
}
}
pub fn new(
thread: Entity<Thread>,
workspace: WeakEntity<Workspace>,
window: &mut Window,
cx: &mut Context<Self>,
) -> Self {
let focus_handle = cx.focus_handle();
let multibuffer = cx.new(|_| MultiBuffer::new(Capability::ReadWrite));
let project = thread.read(cx).project().clone();
let render_diff_hunk_controls = Arc::new({
let assistant_diff = cx.entity();
move |row,
status: &DiffHunkStatus,
hunk_range,
is_created_file,
line_height,
_editor: &Entity<Editor>,
window: &mut Window,
cx: &mut App| {
render_diff_hunk_controls(
row,
status,
hunk_range,
is_created_file,
line_height,
&assistant_diff,
window,
cx,
)
}
});
let editor = cx.new(|cx| {
let mut editor =
Editor::for_multibuffer(multibuffer.clone(), Some(project.clone()), window, cx);
editor.disable_inline_diagnostics();
editor.set_expand_all_diff_hunks(cx);
editor.set_render_diff_hunk_controls(render_diff_hunk_controls, cx);
editor.register_addon(AssistantDiffAddon);
editor
});
let action_log = thread.read(cx).action_log().clone();
let mut this = Self {
_subscriptions: vec![
cx.observe_in(&action_log, window, |this, _action_log, window, cx| {
this.update_excerpts(window, cx)
}),
cx.subscribe(&thread, |this, _thread, event, cx| {
this.handle_thread_event(event, cx)
}),
],
title: SharedString::default(),
multibuffer,
editor,
thread,
focus_handle,
workspace,
};
this.update_excerpts(window, cx);
this.update_title(cx);
this
}
fn update_excerpts(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let thread = self.thread.read(cx);
let changed_buffers = thread.action_log().read(cx).changed_buffers(cx);
let mut paths_to_delete = self.multibuffer.read(cx).paths().collect::<HashSet<_>>();
for (buffer, changed) in changed_buffers {
let Some(file) = buffer.read(cx).file().cloned() else {
continue;
};
let path_key = PathKey::namespaced("", file.full_path(cx).into());
paths_to_delete.remove(&path_key);
let snapshot = buffer.read(cx).snapshot();
let diff = changed.diff.read(cx);
let diff_hunk_ranges = diff
.hunks_intersecting_range(
language::Anchor::MIN..language::Anchor::MAX,
&snapshot,
cx,
)
.map(|diff_hunk| diff_hunk.buffer_range.to_point(&snapshot))
.collect::<Vec<_>>();
let (was_empty, is_excerpt_newly_added) =
self.multibuffer.update(cx, |multibuffer, cx| {
let was_empty = multibuffer.is_empty();
let is_excerpt_newly_added = multibuffer.set_excerpts_for_path(
path_key.clone(),
buffer.clone(),
diff_hunk_ranges,
editor::DEFAULT_MULTIBUFFER_CONTEXT,
cx,
);
multibuffer.add_diff(changed.diff.clone(), cx);
(was_empty, is_excerpt_newly_added)
});
self.editor.update(cx, |editor, cx| {
if was_empty {
editor.change_selections(None, window, cx, |selections| {
selections.select_ranges([0..0])
});
}
if is_excerpt_newly_added
&& buffer
.read(cx)
.file()
.map_or(false, |file| file.disk_state() == DiskState::Deleted)
{
editor.fold_buffer(snapshot.text.remote_id(), cx)
}
});
}
self.multibuffer.update(cx, |multibuffer, cx| {
for path in paths_to_delete {
multibuffer.remove_excerpts_for_path(path, cx);
}
});
if self.multibuffer.read(cx).is_empty()
&& self
.editor
.read(cx)
.focus_handle(cx)
.contains_focused(window, cx)
{
self.focus_handle.focus(window);
} else if self.focus_handle.is_focused(window) && !self.multibuffer.read(cx).is_empty() {
self.editor.update(cx, |editor, cx| {
editor.focus_handle(cx).focus(window);
});
}
}
fn update_title(&mut self, cx: &mut Context<Self>) {
let new_title = self
.thread
.read(cx)
.summary()
.unwrap_or("Assistant Changes".into());
if new_title != self.title {
self.title = new_title;
cx.emit(EditorEvent::TitleChanged);
}
}
fn handle_thread_event(&mut self, event: &ThreadEvent, cx: &mut Context<Self>) {
match event {
ThreadEvent::SummaryChanged => self.update_title(cx),
_ => {}
}
}
fn toggle_keep(&mut self, _: &crate::ToggleKeep, _window: &mut Window, cx: &mut Context<Self>) {
let ranges = self
.editor
.read(cx)
.selections
.disjoint_anchor_ranges()
.collect::<Vec<_>>();
let snapshot = self.multibuffer.read(cx).snapshot(cx);
let diff_hunks_in_ranges = self
.editor
.read(cx)
.diff_hunks_in_ranges(&ranges, &snapshot)
.collect::<Vec<_>>();
for hunk in diff_hunks_in_ranges {
let buffer = self.multibuffer.read(cx).buffer(hunk.buffer_id);
if let Some(buffer) = buffer {
self.thread.update(cx, |thread, cx| {
let accept = hunk.status().has_secondary_hunk();
thread.review_edits_in_range(buffer, hunk.buffer_range, accept, cx)
});
}
}
}
fn reject(&mut self, _: &crate::Reject, window: &mut Window, cx: &mut Context<Self>) {
let ranges = self
.editor
.update(cx, |editor, cx| editor.selections.ranges(cx));
self.editor.update(cx, |editor, cx| {
editor.restore_hunks_in_ranges(ranges, window, cx)
})
}
fn reject_all(&mut self, _: &crate::RejectAll, window: &mut Window, cx: &mut Context<Self>) {
self.editor.update(cx, |editor, cx| {
let max_point = editor.buffer().read(cx).read(cx).max_point();
editor.restore_hunks_in_ranges(vec![Point::zero()..max_point], window, cx)
})
}
fn keep_all(&mut self, _: &crate::KeepAll, _window: &mut Window, cx: &mut Context<Self>) {
self.thread
.update(cx, |thread, cx| thread.keep_all_edits(cx));
}
fn review_diff_hunks(
&mut self,
hunk_ranges: Vec<Range<editor::Anchor>>,
accept: bool,
cx: &mut Context<Self>,
) {
let snapshot = self.multibuffer.read(cx).snapshot(cx);
let diff_hunks_in_ranges = self
.editor
.read(cx)
.diff_hunks_in_ranges(&hunk_ranges, &snapshot)
.collect::<Vec<_>>();
for hunk in diff_hunks_in_ranges {
let buffer = self.multibuffer.read(cx).buffer(hunk.buffer_id);
if let Some(buffer) = buffer {
self.thread.update(cx, |thread, cx| {
thread.review_edits_in_range(buffer, hunk.buffer_range, accept, cx)
});
}
}
}
}
impl EventEmitter<EditorEvent> for AssistantDiff {}
impl Focusable for AssistantDiff {
fn focus_handle(&self, cx: &App) -> FocusHandle {
if self.multibuffer.read(cx).is_empty() {
self.focus_handle.clone()
} else {
self.editor.focus_handle(cx)
}
}
}
impl Item for AssistantDiff {
type Event = EditorEvent;
fn tab_icon(&self, _window: &Window, _cx: &App) -> Option<Icon> {
Some(Icon::new(IconName::ZedAssistant).color(Color::Muted))
}
fn to_item_events(event: &EditorEvent, f: impl FnMut(ItemEvent)) {
Editor::to_item_events(event, f)
}
fn deactivated(&mut self, window: &mut Window, cx: &mut Context<Self>) {
self.editor
.update(cx, |editor, cx| editor.deactivated(window, cx));
}
fn navigate(
&mut self,
data: Box<dyn Any>,
window: &mut Window,
cx: &mut Context<Self>,
) -> bool {
self.editor
.update(cx, |editor, cx| editor.navigate(data, window, cx))
}
fn tab_tooltip_text(&self, _: &App) -> Option<SharedString> {
Some("Assistant Diff".into())
}
fn tab_content(&self, params: TabContentParams, _window: &Window, cx: &App) -> AnyElement {
let summary = self
.thread
.read(cx)
.summary()
.unwrap_or("Assistant Changes".into());
Label::new(format!("Review: {}", summary))
.color(if params.selected {
Color::Default
} else {
Color::Muted
})
.into_any_element()
}
fn telemetry_event_text(&self) -> Option<&'static str> {
Some("Assistant Diff Opened")
}
fn as_searchable(&self, _: &Entity<Self>) -> Option<Box<dyn SearchableItemHandle>> {
Some(Box::new(self.editor.clone()))
}
fn for_each_project_item(
&self,
cx: &App,
f: &mut dyn FnMut(gpui::EntityId, &dyn project::ProjectItem),
) {
self.editor.for_each_project_item(cx, f)
}
fn is_singleton(&self, _: &App) -> bool {
false
}
fn set_nav_history(
&mut self,
nav_history: ItemNavHistory,
_: &mut Window,
cx: &mut Context<Self>,
) {
self.editor.update(cx, |editor, _| {
editor.set_nav_history(Some(nav_history));
});
}
fn clone_on_split(
&self,
_workspace_id: Option<workspace::WorkspaceId>,
window: &mut Window,
cx: &mut Context<Self>,
) -> Option<Entity<Self>>
where
Self: Sized,
{
Some(cx.new(|cx| Self::new(self.thread.clone(), self.workspace.clone(), window, cx)))
}
fn is_dirty(&self, cx: &App) -> bool {
self.multibuffer.read(cx).is_dirty(cx)
}
fn has_conflict(&self, cx: &App) -> bool {
self.multibuffer.read(cx).has_conflict(cx)
}
fn can_save(&self, _: &App) -> bool {
true
}
fn save(
&mut self,
format: bool,
project: Entity<Project>,
window: &mut Window,
cx: &mut Context<Self>,
) -> Task<Result<()>> {
self.editor.save(format, project, window, cx)
}
fn save_as(
&mut self,
_: Entity<Project>,
_: ProjectPath,
_window: &mut Window,
_: &mut Context<Self>,
) -> Task<Result<()>> {
unreachable!()
}
fn reload(
&mut self,
project: Entity<Project>,
window: &mut Window,
cx: &mut Context<Self>,
) -> Task<Result<()>> {
self.editor.reload(project, window, cx)
}
fn act_as_type<'a>(
&'a self,
type_id: TypeId,
self_handle: &'a Entity<Self>,
_: &'a App,
) -> Option<AnyView> {
if type_id == TypeId::of::<Self>() {
Some(self_handle.to_any())
} else if type_id == TypeId::of::<Editor>() {
Some(self.editor.to_any())
} else {
None
}
}
fn breadcrumb_location(&self, _: &App) -> ToolbarItemLocation {
ToolbarItemLocation::PrimaryLeft
}
fn breadcrumbs(&self, theme: &theme::Theme, cx: &App) -> Option<Vec<BreadcrumbText>> {
self.editor.breadcrumbs(theme, cx)
}
fn added_to_workspace(
&mut self,
workspace: &mut Workspace,
window: &mut Window,
cx: &mut Context<Self>,
) {
self.editor.update(cx, |editor, cx| {
editor.added_to_workspace(workspace, window, cx)
});
}
}
impl Render for AssistantDiff {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let is_empty = self.multibuffer.read(cx).is_empty();
div()
.track_focus(&self.focus_handle)
.key_context(if is_empty {
"EmptyPane"
} else {
"AssistantDiff"
})
.on_action(cx.listener(Self::toggle_keep))
.on_action(cx.listener(Self::reject))
.on_action(cx.listener(Self::reject_all))
.on_action(cx.listener(Self::keep_all))
.bg(cx.theme().colors().editor_background)
.flex()
.items_center()
.justify_center()
.size_full()
.when(is_empty, |el| el.child("No changes to review"))
.when(!is_empty, |el| el.child(self.editor.clone()))
}
}
fn render_diff_hunk_controls(
row: u32,
status: &DiffHunkStatus,
hunk_range: Range<editor::Anchor>,
is_created_file: bool,
line_height: Pixels,
assistant_diff: &Entity<AssistantDiff>,
window: &mut Window,
cx: &mut App,
) -> AnyElement {
let editor = assistant_diff.read(cx).editor.clone();
h_flex()
.h(line_height)
.mr_0p5()
.gap_1()
.px_0p5()
.pb_1()
.border_x_1()
.border_b_1()
.border_color(cx.theme().colors().border)
.rounded_b_md()
.bg(cx.theme().colors().editor_background)
.gap_1()
.occlude()
.shadow_md()
.children(if status.has_secondary_hunk() {
vec![
Button::new("reject", "Reject")
.disabled(is_created_file)
.key_binding(
KeyBinding::for_action_in(
&crate::Reject,
&editor.read(cx).focus_handle(cx),
window,
cx,
)
.map(|kb| kb.size(rems_from_px(12.))),
)
.on_click({
let editor = editor.clone();
move |_event, window, cx| {
editor.update(cx, |editor, cx| {
let snapshot = editor.snapshot(window, cx);
let point = hunk_range.start.to_point(&snapshot.buffer_snapshot);
editor.restore_hunks_in_ranges(vec![point..point], window, cx);
});
}
}),
Button::new(("keep", row as u64), "Keep")
.key_binding(
KeyBinding::for_action_in(
&crate::ToggleKeep,
&editor.read(cx).focus_handle(cx),
window,
cx,
)
.map(|kb| kb.size(rems_from_px(12.))),
)
.on_click({
let assistant_diff = assistant_diff.clone();
move |_event, _window, cx| {
assistant_diff.update(cx, |diff, cx| {
diff.review_diff_hunks(
vec![hunk_range.start..hunk_range.start],
true,
cx,
);
});
}
}),
]
} else {
vec![Button::new(("review", row as u64), "Review")
.key_binding(KeyBinding::for_action_in(
&ToggleKeep,
&editor.read(cx).focus_handle(cx),
window,
cx,
))
.on_click({
let assistant_diff = assistant_diff.clone();
move |_event, _window, cx| {
assistant_diff.update(cx, |diff, cx| {
diff.review_diff_hunks(
vec![hunk_range.start..hunk_range.start],
false,
cx,
);
});
}
})]
})
.when(
!editor.read(cx).buffer().read(cx).all_diff_hunks_expanded(),
|el| {
el.child(
IconButton::new(("next-hunk", row as u64), IconName::ArrowDown)
.shape(IconButtonShape::Square)
.icon_size(IconSize::Small)
// .disabled(!has_multiple_hunks)
.tooltip({
let focus_handle = editor.focus_handle(cx);
move |window, cx| {
Tooltip::for_action_in(
"Next Hunk",
&GoToHunk,
&focus_handle,
window,
cx,
)
}
})
.on_click({
let editor = editor.clone();
move |_event, window, cx| {
editor.update(cx, |editor, cx| {
let snapshot = editor.snapshot(window, cx);
let position =
hunk_range.end.to_point(&snapshot.buffer_snapshot);
editor.go_to_hunk_before_or_after_position(
&snapshot,
position,
Direction::Next,
window,
cx,
);
editor.expand_selected_diff_hunks(cx);
});
}
}),
)
.child(
IconButton::new(("prev-hunk", row as u64), IconName::ArrowUp)
.shape(IconButtonShape::Square)
.icon_size(IconSize::Small)
// .disabled(!has_multiple_hunks)
.tooltip({
let focus_handle = editor.focus_handle(cx);
move |window, cx| {
Tooltip::for_action_in(
"Previous Hunk",
&GoToPreviousHunk,
&focus_handle,
window,
cx,
)
}
})
.on_click({
let editor = editor.clone();
move |_event, window, cx| {
editor.update(cx, |editor, cx| {
let snapshot = editor.snapshot(window, cx);
let point =
hunk_range.start.to_point(&snapshot.buffer_snapshot);
editor.go_to_hunk_before_or_after_position(
&snapshot,
point,
Direction::Prev,
window,
cx,
);
editor.expand_selected_diff_hunks(cx);
});
}
}),
)
},
)
.into_any_element()
}
struct AssistantDiffAddon;
impl editor::Addon for AssistantDiffAddon {
fn to_any(&self) -> &dyn std::any::Any {
self
}
fn extend_key_context(&self, key_context: &mut gpui::KeyContext, _: &App) {
key_context.add("assistant_diff");
}
}
pub struct AssistantDiffToolbar {
assistant_diff: Option<WeakEntity<AssistantDiff>>,
_workspace: WeakEntity<Workspace>,
}
impl AssistantDiffToolbar {
pub fn new(workspace: &Workspace, _: &mut Context<Self>) -> Self {
Self {
assistant_diff: None,
_workspace: workspace.weak_handle(),
}
}
fn assistant_diff(&self, _: &App) -> Option<Entity<AssistantDiff>> {
self.assistant_diff.as_ref()?.upgrade()
}
fn dispatch_action(&self, action: &dyn Action, window: &mut Window, cx: &mut Context<Self>) {
if let Some(assistant_diff) = self.assistant_diff(cx) {
assistant_diff.focus_handle(cx).focus(window);
}
let action = action.boxed_clone();
cx.defer(move |cx| {
cx.dispatch_action(action.as_ref());
})
}
}
impl EventEmitter<ToolbarItemEvent> for AssistantDiffToolbar {}
impl ToolbarItemView for AssistantDiffToolbar {
fn set_active_pane_item(
&mut self,
active_pane_item: Option<&dyn ItemHandle>,
_: &mut Window,
cx: &mut Context<Self>,
) -> ToolbarItemLocation {
self.assistant_diff = active_pane_item
.and_then(|item| item.act_as::<AssistantDiff>(cx))
.map(|entity| entity.downgrade());
if self.assistant_diff.is_some() {
ToolbarItemLocation::PrimaryRight
} else {
ToolbarItemLocation::Hidden
}
}
fn pane_focus_update(
&mut self,
_pane_focused: bool,
_window: &mut Window,
_cx: &mut Context<Self>,
) {
}
}
impl Render for AssistantDiffToolbar {
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let assistant_diff = match self.assistant_diff(cx) {
Some(ad) => ad,
None => return div(),
};
let is_empty = assistant_diff.read(cx).multibuffer.read(cx).is_empty();
if is_empty {
return div();
}
h_group_xl()
.my_neg_1()
.items_center()
.p_1()
.flex_wrap()
.justify_between()
.child(
h_group_sm()
.child(
Button::new("reject-all", "Reject All").on_click(cx.listener(
|this, _, window, cx| {
this.dispatch_action(&crate::RejectAll, window, cx)
},
)),
)
.child(Button::new("keep-all", "Keep All").on_click(cx.listener(
|this, _, window, cx| this.dispatch_action(&crate::KeepAll, window, cx),
))),
)
}
}

View File

@@ -39,8 +39,8 @@ use crate::thread::{Thread, ThreadError, ThreadId};
use crate::thread_history::{PastContext, PastThread, ThreadHistory};
use crate::thread_store::ThreadStore;
use crate::{
InlineAssistant, NewPromptEditor, NewThread, OpenActiveThreadAsMarkdown, OpenConfiguration,
OpenHistory,
AssistantDiff, InlineAssistant, NewPromptEditor, NewThread, OpenActiveThreadAsMarkdown,
OpenAssistantDiff, OpenConfiguration, OpenHistory,
};
action_with_deprecated_aliases!(
@@ -65,6 +65,12 @@ pub fn init(cx: &mut App) {
panel.update(cx, |panel, cx| panel.open_history(window, cx));
}
})
.register_action(|workspace, _: &OpenConfiguration, window, cx| {
if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
workspace.focus_panel::<AssistantPanel>(window, cx);
panel.update(cx, |panel, cx| panel.open_configuration(window, cx));
}
})
.register_action(|workspace, _: &NewPromptEditor, window, cx| {
if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
workspace.focus_panel::<AssistantPanel>(window, cx);
@@ -84,6 +90,14 @@ pub fn init(cx: &mut App) {
workspace.focus_panel::<AssistantPanel>(window, cx);
panel.update(cx, |panel, cx| panel.open_configuration(window, cx));
}
})
.register_action(|workspace, _: &OpenAssistantDiff, window, cx| {
if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
workspace.focus_panel::<AssistantPanel>(window, cx);
panel.update(cx, |panel, cx| {
panel.open_assistant_diff(&OpenAssistantDiff, window, cx);
});
}
});
},
)
@@ -113,7 +127,7 @@ pub struct AssistantPanel {
active_view: ActiveView,
history_store: Entity<HistoryStore>,
history: Entity<ThreadHistory>,
new_item_context_menu_handle: PopoverMenuHandle<ContextMenu>,
assistant_dropdown_menu_handle: PopoverMenuHandle<ContextMenu>,
width: Option<Pixels>,
height: Option<Pixels>,
}
@@ -213,7 +227,7 @@ impl AssistantPanel {
.unwrap(),
history_store: history_store.clone(),
history: cx.new(|cx| ThreadHistory::new(weak_self, history_store, cx)),
new_item_context_menu_handle: PopoverMenuHandle::default(),
assistant_dropdown_menu_handle: PopoverMenuHandle::default(),
width: None,
height: None,
}
@@ -225,12 +239,12 @@ impl AssistantPanel {
window: &mut Window,
cx: &mut Context<Workspace>,
) {
let settings = AssistantSettings::get_global(cx);
if !settings.enabled {
return;
if workspace
.panel::<Self>(cx)
.is_some_and(|panel| panel.read(cx).enabled(cx))
{
workspace.toggle_panel_focus::<Self>(window, cx);
}
workspace.toggle_panel_focus::<Self>(window, cx);
}
pub(crate) fn local_timezone(&self) -> UtcOffset {
@@ -426,6 +440,16 @@ impl AssistantPanel {
})
}
pub fn open_assistant_diff(
&mut self,
_: &OpenAssistantDiff,
window: &mut Window,
cx: &mut Context<Self>,
) {
let thread = self.thread.read(cx).thread().clone();
AssistantDiff::deploy(thread, self.workspace.clone(), window, cx).log_err();
}
pub(crate) fn open_configuration(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let context_server_manager = self.thread_store.read(cx).context_server_manager();
let tools = self.thread_store.read(cx).tools();
@@ -560,20 +584,14 @@ impl Focusable for AssistantPanel {
match self.active_view {
ActiveView::Thread => self.message_editor.focus_handle(cx),
ActiveView::History => self.history.focus_handle(cx),
ActiveView::PromptEditor => {
if let Some(context_editor) = self.context_editor.as_ref() {
context_editor.focus_handle(cx)
} else {
cx.focus_handle()
}
}
ActiveView::Configuration => {
if let Some(configuration) = self.configuration.as_ref() {
configuration.focus_handle(cx)
} else {
cx.focus_handle()
}
}
ActiveView::PromptEditor => match self.context_editor.as_ref() {
Some(context_editor) => context_editor.focus_handle(cx),
_ => cx.focus_handle(),
},
ActiveView::Configuration => match self.configuration.as_ref() {
Some(configuration) => configuration.focus_handle(cx),
_ => cx.focus_handle(),
},
}
}
}
@@ -637,12 +655,8 @@ impl Panel for AssistantPanel {
}
fn icon(&self, _window: &Window, cx: &App) -> Option<IconName> {
let settings = AssistantSettings::get_global(cx);
if !settings.enabled || !settings.button {
return None;
}
Some(IconName::ZedAssistant)
(self.enabled(cx) && AssistantSettings::get_global(cx).button)
.then_some(IconName::ZedAssistant)
}
fn icon_tooltip(&self, _window: &Window, _cx: &App) -> Option<&'static str> {
@@ -656,11 +670,20 @@ impl Panel for AssistantPanel {
fn activation_priority(&self) -> u32 {
3
}
fn enabled(&self, cx: &App) -> bool {
AssistantSettings::get_global(cx).enabled
}
}
impl AssistantPanel {
fn render_toolbar(&self, cx: &mut Context<Self>) -> impl IntoElement {
fn render_toolbar(
&self,
_window: &mut Window,
cx: &mut Context<Self>,
) -> impl IntoElement + use<> {
let thread = self.thread.read(cx);
let focus_handle = self.focus_handle(cx);
let title = match self.active_view {
ActiveView::Thread => {
@@ -680,7 +703,7 @@ impl AssistantPanel {
})
.unwrap_or_else(|| SharedString::from("Loading Summary…")),
ActiveView::History => "History".into(),
ActiveView::Configuration => "Assistant Settings".into(),
ActiveView::Configuration => "Settings".into(),
};
h_flex()
@@ -720,57 +743,47 @@ impl AssistantPanel {
.border_color(cx.theme().colors().border)
.gap(DynamicSpacing::Base02.rems(cx))
.child(
PopoverMenu::new("assistant-toolbar-new-popover-menu")
IconButton::new("new", IconName::Plus)
.icon_size(IconSize::Small)
.style(ButtonStyle::Subtle)
.tooltip(move |window, cx| {
Tooltip::for_action_in(
"New Thread",
&NewThread,
&focus_handle,
window,
cx,
)
})
.on_click(move |_event, window, cx| {
window.dispatch_action(NewThread.boxed_clone(), cx);
}),
)
.child(
PopoverMenu::new("assistant-menu")
.trigger_with_tooltip(
IconButton::new("new", IconName::Plus)
IconButton::new("new", IconName::Ellipsis)
.icon_size(IconSize::Small)
.style(ButtonStyle::Subtle),
Tooltip::text("New…"),
Tooltip::text("Toggle Assistant Menu"),
)
.anchor(Corner::TopRight)
.with_handle(self.new_item_context_menu_handle.clone())
.with_handle(self.assistant_dropdown_menu_handle.clone())
.menu(move |window, cx| {
Some(ContextMenu::build(
window,
cx,
|menu, _window, _cx| {
menu.action("New Thread", NewThread.boxed_clone())
.action(
"New Prompt Editor",
NewPromptEditor.boxed_clone(),
)
menu.action(
"New Prompt Editor",
NewPromptEditor.boxed_clone(),
)
.separator()
.action("History", OpenHistory.boxed_clone())
.action("Settings", OpenConfiguration.boxed_clone())
},
))
}),
)
.child(
IconButton::new("open-history", IconName::HistoryRerun)
.icon_size(IconSize::Small)
.style(ButtonStyle::Subtle)
.tooltip({
let focus_handle = self.focus_handle(cx);
move |window, cx| {
Tooltip::for_action_in(
"History",
&OpenHistory,
&focus_handle,
window,
cx,
)
}
})
.on_click(move |_event, window, cx| {
window.dispatch_action(OpenHistory.boxed_clone(), cx);
}),
)
.child(
IconButton::new("configure-assistant", IconName::Settings)
.icon_size(IconSize::Small)
.style(ButtonStyle::Subtle)
.tooltip(Tooltip::text("Assistant Settings"))
.on_click(move |_event, window, cx| {
window.dispatch_action(OpenConfiguration.boxed_clone(), cx);
}),
),
),
)
@@ -810,7 +823,7 @@ impl AssistantPanel {
&self,
window: &mut Window,
cx: &mut Context<Self>,
) -> impl IntoElement {
) -> impl IntoElement + use<> {
let recent_history = self
.history_store
.update(cx, |this, cx| this.recent_entries(6, cx));
@@ -1103,9 +1116,13 @@ impl Render for AssistantPanel {
.on_action(cx.listener(|this, _: &OpenHistory, window, cx| {
this.open_history(window, cx);
}))
.on_action(cx.listener(|this, _: &OpenConfiguration, window, cx| {
this.open_configuration(window, cx);
}))
.on_action(cx.listener(Self::open_active_thread_as_markdown))
.on_action(cx.listener(Self::deploy_prompt_library))
.child(self.render_toolbar(cx))
.on_action(cx.listener(Self::open_assistant_diff))
.child(self.render_toolbar(window, cx))
.map(|parent| match self.active_view {
ActiveView::Thread => parent
.child(self.render_active_thread_or_empty_state(window, cx))

View File

@@ -676,10 +676,13 @@ impl CodegenAlternative {
.update(cx, |this, cx| {
this.message_id = message_id;
this.last_equal_ranges.clear();
if let Err(error) = result {
this.status = CodegenStatus::Error(error);
} else {
this.status = CodegenStatus::Done;
match result {
Err(error) => {
this.status = CodegenStatus::Error(error);
}
_ => {
this.status = CodegenStatus::Done;
}
}
this.elapsed_time = Some(elapsed_time);
this.completion = Some(completion.lock().clone());

View File

@@ -1,12 +1,12 @@
use std::path::Path;
use std::rc::Rc;
use std::ops::Range;
use file_icons::FileIcons;
use gpui::{App, Entity, SharedString};
use language::Buffer;
use language_model::{LanguageModelRequestMessage, MessageContent};
use project::ProjectPath;
use serde::{Deserialize, Serialize};
use text::BufferId;
use text::{Anchor, BufferId};
use ui::IconName;
use util::post_inc;
@@ -38,6 +38,7 @@ pub struct ContextSnapshot {
pub enum ContextKind {
File,
Directory,
Symbol,
FetchedUrl,
Thread,
}
@@ -47,8 +48,9 @@ impl ContextKind {
match self {
ContextKind::File => IconName::File,
ContextKind::Directory => IconName::Folder,
ContextKind::Symbol => IconName::Code,
ContextKind::FetchedUrl => IconName::Globe,
ContextKind::Thread => IconName::MessageCircle,
ContextKind::Thread => IconName::MessageBubbles,
}
}
}
@@ -57,6 +59,7 @@ impl ContextKind {
pub enum AssistantContext {
File(FileContext),
Directory(DirectoryContext),
Symbol(SymbolContext),
FetchedUrl(FetchedUrlContext),
Thread(ThreadContext),
}
@@ -66,6 +69,7 @@ impl AssistantContext {
match self {
Self::File(file) => file.id,
Self::Directory(directory) => directory.snapshot.id,
Self::Symbol(symbol) => symbol.id,
Self::FetchedUrl(url) => url.id,
Self::Thread(thread) => thread.id,
}
@@ -80,11 +84,17 @@ pub struct FileContext {
#[derive(Debug)]
pub struct DirectoryContext {
pub path: Rc<Path>,
pub path: ProjectPath,
pub context_buffers: Vec<ContextBuffer>,
pub snapshot: ContextSnapshot,
}
#[derive(Debug)]
pub struct SymbolContext {
pub id: ContextId,
pub context_symbol: ContextSymbol,
}
#[derive(Debug)]
pub struct FetchedUrlContext {
pub id: ContextId,
@@ -113,11 +123,30 @@ pub struct ContextBuffer {
pub text: SharedString,
}
#[derive(Debug, Clone)]
pub struct ContextSymbol {
pub id: ContextSymbolId,
pub buffer: Entity<Buffer>,
pub buffer_version: clock::Global,
/// The range that the symbol encloses, e.g. for function symbol, this will
/// include not only the signature, but also the body
pub enclosing_range: Range<Anchor>,
pub text: SharedString,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct ContextSymbolId {
pub path: ProjectPath,
pub name: SharedString,
pub range: Range<Anchor>,
}
impl AssistantContext {
pub fn snapshot(&self, cx: &App) -> Option<ContextSnapshot> {
match &self {
Self::File(file_context) => file_context.snapshot(cx),
Self::Directory(directory_context) => Some(directory_context.snapshot()),
Self::Symbol(symbol_context) => symbol_context.snapshot(cx),
Self::FetchedUrl(fetched_url_context) => Some(fetched_url_context.snapshot()),
Self::Thread(thread_context) => Some(thread_context.snapshot(cx)),
}
@@ -155,17 +184,18 @@ impl FileContext {
impl DirectoryContext {
pub fn new(
id: ContextId,
path: &Path,
project_path: ProjectPath,
context_buffers: Vec<ContextBuffer>,
) -> DirectoryContext {
let full_path: SharedString = path.to_string_lossy().into_owned().into();
let full_path: SharedString = project_path.path.to_string_lossy().into_owned().into();
let name = match path.file_name() {
let name = match project_path.path.file_name() {
Some(name) => name.to_string_lossy().into_owned().into(),
None => full_path.clone(),
};
let parent = path
let parent = project_path
.path
.parent()
.and_then(|p| p.file_name())
.map(|p| p.to_string_lossy().into_owned().into());
@@ -178,7 +208,7 @@ impl DirectoryContext {
.into();
DirectoryContext {
path: path.into(),
path: project_path,
context_buffers,
snapshot: ContextSnapshot {
id,
@@ -197,6 +227,27 @@ impl DirectoryContext {
}
}
impl SymbolContext {
pub fn snapshot(&self, cx: &App) -> Option<ContextSnapshot> {
let buffer = self.context_symbol.buffer.read(cx);
let name = self.context_symbol.id.name.clone();
let path = buffer_path_log_err(buffer)?
.to_string_lossy()
.into_owned()
.into();
Some(ContextSnapshot {
id: self.id,
name,
parent: Some(path),
tooltip: None,
icon_path: None,
kind: ContextKind::Symbol,
text: Box::new([self.context_symbol.text.clone()]),
})
}
}
impl FetchedUrlContext {
pub fn snapshot(&self) -> ContextSnapshot {
ContextSnapshot {
@@ -232,6 +283,7 @@ pub fn attach_context_to_message(
) {
let mut file_context = Vec::new();
let mut directory_context = Vec::new();
let mut symbol_context = Vec::new();
let mut fetch_context = Vec::new();
let mut thread_context = Vec::new();
@@ -241,6 +293,7 @@ pub fn attach_context_to_message(
match context.kind {
ContextKind::File => file_context.push(context),
ContextKind::Directory => directory_context.push(context),
ContextKind::Symbol => symbol_context.push(context),
ContextKind::FetchedUrl => fetch_context.push(context),
ContextKind::Thread => thread_context.push(context),
}
@@ -251,6 +304,9 @@ pub fn attach_context_to_message(
if !directory_context.is_empty() {
capacity += 1;
}
if !symbol_context.is_empty() {
capacity += 1;
}
if !fetch_context.is_empty() {
capacity += 1 + fetch_context.len();
}
@@ -281,6 +337,15 @@ pub fn attach_context_to_message(
}
}
if !symbol_context.is_empty() {
context_chunks.push("The following symbols are available:\n");
for context in &symbol_context {
for chunk in &context.text {
context_chunks.push(&chunk);
}
}
}
if !fetch_context.is_empty() {
context_chunks.push("The following fetched results are available:\n");
for context in &fetch_context {

View File

@@ -1,6 +1,7 @@
mod completion_provider;
mod fetch_context_picker;
mod file_context_picker;
mod symbol_context_picker;
mod thread_context_picker;
use std::ops::Range;
@@ -16,6 +17,7 @@ use gpui::{
};
use multi_buffer::MultiBufferRow;
use project::ProjectPath;
use symbol_context_picker::SymbolContextPicker;
use thread_context_picker::{render_thread_context_entry, ThreadContextEntry};
use ui::{
prelude::*, ButtonLike, ContextMenu, ContextMenuEntry, ContextMenuItem, Disclosure, TintColor,
@@ -39,6 +41,7 @@ pub enum ConfirmBehavior {
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum ContextPickerMode {
File,
Symbol,
Fetch,
Thread,
}
@@ -49,6 +52,7 @@ impl TryFrom<&str> for ContextPickerMode {
fn try_from(value: &str) -> Result<Self, Self::Error> {
match value {
"file" => Ok(Self::File),
"symbol" => Ok(Self::Symbol),
"fetch" => Ok(Self::Fetch),
"thread" => Ok(Self::Thread),
_ => Err(format!("Invalid context picker mode: {}", value)),
@@ -60,6 +64,7 @@ impl ContextPickerMode {
pub fn mention_prefix(&self) -> &'static str {
match self {
Self::File => "file",
Self::Symbol => "symbol",
Self::Fetch => "fetch",
Self::Thread => "thread",
}
@@ -68,6 +73,7 @@ impl ContextPickerMode {
pub fn label(&self) -> &'static str {
match self {
Self::File => "Files & Directories",
Self::Symbol => "Symbols",
Self::Fetch => "Fetch",
Self::Thread => "Thread",
}
@@ -76,8 +82,9 @@ impl ContextPickerMode {
pub fn icon(&self) -> IconName {
match self {
Self::File => IconName::File,
Self::Symbol => IconName::Code,
Self::Fetch => IconName::Globe,
Self::Thread => IconName::MessageCircle,
Self::Thread => IconName::MessageBubbles,
}
}
}
@@ -86,6 +93,7 @@ impl ContextPickerMode {
enum ContextPickerState {
Default(Entity<ContextMenu>),
File(Entity<FileContextPicker>),
Symbol(Entity<SymbolContextPicker>),
Fetch(Entity<FetchContextPicker>),
Thread(Entity<ThreadContextPicker>),
}
@@ -205,6 +213,18 @@ impl ContextPicker {
)
}));
}
ContextPickerMode::Symbol => {
self.mode = ContextPickerState::Symbol(cx.new(|cx| {
SymbolContextPicker::new(
context_picker.clone(),
self.workspace.clone(),
self.context_store.clone(),
self.confirm_behavior,
window,
cx,
)
}));
}
ContextPickerMode::Fetch => {
self.mode = ContextPickerState::Fetch(cx.new(|cx| {
FetchContextPicker::new(
@@ -416,6 +436,7 @@ impl Focusable for ContextPicker {
match &self.mode {
ContextPickerState::Default(menu) => menu.focus_handle(cx),
ContextPickerState::File(file_picker) => file_picker.focus_handle(cx),
ContextPickerState::Symbol(symbol_picker) => symbol_picker.focus_handle(cx),
ContextPickerState::Fetch(fetch_picker) => fetch_picker.focus_handle(cx),
ContextPickerState::Thread(thread_picker) => thread_picker.focus_handle(cx),
}
@@ -430,6 +451,7 @@ impl Render for ContextPicker {
.map(|parent| match &self.mode {
ContextPickerState::Default(menu) => parent.child(menu.clone()),
ContextPickerState::File(file_picker) => parent.child(file_picker.clone()),
ContextPickerState::Symbol(symbol_picker) => parent.child(symbol_picker.clone()),
ContextPickerState::Fetch(fetch_picker) => parent.child(fetch_picker.clone()),
ContextPickerState::Thread(thread_picker) => parent.child(thread_picker.clone()),
})
@@ -446,7 +468,11 @@ enum RecentEntry {
fn supported_context_picker_modes(
thread_store: &Option<WeakEntity<ThreadStore>>,
) -> Vec<ContextPickerMode> {
let mut modes = vec![ContextPickerMode::File, ContextPickerMode::Fetch];
let mut modes = vec![
ContextPickerMode::File,
ContextPickerMode::Symbol,
ContextPickerMode::Fetch,
];
if thread_store.is_some() {
modes.push(ContextPickerMode::Thread);
}

View File

@@ -1,6 +1,6 @@
use std::cell::RefCell;
use std::ops::Range;
use std::path::{Path, PathBuf};
use std::path::Path;
use std::rc::Rc;
use std::sync::atomic::AtomicBool;
use std::sync::Arc;
@@ -12,7 +12,7 @@ use gpui::{App, Entity, Task, WeakEntity};
use http_client::HttpClientWithUrl;
use language::{Buffer, CodeLabel, HighlightId};
use lsp::CompletionContext;
use project::{Completion, CompletionIntent, ProjectPath, WorktreeId};
use project::{Completion, CompletionIntent, ProjectPath, Symbol, WorktreeId};
use rope::Point;
use text::{Anchor, ToPoint};
use ui::prelude::*;
@@ -70,18 +70,18 @@ impl ContextPickerCompletionProvider {
.filter_map(|entry| match entry {
super::RecentEntry::File {
project_path,
path_prefix: _,
} => Self::completion_for_path(
path_prefix,
} => Some(Self::completion_for_path(
project_path.clone(),
path_prefix,
true,
false,
excerpt_id,
source_range.clone(),
editor.clone(),
context_store.clone(),
workspace.clone(),
cx,
),
)),
super::RecentEntry::Thread(thread_context_entry) => {
let thread_store = thread_store
.as_ref()
@@ -120,56 +120,24 @@ impl ContextPickerCompletionProvider {
completions
}
fn full_path_for_entry(
worktree_id: WorktreeId,
path: &Path,
workspace: Entity<Workspace>,
cx: &App,
) -> Option<PathBuf> {
let worktree = workspace
.read(cx)
.project()
.read(cx)
.worktree_for_id(worktree_id, cx)?
.read(cx);
let mut full_path = PathBuf::from(worktree.root_name());
full_path.push(path);
Some(full_path)
}
fn build_code_label_for_full_path(
worktree_id: WorktreeId,
path: &Path,
workspace: Entity<Workspace>,
file_name: &str,
directory: Option<&str>,
cx: &App,
) -> Option<CodeLabel> {
) -> CodeLabel {
let comment_id = cx.theme().syntax().highlight_id("comment").map(HighlightId);
let mut label = CodeLabel::default();
let worktree = workspace
.read(cx)
.project()
.read(cx)
.worktree_for_id(worktree_id, cx)?;
let entry = worktree.read(cx).entry_for_path(&path)?;
let file_name = path.file_name()?.to_string_lossy();
label.push_str(&file_name, None);
if entry.is_dir() {
label.push_str("/ ", None);
} else {
label.push_str(" ", None);
};
label.push_str(" ", None);
let mut path_hint = PathBuf::from(worktree.read(cx).root_name());
if let Some(path_to_entry) = path.parent() {
path_hint.push(path_to_entry);
if let Some(directory) = directory {
label.push_str(&directory, comment_id);
}
label.push_str(&path_hint.to_string_lossy(), comment_id);
label.filter_range = 0..label.text().len();
Some(label)
label
}
fn completion_for_thread(
@@ -184,7 +152,7 @@ impl ContextPickerCompletionProvider {
let icon_for_completion = if recent {
IconName::HistoryRerun
} else {
IconName::MessageCircle
IconName::MessageBubbles
};
let new_text = format!("@thread {}", thread_entry.summary);
let new_text_len = new_text.len();
@@ -196,7 +164,7 @@ impl ContextPickerCompletionProvider {
source: project::CompletionSource::Custom,
icon_path: Some(icon_for_completion.path().into()),
confirm: Some(confirm_completion_callback(
IconName::MessageCircle.path().into(),
IconName::MessageBubbles.path().into(),
thread_entry.summary.clone(),
excerpt_id,
source_range.start,
@@ -274,32 +242,36 @@ impl ContextPickerCompletionProvider {
fn completion_for_path(
project_path: ProjectPath,
path_prefix: &str,
is_recent: bool,
is_directory: bool,
excerpt_id: ExcerptId,
source_range: Range<Anchor>,
editor: Entity<Editor>,
context_store: Entity<ContextStore>,
workspace: Entity<Workspace>,
cx: &App,
) -> Option<Completion> {
) -> Completion {
let (file_name, directory) = super::file_context_picker::extract_file_name_and_directory(
&project_path.path,
path_prefix,
);
let label = Self::build_code_label_for_full_path(
project_path.worktree_id,
&project_path.path,
workspace.clone(),
&file_name,
directory.as_ref().map(|s| s.as_ref()),
cx,
)?;
let full_path = Self::full_path_for_entry(
project_path.worktree_id,
&project_path.path,
workspace.clone(),
cx,
)?;
);
let full_path = if let Some(directory) = directory {
format!("{}{}", directory, file_name)
} else {
file_name.to_string()
};
let crease_icon_path = if is_directory {
FileIcons::get_folder_icon(false, cx).unwrap_or_else(|| IconName::Folder.path().into())
} else {
FileIcons::get_icon(&full_path, cx).unwrap_or_else(|| IconName::File.path().into())
FileIcons::get_icon(Path::new(&full_path), cx)
.unwrap_or_else(|| IconName::File.path().into())
};
let completion_icon_path = if is_recent {
IconName::HistoryRerun.path().into()
@@ -307,15 +279,9 @@ impl ContextPickerCompletionProvider {
crease_icon_path.clone()
};
let crease_name = project_path
.path
.file_name()
.map(|file_name| file_name.to_string_lossy().to_string())
.unwrap_or_else(|| "untitled".to_string());
let new_text = format!("@file {}", full_path.to_string_lossy());
let new_text = format!("@file {}", full_path);
let new_text_len = new_text.len();
Some(Completion {
Completion {
old_range: source_range.clone(),
new_text,
label,
@@ -324,7 +290,7 @@ impl ContextPickerCompletionProvider {
icon_path: Some(completion_icon_path),
confirm: Some(confirm_completion_callback(
crease_icon_path,
crease_name.into(),
file_name,
excerpt_id,
source_range.start,
new_text_len,
@@ -340,6 +306,66 @@ impl ContextPickerCompletionProvider {
})
},
)),
}
}
fn completion_for_symbol(
symbol: Symbol,
excerpt_id: ExcerptId,
source_range: Range<Anchor>,
editor: Entity<Editor>,
context_store: Entity<ContextStore>,
workspace: Entity<Workspace>,
cx: &mut App,
) -> Option<Completion> {
let path_prefix = workspace
.read(cx)
.project()
.read(cx)
.worktree_for_id(symbol.path.worktree_id, cx)?
.read(cx)
.root_name();
let (file_name, _) = super::file_context_picker::extract_file_name_and_directory(
&symbol.path.path,
path_prefix,
);
let comment_id = cx.theme().syntax().highlight_id("comment").map(HighlightId);
let mut label = CodeLabel::plain(symbol.name.clone(), None);
label.push_str(" ", None);
label.push_str(&file_name, comment_id);
let new_text = format!("@symbol {}:{}", file_name, symbol.name);
let new_text_len = new_text.len();
Some(Completion {
old_range: source_range.clone(),
new_text,
label,
documentation: None,
source: project::CompletionSource::Custom,
icon_path: Some(IconName::Code.path().into()),
confirm: Some(confirm_completion_callback(
IconName::Code.path().into(),
symbol.name.clone().into(),
excerpt_id,
source_range.start,
new_text_len,
editor.clone(),
move |cx| {
let symbol = symbol.clone();
let context_store = context_store.clone();
let workspace = workspace.clone();
super::symbol_context_picker::add_symbol(
symbol.clone(),
false,
workspace.clone(),
context_store.downgrade(),
cx,
)
.detach_and_log_err(cx);
},
)),
})
}
}
@@ -384,46 +410,72 @@ impl CompletionProvider for ContextPickerCompletionProvider {
cx.spawn(async move |_, cx| {
let mut completions = Vec::new();
let MentionCompletion {
mode: category,
argument,
..
} = state;
let MentionCompletion { mode, argument, .. } = state;
let query = argument.unwrap_or_else(|| "".to_string());
match category {
match mode {
Some(ContextPickerMode::File) => {
let path_matches = cx
.update(|cx| {
super::file_context_picker::search_paths(
query,
Arc::new(AtomicBool::default()),
Arc::<AtomicBool>::default(),
&workspace,
cx,
)
})?
.await;
completions.reserve(path_matches.len());
cx.update(|cx| {
completions.extend(path_matches.iter().filter_map(|mat| {
let editor = editor.upgrade()?;
Self::completion_for_path(
ProjectPath {
worktree_id: WorktreeId::from_usize(mat.worktree_id),
path: mat.path.clone(),
if let Some(editor) = editor.upgrade() {
completions.reserve(path_matches.len());
cx.update(|cx| {
completions.extend(path_matches.iter().map(|mat| {
Self::completion_for_path(
ProjectPath {
worktree_id: WorktreeId::from_usize(mat.worktree_id),
path: mat.path.clone(),
},
&mat.path_prefix,
false,
mat.is_dir,
excerpt_id,
source_range.clone(),
editor.clone(),
context_store.clone(),
cx,
)
}));
})?;
}
}
Some(ContextPickerMode::Symbol) => {
if let Some(editor) = editor.upgrade() {
let symbol_matches = cx
.update(|cx| {
super::symbol_context_picker::search_symbols(
query,
Arc::new(AtomicBool::default()),
&workspace,
cx,
)
})?
.await?;
cx.update(|cx| {
completions.extend(symbol_matches.into_iter().filter_map(
|(_, symbol)| {
Self::completion_for_symbol(
symbol,
excerpt_id,
source_range.clone(),
editor.clone(),
context_store.clone(),
workspace.clone(),
cx,
)
},
false,
mat.is_dir,
excerpt_id,
source_range.clone(),
editor.clone(),
context_store.clone(),
workspace.clone(),
cx,
)
}));
})?;
));
})?;
}
}
Some(ContextPickerMode::Fetch) => {
if let Some(editor) = editor.upgrade() {
@@ -529,15 +581,14 @@ impl CompletionProvider for ContextPickerCompletionProvider {
let line_start = Point::new(position.row, 0);
let offset_to_line = buffer.point_to_offset(line_start);
let mut lines = buffer.text_for_range(line_start..position).lines();
if let Some(line) = lines.next() {
MentionCompletion::try_parse(line, offset_to_line)
match lines.next() {
Some(line) => MentionCompletion::try_parse(line, offset_to_line)
.map(|completion| {
completion.source_range.start <= offset_to_line + position.column as usize
&& completion.source_range.end >= offset_to_line + position.column as usize
})
.unwrap_or(false)
} else {
false
.unwrap_or(false),
_ => false,
}
}
@@ -771,7 +822,6 @@ mod tests {
.unwrap();
}
//TODO: Construct the editor without an actual buffer that points to a file
let item = workspace
.update_in(&mut cx, |workspace, window, cx| {
workspace.open_path(
@@ -821,11 +871,12 @@ mod tests {
assert_eq!(
current_completion_labels(editor),
&[
format!("seven.txt {}", separator!("dir/b")).as_str(),
format!("six.txt {}", separator!("dir/b")).as_str(),
format!("five.txt {}", separator!("dir/b")).as_str(),
format!("four.txt {}", separator!("dir/a")).as_str(),
"seven.txt dir/b/",
"six.txt dir/b/",
"five.txt dir/b/",
"four.txt dir/a/",
"Files & Directories",
"Symbols",
"Fetch"
]
);
@@ -853,10 +904,7 @@ mod tests {
editor.update(&mut cx, |editor, cx| {
assert_eq!(editor.text(cx), "Lorem @file one");
assert!(editor.has_visible_completions_menu());
assert_eq!(
current_completion_labels(editor),
vec![format!("one.txt {}", separator!("dir/a")).as_str(),]
);
assert_eq!(current_completion_labels(editor), vec!["one.txt dir/a/"]);
});
editor.update_in(&mut cx, |editor, window, cx| {
@@ -865,10 +913,7 @@ mod tests {
});
editor.update(&mut cx, |editor, cx| {
assert_eq!(
editor.text(cx),
format!("Lorem @file {}", separator!("dir/a/one.txt"))
);
assert_eq!(editor.text(cx), "Lorem @file dir/a/one.txt",);
assert!(!editor.has_visible_completions_menu());
assert_eq!(
crease_ranges(editor, cx),
@@ -879,10 +924,7 @@ mod tests {
cx.simulate_input(" ");
editor.update(&mut cx, |editor, cx| {
assert_eq!(
editor.text(cx),
format!("Lorem @file {} ", separator!("dir/a/one.txt"))
);
assert_eq!(editor.text(cx), "Lorem @file dir/a/one.txt ",);
assert!(!editor.has_visible_completions_menu());
assert_eq!(
crease_ranges(editor, cx),
@@ -893,10 +935,7 @@ mod tests {
cx.simulate_input("Ipsum ");
editor.update(&mut cx, |editor, cx| {
assert_eq!(
editor.text(cx),
format!("Lorem @file {} Ipsum ", separator!("dir/a/one.txt"))
);
assert_eq!(editor.text(cx), "Lorem @file dir/a/one.txt Ipsum ",);
assert!(!editor.has_visible_completions_menu());
assert_eq!(
crease_ranges(editor, cx),
@@ -907,10 +946,7 @@ mod tests {
cx.simulate_input("@file ");
editor.update(&mut cx, |editor, cx| {
assert_eq!(
editor.text(cx),
format!("Lorem @file {} Ipsum @file ", separator!("dir/a/one.txt"))
);
assert_eq!(editor.text(cx), "Lorem @file dir/a/one.txt Ipsum @file ",);
assert!(editor.has_visible_completions_menu());
assert_eq!(
crease_ranges(editor, cx),
@@ -927,11 +963,7 @@ mod tests {
editor.update(&mut cx, |editor, cx| {
assert_eq!(
editor.text(cx),
format!(
"Lorem @file {} Ipsum @file {}",
separator!("dir/a/one.txt"),
separator!("dir/b/seven.txt")
)
"Lorem @file dir/a/one.txt Ipsum @file dir/b/seven.txt"
);
assert!(!editor.has_visible_completions_menu());
assert_eq!(
@@ -948,11 +980,7 @@ mod tests {
editor.update(&mut cx, |editor, cx| {
assert_eq!(
editor.text(cx),
format!(
"Lorem @file {} Ipsum @file {}\n@",
separator!("dir/a/one.txt"),
separator!("dir/b/seven.txt")
)
"Lorem @file dir/a/one.txt Ipsum @file dir/b/seven.txt\n@"
);
assert!(editor.has_visible_completions_menu());
assert_eq!(
@@ -973,12 +1001,7 @@ mod tests {
editor.update(&mut cx, |editor, cx| {
assert_eq!(
editor.text(cx),
format!(
"Lorem @file {} Ipsum @file {}\n@file {}",
separator!("dir/a/one.txt"),
separator!("dir/b/seven.txt"),
separator!("dir/b/six.txt"),
)
"Lorem @file dir/a/one.txt Ipsum @file dir/b/seven.txt\n@file dir/b/six.txt"
);
assert!(!editor.has_visible_completions_menu());
assert_eq!(

View File

@@ -273,17 +273,17 @@ pub(crate) fn search_paths(
}
}
pub fn render_file_context_entry(
id: ElementId,
pub fn extract_file_name_and_directory(
path: &Path,
path_prefix: &Arc<str>,
is_directory: bool,
context_store: WeakEntity<ContextStore>,
cx: &App,
) -> Stateful<Div> {
let (file_name, directory) = if path == Path::new("") {
path_prefix: &str,
) -> (SharedString, Option<SharedString>) {
if path == Path::new("") {
(
SharedString::from(path_prefix.trim_end_matches('/').to_string()),
SharedString::from(
path_prefix
.trim_end_matches(std::path::MAIN_SEPARATOR)
.to_string(),
),
None,
)
} else {
@@ -294,7 +294,9 @@ pub fn render_file_context_entry(
.to_string()
.into();
let mut directory = path_prefix.to_string();
let mut directory = path_prefix
.trim_end_matches(std::path::MAIN_SEPARATOR)
.to_string();
if !directory.ends_with('/') {
directory.push('/');
}
@@ -303,8 +305,19 @@ pub fn render_file_context_entry(
directory.push('/');
}
(file_name, Some(directory))
};
(file_name, Some(directory.into()))
}
}
pub fn render_file_context_entry(
id: ElementId,
path: &Path,
path_prefix: &Arc<str>,
is_directory: bool,
context_store: WeakEntity<ContextStore>,
cx: &App,
) -> Stateful<Div> {
let (file_name, directory) = extract_file_name_and_directory(path, path_prefix);
let added = context_store.upgrade().and_then(|context_store| {
if is_directory {

View File

@@ -0,0 +1,438 @@
use std::cmp::Reverse;
use std::sync::atomic::AtomicBool;
use std::sync::Arc;
use anyhow::{Context as _, Result};
use fuzzy::{StringMatch, StringMatchCandidate};
use gpui::{
App, AppContext, DismissEvent, Entity, FocusHandle, Focusable, Stateful, Task, WeakEntity,
};
use ordered_float::OrderedFloat;
use picker::{Picker, PickerDelegate};
use project::{DocumentSymbol, Symbol};
use text::OffsetRangeExt;
use ui::{prelude::*, ListItem};
use util::ResultExt as _;
use workspace::Workspace;
use crate::context_picker::{ConfirmBehavior, ContextPicker};
use crate::context_store::ContextStore;
pub struct SymbolContextPicker {
picker: Entity<Picker<SymbolContextPickerDelegate>>,
}
impl SymbolContextPicker {
pub fn new(
context_picker: WeakEntity<ContextPicker>,
workspace: WeakEntity<Workspace>,
context_store: WeakEntity<ContextStore>,
confirm_behavior: ConfirmBehavior,
window: &mut Window,
cx: &mut Context<Self>,
) -> Self {
let delegate = SymbolContextPickerDelegate::new(
context_picker,
workspace,
context_store,
confirm_behavior,
);
let picker = cx.new(|cx| Picker::uniform_list(delegate, window, cx));
Self { picker }
}
}
impl Focusable for SymbolContextPicker {
fn focus_handle(&self, cx: &App) -> FocusHandle {
self.picker.focus_handle(cx)
}
}
impl Render for SymbolContextPicker {
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
self.picker.clone()
}
}
pub struct SymbolContextPickerDelegate {
context_picker: WeakEntity<ContextPicker>,
workspace: WeakEntity<Workspace>,
context_store: WeakEntity<ContextStore>,
confirm_behavior: ConfirmBehavior,
matches: Vec<SymbolEntry>,
selected_index: usize,
}
impl SymbolContextPickerDelegate {
pub fn new(
context_picker: WeakEntity<ContextPicker>,
workspace: WeakEntity<Workspace>,
context_store: WeakEntity<ContextStore>,
confirm_behavior: ConfirmBehavior,
) -> Self {
Self {
context_picker,
workspace,
context_store,
confirm_behavior,
matches: Vec::new(),
selected_index: 0,
}
}
}
impl PickerDelegate for SymbolContextPickerDelegate {
type ListItem = ListItem;
fn match_count(&self) -> usize {
self.matches.len()
}
fn selected_index(&self) -> usize {
self.selected_index
}
fn set_selected_index(
&mut self,
ix: usize,
_window: &mut Window,
_cx: &mut Context<Picker<Self>>,
) {
self.selected_index = ix;
}
fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> {
"Search symbols…".into()
}
fn update_matches(
&mut self,
query: String,
window: &mut Window,
cx: &mut Context<Picker<Self>>,
) -> Task<()> {
let Some(workspace) = self.workspace.upgrade() else {
return Task::ready(());
};
let search_task = search_symbols(query, Arc::<AtomicBool>::default(), &workspace, cx);
let context_store = self.context_store.clone();
cx.spawn_in(window, async move |this, cx| {
let symbols = search_task
.await
.context("Failed to load symbols")
.log_err()
.unwrap_or_default();
let symbol_entries = context_store
.read_with(cx, |context_store, cx| {
compute_symbol_entries(symbols, context_store, cx)
})
.log_err()
.unwrap_or_default();
this.update(cx, |this, _cx| {
this.delegate.matches = symbol_entries;
})
.log_err();
})
}
fn confirm(&mut self, _secondary: bool, window: &mut Window, cx: &mut Context<Picker<Self>>) {
let Some(mat) = self.matches.get(self.selected_index) else {
return;
};
let Some(workspace) = self.workspace.upgrade() else {
return;
};
let confirm_behavior = self.confirm_behavior;
let add_symbol_task = add_symbol(
mat.symbol.clone(),
true,
workspace,
self.context_store.clone(),
cx,
);
let selected_index = self.selected_index;
cx.spawn_in(window, async move |this, cx| {
let included = add_symbol_task.await?;
this.update_in(cx, |this, window, cx| {
if let Some(mat) = this.delegate.matches.get_mut(selected_index) {
mat.is_included = included;
}
match confirm_behavior {
ConfirmBehavior::KeepOpen => {}
ConfirmBehavior::Close => this.delegate.dismissed(window, cx),
}
})
})
.detach_and_log_err(cx);
}
fn dismissed(&mut self, _: &mut Window, cx: &mut Context<Picker<Self>>) {
self.context_picker
.update(cx, |_, cx| {
cx.emit(DismissEvent);
})
.ok();
}
fn render_match(
&self,
ix: usize,
selected: bool,
_window: &mut Window,
_: &mut Context<Picker<Self>>,
) -> Option<Self::ListItem> {
let mat = &self.matches[ix];
Some(ListItem::new(ix).inset(true).toggle_state(selected).child(
render_symbol_context_entry(
ElementId::NamedInteger("symbol-ctx-picker".into(), ix),
mat,
),
))
}
}
pub(crate) struct SymbolEntry {
pub symbol: Symbol,
pub is_included: bool,
}
pub(crate) fn add_symbol(
symbol: Symbol,
remove_if_exists: bool,
workspace: Entity<Workspace>,
context_store: WeakEntity<ContextStore>,
cx: &mut App,
) -> Task<Result<bool>> {
let project = workspace.read(cx).project().clone();
let open_buffer_task = project.update(cx, |project, cx| {
project.open_buffer(symbol.path.clone(), cx)
});
cx.spawn(async move |cx| {
let buffer = open_buffer_task.await?;
let document_symbols = project
.update(cx, |project, cx| project.document_symbols(&buffer, cx))?
.await?;
// Try to find a matching document symbol. Document symbols include
// not only the symbol itself (e.g. function name), but they also
// include the context that they contain (e.g. function body).
let (name, range, enclosing_range) = if let Some(DocumentSymbol {
name,
range,
selection_range,
..
}) =
find_matching_symbol(&symbol, document_symbols.as_slice())
{
(name, selection_range, range)
} else {
// If we do not find a matching document symbol, fall back to
// just the symbol itself
(symbol.name, symbol.range.clone(), symbol.range)
};
let (range, enclosing_range) = buffer.read_with(cx, |buffer, _| {
(
buffer.anchor_after(range.start)..buffer.anchor_before(range.end),
buffer.anchor_after(enclosing_range.start)
..buffer.anchor_before(enclosing_range.end),
)
})?;
context_store
.update(cx, move |context_store, cx| {
context_store.add_symbol(
buffer,
name.into(),
range,
enclosing_range,
remove_if_exists,
cx,
)
})?
.await
})
}
fn find_matching_symbol(symbol: &Symbol, candidates: &[DocumentSymbol]) -> Option<DocumentSymbol> {
let mut candidates = candidates.iter();
let mut candidate = candidates.next()?;
loop {
if candidate.range.start > symbol.range.end {
return None;
}
if candidate.range.end < symbol.range.start {
candidate = candidates.next()?;
continue;
}
if candidate.selection_range == symbol.range {
return Some(candidate.clone());
}
if candidate.range.start <= symbol.range.start && symbol.range.end <= candidate.range.end {
candidates = candidate.children.iter();
candidate = candidates.next()?;
continue;
}
return None;
}
}
pub(crate) fn search_symbols(
query: String,
cancellation_flag: Arc<AtomicBool>,
workspace: &Entity<Workspace>,
cx: &mut App,
) -> Task<Result<Vec<(StringMatch, Symbol)>>> {
let symbols_task = workspace.update(cx, |workspace, cx| {
workspace
.project()
.update(cx, |project, cx| project.symbols(&query, cx))
});
let project = workspace.read(cx).project().clone();
cx.spawn(async move |cx| {
let symbols = symbols_task.await?;
let (visible_match_candidates, external_match_candidates): (Vec<_>, Vec<_>) = project
.update(cx, |project, cx| {
symbols
.iter()
.enumerate()
.map(|(id, symbol)| StringMatchCandidate::new(id, &symbol.label.filter_text()))
.partition(|candidate| {
project
.entry_for_path(&symbols[candidate.id].path, cx)
.map_or(false, |e| !e.is_ignored)
})
})?;
const MAX_MATCHES: usize = 100;
let mut visible_matches = cx.background_executor().block(fuzzy::match_strings(
&visible_match_candidates,
&query,
false,
MAX_MATCHES,
&cancellation_flag,
cx.background_executor().clone(),
));
let mut external_matches = cx.background_executor().block(fuzzy::match_strings(
&external_match_candidates,
&query,
false,
MAX_MATCHES - visible_matches.len().min(MAX_MATCHES),
&cancellation_flag,
cx.background_executor().clone(),
));
let sort_key_for_match = |mat: &StringMatch| {
let symbol = &symbols[mat.candidate_id];
(Reverse(OrderedFloat(mat.score)), symbol.label.filter_text())
};
visible_matches.sort_unstable_by_key(sort_key_for_match);
external_matches.sort_unstable_by_key(sort_key_for_match);
let mut matches = visible_matches;
matches.append(&mut external_matches);
Ok(matches
.into_iter()
.map(|mut mat| {
let symbol = symbols[mat.candidate_id].clone();
let filter_start = symbol.label.filter_range.start;
for position in &mut mat.positions {
*position += filter_start;
}
(mat, symbol)
})
.collect())
})
}
fn compute_symbol_entries(
symbols: Vec<(StringMatch, Symbol)>,
context_store: &ContextStore,
cx: &App,
) -> Vec<SymbolEntry> {
let mut symbol_entries = Vec::with_capacity(symbols.len());
for (_, symbol) in symbols {
let symbols_for_path = context_store.included_symbols_by_path().get(&symbol.path);
let is_included = if let Some(symbols_for_path) = symbols_for_path {
let mut is_included = false;
for included_symbol_id in symbols_for_path {
if included_symbol_id.name.as_ref() == symbol.name.as_str() {
if let Some(buffer) = context_store.buffer_for_symbol(included_symbol_id) {
let snapshot = buffer.read(cx).snapshot();
let included_symbol_range =
included_symbol_id.range.to_point_utf16(&snapshot);
if included_symbol_range.start == symbol.range.start.0
&& included_symbol_range.end == symbol.range.end.0
{
is_included = true;
break;
}
}
}
}
is_included
} else {
false
};
symbol_entries.push(SymbolEntry {
symbol,
is_included,
})
}
symbol_entries
}
pub fn render_symbol_context_entry(id: ElementId, entry: &SymbolEntry) -> Stateful<Div> {
let path = entry
.symbol
.path
.path
.file_name()
.map(|s| s.to_string_lossy())
.unwrap_or_default();
let symbol_location = format!("{} L{}", path, entry.symbol.range.start.0.row + 1);
h_flex()
.id(id)
.gap_1p5()
.w_full()
.child(
Icon::new(IconName::Code)
.size(IconSize::Small)
.color(Color::Muted),
)
.child(
h_flex()
.gap_1()
.child(Label::new(&entry.symbol.name))
.child(
Label::new(symbol_location)
.size(LabelSize::Small)
.color(Color::Muted),
),
)
.when(entry.is_included, |el| {
el.child(
h_flex()
.w_full()
.justify_end()
.gap_0p5()
.child(
Icon::new(IconName::Check)
.size(IconSize::Small)
.color(Color::Success),
)
.child(Label::new("Added").size(LabelSize::Small)),
)
})
}

View File

@@ -197,7 +197,7 @@ pub fn render_thread_context_entry(
.gap_1p5()
.max_w_72()
.child(
Icon::new(IconName::MessageCircle)
Icon::new(IconName::MessageBubbles)
.size(IconSize::XSmall)
.color(Color::Muted),
)

View File

@@ -1,3 +1,4 @@
use std::ops::Range;
use std::path::{Path, PathBuf};
use std::sync::Arc;
@@ -6,15 +7,15 @@ use collections::{BTreeMap, HashMap, HashSet};
use futures::{self, future, Future, FutureExt};
use gpui::{App, AppContext as _, AsyncApp, Context, Entity, SharedString, Task, WeakEntity};
use language::Buffer;
use project::{ProjectPath, Worktree};
use project::{ProjectItem, ProjectPath, Worktree};
use rope::Rope;
use text::BufferId;
use text::{Anchor, BufferId, OffsetRangeExt};
use util::maybe;
use workspace::Workspace;
use crate::context::{
AssistantContext, ContextBuffer, ContextId, ContextSnapshot, DirectoryContext,
FetchedUrlContext, FileContext, ThreadContext,
AssistantContext, ContextBuffer, ContextId, ContextSnapshot, ContextSymbol, ContextSymbolId,
DirectoryContext, FetchedUrlContext, FileContext, SymbolContext, ThreadContext,
};
use crate::context_strip::SuggestedContext;
use crate::thread::{Thread, ThreadId};
@@ -26,6 +27,9 @@ pub struct ContextStore {
next_context_id: ContextId,
files: BTreeMap<BufferId, ContextId>,
directories: HashMap<PathBuf, ContextId>,
symbols: HashMap<ContextSymbolId, ContextId>,
symbol_buffers: HashMap<ContextSymbolId, Entity<Buffer>>,
symbols_by_path: HashMap<ProjectPath, Vec<ContextSymbolId>>,
threads: HashMap<ThreadId, ContextId>,
fetched_urls: HashMap<String, ContextId>,
}
@@ -38,6 +42,9 @@ impl ContextStore {
next_context_id: ContextId(0),
files: BTreeMap::default(),
directories: HashMap::default(),
symbols: HashMap::default(),
symbol_buffers: HashMap::default(),
symbols_by_path: HashMap::default(),
threads: HashMap::default(),
fetched_urls: HashMap::default(),
}
@@ -53,6 +60,10 @@ impl ContextStore {
&self.context
}
pub fn context_for_id(&self, id: ContextId) -> Option<&AssistantContext> {
self.context().iter().find(|context| context.id() == id)
}
pub fn clear(&mut self) {
self.context.clear();
self.files.clear();
@@ -107,6 +118,7 @@ impl ContextStore {
project_path.path.clone(),
buffer_entity,
buffer,
None,
cx.to_async(),
)
})?;
@@ -136,6 +148,7 @@ impl ContextStore {
file.path().clone(),
buffer_entity,
buffer,
None,
cx.to_async(),
))
})??;
@@ -222,6 +235,7 @@ impl ContextStore {
path,
buffer_entity,
buffer,
None,
cx.to_async(),
);
buffer_infos.push(buffer_info);
@@ -243,37 +257,118 @@ impl ContextStore {
}
this.update(cx, |this, _| {
this.insert_directory(&project_path.path, context_buffers);
this.insert_directory(project_path, context_buffers);
})?;
anyhow::Ok(())
})
}
fn insert_directory(&mut self, path: &Path, context_buffers: Vec<ContextBuffer>) {
fn insert_directory(&mut self, project_path: ProjectPath, context_buffers: Vec<ContextBuffer>) {
let id = self.next_context_id.post_inc();
self.directories.insert(path.to_path_buf(), id);
self.directories.insert(project_path.path.to_path_buf(), id);
self.context
.push(AssistantContext::Directory(DirectoryContext::new(
id,
path,
project_path,
context_buffers,
)));
}
pub fn add_symbol(
&mut self,
buffer: Entity<Buffer>,
symbol_name: SharedString,
symbol_range: Range<Anchor>,
symbol_enclosing_range: Range<Anchor>,
remove_if_exists: bool,
cx: &mut Context<Self>,
) -> Task<Result<bool>> {
let buffer_ref = buffer.read(cx);
let Some(file) = buffer_ref.file() else {
return Task::ready(Err(anyhow!("Buffer has no path.")));
};
let Some(project_path) = buffer_ref.project_path(cx) else {
return Task::ready(Err(anyhow!("Buffer has no project path.")));
};
if let Some(symbols_for_path) = self.symbols_by_path.get(&project_path) {
let mut matching_symbol_id = None;
for symbol in symbols_for_path {
if &symbol.name == &symbol_name {
let snapshot = buffer_ref.snapshot();
if symbol.range.to_offset(&snapshot) == symbol_range.to_offset(&snapshot) {
matching_symbol_id = self.symbols.get(symbol).cloned();
break;
}
}
}
if let Some(id) = matching_symbol_id {
if remove_if_exists {
self.remove_context(id);
}
return Task::ready(Ok(false));
}
}
let (buffer_info, collect_content_task) = collect_buffer_info_and_text(
file.path().clone(),
buffer,
buffer_ref,
Some(symbol_enclosing_range.clone()),
cx.to_async(),
);
cx.spawn(async move |this, cx| {
let content = collect_content_task.await;
this.update(cx, |this, _cx| {
this.insert_symbol(make_context_symbol(
buffer_info,
project_path,
symbol_name,
symbol_range,
symbol_enclosing_range,
content,
))
})?;
anyhow::Ok(true)
})
}
fn insert_symbol(&mut self, context_symbol: ContextSymbol) {
let id = self.next_context_id.post_inc();
self.symbols.insert(context_symbol.id.clone(), id);
self.symbols_by_path
.entry(context_symbol.id.path.clone())
.or_insert_with(Vec::new)
.push(context_symbol.id.clone());
self.symbol_buffers
.insert(context_symbol.id.clone(), context_symbol.buffer.clone());
self.context.push(AssistantContext::Symbol(SymbolContext {
id,
context_symbol,
}));
}
pub fn add_thread(
&mut self,
thread: Entity<Thread>,
remove_if_exists: bool,
cx: &mut Context<Self>,
) {
if let Some(context_id) = self.includes_thread(&thread.read(cx).id()) {
if remove_if_exists {
self.remove_context(context_id);
match self.includes_thread(&thread.read(cx).id()) {
Some(context_id) => {
if remove_if_exists {
self.remove_context(context_id);
}
}
_ => {
self.insert_thread(thread, cx);
}
} else {
self.insert_thread(thread, cx);
}
}
@@ -340,6 +435,19 @@ impl ContextStore {
AssistantContext::Directory(_) => {
self.directories.retain(|_, context_id| *context_id != id);
}
AssistantContext::Symbol(symbol) => {
if let Some(symbols_in_path) =
self.symbols_by_path.get_mut(&symbol.context_symbol.id.path)
{
symbols_in_path.retain(|s| {
self.symbols
.get(s)
.map_or(false, |context_id| *context_id != id)
});
}
self.symbol_buffers.remove(&symbol.context_symbol.id);
self.symbols.retain(|_, context_id| *context_id != id);
}
AssistantContext::FetchedUrl(_) => {
self.fetched_urls.retain(|_, context_id| *context_id != id);
}
@@ -403,6 +511,18 @@ impl ContextStore {
self.directories.get(path).copied()
}
pub fn included_symbol(&self, symbol_id: &ContextSymbolId) -> Option<ContextId> {
self.symbols.get(symbol_id).copied()
}
pub fn included_symbols_by_path(&self) -> &HashMap<ProjectPath, Vec<ContextSymbolId>> {
&self.symbols_by_path
}
pub fn buffer_for_symbol(&self, symbol_id: &ContextSymbolId) -> Option<Entity<Buffer>> {
self.symbol_buffers.get(symbol_id).cloned()
}
pub fn includes_thread(&self, thread_id: &ThreadId) -> Option<ContextId> {
self.threads.get(thread_id).copied()
}
@@ -431,6 +551,7 @@ impl ContextStore {
buffer_path_log_err(buffer).map(|p| p.to_path_buf())
}
AssistantContext::Directory(_)
| AssistantContext::Symbol(_)
| AssistantContext::FetchedUrl(_)
| AssistantContext::Thread(_) => None,
})
@@ -463,10 +584,28 @@ fn make_context_buffer(info: BufferInfo, text: SharedString) -> ContextBuffer {
}
}
fn make_context_symbol(
info: BufferInfo,
path: ProjectPath,
name: SharedString,
range: Range<Anchor>,
enclosing_range: Range<Anchor>,
text: SharedString,
) -> ContextSymbol {
ContextSymbol {
id: ContextSymbolId { name, range, path },
buffer_version: info.version,
enclosing_range,
buffer: info.buffer_entity,
text,
}
}
fn collect_buffer_info_and_text(
path: Arc<Path>,
buffer_entity: Entity<Buffer>,
buffer: &Buffer,
range: Option<Range<Anchor>>,
cx: AsyncApp,
) -> (BufferInfo, Task<SharedString>) {
let buffer_info = BufferInfo {
@@ -475,7 +614,11 @@ fn collect_buffer_info_and_text(
version: buffer.version(),
};
// Important to collect version at the same time as content so that staleness logic is correct.
let content = buffer.as_rope().clone();
let content = if let Some(range) = range {
buffer.text_for_range(range).collect::<Rope>()
} else {
buffer.as_rope().clone()
};
let text_task = cx.background_spawn(async move { to_fenced_codeblock(&path, content) });
(buffer_info, text_task)
}
@@ -547,7 +690,7 @@ pub fn refresh_context_store_text(
context_store: Entity<ContextStore>,
changed_buffers: &HashSet<Entity<Buffer>>,
cx: &App,
) -> impl Future<Output = Vec<ContextId>> {
) -> impl Future<Output = Vec<ContextId>> + use<> {
let mut tasks = Vec::new();
for context in &context_store.read(cx).context {
@@ -568,8 +711,9 @@ pub fn refresh_context_store_text(
|| changed_buffers.iter().any(|buffer| {
let buffer = buffer.read(cx);
buffer_path_log_err(&buffer)
.map_or(false, |path| path.starts_with(&directory_context.path))
buffer_path_log_err(&buffer).map_or(false, |path| {
path.starts_with(&directory_context.path.path)
})
});
if should_refresh {
@@ -577,6 +721,14 @@ pub fn refresh_context_store_text(
return refresh_directory_text(context_store, directory_context, cx);
}
}
AssistantContext::Symbol(symbol_context) => {
if changed_buffers.is_empty()
|| changed_buffers.contains(&symbol_context.context_symbol.buffer)
{
let context_store = context_store.clone();
return refresh_symbol_text(context_store, symbol_context, cx);
}
}
AssistantContext::Thread(thread_context) => {
if changed_buffers.is_empty() {
let context_store = context_store.clone();
@@ -607,8 +759,8 @@ fn refresh_file_text(
) -> Option<Task<()>> {
let id = file_context.id;
let task = refresh_context_buffer(&file_context.context_buffer, cx);
if let Some(task) = task {
Some(cx.spawn(async move |cx| {
match task {
Some(task) => Some(cx.spawn(async move |cx| {
let context_buffer = task.await;
context_store
.update(cx, |context_store, _| {
@@ -616,9 +768,8 @@ fn refresh_file_text(
context_store.replace_context(AssistantContext::File(new_file_context));
})
.ok();
}))
} else {
None
})),
_ => None,
}
}
@@ -631,14 +782,15 @@ fn refresh_directory_text(
let futures = directory_context
.context_buffers
.iter()
.map(|context_buffer| {
if let Some(refresh_task) = refresh_context_buffer(context_buffer, cx) {
stale = true;
future::Either::Left(refresh_task)
} else {
future::Either::Right(future::ready((*context_buffer).clone()))
}
})
.map(
|context_buffer| match refresh_context_buffer(context_buffer, cx) {
Some(refresh_task) => {
stale = true;
future::Either::Left(refresh_task)
}
_ => future::Either::Right(future::ready((*context_buffer).clone())),
},
)
.collect::<Vec<_>>();
if !stale {
@@ -653,13 +805,34 @@ fn refresh_directory_text(
let context_buffers = context_buffers.await;
context_store
.update(cx, |context_store, _| {
let new_directory_context = DirectoryContext::new(id, &path, context_buffers);
let new_directory_context = DirectoryContext::new(id, path, context_buffers);
context_store.replace_context(AssistantContext::Directory(new_directory_context));
})
.ok();
}))
}
fn refresh_symbol_text(
context_store: Entity<ContextStore>,
symbol_context: &SymbolContext,
cx: &App,
) -> Option<Task<()>> {
let id = symbol_context.id;
let task = refresh_context_symbol(&symbol_context.context_symbol, cx);
match task {
Some(task) => Some(cx.spawn(async move |cx| {
let context_symbol = task.await;
context_store
.update(cx, |context_store, _| {
let new_symbol_context = SymbolContext { id, context_symbol };
context_store.replace_context(AssistantContext::Symbol(new_symbol_context));
})
.ok();
})),
_ => None,
}
}
fn refresh_thread_text(
context_store: Entity<ContextStore>,
thread_context: &ThreadContext,
@@ -684,7 +857,7 @@ fn refresh_thread_text(
fn refresh_context_buffer(
context_buffer: &ContextBuffer,
cx: &App,
) -> Option<impl Future<Output = ContextBuffer>> {
) -> Option<impl Future<Output = ContextBuffer> + use<>> {
let buffer = context_buffer.buffer.read(cx);
let path = buffer_path_log_err(buffer)?;
if buffer.version.changed_since(&context_buffer.version) {
@@ -692,6 +865,7 @@ fn refresh_context_buffer(
path,
context_buffer.buffer.clone(),
buffer,
None,
cx.to_async(),
);
Some(text_task.map(move |text| make_context_buffer(buffer_info, text)))
@@ -699,3 +873,36 @@ fn refresh_context_buffer(
None
}
}
fn refresh_context_symbol(
context_symbol: &ContextSymbol,
cx: &App,
) -> Option<impl Future<Output = ContextSymbol> + use<>> {
let buffer = context_symbol.buffer.read(cx);
let path = buffer_path_log_err(buffer)?;
let project_path = buffer.project_path(cx)?;
if buffer.version.changed_since(&context_symbol.buffer_version) {
let (buffer_info, text_task) = collect_buffer_info_and_text(
path,
context_symbol.buffer.clone(),
buffer,
Some(context_symbol.enclosing_range.clone()),
cx.to_async(),
);
let name = context_symbol.id.name.clone();
let range = context_symbol.id.range.clone();
let enclosing_range = context_symbol.enclosing_range.clone();
Some(text_task.map(move |text| {
make_context_symbol(
buffer_info,
project_path,
name,
range,
enclosing_range,
text,
)
}))
} else {
None
}
}

View File

@@ -4,15 +4,15 @@ use collections::HashSet;
use editor::Editor;
use file_icons::FileIcons;
use gpui::{
App, Bounds, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, Subscription,
WeakEntity,
App, Bounds, ClickEvent, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable,
Subscription, WeakEntity,
};
use itertools::Itertools;
use language::Buffer;
use ui::{prelude::*, KeyBinding, PopoverMenu, PopoverMenuHandle, Tooltip};
use workspace::{notifications::NotifyResultExt, Workspace};
use crate::context::ContextKind;
use crate::context::{ContextId, ContextKind};
use crate::context_picker::{ConfirmBehavior, ContextPicker};
use crate::context_store::ContextStore;
use crate::thread::Thread;
@@ -277,6 +277,14 @@ impl ContextStrip {
best.map(|(index, _, _)| index)
}
fn open_context(&mut self, id: ContextId, window: &mut Window, cx: &mut App) {
let Some(workspace) = self.workspace.upgrade() else {
return;
};
crate::active_thread::open_context(id, self.context_store.clone(), workspace, window, cx);
}
fn remove_focused_context(
&mut self,
_: &RemoveFocusedContext,
@@ -458,6 +466,7 @@ impl Render for ContextStrip {
}
})
.children(context.iter().enumerate().map(|(i, context)| {
let id = context.id;
ContextPill::added(
context.clone(),
dupe_names.contains(&context.name),
@@ -473,10 +482,16 @@ impl Render for ContextStrip {
}))
}),
)
.on_click(Rc::new(cx.listener(move |this, _, _window, cx| {
this.focused_index = Some(i);
cx.notify();
})))
.on_click(Rc::new(cx.listener(
move |this, event: &ClickEvent, window, cx| {
if event.down.click_count > 1 {
this.open_context(id, window, cx);
} else {
this.focused_index = Some(i);
}
cx.notify();
},
)))
}))
.when_some(suggested_context, |el, suggested| {
el.child(

View File

@@ -341,33 +341,35 @@ impl InlineAssistant {
selection.end.column = snapshot
.buffer_snapshot
.line_len(MultiBufferRow(selection.end.row));
} else if let Some(fold) =
snapshot.crease_for_buffer_row(MultiBufferRow(selection.end.row))
{
selection.start = fold.range().start;
selection.end = fold.range().end;
if MultiBufferRow(selection.end.row) < snapshot.buffer_snapshot.max_row() {
let chars = snapshot
.buffer_snapshot
.chars_at(Point::new(selection.end.row + 1, 0));
for c in chars {
if c == '\n' {
break;
}
if c.is_whitespace() {
continue;
}
if snapshot
.language_at(selection.end)
.is_some_and(|language| language.config().brackets.is_closing_brace(c))
{
selection.end.row += 1;
selection.end.column = snapshot
} else {
match snapshot.crease_for_buffer_row(MultiBufferRow(selection.end.row)) {
Some(fold) => {
selection.start = fold.range().start;
selection.end = fold.range().end;
if MultiBufferRow(selection.end.row) < snapshot.buffer_snapshot.max_row() {
let chars = snapshot
.buffer_snapshot
.line_len(MultiBufferRow(selection.end.row));
.chars_at(Point::new(selection.end.row + 1, 0));
for c in chars {
if c == '\n' {
break;
}
if c.is_whitespace() {
continue;
}
if snapshot.language_at(selection.end).is_some_and(|language| {
language.config().brackets.is_closing_brace(c)
}) {
selection.end.row += 1;
selection.end.column = snapshot
.buffer_snapshot
.line_len(MultiBufferRow(selection.end.row));
}
}
}
}
_ => {}
}
}
@@ -1129,24 +1131,27 @@ impl InlineAssistant {
let mut scroll_target_top;
let mut scroll_target_bottom;
if let Some(decorations) = assist.decorations.as_ref() {
scroll_target_top = editor
.row_for_block(decorations.prompt_block_id, cx)
.unwrap()
.0 as f32;
scroll_target_bottom = editor
.row_for_block(decorations.end_block_id, cx)
.unwrap()
.0 as f32;
} else {
let snapshot = editor.snapshot(window, cx);
let start_row = assist
.range
.start
.to_display_point(&snapshot.display_snapshot)
.row();
scroll_target_top = start_row.0 as f32;
scroll_target_bottom = scroll_target_top + 1.;
match assist.decorations.as_ref() {
Some(decorations) => {
scroll_target_top = editor
.row_for_block(decorations.prompt_block_id, cx)
.unwrap()
.0 as f32;
scroll_target_bottom = editor
.row_for_block(decorations.end_block_id, cx)
.unwrap()
.0 as f32;
}
_ => {
let snapshot = editor.snapshot(window, cx);
let start_row = assist
.range
.start
.to_display_point(&snapshot.display_snapshot)
.row();
scroll_target_top = start_row.0 as f32;
scroll_target_bottom = scroll_target_top + 1.;
}
}
scroll_target_top -= editor.vertical_scroll_margin() as f32;
scroll_target_bottom += editor.vertical_scroll_margin() as f32;
@@ -1191,10 +1196,11 @@ impl InlineAssistant {
}
pub fn start_assist(&mut self, assist_id: InlineAssistId, window: &mut Window, cx: &mut App) {
let assist = if let Some(assist) = self.assists.get_mut(&assist_id) {
assist
} else {
return;
let assist = match self.assists.get_mut(&assist_id) {
Some(assist) => assist,
_ => {
return;
}
};
let assist_group_id = assist.group_id;
@@ -1222,10 +1228,11 @@ impl InlineAssistant {
}
pub fn stop_assist(&mut self, assist_id: InlineAssistId, cx: &mut App) {
let assist = if let Some(assist) = self.assists.get_mut(&assist_id) {
assist
} else {
return;
let assist = match self.assists.get_mut(&assist_id) {
Some(assist) => assist,
_ => {
return;
}
};
assist.codegen.update(cx, |codegen, cx| codegen.stop(cx));
@@ -1449,20 +1456,27 @@ impl InlineAssistant {
}
});
if let Some(context_editor) = context_editor {
Some(InlineAssistTarget::Editor(context_editor))
} else if let Some(workspace_editor) = workspace
.active_item(cx)
.and_then(|item| item.act_as::<Editor>(cx))
{
Some(InlineAssistTarget::Editor(workspace_editor))
} else if let Some(terminal_view) = workspace
.active_item(cx)
.and_then(|item| item.act_as::<TerminalView>(cx))
{
Some(InlineAssistTarget::Terminal(terminal_view))
} else {
None
match context_editor {
Some(context_editor) => Some(InlineAssistTarget::Editor(context_editor)),
_ => {
match workspace
.active_item(cx)
.and_then(|item| item.act_as::<Editor>(cx))
{
Some(workspace_editor) => Some(InlineAssistTarget::Editor(workspace_editor)),
_ => {
match workspace
.active_item(cx)
.and_then(|item| item.act_as::<TerminalView>(cx))
{
Some(terminal_view) => {
Some(InlineAssistTarget::Terminal(terminal_view))
}
_ => None,
}
}
}
}
}
}
}
@@ -1654,10 +1668,11 @@ impl InlineAssist {
InlineAssistant::update_global(cx, |this, cx| match event {
CodegenEvent::Undone => this.finish_assist(assist_id, false, window, cx),
CodegenEvent::Finished => {
let assist = if let Some(assist) = this.assists.get(&assist_id) {
assist
} else {
return;
let assist = match this.assists.get(&assist_id) {
Some(assist) => assist,
_ => {
return;
}
};
if let CodegenStatus::Error(error) = codegen.read(cx).status(cx) {

View File

@@ -677,7 +677,7 @@ impl<T: 'static> PromptEditor<T> {
.into_any_element()
}
fn render_rate_limit_notice(&self, cx: &mut Context<Self>) -> impl IntoElement {
fn render_rate_limit_notice(&self, cx: &mut Context<Self>) -> impl IntoElement + use<T> {
Popover::new().child(
v_flex()
.occlude()

View File

@@ -3,12 +3,11 @@ use std::sync::Arc;
use collections::HashSet;
use editor::actions::MoveUp;
use editor::{ContextMenuOptions, ContextMenuPlacement, Editor, EditorElement, EditorStyle};
use file_icons::FileIcons;
use fs::Fs;
use git::ExpandCommitEditor;
use git_ui::git_panel;
use gpui::{
point, Animation, AnimationExt, App, DismissEvent, Entity, Focusable, Subscription, TextStyle,
WeakEntity,
linear_color_stop, linear_gradient, point, Animation, AnimationExt, App, DismissEvent, Entity,
Focusable, Subscription, TextStyle, WeakEntity,
};
use language_model::LanguageModelRegistry;
use language_model_selector::ToggleModelSelector;
@@ -17,8 +16,10 @@ use settings::Settings;
use std::time::Duration;
use theme::ThemeSettings;
use ui::{
prelude::*, ButtonLike, KeyBinding, PlatformStyle, PopoverMenu, PopoverMenuHandle, Tooltip,
prelude::*, ButtonLike, Disclosure, KeyBinding, PlatformStyle, PopoverMenu, PopoverMenuHandle,
Tooltip,
};
use util::ResultExt;
use vim_mode_setting::VimModeSetting;
use workspace::Workspace;
@@ -29,7 +30,10 @@ use crate::context_strip::{ContextStrip, ContextStripEvent, SuggestContextKind};
use crate::profile_selector::ProfileSelector;
use crate::thread::{RequestKind, Thread};
use crate::thread_store::ThreadStore;
use crate::{Chat, ChatMode, RemoveAllContext, ThreadEvent, ToggleContextPicker};
use crate::{
AssistantDiff, Chat, ChatMode, OpenAssistantDiff, RemoveAllContext, ThreadEvent,
ToggleContextPicker, ToggleProfileSelector,
};
pub struct MessageEditor {
thread: Entity<Thread>,
@@ -44,6 +48,7 @@ pub struct MessageEditor {
inline_context_picker_menu_handle: PopoverMenuHandle<ContextPicker>,
model_selector: Entity<AssistantModelSelector>,
profile_selector: Entity<ProfileSelector>,
edits_expanded: bool,
_subscriptions: Vec<Subscription>,
}
@@ -135,7 +140,9 @@ impl MessageEditor {
cx,
)
}),
profile_selector: cx.new(|cx| ProfileSelector::new(fs, thread_store, cx)),
edits_expanded: false,
profile_selector: cx
.new(|cx| ProfileSelector::new(fs, thread_store, editor.focus_handle(cx), cx)),
_subscriptions: subscriptions,
}
}
@@ -233,6 +240,9 @@ impl MessageEditor {
thread
.update(cx, |thread, cx| {
let context = context_store.read(cx).snapshot(cx).collect::<Vec<_>>();
thread.action_log().update(cx, |action_log, cx| {
action_log.clear_reviewed_changes(cx);
});
thread.insert_user_message(user_message, context, checkpoint, cx);
thread.send_to_model(model, request_kind, cx);
})
@@ -279,6 +289,10 @@ impl MessageEditor {
self.context_strip.focus_handle(cx).focus(window);
}
}
fn handle_review_click(&self, window: &mut Window, cx: &mut Context<Self>) {
AssistantDiff::deploy(self.thread.clone(), self.workspace.clone(), window, cx).log_err();
}
}
impl Focusable for MessageEditor {
@@ -295,7 +309,6 @@ impl Render for MessageEditor {
let focus_handle = self.editor.focus_handle(cx);
let inline_context_picker = self.inline_context_picker.clone();
let empty_thread = self.thread.read(cx).is_empty();
let is_generating = self.thread.read(cx).is_generating();
let is_model_selected = self.is_model_selected(cx);
let is_editor_empty = self.is_editor_empty(cx);
@@ -315,31 +328,15 @@ impl Render for MessageEditor {
px(64.)
};
let project = self.thread.read(cx).project();
let changed_files = if let Some(repository) = project.read(cx).active_repository(cx) {
repository.read(cx).cached_status().count()
} else {
0
};
let action_log = self.thread.read(cx).action_log();
let changed_buffers = action_log.read(cx).changed_buffers(cx);
let changed_buffers_count = changed_buffers.len();
let editor_bg_color = cx.theme().colors().editor_background;
let border_color = cx.theme().colors().border;
let active_color = cx.theme().colors().element_selected;
let editor_bg_color = cx.theme().colors().editor_background;
let bg_edit_files_disclosure = editor_bg_color.blend(active_color.opacity(0.3));
let edit_files_container = || {
h_flex()
.mx_2()
.py_1()
.pl_2p5()
.pr_1()
.bg(bg_edit_files_disclosure)
.border_1()
.border_color(border_color)
.justify_between()
.flex_wrap()
};
v_flex()
.size_full()
.when(is_generating, |parent| {
@@ -400,167 +397,213 @@ impl Render for MessageEditor {
),
)
})
.when(
changed_files > 0 && !is_generating && !empty_thread,
|parent| {
parent.child(
edit_files_container()
.border_b_0()
.rounded_t_md()
.shadow(smallvec::smallvec![gpui::BoxShadow {
color: gpui::black().opacity(0.15),
offset: point(px(1.), px(-1.)),
blur_radius: px(3.),
spread_radius: px(0.),
}])
.child(
h_flex()
.gap_2()
.child(Label::new("Edits").size(LabelSize::XSmall))
.child(div().size_1().rounded_full().bg(border_color))
.child(
Label::new(format!(
"{} {}",
changed_files,
if changed_files == 1 { "file" } else { "files" }
))
.size(LabelSize::XSmall),
.when(changed_buffers_count > 0, |parent| {
parent.child(
v_flex()
.mx_2()
.bg(bg_edit_files_disclosure)
.border_1()
.border_b_0()
.border_color(border_color)
.rounded_t_md()
.shadow(smallvec::smallvec![gpui::BoxShadow {
color: gpui::black().opacity(0.15),
offset: point(px(1.), px(-1.)),
blur_radius: px(3.),
spread_radius: px(0.),
}])
.child(
h_flex()
.p_1p5()
.justify_between()
.when(self.edits_expanded, |this| {
this.border_b_1().border_color(border_color)
})
.child(
h_flex()
.gap_1()
.child(
Disclosure::new(
"edits-disclosure",
self.edits_expanded,
)
.on_click(
cx.listener(|this, _ev, _window, cx| {
this.edits_expanded = !this.edits_expanded;
cx.notify();
}),
),
)
.child(
Label::new("Edits")
.size(LabelSize::Small)
.color(Color::Muted),
)
.child(
Label::new("")
.size(LabelSize::XSmall)
.color(Color::Muted),
)
.child(
Label::new(format!(
"{} {}",
changed_buffers_count,
if changed_buffers_count == 1 {
"file"
} else {
"files"
}
))
.size(LabelSize::Small)
.color(Color::Muted),
),
)
.child(
Button::new("review", "Review Changes")
.label_size(LabelSize::Small)
.key_binding(
KeyBinding::for_action_in(
&OpenAssistantDiff,
&focus_handle,
window,
cx,
)
.map(|kb| kb.size(rems_from_px(12.))),
)
.on_click(cx.listener(|this, _, window, cx| {
this.handle_review_click(window, cx)
})),
),
)
.when(self.edits_expanded, |parent| {
parent.child(
v_flex().bg(cx.theme().colors().editor_background).children(
changed_buffers.into_iter().enumerate().flat_map(
|(index, (buffer, changed))| {
let file = buffer.read(cx).file()?;
let path = file.path();
let parent_label = path.parent().and_then(|parent| {
let parent_str = parent.to_string_lossy();
if parent_str.is_empty() {
None
} else {
Some(
Label::new(format!(
"{}{}",
parent_str,
std::path::MAIN_SEPARATOR_STR
))
.color(Color::Muted)
.size(LabelSize::XSmall)
.buffer_font(cx),
)
}
});
let name_label = path.file_name().map(|name| {
Label::new(name.to_string_lossy().to_string())
.size(LabelSize::XSmall)
.buffer_font(cx)
});
let file_icon = FileIcons::get_icon(&path, cx)
.map(Icon::from_path)
.map(|icon| {
icon.color(Color::Muted).size(IconSize::Small)
})
.unwrap_or_else(|| {
Icon::new(IconName::File)
.color(Color::Muted)
.size(IconSize::Small)
});
let element = div()
.relative()
.py_1()
.px_2()
.when(index + 1 < changed_buffers_count, |parent| {
parent.border_color(border_color).border_b_1()
})
.child(
h_flex()
.gap_2()
.justify_between()
.child(
h_flex()
.id("file-container")
.pr_8()
.gap_1p5()
.max_w_full()
.overflow_x_scroll()
.child(file_icon)
.child(
h_flex()
.children(parent_label)
.children(name_label),
) // TODO: show lines changed
.child(
Label::new("+")
.color(Color::Created),
)
.child(
Label::new("-")
.color(Color::Deleted),
),
)
.when(!changed.needs_review, |parent| {
parent.child(
Icon::new(IconName::Check)
.color(Color::Success),
)
})
.child(
div()
.h_full()
.absolute()
.w_8()
.bottom_0()
.map(|this| {
if !changed.needs_review {
this.right_4()
} else {
this.right_0()
}
})
.bg(linear_gradient(
90.,
linear_color_stop(
editor_bg_color,
1.,
),
linear_color_stop(
editor_bg_color
.opacity(0.2),
0.,
),
)),
),
);
Some(element)
},
),
),
)
.child(
h_flex()
.gap_1()
.child(
Button::new("panel", "Open Git Panel")
.label_size(LabelSize::XSmall)
.key_binding({
let focus_handle = focus_handle.clone();
KeyBinding::for_action_in(
&git_panel::ToggleFocus,
&focus_handle,
window,
cx,
)
.map(|kb| kb.size(rems_from_px(10.)))
})
.on_click(|_ev, _window, cx| {
cx.defer(|cx| {
cx.dispatch_action(&git_panel::ToggleFocus)
});
}),
)
.child(
Button::new("review", "Review Diff")
.label_size(LabelSize::XSmall)
.key_binding({
let focus_handle = focus_handle.clone();
KeyBinding::for_action_in(
&git_ui::project_diff::Diff,
&focus_handle,
window,
cx,
)
.map(|kb| kb.size(rems_from_px(10.)))
})
.on_click(|_event, _window, cx| {
cx.defer(|cx| {
cx.dispatch_action(&git_ui::project_diff::Diff)
});
}),
)
.child(
Button::new("commit", "Commit Changes")
.label_size(LabelSize::XSmall)
.key_binding({
let focus_handle = focus_handle.clone();
KeyBinding::for_action_in(
&ExpandCommitEditor,
&focus_handle,
window,
cx,
)
.map(|kb| kb.size(rems_from_px(10.)))
})
.on_click(|_event, _window, cx| {
cx.defer(|cx| {
cx.dispatch_action(&ExpandCommitEditor)
});
}),
),
),
)
},
)
.when(
changed_files > 0 && !is_generating && empty_thread,
|parent| {
parent.child(
edit_files_container()
.mb_2()
.rounded_md()
.child(
h_flex()
.gap_2()
.child(Label::new("Consider committing your changes before starting a fresh thread").size(LabelSize::XSmall))
.child(div().size_1().rounded_full().bg(border_color))
.child(
Label::new(format!(
"{} {}",
changed_files,
if changed_files == 1 { "file" } else { "files" }
))
.size(LabelSize::XSmall),
),
)
.child(
h_flex()
.gap_1()
.child(
Button::new("review", "Review Diff")
.label_size(LabelSize::XSmall)
.key_binding({
let focus_handle = focus_handle.clone();
KeyBinding::for_action_in(
&git_ui::project_diff::Diff,
&focus_handle,
window,
cx,
)
.map(|kb| kb.size(rems_from_px(10.)))
})
.on_click(|_event, _window, cx| {
cx.defer(|cx| {
cx.dispatch_action(&git_ui::project_diff::Diff)
});
}),
)
.child(
Button::new("commit", "Commit Changes")
.label_size(LabelSize::XSmall)
.key_binding({
let focus_handle = focus_handle.clone();
KeyBinding::for_action_in(
&ExpandCommitEditor,
&focus_handle,
window,
cx,
)
.map(|kb| kb.size(rems_from_px(10.)))
})
.on_click(|_event, _window, cx| {
cx.defer(|cx| {
cx.dispatch_action(&ExpandCommitEditor)
});
}),
),
),
)
},
)
}),
)
})
.child(
v_flex()
.key_context("MessageEditor")
.on_action(cx.listener(Self::chat))
.on_action(cx.listener(|this, _: &ToggleProfileSelector, window, cx| {
this.profile_selector
.read(cx)
.menu_handle()
.toggle(window, cx);
}))
.on_action(cx.listener(|this, _: &ToggleModelSelector, window, cx| {
this.model_selector
.update(cx, |model_selector, cx| model_selector.toggle(window, cx));

View File

@@ -2,18 +2,23 @@ use std::sync::Arc;
use assistant_settings::{AgentProfile, AssistantSettings};
use fs::Fs;
use gpui::{prelude::*, Action, Entity, Subscription, WeakEntity};
use gpui::{prelude::*, Action, Entity, FocusHandle, Subscription, WeakEntity};
use indexmap::IndexMap;
use settings::{update_settings_file, Settings as _, SettingsStore};
use ui::{prelude::*, ContextMenu, ContextMenuEntry, PopoverMenu, Tooltip};
use ui::{
prelude::*, ButtonLike, ContextMenu, ContextMenuEntry, KeyBinding, PopoverMenu,
PopoverMenuHandle,
};
use util::ResultExt as _;
use crate::{ManageProfiles, ThreadStore};
use crate::{ManageProfiles, ThreadStore, ToggleProfileSelector};
pub struct ProfileSelector {
profiles: IndexMap<Arc<str>, AgentProfile>,
fs: Arc<dyn Fs>,
thread_store: WeakEntity<ThreadStore>,
focus_handle: FocusHandle,
menu_handle: PopoverMenuHandle<ContextMenu>,
_subscriptions: Vec<Subscription>,
}
@@ -21,6 +26,7 @@ impl ProfileSelector {
pub fn new(
fs: Arc<dyn Fs>,
thread_store: WeakEntity<ThreadStore>,
focus_handle: FocusHandle,
cx: &mut Context<Self>,
) -> Self {
let settings_subscription = cx.observe_global::<SettingsStore>(move |this, cx| {
@@ -31,6 +37,8 @@ impl ProfileSelector {
profiles: IndexMap::default(),
fs,
thread_store,
focus_handle,
menu_handle: PopoverMenuHandle::default(),
_subscriptions: vec![settings_subscription],
};
this.refresh_profiles(cx);
@@ -38,6 +46,10 @@ impl ProfileSelector {
this
}
pub fn menu_handle(&self) -> PopoverMenuHandle<ContextMenu> {
self.menu_handle.clone()
}
fn refresh_profiles(&mut self, cx: &mut Context<Self>) {
let settings = AssistantSettings::get_global(cx);
@@ -51,7 +63,7 @@ impl ProfileSelector {
) -> Entity<ContextMenu> {
ContextMenu::build(window, cx, |mut menu, _window, cx| {
let settings = AssistantSettings::get_global(cx);
let icon_position = IconPosition::Start;
let icon_position = IconPosition::End;
menu = menu.header("Profiles");
for (profile_id, profile) in self.profiles.clone() {
@@ -82,14 +94,23 @@ impl ProfileSelector {
}
menu = menu.separator();
menu = menu.item(
ContextMenuEntry::new("Configure Profiles")
.icon(IconName::Pencil)
.icon_color(Color::Muted)
.handler(move |window, cx| {
window.dispatch_action(ManageProfiles.boxed_clone(), cx);
}),
);
menu = menu.header("Customize Current Profile");
menu = menu.item(ContextMenuEntry::new("Tools…").handler({
let profile_id = settings.default_profile.clone();
move |window, cx| {
window.dispatch_action(
ManageProfiles::customize_tools(profile_id.clone()).boxed_clone(),
cx,
);
}
}));
menu = menu.separator();
menu = menu.item(ContextMenuEntry::new("Configure Profiles…").handler(
move |window, cx| {
window.dispatch_action(ManageProfiles::default().boxed_clone(), cx);
},
));
menu
})
@@ -97,25 +118,55 @@ impl ProfileSelector {
}
impl Render for ProfileSelector {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let settings = AssistantSettings::get_global(cx);
let profile = settings
.profiles
.get(&settings.default_profile)
let profile_id = &settings.default_profile;
let profile = settings.profiles.get(profile_id);
let selected_profile = profile
.map(|profile| profile.name.clone())
.unwrap_or_else(|| "Unknown".into());
let icon = match profile_id.as_ref() {
"write" => IconName::Pencil,
"ask" => IconName::MessageBubbles,
_ => IconName::UserRoundPen,
};
let this = cx.entity().clone();
PopoverMenu::new("tool-selector")
let focus_handle = self.focus_handle.clone();
PopoverMenu::new("profile-selector")
.menu(move |window, cx| {
Some(this.update(cx, |this, cx| this.build_context_menu(window, cx)))
})
.trigger_with_tooltip(
Button::new("profile-selector-button", profile)
.style(ButtonStyle::Filled)
.label_size(LabelSize::Small),
Tooltip::text("Change Profile"),
.trigger(
ButtonLike::new("profile-selector-button").child(
h_flex()
.gap_1()
.child(Icon::new(icon).size(IconSize::XSmall).color(Color::Muted))
.child(
Label::new(selected_profile)
.size(LabelSize::Small)
.color(Color::Muted),
)
.child(
Icon::new(IconName::ChevronDown)
.size(IconSize::XSmall)
.color(Color::Muted),
)
.child(div().opacity(0.5).children({
let focus_handle = focus_handle.clone();
KeyBinding::for_action_in(
&ToggleProfileSelector,
&focus_handle,
window,
cx,
)
.map(|kb| kb.size(rems_from_px(10.)))
})),
),
)
.anchor(gpui::Corner::BottomLeft)
.with_handle(self.menu_handle.clone())
}
}

View File

@@ -117,10 +117,13 @@ impl TerminalCodegen {
let result = generate.await;
this.update(cx, |this, cx| {
if let Err(error) = result {
this.status = CodegenStatus::Error(error);
} else {
this.status = CodegenStatus::Done;
match result {
Err(error) => {
this.status = CodegenStatus::Error(error);
}
_ => {
this.status = CodegenStatus::Done;
}
}
cx.emit(CodegenEvent::Finished);
cx.notify();

View File

@@ -164,10 +164,11 @@ impl TerminalInlineAssistant {
}
fn start_assist(&mut self, assist_id: TerminalInlineAssistId, cx: &mut App) {
let assist = if let Some(assist) = self.assists.get_mut(&assist_id) {
assist
} else {
return;
let assist = match self.assists.get_mut(&assist_id) {
Some(assist) => assist,
_ => {
return;
}
};
let Some(user_prompt) = assist
@@ -202,10 +203,11 @@ impl TerminalInlineAssistant {
}
fn stop_assist(&mut self, assist_id: TerminalInlineAssistId, cx: &mut App) {
let assist = if let Some(assist) = self.assists.get_mut(&assist_id) {
assist
} else {
return;
let assist = match self.assists.get_mut(&assist_id) {
Some(assist) => assist,
_ => {
return;
}
};
assist.codegen.update(cx, |codegen, cx| codegen.stop(cx));
@@ -402,10 +404,11 @@ impl TerminalInlineAssist {
window.subscribe(&codegen, cx, move |codegen, event, window, cx| {
TerminalInlineAssistant::update_global(cx, |this, cx| match event {
CodegenEvent::Finished => {
let assist = if let Some(assist) = this.assists.get(&assist_id) {
assist
} else {
return;
let assist = match this.assists.get(&assist_id) {
Some(assist) => assist,
_ => {
return;
}
};
if let CodegenStatus::Error(error) = &codegen.read(cx).status {

View File

@@ -1,5 +1,6 @@
use std::fmt::Write as _;
use std::io::Write;
use std::ops::Range;
use std::sync::Arc;
use anyhow::{Context as _, Result};
@@ -378,14 +379,17 @@ impl Thread {
cx.spawn(async move |this, cx| {
let result = restore.await;
this.update(cx, |this, cx| {
if let Err(err) = result.as_ref() {
this.last_restore_checkpoint = Some(LastRestoreCheckpoint::Error {
message_id: checkpoint.message_id,
error: err.to_string(),
});
} else {
this.truncate(checkpoint.message_id, cx);
this.last_restore_checkpoint = None;
match result.as_ref() {
Err(err) => {
this.last_restore_checkpoint = Some(LastRestoreCheckpoint::Error {
message_id: checkpoint.message_id,
error: err.to_string(),
});
}
_ => {
this.truncate(checkpoint.message_id, cx);
this.last_restore_checkpoint = None;
}
}
this.pending_checkpoint = None;
cx.emit(ThreadEvent::CheckpointChanged);
@@ -720,8 +724,8 @@ impl Thread {
})
.next();
if let Some((rel_rules_path, abs_rules_path)) = selected_rules_file {
cx.spawn(async move |_| {
match selected_rules_file {
Some((rel_rules_path, abs_rules_path)) => cx.spawn(async move |_| {
let rules_file_result = maybe!(async move {
let abs_rules_path = abs_rules_path?;
let text = fs.load(&abs_rules_path).await.with_context(|| {
@@ -750,16 +754,15 @@ impl Thread {
rules_file,
};
(worktree_info, rules_file_error)
})
} else {
Task::ready((
}),
_ => Task::ready((
WorktreeInfoForSystemPrompt {
root_name,
abs_path,
rules_file: None,
},
None,
))
)),
}
}
@@ -786,6 +789,18 @@ impl Thread {
self.stream_completion(request, model, cx);
}
pub fn used_tools_since_last_user_message(&self) -> bool {
for message in self.messages.iter().rev() {
if self.tool_use.message_has_tool_results(message.id) {
return true;
} else if message.role == Role::User {
return false;
}
}
false
}
pub fn to_completion_request(
&self,
request_kind: RequestKind,
@@ -835,6 +850,9 @@ impl Thread {
}
RequestKind::Summarize => {
// We don't care about tool use during summarization.
if self.tool_use.message_has_tool_results(message.id) {
continue;
}
}
}
@@ -857,6 +875,13 @@ impl Thread {
request.messages.push(request_message);
}
// Set a cache breakpoint at the second-to-last message.
// https://docs.anthropic.com/en/docs/build-with-claude/prompt-caching
let breakpoint_index = request.messages.len() - 2;
for (index, message) in request.messages.iter_mut().enumerate() {
message.cache = index == breakpoint_index;
}
if !referenced_context_ids.is_empty() {
let mut context_message = LanguageModelRequestMessage {
role: Role::User,
@@ -1119,7 +1144,10 @@ impl Thread {
request.messages.push(LanguageModelRequestMessage {
role: Role::User,
content: vec![
"Generate a concise 3-7 word title for this conversation, omitting punctuation. Go straight to the title, without any preamble and prefix like `Here's a concise suggestion:...` or `Title:`"
"Generate a concise 3-7 word title for this conversation, omitting punctuation. \
Go straight to the title, without any preamble and prefix like `Here's a concise suggestion:...` or `Title:`. \
If the conversation is about a specific subject, include it in the title. \
Be descriptive. DO NOT speak in the first person."
.into(),
],
cache: false,
@@ -1160,7 +1188,7 @@ impl Thread {
pub fn use_pending_tools(
&mut self,
cx: &mut Context<Self>,
) -> impl IntoIterator<Item = PendingToolUse> {
) -> impl IntoIterator<Item = PendingToolUse> + use<> {
let request = self.to_completion_request(RequestKind::Chat, cx);
let messages = Arc::new(request.messages);
let pending_tool_uses = self
@@ -1172,37 +1200,43 @@ impl Thread {
.collect::<Vec<_>>();
for tool_use in pending_tool_uses.iter() {
if let Some(tool) = self.tools.tool(&tool_use.name, cx) {
if tool.needs_confirmation()
&& !AssistantSettings::get_global(cx).always_allow_tool_actions
{
self.tool_use.confirm_tool_use(
tool_use.id.clone(),
tool_use.ui_text.clone(),
tool_use.input.clone(),
messages.clone(),
tool,
);
cx.emit(ThreadEvent::ToolConfirmationNeeded);
} else {
self.run_tool(
tool_use.id.clone(),
tool_use.ui_text.clone(),
tool_use.input.clone(),
&messages,
tool,
cx,
);
match self.tools.tool(&tool_use.name, cx) {
Some(tool) => {
if tool.needs_confirmation()
&& !AssistantSettings::get_global(cx).always_allow_tool_actions
{
self.tool_use.confirm_tool_use(
tool_use.id.clone(),
tool_use.ui_text.clone(),
tool_use.input.clone(),
messages.clone(),
tool,
);
cx.emit(ThreadEvent::ToolConfirmationNeeded);
} else {
self.run_tool(
tool_use.id.clone(),
tool_use.ui_text.clone(),
tool_use.input.clone(),
&messages,
tool,
cx,
);
}
}
} else if let Some(tool) = self.tools.tool(&tool_use.name, cx) {
self.run_tool(
tool_use.id.clone(),
tool_use.ui_text.clone(),
tool_use.input.clone(),
&messages,
tool,
cx,
);
_ => match self.tools.tool(&tool_use.name, cx) {
Some(tool) => {
self.run_tool(
tool_use.id.clone(),
tool_use.ui_text.clone(),
tool_use.input.clone(),
&messages,
tool,
cx,
);
}
_ => {}
},
}
}
@@ -1216,7 +1250,7 @@ impl Thread {
input: serde_json::Value,
messages: &[LanguageModelRequestMessage],
tool: Arc<dyn Tool>,
cx: &mut Context<'_, Thread>,
cx: &mut Context<Thread>,
) {
let task = self.spawn_tool_use(tool_use_id.clone(), messages, input, tool, cx);
self.tool_use
@@ -1504,6 +1538,25 @@ impl Thread {
Ok(String::from_utf8_lossy(&markdown).to_string())
}
pub fn review_edits_in_range(
&mut self,
buffer: Entity<language::Buffer>,
buffer_range: Range<language::Anchor>,
accept: bool,
cx: &mut Context<Self>,
) {
self.action_log.update(cx, |action_log, cx| {
action_log.review_edits_in_range(buffer, buffer_range, accept, cx)
});
}
/// Keeps all edits across all buffers at once.
/// This provides a more performant alternative to calling review_edits_in_range for each buffer.
pub fn keep_all_edits(&mut self, cx: &mut Context<Self>) {
self.action_log
.update(cx, |action_log, _cx| action_log.keep_all_edits());
}
pub fn action_log(&self) -> &Entity<ActionLog> {
&self.action_log
}

View File

@@ -23,6 +23,7 @@ pub struct ToolUse {
pub status: ToolUseStatus,
pub input: serde_json::Value,
pub icon: ui::IconName,
pub needs_confirmation: bool,
}
#[derive(Debug, Clone)]
@@ -165,8 +166,8 @@ impl ToolUseState {
};
}
if let Some(pending_tool_use) = self.pending_tool_uses_by_id.get(&tool_use.id) {
match pending_tool_use.status {
match self.pending_tool_uses_by_id.get(&tool_use.id) {
Some(pending_tool_use) => match pending_tool_use.status {
PendingToolUseStatus::Idle => ToolUseStatus::Pending,
PendingToolUseStatus::NeedsConfirmation { .. } => {
ToolUseStatus::NeedsConfirmation
@@ -175,16 +176,14 @@ impl ToolUseState {
PendingToolUseStatus::Error(ref err) => {
ToolUseStatus::Error(err.clone().into())
}
}
} else {
ToolUseStatus::Pending
},
_ => ToolUseStatus::Pending,
}
})();
let icon = if let Some(tool) = self.tools.tool(&tool_use.name, cx) {
tool.icon()
} else {
IconName::Cog
let (icon, needs_confirmation) = match self.tools.tool(&tool_use.name, cx) {
Some(tool) => (tool.icon(), tool.needs_confirmation()),
_ => (IconName::Cog, false),
};
tool_uses.push(ToolUse {
@@ -194,6 +193,7 @@ impl ToolUseState {
input: tool_use.input.clone(),
status,
icon,
needs_confirmation,
})
}
@@ -206,10 +206,9 @@ impl ToolUseState {
input: &serde_json::Value,
cx: &App,
) -> SharedString {
if let Some(tool) = self.tools.tool(tool_name, cx) {
tool.ui_text(input).into()
} else {
"Unknown tool".into()
match self.tools.tool(tool_name, cx) {
Some(tool) => tool.ui_text(input).into(),
_ => format!("Unknown tool {tool_name:?}").into(),
}
}

View File

@@ -1,5 +1,5 @@
mod agent_notification;
mod context_pill;
mod tool_ready_pop_up;
pub use agent_notification::*;
pub use context_pill::*;
pub use tool_ready_pop_up::*;

View File

@@ -1,30 +1,35 @@
use gpui::{
point, App, Context, EventEmitter, IntoElement, PlatformDisplay, Size, Window,
WindowBackgroundAppearance, WindowBounds, WindowDecorations, WindowKind, WindowOptions,
linear_color_stop, linear_gradient, point, App, Context, EventEmitter, IntoElement,
PlatformDisplay, Size, Window, WindowBackgroundAppearance, WindowBounds, WindowDecorations,
WindowKind, WindowOptions,
};
use release_channel::ReleaseChannel;
use std::rc::Rc;
use theme;
use ui::{prelude::*, Render};
pub struct ToolReadyPopUp {
pub struct AgentNotification {
title: SharedString,
caption: SharedString,
icon: IconName,
icon_color: Color,
}
impl ToolReadyPopUp {
pub fn new(caption: impl Into<SharedString>, icon: IconName, icon_color: Color) -> Self {
impl AgentNotification {
pub fn new(
title: impl Into<SharedString>,
caption: impl Into<SharedString>,
icon: IconName,
) -> Self {
Self {
title: title.into(),
caption: caption.into(),
icon,
icon_color,
}
}
pub fn window_options(screen: Rc<dyn PlatformDisplay>, cx: &App) -> WindowOptions {
let size = Size {
width: px(440.),
width: px(450.),
height: px(72.),
};
@@ -58,19 +63,35 @@ impl ToolReadyPopUp {
}
}
pub enum ToolReadyPopupEvent {
pub enum AgentNotificationEvent {
Accepted,
Dismissed,
}
impl EventEmitter<ToolReadyPopupEvent> for ToolReadyPopUp {}
impl EventEmitter<AgentNotificationEvent> for AgentNotification {}
impl Render for ToolReadyPopUp {
impl Render for AgentNotification {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let ui_font = theme::setup_ui_font(window, cx);
let line_height = window.line_height();
let bg = cx.theme().colors().elevated_surface_background;
let gradient_overflow = || {
div()
.h_full()
.absolute()
.w_8()
.bottom_0()
.right_0()
.bg(linear_gradient(
90.,
linear_color_stop(bg, 1.),
linear_color_stop(bg.opacity(0.2), 0.),
))
};
h_flex()
.id("agent-notification")
.size_full()
.p_3()
.gap_4()
@@ -80,38 +101,62 @@ impl Render for ToolReadyPopUp {
.font(ui_font)
.border_color(cx.theme().colors().border)
.rounded_xl()
.on_click(cx.listener(|_, _, _, cx| {
cx.emit(AgentNotificationEvent::Accepted);
}))
.child(
h_flex()
.items_start()
.gap_2()
.flex_1()
.child(
h_flex().h(line_height).justify_center().child(
Icon::new(self.icon)
.color(self.icon_color)
.color(Color::Muted)
.size(IconSize::Small),
),
)
.child(
v_flex()
.child(Headline::new("Agent Panel").size(HeadlineSize::XSmall))
.child(Label::new(self.caption.clone()).color(Color::Muted)),
.child(
div()
.relative()
.text_size(px(14.))
.text_color(cx.theme().colors().text)
.max_w(px(300.))
.truncate()
.child(self.title.clone())
.child(gradient_overflow()),
)
.child(
div()
.relative()
.text_size(px(12.))
.text_color(cx.theme().colors().text_muted)
.max_w(px(340.))
.truncate()
.child(self.caption.clone())
.child(gradient_overflow()),
),
),
)
.child(
h_flex()
.gap_0p5()
v_flex()
.gap_1()
.items_center()
.child(
Button::new("open", "View Panel")
.style(ButtonStyle::Tinted(ui::TintColor::Accent))
.full_width()
.on_click({
cx.listener(move |_this, _event, _, cx| {
cx.emit(ToolReadyPopupEvent::Accepted);
cx.emit(AgentNotificationEvent::Accepted);
})
}),
)
.child(Button::new("dismiss", "Dismiss").on_click({
.child(Button::new("dismiss", "Dismiss").full_width().on_click({
cx.listener(move |_, _event, _, cx| {
cx.emit(ToolReadyPopupEvent::Dismissed);
cx.emit(AgentNotificationEvent::Dismissed);
})
})),
)

View File

@@ -162,7 +162,9 @@ impl RenderOnce for ContextPill {
})
.when_some(on_click.as_ref(), |element, on_click| {
let on_click = on_click.clone();
element.on_click(move |event, window, cx| on_click(event, window, cx))
element
.cursor_pointer()
.on_click(move |event, window, cx| on_click(event, window, cx))
}),
ContextPill::Suggested {
name,
@@ -173,10 +175,14 @@ impl RenderOnce for ContextPill {
} => base_pill
.cursor_pointer()
.pr_1()
.when(*focused, |this| {
this.bg(color.element_background.opacity(0.5))
})
.border_dashed()
.border_color(if *focused {
color.border_focused
} else {
color.border_variant.opacity(0.5)
color.border
})
.hover(|style| style.bg(color.element_hover.opacity(0.5)))
.child(
@@ -190,9 +196,10 @@ impl RenderOnce for ContextPill {
.child(
Label::new(match kind {
ContextKind::File => "Active Tab",
ContextKind::Thread | ContextKind::Directory | ContextKind::FetchedUrl => {
"Active"
}
ContextKind::Thread
| ContextKind::Directory
| ContextKind::FetchedUrl
| ContextKind::Symbol => "Active",
})
.size(LabelSize::XSmall)
.color(Color::Muted),

View File

@@ -1877,15 +1877,18 @@ impl AssistantContext {
if let Some(mut pending_patch) = pending_patch {
let patch_start = pending_patch.range.start.to_offset(buffer);
if let Some(message) = self.message_for_offset(patch_start, cx) {
if message.anchor_range.end == text::Anchor::MAX {
pending_patch.range.end = text::Anchor::MAX;
} else {
let message_end = buffer.anchor_after(message.offset_range.end - 1);
pending_patch.range.end = message_end;
match self.message_for_offset(patch_start, cx) {
Some(message) => {
if message.anchor_range.end == text::Anchor::MAX {
pending_patch.range.end = text::Anchor::MAX;
} else {
let message_end = buffer.anchor_after(message.offset_range.end - 1);
pending_patch.range.end = message_end;
}
}
_ => {
pending_patch.range.end = text::Anchor::MAX;
}
} else {
pending_patch.range.end = text::Anchor::MAX;
}
new_patches.push(pending_patch);
@@ -2453,7 +2456,7 @@ impl AssistantContext {
let result = stream_completion.await;
this.update(cx, |this, cx| {
let error_message = if let Some(error) = result.as_ref().err() {
let error_message = match result.as_ref().err() { Some(error) => {
if error.is::<PaymentRequiredError>() {
cx.emit(ContextEvent::ShowPaymentRequiredError);
this.update_metadata(assistant_message_id, cx, |metadata| {
@@ -2481,12 +2484,12 @@ impl AssistantContext {
});
Some(error_message)
}
} else {
} _ => {
this.update_metadata(assistant_message_id, cx, |metadata| {
metadata.status = MessageStatus::Done;
});
None
};
}};
let language_name = this
.buffer
@@ -2631,15 +2634,16 @@ impl AssistantContext {
}
pub fn cancel_last_assist(&mut self, cx: &mut Context<Self>) -> bool {
if let Some(pending_completion) = self.pending_completions.pop() {
self.update_metadata(pending_completion.assistant_message_id, cx, |metadata| {
if metadata.status == MessageStatus::Pending {
metadata.status = MessageStatus::Canceled;
}
});
true
} else {
false
match self.pending_completions.pop() {
Some(pending_completion) => {
self.update_metadata(pending_completion.assistant_message_id, cx, |metadata| {
if metadata.status == MessageStatus::Pending {
metadata.status = MessageStatus::Canceled;
}
});
true
}
_ => false,
}
}
@@ -2799,121 +2803,123 @@ impl AssistantContext {
) -> (Option<MessageAnchor>, Option<MessageAnchor>) {
let start_message = self.message_for_offset(range.start, cx);
let end_message = self.message_for_offset(range.end, cx);
if let Some((start_message, end_message)) = start_message.zip(end_message) {
// Prevent splitting when range spans multiple messages.
if start_message.id != end_message.id {
return (None, None);
}
let message = start_message;
let role = message.role;
let mut edited_buffer = false;
let mut suffix_start = None;
// TODO: why did this start panicking?
if range.start > message.offset_range.start
&& range.end < message.offset_range.end.saturating_sub(1)
{
if self.buffer.read(cx).chars_at(range.end).next() == Some('\n') {
suffix_start = Some(range.end + 1);
} else if self.buffer.read(cx).reversed_chars_at(range.end).next() == Some('\n') {
suffix_start = Some(range.end);
match start_message.zip(end_message) {
Some((start_message, end_message)) => {
// Prevent splitting when range spans multiple messages.
if start_message.id != end_message.id {
return (None, None);
}
}
let version = self.version.clone();
let suffix = if let Some(suffix_start) = suffix_start {
MessageAnchor {
id: MessageId(self.next_timestamp()),
start: self.buffer.read(cx).anchor_before(suffix_start),
}
} else {
self.buffer.update(cx, |buffer, cx| {
buffer.edit([(range.end..range.end, "\n")], None, cx);
});
edited_buffer = true;
MessageAnchor {
id: MessageId(self.next_timestamp()),
start: self.buffer.read(cx).anchor_before(range.end + 1),
}
};
let message = start_message;
let role = message.role;
let mut edited_buffer = false;
let suffix_metadata = MessageMetadata {
role,
status: MessageStatus::Done,
timestamp: suffix.id.0,
cache: None,
};
self.insert_message(suffix.clone(), suffix_metadata.clone(), cx);
self.push_op(
ContextOperation::InsertMessage {
anchor: suffix.clone(),
metadata: suffix_metadata,
version,
},
cx,
);
let mut suffix_start = None;
let new_messages =
if range.start == range.end || range.start == message.offset_range.start {
(None, Some(suffix))
} else {
let mut prefix_end = None;
if range.start > message.offset_range.start
&& range.end < message.offset_range.end - 1
// TODO: why did this start panicking?
if range.start > message.offset_range.start
&& range.end < message.offset_range.end.saturating_sub(1)
{
if self.buffer.read(cx).chars_at(range.end).next() == Some('\n') {
suffix_start = Some(range.end + 1);
} else if self.buffer.read(cx).reversed_chars_at(range.end).next() == Some('\n')
{
if self.buffer.read(cx).chars_at(range.start).next() == Some('\n') {
prefix_end = Some(range.start + 1);
} else if self.buffer.read(cx).reversed_chars_at(range.start).next()
== Some('\n')
{
prefix_end = Some(range.start);
}
suffix_start = Some(range.end);
}
}
let version = self.version.clone();
let selection = if let Some(prefix_end) = prefix_end {
MessageAnchor {
id: MessageId(self.next_timestamp()),
start: self.buffer.read(cx).anchor_before(prefix_end),
}
} else {
self.buffer.update(cx, |buffer, cx| {
buffer.edit([(range.start..range.start, "\n")], None, cx)
});
edited_buffer = true;
MessageAnchor {
id: MessageId(self.next_timestamp()),
start: self.buffer.read(cx).anchor_before(range.end + 1),
}
};
let selection_metadata = MessageMetadata {
role,
status: MessageStatus::Done,
timestamp: selection.id.0,
cache: None,
};
self.insert_message(selection.clone(), selection_metadata.clone(), cx);
self.push_op(
ContextOperation::InsertMessage {
anchor: selection.clone(),
metadata: selection_metadata,
version,
},
cx,
);
(Some(selection), Some(suffix))
let version = self.version.clone();
let suffix = if let Some(suffix_start) = suffix_start {
MessageAnchor {
id: MessageId(self.next_timestamp()),
start: self.buffer.read(cx).anchor_before(suffix_start),
}
} else {
self.buffer.update(cx, |buffer, cx| {
buffer.edit([(range.end..range.end, "\n")], None, cx);
});
edited_buffer = true;
MessageAnchor {
id: MessageId(self.next_timestamp()),
start: self.buffer.read(cx).anchor_before(range.end + 1),
}
};
if !edited_buffer {
cx.emit(ContextEvent::MessagesEdited);
let suffix_metadata = MessageMetadata {
role,
status: MessageStatus::Done,
timestamp: suffix.id.0,
cache: None,
};
self.insert_message(suffix.clone(), suffix_metadata.clone(), cx);
self.push_op(
ContextOperation::InsertMessage {
anchor: suffix.clone(),
metadata: suffix_metadata,
version,
},
cx,
);
let new_messages =
if range.start == range.end || range.start == message.offset_range.start {
(None, Some(suffix))
} else {
let mut prefix_end = None;
if range.start > message.offset_range.start
&& range.end < message.offset_range.end - 1
{
if self.buffer.read(cx).chars_at(range.start).next() == Some('\n') {
prefix_end = Some(range.start + 1);
} else if self.buffer.read(cx).reversed_chars_at(range.start).next()
== Some('\n')
{
prefix_end = Some(range.start);
}
}
let version = self.version.clone();
let selection = if let Some(prefix_end) = prefix_end {
MessageAnchor {
id: MessageId(self.next_timestamp()),
start: self.buffer.read(cx).anchor_before(prefix_end),
}
} else {
self.buffer.update(cx, |buffer, cx| {
buffer.edit([(range.start..range.start, "\n")], None, cx)
});
edited_buffer = true;
MessageAnchor {
id: MessageId(self.next_timestamp()),
start: self.buffer.read(cx).anchor_before(range.end + 1),
}
};
let selection_metadata = MessageMetadata {
role,
status: MessageStatus::Done,
timestamp: selection.id.0,
cache: None,
};
self.insert_message(selection.clone(), selection_metadata.clone(), cx);
self.push_op(
ContextOperation::InsertMessage {
anchor: selection.clone(),
metadata: selection_metadata,
version,
},
cx,
);
(Some(selection), Some(suffix))
};
if !edited_buffer {
cx.emit(ContextEvent::MessagesEdited);
}
new_messages
}
new_messages
} else {
(None, None)
_ => (None, None),
}
}

View File

@@ -403,23 +403,31 @@ impl ContextEditor {
if request_type == RequestType::SuggestEdits && !self.context.read(cx).contains_files(cx) {
self.last_error = Some(AssistError::FileRequired);
cx.notify();
} else if let Some(user_message) = self
.context
.update(cx, |context, cx| context.assist(request_type, cx))
{
let new_selection = {
let cursor = user_message
.start
.to_offset(self.context.read(cx).buffer().read(cx));
cursor..cursor
};
self.editor.update(cx, |editor, cx| {
editor.change_selections(Some(Autoscroll::fit()), window, cx, |selections| {
selections.select_ranges([new_selection])
});
});
// Avoid scrolling to the new cursor position so the assistant's output is stable.
cx.defer_in(window, |this, _, _| this.scroll_position = None);
} else {
match self
.context
.update(cx, |context, cx| context.assist(request_type, cx))
{
Some(user_message) => {
let new_selection = {
let cursor = user_message
.start
.to_offset(self.context.read(cx).buffer().read(cx));
cursor..cursor
};
self.editor.update(cx, |editor, cx| {
editor.change_selections(
Some(Autoscroll::fit()),
window,
cx,
|selections| selections.select_ranges([new_selection]),
);
});
// Avoid scrolling to the new cursor position so the assistant's output is stable.
cx.defer_in(window, |this, _, _| this.scroll_position = None);
}
_ => {}
}
}
cx.notify();
@@ -817,62 +825,68 @@ impl ContextEditor {
}
self.editor.update(cx, |editor, cx| {
if let Some(invoked_slash_command) =
self.context.read(cx).invoked_slash_command(&command_id)
{
if let InvokedSlashCommandStatus::Finished = invoked_slash_command.status {
let buffer = editor.buffer().read(cx).snapshot(cx);
let (&excerpt_id, _buffer_id, _buffer_snapshot) =
buffer.as_singleton().unwrap();
match self.context.read(cx).invoked_slash_command(&command_id) {
Some(invoked_slash_command) => match invoked_slash_command.status {
InvokedSlashCommandStatus::Finished => {
let buffer = editor.buffer().read(cx).snapshot(cx);
let (&excerpt_id, _buffer_id, _buffer_snapshot) =
buffer.as_singleton().unwrap();
let start = buffer
.anchor_in_excerpt(excerpt_id, invoked_slash_command.range.start)
.unwrap();
let end = buffer
.anchor_in_excerpt(excerpt_id, invoked_slash_command.range.end)
.unwrap();
editor.remove_folds_with_type(
&[start..end],
TypeId::of::<PendingSlashCommand>(),
false,
cx,
);
let start = buffer
.anchor_in_excerpt(excerpt_id, invoked_slash_command.range.start)
.unwrap();
let end = buffer
.anchor_in_excerpt(excerpt_id, invoked_slash_command.range.end)
.unwrap();
editor.remove_folds_with_type(
&[start..end],
TypeId::of::<PendingSlashCommand>(),
false,
cx,
);
editor.remove_creases(
HashSet::from_iter(
self.invoked_slash_command_creases.remove(&command_id),
),
cx,
);
}
_ => {
if let hash_map::Entry::Vacant(entry) =
self.invoked_slash_command_creases.entry(command_id)
{
let buffer = editor.buffer().read(cx).snapshot(cx);
let (&excerpt_id, _buffer_id, _buffer_snapshot) =
buffer.as_singleton().unwrap();
let context = self.context.downgrade();
let crease_start = buffer
.anchor_in_excerpt(excerpt_id, invoked_slash_command.range.start)
.unwrap();
let crease_end = buffer
.anchor_in_excerpt(excerpt_id, invoked_slash_command.range.end)
.unwrap();
let crease = Crease::inline(
crease_start..crease_end,
invoked_slash_command_fold_placeholder(command_id, context),
fold_toggle("invoked-slash-command"),
|_row, _folded, _window, _cx| Empty.into_any(),
);
let crease_ids = editor.insert_creases([crease.clone()], cx);
editor.fold_creases(vec![crease], false, window, cx);
entry.insert(crease_ids[0]);
} else {
cx.notify()
}
}
},
_ => {
editor.remove_creases(
HashSet::from_iter(self.invoked_slash_command_creases.remove(&command_id)),
cx,
);
} else if let hash_map::Entry::Vacant(entry) =
self.invoked_slash_command_creases.entry(command_id)
{
let buffer = editor.buffer().read(cx).snapshot(cx);
let (&excerpt_id, _buffer_id, _buffer_snapshot) =
buffer.as_singleton().unwrap();
let context = self.context.downgrade();
let crease_start = buffer
.anchor_in_excerpt(excerpt_id, invoked_slash_command.range.start)
.unwrap();
let crease_end = buffer
.anchor_in_excerpt(excerpt_id, invoked_slash_command.range.end)
.unwrap();
let crease = Crease::inline(
crease_start..crease_end,
invoked_slash_command_fold_placeholder(command_id, context),
fold_toggle("invoked-slash-command"),
|_row, _folded, _window, _cx| Empty.into_any(),
);
let crease_ids = editor.insert_creases([crease.clone()], cx);
editor.fold_creases(vec![crease], false, window, cx);
entry.insert(crease_ids[0]);
} else {
cx.notify()
cx.notify();
}
} else {
editor.remove_creases(
HashSet::from_iter(self.invoked_slash_command_creases.remove(&command_id)),
cx,
);
cx.notify();
};
});
}
@@ -958,34 +972,37 @@ impl ContextEditor {
);
let should_refold;
if let Some(state) = self.patches.get_mut(&range) {
if let Some(editor_state) = &state.editor {
if editor_state.opened_patch != patch {
state.update_task = Some({
let this = this.clone();
cx.spawn_in(window, async move |_, cx| {
Self::update_patch_editor(this.clone(), patch, cx)
.await
.log_err();
})
});
match self.patches.get_mut(&range) {
Some(state) => {
if let Some(editor_state) = &state.editor {
if editor_state.opened_patch != patch {
state.update_task = Some({
let this = this.clone();
cx.spawn_in(window, async move |_, cx| {
Self::update_patch_editor(this.clone(), patch, cx)
.await
.log_err();
})
});
}
}
should_refold = snapshot
.intersects_fold(patch_start.to_offset(&snapshot.buffer_snapshot));
}
_ => {
let crease_id = editor.insert_creases([crease.clone()], cx)[0];
self.patches.insert(
range.clone(),
PatchViewState {
crease_id,
editor: None,
update_task: None,
},
);
should_refold =
snapshot.intersects_fold(patch_start.to_offset(&snapshot.buffer_snapshot));
} else {
let crease_id = editor.insert_creases([crease.clone()], cx)[0];
self.patches.insert(
range.clone(),
PatchViewState {
crease_id,
editor: None,
update_task: None,
},
);
should_refold = true;
should_refold = true;
}
}
if should_refold {
@@ -1175,16 +1192,20 @@ impl ContextEditor {
}
}
if let Some(editor) = editor {
self.workspace
.update(cx, |workspace, cx| {
workspace.activate_item(&editor, true, false, window, cx);
})
.ok();
} else {
patch_state.update_task = Some(cx.spawn_in(window, async move |this, cx| {
Self::open_patch_editor(this, new_patch, cx).await.log_err();
}));
match editor {
Some(editor) => {
self.workspace
.update(cx, |workspace, cx| {
workspace.activate_item(&editor, true, false, window, cx);
})
.ok();
}
_ => {
patch_state.update_task =
Some(cx.spawn_in(window, async move |this, cx| {
Self::open_patch_editor(this, new_patch, cx).await.log_err();
}));
}
}
}
}
@@ -1282,16 +1303,19 @@ impl ContextEditor {
.collect();
if let Some(state) = &mut patch_state.editor {
if let Some(editor) = state.editor.upgrade() {
editor.update(cx, |editor, cx| {
editor.set_title(patch.title.clone(), cx);
editor.reset_locations(locations, window, cx);
resolved_patch.apply(editor, cx);
});
match state.editor.upgrade() {
Some(editor) => {
editor.update(cx, |editor, cx| {
editor.set_title(patch.title.clone(), cx);
editor.reset_locations(locations, window, cx);
resolved_patch.apply(editor, cx);
});
state.opened_patch = patch;
} else {
patch_state.editor.take();
state.opened_patch = patch;
}
_ => {
patch_state.editor.take();
}
}
}
patch_state.update_task.take();
@@ -1573,25 +1597,28 @@ impl ContextEditor {
let mut new_blocks = vec![];
let mut block_index_to_message = vec![];
for message in self.context.read(cx).messages(cx) {
if let Some(_) = blocks_to_remove.remove(&message.id) {
// This is an old message that we might modify.
let Some((meta, block_id)) = old_blocks.get_mut(&message.id) else {
debug_assert!(
false,
"old_blocks should contain a message_id we've just removed."
);
continue;
};
// Should we modify it?
let message_meta = MessageMetadata::from(&message);
if meta != &message_meta {
blocks_to_replace.insert(*block_id, render_block(message_meta.clone()));
*meta = message_meta;
match blocks_to_remove.remove(&message.id) {
Some(_) => {
// This is an old message that we might modify.
let Some((meta, block_id)) = old_blocks.get_mut(&message.id) else {
debug_assert!(
false,
"old_blocks should contain a message_id we've just removed."
);
continue;
};
// Should we modify it?
let message_meta = MessageMetadata::from(&message);
if meta != &message_meta {
blocks_to_replace.insert(*block_id, render_block(message_meta.clone()));
*meta = message_meta;
}
}
_ => {
// This is a new message.
new_blocks.push(create_block_properties(&message));
block_index_to_message.push((message.id, MessageMetadata::from(&message)));
}
} else {
// This is a new message.
new_blocks.push(create_block_properties(&message));
block_index_to_message.push((message.id, MessageMetadata::from(&message)));
}
}
editor.replace_blocks(blocks_to_replace, None, cx);
@@ -2321,54 +2348,67 @@ impl ContextEditor {
)
.into_any_element(),
)
} else if let Some(configuration_error) = configuration_error(cx) {
let label = match configuration_error {
ConfigurationError::NoProvider => "No LLM provider selected.",
ConfigurationError::ProviderNotAuthenticated => "LLM provider is not configured.",
ConfigurationError::ProviderPendingTermsAcceptance(_) => {
"LLM provider requires accepting the Terms of Service."
}
};
Some(
h_flex()
.px_3()
.py_2()
.border_b_1()
.border_color(cx.theme().colors().border_variant)
.bg(cx.theme().colors().editor_background)
.justify_between()
.child(
h_flex()
.gap_3()
.child(
Icon::new(IconName::Warning)
.size(IconSize::Small)
.color(Color::Warning),
)
.child(Label::new(label)),
)
.child(
Button::new("open-configuration", "Configure Providers")
.size(ButtonSize::Compact)
.icon(Some(IconName::SlidersVertical))
.icon_size(IconSize::Small)
.icon_position(IconPosition::Start)
.style(ButtonStyle::Filled)
.on_click({
let focus_handle = self.focus_handle(cx).clone();
move |_event, window, cx| {
focus_handle.dispatch_action(&ShowConfiguration, window, cx);
}
}),
)
.into_any_element(),
)
} else {
None
match configuration_error(cx) {
Some(configuration_error) => {
let label = match configuration_error {
ConfigurationError::NoProvider => "No LLM provider selected.",
ConfigurationError::ProviderNotAuthenticated => {
"LLM provider is not configured."
}
ConfigurationError::ProviderPendingTermsAcceptance(_) => {
"LLM provider requires accepting the Terms of Service."
}
};
Some(
h_flex()
.px_3()
.py_2()
.border_b_1()
.border_color(cx.theme().colors().border_variant)
.bg(cx.theme().colors().editor_background)
.justify_between()
.child(
h_flex()
.gap_3()
.child(
Icon::new(IconName::Warning)
.size(IconSize::Small)
.color(Color::Warning),
)
.child(Label::new(label)),
)
.child(
Button::new("open-configuration", "Configure Providers")
.size(ButtonSize::Compact)
.icon(Some(IconName::SlidersVertical))
.icon_size(IconSize::Small)
.icon_position(IconPosition::Start)
.style(ButtonStyle::Filled)
.on_click({
let focus_handle = self.focus_handle(cx).clone();
move |_event, window, cx| {
focus_handle.dispatch_action(
&ShowConfiguration,
window,
cx,
);
}
}),
)
.into_any_element(),
)
}
_ => None,
}
}
}
fn render_send_button(&self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
fn render_send_button(
&self,
window: &mut Window,
cx: &mut Context<Self>,
) -> impl IntoElement + use<> {
let focus_handle = self.focus_handle(cx).clone();
let (style, tooltip) = match token_state(&self.context, cx) {
@@ -2427,7 +2467,11 @@ impl ContextEditor {
})
}
fn render_edit_button(&self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
fn render_edit_button(
&self,
window: &mut Window,
cx: &mut Context<Self>,
) -> impl IntoElement + use<> {
let focus_handle = self.focus_handle(cx).clone();
let (style, tooltip) = match token_state(&self.context, cx) {
@@ -2480,7 +2524,7 @@ impl ContextEditor {
})
}
fn render_inject_context_menu(&self, cx: &mut Context<Self>) -> impl IntoElement {
fn render_inject_context_menu(&self, cx: &mut Context<Self>) -> impl IntoElement + use<> {
slash_command_picker::SlashCommandSelector::new(
self.slash_commands.clone(),
cx.entity().downgrade(),
@@ -2499,7 +2543,7 @@ impl ContextEditor {
)
}
fn render_language_model_selector(&self, cx: &mut Context<Self>) -> impl IntoElement {
fn render_language_model_selector(&self, cx: &mut Context<Self>) -> impl IntoElement + use<> {
let active_model = LanguageModelRegistry::read_global(cx).active_model();
let focus_handle = self.editor().focus_handle(cx).clone();
let model_name = match active_model {
@@ -3283,12 +3327,9 @@ impl FollowableItem for ContextEditor {
Some(proto::view::Variant::ContextEditor(
proto::view::ContextEditor {
context_id: context.id().to_proto(),
editor: if let Some(proto::view::Variant::Editor(proto)) =
self.editor.read(cx).to_state_proto(window, cx)
{
Some(proto)
} else {
None
editor: match self.editor.read(cx).to_state_proto(window, cx) {
Some(proto::view::Variant::Editor(proto)) => Some(proto),
_ => None,
},
},
))
@@ -3413,7 +3454,7 @@ impl ContextEditorToolbarItem {
pub fn render_remaining_tokens(
context_editor: &Entity<ContextEditor>,
cx: &App,
) -> Option<impl IntoElement> {
) -> Option<impl IntoElement + use<>> {
let context = &context_editor.read(cx).context;
let (token_count_color, token_count, max_token_count, tooltip) = match token_state(context, cx)?

View File

@@ -299,13 +299,12 @@ impl ContextStore {
}
if is_shared {
self.contexts.retain_mut(|context| {
if let Some(strong_context) = context.upgrade() {
self.contexts.retain_mut(|context| match context.upgrade() {
Some(strong_context) => {
*context = ContextHandle::Strong(strong_context);
true
} else {
false
}
_ => false,
});
let remote_id = self.project.read(cx).remote_id().unwrap();
self.client_subscription = self
@@ -336,8 +335,8 @@ impl ContextStore {
self.synchronize_contexts(cx);
}
project::Event::DisconnectedFromHost => {
self.contexts.retain_mut(|context| {
if let Some(strong_context) = context.upgrade() {
self.contexts.retain_mut(|context| match context.upgrade() {
Some(strong_context) => {
*context = ContextHandle::Weak(context.downgrade());
strong_context.update(cx, |context, cx| {
if context.replica_id() != ReplicaId::default() {
@@ -345,9 +344,8 @@ impl ContextStore {
}
});
true
} else {
false
}
_ => false,
});
self.host_contexts.clear();
cx.notify();
@@ -422,12 +420,13 @@ impl ContextStore {
.await?;
context.update(cx, |context, cx| context.apply_ops(operations, cx))?;
this.update(cx, |this, cx| {
if let Some(existing_context) = this.loaded_context_for_id(&context_id, cx) {
existing_context
} else {
this.register_context(&context, cx);
this.synchronize_contexts(cx);
context
match this.loaded_context_for_id(&context_id, cx) {
Some(existing_context) => existing_context,
_ => {
this.register_context(&context, cx);
this.synchronize_contexts(cx);
context
}
}
})
})
@@ -471,11 +470,12 @@ impl ContextStore {
)
})?;
this.update(cx, |this, cx| {
if let Some(existing_context) = this.loaded_context_for_path(&path, cx) {
existing_context
} else {
this.register_context(&context, cx);
context
match this.loaded_context_for_path(&path, cx) {
Some(existing_context) => existing_context,
_ => {
this.register_context(&context, cx);
context
}
}
})
})
@@ -591,12 +591,13 @@ impl ContextStore {
.await?;
context.update(cx, |context, cx| context.apply_ops(operations, cx))?;
this.update(cx, |this, cx| {
if let Some(existing_context) = this.loaded_context_for_id(&context_id, cx) {
existing_context
} else {
this.register_context(&context, cx);
this.synchronize_contexts(cx);
context
match this.loaded_context_for_id(&context_id, cx) {
Some(existing_context) => existing_context,
_ => {
this.register_context(&context, cx);
this.synchronize_contexts(cx);
context
}
}
})
})

View File

@@ -151,29 +151,27 @@ impl SlashCommandCompletionProvider {
let mut flag = self.cancel_flag.lock();
flag.store(true, SeqCst);
*flag = new_cancel_flag.clone();
if let Some(command) = self.slash_commands.command(command_name, cx) {
let completions = command.complete_argument(
arguments,
new_cancel_flag.clone(),
self.workspace.clone(),
window,
cx,
);
let command_name: Arc<str> = command_name.into();
let editor = self.editor.clone();
let workspace = self.workspace.clone();
let arguments = arguments.to_vec();
cx.background_spawn(async move {
Ok(Some(
completions
.await?
.into_iter()
.map(|new_argument| {
let confirm =
editor
.clone()
.zip(workspace.clone())
.map(|(editor, workspace)| {
match self.slash_commands.command(command_name, cx) {
Some(command) => {
let completions = command.complete_argument(
arguments,
new_cancel_flag.clone(),
self.workspace.clone(),
window,
cx,
);
let command_name: Arc<str> = command_name.into();
let editor = self.editor.clone();
let workspace = self.workspace.clone();
let arguments = arguments.to_vec();
cx.background_spawn(async move {
Ok(Some(
completions
.await?
.into_iter()
.map(|new_argument| {
let confirm = editor.clone().zip(workspace.clone()).map(
|(editor, workspace)| {
Arc::new({
let mut completed_arguments = arguments.clone();
if new_argument.replace_previous_arguments {
@@ -210,32 +208,33 @@ impl SlashCommandCompletionProvider {
}
}
}) as Arc<_>
});
},
);
let mut new_text = new_argument.new_text.clone();
if new_argument.after_completion == AfterCompletion::Continue {
new_text.push(' ');
}
let mut new_text = new_argument.new_text.clone();
if new_argument.after_completion == AfterCompletion::Continue {
new_text.push(' ');
}
project::Completion {
old_range: if new_argument.replace_previous_arguments {
argument_range.clone()
} else {
last_argument_range.clone()
},
label: new_argument.label,
icon_path: None,
new_text,
documentation: None,
confirm,
source: CompletionSource::Custom,
}
})
.collect(),
))
})
} else {
Task::ready(Ok(Some(Vec::new())))
project::Completion {
old_range: if new_argument.replace_previous_arguments {
argument_range.clone()
} else {
last_argument_range.clone()
},
label: new_argument.label,
icon_path: None,
new_text,
documentation: None,
confirm,
source: CompletionSource::Custom,
}
})
.collect(),
))
})
}
_ => Task::ready(Ok(Some(Vec::new()))),
}
}
}
@@ -333,10 +332,9 @@ impl CompletionProvider for SlashCommandCompletionProvider {
let position = position.to_point(buffer);
let line_start = Point::new(position.row, 0);
let mut lines = buffer.text_for_range(line_start..position).lines();
if let Some(line) = lines.next() {
SlashCommandLine::parse(line).is_some()
} else {
false
match lines.next() {
Some(line) => SlashCommandLine::parse(line).is_some(),
_ => false,
}
}

View File

@@ -21,6 +21,7 @@ clap.workspace = true
client.workspace = true
collections.workspace = true
context_server.workspace = true
dap.workspace = true
env_logger.workspace = true
fs.workspace = true
futures.workspace = true

View File

@@ -6,15 +6,6 @@ fn main() {
if cfg!(target_os = "macos") {
println!("cargo:rustc-env=MACOSX_DEPLOYMENT_TARGET=10.15.7");
println!("cargo:rerun-if-env-changed=ZED_BUNDLE");
if std::env::var("ZED_BUNDLE").ok().as_deref() == Some("true") {
// Find WebRTC.framework in the Frameworks folder when running as part of an application bundle.
println!("cargo:rustc-link-arg=-Wl,-rpath,@executable_path/../Frameworks");
} else {
// Find WebRTC.framework as a sibling of the executable when running outside of an application bundle.
println!("cargo:rustc-link-arg=-Wl,-rpath,@executable_path");
}
// Weakly link ReplayKit to ensure Zed can be used on macOS 10.15+.
println!("cargo:rustc-link-arg=-Wl,-weak_framework,ReplayKit");

View File

@@ -3,6 +3,7 @@ use assistant2::{RequestKind, Thread, ThreadEvent, ThreadStore};
use assistant_tool::ToolWorkingSet;
use client::{Client, UserStore};
use collections::HashMap;
use dap::DapRegistry;
use futures::StreamExt;
use gpui::{prelude::*, App, AsyncApp, Entity, SemanticVersion, Subscription, Task};
use language::LanguageRegistry;
@@ -50,6 +51,7 @@ impl HeadlessAssistant {
app_state.node_runtime.clone(),
app_state.user_store.clone(),
app_state.languages.clone(),
Arc::new(DapRegistry::default()),
app_state.fs.clone(),
env,
cx,

View File

@@ -26,6 +26,15 @@ pub enum AssistantDockPosition {
Bottom,
}
#[derive(Copy, Clone, Default, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum NotifyWhenAgentWaiting {
#[default]
PrimaryScreen,
AllScreens,
Never,
}
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
#[serde(tag = "name", rename_all = "snake_case")]
pub enum AssistantProviderContentV1 {
@@ -74,7 +83,7 @@ pub struct AssistantSettings {
pub default_profile: Arc<str>,
pub profiles: IndexMap<Arc<str>, AgentProfile>,
pub always_allow_tool_actions: bool,
pub notify_when_agent_waiting: bool,
pub notify_when_agent_waiting: NotifyWhenAgentWaiting,
}
impl AssistantSettings {
@@ -96,8 +105,8 @@ impl JsonSchema for AssistantSettingsContent {
VersionedAssistantSettingsContent::schema_name()
}
fn json_schema(gen: &mut schemars::gen::SchemaGenerator) -> Schema {
VersionedAssistantSettingsContent::json_schema(gen)
fn json_schema(r#gen: &mut schemars::r#gen::SchemaGenerator) -> Schema {
VersionedAssistantSettingsContent::json_schema(r#gen)
}
fn is_referenceable() -> bool {
@@ -394,10 +403,10 @@ pub struct AssistantSettingsContentV2 {
///
/// Default: false
always_allow_tool_actions: Option<bool>,
/// Whether to show a popup notification when the agent is waiting for user input.
/// Where to show a popup notification when the agent is waiting for user input.
///
/// Default: true
notify_when_agent_waiting: Option<bool>,
/// Default: "primary_screen"
notify_when_agent_waiting: Option<NotifyWhenAgentWaiting>,
}
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
@@ -407,7 +416,7 @@ pub struct LanguageModelSelection {
pub model: String,
}
fn providers_schema(_: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema {
fn providers_schema(_: &mut schemars::r#gen::SchemaGenerator) -> schemars::schema::Schema {
schemars::schema::SchemaObject {
enum_values: Some(vec![
"anthropic".into(),

View File

@@ -29,13 +29,9 @@ html_to_markdown.workspace = true
http_client.workspace = true
indexed_docs.workspace = true
language.workspace = true
language_model.workspace = true
log.workspace = true
project.workspace = true
prompt_store.workspace = true
rope.workspace = true
schemars.workspace = true
semantic_index.workspace = true
serde.workspace = true
serde_json.workspace = true
smol.workspace = true

View File

@@ -1,4 +1,3 @@
mod auto_command;
mod cargo_workspace_command;
mod context_server_command;
mod default_command;
@@ -8,9 +7,7 @@ mod docs_command;
mod fetch_command;
mod file_command;
mod now_command;
mod project_command;
mod prompt_command;
mod search_command;
mod selection_command;
mod streaming_example_command;
mod symbols_command;
@@ -21,7 +18,6 @@ use gpui::App;
use language::{CodeLabel, HighlightId};
use ui::ActiveTheme as _;
pub use crate::auto_command::*;
pub use crate::cargo_workspace_command::*;
pub use crate::context_server_command::*;
pub use crate::default_command::*;
@@ -31,9 +27,7 @@ pub use crate::docs_command::*;
pub use crate::fetch_command::*;
pub use crate::file_command::*;
pub use crate::now_command::*;
pub use crate::project_command::*;
pub use crate::prompt_command::*;
pub use crate::search_command::*;
pub use crate::selection_command::*;
pub use crate::streaming_example_command::*;
pub use crate::symbols_command::*;

View File

@@ -1,371 +0,0 @@
use anyhow::{anyhow, Result};
use assistant_slash_command::{
ArgumentCompletion, SlashCommand, SlashCommandOutput, SlashCommandOutputSection,
SlashCommandResult,
};
use feature_flags::FeatureFlag;
use futures::StreamExt;
use gpui::{App, AsyncApp, Task, WeakEntity, Window};
use language::{CodeLabel, LspAdapterDelegate};
use language_model::{
LanguageModelCompletionEvent, LanguageModelRegistry, LanguageModelRequest,
LanguageModelRequestMessage, Role,
};
use semantic_index::{FileSummary, SemanticDb};
use smol::channel;
use std::sync::{atomic::AtomicBool, Arc};
use ui::{prelude::*, BorrowAppContext};
use util::ResultExt;
use workspace::Workspace;
use crate::create_label_for_command;
pub struct AutoSlashCommandFeatureFlag;
impl FeatureFlag for AutoSlashCommandFeatureFlag {
const NAME: &'static str = "auto-slash-command";
}
pub struct AutoCommand;
impl SlashCommand for AutoCommand {
fn name(&self) -> String {
"auto".into()
}
fn description(&self) -> String {
"Automatically infer what context to add".into()
}
fn icon(&self) -> IconName {
IconName::Wand
}
fn menu_text(&self) -> String {
self.description()
}
fn label(&self, cx: &App) -> CodeLabel {
create_label_for_command("auto", &["--prompt"], cx)
}
fn complete_argument(
self: Arc<Self>,
_arguments: &[String],
_cancel: Arc<AtomicBool>,
workspace: Option<WeakEntity<Workspace>>,
_window: &mut Window,
cx: &mut App,
) -> Task<Result<Vec<ArgumentCompletion>>> {
// There's no autocomplete for a prompt, since it's arbitrary text.
// However, we can use this opportunity to kick off a drain of the backlog.
// That way, it can hopefully be done resummarizing by the time we've actually
// typed out our prompt. This re-runs on every keystroke during autocomplete,
// but in the future, we could instead do it only once, when /auto is first entered.
let Some(workspace) = workspace.and_then(|ws| ws.upgrade()) else {
log::warn!("workspace was dropped or unavailable during /auto autocomplete");
return Task::ready(Ok(Vec::new()));
};
let project = workspace.read(cx).project().clone();
let Some(project_index) =
cx.update_global(|index: &mut SemanticDb, cx| index.project_index(project, cx))
else {
return Task::ready(Err(anyhow!("No project indexer, cannot use /auto")));
};
let cx: &mut App = cx;
cx.spawn(async move |cx| {
let task = project_index.read_with(cx, |project_index, cx| {
project_index.flush_summary_backlogs(cx)
})?;
cx.background_spawn(task).await;
anyhow::Ok(Vec::new())
})
}
fn requires_argument(&self) -> bool {
true
}
fn run(
self: Arc<Self>,
arguments: &[String],
_context_slash_command_output_sections: &[SlashCommandOutputSection<language::Anchor>],
_context_buffer: language::BufferSnapshot,
workspace: WeakEntity<Workspace>,
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
window: &mut Window,
cx: &mut App,
) -> Task<SlashCommandResult> {
let Some(workspace) = workspace.upgrade() else {
return Task::ready(Err(anyhow::anyhow!("workspace was dropped")));
};
if arguments.is_empty() {
return Task::ready(Err(anyhow!("missing prompt")));
};
let argument = arguments.join(" ");
let original_prompt = argument.to_string();
let project = workspace.read(cx).project().clone();
let Some(project_index) =
cx.update_global(|index: &mut SemanticDb, cx| index.project_index(project, cx))
else {
return Task::ready(Err(anyhow!("no project indexer")));
};
let task = window.spawn(cx, async move |cx| {
let summaries = project_index
.read_with(cx, |project_index, cx| project_index.all_summaries(cx))?
.await?;
commands_for_summaries(&summaries, &original_prompt, &cx).await
});
// As a convenience, append /auto's argument to the end of the prompt
// so you don't have to write it again.
let original_prompt = argument.to_string();
cx.background_spawn(async move {
let commands = task.await?;
let mut prompt = String::new();
log::info!(
"Translating this response into slash-commands: {:?}",
commands
);
for command in commands {
prompt.push('/');
prompt.push_str(&command.name);
prompt.push(' ');
prompt.push_str(&command.arg);
prompt.push('\n');
}
prompt.push('\n');
prompt.push_str(&original_prompt);
Ok(SlashCommandOutput {
text: prompt,
sections: Vec::new(),
run_commands_in_text: true,
}
.to_event_stream())
})
}
}
const PROMPT_INSTRUCTIONS_BEFORE_SUMMARY: &str = include_str!("prompt_before_summary.txt");
const PROMPT_INSTRUCTIONS_AFTER_SUMMARY: &str = include_str!("prompt_after_summary.txt");
fn summaries_prompt(summaries: &[FileSummary], original_prompt: &str) -> String {
let json_summaries = serde_json::to_string(summaries).unwrap();
format!("{PROMPT_INSTRUCTIONS_BEFORE_SUMMARY}\n{json_summaries}\n{PROMPT_INSTRUCTIONS_AFTER_SUMMARY}\n{original_prompt}")
}
/// The slash commands that the model is told about, and which we look for in the inference response.
const SUPPORTED_SLASH_COMMANDS: &[&str] = &["search", "file"];
#[derive(Debug, Clone)]
struct CommandToRun {
name: String,
arg: String,
}
/// Given the pre-indexed file summaries for this project, as well as the original prompt
/// string passed to `/auto`, get a list of slash commands to run, along with their arguments.
///
/// The prompt's output does not include the slashes (to reduce the chance that it makes a mistake),
/// so taking one of these returned Strings and turning it into a real slash-command-with-argument
/// involves prepending a slash to it.
///
/// This function will validate that each of the returned lines begins with one of SUPPORTED_SLASH_COMMANDS.
/// Any other lines it encounters will be discarded, with a warning logged.
async fn commands_for_summaries(
summaries: &[FileSummary],
original_prompt: &str,
cx: &AsyncApp,
) -> Result<Vec<CommandToRun>> {
if summaries.is_empty() {
log::warn!("Inferring no context because there were no summaries available.");
return Ok(Vec::new());
}
// Use the globally configured model to translate the summaries into slash-commands,
// because Qwen2-7B-Instruct has not done a good job at that task.
let Some(model) = cx.update(|cx| LanguageModelRegistry::read_global(cx).active_model())? else {
log::warn!("Can't infer context because there's no active model.");
return Ok(Vec::new());
};
// Only go up to 90% of the actual max token count, to reduce chances of
// exceeding the token count due to inaccuracies in the token counting heuristic.
let max_token_count = (model.max_token_count() * 9) / 10;
// Rather than recursing (which would require this async function use a pinned box),
// we use an explicit stack of arguments and answers for when we need to "recurse."
let mut stack = vec![summaries];
let mut final_response = Vec::new();
let mut prompts = Vec::new();
// TODO We only need to create multiple Requests because we currently
// don't have the ability to tell if a CompletionProvider::complete response
// was a "too many tokens in this request" error. If we had that, then
// we could try the request once, instead of having to make separate requests
// to check the token count and then afterwards to run the actual prompt.
let make_request = |prompt: String| LanguageModelRequest {
messages: vec![LanguageModelRequestMessage {
role: Role::User,
content: vec![prompt.into()],
// Nothing in here will benefit from caching
cache: false,
}],
tools: Vec::new(),
stop: Vec::new(),
temperature: None,
};
while let Some(current_summaries) = stack.pop() {
// The split can result in one slice being empty and the other having one element.
// Whenever that happens, skip the empty one.
if current_summaries.is_empty() {
continue;
}
log::info!(
"Inferring prompt context using {} file summaries",
current_summaries.len()
);
let prompt = summaries_prompt(&current_summaries, original_prompt);
let start = std::time::Instant::now();
// Per OpenAI, 1 token ~= 4 chars in English (we go with 4.5 to overestimate a bit, because failed API requests cost a lot of perf)
// Verifying this against an actual model.count_tokens() confirms that it's usually within ~5% of the correct answer, whereas
// getting the correct answer from tiktoken takes hundreds of milliseconds (compared to this arithmetic being ~free).
// source: https://help.openai.com/en/articles/4936856-what-are-tokens-and-how-to-count-them
let token_estimate = prompt.len() * 2 / 9;
let duration = start.elapsed();
log::info!(
"Time taken to count tokens for prompt of length {:?}B: {:?}",
prompt.len(),
duration
);
if token_estimate < max_token_count {
prompts.push(prompt);
} else if current_summaries.len() == 1 {
log::warn!("Inferring context for a single file's summary failed because the prompt's token length exceeded the model's token limit.");
} else {
log::info!(
"Context inference using file summaries resulted in a prompt containing {token_estimate} tokens, which exceeded the model's max of {max_token_count}. Retrying as two separate prompts, each including half the number of summaries.",
);
let (left, right) = current_summaries.split_at(current_summaries.len() / 2);
stack.push(right);
stack.push(left);
}
}
let all_start = std::time::Instant::now();
let (tx, rx) = channel::bounded(1024);
let completion_streams = prompts
.into_iter()
.map(|prompt| {
let request = make_request(prompt.clone());
let model = model.clone();
let tx = tx.clone();
let stream = model.stream_completion(request, &cx);
(stream, tx)
})
.collect::<Vec<_>>();
cx.background_spawn(async move {
let futures = completion_streams
.into_iter()
.enumerate()
.map(|(ix, (stream, tx))| async move {
let start = std::time::Instant::now();
let events = stream.await?;
log::info!("Time taken for awaiting /await chunk stream #{ix}: {:?}", start.elapsed());
let completion: String = events
.filter_map(|event| async {
if let Ok(LanguageModelCompletionEvent::Text(text)) = event {
Some(text)
} else {
None
}
})
.collect()
.await;
log::info!("Time taken for all /auto chunks to come back for #{ix}: {:?}", start.elapsed());
for line in completion.split('\n') {
if let Some(first_space) = line.find(' ') {
let command = &line[..first_space].trim();
let arg = &line[first_space..].trim();
tx.send(CommandToRun {
name: command.to_string(),
arg: arg.to_string(),
})
.await?;
} else if !line.trim().is_empty() {
// All slash-commands currently supported in context inference need a space for the argument.
log::warn!(
"Context inference returned a non-blank line that contained no spaces (meaning no argument for the slash command): {:?}",
line
);
}
}
anyhow::Ok(())
})
.collect::<Vec<_>>();
let _ = futures::future::try_join_all(futures).await.log_err();
let duration = all_start.elapsed();
eprintln!("All futures completed in {:?}", duration);
})
.await;
drop(tx); // Close the channel so that rx.collect() won't hang. This is safe because all futures have completed.
let results = rx.collect::<Vec<_>>().await;
eprintln!(
"Finished collecting from the channel with {} results",
results.len()
);
for command in results {
// Don't return empty or duplicate commands
if !command.name.is_empty()
&& !final_response
.iter()
.any(|cmd: &CommandToRun| cmd.name == command.name && cmd.arg == command.arg)
{
if SUPPORTED_SLASH_COMMANDS
.iter()
.any(|supported| &command.name == supported)
{
final_response.push(command);
} else {
log::warn!(
"Context inference returned an unrecognized slash command: {:?}",
command
);
}
}
}
// Sort the commands by name (reversed just so that /search appears before /file)
final_response.sort_by(|cmd1, cmd2| cmd1.name.cmp(&cmd2.name).reverse());
Ok(final_response)
}

View File

@@ -88,8 +88,8 @@ impl SlashCommand for ContextServerSlashCommand {
let server_id = self.server_id.clone();
let prompt_name = self.prompt.name.clone();
if let Some(server) = self.server_manager.read(cx).get_server(&server_id) {
cx.foreground_executor().spawn(async move {
match self.server_manager.read(cx).get_server(&server_id) {
Some(server) => cx.foreground_executor().spawn(async move {
let Some(protocol) = server.client() else {
return Err(anyhow!("Context server not initialized"));
};
@@ -118,9 +118,8 @@ impl SlashCommand for ContextServerSlashCommand {
})
.collect();
Ok(completions)
})
} else {
Task::ready(Err(anyhow!("Context server not found")))
}),
_ => Task::ready(Err(anyhow!("Context server not found"))),
}
}
@@ -143,56 +142,57 @@ impl SlashCommand for ContextServerSlashCommand {
};
let manager = self.server_manager.read(cx);
if let Some(server) = manager.get_server(&server_id) {
cx.foreground_executor().spawn(async move {
let Some(protocol) = server.client() else {
return Err(anyhow!("Context server not initialized"));
};
let result = protocol.run_prompt(&prompt_name, prompt_args).await?;
match manager.get_server(&server_id) {
Some(server) => {
cx.foreground_executor().spawn(async move {
let Some(protocol) = server.client() else {
return Err(anyhow!("Context server not initialized"));
};
let result = protocol.run_prompt(&prompt_name, prompt_args).await?;
// Check that there are only user roles
if result
.messages
.iter()
.any(|msg| !matches!(msg.role, context_server::types::Role::User))
{
return Err(anyhow!(
"Prompt contains non-user roles, which is not supported"
));
}
// Check that there are only user roles
if result
.messages
.iter()
.any(|msg| !matches!(msg.role, context_server::types::Role::User))
{
return Err(anyhow!(
"Prompt contains non-user roles, which is not supported"
));
}
// Extract text from user messages into a single prompt string
let mut prompt = result
.messages
.into_iter()
.filter_map(|msg| match msg.content {
context_server::types::MessageContent::Text { text, .. } => Some(text),
_ => None,
})
.collect::<Vec<String>>()
.join("\n\n");
// Extract text from user messages into a single prompt string
let mut prompt = result
.messages
.into_iter()
.filter_map(|msg| match msg.content {
context_server::types::MessageContent::Text { text, .. } => Some(text),
_ => None,
})
.collect::<Vec<String>>()
.join("\n\n");
// We must normalize the line endings here, since servers might return CR characters.
LineEnding::normalize(&mut prompt);
// We must normalize the line endings here, since servers might return CR characters.
LineEnding::normalize(&mut prompt);
Ok(SlashCommandOutput {
sections: vec![SlashCommandOutputSection {
range: 0..(prompt.len()),
icon: IconName::ZedAssistant,
label: SharedString::from(
result
.description
.unwrap_or(format!("Result from {}", prompt_name)),
),
metadata: None,
}],
text: prompt,
run_commands_in_text: false,
}
.to_event_stream())
})
} else {
Task::ready(Err(anyhow!("Context server not found")))
Ok(SlashCommandOutput {
sections: vec![SlashCommandOutputSection {
range: 0..(prompt.len()),
icon: IconName::ZedAssistant,
label: SharedString::from(
result
.description
.unwrap_or(format!("Result from {}", prompt_name)),
),
metadata: None,
}],
text: prompt,
run_commands_in_text: false,
}
.to_event_stream())
})
}
_ => Task::ready(Err(anyhow!("Context server not found"))),
}
}
}

View File

@@ -229,19 +229,20 @@ fn collect_diagnostics(
options: Options,
cx: &mut App,
) -> Task<Result<Option<SlashCommandOutput>>> {
let error_source = if let Some(path_matcher) = &options.path_matcher {
debug_assert_eq!(path_matcher.sources().len(), 1);
Some(path_matcher.sources().first().cloned().unwrap_or_default())
} else {
None
let error_source = match &options.path_matcher {
Some(path_matcher) => {
debug_assert_eq!(path_matcher.sources().len(), 1);
Some(path_matcher.sources().first().cloned().unwrap_or_default())
}
_ => None,
};
let glob_is_exact_file_match = if let Some(path) = options
let glob_is_exact_file_match = match options
.path_matcher
.as_ref()
.and_then(|pm| pm.sources().first())
{
PathBuf::try_from(path)
Some(path) => PathBuf::try_from(path)
.ok()
.and_then(|path| {
project.read(cx).worktrees(cx).find_map(|worktree| {
@@ -251,9 +252,8 @@ fn collect_diagnostics(
worktree.absolutize(&relative_path).ok()
})
})
.is_some()
} else {
false
.is_some(),
_ => false,
};
let project_handle = project.downgrade();

View File

@@ -221,7 +221,7 @@ fn collect_files(
project: Entity<Project>,
glob_inputs: &[String],
cx: &mut App,
) -> impl Stream<Item = Result<SlashCommandEvent>> {
) -> impl Stream<Item = Result<SlashCommandEvent>> + use<> {
let Ok(matchers) = glob_inputs
.into_iter()
.map(|glob_input| {
@@ -285,22 +285,25 @@ fn collect_files(
if entry.is_dir() {
// Auto-fold directories that contain no files
let mut child_entries = snapshot.child_entries(&entry.path);
if let Some(child) = child_entries.next() {
if child_entries.next().is_none() && child.kind.is_dir() {
if is_top_level_directory {
is_top_level_directory = false;
folded_directory_names_stack.push(
path_including_worktree_name.to_string_lossy().to_string(),
);
} else {
folded_directory_names_stack.push(filename.to_string());
match child_entries.next() {
Some(child) => {
if child_entries.next().is_none() && child.kind.is_dir() {
if is_top_level_directory {
is_top_level_directory = false;
folded_directory_names_stack.push(
path_including_worktree_name.to_string_lossy().to_string(),
);
} else {
folded_directory_names_stack.push(filename.to_string());
}
continue;
}
}
_ => {
// Skip empty directories
folded_directory_names_stack.clear();
continue;
}
} else {
// Skip empty directories
folded_directory_names_stack.clear();
continue;
}
let prefix_paths = folded_directory_names_stack.drain(..).as_slice().join("/");
if prefix_paths.is_empty() {

View File

@@ -1,197 +0,0 @@
use std::{
fmt::Write as _,
ops::DerefMut,
sync::{atomic::AtomicBool, Arc},
};
use anyhow::{anyhow, Result};
use assistant_slash_command::{
ArgumentCompletion, SlashCommand, SlashCommandOutput, SlashCommandOutputSection,
SlashCommandResult,
};
use feature_flags::FeatureFlag;
use gpui::{App, Task, WeakEntity};
use language::{Anchor, CodeLabel, LspAdapterDelegate};
use language_model::{LanguageModelRegistry, LanguageModelTool};
use prompt_store::PromptBuilder;
use schemars::JsonSchema;
use semantic_index::SemanticDb;
use serde::Deserialize;
use ui::prelude::*;
use workspace::Workspace;
use super::{create_label_for_command, search_command::add_search_result_section};
pub struct ProjectSlashCommandFeatureFlag;
impl FeatureFlag for ProjectSlashCommandFeatureFlag {
const NAME: &'static str = "project-slash-command";
}
pub struct ProjectSlashCommand {
prompt_builder: Arc<PromptBuilder>,
}
impl ProjectSlashCommand {
pub fn new(prompt_builder: Arc<PromptBuilder>) -> Self {
Self { prompt_builder }
}
}
impl SlashCommand for ProjectSlashCommand {
fn name(&self) -> String {
"project".into()
}
fn label(&self, cx: &App) -> CodeLabel {
create_label_for_command("project", &[], cx)
}
fn description(&self) -> String {
"Generate a semantic search based on context".into()
}
fn icon(&self) -> IconName {
IconName::Folder
}
fn menu_text(&self) -> String {
self.description()
}
fn requires_argument(&self) -> bool {
false
}
fn complete_argument(
self: Arc<Self>,
_arguments: &[String],
_cancel: Arc<AtomicBool>,
_workspace: Option<WeakEntity<Workspace>>,
_window: &mut Window,
_cx: &mut App,
) -> Task<Result<Vec<ArgumentCompletion>>> {
Task::ready(Ok(Vec::new()))
}
fn run(
self: Arc<Self>,
_arguments: &[String],
_context_slash_command_output_sections: &[SlashCommandOutputSection<Anchor>],
context_buffer: language::BufferSnapshot,
workspace: WeakEntity<Workspace>,
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
window: &mut Window,
cx: &mut App,
) -> Task<SlashCommandResult> {
let model_registry = LanguageModelRegistry::read_global(cx);
let current_model = model_registry.active_model();
let prompt_builder = self.prompt_builder.clone();
let Some(workspace) = workspace.upgrade() else {
return Task::ready(Err(anyhow::anyhow!("workspace was dropped")));
};
let project = workspace.read(cx).project().clone();
let fs = project.read(cx).fs().clone();
let Some(project_index) =
cx.update_global(|index: &mut SemanticDb, cx| index.project_index(project, cx))
else {
return Task::ready(Err(anyhow::anyhow!("no project indexer")));
};
window.spawn(cx, async move |cx| {
let current_model = current_model.ok_or_else(|| anyhow!("no model selected"))?;
let prompt =
prompt_builder.generate_project_slash_command_prompt(context_buffer.text())?;
let search_queries = current_model
.use_tool::<SearchQueries>(
language_model::LanguageModelRequest {
messages: vec![language_model::LanguageModelRequestMessage {
role: language_model::Role::User,
content: vec![language_model::MessageContent::Text(prompt)],
cache: false,
}],
tools: vec![],
stop: vec![],
temperature: None,
},
cx.deref_mut(),
)
.await?
.search_queries;
let results = project_index
.read_with(cx, |project_index, cx| {
project_index.search(search_queries.clone(), 25, cx)
})?
.await?;
let results = SemanticDb::load_results(results, &fs, &cx).await?;
cx.background_spawn(async move {
let mut output = "Project context:\n".to_string();
let mut sections = Vec::new();
for (ix, query) in search_queries.into_iter().enumerate() {
let start_ix = output.len();
writeln!(&mut output, "Results for {query}:").unwrap();
let mut has_results = false;
for result in &results {
if result.query_index == ix {
add_search_result_section(result, &mut output, &mut sections);
has_results = true;
}
}
if has_results {
sections.push(SlashCommandOutputSection {
range: start_ix..output.len(),
icon: IconName::MagnifyingGlass,
label: query.into(),
metadata: None,
});
output.push('\n');
} else {
output.truncate(start_ix);
}
}
sections.push(SlashCommandOutputSection {
range: 0..output.len(),
icon: IconName::Book,
label: "Project context".into(),
metadata: None,
});
Ok(SlashCommandOutput {
text: output,
sections,
run_commands_in_text: true,
}
.to_event_stream())
})
.await
})
}
}
#[derive(JsonSchema, Deserialize)]
struct SearchQueries {
/// An array of semantic search queries.
///
/// These queries will be used to search the user's codebase.
/// The function can only accept 4 queries, otherwise it will error.
/// As such, it's important that you limit the length of the search_queries array to 5 queries or less.
search_queries: Vec<String>,
}
impl LanguageModelTool for SearchQueries {
fn name() -> String {
"search_queries".to_string()
}
fn description() -> String {
"Generate semantic search queries based on context".to_string()
}
}

View File

@@ -1,24 +0,0 @@
Actions have a cost, so only include actions that you think
will be helpful to you in doing a great job answering the
prompt in the future.
You must respond ONLY with a list of actions you would like to
perform. Each action should be on its own line, and followed by a space and then its parameter.
Actions can be performed more than once with different parameters.
Here is an example valid response:
```
file path/to/my/file.txt
file path/to/another/file.txt
search something to search for
search something else to search for
```
Once again, do not forget: you must respond ONLY in the format of
one action per line, and the action name should be followed by
its parameter. Your response must not include anything other
than a list of actions, with one action per line, in this format.
It is extremely important that you do not deviate from this format even slightly!
This is the end of my instructions for how to respond. The rest is the prompt:

View File

@@ -1,31 +0,0 @@
I'm going to give you a prompt. I don't want you to respond
to the prompt itself. I want you to figure out which of the following
actions on my project, if any, would help you answer the prompt.
Here are the actions:
## file
This action's parameter is a file path to one of the files
in the project. If you ask for this action, I will tell you
the full contents of the file, so you can learn all the
details of the file.
## search
This action's parameter is a string to do a semantic search for
across the files in the project. (You will have a JSON summary
of all the files in the project.) It will tell you which files this string
(or similar strings; it is a semantic search) appear in,
as well as some context of the lines surrounding each result.
It's very important that you only use this action when you think
that searching across the specific files in this project for the query
in question will be useful. For example, don't use this command to search
for queries you might put into a general Web search engine, because those
will be too general to give useful results in this project-specific search.
---
That was the end of the list of actions.
Here is a JSON summary of each of the files in my project:

View File

@@ -1,181 +0,0 @@
use anyhow::Result;
use assistant_slash_command::{
ArgumentCompletion, SlashCommand, SlashCommandOutput, SlashCommandOutputSection,
SlashCommandResult,
};
use feature_flags::FeatureFlag;
use gpui::{App, Task, WeakEntity};
use language::{CodeLabel, LspAdapterDelegate};
use semantic_index::{LoadedSearchResult, SemanticDb};
use std::{
fmt::Write,
sync::{atomic::AtomicBool, Arc},
};
use ui::{prelude::*, IconName};
use workspace::Workspace;
use crate::create_label_for_command;
use crate::file_command::{build_entry_output_section, codeblock_fence_for_path};
pub struct SearchSlashCommandFeatureFlag;
impl FeatureFlag for SearchSlashCommandFeatureFlag {
const NAME: &'static str = "search-slash-command";
fn enabled_for_staff() -> bool {
false
}
}
pub struct SearchSlashCommand;
impl SlashCommand for SearchSlashCommand {
fn name(&self) -> String {
"search".into()
}
fn label(&self, cx: &App) -> CodeLabel {
create_label_for_command("search", &["--n"], cx)
}
fn description(&self) -> String {
"Search your project semantically".into()
}
fn icon(&self) -> IconName {
IconName::SearchCode
}
fn menu_text(&self) -> String {
self.description()
}
fn requires_argument(&self) -> bool {
true
}
fn complete_argument(
self: Arc<Self>,
_arguments: &[String],
_cancel: Arc<AtomicBool>,
_workspace: Option<WeakEntity<Workspace>>,
_window: &mut Window,
_cx: &mut App,
) -> Task<Result<Vec<ArgumentCompletion>>> {
Task::ready(Ok(Vec::new()))
}
fn run(
self: Arc<Self>,
arguments: &[String],
_context_slash_command_output_sections: &[SlashCommandOutputSection<language::Anchor>],
_context_buffer: language::BufferSnapshot,
workspace: WeakEntity<Workspace>,
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
window: &mut Window,
cx: &mut App,
) -> Task<SlashCommandResult> {
let Some(workspace) = workspace.upgrade() else {
return Task::ready(Err(anyhow::anyhow!("workspace was dropped")));
};
if arguments.is_empty() {
return Task::ready(Err(anyhow::anyhow!("missing search query")));
};
let mut limit = None;
let mut query = String::new();
for part in arguments {
if let Some(parameter) = part.strip_prefix("--") {
if let Ok(count) = parameter.parse::<usize>() {
limit = Some(count);
continue;
}
}
query.push_str(part);
query.push(' ');
}
query.pop();
if query.is_empty() {
return Task::ready(Err(anyhow::anyhow!("missing search query")));
}
let project = workspace.read(cx).project().clone();
let fs = project.read(cx).fs().clone();
let Some(project_index) =
cx.update_global(|index: &mut SemanticDb, cx| index.project_index(project, cx))
else {
return Task::ready(Err(anyhow::anyhow!("no project indexer")));
};
window.spawn(cx, async move |cx| {
let results = project_index
.read_with(cx, |project_index, cx| {
project_index.search(vec![query.clone()], limit.unwrap_or(5), cx)
})?
.await?;
let loaded_results = SemanticDb::load_results(results, &fs, &cx).await?;
let output = cx
.background_spawn(async move {
let mut text = format!("Search results for {query}:\n");
let mut sections = Vec::new();
for loaded_result in &loaded_results {
add_search_result_section(loaded_result, &mut text, &mut sections);
}
let query = SharedString::from(query);
sections.push(SlashCommandOutputSection {
range: 0..text.len(),
icon: IconName::MagnifyingGlass,
label: query,
metadata: None,
});
SlashCommandOutput {
text,
sections,
run_commands_in_text: false,
}
.to_event_stream()
})
.await;
Ok(output)
})
}
}
pub fn add_search_result_section(
loaded_result: &LoadedSearchResult,
text: &mut String,
sections: &mut Vec<SlashCommandOutputSection<usize>>,
) {
let LoadedSearchResult {
path,
full_path,
excerpt_content,
row_range,
..
} = loaded_result;
let section_start_ix = text.len();
text.push_str(&codeblock_fence_for_path(
Some(&path),
Some(row_range.clone()),
));
text.push_str(&excerpt_content);
if !text.ends_with('\n') {
text.push('\n');
}
writeln!(text, "```\n").unwrap();
let section_end_ix = text.len() - 1;
sections.push(build_entry_output_section(
section_start_ix..section_end_ix,
Some(&full_path),
false,
Some(row_range.start() + 1..row_range.end() + 1),
));
}

View File

@@ -13,6 +13,8 @@ path = "src/assistant_tool.rs"
[dependencies]
anyhow.workspace = true
async-watch.workspace = true
buffer_diff.workspace = true
clock.workspace = true
collections.workspace = true
derive_more.workspace = true
@@ -24,3 +26,16 @@ parking_lot.workspace = true
project.workspace = true
serde.workspace = true
serde_json.workspace = true
text.workspace = true
[dev-dependencies]
buffer_diff = { workspace = true, features = ["test-support"] }
collections = { workspace = true, features = ["test-support"] }
clock = { workspace = true, features = ["test-support"] }
gpui = { workspace = true, features = ["test-support"] }
language = { workspace = true, features = ["test-support"] }
language_model = { workspace = true, features = ["test-support"] }
project = { workspace = true, features = ["test-support"] }
settings = { workspace = true, features = ["test-support"] }
text = { workspace = true, features = ["test-support"] }
util = { workspace = true, features = ["test-support"] }

View File

@@ -0,0 +1,990 @@
use anyhow::{Context as _, Result};
use buffer_diff::BufferDiff;
use collections::{BTreeMap, HashMap, HashSet};
use gpui::{App, AppContext, AsyncApp, Context, Entity, Subscription, Task, WeakEntity};
use language::{
Buffer, BufferEvent, DiskState, OffsetRangeExt, Operation, TextBufferSnapshot, ToOffset,
};
use std::{ops::Range, sync::Arc};
/// Tracks actions performed by tools in a thread
pub struct ActionLog {
/// Buffers that user manually added to the context, and whose content has
/// changed since the model last saw them.
stale_buffers_in_context: HashSet<Entity<Buffer>>,
/// Buffers that we want to notify the model about when they change.
tracked_buffers: BTreeMap<Entity<Buffer>, TrackedBuffer>,
/// Has the model edited a file since it last checked diagnostics?
edited_since_project_diagnostics_check: bool,
}
impl ActionLog {
/// Creates a new, empty action log.
pub fn new() -> Self {
Self {
stale_buffers_in_context: HashSet::default(),
tracked_buffers: BTreeMap::default(),
edited_since_project_diagnostics_check: false,
}
}
pub fn clear_reviewed_changes(&mut self, cx: &mut Context<Self>) {
self.tracked_buffers
.retain(|_buffer, tracked_buffer| match &mut tracked_buffer.change {
Change::Edited {
accepted_edit_ids, ..
} => {
accepted_edit_ids.clear();
tracked_buffer.schedule_diff_update();
true
}
Change::Deleted { reviewed, .. } => !*reviewed,
});
cx.notify();
}
/// Notifies a diagnostics check
pub fn checked_project_diagnostics(&mut self) {
self.edited_since_project_diagnostics_check = false;
}
/// Returns true if any files have been edited since the last project diagnostics check
pub fn has_edited_files_since_project_diagnostics_check(&self) -> bool {
self.edited_since_project_diagnostics_check
}
fn track_buffer(
&mut self,
buffer: Entity<Buffer>,
created: bool,
cx: &mut Context<Self>,
) -> &mut TrackedBuffer {
let tracked_buffer = self
.tracked_buffers
.entry(buffer.clone())
.or_insert_with(|| {
let text_snapshot = buffer.read(cx).text_snapshot();
let unreviewed_diff = cx.new(|cx| BufferDiff::new(&text_snapshot, cx));
let diff = cx.new(|cx| {
let mut diff = BufferDiff::new(&text_snapshot, cx);
diff.set_secondary_diff(unreviewed_diff.clone());
diff
});
let (diff_update_tx, diff_update_rx) = async_watch::channel(());
TrackedBuffer {
buffer: buffer.clone(),
change: Change::Edited {
unreviewed_edit_ids: HashSet::default(),
accepted_edit_ids: HashSet::default(),
initial_content: if created {
None
} else {
Some(text_snapshot.clone())
},
},
version: buffer.read(cx).version(),
diff,
secondary_diff: unreviewed_diff,
diff_update: diff_update_tx,
_maintain_diff: cx.spawn({
let buffer = buffer.clone();
async move |this, cx| {
Self::maintain_diff(this, buffer, diff_update_rx, cx)
.await
.ok();
}
}),
_subscription: cx.subscribe(&buffer, Self::handle_buffer_event),
}
});
tracked_buffer.version = buffer.read(cx).version();
tracked_buffer
}
fn handle_buffer_event(
&mut self,
buffer: Entity<Buffer>,
event: &BufferEvent,
cx: &mut Context<Self>,
) {
match event {
BufferEvent::Operation { operation, .. } => {
self.handle_buffer_operation(buffer, operation, cx)
}
BufferEvent::FileHandleChanged => {
self.handle_buffer_file_changed(buffer, cx);
}
_ => {}
};
}
fn handle_buffer_operation(
&mut self,
buffer: Entity<Buffer>,
operation: &Operation,
cx: &mut Context<Self>,
) {
let Some(tracked_buffer) = self.tracked_buffers.get_mut(&buffer) else {
return;
};
let Operation::Buffer(text::Operation::Edit(operation)) = operation else {
return;
};
let Change::Edited {
unreviewed_edit_ids,
accepted_edit_ids,
..
} = &mut tracked_buffer.change
else {
return;
};
if unreviewed_edit_ids.contains(&operation.timestamp)
|| accepted_edit_ids.contains(&operation.timestamp)
{
return;
}
let buffer = buffer.read(cx);
let operation_edit_ranges = buffer
.edited_ranges_for_edit_ids::<usize>([&operation.timestamp])
.collect::<Vec<_>>();
let intersects_unreviewed_edits = ranges_intersect(
operation_edit_ranges.iter().cloned(),
buffer.edited_ranges_for_edit_ids::<usize>(unreviewed_edit_ids.iter()),
);
let mut intersected_accepted_edits = HashSet::default();
for accepted_edit_id in accepted_edit_ids.iter() {
let intersects_accepted_edit = ranges_intersect(
operation_edit_ranges.iter().cloned(),
buffer.edited_ranges_for_edit_ids::<usize>([accepted_edit_id]),
);
if intersects_accepted_edit {
intersected_accepted_edits.insert(*accepted_edit_id);
}
}
// If the buffer operation overlaps with any tracked edits, mark it as unreviewed.
// If it intersects an already-accepted id, mark that edit as unreviewed again.
if intersects_unreviewed_edits || !intersected_accepted_edits.is_empty() {
unreviewed_edit_ids.insert(operation.timestamp);
for accepted_edit_id in intersected_accepted_edits {
unreviewed_edit_ids.insert(accepted_edit_id);
accepted_edit_ids.remove(&accepted_edit_id);
}
tracked_buffer.schedule_diff_update();
}
}
fn handle_buffer_file_changed(&mut self, buffer: Entity<Buffer>, cx: &mut Context<Self>) {
let Some(tracked_buffer) = self.tracked_buffers.get_mut(&buffer) else {
return;
};
match tracked_buffer.change {
Change::Deleted { .. } => {
if buffer
.read(cx)
.file()
.map_or(false, |file| file.disk_state() != DiskState::Deleted)
{
// If the buffer had been deleted by a tool, but it got
// resurrected externally, we want to clear the changes we
// were tracking and reset the buffer's state.
tracked_buffer.change = Change::Edited {
unreviewed_edit_ids: HashSet::default(),
accepted_edit_ids: HashSet::default(),
initial_content: Some(buffer.read(cx).text_snapshot()),
};
}
tracked_buffer.schedule_diff_update();
}
Change::Edited { .. } => {
if buffer
.read(cx)
.file()
.map_or(false, |file| file.disk_state() == DiskState::Deleted)
{
// If the buffer had been edited by a tool, but it got
// deleted externally, we want to stop tracking it.
self.tracked_buffers.remove(&buffer);
} else {
tracked_buffer.schedule_diff_update();
}
}
}
}
async fn maintain_diff(
this: WeakEntity<Self>,
buffer: Entity<Buffer>,
mut diff_update: async_watch::Receiver<()>,
cx: &mut AsyncApp,
) -> Result<()> {
while let Some(_) = diff_update.recv().await.ok() {
let update = this.update(cx, |this, cx| {
let tracked_buffer = this
.tracked_buffers
.get_mut(&buffer)
.context("buffer not tracked")?;
anyhow::Ok(tracked_buffer.update_diff(cx))
})??;
update.await;
this.update(cx, |_this, cx| cx.notify())?;
}
Ok(())
}
/// Track a buffer as read, so we can notify the model about user edits.
pub fn buffer_read(&mut self, buffer: Entity<Buffer>, cx: &mut Context<Self>) {
self.track_buffer(buffer, false, cx);
}
/// Track a buffer as read, so we can notify the model about user edits.
pub fn will_create_buffer(
&mut self,
buffer: Entity<Buffer>,
edit_id: Option<clock::Lamport>,
cx: &mut Context<Self>,
) {
self.track_buffer(buffer.clone(), true, cx);
self.buffer_edited(buffer, edit_id.into_iter().collect(), cx)
}
/// Mark a buffer as edited, so we can refresh it in the context
pub fn buffer_edited(
&mut self,
buffer: Entity<Buffer>,
mut edit_ids: Vec<clock::Lamport>,
cx: &mut Context<Self>,
) {
self.edited_since_project_diagnostics_check = true;
self.stale_buffers_in_context.insert(buffer.clone());
let tracked_buffer = self.track_buffer(buffer.clone(), false, cx);
match &mut tracked_buffer.change {
Change::Edited {
unreviewed_edit_ids,
..
} => {
unreviewed_edit_ids.extend(edit_ids.iter().copied());
}
Change::Deleted {
deleted_content,
deletion_id,
..
} => {
edit_ids.extend(*deletion_id);
tracked_buffer.change = Change::Edited {
unreviewed_edit_ids: edit_ids.into_iter().collect(),
accepted_edit_ids: HashSet::default(),
initial_content: Some(deleted_content.clone()),
};
}
}
tracked_buffer.schedule_diff_update();
}
pub fn will_delete_buffer(&mut self, buffer: Entity<Buffer>, cx: &mut Context<Self>) {
let tracked_buffer = self.track_buffer(buffer.clone(), false, cx);
if let Change::Edited {
initial_content, ..
} = &tracked_buffer.change
{
if let Some(initial_content) = initial_content {
let deletion_id = buffer.update(cx, |buffer, cx| buffer.set_text("", cx));
tracked_buffer.change = Change::Deleted {
reviewed: false,
deleted_content: initial_content.clone(),
deletion_id,
};
tracked_buffer.schedule_diff_update();
} else {
self.tracked_buffers.remove(&buffer);
cx.notify();
}
}
}
/// Accepts edits in a given range within a buffer.
pub fn review_edits_in_range<T: ToOffset>(
&mut self,
buffer: Entity<Buffer>,
buffer_range: Range<T>,
accept: bool,
cx: &mut Context<Self>,
) {
let Some(tracked_buffer) = self.tracked_buffers.get_mut(&buffer) else {
return;
};
let buffer = buffer.read(cx);
let buffer_range = buffer_range.to_offset(buffer);
match &mut tracked_buffer.change {
Change::Deleted { reviewed, .. } => {
*reviewed = accept;
}
Change::Edited {
unreviewed_edit_ids,
accepted_edit_ids,
..
} => {
let (source, destination) = if accept {
(unreviewed_edit_ids, accepted_edit_ids)
} else {
(accepted_edit_ids, unreviewed_edit_ids)
};
source.retain(|edit_id| {
for range in buffer.edited_ranges_for_edit_ids::<usize>([edit_id]) {
if buffer_range.end >= range.start && buffer_range.start <= range.end {
destination.insert(*edit_id);
return false;
}
}
true
});
}
}
tracked_buffer.schedule_diff_update();
}
/// Keep all edits across all buffers.
/// This is a more performant alternative to calling review_edits_in_range for each buffer.
pub fn keep_all_edits(&mut self) {
// Process all tracked buffers
for (_, tracked_buffer) in self.tracked_buffers.iter_mut() {
match &mut tracked_buffer.change {
Change::Deleted { reviewed, .. } => {
*reviewed = true;
}
Change::Edited {
unreviewed_edit_ids,
accepted_edit_ids,
..
} => {
accepted_edit_ids.extend(unreviewed_edit_ids.drain());
}
}
tracked_buffer.schedule_diff_update();
}
}
/// Returns the set of buffers that contain changes that haven't been reviewed by the user.
pub fn changed_buffers(&self, cx: &App) -> BTreeMap<Entity<Buffer>, ChangedBuffer> {
self.tracked_buffers
.iter()
.filter(|(_, tracked)| tracked.has_changes(cx))
.map(|(buffer, tracked)| {
(
buffer.clone(),
ChangedBuffer {
diff: tracked.diff.clone(),
needs_review: match &tracked.change {
Change::Edited {
unreviewed_edit_ids,
..
} => !unreviewed_edit_ids.is_empty(),
Change::Deleted { reviewed, .. } => !reviewed,
},
},
)
})
.collect()
}
/// Iterate over buffers changed since last read or edited by the model
pub fn stale_buffers<'a>(&'a self, cx: &'a App) -> impl Iterator<Item = &'a Entity<Buffer>> {
self.tracked_buffers
.iter()
.filter(|(buffer, tracked)| tracked.version != buffer.read(cx).version)
.map(|(buffer, _)| buffer)
}
/// Takes and returns the set of buffers pending refresh, clearing internal state.
pub fn take_stale_buffers_in_context(&mut self) -> HashSet<Entity<Buffer>> {
std::mem::take(&mut self.stale_buffers_in_context)
}
}
fn ranges_intersect(
ranges_a: impl IntoIterator<Item = Range<usize>>,
ranges_b: impl IntoIterator<Item = Range<usize>>,
) -> bool {
let mut ranges_a_iter = ranges_a.into_iter().peekable();
let mut ranges_b_iter = ranges_b.into_iter().peekable();
while let (Some(range_a), Some(range_b)) = (ranges_a_iter.peek(), ranges_b_iter.peek()) {
if range_a.end < range_b.start {
ranges_a_iter.next();
} else if range_b.end < range_a.start {
ranges_b_iter.next();
} else {
return true;
}
}
false
}
struct TrackedBuffer {
buffer: Entity<Buffer>,
change: Change,
version: clock::Global,
diff: Entity<BufferDiff>,
secondary_diff: Entity<BufferDiff>,
diff_update: async_watch::Sender<()>,
_maintain_diff: Task<()>,
_subscription: Subscription,
}
enum Change {
Edited {
unreviewed_edit_ids: HashSet<clock::Lamport>,
accepted_edit_ids: HashSet<clock::Lamport>,
initial_content: Option<TextBufferSnapshot>,
},
Deleted {
reviewed: bool,
deleted_content: TextBufferSnapshot,
deletion_id: Option<clock::Lamport>,
},
}
impl TrackedBuffer {
fn has_changes(&self, cx: &App) -> bool {
self.diff
.read(cx)
.hunks(&self.buffer.read(cx), cx)
.next()
.is_some()
}
fn schedule_diff_update(&self) {
self.diff_update.send(()).ok();
}
fn update_diff(&mut self, cx: &mut App) -> Task<()> {
match &self.change {
Change::Edited {
unreviewed_edit_ids,
accepted_edit_ids,
..
} => {
let edits_to_undo = unreviewed_edit_ids
.iter()
.chain(accepted_edit_ids)
.map(|edit_id| (*edit_id, u32::MAX))
.collect::<HashMap<_, _>>();
let buffer_without_edits = self.buffer.update(cx, |buffer, cx| buffer.branch(cx));
buffer_without_edits
.update(cx, |buffer, cx| buffer.undo_operations(edits_to_undo, cx));
let primary_diff_update = self.diff.update(cx, |diff, cx| {
diff.set_base_text(
buffer_without_edits,
self.buffer.read(cx).text_snapshot(),
cx,
)
});
let unreviewed_edits_to_undo = unreviewed_edit_ids
.iter()
.map(|edit_id| (*edit_id, u32::MAX))
.collect::<HashMap<_, _>>();
let buffer_without_unreviewed_edits =
self.buffer.update(cx, |buffer, cx| buffer.branch(cx));
buffer_without_unreviewed_edits.update(cx, |buffer, cx| {
buffer.undo_operations(unreviewed_edits_to_undo, cx)
});
let secondary_diff_update = self.secondary_diff.update(cx, |diff, cx| {
diff.set_base_text(
buffer_without_unreviewed_edits.clone(),
self.buffer.read(cx).text_snapshot(),
cx,
)
});
cx.background_spawn(async move {
_ = primary_diff_update.await;
_ = secondary_diff_update.await;
})
}
Change::Deleted {
reviewed,
deleted_content,
..
} => {
let reviewed = *reviewed;
let deleted_content = deleted_content.clone();
let primary_diff = self.diff.clone();
let secondary_diff = self.secondary_diff.clone();
let buffer_snapshot = self.buffer.read(cx).text_snapshot();
let language = self.buffer.read(cx).language().cloned();
let language_registry = self.buffer.read(cx).language_registry().clone();
cx.spawn(async move |cx| {
let base_text = Arc::new(deleted_content.text());
let primary_diff_snapshot = BufferDiff::update_diff(
primary_diff.clone(),
buffer_snapshot.clone(),
Some(base_text.clone()),
true,
false,
language.clone(),
language_registry.clone(),
cx,
)
.await;
let secondary_diff_snapshot = BufferDiff::update_diff(
secondary_diff.clone(),
buffer_snapshot.clone(),
if reviewed {
None
} else {
Some(base_text.clone())
},
true,
false,
language.clone(),
language_registry.clone(),
cx,
)
.await;
if let Ok(primary_diff_snapshot) = primary_diff_snapshot {
primary_diff
.update(cx, |diff, cx| {
diff.set_snapshot(
&buffer_snapshot,
primary_diff_snapshot,
false,
None,
cx,
)
})
.ok();
}
if let Ok(secondary_diff_snapshot) = secondary_diff_snapshot {
secondary_diff
.update(cx, |diff, cx| {
diff.set_snapshot(
&buffer_snapshot,
secondary_diff_snapshot,
false,
None,
cx,
)
})
.ok();
}
})
}
}
}
}
pub struct ChangedBuffer {
pub diff: Entity<BufferDiff>,
pub needs_review: bool,
}
#[cfg(test)]
mod tests {
use super::*;
use buffer_diff::DiffHunkStatusKind;
use gpui::TestAppContext;
use language::Point;
use project::{FakeFs, Fs, Project, RemoveOptions};
use serde_json::json;
use settings::SettingsStore;
use util::path;
#[gpui::test(iterations = 10)]
async fn test_edit_review(cx: &mut TestAppContext) {
let action_log = cx.new(|_| ActionLog::new());
let buffer = cx.new(|cx| Buffer::local("abc\ndef\nghi\njkl\nmno", cx));
let edit1 = buffer.update(cx, |buffer, cx| {
buffer
.edit([(Point::new(1, 1)..Point::new(1, 2), "E")], None, cx)
.unwrap()
});
let edit2 = buffer.update(cx, |buffer, cx| {
buffer
.edit([(Point::new(4, 2)..Point::new(4, 3), "O")], None, cx)
.unwrap()
});
assert_eq!(
buffer.read_with(cx, |buffer, _| buffer.text()),
"abc\ndEf\nghi\njkl\nmnO"
);
action_log.update(cx, |log, cx| {
log.buffer_edited(buffer.clone(), vec![edit1, edit2], cx)
});
cx.run_until_parked();
assert_eq!(
unreviewed_hunks(&action_log, cx),
vec![(
buffer.clone(),
vec![
HunkStatus {
range: Point::new(1, 0)..Point::new(2, 0),
review_status: ReviewStatus::Unreviewed,
diff_status: DiffHunkStatusKind::Modified,
old_text: "def\n".into(),
},
HunkStatus {
range: Point::new(4, 0)..Point::new(4, 3),
review_status: ReviewStatus::Unreviewed,
diff_status: DiffHunkStatusKind::Modified,
old_text: "mno".into(),
}
],
)]
);
action_log.update(cx, |log, cx| {
log.review_edits_in_range(buffer.clone(), Point::new(3, 0)..Point::new(4, 3), true, cx)
});
cx.run_until_parked();
assert_eq!(
unreviewed_hunks(&action_log, cx),
vec![(
buffer.clone(),
vec![
HunkStatus {
range: Point::new(1, 0)..Point::new(2, 0),
review_status: ReviewStatus::Unreviewed,
diff_status: DiffHunkStatusKind::Modified,
old_text: "def\n".into(),
},
HunkStatus {
range: Point::new(4, 0)..Point::new(4, 3),
review_status: ReviewStatus::Reviewed,
diff_status: DiffHunkStatusKind::Modified,
old_text: "mno".into(),
}
],
)]
);
action_log.update(cx, |log, cx| {
log.review_edits_in_range(
buffer.clone(),
Point::new(3, 0)..Point::new(4, 3),
false,
cx,
)
});
cx.run_until_parked();
assert_eq!(
unreviewed_hunks(&action_log, cx),
vec![(
buffer.clone(),
vec![
HunkStatus {
range: Point::new(1, 0)..Point::new(2, 0),
review_status: ReviewStatus::Unreviewed,
diff_status: DiffHunkStatusKind::Modified,
old_text: "def\n".into(),
},
HunkStatus {
range: Point::new(4, 0)..Point::new(4, 3),
review_status: ReviewStatus::Unreviewed,
diff_status: DiffHunkStatusKind::Modified,
old_text: "mno".into(),
}
],
)]
);
action_log.update(cx, |log, cx| {
log.review_edits_in_range(buffer.clone(), Point::new(0, 0)..Point::new(4, 3), true, cx)
});
cx.run_until_parked();
assert_eq!(
unreviewed_hunks(&action_log, cx),
vec![(
buffer.clone(),
vec![
HunkStatus {
range: Point::new(1, 0)..Point::new(2, 0),
review_status: ReviewStatus::Reviewed,
diff_status: DiffHunkStatusKind::Modified,
old_text: "def\n".into(),
},
HunkStatus {
range: Point::new(4, 0)..Point::new(4, 3),
review_status: ReviewStatus::Reviewed,
diff_status: DiffHunkStatusKind::Modified,
old_text: "mno".into(),
}
],
)]
);
}
#[gpui::test(iterations = 10)]
async fn test_overlapping_user_edits(cx: &mut TestAppContext) {
let action_log = cx.new(|_| ActionLog::new());
let buffer = cx.new(|cx| Buffer::local("abc\ndef\nghi\njkl\nmno", cx));
let tool_edit = buffer.update(cx, |buffer, cx| {
buffer
.edit(
[(Point::new(0, 2)..Point::new(2, 3), "C\nDEF\nGHI")],
None,
cx,
)
.unwrap()
});
assert_eq!(
buffer.read_with(cx, |buffer, _| buffer.text()),
"abC\nDEF\nGHI\njkl\nmno"
);
action_log.update(cx, |log, cx| {
log.buffer_edited(buffer.clone(), vec![tool_edit], cx)
});
cx.run_until_parked();
assert_eq!(
unreviewed_hunks(&action_log, cx),
vec![(
buffer.clone(),
vec![HunkStatus {
range: Point::new(0, 0)..Point::new(3, 0),
review_status: ReviewStatus::Unreviewed,
diff_status: DiffHunkStatusKind::Modified,
old_text: "abc\ndef\nghi\n".into(),
}],
)]
);
action_log.update(cx, |log, cx| {
log.review_edits_in_range(buffer.clone(), Point::new(0, 0)..Point::new(1, 0), true, cx)
});
cx.run_until_parked();
assert_eq!(
unreviewed_hunks(&action_log, cx),
vec![(
buffer.clone(),
vec![HunkStatus {
range: Point::new(0, 0)..Point::new(3, 0),
review_status: ReviewStatus::Reviewed,
diff_status: DiffHunkStatusKind::Modified,
old_text: "abc\ndef\nghi\n".into(),
}],
)]
);
buffer.update(cx, |buffer, cx| {
buffer.edit([(Point::new(0, 2)..Point::new(0, 2), "X")], None, cx)
});
cx.run_until_parked();
assert_eq!(
unreviewed_hunks(&action_log, cx),
vec![(
buffer.clone(),
vec![HunkStatus {
range: Point::new(0, 0)..Point::new(3, 0),
review_status: ReviewStatus::Unreviewed,
diff_status: DiffHunkStatusKind::Modified,
old_text: "abc\ndef\nghi\n".into(),
}],
)]
);
action_log.update(cx, |log, cx| log.clear_reviewed_changes(cx));
cx.run_until_parked();
assert_eq!(
unreviewed_hunks(&action_log, cx),
vec![(
buffer.clone(),
vec![HunkStatus {
range: Point::new(0, 0)..Point::new(3, 0),
review_status: ReviewStatus::Unreviewed,
diff_status: DiffHunkStatusKind::Modified,
old_text: "abc\ndef\nghi\n".into(),
}],
)]
);
}
#[gpui::test(iterations = 10)]
async fn test_deletion(cx: &mut TestAppContext) {
cx.update(|cx| {
let settings_store = SettingsStore::test(cx);
cx.set_global(settings_store);
language::init(cx);
Project::init_settings(cx);
});
let fs = FakeFs::new(cx.executor());
fs.insert_tree(
path!("/dir"),
json!({"file1": "lorem\n", "file2": "ipsum\n"}),
)
.await;
let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
let file1_path = project
.read_with(cx, |project, cx| project.find_project_path("dir/file1", cx))
.unwrap();
let file2_path = project
.read_with(cx, |project, cx| project.find_project_path("dir/file2", cx))
.unwrap();
let action_log = cx.new(|_| ActionLog::new());
let buffer1 = project
.update(cx, |project, cx| {
project.open_buffer(file1_path.clone(), cx)
})
.await
.unwrap();
let buffer2 = project
.update(cx, |project, cx| {
project.open_buffer(file2_path.clone(), cx)
})
.await
.unwrap();
action_log.update(cx, |log, cx| log.will_delete_buffer(buffer1.clone(), cx));
action_log.update(cx, |log, cx| log.will_delete_buffer(buffer2.clone(), cx));
project
.update(cx, |project, cx| {
project.delete_file(file1_path.clone(), false, cx)
})
.unwrap()
.await
.unwrap();
project
.update(cx, |project, cx| {
project.delete_file(file2_path.clone(), false, cx)
})
.unwrap()
.await
.unwrap();
cx.run_until_parked();
assert_eq!(
unreviewed_hunks(&action_log, cx),
vec![
(
buffer1.clone(),
vec![HunkStatus {
range: Point::new(0, 0)..Point::new(0, 0),
review_status: ReviewStatus::Unreviewed,
diff_status: DiffHunkStatusKind::Deleted,
old_text: "lorem\n".into(),
}]
),
(
buffer2.clone(),
vec![HunkStatus {
range: Point::new(0, 0)..Point::new(0, 0),
review_status: ReviewStatus::Unreviewed,
diff_status: DiffHunkStatusKind::Deleted,
old_text: "ipsum\n".into(),
}],
)
]
);
// Simulate file1 being recreated externally.
fs.insert_file(path!("/dir/file1"), "LOREM".as_bytes().to_vec())
.await;
let buffer2 = project
.update(cx, |project, cx| project.open_buffer(file2_path, cx))
.await
.unwrap();
cx.run_until_parked();
// Simulate file2 being recreated by a tool.
let edit_id = buffer2.update(cx, |buffer, cx| buffer.set_text("IPSUM", cx));
action_log.update(cx, |log, cx| {
log.will_create_buffer(buffer2.clone(), edit_id, cx)
});
project
.update(cx, |project, cx| project.save_buffer(buffer2.clone(), cx))
.await
.unwrap();
cx.run_until_parked();
assert_eq!(
unreviewed_hunks(&action_log, cx),
vec![(
buffer2.clone(),
vec![HunkStatus {
range: Point::new(0, 0)..Point::new(0, 5),
review_status: ReviewStatus::Unreviewed,
diff_status: DiffHunkStatusKind::Modified,
old_text: "ipsum\n".into(),
}],
)]
);
// Simulate file2 being deleted externally.
fs.remove_file(path!("/dir/file2").as_ref(), RemoveOptions::default())
.await
.unwrap();
cx.run_until_parked();
assert_eq!(unreviewed_hunks(&action_log, cx), vec![]);
}
#[derive(Debug, Clone, PartialEq, Eq)]
struct HunkStatus {
range: Range<Point>,
review_status: ReviewStatus,
diff_status: DiffHunkStatusKind,
old_text: String,
}
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
enum ReviewStatus {
Unreviewed,
Reviewed,
}
fn unreviewed_hunks(
action_log: &Entity<ActionLog>,
cx: &TestAppContext,
) -> Vec<(Entity<Buffer>, Vec<HunkStatus>)> {
cx.read(|cx| {
action_log
.read(cx)
.changed_buffers(cx)
.into_iter()
.map(|(buffer, tracked_buffer)| {
let snapshot = buffer.read(cx).snapshot();
(
buffer,
tracked_buffer
.diff
.read(cx)
.hunks(&snapshot, cx)
.map(|hunk| HunkStatus {
review_status: if hunk.status().has_secondary_hunk() {
ReviewStatus::Unreviewed
} else {
ReviewStatus::Reviewed
},
diff_status: hunk.status().kind,
range: hunk.range,
old_text: tracked_buffer
.diff
.read(cx)
.base_text()
.text_for_range(hunk.diff_base_byte_range)
.collect(),
})
.collect(),
)
})
.collect()
})
}
}

View File

@@ -1,17 +1,19 @@
mod action_log;
mod tool_registry;
mod tool_working_set;
use std::fmt::{self, Debug, Formatter};
use std::fmt;
use std::fmt::Debug;
use std::fmt::Formatter;
use std::sync::Arc;
use anyhow::Result;
use collections::{HashMap, HashSet};
use gpui::{App, Context, Entity, SharedString, Task};
use gpui::{App, Entity, SharedString, Task};
use icons::IconName;
use language::Buffer;
use language_model::LanguageModelRequestMessage;
use project::Project;
pub use crate::action_log::*;
pub use crate::tool_registry::*;
pub use crate::tool_working_set::*;
@@ -71,71 +73,3 @@ impl Debug for dyn Tool {
f.debug_struct("Tool").field("name", &self.name()).finish()
}
}
/// Tracks actions performed by tools in a thread
#[derive(Debug)]
pub struct ActionLog {
/// Buffers that user manually added to the context, and whose content has
/// changed since the model last saw them.
stale_buffers_in_context: HashSet<Entity<Buffer>>,
/// Buffers that we want to notify the model about when they change.
tracked_buffers: HashMap<Entity<Buffer>, TrackedBuffer>,
/// Has the model edited a file since it last checked diagnostics?
edited_since_project_diagnostics_check: bool,
}
#[derive(Debug, Default)]
struct TrackedBuffer {
version: clock::Global,
}
impl ActionLog {
/// Creates a new, empty action log.
pub fn new() -> Self {
Self {
stale_buffers_in_context: HashSet::default(),
tracked_buffers: HashMap::default(),
edited_since_project_diagnostics_check: false,
}
}
/// Track a buffer as read, so we can notify the model about user edits.
pub fn buffer_read(&mut self, buffer: Entity<Buffer>, cx: &mut Context<Self>) {
let tracked_buffer = self.tracked_buffers.entry(buffer.clone()).or_default();
tracked_buffer.version = buffer.read(cx).version();
}
/// Mark a buffer as edited, so we can refresh it in the context
pub fn buffer_edited(&mut self, buffers: HashSet<Entity<Buffer>>, cx: &mut Context<Self>) {
for buffer in &buffers {
let tracked_buffer = self.tracked_buffers.entry(buffer.clone()).or_default();
tracked_buffer.version = buffer.read(cx).version();
}
self.stale_buffers_in_context.extend(buffers);
self.edited_since_project_diagnostics_check = true;
}
/// Notifies a diagnostics check
pub fn checked_project_diagnostics(&mut self) {
self.edited_since_project_diagnostics_check = false;
}
/// Iterate over buffers changed since last read or edited by the model
pub fn stale_buffers<'a>(&'a self, cx: &'a App) -> impl Iterator<Item = &'a Entity<Buffer>> {
self.tracked_buffers
.iter()
.filter(|(buffer, tracked)| tracked.version != buffer.read(cx).version)
.map(|(buffer, _)| buffer)
}
/// Returns true if any files have been edited since the last project diagnostics check
pub fn has_edited_files_since_project_diagnostics_check(&self) -> bool {
self.edited_since_project_diagnostics_check
}
/// Takes and returns the set of buffers pending refresh, clearing internal state.
pub fn take_stale_buffers_in_context(&mut self) -> HashSet<Entity<Buffer>> {
std::mem::take(&mut self.stale_buffers_in_context)
}
}

View File

@@ -14,6 +14,7 @@ path = "src/assistant_tools.rs"
[dependencies]
anyhow.workspace = true
assistant_tool.workspace = true
clock.workspace = true
chrono.workspace = true
collections.workspace = true
feature_flags.workspace = true
@@ -24,7 +25,10 @@ http_client.workspace = true
itertools.workspace = true
language.workspace = true
language_model.workspace = true
log.workspace = true
lsp.workspace = true
project.workspace = true
regex.workspace = true
release_channel.workspace = true
schemars.workspace = true
serde.workspace = true
@@ -35,8 +39,10 @@ ui.workspace = true
util.workspace = true
workspace.workspace = true
worktree.workspace = true
open = { workspace = true }
[dev-dependencies]
clock = { workspace = true, features = ["test-support"] }
collections = { workspace = true, features = ["test-support"] }
gpui = { workspace = true, features = ["test-support"] }
language = { workspace = true, features = ["test-support"] }

View File

@@ -1,4 +1,7 @@
mod bash_tool;
mod batch_tool;
mod code_symbol_iter;
mod code_symbols_tool;
mod copy_path_tool;
mod create_directory_tool;
mod create_file_tool;
@@ -10,10 +13,12 @@ mod find_replace_file_tool;
mod list_directory_tool;
mod move_path_tool;
mod now_tool;
mod open_tool;
mod path_search_tool;
mod read_file_tool;
mod regex_search_tool;
mod replace;
mod symbol_info_tool;
mod thinking_tool;
use std::sync::Arc;
@@ -25,6 +30,8 @@ use http_client::HttpClientWithUrl;
use move_path_tool::MovePathTool;
use crate::bash_tool::BashTool;
use crate::batch_tool::BatchTool;
use crate::code_symbols_tool::CodeSymbolsTool;
use crate::create_directory_tool::CreateDirectoryTool;
use crate::create_file_tool::CreateFileTool;
use crate::delete_path_tool::DeletePathTool;
@@ -34,9 +41,11 @@ use crate::fetch_tool::FetchTool;
use crate::find_replace_file_tool::FindReplaceFileTool;
use crate::list_directory_tool::ListDirectoryTool;
use crate::now_tool::NowTool;
use crate::open_tool::OpenTool;
use crate::path_search_tool::PathSearchTool;
use crate::read_file_tool::ReadFileTool;
use crate::regex_search_tool::RegexSearchTool;
use crate::symbol_info_tool::SymbolInfoTool;
use crate::thinking_tool::ThinkingTool;
pub fn init(http_client: Arc<HttpClientWithUrl>, cx: &mut App) {
@@ -45,16 +54,20 @@ pub fn init(http_client: Arc<HttpClientWithUrl>, cx: &mut App) {
let registry = ToolRegistry::global(cx);
registry.register_tool(BashTool);
registry.register_tool(BatchTool);
registry.register_tool(CreateDirectoryTool);
registry.register_tool(CreateFileTool);
registry.register_tool(CopyPathTool);
registry.register_tool(DeletePathTool);
registry.register_tool(FindReplaceFileTool);
registry.register_tool(SymbolInfoTool);
registry.register_tool(MovePathTool);
registry.register_tool(DiagnosticsTool);
registry.register_tool(EditFilesTool);
registry.register_tool(ListDirectoryTool);
registry.register_tool(NowTool);
registry.register_tool(OpenTool);
registry.register_tool(CodeSymbolsTool);
registry.register_tool(PathSearchTool);
registry.register_tool(ReadFileTool);
registry.register_tool(RegexSearchTool);

View File

@@ -5,6 +5,7 @@ use language_model::LanguageModelRequestMessage;
use project::Project;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::path::Path;
use std::sync::Arc;
use ui::IconName;
use util::command::new_smol_command;
@@ -68,10 +69,44 @@ impl Tool for BashTool {
Err(err) => return Task::ready(Err(anyhow!(err))),
};
let Some(worktree) = project.read(cx).worktree_for_root_name(&input.cd, cx) else {
return Task::ready(Err(anyhow!("Working directory not found in the project")));
let project = project.read(cx);
let input_path = Path::new(&input.cd);
let working_dir = if input.cd == "." {
// Accept "." as meaning "the one worktree" if we only have one worktree.
let mut worktrees = project.worktrees(cx);
let only_worktree = match worktrees.next() {
Some(worktree) => worktree,
None => return Task::ready(Err(anyhow!("No worktrees found in the project"))),
};
if worktrees.next().is_some() {
return Task::ready(Err(anyhow!("'.' is ambiguous in multi-root workspaces. Please specify a root directory explicitly.")));
}
only_worktree.read(cx).abs_path()
} else if input_path.is_absolute() {
// Absolute paths are allowed, but only if they're in one of the project's worktrees.
if !project
.worktrees(cx)
.any(|worktree| input_path.starts_with(&worktree.read(cx).abs_path()))
{
return Task::ready(Err(anyhow!(
"The absolute path must be within one of the project's worktrees"
)));
}
input_path.into()
} else {
let Some(worktree) = project.worktree_for_root_name(&input.cd, cx) else {
return Task::ready(Err(anyhow!(
"`cd` directory {} not found in the project",
&input.cd
)));
};
worktree.read(cx).abs_path()
};
let working_directory = worktree.read(cx).abs_path();
cx.spawn(async move |_| {
// Add 2>&1 to merge stderr into stdout for proper interleaving.
@@ -80,7 +115,7 @@ impl Tool for BashTool {
let output = new_smol_command("bash")
.arg("-c")
.arg(&command)
.current_dir(working_directory)
.current_dir(working_dir)
.output()
.await
.context("Failed to execute bash command")?;

View File

@@ -0,0 +1,301 @@
use anyhow::{anyhow, Result};
use assistant_tool::{ActionLog, Tool, ToolWorkingSet};
use futures::future::join_all;
use gpui::{App, AppContext, Entity, Task};
use language_model::LanguageModelRequestMessage;
use project::Project;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::sync::Arc;
use ui::IconName;
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct ToolInvocation {
/// The name of the tool to invoke
pub name: String,
/// The input to the tool in JSON format
pub input: serde_json::Value,
}
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct BatchToolInput {
/// The tool invocations to run as a batch. These tools will be run either sequentially
/// or concurrently depending on the `run_tools_concurrently` flag.
///
/// <example>
/// Basic file operations (concurrent)
///
/// ```json
/// {
/// "invocations": [
/// {
/// "name": "read-file",
/// "input": {
/// "path": "src/main.rs"
/// }
/// },
/// {
/// "name": "list-directory",
/// "input": {
/// "path": "src/lib"
/// }
/// },
/// {
/// "name": "regex-search",
/// "input": {
/// "regex": "fn run\\("
/// }
/// }
/// ],
/// "run_tools_concurrently": true
/// }
/// ```
/// </example>
///
/// <example>
/// Multiple find-replace operations on the same file (sequential)
///
/// ```json
/// {
/// "invocations": [
/// {
/// "name": "find-replace-file",
/// "input": {
/// "path": "src/config.rs",
/// "display_description": "Update default timeout value",
/// "find": "pub const DEFAULT_TIMEOUT: u64 = 30;\n\npub const MAX_RETRIES: u32 = 3;\n\npub const SERVER_URL: &str = \"https://api.example.com\";",
/// "replace": "pub const DEFAULT_TIMEOUT: u64 = 60;\n\npub const MAX_RETRIES: u32 = 3;\n\npub const SERVER_URL: &str = \"https://api.example.com\";"
/// }
/// },
/// {
/// "name": "find-replace-file",
/// "input": {
/// "path": "src/config.rs",
/// "display_description": "Update API endpoint URL",
/// "find": "pub const MAX_RETRIES: u32 = 3;\n\npub const SERVER_URL: &str = \"https://api.example.com\";\n\npub const API_VERSION: &str = \"v1\";",
/// "replace": "pub const MAX_RETRIES: u32 = 3;\n\npub const SERVER_URL: &str = \"https://api.newdomain.com\";\n\npub const API_VERSION: &str = \"v1\";"
/// }
/// }
/// ],
/// "run_tools_concurrently": false
/// }
/// ```
/// </example>
///
/// <example>
/// Searching and analyzing code (concurrent)
///
/// ```json
/// {
/// "invocations": [
/// {
/// "name": "regex-search",
/// "input": {
/// "regex": "impl Database"
/// }
/// },
/// {
/// "name": "path-search",
/// "input": {
/// "glob": "**/*test*.rs"
/// }
/// }
/// ],
/// "run_tools_concurrently": true
/// }
/// ```
/// </example>
///
/// <example>
/// Multi-file refactoring (concurrent)
///
/// ```json
/// {
/// "invocations": [
/// {
/// "name": "find-replace-file",
/// "input": {
/// "path": "src/models/user.rs",
/// "display_description": "Add email field to User struct",
/// "find": "pub struct User {\n pub id: u64,\n pub username: String,\n pub created_at: DateTime<Utc>,\n}",
/// "replace": "pub struct User {\n pub id: u64,\n pub username: String,\n pub email: String,\n pub created_at: DateTime<Utc>,\n}"
/// }
/// },
/// {
/// "name": "find-replace-file",
/// "input": {
/// "path": "src/db/queries.rs",
/// "display_description": "Update user insertion query",
/// "find": "pub async fn insert_user(conn: &mut Connection, user: &User) -> Result<(), DbError> {\n conn.execute(\n \"INSERT INTO users (id, username, created_at) VALUES ($1, $2, $3)\",\n &[&user.id, &user.username, &user.created_at],\n ).await?;\n \n Ok(())\n}",
/// "replace": "pub async fn insert_user(conn: &mut Connection, user: &User) -> Result<(), DbError> {\n conn.execute(\n \"INSERT INTO users (id, username, email, created_at) VALUES ($1, $2, $3, $4)\",\n &[&user.id, &user.username, &user.email, &user.created_at],\n ).await?;\n \n Ok(())\n}"
/// }
/// }
/// ],
/// "run_tools_concurrently": true
/// }
/// ```
/// </example>
pub invocations: Vec<ToolInvocation>,
/// Whether to run the tools in this batch concurrently. If this is false (the default), the tools will run sequentially.
#[serde(default)]
pub run_tools_concurrently: bool,
}
pub struct BatchTool;
impl Tool for BatchTool {
fn name(&self) -> String {
"batch-tool".into()
}
fn needs_confirmation(&self) -> bool {
true
}
fn description(&self) -> String {
include_str!("./batch_tool/description.md").into()
}
fn icon(&self) -> IconName {
IconName::Cog
}
fn input_schema(&self) -> serde_json::Value {
let schema = schemars::schema_for!(BatchToolInput);
serde_json::to_value(&schema).unwrap()
}
fn ui_text(&self, input: &serde_json::Value) -> String {
match serde_json::from_value::<BatchToolInput>(input.clone()) {
Ok(input) => {
let count = input.invocations.len();
let mode = if input.run_tools_concurrently {
"concurrently"
} else {
"sequentially"
};
let first_tool_name = input
.invocations
.first()
.map(|inv| inv.name.clone())
.unwrap_or_default();
let all_same = input
.invocations
.iter()
.all(|invocation| invocation.name == first_tool_name);
if all_same {
format!(
"Run `{}` {} times {}",
first_tool_name,
input.invocations.len(),
mode
)
} else {
format!("Run {} tools {}", count, mode)
}
}
Err(_) => "Batch tools".to_string(),
}
}
fn run(
self: Arc<Self>,
input: serde_json::Value,
messages: &[LanguageModelRequestMessage],
project: Entity<Project>,
action_log: Entity<ActionLog>,
cx: &mut App,
) -> Task<Result<String>> {
let input = match serde_json::from_value::<BatchToolInput>(input) {
Ok(input) => input,
Err(err) => return Task::ready(Err(anyhow!(err))),
};
if input.invocations.is_empty() {
return Task::ready(Err(anyhow!("No tool invocations provided")));
}
let run_tools_concurrently = input.run_tools_concurrently;
let foreground_task = {
let working_set = ToolWorkingSet::default();
let invocations = input.invocations;
let messages = messages.to_vec();
cx.spawn(async move |cx| {
let mut tasks = Vec::new();
let mut tool_names = Vec::new();
for invocation in invocations {
let tool_name = invocation.name.clone();
tool_names.push(tool_name.clone());
let tool = cx
.update(|cx| working_set.tool(&tool_name, cx))
.map_err(|err| {
anyhow!("Failed to look up tool '{}': {}", tool_name, err)
})?;
let Some(tool) = tool else {
return Err(anyhow!("Tool '{}' not found", tool_name));
};
let project = project.clone();
let action_log = action_log.clone();
let messages = messages.clone();
let task = cx
.update(|cx| tool.run(invocation.input, &messages, project, action_log, cx))
.map_err(|err| anyhow!("Failed to start tool '{}': {}", tool_name, err))?;
tasks.push(task);
}
Ok((tasks, tool_names))
})
};
cx.background_spawn(async move {
let (tasks, tool_names) = foreground_task.await?;
let mut results = Vec::with_capacity(tasks.len());
if run_tools_concurrently {
results.extend(join_all(tasks).await)
} else {
for task in tasks {
results.push(task.await);
}
};
let mut formatted_results = String::new();
let mut error_occurred = false;
for (i, result) in results.into_iter().enumerate() {
let tool_name = &tool_names[i];
match result {
Ok(output) => {
formatted_results
.push_str(&format!("Tool '{}' result:\n{}\n\n", tool_name, output));
}
Err(err) => {
error_occurred = true;
formatted_results
.push_str(&format!("Tool '{}' error: {}\n\n", tool_name, err));
}
}
}
if error_occurred {
formatted_results
.push_str("Note: Some tool invocations failed. See individual results above.");
}
Ok(formatted_results.trim().to_string())
})
}
}

View File

@@ -0,0 +1,9 @@
Invoke multiple other tool calls either sequentially or concurrently.
This tool is useful when you need to perform several operations at once, improving efficiency by reducing the number of back-and-forth interactions needed to complete complex tasks.
If the tool calls are set to be run sequentially, then each tool call within the batch is executed in the order provided. If it's set to run concurrently, then they may run in a different order. Regardless, all tool calls will have the same permissions and context as if they were called individually.
This tool should never be used to run a total of one tool. Instead, just run that one tool directly. You can run batches within batches if desired, which is a way you can mix concurrent and sequential tool call execution.
When it's possible to run tools in a batch, you should run as many as possible in the batch, up to a maximum of 32. For example, don't run multiple consecutive batches of 10 when you could instead run one batch of 30.

View File

@@ -0,0 +1,88 @@
use project::DocumentSymbol;
use regex::Regex;
#[derive(Debug, Clone)]
pub struct Entry {
pub name: String,
pub kind: lsp::SymbolKind,
pub depth: u32,
pub start_line: usize,
pub end_line: usize,
}
/// An iterator that filters document symbols based on a regex pattern.
/// This iterator recursively traverses the document symbol tree, incrementing depth for child symbols.
#[derive(Debug, Clone)]
pub struct CodeSymbolIterator<'a> {
symbols: &'a [DocumentSymbol],
regex: Option<Regex>,
// Stack of (symbol, depth) pairs to process
pending_symbols: Vec<(&'a DocumentSymbol, u32)>,
current_index: usize,
current_depth: u32,
}
impl<'a> CodeSymbolIterator<'a> {
pub fn new(symbols: &'a [DocumentSymbol], regex: Option<Regex>) -> Self {
Self {
symbols,
regex,
pending_symbols: Vec::new(),
current_index: 0,
current_depth: 0,
}
}
}
impl Iterator for CodeSymbolIterator<'_> {
type Item = Entry;
fn next(&mut self) -> Option<Self::Item> {
if let Some((symbol, depth)) = self.pending_symbols.pop() {
for child in symbol.children.iter().rev() {
self.pending_symbols.push((child, depth + 1));
}
return Some(Entry {
name: symbol.name.clone(),
kind: symbol.kind,
depth,
start_line: symbol.range.start.0.row as usize,
end_line: symbol.range.end.0.row as usize,
});
}
while self.current_index < self.symbols.len() {
let regex = self.regex.as_ref();
let symbol = &self.symbols[self.current_index];
self.current_index += 1;
if regex.is_none_or(|regex| regex.is_match(&symbol.name)) {
// Push in reverse order to maintain traversal order
for child in symbol.children.iter().rev() {
self.pending_symbols.push((child, self.current_depth + 1));
}
return Some(Entry {
name: symbol.name.clone(),
kind: symbol.kind,
depth: self.current_depth,
start_line: symbol.range.start.0.row as usize,
end_line: symbol.range.end.0.row as usize,
});
} else {
// Even if parent doesn't match, push children to check them later
for child in symbol.children.iter().rev() {
self.pending_symbols.push((child, self.current_depth + 1));
}
// Check if any pending children match our criteria
if let Some(result) = self.next() {
return Some(result);
}
}
}
None
}
}

View File

@@ -0,0 +1,441 @@
use std::fmt::{self, Write};
use std::path::PathBuf;
use std::sync::Arc;
use anyhow::{anyhow, Result};
use assistant_tool::{ActionLog, Tool};
use collections::IndexMap;
use gpui::{App, AsyncApp, Entity, Task};
use language::{CodeLabel, Language, LanguageRegistry};
use language_model::LanguageModelRequestMessage;
use lsp::SymbolKind;
use project::{DocumentSymbol, Project, Symbol};
use regex::{Regex, RegexBuilder};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use ui::IconName;
use util::markdown::MarkdownString;
use crate::code_symbol_iter::{CodeSymbolIterator, Entry};
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct CodeSymbolsInput {
/// The relative path of the source code file to read and get the symbols for.
/// This tool should only be used on source code files, never on any other type of file.
///
/// This path should never be absolute, and the first component
/// of the path should always be a root directory in a project.
///
/// If no path is specified, this tool returns a flat list of all symbols in the project
/// instead of a hierarchical outline of a specific file.
///
/// <example>
/// If the project has the following root directories:
///
/// - directory1
/// - directory2
///
/// If you want to access `file.md` in `directory1`, you should use the path `directory1/file.md`.
/// If you want to access `file.md` in `directory2`, you should use the path `directory2/file.md`.
/// </example>
#[serde(default)]
pub path: Option<String>,
/// Optional regex pattern to filter symbols by name.
/// When provided, only symbols whose names match this pattern will be included in the results.
///
/// <example>
/// To find only symbols that contain the word "test", use the regex pattern "test".
/// To find methods that start with "get_", use the regex pattern "^get_".
/// </example>
#[serde(default)]
pub regex: Option<String>,
/// Whether the regex is case-sensitive. Defaults to false (case-insensitive).
///
/// <example>
/// Set to `true` to make regex matching case-sensitive.
/// </example>
#[serde(default)]
pub case_sensitive: bool,
/// Optional starting position for paginated results (0-based).
/// When not provided, starts from the beginning.
#[serde(default)]
pub offset: u32,
}
impl CodeSymbolsInput {
/// Which page of search results this is.
pub fn page(&self) -> u32 {
1 + (self.offset / RESULTS_PER_PAGE)
}
}
const RESULTS_PER_PAGE: u32 = 2000;
pub struct CodeSymbolsTool;
impl Tool for CodeSymbolsTool {
fn name(&self) -> String {
"code-symbols".into()
}
fn needs_confirmation(&self) -> bool {
false
}
fn description(&self) -> String {
include_str!("./code_symbols_tool/description.md").into()
}
fn icon(&self) -> IconName {
IconName::Eye
}
fn input_schema(&self) -> serde_json::Value {
let schema = schemars::schema_for!(CodeSymbolsInput);
serde_json::to_value(&schema).unwrap()
}
fn ui_text(&self, input: &serde_json::Value) -> String {
match serde_json::from_value::<CodeSymbolsInput>(input.clone()) {
Ok(input) => {
let page = input.page();
match &input.path {
Some(path) => {
let path = MarkdownString::inline_code(path);
if page > 1 {
format!("List page {page} of code symbols for {path}")
} else {
format!("List code symbols for {path}")
}
}
None => {
if page > 1 {
format!("List page {page} of project symbols")
} else {
"List all project symbols".to_string()
}
}
}
}
Err(_) => "List code symbols".to_string(),
}
}
fn run(
self: Arc<Self>,
input: serde_json::Value,
_messages: &[LanguageModelRequestMessage],
project: Entity<Project>,
action_log: Entity<ActionLog>,
cx: &mut App,
) -> Task<Result<String>> {
let input = match serde_json::from_value::<CodeSymbolsInput>(input) {
Ok(input) => input,
Err(err) => return Task::ready(Err(anyhow!(err))),
};
let regex = match input.regex {
Some(regex_str) => match RegexBuilder::new(&regex_str)
.case_insensitive(!input.case_sensitive)
.build()
{
Ok(regex) => Some(regex),
Err(err) => return Task::ready(Err(anyhow!("Invalid regex: {err}"))),
},
None => None,
};
cx.spawn(async move |cx| match input.path {
Some(path) => file_outline(project, path, action_log, regex, input.offset, cx).await,
None => project_symbols(project, regex, input.offset, cx).await,
})
}
}
async fn file_outline(
project: Entity<Project>,
path: String,
action_log: Entity<ActionLog>,
regex: Option<Regex>,
offset: u32,
cx: &mut AsyncApp,
) -> anyhow::Result<String> {
let buffer = {
let project_path = project.read_with(cx, |project, cx| {
project
.find_project_path(&path, cx)
.ok_or_else(|| anyhow!("Path {path} not found in project"))
})??;
project
.update(cx, |project, cx| project.open_buffer(project_path, cx))?
.await?
};
action_log.update(cx, |action_log, cx| {
action_log.buffer_read(buffer.clone(), cx);
})?;
let symbols = project
.update(cx, |project, cx| project.document_symbols(&buffer, cx))?
.await?;
if symbols.is_empty() {
return Err(
if buffer.read_with(cx, |buffer, _| buffer.snapshot().is_empty())? {
anyhow!("This file is empty.")
} else {
anyhow!("No outline information available for this file.")
},
);
}
let language = buffer.read_with(cx, |buffer, _| buffer.language().cloned())?;
let language_registry = project.read_with(cx, |project, _| project.languages().clone())?;
render_outline(&symbols, language, language_registry, regex, offset).await
}
async fn project_symbols(
project: Entity<Project>,
regex: Option<Regex>,
offset: u32,
cx: &mut AsyncApp,
) -> anyhow::Result<String> {
let symbols = project
.update(cx, |project, cx| project.symbols("", cx))?
.await?;
if symbols.is_empty() {
return Err(anyhow!("No symbols found in project."));
}
let mut symbols_by_path: IndexMap<PathBuf, Vec<&Symbol>> = IndexMap::default();
for symbol in symbols
.iter()
.filter(|symbol| match &regex {
Some(regex) => regex.is_match(&symbol.name),
_ => true,
})
.skip(offset as usize)
// Take 1 more than RESULTS_PER_PAGE so we can tell if there are more results.
.take((RESULTS_PER_PAGE as usize).saturating_add(1))
{
if let Some(worktree_path) = project.read_with(cx, |project, cx| {
project
.worktree_for_id(symbol.path.worktree_id, cx)
.map(|worktree| PathBuf::from(worktree.read(cx).root_name()))
})? {
let path = worktree_path.join(&symbol.path.path);
symbols_by_path.entry(path).or_default().push(symbol);
}
}
// If no symbols matched the filter, return early
if symbols_by_path.is_empty() {
return Err(anyhow!("No symbols found matching the criteria."));
}
let mut symbols_rendered = 0;
let mut has_more_symbols = false;
let mut output = String::new();
'outer: for (file_path, file_symbols) in symbols_by_path {
if symbols_rendered > 0 {
output.push('\n');
}
writeln!(&mut output, "{}", file_path.display()).ok();
for symbol in file_symbols {
if symbols_rendered >= RESULTS_PER_PAGE {
has_more_symbols = true;
break 'outer;
}
write!(&mut output, " {} ", symbol.label.text()).ok();
// Convert to 1-based line numbers for display
let start_line = symbol.range.start.0.row as usize + 1;
let end_line = symbol.range.end.0.row as usize + 1;
if start_line == end_line {
writeln!(&mut output, "[L{}]", start_line).ok();
} else {
writeln!(&mut output, "[L{}-{}]", start_line, end_line).ok();
}
symbols_rendered += 1;
}
}
Ok(if symbols_rendered == 0 {
"No symbols found in the requested page.".to_string()
} else if has_more_symbols {
format!(
"{output}\nShowing symbols {}-{} (more symbols were found; use offset: {} to see next page)",
offset + 1,
offset + symbols_rendered,
offset + RESULTS_PER_PAGE,
)
} else {
output
})
}
async fn render_outline(
symbols: &[DocumentSymbol],
language: Option<Arc<Language>>,
registry: Arc<LanguageRegistry>,
regex: Option<Regex>,
offset: u32,
) -> Result<String> {
const RESULTS_PER_PAGE_USIZE: usize = RESULTS_PER_PAGE as usize;
let entries = CodeSymbolIterator::new(symbols, regex.clone())
.skip(offset as usize)
// Take 1 more than RESULTS_PER_PAGE so we can tell if there are more results.
.take(RESULTS_PER_PAGE_USIZE.saturating_add(1))
.collect::<Vec<Entry>>();
let has_more = entries.len() > RESULTS_PER_PAGE_USIZE;
// Get language-specific labels, if available
let labels = match &language {
Some(lang) => {
let entries_for_labels: Vec<(String, SymbolKind)> = entries
.iter()
.take(RESULTS_PER_PAGE_USIZE)
.map(|entry| (entry.name.clone(), entry.kind))
.collect();
let lang_name = lang.name();
match registry.lsp_adapters(&lang_name).first().cloned() {
Some(lsp_adapter) => lsp_adapter
.labels_for_symbols(&entries_for_labels, lang)
.await
.ok(),
_ => None,
}
}
None => None,
};
let mut output = String::new();
let entries_rendered = match &labels {
Some(label_list) => render_entries(
&mut output,
entries
.into_iter()
.take(RESULTS_PER_PAGE_USIZE)
.zip(label_list.iter())
.map(|(entry, label)| (entry, label.as_ref())),
),
None => render_entries(
&mut output,
entries
.into_iter()
.take(RESULTS_PER_PAGE_USIZE)
.map(|entry| (entry, None)),
),
};
// Calculate pagination information
let page_start = offset + 1;
let page_end = offset + entries_rendered;
let total_symbols = if has_more {
format!("more than {}", page_end)
} else {
page_end.to_string()
};
// Add pagination information
if has_more {
writeln!(&mut output, "\nShowing symbols {page_start}-{page_end} (there were more symbols found; use offset: {page_end} to see next page)",
)
} else {
writeln!(
&mut output,
"\nShowing symbols {page_start}-{page_end} (total symbols: {total_symbols})",
)
}
.ok();
Ok(output)
}
fn render_entries<'a>(
output: &mut String,
entries: impl IntoIterator<Item = (Entry, Option<&'a CodeLabel>)>,
) -> u32 {
let mut entries_rendered = 0;
for (entry, label) in entries {
// Indent based on depth ("" for level 0, " " for level 1, etc.)
for _ in 0..entry.depth {
output.push_str(" ");
}
match label {
Some(label) => {
output.push_str(label.text());
}
None => {
write_symbol_kind(output, entry.kind).ok();
output.push_str(&entry.name);
}
}
// Add position information - convert to 1-based line numbers for display
let start_line = entry.start_line + 1;
let end_line = entry.end_line + 1;
if start_line == end_line {
writeln!(output, " [L{}]", start_line).ok();
} else {
writeln!(output, " [L{}-{}]", start_line, end_line).ok();
}
entries_rendered += 1;
}
entries_rendered
}
// We may not have a language server adapter to have language-specific
// ways to translate SymbolKnd into a string. In that situation,
// fall back on some reasonable default strings to render.
fn write_symbol_kind(buf: &mut String, kind: SymbolKind) -> Result<(), fmt::Error> {
match kind {
SymbolKind::FILE => write!(buf, "file "),
SymbolKind::MODULE => write!(buf, "module "),
SymbolKind::NAMESPACE => write!(buf, "namespace "),
SymbolKind::PACKAGE => write!(buf, "package "),
SymbolKind::CLASS => write!(buf, "class "),
SymbolKind::METHOD => write!(buf, "method "),
SymbolKind::PROPERTY => write!(buf, "property "),
SymbolKind::FIELD => write!(buf, "field "),
SymbolKind::CONSTRUCTOR => write!(buf, "constructor "),
SymbolKind::ENUM => write!(buf, "enum "),
SymbolKind::INTERFACE => write!(buf, "interface "),
SymbolKind::FUNCTION => write!(buf, "function "),
SymbolKind::VARIABLE => write!(buf, "variable "),
SymbolKind::CONSTANT => write!(buf, "constant "),
SymbolKind::STRING => write!(buf, "string "),
SymbolKind::NUMBER => write!(buf, "number "),
SymbolKind::BOOLEAN => write!(buf, "boolean "),
SymbolKind::ARRAY => write!(buf, "array "),
SymbolKind::OBJECT => write!(buf, "object "),
SymbolKind::KEY => write!(buf, "key "),
SymbolKind::NULL => write!(buf, "null "),
SymbolKind::ENUM_MEMBER => write!(buf, "enum member "),
SymbolKind::STRUCT => write!(buf, "struct "),
SymbolKind::EVENT => write!(buf, "event "),
SymbolKind::OPERATOR => write!(buf, "operator "),
SymbolKind::TYPE_PARAMETER => write!(buf, "type parameter "),
_ => Ok(()),
}
}

View File

@@ -0,0 +1,39 @@
Returns either an outline of the public code symbols in the entire project (grouped by file) or else an outline of both the public and private code symbols within a particular file.
When a path is provided, this tool returns a hierarchical outline of code symbols for that specific file.
When no path is provided, it returns a list of all public code symbols in the project, organized by file.
You can also provide an optional regular expression which filters the output by only showing code symbols which match that regex.
Results are paginated with 2000 entries per page. Use the optional 'offset' parameter to request subsequent pages.
Markdown headings indicate the structure of the output; just like
with markdown headings, the more # symbols there are at the beginning of a line,
the deeper it is in the hierarchy.
Each code symbol entry ends with a line number or range, which tells you what portion of the
underlying source code file corresponds to that part of the outline. You can use
that line information with other tools, to strategically read portions of the source code.
For example, you can use this tool to find a relevant symbol in the project, then get the outline of the file which contains that symbol, then use the line number information from that file's outline to read different sections of that file, without having to read the entire file all at once (which can be slow, or use a lot of tokens).
<example>
# class Foo [L123-136]
## method do_something(arg1, arg2) [L124-126]
## method process_data(data) [L128-135]
# class Bar [L145-161]
## method initialize() [L146-149]
## method update_state(new_state) [L160]
## private method _validate_state(state) [L161-162]
</example>
This example shows how tree-sitter outlines the structure of source code:
1. `class Foo` is defined on lines 123-136
- It contains a method `do_something` spanning lines 124-126
- It also has a method `process_data` spanning lines 128-135
2. `class Bar` is defined on lines 145-161
- It has an `initialize` method spanning lines 146-149
- It has an `update_state` method on line 160
- It has a private method `_validate_state` spanning lines 161-162

View File

@@ -70,7 +70,7 @@ impl Tool for CreateFileTool {
input: serde_json::Value,
_messages: &[LanguageModelRequestMessage],
project: Entity<Project>,
_action_log: Entity<ActionLog>,
action_log: Entity<ActionLog>,
cx: &mut App,
) -> Task<Result<String>> {
let input = match serde_json::from_value::<CreateFileToolInput>(input) {
@@ -85,24 +85,20 @@ impl Tool for CreateFileTool {
let destination_path: Arc<str> = input.path.as_str().into();
cx.spawn(async move |cx| {
project
.update(cx, |project, cx| {
project.create_entry(project_path.clone(), false, cx)
})?
.await
.map_err(|err| anyhow!("Unable to create {destination_path}: {err}"))?;
let buffer = project
.update(cx, |project, cx| {
project.open_buffer(project_path.clone(), cx)
})?
.await
.map_err(|err| anyhow!("Unable to open buffer for {destination_path}: {err}"))?;
buffer.update(cx, |buffer, cx| {
buffer.set_text(contents, cx);
let edit_id = buffer.update(cx, |buffer, cx| buffer.set_text(contents, cx))?;
action_log.update(cx, |action_log, cx| {
action_log.will_create_buffer(buffer.clone(), edit_id, cx)
})?;
project
.update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))?
.update(cx, |project, cx| project.save_buffer(buffer, cx))?
.await
.map_err(|err| anyhow!("Unable to save buffer for {destination_path}: {err}"))?;

View File

@@ -1,8 +1,9 @@
use anyhow::{anyhow, Result};
use assistant_tool::{ActionLog, Tool};
use futures::{channel::mpsc, SinkExt, StreamExt};
use gpui::{App, AppContext, Entity, Task};
use language_model::LanguageModelRequestMessage;
use project::Project;
use project::{Project, ProjectPath};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::sync::Arc;
@@ -60,28 +61,76 @@ impl Tool for DeletePathTool {
input: serde_json::Value,
_messages: &[LanguageModelRequestMessage],
project: Entity<Project>,
_action_log: Entity<ActionLog>,
action_log: Entity<ActionLog>,
cx: &mut App,
) -> Task<Result<String>> {
let path_str = match serde_json::from_value::<DeletePathToolInput>(input) {
Ok(input) => input.path,
Err(err) => return Task::ready(Err(anyhow!(err))),
};
let Some(project_path) = project.read(cx).find_project_path(&path_str, cx) else {
return Task::ready(Err(anyhow!(
"Couldn't delete {path_str} because that path isn't in this project."
)));
};
match project
let Some(worktree) = project
.read(cx)
.find_project_path(&path_str, cx)
.and_then(|path| project.update(cx, |project, cx| project.delete_file(path, false, cx)))
{
Some(deletion_task) => cx.background_spawn(async move {
match deletion_task.await {
.worktree_for_id(project_path.worktree_id, cx)
else {
return Task::ready(Err(anyhow!(
"Couldn't delete {path_str} because that path isn't in this project."
)));
};
let worktree_snapshot = worktree.read(cx).snapshot();
let (mut paths_tx, mut paths_rx) = mpsc::channel(256);
cx.background_spawn({
let project_path = project_path.clone();
async move {
for entry in
worktree_snapshot.traverse_from_path(true, false, false, &project_path.path)
{
if !entry.path.starts_with(&project_path.path) {
break;
}
paths_tx
.send(ProjectPath {
worktree_id: project_path.worktree_id,
path: entry.path.clone(),
})
.await?;
}
anyhow::Ok(())
}
})
.detach();
cx.spawn(async move |cx| {
while let Some(path) = paths_rx.next().await {
if let Ok(buffer) = project
.update(cx, |project, cx| project.open_buffer(path, cx))?
.await
{
action_log.update(cx, |action_log, cx| {
action_log.will_delete_buffer(buffer.clone(), cx)
})?;
}
}
let delete = project.update(cx, |project, cx| {
project.delete_file(project_path, false, cx)
})?;
match delete {
Some(deletion_task) => match deletion_task.await {
Ok(()) => Ok(format!("Deleted {path_str}")),
Err(err) => Err(anyhow!("Failed to delete {path_str}: {err}")),
}
}),
None => Task::ready(Err(anyhow!(
"Couldn't delete {path_str} because that path isn't in this project."
))),
}
},
None => Err(anyhow!(
"Couldn't delete {path_str} because that path isn't in this project."
)),
}
})
}
}

View File

@@ -63,16 +63,16 @@ impl Tool for DiagnosticsTool {
}
fn ui_text(&self, input: &serde_json::Value) -> String {
if let Some(path) = serde_json::from_value::<DiagnosticsToolInput>(input.clone())
match serde_json::from_value::<DiagnosticsToolInput>(input.clone())
.ok()
.and_then(|input| match input.path {
Some(path) if !path.is_empty() => Some(MarkdownString::inline_code(&path)),
_ => None,
})
{
format!("Check diagnostics for {path}")
} else {
"Check project diagnostics".to_string()
}) {
Some(path) => {
format!("Check diagnostics for {path}")
}
_ => "Check project diagnostics".to_string(),
}
}

View File

@@ -173,6 +173,7 @@ enum EditorResponse {
struct AppliedAction {
source: String,
buffer: Entity<language::Buffer>,
edit_ids: Vec<clock::Lamport>,
}
#[derive(Debug)]
@@ -340,9 +341,18 @@ impl EditToolRequest {
self.push_search_error(error);
}
DiffResult::Diff(diff) => {
let _clock = buffer.update(cx, |buffer, cx| buffer.apply_diff(diff, cx))?;
let edit_ids = buffer.update(cx, |buffer, cx| {
buffer.finalize_last_transaction();
buffer.apply_diff(diff, false, cx);
let transaction = buffer.finalize_last_transaction();
transaction.map_or(Vec::new(), |transaction| transaction.edit_ids.clone())
})?;
self.push_applied_action(AppliedAction { source, buffer });
self.push_applied_action(AppliedAction {
source,
buffer,
edit_ids,
});
}
}
@@ -464,7 +474,10 @@ impl EditToolRequest {
let mut changed_buffers = HashSet::default();
for action in applied {
changed_buffers.insert(action.buffer);
changed_buffers.insert(action.buffer.clone());
self.action_log.update(cx, |log, cx| {
log.buffer_edited(action.buffer, action.edit_ids, cx)
})?;
write!(&mut output, "\n\n{}", action.source)?;
}
@@ -474,10 +487,6 @@ impl EditToolRequest {
.await?;
}
self.action_log
.update(cx, |log, cx| log.buffer_edited(changed_buffers.clone(), cx))
.log_err();
if !search_errors.is_empty() {
writeln!(
&mut output,

View File

@@ -1,5 +1,7 @@
Edit files in the current project by specifying instructions in natural language.
IMPORTANT NOTE: If there is a find-replace tool, use that instead of this tool! This tool is only to be used as a fallback in case that tool is unavailable. Always prefer that tool if it is available.
When using this tool, you should suggest one coherent edit that can be made to the codebase.
When the set of edits you want to make is large or complex, feel free to invoke this tool multiple times, each time focusing on a specific change you wanna make.

View File

@@ -5,7 +5,7 @@ use language_model::LanguageModelRequestMessage;
use project::Project;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::{collections::HashSet, path::PathBuf, sync::Arc};
use std::{path::PathBuf, sync::Arc};
use ui::IconName;
use crate::replace::replace_exact;
@@ -188,23 +188,24 @@ impl Tool for FindReplaceFileTool {
})
.await;
if let Some(diff) = result {
buffer.update(cx, |buffer, cx| {
let _ = buffer.apply_diff(diff, cx);
match result { Some(diff) => {
let edit_ids = buffer.update(cx, |buffer, cx| {
buffer.finalize_last_transaction();
buffer.apply_diff(diff, false, cx);
let transaction = buffer.finalize_last_transaction();
transaction.map_or(Vec::new(), |transaction| transaction.edit_ids.clone())
})?;
action_log.update(cx, |log, cx| {
log.buffer_edited(buffer.clone(), edit_ids, cx)
})?;
project.update(cx, |project, cx| {
project.save_buffer(buffer.clone(), cx)
project.save_buffer(buffer, cx)
})?.await?;
action_log.update(cx, |log, cx| {
let mut buffers = HashSet::default();
buffers.insert(buffer);
log.buffer_edited(buffers, cx);
})?;
Ok(format!("Edited {}", input.path.display()))
} else {
} _ => {
let err = buffer.read_with(cx, |buffer, _cx| {
let file_exists = buffer
.file()
@@ -223,7 +224,7 @@ impl Tool for FindReplaceFileTool {
})?;
Err(err)
}
}}
})
}
}

View File

@@ -0,0 +1,68 @@
use anyhow::{anyhow, Context as _, Result};
use assistant_tool::{ActionLog, Tool};
use gpui::{App, AppContext, Entity, Task};
use language_model::LanguageModelRequestMessage;
use project::Project;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::sync::Arc;
use ui::IconName;
use util::markdown::MarkdownString;
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct OpenToolInput {
/// The path or URL to open with the default application.
path_or_url: String,
}
pub struct OpenTool;
impl Tool for OpenTool {
fn name(&self) -> String {
"open".to_string()
}
fn needs_confirmation(&self) -> bool {
true
}
fn description(&self) -> String {
include_str!("./open_tool/description.md").to_string()
}
fn icon(&self) -> IconName {
IconName::ExternalLink
}
fn input_schema(&self) -> serde_json::Value {
let schema = schemars::schema_for!(OpenToolInput);
serde_json::to_value(&schema).unwrap()
}
fn ui_text(&self, input: &serde_json::Value) -> String {
match serde_json::from_value::<OpenToolInput>(input.clone()) {
Ok(input) => format!("Open `{}`", MarkdownString::escape(&input.path_or_url)),
Err(_) => "Open file or URL".to_string(),
}
}
fn run(
self: Arc<Self>,
input: serde_json::Value,
_messages: &[LanguageModelRequestMessage],
_project: Entity<Project>,
_action_log: Entity<ActionLog>,
cx: &mut App,
) -> Task<Result<String>> {
let input: OpenToolInput = match serde_json::from_value(input) {
Ok(input) => input,
Err(err) => return Task::ready(Err(anyhow!(err))),
};
cx.background_spawn(async move {
open::that(&input.path_or_url).context("Failed to open URL or file path")?;
Ok(format!("Successfully opened {}", input.path_or_url))
})
}
}

View File

@@ -0,0 +1,6 @@
This tool opens a file or URL with the default application associated with it on the user's operating system:
- On macOS, it's equivalent to the `open` command
- On Windows, it's equivalent to `start`
- On Linux, it uses something like `xdg-open`, `gio open`, `gnome-open`, `kde-open`, `wslview` as appropriate
For example, it can open a web browser with a URL, open a PDF file with the default PDF viewer, etc.

View File

@@ -28,7 +28,7 @@ pub struct PathSearchToolInput {
/// Optional starting position for paginated results (0-based).
/// When not provided, starts from the beginning.
#[serde(default)]
pub offset: Option<usize>,
pub offset: u32,
}
const RESULTS_PER_PAGE: usize = 50;
@@ -73,7 +73,7 @@ impl Tool for PathSearchTool {
cx: &mut App,
) -> Task<Result<String>> {
let (offset, glob) = match serde_json::from_value::<PathSearchToolInput>(input) {
Ok(input) => (input.offset.unwrap_or(0), input.glob),
Ok(input) => (input.offset, input.glob),
Err(err) => return Task::ready(Err(anyhow!(err))),
};
@@ -116,10 +116,10 @@ impl Tool for PathSearchTool {
matches.sort();
let total_matches = matches.len();
let response = if total_matches > offset + RESULTS_PER_PAGE {
let paginated_matches: Vec<_> = matches
let response = if total_matches > RESULTS_PER_PAGE + offset as usize {
let paginated_matches: Vec<_> = matches
.into_iter()
.skip(offset)
.skip(offset as usize)
.take(RESULTS_PER_PAGE)
.collect();
@@ -127,7 +127,7 @@ impl Tool for PathSearchTool {
"Found {} total matches. Showing results {}-{} (provide 'offset' parameter for more results):\n\n{}",
total_matches,
offset + 1,
offset + paginated_matches.len(),
offset as usize + paginated_matches.len(),
paginated_matches.join("\n")
)
} else {

View File

@@ -24,13 +24,13 @@ pub struct RegexSearchToolInput {
/// Optional starting position for paginated results (0-based).
/// When not provided, starts from the beginning.
#[serde(default)]
pub offset: Option<u32>,
pub offset: u32,
}
impl RegexSearchToolInput {
/// Which page of search results this is.
pub fn page(&self) -> u32 {
1 + (self.offset.unwrap_or(0) / RESULTS_PER_PAGE)
1 + (self.offset / RESULTS_PER_PAGE)
}
}
@@ -87,7 +87,7 @@ impl Tool for RegexSearchTool {
const CONTEXT_LINES: u32 = 2;
let (offset, regex) = match serde_json::from_value::<RegexSearchToolInput>(input) {
Ok(input) => (input.offset.unwrap_or(0), input.regex),
Ok(input) => (input.offset, input.regex),
Err(err) => return Task::ready(Err(anyhow!(err))),
};

View File

@@ -1,5 +1,7 @@
Searches the entire project for the given regular expression.
Returns a list of paths that matched the query. For each path, it returns a list of excerpts of the matched text.
Returns a list of paths that matched the query. For each path, it returns some excerpts of the matched text.
Results are paginated with 20 matches per page. Use the optional 'offset' parameter to request subsequent pages.
This tool is not aware of semantics and does not use any information from language servers, so it should only be used when no available semantic tool (e.g. one that uses language servers) could fit a particular use case instead.

View File

@@ -518,7 +518,7 @@ mod tests {
// Call replace_flexible and transform the result
replace_with_flexible_indent(old, new, &buffer_snapshot).map(|diff| {
buffer.update(cx, |buffer, cx| {
let _ = buffer.apply_diff(diff, cx);
let _ = buffer.apply_diff(diff, false, cx);
buffer.text()
})
})

View File

@@ -0,0 +1,305 @@
use anyhow::{anyhow, Context as _, Result};
use assistant_tool::{ActionLog, Tool};
use gpui::{App, AsyncApp, Entity, Task};
use language::{self, Anchor, Buffer, BufferSnapshot, Location, Point, ToPoint, ToPointUtf16};
use language_model::LanguageModelRequestMessage;
use project::Project;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::{fmt::Write, ops::Range, sync::Arc};
use ui::IconName;
use util::markdown::MarkdownString;
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct SymbolInfoToolInput {
/// The relative path to the file containing the symbol.
///
/// WARNING: you MUST start this path with one of the project's root directories.
pub path: String,
/// The information to get about the symbol.
pub command: Info,
/// The text that comes immediately before the symbol in the file.
pub context_before_symbol: String,
/// The symbol name. This text must appear in the file right between `context_before_symbol`
/// and `context_after_symbol`.
///
/// The file must contain exactly one occurrence of `context_before_symbol` followed by
/// `symbol` followed by `context_after_symbol`. If the file contains zero occurrences,
/// or if it contains more than one occurrence, the tool will fail, so it is absolutely
/// critical that you verify ahead of time that the string is unique. You can search
/// the file's contents to verify this ahead of time.
///
/// To make the string more likely to be unique, include a minimum of 1 line of context
/// before the symbol, as well as a minimum of 1 line of context after the symbol.
/// If these lines of context are not enough to obtain a string that appears only once
/// in the file, then double the number of context lines until the string becomes unique.
/// (Start with 1 line before and 1 line after though, because too much context is
/// needlessly costly.)
///
/// Do not alter the context lines of code in any way, and make sure to preserve all
/// whitespace and indentation for all lines of code. The combined string must be exactly
/// as it appears in the file, or else this tool call will fail.
pub symbol: String,
/// The text that comes immediately after the symbol in the file.
pub context_after_symbol: String,
}
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum Info {
/// Get the symbol's definition (where it's first assigned, even if it's declared elsewhere)
Definition,
/// Get the symbol's declaration (where it's first declared)
Declaration,
/// Get the symbol's implementation
Implementation,
/// Get the symbol's type definition
TypeDefinition,
/// Find all references to the symbol in the project
References,
}
pub struct SymbolInfoTool;
impl Tool for SymbolInfoTool {
fn name(&self) -> String {
"symbol-info".into()
}
fn needs_confirmation(&self) -> bool {
false
}
fn description(&self) -> String {
include_str!("./symbol_info_tool/description.md").into()
}
fn icon(&self) -> IconName {
IconName::Eye
}
fn input_schema(&self) -> serde_json::Value {
let schema = schemars::schema_for!(SymbolInfoToolInput);
serde_json::to_value(&schema).unwrap()
}
fn ui_text(&self, input: &serde_json::Value) -> String {
match serde_json::from_value::<SymbolInfoToolInput>(input.clone()) {
Ok(input) => {
let symbol = MarkdownString::inline_code(&input.symbol);
match input.command {
Info::Definition => {
format!("Find definition for {symbol}")
}
Info::Declaration => {
format!("Find declaration for {symbol}")
}
Info::Implementation => {
format!("Find implementation for {symbol}")
}
Info::TypeDefinition => {
format!("Find type definition for {symbol}")
}
Info::References => {
format!("Find references for {symbol}")
}
}
}
Err(_) => "Get symbol info".to_string(),
}
}
fn run(
self: Arc<Self>,
input: serde_json::Value,
_messages: &[LanguageModelRequestMessage],
project: Entity<Project>,
action_log: Entity<ActionLog>,
cx: &mut App,
) -> Task<Result<String>> {
let input = match serde_json::from_value::<SymbolInfoToolInput>(input) {
Ok(input) => input,
Err(err) => return Task::ready(Err(anyhow!(err))),
};
cx.spawn(async move |cx| {
let buffer = {
let project_path = project.read_with(cx, |project, cx| {
project
.find_project_path(&input.path, cx)
.context("Path not found in project")
})??;
project.update(cx, |project, cx| project.open_buffer(project_path, cx))?.await?
};
action_log.update(cx, |action_log, cx| {
action_log.buffer_read(buffer.clone(), cx);
})?;
let position = {
let Some(range) = buffer.read_with(cx, |buffer, _cx| {
find_symbol_range(&buffer, &input.context_before_symbol, &input.symbol, &input.context_after_symbol)
})? else {
return Err(anyhow!(
"Failed to locate the text specified by context_before_symbol, symbol, and context_after_symbol. Make sure context_before_symbol and context_after_symbol each match exactly once in the file."
));
};
buffer.read_with(cx, |buffer, _| {
range.start.to_point_utf16(&buffer.snapshot())
})?
};
let output: String = match input.command {
Info::Definition => {
render_locations(project
.update(cx, |project, cx| {
project.definition(&buffer, position, cx)
})?
.await?.into_iter().map(|link| link.target),
cx)
}
Info::Declaration => {
render_locations(project
.update(cx, |project, cx| {
project.declaration(&buffer, position, cx)
})?
.await?.into_iter().map(|link| link.target),
cx)
}
Info::Implementation => {
render_locations(project
.update(cx, |project, cx| {
project.implementation(&buffer, position, cx)
})?
.await?.into_iter().map(|link| link.target),
cx)
}
Info::TypeDefinition => {
render_locations(project
.update(cx, |project, cx| {
project.type_definition(&buffer, position, cx)
})?
.await?.into_iter().map(|link| link.target),
cx)
}
Info::References => {
render_locations(project
.update(cx, |project, cx| {
project.references(&buffer, position, cx)
})?
.await?,
cx)
}
};
if output.is_empty() {
Err(anyhow!("None found."))
} else {
Ok(output)
}
})
}
}
/// Finds the range of the symbol in the buffer, if it appears between context_before_symbol
/// and context_after_symbol, and if that combined string has one unique result in the buffer.
fn find_symbol_range(
buffer: &Buffer,
context_before_symbol: &str,
symbol: &str,
context_after_symbol: &str,
) -> Option<Range<Anchor>> {
let snapshot = buffer.snapshot();
let text = snapshot.text();
let search_string = format!("{context_before_symbol}{symbol}{context_after_symbol}");
let mut positions = text.match_indices(&search_string);
let position = positions.next()?.0;
// The combined string must appear exactly once.
if positions.next().is_some() {
return None;
}
let symbol_start = position + context_before_symbol.len();
let symbol_end = symbol_start + symbol.len();
let symbol_start_anchor = snapshot.anchor_before(snapshot.offset_to_point(symbol_start));
let symbol_end_anchor = snapshot.anchor_before(snapshot.offset_to_point(symbol_end));
Some(symbol_start_anchor..symbol_end_anchor)
}
fn render_locations(locations: impl IntoIterator<Item = Location>, cx: &mut AsyncApp) -> String {
let mut answer = String::new();
for location in locations {
location
.buffer
.read_with(cx, |buffer, _cx| {
if let Some(target_path) = buffer
.file()
.and_then(|file| file.path().as_os_str().to_str())
{
let snapshot = buffer.snapshot();
let start = location.range.start.to_point(&snapshot);
let end = location.range.end.to_point(&snapshot);
let start_line = start.row + 1;
let start_col = start.column + 1;
let end_line = end.row + 1;
let end_col = end.column + 1;
if start_line == end_line {
writeln!(answer, "{target_path}:{start_line},{start_col}")
} else {
writeln!(
answer,
"{target_path}:{start_line},{start_col}-{end_line},{end_col}",
)
}
.ok();
write_code_excerpt(&mut answer, &snapshot, &location.range);
}
})
.ok();
}
// Trim trailing newlines without reallocating.
answer.truncate(answer.trim_end().len());
answer
}
fn write_code_excerpt(buf: &mut String, snapshot: &BufferSnapshot, range: &Range<Anchor>) {
const MAX_LINE_LEN: u32 = 200;
let start = range.start.to_point(snapshot);
let end = range.end.to_point(snapshot);
for row in start.row..=end.row {
let row_start = Point::new(row, 0);
let row_end = if row < snapshot.max_point().row {
Point::new(row + 1, 0)
} else {
Point::new(row, u32::MAX)
};
buf.extend(
snapshot
.text_for_range(row_start..row_end)
.take(MAX_LINE_LEN as usize),
);
if row_end.column > MAX_LINE_LEN {
buf.push_str("\n");
}
buf.push('\n');
}
}

View File

@@ -0,0 +1,11 @@
Gives detailed information about code symbols in your project such as variables, functions, classes, interface, traits, and other programming constructs, using the editor's integrated Language Server Protocol (LSP) servers.
This tool is the preferred way to do things like:
* Find out where a code symbol is first declared (or first defined - that is, assigned)
* Find all the places where a code symbol is referenced
* Find the type definition for a code symbol
* Find a code symbol's implementation
This tool gives more reliable answers than things like regex searches, because it can account for relevant semantics like aliases. It should be used over textual search tools (e.g. regex) when searching for information about code symbols that this tool supports directly.
This tool should not be used when you need to search for something that is not a code symbol.

View File

@@ -35,7 +35,7 @@ impl SoundRegistry {
cx.set_global(GlobalSoundRegistry(SoundRegistry::new(source)));
}
pub fn get(&self, name: &str) -> Result<impl Source<Item = f32>> {
pub fn get(&self, name: &str) -> Result<impl Source<Item = f32> + use<>> {
if let Some(wav) = self.cache.lock().get(name) {
return Ok(wav.clone());
}

View File

@@ -201,16 +201,19 @@ pub fn check(_: &Check, window: &mut Window, cx: &mut App) {
return;
}
if let Some(updater) = AutoUpdater::get(cx) {
updater.update(cx, |updater, cx| updater.poll(cx));
} else {
drop(window.prompt(
gpui::PromptLevel::Info,
"Could not check for updates",
Some("Auto-updates disabled for non-bundled app."),
&["Ok"],
cx,
));
match AutoUpdater::get(cx) {
Some(updater) => {
updater.update(cx, |updater, cx| updater.poll(cx));
}
_ => {
drop(window.prompt(
gpui::PromptLevel::Info,
"Could not check for updates",
Some("Auto-updates disabled for non-bundled app."),
&["Ok"],
cx,
));
}
}
}

View File

@@ -37,6 +37,8 @@ pub enum Model {
CohereCommandRV1,
CohereCommandRPlusV1,
CohereCommandLightTextV14_4k,
// DeepSeek
DeepSeekR1,
// Meta models
MetaLlama38BInstructV1,
MetaLlama370BInstructV1,
@@ -91,6 +93,7 @@ impl Model {
Model::AmazonNovaLite => "us.amazon.nova-lite-v1:0",
Model::AmazonNovaMicro => "us.amazon.nova-micro-v1:0",
Model::AmazonNovaPro => "us.amazon.nova-pro-v1:0",
Model::DeepSeekR1 => "us.deepseek.r1-v1:0",
Model::AI21J2GrandeInstruct => "ai21.j2-grande-instruct",
Model::AI21J2JumboInstruct => "ai21.j2-jumbo-instruct",
Model::AI21J2Mid => "ai21.j2-mid",
@@ -133,6 +136,7 @@ impl Model {
Self::AmazonNovaLite => "Amazon Nova Lite",
Self::AmazonNovaMicro => "Amazon Nova Micro",
Self::AmazonNovaPro => "Amazon Nova Pro",
Self::DeepSeekR1 => "DeepSeek R1",
Self::AI21J2GrandeInstruct => "AI21 Jurassic2 Grande Instruct",
Self::AI21J2JumboInstruct => "AI21 Jurassic2 Jumbo Instruct",
Self::AI21J2Mid => "AI21 Jurassic2 Mid",

View File

@@ -110,8 +110,8 @@ impl Render for Breadcrumbs {
}
}
})
.tooltip(move |window, cx| {
if let Some(editor) = editor.upgrade() {
.tooltip(move |window, cx| match editor.upgrade() {
Some(editor) => {
let focus_handle = editor.read(cx).focus_handle(cx);
Tooltip::for_action_in(
"Show Symbol Outline",
@@ -120,14 +120,13 @@ impl Render for Breadcrumbs {
window,
cx,
)
} else {
Tooltip::for_action(
"Show Symbol Outline",
&zed_actions::outline::ToggleOutline,
window,
cx,
)
}
_ => Tooltip::for_action(
"Show Symbol Outline",
&zed_actions::outline::ToggleOutline,
window,
cx,
),
}),
),
None => element

View File

@@ -608,55 +608,58 @@ fn compute_hunks(
) -> SumTree<InternalDiffHunk> {
let mut tree = SumTree::new(&buffer);
if let Some((diff_base, diff_base_rope)) = diff_base {
let buffer_text = buffer.as_rope().to_string();
match diff_base {
Some((diff_base, diff_base_rope)) => {
let buffer_text = buffer.as_rope().to_string();
let mut options = GitOptions::default();
options.context_lines(0);
let patch = GitPatch::from_buffers(
diff_base.as_bytes(),
None,
buffer_text.as_bytes(),
None,
Some(&mut options),
)
.log_err();
let mut options = GitOptions::default();
options.context_lines(0);
let patch = GitPatch::from_buffers(
diff_base.as_bytes(),
None,
buffer_text.as_bytes(),
None,
Some(&mut options),
)
.log_err();
// A common case in Zed is that the empty buffer is represented as just a newline,
// but if we just compute a naive diff you get a "preserved" line in the middle,
// which is a bit odd.
if buffer_text == "\n" && diff_base.ends_with("\n") && diff_base.len() > 1 {
// A common case in Zed is that the empty buffer is represented as just a newline,
// but if we just compute a naive diff you get a "preserved" line in the middle,
// which is a bit odd.
if buffer_text == "\n" && diff_base.ends_with("\n") && diff_base.len() > 1 {
tree.push(
InternalDiffHunk {
buffer_range: buffer.anchor_before(0)..buffer.anchor_before(0),
diff_base_byte_range: 0..diff_base.len() - 1,
},
&buffer,
);
return tree;
}
if let Some(patch) = patch {
let mut divergence = 0;
for hunk_index in 0..patch.num_hunks() {
let hunk = process_patch_hunk(
&patch,
hunk_index,
&diff_base_rope,
&buffer,
&mut divergence,
);
tree.push(hunk, &buffer);
}
}
}
_ => {
tree.push(
InternalDiffHunk {
buffer_range: buffer.anchor_before(0)..buffer.anchor_before(0),
diff_base_byte_range: 0..diff_base.len() - 1,
buffer_range: Anchor::MIN..Anchor::MAX,
diff_base_byte_range: 0..0,
},
&buffer,
);
return tree;
}
if let Some(patch) = patch {
let mut divergence = 0;
for hunk_index in 0..patch.num_hunks() {
let hunk = process_patch_hunk(
&patch,
hunk_index,
&diff_base_rope,
&buffer,
&mut divergence,
);
tree.push(hunk, &buffer);
}
}
} else {
tree.push(
InternalDiffHunk {
buffer_range: Anchor::MIN..Anchor::MAX,
diff_base_byte_range: 0..0,
},
&buffer,
);
}
tree
@@ -776,7 +779,7 @@ impl BufferDiff {
language: Option<Arc<Language>>,
language_registry: Option<Arc<LanguageRegistry>>,
cx: &mut App,
) -> impl Future<Output = BufferDiffInner> {
) -> impl Future<Output = BufferDiffInner> + use<> {
let base_text_pair;
let base_text_exists;
let base_text_snapshot;
@@ -818,7 +821,7 @@ impl BufferDiff {
base_text: Option<Arc<String>>,
base_text_snapshot: language::BufferSnapshot,
cx: &App,
) -> impl Future<Output = BufferDiffInner> {
) -> impl Future<Output = BufferDiffInner> + use<> {
let base_text_exists = base_text.is_some();
let base_text_pair = base_text.map(|text| (text, base_text_snapshot.as_rope().clone()));
cx.background_spawn(async move {
@@ -2071,7 +2074,7 @@ mod tests {
)
});
let working_copy = working_copy.read_with(cx, |working_copy, _| working_copy.snapshot());
let mut index_text = if rng.gen() {
let mut index_text = if rng.r#gen() {
Rope::from(head_text.as_str())
} else {
working_copy.as_rope().clone()

View File

@@ -18,7 +18,6 @@ test-support = [
"collections/test-support",
"gpui/test-support",
"livekit_client/test-support",
"livekit_client_macos/test-support",
"project/test-support",
"util/test-support"
]
@@ -41,11 +40,7 @@ serde_derive.workspace = true
settings.workspace = true
telemetry.workspace = true
util.workspace = true
[target.'cfg(target_os = "macos")'.dependencies]
livekit_client_macos.workspace = true
[target.'cfg(not(target_os = "macos"))'.dependencies]
gpui_tokio.workspace = true
livekit_client.workspace = true
[dev-dependencies]
@@ -57,9 +52,4 @@ language = { workspace = true, features = ["test-support"] }
project = { workspace = true, features = ["test-support"] }
util = { workspace = true, features = ["test-support"] }
http_client = { workspace = true, features = ["test-support"] }
[target.'cfg(target_os = "macos")'.dev-dependencies]
livekit_client_macos = { workspace = true, features = ["test-support"] }
[target.'cfg(not(target_os = "macos"))'.dev-dependencies]
livekit_client = { workspace = true, features = ["test-support"] }

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