Compare commits

...

224 Commits

Author SHA1 Message Date
Richard Feldman
7cf2265a97 Combine duplicated code in both if and else branches 2025-03-27 09:43:24 -04:00
Richard Feldman
88e6a957ef Handle 502 errors from Anthropic 2025-03-27 07:30:36 -04:00
renovate[bot]
71ddb3dad4 Update Rust crate log to v0.4.27 (#27540)
This PR contains the following updates:

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

---

### Release Notes

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

###
[`v0.4.27`](https://redirect.github.com/rust-lang/log/blob/HEAD/CHANGELOG.md#0427---2025-03-24)

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

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

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Kirill Bulatov <kirill@zed.dev>
2025-03-27 10:10:54 +00:00
renovate[bot]
2bf9c472bd Update Rust crate oo7 to v0.4.3 (#27549)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [oo7](https://redirect.github.com/bilelmoussaoui/oo7) | dependencies |
patch | `0.4.1` -> `0.4.3` |

---

### Release Notes

<details>
<summary>bilelmoussaoui/oo7 (oo7)</summary>

###
[`v0.4.3`](https://redirect.github.com/bilelmoussaoui/oo7/releases/tag/0.4.3)

[Compare
Source](https://redirect.github.com/bilelmoussaoui/oo7/compare/0.4.2...0.4.3)

- [cli: Add custom keyring file
support](248d96f225)
- [dbus/secret: Workaround gnome-keyring being non-spec
compliant](cac94b502f)
- [client: Deprecate
Keyring::with_broken_item_cleanup](cce024ba64)
- [cli: Add a new argument for listing all
items](88f5604147)

###
[`v0.4.2`](https://redirect.github.com/bilelmoussaoui/oo7/releases/tag/0.4.2)

#### What's Changed

Relax the condition when validating the file backend keyring secret and
provide various APIs to allow the developer to recover the non-broken
items in a keyring or delete the broken ones.

The root cause of the issue has not been identified but at least apps
will continue to work as normal if the developer uses
`Keyring::with_broken_item_cleanup` instead of `Keyring::new`.

More details can be found in
[#&#8203;207](https://redirect.github.com/bilelmoussaoui/oo7/issues/207)

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

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-27 09:54:09 +00:00
Antonio Scandurra
82a06f0ca9 Introduce primitives in GitStore to support reviewing assistant diffs (#27576)
Release Notes:

- N/A
2025-03-27 09:46:31 +00:00
renovate[bot]
cd6b1d32d0 Update Rust crate blake3 to v1.7.0 (#27566)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [blake3](https://redirect.github.com/BLAKE3-team/BLAKE3) |
workspace.dependencies | minor | `1.6.1` -> `1.7.0` |

---

### Release Notes

<details>
<summary>BLAKE3-team/BLAKE3 (blake3)</summary>

###
[`v1.7.0`](https://redirect.github.com/BLAKE3-team/BLAKE3/releases/tag/1.7.0)

[Compare
Source](https://redirect.github.com/BLAKE3-team/BLAKE3/compare/1.6.1...1.7.0)

version 1.7.0

Changes since 1.6.1:

-   The C implementation has gained multithreading support, based on
    Intel's oneTBB library. This works similarly to the Rayon-based
    multithreading used in the Rust implementation. See c/README.md for
details. Contributed by
[@&#8203;silvanshade](https://redirect.github.com/silvanshade)
([#&#8203;445](https://redirect.github.com/BLAKE3-team/BLAKE3/issues/445)).
-   The Rust implementation has gained a WASM SIMD backend, gated by the
`wasm32_simd` Cargo feature. Under Wasmtime on my laptop, this is a 6x
    performance improvement for large inputs. This backend is currently
Rust-only. Contributed by
[@&#8203;monoid](https://redirect.github.com/monoid)
([#&#8203;341](https://redirect.github.com/BLAKE3-team/BLAKE3/issues/341)).
-   Fixed cross-compilation builds targeting Windows with cargo-xwin.
Contributed by [@&#8203;Sporif](https://redirect.github.com/Sporif) and
[@&#8203;toothbrush7777777](https://redirect.github.com/toothbrush7777777)
([#&#8203;230](https://redirect.github.com/BLAKE3-team/BLAKE3/issues/230)).
-   Added `b3sum --tag`, which changes the output format. This is for
    compatibility with GNU checksum tools (which use the same flag) and
    BSD checksum tools (which use the output format this flag turns on).
Contributed by
[@&#8203;leahneukirchen](https://redirect.github.com/leahneukirchen)
([#&#8203;453](https://redirect.github.com/BLAKE3-team/BLAKE3/issues/453))
and [@&#8203;dbohdan](https://redirect.github.com/dbohdan)
([#&#8203;430](https://redirect.github.com/BLAKE3-team/BLAKE3/issues/430)).

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

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-27 11:37:20 +02:00
renovate[bot]
5033a2aba0 Update Rust crate image to v0.25.6 (#27539)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [image](https://redirect.github.com/image-rs/image) | dependencies |
patch | `0.25.5` -> `0.25.6` |
| [image](https://redirect.github.com/image-rs/image) |
workspace.dependencies | patch | `0.25.5` -> `0.25.6` |

---

### Release Notes

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

###
[`v0.25.6`](https://redirect.github.com/image-rs/image/blob/HEAD/CHANGES.md#Version-0256)

[Compare
Source](https://redirect.github.com/image-rs/image/compare/v0.25.5...v0.25.6)

Features:

- Improved format detection
([#&#8203;2418](https://redirect.github.com/image-rs/image/pull/2418))
- Implement writing ICC profiles for JPEG and PNG images
([#&#8203;2389](https://redirect.github.com/image-rs/image/pull/2389))

Bug fixes:

- JPEG encoding bugfix
([#&#8203;2387](https://redirect.github.com/image-rs/image/pull/2387))
- Expanded ICO format detection
([#&#8203;2434](https://redirect.github.com/image-rs/image/pull/2434))
- Fixed EXR bug with NaNs
([#&#8203;2381](https://redirect.github.com/image-rs/image/pull/2381))
-   Various 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 these
updates again.

---

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

---

Release Notes:

- N/A

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzOS4yMDcuMSIsInVwZGF0ZWRJblZlciI6IjM5LjIwNy4xIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6W119-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-27 11:36:33 +02:00
renovate[bot]
0392ef10cf Update Rust crate bitflags to v2.9.0 (#27565)
This PR contains the following updates:

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

---

### Release Notes

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

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

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

#### What's Changed

- `Flags` trait: add `clear(&mut self)` method by
[@&#8203;wysiwys](https://redirect.github.com/wysiwys) in
[https://github.com/bitflags/bitflags/pull/437](https://redirect.github.com/bitflags/bitflags/pull/437)
- Fix up UI tests by
[@&#8203;KodrAus](https://redirect.github.com/KodrAus) in
[https://github.com/bitflags/bitflags/pull/438](https://redirect.github.com/bitflags/bitflags/pull/438)

**Full Changelog**:
https://github.com/bitflags/bitflags/compare/2.8.0...2.9.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:eyJjcmVhdGVkSW5WZXIiOiIzOS4yMDcuMSIsInVwZGF0ZWRJblZlciI6IjM5LjIwNy4xIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6W119-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-27 11:36:02 +02:00
Antonio Scandurra
7354ef91e1 Make GitRepository::status async and remove cx parameter (#27514)
This lays the groundwork for using `status` as part of the new agent
panel.

Release Notes:

- N/A
2025-03-27 09:05:54 +00:00
张小白
926d10cc45 Move the EventKind::Access filtering before the loop starts (#27569)
Follow up #27498

Release Notes:

- N/A
2025-03-27 15:15:24 +08:00
renovate[bot]
a7697be857 Update Rust crate async-compression to v0.4.22 (#27529)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
|
[async-compression](https://redirect.github.com/Nullus157/async-compression)
| workspace.dependencies | patch | `0.4.21` -> `0.4.22` |

---

### Release Notes

<details>
<summary>Nullus157/async-compression (async-compression)</summary>

###
[`v0.4.22`](https://redirect.github.com/Nullus157/async-compression/blob/HEAD/CHANGELOG.md#0422---2025-03-25)

[Compare
Source](https://redirect.github.com/Nullus157/async-compression/compare/v0.4.21...v0.4.22)

##### Other

-   Add lz4 encoder/decoder
-   Expose total_in/total_out in DeflateEncoder

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

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-27 00:00:41 -04:00
renovate[bot]
97392a23e3 Update Rust crate time to v0.3.41 (#27553)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [time](https://time-rs.github.io)
([source](https://redirect.github.com/time-rs/time)) |
workspace.dependencies | patch | `0.3.40` -> `0.3.41` |

---

### Release Notes

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

###
[`v0.3.41`](https://redirect.github.com/time-rs/time/blob/HEAD/CHANGELOG.md#0341-2025-03-23)

[Compare
Source](https://redirect.github.com/time-rs/time/compare/v0.3.40...v0.3.41)

##### Fixed

- Compatibility with the latest release of `deranged`. This fix is
permanent and covers future
    similar changes upstream.

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

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-26 22:55:45 -04:00
renovate[bot]
3f40e0f433 Update Rust crate clap to v4.5.34 (#27530)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [clap](https://redirect.github.com/clap-rs/clap) |
workspace.dependencies | patch | `4.5.32` -> `4.5.34` |

---

### Release Notes

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

###
[`v4.5.34`](https://redirect.github.com/clap-rs/clap/blob/HEAD/CHANGELOG.md#4534---2025-03-27)

[Compare
Source](https://redirect.github.com/clap-rs/clap/compare/v4.5.33...v4.5.34)

##### Fixes

- *(help)* Don't add extra blank lines with `flatten_help(true)` and
subcommands without arguments

###
[`v4.5.33`](https://redirect.github.com/clap-rs/clap/blob/HEAD/CHANGELOG.md#4533---2025-03-26)

[Compare
Source](https://redirect.github.com/clap-rs/clap/compare/v4.5.32...v4.5.33)

##### Fixes

- *(error)* When showing the usage of a suggestion for an unknown
argument, don't show the group

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

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-26 22:55:18 -04:00
renovate[bot]
3e6d5c0814 Update dependency @tsconfig/node20 to v20.1.5 (#27560)
This PR contains the following updates:

| Package | Change | Age | Adoption | Passing | Confidence |
|---|---|---|---|---|---|
| [@tsconfig/node20](https://redirect.github.com/tsconfig/bases)
([source](https://redirect.github.com/tsconfig/bases/tree/HEAD/bases)) |
[`20.1.4` ->
`20.1.5`](https://renovatebot.com/diffs/npm/@tsconfig%2fnode20/20.1.4/20.1.5)
|
[![age](https://developer.mend.io/api/mc/badges/age/npm/@tsconfig%2fnode20/20.1.5?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/@tsconfig%2fnode20/20.1.5?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/@tsconfig%2fnode20/20.1.4/20.1.5?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@tsconfig%2fnode20/20.1.4/20.1.5?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|

---

### Release Notes

<details>
<summary>tsconfig/bases (@&#8203;tsconfig/node20)</summary>

###
[`v20.1.5`](be6b3bb160...f6e0345911)

[Compare
Source](be6b3bb160...f6e0345911)

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

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-26 22:54:49 -04:00
renovate[bot]
2bc91e8c59 Update Rust crate plist to v1.7.1 (#27550)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [plist](https://redirect.github.com/ebarnard/rust-plist) |
dependencies | patch | `1.7.0` -> `1.7.1` |

---

### Release Notes

<details>
<summary>ebarnard/rust-plist (plist)</summary>

###
[`v1.7.1`](https://redirect.github.com/ebarnard/rust-plist/compare/v1.7.0...v1.7.1)

[Compare
Source](https://redirect.github.com/ebarnard/rust-plist/compare/v1.7.0...v1.7.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:eyJjcmVhdGVkSW5WZXIiOiIzOS4yMDcuMSIsInVwZGF0ZWRJblZlciI6IjM5LjIwNy4xIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6W119-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-26 22:51:58 -04:00
renovate[bot]
bbc80c78fd Update dependency @slack/webhook to v7.0.5 (#27554)
This PR contains the following updates:

| Package | Change | Age | Adoption | Passing | Confidence |
|---|---|---|---|---|---|
| [@slack/webhook](https://tools.slack.dev/node-slack-sdk/webhook)
([source](https://redirect.github.com/slackapi/node-slack-sdk)) |
[`7.0.4` ->
`7.0.5`](https://renovatebot.com/diffs/npm/@slack%2fwebhook/7.0.4/7.0.5)
|
[![age](https://developer.mend.io/api/mc/badges/age/npm/@slack%2fwebhook/7.0.5?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/@slack%2fwebhook/7.0.5?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/@slack%2fwebhook/7.0.4/7.0.5?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@slack%2fwebhook/7.0.4/7.0.5?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|

---

### Release Notes

<details>
<summary>slackapi/node-slack-sdk (@&#8203;slack/webhook)</summary>

###
[`v7.0.5`](https://redirect.github.com/slackapi/node-slack-sdk/releases/tag/%40slack/webhook%407.0.5)

[Compare
Source](https://redirect.github.com/slackapi/node-slack-sdk/compare/@slack/webhook@7.0.4...@slack/webhook@7.0.5)

#### What's Changed

This patch release updates the `axios` dependency used to send webhooks
with internal bug fixes.

- fix(webhook): bump axios to 1.8.3 to address CVE-2025-27152 by
[@&#8203;zimeg](https://redirect.github.com/zimeg) in
[https://github.com/slackapi/node-slack-sdk/pull/2173](https://redirect.github.com/slackapi/node-slack-sdk/pull/2173)

**Full Changelog**:
https://github.com/slackapi/node-slack-sdk/compare/[@&#8203;slack/webhook](https://redirect.github.com/slack/webhook)[@&#8203;7](https://redirect.github.com/7).0.4..[@&#8203;slack/webhook](https://redirect.github.com/slack/webhook)[@&#8203;7](https://redirect.github.com/7).0.5
**Milestone**: https://github.com/slackapi/node-slack-sdk/milestone/130

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

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-26 22:50:40 -04:00
renovate[bot]
24ab5afa10 Update serde monorepo to v1.0.219 (#27561)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [serde](https://serde.rs)
([source](https://redirect.github.com/serde-rs/serde)) | dependencies |
patch | `1.0.218` -> `1.0.219` |
| [serde](https://serde.rs)
([source](https://redirect.github.com/serde-rs/serde)) |
workspace.dependencies | patch | `1.0.218` -> `1.0.219` |
| [serde_derive](https://serde.rs)
([source](https://redirect.github.com/serde-rs/serde)) |
workspace.dependencies | patch | `1.0.218` -> `1.0.219` |

---

### Release Notes

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

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

[Compare
Source](https://redirect.github.com/serde-rs/serde/compare/v1.0.218...v1.0.219)

- Prevent `absolute_paths` Clippy restriction being triggered inside
macro-generated code
([#&#8203;2906](https://redirect.github.com/serde-rs/serde/issues/2906),
thanks [@&#8203;davidzeng0](https://redirect.github.com/davidzeng0))

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

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-26 22:48:53 -04:00
Marshall Bowers
af8acba353 Remove unneeded inline tables in Cargo.tomls (#27563)
This PR removes some unneeded inline tables from our `Cargo.toml`s.

Release Notes:

- N/A
2025-03-27 02:36:47 +00:00
Marshall Bowers
231e9c2000 assistant2: Add ability to configure tools for profiles in the UI (#27562)
This PR adds the ability to configure tools for a profile in the UI:


https://github.com/user-attachments/assets/16642f14-8faa-4a91-bb9e-1d480692f1f2

Note: Doesn't yet work for customizing tools for the default profiles.

Release Notes:

- N/A
2025-03-27 02:19:45 +00:00
João Marcos
47b94e5ef0 Git: Fix hunks being skipped when staging too quickly (#27552)
Release Notes:

- Git: Fix hunks being skipped when staging too quickly.

---------

Co-authored-by: Max Brunsfeld <maxbrunsfeld@gmail.com>
2025-03-26 23:22:37 +00:00
Cole Miller
29e2e13e6d Fix broken merge (#27551)
This PR fixes main after a semantic merge conflict with
https://github.com/zed-industries/zed/pull/27391.

Release Notes:

- N/A
2025-03-26 23:00:08 +00:00
João Marcos
e635798fe0 Fix crash when staging a hunk that overlaps multiple unstaged hunks (#27545)
Release Notes:

- Git: Fix crash when staging a hunk that overlaps multiple unstaged
hunks.

---------

Co-authored-by: Max Brunsfeld <maxbrunsfeld@gmail.com>
2025-03-26 19:54:51 -03:00
Cole Miller
6924720b35 Move repository state RPC handlers to the GitStore (#27391)
This is another in the series of PRs to make the GitStore own all
repository state and enable better concurrency control for git
repository scans.

After this PR, the `RepositoryEntry`s stored in worktree snapshots are
used only as a staging ground for local GitStores to pull from after
git-related events; non-local worktrees don't store them at all,
although this is not reflected in the types. GitTraversal and other
places that need information about repositories get it from the
GitStore. The GitStore also takes over handling of the new
UpdateRepository and RemoveRepository messages. However, repositories
are still discovered and scanned on a per-worktree basis, and we're
still identifying them by the (worktree-specific) project entry ID of
their working directory.

- [x] Remove WorkDirectory from RepositoryEntry
- [x] Remove worktree IDs from repository-related RPC messages
- [x] Handle UpdateRepository and RemoveRepository RPCs from the
GitStore

Release Notes:

- N/A

---------

Co-authored-by: Max <max@zed.dev>
Co-authored-by: Max Brunsfeld <maxbrunsfeld@gmail.com>
2025-03-26 18:23:44 -04:00
Thomas Mickley-Doyle
1e8b50f471 Add token usage to LanguageModelTextStream (#27490)
Release Notes:

- N/A

---------

Co-authored-by: Michael Sloan <michael@zed.dev>
2025-03-26 22:21:01 +00:00
Anthony Eid
5f8c53ffe8 Debugger UI: Fix breakpoint rendering in git hunks (#27538)
This PR fixes a bug where breakpoints would be rendered on incorrect
lines when openings a git hunk that contained breakpoints. This also
disables breakpoints from being shown in deleted git hunks as well.

Note: There's some unexpected behavior when using an anchor to get a
display point that is in an open git hunk, where the
`anchor.to_point().col == 0`.

```rust
                let position = multi_buffer_anchor
                    .to_point(&multi_buffer_snapshot)
                    .to_display_point(&snapshot);
```

The above code will return a display point that is one line below where
the anchor actually represents when it's in an opened hunk diff. Which
causes the bug shown below



https://github.com/user-attachments/assets/bd15d02a-3cdc-4c8e-841f-bef238583351


@ConradIrwin Is this expected behavior when calling
`.to_display_point(&snapshot)`?

Release Notes:

- N/A
2025-03-26 18:12:00 -04:00
Smit Barmase
6e82bbf367 Revert "editor: Do not use hide_mouse_while_typing for single line editor" (#27547)
Reverts zed-industries/zed#27536

Looks like hiding cursor on single editor is okay and is default
behavior for other apps.
2025-03-26 22:02:09 +00:00
Marshall Bowers
0ac717c3a8 assistant2: Start on modal for managing profiles (#27546)
This PR starts work on a modal for managing profiles.

Release Notes:

- N/A
2025-03-26 18:01:34 -04:00
Michael Sloan
44aff7cd46 Fix tools' ui_text to use inline code escaping (#27543)
Markdown escaping was added in #27502.

Release Notes:

- N/A
2025-03-26 21:49:51 +00:00
Bennet Bo Fenner
2b5095ac91 assistant2: Fix filtering issue when using @mention completion provider (#27541)
Previously `src` would not show up because it was filtered out:

<img width="466" alt="image"
src="https://github.com/user-attachments/assets/f3802660-ad73-44be-967d-c332466d9aba"
/>

Release Notes:

- N/A
2025-03-26 21:18:25 +00:00
Mikayla Maki
9e02fee98d Align project panel and git panel deletion behavior (#27525)
This change makes the git panel and project panel behave the same, on
Linux and macOS, and adds prompts.

Release Notes:

- Changed the git panel to prompt before restoring a file.
2025-03-26 21:15:24 +00:00
loczek
999ad77a59 workspace: Double click empty pane to open new file (#27521)
Release Notes:

- Added ability to double click on empty pane to open a new file
2025-03-26 14:07:54 -07:00
Smit Barmase
780d0eb427 editor: Do not use hide_mouse_while_typing for single line editor (#27536)
Release Notes:

- N/A
2025-03-27 02:32:16 +05:30
Agus Zubiaga
7b40ab30d7 assistant2: Add scrollbar to active thread (#27534)
This required adding scrollbar support to `list`. Since `list` is
virtualized, the scrollbar height will change as more items are
measured. When the user manually drags the scrollbar, we'll persist the
initial height and offset calculations accordingly to prevent the
scrollbar from moving away from the cursor as new items are measured.

We're not doing this yet, but in the future, it'd be nice to budget some
time each frame to layout unmeasured items so that the scrollbar height
is as accurate as possible.

Release Notes:

- N/A
2025-03-26 18:01:13 -03:00
Kirill Bulatov
0a3c8a6790 Remove project strong reference from git panel's log output editor (#27496)
A readonly buffer built from a static `&str` output does not need rich
project-based capabilities, and leaking projects in global git panel
might be dangerous.

Also adds readonly capability to the buffer, as
`editor.set_read_only(true);` API is a separate thing.

Release Notes:

- N/A
2025-03-26 23:01:03 +02:00
Ben Kunkle
1463b4d201 gpui/blade: Allow forcing use of a specific GPU with ZED_DEVICE_ID env var (#27531)
Workaround for users affected by #25899

Thanks to the work done by @kvark in
https://github.com/kvark/blade/pull/210, we have the ability to tell
Vulkan (through blade) a specific GPU to use.

This will hopefully allow some of the users affected by #25899 to use
Zed by allowing them to use a specific GPU, if the primary/default GPU
will not work

Release Notes:

- Added the ability to specify which GPU Zed uses on Linux by setting
the `ZED_DEVICE_ID` environment variable. You can obtain the device ID
of your GPU by running `lspci -nn | grep VGA` which will output each GPU
on one line like:
  ```
08:00.0 VGA compatible controller [0300]: NVIDIA Corporation GA104
[GeForce RTX 3070] [10de:2484] (rev a1)
  ````
where the device ID here is `2484`. This value is in hexadecimal, so to
force Zed to use this specific GPU you would set the environment
variable like so:
  ```
  ZED_DEVICE_ID=0x2484
  ```
Make sure to export the variable if you choose to define it globally in
a `.bashrc` or similar
2025-03-26 20:32:36 +00:00
Smit Barmase
77856bf017 Hide the mouse when the user is typing in the editor - take 2 (#27519)
Closes #4461

Take 2 on https://github.com/zed-industries/zed/pull/25040. 

Fixes panic caused due to using `setHiddenUntilMouseMoves` return type
to `set` cursor on macOS.

Release Notes:

- Now cursor hides when the user is typing in editor. It will stay
hidden until it is moved again. This behavior is `true` by default, and
can be configured with `hide_mouse_while_typing` in settings.

---------

Co-authored-by: Peter Tripp <peter@zed.dev>
Co-authored-by: Thomas Mickley-Doyle <thomas@zed.dev>
Co-authored-by: Agus <agus@zed.dev>
Co-authored-by: Kirill Bulatov <kirill@zed.dev>
Co-authored-by: Agus Zubiaga <hi@aguz.me>
Co-authored-by: Angelk90 <angelo.k90@hotmail.it>
2025-03-27 01:58:26 +05:30
Marshall Bowers
848a99c605 assistant2: Rework enabled tool representation (#27527)
This PR reworks how we store enabled tools in the `ToolWorkingSet`.

We now track them based on which tools are explicitly enabled, rather
than by the tools that have been disabled.

Also fixed an issue where switching profiles wouldn't properly set the
right tools.

Release Notes:

- N/A
2025-03-26 20:26:26 +00:00
Peter Tripp
435a36b9f9 html: Improve settings, formatting and user binaries (#27524)
Added support for using `language_server` as HTML formatter.
Added support for finding `vscode-html-language-server` in user's path.

Release Notes:

- N/A
2025-03-26 16:24:37 -04:00
Bennet Bo Fenner
8b3ddcd545 assistant2: Fix \\ appearing for paths in file context picker (#27528)
Closes #ISSUE

Release Notes:

- N/A
2025-03-26 20:22:13 +00:00
Elvis Pranskevichus
13bf179aae python: Show environment name if available (#26741)
Right now the toolchain popup is a nondescript list of duplicate entries
like `Python 3.10.15 (VirtualEnvWrapper)` and one has to look at the
interpreter path to distinguish one virtualenv from another.

Fix this by including the env name as reported by pet, so the entries
looks like `Python 3.10.15 (myproject; VirtualEnvWrapper)`.

Release Notes:

- Python: Improved display of environments in toolchain selector
2025-03-26 21:08:26 +01:00
Marshall Bowers
cdaad2655a assistant2: Add profile selector (#27520)
This PR replaces the tool selector with a new profile selector.

<img width="1394" alt="Screenshot 2025-03-26 at 2 35 42 PM"
src="https://github.com/user-attachments/assets/9631c6e9-9c47-411e-b9fc-5d61ed9ca1fe"
/>

<img width="1394" alt="Screenshot 2025-03-26 at 2 35 50 PM"
src="https://github.com/user-attachments/assets/3abe4e08-d044-4d3f-aa95-f472938452a8"
/>

Release Notes:

- N/A
2025-03-26 18:51:38 +00:00
Michael Sloan
7e4320f587 Fix drawing of 0-width borders when quad has other borders (#27511)
Closes #27485

Release Notes:

- N/A
2025-03-26 11:13:34 -06:00
Agus Zubiaga
130abc8998 assistant2: Encourage diagnostics check (#27510)
Release Notes:

- N/A
2025-03-26 13:42:09 -03:00
Richard Feldman
9db4c8b710 Add Create Directory Tool (#27505)
`mkdir -p` but it works cross-platform and uses project abstractions.

<img width="629" alt="Screenshot 2025-03-26 at 11 02 37 AM"
src="https://github.com/user-attachments/assets/9ef58d53-3343-4c94-a8f3-b82ab942611b"
/>

Release Notes:

- N/A
2025-03-26 11:59:03 -04:00
Marshall Bowers
e67ad1a1b6 extension_host: Rename Extension variants so that the version number components are clearer (#27507)
This PR renames the variants of the `Extension` enum with delimiters
between the version number components so that it's clearer which version
of the extension API they refer to.

Release Notes:

- N/A
2025-03-26 15:54:14 +00:00
Alvaro Parker
82536f5243 Add support for excluding files based on .gitignore (#26636)
Closes: #17543

Release Notes:

- **New Feature:** Introduced the ability to automatically remove files
and directories from the Zed project panel that are specified in
`.gitignore`.
- **Configuration Option:** This behavior can be controlled via the new
`project_panel.hide_gitignore` setting. By setting it to `true`, files
listed in `.gitignore` will be excluded from the project panel.
- **Toggle:** Ability to toggle this setting using the action
`ProjectPanel::ToggleHideGitIgnore`

```json
  "project_panel": {
    "hide_gitignore": true
  },

```

This results in a cleaner and easier to browse project panel for
projects that generate a lot of object files like `xv6-riscv` or `linux`
without needing to tweak `file_scan_exclusions` on `settings.json`

**Preview:**
- With `"project_panel.hide_gitignore": false` (default, this is how zed
currently looks)

![Screenshot From 2025-03-23
12-50-17](https://github.com/user-attachments/assets/15607e73-a474-4188-982a-eed4e0551061)

- With `"project_panel.hide_gitignore": true` 

![Screenshot From 2025-03-23
12-50-27](https://github.com/user-attachments/assets/3e281f92-294c-4133-b5e3-25e17f15bd4d)

- Action `ProjectPanel::ToggleHideGitIgnore`

![Screenshot From 2025-03-23
12-50-55](https://github.com/user-attachments/assets/4d03db33-75ad-471c-814c-098698a8cb38)
2025-03-26 20:57:09 +05:30
Richard Feldman
9eacac62a9 Escape markdown in tools' ui_text (#27502)
Escape markdown in tools' `ui_text`

<img width="628" alt="Screenshot 2025-03-26 at 10 43 23 AM"
src="https://github.com/user-attachments/assets/bb694821-aae7-4ccf-a35a-a3317b0222d5"
/>


Release Notes:

- N/A
2025-03-26 11:27:02 -04:00
Richard Feldman
82b0881dcb Make the "View Panel" focus the assistant panel (#27504)
Release Notes:

- N/A
2025-03-26 11:26:49 -04:00
iyht
0a49ccbebf Allow the keybinding context to detect the terminal vi_mode (#26236)
Release Notes:

- Added support for detecting the vi_mode in the keybinding context. Now
we can define and use the keybinding when the terminal is in vi_mode.


https://github.com/user-attachments/assets/a927b6c9-c634-4739-9502-8457614d9a90
2025-03-26 20:53:23 +05:30
张小白
d232150d67 windows: Fix performance issues after trashing or deleting a folder (#27498)
Closes #25247

Since the upstream `Notify` repo hasn't merged the related PR yet, this
is basically a temporary patch to work around it.

Release Notes:

- N/A
2025-03-26 23:20:09 +08:00
Joseph T. Lyons
9a2dfa687d Bump Zed to v0.181 (#27506)
Release Notes:

-N/A
2025-03-26 11:15:42 -04:00
Bennet Bo Fenner
1e22faebc9 lsp: Check if language server supports workspace/symbol request (#27491)
This ensures that we do not get a bunch of error logs when using the
symbol search:
```
[2025-03-26T13:23:32+01:00 ERROR project] Method not found
[2025-03-26T13:23:32+01:00 ERROR project] Method not found
[2025-03-26T13:23:32+01:00 ERROR project] Method not found
[2025-03-26T13:23:32+01:00 ERROR project] Method not found
[2025-03-26T13:23:32+01:00 ERROR project] Method not found
[2025-03-26T13:23:33+01:00 ERROR project] Method not found
...
```

Release Notes:

- N/A
2025-03-26 13:09:41 +00:00
Danilo Leal
1d9c581ae0 assistant2: Use different icons in the notification popover depending on status (#27493)
Using a check green icon for "success, you're changes are applied" and
the info, muted icon for just "there are news".

<img
src="https://github.com/user-attachments/assets/6b7e06bc-ca03-40fd-8962-7e21f5cd85d9"
width="500"/>
<img
src="https://github.com/user-attachments/assets/347ac8ac-792f-4e18-94d5-69bb9d5270e8"
width="500"/>

Release Notes:

- N/A
2025-03-26 10:03:06 -03:00
Danilo Leal
39af3b434a assistant2: Improve tool card header scrolling affordance (#27492)
Follow up to https://github.com/zed-industries/zed/pull/27489

Added this subtle gradient to the right side of the tool card header so
users know there is more content, suggesting it can be scrolled. Also
took the opportunity to extract out commonly used custom colors in all
of these cards into their own functions to ensure consistency.

Here's the final product:


https://github.com/user-attachments/assets/e44150f9-7751-46c7-8790-149b86cc5e0f

Release Notes:

- N/A
2025-03-26 09:45:51 -03:00
Danilo Leal
64b3eea3cd assistant2: Fix tool label overflowing on card header (#27489)
### Before

<img
src="https://github.com/user-attachments/assets/f56211d8-d60d-4e6c-9c40-af2523f71431"
width="600" />

### After

Now, you can horizontally scroll when the header holds an overflowing
label.


https://github.com/user-attachments/assets/6cb90de3-1db5-4a30-8f23-22221098a233

Release Notes:

- N/A
2025-03-26 09:10:40 -03:00
Bennet Bo Fenner
72318df4b5 lsp: Add support for textDocument/documentSymbol (#27488)
This PR adds support for retrieving the outline of a specific
buffer/document from the LSP.
E.g. for this code (`crates/cli/src/cli.rs`):
```rs
use collections::HashMap;
pub use ipc_channel::ipc;
use serde::{Deserialize, Serialize};

#[derive(Serialize, Deserialize)]
pub struct IpcHandshake {
    pub requests: ipc::IpcSender<CliRequest>,
    pub responses: ipc::IpcReceiver<CliResponse>,
}

#[derive(Debug, Serialize, Deserialize)]
pub enum CliRequest {
    Open {
        paths: Vec<String>,
        urls: Vec<String>,
        wait: bool,
        open_new_workspace: Option<bool>,
        env: Option<HashMap<String, String>>,
    },
}

#[derive(Debug, Serialize, Deserialize)]
pub enum CliResponse {
    Ping,
    Stdout { message: String },
    Stderr { message: String },
    Exit { status: i32 },
}

/// When Zed started not as an *.app but as a binary (e.g. local development),
/// there's a possibility to tell it to behave "regularly".
pub const FORCE_CLI_MODE_ENV_VAR_NAME: &str = "ZED_FORCE_CLI_MODE";
```

Rust-analyzer responds with:
```
Symbol: 'IpcHandshake' - Struct - (4:0-8:1) (5:11-5:23)
  Symbol: 'requests' - Field - (6:4-6:44) (6:8-6:16)
  Symbol: 'responses' - Field - (7:4-7:48) (7:8-7:17)
Symbol: 'CliRequest' - Enum - (10:0-19:1) (11:9-11:19)
  Symbol: 'Open' - EnumMember - (12:4-18:5) (12:4-12:8)
    Symbol: 'paths' - Field - (13:8-13:26) (13:8-13:13)
    Symbol: 'urls' - Field - (14:8-14:25) (14:8-14:12)
    Symbol: 'wait' - Field - (15:8-15:18) (15:8-15:12)
    Symbol: 'open_new_workspace' - Field - (16:8-16:40) (16:8-16:26)
    Symbol: 'env' - Field - (17:8-17:44) (17:8-17:11)
Symbol: 'CliResponse' - Enum - (21:0-27:1) (22:9-22:20)
  Symbol: 'Ping' - EnumMember - (23:4-23:8) (23:4-23:8)
  Symbol: 'Stdout' - EnumMember - (24:4-24:30) (24:4-24:10)
    Symbol: 'message' - Field - (24:13-24:28) (24:13-24:20)
  Symbol: 'Stderr' - EnumMember - (25:4-25:30) (25:4-25:10)
    Symbol: 'message' - Field - (25:13-25:28) (25:13-25:20)
  Symbol: 'Exit' - EnumMember - (26:4-26:24) (26:4-26:8)
    Symbol: 'status' - Field - (26:11-26:22) (26:11-26:17)
Symbol: 'FORCE_CLI_MODE_ENV_VAR_NAME' - Constant - (29:0-31:67) (31:10-31:37)
```

We'll use this to reference specific symbols in assistant2

Release Notes:

- N/A
2025-03-26 11:38:22 +00:00
Danilo Leal
d52291bac1 assistant2: Add new icons for create and delete file tool (#27487)
Release Notes:

- N/A
2025-03-26 08:20:09 -03:00
Kirill Bulatov
7462e74fbf Improve editor::CopyAndTrim action's discoverability (#27484)
Follow-up of https://github.com/zed-industries/zed/pull/27206

Add it to the editor context menu and Zed's app menu near the `Copy`
action.

Release Notes:

- N/A
2025-03-26 10:28:32 +00:00
CharlesChen0823
de67d93a92 git: Fix staging file hunks on Windows (#26661)
Closes #26458 

I had checked, and also reference the implementation in vscode.

Release Notes:

- N/A

---------

Co-authored-by: 张小白 <364772080@qq.com>
2025-03-26 15:46:04 +08:00
Anthony Eid
a02f7e5cda Debugger UI: Update tab content and remove red tint from terminated session (#27475)
The new tab content makes it obvious when a session is shutdown so the
red tint is no longer needed.

Release Notes:

- N/A
2025-03-26 07:13:12 +00:00
Anthony Eid
d70ac64fe4 Allow enabling/disabling breakpoints (#27280)
This PR adds the ability to enable/disable breakpoints. It also fixes a
bug where toggling a log breakpoint from the breakpoint context menu
would add a standard breakpoint on top of the log breakpoint instead of
deleting it.

todo: 
- [x] Add `BreakpointState` field Breakpoint that manages if a
breakpoint is active or not
- [x] Don't send disabled breakpoints to DAP servers - in progress
- [x] Half the opacity of disabled breakpoints - in progress
- [x] Add `BreakpointState` to database
- [x] Editor test for enabling/disabling breakpoints
- [ ] Integration Test to make sure we don't send disabled breakpoints
to DAP servers
- [x] Database test to make sure we properly serialize/deserialize
BreakpointState

Release Notes:

- N/A

---------

Co-authored-by: Piotr <piotr@zed.dev>
Co-authored-by: Conrad <conrad@zed.dev>
Co-authored-by: Mikayla Maki <mikayla.c.maki@gmail.com>
2025-03-26 02:06:08 -04:00
Michael Sloan
df583d73b9 Fix corner_radii clamping for Img when actual size is smaller than the container (#27473)
Noticed this when working on #27472

Release Notes:

- N/A
2025-03-26 05:23:27 +00:00
Michael Sloan
e091c5faaf Fix paint_quad behavior change which clamped corner rounding (#27472)
Turns out that git deletion indicator relied on using larger-than-bounds
corner rounding - see
https://github.com/zed-industries/zed/pull/27460#issuecomment-2753174153

Release Notes:

- N/A
2025-03-26 05:09:18 +00:00
Richard Feldman
931a6d6f40 Notify when tool is finished (#27459)
Now if a tool call finishes (or is blocked on confirmation) and the Zed
window is not active, you get a notification popup. You can turn it off
with a setting.

<img width="420" alt="Screenshot 2025-03-25 at 5 19 25 PM"
src="https://github.com/user-attachments/assets/bdf7b6b8-4428-4b46-8b09-e0be140f8a51"
/>
<img width="420 alt="Screenshot 2025-03-25 at 5 18 13 PM"
src="https://github.com/user-attachments/assets/1325e7b8-cd5a-44b9-a82d-5db928ad3cfc"
/>

Release Notes:

- N/A

---------

Co-authored-by: Agus Zubiaga <hi@aguz.me>
Co-authored-by: Danilo Leal <daniloleal09@gmail.com>
2025-03-25 20:52:44 -04:00
Finn Evers
a605b66ce1 editor: Do not insert scrollbar hitboxes when scrollbars are disabled (#27467)
This PR fixes an issue where a clickable scrollbar track was inserted in
the editor even when scrollbars were explicitly disabled via the user's
settings. If the user has

```json
"scrollbar": {
    "show": "never"
}
```
in their settings, invisible and interactable scrollbar tracks will be
inserted in the editor if scrollbars would be required, as seen below:


https://github.com/user-attachments/assets/b0d915a9-7a7e-4518-84d8-96d9b15aab12

The bug occurs because we only set the scrollbar contents to be
invisible, which however does not affect the insertion of hitboxes for
the scrollbars.

This PR fixes this behaviour by preventing any scrollbar layouting from
happening when scrollbars are explicitly disabled via the settings:


https://github.com/user-attachments/assets/a3f5725b-aead-4fec-9fd8-e574cf269d45

All existing panels which have configurable scrollbars behave the same
way, see
fb2586a553/crates/outline_panel/src/outline_panel.rs (L4362-L4373)

for example. Following this check, neither a thumb nor a track is
inserted in any case when scrollbars are never to be shown.

Release Notes:

- Fixed invisible scrollbar tracks being inserted into the editor when
scrollbars are explicitly disabled via settings.
2025-03-26 01:44:35 +01:00
Michael Sloan
7376c6f377 Add support for Gemini 2.5 Pro Experimental model (#27468)
Release Notes:

- Added support for Gemini 2.5 Pro Experimental model to Zed AI.

Co-authored-by: Wilhelm Klopp <wil.klopp@gmail.com>
2025-03-26 00:12:10 +00:00
Smit Barmase
ee08776f34 markdown_preview: Fix code block highlight and indentation (#27463)
Closes #23218

Before:
<img width="1463" alt="before"
src="https://github.com/user-attachments/assets/4f77a4e0-61b8-4516-91a4-366f73e24760"
/>

After:
<img width="1463" alt="after"
src="https://github.com/user-attachments/assets/61e2c69d-fa6b-4c52-b1eb-ad7a61e274e0"
/>


Release Notes:

- Fixed issue where code block highlight and indentation in markdown
preview was rendered incorrectly.
2025-03-26 04:14:23 +05:30
Kirill Bulatov
30f7e896cf Add a way to control go to definition fallback (#27426)
Follow-up of https://github.com/zed-industries/zed/pull/9243 and
https://github.com/zed-industries/zed/pull/16512

Release Notes:

- Added a way to control go to definition fallback
2025-03-25 22:13:35 +00:00
Michael Sloan
8c8f50d916 Pull logic for clamping corner rounding radii out of Corners::to_pixels (#27460)
This seems more correct as corners are not necessarily only for rounding
radii.

Also applies clamping after scaling in `paint_quad`, deduplicating that
logic. This also provides a more precise result by doing the clamping
after scaling, avoiding floating point rounding issues (probably a
non-issue).

Release Notes:

- N/A
2025-03-25 21:56:17 +00:00
Ben Kunkle
76192ea93c worktree: Don't open files >= 6GB in size (#27458)
Temporary Workaround For: #27283

This PR can (and should!) be reverted once the underlying inefficiencies
are resolved

Release Notes:

- Files that are 6GB or larger will now not open. This is a temporary
workaround for inefficient handling of large files resulting in
extremely high memory usage, often resulting in system freezing,
requiring a restart of Zed or the entire system.
2025-03-25 16:43:40 -05:00
Danilo Leal
fb2586a553 assistant2: Add some padding top to the checkpoint element (#27456)
Most minimal PR ever.

Release Notes:

- N/A
2025-03-25 21:21:03 +00:00
Peter Tripp
28e0bb5a12 ci: Run nix nightly on any mac runners (#27457)
Nix nightly builds can now run on all macOS runners.

Follow-up to: https://github.com/zed-industries/zed/pull/27014

Release Notes:

- N/A
2025-03-25 17:15:15 -04:00
Finn Evers
a65ea2708c editor: Refactor scrollbar-related code (#24134)
This PR is primarily an implementation of @osiewicz
[comment](https://github.com/zed-industries/zed/pull/19495#pullrequestreview-2488877957)
in an effort to increase maintainability after the horizontal editor
scrollbar was added in #19495 . I also want to build on these changes in
future PRs to adress some other small bugs.

This primarily does the following:
1. Uses `along` wherever possible
2. Fixes the amount of mouse event listeners attached to the editor when
scrollbars are displayed to 2 instead of 2-4 in case both scrollbars are
displayed.

This can be done since only one scrollbar can be dragged by the cursor
at any given time, so the event listeners now account for that. The
state reflecting the scrollbar dragging state was also updated
accordingly.

It does not change any functionality besides the aforementioned event
listener code as well as some minor bugs which where present after
#19495 , namely:
- One missing `cx.stop_propagation()` (see
[here](a8741dc310/crates/editor/src/element.rs (L4684))
and
[here](a8741dc310/crates/editor/src/element.rs (L4838))
respectively).
- The horizontal scrollbar thumb having a small border on the left side,
which seems to be unintended for the horizontal scrollbar whilst
intended for the vertical one. Since this is a minimal change, I figured
it could be already included in this PR.

This PR admittetly grew quite large over time, however, much of the diff
is just renames to account for the code now working for both axes as
well as moved code. The logic remains (or should at least be)
unaffected. If I should split this into two PRs or remove some of the
changes, please let me know.

Release Notes:

- N/A
2025-03-25 22:08:46 +01:00
Alex van de Griendt
1574a3a2fd python: Add task for running modules (#26462)
Closes #26460

I am new to contributing to Zed (and pretty new to Rust in general). I'm
not too familiar with code style, guidelines etc. so please feel free to
suggest changes/improvements.

This PR adds a run icon to Python files that have a "main" function:
```python
if __name__ == "__main__":
    ...
```

In addition to the gutter icon, there is now also an extra task in the
command palette "run module".

Release Notes:

- Added detection for runnable Python modules
- Added Python-specific task to run a Python file as a module from
inside the project's scope

---------

Co-authored-by: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com>
2025-03-25 22:08:16 +01:00
Danilo Leal
152432f1d9 title bar: Adjust the onboarding banner component API (#27455)
This PR encapsulates the layout building of the Onboarding Banner
component inside of it, allowing to, at the call site, just pass an
icon, title, and subtitle. The `subtitle` parameter, by default, uses
the `Introducing:` label, which I think will be the one we'll use most
of the time for this specific component.

Release Notes:

- N/A
2025-03-25 17:57:42 -03:00
Marshall Bowers
5953c6167b assistant2: Allow profiles to manage context server tools (#27452)
This PR updates the agent profiles with support for managing context
server tools.

Release Notes:

- N/A
2025-03-25 19:55:59 +00:00
Danilo Leal
aab02a4166 Fix color swatch shrinking in the LSP completion menu (#27450)
### Before

Note how in the second time I try to add a color property, the color
swatch is shrunk.


https://github.com/user-attachments/assets/677a37b2-8ae4-408d-8dea-d9cc4bc2e119

### After


https://github.com/user-attachments/assets/aa902573-1f44-48e1-a396-e22ad5703155

Release Notes:

- Fixed color swatches shrinking upon re-adding a color-related
property.
2025-03-25 16:55:42 -03:00
Michael Sloan
9fc570c4be Remove Pixels: Mul<Pixels, Output = Pixels> impl, add ScaledPixels ops (#27451)
It doesn't make sense to have `Pixels: Mul<Pixels, Output = Pixels>` as
the output should be `Pixels^2` (area), so these impls are removed. All
code where these impls were used are improved by instead multiplying by
`f32` or `usize`.

Also adds math op impls that are present for `Pixels` but absent for
`ScaledPixels`. Adds missing `Mul<Pixels> for usize` to both.

Release Notes:

- N/A
2025-03-25 19:34:26 +00:00
Marshall Bowers
581d67398a Remove ui dependency from assistant_tool and context_server (#27449)
This PR removes the dependency on the `ui` crate from the
`assistant_tool` and `context_server` crates.

These crates were only depending on it for `IconName`, which can now be
depended on from `icons` directly.

Release Notes:

- N/A
2025-03-25 18:37:15 +00:00
Marshall Bowers
4a30b960d4 language_model: Remove dependency on ui (#27448)
This PR removes the dependency on the `ui` crate from the
`language_model` crate.

We were only depending on it to import `IconName`—which now lives in
`icons`—and some re-exported GPUI items.

Release Notes:

- N/A
2025-03-25 18:30:46 +00:00
Marshall Bowers
503bf607c5 Add icons crate (#27447)
This PR adds a new `icons` crate and moves the `IconName` into it.

We have a number of crates that are taking a dependency on `ui` just so
they can talk about icons, which is not ideal.

Release Notes:

- N/A
2025-03-25 18:18:22 +00:00
Agus Zubiaga
46e86f003f assistant tools: Add Tool::icon method instead of matching on name (#27444)
Release Notes:

- N/A
2025-03-25 18:17:36 +00:00
Marshall Bowers
0339d654d2 zed: Make inline assist quick action dispatch an action (#27445)
This PR makes the "Inline Assist" quick action in the quick action bar
work by dispatching the `InlineAssist` action.

This fixes an issue where the button was not working with Assistant 2.

Release Notes:

- N/A
2025-03-25 18:11:30 +00:00
Agus Zubiaga
24d76a64c3 Reapply #27200 after bad conflict resolution (#27446)
Release Notes:

- N/A
2025-03-25 18:06:45 +00:00
Piotr Osiewicz
3ba624391f debugger ui: Make variable values muted by default (#27441)
Closes #ISSUE

Release Notes:

- N/A
2025-03-25 18:43:27 +01:00
Michael Sloan
31e3c13ea9 Add .github/copilot-instructions.md to paths loaded for rules (#27442)
Release Notes:

- N/A
2025-03-25 17:30:37 +00:00
Marshall Bowers
0fec04a1b7 assistant2: Disable "Add Server" button when the required fields are empty (#27440)
This PR updates the add context server modal to disable the "Add Server"
button when the required fields are not filled out.

Release Notes:

- N/A
2025-03-25 17:19:30 +00:00
Richard Feldman
bf255486c0 Add find-replace-file tool, use it by default over edit-files-tool (#27438)
@agu-z and paired on trying out a "one tool call per edit" approach for
editing files. (The previous approach is still available, it's just
unchecked by default for now.)

Release Notes:

- N/A

---------

Co-authored-by: Agus <agus@zed.dev>
2025-03-25 13:12:50 -04:00
Nathan Sobo
cd1e56d6c7 Add support for dashed borders to GPUI (#27139)
Features:

* Scales dash spacing with border width.
* Laying out dashes around rounded corners.
* Varying border widths with rounded corners - now uses an ellipse for the inner edge of the border.
* When there are no rounded corners, each straight border is laid out separately, so that the dashes to meet at the corners.
* All sides of each dash are antialiased.

![image](https://github.com/user-attachments/assets/b3789a98-a5be-4f97-9736-c4e59615afe6)

![image](https://github.com/user-attachments/assets/739bdc57-4580-42c8-bfc3-6e287411a408)

Release Notes:

- N/A

---------

Co-authored-by: Michael Sloan <michael@zed.dev>
Co-authored-by: Ben <ben@zed.dev>
2025-03-25 11:11:04 -06:00
Piotr Osiewicz
2fe2028e20 debugger: Fix typing in active buffer resulting a jump to an active debug line (#27439)
/cc @iamnbutler 

Release Notes:

- N/A

---------

Co-authored-by: Anthony Eid <hello@anthonyeid.me>
2025-03-25 17:08:36 +00:00
Piotr Osiewicz
f9212a001e debugger: Make UI a bit more dense (#27429)
Closes #ISSUE

Release Notes:

- N/A
2025-03-25 17:58:51 +01:00
Joseph T. Lyons
954a56ce0c Update stale issue bot to run on Wednesday (#27437)
Release Notes:

- N/A
2025-03-25 16:40:43 +00:00
Marshall Bowers
275cecb262 assistant2: Add modal for adding context servers (#27434)
This PR adds a modal for adding context servers from the Assistant 2
configuration view:

<img width="1394" alt="Screenshot 2025-03-25 at 12 22 50 PM"
src="https://github.com/user-attachments/assets/52fe194f-7d88-4f3b-aee1-8c6385136e6b"
/>

Release Notes:

- N/A
2025-03-25 16:37:56 +00:00
Richard Feldman
6b7167a32d Gracefully handle models searching for empty glob (#27370)
Sometimes we've seen models provide an empty string for the path search
glob. This assumes they meant "*" when that happens.

Separately, this also removes an unnecessary `clone` of a `String`.

Release Notes:

- N/A
2025-03-25 12:32:20 -04:00
Peter Tripp
430814c0a9 ci: Hide harmless cargo about error from release output (#27433)
Hide harmless errors like this from CI output.

<img width="834" alt="Screenshot 2025-03-25 at 12 07 36"
src="https://github.com/user-attachments/assets/55450812-000d-49a0-9926-cae0df8aa281"
/>

Release Notes:

- N/A
2025-03-25 16:24:05 +00:00
Luke Naylor
5aba5e1b3d Add default settings for LaTeX (#27286)
Closes https://github.com/rzukic/zed-latex/issues/70 where the language
server `texlab` is not used for code formatting when the "cspell"
extension is also installed, because it also provides a language server
for the LaTeX filetype but only for spell checking.

Release Notes:

- Fix conflict between LaTeX and cspell extensions affecting code formatting on save.
2025-03-25 11:36:48 -04:00
Agus Zubiaga
408e157d0f assistant edit tool: Reliability improvements (#27431)
- Add instructions in description to read before editing
- Add instructions in edit prefix to explicitly ask for reads
- Fix `connection error: delay between messages too long` by processing
chunks off a channel

Release Notes:

- N/A
2025-03-25 12:34:01 -03:00
Richard Feldman
2414855121 Gracefully handle bad LLM-generated paths in dir tool (#27202)
Release Notes:

- N/A

Co-authored-by: Ben <ben@zed.dev>
2025-03-25 11:32:27 -04:00
Marshall Bowers
e7f7ed349b assistant2: Add create-file and copy-path tools to the "Code Writer" profile (#27432)
This PR adds the `create-file` and `copy-path` tools to the "Code
Writer" profile.

Release Notes:

- N/A
2025-03-25 15:23:37 +00:00
Ben Kunkle
e273de5490 python: Fix incorrect indenting of except, finally, else, and elif control flow (#27428)
Closes #10832

Note: This PR only fixes the issue where when entering one of `except`,
`finally`, `else`, and `elif` after another block like so:

```python
try:
    for i in range(n):
        pass
except:|
```

The `except` would be indented resulting in the following:

```python
try:
    for i in range(n):
        pass
    except:|
```

This PR does not fix a separate issue in which the indentation is not
corrected from the second example to the first, i.e. if example 2 is
typed verbatim in Zed it will not auto-indent to look like example 1.
Handling of this case would likely require specific logic to handle, or
changes to the tree-sitter grammar for Python, as the current grammar
results in ERROR nodes that obscure the natural structure (cannot tie
the `except` to the `try`)

Release Notes:

- Fixed an issue where `except`, `finally`, `else`, and `elif` control
flow keywords in Python would be incorrectly indented when entered at
the correct level of indentation.
2025-03-25 14:58:41 +00:00
Richard Feldman
7046b9641d Add create-file-tool (#27381)
<img width="627" alt="Screenshot 2025-03-24 at 12 52 04 PM"
src="https://github.com/user-attachments/assets/0e8c061a-11c5-4d60-a694-55575b6c8f5e"
/>

Release Notes:

- N/A
2025-03-25 10:56:41 -04:00
Kirill Bulatov
9b883e02a6 Revert nix package bump attempt (#27425)
Follow-up of
https://github.com/zed-industries/zed/pull/27424/files#r2012188255

Release Notes:

- N/A
2025-03-25 14:54:48 +00:00
Kirill Bulatov
ffee218070 Use newer cargo-bundle without the build error (#27424)
Fixes Nightly builds for macOS.

Release Notes:

- N/A
2025-03-25 14:16:23 +00:00
Naim A.
d9dcc59334 Merge clangd's inactiveRegions with existing diagnostics (#26737)
Closes: https://github.com/zed-industries/zed/issues/13089

This PR attempts to resolve the issues discussed in my previous PR
#26146.

Release Notes:

- Fixed: `inactiveRegions` doesn't replace existing diagnostics anymore
2025-03-25 15:13:53 +02:00
Smit Barmase
8f1023360d extension: Add support for additional_workspace_configuration and additional_initialization_options (#27407)
Closes #22410

With this PR extensions can provide additional workspace configuration
for other LSP Adapters. This allows extensions like Astro, Svelte, Vue,
etc to provide plugins for vtsls typescript server, fixing issues like:
https://github.com/zed-industries/zed/issues/4577,
https://github.com/zed-industries/zed/issues/21697,
https://github.com/zed-industries/zed/issues/26901#issuecomment-2737485096

Todo:

- [x] Test case when extension is installed, does vtsls workspace config
refreshes?

Before:
<img width="450" alt="image"
src="https://github.com/user-attachments/assets/f242167c-5264-44ab-b5a7-8c90eb75c6a1"
/>

After:
<img width="450" alt="image"
src="https://github.com/user-attachments/assets/6a5f1afe-a0e1-4f64-8a95-919b0bf97614"
/>

Release Notes:

- N/A
2025-03-25 18:23:59 +05:30
Danilo Leal
9468b9699e assistant2: Add icon for copy-path tool (#27421)
Release Notes:

- N/A
2025-03-25 12:44:44 +00:00
Nate Butler
f1a9fdddab docs: Improve Git page (#26566)
Release Notes:

- N/A

---------

Co-authored-by: Danilo Leal <67129314+danilo-leal@users.noreply.github.com>
Co-authored-by: Danilo Leal <daniloleal09@gmail.com>
2025-03-25 09:22:01 -03:00
Jason Lee
d40f8889b9 title_bar: Anchor user menu popover at top right corner (#27329)
Release Notes:

- Improved user menu placement

| Before | After |
| --- | --- |
| ![Pasted image
(2)](https://github.com/user-attachments/assets/fae750fd-d6bc-45d2-9ab0-7a1a1c6262ba)
|
![image](https://github.com/user-attachments/assets/c5d0453f-5bd5-45e2-a56d-e200e90c0a2a)
|
2025-03-25 08:20:50 -03:00
Danilo Leal
6715c8582f assistant2: Remove the Prompt Library section from the settings (#27404) 2025-03-25 07:34:11 -03:00
isomo
baf03e355b copilot: Add Claude 3.7-Sonnet-Thought model to Copilot Chat (#27409)
- Follow-up to: #25529 

Release Notes:

- Added Claude Sonnet 3.7 Thought to GitHub Copilot Chat
2025-03-25 10:56:27 +01:00
Mikayla Maki
35ec4753b4 Make a single re-usable banner component (#27412)
Release Notes:

- Fixed an issue where both the predict edit and git onboarding banners
would both show at the same time.
2025-03-25 07:05:25 +00:00
张小白
b85492bd00 windows: Detect pwsh (#25713)
Closes #22015


Release Notes:

- N/A
2025-03-24 22:31:11 -07:00
Julia Ryan
fca7ce9a14 nix: Cache deps with crane (#27411)
It turns out that on linux crane's `buildDepsOnly` was working fine, so
I'm re-enabling it and will worry about why it's failing on darwin
later.

This should significantly improve the amount of artifact-reuse for the
linux nix builds, which will hopefully bring build times low enough that
we're ok putting it in CI.

Release Notes:

- N/A
2025-03-24 22:24:02 -07:00
Mikayla Maki
42f01cc903 Set edit predictions to default to the Zed provider (#27394)
Release Notes:

- Changed the default edit prediction provider from Copilot to Zed
2025-03-24 20:45:06 -07:00
Ben Kunkle
ffa736e566 zlog: Implement better scope map (#27408)
Another step towards having `zlog` as the default logging solution in
Zed.
The new ScopeMap replaces the previous HashMap based implementation used
for scope lookups to:

A. Reduce complexity
B. Increase speed at which non-enabled logs can be filtered out
C. Provide more granular control over how scopes are determined to be
enabled/disabled,
   and what caching/other speed increase opportunities are available

Release Notes:

- N/A
2025-03-25 03:05:32 +00:00
Richard Feldman
e9e6529df4 Add copy-path tool (#27371)
<img width="631" alt="Screenshot 2025-03-24 at 11 01 10 AM"
src="https://github.com/user-attachments/assets/7e144619-83d0-4455-8d80-cc7ec6a7b03e"
/>

Release Notes:

- N/A
2025-03-24 21:21:55 -04:00
Piotr Osiewicz
3205ae3884 debugger/lldb: Remove xcrun-based lldb-dap binary lookup (#27405)
Closes #ISSUE
/cc @RemcoSmitsDev 

Release Notes:

- N/A
2025-03-25 00:21:09 +00:00
Danilo Leal
03102b4d7e assistant: Allow opening the Prompt Library via the command palette (#27368)
Also took the opportunity to rename the action to something that would
be clearer in the command palette, from `DeployPromptLibrary` to
`OpenPromptLibrary`.

Release Notes:

- N/A

---------

Co-authored-by: Marshall Bowers <git@maxdeviant.com>
2025-03-24 21:00:33 -03:00
Danilo Leal
c32dece1b8 assistant2: Add tiny design improvements (#27399)
Really small stuff, like adding icons and adjusting colors.

Release Notes:

- N/A
2025-03-24 21:00:21 -03:00
Remco Smits
bcfc9e4437 debugger: Improve variable list keyboard navigation (#27308)
This PR improves the keyboard navigation for the variable list. 
Before this PR, if you want to open/close nested variables, you had to
use the right/left & up/down arrow keys.
Now you can step through with just only using your left/right arrow
keys, this feels a bit more natural and more similar to how other
editors allow you to navigate through variables.

This PR also fixes the following issues:
- Allow selecting a scope to be the start of your selection
- Allow selecting previous item if the first item is selected

-----


https://github.com/user-attachments/assets/aff0b133-97be-4c09-8ee6-b11495ad5568

Release Notes:

- N/A
2025-03-25 00:36:09 +01:00
Marshall Bowers
a52095f558 copilot: Switch to official @github/copilot-language-server (#27401)
This PR updates Copilot to use the official
[`@github/copilot-language-server`](https://github.com/github/copilot-language-server-release).

It's [available on
npm](https://www.npmjs.com/package/@github/copilot-language-server), so
we're installing it from there.

I tested it out locally and it seemed to be working as expected.

Release Notes:

- Updated Copilot to use the official
[`@github/copilot-language-server`](https://github.com/github/copilot-language-server-release).
2025-03-24 23:35:29 +00:00
Finn Evers
be83c5e1c5 migrator: Add migration for settings changed prior to migrator-introduction (#27375)
This PR updates two existing settings to use the settings migrator
instead of a manually implemented visitor. Both of these settings were
changed prior to the introduction of automatic migrations and the
visitor ensured that the settings were kept backwards compatible. See
https://github.com/zed-industries/zed/pull/22200 and
https://github.com/zed-industries/zed/pull/22364 respectively.

WIth this change, existing user configurations are updated accordingly
and the corresponding settings can derive `Deserialize` again.

I also added tests for the replacement of settings values, as there was
no test for this behaviour. Additionally, I added a seperate test for
the existing migration of `always_show_close_button`, since that
migration updated both the key and value.

Release Notes:

- N/A
2025-03-25 04:35:01 +05:30
Conrad Irwin
46d67a33c7 Don't assume that the excerpt can be found (#27395)
Release Notes:

- Fix (rare) panic in the project diff view
2025-03-24 16:12:26 -06:00
João Marcos
10c04afc81 Fix regression in do_completion changes (#27396)
Caused by #27313

Release Notes:

- N/A
2025-03-24 21:53:03 +00:00
Marshall Bowers
920eda07a5 zed_extension_api: Fork new version of extension API (#27390)
This PR forks a new version of the `zed_extension_api` in preparation
for new changes.

Release Notes:

- N/A
2025-03-24 17:06:05 -04:00
Marshall Bowers
a469c0e261 assistant2: Add move-path tool to the "Code Writer" profile (#27389)
This PR adds the `move-path` tool to the "Code Writer" profile.

Release Notes:

- N/A
2025-03-24 20:22:26 +00:00
Richard Feldman
5d05c4aa70 Remove Lua scripting tool (#27388)
We decided to take this out for now. It doesn't seem necessary, and it
complicates the code a lot. We can always put it back later if desired.

Release Notes:

- N/A
2025-03-24 15:58:07 -04:00
Peter Tripp
5465198d0d docs: Improve Haskell documentation (#27372)
Closes: https://github.com/zed-industries/zed/issues/27365

Release Notes:

- N/A
2025-03-24 15:33:02 -04:00
João Marcos
bbc7fcc54f Fix crash when toggling deleted hunk (#27138)
Release Notes:

- Fix rare crash when toggling deleted hunks.

---------

Co-authored-by: Max Brunsfeld <maxbrunsfeld@gmail.com>
2025-03-24 19:25:56 +00:00
João Marcos
11552cc0bd Git: reload index before reading it (#27386)
This is one of the causes for race conditions, but isn't a specific bug fix by itself.

Release Notes:

- N/A

Co-authored-by: Max Brunsfeld <maxbrunsfeld@gmail.com>
2025-03-24 19:03:57 +00:00
Bennet Bo Fenner
699369995b assistant2: Rework @mentions (#26983)
https://github.com/user-attachments/assets/167f753f-2775-4d31-bfef-55565e61e4bc

Release Notes:

- N/A
2025-03-24 19:32:52 +01:00
Richard Feldman
4a5f89aded Make system prompt be more explicit about root paths (#27383)
## Before

<img width="627" alt="Screenshot 2025-03-24 at 12 55 15 PM"
src="https://github.com/user-attachments/assets/349d7025-e65e-4107-86ae-45eb321003b3"
/>

## After

<img width="627" alt="Screenshot 2025-03-24 at 12 52 04 PM"
src="https://github.com/user-attachments/assets/0e8c061a-11c5-4d60-a694-55575b6c8f5e"
/>

Release Notes:

- N/A
2025-03-24 14:00:16 -04:00
Piotr Osiewicz
e661a0afd6 python: Enable subroot detection for pylsp and pyright (#27364)
This is not going to fully fix the multi-root story, as we still need to
weave venvs through. Hence, no release note just yet.

Release Notes:

- N/A
2025-03-24 18:56:18 +01:00
张小白
57ffd6d78d Update windows-rs (#27379)
Release Notes:

- N/A
2025-03-24 16:29:18 +00:00
Anthony Eid
350c1e41d2 Serialize breakpoints when changing the state of a breakpoint (#27373)
This fixes a rare bug where a breakpoint isn't saved in the database
when a user toggles a breakpoint and immediately exits out of zed.

Release Notes:

- N/A

Co-authored-by: Piotr Osiewicz <peterosiewicz@gmail.com>
2025-03-24 12:25:17 -04:00
tidely
a23e293ca2 Upgrade xkbcommon to v0.8.0 (#27376)
Switch back to xkbcommon releases after
https://github.com/rust-x-bindings/xkbcommon-rs/pull/54 got merged in
0.8.0

Release Notes:

- N/A
2025-03-24 18:13:46 +02:00
Ben Kunkle
f2be201495 jsx_tag_auto_close: Fix <Foo.Bar> component auto-close (#27374)
- **support alternate tag name node names to fix autoclosing of
`<Foo.Bar>` style tags in TSX**
- **remove checks against close tag name while checking if tag is
closed**
- **move jsx tag auto close tests into jsx_tag_auto_close.rs**

Closes #27335

Release Notes:

- Fixed an issue with JSX tag auto-close where components containing a
`.` access like `<Foo.Bar>` would be auto-closed as `</>` instead of
`</Foo.Bar>`
2025-03-24 15:43:06 +00:00
Richard Feldman
43712285bf Add move_path tool (#27366)
<img width="629" alt="Screenshot 2025-03-24 at 10 06 39 AM"
src="https://github.com/user-attachments/assets/b099fcc0-b2f4-44ee-8c8f-416808363689"
/>

Release Notes:

- N/A

---------

Co-authored-by: Marshall Bowers <git@maxdeviant.com>
2025-03-24 14:45:19 +00:00
Acaibrid
f38ee81e83 Treat bun.lock as JSONC (#27359)
Closes #27355 
This PR treat `bun.lock` file as `.jsonc`
note: 
[bun.lock](https://bun.sh/blog/bun-lock-text-lockfile) is a lockfile of
bun.js

Release Notes:

- Updated `bun.lock` files to be recognized as JSONC.
2025-03-24 09:46:23 -04:00
Alvaro Parker
ddf8d07f02 Update cosmic text to 0.13.2 (#27362)
Related #14222

Release Notes:

- This PR updates `cosmic-text` dependency on `gpui` crate from `0.11.2`
to 0.13.2`. This decreases RAM usage zed depending on the amount of
monospace fonts installed on the system. On Arch Linux with `nerd-fonts`
package installed (which provides around 2000 monospaced fonts), it
decreases ram usage from ~800mb to around ~300mb.

- Updated `cosmic-text` to `0.13.2` on `gpui` crate
2025-03-24 13:37:41 +00:00
Danilo Leal
7db9077835 assistant2: Polish the thinking card (#27363)
Mostly just adjusting spacing and making it consistent with how we
display other tool calls.

<img
src="https://github.com/user-attachments/assets/85892006-9029-4cb8-b805-bebe4232e458"
width="600px" />

Release Notes:

- N/A
2025-03-24 10:08:49 -03:00
Nils Koch
4e33aaa55c markdown_preview: Fix preview not rendering bullet points beginning with HTML (#27018)
Closes #26631

Release Notes:

- Fixed markdown preview not rendering bullet points beginning with HTML

Before:
<img width="822" alt="before"
src="https://github.com/user-attachments/assets/102a7eac-fcc2-457b-9587-4a021800841d"
/>


After:
<img width="822" alt="after"
src="https://github.com/user-attachments/assets/8544b318-0efa-46c3-bd9b-c6c21444fab7"
/>

Note: I thought it would make sense to return `true` in `is_text_like`
for `Event::InlineMath` and `Event::DisplayMath` as well, but it looks
like rendering inline math is not supported at all at the moment:


4402e033a4/crates/markdown/src/parser.rs (L108)
2025-03-24 13:57:06 +01:00
Thorben Kröger
d253d46fdf Make python's file, line output clickable in terminal (#26903)
Closes #16004.


![image](https://github.com/user-attachments/assets/73cfe9da-5575-4616-9ed0-99fcb3ab61f5)

Python formats file and line number references in the form `File
"file.py", line 8"`
I'm not a CPython expert, but from a quick look, they appear to come
from:
-
80e00ecc39/Python/traceback.c (L613)
-
80e00ecc39/Python/traceback.c (L927)
I am not aware of the possiblity to also encode the column information.

Release Notes:

- File, line references from Python, like 'File "file.py", line 8' are
now clickable in the terminal

---------

Co-authored-by: Kirill Bulatov <kirill@zed.dev>
2025-03-24 12:36:14 +00:00
Junseong Park
07727f939e Update docs for some settings (#27293)
- update tooltips of `auto_install_extensions` , `active_pane_modifiers`
- update docs of `auto_install_extensions`, `active_pane_modifiers`,
`buffer_line_height`

Release Notes:

- N/A

---------

Co-authored-by: Kirill Bulatov <mail4score@gmail.com>
2025-03-24 13:05:06 +02:00
AltCode
08e8109e79 docs: Add Erlang language server documentation (#27346)
This PR updates the Erlang docs to list the two language servers the
extension offers support for, as well as how to switch from `erlang_ls`
to `erlang-language-platform`.

Release Notes:

- N/A
2025-03-24 11:34:21 +02:00
Andy Waite
f19e1e3b5f docs: Document how to use project-specific settings for Ruby LSP (#27310)
I think it's helpful to illustrate how some settings can be added to the
project settings file rather than being global.

Release Notes:

- N/A

---------

Co-authored-by: Peter Tripp <peter@zed.dev>
2025-03-23 14:54:55 -04:00
Andy Waite
8294b9f674 docs: Add tip for viewing LSP logs when configuring Ruby (#27318)
Ruby LSP displays important information in its logs upon startup, such
as which formatter is detected. Being able to see this helps a lot when
configuring or troubleshooting.

cc @vitallium 

Release Notes:

- N/A
2025-03-22 18:32:16 -04:00
Andy Waite
352c71f8a6 docs: Update Minitest example to clarify it only works with Rails (#27311)
The example in the docs works with Rails but not plain minitest.

There are workarounds such as the [`m`](https://github.com/qrush/m) gem,
or adding ActiveSupport to a non-Rails project, but I feel they are
beyond the scope of the docs here.

Release Notes:

- N/A
2025-03-22 18:30:34 -04:00
João Marcos
9f0b09007b Rename LSP function and simplify tests (#27313)
While working on a fix I found opportunities to improve readability, but
it's a big rename diff, so I'm landing separately.

Release Notes:

- N/A
2025-03-22 19:23:11 +00:00
chbk
f4d1e7901c Improve Regex syntax highlighting (#25332)
Release Notes:

  - Improved Regex syntax highlighting

| Zed 0.174.6 | With this PR |
| --- | --- |
|
![Image](https://github.com/user-attachments/assets/2a0bb1d9-cbba-490c-b2be-08c94bc79517)
|
![Image](https://github.com/user-attachments/assets/b19f3871-ee83-4165-a026-207bdbbcad44)
|

- `(?P=`, `<`: `punctuation.bracket`
- `group_name`: `property` -> `label`
- `^`, `$`: `string.escape` -> `operator`
- `\b`, `\B`, `\k`: `string.escape` -> `keyword.operator`
- added `regex` scope to target regex tokens specifically

```js
regex = /^(?<group>[!\.\?])\b[a-z]+word\k<group>$/g
```

---------

Co-authored-by: João Marcos <marcospb19@hotmail.com>
2025-03-22 16:11:19 -03:00
Smit Barmase
044eb7b990 extensions_ui : Scroll to top on filter change (#27305)
Closes #27100


https://github.com/user-attachments/assets/0fc1409d-01c1-4caa-a2ed-762c8951930f

Release Notes:

- N/A
2025-03-22 18:38:01 +05:30
Smit Barmase
d96a50b029 extensions_ui: Add scrollbar (#27303)
This PR adds scrollbar to extensions page. 

For now haven't added setting to hide or configure this scrollbar, can
be handled later.

<img width="1258" alt="image"
src="https://github.com/user-attachments/assets/0d260051-5e4a-4e3f-9738-b5c5a988419e"
/>


Release Notes:

- Added scrollbar to extensions page.
2025-03-22 17:56:36 +05:30
Smit Barmase
b5e5959339 terminal: Make alternate_scroll on by default (#27302)
Most terminal emulators, like macOS Terminal, Alacritty, and Ghostty,
have alternate scroll turned on by default. I think it makes sense for
the Zed terminal to do the same and make it more of an opt-out feature.

Release Notes:

- N/A
2025-03-22 17:56:19 +05:30
João Marcos
9918b6cade Scroll to follow expanding part of editor::SelectLargerSyntaxNode (#27295)
When the selection grows both ways, the new code prioritizes the top
part instead of bottom one, this is usually more helpful considering
that most programming language grammars tend to define tokens right
before large delimited blocks, and rarely after (because humans and
parsers read from top to bottom).

Also, revert selection when convenient, so you have more control over
what you're selecting, looking at the selection `head` is commonly more
convenient than at the `tail`.

Release Notes:

- Improve scrolling of `editor::SelectLargerSyntaxNode` for better
visibility.
2025-03-22 09:06:13 +00:00
AidanV
fa677bdc38 vim: Single quote mark (#27231)
Closes #22398

Release Notes:

- vim: Adds `'` and `"` marks (last location jumped from in the current
buffer, and location when last exiting a buffer)

---------

Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
2025-03-22 05:45:57 +00:00
AidanV
d82b547596 vim: View Marks (#26885)
Closes #26884

Release Notes:

- vim: Added `:marks` which brings up list of current marks
- confirming on selected mark in the view jumps to that mark

---------

Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
2025-03-22 04:46:04 +00:00
Richard Feldman
4c86cda909 Prompt before running some tools (#27284)
Also includes some fixes for how the Lua tool was being generated.

<img width="644" alt="Screenshot 2025-03-21 at 6 26 18 PM"
src="https://github.com/user-attachments/assets/51bd1685-5b3f-4ed3-b11e-6fa8017847d4"
/>


Release Notes:

- N/A

---------

Co-authored-by: Ben <ben@zed.dev>
Co-authored-by: Agus Zubiaga <agus@zed.dev>
Co-authored-by: Joseph T. Lyons <JosephTLyons@gmail.com>
Co-authored-by: Danilo Leal <daniloleal09@gmail.com>
Co-authored-by: Ben Kunkle <ben.kunkle@gmail.com>
2025-03-22 00:05:34 -04:00
Conrad Irwin
90649fbc89 Automatically expand context for inline assistant to nearest block (#27282)
Release Notes:

- Inline assistant will now expand empty selections to the block under
the cursor.

Co-authored-by: andrew j <andjones100@gmail.com>
2025-03-21 21:24:27 -06:00
Ben Kunkle
c783fd072f zed: Add --system-specs arg (#27285)
Adds the `--system-specs` flag to the Zed binary, so that users who wish
to report issues can retrieve their system specs, even if Zed is failing
to launch

Still TODO:
- [x] Test and do best effort GPU info detection on Linux
- [ ] Modify GitHub issue templates to tell users that the flag is
available if they are unable to launch Zed

Release Notes:

- Added the `--system-specs` flag to the Zed binary (not the cli!), to
retrieve the system specs we ask for in GitHub issues without needing to
open Zed
2025-03-22 02:56:25 +00:00
Martin Fischer
85a761cb2b markdown_preview: Fix rendering image not at all or too often (#25592)
Before MarkdownParagraphChunk::Image was pushed for every Text event if
we're currently inside an image. This was wrong since pulldown-cmark
parses `![](foo)` as:

Start(Image { link_type: Inline, dest_url: "foo", title: "", id: "" })
    End(Image)

If there is no alt text, no Text event is emitted. Which caused images
without any alt text not to be rendered at all.

For alt texts containing inline formatting this was even more obviously
broken since e.g. `![foo *bar* baz](foo)` gets parsed as:

Start(Image { link_type: Inline, dest_url: "foo", title: "", id: "" })
      Text(Borrowed("foo "))
      Start(Emphasis)
        Text(Borrowed("bar"))
      End(Emphasis)
      Text(Borrowed(" baz"))
    End(Image)

which for this example caused the image to appear 3 times in the
preview.

This commit fixes these two bugs which have existed since the
introduction of the image previews in
96854c68ea.

Release Notes:

- Fixed images in the markdown preview appearing not at all or too
often.
2025-03-21 22:17:42 +01:00
Anthony Eid
739f45eb23 Clear breakpoints action (#27254)
This PR adds an action that clears all breakpoints and notifies any
active DAPs.

todo
- [x] Implement clear functionality
- [x] Write an integration test for this

Release Notes:

- N/A *or* Added/Fixed/Improved ...

---------

Co-authored-by: Piotr Osiewicz <peterosiewicz@gmail.com>
2025-03-21 20:18:08 +00:00
Ben Kunkle
16ad7424d6 zlog: Init (#27273)
Scaffolding for a revised way of logging in Zed. Very WIP, but the idea
is to allow maintainers to tell users to paste
```json
{
    "log": {
        "project.format": "trace"
    }
}
```
into their settings so that even trace logs are emitted for the log
statements emitted from a logger under the `project.format` scope.

The plan is to eventually implement the `Log` trait from the `log` crate
instead of just wrapping the `log` crate, which will simplify the
implementation greatly, and remove our need for both the `env_logger`
and `simplelog` crates.
Additionally, work will be done to transition to using the scoped
logging APIs throughout the app, focusing on bug hotspots to start
(currently, scoped logging is only used in the format codepath).

Release Notes:

- N/A
2025-03-21 20:08:03 +00:00
Danilo Leal
b32c792b68 assistant2: Polish spacing and alignment (#27264)
Release Notes:

- N/A

---------

Co-authored-by: Nathan Sobo <1789+nathansobo@users.noreply.github.com>
2025-03-21 16:17:32 -03:00
Sunli
1db621dc50 Refactor TextLayout to use Rc<RefCell> instead of Arc<Mutex> for improved performance (#27177)
Since `TextLayout` is not shared by multiple threads, changing it to
`Rc<RefCell<T>>` should improve performance.

I also found several codes with the same problem. If you think this
change is beneficial, I will continue to improve it in the subsequent
PR. 🙂

Release Notes:

- N/A
2025-03-21 15:06:22 -04:00
Nate Butler
6143da95fc editor: Fix regression in git label colors due to status color changes (#27272)
This PR fixes the new awkward-looking git status labels due to the
change in version control colors. We want to enable styling version
control colors distinctly from other statuses, but these colors aren't
great for labels as they are meant to be quite high contrast.

We may need to split version control colors into a primary color and a
text color if we want to improve theming this overall.

| Before | After |
|--------|-------|
| ![CleanShot 2025-03-21 at 14 12
22@2x](https://github.com/user-attachments/assets/fadb93b1-06b6-44cc-bf16-7e1279166ed0)
| ![CleanShot 2025-03-21 at 14 12
49@2x](https://github.com/user-attachments/assets/262ffc23-60b9-4cee-8a2b-9e864130912f)
|

Release Notes:

- Fixes a regression in git status colors in the project panel
2025-03-21 18:49:52 +00:00
João Marcos
7ced1b7a90 Fix strikethrough and underline in Linux (#27267)
Follow up to #26827 and #24721, which introduced a bug in Linux.

|before|now|
|---|---|

|![image](https://github.com/user-attachments/assets/6471502d-bf92-4808-ad42-9e0c66569d4f)|!![image](https://github.com/user-attachments/assets/ae45510a-8bc9-4f89-90a0-7496842fecb6)|


Release Notes:

- N/A

Co-authored-by: Jason Lee <huacnlee@gmail.com>
2025-03-21 17:17:25 +00:00
Antonio Scandurra
0e9e2d70cd Delete unused checkpoints (#27260)
Release Notes:

- N/A
2025-03-21 16:39:01 +00:00
Bennet Bo Fenner
a52e2f9553 Show claude-3-7-sonnet-thinking model for all users (#27256)
Release Notes:

- N/A
2025-03-21 17:23:36 +01:00
Conrad Irwin
a551a6139c Bump up default timeout (#27250)
Release Notes:

- Extended timeout used when connecting to remote instances
2025-03-21 10:04:39 -06:00
Danilo Leal
c394a3a890 Adjust multibuffer header fold button size (#27253)
This PR adjust the size of the fold button on the multibuffer header.
Had to make the `height` method public on the Button Like to pull that
off without other major changes.

| Before | After |
|--------|--------|
| ![CleanShot 2025-03-21 at 12  11
40@2x](https://github.com/user-attachments/assets/003b2965-b1cc-43ad-8528-2bd11cf0f9cc)
| ![CleanShot 2025-03-21 at 12  11
28@2x](https://github.com/user-attachments/assets/d4927b72-3f41-4c4b-9813-49e676170419)
|

Release Notes:

- N/A
2025-03-21 12:49:54 -03:00
Peter Tripp
1cca2e37b0 keymap: Remove backspace/delete as shortcuts for git::RestoreFile (#27257)
- See also: https://github.com/zed-industries/zed/pull/27004

Release Notes:

- N/A
2025-03-21 15:48:12 +00:00
Marshall Bowers
0de5c2ed53 assistant2: Order agent profiles in the order they are defined in settings (#27255)
This PR updates the ordering of the agent profiles in the tool selector
to respect the order they are defined in in the settings instead of
sorting them alphabetically.

This gives the user more control, and allows them to order the profiles
as they desire.

Release Notes:

- N/A
2025-03-21 11:38:02 -04:00
Kirill Bulatov
6397872c49 Persist editor folds between restarts (#27252)
Part of https://github.com/zed-industries/zed/issues/11626


https://github.com/user-attachments/assets/276cca5f-dd87-4496-b1b8-40b211f65aa7

Folds restoration between editor reopens will follow later

Release Notes:

- Started to persist editor folds between restarts
2025-03-21 15:28:11 +00:00
Anthony Eid
93bd32b425 Fix toggling breakpoints not working when text anchor isn't at start (#27249)
This fixes a bug where breakpoint's were unable to be toggled if the
text::Anchor representing the breakpoint position was not at the
beginning of a line.

Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-03-21 11:21:05 -04:00
Marshall Bowers
f119550838 assistant2: Define built-in agent profiles in the default settings (#27251)
This PR moves the definitions of the built-in agent profiles into the
default `settings.json`.

It also changes the behavior of how this setting is treated when merging
settings such that the set of profiles will be merged. This is so users
don't clobber the built-in profiles when adding profiles of their own.

Release Notes:

- N/A
2025-03-21 15:11:45 +00:00
Marshall Bowers
4e93e38b0a assistant2: Sort tools in the tool selector by ID (#27247)
This PR makes it so the tools in the tool selector are sorted by ID so
that they have a deterministic order.

Release Notes:

- N/A
2025-03-21 14:41:27 +00:00
Piotr Osiewicz
05aa8880a4 project: Track manifest locations per unique manifest locator (#27194)
This pull request paves way for exposing manifest tracking to
extensions.
- Project tree was renamed to manifest tree to better reflect it's
intent (and avoid confusion).
- Language server adapters now provide a name of their *manifest
locator*. If multiple language servers refer to the same locator, the
locating code will run just once for a given path.

Release Notes:

- N/A *or* Added/Fixed/Improved ...

---------

Co-authored-by: Anthony <anthony@zed.dev>
2025-03-21 15:22:36 +01:00
Marshall Bowers
6bced3a834 assistant2: Ensure scripting tool gets disabled when switching profiles (#27244)
This PR fixes an issue where the scripting tool wasn't being disabled
when switching to a profile that did not have it enabled.

Release Notes:

- N/A
2025-03-21 14:22:00 +00:00
Antonio Scandurra
e14ebcf267 Show "Restore Checkpoint" only when there were changes (#27243)
Release Notes:

- N/A

---------

Co-authored-by: Agus Zubiaga <hi@aguz.me>
Co-authored-by: Bennet Bo Fenner <bennetbo@gmx.de>
Co-authored-by: Danilo Leal <daniloleal09@gmail.com>
2025-03-21 15:10:43 +01:00
Peter Tripp
9d965bc98a Change default Markdown soft_wrap to "bounded" (#27205)
- Follow-up to: https://github.com/zed-industries/zed/pull/26247

Previously with defaults meant that Markdown would softwrap even if you
had available window space (soft_wrap occurred at default
`preferred_line_length` of 80).

Release Notes:

- Changed Markdown default to soft_wrap at window width instead of
preferred_line_length
2025-03-21 10:00:01 -04:00
Piotr Osiewicz
579868110b lsp: Fix workspace folders being cleared when new set is the same as the old one (#27242)
Release Notes:

- N/A
2025-03-21 13:47:06 +00:00
Bennet Bo Fenner
a709d4c7c6 assistant: Add support for claude-3-7-sonnet-thinking (#27085)
Closes #25671

Release Notes:

- Added support for `claude-3-7-sonnet-thinking` in the assistant panel

---------

Co-authored-by: Danilo Leal <daniloleal09@gmail.com>
Co-authored-by: Antonio Scandurra <me@as-cii.com>
Co-authored-by: Agus Zubiaga <hi@aguz.me>
2025-03-21 12:29:07 +00:00
Kirill Bulatov
2ffce4f516 Add non-blob columns to SQLite (#27236) 2025-03-21 12:04:59 +02:00
Smit Barmase
33fc1f4af2 languages: Fix JS/TS imports not showing correct suggestions after using period (#27235)
Closes #21728

This PR improves autocomplete for imports for all kinds of javascript
and typescript files.

Adds `.` as `completion_query_characters` which will make it act like
word for auto completion context. This allows capturing compete
`format.` as query.

Before:
<img width="500" alt="image"
src="https://github.com/user-attachments/assets/849fb342-db73-48e7-a9d8-93f0e5a14b58"
/>

After:
<img width="500" alt="image"
src="https://github.com/user-attachments/assets/e3ac3272-3217-4bcd-857f-4a83afc5980e"
/>

Release Notes:

- Improved autocomplete suggestions for JavaScript and TypeScript
imports.
2025-03-21 15:13:31 +05:30
David Barsky
7ade7d8e45 lsp-config: Allow setting a server's environment variables (#27213)
Closes https://github.com/zed-industries/zed/issues/14334, allowing
users to set environment variables for a language server binary like:

```json
"lsp": {
  "rust-analyzer": {
    "binary": {
      "path": "/Users/dbarsky/.cargo/bin/rust-analyzer",
      "env": {
        "RA_PROFILE": "*>100"
      }
    },
  }
}
```

The newly introduced environment variables are merged with the shell
environment. Perhaps more controversially, I've _also_ removed the
trimming/`stderr:`-prefixing of language server logs. This because
rust-analyzer has some nice, tree-shaped profiling built-in, and it
prevents us from printing profiles like this:

<details>
<img width="1147" alt="Screenshot 2025-03-20 at 12 09 14 PM"
src="https://github.com/user-attachments/assets/b7066651-6394-492b-b745-906c66d3c7b2"
/>
</details>

Release Notes:

- Added the ability to set a language server's environment variables.
- Removed the `stderr`-prefix of a language server's stderr logs.
2025-03-21 09:15:41 +02:00
Danilo Leal
8f86cd758a assistant2: Add design refinements (#27160)
Release Notes:

- N/A

---------

Co-authored-by: Bennet Bo Fenner <bennetbo@gmx.de>
Co-authored-by: Antonio Scandurra <me@as-cii.com>
Co-authored-by: Agus Zubiaga <hi@aguz.me>
2025-03-21 03:19:41 -03:00
迷渡
962709f42c docs: Change render function's return type (#27229) 2025-03-20 22:48:22 -06:00
Cole Miller
cf7d639fbc Migrate most callers of git-related worktree APIs to use the GitStore (#27225)
This is a pure refactoring PR that goes through all the git-related APIs
exposed by the worktree crate and minimizes their use outside that
crate, migrating callers of those APIs to read from the GitStore
instead. This is to prepare for evacuating git repository state from
worktrees and making the GitStore the new source of truth.

Other drive-by changes:

- `project::git` is now `project::git_store`, for consistency with the
other project stores
- the project panel's test module has been split into its own file

Release Notes:

- N/A

---------

Co-authored-by: Max Brunsfeld <maxbrunsfeld@gmail.com>
2025-03-21 00:10:17 -04:00
Smit Barmase
9134630841 extensions: Add copy author info button in context menu (#27221)
Closes #26108

Add "Copy Author Info" button to extension context menu.

Release Notes:

- Added option to copy extension author's name and email from extension
context menu.
2025-03-21 03:45:06 +05:30
Cole Miller
bc1c0a2297 Separate repository state synchronization from worktree synchronization (#27140)
This PR updates our DB schemas and wire protocol to separate the
synchronization of git statuses and other repository state from the
synchronization of worktrees. This paves the way for moving the code
that executes git status updates out of the `worktree` crate and onto
the new `GitStore`. That end goal is motivated by two (related) points:

- Disentangling git status updates from the worktree's
`BackgroundScanner` will allow us to implement a simpler concurrency
story for those updates, hopefully fixing some known but elusive bugs
(upstream state not updating after push; statuses getting out of sync in
remote projects).
- By moving git repository state to the project-scoped `GitStore`, we
can get rid of the duplication that currently happens when two worktrees
are associated with the same git repository.

Co-authored-by: Max <max@zed.dev>

Release Notes:

- N/A

---------

Co-authored-by: Max <max@zed.dev>
Co-authored-by: Max Brunsfeld <maxbrunsfeld@gmail.com>
2025-03-20 18:07:03 -04:00
Marshall Bowers
700af63c45 assistant2: Watch settings for changes to profiles (#27219)
This PR makes it so we watch the settings and update when the profiles
change.

Release Notes:

- N/A
2025-03-20 21:12:58 +00:00
Marshall Bowers
4b5df2189b assistant2: Allow creating agent profiles via settings (#27216)
This PR adds support for creating new agent profiles via the settings:

```json
{
  "assistant": {
    "profiles": {
      "lua": {
        "name": "Lua",
        "tools": {
          "lua-interpreter": true
        }
      },
      "lua-thinking": {
        "name": "Lua + Thinking",
        "tools": {
          "lua-interpreter": true,
          "thinking": true
        }
      }
    }
  }
}
```

Release Notes:

- N/A
2025-03-20 20:30:07 +00:00
Finn Evers
48b1a43f5e docs: Fix rendering of keybind in languages.md (#27217)
This fixes a broken keybind in the language extension docs: [Language
metadata](https://zed.dev/docs/extensions/languages#language-metadata) >
`line_comments`.

Release Notes:

- N/A
2025-03-20 20:25:54 +00:00
Kirill Bulatov
9609e04bb2 Add a way to copy with the selections trimmed (#27206)
No default binding currently, `cmd/ctr-shift-c` seem somewhat natural
but those are occupied by the collab panel.


https://github.com/user-attachments/assets/702cc52a-a4b7-4f2c-bb7f-12ca0c66faeb


Release Notes:

- Added a way to copy with the selections trimmed

---------

Co-authored-by: Cole Miller <m@cole-miller.net>
2025-03-20 19:58:51 +00:00
Anthony Eid
a74f2bb18b Reuse values from last debug panel inert state if they exist (#27211)
This should allow the team to iterate faster when using the debug panel
to set up a session

Release Notes:

- N/A
2025-03-20 18:53:11 +00:00
Remco Smits
ac452799b0 debugger: Fix shutdown issues (#27071)
This PR fixes a few issues around shutting down a debug adapter.

The first issue I discovered was when I shut down all sessions via
`shutdown all adapters` command. We would still fetch the threads
request again, because we receive a thread event that indicated that it
exited. But this will always time out because the debug adapter is
already shutdown at this point, so by updating the check so we don't
allow fetching a request when the session is terminated fixes the issue.

The second issue fixes a bug where we would always shut down the parent
session, when a child session is terminated. This was reintroduced by
the big refactor. This is not something we want, because you could
receive multiple StartDebugging reverse requests, so if one child is
shutting down that does not mean the other ones should have been
shutting down as well.
Issue was original fixed in
https://github.com/RemcoSmitsDev/zed/pull/80#issuecomment-2573943661.


## TODO:
- [x] Add tests

Release Notes:

- N/A
2025-03-20 18:32:37 +00:00
Smit Barmase
7b80cd865d Show more possible matches in code context completion (#27199)
Closes #24794

We now don't filter matches provided by the fuzzy matcher, as it already
performs most of the filtering for us. Instead, the custom logic we
previously used for filtering is now used to partition, where before
discarded matches will be appended at end of list.

Before - Filtering out matches with higher fuzzy score
<img width="400" alt="image"
src="https://github.com/user-attachments/assets/7f9d66a2-0921-499c-af8a-f1e530da50b1"
/>

After - Changing filter to partition instead, and appending remaining
items at the end
<img width="400" alt="image"
src="https://github.com/user-attachments/assets/45848f70-ed51-4935-976c-6c16c5b5777b"
/>


Release Notes:

- Improved LSP auto complete to show more possible matches.

---------

Co-authored-by: Peter Tripp <petertripp@gmail.com>
2025-03-20 23:46:20 +05:30
Joseph T. Lyons
7931b1d345 Pre-fill body of email with system specs (#27210)
I think we still want to be able to easily capture system spec info from
users. They can decide if they want to include it or not.

Release Notes:

- N/A
2025-03-20 18:09:10 +00:00
Marshall Bowers
27ebedf517 gpui: Make App::get_name return an Option (#27209)
This PR makes `App::get_name` return an `Option` instead of panicking if
the name is not set.

We'll let the caller be responsible for dealing with the absence of a
name.

Release Notes:

- N/A
2025-03-20 17:56:27 +00:00
Marshall Bowers
f9f5126d2c assistant2: Uniquely identify context server entries in configuration view (#27207)
This PR gives each context server entry in the configuration view a
unique element ID.

This fixes some issues where the disclosures and switches weren't
working properly due to element ID collisions.

Release Notes:

- N/A
2025-03-20 17:37:15 +00:00
Agus Zubiaga
6408ae81d1 assistant2: Return no-edits response to architect model (#27200)
Sometimes the editor model returns no search/replace blocks. This
usually happens when the architect model calls the edit tool before
reading any files. When this happens, we'll now return the raw response
from the editor model to the architect model so it can recover
accordingly.

Release Notes:

- N/A
2025-03-20 14:29:34 -03:00
Marshall Bowers
c60a7034c8 context_server: Interpret context server command paths relative to the extension's work dir (#27201)
This PR fixes an issues where the commands returned from context server
extensions were being used as-is instead of interpreting them relative
to the extension's work dir.

Release Notes:

- Fixed an issue with context server paths not being interpreted
relative to the extension's work dir.

---------

Co-authored-by: Max Brunsfeld <maxbrunsfeld@gmail.com>
Co-authored-by: Thomas Mickley-Doyle <tmickleydoyle@gmail.com>
2025-03-20 16:36:40 +00:00
Antonio Scandurra
7feb50fafe Add UI feedback for checkpoint restoration (#27203)
Release Notes:

- N/A

Co-authored-by: Agus Zubiaga <hi@aguz.me>
Co-authored-by: Bennet Bo Fenner <bennetbo@gmx.de>
2025-03-20 16:35:44 +00:00
Antonio Scandurra
f365b80814 Avoid polluting branch list and restore parent commit when using checkpoints (#27191)
Release Notes:

- N/A
2025-03-20 15:00:23 +00:00
Joseph T. Lyons
d0641a38a4 Rework feedback modal (#27186)
After our last community sync, we came to the conclusion that feedback
being sent outside of email is difficult to reply to. Our decision was
to use the old, tried and true email system, so that we can better
respond to people asking questions.

<img width="392" alt="SCR-20250320-igub"
src="https://github.com/user-attachments/assets/f1d01771-30eb-4b6f-b031-c68ddaac5700"
/>

Release Notes:

- N/A

---------

Co-authored-by: Danilo Leal <daniloleal09@gmail.com>
2025-03-20 10:28:43 -04:00
Agus Zubiaga
2e8c0ff244 assistant edit tool: Report when file is empty or doesn't exist (#27190)
Instead of just reporting a search match failure, we'll now indicate
whether the file is empty or exists to help the model recover better
from bad edits.


Release Notes:

- N/A
2025-03-20 10:42:10 -03:00
Danilo Leal
4421bdd12e assistant: Dismiss model picker upon selection (#27162)
This PR makes the model picker close when you click on a new item.

Release Notes:

- N/A
2025-03-20 10:22:49 -03:00
Peter Tripp
aa2fe9cce1 Add additional git-blame-ignore-revs (#27189)
Release Notes:

- N/A
2025-03-20 09:17:56 -04:00
Richard Feldman
e3578fc44a Display what the tool is doing (#27120)
<img width="639" alt="Screenshot 2025-03-19 at 4 56 47 PM"
src="https://github.com/user-attachments/assets/b997f04d-4aff-4070-87b1-ffdb61019bd1"
/>

Release Notes:

- N/A

---------

Co-authored-by: Agus Zubiaga <hi@aguz.me>
2025-03-20 09:16:39 -04:00
Kirill Bulatov
aae81fd54c Notify about broken task file contents (#27185)
Closes https://github.com/zed-industries/zed/issues/23783


https://github.com/user-attachments/assets/df019f68-a76b-4953-967a-a35ed21206ab

Release Notes:

- Added notifications when invalid tasks.json/debug.json is saved
2025-03-20 13:06:10 +00:00
Kirill Bulatov
de99febd9b debugger: Ensure both debug and regular global tasks are correctly merged (#27184)
Follow-up of https://github.com/zed-industries/zed/pull/13433
Closes https://github.com/zed-industries/zed/issues/27124
Closes https://github.com/zed-industries/zed/issues/27066

After this change, both old global task source, `tasks.json` and new,
`debug.json` started to call for the same task update method:


14920ab910/crates/project/src/task_inventory.rs (L414)

erasing previous declarations.

The PR puts this data under different paths instead and adjusts the code
around it.

Release Notes:

- Fixed custom tasks not shown
2025-03-20 12:51:26 +00:00
Kirill Bulatov
5bef32f3ed When determining Python task context, do not consider worktree-less files as an error (#27183)
Makes Python plugin to output


![image](https://github.com/user-attachments/assets/4960bc48-21b7-4392-82b9-18bfd1dd9cd0)

for standalone Python files now, instead of nothing as now.

Before the change, no task context was created for the standalone file
due to `VariableName::RelativeFile` lookup considered as an error.
Now, Zed continues and constructs whatever possible context instead.

That `pytest` task seems odd, as the logic fixed here needs a relative
path (hence, a worktree) to consider unit tests.
We do not have variables at the moment the associated tasks are queried
for:


14920ab910/crates/languages/src/python.rs (L359-L363)


14920ab910/crates/languages/src/python.rs (L417-L446)

so we cannot filter this the same way the PR does.
Maybe, we can use a `VariableName::RelativeFile` instead of
`VariableName::File` there?

Release Notes:

- Show tasks from Python plugin for standalone files
2025-03-20 12:44:07 +00:00
Smit Barmase
23e8519057 Add completion_query_characters in language (#27175)
Closes #18581

Now characters for completing query and word characters, which are
responsible for selecting words by double clicking or navigating, are
different. This fixes a bunch of things:

For settings.json, this improves completions to treat the whole string
as a completion query, instead of just the last word. We now added
"space" as a completion query character without it being a word
character.

For keymap.json, this improves selecting part of an action as the ":"
character is only a completion character and not a word character. So,
completions would still trigger on ":" and query capture will treat ":"
as a word, but for actions like selections and navigation, ":" will be
treated as punctuation.

Before:
Unnecessary related suggestions as query is only the last word which is
"d".
<img width="300" alt="image"
src="https://github.com/user-attachments/assets/8199a715-7521-49dd-948b-e6aaed04c488"
/>

Double clicking `ToggleFold` selects the whole action:
<img width="300" alt="image"
src="https://github.com/user-attachments/assets/c7f91a6b-06d5-45b6-9d59-61a1b2deda71"
/>

After:
Now query is "one d" and it shows only matched ones.
<img width="300" alt="image"
src="https://github.com/user-attachments/assets/1455dfbc-9906-42e8-b8aa-b3f551194ca2"
/>

Double clicking `ToggleFold` only selects part of the action, which is
more refined behavior.
<img width="300" alt="image"
src="https://github.com/user-attachments/assets/34b1c3c2-184f-402f-9dc8-73030a8c370f"
/>

Release Notes:

- Improved autocomplete suggestions in `settings.json`, now whole string
is queried instead of just last word of string, which filters out lot of
false positives.
- Improved selection of action in `keymap.json`, where now you can
double click to only select certain part of action, instead of selecting
whole action.

---------

Co-authored-by: Max Brunsfeld <maxbrunsfeld@gmail.com>
Co-authored-by: Ben Kunkle <ben@zed.dev>
2025-03-20 16:45:35 +05:30
Michael Sloan
1180b6fbc7 Initial support for AI assistant rules files (#27168)
Release Notes:

- N/A

---------

Co-authored-by: Danilo <danilo@zed.dev>
Co-authored-by: Nathan <nathan@zed.dev>
Co-authored-by: Thomas <thomas@zed.dev>
2025-03-20 08:30:04 +00:00
renovate[bot]
14920ab910 Update swatinem/rust-cache digest to 9d47c6a (#27121)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [swatinem/rust-cache](https://redirect.github.com/swatinem/rust-cache)
| action | digest | `f0deed1` -> `9d47c6a` |

---

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

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-20 07:58:22 +02:00
renovate[bot]
000b981cb4 Update Rust crate rustls-platform-verifier to v0.5.1 (#27136)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
|
[rustls-platform-verifier](https://redirect.github.com/rustls/rustls-platform-verifier)
| workspace.dependencies | patch | `0.5.0` -> `0.5.1` |

---

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

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-20 07:52:49 +02:00
renovate[bot]
c9bff6e762 Update Rust crate sea-orm to v1.1.7 (#27137)
This PR contains the following updates:

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

---

### Release Notes

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

###
[`v1.1.7`](https://redirect.github.com/SeaQL/sea-orm/blob/HEAD/CHANGELOG.md#117---2025-03-02)

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

##### New Features

- Support nested entities in `FromQueryResult`
[https://github.com/SeaQL/sea-orm/pull/2508](https://redirect.github.com/SeaQL/sea-orm/pull/2508)

```rust

#[derive(FromQueryResult)]
struct Cake {
    id: i32,
    name: String,
    #[sea_orm(nested)]
    bakery: Option<CakeBakery>,
}

#[derive(FromQueryResult)]
struct CakeBakery {
    #[sea_orm(from_alias = "bakery_id")]
    id: i32,
    #[sea_orm(from_alias = "bakery_name")]
    title: String,
}

let cake: Cake = cake::Entity::find()
    .select_only()
    .column(cake::Column::Id)
    .column(cake::Column::Name)
    .column_as(bakery::Column::Id, "bakery_id")
    .column_as(bakery::Column::Name, "bakery_name")
    .left_join(bakery::Entity)
    .order_by_asc(cake::Column::Id)
    .into_model()
    .one(&ctx.db)
    .await?
    .unwrap();

assert_eq!(
    cake,
    Cake {
        id: 1,
        name: "Cake".to_string(),
        bakery: Some(CakeBakery {
            id: 20,
            title: "Bakery".to_string(),
        })
    }
);
```

- Support nested entities in `DerivePartialModel`
[https://github.com/SeaQL/sea-orm/pull/2508](https://redirect.github.com/SeaQL/sea-orm/pull/2508)

```rust

#[derive(DerivePartialModel)] // FromQueryResult is no longer needed
#[sea_orm(entity = "cake::Entity", from_query_result)]
struct Cake {
    id: i32,
    name: String,
    #[sea_orm(nested)]
    bakery: Option<Bakery>,
}

#[derive(DerivePartialModel)]

#[sea_orm(entity = "bakery::Entity", from_query_result)]
struct Bakery {
    id: i32,
    #[sea_orm(from_col = "Name")]
    title: String,
}

// same as previous example, but without the custom selects
let cake: Cake = cake::Entity::find()
    .left_join(bakery::Entity)
    .order_by_asc(cake::Column::Id)
    .into_partial_model()
    .one(&ctx.db)
    .await?
    .unwrap();

assert_eq!(
    cake,
    Cake {
        id: 1,
        name: "Cake".to_string(),
        bakery: Some(CakeBakery {
            id: 20,
            title: "Bakery".to_string(),
        })
    }
);
```

- Derive also `IntoActiveModel` with `DerivePartialModel`
[https://github.com/SeaQL/sea-orm/pull/2517](https://redirect.github.com/SeaQL/sea-orm/pull/2517)

```rust

#[derive(DerivePartialModel)]
#[sea_orm(entity = "cake::Entity", into_active_model)]
struct Cake {
    id: i32,
    name: String,
}

assert_eq!(
    Cake {
        id: 12,
        name: "Lemon Drizzle".to_owned(),
    }
    .into_active_model(),
    cake::ActiveModel {
        id: Set(12),
        name: Set("Lemon Drizzle".to_owned()),
        ..Default::default()
    }
);
```

- Added `SelectThree`
[https://github.com/SeaQL/sea-orm/pull/2518](https://redirect.github.com/SeaQL/sea-orm/pull/2518)

```rust
// Order -> (many) Lineitem -> Cake
let items: Vec<(order::Model, Option<lineitem::Model>, Option<cake::Model>)> =
    order::Entity::find()
        .find_also_related(lineitem::Entity)
        .and_also_related(cake::Entity)
        .order_by_asc(order::Column::Id)
        .order_by_asc(lineitem::Column::Id)
        .all(&ctx.db)
        .await?;
```

##### Enhancements

- Support complex type path in `DeriveIntoActiveModel`
[https://github.com/SeaQL/sea-orm/pull/2517](https://redirect.github.com/SeaQL/sea-orm/pull/2517)

```rust

#[derive(DeriveIntoActiveModel)]
#[sea_orm(active_model = "<fruit::Entity as EntityTrait>::ActiveModel")]
struct Fruit {
    cake_id: Option<Option<i32>>,
}
```

- Added `DatabaseConnection::close_by_ref`
[https://github.com/SeaQL/sea-orm/pull/2511](https://redirect.github.com/SeaQL/sea-orm/pull/2511)

```rust
pub async fn close(self) -> Result<(), DbErr> { .. } // existing
pub async fn close_by_ref(&self) -> Result<(), DbErr> { .. } // new
```

##### House Keeping

- Cleanup legacy `ActiveValue::Set`
[https://github.com/SeaQL/sea-orm/pull/2515](https://redirect.github.com/SeaQL/sea-orm/pull/2515)

###
[`v1.1.6`](https://redirect.github.com/SeaQL/sea-orm/blob/HEAD/CHANGELOG.md#116---2025-02-24)

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

##### New Features

- Support PgVector (under feature flag `postgres-vector`)
[https://github.com/SeaQL/sea-orm/pull/2500](https://redirect.github.com/SeaQL/sea-orm/pull/2500)

```rust
// Model

#[derive(Clone, Debug, PartialEq, DeriveEntityModel)]
#[sea_orm(table_name = "image_model")]
pub struct Model {
    #[sea_orm(primary_key, auto_increment = false)]
    pub id: i32,
    pub embedding: PgVector,
}
 
// Schema
sea_query::Table::create()
    .table(image_model::Entity.table_ref())
    .col(ColumnDef::new(Column::Id).integer().not_null().primary_key())
    .col(ColumnDef::new(Column::Embedding).vector(None).not_null())
    ..

// Insert
ActiveModel {
    id: NotSet,
    embedding: Set(PgVector::from(vec![1., 2., 3.])),
}
.insert(db)
.await?
```

- Added `Insert::exec_with_returning_keys` &
`Insert::exec_with_returning_many` (Postgres only)

```rust
assert_eq!(
    Entity::insert_many([
        ActiveModel { id: NotSet, name: Set("two".into()) },
        ActiveModel { id: NotSet, name: Set("three".into()) },
    ])
    .exec_with_returning_many(db)
    .await
    .unwrap(),
    [
        Model { id: 2, name: "two".into() },
        Model { id: 3, name: "three".into() },
    ]
);

assert_eq!(
    cakes_bakers::Entity::insert_many([
        cakes_bakers::ActiveModel {
            cake_id: Set(1),
            baker_id: Set(2),
        },
        cakes_bakers::ActiveModel {
            cake_id: Set(2),
            baker_id: Set(1),
        },
    ])
    .exec_with_returning_keys(db)
    .await
    .unwrap(),
    [(1, 2), (2, 1)]
);
```

- Added `DeleteOne::exec_with_returning` &
`DeleteMany::exec_with_returning`
[https://github.com/SeaQL/sea-orm/pull/2432](https://redirect.github.com/SeaQL/sea-orm/pull/2432)

##### Enhancements

- Expose underlying row types (e.g. `sqlx::postgres::PgRow`)
[https://github.com/SeaQL/sea-orm/pull/2265](https://redirect.github.com/SeaQL/sea-orm/pull/2265)
- \[sea-orm-cli] Added `acquire-timeout` option
[https://github.com/SeaQL/sea-orm/pull/2461](https://redirect.github.com/SeaQL/sea-orm/pull/2461)
- \[sea-orm-cli] Added `with-prelude` option
[https://github.com/SeaQL/sea-orm/pull/2322](https://redirect.github.com/SeaQL/sea-orm/pull/2322)
- \[sea-orm-cli] Added `impl-active-model-behavior` option
[https://github.com/SeaQL/sea-orm/pull/2487](https://redirect.github.com/SeaQL/sea-orm/pull/2487)

##### Bug Fixes

- Fixed `seaography::register_active_enums` macro
[https://github.com/SeaQL/sea-orm/pull/2475](https://redirect.github.com/SeaQL/sea-orm/pull/2475)

##### House keeping

- Remove `futures` crate, replace with `futures-util`
[https://github.com/SeaQL/sea-orm/pull/2466](https://redirect.github.com/SeaQL/sea-orm/pull/2466)

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

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-20 07:52:35 +02:00
renovate[bot]
9fd2d064ee Update Rust crate mimalloc to v0.1.44 (#27131)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [mimalloc](https://redirect.github.com/purpleprotocol/mimalloc_rust) |
dependencies | patch | `0.1.43` -> `0.1.44` |

---

### Release Notes

<details>
<summary>purpleprotocol/mimalloc_rust (mimalloc)</summary>

###
[`v0.1.44`](https://redirect.github.com/purpleprotocol/mimalloc_rust/releases/tag/v0.1.44):
Version 0.1.44

[Compare
Source](https://redirect.github.com/purpleprotocol/mimalloc_rust/compare/v0.1.43...v0.1.44)

##### Changes

-   Mimalloc v2.2.2

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

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-20 07:48:35 +02:00
renovate[bot]
11425cf5f1 Update Rust crate unindent to v0.2.4 (#27151)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [unindent](https://redirect.github.com/dtolnay/indoc) |
workspace.dependencies | patch | `0.2.3` -> `0.2.4` |

---

### Release Notes

<details>
<summary>dtolnay/indoc (unindent)</summary>

###
[`v0.2.4`](https://redirect.github.com/dtolnay/indoc/releases/tag/0.2.4)

[Compare
Source](https://redirect.github.com/dtolnay/indoc/compare/0.2.3...0.2.4)

-   Update to Syn 0.13

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

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-20 07:42:39 +02:00
renovate[bot]
b54c92079f Update Rust crate winresource to v0.1.20 (#27152)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [winresource](https://redirect.github.com/BenjaminRi/winresource) |
build-dependencies | patch | `0.1.19` -> `0.1.20` |

---

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

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-20 07:42:20 +02:00
renovate[bot]
3bbdc546ec Update Rust crate time to v0.3.40 (#27147)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [time](https://time-rs.github.io)
([source](https://redirect.github.com/time-rs/time)) |
workspace.dependencies | patch | `0.3.37` -> `0.3.40` |

---

### Release Notes

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

###
[`v0.3.40`](https://redirect.github.com/time-rs/time/blob/HEAD/CHANGELOG.md#0340-2025-03-18)

[Compare
Source](https://redirect.github.com/time-rs/time/compare/v0.3.39...v0.3.40)

##### Added

- Visibility modifiers may now be added to the `mod` generated by
`time::sere::format_description!`.

###
[`v0.3.39`](https://redirect.github.com/time-rs/time/blob/HEAD/CHANGELOG.md#0339-2025-03-06)

[Compare
Source](https://redirect.github.com/time-rs/time/compare/v0.3.38...v0.3.39)

##### Fixed

-   Doc tests run successfully with the default feature set.
-   wasm builds work again.

Both of these were regressions in v0.3.38 and are now checked in CI.

###
[`v0.3.38`](https://redirect.github.com/time-rs/time/blob/HEAD/CHANGELOG.md#0338-2025-03-05)

[Compare
Source](https://redirect.github.com/time-rs/time/compare/v0.3.37...v0.3.38)

##### Added

- The `[year]` component (in format descriptions) now supports a `range`
modifier, which can be
either `standard` or `extended`. The default is `extended` for backwards
compatibility. This is
intended as a manner to opt *out* of the extended range when the
`large-dates` feature is enabled.
When the `large-dates` feature is not enabled, the modifier has no
effect.
- `UtcDateTime`, which is semantically equivalent to an `OffsetDateTime`
with UTC as its offset. The
advantage is that it is the same size as a `PrimitiveDateTime` and has
improved operability with
    well-known formats.

    As part of this, there were some other additions:

- `utc_datetime!` macro, which is similar to the `datetime!` macro but
constructs a `UtcDateTime`.
    -   `PrimitiveDateTime::as_utc`
    -   `OffsetDateTime::to_utc`
    -   `OffsetDateTime::checked_to_utc`
- `time::serde::timestamp::milliseconds_i64`, which is a module to
serialize/deserialize timestamps
as the Unix timestamp. The pre-existing module does this as an `i128`
where an `i64` would
    suffice. This new module should be preferred.

##### Changed

- `error::Format` has had its `source()` implementation changed to no
longer return a boxed value
from the `ComponentRange` variant. If you were explicitly expecting
this, you will need to update
    your code. The method API remains unchanged.
-   `[year repr:century]` supports single-digit values.
-   All `format_into` methods accept `?Sized` references.

##### Miscellaneous

- Some non-exhaustive enum variants that are no longer used have been
modified to be statically
proven as uninhabited. The relevant fields are doc-hidden and not
semver-guaranteed to remain as
    such, though it is unlikely to change.
-   An unnecessary check when parsing RFC 2822 has been removed.
- Various methods have had their implementations changed, resulting in
significant performance
    gains. Among the methods changed are
    -   `util::is_leap_year`
    -   `util::weeks_in_year`
    -   `Month::length`
    -   `Date::to_calendar_date`
    -   `Date::month`
    -   `Date::day`
    -   `Date::from_julian_day`
    -   `Date::to_julian_day`
    -   other methods that call into these methods

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

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-19 23:36:33 -04:00
renovate[bot]
e4e3ce6a38 Update Rust crate serde_repr to v0.1.20 (#27146)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [serde_repr](https://redirect.github.com/dtolnay/serde-repr) |
workspace.dependencies | patch | `0.1.19` -> `0.1.20` |

---

### Release Notes

<details>
<summary>dtolnay/serde-repr (serde_repr)</summary>

###
[`v0.1.20`](https://redirect.github.com/dtolnay/serde-repr/releases/tag/0.1.20)

[Compare
Source](https://redirect.github.com/dtolnay/serde-repr/compare/0.1.19...0.1.20)

-   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:eyJjcmVhdGVkSW5WZXIiOiIzOS4yMDcuMSIsInVwZGF0ZWRJblZlciI6IjM5LjIwNy4xIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6W119-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-19 23:35:25 -04:00
renovate[bot]
8cd96cbf59 Update Rust crate serde_json to v1.0.140 (#27144)
This PR contains the following updates:

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

---

### Release Notes

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

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

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

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

---

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

---

Release Notes:

- N/A

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzOS4yMDcuMSIsInVwZGF0ZWRJblZlciI6IjM5LjIwNy4xIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6W119-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-19 23:35:21 -04:00
Ben Kunkle
274124256d Fix code action formatters creating separate transaction (#26311)
Closes #24588
Closes #25419

Restructures `LspStore.format_local` a decent bit in order to make how
the transaction history is preserved more clear, and in doing so fix
various bugs with how the transaction history is handled during a format
request (especially when formatting in remote dev)

Release Notes:

- Fixed an issue that prevented formatting from working when working
with remote dev
- Fixed an issue when using code actions as a format step where the
edits made by the code actions would not be grouped with the other
format edits in the undo history
2025-03-19 20:59:43 -05:00
381 changed files with 30339 additions and 17795 deletions

View File

@@ -19,6 +19,10 @@
# https://github.com/zed-industries/zed/pull/2394
eca93c124a488b4e538946cd2d313bd571aa2b86
# 2024-02-15 Format YAML files
# https://github.com/zed-industries/zed/pull/7887
a161a7d0c95ca7505bf9218bfae640ee5444c88b
# 2024-02-25 Format JSON files in assets/
# https://github.com/zed-industries/zed/pull/8405
ffdda588b41f7d9d270ffe76cab116f828ad545e

View File

@@ -235,7 +235,7 @@ jobs:
clean: false
- name: Cache dependencies
uses: swatinem/rust-cache@f0deed1e0edfc6a9be95417288c0e1099b1eeec3 # v2
uses: swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2
with:
save-if: ${{ github.ref == 'refs/heads/main' }}
cache-provider: "buildjet"
@@ -287,7 +287,7 @@ jobs:
clean: false
- name: Cache dependencies
uses: swatinem/rust-cache@f0deed1e0edfc6a9be95417288c0e1099b1eeec3 # v2
uses: swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2
with:
save-if: ${{ github.ref == 'refs/heads/main' }}
cache-provider: "buildjet"
@@ -334,7 +334,7 @@ jobs:
Copy-Item -Path "${{ github.workspace }}" -Destination "${{ env.ZED_WORKSPACE }}" -Recurse
- name: Cache dependencies
uses: swatinem/rust-cache@f0deed1e0edfc6a9be95417288c0e1099b1eeec3 # v2
uses: swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2
with:
save-if: ${{ github.ref == 'refs/heads/main' }}
workspaces: ${{ env.ZED_WORKSPACE }}
@@ -393,7 +393,7 @@ jobs:
Copy-Item -Path "${{ github.workspace }}" -Destination "${{ env.ZED_WORKSPACE }}" -Recurse
- name: Cache dependencies
uses: swatinem/rust-cache@f0deed1e0edfc6a9be95417288c0e1099b1eeec3 # v2
uses: swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2
with:
save-if: ${{ github.ref == 'refs/heads/main' }}
workspaces: ${{ env.ZED_WORKSPACE }}

View File

@@ -1,7 +1,7 @@
name: "Close Stale Issues"
on:
schedule:
- cron: "0 7,9,11 * * 2"
- cron: "0 7,9,11 * * 3"
workflow_dispatch:
jobs:

View File

@@ -22,7 +22,7 @@ jobs:
clean: false
- name: Cache dependencies
uses: swatinem/rust-cache@f0deed1e0edfc6a9be95417288c0e1099b1eeec3 # v2
uses: swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2
with:
save-if: ${{ github.ref == 'refs/heads/main' }}
cache-provider: "github"

View File

@@ -182,8 +182,7 @@ jobs:
runner: buildjet-16vcpu-ubuntu-2204
install_nix: true
- os: arm Mac
# TODO: once other macs are provisioned for nix, remove that constraint from the runner
runner: [macOS, ARM64, nix]
runner: [macOS, ARM64, test]
install_nix: false
- os: arm Linux
runner: buildjet-16vcpu-ubuntu-2204-arm

659
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -70,6 +70,7 @@ members = [
"crates/html_to_markdown",
"crates/http_client",
"crates/http_client_tls",
"crates/icons",
"crates/image_viewer",
"crates/indexed_docs",
"crates/inline_completion",
@@ -124,7 +125,6 @@ members = [
"crates/rope",
"crates/rpc",
"crates/schema_generator",
"crates/scripting_tool",
"crates/search",
"crates/semantic_index",
"crates/semantic_version",
@@ -171,6 +171,8 @@ members = [
"crates/zed",
"crates/zed_actions",
"crates/zeta",
"crates/zlog",
"crates/zlog_settings",
#
# Extensions
@@ -273,6 +275,7 @@ gpui_tokio = { path = "crates/gpui_tokio" }
html_to_markdown = { path = "crates/html_to_markdown" }
http_client = { path = "crates/http_client" }
http_client_tls = { path = "crates/http_client_tls" }
icons = { path = "crates/icons" }
image_viewer = { path = "crates/image_viewer" }
indexed_docs = { path = "crates/indexed_docs" }
inline_completion = { path = "crates/inline_completion" }
@@ -327,7 +330,6 @@ reqwest_client = { path = "crates/reqwest_client" }
rich_text = { path = "crates/rich_text" }
rope = { path = "crates/rope" }
rpc = { path = "crates/rpc" }
scripting_tool = { path = "crates/scripting_tool" }
search = { path = "crates/search" }
semantic_index = { path = "crates/semantic_index" }
semantic_version = { path = "crates/semantic_version" }
@@ -374,6 +376,8 @@ worktree = { path = "crates/worktree" }
zed = { path = "crates/zed" }
zed_actions = { path = "crates/zed_actions" }
zeta = { path = "crates/zeta" }
zlog = { path = "crates/zlog" }
zlog_settings = { path = "crates/zlog_settings" }
#
# External crates
@@ -609,7 +613,7 @@ features = [
]
[workspace.dependencies.windows]
version = "0.60"
version = "0.61"
features = [
"Foundation_Collections",
"Foundation_Numerics",

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-arrow-right-left"><path d="m16 3 4 4-4 4"/><path d="M20 7H4"/><path d="m8 21-4-4 4-4"/><path d="M4 17h16"/></svg>

After

Width:  |  Height:  |  Size: 316 B

View File

@@ -0,0 +1,3 @@
<svg width="8" height="8" viewBox="0 0 8 8" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M5.4 2.6H5.75C5.75 2.50717 5.71312 2.41815 5.64749 2.35251C5.58185 2.28688 5.49283 2.25 5.4 2.25V2.6ZM2.6 2.25C2.4067 2.25 2.25 2.4067 2.25 2.6C2.25 2.7933 2.4067 2.95 2.6 2.95V2.25ZM5.05 5.4C5.05 5.5933 5.2067 5.75 5.4 5.75C5.5933 5.75 5.75 5.5933 5.75 5.4H5.05ZM2.35252 5.15251C2.21583 5.2892 2.21583 5.5108 2.35252 5.64748C2.4892 5.78417 2.7108 5.78417 2.84749 5.64748L2.35252 5.15251ZM5.4 2.25H2.6V2.95H5.4V2.25ZM5.05 2.6V5.4H5.75V2.6H5.05ZM5.15252 2.35251L2.35252 5.15251L2.84749 5.64748L5.64749 2.84748L5.15252 2.35251Z" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 650 B

1
assets/icons/brain.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-brain"><path d="M12 5a3 3 0 1 0-5.997.125 4 4 0 0 0-2.526 5.77 4 4 0 0 0 .556 6.588A4 4 0 1 0 12 18Z"/><path d="M12 5a3 3 0 1 1 5.997.125 4 4 0 0 1 2.526 5.77 4 4 0 0 1-.556 6.588A4 4 0 1 1 12 18Z"/><path d="M15 13a4.5 4.5 0 0 1-3-4 4.5 4.5 0 0 1-3 4"/><path d="M17.599 6.5a3 3 0 0 0 .399-1.375"/><path d="M6.003 5.125A3 3 0 0 0 6.401 6.5"/><path d="M3.477 10.896a4 4 0 0 1 .585-.396"/><path d="M19.938 10.5a4 4 0 0 1 .585.396"/><path d="M6 18a4 4 0 0 1-1.967-.516"/><path d="M19.967 17.484A4 4 0 0 1 18 18"/></svg>

After

Width:  |  Height:  |  Size: 718 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-clipboard"><rect width="8" height="4" x="8" y="2" rx="1" ry="1"/><path d="M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2"/></svg>

After

Width:  |  Height:  |  Size: 358 B

1
assets/icons/cog.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-cog"><path d="M12 20a8 8 0 1 0 0-16 8 8 0 0 0 0 16Z"/><path d="M12 14a2 2 0 1 0 0-4 2 2 0 0 0 0 4Z"/><path d="M12 2v2"/><path d="M12 22v-2"/><path d="m17 20.66-1-1.73"/><path d="M11 10.27 7 3.34"/><path d="m20.66 17-1.73-1"/><path d="m3.34 7 1.73 1"/><path d="M14 12h8"/><path d="M2 12h2"/><path d="m20.66 7-1.73 1"/><path d="m3.34 17 1.73-1"/><path d="m17 3.34-1 1.73"/><path d="m11 13.73-4 6.93"/></svg>

After

Width:  |  Height:  |  Size: 608 B

View File

@@ -0,0 +1,5 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M10.0001 1.33334H4.00008C3.64646 1.33334 3.30732 1.47382 3.05727 1.72387C2.80722 1.97392 2.66675 2.31305 2.66675 2.66668V13.3333C2.66675 13.687 2.80722 14.0261 3.05727 14.2762C3.30732 14.5262 3.64646 14.6667 4.00008 14.6667H12.0001C12.3537 14.6667 12.6928 14.5262 12.9429 14.2762C13.1929 14.0261 13.3334 13.687 13.3334 13.3333V4.66668L10.0001 1.33334Z" stroke="black" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M6 8H10" stroke="black" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M8 10V6" stroke="black" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 762 B

View File

@@ -0,0 +1,5 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M10.0001 1.33334H4.00008C3.64646 1.33334 3.30732 1.47382 3.05727 1.72387C2.80722 1.97392 2.66675 2.31305 2.66675 2.66668V13.3333C2.66675 13.687 2.80722 14.0261 3.05727 14.2762C3.30732 14.5262 3.64646 14.6667 4.00008 14.6667H12.0001C12.3537 14.6667 12.6928 14.5262 12.9429 14.2762C13.1929 14.0261 13.3334 13.687 13.3334 13.3333V4.66668L10.0001 1.33334Z" stroke="black" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M9.66659 6.5L6.33325 9.83333" stroke="black" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M6.33325 6.5L9.66659 9.83333" stroke="black" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 804 B

View File

@@ -195,7 +195,7 @@
"ctrl-shift-g": "search::SelectPreviousMatch",
"ctrl-alt-/": "assistant::ToggleModelSelector",
"ctrl-k h": "assistant::DeployHistory",
"ctrl-k l": "assistant::DeployPromptLibrary",
"ctrl-k l": "assistant::OpenPromptLibrary",
"new": "assistant::NewChat",
"ctrl-t": "assistant::NewChat",
"ctrl-n": "assistant::NewChat"
@@ -754,9 +754,11 @@
"escape": "git_panel::ToggleFocus",
"ctrl-enter": "git::Commit",
"alt-enter": "menu::SecondaryConfirm",
"delete": "git::RestoreFile",
"shift-delete": "git::RestoreFile",
"backspace": "git::RestoreFile"
"delete": ["git::RestoreFile", { "skip_prompt": false }],
"backspace": ["git::RestoreFile", { "skip_prompt": false }],
"shift-delete": ["git::RestoreFile", { "skip_prompt": false }],
"ctrl-backspace": ["git::RestoreFile", { "skip_prompt": false }],
"ctrl-delete": ["git::RestoreFile", { "skip_prompt": false }]
}
},
{

View File

@@ -241,7 +241,7 @@
"cmd-shift-g": "search::SelectPreviousMatch",
"cmd-alt-/": "assistant::ToggleModelSelector",
"cmd-k h": "assistant::DeployHistory",
"cmd-k l": "assistant::DeployPromptLibrary",
"cmd-k l": "assistant::OpenPromptLibrary",
"cmd-t": "assistant::NewChat",
"cmd-n": "assistant::NewChat"
}
@@ -287,7 +287,9 @@
"context": "MessageEditor > Editor",
"use_key_equivalents": true,
"bindings": {
"enter": "assistant2::Chat"
"enter": "assistant2::Chat",
"cmd-g d": "git::Diff",
"shift-escape": "git::ExpandCommitEditor"
}
},
{
@@ -801,9 +803,10 @@
"shift-tab": "git_panel::FocusEditor",
"escape": "git_panel::ToggleFocus",
"cmd-enter": "git::Commit",
"delete": "git::RestoreFile",
"cmd-backspace": "git::RestoreFile",
"backspace": "git::RestoreFile"
"backspace": ["git::RestoreFile", { "skip_prompt": false }],
"delete": ["git::RestoreFile", { "skip_prompt": false }],
"cmd-backspace": ["git::RestoreFile", { "skip_prompt": true }],
"cmd-delete": ["git::RestoreFile", { "skip_prompt": true }]
}
},
{

View File

@@ -8,11 +8,27 @@ It will be up to you to decide which of these you are doing based on what the us
You should only perform actions that modify the users system if explicitly requested by the user:
- If the user asks a question about how to accomplish a task, provide guidance or information, and use read-only tools (e.g., search) to assist. You may suggest potential actions, but do not directly modify the users system without explicit instruction.
- If the user clearly requests that you perform an action, carry out the action directly without explaining why you are doing so.
- The editing actions you perform might produce errors or warnings. At the end of your changes, check whether you introduced any problems, and fix them before providing a summary of the changes you made.
- Do not fix errors unrelated to your changes unless the user explicitly asks you to do so.
Be concise and direct in your responses.
The user has opened a project that contains the following root directories/files:
The user has opened a project that contains the following root directories/files. Whenever you specify a path in the project, it must be a relative path which begins with one of these root directories/files:
{{#each worktrees}}
- {{root_name}} (absolute path: {{abs_path}})
- `{{root_name}}` (absolute path: `{{abs_path}}`)
{{/each}}
{{#if has_rules}}
There are rules that apply to these root directories:
{{#each worktrees}}
{{#if rules_file}}
`{{root_name}}/{{rules_file.rel_path}}`:
``````
{{{rules_file.text}}}
``````
{{/if}}
{{/each}}
{{/if}}

View File

@@ -25,7 +25,7 @@
// Features that can be globally enabled or disabled
"features": {
// Which edit prediction provider to use.
"edit_prediction_provider": "copilot"
"edit_prediction_provider": "zed"
},
// The name of a font to use for rendering text in the editor
"buffer_font_family": "Zed Plex Mono",
@@ -155,6 +155,8 @@
//
// Default: not set, defaults to "bar"
"cursor_shape": null,
// Determines whether the mouse cursor is hidden when typing in an editor or input box.
"hide_mouse_while_typing": true,
// How to highlight the current line in the editor.
//
// 1. Don't highlight the current line:
@@ -184,6 +186,11 @@
// Whether to show the signature help after completion or a bracket pair inserted.
// If `auto_signature_help` is enabled, this setting will be treated as enabled also.
"show_signature_help_after_edits": false,
// What to do when go to definition yields no results.
//
// 1. Do nothing: `none`
// 2. Find references for the same symbol: `find_all_references` (default)
"go_to_definition_fallback": "find_all_references",
// Whether to show wrap guides (vertical rulers) in the editor.
// Setting this to true will show a guide at the 'preferred_line_length' value
// if 'soft_wrap' is set to 'preferred_line_length', and will show any
@@ -422,6 +429,8 @@
"project_panel": {
// Whether to show the project panel button in the status bar
"button": true,
// Whether to hide the gitignore entries in the project panel.
"hide_gitignore": false,
// Default width of the project panel.
"default_width": 240,
// Where to dock the project panel. Can be 'left' or 'right'.
@@ -614,7 +623,44 @@
"provider": "zed.dev",
// The model to use.
"model": "claude-3-5-sonnet-latest"
}
},
"default_profile": "code-writer",
"profiles": {
"read-only": {
"name": "Read-only",
"tools": {
"diagnostics": true,
"fetch": true,
"list-directory": true,
"now": true,
"path-search": true,
"read-file": true,
"regex-search": true,
"thinking": true
}
},
"code-writer": {
"name": "Code Writer",
"tools": {
"bash": true,
"copy-path": true,
"create-file": true,
"delete-path": true,
"diagnostics": true,
"find-replace-file": true,
"edit-files": false,
"fetch": true,
"list-directory": true,
"move-path": true,
"now": true,
"path-search": true,
"read-file": true,
"regex-search": true,
"thinking": true
}
}
},
"notify_when_agent_waiting": true
},
// The settings for slash commands.
"slash_commands": {
@@ -981,7 +1027,7 @@
// "alternate_scroll": "on",
// 2. Default alternate scroll mode to off
// "alternate_scroll": "off",
"alternate_scroll": "off",
"alternate_scroll": "on",
// Set whether the option key behaves as the meta key.
// May take 2 values:
// 1. Rely on default platform handling of option key, on macOS
@@ -1224,11 +1270,19 @@
"allowed": true
}
},
"LaTeX": {
"format_on_save": "on",
"formatter": "language_server",
"language_servers": ["texlab", "..."],
"prettier": {
"allowed": false
}
},
"Markdown": {
"format_on_save": "off",
"use_on_type_format": false,
"allow_rewrap": "anywhere",
"soft_wrap": "bounded",
"soft_wrap": "editor_width",
"prettier": {
"allowed": true
}

View File

@@ -1,6 +1,6 @@
mod supported_countries;
use std::{pin::Pin, str::FromStr};
use std::{fmt::Display, pin::Pin, str};
use anyhow::{anyhow, Context as _, Result};
use chrono::{DateTime, Utc};
@@ -8,7 +8,7 @@ use futures::{io::BufReader, stream::BoxStream, AsyncBufReadExt, AsyncReadExt, S
use http_client::http::{HeaderMap, HeaderValue};
use http_client::{AsyncBody, HttpClient, Method, Request as HttpRequest};
use serde::{Deserialize, Serialize};
use strum::{EnumIter, EnumString};
use strum::{EnumIter, EnumString, FromRepr};
use thiserror::Error;
use util::ResultExt as _;
@@ -24,6 +24,16 @@ pub struct AnthropicModelCacheConfiguration {
pub max_cache_anchors: usize,
}
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)]
pub enum AnthropicModelMode {
#[default]
Default,
Thinking {
budget_tokens: Option<u32>,
},
}
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, EnumIter)]
pub enum Model {
@@ -32,6 +42,11 @@ pub enum Model {
Claude3_5Sonnet,
#[serde(rename = "claude-3-7-sonnet", alias = "claude-3-7-sonnet-latest")]
Claude3_7Sonnet,
#[serde(
rename = "claude-3-7-sonnet-thinking",
alias = "claude-3-7-sonnet-thinking-latest"
)]
Claude3_7SonnetThinking,
#[serde(rename = "claude-3-5-haiku", alias = "claude-3-5-haiku-latest")]
Claude3_5Haiku,
#[serde(rename = "claude-3-opus", alias = "claude-3-opus-latest")]
@@ -54,6 +69,8 @@ pub enum Model {
default_temperature: Option<f32>,
#[serde(default)]
extra_beta_headers: Vec<String>,
#[serde(default)]
mode: AnthropicModelMode,
},
}
@@ -61,6 +78,8 @@ impl Model {
pub fn from_id(id: &str) -> Result<Self> {
if id.starts_with("claude-3-5-sonnet") {
Ok(Self::Claude3_5Sonnet)
} else if id.starts_with("claude-3-7-sonnet-thinking") {
Ok(Self::Claude3_7SonnetThinking)
} else if id.starts_with("claude-3-7-sonnet") {
Ok(Self::Claude3_7Sonnet)
} else if id.starts_with("claude-3-5-haiku") {
@@ -80,6 +99,20 @@ impl Model {
match self {
Model::Claude3_5Sonnet => "claude-3-5-sonnet-latest",
Model::Claude3_7Sonnet => "claude-3-7-sonnet-latest",
Model::Claude3_7SonnetThinking => "claude-3-7-sonnet-thinking-latest",
Model::Claude3_5Haiku => "claude-3-5-haiku-latest",
Model::Claude3Opus => "claude-3-opus-latest",
Model::Claude3Sonnet => "claude-3-sonnet-20240229",
Model::Claude3Haiku => "claude-3-haiku-20240307",
Self::Custom { name, .. } => name,
}
}
/// The id of the model that should be used for making API requests
pub fn request_id(&self) -> &str {
match self {
Model::Claude3_5Sonnet => "claude-3-5-sonnet-latest",
Model::Claude3_7Sonnet | Model::Claude3_7SonnetThinking => "claude-3-7-sonnet-latest",
Model::Claude3_5Haiku => "claude-3-5-haiku-latest",
Model::Claude3Opus => "claude-3-opus-latest",
Model::Claude3Sonnet => "claude-3-sonnet-20240229",
@@ -92,6 +125,7 @@ impl Model {
match self {
Self::Claude3_7Sonnet => "Claude 3.7 Sonnet",
Self::Claude3_5Sonnet => "Claude 3.5 Sonnet",
Self::Claude3_7SonnetThinking => "Claude 3.7 Sonnet Thinking",
Self::Claude3_5Haiku => "Claude 3.5 Haiku",
Self::Claude3Opus => "Claude 3 Opus",
Self::Claude3Sonnet => "Claude 3 Sonnet",
@@ -107,6 +141,7 @@ impl Model {
Self::Claude3_5Sonnet
| Self::Claude3_5Haiku
| Self::Claude3_7Sonnet
| Self::Claude3_7SonnetThinking
| Self::Claude3Haiku => Some(AnthropicModelCacheConfiguration {
min_total_token: 2_048,
should_speculate: true,
@@ -125,6 +160,7 @@ impl Model {
Self::Claude3_5Sonnet
| Self::Claude3_5Haiku
| Self::Claude3_7Sonnet
| Self::Claude3_7SonnetThinking
| Self::Claude3Opus
| Self::Claude3Sonnet
| Self::Claude3Haiku => 200_000,
@@ -135,7 +171,10 @@ impl Model {
pub fn max_output_tokens(&self) -> u32 {
match self {
Self::Claude3Opus | Self::Claude3Sonnet | Self::Claude3Haiku => 4_096,
Self::Claude3_5Sonnet | Self::Claude3_7Sonnet | Self::Claude3_5Haiku => 8_192,
Self::Claude3_5Sonnet
| Self::Claude3_7Sonnet
| Self::Claude3_7SonnetThinking
| Self::Claude3_5Haiku => 8_192,
Self::Custom {
max_output_tokens, ..
} => max_output_tokens.unwrap_or(4_096),
@@ -146,6 +185,7 @@ impl Model {
match self {
Self::Claude3_5Sonnet
| Self::Claude3_7Sonnet
| Self::Claude3_7SonnetThinking
| Self::Claude3_5Haiku
| Self::Claude3Opus
| Self::Claude3Sonnet
@@ -157,6 +197,21 @@ impl Model {
}
}
pub fn mode(&self) -> AnthropicModelMode {
match self {
Self::Claude3_5Sonnet
| Self::Claude3_7Sonnet
| Self::Claude3_5Haiku
| Self::Claude3Opus
| Self::Claude3Sonnet
| Self::Claude3Haiku => AnthropicModelMode::Default,
Self::Claude3_7SonnetThinking => AnthropicModelMode::Thinking {
budget_tokens: Some(4_096),
},
Self::Custom { mode, .. } => mode.clone(),
}
}
pub const DEFAULT_BETA_HEADERS: &[&str] = &["prompt-caching-2024-07-31"];
pub fn beta_headers(&self) -> String {
@@ -188,7 +243,7 @@ impl Model {
{
tool_override
} else {
self.id()
self.request_id()
}
}
}
@@ -221,30 +276,21 @@ pub async fn complete(
.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
.context("failed to read response body")?;
let status = response.status();
let mut body = Vec::new();
response
.body_mut()
.read_to_end(&mut body)
.await
.context("failed to read response body")?;
if status.is_success() {
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
.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
)))
let body_str = str::from_utf8(&body).context("failed to parse response body as UTF-8")?;
Err(AnthropicError::from_response(status.as_u16(), body_str))
}
}
@@ -338,7 +384,8 @@ pub async fn stream_completion_with_rate_limit_info(
.send(request)
.await
.context("failed to send request to Anthropic")?;
if response.status().is_success() {
let status = response.status();
if status.is_success() {
let rate_limits = RateLimitInfo::from_headers(response.headers());
let reader = BufReader::new(response.into_body());
let stream = reader
@@ -365,20 +412,8 @@ pub async fn stream_completion_with_rate_limit_info(
.await
.context("failed to read response 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(AnthropicError::ApiError(error)),
Ok(_) => Err(AnthropicError::Other(anyhow!(
"Unexpected success response while expecting an error: '{body_str}'",
))),
Err(_) => Err(AnthropicError::Other(anyhow!(
"Failed to connect to API: {} {}",
response.status(),
body_str,
))),
}
let body_str = str::from_utf8(&body).context("failed to parse response body as UTF-8")?;
Err(AnthropicError::from_response(status.as_u16(), body_str))
}
}
@@ -409,6 +444,8 @@ pub async fn extract_tool_args_from_events(
Err(error) => Some(Err(error)),
Ok(Event::ContentBlockDelta { index, delta }) => match delta {
ContentDelta::TextDelta { .. } => None,
ContentDelta::ThinkingDelta { .. } => None,
ContentDelta::SignatureDelta { .. } => None,
ContentDelta::InputJsonDelta { partial_json } => {
if index == tool_use_index {
Some(Ok(partial_json))
@@ -487,6 +524,10 @@ pub enum RequestContent {
pub enum ResponseContent {
#[serde(rename = "text")]
Text { text: String },
#[serde(rename = "thinking")]
Thinking { thinking: String },
#[serde(rename = "redacted_thinking")]
RedactedThinking { data: String },
#[serde(rename = "tool_use")]
ToolUse {
id: String,
@@ -518,6 +559,12 @@ pub enum ToolChoice {
Tool { name: String },
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "lowercase")]
pub enum Thinking {
Enabled { budget_tokens: Option<u32> },
}
#[derive(Debug, Serialize, Deserialize)]
pub struct Request {
pub model: String,
@@ -526,6 +573,8 @@ pub struct Request {
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub tools: Vec<Tool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub thinking: Option<Thinking>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub tool_choice: Option<ToolChoice>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub system: Option<String>,
@@ -609,6 +658,10 @@ pub enum Event {
pub enum ContentDelta {
#[serde(rename = "text_delta")]
TextDelta { text: String },
#[serde(rename = "thinking_delta")]
ThinkingDelta { thinking: String },
#[serde(rename = "signature_delta")]
SignatureDelta { signature: String },
#[serde(rename = "input_json_delta")]
InputJsonDelta { partial_json: String },
}
@@ -621,48 +674,72 @@ pub struct MessageDelta {
#[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)]
#[error("an error occurred while interacting with the Anthropic API: {error_code}: {message}", error_code = .0.code, message = .0.message)]
ApiError(ApiError),
#[error("{0}")]
Other(#[from] anyhow::Error),
}
impl AnthropicError {
pub fn from_response(status: u16, body: &str) -> Self {
let error = match serde_json::from_str::<Event>(body) {
Ok(Event::Error { error }) => Self::ApiError(error),
result => match ApiErrorCode::from_repr(status) {
Some(error_code) => Self::ApiError(ApiError {
code: error_code,
message: body.into(),
}),
None => {
if result.is_ok() {
Self::Other(anyhow!(
"Unexpected success response while expecting an error: '{body}'",
))
} else {
Self::Other(anyhow!("Failed to connect to API: {status} {body}",))
}
}
},
};
error
}
}
#[derive(Debug, Serialize, Deserialize)]
pub struct ApiError {
#[serde(rename = "type")]
pub error_type: String,
pub code: ApiErrorCode,
pub message: String,
}
/// An Anthropic API error code.
/// <https://docs.anthropic.com/en/api/errors#http-errors>
#[derive(Debug, PartialEq, Eq, Clone, Copy, EnumString)]
#[derive(Debug, PartialEq, Eq, Clone, Copy, EnumString, Serialize, Deserialize, FromRepr)]
#[serde(rename_all = "snake_case")]
#[repr(u16)]
#[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,
InvalidRequestError = 400,
/// 401 - `authentication_error`: There's an issue with your API key.
AuthenticationError,
AuthenticationError = 401,
/// 403 - `permission_error`: Your API key does not have permission to use the specified resource.
PermissionError,
PermissionError = 403,
/// 404 - `not_found_error`: The requested resource was not found.
NotFoundError,
NotFoundError = 404,
/// 413 - `request_too_large`: Request exceeds the maximum allowed number of bytes.
RequestTooLarge,
RequestTooLarge = 413,
/// 429 - `rate_limit_error`: Your account has hit a rate limit.
RateLimitError,
RateLimitError = 429,
/// 500 - `api_error`: An unexpected error has occurred internal to Anthropic's systems.
ApiError,
ApiError = 500,
/// 502 - Bad Gateway. This is not in Anthropic's docs, but we have seen it in the wild.
BadGateway = 502,
/// 529 - `overloaded_error`: Anthropic's API is temporarily overloaded.
OverloadedError,
OverloadedError = 529,
}
impl ApiError {
pub fn code(&self) -> Option<ApiErrorCode> {
ApiErrorCode::from_str(&self.error_type).ok()
}
pub fn is_rate_limit_error(&self) -> bool {
matches!(self.error_type.as_str(), "rate_limit_error")
impl Display for ApiErrorCode {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "HTTP {}", *self as u16)
}
}

View File

@@ -188,7 +188,7 @@ impl AskPassSession {
}
pub async fn run(&mut self) -> AskPassResult {
futures::FutureExt::fuse(smol::Timer::after(Duration::from_secs(10))).await;
futures::FutureExt::fuse(smol::Timer::after(Duration::from_secs(20))).await;
AskPassResult::Timedout
}
}

View File

@@ -38,7 +38,7 @@ use workspace::{
dock::{DockPosition, Panel, PanelEvent},
pane, DraggedSelection, Pane, ShowConfiguration, ToggleZoom, Workspace,
};
use zed_actions::assistant::{DeployPromptLibrary, InlineAssist, ToggleFocus};
use zed_actions::assistant::{InlineAssist, OpenPromptLibrary, ToggleFocus};
pub fn init(cx: &mut App) {
workspace::FollowableViewRegistry::register::<ContextEditor>(cx);
@@ -259,7 +259,7 @@ impl AssistantPanel {
menu.context(focus_handle.clone())
.action("New Chat", Box::new(NewChat))
.action("History", Box::new(DeployHistory))
.action("Prompt Library", Box::new(DeployPromptLibrary))
.action("Prompt Library", Box::new(OpenPromptLibrary))
.action("Configure", Box::new(ShowConfiguration))
.action(zoom_label, Box::new(ToggleZoom))
}))
@@ -1028,7 +1028,7 @@ impl AssistantPanel {
fn deploy_prompt_library(
&mut self,
_: &DeployPromptLibrary,
_: &OpenPromptLibrary,
_window: &mut Window,
cx: &mut Context<Self>,
) {

View File

@@ -232,7 +232,7 @@ impl InlineAssistant {
) {
let (snapshot, initial_selections) = editor.update(cx, |editor, cx| {
(
editor.buffer().read(cx).snapshot(cx),
editor.snapshot(window, cx),
editor.selections.all::<Point>(cx),
)
});
@@ -246,7 +246,37 @@ impl InlineAssistant {
if selection.end.column == 0 {
selection.end.row -= 1;
}
selection.end.column = snapshot.line_len(MultiBufferRow(selection.end.row));
selection.end.column = snapshot
.buffer_snapshot
.line_len(MultiBufferRow(selection.end.row));
} else if let Some(fold) =
snapshot.crease_for_buffer_row(MultiBufferRow(selection.end.row))
{
selection.start = fold.range().start;
selection.end = fold.range().end;
if MultiBufferRow(selection.end.row) < snapshot.buffer_snapshot.max_row() {
let chars = snapshot
.buffer_snapshot
.chars_at(Point::new(selection.end.row + 1, 0));
for c in chars {
if c == '\n' {
break;
}
if c.is_whitespace() {
continue;
}
if snapshot
.language_at(selection.end)
.is_some_and(|language| language.config().brackets.is_closing_brace(c))
{
selection.end.row += 1;
selection.end.column = snapshot
.buffer_snapshot
.line_len(MultiBufferRow(selection.end.row));
}
}
}
}
if let Some(prev_selection) = selections.last_mut() {
@@ -262,6 +292,7 @@ impl InlineAssistant {
}
selections.push(selection);
}
let snapshot = &snapshot.buffer_snapshot;
let newest_selection = newest_selection.unwrap();
let mut codegen_ranges = Vec::new();
@@ -3681,7 +3712,7 @@ mod tests {
language_settings, tree_sitter_rust, Buffer, Language, LanguageConfig, LanguageMatcher,
Point,
};
use language_model::LanguageModelRegistry;
use language_model::{LanguageModelRegistry, TokenUsage};
use rand::prelude::*;
use serde::Serialize;
use settings::SettingsStore;
@@ -4060,6 +4091,7 @@ mod tests {
future::ready(Ok(LanguageModelTextStream {
message_id: None,
stream: chunks_rx.map(Ok).boxed(),
last_token_usage: Arc::new(Mutex::new(TokenUsage::default())),
})),
cx,
);

View File

@@ -44,6 +44,7 @@ gpui.workspace = true
heed.workspace = true
html_to_markdown.workspace = true
http_client.workspace = true
indexmap.workspace = true
itertools.workspace = true
language.workspace = true
language_model.workspace = true
@@ -60,11 +61,12 @@ project.workspace = true
prompt_library.workspace = true
prompt_store.workspace = true
proto.workspace = true
release_channel.workspace = true
rope.workspace = true
scripting_tool.workspace = true
serde.workspace = true
serde_json.workspace = true
settings.workspace = true
smallvec.workspace = true
smol.workspace = true
streaming_diff.workspace = true
telemetry.workspace = true

File diff suppressed because it is too large Load Diff

View File

@@ -1,59 +0,0 @@
use std::sync::Arc;
use collections::HashMap;
use gpui::SharedString;
/// A profile for the Zed Agent that controls its behavior.
#[derive(Debug, Clone)]
pub struct AgentProfile {
/// The name of the profile.
pub name: SharedString,
pub tools: HashMap<Arc<str>, bool>,
#[allow(dead_code)]
pub context_servers: HashMap<Arc<str>, ContextServerPreset>,
}
#[derive(Debug, Clone)]
pub struct ContextServerPreset {
#[allow(dead_code)]
pub tools: HashMap<Arc<str>, bool>,
}
impl AgentProfile {
pub fn read_only() -> Self {
Self {
name: "Read-only".into(),
tools: HashMap::from_iter([
("diagnostics".into(), true),
("fetch".into(), true),
("list-directory".into(), true),
("now".into(), true),
("path-search".into(), true),
("read-file".into(), true),
("regex-search".into(), true),
("thinking".into(), true),
]),
context_servers: HashMap::default(),
}
}
pub fn code_writer() -> Self {
Self {
name: "Code Writer".into(),
tools: HashMap::from_iter([
("bash".into(), true),
("delete-path".into(), true),
("diagnostics".into(), true),
("edit-files".into(), true),
("fetch".into(), true),
("list-directory".into(), true),
("now".into(), true),
("path-search".into(), true),
("read-file".into(), true),
("regex-search".into(), true),
("thinking".into(), true),
]),
context_servers: HashMap::default(),
}
}
}

View File

@@ -1,5 +1,4 @@
mod active_thread;
mod agent_profile;
mod assistant_configuration;
mod assistant_model_selector;
mod assistant_panel;
@@ -12,12 +11,12 @@ mod history_store;
mod inline_assistant;
mod inline_prompt_editor;
mod message_editor;
mod profile_selector;
mod terminal_codegen;
mod terminal_inline_assistant;
mod thread;
mod thread_history;
mod thread_store;
mod tool_selector;
mod tool_use;
mod ui;
@@ -33,6 +32,7 @@ use prompt_store::PromptBuilder;
use settings::Settings as _;
pub use crate::active_thread::ActiveThread;
use crate::assistant_configuration::{AddContextServerModal, ManageProfilesModal};
pub use crate::assistant_panel::{AssistantPanel, ConcreteAssistantPanelDelegate};
pub use crate::inline_assistant::InlineAssistant;
pub use crate::thread::{Message, RequestKind, Thread, ThreadEvent};
@@ -47,6 +47,8 @@ actions!(
RemoveAllContext,
OpenHistory,
OpenConfiguration,
ManageProfiles,
AddContextServer,
RemoveSelectedThread,
Chat,
ChatMode,
@@ -87,6 +89,8 @@ pub fn init(
client.telemetry().clone(),
cx,
);
cx.observe_new(AddContextServerModal::register).detach();
cx.observe_new(ManageProfilesModal::register).detach();
feature_gate_assistant2_actions(cx);
}

View File

@@ -1,3 +1,8 @@
mod add_context_server_modal;
mod manage_profiles_modal;
mod profile_picker;
mod tool_picker;
use std::sync::Arc;
use assistant_tool::{ToolSource, ToolWorkingSet};
@@ -5,13 +10,15 @@ use collections::HashMap;
use context_server::manager::ContextServerManager;
use gpui::{Action, AnyView, App, Entity, EventEmitter, FocusHandle, Focusable, Subscription};
use language_model::{LanguageModelProvider, LanguageModelProviderId, LanguageModelRegistry};
use ui::{
prelude::*, Disclosure, Divider, DividerColor, ElevationIndex, Indicator, Switch, Tooltip,
};
use ui::{prelude::*, Disclosure, Divider, DividerColor, ElevationIndex, Indicator, Switch};
use util::ResultExt as _;
use zed_actions::assistant::DeployPromptLibrary;
use zed_actions::ExtensionCategoryFilter;
pub(crate) use add_context_server_modal::AddContextServerModal;
pub(crate) use manage_profiles_modal::ManageProfilesModal;
use crate::AddContextServer;
pub struct AssistantConfiguration {
focus_handle: FocusHandle,
configuration_views_by_provider: HashMap<LanguageModelProviderId, AnyView>,
@@ -170,7 +177,6 @@ impl AssistantConfiguration {
v_flex()
.p(DynamicSpacing::Base16.rems(cx))
.mt_1()
.gap_2()
.flex_1()
.child(
@@ -195,6 +201,7 @@ impl AssistantConfiguration {
let tool_count = tools.len();
v_flex()
.id(SharedString::from(context_server.id()))
.border_1()
.rounded_sm()
.border_color(cx.theme().colors().border)
@@ -308,8 +315,9 @@ impl AssistantConfiguration {
.icon(IconName::Plus)
.icon_size(IconSize::Small)
.icon_position(IconPosition::Start)
.disabled(true)
.tooltip(Tooltip::text("Not yet implemented")),
.on_click(|_event, window, cx| {
window.dispatch_action(AddContextServer.boxed_clone(), cx)
}),
),
)
.child(
@@ -351,33 +359,6 @@ impl Render for AssistantConfiguration {
.bg(cx.theme().colors().panel_background)
.size_full()
.overflow_y_scroll()
.child(
v_flex()
.p(DynamicSpacing::Base16.rems(cx))
.gap_2()
.child(
v_flex()
.gap_0p5()
.child(Headline::new("Prompt Library").size(HeadlineSize::Small))
.child(
Label::new("Create reusable prompts and tag which ones you want sent in every LLM interaction.")
.color(Color::Muted),
),
)
.child(
Button::new("open-prompt-library", "Open Prompt Library")
.style(ButtonStyle::Filled)
.layer(ElevationIndex::ModalSurface)
.full_width()
.icon(IconName::Book)
.icon_size(IconSize::Small)
.icon_position(IconPosition::Start)
.on_click(|_event, window, cx| {
window.dispatch_action(DeployPromptLibrary.boxed_clone(), cx)
}),
),
)
.child(Divider::horizontal().color(DividerColor::Border))
.child(self.render_context_servers_section(cx))
.child(Divider::horizontal().color(DividerColor::Border))
.child(

View File

@@ -0,0 +1,164 @@
use context_server::{ContextServerSettings, ServerCommand, ServerConfig};
use editor::Editor;
use gpui::{prelude::*, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, WeakEntity};
use serde_json::json;
use settings::update_settings_file;
use ui::{prelude::*, Modal, ModalFooter, ModalHeader, Section, Tooltip};
use workspace::{ModalView, Workspace};
use crate::AddContextServer;
pub struct AddContextServerModal {
workspace: WeakEntity<Workspace>,
name_editor: Entity<Editor>,
command_editor: Entity<Editor>,
}
impl AddContextServerModal {
pub fn register(
workspace: &mut Workspace,
_window: Option<&mut Window>,
_cx: &mut Context<Workspace>,
) {
workspace.register_action(|workspace, _: &AddContextServer, window, cx| {
let workspace_handle = cx.entity().downgrade();
workspace.toggle_modal(window, cx, |window, cx| {
Self::new(workspace_handle, window, cx)
})
});
}
pub fn new(
workspace: WeakEntity<Workspace>,
window: &mut Window,
cx: &mut Context<Self>,
) -> Self {
let name_editor = cx.new(|cx| Editor::single_line(window, cx));
let command_editor = cx.new(|cx| Editor::single_line(window, cx));
name_editor.update(cx, |editor, cx| {
editor.set_placeholder_text("Context server name", cx);
});
command_editor.update(cx, |editor, cx| {
editor.set_placeholder_text("Command to run the context server", cx);
});
Self {
name_editor,
command_editor,
workspace,
}
}
fn confirm(&mut self, cx: &mut Context<Self>) {
let name = self.name_editor.read(cx).text(cx).trim().to_string();
let command = self.command_editor.read(cx).text(cx).trim().to_string();
if name.is_empty() || command.is_empty() {
return;
}
let mut command_parts = command.split(' ').map(|part| part.trim().to_string());
let Some(path) = command_parts.next() else {
return;
};
let args = command_parts.collect::<Vec<_>>();
if let Some(workspace) = self.workspace.upgrade() {
workspace.update(cx, |workspace, cx| {
let fs = workspace.app_state().fs.clone();
update_settings_file::<ContextServerSettings>(fs.clone(), cx, |settings, _| {
settings.context_servers.insert(
name.into(),
ServerConfig {
command: Some(ServerCommand {
path,
args,
env: None,
}),
settings: Some(json!({})),
},
);
});
});
}
cx.emit(DismissEvent);
}
fn cancel(&mut self, cx: &mut Context<Self>) {
cx.emit(DismissEvent);
}
}
impl ModalView for AddContextServerModal {}
impl Focusable for AddContextServerModal {
fn focus_handle(&self, cx: &App) -> FocusHandle {
self.name_editor.focus_handle(cx).clone()
}
}
impl EventEmitter<DismissEvent> for AddContextServerModal {}
impl Render for AddContextServerModal {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let is_name_empty = self.name_editor.read(cx).text(cx).trim().is_empty();
let is_command_empty = self.command_editor.read(cx).text(cx).trim().is_empty();
div()
.elevation_3(cx)
.w(rems(34.))
.key_context("AddContextServerModal")
.on_action(cx.listener(|this, _: &menu::Cancel, _window, cx| this.cancel(cx)))
.on_action(cx.listener(|this, _: &menu::Confirm, _window, cx| this.confirm(cx)))
.capture_any_mouse_down(cx.listener(|this, _, window, cx| {
this.focus_handle(cx).focus(window);
}))
.on_mouse_down_out(cx.listener(|_this, _, _, cx| cx.emit(DismissEvent)))
.child(
Modal::new("add-context-server", None)
.header(ModalHeader::new().headline("Add Context Server"))
.section(
Section::new()
.child(
v_flex()
.gap_1()
.child(Label::new("Name"))
.child(self.name_editor.clone()),
)
.child(
v_flex()
.gap_1()
.child(Label::new("Command"))
.child(self.command_editor.clone()),
),
)
.footer(
ModalFooter::new()
.start_slot(
Button::new("cancel", "Cancel").on_click(
cx.listener(|this, _event, _window, cx| this.cancel(cx)),
),
)
.end_slot(
Button::new("add-server", "Add Server")
.disabled(is_name_empty || is_command_empty)
.map(|button| {
if is_name_empty {
button.tooltip(Tooltip::text("Name is required"))
} else if is_command_empty {
button.tooltip(Tooltip::text("Command is required"))
} else {
button
}
})
.on_click(
cx.listener(|this, _event, _window, cx| this.confirm(cx)),
),
),
),
)
}
}

View File

@@ -0,0 +1,201 @@
use std::sync::Arc;
use assistant_settings::AssistantSettings;
use assistant_tool::ToolWorkingSet;
use fs::Fs;
use gpui::{prelude::*, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable};
use settings::Settings as _;
use ui::{prelude::*, ListItem, ListItemSpacing, Navigable, NavigableEntry};
use workspace::{ModalView, Workspace};
use crate::assistant_configuration::profile_picker::{ProfilePicker, ProfilePickerDelegate};
use crate::assistant_configuration::tool_picker::{ToolPicker, ToolPickerDelegate};
use crate::{AssistantPanel, ManageProfiles};
enum Mode {
ChooseProfile(Entity<ProfilePicker>),
ViewProfile(ViewProfileMode),
ConfigureTools(Entity<ToolPicker>),
}
#[derive(Clone)]
pub struct ViewProfileMode {
profile_id: Arc<str>,
configure_tools: NavigableEntry,
}
pub struct ManageProfilesModal {
fs: Arc<dyn Fs>,
tools: Arc<ToolWorkingSet>,
focus_handle: FocusHandle,
mode: Mode,
}
impl ManageProfilesModal {
pub fn register(
workspace: &mut Workspace,
_window: Option<&mut Window>,
_cx: &mut Context<Workspace>,
) {
workspace.register_action(|workspace, _: &ManageProfiles, window, cx| {
if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
let fs = workspace.app_state().fs.clone();
let thread_store = panel.read(cx).thread_store().read(cx);
let tools = thread_store.tools();
workspace.toggle_modal(window, cx, |window, cx| Self::new(fs, tools, window, cx))
}
});
}
pub fn new(
fs: Arc<dyn Fs>,
tools: Arc<ToolWorkingSet>,
window: &mut Window,
cx: &mut Context<Self>,
) -> Self {
let focus_handle = cx.focus_handle();
let handle = cx.entity();
Self {
fs,
tools,
focus_handle,
mode: Mode::ChooseProfile(cx.new(|cx| {
let delegate = ProfilePickerDelegate::new(
move |profile_id, window, cx| {
handle.update(cx, |this, cx| {
this.view_profile(profile_id.clone(), window, cx);
})
},
cx,
);
ProfilePicker::new(delegate, window, cx)
})),
}
}
pub fn view_profile(
&mut self,
profile_id: Arc<str>,
window: &mut Window,
cx: &mut Context<Self>,
) {
self.mode = Mode::ViewProfile(ViewProfileMode {
profile_id,
configure_tools: NavigableEntry::focusable(cx),
});
self.focus_handle(cx).focus(window);
}
fn configure_tools(
&mut self,
profile_id: Arc<str>,
window: &mut Window,
cx: &mut Context<Self>,
) {
let settings = AssistantSettings::get_global(cx);
let Some(profile) = settings.profiles.get(&profile_id).cloned() else {
return;
};
self.mode = Mode::ConfigureTools(cx.new(|cx| {
let delegate = ToolPickerDelegate::new(
self.fs.clone(),
self.tools.clone(),
profile_id,
profile,
cx,
);
ToolPicker::new(delegate, window, cx)
}));
self.focus_handle(cx).focus(window);
}
fn confirm(&mut self, _window: &mut Window, _cx: &mut Context<Self>) {}
fn cancel(&mut self, _window: &mut Window, _cx: &mut Context<Self>) {}
}
impl ModalView for ManageProfilesModal {}
impl Focusable for ManageProfilesModal {
fn focus_handle(&self, cx: &App) -> FocusHandle {
match &self.mode {
Mode::ChooseProfile(profile_picker) => profile_picker.focus_handle(cx),
Mode::ConfigureTools(tool_picker) => tool_picker.focus_handle(cx),
Mode::ViewProfile(_) => self.focus_handle.clone(),
}
}
}
impl EventEmitter<DismissEvent> for ManageProfilesModal {}
impl ManageProfilesModal {
fn render_view_profile(
&mut self,
mode: ViewProfileMode,
window: &mut Window,
cx: &mut Context<Self>,
) -> impl IntoElement {
Navigable::new(
div()
.track_focus(&self.focus_handle(cx))
.size_full()
.child(
v_flex().child(
div()
.id("configure-tools")
.track_focus(&mode.configure_tools.focus_handle)
.on_action({
let profile_id = mode.profile_id.clone();
cx.listener(move |this, _: &menu::Confirm, window, cx| {
this.configure_tools(profile_id.clone(), window, cx);
})
})
.child(
ListItem::new("configure-tools")
.toggle_state(
mode.configure_tools
.focus_handle
.contains_focused(window, cx),
)
.inset(true)
.spacing(ListItemSpacing::Sparse)
.start_slot(Icon::new(IconName::Cog))
.child(Label::new("Configure Tools"))
.on_click({
let profile_id = mode.profile_id.clone();
cx.listener(move |this, _, window, cx| {
this.configure_tools(profile_id.clone(), window, cx);
})
}),
),
),
)
.into_any_element(),
)
.entry(mode.configure_tools)
}
}
impl Render for ManageProfilesModal {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
div()
.elevation_3(cx)
.w(rems(34.))
.key_context("ManageProfilesModal")
.on_action(cx.listener(|this, _: &menu::Cancel, window, cx| this.cancel(window, cx)))
.on_action(cx.listener(|this, _: &menu::Confirm, window, cx| this.confirm(window, cx)))
.capture_any_mouse_down(cx.listener(|this, _, window, cx| {
this.focus_handle(cx).focus(window);
}))
.on_mouse_down_out(cx.listener(|_this, _, _, cx| cx.emit(DismissEvent)))
.child(match &self.mode {
Mode::ChooseProfile(profile_picker) => profile_picker.clone().into_any_element(),
Mode::ViewProfile(mode) => self
.render_view_profile(mode.clone(), window, cx)
.into_any_element(),
Mode::ConfigureTools(tool_picker) => tool_picker.clone().into_any_element(),
})
}
}

View File

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

View File

@@ -0,0 +1,267 @@
use std::sync::Arc;
use assistant_settings::{
AgentProfile, AssistantSettings, AssistantSettingsContent, VersionedAssistantSettingsContent,
};
use assistant_tool::{ToolSource, ToolWorkingSet};
use fs::Fs;
use fuzzy::{match_strings, StringMatch, StringMatchCandidate};
use gpui::{App, Context, DismissEvent, Entity, EventEmitter, Focusable, Task, WeakEntity, Window};
use picker::{Picker, PickerDelegate};
use settings::update_settings_file;
use ui::{prelude::*, HighlightedLabel, ListItem, ListItemSpacing};
use util::ResultExt as _;
pub struct ToolPicker {
picker: Entity<Picker<ToolPickerDelegate>>,
}
impl ToolPicker {
pub fn new(delegate: ToolPickerDelegate, window: &mut Window, cx: &mut Context<Self>) -> Self {
let picker = cx.new(|cx| Picker::uniform_list(delegate, window, cx));
Self { picker }
}
}
impl EventEmitter<DismissEvent> for ToolPicker {}
impl Focusable for ToolPicker {
fn focus_handle(&self, cx: &App) -> gpui::FocusHandle {
self.picker.focus_handle(cx)
}
}
impl Render for ToolPicker {
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
v_flex().w(rems(34.)).child(self.picker.clone())
}
}
#[derive(Debug, Clone)]
pub struct ToolEntry {
pub name: Arc<str>,
pub source: ToolSource,
}
pub struct ToolPickerDelegate {
tool_picker: WeakEntity<ToolPicker>,
fs: Arc<dyn Fs>,
tools: Vec<ToolEntry>,
profile_id: Arc<str>,
profile: AgentProfile,
matches: Vec<StringMatch>,
selected_index: usize,
}
impl ToolPickerDelegate {
pub fn new(
fs: Arc<dyn Fs>,
tool_set: Arc<ToolWorkingSet>,
profile_id: Arc<str>,
profile: AgentProfile,
cx: &mut Context<ToolPicker>,
) -> Self {
let mut tool_entries = Vec::new();
for (source, tools) in tool_set.tools_by_source(cx) {
tool_entries.extend(tools.into_iter().map(|tool| ToolEntry {
name: tool.name().into(),
source: source.clone(),
}));
}
Self {
tool_picker: cx.entity().downgrade(),
fs,
tools: tool_entries,
profile_id,
profile,
matches: Vec::new(),
selected_index: 0,
}
}
}
impl PickerDelegate for ToolPickerDelegate {
type ListItem = ListItem;
fn match_count(&self) -> usize {
self.matches.len()
}
fn selected_index(&self) -> usize {
self.selected_index
}
fn set_selected_index(
&mut self,
ix: usize,
_window: &mut Window,
_cx: &mut Context<Picker<Self>>,
) {
self.selected_index = ix;
}
fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> {
"Search tools…".into()
}
fn update_matches(
&mut self,
query: String,
window: &mut Window,
cx: &mut Context<Picker<Self>>,
) -> Task<()> {
let background = cx.background_executor().clone();
let candidates = self
.tools
.iter()
.enumerate()
.map(|(id, profile)| StringMatchCandidate::new(id, profile.name.as_ref()))
.collect::<Vec<_>>();
cx.spawn_in(window, async move |this, cx| {
let matches = if query.is_empty() {
candidates
.into_iter()
.enumerate()
.map(|(index, candidate)| StringMatch {
candidate_id: index,
string: candidate.string,
positions: Vec::new(),
score: 0.,
})
.collect()
} else {
match_strings(
&candidates,
&query,
false,
100,
&Default::default(),
background,
)
.await
};
this.update(cx, |this, _cx| {
this.delegate.matches = matches;
this.delegate.selected_index = this
.delegate
.selected_index
.min(this.delegate.matches.len().saturating_sub(1));
})
.log_err();
})
}
fn confirm(&mut self, _secondary: bool, window: &mut Window, cx: &mut Context<Picker<Self>>) {
if self.matches.is_empty() {
self.dismissed(window, cx);
return;
}
let candidate_id = self.matches[self.selected_index].candidate_id;
let tool = &self.tools[candidate_id];
let is_enabled = match &tool.source {
ToolSource::Native => {
let is_enabled = self.profile.tools.entry(tool.name.clone()).or_default();
*is_enabled = !*is_enabled;
*is_enabled
}
ToolSource::ContextServer { id } => {
let preset = self
.profile
.context_servers
.entry(id.clone().into())
.or_default();
let is_enabled = preset.tools.entry(tool.name.clone()).or_default();
*is_enabled = !*is_enabled;
*is_enabled
}
};
update_settings_file::<AssistantSettings>(self.fs.clone(), cx, {
let profile_id = self.profile_id.clone();
let tool = tool.clone();
move |settings, _cx| match settings {
AssistantSettingsContent::Versioned(VersionedAssistantSettingsContent::V2(
settings,
)) => {
if let Some(profiles) = &mut settings.profiles {
if let Some(profile) = profiles.get_mut(&profile_id) {
match tool.source {
ToolSource::Native => {
*profile.tools.entry(tool.name).or_default() = is_enabled;
}
ToolSource::ContextServer { id } => {
let preset = profile
.context_servers
.entry(id.clone().into())
.or_default();
*preset.tools.entry(tool.name.clone()).or_default() =
is_enabled;
}
}
}
}
}
_ => {}
}
});
}
fn dismissed(&mut self, _window: &mut Window, cx: &mut Context<Picker<Self>>) {
self.tool_picker
.update(cx, |_this, cx| cx.emit(DismissEvent))
.log_err();
}
fn render_match(
&self,
ix: usize,
selected: bool,
_window: &mut Window,
_cx: &mut Context<Picker<Self>>,
) -> Option<Self::ListItem> {
let tool_match = &self.matches[ix];
let tool = &self.tools[tool_match.candidate_id];
let is_enabled = match &tool.source {
ToolSource::Native => self.profile.tools.get(&tool.name).copied().unwrap_or(false),
ToolSource::ContextServer { id } => self
.profile
.context_servers
.get(id.as_ref())
.and_then(|preset| preset.tools.get(&tool.name))
.copied()
.unwrap_or(false),
};
Some(
ListItem::new(ix)
.inset(true)
.spacing(ListItemSpacing::Sparse)
.toggle_state(selected)
.child(
h_flex()
.gap_2()
.child(HighlightedLabel::new(
tool_match.string.clone(),
tool_match.positions.clone(),
))
.map(|parent| match &tool.source {
ToolSource::Native => parent,
ToolSource::ContextServer { id } => parent
.child(Label::new(id).size(LabelSize::XSmall).color(Color::Muted)),
}),
)
.end_slot::<Icon>(is_enabled.then(|| {
Icon::new(IconName::Check)
.size(IconSize::Small)
.color(Color::Success)
})),
)
}
}

View File

@@ -14,9 +14,9 @@ use client::zed_urls;
use editor::{Editor, MultiBuffer};
use fs::Fs;
use gpui::{
prelude::*, Action, AnyElement, App, AsyncWindowContext, Corner, Entity, EventEmitter,
FocusHandle, Focusable, FontWeight, KeyContext, Pixels, Subscription, Task, UpdateGlobal,
WeakEntity,
action_with_deprecated_aliases, prelude::*, Action, AnyElement, App, AsyncWindowContext,
Corner, Entity, EventEmitter, FocusHandle, Focusable, FontWeight, KeyContext, Pixels,
Subscription, Task, UpdateGlobal, WeakEntity,
};
use language::LanguageRegistry;
use language_model::{LanguageModelProviderTosView, LanguageModelRegistry};
@@ -29,7 +29,7 @@ use ui::{prelude::*, ContextMenu, KeyBinding, PopoverMenu, PopoverMenuHandle, Ta
use util::ResultExt as _;
use workspace::dock::{DockPosition, Panel, PanelEvent};
use workspace::Workspace;
use zed_actions::assistant::{DeployPromptLibrary, ToggleFocus};
use zed_actions::assistant::ToggleFocus;
use crate::active_thread::ActiveThread;
use crate::assistant_configuration::{AssistantConfiguration, AssistantConfigurationEvent};
@@ -43,6 +43,12 @@ use crate::{
OpenHistory,
};
action_with_deprecated_aliases!(
assistant,
OpenPromptLibrary,
["assistant::DeployPromptLibrary"]
);
pub fn init(cx: &mut App) {
cx.observe_new(
|workspace: &mut Workspace, _window, _cx: &mut Context<Workspace>| {
@@ -65,6 +71,14 @@ pub fn init(cx: &mut App) {
panel.update(cx, |panel, cx| panel.new_prompt_editor(window, cx));
}
})
.register_action(|workspace, _: &OpenPromptLibrary, window, cx| {
if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
workspace.focus_panel::<AssistantPanel>(window, cx);
panel.update(cx, |panel, cx| {
panel.deploy_prompt_library(&OpenPromptLibrary, window, cx)
});
}
})
.register_action(|workspace, _: &OpenConfiguration, window, cx| {
if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
workspace.focus_panel::<AssistantPanel>(window, cx);
@@ -174,6 +188,7 @@ impl AssistantPanel {
thread_store.clone(),
language_registry.clone(),
message_editor_context_store.clone(),
workspace.clone(),
window,
cx,
)
@@ -252,6 +267,7 @@ impl AssistantPanel {
self.thread_store.clone(),
self.language_registry.clone(),
message_editor_context_store.clone(),
self.workspace.clone(),
window,
cx,
)
@@ -301,7 +317,7 @@ impl AssistantPanel {
fn deploy_prompt_library(
&mut self,
_: &DeployPromptLibrary,
_: &OpenPromptLibrary,
_window: &mut Window,
cx: &mut Context<Self>,
) {
@@ -389,6 +405,7 @@ impl AssistantPanel {
this.thread_store.clone(),
this.language_registry.clone(),
message_editor_context_store.clone(),
this.workspace.clone(),
window,
cx,
)
@@ -455,7 +472,7 @@ impl AssistantPanel {
workspace.update_in(cx, |workspace, window, cx| {
let thread = thread.read(cx);
let markdown = thread.to_markdown()?;
let markdown = thread.to_markdown(cx)?;
let thread_summary = thread
.summary()
.map(|summary| summary.to_string())
@@ -922,8 +939,8 @@ impl AssistantPanel {
ThreadError::MaxMonthlySpendReached => {
self.render_max_monthly_spend_reached_error(cx)
}
ThreadError::Message(error_message) => {
self.render_error_message(&error_message, cx)
ThreadError::Message { header, message } => {
self.render_error_message(header, message, cx)
}
})
.into_any(),
@@ -1026,7 +1043,8 @@ impl AssistantPanel {
fn render_error_message(
&self,
error_message: &SharedString,
header: SharedString,
message: SharedString,
cx: &mut Context<Self>,
) -> AnyElement {
v_flex()
@@ -1036,17 +1054,14 @@ impl AssistantPanel {
.gap_1p5()
.items_center()
.child(Icon::new(IconName::XCircle).color(Color::Error))
.child(
Label::new("Error interacting with language model")
.weight(FontWeight::MEDIUM),
),
.child(Label::new(header).weight(FontWeight::MEDIUM)),
)
.child(
div()
.id("error-message")
.max_h_32()
.overflow_y_scroll()
.child(Label::new(error_message.clone())),
.child(Label::new(message)),
)
.child(
h_flex()

View File

@@ -482,11 +482,17 @@ impl CodegenAlternative {
self.generation = cx.spawn(async move |codegen, cx| {
let stream = stream.await;
let token_usage = stream
.as_ref()
.ok()
.map(|stream| stream.last_token_usage.clone());
let message_id = stream
.as_ref()
.ok()
.and_then(|stream| stream.message_id.clone());
let generate = async {
let model_telemetry_id = model_telemetry_id.clone();
let model_provider_id = model_provider_id.clone();
let (mut diff_tx, mut diff_rx) = mpsc::channel(1);
let executor = cx.background_executor().clone();
let message_id = message_id.clone();
@@ -596,7 +602,7 @@ impl CodegenAlternative {
kind: AssistantKind::Inline,
phase: AssistantPhase::Response,
model: model_telemetry_id,
model_provider: model_provider_id.to_string(),
model_provider: model_provider_id,
response_latency,
error_message,
language_name: language_name.map(|name| name.to_proto()),
@@ -677,6 +683,16 @@ impl CodegenAlternative {
}
this.elapsed_time = Some(elapsed_time);
this.completion = Some(completion.lock().clone());
if let Some(usage) = token_usage {
let usage = usage.lock();
telemetry::event!(
"Inline Assistant Completion",
model = model_telemetry_id,
model_provider = model_provider_id,
input_tokens = usage.input_tokens,
output_tokens = usage.output_tokens,
)
}
cx.emit(CodegenEvent::Finished);
cx.notify();
})
@@ -1021,7 +1037,7 @@ mod tests {
language_settings, tree_sitter_rust, Buffer, Language, LanguageConfig, LanguageMatcher,
Point,
};
use language_model::LanguageModelRegistry;
use language_model::{LanguageModelRegistry, TokenUsage};
use rand::prelude::*;
use serde::Serialize;
use settings::SettingsStore;
@@ -1405,6 +1421,7 @@ mod tests {
future::ready(Ok(LanguageModelTextStream {
message_id: None,
stream: chunks_rx.map(Ok).boxed(),
last_token_usage: Arc::new(Mutex::new(TokenUsage::default())),
})),
cx,
);

View File

@@ -1,19 +1,28 @@
mod completion_provider;
mod fetch_context_picker;
mod file_context_picker;
mod thread_context_picker;
use std::ops::Range;
use std::path::PathBuf;
use std::sync::Arc;
use anyhow::{anyhow, Result};
use editor::Editor;
use editor::display_map::{Crease, FoldId};
use editor::{Anchor, AnchorRangeExt as _, Editor, ExcerptId, FoldPlaceholder, ToOffset};
use file_context_picker::render_file_context_entry;
use gpui::{App, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, Task, WeakEntity};
use gpui::{
App, DismissEvent, Empty, Entity, EventEmitter, FocusHandle, Focusable, Task, WeakEntity,
};
use multi_buffer::MultiBufferRow;
use project::ProjectPath;
use thread_context_picker::{render_thread_context_entry, ThreadContextEntry};
use ui::{prelude::*, ContextMenu, ContextMenuEntry, ContextMenuItem};
use ui::{
prelude::*, ButtonLike, ContextMenu, ContextMenuEntry, ContextMenuItem, Disclosure, TintColor,
};
use workspace::{notifications::NotifyResultExt, Workspace};
pub use crate::context_picker::completion_provider::ContextPickerCompletionProvider;
use crate::context_picker::fetch_context_picker::FetchContextPicker;
use crate::context_picker::file_context_picker::FileContextPicker;
use crate::context_picker::thread_context_picker::ThreadContextPicker;
@@ -34,10 +43,31 @@ enum ContextPickerMode {
Thread,
}
impl TryFrom<&str> for ContextPickerMode {
type Error = String;
fn try_from(value: &str) -> Result<Self, Self::Error> {
match value {
"file" => Ok(Self::File),
"fetch" => Ok(Self::Fetch),
"thread" => Ok(Self::Thread),
_ => Err(format!("Invalid context picker mode: {}", value)),
}
}
}
impl ContextPickerMode {
pub fn mention_prefix(&self) -> &'static str {
match self {
Self::File => "file",
Self::Fetch => "fetch",
Self::Thread => "thread",
}
}
pub fn label(&self) -> &'static str {
match self {
Self::File => "File/Directory",
Self::File => "Files & Directories",
Self::Fetch => "Fetch",
Self::Thread => "Thread",
}
@@ -63,7 +93,6 @@ enum ContextPickerState {
pub(super) struct ContextPicker {
mode: ContextPickerState,
workspace: WeakEntity<Workspace>,
editor: WeakEntity<Editor>,
context_store: WeakEntity<ContextStore>,
thread_store: Option<WeakEntity<ThreadStore>>,
confirm_behavior: ConfirmBehavior,
@@ -74,7 +103,6 @@ impl ContextPicker {
workspace: WeakEntity<Workspace>,
thread_store: Option<WeakEntity<ThreadStore>>,
context_store: WeakEntity<ContextStore>,
editor: WeakEntity<Editor>,
confirm_behavior: ConfirmBehavior,
window: &mut Window,
cx: &mut Context<Self>,
@@ -88,7 +116,6 @@ impl ContextPicker {
workspace,
context_store,
thread_store,
editor,
confirm_behavior,
}
}
@@ -109,10 +136,7 @@ impl ContextPicker {
.enumerate()
.map(|(ix, entry)| self.recent_menu_item(context_picker.clone(), ix, entry));
let mut modes = vec![ContextPickerMode::File, ContextPickerMode::Fetch];
if self.allow_threads() {
modes.push(ContextPickerMode::Thread);
}
let modes = supported_context_picker_modes(&self.thread_store);
let menu = menu
.when(has_recent, |menu| {
@@ -174,7 +198,6 @@ impl ContextPicker {
FileContextPicker::new(
context_picker.clone(),
self.workspace.clone(),
self.editor.clone(),
self.context_store.clone(),
self.confirm_behavior,
window,
@@ -278,7 +301,7 @@ impl ContextPicker {
};
let task = context_store.update(cx, |context_store, cx| {
context_store.add_file_from_path(project_path.clone(), cx)
context_store.add_file_from_path(project_path.clone(), true, cx)
});
cx.spawn_in(window, async move |_, cx| task.await.notify_async_err(cx))
@@ -308,7 +331,7 @@ impl ContextPicker {
cx.spawn(async move |this, cx| {
let thread = open_thread_task.await?;
context_store.update(cx, |context_store, cx| {
context_store.add_thread(thread, cx);
context_store.add_thread(thread, true, cx);
})?;
this.update(cx, |_this, cx| cx.notify())
@@ -328,7 +351,7 @@ impl ContextPicker {
let mut current_files = context_store.file_paths(cx);
if let Some(active_path) = Self::active_singleton_buffer_path(&workspace, cx) {
if let Some(active_path) = active_singleton_buffer_path(&workspace, cx) {
current_files.insert(active_path);
}
@@ -384,16 +407,6 @@ impl ContextPicker {
recent
}
fn active_singleton_buffer_path(workspace: &Workspace, cx: &App) -> Option<PathBuf> {
let active_item = workspace.active_item(cx)?;
let editor = active_item.to_any().downcast::<Editor>().ok()?.read(cx);
let buffer = editor.buffer().read(cx).as_singleton()?;
let path = buffer.read(cx).file()?.path().to_path_buf();
Some(path)
}
}
impl EventEmitter<DismissEvent> for ContextPicker {}
@@ -429,3 +442,212 @@ enum RecentEntry {
},
Thread(ThreadContextEntry),
}
fn supported_context_picker_modes(
thread_store: &Option<WeakEntity<ThreadStore>>,
) -> Vec<ContextPickerMode> {
let mut modes = vec![ContextPickerMode::File, ContextPickerMode::Fetch];
if thread_store.is_some() {
modes.push(ContextPickerMode::Thread);
}
modes
}
fn active_singleton_buffer_path(workspace: &Workspace, cx: &App) -> Option<PathBuf> {
let active_item = workspace.active_item(cx)?;
let editor = active_item.to_any().downcast::<Editor>().ok()?.read(cx);
let buffer = editor.buffer().read(cx).as_singleton()?;
let path = buffer.read(cx).file()?.path().to_path_buf();
Some(path)
}
fn recent_context_picker_entries(
context_store: Entity<ContextStore>,
thread_store: Option<WeakEntity<ThreadStore>>,
workspace: Entity<Workspace>,
cx: &App,
) -> Vec<RecentEntry> {
let mut recent = Vec::with_capacity(6);
let mut current_files = context_store.read(cx).file_paths(cx);
let workspace = workspace.read(cx);
if let Some(active_path) = active_singleton_buffer_path(workspace, cx) {
current_files.insert(active_path);
}
let project = workspace.project().read(cx);
recent.extend(
workspace
.recent_navigation_history_iter(cx)
.filter(|(path, _)| !current_files.contains(&path.path.to_path_buf()))
.take(4)
.filter_map(|(project_path, _)| {
project
.worktree_for_id(project_path.worktree_id, cx)
.map(|worktree| RecentEntry::File {
project_path,
path_prefix: worktree.read(cx).root_name().into(),
})
}),
);
let mut current_threads = context_store.read(cx).thread_ids();
if let Some(active_thread) = workspace
.panel::<AssistantPanel>(cx)
.map(|panel| panel.read(cx).active_thread(cx))
{
current_threads.insert(active_thread.read(cx).id().clone());
}
if let Some(thread_store) = thread_store.and_then(|thread_store| thread_store.upgrade()) {
recent.extend(
thread_store
.read(cx)
.threads()
.into_iter()
.filter(|thread| !current_threads.contains(&thread.id))
.take(2)
.map(|thread| {
RecentEntry::Thread(ThreadContextEntry {
id: thread.id,
summary: thread.summary,
})
}),
);
}
recent
}
pub(crate) fn insert_crease_for_mention(
excerpt_id: ExcerptId,
crease_start: text::Anchor,
content_len: usize,
crease_label: SharedString,
crease_icon_path: SharedString,
editor_entity: Entity<Editor>,
window: &mut Window,
cx: &mut App,
) {
editor_entity.update(cx, |editor, cx| {
let snapshot = editor.buffer().read(cx).snapshot(cx);
let Some(start) = snapshot.anchor_in_excerpt(excerpt_id, crease_start) else {
return;
};
let end = snapshot.anchor_before(start.to_offset(&snapshot) + content_len);
let placeholder = FoldPlaceholder {
render: render_fold_icon_button(
crease_icon_path,
crease_label,
editor_entity.downgrade(),
),
..Default::default()
};
let render_trailer =
move |_row, _unfold, _window: &mut Window, _cx: &mut App| Empty.into_any();
let crease = Crease::inline(
start..end,
placeholder.clone(),
fold_toggle("mention"),
render_trailer,
);
editor.insert_creases(vec![crease.clone()], cx);
editor.fold_creases(vec![crease], false, window, cx);
});
}
fn render_fold_icon_button(
icon_path: SharedString,
label: SharedString,
editor: WeakEntity<Editor>,
) -> Arc<dyn Send + Sync + Fn(FoldId, Range<Anchor>, &mut App) -> AnyElement> {
Arc::new({
move |fold_id, fold_range, cx| {
let is_in_text_selection = editor.upgrade().is_some_and(|editor| {
editor.update(cx, |editor, cx| {
let snapshot = editor
.buffer()
.update(cx, |multi_buffer, cx| multi_buffer.snapshot(cx));
let is_in_pending_selection = || {
editor
.selections
.pending
.as_ref()
.is_some_and(|pending_selection| {
pending_selection
.selection
.range()
.includes(&fold_range, &snapshot)
})
};
let mut is_in_complete_selection = || {
editor
.selections
.disjoint_in_range::<usize>(fold_range.clone(), cx)
.into_iter()
.any(|selection| {
// This is needed to cover a corner case, if we just check for an existing
// selection in the fold range, having a cursor at the start of the fold
// marks it as selected. Non-empty selections don't cause this.
let length = selection.end - selection.start;
length > 0
})
};
is_in_pending_selection() || is_in_complete_selection()
})
});
ButtonLike::new(fold_id)
.style(ButtonStyle::Filled)
.selected_style(ButtonStyle::Tinted(TintColor::Accent))
.toggle_state(is_in_text_selection)
.child(
h_flex()
.gap_1()
.child(
Icon::from_path(icon_path.clone())
.size(IconSize::Small)
.color(Color::Muted),
)
.child(
Label::new(label.clone())
.size(LabelSize::Small)
.single_line(),
),
)
.into_any_element()
}
})
}
fn fold_toggle(
name: &'static str,
) -> impl Fn(
MultiBufferRow,
bool,
Arc<dyn Fn(bool, &mut Window, &mut App) + Send + Sync>,
&mut Window,
&mut App,
) -> AnyElement {
move |row, is_folded, fold, _window, _cx| {
Disclosure::new((name, row.0 as u64), !is_folded)
.toggle_state(is_folded)
.on_click(move |_e, window, cx| fold(!is_folded, window, cx))
.into_any_element()
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -81,77 +81,80 @@ impl FetchContextPickerDelegate {
url: String::new(),
}
}
}
async fn build_message(http_client: Arc<HttpClientWithUrl>, url: String) -> Result<String> {
let url = if !url.starts_with("https://") && !url.starts_with("http://") {
format!("https://{url}")
} else {
url
};
pub(crate) async fn fetch_url_content(
http_client: Arc<HttpClientWithUrl>,
url: String,
) -> Result<String> {
let url = if !url.starts_with("https://") && !url.starts_with("http://") {
format!("https://{url}")
} else {
url
};
let mut response = http_client.get(&url, AsyncBody::default(), true).await?;
let mut response = http_client.get(&url, AsyncBody::default(), true).await?;
let mut body = Vec::new();
response
.body_mut()
.read_to_end(&mut body)
.await
.context("error reading response body")?;
let mut body = Vec::new();
response
.body_mut()
.read_to_end(&mut body)
.await
.context("error reading response body")?;
if response.status().is_client_error() {
let text = String::from_utf8_lossy(body.as_slice());
bail!(
"status error {}, response: {text:?}",
response.status().as_u16()
);
if response.status().is_client_error() {
let text = String::from_utf8_lossy(body.as_slice());
bail!(
"status error {}, response: {text:?}",
response.status().as_u16()
);
}
let Some(content_type) = response.headers().get("content-type") else {
bail!("missing Content-Type header");
};
let content_type = content_type
.to_str()
.context("invalid Content-Type header")?;
let content_type = match content_type {
"text/html" => ContentType::Html,
"text/plain" => ContentType::Plaintext,
"application/json" => ContentType::Json,
_ => ContentType::Html,
};
match content_type {
ContentType::Html => {
let mut handlers: Vec<TagHandler> = vec![
Rc::new(RefCell::new(markdown::WebpageChromeRemover)),
Rc::new(RefCell::new(markdown::ParagraphHandler)),
Rc::new(RefCell::new(markdown::HeadingHandler)),
Rc::new(RefCell::new(markdown::ListHandler)),
Rc::new(RefCell::new(markdown::TableHandler::new())),
Rc::new(RefCell::new(markdown::StyledTextHandler)),
];
if url.contains("wikipedia.org") {
use html_to_markdown::structure::wikipedia;
handlers.push(Rc::new(RefCell::new(wikipedia::WikipediaChromeRemover)));
handlers.push(Rc::new(RefCell::new(wikipedia::WikipediaInfoboxHandler)));
handlers.push(Rc::new(
RefCell::new(wikipedia::WikipediaCodeHandler::new()),
));
} else {
handlers.push(Rc::new(RefCell::new(markdown::CodeHandler)));
}
convert_html_to_markdown(&body[..], &mut handlers)
}
ContentType::Plaintext => Ok(std::str::from_utf8(&body)?.to_owned()),
ContentType::Json => {
let json: serde_json::Value = serde_json::from_slice(&body)?;
let Some(content_type) = response.headers().get("content-type") else {
bail!("missing Content-Type header");
};
let content_type = content_type
.to_str()
.context("invalid Content-Type header")?;
let content_type = match content_type {
"text/html" => ContentType::Html,
"text/plain" => ContentType::Plaintext,
"application/json" => ContentType::Json,
_ => ContentType::Html,
};
match content_type {
ContentType::Html => {
let mut handlers: Vec<TagHandler> = vec![
Rc::new(RefCell::new(markdown::WebpageChromeRemover)),
Rc::new(RefCell::new(markdown::ParagraphHandler)),
Rc::new(RefCell::new(markdown::HeadingHandler)),
Rc::new(RefCell::new(markdown::ListHandler)),
Rc::new(RefCell::new(markdown::TableHandler::new())),
Rc::new(RefCell::new(markdown::StyledTextHandler)),
];
if url.contains("wikipedia.org") {
use html_to_markdown::structure::wikipedia;
handlers.push(Rc::new(RefCell::new(wikipedia::WikipediaChromeRemover)));
handlers.push(Rc::new(RefCell::new(wikipedia::WikipediaInfoboxHandler)));
handlers.push(Rc::new(
RefCell::new(wikipedia::WikipediaCodeHandler::new()),
));
} else {
handlers.push(Rc::new(RefCell::new(markdown::CodeHandler)));
}
convert_html_to_markdown(&body[..], &mut handlers)
}
ContentType::Plaintext => Ok(std::str::from_utf8(&body)?.to_owned()),
ContentType::Json => {
let json: serde_json::Value = serde_json::from_slice(&body)?;
Ok(format!(
"```json\n{}\n```",
serde_json::to_string_pretty(&json)?
))
}
Ok(format!(
"```json\n{}\n```",
serde_json::to_string_pretty(&json)?
))
}
}
}
@@ -208,7 +211,7 @@ impl PickerDelegate for FetchContextPickerDelegate {
let confirm_behavior = self.confirm_behavior;
cx.spawn_in(window, async move |this, cx| {
let text = cx
.background_spawn(Self::build_message(http_client, url.clone()))
.background_spawn(fetch_url_content(http_client, url.clone()))
.await?;
this.update_in(cx, |this, window, cx| {

View File

@@ -1,25 +1,15 @@
use std::collections::BTreeSet;
use std::ops::Range;
use std::path::Path;
use std::sync::atomic::AtomicBool;
use std::sync::Arc;
use editor::actions::FoldAt;
use editor::display_map::{Crease, FoldId};
use editor::scroll::Autoscroll;
use editor::{Anchor, AnchorRangeExt, Editor, FoldPlaceholder, ToPoint};
use file_icons::FileIcons;
use fuzzy::PathMatch;
use gpui::{
AnyElement, App, AppContext, DismissEvent, Empty, Entity, FocusHandle, Focusable, Stateful,
Task, WeakEntity,
App, AppContext, DismissEvent, Entity, FocusHandle, Focusable, Stateful, Task, WeakEntity,
};
use multi_buffer::{MultiBufferPoint, MultiBufferRow};
use picker::{Picker, PickerDelegate};
use project::{PathMatchCandidateSet, ProjectPath, WorktreeId};
use rope::Point;
use text::SelectionGoal;
use ui::{prelude::*, ButtonLike, Disclosure, ListItem, TintColor, Tooltip};
use ui::{prelude::*, ListItem, Tooltip};
use util::ResultExt as _;
use workspace::{notifications::NotifyResultExt, Workspace};
@@ -34,7 +24,6 @@ impl FileContextPicker {
pub fn new(
context_picker: WeakEntity<ContextPicker>,
workspace: WeakEntity<Workspace>,
editor: WeakEntity<Editor>,
context_store: WeakEntity<ContextStore>,
confirm_behavior: ConfirmBehavior,
window: &mut Window,
@@ -43,7 +32,6 @@ impl FileContextPicker {
let delegate = FileContextPickerDelegate::new(
context_picker,
workspace,
editor,
context_store,
confirm_behavior,
);
@@ -68,7 +56,6 @@ impl Render for FileContextPicker {
pub struct FileContextPickerDelegate {
context_picker: WeakEntity<ContextPicker>,
workspace: WeakEntity<Workspace>,
editor: WeakEntity<Editor>,
context_store: WeakEntity<ContextStore>,
confirm_behavior: ConfirmBehavior,
matches: Vec<PathMatch>,
@@ -79,95 +66,18 @@ impl FileContextPickerDelegate {
pub fn new(
context_picker: WeakEntity<ContextPicker>,
workspace: WeakEntity<Workspace>,
editor: WeakEntity<Editor>,
context_store: WeakEntity<ContextStore>,
confirm_behavior: ConfirmBehavior,
) -> Self {
Self {
context_picker,
workspace,
editor,
context_store,
confirm_behavior,
matches: Vec::new(),
selected_index: 0,
}
}
fn search(
&mut self,
query: String,
cancellation_flag: Arc<AtomicBool>,
workspace: &Entity<Workspace>,
cx: &mut Context<Picker<Self>>,
) -> Task<Vec<PathMatch>> {
if query.is_empty() {
let workspace = workspace.read(cx);
let project = workspace.project().read(cx);
let recent_matches = workspace
.recent_navigation_history(Some(10), cx)
.into_iter()
.filter_map(|(project_path, _)| {
let worktree = project.worktree_for_id(project_path.worktree_id, cx)?;
Some(PathMatch {
score: 0.,
positions: Vec::new(),
worktree_id: project_path.worktree_id.to_usize(),
path: project_path.path,
path_prefix: worktree.read(cx).root_name().into(),
distance_to_relative_ancestor: 0,
is_dir: false,
})
});
let file_matches = project.worktrees(cx).flat_map(|worktree| {
let worktree = worktree.read(cx);
let path_prefix: Arc<str> = worktree.root_name().into();
worktree.entries(false, 0).map(move |entry| PathMatch {
score: 0.,
positions: Vec::new(),
worktree_id: worktree.id().to_usize(),
path: entry.path.clone(),
path_prefix: path_prefix.clone(),
distance_to_relative_ancestor: 0,
is_dir: entry.is_dir(),
})
});
Task::ready(recent_matches.chain(file_matches).collect())
} else {
let worktrees = workspace.read(cx).visible_worktrees(cx).collect::<Vec<_>>();
let candidate_sets = worktrees
.into_iter()
.map(|worktree| {
let worktree = worktree.read(cx);
PathMatchCandidateSet {
snapshot: worktree.snapshot(),
include_ignored: worktree
.root_entry()
.map_or(false, |entry| entry.is_ignored),
include_root_name: true,
candidates: project::Candidates::Entries,
}
})
.collect::<Vec<_>>();
let executor = cx.background_executor().clone();
cx.foreground_executor().spawn(async move {
fuzzy::match_path_sets(
candidate_sets.as_slice(),
query.as_str(),
None,
false,
100,
&cancellation_flag,
executor,
)
.await
})
}
}
}
impl PickerDelegate for FileContextPickerDelegate {
@@ -204,7 +114,7 @@ impl PickerDelegate for FileContextPickerDelegate {
return Task::ready(());
};
let search_task = self.search(query, Arc::<AtomicBool>::default(), &workspace, cx);
let search_task = search_paths(query, Arc::<AtomicBool>::default(), &workspace, cx);
cx.spawn_in(window, async move |this, cx| {
// TODO: This should be probably be run in the background.
@@ -222,14 +132,6 @@ impl PickerDelegate for FileContextPickerDelegate {
return;
};
let file_name = mat
.path
.file_name()
.map(|os_str| os_str.to_string_lossy().into_owned())
.unwrap_or(mat.path_prefix.to_string());
let full_path = mat.path.display().to_string();
let project_path = ProjectPath {
worktree_id: WorktreeId::from_usize(mat.worktree_id),
path: mat.path.clone(),
@@ -237,106 +139,13 @@ impl PickerDelegate for FileContextPickerDelegate {
let is_directory = mat.is_dir;
let Some(editor_entity) = self.editor.upgrade() else {
return;
};
editor_entity.update(cx, |editor, cx| {
editor.transact(window, cx, |editor, window, cx| {
// Move empty selections left by 1 column to select the `@`s, so they get overwritten when we insert.
{
let mut selections = editor.selections.all::<MultiBufferPoint>(cx);
for selection in selections.iter_mut() {
if selection.is_empty() {
let old_head = selection.head();
let new_head = MultiBufferPoint::new(
old_head.row,
old_head.column.saturating_sub(1),
);
selection.set_head(new_head, SelectionGoal::None);
}
}
editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
s.select(selections)
});
}
let start_anchors = {
let snapshot = editor.buffer().read(cx).snapshot(cx);
editor
.selections
.all::<Point>(cx)
.into_iter()
.map(|selection| snapshot.anchor_before(selection.start))
.collect::<Vec<_>>()
};
editor.insert(&full_path, window, cx);
let end_anchors = {
let snapshot = editor.buffer().read(cx).snapshot(cx);
editor
.selections
.all::<Point>(cx)
.into_iter()
.map(|selection| snapshot.anchor_after(selection.end))
.collect::<Vec<_>>()
};
editor.insert("\n", window, cx); // Needed to end the fold
let file_icon = if is_directory {
FileIcons::get_folder_icon(false, cx)
} else {
FileIcons::get_icon(&Path::new(&full_path), cx)
}
.unwrap_or_else(|| SharedString::new(""));
let placeholder = FoldPlaceholder {
render: render_fold_icon_button(
file_icon,
file_name.into(),
editor_entity.downgrade(),
),
..Default::default()
};
let render_trailer =
move |_row, _unfold, _window: &mut Window, _cx: &mut App| Empty.into_any();
let buffer = editor.buffer().read(cx).snapshot(cx);
let mut rows_to_fold = BTreeSet::new();
let crease_iter = start_anchors
.into_iter()
.zip(end_anchors)
.map(|(start, end)| {
rows_to_fold.insert(MultiBufferRow(start.to_point(&buffer).row));
Crease::inline(
start..end,
placeholder.clone(),
fold_toggle("tool-use"),
render_trailer,
)
});
editor.insert_creases(crease_iter, cx);
for buffer_row in rows_to_fold {
editor.fold_at(&FoldAt { buffer_row }, window, cx);
}
});
});
let Some(task) = self
.context_store
.update(cx, |context_store, cx| {
if is_directory {
context_store.add_directory(project_path, cx)
context_store.add_directory(project_path, true, cx)
} else {
context_store.add_file_from_path(project_path, cx)
context_store.add_file_from_path(project_path, true, cx)
}
})
.ok()
@@ -390,6 +199,80 @@ impl PickerDelegate for FileContextPickerDelegate {
}
}
pub(crate) fn search_paths(
query: String,
cancellation_flag: Arc<AtomicBool>,
workspace: &Entity<Workspace>,
cx: &App,
) -> Task<Vec<PathMatch>> {
if query.is_empty() {
let workspace = workspace.read(cx);
let project = workspace.project().read(cx);
let recent_matches = workspace
.recent_navigation_history(Some(10), cx)
.into_iter()
.filter_map(|(project_path, _)| {
let worktree = project.worktree_for_id(project_path.worktree_id, cx)?;
Some(PathMatch {
score: 0.,
positions: Vec::new(),
worktree_id: project_path.worktree_id.to_usize(),
path: project_path.path,
path_prefix: worktree.read(cx).root_name().into(),
distance_to_relative_ancestor: 0,
is_dir: false,
})
});
let file_matches = project.worktrees(cx).flat_map(|worktree| {
let worktree = worktree.read(cx);
let path_prefix: Arc<str> = worktree.root_name().into();
worktree.entries(false, 0).map(move |entry| PathMatch {
score: 0.,
positions: Vec::new(),
worktree_id: worktree.id().to_usize(),
path: entry.path.clone(),
path_prefix: path_prefix.clone(),
distance_to_relative_ancestor: 0,
is_dir: entry.is_dir(),
})
});
Task::ready(recent_matches.chain(file_matches).collect())
} else {
let worktrees = workspace.read(cx).visible_worktrees(cx).collect::<Vec<_>>();
let candidate_sets = worktrees
.into_iter()
.map(|worktree| {
let worktree = worktree.read(cx);
PathMatchCandidateSet {
snapshot: worktree.snapshot(),
include_ignored: worktree
.root_entry()
.map_or(false, |entry| entry.is_ignored),
include_root_name: true,
candidates: project::Candidates::Entries,
}
})
.collect::<Vec<_>>();
let executor = cx.background_executor().clone();
cx.foreground_executor().spawn(async move {
fuzzy::match_path_sets(
candidate_sets.as_slice(),
query.as_str(),
None,
false,
100,
&cancellation_flag,
executor,
)
.await
})
}
}
pub fn render_file_context_entry(
id: ElementId,
path: &Path,
@@ -399,7 +282,10 @@ pub fn render_file_context_entry(
cx: &App,
) -> Stateful<Div> {
let (file_name, directory) = if path == Path::new("") {
(SharedString::from(path_prefix.clone()), None)
(
SharedString::from(path_prefix.trim_end_matches('/').to_string()),
None,
)
} else {
let file_name = path
.file_name()
@@ -408,8 +294,10 @@ pub fn render_file_context_entry(
.to_string()
.into();
let mut directory = format!("{}/", path_prefix);
let mut directory = path_prefix.to_string();
if !directory.ends_with('/') {
directory.push('/');
}
if let Some(parent) = path.parent().filter(|parent| parent != &Path::new("")) {
directory.push_str(&parent.to_string_lossy());
directory.push('/');
@@ -484,85 +372,3 @@ pub fn render_file_context_entry(
}
})
}
fn render_fold_icon_button(
icon: SharedString,
label: SharedString,
editor: WeakEntity<Editor>,
) -> Arc<dyn Send + Sync + Fn(FoldId, Range<Anchor>, &mut App) -> AnyElement> {
Arc::new(move |fold_id, fold_range, cx| {
let is_in_text_selection = editor.upgrade().is_some_and(|editor| {
editor.update(cx, |editor, cx| {
let snapshot = editor
.buffer()
.update(cx, |multi_buffer, cx| multi_buffer.snapshot(cx));
let is_in_pending_selection = || {
editor
.selections
.pending
.as_ref()
.is_some_and(|pending_selection| {
pending_selection
.selection
.range()
.includes(&fold_range, &snapshot)
})
};
let mut is_in_complete_selection = || {
editor
.selections
.disjoint_in_range::<usize>(fold_range.clone(), cx)
.into_iter()
.any(|selection| {
// This is needed to cover a corner case, if we just check for an existing
// selection in the fold range, having a cursor at the start of the fold
// marks it as selected. Non-empty selections don't cause this.
let length = selection.end - selection.start;
length > 0
})
};
is_in_pending_selection() || is_in_complete_selection()
})
});
ButtonLike::new(fold_id)
.style(ButtonStyle::Filled)
.selected_style(ButtonStyle::Tinted(TintColor::Accent))
.toggle_state(is_in_text_selection)
.child(
h_flex()
.gap_1()
.child(
Icon::from_path(icon.clone())
.size(IconSize::Small)
.color(Color::Muted),
)
.child(
Label::new(label.clone())
.size(LabelSize::Small)
.single_line(),
),
)
.into_any_element()
})
}
fn fold_toggle(
name: &'static str,
) -> impl Fn(
MultiBufferRow,
bool,
Arc<dyn Fn(bool, &mut Window, &mut App) + Send + Sync>,
&mut Window,
&mut App,
) -> AnyElement {
move |row, is_folded, fold, _window, _cx| {
Disclosure::new((name, row.0 as u64), !is_folded)
.toggle_state(is_folded)
.on_click(move |_e, window, cx| fold(!is_folded, window, cx))
.into_any_element()
}
}

View File

@@ -110,45 +110,11 @@ impl PickerDelegate for ThreadContextPickerDelegate {
window: &mut Window,
cx: &mut Context<Picker<Self>>,
) -> Task<()> {
let Ok(threads) = self.thread_store.update(cx, |this, _cx| {
this.threads()
.into_iter()
.map(|thread| ThreadContextEntry {
id: thread.id,
summary: thread.summary,
})
.collect::<Vec<_>>()
}) else {
let Some(threads) = self.thread_store.upgrade() else {
return Task::ready(());
};
let executor = cx.background_executor().clone();
let search_task = cx.background_spawn(async move {
if query.is_empty() {
threads
} else {
let candidates = threads
.iter()
.enumerate()
.map(|(id, thread)| StringMatchCandidate::new(id, &thread.summary))
.collect::<Vec<_>>();
let matches = fuzzy::match_strings(
&candidates,
&query,
false,
100,
&Default::default(),
executor,
)
.await;
matches
.into_iter()
.map(|mat| threads[mat.candidate_id].clone())
.collect()
}
});
let search_task = search_threads(query, threads, cx);
cx.spawn_in(window, async move |this, cx| {
let matches = search_task.await;
this.update(cx, |this, cx| {
@@ -176,7 +142,9 @@ impl PickerDelegate for ThreadContextPickerDelegate {
this.update_in(cx, |this, window, cx| {
this.delegate
.context_store
.update(cx, |context_store, cx| context_store.add_thread(thread, cx))
.update(cx, |context_store, cx| {
context_store.add_thread(thread, true, cx)
})
.ok();
match this.delegate.confirm_behavior {
@@ -248,3 +216,46 @@ pub fn render_thread_context_entry(
)
})
}
pub(crate) fn search_threads(
query: String,
thread_store: Entity<ThreadStore>,
cx: &mut App,
) -> Task<Vec<ThreadContextEntry>> {
let threads = thread_store.update(cx, |this, _cx| {
this.threads()
.into_iter()
.map(|thread| ThreadContextEntry {
id: thread.id,
summary: thread.summary,
})
.collect::<Vec<_>>()
});
let executor = cx.background_executor().clone();
cx.background_spawn(async move {
if query.is_empty() {
threads
} else {
let candidates = threads
.iter()
.enumerate()
.map(|(id, thread)| StringMatchCandidate::new(id, &thread.summary))
.collect::<Vec<_>>();
let matches = fuzzy::match_strings(
&candidates,
&query,
false,
100,
&Default::default(),
executor,
)
.await;
matches
.into_iter()
.map(|mat| threads[mat.candidate_id].clone())
.collect()
}
})
}

View File

@@ -64,6 +64,7 @@ impl ContextStore {
pub fn add_file_from_path(
&mut self,
project_path: ProjectPath,
remove_if_exists: bool,
cx: &mut Context<Self>,
) -> Task<Result<()>> {
let workspace = self.workspace.clone();
@@ -86,7 +87,9 @@ impl ContextStore {
let already_included = this.update(cx, |this, _cx| {
match this.will_include_buffer(buffer_id, &project_path.path) {
Some(FileInclusion::Direct(context_id)) => {
this.remove_context(context_id);
if remove_if_exists {
this.remove_context(context_id);
}
true
}
Some(FileInclusion::InDirectory(_)) => true,
@@ -157,6 +160,7 @@ impl ContextStore {
pub fn add_directory(
&mut self,
project_path: ProjectPath,
remove_if_exists: bool,
cx: &mut Context<Self>,
) -> Task<Result<()>> {
let workspace = self.workspace.clone();
@@ -169,7 +173,9 @@ impl ContextStore {
let already_included = if let Some(context_id) = self.includes_directory(&project_path.path)
{
self.remove_context(context_id);
if remove_if_exists {
self.remove_context(context_id);
}
true
} else {
false
@@ -256,9 +262,16 @@ impl ContextStore {
)));
}
pub fn add_thread(&mut self, thread: Entity<Thread>, cx: &mut Context<Self>) {
pub fn add_thread(
&mut self,
thread: Entity<Thread>,
remove_if_exists: bool,
cx: &mut Context<Self>,
) {
if let Some(context_id) = self.includes_thread(&thread.read(cx).id()) {
self.remove_context(context_id);
if remove_if_exists {
self.remove_context(context_id);
}
} else {
self.insert_thread(thread, cx);
}

View File

@@ -39,7 +39,6 @@ impl ContextStrip {
pub fn new(
context_store: Entity<ContextStore>,
workspace: WeakEntity<Workspace>,
editor: WeakEntity<Editor>,
thread_store: Option<WeakEntity<ThreadStore>>,
context_picker_menu_handle: PopoverMenuHandle<ContextPicker>,
suggest_context_kind: SuggestContextKind,
@@ -51,7 +50,6 @@ impl ContextStrip {
workspace.clone(),
thread_store.clone(),
context_store.downgrade(),
editor.clone(),
ConfirmBehavior::KeepOpen,
window,
cx,

View File

@@ -324,7 +324,7 @@ impl InlineAssistant {
) {
let (snapshot, initial_selections) = editor.update(cx, |editor, cx| {
(
editor.buffer().read(cx).snapshot(cx),
editor.snapshot(window, cx),
editor.selections.all::<Point>(cx),
)
});
@@ -338,7 +338,37 @@ impl InlineAssistant {
if selection.end.column == 0 {
selection.end.row -= 1;
}
selection.end.column = snapshot.line_len(MultiBufferRow(selection.end.row));
selection.end.column = snapshot
.buffer_snapshot
.line_len(MultiBufferRow(selection.end.row));
} else if let Some(fold) =
snapshot.crease_for_buffer_row(MultiBufferRow(selection.end.row))
{
selection.start = fold.range().start;
selection.end = fold.range().end;
if MultiBufferRow(selection.end.row) < snapshot.buffer_snapshot.max_row() {
let chars = snapshot
.buffer_snapshot
.chars_at(Point::new(selection.end.row + 1, 0));
for c in chars {
if c == '\n' {
break;
}
if c.is_whitespace() {
continue;
}
if snapshot
.language_at(selection.end)
.is_some_and(|language| language.config().brackets.is_closing_brace(c))
{
selection.end.row += 1;
selection.end.column = snapshot
.buffer_snapshot
.line_len(MultiBufferRow(selection.end.row));
}
}
}
}
if let Some(prev_selection) = selections.last_mut() {
@@ -354,6 +384,7 @@ impl InlineAssistant {
}
selections.push(selection);
}
let snapshot = &snapshot.buffer_snapshot;
let newest_selection = newest_selection.unwrap();
let mut codegen_ranges = Vec::new();

View File

@@ -861,7 +861,6 @@ impl PromptEditor<BufferCodegen> {
ContextStrip::new(
context_store.clone(),
workspace.clone(),
prompt_editor.downgrade(),
thread_store.clone(),
context_picker_menu_handle.clone(),
SuggestContextKind::Thread,
@@ -1014,7 +1013,6 @@ impl PromptEditor<TerminalCodegen> {
ContextStrip::new(
context_store.clone(),
workspace.clone(),
prompt_editor.downgrade(),
thread_store.clone(),
context_picker_menu_handle.clone(),
SuggestContextKind::Thread,

View File

@@ -2,42 +2,39 @@ use std::sync::Arc;
use collections::HashSet;
use editor::actions::MoveUp;
use editor::{Editor, EditorElement, EditorEvent, EditorStyle};
use editor::{ContextMenuOptions, ContextMenuPlacement, Editor, EditorElement, EditorStyle};
use fs::Fs;
use git::ExpandCommitEditor;
use git_ui::git_panel;
use gpui::{
Animation, AnimationExt, App, DismissEvent, Entity, Focusable, Subscription, TextStyle,
point, Animation, AnimationExt, App, DismissEvent, Entity, Focusable, Subscription, TextStyle,
WeakEntity,
};
use language_model::LanguageModelRegistry;
use language_model_selector::ToggleModelSelector;
use project::Project;
use rope::Point;
use settings::Settings;
use std::time::Duration;
use text::Bias;
use theme::ThemeSettings;
use ui::{
prelude::*, ButtonLike, KeyBinding, PlatformStyle, PopoverMenu, PopoverMenuHandle, Tooltip,
};
use util::ResultExt;
use vim_mode_setting::VimModeSetting;
use workspace::notifications::{NotificationId, NotifyTaskExt};
use workspace::{Toast, Workspace};
use workspace::Workspace;
use crate::assistant_model_selector::AssistantModelSelector;
use crate::context_picker::{ConfirmBehavior, ContextPicker};
use crate::context_picker::{ConfirmBehavior, ContextPicker, ContextPickerCompletionProvider};
use crate::context_store::{refresh_context_store_text, ContextStore};
use crate::context_strip::{ContextStrip, ContextStripEvent, SuggestContextKind};
use crate::profile_selector::ProfileSelector;
use crate::thread::{RequestKind, Thread};
use crate::thread_store::ThreadStore;
use crate::tool_selector::ToolSelector;
use crate::{Chat, ChatMode, RemoveAllContext, ToggleContextPicker};
use crate::{Chat, ChatMode, RemoveAllContext, ThreadEvent, ToggleContextPicker};
pub struct MessageEditor {
thread: Entity<Thread>,
editor: Entity<Editor>,
#[allow(dead_code)]
workspace: WeakEntity<Workspace>,
project: Entity<Project>,
context_store: Entity<ContextStore>,
@@ -46,7 +43,7 @@ pub struct MessageEditor {
inline_context_picker: Entity<ContextPicker>,
inline_context_picker_menu_handle: PopoverMenuHandle<ContextPicker>,
model_selector: Entity<AssistantModelSelector>,
tool_selector: Entity<ToolSelector>,
profile_selector: Entity<ProfileSelector>,
_subscriptions: Vec<Subscription>,
}
@@ -60,7 +57,6 @@ impl MessageEditor {
window: &mut Window,
cx: &mut Context<Self>,
) -> Self {
let tools = thread.read(cx).tools().clone();
let context_picker_menu_handle = PopoverMenuHandle::default();
let inline_context_picker_menu_handle = PopoverMenuHandle::default();
let model_selector_menu_handle = PopoverMenuHandle::default();
@@ -69,16 +65,30 @@ impl MessageEditor {
let mut editor = Editor::auto_height(10, window, cx);
editor.set_placeholder_text("Ask anything, @ to mention, ↑ to select", cx);
editor.set_show_indent_guides(false, cx);
editor.set_context_menu_options(ContextMenuOptions {
min_entries_visible: 12,
max_entries_visible: 12,
placement: Some(ContextMenuPlacement::Above),
});
editor
});
let editor_entity = editor.downgrade();
editor.update(cx, |editor, _| {
editor.set_completion_provider(Some(Box::new(ContextPickerCompletionProvider::new(
workspace.clone(),
context_store.downgrade(),
Some(thread_store.clone()),
editor_entity,
))));
});
let inline_context_picker = cx.new(|cx| {
ContextPicker::new(
workspace.clone(),
Some(thread_store.clone()),
context_store.downgrade(),
editor.downgrade(),
ConfirmBehavior::Close,
window,
cx,
@@ -89,7 +99,6 @@ impl MessageEditor {
ContextStrip::new(
context_store.clone(),
workspace.clone(),
editor.downgrade(),
Some(thread_store.clone()),
context_picker_menu_handle.clone(),
SuggestContextKind::File,
@@ -99,7 +108,6 @@ impl MessageEditor {
});
let subscriptions = vec![
cx.subscribe_in(&editor, window, Self::handle_editor_event),
cx.subscribe_in(
&inline_context_picker,
window,
@@ -120,14 +128,14 @@ impl MessageEditor {
inline_context_picker_menu_handle,
model_selector: cx.new(|cx| {
AssistantModelSelector::new(
fs,
fs.clone(),
model_selector_menu_handle,
editor.focus_handle(cx),
window,
cx,
)
}),
tool_selector: cx.new(|cx| ToolSelector::new(tools, cx)),
profile_selector: cx.new(|cx| ProfileSelector::new(fs, thread_store, cx)),
_subscriptions: subscriptions,
}
}
@@ -144,7 +152,6 @@ impl MessageEditor {
) {
self.context_picker_menu_handle.toggle(window, cx);
}
pub fn remove_all_context(
&mut self,
_: &RemoveAllContext,
@@ -206,13 +213,23 @@ impl MessageEditor {
let refresh_task =
refresh_context_store_text(self.context_store.clone(), &HashSet::default(), cx);
let system_prompt_context_task = self.thread.read(cx).load_system_prompt_context(cx);
let thread = self.thread.clone();
let context_store = self.context_store.clone();
let git_store = self.project.read(cx).git_store();
let checkpoint = git_store.read(cx).checkpoint(cx);
let checkpoint = self.project.read(cx).git_store().read(cx).checkpoint(cx);
cx.spawn(async move |_, cx| {
let checkpoint = checkpoint.await.ok();
refresh_task.await;
let checkpoint = checkpoint.await.log_err();
let (system_prompt_context, load_error) = system_prompt_context_task.await;
thread
.update(cx, |thread, cx| {
thread.set_system_prompt_context(system_prompt_context);
if let Some(load_error) = load_error {
cx.emit(ThreadEvent::ShowError(load_error));
}
})
.ok();
thread
.update(cx, |thread, cx| {
let context = context_store.read(cx).snapshot(cx).collect::<Vec<_>>();
@@ -224,34 +241,6 @@ impl MessageEditor {
.detach();
}
fn handle_editor_event(
&mut self,
editor: &Entity<Editor>,
event: &EditorEvent,
window: &mut Window,
cx: &mut Context<Self>,
) {
match event {
EditorEvent::SelectionsChanged { .. } => {
editor.update(cx, |editor, cx| {
let snapshot = editor.buffer().read(cx).snapshot(cx);
let newest_cursor = editor.selections.newest::<Point>(cx).head();
if newest_cursor.column > 0 {
let behind_cursor = snapshot.clip_point(
Point::new(newest_cursor.row, newest_cursor.column - 1),
Bias::Left,
);
let char_behind_cursor = snapshot.chars_at(behind_cursor).next();
if char_behind_cursor == Some('@') {
self.inline_context_picker_menu_handle.show(window, cx);
}
}
});
}
_ => {}
}
}
fn handle_inline_context_picker_event(
&mut self,
_inline_context_picker: &Entity<ContextPicker>,
@@ -290,34 +279,6 @@ impl MessageEditor {
self.context_strip.focus_handle(cx).focus(window);
}
}
fn handle_feedback_click(
&mut self,
is_positive: bool,
window: &mut Window,
cx: &mut Context<Self>,
) {
let workspace = self.workspace.clone();
let report = self
.thread
.update(cx, |thread, cx| thread.report_feedback(is_positive, cx));
cx.spawn(async move |_, cx| {
report.await?;
workspace.update(cx, |workspace, cx| {
let message = if is_positive {
"Positive feedback recorded. Thank you!"
} else {
"Negative feedback recorded. Thank you for helping us improve!"
};
struct ThreadFeedback;
let id = NotificationId::unique::<ThreadFeedback>();
workspace.show_toast(Toast::new(id, message).autohide(), cx)
})
})
.detach_and_notify_err(window, cx);
}
}
impl Focusable for MessageEditor {
@@ -330,9 +291,11 @@ impl Render for MessageEditor {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let font_size = TextSize::Default.rems(cx);
let line_height = font_size.to_pixels(window.rem_size()) * 1.5;
let focus_handle = self.editor.focus_handle(cx);
let inline_context_picker = self.inline_context_picker.clone();
let bg_color = cx.theme().colors().editor_background;
let empty_thread = self.thread.read(cx).is_empty();
let is_generating = self.thread.read(cx).is_generating();
let is_model_selected = self.is_model_selected(cx);
let is_editor_empty = self.is_editor_empty(cx);
@@ -354,11 +317,29 @@ impl Render for MessageEditor {
let project = self.thread.read(cx).project();
let changed_files = if let Some(repository) = project.read(cx).active_repository(cx) {
repository.read(cx).status().count()
repository.read(cx).cached_status().count()
} else {
0
};
let border_color = cx.theme().colors().border;
let active_color = cx.theme().colors().element_selected;
let editor_bg_color = cx.theme().colors().editor_background;
let bg_edit_files_disclosure = editor_bg_color.blend(active_color.opacity(0.3));
let edit_files_container = || {
h_flex()
.mx_2()
.py_1()
.pl_2p5()
.pr_1()
.bg(bg_edit_files_disclosure)
.border_1()
.border_color(border_color)
.justify_between()
.flex_wrap()
};
v_flex()
.size_full()
.when(is_generating, |parent| {
@@ -370,7 +351,7 @@ impl Render for MessageEditor {
.pl_2()
.pr_1()
.py_1()
.bg(cx.theme().colors().editor_background)
.bg(editor_bg_color)
.border_1()
.border_color(cx.theme().colors().border_variant)
.rounded_lg()
@@ -419,73 +400,163 @@ impl Render for MessageEditor {
),
)
})
.when(changed_files > 0, |parent| {
parent.child(
v_flex()
.mx_2()
.bg(cx.theme().colors().element_background)
.border_1()
.border_b_0()
.border_color(cx.theme().colors().border)
.rounded_t_md()
.child(
h_flex()
.justify_between()
.p_2()
.child(
h_flex()
.gap_2()
.child(
IconButton::new(
"edits-disclosure",
IconName::GitBranchSmall,
)
.icon_size(IconSize::Small)
.on_click(
|_ev, _window, cx| {
cx.defer(|cx| {
cx.dispatch_action(&git_panel::ToggleFocus)
});
},
),
)
.child(
Label::new(format!(
"{} {} changed",
changed_files,
if changed_files == 1 { "file" } else { "files" }
))
.size(LabelSize::XSmall)
.color(Color::Muted),
),
)
.child(
h_flex()
.gap_2()
.child(
Button::new("review", "Review")
.label_size(LabelSize::XSmall)
.on_click(|_event, _window, cx| {
cx.defer(|cx| {
cx.dispatch_action(
&git_ui::project_diff::Diff,
);
});
}),
)
.child(
Button::new("commit", "Commit")
.label_size(LabelSize::XSmall)
.on_click(|_event, _window, cx| {
cx.defer(|cx| {
cx.dispatch_action(&ExpandCommitEditor)
});
}),
),
),
),
)
})
.when(
changed_files > 0 && !is_generating && !empty_thread,
|parent| {
parent.child(
edit_files_container()
.border_b_0()
.rounded_t_md()
.shadow(smallvec::smallvec![gpui::BoxShadow {
color: gpui::black().opacity(0.15),
offset: point(px(1.), px(-1.)),
blur_radius: px(3.),
spread_radius: px(0.),
}])
.child(
h_flex()
.gap_2()
.child(Label::new("Edits").size(LabelSize::XSmall))
.child(div().size_1().rounded_full().bg(border_color))
.child(
Label::new(format!(
"{} {}",
changed_files,
if changed_files == 1 { "file" } else { "files" }
))
.size(LabelSize::XSmall),
),
)
.child(
h_flex()
.gap_1()
.child(
Button::new("panel", "Open Git Panel")
.label_size(LabelSize::XSmall)
.key_binding({
let focus_handle = focus_handle.clone();
KeyBinding::for_action_in(
&git_panel::ToggleFocus,
&focus_handle,
window,
cx,
)
.map(|kb| kb.size(rems_from_px(10.)))
})
.on_click(|_ev, _window, cx| {
cx.defer(|cx| {
cx.dispatch_action(&git_panel::ToggleFocus)
});
}),
)
.child(
Button::new("review", "Review Diff")
.label_size(LabelSize::XSmall)
.key_binding({
let focus_handle = focus_handle.clone();
KeyBinding::for_action_in(
&git_ui::project_diff::Diff,
&focus_handle,
window,
cx,
)
.map(|kb| kb.size(rems_from_px(10.)))
})
.on_click(|_event, _window, cx| {
cx.defer(|cx| {
cx.dispatch_action(&git_ui::project_diff::Diff)
});
}),
)
.child(
Button::new("commit", "Commit Changes")
.label_size(LabelSize::XSmall)
.key_binding({
let focus_handle = focus_handle.clone();
KeyBinding::for_action_in(
&ExpandCommitEditor,
&focus_handle,
window,
cx,
)
.map(|kb| kb.size(rems_from_px(10.)))
})
.on_click(|_event, _window, cx| {
cx.defer(|cx| {
cx.dispatch_action(&ExpandCommitEditor)
});
}),
),
),
)
},
)
.when(
changed_files > 0 && !is_generating && empty_thread,
|parent| {
parent.child(
edit_files_container()
.mb_2()
.rounded_md()
.child(
h_flex()
.gap_2()
.child(Label::new("Consider committing your changes before starting a fresh thread").size(LabelSize::XSmall))
.child(div().size_1().rounded_full().bg(border_color))
.child(
Label::new(format!(
"{} {}",
changed_files,
if changed_files == 1 { "file" } else { "files" }
))
.size(LabelSize::XSmall),
),
)
.child(
h_flex()
.gap_1()
.child(
Button::new("review", "Review Diff")
.label_size(LabelSize::XSmall)
.key_binding({
let focus_handle = focus_handle.clone();
KeyBinding::for_action_in(
&git_ui::project_diff::Diff,
&focus_handle,
window,
cx,
)
.map(|kb| kb.size(rems_from_px(10.)))
})
.on_click(|_event, _window, cx| {
cx.defer(|cx| {
cx.dispatch_action(&git_ui::project_diff::Diff)
});
}),
)
.child(
Button::new("commit", "Commit Changes")
.label_size(LabelSize::XSmall)
.key_binding({
let focus_handle = focus_handle.clone();
KeyBinding::for_action_in(
&ExpandCommitEditor,
&focus_handle,
window,
cx,
)
.map(|kb| kb.size(rems_from_px(10.)))
})
.on_click(|_event, _window, cx| {
cx.defer(|cx| {
cx.dispatch_action(&ExpandCommitEditor)
});
}),
),
),
)
},
)
.child(
v_flex()
.key_context("MessageEditor")
@@ -500,48 +571,10 @@ impl Render for MessageEditor {
.on_action(cx.listener(Self::toggle_chat_mode))
.gap_2()
.p_2()
.bg(bg_color)
.bg(editor_bg_color)
.border_t_1()
.border_color(cx.theme().colors().border)
.child(
h_flex()
.justify_between()
.child(self.context_strip.clone())
.when(!self.thread.read(cx).is_empty(), |this| {
this.child(
h_flex()
.gap_2()
.child(
IconButton::new(
"feedback-thumbs-up",
IconName::ThumbsUp,
)
.style(ButtonStyle::Subtle)
.icon_size(IconSize::Small)
.tooltip(Tooltip::text("Helpful"))
.on_click(
cx.listener(|this, _, window, cx| {
this.handle_feedback_click(true, window, cx);
}),
),
)
.child(
IconButton::new(
"feedback-thumbs-down",
IconName::ThumbsDown,
)
.style(ButtonStyle::Subtle)
.icon_size(IconSize::Small)
.tooltip(Tooltip::text("Not Helpful"))
.on_click(
cx.listener(|this, _, window, cx| {
this.handle_feedback_click(false, window, cx);
}),
),
),
)
}),
)
.child(h_flex().justify_between().child(self.context_strip.clone()))
.child(
v_flex()
.gap_5()
@@ -561,9 +594,10 @@ impl Render for MessageEditor {
EditorElement::new(
&self.editor,
EditorStyle {
background: bg_color,
background: editor_bg_color,
local_player: cx.theme().players().local(),
text: text_style,
syntax: cx.theme().syntax().clone(),
..Default::default()
},
)
@@ -589,7 +623,7 @@ impl Render for MessageEditor {
.child(
h_flex()
.justify_between()
.child(h_flex().gap_2().child(self.tool_selector.clone()))
.child(h_flex().gap_2().child(self.profile_selector.clone()))
.child(
h_flex().gap_1().child(self.model_selector.clone()).child(
ButtonLike::new("submit-message")

View File

@@ -0,0 +1,121 @@
use std::sync::Arc;
use assistant_settings::{AgentProfile, AssistantSettings};
use fs::Fs;
use gpui::{prelude::*, Action, Entity, Subscription, WeakEntity};
use indexmap::IndexMap;
use settings::{update_settings_file, Settings as _, SettingsStore};
use ui::{prelude::*, ContextMenu, ContextMenuEntry, PopoverMenu, Tooltip};
use util::ResultExt as _;
use crate::{ManageProfiles, ThreadStore};
pub struct ProfileSelector {
profiles: IndexMap<Arc<str>, AgentProfile>,
fs: Arc<dyn Fs>,
thread_store: WeakEntity<ThreadStore>,
_subscriptions: Vec<Subscription>,
}
impl ProfileSelector {
pub fn new(
fs: Arc<dyn Fs>,
thread_store: WeakEntity<ThreadStore>,
cx: &mut Context<Self>,
) -> Self {
let settings_subscription = cx.observe_global::<SettingsStore>(move |this, cx| {
this.refresh_profiles(cx);
});
let mut this = Self {
profiles: IndexMap::default(),
fs,
thread_store,
_subscriptions: vec![settings_subscription],
};
this.refresh_profiles(cx);
this
}
fn refresh_profiles(&mut self, cx: &mut Context<Self>) {
let settings = AssistantSettings::get_global(cx);
self.profiles = settings.profiles.clone();
}
fn build_context_menu(
&self,
window: &mut Window,
cx: &mut Context<Self>,
) -> Entity<ContextMenu> {
ContextMenu::build(window, cx, |mut menu, _window, cx| {
let settings = AssistantSettings::get_global(cx);
let icon_position = IconPosition::Start;
menu = menu.header("Profiles");
for (profile_id, profile) in self.profiles.clone() {
menu = menu.toggleable_entry(
profile.name.clone(),
profile_id == settings.default_profile,
icon_position,
None,
{
let fs = self.fs.clone();
let thread_store = self.thread_store.clone();
move |_window, cx| {
update_settings_file::<AssistantSettings>(fs.clone(), cx, {
let profile_id = profile_id.clone();
move |settings, _cx| {
settings.set_profile(profile_id.clone());
}
});
thread_store
.update(cx, |this, cx| {
this.load_profile_by_id(&profile_id, cx);
})
.log_err();
}
},
);
}
menu = menu.separator();
menu = menu.item(
ContextMenuEntry::new("Configure Profiles")
.icon(IconName::Pencil)
.icon_color(Color::Muted)
.handler(move |window, cx| {
window.dispatch_action(ManageProfiles.boxed_clone(), cx);
}),
);
menu
})
}
}
impl Render for ProfileSelector {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let settings = AssistantSettings::get_global(cx);
let profile = settings
.profiles
.get(&settings.default_profile)
.map(|profile| profile.name.clone())
.unwrap_or_else(|| "Unknown".into());
let this = cx.entity().clone();
PopoverMenu::new("tool-selector")
.menu(move |window, cx| {
Some(this.update(cx, |this, cx| this.build_context_menu(window, cx)))
})
.trigger_with_tooltip(
Button::new("profile-selector-button", profile)
.style(ButtonStyle::Filled)
.label_size(LabelSize::Small),
Tooltip::text("Change Profile"),
)
.anchor(gpui::Corner::BottomLeft)
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,8 +1,10 @@
use std::borrow::Cow;
use std::path::PathBuf;
use std::sync::Arc;
use anyhow::{anyhow, Result};
use assistant_tool::{ToolId, ToolWorkingSet};
use assistant_settings::{AgentProfile, AssistantSettings};
use assistant_tool::{ToolId, ToolSource, ToolWorkingSet};
use chrono::{DateTime, Utc};
use collections::HashMap;
use context_server::manager::ContextServerManager;
@@ -12,15 +14,16 @@ use futures::FutureExt as _;
use gpui::{
prelude::*, App, BackgroundExecutor, Context, Entity, Global, ReadGlobal, SharedString, Task,
};
use heed::types::{SerdeBincode, SerdeJson};
use heed::types::SerdeBincode;
use heed::Database;
use language_model::{LanguageModelToolUseId, Role};
use project::Project;
use prompt_store::PromptBuilder;
use serde::{Deserialize, Serialize};
use settings::Settings as _;
use util::ResultExt as _;
use crate::thread::{MessageId, ProjectSnapshot, Thread, ThreadId};
use crate::thread::{MessageId, ProjectSnapshot, Thread, ThreadEvent, ThreadId};
pub fn init(cx: &mut App) {
ThreadsDatabase::init(cx);
@@ -56,6 +59,7 @@ impl ThreadStore {
context_server_tool_ids: HashMap::default(),
threads: Vec::new(),
};
this.load_default_profile(cx);
this.register_context_server_handlers(cx);
this.reload(cx).detach_and_log_err(cx);
@@ -113,7 +117,7 @@ impl ThreadStore {
.await?
.ok_or_else(|| anyhow!("no thread found with ID: {id:?}"))?;
this.update(cx, |this, cx| {
let thread = this.update(cx, |this, cx| {
cx.new(|cx| {
Thread::deserialize(
id.clone(),
@@ -124,7 +128,19 @@ impl ThreadStore {
cx,
)
})
})
})?;
let (system_prompt_context, load_error) = thread
.update(cx, |thread, cx| thread.load_system_prompt_context(cx))?
.await;
thread.update(cx, |thread, cx| {
thread.set_system_prompt_context(system_prompt_context);
if let Some(load_error) = load_error {
cx.emit(ThreadEvent::ShowError(load_error));
}
})?;
Ok(thread)
})
}
@@ -171,6 +187,45 @@ impl ThreadStore {
})
}
fn load_default_profile(&self, cx: &Context<Self>) {
let assistant_settings = AssistantSettings::get_global(cx);
self.load_profile_by_id(&assistant_settings.default_profile, cx);
}
pub fn load_profile_by_id(&self, profile_id: &Arc<str>, cx: &Context<Self>) {
let assistant_settings = AssistantSettings::get_global(cx);
if let Some(profile) = assistant_settings.profiles.get(profile_id) {
self.load_profile(profile);
}
}
pub fn load_profile(&self, profile: &AgentProfile) {
self.tools.disable_all_tools();
self.tools.enable(
ToolSource::Native,
&profile
.tools
.iter()
.filter_map(|(tool, enabled)| enabled.then(|| tool.clone()))
.collect::<Vec<_>>(),
);
for (context_server_id, preset) in &profile.context_servers {
self.tools.enable(
ToolSource::ContextServer {
id: context_server_id.clone().into(),
},
&preset
.tools
.iter()
.filter_map(|(tool, enabled)| enabled.then(|| tool.clone()))
.collect::<Vec<_>>(),
)
}
}
fn register_context_server_handlers(&self, cx: &mut Context<Self>) {
cx.subscribe(
&self.context_server_manager.clone(),
@@ -247,6 +302,7 @@ pub struct SerializedThreadMetadata {
#[derive(Serialize, Deserialize)]
pub struct SerializedThread {
pub version: String,
pub summary: SharedString,
pub updated_at: DateTime<Utc>,
pub messages: Vec<SerializedMessage>,
@@ -254,17 +310,55 @@ pub struct SerializedThread {
pub initial_project_snapshot: Option<Arc<ProjectSnapshot>>,
}
impl SerializedThread {
pub const VERSION: &'static str = "0.1.0";
pub fn from_json(json: &[u8]) -> Result<Self> {
let saved_thread_json = serde_json::from_slice::<serde_json::Value>(json)?;
match saved_thread_json.get("version") {
Some(serde_json::Value::String(version)) => match version.as_str() {
SerializedThread::VERSION => Ok(serde_json::from_value::<SerializedThread>(
saved_thread_json,
)?),
_ => Err(anyhow!(
"unrecognized serialized thread version: {}",
version
)),
},
None => {
let saved_thread =
serde_json::from_value::<LegacySerializedThread>(saved_thread_json)?;
Ok(saved_thread.upgrade())
}
version => Err(anyhow!(
"unrecognized serialized thread version: {:?}",
version
)),
}
}
}
#[derive(Debug, Serialize, Deserialize)]
pub struct SerializedMessage {
pub id: MessageId,
pub role: Role,
pub text: String,
#[serde(default)]
pub segments: Vec<SerializedMessageSegment>,
#[serde(default)]
pub tool_uses: Vec<SerializedToolUse>,
#[serde(default)]
pub tool_results: Vec<SerializedToolResult>,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(tag = "type")]
pub enum SerializedMessageSegment {
#[serde(rename = "text")]
Text { text: String },
#[serde(rename = "thinking")]
Thinking { text: String },
}
#[derive(Debug, Serialize, Deserialize)]
pub struct SerializedToolUse {
pub id: LanguageModelToolUseId,
@@ -279,6 +373,50 @@ pub struct SerializedToolResult {
pub content: Arc<str>,
}
#[derive(Serialize, Deserialize)]
struct LegacySerializedThread {
pub summary: SharedString,
pub updated_at: DateTime<Utc>,
pub messages: Vec<LegacySerializedMessage>,
#[serde(default)]
pub initial_project_snapshot: Option<Arc<ProjectSnapshot>>,
}
impl LegacySerializedThread {
pub fn upgrade(self) -> SerializedThread {
SerializedThread {
version: SerializedThread::VERSION.to_string(),
summary: self.summary,
updated_at: self.updated_at,
messages: self.messages.into_iter().map(|msg| msg.upgrade()).collect(),
initial_project_snapshot: self.initial_project_snapshot,
}
}
}
#[derive(Debug, Serialize, Deserialize)]
struct LegacySerializedMessage {
pub id: MessageId,
pub role: Role,
pub text: String,
#[serde(default)]
pub tool_uses: Vec<SerializedToolUse>,
#[serde(default)]
pub tool_results: Vec<SerializedToolResult>,
}
impl LegacySerializedMessage {
fn upgrade(self) -> SerializedMessage {
SerializedMessage {
id: self.id,
role: self.role,
segments: vec![SerializedMessageSegment::Text { text: self.text }],
tool_uses: self.tool_uses,
tool_results: self.tool_results,
}
}
}
struct GlobalThreadsDatabase(
Shared<BoxFuture<'static, Result<Arc<ThreadsDatabase>, Arc<anyhow::Error>>>>,
);
@@ -288,7 +426,25 @@ impl Global for GlobalThreadsDatabase {}
pub(crate) struct ThreadsDatabase {
executor: BackgroundExecutor,
env: heed::Env,
threads: Database<SerdeBincode<ThreadId>, SerdeJson<SerializedThread>>,
threads: Database<SerdeBincode<ThreadId>, SerializedThread>,
}
impl heed::BytesEncode<'_> for SerializedThread {
type EItem = SerializedThread;
fn bytes_encode(item: &Self::EItem) -> Result<Cow<[u8]>, heed::BoxedError> {
serde_json::to_vec(item).map(Cow::Owned).map_err(Into::into)
}
}
impl<'a> heed::BytesDecode<'a> for SerializedThread {
type DItem = SerializedThread;
fn bytes_decode(bytes: &'a [u8]) -> Result<Self::DItem, heed::BoxedError> {
// We implement this type manually because we want to call `SerializedThread::from_json`,
// instead of the Deserialize trait implementation for `SerializedThread`.
SerializedThread::from_json(bytes).map_err(Into::into)
}
}
impl ThreadsDatabase {

View File

@@ -1,156 +0,0 @@
use std::sync::Arc;
use assistant_tool::{ToolSource, ToolWorkingSet};
use gpui::Entity;
use scripting_tool::ScriptingTool;
use ui::{prelude::*, ContextMenu, PopoverMenu, Tooltip};
use crate::agent_profile::AgentProfile;
pub struct ToolSelector {
profiles: Vec<AgentProfile>,
tools: Arc<ToolWorkingSet>,
}
impl ToolSelector {
pub fn new(tools: Arc<ToolWorkingSet>, _cx: &mut Context<Self>) -> Self {
Self {
profiles: vec![AgentProfile::read_only(), AgentProfile::code_writer()],
tools,
}
}
fn build_context_menu(
&self,
window: &mut Window,
cx: &mut Context<Self>,
) -> Entity<ContextMenu> {
let profiles = self.profiles.clone();
let tool_set = self.tools.clone();
ContextMenu::build_persistent(window, cx, move |mut menu, _window, cx| {
let icon_position = IconPosition::End;
menu = menu.header("Profiles");
for profile in profiles.clone() {
menu = menu.toggleable_entry(profile.name.clone(), false, icon_position, None, {
let tools = tool_set.clone();
move |_window, cx| {
tools.disable_source(ToolSource::Native, cx);
tools.enable(
ToolSource::Native,
&profile
.tools
.iter()
.filter_map(|(tool, enabled)| enabled.then(|| tool.clone()))
.collect::<Vec<_>>(),
);
}
});
}
menu = menu.separator();
let tools_by_source = tool_set.tools_by_source(cx);
let all_tools_enabled = tool_set.are_all_tools_enabled();
menu = menu.toggleable_entry("All Tools", all_tools_enabled, icon_position, None, {
let tools = tool_set.clone();
move |_window, cx| {
if all_tools_enabled {
tools.disable_all_tools(cx);
} else {
tools.enable_all_tools();
}
}
});
for (source, tools) in tools_by_source {
let mut tools = tools
.into_iter()
.map(|tool| {
let source = tool.source();
let name = tool.name().into();
let is_enabled = tool_set.is_enabled(&source, &name);
(source, name, is_enabled)
})
.collect::<Vec<_>>();
if ToolSource::Native == source {
tools.push((
ToolSource::Native,
ScriptingTool::NAME.into(),
tool_set.is_scripting_tool_enabled(),
));
tools.sort_by(|(_, name_a, _), (_, name_b, _)| name_a.cmp(name_b));
}
menu = match &source {
ToolSource::Native => menu.separator().header("Zed Tools"),
ToolSource::ContextServer { id } => {
let all_tools_from_source_enabled =
tool_set.are_all_tools_from_source_enabled(&source);
menu.separator().header(id).toggleable_entry(
"All Tools",
all_tools_from_source_enabled,
icon_position,
None,
{
let tools = tool_set.clone();
let source = source.clone();
move |_window, cx| {
if all_tools_from_source_enabled {
tools.disable_source(source.clone(), cx);
} else {
tools.enable_source(&source);
}
}
},
)
}
};
for (source, name, is_enabled) in tools {
menu = menu.toggleable_entry(name.clone(), is_enabled, icon_position, None, {
let tools = tool_set.clone();
move |_window, _cx| {
if name.as_ref() == ScriptingTool::NAME {
if is_enabled {
tools.disable_scripting_tool();
} else {
tools.enable_scripting_tool();
}
} else {
if is_enabled {
tools.disable(source.clone(), &[name.clone()]);
} else {
tools.enable(source.clone(), &[name.clone()]);
}
}
}
});
}
}
menu
})
}
}
impl Render for ToolSelector {
fn render(&mut self, _window: &mut Window, cx: &mut Context<'_, Self>) -> impl IntoElement {
let this = cx.entity().clone();
PopoverMenu::new("tool-selector")
.menu(move |window, cx| {
Some(this.update(cx, |this, cx| this.build_context_menu(window, cx)))
})
.trigger_with_tooltip(
IconButton::new("tool-selector-button", IconName::SettingsAlt)
.icon_size(IconSize::Small)
.icon_color(Color::Muted),
Tooltip::text("Customize Tools"),
)
.anchor(gpui::Corner::BottomLeft)
}
}

View File

@@ -1,14 +1,16 @@
use std::sync::Arc;
use anyhow::Result;
use assistant_tool::{Tool, ToolWorkingSet};
use collections::HashMap;
use futures::future::Shared;
use futures::FutureExt as _;
use gpui::{SharedString, Task};
use gpui::{App, SharedString, Task};
use language_model::{
LanguageModelRequestMessage, LanguageModelToolResult, LanguageModelToolUse,
LanguageModelToolUseId, MessageContent, Role,
};
use ui::IconName;
use crate::thread::MessageId;
use crate::thread_store::SerializedMessage;
@@ -17,12 +19,15 @@ use crate::thread_store::SerializedMessage;
pub struct ToolUse {
pub id: LanguageModelToolUseId,
pub name: SharedString,
pub ui_text: SharedString,
pub status: ToolUseStatus,
pub input: serde_json::Value,
pub icon: ui::IconName,
}
#[derive(Debug, Clone)]
pub enum ToolUseStatus {
NeedsConfirmation,
Pending,
Running,
Finished(SharedString),
@@ -30,6 +35,7 @@ pub enum ToolUseStatus {
}
pub struct ToolUseState {
tools: Arc<ToolWorkingSet>,
tool_uses_by_assistant_message: HashMap<MessageId, Vec<LanguageModelToolUse>>,
tool_uses_by_user_message: HashMap<MessageId, Vec<LanguageModelToolUseId>>,
tool_results: HashMap<LanguageModelToolUseId, LanguageModelToolResult>,
@@ -37,8 +43,9 @@ pub struct ToolUseState {
}
impl ToolUseState {
pub fn new() -> Self {
pub fn new(tools: Arc<ToolWorkingSet>) -> Self {
Self {
tools,
tool_uses_by_assistant_message: HashMap::default(),
tool_uses_by_user_message: HashMap::default(),
tool_results: HashMap::default(),
@@ -50,10 +57,11 @@ impl ToolUseState {
///
/// Accepts a function to filter the tools that should be used to populate the state.
pub fn from_serialized_messages(
tools: Arc<ToolWorkingSet>,
messages: &[SerializedMessage],
mut filter_by_tool_name: impl FnMut(&str) -> bool,
) -> Self {
let mut this = Self::new();
let mut this = Self::new(tools);
let mut tool_names_by_id = HashMap::default();
for message in messages {
@@ -138,7 +146,7 @@ impl ToolUseState {
self.pending_tool_uses_by_id.values().collect()
}
pub fn tool_uses_for_message(&self, id: MessageId) -> Vec<ToolUse> {
pub fn tool_uses_for_message(&self, id: MessageId, cx: &App) -> Vec<ToolUse> {
let Some(tool_uses_for_message) = &self.tool_uses_by_assistant_message.get(&id) else {
return Vec::new();
};
@@ -158,29 +166,53 @@ impl ToolUseState {
}
if let Some(pending_tool_use) = self.pending_tool_uses_by_id.get(&tool_use.id) {
return match pending_tool_use.status {
match pending_tool_use.status {
PendingToolUseStatus::Idle => ToolUseStatus::Pending,
PendingToolUseStatus::NeedsConfirmation { .. } => {
ToolUseStatus::NeedsConfirmation
}
PendingToolUseStatus::Running { .. } => ToolUseStatus::Running,
PendingToolUseStatus::Error(ref err) => {
ToolUseStatus::Error(err.clone().into())
}
};
}
} else {
ToolUseStatus::Pending
}
ToolUseStatus::Pending
})();
let icon = if let Some(tool) = self.tools.tool(&tool_use.name, cx) {
tool.icon()
} else {
IconName::Cog
};
tool_uses.push(ToolUse {
id: tool_use.id.clone(),
name: tool_use.name.clone().into(),
ui_text: self.tool_ui_label(&tool_use.name, &tool_use.input, cx),
input: tool_use.input.clone(),
status,
icon,
})
}
tool_uses
}
pub fn tool_ui_label(
&self,
tool_name: &str,
input: &serde_json::Value,
cx: &App,
) -> SharedString {
if let Some(tool) = self.tools.tool(tool_name, cx) {
tool.ui_text(input).into()
} else {
"Unknown tool".into()
}
}
pub fn tool_results_for_message(&self, message_id: MessageId) -> Vec<&LanguageModelToolResult> {
let empty = Vec::new();
@@ -209,6 +241,7 @@ impl ToolUseState {
&mut self,
assistant_message_id: MessageId,
tool_use: LanguageModelToolUse,
cx: &App,
) {
self.tool_uses_by_assistant_message
.entry(assistant_message_id)
@@ -228,21 +261,52 @@ impl ToolUseState {
PendingToolUse {
assistant_message_id,
id: tool_use.id,
name: tool_use.name,
name: tool_use.name.clone(),
ui_text: self
.tool_ui_label(&tool_use.name, &tool_use.input, cx)
.into(),
input: tool_use.input,
status: PendingToolUseStatus::Idle,
},
);
}
pub fn run_pending_tool(&mut self, tool_use_id: LanguageModelToolUseId, task: Task<()>) {
pub fn run_pending_tool(
&mut self,
tool_use_id: LanguageModelToolUseId,
ui_text: SharedString,
task: Task<()>,
) {
if let Some(tool_use) = self.pending_tool_uses_by_id.get_mut(&tool_use_id) {
tool_use.ui_text = ui_text.into();
tool_use.status = PendingToolUseStatus::Running {
_task: task.shared(),
};
}
}
pub fn confirm_tool_use(
&mut self,
tool_use_id: LanguageModelToolUseId,
ui_text: impl Into<Arc<str>>,
input: serde_json::Value,
messages: Arc<Vec<LanguageModelRequestMessage>>,
tool: Arc<dyn Tool>,
) {
if let Some(tool_use) = self.pending_tool_uses_by_id.get_mut(&tool_use_id) {
let ui_text = ui_text.into();
tool_use.ui_text = ui_text.clone();
let confirmation = Confirmation {
tool_use_id,
input,
messages,
tool,
ui_text,
};
tool_use.status = PendingToolUseStatus::NeedsConfirmation(Arc::new(confirmation));
}
}
pub fn insert_tool_output(
&mut self,
tool_use_id: LanguageModelToolUseId,
@@ -335,13 +399,24 @@ pub struct PendingToolUse {
#[allow(unused)]
pub assistant_message_id: MessageId,
pub name: Arc<str>,
pub ui_text: Arc<str>,
pub input: serde_json::Value,
pub status: PendingToolUseStatus,
}
#[derive(Debug, Clone)]
pub struct Confirmation {
pub tool_use_id: LanguageModelToolUseId,
pub input: serde_json::Value,
pub ui_text: Arc<str>,
pub messages: Arc<Vec<LanguageModelRequestMessage>>,
pub tool: Arc<dyn Tool>,
}
#[derive(Debug, Clone)]
pub enum PendingToolUseStatus {
Idle,
NeedsConfirmation(Arc<Confirmation>),
Running { _task: Shared<Task<()>> },
Error(#[allow(unused)] Arc<str>),
}
@@ -354,4 +429,8 @@ impl PendingToolUseStatus {
pub fn is_error(&self) -> bool {
matches!(self, PendingToolUseStatus::Error(_))
}
pub fn needs_confirmation(&self) -> bool {
matches!(self, PendingToolUseStatus::NeedsConfirmation { .. })
}
}

View File

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

View File

@@ -0,0 +1,119 @@
use gpui::{
point, App, Context, EventEmitter, IntoElement, PlatformDisplay, Size, Window,
WindowBackgroundAppearance, WindowBounds, WindowDecorations, WindowKind, WindowOptions,
};
use release_channel::ReleaseChannel;
use std::rc::Rc;
use theme;
use ui::{prelude::*, Render};
pub struct ToolReadyPopUp {
caption: SharedString,
icon: IconName,
icon_color: Color,
}
impl ToolReadyPopUp {
pub fn new(caption: impl Into<SharedString>, icon: IconName, icon_color: Color) -> Self {
Self {
caption: caption.into(),
icon,
icon_color,
}
}
pub fn window_options(screen: Rc<dyn PlatformDisplay>, cx: &App) -> WindowOptions {
let size = Size {
width: px(440.),
height: px(72.),
};
let notification_margin_width = px(16.);
let notification_margin_height = px(-48.);
let bounds = gpui::Bounds::<Pixels> {
origin: screen.bounds().top_right()
- point(
size.width + notification_margin_width,
notification_margin_height,
),
size,
};
let app_id = ReleaseChannel::global(cx).app_id();
WindowOptions {
window_bounds: Some(WindowBounds::Windowed(bounds)),
titlebar: None,
focus: false,
show: true,
kind: WindowKind::PopUp,
is_movable: false,
display_id: Some(screen.id()),
window_background: WindowBackgroundAppearance::Transparent,
app_id: Some(app_id.to_owned()),
window_min_size: None,
window_decorations: Some(WindowDecorations::Client),
}
}
}
pub enum ToolReadyPopupEvent {
Accepted,
Dismissed,
}
impl EventEmitter<ToolReadyPopupEvent> for ToolReadyPopUp {}
impl Render for ToolReadyPopUp {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let ui_font = theme::setup_ui_font(window, cx);
let line_height = window.line_height();
h_flex()
.size_full()
.p_3()
.gap_4()
.justify_between()
.elevation_3(cx)
.text_ui(cx)
.font(ui_font)
.border_color(cx.theme().colors().border)
.rounded_xl()
.child(
h_flex()
.items_start()
.gap_2()
.child(
h_flex().h(line_height).justify_center().child(
Icon::new(self.icon)
.color(self.icon_color)
.size(IconSize::Small),
),
)
.child(
v_flex()
.child(Headline::new("Agent Panel").size(HeadlineSize::XSmall))
.child(Label::new(self.caption.clone()).color(Color::Muted)),
),
)
.child(
h_flex()
.gap_0p5()
.child(
Button::new("open", "View Panel")
.style(ButtonStyle::Tinted(ui::TintColor::Accent))
.on_click({
cx.listener(move |_this, _event, _, cx| {
cx.emit(ToolReadyPopupEvent::Accepted);
})
}),
)
.child(Button::new("dismiss", "Dismiss").on_click({
cx.listener(move |_, _event, _, cx| {
cx.emit(ToolReadyPopupEvent::Dismissed);
})
})),
)
}
}

View File

@@ -162,6 +162,11 @@ pub enum ContextOperation {
section: SlashCommandOutputSection<language::Anchor>,
version: clock::Global,
},
ThoughtProcessOutputSectionAdded {
timestamp: clock::Lamport,
section: ThoughtProcessOutputSection<language::Anchor>,
version: clock::Global,
},
BufferOperation(language::Operation),
}
@@ -259,6 +264,20 @@ impl ContextOperation {
version: language::proto::deserialize_version(&message.version),
})
}
proto::context_operation::Variant::ThoughtProcessOutputSectionAdded(message) => {
let section = message.section.context("missing section")?;
Ok(Self::ThoughtProcessOutputSectionAdded {
timestamp: language::proto::deserialize_timestamp(
message.timestamp.context("missing timestamp")?,
),
section: ThoughtProcessOutputSection {
range: language::proto::deserialize_anchor_range(
section.range.context("invalid range")?,
)?,
},
version: language::proto::deserialize_version(&message.version),
})
}
proto::context_operation::Variant::BufferOperation(op) => Ok(Self::BufferOperation(
language::proto::deserialize_operation(
op.operation.context("invalid buffer operation")?,
@@ -370,6 +389,27 @@ impl ContextOperation {
},
)),
},
Self::ThoughtProcessOutputSectionAdded {
timestamp,
section,
version,
} => proto::ContextOperation {
variant: Some(
proto::context_operation::Variant::ThoughtProcessOutputSectionAdded(
proto::context_operation::ThoughtProcessOutputSectionAdded {
timestamp: Some(language::proto::serialize_timestamp(*timestamp)),
section: Some({
proto::ThoughtProcessOutputSection {
range: Some(language::proto::serialize_anchor_range(
section.range.clone(),
)),
}
}),
version: language::proto::serialize_version(version),
},
),
),
},
Self::BufferOperation(operation) => proto::ContextOperation {
variant: Some(proto::context_operation::Variant::BufferOperation(
proto::context_operation::BufferOperation {
@@ -387,7 +427,8 @@ impl ContextOperation {
Self::UpdateSummary { summary, .. } => summary.timestamp,
Self::SlashCommandStarted { id, .. } => id.0,
Self::SlashCommandOutputSectionAdded { timestamp, .. }
| Self::SlashCommandFinished { timestamp, .. } => *timestamp,
| Self::SlashCommandFinished { timestamp, .. }
| Self::ThoughtProcessOutputSectionAdded { timestamp, .. } => *timestamp,
Self::BufferOperation(_) => {
panic!("reading the timestamp of a buffer operation is not supported")
}
@@ -402,7 +443,8 @@ impl ContextOperation {
| Self::UpdateSummary { version, .. }
| Self::SlashCommandStarted { version, .. }
| Self::SlashCommandOutputSectionAdded { version, .. }
| Self::SlashCommandFinished { version, .. } => version,
| Self::SlashCommandFinished { version, .. }
| Self::ThoughtProcessOutputSectionAdded { version, .. } => version,
Self::BufferOperation(_) => {
panic!("reading the version of a buffer operation is not supported")
}
@@ -418,6 +460,8 @@ pub enum ContextEvent {
MessagesEdited,
SummaryChanged,
StreamedCompletion,
StartedThoughtProcess(Range<language::Anchor>),
EndedThoughtProcess(language::Anchor),
PatchesUpdated {
removed: Vec<Range<language::Anchor>>,
updated: Vec<Range<language::Anchor>>,
@@ -498,6 +542,17 @@ impl MessageMetadata {
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct ThoughtProcessOutputSection<T> {
pub range: Range<T>,
}
impl ThoughtProcessOutputSection<language::Anchor> {
pub fn is_valid(&self, buffer: &language::TextBuffer) -> bool {
self.range.start.is_valid(buffer) && !self.range.to_offset(buffer).is_empty()
}
}
#[derive(Clone, Debug)]
pub struct Message {
pub offset_range: Range<usize>,
@@ -580,6 +635,7 @@ pub struct AssistantContext {
edits_since_last_parse: language::Subscription,
slash_commands: Arc<SlashCommandWorkingSet>,
slash_command_output_sections: Vec<SlashCommandOutputSection<language::Anchor>>,
thought_process_output_sections: Vec<ThoughtProcessOutputSection<language::Anchor>>,
message_anchors: Vec<MessageAnchor>,
contents: Vec<Content>,
messages_metadata: HashMap<MessageId, MessageMetadata>,
@@ -682,6 +738,7 @@ impl AssistantContext {
parsed_slash_commands: Vec::new(),
invoked_slash_commands: HashMap::default(),
slash_command_output_sections: Vec::new(),
thought_process_output_sections: Vec::new(),
edits_since_last_parse: edits_since_last_slash_command_parse,
summary: None,
pending_summary: Task::ready(None),
@@ -764,6 +821,18 @@ impl AssistantContext {
}
})
.collect(),
thought_process_output_sections: self
.thought_process_output_sections
.iter()
.filter_map(|section| {
if section.is_valid(buffer) {
let range = section.range.to_offset(buffer);
Some(ThoughtProcessOutputSection { range })
} else {
None
}
})
.collect(),
}
}
@@ -957,6 +1026,16 @@ impl AssistantContext {
cx.emit(ContextEvent::SlashCommandOutputSectionAdded { section });
}
}
ContextOperation::ThoughtProcessOutputSectionAdded { section, .. } => {
let buffer = self.buffer.read(cx);
if let Err(ix) = self
.thought_process_output_sections
.binary_search_by(|probe| probe.range.cmp(&section.range, buffer))
{
self.thought_process_output_sections
.insert(ix, section.clone());
}
}
ContextOperation::SlashCommandFinished {
id,
error_message,
@@ -1020,6 +1099,9 @@ impl AssistantContext {
ContextOperation::SlashCommandOutputSectionAdded { section, .. } => {
self.has_received_operations_for_anchor_range(section.range.clone(), cx)
}
ContextOperation::ThoughtProcessOutputSectionAdded { section, .. } => {
self.has_received_operations_for_anchor_range(section.range.clone(), cx)
}
ContextOperation::SlashCommandFinished { .. } => true,
ContextOperation::BufferOperation(_) => {
panic!("buffer operations should always be applied")
@@ -1128,6 +1210,12 @@ impl AssistantContext {
&self.slash_command_output_sections
}
pub fn thought_process_output_sections(
&self,
) -> &[ThoughtProcessOutputSection<language::Anchor>] {
&self.thought_process_output_sections
}
pub fn contains_files(&self, cx: &App) -> bool {
let buffer = self.buffer.read(cx);
self.slash_command_output_sections.iter().any(|section| {
@@ -2168,6 +2256,35 @@ impl AssistantContext {
);
}
fn insert_thought_process_output_section(
&mut self,
section: ThoughtProcessOutputSection<language::Anchor>,
cx: &mut Context<Self>,
) {
let buffer = self.buffer.read(cx);
let insertion_ix = match self
.thought_process_output_sections
.binary_search_by(|probe| probe.range.cmp(&section.range, buffer))
{
Ok(ix) | Err(ix) => ix,
};
self.thought_process_output_sections
.insert(insertion_ix, section.clone());
// cx.emit(ContextEvent::ThoughtProcessOutputSectionAdded {
// section: section.clone(),
// });
let version = self.version.clone();
let timestamp = self.next_timestamp();
self.push_op(
ContextOperation::ThoughtProcessOutputSectionAdded {
timestamp,
section,
version,
},
cx,
);
}
pub fn completion_provider_changed(&mut self, cx: &mut Context<Self>) {
self.count_remaining_tokens(cx);
}
@@ -2220,6 +2337,10 @@ impl AssistantContext {
let request_start = Instant::now();
let mut events = stream.await?;
let mut stop_reason = StopReason::EndTurn;
let mut thought_process_stack = Vec::new();
const THOUGHT_PROCESS_START_MARKER: &str = "<think>\n";
const THOUGHT_PROCESS_END_MARKER: &str = "\n</think>";
while let Some(event) = events.next().await {
if response_latency.is_none() {
@@ -2227,6 +2348,9 @@ impl AssistantContext {
}
let event = event?;
let mut context_event = None;
let mut thought_process_output_section = None;
this.update(cx, |this, cx| {
let message_ix = this
.message_anchors
@@ -2245,7 +2369,50 @@ impl AssistantContext {
LanguageModelCompletionEvent::Stop(reason) => {
stop_reason = reason;
}
LanguageModelCompletionEvent::Text(chunk) => {
LanguageModelCompletionEvent::Thinking(chunk) => {
if thought_process_stack.is_empty() {
let start =
buffer.anchor_before(message_old_end_offset);
thought_process_stack.push(start);
let chunk =
format!("{THOUGHT_PROCESS_START_MARKER}{chunk}{THOUGHT_PROCESS_END_MARKER}");
let chunk_len = chunk.len();
buffer.edit(
[(
message_old_end_offset..message_old_end_offset,
chunk,
)],
None,
cx,
);
let end = buffer
.anchor_before(message_old_end_offset + chunk_len);
context_event = Some(
ContextEvent::StartedThoughtProcess(start..end),
);
} else {
// This ensures that all the thinking chunks are inserted inside the thinking tag
let insertion_position =
message_old_end_offset - THOUGHT_PROCESS_END_MARKER.len();
buffer.edit(
[(insertion_position..insertion_position, chunk)],
None,
cx,
);
}
}
LanguageModelCompletionEvent::Text(mut chunk) => {
if let Some(start) = thought_process_stack.pop() {
let end = buffer.anchor_before(message_old_end_offset);
context_event =
Some(ContextEvent::EndedThoughtProcess(end));
thought_process_output_section =
Some(ThoughtProcessOutputSection {
range: start..end,
});
chunk.insert_str(0, "\n\n");
}
buffer.edit(
[(
message_old_end_offset..message_old_end_offset,
@@ -2260,6 +2427,13 @@ impl AssistantContext {
}
});
if let Some(section) = thought_process_output_section.take() {
this.insert_thought_process_output_section(section, cx);
}
if let Some(context_event) = context_event.take() {
cx.emit(context_event);
}
cx.emit(ContextEvent::StreamedCompletion);
Some(())
@@ -3127,6 +3301,8 @@ pub struct SavedContext {
pub summary: String,
pub slash_command_output_sections:
Vec<assistant_slash_command::SlashCommandOutputSection<usize>>,
#[serde(default)]
pub thought_process_output_sections: Vec<ThoughtProcessOutputSection<usize>>,
}
impl SavedContext {
@@ -3228,6 +3404,20 @@ impl SavedContext {
version.observe(timestamp);
}
for section in self.thought_process_output_sections {
let timestamp = next_timestamp.tick();
operations.push(ContextOperation::ThoughtProcessOutputSectionAdded {
timestamp,
section: ThoughtProcessOutputSection {
range: buffer.anchor_after(section.range.start)
..buffer.anchor_before(section.range.end),
},
version: version.clone(),
});
version.observe(timestamp);
}
let timestamp = next_timestamp.tick();
operations.push(ContextOperation::UpdateSummary {
summary: ContextSummary {
@@ -3302,6 +3492,7 @@ impl SavedContextV0_3_0 {
.collect(),
summary: self.summary,
slash_command_output_sections: self.slash_command_output_sections,
thought_process_output_sections: Vec::new(),
}
}
}

View File

@@ -13,7 +13,7 @@ use editor::{
BlockContext, BlockId, BlockPlacement, BlockProperties, BlockStyle, Crease, CreaseMetadata,
CustomBlockId, FoldId, RenderBlock, ToDisplayPoint,
},
scroll::{Autoscroll, AutoscrollStrategy},
scroll::Autoscroll,
Anchor, Editor, EditorEvent, MenuInlineCompletionsPolicy, ProposedChangeLocation,
ProposedChangesEditor, RowExt, ToOffset as _, ToPoint,
};
@@ -64,7 +64,10 @@ use workspace::{
Workspace,
};
use crate::{slash_command::SlashCommandCompletionProvider, slash_command_picker};
use crate::{
slash_command::SlashCommandCompletionProvider, slash_command_picker,
ThoughtProcessOutputSection,
};
use crate::{
AssistantContext, AssistantPatch, AssistantPatchStatus, CacheStatus, Content, ContextEvent,
ContextId, InvokedSlashCommandId, InvokedSlashCommandStatus, Message, MessageId,
@@ -120,6 +123,11 @@ enum AssistError {
Message(SharedString),
}
pub enum ThoughtProcessStatus {
Pending,
Completed,
}
pub trait AssistantPanelDelegate {
fn active_context_editor(
&self,
@@ -178,6 +186,7 @@ pub struct ContextEditor {
project: Entity<Project>,
lsp_adapter_delegate: Option<Arc<dyn LspAdapterDelegate>>,
editor: Entity<Editor>,
pending_thought_process: Option<(CreaseId, language::Anchor)>,
blocks: HashMap<MessageId, (MessageHeader, CustomBlockId)>,
image_blocks: HashSet<CustomBlockId>,
scroll_position: Option<ScrollPosition>,
@@ -253,7 +262,8 @@ impl ContextEditor {
cx.observe_global_in::<SettingsStore>(window, Self::settings_changed),
];
let sections = context.read(cx).slash_command_output_sections().to_vec();
let slash_command_sections = context.read(cx).slash_command_output_sections().to_vec();
let thought_process_sections = context.read(cx).thought_process_output_sections().to_vec();
let patch_ranges = context.read(cx).patch_ranges().collect::<Vec<_>>();
let slash_commands = context.read(cx).slash_commands().clone();
let mut this = Self {
@@ -265,6 +275,7 @@ impl ContextEditor {
image_blocks: Default::default(),
scroll_position: None,
remote_id: None,
pending_thought_process: None,
fs: fs.clone(),
workspace,
project,
@@ -294,7 +305,14 @@ impl ContextEditor {
};
this.update_message_headers(cx);
this.update_image_blocks(cx);
this.insert_slash_command_output_sections(sections, false, window, cx);
this.insert_slash_command_output_sections(slash_command_sections, false, window, cx);
this.insert_thought_process_output_sections(
thought_process_sections
.into_iter()
.map(|section| (section, ThoughtProcessStatus::Completed)),
window,
cx,
);
this.patches_updated(&Vec::new(), &patch_ranges, window, cx);
this
}
@@ -396,12 +414,9 @@ impl ContextEditor {
cursor..cursor
};
self.editor.update(cx, |editor, cx| {
editor.change_selections(
Some(Autoscroll::Strategy(AutoscrollStrategy::Fit)),
window,
cx,
|selections| selections.select_ranges([new_selection]),
);
editor.change_selections(Some(Autoscroll::fit()), window, cx, |selections| {
selections.select_ranges([new_selection])
});
});
// Avoid scrolling to the new cursor position so the assistant's output is stable.
cx.defer_in(window, |this, _, _| this.scroll_position = None);
@@ -599,6 +614,47 @@ impl ContextEditor {
context.save(Some(Duration::from_millis(500)), self.fs.clone(), cx);
});
}
ContextEvent::StartedThoughtProcess(range) => {
let creases = self.insert_thought_process_output_sections(
[(
ThoughtProcessOutputSection {
range: range.clone(),
},
ThoughtProcessStatus::Pending,
)],
window,
cx,
);
self.pending_thought_process = Some((creases[0], range.start));
}
ContextEvent::EndedThoughtProcess(end) => {
if let Some((crease_id, start)) = self.pending_thought_process.take() {
self.editor.update(cx, |editor, cx| {
let multi_buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
let (excerpt_id, _, _) = multi_buffer_snapshot.as_singleton().unwrap();
let start_anchor = multi_buffer_snapshot
.anchor_in_excerpt(*excerpt_id, start)
.unwrap();
editor.display_map.update(cx, |display_map, cx| {
display_map.unfold_intersecting(
vec![start_anchor..start_anchor],
true,
cx,
);
});
editor.remove_creases(vec![crease_id], cx);
});
self.insert_thought_process_output_sections(
[(
ThoughtProcessOutputSection { range: start..*end },
ThoughtProcessStatus::Completed,
)],
window,
cx,
);
}
}
ContextEvent::StreamedCompletion => {
self.editor.update(cx, |editor, cx| {
if let Some(scroll_position) = self.scroll_position {
@@ -946,6 +1002,62 @@ impl ContextEditor {
self.update_active_patch(window, cx);
}
fn insert_thought_process_output_sections(
&mut self,
sections: impl IntoIterator<
Item = (
ThoughtProcessOutputSection<language::Anchor>,
ThoughtProcessStatus,
),
>,
window: &mut Window,
cx: &mut Context<Self>,
) -> Vec<CreaseId> {
self.editor.update(cx, |editor, cx| {
let buffer = editor.buffer().read(cx).snapshot(cx);
let excerpt_id = *buffer.as_singleton().unwrap().0;
let mut buffer_rows_to_fold = BTreeSet::new();
let mut creases = Vec::new();
for (section, status) in sections {
let start = buffer
.anchor_in_excerpt(excerpt_id, section.range.start)
.unwrap();
let end = buffer
.anchor_in_excerpt(excerpt_id, section.range.end)
.unwrap();
let buffer_row = MultiBufferRow(start.to_point(&buffer).row);
buffer_rows_to_fold.insert(buffer_row);
creases.push(
Crease::inline(
start..end,
FoldPlaceholder {
render: render_thought_process_fold_icon_button(
cx.entity().downgrade(),
status,
),
merge_adjacent: false,
..Default::default()
},
render_slash_command_output_toggle,
|_, _, _, _| Empty.into_any_element(),
)
.with_metadata(CreaseMetadata {
icon: IconName::Ai,
label: "Thinking Process".into(),
}),
);
}
let creases = editor.insert_creases(creases, cx);
for buffer_row in buffer_rows_to_fold.into_iter().rev() {
editor.fold_at(&FoldAt { buffer_row }, window, cx);
}
creases
})
}
fn insert_slash_command_output_sections(
&mut self,
sections: impl IntoIterator<Item = SlashCommandOutputSection<language::Anchor>>,
@@ -2652,6 +2764,52 @@ fn find_surrounding_code_block(snapshot: &BufferSnapshot, offset: usize) -> Opti
None
}
fn render_thought_process_fold_icon_button(
editor: WeakEntity<Editor>,
status: ThoughtProcessStatus,
) -> Arc<dyn Send + Sync + Fn(FoldId, Range<Anchor>, &mut App) -> AnyElement> {
Arc::new(move |fold_id, fold_range, _cx| {
let editor = editor.clone();
let button = ButtonLike::new(fold_id).layer(ElevationIndex::ElevatedSurface);
let button = match status {
ThoughtProcessStatus::Pending => button
.child(
Icon::new(IconName::Brain)
.size(IconSize::Small)
.color(Color::Muted),
)
.child(
Label::new("Thinking…").color(Color::Muted).with_animation(
"pulsating-label",
Animation::new(Duration::from_secs(2))
.repeat()
.with_easing(pulsating_between(0.4, 0.8)),
|label, delta| label.alpha(delta),
),
),
ThoughtProcessStatus::Completed => button
.style(ButtonStyle::Filled)
.child(Icon::new(IconName::Brain).size(IconSize::Small))
.child(Label::new("Thought Process").single_line()),
};
button
.on_click(move |_, window, cx| {
editor
.update(cx, |editor, cx| {
let buffer_start = fold_range
.start
.to_point(&editor.buffer().read(cx).read(cx));
let buffer_row = MultiBufferRow(buffer_start.row);
editor.unfold_at(&UnfoldAt { buffer_row }, window, cx);
})
.ok();
})
.into_any_element()
})
}
fn render_fold_icon_button(
editor: WeakEntity<Editor>,
icon: IconName,

View File

@@ -2,7 +2,7 @@ use crate::context_editor::ContextEditor;
use anyhow::Result;
pub use assistant_slash_command::SlashCommand;
use assistant_slash_command::{AfterCompletion, SlashCommandLine, SlashCommandWorkingSet};
use editor::{CompletionProvider, Editor};
use editor::{CompletionProvider, Editor, ExcerptId};
use fuzzy::{match_strings, StringMatchCandidate};
use gpui::{App, AppContext as _, Context, Entity, Task, WeakEntity, Window};
use language::{Anchor, Buffer, ToPoint};
@@ -126,6 +126,7 @@ impl SlashCommandCompletionProvider {
)),
new_text,
label: command.label(cx),
icon_path: None,
confirm,
source: CompletionSource::Custom,
})
@@ -223,6 +224,7 @@ impl SlashCommandCompletionProvider {
last_argument_range.clone()
},
label: new_argument.label,
icon_path: None,
new_text,
documentation: None,
confirm,
@@ -241,6 +243,7 @@ impl SlashCommandCompletionProvider {
impl CompletionProvider for SlashCommandCompletionProvider {
fn completions(
&self,
_excerpt_id: ExcerptId,
buffer: &Entity<Buffer>,
buffer_position: Anchor,
_: editor::CompletionContext,

View File

@@ -79,10 +79,25 @@ impl Eval {
let start_time = std::time::SystemTime::now();
let (system_prompt_context, load_error) = cx
.update(|cx| {
assistant
.read(cx)
.thread
.read(cx)
.load_system_prompt_context(cx)
})?
.await;
if let Some(load_error) = load_error {
return Err(anyhow!("{:?}", load_error));
};
assistant.update(cx, |assistant, cx| {
assistant.thread.update(cx, |thread, cx| {
let context = vec![];
thread.insert_user_message(self.user_prompt.clone(), context, None, cx);
thread.set_system_prompt_context(system_prompt_context);
thread.send_to_model(model, RequestKind::Chat, cx);
});
})?;
@@ -105,7 +120,7 @@ impl Eval {
.count();
Ok(EvalOutput {
diff,
last_message: last_message.text.clone(),
last_message: last_message.to_string(),
elapsed_time,
assistant_response_count,
tool_use_counts: assistant.tool_use_counts.clone(),

View File

@@ -89,7 +89,7 @@ impl HeadlessAssistant {
ThreadEvent::DoneStreaming => {
let thread = thread.read(cx);
if let Some(message) = thread.messages().last() {
println!("Message: {}", message.text,);
println!("Message: {}", message.to_string());
}
if thread.all_tools_finished() {
self.done_tx.send_blocking(Ok(())).unwrap()
@@ -128,12 +128,7 @@ impl HeadlessAssistant {
}
}
}
ThreadEvent::StreamedCompletion
| ThreadEvent::SummaryChanged
| ThreadEvent::StreamedAssistantText(_, _)
| ThreadEvent::MessageAdded(_)
| ThreadEvent::MessageEdited(_)
| ThreadEvent::MessageDeleted(_) => {}
_ => {}
}
}
}
@@ -154,7 +149,10 @@ pub fn init(cx: &mut App) -> Arc<HeadlessAppState> {
cx.set_http_client(client.http_client().clone());
let git_binary_path = None;
let fs = Arc::new(RealFs::new(git_binary_path));
let fs = Arc::new(RealFs::new(
git_binary_path,
cx.background_executor().clone(),
));
let languages = Arc::new(LanguageRegistry::new(cx.background_executor().clone()));

View File

@@ -16,6 +16,7 @@ anthropic = { workspace = true, features = ["schemars"] }
anyhow.workspace = true
feature_flags.workspace = true
gpui.workspace = true
indexmap.workspace = true
language_model.workspace = true
lmstudio = { workspace = true, features = ["schemars"] }
log.workspace = true

View File

@@ -0,0 +1,18 @@
use std::sync::Arc;
use gpui::SharedString;
use indexmap::IndexMap;
/// A profile for the Zed Agent that controls its behavior.
#[derive(Debug, Clone)]
pub struct AgentProfile {
/// The name of the profile.
pub name: SharedString,
pub tools: IndexMap<Arc<str>, bool>,
pub context_servers: IndexMap<Arc<str>, ContextServerPreset>,
}
#[derive(Debug, Clone, Default)]
pub struct ContextServerPreset {
pub tools: IndexMap<Arc<str>, bool>,
}

View File

@@ -1,3 +1,5 @@
mod agent_profile;
use std::sync::Arc;
use ::open_ai::Model as OpenAiModel;
@@ -5,6 +7,7 @@ use anthropic::Model as AnthropicModel;
use deepseek::Model as DeepseekModel;
use feature_flags::FeatureFlagAppExt;
use gpui::{App, Pixels};
use indexmap::IndexMap;
use language_model::{CloudModel, LanguageModel};
use lmstudio::Model as LmStudioModel;
use ollama::Model as OllamaModel;
@@ -12,6 +15,8 @@ use schemars::{schema::Schema, JsonSchema};
use serde::{Deserialize, Serialize};
use settings::{Settings, SettingsSources};
pub use crate::agent_profile::*;
#[derive(Copy, Clone, Default, Debug, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum AssistantDockPosition {
@@ -66,6 +71,10 @@ pub struct AssistantSettings {
pub inline_alternatives: Vec<LanguageModelSelection>,
pub using_outdated_settings_version: bool,
pub enable_experimental_live_diffs: bool,
pub default_profile: Arc<str>,
pub profiles: IndexMap<Arc<str>, AgentProfile>,
pub always_allow_tool_actions: bool,
pub notify_when_agent_waiting: bool,
}
impl AssistantSettings {
@@ -166,6 +175,10 @@ impl AssistantSettingsContent {
editor_model: None,
inline_alternatives: None,
enable_experimental_live_diffs: None,
default_profile: None,
profiles: None,
always_allow_tool_actions: None,
notify_when_agent_waiting: None,
},
VersionedAssistantSettingsContent::V2(settings) => settings.clone(),
},
@@ -187,6 +200,10 @@ impl AssistantSettingsContent {
editor_model: None,
inline_alternatives: None,
enable_experimental_live_diffs: None,
default_profile: None,
profiles: None,
always_allow_tool_actions: None,
notify_when_agent_waiting: None,
},
}
}
@@ -293,6 +310,18 @@ impl AssistantSettingsContent {
}
}
}
pub fn set_profile(&mut self, profile_id: Arc<str>) {
match self {
AssistantSettingsContent::Versioned(settings) => match settings {
VersionedAssistantSettingsContent::V2(settings) => {
settings.default_profile = Some(profile_id);
}
VersionedAssistantSettingsContent::V1(_) => {}
},
AssistantSettingsContent::Legacy(_) => {}
}
}
}
#[derive(Clone, Serialize, Deserialize, JsonSchema, Debug)]
@@ -316,6 +345,10 @@ impl Default for VersionedAssistantSettingsContent {
editor_model: None,
inline_alternatives: None,
enable_experimental_live_diffs: None,
default_profile: None,
profiles: None,
always_allow_tool_actions: None,
notify_when_agent_waiting: None,
})
}
}
@@ -352,6 +385,19 @@ pub struct AssistantSettingsContentV2 {
///
/// Default: false
enable_experimental_live_diffs: Option<bool>,
#[schemars(skip)]
default_profile: Option<Arc<str>>,
#[schemars(skip)]
pub profiles: Option<IndexMap<Arc<str>, AgentProfileContent>>,
/// Whenever a tool action would normally wait for your confirmation
/// that you allow it, always choose to allow it.
///
/// Default: false
always_allow_tool_actions: Option<bool>,
/// Whether to show a popup notification when the agent is waiting for user input.
///
/// Default: true
notify_when_agent_waiting: Option<bool>,
}
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
@@ -388,6 +434,19 @@ impl Default for LanguageModelSelection {
}
}
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, JsonSchema)]
pub struct AgentProfileContent {
pub name: Arc<str>,
pub tools: IndexMap<Arc<str>, bool>,
#[serde(default)]
pub context_servers: IndexMap<Arc<str>, ContextServerPresetContent>,
}
#[derive(Debug, PartialEq, Clone, Default, Serialize, Deserialize, JsonSchema)]
pub struct ContextServerPresetContent {
pub tools: IndexMap<Arc<str>, bool>,
}
#[derive(Clone, Serialize, Deserialize, JsonSchema, Debug)]
pub struct AssistantSettingsContentV1 {
/// Whether the Assistant is enabled.
@@ -482,6 +541,41 @@ impl Settings for AssistantSettings {
&mut settings.enable_experimental_live_diffs,
value.enable_experimental_live_diffs,
);
merge(
&mut settings.always_allow_tool_actions,
value.always_allow_tool_actions,
);
merge(
&mut settings.notify_when_agent_waiting,
value.notify_when_agent_waiting,
);
merge(&mut settings.default_profile, value.default_profile);
if let Some(profiles) = value.profiles {
settings
.profiles
.extend(profiles.into_iter().map(|(id, profile)| {
(
id,
AgentProfile {
name: profile.name.into(),
tools: profile.tools,
context_servers: profile
.context_servers
.into_iter()
.map(|(context_server_id, preset)| {
(
context_server_id,
ContextServerPreset {
tools: preset.tools.clone(),
},
)
})
.collect(),
},
)
}));
}
}
Ok(settings)
@@ -546,6 +640,10 @@ mod tests {
default_width: None,
default_height: None,
enable_experimental_live_diffs: None,
default_profile: None,
profiles: None,
always_allow_tool_actions: None,
notify_when_agent_waiting: None,
}),
)
},

View File

@@ -13,10 +13,11 @@ path = "src/assistant_tool.rs"
[dependencies]
anyhow.workspace = true
collections.workspace = true
clock.workspace = true
collections.workspace = true
derive_more.workspace = true
gpui.workspace = true
icons.workspace = true
language.workspace = true
language_model.workspace = true
parking_lot.workspace = true

View File

@@ -1,12 +1,13 @@
mod tool_registry;
mod tool_working_set;
use std::fmt::{self, Debug, Formatter};
use std::sync::Arc;
use anyhow::Result;
use collections::{HashMap, HashSet};
use gpui::Context;
use gpui::{App, Entity, SharedString, Task};
use gpui::{App, Context, Entity, SharedString, Task};
use icons::IconName;
use language::Buffer;
use language_model::LanguageModelRequestMessage;
use project::Project;
@@ -34,16 +35,26 @@ pub trait Tool: 'static + Send + Sync {
/// Returns the description of the tool.
fn description(&self) -> String;
/// Returns the icon for the tool.
fn icon(&self) -> IconName;
/// Returns the source of the tool.
fn source(&self) -> ToolSource {
ToolSource::Native
}
/// Returns true iff the tool needs the users's confirmation
/// before having permission to run.
fn needs_confirmation(&self) -> bool;
/// Returns the JSON schema that describes the tool's input.
fn input_schema(&self) -> serde_json::Value {
serde_json::Value::Object(serde_json::Map::default())
}
/// Returns markdown to be displayed in the UI for this tool.
fn ui_text(&self, input: &serde_json::Value) -> String;
/// Runs the tool with the provided input.
fn run(
self: Arc<Self>,
@@ -55,6 +66,12 @@ pub trait Tool: 'static + Send + Sync {
) -> Task<Result<String>>;
}
impl Debug for dyn Tool {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
f.debug_struct("Tool").field("name", &self.name()).finish()
}
}
/// Tracks actions performed by tools in a thread
#[derive(Debug)]
pub struct ActionLog {
@@ -63,6 +80,8 @@ pub struct ActionLog {
stale_buffers_in_context: HashSet<Entity<Buffer>>,
/// Buffers that we want to notify the model about when they change.
tracked_buffers: HashMap<Entity<Buffer>, TrackedBuffer>,
/// Has the model edited a file since it last checked diagnostics?
edited_since_project_diagnostics_check: bool,
}
#[derive(Debug, Default)]
@@ -76,6 +95,7 @@ impl ActionLog {
Self {
stale_buffers_in_context: HashSet::default(),
tracked_buffers: HashMap::default(),
edited_since_project_diagnostics_check: false,
}
}
@@ -93,6 +113,12 @@ impl ActionLog {
}
self.stale_buffers_in_context.extend(buffers);
self.edited_since_project_diagnostics_check = true;
}
/// Notifies a diagnostics check
pub fn checked_project_diagnostics(&mut self) {
self.edited_since_project_diagnostics_check = false;
}
/// Iterate over buffers changed since last read or edited by the model
@@ -103,6 +129,11 @@ impl ActionLog {
.map(|(buffer, _)| buffer)
}
/// Returns true if any files have been edited since the last project diagnostics check
pub fn has_edited_files_since_project_diagnostics_check(&self) -> bool {
self.edited_since_project_diagnostics_check
}
/// Takes and returns the set of buffers pending refresh, clearing internal state.
pub fn take_stale_buffers_in_context(&mut self) -> HashSet<Entity<Buffer>> {
std::mem::take(&mut self.stale_buffers_in_context)

View File

@@ -15,26 +15,14 @@ pub struct ToolWorkingSet {
state: Mutex<WorkingSetState>,
}
#[derive(Default)]
struct WorkingSetState {
context_server_tools_by_id: HashMap<ToolId, Arc<dyn Tool>>,
context_server_tools_by_name: HashMap<String, Arc<dyn Tool>>,
disabled_tools_by_source: HashMap<ToolSource, HashSet<Arc<str>>>,
is_scripting_tool_disabled: bool,
enabled_tools_by_source: HashMap<ToolSource, HashSet<Arc<str>>>,
next_tool_id: ToolId,
}
impl Default for WorkingSetState {
fn default() -> Self {
Self {
context_server_tools_by_id: HashMap::default(),
context_server_tools_by_name: HashMap::default(),
disabled_tools_by_source: HashMap::default(),
is_scripting_tool_disabled: true,
next_tool_id: ToolId::default(),
}
}
}
impl ToolWorkingSet {
pub fn tool(&self, name: &str, cx: &App) -> Option<Arc<dyn Tool>> {
self.state
@@ -53,39 +41,23 @@ impl ToolWorkingSet {
self.state.lock().tools_by_source(cx)
}
pub fn are_all_tools_enabled(&self) -> bool {
let state = self.state.lock();
state.disabled_tools_by_source.is_empty() && !state.is_scripting_tool_disabled
}
pub fn are_all_tools_from_source_enabled(&self, source: &ToolSource) -> bool {
let state = self.state.lock();
!state.disabled_tools_by_source.contains_key(source)
}
pub fn enabled_tools(&self, cx: &App) -> Vec<Arc<dyn Tool>> {
self.state.lock().enabled_tools(cx)
}
pub fn enable_all_tools(&self) {
pub fn disable_all_tools(&self) {
let mut state = self.state.lock();
state.disabled_tools_by_source.clear();
state.enable_scripting_tool();
state.disable_all_tools();
}
pub fn disable_all_tools(&self, cx: &App) {
pub fn enable_source(&self, source: ToolSource, cx: &App) {
let mut state = self.state.lock();
state.disable_all_tools(cx);
state.enable_source(source, cx);
}
pub fn enable_source(&self, source: &ToolSource) {
pub fn disable_source(&self, source: &ToolSource) {
let mut state = self.state.lock();
state.enable_source(source);
}
pub fn disable_source(&self, source: ToolSource, cx: &App) {
let mut state = self.state.lock();
state.disable_source(source, cx);
state.disable_source(source);
}
pub fn insert(&self, tool: Arc<dyn Tool>) -> ToolId {
@@ -124,21 +96,6 @@ impl ToolWorkingSet {
.retain(|id, _| !tool_ids_to_remove.contains(id));
state.tools_changed();
}
pub fn is_scripting_tool_enabled(&self) -> bool {
let state = self.state.lock();
!state.is_scripting_tool_disabled
}
pub fn enable_scripting_tool(&self) {
let mut state = self.state.lock();
state.enable_scripting_tool();
}
pub fn disable_scripting_tool(&self) {
let mut state = self.state.lock();
state.disable_scripting_tool();
}
}
impl WorkingSetState {
@@ -187,40 +144,36 @@ impl WorkingSetState {
}
fn is_enabled(&self, source: &ToolSource, name: &Arc<str>) -> bool {
!self.is_disabled(source, name)
self.enabled_tools_by_source
.get(source)
.map_or(false, |enabled_tools| enabled_tools.contains(name))
}
fn is_disabled(&self, source: &ToolSource, name: &Arc<str>) -> bool {
self.disabled_tools_by_source
.get(source)
.map_or(false, |disabled_tools| disabled_tools.contains(name))
!self.is_enabled(source, name)
}
fn enable(&mut self, source: ToolSource, tools_to_enable: &[Arc<str>]) {
self.disabled_tools_by_source
self.enabled_tools_by_source
.entry(source)
.or_default()
.retain(|name| !tools_to_enable.contains(name));
.extend(tools_to_enable.into_iter().cloned());
}
fn disable(&mut self, source: ToolSource, tools_to_disable: &[Arc<str>]) {
self.disabled_tools_by_source
self.enabled_tools_by_source
.entry(source)
.or_default()
.extend(tools_to_disable.into_iter().cloned());
.retain(|name| !tools_to_disable.contains(name));
}
fn enable_source(&mut self, source: &ToolSource) {
self.disabled_tools_by_source.remove(source);
}
fn disable_source(&mut self, source: ToolSource, cx: &App) {
fn enable_source(&mut self, source: ToolSource, cx: &App) {
let tools_by_source = self.tools_by_source(cx);
let Some(tools) = tools_by_source.get(&source) else {
return;
};
self.disabled_tools_by_source.insert(
self.enabled_tools_by_source.insert(
source,
tools
.into_iter()
@@ -229,26 +182,11 @@ impl WorkingSetState {
);
}
fn disable_all_tools(&mut self, cx: &App) {
let tools = self.tools_by_source(cx);
for (source, tools) in tools {
let tool_names = tools
.into_iter()
.map(|tool| tool.name().into())
.collect::<Vec<_>>();
self.disable(source, &tool_names);
}
self.disable_scripting_tool();
fn disable_source(&mut self, source: &ToolSource) {
self.enabled_tools_by_source.remove(source);
}
fn enable_scripting_tool(&mut self) {
self.is_scripting_tool_disabled = false;
}
fn disable_scripting_tool(&mut self) {
self.is_scripting_tool_disabled = true;
fn disable_all_tools(&mut self) {
self.enabled_tools_by_source.clear();
}
}

View File

@@ -1,26 +1,37 @@
mod bash_tool;
mod copy_path_tool;
mod create_directory_tool;
mod create_file_tool;
mod delete_path_tool;
mod diagnostics_tool;
mod edit_files_tool;
mod fetch_tool;
mod find_replace_file_tool;
mod list_directory_tool;
mod move_path_tool;
mod now_tool;
mod path_search_tool;
mod read_file_tool;
mod regex_search_tool;
mod replace;
mod thinking_tool;
use std::sync::Arc;
use assistant_tool::ToolRegistry;
use copy_path_tool::CopyPathTool;
use gpui::App;
use http_client::HttpClientWithUrl;
use move_path_tool::MovePathTool;
use crate::bash_tool::BashTool;
use crate::create_directory_tool::CreateDirectoryTool;
use crate::create_file_tool::CreateFileTool;
use crate::delete_path_tool::DeletePathTool;
use crate::diagnostics_tool::DiagnosticsTool;
use crate::edit_files_tool::EditFilesTool;
use crate::fetch_tool::FetchTool;
use crate::find_replace_file_tool::FindReplaceFileTool;
use crate::list_directory_tool::ListDirectoryTool;
use crate::now_tool::NowTool;
use crate::path_search_tool::PathSearchTool;
@@ -34,7 +45,12 @@ pub fn init(http_client: Arc<HttpClientWithUrl>, cx: &mut App) {
let registry = ToolRegistry::global(cx);
registry.register_tool(BashTool);
registry.register_tool(CreateDirectoryTool);
registry.register_tool(CreateFileTool);
registry.register_tool(CopyPathTool);
registry.register_tool(DeletePathTool);
registry.register_tool(FindReplaceFileTool);
registry.register_tool(MovePathTool);
registry.register_tool(DiagnosticsTool);
registry.register_tool(EditFilesTool);
registry.register_tool(ListDirectoryTool);

View File

@@ -6,7 +6,9 @@ use project::Project;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::sync::Arc;
use ui::IconName;
use util::command::new_smol_command;
use util::markdown::MarkdownString;
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct BashToolInput {
@@ -23,15 +25,36 @@ impl Tool for BashTool {
"bash".to_string()
}
fn needs_confirmation(&self) -> bool {
true
}
fn description(&self) -> String {
include_str!("./bash_tool/description.md").to_string()
}
fn icon(&self) -> IconName {
IconName::Terminal
}
fn input_schema(&self) -> serde_json::Value {
let schema = schemars::schema_for!(BashToolInput);
serde_json::to_value(&schema).unwrap()
}
fn ui_text(&self, input: &serde_json::Value) -> String {
match serde_json::from_value::<BashToolInput>(input.clone()) {
Ok(input) => {
if input.command.contains('\n') {
MarkdownString::code_block("bash", &input.command).0
} else {
MarkdownString::inline_code(&input.command).0
}
}
Err(_) => "Run bash command".to_string(),
}
}
fn run(
self: Arc<Self>,
input: serde_json::Value,

View File

@@ -0,0 +1,120 @@
use anyhow::{anyhow, Result};
use assistant_tool::{ActionLog, Tool};
use gpui::{App, AppContext, Entity, Task};
use language_model::LanguageModelRequestMessage;
use project::Project;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::sync::Arc;
use ui::IconName;
use util::markdown::MarkdownString;
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct CopyPathToolInput {
/// The source path of the file or directory to copy.
/// If a directory is specified, its contents will be copied recursively (like `cp -r`).
///
/// <example>
/// If the project has the following files:
///
/// - directory1/a/something.txt
/// - directory2/a/things.txt
/// - directory3/a/other.txt
///
/// You can copy the first file by providing a source_path of "directory1/a/something.txt"
/// </example>
pub source_path: String,
/// The destination path where the file or directory should be copied to.
///
/// <example>
/// To copy "directory1/a/something.txt" to "directory2/b/copy.txt",
/// provide a destination_path of "directory2/b/copy.txt"
/// </example>
pub destination_path: String,
}
pub struct CopyPathTool;
impl Tool for CopyPathTool {
fn name(&self) -> String {
"copy-path".into()
}
fn needs_confirmation(&self) -> bool {
true
}
fn description(&self) -> String {
include_str!("./copy_path_tool/description.md").into()
}
fn icon(&self) -> IconName {
IconName::Clipboard
}
fn input_schema(&self) -> serde_json::Value {
let schema = schemars::schema_for!(CopyPathToolInput);
serde_json::to_value(&schema).unwrap()
}
fn ui_text(&self, input: &serde_json::Value) -> String {
match serde_json::from_value::<CopyPathToolInput>(input.clone()) {
Ok(input) => {
let src = MarkdownString::inline_code(&input.source_path);
let dest = MarkdownString::inline_code(&input.destination_path);
format!("Copy {src} to {dest}")
}
Err(_) => "Copy path".to_string(),
}
}
fn run(
self: Arc<Self>,
input: serde_json::Value,
_messages: &[LanguageModelRequestMessage],
project: Entity<Project>,
_action_log: Entity<ActionLog>,
cx: &mut App,
) -> Task<Result<String>> {
let input = match serde_json::from_value::<CopyPathToolInput>(input) {
Ok(input) => input,
Err(err) => return Task::ready(Err(anyhow!(err))),
};
let copy_task = project.update(cx, |project, cx| {
match project
.find_project_path(&input.source_path, cx)
.and_then(|project_path| project.entry_for_path(&project_path, cx))
{
Some(entity) => match project.find_project_path(&input.destination_path, cx) {
Some(project_path) => {
project.copy_entry(entity.id, None, project_path.path, cx)
}
None => Task::ready(Err(anyhow!(
"Destination path {} was outside the project.",
input.destination_path
))),
},
None => Task::ready(Err(anyhow!(
"Source path {} was not found in the project.",
input.source_path
))),
}
});
cx.background_spawn(async move {
match copy_task.await {
Ok(_) => Ok(format!(
"Copied {} to {}",
input.source_path, input.destination_path
)),
Err(err) => Err(anyhow!(
"Failed to copy {} to {}: {}",
input.source_path,
input.destination_path,
err
)),
}
})
}
}

View File

@@ -0,0 +1,6 @@
Copies a file or directory in the project, and returns confirmation that the copy succeeded.
Directory contents will be copied recursively (like `cp -r`).
This tool should be used when it's desirable to create a copy of a file or directory without modifying the original.
It's much more efficient than doing this by separately reading and then writing the file or directory's contents,
so this tool should be preferred over that approach whenever copying is the goal.

View File

@@ -0,0 +1,92 @@
use anyhow::{anyhow, Result};
use assistant_tool::{ActionLog, Tool};
use gpui::{App, Entity, Task};
use language_model::LanguageModelRequestMessage;
use project::Project;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::sync::Arc;
use ui::IconName;
use util::markdown::MarkdownString;
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct CreateDirectoryToolInput {
/// The path of the new directory.
///
/// <example>
/// If the project has the following structure:
///
/// - directory1/
/// - directory2/
///
/// You can create a new directory by providing a path of "directory1/new_directory"
/// </example>
pub path: String,
}
pub struct CreateDirectoryTool;
impl Tool for CreateDirectoryTool {
fn name(&self) -> String {
"create-directory".into()
}
fn needs_confirmation(&self) -> bool {
true
}
fn description(&self) -> String {
include_str!("./create_directory_tool/description.md").into()
}
fn icon(&self) -> IconName {
IconName::Folder
}
fn input_schema(&self) -> serde_json::Value {
let schema = schemars::schema_for!(CreateDirectoryToolInput);
serde_json::to_value(&schema).unwrap()
}
fn ui_text(&self, input: &serde_json::Value) -> String {
match serde_json::from_value::<CreateDirectoryToolInput>(input.clone()) {
Ok(input) => {
format!(
"Create directory {}",
MarkdownString::inline_code(&input.path)
)
}
Err(_) => "Create directory".to_string(),
}
}
fn run(
self: Arc<Self>,
input: serde_json::Value,
_messages: &[LanguageModelRequestMessage],
project: Entity<Project>,
_action_log: Entity<ActionLog>,
cx: &mut App,
) -> Task<Result<String>> {
let input = match serde_json::from_value::<CreateDirectoryToolInput>(input) {
Ok(input) => input,
Err(err) => return Task::ready(Err(anyhow!(err))),
};
let project_path = match project.read(cx).find_project_path(&input.path, cx) {
Some(project_path) => project_path,
None => return Task::ready(Err(anyhow!("Path to create was outside the project"))),
};
let destination_path: Arc<str> = input.path.as_str().into();
cx.spawn(async move |cx| {
project
.update(cx, |project, cx| {
project.create_entry(project_path.clone(), true, cx)
})?
.await
.map_err(|err| anyhow!("Unable to create directory {destination_path}: {err}"))?;
Ok(format!("Created directory {destination_path}"))
})
}
}

View File

@@ -0,0 +1,3 @@
Creates a new directory at the specified path within the project. Returns confirmation that the directory was created.
This tool creates a directory and all necessary parent directories (similar to `mkdir -p`). It should be used whenever you need to create new directories within the project.

View File

@@ -0,0 +1,112 @@
use anyhow::{anyhow, Result};
use assistant_tool::{ActionLog, Tool};
use gpui::{App, Entity, Task};
use language_model::LanguageModelRequestMessage;
use project::Project;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::sync::Arc;
use ui::IconName;
use util::markdown::MarkdownString;
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct CreateFileToolInput {
/// The path where the file should be created.
///
/// <example>
/// If the project has the following structure:
///
/// - directory1/
/// - directory2/
///
/// You can create a new file by providing a path of "directory1/new_file.txt"
/// </example>
pub path: String,
/// The text contents of the file to create.
///
/// <example>
/// To create a file with the text "Hello, World!", provide contents of "Hello, World!"
/// </example>
pub contents: String,
}
pub struct CreateFileTool;
impl Tool for CreateFileTool {
fn name(&self) -> String {
"create-file".into()
}
fn needs_confirmation(&self) -> bool {
true
}
fn description(&self) -> String {
include_str!("./create_file_tool/description.md").into()
}
fn icon(&self) -> IconName {
IconName::FileCreate
}
fn input_schema(&self) -> serde_json::Value {
let schema = schemars::schema_for!(CreateFileToolInput);
serde_json::to_value(&schema).unwrap()
}
fn ui_text(&self, input: &serde_json::Value) -> String {
match serde_json::from_value::<CreateFileToolInput>(input.clone()) {
Ok(input) => {
let path = MarkdownString::inline_code(&input.path);
format!("Create file {path}")
}
Err(_) => "Create file".to_string(),
}
}
fn run(
self: Arc<Self>,
input: serde_json::Value,
_messages: &[LanguageModelRequestMessage],
project: Entity<Project>,
_action_log: Entity<ActionLog>,
cx: &mut App,
) -> Task<Result<String>> {
let input = match serde_json::from_value::<CreateFileToolInput>(input) {
Ok(input) => input,
Err(err) => return Task::ready(Err(anyhow!(err))),
};
let project_path = match project.read(cx).find_project_path(&input.path, cx) {
Some(project_path) => project_path,
None => return Task::ready(Err(anyhow!("Path to create was outside the project"))),
};
let contents: Arc<str> = input.contents.as_str().into();
let destination_path: Arc<str> = input.path.as_str().into();
cx.spawn(async move |cx| {
project
.update(cx, |project, cx| {
project.create_entry(project_path.clone(), false, cx)
})?
.await
.map_err(|err| anyhow!("Unable to create {destination_path}: {err}"))?;
let buffer = project
.update(cx, |project, cx| {
project.open_buffer(project_path.clone(), cx)
})?
.await
.map_err(|err| anyhow!("Unable to open buffer for {destination_path}: {err}"))?;
buffer.update(cx, |buffer, cx| {
buffer.set_text(contents, cx);
})?;
project
.update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))?
.await
.map_err(|err| anyhow!("Unable to save buffer for {destination_path}: {err}"))?;
Ok(format!("Created file {destination_path}"))
})
}
}

View File

@@ -0,0 +1,3 @@
Creates a new file at the specified path within the project, containing the given text content. Returns confirmation that the file was created.
This tool is the most efficient way to create new files within the project, so it should always be chosen whenever it's necessary to create a new file in the project with specific text content, or whenever a file in the project needs such a drastic change that you would prefer to replace the entire thing instead of making individual edits. This tool should not be used when making changes to parts of an existing file but not all of it. In those cases, it's better to use another approach to edit the file.

View File

@@ -6,6 +6,7 @@ use project::Project;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::sync::Arc;
use ui::IconName;
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct DeletePathToolInput {
@@ -30,15 +31,30 @@ impl Tool for DeletePathTool {
"delete-path".into()
}
fn needs_confirmation(&self) -> bool {
true
}
fn description(&self) -> String {
include_str!("./delete_path_tool/description.md").into()
}
fn icon(&self) -> IconName {
IconName::FileDelete
}
fn input_schema(&self) -> serde_json::Value {
let schema = schemars::schema_for!(DeletePathToolInput);
serde_json::to_value(&schema).unwrap()
}
fn ui_text(&self, input: &serde_json::Value) -> String {
match serde_json::from_value::<DeletePathToolInput>(input.clone()) {
Ok(input) => format!("Delete “`{}`”", input.path),
Err(_) => "Delete path".to_string(),
}
}
fn run(
self: Arc<Self>,
input: serde_json::Value,
@@ -59,13 +75,12 @@ impl Tool for DeletePathTool {
{
Some(deletion_task) => cx.background_spawn(async move {
match deletion_task.await {
Ok(()) => Ok(format!("Deleted {}", &path_str)),
Err(err) => Err(anyhow!("Failed to delete {}: {}", &path_str, err)),
Ok(()) => Ok(format!("Deleted {path_str}")),
Err(err) => Err(anyhow!("Failed to delete {path_str}: {err}")),
}
}),
None => Task::ready(Err(anyhow!(
"Couldn't delete {} because that path isn't in this project.",
path_str
"Couldn't delete {path_str} because that path isn't in this project."
))),
}
}

View File

@@ -6,11 +6,9 @@ use language_model::LanguageModelRequestMessage;
use project::Project;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::{
fmt::Write,
path::{Path, PathBuf},
sync::Arc,
};
use std::{fmt::Write, path::Path, sync::Arc};
use ui::IconName;
use util::markdown::MarkdownString;
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct DiagnosticsToolInput {
@@ -27,7 +25,17 @@ pub struct DiagnosticsToolInput {
///
/// If you wanna access diagnostics for `dolor.txt` in `ipsum`, you should use the path `ipsum/dolor.txt`.
/// </example>
pub path: Option<PathBuf>,
#[serde(deserialize_with = "deserialize_path")]
pub path: Option<String>,
}
fn deserialize_path<'de, D>(deserializer: D) -> Result<Option<String>, D::Error>
where
D: serde::Deserializer<'de>,
{
let opt = Option::<String>::deserialize(deserializer)?;
// The model passes an empty string sometimes
Ok(opt.filter(|s| !s.is_empty()))
}
pub struct DiagnosticsTool;
@@ -37,91 +45,120 @@ impl Tool for DiagnosticsTool {
"diagnostics".into()
}
fn needs_confirmation(&self) -> bool {
false
}
fn description(&self) -> String {
include_str!("./diagnostics_tool/description.md").into()
}
fn icon(&self) -> IconName {
IconName::Warning
}
fn input_schema(&self) -> serde_json::Value {
let schema = schemars::schema_for!(DiagnosticsToolInput);
serde_json::to_value(&schema).unwrap()
}
fn ui_text(&self, input: &serde_json::Value) -> String {
if let Some(path) = serde_json::from_value::<DiagnosticsToolInput>(input.clone())
.ok()
.and_then(|input| match input.path {
Some(path) if !path.is_empty() => Some(MarkdownString::inline_code(&path)),
_ => None,
})
{
format!("Check diagnostics for {path}")
} else {
"Check project diagnostics".to_string()
}
}
fn run(
self: Arc<Self>,
input: serde_json::Value,
_messages: &[LanguageModelRequestMessage],
project: Entity<Project>,
_action_log: Entity<ActionLog>,
action_log: Entity<ActionLog>,
cx: &mut App,
) -> Task<Result<String>> {
let input = match serde_json::from_value::<DiagnosticsToolInput>(input) {
Ok(input) => input,
Err(err) => return Task::ready(Err(anyhow!(err))),
};
match serde_json::from_value::<DiagnosticsToolInput>(input)
.ok()
.and_then(|input| input.path)
{
Some(path) if !path.is_empty() => {
let Some(project_path) = project.read(cx).find_project_path(&path, cx) else {
return Task::ready(Err(anyhow!("Could not find path {path} in project",)));
};
if let Some(path) = input.path {
let Some(project_path) = project.read(cx).find_project_path(&path, cx) else {
return Task::ready(Err(anyhow!("Could not find path in project")));
};
let buffer = project.update(cx, |project, cx| project.open_buffer(project_path, cx));
let buffer =
project.update(cx, |project, cx| project.open_buffer(project_path, cx));
cx.spawn(async move |cx| {
let mut output = String::new();
let buffer = buffer.await?;
let snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot())?;
cx.spawn(async move |cx| {
let mut output = String::new();
let buffer = buffer.await?;
let snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot())?;
for (_, group) in snapshot.diagnostic_groups(None) {
let entry = &group.entries[group.primary_ix];
let range = entry.range.to_point(&snapshot);
let severity = match entry.diagnostic.severity {
DiagnosticSeverity::ERROR => "error",
DiagnosticSeverity::WARNING => "warning",
_ => continue,
};
for (_, group) in snapshot.diagnostic_groups(None) {
let entry = &group.entries[group.primary_ix];
let range = entry.range.to_point(&snapshot);
let severity = match entry.diagnostic.severity {
DiagnosticSeverity::ERROR => "error",
DiagnosticSeverity::WARNING => "warning",
_ => continue,
};
writeln!(
output,
"{} at line {}: {}",
severity,
range.start.row + 1,
entry.diagnostic.message
)?;
}
writeln!(
output,
"{} at line {}: {}",
severity,
range.start.row + 1,
entry.diagnostic.message
)?;
}
if output.is_empty() {
Ok("File doesn't have errors or warnings!".to_string())
} else {
Ok(output)
}
})
} else {
let project = project.read(cx);
let mut output = String::new();
let mut has_diagnostics = false;
for (project_path, _, summary) in project.diagnostic_summaries(true, cx) {
if summary.error_count > 0 || summary.warning_count > 0 {
let Some(worktree) = project.worktree_for_id(project_path.worktree_id, cx)
else {
continue;
};
has_diagnostics = true;
output.push_str(&format!(
"{}: {} error(s), {} warning(s)\n",
Path::new(worktree.read(cx).root_name())
.join(project_path.path)
.display(),
summary.error_count,
summary.warning_count
));
}
if output.is_empty() {
Ok("File doesn't have errors or warnings!".to_string())
} else {
Ok(output)
}
})
}
_ => {
let project = project.read(cx);
let mut output = String::new();
let mut has_diagnostics = false;
if has_diagnostics {
Task::ready(Ok(output))
} else {
Task::ready(Ok("No errors or warnings found in the project.".to_string()))
for (project_path, _, summary) in project.diagnostic_summaries(true, cx) {
if summary.error_count > 0 || summary.warning_count > 0 {
let Some(worktree) = project.worktree_for_id(project_path.worktree_id, cx)
else {
continue;
};
has_diagnostics = true;
output.push_str(&format!(
"{}: {} error(s), {} warning(s)\n",
Path::new(worktree.read(cx).root_name())
.join(project_path.path)
.display(),
summary.error_count,
summary.warning_count
));
}
}
action_log.update(cx, |action_log, _cx| {
action_log.checked_project_diagnostics();
});
if has_diagnostics {
Task::ready(Ok(output))
} else {
Task::ready(Ok("No errors or warnings found in the project.".to_string()))
}
}
}
}

View File

@@ -14,3 +14,5 @@ To get diagnostics for a specific file:
To get a project-wide diagnostic summary:
{}
</example>
IMPORTANT: When you're done making changes, you **MUST** get the **project** diagnostics (input: `{}`) at the end of your edits so you can fix any problems you might have introduced. **DO NOT** tell the user you're done before doing this!

View File

@@ -1,33 +1,30 @@
mod edit_action;
pub mod log;
mod replace;
use crate::replace::{replace_exact, replace_with_flexible_indent};
use anyhow::{anyhow, Context, Result};
use assistant_tool::{ActionLog, Tool};
use collections::HashSet;
use edit_action::{EditAction, EditActionParser};
use futures::StreamExt;
use gpui::{App, AsyncApp, Entity, Task};
use futures::{channel::mpsc, SinkExt, StreamExt};
use gpui::{App, AppContext, AsyncApp, Entity, Task};
use language_model::{
LanguageModelRegistry, LanguageModelRequest, LanguageModelRequestMessage, MessageContent, Role,
};
use log::{EditToolLog, EditToolRequestId};
use project::Project;
use replace::{replace_exact, replace_with_flexible_indent};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::fmt::Write;
use std::sync::Arc;
use ui::IconName;
use util::ResultExt;
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct EditFilesToolInput {
/// High-level edit instructions. These will be interpreted by a smaller
/// model, so explain the changes you want that model to make and which
/// file paths need changing.
///
/// The description should be concise and clear. We will show this
/// description to the user as well.
/// file paths need changing. The description should be concise and clear.
///
/// WARNING: When specifying which file paths need changing, you MUST
/// start each path with one of the project's root directories.
@@ -58,6 +55,21 @@ pub struct EditFilesToolInput {
/// Notice how we never specify code snippets in the instructions!
/// </example>
pub edit_instructions: String,
/// A user-friendly description of what changes are being made.
/// This will be shown to the user in the UI to describe the edit operation. The screen real estate for this UI will be extremely
/// constrained, so make the description extremely terse.
///
/// <example>
/// For fixing a broken authentication system:
/// "Fix auth bug in login flow"
/// </example>
///
/// <example>
/// For adding unit tests to a module:
/// "Add tests for user profile logic"
/// </example>
pub display_description: String,
}
pub struct EditFilesTool;
@@ -67,15 +79,30 @@ impl Tool for EditFilesTool {
"edit-files".into()
}
fn needs_confirmation(&self) -> bool {
true
}
fn description(&self) -> String {
include_str!("./edit_files_tool/description.md").into()
}
fn icon(&self) -> IconName {
IconName::Pencil
}
fn input_schema(&self) -> serde_json::Value {
let schema = schemars::schema_for!(EditFilesToolInput);
serde_json::to_value(&schema).unwrap()
}
fn ui_text(&self, input: &serde_json::Value) -> String {
match serde_json::from_value::<EditFilesToolInput>(input.clone()) {
Ok(input) => input.display_description,
Err(_) => "Edit files".to_string(),
}
}
fn run(
self: Arc<Self>,
input: serde_json::Value,
@@ -126,24 +153,45 @@ impl Tool for EditFilesTool {
struct EditToolRequest {
parser: EditActionParser,
output: String,
changed_buffers: HashSet<Entity<language::Buffer>>,
bad_searches: Vec<BadSearch>,
editor_response: EditorResponse,
project: Entity<Project>,
action_log: Entity<ActionLog>,
tool_log: Option<(Entity<EditToolLog>, EditToolRequestId)>,
}
#[derive(Debug)]
enum DiffResult {
BadSearch(BadSearch),
Diff(language::Diff),
enum EditorResponse {
/// The editor model hasn't produced any actions yet.
/// If we don't have any by the end, we'll return its message to the architect model.
Message(String),
/// The editor model produced at least one action.
Actions {
applied: Vec<AppliedAction>,
search_errors: Vec<SearchError>,
},
}
struct AppliedAction {
source: String,
buffer: Entity<language::Buffer>,
}
#[derive(Debug)]
struct BadSearch {
file_path: String,
search: String,
enum DiffResult {
Diff(language::Diff),
SearchError(SearchError),
}
#[derive(Debug)]
enum SearchError {
NoMatch {
file_path: String,
search: String,
},
EmptyBuffer {
file_path: String,
search: String,
exists: bool,
},
}
impl EditToolRequest {
@@ -195,24 +243,36 @@ impl EditToolRequest {
temperature: Some(0.0),
};
let (mut tx, mut rx) = mpsc::channel::<String>(32);
let stream = model.stream_completion_text(llm_request, &cx);
let mut chunks = stream.await?;
let reader_task = cx.background_spawn(async move {
let mut chunks = stream.await?;
while let Some(chunk) = chunks.stream.next().await {
if let Some(chunk) = chunk.log_err() {
// we don't process here because the API fails
// if we take too long between reads
tx.send(chunk).await?
}
}
tx.close().await?;
anyhow::Ok(())
});
let mut request = Self {
parser: EditActionParser::new(),
// we start with the success header so we don't need to shift the output in the common case
output: Self::SUCCESS_OUTPUT_HEADER.to_string(),
changed_buffers: HashSet::default(),
bad_searches: Vec::new(),
editor_response: EditorResponse::Message(String::with_capacity(256)),
action_log,
project,
tool_log,
};
while let Some(chunk) = chunks.stream.next().await {
request.process_response_chunk(&chunk?, cx).await?;
while let Some(chunk) = rx.next().await {
request.process_response_chunk(&chunk, cx).await?;
}
reader_task.await?;
request.finalize(cx).await
})
}
@@ -220,6 +280,12 @@ impl EditToolRequest {
async fn process_response_chunk(&mut self, chunk: &str, cx: &mut AsyncApp) -> Result<()> {
let new_actions = self.parser.parse_chunk(chunk);
if let EditorResponse::Message(ref mut message) = self.editor_response {
if new_actions.is_empty() {
message.push_str(chunk);
}
}
if let Some((ref log, req_id)) = self.tool_log {
log.update(cx, |log, cx| {
log.push_editor_response_chunk(req_id, chunk, &new_actions, cx)
@@ -270,18 +336,45 @@ impl EditToolRequest {
}?;
match result {
DiffResult::BadSearch(invalid_replace) => {
self.bad_searches.push(invalid_replace);
DiffResult::SearchError(error) => {
self.push_search_error(error);
}
DiffResult::Diff(diff) => {
let _clock = buffer.update(cx, |buffer, cx| buffer.apply_diff(diff, cx))?;
write!(&mut self.output, "\n\n{}", source)?;
self.changed_buffers.insert(buffer);
self.push_applied_action(AppliedAction { source, buffer });
}
}
Ok(())
anyhow::Ok(())
}
fn push_search_error(&mut self, error: SearchError) {
match &mut self.editor_response {
EditorResponse::Message(_) => {
self.editor_response = EditorResponse::Actions {
applied: Vec::new(),
search_errors: vec![error],
};
}
EditorResponse::Actions { search_errors, .. } => {
search_errors.push(error);
}
}
}
fn push_applied_action(&mut self, action: AppliedAction) {
match &mut self.editor_response {
EditorResponse::Message(_) => {
self.editor_response = EditorResponse::Actions {
applied: vec![action],
search_errors: Vec::new(),
};
}
EditorResponse::Actions { applied, .. } => {
applied.push(action);
}
}
}
async fn replace_diff(
@@ -290,119 +383,171 @@ impl EditToolRequest {
file_path: std::path::PathBuf,
snapshot: language::BufferSnapshot,
) -> Result<DiffResult> {
let result =
if snapshot.is_empty() {
let exists = snapshot
.file()
.map_or(false, |file| file.disk_state().exists());
let error = SearchError::EmptyBuffer {
file_path: file_path.display().to_string(),
exists,
search: old,
};
return Ok(DiffResult::SearchError(error));
}
let replace_result =
// Try to match exactly
replace_exact(&old, &new, &snapshot)
.await
// If that fails, try being flexible about indentation
.or_else(|| replace_with_flexible_indent(&old, &new, &snapshot));
let Some(diff) = result else {
return anyhow::Ok(DiffResult::BadSearch(BadSearch {
let Some(diff) = replace_result else {
let error = SearchError::NoMatch {
search: old,
file_path: file_path.display().to_string(),
}));
};
return Ok(DiffResult::SearchError(error));
};
anyhow::Ok(DiffResult::Diff(diff))
Ok(DiffResult::Diff(diff))
}
const SUCCESS_OUTPUT_HEADER: &str = "Successfully applied. Here's a list of changes:";
const ERROR_OUTPUT_HEADER_NO_EDITS: &str = "I couldn't apply any edits!";
const ERROR_OUTPUT_HEADER_WITH_EDITS: &str =
"Errors occurred. First, here's a list of the edits we managed to apply:";
async fn finalize(self, cx: &mut AsyncApp) -> Result<String> {
let changed_buffer_count = self.changed_buffers.len();
match self.editor_response {
EditorResponse::Message(message) => Err(anyhow!(
"No edits were applied! You might need to provide more context.\n\n{}",
message
)),
EditorResponse::Actions {
applied,
search_errors,
} => {
let mut output = String::with_capacity(1024);
// Save each buffer once at the end
for buffer in &self.changed_buffers {
self.project
.update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))?
.await?;
}
let parse_errors = self.parser.errors();
let has_errors = !search_errors.is_empty() || !parse_errors.is_empty();
self.action_log
.update(cx, |log, cx| log.buffer_edited(self.changed_buffers, cx))
.log_err();
if has_errors {
let error_count = search_errors.len() + parse_errors.len();
let errors = self.parser.errors();
if applied.is_empty() {
writeln!(
&mut output,
"{} errors occurred! No edits were applied.",
error_count,
)?;
} else {
writeln!(
&mut output,
"{} errors occurred, but {} edits were correctly applied.",
error_count,
applied.len(),
)?;
if errors.is_empty() && self.bad_searches.is_empty() {
if changed_buffer_count == 0 {
return Err(anyhow!(
"The instructions didn't lead to any changes. You might need to consult the file contents first."
));
}
Ok(self.output)
} else {
let mut output = self.output;
if output.is_empty() {
output.replace_range(
0..Self::SUCCESS_OUTPUT_HEADER.len(),
Self::ERROR_OUTPUT_HEADER_NO_EDITS,
);
} else {
output.replace_range(
0..Self::SUCCESS_OUTPUT_HEADER.len(),
Self::ERROR_OUTPUT_HEADER_WITH_EDITS,
);
}
if !self.bad_searches.is_empty() {
writeln!(
&mut output,
"\n\n# {} SEARCH/REPLACE block(s) failed to match:\n",
self.bad_searches.len()
)?;
for replace in self.bad_searches {
writeln!(
writeln!(
&mut output,
"# {} SEARCH/REPLACE block(s) applied:\n\nDo not re-send these since they are already applied!\n",
applied.len()
)?;
}
} else {
write!(
&mut output,
"## No exact match in: {}\n```\n{}\n```\n",
replace.file_path, replace.search,
"Successfully applied! Here's a list of applied edits:"
)?;
}
write!(&mut output,
"The SEARCH section must exactly match an existing block of lines including all white \
space, comments, indentation, docstrings, etc."
)?;
}
let mut changed_buffers = HashSet::default();
if !errors.is_empty() {
writeln!(
&mut output,
"\n\n# {} SEARCH/REPLACE blocks failed to parse:",
errors.len()
)?;
for error in errors {
writeln!(&mut output, "- {}", error)?;
for action in applied {
changed_buffers.insert(action.buffer);
write!(&mut output, "\n\n{}", action.source)?;
}
}
if changed_buffer_count > 0 {
writeln!(
&mut output,
"\n\nThe other SEARCH/REPLACE blocks were applied successfully. Do not re-send them!",
)?;
}
for buffer in &changed_buffers {
self.project
.update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))?
.await?;
}
writeln!(
&mut output,
"{}You can fix errors by running the tool again. You can include instructions, \
but errors are part of the conversation so you don't need to repeat them.",
if changed_buffer_count == 0 {
"\n\n"
self.action_log
.update(cx, |log, cx| log.buffer_edited(changed_buffers.clone(), cx))
.log_err();
if !search_errors.is_empty() {
writeln!(
&mut output,
"\n\n## {} SEARCH/REPLACE block(s) failed to match:\n",
search_errors.len()
)?;
for error in search_errors {
match error {
SearchError::NoMatch { file_path, search } => {
writeln!(
&mut output,
"### No exact match in: `{}`\n```\n{}\n```\n",
file_path, search,
)?;
}
SearchError::EmptyBuffer {
file_path,
exists: true,
search,
} => {
writeln!(
&mut output,
"### No match because `{}` is empty:\n```\n{}\n```\n",
file_path, search,
)?;
}
SearchError::EmptyBuffer {
file_path,
exists: false,
search,
} => {
writeln!(
&mut output,
"### No match because `{}` does not exist:\n```\n{}\n```\n",
file_path, search,
)?;
}
}
}
write!(&mut output,
"The SEARCH section must exactly match an existing block of lines including all white \
space, comments, indentation, docstrings, etc."
)?;
}
if !parse_errors.is_empty() {
writeln!(
&mut output,
"\n\n## {} SEARCH/REPLACE blocks failed to parse:",
parse_errors.len()
)?;
for error in parse_errors {
writeln!(&mut output, "- {}", error)?;
}
}
if has_errors {
writeln!(&mut output,
"\n\nYou can fix errors by running the tool again. You can include instructions, \
but errors are part of the conversation so you don't need to repeat them.",
)?;
Err(anyhow!(output))
} else {
""
Ok(output)
}
)?;
Err(anyhow!(output))
}
}
}
}

View File

@@ -3,3 +3,7 @@ Edit files in the current project by specifying instructions in natural language
When using this tool, you should suggest one coherent edit that can be made to the codebase.
When the set of edits you want to make is large or complex, feel free to invoke this tool multiple times, each time focusing on a specific change you wanna make.
You should use this tool when you want to edit a subset of a file's contents, but not the entire file. You should not use this tool when you want to replace the entire contents of a file with completely different contents, and you absolutely must never use this tool to create new files from scratch. If you ever consider using this tool to create a new file from scratch, for any reason, instead you must reconsider and choose a different approach.
DO NOT call this tool until the code to be edited appears in the conversation! You must use the `read-files` tool or ask the user to add it to context first.

View File

@@ -120,7 +120,7 @@ Break large *SEARCH/REPLACE* blocks into a series of smaller blocks that each ch
Include just the changing lines, and a few surrounding lines if needed for uniqueness.
Do not include long runs of unchanging lines in *SEARCH/REPLACE* blocks.
Only create *SEARCH/REPLACE* blocks for files that the user has added to the chat!
Only create *SEARCH/REPLACE* blocks for files that have been read! Even though the conversation includes `read-file` tool results, you *CANNOT* issue your own reads. If the conversation doesn't include the code you need to edit, ask for it to be read explicitly.
To move code within a file, use 2 *SEARCH/REPLACE* blocks: 1 to delete it from its current location, 1 to insert it in the new location.

View File

@@ -12,6 +12,8 @@ use language_model::LanguageModelRequestMessage;
use project::Project;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use ui::IconName;
use util::markdown::MarkdownString;
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]
enum ContentType {
@@ -113,15 +115,30 @@ impl Tool for FetchTool {
"fetch".to_string()
}
fn needs_confirmation(&self) -> bool {
true
}
fn description(&self) -> String {
include_str!("./fetch_tool/description.md").to_string()
}
fn icon(&self) -> IconName {
IconName::Globe
}
fn input_schema(&self) -> serde_json::Value {
let schema = schemars::schema_for!(FetchToolInput);
serde_json::to_value(&schema).unwrap()
}
fn ui_text(&self, input: &serde_json::Value) -> String {
match serde_json::from_value::<FetchToolInput>(input.clone()) {
Ok(input) => format!("Fetch {}", MarkdownString::escape(&input.url)),
Err(_) => "Fetch URL".to_string(),
}
}
fn run(
self: Arc<Self>,
input: serde_json::Value,

View File

@@ -0,0 +1,229 @@
use anyhow::{anyhow, Context as _, Result};
use assistant_tool::{ActionLog, Tool};
use gpui::{App, AppContext, Entity, Task};
use language_model::LanguageModelRequestMessage;
use project::Project;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::{collections::HashSet, path::PathBuf, sync::Arc};
use ui::IconName;
use crate::replace::replace_exact;
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct FindReplaceFileToolInput {
/// The path of the file to modify.
///
/// WARNING: When specifying which file path need changing, you MUST
/// start each path with one of the project's root directories.
///
/// The following examples assume we have two root directories in the project:
/// - backend
/// - frontend
///
/// <example>
/// `backend/src/main.rs`
///
/// Notice how the file path starts with root-1. Without that, the path
/// would be ambiguous and the call would fail!
/// </example>
///
/// <example>
/// `frontend/db.js`
/// </example>
pub path: PathBuf,
/// A user-friendly markdown description of what's being replaced. This will be shown in the UI.
///
/// <example>Fix API endpoint URLs</example>
/// <example>Update copyright year in `page_footer`</example>
pub display_description: String,
/// The unique string to find in the file. This string cannot be empty;
/// if the string is empty, the tool call will fail. Remember, do not use this tool
/// to create new files from scratch, or to overwrite existing files! Use a different
/// approach if you want to do that.
///
/// If this string appears more than once in the file, this tool call will fail,
/// so it is absolutely critical that you verify ahead of time that the string
/// is unique. You can search within the file to verify this.
///
/// To make the string more likely to be unique, include a minimum of 3 lines of context
/// before the string you actually want to find, as well as a minimum of 3 lines of
/// context after the string you want to find. (These lines of context should appear
/// in the `replace` string as well.) If 3 lines of context is not enough to obtain
/// a string that appears only once in the file, then double the number of context lines
/// until the string becomes unique. (Start with 3 lines before and 3 lines after
/// though, because too much context is needlessly costly.)
///
/// Do not alter the context lines of code in any way, and make sure to preserve all
/// whitespace and indentation for all lines of code. This string must be exactly as
/// it appears in the file, because this tool will do a literal find/replace, and if
/// even one character in this string is different in any way from how it appears
/// in the file, then the tool call will fail.
///
/// <example>
/// If a file contains this code:
///
/// ```rust
/// fn check_user_permissions(user_id: &str) -> Result<bool> {
/// // Check if user exists first
/// let user = database.find_user(user_id)?;
///
/// // This is the part we want to modify
/// if user.role == "admin" {
/// return Ok(true);
/// }
///
/// // Check other permissions
/// check_custom_permissions(user_id)
/// }
/// ```
///
/// Your find string should include at least 3 lines of context before and after the part
/// you want to change:
///
/// ```
/// fn check_user_permissions(user_id: &str) -> Result<bool> {
/// // Check if user exists first
/// let user = database.find_user(user_id)?;
///
/// // This is the part we want to modify
/// if user.role == "admin" {
/// return Ok(true);
/// }
///
/// // Check other permissions
/// check_custom_permissions(user_id)
/// }
/// ```
///
/// And your replace string might look like:
///
/// ```
/// fn check_user_permissions(user_id: &str) -> Result<bool> {
/// // Check if user exists first
/// let user = database.find_user(user_id)?;
///
/// // This is the part we want to modify
/// if user.role == "admin" || user.role == "superuser" {
/// return Ok(true);
/// }
///
/// // Check other permissions
/// check_custom_permissions(user_id)
/// }
/// ```
/// </example>
pub find: String,
/// The string to replace the one unique occurrence of the find string with.
pub replace: String,
}
pub struct FindReplaceFileTool;
impl Tool for FindReplaceFileTool {
fn name(&self) -> String {
"find-replace-file".into()
}
fn needs_confirmation(&self) -> bool {
true
}
fn description(&self) -> String {
include_str!("find_replace_tool/description.md").to_string()
}
fn icon(&self) -> IconName {
IconName::Pencil
}
fn input_schema(&self) -> serde_json::Value {
let schema = schemars::schema_for!(FindReplaceFileToolInput);
serde_json::to_value(&schema).unwrap()
}
fn ui_text(&self, input: &serde_json::Value) -> String {
match serde_json::from_value::<FindReplaceFileToolInput>(input.clone()) {
Ok(input) => input.display_description,
Err(_) => "Edit file".to_string(),
}
}
fn run(
self: Arc<Self>,
input: serde_json::Value,
_messages: &[LanguageModelRequestMessage],
project: Entity<Project>,
action_log: Entity<ActionLog>,
cx: &mut App,
) -> Task<Result<String>> {
let input = match serde_json::from_value::<FindReplaceFileToolInput>(input) {
Ok(input) => input,
Err(err) => return Task::ready(Err(anyhow!(err))),
};
cx.spawn(async move |cx| {
let project_path = project.read_with(cx, |project, cx| {
project
.find_project_path(&input.path, cx)
.context("Path not found in project")
})??;
let buffer = project
.update(cx, |project, cx| project.open_buffer(project_path, cx))?
.await?;
let snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot())?;
if input.find.is_empty() {
return Err(anyhow!("`find` string cannot be empty. Use a different tool if you want to create a file."));
}
let result = cx
.background_spawn(async move {
replace_exact(&input.find, &input.replace, &snapshot).await
})
.await;
if let Some(diff) = result {
buffer.update(cx, |buffer, cx| {
let _ = buffer.apply_diff(diff, cx);
})?;
project.update(cx, |project, cx| {
project.save_buffer(buffer.clone(), cx)
})?.await?;
action_log.update(cx, |log, cx| {
let mut buffers = HashSet::default();
buffers.insert(buffer);
log.buffer_edited(buffers, cx);
})?;
Ok(format!("Edited {}", input.path.display()))
} else {
let err = buffer.read_with(cx, |buffer, _cx| {
let file_exists = buffer
.file()
.map_or(false, |file| file.disk_state().exists());
if !file_exists {
anyhow!("{} does not exist", input.path.display())
} else if buffer.is_empty() {
anyhow!(
"{} is empty, so the provided `find` string wasn't found.",
input.path.display()
)
} else {
anyhow!("Failed to match the provided `find` string")
}
})?;
Err(err)
}
})
}
}

View File

@@ -0,0 +1,7 @@
Find one unique part of a file in the project and replace that text with new text.
This tool is the preferred way to make edits to files. If you have multiple edits to make, including edits across multiple files, then make a plan to respond with a single message containing multiple calls to this tool - one call for each find/replace operation.
You should use this tool when you want to edit a subset of a file's contents, but not the entire file. You should not use this tool when you want to replace the entire contents of a file with completely different contents. You also should not use this tool when you want to move or rename a file. You absolutely must NEVER use this tool to create new files from scratch. If you ever consider using this tool to create a new file from scratch, for any reason, instead you must reconsider and choose a different approach.
DO NOT call this tool until the code to be edited appears in the conversation! You must use another tool to read the file's contents into the conversation, or ask the user to add it to context first.

View File

@@ -6,6 +6,8 @@ use project::Project;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::{fmt::Write, path::Path, sync::Arc};
use ui::IconName;
use util::markdown::MarkdownString;
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct ListDirectoryToolInput {
@@ -31,7 +33,7 @@ pub struct ListDirectoryToolInput {
///
/// If you wanna list contents in the directory `foo/baz`, you should use the path `foo/baz`.
/// </example>
pub path: Arc<Path>,
pub path: String,
}
pub struct ListDirectoryTool;
@@ -41,15 +43,33 @@ impl Tool for ListDirectoryTool {
"list-directory".into()
}
fn needs_confirmation(&self) -> bool {
false
}
fn description(&self) -> String {
include_str!("./list_directory_tool/description.md").into()
}
fn icon(&self) -> IconName {
IconName::Folder
}
fn input_schema(&self) -> serde_json::Value {
let schema = schemars::schema_for!(ListDirectoryToolInput);
serde_json::to_value(&schema).unwrap()
}
fn ui_text(&self, input: &serde_json::Value) -> String {
match serde_json::from_value::<ListDirectoryToolInput>(input.clone()) {
Ok(input) => {
let path = MarkdownString::inline_code(&input.path);
format!("List the {path} directory's contents")
}
Err(_) => "List directory".to_string(),
}
}
fn run(
self: Arc<Self>,
input: serde_json::Value,
@@ -63,8 +83,29 @@ impl Tool for ListDirectoryTool {
Err(err) => return Task::ready(Err(anyhow!(err))),
};
// Sometimes models will return these even though we tell it to give a path and not a glob.
// When this happens, just list the root worktree directories.
if matches!(input.path.as_str(), "." | "" | "./" | "*") {
let output = project
.read(cx)
.worktrees(cx)
.filter_map(|worktree| {
worktree.read(cx).root_entry().and_then(|entry| {
if entry.is_dir() {
entry.path.to_str()
} else {
None
}
})
})
.collect::<Vec<_>>()
.join("\n");
return Task::ready(Ok(output));
}
let Some(project_path) = project.read(cx).find_project_path(&input.path, cx) else {
return Task::ready(Err(anyhow!("Path not found in project")));
return Task::ready(Err(anyhow!("Path {} not found in project", input.path)));
};
let Some(worktree) = project
.read(cx)
@@ -75,11 +116,11 @@ impl Tool for ListDirectoryTool {
let worktree = worktree.read(cx);
let Some(entry) = worktree.entry_for_path(&project_path.path) else {
return Task::ready(Err(anyhow!("Path not found: {}", input.path.display())));
return Task::ready(Err(anyhow!("Path not found: {}", input.path)));
};
if !entry.is_dir() {
return Task::ready(Err(anyhow!("{} is a file.", input.path.display())));
return Task::ready(Err(anyhow!("{} is not a directory.", input.path)));
}
let mut output = String::new();
@@ -92,7 +133,7 @@ impl Tool for ListDirectoryTool {
.unwrap();
}
if output.is_empty() {
return Task::ready(Ok(format!("{} is empty.", input.path.display())));
return Task::ready(Ok(format!("{} is empty.", input.path)));
}
Task::ready(Ok(output))
}

View File

@@ -0,0 +1,132 @@
use anyhow::{anyhow, Result};
use assistant_tool::{ActionLog, Tool};
use gpui::{App, AppContext, Entity, Task};
use language_model::LanguageModelRequestMessage;
use project::Project;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::{path::Path, sync::Arc};
use ui::IconName;
use util::markdown::MarkdownString;
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct MovePathToolInput {
/// The source path of the file or directory to move/rename.
///
/// <example>
/// If the project has the following files:
///
/// - directory1/a/something.txt
/// - directory2/a/things.txt
/// - directory3/a/other.txt
///
/// You can move the first file by providing a source_path of "directory1/a/something.txt"
/// </example>
pub source_path: String,
/// The destination path where the file or directory should be moved/renamed to.
/// If the paths are the same except for the filename, then this will be a rename.
///
/// <example>
/// To move "directory1/a/something.txt" to "directory2/b/renamed.txt",
/// provide a destination_path of "directory2/b/renamed.txt"
/// </example>
pub destination_path: String,
}
pub struct MovePathTool;
impl Tool for MovePathTool {
fn name(&self) -> String {
"move-path".into()
}
fn needs_confirmation(&self) -> bool {
true
}
fn description(&self) -> String {
include_str!("./move_path_tool/description.md").into()
}
fn icon(&self) -> IconName {
IconName::ArrowRightLeft
}
fn input_schema(&self) -> serde_json::Value {
let schema = schemars::schema_for!(MovePathToolInput);
serde_json::to_value(&schema).unwrap()
}
fn ui_text(&self, input: &serde_json::Value) -> String {
match serde_json::from_value::<MovePathToolInput>(input.clone()) {
Ok(input) => {
let src = MarkdownString::inline_code(&input.source_path);
let dest = MarkdownString::inline_code(&input.destination_path);
let src_path = Path::new(&input.source_path);
let dest_path = Path::new(&input.destination_path);
match dest_path
.file_name()
.and_then(|os_str| os_str.to_os_string().into_string().ok())
{
Some(filename) if src_path.parent() == dest_path.parent() => {
let filename = MarkdownString::inline_code(&filename);
format!("Rename {src} to {filename}")
}
_ => {
format!("Move {src} to {dest}")
}
}
}
Err(_) => "Move path".to_string(),
}
}
fn run(
self: Arc<Self>,
input: serde_json::Value,
_messages: &[LanguageModelRequestMessage],
project: Entity<Project>,
_action_log: Entity<ActionLog>,
cx: &mut App,
) -> Task<Result<String>> {
let input = match serde_json::from_value::<MovePathToolInput>(input) {
Ok(input) => input,
Err(err) => return Task::ready(Err(anyhow!(err))),
};
let rename_task = project.update(cx, |project, cx| {
match project
.find_project_path(&input.source_path, cx)
.and_then(|project_path| project.entry_for_path(&project_path, cx))
{
Some(entity) => match project.find_project_path(&input.destination_path, cx) {
Some(project_path) => project.rename_entry(entity.id, project_path.path, cx),
None => Task::ready(Err(anyhow!(
"Destination path {} was outside the project.",
input.destination_path
))),
},
None => Task::ready(Err(anyhow!(
"Source path {} was not found in the project.",
input.source_path
))),
}
});
cx.background_spawn(async move {
match rename_task.await {
Ok(_) => Ok(format!(
"Moved {} to {}",
input.source_path, input.destination_path
)),
Err(err) => Err(anyhow!(
"Failed to move {} to {}: {}",
input.source_path,
input.destination_path,
err
)),
}
})
}
}

View File

@@ -0,0 +1,5 @@
Moves or rename a file or directory in the project, and returns confirmation that the move succeeded.
If the source and destination directories are the same, but the filename is different, this performs
a rename. Otherwise, it performs a move.
This tool should be used when it's desirable to move or rename a file or directory without changing its contents at all.

View File

@@ -8,6 +8,7 @@ use language_model::LanguageModelRequestMessage;
use project::Project;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use ui::IconName;
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
@@ -31,15 +32,27 @@ impl Tool for NowTool {
"now".into()
}
fn needs_confirmation(&self) -> bool {
false
}
fn description(&self) -> String {
"Returns the current datetime in RFC 3339 format. Only use this tool when the user specifically asks for it or the current task would benefit from knowing the current datetime.".into()
}
fn icon(&self) -> IconName {
IconName::Info
}
fn input_schema(&self) -> serde_json::Value {
let schema = schemars::schema_for!(NowToolInput);
serde_json::to_value(&schema).unwrap()
}
fn ui_text(&self, _input: &serde_json::Value) -> String {
"Get current time".to_string()
}
fn run(
self: Arc<Self>,
input: serde_json::Value,

View File

@@ -6,6 +6,7 @@ use project::Project;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::{path::PathBuf, sync::Arc};
use ui::IconName;
use util::paths::PathMatcher;
use worktree::Snapshot;
@@ -39,15 +40,30 @@ impl Tool for PathSearchTool {
"path-search".into()
}
fn needs_confirmation(&self) -> bool {
false
}
fn description(&self) -> String {
include_str!("./path_search_tool/description.md").into()
}
fn icon(&self) -> IconName {
IconName::SearchCode
}
fn input_schema(&self) -> serde_json::Value {
let schema = schemars::schema_for!(PathSearchToolInput);
serde_json::to_value(&schema).unwrap()
}
fn ui_text(&self, input: &serde_json::Value) -> String {
match serde_json::from_value::<PathSearchToolInput>(input.clone()) {
Ok(input) => format!("Find paths matching “`{}`”", input.glob),
Err(_) => "Search paths".to_string(),
}
}
fn run(
self: Arc<Self>,
input: serde_json::Value,
@@ -60,9 +76,13 @@ impl Tool for PathSearchTool {
Ok(input) => (input.offset.unwrap_or(0), input.glob),
Err(err) => return Task::ready(Err(anyhow!(err))),
};
let path_matcher = match PathMatcher::new(&[glob.clone()]) {
let path_matcher = match PathMatcher::new([
// Sometimes models try to search for "". In this case, return all paths in the project.
if glob.is_empty() { "*" } else { &glob },
]) {
Ok(matcher) => matcher,
Err(err) => return Task::ready(Err(anyhow!("Invalid glob: {}", err))),
Err(err) => return Task::ready(Err(anyhow!("Invalid glob: {err}"))),
};
let snapshots: Vec<Snapshot> = project
.read(cx)

View File

@@ -9,6 +9,8 @@ use language_model::LanguageModelRequestMessage;
use project::Project;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use ui::IconName;
use util::markdown::MarkdownString;
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct ReadFileToolInput {
@@ -44,15 +46,33 @@ impl Tool for ReadFileTool {
"read-file".into()
}
fn needs_confirmation(&self) -> bool {
false
}
fn description(&self) -> String {
include_str!("./read_file_tool/description.md").into()
}
fn icon(&self) -> IconName {
IconName::Eye
}
fn input_schema(&self) -> serde_json::Value {
let schema = schemars::schema_for!(ReadFileToolInput);
serde_json::to_value(&schema).unwrap()
}
fn ui_text(&self, input: &serde_json::Value) -> String {
match serde_json::from_value::<ReadFileToolInput>(input.clone()) {
Ok(input) => {
let path = MarkdownString::inline_code(&input.path.display().to_string());
format!("Read file {path}")
}
Err(_) => "Read file".to_string(),
}
}
fn run(
self: Arc<Self>,
input: serde_json::Value,
@@ -67,7 +87,10 @@ impl Tool for ReadFileTool {
};
let Some(project_path) = project.read(cx).find_project_path(&input.path, cx) else {
return Task::ready(Err(anyhow!("Path not found in project")));
return Task::ready(Err(anyhow!(
"Path {} not found in project",
&input.path.display()
)));
};
cx.spawn(async move |cx| {

View File

@@ -11,6 +11,8 @@ use project::{
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::{cmp, fmt::Write, sync::Arc};
use ui::IconName;
use util::markdown::MarkdownString;
use util::paths::PathMatcher;
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
@@ -22,10 +24,17 @@ pub struct RegexSearchToolInput {
/// Optional starting position for paginated results (0-based).
/// When not provided, starts from the beginning.
#[serde(default)]
pub offset: Option<usize>,
pub offset: Option<u32>,
}
const RESULTS_PER_PAGE: usize = 20;
impl RegexSearchToolInput {
/// Which page of search results this is.
pub fn page(&self) -> u32 {
1 + (self.offset.unwrap_or(0) / RESULTS_PER_PAGE)
}
}
const RESULTS_PER_PAGE: u32 = 20;
pub struct RegexSearchTool;
@@ -34,15 +43,39 @@ impl Tool for RegexSearchTool {
"regex-search".into()
}
fn needs_confirmation(&self) -> bool {
false
}
fn description(&self) -> String {
include_str!("./regex_search_tool/description.md").into()
}
fn icon(&self) -> IconName {
IconName::Regex
}
fn input_schema(&self) -> serde_json::Value {
let schema = schemars::schema_for!(RegexSearchToolInput);
serde_json::to_value(&schema).unwrap()
}
fn ui_text(&self, input: &serde_json::Value) -> String {
match serde_json::from_value::<RegexSearchToolInput>(input.clone()) {
Ok(input) => {
let page = input.page();
let regex = MarkdownString::inline_code(&input.regex);
if page > 1 {
format!("Get page {page} of search results for regex “{regex}")
} else {
format!("Search files for regex “{regex}")
}
}
Err(_) => "Search with regex".to_string(),
}
}
fn run(
self: Arc<Self>,
input: serde_json::Value,
@@ -154,7 +187,7 @@ impl Tool for RegexSearchTool {
offset + matches_found,
offset + RESULTS_PER_PAGE,
))
} else {
} else {
Ok(format!("Found {matches_found} matches:\n{output}"))
}
})

View File

@@ -1,5 +1,6 @@
use language::{BufferSnapshot, Diff, Point, ToOffset};
use project::search::SearchQuery;
use std::iter;
use util::{paths::PathMatcher, ResultExt as _};
/// Performs an exact string replacement in a buffer, requiring precise character-for-character matching.
@@ -11,8 +12,8 @@ pub async fn replace_exact(old: &str, new: &str, snapshot: &BufferSnapshot) -> O
false,
true,
true,
PathMatcher::new(&[]).ok()?,
PathMatcher::new(&[]).ok()?,
PathMatcher::new(iter::empty::<&str>()).ok()?,
PathMatcher::new(iter::empty::<&str>()).ok()?,
None,
)
.log_err()?;

View File

@@ -7,6 +7,7 @@ use language_model::LanguageModelRequestMessage;
use project::Project;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use ui::IconName;
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct ThinkingToolInput {
@@ -22,15 +23,27 @@ impl Tool for ThinkingTool {
"thinking".to_string()
}
fn needs_confirmation(&self) -> bool {
false
}
fn description(&self) -> String {
include_str!("./thinking_tool/description.md").to_string()
}
fn icon(&self) -> IconName {
IconName::Brain
}
fn input_schema(&self) -> serde_json::Value {
let schema = schemars::schema_for!(ThinkingToolInput);
serde_json::to_value(&schema).unwrap()
}
fn ui_text(&self, _input: &serde_json::Value) -> String {
"Thinking".to_string()
}
fn run(
self: Arc<Self>,
input: serde_json::Value,

View File

@@ -3,12 +3,9 @@ use git2::{DiffLineType as GitDiffLineType, DiffOptions as GitOptions, Patch as
use gpui::{App, AppContext as _, AsyncApp, Context, Entity, EventEmitter, Task};
use language::{Language, LanguageRegistry};
use rope::Rope;
use std::cmp::Ordering;
use std::mem;
use std::{future::Future, iter, ops::Range, sync::Arc};
use std::{cmp::Ordering, future::Future, iter, mem, ops::Range, sync::Arc};
use sum_tree::SumTree;
use text::{Anchor, Bias, BufferId, OffsetRangeExt, Point};
use text::{AnchorRangeExt, ToOffset as _};
use text::{Anchor, Bias, BufferId, OffsetRangeExt, Point, ToOffset as _};
use util::ResultExt;
pub struct BufferDiff {
@@ -189,14 +186,14 @@ impl BufferDiffSnapshot {
impl BufferDiffInner {
/// Returns the new index text and new pending hunks.
fn stage_or_unstage_hunks(
fn stage_or_unstage_hunks_impl(
&mut self,
unstaged_diff: &Self,
stage: bool,
hunks: &[DiffHunk],
buffer: &text::BufferSnapshot,
file_exists: bool,
) -> (Option<Rope>, SumTree<PendingHunk>) {
) -> Option<Rope> {
let head_text = self
.base_text_exists
.then(|| self.base_text.as_rope().clone());
@@ -209,7 +206,7 @@ impl BufferDiffInner {
let (index_text, head_text) = match (index_text, head_text) {
(Some(index_text), Some(head_text)) if file_exists || !stage => (index_text, head_text),
(index_text, head_text) => {
let (rope, new_status) = if stage {
let (new_index_text, new_status) = if stage {
log::debug!("stage all");
(
file_exists.then(|| buffer.as_rope().clone()),
@@ -229,18 +226,13 @@ impl BufferDiffInner {
buffer_version: buffer.version().clone(),
new_status,
};
let tree = SumTree::from_item(hunk, buffer);
return (rope, tree);
self.pending_hunks = SumTree::from_item(hunk, buffer);
return new_index_text;
}
};
let mut unstaged_hunk_cursor = unstaged_diff.hunks.cursor::<DiffHunkSummary>(buffer);
unstaged_hunk_cursor.next(buffer);
let mut pending_hunks = SumTree::new(buffer);
let mut old_pending_hunks = unstaged_diff
.pending_hunks
.cursor::<DiffHunkSummary>(buffer);
let mut old_pending_hunks = self.pending_hunks.cursor::<DiffHunkSummary>(buffer);
// first, merge new hunks into pending_hunks
for DiffHunk {
@@ -252,22 +244,19 @@ impl BufferDiffInner {
{
let preceding_pending_hunks =
old_pending_hunks.slice(&buffer_range.start, Bias::Left, buffer);
pending_hunks.append(preceding_pending_hunks, buffer);
// skip all overlapping old pending hunks
while old_pending_hunks
.item()
.is_some_and(|preceding_pending_hunk_item| {
preceding_pending_hunk_item
.buffer_range
.overlaps(&buffer_range, buffer)
})
{
// Skip all overlapping or adjacent old pending hunks
while old_pending_hunks.item().is_some_and(|old_hunk| {
old_hunk
.buffer_range
.start
.cmp(&buffer_range.end, buffer)
.is_le()
}) {
old_pending_hunks.next(buffer);
}
// merge into pending hunks
if (stage && secondary_status == DiffHunkSecondaryStatus::NoSecondaryHunk)
|| (!stage && secondary_status == DiffHunkSecondaryStatus::HasSecondaryHunk)
{
@@ -291,56 +280,74 @@ impl BufferDiffInner {
// append the remainder
pending_hunks.append(old_pending_hunks.suffix(buffer), buffer);
let mut prev_unstaged_hunk_buffer_offset = 0;
let mut prev_unstaged_hunk_base_text_offset = 0;
let mut edits = Vec::<(Range<usize>, String)>::new();
let mut unstaged_hunk_cursor = unstaged_diff.hunks.cursor::<DiffHunkSummary>(buffer);
unstaged_hunk_cursor.next(buffer);
// then, iterate over all pending hunks (both new ones and the existing ones) and compute the edits
for PendingHunk {
let mut prev_unstaged_hunk_buffer_end = 0;
let mut prev_unstaged_hunk_base_text_end = 0;
let mut edits = Vec::<(Range<usize>, String)>::new();
let mut pending_hunks_iter = pending_hunks.iter().cloned().peekable();
while let Some(PendingHunk {
buffer_range,
diff_base_byte_range,
..
} in pending_hunks.iter().cloned()
}) = pending_hunks_iter.next()
{
let skipped_hunks = unstaged_hunk_cursor.slice(&buffer_range.start, Bias::Left, buffer);
// Advance unstaged_hunk_cursor to skip unstaged hunks before current hunk
let skipped_unstaged =
unstaged_hunk_cursor.slice(&buffer_range.start, Bias::Left, buffer);
if let Some(secondary_hunk) = skipped_hunks.last() {
prev_unstaged_hunk_base_text_offset = secondary_hunk.diff_base_byte_range.end;
prev_unstaged_hunk_buffer_offset =
secondary_hunk.buffer_range.end.to_offset(buffer);
if let Some(unstaged_hunk) = skipped_unstaged.last() {
prev_unstaged_hunk_base_text_end = unstaged_hunk.diff_base_byte_range.end;
prev_unstaged_hunk_buffer_end = unstaged_hunk.buffer_range.end.to_offset(buffer);
}
// Find where this hunk is in the index if it doesn't overlap
let mut buffer_offset_range = buffer_range.to_offset(buffer);
let start_overshoot = buffer_offset_range.start - prev_unstaged_hunk_buffer_offset;
let mut index_start = prev_unstaged_hunk_base_text_offset + start_overshoot;
let start_overshoot = buffer_offset_range.start - prev_unstaged_hunk_buffer_end;
let mut index_start = prev_unstaged_hunk_base_text_end + start_overshoot;
while let Some(unstaged_hunk) = unstaged_hunk_cursor.item().filter(|item| {
item.buffer_range
.start
.cmp(&buffer_range.end, buffer)
.is_le()
}) {
let unstaged_hunk_offset_range = unstaged_hunk.buffer_range.to_offset(buffer);
prev_unstaged_hunk_base_text_offset = unstaged_hunk.diff_base_byte_range.end;
prev_unstaged_hunk_buffer_offset = unstaged_hunk_offset_range.end;
loop {
// Merge this hunk with any overlapping unstaged hunks.
if let Some(unstaged_hunk) = unstaged_hunk_cursor.item() {
let unstaged_hunk_offset_range = unstaged_hunk.buffer_range.to_offset(buffer);
if unstaged_hunk_offset_range.start <= buffer_offset_range.end {
prev_unstaged_hunk_base_text_end = unstaged_hunk.diff_base_byte_range.end;
prev_unstaged_hunk_buffer_end = unstaged_hunk_offset_range.end;
index_start = index_start.min(unstaged_hunk.diff_base_byte_range.start);
buffer_offset_range.start = buffer_offset_range
.start
.min(unstaged_hunk_offset_range.start);
index_start = index_start.min(unstaged_hunk.diff_base_byte_range.start);
buffer_offset_range.start = buffer_offset_range
.start
.min(unstaged_hunk_offset_range.start);
buffer_offset_range.end =
buffer_offset_range.end.max(unstaged_hunk_offset_range.end);
unstaged_hunk_cursor.next(buffer);
unstaged_hunk_cursor.next(buffer);
continue;
}
}
// If any unstaged hunks were merged, then subsequent pending hunks may
// now overlap this hunk. Merge them.
if let Some(next_pending_hunk) = pending_hunks_iter.peek() {
let next_pending_hunk_offset_range =
next_pending_hunk.buffer_range.to_offset(buffer);
if next_pending_hunk_offset_range.start <= buffer_offset_range.end {
buffer_offset_range.end = next_pending_hunk_offset_range.end;
pending_hunks_iter.next();
continue;
}
}
break;
}
let end_overshoot = buffer_offset_range
.end
.saturating_sub(prev_unstaged_hunk_buffer_offset);
let index_end = prev_unstaged_hunk_base_text_offset + end_overshoot;
let index_range = index_start..index_end;
buffer_offset_range.end = buffer_offset_range
.end
.max(prev_unstaged_hunk_buffer_offset);
.saturating_sub(prev_unstaged_hunk_buffer_end);
let index_end = prev_unstaged_hunk_base_text_end + end_overshoot;
let index_byte_range = index_start..index_end;
let replacement_text = if stage {
log::debug!("stage hunk {:?}", buffer_offset_range);
@@ -354,10 +361,19 @@ impl BufferDiffInner {
.collect::<String>()
};
edits.push((index_range, replacement_text));
edits.push((index_byte_range, replacement_text));
}
drop(pending_hunks_iter);
drop(old_pending_hunks);
self.pending_hunks = pending_hunks;
debug_assert!(edits.iter().is_sorted_by_key(|(range, _)| range.start));
#[cfg(debug_assertions)] // invariants: non-overlapping and sorted
{
for window in edits.windows(2) {
let (range_a, range_b) = (&window[0].0, &window[1].0);
debug_assert!(range_a.end < range_b.start);
}
}
let mut new_index_text = Rope::new();
let mut index_cursor = index_text.cursor(0);
@@ -368,7 +384,7 @@ impl BufferDiffInner {
new_index_text.push(&replacement_text);
}
new_index_text.append(index_cursor.suffix());
(Some(new_index_text), pending_hunks)
Some(new_index_text)
}
fn hunks_intersecting_range<'a>(
@@ -405,15 +421,14 @@ impl BufferDiffInner {
]
});
let mut pending_hunks_cursor = self.pending_hunks.cursor::<DiffHunkSummary>(buffer);
pending_hunks_cursor.next(buffer);
let mut secondary_cursor = None;
let mut pending_hunks_cursor = None;
if let Some(secondary) = secondary.as_ref() {
let mut cursor = secondary.hunks.cursor::<DiffHunkSummary>(buffer);
cursor.next(buffer);
secondary_cursor = Some(cursor);
let mut cursor = secondary.pending_hunks.cursor::<DiffHunkSummary>(buffer);
cursor.next(buffer);
pending_hunks_cursor = Some(cursor);
}
let max_point = buffer.max_point();
@@ -435,29 +450,27 @@ impl BufferDiffInner {
let mut secondary_status = DiffHunkSecondaryStatus::NoSecondaryHunk;
let mut has_pending = false;
if let Some(pending_cursor) = pending_hunks_cursor.as_mut() {
if start_anchor
.cmp(&pending_cursor.start().buffer_range.start, buffer)
.is_gt()
{
pending_cursor.seek_forward(&start_anchor, Bias::Left, buffer);
if start_anchor
.cmp(&pending_hunks_cursor.start().buffer_range.start, buffer)
.is_gt()
{
pending_hunks_cursor.seek_forward(&start_anchor, Bias::Left, buffer);
}
if let Some(pending_hunk) = pending_hunks_cursor.item() {
let mut pending_range = pending_hunk.buffer_range.to_point(buffer);
if pending_range.end.column > 0 {
pending_range.end.row += 1;
pending_range.end.column = 0;
}
if let Some(pending_hunk) = pending_cursor.item() {
let mut pending_range = pending_hunk.buffer_range.to_point(buffer);
if pending_range.end.column > 0 {
pending_range.end.row += 1;
pending_range.end.column = 0;
}
if pending_range == (start_point..end_point) {
if !buffer.has_edits_since_in_range(
&pending_hunk.buffer_version,
start_anchor..end_anchor,
) {
has_pending = true;
secondary_status = pending_hunk.new_status;
}
if pending_range == (start_point..end_point) {
if !buffer.has_edits_since_in_range(
&pending_hunk.buffer_version,
start_anchor..end_anchor,
) {
has_pending = true;
secondary_status = pending_hunk.new_status;
}
}
}
@@ -836,10 +849,8 @@ impl BufferDiff {
}
pub fn clear_pending_hunks(&mut self, cx: &mut Context<Self>) {
if let Some(secondary_diff) = &self.secondary_diff {
secondary_diff.update(cx, |diff, _| {
diff.inner.pending_hunks = SumTree::from_summary(DiffHunkSummary::default());
});
if self.secondary_diff.is_some() {
self.inner.pending_hunks = SumTree::from_summary(DiffHunkSummary::default());
cx.emit(BufferDiffEvent::DiffChanged {
changed_range: Some(Anchor::MIN..Anchor::MAX),
});
@@ -854,7 +865,7 @@ impl BufferDiff {
file_exists: bool,
cx: &mut Context<Self>,
) -> Option<Rope> {
let (new_index_text, new_pending_hunks) = self.inner.stage_or_unstage_hunks(
let new_index_text = self.inner.stage_or_unstage_hunks_impl(
&self.secondary_diff.as_ref()?.read(cx).inner,
stage,
&hunks,
@@ -862,11 +873,6 @@ impl BufferDiff {
file_exists,
);
if let Some(unstaged_diff) = &self.secondary_diff {
unstaged_diff.update(cx, |diff, _| {
diff.inner.pending_hunks = new_pending_hunks;
});
}
cx.emit(BufferDiffEvent::HunksStagedOrUnstaged(
new_index_text.clone(),
));
@@ -1240,13 +1246,13 @@ impl DiffHunkStatus {
}
}
/// Range (crossing new lines), old, new
#[cfg(any(test, feature = "test-support"))]
#[track_caller]
pub fn assert_hunks<ExpectedText, HunkIter>(
diff_hunks: HunkIter,
buffer: &text::BufferSnapshot,
diff_base: &str,
// Line range, deleted, added, status
expected_hunks: &[(Range<u32>, ExpectedText, ExpectedText, DiffHunkStatus)],
) where
HunkIter: Iterator<Item = DiffHunk>,
@@ -1267,11 +1273,11 @@ pub fn assert_hunks<ExpectedText, HunkIter>(
let expected_hunks: Vec<_> = expected_hunks
.iter()
.map(|(r, old_text, new_text, status)| {
.map(|(line_range, deleted_text, added_text, status)| {
(
Point::new(r.start, 0)..Point::new(r.end, 0),
old_text.as_ref(),
new_text.as_ref().to_string(),
Point::new(line_range.start, 0)..Point::new(line_range.end, 0),
deleted_text.as_ref(),
added_text.as_ref().to_string(),
*status,
)
})
@@ -1286,6 +1292,7 @@ mod tests {
use super::*;
use gpui::TestAppContext;
use pretty_assertions::{assert_eq, assert_ne};
use rand::{rngs::StdRng, Rng as _};
use text::{Buffer, BufferId, Rope};
use unindent::Unindent as _;
@@ -1645,6 +1652,75 @@ mod tests {
"
.unindent(),
},
Example {
name: "one unstaged hunk that contains two uncommitted hunks",
head_text: "
one
two
three
four
"
.unindent(),
index_text: "
one
two
three
four
"
.unindent(),
buffer_marked_text: "
«one
three // modified
four»
"
.unindent(),
final_index_text: "
one
three // modified
four
"
.unindent(),
},
Example {
name: "one uncommitted hunk that contains two unstaged hunks",
head_text: "
one
two
three
four
five
"
.unindent(),
index_text: "
ZERO
one
TWO
THREE
FOUR
five
"
.unindent(),
buffer_marked_text: "
«one
TWO_HUNDRED
THREE
FOUR_HUNDRED
five»
"
.unindent(),
final_index_text: "
ZERO
one
TWO_HUNDRED
THREE
FOUR_HUNDRED
five
"
.unindent(),
},
];
for example in table {
@@ -1705,6 +1781,66 @@ mod tests {
}
}
#[gpui::test]
async fn test_toggling_stage_and_unstage_same_hunk(cx: &mut TestAppContext) {
let head_text = "
one
two
three
"
.unindent();
let index_text = head_text.clone();
let buffer_text = "
one
three
"
.unindent();
let buffer = Buffer::new(0, BufferId::new(1).unwrap(), buffer_text.clone());
let unstaged = BufferDiff::build_sync(buffer.clone(), index_text, cx);
let uncommitted = BufferDiff::build_sync(buffer.clone(), head_text.clone(), cx);
let unstaged_diff = cx.new(|cx| {
let mut diff = BufferDiff::new(&buffer, cx);
diff.set_state(unstaged, &buffer);
diff
});
let uncommitted_diff = cx.new(|cx| {
let mut diff = BufferDiff::new(&buffer, cx);
diff.set_state(uncommitted, &buffer);
diff.set_secondary_diff(unstaged_diff.clone());
diff
});
uncommitted_diff.update(cx, |diff, cx| {
let hunk = diff.hunks(&buffer, cx).next().unwrap();
let new_index_text = diff
.stage_or_unstage_hunks(true, &[hunk.clone()], &buffer, true, cx)
.unwrap()
.to_string();
assert_eq!(new_index_text, buffer_text);
let hunk = diff.hunks(&buffer, &cx).next().unwrap();
assert_eq!(
hunk.secondary_status,
DiffHunkSecondaryStatus::SecondaryHunkRemovalPending
);
let index_text = diff
.stage_or_unstage_hunks(false, &[hunk], &buffer, true, cx)
.unwrap()
.to_string();
assert_eq!(index_text, head_text);
let hunk = diff.hunks(&buffer, &cx).next().unwrap();
// optimistically unstaged (fine, could also be HasSecondaryHunk)
assert_eq!(
hunk.secondary_status,
DiffHunkSecondaryStatus::SecondaryHunkAdditionPending
);
});
}
#[gpui::test]
async fn test_buffer_diff_compare(cx: &mut TestAppContext) {
let base_text = "

View File

@@ -43,10 +43,10 @@ telemetry.workspace = true
util.workspace = true
[target.'cfg(target_os = "macos")'.dependencies]
livekit_client_macos = { workspace = true }
livekit_client_macos.workspace = true
[target.'cfg(not(target_os = "macos"))'.dependencies]
livekit_client = { workspace = true }
livekit_client.workspace = true
[dev-dependencies]
client = { workspace = true, features = ["test-support"] }

View File

@@ -469,18 +469,25 @@ impl Room {
let project = handle.read(cx);
if let Some(project_id) = project.remote_id() {
projects.insert(project_id, handle.clone());
let mut worktrees = Vec::new();
let mut repositories = Vec::new();
for worktree in project.worktrees(cx) {
let worktree = worktree.read(cx);
worktrees.push(proto::RejoinWorktree {
id: worktree.id().to_proto(),
scan_id: worktree.completed_scan_id() as u64,
});
for repository in worktree.repositories().iter() {
repositories.push(proto::RejoinRepository {
id: repository.work_directory_id().to_proto(),
scan_id: worktree.completed_scan_id() as u64,
});
}
}
rejoined_projects.push(proto::RejoinProject {
id: project_id,
worktrees: project
.worktrees(cx)
.map(|worktree| {
let worktree = worktree.read(cx);
proto::RejoinWorktree {
id: worktree.id().to_proto(),
scan_id: worktree.completed_scan_id() as u64,
}
})
.collect(),
worktrees,
repositories,
});
}
return true;

View File

@@ -524,18 +524,27 @@ impl Room {
let project = handle.read(cx);
if let Some(project_id) = project.remote_id() {
projects.insert(project_id, handle.clone());
let mut worktrees = Vec::new();
let mut repositories = Vec::new();
for worktree in project.worktrees(cx) {
let worktree = worktree.read(cx);
worktrees.push(proto::RejoinWorktree {
id: worktree.id().to_proto(),
scan_id: worktree.completed_scan_id() as u64,
});
}
for (entry_id, repository) in project.repositories(cx) {
let repository = repository.read(cx);
repositories.push(proto::RejoinRepository {
id: entry_id.to_proto(),
scan_id: repository.completed_scan_id as u64,
});
}
rejoined_projects.push(proto::RejoinProject {
id: project_id,
worktrees: project
.worktrees(cx)
.map(|worktree| {
let worktree = worktree.read(cx);
proto::RejoinWorktree {
id: worktree.id().to_proto(),
scan_id: worktree.completed_scan_id() as u64,
}
})
.collect(),
worktrees,
repositories,
});
}
return true;

View File

@@ -27,6 +27,7 @@ trait InstalledApp {
fn zed_version_string(&self) -> String;
fn launch(&self, ipc_url: String) -> anyhow::Result<()>;
fn run_foreground(&self, ipc_url: String) -> io::Result<ExitStatus>;
fn path(&self) -> PathBuf;
}
#[derive(Parser, Debug)]
@@ -73,6 +74,10 @@ struct Args {
/// Run zed in dev-server mode
#[arg(long)]
dev_server_token: Option<String>,
/// Not supported in Zed CLI, only supported on Zed binary
/// Will attempt to give the correct command to run
#[arg(long)]
system_specs: bool,
/// Uninstall Zed from user system
#[cfg(all(
any(target_os = "linux", target_os = "macos"),
@@ -140,6 +145,16 @@ fn main() -> Result<()> {
return Ok(());
}
if args.system_specs {
let path = app.path();
let msg = [
"The `--system-specs` argument is not supported in the Zed CLI, only on Zed binary.",
"To retrieve the system specs on the command line, run the following command:",
&format!("{} --system-specs", path.display()),
];
return Err(anyhow::anyhow!(msg.join("\n")));
}
#[cfg(all(
any(target_os = "linux", target_os = "macos"),
not(feature = "no-bundled-uninstall")
@@ -437,6 +452,10 @@ mod linux {
.arg(ipc_url)
.status()
}
fn path(&self) -> PathBuf {
self.0.clone()
}
}
impl App {
@@ -674,6 +693,10 @@ mod windows {
.spawn()?
.wait()
}
fn path(&self) -> PathBuf {
self.0.clone()
}
}
impl Detect {
@@ -876,6 +899,13 @@ mod mac_os {
std::process::Command::new(path).arg(ipc_url).status()
}
fn path(&self) -> PathBuf {
match self {
Bundle::App { app_bundle, .. } => app_bundle.join("Contents/MacOS/zed").clone(),
Bundle::LocalPath { executable, .. } => executable.clone(),
}
}
}
impl Bundle {

View File

@@ -115,7 +115,7 @@ notifications = { workspace = true, features = ["test-support"] }
pretty_assertions.workspace = true
project = { workspace = true, features = ["test-support"] }
prompt_store.workspace = true
recent_projects = { workspace = true }
recent_projects.workspace = true
release_channel.workspace = true
remote = { workspace = true, features = ["test-support"] }
remote_server.workspace = true

View File

@@ -15,9 +15,13 @@ CREATE TABLE "users" (
"github_user_created_at" TIMESTAMP WITHOUT TIME ZONE,
"custom_llm_monthly_allowance_in_cents" INTEGER
);
CREATE UNIQUE INDEX "index_users_github_login" ON "users" ("github_login");
CREATE UNIQUE INDEX "index_invite_code_users" ON "users" ("invite_code");
CREATE INDEX "index_users_on_email_address" ON "users" ("email_address");
CREATE UNIQUE INDEX "index_users_on_github_user_id" ON "users" ("github_user_id");
CREATE TABLE "access_tokens" (
@@ -26,6 +30,7 @@ CREATE TABLE "access_tokens" (
"impersonated_user_id" INTEGER REFERENCES users (id),
"hash" VARCHAR(128)
);
CREATE INDEX "index_access_tokens_user_id" ON "access_tokens" ("user_id");
CREATE TABLE "contacts" (
@@ -36,7 +41,9 @@ CREATE TABLE "contacts" (
"should_notify" BOOLEAN NOT NULL,
"accepted" BOOLEAN NOT NULL
);
CREATE UNIQUE INDEX "index_contacts_user_ids" ON "contacts" ("user_id_a", "user_id_b");
CREATE INDEX "index_contacts_user_id_b" ON "contacts" ("user_id_b");
CREATE TABLE "rooms" (
@@ -45,6 +52,7 @@ CREATE TABLE "rooms" (
"environment" VARCHAR,
"channel_id" INTEGER REFERENCES channels (id) ON DELETE CASCADE
);
CREATE UNIQUE INDEX "index_rooms_on_channel_id" ON "rooms" ("channel_id");
CREATE TABLE "projects" (
@@ -55,7 +63,9 @@ CREATE TABLE "projects" (
"host_connection_server_id" INTEGER REFERENCES servers (id) ON DELETE CASCADE,
"unregistered" BOOLEAN NOT NULL DEFAULT FALSE
);
CREATE INDEX "index_projects_on_host_connection_server_id" ON "projects" ("host_connection_server_id");
CREATE INDEX "index_projects_on_host_connection_id_and_host_connection_server_id" ON "projects" ("host_connection_id", "host_connection_server_id");
CREATE TABLE "worktrees" (
@@ -67,8 +77,9 @@ CREATE TABLE "worktrees" (
"scan_id" INTEGER NOT NULL,
"is_complete" BOOL NOT NULL DEFAULT FALSE,
"completed_scan_id" INTEGER NOT NULL,
PRIMARY KEY(project_id, id)
PRIMARY KEY (project_id, id)
);
CREATE INDEX "index_worktrees_on_project_id" ON "worktrees" ("project_id");
CREATE TABLE "worktree_entries" (
@@ -87,32 +98,33 @@ CREATE TABLE "worktree_entries" (
"is_deleted" BOOL NOT NULL,
"git_status" INTEGER,
"is_fifo" BOOL NOT NULL,
PRIMARY KEY(project_id, worktree_id, id),
FOREIGN KEY(project_id, worktree_id) REFERENCES worktrees (project_id, id) ON DELETE CASCADE
PRIMARY KEY (project_id, worktree_id, id),
FOREIGN KEY (project_id, worktree_id) REFERENCES worktrees (project_id, id) ON DELETE CASCADE
);
CREATE INDEX "index_worktree_entries_on_project_id" ON "worktree_entries" ("project_id");
CREATE INDEX "index_worktree_entries_on_project_id_and_worktree_id" ON "worktree_entries" ("project_id", "worktree_id");
CREATE TABLE "worktree_repositories" (
CREATE TABLE "project_repositories" (
"project_id" INTEGER NOT NULL,
"worktree_id" INTEGER NOT NULL,
"work_directory_id" INTEGER NOT NULL,
"abs_path" VARCHAR,
"id" INTEGER NOT NULL,
"entry_ids" VARCHAR,
"legacy_worktree_id" INTEGER,
"branch" VARCHAR,
"scan_id" INTEGER NOT NULL,
"is_deleted" BOOL NOT NULL,
"current_merge_conflicts" VARCHAR,
"branch_summary" VARCHAR,
PRIMARY KEY(project_id, worktree_id, work_directory_id),
FOREIGN KEY(project_id, worktree_id) REFERENCES worktrees (project_id, id) ON DELETE CASCADE,
FOREIGN KEY(project_id, worktree_id, work_directory_id) REFERENCES worktree_entries (project_id, worktree_id, id) ON DELETE CASCADE
PRIMARY KEY (project_id, id)
);
CREATE INDEX "index_worktree_repositories_on_project_id" ON "worktree_repositories" ("project_id");
CREATE INDEX "index_worktree_repositories_on_project_id_and_worktree_id" ON "worktree_repositories" ("project_id", "worktree_id");
CREATE TABLE "worktree_repository_statuses" (
CREATE INDEX "index_project_repositories_on_project_id" ON "project_repositories" ("project_id");
CREATE TABLE "project_repository_statuses" (
"project_id" INTEGER NOT NULL,
"worktree_id" INT8 NOT NULL,
"work_directory_id" INT8 NOT NULL,
"repository_id" INTEGER NOT NULL,
"repo_path" VARCHAR NOT NULL,
"status" INT8 NOT NULL,
"status_kind" INT4 NOT NULL,
@@ -120,13 +132,12 @@ CREATE TABLE "worktree_repository_statuses" (
"second_status" INT4 NULL,
"scan_id" INT8 NOT NULL,
"is_deleted" BOOL NOT NULL,
PRIMARY KEY(project_id, worktree_id, work_directory_id, repo_path),
FOREIGN KEY(project_id, worktree_id) REFERENCES worktrees (project_id, id) ON DELETE CASCADE,
FOREIGN KEY(project_id, worktree_id, work_directory_id) REFERENCES worktree_entries (project_id, worktree_id, id) ON DELETE CASCADE
PRIMARY KEY (project_id, repository_id, repo_path)
);
CREATE INDEX "index_wt_repos_statuses_on_project_id" ON "worktree_repository_statuses" ("project_id");
CREATE INDEX "index_wt_repos_statuses_on_project_id_and_wt_id" ON "worktree_repository_statuses" ("project_id", "worktree_id");
CREATE INDEX "index_wt_repos_statuses_on_project_id_and_wt_id_and_wd_id" ON "worktree_repository_statuses" ("project_id", "worktree_id", "work_directory_id");
CREATE INDEX "index_project_repos_statuses_on_project_id" ON "project_repository_statuses" ("project_id");
CREATE INDEX "index_project_repos_statuses_on_project_id_and_repo_id" ON "project_repository_statuses" ("project_id", "repository_id");
CREATE TABLE "worktree_settings_files" (
"project_id" INTEGER NOT NULL,
@@ -134,10 +145,12 @@ CREATE TABLE "worktree_settings_files" (
"path" VARCHAR NOT NULL,
"content" TEXT,
"kind" VARCHAR,
PRIMARY KEY(project_id, worktree_id, path),
FOREIGN KEY(project_id, worktree_id) REFERENCES worktrees (project_id, id) ON DELETE CASCADE
PRIMARY KEY (project_id, worktree_id, path),
FOREIGN KEY (project_id, worktree_id) REFERENCES worktrees (project_id, id) ON DELETE CASCADE
);
CREATE INDEX "index_worktree_settings_files_on_project_id" ON "worktree_settings_files" ("project_id");
CREATE INDEX "index_worktree_settings_files_on_project_id_and_worktree_id" ON "worktree_settings_files" ("project_id", "worktree_id");
CREATE TABLE "worktree_diagnostic_summaries" (
@@ -147,18 +160,21 @@ CREATE TABLE "worktree_diagnostic_summaries" (
"language_server_id" INTEGER NOT NULL,
"error_count" INTEGER NOT NULL,
"warning_count" INTEGER NOT NULL,
PRIMARY KEY(project_id, worktree_id, path),
FOREIGN KEY(project_id, worktree_id) REFERENCES worktrees (project_id, id) ON DELETE CASCADE
PRIMARY KEY (project_id, worktree_id, path),
FOREIGN KEY (project_id, worktree_id) REFERENCES worktrees (project_id, id) ON DELETE CASCADE
);
CREATE INDEX "index_worktree_diagnostic_summaries_on_project_id" ON "worktree_diagnostic_summaries" ("project_id");
CREATE INDEX "index_worktree_diagnostic_summaries_on_project_id_and_worktree_id" ON "worktree_diagnostic_summaries" ("project_id", "worktree_id");
CREATE TABLE "language_servers" (
"id" INTEGER NOT NULL,
"project_id" INTEGER NOT NULL REFERENCES projects (id) ON DELETE CASCADE,
"name" VARCHAR NOT NULL,
PRIMARY KEY(project_id, id)
PRIMARY KEY (project_id, id)
);
CREATE INDEX "index_language_servers_on_project_id" ON "language_servers" ("project_id");
CREATE TABLE "project_collaborators" (
@@ -170,11 +186,20 @@ CREATE TABLE "project_collaborators" (
"replica_id" INTEGER NOT NULL,
"is_host" BOOLEAN NOT NULL
);
CREATE INDEX "index_project_collaborators_on_project_id" ON "project_collaborators" ("project_id");
CREATE UNIQUE INDEX "index_project_collaborators_on_project_id_and_replica_id" ON "project_collaborators" ("project_id", "replica_id");
CREATE INDEX "index_project_collaborators_on_connection_server_id" ON "project_collaborators" ("connection_server_id");
CREATE INDEX "index_project_collaborators_on_connection_id" ON "project_collaborators" ("connection_id");
CREATE UNIQUE INDEX "index_project_collaborators_on_project_id_connection_id_and_server_id" ON "project_collaborators" ("project_id", "connection_id", "connection_server_id");
CREATE UNIQUE INDEX "index_project_collaborators_on_project_id_connection_id_and_server_id" ON "project_collaborators" (
"project_id",
"connection_id",
"connection_server_id"
);
CREATE TABLE "room_participants" (
"id" INTEGER PRIMARY KEY AUTOINCREMENT,
@@ -193,12 +218,21 @@ CREATE TABLE "room_participants" (
"role" TEXT,
"in_call" BOOLEAN NOT NULL DEFAULT FALSE
);
CREATE UNIQUE INDEX "index_room_participants_on_user_id" ON "room_participants" ("user_id");
CREATE INDEX "index_room_participants_on_room_id" ON "room_participants" ("room_id");
CREATE INDEX "index_room_participants_on_answering_connection_server_id" ON "room_participants" ("answering_connection_server_id");
CREATE INDEX "index_room_participants_on_calling_connection_server_id" ON "room_participants" ("calling_connection_server_id");
CREATE INDEX "index_room_participants_on_answering_connection_id" ON "room_participants" ("answering_connection_id");
CREATE UNIQUE INDEX "index_room_participants_on_answering_connection_id_and_answering_connection_server_id" ON "room_participants" ("answering_connection_id", "answering_connection_server_id");
CREATE UNIQUE INDEX "index_room_participants_on_answering_connection_id_and_answering_connection_server_id" ON "room_participants" (
"answering_connection_id",
"answering_connection_server_id"
);
CREATE TABLE "servers" (
"id" INTEGER PRIMARY KEY AUTOINCREMENT,
@@ -214,9 +248,15 @@ CREATE TABLE "followers" (
"follower_connection_server_id" INTEGER NOT NULL REFERENCES servers (id) ON DELETE CASCADE,
"follower_connection_id" INTEGER NOT NULL
);
CREATE UNIQUE INDEX
"index_followers_on_project_id_and_leader_connection_server_id_and_leader_connection_id_and_follower_connection_server_id_and_follower_connection_id"
ON "followers" ("project_id", "leader_connection_server_id", "leader_connection_id", "follower_connection_server_id", "follower_connection_id");
CREATE UNIQUE INDEX "index_followers_on_project_id_and_leader_connection_server_id_and_leader_connection_id_and_follower_connection_server_id_and_follower_connection_id" ON "followers" (
"project_id",
"leader_connection_server_id",
"leader_connection_id",
"follower_connection_server_id",
"follower_connection_id"
);
CREATE INDEX "index_followers_on_room_id" ON "followers" ("room_id");
CREATE TABLE "channels" (
@@ -237,6 +277,7 @@ CREATE TABLE IF NOT EXISTS "channel_chat_participants" (
"connection_id" INTEGER NOT NULL,
"connection_server_id" INTEGER NOT NULL REFERENCES servers (id) ON DELETE CASCADE
);
CREATE INDEX "index_channel_chat_participants_on_channel_id" ON "channel_chat_participants" ("channel_id");
CREATE TABLE IF NOT EXISTS "channel_messages" (
@@ -249,7 +290,9 @@ CREATE TABLE IF NOT EXISTS "channel_messages" (
"nonce" BLOB NOT NULL,
"reply_to_message_id" INTEGER DEFAULT NULL
);
CREATE INDEX "index_channel_messages_on_channel_id" ON "channel_messages" ("channel_id");
CREATE UNIQUE INDEX "index_channel_messages_on_sender_id_nonce" ON "channel_messages" ("sender_id", "nonce");
CREATE TABLE "channel_message_mentions" (
@@ -257,7 +300,7 @@ CREATE TABLE "channel_message_mentions" (
"start_offset" INTEGER NOT NULL,
"end_offset" INTEGER NOT NULL,
"user_id" INTEGER NOT NULL REFERENCES users (id) ON DELETE CASCADE,
PRIMARY KEY(message_id, start_offset)
PRIMARY KEY (message_id, start_offset)
);
CREATE TABLE "channel_members" (
@@ -288,7 +331,7 @@ CREATE TABLE "buffer_operations" (
"replica_id" INTEGER NOT NULL,
"lamport_timestamp" INTEGER NOT NULL,
"value" BLOB NOT NULL,
PRIMARY KEY(buffer_id, epoch, lamport_timestamp, replica_id)
PRIMARY KEY (buffer_id, epoch, lamport_timestamp, replica_id)
);
CREATE TABLE "buffer_snapshots" (
@@ -296,7 +339,7 @@ CREATE TABLE "buffer_snapshots" (
"epoch" INTEGER NOT NULL,
"text" TEXT NOT NULL,
"operation_serialization_version" INTEGER NOT NULL,
PRIMARY KEY(buffer_id, epoch)
PRIMARY KEY (buffer_id, epoch)
);
CREATE TABLE "channel_buffer_collaborators" (
@@ -310,11 +353,18 @@ CREATE TABLE "channel_buffer_collaborators" (
);
CREATE INDEX "index_channel_buffer_collaborators_on_channel_id" ON "channel_buffer_collaborators" ("channel_id");
CREATE UNIQUE INDEX "index_channel_buffer_collaborators_on_channel_id_and_replica_id" ON "channel_buffer_collaborators" ("channel_id", "replica_id");
CREATE INDEX "index_channel_buffer_collaborators_on_connection_server_id" ON "channel_buffer_collaborators" ("connection_server_id");
CREATE INDEX "index_channel_buffer_collaborators_on_connection_id" ON "channel_buffer_collaborators" ("connection_id");
CREATE UNIQUE INDEX "index_channel_buffer_collaborators_on_channel_id_connection_id_and_server_id" ON "channel_buffer_collaborators" ("channel_id", "connection_id", "connection_server_id");
CREATE UNIQUE INDEX "index_channel_buffer_collaborators_on_channel_id_and_replica_id" ON "channel_buffer_collaborators" ("channel_id", "replica_id");
CREATE INDEX "index_channel_buffer_collaborators_on_connection_server_id" ON "channel_buffer_collaborators" ("connection_server_id");
CREATE INDEX "index_channel_buffer_collaborators_on_connection_id" ON "channel_buffer_collaborators" ("connection_id");
CREATE UNIQUE INDEX "index_channel_buffer_collaborators_on_channel_id_connection_id_and_server_id" ON "channel_buffer_collaborators" (
"channel_id",
"connection_id",
"connection_server_id"
);
CREATE TABLE "feature_flags" (
"id" INTEGER PRIMARY KEY AUTOINCREMENT,
@@ -324,7 +374,6 @@ CREATE TABLE "feature_flags" (
CREATE INDEX "index_feature_flags" ON "feature_flags" ("id");
CREATE TABLE "user_features" (
"user_id" INTEGER NOT NULL REFERENCES users (id) ON DELETE CASCADE,
"feature_id" INTEGER NOT NULL REFERENCES feature_flags (id) ON DELETE CASCADE,
@@ -332,9 +381,10 @@ CREATE TABLE "user_features" (
);
CREATE UNIQUE INDEX "index_user_features_user_id_and_feature_id" ON "user_features" ("user_id", "feature_id");
CREATE INDEX "index_user_features_on_user_id" ON "user_features" ("user_id");
CREATE INDEX "index_user_features_on_feature_id" ON "user_features" ("feature_id");
CREATE INDEX "index_user_features_on_user_id" ON "user_features" ("user_id");
CREATE INDEX "index_user_features_on_feature_id" ON "user_features" ("feature_id");
CREATE TABLE "observed_buffer_edits" (
"user_id" INTEGER NOT NULL REFERENCES users (id) ON DELETE CASCADE,
@@ -374,13 +424,10 @@ CREATE TABLE "notifications" (
"response" BOOLEAN
);
CREATE INDEX
"index_notifications_on_recipient_id_is_read_kind_entity_id"
ON "notifications"
("recipient_id", "is_read", "kind", "entity_id");
CREATE INDEX "index_notifications_on_recipient_id_is_read_kind_entity_id" ON "notifications" ("recipient_id", "is_read", "kind", "entity_id");
CREATE TABLE contributors (
user_id INTEGER REFERENCES users(id),
user_id INTEGER REFERENCES users (id),
signed_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (user_id)
);
@@ -394,7 +441,7 @@ CREATE TABLE extensions (
);
CREATE TABLE extension_versions (
extension_id INTEGER REFERENCES extensions(id),
extension_id INTEGER REFERENCES extensions (id),
version TEXT NOT NULL,
published_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
authors TEXT NOT NULL,
@@ -416,6 +463,7 @@ CREATE TABLE extension_versions (
);
CREATE UNIQUE INDEX "index_extensions_external_id" ON "extensions" ("external_id");
CREATE INDEX "index_extensions_total_download_count" ON "extensions" ("total_download_count");
CREATE TABLE rate_buckets (
@@ -424,14 +472,15 @@ CREATE TABLE rate_buckets (
token_count INT NOT NULL,
last_refill TIMESTAMP WITHOUT TIME ZONE NOT NULL,
PRIMARY KEY (user_id, rate_limit_name),
FOREIGN KEY (user_id) REFERENCES users(id)
FOREIGN KEY (user_id) REFERENCES users (id)
);
CREATE INDEX idx_user_id_rate_limit ON rate_buckets (user_id, rate_limit_name);
CREATE TABLE IF NOT EXISTS billing_preferences (
id INTEGER PRIMARY KEY AUTOINCREMENT,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
user_id INTEGER NOT NULL REFERENCES users(id),
user_id INTEGER NOT NULL REFERENCES users (id),
max_monthly_llm_usage_spending_in_cents INTEGER NOT NULL
);
@@ -440,18 +489,19 @@ CREATE UNIQUE INDEX "uix_billing_preferences_on_user_id" ON billing_preferences
CREATE TABLE IF NOT EXISTS billing_customers (
id INTEGER PRIMARY KEY AUTOINCREMENT,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
user_id INTEGER NOT NULL REFERENCES users(id),
user_id INTEGER NOT NULL REFERENCES users (id),
has_overdue_invoices BOOLEAN NOT NULL DEFAULT FALSE,
stripe_customer_id TEXT NOT NULL
);
CREATE UNIQUE INDEX "uix_billing_customers_on_user_id" ON billing_customers (user_id);
CREATE UNIQUE INDEX "uix_billing_customers_on_stripe_customer_id" ON billing_customers (stripe_customer_id);
CREATE TABLE IF NOT EXISTS billing_subscriptions (
id INTEGER PRIMARY KEY AUTOINCREMENT,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
billing_customer_id INTEGER NOT NULL REFERENCES billing_customers(id),
billing_customer_id INTEGER NOT NULL REFERENCES billing_customers (id),
stripe_subscription_id TEXT NOT NULL,
stripe_subscription_status TEXT NOT NULL,
stripe_cancel_at TIMESTAMP,
@@ -459,6 +509,7 @@ CREATE TABLE IF NOT EXISTS billing_subscriptions (
);
CREATE INDEX "ix_billing_subscriptions_on_billing_customer_id" ON billing_subscriptions (billing_customer_id);
CREATE UNIQUE INDEX "uix_billing_subscriptions_on_stripe_subscription_id" ON billing_subscriptions (stripe_subscription_id);
CREATE TABLE IF NOT EXISTS processed_stripe_events (
@@ -479,4 +530,5 @@ CREATE TABLE IF NOT EXISTS "breakpoints" (
"path" TEXT NOT NULL,
"kind" VARCHAR NOT NULL
);
CREATE INDEX "index_breakpoints_on_project_id" ON "breakpoints" ("project_id");

View File

@@ -0,0 +1,32 @@
CREATE TABLE "project_repositories" (
"project_id" INTEGER NOT NULL,
"abs_path" VARCHAR,
"id" INT8 NOT NULL,
"legacy_worktree_id" INT8,
"entry_ids" VARCHAR,
"branch" VARCHAR,
"scan_id" INT8 NOT NULL,
"is_deleted" BOOL NOT NULL,
"current_merge_conflicts" VARCHAR,
"branch_summary" VARCHAR,
PRIMARY KEY (project_id, id)
);
CREATE INDEX "index_project_repositories_on_project_id" ON "project_repositories" ("project_id");
CREATE TABLE "project_repository_statuses" (
"project_id" INTEGER NOT NULL,
"repository_id" INT8 NOT NULL,
"repo_path" VARCHAR NOT NULL,
"status" INT8 NOT NULL,
"status_kind" INT4 NOT NULL,
"first_status" INT4 NULL,
"second_status" INT4 NULL,
"scan_id" INT8 NOT NULL,
"is_deleted" BOOL NOT NULL,
PRIMARY KEY (project_id, repository_id, repo_path)
);
CREATE INDEX "index_project_repos_statuses_on_project_id" ON "project_repository_statuses" ("project_id");
CREATE INDEX "index_project_repos_statuses_on_project_id_and_repo_id" ON "project_repository_statuses" ("project_id", "repository_id");

View File

@@ -9,6 +9,7 @@ use anyhow::anyhow;
use collections::{BTreeMap, BTreeSet, HashMap, HashSet};
use dashmap::DashMap;
use futures::StreamExt;
use project_repository_statuses::StatusKind;
use rand::{prelude::StdRng, Rng, SeedableRng};
use rpc::ExtensionProvides;
use rpc::{
@@ -36,7 +37,6 @@ use std::{
};
use time::PrimitiveDateTime;
use tokio::sync::{Mutex, OwnedMutexGuard};
use worktree_repository_statuses::StatusKind;
use worktree_settings_file::LocalSettingsKind;
#[cfg(test)]
@@ -658,6 +658,8 @@ pub struct RejoinedProject {
pub old_connection_id: ConnectionId,
pub collaborators: Vec<ProjectCollaborator>,
pub worktrees: Vec<RejoinedWorktree>,
pub updated_repositories: Vec<proto::UpdateRepository>,
pub removed_repositories: Vec<u64>,
pub language_servers: Vec<proto::LanguageServer>,
}
@@ -726,6 +728,7 @@ pub struct Project {
pub role: ChannelRole,
pub collaborators: Vec<ProjectCollaborator>,
pub worktrees: BTreeMap<u64, Worktree>,
pub repositories: Vec<proto::UpdateRepository>,
pub language_servers: Vec<proto::LanguageServer>,
}
@@ -760,7 +763,7 @@ pub struct Worktree {
pub root_name: String,
pub visible: bool,
pub entries: Vec<proto::Entry>,
pub repository_entries: BTreeMap<u64, proto::RepositoryEntry>,
pub legacy_repository_entries: BTreeMap<u64, proto::RepositoryEntry>,
pub diagnostic_summaries: Vec<proto::DiagnosticSummary>,
pub settings_files: Vec<WorktreeSettingsFile>,
pub scan_id: u64,
@@ -810,7 +813,7 @@ impl LocalSettingsKind {
}
fn db_status_to_proto(
entry: worktree_repository_statuses::Model,
entry: project_repository_statuses::Model,
) -> anyhow::Result<proto::StatusEntry> {
use proto::git_file_status::{Tracked, Unmerged, Variant};

View File

@@ -324,119 +324,135 @@ impl Database {
.await?;
}
if !update.updated_repositories.is_empty() {
worktree_repository::Entity::insert_many(update.updated_repositories.iter().map(
|repository| {
worktree_repository::ActiveModel {
project_id: ActiveValue::set(project_id),
worktree_id: ActiveValue::set(worktree_id),
work_directory_id: ActiveValue::set(
repository.work_directory_id as i64,
),
scan_id: ActiveValue::set(update.scan_id as i64),
branch: ActiveValue::set(repository.branch.clone()),
is_deleted: ActiveValue::set(false),
branch_summary: ActiveValue::Set(
repository
.branch_summary
.as_ref()
.map(|summary| serde_json::to_string(summary).unwrap()),
),
current_merge_conflicts: ActiveValue::Set(Some(
serde_json::to_string(&repository.current_merge_conflicts).unwrap(),
)),
}
},
))
.on_conflict(
OnConflict::columns([
worktree_repository::Column::ProjectId,
worktree_repository::Column::WorktreeId,
worktree_repository::Column::WorkDirectoryId,
])
.update_columns([
worktree_repository::Column::ScanId,
worktree_repository::Column::Branch,
worktree_repository::Column::BranchSummary,
worktree_repository::Column::CurrentMergeConflicts,
])
.to_owned(),
)
.exec(&*tx)
.await?;
// Backward-compatibility for old Zed clients.
//
// Remove this block when Zed 1.80 stable has been out for a week.
{
if !update.updated_repositories.is_empty() {
project_repository::Entity::insert_many(
update.updated_repositories.iter().map(|repository| {
project_repository::ActiveModel {
project_id: ActiveValue::set(project_id),
legacy_worktree_id: ActiveValue::set(Some(worktree_id)),
id: ActiveValue::set(repository.work_directory_id as i64),
scan_id: ActiveValue::set(update.scan_id as i64),
is_deleted: ActiveValue::set(false),
branch_summary: ActiveValue::Set(
repository
.branch_summary
.as_ref()
.map(|summary| serde_json::to_string(summary).unwrap()),
),
current_merge_conflicts: ActiveValue::Set(Some(
serde_json::to_string(&repository.current_merge_conflicts)
.unwrap(),
)),
let has_any_statuses = update
.updated_repositories
.iter()
.any(|repository| !repository.updated_statuses.is_empty());
if has_any_statuses {
worktree_repository_statuses::Entity::insert_many(
update.updated_repositories.iter().flat_map(
|repository: &proto::RepositoryEntry| {
repository.updated_statuses.iter().map(|status_entry| {
let (repo_path, status_kind, first_status, second_status) =
proto_status_to_db(status_entry.clone());
worktree_repository_statuses::ActiveModel {
project_id: ActiveValue::set(project_id),
worktree_id: ActiveValue::set(worktree_id),
work_directory_id: ActiveValue::set(
repository.work_directory_id as i64,
),
scan_id: ActiveValue::set(update.scan_id as i64),
is_deleted: ActiveValue::set(false),
repo_path: ActiveValue::set(repo_path),
status: ActiveValue::set(0),
status_kind: ActiveValue::set(status_kind),
first_status: ActiveValue::set(first_status),
second_status: ActiveValue::set(second_status),
}
})
},
),
// Old clients do not use abs path or entry ids.
abs_path: ActiveValue::set(String::new()),
entry_ids: ActiveValue::set("[]".into()),
}
}),
)
.on_conflict(
OnConflict::columns([
worktree_repository_statuses::Column::ProjectId,
worktree_repository_statuses::Column::WorktreeId,
worktree_repository_statuses::Column::WorkDirectoryId,
worktree_repository_statuses::Column::RepoPath,
project_repository::Column::ProjectId,
project_repository::Column::Id,
])
.update_columns([
worktree_repository_statuses::Column::ScanId,
worktree_repository_statuses::Column::StatusKind,
worktree_repository_statuses::Column::FirstStatus,
worktree_repository_statuses::Column::SecondStatus,
project_repository::Column::ScanId,
project_repository::Column::BranchSummary,
project_repository::Column::CurrentMergeConflicts,
])
.to_owned(),
)
.exec(&*tx)
.await?;
let has_any_statuses = update
.updated_repositories
.iter()
.any(|repository| !repository.updated_statuses.is_empty());
if has_any_statuses {
project_repository_statuses::Entity::insert_many(
update.updated_repositories.iter().flat_map(
|repository: &proto::RepositoryEntry| {
repository.updated_statuses.iter().map(|status_entry| {
let (repo_path, status_kind, first_status, second_status) =
proto_status_to_db(status_entry.clone());
project_repository_statuses::ActiveModel {
project_id: ActiveValue::set(project_id),
repository_id: ActiveValue::set(
repository.work_directory_id as i64,
),
scan_id: ActiveValue::set(update.scan_id as i64),
is_deleted: ActiveValue::set(false),
repo_path: ActiveValue::set(repo_path),
status: ActiveValue::set(0),
status_kind: ActiveValue::set(status_kind),
first_status: ActiveValue::set(first_status),
second_status: ActiveValue::set(second_status),
}
})
},
),
)
.on_conflict(
OnConflict::columns([
project_repository_statuses::Column::ProjectId,
project_repository_statuses::Column::RepositoryId,
project_repository_statuses::Column::RepoPath,
])
.update_columns([
project_repository_statuses::Column::ScanId,
project_repository_statuses::Column::StatusKind,
project_repository_statuses::Column::FirstStatus,
project_repository_statuses::Column::SecondStatus,
])
.to_owned(),
)
.exec(&*tx)
.await?;
}
for repo in &update.updated_repositories {
if !repo.removed_statuses.is_empty() {
project_repository_statuses::Entity::update_many()
.filter(
project_repository_statuses::Column::ProjectId
.eq(project_id)
.and(
project_repository_statuses::Column::RepositoryId
.eq(repo.work_directory_id),
)
.and(
project_repository_statuses::Column::RepoPath
.is_in(repo.removed_statuses.iter()),
),
)
.set(project_repository_statuses::ActiveModel {
is_deleted: ActiveValue::Set(true),
scan_id: ActiveValue::Set(update.scan_id as i64),
..Default::default()
})
.exec(&*tx)
.await?;
}
}
}
let has_any_removed_statuses = update
.updated_repositories
.iter()
.any(|repository| !repository.removed_statuses.is_empty());
if has_any_removed_statuses {
worktree_repository_statuses::Entity::update_many()
if !update.removed_repositories.is_empty() {
project_repository::Entity::update_many()
.filter(
worktree_repository_statuses::Column::ProjectId
project_repository::Column::ProjectId
.eq(project_id)
.and(
worktree_repository_statuses::Column::WorktreeId
.eq(worktree_id),
)
.and(
worktree_repository_statuses::Column::RepoPath.is_in(
update.updated_repositories.iter().flat_map(|repository| {
repository.removed_statuses.iter()
}),
),
),
.and(project_repository::Column::LegacyWorktreeId.eq(worktree_id))
.and(project_repository::Column::Id.is_in(
update.removed_repositories.iter().map(|id| *id as i64),
)),
)
.set(worktree_repository_statuses::ActiveModel {
.set(project_repository::ActiveModel {
is_deleted: ActiveValue::Set(true),
scan_id: ActiveValue::Set(update.scan_id as i64),
..Default::default()
@@ -446,18 +462,109 @@ impl Database {
}
}
if !update.removed_repositories.is_empty() {
worktree_repository::Entity::update_many()
let connection_ids = self.project_guest_connection_ids(project_id, &tx).await?;
Ok(connection_ids)
})
.await
}
pub async fn update_repository(
&self,
update: &proto::UpdateRepository,
_connection: ConnectionId,
) -> Result<TransactionGuard<Vec<ConnectionId>>> {
let project_id = ProjectId::from_proto(update.project_id);
let repository_id = update.id as i64;
self.project_transaction(project_id, |tx| async move {
project_repository::Entity::insert(project_repository::ActiveModel {
project_id: ActiveValue::set(project_id),
id: ActiveValue::set(repository_id),
legacy_worktree_id: ActiveValue::set(None),
abs_path: ActiveValue::set(update.abs_path.clone()),
entry_ids: ActiveValue::Set(serde_json::to_string(&update.entry_ids).unwrap()),
scan_id: ActiveValue::set(update.scan_id as i64),
is_deleted: ActiveValue::set(false),
branch_summary: ActiveValue::Set(
update
.branch_summary
.as_ref()
.map(|summary| serde_json::to_string(summary).unwrap()),
),
current_merge_conflicts: ActiveValue::Set(Some(
serde_json::to_string(&update.current_merge_conflicts).unwrap(),
)),
})
.on_conflict(
OnConflict::columns([
project_repository::Column::ProjectId,
project_repository::Column::Id,
])
.update_columns([
project_repository::Column::ScanId,
project_repository::Column::BranchSummary,
project_repository::Column::EntryIds,
project_repository::Column::AbsPath,
project_repository::Column::CurrentMergeConflicts,
])
.to_owned(),
)
.exec(&*tx)
.await?;
let has_any_statuses = !update.updated_statuses.is_empty();
if has_any_statuses {
project_repository_statuses::Entity::insert_many(
update.updated_statuses.iter().map(|status_entry| {
let (repo_path, status_kind, first_status, second_status) =
proto_status_to_db(status_entry.clone());
project_repository_statuses::ActiveModel {
project_id: ActiveValue::set(project_id),
repository_id: ActiveValue::set(repository_id),
scan_id: ActiveValue::set(update.scan_id as i64),
is_deleted: ActiveValue::set(false),
repo_path: ActiveValue::set(repo_path),
status: ActiveValue::set(0),
status_kind: ActiveValue::set(status_kind),
first_status: ActiveValue::set(first_status),
second_status: ActiveValue::set(second_status),
}
}),
)
.on_conflict(
OnConflict::columns([
project_repository_statuses::Column::ProjectId,
project_repository_statuses::Column::RepositoryId,
project_repository_statuses::Column::RepoPath,
])
.update_columns([
project_repository_statuses::Column::ScanId,
project_repository_statuses::Column::StatusKind,
project_repository_statuses::Column::FirstStatus,
project_repository_statuses::Column::SecondStatus,
])
.to_owned(),
)
.exec(&*tx)
.await?;
}
let has_any_removed_statuses = !update.removed_statuses.is_empty();
if has_any_removed_statuses {
project_repository_statuses::Entity::update_many()
.filter(
worktree_repository::Column::ProjectId
project_repository_statuses::Column::ProjectId
.eq(project_id)
.and(worktree_repository::Column::WorktreeId.eq(worktree_id))
.and(
worktree_repository::Column::WorkDirectoryId
.is_in(update.removed_repositories.iter().map(|id| *id as i64)),
project_repository_statuses::Column::RepositoryId.eq(repository_id),
)
.and(
project_repository_statuses::Column::RepoPath
.is_in(update.removed_statuses.iter()),
),
)
.set(worktree_repository::ActiveModel {
.set(project_repository_statuses::ActiveModel {
is_deleted: ActiveValue::Set(true),
scan_id: ActiveValue::Set(update.scan_id as i64),
..Default::default()
@@ -472,6 +579,34 @@ impl Database {
.await
}
pub async fn remove_repository(
&self,
remove: &proto::RemoveRepository,
_connection: ConnectionId,
) -> Result<TransactionGuard<Vec<ConnectionId>>> {
let project_id = ProjectId::from_proto(remove.project_id);
let repository_id = remove.id as i64;
self.project_transaction(project_id, |tx| async move {
project_repository::Entity::update_many()
.filter(
project_repository::Column::ProjectId
.eq(project_id)
.and(project_repository::Column::Id.eq(repository_id)),
)
.set(project_repository::ActiveModel {
is_deleted: ActiveValue::Set(true),
// scan_id: ActiveValue::Set(update.scan_id as i64),
..Default::default()
})
.exec(&*tx)
.await?;
let connection_ids = self.project_guest_connection_ids(project_id, &tx).await?;
Ok(connection_ids)
})
.await
}
/// Updates the diagnostic summary for the given connection.
pub async fn update_diagnostic_summary(
&self,
@@ -703,11 +838,11 @@ impl Database {
root_name: db_worktree.root_name,
visible: db_worktree.visible,
entries: Default::default(),
repository_entries: Default::default(),
diagnostic_summaries: Default::default(),
settings_files: Default::default(),
scan_id: db_worktree.scan_id as u64,
completed_scan_id: db_worktree.completed_scan_id as u64,
legacy_repository_entries: Default::default(),
},
)
})
@@ -750,65 +885,77 @@ impl Database {
}
// Populate repository entries.
let mut repositories = Vec::new();
{
let db_repository_entries = worktree_repository::Entity::find()
let db_repository_entries = project_repository::Entity::find()
.filter(
Condition::all()
.add(worktree_repository::Column::ProjectId.eq(project.id))
.add(worktree_repository::Column::IsDeleted.eq(false)),
.add(project_repository::Column::ProjectId.eq(project.id))
.add(project_repository::Column::IsDeleted.eq(false)),
)
.all(tx)
.await?;
for db_repository_entry in db_repository_entries {
if let Some(worktree) = worktrees.get_mut(&(db_repository_entry.worktree_id as u64))
{
let mut repository_statuses = worktree_repository_statuses::Entity::find()
.filter(
Condition::all()
.add(worktree_repository_statuses::Column::ProjectId.eq(project.id))
.add(
worktree_repository_statuses::Column::WorktreeId
.eq(worktree.id),
)
.add(
worktree_repository_statuses::Column::WorkDirectoryId
.eq(db_repository_entry.work_directory_id),
)
.add(worktree_repository_statuses::Column::IsDeleted.eq(false)),
)
.stream(tx)
.await?;
let mut updated_statuses = Vec::new();
while let Some(status_entry) = repository_statuses.next().await {
let status_entry: worktree_repository_statuses::Model = status_entry?;
updated_statuses.push(db_status_to_proto(status_entry)?);
let mut repository_statuses = project_repository_statuses::Entity::find()
.filter(
Condition::all()
.add(project_repository_statuses::Column::ProjectId.eq(project.id))
.add(
project_repository_statuses::Column::RepositoryId
.eq(db_repository_entry.id),
)
.add(project_repository_statuses::Column::IsDeleted.eq(false)),
)
.stream(tx)
.await?;
let mut updated_statuses = Vec::new();
while let Some(status_entry) = repository_statuses.next().await {
let status_entry = status_entry?;
updated_statuses.push(db_status_to_proto(status_entry)?);
}
let current_merge_conflicts = db_repository_entry
.current_merge_conflicts
.as_ref()
.map(|conflicts| serde_json::from_str(&conflicts))
.transpose()?
.unwrap_or_default();
let branch_summary = db_repository_entry
.branch_summary
.as_ref()
.map(|branch_summary| serde_json::from_str(&branch_summary))
.transpose()?
.unwrap_or_default();
let entry_ids = serde_json::from_str(&db_repository_entry.entry_ids)
.context("failed to deserialize repository's entry ids")?;
if let Some(worktree_id) = db_repository_entry.legacy_worktree_id {
if let Some(worktree) = worktrees.get_mut(&(worktree_id as u64)) {
worktree.legacy_repository_entries.insert(
db_repository_entry.id as u64,
proto::RepositoryEntry {
work_directory_id: db_repository_entry.id as u64,
updated_statuses,
removed_statuses: Vec::new(),
current_merge_conflicts,
branch_summary,
},
);
}
let current_merge_conflicts = db_repository_entry
.current_merge_conflicts
.as_ref()
.map(|conflicts| serde_json::from_str(&conflicts))
.transpose()?
.unwrap_or_default();
let branch_summary = db_repository_entry
.branch_summary
.as_ref()
.map(|branch_summary| serde_json::from_str(&branch_summary))
.transpose()?
.unwrap_or_default();
worktree.repository_entries.insert(
db_repository_entry.work_directory_id as u64,
proto::RepositoryEntry {
work_directory_id: db_repository_entry.work_directory_id as u64,
branch: db_repository_entry.branch,
updated_statuses,
removed_statuses: Vec::new(),
current_merge_conflicts,
branch_summary,
},
);
} else {
repositories.push(proto::UpdateRepository {
project_id: db_repository_entry.project_id.0 as u64,
id: db_repository_entry.id as u64,
abs_path: db_repository_entry.abs_path,
entry_ids,
updated_statuses,
removed_statuses: Vec::new(),
current_merge_conflicts,
branch_summary,
scan_id: db_repository_entry.scan_id as u64,
});
}
}
}
@@ -871,6 +1018,7 @@ impl Database {
})
.collect(),
worktrees,
repositories,
language_servers: language_servers
.into_iter()
.map(|language_server| proto::LanguageServer {

View File

@@ -1,3 +1,5 @@
use anyhow::Context as _;
use super::*;
impl Database {
@@ -606,6 +608,11 @@ impl Database {
let mut worktrees = Vec::new();
let db_worktrees = project.find_related(worktree::Entity).all(tx).await?;
let db_repos = project
.find_related(project_repository::Entity)
.all(tx)
.await?;
for db_worktree in db_worktrees {
let mut worktree = RejoinedWorktree {
id: db_worktree.id as u64,
@@ -673,96 +680,112 @@ impl Database {
}
}
// Repository Entries
{
let repository_entry_filter = if let Some(rejoined_worktree) = rejoined_worktree {
worktree_repository::Column::ScanId.gt(rejoined_worktree.scan_id)
worktrees.push(worktree);
}
let mut removed_repositories = Vec::new();
let mut updated_repositories = Vec::new();
for db_repo in db_repos {
let rejoined_repository = rejoined_project
.repositories
.iter()
.find(|repo| repo.id == db_repo.id as u64);
let repository_filter = if let Some(rejoined_repository) = rejoined_repository {
project_repository::Column::ScanId.gt(rejoined_repository.scan_id)
} else {
project_repository::Column::IsDeleted.eq(false)
};
let db_repositories = project_repository::Entity::find()
.filter(
Condition::all()
.add(project_repository::Column::ProjectId.eq(project.id))
.add(repository_filter),
)
.all(tx)
.await?;
for db_repository in db_repositories.into_iter() {
if db_repository.is_deleted {
removed_repositories.push(db_repository.id as u64);
} else {
worktree_repository::Column::IsDeleted.eq(false)
};
let db_repositories = worktree_repository::Entity::find()
.filter(
Condition::all()
.add(worktree_repository::Column::ProjectId.eq(project.id))
.add(worktree_repository::Column::WorktreeId.eq(worktree.id))
.add(repository_entry_filter),
)
.all(tx)
.await?;
for db_repository in db_repositories.into_iter() {
if db_repository.is_deleted {
worktree
.removed_repositories
.push(db_repository.work_directory_id as u64);
let status_entry_filter = if let Some(rejoined_repository) = rejoined_repository
{
project_repository_statuses::Column::ScanId.gt(rejoined_repository.scan_id)
} else {
let status_entry_filter = if let Some(rejoined_worktree) = rejoined_worktree
{
worktree_repository_statuses::Column::ScanId
.gt(rejoined_worktree.scan_id)
project_repository_statuses::Column::IsDeleted.eq(false)
};
let mut db_statuses = project_repository_statuses::Entity::find()
.filter(
Condition::all()
.add(project_repository_statuses::Column::ProjectId.eq(project.id))
.add(
project_repository_statuses::Column::RepositoryId
.eq(db_repository.id),
)
.add(status_entry_filter),
)
.stream(tx)
.await?;
let mut removed_statuses = Vec::new();
let mut updated_statuses = Vec::new();
while let Some(db_status) = db_statuses.next().await {
let db_status: project_repository_statuses::Model = db_status?;
if db_status.is_deleted {
removed_statuses.push(db_status.repo_path);
} else {
worktree_repository_statuses::Column::IsDeleted.eq(false)
};
let mut db_statuses = worktree_repository_statuses::Entity::find()
.filter(
Condition::all()
.add(
worktree_repository_statuses::Column::ProjectId
.eq(project.id),
)
.add(
worktree_repository_statuses::Column::WorktreeId
.eq(worktree.id),
)
.add(
worktree_repository_statuses::Column::WorkDirectoryId
.eq(db_repository.work_directory_id),
)
.add(status_entry_filter),
)
.stream(tx)
.await?;
let mut removed_statuses = Vec::new();
let mut updated_statuses = Vec::new();
while let Some(db_status) = db_statuses.next().await {
let db_status: worktree_repository_statuses::Model = db_status?;
if db_status.is_deleted {
removed_statuses.push(db_status.repo_path);
} else {
updated_statuses.push(db_status_to_proto(db_status)?);
}
updated_statuses.push(db_status_to_proto(db_status)?);
}
}
let current_merge_conflicts = db_repository
.current_merge_conflicts
.as_ref()
.map(|conflicts| serde_json::from_str(&conflicts))
.transpose()?
.unwrap_or_default();
let current_merge_conflicts = db_repository
.current_merge_conflicts
.as_ref()
.map(|conflicts| serde_json::from_str(&conflicts))
.transpose()?
.unwrap_or_default();
let branch_summary = db_repository
.branch_summary
.as_ref()
.map(|branch_summary| serde_json::from_str(&branch_summary))
.transpose()?
.unwrap_or_default();
let branch_summary = db_repository
.branch_summary
.as_ref()
.map(|branch_summary| serde_json::from_str(&branch_summary))
.transpose()?
.unwrap_or_default();
worktree.updated_repositories.push(proto::RepositoryEntry {
work_directory_id: db_repository.work_directory_id as u64,
branch: db_repository.branch,
let entry_ids = serde_json::from_str(&db_repository.entry_ids)
.context("failed to deserialize repository's entry ids")?;
if let Some(legacy_worktree_id) = db_repository.legacy_worktree_id {
if let Some(worktree) = worktrees
.iter_mut()
.find(|worktree| worktree.id as i64 == legacy_worktree_id)
{
worktree.updated_repositories.push(proto::RepositoryEntry {
work_directory_id: db_repository.id as u64,
updated_statuses,
removed_statuses,
current_merge_conflicts,
branch_summary,
});
}
} else {
updated_repositories.push(proto::UpdateRepository {
entry_ids,
updated_statuses,
removed_statuses,
current_merge_conflicts,
branch_summary,
project_id: project_id.to_proto(),
id: db_repository.id as u64,
abs_path: db_repository.abs_path,
scan_id: db_repository.scan_id as u64,
});
}
}
}
worktrees.push(worktree);
}
let language_servers = project
@@ -832,6 +855,8 @@ impl Database {
id: project_id,
old_connection_id,
collaborators,
updated_repositories,
removed_repositories,
worktrees,
language_servers,
}))

View File

@@ -26,6 +26,8 @@ pub mod observed_channel_messages;
pub mod processed_stripe_event;
pub mod project;
pub mod project_collaborator;
pub mod project_repository;
pub mod project_repository_statuses;
pub mod rate_buckets;
pub mod room;
pub mod room_participant;
@@ -36,6 +38,4 @@ pub mod user_feature;
pub mod worktree;
pub mod worktree_diagnostic_summary;
pub mod worktree_entry;
pub mod worktree_repository;
pub mod worktree_repository_statuses;
pub mod worktree_settings_file;

View File

@@ -45,6 +45,8 @@ pub enum Relation {
Room,
#[sea_orm(has_many = "super::worktree::Entity")]
Worktrees,
#[sea_orm(has_many = "super::project_repository::Entity")]
Repositories,
#[sea_orm(has_many = "super::project_collaborator::Entity")]
Collaborators,
#[sea_orm(has_many = "super::language_server::Entity")]
@@ -69,6 +71,12 @@ impl Related<super::worktree::Entity> for Entity {
}
}
impl Related<super::project_repository::Entity> for Entity {
fn to() -> RelationDef {
Relation::Repositories.def()
}
}
impl Related<super::project_collaborator::Entity> for Entity {
fn to() -> RelationDef {
Relation::Collaborators.def()

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