Compare commits

..

75 Commits

Author SHA1 Message Date
Antonio Scandurra
5a69335491 Merge remote-tracking branch 'origin/main' into review-only-assistant-changes 2025-03-27 10:13:43 +01: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
Antonio Scandurra
26e461b4d4 Merge branch 'async-status' into review-only-assistant-changes
# Conflicts:
#	crates/fs/src/fake_git_repo.rs
#	crates/git_ui/src/project_diff.rs
#	crates/project/src/git_store.rs
#	crates/worktree/src/worktree.rs
2025-03-27 09:07:51 +01:00
Antonio Scandurra
93e3780ffc Use with_state_async 2025-03-27 08:55:41 +01:00
Antonio Scandurra
e45151b893 Merge remote-tracking branch 'origin/main' into async-status
# Conflicts:
#	crates/fs/src/fake_git_repo.rs
#	crates/worktree/src/worktree.rs
#	crates/worktree/src/worktree_tests.rs
2025-03-27 08:50:22 +01: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
Antonio Scandurra
7d6cc2e028 Implement GitStore::status
Co-authored-by: Nathan Sobo <nathan@zed.dev>
2025-03-26 20:16:05 +01:00
Antonio Scandurra
c06d004fce Allow passing a custom index when loading index text
We also stopped using libgit2 for retrieving the index text.

Co-authored-by: Nathan Sobo <nathan@zed.dev>
2025-03-26 20:02:55 +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
Antonio Scandurra
137485819a Support custom indices in GitRepository::status
Co-authored-by: Nathan Sobo <nathan@zed.dev>
2025-03-26 19:24:33 +01:00
Antonio Scandurra
fd17b4486c Merge branch 'async-status' into review-only-assistant-changes
Co-authored-by: Nathan Sobo <nathan@zed.dev>
2025-03-26 18:58:19 +01:00
Antonio Scandurra
af0011b9bd Remove stale comment
Co-authored-by: Nathan Sobo <nathan@zed.dev>
2025-03-26 18:44:35 +01:00
Antonio Scandurra
65d687458c Use executor.block instead of smol::block_on
Co-authored-by: Nathan Sobo <nathan@zed.dev>
2025-03-26 18:41:14 +01:00
Antonio Scandurra
b0568d24ed Fix warnings 2025-03-26 18:22:49 +01:00
Antonio Scandurra
53227bb847 Make GitRepository::status async and remove cx parameter 2025-03-26 18:22:37 +01: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
Antonio Scandurra
cc76992a55 WIP
Co-authored-by: Nathan Sobo <nathan@zed.dev>
2025-03-26 17:27:05 +01:00
Antonio Scandurra
365817370a WIP
Co-authored-by: Nathan Sobo <nathan@zed.dev>
2025-03-26 17:16:06 +01: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
Antonio Scandurra
52aaa6c561 Use git status to determine what changed in virtual branch 2025-03-26 15:35:44 +01:00
Antonio Scandurra
36ba2ac238 Introduce a new ThreadDiff primitive 2025-03-26 14:39:43 +01: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
Antonio Scandurra
807b261403 WIP 2025-03-26 11:24:54 +01:00
Antonio Scandurra
f83a11741c Rename ReviewBranch to VirtualBranch 2025-03-26 11:20:04 +01:00
Antonio Scandurra
2b84d34591 Checkpoint 2025-03-26 11:04:37 +01:00
Antonio Scandurra
9984694ca7 Merge remote-tracking branch 'origin/main' into review-only-assistant-changes 2025-03-26 10:13:24 +01:00
Antonio Scandurra
a9744d6c00 WIP 2025-03-26 10:05:28 +01:00
Antonio Scandurra
4d13db41b3 WIP 2025-03-24 19:11:49 +01:00
Antonio Scandurra
05093d988b Merge remote-tracking branch 'origin/main' into review-only-assistant-changes 2025-03-24 17:49:52 +01:00
Antonio Scandurra
933048e867 WIP 2025-03-24 17:49:48 +01:00
Antonio Scandurra
99ba285738 WIP 2025-03-23 11:27:07 +01:00
126 changed files with 5797 additions and 3054 deletions

147
Cargo.lock generated
View File

