Compare commits

..

268 Commits

Author SHA1 Message Date
Nate Butler
2be682338d docs: Redirect old assistant page to assistant/assitant.html 2024-08-22 14:01:59 -04:00
Nate Butler
b50f9f8c5d docs: Flatten out docs nav 2024-08-22 14:01:40 -04:00
Peter Tripp
4e2b08b909 docs: Terminal line_height (#16687)
Closes https://github.com/zed-industries/zed/issues/16686

Release Notes:

- N/A
2024-08-22 13:48:33 -04:00
Conrad Irwin
c697eaba82 Use split direction preferences more (#16679)
Use new split direction preferences in more places (#16345)

Release Notes:

- N/A
2024-08-22 11:13:33 -06:00
Marshall Bowers
93642c9c51 Pass through Anthropic cache configuration when using Zed provider (#16685)
This PR makes it so the model's cache configuration gets passed through
from the base model when using the Zed provider.

Release Notes:

- Fixed caching for Anthropic models when using the Zed provider.
2024-08-22 12:48:47 -04:00
Cherry
25cdd2ad25 Update blade to 7f54ddf to fix compilation error in opengl mode (#16682)
Update blade to latest commit. This fixes a compilation error in zed
when compiling with `RUSTFLAGS="--cfg gles"`.

Closes #16677 

Release Notes:

- N/A
2024-08-22 19:06:42 +03:00
Piotr Osiewicz
182b7af299 ui: Use popover menus for tab bar in panes (#16497)
Closes #ISSUE

Release Notes:

- N/A
2024-08-22 18:05:23 +02:00
Kirill Bulatov
72b5cda356 Deduplicate /tab all buffers inserted (#16681)
Closes https://github.com/zed-industries/zed/issues/16678

Release Notes:

- Fixed `/tab all` inserting duplicate buffers
([!16678](https://github.com/zed-industries/zed/issues/16678))
2024-08-22 19:04:03 +03:00
renovate[bot]
912ed20a3b Update Rust crate clap to v4.5.16 (#16625)
[![Mend
Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com)

This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [clap](https://togithub.com/clap-rs/clap) | workspace.dependencies |
patch | `4.5.15` -> `4.5.16` |

---

### Release Notes

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

###
[`v4.5.16`](https://togithub.com/clap-rs/clap/compare/clap_complete-v4.5.15...clap_complete-v4.5.16)

[Compare
Source](https://togithub.com/clap-rs/clap/compare/v4.5.15...v4.5.16)

</details>

---

### Configuration

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

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

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

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

---

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

---

Release Notes:

- N/A

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

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-08-22 11:46:11 -04:00
renovate[bot]
3d94ed3242 Update serde monorepo to v1.0.208 (#16647)
[![Mend
Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com)

This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [serde](https://serde.rs)
([source](https://togithub.com/serde-rs/serde)) | dependencies | patch |
`1.0.207` -> `1.0.208` |
| [serde](https://serde.rs)
([source](https://togithub.com/serde-rs/serde)) | workspace.dependencies
| patch | `1.0.207` -> `1.0.208` |
| [serde_derive](https://serde.rs)
([source](https://togithub.com/serde-rs/serde)) | workspace.dependencies
| patch | `1.0.207` -> `1.0.208` |

---

### Release Notes

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

###
[`v1.0.208`](https://togithub.com/serde-rs/serde/releases/tag/v1.0.208)

[Compare
Source](https://togithub.com/serde-rs/serde/compare/v1.0.207...v1.0.208)

- Support serializing and deserializing unit structs in a `flatten`
field ([#&#8203;2802](https://togithub.com/serde-rs/serde/issues/2802),
thanks [@&#8203;jonhoo](https://togithub.com/jonhoo))

</details>

---

### Configuration

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

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

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

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

---

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

---

Release Notes:

- N/A

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

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-08-22 11:25:45 -04:00
Jeroen van Baarsen
3a593fe803 Add option to set split direction (#16345)
This adds an option to set the split direction for both the horizontal
splits, and the vertical splits.

A couple of things to look for when reviewing:

* The `derive` keywords on the Enums were copy pasted, no clue what they
should be
* Tried adding tests for this, but got stuck.

Co-authored with @Tobbe

Fixes: https://github.com/zed-industries/zed/issues/11342
2024-08-22 08:53:43 -06:00
renovate[bot]
f08be779c0 Update Rust crate tree-sitter-go to v0.21.2 (#16641)
[![Mend
Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com)

This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [tree-sitter-go](https://togithub.com/tree-sitter/tree-sitter-go) |
workspace.dependencies | patch | `0.21.0` -> `0.21.2` |

---

### Release Notes

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

###
[`v0.21.2`](https://togithub.com/tree-sitter/tree-sitter-go/compare/v0.21.1...v0.21.2)

[Compare
Source](https://togithub.com/tree-sitter/tree-sitter-go/compare/v0.21.1...v0.21.2)

###
[`v0.21.1`](https://togithub.com/tree-sitter/tree-sitter-go/compare/v0.21.0...v0.21.1)

[Compare
Source](https://togithub.com/tree-sitter/tree-sitter-go/compare/v0.21.0...v0.21.1)

</details>

---

### Configuration

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

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

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

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

---

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

---

Release Notes:

- N/A

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

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-08-22 16:49:34 +02:00
Jonathan Dickinson
278864e19f lsp_log: Show messages before init and add filtering (#15893)
Allows language server logs to be published prior to the completion of
the initialize request. OmniSharp is one example of an LSP that
publishes (many) messages prior to the initialization response, and this
completely floods the Zed logs.

Also adds level filtering as demonstrated below. Again, this is due to
my experience with the massive amount of log messages that OmniSharp
publishes.

Release Notes:

- Added level filtering to language server logs

![Log Level
Filtering](https://github.com/user-attachments/assets/e3fcb7af-28cb-4787-9068-8e5e7eb3cf7d)

---------

Co-authored-by: Thorsten Ball <mrnugget@gmail.com>
2024-08-22 16:47:34 +02:00
Tau Gärtli
9245015d1a terminal: Retain relative order of responses (#16456)
Partially addresses #8497 (namely, the occurring with `delta`)

As I mentioned in
https://github.com/zed-industries/zed/issues/8497#issuecomment-2226896371,
zed currently replies to OSC color requests (`OSC 10`, `OSC 11`, ...)
out of order when immediately followed by another request (for example
`CSI c`). All other terminals that [I have
tested](https://github.com/bash/terminal-colorsaurus/blob/main/doc/terminal-survey.md)
maintain relative order when replying to requests.

## Solution
Respond to the `ColorRequest` in `process_event` (in the same place
where other PTY writes happen) instead of queuing it up in the internal
event queue.

## Alternative
I initially thought that I could handle the color request similarly to
the `TextAreaSizeRequest` where the size is stored in `last_content` and
updated on `sync`. However this causes the terminal to report
out-of-date values when a "set color" sequence is followed by a color
request.

## Tests

1. `OSC 11; ?` (request bg color) + `CSI c` (request device attributes):
   ```shell
   printf '\e]11;?\e\\ \e[c' && cat -v
   # Expected result: ^[]11;rgb:dcdc/dcdc/dddd^[\^[[?6c
# Current result: ^[[?6c^[]11;rgb:dcdc/dcdc/dddd^[\ ()
# Result with this PR: ^[]11;rgb:dcdc/dcdc/dddd^[\^[[?6c ()
# Result with alternative: ^[]11;rgb:dcdc/dcdc/dddd^[\^[[?6c ()
   ```
2. `OSC 11; rgb:f0f0/f0f0/f0f0` (set bg color) + `OSC 11; ?` (request bg
color)
   ```shell
   printf '\e]11;rgb:f0f0/f0f0/f0f0\e\\ \e]11;?\e\\' && cat -v
   # Expected result: ^[]11;rgb:f0f0/f0f0/f0f0^[\
# Current result: ^[]11;rgb:f0f0/f0f0/f0f0^[\ ()
# Result with this PR: ^[]11;rgb:f0f0/f0f0/f0f0^[\ ()
# Result with alternative: ^[]11;rgb:OUT_OF_DATE_COLOR_HERE^[\ ()
   ```

Release Notes:

- N/A
2024-08-22 16:19:24 +02:00
Kajus
b7a66e4491 project_panel: Allow copying the paths of multiple selected files at once (#16558)
Closes #16555 

Release Notes:

- Improved the "Copy Path" and "Copy Relative Path" actions in the
project panel's context menu when selecting multiple files. All selected
files' paths will now be copied, separated by newlines.
2024-08-22 16:05:01 +02:00
Thorsten Ball
59dd7c9138 zig: Bump to v0.3.0 (#16669)
This PR bumps the Zig extension to v0.3.0

Changes:

- #16645

Release Notes:

- N/A
2024-08-22 15:56:09 +02:00
renovate[bot]
3c577e1a42 Update Rust crate aws-sdk-s3 to v1.46.0 (#16651)
[![Mend
Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com)

This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [aws-sdk-s3](https://togithub.com/awslabs/aws-sdk-rust) | dependencies
| minor | `1.43.0` -> `1.46.0` |

---

### Configuration

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

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

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

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

---

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

---

Release Notes:

- N/A

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

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-08-22 09:53:42 -04:00
versecafe
bb725d3158 zig: Unpin Zig LSP grab newest version off GH releases, and download from zigtools.org (#16645)
Fixed Zig LSP being pinned to 0.11.0 due to discontinuation of `.tar.gz`

Release Notes:

- N/A
2024-08-22 15:45:04 +02:00
renovate[bot]
5250866c1a Update Rust crate which to v6.0.3 (#16646)
[![Mend
Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com)

This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [which](https://togithub.com/harryfei/which-rs) |
workspace.dependencies | patch | `6.0.2` -> `6.0.3` |

---

### Release Notes

<details>
<summary>harryfei/which-rs (which)</summary>

###
[`v6.0.3`](https://togithub.com/harryfei/which-rs/blob/HEAD/CHANGELOG.md#603)

[Compare
Source](https://togithub.com/harryfei/which-rs/compare/6.0.2...6.0.3)

- Enhance `tracing` feature with some `debug` level logs for higher
level logic.

</details>

---

### Configuration

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

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

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

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

---

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

---

Release Notes:

- N/A

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

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-08-22 15:31:50 +02:00
renovate[bot]
1ae96025f5 Update Rust crate arrayvec to v0.7.6 (#16614)
[![Mend
Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com)

This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [arrayvec](https://togithub.com/bluss/arrayvec) | dependencies | patch
| `0.7.4` -> `0.7.6` |

---

### Release Notes

<details>
<summary>bluss/arrayvec (arrayvec)</summary>

###
[`v0.7.6`](https://togithub.com/bluss/arrayvec/blob/HEAD/CHANGELOG.md#076)

[Compare
Source](https://togithub.com/bluss/arrayvec/compare/0.7.5...0.7.6)

- Fix no-std build
[#&#8203;274](https://togithub.com/bluss/arrayvec/pull/274)

###
[`v0.7.5`](https://togithub.com/bluss/arrayvec/blob/HEAD/CHANGELOG.md#075)

[Compare
Source](https://togithub.com/bluss/arrayvec/compare/0.7.4...0.7.5)

- Add `as_ptr` and `as_mut_ptr` to `ArrayString`
[@&#8203;YuhanLiin](https://togithub.com/YuhanLiin)
[#&#8203;260](https://togithub.com/bluss/arrayvec/pull/260)
- Add borsh serialization support by
[@&#8203;honzasp](https://togithub.com/honzasp) and
[@&#8203;Fuuzetsu](https://togithub.com/Fuuzetsu)
[#&#8203;259](https://togithub.com/bluss/arrayvec/pull/259)
- Move length field before before data in ArrayVec and ArrayString by
[@&#8203;JakkuSakura](https://togithub.com/JakkuSakura)
[#&#8203;255](https://togithub.com/bluss/arrayvec/pull/255)
- Fix miri error for ZST case in extend by
[@&#8203;bluss](https://togithub.com/bluss)
- implement AsRef<Path> for ArrayString by
[@&#8203;Zoybean](https://togithub.com/Zoybean)
[#&#8203;218](https://togithub.com/bluss/arrayvec/pull/218)
- Fix typos in changelog by
[@&#8203;striezel](https://togithub.com/striezel)
[#&#8203;241](https://togithub.com/bluss/arrayvec/pull/241)
- Add `as_slice`, `as_mut_slice` methods to `IntoIter` by
[@&#8203;clarfonthey](https://togithub.com/clarfonthey)
[#&#8203;224](https://togithub.com/bluss/arrayvec/pull/224)

</details>

---

### Configuration

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

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

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

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

---

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

---

Release Notes:

- N/A

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

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-08-22 08:53:27 -04:00
Kirill Bulatov
6b9fa68dc5 Force Vue and Svelte language servers to be the first in the list for their languages (#16654)
Follow-up of https://github.com/zed-industries/zed/pull/15624

Fixes https://github.com/zed-industries/zed/issues/13769
Fixes https://github.com/zed-industries/zed/issues/16469

This way, those are considered "primary" and serve all LSP requests like
go to definition. Before, Tailwind language server was first and
returned nothing for all LSP requests.

- Fixed Vue and Svelte languages integrations not handling LSP requests
properly ([#13769](https://github.com/zed-industries/zed/issues/13769))
([#16469](https://github.com/zed-industries/zed/issues/16469))
2024-08-22 15:36:31 +03:00
Thorsten Ball
db0c1fd592 vim: Add 'gf' command, make files cmd-clickable (#16534)
Release Notes:

- vim: Added `gf` command to open files under the cursor.
- Filenames can now be `cmd`/`ctrl`-clicked, which opens them.

TODOs:

- [x] `main_test.go` <-- works
- [x] `./my-pkg/my_pkg.go` <-- works
- [x] `../go.mod` <-- works
- [x] `my-pkg/my_pkg.go` <-- works
- [x] `my-pkg/subpkg/subpkg_test.go` <-- works
- [x] `file\ with\ space\ in\ it.txt` <-- works
- [x] `"file\ with\ space\ in\ it.txt"` <-- works
- [x] `"main_test.go"` <-- works
- [x] `/Users/thorstenball/.vimrc` <-- works, but only locally
- [x] `~/.vimrc` <--works, but only locally
- [x] Get it working over collab
- [x] Get hover links working

Demo:



https://github.com/user-attachments/assets/26af7f3b-c392-4aaf-849a-95d6c3b00067

Collab demo:




https://github.com/user-attachments/assets/272598bd-0e82-4556-8f9c-ba53d3a95682
2024-08-22 14:27:11 +02:00
Henrikh Kantuni
1e39d407c2 Fix typo (#16657)
`format_on_save` → `formatter`

Release Notes:
- N/A
2024-08-22 14:02:41 +03:00
Kirill Bulatov
61ca36ecad Document proper default value for auto_fold_dirs 2024-08-22 13:56:27 +03:00
Conrad Irwin
eb9eae09b1 Fix manual copilot with show_inline_completions: false (#16621)
For @mre and friends!

Release Notes:

- Fixed manually trigging completions when `show_inline_completions:
false`
2024-08-21 20:27:19 -06:00
Peter Tripp
136f75ee9a docs: Update telemetry documentation (#16628)
- Add references to locations in code for Metrics and Panic telemetry
- Remove outdated documentation (ClickhouseEvent,
ClickhouseEventWrapper, ClickhouseEventRequestBody)
- Migrate struct documentation from web docs to inline doc comments on
struct members.
2024-08-21 20:24:35 -04:00
Danilo Leal
1f8fa82ac3 docs: Add missing link to the Prompt Library page (#16639)
Added in the Command page within the Assistant section.

Release Notes:

- N/A
2024-08-21 20:01:49 -03:00
renovate[bot]
8895084604 Update Rust crate tokio to v1.39.3 (#16634)
[![Mend
Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com)

This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [tokio](https://tokio.rs)
([source](https://togithub.com/tokio-rs/tokio)) | dependencies | patch |
`1.39.2` -> `1.39.3` |
| [tokio](https://tokio.rs)
([source](https://togithub.com/tokio-rs/tokio)) | workspace.dependencies
| patch | `1.39.2` -> `1.39.3` |

---

### Release Notes

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

###
[`v1.39.3`](https://togithub.com/tokio-rs/tokio/releases/tag/tokio-1.39.3):
Tokio v1.39.3

[Compare
Source](https://togithub.com/tokio-rs/tokio/compare/tokio-1.39.2...tokio-1.39.3)

### 1.39.3 (August 17th, 2024)

This release fixes a regression where the unix socket api stopped
accepting the abstract socket namespace. ([#&#8203;6772])

[#&#8203;6772]: https://togithub.com/tokio-rs/tokio/pull/6772

</details>

---

### Configuration

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

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

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

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

---

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

---

Release Notes:

- N/A

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

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-08-21 18:54:46 -04:00
evren
1abbe9c65d Update .mailmap (#16640)
Updated mailmap to contain my correct github noreply mail address.

Release Notes:

- N/A

---------

Co-authored-by: Marshall Bowers <elliott.codes@gmail.com>
2024-08-21 18:51:58 -04:00
Danilo Leal
ec98e71190 docs: Add tweaks to the assistant Configuration page (#16632)
This PR adds some slight writing tweaks to the Configuration page under
the assistant section. As a general rule of thumb, I usually avoid
adding links in the word "here" when that's within a sentence; a more
descriptive approach can be clearer.

---

Release Notes:

- N/A
2024-08-21 19:26:15 -03:00
Marshall Bowers
1d986b0c77 collab: Report active user counts separately, as well (#16629)
This PR adds additional reporting of the active user counts as separate
logs.

We were already reporting these on individual rate limit events/logs,
but it seems like something that would be good to report on independent
of user activity.

Release Notes:

- N/A
2024-08-21 18:15:15 -04:00
Cherry
feab1261c8 Fix some typos (#16623)
This PR fixes some typos I found in the source code.

Release Notes:

- N/A
2024-08-21 17:33:19 -04:00
Michael Angerman
406d3b413d gpui: Remove extra "which" in comment (#16620)
Fix a typo in the comment...

Release Notes:

- N/A
2024-08-21 17:24:38 -04:00
renovate[bot]
643d60f551 Update rui314/setup-mold digest to 0bf4f07 (#16613)
[![Mend
Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com)

This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [rui314/setup-mold](https://togithub.com/rui314/setup-mold) | action |
digest | `2e332a0` -> `0bf4f07` |

---

### Configuration

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

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

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

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

---

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

---

Release Notes:

- N/A

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

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-08-21 17:23:57 -04:00
Marshall Bowers
0229d3ccac collab: Track active user counts independently for each model (#16624)
This PR fixes an issue where the active user count spanned individual
models.

We now track the active user counts on a per-model basis.

Release Notes:

- N/A
2024-08-21 17:19:47 -04:00
Thorben Kröger
f85ca387a7 clangd: Implement switch source/header extension (#14646)
Release Notes:

- Added switch source/header action for clangd language server (fixes
[#12801](https://github.com/zed-industries/zed/issues/12801)).

Note: I'm new to both rust and this codebase. I started my
implementation by copying how rust analyzer's "expand macro" LSP
extension is implemented. I don't yet understand some of the code I
copied (mostly the way to get the `server_to_query` in `clangd_ext.rs`
and the whole proto implementation).

---------

Co-authored-by: Kirill Bulatov <kirill@zed.dev>
2024-08-21 22:15:08 +03:00
Marshall Bowers
96bcceed40 collab: Add traces for user LLM rate limits (#16610)
This PR adds traces for when users hit LLM rate limits.

We were already emitting telemetry events for these to Clickhouse, but
it will be handy to have them available in Axiom as well.

Release Notes:

- N/A
2024-08-21 15:13:55 -04:00
Kyle Kelley
2ad9a742dd repl: Add restart kernel action and improve shutdown (#16609)
- Implement restart kernel functionality
- Clean up shutdown process to properly drop messaging and exit status
tasks
- Refactor kernel state handling for better consistency

Closes #16037

Release Notes:

- repl: Added restart kernel action
- repl: Fixed issue with shutting down kernels that are in a failure
state
2024-08-21 11:51:58 -07:00
Ikko Eltociear Ashimine
9f0438b540 gpui: Remove extra "the" in comment (#16608)
Release Notes:

- N/A
2024-08-21 14:07:51 -04:00
Marshall Bowers
d274be67d6 Mark the user-backfiller secret as optional 2024-08-21 13:25:05 -04:00
Marshall Bowers
19f0c4af6d collab: Update user backfiller to be mindful of GitHub rate limits (#16602)
This PR updates the user backfiller to be mindful of GitHub rate limits
and back off when rate-limited.

Release Notes:

- N/A
2024-08-21 13:23:24 -04:00
Conrad Irwin
09c698d8d7 Fix a panic when diagnostics contain multiple links (#16601)
Follow up from #14518

Release Notes:

- Fixed a panic when diagnostics contain multiple links
2024-08-21 11:18:43 -06:00
Marshall Bowers
8a5fcc2c22 collab: Backfill github_user_created_at on users (#16600)
This PR adds a backfiller to backfill the `github_user_created_at`
column on users.

Release Notes:

- N/A
2024-08-21 12:38:51 -04:00
Thorsten Ball
28568429aa collab panel: Unfocus filter editor on escape (#16579)
This has been bugging me for a while, because it meant I was stuck in
the collab panel when I accidentally navigated there via keyboard
shortcuts.

Now I can press esc and get out of that state.

Release Notes:

- Fixed `esc` not removing focus from the filter editor in the
collaboration panel.

### Before


https://github.com/user-attachments/assets/3bebac03-0e6a-49b0-9823-d9f3190aa5d2


### After


https://github.com/user-attachments/assets/d04c309d-9d1c-44b6-abd3-d48f55207e31
2024-08-21 09:38:12 +02:00
邻二氮杂菲
f1778dd9de Add max_output_tokens to OpenAI models and integrate into requests (#16381)
### Pull Request Title
Introduce `max_output_tokens` Field for OpenAI Models


https://platform.deepseek.com/api-docs/news/news0725/#4-8k-max_tokens-betarelease-longer-possibilities

### Description
This commit introduces a new field `max_output_tokens` to the OpenAI
models, which allows specifying the maximum number of tokens that can be
generated in the output. This field is now integrated into the request
handling across multiple crates, ensuring that the output token limit is
respected during language model completions.

Changes include:
- Adding `max_output_tokens` to the `Custom` variant of the
`open_ai::Model` enum.
- Updating the `into_open_ai` method in `LanguageModelRequest` to accept
and use `max_output_tokens`.
- Modifying the `OpenAiLanguageModel` and `CloudLanguageModel`
implementations to pass `max_output_tokens` when converting requests.
- Ensuring that the `max_output_tokens` field is correctly serialized
and deserialized in relevant structures.

This enhancement provides more control over the output length of OpenAI
model responses, improving the flexibility and accuracy of language
model interactions.

### Changes
- Added `max_output_tokens` to the `Custom` variant of the
`open_ai::Model` enum.
- Updated the `into_open_ai` method in `LanguageModelRequest` to accept
and use `max_output_tokens`.
- Modified the `OpenAiLanguageModel` and `CloudLanguageModel`
implementations to pass `max_output_tokens` when converting requests.
- Ensured that the `max_output_tokens` field is correctly serialized and
deserialized in relevant structures.

### Related Issue
https://github.com/zed-industries/zed/pull/16358

### Screenshots / Media
N/A

### Checklist
- [x] Code compiles correctly.
- [x] All tests pass.
- [ ] Documentation has been updated accordingly.
- [ ] Additional tests have been added to cover new functionality.
- [ ] Relevant documentation has been updated or added.

### Release Notes

- Added `max_output_tokens` field to OpenAI models for controlling
output token length.
2024-08-21 00:39:10 -04:00
Conrad Irwin
36d51fe4a5 vim: Improve lifecycle (#16477)
Closes #13579

A major painpoint in the Vim crate has been life-cycle management. We
used to have one global Vim instance that tried to track per-editor
state; this led to a number of subtle issues (e.g. #13579, the mode
indicator being global, and quick toggling between windows letting vim
mode's notion of the active editor get out of sync).

This PR changes the internal structure of the code so that there is now
one `Vim` instance per `Editor` (stored as an `Addon`); and the global
stuff is separated out. This fixes the above problems, and tidies up a
bunch of the mess in the codebase.

Release Notes:

* vim: Fixed accidental visual mode in project search and go to
references
([#13579](https://github.com/zed-industries/zed/issues/13579)).
2024-08-20 20:48:50 -06:00
Marshall Bowers
c4c07583c3 docs: Black-hole zombie pages 2024-08-20 21:37:41 -04:00
Marshall Bowers
a82cc80d1d docs: Remove context servers documentation (#16560)
This PR removes the docs for context servers.

Release Notes:

- N/A
2024-08-20 21:12:12 -04:00
jvmncs
a3672d96d4 docs: Fix outdated context server setting example (#16545)
Release Notes:

- N/A
2024-08-20 17:28:29 -04:00
Eli Perkins
a1438a49df project_panel: Set scrollbar track background from theme (#16546)
This was previously using the value for the scrollbar border, instead of
the background.

I noticed this while trying out a new Zed theme.

Release Notes:

- Updated project panel scrollbar to respect the
`scrollbar.track.background` from the theme.

| Before | After |
| --- | ---- |
| <img width="281" alt="Screenshot 2024-08-20 at 2 46 23 PM"
src="https://github.com/user-attachments/assets/46d48e75-f472-4060-bcd7-9c2f7d97963d">
| <img width="280" alt="Screenshot 2024-08-20 at 2 46 37 PM"
src="https://github.com/user-attachments/assets/081038a4-4337-4c9f-9a95-93497003fb56">
|
2024-08-20 17:11:07 -04:00
jvmncs
d2a7caa84b docs: Fix last missing links in context servers docs (#16539)
Release Notes:

- N/A
2024-08-20 14:29:28 -04:00
Sinan Gençoğlu
ff7017c308 Replace lazy_static with std::sync::LazyLock (#16066)
Closes #15860 

Since rust std now supports LazyLock replacing lazy_static with it
reduce the external dependency.

Release Notes:

- N/A

---------

Co-authored-by: Marshall Bowers <elliott.codes@gmail.com>
2024-08-20 14:27:33 -04:00
jvmncs
85731dfe8e docs: Fix links in context-servers documentation (#16538)
Release Notes:

- N/A
2024-08-20 14:16:51 -04:00
Marshall Bowers
8381b06d2d gpui: Fix "cursor" typos (#16536)
This PR fixes some typos of the word "cursor" in `gpui`.

Release Notes:

- N/A
2024-08-20 13:49:15 -04:00
jvmncs
a89844bcc9 docs: Document context servers and model context protocol (#16531)
Release Notes:

- N/A
2024-08-20 13:42:46 -04:00
Matthew D. Scholefield
5c0d800b21 Make unnecessary code fade configurable (#14442)
This PR allows configuring the intensity of code fade applied to unused
code (relates to #4785).

_Note: Right now I included it as a top level config which might be a
little out of place (but is easiest to instrument). Open for suggestions
on where else it would be better suited for._

_Note 2: I am unfamiliar with the codebase. Feel free to either close
this PR and re-implement in a better way or suggest high level changes
if I'm approaching anything wrong :)._

Release Notes:

- Added `unnecesary_code_fade` setting to control how strongly to fade
unused code.

---------

Co-authored-by: Marshall Bowers <elliott.codes@gmail.com>
2024-08-20 13:13:27 -04:00
Finn Evers
ebcc2945c5 docs: Remove unneeded theme links in index.hbs (#16532)
PR #16501 introduced the new keybinding syntax for the docs and added
Javascript- and CSS-files to the theme to implement the change. These
new files were then referenced in the `book.toml` as well as added to
the `index.hbs`.

However, the manually added links to these assets in the
[`index.hbs`-file](https://github.com/zed-industries/zed/pull/16501/files#diff-8fde917c42f58487036335ccc9980f1467790fc1c257ec57a0c44bc5dfc3ef4f)
are neither needed nor resolvable:

<img width="328" alt="grafik"
src="https://github.com/user-attachments/assets/4a710b22-9274-4d27-96a8-6da59b0c9e68">


Therefore, this PR reverts the changes to the`index.hbs` - file, as the
correct links to the needed assets are inserted via the
`each`-directives from the `book.toml` respectively.

Release Notes:

- N/A
2024-08-20 12:42:36 -04:00
Bennet Bo Fenner
e884d0060e docs: Fix links in assistant-panel.md (#16529)
Release Notes:

- N/A
2024-08-20 17:59:19 +02:00
Bennet Bo Fenner
0c980cde74 docs: Cleanup assistant configuration documentation (#16526)
Release Notes:

- N/A
2024-08-20 17:40:19 +02:00
Nate Butler
9951df7709 Update some docs keybindings to new format (#16524)
Updates some of the docs pages to the new keybinding format.

Release Notes:

- N/A
2024-08-20 11:23:40 -04:00
Marshall Bowers
936466e02c docs: Reword "Extensibility" section of slash command docs (#16521)
This PR rewords the "Extensibility" section of the slash command docs.

Release Notes:

- N/A
2024-08-20 10:13:53 -04:00
Marshall Bowers
1eb1e16954 docs: Fix possessive "its" typos (#16516)
This PR fixes a number of typos where possessive "its" wasn't being used
properly.

Release Notes:

- N/A
2024-08-20 09:59:29 -04:00
Suhun Han
b67404323c workspace: Improve error handling when dropping a file that cannot be opened into the workspace pane (#15613)
This PR can improve the UX when dropping a file that cannot be opened
into the workspace pane. Previously, nothing happened without any
messages when such error occurred, which could be awkward for users.
Additionally the pane was being split even though the file failed to
open.

Here's a screen recording demonstrating the previous/updated behavior:


https://github.com/user-attachments/assets/cfdf3488-9464-4568-b16a-9b87718bd729

Changes:

- It now displays an error message if a file cannot be opened.
- Updated the logic to first try to open the file. The pane splits only
if the file opening process is successful.

Release Notes:

- Improved error handling when opening files in the workspace pane. An
error message will now be displayed if the file cannot be opened.
- Fixed an issue where unnecessary pane splitting occurred when a file
fails to open.
2024-08-20 15:05:59 +02:00
Bennet Bo Fenner
c251a50e41 assistant: Update docs (#16515)
- Fix links on assistant page to subpages
- Mention the configuration view in the `configuration.md` and document
more settings

Release Notes:

- N/A

---------

Co-authored-by: Piotr <piotr@zed.dev>
2024-08-20 14:42:10 +02:00
Kirill Bulatov
e482fcde5b Fall back to FindAllReferences if GoToDefinition have not navigated (#16512)
Follow-up of https://github.com/zed-industries/zed/pull/9243 

Release Notes:

- N/A

---------

Co-authored-by: Alex Kladov <aleksey.kladov@gmail.com>
2024-08-20 14:56:19 +03:00
Kyle Kelley
f185269d03 repl: Upgrade runtimelib (#16499)
Upgrades runtimelib to bring in some fixes from
https://github.com/runtimed/runtimed/pull/114 and
https://github.com/runtimed/runtimed/pull/113 that work towards
addressing issues interfacing with the Julia kernel.

Release Notes:

- N/A
2024-08-19 22:39:17 -07:00
Nate Butler
1f0dc8b754 Expand assistant docs (#16501)
This PR significantly expands the assistant documentation, breaking it
out into sections, adding examples and further documenting features.

This PR introduces a convention in docs for swapping keybindings for mac
vs linux:

`<kbd>cmd-enter|ctrl-enter</kbd>`

In the above example, the first will be shown for mac, the second for
linux or windows.

TODO:

- [ ] Fix table style (for `/assistant/configuration`)
- [x] Add script to swap keybindings based on platform
- It should take in this format: [`cmd-n` (mac)|`ctrl-n`(linux)] and
return just the correct binding for the viewer's platform.
- [ ] Add image/video assets (non-blocking)

Release Notes:

- Updated assistant documentation
2024-08-19 23:50:09 -04:00
Marshall Bowers
395a68133d Add Postgrest to Docker Compose (#16498)
This PR adds two Postgrest containers—one for the app database and one
for the LLM database—to the Docker Compose cluster.

Also fixed an issue where `postgres_app.conf` and `postgres_llm.conf`
had been switched.

Release Notes:

- N/A
2024-08-19 20:50:45 -04:00
Marshall Bowers
77c08fade5 elixir: Bump to v0.0.8 (#16495)
This PR bumps the Elixir extension to v0.0.8.

Changes:

- #16382

Release Notes:

- N/A
2024-08-19 19:15:41 -04:00
Marshall Bowers
f7f7cd5bb9 repl: Don't prefix free variables with _ (#16494)
This PR is a small refactor to remove the leading `_` for some free
variables, as this unintentionally marks them as unused to the compiler.

While the fields on the struct _are_ unused, the free variables should
participate in usage tracking, as we want to make sure they get stored
on the struct.

Release Notes:

- N/A
2024-08-19 19:15:27 -04:00
Bennet Bo Fenner
6f5674691c assistant: Set default provider to zed.dev (#16454)
Do NOT merge until tomorrow

Release Notes:

- N/A

---------

Co-authored-by: Thorsten <thorsten@zed.dev>
2024-08-19 19:00:38 -04:00
Stanislav Alekseev
8993a9f2ee elixir: Make two more files required by lexical executable (#16382)
I still haven't fixed building dev extensions with rust managed by nix,
so I'd appreciate testing this for me

Release Notes:

- N/A
2024-08-19 18:48:05 -04:00
Joseph T Lyons
9f66f12f7b v0.151.x dev 2024-08-19 18:40:19 -04:00
Peter Tripp
3eb5488c63 Update Terms and Conditions (#16478)
- Update Zed Terms of Use:
  - Rename from 'EULA' / 'Terms and Conditions'
  - Rename 'Zed Network Based Service' to 'Zed Service'
  - 3.3.2 Usage Data (formerly Telemetry Data)
    - Add examples of 'Usage Data'
- Add link to https://zed.dev/docs/telemetry - Explain 'telemetry ID' and user linkage
- 3.3.5 Privacy Policy - Add privacy policy reference - Add link to https://zed.dev/privacy-policy/
  - 5. OWNERSHIP
- Move "You retain all right, title and interest..." from 3.3 Customer Data
    - Additional note that customers retain Intellectual Property rights
- 9. Third Party Services - Add link to https://zed.dev/third-party-terms
- Add Privacy Policy
- Add Subprocessors
- Add Third Party Terms
- Update script/terms/terms.rtf for DMG bundle
2024-08-19 17:08:46 -04:00
Max Brunsfeld
30bfa56a24 Avoid double message header in new contexts, don't expand default prompt (#16490)
Follow-up to https://github.com/zed-industries/zed/pull/16471

* Don't expand the default prompt by default, since it looks strange in
the expanded state
* Don't create two `You` headers by default. Just insert a blank line
after the default prompt.

Release Notes:

- N/A
2024-08-19 12:54:03 -07:00
Roy Williams
0042c24d3c Simplify logic & add UI affordances to show model cache status (#16395)
Release Notes:

- Adds UI affordances to the assistant panel to show which messages have
been cached
- Migrate cache invalidation to be based on `has_edits_since_in_range`
to be smarter and more selective about when to invalidate the cache and
when to fetch.

<img width="310" alt="Screenshot 2024-08-16 at 11 19 23 PM"
src="https://github.com/user-attachments/assets/4ee2d111-2f55-4b0e-b944-50c4f78afc42">

<img width="580" alt="Screenshot 2024-08-18 at 10 05 16 PM"
src="https://github.com/user-attachments/assets/17630a60-7b78-421c-ae39-425246638a12">


I had originally added the lightening bolt on every message and only
added the tooltip warning about editing prior messages on the first
anchor, but thought it looked too busy, so I settled on just annotating
the last anchor.
2024-08-19 12:06:14 -07:00
Marshall Bowers
971db5c6f6 ci: Set the ZED_CLOUD_PROVIDER_ADDITIONAL_MODELS_JSON for builds (#16486)
This PR updates the various GitHub Actions that build Zed binaries to
set the `ZED_CLOUD_PROVIDER_ADDITIONAL_MODELS_JSON` environment variable
from the corresponding secret.

Release Notes:

- N/A
2024-08-19 14:47:20 -04:00
Max Brunsfeld
b5bd8a5c5d Add logic for closed beta LLM models (#16482)
Release Notes:

- N/A

---------

Co-authored-by: Marshall <marshall@zed.dev>
2024-08-19 11:09:52 -07:00
Nate Butler
41fc6d0885 Make providers more clear in model selector (#16480)
Make providers more clear in model selector

Before:

![CleanShot 2024-08-19 at 13 20
36@2x](https://github.com/user-attachments/assets/5b43fa27-4aca-446a-a035-bc8bcb0d9b0e)

After:

![CleanShot 2024-08-19 at 13 20
05@2x](https://github.com/user-attachments/assets/cb961405-b573-42fe-80e1-f3c2ce828ea4)


Release Notes:

- N/A
2024-08-19 13:38:19 -04:00
Bennet Bo Fenner
90897707c3 assistant: Add imports in a single area when using workflows (#16355)
Co-Authored-by: Kirill <kirill@zed.dev>

Release Notes:

- N/A

---------

Co-authored-by: Kirill <kirill@zed.dev>
Co-authored-by: Thorsten <thorsten@zed.dev>
2024-08-19 19:01:45 +02:00
Piotr Osiewicz
7fbea39566 ui: Dismiss popovers when clicking on trigger button (#16476)
Release Notes:

- Clicking on an already-deployed popover menu trigger now hides the
popover menu.
2024-08-19 18:48:57 +02:00
Thorsten Ball
037cf1393c assistant: Undo workflow step when buffer is discarded (#16465)
This fixes a weird bug:

1. Use `/workflow` in assistant
2. Have it generate a step that modifies a file
3. Either (a) select the step in the assistant and have it auto-insert
newlines (b) select "Transform" to have the step applied
4. Close the modified file in the editor ("Discard")
5. Re-open the file
6. BUG: the changes made by assistant are still there!

The reason for the bug is that the assistant keeps references to buffers
and they're not closed/reloaded when closed/reopened.

To fix the bug we now rollback the applied workflow steps when
discarding a buffer.

(This does *not* yet fix the issue where a workflow step inserts a new
buffer into the project/worktree that does not show up on the file
system yet but in `/file` and hangs around until Zed is closed.)


Release Notes:

- N/A

Co-authored-by: Bennet <bennet@zed.dev>
2024-08-19 18:42:49 +02:00
Kirill Bulatov
69aae2037d Display default prompts more elaborately (#16471)
Show them under `User` role instead of a `System` one, and insert them
expanded.

Release Notes:

- N/A
2024-08-19 18:44:52 +03:00
Piotr Osiewicz
bac8e81e73 assistant: Add the "create your command" item (#16467)
This PR adds an extra item to the slash command picker that links users to the doc that teaches how to create a custom one.

Release Notes:

- N/A

---------

Co-authored-by: Danilo Leal <67129314+danilo-leal@users.noreply.github.com>
2024-08-19 12:29:16 -03:00
Marshall Bowers
0bea4d5fa6 theme: Change autocomplete value for ui_font_features and buffer_font_features (#16466)
This PR changes the default value used when autocompleting the
`ui_font_features` and `ui_font_features` settings from `null` to `{}`.

Release Notes:

- N/A
2024-08-19 10:55:25 -04:00
renovate[bot]
4dec7806cb Update Rust crate heed to v0.20.5 (#16464)
[![Mend
Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com)

This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [heed](https://togithub.com/Kerollmops/heed) | workspace.dependencies
| patch | `0.20.4` -> `0.20.5` |

---

### Release Notes

<details>
<summary>Kerollmops/heed (heed)</summary>

###
[`v0.20.5`](https://togithub.com/meilisearch/heed/releases/tag/v0.20.5):
🛁

[Compare
Source](https://togithub.com/Kerollmops/heed/compare/v0.20.4...v0.20.5)

<p align="center"><img width="280px"
src="https://raw.githubusercontent.com/meilisearch/heed/main/assets/heed-pigeon-logo.png"></a></p>
<h1 align="center" >heed</h1>

##### What's Changed
* fix function docs (clippy warnings) by
@&#8203;antonil[https://github.com/meilisearch/heed/pull/273](https://togithub.com/meilisearch/heed/pull/273)ll/273
* fix custom_key_cmp_wrapper being able to unwind to C code (ub) by
@&#8203;antonil[https://github.com/meilisearch/heed/pull/275](https://togithub.com/meilisearch/heed/pull/275)ll/275

##### New Contributors
* @&#8203;antonilol made their first
contributi[https://github.com/meilisearch/heed/pull/273](https://togithub.com/meilisearch/heed/pull/273)ll/273

</details>

---

### Configuration

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

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

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

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

---

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

---

Release Notes:

- N/A

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

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-08-19 10:26:14 -04:00
Marshall Bowers
de41c151c8 collab: Add is_staff to upstream rate limit spans (#16463)
This PR adds the `is_staff` field to the `upstream rate limit` spans.

Since we use different API keys for staff vs non-staff, it will be
useful to break down the rate limits accordingly.

Release Notes:

- N/A
2024-08-19 10:15:25 -04:00
Piotr Osiewicz
56f1ab9459 assistant: Remove "Resolving" text for step resolution and use Transform instead (#16461)
That way, user can click on "Transform" straight away and get it applied
immediately when it's resolved.



https://github.com/user-attachments/assets/08c99804-3841-4eba-a5eb-7066a9f45b47


TODO:
- [x] Tie "Send" button at the bottom into the same behavior

Release Notes:

- N/A
2024-08-19 15:17:04 +02:00
Piotr Osiewicz
911112d94a assistant: Fix toggling slash command menu from toolbar menu (#16459)
Release Notes:

- N/A
2024-08-19 14:47:05 +02:00
Thorsten Ball
e68b2d5ecc assistant panel: Disable send button on config error (#16455)
Release Notes:

- N/A

Co-authored-by: Bennet <bennet@zed.dev>
2024-08-19 11:44:56 +02:00
Thorsten Ball
f651333896 assistant panel: Show if env var with API key is set (#16453)
This makes it easier to debug why resetting a key doesn't work. We now
show when the key is set via an env var and if so, we disable the
reset-key button and instead give instructions.

![screenshot-2024-08-19-11 22
05@2x](https://github.com/user-attachments/assets/6c75dc82-cb61-4661-9647-f77fca8fdf41)


Release Notes:

- N/A

Co-authored-by: Bennet <bennet@zed.dev>
2024-08-19 11:34:58 +02:00
Bennet Bo Fenner
14fa4abce4 assistant: Fix edge case where "Open new context" button would do nothing (#16452)
Co-Authored-by: Thorsten <thorsten@zed.dev>

Release Notes:

- N/A

Co-authored-by: Thorsten <thorsten@zed.dev>
2024-08-19 11:07:04 +02:00
Ryan Hawkins
8a320668ed Add support for GPT-4o in Copilot Chat (#16446)
Release Notes:
- Added support for GPT-4o for Copilot Chat.
2024-08-19 09:03:06 +02:00
Mikayla Maki
86efde4b76 Fixed bugs in workflow step preview (#16445)
Release Notes:

- N/A
2024-08-18 22:18:04 -07:00
Nathan Sobo
43e13df9f3 Add a /perplexity slash command in an extension (#16438)
Release Notes:

- N/A
2024-08-18 16:34:55 -06:00
Nathan Sobo
b9176fe4bb Add custom icon for Anthropic hosted models (#16436)
This commit adds a custom icon for Anthropic hosted models.


![CleanShot 2024-08-18 at 15 40
38@2x](https://github.com/user-attachments/assets/d467ccab-9628-4258-89fc-782e0d4a48d4)
![CleanShot 2024-08-18 at 15 40
34@2x](https://github.com/user-attachments/assets/7efaff9c-6a58-47ba-87ea-e0fe0586fedc)


- Adding a new SVG icon for Anthropic hosted models.
  - The new icon is located at: `assets/icons/ai_anthropic_hosted.svg`
- Updating the LanguageModel trait to include an optional icon method
- Implementing the icon method for CloudModel to return the custom icon
for Anthropic hosted models
- Updating the UI components to use the model-specific icon when
available
- Adding a new IconName variant for the Anthropic hosted icon

We should change the non-hosted icon in some small way to distinguish it
from the hosted version. I duplicated the path for now so we can
hopefully add it for the next release.

Release Notes:

- N/A
2024-08-18 16:07:15 -06:00
Nathan Sobo
11753914d7 Add a setting to show time to first window draw and frames per second in status bar (#16422)
I want to showcase Zed's performance via videos, and this seemed like a
good way to demonstrate it.


https://github.com/user-attachments/assets/f4a5fabc-efe7-4b48-9ba5-719882fdc856

Release Notes:

- On macOS, you can now set assign `performance.show_in_status_bar:
true` in your settings to show the time to the first window draw on
startup and then current FPS of the containing window's renderer.

---------

Co-authored-by: Max Brunsfeld <maxbrunsfeld@gmail.com>
Co-authored-by: Kirill Bulatov <kirill@zed.dev>
Co-authored-by: David Soria Parra <167242713+dsp-ant@users.noreply.github.com>
Co-authored-by: Danny Hua <danny.hua@hey.com>
2024-08-18 15:22:19 -06:00
Danny Hua
6f93b42ecb docs: Fix example extension directory structure (#16424)
Add language-specific subdirectory in example directory structure, since
that's the requisite structure - see `extensions/languages.md`

Release Notes:

- N/A
2024-08-18 07:40:08 -04:00
David Soria Parra
10a996cbc4 context_servers: Fix argument handling (#16402) 2024-08-17 20:04:34 -07:00
Kirill Bulatov
5e6e465294 Show correct number of characters selected (#16420) 2024-08-18 02:24:32 +03:00
Max Brunsfeld
8841d6faad Avoid redundant newline insertion after file command (#16419)
Release Notes:

- Fixed an issue where an extra newline was inserted after running a
`/file` command in the assistant.
2024-08-17 15:10:10 -07:00
Nathan Sobo
c9c5eef8f2 Improve dev experience for built-in prompts (#16413)
When launching Zed from the CLI via `cargo run`, we'll always prompt
load templates from the repo.

This restores behavior that I reverted last night in #16403.

Also, I've improved the `script/prompts link/unlink` workflow for
overriding prompts of your production copy of Zed. Zed now detects when
the overrides directory is created or removed, and does the right thing.
You can link and unlink repeatedly without restarting Zed.

Release Notes:

- N/A
2024-08-17 12:28:53 -06:00
Danilo Leal
7c268d0c6d assistant: Remove meta description from quote selection tooltip (#16412)
The original idea was for the keybinding to be within the description, but given it's already inline with the title, I figure we don't need this anymore—cleaning it up a bit!

--- 

Release Notes:

- N/A
2024-08-17 13:30:32 -03:00
Danilo Leal
e4a591dcbd workflow: Add button to open the step view (#16387)
This PR adds an icon button that appears as you hover over the step header, which allows users to visit the step view.

---

Release Notes:

- N/A
2024-08-17 13:06:34 -03:00
Nathan Sobo
07d5e22cbe Revert changes to inline assist indentation logic and prompt (#16403)
This PR reverts #16145 and subsequent changes.

This reverts commit a515442a36.

We still have issues with our approach to indentation in Python
unfortunately, but this feels like a safer equilibrium than where we
were.

Release Notes:

- Returned to our previous prompt for inline assist transformations,
since recent changes were introducing issues.
2024-08-17 02:24:55 -06:00
Joseph T. Lyons
ebecd7e65f Fix issue with fetching users in seed script (#16393)
Release Notes:

- N/A
2024-08-16 21:51:51 -04:00
Joseph T Lyons
18f0626e08 Update assistant docs to mention inline works in the terminal 2024-08-16 21:13:02 -04:00
Marshall Bowers
3d997e5fd6 collab: Add is_staff to spans (#16389)
This PR adds the `is_staff` field to our LLM spans so that we can
distinguish between staff and non-staff traffic.

Release Notes:

- N/A
2024-08-16 18:42:44 -04:00
Max Brunsfeld
1b1070e0f7 Add tracing needed for LLM rate limit dashboards (#16388)
Release Notes:

- N/A

---------

Co-authored-by: Marshall <marshall@zed.dev>
2024-08-16 17:52:31 -04:00
Joseph T. Lyons
9ef3306f55 Add feature flags to seed script (#16385)
Release Notes:

- N/A
2024-08-16 17:08:44 -04:00
Kyle Kelley
0fdc9d0f05 context_servers: Log errors from detached context server tasks (#16377)
Logged several of the detached tasks that before would silently fail if
the context server wasn't in compliance.

Release Notes:

- N/A
2024-08-16 13:50:19 -07:00
Nathan Sobo
907d76208d Allow display name of custom Anthropic models to be customized (#16376)
Also added some docs for our settings.

Release Notes:

- N/A
2024-08-16 14:02:37 -06:00
Kirill Bulatov
ae9e6a9daa Allow rerunning tasks with unknown termination status (#16374) 2024-08-16 23:00:20 +03:00
Danilo Leal
e36e605c96 assistant: Fine-tune error toast design (#16373)
Just some super small padding and absolute-positioning tweaks. 

---

Release Notes:

- N/A
2024-08-16 16:56:52 -03:00
Marshall Bowers
35cd397a40 collab: Allow enabling feature flags for all users (#16372)
This PR adds a new `enabled_for_all` column to the `feature_flags` table
to allow enabling a feature flag for all users.

Release Notes:

- N/A
2024-08-16 15:17:03 -04:00
Danilo Leal
2180dbdb50 assistant: Add action footer and refine slash command popover (#16360)
- [x] Put the slash command popover on the footer
- [x] Refine the popover (change it to a picker)
- [x] Add more options dropdown on the assistant's toolbar
- [x] Add quote selection button on the footer

---

Release Notes:

- N/A

---------

Co-authored-by: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com>
Co-authored-by: Nate Butler <iamnbutler@gmail.com>
Co-authored-by: Kirill Bulatov <mail4score@gmail.com>
2024-08-16 16:07:42 -03:00
Mikayla Maki
23d56a1a84 Add configuration flow for inline assist button (#16369)
This adds a configuration prompt when using the inline assist button in
the editor.

Release Notes:

- N/A
2024-08-16 11:21:30 -07:00
Marshall Bowers
a9441879c3 collab: Fix writing LLM rate limit events to Clickhouse (#16367)
This PR fixes the writing of LLM rate limit events to Clickhouse.

We had a table in the table name: `llm_rate_limits` instead of
`llm_rate_limit_events`.

I also extracted a helper function to write to Clickhouse so we can use
it anywhere we need to.

Release Notes:

- N/A
2024-08-16 14:03:34 -04:00
Nate Butler
6cfbb54ede Switch icon (#16363)
Updates instances of the `MagicWand` icon to our more recent `Sparkle` /
`ZedAssistant` icon in places where we reference inline assist.

Before:

![CleanShot 2024-08-16 at 13 41
58@2x](https://github.com/user-attachments/assets/67af27a2-a09b-44bb-a8af-2bafcbbd9038)

After:
![CleanShot 2024-08-16 at 13 48
34@2x](https://github.com/user-attachments/assets/229ccc8e-8a93-44c1-abe0-7b6e22ca93e2)


Release Notes:

- Updated inline assist icon in the editor & terminal.
2024-08-16 14:01:56 -04:00
Marshall Bowers
7a5acc0b0c collab: Rework model name checks (#16365)
This PR reworks how we do checks for model names in the LLM service.

We now normalize the model names using the models defined in the
database.

Release Notes:

- N/A
2024-08-16 13:54:28 -04:00
Joseph T Lyons
463ac7f5e4 Correct H1 text for assistant documentation 2024-08-16 13:52:41 -04:00
Joseph T. Lyons
ee27114b35 Remove redundant assistant content (#16364)
Release Notes:

- N/A
2024-08-16 13:42:16 -04:00
Joseph T. Lyons
ebac9a7342 Combine assistant documentation (#16362)
Release Notes:

- N/A
2024-08-16 13:37:54 -04:00
Mikayla Maki
455850505f Fix more bugs in files (#16241)
Fixes:
- [x] an issue where directories would only match by prefix, causing
both a directory and a file to be matched if in the same directory
- [x] An issue where you could not continue a file completion when
selecting a directory, as `tab` on a file would always run the command.
This effectively disabled directory sub queries.
- [x] Inconsistent rendering of files and directories in the slash
command

Release Notes:

- N/A

---------

Co-authored-by: max <max@zed.dev>
2024-08-16 10:09:38 -07:00
jvmncs
a3a6ebcf31 Small fixes to content generation prompt (#16359)
Fixed the output format section of the content_prompt.hbs template
getting rendered away by handlebars. Also fixed a leftover hardcoded
"Rust" in the rewrite section snippet. (follow-up to #16333)

Release Notes:

- N/A
2024-08-16 12:31:38 -04:00
Roy Williams
b4f5f5024e Support 8192 output tokens for Claude Sonnet 3.5 (#16358)
Release Notes:

- Added support for 8192 output tokens from Claude Sonnet 3.5
(https://x.com/alexalbert__/status/1812921642143900036)
2024-08-16 11:47:39 -04:00
Kirill Bulatov
7eab57a264 Add a task for running zed in "local release" mode (#16357)
Release Notes:

- N/A
2024-08-16 17:33:04 +03:00
Kirill Bulatov
f1a2638d29 Do not enable copilot for always read-only editors (#16356)
Release Notes:

- N/A
2024-08-16 17:22:47 +03:00
Kirill Bulatov
0fe22f2a48 After streaming generation is over, show a regular, batch diff in the file altered (#16350)
Release Notes:

- N/A
2024-08-16 15:31:02 +03:00
Kyle Kelley
f7f5a25584 repl: Apply border for error output on left (#16334) 2024-08-16 00:31:46 -05:00
张小白
40d97fd346 windows: Fix missing title bar on prompt library (#16302)
Closes #16297

It seems that currently we can't draw custom title bar. I have checked
the `title_bar` crate, it seems to be `zed` only.

Before:

![Screenshot 2024-08-16
004350](https://github.com/user-attachments/assets/e11aa0bb-9d3e-47d5-b488-d3b8220158cc)

After:

![Screenshot 2024-08-16
004235](https://github.com/user-attachments/assets/028b4eb2-c878-4ea7-87e3-22817caefa00)


Release Notes:

- N/A
2024-08-15 22:23:11 -06:00
Nathan Sobo
ad44b459cd Improve content generation prompt to reduce over-generation (#16333)
I focused on cases where we're inserting doc comments or annotations
above symbols.

I added 5 new examples to the content generation prompt, covering
various scenarios:

1. Inserting documentation for a Rust struct
2. Writing docstrings for a Python class
3. Adding comments to a TypeScript method
4. Adding a derive attribute to a Rust struct
5. Adding a decorator to a Python class

These examples demonstrate how to handle different languages and common
tasks like adding documentation, attributes, and decorators.

To improve context integration, I've made the following changes:

1. Added a `transform_context_range` that includes 3 lines before and
after the transform range
2. Introduced `rewrite_section_prefix` and `rewrite_section_suffix` to
provide more context around the section being rewritten
3. Updated the prompt template to include this additional context in a
separate code snippet

Release Notes:

- Reduced instances of over-generation when inserting docs or
annotations above a symbol.
2024-08-15 22:20:11 -06:00
Kyle Kelley
bac39d7743 assistant: Only push text content if not empty with image content (#16270)
If you submit an image with empty space above it and text below, it will
fail with this error:


![image](https://github.com/user-attachments/assets/a4a2265e-815f-48b5-b09e-e178fce82ef7)

Now instead it fails with an error about needing a message.

<img width="640" alt="image"
src="https://github.com/user-attachments/assets/72b267eb-b288-40a5-a829-750121ff16cc">

It will however work with text above and empty text below the image now.

Release Notes:

- Improved conformance with Anthropic Images in Chat Completions API
2024-08-15 22:38:52 -05:00
Roy Williams
46fb917e02 Implement Anthropic prompt caching (#16274)
Release Notes:

- Adds support for Prompt Caching in Anthropic. For models that support
it this can dramatically lower cost while improving performance.
2024-08-15 22:21:06 -05:00
Max Brunsfeld
09b6e3f2a6 Improve workflow step view (#16329)
* Improve the tab title: give it an icon, and indicate the step index.
* Display the line number ranges that the symbols resolve to.
* Don't open duplicate tabs for the same step

Release Notes:

- N/A
2024-08-15 17:45:23 -07:00
Max Brunsfeld
364a58a262 Move context tests into their own file (#16327)
This makes it easier to use the outline view to navigate `context.rs`,
and reduces the indentation level of the tests.

Release Notes:

- N/A
2024-08-15 17:14:05 -07:00
Max Brunsfeld
c896ff292c Remove workflow inspector, clean up workflow code (#16325)
Now that there's a dedicated, user-facing view for each workflow step,
we don't need the inspector functionality. This PR also cleans up some
naming around workflow steps and step resolutions.

Release Notes:

- N/A
2024-08-15 16:47:29 -07:00
Kyle Kelley
da2bfbd29f repl: Scale the text_style font_size and line_height (#16308)
Replaces #16273.

Release Notes:

- repl: Fixed scaling of stdout/stderr line heights

---------

Co-authored-by: Mikayla <mikayla@zed.dev>
Co-authored-by: Nate Butler <iamnbutler@gmail.com>
2024-08-15 18:46:36 -05:00
Joseph T. Lyons
0b407164d0 Update assistant docs (#16324)
Release Notes:

- N/A
2024-08-15 18:42:13 -04:00
Marshall Bowers
b151241d84 assistant: Improve the empty state for the prompt library (#16320)
This PR improves the empty state of the prompt library.

The right-hand side of the library is now dedicated to an empty state
that guides the user to create their first prompt.

Additionally, the message in the picker now reads "No prompts." when
there are no prompts.

#### No prompts

<img width="1136" alt="Screenshot 2024-08-15 at 6 20 26 PM"
src="https://github.com/user-attachments/assets/f9af2b5d-c4d3-4e2c-9ba2-f17e89f19bb7">

#### No prompts that match the search

<img width="1136" alt="Screenshot 2024-08-15 at 5 55 07 PM"
src="https://github.com/user-attachments/assets/2cd4ff9b-958d-4bd0-90d3-dca62b1a91a0">

Release Notes:

- N/A
2024-08-15 18:28:17 -04:00
Marshall Bowers
f65b2b9a2d assistant: Fix toggling the model selector via keybind (#16319)
This PR restores the ability to toggle the model selector via a keybind
after it was lost in #15693.

Release Notes:

- Restored the ability to toggle the model selector in the Assistant via
a keybinding (Preview only).
2024-08-15 17:45:25 -04:00
Max Brunsfeld
776442f3ae Add a workflow step resolution view (#16315)
You can now click on a step header (the words `Step 3`, etc) to open a
new tab containing a dedicated view for the resolution of that step.
This view looks similar to a context editor, and has sections for the
step input, the streaming tool output, and the interpreted results.

Hitting `cmd-enter` in this view re-resolves the step.


https://github.com/user-attachments/assets/64d82cdb-e70f-4204-8697-b30df5a645d5



Release Notes:

- N/A

---------

Co-authored-by: Nathan <nathan@zed.dev>
2024-08-15 14:16:58 -07:00
Marshall Bowers
583959f82a collab: Add support for reading boolean values from .env.toml (#16317)
This PR adds support for reading boolean values from `.env.toml`, since
it wasn't supported previously.

Release Notes:

- N/A
2024-08-15 17:07:17 -04:00
Marshall Bowers
9233418cb8 collab: Attach GitHub login to LLM spans (#16316)
This PR updates the LLM service to include the GitHub login on its
spans.

We need to pass this information through on the LLM token, so it will
temporarily be `None` until this change is deployed and new tokens have
been issued.

Release Notes:

- N/A
2024-08-15 17:06:20 -04:00
Marshall Bowers
df20bae80e danger: Don't look for #NNNN, as it's not specific enough (#16313)
This PR updates the regex we use to search for issues to not search for
`#NNNN`, as it's not specific enough.

It currently catches issue numbers from other repos, which are then
linked to random Zed issues/PRs that happen to have the same number:

<img width="935" alt="Screenshot 2024-08-15 at 3 50 29 PM"
src="https://github.com/user-attachments/assets/b779e503-3027-43e2-b355-e81d8d094694">

As well as catching PRs:

<img width="924" alt="Screenshot 2024-08-15 at 3 48 59 PM"
src="https://github.com/user-attachments/assets/6c2f7594-9234-4454-97da-5a33a1844892">

Given that:

1. We can't distinguish any given `#NNNN` as an issue _and_ can't ensure
it belongs to the Zed repo
2. Any issue/PR referenced as `#NNNN` will already create a backlink

It seems that looking for these is causing more noise than signal.

Release Notes:

- N/A
2024-08-15 15:57:42 -04:00
Marshall Bowers
cb423bcb6f Remove tooltip_text from extension manifests (#16312)
This PR removes the `tooltip_text` from the extension manifests.

We stopped reading this value in #16306, as it wasn't being used, so we
don't need to include it in the manifest anymore.

Release Notes:

- N/A
2024-08-15 15:54:53 -04:00
Marshall Bowers
5e05821d18 collab: Attach user_id to LLM spans (#16311)
This PR updates the LLM service to attach the user ID to the spans.

Release Notes:

- N/A
2024-08-15 15:49:12 -04:00
Kirill Bulatov
ff83e5b55a Improve workflow suggestion steps and debug info (#16309)
Release Notes:

- N/A

---------

Co-authored-by: Nathan Sobo <nathan@zed.dev>
Co-authored-by: Bennet Bo Fenner <bennet@zed.dev>
2024-08-15 22:46:19 +03:00
Max Brunsfeld
6b7664ef4a Fix bugs preventing non-staff users from using LLM service (#16307)
- db deadlock in GetLlmToken for non-staff users
- typo in allowed model name for non-staff users

Release Notes:

- N/A

---------

Co-authored-by: Marshall <marshall@zed.dev>
Co-authored-by: Joseph <joseph@zed.dev>
2024-08-15 11:21:19 -07:00
Marshall Bowers
931883aca9 extension: Remove tooltip_text from SlashCommandManifestEntry (#16306)
This PR removes the `tooltip_text` field from
`SlashCommandManifestEntry`s.

The `tooltip_text` is currently only used to set the `menu_text` on a
slash command, which is only used for featured slash commands.

Since slash commands from extensions are not currently able to be
featured, we don't need extension authors to provide this field in the
manifest.

This is a backwards-compatible change.

Release Notes:

- N/A
2024-08-15 13:25:55 -04:00
Marshall Bowers
e982ff7b9e zed_extension_api: Start a list of pending changes (#16305)
This PR starts a list of pending changes for the Zed extension API.

We'll want to keep this list updated as we note things that we want to
change in the next version of the extension API. This will help with
batching breaking changes together so that we're not constantly creating
new versions of the extension API for one-off changes.

Release Notes:

- N/A
2024-08-15 13:10:46 -04:00
Bennet Bo Fenner
0b3e5b2649 assistant: Support retrying empty workflow step (#16301)
Co-Authored-by: Nathan <nathan@zed.dev>
Co-Authored-by: Kirill <kirill@zed.dev>

Release Notes:

- N/A

Co-authored-by: Nathan <nathan@zed.dev>
Co-authored-by: Kirill <kirill@zed.dev>
2024-08-15 19:05:30 +02:00
Marshall Bowers
7434b56e68 docs: Link to slash-commands-example extension (#16304)
This PR updates the slash command extension docs to link to the
`slash-commands-example` extension, for a quick start.

Release Notes:

- N/A
2024-08-15 13:00:31 -04:00
Marshall Bowers
b764174e8b docs: Add docs for defining slash commands in extensions (#16303)
This PR adds docs for defining slash commands within extensions.

Release Notes:

- N/A
2024-08-15 12:50:30 -04:00
Kirill Bulatov
c45adce2e3 Run slash commands both on enter and on argument completion that requires it (#16283)
Release Notes:

- N/A
2024-08-15 19:36:30 +03:00
Marshall Bowers
5a30e29848 Add example extension to showcase slash commands (#16300)
This PR adds an example extension to showcase how to write slash
commands in an extension.

Release Notes:

- N/A
2024-08-15 12:26:13 -04:00
Marshall Bowers
03b843ebf3 live_kit_client: Suppress clippy::arc_with_non_send_sync (#16298)
This PR suppresses the
[`clippy::arc_with_non_send_sync`](https://rust-lang.github.io/rust-clippy/master/index.html#/arc_with_non_send_sync),
as there were some warnings that would—only sometimes—show up when
running Clippy.

Release Notes:

- N/A
2024-08-15 11:46:43 -04:00
David Soria Parra
02ea6ac845 context_servers: Add initial implementation (#16103)
This commit proposes the addition of "context serveres" and the
underlying protocol (model context protocol). Context servers allow
simple definition of slash commands in another language and running
local on the user machines. This aims to quickly prototype new commands,
and provide a way to add personal (or company wide) customizations to
the assistant panel, without having to maintain an extension. We can
use this to reuse our existing codebase, with authenticators, etc and
easily have it provide context into the assistant panel.

As such it occupies a different design space as extensions, which I
think are
more aimed towards long-term, well maintained pieces of code that can be
easily distributed.

It's implemented as a central crate for easy reusability across the
codebase
and to easily hook into the assistant panel at all points.

Design wise there are a few pieces:
1. client.rs: A simple JSON-RPC client talking over stdio to a spawned
server. This is
very close to how LSP work and likely there could be a combined client
down the line.
2. types.rs: Serialization and deserialization client for the underlying
model context protocol.
3. protocol.rs: Handling the session between client and server.
4. manager.rs: Manages settings and adding and deleting servers from a
central pool.

A server can be defined in the settings.json as:

```
"context_servers": [
   {"id": "test", "executable": "python", "args": ["-m", "context_server"]
]
```

## Quick Example
A quick example of how a theoretical backend site can look like. With
roughly 100 lines
of code (nicely generated by Claude) and a bit of decorator magic (200
lines in total), one
can come up with a framework that makes it as easy as:

```python
@context_server.slash_command(name="rot13", description="Perform a rot13 transformation")
@context_server.argument(name="input", type=str, help="String to rot13")
async def rot13(input: str) -> str:
    return ''.join(chr((ord(c) - 97 + 13) % 26 + 97) if c.isalpha() else c for c in echo.lower())
```

to define a new slash_command.

## Todo:
 - Allow context servers to be defined in workspace settings.
 - Allow passing env variables to context_servers


Release Notes:

- N/A

---------

Co-authored-by: Marshall Bowers <elliott.codes@gmail.com>
2024-08-15 10:49:30 -04:00
renovate[bot]
d54818fd9e Update 2428392/gh-truncate-string-action action to v1.4.0 (#16263)
[![Mend
Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com)

This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
|
[2428392/gh-truncate-string-action](https://togithub.com/2428392/gh-truncate-string-action)
| action | minor | `v1.3.0` -> `v1.4.0` |

---

### Release Notes

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

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

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

#### What's Changed

- feat: update nodejs to version 20 by
[@&#8203;psilore](https://togithub.com/psilore) in
[https://github.com/2428392/gh-truncate-string-action/pull/11](https://togithub.com/2428392/gh-truncate-string-action/pull/11)

#### New Contributors

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

**Full Changelog**:
https://github.com/2428392/gh-truncate-string-action/compare/v1.3.0...v1.4.0

</details>

---

### Configuration

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

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

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

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

---

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

---

Release Notes:

- N/A

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

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-08-15 10:38:06 -04:00
renovate[bot]
add0f0dbe5 Update Rust crate async-stripe to 0.38 (#16264)
[![Mend
Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com)

This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [async-stripe](https://togithub.com/arlyon/async-stripe) |
workspace.dependencies | minor | `0.37` -> `0.38` |

---

### Release Notes

<details>
<summary>arlyon/async-stripe (async-stripe)</summary>

###
[`v0.38.1`](https://togithub.com/arlyon/async-stripe/blob/HEAD/CHANGELOG.md#0381-2024-08-06)

[Compare
Source](https://togithub.com/arlyon/async-stripe/compare/v0.38.0...v0.38.1)

##### Bug Fixes

- [#&#8203;578](https://togithub.com/arlyon/async-stripe/issues/578)
allow arbitrary strings for priceId
([a16bc6e](a16bc6e80c))

###
[`v0.38.0`](https://togithub.com/arlyon/async-stripe/blob/HEAD/CHANGELOG.md#0380-2024-07-31)

[Compare
Source](https://togithub.com/arlyon/async-stripe/compare/v0.37.3...v0.38.0)

##### Features

- add support for TestClock operations
([d792798](d792798c3f)),
closes
[#&#8203;574](https://togithub.com/arlyon/async-stripe/issues/574)

####
[0.37.3](https://togithub.com/arlyon/async-stripe/compare/v0.37.2...v0.37.3)
(2024-07-29)

##### Bug Fixes

- linting issue for Rust 1.80
([9232213](9232213c06))

####
[0.37.2](https://togithub.com/arlyon/async-stripe/compare/v0.37.1...v0.37.2)
(2024-07-23)

##### Bug Fixes

- rtx id prefix
([67ea232](67ea2325ba))

####
[0.37.1](https://togithub.com/arlyon/async-stripe/compare/v0.37.0...v0.37.1)
(2024-05-24)

##### Bug Fixes

- Leftover clippy warnings
([888307d](888307d23d))
- Run clippy on openapi generator
([c63c197](c63c197e7c))

</details>

---

### Configuration

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

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

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

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

---

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

---

Release Notes:

- N/A

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

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-08-15 09:50:59 -04:00
renovate[bot]
506b7198c2 Update Rust crate serde_json to v1.0.125 (#16253)
[![Mend
Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com)

This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [serde_json](https://togithub.com/serde-rs/json) | dependencies |
patch | `1.0.122` -> `1.0.125` |
| [serde_json](https://togithub.com/serde-rs/json) |
workspace.dependencies | patch | `1.0.122` -> `1.0.125` |

---

### Release Notes

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

###
[`v1.0.125`](https://togithub.com/serde-rs/json/releases/tag/1.0.125)

[Compare
Source](https://togithub.com/serde-rs/json/compare/v1.0.124...1.0.125)

- Speed up \uXXXX parsing and improve handling of unpaired surrogates
when deserializing to bytes
([#&#8203;1172](https://togithub.com/serde-rs/json/issues/1172),
[#&#8203;1175](https://togithub.com/serde-rs/json/issues/1175), thanks
[@&#8203;purplesyringa](https://togithub.com/purplesyringa))

###
[`v1.0.124`](https://togithub.com/serde-rs/json/releases/tag/v1.0.124)

[Compare
Source](https://togithub.com/serde-rs/json/compare/v1.0.123...v1.0.124)

- Fix a bug in processing string escapes in big-endian architectures
([#&#8203;1173](https://togithub.com/serde-rs/json/issues/1173), thanks
[@&#8203;purplesyringa](https://togithub.com/purplesyringa))

###
[`v1.0.123`](https://togithub.com/serde-rs/json/releases/tag/v1.0.123)

[Compare
Source](https://togithub.com/serde-rs/json/compare/v1.0.122...v1.0.123)

- Optimize string parsing by applying SIMD-within-a-register: 30.3%
improvement on
[twitter.json](https://togithub.com/miloyip/nativejson-benchmark/blob/v1.0.0/data/twitter.json)
from 613 MB/s to 799 MB/s
([#&#8203;1161](https://togithub.com/serde-rs/json/issues/1161), thanks
[@&#8203;purplesyringa](https://togithub.com/purplesyringa))

</details>

---

### Configuration

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

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

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

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

---

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

---

Release Notes:

- N/A

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

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-08-15 09:22:05 -04:00
Marshall Bowers
e10f2d8deb danger: Use a regular message for notice about GitHub Issue links (#16287)
This PR make it so Danger will use a regular message—as opposed to a
warning—for notices about GitHub issue links.

There are still some false-positives getting flagged, so showing a
warning is a bit too aggressive.

Release Notes:

- N/A
2024-08-15 09:21:50 -04:00
renovate[bot]
3c8d890702 Update Rust crate heed to v0.20.4 (#16250)
[![Mend
Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com)

This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [heed](https://togithub.com/Kerollmops/heed) | workspace.dependencies
| patch | `0.20.3` -> `0.20.4` |

---

### Release Notes

<details>
<summary>Kerollmops/heed (heed)</summary>

###
[`v0.20.4`](https://togithub.com/meilisearch/heed/releases/tag/v0.20.4):
🛁

[Compare
Source](https://togithub.com/Kerollmops/heed/compare/v0.20.3...v0.20.4)

<p align="center"><img width="280px"
src="https://raw.githubusercontent.com/meilisearch/heed/main/assets/heed-pigeon-logo.png"></a></p>
<h1 align="center" >heed</h1>

##### What's Changed
* Use features that actually exists by
@&#8203;Kerollmo[https://github.com/meilisearch/heed/pull/270](https://togithub.com/meilisearch/heed/pull/270)ll/270
* Bump lmdb submodules commit by
@&#8203;zanavis[https://github.com/meilisearch/heed/pull/269](https://togithub.com/meilisearch/heed/pull/269)ll/269

##### New Contributors
* @&#8203;zanaviska made their first
contributi[https://github.com/meilisearch/heed/pull/269](https://togithub.com/meilisearch/heed/pull/269)ll/269

</details>

---

### Configuration

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

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

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

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

---

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

---

Release Notes:

- N/A

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

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-08-15 08:59:10 -04:00
renovate[bot]
05f7583444 Update aws-sdk-rust monorepo (#16257)
[![Mend
Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com)

This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [aws-config](https://togithub.com/smithy-lang/smithy-rs) |
dependencies | patch | `1.5.4` -> `1.5.5` |
| [aws-sdk-s3](https://togithub.com/awslabs/aws-sdk-rust) | dependencies
| minor | `1.42.0` -> `1.43.0` |

---

### Configuration

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

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

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

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

---

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

---

Release Notes:

- N/A

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

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-08-15 08:56:44 -04:00
renovate[bot]
e6ed97051d Update serde monorepo to v1.0.207 (#16259)
[![Mend
Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com)

This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [serde](https://serde.rs)
([source](https://togithub.com/serde-rs/serde)) | dependencies | patch |
`1.0.204` -> `1.0.207` |
| [serde](https://serde.rs)
([source](https://togithub.com/serde-rs/serde)) | workspace.dependencies
| patch | `1.0.204` -> `1.0.207` |
| [serde_derive](https://serde.rs)
([source](https://togithub.com/serde-rs/serde)) | workspace.dependencies
| patch | `1.0.204` -> `1.0.207` |

---

### Release Notes

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

###
[`v1.0.207`](https://togithub.com/serde-rs/serde/releases/tag/v1.0.207)

[Compare
Source](https://togithub.com/serde-rs/serde/compare/v1.0.206...v1.0.207)

- Improve interactions between `flatten` attribute and
`skip_serializing`/`skip_deserializing`
([#&#8203;2795](https://togithub.com/serde-rs/serde/issues/2795), thanks
[@&#8203;Mingun](https://togithub.com/Mingun))

###
[`v1.0.206`](https://togithub.com/serde-rs/serde/releases/tag/v1.0.206)

[Compare
Source](https://togithub.com/serde-rs/serde/compare/v1.0.205...v1.0.206)

- Improve support for `flatten` attribute inside of enums
([#&#8203;2567](https://togithub.com/serde-rs/serde/issues/2567), thanks
[@&#8203;Mingun](https://togithub.com/Mingun))

###
[`v1.0.205`](https://togithub.com/serde-rs/serde/releases/tag/v1.0.205)

[Compare
Source](https://togithub.com/serde-rs/serde/compare/v1.0.204...v1.0.205)

- Use serialize_entry instead of serialize_key + serialize_value when
serialize flattened newtype enum variants
([#&#8203;2785](https://togithub.com/serde-rs/serde/issues/2785), thanks
[@&#8203;Mingun](https://togithub.com/Mingun))
- Avoid triggering a collection_is_never_read lint in the
deserialization of enums containing flattened fields
([#&#8203;2791](https://togithub.com/serde-rs/serde/issues/2791))

</details>

---

### Configuration

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

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

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

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

---

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

---

Release Notes:

- N/A

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

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-08-15 08:55:33 -04:00
Max Brunsfeld
e0cabbd142 Make WorkflowStepResolution an entity (#16268)
This PR is just a refactor, to pave the way toward adding a view for
workflow step resolution. The entity carries the state of the tool
call's streaming output.

Release Notes:

- N/A
2024-08-14 22:44:44 -07:00
Danilo Leal
102796979b assistant: Fine-tune workflow step header design (#16272)
- Removes layout shift when buttons appear after transformation resolution
- Refine icons and button colors

Release Notes:

- N/A
2024-08-15 01:28:54 -03:00
Max Brunsfeld
4c390b82fb Make LanguageModel::use_any_tool return a stream of chunks (#16262)
This PR is a refactor to pave the way for allowing the user to view and
edit workflow step resolutions. I've made tool calls work more like
normal streaming completions for all providers. The `use_any_tool`
method returns a stream of strings (which contain chunks of JSON). I've
also done some minor cleanup of language model providers in general,
removing the duplication around handling streaming responses.

Release Notes:

- N/A
2024-08-14 18:02:46 -07:00
Marshall Bowers
1117d89057 zig: Bump to v0.2.0 (#16261)
This PR bumps the Zig extension to v0.2.0.

Changes:

- https://github.com/zed-industries/zed/pull/16260

Release Notes:

- N/A
2024-08-14 18:57:43 -04:00
Marshall Bowers
0df4d12234 zig: Upgrade zed_extension_api to v0.1.0 (#16260)
This PR updates the Zig extension to use v0.1.0 of the Zed extension
API.

This allows us to pin ZLS to v0.11.0, as the more recent releases of ZLS
don't have `.tar.gz` assets available.

Release Notes:

- N/A
2024-08-14 18:40:27 -04:00
Richard Feldman
796cba9e0e Improve workflow prompt, accept nonexistent directories from workflows (#16251)
Release Notes:

- Workflows can now create new files in folders that didn't exist

---------

Co-authored-by: jvmncs <7891333+jvmncs@users.noreply.github.com>
2024-08-14 16:18:41 -06:00
Marshall Bowers
a6461f90a1 gleam: Bump to v0.2.0 (#16258)
This PR bumps the Gleam extension to v0.2.0.

Changes:

- Added `/gleam-project` slash command
- Added `gleam-hexdocs` provider for the `/docs` slash command
- https://github.com/zed-industries/zed/pull/12221
- https://github.com/zed-industries/zed/pull/15659

Release Notes:

- N/A
2024-08-14 18:14:19 -04:00
Marshall Bowers
2e1750d5e2 gleam: Switch to published version of zed_extension_api (#16256)
This PR updates the Gleam extension to use the published version of the
`zed_extension_api`.

Release Notes:

- N/A
2024-08-14 18:04:05 -04:00
Marshall Bowers
18aff55f34 zed_extension_api: Release v0.1.0 (#16254)
This PR releases v0.1.0 of the Zed extension API.

Release Notes:

- N/A
2024-08-14 17:33:17 -04:00
renovate[bot]
ec062cfe66 Update Rust crate linkme to v0.3.28 (#16252)
[![Mend
Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com)

This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [linkme](https://togithub.com/dtolnay/linkme) | dependencies | patch |
`0.3.27` -> `0.3.28` |

---

### Release Notes

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

### [`v0.3.28`](https://togithub.com/dtolnay/linkme/releases/tag/0.3.28)

[Compare
Source](https://togithub.com/dtolnay/linkme/compare/0.3.27...0.3.28)

-   Documentation improvements

</details>

---

### Configuration

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

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

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

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

---

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

---

Release Notes:

- N/A

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

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-08-14 17:17:01 -04:00
renovate[bot]
e39671cad5 Update Rust crate ctrlc to v3.4.5 (#16248)
[![Mend
Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com)

This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [ctrlc](https://togithub.com/Detegr/rust-ctrlc) | dependencies | patch
| `3.4.4` -> `3.4.5` |

---

### Release Notes

<details>
<summary>Detegr/rust-ctrlc (ctrlc)</summary>

###
[`v3.4.5`](https://togithub.com/Detegr/rust-ctrlc/compare/3.4.4...3.4.5)

[Compare
Source](https://togithub.com/Detegr/rust-ctrlc/compare/3.4.4...3.4.5)

</details>

---

### Configuration

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

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

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

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

---

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

---

Release Notes:

- N/A

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

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-08-14 17:05:38 -04:00
renovate[bot]
ffaecbaa0c Update Rust crate core-foundation-sys to v0.8.7 (#16244)
[![Mend
Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com)

This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [core-foundation-sys](https://togithub.com/servo/core-foundation-rs) |
dependencies | patch | `0.8.6` -> `0.8.7` |
| [core-foundation-sys](https://togithub.com/servo/core-foundation-rs) |
workspace.dependencies | patch | `0.8.6` -> `0.8.7` |

---

### Release Notes

<details>
<summary>servo/core-foundation-rs (core-foundation-sys)</summary>

###
[`v0.8.7`](https://togithub.com/servo/core-foundation-rs/compare/core-foundation-sys-v0.8.6...core-foundation-sys-v0.8.7)

[Compare
Source](https://togithub.com/servo/core-foundation-rs/compare/core-foundation-sys-v0.8.6...core-foundation-sys-v0.8.7)

</details>

---

### Configuration

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

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

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

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

---

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

---

Release Notes:

- N/A

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

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-08-14 16:57:38 -04:00
renovate[bot]
7a693235a5 Update Rust crate clap to v4.5.15 (#16243)
[![Mend
Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com)

This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [clap](https://togithub.com/clap-rs/clap) | workspace.dependencies |
patch | `4.5.13` -> `4.5.15` |

---

### Release Notes

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

###
[`v4.5.15`](https://togithub.com/clap-rs/clap/blob/HEAD/CHANGELOG.md#4515---2024-08-10)

[Compare
Source](https://togithub.com/clap-rs/clap/compare/v4.5.14...v4.5.15)

##### Compatiblity

-   *(unstable-ext)* `Arg::remove` changed return types

##### Fixes

-   *(unstable-ext)* Make `Arg::remove` return the removed item

###
[`v4.5.14`](https://togithub.com/clap-rs/clap/blob/HEAD/CHANGELOG.md#4514---2024-08-08)

[Compare
Source](https://togithub.com/clap-rs/clap/compare/v4.5.13...v4.5.14)

##### Features

- *(unstable-ext)* Added `Arg::add` for attaching arbitrary state, like
completion hints, to `Arg` without `Arg` knowing about it

</details>

---

### Configuration

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

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

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

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

---

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

---

Release Notes:

- N/A

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

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-08-14 16:57:05 -04:00
Nathan Sobo
177aa7d9c0 Revert "Match VSCode behavior for ctrl-a/ctrl-e on MacOS" (#16246)
Reverts zed-industries/zed#15981

Release Notes:

- Restored the behavior of `ctrl-a` until we can separate the behavior
of the command for soft wraps and leading indentation.
2024-08-14 14:51:49 -06:00
Mikayla Maki
271e774713 Fix a bug where directories were not matching in the fuzzy matcher, when query contains the worktree root name (#16242)
Release Notes:

- N/A

Co-authored-by: Max <max@zed.dev>
2024-08-14 12:43:00 -07:00
Kirill Bulatov
e8bae839ed Disable forceful sorting of the slash command argument completions (#16240)
Also bubble up the current active tab's path in the \tab argument
completions.

Release Notes:

- N/A
2024-08-14 22:34:25 +03:00
Vitaly Slobodin
b55e8383c8 terminal: Fix Python virtual environment detection (#15989)
A Python virtual environment places a copy of the Python interpreter and
related files into a special directory, such as `.env` or `env`.
Currently, the built-in Zed terminal does not check if any entries
specified in `terminal.detect_venv.directories` are directories. If a
regular file with the same name exists, the terminal incorrectly
attempts to activate it as a virtual environment. The fix is to ensure
that an entry is a directory before attempting to activate the virtual
environment.

Here are screenshots of 3 possible scenarios:

# With a regular file `.env` in the worktree

## Before


![before](https://github.com/user-attachments/assets/6237a048-432c-4530-892e-91db16ac71bb)

## After


![after](https://github.com/user-attachments/assets/8268dbf4-7f22-441c-a46d-5df9c38131f9)


# With a directory called `.env` in the worktree


![with_pyenv](https://github.com/user-attachments/assets/8d901874-758d-4473-b35a-9c3db32d3b38)


Release Notes:

- Fixed detection of Python virtual environments
([#15570](https://github.com/zed-industries/zed/issues/15570)).
2024-08-14 21:33:02 +03:00
Bennet Bo Fenner
ccd8f75cff assistant: Adjust terms of service notice (#16235)
Co-Authored-by: Max <max@zed.dev>
Co-Authored-by: Marshall <marshall@zed.dev>
Co-Authored-by: Peter <peter@zed.dev>

<img width="396" alt="image"
src="https://github.com/user-attachments/assets/62282506-c74a-455e-ae4d-0438d47fed96">

Release Notes:

- N/A

Co-authored-by: Max <max@zed.dev>
Co-authored-by: Marshall <marshall@zed.dev>
Co-authored-by: Peter <peter@zed.dev>
2024-08-14 19:21:07 +02:00
Marshall Bowers
66e750eea2 Fix Windows build in CI (#15990)
This PR fixes the Windows build in CI, which was failing due to Clippy
warnings.

Release Notes:

- N/A
2024-08-14 13:17:49 -04:00
Joseph T Lyons
04ee5e3e6e v0.150.x dev 2024-08-14 12:46:00 -04:00
Marshall Bowers
8ad7d69378 indexed_docs: Normalize - in crate names to _ when computing rustdoc output path (#16234)
This PR fixes an issue where crate names that included `-`s would not
work properly when indexing them with rustdoc, due to the output
directories using `_` instead of `-`.

Release Notes:

- N/A
2024-08-14 12:37:02 -04:00
Kirill Bulatov
8df21f7bcd Fix slash command argument completion bugs (#16233)
Release Notes:

- N/A

---------

Co-authored-by: Mikayla Maki <mikayla@zed.dev>
2024-08-14 19:36:55 +03:00
Marshall Bowers
6365000b68 gleam: Clean up slash commands in preparation for release (#16232)
This PR cleans up the slash command functionality in preparation for an
upcoming release:

- Removed arguments to `/gleam-project` that were just used as an
example
- Removed `/gleam-docs` in favor of `/docs` with the `gleam-hexdocs`
provider
- Pulled a list of all Gleam packages to use as suggestions

Release Notes:

- N/A
2024-08-14 12:22:22 -04:00
jvmncs
6713e40875 Revert "Simplify inline assist to avoid spurious xml in completions (… (#16231)
…#16184)"

This reverts commit c3edbd7d9a, which
caused a regression that leaked chatter into inline assist replacements
and <rewrite_this> tags into insertions.

Release Notes:

- N/A
2024-08-14 12:20:22 -04:00
Bennet Bo Fenner
793a90c3e1 assistant: Improve terminal slash command (#16229)
- Fix terminal slash command not working when terminal tab was placed in
center workspace
- Removed `--line-count` argument, you can now just pass a number to the
slash command
e.g. `/terminal 10` will show the last 10 lines of the active terminal
- Increase default context lines to 50
- We will revisit this once we add support for only including the last n
amount of commands that were run

Release Notes:

- N/A
2024-08-14 18:04:36 +02:00
Marshall Bowers
340a1d145e haskell: Bump to v0.1.1 (#16228)
This PR bumps the Haskell extension to v0.1.1.

Changes:

- https://github.com/zed-industries/zed/pull/13268
- https://github.com/zed-industries/zed/pull/15998

Release Notes:

- N/A
2024-08-14 11:57:47 -04:00
Bennet Bo Fenner
caf222a71d assistant: Show errors without mouse interaction (#16226)
https://github.com/user-attachments/assets/a8e5e0ce-349d-4836-afe6-fc960a307c9f


Release Notes:

- N/A

---------

Co-authored-by: Thorsten <thorsten@zed.dev>
Co-authored-by: Danilo <danilo@zed.dev>
2024-08-14 17:18:39 +02:00
Thanabodee Charoenpiriyakij
f5532afaa7 erlang: Update tree-sitter grammar and query files (#15973)
Hi, I bump the tree-sitter-erlang to a newest version and sync the
hightlight query to fix long standing issue in the Zed erlang extension
about incorrect function highlighting, not support the triple quote
string and many more.

Release Notes:

- N/A

Signed-off-by: Thanabodee Charoenpiriyakij <wingyminus@gmail.com>
2024-08-14 11:07:54 -04:00
Kirill Bulatov
8d9dcf1e78 Stop automatically running /file command afer file suggestions (#16222)
https://github.com/user-attachments/assets/c5723950-7628-4073-bf03-f0a7473e984e

Release Notes:

- N/A
2024-08-14 17:47:03 +03:00
Stanislav Alekseev
aa31f9ded0 Fix diagnostic popups not having a max width (#16092)
They were probably broken by #14518 

Release Notes:

- N/A
2024-08-14 16:37:35 +02:00
Marshall Bowers
f8a72b5d0a assistant: Run /docs when completing a suggested (unindexed) package (#16218)
This PR is a follow-up to
https://github.com/zed-industries/zed/pull/16216, as we want to run the
`/docs` command when completing a suggested package that has yet to be
indexed.

Release Notes:

- N/A
2024-08-14 10:29:03 -04:00
Kirill Bulatov
8fe2de1737 Further improve /tabs command and slash arguments completion (#16216)
* renames `/tabs` to `/tab`
* allows to insert multiple tabs when fuzzy matching by the names
* improve slash command completion API, introduce a notion of multiple
arguments
* properly fire off commands on arguments' completions with
`run_command: true`

Release Notes:

- N/A

---------

Co-authored-by: Marshall Bowers <marshall@zed.dev>
2024-08-14 17:11:51 +03:00
张小白
88a12b60a9 windows: Fix supermaven (#16203)
Closes #16194

This PR introduces the following changes:

1. Updated the download process to retrieve the `.exe` file, as the API
response indicates that the `.exe` file should be downloaded on Windows.
> API response:
"https://supermaven-public.s3.amazonaws.com/sm-agent/26/windows/amd64/sm-agent.exe"
2. Modified the startup behavior of supermaven to prevent the cmd window
from appearing.

Release Notes:

- N/A
2024-08-14 17:01:16 +03:00
Thorsten Ball
0eb96c72e3 context inspector: Log when no suggestions (#16208)
Release Notes:

- N/A
2024-08-14 14:58:32 +02:00
Danilo Leal
55563831c5 assistant: Adjust slash command popover padding (#16181)
I've looked for other instances of the popover component where this change could cause a spacing regression but couldn't find any yet. Let me know if you do! Intuitively, I wouldn't change this padding directly on the component container, but I didn't find any other way to tackle it.

Release Notes:

- N/A
2024-08-14 09:48:48 -03:00
Piotr Osiewicz
e28681c27e outline: Match on full item path in Outline::find_most_similar (#16206)
Previously, we were only looking at a simple syntax node name at a time
and not the full path to an item. E.g. in a rust-toolchain.toml file:
```rs
[toolchain]
targets = [ "x86_64-apple-darwin", "aarch64-apple-darwin", "x86_64-unknown-linux-gnu", "wasm32-wasi" ]
```
When matching against a query "toolchain targets" from the Assistant,
we'd try to match it against "toolchain" and "targets" and not against
"toolchain targets" - we only look at the name of the innermost node and
not it's full path.
I'd expect it to significantly improve precision of outline item
matching.

Release Notes:

- N/A

Co-authored-by: Bennet Bo <bennet@zed.dev>
2024-08-14 14:03:28 +02:00
Thorsten Ball
b7dcd4e4d6 assistant panel: Fix pending completions not being cleaned up (#16201)
Turns out that you could always cancel a completion, even if it was
already done and completed, because it was never cleaned up.

Release Notes:

- N/A
2024-08-14 12:13:09 +02:00
Thorsten Ball
8b8335f449 assistant panel: Stop animation & show explicit state if canceled (#16200)
This fixes a bug by stopping the animation when a completion is canceled
and it also makes the state more explicit, which I think is very
valuable.



https://github.com/user-attachments/assets/9ede9b25-86ac-4901-8434-7407896bb799


Release Notes:

- N/A
2024-08-14 11:18:40 +02:00
Mikayla Maki
97469cd049 Improve slash commands (#16195)
This PR:

- Makes slash commands easier to compose by adding a concept,
`CompletionIntent`. When using `tab` on a completion in the assistant
panel, that completion item will be expanded but the associated command
will not be run. Using `enter` will still either run the completion item
or continue command composition as before.
- Fixes a bug where running `/diagnostics` on a project with no
diagnostics will delete the entire command, rather than rendering an
empty header.
- Improves the autocomplete rendering for files, showing when
directories are selected and re-arranging the results to have the file
name or trailing directory show first.

<img width="642" alt="Screenshot 2024-08-13 at 8 12 43 PM"
src="https://github.com/user-attachments/assets/97c96cd2-741f-4f15-ad03-7cf78129a71c">


Release Notes:

- N/A
2024-08-13 23:06:07 -07:00
Nathan Sobo
5cb4de4ec6 Fix regression: Restore creation of multiple assist editors on ctrl-enter when selections span across multiple excerpts (#16190)
Release Notes:

- N/A
2024-08-13 17:19:10 -06:00
Marshall Bowers
47628515e1 assistant: Put /docs and /project behind a setting (#16186)
This PR puts the availability of the `/docs` and `/project` slash
commands behind their respective settings.

Release Notes:

- N/A
2024-08-13 17:32:24 -04:00
jvmncs
c3edbd7d9a Simplify inline assist to avoid spurious xml in completions (#16184)
Some prompt changes to highlight:
- Removes `<rewrite_section>` rendering, preferring to just show
`<rewrite_section_with_selections>`
- Concise, terse instructions throughout

I'd like to have experimented with prefilling the assistant response,
but I don't think OpenAI allows for that and wouldn't want to break
compatibility with gpt-4 et al.

Release Notes:

- N/A
2024-08-13 17:05:50 -04:00
Marshall Bowers
aa12ae0e3c Allow extensions to suggest packages for /docs completions (#16185)
This PR gives extensions the ability to suggest packages to show up in
`/docs` completions by default.

Release Notes:

- N/A
2024-08-13 16:50:57 -04:00
Danilo Leal
c6a1d9aa33 assistant: Polish terms of service toast design (#16183)
Pushing in tiny design tweaks and wording change on the button so it's a bit more explicit.

Release Notes:

- N/A
2024-08-13 17:31:46 -03:00
Danilo Leal
07c21bb8ee assistant: Change the quote selection crease icon (#16180)
Felt like this icon was more fitting to the idea of selected text, and looked less like a document.

Release Notes:

- N/A
2024-08-13 17:31:23 -03:00
Danilo Leal
2f5031bd28 Adjust pulsating animation ranges (#16179)
Just a fine-grain refinement to the pulsating animation range.

Release Notes:

- N/A
2024-08-13 17:31:10 -03:00
Danilo Leal
170ad46f5e assistant: Add gap to the context toolbar (#16178)
Not the ideal solution yet, but just a small treatment so these two blocks don't collide.

Release Notes:

- N/A
2024-08-13 17:30:57 -03:00
Marshall Bowers
a81e355dc5 Promote package suggestions to a first-class concept on IndexedDocsProviders (#16177)
This PR promotes package suggestions to a first-class concept on the
`IndexedDocsProvider` trait.

This will allow any implementer of `IndexedDocsProvider` to provide a
list of package names to suggest for use with `/docs`.

For the docs.rs provider we use the 250 most popular Rust crates (as
identified [here](https://lib.rs/std)), and for the rustdoc provider we
use the packages in the Cargo workspace.

Release Notes:

- N/A
2024-08-13 16:01:58 -04:00
Peter Tripp
bd71e9192c Match VSCode behavior for ctrl-a/ctrl-e on MacOS (#15981)
- Make `ctrl-a` and `ctrl-e` ignore soft_wraps on MacOS, matching the behavior of VSCode.
- Unchanged: `home`, `end`, `cmd-left`, `cmd-right` respect softwrap (both in Zed and VSCode).
2024-08-13 15:12:20 -04:00
Piotr Osiewicz
7eeb37262a assistant: Fix nested slash command rendering (#16173)
/rant on
We have this issue where if a prompt starts with a slash command (e.g.
/workflow), the rendering is a bit messed up. The nested slash command
gets picked up as the parent of a command that includes it (/prompt).
This is due to how we parse slash commands; their output is obtained
asynchronously. When we run `/prompt "My prompt"` whose contents are
`/workflow`, we first include the prompt content verbatim and then
reparse the whole buffer, picking up /workflow as a new command (as if
it was typed by an user). The problem with that is that the range of
parent /prompt does not include the expanded range of a /workflow; in
fact, after doing full expansion of "My prompt", we lose track of the
parent-children relationship of these two slash commands and treat them
as if top-level user prompt was `/workflow/prompt "My prompt"` and not
`/prompt "My prompt"` (which, by the way, would not be parsable for us).

The "proper" fix would be to update the parent range whenever we parse a
new children within it. We could do that. But then, the question is;
what do we gain from it? Slash command output is put behind a crease,
which is fundamentally a fold. Given "My prompt", we'd have to put two
fold indicators on a single line even if the ranges were set up
correctly. So that merely moves the target elsewhere into yet another
issue. Even if we did solve two-fold problem somehow (by e.g. sorting
same-line folds by the end point), we would still be stuck with
suboptimal render. What do we gain from all that anyways? Proper
handling of a relatively obscure (although - at the same time - quite
common) edge case which may as well be handled by having /prompt insert
a new line if there's a slight chance that the edge case could occur.
And that hacky, "inproper" solution is what this PR does; in fact, it's
not the first time it was done, as /default also has the same issue
which it solves in precisely the same manner.

/rant off

Release Notes:

- N/A
2024-08-13 20:50:51 +02:00
Athish Pranav D
d4761a3296 Recognize Cuda files as C++ (#16098)
Signed-off-by: Athish Pranav D <athishanna@gmail.com>
2024-08-13 14:44:48 -04:00
Marshall Bowers
ac30ed0754 assistant: Populate /docs rustdoc with workspace crates (#16172)
This PR makes the `/docs` slash command populate the list with all of
the workspace crates when using the local rustdoc provider.

The workspace crates are shown in the search results when a crate is not
already indexed:

<img width="577" alt="Screenshot 2024-08-13 at 2 18 39 PM"
src="https://github.com/user-attachments/assets/39bee576-8e1a-4b21-a9f8-7951ebae4cc3">

These crates are shown with an `(unindexed)` suffix to convey this:

<img width="570" alt="Screenshot 2024-08-13 at 2 18 45 PM"
src="https://github.com/user-attachments/assets/4eeb07f7-378f-44d4-ae11-4ffe45a23964">

Release Notes:

- N/A
2024-08-13 14:31:34 -04:00
Richard Feldman
b1a581e81b Copy/paste images into editors (Mac only) (#15782)
For future reference: WIP branch of copy/pasting a mixture of images and
text: https://github.com/zed-industries/zed/tree/copy-paste-images -
we'll come back to that one after landing this one.

Release Notes:

- You can now paste images into the Assistant Panel to include them as
context. Currently works only on Mac, and with Anthropic models. Future
support is planned for more models, operating systems, and image
clipboard operations.

---------

Co-authored-by: Antonio <antonio@zed.dev>
Co-authored-by: Mikayla <mikayla@zed.dev>
Co-authored-by: Jason <jason@zed.dev>
Co-authored-by: Kyle <kylek@zed.dev>
2024-08-13 13:18:25 -04:00
Marshall Bowers
e3b0de5dda assistant: Auto-suggest crates for /docs when using the docs.rs provider (#16169)
This PR improves the initial experience of using `/docs docs-rs` with an
empty index.

We now show a brief explainer of what is expected (a crate name) as well
as list some popular Rust crates to try:

<img width="540" alt="Screenshot 2024-08-13 at 12 25 39 PM"
src="https://github.com/user-attachments/assets/942de250-2901-45df-9e3e-52ff3b3fc517">

Release Notes:

- N/A
2024-08-13 12:37:13 -04:00
张小白
03796e79b0 repl: Don't show cmd window on Windows (#16016)
Closes #15955 .

Release Notes:

- Fixed `cmd` window showing when repl executing
commands([#15955](https://github.com/zed-industries/zed/issues/15955) ).
2024-08-13 09:12:42 -07:00
Piotr Osiewicz
fa51651d06 assistant: Fix debug inspector removing workflow step it's applied to (#16166)
Debug inspector broke immediately after merge as #16036 landed in
parallel; one of the changes of that PR is removing any steps whose
content was edited, which is what debug inspector happened to do. The
fix is to make the edit right past the step block.


Release Notes:

- N/A
2024-08-13 18:01:55 +02:00
Kirill Bulatov
7aed240729 Improve /tabs completion workflow (#16168)
Follow-up of https://github.com/zed-industries/zed/pull/16154

Reworks /tabs arguments to allow:
* current tab by default, if no arguments are present
* fuzzy-matching over paths of the related tabs
* `all` case to insert all tabs at once

Release Notes:

- N/A
2024-08-13 18:40:24 +03:00
Marshall Bowers
7b613cb169 Revert "Remove extra empty space for files when file icons are turned off (#16142) (#16167)
This PR reverts #16142, as it isn't what we want from a design
standpoint.

Having the file names misaligned from the folder names is not the
desired behavior:

<img width="243" alt="Screenshot 2024-08-13 at 11 16 53 AM"
src="https://github.com/user-attachments/assets/12914e89-2641-4932-96c2-00e89e56d6d7">

We can revisit when we have design bandwidth.

This reverts commit ee6a40137f.

Release Notes:

- Reverted #16142.
  - @JosephTLyons @notpeter for release notes curation
2024-08-13 11:37:29 -04:00
Marshall Bowers
8a9c58e515 zed_extension_api: Add HttpRequestBuilder (#16165)
This PR adds an `HttpRequestBuilder` to the extension API to allow for a
more ergonomic way for constructing HTTP requests within extensions.

The HTTP client functionality is now also exposed via the
`zed_extension_api::http_client` module instead of top-level.

Release Notes:

- N/A
2024-08-13 11:12:10 -04:00
Piotr Osiewicz
0dbecee03f assistant: Refine workflow step labels (#16161)
https://github.com/user-attachments/assets/f6325507-091a-482e-ac28-dd09877ebaa2


Release Notes:

- N/A

---------

Co-authored-by: Danilo <daniloleal09@gmail.com>
2024-08-13 17:08:55 +02:00
Marshall Bowers
82529499df Fix min and max versions for v0.1.0 of the extension API (#16163)
Missed this in #16158.

Release Notes:

- N/A
2024-08-13 10:40:29 -04:00
Marshall Bowers
4450ebff6b Allow extensions to control the redirect policy for the HTTP client (#16162)
This PR extends the extension API with support for controlling the
redirect policy used by the HTTP client.

Release Notes:

- N/A
2024-08-13 10:40:21 -04:00
Marshall Bowers
98a2ab0686 zed_extension_api: Bump to v0.1.0 (#16158)
This PR changes v0.0.7 of the extension API to v0.1.0.

We had a false-start in releasing v0.0.7, which has since been yanked,
so we need a new version number. We'll publish v0.1.0 to crates.io once
the Preview build is out tomorrow.

We're incrementing the minor version so that we have some leeway in
putting out patch releases of the crate within a given extension API
release.

Release Notes:

- N/A
2024-08-13 10:04:34 -04:00
Rudolf Kastl
47eed12f77 Update zed.desktop.in to include the MimeType for empty files by default (#15623)
Update zed.desktop.in to include the MimeType for empty files.
Seems to be the default for all "text editors" .desktop files.

Release Notes:

- Improved MimeType list in XDG .desktop file
2024-08-13 16:56:44 +03:00
Marshall Bowers
cade9fbd3d assistant: Show a better error when /docs is used without a package name (#16157)
This PR makes it so that running `/docs` without providing a package
name gives a better error message:

<img width="248" alt="Screenshot 2024-08-13 at 9 24 58 AM"
src="https://github.com/user-attachments/assets/c1cc794e-0fa0-490a-871a-a56702b03d42">

<img width="228" alt="Screenshot 2024-08-13 at 9 25 05 AM"
src="https://github.com/user-attachments/assets/45dca2d7-171f-48f0-a03c-254b552cb50d">

Release Notes:

- N/A
2024-08-13 09:39:26 -04:00
CharlesChen0823
fe190359d5 editor: Add revert file action to command palette (#16012)
Release Notes:

- Added an `editor::RevertFile` action
2024-08-13 14:44:41 +03:00
fcolecumberri
ab98f16280 Update key-bindings.md (#15994)
Closes https://github.com/zed-industries/zed/pull/15238

Release Notes:

- N/A
2024-08-13 14:40:27 +03:00
张小白
ac6bff12b9 windows: Remove unused dependencies (#15857)
I have removed some unused dependencies, reducing the total number of
packages during the build from 1141 to 1112. This should slightly
decrease the build time.

![Screenshot 2024-08-06
230726](https://github.com/user-attachments/assets/58a49fd4-4a0a-4026-b6b7-79b95529ec74)


Release Notes:

- N/A
2024-08-13 13:31:02 +03:00
Kirill Bulatov
081cbcebd9 Merge /active command into /tabs one (#16154)
Now, tabs have arguments, `active` (default, applied also for no
arguments case) and `all` to insert the active tab only or all tabs.

Release Notes:

- N/A
2024-08-13 13:15:57 +03:00
Stanislav Alekseev
c2b254a67a Fallback to using tree-sitter when determining ranges for info popovers (#16062)
Closes #15382

Release Notes:

- Added fallback to a smallest tree sitter node when hovering over a
symbol
2024-08-13 12:01:14 +02:00
Thorsten Ball
af36d4934c assistant panel: Animate assistant label if message is pending (#16152)
This adds a pulsating effect to the assistant header in case the message
is pending.

The pulsating effect is capped between 0.2 and 1.0 and I tried (with the
help of Claude) to give it a "breathing" effect, since I found the
normal bounce a bit too much.

Also opted for setting the `alpha` on the `LabelLike` things, vs.
overwriting the color, since I think that's cleaner instead of exposing
the color and mutating that.


https://github.com/user-attachments/assets/4a94a1c5-8dc7-4c40-b30f-d92d112db7b5


Release Notes:

- N/A
2024-08-13 11:41:44 +02:00
Kirill Bulatov
b36d1386a9 Fix editor::Cancel action not inline assistant inputs created for empty selections (#16150)
Release Notes:

- N/A
2024-08-13 12:11:57 +03:00
Uladzislau Kaminski
ee6a40137f Remove extra empty space for files when file icons are turned off (#16142)
Closes #16073

<img width="269" alt="image" src="https://github.com/user-attachments/assets/88b7ff9f-17ec-4764-b37a-c218d7ad14ec">

Release Notes:

- Removed extra empty space for files when file icons are turned off ([#16073](https://github.com/zed-industries/zed/issues/16073))
2024-08-13 12:09:13 +03:00
Nathan Sobo
1c189e82a0 Improve tooltip text (#16147)
![image](https://github.com/user-attachments/assets/90faf9ce-0515-4a99-92ca-c69b17b5149e)

Release Notes:

- Include a count of the context tokens when hovering token counts in
the inline assist.
2024-08-12 23:24:27 -06:00
Nathan Sobo
a515442a36 Rely on model to determine indentation level and always rewrite the full line (#16145)
This PR simplifies our approach to indentation in the inline assistant
in hopes of improving our experience for Python. We tell the model to
generate the correct indentation in the prompt, and always start
generating at the start of the line. This may fall down for less capable
models, but I want to get a solid experience on the best models and then
figure the rest out later.

Also: We now prefer `./assets/prompts` as an overrides directory when
stdout is a PTY, so you can do `cargo run` and then iterate prompts for
the current run inside the current working copy.

cc @trishume @dsp-ant 

Release Notes:

- Zed now allows the model to control indentation when performing inline
transformation. We're hoping this improves the indentation experience in
Python and other indentation-sensitive languages, but it does require
more from the model.

---------

Co-authored-by: Mikayla <mikayla@zed.dev>
2024-08-12 22:41:24 -06:00
Marshall Bowers
e662bfc74f Configure squawk rules (#16144)
This PR configures [`squawk`](https://squawkhq.com/) to match our
database conventions.

Release Notes:

- N/A
2024-08-12 22:11:36 -04:00
Marshall Bowers
b4c22cc861 collab: Add ability to revoke LLM service access tokens (#16143)
This PR adds the ability to revoke access tokens for the LLM service.

There is a new `revoked_access_tokens` table that contains the
identifiers (`jti`) of revoked access tokens.

To revoke an access token, insert a record into this table:

```sql
insert into revoked_access_tokens (jti) values ('1e887b9e-37f5-49e8-8feb-3274e5a86b67');
```

We now attach the `jti` as `authn.jti` to the tracing spans so that we
can associate an access token with a given request to the LLM service.

Release Notes:

- N/A
2024-08-12 21:47:05 -04:00
Piotr Osiewicz
0bc9fc9487 assistant: Slash command tweaks (#16140)
Release Notes:

- N/A
2024-08-13 02:34:03 +02:00
Max Brunsfeld
dbcd06642c Track lifetime spending for each user and model (#16137)
Release Notes:

- N/A

Co-authored-by: Marshall <marshall@zed.dev>
2024-08-12 20:15:26 -04:00
Piotr Osiewicz
68ae347077 chore: Remove a bunch of unused structs (#16139)
Found by beta clippy.

Release Notes:

- N/A
2024-08-13 01:43:19 +02:00
Stanislav Alekseev
f956257638 haskell: Pass environment to hls (#15998)
Fixed environment not being passed to hls, breaking local nix-based
installations

Release Notes:

- N/A
2024-08-12 18:38:23 -04:00
Marshall Bowers
8a148f3a13 Add feature-flagged access to LLM service (#16136)
This PR adds feature-flagged access to the LLM service.

We've repurposed the `language-models` feature flag to be used for
providing access to Claude 3.5 Sonnet through the Zed provider.

The remaining RPC endpoints that were previously behind the
`language-models` feature flag are now behind a staff check.

We also put some Zed Pro related messaging behind a feature flag.

Release Notes:

- N/A

---------

Co-authored-by: Max <max@zed.dev>
2024-08-12 18:13:40 -04:00
Kirill Bulatov
3bebb8b401 Allow to cycle through center/top/bot scroll positions (#16134)
On top of `editor::ScrollCursorCenter`, `editor::ScrollCursorTop`,
`editor::ScrollCursorBottom` actions, adds an
`editor::ScrollCursorCenterTopBottom` one, that allows using a single
keybinding to scroll between positions on the screen.

The implementation matches a corresponding Emacs feature: there's a
timeout (1s) that is kept after every switch, to allow continuously
changing the positions, center (initial) -> top -> bottom
Scrolling behavior is the same as the existing actions (e.g. editor will
ignore scroll to bottom, if there's not enough space above).

After 1s, next position is reset to the initial, center, one.


Release Notes:

- Added an `editor::ScrollCursorCenterTopBottom` action for toggling
scroll position with a single keybinding

---------

Co-authored-by: Alex Kladov <aleksey.kladov@gmail.com>
2024-08-13 00:32:30 +03:00
Marshall Bowers
98516b5527 collab: Restrict usage of the LLM service to accounts older than 30 days (#16133)
This PR restricts usage of the LLM service to accounts older than 30
days.

We now store the GitHub user's `created_at` timestamp to check the
GitHub account age. If this is not set—which it won't be for existing
users—then we use the `created_at` timestamp in the Zed database.

Release Notes:

- N/A

---------

Co-authored-by: Max <max@zed.dev>
2024-08-12 17:27:21 -04:00
Kirill Bulatov
f398ecc3fb Allow inserting text into the editor via the action (#16131)
Improves workflows, based on the keymaps that group actions behind a
certain symbol.
E.g.

```json5
", o k": "zed::OpenKeymap",
", o K": "zed::OpenDefaultKeymap",
// other `,`-based keymaps
```

Now, it's possible to do 
```json
", ,": ["editor::HandleInput", ","]
```

and type `,` without waiting for the timeout due to `,`-based bindings.

Release Notes:

- Add an `editor::HandleInput` action to ease typing symbols that are
part of keymaps. E.g. if `, o k` keybinding is bound, `", ,":
["editor::HandleInput", ","]` would allow to type `,` without timeouts.

---------

Co-authored-by: Alex Kladov <aleksey.kladov@gmail.com>
2024-08-13 00:02:52 +03:00
Max Brunsfeld
a3c79218c4 Report telemetry events for rate limit errors (#16130)
clickhouse telemetry schema:

```
CREATE TABLE default.llm_rate_limit_events
(
    `time` DateTime64(3),
    `user_id` Int32,
    `is_staff` Bool,
    `plan` LowCardinality(String),
    `model` String,
    `provider` LowCardinality(String),
    `usage_measure` LowCardinality(String),
    `requests_this_minute` UInt64,
    `tokens_this_minute` UInt64,
    `tokens_this_day` UInt64,
    `max_requests_per_minute` UInt64,
    `max_tokens_per_minute` UInt64,
    `max_tokens_per_day` UInt64,
    `users_in_recent_minutes` UInt64,
    `users_in_recent_days` UInt64
)
ORDER BY tuple()
```

Release Notes:

- N/A

Co-authored-by: Marshall <marshall@zed.dev>
2024-08-12 16:31:11 -04:00
Max Brunsfeld
1674e12ccb Expose anthropic API errors to the client (#16129)
Now, when an anthropic request is invalid or anthropic's API is down,
we'll expose that to the user instead of just returning a generic 500.

Release Notes:

- N/A

Co-authored-by: Marshall <marshall@zed.dev>
2024-08-12 13:11:48 -07:00
Marshall Bowers
f3ec8d425f collab: Use a separate Anthropic API key for Zed staff (#16128)
This PR makes it so Zed staff can use a separate Anthropic API key for
the LLM service.

We also added an `is_staff` column to the `usages` table so that we can
exclude staff usage from the "active users" metrics that influence the
rate limits.

Release Notes:

- N/A

---------

Co-authored-by: Max <max@zed.dev>
2024-08-12 15:20:34 -04:00
Marshall Bowers
ebdde5994d collab: Don't issue LLM API tokens if the user has not accepted the ToS (#16123)
This PR adds a check to the LLM API token issuance to ensure that we
only issue tokens to users that have accepted the terms of service.

Release Notes:

- N/A
2024-08-12 14:10:08 -04:00
Neil
df70e901af docs: Add "dock" to "terminal" section (#16122)
Documentation-related fix for #16121.

Co-authored-by: Neil Alexander <neilalexander@users.noreply.github.com>

Release Notes:
* N/A
2024-08-12 20:23:55 +03:00
Michael Angerman
63aef7f798 command_palette: Remove project as a dependency in Cargo.toml (#16082)
The *project crate* is only needed as a dev dependency in the command
palette..
So I am doing some code / dependency cleanup...

Release Notes:

- N/A
2024-08-12 13:01:32 -04:00
renovate[bot]
9ae0d5388e Update Rust crate quote to v1.0.36 (#15945)
[![Mend
Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com)

This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [quote](https://togithub.com/dtolnay/quote) | dependencies | patch |
`1.0.35` -> `1.0.36` |

---

### Release Notes

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

### [`v1.0.36`](https://togithub.com/dtolnay/quote/releases/tag/1.0.36)

[Compare
Source](https://togithub.com/dtolnay/quote/compare/1.0.35...1.0.36)

-   Documentation improvements

</details>

---

### Configuration

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

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

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

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

---

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

---

Release Notes:

- N/A

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

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-08-12 12:51:13 -04:00
Marshall Bowers
ebdb755fef Surface upstream rate limits from Anthropic (#16118)
This PR makes it so hitting upstream rate limits from Anthropic result
in an HTTP 429 response instead of an HTTP 500.

To do this we need to surface structured errors out of the `anthropic`
crate.

Release Notes:

- N/A
2024-08-12 11:59:24 -04:00
Thorsten Ball
fbb533b3e0 assistant: Require user to accept TOS for cloud provider (#16111)
This adds the requirement for users to accept the terms of service the
first time they send a message with the Cloud provider.

Once this is out and in a nightly, we need to add the check to the
server side too, to authenticate access to the models.

Demo:


https://github.com/user-attachments/assets/0edebf74-8120-4fa2-b801-bb76f04e8a17



Release Notes:

- N/A
2024-08-12 17:43:35 +02:00
Piotr Osiewicz
98f314ba21 assistant: Add debug inspector (#16105)
I went with inline approach directly within the panel.
First, enable workflow debugging in the hamburger menu (this works
retroactively as well):


![image](https://github.com/user-attachments/assets/d2ab8edf-bb7b-49a4-8f70-9a6fe94dc7dd)

This enables debug buttons in the header of each step:

![image](https://github.com/user-attachments/assets/3b5d479f-7473-4c41-a2e7-8c10bb71f0ff)
Enabling one pretty-prints the workflow step internals:

![image](https://github.com/user-attachments/assets/e651e826-1270-49ff-8bb6-046c07c006bf)


Release Notes:

- N/A
2024-08-12 17:05:54 +02:00
Piotr Osiewicz
b6b081596a assistant: Show tooltips on workflow step buttons only when cursor is in step (#16108)
Release Notes:

- N/A
2024-08-12 16:53:11 +02:00
Nathan Sobo
fc64843dd5 Enhance HTTP API for extensions (#16067)
# HTTP Client Improvements for Extension API

This PR enhances the HTTP client functionality in the Zed extension API,
providing more control over requests and allowing for streaming
responses.

## Key Changes

1. Extended `HttpRequest` struct:
   - Added `method` field to specify HTTP method
   - Added `headers` field for custom headers
   - Added optional `body` field for request payload

2. Introduced `HttpMethod` enum for supported HTTP methods

3. Updated `HttpResponse` struct:
   - Added `headers` field to access response headers
- Changed `body` type from `String` to `Vec<u8>` for binary data support

4. Added streaming support:
   - New `fetch_stream` function to get a response stream
   - Introduced `HttpResponseStream` resource for chunked reading

5. Updated internal implementations to support these new features

6. Modified the Gleam extension to use the new API structure

## Motivation

These changes provide extension developers with more flexibility and
control over HTTP requests. The streaming support is particularly useful
for handling large responses efficiently or ideally streaming into the
UI.

## Testing

- [x] Updated existing tests
- [ ] Added new tests for streaming functionality

## Next Steps

- Consider adding more comprehensive examples in the documentation
- Evaluate performance impact of streaming for large responses

Please review and let me know if any adjustments are needed.

Release Notes:

- N/A

---------

Co-authored-by: Marshall Bowers <elliott.codes@gmail.com>
2024-08-12 10:36:49 -04:00
Marshall Bowers
f952126319 collab: Remove LLM completions over RPC (#16114)
This PR removes the LLM completion messages from the RPC protocol, as
these now go through the LLM service as of #16113.

Release Notes:

- N/A
2024-08-12 10:08:56 -04:00
Nate Butler
f992cfdc7f Update provider logos (#16115)
- Updates provider logos for Anthropic, Google and Ollama
- Increases the size of icons in the model selector

Release Notes:

- N/A
2024-08-12 09:55:00 -04:00
Marshall Bowers
6389c613a2 Always stream completions through the LLM service (#16113)
This PR removes the `llm-service` feature flag and makes it so all
completions are done via the LLM service when using the Zed provider.

Release Notes:

- N/A
2024-08-12 09:33:24 -04:00
Son
bab4da78b7 Fix corner radius when doing rounded_full (#15663)
Release Notes:

- Fixed issue when doing `rounded_full`, it should render a pill-shape
rect instead of current eye-shape.

For example with this code
`div().h_4().w_16().bg(rgb(0xffffff)).rounded_full()`

Current:

<img width="144" alt="image"
src="https://github.com/user-attachments/assets/f8b20c7c-d91f-4c20-9f38-d435f59e72b7">

Fixed:

<img width="172" alt="image"
src="https://github.com/user-attachments/assets/ff8bbe26-2b31-4ef1-a2fb-25b458386ffb">
2024-08-12 15:24:32 +02:00
Antonio Scandurra
a15a9565ab Accept finished inline transformations only if the user saves manually (#16112)
Closes https://github.com/zed-industries/zed/issues/16042

This commit modifies the behavior of inline transformations to only
accept finished transformations when the user manually saves the file.
Previously, transformations were automatically accepted on any save
event, including autosaves.

This was achieved by updating the `Pane` and `Workspace` structs to emit
a new `UserSavedItem` event when a manual save occurs, and modifying the
`InlineAssistant` to register and handle this new event (instead of
`editor::Saved`).

Release Notes:

- N/A
2024-08-12 15:16:55 +02:00
Antonio Scandurra
48f6193628 Improve workflow step pruning and symbol similarity matching (#16036)
This PR improves workflow step management and symbol matching. We've
optimized step pruning to remove any step that intersects an edit and
switched to normalized Levenshtein distance for more accurate symbol
matching.

Release Notes:

- N/A
2024-08-12 11:09:07 +02:00
396 changed files with 21639 additions and 11039 deletions

View File

@@ -3,6 +3,15 @@ export default {
const url = new URL(request.url);
url.hostname = "docs-anw.pages.dev";
// These pages were removed, but may still be served due to Cloudflare's
// [asset retention](https://developers.cloudflare.com/pages/configuration/serving-pages/#asset-retention).
if (
url.pathname === "/docs/assistant/context-servers" ||
url.pathname === "/docs/assistant/model-context-protocol"
) {
return await fetch("https://zed.dev/404");
}
let res = await fetch(url, request);
if (res.status === 404) {

View File

@@ -167,6 +167,7 @@ jobs:
APPLE_NOTARIZATION_USERNAME: ${{ secrets.APPLE_NOTARIZATION_USERNAME }}
APPLE_NOTARIZATION_PASSWORD: ${{ secrets.APPLE_NOTARIZATION_PASSWORD }}
ZED_CLIENT_CHECKSUM_SEED: ${{ secrets.ZED_CLIENT_CHECKSUM_SEED }}
ZED_CLOUD_PROVIDER_ADDITIONAL_MODELS_JSON: ${{ secrets.ZED_CLOUD_PROVIDER_ADDITIONAL_MODELS_JSON }}
DIGITALOCEAN_SPACES_ACCESS_KEY: ${{ secrets.DIGITALOCEAN_SPACES_ACCESS_KEY }}
DIGITALOCEAN_SPACES_SECRET_KEY: ${{ secrets.DIGITALOCEAN_SPACES_SECRET_KEY }}
steps:
@@ -276,6 +277,7 @@ jobs:
needs: [linux_tests]
env:
ZED_CLIENT_CHECKSUM_SEED: ${{ secrets.ZED_CLIENT_CHECKSUM_SEED }}
ZED_CLOUD_PROVIDER_ADDITIONAL_MODELS_JSON: ${{ secrets.ZED_CLOUD_PROVIDER_ADDITIONAL_MODELS_JSON }}
steps:
- name: Add Rust to the PATH
run: echo "$HOME/.cargo/bin" >> $GITHUB_PATH
@@ -346,6 +348,7 @@ jobs:
needs: [linux_tests]
env:
ZED_CLIENT_CHECKSUM_SEED: ${{ secrets.ZED_CLIENT_CHECKSUM_SEED }}
ZED_CLOUD_PROVIDER_ADDITIONAL_MODELS_JSON: ${{ secrets.ZED_CLOUD_PROVIDER_ADDITIONAL_MODELS_JSON }}
steps:
- name: Checkout repo
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4
@@ -360,7 +363,7 @@ jobs:
sudo apt-get install -y llvm-10 clang-10 build-essential cmake pkg-config libasound2-dev libfontconfig-dev libwayland-dev libxkbcommon-x11-dev libssl-dev libsqlite3-dev libzstd-dev libvulkan1 libgit2-dev
echo "/usr/lib/llvm-10/bin" >> $GITHUB_PATH
- uses: rui314/setup-mold@2e332a0b602c2fc65d2d3995941b1b29a5f554a0 # v1
- uses: rui314/setup-mold@0bf4f07ef9048ec62a45f9dbf2f098afa49695f0 # v1
with:
mold-version: 2.32.0

View File

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

View File

@@ -67,6 +67,7 @@ jobs:
DIGITALOCEAN_SPACES_ACCESS_KEY: ${{ secrets.DIGITALOCEAN_SPACES_ACCESS_KEY }}
DIGITALOCEAN_SPACES_SECRET_KEY: ${{ secrets.DIGITALOCEAN_SPACES_SECRET_KEY }}
ZED_CLIENT_CHECKSUM_SEED: ${{ secrets.ZED_CLIENT_CHECKSUM_SEED }}
ZED_CLOUD_PROVIDER_ADDITIONAL_MODELS_JSON: ${{ secrets.ZED_CLOUD_PROVIDER_ADDITIONAL_MODELS_JSON }}
steps:
- name: Install Node
uses: actions/setup-node@1e60f620b9541d16bece96c5465dc8ee9832be0b # v4
@@ -106,6 +107,7 @@ jobs:
DIGITALOCEAN_SPACES_ACCESS_KEY: ${{ secrets.DIGITALOCEAN_SPACES_ACCESS_KEY }}
DIGITALOCEAN_SPACES_SECRET_KEY: ${{ secrets.DIGITALOCEAN_SPACES_SECRET_KEY }}
ZED_CLIENT_CHECKSUM_SEED: ${{ secrets.ZED_CLIENT_CHECKSUM_SEED }}
ZED_CLOUD_PROVIDER_ADDITIONAL_MODELS_JSON: ${{ secrets.ZED_CLOUD_PROVIDER_ADDITIONAL_MODELS_JSON }}
steps:
- name: Checkout repo
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4
@@ -139,6 +141,7 @@ jobs:
DIGITALOCEAN_SPACES_ACCESS_KEY: ${{ secrets.DIGITALOCEAN_SPACES_ACCESS_KEY }}
DIGITALOCEAN_SPACES_SECRET_KEY: ${{ secrets.DIGITALOCEAN_SPACES_SECRET_KEY }}
ZED_CLIENT_CHECKSUM_SEED: ${{ secrets.ZED_CLIENT_CHECKSUM_SEED }}
ZED_CLOUD_PROVIDER_ADDITIONAL_MODELS_JSON: ${{ secrets.ZED_CLOUD_PROVIDER_ADDITIONAL_MODELS_JSON }}
steps:
- name: Checkout repo
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4
@@ -154,7 +157,7 @@ jobs:
sudo apt-get install -y llvm-10 clang-10 build-essential cmake pkg-config libasound2-dev libfontconfig-dev libwayland-dev libxkbcommon-x11-dev libssl-dev libsqlite3-dev libzstd-dev libvulkan1 libgit2-dev
echo "/usr/lib/llvm-10/bin" >> $GITHUB_PATH
- uses: rui314/setup-mold@2e332a0b602c2fc65d2d3995941b1b29a5f554a0 # v1
- uses: rui314/setup-mold@0bf4f07ef9048ec62a45f9dbf2f098afa49695f0 # v1
with:
mold-version: 2.32.0

View File

@@ -24,7 +24,8 @@ Conrad Irwin <conrad@zed.dev>
Conrad Irwin <conrad@zed.dev> <conrad.irwin@gmail.com>
Danilo Leal <danilo@zed.dev>
Danilo Leal <danilo@zed.dev> <67129314+danilo-leal@users.noreply.github.com>
Evren Sen <146845123+evrsen@users.noreply.github.com>
Evren Sen <146845123+evrensen467@users.noreply.github.com>
Evren Sen <146845123+evrensen467@users.noreply.github.com> <146845123+evrsen@users.noreply.github.com>
Fernando Tagawa <tagawafernando@gmail.com>
Fernando Tagawa <tagawafernando@gmail.com> <fernando.tagawa.gamail.com@gmail.com>
Greg Morenz <greg-morenz@droid.cafe>

View File

@@ -3,5 +3,10 @@
"label": "clippy",
"command": "./script/clippy",
"args": []
},
{
"label": "cargo run --profile release-fast",
"command": "cargo",
"args": ["run", "--profile", "release-fast"]
}
]

233
Cargo.lock generated
View File

@@ -223,6 +223,7 @@ name = "anthropic"
version = "0.1.0"
dependencies = [
"anyhow",
"chrono",
"futures 0.3.30",
"http_client",
"isahc",
@@ -230,7 +231,9 @@ dependencies = [
"serde",
"serde_json",
"strum",
"thiserror",
"tokio",
"util",
]
[[package]]
@@ -279,9 +282,9 @@ checksum = "9d151e35f61089500b617991b791fc8bfd237ae50cd5950803758a179b41e67a"
[[package]]
name = "arrayvec"
version = "0.7.4"
version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711"
checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50"
[[package]]
name = "as-raw-xcb-connection"
@@ -357,6 +360,7 @@ dependencies = [
"clock",
"collections",
"command_palette_hooks",
"context_servers",
"ctor",
"db",
"editor",
@@ -365,6 +369,7 @@ dependencies = [
"fs",
"futures 0.3.30",
"fuzzy",
"globset",
"gpui",
"handlebars",
"heed",
@@ -791,9 +796,9 @@ dependencies = [
[[package]]
name = "async-stripe"
version = "0.37.3"
version = "0.38.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2f14b5943a52cf051bbbbb68538e93a69d1e291934174121e769f4b181113f5"
checksum = "97ddaa6999d246ba2c6c84d830a1ba0cd16c9234d58701988b3869f0e5bd732d"
dependencies = [
"chrono",
"futures-util",
@@ -983,9 +988,9 @@ dependencies = [
[[package]]
name = "aws-config"
version = "1.5.4"
version = "1.5.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "caf6cfe2881cb1fcbba9ae946fb9a6480d3b7a714ca84c74925014a89ef3387a"
checksum = "4e95816a168520d72c0e7680c405a5a8c1fb6a035b4bc4b9d7b0de8e1a941697"
dependencies = [
"aws-credential-types",
"aws-runtime",
@@ -1003,7 +1008,6 @@ dependencies = [
"fastrand 2.1.0",
"hex",
"http 0.2.12",
"hyper",
"ring",
"time",
"tokio",
@@ -1026,9 +1030,9 @@ dependencies = [
[[package]]
name = "aws-runtime"
version = "1.3.1"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "87c5f920ffd1e0526ec9e70e50bf444db50b204395a0fa7016bbf9e31ea1698f"
checksum = "f42c2d4218de4dcd890a109461e2f799a1a2ba3bcd2cde9af88360f5df9266c6"
dependencies = [
"aws-credential-types",
"aws-sigv4",
@@ -1042,6 +1046,7 @@ dependencies = [
"fastrand 2.1.0",
"http 0.2.12",
"http-body 0.4.6",
"once_cell",
"percent-encoding",
"pin-project-lite",
"tracing",
@@ -1050,9 +1055,9 @@ dependencies = [
[[package]]
name = "aws-sdk-s3"
version = "1.42.0"
version = "1.46.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "558bbcec8db82a1a8af1610afcb3b10d00652d25ad366a0558eecdff2400a1d1"
checksum = "4abf69a87be33b6f125a93d5046b5f7395c26d1f449bf8d3927f5577463b6de0"
dependencies = [
"ahash 0.8.11",
"aws-credential-types",
@@ -1085,9 +1090,9 @@ dependencies = [
[[package]]
name = "aws-sdk-sso"
version = "1.36.0"
version = "1.37.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6acca681c53374bf1d9af0e317a41d12a44902ca0f2d1e10e5cb5bb98ed74f35"
checksum = "1074e818fbe4f9169242d78448b15be8916a79daa38ea1231f2e2e10d993fcd2"
dependencies = [
"aws-credential-types",
"aws-runtime",
@@ -1107,9 +1112,9 @@ dependencies = [
[[package]]
name = "aws-sdk-ssooidc"
version = "1.37.0"
version = "1.38.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b79c6bdfe612503a526059c05c9ccccbf6bd9530b003673cb863e547fd7c0c9a"
checksum = "29755c51e33fa3f678598f64324a169cf4b7d3c4865d2709d4308f53366a92a4"
dependencies = [
"aws-credential-types",
"aws-runtime",
@@ -1129,9 +1134,9 @@ dependencies = [
[[package]]
name = "aws-sdk-sts"
version = "1.36.0"
version = "1.37.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32e6ecdb2bd756f3b2383e6f0588dc10a4e65f5d551e70a56e0bfe0c884673ce"
checksum = "6e52dc3fd7dfa6c01a69cf3903e00aa467261639138a05b06cd92314d2c8fb07"
dependencies = [
"aws-credential-types",
"aws-runtime",
@@ -1192,9 +1197,9 @@ dependencies = [
[[package]]
name = "aws-smithy-checksums"
version = "0.60.11"
version = "0.60.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "48c4134cf3adaeacff34d588dbe814200357b0c466d730cf1c0d8054384a2de4"
checksum = "598b1689d001c4d4dc3cb386adb07d37786783aee3ac4b324bcadac116bf3d23"
dependencies = [
"aws-smithy-http",
"aws-smithy-types",
@@ -1264,9 +1269,9 @@ dependencies = [
[[package]]
name = "aws-smithy-runtime"
version = "1.6.2"
version = "1.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ce87155eba55e11768b8c1afa607f3e864ae82f03caf63258b37455b0ad02537"
checksum = "0abbf454960d0db2ad12684a1640120e7557294b0ff8e2f11236290a1b293225"
dependencies = [
"aws-smithy-async",
"aws-smithy-http",
@@ -1291,9 +1296,9 @@ dependencies = [
[[package]]
name = "aws-smithy-runtime-api"
version = "1.7.1"
version = "1.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "30819352ed0a04ecf6a2f3477e344d2d1ba33d43e0f09ad9047c12e0d923616f"
checksum = "e086682a53d3aa241192aa110fa8dfce98f2f5ac2ead0de84d41582c7e8fdb96"
dependencies = [
"aws-smithy-async",
"aws-smithy-types",
@@ -1308,9 +1313,9 @@ dependencies = [
[[package]]
name = "aws-smithy-types"
version = "1.2.0"
version = "1.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cfe321a6b21f5d8eabd0ade9c55d3d0335f3c3157fc2b3e87f05f34b539e4df5"
checksum = "6cee7cadb433c781d3299b916fbf620fea813bf38f49db282fb6858141a05cc8"
dependencies = [
"base64-simd",
"bytes 1.7.1",
@@ -1536,7 +1541,7 @@ dependencies = [
"bitflags 2.6.0",
"cexpr",
"clang-sys",
"itertools 0.12.1",
"itertools 0.10.5",
"lazy_static",
"lazycell",
"proc-macro2",
@@ -1619,7 +1624,7 @@ dependencies = [
[[package]]
name = "blade-graphics"
version = "0.4.0"
source = "git+https://github.com/kvark/blade?rev=ac25c77ed8d86c386a541c935ffe0a0f6024e701#ac25c77ed8d86c386a541c935ffe0a0f6024e701"
source = "git+https://github.com/kvark/blade?rev=7f54ddfc001edc48225e6602d3c38ebb855421aa#7f54ddfc001edc48225e6602d3c38ebb855421aa"
dependencies = [
"ash",
"ash-window",
@@ -1649,7 +1654,7 @@ dependencies = [
[[package]]
name = "blade-macros"
version = "0.2.1"
source = "git+https://github.com/kvark/blade?rev=ac25c77ed8d86c386a541c935ffe0a0f6024e701#ac25c77ed8d86c386a541c935ffe0a0f6024e701"
source = "git+https://github.com/kvark/blade?rev=7f54ddfc001edc48225e6602d3c38ebb855421aa#7f54ddfc001edc48225e6602d3c38ebb855421aa"
dependencies = [
"proc-macro2",
"quote",
@@ -1659,7 +1664,7 @@ dependencies = [
[[package]]
name = "blade-util"
version = "0.1.0"
source = "git+https://github.com/kvark/blade?rev=ac25c77ed8d86c386a541c935ffe0a0f6024e701#ac25c77ed8d86c386a541c935ffe0a0f6024e701"
source = "git+https://github.com/kvark/blade?rev=7f54ddfc001edc48225e6602d3c38ebb855421aa#7f54ddfc001edc48225e6602d3c38ebb855421aa"
dependencies = [
"blade-graphics",
"bytemuck",
@@ -2206,9 +2211,9 @@ dependencies = [
[[package]]
name = "clap"
version = "4.5.13"
version = "4.5.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fbb260a053428790f3de475e304ff84cdbc4face759ea7a3e64c1edd938a7fc"
checksum = "ed6719fffa43d0d87e5fd8caeab59be1554fb028cd30edc88fc4369b17971019"
dependencies = [
"clap_builder",
"clap_derive",
@@ -2216,9 +2221,9 @@ dependencies = [
[[package]]
name = "clap_builder"
version = "4.5.13"
version = "4.5.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "64b17d7ea74e9f833c7dbf2cbe4fb12ff26783eda4782a8975b72f895c9b4d99"
checksum = "216aec2b177652e3846684cbfe25c9964d18ec45234f0f5da5157b207ed1aab6"
dependencies = [
"anstream",
"anstyle",
@@ -2324,7 +2329,6 @@ dependencies = [
"futures 0.3.30",
"gpui",
"http_client",
"lazy_static",
"log",
"once_cell",
"parking_lot",
@@ -2526,7 +2530,6 @@ dependencies = [
"gpui",
"http_client",
"language",
"lazy_static",
"menu",
"notifications",
"parking_lot",
@@ -2668,6 +2671,27 @@ dependencies = [
"tiny-keccak",
]
[[package]]
name = "context_servers"
version = "0.1.0"
dependencies = [
"anyhow",
"collections",
"futures 0.3.30",
"gpui",
"log",
"parking_lot",
"postage",
"schemars",
"serde",
"serde_json",
"settings",
"smol",
"url",
"util",
"workspace",
]
[[package]]
name = "convert_case"
version = "0.4.0"
@@ -2736,9 +2760,9 @@ dependencies = [
[[package]]
name = "core-foundation-sys"
version = "0.8.6"
version = "0.8.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f"
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
[[package]]
name = "core-graphics"
@@ -3141,12 +3165,12 @@ dependencies = [
[[package]]
name = "ctrlc"
version = "3.4.4"
version = "3.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "672465ae37dc1bc6380a6547a8883d5dd397b0f1faaad4f265726cc7042a5345"
checksum = "90eeab0aa92f3f9b4e87f258c72b139c207d251f9cbc1080a0086b86a8870dd3"
dependencies = [
"nix 0.28.0",
"windows-sys 0.52.0",
"nix 0.29.0",
"windows-sys 0.59.0",
]
[[package]]
@@ -3237,7 +3261,6 @@ dependencies = [
"anyhow",
"gpui",
"indoc",
"lazy_static",
"log",
"paths",
"release_channel",
@@ -3517,7 +3540,6 @@ dependencies = [
"indoc",
"itertools 0.11.0",
"language",
"lazy_static",
"linkify",
"log",
"lsp",
@@ -4280,7 +4302,6 @@ dependencies = [
"git",
"git2",
"gpui",
"lazy_static",
"libc",
"notify",
"objc",
@@ -4606,7 +4627,6 @@ dependencies = [
"git2",
"gpui",
"http_client",
"lazy_static",
"log",
"parking_lot",
"pretty_assertions",
@@ -4805,7 +4825,6 @@ dependencies = [
"http_client",
"image",
"itertools 0.11.0",
"lazy_static",
"linkme",
"log",
"media",
@@ -5033,9 +5052,9 @@ checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
[[package]]
name = "heed"
version = "0.20.3"
version = "0.20.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2bc30da4a93ff8cb98e535d595d6de42731d4719d707bc1c86f579158751a24e"
checksum = "7d4f449bab7320c56003d37732a917e18798e2f1709d80263face2b4f9436ddb"
dependencies = [
"bitflags 2.6.0",
"byteorder",
@@ -5469,6 +5488,7 @@ version = "0.1.0"
dependencies = [
"anyhow",
"async-trait",
"cargo_metadata",
"collections",
"derive_more",
"fs",
@@ -5854,9 +5874,9 @@ dependencies = [
[[package]]
name = "khronos-egl"
version = "5.0.0"
version = "6.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d1382b16c04aeb821453d6215a3c80ba78f24c6595c5aa85653378aabe0c83e3"
checksum = "6aae1df220ece3c0ada96b8153459b67eebe9ae9212258bb0134ae60416fdf76"
dependencies = [
"libc",
"libloading",
@@ -5920,7 +5940,6 @@ dependencies = [
"http_client",
"indoc",
"itertools 0.11.0",
"lazy_static",
"log",
"lsp",
"parking_lot",
@@ -6054,7 +6073,6 @@ dependencies = [
"gpui",
"http_client",
"language",
"lazy_static",
"log",
"lsp",
"node_runtime",
@@ -6156,7 +6174,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4979f22fdb869068da03c9f7528f8297c6fd2606bc3a4affe42e6a823fdb8da4"
dependencies = [
"cfg-if",
"windows-targets 0.52.6",
"windows-targets 0.48.5",
]
[[package]]
@@ -6219,18 +6237,18 @@ dependencies = [
[[package]]
name = "linkme"
version = "0.3.27"
version = "0.3.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ccb76662d78edc9f9bf56360d6919bdacc8b7761227727e5082f128eeb90bbf5"
checksum = "3c943daedff228392b791b33bba32e75737756e80a613e32e246c6ce9cbab20a"
dependencies = [
"linkme-impl",
]
[[package]]
name = "linkme-impl"
version = "0.3.27"
version = "0.3.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8dccda732e04fa3baf2e17cf835bfe2601c7c2edafd64417c627dabae3a8cda"
checksum = "cb26336e6dc7cc76e7927d2c9e7e3bb376d7af65a6f56a0b16c47d18a9b1abc5"
dependencies = [
"proc-macro2",
"quote",
@@ -6289,9 +6307,9 @@ dependencies = [
[[package]]
name = "lmdb-master-sys"
version = "0.2.2"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57640c190703d5ccf4a86aff4aeb749b2d287a8cb1723c76b51f39d77ab53b24"
checksum = "472c3760e2a8d0f61f322fb36788021bb36d573c502b50fa3e2bcaac3ec326c9"
dependencies = [
"cc",
"doxygen-rs",
@@ -7565,6 +7583,29 @@ version = "2.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
[[package]]
name = "performance"
version = "0.1.0"
dependencies = [
"anyhow",
"collections",
"gpui",
"log",
"schemars",
"serde",
"settings",
"util",
"workspace",
]
[[package]]
name = "perplexity"
version = "0.1.0"
dependencies = [
"serde",
"zed_extension_api 0.1.0",
]
[[package]]
name = "pest"
version = "2.7.11"
@@ -8084,10 +8125,9 @@ dependencies = [
"tempfile",
"terminal",
"text",
"unicase",
"unindent",
"util",
"which 6.0.2",
"which 6.0.3",
"worktree",
]
@@ -8766,6 +8806,7 @@ dependencies = [
"ui",
"util",
"uuid",
"windows 0.58.0",
"workspace",
]
@@ -9008,9 +9049,9 @@ dependencies = [
[[package]]
name = "runtimelib"
version = "0.14.0"
version = "0.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c3d817764e3971867351e6103955b17d808f5330e9ef63aaaaab55bf8c664c1"
checksum = "a7d76d28b882a7b889ebb04e79bc2b160b3061821ea596ff0f4a838fc7a76db0"
dependencies = [
"anyhow",
"async-dispatcher",
@@ -9532,18 +9573,18 @@ dependencies = [
[[package]]
name = "serde"
version = "1.0.204"
version = "1.0.208"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bc76f558e0cbb2a839d37354c575f1dc3fdc6546b5be373ba43d95f231bf7c12"
checksum = "cff085d2cb684faa248efb494c39b68e522822ac0de72ccf08109abde717cfb2"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.204"
version = "1.0.208"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e0cd7e117be63d3c3678776753929474f3b04a43a080c744d6b0ae2a8c28e222"
checksum = "24008e81ff7613ed8e5ba0cfaf24e2c2f1e5b8a0495711e44fcd4882fca62bcf"
dependencies = [
"proc-macro2",
"quote",
@@ -9583,9 +9624,9 @@ dependencies = [
[[package]]
name = "serde_json"
version = "1.0.122"
version = "1.0.125"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "784b6203951c57ff748476b126ccb5e8e2959a5c19e5c617ab1956be3dbc68da"
checksum = "83c8e735a073ccf5be70aa8066aa984eaf2fa000db6c8d0100ae605b366d31ed"
dependencies = [
"indexmap 2.3.0",
"itoa",
@@ -9691,7 +9732,6 @@ dependencies = [
"futures 0.3.30",
"gpui",
"indoc",
"lazy_static",
"log",
"paths",
"pretty_assertions",
@@ -9919,6 +9959,13 @@ dependencies = [
"autocfg",
]
[[package]]
name = "slash_commands_example"
version = "0.1.0"
dependencies = [
"zed_extension_api 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "slice-group-by"
version = "0.3.1"
@@ -10096,7 +10143,6 @@ dependencies = [
"collections",
"futures 0.3.30",
"indoc",
"lazy_static",
"libsqlite3-sys",
"parking_lot",
"smol",
@@ -10109,7 +10155,6 @@ dependencies = [
name = "sqlez_macros"
version = "0.1.0"
dependencies = [
"lazy_static",
"sqlez",
"sqlformat",
"syn 1.0.109",
@@ -10508,6 +10553,7 @@ dependencies = [
"theme",
"ui",
"util",
"windows 0.58.0",
]
[[package]]
@@ -10956,7 +11002,6 @@ dependencies = [
"env_logger",
"gpui",
"http_client",
"lazy_static",
"log",
"parking_lot",
"postage",
@@ -11245,9 +11290,9 @@ dependencies = [
[[package]]
name = "tokio"
version = "1.39.2"
version = "1.39.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "daa4fb1bc778bd6f04cbfc4bb2d06a7396a8f299dc33ea1900cedaa316f467b1"
checksum = "9babc99b9923bfa4804bd74722ff02c0381021eafa4db9949217e3be8e84fff5"
dependencies = [
"backtrace",
"bytes 1.7.1",
@@ -11642,9 +11687,9 @@ dependencies = [
[[package]]
name = "tree-sitter-go"
version = "0.21.0"
version = "0.21.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "55cb318be5ccf75f44e054acf6898a5c95d59b53443eed578e16be0cd7ec037f"
checksum = "b8d702a98d3c7e70e466456e58ff2b1ac550bf1e29b97e5770676d2fdabec00d"
dependencies = [
"cc",
"tree-sitter",
@@ -12154,7 +12199,6 @@ dependencies = [
"indoc",
"itertools 0.11.0",
"language",
"lazy_static",
"log",
"lsp",
"multi_buffer",
@@ -12835,9 +12879,9 @@ dependencies = [
[[package]]
name = "which"
version = "6.0.2"
version = "6.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d9c5ed668ee1f17edb3b627225343d210006a90bb1e3745ce1f30b1fb115075"
checksum = "b4ee928febd44d98f2f459a4a79bd4d928591333a494a10a868418ac1b39cf1f"
dependencies = [
"either",
"home",
@@ -13497,7 +13541,6 @@ dependencies = [
"http_client",
"itertools 0.11.0",
"language",
"lazy_static",
"log",
"node_runtime",
"parking_lot",
@@ -13781,7 +13824,7 @@ dependencies = [
[[package]]
name = "zed"
version = "0.149.0"
version = "0.151.0"
dependencies = [
"activity_indicator",
"anyhow",
@@ -13842,6 +13885,7 @@ dependencies = [
"outline_panel",
"parking_lot",
"paths",
"performance",
"profiling",
"project",
"project_panel",
@@ -13929,7 +13973,7 @@ dependencies = [
[[package]]
name = "zed_elixir"
version = "0.0.7"
version = "0.0.8"
dependencies = [
"zed_extension_api 0.0.6",
]
@@ -13968,7 +14012,18 @@ dependencies = [
[[package]]
name = "zed_extension_api"
version = "0.0.7"
version = "0.1.0"
dependencies = [
"serde",
"serde_json",
"wit-bindgen",
]
[[package]]
name = "zed_extension_api"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "594fd10dd0f2f853eb243e2425e7c95938cef49adb81d9602921d002c5e6d9d9"
dependencies = [
"serde",
"serde_json",
@@ -13977,10 +14032,10 @@ dependencies = [
[[package]]
name = "zed_gleam"
version = "0.1.3"
version = "0.2.0"
dependencies = [
"html_to_markdown 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
"zed_extension_api 0.0.7",
"zed_extension_api 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
@@ -13992,7 +14047,7 @@ dependencies = [
[[package]]
name = "zed_haskell"
version = "0.1.0"
version = "0.1.1"
dependencies = [
"zed_extension_api 0.0.6",
]
@@ -14106,9 +14161,9 @@ dependencies = [
[[package]]
name = "zed_zig"
version = "0.1.5"
version = "0.3.0"
dependencies = [
"zed_extension_api 0.0.6",
"zed_extension_api 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]

View File

@@ -19,6 +19,7 @@ members = [
"crates/collections",
"crates/command_palette",
"crates/command_palette_hooks",
"crates/context_servers",
"crates/copilot",
"crates/db",
"crates/dev_server_projects",
@@ -69,6 +70,7 @@ members = [
"crates/outline",
"crates/outline_panel",
"crates/paths",
"crates/performance",
"crates/picker",
"crates/prettier",
"crates/project",
@@ -144,10 +146,12 @@ members = [
"extensions/lua",
"extensions/ocaml",
"extensions/php",
"extensions/perplexity",
"extensions/prisma",
"extensions/purescript",
"extensions/ruff",
"extensions/ruby",
"extensions/slash-commands-example",
"extensions/snippets",
"extensions/svelte",
"extensions/terraform",
@@ -189,6 +193,7 @@ collab_ui = { path = "crates/collab_ui" }
collections = { path = "crates/collections" }
command_palette = { path = "crates/command_palette" }
command_palette_hooks = { path = "crates/command_palette_hooks" }
context_servers = { path = "crates/context_servers" }
copilot = { path = "crates/copilot" }
db = { path = "crates/db" }
dev_server_projects = { path = "crates/dev_server_projects" }
@@ -238,6 +243,7 @@ open_ai = { path = "crates/open_ai" }
outline = { path = "crates/outline" }
outline_panel = { path = "crates/outline_panel" }
paths = { path = "crates/paths" }
performance = { path = "crates/performance" }
picker = { path = "crates/picker" }
plugin = { path = "crates/plugin" }
plugin_macros = { path = "crates/plugin_macros" }
@@ -315,9 +321,9 @@ async-watch = "0.3.1"
async_zip = { version = "0.0.17", features = ["deflate", "deflate64"] }
base64 = "0.22"
bitflags = "2.6.0"
blade-graphics = { git = "https://github.com/kvark/blade", rev = "ac25c77ed8d86c386a541c935ffe0a0f6024e701" }
blade-macros = { git = "https://github.com/kvark/blade", rev = "ac25c77ed8d86c386a541c935ffe0a0f6024e701" }
blade-util = { git = "https://github.com/kvark/blade", rev = "ac25c77ed8d86c386a541c935ffe0a0f6024e701" }
blade-graphics = { git = "https://github.com/kvark/blade", rev = "7f54ddfc001edc48225e6602d3c38ebb855421aa" }
blade-macros = { git = "https://github.com/kvark/blade", rev = "7f54ddfc001edc48225e6602d3c38ebb855421aa" }
blade-util = { git = "https://github.com/kvark/blade", rev = "7f54ddfc001edc48225e6602d3c38ebb855421aa" }
cargo_metadata = "0.18"
cargo_toml = "0.20"
chrono = { version = "0.4", features = ["serde"] }
@@ -353,7 +359,6 @@ isahc = { version = "1.7.2", default-features = false, features = [
] }
itertools = "0.11.0"
jsonwebtoken = "9.3"
lazy_static = "1.4.0"
libc = "0.2"
linkify = "0.10.0"
log = { version = "0.4.16", features = ["kv_unstable_serde", "serde"] }
@@ -377,7 +382,7 @@ rand = "0.8.5"
regex = "1.5"
repair_json = "0.1.0"
rsa = "0.9.6"
runtimelib = { version = "0.14", default-features = false, features = [
runtimelib = { version = "0.15", default-features = false, features = [
"async-dispatcher-runtime",
] }
rusqlite = { version = "0.29.0", features = ["blob", "array", "modern_sqlite"] }
@@ -461,7 +466,7 @@ which = "6.0.0"
wit-component = "0.201"
[workspace.dependencies.async-stripe]
version = "0.37"
version = "0.38"
default-features = false
features = [
"runtime-tokio-hyper-rustls",

View File

@@ -1,4 +1,11 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3.43331 10.1846L6.66616 2.33334L9.89902 10.1846M3.43331 10.1846L1.9995 13.6667M3.43331 10.1846H9.89902M11.3328 13.6667L9.89902 10.1846" stroke="black" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M14.0613 13.647L9.34721 2.33334" stroke="black" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
<g clip-path="url(#clip0_1896_18)">
<path d="M11.094 3.09999H8.952L12.858 12.9H15L11.094 3.09999Z" fill="#1F1F1E"/>
<path d="M4.906 3.09999L1 12.9H3.184L3.98284 10.842H8.06915L8.868 12.9H11.052L7.146 3.09999H4.906ZM4.68928 9.02199L6.026 5.57799L7.3627 9.02199H4.68928Z" fill="#1F1F1E"/>
</g>
<defs>
<clipPath id="clip0_1896_18">
<rect width="14" height="9.8" fill="white" transform="translate(1 3.09999)"/>
</clipPath>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 459 B

After

Width:  |  Height:  |  Size: 530 B

View File

@@ -0,0 +1,12 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="16" height="16" rx="2" fill="black" fill-opacity="0.2"/>
<g clip-path="url(#clip0_1916_18)">
<path d="M10.652 3.79999H8.816L12.164 12.2H14L10.652 3.79999Z" fill="#1F1F1E"/>
<path d="M5.348 3.79999L2 12.2H3.872L4.55672 10.436H8.05927L8.744 12.2H10.616L7.268 3.79999H5.348ZM5.16224 8.87599L6.308 5.92399L7.45374 8.87599H5.16224Z" fill="#1F1F1E"/>
</g>
<defs>
<clipPath id="clip0_1916_18">
<rect width="12" height="8.4" fill="white" transform="translate(2 3.79999)"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 601 B

View File

@@ -1,3 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M14.8695 8.16639C14.8695 12.2258 12.0896 15.1147 7.98425 15.1147C4.04818 15.1147 0.869492 11.9361 0.869492 7.99999C0.869492 4.06393 4.04818 0.885239 7.98425 0.885239C9.90064 0.885239 11.5129 1.58811 12.7551 2.74712L10.8187 4.60901C8.28547 2.16475 3.57482 4.00081 3.57482 7.99999C3.57482 10.4816 5.5572 12.4926 7.98425 12.4926C10.8015 12.4926 11.8572 10.4729 12.0236 9.42581H7.98425V6.97868H14.7576C14.8236 7.34303 14.8695 7.69303 14.8695 8.16639Z" fill="black"/>
<path d="M13.8695 8.14262C13.8695 11.6221 11.4867 14.0984 7.96785 14.0984C4.59408 14.0984 1.86949 11.3738 1.86949 7.99999C1.86949 4.62622 4.59408 1.90163 7.96785 1.90163C9.61048 1.90163 10.9924 2.50409 12.0572 3.49754L10.3974 5.09344C8.22605 2.99836 4.18834 4.57213 4.18834 7.99999C4.18834 10.127 5.88752 11.8508 7.96785 11.8508C10.3826 11.8508 11.2875 10.1197 11.4301 9.22213H7.96785V7.12458H13.7736C13.8301 7.43688 13.8695 7.73688 13.8695 8.14262Z" fill="black"/>
</svg>

Before

Width:  |  Height:  |  Size: 575 B

After

Width:  |  Height:  |  Size: 569 B

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 8.5 KiB

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-database-zap"><ellipse cx="12" cy="5" rx="9" ry="3"/><path d="M3 5V19A9 3 0 0 0 15 21.84"/><path d="M21 5V8"/><path d="M21 12L18 17H22L19 22"/><path d="M3 12A9 3 0 0 0 14.59 14.87"/></svg>

After

Width:  |  Height:  |  Size: 391 B

View File

@@ -1 +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-text-search"><path d="M21 6H3"/><path d="M10 12H3"/><path d="M10 18H3"/><circle cx="17" cy="15" r="3"/><path d="m21 19-1.9-1.9"/></svg>
<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-ellipsis-vertical"><circle cx="12" cy="12" r="1"/><circle cx="12" cy="5" r="1"/><circle cx="12" cy="19" r="1"/></svg>

Before

Width:  |  Height:  |  Size: 338 B

After

Width:  |  Height:  |  Size: 320 B

View File

@@ -1,10 +0,0 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3 13L7.01562 8.98438" stroke="black" stroke-width="1.5" stroke-linecap="round"/>
<path d="M8.6875 7.3125L9.5 6.5" stroke="black" stroke-width="1.5" stroke-linecap="round"/>
<path d="M7 5V3" stroke="black" stroke-width="1.5" stroke-linecap="round"/>
<path d="M12 5V3" stroke="black" stroke-width="1.5" stroke-linecap="round"/>
<path d="M12 10V8" stroke="black" stroke-width="1.5" stroke-linecap="round"/>
<path d="M6 4L8 4" stroke="black" stroke-width="1.5" stroke-linecap="round"/>
<path d="M11 4L13 4" stroke="black" stroke-width="1.5" stroke-linecap="round"/>
<path d="M11 9L13 9" stroke="black" stroke-width="1.5" stroke-linecap="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 787 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-microscope"><path d="M6 18h8"/><path d="M3 22h18"/><path d="M14 22a7 7 0 1 0 0-14h-1"/><path d="M9 14h2"/><path d="M9 12a2 2 0 0 1-2-2V6h6v4a2 2 0 0 1-2 2Z"/><path d="M12 6V3a1 1 0 0 0-1-1H9a1 1 0 0 0-1 1v3"/></svg>

After

Width:  |  Height:  |  Size: 418 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-search-code"><path d="m13 13.5 2-2.5-2-2.5"/><path d="m21 21-4.3-4.3"/><path d="M9 8.5 7 11l2 2.5"/><circle cx="11" cy="11" r="8"/></svg>

After

Width:  |  Height:  |  Size: 340 B

1
assets/icons/slash.svg Normal file
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-slash"><path d="M22 2 2 22"/></svg>

After

Width:  |  Height:  |  Size: 238 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-square-slash"><rect width="18" height="18" x="3" y="3" rx="2"/><line x1="9" x2="15" y1="15" y2="9"/></svg>

After

Width:  |  Height:  |  Size: 309 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-text-select"><path d="M5 3a2 2 0 0 0-2 2"/><path d="M19 3a2 2 0 0 1 2 2"/><path d="M21 19a2 2 0 0 1-2 2"/><path d="M5 21a2 2 0 0 1-2-2"/><path d="M9 3h1"/><path d="M9 21h1"/><path d="M14 3h1"/><path d="M14 21h1"/><path d="M3 9v1"/><path d="M21 9v1"/><path d="M3 14v1"/><path d="M21 14v1"/><line x1="7" x2="15" y1="8" y2="8"/><line x1="7" x2="17" y1="12" y2="12"/><line x1="7" x2="13" y1="16" y2="16"/></svg>

After

Width:  |  Height:  |  Size: 610 B

View File

@@ -437,7 +437,7 @@
"context": "Editor && showing_completions",
"bindings": {
"enter": "editor::ConfirmCompletion",
"tab": "editor::ConfirmCompletion"
"tab": "editor::ComposeCompletion"
}
},
{

View File

@@ -474,7 +474,7 @@
"context": "Editor && showing_completions",
"bindings": {
"enter": "editor::ConfirmCompletion",
"tab": "editor::ConfirmCompletion"
"tab": "editor::ComposeCompletion"
}
},
{

View File

@@ -92,6 +92,7 @@
"g y": "editor::GoToTypeDefinition",
"g shift-i": "editor::GoToImplementation",
"g x": "editor::OpenUrl",
"g f": "editor::OpenFile",
"g n": "vim::SelectNextMatch",
"g shift-n": "vim::SelectPreviousMatch",
"g l": "vim::SelectNext",
@@ -176,19 +177,19 @@
"ctrl-w ctrl-p": "workspace::ActivatePreviousPane",
"ctrl-w shift-w": "workspace::ActivatePreviousPane",
"ctrl-w ctrl-shift-w": "workspace::ActivatePreviousPane",
"ctrl-w v": "pane::SplitLeft",
"ctrl-w ctrl-v": "pane::SplitLeft",
"ctrl-w s": "pane::SplitUp",
"ctrl-w shift-s": "pane::SplitUp",
"ctrl-w ctrl-s": "pane::SplitUp",
"ctrl-w v": "pane::SplitVertical",
"ctrl-w ctrl-v": "pane::SplitVertical",
"ctrl-w s": "pane::SplitHorizontal",
"ctrl-w shift-s": "pane::SplitHorizontal",
"ctrl-w ctrl-s": "pane::SplitHorizontal",
"ctrl-w c": "pane::CloseAllItems",
"ctrl-w ctrl-c": "pane::CloseAllItems",
"ctrl-w q": "pane::CloseAllItems",
"ctrl-w ctrl-q": "pane::CloseAllItems",
"ctrl-w o": "workspace::CloseInactiveTabsAndPanes",
"ctrl-w ctrl-o": "workspace::CloseInactiveTabsAndPanes",
"ctrl-w n": ["workspace::NewFileInDirection", "Up"],
"ctrl-w ctrl-n": ["workspace::NewFileInDirection", "Up"],
"ctrl-w n": "workspace::NewFileSplitHorizontal",
"ctrl-w ctrl-n": "workspace::NewFileSplitHorizontal",
"ctrl-w d": "editor::GoToDefinitionSplit",
"ctrl-w g d": "editor::GoToDefinitionSplit",
"ctrl-w shift-d": "editor::GoToTypeDefinitionSplit",

View File

@@ -32,7 +32,7 @@ Match the indentation in the original file in the inserted {{content_type}}, don
Immediately start with the following format with no remarks:
```
{{INSERTED_CODE}}
\{{INSERTED_CODE}}
```
{{else}}
Edit the section of {{content_type}} in <rewrite_this></rewrite_this> tags based on the following prompt:
@@ -56,6 +56,6 @@ Start at the indentation level in the original file in the rewritten {{content_t
Immediately start with the following format with no remarks:
```
{{REWRITTEN_CODE}}
\{{REWRITTEN_CODE}}
```
{{/if}}

View File

@@ -1,87 +1,302 @@
<workflow>
Guide the user through code changes in numbered steps that focus on individual functions, type definitions, etc.
Surround each distinct step in a <step></step> XML tag.
Surround each distinct step in a <step></step> XML tag. The user will be performing these steps in a code editor
named Zed, which is where they will have entered this prompt and will be seeing the response.
Here's an example of a user-assistant dialog with step numbers and a follow-up question in Rust:
<instructions>
- Use the language of the file for code fence blocks unless otherwise specified.
- Include a code or file action in each step.
- Only put code in separate steps if it should either go in separate files, or in different (non-contiguous) places in the same file.
- Provide error handling and input validation where appropriate.
- Adapt explanations based on the user's perceived level of expertise.
- Include comments in code examples to enhance understanding.
- Consider more complex programming scenarios when relevant.
- Avoid using the terminal to perform filesystem operations such as creating, deleting, renaming, and editing files. Assume the user prefers to use Zed for these operations, unless the prompt suggests that the user is expecting terminal commands.
- When creating a new file, Zed will automatically create any necessary directories in that file's path. So when a step will create a new file in Zed, don't mention needing to create the directory that it will go in.
- Assume the user is performing these actions in the Zed code editor, so avoid redundancies like "In your code editor, ..." or "In Zed, ..."
- Be concise without leaving out important information.
</instructions>
1. User: I want to create a User struct with name and age fields, and a method to print the user's information. Can you help me with that?
A: Certainly! Let's create a User struct with the specified fields and a method to print the information:
<step>Define the User struct
Examples of user-assistant dialogs with step numbers and user follow-up requests:
<example language="rust">
<turn number="1">
<user>Create a Library struct with a vector of Book structs. Include methods to add books and find books by author. Use generics for the book ID.</user>
<assistant>Certainly! Let's create a Library struct with a vector of Book structs, including methods to add books and find books by author, using generics for the book ID:
<step>Define the Book and Library structs
```rust
struct User {
name: String,
age: u32,
use std::fmt::Display;
struct Book<T>
where
T: Display + Clone
{
id: T,
title: String,
author: String,
}
struct Library<T>
where
T: Display + Clone
{
books: Vec<Book<T>>,
}
```
</step>
<step>Implement methods for the User struct
<step>Implement methods for the Library struct
```rust
impl User {
fn new(name: String, age: u32) -> Self {
User { name, age }
impl<T> Library<T>
where
T: Display + Clone
{
fn new() -> Self {
Library { books: Vec::new() }
}
fn print_info(&self) {
println!("Name: {}, Age: {}", self.name, self.age);
fn add_book(&mut self, id: T, title: String, author: String) {
let book = Book { id, title, author };
self.books.push(book);
}
fn find_books_by_author(&self, author: &str) -> Vec<&Book<T>> {
self.books
.iter()
.filter(|book| book.author.to_lowercase() == author.to_lowercase())
.collect()
}
}
```
</step>
This code defines a User struct and implements two methods:
- `new`: a constructor to create a new User
- `print_info`: a method to print the user's information
This code defines:
1. A generic Book struct with an ID of type T, title, and author
2. A Library struct containing a vector of Books
3. Methods to create a new Library, add books, and find books by author
The use of generics allows flexibility in the type of ID used for books.</assistant>
</turn>
2. User: Great! Now, how can I create a User instance and call the print_info method?
A: You can create a User instance and call the print_info method like this:
<turn number="2">
<user>How can I use this Library struct in a main function?</user>
<assistant>Here's how you can use the Library struct in a main function:
<step>Create a User instance and call print_info
<step>Create a main function to demonstrate Library usage
```rust
fn main() {
let user = User::new(String::from("Alice"), 30);
user.print_info();
}
```
</step>
// Create a new Library with string IDs
let mut library = Library::new();
This will output: `Name: Alice, Age: 30`
// Add some books
library.add_book(String::from("001"), String::from("The Rust Programming Language"), String::from("Steve Klabnik"));
library.add_book(String::from("002"), String::from("Programming Rust"), String::from("Jim Blandy"));
library.add_book(String::from("003"), String::from("Rust in Action"), String::from("Tim McNamara"));
3. User: Can you add an email field to the User struct and update the print_info method to include it?
A: Certainly! Let's modify the User struct and update the print_info method:
// Find books by author
let author = "Steve Klabnik";
let books = library.find_books_by_author(author);
<step>Add email field to User struct
```rust
struct User {
name: String,
age: u32,
email: String,
}
```
</step>
<step>Update the new method and print_info method
```rust
impl User {
fn new(name: String, age: u32, email: String) -> Self {
User { name, age, email }
}
fn print_info(&self) {
println!("Name: {}, Age: {}, Email: {}", self.name, self.age, self.email);
println!("Books by {}:", author);
for book in books {
println!("ID: {}, Title: {}", book.id, book.title);
}
}
```
</step>
<step>Update the main function
This main function demonstrates:
1. Creating a new Library instance
2. Adding books with string IDs
3. Finding and displaying books by a specific author</assistant>
</turn>
<turn number="3">
<user>Can you add a method to remove a book by ID and handle potential errors?</user>
<assistant>Certainly! Let's add a method to remove a book by ID and handle potential errors:
<step>Add a remove_book method to the Library struct
```rust
use std::fmt::Display;
use std::error::Error;
use std::fmt;
#[derive(Debug)]
struct BookNotFoundError;
impl Error for BookNotFoundError {}
impl fmt::Display for BookNotFoundError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "Book not found")
}
}
impl<T> Library<T>
where
T: Display + Clone + PartialEq
{
// ... (previous methods remain the same)
fn remove_book(&mut self, id: &T) -> Result<Book<T>, BookNotFoundError> {
if let Some(index) = self.books.iter().position(|book| &book.id == id) {
Ok(self.books.remove(index))
} else {
Err(BookNotFoundError)
}
}
}
```
</step>
<step>Update the main function to demonstrate book removal
```rust
fn main() {
let user = User::new(String::from("Alice"), 30, String::from("alice@example.com"));
user.print_info();
// ... (previous code remains the same)
// Attempt to remove a book
let id_to_remove = String::from("002");
match library.remove_book(&id_to_remove) {
Ok(removed_book) => println!("Removed book: {} by {}", removed_book.title, removed_book.author),
Err(e) => println!("Error removing book: {}", e),
}
// Attempt to remove a non-existent book
let non_existent_id = String::from("999");
match library.remove_book(&non_existent_id) {
Ok(_) => println!("Book removed successfully"),
Err(e) => println!("Error removing book: {}", e),
}
// Display remaining books
println!("\nRemaining books:");
for book in &library.books {
println!("ID: {}, Title: {} by {}", book.id, book.title, book.author);
}
}
```
</step>
This will now output: `Name: Alice, Age: 30, Email: alice@example.com`
The changes include:
1. Adding the `email` field to the User struct
2. Updating the `new` method to accept an email parameter
3. Modifying the `print_info` method to include the email
4. Updating the main function to provide an email when creating a User instance
This update includes:
1. A new remove_book method that returns a Result type
2. A custom BookNotFoundError for error handling
3. Demonstration of successful book removal and error handling for non-existent books
4. Display of remaining books after removal attempts
The remove_book method uses the PartialEq trait to compare book IDs, allowing for flexible ID types while maintaining type safety.</assistant>
</turn>
</example>
<example language="go">
<turn number="1">
<user>Create a Person struct with name, age, and email fields, and a method to greet. Include error handling for age.</user>
<assistant>Certainly! Let's create a Person struct with the specified fields and a method to greet in Go:
<step>Define the Person struct and NewPerson function with error handling
```go
type Person struct {
Name string
Age int
Email string
}
func NewPerson(name string, age int, email string) (*Person, error) {
if age < 0 || age > 150 {
return nil, fmt.Errorf("invalid age: %d", age)
}
return &Person{Name: name, Age: age, Email: email}, nil
}
```
</step>
<step>Implement the Greet method for the Person struct
```go
func (p *Person) Greet() string {
return fmt.Sprintf("Hello, I'm %s, %d years old. You can reach me at %s.", p.Name, p.Age, p.Email)
}
```
</step>
This code defines a Person struct, a NewPerson function with age validation, and a Greet method.</assistant>
</turn>
<turn number="2">
<user>How can I use these in a main function?</user>
<assistant>Here's how to use the Person struct and its methods in a main function:
<step>Create a main function with error handling
```go
func main() {
// Create a new Person instance
person, err := NewPerson("Alice", 30, "alice@example.com")
if err != nil {
fmt.Println("Error creating person:", err)
return
}
// Use the Greet method
fmt.Println(person.Greet())
// Demonstrate error handling with invalid age
invalidPerson, err := NewPerson("Bob", 200, "bob@example.com")
if err != nil {
fmt.Println("Error creating person with invalid age:", err)
}
}
```
</step>
This main function demonstrates:
1. Creating a valid Person instance
2. Using the Greet method
3. Handling errors when creating a Person with an invalid age</assistant>
</turn>
<turn number="3">
<user>Can you add a method to update the email with validation?</user>
<assistant>Certainly! Let's add a method to update the email with validation:
<step>Add UpdateEmail method with validation
```go
import (
"fmt"
"regexp"
)
func (p *Person) UpdateEmail(newEmail string) error {
// Simple email validation using regex
emailRegex := regexp.MustCompile(`^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`)
if !emailRegex.MatchString(newEmail) {
return fmt.Errorf("invalid email format: %s", newEmail)
}
p.Email = newEmail
return nil
}
```
</step>
<step>Update the main function to demonstrate email update
```go
func main() {
// ... (previous code remains the same)
// Demonstrate email update
err = person.UpdateEmail("alice.new@example.com")
if err != nil {
fmt.Println("Error updating email:", err)
} else {
fmt.Println("Updated greeting:", person.Greet())
}
// Demonstrate email update with invalid email
err = person.UpdateEmail("invalid-email")
if err != nil {
fmt.Println("Error updating email:", err)
}
}
```
</step>
This update includes:
1. An UpdateEmail method with email format validation
2. Demonstration of successful email update in the main function
3. Handling of invalid email update attempt</assistant>
</turn>
</example>
</workflow>

View File

@@ -1,22 +1,28 @@
Your task is to map a step from the conversation above to suggestions on symbols inside the provided source files.
<overview>
Your task is to map a step from a workflow to locations in source code where code needs to be changed to fulfill that step.
Given a workflow containing background context plus a series of <step> tags, you will resolve *one* of these step tags to resolve to one or more locations in the code.
With each location, you will produce a brief, one-line description of the changes to be made.
Guidelines:
<guidelines>
- There's no need to describe *what* to do, just *where* to do it.
- Only reference locations that actually exist (unless you're creating a file).
- If creating a file, assume any subsequent updates are included at the time of creation.
- Don't create and then update a file.
- We'll create it in one shot.
- Don't create and then update a file. Always create new files in shot.
- Prefer updating symbols lower in the syntax tree if possible.
- Never include suggestions on a parent symbol and one of its children in the same suggestions block.
- Never nest an operation with another operation or include CDATA or other content. All suggestions are leaf nodes.
- Include a description attribute for each operation with a brief, one-line description of the change to perform.
- Descriptions are required for all suggestions except delete.
- When generating multiple suggestions, ensure the descriptions are specific to each individual operation.
- Avoid referring to the location in the description. Focus on the change to be made, not the location where it's made. That's implicit with the symbol you provide.
- Don't generate multiple suggestions at the same location. Instead, combine them together in a single operation with a succinct combined description.
- To add imports respond with a suggestion where the `"symbol"` key is set to `"#imports"`
</guidelines>
</overview>
Example 1:
User:
<examples>
<example>
<workflow_context>
<message role="user">
```rs src/rectangle.rs
struct Rectangle {
width: f64,
@@ -30,12 +36,21 @@ impl Rectangle {
}
```
We need to add methods to calculate the area and perimeter of the rectangle. Can you help with that?
</message>
<message role="assistant">
Sure, I can help with that!
<step>Add new methods 'calculate_area' and 'calculate_perimeter' to the Rectangle struct</step>
<step>Implement the 'Display' trait for the Rectangle struct</step>
</message>
</workflow_context>
What are the suggestions for the step: <step>Add a new method 'calculate_area' to the Rectangle struct</step>
<step_to_resolve>
Add new methods 'calculate_area' and 'calculate_perimeter' to the Rectangle struct
</step_to_resolve>
A (wrong):
<incorrect_output reason="NEVER append multiple children at the same location.">
{
"title": "Add Rectangle methods",
"suggestions": [
@@ -53,10 +68,9 @@ A (wrong):
}
]
}
</incorrect_output>
This demonstrates what NOT to do. NEVER append multiple children at the same location.
A (corrected):
<correct_output>
{
"title": "Add Rectangle methods",
"suggestions": [
@@ -68,11 +82,13 @@ A (corrected):
}
]
}
</correct_output>
User:
What are the suggestions for the step: <step>Implement the 'Display' trait for the Rectangle struct</step>
<step_to_resolve>
Implement the 'Display' trait for the Rectangle struct
</step_to_resolve>
A:
<output>
{
"title": "Implement Display for Rectangle",
"suggestions": [
@@ -84,10 +100,11 @@ A:
}
]
}
</output>
Example 2:
User:
<example>
<workflow_context>
<message role="user">
```rs src/user.rs
struct User {
pub name: String,
@@ -105,13 +122,19 @@ impl User {
}
}
```
</message>
<message role="assistant">
Certainly!
<step>Update the 'print_info' method to use formatted output</step>
<step>Remove the 'email' field from the User struct</step>
</message>
</workflow_context>
What are the suggestions for the step: <step>Update the 'print_info' method to use formatted output</step>
<step_to_resolve>
Update the 'print_info' method to use formatted output
</step_to_resolve>
A:
<output>
{
"title": "Use formatted output",
"suggestions": [
@@ -123,11 +146,13 @@ A:
}
]
}
</output>
User:
What are the suggestions for the step: <step>Remove the 'email' field from the User struct</step>
<step_to_resolve>
Remove the 'email' field from the User struct
</step_to_resolve>
A:
<output>
{
"title": "Remove email field",
"suggestions": [
@@ -138,10 +163,12 @@ A:
}
]
}
</output>
</example>
Example 3:
User:
<example>
<workflow_context>
<message role="user">
```rs src/vehicle.rs
struct Vehicle {
make: String,
@@ -159,28 +186,36 @@ impl Vehicle {
}
}
```
</message>
<message role="assistant">
<step>Add a 'use std::fmt;' statement at the beginning of the file</step>
<step>Add a new method 'start_engine' in the Vehicle impl block</step>
</message>
</workflow_context>
What are the suggestions for the step: <step>Add a 'use std::fmt;' statement at the beginning of the file</step>
<step_to_resolve>
Add a 'use std::fmt;' statement at the beginning of the file
</step_to_resolve>
A:
<output>
{
"title": "Add use std::fmt statement",
"suggestions": [
{
"kind": "PrependChild",
"path": "src/vehicle.rs",
"symbol": "#imports",
"description": "Add 'use std::fmt' statement"
}
]
}
</output>
User:
What are the suggestions for the step: <step>Add a new method 'start_engine' in the Vehicle impl block</step>
<step_to_resolve>
Add a new method 'start_engine' in the Vehicle impl block
</step_to_resolve>
A:
<output>
{
"title": "Add start_engine method",
"suggestions": [
@@ -192,10 +227,12 @@ A:
}
]
}
</output>
</example>
Example 4:
User:
<example>
<workflow_context>
<message role="user">
```rs src/employee.rs
struct Employee {
name: String,
@@ -219,12 +256,18 @@ impl Employee {
}
}
```
</message>
<message role="assistant">
<step>Make salary an f32</step>
<step>Remove the 'department' field and update the 'print_details' method</step>
</message>
</workflow_context>
What are the suggestions for the step: <step>Make salary an f32</step>
<step_to_resolve>
Make salary an f32
</step_to_resolve>
A (wrong):
<incorrect_output reason="NEVER include suggestions on a parent symbol and one of its children in the same suggestions block.">
{
"title": "Change salary to f32",
"suggestions": [
@@ -242,10 +285,9 @@ A (wrong):
}
]
}
</incorrect_output>
This example demonstrates what not to do. `struct Employee salary` is a child of `struct Employee`.
A (corrected):
<correct_output>
{
"title": "Change salary to f32",
"suggestions": [
@@ -257,11 +299,13 @@ A (corrected):
}
]
}
</correct_output>
User:
What are the correct suggestions for the step: <step>Remove the 'department' field and update the 'print_details' method</step>
<step_to_resolve>
Remove the 'department' field and update the 'print_details' method
</step_to_resolve>
A:
<output>
{
"title": "Remove department",
"suggestions": [
@@ -278,10 +322,12 @@ A:
}
]
}
</output>
</example>
Example 5:
User:
<example>
<workflow_context>
<message role="user">
```rs src/game.rs
struct Player {
name: String,
@@ -305,10 +351,17 @@ impl Game {
}
}
```
</message>
<message role="assistant">
<step>Add a 'level' field to Player and update the 'new' method</step>
</message>
</workflow_context>
A:
<step_to_resolve>
Add a 'level' field to Player and update the 'new' method
</step_to_resolve>
<output>
{
"title": "Add level field to Player",
"suggestions": [
@@ -326,10 +379,12 @@ A:
}
]
}
</output>
</example>
Example 6:
User:
<example>
<workflow_context>
<message role="user">
```rs src/config.rs
use std::collections::HashMap;
@@ -343,16 +398,24 @@ impl Config {
}
}
```
</message>
<message role="assistant">
<step>Add a 'load_from_file' method to Config and import necessary modules</step>
</message>
</workflow_context>
A:
<step_to_resolve>
Add a 'load_from_file' method to Config and import necessary modules
</step_to_resolve>
<output>
{
"title": "Add load_from_file method",
"suggestions": [
{
"kind": "PrependChild",
"path": "src/config.rs",
"symbol": "#imports",
"description": "Import std::fs and std::io modules"
},
{
@@ -363,10 +426,12 @@ A:
}
]
}
</output>
</example>
Example 7:
User:
<example>
<workflow_context>
<message role="user">
```rs src/database.rs
pub(crate) struct Database {
connection: Connection,
@@ -383,10 +448,17 @@ impl Database {
}
}
```
</message>
<message role="assistant">
<step>Add error handling to the 'query' method and create a custom error type</step>
</message>
</workflow_context>
A:
<step_to_resolve>
Add error handling to the 'query' method and create a custom error type
</step_to_resolve>
<output>
{
"title": "Add error handling to query",
"suggestions": [
@@ -409,5 +481,16 @@ A:
}
]
}
</output>
</example>
</examples>
Now generate the suggestions for the following step:
<workflow_context>
{{{workflow_context}}}
</workflow_context>
<step_to_resolve>
{{{step_to_resolve}}}
</step_to_resolve>

View File

@@ -64,9 +64,15 @@
"ui_font_weight": 400,
// The default font size for text in the UI
"ui_font_size": 16,
// How much to fade out unused code.
"unnecessary_code_fade": 0.3,
// The factor to grow the active pane by. Defaults to 1.0
// which gives the same size as all other panes.
"active_pane_magnification": 1.0,
// The direction that you want to split panes horizontally. Defaults to "up"
"pane_split_direction_horizontal": "up",
// The direction that you want to split panes horizontally. Defaults to "left"
"pane_split_direction_vertical": "left",
// Centered layout related settings.
"centered_layout": {
// The relative width of the left padding of the central pane from the
@@ -395,9 +401,22 @@
// The default model to use when creating new contexts.
"default_model": {
// The provider to use.
"provider": "openai",
"provider": "zed.dev",
// The model to use.
"model": "gpt-4o"
"model": "claude-3-5-sonnet"
}
},
// The settings for slash commands.
"slash_commands": {
// Settings for the `/docs` slash command.
"docs": {
// Whether `/docs` is enabled.
"enabled": false
},
// Settings for the `/project` slash command.
"project": {
// Whether `/project` is enabled.
"enabled": false
}
},
// Whether the screen sharing icon is shown in the os status bar.
@@ -823,6 +842,7 @@
"language_servers": ["starpls", "!buck2-lsp", "..."]
},
"Svelte": {
"language_servers": ["svelte-language-server", "..."],
"prettier": {
"allowed": true,
"plugins": ["prettier-plugin-svelte"]
@@ -846,6 +866,7 @@
}
},
"Vue.js": {
"language_servers": ["vue-language-server", "..."],
"prettier": {
"allowed": true
}
@@ -997,5 +1018,16 @@
// ]
// }
// ]
"ssh_connections": null
"ssh_connections": null,
// Configures the Context Server Protocol binaries
//
// Examples:
// {
// "id": "server-1",
// "executable": "/path",
// "args": ['arg1", "args2"]
// }
"experimental.context_servers": {
"servers": []
}
}

View File

@@ -33,5 +33,31 @@ services:
volumes:
- ./livekit.yaml:/livekit.yaml
postgrest_app:
image: postgrest/postgrest
container_name: postgrest_app
ports:
- 8081:8081
environment:
PGRST_DB_URI: postgres://postgres@postgres:5432/zed
volumes:
- ./crates/collab/postgrest_app.conf:/etc/postgrest.conf
command: postgrest /etc/postgrest.conf
depends_on:
- postgres
postgrest_llm:
image: postgrest/postgrest
container_name: postgrest_llm
ports:
- 8082:8082
environment:
PGRST_DB_URI: postgres://postgres@postgres:5432/zed_llm
volumes:
- ./crates/collab/postgrest_llm.conf:/etc/postgrest.conf
command: postgrest /etc/postgrest.conf
depends_on:
- postgres
volumes:
postgres_data:

View File

@@ -3,10 +3,9 @@ use editor::Editor;
use extension::ExtensionStore;
use futures::StreamExt;
use gpui::{
actions, anchored, deferred, percentage, Animation, AnimationExt as _, AppContext, CursorStyle,
DismissEvent, EventEmitter, InteractiveElement as _, Model, ParentElement as _, Render,
SharedString, StatefulInteractiveElement, Styled, Transformation, View, ViewContext,
VisualContext as _,
actions, percentage, Animation, AnimationExt as _, AppContext, CursorStyle, EventEmitter,
InteractiveElement as _, Model, ParentElement as _, Render, SharedString,
StatefulInteractiveElement, Styled, Transformation, View, ViewContext, VisualContext as _,
};
use language::{
LanguageRegistry, LanguageServerBinaryStatus, LanguageServerId, LanguageServerName,
@@ -14,7 +13,7 @@ use language::{
use project::{LanguageServerProgress, Project};
use smallvec::SmallVec;
use std::{cmp::Reverse, fmt::Write, sync::Arc, time::Duration};
use ui::{prelude::*, ContextMenu};
use ui::{prelude::*, ButtonLike, ContextMenu, PopoverMenu, PopoverMenuHandle};
use workspace::{item::ItemHandle, StatusItemView, Workspace};
actions!(activity_indicator, [ShowErrorMessage]);
@@ -27,7 +26,7 @@ pub struct ActivityIndicator {
statuses: Vec<LspStatus>,
project: Model<Project>,
auto_updater: Option<Model<AutoUpdater>>,
context_menu: Option<View<ContextMenu>>,
context_menu_handle: PopoverMenuHandle<ContextMenu>,
}
struct LspStatus {
@@ -79,7 +78,7 @@ impl ActivityIndicator {
statuses: Default::default(),
project: project.clone(),
auto_updater,
context_menu: None,
context_menu_handle: Default::default(),
}
});
@@ -368,72 +367,7 @@ impl ActivityIndicator {
}
fn toggle_language_server_work_context_menu(&mut self, cx: &mut ViewContext<Self>) {
if self.context_menu.take().is_some() {
return;
}
self.build_lsp_work_context_menu(cx);
cx.notify();
}
fn build_lsp_work_context_menu(&mut self, cx: &mut ViewContext<Self>) {
let mut has_work = false;
let this = cx.view().downgrade();
let context_menu = ContextMenu::build(cx, |mut menu, cx| {
for work in self.pending_language_server_work(cx) {
has_work = true;
let this = this.clone();
let title = SharedString::from(
work.progress
.title
.as_deref()
.unwrap_or(work.progress_token)
.to_string(),
);
if work.progress.is_cancellable {
let language_server_id = work.language_server_id;
let token = work.progress_token.to_string();
menu = menu.custom_entry(
move |_| {
h_flex()
.w_full()
.justify_between()
.child(Label::new(title.clone()))
.child(Icon::new(IconName::XCircle))
.into_any_element()
},
move |cx| {
this.update(cx, |this, cx| {
this.project.update(cx, |project, cx| {
project.cancel_language_server_work(
language_server_id,
Some(token.clone()),
cx,
);
});
this.context_menu.take();
})
.ok();
},
);
} else {
menu = menu.label(title.clone());
}
}
menu
});
if has_work {
cx.subscribe(&context_menu, |this, _, _: &DismissEvent, cx| {
this.context_menu.take();
cx.notify();
})
.detach();
cx.focus_view(&context_menu);
self.context_menu = Some(context_menu);
cx.notify();
}
self.context_menu_handle.toggle(cx);
}
}
@@ -455,19 +389,72 @@ impl Render for ActivityIndicator {
on_click(this, cx);
}))
}
result
.gap_2()
.children(content.icon)
.child(Label::new(SharedString::from(content.message)).size(LabelSize::Small))
.children(self.context_menu.as_ref().map(|menu| {
deferred(
anchored()
.anchor(gpui::AnchorCorner::BottomLeft)
.child(menu.clone()),
let this = cx.view().downgrade();
result.gap_2().child(
PopoverMenu::new("activity-indicator-popover")
.trigger(
ButtonLike::new("activity-indicator-trigger").child(
h_flex()
.gap_2()
.children(content.icon)
.child(Label::new(content.message).size(LabelSize::Small)),
),
)
.with_priority(1)
}))
.anchor(gpui::AnchorCorner::BottomLeft)
.menu(move |cx| {
let strong_this = this.upgrade()?;
ContextMenu::build(cx, |mut menu, cx| {
for work in strong_this.read(cx).pending_language_server_work(cx) {
let this = this.clone();
let mut title = work
.progress
.title
.as_deref()
.unwrap_or(work.progress_token)
.to_owned();
if work.progress.is_cancellable {
let language_server_id = work.language_server_id;
let token = work.progress_token.to_string();
let title = SharedString::from(title);
menu = menu.custom_entry(
move |_| {
h_flex()
.w_full()
.justify_between()
.child(Label::new(title.clone()))
.child(Icon::new(IconName::XCircle))
.into_any_element()
},
move |cx| {
this.update(cx, |this, cx| {
this.project.update(cx, |project, cx| {
project.cancel_language_server_work(
language_server_id,
Some(token.clone()),
cx,
);
});
this.context_menu_handle.hide(cx);
cx.notify();
})
.ok();
},
);
} else {
if let Some(progress_message) = work.progress.message.as_ref() {
title.push_str(": ");
title.push_str(progress_message);
}
menu = menu.label(title);
}
}
menu
})
.into()
}),
)
}
}

View File

@@ -17,6 +17,7 @@ path = "src/anthropic.rs"
[dependencies]
anyhow.workspace = true
chrono.workspace = true
futures.workspace = true
http_client.workspace = true
isahc.workspace = true
@@ -24,6 +25,8 @@ schemars = { workspace = true, optional = true }
serde.workspace = true
serde_json.workspace = true
strum.workspace = true
thiserror.workspace = true
util.workspace = true
[dev-dependencies]
tokio.workspace = true

View File

@@ -1,17 +1,30 @@
mod supported_countries;
use anyhow::{anyhow, Result};
use anyhow::{anyhow, Context, Result};
use chrono::{DateTime, Utc};
use futures::{io::BufReader, stream::BoxStream, AsyncBufReadExt, AsyncReadExt, Stream, StreamExt};
use http_client::{AsyncBody, HttpClient, Method, Request as HttpRequest};
use isahc::config::Configurable;
use isahc::http::{HeaderMap, HeaderValue};
use serde::{Deserialize, Serialize};
use std::time::Duration;
use strum::EnumIter;
use std::{pin::Pin, str::FromStr};
use strum::{EnumIter, EnumString};
use thiserror::Error;
use util::ResultExt as _;
pub use supported_countries::*;
pub const ANTHROPIC_API_URL: &'static str = "https://api.anthropic.com";
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)]
pub struct AnthropicModelCacheConfiguration {
pub min_total_token: usize,
pub should_speculate: bool,
pub max_cache_anchors: usize,
}
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, EnumIter)]
pub enum Model {
@@ -28,8 +41,13 @@ pub enum Model {
Custom {
name: String,
max_tokens: usize,
/// The name displayed in the UI, such as in the assistant panel model dropdown menu.
display_name: Option<String>,
/// Override this model with a different Anthropic model for tool calls.
tool_override: Option<String>,
/// Indicates whether this custom model supports caching.
cache_configuration: Option<AnthropicModelCacheConfiguration>,
max_output_tokens: Option<u32>,
},
}
@@ -64,7 +82,24 @@ impl Model {
Self::Claude3Opus => "Claude 3 Opus",
Self::Claude3Sonnet => "Claude 3 Sonnet",
Self::Claude3Haiku => "Claude 3 Haiku",
Self::Custom { name, .. } => name,
Self::Custom {
name, display_name, ..
} => display_name.as_ref().unwrap_or(name),
}
}
pub fn cache_configuration(&self) -> Option<AnthropicModelCacheConfiguration> {
match self {
Self::Claude3_5Sonnet | Self::Claude3Haiku => Some(AnthropicModelCacheConfiguration {
min_total_token: 2_048,
should_speculate: true,
max_cache_anchors: 4,
}),
Self::Custom {
cache_configuration,
..
} => cache_configuration.clone(),
_ => None,
}
}
@@ -78,6 +113,16 @@ impl Model {
}
}
pub fn max_output_tokens(&self) -> u32 {
match self {
Self::Claude3Opus | Self::Claude3Sonnet | Self::Claude3Haiku => 4_096,
Self::Claude3_5Sonnet => 8_192,
Self::Custom {
max_output_tokens, ..
} => max_output_tokens.unwrap_or(4_096),
}
}
pub fn tool_model_id(&self) -> &str {
if let Self::Custom {
tool_override: Some(tool_override),
@@ -96,34 +141,53 @@ pub async fn complete(
api_url: &str,
api_key: &str,
request: Request,
) -> Result<Response> {
) -> Result<Response, AnthropicError> {
let uri = format!("{api_url}/v1/messages");
let request_builder = HttpRequest::builder()
.method(Method::POST)
.uri(uri)
.header("Anthropic-Version", "2023-06-01")
.header("Anthropic-Beta", "tools-2024-04-04")
.header(
"Anthropic-Beta",
"tools-2024-04-04,prompt-caching-2024-07-31,max-tokens-3-5-sonnet-2024-07-15",
)
.header("X-Api-Key", api_key)
.header("Content-Type", "application/json");
let serialized_request = serde_json::to_string(&request)?;
let request = request_builder.body(AsyncBody::from(serialized_request))?;
let serialized_request =
serde_json::to_string(&request).context("failed to serialize request")?;
let request = request_builder
.body(AsyncBody::from(serialized_request))
.context("failed to construct request body")?;
let mut response = client.send(request).await?;
let mut response = client
.send(request)
.await
.context("failed to send request to Anthropic")?;
if response.status().is_success() {
let mut body = Vec::new();
response.body_mut().read_to_end(&mut body).await?;
let response_message: Response = serde_json::from_slice(&body)?;
response
.body_mut()
.read_to_end(&mut body)
.await
.context("failed to read response body")?;
let response_message: Response =
serde_json::from_slice(&body).context("failed to deserialize response body")?;
Ok(response_message)
} else {
let mut body = Vec::new();
response.body_mut().read_to_end(&mut body).await?;
let body_str = std::str::from_utf8(&body)?;
Err(anyhow!(
response
.body_mut()
.read_to_end(&mut body)
.await
.context("failed to read response body")?;
let body_str =
std::str::from_utf8(&body).context("failed to parse response body as UTF-8")?;
Err(AnthropicError::Other(anyhow!(
"Failed to connect to API: {} {}",
response.status(),
body_str
))
)))
}
}
@@ -133,7 +197,67 @@ pub async fn stream_completion(
api_key: &str,
request: Request,
low_speed_timeout: Option<Duration>,
) -> Result<BoxStream<'static, Result<Event>>> {
) -> Result<BoxStream<'static, Result<Event, AnthropicError>>, AnthropicError> {
stream_completion_with_rate_limit_info(client, api_url, api_key, request, low_speed_timeout)
.await
.map(|output| output.0)
}
/// https://docs.anthropic.com/en/api/rate-limits#response-headers
#[derive(Debug)]
pub struct RateLimitInfo {
pub requests_limit: usize,
pub requests_remaining: usize,
pub requests_reset: DateTime<Utc>,
pub tokens_limit: usize,
pub tokens_remaining: usize,
pub tokens_reset: DateTime<Utc>,
}
impl RateLimitInfo {
fn from_headers(headers: &HeaderMap<HeaderValue>) -> Result<Self> {
let tokens_limit = get_header("anthropic-ratelimit-tokens-limit", headers)?.parse()?;
let requests_limit = get_header("anthropic-ratelimit-requests-limit", headers)?.parse()?;
let tokens_remaining =
get_header("anthropic-ratelimit-tokens-remaining", headers)?.parse()?;
let requests_remaining =
get_header("anthropic-ratelimit-requests-remaining", headers)?.parse()?;
let requests_reset = get_header("anthropic-ratelimit-requests-reset", headers)?;
let tokens_reset = get_header("anthropic-ratelimit-tokens-reset", headers)?;
let requests_reset = DateTime::parse_from_rfc3339(requests_reset)?.to_utc();
let tokens_reset = DateTime::parse_from_rfc3339(tokens_reset)?.to_utc();
Ok(Self {
requests_limit,
tokens_limit,
requests_remaining,
tokens_remaining,
requests_reset,
tokens_reset,
})
}
}
fn get_header<'a>(key: &str, headers: &'a HeaderMap) -> Result<&'a str, anyhow::Error> {
Ok(headers
.get(key)
.ok_or_else(|| anyhow!("missing header `{key}`"))?
.to_str()?)
}
pub async fn stream_completion_with_rate_limit_info(
client: &dyn HttpClient,
api_url: &str,
api_key: &str,
request: Request,
low_speed_timeout: Option<Duration>,
) -> Result<
(
BoxStream<'static, Result<Event, AnthropicError>>,
Option<RateLimitInfo>,
),
AnthropicError,
> {
let request = StreamingRequest {
base: request,
stream: true,
@@ -143,19 +267,29 @@ pub async fn stream_completion(
.method(Method::POST)
.uri(uri)
.header("Anthropic-Version", "2023-06-01")
.header("Anthropic-Beta", "tools-2024-04-04")
.header(
"Anthropic-Beta",
"tools-2024-04-04,prompt-caching-2024-07-31,max-tokens-3-5-sonnet-2024-07-15",
)
.header("X-Api-Key", api_key)
.header("Content-Type", "application/json");
if let Some(low_speed_timeout) = low_speed_timeout {
request_builder = request_builder.low_speed_timeout(100, low_speed_timeout);
}
let serialized_request = serde_json::to_string(&request)?;
let request = request_builder.body(AsyncBody::from(serialized_request))?;
let serialized_request =
serde_json::to_string(&request).context("failed to serialize request")?;
let request = request_builder
.body(AsyncBody::from(serialized_request))
.context("failed to construct request body")?;
let mut response = client.send(request).await?;
let mut response = client
.send(request)
.await
.context("failed to send request to Anthropic")?;
if response.status().is_success() {
let rate_limits = RateLimitInfo::from_headers(response.headers());
let reader = BufReader::new(response.into_body());
Ok(reader
let stream = reader
.lines()
.filter_map(|line| async move {
match line {
@@ -163,48 +297,54 @@ pub async fn stream_completion(
let line = line.strip_prefix("data: ")?;
match serde_json::from_str(line) {
Ok(response) => Some(Ok(response)),
Err(error) => Some(Err(anyhow!(error))),
Err(error) => Some(Err(AnthropicError::Other(anyhow!(error)))),
}
}
Err(error) => Some(Err(anyhow!(error))),
Err(error) => Some(Err(AnthropicError::Other(anyhow!(error)))),
}
})
.boxed())
.boxed();
Ok((stream, rate_limits.log_err()))
} else {
let mut body = Vec::new();
response.body_mut().read_to_end(&mut body).await?;
response
.body_mut()
.read_to_end(&mut body)
.await
.context("failed to read response body")?;
let body_str = std::str::from_utf8(&body)?;
let body_str =
std::str::from_utf8(&body).context("failed to parse response body as UTF-8")?;
match serde_json::from_str::<Event>(body_str) {
Ok(Event::Error { error }) => Err(api_error_to_err(error)),
Ok(_) => Err(anyhow!(
Ok(Event::Error { error }) => Err(AnthropicError::ApiError(error)),
Ok(_) => Err(AnthropicError::Other(anyhow!(
"Unexpected success response while expecting an error: '{body_str}'",
)),
Err(_) => Err(anyhow!(
))),
Err(_) => Err(AnthropicError::Other(anyhow!(
"Failed to connect to API: {} {}",
response.status(),
body_str,
)),
))),
}
}
}
pub fn extract_text_from_events(
response: impl Stream<Item = Result<Event>>,
) -> impl Stream<Item = Result<String>> {
response: impl Stream<Item = Result<Event, AnthropicError>>,
) -> impl Stream<Item = Result<String, AnthropicError>> {
response.filter_map(|response| async move {
match response {
Ok(response) => match response {
Event::ContentBlockStart { content_block, .. } => match content_block {
Content::Text { text } => Some(Ok(text)),
Content::Text { text, .. } => Some(Ok(text)),
_ => None,
},
Event::ContentBlockDelta { delta, .. } => match delta {
ContentDelta::TextDelta { text } => Some(Ok(text)),
_ => None,
},
Event::Error { error } => Some(Err(api_error_to_err(error))),
Event::Error { error } => Some(Err(AnthropicError::ApiError(error))),
_ => None,
},
Err(error) => Some(Err(error)),
@@ -212,13 +352,60 @@ pub fn extract_text_from_events(
})
}
fn api_error_to_err(
ApiError {
error_type,
message,
}: ApiError,
) -> anyhow::Error {
anyhow!("API error. Type: '{error_type}', message: '{message}'",)
pub async fn extract_tool_args_from_events(
tool_name: String,
mut events: Pin<Box<dyn Send + Stream<Item = Result<Event>>>>,
) -> Result<impl Send + Stream<Item = Result<String>>> {
let mut tool_use_index = None;
while let Some(event) = events.next().await {
if let Event::ContentBlockStart {
index,
content_block,
} = event?
{
if let Content::ToolUse { name, .. } = content_block {
if name == tool_name {
tool_use_index = Some(index);
break;
}
}
}
}
let Some(tool_use_index) = tool_use_index else {
return Err(anyhow!("tool not used"));
};
Ok(events.filter_map(move |event| {
let result = match event {
Err(error) => Some(Err(error)),
Ok(Event::ContentBlockDelta { index, delta }) => match delta {
ContentDelta::TextDelta { .. } => None,
ContentDelta::InputJsonDelta { partial_json } => {
if index == tool_use_index {
Some(Ok(partial_json))
} else {
None
}
}
},
_ => None,
};
async move { result }
}))
}
#[derive(Debug, Serialize, Deserialize, Copy, Clone)]
#[serde(rename_all = "lowercase")]
pub enum CacheControlType {
Ephemeral,
}
#[derive(Debug, Serialize, Deserialize, Copy, Clone)]
pub struct CacheControl {
#[serde(rename = "type")]
pub cache_type: CacheControlType,
}
#[derive(Debug, Serialize, Deserialize)]
@@ -227,7 +414,7 @@ pub struct Message {
pub content: Vec<Content>,
}
#[derive(Debug, Serialize, Deserialize)]
#[derive(Debug, Serialize, Deserialize, Eq, PartialEq, Hash)]
#[serde(rename_all = "lowercase")]
pub enum Role {
User,
@@ -238,19 +425,31 @@ pub enum Role {
#[serde(tag = "type")]
pub enum Content {
#[serde(rename = "text")]
Text { text: String },
Text {
text: String,
#[serde(skip_serializing_if = "Option::is_none")]
cache_control: Option<CacheControl>,
},
#[serde(rename = "image")]
Image { source: ImageSource },
Image {
source: ImageSource,
#[serde(skip_serializing_if = "Option::is_none")]
cache_control: Option<CacheControl>,
},
#[serde(rename = "tool_use")]
ToolUse {
id: String,
name: String,
input: serde_json::Value,
#[serde(skip_serializing_if = "Option::is_none")]
cache_control: Option<CacheControl>,
},
#[serde(rename = "tool_result")]
ToolResult {
tool_use_id: String,
content: String,
#[serde(skip_serializing_if = "Option::is_none")]
cache_control: Option<CacheControl>,
},
}
@@ -374,9 +573,53 @@ pub struct MessageDelta {
pub stop_sequence: Option<String>,
}
#[derive(Error, Debug)]
pub enum AnthropicError {
#[error("an error occurred while interacting with the Anthropic API: {error_type}: {message}", error_type = .0.error_type, message = .0.message)]
ApiError(ApiError),
#[error("{0}")]
Other(#[from] anyhow::Error),
}
#[derive(Debug, Serialize, Deserialize)]
pub struct ApiError {
#[serde(rename = "type")]
pub error_type: String,
pub message: String,
}
/// An Anthropic API error code.
/// https://docs.anthropic.com/en/api/errors#http-errors
#[derive(Debug, PartialEq, Eq, Clone, Copy, EnumString)]
#[strum(serialize_all = "snake_case")]
pub enum ApiErrorCode {
/// 400 - `invalid_request_error`: There was an issue with the format or content of your request.
InvalidRequestError,
/// 401 - `authentication_error`: There's an issue with your API key.
AuthenticationError,
/// 403 - `permission_error`: Your API key does not have permission to use the specified resource.
PermissionError,
/// 404 - `not_found_error`: The requested resource was not found.
NotFoundError,
/// 413 - `request_too_large`: Request exceeds the maximum allowed number of bytes.
RequestTooLarge,
/// 429 - `rate_limit_error`: Your account has hit a rate limit.
RateLimitError,
/// 500 - `api_error`: An unexpected error has occurred internal to Anthropic's systems.
ApiError,
/// 529 - `overloaded_error`: Anthropic's API is temporarily overloaded.
OverloadedError,
}
impl ApiError {
pub fn code(&self) -> Option<ApiErrorCode> {
ApiErrorCode::from_str(&self.error_type).ok()
}
pub fn is_rate_limit_error(&self) -> bool {
match self.error_type.as_str() {
"rate_limit_error" => true,
_ => false,
}
}
}

View File

@@ -33,11 +33,13 @@ clock.workspace = true
collections.workspace = true
command_palette_hooks.workspace = true
db.workspace = true
context_servers.workspace = true
editor.workspace = true
feature_flags.workspace = true
fs.workspace = true
futures.workspace = true
fuzzy.workspace = true
globset.workspace = true
gpui.workspace = true
handlebars.workspace = true
heed.workspace = true
@@ -66,8 +68,8 @@ semantic_index.workspace = true
serde.workspace = true
serde_json.workspace = true
settings.workspace = true
similar.workspace = true
smallvec.workspace = true
similar.workspace = true
smol.workspace = true
telemetry_events.workspace = true
terminal.workspace = true

View File

@@ -9,8 +9,11 @@ mod model_selector;
mod prompt_library;
mod prompts;
mod slash_command;
pub(crate) mod slash_command_picker;
pub mod slash_command_settings;
mod streaming_diff;
mod terminal_inline_assistant;
mod workflow;
pub use assistant_panel::{AssistantPanel, AssistantPanelEvent};
use assistant_settings::AssistantSettings;
@@ -18,9 +21,11 @@ use assistant_slash_command::SlashCommandRegistry;
use client::{proto, Client};
use command_palette_hooks::CommandPaletteFilter;
pub use context::*;
use context_servers::ContextServerRegistry;
pub use context_store::*;
use feature_flags::FeatureFlagAppExt;
use fs::Fs;
use gpui::Context as _;
use gpui::{actions, impl_actions, AppContext, Global, SharedString, UpdateGlobal};
use indexed_docs::IndexedDocsRegistry;
pub(crate) use inline_assistant::*;
@@ -29,17 +34,21 @@ use language_model::{
};
pub(crate) use model_selector::*;
pub use prompts::PromptBuilder;
use prompts::PromptLoadingParams;
use semantic_index::{CloudEmbeddingProvider, SemanticIndex};
use serde::{Deserialize, Serialize};
use settings::{update_settings_file, Settings, SettingsStore};
use slash_command::{
active_command, default_command, diagnostics_command, docs_command, fetch_command,
context_server_command, default_command, diagnostics_command, docs_command, fetch_command,
file_command, now_command, project_command, prompt_command, search_command, symbols_command,
tabs_command, term_command, workflow_command,
tab_command, terminal_command, workflow_command,
};
use std::sync::Arc;
pub(crate) use streaming_diff::*;
use util::ResultExt;
pub use workflow::*;
use crate::slash_command_settings::SlashCommandSettings;
actions!(
assistant,
@@ -51,16 +60,14 @@ actions!(
InsertIntoEditor,
ToggleFocus,
InsertActivePrompt,
ShowConfiguration,
DeployHistory,
DeployPromptLibrary,
ConfirmCommand,
ToggleModelSelector,
DebugWorkflowSteps
]
);
const DEFAULT_CONTEXT_LINES: usize = 20;
const DEFAULT_CONTEXT_LINES: usize = 50;
#[derive(Clone, Default, Deserialize, PartialEq)]
pub struct InlineAssist {
@@ -97,6 +104,7 @@ pub enum MessageStatus {
Pending,
Done,
Error(SharedString),
Canceled,
}
impl MessageStatus {
@@ -107,6 +115,7 @@ impl MessageStatus {
Some(proto::context_message_status::Variant::Error(error)) => {
MessageStatus::Error(error.message.into())
}
Some(proto::context_message_status::Variant::Canceled(_)) => MessageStatus::Canceled,
None => MessageStatus::Pending,
}
}
@@ -130,6 +139,11 @@ impl MessageStatus {
},
)),
},
MessageStatus::Canceled => proto::ContextMessageStatus {
variant: Some(proto::context_message_status::Variant::Canceled(
proto::context_message_status::Canceled {},
)),
},
}
}
}
@@ -167,9 +181,15 @@ impl Assistant {
}
}
pub fn init(fs: Arc<dyn Fs>, client: Arc<Client>, cx: &mut AppContext) -> Arc<PromptBuilder> {
pub fn init(
fs: Arc<dyn Fs>,
client: Arc<Client>,
stdout_is_a_pty: bool,
cx: &mut AppContext,
) -> Arc<PromptBuilder> {
cx.set_global(Assistant::default());
AssistantSettings::register(cx);
SlashCommandSettings::register(cx);
// TODO: remove this when 0.148.0 is released.
if AssistantSettings::get_global(cx).using_outdated_settings_version {
@@ -201,11 +221,18 @@ pub fn init(fs: Arc<dyn Fs>, client: Arc<Client>, cx: &mut AppContext) -> Arc<Pr
init_language_model_settings(cx);
assistant_slash_command::init(cx);
assistant_panel::init(cx);
context_servers::init(cx);
let prompt_builder = prompts::PromptBuilder::new(Some((fs.clone(), cx)))
.log_err()
.map(Arc::new)
.unwrap_or_else(|| Arc::new(prompts::PromptBuilder::new(None).unwrap()));
let prompt_builder = prompts::PromptBuilder::new(Some(PromptLoadingParams {
fs: fs.clone(),
repo_path: stdout_is_a_pty
.then(|| std::env::current_dir().log_err())
.flatten(),
cx,
}))
.log_err()
.map(Arc::new)
.unwrap_or_else(|| Arc::new(prompts::PromptBuilder::new(None).unwrap()));
register_slash_commands(Some(prompt_builder.clone()), cx);
inline_assistant::init(
fs.clone(),
@@ -237,9 +264,69 @@ pub fn init(fs: Arc<dyn Fs>, client: Arc<Client>, cx: &mut AppContext) -> Arc<Pr
})
.detach();
register_context_server_handlers(cx);
prompt_builder
}
fn register_context_server_handlers(cx: &mut AppContext) {
cx.subscribe(
&context_servers::manager::ContextServerManager::global(cx),
|manager, event, cx| match event {
context_servers::manager::Event::ServerStarted { server_id } => {
cx.update_model(
&manager,
|manager: &mut context_servers::manager::ContextServerManager, cx| {
let slash_command_registry = SlashCommandRegistry::global(cx);
let context_server_registry = ContextServerRegistry::global(cx);
if let Some(server) = manager.get_server(server_id) {
cx.spawn(|_, _| async move {
let Some(protocol) = server.client.read().clone() else {
return;
};
if let Some(prompts) = protocol.list_prompts().await.log_err() {
for prompt in prompts
.into_iter()
.filter(context_server_command::acceptable_prompt)
{
log::info!(
"registering context server command: {:?}",
prompt.name
);
context_server_registry.register_command(
server.id.clone(),
prompt.name.as_str(),
);
slash_command_registry.register_command(
context_server_command::ContextServerSlashCommand::new(
&server, prompt,
),
true,
);
}
}
})
.detach();
}
},
);
}
context_servers::manager::Event::ServerStopped { server_id } => {
let slash_command_registry = SlashCommandRegistry::global(cx);
let context_server_registry = ContextServerRegistry::global(cx);
if let Some(commands) = context_server_registry.get_commands(server_id) {
for command_name in commands {
slash_command_registry.unregister_command_by_name(&command_name);
context_server_registry.unregister_command(&server_id, &command_name);
}
}
}
},
)
.detach();
}
fn init_language_model_settings(cx: &mut AppContext) {
update_active_language_model_from_settings(cx);
@@ -271,15 +358,15 @@ fn update_active_language_model_from_settings(cx: &mut AppContext) {
fn register_slash_commands(prompt_builder: Option<Arc<PromptBuilder>>, cx: &mut AppContext) {
let slash_command_registry = SlashCommandRegistry::global(cx);
slash_command_registry.register_command(file_command::FileSlashCommand, true);
slash_command_registry.register_command(active_command::ActiveSlashCommand, true);
slash_command_registry.register_command(symbols_command::OutlineSlashCommand, true);
slash_command_registry.register_command(tabs_command::TabsSlashCommand, true);
slash_command_registry.register_command(tab_command::TabSlashCommand, true);
slash_command_registry.register_command(project_command::ProjectSlashCommand, true);
slash_command_registry.register_command(prompt_command::PromptSlashCommand, true);
slash_command_registry.register_command(default_command::DefaultSlashCommand, true);
slash_command_registry.register_command(term_command::TermSlashCommand, true);
slash_command_registry.register_command(now_command::NowSlashCommand, true);
slash_command_registry.register_command(default_command::DefaultSlashCommand, false);
slash_command_registry.register_command(terminal_command::TerminalSlashCommand, true);
slash_command_registry.register_command(now_command::NowSlashCommand, false);
slash_command_registry.register_command(diagnostics_command::DiagnosticsSlashCommand, true);
if let Some(prompt_builder) = prompt_builder {
slash_command_registry.register_command(
workflow_command::WorkflowSlashCommand::new(prompt_builder),
@@ -288,15 +375,10 @@ fn register_slash_commands(prompt_builder: Option<Arc<PromptBuilder>>, cx: &mut
}
slash_command_registry.register_command(fetch_command::FetchSlashCommand, false);
cx.observe_flag::<docs_command::DocsSlashCommandFeatureFlag, _>({
let slash_command_registry = slash_command_registry.clone();
move |is_enabled, _cx| {
if is_enabled {
slash_command_registry.register_command(docs_command::DocsSlashCommand, true);
}
}
})
.detach();
update_slash_commands_from_settings(cx);
cx.observe_global::<SettingsStore>(update_slash_commands_from_settings)
.detach();
cx.observe_flag::<search_command::SearchSlashCommandFeatureFlag, _>({
let slash_command_registry = slash_command_registry.clone();
move |is_enabled, _cx| {
@@ -308,6 +390,23 @@ fn register_slash_commands(prompt_builder: Option<Arc<PromptBuilder>>, cx: &mut
.detach();
}
fn update_slash_commands_from_settings(cx: &mut AppContext) {
let slash_command_registry = SlashCommandRegistry::global(cx);
let settings = SlashCommandSettings::get_global(cx);
if settings.docs.enabled {
slash_command_registry.register_command(docs_command::DocsSlashCommand, true);
} else {
slash_command_registry.unregister_command(docs_command::DocsSlashCommand);
}
if settings.project.enabled {
slash_command_registry.register_command(project_command::ProjectSlashCommand, true);
} else {
slash_command_registry.unregister_command(project_command::ProjectSlashCommand);
}
}
pub fn humanize_token_count(count: usize) -> String {
match count {
0..=999 => count.to_string(),

File diff suppressed because it is too large Load Diff

View File

@@ -153,8 +153,8 @@ impl AssistantSettingsContent {
models
.into_iter()
.filter_map(|model| match model {
open_ai::Model::Custom { name, max_tokens } => {
Some(language_model::provider::open_ai::AvailableModel { name, max_tokens })
open_ai::Model::Custom { name, max_tokens,max_output_tokens } => {
Some(language_model::provider::open_ai::AvailableModel { name, max_tokens,max_output_tokens })
}
_ => None,
})
@@ -543,8 +543,8 @@ mod tests {
assert_eq!(
AssistantSettings::get_global(cx).default_model,
LanguageModelSelection {
provider: "openai".into(),
model: "gpt-4o".into(),
provider: "zed.dev".into(),
model: "claude-3-5-sonnet".into(),
}
);
});

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -19,6 +19,7 @@ use fs::Fs;
use futures::{
channel::mpsc,
future::{BoxFuture, LocalBoxFuture},
join,
stream::{self, BoxStream},
SinkExt, Stream, StreamExt,
};
@@ -58,6 +59,13 @@ pub fn init(
cx: &mut AppContext,
) {
cx.set_global(InlineAssistant::new(fs, prompt_builder, telemetry));
cx.observe_new_views(|_, cx| {
let workspace = cx.view().clone();
InlineAssistant::update_global(cx, |inline_assistant, cx| {
inline_assistant.register_workspace(&workspace, cx)
})
})
.detach();
}
const PROMPT_HISTORY_MAX_LEN: usize = 20;
@@ -68,8 +76,13 @@ pub struct InlineAssistant {
assists: HashMap<InlineAssistId, InlineAssist>,
assists_by_editor: HashMap<WeakView<Editor>, EditorInlineAssists>,
assist_groups: HashMap<InlineAssistGroupId, InlineAssistGroup>,
assist_observations:
HashMap<InlineAssistId, (async_watch::Sender<()>, async_watch::Receiver<()>)>,
assist_observations: HashMap<
InlineAssistId,
(
async_watch::Sender<AssistStatus>,
async_watch::Receiver<AssistStatus>,
),
>,
confirmed_assists: HashMap<InlineAssistId, Model<Codegen>>,
prompt_history: VecDeque<String>,
prompt_builder: Arc<PromptBuilder>,
@@ -77,6 +90,19 @@ pub struct InlineAssistant {
fs: Arc<dyn Fs>,
}
pub enum AssistStatus {
Idle,
Started,
Stopped,
Finished,
}
impl AssistStatus {
pub fn is_done(&self) -> bool {
matches!(self, Self::Stopped | Self::Finished)
}
}
impl Global for InlineAssistant {}
impl InlineAssistant {
@@ -100,6 +126,29 @@ impl InlineAssistant {
}
}
pub fn register_workspace(&mut self, workspace: &View<Workspace>, cx: &mut WindowContext) {
cx.subscribe(workspace, |_, event, cx| {
Self::update_global(cx, |this, cx| this.handle_workspace_event(event, cx));
})
.detach();
}
fn handle_workspace_event(&mut self, event: &workspace::Event, cx: &mut WindowContext) {
// When the user manually saves an editor, automatically accepts all finished transformations.
if let workspace::Event::UserSavedItem { item, .. } = event {
if let Some(editor) = item.upgrade().and_then(|item| item.act_as::<Editor>(cx)) {
if let Some(editor_assists) = self.assists_by_editor.get(&editor.downgrade()) {
for assist_id in editor_assists.assist_ids.clone() {
let assist = &self.assists[&assist_id];
if let CodegenStatus::Done = &assist.codegen.read(cx).status {
self.finish_assist(assist_id, false, cx)
}
}
}
}
}
}
pub fn assist(
&mut self,
editor: &View<Editor>,
@@ -498,17 +547,43 @@ impl InlineAssistant {
if editor.selections.count() == 1 {
let selection = editor.selections.newest::<usize>(cx);
let buffer = editor.buffer().read(cx).snapshot(cx);
let mut closest_assist_fallback = None;
for assist_id in &editor_assists.assist_ids {
let assist = &self.assists[assist_id];
let assist_range = assist.range.to_offset(&buffer);
if assist.decorations.is_some()
&& assist_range.contains(&selection.start)
&& assist_range.contains(&selection.end)
{
self.focus_assist(*assist_id, cx);
return;
if assist.decorations.is_some() {
if assist_range.contains(&selection.start)
&& assist_range.contains(&selection.end)
{
self.focus_assist(*assist_id, cx);
return;
} else {
let distance_from_selection = assist_range
.start
.abs_diff(selection.start)
.min(assist_range.start.abs_diff(selection.end))
+ assist_range
.end
.abs_diff(selection.start)
.min(assist_range.end.abs_diff(selection.end));
match closest_assist_fallback {
Some((_, old_distance)) => {
if distance_from_selection < old_distance {
closest_assist_fallback =
Some((assist_id, distance_from_selection));
}
}
None => {
closest_assist_fallback = Some((assist_id, distance_from_selection))
}
}
}
}
}
if let Some((&assist_id, _)) = closest_assist_fallback {
self.focus_assist(assist_id, cx);
}
}
cx.propagate();
@@ -558,14 +633,6 @@ impl InlineAssistant {
};
match event {
EditorEvent::Saved => {
for assist_id in editor_assists.assist_ids.clone() {
let assist = &self.assists[&assist_id];
if let CodegenStatus::Done = &assist.codegen.read(cx).status {
self.finish_assist(assist_id, false, cx)
}
}
}
EditorEvent::Edited { transaction_id } => {
let buffer = editor.read(cx).buffer().read(cx);
let edited_ranges =
@@ -876,7 +943,7 @@ impl InlineAssistant {
.log_err();
if let Some((tx, _)) = self.assist_observations.get(&assist_id) {
tx.send(()).ok();
tx.send(AssistStatus::Started).ok();
}
}
@@ -890,7 +957,7 @@ impl InlineAssistant {
assist.codegen.update(cx, |codegen, cx| codegen.stop(cx));
if let Some((tx, _)) = self.assist_observations.get(&assist_id) {
tx.send(()).ok();
tx.send(AssistStatus::Stopped).ok();
}
}
@@ -1055,6 +1122,7 @@ impl InlineAssistant {
editor.set_show_gutter(false, cx);
editor.scroll_manager.set_forbid_vertical_scroll(true);
editor.set_read_only(true);
editor.set_show_inline_completions(false);
editor.highlight_rows::<DeletedLines>(
Anchor::min()..=Anchor::max(),
Some(cx.theme().status().deleted_background),
@@ -1091,11 +1159,14 @@ impl InlineAssistant {
})
}
pub fn observe_assist(&mut self, assist_id: InlineAssistId) -> async_watch::Receiver<()> {
pub fn observe_assist(
&mut self,
assist_id: InlineAssistId,
) -> async_watch::Receiver<AssistStatus> {
if let Some((_, rx)) = self.assist_observations.get(&assist_id) {
rx.clone()
} else {
let (tx, rx) = async_watch::channel(());
let (tx, rx) = async_watch::channel(AssistStatus::Idle);
self.assist_observations.insert(assist_id, (tx, rx.clone()));
rx
}
@@ -1228,12 +1299,6 @@ fn build_assist_editor_renderer(editor: &View<PromptEditor>) -> RenderBlock {
})
}
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub enum InitialInsertion {
NewlineBefore,
NewlineAfter,
}
#[derive(Copy, Clone, Default, Debug, PartialEq, Eq, Hash)]
pub struct InlineAssistId(usize);
@@ -1277,12 +1342,18 @@ struct PromptEditor {
_codegen_subscription: Subscription,
editor_subscriptions: Vec<Subscription>,
pending_token_count: Task<Result<()>>,
token_count: Option<usize>,
token_counts: Option<TokenCounts>,
_token_count_subscriptions: Vec<Subscription>,
workspace: Option<WeakView<Workspace>>,
show_rate_limit_notice: bool,
}
#[derive(Copy, Clone)]
pub struct TokenCounts {
total: usize,
assistant_panel: usize,
}
impl EventEmitter<PromptEditorEvent> for PromptEditor {}
impl Render for PromptEditor {
@@ -1528,7 +1599,7 @@ impl PromptEditor {
codegen,
fs,
pending_token_count: Task::ready(Ok(())),
token_count: None,
token_counts: None,
_token_count_subscriptions: token_count_subscriptions,
workspace,
show_rate_limit_notice: false,
@@ -1618,7 +1689,7 @@ impl PromptEditor {
.await?;
this.update(&mut cx, |this, cx| {
this.token_count = Some(token_count);
this.token_counts = Some(token_count);
cx.notify();
})
})
@@ -1759,13 +1830,13 @@ impl PromptEditor {
fn render_token_count(&self, cx: &mut ViewContext<Self>) -> Option<impl IntoElement> {
let model = LanguageModelRegistry::read_global(cx).active_model()?;
let token_count = self.token_count?;
let token_counts = self.token_counts?;
let max_token_count = model.max_token_count();
let remaining_tokens = max_token_count as isize - token_count as isize;
let remaining_tokens = max_token_count as isize - token_counts.total as isize;
let token_count_color = if remaining_tokens <= 0 {
Color::Error
} else if token_count as f32 / max_token_count as f32 >= 0.8 {
} else if token_counts.total as f32 / max_token_count as f32 >= 0.8 {
Color::Warning
} else {
Color::Muted
@@ -1775,7 +1846,7 @@ impl PromptEditor {
.id("token_count")
.gap_0p5()
.child(
Label::new(humanize_token_count(token_count))
Label::new(humanize_token_count(token_counts.total))
.size(LabelSize::Small)
.color(token_count_color),
)
@@ -1787,11 +1858,14 @@ impl PromptEditor {
);
if let Some(workspace) = self.workspace.clone() {
token_count = token_count
.tooltip(|cx| {
.tooltip(move |cx| {
Tooltip::with_meta(
"Tokens Used by Inline Assistant",
format!(
"Tokens Used ({} from the Assistant Panel)",
humanize_token_count(token_counts.assistant_panel)
),
None,
"Click to Open Assistant Panel",
"Click to open the Assistant Panel",
cx,
)
})
@@ -1808,7 +1882,7 @@ impl PromptEditor {
} else {
token_count = token_count
.cursor_default()
.tooltip(|cx| Tooltip::text("Tokens Used by Inline Assistant", cx));
.tooltip(|cx| Tooltip::text("Tokens used", cx));
}
Some(token_count)
@@ -2026,7 +2100,7 @@ impl InlineAssist {
if assist.decorations.is_none() {
this.finish_assist(assist_id, false, cx);
} else if let Some(tx) = this.assist_observations.get(&assist_id) {
tx.0.send(()).ok();
tx.0.send(AssistStatus::Finished).ok();
}
}
})
@@ -2057,7 +2131,7 @@ impl InlineAssist {
}
}
pub fn count_tokens(&self, cx: &WindowContext) -> BoxFuture<'static, Result<usize>> {
pub fn count_tokens(&self, cx: &WindowContext) -> BoxFuture<'static, Result<TokenCounts>> {
let Some(user_prompt) = self.user_prompt(cx) else {
return future::ready(Err(anyhow!("no user prompt"))).boxed();
};
@@ -2194,11 +2268,25 @@ impl Codegen {
user_prompt: String,
assistant_panel_context: Option<LanguageModelRequest>,
cx: &AppContext,
) -> BoxFuture<'static, Result<usize>> {
) -> BoxFuture<'static, Result<TokenCounts>> {
if let Some(model) = LanguageModelRegistry::read_global(cx).active_model() {
let request = self.build_request(user_prompt, assistant_panel_context, edit_range, cx);
let request =
self.build_request(user_prompt, assistant_panel_context.clone(), edit_range, cx);
match request {
Ok(request) => model.count_tokens(request, cx),
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?,
})
}
.boxed()
}
Err(error) => futures::future::ready(Err(error)).boxed(),
}
} else {
@@ -2290,6 +2378,7 @@ impl Codegen {
} else {
return Err(anyhow::anyhow!("invalid transformation range"));
};
let prompt = self
.builder
.generate_content_prompt(user_prompt, language_name, buffer, range)
@@ -2303,6 +2392,7 @@ impl Codegen {
messages.push(LanguageModelRequestMessage {
role: Role::User,
content: vec![prompt.into()],
cache: false,
});
Ok(LanguageModelRequest {
@@ -2349,12 +2439,12 @@ impl Codegen {
self.diff = Diff::default();
self.status = CodegenStatus::Pending;
let mut edit_start = edit_range.start.to_offset(&snapshot);
self.generation = cx.spawn(|this, mut cx| {
self.generation = cx.spawn(|codegen, mut cx| {
async move {
let chunks = stream.await;
let generate = async {
let (mut diff_tx, mut diff_rx) = mpsc::channel(1);
let diff: Task<anyhow::Result<()>> =
let line_based_stream_diff: Task<anyhow::Result<()>> =
cx.background_executor().spawn(async move {
let mut response_latency = None;
let request_start = Instant::now();
@@ -2470,10 +2560,10 @@ impl Codegen {
});
while let Some((char_ops, line_diff)) = diff_rx.next().await {
this.update(&mut cx, |this, cx| {
this.last_equal_ranges.clear();
codegen.update(&mut cx, |codegen, cx| {
codegen.last_equal_ranges.clear();
let transaction = this.buffer.update(cx, |buffer, cx| {
let transaction = codegen.buffer.update(cx, |buffer, cx| {
// Avoid grouping assistant edits with user edits.
buffer.finalize_last_transaction(cx);
@@ -2498,23 +2588,24 @@ impl Codegen {
let edit_range = snapshot.anchor_after(edit_start)
..snapshot.anchor_before(edit_end);
edit_start = edit_end;
this.last_equal_ranges.push(edit_range);
codegen.last_equal_ranges.push(edit_range);
None
}
}),
None,
cx,
);
this.edit_position = Some(snapshot.anchor_after(edit_start));
codegen.edit_position = Some(snapshot.anchor_after(edit_start));
buffer.end_transaction(cx)
});
if let Some(transaction) = transaction {
if let Some(first_transaction) = this.transformation_transaction_id
if let Some(first_transaction) =
codegen.transformation_transaction_id
{
// Group all assistant edits into the first transaction.
this.buffer.update(cx, |buffer, cx| {
codegen.buffer.update(cx, |buffer, cx| {
buffer.merge_transactions(
transaction,
first_transaction,
@@ -2522,36 +2613,45 @@ impl Codegen {
)
});
} else {
this.transformation_transaction_id = Some(transaction);
this.buffer.update(cx, |buffer, cx| {
codegen.transformation_transaction_id = Some(transaction);
codegen.buffer.update(cx, |buffer, cx| {
buffer.finalize_last_transaction(cx)
});
}
}
this.update_diff(edit_range.clone(), line_diff, cx);
codegen.reapply_line_based_diff(edit_range.clone(), line_diff, cx);
cx.notify();
})?;
}
diff.await?;
// Streaming stopped and we have the new text in the buffer, and a line-based diff applied for the whole new buffer.
// That diff is not what a regular diff is and might look unexpected, ergo apply a regular diff.
// It's fine to apply even if the rest of the line diffing fails, as no more hunks are coming through `diff_rx`.
let batch_diff_task = codegen.update(&mut cx, |codegen, cx| {
codegen.reapply_batch_diff(edit_range.clone(), cx)
})?;
let (line_based_stream_diff, ()) =
join!(line_based_stream_diff, batch_diff_task);
line_based_stream_diff?;
anyhow::Ok(())
};
let result = generate.await;
this.update(&mut cx, |this, cx| {
this.last_equal_ranges.clear();
if let Err(error) = result {
this.status = CodegenStatus::Error(error);
} else {
this.status = CodegenStatus::Done;
}
cx.emit(CodegenEvent::Finished);
cx.notify();
})
.ok();
codegen
.update(&mut cx, |this, cx| {
this.last_equal_ranges.clear();
if let Err(error) = result {
this.status = CodegenStatus::Error(error);
} else {
this.status = CodegenStatus::Done;
}
cx.emit(CodegenEvent::Finished);
cx.notify();
})
.ok();
}
});
cx.notify();
@@ -2583,7 +2683,7 @@ impl Codegen {
});
}
fn update_diff(
fn reapply_line_based_diff(
&mut self,
edit_range: Range<Anchor>,
line_operations: Vec<LineOperation>,
@@ -2642,6 +2742,99 @@ impl Codegen {
cx.notify();
}
}
fn reapply_batch_diff(
&mut self,
edit_range: Range<Anchor>,
cx: &mut ModelContext<Self>,
) -> Task<()> {
let old_snapshot = self.snapshot.clone();
let old_range = edit_range.to_point(&old_snapshot);
let new_snapshot = self.buffer.read(cx).snapshot(cx);
let new_range = edit_range.to_point(&new_snapshot);
cx.spawn(|codegen, mut cx| async move {
let (deleted_row_ranges, inserted_row_ranges) = cx
.background_executor()
.spawn(async move {
let old_text = old_snapshot
.text_for_range(
Point::new(old_range.start.row, 0)
..Point::new(
old_range.end.row,
old_snapshot.line_len(MultiBufferRow(old_range.end.row)),
),
)
.collect::<String>();
let new_text = new_snapshot
.text_for_range(
Point::new(new_range.start.row, 0)
..Point::new(
new_range.end.row,
new_snapshot.line_len(MultiBufferRow(new_range.end.row)),
),
)
.collect::<String>();
let mut old_row = old_range.start.row;
let mut new_row = new_range.start.row;
let batch_diff =
similar::TextDiff::from_lines(old_text.as_str(), new_text.as_str());
let mut deleted_row_ranges: Vec<(Anchor, RangeInclusive<u32>)> = Vec::new();
let mut inserted_row_ranges = Vec::new();
for change in batch_diff.iter_all_changes() {
let line_count = change.value().lines().count() as u32;
match change.tag() {
similar::ChangeTag::Equal => {
old_row += line_count;
new_row += line_count;
}
similar::ChangeTag::Delete => {
let old_end_row = old_row + line_count - 1;
let new_row = new_snapshot.anchor_before(Point::new(new_row, 0));
if let Some((_, last_deleted_row_range)) =
deleted_row_ranges.last_mut()
{
if *last_deleted_row_range.end() + 1 == old_row {
*last_deleted_row_range =
*last_deleted_row_range.start()..=old_end_row;
} else {
deleted_row_ranges.push((new_row, old_row..=old_end_row));
}
} else {
deleted_row_ranges.push((new_row, old_row..=old_end_row));
}
old_row += line_count;
}
similar::ChangeTag::Insert => {
let new_end_row = new_row + line_count - 1;
let start = new_snapshot.anchor_before(Point::new(new_row, 0));
let end = new_snapshot.anchor_before(Point::new(
new_end_row,
new_snapshot.line_len(MultiBufferRow(new_end_row)),
));
inserted_row_ranges.push(start..=end);
new_row += line_count;
}
}
}
(deleted_row_ranges, inserted_row_ranges)
})
.await;
codegen
.update(&mut cx, |codegen, cx| {
codegen.diff.deleted_row_ranges = deleted_row_ranges;
codegen.diff.inserted_row_ranges = inserted_row_ranges;
cx.notify();
})
.ok();
})
}
}
struct StripInvalidSpans<T> {

View File

@@ -1,15 +1,16 @@
use feature_flags::ZedPro;
use gpui::Action;
use gpui::DismissEvent;
use language_model::{LanguageModel, LanguageModelAvailability, LanguageModelRegistry};
use proto::Plan;
use workspace::ShowConfiguration;
use std::sync::Arc;
use ui::ListItemSpacing;
use crate::assistant_settings::AssistantSettings;
use crate::ShowConfiguration;
use fs::Fs;
use gpui::Action;
use gpui::SharedString;
use gpui::Task;
use picker::{Picker, PickerDelegate};
@@ -36,7 +37,7 @@ pub struct ModelPickerDelegate {
#[derive(Clone)]
struct ModelInfo {
model: Arc<dyn LanguageModel>,
provider_icon: IconName,
icon: IconName,
availability: LanguageModelAvailability,
is_selected: bool,
}
@@ -149,6 +150,8 @@ impl PickerDelegate for ModelPickerDelegate {
use feature_flags::FeatureFlagAppExt;
let model_info = self.filtered_models.get(ix)?;
let show_badges = cx.has_flag::<ZedPro>();
let provider_name: String = model_info.model.provider_name().0.into();
Some(
ListItem::new(ix)
.inset(true)
@@ -156,9 +159,9 @@ impl PickerDelegate for ModelPickerDelegate {
.selected(selected)
.start_slot(
div().pr_1().child(
Icon::new(model_info.provider_icon)
Icon::new(model_info.icon)
.color(Color::Muted)
.size(IconSize::XSmall),
.size(IconSize::Medium),
),
)
.child(
@@ -166,11 +169,16 @@ impl PickerDelegate for ModelPickerDelegate {
.w_full()
.justify_between()
.font_buffer(cx)
.min_w(px(200.))
.min_w(px(240.))
.child(
h_flex()
.gap_2()
.child(Label::new(model_info.model.name().0.clone()))
.child(
Label::new(provider_name)
.size(LabelSize::XSmall)
.color(Color::Muted),
)
.children(match model_info.availability {
LanguageModelAvailability::Public => None,
LanguageModelAvailability::RequiresPlan(Plan::Free) => None,
@@ -261,16 +269,17 @@ impl<T: PopoverTrigger> RenderOnce for ModelSelector<T> {
.iter()
.flat_map(|provider| {
let provider_id = provider.id();
let provider_icon = provider.icon();
let icon = provider.icon();
let selected_model = selected_model.clone();
let selected_provider = selected_provider.clone();
provider.provided_models(cx).into_iter().map(move |model| {
let model = model.clone();
let icon = model.icon().unwrap_or(icon);
ModelInfo {
model: model.clone(),
provider_icon,
icon,
availability: model.availability(),
is_selected: selected_model.as_ref() == Some(&model.id())
&& selected_provider.as_ref() == Some(&provider_id),
@@ -295,5 +304,6 @@ impl<T: PopoverTrigger> RenderOnce for ModelSelector<T> {
.menu(move |_cx| Some(picker_view.clone()))
.trigger(self.trigger)
.attach(gpui::AnchorCorner::BottomLeft)
.when_some(self.handle, |menu, handle| menu.with_handle(handle))
}
}

View File

@@ -11,8 +11,8 @@ use futures::{
};
use fuzzy::StringMatchCandidate;
use gpui::{
actions, point, size, transparent_black, AppContext, BackgroundExecutor, Bounds, EventEmitter,
Global, HighlightStyle, PromptLevel, ReadGlobal, Subscription, Task, TextStyle,
actions, point, size, transparent_black, Action, AppContext, BackgroundExecutor, Bounds,
EventEmitter, Global, HighlightStyle, PromptLevel, ReadGlobal, Subscription, Task, TextStyle,
TitlebarOptions, UpdateGlobal, View, WindowBounds, WindowHandle, WindowOptions,
};
use heed::{
@@ -38,7 +38,7 @@ use std::{
use text::LineEnding;
use theme::ThemeSettings;
use ui::{
div, prelude::*, IconButtonShape, ListItem, ListItemSpacing, ParentElement, Render,
div, prelude::*, IconButtonShape, KeyBinding, ListItem, ListItemSpacing, ParentElement, Render,
SharedString, Styled, Tooltip, ViewContext, VisualContext,
};
use util::{ResultExt, TryFutureExt};
@@ -100,7 +100,7 @@ pub fn open_prompt_library(
WindowOptions {
titlebar: Some(TitlebarOptions {
title: Some("Prompt Library".into()),
appears_transparent: true,
appears_transparent: !cfg!(windows),
traffic_light_position: Some(point(px(9.0), px(9.0))),
}),
window_bounds: Some(WindowBounds::Windowed(bounds)),
@@ -155,6 +155,14 @@ impl PickerDelegate for PromptPickerDelegate {
self.matches.len()
}
fn no_matches_text(&self, _cx: &mut WindowContext) -> SharedString {
if self.store.prompt_count() == 0 {
"No prompts.".into()
} else {
"No prompts found matching your search.".into()
}
}
fn selected_index(&self) -> usize {
self.selected_index
}
@@ -486,7 +494,10 @@ impl PromptLibrary {
let mut editor = Editor::auto_width(cx);
editor.set_placeholder_text("Untitled", cx);
editor.set_text(prompt_metadata.title.unwrap_or_default(), cx);
editor.set_read_only(prompt_id.is_built_in());
if prompt_id.is_built_in() {
editor.set_read_only(true);
editor.set_show_inline_completions(false);
}
editor
});
let body_editor = cx.new_view(|cx| {
@@ -498,7 +509,10 @@ impl PromptLibrary {
});
let mut editor = Editor::for_buffer(buffer, None, cx);
editor.set_read_only(prompt_id.is_built_in());
if prompt_id.is_built_in() {
editor.set_read_only(true);
editor.set_show_inline_completions(false);
}
editor.set_soft_wrap_mode(SoftWrap::EditorWidth, cx);
editor.set_show_gutter(false, cx);
editor.set_show_wrap_guides(false, cx);
@@ -776,6 +790,7 @@ impl PromptLibrary {
messages: vec![LanguageModelRequestMessage {
role: Role::System,
content: vec![body.to_string().into()],
cache: false,
}],
stop: Vec::new(),
temperature: 1.,
@@ -911,6 +926,7 @@ impl PromptLibrary {
color: Some(cx.theme().status().predictive),
..HighlightStyle::default()
},
..EditorStyle::default()
},
)),
),
@@ -1094,7 +1110,55 @@ impl Render for PromptLibrary {
.font(ui_font)
.text_color(theme.colors().text)
.child(self.render_prompt_list(cx))
.child(self.render_active_prompt(cx))
.map(|el| {
if self.store.prompt_count() == 0 {
el.child(
v_flex()
.w_2_3()
.h_full()
.items_center()
.justify_center()
.gap_4()
.bg(cx.theme().colors().editor_background)
.child(
h_flex()
.gap_2()
.child(
Icon::new(IconName::Book)
.size(IconSize::Medium)
.color(Color::Muted),
)
.child(
Label::new("No prompts yet")
.size(LabelSize::Large)
.color(Color::Muted),
),
)
.child(
h_flex()
.child(h_flex())
.child(
v_flex()
.gap_1()
.child(Label::new("Create your first prompt:"))
.child(
Button::new("create-prompt", "New Prompt")
.full_width()
.key_binding(KeyBinding::for_action(
&NewPrompt, cx,
))
.on_click(|_, cx| {
cx.dispatch_action(NewPrompt.boxed_clone())
}),
),
)
.child(h_flex()),
),
)
} else {
el.child(self.render_active_prompt(cx))
}
})
}
}
@@ -1342,6 +1406,11 @@ impl PromptStore {
})
}
/// Returns the number of prompts in the store.
fn prompt_count(&self) -> usize {
self.metadata_cache.read().metadata.len()
}
fn metadata(&self, id: PromptId) -> Option<PromptMetadata> {
self.metadata_cache.read().metadata_by_id.get(&id).cloned()
}

View File

@@ -1,11 +1,13 @@
use anyhow::Result;
use assets::Assets;
use fs::Fs;
use futures::StreamExt;
use handlebars::{Handlebars, RenderError, TemplateError};
use gpui::AssetSource;
use handlebars::{Handlebars, RenderError};
use language::BufferSnapshot;
use parking_lot::Mutex;
use serde::Serialize;
use std::{ops::Range, sync::Arc, time::Duration};
use std::{ops::Range, path::PathBuf, sync::Arc, time::Duration};
use util::ResultExt;
#[derive(Serialize)]
@@ -29,115 +31,171 @@ pub struct TerminalAssistantPromptContext {
pub user_prompt: String,
}
/// Context required to generate a workflow step resolution prompt.
#[derive(Debug, Serialize)]
pub struct StepResolutionContext {
/// The full context, including <step>...</step> tags
pub workflow_context: String,
/// The text of the specific step from the context to resolve
pub step_to_resolve: String,
}
pub struct PromptLoadingParams<'a> {
pub fs: Arc<dyn Fs>,
pub repo_path: Option<PathBuf>,
pub cx: &'a gpui::AppContext,
}
pub struct PromptBuilder {
handlebars: Arc<Mutex<Handlebars<'static>>>,
}
impl PromptBuilder {
pub fn new(
fs_and_cx: Option<(Arc<dyn Fs>, &gpui::AppContext)>,
) -> Result<Self, Box<TemplateError>> {
pub fn new(loading_params: Option<PromptLoadingParams>) -> Result<Self> {
let mut handlebars = Handlebars::new();
Self::register_templates(&mut handlebars)?;
Self::register_built_in_templates(&mut handlebars)?;
let handlebars = Arc::new(Mutex::new(handlebars));
if let Some((fs, cx)) = fs_and_cx {
Self::watch_fs_for_template_overrides(fs, cx, handlebars.clone());
if let Some(params) = loading_params {
Self::watch_fs_for_template_overrides(params, handlebars.clone());
}
Ok(Self { handlebars })
}
/// Watches the filesystem for changes to prompt template overrides.
///
/// This function sets up a file watcher on the prompt templates directory. It performs
/// an initial scan of the directory and registers any existing template overrides.
/// Then it continuously monitors for changes, reloading templates as they are
/// modified or added.
///
/// If the templates directory doesn't exist initially, it waits for it to be created.
/// If the directory is removed, it restores the built-in templates and waits for the
/// directory to be recreated.
///
/// # Arguments
///
/// * `params` - A `PromptLoadingParams` struct containing the filesystem, repository path,
/// and application context.
/// * `handlebars` - An `Arc<Mutex<Handlebars>>` for registering and updating templates.
fn watch_fs_for_template_overrides(
fs: Arc<dyn Fs>,
cx: &gpui::AppContext,
mut params: PromptLoadingParams,
handlebars: Arc<Mutex<Handlebars<'static>>>,
) {
let templates_dir = paths::prompt_overrides_dir();
cx.background_executor()
params.repo_path = None;
let templates_dir = paths::prompt_overrides_dir(params.repo_path.as_deref());
params.cx.background_executor()
.spawn(async move {
// Create the prompt templates directory if it doesn't exist
if !fs.is_dir(templates_dir).await {
if let Err(e) = fs.create_dir(templates_dir).await {
log::error!("Failed to create prompt templates directory: {}", e);
return;
let Some(parent_dir) = templates_dir.parent() else {
return;
};
let mut found_dir_once = false;
loop {
// Check if the templates directory exists and handle its status
// If it exists, log its presence and check if it's a symlink
// If it doesn't exist:
// - Log that we're using built-in prompts
// - Check if it's a broken symlink and log if so
// - Set up a watcher to detect when it's created
// After the first check, set the `found_dir_once` flag
// This allows us to avoid logging when looping back around after deleting the prompt overrides directory.
let dir_status = params.fs.is_dir(&templates_dir).await;
let symlink_status = params.fs.read_link(&templates_dir).await.ok();
if dir_status {
let mut log_message = format!("Prompt template overrides directory found at {}", templates_dir.display());
if let Some(target) = symlink_status {
log_message.push_str(" -> ");
log_message.push_str(&target.display().to_string());
}
log::info!("{}.", log_message);
} else {
if !found_dir_once {
log::info!("No prompt template overrides directory found at {}. Using built-in prompts.", templates_dir.display());
if let Some(target) = symlink_status {
log::info!("Symlink found pointing to {}, but target is invalid.", target.display());
}
}
if params.fs.is_dir(parent_dir).await {
let (mut changes, _watcher) = params.fs.watch(parent_dir, Duration::from_secs(1)).await;
while let Some(changed_paths) = changes.next().await {
if changed_paths.iter().any(|p| p == &templates_dir) {
let mut log_message = format!("Prompt template overrides directory detected at {}", templates_dir.display());
if let Ok(target) = params.fs.read_link(&templates_dir).await {
log_message.push_str(" -> ");
log_message.push_str(&target.display().to_string());
}
log::info!("{}.", log_message);
break;
}
}
} else {
return;
}
}
}
// Initial scan of the prompts directory
if let Ok(mut entries) = fs.read_dir(templates_dir).await {
while let Some(Ok(file_path)) = entries.next().await {
if file_path.to_string_lossy().ends_with(".hbs") {
if let Ok(content) = fs.load(&file_path).await {
let file_name = file_path.file_stem().unwrap().to_string_lossy();
found_dir_once = true;
match handlebars.lock().register_template_string(&file_name, content) {
Ok(_) => {
log::info!(
"Successfully registered template override: {} ({})",
file_name,
file_path.display()
);
},
Err(e) => {
log::error!(
"Failed to register template during initial scan: {} ({})",
e,
file_path.display()
);
},
// Initial scan of the prompt overrides directory
if let Ok(mut entries) = params.fs.read_dir(&templates_dir).await {
while let Some(Ok(file_path)) = entries.next().await {
if file_path.to_string_lossy().ends_with(".hbs") {
if let Ok(content) = params.fs.load(&file_path).await {
let file_name = file_path.file_stem().unwrap().to_string_lossy();
log::info!("Registering prompt template override: {}", file_name);
handlebars.lock().register_template_string(&file_name, content).log_err();
}
}
}
}
}
// Watch for changes
let (mut changes, watcher) = fs.watch(templates_dir, Duration::from_secs(1)).await;
while let Some(changed_paths) = changes.next().await {
for changed_path in changed_paths {
if changed_path.extension().map_or(false, |ext| ext == "hbs") {
log::info!("Reloading template: {}", changed_path.display());
if let Some(content) = fs.load(&changed_path).await.log_err() {
let file_name = changed_path.file_stem().unwrap().to_string_lossy();
let file_path = changed_path.to_string_lossy();
match handlebars.lock().register_template_string(&file_name, content) {
Ok(_) => log::info!(
"Successfully reloaded template: {} ({})",
file_name,
file_path
),
Err(e) => log::error!(
"Failed to register template: {} ({})",
e,
file_path
),
// Watch both the parent directory and the template overrides directory:
// - Monitor the parent directory to detect if the template overrides directory is deleted.
// - Monitor the template overrides directory to re-register templates when they change.
// Combine both watch streams into a single stream.
let (parent_changes, parent_watcher) = params.fs.watch(parent_dir, Duration::from_secs(1)).await;
let (changes, watcher) = params.fs.watch(&templates_dir, Duration::from_secs(1)).await;
let mut combined_changes = futures::stream::select(changes, parent_changes);
while let Some(changed_paths) = combined_changes.next().await {
if changed_paths.iter().any(|p| p == &templates_dir) {
if !params.fs.is_dir(&templates_dir).await {
log::info!("Prompt template overrides directory removed. Restoring built-in prompt templates.");
Self::register_built_in_templates(&mut handlebars.lock()).log_err();
break;
}
}
for changed_path in changed_paths {
if changed_path.starts_with(&templates_dir) && changed_path.extension().map_or(false, |ext| ext == "hbs") {
log::info!("Reloading prompt template override: {}", changed_path.display());
if let Some(content) = params.fs.load(&changed_path).await.log_err() {
let file_name = changed_path.file_stem().unwrap().to_string_lossy();
handlebars.lock().register_template_string(&file_name, content).log_err();
}
}
}
}
drop(watcher);
drop(parent_watcher);
}
drop(watcher);
})
.detach();
}
fn register_templates(handlebars: &mut Handlebars) -> Result<(), Box<TemplateError>> {
let mut register_template = |id: &str| {
let prompt = Assets::get(&format!("prompts/{}.hbs", id))
.unwrap_or_else(|| panic!("{} prompt template not found", id))
.data;
handlebars
.register_template_string(id, String::from_utf8_lossy(&prompt))
.map_err(Box::new)
};
register_template("content_prompt")?;
register_template("terminal_assistant_prompt")?;
register_template("edit_workflow")?;
register_template("step_resolution")?;
fn register_built_in_templates(handlebars: &mut Handlebars) -> Result<()> {
for path in Assets.list("prompts")? {
if let Some(id) = path.split('/').last().and_then(|s| s.strip_suffix(".hbs")) {
if let Some(prompt) = Assets.load(path.as_ref()).log_err().flatten() {
log::info!("Registering built-in prompt template: {}", id);
handlebars
.register_template_string(id, String::from_utf8_lossy(prompt.as_ref()))?
}
}
}
Ok(())
}
@@ -239,7 +297,10 @@ impl PromptBuilder {
self.handlebars.lock().render("edit_workflow", &())
}
pub fn generate_step_resolution_prompt(&self) -> Result<String, RenderError> {
self.handlebars.lock().render("step_resolution", &())
pub fn generate_step_resolution_prompt(
&self,
context: &StepResolutionContext,
) -> Result<String, RenderError> {
self.handlebars.lock().render("step_resolution", context)
}
}

View File

@@ -1,11 +1,13 @@
use crate::assistant_panel::ContextEditor;
use anyhow::Result;
use assistant_slash_command::AfterCompletion;
pub use assistant_slash_command::{SlashCommand, SlashCommandOutput, SlashCommandRegistry};
use editor::{CompletionProvider, Editor};
use fuzzy::{match_strings, StringMatchCandidate};
use gpui::{AppContext, Model, Task, ViewContext, WeakView, WindowContext};
use language::{Anchor, Buffer, CodeLabel, Documentation, HighlightId, LanguageServerId, ToPoint};
use parking_lot::{Mutex, RwLock};
use project::CompletionIntent;
use rope::Point;
use std::{
ops::Range,
@@ -17,7 +19,7 @@ use std::{
use ui::ActiveTheme;
use workspace::Workspace;
pub mod active_command;
pub mod context_server_command;
pub mod default_command;
pub mod diagnostics_command;
pub mod docs_command;
@@ -28,8 +30,8 @@ pub mod project_command;
pub mod prompt_command;
pub mod search_command;
pub mod symbols_command;
pub mod tabs_command;
pub mod term_command;
pub mod tab_command;
pub mod terminal_command;
pub mod workflow_command;
pub(crate) struct SlashCommandCompletionProvider {
@@ -41,8 +43,8 @@ pub(crate) struct SlashCommandCompletionProvider {
pub(crate) struct SlashCommandLine {
/// The range within the line containing the command name.
pub name: Range<usize>,
/// The range within the line containing the command argument.
pub argument: Option<Range<usize>>,
/// Ranges within the line containing the command arguments.
pub arguments: Vec<Range<usize>>,
}
impl SlashCommandCompletionProvider {
@@ -96,34 +98,45 @@ impl SlashCommandCompletionProvider {
let command = commands.command(&mat.string)?;
let mut new_text = mat.string.clone();
let requires_argument = command.requires_argument();
if requires_argument {
let accepts_arguments = command.accepts_arguments();
if requires_argument || accepts_arguments {
new_text.push(' ');
}
let confirm = editor.clone().zip(workspace.clone()).and_then(
|(editor, workspace)| {
(!requires_argument).then(|| {
let confirm =
editor
.clone()
.zip(workspace.clone())
.map(|(editor, workspace)| {
let command_name = mat.string.clone();
let command_range = command_range.clone();
let editor = editor.clone();
let workspace = workspace.clone();
Arc::new(move |cx: &mut WindowContext| {
editor
.update(cx, |editor, cx| {
editor.run_command(
command_range.clone(),
&command_name,
None,
true,
workspace.clone(),
cx,
);
})
.ok();
}) as Arc<_>
})
},
);
Arc::new(
move |intent: CompletionIntent, cx: &mut WindowContext| {
if !requires_argument
&& (!accepts_arguments || intent.is_complete())
{
editor
.update(cx, |editor, cx| {
editor.run_command(
command_range.clone(),
&command_name,
&[],
true,
false,
workspace.clone(),
cx,
);
})
.ok();
false
} else {
requires_argument || accepts_arguments
}
},
) as Arc<_>
});
Some(project::Completion {
old_range: name_range.clone(),
documentation: Some(Documentation::SingleLine(command.description())),
@@ -131,7 +144,6 @@ impl SlashCommandCompletionProvider {
label: command.label(cx),
server_id: LanguageServerId(0),
lsp_completion: Default::default(),
show_new_completions_on_confirm: requires_argument,
confirm,
})
})
@@ -143,20 +155,20 @@ impl SlashCommandCompletionProvider {
fn complete_command_argument(
&self,
command_name: &str,
argument: String,
arguments: &[String],
command_range: Range<Anchor>,
argument_range: Range<Anchor>,
last_argument_range: Range<Anchor>,
cx: &mut WindowContext,
) -> Task<Result<Vec<project::Completion>>> {
let new_cancel_flag = Arc::new(AtomicBool::new(false));
let mut flag = self.cancel_flag.lock();
flag.store(true, SeqCst);
*flag = new_cancel_flag.clone();
let commands = SlashCommandRegistry::global(cx);
if let Some(command) = commands.command(command_name) {
let completions = command.complete_argument(
argument,
arguments,
new_cancel_flag.clone(),
self.workspace.clone(),
cx,
@@ -164,61 +176,76 @@ impl SlashCommandCompletionProvider {
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_executor().spawn(async move {
Ok(completions
.await?
.into_iter()
.map(|command_argument| {
let confirm = if command_argument.run_command {
.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 {
completed_arguments.clear();
} else {
completed_arguments.pop();
}
completed_arguments.push(new_argument.new_text.clone());
let command_range = command_range.clone();
let command_name = command_name.clone();
let command_argument = command_argument.new_text.clone();
move |cx: &mut WindowContext| {
editor
.update(cx, |editor, cx| {
editor.run_command(
command_range.clone(),
&command_name,
Some(&command_argument),
true,
workspace.clone(),
cx,
);
})
.ok();
move |intent: CompletionIntent, cx: &mut WindowContext| {
if new_argument.after_completion.run()
|| intent.is_complete()
{
editor
.update(cx, |editor, cx| {
editor.run_command(
command_range.clone(),
&command_name,
&completed_arguments,
true,
false,
workspace.clone(),
cx,
);
})
.ok();
false
} else {
!new_argument.after_completion.run()
}
}
}) as Arc<_>
})
} else {
None
};
});
let mut new_text = command_argument.new_text.clone();
if !command_argument.run_command {
let mut new_text = new_argument.new_text.clone();
if new_argument.after_completion == AfterCompletion::Continue {
new_text.push(' ');
}
project::Completion {
old_range: argument_range.clone(),
label: CodeLabel::plain(command_argument.label, None),
old_range: if new_argument.replace_previous_arguments {
argument_range.clone()
} else {
last_argument_range.clone()
},
label: new_argument.label,
new_text,
documentation: None,
server_id: LanguageServerId(0),
lsp_completion: Default::default(),
show_new_completions_on_confirm: !command_argument.run_command,
confirm,
}
})
.collect())
})
} else {
cx.background_executor()
.spawn(async move { Ok(Vec::new()) })
Task::ready(Ok(Vec::new()))
}
}
}
@@ -231,7 +258,7 @@ impl CompletionProvider for SlashCommandCompletionProvider {
_: editor::CompletionContext,
cx: &mut ViewContext<Editor>,
) -> Task<Result<Vec<project::Completion>>> {
let Some((name, argument, command_range, argument_range)) =
let Some((name, arguments, command_range, last_argument_range)) =
buffer.update(cx, |buffer, _cx| {
let position = buffer_position.to_point(buffer);
let line_start = Point::new(position.row, 0);
@@ -242,32 +269,52 @@ impl CompletionProvider for SlashCommandCompletionProvider {
let command_range_start = Point::new(position.row, call.name.start as u32 - 1);
let command_range_end = Point::new(
position.row,
call.argument.as_ref().map_or(call.name.end, |arg| arg.end) as u32,
call.arguments.last().map_or(call.name.end, |arg| arg.end) as u32,
);
let command_range = buffer.anchor_after(command_range_start)
..buffer.anchor_after(command_range_end);
let name = line[call.name.clone()].to_string();
Some(if let Some(argument) = call.argument {
let start =
let (arguments, last_argument_range) = if let Some(argument) = call.arguments.last()
{
let last_arg_start =
buffer.anchor_after(Point::new(position.row, argument.start as u32));
let argument = line[argument.clone()].to_string();
(name, Some(argument), command_range, start..buffer_position)
let first_arg_start = call.arguments.first().expect("we have the last element");
let first_arg_start =
buffer.anchor_after(Point::new(position.row, first_arg_start.start as u32));
let arguments = call
.arguments
.iter()
.filter_map(|argument| Some(line.get(argument.clone())?.to_string()))
.collect::<Vec<_>>();
let argument_range = first_arg_start..buffer_position;
(
Some((arguments, argument_range)),
last_arg_start..buffer_position,
)
} else {
let start =
buffer.anchor_after(Point::new(position.row, call.name.start as u32));
(name, None, command_range, start..buffer_position)
})
(None, start..buffer_position)
};
Some((name, arguments, command_range, last_argument_range))
})
else {
return Task::ready(Ok(Vec::new()));
};
if let Some(argument) = argument {
self.complete_command_argument(&name, argument, command_range, argument_range, cx)
if let Some((arguments, argument_range)) = arguments {
self.complete_command_argument(
&name,
&arguments,
command_range,
argument_range,
last_argument_range,
cx,
)
} else {
self.complete_command_name(&name, command_range, argument_range, cx)
self.complete_command_name(&name, command_range, last_argument_range, cx)
}
}
@@ -309,6 +356,10 @@ impl CompletionProvider for SlashCommandCompletionProvider {
false
}
}
fn sort_completions(&self) -> bool {
false
}
}
impl SlashCommandLine {
@@ -320,16 +371,23 @@ impl SlashCommandLine {
if let Some(call) = &mut call {
// The command arguments start at the first non-whitespace character
// after the command name, and continue until the end of the line.
if let Some(argument) = &mut call.argument {
if (*argument).is_empty() && c.is_whitespace() {
argument.start = next_ix;
if let Some(argument) = call.arguments.last_mut() {
if c.is_whitespace() {
if (*argument).is_empty() {
argument.start = next_ix;
argument.end = next_ix;
} else {
argument.end = ix;
call.arguments.push(next_ix..next_ix);
}
} else {
argument.end = next_ix;
}
argument.end = next_ix;
}
// The command name ends at the first whitespace character.
else if !call.name.is_empty() {
if c.is_whitespace() {
call.argument = Some(next_ix..next_ix);
call.arguments = vec![next_ix..next_ix];
} else {
call.name.end = next_ix;
}
@@ -345,7 +403,7 @@ impl SlashCommandLine {
else if c == '/' {
call = Some(SlashCommandLine {
name: next_ix..next_ix,
argument: None,
arguments: Vec::new(),
});
}
// The line can't contain anything before the slash except for whitespace.

View File

@@ -1,102 +0,0 @@
use super::{
diagnostics_command::write_single_file_diagnostics,
file_command::{build_entry_output_section, codeblock_fence_for_path},
SlashCommand, SlashCommandOutput,
};
use anyhow::{anyhow, Result};
use assistant_slash_command::ArgumentCompletion;
use editor::Editor;
use gpui::{AppContext, Task, WeakView};
use language::LspAdapterDelegate;
use std::sync::atomic::AtomicBool;
use std::sync::Arc;
use ui::WindowContext;
use workspace::Workspace;
pub(crate) struct ActiveSlashCommand;
impl SlashCommand for ActiveSlashCommand {
fn name(&self) -> String {
"active".into()
}
fn description(&self) -> String {
"insert active tab".into()
}
fn menu_text(&self) -> String {
"Insert Active Tab".into()
}
fn complete_argument(
self: Arc<Self>,
_query: String,
_cancel: Arc<AtomicBool>,
_workspace: Option<WeakView<Workspace>>,
_cx: &mut AppContext,
) -> Task<Result<Vec<ArgumentCompletion>>> {
Task::ready(Err(anyhow!("this command does not require argument")))
}
fn requires_argument(&self) -> bool {
false
}
fn run(
self: Arc<Self>,
_argument: Option<&str>,
workspace: WeakView<Workspace>,
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
cx: &mut WindowContext,
) -> Task<Result<SlashCommandOutput>> {
let output = workspace.update(cx, |workspace, cx| {
let Some(active_item) = workspace.active_item(cx) else {
return Task::ready(Err(anyhow!("no active tab")));
};
let Some(buffer) = active_item
.downcast::<Editor>()
.and_then(|editor| editor.read(cx).buffer().read(cx).as_singleton())
else {
return Task::ready(Err(anyhow!("active tab is not an editor")));
};
let snapshot = buffer.read(cx).snapshot();
let path = snapshot.resolve_file_path(cx, true);
let task = cx.background_executor().spawn({
let path = path.clone();
async move {
let mut output = String::new();
output.push_str(&codeblock_fence_for_path(path.as_deref(), None));
for chunk in snapshot.as_rope().chunks() {
output.push_str(chunk);
}
if !output.ends_with('\n') {
output.push('\n');
}
output.push_str("```\n");
let has_diagnostics =
write_single_file_diagnostics(&mut output, path.as_deref(), &snapshot);
if output.ends_with('\n') {
output.pop();
}
(output, has_diagnostics)
}
});
cx.foreground_executor().spawn(async move {
let (text, has_diagnostics) = task.await;
let range = 0..text.len();
Ok(SlashCommandOutput {
text,
sections: vec![build_entry_output_section(
range,
path.as_deref(),
false,
None,
)],
run_commands_in_text: has_diagnostics,
})
})
});
output.unwrap_or_else(|error| Task::ready(Err(error)))
}
}

View File

@@ -0,0 +1,134 @@
use anyhow::{anyhow, Result};
use assistant_slash_command::{
ArgumentCompletion, SlashCommand, SlashCommandOutput, SlashCommandOutputSection,
};
use collections::HashMap;
use context_servers::{
manager::{ContextServer, ContextServerManager},
protocol::PromptInfo,
};
use gpui::{Task, WeakView, WindowContext};
use language::LspAdapterDelegate;
use std::sync::atomic::AtomicBool;
use std::sync::Arc;
use ui::{IconName, SharedString};
use workspace::Workspace;
pub struct ContextServerSlashCommand {
server_id: String,
prompt: PromptInfo,
}
impl ContextServerSlashCommand {
pub fn new(server: &Arc<ContextServer>, prompt: PromptInfo) -> Self {
Self {
server_id: server.id.clone(),
prompt,
}
}
}
impl SlashCommand for ContextServerSlashCommand {
fn name(&self) -> String {
self.prompt.name.clone()
}
fn description(&self) -> String {
format!("Run context server command: {}", self.prompt.name)
}
fn menu_text(&self) -> String {
format!("Run '{}' from {}", self.prompt.name, self.server_id)
}
fn requires_argument(&self) -> bool {
self.prompt
.arguments
.as_ref()
.map_or(false, |args| !args.is_empty())
}
fn complete_argument(
self: Arc<Self>,
_arguments: &[String],
_cancel: Arc<AtomicBool>,
_workspace: Option<WeakView<Workspace>>,
_cx: &mut WindowContext,
) -> Task<Result<Vec<ArgumentCompletion>>> {
Task::ready(Ok(Vec::new()))
}
fn run(
self: Arc<Self>,
arguments: &[String],
_workspace: WeakView<Workspace>,
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
cx: &mut WindowContext,
) -> Task<Result<SlashCommandOutput>> {
let server_id = self.server_id.clone();
let prompt_name = self.prompt.name.clone();
let prompt_args = match prompt_arguments(&self.prompt, arguments) {
Ok(args) => args,
Err(e) => return Task::ready(Err(e)),
};
let manager = ContextServerManager::global(cx);
let manager = manager.read(cx);
if let Some(server) = manager.get_server(&server_id) {
cx.foreground_executor().spawn(async move {
let Some(protocol) = server.client.read().clone() else {
return Err(anyhow!("Context server not initialized"));
};
let result = protocol.run_prompt(&prompt_name, prompt_args).await?;
Ok(SlashCommandOutput {
sections: vec![SlashCommandOutputSection {
range: 0..result.len(),
icon: IconName::ZedAssistant,
label: SharedString::from(format!("Result from {}", prompt_name)),
}],
text: result,
run_commands_in_text: false,
})
})
} else {
Task::ready(Err(anyhow!("Context server not found")))
}
}
}
fn prompt_arguments(prompt: &PromptInfo, arguments: &[String]) -> Result<HashMap<String, String>> {
match &prompt.arguments {
Some(args) if args.len() > 1 => Err(anyhow!(
"Prompt has more than one argument, which is not supported"
)),
Some(args) if args.len() == 1 => {
if !arguments.is_empty() {
let mut map = HashMap::default();
map.insert(args[0].name.clone(), arguments.join(" "));
Ok(map)
} else {
Err(anyhow!("Prompt expects argument but none given"))
}
}
Some(_) | None => {
if arguments.is_empty() {
Ok(HashMap::default())
} else {
Err(anyhow!("Prompt expects no arguments but some were given"))
}
}
}
}
/// MCP servers can return prompts with multiple arguments. Since we only
/// support one argument, we ignore all others. This is the necessary predicate
/// for this.
pub fn acceptable_prompt(prompt: &PromptInfo) -> bool {
match &prompt.arguments {
None => true,
Some(args) if args.len() == 1 => true,
_ => false,
}
}

View File

@@ -2,7 +2,7 @@ use super::{SlashCommand, SlashCommandOutput};
use crate::prompt_library::PromptStore;
use anyhow::{anyhow, Result};
use assistant_slash_command::{ArgumentCompletion, SlashCommandOutputSection};
use gpui::{AppContext, Task, WeakView};
use gpui::{Task, WeakView};
use language::LspAdapterDelegate;
use std::{
fmt::Write,
@@ -32,17 +32,17 @@ impl SlashCommand for DefaultSlashCommand {
fn complete_argument(
self: Arc<Self>,
_query: String,
_arguments: &[String],
_cancellation_flag: Arc<AtomicBool>,
_workspace: Option<WeakView<Workspace>>,
_cx: &mut AppContext,
_cx: &mut WindowContext,
) -> Task<Result<Vec<ArgumentCompletion>>> {
Task::ready(Err(anyhow!("this command does not require argument")))
}
fn run(
self: Arc<Self>,
_argument: Option<&str>,
_arguments: &[String],
_workspace: WeakView<Workspace>,
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
cx: &mut WindowContext,

View File

@@ -43,6 +43,7 @@ impl DiagnosticsSlashCommand {
worktree_id: entry.worktree_id.to_usize(),
path: entry.path.clone(),
path_prefix: path_prefix.clone(),
is_dir: false, // Diagnostics can't be produced for directories
distance_to_relative_ancestor: 0,
})
.collect(),
@@ -102,17 +103,21 @@ impl SlashCommand for DiagnosticsSlashCommand {
false
}
fn accepts_arguments(&self) -> bool {
true
}
fn complete_argument(
self: Arc<Self>,
query: String,
arguments: &[String],
cancellation_flag: Arc<AtomicBool>,
workspace: Option<WeakView<Workspace>>,
cx: &mut AppContext,
cx: &mut WindowContext,
) -> Task<Result<Vec<ArgumentCompletion>>> {
let Some(workspace) = workspace.and_then(|workspace| workspace.upgrade()) else {
return Task::ready(Err(anyhow!("workspace was dropped")));
};
let query = query.split_whitespace().last().unwrap_or("").to_string();
let query = arguments.last().cloned().unwrap_or_default();
let paths = self.search_paths(query.clone(), cancellation_flag.clone(), &workspace, cx);
let executor = cx.background_executor().clone();
@@ -146,9 +151,10 @@ impl SlashCommand for DiagnosticsSlashCommand {
Ok(matches
.into_iter()
.map(|completion| ArgumentCompletion {
label: completion.clone(),
label: completion.clone().into(),
new_text: completion,
run_command: true,
after_completion: assistant_slash_command::AfterCompletion::Run,
replace_previous_arguments: false,
})
.collect())
})
@@ -156,7 +162,7 @@ impl SlashCommand for DiagnosticsSlashCommand {
fn run(
self: Arc<Self>,
argument: Option<&str>,
arguments: &[String],
workspace: WeakView<Workspace>,
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
cx: &mut WindowContext,
@@ -165,61 +171,69 @@ impl SlashCommand for DiagnosticsSlashCommand {
return Task::ready(Err(anyhow!("workspace was dropped")));
};
let options = Options::parse(argument);
let options = Options::parse(arguments);
let task = collect_diagnostics(workspace.read(cx).project().clone(), options, cx);
cx.spawn(move |_| async move {
let Some((text, sections)) = task.await? else {
return Ok(SlashCommandOutput::default());
return Ok(SlashCommandOutput {
sections: vec![SlashCommandOutputSection {
range: 0..1,
icon: IconName::Library,
label: "No Diagnostics".into(),
}],
text: "\n".to_string(),
run_commands_in_text: true,
});
};
let sections = sections
.into_iter()
.map(|(range, placeholder_type)| SlashCommandOutputSection {
range,
icon: match placeholder_type {
PlaceholderType::Root(_, _) => IconName::ExclamationTriangle,
PlaceholderType::File(_) => IconName::File,
PlaceholderType::Diagnostic(DiagnosticType::Error, _) => IconName::XCircle,
PlaceholderType::Diagnostic(DiagnosticType::Warning, _) => {
IconName::ExclamationTriangle
}
},
label: match placeholder_type {
PlaceholderType::Root(summary, source) => {
let mut label = String::new();
label.push_str("Diagnostics");
if let Some(source) = source {
write!(label, " ({})", source).unwrap();
}
if summary.error_count > 0 || summary.warning_count > 0 {
label.push(':');
if summary.error_count > 0 {
write!(label, " {} errors", summary.error_count).unwrap();
if summary.warning_count > 0 {
label.push_str(",");
}
}
if summary.warning_count > 0 {
write!(label, " {} warnings", summary.warning_count).unwrap();
}
}
label.into()
}
PlaceholderType::File(file_path) => file_path.into(),
PlaceholderType::Diagnostic(_, message) => message.into(),
},
})
.collect();
Ok(SlashCommandOutput {
text,
sections: sections
.into_iter()
.map(|(range, placeholder_type)| SlashCommandOutputSection {
range,
icon: match placeholder_type {
PlaceholderType::Root(_, _) => IconName::ExclamationTriangle,
PlaceholderType::File(_) => IconName::File,
PlaceholderType::Diagnostic(DiagnosticType::Error, _) => {
IconName::XCircle
}
PlaceholderType::Diagnostic(DiagnosticType::Warning, _) => {
IconName::ExclamationTriangle
}
},
label: match placeholder_type {
PlaceholderType::Root(summary, source) => {
let mut label = String::new();
label.push_str("Diagnostics");
if let Some(source) = source {
write!(label, " ({})", source).unwrap();
}
if summary.error_count > 0 || summary.warning_count > 0 {
label.push(':');
if summary.error_count > 0 {
write!(label, " {} errors", summary.error_count).unwrap();
if summary.warning_count > 0 {
label.push_str(",");
}
}
if summary.warning_count > 0 {
write!(label, " {} warnings", summary.warning_count)
.unwrap();
}
}
label.into()
}
PlaceholderType::File(file_path) => file_path.into(),
PlaceholderType::Diagnostic(_, message) => message.into(),
},
})
.collect(),
sections,
run_commands_in_text: false,
})
})
@@ -235,25 +249,20 @@ struct Options {
const INCLUDE_WARNINGS_ARGUMENT: &str = "--include-warnings";
impl Options {
fn parse(arguments_line: Option<&str>) -> Self {
arguments_line
.map(|arguments_line| {
let args = arguments_line.split_whitespace().collect::<Vec<_>>();
let mut include_warnings = false;
let mut path_matcher = None;
for arg in args {
if arg == INCLUDE_WARNINGS_ARGUMENT {
include_warnings = true;
} else {
path_matcher = PathMatcher::new(&[arg.to_owned()]).log_err();
}
}
Self {
include_warnings,
path_matcher,
}
})
.unwrap_or_default()
fn parse(arguments: &[String]) -> Self {
let mut include_warnings = false;
let mut path_matcher = None;
for arg in arguments {
if arg == INCLUDE_WARNINGS_ARGUMENT {
include_warnings = true;
} else {
path_matcher = PathMatcher::new(&[arg.to_owned()]).log_err();
}
}
Self {
include_warnings,
path_matcher,
}
}
fn match_candidates_for_args() -> [StringMatchCandidate; 1] {

View File

@@ -7,7 +7,6 @@ use anyhow::{anyhow, bail, Result};
use assistant_slash_command::{
ArgumentCompletion, SlashCommand, SlashCommandOutput, SlashCommandOutputSection,
};
use feature_flags::FeatureFlag;
use gpui::{AppContext, BackgroundExecutor, Model, Task, WeakView};
use indexed_docs::{
DocsDotRsProvider, IndexedDocsRegistry, IndexedDocsStore, LocalRustdocProvider, PackageName,
@@ -19,12 +18,6 @@ use ui::prelude::*;
use util::{maybe, ResultExt};
use workspace::Workspace;
pub(crate) struct DocsSlashCommandFeatureFlag;
impl FeatureFlag for DocsSlashCommandFeatureFlag {
const NAME: &'static str = "docs-slash-command";
}
pub(crate) struct DocsSlashCommand;
impl DocsSlashCommand {
@@ -168,30 +161,28 @@ impl SlashCommand for DocsSlashCommand {
fn complete_argument(
self: Arc<Self>,
query: String,
arguments: &[String],
_cancel: Arc<AtomicBool>,
workspace: Option<WeakView<Workspace>>,
cx: &mut AppContext,
cx: &mut WindowContext,
) -> Task<Result<Vec<ArgumentCompletion>>> {
self.ensure_rust_doc_providers_are_registered(workspace, cx);
let indexed_docs_registry = IndexedDocsRegistry::global(cx);
let args = DocsSlashCommandArgs::parse(&query);
let args = DocsSlashCommandArgs::parse(arguments);
let store = args
.provider()
.ok_or_else(|| anyhow!("no docs provider specified"))
.and_then(|provider| IndexedDocsStore::try_global(provider, cx));
cx.background_executor().spawn(async move {
fn build_completions(
provider: ProviderId,
items: Vec<String>,
) -> Vec<ArgumentCompletion> {
fn build_completions(items: Vec<String>) -> Vec<ArgumentCompletion> {
items
.into_iter()
.map(|item| ArgumentCompletion {
label: item.clone(),
new_text: format!("{provider} {item}"),
run_command: true,
label: item.clone().into(),
new_text: item.to_string(),
after_completion: assistant_slash_command::AfterCompletion::Run,
replace_previous_arguments: false,
})
.collect()
}
@@ -201,18 +192,20 @@ impl SlashCommand for DocsSlashCommand {
let providers = indexed_docs_registry.list_providers();
if providers.is_empty() {
return Ok(vec![ArgumentCompletion {
label: "No available docs providers.".to_string(),
label: "No available docs providers.".into(),
new_text: String::new(),
run_command: false,
after_completion: false.into(),
replace_previous_arguments: false,
}]);
}
Ok(providers
.into_iter()
.map(|provider| ArgumentCompletion {
label: provider.to_string(),
label: provider.to_string().into(),
new_text: provider.to_string(),
run_command: false,
after_completion: false.into(),
replace_previous_arguments: false,
})
.collect())
}
@@ -229,17 +222,45 @@ impl SlashCommand for DocsSlashCommand {
drop(store.clone().index(package.as_str().into()));
}
let items = store.search(package).await;
Ok(build_completions(provider, items))
let suggested_packages = store.clone().suggest_packages().await?;
let search_results = store.search(package).await;
let mut items = build_completions(search_results);
let workspace_crate_completions = suggested_packages
.into_iter()
.filter(|package_name| {
!items
.iter()
.any(|item| item.label.text() == package_name.as_ref())
})
.map(|package_name| ArgumentCompletion {
label: format!("{package_name} (unindexed)").into(),
new_text: format!("{package_name}"),
after_completion: true.into(),
replace_previous_arguments: false,
})
.collect::<Vec<_>>();
items.extend(workspace_crate_completions);
if items.is_empty() {
return Ok(vec![ArgumentCompletion {
label: format!(
"Enter a {package_term} name.",
package_term = package_term(&provider)
)
.into(),
new_text: provider.to_string(),
after_completion: false.into(),
replace_previous_arguments: false,
}]);
}
Ok(items)
}
DocsSlashCommandArgs::SearchItemDocs {
provider,
item_path,
..
} => {
DocsSlashCommandArgs::SearchItemDocs { item_path, .. } => {
let store = store?;
let items = store.search(item_path).await;
Ok(build_completions(provider, items))
Ok(build_completions(items))
}
}
})
@@ -247,16 +268,16 @@ impl SlashCommand for DocsSlashCommand {
fn run(
self: Arc<Self>,
argument: Option<&str>,
arguments: &[String],
_workspace: WeakView<Workspace>,
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
cx: &mut WindowContext,
) -> Task<Result<SlashCommandOutput>> {
let Some(argument) = argument else {
return Task::ready(Err(anyhow!("missing argument")));
if arguments.is_empty() {
return Task::ready(Err(anyhow!("missing an argument")));
};
let args = DocsSlashCommandArgs::parse(argument);
let args = DocsSlashCommandArgs::parse(arguments);
let executor = cx.background_executor().clone();
let task = cx.background_executor().spawn({
let store = args
@@ -276,6 +297,13 @@ impl SlashCommand for DocsSlashCommand {
} => (provider, item_path),
};
if key.trim().is_empty() {
bail!(
"no {package_term} name provided",
package_term = package_term(&provider)
);
}
let store = store?;
if let Some(package) = args.package() {
@@ -349,12 +377,18 @@ pub(crate) enum DocsSlashCommandArgs {
}
impl DocsSlashCommandArgs {
pub fn parse(argument: &str) -> Self {
let Some((provider, argument)) = argument.split_once(' ') else {
pub fn parse(arguments: &[String]) -> Self {
let Some(provider) = arguments
.get(0)
.cloned()
.filter(|arg| !arg.trim().is_empty())
else {
return Self::NoProvider;
};
let provider = ProviderId(provider.into());
let Some(argument) = arguments.get(1) else {
return Self::NoProvider;
};
if let Some((package, rest)) = argument.split_once(is_item_path_delimiter) {
if rest.trim().is_empty() {
@@ -398,6 +432,15 @@ impl DocsSlashCommandArgs {
}
}
/// Returns the term used to refer to a package.
fn package_term(provider: &ProviderId) -> &'static str {
if provider == &DocsDotRsProvider::id() || provider == &LocalRustdocProvider::id() {
return "crate";
}
"package"
}
#[cfg(test)]
mod tests {
use super::*;
@@ -405,16 +448,16 @@ mod tests {
#[test]
fn test_parse_docs_slash_command_args() {
assert_eq!(
DocsSlashCommandArgs::parse(""),
DocsSlashCommandArgs::parse(&["".to_string()]),
DocsSlashCommandArgs::NoProvider
);
assert_eq!(
DocsSlashCommandArgs::parse("rustdoc"),
DocsSlashCommandArgs::parse(&["rustdoc".to_string()]),
DocsSlashCommandArgs::NoProvider
);
assert_eq!(
DocsSlashCommandArgs::parse("rustdoc "),
DocsSlashCommandArgs::parse(&["rustdoc".to_string(), "".to_string()]),
DocsSlashCommandArgs::SearchPackageDocs {
provider: ProviderId("rustdoc".into()),
package: "".into(),
@@ -422,7 +465,7 @@ mod tests {
}
);
assert_eq!(
DocsSlashCommandArgs::parse("gleam "),
DocsSlashCommandArgs::parse(&["gleam".to_string(), "".to_string()]),
DocsSlashCommandArgs::SearchPackageDocs {
provider: ProviderId("gleam".into()),
package: "".into(),
@@ -431,7 +474,7 @@ mod tests {
);
assert_eq!(
DocsSlashCommandArgs::parse("rustdoc gpui"),
DocsSlashCommandArgs::parse(&["rustdoc".to_string(), "gpui".to_string()]),
DocsSlashCommandArgs::SearchPackageDocs {
provider: ProviderId("rustdoc".into()),
package: "gpui".into(),
@@ -439,7 +482,7 @@ mod tests {
}
);
assert_eq!(
DocsSlashCommandArgs::parse("gleam gleam_stdlib"),
DocsSlashCommandArgs::parse(&["gleam".to_string(), "gleam_stdlib".to_string()]),
DocsSlashCommandArgs::SearchPackageDocs {
provider: ProviderId("gleam".into()),
package: "gleam_stdlib".into(),
@@ -449,7 +492,7 @@ mod tests {
// Adding an item path delimiter indicates we can start indexing.
assert_eq!(
DocsSlashCommandArgs::parse("rustdoc gpui:"),
DocsSlashCommandArgs::parse(&["rustdoc".to_string(), "gpui:".to_string()]),
DocsSlashCommandArgs::SearchPackageDocs {
provider: ProviderId("rustdoc".into()),
package: "gpui".into(),
@@ -457,7 +500,7 @@ mod tests {
}
);
assert_eq!(
DocsSlashCommandArgs::parse("gleam gleam_stdlib/"),
DocsSlashCommandArgs::parse(&["gleam".to_string(), "gleam_stdlib/".to_string()]),
DocsSlashCommandArgs::SearchPackageDocs {
provider: ProviderId("gleam".into()),
package: "gleam_stdlib".into(),
@@ -466,7 +509,10 @@ mod tests {
);
assert_eq!(
DocsSlashCommandArgs::parse("rustdoc gpui::foo::bar::Baz"),
DocsSlashCommandArgs::parse(&[
"rustdoc".to_string(),
"gpui::foo::bar::Baz".to_string()
]),
DocsSlashCommandArgs::SearchItemDocs {
provider: ProviderId("rustdoc".into()),
package: "gpui".into(),
@@ -474,7 +520,10 @@ mod tests {
}
);
assert_eq!(
DocsSlashCommandArgs::parse("gleam gleam_stdlib/gleam/int"),
DocsSlashCommandArgs::parse(&[
"gleam".to_string(),
"gleam_stdlib/gleam/int".to_string()
]),
DocsSlashCommandArgs::SearchItemDocs {
provider: ProviderId("gleam".into()),
package: "gleam_stdlib".into(),

View File

@@ -8,7 +8,7 @@ use assistant_slash_command::{
ArgumentCompletion, SlashCommand, SlashCommandOutput, SlashCommandOutputSection,
};
use futures::AsyncReadExt;
use gpui::{AppContext, Task, WeakView};
use gpui::{Task, WeakView};
use html_to_markdown::{convert_html_to_markdown, markdown, TagHandler};
use http_client::{AsyncBody, HttpClient, HttpClientWithUrl};
use language::LspAdapterDelegate;
@@ -117,22 +117,22 @@ impl SlashCommand for FetchSlashCommand {
fn complete_argument(
self: Arc<Self>,
_query: String,
_arguments: &[String],
_cancel: Arc<AtomicBool>,
_workspace: Option<WeakView<Workspace>>,
_cx: &mut AppContext,
_cx: &mut WindowContext,
) -> Task<Result<Vec<ArgumentCompletion>>> {
Task::ready(Ok(Vec::new()))
}
fn run(
self: Arc<Self>,
argument: Option<&str>,
arguments: &[String],
workspace: WeakView<Workspace>,
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
cx: &mut WindowContext,
) -> Task<Result<SlashCommandOutput>> {
let Some(argument) = argument else {
let Some(argument) = arguments.first() else {
return Task::ready(Err(anyhow!("missing URL")));
};
let Some(workspace) = workspace.upgrade() else {

View File

@@ -1,9 +1,9 @@
use super::{diagnostics_command::write_single_file_diagnostics, SlashCommand, SlashCommandOutput};
use anyhow::{anyhow, Result};
use assistant_slash_command::{ArgumentCompletion, SlashCommandOutputSection};
use anyhow::{anyhow, Context as _, Result};
use assistant_slash_command::{AfterCompletion, ArgumentCompletion, SlashCommandOutputSection};
use fuzzy::PathMatch;
use gpui::{AppContext, Model, Task, View, WeakView};
use language::{BufferSnapshot, LineEnding, LspAdapterDelegate};
use language::{BufferSnapshot, CodeLabel, HighlightId, LineEnding, LspAdapterDelegate};
use project::{PathMatchCandidateSet, Project};
use std::{
fmt::Write,
@@ -12,7 +12,7 @@ use std::{
sync::{atomic::AtomicBool, Arc},
};
use ui::prelude::*;
use util::{paths::PathMatcher, ResultExt};
use util::ResultExt;
use workspace::Workspace;
pub(crate) struct FileSlashCommand;
@@ -29,11 +29,30 @@ impl FileSlashCommand {
let workspace = workspace.read(cx);
let project = workspace.project().read(cx);
let entries = workspace.recent_navigation_history(Some(10), cx);
let entries = entries
.into_iter()
.map(|entries| (entries.0, false))
.chain(project.worktrees(cx).flat_map(|worktree| {
let worktree = worktree.read(cx);
let id = worktree.id();
worktree.child_entries(Path::new("")).map(move |entry| {
(
project::ProjectPath {
worktree_id: id,
path: entry.path.clone(),
},
entry.kind.is_dir(),
)
})
}))
.collect::<Vec<_>>();
let path_prefix: Arc<str> = Arc::default();
Task::ready(
entries
.into_iter()
.filter_map(|(entry, _)| {
.filter_map(|(entry, is_dir)| {
let worktree = project.worktree_for_id(entry.worktree_id, cx)?;
let mut full_path = PathBuf::from(worktree.read(cx).root_name());
full_path.push(&entry.path);
@@ -44,6 +63,7 @@ impl FileSlashCommand {
path: full_path.into(),
path_prefix: path_prefix.clone(),
distance_to_relative_ancestor: 0,
is_dir,
})
})
.collect(),
@@ -54,6 +74,7 @@ impl FileSlashCommand {
.into_iter()
.map(|worktree| {
let worktree = worktree.read(cx);
PathMatchCandidateSet {
snapshot: worktree.snapshot(),
include_ignored: worktree
@@ -101,32 +122,55 @@ impl SlashCommand for FileSlashCommand {
fn complete_argument(
self: Arc<Self>,
query: String,
arguments: &[String],
cancellation_flag: Arc<AtomicBool>,
workspace: Option<WeakView<Workspace>>,
cx: &mut AppContext,
cx: &mut WindowContext,
) -> Task<Result<Vec<ArgumentCompletion>>> {
let Some(workspace) = workspace.and_then(|workspace| workspace.upgrade()) else {
return Task::ready(Err(anyhow!("workspace was dropped")));
};
let paths = self.search_paths(query, cancellation_flag, &workspace, cx);
let paths = self.search_paths(
arguments.last().cloned().unwrap_or_default(),
cancellation_flag,
&workspace,
cx,
);
let comment_id = cx.theme().syntax().highlight_id("comment").map(HighlightId);
cx.background_executor().spawn(async move {
Ok(paths
.await
.into_iter()
.map(|path_match| {
.filter_map(|path_match| {
let text = format!(
"{}{}",
path_match.path_prefix,
path_match.path.to_string_lossy()
);
ArgumentCompletion {
label: text.clone(),
let mut label = CodeLabel::default();
let file_name = path_match.path.file_name()?.to_string_lossy();
let label_text = if path_match.is_dir {
format!("{}/ ", file_name)
} else {
format!("{} ", file_name)
};
label.push_str(label_text.as_str(), None);
label.push_str(&text, comment_id);
label.filter_range = 0..file_name.len();
Some(ArgumentCompletion {
label,
new_text: text,
run_command: true,
}
after_completion: if path_match.is_dir {
AfterCompletion::Compose
} else {
AfterCompletion::Run
},
replace_previous_arguments: false,
})
})
.collect())
})
@@ -134,7 +178,7 @@ impl SlashCommand for FileSlashCommand {
fn run(
self: Arc<Self>,
argument: Option<&str>,
arguments: &[String],
workspace: WeakView<Workspace>,
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
cx: &mut WindowContext,
@@ -143,23 +187,24 @@ impl SlashCommand for FileSlashCommand {
return Task::ready(Err(anyhow!("workspace was dropped")));
};
let Some(argument) = argument else {
if arguments.is_empty() {
return Task::ready(Err(anyhow!("missing path")));
};
let task = collect_files(workspace.read(cx).project().clone(), argument, cx);
let task = collect_files(workspace.read(cx).project().clone(), arguments, cx);
cx.foreground_executor().spawn(async move {
let (text, ranges) = task.await?;
let output = task.await?;
Ok(SlashCommandOutput {
text,
sections: ranges
text: output.completion_text,
sections: output
.files
.into_iter()
.map(|(range, path, entry_type)| {
.map(|file| {
build_entry_output_section(
range,
Some(&path),
entry_type == EntryType::Directory,
file.range_in_text,
Some(&file.path),
file.entry_type == EntryType::Directory,
None,
)
})
@@ -170,18 +215,38 @@ impl SlashCommand for FileSlashCommand {
}
}
#[derive(Clone, Copy, PartialEq)]
#[derive(Clone, Copy, PartialEq, Debug)]
enum EntryType {
File,
Directory,
}
#[derive(Clone, PartialEq, Debug)]
struct FileCommandOutput {
completion_text: String,
files: Vec<OutputFile>,
}
#[derive(Clone, PartialEq, Debug)]
struct OutputFile {
range_in_text: Range<usize>,
path: PathBuf,
entry_type: EntryType,
}
fn collect_files(
project: Model<Project>,
glob_input: &str,
glob_inputs: &[String],
cx: &mut AppContext,
) -> Task<Result<(String, Vec<(Range<usize>, PathBuf, EntryType)>)>> {
let Ok(matcher) = PathMatcher::new(&[glob_input.to_owned()]) else {
) -> Task<Result<FileCommandOutput>> {
let Ok(matchers) = glob_inputs
.into_iter()
.map(|glob_input| {
custom_path_matcher::PathMatcher::new(&[glob_input.to_owned()])
.with_context(|| format!("invalid path {glob_input}"))
})
.collect::<anyhow::Result<Vec<custom_path_matcher::PathMatcher>>>()
else {
return Task::ready(Err(anyhow!("invalid path")));
};
@@ -191,6 +256,7 @@ fn collect_files(
.worktrees(cx)
.map(|worktree| worktree.read(cx).snapshot())
.collect::<Vec<_>>();
cx.spawn(|mut cx| async move {
let mut text = String::new();
let mut ranges = Vec::new();
@@ -199,11 +265,16 @@ fn collect_files(
let mut directory_stack: Vec<(Arc<Path>, String, usize)> = Vec::new();
let mut folded_directory_names_stack = Vec::new();
let mut is_top_level_directory = true;
for entry in snapshot.entries(false, 0) {
let mut path_including_worktree_name = PathBuf::new();
path_including_worktree_name.push(snapshot.root_name());
path_including_worktree_name.push(&entry.path);
if !matcher.is_match(&path_including_worktree_name) {
if !matchers
.iter()
.any(|matcher| matcher.is_match(&path_including_worktree_name))
{
continue;
}
@@ -212,11 +283,11 @@ fn collect_files(
break;
}
let (_, entry_name, start) = directory_stack.pop().unwrap();
ranges.push((
start..text.len().saturating_sub(1),
PathBuf::from(entry_name),
EntryType::Directory,
));
ranges.push(OutputFile {
range_in_text: start..text.len().saturating_sub(1),
path: PathBuf::from(entry_name),
entry_type: EntryType::Directory,
});
}
let filename = entry
@@ -289,24 +360,39 @@ fn collect_files(
) {
text.pop();
}
ranges.push((
prev_len..text.len(),
path_including_worktree_name,
EntryType::File,
));
ranges.push(OutputFile {
range_in_text: prev_len..text.len(),
path: path_including_worktree_name,
entry_type: EntryType::File,
});
text.push('\n');
}
}
}
while let Some((dir, _, start)) = directory_stack.pop() {
let mut root_path = PathBuf::new();
root_path.push(snapshot.root_name());
root_path.push(&dir);
ranges.push((start..text.len(), root_path, EntryType::Directory));
while let Some((dir, entry, start)) = directory_stack.pop() {
if directory_stack.is_empty() {
let mut root_path = PathBuf::new();
root_path.push(snapshot.root_name());
root_path.push(&dir);
ranges.push(OutputFile {
range_in_text: start..text.len(),
path: root_path,
entry_type: EntryType::Directory,
});
} else {
ranges.push(OutputFile {
range_in_text: start..text.len(),
path: PathBuf::from(entry.as_str()),
entry_type: EntryType::Directory,
});
}
}
}
Ok((text, ranges))
Ok(FileCommandOutput {
completion_text: text,
files: ranges,
})
})
}
@@ -374,3 +460,300 @@ pub fn build_entry_output_section(
label: label.into(),
}
}
/// This contains a small fork of the util::paths::PathMatcher, that is stricter about the prefix
/// check. Only subpaths pass the prefix check, rather than any prefix.
mod custom_path_matcher {
use std::{fmt::Debug as _, path::Path};
use globset::{Glob, GlobSet, GlobSetBuilder};
#[derive(Clone, Debug, Default)]
pub struct PathMatcher {
sources: Vec<String>,
sources_with_trailing_slash: Vec<String>,
glob: GlobSet,
}
impl std::fmt::Display for PathMatcher {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.sources.fmt(f)
}
}
impl PartialEq for PathMatcher {
fn eq(&self, other: &Self) -> bool {
self.sources.eq(&other.sources)
}
}
impl Eq for PathMatcher {}
impl PathMatcher {
pub fn new(globs: &[String]) -> Result<Self, globset::Error> {
let globs = globs
.into_iter()
.map(|glob| Glob::new(&glob))
.collect::<Result<Vec<_>, _>>()?;
let sources = globs.iter().map(|glob| glob.glob().to_owned()).collect();
let sources_with_trailing_slash = globs
.iter()
.map(|glob| glob.glob().to_string() + std::path::MAIN_SEPARATOR_STR)
.collect();
let mut glob_builder = GlobSetBuilder::new();
for single_glob in globs {
glob_builder.add(single_glob);
}
let glob = glob_builder.build()?;
Ok(PathMatcher {
glob,
sources,
sources_with_trailing_slash,
})
}
pub fn sources(&self) -> &[String] {
&self.sources
}
pub fn is_match<P: AsRef<Path>>(&self, other: P) -> bool {
let other_path = other.as_ref();
self.sources
.iter()
.zip(self.sources_with_trailing_slash.iter())
.any(|(source, with_slash)| {
let as_bytes = other_path.as_os_str().as_encoded_bytes();
let with_slash = if source.ends_with("/") {
source.as_bytes()
} else {
with_slash.as_bytes()
};
as_bytes.starts_with(with_slash) || as_bytes.ends_with(source.as_bytes())
})
|| self.glob.is_match(other_path)
|| self.check_with_end_separator(other_path)
}
fn check_with_end_separator(&self, path: &Path) -> bool {
let path_str = path.to_string_lossy();
let separator = std::path::MAIN_SEPARATOR_STR;
if path_str.ends_with(separator) {
return false;
} else {
self.glob.is_match(path_str.to_string() + separator)
}
}
}
}
#[cfg(test)]
mod test {
use fs::FakeFs;
use gpui::TestAppContext;
use project::Project;
use serde_json::json;
use settings::SettingsStore;
use crate::slash_command::file_command::collect_files;
pub fn init_test(cx: &mut gpui::TestAppContext) {
if std::env::var("RUST_LOG").is_ok() {
env_logger::try_init().ok();
}
cx.update(|cx| {
let settings_store = SettingsStore::test(cx);
cx.set_global(settings_store);
// release_channel::init(SemanticVersion::default(), cx);
language::init(cx);
Project::init_settings(cx);
});
}
#[gpui::test]
async fn test_file_exact_matching(cx: &mut TestAppContext) {
init_test(cx);
let fs = FakeFs::new(cx.executor());
fs.insert_tree(
"/root",
json!({
"dir": {
"subdir": {
"file_0": "0"
},
"file_1": "1",
"file_2": "2",
"file_3": "3",
},
"dir.rs": "4"
}),
)
.await;
let project = Project::test(fs, ["/root".as_ref()], cx).await;
let result_1 = cx
.update(|cx| collect_files(project.clone(), &["root/dir".to_string()], cx))
.await
.unwrap();
assert!(result_1.completion_text.starts_with("root/dir"));
// 4 files + 2 directories
assert_eq!(6, result_1.files.len());
let result_2 = cx
.update(|cx| collect_files(project.clone(), &["root/dir/".to_string()], cx))
.await
.unwrap();
assert_eq!(result_1, result_2);
let result = cx
.update(|cx| collect_files(project.clone(), &["root/dir*".to_string()], cx))
.await
.unwrap();
assert!(result.completion_text.starts_with("root/dir"));
// 5 files + 2 directories
assert_eq!(7, result.files.len());
// Ensure that the project lasts until after the last await
drop(project);
}
#[gpui::test]
async fn test_file_sub_directory_rendering(cx: &mut TestAppContext) {
init_test(cx);
let fs = FakeFs::new(cx.executor());
fs.insert_tree(
"/zed",
json!({
"assets": {
"dir1": {
".gitkeep": ""
},
"dir2": {
".gitkeep": ""
},
"themes": {
"ayu": {
"LICENSE": "1",
},
"andromeda": {
"LICENSE": "2",
},
"summercamp": {
"LICENSE": "3",
},
},
},
}),
)
.await;
let project = Project::test(fs, ["/zed".as_ref()], cx).await;
let result = cx
.update(|cx| collect_files(project.clone(), &["zed/assets/themes".to_string()], cx))
.await
.unwrap();
// Sanity check
assert!(result.completion_text.starts_with("zed/assets/themes\n"));
assert_eq!(7, result.files.len());
// Ensure that full file paths are included in the real output
assert!(result
.completion_text
.contains("zed/assets/themes/andromeda/LICENSE"));
assert!(result
.completion_text
.contains("zed/assets/themes/ayu/LICENSE"));
assert!(result
.completion_text
.contains("zed/assets/themes/summercamp/LICENSE"));
assert_eq!("summercamp", result.files[5].path.to_string_lossy());
// Ensure that things are in descending order, with properly relativized paths
assert_eq!(
"zed/assets/themes/andromeda/LICENSE",
result.files[0].path.to_string_lossy()
);
assert_eq!("andromeda", result.files[1].path.to_string_lossy());
assert_eq!(
"zed/assets/themes/ayu/LICENSE",
result.files[2].path.to_string_lossy()
);
assert_eq!("ayu", result.files[3].path.to_string_lossy());
assert_eq!(
"zed/assets/themes/summercamp/LICENSE",
result.files[4].path.to_string_lossy()
);
// Ensure that the project lasts until after the last await
drop(project);
}
#[gpui::test]
async fn test_file_deep_sub_directory_rendering(cx: &mut TestAppContext) {
init_test(cx);
let fs = FakeFs::new(cx.executor());
fs.insert_tree(
"/zed",
json!({
"assets": {
"themes": {
"LICENSE": "1",
"summercamp": {
"LICENSE": "1",
"subdir": {
"LICENSE": "1",
"subsubdir": {
"LICENSE": "3",
}
}
},
},
},
}),
)
.await;
let project = Project::test(fs, ["/zed".as_ref()], cx).await;
let result = cx
.update(|cx| collect_files(project.clone(), &["zed/assets/themes".to_string()], cx))
.await
.unwrap();
assert!(result.completion_text.starts_with("zed/assets/themes\n"));
assert_eq!(
"zed/assets/themes/LICENSE",
result.files[0].path.to_string_lossy()
);
assert_eq!(
"zed/assets/themes/summercamp/LICENSE",
result.files[1].path.to_string_lossy()
);
assert_eq!(
"zed/assets/themes/summercamp/subdir/LICENSE",
result.files[2].path.to_string_lossy()
);
assert_eq!(
"zed/assets/themes/summercamp/subdir/subsubdir/LICENSE",
result.files[3].path.to_string_lossy()
);
assert_eq!("subsubdir", result.files[4].path.to_string_lossy());
assert_eq!("subdir", result.files[5].path.to_string_lossy());
assert_eq!("summercamp", result.files[6].path.to_string_lossy());
assert_eq!("zed/assets/themes", result.files[7].path.to_string_lossy());
// Ensure that the project lasts until after the last await
drop(project);
}
}

View File

@@ -6,7 +6,7 @@ use assistant_slash_command::{
ArgumentCompletion, SlashCommand, SlashCommandOutput, SlashCommandOutputSection,
};
use chrono::Local;
use gpui::{AppContext, Task, WeakView};
use gpui::{Task, WeakView};
use language::LspAdapterDelegate;
use ui::prelude::*;
use workspace::Workspace;
@@ -32,17 +32,17 @@ impl SlashCommand for NowSlashCommand {
fn complete_argument(
self: Arc<Self>,
_query: String,
_arguments: &[String],
_cancel: Arc<AtomicBool>,
_workspace: Option<WeakView<Workspace>>,
_cx: &mut AppContext,
_cx: &mut WindowContext,
) -> Task<Result<Vec<ArgumentCompletion>>> {
Task::ready(Ok(Vec::new()))
}
fn run(
self: Arc<Self>,
_argument: Option<&str>,
_arguments: &[String],
_workspace: WeakView<Workspace>,
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
_cx: &mut WindowContext,

View File

@@ -103,10 +103,10 @@ impl SlashCommand for ProjectSlashCommand {
fn complete_argument(
self: Arc<Self>,
_query: String,
_arguments: &[String],
_cancel: Arc<AtomicBool>,
_workspace: Option<WeakView<Workspace>>,
_cx: &mut AppContext,
_cx: &mut WindowContext,
) -> Task<Result<Vec<ArgumentCompletion>>> {
Task::ready(Err(anyhow!("this command does not require argument")))
}
@@ -117,7 +117,7 @@ impl SlashCommand for ProjectSlashCommand {
fn run(
self: Arc<Self>,
_argument: Option<&str>,
_arguments: &[String],
workspace: WeakView<Workspace>,
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
cx: &mut WindowContext,

View File

@@ -2,7 +2,7 @@ use super::{SlashCommand, SlashCommandOutput};
use crate::prompt_library::PromptStore;
use anyhow::{anyhow, Context, Result};
use assistant_slash_command::{ArgumentCompletion, SlashCommandOutputSection};
use gpui::{AppContext, Task, WeakView};
use gpui::{Task, WeakView};
use language::LspAdapterDelegate;
use std::sync::{atomic::AtomicBool, Arc};
use ui::prelude::*;
@@ -29,12 +29,13 @@ impl SlashCommand for PromptSlashCommand {
fn complete_argument(
self: Arc<Self>,
query: String,
arguments: &[String],
_cancellation_flag: Arc<AtomicBool>,
_workspace: Option<WeakView<Workspace>>,
cx: &mut AppContext,
cx: &mut WindowContext,
) -> Task<Result<Vec<ArgumentCompletion>>> {
let store = PromptStore::global(cx);
let query = arguments.to_owned().join(" ");
cx.background_executor().spawn(async move {
let prompts = store.await?.search(query).await;
Ok(prompts
@@ -42,9 +43,10 @@ impl SlashCommand for PromptSlashCommand {
.filter_map(|prompt| {
let prompt_title = prompt.title?.to_string();
Some(ArgumentCompletion {
label: prompt_title.clone(),
label: prompt_title.clone().into(),
new_text: prompt_title,
run_command: true,
after_completion: true.into(),
replace_previous_arguments: true,
})
})
.collect())
@@ -53,17 +55,18 @@ impl SlashCommand for PromptSlashCommand {
fn run(
self: Arc<Self>,
title: Option<&str>,
arguments: &[String],
_workspace: WeakView<Workspace>,
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
cx: &mut WindowContext,
) -> Task<Result<SlashCommandOutput>> {
let Some(title) = title else {
let title = arguments.to_owned().join(" ");
if title.trim().is_empty() {
return Task::ready(Err(anyhow!("missing prompt name")));
};
let store = PromptStore::global(cx);
let title = SharedString::from(title.to_string());
let title = SharedString::from(title.clone());
let prompt = cx.background_executor().spawn({
let title = title.clone();
async move {
@@ -77,6 +80,11 @@ impl SlashCommand for PromptSlashCommand {
});
cx.foreground_executor().spawn(async move {
let mut prompt = prompt.await?;
if prompt.starts_with('/') {
// Prevent an edge case where the inserted prompt starts with a slash command (that leads to funky rendering).
prompt.insert(0, '\n');
}
if prompt.is_empty() {
prompt.push('\n');
}

View File

@@ -49,17 +49,17 @@ impl SlashCommand for SearchSlashCommand {
fn complete_argument(
self: Arc<Self>,
_query: String,
_arguments: &[String],
_cancel: Arc<AtomicBool>,
_workspace: Option<WeakView<Workspace>>,
_cx: &mut AppContext,
_cx: &mut WindowContext,
) -> Task<Result<Vec<ArgumentCompletion>>> {
Task::ready(Ok(Vec::new()))
}
fn run(
self: Arc<Self>,
argument: Option<&str>,
arguments: &[String],
workspace: WeakView<Workspace>,
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
cx: &mut WindowContext,
@@ -67,13 +67,13 @@ impl SlashCommand for SearchSlashCommand {
let Some(workspace) = workspace.upgrade() else {
return Task::ready(Err(anyhow::anyhow!("workspace was dropped")));
};
let Some(argument) = argument else {
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 argument.split(' ') {
for part in arguments {
if let Some(parameter) = part.strip_prefix("--") {
if let Ok(count) = parameter.parse::<usize>() {
limit = Some(count);

View File

@@ -2,7 +2,7 @@ use super::{SlashCommand, SlashCommandOutput};
use anyhow::{anyhow, Context as _, Result};
use assistant_slash_command::{ArgumentCompletion, SlashCommandOutputSection};
use editor::Editor;
use gpui::{AppContext, Task, WeakView};
use gpui::{Task, WeakView};
use language::LspAdapterDelegate;
use std::sync::Arc;
use std::{path::Path, sync::atomic::AtomicBool};
@@ -26,10 +26,10 @@ impl SlashCommand for OutlineSlashCommand {
fn complete_argument(
self: Arc<Self>,
_query: String,
_arguments: &[String],
_cancel: Arc<AtomicBool>,
_workspace: Option<WeakView<Workspace>>,
_cx: &mut AppContext,
_cx: &mut WindowContext,
) -> Task<Result<Vec<ArgumentCompletion>>> {
Task::ready(Err(anyhow!("this command does not require argument")))
}
@@ -40,7 +40,7 @@ impl SlashCommand for OutlineSlashCommand {
fn run(
self: Arc<Self>,
_argument: Option<&str>,
_arguments: &[String],
workspace: WeakView<Workspace>,
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
cx: &mut WindowContext,

View File

@@ -0,0 +1,321 @@
use super::{
diagnostics_command::write_single_file_diagnostics,
file_command::{build_entry_output_section, codeblock_fence_for_path},
SlashCommand, SlashCommandOutput,
};
use anyhow::{Context, Result};
use assistant_slash_command::ArgumentCompletion;
use collections::{HashMap, HashSet};
use editor::Editor;
use futures::future::join_all;
use gpui::{Entity, Task, WeakView};
use language::{BufferSnapshot, LspAdapterDelegate};
use std::{
fmt::Write,
path::PathBuf,
sync::{atomic::AtomicBool, Arc},
};
use ui::WindowContext;
use workspace::Workspace;
pub(crate) struct TabSlashCommand;
const ALL_TABS_COMPLETION_ITEM: &str = "all";
impl SlashCommand for TabSlashCommand {
fn name(&self) -> String {
"tab".into()
}
fn description(&self) -> String {
"insert open tabs (active tab by default)".to_owned()
}
fn menu_text(&self) -> String {
"Insert Open Tabs".to_owned()
}
fn requires_argument(&self) -> bool {
false
}
fn accepts_arguments(&self) -> bool {
true
}
fn complete_argument(
self: Arc<Self>,
arguments: &[String],
cancel: Arc<AtomicBool>,
workspace: Option<WeakView<Workspace>>,
cx: &mut WindowContext,
) -> Task<Result<Vec<ArgumentCompletion>>> {
let mut has_all_tabs_completion_item = false;
let argument_set = arguments
.iter()
.filter(|argument| {
if has_all_tabs_completion_item || ALL_TABS_COMPLETION_ITEM == argument.as_str() {
has_all_tabs_completion_item = true;
false
} else {
true
}
})
.cloned()
.collect::<HashSet<_>>();
if has_all_tabs_completion_item {
return Task::ready(Ok(Vec::new()));
}
let active_item_path = workspace.as_ref().and_then(|workspace| {
workspace
.update(cx, |workspace, cx| {
let snapshot = active_item_buffer(workspace, cx).ok()?;
snapshot.resolve_file_path(cx, true)
})
.ok()
.flatten()
});
let current_query = arguments.last().cloned().unwrap_or_default();
let tab_items_search =
tab_items_for_queries(workspace, &[current_query], cancel, false, cx);
cx.spawn(|_| async move {
let tab_items = tab_items_search.await?;
let run_command = tab_items.len() == 1;
let tab_completion_items = tab_items.into_iter().filter_map(|(path, ..)| {
let path_string = path.as_deref()?.to_string_lossy().to_string();
if argument_set.contains(&path_string) {
return None;
}
if active_item_path.is_some() && active_item_path == path {
return None;
}
Some(ArgumentCompletion {
label: path_string.clone().into(),
new_text: path_string,
replace_previous_arguments: false,
after_completion: run_command.into(),
})
});
let active_item_completion = active_item_path
.as_deref()
.map(|active_item_path| active_item_path.to_string_lossy().to_string())
.filter(|path_string| !argument_set.contains(path_string))
.map(|path_string| ArgumentCompletion {
label: path_string.clone().into(),
new_text: path_string,
replace_previous_arguments: false,
after_completion: run_command.into(),
});
Ok(active_item_completion
.into_iter()
.chain(Some(ArgumentCompletion {
label: ALL_TABS_COMPLETION_ITEM.into(),
new_text: ALL_TABS_COMPLETION_ITEM.to_owned(),
replace_previous_arguments: false,
after_completion: true.into(),
}))
.chain(tab_completion_items)
.collect())
})
}
fn run(
self: Arc<Self>,
arguments: &[String],
workspace: WeakView<Workspace>,
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
cx: &mut WindowContext,
) -> Task<Result<SlashCommandOutput>> {
let tab_items_search = tab_items_for_queries(
Some(workspace),
arguments,
Arc::new(AtomicBool::new(false)),
true,
cx,
);
cx.background_executor().spawn(async move {
let mut sections = Vec::new();
let mut text = String::new();
let mut has_diagnostics = false;
for (full_path, buffer, _) in tab_items_search.await? {
let section_start_ix = text.len();
text.push_str(&codeblock_fence_for_path(full_path.as_deref(), None));
for chunk in buffer.as_rope().chunks() {
text.push_str(chunk);
}
if !text.ends_with('\n') {
text.push('\n');
}
writeln!(text, "```").unwrap();
if write_single_file_diagnostics(&mut text, full_path.as_deref(), &buffer) {
has_diagnostics = true;
}
if !text.ends_with('\n') {
text.push('\n');
}
let section_end_ix = text.len() - 1;
sections.push(build_entry_output_section(
section_start_ix..section_end_ix,
full_path.as_deref(),
false,
None,
));
}
Ok(SlashCommandOutput {
text,
sections,
run_commands_in_text: has_diagnostics,
})
})
}
}
fn tab_items_for_queries(
workspace: Option<WeakView<Workspace>>,
queries: &[String],
cancel: Arc<AtomicBool>,
strict_match: bool,
cx: &mut WindowContext,
) -> Task<anyhow::Result<Vec<(Option<PathBuf>, BufferSnapshot, usize)>>> {
let empty_query = queries.is_empty() || queries.iter().all(|query| query.trim().is_empty());
let queries = queries.to_owned();
cx.spawn(|mut cx| async move {
let mut open_buffers =
workspace
.context("no workspace")?
.update(&mut cx, |workspace, cx| {
if strict_match && empty_query {
let snapshot = active_item_buffer(workspace, cx)?;
let full_path = snapshot.resolve_file_path(cx, true);
return anyhow::Ok(vec![(full_path, snapshot, 0)]);
}
let mut timestamps_by_entity_id = HashMap::default();
let mut visited_buffers = HashSet::default();
let mut open_buffers = Vec::new();
for pane in workspace.panes() {
let pane = pane.read(cx);
for entry in pane.activation_history() {
timestamps_by_entity_id.insert(entry.entity_id, entry.timestamp);
}
}
for editor in workspace.items_of_type::<Editor>(cx) {
if let Some(buffer) = editor.read(cx).buffer().read(cx).as_singleton() {
if let Some(timestamp) =
timestamps_by_entity_id.get(&editor.entity_id())
{
if visited_buffers.insert(buffer.read(cx).remote_id()) {
let snapshot = buffer.read(cx).snapshot();
let full_path = snapshot.resolve_file_path(cx, true);
open_buffers.push((full_path, snapshot, *timestamp));
}
}
}
}
Ok(open_buffers)
})??;
let background_executor = cx.background_executor().clone();
cx.background_executor()
.spawn(async move {
open_buffers.sort_by_key(|(_, _, timestamp)| *timestamp);
if empty_query
|| queries
.iter()
.any(|query| query == ALL_TABS_COMPLETION_ITEM)
{
return Ok(open_buffers);
}
let matched_items = if strict_match {
let match_candidates = open_buffers
.iter()
.enumerate()
.filter_map(|(id, (full_path, ..))| {
let path_string = full_path.as_deref()?.to_string_lossy().to_string();
Some((id, path_string))
})
.fold(HashMap::default(), |mut candidates, (id, path_string)| {
candidates
.entry(path_string)
.or_insert_with(|| Vec::new())
.push(id);
candidates
});
queries
.iter()
.filter_map(|query| match_candidates.get(query))
.flatten()
.copied()
.filter_map(|id| open_buffers.get(id))
.cloned()
.collect()
} else {
let match_candidates = open_buffers
.iter()
.enumerate()
.filter_map(|(id, (full_path, ..))| {
let path_string = full_path.as_deref()?.to_string_lossy().to_string();
Some(fuzzy::StringMatchCandidate {
id,
char_bag: path_string.as_str().into(),
string: path_string,
})
})
.collect::<Vec<_>>();
let mut processed_matches = HashSet::default();
let file_queries = queries.iter().map(|query| {
fuzzy::match_strings(
&match_candidates,
query,
true,
usize::MAX,
&cancel,
background_executor.clone(),
)
});
join_all(file_queries)
.await
.into_iter()
.flatten()
.filter(|string_match| processed_matches.insert(string_match.candidate_id))
.filter_map(|string_match| open_buffers.get(string_match.candidate_id))
.cloned()
.collect()
};
Ok(matched_items)
})
.await
})
}
fn active_item_buffer(
workspace: &mut Workspace,
cx: &mut ui::ViewContext<Workspace>,
) -> anyhow::Result<BufferSnapshot> {
let active_editor = workspace
.active_item(cx)
.context("no active item")?
.downcast::<Editor>()
.context("active item is not an editor")?;
let snapshot = active_editor
.read(cx)
.buffer()
.read(cx)
.as_singleton()
.context("active editor is not a singleton buffer")?
.read(cx)
.snapshot();
Ok(snapshot)
}

View File

@@ -1,118 +0,0 @@
use super::{
diagnostics_command::write_single_file_diagnostics,
file_command::{build_entry_output_section, codeblock_fence_for_path},
SlashCommand, SlashCommandOutput,
};
use anyhow::{anyhow, Result};
use assistant_slash_command::ArgumentCompletion;
use collections::HashMap;
use editor::Editor;
use gpui::{AppContext, Entity, Task, WeakView};
use language::LspAdapterDelegate;
use std::{fmt::Write, sync::Arc};
use ui::WindowContext;
use workspace::Workspace;
pub(crate) struct TabsSlashCommand;
impl SlashCommand for TabsSlashCommand {
fn name(&self) -> String {
"tabs".into()
}
fn description(&self) -> String {
"insert open tabs".into()
}
fn menu_text(&self) -> String {
"Insert Open Tabs".into()
}
fn requires_argument(&self) -> bool {
false
}
fn complete_argument(
self: Arc<Self>,
_query: String,
_cancel: Arc<std::sync::atomic::AtomicBool>,
_workspace: Option<WeakView<Workspace>>,
_cx: &mut AppContext,
) -> Task<Result<Vec<ArgumentCompletion>>> {
Task::ready(Err(anyhow!("this command does not require argument")))
}
fn run(
self: Arc<Self>,
_argument: Option<&str>,
workspace: WeakView<Workspace>,
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
cx: &mut WindowContext,
) -> Task<Result<SlashCommandOutput>> {
let open_buffers = workspace.update(cx, |workspace, cx| {
let mut timestamps_by_entity_id = HashMap::default();
let mut open_buffers = Vec::new();
for pane in workspace.panes() {
let pane = pane.read(cx);
for entry in pane.activation_history() {
timestamps_by_entity_id.insert(entry.entity_id, entry.timestamp);
}
}
for editor in workspace.items_of_type::<Editor>(cx) {
if let Some(buffer) = editor.read(cx).buffer().read(cx).as_singleton() {
if let Some(timestamp) = timestamps_by_entity_id.get(&editor.entity_id()) {
let snapshot = buffer.read(cx).snapshot();
let full_path = snapshot.resolve_file_path(cx, true);
open_buffers.push((full_path, snapshot, *timestamp));
}
}
}
open_buffers
});
match open_buffers {
Ok(mut open_buffers) => cx.background_executor().spawn(async move {
open_buffers.sort_by_key(|(_, _, timestamp)| *timestamp);
let mut sections = Vec::new();
let mut text = String::new();
let mut has_diagnostics = false;
for (full_path, buffer, _) in open_buffers {
let section_start_ix = text.len();
text.push_str(&codeblock_fence_for_path(full_path.as_deref(), None));
for chunk in buffer.as_rope().chunks() {
text.push_str(chunk);
}
if !text.ends_with('\n') {
text.push('\n');
}
writeln!(text, "```").unwrap();
if write_single_file_diagnostics(&mut text, full_path.as_deref(), &buffer) {
has_diagnostics = true;
}
if !text.ends_with('\n') {
text.push('\n');
}
let section_end_ix = text.len() - 1;
sections.push(build_entry_output_section(
section_start_ix..section_end_ix,
full_path.as_deref(),
false,
None,
));
}
Ok(SlashCommandOutput {
text,
sections,
run_commands_in_text: has_diagnostics,
})
}),
Err(error) => Task::ready(Err(error)),
}
}
}

View File

@@ -5,7 +5,7 @@ use anyhow::Result;
use assistant_slash_command::{
ArgumentCompletion, SlashCommand, SlashCommandOutput, SlashCommandOutputSection,
};
use gpui::{AppContext, Task, WeakView};
use gpui::{AppContext, Task, View, WeakView};
use language::{CodeLabel, LspAdapterDelegate};
use terminal_view::{terminal_panel::TerminalPanel, TerminalView};
use ui::prelude::*;
@@ -15,17 +15,17 @@ use crate::DEFAULT_CONTEXT_LINES;
use super::create_label_for_command;
pub(crate) struct TermSlashCommand;
pub(crate) struct TerminalSlashCommand;
const LINE_COUNT_ARG: &str = "--line-count";
impl SlashCommand for TermSlashCommand {
impl SlashCommand for TerminalSlashCommand {
fn name(&self) -> String {
"term".into()
"terminal".into()
}
fn label(&self, cx: &AppContext) -> CodeLabel {
create_label_for_command("term", &[LINE_COUNT_ARG], cx)
create_label_for_command("terminal", &[LINE_COUNT_ARG], cx)
}
fn description(&self) -> String {
@@ -40,23 +40,23 @@ impl SlashCommand for TermSlashCommand {
false
}
fn accepts_arguments(&self) -> bool {
true
}
fn complete_argument(
self: Arc<Self>,
_query: String,
_arguments: &[String],
_cancel: Arc<AtomicBool>,
_workspace: Option<WeakView<Workspace>>,
_cx: &mut AppContext,
_cx: &mut WindowContext,
) -> Task<Result<Vec<ArgumentCompletion>>> {
Task::ready(Ok(vec![ArgumentCompletion {
label: LINE_COUNT_ARG.to_string(),
new_text: LINE_COUNT_ARG.to_string(),
run_command: true,
}]))
Task::ready(Ok(Vec::new()))
}
fn run(
self: Arc<Self>,
argument: Option<&str>,
arguments: &[String],
workspace: WeakView<Workspace>,
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
cx: &mut WindowContext,
@@ -64,19 +64,14 @@ impl SlashCommand for TermSlashCommand {
let Some(workspace) = workspace.upgrade() else {
return Task::ready(Err(anyhow::anyhow!("workspace was dropped")));
};
let Some(terminal_panel) = workspace.read(cx).panel::<TerminalPanel>(cx) else {
return Task::ready(Err(anyhow::anyhow!("no terminal panel open")));
};
let Some(active_terminal) = terminal_panel.read(cx).pane().and_then(|pane| {
pane.read(cx)
.active_item()
.and_then(|t| t.downcast::<TerminalView>())
}) else {
let Some(active_terminal) = resolve_active_terminal(&workspace, cx) else {
return Task::ready(Err(anyhow::anyhow!("no active terminal")));
};
let line_count = argument
.and_then(|a| parse_argument(a))
let line_count = arguments
.get(0)
.and_then(|s| s.parse::<usize>().ok())
.unwrap_or(DEFAULT_CONTEXT_LINES);
let lines = active_terminal
@@ -102,12 +97,22 @@ impl SlashCommand for TermSlashCommand {
}
}
fn parse_argument(argument: &str) -> Option<usize> {
let mut args = argument.split(' ');
if args.next() == Some(LINE_COUNT_ARG) {
if let Some(line_count) = args.next().and_then(|s| s.parse::<usize>().ok()) {
return Some(line_count);
}
fn resolve_active_terminal(
workspace: &View<Workspace>,
cx: &WindowContext,
) -> Option<View<TerminalView>> {
if let Some(terminal_view) = workspace
.read(cx)
.active_item(cx)
.and_then(|item| item.act_as::<TerminalView>(cx))
{
return Some(terminal_view);
}
None
let terminal_panel = workspace.read(cx).panel::<TerminalPanel>(cx)?;
terminal_panel.read(cx).pane().and_then(|pane| {
pane.read(cx)
.active_item()
.and_then(|t| t.downcast::<TerminalView>())
})
}

View File

@@ -7,7 +7,7 @@ use anyhow::Result;
use assistant_slash_command::{
ArgumentCompletion, SlashCommand, SlashCommandOutput, SlashCommandOutputSection,
};
use gpui::{AppContext, Task, WeakView};
use gpui::{Task, WeakView};
use language::LspAdapterDelegate;
use ui::prelude::*;
@@ -42,17 +42,17 @@ impl SlashCommand for WorkflowSlashCommand {
fn complete_argument(
self: Arc<Self>,
_query: String,
_arguments: &[String],
_cancel: Arc<AtomicBool>,
_workspace: Option<WeakView<Workspace>>,
_cx: &mut AppContext,
_cx: &mut WindowContext,
) -> Task<Result<Vec<ArgumentCompletion>>> {
Task::ready(Ok(Vec::new()))
}
fn run(
self: Arc<Self>,
_argument: Option<&str>,
_arguments: &[String],
_workspace: WeakView<Workspace>,
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
cx: &mut WindowContext,

View File

@@ -0,0 +1,306 @@
use std::sync::Arc;
use assistant_slash_command::SlashCommandRegistry;
use gpui::AnyElement;
use gpui::DismissEvent;
use gpui::WeakView;
use picker::PickerEditorPosition;
use ui::ListItemSpacing;
use gpui::SharedString;
use gpui::Task;
use picker::{Picker, PickerDelegate};
use ui::{prelude::*, ListItem, PopoverMenu, PopoverTrigger};
use crate::assistant_panel::ContextEditor;
#[derive(IntoElement)]
pub(super) struct SlashCommandSelector<T: PopoverTrigger> {
registry: Arc<SlashCommandRegistry>,
active_context_editor: WeakView<ContextEditor>,
trigger: T,
}
#[derive(Clone)]
struct SlashCommandInfo {
name: SharedString,
description: SharedString,
args: Option<SharedString>,
}
#[derive(Clone)]
enum SlashCommandEntry {
Info(SlashCommandInfo),
Advert {
name: SharedString,
renderer: fn(&mut WindowContext<'_>) -> AnyElement,
on_confirm: fn(&mut WindowContext<'_>),
},
}
impl AsRef<str> for SlashCommandEntry {
fn as_ref(&self) -> &str {
match self {
SlashCommandEntry::Info(SlashCommandInfo { name, .. })
| SlashCommandEntry::Advert { name, .. } => name,
}
}
}
pub(crate) struct SlashCommandDelegate {
all_commands: Vec<SlashCommandEntry>,
filtered_commands: Vec<SlashCommandEntry>,
active_context_editor: WeakView<ContextEditor>,
selected_index: usize,
}
impl<T: PopoverTrigger> SlashCommandSelector<T> {
pub(crate) fn new(
registry: Arc<SlashCommandRegistry>,
active_context_editor: WeakView<ContextEditor>,
trigger: T,
) -> Self {
SlashCommandSelector {
registry,
active_context_editor,
trigger,
}
}
}
impl PickerDelegate for SlashCommandDelegate {
type ListItem = ListItem;
fn match_count(&self) -> usize {
self.filtered_commands.len()
}
fn selected_index(&self) -> usize {
self.selected_index
}
fn set_selected_index(&mut self, ix: usize, cx: &mut ViewContext<Picker<Self>>) {
self.selected_index = ix.min(self.filtered_commands.len().saturating_sub(1));
cx.notify();
}
fn placeholder_text(&self, _cx: &mut WindowContext) -> Arc<str> {
"Select a command...".into()
}
fn update_matches(&mut self, query: String, cx: &mut ViewContext<Picker<Self>>) -> Task<()> {
let all_commands = self.all_commands.clone();
cx.spawn(|this, mut cx| async move {
let filtered_commands = cx
.background_executor()
.spawn(async move {
if query.is_empty() {
all_commands
} else {
all_commands
.into_iter()
.filter(|model_info| {
model_info
.as_ref()
.to_lowercase()
.contains(&query.to_lowercase())
})
.collect()
}
})
.await;
this.update(&mut cx, |this, cx| {
this.delegate.filtered_commands = filtered_commands;
this.delegate.set_selected_index(0, cx);
cx.notify();
})
.ok();
})
}
fn separators_after_indices(&self) -> Vec<usize> {
let mut ret = vec![];
let mut previous_is_advert = false;
for (index, command) in self.filtered_commands.iter().enumerate() {
if previous_is_advert {
if let SlashCommandEntry::Info(_) = command {
previous_is_advert = false;
debug_assert_ne!(
index, 0,
"index cannot be zero, as we can never have a separator at 0th position"
);
ret.push(index - 1);
}
} else {
if let SlashCommandEntry::Advert { .. } = command {
previous_is_advert = true;
if index != 0 {
ret.push(index - 1);
}
}
}
}
ret
}
fn confirm(&mut self, _secondary: bool, cx: &mut ViewContext<Picker<Self>>) {
if let Some(command) = self.filtered_commands.get(self.selected_index) {
if let SlashCommandEntry::Info(info) = command {
self.active_context_editor
.update(cx, |context_editor, cx| {
context_editor.insert_command(&info.name, cx)
})
.ok();
} else if let SlashCommandEntry::Advert { on_confirm, .. } = command {
on_confirm(cx);
}
cx.emit(DismissEvent);
}
}
fn dismissed(&mut self, _cx: &mut ViewContext<Picker<Self>>) {}
fn editor_position(&self) -> PickerEditorPosition {
PickerEditorPosition::End
}
fn render_match(
&self,
ix: usize,
selected: bool,
cx: &mut ViewContext<Picker<Self>>,
) -> Option<Self::ListItem> {
let command_info = self.filtered_commands.get(ix)?;
match command_info {
SlashCommandEntry::Info(info) => Some(
ListItem::new(ix)
.inset(true)
.spacing(ListItemSpacing::Sparse)
.selected(selected)
.child(
h_flex()
.group(format!("command-entry-label-{ix}"))
.w_full()
.min_w(px(220.))
.child(
v_flex()
.child(
h_flex()
.child(div().font_buffer(cx).child({
let mut label = format!("/{}", info.name);
if let Some(args) =
info.args.as_ref().filter(|_| selected)
{
label.push_str(&args);
}
Label::new(label).size(LabelSize::Small)
}))
.children(info.args.clone().filter(|_| !selected).map(
|args| {
div()
.font_buffer(cx)
.child(
Label::new(args).size(LabelSize::Small),
)
.visible_on_hover(format!(
"command-entry-label-{ix}"
))
},
)),
)
.child(
Label::new(info.description.clone())
.size(LabelSize::Small)
.color(Color::Muted),
),
),
),
),
SlashCommandEntry::Advert { renderer, .. } => Some(
ListItem::new(ix)
.inset(true)
.spacing(ListItemSpacing::Sparse)
.selected(selected)
.child(renderer(cx)),
),
}
}
}
impl<T: PopoverTrigger> RenderOnce for SlashCommandSelector<T> {
fn render(self, cx: &mut WindowContext) -> impl IntoElement {
let all_models = self
.registry
.featured_command_names()
.into_iter()
.filter_map(|command_name| {
let command = self.registry.command(&command_name)?;
let menu_text = SharedString::from(Arc::from(command.menu_text()));
let label = command.label(cx);
let args = label.filter_range.end.ne(&label.text.len()).then(|| {
SharedString::from(
label.text[label.filter_range.end..label.text.len()].to_owned(),
)
});
Some(SlashCommandEntry::Info(SlashCommandInfo {
name: command_name.into(),
description: menu_text,
args,
}))
})
.chain([SlashCommandEntry::Advert {
name: "create-your-command".into(),
renderer: |cx| {
v_flex()
.child(
h_flex()
.font_buffer(cx)
.items_center()
.gap_1()
.child(div().font_buffer(cx).child(
Label::new("create-your-command").size(LabelSize::Small),
))
.child(Icon::new(IconName::ArrowUpRight).size(IconSize::XSmall)),
)
.child(
Label::new("Learn how to create a custom command")
.size(LabelSize::Small)
.color(Color::Muted),
)
.into_any_element()
},
on_confirm: |cx| cx.open_url("https://zed.dev/docs/extensions/slash-commands"),
}])
.collect::<Vec<_>>();
let delegate = SlashCommandDelegate {
all_commands: all_models.clone(),
active_context_editor: self.active_context_editor.clone(),
filtered_commands: all_models,
selected_index: 0,
};
let picker_view = cx.new_view(|cx| {
let picker = Picker::uniform_list(delegate, cx).max_height(Some(rems(20.).into()));
picker
});
let handle = self
.active_context_editor
.update(cx, |this, _| this.slash_menu_handle.clone())
.ok();
PopoverMenu::new("model-switcher")
.menu(move |_cx| Some(picker_view.clone()))
.trigger(self.trigger)
.attach(gpui::AnchorCorner::TopLeft)
.anchor(gpui::AnchorCorner::BottomLeft)
.offset(gpui::Point {
x: px(0.0),
y: px(-16.0),
})
.when_some(handle, |this, handle| this.with_handle(handle))
}
}

View File

@@ -0,0 +1,44 @@
use anyhow::Result;
use gpui::AppContext;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use settings::{Settings, SettingsSources};
/// Settings for slash commands.
#[derive(Deserialize, Serialize, Debug, Default, Clone, JsonSchema)]
pub struct SlashCommandSettings {
/// Settings for the `/docs` slash command.
#[serde(default)]
pub docs: DocsCommandSettings,
/// Settings for the `/project` slash command.
#[serde(default)]
pub project: ProjectCommandSettings,
}
/// Settings for the `/docs` slash command.
#[derive(Deserialize, Serialize, Debug, Default, Clone, JsonSchema)]
pub struct DocsCommandSettings {
/// Whether `/docs` is enabled.
#[serde(default)]
pub enabled: bool,
}
/// Settings for the `/project` slash command.
#[derive(Deserialize, Serialize, Debug, Default, Clone, JsonSchema)]
pub struct ProjectCommandSettings {
/// Whether `/project` is enabled.
#[serde(default)]
pub enabled: bool,
}
impl Settings for SlashCommandSettings {
const KEY: Option<&'static str> = Some("slash_commands");
type FileContent = Self;
fn load(sources: SettingsSources<Self::FileContent>, _cx: &mut AppContext) -> Result<Self> {
SettingsSources::<Self::FileContent>::json_merge_with(
[sources.default].into_iter().chain(sources.user),
)
}
}

View File

@@ -277,6 +277,7 @@ impl TerminalInlineAssistant {
messages.push(LanguageModelRequestMessage {
role: Role::User,
content: vec![prompt.into()],
cache: false,
});
Ok(LanguageModelRequest {

View File

@@ -1,25 +0,0 @@
### Using the Assistant
Once you have configured a provider, you can interact with the provider's language models in a context editor.
To create a new context editor, use the menu in the top right of the assistant panel and the `New Context` option.
In the context editor, select a model from one of the configured providers, type a message in the `You` block, and submit with `cmd-enter` (or `ctrl-enter` on Linux).
### Inline assistant
When you're in a normal editor, you can use `ctrl-enter` to open the inline assistant.
The inline assistant allows you to send the current selection (or the current line) to a language model and modify the selection with the language model's response.
### Adding Prompts
You can customize the default prompts that are used in new context editor, by opening the `Prompt Library`.
Open the `Prompt Library` using either the menu in the top right of the assistant panel and choosing the `Prompt Library` option, or by using the `assistant: deploy prompt library` command when the assistant panel is focused.
### Viewing past contexts
You view all previous contexts by opening up the `History` tab in the assistant panel.
Open the `History` using the menu in the top right of the assistant panel and choosing the `History`.

View File

@@ -0,0 +1,803 @@
mod step_view;
use crate::{
prompts::StepResolutionContext, AssistantPanel, Context, InlineAssistId, InlineAssistant,
};
use anyhow::{anyhow, Error, Result};
use collections::HashMap;
use editor::Editor;
use futures::future;
use gpui::{
Model, ModelContext, Task, UpdateGlobal as _, View, WeakModel, WeakView, WindowContext,
};
use language::{Anchor, Buffer, BufferSnapshot, SymbolPath};
use language_model::{LanguageModelRegistry, LanguageModelRequestMessage, Role};
use project::Project;
use rope::Point;
use serde::{Deserialize, Serialize};
use smol::stream::StreamExt;
use std::{cmp, fmt::Write, ops::Range, sync::Arc};
use text::{AnchorRangeExt as _, OffsetRangeExt as _};
use util::ResultExt as _;
use workspace::Workspace;
pub use step_view::WorkflowStepView;
const IMPORTS_SYMBOL: &str = "#imports";
pub struct WorkflowStep {
context: WeakModel<Context>,
context_buffer_range: Range<Anchor>,
tool_output: String,
resolve_task: Option<Task<()>>,
pub resolution: Option<Result<WorkflowStepResolution, Arc<Error>>>,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct WorkflowStepResolution {
pub title: String,
pub suggestion_groups: HashMap<Model<Buffer>, Vec<WorkflowSuggestionGroup>>,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct WorkflowSuggestionGroup {
pub context_range: Range<language::Anchor>,
pub suggestions: Vec<WorkflowSuggestion>,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum WorkflowSuggestion {
Update {
symbol_path: SymbolPath,
range: Range<language::Anchor>,
description: String,
},
CreateFile {
description: String,
},
InsertSiblingBefore {
symbol_path: SymbolPath,
position: language::Anchor,
description: String,
},
InsertSiblingAfter {
symbol_path: SymbolPath,
position: language::Anchor,
description: String,
},
PrependChild {
symbol_path: Option<SymbolPath>,
position: language::Anchor,
description: String,
},
AppendChild {
symbol_path: Option<SymbolPath>,
position: language::Anchor,
description: String,
},
Delete {
symbol_path: SymbolPath,
range: Range<language::Anchor>,
},
}
impl WorkflowStep {
pub fn new(range: Range<Anchor>, context: WeakModel<Context>) -> Self {
Self {
context_buffer_range: range,
tool_output: String::new(),
context,
resolution: None,
resolve_task: None,
}
}
pub fn resolve(&mut self, cx: &mut ModelContext<WorkflowStep>) -> Option<()> {
let range = self.context_buffer_range.clone();
let context = self.context.upgrade()?;
let context = context.read(cx);
let project = context.project()?;
let prompt_builder = context.prompt_builder();
let mut request = context.to_completion_request(cx);
let model = LanguageModelRegistry::read_global(cx).active_model();
let context_buffer = context.buffer();
let step_text = context_buffer
.read(cx)
.text_for_range(range.clone())
.collect::<String>();
let mut workflow_context = String::new();
for message in context.messages(cx) {
write!(&mut workflow_context, "<message role={}>", message.role).unwrap();
for chunk in context_buffer.read(cx).text_for_range(message.offset_range) {
write!(&mut workflow_context, "{chunk}").unwrap();
}
write!(&mut workflow_context, "</message>").unwrap();
}
self.resolve_task = Some(cx.spawn(|this, mut cx| async move {
let result = async {
let Some(model) = model else {
return Err(anyhow!("no model selected"));
};
this.update(&mut cx, |this, cx| {
this.tool_output.clear();
this.resolution = None;
this.result_updated(cx);
cx.notify();
})?;
let resolution_context = StepResolutionContext {
workflow_context,
step_to_resolve: step_text.clone(),
};
let mut prompt =
prompt_builder.generate_step_resolution_prompt(&resolution_context)?;
prompt.push_str(&step_text);
request.messages.push(LanguageModelRequestMessage {
role: Role::User,
content: vec![prompt.into()],
cache: false,
});
// Invoke the model to get its edit suggestions for this workflow step.
let mut stream = model
.use_tool_stream::<tool::WorkflowStepResolutionTool>(request, &cx)
.await?;
while let Some(chunk) = stream.next().await {
let chunk = chunk?;
this.update(&mut cx, |this, cx| {
this.tool_output.push_str(&chunk);
cx.notify();
})?;
}
let resolution = this.update(&mut cx, |this, _| {
serde_json::from_str::<tool::WorkflowStepResolutionTool>(&this.tool_output)
})??;
this.update(&mut cx, |this, cx| {
this.tool_output = serde_json::to_string_pretty(&resolution).unwrap();
cx.notify();
})?;
// Translate the parsed suggestions to our internal types, which anchor the suggestions to locations in the code.
let suggestion_tasks: Vec<_> = resolution
.suggestions
.iter()
.map(|suggestion| suggestion.resolve(project.clone(), cx.clone()))
.collect();
// Expand the context ranges of each suggestion and group suggestions with overlapping context ranges.
let suggestions = future::join_all(suggestion_tasks)
.await
.into_iter()
.filter_map(|task| task.log_err())
.collect::<Vec<_>>();
let mut suggestions_by_buffer = HashMap::default();
for (buffer, suggestion) in suggestions {
suggestions_by_buffer
.entry(buffer)
.or_insert_with(Vec::new)
.push(suggestion);
}
let mut suggestion_groups_by_buffer = HashMap::default();
for (buffer, mut suggestions) in suggestions_by_buffer {
let mut suggestion_groups = Vec::<WorkflowSuggestionGroup>::new();
let snapshot = buffer.update(&mut cx, |buffer, _| buffer.snapshot())?;
// Sort suggestions by their range so that earlier, larger ranges come first
suggestions.sort_by(|a, b| a.range().cmp(&b.range(), &snapshot));
// Merge overlapping suggestions
suggestions.dedup_by(|a, b| b.try_merge(a, &snapshot));
// Create context ranges for each suggestion
for suggestion in suggestions {
let context_range = {
let suggestion_point_range = suggestion.range().to_point(&snapshot);
let start_row = suggestion_point_range.start.row.saturating_sub(5);
let end_row = cmp::min(
suggestion_point_range.end.row + 5,
snapshot.max_point().row,
);
let start = snapshot.anchor_before(Point::new(start_row, 0));
let end = snapshot
.anchor_after(Point::new(end_row, snapshot.line_len(end_row)));
start..end
};
if let Some(last_group) = suggestion_groups.last_mut() {
if last_group
.context_range
.end
.cmp(&context_range.start, &snapshot)
.is_ge()
{
// Merge with the previous group if context ranges overlap
last_group.context_range.end = context_range.end;
last_group.suggestions.push(suggestion);
} else {
// Create a new group
suggestion_groups.push(WorkflowSuggestionGroup {
context_range,
suggestions: vec![suggestion],
});
}
} else {
// Create the first group
suggestion_groups.push(WorkflowSuggestionGroup {
context_range,
suggestions: vec![suggestion],
});
}
}
suggestion_groups_by_buffer.insert(buffer, suggestion_groups);
}
Ok((resolution.step_title, suggestion_groups_by_buffer))
};
let result = result.await;
this.update(&mut cx, |this, cx| {
this.resolution = Some(match result {
Ok((title, suggestion_groups)) => Ok(WorkflowStepResolution {
title,
suggestion_groups,
}),
Err(error) => Err(Arc::new(error)),
});
this.context
.update(cx, |context, cx| context.workflow_step_updated(range, cx))
.ok();
cx.notify();
})
.ok();
}));
None
}
fn result_updated(&mut self, cx: &mut ModelContext<Self>) {
self.context
.update(cx, |context, cx| {
context.workflow_step_updated(self.context_buffer_range.clone(), cx)
})
.ok();
}
}
impl WorkflowSuggestion {
pub fn range(&self) -> Range<language::Anchor> {
match self {
Self::Update { range, .. } => range.clone(),
Self::CreateFile { .. } => language::Anchor::MIN..language::Anchor::MAX,
Self::InsertSiblingBefore { position, .. }
| Self::InsertSiblingAfter { position, .. }
| Self::PrependChild { position, .. }
| Self::AppendChild { position, .. } => *position..*position,
Self::Delete { range, .. } => range.clone(),
}
}
pub fn description(&self) -> Option<&str> {
match self {
Self::Update { description, .. }
| Self::CreateFile { description }
| Self::InsertSiblingBefore { description, .. }
| Self::InsertSiblingAfter { description, .. }
| Self::PrependChild { description, .. }
| Self::AppendChild { description, .. } => Some(description),
Self::Delete { .. } => None,
}
}
fn description_mut(&mut self) -> Option<&mut String> {
match self {
Self::Update { description, .. }
| Self::CreateFile { description }
| Self::InsertSiblingBefore { description, .. }
| Self::InsertSiblingAfter { description, .. }
| Self::PrependChild { description, .. }
| Self::AppendChild { description, .. } => Some(description),
Self::Delete { .. } => None,
}
}
fn symbol_path(&self) -> Option<&SymbolPath> {
match self {
Self::Update { symbol_path, .. } => Some(symbol_path),
Self::InsertSiblingBefore { symbol_path, .. } => Some(symbol_path),
Self::InsertSiblingAfter { symbol_path, .. } => Some(symbol_path),
Self::PrependChild { symbol_path, .. } => symbol_path.as_ref(),
Self::AppendChild { symbol_path, .. } => symbol_path.as_ref(),
Self::Delete { symbol_path, .. } => Some(symbol_path),
Self::CreateFile { .. } => None,
}
}
fn kind(&self) -> &str {
match self {
Self::Update { .. } => "Update",
Self::CreateFile { .. } => "CreateFile",
Self::InsertSiblingBefore { .. } => "InsertSiblingBefore",
Self::InsertSiblingAfter { .. } => "InsertSiblingAfter",
Self::PrependChild { .. } => "PrependChild",
Self::AppendChild { .. } => "AppendChild",
Self::Delete { .. } => "Delete",
}
}
fn try_merge(&mut self, other: &Self, buffer: &BufferSnapshot) -> bool {
let range = self.range();
let other_range = other.range();
// Don't merge if we don't contain the other suggestion.
if range.start.cmp(&other_range.start, buffer).is_gt()
|| range.end.cmp(&other_range.end, buffer).is_lt()
{
return false;
}
if let Some(description) = self.description_mut() {
if let Some(other_description) = other.description() {
description.push('\n');
description.push_str(other_description);
}
}
true
}
pub fn show(
&self,
editor: &View<Editor>,
excerpt_id: editor::ExcerptId,
workspace: &WeakView<Workspace>,
assistant_panel: &View<AssistantPanel>,
cx: &mut WindowContext,
) -> Option<InlineAssistId> {
let mut initial_transaction_id = None;
let initial_prompt;
let suggestion_range;
let buffer = editor.read(cx).buffer().clone();
let snapshot = buffer.read(cx).snapshot(cx);
match self {
Self::Update {
range, description, ..
} => {
initial_prompt = description.clone();
suggestion_range = snapshot.anchor_in_excerpt(excerpt_id, range.start)?
..snapshot.anchor_in_excerpt(excerpt_id, range.end)?;
}
Self::CreateFile { description } => {
initial_prompt = description.clone();
suggestion_range = editor::Anchor::min()..editor::Anchor::min();
}
Self::InsertSiblingBefore {
position,
description,
..
} => {
let position = snapshot.anchor_in_excerpt(excerpt_id, *position)?;
initial_prompt = description.clone();
suggestion_range = buffer.update(cx, |buffer, cx| {
buffer.start_transaction(cx);
let line_start = buffer.insert_empty_line(position, true, true, cx);
initial_transaction_id = buffer.end_transaction(cx);
buffer.refresh_preview(cx);
let line_start = buffer.read(cx).anchor_before(line_start);
line_start..line_start
});
}
Self::InsertSiblingAfter {
position,
description,
..
} => {
let position = snapshot.anchor_in_excerpt(excerpt_id, *position)?;
initial_prompt = description.clone();
suggestion_range = buffer.update(cx, |buffer, cx| {
buffer.start_transaction(cx);
let line_start = buffer.insert_empty_line(position, true, true, cx);
initial_transaction_id = buffer.end_transaction(cx);
buffer.refresh_preview(cx);
let line_start = buffer.read(cx).anchor_before(line_start);
line_start..line_start
});
}
Self::PrependChild {
position,
description,
..
} => {
let position = snapshot.anchor_in_excerpt(excerpt_id, *position)?;
initial_prompt = description.clone();
suggestion_range = buffer.update(cx, |buffer, cx| {
buffer.start_transaction(cx);
let line_start = buffer.insert_empty_line(position, false, true, cx);
initial_transaction_id = buffer.end_transaction(cx);
buffer.refresh_preview(cx);
let line_start = buffer.read(cx).anchor_before(line_start);
line_start..line_start
});
}
Self::AppendChild {
position,
description,
..
} => {
let position = snapshot.anchor_in_excerpt(excerpt_id, *position)?;
initial_prompt = description.clone();
suggestion_range = buffer.update(cx, |buffer, cx| {
buffer.start_transaction(cx);
let line_start = buffer.insert_empty_line(position, true, false, cx);
initial_transaction_id = buffer.end_transaction(cx);
buffer.refresh_preview(cx);
let line_start = buffer.read(cx).anchor_before(line_start);
line_start..line_start
});
}
Self::Delete { range, .. } => {
initial_prompt = "Delete".to_string();
suggestion_range = snapshot.anchor_in_excerpt(excerpt_id, range.start)?
..snapshot.anchor_in_excerpt(excerpt_id, range.end)?;
}
}
InlineAssistant::update_global(cx, |inline_assistant, cx| {
Some(inline_assistant.suggest_assist(
editor,
suggestion_range,
initial_prompt,
initial_transaction_id,
Some(workspace.clone()),
Some(assistant_panel),
cx,
))
})
}
}
pub mod tool {
use super::*;
use anyhow::Context as _;
use gpui::AsyncAppContext;
use language::{Outline, OutlineItem, ParseStatus};
use language_model::LanguageModelTool;
use project::ProjectPath;
use schemars::JsonSchema;
use std::path::Path;
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct WorkflowStepResolutionTool {
/// An extremely short title for the edit step represented by these operations.
pub step_title: String,
/// A sequence of operations to apply to the codebase.
/// When multiple operations are required for a step, be sure to include multiple operations in this list.
pub suggestions: Vec<WorkflowSuggestionTool>,
}
impl LanguageModelTool for WorkflowStepResolutionTool {
fn name() -> String {
"edit".into()
}
fn description() -> String {
"suggest edits to one or more locations in the codebase".into()
}
}
/// A description of an operation to apply to one location in the codebase.
///
/// This object represents a single edit operation that can be performed on a specific file
/// in the codebase. It encapsulates both the location (file path) and the nature of the
/// edit to be made.
///
/// # Fields
///
/// * `path`: A string representing the file path where the edit operation should be applied.
/// This path is relative to the root of the project or repository.
///
/// * `kind`: An enum representing the specific type of edit operation to be performed.
///
/// # Usage
///
/// `EditOperation` is used within a code editor to represent and apply
/// programmatic changes to source code. It provides a structured way to describe
/// edits for features like refactoring tools or AI-assisted coding suggestions.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct WorkflowSuggestionTool {
/// The path to the file containing the relevant operation
pub path: String,
#[serde(flatten)]
pub kind: WorkflowSuggestionToolKind,
}
impl WorkflowSuggestionTool {
pub(super) async fn resolve(
&self,
project: Model<Project>,
mut cx: AsyncAppContext,
) -> Result<(Model<Buffer>, super::WorkflowSuggestion)> {
let path = self.path.clone();
let kind = self.kind.clone();
let buffer = project
.update(&mut cx, |project, cx| {
let project_path = project
.find_project_path(Path::new(&path), cx)
.or_else(|| {
// If we couldn't find a project path for it, put it in the active worktree
// so that when we create the buffer, it can be saved.
let worktree = project
.active_entry()
.and_then(|entry_id| project.worktree_for_entry(entry_id, cx))
.or_else(|| project.worktrees(cx).next())?;
let worktree = worktree.read(cx);
Some(ProjectPath {
worktree_id: worktree.id(),
path: Arc::from(Path::new(&path)),
})
})
.with_context(|| format!("worktree not found for {:?}", path))?;
anyhow::Ok(project.open_buffer(project_path, cx))
})??
.await?;
let mut parse_status = buffer.read_with(&cx, |buffer, _cx| buffer.parse_status())?;
while *parse_status.borrow() != ParseStatus::Idle {
parse_status.changed().await?;
}
let snapshot = buffer.update(&mut cx, |buffer, _| buffer.snapshot())?;
let outline = snapshot.outline(None).context("no outline for buffer")?;
let suggestion = match kind {
WorkflowSuggestionToolKind::Update {
symbol,
description,
} => {
let (symbol_path, symbol) = Self::resolve_symbol(&snapshot, &outline, &symbol)?;
let start = symbol
.annotation_range
.map_or(symbol.range.start, |range| range.start);
let start = Point::new(start.row, 0);
let end = Point::new(
symbol.range.end.row,
snapshot.line_len(symbol.range.end.row),
);
let range = snapshot.anchor_before(start)..snapshot.anchor_after(end);
WorkflowSuggestion::Update {
range,
description,
symbol_path,
}
}
WorkflowSuggestionToolKind::Create { description } => {
WorkflowSuggestion::CreateFile { description }
}
WorkflowSuggestionToolKind::InsertSiblingBefore {
symbol,
description,
} => {
let (symbol_path, symbol) = Self::resolve_symbol(&snapshot, &outline, &symbol)?;
let position = snapshot.anchor_before(
symbol
.annotation_range
.map_or(symbol.range.start, |annotation_range| {
annotation_range.start
}),
);
WorkflowSuggestion::InsertSiblingBefore {
position,
description,
symbol_path,
}
}
WorkflowSuggestionToolKind::InsertSiblingAfter {
symbol,
description,
} => {
let (symbol_path, symbol) = Self::resolve_symbol(&snapshot, &outline, &symbol)?;
let position = snapshot.anchor_after(symbol.range.end);
WorkflowSuggestion::InsertSiblingAfter {
position,
description,
symbol_path,
}
}
WorkflowSuggestionToolKind::PrependChild {
symbol,
description,
} => {
if let Some(symbol) = symbol {
let (symbol_path, symbol) =
Self::resolve_symbol(&snapshot, &outline, &symbol)?;
let position = snapshot.anchor_after(
symbol
.body_range
.map_or(symbol.range.start, |body_range| body_range.start),
);
WorkflowSuggestion::PrependChild {
position,
description,
symbol_path: Some(symbol_path),
}
} else {
WorkflowSuggestion::PrependChild {
position: language::Anchor::MIN,
description,
symbol_path: None,
}
}
}
WorkflowSuggestionToolKind::AppendChild {
symbol,
description,
} => {
if let Some(symbol) = symbol {
let (symbol_path, symbol) =
Self::resolve_symbol(&snapshot, &outline, &symbol)?;
let position = snapshot.anchor_before(
symbol
.body_range
.map_or(symbol.range.end, |body_range| body_range.end),
);
WorkflowSuggestion::AppendChild {
position,
description,
symbol_path: Some(symbol_path),
}
} else {
WorkflowSuggestion::PrependChild {
position: language::Anchor::MAX,
description,
symbol_path: None,
}
}
}
WorkflowSuggestionToolKind::Delete { symbol } => {
let (symbol_path, symbol) = Self::resolve_symbol(&snapshot, &outline, &symbol)?;
let start = symbol
.annotation_range
.map_or(symbol.range.start, |range| range.start);
let start = Point::new(start.row, 0);
let end = Point::new(
symbol.range.end.row,
snapshot.line_len(symbol.range.end.row),
);
let range = snapshot.anchor_before(start)..snapshot.anchor_after(end);
WorkflowSuggestion::Delete { range, symbol_path }
}
};
Ok((buffer, suggestion))
}
fn resolve_symbol(
snapshot: &BufferSnapshot,
outline: &Outline<Anchor>,
symbol: &str,
) -> Result<(SymbolPath, OutlineItem<Point>)> {
if symbol == IMPORTS_SYMBOL {
let target_row = find_first_non_comment_line(snapshot);
Ok((
SymbolPath(IMPORTS_SYMBOL.to_string()),
OutlineItem {
range: Point::new(target_row, 0)..Point::new(target_row + 1, 0),
..Default::default()
},
))
} else {
let (symbol_path, symbol) = outline
.find_most_similar(symbol)
.with_context(|| format!("symbol not found: {symbol}"))?;
Ok((symbol_path, symbol.to_point(snapshot)))
}
}
}
fn find_first_non_comment_line(snapshot: &BufferSnapshot) -> u32 {
let Some(language) = snapshot.language() else {
return 0;
};
let scope = language.default_scope();
let comment_prefixes = scope.line_comment_prefixes();
let mut chunks = snapshot.as_rope().chunks();
let mut target_row = 0;
loop {
let starts_with_comment = chunks
.peek()
.map(|chunk| {
comment_prefixes
.iter()
.any(|s| chunk.starts_with(s.as_ref().trim_end()))
})
.unwrap_or(false);
if !starts_with_comment {
break;
}
target_row += 1;
if !chunks.next_line() {
break;
}
}
target_row
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(tag = "kind")]
pub enum WorkflowSuggestionToolKind {
/// Rewrites the specified symbol entirely based on the given description.
/// This operation completely replaces the existing symbol with new content.
Update {
/// A fully-qualified reference to the symbol, e.g. `mod foo impl Bar pub fn baz` instead of just `fn baz`.
/// The path should uniquely identify the symbol within the containing file.
symbol: String,
/// A brief description of the transformation to apply to the symbol.
description: String,
},
/// Creates a new file with the given path based on the provided description.
/// This operation adds a new file to the codebase.
Create {
/// A brief description of the file to be created.
description: String,
},
/// Inserts a new symbol based on the given description before the specified symbol.
/// This operation adds new content immediately preceding an existing symbol.
InsertSiblingBefore {
/// A fully-qualified reference to the symbol, e.g. `mod foo impl Bar pub fn baz` instead of just `fn baz`.
/// The new content will be inserted immediately before this symbol.
symbol: String,
/// A brief description of the new symbol to be inserted.
description: String,
},
/// Inserts a new symbol based on the given description after the specified symbol.
/// This operation adds new content immediately following an existing symbol.
InsertSiblingAfter {
/// A fully-qualified reference to the symbol, e.g. `mod foo impl Bar pub fn baz` instead of just `fn baz`.
/// The new content will be inserted immediately after this symbol.
symbol: String,
/// A brief description of the new symbol to be inserted.
description: String,
},
/// Inserts a new symbol as a child of the specified symbol at the start.
/// This operation adds new content as the first child of an existing symbol (or file if no symbol is provided).
PrependChild {
/// An optional fully-qualified reference to the symbol after the code you want to insert, e.g. `mod foo impl Bar pub fn baz` instead of just `fn baz`.
/// If provided, the new content will be inserted as the first child of this symbol.
/// If not provided, the new content will be inserted at the top of the file.
symbol: Option<String>,
/// A brief description of the new symbol to be inserted.
description: String,
},
/// Inserts a new symbol as a child of the specified symbol at the end.
/// This operation adds new content as the last child of an existing symbol (or file if no symbol is provided).
AppendChild {
/// An optional fully-qualified reference to the symbol before the code you want to insert, e.g. `mod foo impl Bar pub fn baz` instead of just `fn baz`.
/// If provided, the new content will be inserted as the last child of this symbol.
/// If not provided, the new content will be applied at the bottom of the file.
symbol: Option<String>,
/// A brief description of the new symbol to be inserted.
description: String,
},
/// Deletes the specified symbol from the containing file.
Delete {
/// An fully-qualified reference to the symbol to be deleted, e.g. `mod foo impl Bar pub fn baz` instead of just `fn baz`.
symbol: String,
},
}
}

View File

@@ -0,0 +1,315 @@
use super::WorkflowStep;
use crate::{Assist, Context};
use editor::{
display_map::{BlockDisposition, BlockProperties, BlockStyle},
Editor, EditorEvent, ExcerptRange, MultiBuffer,
};
use gpui::{
div, AnyElement, AppContext, Context as _, Empty, EventEmitter, FocusableView, IntoElement,
Model, ParentElement as _, Render, SharedString, Styled as _, View, ViewContext,
VisualContext as _, WeakModel, WindowContext,
};
use language::{language_settings::SoftWrap, Anchor, Buffer, LanguageRegistry};
use std::{ops::DerefMut, sync::Arc};
use text::OffsetRangeExt;
use theme::ActiveTheme as _;
use ui::{
h_flex, v_flex, ButtonCommon as _, ButtonLike, ButtonStyle, Color, Icon, IconName,
InteractiveElement as _, Label, LabelCommon as _,
};
use workspace::{
item::{self, Item},
pane,
searchable::SearchableItemHandle,
};
pub struct WorkflowStepView {
step: WeakModel<WorkflowStep>,
tool_output_buffer: Model<Buffer>,
editor: View<Editor>,
}
impl WorkflowStepView {
pub fn new(
context: Model<Context>,
step: Model<WorkflowStep>,
language_registry: Arc<LanguageRegistry>,
cx: &mut ViewContext<Self>,
) -> Self {
let tool_output_buffer =
cx.new_model(|cx| Buffer::local(step.read(cx).tool_output.clone(), cx));
let buffer = cx.new_model(|cx| {
let mut buffer = MultiBuffer::without_headers(0, language::Capability::ReadWrite);
buffer.push_excerpts(
context.read(cx).buffer().clone(),
[ExcerptRange {
context: step.read(cx).context_buffer_range.clone(),
primary: None,
}],
cx,
);
buffer.push_excerpts(
tool_output_buffer.clone(),
[ExcerptRange {
context: Anchor::MIN..Anchor::MAX,
primary: None,
}],
cx,
);
buffer
});
let buffer_snapshot = buffer.read(cx).snapshot(cx);
let output_excerpt = buffer_snapshot.excerpts().skip(1).next().unwrap().0;
let input_start_anchor = multi_buffer::Anchor::min();
let output_start_anchor = buffer_snapshot
.anchor_in_excerpt(output_excerpt, Anchor::MIN)
.unwrap();
let output_end_anchor = multi_buffer::Anchor::max();
let handle = cx.view().downgrade();
let editor = cx.new_view(|cx| {
let mut editor = Editor::for_multibuffer(buffer.clone(), None, false, cx);
editor.set_soft_wrap_mode(SoftWrap::EditorWidth, cx);
editor.set_show_line_numbers(false, cx);
editor.set_show_git_diff_gutter(false, cx);
editor.set_show_code_actions(false, cx);
editor.set_show_runnables(false, cx);
editor.set_show_wrap_guides(false, cx);
editor.set_show_indent_guides(false, cx);
editor.set_read_only(true);
editor.set_show_inline_completions(false);
editor.insert_blocks(
[
BlockProperties {
position: input_start_anchor,
height: 1,
style: BlockStyle::Fixed,
render: Box::new(|cx| section_header("Step Input", cx)),
disposition: BlockDisposition::Above,
priority: 0,
},
BlockProperties {
position: output_start_anchor,
height: 1,
style: BlockStyle::Fixed,
render: Box::new(|cx| section_header("Tool Output", cx)),
disposition: BlockDisposition::Above,
priority: 0,
},
BlockProperties {
position: output_end_anchor,
height: 1,
style: BlockStyle::Fixed,
render: Box::new(move |cx| {
if let Some(result) = handle.upgrade().and_then(|this| {
this.update(cx.deref_mut(), |this, cx| this.render_result(cx))
}) {
v_flex()
.child(section_header("Output", cx))
.child(
div().pl(cx.gutter_dimensions.full_width()).child(result),
)
.into_any_element()
} else {
Empty.into_any_element()
}
}),
disposition: BlockDisposition::Below,
priority: 0,
},
],
None,
cx,
);
editor
});
cx.observe(&step, Self::step_updated).detach();
cx.observe_release(&step, Self::step_released).detach();
cx.spawn(|this, mut cx| async move {
if let Ok(language) = language_registry.language_for_name("JSON").await {
this.update(&mut cx, |this, cx| {
this.tool_output_buffer.update(cx, |buffer, cx| {
buffer.set_language(Some(language), cx);
});
})
.ok();
}
})
.detach();
Self {
tool_output_buffer,
step: step.downgrade(),
editor,
}
}
pub fn step(&self) -> &WeakModel<WorkflowStep> {
&self.step
}
fn render_result(&mut self, cx: &mut ViewContext<Self>) -> Option<AnyElement> {
let step = self.step.upgrade()?;
let result = step.read(cx).resolution.as_ref()?;
match result {
Ok(result) => {
Some(
v_flex()
.child(result.title.clone())
.children(result.suggestion_groups.iter().filter_map(
|(buffer, suggestion_groups)| {
let buffer = buffer.read(cx);
let path = buffer.file().map(|f| f.path());
let snapshot = buffer.snapshot();
v_flex()
.mb_2()
.border_b_1()
.children(path.map(|path| format!("path: {}", path.display())))
.children(suggestion_groups.iter().map(|group| {
v_flex().pt_2().pl_2().children(
group.suggestions.iter().map(|suggestion| {
let range = suggestion.range().to_point(&snapshot);
v_flex()
.children(
suggestion.description().map(|desc| {
format!("description: {desc}")
}),
)
.child(format!("kind: {}", suggestion.kind()))
.children(suggestion.symbol_path().map(
|path| format!("symbol path: {}", path.0),
))
.child(format!(
"lines: {} - {}",
range.start.row + 1,
range.end.row + 1
))
}),
)
}))
.into()
},
))
.into_any_element(),
)
}
Err(error) => Some(format!("{:?}", error).into_any_element()),
}
}
fn step_updated(&mut self, step: Model<WorkflowStep>, cx: &mut ViewContext<Self>) {
self.tool_output_buffer.update(cx, |buffer, cx| {
let text = step.read(cx).tool_output.clone();
buffer.set_text(text, cx);
});
cx.notify();
}
fn step_released(&mut self, _: &mut WorkflowStep, cx: &mut ViewContext<Self>) {
cx.emit(EditorEvent::Closed);
}
fn resolve(&mut self, _: &Assist, cx: &mut ViewContext<Self>) {
self.step
.update(cx, |step, cx| {
step.resolve(cx);
})
.ok();
}
}
fn section_header(
name: &'static str,
cx: &mut editor::display_map::BlockContext,
) -> gpui::AnyElement {
h_flex()
.pl(cx.gutter_dimensions.full_width())
.h_11()
.w_full()
.relative()
.gap_1()
.child(
ButtonLike::new("role")
.style(ButtonStyle::Filled)
.child(Label::new(name).color(Color::Default)),
)
.into_any_element()
}
impl Render for WorkflowStepView {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
div()
.key_context("ContextEditor")
.on_action(cx.listener(Self::resolve))
.flex_grow()
.bg(cx.theme().colors().editor_background)
.child(self.editor.clone())
}
}
impl EventEmitter<EditorEvent> for WorkflowStepView {}
impl FocusableView for WorkflowStepView {
fn focus_handle(&self, cx: &gpui::AppContext) -> gpui::FocusHandle {
self.editor.read(cx).focus_handle(cx)
}
}
impl Item for WorkflowStepView {
type Event = EditorEvent;
fn tab_content_text(&self, cx: &WindowContext) -> Option<SharedString> {
let step = self.step.upgrade()?.read(cx);
let context = step.context.upgrade()?.read(cx);
let buffer = context.buffer().read(cx);
let index = context
.workflow_step_index_for_range(&step.context_buffer_range, buffer)
.ok()?
+ 1;
Some(format!("Step {index}").into())
}
fn tab_icon(&self, _cx: &WindowContext) -> Option<ui::Icon> {
Some(Icon::new(IconName::SearchCode))
}
fn to_item_events(event: &Self::Event, mut f: impl FnMut(item::ItemEvent)) {
match event {
EditorEvent::Edited { .. } => {
f(item::ItemEvent::Edit);
}
EditorEvent::TitleChanged => {
f(item::ItemEvent::UpdateTab);
}
EditorEvent::Closed => f(item::ItemEvent::CloseItem),
_ => {}
}
}
fn tab_tooltip_text(&self, _cx: &AppContext) -> Option<SharedString> {
None
}
fn as_searchable(&self, _handle: &View<Self>) -> Option<Box<dyn SearchableItemHandle>> {
None
}
fn set_nav_history(&mut self, nav_history: pane::ItemNavHistory, cx: &mut ViewContext<Self>) {
self.editor.update(cx, |editor, cx| {
Item::set_nav_history(editor, nav_history, cx)
})
}
fn navigate(&mut self, data: Box<dyn std::any::Any>, cx: &mut ViewContext<Self>) -> bool {
self.editor
.update(cx, |editor, cx| Item::navigate(editor, data, cx))
}
fn deactivated(&mut self, cx: &mut ViewContext<Self>) {
self.editor
.update(cx, |editor, cx| Item::deactivated(editor, cx))
}
}

View File

@@ -15,14 +15,45 @@ pub fn init(cx: &mut AppContext) {
SlashCommandRegistry::default_global(cx);
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum AfterCompletion {
/// Run the command
Run,
/// Continue composing the current argument, doesn't add a space
Compose,
/// Continue the command composition, adds a space
Continue,
}
impl From<bool> for AfterCompletion {
fn from(value: bool) -> Self {
if value {
AfterCompletion::Run
} else {
AfterCompletion::Continue
}
}
}
impl AfterCompletion {
pub fn run(&self) -> bool {
match self {
AfterCompletion::Run => true,
AfterCompletion::Compose | AfterCompletion::Continue => false,
}
}
}
#[derive(Debug)]
pub struct ArgumentCompletion {
/// The label to display for this completion.
pub label: String,
pub label: CodeLabel,
/// The new text that should be inserted into the command when this completion is accepted.
pub new_text: String,
/// Whether the command should be run when accepting this completion.
pub run_command: bool,
pub after_completion: AfterCompletion,
/// Whether to replace the all arguments, or whether to treat this as an independent argument.
pub replace_previous_arguments: bool,
}
pub trait SlashCommand: 'static + Send + Sync {
@@ -34,15 +65,18 @@ pub trait SlashCommand: 'static + Send + Sync {
fn menu_text(&self) -> String;
fn complete_argument(
self: Arc<Self>,
query: String,
arguments: &[String],
cancel: Arc<AtomicBool>,
workspace: Option<WeakView<Workspace>>,
cx: &mut AppContext,
cx: &mut WindowContext,
) -> Task<Result<Vec<ArgumentCompletion>>>;
fn requires_argument(&self) -> bool;
fn accepts_arguments(&self) -> bool {
self.requires_argument()
}
fn run(
self: Arc<Self>,
argument: Option<&str>,
arguments: &[String],
workspace: WeakView<Workspace>,
// TODO: We're just using the `LspAdapterDelegate` here because that is
// what the extension API is already expecting.

View File

@@ -56,6 +56,18 @@ impl SlashCommandRegistry {
state.commands.insert(command_name, Arc::new(command));
}
/// Unregisters the provided [`SlashCommand`].
pub fn unregister_command(&self, command: impl SlashCommand) {
self.unregister_command_by_name(command.name().as_str())
}
/// Unregisters the command with the given name.
pub fn unregister_command_by_name(&self, command_name: &str) {
let mut state = self.state.write();
state.featured_commands.remove(command_name);
state.commands.remove(command_name);
}
/// Returns the names of registered [`SlashCommand`]s.
pub fn command_names(&self) -> Vec<Arc<str>> {
self.state.read().commands.keys().cloned().collect()

View File

@@ -27,7 +27,6 @@ fs.workspace = true
futures.workspace = true
gpui.workspace = true
http_client.workspace = true
lazy_static.workspace = true
log.workspace = true
once_cell.workspace = true
paths.workspace = true

View File

@@ -22,7 +22,6 @@ use gpui::{
actions, AnyModel, AnyWeakModel, AppContext, AsyncAppContext, Global, Model, Task, WeakModel,
};
use http_client::{AsyncBody, HttpClient, HttpClientWithUrl};
use lazy_static::lazy_static;
use parking_lot::RwLock;
use postage::watch;
use proto::ProtoClient;
@@ -43,7 +42,7 @@ use std::{
path::PathBuf,
sync::{
atomic::{AtomicU64, Ordering},
Arc, Weak,
Arc, LazyLock, Weak,
},
time::{Duration, Instant},
};
@@ -65,27 +64,35 @@ impl fmt::Display for DevServerToken {
}
}
lazy_static! {
static ref ZED_SERVER_URL: Option<String> = std::env::var("ZED_SERVER_URL").ok();
static ref ZED_RPC_URL: Option<String> = std::env::var("ZED_RPC_URL").ok();
/// An environment variable whose presence indicates that the development auth
/// provider should be used.
///
/// Only works in development. Setting this environment variable in other release
/// channels is a no-op.
pub static ref ZED_DEVELOPMENT_AUTH: bool =
std::env::var("ZED_DEVELOPMENT_AUTH").map_or(false, |value| !value.is_empty());
pub static ref IMPERSONATE_LOGIN: Option<String> = std::env::var("ZED_IMPERSONATE")
static ZED_SERVER_URL: LazyLock<Option<String>> =
LazyLock::new(|| std::env::var("ZED_SERVER_URL").ok());
static ZED_RPC_URL: LazyLock<Option<String>> = LazyLock::new(|| std::env::var("ZED_RPC_URL").ok());
/// An environment variable whose presence indicates that the development auth
/// provider should be used.
///
/// Only works in development. Setting this environment variable in other release
/// channels is a no-op.
pub static ZED_DEVELOPMENT_AUTH: LazyLock<bool> = LazyLock::new(|| {
std::env::var("ZED_DEVELOPMENT_AUTH").map_or(false, |value| !value.is_empty())
});
pub static IMPERSONATE_LOGIN: LazyLock<Option<String>> = LazyLock::new(|| {
std::env::var("ZED_IMPERSONATE")
.ok()
.and_then(|s| if s.is_empty() { None } else { Some(s) });
pub static ref ADMIN_API_TOKEN: Option<String> = std::env::var("ZED_ADMIN_API_TOKEN")
.and_then(|s| if s.is_empty() { None } else { Some(s) })
});
pub static ADMIN_API_TOKEN: LazyLock<Option<String>> = LazyLock::new(|| {
std::env::var("ZED_ADMIN_API_TOKEN")
.ok()
.and_then(|s| if s.is_empty() { None } else { Some(s) });
pub static ref ZED_APP_PATH: Option<PathBuf> =
std::env::var("ZED_APP_PATH").ok().map(PathBuf::from);
pub static ref ZED_ALWAYS_ACTIVE: bool =
std::env::var("ZED_ALWAYS_ACTIVE").map_or(false, |e| !e.is_empty());
}
.and_then(|s| if s.is_empty() { None } else { Some(s) })
});
pub static ZED_APP_PATH: LazyLock<Option<PathBuf>> =
LazyLock::new(|| std::env::var("ZED_APP_PATH").ok().map(PathBuf::from));
pub static ZED_ALWAYS_ACTIVE: LazyLock<bool> =
LazyLock::new(|| std::env::var("ZED_ALWAYS_ACTIVE").map_or(false, |e| !e.is_empty()));
pub const INITIAL_RECONNECTION_DELAY: Duration = Duration::from_millis(500);
pub const MAX_RECONNECTION_DELAY: Duration = Duration::from_secs(10);

View File

@@ -1,5 +1,6 @@
use crate::{Client, Connection, Credentials, EstablishConnectionError, UserStore};
use anyhow::{anyhow, Result};
use chrono::Duration;
use futures::{stream::BoxStream, StreamExt};
use gpui::{BackgroundExecutor, Context, Model, TestAppContext};
use parking_lot::Mutex;
@@ -162,6 +163,11 @@ impl FakeServer {
return Ok(*message.downcast().unwrap());
}
let accepted_tos_at = chrono::Utc::now()
.checked_sub_signed(Duration::hours(5))
.expect("failed to build accepted_tos_at")
.timestamp() as u64;
if message.is::<TypedEnvelope<GetPrivateUserInfo>>() {
self.respond(
message
@@ -172,6 +178,7 @@ impl FakeServer {
metrics_id: "the-metrics-id".into(),
staff: false,
flags: Default::default(),
accepted_tos_at: Some(accepted_tos_at),
},
);
continue;

View File

@@ -1,5 +1,6 @@
use super::{proto, Client, Status, TypedEnvelope};
use anyhow::{anyhow, Context, Result};
use chrono::{DateTime, Utc};
use collections::{hash_map::Entry, HashMap, HashSet};
use feature_flags::FeatureFlagAppExt;
use futures::{channel::mpsc, Future, StreamExt};
@@ -94,6 +95,7 @@ pub struct UserStore {
update_contacts_tx: mpsc::UnboundedSender<UpdateContacts>,
current_plan: Option<proto::Plan>,
current_user: watch::Receiver<Option<Arc<User>>>,
accepted_tos_at: Option<Option<DateTime<Utc>>>,
contacts: Vec<Arc<Contact>>,
incoming_contact_requests: Vec<Arc<User>>,
outgoing_contact_requests: Vec<Arc<User>>,
@@ -150,6 +152,7 @@ impl UserStore {
by_github_login: Default::default(),
current_user: current_user_rx,
current_plan: None,
accepted_tos_at: None,
contacts: Default::default(),
incoming_contact_requests: Default::default(),
participant_indices: Default::default(),
@@ -189,9 +192,10 @@ impl UserStore {
} else {
break;
};
let fetch_metrics_id =
let fetch_private_user_info =
client.request(proto::GetPrivateUserInfo {}).log_err();
let (user, info) = futures::join!(fetch_user, fetch_metrics_id);
let (user, info) =
futures::join!(fetch_user, fetch_private_user_info);
cx.update(|cx| {
if let Some(info) = info {
@@ -202,9 +206,17 @@ impl UserStore {
client.telemetry.set_authenticated_user_info(
Some(info.metrics_id.clone()),
staff,
)
);
this.update(cx, |this, _| {
this.set_current_user_accepted_tos_at(
info.accepted_tos_at,
);
})
} else {
anyhow::Ok(())
}
})?;
})??;
current_user_tx.send(user).await.ok();
@@ -680,6 +692,39 @@ impl UserStore {
self.current_user.clone()
}
pub fn current_user_has_accepted_terms(&self) -> Option<bool> {
self.accepted_tos_at
.map(|accepted_tos_at| accepted_tos_at.is_some())
}
pub fn accept_terms_of_service(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
if self.current_user().is_none() {
return Task::ready(Err(anyhow!("no current user")));
};
let client = self.client.clone();
cx.spawn(move |this, mut cx| async move {
if let Some(client) = client.upgrade() {
let response = client
.request(proto::AcceptTermsOfService {})
.await
.context("error accepting tos")?;
this.update(&mut cx, |this, _| {
this.set_current_user_accepted_tos_at(Some(response.accepted_tos_at))
})
} else {
Err(anyhow!("client not found"))
}
})
}
fn set_current_user_accepted_tos_at(&mut self, accepted_tos_at: Option<u64>) {
self.accepted_tos_at = Some(
accepted_tos_at.and_then(|timestamp| DateTime::from_timestamp(timestamp as i64, 0)),
);
}
fn load_users(
&mut self,
request: impl RequestMessage<Response = UsersResponse>,

View File

@@ -134,6 +134,16 @@ spec:
secretKeyRef:
name: anthropic
key: api_key
- name: ANTHROPIC_STAFF_API_KEY
valueFrom:
secretKeyRef:
name: anthropic
key: staff_api_key
- name: LLM_CLOSED_BETA_MODEL_NAME
valueFrom:
secretKeyRef:
name: llm-closed-beta
key: model_name
- name: GOOGLE_AI_API_KEY
valueFrom:
secretKeyRef:
@@ -206,6 +216,12 @@ spec:
secretKeyRef:
name: supermaven
key: api_key
- name: USER_BACKFILLER_GITHUB_ACCESS_TOKEN
valueFrom:
secretKeyRef:
name: user-backfiller
key: github_access_token
optional: true
- name: INVITE_LINK_PREFIX
value: ${INVITE_LINK_PREFIX}
- name: RUST_BACKTRACE

View File

@@ -9,7 +9,9 @@ CREATE TABLE "users" (
"connected_once" BOOLEAN NOT NULL DEFAULT false,
"created_at" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
"metrics_id" TEXT,
"github_user_id" INTEGER
"github_user_id" INTEGER,
"accepted_tos_at" TIMESTAMP WITHOUT TIME ZONE,
"github_user_created_at" TIMESTAMP WITHOUT TIME ZONE
);
CREATE UNIQUE INDEX "index_users_github_login" ON "users" ("github_login");
CREATE UNIQUE INDEX "index_invite_code_users" ON "users" ("invite_code");
@@ -293,7 +295,8 @@ CREATE UNIQUE INDEX "index_channel_buffer_collaborators_on_channel_id_connection
CREATE TABLE "feature_flags" (
"id" INTEGER PRIMARY KEY AUTOINCREMENT,
"flag" TEXT NOT NULL UNIQUE
"flag" TEXT NOT NULL UNIQUE,
"enabled_for_all" BOOLEAN NOT NULL DEFAULT false
);
CREATE INDEX "index_feature_flags" ON "feature_flags" ("id");

View File

@@ -0,0 +1 @@
ALTER TABLE users ADD accepted_tos_at TIMESTAMP WITHOUT TIME ZONE;

View File

@@ -0,0 +1 @@
ALTER TABLE "users" ADD COLUMN "github_user_created_at" TIMESTAMP WITHOUT TIME ZONE;

View File

@@ -0,0 +1 @@
alter table feature_flags add column enabled_for_all boolean not null default false;

View File

@@ -0,0 +1 @@
alter table usages add column is_staff boolean not null default false;

View File

@@ -0,0 +1,9 @@
create table lifetime_usages (
id serial primary key,
user_id integer not null,
model_id integer not null references models (id) on delete cascade,
input_tokens bigint not null default 0,
output_tokens bigint not null default 0
);
create unique index uix_lifetime_usages_on_user_id_model_id on lifetime_usages (user_id, model_id);

View File

@@ -0,0 +1,7 @@
create table revoked_access_tokens (
id serial primary key,
jti text not null,
revoked_at timestamp without time zone not null default now()
);
create unique index uix_revoked_access_tokens_on_jti on revoked_access_tokens (jti);

View File

@@ -1,4 +1,4 @@
db-uri = "postgres://postgres@localhost/zed_llm"
server-port = 8082
db-uri = "postgres://postgres@localhost/zed"
server-port = 8081
jwt-secret = "the-postgrest-jwt-secret-for-authorization"
log-level = "info"

View File

@@ -1,4 +1,4 @@
db-uri = "postgres://postgres@localhost/zed"
server-port = 8081
db-uri = "postgres://postgres@localhost/zed_llm"
server-port = 8082
jwt-secret = "the-postgrest-jwt-secret-for-authorization"
log-level = "info"

View File

@@ -111,6 +111,7 @@ struct AuthenticatedUserParams {
github_user_id: Option<i32>,
github_login: String,
github_email: Option<String>,
github_user_created_at: Option<chrono::DateTime<chrono::Utc>>,
}
#[derive(Debug, Serialize)]
@@ -131,6 +132,7 @@ async fn get_authenticated_user(
&params.github_login,
params.github_user_id,
params.github_email.as_deref(),
params.github_user_created_at,
initial_channel_id,
)
.await?;

View File

@@ -115,6 +115,7 @@ async fn add_contributor(
&params.github_login,
params.github_user_id,
params.github_email.as_deref(),
params.github_user_created_at,
initial_channel_id,
)
.await

View File

@@ -1,5 +1,6 @@
use super::ips_file::IpsFile;
use crate::api::CloudflareIpCountryHeader;
use crate::clickhouse::write_to_table;
use crate::{api::slack, AppState, Error, Result};
use anyhow::{anyhow, Context};
use aws_sdk_s3::primitives::ByteStream;
@@ -529,12 +530,12 @@ struct ToUpload {
impl ToUpload {
pub async fn upload(&self, clickhouse_client: &clickhouse::Client) -> anyhow::Result<()> {
const EDITOR_EVENTS_TABLE: &str = "editor_events";
Self::upload_to_table(EDITOR_EVENTS_TABLE, &self.editor_events, clickhouse_client)
write_to_table(EDITOR_EVENTS_TABLE, &self.editor_events, clickhouse_client)
.await
.with_context(|| format!("failed to upload to table '{EDITOR_EVENTS_TABLE}'"))?;
const INLINE_COMPLETION_EVENTS_TABLE: &str = "inline_completion_events";
Self::upload_to_table(
write_to_table(
INLINE_COMPLETION_EVENTS_TABLE,
&self.inline_completion_events,
clickhouse_client,
@@ -543,7 +544,7 @@ impl ToUpload {
.with_context(|| format!("failed to upload to table '{INLINE_COMPLETION_EVENTS_TABLE}'"))?;
const ASSISTANT_EVENTS_TABLE: &str = "assistant_events";
Self::upload_to_table(
write_to_table(
ASSISTANT_EVENTS_TABLE,
&self.assistant_events,
clickhouse_client,
@@ -552,27 +553,27 @@ impl ToUpload {
.with_context(|| format!("failed to upload to table '{ASSISTANT_EVENTS_TABLE}'"))?;
const CALL_EVENTS_TABLE: &str = "call_events";
Self::upload_to_table(CALL_EVENTS_TABLE, &self.call_events, clickhouse_client)
write_to_table(CALL_EVENTS_TABLE, &self.call_events, clickhouse_client)
.await
.with_context(|| format!("failed to upload to table '{CALL_EVENTS_TABLE}'"))?;
const CPU_EVENTS_TABLE: &str = "cpu_events";
Self::upload_to_table(CPU_EVENTS_TABLE, &self.cpu_events, clickhouse_client)
write_to_table(CPU_EVENTS_TABLE, &self.cpu_events, clickhouse_client)
.await
.with_context(|| format!("failed to upload to table '{CPU_EVENTS_TABLE}'"))?;
const MEMORY_EVENTS_TABLE: &str = "memory_events";
Self::upload_to_table(MEMORY_EVENTS_TABLE, &self.memory_events, clickhouse_client)
write_to_table(MEMORY_EVENTS_TABLE, &self.memory_events, clickhouse_client)
.await
.with_context(|| format!("failed to upload to table '{MEMORY_EVENTS_TABLE}'"))?;
const APP_EVENTS_TABLE: &str = "app_events";
Self::upload_to_table(APP_EVENTS_TABLE, &self.app_events, clickhouse_client)
write_to_table(APP_EVENTS_TABLE, &self.app_events, clickhouse_client)
.await
.with_context(|| format!("failed to upload to table '{APP_EVENTS_TABLE}'"))?;
const SETTING_EVENTS_TABLE: &str = "setting_events";
Self::upload_to_table(
write_to_table(
SETTING_EVENTS_TABLE,
&self.setting_events,
clickhouse_client,
@@ -581,7 +582,7 @@ impl ToUpload {
.with_context(|| format!("failed to upload to table '{SETTING_EVENTS_TABLE}'"))?;
const EXTENSION_EVENTS_TABLE: &str = "extension_events";
Self::upload_to_table(
write_to_table(
EXTENSION_EVENTS_TABLE,
&self.extension_events,
clickhouse_client,
@@ -590,48 +591,22 @@ impl ToUpload {
.with_context(|| format!("failed to upload to table '{EXTENSION_EVENTS_TABLE}'"))?;
const EDIT_EVENTS_TABLE: &str = "edit_events";
Self::upload_to_table(EDIT_EVENTS_TABLE, &self.edit_events, clickhouse_client)
write_to_table(EDIT_EVENTS_TABLE, &self.edit_events, clickhouse_client)
.await
.with_context(|| format!("failed to upload to table '{EDIT_EVENTS_TABLE}'"))?;
const ACTION_EVENTS_TABLE: &str = "action_events";
Self::upload_to_table(ACTION_EVENTS_TABLE, &self.action_events, clickhouse_client)
write_to_table(ACTION_EVENTS_TABLE, &self.action_events, clickhouse_client)
.await
.with_context(|| format!("failed to upload to table '{ACTION_EVENTS_TABLE}'"))?;
const REPL_EVENTS_TABLE: &str = "repl_events";
Self::upload_to_table(REPL_EVENTS_TABLE, &self.repl_events, clickhouse_client)
write_to_table(REPL_EVENTS_TABLE, &self.repl_events, clickhouse_client)
.await
.with_context(|| format!("failed to upload to table '{REPL_EVENTS_TABLE}'"))?;
Ok(())
}
async fn upload_to_table<T: clickhouse::Row + Serialize + std::fmt::Debug>(
table: &str,
rows: &[T],
clickhouse_client: &clickhouse::Client,
) -> anyhow::Result<()> {
if rows.is_empty() {
return Ok(());
}
let mut insert = clickhouse_client.insert(table)?;
for event in rows {
insert.write(event).await?;
}
insert.end().await?;
let event_count = rows.len();
log::info!(
"wrote {event_count} {event_specifier} to '{table}'",
event_specifier = if event_count == 1 { "event" } else { "events" }
);
Ok(())
}
}
pub fn serialize_country_code<S>(country_code: &str, serializer: S) -> Result<S::Ok, S::Error>

View File

@@ -0,0 +1,28 @@
use serde::Serialize;
/// Writes the given rows to the specified Clickhouse table.
pub async fn write_to_table<T: clickhouse::Row + Serialize + std::fmt::Debug>(
table: &str,
rows: &[T],
clickhouse_client: &clickhouse::Client,
) -> anyhow::Result<()> {
if rows.is_empty() {
return Ok(());
}
let mut insert = clickhouse_client.insert(table)?;
for event in rows {
insert.write(event).await?;
}
insert.end().await?;
let event_count = rows.len();
log::info!(
"wrote {event_count} {event_specifier} to '{table}'",
event_specifier = if event_count == 1 { "event" } else { "events" }
);
Ok(())
}

View File

@@ -65,6 +65,7 @@ impl Database {
github_login: &str,
github_user_id: Option<i32>,
github_email: Option<&str>,
github_user_created_at: Option<DateTimeUtc>,
initial_channel_id: Option<ChannelId>,
) -> Result<()> {
self.transaction(|tx| async move {
@@ -73,6 +74,7 @@ impl Database {
github_login,
github_user_id,
github_email,
github_user_created_at.map(|time| time.naive_utc()),
initial_channel_id,
&tx,
)

View File

@@ -1,3 +1,5 @@
use chrono::NaiveDateTime;
use super::*;
impl Database {
@@ -99,6 +101,7 @@ impl Database {
github_login: &str,
github_user_id: Option<i32>,
github_email: Option<&str>,
github_user_created_at: Option<DateTimeUtc>,
initial_channel_id: Option<ChannelId>,
) -> Result<User> {
self.transaction(|tx| async move {
@@ -106,6 +109,7 @@ impl Database {
github_login,
github_user_id,
github_email,
github_user_created_at.map(|created_at| created_at.naive_utc()),
initial_channel_id,
&tx,
)
@@ -119,6 +123,7 @@ impl Database {
github_login: &str,
github_user_id: Option<i32>,
github_email: Option<&str>,
github_user_created_at: Option<NaiveDateTime>,
initial_channel_id: Option<ChannelId>,
tx: &DatabaseTransaction,
) -> Result<User> {
@@ -130,6 +135,10 @@ impl Database {
{
let mut user_by_github_user_id = user_by_github_user_id.into_active_model();
user_by_github_user_id.github_login = ActiveValue::set(github_login.into());
if github_user_created_at.is_some() {
user_by_github_user_id.github_user_created_at =
ActiveValue::set(github_user_created_at);
}
Ok(user_by_github_user_id.update(tx).await?)
} else if let Some(user_by_github_login) = user::Entity::find()
.filter(user::Column::GithubLogin.eq(github_login))
@@ -138,12 +147,17 @@ impl Database {
{
let mut user_by_github_login = user_by_github_login.into_active_model();
user_by_github_login.github_user_id = ActiveValue::set(Some(github_user_id));
if github_user_created_at.is_some() {
user_by_github_login.github_user_created_at =
ActiveValue::set(github_user_created_at);
}
Ok(user_by_github_login.update(tx).await?)
} else {
let user = user::Entity::insert(user::ActiveModel {
email_address: ActiveValue::set(github_email.map(|email| email.into())),
github_login: ActiveValue::set(github_login.into()),
github_user_id: ActiveValue::set(Some(github_user_id)),
github_user_created_at: ActiveValue::set(github_user_created_at),
admin: ActiveValue::set(false),
invite_count: ActiveValue::set(0),
invite_code: ActiveValue::set(None),
@@ -225,6 +239,26 @@ impl Database {
.await
}
/// Sets "accepted_tos_at" on the user to the given timestamp.
pub async fn set_user_accepted_tos_at(
&self,
id: UserId,
accepted_tos_at: Option<DateTime>,
) -> Result<()> {
self.transaction(|tx| async move {
user::Entity::update_many()
.filter(user::Column::Id.eq(id))
.set(user::ActiveModel {
accepted_tos_at: ActiveValue::set(accepted_tos_at),
..Default::default()
})
.exec(&*tx)
.await?;
Ok(())
})
.await
}
/// hard delete the user.
pub async fn destroy_user(&self, id: UserId) -> Result<()> {
self.transaction(|tx| async move {
@@ -278,10 +312,11 @@ impl Database {
}
/// Creates a new feature flag.
pub async fn create_user_flag(&self, flag: &str) -> Result<FlagId> {
pub async fn create_user_flag(&self, flag: &str, enabled_for_all: bool) -> Result<FlagId> {
self.transaction(|tx| async move {
let flag = feature_flag::Entity::insert(feature_flag::ActiveModel {
flag: ActiveValue::set(flag.to_string()),
enabled_for_all: ActiveValue::set(enabled_for_all),
..Default::default()
})
.exec(&*tx)
@@ -316,7 +351,15 @@ impl Database {
Flag,
}
let flags = user::Model {
let flags_enabled_for_all = feature_flag::Entity::find()
.filter(feature_flag::Column::EnabledForAll.eq(true))
.select_only()
.column(feature_flag::Column::Flag)
.into_values::<_, QueryAs>()
.all(&*tx)
.await?;
let flags_enabled_for_user = user::Model {
id: user,
..Default::default()
}
@@ -327,7 +370,20 @@ impl Database {
.all(&*tx)
.await?;
Ok(flags)
let mut all_flags = HashSet::from_iter(flags_enabled_for_all);
all_flags.extend(flags_enabled_for_user);
Ok(all_flags.into_iter().collect())
})
.await
}
pub async fn get_users_missing_github_user_created_at(&self) -> Result<Vec<user::Model>> {
self.transaction(|tx| async move {
Ok(user::Entity::find()
.filter(user::Column::GithubUserCreatedAt.is_null())
.all(&*tx)
.await?)
})
.await
}

View File

@@ -8,6 +8,7 @@ pub struct Model {
#[sea_orm(primary_key)]
pub id: FlagId,
pub flag: String,
pub enabled_for_all: bool,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]

View File

@@ -1,4 +1,5 @@
use crate::db::UserId;
use chrono::NaiveDateTime;
use sea_orm::entity::prelude::*;
use serde::Serialize;
@@ -10,6 +11,7 @@ pub struct Model {
pub id: UserId,
pub github_login: String,
pub github_user_id: Option<i32>,
pub github_user_created_at: Option<NaiveDateTime>,
pub email_address: Option<String>,
pub admin: bool,
pub invite_code: Option<String>,
@@ -17,7 +19,8 @@ pub struct Model {
pub inviter_id: Option<UserId>,
pub connected_once: bool,
pub metrics_id: Uuid,
pub created_at: DateTime,
pub created_at: NaiveDateTime,
pub accepted_tos_at: Option<NaiveDateTime>,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]

View File

@@ -10,6 +10,7 @@ mod extension_tests;
mod feature_flag_tests;
mod message_tests;
mod processed_stripe_event_tests;
mod user_tests;
use crate::migrations::run_database_migrations;

View File

@@ -1,3 +1,5 @@
use chrono::Utc;
use super::Database;
use crate::{db::NewUserParams, test_both_dbs};
use std::sync::Arc;
@@ -22,7 +24,8 @@ async fn test_contributors(db: &Arc<Database>) {
assert_eq!(db.get_contributors().await.unwrap(), Vec::<String>::new());
db.add_contributor("user1", Some(1), None, None)
let user1_created_at = Utc::now();
db.add_contributor("user1", Some(1), None, Some(user1_created_at), None)
.await
.unwrap();
assert_eq!(
@@ -30,7 +33,8 @@ async fn test_contributors(db: &Arc<Database>) {
vec!["user1".to_string()]
);
db.add_contributor("user2", Some(2), None, None)
let user2_created_at = Utc::now();
db.add_contributor("user2", Some(2), None, Some(user2_created_at), None)
.await
.unwrap();
assert_eq!(

View File

@@ -1,5 +1,6 @@
use super::*;
use crate::test_both_dbs;
use chrono::Utc;
use pretty_assertions::{assert_eq, assert_ne};
use std::sync::Arc;
@@ -100,7 +101,13 @@ async fn test_get_or_create_user_by_github_account(db: &Arc<Database>) {
.user_id;
let user = db
.get_or_create_user_by_github_account("the-new-login2", Some(102), None, None)
.get_or_create_user_by_github_account(
"the-new-login2",
Some(102),
None,
Some(Utc::now()),
None,
)
.await
.unwrap();
assert_eq!(user.id, user_id2);
@@ -108,7 +115,13 @@ async fn test_get_or_create_user_by_github_account(db: &Arc<Database>) {
assert_eq!(user.github_user_id, Some(102));
let user = db
.get_or_create_user_by_github_account("login3", Some(103), Some("user3@example.com"), None)
.get_or_create_user_by_github_account(
"login3",
Some(103),
Some("user3@example.com"),
Some(Utc::now()),
None,
)
.await
.unwrap();
assert_eq!(&user.github_login, "login3");

View File

@@ -2,6 +2,7 @@ use crate::{
db::{Database, NewUserParams},
test_both_dbs,
};
use pretty_assertions::assert_eq;
use std::sync::Arc;
test_both_dbs!(
@@ -37,22 +38,27 @@ async fn test_get_user_flags(db: &Arc<Database>) {
.unwrap()
.user_id;
const CHANNELS_ALPHA: &str = "channels-alpha";
const NEW_SEARCH: &str = "new-search";
const FEATURE_FLAG_ONE: &str = "brand-new-ux";
const FEATURE_FLAG_TWO: &str = "cool-feature";
const FEATURE_FLAG_THREE: &str = "feature-enabled-for-everyone";
let channels_flag = db.create_user_flag(CHANNELS_ALPHA).await.unwrap();
let search_flag = db.create_user_flag(NEW_SEARCH).await.unwrap();
let feature_flag_one = db.create_user_flag(FEATURE_FLAG_ONE, false).await.unwrap();
let feature_flag_two = db.create_user_flag(FEATURE_FLAG_TWO, false).await.unwrap();
db.create_user_flag(FEATURE_FLAG_THREE, true).await.unwrap();
db.add_user_flag(user_1, channels_flag).await.unwrap();
db.add_user_flag(user_1, search_flag).await.unwrap();
db.add_user_flag(user_1, feature_flag_one).await.unwrap();
db.add_user_flag(user_1, feature_flag_two).await.unwrap();
db.add_user_flag(user_2, channels_flag).await.unwrap();
db.add_user_flag(user_2, feature_flag_one).await.unwrap();
let mut user_1_flags = db.get_user_flags(user_1).await.unwrap();
user_1_flags.sort();
assert_eq!(user_1_flags, &[CHANNELS_ALPHA, NEW_SEARCH]);
assert_eq!(
user_1_flags,
&[FEATURE_FLAG_ONE, FEATURE_FLAG_TWO, FEATURE_FLAG_THREE]
);
let mut user_2_flags = db.get_user_flags(user_2).await.unwrap();
user_2_flags.sort();
assert_eq!(user_2_flags, &[CHANNELS_ALPHA]);
assert_eq!(user_2_flags, &[FEATURE_FLAG_ONE, FEATURE_FLAG_THREE]);
}

View File

@@ -0,0 +1,45 @@
use chrono::Utc;
use crate::{
db::{Database, NewUserParams},
test_both_dbs,
};
use std::sync::Arc;
test_both_dbs!(
test_accepted_tos,
test_accepted_tos_postgres,
test_accepted_tos_sqlite
);
async fn test_accepted_tos(db: &Arc<Database>) {
let user_id = db
.create_user(
"user1@example.com",
false,
NewUserParams {
github_login: "user1".to_string(),
github_user_id: 1,
},
)
.await
.unwrap()
.user_id;
let user = db.get_user_by_id(user_id).await.unwrap().unwrap();
assert!(user.accepted_tos_at.is_none());
let accepted_tos_at = Utc::now().naive_utc();
db.set_user_accepted_tos_at(user_id, Some(accepted_tos_at))
.await
.unwrap();
let user = db.get_user_by_id(user_id).await.unwrap().unwrap();
assert!(user.accepted_tos_at.is_some());
assert_eq!(user.accepted_tos_at, Some(accepted_tos_at));
db.set_user_accepted_tos_at(user_id, None).await.unwrap();
let user = db.get_user_by_id(user_id).await.unwrap().unwrap();
assert!(user.accepted_tos_at.is_none());
}

View File

@@ -32,6 +32,7 @@ fn add_vars(env_content: String, vars: &mut Vec<(String, String)>) -> Result<()>
toml::Value::String(value) => value,
toml::Value::Integer(value) => value.to_string(),
toml::Value::Float(value) => value.to_string(),
toml::Value::Boolean(value) => value.to_string(),
_ => panic!("unsupported TOML value in .env.toml for key {}", key),
};
vars.push((key, value));

View File

@@ -1,5 +1,6 @@
pub mod api;
pub mod auth;
pub mod clickhouse;
pub mod db;
pub mod env;
pub mod executor;
@@ -8,6 +9,7 @@ pub mod migrations;
mod rate_limiter;
pub mod rpc;
pub mod seed;
pub mod user_backfiller;
#[cfg(test)]
mod tests;
@@ -166,6 +168,8 @@ pub struct Config {
pub openai_api_key: Option<Arc<str>>,
pub google_ai_api_key: Option<Arc<str>>,
pub anthropic_api_key: Option<Arc<str>>,
pub anthropic_staff_api_key: Option<Arc<str>>,
pub llm_closed_beta_model_name: Option<Arc<str>>,
pub qwen2_7b_api_key: Option<Arc<str>>,
pub qwen2_7b_api_url: Option<Arc<str>>,
pub zed_client_checksum_seed: Option<String>,
@@ -174,6 +178,7 @@ pub struct Config {
pub stripe_api_key: Option<String>,
pub stripe_price_id: Option<Arc<str>>,
pub supermaven_admin_api_key: Option<Arc<str>>,
pub user_backfiller_github_access_token: Option<Arc<str>>,
}
impl Config {
@@ -216,6 +221,8 @@ impl Config {
openai_api_key: None,
google_ai_api_key: None,
anthropic_api_key: None,
anthropic_staff_api_key: None,
llm_closed_beta_model_name: None,
clickhouse_url: None,
clickhouse_user: None,
clickhouse_password: None,
@@ -230,6 +237,7 @@ impl Config {
supermaven_admin_api_key: None,
qwen2_7b_api_key: None,
qwen2_7b_api_url: None,
user_backfiller_github_access_token: None,
}
}
}
@@ -265,7 +273,7 @@ pub struct AppState {
pub stripe_client: Option<Arc<stripe::Client>>,
pub rate_limiter: Arc<RateLimiter>,
pub executor: Executor,
pub clickhouse_client: Option<clickhouse::Client>,
pub clickhouse_client: Option<::clickhouse::Client>,
pub config: Config,
}
@@ -356,8 +364,8 @@ async fn build_blob_store_client(config: &Config) -> anyhow::Result<aws_sdk_s3::
Ok(aws_sdk_s3::Client::new(&s3_config))
}
fn build_clickhouse_client(config: &Config) -> anyhow::Result<clickhouse::Client> {
Ok(clickhouse::Client::default()
fn build_clickhouse_client(config: &Config) -> anyhow::Result<::clickhouse::Client> {
Ok(::clickhouse::Client::default()
.with_url(
config
.clickhouse_url

View File

@@ -4,8 +4,8 @@ mod telemetry;
mod token;
use crate::{
api::CloudflareIpCountryHeader, build_clickhouse_client, executor::Executor, Config, Error,
Result,
api::CloudflareIpCountryHeader, build_clickhouse_client, db::UserId, executor::Executor,
Config, Error, Result,
};
use anyhow::{anyhow, Context as _};
use authorization::authorize_access_to_language_model;
@@ -18,7 +18,8 @@ use axum::{
Extension, Json, Router, TypedHeader,
};
use chrono::{DateTime, Duration, Utc};
use db::{ActiveUserCount, LlmDatabase};
use collections::HashMap;
use db::{usage_measure::UsageMeasure, ActiveUserCount, LlmDatabase};
use futures::{Stream, StreamExt as _};
use http_client::IsahcHttpClient;
use rpc::{
@@ -29,7 +30,8 @@ use std::{
sync::Arc,
task::{Context, Poll},
};
use telemetry::{report_llm_usage, LlmUsageEventRow};
use strum::IntoEnumIterator;
use telemetry::{report_llm_rate_limit, report_llm_usage, LlmRateLimitEventRow, LlmUsageEventRow};
use tokio::sync::RwLock;
use util::ResultExt;
@@ -41,7 +43,8 @@ pub struct LlmState {
pub db: Arc<LlmDatabase>,
pub http_client: IsahcHttpClient,
pub clickhouse_client: Option<clickhouse::Client>,
active_user_count: RwLock<Option<(DateTime<Utc>, ActiveUserCount)>>,
active_user_count_by_model:
RwLock<HashMap<(LanguageModelProvider, String), (DateTime<Utc>, ActiveUserCount)>>,
}
const ACTIVE_USER_COUNT_CACHE_DURATION: Duration = Duration::seconds(30);
@@ -69,9 +72,6 @@ impl LlmState {
.build()
.context("failed to construct http client")?;
let initial_active_user_count =
Some((Utc::now(), db.get_active_user_count(Utc::now()).await?));
let this = Self {
executor,
db,
@@ -80,25 +80,34 @@ impl LlmState {
.clickhouse_url
.as_ref()
.and_then(|_| build_clickhouse_client(&config).log_err()),
active_user_count: RwLock::new(initial_active_user_count),
active_user_count_by_model: RwLock::new(HashMap::default()),
config,
};
Ok(Arc::new(this))
}
pub async fn get_active_user_count(&self) -> Result<ActiveUserCount> {
pub async fn get_active_user_count(
&self,
provider: LanguageModelProvider,
model: &str,
) -> Result<ActiveUserCount> {
let now = Utc::now();
if let Some((last_updated, count)) = self.active_user_count.read().await.as_ref() {
if now - *last_updated < ACTIVE_USER_COUNT_CACHE_DURATION {
return Ok(*count);
{
let active_user_count_by_model = self.active_user_count_by_model.read().await;
if let Some((last_updated, count)) =
active_user_count_by_model.get(&(provider, model.to_string()))
{
if now - *last_updated < ACTIVE_USER_COUNT_CACHE_DURATION {
return Ok(*count);
}
}
}
let mut cache = self.active_user_count.write().await;
let new_count = self.db.get_active_user_count(now).await?;
*cache = Some((now, new_count));
let mut cache = self.active_user_count_by_model.write().await;
let new_count = self.db.get_active_user_count(provider, model, now).await?;
cache.insert((provider, model.to_string()), (now, new_count));
Ok(new_count)
}
}
@@ -131,6 +140,19 @@ async fn validate_api_token<B>(mut req: Request<B>, next: Next<B>) -> impl IntoR
let state = req.extensions().get::<Arc<LlmState>>().unwrap();
match LlmTokenClaims::validate(&token, &state.config) {
Ok(claims) => {
if state.db.is_access_token_revoked(&claims.jti).await? {
return Err(Error::http(
StatusCode::UNAUTHORIZED,
"unauthorized".to_string(),
));
}
tracing::Span::current()
.record("user_id", claims.user_id)
.record("login", claims.github_user_login.clone())
.record("authn.jti", &claims.jti)
.record("is_staff", &claims.is_staff);
req.extensions_mut().insert(claims);
Ok::<_, Error>(next.run(req).await.into_response())
}
@@ -157,7 +179,10 @@ async fn perform_completion(
country_code_header: Option<TypedHeader<CloudflareIpCountryHeader>>,
Json(params): Json<PerformCompletionParams>,
) -> Result<impl IntoResponse> {
let model = normalize_model_name(params.provider, params.model);
let model = normalize_model_name(
state.db.model_names_for_provider(params.provider),
params.model,
);
authorize_access_to_language_model(
&state.config,
@@ -171,33 +196,77 @@ async fn perform_completion(
let stream = match params.provider {
LanguageModelProvider::Anthropic => {
let api_key = state
.config
.anthropic_api_key
.as_ref()
.context("no Anthropic AI API key configured on the server")?;
let api_key = if claims.is_staff {
state
.config
.anthropic_staff_api_key
.as_ref()
.context("no Anthropic AI staff API key configured on the server")?
} else {
state
.config
.anthropic_api_key
.as_ref()
.context("no Anthropic AI API key configured on the server")?
};
let mut request: anthropic::Request =
serde_json::from_str(&params.provider_request.get())?;
// Parse the model, throw away the version that was included, and then set a specific
// version that we control on the server.
// Override the model on the request with the latest version of the model that is
// known to the server.
//
// Right now, we use the version that's defined in `model.id()`, but we will likely
// want to change this code once a new version of an Anthropic model is released,
// so that users can use the new version, without having to update Zed.
request.model = match anthropic::Model::from_id(&request.model) {
Ok(model) => model.id().to_string(),
Err(_) => request.model,
request.model = match model.as_str() {
"claude-3-5-sonnet" => anthropic::Model::Claude3_5Sonnet.id().to_string(),
"claude-3-opus" => anthropic::Model::Claude3Opus.id().to_string(),
"claude-3-haiku" => anthropic::Model::Claude3Haiku.id().to_string(),
"claude-3-sonnet" => anthropic::Model::Claude3Sonnet.id().to_string(),
_ => request.model,
};
let chunks = anthropic::stream_completion(
let (chunks, rate_limit_info) = anthropic::stream_completion_with_rate_limit_info(
&state.http_client,
anthropic::ANTHROPIC_API_URL,
api_key,
request,
None,
)
.await?;
.await
.map_err(|err| match err {
anthropic::AnthropicError::ApiError(ref api_error) => match api_error.code() {
Some(anthropic::ApiErrorCode::RateLimitError) => Error::http(
StatusCode::TOO_MANY_REQUESTS,
"Upstream Anthropic rate limit exceeded.".to_string(),
),
Some(anthropic::ApiErrorCode::InvalidRequestError) => {
Error::http(StatusCode::BAD_REQUEST, api_error.message.clone())
}
Some(anthropic::ApiErrorCode::OverloadedError) => {
Error::http(StatusCode::SERVICE_UNAVAILABLE, api_error.message.clone())
}
Some(_) => {
Error::http(StatusCode::INTERNAL_SERVER_ERROR, api_error.message.clone())
}
None => Error::Internal(anyhow!(err)),
},
anthropic::AnthropicError::Other(err) => Error::Internal(err),
})?;
if let Some(rate_limit_info) = rate_limit_info {
tracing::info!(
target: "upstream rate limit",
is_staff = claims.is_staff,
provider = params.provider.to_string(),
model = model,
tokens_remaining = rate_limit_info.tokens_remaining,
requests_remaining = rate_limit_info.requests_remaining,
requests_reset = ?rate_limit_info.requests_reset,
tokens_reset = ?rate_limit_info.tokens_reset,
);
}
chunks
.map(move |event| {
@@ -330,31 +399,13 @@ async fn perform_completion(
})))
}
fn normalize_model_name(provider: LanguageModelProvider, name: String) -> String {
let prefixes: &[_] = match provider {
LanguageModelProvider::Anthropic => &[
"claude-3-5-sonnet",
"claude-3-haiku",
"claude-3-opus",
"claude-3-sonnet",
],
LanguageModelProvider::OpenAi => &[
"gpt-3.5-turbo",
"gpt-4-turbo-preview",
"gpt-4o-mini",
"gpt-4o",
"gpt-4",
],
LanguageModelProvider::Google => &[],
LanguageModelProvider::Zed => &[],
};
if let Some(prefix) = prefixes
fn normalize_model_name(known_models: Vec<String>, name: String) -> String {
if let Some(known_model_name) = known_models
.iter()
.filter(|&&prefix| name.starts_with(prefix))
.max_by_key(|&&prefix| prefix.len())
.filter(|known_model_name| name.starts_with(known_model_name.as_str()))
.max_by_key(|known_model_name| known_model_name.len())
{
prefix.to_string()
known_model_name.to_string()
} else {
name
}
@@ -369,43 +420,103 @@ async fn check_usage_limit(
let model = state.db.model(provider, model_name)?;
let usage = state
.db
.get_usage(claims.user_id as i32, provider, model_name, Utc::now())
.get_usage(
UserId::from_proto(claims.user_id),
provider,
model_name,
Utc::now(),
)
.await?;
let active_users = state.get_active_user_count().await?;
let active_users = state.get_active_user_count(provider, model_name).await?;
let users_in_recent_minutes = active_users.users_in_recent_minutes.max(1);
let users_in_recent_days = active_users.users_in_recent_days.max(1);
let per_user_max_requests_per_minute =
model.max_requests_per_minute as usize / active_users.users_in_recent_minutes.max(1);
model.max_requests_per_minute as usize / users_in_recent_minutes;
let per_user_max_tokens_per_minute =
model.max_tokens_per_minute as usize / active_users.users_in_recent_minutes.max(1);
let per_user_max_tokens_per_day =
model.max_tokens_per_day as usize / active_users.users_in_recent_days.max(1);
model.max_tokens_per_minute as usize / users_in_recent_minutes;
let per_user_max_tokens_per_day = model.max_tokens_per_day as usize / users_in_recent_days;
let checks = [
(
usage.requests_this_minute,
per_user_max_requests_per_minute,
"requests per minute",
UsageMeasure::RequestsPerMinute,
),
(
usage.tokens_this_minute,
per_user_max_tokens_per_minute,
"tokens per minute",
UsageMeasure::TokensPerMinute,
),
(
usage.tokens_this_day,
per_user_max_tokens_per_day,
"tokens per day",
UsageMeasure::TokensPerDay,
),
];
for (usage, limit, resource) in checks {
for (used, limit, usage_measure) in checks {
// Temporarily bypass rate-limiting for staff members.
if claims.is_staff {
continue;
}
if usage > limit {
if used > limit {
let resource = match usage_measure {
UsageMeasure::RequestsPerMinute => "requests_per_minute",
UsageMeasure::TokensPerMinute => "tokens_per_minute",
UsageMeasure::TokensPerDay => "tokens_per_day",
_ => "",
};
if let Some(client) = state.clickhouse_client.as_ref() {
tracing::info!(
target: "user rate limit",
user_id = claims.user_id,
login = claims.github_user_login,
authn.jti = claims.jti,
is_staff = claims.is_staff,
provider = provider.to_string(),
model = model.name,
requests_this_minute = usage.requests_this_minute,
tokens_this_minute = usage.tokens_this_minute,
tokens_this_day = usage.tokens_this_day,
users_in_recent_minutes = users_in_recent_minutes,
users_in_recent_days = users_in_recent_days,
max_requests_per_minute = per_user_max_requests_per_minute,
max_tokens_per_minute = per_user_max_tokens_per_minute,
max_tokens_per_day = per_user_max_tokens_per_day,
);
report_llm_rate_limit(
client,
LlmRateLimitEventRow {
time: Utc::now().timestamp_millis(),
user_id: claims.user_id as i32,
is_staff: claims.is_staff,
plan: match claims.plan {
Plan::Free => "free".to_string(),
Plan::ZedPro => "zed_pro".to_string(),
},
model: model.name.clone(),
provider: provider.to_string(),
usage_measure: resource.to_string(),
requests_this_minute: usage.requests_this_minute as u64,
tokens_this_minute: usage.tokens_this_minute as u64,
tokens_this_day: usage.tokens_this_day as u64,
users_in_recent_minutes: users_in_recent_minutes as u64,
users_in_recent_days: users_in_recent_days as u64,
max_requests_per_minute: per_user_max_requests_per_minute as u64,
max_tokens_per_minute: per_user_max_tokens_per_minute as u64,
max_tokens_per_day: per_user_max_tokens_per_day as u64,
},
)
.await
.log_err();
}
return Err(Error::http(
StatusCode::TOO_MANY_REQUESTS,
format!("Rate limit exceeded. Maximum {} reached.", resource),
@@ -459,7 +570,8 @@ impl<S> Drop for TokenCountingStream<S> {
let usage = state
.db
.record_usage(
claims.user_id as i32,
UserId::from_proto(claims.user_id),
claims.is_staff,
provider,
&model,
input_token_count,
@@ -469,32 +581,91 @@ impl<S> Drop for TokenCountingStream<S> {
.await
.log_err();
if let Some((clickhouse_client, usage)) = state.clickhouse_client.as_ref().zip(usage) {
report_llm_usage(
clickhouse_client,
LlmUsageEventRow {
time: Utc::now().timestamp_millis(),
user_id: claims.user_id as i32,
is_staff: claims.is_staff,
plan: match claims.plan {
Plan::Free => "free".to_string(),
Plan::ZedPro => "zed_pro".to_string(),
if let Some(usage) = usage {
tracing::info!(
target: "user usage",
user_id = claims.user_id,
login = claims.github_user_login,
authn.jti = claims.jti,
is_staff = claims.is_staff,
requests_this_minute = usage.requests_this_minute,
tokens_this_minute = usage.tokens_this_minute,
);
if let Some(clickhouse_client) = state.clickhouse_client.as_ref() {
report_llm_usage(
clickhouse_client,
LlmUsageEventRow {
time: Utc::now().timestamp_millis(),
user_id: claims.user_id as i32,
is_staff: claims.is_staff,
plan: match claims.plan {
Plan::Free => "free".to_string(),
Plan::ZedPro => "zed_pro".to_string(),
},
model,
provider: provider.to_string(),
input_token_count: input_token_count as u64,
output_token_count: output_token_count as u64,
requests_this_minute: usage.requests_this_minute as u64,
tokens_this_minute: usage.tokens_this_minute as u64,
tokens_this_day: usage.tokens_this_day as u64,
input_tokens_this_month: usage.input_tokens_this_month as u64,
output_tokens_this_month: usage.output_tokens_this_month as u64,
spending_this_month: usage.spending_this_month as u64,
lifetime_spending: usage.lifetime_spending as u64,
},
model,
provider: provider.to_string(),
input_token_count: input_token_count as u64,
output_token_count: output_token_count as u64,
requests_this_minute: usage.requests_this_minute as u64,
tokens_this_minute: usage.tokens_this_minute as u64,
tokens_this_day: usage.tokens_this_day as u64,
input_tokens_this_month: usage.input_tokens_this_month as u64,
output_tokens_this_month: usage.output_tokens_this_month as u64,
spending_this_month: usage.spending_this_month as u64,
},
)
.await
.log_err();
)
.await
.log_err();
}
}
})
}
}
pub fn log_usage_periodically(state: Arc<LlmState>) {
state.executor.clone().spawn_detached(async move {
loop {
state
.executor
.sleep(std::time::Duration::from_secs(30))
.await;
for provider in LanguageModelProvider::iter() {
for model in state.db.model_names_for_provider(provider) {
if let Some(active_user_count) = state
.get_active_user_count(provider, &model)
.await
.log_err()
{
tracing::info!(
target: "active user counts",
provider = provider.to_string(),
model = model,
users_in_recent_minutes = active_user_count.users_in_recent_minutes,
users_in_recent_days = active_user_count.users_in_recent_days,
);
}
}
}
if let Some(usages) = state
.db
.get_application_wide_usages_by_model(Utc::now())
.await
.log_err()
{
for usage in usages {
tracing::info!(
target: "computed usage",
provider = usage.provider.to_string(),
model = usage.model,
requests_this_minute = usage.requests_this_minute,
tokens_this_minute = usage.tokens_this_minute,
);
}
}
}
})
}

View File

@@ -12,11 +12,12 @@ pub fn authorize_access_to_language_model(
model: &str,
) -> Result<()> {
authorize_access_for_country(config, country_code, provider)?;
authorize_access_to_model(claims, provider, model)?;
authorize_access_to_model(config, claims, provider, model)?;
Ok(())
}
fn authorize_access_to_model(
config: &Config,
claims: &LlmTokenClaims,
provider: LanguageModelProvider,
model: &str,
@@ -25,15 +26,25 @@ fn authorize_access_to_model(
return Ok(());
}
match (provider, model) {
(LanguageModelProvider::Anthropic, model) if model.starts_with("claude-3.5-sonnet") => {
Ok(())
match provider {
LanguageModelProvider::Anthropic => {
if model == "claude-3-5-sonnet" {
return Ok(());
}
if claims.has_llm_closed_beta_feature_flag
&& Some(model) == config.llm_closed_beta_model_name.as_deref()
{
return Ok(());
}
}
_ => Err(Error::http(
StatusCode::FORBIDDEN,
format!("access to model {model:?} is not included in your plan"),
))?,
_ => {}
}
Err(Error::http(
StatusCode::FORBIDDEN,
format!("access to model {model:?} is not included in your plan"),
))
}
fn authorize_access_for_country(
@@ -240,14 +251,14 @@ mod tests {
(
Plan::ZedPro,
LanguageModelProvider::Anthropic,
"claude-3.5-sonnet",
"claude-3-5-sonnet",
true,
),
// Free plan should have access to claude-3.5-sonnet
(
Plan::Free,
LanguageModelProvider::Anthropic,
"claude-3.5-sonnet",
"claude-3-5-sonnet",
true,
),
// Pro plan should NOT have access to other Anthropic models
@@ -303,7 +314,7 @@ mod tests {
// Staff should have access to all models
let test_cases = vec![
(LanguageModelProvider::Anthropic, "claude-3.5-sonnet"),
(LanguageModelProvider::Anthropic, "claude-3-5-sonnet"),
(LanguageModelProvider::Anthropic, "claude-2"),
(LanguageModelProvider::Anthropic, "claude-123-agi"),
(LanguageModelProvider::OpenAi, "gpt-4"),

View File

@@ -67,6 +67,21 @@ impl LlmDatabase {
Ok(())
}
/// Returns the names of the known models for the given [`LanguageModelProvider`].
pub fn model_names_for_provider(&self, provider: LanguageModelProvider) -> Vec<String> {
self.models
.keys()
.filter_map(|(model_provider, model_name)| {
if model_provider == &provider {
Some(model_name)
} else {
None
}
})
.cloned()
.collect::<Vec<_>>()
}
pub fn model(&self, provider: LanguageModelProvider, name: &str) -> Result<&model::Model> {
Ok(self
.models

View File

@@ -7,3 +7,4 @@ id_type!(ModelId);
id_type!(ProviderId);
id_type!(UsageId);
id_type!(UsageMeasureId);
id_type!(RevokedAccessTokenId);

View File

@@ -1,4 +1,5 @@
use super::*;
pub mod providers;
pub mod revoked_access_tokens;
pub mod usages;

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