@@ -453,6 +453,7 @@ dependencies = [
"assistant_slash_command",
"assistant_tool",
"async-watch",
"buffer_diff",
"chrono",
"client",
"clock",
@@ -799,9 +800,9 @@ dependencies = [
[[package]]
name = "async-compression"
version = "0.4.21"
version = "0.4.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c0cf008e5e1a9e9e22a7d3c9a4992e21a350290069e36d8fb72304ed17e8f2d2"
checksum = "59a194f9d963d8099596278594b3107448656ba73831c9d8c783e613ce86da64"
dependencies = [
"deflate64",
"flate2",
@@ -2360,7 +2361,7 @@ dependencies = [
"cap-primitives",
"cap-std",
"io-lifetimes",
"windows-sys 0.59.0",
"windows-sys 0.52.0",
]
[[package]]
@@ -2388,7 +2389,7 @@ dependencies = [
"ipnet",
"maybe-owned",
"rustix",
"windows-sys 0.59.0",
"windows-sys 0.52.0",
"winx",
]
@@ -2668,9 +2669,9 @@ dependencies = [
[[package]]
name = "clap"
version = "4.5.32"
version = "4.5.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6088f3ae8c3608d19260cd7445411865a485688711b78b5be70d78cd96136f83"
checksum = "e958897981290da2a852763fe9cdb89cd36977a5d729023127095fa94d95e2ff"
dependencies = [
"clap_builder",
"clap_derive",
@@ -2678,9 +2679,9 @@ dependencies = [
[[package]]
name = "clap_builder"
version = "4.5.32"
version = "4.5.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22a7ef7f676155edfb82daa97f99441f3ebf4a58d5e32f295a56259f1b6facc8"
checksum = "83b0f35019843db2160b5bb19ae09b4e6411ac33fc6a712003c33e03090e2489"
dependencies = [
"anstream",
"anstyle",
@@ -4589,7 +4590,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d"
dependencies = [
"libc",
"windows-sys 0.59.0",
"windows-sys 0.52.0",
]
[[package]]
@@ -4749,6 +4750,7 @@ dependencies = [
"env_logger 0.11.7",
"extension",
"fs",
"gpui",
"language",
"log",
"reqwest_client",
@@ -5228,7 +5230,7 @@ dependencies = [
"ignore",
"libc",
"log",
"notify 6.1.1",
"notify 8.0.0 (git+https://github.com/zed-industries/notify.git?rev=bbb9ea5ae52b253e095737847e367c30653a2e96)",
"objc",
"parking_lot",
"paths",
@@ -5252,7 +5254,7 @@ checksum = "5e2e6123af26f0f2c51cc66869137080199406754903cc926a7690401ce09cb4"
dependencies = [
"io-lifetimes",
"rustix",
"windows-sys 0.59.0",
"windows-sys 0.52.0",
]
[[package]]
@@ -6833,17 +6835,6 @@ dependencies = [
"zeta",
]
[[package]]
name = "inotify"
version = "0.9.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8069d3ec154eb856955c1c0fbffefbf5f3c40a104ec912d4797314c1801abff"
dependencies = [
"bitflags 1.3.2",
"inotify-sys",
"libc",
]
[[package]]
name = "inotify"
version = "0.11.0"
@@ -6920,7 +6911,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2285ddfe3054097ef4b2fe909ef8c3bcd1ea52a8f0d274416caebeef39f04a65"
dependencies = [
"io-lifetimes",
"windows-sys 0.59.0",
"windows-sys 0.52.0",
]
[[package]]
@@ -6949,7 +6940,7 @@ dependencies = [
"fnv",
"lazy_static",
"libc",
"mio 1.0.3",
"mio",
"rand 0.8.5",
"serde",
"tempfile",
@@ -7566,7 +7557,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc2f4eb4bc735547cfed7c0a4922cbd04a4655978c09b54f1f7b228750664c34"
dependencies = [
"cfg-if",
"windows-targets 0.52.6",
"windows-targets 0.48.5",
]
[[package]]
@@ -8151,7 +8142,7 @@ dependencies = [
"ignore",
"log",
"memchr",
"notify 8.0.0",
"notify 8.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
"notify-debouncer-mini",
"once_cell",
"opener",
@@ -8300,18 +8291,6 @@ version = "0.5.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e53debba6bda7a793e5f99b8dacf19e626084f525f7829104ba9898f367d85ff"
[[package]]
name = "mio"
version = "0.8.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c"
dependencies = [
"libc",
"log",
"wasi 0.11.0+wasi-snapshot-preview1",
"windows-sys 0.48.0",
]
[[package]]
name = "mio"
version = "1.0.3"
@@ -8589,25 +8568,6 @@ dependencies = [
"workspace",
]
[[package]]
name = "notify"
version = "6.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6205bd8bb1e454ad2e27422015fb5e4f2bcc7e08fa8f27058670d208324a4d2d"
dependencies = [
"bitflags 2.8.0",
"crossbeam-channel",
"filetime",
"fsevent-sys 4.1.0",
"inotify 0.9.6",
"kqueue",
"libc",
"log",
"mio 0.8.11",
"walkdir",
"windows-sys 0.48.0",
]
[[package]]
name = "notify"
version = "8.0.0"
@@ -8617,12 +8577,30 @@ dependencies = [
"bitflags 2.8.0",
"filetime",
"fsevent-sys 4.1.0",
"inotify 0.11.0",
"inotify",
"kqueue",
"libc",
"log",
"mio 1.0.3",
"notify-types",
"mio",
"notify-types 2.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
"walkdir",
"windows-sys 0.59.0",
]
[[package]]
name = "notify"
version = "8.0.0"
source = "git+https://github.com/zed-industries/notify.git?rev=bbb9ea5ae52b253e095737847e367c30653a2e96#bbb9ea5ae52b253e095737847e367c30653a2e96"
dependencies = [
"bitflags 2.8.0",
"filetime",
"fsevent-sys 4.1.0",
"inotify",
"kqueue",
"libc",
"log",
"mio",
"notify-types 2.0.0 (git+https://github.com/zed-industries/notify.git?rev=bbb9ea5ae52b253e095737847e367c30653a2e96)",
"walkdir",
"windows-sys 0.59.0",
]
@@ -8634,8 +8612,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a689eb4262184d9a1727f9087cd03883ea716682ab03ed24efec57d7716dccb8"
dependencies = [
"log",
"notify 8.0.0",
"notify-types",
"notify 8.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
"notify-types 2.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
"tempfile",
]
@@ -8645,6 +8623,11 @@ version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e0826a989adedc2a244799e823aece04662b66609d96af8dff7ac6df9a8925d"
[[package]]
name = "notify-types"
version = "2.0.0"
source = "git+https://github.com/zed-industries/notify.git?rev=bbb9ea5ae52b253e095737847e367c30653a2e96#bbb9ea5ae52b253e095737847e367c30653a2e96"
[[package]]
name = "ntapi"
version = "0.4.1"
@@ -10245,9 +10228,9 @@ checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2"
[[package]]
name = "plist"
version = "1.7.0"
version = "1.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42cf17e9a1800f5f396bc67d193dc9411b59012a5876445ef450d449881e1016"
checksum = "eac26e981c03a6e53e0aee43c113e3202f5581d5360dae7bd2c70e800dd0451d"
dependencies = [
"base64 0.22.1",
"indexmap",
@@ -10729,7 +10712,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22505a5c94da8e3b7c2996394d1c933236c4d743e81a410bcca4e6989fc066a4"
dependencies = [
"bytes 1.10.1",
"heck 0.5.0",
"heck 0.4.1",
"itertools 0.12.1",
"log",
"multimap 0.10.0",
@@ -10957,7 +10940,7 @@ dependencies = [
"once_cell",
"socket2",
"tracing",
"windows-sys 0.59.0",
"windows-sys 0.52.0",
]
[[package]]
@@ -11882,7 +11865,7 @@ dependencies = [
"libc",
"linux-raw-sys",
"once_cell",
"windows-sys 0.59.0",
"windows-sys 0.52.0",
]
[[package]]
@@ -11993,7 +11976,7 @@ dependencies = [
"security-framework 3.0.1",
"security-framework-sys",
"webpki-root-certs",
"windows-sys 0.59.0",
"windows-sys 0.52.0",
]
[[package]]
@@ -12415,18 +12398,18 @@ dependencies = [
[[package]]
name = "serde"
version = "1.0.218"
version = "1.0.219"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e8dfc9d19bdbf6d17e22319da49161d5d0108e4188e8b680aef6299eed22df60"
checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.218"
version = "1.0.219"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f09503e191f4e797cb8aac08e9a4a4695c5edf6a2e70e376d961ddd5c969f82b"
checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00"
dependencies = [
"proc-macro2",
"quote",
@@ -13657,7 +13640,7 @@ dependencies = [
"fd-lock",
"io-lifetimes",
"rustix",
"windows-sys 0.59.0",
"windows-sys 0.52.0",
"winx",
]
@@ -13801,7 +13784,7 @@ dependencies = [
"getrandom 0.3.1",
"once_cell",
"rustix",
"windows-sys 0.59.0",
"windows-sys 0.52.0",
]
[[package]]
@@ -14075,9 +14058,9 @@ dependencies = [
[[package]]
name = "time"
version = "0.3.40"
version = "0.3.41"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9d9c75b47bdff86fa3334a3db91356b8d7d86a9b839dab7d0bdc5c3d3a077618"
checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40"
dependencies = [
"deranged",
"itoa",
@@ -14098,9 +14081,9 @@ checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c"
[[package]]
name = "time-macros"
version = "0.2.21"
version = "0.2.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "29aa485584182073ed57fd5004aa09c371f021325014694e432313345865fd04"
checksum = "3526739392ec93fd8b359c8e98514cb3e8e021beb4e5f597b00a0221f8ed8a49"
dependencies = [
"num-conv",
"time-core",
@@ -14241,7 +14224,7 @@ dependencies = [
"backtrace",
"bytes 1.10.1",
"libc",
"mio 1.0.3",
"mio",
"parking_lot",
"pin-project-lite",
"signal-hook-registry",
@@ -16190,7 +16173,7 @@ version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
dependencies = [
"windows-sys 0.59.0",
"windows-sys 0.48.0",
]
[[package]]
@@ -16750,7 +16733,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f3fd376f71958b862e7afb20cfe5a22830e1963462f3a17f49d82a6c1d1f42d"
dependencies = [
"bitflags 2.8.0",
"windows-sys 0.59.0",
"windows-sys 0.52.0",
]
[[package]]
@@ -17272,7 +17255,7 @@ dependencies = [
[[package]]
name = "zed"
version = "0.180.0"
version = "0.181.0"
dependencies = [
"activity_indicator",
"anyhow",

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

@@ -754,8 +754,11 @@
"escape": "git_panel::ToggleFocus",
"ctrl-enter": "git::Commit",
"alt-enter": "menu::SecondaryConfirm",
"shift-delete": "git::RestoreFile",
"ctrl-delete": "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

@@ -803,7 +803,10 @@
"shift-tab": "git_panel::FocusEditor",
"escape": "git_panel::ToggleFocus",
"cmd-enter": "git::Commit",
"cmd-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,6 +8,8 @@ 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.

View File

@@ -429,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'.
@@ -622,6 +624,7 @@
// The model to use.
"model": "claude-3-5-sonnet-latest"
},
"default_profile": "code-writer",
"profiles": {
"read-only": {
"name": "Read-only",

View File

@@ -3712,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;
@@ -4091,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

@@ -25,6 +25,7 @@ assistant_settings.workspace = true
assistant_slash_command.workspace = true
assistant_tool.workspace = true
async-watch.workspace = true
buffer_diff.workspace = true
chrono.workspace = true
client.workspace = true
clock.workspace = true
@@ -85,6 +86,7 @@ workspace.workspace = true
zed_actions.workspace = true
[dev-dependencies]
buffer_diff = { workspace = true, features = ["test-support"] }
editor = { workspace = true, features = ["test-support"] }
gpui = { workspace = true, "features" = ["test-support"] }
indoc.workspace = true

View File

@@ -5,16 +5,16 @@ use crate::thread::{
use crate::thread_store::ThreadStore;
use crate::tool_use::{PendingToolUseStatus, ToolUse, ToolUseStatus};
use crate::ui::{ContextPill, ToolReadyPopUp, ToolReadyPopupEvent};
use crate::AssistantPanel;
use assistant_settings::AssistantSettings;
use collections::HashMap;
use editor::{Editor, MultiBuffer};
use gpui::{
linear_color_stop, linear_gradient, list, percentage, pulsating_between, AbsoluteLength,
Animation, AnimationExt, AnyElement, App, ClickEvent, DefiniteLength, EdgesRefinement, Empty,
Entity, Focusable, Length, ListAlignment, ListOffset, ListState, ScrollHandle, StyleRefinement,
Subscription, Task, TextStyleRefinement, Transformation, UnderlineStyle, WeakEntity,
WindowHandle,
Entity, Focusable, Hsla, Length, ListAlignment, ListOffset, ListState, MouseButton,
ScrollHandle, Stateful, StyleRefinement, Subscription, Task, TextStyleRefinement,
Transformation, UnderlineStyle, WeakEntity, WindowHandle,
};
use language::{Buffer, LanguageRegistry};
use language_model::{LanguageModelRegistry, LanguageModelToolUseId, Role};
@@ -23,7 +23,7 @@ use settings::Settings as _;
use std::sync::Arc;
use std::time::Duration;
use theme::ThemeSettings;
use ui::{prelude::*, Disclosure, IconButton, KeyBinding, Tooltip};
use ui::{prelude::*, Disclosure, IconButton, KeyBinding, Scrollbar, ScrollbarState, Tooltip};
use util::ResultExt as _;
use workspace::{OpenOptions, Workspace};
@@ -38,6 +38,7 @@ pub struct ActiveThread {
save_thread_task: Option<Task<()>>,
messages: Vec<MessageId>,
list_state: ListState,
scrollbar_state: ScrollbarState,
rendered_messages_by_id: HashMap<MessageId, RenderedMessage>,
rendered_tool_use_labels: HashMap<LanguageModelToolUseId, Entity<Markdown>>,
editing_message: Option<(MessageId, EditMessageState)>,
@@ -226,6 +227,14 @@ impl ActiveThread {
cx.subscribe_in(&thread, window, Self::handle_thread_event),
];
let list_state = ListState::new(0, ListAlignment::Bottom, px(2048.), {
let this = cx.entity().downgrade();
move |ix, window: &mut Window, cx: &mut App| {
this.update(cx, |this, cx| this.render_message(ix, window, cx))
.unwrap()
}
});
let mut this = Self {
language_registry,
thread_store,
@@ -238,13 +247,8 @@ impl ActiveThread {
rendered_tool_use_labels: HashMap::default(),
expanded_tool_uses: HashMap::default(),
expanded_thinking_segments: HashMap::default(),
list_state: ListState::new(0, ListAlignment::Bottom, px(1024.), {
let this = cx.entity().downgrade();
move |ix, window: &mut Window, cx: &mut App| {
this.update(cx, |this, cx| this.render_message(ix, window, cx))
.unwrap()
}
}),
list_state: list_state.clone(),
scrollbar_state: ScrollbarState::new(list_state),
editing_message: None,
last_error: None,
pop_ups: Vec::new(),
@@ -376,11 +380,23 @@ impl ActiveThread {
}
ThreadEvent::DoneStreaming => {
if !self.thread().read(cx).is_generating() {
self.show_notification("Your changes have been applied.", window, cx);
self.show_notification(
"Your changes have been applied.",
IconName::Check,
Color::Success,
window,
cx,
);
}
}
ThreadEvent::ToolConfirmationNeeded => {
self.show_notification("There's a tool confirmation needed.", window, cx);
self.show_notification(
"There's a tool confirmation needed.",
IconName::Info,
Color::Muted,
window,
cx,
);
}
ThreadEvent::StreamedAssistantText(message_id, text) => {
if let Some(rendered_message) = self.rendered_messages_by_id.get_mut(&message_id) {
@@ -505,12 +521,18 @@ impl ActiveThread {
}
}
ThreadEvent::CheckpointChanged => cx.notify(),
ThreadEvent::DiffChanged => {
// todo!("update list of changed files")
cx.notify();
}
}
}
fn show_notification(
&mut self,
caption: impl Into<SharedString>,
icon: IconName,
icon_color: Color,
window: &mut Window,
cx: &mut Context<'_, ActiveThread>,
) {
@@ -525,7 +547,7 @@ impl ActiveThread {
if let Some(screen_window) = cx
.open_window(options, |_, cx| {
cx.new(|_| ToolReadyPopUp::new(caption.clone()))
cx.new(|_| ToolReadyPopUp::new(caption.clone(), icon, icon_color))
})
.log_err()
{
@@ -536,11 +558,22 @@ impl ActiveThread {
let handle = window.window_handle();
cx.activate(true); // Switch back to the Zed application
let workspace_handle = this.workspace.clone();
// If there are multiple Zed windows, activate the correct one.
cx.defer(move |cx| {
handle
.update(cx, |_view, window, _cx| {
window.activate_window();
if let Some(workspace) = workspace_handle.upgrade()
{
workspace.update(_cx, |workspace, cx| {
workspace.focus_panel::<AssistantPanel>(
window, cx,
);
});
}
})
.log_err();
});
@@ -1145,6 +1178,17 @@ impl ActiveThread {
)
}
fn tool_card_border_color(&self, cx: &Context<Self>) -> Hsla {
cx.theme().colors().border.opacity(0.5)
}
fn tool_card_header_bg(&self, cx: &Context<Self>) -> Hsla {
cx.theme()
.colors()
.element_background
.blend(cx.theme().colors().editor_foreground.opacity(0.025))
}
fn render_message_thinking_segment(
&self,
message_id: MessageId,
@@ -1160,26 +1204,25 @@ impl ActiveThread {
.copied()
.unwrap_or_default();
let lighter_border = cx.theme().colors().border.opacity(0.5);
let editor_bg = cx.theme().colors().editor_background;
div().py_2().child(
v_flex()
.rounded_lg()
.border_1()
.border_color(lighter_border)
.border_color(self.tool_card_border_color(cx))
.child(
h_flex()
.group("disclosure-header")
.justify_between()
.py_1()
.px_2()
.bg(cx.theme().colors().editor_foreground.opacity(0.025))
.bg(self.tool_card_header_bg(cx))
.map(|this| {
if pending || is_open {
this.rounded_t_md()
.border_b_1()
.border_color(lighter_border)
.border_color(self.tool_card_border_color(cx))
} else {
this.rounded_md()
}
@@ -1314,21 +1357,21 @@ impl ActiveThread {
.copied()
.unwrap_or_default();
let lighter_border = cx.theme().colors().border.opacity(0.5);
div().py_2().child(
v_flex()
.rounded_lg()
.border_1()
.border_color(lighter_border)
.border_color(self.tool_card_border_color(cx))
.overflow_hidden()
.child(
h_flex()
.group("disclosure-header")
.relative()
.gap_1p5()
.justify_between()
.py_1()
.px_2()
.bg(cx.theme().colors().editor_foreground.opacity(0.025))
.bg(self.tool_card_header_bg(cx))
.map(|element| {
if is_open {
element.border_b_1().rounded_t_md()
@@ -1336,25 +1379,22 @@ impl ActiveThread {
element.rounded_md()
}
})
.border_color(lighter_border)
.border_color(self.tool_card_border_color(cx))
.child(
h_flex()
.id("tool-label-container")
.relative()
.gap_1p5()
.max_w_full()
.overflow_x_scroll()
.child(
Icon::new(tool_use.icon)
.size(IconSize::XSmall)
.color(Color::Muted),
)
.child(
div()
.text_ui_sm(cx)
.children(
self.rendered_tool_use_labels
.get(&tool_use.id)
.cloned(),
)
.truncate(),
),
.child(h_flex().pr_8().text_ui_sm(cx).children(
self.rendered_tool_use_labels.get(&tool_use.id).cloned(),
)),
)
.child(
h_flex()
@@ -1412,7 +1452,14 @@ impl ActiveThread {
icon.into_any_element()
}
}),
),
)
.child(div().h_full().absolute().w_8().bottom_0().right_12().bg(
linear_gradient(
90.,
linear_color_stop(self.tool_card_header_bg(cx), 1.),
linear_color_stop(self.tool_card_header_bg(cx).opacity(0.2), 0.),
),
)),
)
.map(|parent| {
if !is_open {
@@ -1429,7 +1476,7 @@ impl ActiveThread {
.child(
content_container()
.border_b_1()
.border_color(lighter_border)
.border_color(self.tool_card_border_color(cx))
.child(
Label::new("Input")
.size(LabelSize::XSmall)
@@ -1709,13 +1756,48 @@ impl ActiveThread {
.ok();
}
}
fn render_vertical_scrollbar(&self, cx: &mut Context<Self>) -> Stateful<Div> {
div()
.occlude()
.id("active-thread-scrollbar")
.on_mouse_move(cx.listener(|_, _, _, cx| {
cx.notify();
cx.stop_propagation()
}))
.on_hover(|_, _, cx| {
cx.stop_propagation();
})
.on_any_mouse_down(|_, _, cx| {
cx.stop_propagation();
})
.on_mouse_up(
MouseButton::Left,
cx.listener(|_, _, _, cx| {
cx.stop_propagation();
}),
)
.on_scroll_wheel(cx.listener(|_, _, _, cx| {
cx.notify();
}))
.h_full()
.absolute()
.right_1()
.top_1()
.bottom_0()
.w(px(12.))
.cursor_default()
.children(Scrollbar::vertical(self.scrollbar_state.clone()))
}
}
impl Render for ActiveThread {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
v_flex()
.size_full()
.relative()
.child(list(self.list_state.clone()).flex_grow())
.children(self.render_confirmations(cx))
.child(self.render_vertical_scrollbar(cx))
}
}

View File

@@ -11,12 +11,13 @@ 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_diff;
mod thread_history;
mod thread_store;
mod tool_selector;
mod tool_use;
mod ui;
@@ -32,10 +33,11 @@ use prompt_store::PromptBuilder;
use settings::Settings as _;
pub use crate::active_thread::ActiveThread;
use crate::assistant_configuration::AddContextServerModal;
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};
pub(crate) use crate::thread_diff::*;
pub use crate::thread_store::ThreadStore;
actions!(
@@ -47,6 +49,7 @@ actions!(
RemoveAllContext,
OpenHistory,
OpenConfiguration,
ManageProfiles,
AddContextServer,
RemoveSelectedThread,
Chat,
@@ -89,6 +92,7 @@ pub fn init(
cx,
);
cx.observe_new(AddContextServerModal::register).detach();
cx.observe_new(ManageProfilesModal::register).detach();
feature_gate_assistant2_actions(cx);
}

View File

@@ -1,4 +1,7 @@
mod add_context_server_modal;
mod manage_profiles_modal;
mod profile_picker;
mod tool_picker;
use std::sync::Arc;
@@ -12,6 +15,7 @@ use util::ResultExt as _;
use zed_actions::ExtensionCategoryFilter;
pub(crate) use add_context_server_modal::AddContextServerModal;
pub(crate) use manage_profiles_modal::ManageProfilesModal;
use crate::AddContextServer;

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

@@ -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

@@ -544,6 +544,10 @@ impl CompletionProvider for ContextPickerCompletionProvider {
fn sort_completions(&self) -> bool {
false
}
fn filter_completions(&self) -> bool {
false
}
}
fn confirm_completion_callback(

View File

@@ -282,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()
@@ -291,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('/');

View File

@@ -26,9 +26,9 @@ use crate::assistant_model_selector::AssistantModelSelector;
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, ThreadEvent, ToggleContextPicker};
pub struct MessageEditor {
@@ -43,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>,
}
@@ -57,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();
@@ -129,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,
}
}
@@ -318,7 +317,7 @@ 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
};
@@ -624,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)
}
}

View File

@@ -34,6 +34,7 @@ use crate::thread_store::{
SerializedToolUse,
};
use crate::tool_use::{PendingToolUse, ToolUse, ToolUseState};
use crate::{ChangeAuthor, ThreadDiff};
#[derive(Debug, Clone, Copy)]
pub enum RequestKind {
@@ -190,6 +191,7 @@ pub struct Thread {
initial_project_snapshot: Shared<Task<Option<Arc<ProjectSnapshot>>>>,
cumulative_token_usage: TokenUsage,
feedback: Option<ThreadFeedback>,
diff: Entity<ThreadDiff>,
}
impl Thread {
@@ -220,13 +222,14 @@ impl Thread {
tool_use: ToolUseState::new(tools.clone()),
action_log: cx.new(|_| ActionLog::new()),
initial_project_snapshot: {
let project_snapshot = Self::project_snapshot(project, cx);
let project_snapshot = Self::project_snapshot(project.clone(), cx);
cx.foreground_executor()
.spawn(async move { Some(project_snapshot.await) })
.shared()
},
cumulative_token_usage: TokenUsage::default(),
feedback: None,
diff: cx.new(|cx| ThreadDiff::new(project.clone(), cx)),
}
}
@@ -280,6 +283,7 @@ impl Thread {
pending_completions: Vec::new(),
last_restore_checkpoint: None,
pending_checkpoint: None,
diff: cx.new(|cx| ThreadDiff::new(project.clone(), cx)),
project,
prompt_builder,
tools,
@@ -873,17 +877,23 @@ impl Thread {
request.messages.push(context_message);
}
self.attach_stale_files(&mut request.messages, cx);
self.attached_tracked_files_state(&mut request.messages, cx);
request
}
fn attach_stale_files(&self, messages: &mut Vec<LanguageModelRequestMessage>, cx: &App) {
fn attached_tracked_files_state(
&self,
messages: &mut Vec<LanguageModelRequestMessage>,
cx: &App,
) {
const STALE_FILES_HEADER: &str = "These files changed since last read:";
let mut stale_message = String::new();
for stale_file in self.action_log.read(cx).stale_buffers(cx) {
let action_log = self.action_log.read(cx);
for stale_file in action_log.stale_buffers(cx) {
let Some(file) = stale_file.read(cx).file() else {
continue;
};
@@ -895,10 +905,22 @@ impl Thread {
writeln!(&mut stale_message, "- {}", file.path().display()).ok();
}
let mut content = Vec::with_capacity(2);
if !stale_message.is_empty() {
content.push(stale_message.into());
}
if action_log.has_edited_files_since_project_diagnostics_check() {
content.push(
"When you're done making changes, make sure to check project diagnostics and fix all errors AND warnings you introduced!".into(),
);
}
if !content.is_empty() {
let context_message = LanguageModelRequestMessage {
role: Role::User,
content: vec![stale_message.into()],
content,
cache: false,
};
@@ -1213,6 +1235,9 @@ impl Thread {
tool: Arc<dyn Tool>,
cx: &mut Context<Thread>,
) -> Task<()> {
self.diff
.update(cx, |diff, cx| diff.compute_changes(ChangeAuthor::User, cx));
let run_tool = tool.run(
input,
messages,
@@ -1227,6 +1252,10 @@ impl Thread {
thread
.update(cx, |thread, cx| {
thread
.diff
.update(cx, |diff, cx| diff.compute_changes(ChangeAuthor::Agent, cx));
let pending_tool_use = thread
.tool_use
.insert_tool_output(tool_use_id.clone(), output);
@@ -1392,7 +1421,7 @@ impl Thread {
git_store
.repositories()
.values()
.find(|repo| repo.read(cx).worktree_id == snapshot.id())
.find(|repo| repo.read(cx).worktree_id == Some(snapshot.id()))
.and_then(|repo| {
let repo = repo.read(cx);
Some((repo.branch().cloned(), repo.local_repository()?))
@@ -1411,7 +1440,7 @@ impl Thread {
// Get diff asynchronously
let diff = repo
.diff(git::repository::DiffType::HeadToWorktree, cx.clone())
.diff(git::repository::DiffType::HeadToWorktree)
.await
.ok();
@@ -1544,6 +1573,7 @@ pub enum ThreadEvent {
canceled: bool,
},
CheckpointChanged,
DiffChanged,
ToolConfirmationNeeded,
}

View File

@@ -0,0 +1,125 @@
use anyhow::Result;
use buffer_diff::BufferDiff;
use collections::HashMap;
use futures::{future::Shared, FutureExt};
use gpui::{prelude::*, App, Entity, Task};
use language::Buffer;
use project::{
git_store::{GitStore, GitStoreCheckpoint, GitStoreIndex, GitStoreStatus},
Project,
};
use util::TryFutureExt;
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub enum ChangeAuthor {
User,
Agent,
}
pub struct ThreadDiff {
base: Shared<Task<Option<GitStoreIndex>>>,
diffs_by_buffer: HashMap<Entity<Buffer>, Entity<BufferDiff>>,
last_checkpoint: Option<Task<Result<GitStoreCheckpoint>>>,
project: Entity<Project>,
git_store: Entity<GitStore>,
}
impl ThreadDiff {
pub fn new(project: Entity<Project>, cx: &mut Context<Self>) -> Self {
let mut this = Self {
base: cx
.background_spawn(
project
.read(cx)
.git_store()
.read(cx)
.create_index(cx)
.log_err(),
)
.shared(),
diffs_by_buffer: HashMap::default(),
last_checkpoint: None,
git_store: project.read(cx).git_store().clone(),
project,
};
this.compute_changes(ChangeAuthor::User, cx);
this
}
pub fn compute_changes(&mut self, author: ChangeAuthor, cx: &mut Context<Self>) {
let last_checkpoint = self.last_checkpoint.take();
let git_store = self.project.read(cx).git_store().clone();
let checkpoint = git_store.read(cx).checkpoint(cx);
let base = self.base.clone();
self.last_checkpoint = Some(cx.spawn(async move |this, cx| {
let checkpoint = checkpoint.await?;
if let Some(base) = base.await {
if let Some(last_checkpoint) = last_checkpoint {
if let Ok(last_checkpoint) = last_checkpoint.await {
if author == ChangeAuthor::User {
let diff = git_store
.read_with(cx, |store, cx| {
store.diff_checkpoints(last_checkpoint, checkpoint.clone(), cx)
})?
.await;
if let Ok(diff) = diff {
_ = git_store
.read_with(cx, |store, cx| {
store.apply_diff(base.clone(), diff, cx)
})?
.await;
}
}
}
}
let status = git_store
.read_with(cx, |store, cx| store.status(Some(base), cx))?
.await
.unwrap_or_default();
this.update(cx, |this, cx| this.set_status(status, cx))?;
}
Ok(checkpoint)
}));
}
pub fn set_status(&mut self, status: GitStoreStatus, cx: &mut Context<Self>) {}
}
struct ThreadDiffSource {
thread_diff: Entity<ThreadDiff>,
git_store: Entity<GitStore>,
}
impl git_ui::project_diff::DiffSource for ThreadDiff {
fn status(&self, cx: &App) -> Vec<(project::ProjectPath, git::status::FileStatus, bool)> {
let mut results = Vec::new();
todo!();
// for (repo, repo_path, change) in self.changes.iter(&self.git_store, cx) {
// let Some(project_path) = repo.read(cx).repo_path_to_project_path(repo_path) else {
// continue;
// };
// results.push((
// project_path,
// // todo!("compute the correct status")
// git::status::FileStatus::worktree(git::status::StatusCode::Modified),
// false,
// ))
// }
results
}
fn open_uncommitted_diff(
&self,
buffer: Entity<Buffer>,
cx: &mut App,
) -> Task<Result<Entity<BufferDiff>>> {
todo!()
}
}

View File

@@ -3,7 +3,8 @@ 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;
@@ -19,6 +20,7 @@ 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, ThreadEvent, ThreadId};
@@ -57,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);
@@ -184,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(),

View File

@@ -1,172 +0,0 @@
use std::sync::Arc;
use assistant_settings::{AgentProfile, AssistantSettings};
use assistant_tool::{ToolSource, ToolWorkingSet};
use gpui::{Entity, Subscription};
use indexmap::IndexMap;
use settings::{Settings as _, SettingsStore};
use ui::{prelude::*, ContextMenu, PopoverMenu, Tooltip};
pub struct ToolSelector {
profiles: IndexMap<Arc<str>, AgentProfile>,
tools: Arc<ToolWorkingSet>,
_subscriptions: Vec<Subscription>,
}
impl ToolSelector {
pub fn new(tools: Arc<ToolWorkingSet>, 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(),
tools,
_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> {
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 (_id, 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_all_tools(cx);
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 {
tools.enable(
ToolSource::ContextServer {
id: context_server_id.clone().into(),
},
&preset
.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.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 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

@@ -9,12 +9,16 @@ use ui::{prelude::*, Render};
pub struct ToolReadyPopUp {
caption: SharedString,
icon: IconName,
icon_color: Color,
}
impl ToolReadyPopUp {
pub fn new(caption: impl Into<SharedString>) -> Self {
pub fn new(caption: impl Into<SharedString>, icon: IconName, icon_color: Color) -> Self {
Self {
caption: caption.into(),
icon,
icon_color,
}
}
@@ -82,9 +86,9 @@ impl Render for ToolReadyPopUp {
.gap_2()
.child(
h_flex().h(line_height).justify_center().child(
Icon::new(IconName::Info)
.size(IconSize::Small)
.color(Color::Muted),
Icon::new(self.icon)
.color(self.icon_color)
.size(IconSize::Small),
),
)
.child(

View File

@@ -149,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

@@ -12,7 +12,7 @@ pub struct AgentProfile {
pub context_servers: IndexMap<Arc<str>, ContextServerPreset>,
}
#[derive(Debug, Clone)]
#[derive(Debug, Clone, Default)]
pub struct ContextServerPreset {
pub tools: IndexMap<Arc<str>, bool>,
}

View File

@@ -71,6 +71,7 @@ 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,
@@ -174,6 +175,7 @@ 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,
@@ -198,6 +200,7 @@ 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,
@@ -307,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)]
@@ -330,6 +345,7 @@ 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,
@@ -370,7 +386,9 @@ pub struct AssistantSettingsContentV2 {
/// Default: false
enable_experimental_live_diffs: Option<bool>,
#[schemars(skip)]
profiles: Option<IndexMap<Arc<str>, AgentProfileContent>>,
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.
///
@@ -424,7 +442,7 @@ pub struct AgentProfileContent {
pub context_servers: IndexMap<Arc<str>, ContextServerPresetContent>,
}
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, JsonSchema)]
#[derive(Debug, PartialEq, Clone, Default, Serialize, Deserialize, JsonSchema)]
pub struct ContextServerPresetContent {
pub tools: IndexMap<Arc<str>, bool>,
}
@@ -531,6 +549,7 @@ impl Settings for AssistantSettings {
&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
@@ -621,6 +640,7 @@ 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

@@ -80,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)]
@@ -93,6 +95,7 @@ impl ActionLog {
Self {
stale_buffers_in_context: HashSet::default(),
tracked_buffers: HashMap::default(),
edited_since_project_diagnostics_check: false,
}
}
@@ -110,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
@@ -120,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

@@ -19,7 +19,7 @@ pub struct ToolWorkingSet {
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>>>,
enabled_tools_by_source: HashMap<ToolSource, HashSet<Arc<str>>>,
next_tool_id: ToolId,
}
@@ -41,38 +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()
}
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.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 {
@@ -159,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()
@@ -201,16 +182,11 @@ impl WorkingSetState {
);
}
fn disable_all_tools(&mut self, cx: &App) {
let tools = self.tools_by_source(cx);
fn disable_source(&mut self, source: &ToolSource) {
self.enabled_tools_by_source.remove(source);
}
for (source, tools) in tools {
let tool_names = tools
.into_iter()
.map(|tool| tool.name().into())
.collect::<Vec<_>>();
self.disable(source, &tool_names);
}
fn disable_all_tools(&mut self) {
self.enabled_tools_by_source.clear();
}
}

View File

@@ -1,5 +1,6 @@
mod bash_tool;
mod copy_path_tool;
mod create_directory_tool;
mod create_file_tool;
mod delete_path_tool;
mod diagnostics_tool;
@@ -24,6 +25,7 @@ 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;
@@ -43,6 +45,7 @@ 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);

View File

@@ -8,6 +8,7 @@ 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 {
@@ -43,7 +44,13 @@ impl Tool for BashTool {
fn ui_text(&self, input: &serde_json::Value) -> String {
match serde_json::from_value::<BashToolInput>(input.clone()) {
Ok(input) => format!("`{}`", input.command),
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(),
}
}

View File

@@ -7,6 +7,7 @@ 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 {
@@ -60,9 +61,9 @@ impl Tool for CopyPathTool {
fn ui_text(&self, input: &serde_json::Value) -> String {
match serde_json::from_value::<CopyPathToolInput>(input.clone()) {
Ok(input) => {
let src = input.source_path.as_str();
let dest = input.destination_path.as_str();
format!("Copy `{src}` to `{dest}`")
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(),
}

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

@@ -7,6 +7,7 @@ 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 {
@@ -46,7 +47,7 @@ impl Tool for CreateFileTool {
}
fn icon(&self) -> IconName {
IconName::File
IconName::FileCreate
}
fn input_schema(&self) -> serde_json::Value {
@@ -57,8 +58,8 @@ impl Tool for CreateFileTool {
fn ui_text(&self, input: &serde_json::Value) -> String {
match serde_json::from_value::<CreateFileToolInput>(input.clone()) {
Ok(input) => {
let path = input.path.as_str();
format!("Create file `{path}`")
let path = MarkdownString::inline_code(&input.path);
format!("Create file {path}")
}
Err(_) => "Create file".to_string(),
}

View File

@@ -40,7 +40,7 @@ impl Tool for DeletePathTool {
}
fn icon(&self) -> IconName {
IconName::Trash
IconName::FileDelete
}
fn input_schema(&self) -> serde_json::Value {

View File

@@ -6,12 +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 {
@@ -28,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;
@@ -58,9 +65,12 @@ impl Tool for DiagnosticsTool {
fn ui_text(&self, input: &serde_json::Value) -> String {
if let Some(path) = serde_json::from_value::<DiagnosticsToolInput>(input.clone())
.ok()
.and_then(|input| input.path)
.and_then(|input| match input.path {
Some(path) if !path.is_empty() => Some(MarkdownString::inline_code(&path)),
_ => None,
})
{
format!("Check diagnostics for “`{}`”", path.display())
format!("Check diagnostics for {path}")
} else {
"Check project diagnostics".to_string()
}
@@ -71,78 +81,84 @@ impl Tool for DiagnosticsTool {
input: serde_json::Value,
_messages: &[LanguageModelRequestMessage],
project: Entity<Project>,
_action_log: Entity<ActionLog>,
action_log: Entity<ActionLog>,
cx: &mut App,
) -> Task<Result<String>> {
if let Some(path) = serde_json::from_value::<DiagnosticsToolInput>(input)
match serde_json::from_value::<DiagnosticsToolInput>(input)
.ok()
.and_then(|input| 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",
path.display()
)));
};
let buffer = project.update(cx, |project, cx| project.open_buffer(project_path, cx));
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",)));
};
cx.spawn(async move |cx| {
let mut output = String::new();
let buffer = buffer.await?;
let snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot())?;
let buffer =
project.update(cx, |project, cx| project.open_buffer(project_path, cx));
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,
};
cx.spawn(async move |cx| {
let mut output = String::new();
let buffer = buffer.await?;
let snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot())?;
writeln!(
output,
"{} at line {}: {}",
severity,
range.start.row + 1,
entry.diagnostic.message
)?;
}
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,
};
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;
writeln!(
output,
"{} at line {}: {}",
severity,
range.start.row + 1,
entry.diagnostic.message
)?;
}
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

@@ -13,6 +13,7 @@ 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 {
@@ -133,7 +134,7 @@ impl Tool for FetchTool {
fn ui_text(&self, input: &serde_json::Value) -> String {
match serde_json::from_value::<FetchToolInput>(input.clone()) {
Ok(input) => format!("Fetch `{}`", input.url),
Ok(input) => format!("Fetch {}", MarkdownString::escape(&input.url)),
Err(_) => "Fetch URL".to_string(),
}
}

View File

@@ -33,10 +33,10 @@ pub struct FindReplaceFileToolInput {
/// </example>
pub path: PathBuf,
/// A user-friendly description of what's being replaced. This will be shown in the UI.
/// 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</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;

View File

@@ -7,6 +7,7 @@ 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 {
@@ -61,7 +62,10 @@ impl Tool for ListDirectoryTool {
fn ui_text(&self, input: &serde_json::Value) -> String {
match serde_json::from_value::<ListDirectoryToolInput>(input.clone()) {
Ok(input) => format!("List the `{}` directory's contents", input.path),
Ok(input) => {
let path = MarkdownString::inline_code(&input.path);
format!("List the {path} directory's contents")
}
Err(_) => "List directory".to_string(),
}
}

View File

@@ -7,6 +7,7 @@ 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 {
@@ -60,20 +61,21 @@ impl Tool for MovePathTool {
fn ui_text(&self, input: &serde_json::Value) -> String {
match serde_json::from_value::<MovePathToolInput>(input.clone()) {
Ok(input) => {
let src = input.source_path.as_str();
let dest = input.destination_path.as_str();
let src_path = Path::new(src);
let dest_path = Path::new(dest);
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() => {
format!("Rename `{src}` to `{filename}`")
let filename = MarkdownString::inline_code(&filename);
format!("Rename {src} to {filename}")
}
_ => {
format!("Move `{src}` to `{dest}`")
format!("Move {src} to {dest}")
}
}
}

View File

@@ -10,6 +10,7 @@ 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 {
@@ -64,7 +65,10 @@ impl Tool for ReadFileTool {
fn ui_text(&self, input: &serde_json::Value) -> String {
match serde_json::from_value::<ReadFileToolInput>(input.clone()) {
Ok(input) => format!("Read file `{}`", input.path.display()),
Ok(input) => {
let path = MarkdownString::inline_code(&input.path.display().to_string());
format!("Read file {path}")
}
Err(_) => "Read file".to_string(),
}
}

View File

@@ -12,6 +12,7 @@ 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)]
@@ -63,14 +64,12 @@ impl Tool for RegexSearchTool {
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 “`{}`”",
input.regex
)
format!("Get page {page} of search results for regex “{regex}")
} else {
format!("Search files for regex “`{}`", input.regex)
format!("Search files for regex “{regex}")
}
}
Err(_) => "Search with regex".to_string(),

View File

@@ -3,9 +3,7 @@ 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, ToOffset as _};
use util::ResultExt;
@@ -195,7 +193,7 @@ impl BufferDiffInner {
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());
@@ -208,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()),
@@ -228,15 +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 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 {
@@ -261,7 +257,6 @@ impl BufferDiffInner {
old_pending_hunks.next(buffer);
}
// merge into pending hunks
if (stage && secondary_status == DiffHunkSecondaryStatus::NoSecondaryHunk)
|| (!stage && secondary_status == DiffHunkSecondaryStatus::HasSecondaryHunk)
{
@@ -288,56 +283,71 @@ impl BufferDiffInner {
let mut unstaged_hunk_cursor = unstaged_diff.hunks.cursor::<DiffHunkSummary>(buffer);
unstaged_hunk_cursor.next(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();
// 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);
@@ -351,8 +361,11 @@ 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;
#[cfg(debug_assertions)] // invariants: non-overlapping and sorted
{
@@ -371,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>(
@@ -408,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();
@@ -438,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;
}
}
}
@@ -839,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),
});
@@ -857,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_impl(
let new_index_text = self.inner.stage_or_unstage_hunks_impl(
&self.secondary_diff.as_ref()?.read(cx).inner,
stage,
&hunks,
@@ -865,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(),
));
@@ -1649,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 {

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

@@ -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

@@ -304,6 +304,7 @@ impl Server {
.add_request_handler(forward_read_only_project_request::<proto::GetReferences>)
.add_request_handler(forward_find_search_candidates_request)
.add_request_handler(forward_read_only_project_request::<proto::GetDocumentHighlights>)
.add_request_handler(forward_read_only_project_request::<proto::GetDocumentSymbols>)
.add_request_handler(forward_read_only_project_request::<proto::GetProjectSymbols>)
.add_request_handler(forward_read_only_project_request::<proto::OpenBufferForSymbol>)
.add_request_handler(forward_read_only_project_request::<proto::OpenBufferById>)

View File

@@ -26,7 +26,7 @@ use language::{
tree_sitter_rust, tree_sitter_typescript, Diagnostic, DiagnosticEntry, FakeLspAdapter,
Language, LanguageConfig, LanguageMatcher, LineEnding, OffsetRangeExt, Point, Rope,
};
use lsp::LanguageServerId;
use lsp::{LanguageServerId, OneOf};
use parking_lot::Mutex;
use pretty_assertions::assert_eq;
use project::{
@@ -2892,15 +2892,17 @@ async fn test_git_branch_name(
#[track_caller]
fn assert_branch(branch_name: Option<impl Into<String>>, project: &Project, cx: &App) {
let branch_name = branch_name.map(Into::into);
let worktrees = project.visible_worktrees(cx).collect::<Vec<_>>();
assert_eq!(worktrees.len(), 1);
let worktree = worktrees[0].clone();
let snapshot = worktree.read(cx).snapshot();
let repo = snapshot.repositories().first().unwrap();
let repositories = project.repositories(cx).values().collect::<Vec<_>>();
assert_eq!(repositories.len(), 1);
let repository = repositories[0].clone();
assert_eq!(
repo.branch().map(|branch| branch.name.to_string()),
repository
.read(cx)
.repository_entry
.branch()
.map(|branch| branch.name.to_string()),
branch_name
);
)
}
// Smoke test branch reading
@@ -3022,11 +3024,20 @@ async fn test_git_status_sync(
cx: &App,
) {
let file = file.as_ref();
let worktrees = project.visible_worktrees(cx).collect::<Vec<_>>();
assert_eq!(worktrees.len(), 1);
let worktree = worktrees[0].clone();
let snapshot = worktree.read(cx).snapshot();
assert_eq!(snapshot.status_for_file(file), status);
let repos = project
.repositories(cx)
.values()
.cloned()
.collect::<Vec<_>>();
assert_eq!(repos.len(), 1);
let repo = repos.into_iter().next().unwrap();
assert_eq!(
repo.read(cx)
.repository_entry
.status_for_path(&file.into())
.map(|entry| entry.status),
status
);
}
project_local.read_with(cx_a, |project, cx| {
@@ -3094,6 +3105,27 @@ async fn test_git_status_sync(
assert_status("b.txt", Some(B_STATUS_END), project, cx);
assert_status("c.txt", Some(C_STATUS_END), project, cx);
});
// Now remove the original git repository and check that collaborators are notified.
client_a
.fs()
.remove_dir("/dir/.git".as_ref(), RemoveOptions::default())
.await
.unwrap();
executor.run_until_parked();
project_remote.update(cx_b, |project, cx| {
pretty_assertions::assert_eq!(
project.git_store().read(cx).repo_snapshots(cx),
HashMap::default()
);
});
project_remote_c.update(cx_c, |project, cx| {
pretty_assertions::assert_eq!(
project.git_store().read(cx).repo_snapshots(cx),
HashMap::default()
);
});
}
#[gpui::test(iterations = 10)]
@@ -5399,9 +5431,16 @@ async fn test_project_symbols(
let active_call_a = cx_a.read(ActiveCall::global);
client_a.language_registry().add(rust_lang());
let mut fake_language_servers = client_a
.language_registry()
.register_fake_lsp("Rust", Default::default());
let mut fake_language_servers = client_a.language_registry().register_fake_lsp(
"Rust",
FakeLspAdapter {
capabilities: lsp::ServerCapabilities {
workspace_symbol_provider: Some(OneOf::Left(true)),
..Default::default()
},
..Default::default()
},
);
client_a
.fs()

View File

@@ -1,8 +1,8 @@
use crate::tests::TestServer;
use call::ActiveCall;
use collections::HashSet;
use collections::{HashMap, HashSet};
use extension::ExtensionHostProxy;
use fs::{FakeFs, Fs as _};
use fs::{FakeFs, Fs as _, RemoveOptions};
use futures::StreamExt as _;
use gpui::{
AppContext as _, BackgroundExecutor, SemanticVersion, TestAppContext, UpdateGlobal as _,
@@ -356,6 +356,26 @@ async fn test_ssh_collaboration_git_branches(
});
assert_eq!(server_branch.name, "totally-new-branch");
// Remove the git repository and check that all participants get the update.
remote_fs
.remove_dir("/project/.git".as_ref(), RemoveOptions::default())
.await
.unwrap();
executor.run_until_parked();
project_a.update(cx_a, |project, cx| {
pretty_assertions::assert_eq!(
project.git_store().read(cx).repo_snapshots(cx),
HashMap::default()
);
});
project_b.update(cx_b, |project, cx| {
pretty_assertions::assert_eq!(
project.git_store().read(cx).repo_snapshots(cx),
HashMap::default()
);
});
}
#[gpui::test]

View File

@@ -3411,6 +3411,7 @@ impl Editor {
}
pub fn newline(&mut self, _: &Newline, window: &mut Window, cx: &mut Context<Self>) {
self.mouse_cursor_hidden = self.hide_mouse_while_typing;
self.transact(window, cx, |this, window, cx| {
let (edits, selection_fixup_info): (Vec<_>, Vec<_>) = {
let selections = this.selections.all::<usize>(cx);
@@ -3526,6 +3527,8 @@ impl Editor {
}
pub fn newline_above(&mut self, _: &NewlineAbove, window: &mut Window, cx: &mut Context<Self>) {
self.mouse_cursor_hidden = self.hide_mouse_while_typing;
let buffer = self.buffer.read(cx);
let snapshot = buffer.snapshot(cx);
@@ -3583,6 +3586,8 @@ impl Editor {
}
pub fn newline_below(&mut self, _: &NewlineBelow, window: &mut Window, cx: &mut Context<Self>) {
self.mouse_cursor_hidden = self.hide_mouse_while_typing;
let buffer = self.buffer.read(cx);
let snapshot = buffer.snapshot(cx);
@@ -4315,6 +4320,10 @@ impl Editor {
.as_ref()
.map_or(true, |provider| provider.sort_completions());
let filter_completions = provider
.as_ref()
.map_or(true, |provider| provider.filter_completions());
let id = post_inc(&mut self.next_completion_id);
let task = cx.spawn_in(window, async move |editor, cx| {
async move {
@@ -4363,8 +4372,15 @@ impl Editor {
completions.into(),
);
menu.filter(query.as_deref(), cx.background_executor().clone())
.await;
menu.filter(
if filter_completions {
query.as_deref()
} else {
None
},
cx.background_executor().clone(),
)
.await;
menu.visible().then_some(menu)
};
@@ -6112,35 +6128,6 @@ impl Editor {
return breakpoint_display_points;
};
if let Some(buffer) = self.buffer.read(cx).as_singleton() {
let buffer_snapshot = buffer.read(cx).snapshot();
for breakpoint in
breakpoint_store
.read(cx)
.breakpoints(&buffer, None, &buffer_snapshot, cx)
{
let point = buffer_snapshot.summary_for_anchor::<Point>(&breakpoint.0);
let mut anchor = multi_buffer_snapshot.anchor_before(point);
anchor.text_anchor = breakpoint.0;
breakpoint_display_points.insert(
snapshot
.point_to_display_point(
MultiBufferPoint {
row: point.row,
column: point.column,
},
Bias::Left,
)
.row(),
(anchor, breakpoint.1.clone()),
);
}
return breakpoint_display_points;
}
let range = snapshot.display_point_to_point(DisplayPoint::new(range.start, 0), Bias::Left)
..snapshot.display_point_to_point(DisplayPoint::new(range.end, 0), Bias::Right);
@@ -7773,6 +7760,7 @@ impl Editor {
}
pub fn backspace(&mut self, _: &Backspace, window: &mut Window, cx: &mut Context<Self>) {
self.mouse_cursor_hidden = self.hide_mouse_while_typing;
self.transact(window, cx, |this, window, cx| {
this.select_autoclose_pair(window, cx);
let mut linked_ranges = HashMap::<_, Vec<_>>::default();
@@ -7871,6 +7859,7 @@ impl Editor {
}
pub fn delete(&mut self, _: &Delete, window: &mut Window, cx: &mut Context<Self>) {
self.mouse_cursor_hidden = self.hide_mouse_while_typing;
self.transact(window, cx, |this, window, cx| {
this.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
let line_mode = s.line_mode;
@@ -7892,7 +7881,7 @@ impl Editor {
if self.move_to_prev_snippet_tabstop(window, cx) {
return;
}
self.mouse_cursor_hidden = self.hide_mouse_while_typing;
self.outdent(&Outdent, window, cx);
}
@@ -7900,7 +7889,7 @@ impl Editor {
if self.move_to_next_snippet_tabstop(window, cx) || self.read_only(cx) {
return;
}
self.mouse_cursor_hidden = self.hide_mouse_while_typing;
let mut selections = self.selections.all_adjusted(cx);
let buffer = self.buffer.read(cx);
let snapshot = buffer.snapshot(cx);
@@ -18024,6 +18013,10 @@ pub trait CompletionProvider {
fn sort_completions(&self) -> bool {
true
}
fn filter_completions(&self) -> bool {
true
}
}
pub trait CodeActionProvider {

View File

@@ -1955,7 +1955,12 @@ impl EditorElement {
.filter_map(|(display_row, (text_anchor, bp))| {
if row_infos
.get((display_row.0.saturating_sub(range.start.0)) as usize)
.is_some_and(|row_info| row_info.expand_info.is_some())
.is_some_and(|row_info| {
row_info.expand_info.is_some()
|| row_info
.diff_status
.is_some_and(|status| status.is_deleted())
})
{
return None;
}
@@ -4308,7 +4313,7 @@ impl EditorElement {
let is_singleton = self.editor.read(cx).is_singleton(cx);
let line_height = layout.position_map.line_height;
window.set_cursor_style(CursorStyle::Arrow, &layout.gutter_hitbox);
window.set_cursor_style(CursorStyle::Arrow, Some(&layout.gutter_hitbox));
for LineNumberLayout {
shaped_line,
@@ -4341,9 +4346,9 @@ impl EditorElement {
// In singleton buffers, we select corresponding lines on the line number click, so use | -like cursor.
// In multi buffers, we open file at the line number clicked, so use a pointing hand cursor.
if is_singleton {
window.set_cursor_style(CursorStyle::IBeam, &hitbox);
window.set_cursor_style(CursorStyle::IBeam, Some(&hitbox));
} else {
window.set_cursor_style(CursorStyle::PointingHand, &hitbox);
window.set_cursor_style(CursorStyle::PointingHand, Some(&hitbox));
}
}
}
@@ -4565,7 +4570,7 @@ impl EditorElement {
.read(cx)
.all_diff_hunks_expanded()
{
window.set_cursor_style(CursorStyle::PointingHand, hunk_hitbox);
window.set_cursor_style(CursorStyle::PointingHand, Some(hunk_hitbox));
}
}
}
@@ -4638,18 +4643,23 @@ impl EditorElement {
}),
|window| {
let editor = self.editor.read(cx);
let cursor_style = if editor.mouse_cursor_hidden {
CursorStyle::None
if editor.mouse_cursor_hidden {
window.set_cursor_style(CursorStyle::None, None);
} else if editor
.hovered_link_state
.as_ref()
.is_some_and(|hovered_link_state| !hovered_link_state.links.is_empty())
{
CursorStyle::PointingHand
window.set_cursor_style(
CursorStyle::PointingHand,
Some(&layout.position_map.text_hitbox),
);
} else {
CursorStyle::IBeam
window.set_cursor_style(
CursorStyle::IBeam,
Some(&layout.position_map.text_hitbox),
);
};
window.set_cursor_style(cursor_style, &layout.position_map.text_hitbox);
self.paint_lines_background(layout, window, cx);
let invisible_display_ranges = self.paint_highlights(layout, window);
@@ -4844,7 +4854,7 @@ impl EditorElement {
));
})
}
window.set_cursor_style(CursorStyle::Arrow, &hitbox);
window.set_cursor_style(CursorStyle::Arrow, Some(&hitbox));
}
window.on_mouse_event({

View File

@@ -150,7 +150,7 @@ impl GitBlame {
this.generate(cx);
}
}
project::Event::WorktreeUpdatedGitRepositories(_) => {
project::Event::GitStateUpdated => {
log::debug!("Status of git repositories updated. Regenerating blame data...",);
this.generate(cx);
}

View File

@@ -1,4 +1,5 @@
use crate::actions::FormatSelections;
use crate::CopyAndTrim;
use crate::{
actions::Format, selections_collection::SelectionsCollection, Copy, CopyPermalinkToLine, Cut,
DisplayPoint, DisplaySnapshot, Editor, EditorMode, FindAllReferences, GoToDeclaration,
@@ -191,6 +192,7 @@ pub fn deploy_context_menu(
.separator()
.action("Cut", Box::new(Cut))
.action("Copy", Box::new(Copy))
.action("Copy and trim", Box::new(CopyAndTrim))
.action("Paste", Box::new(Paste))
.separator()
.map(|builder| {

View File

@@ -339,7 +339,8 @@ impl EditorTestContext {
let mut found = None;
fs.with_git_state(&Self::root_path().join(".git"), false, |git_state| {
found = git_state.index_contents.get(path.as_ref()).cloned();
});
})
.unwrap();
assert_eq!(expected, found.as_deref());
}

View File

@@ -273,7 +273,7 @@ async fn run_evaluation(
let repos_dir = Path::new(EVAL_REPOS_DIR);
let db_path = Path::new(EVAL_DB_PATH);
let api_key = std::env::var("OPENAI_API_KEY").unwrap();
let fs = Arc::new(RealFs::new(None)) as Arc<dyn Fs>;
let fs = Arc::new(RealFs::new(None, cx.background_executor().clone())) as Arc<dyn Fs>;
let clock = Arc::new(RealSystemClock);
let client = cx
.update(|cx| {

View File

@@ -18,6 +18,7 @@ clap = { workspace = true, features = ["derive"] }
env_logger.workspace = true
extension.workspace = true
fs.workspace = true
gpui.workspace = true
language.workspace = true
log.workspace = true
reqwest_client.workspace = true

View File

@@ -34,7 +34,7 @@ async fn main() -> Result<()> {
env_logger::init();
let args = Args::parse();
let fs = Arc::new(RealFs::default());
let fs = Arc::new(RealFs::new(None, gpui::background_executor()));
let engine = wasmtime::Engine::default();
let mut wasm_store = WasmStore::new(&engine)?;

View File

@@ -477,7 +477,7 @@ async fn test_extension_store_with_test_extension(cx: &mut TestAppContext) {
let test_extension_id = "test-extension";
let test_extension_dir = root_dir.join("extensions").join(test_extension_id);
let fs = Arc::new(RealFs::default());
let fs = Arc::new(RealFs::new(None, cx.executor()));
let extensions_dir = TempTree::new(json!({
"installed": {},
"work": {}

View File

@@ -90,13 +90,13 @@ pub fn authorize_access_to_unreleased_wasm_api_version(
}
pub enum Extension {
V040(since_v0_4_0::Extension),
V030(since_v0_3_0::Extension),
V020(since_v0_2_0::Extension),
V010(since_v0_1_0::Extension),
V006(since_v0_0_6::Extension),
V004(since_v0_0_4::Extension),
V001(since_v0_0_1::Extension),
V0_4_0(since_v0_4_0::Extension),
V0_3_0(since_v0_3_0::Extension),
V0_2_0(since_v0_2_0::Extension),
V0_1_0(since_v0_1_0::Extension),
V0_0_6(since_v0_0_6::Extension),
V0_0_4(since_v0_0_4::Extension),
V0_0_1(since_v0_0_1::Extension),
}
impl Extension {
@@ -116,7 +116,7 @@ impl Extension {
latest::Extension::instantiate_async(store, component, latest::linker())
.await
.context("failed to instantiate wasm extension")?;
Ok(Self::V040(extension))
Ok(Self::V0_4_0(extension))
} else if version >= since_v0_3_0::MIN_VERSION {
let extension = since_v0_3_0::Extension::instantiate_async(
store,
@@ -125,7 +125,7 @@ impl Extension {
)
.await
.context("failed to instantiate wasm extension")?;
Ok(Self::V030(extension))
Ok(Self::V0_3_0(extension))
} else if version >= since_v0_2_0::MIN_VERSION {
let extension = since_v0_2_0::Extension::instantiate_async(
store,
@@ -134,7 +134,7 @@ impl Extension {
)
.await
.context("failed to instantiate wasm extension")?;
Ok(Self::V020(extension))
Ok(Self::V0_2_0(extension))
} else if version >= since_v0_1_0::MIN_VERSION {
let extension = since_v0_1_0::Extension::instantiate_async(
store,
@@ -143,7 +143,7 @@ impl Extension {
)
.await
.context("failed to instantiate wasm extension")?;
Ok(Self::V010(extension))
Ok(Self::V0_1_0(extension))
} else if version >= since_v0_0_6::MIN_VERSION {
let extension = since_v0_0_6::Extension::instantiate_async(
store,
@@ -152,7 +152,7 @@ impl Extension {
)
.await
.context("failed to instantiate wasm extension")?;
Ok(Self::V006(extension))
Ok(Self::V0_0_6(extension))
} else if version >= since_v0_0_4::MIN_VERSION {
let extension = since_v0_0_4::Extension::instantiate_async(
store,
@@ -161,7 +161,7 @@ impl Extension {
)
.await
.context("failed to instantiate wasm extension")?;
Ok(Self::V004(extension))
Ok(Self::V0_0_4(extension))
} else {
let extension = since_v0_0_1::Extension::instantiate_async(
store,
@@ -170,19 +170,19 @@ impl Extension {
)
.await
.context("failed to instantiate wasm extension")?;
Ok(Self::V001(extension))
Ok(Self::V0_0_1(extension))
}
}
pub async fn call_init_extension(&self, store: &mut Store<WasmState>) -> Result<()> {
match self {
Extension::V040(ext) => ext.call_init_extension(store).await,
Extension::V030(ext) => ext.call_init_extension(store).await,
Extension::V020(ext) => ext.call_init_extension(store).await,
Extension::V010(ext) => ext.call_init_extension(store).await,
Extension::V006(ext) => ext.call_init_extension(store).await,
Extension::V004(ext) => ext.call_init_extension(store).await,
Extension::V001(ext) => ext.call_init_extension(store).await,
Extension::V0_4_0(ext) => ext.call_init_extension(store).await,
Extension::V0_3_0(ext) => ext.call_init_extension(store).await,
Extension::V0_2_0(ext) => ext.call_init_extension(store).await,
Extension::V0_1_0(ext) => ext.call_init_extension(store).await,
Extension::V0_0_6(ext) => ext.call_init_extension(store).await,
Extension::V0_0_4(ext) => ext.call_init_extension(store).await,
Extension::V0_0_1(ext) => ext.call_init_extension(store).await,
}
}
@@ -194,27 +194,27 @@ impl Extension {
resource: Resource<Arc<dyn WorktreeDelegate>>,
) -> Result<Result<Command, String>> {
match self {
Extension::V040(ext) => {
Extension::V0_4_0(ext) => {
ext.call_language_server_command(store, &language_server_id.0, resource)
.await
}
Extension::V030(ext) => {
Extension::V0_3_0(ext) => {
ext.call_language_server_command(store, &language_server_id.0, resource)
.await
}
Extension::V020(ext) => Ok(ext
Extension::V0_2_0(ext) => Ok(ext
.call_language_server_command(store, &language_server_id.0, resource)
.await?
.map(|command| command.into())),
Extension::V010(ext) => Ok(ext
Extension::V0_1_0(ext) => Ok(ext
.call_language_server_command(store, &language_server_id.0, resource)
.await?
.map(|command| command.into())),
Extension::V006(ext) => Ok(ext
Extension::V0_0_6(ext) => Ok(ext
.call_language_server_command(store, &language_server_id.0, resource)
.await?
.map(|command| command.into())),
Extension::V004(ext) => Ok(ext
Extension::V0_0_4(ext) => Ok(ext
.call_language_server_command(
store,
&LanguageServerConfig {
@@ -225,7 +225,7 @@ impl Extension {
)
.await?
.map(|command| command.into())),
Extension::V001(ext) => Ok(ext
Extension::V0_0_1(ext) => Ok(ext
.call_language_server_command(
store,
&LanguageServerConfig {
@@ -248,7 +248,7 @@ impl Extension {
resource: Resource<Arc<dyn WorktreeDelegate>>,
) -> Result<Result<Option<String>, String>> {
match self {
Extension::V040(ext) => {
Extension::V0_4_0(ext) => {
ext.call_language_server_initialization_options(
store,
&language_server_id.0,
@@ -256,7 +256,7 @@ impl Extension {
)
.await
}
Extension::V030(ext) => {
Extension::V0_3_0(ext) => {
ext.call_language_server_initialization_options(
store,
&language_server_id.0,
@@ -264,7 +264,7 @@ impl Extension {
)
.await
}
Extension::V020(ext) => {
Extension::V0_2_0(ext) => {
ext.call_language_server_initialization_options(
store,
&language_server_id.0,
@@ -272,7 +272,7 @@ impl Extension {
)
.await
}
Extension::V010(ext) => {
Extension::V0_1_0(ext) => {
ext.call_language_server_initialization_options(
store,
&language_server_id.0,
@@ -280,7 +280,7 @@ impl Extension {
)
.await
}
Extension::V006(ext) => {
Extension::V0_0_6(ext) => {
ext.call_language_server_initialization_options(
store,
&language_server_id.0,
@@ -288,7 +288,7 @@ impl Extension {
)
.await
}
Extension::V004(ext) => {
Extension::V0_0_4(ext) => {
ext.call_language_server_initialization_options(
store,
&LanguageServerConfig {
@@ -299,7 +299,7 @@ impl Extension {
)
.await
}
Extension::V001(ext) => {
Extension::V0_0_1(ext) => {
ext.call_language_server_initialization_options(
store,
&LanguageServerConfig {
@@ -321,7 +321,7 @@ impl Extension {
resource: Resource<Arc<dyn WorktreeDelegate>>,
) -> Result<Result<Option<String>, String>> {
match self {
Extension::V040(ext) => {
Extension::V0_4_0(ext) => {
ext.call_language_server_workspace_configuration(
store,
&language_server_id.0,
@@ -329,7 +329,7 @@ impl Extension {
)
.await
}
Extension::V030(ext) => {
Extension::V0_3_0(ext) => {
ext.call_language_server_workspace_configuration(
store,
&language_server_id.0,
@@ -337,7 +337,7 @@ impl Extension {
)
.await
}
Extension::V020(ext) => {
Extension::V0_2_0(ext) => {
ext.call_language_server_workspace_configuration(
store,
&language_server_id.0,
@@ -345,7 +345,7 @@ impl Extension {
)
.await
}
Extension::V010(ext) => {
Extension::V0_1_0(ext) => {
ext.call_language_server_workspace_configuration(
store,
&language_server_id.0,
@@ -353,7 +353,7 @@ impl Extension {
)
.await
}
Extension::V006(ext) => {
Extension::V0_0_6(ext) => {
ext.call_language_server_workspace_configuration(
store,
&language_server_id.0,
@@ -361,7 +361,7 @@ impl Extension {
)
.await
}
Extension::V004(_) | Extension::V001(_) => Ok(Ok(None)),
Extension::V0_0_4(_) | Extension::V0_0_1(_) => Ok(Ok(None)),
}
}
@@ -373,7 +373,7 @@ impl Extension {
resource: Resource<Arc<dyn WorktreeDelegate>>,
) -> Result<Result<Option<String>, String>> {
match self {
Extension::V040(ext) => {
Extension::V0_4_0(ext) => {
ext.call_language_server_additional_initialization_options(
store,
&language_server_id.0,
@@ -382,12 +382,12 @@ impl Extension {
)
.await
}
Extension::V030(_)
| Extension::V020(_)
| Extension::V010(_)
| Extension::V006(_)
| Extension::V004(_)
| Extension::V001(_) => Ok(Ok(None)),
Extension::V0_3_0(_)
| Extension::V0_2_0(_)
| Extension::V0_1_0(_)
| Extension::V0_0_6(_)
| Extension::V0_0_4(_)
| Extension::V0_0_1(_) => Ok(Ok(None)),
}
}
@@ -399,7 +399,7 @@ impl Extension {
resource: Resource<Arc<dyn WorktreeDelegate>>,
) -> Result<Result<Option<String>, String>> {
match self {
Extension::V040(ext) => {
Extension::V0_4_0(ext) => {
ext.call_language_server_additional_workspace_configuration(
store,
&language_server_id.0,
@@ -408,12 +408,12 @@ impl Extension {
)
.await
}
Extension::V030(_)
| Extension::V020(_)
| Extension::V010(_)
| Extension::V006(_)
| Extension::V004(_)
| Extension::V001(_) => Ok(Ok(None)),
Extension::V0_3_0(_)
| Extension::V0_2_0(_)
| Extension::V0_1_0(_)
| Extension::V0_0_6(_)
| Extension::V0_0_4(_)
| Extension::V0_0_1(_) => Ok(Ok(None)),
}
}
@@ -424,11 +424,11 @@ impl Extension {
completions: Vec<latest::Completion>,
) -> Result<Result<Vec<Option<CodeLabel>>, String>> {
match self {
Extension::V040(ext) => {
Extension::V0_4_0(ext) => {
ext.call_labels_for_completions(store, &language_server_id.0, &completions)
.await
}
Extension::V030(ext) => Ok(ext
Extension::V0_3_0(ext) => Ok(ext
.call_labels_for_completions(
store,
&language_server_id.0,
@@ -441,7 +441,7 @@ impl Extension {
.map(|label| label.map(Into::into))
.collect()
})),
Extension::V020(ext) => Ok(ext
Extension::V0_2_0(ext) => Ok(ext
.call_labels_for_completions(
store,
&language_server_id.0,
@@ -454,7 +454,7 @@ impl Extension {
.map(|label| label.map(Into::into))
.collect()
})),
Extension::V010(ext) => Ok(ext
Extension::V0_1_0(ext) => Ok(ext
.call_labels_for_completions(
store,
&language_server_id.0,
@@ -467,7 +467,7 @@ impl Extension {
.map(|label| label.map(Into::into))
.collect()
})),
Extension::V006(ext) => Ok(ext
Extension::V0_0_6(ext) => Ok(ext
.call_labels_for_completions(
store,
&language_server_id.0,
@@ -480,7 +480,7 @@ impl Extension {
.map(|label| label.map(Into::into))
.collect()
})),
Extension::V001(_) | Extension::V004(_) => Ok(Ok(Vec::new())),
Extension::V0_0_1(_) | Extension::V0_0_4(_) => Ok(Ok(Vec::new())),
}
}
@@ -491,11 +491,11 @@ impl Extension {
symbols: Vec<latest::Symbol>,
) -> Result<Result<Vec<Option<CodeLabel>>, String>> {
match self {
Extension::V040(ext) => {
Extension::V0_4_0(ext) => {
ext.call_labels_for_symbols(store, &language_server_id.0, &symbols)
.await
}
Extension::V030(ext) => Ok(ext
Extension::V0_3_0(ext) => Ok(ext
.call_labels_for_symbols(
store,
&language_server_id.0,
@@ -508,7 +508,7 @@ impl Extension {
.map(|label| label.map(Into::into))
.collect()
})),
Extension::V020(ext) => Ok(ext
Extension::V0_2_0(ext) => Ok(ext
.call_labels_for_symbols(
store,
&language_server_id.0,
@@ -521,7 +521,7 @@ impl Extension {
.map(|label| label.map(Into::into))
.collect()
})),
Extension::V010(ext) => Ok(ext
Extension::V0_1_0(ext) => Ok(ext
.call_labels_for_symbols(
store,
&language_server_id.0,
@@ -534,7 +534,7 @@ impl Extension {
.map(|label| label.map(Into::into))
.collect()
})),
Extension::V006(ext) => Ok(ext
Extension::V0_0_6(ext) => Ok(ext
.call_labels_for_symbols(
store,
&language_server_id.0,
@@ -547,7 +547,7 @@ impl Extension {
.map(|label| label.map(Into::into))
.collect()
})),
Extension::V001(_) | Extension::V004(_) => Ok(Ok(Vec::new())),
Extension::V0_0_1(_) | Extension::V0_0_4(_) => Ok(Ok(Vec::new())),
}
}
@@ -558,23 +558,25 @@ impl Extension {
arguments: &[String],
) -> Result<Result<Vec<SlashCommandArgumentCompletion>, String>> {
match self {
Extension::V040(ext) => {
Extension::V0_4_0(ext) => {
ext.call_complete_slash_command_argument(store, command, arguments)
.await
}
Extension::V030(ext) => {
Extension::V0_3_0(ext) => {
ext.call_complete_slash_command_argument(store, command, arguments)
.await
}
Extension::V020(ext) => {
Extension::V0_2_0(ext) => {
ext.call_complete_slash_command_argument(store, command, arguments)
.await
}
Extension::V010(ext) => {
Extension::V0_1_0(ext) => {
ext.call_complete_slash_command_argument(store, command, arguments)
.await
}
Extension::V001(_) | Extension::V004(_) | Extension::V006(_) => Ok(Ok(Vec::new())),
Extension::V0_0_1(_) | Extension::V0_0_4(_) | Extension::V0_0_6(_) => {
Ok(Ok(Vec::new()))
}
}
}
@@ -586,23 +588,23 @@ impl Extension {
resource: Option<Resource<Arc<dyn WorktreeDelegate>>>,
) -> Result<Result<SlashCommandOutput, String>> {
match self {
Extension::V040(ext) => {
Extension::V0_4_0(ext) => {
ext.call_run_slash_command(store, command, arguments, resource)
.await
}
Extension::V030(ext) => {
Extension::V0_3_0(ext) => {
ext.call_run_slash_command(store, command, arguments, resource)
.await
}
Extension::V020(ext) => {
Extension::V0_2_0(ext) => {
ext.call_run_slash_command(store, command, arguments, resource)
.await
}
Extension::V010(ext) => {
Extension::V0_1_0(ext) => {
ext.call_run_slash_command(store, command, arguments, resource)
.await
}
Extension::V001(_) | Extension::V004(_) | Extension::V006(_) => {
Extension::V0_0_1(_) | Extension::V0_0_4(_) | Extension::V0_0_6(_) => {
Err(anyhow!("`run_slash_command` not available prior to v0.1.0"))
}
}
@@ -615,23 +617,24 @@ impl Extension {
project: Resource<ExtensionProject>,
) -> Result<Result<Command, String>> {
match self {
Extension::V040(ext) => {
Extension::V0_4_0(ext) => {
ext.call_context_server_command(store, &context_server_id, project)
.await
}
Extension::V030(ext) => {
Extension::V0_3_0(ext) => {
ext.call_context_server_command(store, &context_server_id, project)
.await
}
Extension::V020(ext) => Ok(ext
Extension::V0_2_0(ext) => Ok(ext
.call_context_server_command(store, &context_server_id, project)
.await?
.map(Into::into)),
Extension::V001(_) | Extension::V004(_) | Extension::V006(_) | Extension::V010(_) => {
Err(anyhow!(
"`context_server_command` not available prior to v0.2.0"
))
}
Extension::V0_0_1(_)
| Extension::V0_0_4(_)
| Extension::V0_0_6(_)
| Extension::V0_1_0(_) => Err(anyhow!(
"`context_server_command` not available prior to v0.2.0"
)),
}
}
@@ -641,11 +644,11 @@ impl Extension {
provider: &str,
) -> Result<Result<Vec<String>, String>> {
match self {
Extension::V040(ext) => ext.call_suggest_docs_packages(store, provider).await,
Extension::V030(ext) => ext.call_suggest_docs_packages(store, provider).await,
Extension::V020(ext) => ext.call_suggest_docs_packages(store, provider).await,
Extension::V010(ext) => ext.call_suggest_docs_packages(store, provider).await,
Extension::V001(_) | Extension::V004(_) | Extension::V006(_) => Err(anyhow!(
Extension::V0_4_0(ext) => ext.call_suggest_docs_packages(store, provider).await,
Extension::V0_3_0(ext) => ext.call_suggest_docs_packages(store, provider).await,
Extension::V0_2_0(ext) => ext.call_suggest_docs_packages(store, provider).await,
Extension::V0_1_0(ext) => ext.call_suggest_docs_packages(store, provider).await,
Extension::V0_0_1(_) | Extension::V0_0_4(_) | Extension::V0_0_6(_) => Err(anyhow!(
"`suggest_docs_packages` not available prior to v0.1.0"
)),
}
@@ -659,23 +662,23 @@ impl Extension {
kv_store: Resource<Arc<dyn KeyValueStoreDelegate>>,
) -> Result<Result<(), String>> {
match self {
Extension::V040(ext) => {
Extension::V0_4_0(ext) => {
ext.call_index_docs(store, provider, package_name, kv_store)
.await
}
Extension::V030(ext) => {
Extension::V0_3_0(ext) => {
ext.call_index_docs(store, provider, package_name, kv_store)
.await
}
Extension::V020(ext) => {
Extension::V0_2_0(ext) => {
ext.call_index_docs(store, provider, package_name, kv_store)
.await
}
Extension::V010(ext) => {
Extension::V0_1_0(ext) => {
ext.call_index_docs(store, provider, package_name, kv_store)
.await
}
Extension::V001(_) | Extension::V004(_) | Extension::V006(_) => {
Extension::V0_0_1(_) | Extension::V0_0_4(_) | Extension::V0_0_6(_) => {
Err(anyhow!("`index_docs` not available prior to v0.1.0"))
}
}

View File

@@ -40,7 +40,7 @@ objc = "0.2"
cocoa = "0.26"
[target.'cfg(not(target_os = "macos"))'.dependencies]
notify = "6.1.1"
notify = { git = "https://github.com/zed-industries/notify.git", rev = "bbb9ea5ae52b253e095737847e367c30653a2e96" }
[target.'cfg(target_os = "windows")'.dependencies]
windows.workspace = true

View File

@@ -5,8 +5,8 @@ use futures::future::{self, BoxFuture};
use git::{
blame::Blame,
repository::{
AskPassSession, Branch, CommitDetails, GitRepository, GitRepositoryCheckpoint, PushOptions,
Remote, RepoPath, ResetMode,
AskPassSession, Branch, CommitDetails, GitIndex, GitRepository, GitRepositoryCheckpoint,
PushOptions, Remote, RepoPath, ResetMode,
},
status::{FileStatus, GitStatus, StatusCode, TrackedStatus, UnmergedStatus},
};
@@ -57,12 +57,14 @@ impl FakeGitRepository {
where
F: FnOnce(&mut FakeGitRepositoryState) -> T,
{
self.fs.with_git_state(&self.dot_git_path, false, f)
self.fs
.with_git_state(&self.dot_git_path, false, f)
.unwrap()
}
fn with_state_async<F, T>(&self, write: bool, f: F) -> BoxFuture<T>
fn with_state_async<F, T>(&self, write: bool, f: F) -> BoxFuture<'static, Result<T>>
where
F: 'static + Send + FnOnce(&mut FakeGitRepositoryState) -> T,
F: 'static + Send + FnOnce(&mut FakeGitRepositoryState) -> Result<T>,
T: Send,
{
let fs = self.fs.clone();
@@ -70,7 +72,7 @@ impl FakeGitRepository {
let dot_git_path = self.dot_git_path.clone();
async move {
executor.simulate_random_delay().await;
fs.with_git_state(&dot_git_path, write, f)
fs.with_git_state(&dot_git_path, write, f)?
}
.boxed()
}
@@ -79,16 +81,38 @@ impl FakeGitRepository {
impl GitRepository for FakeGitRepository {
fn reload_index(&self) {}
fn load_index_text(&self, path: RepoPath, _cx: AsyncApp) -> BoxFuture<Option<String>> {
self.with_state_async(false, move |state| {
state.index_contents.get(path.as_ref()).cloned()
})
fn load_index_text(
&self,
index: Option<GitIndex>,
path: RepoPath,
) -> BoxFuture<Option<String>> {
async {
self.with_state_async(false, move |state| {
state
.index_contents
.get(path.as_ref())
.ok_or_else(|| anyhow!("not present in index"))
.cloned()
})
.await
.ok()
}
.boxed()
}
fn load_committed_text(&self, path: RepoPath, _cx: AsyncApp) -> BoxFuture<Option<String>> {
self.with_state_async(false, move |state| {
state.head_contents.get(path.as_ref()).cloned()
})
fn load_committed_text(&self, path: RepoPath) -> BoxFuture<Option<String>> {
async {
self.with_state_async(false, move |state| {
state
.head_contents
.get(path.as_ref())
.ok_or_else(|| anyhow!("not present in HEAD"))
.cloned()
})
.await
.ok()
}
.boxed()
}
fn set_index_text(
@@ -96,7 +120,6 @@ impl GitRepository for FakeGitRepository {
path: RepoPath,
content: Option<String>,
_env: HashMap<String, String>,
_cx: AsyncApp,
) -> BoxFuture<anyhow::Result<()>> {
self.with_state_async(true, move |state| {
if let Some(message) = state.simulated_index_write_error_message.clone() {
@@ -122,7 +145,7 @@ impl GitRepository for FakeGitRepository {
vec![]
}
fn show(&self, _commit: String, _cx: AsyncApp) -> BoxFuture<Result<CommitDetails>> {
fn show(&self, _commit: String) -> BoxFuture<Result<CommitDetails>> {
unimplemented!()
}
@@ -152,7 +175,16 @@ impl GitRepository for FakeGitRepository {
self.path()
}
fn status(&self, path_prefixes: &[RepoPath]) -> Result<GitStatus> {
fn status(
&self,
index: Option<GitIndex>,
path_prefixes: &[RepoPath],
) -> BoxFuture<'static, Result<GitStatus>> {
let status = self.status_blocking(path_prefixes);
async move { status }.boxed()
}
fn status_blocking(&self, path_prefixes: &[RepoPath]) -> Result<GitStatus> {
let workdir_path = self.dot_git_path.parent().unwrap();
// Load gitignores
@@ -194,7 +226,7 @@ impl GitRepository for FakeGitRepository {
})
.collect();
self.with_state(|state| {
self.fs.with_git_state(&self.dot_git_path, false, |state| {
let mut entries = Vec::new();
let paths = state
.head_contents
@@ -278,7 +310,7 @@ impl GitRepository for FakeGitRepository {
Ok(GitStatus {
entries: entries.into(),
})
})
})?
}
fn branches(&self) -> BoxFuture<Result<Vec<Branch>>> {
@@ -297,26 +329,21 @@ impl GitRepository for FakeGitRepository {
})
}
fn change_branch(&self, name: String, _cx: AsyncApp) -> BoxFuture<Result<()>> {
fn change_branch(&self, name: String) -> BoxFuture<Result<()>> {
self.with_state_async(true, |state| {
state.current_branch_name = Some(name);
Ok(())
})
}
fn create_branch(&self, name: String, _: AsyncApp) -> BoxFuture<Result<()>> {
fn create_branch(&self, name: String) -> BoxFuture<Result<()>> {
self.with_state_async(true, move |state| {
state.branches.insert(name.to_owned());
Ok(())
})
}
fn blame(
&self,
path: RepoPath,
_content: Rope,
_cx: &mut AsyncApp,
) -> BoxFuture<Result<git::blame::Blame>> {
fn blame(&self, path: RepoPath, _content: Rope) -> BoxFuture<Result<git::blame::Blame>> {
self.with_state_async(false, move |state| {
state
.blames
@@ -330,7 +357,6 @@ impl GitRepository for FakeGitRepository {
&self,
_paths: Vec<RepoPath>,
_env: HashMap<String, String>,
_cx: AsyncApp,
) -> BoxFuture<Result<()>> {
unimplemented!()
}
@@ -339,7 +365,6 @@ impl GitRepository for FakeGitRepository {
&self,
_paths: Vec<RepoPath>,
_env: HashMap<String, String>,
_cx: AsyncApp,
) -> BoxFuture<Result<()>> {
unimplemented!()
}
@@ -349,7 +374,6 @@ impl GitRepository for FakeGitRepository {
_message: gpui::SharedString,
_name_and_email: Option<(gpui::SharedString, gpui::SharedString)>,
_env: HashMap<String, String>,
_cx: AsyncApp,
) -> BoxFuture<Result<()>> {
unimplemented!()
}
@@ -386,38 +410,23 @@ impl GitRepository for FakeGitRepository {
unimplemented!()
}
fn get_remotes(
&self,
_branch: Option<String>,
_cx: AsyncApp,
) -> BoxFuture<Result<Vec<Remote>>> {
fn get_remotes(&self, _branch: Option<String>) -> BoxFuture<Result<Vec<Remote>>> {
unimplemented!()
}
fn check_for_pushed_commit(
&self,
_cx: gpui::AsyncApp,
) -> BoxFuture<Result<Vec<gpui::SharedString>>> {
fn check_for_pushed_commit(&self) -> BoxFuture<Result<Vec<gpui::SharedString>>> {
future::ready(Ok(Vec::new())).boxed()
}
fn diff(
&self,
_diff: git::repository::DiffType,
_cx: gpui::AsyncApp,
) -> BoxFuture<Result<String>> {
fn diff(&self, _diff: git::repository::DiffType) -> BoxFuture<Result<String>> {
unimplemented!()
}
fn checkpoint(&self, _cx: AsyncApp) -> BoxFuture<Result<GitRepositoryCheckpoint>> {
fn checkpoint(&self) -> BoxFuture<'static, Result<GitRepositoryCheckpoint>> {
unimplemented!()
}
fn restore_checkpoint(
&self,
_checkpoint: GitRepositoryCheckpoint,
_cx: AsyncApp,
) -> BoxFuture<Result<()>> {
fn restore_checkpoint(&self, _checkpoint: GitRepositoryCheckpoint) -> BoxFuture<Result<()>> {
unimplemented!()
}
@@ -425,16 +434,27 @@ impl GitRepository for FakeGitRepository {
&self,
_left: GitRepositoryCheckpoint,
_right: GitRepositoryCheckpoint,
_cx: AsyncApp,
) -> BoxFuture<Result<bool>> {
unimplemented!()
}
fn delete_checkpoint(
fn delete_checkpoint(&self, _checkpoint: GitRepositoryCheckpoint) -> BoxFuture<Result<()>> {
unimplemented!()
}
fn diff_checkpoints(
&self,
_checkpoint: GitRepositoryCheckpoint,
_cx: AsyncApp,
) -> BoxFuture<Result<()>> {
_base_checkpoint: GitRepositoryCheckpoint,
_target_checkpoint: GitRepositoryCheckpoint,
) -> BoxFuture<Result<String>> {
unimplemented!()
}
fn create_index(&self) -> BoxFuture<Result<GitIndex>> {
unimplemented!()
}
fn apply_diff(&self, _index: GitIndex, _diff: String) -> BoxFuture<Result<()>> {
unimplemented!()
}
}

View File

@@ -8,6 +8,7 @@ use anyhow::{anyhow, Context as _, Result};
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
use ashpd::desktop::trash;
use gpui::App;
use gpui::BackgroundExecutor;
use gpui::Global;
use gpui::ReadGlobal as _;
use std::borrow::Cow;
@@ -240,9 +241,9 @@ impl From<MTime> for proto::Timestamp {
}
}
#[derive(Default)]
pub struct RealFs {
git_binary_path: Option<PathBuf>,
executor: BackgroundExecutor,
}
pub trait FileHandle: Send + Sync + std::fmt::Debug {
@@ -294,8 +295,11 @@ impl FileHandle for std::fs::File {
pub struct RealWatcher {}
impl RealFs {
pub fn new(git_binary_path: Option<PathBuf>) -> Self {
Self { git_binary_path }
pub fn new(git_binary_path: Option<PathBuf>, executor: BackgroundExecutor) -> Self {
Self {
git_binary_path,
executor,
}
}
}
@@ -754,6 +758,7 @@ impl Fs for RealFs {
Some(Arc::new(RealGitRepository::new(
dotgit_path,
self.git_binary_path.clone(),
self.executor.clone(),
)?))
}
@@ -1248,12 +1253,12 @@ impl FakeFs {
.boxed()
}
pub fn with_git_state<T, F>(&self, dot_git: &Path, emit_git_event: bool, f: F) -> T
pub fn with_git_state<T, F>(&self, dot_git: &Path, emit_git_event: bool, f: F) -> Result<T>
where
F: FnOnce(&mut FakeGitRepositoryState) -> T,
{
let mut state = self.state.lock();
let entry = state.read_path(dot_git).unwrap();
let entry = state.read_path(dot_git).context("open .git")?;
let mut entry = entry.lock();
if let FakeFsEntry::Dir { git_repo_state, .. } = &mut *entry {
@@ -1271,9 +1276,9 @@ impl FakeFs {
state.emit_event([(dot_git, None)]);
}
result
Ok(result)
} else {
panic!("not a directory");
Err(anyhow!("not a directory"))
}
}
@@ -1283,6 +1288,7 @@ impl FakeFs {
state.branches.extend(branch.clone());
state.current_branch_name = branch
})
.unwrap();
}
pub fn insert_branches(&self, dot_git: &Path, branches: &[&str]) {
@@ -1296,6 +1302,7 @@ impl FakeFs {
.branches
.extend(branches.iter().map(ToString::to_string));
})
.unwrap();
}
pub fn set_unmerged_paths_for_repo(
@@ -1310,7 +1317,8 @@ impl FakeFs {
.iter()
.map(|(path, content)| (path.clone(), *content)),
);
});
})
.unwrap();
}
pub fn set_index_for_repo(&self, dot_git: &Path, index_state: &[(RepoPath, String)]) {
@@ -1321,7 +1329,8 @@ impl FakeFs {
.iter()
.map(|(path, content)| (path.clone(), content.clone())),
);
});
})
.unwrap();
}
pub fn set_head_for_repo(&self, dot_git: &Path, head_state: &[(RepoPath, String)]) {
@@ -1332,7 +1341,8 @@ impl FakeFs {
.iter()
.map(|(path, content)| (path.clone(), content.clone())),
);
});
})
.unwrap();
}
pub fn set_git_content_for_repo(
@@ -1356,7 +1366,8 @@ impl FakeFs {
)
},
));
});
})
.unwrap();
}
pub fn set_head_and_index_for_repo(
@@ -1371,14 +1382,16 @@ impl FakeFs {
state
.index_contents
.extend(contents_by_path.iter().cloned());
});
})
.unwrap();
}
pub fn set_blame_for_repo(&self, dot_git: &Path, blames: Vec<(RepoPath, git::blame::Blame)>) {
self.with_git_state(dot_git, true, |state| {
state.blames.clear();
state.blames.extend(blames);
});
})
.unwrap();
}
/// Put the given git repository into a state with the given status,
@@ -1460,13 +1473,14 @@ impl FakeFs {
state.head_contents.insert(repo_path.clone(), content);
}
}
});
}).unwrap();
}
pub fn set_error_message_for_index_write(&self, dot_git: &Path, message: Option<String>) {
self.with_git_state(dot_git, true, |state| {
state.simulated_index_write_error_message = message;
});
})
.unwrap();
}
pub fn paths(&self, include_dot_git: bool) -> Vec<PathBuf> {

View File

@@ -105,7 +105,14 @@ static FS_WATCHER_INSTANCE: OnceLock<anyhow::Result<GlobalWatcher, notify::Error
OnceLock::new();
fn handle_event(event: Result<notify::Event, notify::Error>) {
let Some(event) = event.log_err() else { return };
// Filter out access events, which could lead to a weird bug on Linux after upgrading notify
// https://github.com/zed-industries/zed/actions/runs/14085230504/job/39449448832
let Some(event) = event
.log_err()
.filter(|event| !matches!(event.kind, EventKind::Access(_)))
else {
return;
};
global::<()>(move |watcher| {
for f in watcher.watchers.lock().iter() {
f(&event)

View File

@@ -11,7 +11,9 @@ use anyhow::{anyhow, Context as _, Result};
pub use git2 as libgit;
use gpui::action_with_deprecated_aliases;
use gpui::actions;
use gpui::impl_action_with_deprecated_aliases;
pub use repository::WORK_DIRECTORY_REPO_PATH;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::ffi::OsStr;
use std::fmt;
@@ -54,7 +56,13 @@ actions!(
]
);
action_with_deprecated_aliases!(git, RestoreFile, ["editor::RevertFile"]);
#[derive(Clone, Debug, Default, PartialEq, Deserialize, JsonSchema)]
pub struct RestoreFile {
#[serde(default)]
pub skip_prompt: bool,
}
impl_action_with_deprecated_aliases!(git, RestoreFile, ["editor::RevertFile"]);
action_with_deprecated_aliases!(git, Restore, ["editor::RevertSelectedHunks"]);
action_with_deprecated_aliases!(git, Blame, ["editor::ToggleGitBlame"]);

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,7 @@
use crate::repository::RepoPath;
use anyhow::{anyhow, Result};
use serde::{Deserialize, Serialize};
use std::{path::Path, process::Stdio, sync::Arc};
use std::{path::Path, str::FromStr, sync::Arc};
use util::ResultExt;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
@@ -438,50 +438,16 @@ impl std::ops::Sub for GitSummary {
}
}
#[derive(Clone)]
#[derive(Clone, Debug)]
pub struct GitStatus {
pub entries: Arc<[(RepoPath, FileStatus)]>,
}
impl GitStatus {
pub(crate) fn new(
git_binary: &Path,
working_directory: &Path,
path_prefixes: &[RepoPath],
) -> Result<Self> {
let child = util::command::new_std_command(git_binary)
.current_dir(working_directory)
.args([
"--no-optional-locks",
"status",
"--porcelain=v1",
"--untracked-files=all",
"--no-renames",
"-z",
])
.args(path_prefixes.iter().map(|path_prefix| {
if path_prefix.0.as_ref() == Path::new("") {
Path::new(".")
} else {
path_prefix
}
}))
.stdin(Stdio::null())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.map_err(|e| anyhow!("Failed to start git status process: {e}"))?;
impl FromStr for GitStatus {
type Err = anyhow::Error;
let output = child
.wait_with_output()
.map_err(|e| anyhow!("Failed to read git status output: {e}"))?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(anyhow!("git status process failed: {stderr}"));
}
let stdout = String::from_utf8_lossy(&output.stdout);
let mut entries = stdout
fn from_str(s: &str) -> Result<Self> {
let mut entries = s
.split('\0')
.filter_map(|entry| {
let sep = entry.get(2..3)?;

View File

@@ -3,7 +3,7 @@ use crate::commit_modal::CommitModal;
use crate::git_panel_settings::StatusStyle;
use crate::project_diff::Diff;
use crate::remote_output::{self, RemoteAction, SuccessMessage};
use crate::repository_selector::filtered_repository_entries;
use crate::{branch_picker, render_remote_button};
use crate::{
git_panel_settings::GitPanelSettings, git_status_icon, repository_selector::RepositorySelector,
@@ -63,7 +63,7 @@ use ui::{
Tooltip,
};
use util::{maybe, post_inc, ResultExt, TryFutureExt};
use workspace::{AppState, OpenOptions, OpenVisible};
use workspace::AppState;
use notifications::status_toast::{StatusToast, ToastIcon};
use workspace::{
@@ -195,7 +195,6 @@ impl GitListEntry {
#[derive(Debug, PartialEq, Eq, Clone)]
pub struct GitStatusEntry {
pub(crate) repo_path: RepoPath,
pub(crate) worktree_path: Arc<Path>,
pub(crate) abs_path: PathBuf,
pub(crate) status: FileStatus,
pub(crate) staging: StageStatus,
@@ -203,14 +202,14 @@ pub struct GitStatusEntry {
impl GitStatusEntry {
fn display_name(&self) -> String {
self.worktree_path
self.repo_path
.file_name()
.map(|name| name.to_string_lossy().into_owned())
.unwrap_or_else(|| self.worktree_path.to_string_lossy().into_owned())
.unwrap_or_else(|| self.repo_path.to_string_lossy().into_owned())
}
fn parent_dir(&self) -> Option<String> {
self.worktree_path
self.repo_path
.parent()
.map(|parent| parent.to_string_lossy().into_owned())
}
@@ -652,7 +651,7 @@ impl GitPanel {
let Some(git_repo) = self.active_repository.as_ref() else {
return;
};
let Some(repo_path) = git_repo.read(cx).project_path_to_repo_path(&path) else {
let Some(repo_path) = git_repo.read(cx).project_path_to_repo_path(&path, cx) else {
return;
};
let Some(ix) = self.entry_by_path(&repo_path) else {
@@ -865,7 +864,7 @@ impl GitPanel {
if Some(&entry.repo_path)
== git_repo
.read(cx)
.project_path_to_repo_path(&project_path)
.project_path_to_repo_path(&project_path, cx)
.as_ref()
{
project_diff.focus_handle(cx).focus(window);
@@ -875,31 +874,12 @@ impl GitPanel {
}
};
if entry.worktree_path.starts_with("..") {
self.workspace
.update(cx, |workspace, cx| {
workspace
.open_abs_path(
entry.abs_path.clone(),
OpenOptions {
visible: Some(OpenVisible::All),
focus: Some(false),
..Default::default()
},
window,
cx,
)
.detach_and_log_err(cx);
})
.ok();
} else {
self.workspace
.update(cx, |workspace, cx| {
ProjectDiff::deploy_at(workspace, Some(entry.clone()), window, cx);
})
.ok();
self.focus_handle.focus(window);
}
self.workspace
.update(cx, |workspace, cx| {
ProjectDiff::deploy_at(workspace, Some(entry.clone()), window, cx);
})
.ok();
self.focus_handle.focus(window);
Some(())
});
@@ -916,7 +896,7 @@ impl GitPanel {
let active_repo = self.active_repository.as_ref()?;
let path = active_repo
.read(cx)
.repo_path_to_project_path(&entry.repo_path)?;
.repo_path_to_project_path(&entry.repo_path, cx)?;
if entry.status.is_deleted() {
return None;
}
@@ -935,14 +915,49 @@ impl GitPanel {
fn revert_selected(
&mut self,
_: &git::RestoreFile,
action: &git::RestoreFile,
window: &mut Window,
cx: &mut Context<Self>,
) {
maybe!({
let skip_prompt = action.skip_prompt;
let list_entry = self.entries.get(self.selected_entry?)?.clone();
let entry = list_entry.status_entry()?;
self.revert_entry(&entry, window, cx);
let entry = list_entry.status_entry()?.to_owned();
let prompt = if skip_prompt {
Task::ready(Ok(0))
} else {
let prompt = window.prompt(
PromptLevel::Warning,
&format!(
"Are you sure you want to restore {}?",
entry
.repo_path
.file_name()
.unwrap_or(entry.repo_path.as_os_str())
.to_string_lossy()
),
None,
&["Restore", "Cancel"],
cx,
);
cx.background_spawn(prompt)
};
let this = cx.weak_entity();
window
.spawn(cx, async move |cx| {
if prompt.await? != 0 {
return anyhow::Ok(());
}
this.update_in(cx, |this, window, cx| {
this.revert_entry(&entry, window, cx);
})?;
Ok(())
})
.detach();
Some(())
});
}
@@ -957,7 +972,7 @@ impl GitPanel {
let active_repo = self.active_repository.clone()?;
let path = active_repo
.read(cx)
.repo_path_to_project_path(&entry.repo_path)?;
.repo_path_to_project_path(&entry.repo_path, cx)?;
let workspace = self.workspace.clone();
if entry.status.staging().has_staged() {
@@ -1017,7 +1032,7 @@ impl GitPanel {
.filter_map(|entry| {
let path = active_repository
.read(cx)
.repo_path_to_project_path(&entry.repo_path)?;
.repo_path_to_project_path(&entry.repo_path, cx)?;
Some(project.open_buffer(path, cx))
})
.collect()
@@ -1183,7 +1198,7 @@ impl GitPanel {
workspace.project().update(cx, |project, cx| {
let project_path = active_repo
.read(cx)
.repo_path_to_project_path(&entry.repo_path)?;
.repo_path_to_project_path(&entry.repo_path, cx)?;
project.delete_file(project_path, true, cx)
})
})
@@ -2244,7 +2259,7 @@ impl GitPanel {
let repo = repo.read(cx);
for entry in repo.status() {
for entry in repo.cached_status() {
let is_conflict = repo.has_conflict(&entry.repo_path);
let is_new = entry.status.is_created();
let staging = entry.status.staging();
@@ -2260,16 +2275,12 @@ impl GitPanel {
continue;
}
// dot_git_abs path always has at least one component, namely .git.
let abs_path = repo
.dot_git_abs_path
.parent()
.unwrap()
.join(&entry.repo_path);
let worktree_path = repo.repository_entry.unrelativize(&entry.repo_path);
.repository_entry
.work_directory_abs_path
.join(&entry.repo_path.0);
let entry = GitStatusEntry {
repo_path: entry.repo_path.clone(),
worktree_path,
abs_path,
status: entry.status,
staging,
@@ -2495,7 +2506,6 @@ impl GitPanel {
{
return; // Hide the cancelled by user message
} else {
let project = self.project.clone();
workspace.update(cx, |workspace, cx| {
let workspace_weak = cx.weak_entity();
let toast =
@@ -2503,13 +2513,10 @@ impl GitPanel {
this.icon(ToastIcon::new(IconName::XCircle).color(Color::Error))
.action("View Log", move |window, cx| {
let message = message.clone();
let project = project.clone();
let action = action.clone();
workspace_weak
.update(cx, move |workspace, cx| {
Self::open_output(
project, action, workspace, &message, window, cx,
)
Self::open_output(action, workspace, &message, window, cx)
})
.ok();
})
@@ -2531,21 +2538,17 @@ impl GitPanel {
let status_toast = StatusToast::new(message, cx, move |this, _cx| {
use remote_output::SuccessStyle::*;
let project = self.project.clone();
match style {
Toast { .. } => this,
ToastWithLog { output } => this
.icon(ToastIcon::new(IconName::GitBranchSmall).color(Color::Muted))
.action("View Log", move |window, cx| {
let output = output.clone();
let project = project.clone();
let output =
format!("stdout:\n{}\nstderr:\n{}", output.stdout, output.stderr);
workspace_weak
.update(cx, move |workspace, cx| {
Self::open_output(
project, operation, workspace, &output, window, cx,
)
Self::open_output(operation, workspace, &output, window, cx)
})
.ok();
}),
@@ -2559,7 +2562,6 @@ impl GitPanel {
}
fn open_output(
project: Entity<Project>,
operation: impl Into<SharedString>,
workspace: &mut Workspace,
output: &str,
@@ -2568,8 +2570,11 @@ impl GitPanel {
) {
let operation = operation.into();
let buffer = cx.new(|cx| Buffer::local(output, cx));
buffer.update(cx, |buffer, cx| {
buffer.set_capability(language::Capability::ReadOnly, cx);
});
let editor = cx.new(|cx| {
let mut editor = Editor::for_buffer(buffer, Some(project), window, cx);
let mut editor = Editor::for_buffer(buffer, None, window, cx);
editor.buffer().update(cx, |buffer, cx| {
buffer.set_title(format!("Output from git {operation}"), cx);
});
@@ -2854,7 +2859,6 @@ impl GitPanel {
) -> Option<impl IntoElement> {
let active_repository = self.active_repository.clone()?;
let (can_commit, tooltip) = self.configure_commit_button(cx);
let project = self.project.clone().read(cx);
let panel_editor_style = panel_editor_style(true, window, cx);
let enable_coauthors = self.render_co_authors(cx);
@@ -2878,7 +2882,7 @@ impl GitPanel {
let display_name = SharedString::from(Arc::from(
active_repository
.read(cx)
.display_name(project, cx)
.display_name()
.trim_end_matches("/"),
));
let editor_is_long = self.commit_editor.update(cx, |editor, cx| {
@@ -3207,7 +3211,8 @@ impl GitPanel {
cx: &App,
) -> Option<AnyElement> {
let repo = self.active_repository.as_ref()?.read(cx);
let repo_path = repo.worktree_id_path_to_repo_path(file.worktree_id(cx), file.path())?;
let project_path = (file.worktree_id(cx), file.path()).into();
let repo_path = repo.project_path_to_repo_path(&project_path, cx)?;
let ix = self.entry_by_path(&repo_path)?;
let entry = self.entries.get(ix)?;
@@ -3466,7 +3471,7 @@ impl GitPanel {
context_menu
.context(self.focus_handle.clone())
.action(stage_title, ToggleStaged.boxed_clone())
.action(restore_title, git::RestoreFile.boxed_clone())
.action(restore_title, git::RestoreFile::default().boxed_clone())
.separator()
.action("Open Diff", Confirm.boxed_clone())
.action("Open File", SecondaryConfirm.boxed_clone())
@@ -4027,9 +4032,7 @@ impl RenderOnce for PanelRepoFooter {
let single_repo = project
.as_ref()
.map(|project| {
filtered_repository_entries(project.read(cx).git_store().read(cx), cx).len() == 1
})
.map(|project| project.read(cx).git_store().read(cx).repositories().len() == 1)
.unwrap_or(true);
const MAX_BRANCH_LEN: usize = 16;
@@ -4529,66 +4532,65 @@ mod tests {
GitListEntry::GitStatusEntry(GitStatusEntry {
abs_path: path!("/root/zed/crates/gpui/gpui.rs").into(),
repo_path: "crates/gpui/gpui.rs".into(),
worktree_path: Path::new("gpui.rs").into(),
status: StatusCode::Modified.worktree(),
staging: StageStatus::Unstaged,
}),
GitListEntry::GitStatusEntry(GitStatusEntry {
abs_path: path!("/root/zed/crates/util/util.rs").into(),
repo_path: "crates/util/util.rs".into(),
worktree_path: Path::new("../util/util.rs").into(),
status: StatusCode::Modified.worktree(),
staging: StageStatus::Unstaged,
},),
],
);
cx.update_window_entity(&panel, |panel, window, cx| {
panel.select_last(&Default::default(), window, cx);
assert_eq!(panel.selected_entry, Some(2));
panel.open_diff(&Default::default(), window, cx);
});
cx.run_until_parked();
// TODO(cole) restore this once repository deduplication is implemented properly.
//cx.update_window_entity(&panel, |panel, window, cx| {
// panel.select_last(&Default::default(), window, cx);
// assert_eq!(panel.selected_entry, Some(2));
// panel.open_diff(&Default::default(), window, cx);
//});
//cx.run_until_parked();
let worktree_roots = workspace.update(cx, |workspace, cx| {
workspace
.worktrees(cx)
.map(|worktree| worktree.read(cx).abs_path())
.collect::<Vec<_>>()
});
pretty_assertions::assert_eq!(
worktree_roots,
vec![
Path::new(path!("/root/zed/crates/gpui")).into(),
Path::new(path!("/root/zed/crates/util/util.rs")).into(),
]
);
//let worktree_roots = workspace.update(cx, |workspace, cx| {
// workspace
// .worktrees(cx)
// .map(|worktree| worktree.read(cx).abs_path())
// .collect::<Vec<_>>()
//});
//pretty_assertions::assert_eq!(
// worktree_roots,
// vec![
// Path::new(path!("/root/zed/crates/gpui")).into(),
// Path::new(path!("/root/zed/crates/util/util.rs")).into(),
// ]
//);
project.update(cx, |project, cx| {
let git_store = project.git_store().read(cx);
// The repo that comes from the single-file worktree can't be selected through the UI.
let filtered_entries = filtered_repository_entries(git_store, cx)
.iter()
.map(|repo| repo.read(cx).worktree_abs_path.clone())
.collect::<Vec<_>>();
assert_eq!(
filtered_entries,
[Path::new(path!("/root/zed/crates/gpui")).into()]
);
// But we can select it artificially here.
let repo_from_single_file_worktree = git_store
.repositories()
.values()
.find(|repo| {
repo.read(cx).worktree_abs_path.as_ref()
== Path::new(path!("/root/zed/crates/util/util.rs"))
})
.unwrap()
.clone();
//project.update(cx, |project, cx| {
// let git_store = project.git_store().read(cx);
// // The repo that comes from the single-file worktree can't be selected through the UI.
// let filtered_entries = filtered_repository_entries(git_store, cx)
// .iter()
// .map(|repo| repo.read(cx).worktree_abs_path.clone())
// .collect::<Vec<_>>();
// assert_eq!(
// filtered_entries,
// [Path::new(path!("/root/zed/crates/gpui")).into()]
// );
// // But we can select it artificially here.
// let repo_from_single_file_worktree = git_store
// .repositories()
// .values()
// .find(|repo| {
// repo.read(cx).worktree_abs_path.as_ref()
// == Path::new(path!("/root/zed/crates/util/util.rs"))
// })
// .unwrap()
// .clone();
// Paths still make sense when we somehow activate a repo that comes from a single-file worktree.
repo_from_single_file_worktree.update(cx, |repo, cx| repo.set_as_active_repository(cx));
});
// // Paths still make sense when we somehow activate a repo that comes from a single-file worktree.
// repo_from_single_file_worktree.update(cx, |repo, cx| repo.set_as_active_repository(cx));
//});
let handle = cx.update_window_entity(&panel, |panel, _, _| {
std::mem::replace(&mut panel.update_visible_entries_task, Task::ready(()))
@@ -4605,14 +4607,12 @@ mod tests {
GitListEntry::GitStatusEntry(GitStatusEntry {
abs_path: path!("/root/zed/crates/gpui/gpui.rs").into(),
repo_path: "crates/gpui/gpui.rs".into(),
worktree_path: Path::new("../../gpui/gpui.rs").into(),
status: StatusCode::Modified.worktree(),
staging: StageStatus::Unstaged,
}),
GitListEntry::GitStatusEntry(GitStatusEntry {
abs_path: path!("/root/zed/crates/util/util.rs").into(),
repo_path: "crates/util/util.rs".into(),
worktree_path: Path::new("util.rs").into(),
status: StatusCode::Modified.worktree(),
staging: StageStatus::Unstaged,
},),

View File

@@ -26,7 +26,11 @@ use project::{
git_store::{GitEvent, GitStore},
Project, ProjectPath,
};
use std::any::{Any, TypeId};
use std::{
any::{Any, TypeId},
path::Path,
sync::Arc,
};
use theme::ActiveTheme;
use ui::{prelude::*, vertical_divider, KeyBinding, Tooltip};
use util::ResultExt as _;
@@ -39,7 +43,47 @@ use workspace::{
actions!(git, [Diff, Add]);
pub trait DiffSource {
// todo!("return a struct here")
fn status(&self, cx: &App) -> Vec<(ProjectPath, FileStatus, bool)>;
fn open_uncommitted_diff(
&self,
buffer: Entity<Buffer>,
cx: &mut App,
) -> Task<Result<Entity<BufferDiff>>>;
// todo!("add an observe method")
}
pub struct ProjectDiffSource(Entity<Project>);
impl DiffSource for ProjectDiffSource {
fn status(&self, cx: &App) -> Vec<(ProjectPath, FileStatus, bool)> {
let mut result = Vec::new();
if let Some(git_repo) = self.0.read(cx).git_store().read(cx).active_repository() {
let git_repo = git_repo.read(cx);
for entry in git_repo.cached_status() {
if let Some(project_path) = git_repo.repo_path_to_project_path(&entry.repo_path, cx)
{
let has_conflict = git_repo.has_conflict(&entry.repo_path);
result.push((project_path, entry.status, has_conflict));
}
}
}
result
}
fn open_uncommitted_diff(
&self,
buffer: Entity<Buffer>,
cx: &mut App,
) -> Task<Result<Entity<BufferDiff>>> {
self.0
.update(cx, |project, cx| project.open_uncommitted_diff(buffer, cx))
}
}
pub struct ProjectDiff {
source: Arc<dyn DiffSource>,
project: Entity<Project>,
multibuffer: Entity<MultiBuffer>,
editor: Entity<Editor>,
@@ -102,8 +146,16 @@ impl ProjectDiff {
existing
} else {
let workspace_handle = cx.entity();
let project_diff =
cx.new(|cx| Self::new(workspace.project().clone(), workspace_handle, window, cx));
let source = Arc::new(ProjectDiffSource(workspace.project().clone()));
let project_diff = cx.new(|cx| {
Self::new(
source,
workspace.project().clone(),
workspace_handle,
window,
cx,
)
});
workspace.add_item_to_active_pane(
Box::new(project_diff.clone()),
None,
@@ -127,6 +179,7 @@ impl ProjectDiff {
}
fn new(
source: Arc<dyn DiffSource>,
project: Entity<Project>,
workspace: Entity<Workspace>,
window: &mut Window,
@@ -171,6 +224,7 @@ impl ProjectDiff {
*send.borrow_mut() = ();
Self {
source,
project,
git_store: git_store.clone(),
workspace: workspace.downgrade(),
@@ -328,55 +382,53 @@ impl ProjectDiff {
}
fn load_buffers(&mut self, cx: &mut Context<Self>) -> Vec<Task<Result<DiffBuffer>>> {
let Some(repo) = self.git_store.read(cx).active_repository() else {
self.multibuffer.update(cx, |multibuffer, cx| {
multibuffer.clear(cx);
});
return vec![];
};
let mut previous_paths = self.multibuffer.read(cx).paths().collect::<HashSet<_>>();
let mut result = vec![];
repo.update(cx, |repo, cx| {
for entry in repo.status() {
if !entry.status.has_changes() {
continue;
}
let Some(project_path) = repo.repo_path_to_project_path(&entry.repo_path) else {
continue;
};
let namespace = if repo.has_conflict(&entry.repo_path) {
CONFLICT_NAMESPACE
} else if entry.status.is_created() {
NEW_NAMESPACE
} else {
TRACKED_NAMESPACE
};
let path_key = PathKey::namespaced(namespace, entry.repo_path.0.clone());
previous_paths.remove(&path_key);
let load_buffer = self
.project
.update(cx, |project, cx| project.open_buffer(project_path, cx));
let project = self.project.clone();
result.push(cx.spawn(async move |_, cx| {
let buffer = load_buffer.await?;
let changes = project
.update(cx, |project, cx| {
project.open_uncommitted_diff(buffer.clone(), cx)
})?
.await?;
Ok(DiffBuffer {
path_key,
buffer,
diff: changes,
file_status: entry.status,
})
}));
for (project_path, status, has_conflict) in self.source.status(cx) {
if !status.has_changes() {
continue;
}
});
let Some(worktree) = self
.project
.read(cx)
.worktree_for_id(project_path.worktree_id, cx)
else {
continue;
};
let full_path =
Arc::from(Path::new(worktree.read(cx).root_name()).join(&project_path.path));
let namespace = if has_conflict {
CONFLICT_NAMESPACE
} else if status.is_created() {
NEW_NAMESPACE
} else {
TRACKED_NAMESPACE
};
let path_key = PathKey::namespaced(namespace, full_path);
previous_paths.remove(&path_key);
let load_buffer = self
.project
.update(cx, |project, cx| project.open_buffer(project_path, cx));
let source = self.source.clone();
result.push(cx.spawn(async move |_, cx| {
let buffer = load_buffer.await?;
let changes = cx
.update(|cx| source.open_uncommitted_diff(buffer.clone(), cx))?
.await?;
Ok(DiffBuffer {
path_key,
buffer,
diff: changes,
file_status: status,
})
}));
}
self.multibuffer.update(cx, |multibuffer, cx| {
for path in previous_paths {
multibuffer.remove_excerpts_for_path(path, cx);
@@ -585,7 +637,15 @@ impl Item for ProjectDiff {
Self: Sized,
{
let workspace = self.workspace.upgrade()?;
Some(cx.new(|cx| ProjectDiff::new(self.project.clone(), workspace, window, cx)))
Some(cx.new(|cx| {
ProjectDiff::new(
self.source.clone(),
self.project.clone(),
workspace,
window,
cx,
)
}))
}
fn is_dirty(&self, cx: &App) -> bool {
@@ -743,7 +803,7 @@ impl SerializableItem for ProjectDiff {
}
fn deserialize(
_project: Entity<Project>,
project: Entity<Project>,
workspace: WeakEntity<Workspace>,
_workspace_id: workspace::WorkspaceId,
_item_id: workspace::ItemId,
@@ -753,7 +813,16 @@ impl SerializableItem for ProjectDiff {
window.spawn(cx, async move |cx| {
workspace.update_in(cx, |workspace, window, cx| {
let workspace_handle = cx.entity();
cx.new(|cx| Self::new(workspace.project().clone(), workspace_handle, window, cx))
let diff = Arc::new(ProjectDiffSource(project));
cx.new(|cx| {
Self::new(
diff,
workspace.project().clone(),
workspace_handle,
window,
cx,
)
})
})
})
}
@@ -1337,8 +1406,9 @@ mod tests {
let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await;
let (workspace, cx) =
cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
let source = Arc::new(ProjectDiffSource(project.clone()));
let diff = cx.new_window_entity(|window, cx| {
ProjectDiff::new(project.clone(), workspace, window, cx)
ProjectDiff::new(source, project.clone(), workspace, window, cx)
});
cx.run_until_parked();
@@ -1391,8 +1461,9 @@ mod tests {
let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await;
let (workspace, cx) =
cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
let source = Arc::new(ProjectDiffSource(project.clone()));
let diff = cx.new_window_entity(|window, cx| {
ProjectDiff::new(project.clone(), workspace, window, cx)
ProjectDiff::new(source, project.clone(), workspace, window, cx)
});
cx.run_until_parked();
@@ -1464,6 +1535,7 @@ mod tests {
let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await;
let (workspace, cx) =
cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
let source = Arc::new(ProjectDiffSource(project.clone()));
let buffer = project
.update(cx, |project, cx| {
project.open_local_buffer(path!("/project/foo"), cx)
@@ -1474,7 +1546,7 @@ mod tests {
Editor::for_buffer(buffer, Some(project.clone()), window, cx)
});
let diff = cx.new_window_entity(|window, cx| {
ProjectDiff::new(project.clone(), workspace, window, cx)
ProjectDiff::new(source, project.clone(), workspace, window, cx)
});
cx.run_until_parked();

View File

@@ -3,10 +3,7 @@ use gpui::{
};
use itertools::Itertools;
use picker::{Picker, PickerDelegate};
use project::{
git_store::{GitStore, Repository},
Project,
};
use project::{git_store::Repository, Project};
use std::sync::Arc;
use ui::{prelude::*, ListItem, ListItemSpacing};
use workspace::{ModalView, Workspace};
@@ -40,21 +37,23 @@ impl RepositorySelector {
cx: &mut Context<Self>,
) -> Self {
let git_store = project_handle.read(cx).git_store().clone();
let repository_entries = git_store.update(cx, |git_store, cx| {
filtered_repository_entries(git_store, cx)
let repository_entries = git_store.update(cx, |git_store, _cx| {
git_store
.repositories()
.values()
.cloned()
.collect::<Vec<_>>()
});
let project = project_handle.read(cx);
let filtered_repositories = repository_entries.clone();
let widest_item_ix = repository_entries.iter().position_max_by(|a, b| {
a.read(cx)
.display_name(project, cx)
.display_name()
.len()
.cmp(&b.read(cx).display_name(project, cx).len())
.cmp(&b.read(cx).display_name().len())
});
let delegate = RepositorySelectorDelegate {
project: project_handle.downgrade(),
repository_selector: cx.entity().downgrade(),
repository_entries,
filtered_repositories,
@@ -71,36 +70,36 @@ impl RepositorySelector {
}
}
pub(crate) fn filtered_repository_entries(
git_store: &GitStore,
cx: &App,
) -> Vec<Entity<Repository>> {
let repositories = git_store
.repositories()
.values()
.sorted_by_key(|repo| {
let repo = repo.read(cx);
(
repo.dot_git_abs_path.clone(),
repo.worktree_abs_path.clone(),
)
})
.collect::<Vec<&Entity<Repository>>>();
repositories
.chunk_by(|a, b| a.read(cx).dot_git_abs_path == b.read(cx).dot_git_abs_path)
.flat_map(|chunk| {
let has_non_single_file_worktree = chunk
.iter()
.any(|repo| !repo.read(cx).is_from_single_file_worktree);
chunk.iter().filter(move |repo| {
// Remove any entry that comes from a single file worktree and represents a repository that is also represented by a non-single-file worktree.
!repo.read(cx).is_from_single_file_worktree || !has_non_single_file_worktree
})
})
.map(|&repo| repo.clone())
.collect()
}
//pub(crate) fn filtered_repository_entries(
// git_store: &GitStore,
// cx: &App,
//) -> Vec<Entity<Repository>> {
// let repositories = git_store
// .repositories()
// .values()
// .sorted_by_key(|repo| {
// let repo = repo.read(cx);
// (
// repo.dot_git_abs_path.clone(),
// repo.worktree_abs_path.clone(),
// )
// })
// .collect::<Vec<&Entity<Repository>>>();
//
// repositories
// .chunk_by(|a, b| a.read(cx).dot_git_abs_path == b.read(cx).dot_git_abs_path)
// .flat_map(|chunk| {
// let has_non_single_file_worktree = chunk
// .iter()
// .any(|repo| !repo.read(cx).is_from_single_file_worktree);
// chunk.iter().filter(move |repo| {
// // Remove any entry that comes from a single file worktree and represents a repository that is also represented by a non-single-file worktree.
// !repo.read(cx).is_from_single_file_worktree || !has_non_single_file_worktree
// })
// })
// .map(|&repo| repo.clone())
// .collect()
//}
impl EventEmitter<DismissEvent> for RepositorySelector {}
@@ -119,7 +118,6 @@ impl Render for RepositorySelector {
impl ModalView for RepositorySelector {}
pub struct RepositorySelectorDelegate {
project: WeakEntity<Project>,
repository_selector: WeakEntity<RepositorySelector>,
repository_entries: Vec<Entity<Repository>>,
filtered_repositories: Vec<Entity<Repository>>,
@@ -225,9 +223,8 @@ impl PickerDelegate for RepositorySelectorDelegate {
_window: &mut Window,
cx: &mut Context<Picker<Self>>,
) -> Option<Self::ListItem> {
let project = self.project.upgrade()?;
let repo_info = self.filtered_repositories.get(ix)?;
let display_name = repo_info.read(cx).display_name(project.read(cx), cx);
let display_name = repo_info.read(cx).display_name();
Some(
ListItem::new(ix)
.inset(true)

View File

@@ -61,7 +61,7 @@ impl Render for WindowShadow {
CursorStyle::ResizeUpRightDownLeft
}
},
&hitbox,
Some(&hitbox),
);
},
)

View File

@@ -375,16 +375,50 @@ macro_rules! action_with_deprecated_aliases {
$name,
$name,
fn build(
_: gpui::private::serde_json::Value,
value: gpui::private::serde_json::Value,
) -> gpui::Result<::std::boxed::Box<dyn gpui::Action>> {
Ok(Box::new(Self))
},
fn action_json_schema(
generator: &mut gpui::private::schemars::gen::SchemaGenerator,
) -> Option<gpui::private::schemars::schema::Schema> {
None
},
fn deprecated_aliases() -> &'static [&'static str] {
&[
$($alias),*
]
}
);
gpui::register_action!($name);
};
}
/// Defines and registers a unit struct that can be used as an action, with some deprecated aliases.
#[macro_export]
macro_rules! impl_action_with_deprecated_aliases {
($namespace:path, $name:ident, [$($alias:literal),* $(,)?]) => {
gpui::__impl_action!(
$namespace,
$name,
$name,
fn build(
value: gpui::private::serde_json::Value,
) -> gpui::Result<::std::boxed::Box<dyn gpui::Action>> {
Ok(std::boxed::Box::new(gpui::private::serde_json::from_value::<Self>(value)?))
},
fn action_json_schema(
generator: &mut gpui::private::schemars::gen::SchemaGenerator,
) -> Option<gpui::private::schemars::schema::Schema> {
Some(<Self as gpui::private::schemars::JsonSchema>::json_schema(
generator,
))
},
fn deprecated_aliases() -> &'static [&'static str] {
&[
$($alias),*

View File

@@ -1617,7 +1617,7 @@ impl Interactivity {
if !cx.has_active_drag() {
if let Some(mouse_cursor) = style.mouse_cursor {
window.set_cursor_style(mouse_cursor, hitbox);
window.set_cursor_style(mouse_cursor, Some(hitbox));
}
}

View File

@@ -46,6 +46,12 @@ impl List {
#[derive(Clone)]
pub struct ListState(Rc<RefCell<StateInner>>);
impl std::fmt::Debug for ListState {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("ListState")
}
}
struct StateInner {
last_layout_bounds: Option<Bounds<Pixels>>,
last_padding: Option<Edges<Pixels>>,
@@ -57,6 +63,7 @@ struct StateInner {
reset: bool,
#[allow(clippy::type_complexity)]
scroll_handler: Option<Box<dyn FnMut(&ListScrollEvent, &mut Window, &mut App)>>,
scrollbar_drag_start_height: Option<Pixels>,
}
/// Whether the list is scrolling from top to bottom or bottom to top.
@@ -198,6 +205,7 @@ impl ListState {
overdraw,
scroll_handler: None,
reset: false,
scrollbar_drag_start_height: None,
})));
this.splice(0..0, item_count);
this
@@ -211,6 +219,7 @@ impl ListState {
let state = &mut *self.0.borrow_mut();
state.reset = true;
state.logical_scroll_top = None;
state.scrollbar_drag_start_height = None;
state.items.summary().count
};
@@ -355,6 +364,62 @@ impl ListState {
}
None
}
/// Call this method when the user starts dragging the scrollbar.
///
/// This will prevent the height reported to the scrollbar from changing during the drag
/// as items in the overdraw get measured, and help offset scroll position changes accordingly.
pub fn scrollbar_drag_started(&self) {
let mut state = self.0.borrow_mut();
state.scrollbar_drag_start_height = Some(state.items.summary().height);
}
/// Called when the user stops dragging the scrollbar.
///
/// See `scrollbar_drag_started`.
pub fn scrollbar_drag_ended(&self) {
self.0.borrow_mut().scrollbar_drag_start_height.take();
}
/// Set the offset from the scrollbar
pub fn set_offset_from_scrollbar(&self, point: Point<Pixels>) {
self.0.borrow_mut().set_offset_from_scrollbar(point);
}
/// Returns the size of items we have measured.
/// This value remains constant while dragging to prevent the scrollbar from moving away unexpectedly.
pub fn content_size_for_scrollbar(&self) -> Size<Pixels> {
let state = self.0.borrow();
let bounds = state.last_layout_bounds.unwrap_or_default();
let height = state
.scrollbar_drag_start_height
.unwrap_or_else(|| state.items.summary().height);
Size::new(bounds.size.width, height)
}
/// Returns the current scroll offset adjusted for the scrollbar
pub fn scroll_px_offset_for_scrollbar(&self) -> Point<Pixels> {
let state = &self.0.borrow();
let logical_scroll_top = state.logical_scroll_top();
let mut cursor = state.items.cursor::<ListItemSummary>(&());
let summary: ListItemSummary =
cursor.summary(&Count(logical_scroll_top.item_ix), Bias::Right, &());
let content_height = state.items.summary().height;
let drag_offset =
// if dragging the scrollbar, we want to offset the point if the height changed
content_height - state.scrollbar_drag_start_height.unwrap_or(content_height);
let offset = summary.height + logical_scroll_top.offset_in_item - drag_offset;
Point::new(px(0.), -offset)
}
/// Return the bounds of the viewport in pixels.
pub fn viewport_bounds(&self) -> Bounds<Pixels> {
self.0.borrow().last_layout_bounds.unwrap_or_default()
}
}
impl StateInner {
@@ -695,6 +760,37 @@ impl StateInner {
Ok(layout_response)
})
}
// Scrollbar support
fn set_offset_from_scrollbar(&mut self, point: Point<Pixels>) {
let Some(bounds) = self.last_layout_bounds else {
return;
};
let height = bounds.size.height;
let padding = self.last_padding.unwrap_or_default();
let content_height = self.items.summary().height;
let scroll_max = (content_height + padding.top + padding.bottom - height).max(px(0.));
let drag_offset =
// if dragging the scrollbar, we want to offset the point if the height changed
content_height - self.scrollbar_drag_start_height.unwrap_or(content_height);
let new_scroll_top = (point.y - drag_offset).abs().max(px(0.)).min(scroll_max);
if self.alignment == ListAlignment::Bottom && new_scroll_top == scroll_max {
self.logical_scroll_top = None;
} else {
let mut cursor = self.items.cursor::<ListItemSummary>(&());
cursor.seek(&Height(new_scroll_top), Bias::Right, &());
let item_ix = cursor.start().count;
let offset_in_item = new_scroll_top - cursor.start().height;
self.logical_scroll_top = Some(ListOffset {
item_ix,
offset_in_item,
});
}
}
}
impl std::fmt::Debug for ListItem {

View File

@@ -700,7 +700,7 @@ impl Element for InteractiveText {
.iter()
.any(|range| range.contains(&ix))
{
window.set_cursor_style(crate::CursorStyle::PointingHand, hitbox)
window.set_cursor_style(crate::CursorStyle::PointingHand, Some(hitbox))
}
}

View File

@@ -74,6 +74,11 @@ pub(crate) use windows::*;
#[cfg(any(test, feature = "test-support"))]
pub use test::TestScreenCaptureSource;
/// Returns a background executor for the current platform.
pub fn background_executor() -> BackgroundExecutor {
current_platform(true).background_executor()
}
#[cfg(target_os = "macos")]
pub(crate) fn current_platform(headless: bool) -> Rc<dyn Platform> {
Rc::new(MacPlatform::new(headless))

View File

@@ -1,5 +1,7 @@
use anyhow::Context as _;
use blade_graphics as gpu;
use std::sync::Arc;
use util::ResultExt;
#[cfg_attr(target_os = "macos", derive(Clone))]
pub struct BladeContext {
@@ -8,12 +10,24 @@ pub struct BladeContext {
impl BladeContext {
pub fn new() -> anyhow::Result<Self> {
let device_id_forced = match std::env::var("ZED_DEVICE_ID") {
Ok(val) => val
.parse()
.context("Failed to parse device ID from `ZED_DEVICE_ID` environment variable")
.log_err(),
Err(std::env::VarError::NotPresent) => None,
err => {
err.context("Failed to read value of `ZED_DEVICE_ID` environment variable")
.log_err();
None
}
};
let gpu = Arc::new(
unsafe {
gpu::Context::init(gpu::ContextDesc {
presentation: true,
validation: false,
device_id: 0, //TODO: hook up to user settings
device_id: device_id_forced.unwrap_or(0),
..Default::default()
})
}

View File

@@ -532,6 +532,12 @@ fn fs_quad(input: QuadVarying) -> @location(0) vec4<f32> {
quad.border_widths.top,
center_to_point.y < 0.0));
// 0-width borders are reduced so that `inner_sdf >= antialias_threshold`.
// The purpose of this is to not draw antialiasing pixels in this case.
let reduced_border =
vec2<f32>(select(border.x, -antialias_threshold, border.x == 0.0),
select(border.y, -antialias_threshold, border.y == 0.0));
// Vector from the corner of the quad bounds to the point, after mirroring
// the point into the bottom right quadrant. Both components are <= 0.
let corner_to_point = abs(center_to_point) - half_size;
@@ -546,15 +552,15 @@ fn fs_quad(input: QuadVarying) -> @location(0) vec4<f32> {
corner_center_to_point.y >= 0;
// Vector from straight border inner corner to point.
let straight_border_inner_corner_to_point = corner_to_point + border;
let straight_border_inner_corner_to_point = corner_to_point + reduced_border;
// Whether the point is beyond the inner edge of the straight border.
let is_beyond_inner_straight_border =
straight_border_inner_corner_to_point.x > 0 ||
straight_border_inner_corner_to_point.y > 0;
// Whether the point is far enough inside the straight border such that
// pixels are not affected by it.
// Whether the point is far enough inside the quad, such that the pixels are
// not affected by the straight border.
let is_within_inner_straight_border =
straight_border_inner_corner_to_point.x < -antialias_threshold &&
straight_border_inner_corner_to_point.y < -antialias_threshold;
@@ -589,11 +595,11 @@ fn fs_quad(input: QuadVarying) -> @location(0) vec4<f32> {
} else if (is_beyond_inner_straight_border) {
// Fast path for points that must be outside the inner edge.
inner_sdf = -1.0;
} else if (border.x == border.y) {
} else if (reduced_border.x == reduced_border.y) {
// Fast path for circular inner edge.
inner_sdf = -(outer_sdf + border.x);
inner_sdf = -(outer_sdf + reduced_border.x);
} else {
let ellipse_radii = max(vec2<f32>(0.0), corner_radius - border);
let ellipse_radii = max(vec2<f32>(0.0), corner_radius - reduced_border);
inner_sdf = quarter_ellipse_sdf(corner_center_to_point, ellipse_radii);
}

View File

@@ -133,6 +133,12 @@ fragment float4 quad_fragment(QuadFragmentInput input [[stage_in]],
center_to_point.y < 0.0 ? quad.border_widths.top : quad.border_widths.bottom
);
// 0-width borders are reduced so that `inner_sdf >= antialias_threshold`.
// The purpose of this is to not draw antialiasing pixels in this case.
float2 reduced_border = float2(
border.x == 0.0 ? -antialias_threshold : border.x,
border.y == 0.0 ? -antialias_threshold : border.y);
// Vector from the corner of the quad bounds to the point, after mirroring
// the point into the bottom right quadrant. Both components are <= 0.
float2 corner_to_point = fabs(center_to_point) - half_size;
@@ -146,16 +152,20 @@ fragment float4 quad_fragment(QuadFragmentInput input [[stage_in]],
corner_center_to_point.x >= 0.0 &&
corner_center_to_point.y >= 0.0;
// Vector from straight border inner corner to point
float2 straight_border_inner_corner_to_point = corner_to_point + border;
// Vector from straight border inner corner to point.
//
// 0-width borders are turned into width -1 so that inner_sdf is > 1.0 near
// the border. Without this, antialiasing pixels would be drawn.
float2 straight_border_inner_corner_to_point = corner_to_point + reduced_border;
// Whether the point is beyond the inner edge of the straight border
bool is_beyond_inner_straight_border =
straight_border_inner_corner_to_point.x > 0.0 ||
straight_border_inner_corner_to_point.y > 0.0;
// Whether the point is far enough inside the straight border such that
// pixels are not affected by it
// Whether the point is far enough inside the quad, such that the pixels are
// not affected by the straight border.
bool is_within_inner_straight_border =
straight_border_inner_corner_to_point.x < -antialias_threshold &&
straight_border_inner_corner_to_point.y < -antialias_threshold;
@@ -184,11 +194,11 @@ fragment float4 quad_fragment(QuadFragmentInput input [[stage_in]],
} else if (is_beyond_inner_straight_border) {
// Fast path for points that must be outside the inner edge
inner_sdf = -1.0;
} else if (border.x == border.y) {
} else if (reduced_border.x == reduced_border.y) {
// Fast path for circular inner edge.
inner_sdf = -(outer_sdf + border.x);
inner_sdf = -(outer_sdf + reduced_border.x);
} else {
float2 ellipse_radii = max(float2(0.0), float2(corner_radius) - border);
float2 ellipse_radii = max(float2(0.0), float2(corner_radius) - reduced_border);
inner_sdf = quarter_ellipse_sdf(corner_center_to_point, ellipse_radii);
}

View File

@@ -1137,7 +1137,7 @@ fn handle_cursor_changed(lparam: LPARAM, state_ptr: Rc<WindowsWindowStatePtr>) -
};
if had_cursor != state.current_cursor.is_some() {
unsafe { SetCursor(state.current_cursor.as_ref()) };
unsafe { SetCursor(state.current_cursor) };
}
Some(0)
@@ -1151,7 +1151,7 @@ fn handle_set_cursor(lparam: LPARAM, state_ptr: Rc<WindowsWindowStatePtr>) -> Op
return None;
}
unsafe {
SetCursor(state_ptr.state.borrow().current_cursor.as_ref());
SetCursor(state_ptr.state.borrow().current_cursor);
};
Some(1)
}

View File

@@ -407,7 +407,7 @@ pub(crate) type AnyMouseListener =
#[derive(Clone)]
pub(crate) struct CursorStyleRequest {
pub(crate) hitbox_id: HitboxId,
pub(crate) hitbox_id: Option<HitboxId>, // None represents whole window
pub(crate) style: CursorStyle,
}
@@ -1928,10 +1928,10 @@ impl Window {
/// Updates the cursor style at the platform level. This method should only be called
/// during the prepaint phase of element drawing.
pub fn set_cursor_style(&mut self, style: CursorStyle, hitbox: &Hitbox) {
pub fn set_cursor_style(&mut self, style: CursorStyle, hitbox: Option<&Hitbox>) {
self.invalidator.debug_assert_paint();
self.next_frame.cursor_styles.push(CursorStyleRequest {
hitbox_id: hitbox.id,
hitbox_id: hitbox.map(|hitbox| hitbox.id),
style,
});
}
@@ -2984,7 +2984,11 @@ impl Window {
.cursor_styles
.iter()
.rev()
.find(|request| request.hitbox_id.is_hovered(self))
.find(|request| {
request
.hitbox_id
.map_or(true, |hitbox_id| hitbox_id.is_hovered(self))
})
.map(|request| request.style)
.unwrap_or(CursorStyle::Arrow);
cx.platform.set_cursor_style(style);

View File

@@ -100,6 +100,8 @@ pub enum IconName {
Eye,
File,
FileCode,
FileCreate,
FileDelete,
FileDoc,
FileDiff,
FileGeneric,

View File

@@ -14,6 +14,7 @@ use futures::FutureExt;
use futures::{future::BoxFuture, stream::BoxStream, StreamExt, TryStreamExt as _};
use gpui::{AnyElement, AnyView, App, AsyncApp, SharedString, Task, Window};
use icons::IconName;
use parking_lot::Mutex;
use proto::Plan;
use schemars::JsonSchema;
use serde::{de::DeserializeOwned, Deserialize, Serialize};
@@ -141,6 +142,8 @@ pub struct LanguageModelToolUse {
pub struct LanguageModelTextStream {
pub message_id: Option<String>,
pub stream: BoxStream<'static, Result<String>>,
// Has complete token usage after the stream has finished
pub last_token_usage: Arc<Mutex<TokenUsage>>,
}
impl Default for LanguageModelTextStream {
@@ -148,6 +151,7 @@ impl Default for LanguageModelTextStream {
Self {
message_id: None,
stream: Box::pin(futures::stream::empty()),
last_token_usage: Arc::new(Mutex::new(TokenUsage::default())),
}
}
}
@@ -200,6 +204,7 @@ pub trait LanguageModel: Send + Sync {
let mut events = events.await?.fuse();
let mut message_id = None;
let mut first_item_text = None;
let last_token_usage = Arc::new(Mutex::new(TokenUsage::default()));
if let Some(first_event) = events.next().await {
match first_event {
@@ -214,20 +219,33 @@ pub trait LanguageModel: Send + Sync {
}
let stream = futures::stream::iter(first_item_text.map(Ok))
.chain(events.filter_map(|result| async move {
match result {
Ok(LanguageModelCompletionEvent::StartMessage { .. }) => None,
Ok(LanguageModelCompletionEvent::Text(text)) => Some(Ok(text)),
Ok(LanguageModelCompletionEvent::Thinking(_)) => None,
Ok(LanguageModelCompletionEvent::Stop(_)) => None,
Ok(LanguageModelCompletionEvent::ToolUse(_)) => None,
Ok(LanguageModelCompletionEvent::UsageUpdate(_)) => None,
Err(err) => Some(Err(err)),
.chain(events.filter_map({
let last_token_usage = last_token_usage.clone();
move |result| {
let last_token_usage = last_token_usage.clone();
async move {
match result {
Ok(LanguageModelCompletionEvent::StartMessage { .. }) => None,
Ok(LanguageModelCompletionEvent::Text(text)) => Some(Ok(text)),
Ok(LanguageModelCompletionEvent::Thinking(_)) => None,
Ok(LanguageModelCompletionEvent::Stop(_)) => None,
Ok(LanguageModelCompletionEvent::ToolUse(_)) => None,
Ok(LanguageModelCompletionEvent::UsageUpdate(token_usage)) => {
*last_token_usage.lock() = token_usage;
None
}
Err(err) => Some(Err(err)),
}
}
}
}))
.boxed();
Ok(LanguageModelTextStream { message_id, stream })
Ok(LanguageModelTextStream {
message_id,
stream,
last_token_usage,
})
}
.boxed()
}

View File

@@ -29,6 +29,7 @@ use std::{
any::Any,
borrow::Cow,
ffi::OsString,
fmt::Write,
path::{Path, PathBuf},
sync::Arc,
};
@@ -588,6 +589,28 @@ fn python_module_name_from_relative_path(relative_path: &str) -> String {
.to_string()
}
fn python_env_kind_display(k: &PythonEnvironmentKind) -> &'static str {
match k {
PythonEnvironmentKind::Conda => "Conda",
PythonEnvironmentKind::Pixi => "pixi",
PythonEnvironmentKind::Homebrew => "Homebrew",
PythonEnvironmentKind::Pyenv => "global (Pyenv)",
PythonEnvironmentKind::GlobalPaths => "global",
PythonEnvironmentKind::PyenvVirtualEnv => "Pyenv",
PythonEnvironmentKind::Pipenv => "Pipenv",
PythonEnvironmentKind::Poetry => "Poetry",
PythonEnvironmentKind::MacPythonOrg => "global (Python.org)",
PythonEnvironmentKind::MacCommandLineTools => "global (Command Line Tools for Xcode)",
PythonEnvironmentKind::LinuxGlobal => "global",
PythonEnvironmentKind::MacXCode => "global (Xcode)",
PythonEnvironmentKind::Venv => "venv",
PythonEnvironmentKind::VirtualEnv => "virtualenv",
PythonEnvironmentKind::VirtualEnvWrapper => "virtualenvwrapper",
PythonEnvironmentKind::WindowsStore => "global (Windows Store)",
PythonEnvironmentKind::WindowsRegistry => "global (Windows Registry)",
}
}
pub(crate) struct PythonToolchainProvider {
term: SharedString,
}
@@ -683,14 +706,26 @@ impl ToolchainLister for PythonToolchainProvider {
let mut toolchains: Vec<_> = toolchains
.into_iter()
.filter_map(|toolchain| {
let name = if let Some(version) = &toolchain.version {
format!("Python {version} ({:?})", toolchain.kind?)
} else {
format!("{:?}", toolchain.kind?)
let mut name = String::from("Python");
if let Some(ref version) = toolchain.version {
_ = write!(name, " {version}");
}
.into();
let name_and_kind = match (&toolchain.name, &toolchain.kind) {
(Some(name), Some(kind)) => {
Some(format!("({name}; {})", python_env_kind_display(kind)))
}
(Some(name), None) => Some(format!("({name})")),
(None, Some(kind)) => Some(format!("({})", python_env_kind_display(kind))),
(None, None) => None,
};
if let Some(nk) = name_and_kind {
_ = write!(name, " {nk}");
}
Some(Toolchain {
name,
name: name.into(),
path: toolchain.executable.as_ref()?.to_str()?.to_owned().into(),
language_name: LanguageName::new("Python"),
as_json: serde_json::to_value(toolchain).ok()?,

View File

@@ -44,9 +44,9 @@ postage.workspace = true
core-foundation.workspace = true
[target.'cfg(all(not(target_os = "macos")))'.dependencies]
async-trait = { workspace = true }
collections = { workspace = true }
gpui = { workspace = true }
async-trait.workspace = true
collections.workspace = true
gpui.workspace = true
livekit_api.workspace = true
nanoid.workspace = true

View File

@@ -774,6 +774,10 @@ impl LanguageServer {
code_lens: Some(CodeLensClientCapabilities {
dynamic_registration: Some(false),
}),
document_symbol: Some(DocumentSymbolClientCapabilities {
hierarchical_document_symbol_support: Some(true),
..DocumentSymbolClientCapabilities::default()
}),
..TextDocumentClientCapabilities::default()
}),
experimental: Some(json!({
@@ -1479,6 +1483,7 @@ impl LanguageServer {
document_formatting_provider: Some(OneOf::Left(true)),
document_range_formatting_provider: Some(OneOf::Left(true)),
definition_provider: Some(OneOf::Left(true)),
workspace_symbol_provider: Some(OneOf::Left(true)),
implementation_provider: Some(ImplementationProviderCapability::Simple(true)),
type_definition_provider: Some(TypeDefinitionProviderCapability::Simple(true)),
..Default::default()

View File

@@ -411,9 +411,9 @@ impl MarkdownElement {
.is_some();
if is_hovering_link {
window.set_cursor_style(CursorStyle::PointingHand, hitbox);
window.set_cursor_style(CursorStyle::PointingHand, Some(hitbox));
} else {
window.set_cursor_style(CursorStyle::IBeam, hitbox);
window.set_cursor_style(CursorStyle::IBeam, Some(hitbox));
}
self.on_mouse_event(window, cx, {

View File

@@ -14,4 +14,4 @@ doctest = false
[dependencies]
gpui.workspace = true
serde = { workspace = true }
serde.workspace = true

View File

@@ -2555,6 +2555,9 @@ impl OutlinePanel {
let auto_fold_dirs = OutlinePanelSettings::get_global(cx).auto_fold_dirs;
let active_multi_buffer = active_editor.read(cx).buffer().clone();
let new_entries = self.new_entries_for_fs_update.clone();
let repo_snapshots = self.project.update(cx, |project, cx| {
project.git_store().read(cx).repo_snapshots(cx)
});
self.updating_fs_entries = true;
self.fs_entries_update_task = cx.spawn_in(window, async move |outline_panel, cx| {
if let Some(debounce) = debounce {
@@ -2679,13 +2682,15 @@ impl OutlinePanel {
.unwrap_or_default(),
entry,
};
let mut traversal =
GitTraversal::new(worktree.traverse_from_path(
let mut traversal = GitTraversal::new(
&repo_snapshots,
worktree.traverse_from_path(
true,
true,
true,
entry.path.as_ref(),
));
),
);
let mut entries_to_add = HashMap::default();
worktree_excerpts

File diff suppressed because it is too large Load Diff

View File

@@ -1,23 +1,28 @@
use collections::HashMap;
use git::status::GitSummary;
use std::{ops::Deref, path::Path};
use sum_tree::Cursor;
use text::Bias;
use worktree::{Entry, PathProgress, PathTarget, RepositoryEntry, StatusEntry, Traversal};
use worktree::{
Entry, PathProgress, PathTarget, ProjectEntryId, RepositoryEntry, StatusEntry, Traversal,
};
/// Walks the worktree entries and their associated git statuses.
pub struct GitTraversal<'a> {
traversal: Traversal<'a>,
current_entry_summary: Option<GitSummary>,
repo_location: Option<(
&'a RepositoryEntry,
Cursor<'a, StatusEntry, PathProgress<'a>>,
)>,
repo_snapshots: &'a HashMap<ProjectEntryId, RepositoryEntry>,
repo_location: Option<(ProjectEntryId, Cursor<'a, StatusEntry, PathProgress<'a>>)>,
}
impl<'a> GitTraversal<'a> {
pub fn new(traversal: Traversal<'a>) -> GitTraversal<'a> {
pub fn new(
repo_snapshots: &'a HashMap<ProjectEntryId, RepositoryEntry>,
traversal: Traversal<'a>,
) -> GitTraversal<'a> {
let mut this = GitTraversal {
traversal,
repo_snapshots,
current_entry_summary: None,
repo_location: None,
};
@@ -32,7 +37,20 @@ impl<'a> GitTraversal<'a> {
return;
};
let Some(repo) = self.traversal.snapshot().repository_for_path(&entry.path) else {
let Ok(abs_path) = self.traversal.snapshot().absolutize(&entry.path) else {
self.repo_location = None;
return;
};
let Some((repo, repo_path)) = self
.repo_snapshots
.values()
.filter_map(|repo_snapshot| {
let relative_path = repo_snapshot.relativize_abs_path(&abs_path)?;
Some((repo_snapshot, relative_path))
})
.max_by_key(|(repo, _)| repo.work_directory_abs_path.clone())
else {
self.repo_location = None;
return;
};
@@ -42,18 +60,19 @@ impl<'a> GitTraversal<'a> {
|| self
.repo_location
.as_ref()
.map(|(prev_repo, _)| &prev_repo.work_directory)
!= Some(&repo.work_directory)
.map(|(prev_repo_id, _)| *prev_repo_id)
!= Some(repo.work_directory_id())
{
self.repo_location = Some((repo, repo.statuses_by_path.cursor::<PathProgress>(&())));
self.repo_location = Some((
repo.work_directory_id(),
repo.statuses_by_path.cursor::<PathProgress>(&()),
));
}
let Some((repo, statuses)) = &mut self.repo_location else {
let Some((_, statuses)) = &mut self.repo_location else {
return;
};
let repo_path = repo.relativize(&entry.path).unwrap();
if entry.is_dir() {
let mut statuses = statuses.clone();
statuses.seek_forward(&PathTarget::Path(repo_path.as_ref()), Bias::Left, &());
@@ -128,9 +147,15 @@ pub struct ChildEntriesGitIter<'a> {
}
impl<'a> ChildEntriesGitIter<'a> {
pub fn new(snapshot: &'a worktree::Snapshot, parent_path: &'a Path) -> Self {
let mut traversal =
GitTraversal::new(snapshot.traverse_from_path(true, true, true, parent_path));
pub fn new(
repo_snapshots: &'a HashMap<ProjectEntryId, RepositoryEntry>,
worktree_snapshot: &'a worktree::Snapshot,
parent_path: &'a Path,
) -> Self {
let mut traversal = GitTraversal::new(
repo_snapshots,
worktree_snapshot.traverse_from_path(true, true, true, parent_path),
);
traversal.advance();
ChildEntriesGitIter {
parent_path,
@@ -215,6 +240,8 @@ impl AsRef<Entry> for GitEntry {
mod tests {
use std::time::Duration;
use crate::Project;
use super::*;
use fs::FakeFs;
use git::status::{FileStatus, StatusCode, TrackedSummary, UnmergedStatus, UnmergedStatusCode};
@@ -222,7 +249,7 @@ mod tests {
use serde_json::json;
use settings::{Settings as _, SettingsStore};
use util::path;
use worktree::{Worktree, WorktreeSettings};
use worktree::WorktreeSettings;
const CONFLICT: FileStatus = FileStatus::Unmerged(UnmergedStatus {
first_head: UnmergedStatusCode::Updated,
@@ -282,44 +309,35 @@ mod tests {
&[(Path::new("z2.txt"), StatusCode::Added.index())],
);
let tree = Worktree::local(
Path::new(path!("/root")),
true,
fs.clone(),
Default::default(),
&mut cx.to_async(),
)
.await
.unwrap();
let project = Project::test(fs, [path!("/root").as_ref()], cx).await;
cx.executor().run_until_parked();
let snapshot = tree.read_with(cx, |tree, _| tree.snapshot());
let (repo_snapshots, worktree_snapshot) = project.read_with(cx, |project, cx| {
(
project.git_store().read(cx).repo_snapshots(cx),
project.worktrees(cx).next().unwrap().read(cx).snapshot(),
)
});
let mut traversal =
GitTraversal::new(snapshot.traverse_from_path(true, false, true, Path::new("x")));
let entry = traversal.next().unwrap();
assert_eq!(entry.path.as_ref(), Path::new("x/x1.txt"));
assert_eq!(entry.git_summary, GitSummary::UNCHANGED);
let entry = traversal.next().unwrap();
assert_eq!(entry.path.as_ref(), Path::new("x/x2.txt"));
assert_eq!(entry.git_summary, MODIFIED);
let entry = traversal.next().unwrap();
assert_eq!(entry.path.as_ref(), Path::new("x/y/y1.txt"));
assert_eq!(entry.git_summary, GitSummary::CONFLICT);
let entry = traversal.next().unwrap();
assert_eq!(entry.path.as_ref(), Path::new("x/y/y2.txt"));
assert_eq!(entry.git_summary, GitSummary::UNCHANGED);
let entry = traversal.next().unwrap();
assert_eq!(entry.path.as_ref(), Path::new("x/z.txt"));
assert_eq!(entry.git_summary, ADDED);
let entry = traversal.next().unwrap();
assert_eq!(entry.path.as_ref(), Path::new("z/z1.txt"));
assert_eq!(entry.git_summary, GitSummary::UNCHANGED);
let entry = traversal.next().unwrap();
assert_eq!(entry.path.as_ref(), Path::new("z/z2.txt"));
assert_eq!(entry.git_summary, ADDED);
let traversal = GitTraversal::new(
&repo_snapshots,
worktree_snapshot.traverse_from_path(true, false, true, Path::new("x")),
);
let entries = traversal
.map(|entry| (entry.path.clone(), entry.git_summary))
.collect::<Vec<_>>();
pretty_assertions::assert_eq!(
entries,
[
(Path::new("x/x1.txt").into(), GitSummary::UNCHANGED),
(Path::new("x/x2.txt").into(), MODIFIED),
(Path::new("x/y/y1.txt").into(), GitSummary::CONFLICT),
(Path::new("x/y/y2.txt").into(), GitSummary::UNCHANGED),
(Path::new("x/z.txt").into(), ADDED),
(Path::new("z/z1.txt").into(), GitSummary::UNCHANGED),
(Path::new("z/z2.txt").into(), ADDED),
]
)
}
#[gpui::test]
@@ -366,23 +384,20 @@ mod tests {
&[(Path::new("z2.txt"), StatusCode::Added.index())],
);
let tree = Worktree::local(
Path::new(path!("/root")),
true,
fs.clone(),
Default::default(),
&mut cx.to_async(),
)
.await
.unwrap();
let project = Project::test(fs, [path!("/root").as_ref()], cx).await;
cx.executor().run_until_parked();
let snapshot = tree.read_with(cx, |tree, _| tree.snapshot());
let (repo_snapshots, worktree_snapshot) = project.read_with(cx, |project, cx| {
(
project.git_store().read(cx).repo_snapshots(cx),
project.worktrees(cx).next().unwrap().read(cx).snapshot(),
)
});
// Sanity check the propagation for x/y and z
check_git_statuses(
&snapshot,
&repo_snapshots,
&worktree_snapshot,
&[
(Path::new("x/y"), GitSummary::CONFLICT),
(Path::new("x/y/y1.txt"), GitSummary::CONFLICT),
@@ -390,7 +405,8 @@ mod tests {
],
);
check_git_statuses(
&snapshot,
&repo_snapshots,
&worktree_snapshot,
&[
(Path::new("z"), ADDED),
(Path::new("z/z1.txt"), GitSummary::UNCHANGED),
@@ -400,7 +416,8 @@ mod tests {
// Test one of the fundamental cases of propagation blocking, the transition from one git repository to another
check_git_statuses(
&snapshot,
&repo_snapshots,
&worktree_snapshot,
&[
(Path::new("x"), MODIFIED + ADDED),
(Path::new("x/y"), GitSummary::CONFLICT),
@@ -410,7 +427,8 @@ mod tests {
// Sanity check everything around it
check_git_statuses(
&snapshot,
&repo_snapshots,
&worktree_snapshot,
&[
(Path::new("x"), MODIFIED + ADDED),
(Path::new("x/x1.txt"), GitSummary::UNCHANGED),
@@ -424,7 +442,8 @@ mod tests {
// Test the other fundamental case, transitioning from git repository to non-git repository
check_git_statuses(
&snapshot,
&repo_snapshots,
&worktree_snapshot,
&[
(Path::new(""), GitSummary::UNCHANGED),
(Path::new("x"), MODIFIED + ADDED),
@@ -434,7 +453,8 @@ mod tests {
// And all together now
check_git_statuses(
&snapshot,
&repo_snapshots,
&worktree_snapshot,
&[
(Path::new(""), GitSummary::UNCHANGED),
(Path::new("x"), MODIFIED + ADDED),
@@ -490,21 +510,19 @@ mod tests {
],
);
let tree = Worktree::local(
Path::new(path!("/root")),
true,
fs.clone(),
Default::default(),
&mut cx.to_async(),
)
.await
.unwrap();
let project = Project::test(fs, [path!("/root").as_ref()], cx).await;
cx.executor().run_until_parked();
let snapshot = tree.read_with(cx, |tree, _| tree.snapshot());
let (repo_snapshots, worktree_snapshot) = project.read_with(cx, |project, cx| {
(
project.git_store().read(cx).repo_snapshots(cx),
project.worktrees(cx).next().unwrap().read(cx).snapshot(),
)
});
check_git_statuses(
&snapshot,
&repo_snapshots,
&worktree_snapshot,
&[
(Path::new(""), GitSummary::CONFLICT + MODIFIED + ADDED),
(Path::new("g"), GitSummary::CONFLICT),
@@ -513,7 +531,8 @@ mod tests {
);
check_git_statuses(
&snapshot,
&repo_snapshots,
&worktree_snapshot,
&[
(Path::new(""), GitSummary::CONFLICT + ADDED + MODIFIED),
(Path::new("a"), ADDED + MODIFIED),
@@ -530,7 +549,8 @@ mod tests {
);
check_git_statuses(
&snapshot,
&repo_snapshots,
&worktree_snapshot,
&[
(Path::new("a/b"), ADDED),
(Path::new("a/b/c1.txt"), ADDED),
@@ -545,7 +565,8 @@ mod tests {
);
check_git_statuses(
&snapshot,
&repo_snapshots,
&worktree_snapshot,
&[
(Path::new("a/b/c1.txt"), ADDED),
(Path::new("a/b/c2.txt"), GitSummary::UNCHANGED),
@@ -598,26 +619,25 @@ mod tests {
&[(Path::new("z2.txt"), StatusCode::Modified.index())],
);
let tree = Worktree::local(
Path::new(path!("/root")),
true,
fs.clone(),
Default::default(),
&mut cx.to_async(),
)
.await
.unwrap();
let project = Project::test(fs, [path!("/root").as_ref()], cx).await;
cx.executor().run_until_parked();
let snapshot = tree.read_with(cx, |tree, _| tree.snapshot());
let (repo_snapshots, worktree_snapshot) = project.read_with(cx, |project, cx| {
(
project.git_store().read(cx).repo_snapshots(cx),
project.worktrees(cx).next().unwrap().read(cx).snapshot(),
)
});
check_git_statuses(
&snapshot,
&repo_snapshots,
&worktree_snapshot,
&[(Path::new("x"), ADDED), (Path::new("x/x1.txt"), ADDED)],
);
check_git_statuses(
&snapshot,
&repo_snapshots,
&worktree_snapshot,
&[
(Path::new("y"), GitSummary::CONFLICT + MODIFIED),
(Path::new("y/y1.txt"), GitSummary::CONFLICT),
@@ -626,7 +646,8 @@ mod tests {
);
check_git_statuses(
&snapshot,
&repo_snapshots,
&worktree_snapshot,
&[
(Path::new("z"), MODIFIED),
(Path::new("z/z2.txt"), MODIFIED),
@@ -634,12 +655,14 @@ mod tests {
);
check_git_statuses(
&snapshot,
&repo_snapshots,
&worktree_snapshot,
&[(Path::new("x"), ADDED), (Path::new("x/x1.txt"), ADDED)],
);
check_git_statuses(
&snapshot,
&repo_snapshots,
&worktree_snapshot,
&[
(Path::new("x"), ADDED),
(Path::new("x/x1.txt"), ADDED),
@@ -689,18 +712,11 @@ mod tests {
);
cx.run_until_parked();
let tree = Worktree::local(
path!("/root").as_ref(),
true,
fs.clone(),
Default::default(),
&mut cx.to_async(),
)
.await
.unwrap();
let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
cx.executor().run_until_parked();
let (old_entry_ids, old_mtimes) = tree.read_with(cx, |tree, _| {
let (old_entry_ids, old_mtimes) = project.read_with(cx, |project, cx| {
let tree = project.worktrees(cx).next().unwrap().read(cx);
(
tree.entries(true, 0).map(|e| e.id).collect::<Vec<_>>(),
tree.entries(true, 0).map(|e| e.mtime).collect::<Vec<_>>(),
@@ -713,7 +729,8 @@ mod tests {
fs.touch_path(path!("/root")).await;
cx.executor().run_until_parked();
let (new_entry_ids, new_mtimes) = tree.read_with(cx, |tree, _| {
let (new_entry_ids, new_mtimes) = project.read_with(cx, |project, cx| {
let tree = project.worktrees(cx).next().unwrap().read(cx);
(
tree.entries(true, 0).map(|e| e.id).collect::<Vec<_>>(),
tree.entries(true, 0).map(|e| e.mtime).collect::<Vec<_>>(),
@@ -734,10 +751,16 @@ mod tests {
cx.executor().run_until_parked();
cx.executor().advance_clock(Duration::from_secs(1));
let snapshot = tree.read_with(cx, |tree, _| tree.snapshot());
let (repo_snapshots, worktree_snapshot) = project.read_with(cx, |project, cx| {
(
project.git_store().read(cx).repo_snapshots(cx),
project.worktrees(cx).next().unwrap().read(cx).snapshot(),
)
});
check_git_statuses(
&snapshot,
&repo_snapshots,
&worktree_snapshot,
&[
(Path::new(""), MODIFIED),
(Path::new("a.txt"), GitSummary::UNCHANGED),
@@ -748,11 +771,14 @@ mod tests {
#[track_caller]
fn check_git_statuses(
snapshot: &worktree::Snapshot,
repo_snapshots: &HashMap<ProjectEntryId, RepositoryEntry>,
worktree_snapshot: &worktree::Snapshot,
expected_statuses: &[(&Path, GitSummary)],
) {
let mut traversal =
GitTraversal::new(snapshot.traverse_from_path(true, true, false, "".as_ref()));
let mut traversal = GitTraversal::new(
repo_snapshots,
worktree_snapshot.traverse_from_path(true, true, false, "".as_ref()),
);
let found_statuses = expected_statuses
.iter()
.map(|&(path, _)| {
@@ -762,6 +788,6 @@ mod tests {
(path, git_entry.git_summary)
})
.collect::<Vec<_>>();
assert_eq!(found_statuses, expected_statuses);
pretty_assertions::assert_eq!(found_statuses, expected_statuses);
}
}

View File

@@ -2,10 +2,10 @@ mod signature_help;
use crate::{
lsp_store::{LocalLspStore, LspStore},
CodeAction, CompletionSource, CoreCompletion, DocumentHighlight, Hover, HoverBlock,
HoverBlockKind, InlayHint, InlayHintLabel, InlayHintLabelPart, InlayHintLabelPartTooltip,
InlayHintTooltip, Location, LocationLink, LspAction, MarkupContent, PrepareRenameResponse,
ProjectTransaction, ResolveState,
CodeAction, CompletionSource, CoreCompletion, DocumentHighlight, DocumentSymbol, Hover,
HoverBlock, HoverBlockKind, InlayHint, InlayHintLabel, InlayHintLabelPart,
InlayHintLabelPartTooltip, InlayHintTooltip, Location, LocationLink, LspAction, MarkupContent,
PrepareRenameResponse, ProjectTransaction, ResolveState,
};
use anyhow::{anyhow, Context as _, Result};
use async_trait::async_trait;
@@ -28,7 +28,7 @@ use lsp::{
ServerCapabilities,
};
use signature_help::{lsp_to_proto_signature, proto_to_lsp_signature};
use std::{cmp::Reverse, ops::Range, path::Path, sync::Arc};
use std::{cmp::Reverse, mem, ops::Range, path::Path, sync::Arc};
use text::{BufferId, LineEnding};
pub use signature_help::SignatureHelp;
@@ -199,6 +199,9 @@ pub(crate) struct GetDocumentHighlights {
pub position: PointUtf16,
}
#[derive(Debug, Copy, Clone)]
pub(crate) struct GetDocumentSymbols;
#[derive(Clone, Debug)]
pub(crate) struct GetSignatureHelp {
pub position: PointUtf16,
@@ -1488,6 +1491,205 @@ impl LspCommand for GetDocumentHighlights {
}
}
#[async_trait(?Send)]
impl LspCommand for GetDocumentSymbols {
type Response = Vec<DocumentSymbol>;
type LspRequest = lsp::request::DocumentSymbolRequest;
type ProtoRequest = proto::GetDocumentSymbols;
fn display_name(&self) -> &str {
"Get document symbols"
}
fn check_capabilities(&self, capabilities: AdapterServerCapabilities) -> bool {
capabilities
.server_capabilities
.document_symbol_provider
.is_some()
}
fn to_lsp(
&self,
path: &Path,
_: &Buffer,
_: &Arc<LanguageServer>,
_: &App,
) -> Result<lsp::DocumentSymbolParams> {
Ok(lsp::DocumentSymbolParams {
text_document: make_text_document_identifier(path)?,
work_done_progress_params: Default::default(),
partial_result_params: Default::default(),
})
}
async fn response_from_lsp(
self,
lsp_symbols: Option<lsp::DocumentSymbolResponse>,
_: Entity<LspStore>,
_: Entity<Buffer>,
_: LanguageServerId,
_: AsyncApp,
) -> Result<Vec<DocumentSymbol>> {
let Some(lsp_symbols) = lsp_symbols else {
return Ok(Vec::new());
};
let symbols: Vec<_> = match lsp_symbols {
lsp::DocumentSymbolResponse::Flat(symbol_information) => symbol_information
.into_iter()
.map(|lsp_symbol| DocumentSymbol {
name: lsp_symbol.name,
kind: lsp_symbol.kind,
range: range_from_lsp(lsp_symbol.location.range),
selection_range: range_from_lsp(lsp_symbol.location.range),
children: Vec::new(),
})
.collect(),
lsp::DocumentSymbolResponse::Nested(nested_responses) => {
fn convert_symbol(lsp_symbol: lsp::DocumentSymbol) -> DocumentSymbol {
DocumentSymbol {
name: lsp_symbol.name,
kind: lsp_symbol.kind,
range: range_from_lsp(lsp_symbol.range),
selection_range: range_from_lsp(lsp_symbol.selection_range),
children: lsp_symbol
.children
.map(|children| {
children.into_iter().map(convert_symbol).collect::<Vec<_>>()
})
.unwrap_or_default(),
}
}
nested_responses.into_iter().map(convert_symbol).collect()
}
};
Ok(symbols)
}
fn to_proto(&self, project_id: u64, buffer: &Buffer) -> proto::GetDocumentSymbols {
proto::GetDocumentSymbols {
project_id,
buffer_id: buffer.remote_id().into(),
version: serialize_version(&buffer.version()),
}
}
async fn from_proto(
message: proto::GetDocumentSymbols,
_: Entity<LspStore>,
buffer: Entity<Buffer>,
mut cx: AsyncApp,
) -> Result<Self> {
buffer
.update(&mut cx, |buffer, _| {
buffer.wait_for_version(deserialize_version(&message.version))
})?
.await?;
Ok(Self)
}
fn response_to_proto(
response: Vec<DocumentSymbol>,
_: &mut LspStore,
_: PeerId,
_: &clock::Global,
_: &mut App,
) -> proto::GetDocumentSymbolsResponse {
let symbols = response
.into_iter()
.map(|symbol| {
fn convert_symbol_to_proto(symbol: DocumentSymbol) -> proto::DocumentSymbol {
proto::DocumentSymbol {
name: symbol.name.clone(),
kind: unsafe { mem::transmute::<lsp::SymbolKind, i32>(symbol.kind) },
start: Some(proto::PointUtf16 {
row: symbol.range.start.0.row,
column: symbol.range.start.0.column,
}),
end: Some(proto::PointUtf16 {
row: symbol.range.end.0.row,
column: symbol.range.end.0.column,
}),
selection_start: Some(proto::PointUtf16 {
row: symbol.selection_range.start.0.row,
column: symbol.selection_range.start.0.column,
}),
selection_end: Some(proto::PointUtf16 {
row: symbol.selection_range.end.0.row,
column: symbol.selection_range.end.0.column,
}),
children: symbol
.children
.into_iter()
.map(convert_symbol_to_proto)
.collect(),
}
}
convert_symbol_to_proto(symbol)
})
.collect::<Vec<_>>();
proto::GetDocumentSymbolsResponse { symbols }
}
async fn response_from_proto(
self,
message: proto::GetDocumentSymbolsResponse,
_: Entity<LspStore>,
_: Entity<Buffer>,
_: AsyncApp,
) -> Result<Vec<DocumentSymbol>> {
let mut symbols = Vec::with_capacity(message.symbols.len());
for serialized_symbol in message.symbols {
fn deserialize_symbol_with_children(
serialized_symbol: proto::DocumentSymbol,
) -> Result<DocumentSymbol> {
let kind =
unsafe { mem::transmute::<i32, lsp::SymbolKind>(serialized_symbol.kind) };
let start = serialized_symbol
.start
.ok_or_else(|| anyhow!("invalid start"))?;
let end = serialized_symbol
.end
.ok_or_else(|| anyhow!("invalid end"))?;
let selection_start = serialized_symbol
.selection_start
.ok_or_else(|| anyhow!("invalid selection start"))?;
let selection_end = serialized_symbol
.selection_end
.ok_or_else(|| anyhow!("invalid selection end"))?;
Ok(DocumentSymbol {
name: serialized_symbol.name,
kind,
range: Unclipped(PointUtf16::new(start.row, start.column))
..Unclipped(PointUtf16::new(end.row, end.column)),
selection_range: Unclipped(PointUtf16::new(
selection_start.row,
selection_start.column,
))
..Unclipped(PointUtf16::new(selection_end.row, selection_end.column)),
children: serialized_symbol
.children
.into_iter()
.filter_map(|symbol| deserialize_symbol_with_children(symbol).ok())
.collect::<Vec<_>>(),
})
}
symbols.push(deserialize_symbol_with_children(serialized_symbol)?);
}
Ok(symbols)
}
fn buffer_id_from_proto(message: &proto::GetDocumentSymbols) -> Result<BufferId> {
BufferId::new(message.buffer_id)
}
}
#[async_trait(?Send)]
impl LspCommand for GetSignatureHelp {
type Response = Option<SignatureHelp>;

View File

@@ -3432,6 +3432,7 @@ impl LspStore {
client.add_entity_request_handler(Self::handle_lsp_command::<GetDeclaration>);
client.add_entity_request_handler(Self::handle_lsp_command::<GetTypeDefinition>);
client.add_entity_request_handler(Self::handle_lsp_command::<GetDocumentHighlights>);
client.add_entity_request_handler(Self::handle_lsp_command::<GetDocumentSymbols>);
client.add_entity_request_handler(Self::handle_lsp_command::<GetReferences>);
client.add_entity_request_handler(Self::handle_lsp_command::<PrepareRename>);
client.add_entity_request_handler(Self::handle_lsp_command::<PerformRename>);
@@ -5790,48 +5791,57 @@ impl LspStore {
_ => continue 'next_server,
};
let supports_workspace_symbol_request =
match server.capabilities().workspace_symbol_provider {
Some(OneOf::Left(supported)) => supported,
Some(OneOf::Right(_)) => true,
None => false,
};
if !supports_workspace_symbol_request {
continue 'next_server;
}
let worktree_abs_path = worktree.abs_path().clone();
let worktree_handle = worktree_handle.clone();
let server_id = server.server_id();
requests.push(
server
.request::<lsp::request::WorkspaceSymbolRequest>(
lsp::WorkspaceSymbolParams {
query: query.to_string(),
..Default::default()
},
)
.log_err()
.map(move |response| {
let lsp_symbols = response.flatten().map(|symbol_response| match symbol_response {
lsp::WorkspaceSymbolResponse::Flat(flat_responses) => {
flat_responses.into_iter().map(|lsp_symbol| {
(lsp_symbol.name, lsp_symbol.kind, lsp_symbol.location)
}).collect::<Vec<_>>()
}
lsp::WorkspaceSymbolResponse::Nested(nested_responses) => {
nested_responses.into_iter().filter_map(|lsp_symbol| {
let location = match lsp_symbol.location {
OneOf::Left(location) => location,
OneOf::Right(_) => {
log::error!("Unexpected: client capabilities forbid symbol resolutions in workspace.symbol.resolveSupport");
return None
}
};
Some((lsp_symbol.name, lsp_symbol.kind, location))
}).collect::<Vec<_>>()
}
}).unwrap_or_default();
WorkspaceSymbolsResult {
server_id,
lsp_adapter,
worktree: worktree_handle.downgrade(),
worktree_abs_path,
lsp_symbols,
server
.request::<lsp::request::WorkspaceSymbolRequest>(
lsp::WorkspaceSymbolParams {
query: query.to_string(),
..Default::default()
},
)
.log_err()
.map(move |response| {
let lsp_symbols = response.flatten().map(|symbol_response| match symbol_response {
lsp::WorkspaceSymbolResponse::Flat(flat_responses) => {
flat_responses.into_iter().map(|lsp_symbol| {
(lsp_symbol.name, lsp_symbol.kind, lsp_symbol.location)
}).collect::<Vec<_>>()
}
}),
);
lsp::WorkspaceSymbolResponse::Nested(nested_responses) => {
nested_responses.into_iter().filter_map(|lsp_symbol| {
let location = match lsp_symbol.location {
OneOf::Left(location) => location,
OneOf::Right(_) => {
log::error!("Unexpected: client capabilities forbid symbol resolutions in workspace.symbol.resolveSupport");
return None
}
};
Some((lsp_symbol.name, lsp_symbol.kind, location))
}).collect::<Vec<_>>()
}
}).unwrap_or_default();
WorkspaceSymbolsResult {
server_id,
lsp_adapter,
worktree: worktree_handle.downgrade(),
worktree_abs_path,
lsp_symbols,
}
}),
);
}
requested_servers.append(&mut servers_to_query);
}

View File

@@ -24,7 +24,7 @@ mod direnv;
mod environment;
use buffer_diff::BufferDiff;
pub use environment::{EnvironmentErrorMessage, ProjectEnvironmentEvent};
use git_store::Repository;
use git_store::{GitEvent, Repository};
pub mod search_history;
mod yarn;
@@ -270,7 +270,6 @@ pub enum Event {
WorktreeOrderChanged,
WorktreeRemoved(WorktreeId),
WorktreeUpdatedEntries(WorktreeId, UpdatedEntriesSet),
WorktreeUpdatedGitRepositories(WorktreeId),
DiskBasedDiagnosticsStarted {
language_server_id: LanguageServerId,
},
@@ -300,6 +299,8 @@ pub enum Event {
RevealInProjectPanel(ProjectEntryId),
SnippetEdit(BufferId, Vec<(lsp::Range, Snippet)>),
ExpandedAllForEntry(WorktreeId, ProjectEntryId),
GitStateUpdated,
ActiveRepositoryChanged,
}
pub enum DebugAdapterClientState {
@@ -659,6 +660,15 @@ pub struct Symbol {
pub signature: [u8; 32],
}
#[derive(Clone, Debug)]
pub struct DocumentSymbol {
pub name: String,
pub kind: lsp::SymbolKind,
pub range: Range<Unclipped<PointUtf16>>,
pub selection_range: Range<Unclipped<PointUtf16>>,
pub children: Vec<DocumentSymbol>,
}
#[derive(Clone, Debug, PartialEq)]
pub struct HoverBlock {
pub text: String,
@@ -784,8 +794,6 @@ impl Project {
client.add_entity_message_handler(Self::handle_unshare_project);
client.add_entity_request_handler(Self::handle_update_buffer);
client.add_entity_message_handler(Self::handle_update_worktree);
client.add_entity_message_handler(Self::handle_update_repository);
client.add_entity_message_handler(Self::handle_remove_repository);
client.add_entity_request_handler(Self::handle_synchronize_buffers);
client.add_entity_request_handler(Self::handle_search_candidate_buffers);
@@ -913,6 +921,7 @@ impl Project {
cx,
)
});
cx.subscribe(&git_store, Self::on_git_store_event).detach();
cx.subscribe(&lsp_store, Self::on_lsp_store_event).detach();
@@ -1127,8 +1136,6 @@ impl Project {
ssh_proto.add_entity_message_handler(Self::handle_create_buffer_for_peer);
ssh_proto.add_entity_message_handler(Self::handle_update_worktree);
ssh_proto.add_entity_message_handler(Self::handle_update_repository);
ssh_proto.add_entity_message_handler(Self::handle_remove_repository);
ssh_proto.add_entity_message_handler(Self::handle_update_project);
ssh_proto.add_entity_message_handler(Self::handle_toast);
ssh_proto.add_entity_request_handler(Self::handle_language_server_prompt_request);
@@ -1469,7 +1476,7 @@ impl Project {
) -> Entity<Project> {
use clock::FakeSystemClock;
let fs = Arc::new(RealFs::default());
let fs = Arc::new(RealFs::new(None, cx.background_executor().clone()));
let languages = LanguageRegistry::test(cx.background_executor().clone());
let clock = Arc::new(FakeSystemClock::new());
let http_client = http_client::FakeHttpClient::with_404_response();
@@ -2031,6 +2038,11 @@ impl Project {
self.worktree_store.update(cx, |worktree_store, cx| {
worktree_store.send_project_updates(cx);
});
if let Some(remote_id) = self.remote_id() {
self.git_store.update(cx, |git_store, cx| {
git_store.shared(remote_id, self.client.clone().into(), cx)
});
}
cx.emit(Event::Reshared);
Ok(())
}
@@ -2698,6 +2710,19 @@ impl Project {
}
}
fn on_git_store_event(
&mut self,
_: Entity<GitStore>,
event: &GitEvent,
cx: &mut Context<Self>,
) {
match event {
GitEvent::GitStateUpdated => cx.emit(Event::GitStateUpdated),
GitEvent::ActiveRepositoryChanged => cx.emit(Event::ActiveRepositoryChanged),
GitEvent::FileSystemUpdated | GitEvent::IndexWriteError(_) => {}
}
}
fn on_ssh_event(
&mut self,
_: Entity<SshRemoteClient>,
@@ -2783,12 +2808,11 @@ impl Project {
.report_discovered_project_events(*worktree_id, changes);
cx.emit(Event::WorktreeUpdatedEntries(*worktree_id, changes.clone()))
}
WorktreeStoreEvent::WorktreeUpdatedGitRepositories(worktree_id) => {
cx.emit(Event::WorktreeUpdatedGitRepositories(*worktree_id))
}
WorktreeStoreEvent::WorktreeDeletedEntry(worktree_id, id) => {
cx.emit(Event::DeletedEntry(*worktree_id, *id))
}
// Listen to the GitStore instead.
WorktreeStoreEvent::WorktreeUpdatedGitRepositories(_, _) => {}
}
}
@@ -3222,6 +3246,19 @@ impl Project {
self.document_highlights_impl(buffer, position, cx)
}
pub fn document_symbols(
&mut self,
buffer: &Entity<Buffer>,
cx: &mut Context<Self>,
) -> Task<Result<Vec<DocumentSymbol>>> {
self.request_lsp(
buffer.clone(),
LanguageServerToQuery::FirstCapable,
GetDocumentSymbols,
cx,
)
}
pub fn symbols(&self, query: &str, cx: &mut Context<Self>) -> Task<Result<Vec<Symbol>>> {
self.lsp_store
.update(cx, |lsp_store, cx| lsp_store.symbols(query, cx))
@@ -4287,43 +4324,7 @@ impl Project {
if let Some(worktree) = this.worktree_for_id(worktree_id, cx) {
worktree.update(cx, |worktree, _| {
let worktree = worktree.as_remote_mut().unwrap();
worktree.update_from_remote(envelope.payload.into());
});
}
Ok(())
})?
}
async fn handle_update_repository(
this: Entity<Self>,
envelope: TypedEnvelope<proto::UpdateRepository>,
mut cx: AsyncApp,
) -> Result<()> {
this.update(&mut cx, |this, cx| {
if let Some((worktree, _relative_path)) =
this.find_worktree(envelope.payload.abs_path.as_ref(), cx)
{
worktree.update(cx, |worktree, _| {
let worktree = worktree.as_remote_mut().unwrap();
worktree.update_from_remote(envelope.payload.into());
});
}
Ok(())
})?
}
async fn handle_remove_repository(
this: Entity<Self>,
envelope: TypedEnvelope<proto::RemoveRepository>,
mut cx: AsyncApp,
) -> Result<()> {
this.update(&mut cx, |this, cx| {
if let Some(worktree) =
this.worktree_for_entry(ProjectEntryId::from_proto(envelope.payload.id), cx)
{
worktree.update(cx, |worktree, _| {
let worktree = worktree.as_remote_mut().unwrap();
worktree.update_from_remote(envelope.payload.into());
worktree.update_from_remote(envelope.payload);
});
}
Ok(())

View File

@@ -6,7 +6,8 @@ use buffer_diff::{
};
use fs::FakeFs;
use futures::{future, StreamExt};
use gpui::{App, SemanticVersion, UpdateGlobal};
use git::repository::RepoPath;
use gpui::{App, BackgroundExecutor, SemanticVersion, UpdateGlobal};
use http_client::Url;
use language::{
language_settings::{language_settings, AllLanguageSettings, LanguageSettingsContent},
@@ -34,6 +35,7 @@ use util::{
test::{marked_text_offsets, TempTree},
uri, TryFutureExt as _,
};
use worktree::WorktreeModelHandle as _;
#[gpui::test]
async fn test_block_via_channel(cx: &mut gpui::TestAppContext) {
@@ -97,7 +99,12 @@ async fn test_symlinks(cx: &mut gpui::TestAppContext) {
)
.unwrap();
let project = Project::test(Arc::new(RealFs::default()), [root_link_path.as_ref()], cx).await;
let project = Project::test(
Arc::new(RealFs::new(None, cx.executor())),
[root_link_path.as_ref()],
cx,
)
.await;
project.update(cx, |project, cx| {
let tree = project.worktrees(cx).next().unwrap().read(cx);
@@ -3330,7 +3337,7 @@ async fn test_rescan_and_remote_updates(cx: &mut gpui::TestAppContext) {
}
}));
let project = Project::test(Arc::new(RealFs::default()), [dir.path()], cx).await;
let project = Project::test(Arc::new(RealFs::new(None, cx.executor())), [dir.path()], cx).await;
let buffer_for_path = |path: &'static str, cx: &mut gpui::TestAppContext| {
let buffer = project.update(cx, |p, cx| p.open_local_buffer(dir.path().join(path), cx));
@@ -6769,6 +6776,158 @@ async fn test_single_file_diffs(cx: &mut gpui::TestAppContext) {
});
}
#[gpui::test]
async fn test_repository_and_path_for_project_path(
background_executor: BackgroundExecutor,
cx: &mut gpui::TestAppContext,
) {
init_test(cx);
let fs = FakeFs::new(background_executor);
fs.insert_tree(
path!("/root"),
json!({
"c.txt": "",
"dir1": {
".git": {},
"deps": {
"dep1": {
".git": {},
"src": {
"a.txt": ""
}
}
},
"src": {
"b.txt": ""
}
},
}),
)
.await;
let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
let tree = project.read_with(cx, |project, cx| project.worktrees(cx).next().unwrap());
let tree_id = tree.read_with(cx, |tree, _| tree.id());
tree.read_with(cx, |tree, _| tree.as_local().unwrap().scan_complete())
.await;
tree.flush_fs_events(cx).await;
project.read_with(cx, |project, cx| {
let git_store = project.git_store().read(cx);
let pairs = [
("c.txt", None),
("dir1/src/b.txt", Some((path!("/root/dir1"), "src/b.txt"))),
(
"dir1/deps/dep1/src/a.txt",
Some((path!("/root/dir1/deps/dep1"), "src/a.txt")),
),
];
let expected = pairs
.iter()
.map(|(path, result)| {
(
path,
result.map(|(repo, repo_path)| {
(Path::new(repo).to_owned(), RepoPath::from(repo_path))
}),
)
})
.collect::<Vec<_>>();
let actual = pairs
.iter()
.map(|(path, _)| {
let project_path = (tree_id, Path::new(path)).into();
let result = maybe!({
let (repo, repo_path) =
git_store.repository_and_path_for_project_path(&project_path, cx)?;
Some((
repo.read(cx)
.repository_entry
.work_directory_abs_path
.clone(),
repo_path,
))
});
(path, result)
})
.collect::<Vec<_>>();
pretty_assertions::assert_eq!(expected, actual);
});
fs.remove_dir(path!("/root/dir1/.git").as_ref(), RemoveOptions::default())
.await
.unwrap();
tree.flush_fs_events(cx).await;
project.read_with(cx, |project, cx| {
let git_store = project.git_store().read(cx);
assert_eq!(
git_store.repository_and_path_for_project_path(
&(tree_id, Path::new("dir1/src/b.txt")).into(),
cx
),
None
);
});
}
#[gpui::test]
async fn test_home_dir_as_git_repository(cx: &mut gpui::TestAppContext) {
init_test(cx);
let fs = FakeFs::new(cx.background_executor.clone());
fs.insert_tree(
path!("/root"),
json!({
"home": {
".git": {},
"project": {
"a.txt": "A"
},
},
}),
)
.await;
fs.set_home_dir(Path::new(path!("/root/home")).to_owned());
let project = Project::test(fs.clone(), [path!("/root/home/project").as_ref()], cx).await;
let tree = project.read_with(cx, |project, cx| project.worktrees(cx).next().unwrap());
let tree_id = tree.read_with(cx, |tree, _| tree.id());
tree.read_with(cx, |tree, _| tree.as_local().unwrap().scan_complete())
.await;
tree.flush_fs_events(cx).await;
project.read_with(cx, |project, cx| {
let containing = project
.git_store()
.read(cx)
.repository_and_path_for_project_path(&(tree_id, "a.txt").into(), cx);
assert!(containing.is_none());
});
let project = Project::test(fs.clone(), [path!("/root/home").as_ref()], cx).await;
let tree = project.read_with(cx, |project, cx| project.worktrees(cx).next().unwrap());
let tree_id = tree.read_with(cx, |tree, _| tree.id());
tree.read_with(cx, |tree, _| tree.as_local().unwrap().scan_complete())
.await;
tree.flush_fs_events(cx).await;
project.read_with(cx, |project, cx| {
let containing = project
.git_store()
.read(cx)
.repository_and_path_for_project_path(&(tree_id, "project/a.txt").into(), cx);
assert_eq!(
containing
.unwrap()
.0
.read(cx)
.repository_entry
.work_directory_abs_path,
Path::new(path!("/root/home"))
);
});
}
async fn search(
project: &Entity<Project>,
query: SearchQuery,

View File

@@ -26,7 +26,10 @@ use smol::{
};
use text::ReplicaId;
use util::{paths::SanitizedPath, ResultExt};
use worktree::{Entry, ProjectEntryId, UpdatedEntriesSet, Worktree, WorktreeId, WorktreeSettings};
use worktree::{
Entry, ProjectEntryId, UpdatedEntriesSet, UpdatedGitRepositoriesSet, Worktree, WorktreeId,
WorktreeSettings,
};
use crate::{search::SearchQuery, ProjectPath};
@@ -66,7 +69,7 @@ pub enum WorktreeStoreEvent {
WorktreeOrderChanged,
WorktreeUpdateSent(Entity<Worktree>),
WorktreeUpdatedEntries(WorktreeId, UpdatedEntriesSet),
WorktreeUpdatedGitRepositories(WorktreeId),
WorktreeUpdatedGitRepositories(WorktreeId, UpdatedGitRepositoriesSet),
WorktreeDeletedEntry(WorktreeId, ProjectEntryId),
}
@@ -156,6 +159,11 @@ impl WorktreeStore {
None
}
pub fn absolutize(&self, project_path: &ProjectPath, cx: &App) -> Option<PathBuf> {
let worktree = self.worktree_for_id(project_path.worktree_id, cx)?;
worktree.read(cx).absolutize(&project_path.path).ok()
}
pub fn find_or_create_worktree(
&mut self,
abs_path: impl AsRef<Path>,
@@ -367,9 +375,10 @@ impl WorktreeStore {
changes.clone(),
));
}
worktree::Event::UpdatedGitRepositories(_) => {
worktree::Event::UpdatedGitRepositories(set) => {
cx.emit(WorktreeStoreEvent::WorktreeUpdatedGitRepositories(
worktree_id,
set.clone(),
));
}
worktree::Event::DeletedEntry(id) => {
@@ -561,44 +570,12 @@ impl WorktreeStore {
let client = client.clone();
async move {
if client.is_via_collab() {
match update {
proto::WorktreeRelatedMessage::UpdateWorktree(
update,
) => {
client
.request(update)
.map(|result| result.log_err().is_some())
.await
}
proto::WorktreeRelatedMessage::UpdateRepository(
update,
) => {
client
.request(update)
.map(|result| result.log_err().is_some())
.await
}
proto::WorktreeRelatedMessage::RemoveRepository(
update,
) => {
client
.request(update)
.map(|result| result.log_err().is_some())
.await
}
}
client
.request(update)
.map(|result| result.log_err().is_some())
.await
} else {
match update {
proto::WorktreeRelatedMessage::UpdateWorktree(
update,
) => client.send(update).log_err().is_some(),
proto::WorktreeRelatedMessage::UpdateRepository(
update,
) => client.send(update).log_err().is_some(),
proto::WorktreeRelatedMessage::RemoveRepository(
update,
) => client.send(update).log_err().is_some(),
}
client.send(update).log_err().is_some()
}
}
}

View File

@@ -36,7 +36,7 @@ use project_panel_settings::{
};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use settings::{Settings, SettingsStore};
use settings::{update_settings_file, Settings, SettingsStore};
use smallvec::SmallVec;
use std::any::TypeId;
use std::{
@@ -197,6 +197,7 @@ actions!(
Open,
OpenPermanent,
ToggleFocus,
ToggleHideGitIgnore,
NewSearchInDirectory,
UnfoldDirectory,
FoldDirectory,
@@ -233,6 +234,13 @@ pub fn init(cx: &mut App) {
workspace.register_action(|workspace, _: &ToggleFocus, window, cx| {
workspace.toggle_panel_focus::<ProjectPanel>(window, cx);
});
workspace.register_action(|workspace, _: &ToggleHideGitIgnore, _, cx| {
let fs = workspace.app_state().fs.clone();
update_settings_file::<ProjectPanelSettings>(fs, cx, move |setting, _| {
setting.hide_gitignore = Some(!setting.hide_gitignore.unwrap_or(false));
})
});
})
.detach();
}
@@ -326,7 +334,8 @@ impl ProjectPanel {
this.update_visible_entries(None, cx);
cx.notify();
}
project::Event::WorktreeUpdatedGitRepositories(_)
project::Event::GitStateUpdated
| project::Event::ActiveRepositoryChanged
| project::Event::WorktreeUpdatedEntries(_, _)
| project::Event::WorktreeAdded(_)
| project::Event::WorktreeOrderChanged => {
@@ -414,6 +423,9 @@ impl ProjectPanel {
cx.observe_global::<SettingsStore>(move |this, cx| {
let new_settings = *ProjectPanelSettings::get_global(cx);
if project_panel_settings != new_settings {
if project_panel_settings.hide_gitignore != new_settings.hide_gitignore {
this.update_visible_entries(None, cx);
}
project_panel_settings = new_settings;
this.update_diagnostics(cx);
cx.notify();
@@ -1536,13 +1548,13 @@ impl ProjectPanel {
if sanitized_entries.is_empty() {
return None;
}
let project = self.project.read(cx);
let (worktree_id, worktree) = sanitized_entries
.iter()
.map(|entry| entry.worktree_id)
.filter_map(|id| project.worktree_for_id(id, cx).map(|w| (id, w.read(cx))))
.max_by(|(_, a), (_, b)| a.root_name().cmp(b.root_name()))?;
let git_store = project.git_store().read(cx);
let marked_entries_in_worktree = sanitized_entries
.iter()
@@ -1567,17 +1579,20 @@ impl ProjectPanel {
let parent_entry = worktree.entry_for_path(parent_path)?;
// Remove all siblings that are being deleted except the last marked entry
let snapshot = worktree.snapshot();
let mut siblings: Vec<_> = ChildEntriesGitIter::new(&snapshot, parent_path)
.filter(|sibling| {
sibling.id == latest_entry.id
|| !marked_entries_in_worktree.contains(&&SelectedEntry {
worktree_id,
entry_id: sibling.id,
})
})
.map(|entry| entry.to_owned())
.collect();
let repo_snapshots = git_store.repo_snapshots(cx);
let worktree_snapshot = worktree.snapshot();
let hide_gitignore = ProjectPanelSettings::get_global(cx).hide_gitignore;
let mut siblings: Vec<_> =
ChildEntriesGitIter::new(&repo_snapshots, &worktree_snapshot, parent_path)
.filter(|sibling| {
(sibling.id == latest_entry.id)
|| (!marked_entries_in_worktree.contains(&&SelectedEntry {
worktree_id,
entry_id: sibling.id,
}) && (!hide_gitignore || !sibling.is_ignored))
})
.map(|entry| entry.to_owned())
.collect();
project::sort_worktree_entries(&mut siblings);
let sibling_entry_index = siblings
@@ -2590,8 +2605,11 @@ impl ProjectPanel {
new_selected_entry: Option<(WorktreeId, ProjectEntryId)>,
cx: &mut Context<Self>,
) {
let auto_collapse_dirs = ProjectPanelSettings::get_global(cx).auto_fold_dirs;
let settings = ProjectPanelSettings::get_global(cx);
let auto_collapse_dirs = settings.auto_fold_dirs;
let hide_gitignore = settings.hide_gitignore;
let project = self.project.read(cx);
let repo_snapshots = project.git_store().read(cx).repo_snapshots(cx);
self.last_worktree_root_id = project
.visible_worktrees(cx)
.next_back()
@@ -2602,15 +2620,15 @@ impl ProjectPanel {
self.visible_entries.clear();
let mut max_width_item = None;
for worktree in project.visible_worktrees(cx) {
let snapshot = worktree.read(cx).snapshot();
let worktree_id = snapshot.id();
let worktree_snapshot = worktree.read(cx).snapshot();
let worktree_id = worktree_snapshot.id();
let expanded_dir_ids = match self.expanded_dir_ids.entry(worktree_id) {
hash_map::Entry::Occupied(e) => e.into_mut(),
hash_map::Entry::Vacant(e) => {
// The first time a worktree's root entry becomes available,
// mark that root entry as expanded.
if let Some(entry) = snapshot.root_entry() {
if let Some(entry) = worktree_snapshot.root_entry() {
e.insert(vec![entry.id]).as_slice()
} else {
&[]
@@ -2632,14 +2650,15 @@ impl ProjectPanel {
}
let mut visible_worktree_entries = Vec::new();
let mut entry_iter = GitTraversal::new(snapshot.entries(true, 0));
let mut entry_iter =
GitTraversal::new(&repo_snapshots, worktree_snapshot.entries(true, 0));
let mut auto_folded_ancestors = vec![];
while let Some(entry) = entry_iter.entry() {
if auto_collapse_dirs && entry.kind.is_dir() {
auto_folded_ancestors.push(entry.id);
if !self.unfolded_dir_ids.contains(&entry.id) {
if let Some(root_path) = snapshot.root_entry() {
let mut child_entries = snapshot.child_entries(&entry.path);
if let Some(root_path) = worktree_snapshot.root_entry() {
let mut child_entries = worktree_snapshot.child_entries(&entry.path);
if let Some(child) = child_entries.next() {
if entry.path != root_path.path
&& child_entries.next().is_none()
@@ -2675,7 +2694,9 @@ impl ProjectPanel {
}
}
auto_folded_ancestors.clear();
visible_worktree_entries.push(entry.to_owned());
if !hide_gitignore || !entry.is_ignored {
visible_worktree_entries.push(entry.to_owned());
}
let precedes_new_entry = if let Some(new_entry_id) = new_entry_parent_id {
entry.id == new_entry_id || {
self.ancestors.get(&entry.id).map_or(false, |entries| {
@@ -2688,7 +2709,7 @@ impl ProjectPanel {
} else {
false
};
if precedes_new_entry {
if precedes_new_entry && (!hide_gitignore || !entry.is_ignored) {
visible_worktree_entries.push(GitEntry {
entry: Entry {
id: NEW_ENTRY_ID,
@@ -3282,10 +3303,16 @@ impl ProjectPanel {
.cloned();
}
let repo_snapshots = self
.project
.read(cx)
.git_store()
.read(cx)
.repo_snapshots(cx);
let worktree = self.project.read(cx).worktree_for_id(worktree_id, cx)?;
worktree.update(cx, |tree, _| {
utils::ReversibleIterable::new(
GitTraversal::new(tree.entries(true, 0usize)),
GitTraversal::new(&repo_snapshots, tree.entries(true, 0usize)),
reverse_search,
)
.find_single_ended(|ele| predicate(*ele, worktree_id))
@@ -3305,6 +3332,12 @@ impl ProjectPanel {
.iter()
.map(|(worktree_id, _, _)| *worktree_id)
.collect();
let repo_snapshots = self
.project
.read(cx)
.git_store()
.read(cx)
.repo_snapshots(cx);
let mut last_found: Option<SelectedEntry> = None;
@@ -3319,12 +3352,10 @@ impl ProjectPanel {
let root_entry = tree.root_entry()?;
let tree_id = tree.id();
let mut first_iter = GitTraversal::new(tree.traverse_from_path(
true,
true,
true,
entry.path.as_ref(),
));
let mut first_iter = GitTraversal::new(
&repo_snapshots,
tree.traverse_from_path(true, true, true, entry.path.as_ref()),
);
if reverse_search {
first_iter.next();
@@ -3337,7 +3368,7 @@ impl ProjectPanel {
.find(|ele| predicate(*ele, tree_id))
.map(|ele| ele.to_owned());
let second_iter = GitTraversal::new(tree.entries(true, 0usize));
let second_iter = GitTraversal::new(&repo_snapshots, tree.entries(true, 0usize));
let second = if reverse_search {
second_iter

View File

@@ -31,6 +31,7 @@ pub enum EntrySpacing {
#[derive(Deserialize, Debug, Clone, Copy, PartialEq)]
pub struct ProjectPanelSettings {
pub button: bool,
pub hide_gitignore: bool,
pub default_width: Pixels,
pub dock: ProjectPanelDockPosition,
pub entry_spacing: EntrySpacing,
@@ -93,6 +94,10 @@ pub struct ProjectPanelSettingsContent {
///
/// Default: true
pub button: Option<bool>,
/// Whether to hide gitignore files in the project panel.
///
/// Default: false
pub hide_gitignore: Option<bool>,
/// Customize default width (in pixels) taken by project panel
///
/// Default: 240

View File

@@ -3735,6 +3735,172 @@ async fn test_basic_file_deletion_scenarios(cx: &mut gpui::TestAppContext) {
);
}
#[gpui::test]
async fn test_deletion_gitignored(cx: &mut gpui::TestAppContext) {
init_test_with_editor(cx);
let fs = FakeFs::new(cx.executor().clone());
fs.insert_tree(
path!("/root"),
json!({
"aa": "// Testing 1",
"bb": "// Testing 2",
"cc": "// Testing 3",
"dd": "// Testing 4",
"ee": "// Testing 5",
"ff": "// Testing 6",
"gg": "// Testing 7",
"hh": "// Testing 8",
"ii": "// Testing 8",
".gitignore": "bb\ndd\nee\nff\nii\n'",
}),
)
.await;
let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
let cx = &mut VisualTestContext::from_window(*workspace, cx);
// Test 1: Auto selection with one gitignored file next to the deleted file
cx.update(|_, cx| {
let settings = *ProjectPanelSettings::get_global(cx);
ProjectPanelSettings::override_global(
ProjectPanelSettings {
hide_gitignore: true,
..settings
},
cx,
);
});
let panel = workspace.update(cx, ProjectPanel::new).unwrap();
select_path(&panel, "root/aa", cx);
assert_eq!(
visible_entries_as_strings(&panel, 0..10, cx),
&[
"v root",
" .gitignore",
" aa <== selected",
" cc",
" gg",
" hh"
],
"Initial state should hide files on .gitignore"
);
submit_deletion(&panel, cx);
assert_eq!(
visible_entries_as_strings(&panel, 0..10, cx),
&[
"v root",
" .gitignore",
" cc <== selected",
" gg",
" hh"
],
"Should select next entry not on .gitignore"
);
// Test 2: Auto selection with many gitignored files next to the deleted file
submit_deletion(&panel, cx);
assert_eq!(
visible_entries_as_strings(&panel, 0..10, cx),
&[
"v root",
" .gitignore",
" gg <== selected",
" hh"
],
"Should select next entry not on .gitignore"
);
// Test 3: Auto selection of entry before deleted file
select_path(&panel, "root/hh", cx);
assert_eq!(
visible_entries_as_strings(&panel, 0..10, cx),
&[
"v root",
" .gitignore",
" gg",
" hh <== selected"
],
"Should select next entry not on .gitignore"
);
submit_deletion(&panel, cx);
assert_eq!(
visible_entries_as_strings(&panel, 0..10, cx),
&["v root", " .gitignore", " gg <== selected"],
"Should select next entry not on .gitignore"
);
}
#[gpui::test]
async fn test_nested_deletion_gitignore(cx: &mut gpui::TestAppContext) {
init_test_with_editor(cx);
let fs = FakeFs::new(cx.executor().clone());
fs.insert_tree(
path!("/root"),
json!({
"dir1": {
"file1": "// Testing",
"file2": "// Testing",
"file3": "// Testing"
},
"aa": "// Testing",
".gitignore": "file1\nfile3\n",
}),
)
.await;
let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
let cx = &mut VisualTestContext::from_window(*workspace, cx);
cx.update(|_, cx| {
let settings = *ProjectPanelSettings::get_global(cx);
ProjectPanelSettings::override_global(
ProjectPanelSettings {
hide_gitignore: true,
..settings
},
cx,
);
});
let panel = workspace.update(cx, ProjectPanel::new).unwrap();
// Test 1: Visible items should exclude files on gitignore
toggle_expand_dir(&panel, "root/dir1", cx);
select_path(&panel, "root/dir1/file2", cx);
assert_eq!(
visible_entries_as_strings(&panel, 0..10, cx),
&[
"v root",
" v dir1",
" file2 <== selected",
" .gitignore",
" aa"
],
"Initial state should hide files on .gitignore"
);
submit_deletion(&panel, cx);
// Test 2: Auto selection should go to the parent
assert_eq!(
visible_entries_as_strings(&panel, 0..10, cx),
&[
"v root",
" v dir1 <== selected",
" .gitignore",
" aa"
],
"Initial state should hide files on .gitignore"
);
}
#[gpui::test]
async fn test_complex_selection_scenarios(cx: &mut gpui::TestAppContext) {
init_test_with_editor(cx);

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