Compare commits

..

97 Commits

Author SHA1 Message Date
Richard Feldman
de95efb7bb wip 2025-03-28 10:34:57 -04:00
Richard Feldman
60a7455f12 assistant2: Don't scroll down if user has scrolled up (#27614)
This caused undesirable and unnecessary scrolling, in that if you scroll
up, then as new messages stream into the panel, they jump the scroll bar
back down.

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

Release Notes:

- N/A

---------

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

Release Notes:

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

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

Release Notes:

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

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

Release Notes:

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

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

Release Notes:

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

Release Notes:

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

Release Notes:

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

Release Notes:

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

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

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

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

Release Notes:

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

Release Notes:

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


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

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


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

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

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


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

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



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



Release Notes:

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

Release Notes:

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

- Added support for DeepSeek R1 hosted on AWS Bedrock.

---------

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

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

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

Release Notes:

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

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

Release Notes:

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

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

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

Release Notes:

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

- N/A

---------

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

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

Release Notes:

- N/A

---------

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

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

Release Notes:

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

This also fixes a related subscription leak.

Release Notes:

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

Release Notes:

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

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


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

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

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

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

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

---

#### Small downsight of this approach

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

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

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

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

Release Notes:

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

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

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

Release Notes:

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

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

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

Release Notes:

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

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

Release Notes:

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


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

Release Notes:

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

Release Notes:

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

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

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

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

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

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

Release Notes:

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

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

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

Release Notes:

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

Release Notes:

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

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

Release Notes:

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

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

Release Notes:

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

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

Release Notes:

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

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

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

Release Notes:

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

Release Notes:

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

- N/A
2025-03-27 13:51:32 -03:00
Marshall Bowers
2dee03ebca assistant2: Allow customizing tools for default profiles (#27594)
This PR adds support for customizing the tools for the default profiles.

Release Notes:

- N/A
2025-03-27 15:13:00 +00:00
Danilo Leal
1c7cf1a5c1 docs: Clarify how to turn edit predictions off (#27592)
Closes https://github.com/zed-industries/zed/issues/27590

Release Notes:

- N/A
2025-03-27 11:25:52 -03:00
Agus Zubiaga
f15a241d3e assistant2: Serialize token usage (#27586)
We'll need this for detecting old long threads

Release Notes:

- N/A
2025-03-27 13:38:08 +00:00
Richard Feldman
76d3a9a0f0 Retry on 5xx errors from cloud language model providers (#27584)
Release Notes:

- N/A
2025-03-27 09:35:16 -04:00
Peter Tripp
e6c473a488 html: Update HTML Extension to v0.2.0 (#27548)
Includes:
- https://github.com/zed-industries/zed/pull/27524

Release Notes:

- N/A
2025-03-27 09:00:30 -04:00
张小白
06960670bd windows: Enable collab tests (#27587)
Release Notes:

- N/A
2025-03-27 20:42:22 +08:00
renovate[bot]
71ddb3dad4 Update Rust crate log to v0.4.27 (#27540)
This PR contains the following updates:

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

---

### Release Notes

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

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

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

</details>

---

### Configuration

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

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

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

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

---

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

---

Release Notes:

- N/A

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

---------

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

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

---

### Release Notes

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

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

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

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

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

#### What's Changed

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

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

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

</details>

---

### Configuration

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

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

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

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

---

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

---

Release Notes:

- N/A

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

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

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

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

---

### Release Notes

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

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

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

version 1.7.0

Changes since 1.6.1:

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

</details>

---

### Configuration

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

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

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

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

---

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

---

Release Notes:

- N/A

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

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

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

---

### Release Notes

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

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

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

Features:

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

Bug fixes:

- JPEG encoding bugfix
([#&#8203;2387](https://redirect.github.com/image-rs/image/pull/2387))
- Expanded ICO format detection
([#&#8203;2434](https://redirect.github.com/image-rs/image/pull/2434))
- Fixed EXR bug with NaNs
([#&#8203;2381](https://redirect.github.com/image-rs/image/pull/2381))
-   Various documentation improvements

</details>

---

### Configuration

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

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

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

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

---

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

---

Release Notes:

- N/A

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

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

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

---

### Release Notes

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

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

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

#### What's Changed

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

**Full Changelog**:
https://github.com/bitflags/bitflags/compare/2.8.0...2.9.0

</details>

---

### Configuration

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

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

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

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

---

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

---

Release Notes:

- N/A

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

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

Release Notes:

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

Release Notes:

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

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

---

### Release Notes

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

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

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

##### Other

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

</details>

---

### Configuration

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

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

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

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

---

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

---

Release Notes:

- N/A

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

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

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

---

### Release Notes

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

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

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

##### Fixed

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

</details>

---

### Configuration

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

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

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

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

---

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

---

Release Notes:

- N/A

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

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

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

---

### Release Notes

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

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

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

##### Fixes

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

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

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

##### Fixes

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

</details>

---

### Configuration

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

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

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

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

---

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

---

Release Notes:

- N/A

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

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

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

---

### Release Notes

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

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

[Compare
Source](be6b3bb160...f6e0345911)

</details>

---

### Configuration

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

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

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

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

---

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

---

Release Notes:

- N/A

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

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

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

---

### Release Notes

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

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

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

</details>

---

### Configuration

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

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

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

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

---

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

---

Release Notes:

- N/A

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

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

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

---

### Release Notes

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

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

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

#### What's Changed

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

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

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

</details>

---

### Configuration

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

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

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

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

---

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

---

Release Notes:

- N/A

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

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

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

---

### Release Notes

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

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

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

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

</details>

---

### Configuration

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

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

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

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

---

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

---

Release Notes:

- N/A

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

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

Release Notes:

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


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

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

Release Notes:

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

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

---------

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

Release Notes:

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

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

---------

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

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

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

Release Notes:

- N/A

---------

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

- N/A

---------

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

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

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

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



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


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

Release Notes:

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

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

Release Notes:

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

Release Notes:

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

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

Release Notes:

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

Release Notes:

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

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

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

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

Release Notes:

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

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

Release Notes:

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

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

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

Release Notes:

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

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

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

Release Notes:

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

---------

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

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

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

Release Notes:

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

Release Notes:

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

Release Notes:

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

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

Release Notes:

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

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

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

Release Notes:

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

Release Notes:

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

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

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

Release Notes:

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

Release Notes:

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

Release Notes:

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

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

```

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

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

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

- With `"project_panel.hide_gitignore": true` 

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

- Action `ProjectPanel::ToggleHideGitIgnore`

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

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


Release Notes:

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

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

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


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

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

Release Notes:

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

-N/A
2025-03-26 11:15:42 -04:00
192 changed files with 7316 additions and 5238 deletions

277
Cargo.lock generated
View File

@@ -86,7 +86,7 @@ version = "0.25.1-dev"
source = "git+https://github.com/zed-industries/alacritty.git?branch=add-hush-login-flag#828457c9ff1f7ea0a0469337cc8a37ee3a1b0590"
dependencies = [
"base64 0.22.1",
"bitflags 2.8.0",
"bitflags 2.9.0",
"home",
"libc",
"log",
@@ -128,7 +128,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed7572b7ba83a31e20d1b48970ee402d2e3e0537dcfe0a3ff4d6eb7508617d43"
dependencies = [
"alsa-sys",
"bitflags 2.8.0",
"bitflags 2.9.0",
"cfg-if",
"libc",
]
@@ -413,7 +413,6 @@ dependencies = [
"menu",
"multi_buffer",
"parking_lot",
"paths",
"pretty_assertions",
"project",
"prompt_library",
@@ -423,7 +422,6 @@ dependencies = [
"rope",
"schemars",
"search",
"semantic_index",
"serde",
"serde_json_lenient",
"settings",
@@ -459,6 +457,7 @@ dependencies = [
"collections",
"command_palette_hooks",
"context_server",
"convert_case 0.8.0",
"db",
"editor",
"feature_flags",
@@ -580,6 +579,7 @@ dependencies = [
"client",
"collections",
"context_server",
"dap",
"env_logger 0.11.7",
"fs",
"futures 0.3.31",
@@ -669,14 +669,10 @@ dependencies = [
"http_client",
"indexed_docs",
"language",
"language_model",
"log",
"pretty_assertions",
"project",
"prompt_store",
"rope",
"schemars",
"semantic_index",
"serde",
"serde_json",
"settings",
@@ -724,6 +720,7 @@ dependencies = [
"itertools 0.14.0",
"language",
"language_model",
"open",
"project",
"rand 0.8.5",
"release_channel",
@@ -799,9 +796,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",
@@ -1880,10 +1877,10 @@ version = "0.69.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "271383c67ccabffb7381723dea0672a673f292304fcb45c01cc648c7a8d58088"
dependencies = [
"bitflags 2.8.0",
"bitflags 2.9.0",
"cexpr",
"clang-sys",
"itertools 0.12.1",
"itertools 0.10.5",
"lazy_static",
"lazycell",
"log",
@@ -1903,10 +1900,10 @@ version = "0.70.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f49d8fed880d473ea71efb9bf597651e77201bdd4893efe54c9e5d65ae04ce6f"
dependencies = [
"bitflags 2.8.0",
"bitflags 2.9.0",
"cexpr",
"clang-sys",
"itertools 0.12.1",
"itertools 0.10.5",
"log",
"prettyplease",
"proc-macro2",
@@ -1961,9 +1958,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]]
name = "bitflags"
version = "2.8.0"
version = "2.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f68f53c83ab957f72c32642f3868eec03eb974d1fb82e453128456482613d36"
checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd"
dependencies = [
"serde",
]
@@ -1993,7 +1990,7 @@ source = "git+https://github.com/kvark/blade?rev=b16f5c7bd873c7126f48c82c39e7ae6
dependencies = [
"ash",
"ash-window",
"bitflags 2.8.0",
"bitflags 2.9.0",
"bytemuck",
"codespan-reporting",
"glow",
@@ -2041,9 +2038,9 @@ dependencies = [
[[package]]
name = "blake3"
version = "1.6.1"
version = "1.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "675f87afced0413c9bb02843499dbbd3882a237645883f71a2b59644a6d2f753"
checksum = "b17679a8d69b6d7fd9cd9801a536cec9fa5e5970b69f9d4747f70b39b031f5e7"
dependencies = [
"arrayref",
"arrayvec",
@@ -2322,7 +2319,7 @@ version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b99da2f8558ca23c71f4fd15dc57c906239752dd27ff3c00a1d56b685b7cbfec"
dependencies = [
"bitflags 2.8.0",
"bitflags 2.9.0",
"log",
"polling",
"rustix",
@@ -2668,9 +2665,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 +2675,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",
@@ -2828,7 +2825,7 @@ version = "0.26.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f79398230a6e2c08f5c9760610eb6924b52aa9e7950a619602baba59dcbbdbb2"
dependencies = [
"bitflags 2.8.0",
"bitflags 2.9.0",
"block",
"cocoa-foundation 0.2.0",
"core-foundation 0.10.0",
@@ -2858,7 +2855,7 @@ version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e14045fb83be07b5acf1c0884b2180461635b433455fa35d1cd6f17f1450679d"
dependencies = [
"bitflags 2.8.0",
"bitflags 2.9.0",
"block",
"core-foundation 0.10.0",
"core-graphics-types 0.2.0",
@@ -3327,7 +3324,7 @@ version = "0.24.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fa95a34622365fa5bbf40b20b75dba8dfa8c94c734aea8ac9a5ca38af14316f1"
dependencies = [
"bitflags 2.8.0",
"bitflags 2.9.0",
"core-foundation 0.10.0",
"core-graphics-types 0.2.0",
"foreign-types 0.5.0",
@@ -3351,7 +3348,7 @@ version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb"
dependencies = [
"bitflags 2.8.0",
"bitflags 2.9.0",
"core-foundation 0.10.0",
"libc",
]
@@ -3423,7 +3420,7 @@ version = "0.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e418dd4f5128c3e93eab12246391c54a20c496811131f85754dc8152ee207892"
dependencies = [
"bitflags 2.8.0",
"bitflags 2.9.0",
"fontdb 0.16.2",
"log",
"rangemap",
@@ -3874,6 +3871,7 @@ dependencies = [
"node_runtime",
"parking_lot",
"paths",
"regex",
"schemars",
"serde",
"serde_json",
@@ -3907,7 +3905,6 @@ dependencies = [
"regex",
"serde",
"serde_json",
"sysinfo",
"task",
"util",
]
@@ -4641,6 +4638,7 @@ dependencies = [
"client",
"clock",
"collections",
"dap",
"env_logger 0.11.7",
"feature_flags",
"fs",
@@ -4749,6 +4747,7 @@ dependencies = [
"env_logger 0.11.7",
"extension",
"fs",
"gpui",
"language",
"log",
"reqwest_client",
@@ -5075,7 +5074,7 @@ name = "font-kit"
version = "0.14.1"
source = "git+https://github.com/zed-industries/font-kit?rev=40391b7#40391b7c0041d8a8572af2afa3de32ae088f0120"
dependencies = [
"bitflags 2.8.0",
"bitflags 2.9.0",
"byteorder",
"core-foundation 0.9.4",
"core-graphics 0.23.2",
@@ -5228,7 +5227,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",
@@ -5275,7 +5274,7 @@ checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c"
name = "fsevent"
version = "0.1.0"
dependencies = [
"bitflags 2.8.0",
"bitflags 2.9.0",
"core-foundation 0.9.4",
"fsevent-sys 3.1.0",
"parking_lot",
@@ -5603,7 +5602,7 @@ version = "0.20.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5220b8ba44c68a9a7f7a7659e864dd73692e417ef0211bea133c7b74e031eeb9"
dependencies = [
"bitflags 2.8.0",
"bitflags 2.9.0",
"libc",
"libgit2-sys",
"log",
@@ -5769,7 +5768,7 @@ version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fbcd2dba93594b227a1f57ee09b8b9da8892c34d55aa332e034a228d0fe6a171"
dependencies = [
"bitflags 2.8.0",
"bitflags 2.9.0",
"gpu-alloc-types",
]
@@ -5790,7 +5789,7 @@ version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "98ff03b468aa837d70984d55f5d3f846f6ec31fe34bbb97c4f85219caeee1ca4"
dependencies = [
"bitflags 2.8.0",
"bitflags 2.9.0",
]
[[package]]
@@ -6102,7 +6101,7 @@ version = "0.21.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bd54745cfacb7b97dee45e8fdb91814b62bccddb481debb7de0f9ee6b7bf5b43"
dependencies = [
"bitflags 2.8.0",
"bitflags 2.9.0",
"byteorder",
"heed-traits",
"heed-types",
@@ -6662,9 +6661,9 @@ dependencies = [
[[package]]
name = "image"
version = "0.25.5"
version = "0.25.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cd6f44aed642f18953a158afeb30206f4d50da59fbc66ecb53c66488de73563b"
checksum = "db35664ce6b9810857a38a906215e75a9c879f0696556a39f59c62829710251a"
dependencies = [
"bytemuck",
"byteorder-lite",
@@ -6833,24 +6832,13 @@ 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"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f37dccff2791ab604f9babef0ba14fbe0be30bd368dc541e2b08d07c8aa908f3"
dependencies = [
"bitflags 2.8.0",
"bitflags 2.9.0",
"inotify-sys",
"libc",
]
@@ -6949,7 +6937,7 @@ dependencies = [
"fnv",
"lazy_static",
"libc",
"mio 1.0.3",
"mio",
"rand 0.8.5",
"serde",
"tempfile",
@@ -7566,7 +7554,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]]
@@ -7591,7 +7579,7 @@ version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d"
dependencies = [
"bitflags 2.8.0",
"bitflags 2.9.0",
"libc",
"redox_syscall 0.5.8",
]
@@ -7870,9 +7858,9 @@ dependencies = [
[[package]]
name = "log"
version = "0.4.26"
version = "0.4.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "30bde2b3dc3671ae49d8e2e9f044c7c005836e7a023ee57cffa25ab82764bb9e"
checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94"
dependencies = [
"serde",
"value-bag",
@@ -8151,7 +8139,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",
@@ -8230,7 +8218,7 @@ version = "0.31.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f569fb946490b5743ad69813cb19629130ce9374034abe31614a36402d18f99e"
dependencies = [
"bitflags 2.8.0",
"bitflags 2.9.0",
"block",
"core-graphics-types 0.1.3",
"foreign-types 0.5.0",
@@ -8300,18 +8288,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"
@@ -8407,7 +8383,7 @@ checksum = "364f94bc34f61332abebe8cad6f6cd82a5b65cff22c828d05d0968911462ca4f"
dependencies = [
"arrayvec",
"bit-set 0.8.0",
"bitflags 2.8.0",
"bitflags 2.9.0",
"cfg_aliases 0.1.1",
"codespan-reporting",
"hexf-parse",
@@ -8476,7 +8452,7 @@ version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2076a31b7010b17a38c01907c45b945e8f11495ee4dd588309718901b1f7a5b7"
dependencies = [
"bitflags 2.8.0",
"bitflags 2.9.0",
"jni-sys",
"log",
"ndk-sys",
@@ -8511,7 +8487,7 @@ version = "0.29.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46"
dependencies = [
"bitflags 2.8.0",
"bitflags 2.9.0",
"cfg-if",
"cfg_aliases 0.2.1",
"libc",
@@ -8591,38 +8567,37 @@ dependencies = [
[[package]]
name = "notify"
version = "6.1.1"
version = "8.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6205bd8bb1e454ad2e27422015fb5e4f2bcc7e08fa8f27058670d208324a4d2d"
checksum = "2fee8403b3d66ac7b26aee6e40a897d85dc5ce26f44da36b8b73e987cc52e943"
dependencies = [
"bitflags 2.8.0",
"crossbeam-channel",
"bitflags 2.9.0",
"filetime",
"fsevent-sys 4.1.0",
"inotify 0.9.6",
"inotify",
"kqueue",
"libc",
"log",
"mio 0.8.11",
"mio",
"notify-types 2.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
"walkdir",
"windows-sys 0.48.0",
"windows-sys 0.59.0",
]
[[package]]
name = "notify"
version = "8.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2fee8403b3d66ac7b26aee6e40a897d85dc5ce26f44da36b8b73e987cc52e943"
source = "git+https://github.com/zed-industries/notify.git?rev=bbb9ea5ae52b253e095737847e367c30653a2e96#bbb9ea5ae52b253e095737847e367c30653a2e96"
dependencies = [
"bitflags 2.8.0",
"bitflags 2.9.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 (git+https://github.com/zed-industries/notify.git?rev=bbb9ea5ae52b253e095737847e367c30653a2e96)",
"walkdir",
"windows-sys 0.59.0",
]
@@ -8634,8 +8609,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 +8620,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"
@@ -8885,7 +8865,7 @@ version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e4e89ad9e3d7d297152b17d39ed92cd50ca8063a89a9fa569046d41568891eff"
dependencies = [
"bitflags 2.8.0",
"bitflags 2.9.0",
"block2",
"libc",
"objc2",
@@ -8901,7 +8881,7 @@ version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "74dd3b56391c7a0596a295029734d3c1c5e7e510a4cb30245f8221ccea96b009"
dependencies = [
"bitflags 2.8.0",
"bitflags 2.9.0",
"block2",
"objc2",
"objc2-core-location",
@@ -8925,7 +8905,7 @@ version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "617fbf49e071c178c0b24c080767db52958f716d9eabdf0890523aeae54773ef"
dependencies = [
"bitflags 2.8.0",
"bitflags 2.9.0",
"block2",
"objc2",
"objc2-foundation",
@@ -8967,7 +8947,7 @@ version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8"
dependencies = [
"bitflags 2.8.0",
"bitflags 2.9.0",
"block2",
"libc",
"objc2",
@@ -8991,7 +8971,7 @@ version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6"
dependencies = [
"bitflags 2.8.0",
"bitflags 2.9.0",
"block2",
"objc2",
"objc2-foundation",
@@ -9003,7 +8983,7 @@ version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a"
dependencies = [
"bitflags 2.8.0",
"bitflags 2.9.0",
"block2",
"objc2",
"objc2-foundation",
@@ -9026,7 +9006,7 @@ version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8bb46798b20cd6b91cbd113524c490f1686f4c4e8f49502431415f3512e2b6f"
dependencies = [
"bitflags 2.8.0",
"bitflags 2.9.0",
"block2",
"objc2",
"objc2-cloud-kit",
@@ -9058,7 +9038,7 @@ version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "76cfcbf642358e8689af64cee815d139339f3ed8ad05103ed5eaf73db8d84cb3"
dependencies = [
"bitflags 2.8.0",
"bitflags 2.9.0",
"block2",
"objc2",
"objc2-core-location",
@@ -9120,9 +9100,9 @@ checksum = "945462a4b81e43c4e3ba96bd7b49d834c6f61198356aa858733bc4acf3cbe62e"
[[package]]
name = "oo7"
version = "0.4.1"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72c84df357c7049f98c8b157abe71ee751531166c14ba09366e08bc6ab1ea2c9"
checksum = "6cb23d3ec3527d65a83be1c1795cb883c52cfa57147d42acc797127df56fc489"
dependencies = [
"aes",
"ashpd",
@@ -9201,7 +9181,7 @@ version = "0.10.70"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "61cfb4e166a8bb8c9b55c500bc2308550148ece889be90f609377e58140f42c6"
dependencies = [
"bitflags 2.8.0",
"bitflags 2.9.0",
"cfg-if",
"foreign-types 0.3.2",
"libc",
@@ -10245,9 +10225,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,8 +10709,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22505a5c94da8e3b7c2996394d1c933236c4d743e81a410bcca4e6989fc066a4"
dependencies = [
"bytes 1.10.1",
"heck 0.5.0",
"itertools 0.12.1",
"heck 0.4.1",
"itertools 0.10.5",
"log",
"multimap 0.10.0",
"once_cell",
@@ -10763,7 +10743,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "81bddcdb20abf9501610992b6759a4c888aef7d1a7247ef75e2404275ac24af1"
dependencies = [
"anyhow",
"itertools 0.12.1",
"itertools 0.10.5",
"proc-macro2",
"quote",
"syn 2.0.100",
@@ -10840,7 +10820,7 @@ version = "0.10.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "76979bea66e7875e7509c4ec5300112b316af87fa7a252ca91c448b32dfe3993"
dependencies = [
"bitflags 2.8.0",
"bitflags 2.9.0",
"memchr",
"pulldown-cmark-escape",
"unicase",
@@ -10852,7 +10832,7 @@ version = "0.12.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f86ba2052aebccc42cbbb3ed234b8b13ce76f75c3551a303cb2bcffcff12bb14"
dependencies = [
"bitflags 2.8.0",
"bitflags 2.9.0",
"memchr",
"unicase",
]
@@ -11233,7 +11213,7 @@ version = "0.5.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "03a862b389f93e68874fbf580b9de08dd02facb9a788ebadaf4a3fd33cf58834"
dependencies = [
"bitflags 2.8.0",
"bitflags 2.9.0",
]
[[package]]
@@ -11366,6 +11346,7 @@ dependencies = [
"clap",
"client",
"clock",
"dap",
"env_logger 0.11.7",
"extension",
"extension_host",
@@ -11876,7 +11857,7 @@ version = "0.38.42"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f93dc38ecbab2eb790ff964bb77fa94faf256fd3e73285fd7ba0903b76bedb85"
dependencies = [
"bitflags 2.8.0",
"bitflags 2.9.0",
"errno 0.3.10",
"itoa",
"libc",
@@ -12036,7 +12017,7 @@ version = "0.14.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cfb9cf8877777222e4a3bc7eb247e398b56baba500c38c1c46842431adc8b55c"
dependencies = [
"bitflags 2.8.0",
"bitflags 2.9.0",
"bytemuck",
"libm",
"smallvec",
@@ -12053,7 +12034,7 @@ version = "0.20.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fd3c7c96f8a08ee34eff8857b11b49b07d71d1c3f4e88f8a88d4c9e9f90b1702"
dependencies = [
"bitflags 2.8.0",
"bitflags 2.9.0",
"bytemuck",
"core_maths",
"log",
@@ -12276,7 +12257,7 @@ version = "0.1.0"
dependencies = [
"any_vec",
"anyhow",
"bitflags 2.8.0",
"bitflags 2.9.0",
"client",
"collections",
"editor",
@@ -12318,7 +12299,7 @@ version = "2.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02"
dependencies = [
"bitflags 2.8.0",
"bitflags 2.9.0",
"core-foundation 0.9.4",
"core-foundation-sys",
"libc",
@@ -12331,7 +12312,7 @@ version = "3.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e1415a607e92bec364ea2cf9264646dcce0f91e6d65281bd6f2819cca3bf39c8"
dependencies = [
"bitflags 2.8.0",
"bitflags 2.9.0",
"core-foundation 0.10.0",
"core-foundation-sys",
"libc",
@@ -12415,18 +12396,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",
@@ -12931,7 +12912,7 @@ version = "0.3.0+sdk-1.3.268.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eda41003dc44290527a59b13432d4a0379379fa074b70174882adfbdfd917844"
dependencies = [
"bitflags 2.8.0",
"bitflags 2.9.0",
]
[[package]]
@@ -13098,7 +13079,7 @@ dependencies = [
"atoi",
"base64 0.22.1",
"bigdecimal",
"bitflags 2.8.0",
"bitflags 2.9.0",
"byteorder",
"bytes 1.10.1",
"chrono",
@@ -13145,7 +13126,7 @@ dependencies = [
"atoi",
"base64 0.22.1",
"bigdecimal",
"bitflags 2.8.0",
"bitflags 2.9.0",
"byteorder",
"chrono",
"crc",
@@ -13253,6 +13234,7 @@ dependencies = [
"theme",
"title_bar",
"ui",
"workspace",
]
[[package]]
@@ -13607,7 +13589,7 @@ version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b"
dependencies = [
"bitflags 2.8.0",
"bitflags 2.9.0",
"core-foundation 0.9.4",
"system-configuration-sys 0.6.0",
]
@@ -13651,7 +13633,7 @@ version = "0.27.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cc4592f674ce18521c2a81483873a49596655b179f71c5e05d10c1fe66c78745"
dependencies = [
"bitflags 2.8.0",
"bitflags 2.9.0",
"cap-fs-ext",
"cap-std",
"fd-lock",
@@ -14075,9 +14057,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 +14080,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 +14223,7 @@ dependencies = [
"backtrace",
"bytes 1.10.1",
"libc",
"mio 1.0.3",
"mio",
"parking_lot",
"pin-project-lite",
"signal-hook-registry",
@@ -14471,7 +14453,7 @@ version = "0.4.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "61c5bb1d698276a2443e5ecfabc1008bf15a36c12e6a7176e7bf089ea9131140"
dependencies = [
"bitflags 2.8.0",
"bitflags 2.9.0",
"bytes 1.10.1",
"futures-core",
"futures-util",
@@ -15384,7 +15366,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a5924018406ce0063cd67f8e008104968b74b563ee1b85dde3ed1f7cb87d3dbd"
dependencies = [
"arrayvec",
"bitflags 2.8.0",
"bitflags 2.9.0",
"cursor-icon",
"log",
"memchr",
@@ -15608,7 +15590,7 @@ version = "0.201.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "84e5df6dba6c0d7fafc63a450f1738451ed7a0b52295d83e868218fa286bf708"
dependencies = [
"bitflags 2.8.0",
"bitflags 2.9.0",
"indexmap",
"semver",
]
@@ -15619,7 +15601,7 @@ version = "0.221.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d06bfa36ab3ac2be0dee563380147a5b81ba10dd8885d7fbbc9eb574be67d185"
dependencies = [
"bitflags 2.8.0",
"bitflags 2.9.0",
"hashbrown 0.15.2",
"indexmap",
"semver",
@@ -15645,7 +15627,7 @@ checksum = "11976a250672556d1c4c04c6d5d7656ac9192ac9edc42a4587d6c21460010e69"
dependencies = [
"anyhow",
"async-trait",
"bitflags 2.8.0",
"bitflags 2.9.0",
"bumpalo",
"cc",
"cfg-if",
@@ -15851,7 +15833,7 @@ checksum = "8d1be69bfcab1bdac74daa7a1f9695ab992b9c8e21b9b061e7d66434097e0ca4"
dependencies = [
"anyhow",
"async-trait",
"bitflags 2.8.0",
"bitflags 2.9.0",
"bytes 1.10.1",
"cap-fs-ext",
"cap-net-ext",
@@ -15932,7 +15914,7 @@ version = "0.31.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2120de3d33638aaef5b9f4472bff75f07c56379cf76ea320bd3a3d65ecaf73f"
dependencies = [
"bitflags 2.8.0",
"bitflags 2.9.0",
"rustix",
"wayland-backend",
"wayland-scanner",
@@ -15955,7 +15937,7 @@ version = "0.31.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f81f365b8b4a97f422ac0e8737c438024b5951734506b0e1d775c73030561f4"
dependencies = [
"bitflags 2.8.0",
"bitflags 2.9.0",
"wayland-backend",
"wayland-client",
"wayland-scanner",
@@ -15967,7 +15949,7 @@ version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "23803551115ff9ea9bce586860c5c5a971e360825a0309264102a9495a5ff479"
dependencies = [
"bitflags 2.8.0",
"bitflags 2.9.0",
"wayland-backend",
"wayland-client",
"wayland-protocols",
@@ -16134,7 +16116,7 @@ checksum = "4b9af35bc9629c52c261465320a9a07959164928b4241980ba1cf923b9e6751d"
dependencies = [
"anyhow",
"async-trait",
"bitflags 2.8.0",
"bitflags 2.9.0",
"thiserror 1.0.69",
"tracing",
"wasmtime",
@@ -16190,7 +16172,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]]
@@ -16749,7 +16731,7 @@ version = "0.36.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f3fd376f71958b862e7afb20cfe5a22830e1963462f3a17f49d82a6c1d1f42d"
dependencies = [
"bitflags 2.8.0",
"bitflags 2.9.0",
"windows-sys 0.59.0",
]
@@ -16768,7 +16750,7 @@ version = "0.22.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "288f992ea30e6b5c531b52cdd5f3be81c148554b09ea416f058d16556ba92c27"
dependencies = [
"bitflags 2.8.0",
"bitflags 2.9.0",
"wit-bindgen-rt 0.22.0",
"wit-bindgen-rust-macro",
]
@@ -16795,7 +16777,7 @@ version = "0.33.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3268f3d866458b787f390cf61f4bbb563b922d091359f9608842999eaee3943c"
dependencies = [
"bitflags 2.8.0",
"bitflags 2.9.0",
]
[[package]]
@@ -16833,7 +16815,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "421c0c848a0660a8c22e2fd217929a0191f14476b68962afd2af89fd22e39825"
dependencies = [
"anyhow",
"bitflags 2.8.0",
"bitflags 2.9.0",
"indexmap",
"log",
"serde",
@@ -16852,7 +16834,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "66c55ca8772d2b270e28066caed50ce4e53a28c3ac10e01efbd90e5be31e448b"
dependencies = [
"anyhow",
"bitflags 2.8.0",
"bitflags 2.9.0",
"indexmap",
"log",
"serde",
@@ -16925,6 +16907,7 @@ dependencies = [
"clock",
"collections",
"component",
"dap",
"db",
"derive_more",
"env_logger 0.11.7",
@@ -17095,7 +17078,7 @@ name = "xim-parser"
version = "0.2.1"
source = "git+https://github.com/XDeme1/xim-rs?rev=d50d461764c2213655cd9cf65a0ea94c70d3c4fd#d50d461764c2213655cd9cf65a0ea94c70d3c4fd"
dependencies = [
"bitflags 2.8.0",
"bitflags 2.9.0",
]
[[package]]
@@ -17272,7 +17255,7 @@ dependencies = [
[[package]]
name = "zed"
version = "0.180.3"
version = "0.181.0"
dependencies = [
"activity_indicator",
"anyhow",
@@ -17301,6 +17284,8 @@ dependencies = [
"command_palette_hooks",
"component_preview",
"copilot",
"dap",
"dap_adapters",
"db",
"debugger_tools",
"debugger_ui",
@@ -17448,7 +17433,7 @@ dependencies = [
[[package]]
name = "zed_html"
version = "0.1.6"
version = "0.2.0"
dependencies = [
"zed_extension_api 0.1.0",
]

View File

@@ -470,6 +470,7 @@ mlua = { version = "0.10", features = ["lua54", "vendored", "async", "send"] }
nanoid = "0.4"
nbformat = { version = "0.10.0" }
nix = "0.29"
open = "5.0.0"
num-format = "0.4.4"
ordered-float = "2.1.1"
palette = { version = "0.7.5", default-features = false, features = ["std"] }

View File

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

Before

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -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'.
@@ -641,6 +643,7 @@
"name": "Code Writer",
"tools": {
"bash": true,
"batch-tool": true,
"copy-path": true,
"create-file": true,
"delete-path": true,
@@ -658,6 +661,7 @@
}
}
},
// Shows a notification when the agent needs confirmation before running an edit tool call or when that's concluded.
"notify_when_agent_waiting": true
},
// The settings for slash commands.

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,5 +1,6 @@
use crate::{
AssistantPanel, AssistantPanelEvent, CycleNextInlineAssist, CyclePreviousInlineAssist,
Assistant, AssistantPanel, AssistantPanelEvent, CycleNextInlineAssist,
CyclePreviousInlineAssist,
};
use anyhow::{anyhow, Context as _, Result};
use assistant_context_editor::{humanize_token_count, RequestType};
@@ -3555,7 +3556,7 @@ impl CodeActionProvider for AssistantCodeActionProvider {
_: &mut Window,
cx: &mut App,
) -> Task<Result<Vec<CodeAction>>> {
if !AssistantSettings::get_global(cx).enabled {
if !Assistant::enabled(cx) {
return Task::ready(Ok(Vec::new()));
}
@@ -3712,7 +3713,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 +4092,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

@@ -31,6 +31,7 @@ clock.workspace = true
collections.workspace = true
command_palette_hooks.workspace = true
context_server.workspace = true
convert_case.workspace = true
db.workspace = true
editor.workspace = true
feature_flags.workspace = true

View File

@@ -5,14 +5,14 @@ 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, Hsla, Length, ListAlignment, ListOffset, ListState, ScrollHandle,
Entity, Focusable, Hsla, Length, ListAlignment, ListState, MouseButton, ScrollHandle, Stateful,
StyleRefinement, Subscription, Task, TextStyleRefinement, Transformation, UnderlineStyle,
WeakEntity, WindowHandle,
};
@@ -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)>,
@@ -46,6 +47,7 @@ pub struct ActiveThread {
last_error: Option<ThreadError>,
pop_ups: Vec<WindowHandle<ToolReadyPopUp>>,
_subscriptions: Vec<Subscription>,
pop_up_subscriptions: HashMap<WindowHandle<ToolReadyPopUp>, Vec<Subscription>>,
}
struct RenderedMessage {
@@ -226,6 +228,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,17 +248,13 @@ 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(),
_subscriptions: subscriptions,
pop_up_subscriptions: HashMap::default(),
};
for message in thread.read(cx).messages().cloned().collect::<Vec<_>>() {
@@ -311,10 +317,6 @@ impl ActiveThread {
let rendered_message =
RenderedMessage::from_segments(segments, self.language_registry.clone(), window, cx);
self.rendered_messages_by_id.insert(*id, rendered_message);
self.list_state.scroll_to(ListOffset {
item_ix: old_len,
offset_in_item: Pixels(0.0),
});
}
fn edited_message(
@@ -377,7 +379,7 @@ impl ActiveThread {
ThreadEvent::DoneStreaming => {
if !self.thread().read(cx).is_generating() {
self.show_notification(
"Your changes have been applied.",
"The assistant response has concluded.",
IconName::Check,
Color::Success,
window,
@@ -544,31 +546,64 @@ impl ActiveThread {
.log_err()
{
if let Some(pop_up) = screen_window.entity(cx).log_err() {
cx.subscribe_in(&pop_up, window, {
|this, _, event, window, cx| match event {
ToolReadyPopupEvent::Accepted => {
let handle = window.window_handle();
cx.activate(true); // Switch back to the Zed application
self.pop_up_subscriptions
.entry(screen_window)
.or_insert_with(Vec::new)
.push(cx.subscribe_in(&pop_up, window, {
|this, _, event, window, cx| match event {
ToolReadyPopupEvent::Accepted => {
let handle = window.window_handle();
cx.activate(true); // Switch back to the Zed application
// If there are multiple Zed windows, activate the correct one.
cx.defer(move |cx| {
handle
.update(cx, |_view, window, _cx| {
window.activate_window();
})
.log_err();
});
let workspace_handle = this.workspace.clone();
this.dismiss_notifications(cx);
// 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();
});
this.dismiss_notifications(cx);
}
ToolReadyPopupEvent::Dismissed => {
this.dismiss_notifications(cx);
}
}
ToolReadyPopupEvent::Dismissed => {
this.dismiss_notifications(cx);
}
}
})
.detach();
}));
self.pop_ups.push(screen_window);
// If the user manually refocuses the original window, dismiss the popup.
self.pop_up_subscriptions
.entry(screen_window)
.or_insert_with(Vec::new)
.push({
let pop_up_weak = pop_up.downgrade();
cx.observe_window_activation(window, move |_, window, cx| {
if window.is_window_active() {
if let Some(pop_up) = pop_up_weak.upgrade() {
pop_up.update(cx, |_, cx| {
cx.emit(ToolReadyPopupEvent::Dismissed);
});
}
}
})
});
}
}
}
@@ -1735,15 +1770,52 @@ impl ActiveThread {
window.remove_window();
})
.ok();
self.pop_up_subscriptions.remove(&window);
}
}
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

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

View File

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

View File

@@ -21,7 +21,7 @@ impl ProfilePicker {
window: &mut Window,
cx: &mut Context<Self>,
) -> Self {
let picker = cx.new(|cx| Picker::uniform_list(delegate, window, cx));
let picker = cx.new(|cx| Picker::uniform_list(delegate, window, cx).modal(false));
Self { picker }
}
}

View File

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

View File

@@ -225,12 +225,12 @@ impl AssistantPanel {
window: &mut Window,
cx: &mut Context<Workspace>,
) {
let settings = AssistantSettings::get_global(cx);
if !settings.enabled {
return;
if workspace
.panel::<Self>(cx)
.is_some_and(|panel| panel.read(cx).enabled(cx))
{
workspace.toggle_panel_focus::<Self>(window, cx);
}
workspace.toggle_panel_focus::<Self>(window, cx);
}
pub(crate) fn local_timezone(&self) -> UtcOffset {
@@ -637,12 +637,8 @@ impl Panel for AssistantPanel {
}
fn icon(&self, _window: &Window, cx: &App) -> Option<IconName> {
let settings = AssistantSettings::get_global(cx);
if !settings.enabled || !settings.button {
return None;
}
Some(IconName::ZedAssistant)
(self.enabled(cx) && AssistantSettings::get_global(cx).button)
.then_some(IconName::ZedAssistant)
}
fn icon_tooltip(&self, _window: &Window, _cx: &App) -> Option<&'static str> {
@@ -656,6 +652,10 @@ impl Panel for AssistantPanel {
fn activation_priority(&self) -> u32 {
3
}
fn enabled(&self, cx: &App) -> bool {
AssistantSettings::get_global(cx).enabled
}
}
impl AssistantPanel {

View File

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

View File

@@ -1,6 +1,6 @@
use std::cell::RefCell;
use std::ops::Range;
use std::path::{Path, PathBuf};
use std::path::Path;
use std::rc::Rc;
use std::sync::atomic::AtomicBool;
use std::sync::Arc;
@@ -70,18 +70,18 @@ impl ContextPickerCompletionProvider {
.filter_map(|entry| match entry {
super::RecentEntry::File {
project_path,
path_prefix: _,
} => Self::completion_for_path(
path_prefix,
} => Some(Self::completion_for_path(
project_path.clone(),
path_prefix,
true,
false,
excerpt_id,
source_range.clone(),
editor.clone(),
context_store.clone(),
workspace.clone(),
cx,
),
)),
super::RecentEntry::Thread(thread_context_entry) => {
let thread_store = thread_store
.as_ref()
@@ -120,56 +120,24 @@ impl ContextPickerCompletionProvider {
completions
}
fn full_path_for_entry(
worktree_id: WorktreeId,
path: &Path,
workspace: Entity<Workspace>,
cx: &App,
) -> Option<PathBuf> {
let worktree = workspace
.read(cx)
.project()
.read(cx)
.worktree_for_id(worktree_id, cx)?
.read(cx);
let mut full_path = PathBuf::from(worktree.root_name());
full_path.push(path);
Some(full_path)
}
fn build_code_label_for_full_path(
worktree_id: WorktreeId,
path: &Path,
workspace: Entity<Workspace>,
file_name: &str,
directory: Option<&str>,
cx: &App,
) -> Option<CodeLabel> {
) -> CodeLabel {
let comment_id = cx.theme().syntax().highlight_id("comment").map(HighlightId);
let mut label = CodeLabel::default();
let worktree = workspace
.read(cx)
.project()
.read(cx)
.worktree_for_id(worktree_id, cx)?;
let entry = worktree.read(cx).entry_for_path(&path)?;
let file_name = path.file_name()?.to_string_lossy();
label.push_str(&file_name, None);
if entry.is_dir() {
label.push_str("/ ", None);
} else {
label.push_str(" ", None);
};
label.push_str(" ", None);
let mut path_hint = PathBuf::from(worktree.read(cx).root_name());
if let Some(path_to_entry) = path.parent() {
path_hint.push(path_to_entry);
if let Some(directory) = directory {
label.push_str(&directory, comment_id);
}
label.push_str(&path_hint.to_string_lossy(), comment_id);
label.filter_range = 0..label.text().len();
Some(label)
label
}
fn completion_for_thread(
@@ -274,32 +242,36 @@ impl ContextPickerCompletionProvider {
fn completion_for_path(
project_path: ProjectPath,
path_prefix: &str,
is_recent: bool,
is_directory: bool,
excerpt_id: ExcerptId,
source_range: Range<Anchor>,
editor: Entity<Editor>,
context_store: Entity<ContextStore>,
workspace: Entity<Workspace>,
cx: &App,
) -> Option<Completion> {
) -> Completion {
let (file_name, directory) = super::file_context_picker::extract_file_name_and_directory(
&project_path.path,
path_prefix,
);
let label = Self::build_code_label_for_full_path(
project_path.worktree_id,
&project_path.path,
workspace.clone(),
&file_name,
directory.as_ref().map(|s| s.as_ref()),
cx,
)?;
let full_path = Self::full_path_for_entry(
project_path.worktree_id,
&project_path.path,
workspace.clone(),
cx,
)?;
);
let full_path = if let Some(directory) = directory {
format!("{}{}", directory, file_name)
} else {
file_name.to_string()
};
let crease_icon_path = if is_directory {
FileIcons::get_folder_icon(false, cx).unwrap_or_else(|| IconName::Folder.path().into())
} else {
FileIcons::get_icon(&full_path, cx).unwrap_or_else(|| IconName::File.path().into())
FileIcons::get_icon(Path::new(&full_path), cx)
.unwrap_or_else(|| IconName::File.path().into())
};
let completion_icon_path = if is_recent {
IconName::HistoryRerun.path().into()
@@ -307,15 +279,9 @@ impl ContextPickerCompletionProvider {
crease_icon_path.clone()
};
let crease_name = project_path
.path
.file_name()
.map(|file_name| file_name.to_string_lossy().to_string())
.unwrap_or_else(|| "untitled".to_string());
let new_text = format!("@file {}", full_path.to_string_lossy());
let new_text = format!("@file {}", full_path);
let new_text_len = new_text.len();
Some(Completion {
Completion {
old_range: source_range.clone(),
new_text,
label,
@@ -324,7 +290,7 @@ impl ContextPickerCompletionProvider {
icon_path: Some(completion_icon_path),
confirm: Some(confirm_completion_callback(
crease_icon_path,
crease_name.into(),
file_name,
excerpt_id,
source_range.start,
new_text_len,
@@ -340,7 +306,7 @@ impl ContextPickerCompletionProvider {
})
},
)),
})
}
}
}
@@ -397,33 +363,34 @@ impl CompletionProvider for ContextPickerCompletionProvider {
.update(|cx| {
super::file_context_picker::search_paths(
query,
Arc::new(AtomicBool::default()),
Arc::<AtomicBool>::default(),
&workspace,
cx,
)
})?
.await;
completions.reserve(path_matches.len());
cx.update(|cx| {
completions.extend(path_matches.iter().filter_map(|mat| {
let editor = editor.upgrade()?;
Self::completion_for_path(
ProjectPath {
worktree_id: WorktreeId::from_usize(mat.worktree_id),
path: mat.path.clone(),
},
false,
mat.is_dir,
excerpt_id,
source_range.clone(),
editor.clone(),
context_store.clone(),
workspace.clone(),
cx,
)
}));
})?;
if let Some(editor) = editor.upgrade() {
completions.reserve(path_matches.len());
cx.update(|cx| {
completions.extend(path_matches.iter().map(|mat| {
Self::completion_for_path(
ProjectPath {
worktree_id: WorktreeId::from_usize(mat.worktree_id),
path: mat.path.clone(),
},
&mat.path_prefix,
false,
mat.is_dir,
excerpt_id,
source_range.clone(),
editor.clone(),
context_store.clone(),
cx,
)
}));
})?;
}
}
Some(ContextPickerMode::Fetch) => {
if let Some(editor) = editor.upgrade() {
@@ -544,6 +511,10 @@ impl CompletionProvider for ContextPickerCompletionProvider {
fn sort_completions(&self) -> bool {
false
}
fn filter_completions(&self) -> bool {
false
}
}
fn confirm_completion_callback(
@@ -767,7 +738,6 @@ mod tests {
.unwrap();
}
//TODO: Construct the editor without an actual buffer that points to a file
let item = workspace
.update_in(&mut cx, |workspace, window, cx| {
workspace.open_path(
@@ -817,10 +787,10 @@ mod tests {
assert_eq!(
current_completion_labels(editor),
&[
format!("seven.txt {}", separator!("dir/b")).as_str(),
format!("six.txt {}", separator!("dir/b")).as_str(),
format!("five.txt {}", separator!("dir/b")).as_str(),
format!("four.txt {}", separator!("dir/a")).as_str(),
"seven.txt dir/b/",
"six.txt dir/b/",
"five.txt dir/b/",
"four.txt dir/a/",
"Files & Directories",
"Fetch"
]
@@ -849,10 +819,7 @@ mod tests {
editor.update(&mut cx, |editor, cx| {
assert_eq!(editor.text(cx), "Lorem @file one");
assert!(editor.has_visible_completions_menu());
assert_eq!(
current_completion_labels(editor),
vec![format!("one.txt {}", separator!("dir/a")).as_str(),]
);
assert_eq!(current_completion_labels(editor), vec!["one.txt dir/a/"]);
});
editor.update_in(&mut cx, |editor, window, cx| {
@@ -861,10 +828,7 @@ mod tests {
});
editor.update(&mut cx, |editor, cx| {
assert_eq!(
editor.text(cx),
format!("Lorem @file {}", separator!("dir/a/one.txt"))
);
assert_eq!(editor.text(cx), "Lorem @file dir/a/one.txt",);
assert!(!editor.has_visible_completions_menu());
assert_eq!(
crease_ranges(editor, cx),
@@ -875,10 +839,7 @@ mod tests {
cx.simulate_input(" ");
editor.update(&mut cx, |editor, cx| {
assert_eq!(
editor.text(cx),
format!("Lorem @file {} ", separator!("dir/a/one.txt"))
);
assert_eq!(editor.text(cx), "Lorem @file dir/a/one.txt ",);
assert!(!editor.has_visible_completions_menu());
assert_eq!(
crease_ranges(editor, cx),
@@ -889,10 +850,7 @@ mod tests {
cx.simulate_input("Ipsum ");
editor.update(&mut cx, |editor, cx| {
assert_eq!(
editor.text(cx),
format!("Lorem @file {} Ipsum ", separator!("dir/a/one.txt"))
);
assert_eq!(editor.text(cx), "Lorem @file dir/a/one.txt Ipsum ",);
assert!(!editor.has_visible_completions_menu());
assert_eq!(
crease_ranges(editor, cx),
@@ -903,10 +861,7 @@ mod tests {
cx.simulate_input("@file ");
editor.update(&mut cx, |editor, cx| {
assert_eq!(
editor.text(cx),
format!("Lorem @file {} Ipsum @file ", separator!("dir/a/one.txt"))
);
assert_eq!(editor.text(cx), "Lorem @file dir/a/one.txt Ipsum @file ",);
assert!(editor.has_visible_completions_menu());
assert_eq!(
crease_ranges(editor, cx),
@@ -923,11 +878,7 @@ mod tests {
editor.update(&mut cx, |editor, cx| {
assert_eq!(
editor.text(cx),
format!(
"Lorem @file {} Ipsum @file {}",
separator!("dir/a/one.txt"),
separator!("dir/b/seven.txt")
)
"Lorem @file dir/a/one.txt Ipsum @file dir/b/seven.txt"
);
assert!(!editor.has_visible_completions_menu());
assert_eq!(
@@ -944,11 +895,7 @@ mod tests {
editor.update(&mut cx, |editor, cx| {
assert_eq!(
editor.text(cx),
format!(
"Lorem @file {} Ipsum @file {}\n@",
separator!("dir/a/one.txt"),
separator!("dir/b/seven.txt")
)
"Lorem @file dir/a/one.txt Ipsum @file dir/b/seven.txt\n@"
);
assert!(editor.has_visible_completions_menu());
assert_eq!(
@@ -969,12 +916,7 @@ mod tests {
editor.update(&mut cx, |editor, cx| {
assert_eq!(
editor.text(cx),
format!(
"Lorem @file {} Ipsum @file {}\n@file {}",
separator!("dir/a/one.txt"),
separator!("dir/b/seven.txt"),
separator!("dir/b/six.txt"),
)
"Lorem @file dir/a/one.txt Ipsum @file dir/b/seven.txt\n@file dir/b/six.txt"
);
assert!(!editor.has_visible_completions_menu());
assert_eq!(

View File

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

View File

@@ -317,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
};

View File

@@ -286,8 +286,7 @@ impl Thread {
tool_use,
action_log: cx.new(|_| ActionLog::new()),
initial_project_snapshot: Task::ready(serialized.initial_project_snapshot).shared(),
// TODO: persist token usage?
cumulative_token_usage: TokenUsage::default(),
cumulative_token_usage: serialized.cumulative_token_usage,
feedback: None,
}
}
@@ -648,6 +647,7 @@ impl Thread {
})
.collect(),
initial_project_snapshot,
cumulative_token_usage: this.cumulative_token_usage.clone(),
})
})
}
@@ -857,6 +857,13 @@ impl Thread {
request.messages.push(request_message);
}
// Set a cache breakpoint at the second-to-last message.
// https://docs.anthropic.com/en/docs/build-with-claude/prompt-caching
let breakpoint_index = request.messages.len() - 2;
for (index, message) in request.messages.iter_mut().enumerate() {
message.cache = index == breakpoint_index;
}
if !referenced_context_ids.is_empty() {
let mut context_message = LanguageModelRequestMessage {
role: Role::User,
@@ -873,17 +880,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 +908,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,
};
@@ -1392,7 +1417,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 +1436,7 @@ impl Thread {
// Get diff asynchronously
let diff = repo
.diff(git::repository::DiffType::HeadToWorktree, cx.clone())
.diff(git::repository::DiffType::HeadToWorktree)
.await
.ok();

View File

@@ -16,7 +16,7 @@ use gpui::{
};
use heed::types::SerdeBincode;
use heed::Database;
use language_model::{LanguageModelToolUseId, Role};
use language_model::{LanguageModelToolUseId, Role, TokenUsage};
use project::Project;
use prompt_store::PromptBuilder;
use serde::{Deserialize, Serialize};
@@ -308,6 +308,8 @@ pub struct SerializedThread {
pub messages: Vec<SerializedMessage>,
#[serde(default)]
pub initial_project_snapshot: Option<Arc<ProjectSnapshot>>,
#[serde(default)]
pub cumulative_token_usage: TokenUsage,
}
impl SerializedThread {
@@ -390,6 +392,7 @@ impl LegacySerializedThread {
updated_at: self.updated_at,
messages: self.messages.into_iter().map(|msg| msg.upgrade()).collect(),
initial_project_snapshot: self.initial_project_snapshot,
cumulative_token_usage: TokenUsage::default(),
}
}
}

View File

@@ -24,7 +24,7 @@ impl ToolReadyPopUp {
pub fn window_options(screen: Rc<dyn PlatformDisplay>, cx: &App) -> WindowOptions {
let size = Size {
width: px(440.),
width: px(450.),
height: px(72.),
};
@@ -93,8 +93,18 @@ impl Render for ToolReadyPopUp {
)
.child(
v_flex()
.child(Headline::new("Agent Panel").size(HeadlineSize::XSmall))
.child(Label::new(self.caption.clone()).color(Color::Muted)),
.child(
div()
.text_size(px(16.))
.text_color(cx.theme().colors().text)
.child("Agent Panel"),
)
.child(
div()
.text_size(px(14.))
.text_color(cx.theme().colors().text_muted)
.child(self.caption.clone()),
),
),
)
.child(

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

@@ -35,6 +35,7 @@ ui.workspace = true
util.workspace = true
workspace.workspace = true
worktree.workspace = true
open = { workspace = true }
[dev-dependencies]
collections = { workspace = true, features = ["test-support"] }

View File

@@ -1,5 +1,7 @@
mod bash_tool;
mod batch_tool;
mod copy_path_tool;
mod create_directory_tool;
mod create_file_tool;
mod delete_path_tool;
mod diagnostics_tool;
@@ -9,6 +11,7 @@ mod find_replace_file_tool;
mod list_directory_tool;
mod move_path_tool;
mod now_tool;
mod open_tool;
mod path_search_tool;
mod read_file_tool;
mod regex_search_tool;
@@ -24,6 +27,8 @@ use http_client::HttpClientWithUrl;
use move_path_tool::MovePathTool;
use crate::bash_tool::BashTool;
use crate::batch_tool::BatchTool;
use crate::create_directory_tool::CreateDirectoryTool;
use crate::create_file_tool::CreateFileTool;
use crate::delete_path_tool::DeletePathTool;
use crate::diagnostics_tool::DiagnosticsTool;
@@ -32,6 +37,7 @@ use crate::fetch_tool::FetchTool;
use crate::find_replace_file_tool::FindReplaceFileTool;
use crate::list_directory_tool::ListDirectoryTool;
use crate::now_tool::NowTool;
use crate::open_tool::OpenTool;
use crate::path_search_tool::PathSearchTool;
use crate::read_file_tool::ReadFileTool;
use crate::regex_search_tool::RegexSearchTool;
@@ -43,6 +49,8 @@ pub fn init(http_client: Arc<HttpClientWithUrl>, cx: &mut App) {
let registry = ToolRegistry::global(cx);
registry.register_tool(BashTool);
registry.register_tool(BatchTool);
registry.register_tool(CreateDirectoryTool);
registry.register_tool(CreateFileTool);
registry.register_tool(CopyPathTool);
registry.register_tool(DeletePathTool);
@@ -52,6 +60,7 @@ pub fn init(http_client: Arc<HttpClientWithUrl>, cx: &mut App) {
registry.register_tool(EditFilesTool);
registry.register_tool(ListDirectoryTool);
registry.register_tool(NowTool);
registry.register_tool(OpenTool);
registry.register_tool(PathSearchTool);
registry.register_tool(ReadFileTool);
registry.register_tool(RegexSearchTool);

View File

@@ -5,9 +5,11 @@ use language_model::LanguageModelRequestMessage;
use project::Project;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::path::Path;
use std::sync::Arc;
use ui::IconName;
use util::command::new_smol_command;
use util::markdown::MarkdownString;
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct BashToolInput {
@@ -17,6 +19,41 @@ pub struct BashToolInput {
cd: String,
}
/// Custom deserialization function for BashToolInput that handles missing "cd" field
/// Returns a BashToolInput with the project's first worktree root as default "cd" if missing
fn from_value_with_default_cd(value: serde_json::Value, project: Option<&Project>, cx: Option<&App>) -> Result<BashToolInput> {
// Try standard deserialization first
if let Ok(input) = serde_json::from_value::<BashToolInput>(value.clone()) {
return Ok(input);
}
// If that fails, check if it's because "cd" is missing
let mut obj = match value {
serde_json::Value::Object(obj) => obj,
_ => return Err(anyhow!("Expected object for BashToolInput")),
};
if !obj.contains_key("cd") {
// Find first worktree root to use as default if project context is available
if let (Some(project), Some(cx)) = (project, cx) {
if let Some(worktree) = project.worktrees(cx).next() {
let root_name = worktree.read(cx).root_name().to_string();
obj.insert("cd".to_string(), serde_json::Value::String(root_name));
} else {
// No worktrees available, use "." as fallback
obj.insert("cd".to_string(), serde_json::Value::String(".".to_string()));
}
} else {
// No project context, use "." as fallback
obj.insert("cd".to_string(), serde_json::Value::String(".".to_string()));
}
}
// Try to deserialize with the modified object
serde_json::from_value::<BashToolInput>(serde_json::Value::Object(obj))
.map_err(|e| anyhow!("Failed to deserialize BashToolInput: {}", e))
}
pub struct BashTool;
impl Tool for BashTool {
@@ -42,8 +79,14 @@ 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),
match from_value_with_default_cd(input.clone(), None, None) {
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(),
}
}
@@ -56,15 +99,49 @@ impl Tool for BashTool {
_action_log: Entity<ActionLog>,
cx: &mut App,
) -> Task<Result<String>> {
let input: BashToolInput = match serde_json::from_value(input) {
let input: BashToolInput = match from_value_with_default_cd(input, Some(&project.read(cx)), Some(cx)) {
Ok(input) => input,
Err(err) => return Task::ready(Err(anyhow!(err))),
};
let Some(worktree) = project.read(cx).worktree_for_root_name(&input.cd, cx) else {
return Task::ready(Err(anyhow!("Working directory not found in the project")));
let project = project.read(cx);
let input_path = Path::new(&input.cd);
let working_dir = if input.cd == "." {
// Accept "." as meaning "the one worktree" if we only have one worktree.
let mut worktrees = project.worktrees(cx);
let only_worktree = match worktrees.next() {
Some(worktree) => worktree,
None => return Task::ready(Err(anyhow!("No worktrees found in the project"))),
};
if worktrees.next().is_some() {
return Task::ready(Err(anyhow!("'.' is ambiguous in multi-root workspaces. Please specify a root directory explicitly.")));
}
only_worktree.read(cx).abs_path()
} else if input_path.is_absolute() {
// Absolute paths are allowed, but only if they're in one of the project's worktrees.
if !project
.worktrees(cx)
.any(|worktree| input_path.starts_with(&worktree.read(cx).abs_path()))
{
return Task::ready(Err(anyhow!(
"The absolute path must be within one of the project's worktrees"
)));
}
input_path.into()
} else {
let Some(worktree) = project.worktree_for_root_name(&input.cd, cx) else {
return Task::ready(Err(anyhow!(
"`cd` directory {} not found in the project",
&input.cd
)));
};
worktree.read(cx).abs_path()
};
let working_directory = worktree.read(cx).abs_path();
cx.spawn(async move |_| {
// Add 2>&1 to merge stderr into stdout for proper interleaving.
@@ -73,7 +150,7 @@ impl Tool for BashTool {
let output = new_smol_command("bash")
.arg("-c")
.arg(&command)
.current_dir(working_directory)
.current_dir(working_dir)
.output()
.await
.context("Failed to execute bash command")?;

View File

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

View File

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

View File

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

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

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

View File

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

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

View File

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

View File

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

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

View File

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

@@ -1,6 +1,3 @@
// todo(windows): Actually run the tests
#![cfg(not(target_os = "windows"))]
use std::sync::Arc;
use call::Room;

View File

@@ -36,6 +36,7 @@ use std::{
},
};
use text::Point;
use util::{path, uri};
use workspace::{CloseIntent, Workspace};
#[gpui::test(iterations = 10)]
@@ -190,9 +191,9 @@ async fn test_newline_above_or_below_does_not_move_guest_cursor(
client_a
.fs()
.insert_tree("/dir", json!({ "a.txt": "Some text\n" }))
.insert_tree(path!("/dir"), json!({ "a.txt": "Some text\n" }))
.await;
let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await;
let (project_a, worktree_id) = client_a.build_local_project(path!("/dir"), cx_a).await;
let project_id = active_call_a
.update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
.await
@@ -306,14 +307,14 @@ async fn test_collaborating_with_completion(cx_a: &mut TestAppContext, cx_b: &mu
client_a
.fs()
.insert_tree(
"/a",
path!("/a"),
json!({
"main.rs": "fn main() { a }",
"other.rs": "",
}),
)
.await;
let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
let (project_a, worktree_id) = client_a.build_local_project(path!("/a"), cx_a).await;
let project_id = active_call_a
.update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
.await
@@ -351,7 +352,7 @@ async fn test_collaborating_with_completion(cx_a: &mut TestAppContext, cx_b: &mu
.set_request_handler::<lsp::request::Completion, _, _>(|params, _| async move {
assert_eq!(
params.text_document_position.text_document.uri,
lsp::Url::from_file_path("/a/main.rs").unwrap(),
lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(),
);
assert_eq!(
params.text_document_position.position,
@@ -468,7 +469,7 @@ async fn test_collaborating_with_completion(cx_a: &mut TestAppContext, cx_b: &mu
.set_request_handler::<lsp::request::Completion, _, _>(|params, _| async move {
assert_eq!(
params.text_document_position.text_document.uri,
lsp::Url::from_file_path("/a/main.rs").unwrap(),
lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(),
);
assert_eq!(
params.text_document_position.position,
@@ -562,14 +563,14 @@ async fn test_collaborating_with_code_actions(
client_a
.fs()
.insert_tree(
"/a",
path!("/a"),
json!({
"main.rs": "mod other;\nfn main() { let foo = other::foo(); }",
"other.rs": "pub fn foo() -> usize { 4 }",
}),
)
.await;
let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
let (project_a, worktree_id) = client_a.build_local_project(path!("/a"), cx_a).await;
let project_id = active_call_a
.update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
.await
@@ -592,7 +593,7 @@ async fn test_collaborating_with_code_actions(
.set_request_handler::<lsp::request::CodeActionRequest, _, _>(|params, _| async move {
assert_eq!(
params.text_document.uri,
lsp::Url::from_file_path("/a/main.rs").unwrap(),
lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(),
);
assert_eq!(params.range.start, lsp::Position::new(0, 0));
assert_eq!(params.range.end, lsp::Position::new(0, 0));
@@ -614,7 +615,7 @@ async fn test_collaborating_with_code_actions(
.set_request_handler::<lsp::request::CodeActionRequest, _, _>(|params, _| async move {
assert_eq!(
params.text_document.uri,
lsp::Url::from_file_path("/a/main.rs").unwrap(),
lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(),
);
assert_eq!(params.range.start, lsp::Position::new(1, 31));
assert_eq!(params.range.end, lsp::Position::new(1, 31));
@@ -626,7 +627,7 @@ async fn test_collaborating_with_code_actions(
changes: Some(
[
(
lsp::Url::from_file_path("/a/main.rs").unwrap(),
lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(),
vec![lsp::TextEdit::new(
lsp::Range::new(
lsp::Position::new(1, 22),
@@ -636,7 +637,7 @@ async fn test_collaborating_with_code_actions(
)],
),
(
lsp::Url::from_file_path("/a/other.rs").unwrap(),
lsp::Url::from_file_path(path!("/a/other.rs")).unwrap(),
vec![lsp::TextEdit::new(
lsp::Range::new(
lsp::Position::new(0, 0),
@@ -697,7 +698,7 @@ async fn test_collaborating_with_code_actions(
changes: Some(
[
(
lsp::Url::from_file_path("/a/main.rs").unwrap(),
lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(),
vec![lsp::TextEdit::new(
lsp::Range::new(
lsp::Position::new(1, 22),
@@ -707,7 +708,7 @@ async fn test_collaborating_with_code_actions(
)],
),
(
lsp::Url::from_file_path("/a/other.rs").unwrap(),
lsp::Url::from_file_path(path!("/a/other.rs")).unwrap(),
vec![lsp::TextEdit::new(
lsp::Range::new(
lsp::Position::new(0, 0),
@@ -780,14 +781,14 @@ async fn test_collaborating_with_renames(cx_a: &mut TestAppContext, cx_b: &mut T
client_a
.fs()
.insert_tree(
"/dir",
path!("/dir"),
json!({
"one.rs": "const ONE: usize = 1;",
"two.rs": "const TWO: usize = one::ONE + one::ONE;"
}),
)
.await;
let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await;
let (project_a, worktree_id) = client_a.build_local_project(path!("/dir"), cx_a).await;
let project_id = active_call_a
.update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
.await
@@ -813,7 +814,10 @@ async fn test_collaborating_with_renames(cx_a: &mut TestAppContext, cx_b: &mut T
fake_language_server
.set_request_handler::<lsp::request::PrepareRenameRequest, _, _>(|params, _| async move {
assert_eq!(params.text_document.uri.as_str(), "file:///dir/one.rs");
assert_eq!(
params.text_document.uri.as_str(),
uri!("file:///dir/one.rs")
);
assert_eq!(params.position, lsp::Position::new(0, 7));
Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range::new(
lsp::Position::new(0, 6),
@@ -856,7 +860,10 @@ async fn test_collaborating_with_renames(cx_a: &mut TestAppContext, cx_b: &mut T
fake_language_server
.set_request_handler::<lsp::request::PrepareRenameRequest, _, _>(|params, _| async move {
assert_eq!(params.text_document.uri.as_str(), "file:///dir/one.rs");
assert_eq!(
params.text_document.uri.as_str(),
uri!("file:///dir/one.rs")
);
assert_eq!(params.position, lsp::Position::new(0, 8));
Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range::new(
lsp::Position::new(0, 6),
@@ -894,7 +901,7 @@ async fn test_collaborating_with_renames(cx_a: &mut TestAppContext, cx_b: &mut T
.set_request_handler::<lsp::request::Rename, _, _>(|params, _| async move {
assert_eq!(
params.text_document_position.text_document.uri.as_str(),
"file:///dir/one.rs"
uri!("file:///dir/one.rs")
);
assert_eq!(
params.text_document_position.position,
@@ -905,14 +912,14 @@ async fn test_collaborating_with_renames(cx_a: &mut TestAppContext, cx_b: &mut T
changes: Some(
[
(
lsp::Url::from_file_path("/dir/one.rs").unwrap(),
lsp::Url::from_file_path(path!("/dir/one.rs")).unwrap(),
vec![lsp::TextEdit::new(
lsp::Range::new(lsp::Position::new(0, 6), lsp::Position::new(0, 9)),
"THREE".to_string(),
)],
),
(
lsp::Url::from_file_path("/dir/two.rs").unwrap(),
lsp::Url::from_file_path(path!("/dir/two.rs")).unwrap(),
vec![
lsp::TextEdit::new(
lsp::Range::new(
@@ -999,17 +1006,17 @@ async fn test_language_server_statuses(cx_a: &mut TestAppContext, cx_b: &mut Tes
client_a
.fs()
.insert_tree(
"/dir",
path!("/dir"),
json!({
"main.rs": "const ONE: usize = 1;",
}),
)
.await;
let (project_a, _) = client_a.build_local_project("/dir", cx_a).await;
let (project_a, _) = client_a.build_local_project(path!("/dir"), cx_a).await;
let _buffer_a = project_a
.update(cx_a, |p, cx| {
p.open_local_buffer_with_lsp("/dir/main.rs", cx)
p.open_local_buffer_with_lsp(path!("/dir/main.rs"), cx)
})
.await
.unwrap();
@@ -1106,7 +1113,7 @@ async fn test_share_project(
client_a
.fs()
.insert_tree(
"/a",
path!("/a"),
json!({
".gitignore": "ignored-dir",
"a.txt": "a-contents",
@@ -1120,7 +1127,7 @@ async fn test_share_project(
.await;
// Invite client B to collaborate on a project
let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
let (project_a, worktree_id) = client_a.build_local_project(path!("/a"), cx_a).await;
active_call_a
.update(cx_a, |call, cx| {
call.invite(client_b.user_id().unwrap(), Some(project_a.clone()), cx)
@@ -1292,14 +1299,14 @@ async fn test_on_input_format_from_host_to_guest(
client_a
.fs()
.insert_tree(
"/a",
path!("/a"),
json!({
"main.rs": "fn main() { a }",
"other.rs": "// Test file",
}),
)
.await;
let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
let (project_a, worktree_id) = client_a.build_local_project(path!("/a"), cx_a).await;
let project_id = active_call_a
.update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
.await
@@ -1325,7 +1332,7 @@ async fn test_on_input_format_from_host_to_guest(
|params, _| async move {
assert_eq!(
params.text_document_position.text_document.uri,
lsp::Url::from_file_path("/a/main.rs").unwrap(),
lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(),
);
assert_eq!(
params.text_document_position.position,
@@ -1414,14 +1421,14 @@ async fn test_on_input_format_from_guest_to_host(
client_a
.fs()
.insert_tree(
"/a",
path!("/a"),
json!({
"main.rs": "fn main() { a }",
"other.rs": "// Test file",
}),
)
.await;
let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
let (project_a, worktree_id) = client_a.build_local_project(path!("/a"), cx_a).await;
let project_id = active_call_a
.update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
.await
@@ -1455,7 +1462,7 @@ async fn test_on_input_format_from_guest_to_host(
.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(|params, _| async move {
assert_eq!(
params.text_document_position.text_document.uri,
lsp::Url::from_file_path("/a/main.rs").unwrap(),
lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(),
);
assert_eq!(
params.text_document_position.position,
@@ -1575,14 +1582,14 @@ async fn test_mutual_editor_inlay_hint_cache_update(
client_a
.fs()
.insert_tree(
"/a",
path!("/a"),
json!({
"main.rs": "fn main() { a } // and some long comment to ensure inlay hints are not trimmed out",
"other.rs": "// Test file",
}),
)
.await;
let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
let (project_a, worktree_id) = client_a.build_local_project(path!("/a"), cx_a).await;
active_call_a
.update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
.await
@@ -1605,7 +1612,7 @@ async fn test_mutual_editor_inlay_hint_cache_update(
// The host opens a rust file.
let _buffer_a = project_a
.update(cx_a, |project, cx| {
project.open_local_buffer("/a/main.rs", cx)
project.open_local_buffer(path!("/a/main.rs"), cx)
})
.await
.unwrap();
@@ -1629,7 +1636,7 @@ async fn test_mutual_editor_inlay_hint_cache_update(
async move {
assert_eq!(
params.text_document.uri,
lsp::Url::from_file_path("/a/main.rs").unwrap(),
lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(),
);
let edits_made = task_edits_made.load(atomic::Ordering::Acquire);
Ok(Some(vec![lsp::InlayHint {
@@ -1809,14 +1816,14 @@ async fn test_inlay_hint_refresh_is_forwarded(
client_a
.fs()
.insert_tree(
"/a",
path!("/a"),
json!({
"main.rs": "fn main() { a } // and some long comment to ensure inlay hints are not trimmed out",
"other.rs": "// Test file",
}),
)
.await;
let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
let (project_a, worktree_id) = client_a.build_local_project(path!("/a"), cx_a).await;
active_call_a
.update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
.await
@@ -1864,7 +1871,7 @@ async fn test_inlay_hint_refresh_is_forwarded(
async move {
assert_eq!(
params.text_document.uri,
lsp::Url::from_file_path("/a/main.rs").unwrap(),
lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(),
);
let other_hints = task_other_hints.load(atomic::Ordering::Acquire);
let character = if other_hints { 0 } else { 2 };
@@ -1967,7 +1974,7 @@ async fn test_git_blame_is_forwarded(cx_a: &mut TestAppContext, cx_b: &mut TestA
client_a
.fs()
.insert_tree(
"/my-repo",
path!("/my-repo"),
json!({
".git": {},
"file.txt": "line1\nline2\nline3\nline\n",
@@ -1993,11 +2000,12 @@ async fn test_git_blame_is_forwarded(cx_a: &mut TestAppContext, cx_b: &mut TestA
.collect(),
remote_url: Some("git@github.com:zed-industries/zed.git".to_string()),
};
client_a
.fs()
.set_blame_for_repo(Path::new("/my-repo/.git"), vec![("file.txt".into(), blame)]);
client_a.fs().set_blame_for_repo(
Path::new(path!("/my-repo/.git")),
vec![("file.txt".into(), blame)],
);
let (project_a, worktree_id) = client_a.build_local_project("/my-repo", cx_a).await;
let (project_a, worktree_id) = client_a.build_local_project(path!("/my-repo"), cx_a).await;
let project_id = active_call_a
.update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
.await
@@ -2177,7 +2185,7 @@ async fn test_collaborating_with_editorconfig(
client_a
.fs()
.insert_tree(
"/a",
path!("/a"),
json!({
"src": {
"main.rs": "mod other;\nfn main() { let foo = other::foo(); }",
@@ -2190,7 +2198,7 @@ async fn test_collaborating_with_editorconfig(
}),
)
.await;
let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
let (project_a, worktree_id) = client_a.build_local_project(path!("/a"), cx_a).await;
let project_id = active_call_a
.update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
.await
@@ -2309,7 +2317,7 @@ fn main() { let foo = other::foo(); }"};
client_a
.fs()
.atomic_write(
PathBuf::from("/a/src/.editorconfig"),
PathBuf::from(path!("/a/src/.editorconfig")),
"[*]\ntab_width = 3\n".to_owned(),
)
.await

View File

@@ -16,6 +16,7 @@ use project::WorktreeSettings;
use rpc::proto::PeerId;
use serde_json::json;
use settings::SettingsStore;
use util::path;
use workspace::{item::ItemHandle as _, SplitDirection, Workspace};
use super::TestClient;
@@ -50,7 +51,7 @@ async fn test_basic_following(
client_a
.fs()
.insert_tree(
"/a",
path!("/a"),
json!({
"1.txt": "one\none\none",
"2.txt": "two\ntwo\ntwo",
@@ -58,7 +59,7 @@ async fn test_basic_following(
}),
)
.await;
let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
let (project_a, worktree_id) = client_a.build_local_project(path!("/a"), cx_a).await;
active_call_a
.update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
.await
@@ -436,7 +437,9 @@ async fn test_basic_following(
);
// TODO: Re-enable this test once we can replace our swift Livekit SDK with the rust SDK
#[cfg(not(target_os = "macos"))]
// todo(windows)
// Fix this on Windows
#[cfg(all(not(target_os = "macos"), not(target_os = "windows")))]
{
use crate::rpc::RECONNECT_TIMEOUT;
use gpui::TestScreenCaptureSource;
@@ -570,7 +573,7 @@ async fn test_following_tab_order(
client_a
.fs()
.insert_tree(
util::path!("/a"),
path!("/a"),
json!({
"1.txt": "one",
"2.txt": "two",
@@ -578,7 +581,7 @@ async fn test_following_tab_order(
}),
)
.await;
let (project_a, worktree_id) = client_a.build_local_project(util::path!("/a"), cx_a).await;
let (project_a, worktree_id) = client_a.build_local_project(path!("/a"), cx_a).await;
active_call_a
.update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
.await
@@ -1220,7 +1223,7 @@ async fn test_auto_unfollowing(cx_a: &mut TestAppContext, cx_b: &mut TestAppCont
client_a
.fs()
.insert_tree(
"/a",
path!("/a"),
json!({
"1.txt": "one",
"2.txt": "two",
@@ -1228,7 +1231,7 @@ async fn test_auto_unfollowing(cx_a: &mut TestAppContext, cx_b: &mut TestAppCont
}),
)
.await;
let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
let (project_a, worktree_id) = client_a.build_local_project(path!("/a"), cx_a).await;
active_call_a
.update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
.await
@@ -1435,7 +1438,7 @@ async fn test_following_across_workspaces(cx_a: &mut TestAppContext, cx_b: &mut
client_a
.fs()
.insert_tree(
"/a",
path!("/a"),
json!({
"w.rs": "",
"x.rs": "",
@@ -1446,7 +1449,7 @@ async fn test_following_across_workspaces(cx_a: &mut TestAppContext, cx_b: &mut
client_b
.fs()
.insert_tree(
"/b",
path!("/b"),
json!({
"y.rs": "",
"z.rs": "",
@@ -1460,8 +1463,8 @@ async fn test_following_across_workspaces(cx_a: &mut TestAppContext, cx_b: &mut
let active_call_a = cx_a.read(ActiveCall::global);
let active_call_b = cx_b.read(ActiveCall::global);
let (project_a, worktree_id_a) = client_a.build_local_project("/a", cx_a).await;
let (project_b, worktree_id_b) = client_b.build_local_project("/b", cx_b).await;
let (project_a, worktree_id_a) = client_a.build_local_project(path!("/a"), cx_a).await;
let (project_b, worktree_id_b) = client_b.build_local_project(path!("/b"), cx_b).await;
let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a);
let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
@@ -1718,7 +1721,7 @@ async fn test_following_into_excluded_file(
client_a
.fs()
.insert_tree(
"/a",
path!("/a"),
json!({
".git": {
"COMMIT_EDITMSG": "write your commit message here",
@@ -1729,7 +1732,7 @@ async fn test_following_into_excluded_file(
}),
)
.await;
let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
let (project_a, worktree_id) = client_a.build_local_project(path!("/a"), cx_a).await;
active_call_a
.update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
.await

View File

@@ -9,6 +9,7 @@ use git_ui::project_diff::ProjectDiff;
use gpui::{TestAppContext, VisualTestContext};
use project::ProjectPath;
use serde_json::json;
use util::path;
use workspace::Workspace;
//
@@ -29,7 +30,7 @@ async fn test_project_diff(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext)
client_a
.fs()
.insert_tree(
"/a",
path!("/a"),
json!({
".git": {},
"changed.txt": "after\n",
@@ -41,7 +42,7 @@ async fn test_project_diff(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext)
.await;
client_a.fs().set_git_content_for_repo(
Path::new("/a/.git"),
Path::new(path!("/a/.git")),
&[
("changed.txt".into(), "before\n".to_string(), None),
("unchanged.txt".into(), "unchanged\n".to_string(), None),
@@ -49,7 +50,7 @@ async fn test_project_diff(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext)
("secret.pem".into(), "shh\n".to_string(), None),
],
);
let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
let (project_a, worktree_id) = client_a.build_local_project(path!("/a"), cx_a).await;
let active_call_a = cx_a.read(ActiveCall::global);
let project_id = active_call_a
.update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
@@ -93,7 +94,7 @@ async fn test_project_diff(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext)
client_a
.fs()
.insert_tree(
"/a",
path!("/a"),
json!({
".git": {},
"changed.txt": "before\n",

View File

@@ -50,6 +50,7 @@ use std::{
time::Duration,
};
use unindent::Unindent as _;
use util::{path, separator, uri};
use workspace::Pane;
#[ctor::ctor]
@@ -1459,7 +1460,7 @@ async fn test_project_reconnect(
client_a
.fs()
.insert_tree(
"/root-1",
path!("/root-1"),
json!({
"dir1": {
"a.txt": "a",
@@ -1487,7 +1488,7 @@ async fn test_project_reconnect(
client_a
.fs()
.insert_tree(
"/root-2",
path!("/root-2"),
json!({
"2.txt": "2",
}),
@@ -1496,7 +1497,7 @@ async fn test_project_reconnect(
client_a
.fs()
.insert_tree(
"/root-3",
path!("/root-3"),
json!({
"3.txt": "3",
}),
@@ -1504,9 +1505,11 @@ async fn test_project_reconnect(
.await;
let active_call_a = cx_a.read(ActiveCall::global);
let (project_a1, _) = client_a.build_local_project("/root-1/dir1", cx_a).await;
let (project_a2, _) = client_a.build_local_project("/root-2", cx_a).await;
let (project_a3, _) = client_a.build_local_project("/root-3", cx_a).await;
let (project_a1, _) = client_a
.build_local_project(path!("/root-1/dir1"), cx_a)
.await;
let (project_a2, _) = client_a.build_local_project(path!("/root-2"), cx_a).await;
let (project_a3, _) = client_a.build_local_project(path!("/root-3"), cx_a).await;
let worktree_a1 =
project_a1.read_with(cx_a, |project, cx| project.worktrees(cx).next().unwrap());
let project1_id = active_call_a
@@ -1533,7 +1536,7 @@ async fn test_project_reconnect(
});
let (worktree_a2, _) = project_a1
.update(cx_a, |p, cx| {
p.find_or_create_worktree("/root-1/dir2", true, cx)
p.find_or_create_worktree(path!("/root-1/dir2"), true, cx)
})
.await
.unwrap();
@@ -1579,7 +1582,7 @@ async fn test_project_reconnect(
client_a
.fs()
.insert_tree(
"/root-1/dir1/subdir2",
path!("/root-1/dir1/subdir2"),
json!({
"f.txt": "f-contents",
"g.txt": "g-contents",
@@ -1591,7 +1594,7 @@ async fn test_project_reconnect(
client_a
.fs()
.remove_dir(
"/root-1/dir1/subdir1".as_ref(),
path!("/root-1/dir1/subdir1").as_ref(),
RemoveOptions {
recursive: true,
..Default::default()
@@ -1606,7 +1609,7 @@ async fn test_project_reconnect(
});
let (worktree_a3, _) = project_a1
.update(cx_a, |p, cx| {
p.find_or_create_worktree("/root-1/dir3", true, cx)
p.find_or_create_worktree(path!("/root-1/dir3"), true, cx)
})
.await
.unwrap();
@@ -1647,13 +1650,13 @@ async fn test_project_reconnect(
.map(|p| p.to_str().unwrap())
.collect::<Vec<_>>(),
vec![
"a.txt",
"b.txt",
"subdir2",
"subdir2/f.txt",
"subdir2/g.txt",
"subdir2/h.txt",
"subdir2/i.txt"
separator!("a.txt"),
separator!("b.txt"),
separator!("subdir2"),
separator!("subdir2/f.txt"),
separator!("subdir2/g.txt"),
separator!("subdir2/h.txt"),
separator!("subdir2/i.txt")
]
);
assert!(worktree_a3.read(cx).has_update_observer());
@@ -1680,13 +1683,13 @@ async fn test_project_reconnect(
.map(|p| p.to_str().unwrap())
.collect::<Vec<_>>(),
vec![
"a.txt",
"b.txt",
"subdir2",
"subdir2/f.txt",
"subdir2/g.txt",
"subdir2/h.txt",
"subdir2/i.txt"
separator!("a.txt"),
separator!("b.txt"),
separator!("subdir2"),
separator!("subdir2/f.txt"),
separator!("subdir2/g.txt"),
separator!("subdir2/h.txt"),
separator!("subdir2/i.txt")
]
);
assert!(project.worktree_for_id(worktree2_id, cx).is_none());
@@ -1719,18 +1722,21 @@ async fn test_project_reconnect(
// While client B is disconnected, add and remove files from client A's project
client_a
.fs()
.insert_file("/root-1/dir1/subdir2/j.txt", "j-contents".into())
.insert_file(path!("/root-1/dir1/subdir2/j.txt"), "j-contents".into())
.await;
client_a
.fs()
.remove_file("/root-1/dir1/subdir2/i.txt".as_ref(), Default::default())
.remove_file(
path!("/root-1/dir1/subdir2/i.txt").as_ref(),
Default::default(),
)
.await
.unwrap();
// While client B is disconnected, add and remove worktrees from client A's project.
let (worktree_a4, _) = project_a1
.update(cx_a, |p, cx| {
p.find_or_create_worktree("/root-1/dir4", true, cx)
p.find_or_create_worktree(path!("/root-1/dir4"), true, cx)
})
.await
.unwrap();
@@ -1773,13 +1779,13 @@ async fn test_project_reconnect(
.map(|p| p.to_str().unwrap())
.collect::<Vec<_>>(),
vec![
"a.txt",
"b.txt",
"subdir2",
"subdir2/f.txt",
"subdir2/g.txt",
"subdir2/h.txt",
"subdir2/j.txt"
separator!("a.txt"),
separator!("b.txt"),
separator!("subdir2"),
separator!("subdir2/f.txt"),
separator!("subdir2/g.txt"),
separator!("subdir2/h.txt"),
separator!("subdir2/j.txt")
]
);
assert!(project.worktree_for_id(worktree2_id, cx).is_none());
@@ -2316,14 +2322,14 @@ async fn test_propagate_saves_and_fs_changes(
client_a
.fs()
.insert_tree(
"/a",
path!("/a"),
json!({
"file1.rs": "",
"file2": ""
}),
)
.await;
let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
let (project_a, worktree_id) = client_a.build_local_project(path!("/a"), cx_a).await;
let worktree_a = project_a.read_with(cx_a, |p, cx| p.worktrees(cx).next().unwrap());
let project_id = active_call_a
@@ -2409,18 +2415,25 @@ async fn test_propagate_saves_and_fs_changes(
client_a
.fs()
.rename(
"/a/file1.rs".as_ref(),
"/a/file1.js".as_ref(),
path!("/a/file1.rs").as_ref(),
path!("/a/file1.js").as_ref(),
Default::default(),
)
.await
.unwrap();
client_a
.fs()
.rename("/a/file2".as_ref(), "/a/file3".as_ref(), Default::default())
.rename(
path!("/a/file2").as_ref(),
path!("/a/file3").as_ref(),
Default::default(),
)
.await
.unwrap();
client_a.fs().insert_file("/a/file4", "4".into()).await;
client_a
.fs()
.insert_file(path!("/a/file4"), "4".into())
.await;
executor.run_until_parked();
worktree_a.read_with(cx_a, |tree, _| {
@@ -2892,15 +2905,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
@@ -2957,7 +2972,7 @@ async fn test_git_status_sync(
client_a
.fs()
.insert_tree(
"/dir",
path!("/dir"),
json!({
".git": {},
"a.txt": "a",
@@ -2970,11 +2985,11 @@ async fn test_git_status_sync(
// Initially, a.txt is uncommitted, but present in the index,
// and b.txt is unmerged.
client_a.fs().set_head_for_repo(
"/dir/.git".as_ref(),
path!("/dir/.git").as_ref(),
&[("b.txt".into(), "B".into()), ("c.txt".into(), "c".into())],
);
client_a.fs().set_index_for_repo(
"/dir/.git".as_ref(),
path!("/dir/.git").as_ref(),
&[
("a.txt".into(), "".into()),
("b.txt".into(), "B".into()),
@@ -2982,7 +2997,7 @@ async fn test_git_status_sync(
],
);
client_a.fs().set_unmerged_paths_for_repo(
"/dir/.git".as_ref(),
path!("/dir/.git").as_ref(),
&[(
"b.txt".into(),
UnmergedStatus {
@@ -3001,7 +3016,7 @@ async fn test_git_status_sync(
second_head: UnmergedStatusCode::Deleted,
});
let (project_local, _worktree_id) = client_a.build_local_project("/dir", cx_a).await;
let (project_local, _worktree_id) = client_a.build_local_project(path!("/dir"), cx_a).await;
let project_id = active_call_a
.update(cx_a, |call, cx| {
call.share_project(project_local.clone(), cx)
@@ -3022,11 +3037,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| {
@@ -3057,15 +3081,15 @@ async fn test_git_status_sync(
// Delete b.txt from the index, mark conflict as resolved,
// and modify c.txt in the working copy.
client_a.fs().set_index_for_repo(
"/dir/.git".as_ref(),
path!("/dir/.git").as_ref(),
&[("a.txt".into(), "a".into()), ("c.txt".into(), "c".into())],
);
client_a
.fs()
.set_unmerged_paths_for_repo("/dir/.git".as_ref(), &[]);
.set_unmerged_paths_for_repo(path!("/dir/.git").as_ref(), &[]);
client_a
.fs()
.atomic_write("/dir/c.txt".into(), "CC".into())
.atomic_write(path!("/dir/c.txt").into(), "CC".into())
.await
.unwrap();
@@ -3094,6 +3118,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(path!("/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)]
@@ -3113,14 +3158,14 @@ async fn test_fs_operations(
client_a
.fs()
.insert_tree(
"/dir",
path!("/dir"),
json!({
"a.txt": "a-contents",
"b.txt": "b-contents",
}),
)
.await;
let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await;
let (project_a, worktree_id) = client_a.build_local_project(path!("/dir"), cx_a).await;
let project_id = active_call_a
.update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
.await
@@ -3251,13 +3296,13 @@ async fn test_fs_operations(
.map(|p| p.to_string_lossy())
.collect::<Vec<_>>(),
[
"DIR",
"DIR/SUBDIR",
"DIR/SUBDIR/f.txt",
"DIR/e.txt",
"a.txt",
"b.txt",
"d.txt"
separator!("DIR"),
separator!("DIR/SUBDIR"),
separator!("DIR/SUBDIR/f.txt"),
separator!("DIR/e.txt"),
separator!("a.txt"),
separator!("b.txt"),
separator!("d.txt")
]
);
});
@@ -3269,13 +3314,13 @@ async fn test_fs_operations(
.map(|p| p.to_string_lossy())
.collect::<Vec<_>>(),
[
"DIR",
"DIR/SUBDIR",
"DIR/SUBDIR/f.txt",
"DIR/e.txt",
"a.txt",
"b.txt",
"d.txt"
separator!("DIR"),
separator!("DIR/SUBDIR"),
separator!("DIR/SUBDIR/f.txt"),
separator!("DIR/e.txt"),
separator!("a.txt"),
separator!("b.txt"),
separator!("d.txt")
]
);
});
@@ -3295,14 +3340,14 @@ async fn test_fs_operations(
.map(|p| p.to_string_lossy())
.collect::<Vec<_>>(),
[
"DIR",
"DIR/SUBDIR",
"DIR/SUBDIR/f.txt",
"DIR/e.txt",
"a.txt",
"b.txt",
"d.txt",
"f.txt"
separator!("DIR"),
separator!("DIR/SUBDIR"),
separator!("DIR/SUBDIR/f.txt"),
separator!("DIR/e.txt"),
separator!("a.txt"),
separator!("b.txt"),
separator!("d.txt"),
separator!("f.txt")
]
);
});
@@ -3314,14 +3359,14 @@ async fn test_fs_operations(
.map(|p| p.to_string_lossy())
.collect::<Vec<_>>(),
[
"DIR",
"DIR/SUBDIR",
"DIR/SUBDIR/f.txt",
"DIR/e.txt",
"a.txt",
"b.txt",
"d.txt",
"f.txt"
separator!("DIR"),
separator!("DIR/SUBDIR"),
separator!("DIR/SUBDIR/f.txt"),
separator!("DIR/e.txt"),
separator!("a.txt"),
separator!("b.txt"),
separator!("d.txt"),
separator!("f.txt")
]
);
});
@@ -3538,13 +3583,13 @@ async fn test_buffer_conflict_after_save(
client_a
.fs()
.insert_tree(
"/dir",
path!("/dir"),
json!({
"a.txt": "a-contents",
}),
)
.await;
let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await;
let (project_a, worktree_id) = client_a.build_local_project(path!("/dir"), cx_a).await;
let project_id = active_call_a
.update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
.await
@@ -3602,13 +3647,13 @@ async fn test_buffer_reloading(
client_a
.fs()
.insert_tree(
"/dir",
path!("/dir"),
json!({
"a.txt": "a\nb\nc",
}),
)
.await;
let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await;
let (project_a, worktree_id) = client_a.build_local_project(path!("/dir"), cx_a).await;
let project_id = active_call_a
.update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
.await
@@ -3630,7 +3675,11 @@ async fn test_buffer_reloading(
let new_contents = Rope::from("d\ne\nf");
client_a
.fs()
.save("/dir/a.txt".as_ref(), &new_contents, LineEnding::Windows)
.save(
path!("/dir/a.txt").as_ref(),
&new_contents,
LineEnding::Windows,
)
.await
.unwrap();
@@ -3660,9 +3709,9 @@ async fn test_editing_while_guest_opens_buffer(
client_a
.fs()
.insert_tree("/dir", json!({ "a.txt": "a-contents" }))
.insert_tree(path!("/dir"), json!({ "a.txt": "a-contents" }))
.await;
let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await;
let (project_a, worktree_id) = client_a.build_local_project(path!("/dir"), cx_a).await;
let project_id = active_call_a
.update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
.await
@@ -3975,19 +4024,19 @@ async fn test_collaborating_with_diagnostics(
client_a
.fs()
.insert_tree(
"/a",
path!("/a"),
json!({
"a.rs": "let one = two",
"other.rs": "",
}),
)
.await;
let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
let (project_a, worktree_id) = client_a.build_local_project(path!("/a"), cx_a).await;
// Cause the language server to start.
let _buffer = project_a
.update(cx_a, |project, cx| {
project.open_local_buffer_with_lsp("/a/other.rs", cx)
project.open_local_buffer_with_lsp(path!("/a/other.rs"), cx)
})
.await
.unwrap();
@@ -3999,7 +4048,7 @@ async fn test_collaborating_with_diagnostics(
.await;
fake_language_server.notify::<lsp::notification::PublishDiagnostics>(
&lsp::PublishDiagnosticsParams {
uri: lsp::Url::from_file_path("/a/a.rs").unwrap(),
uri: lsp::Url::from_file_path(path!("/a/a.rs")).unwrap(),
version: None,
diagnostics: vec![lsp::Diagnostic {
severity: Some(lsp::DiagnosticSeverity::WARNING),
@@ -4019,7 +4068,7 @@ async fn test_collaborating_with_diagnostics(
.unwrap();
fake_language_server.notify::<lsp::notification::PublishDiagnostics>(
&lsp::PublishDiagnosticsParams {
uri: lsp::Url::from_file_path("/a/a.rs").unwrap(),
uri: lsp::Url::from_file_path(path!("/a/a.rs")).unwrap(),
version: None,
diagnostics: vec![lsp::Diagnostic {
severity: Some(lsp::DiagnosticSeverity::ERROR),
@@ -4093,7 +4142,7 @@ async fn test_collaborating_with_diagnostics(
// Simulate a language server reporting more errors for a file.
fake_language_server.notify::<lsp::notification::PublishDiagnostics>(
&lsp::PublishDiagnosticsParams {
uri: lsp::Url::from_file_path("/a/a.rs").unwrap(),
uri: lsp::Url::from_file_path(path!("/a/a.rs")).unwrap(),
version: None,
diagnostics: vec![
lsp::Diagnostic {
@@ -4187,7 +4236,7 @@ async fn test_collaborating_with_diagnostics(
// Simulate a language server reporting no errors for a file.
fake_language_server.notify::<lsp::notification::PublishDiagnostics>(
&lsp::PublishDiagnosticsParams {
uri: lsp::Url::from_file_path("/a/a.rs").unwrap(),
uri: lsp::Url::from_file_path(path!("/a/a.rs")).unwrap(),
version: None,
diagnostics: vec![],
},
@@ -4243,7 +4292,7 @@ async fn test_collaborating_with_lsp_progress_updates_and_diagnostics_ordering(
client_a
.fs()
.insert_tree(
"/test",
path!("/test"),
json!({
"one.rs": "const ONE: usize = 1;",
"two.rs": "const TWO: usize = 2;",
@@ -4254,7 +4303,7 @@ async fn test_collaborating_with_lsp_progress_updates_and_diagnostics_ordering(
)
.await;
let (project_a, worktree_id) = client_a.build_local_project("/test", cx_a).await;
let (project_a, worktree_id) = client_a.build_local_project(path!("/test"), cx_a).await;
// Share a project as client A
let active_call_a = cx_a.read(ActiveCall::global);
@@ -4293,7 +4342,7 @@ async fn test_collaborating_with_lsp_progress_updates_and_diagnostics_ordering(
for file_name in file_names {
fake_language_server.notify::<lsp::notification::PublishDiagnostics>(
&lsp::PublishDiagnosticsParams {
uri: lsp::Url::from_file_path(Path::new("/test").join(file_name)).unwrap(),
uri: lsp::Url::from_file_path(Path::new(path!("/test")).join(file_name)).unwrap(),
version: None,
diagnostics: vec![lsp::Diagnostic {
severity: Some(lsp::DiagnosticSeverity::WARNING),
@@ -4360,9 +4409,9 @@ async fn test_reloading_buffer_manually(
client_a
.fs()
.insert_tree("/a", json!({ "a.rs": "let one = 1;" }))
.insert_tree(path!("/a"), json!({ "a.rs": "let one = 1;" }))
.await;
let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
let (project_a, worktree_id) = client_a.build_local_project(path!("/a"), cx_a).await;
let buffer_a = project_a
.update(cx_a, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx))
.await
@@ -4390,7 +4439,7 @@ async fn test_reloading_buffer_manually(
client_a
.fs()
.save(
"/a/a.rs".as_ref(),
path!("/a/a.rs").as_ref(),
&Rope::from("let seven = 7;"),
LineEnding::Unix,
)
@@ -4512,39 +4561,45 @@ async fn test_formatting_buffer(
"let honey = \"two\"\n"
);
// Ensure buffer can be formatted using an external command. Notice how the
// host's configuration is honored as opposed to using the guest's settings.
cx_a.update(|cx| {
SettingsStore::update_global(cx, |store, cx| {
store.update_user_settings::<AllLanguageSettings>(cx, |file| {
file.defaults.formatter = Some(SelectedFormatter::List(FormatterList(
vec![Formatter::External {
command: "awk".into(),
arguments: Some(vec!["{sub(/two/,\"{buffer_path}\")}1".to_string()].into()),
}]
.into(),
)));
// There is no `awk` command on Windows.
#[cfg(not(target_os = "windows"))]
{
// Ensure buffer can be formatted using an external command. Notice how the
// host's configuration is honored as opposed to using the guest's settings.
cx_a.update(|cx| {
SettingsStore::update_global(cx, |store, cx| {
store.update_user_settings::<AllLanguageSettings>(cx, |file| {
file.defaults.formatter = Some(SelectedFormatter::List(FormatterList(
vec![Formatter::External {
command: "awk".into(),
arguments: Some(
vec!["{sub(/two/,\"{buffer_path}\")}1".to_string()].into(),
),
}]
.into(),
)));
});
});
});
});
executor.allow_parking();
project_b
.update(cx_b, |project, cx| {
project.format(
HashSet::from_iter([buffer_b.clone()]),
LspFormatTarget::Buffers,
true,
FormatTrigger::Save,
cx,
)
})
.await
.unwrap();
assert_eq!(
buffer_b.read_with(cx_b, |buffer, _| buffer.text()),
format!("let honey = \"{}/a.rs\"\n", directory.to_str().unwrap())
);
executor.allow_parking();
project_b
.update(cx_b, |project, cx| {
project.format(
HashSet::from_iter([buffer_b.clone()]),
LspFormatTarget::Buffers,
true,
FormatTrigger::Save,
cx,
)
})
.await
.unwrap();
assert_eq!(
buffer_b.read_with(cx_b, |buffer, _| buffer.text()),
format!("let honey = \"{}/a.rs\"\n", directory.to_str().unwrap())
);
}
}
#[gpui::test(iterations = 10)]
@@ -4702,7 +4757,7 @@ async fn test_definition(
client_a
.fs()
.insert_tree(
"/root",
path!("/root"),
json!({
"dir-1": {
"a.rs": "const ONE: usize = b::TWO + b::THREE;",
@@ -4714,7 +4769,9 @@ async fn test_definition(
}),
)
.await;
let (project_a, worktree_id) = client_a.build_local_project("/root/dir-1", cx_a).await;
let (project_a, worktree_id) = client_a
.build_local_project(path!("/root/dir-1"), cx_a)
.await;
let project_id = active_call_a
.update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
.await
@@ -4735,7 +4792,7 @@ async fn test_definition(
|_, _| async move {
Ok(Some(lsp::GotoDefinitionResponse::Scalar(
lsp::Location::new(
lsp::Url::from_file_path("/root/dir-2/b.rs").unwrap(),
lsp::Url::from_file_path(path!("/root/dir-2/b.rs")).unwrap(),
lsp::Range::new(lsp::Position::new(0, 6), lsp::Position::new(0, 9)),
),
)))
@@ -4766,7 +4823,7 @@ async fn test_definition(
|_, _| async move {
Ok(Some(lsp::GotoDefinitionResponse::Scalar(
lsp::Location::new(
lsp::Url::from_file_path("/root/dir-2/b.rs").unwrap(),
lsp::Url::from_file_path(path!("/root/dir-2/b.rs")).unwrap(),
lsp::Range::new(lsp::Position::new(1, 6), lsp::Position::new(1, 11)),
),
)))
@@ -4803,7 +4860,7 @@ async fn test_definition(
);
Ok(Some(lsp::GotoDefinitionResponse::Scalar(
lsp::Location::new(
lsp::Url::from_file_path("/root/dir-2/c.rs").unwrap(),
lsp::Url::from_file_path(path!("/root/dir-2/c.rs")).unwrap(),
lsp::Range::new(lsp::Position::new(0, 5), lsp::Position::new(0, 7)),
),
)))
@@ -4855,7 +4912,7 @@ async fn test_references(
client_a
.fs()
.insert_tree(
"/root",
path!("/root"),
json!({
"dir-1": {
"one.rs": "const ONE: usize = 1;",
@@ -4867,7 +4924,9 @@ async fn test_references(
}),
)
.await;
let (project_a, worktree_id) = client_a.build_local_project("/root/dir-1", cx_a).await;
let (project_a, worktree_id) = client_a
.build_local_project(path!("/root/dir-1"), cx_a)
.await;
let project_id = active_call_a
.update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
.await
@@ -4890,7 +4949,7 @@ async fn test_references(
move |params, _| {
assert_eq!(
params.text_document_position.text_document.uri.as_str(),
"file:///root/dir-1/one.rs"
uri!("file:///root/dir-1/one.rs")
);
let rx = rx.clone();
async move {
@@ -4919,15 +4978,15 @@ async fn test_references(
lsp_response_tx
.unbounded_send(Ok(Some(vec![
lsp::Location {
uri: lsp::Url::from_file_path("/root/dir-1/two.rs").unwrap(),
uri: lsp::Url::from_file_path(path!("/root/dir-1/two.rs")).unwrap(),
range: lsp::Range::new(lsp::Position::new(0, 24), lsp::Position::new(0, 27)),
},
lsp::Location {
uri: lsp::Url::from_file_path("/root/dir-1/two.rs").unwrap(),
uri: lsp::Url::from_file_path(path!("/root/dir-1/two.rs")).unwrap(),
range: lsp::Range::new(lsp::Position::new(0, 35), lsp::Position::new(0, 38)),
},
lsp::Location {
uri: lsp::Url::from_file_path("/root/dir-2/three.rs").unwrap(),
uri: lsp::Url::from_file_path(path!("/root/dir-2/three.rs")).unwrap(),
range: lsp::Range::new(lsp::Position::new(0, 37), lsp::Position::new(0, 40)),
},
])))
@@ -4952,7 +5011,7 @@ async fn test_references(
assert_eq!(references[1].buffer, references[0].buffer);
assert_eq!(
three_buffer.file().unwrap().full_path(cx),
Path::new("/root/dir-2/three.rs")
Path::new(path!("/root/dir-2/three.rs"))
);
assert_eq!(references[0].range.to_offset(two_buffer), 24..27);
@@ -5106,7 +5165,7 @@ async fn test_document_highlights(
client_a
.fs()
.insert_tree(
"/root-1",
path!("/root-1"),
json!({
"main.rs": "fn double(number: i32) -> i32 { number + number }",
}),
@@ -5118,7 +5177,7 @@ async fn test_document_highlights(
.register_fake_lsp("Rust", Default::default());
client_a.language_registry().add(rust_lang());
let (project_a, worktree_id) = client_a.build_local_project("/root-1", cx_a).await;
let (project_a, worktree_id) = client_a.build_local_project(path!("/root-1"), cx_a).await;
let project_id = active_call_a
.update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
.await
@@ -5143,7 +5202,7 @@ async fn test_document_highlights(
.text_document
.uri
.as_str(),
"file:///root-1/main.rs"
uri!("file:///root-1/main.rs")
);
assert_eq!(
params.text_document_position_params.position,
@@ -5206,7 +5265,7 @@ async fn test_lsp_hover(
client_a
.fs()
.insert_tree(
"/root-1",
path!("/root-1"),
json!({
"main.rs": "use std::collections::HashMap;",
}),
@@ -5240,7 +5299,7 @@ async fn test_lsp_hover(
),
];
let (project_a, worktree_id) = client_a.build_local_project("/root-1", cx_a).await;
let (project_a, worktree_id) = client_a.build_local_project(path!("/root-1"), cx_a).await;
let project_id = active_call_a
.update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
.await
@@ -5280,7 +5339,7 @@ async fn test_lsp_hover(
.text_document
.uri
.as_str(),
"file:///root-1/main.rs"
uri!("file:///root-1/main.rs")
);
let name = new_server_name.clone();
async move {
@@ -5306,7 +5365,7 @@ async fn test_lsp_hover(
.text_document
.uri
.as_str(),
"file:///root-1/main.rs"
uri!("file:///root-1/main.rs")
);
assert_eq!(
params.text_document_position_params.position,
@@ -5413,7 +5472,7 @@ async fn test_project_symbols(
client_a
.fs()
.insert_tree(
"/code",
path!("/code"),
json!({
"crate-1": {
"one.rs": "const ONE: usize = 1;",
@@ -5427,7 +5486,9 @@ async fn test_project_symbols(
}),
)
.await;
let (project_a, worktree_id) = client_a.build_local_project("/code/crate-1", cx_a).await;
let (project_a, worktree_id) = client_a
.build_local_project(path!("/code/crate-1"), cx_a)
.await;
let project_id = active_call_a
.update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
.await
@@ -5450,7 +5511,7 @@ async fn test_project_symbols(
lsp::SymbolInformation {
name: "TWO".into(),
location: lsp::Location {
uri: lsp::Url::from_file_path("/code/crate-2/two.rs").unwrap(),
uri: lsp::Url::from_file_path(path!("/code/crate-2/two.rs")).unwrap(),
range: lsp::Range::new(lsp::Position::new(0, 6), lsp::Position::new(0, 9)),
},
kind: lsp::SymbolKind::CONSTANT,
@@ -5481,13 +5542,13 @@ async fn test_project_symbols(
buffer_b_2.read_with(cx_b, |buffer, cx| {
assert_eq!(
buffer.file().unwrap().full_path(cx),
Path::new("/code/crate-2/two.rs")
Path::new(path!("/code/crate-2/two.rs"))
);
});
// Attempt to craft a symbol and violate host's privacy by opening an arbitrary file.
let mut fake_symbol = symbols[0].clone();
fake_symbol.path.path = Path::new("/code/secrets").into();
fake_symbol.path.path = Path::new(path!("/code/secrets")).into();
let error = project_b
.update(cx_b, |project, cx| {
project.open_buffer_for_symbol(&fake_symbol, cx)
@@ -5520,14 +5581,14 @@ async fn test_open_buffer_while_getting_definition_pointing_to_it(
client_a
.fs()
.insert_tree(
"/root",
path!("/root"),
json!({
"a.rs": "const ONE: usize = b::TWO;",
"b.rs": "const TWO: usize = 2",
}),
)
.await;
let (project_a, worktree_id) = client_a.build_local_project("/root", cx_a).await;
let (project_a, worktree_id) = client_a.build_local_project(path!("/root"), cx_a).await;
let project_id = active_call_a
.update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
.await
@@ -5546,7 +5607,7 @@ async fn test_open_buffer_while_getting_definition_pointing_to_it(
|_, _| async move {
Ok(Some(lsp::GotoDefinitionResponse::Scalar(
lsp::Location::new(
lsp::Url::from_file_path("/root/b.rs").unwrap(),
lsp::Url::from_file_path(path!("/root/b.rs")).unwrap(),
lsp::Range::new(lsp::Position::new(0, 6), lsp::Position::new(0, 9)),
),
)))

View File

@@ -27,7 +27,7 @@ use std::{
rc::Rc,
sync::Arc,
};
use util::ResultExt;
use util::{path, ResultExt};
#[gpui::test(
iterations = 100,
@@ -280,7 +280,7 @@ impl RandomizedTest for ProjectCollaborationTest {
let mut paths = client.fs().paths(false);
paths.remove(0);
let new_root_path = if paths.is_empty() || rng.gen() {
Path::new("/").join(plan.next_root_dir_name())
Path::new(path!("/")).join(plan.next_root_dir_name())
} else {
paths.choose(rng).unwrap().clone()
};
@@ -547,7 +547,7 @@ impl RandomizedTest for ProjectCollaborationTest {
first_root_name
);
let root_path = Path::new("/").join(&first_root_name);
let root_path = Path::new(path!("/")).join(&first_root_name);
client.fs().create_dir(&root_path).await.unwrap();
client
.fs()

View File

@@ -1,8 +1,9 @@
use crate::tests::TestServer;
use call::ActiveCall;
use collections::HashSet;
use collections::{HashMap, HashSet};
use dap::DapRegistry;
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 _,
@@ -26,6 +27,7 @@ use remote_server::{HeadlessAppState, HeadlessProject};
use serde_json::json;
use settings::SettingsStore;
use std::{path::Path, sync::Arc};
use util::{path, separator};
#[gpui::test(iterations = 10)]
async fn test_sharing_an_ssh_remote_project(
@@ -52,7 +54,7 @@ async fn test_sharing_an_ssh_remote_project(
let remote_fs = FakeFs::new(server_cx.executor());
remote_fs
.insert_tree(
"/code",
path!("/code"),
json!({
"project1": {
".zed": {
@@ -84,6 +86,7 @@ async fn test_sharing_an_ssh_remote_project(
http_client: remote_http_client,
node_runtime: node,
languages,
debug_adapters: Arc::new(DapRegistry::fake()),
extension_host_proxy: Arc::new(ExtensionHostProxy::new()),
},
cx,
@@ -92,7 +95,7 @@ async fn test_sharing_an_ssh_remote_project(
let client_ssh = SshRemoteClient::fake_client(opts, cx_a).await;
let (project_a, worktree_id) = client_a
.build_ssh_project("/code/project1", client_ssh, cx_a)
.build_ssh_project(path!("/code/project1"), client_ssh, cx_a)
.await;
// While the SSH worktree is being scanned, user A shares the remote project.
@@ -178,7 +181,7 @@ async fn test_sharing_an_ssh_remote_project(
.unwrap();
assert_eq!(
remote_fs
.load("/code/project1/src/renamed.rs".as_ref())
.load(path!("/code/project1/src/renamed.rs").as_ref())
.await
.unwrap(),
"fn one() -> usize { 100 }"
@@ -193,7 +196,7 @@ async fn test_sharing_an_ssh_remote_project(
.path()
.to_string_lossy()
.to_string(),
"src/renamed.rs".to_string()
separator!("src/renamed.rs").to_string()
);
});
}
@@ -251,6 +254,7 @@ async fn test_ssh_collaboration_git_branches(
http_client: remote_http_client,
node_runtime: node,
languages,
debug_adapters: Arc::new(DapRegistry::fake()),
extension_host_proxy: Arc::new(ExtensionHostProxy::new()),
},
cx,
@@ -356,6 +360,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]
@@ -388,7 +412,10 @@ async fn test_ssh_collaboration_formatting_with_prettier(
let buffer_text = "let one = \"two\"";
let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX;
remote_fs
.insert_tree("/project", serde_json::json!({ "a.ts": buffer_text }))
.insert_tree(
path!("/project"),
serde_json::json!({ "a.ts": buffer_text }),
)
.await;
let test_plugin = "test_plugin";
@@ -427,6 +454,7 @@ async fn test_ssh_collaboration_formatting_with_prettier(
http_client: remote_http_client,
node_runtime: NodeRuntime::unavailable(),
languages,
debug_adapters: Arc::new(DapRegistry::fake()),
extension_host_proxy: Arc::new(ExtensionHostProxy::new()),
},
cx,
@@ -435,7 +463,7 @@ async fn test_ssh_collaboration_formatting_with_prettier(
let client_ssh = SshRemoteClient::fake_client(opts, cx_a).await;
let (project_a, worktree_id) = client_a
.build_ssh_project("/project", client_ssh, cx_a)
.build_ssh_project(path!("/project"), client_ssh, cx_a)
.await;
// While the SSH worktree is being scanned, user A shares the remote project.

View File

@@ -14,6 +14,7 @@ use client::{
use clock::FakeSystemClock;
use collab_ui::channel_view::ChannelView;
use collections::{HashMap, HashSet};
use dap::DapRegistry;
use fs::FakeFs;
use futures::{channel::oneshot, StreamExt as _};
use git::GitHostingProviderRegistry;
@@ -43,6 +44,7 @@ use std::{
Arc,
},
};
use util::path;
use workspace::{Workspace, WorkspaceStore};
#[cfg(not(target_os = "macos"))]
@@ -276,12 +278,14 @@ impl TestServer {
let user_store = cx.new(|cx| UserStore::new(client.clone(), cx));
let workspace_store = cx.new(|cx| WorkspaceStore::new(client.clone(), cx));
let language_registry = Arc::new(LanguageRegistry::test(cx.executor()));
let debug_adapters = Arc::new(DapRegistry::default());
let session = cx.new(|cx| AppSession::new(Session::test(), cx));
let app_state = Arc::new(workspace::AppState {
client: client.clone(),
user_store: user_store.clone(),
workspace_store,
languages: language_registry,
debug_adapters,
fs: fs.clone(),
build_window_options: |_, _| Default::default(),
node_runtime: NodeRuntime::unavailable(),
@@ -741,7 +745,7 @@ impl TestClient {
pub async fn build_test_project(&self, cx: &mut TestAppContext) -> Entity<Project> {
self.fs()
.insert_tree(
"/a",
path!("/a"),
json!({
"1.txt": "one\none\none",
"2.js": "function two() { return 2; }",
@@ -749,7 +753,7 @@ impl TestClient {
}),
)
.await;
self.build_local_project("/a", cx).await.0
self.build_local_project(path!("/a"), cx).await.0
}
pub async fn host_workspace(
@@ -794,6 +798,7 @@ impl TestClient {
self.app_state.node_runtime.clone(),
self.app_state.user_store.clone(),
self.app_state.languages.clone(),
self.app_state.debug_adapters.clone(),
self.app_state.fs.clone(),
None,
cx,

View File

@@ -1156,20 +1156,7 @@ impl Panel for ChatPanel {
}
fn icon(&self, _window: &Window, cx: &App) -> Option<ui::IconName> {
let show_icon = match ChatPanelSettings::get_global(cx).button {
ChatPanelButton::Never => false,
ChatPanelButton::Always => true,
ChatPanelButton::WhenInCall => {
let is_in_call = ActiveCall::global(cx)
.read(cx)
.room()
.map_or(false, |room| room.read(cx).contains_guests());
self.active || is_in_call
}
};
show_icon.then(|| ui::IconName::MessageBubbles)
self.enabled(cx).then(|| ui::IconName::MessageBubbles)
}
fn icon_tooltip(&self, _: &Window, _: &App) -> Option<&'static str> {
@@ -1190,6 +1177,21 @@ impl Panel for ChatPanel {
fn activation_priority(&self) -> u32 {
7
}
fn enabled(&self, cx: &App) -> bool {
match ChatPanelSettings::get_global(cx).button {
ChatPanelButton::Never => false,
ChatPanelButton::Always => true,
ChatPanelButton::WhenInCall => {
let is_in_call = ActiveCall::global(cx)
.read(cx)
.room()
.map_or(false, |room| room.read(cx).contains_guests());
self.active || is_in_call
}
}
}
}
impl EventEmitter<PanelEvent> for ChatPanel {}

View File

@@ -247,12 +247,15 @@ impl ContextServerManager {
let mut desired_servers = HashMap::default();
let (registry, project) = this.update(cx, |this, cx| {
let location = this.project.read(cx).worktrees(cx).next().map(|worktree| {
settings::SettingsLocation {
let location = this
.project
.read(cx)
.visible_worktrees(cx)
.next()
.map(|worktree| settings::SettingsLocation {
worktree_id: worktree.read(cx).id(),
path: Path::new(""),
}
});
});
let settings = ContextServerSettings::get(location, cx);
desired_servers = settings.context_servers.clone();

View File

@@ -8,6 +8,10 @@ license = "GPL-3.0-or-later"
[lints]
workspace = true
[lib]
path = "src/dap.rs"
doctest = false
[features]
test-support = [
"gpui/test-support",
@@ -35,6 +39,7 @@ log.workspace = true
node_runtime.workspace = true
parking_lot.workspace = true
paths.workspace = true
regex.workspace = true
schemars.workspace = true
serde.workspace = true
serde_json.workspace = true

View File

@@ -13,15 +13,16 @@ use serde_json::Value;
use settings::WorktreeId;
use smol::{self, fs::File, lock::Mutex};
use std::{
borrow::Borrow,
collections::{HashMap, HashSet},
ffi::{OsStr, OsString},
fmt::Debug,
net::Ipv4Addr,
ops::Deref,
path::{Path, PathBuf},
sync::Arc,
path::PathBuf,
sync::{Arc, LazyLock},
};
use task::DebugAdapterConfig;
use task::{DebugAdapterConfig, DebugTaskDefinition};
use util::ResultExt;
#[derive(Clone, Debug, PartialEq, Eq)]
@@ -46,7 +47,7 @@ pub trait DapDelegate {
}
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Deserialize, Serialize)]
pub struct DebugAdapterName(pub Arc<str>);
pub struct DebugAdapterName(pub SharedString);
impl Deref for DebugAdapterName {
type Target = str;
@@ -62,9 +63,9 @@ impl AsRef<str> for DebugAdapterName {
}
}
impl AsRef<Path> for DebugAdapterName {
fn as_ref(&self) -> &Path {
Path::new(&*self.0)
impl Borrow<str> for DebugAdapterName {
fn borrow(&self) -> &str {
&self.0
}
}
@@ -76,7 +77,7 @@ impl std::fmt::Display for DebugAdapterName {
impl From<DebugAdapterName> for SharedString {
fn from(name: DebugAdapterName) -> Self {
SharedString::from(name.0)
name.0
}
}
@@ -123,7 +124,7 @@ pub async fn download_adapter_from_github(
file_type: DownloadedFileType,
delegate: &dyn DapDelegate,
) -> Result<PathBuf> {
let adapter_path = paths::debug_adapters_dir().join(&adapter_name);
let adapter_path = paths::debug_adapters_dir().join(&adapter_name.as_ref());
let version_path = adapter_path.join(format!("{}_{}", adapter_name, github_version.tag_name));
let fs = delegate.fs();
@@ -288,15 +289,21 @@ pub trait DebugAdapter: 'static + Send + Sync {
) -> Result<DebugAdapterBinary>;
/// Should return base configuration to make the debug adapter work
fn request_args(&self, config: &DebugAdapterConfig) -> Value;
fn request_args(&self, config: &DebugTaskDefinition) -> Value;
fn attach_processes_filter(&self) -> regex::Regex {
EMPTY_REGEX.clone()
}
}
static EMPTY_REGEX: LazyLock<regex::Regex> =
LazyLock::new(|| regex::Regex::new("").expect("Regex compilation to succeed"));
#[cfg(any(test, feature = "test-support"))]
pub struct FakeAdapter {}
#[cfg(any(test, feature = "test-support"))]
impl FakeAdapter {
const ADAPTER_NAME: &'static str = "fake-adapter";
pub const ADAPTER_NAME: &'static str = "fake-adapter";
pub fn new() -> Self {
Self {}
@@ -351,13 +358,13 @@ impl DebugAdapter for FakeAdapter {
unimplemented!("get installed binary");
}
fn request_args(&self, config: &DebugAdapterConfig) -> Value {
fn request_args(&self, config: &DebugTaskDefinition) -> Value {
use serde_json::json;
use task::DebugRequestType;
json!({
"request": match config.request {
DebugRequestType::Launch => "launch",
DebugRequestType::Launch(_) => "launch",
DebugRequestType::Attach(_) => "attach",
},
"process_id": if let DebugRequestType::Attach(attach_config) = &config.request {
@@ -367,4 +374,10 @@ impl DebugAdapter for FakeAdapter {
},
})
}
fn attach_processes_filter(&self) -> regex::Regex {
static REGEX: LazyLock<regex::Regex> =
LazyLock::new(|| regex::Regex::new("^fake-binary").unwrap());
REGEX.clone()
}
}

View File

@@ -71,7 +71,6 @@ impl DebugAdapterClient {
let client_id = this.id;
// start handling events/reverse requests
cx.background_spawn(Self::handle_receive_messages(
client_id,
server_rx,
@@ -119,7 +118,6 @@ impl DebugAdapterClient {
Ok(message) => message,
Err(e) => break Err(e.into()),
};
match message {
Message::Event(ev) => {
log::debug!("Client {} received event `{}`", client_id.0, &ev);
@@ -164,7 +162,6 @@ impl DebugAdapterClient {
command: R::COMMAND.to_string(),
arguments: Some(serialized_arguments),
};
self.transport_delegate
.add_pending_request(sequence_id, callback_tx)
.await;
@@ -172,7 +169,7 @@ impl DebugAdapterClient {
log::debug!(
"Client {} send `{}` request with sequence_id: {}",
self.id.0,
R::COMMAND.to_string(),
R::COMMAND,
sequence_id
);
@@ -434,7 +431,7 @@ mod tests {
let client = DebugAdapterClient::start(
crate::client::SessionId(1),
DebugAdapterName(Arc::from("test-adapter")),
DebugAdapterName("test-adapter".into()),
DebugAdapterBinary {
command: "command".into(),
arguments: Default::default(),

17
crates/dap/src/dap.rs Normal file
View File

@@ -0,0 +1,17 @@
pub mod adapters;
pub mod client;
pub mod debugger_settings;
pub mod proto_conversions;
mod registry;
pub mod transport;
pub use dap_types::*;
pub use registry::DapRegistry;
pub use task::{DebugAdapterConfig, DebugRequestType};
pub type ScopeId = u64;
pub type VariableReference = u64;
pub type StackFrameId = u64;
#[cfg(any(test, feature = "test-support"))]
pub use adapters::FakeAdapter;

View File

@@ -1,38 +0,0 @@
pub mod adapters;
pub mod client;
pub mod debugger_settings;
pub mod proto_conversions;
pub mod transport;
pub use dap_types::*;
pub use task::{DebugAdapterConfig, DebugAdapterKind, DebugRequestType};
pub type ScopeId = u64;
pub type VariableReference = u64;
pub type StackFrameId = u64;
#[cfg(any(test, feature = "test-support"))]
pub use adapters::FakeAdapter;
#[cfg(any(test, feature = "test-support"))]
pub fn test_config(
request: DebugRequestType,
fail: Option<bool>,
caps: Option<Capabilities>,
) -> DebugAdapterConfig {
DebugAdapterConfig {
label: "test config".into(),
kind: DebugAdapterKind::Fake((
fail.unwrap_or_default(),
caps.unwrap_or(Capabilities {
supports_step_back: Some(false),
..Default::default()
}),
)),
request,
program: None,
supports_attach: false,
cwd: None,
initialize_args: None,
}
}

View File

@@ -0,0 +1,39 @@
use parking_lot::RwLock;
use crate::adapters::{DebugAdapter, DebugAdapterName};
use std::{collections::BTreeMap, sync::Arc};
#[derive(Default)]
struct DapRegistryState {
adapters: BTreeMap<DebugAdapterName, Arc<dyn DebugAdapter>>,
}
#[derive(Default)]
/// Stores available debug adapters.
pub struct DapRegistry(Arc<RwLock<DapRegistryState>>);
impl DapRegistry {
pub fn add_adapter(&self, adapter: Arc<dyn DebugAdapter>) {
let name = adapter.name();
let _previous_value = self.0.write().adapters.insert(name, adapter);
debug_assert!(
_previous_value.is_none(),
"Attempted to insert a new debug adapter when one is already registered"
);
}
pub fn adapter(&self, name: &str) -> Option<Arc<dyn DebugAdapter>> {
self.0.read().adapters.get(name).cloned()
}
pub fn enumerate_adapters(&self) -> Vec<DebugAdapterName> {
self.0.read().adapters.keys().cloned().collect()
}
#[cfg(any(test, feature = "test-support"))]
pub fn fake() -> Self {
use crate::FakeAdapter;
let register = Self::default();
register.add_adapter(Arc::new(FakeAdapter::new()));
register
}
}

View File

@@ -261,8 +261,6 @@ impl TransportDelegate {
}
}
}
smol::future::yield_now().await;
};
log::debug!("Handle adapter log dropped");
@@ -319,8 +317,6 @@ impl TransportDelegate {
}
Err(error) => break Err(error.into()),
}
smol::future::yield_now().await;
};
log::debug!("Handle adapter input dropped");
@@ -360,8 +356,6 @@ impl TransportDelegate {
}
Err(e) => break Err(e),
}
smol::future::yield_now().await;
};
drop(client_tx);
@@ -393,8 +387,6 @@ impl TransportDelegate {
}
Err(error) => break Err(error.into()),
}
smol::future::yield_now().await;
};
log::debug!("Handle adapter error dropped");

View File

@@ -30,7 +30,6 @@ paths.workspace = true
regex.workspace = true
serde.workspace = true
serde_json.workspace = true
sysinfo.workspace = true
task.workspace = true
util.workspace = true

View File

@@ -1,84 +0,0 @@
use dap::transport::TcpTransport;
use gpui::AsyncApp;
use serde_json::Value;
use std::{collections::HashMap, ffi::OsString, path::PathBuf};
use sysinfo::{Pid, Process};
use task::DebugAdapterConfig;
use crate::*;
pub(crate) struct CustomDebugAdapter {
custom_args: CustomArgs,
}
impl CustomDebugAdapter {
const ADAPTER_NAME: &'static str = "custom_dap";
pub(crate) async fn new(custom_args: CustomArgs) -> Result<Self> {
Ok(CustomDebugAdapter { custom_args })
}
pub fn attach_processes(processes: &HashMap<Pid, Process>) -> Vec<(&Pid, &Process)> {
processes.iter().collect::<Vec<_>>()
}
}
#[async_trait(?Send)]
impl DebugAdapter for CustomDebugAdapter {
fn name(&self) -> DebugAdapterName {
DebugAdapterName(Self::ADAPTER_NAME.into())
}
async fn get_binary(
&self,
_: &dyn DapDelegate,
config: &DebugAdapterConfig,
_: Option<PathBuf>,
_: &mut AsyncApp,
) -> Result<DebugAdapterBinary> {
let connection = if let DebugConnectionType::TCP(connection) = &self.custom_args.connection
{
Some(adapters::TcpArguments {
host: connection.host(),
port: TcpTransport::port(&connection).await?,
timeout: connection.timeout,
})
} else {
None
};
let ret = DebugAdapterBinary {
command: self.custom_args.command.clone(),
arguments: self
.custom_args
.args
.clone()
.map(|args| args.iter().map(OsString::from).collect()),
cwd: config.cwd.clone(),
envs: self.custom_args.envs.clone(),
connection,
};
Ok(ret)
}
async fn fetch_latest_adapter_version(&self, _: &dyn DapDelegate) -> Result<AdapterVersion> {
bail!("Custom debug adapters don't have latest versions")
}
async fn install_binary(&self, _: AdapterVersion, _: &dyn DapDelegate) -> Result<()> {
bail!("Custom debug adapters cannot be installed")
}
async fn get_installed_binary(
&self,
_: &dyn DapDelegate,
_: &DebugAdapterConfig,
_: Option<PathBuf>,
_: &mut AsyncApp,
) -> Result<DebugAdapterBinary> {
bail!("Custom debug adapters cannot be installed")
}
fn request_args(&self, config: &DebugAdapterConfig) -> Value {
json!({"program": config.program})
}
}

View File

@@ -1,5 +1,3 @@
mod custom;
#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
mod gdb;
mod go;
mod javascript;
@@ -7,16 +5,17 @@ mod lldb;
mod php;
mod python;
use std::{collections::HashMap, sync::Arc};
use std::{net::Ipv4Addr, sync::Arc};
use anyhow::{anyhow, bail, Result};
use anyhow::{anyhow, Result};
use async_trait::async_trait;
use custom::CustomDebugAdapter;
use dap::adapters::{
self, AdapterVersion, DapDelegate, DebugAdapter, DebugAdapterBinary, DebugAdapterName,
GithubRepo,
use dap::{
adapters::{
self, AdapterVersion, DapDelegate, DebugAdapter, DebugAdapterBinary, DebugAdapterName,
GithubRepo,
},
DapRegistry,
};
#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
use gdb::GdbDebugAdapter;
use go::GoDebugAdapter;
use javascript::JsDebugAdapter;
@@ -24,44 +23,28 @@ use lldb::LldbDebugAdapter;
use php::PhpDebugAdapter;
use python::PythonDebugAdapter;
use serde_json::{json, Value};
use sysinfo::{Pid, Process};
use task::{CustomArgs, DebugAdapterConfig, DebugAdapterKind, DebugConnectionType, TCPHost};
use task::{DebugAdapterConfig, TCPHost};
pub async fn build_adapter(kind: &DebugAdapterKind) -> Result<Arc<dyn DebugAdapter>> {
match kind {
DebugAdapterKind::Custom(start_args) => {
Ok(Arc::new(CustomDebugAdapter::new(start_args.clone()).await?))
}
DebugAdapterKind::Python(host) => Ok(Arc::new(PythonDebugAdapter::new(host).await?)),
DebugAdapterKind::Php(host) => Ok(Arc::new(PhpDebugAdapter::new(host.clone()).await?)),
DebugAdapterKind::Javascript(host) => {
Ok(Arc::new(JsDebugAdapter::new(host.clone()).await?))
}
DebugAdapterKind::Lldb => Ok(Arc::new(LldbDebugAdapter::new())),
DebugAdapterKind::Go(host) => Ok(Arc::new(GoDebugAdapter::new(host).await?)),
#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
DebugAdapterKind::Gdb => Ok(Arc::new(GdbDebugAdapter::new())),
#[cfg(any(test, feature = "test-support"))]
DebugAdapterKind::Fake(_) => Ok(Arc::new(dap::adapters::FakeAdapter::new())),
#[cfg(not(any(test, feature = "test-support")))]
#[allow(unreachable_patterns)]
_ => unreachable!("Fake variant only exists with test-support feature"),
}
pub fn init(registry: Arc<DapRegistry>) {
registry.add_adapter(Arc::from(PythonDebugAdapter));
registry.add_adapter(Arc::from(PhpDebugAdapter));
registry.add_adapter(Arc::from(JsDebugAdapter::default()));
registry.add_adapter(Arc::from(LldbDebugAdapter));
registry.add_adapter(Arc::from(GoDebugAdapter));
registry.add_adapter(Arc::from(GdbDebugAdapter));
}
pub fn attach_processes<'a>(
kind: &DebugAdapterKind,
processes: &'a HashMap<Pid, Process>,
) -> Vec<(&'a Pid, &'a Process)> {
match kind {
#[cfg(any(test, feature = "test-support"))]
DebugAdapterKind::Fake(_) => processes
.iter()
.filter(|(pid, _)| pid.as_u32() == std::process::id())
.collect::<Vec<_>>(),
DebugAdapterKind::Custom(_) => CustomDebugAdapter::attach_processes(processes),
DebugAdapterKind::Javascript(_) => JsDebugAdapter::attach_processes(processes),
DebugAdapterKind::Lldb => LldbDebugAdapter::attach_processes(processes),
_ => processes.iter().collect::<Vec<_>>(),
}
pub(crate) async fn configure_tcp_connection(
tcp_connection: TCPHost,
) -> Result<(Ipv4Addr, u16, Option<u64>)> {
let host = tcp_connection.host();
let timeout = tcp_connection.timeout;
let port = if let Some(port) = tcp_connection.port {
port
} else {
dap::transport::TcpTransport::port(&tcp_connection).await?
};
Ok((host, port, timeout))
}

View File

@@ -1,20 +1,17 @@
use std::ffi::OsStr;
use anyhow::Result;
use anyhow::{bail, Result};
use async_trait::async_trait;
use gpui::AsyncApp;
use task::DebugAdapterConfig;
use task::{DebugAdapterConfig, DebugTaskDefinition};
use crate::*;
pub(crate) struct GdbDebugAdapter {}
#[derive(Default)]
pub(crate) struct GdbDebugAdapter;
impl GdbDebugAdapter {
const ADAPTER_NAME: &'static str = "gdb";
pub(crate) fn new() -> Self {
GdbDebugAdapter {}
}
const ADAPTER_NAME: &'static str = "GDB";
}
#[async_trait(?Send)]
@@ -26,7 +23,7 @@ impl DebugAdapter for GdbDebugAdapter {
async fn get_binary(
&self,
delegate: &dyn DapDelegate,
config: &DebugAdapterConfig,
_: &DebugAdapterConfig,
user_installed_path: Option<std::path::PathBuf>,
_: &mut AsyncApp,
) -> Result<DebugAdapterBinary> {
@@ -34,7 +31,6 @@ impl DebugAdapter for GdbDebugAdapter {
.filter(|p| p.exists())
.and_then(|p| p.to_str().map(|s| s.to_string()));
/* GDB implements DAP natively so just need to */
let gdb_path = delegate
.which(OsStr::new("gdb"))
.and_then(|p| p.to_str().map(|s| s.to_string()))
@@ -50,7 +46,7 @@ impl DebugAdapter for GdbDebugAdapter {
command: gdb_path,
arguments: Some(vec!["-i=dap".into()]),
envs: None,
cwd: config.cwd.clone(),
cwd: None,
connection: None,
})
}
@@ -77,7 +73,14 @@ impl DebugAdapter for GdbDebugAdapter {
unimplemented!("GDB cannot be installed by Zed (yet)")
}
fn request_args(&self, config: &DebugAdapterConfig) -> Value {
json!({"program": config.program, "cwd": config.cwd})
fn request_args(&self, config: &DebugTaskDefinition) -> Value {
match &config.request {
dap::DebugRequestType::Attach(attach_config) => {
json!({"pid": attach_config.process_id})
}
dap::DebugRequestType::Launch(launch_config) => {
json!({"program": launch_config.program, "cwd": launch_config.cwd})
}
}
}
}

View File

@@ -1,25 +1,15 @@
use dap::transport::TcpTransport;
use anyhow::bail;
use gpui::AsyncApp;
use std::{ffi::OsStr, net::Ipv4Addr, path::PathBuf};
use std::{ffi::OsStr, path::PathBuf};
use task::DebugTaskDefinition;
use crate::*;
pub(crate) struct GoDebugAdapter {
port: u16,
host: Ipv4Addr,
timeout: Option<u64>,
}
#[derive(Default, Debug)]
pub(crate) struct GoDebugAdapter;
impl GoDebugAdapter {
const ADAPTER_NAME: &'static str = "delve";
pub(crate) async fn new(host: &TCPHost) -> Result<Self> {
Ok(GoDebugAdapter {
port: TcpTransport::port(host).await?,
host: host.host(),
timeout: host.timeout,
})
}
const ADAPTER_NAME: &'static str = "Delve";
}
#[async_trait(?Send)]
@@ -73,28 +63,39 @@ impl DebugAdapter for GoDebugAdapter {
.and_then(|p| p.to_str().map(|p| p.to_string()))
.ok_or(anyhow!("Dlv not found in path"))?;
let Some(tcp_connection) = config.tcp_connection.clone() else {
bail!("Go Debug Adapter expects tcp connection arguments to be provided");
};
let (host, port, timeout) = crate::configure_tcp_connection(tcp_connection).await?;
Ok(DebugAdapterBinary {
command: delve_path,
arguments: Some(vec![
"dap".into(),
"--listen".into(),
format!("{}:{}", self.host, self.port).into(),
format!("{}:{}", host, port).into(),
]),
cwd: config.cwd.clone(),
cwd: None,
envs: None,
connection: Some(adapters::TcpArguments {
host: self.host,
port: self.port,
timeout: self.timeout,
host,
port,
timeout,
}),
})
}
fn request_args(&self, config: &DebugAdapterConfig) -> Value {
json!({
"program": config.program,
"cwd": config.cwd,
"subProcess": true,
})
fn request_args(&self, config: &DebugTaskDefinition) -> Value {
match &config.request {
dap::DebugRequestType::Attach(attach_config) => {
json!({
"processId": attach_config.process_id
})
}
dap::DebugRequestType::Launch(launch_config) => json!({
"program": launch_config.program,
"cwd": launch_config.cwd,
}),
}
}
}

View File

@@ -1,39 +1,28 @@
use adapters::latest_github_release;
use dap::transport::TcpTransport;
use gpui::AsyncApp;
use regex::Regex;
use std::{collections::HashMap, net::Ipv4Addr, path::PathBuf};
use sysinfo::{Pid, Process};
use task::DebugRequestType;
use std::path::PathBuf;
use task::{DebugRequestType, DebugTaskDefinition};
use crate::*;
#[derive(Debug)]
pub(crate) struct JsDebugAdapter {
port: u16,
host: Ipv4Addr,
timeout: Option<u64>,
attach_processes: Regex,
}
impl Default for JsDebugAdapter {
fn default() -> Self {
Self {
attach_processes: Regex::new(r"(?i)^(?:node|bun|iojs)(?:$|\b)")
.expect("Regex compilation to succeed"),
}
}
}
impl JsDebugAdapter {
const ADAPTER_NAME: &'static str = "vscode-js-debug";
const ADAPTER_NAME: &'static str = "JavaScript";
const ADAPTER_NPM_NAME: &'static str = "vscode-js-debug";
const ADAPTER_PATH: &'static str = "js-debug/src/dapDebugServer.js";
pub(crate) async fn new(host: TCPHost) -> Result<Self> {
Ok(JsDebugAdapter {
host: host.host(),
timeout: host.timeout,
port: TcpTransport::port(&host).await?,
})
}
pub fn attach_processes(processes: &HashMap<Pid, Process>) -> Vec<(&Pid, &Process)> {
let regex = Regex::new(r"(?i)^(?:node|bun|iojs)(?:$|\b)").unwrap();
processes
.iter()
.filter(|(_, process)| regex.is_match(&process.name().to_string_lossy()))
.collect::<Vec<_>>()
}
}
#[async_trait(?Send)]
@@ -47,7 +36,7 @@ impl DebugAdapter for JsDebugAdapter {
delegate: &dyn DapDelegate,
) -> Result<AdapterVersion> {
let release = latest_github_release(
&format!("{}/{}", "microsoft", Self::ADAPTER_NAME),
&format!("{}/{}", "microsoft", Self::ADAPTER_NPM_NAME),
true,
false,
delegate.http_client(),
@@ -78,7 +67,7 @@ impl DebugAdapter for JsDebugAdapter {
let adapter_path = if let Some(user_installed_path) = user_installed_path {
user_installed_path
} else {
let adapter_path = paths::debug_adapters_dir().join(self.name());
let adapter_path = paths::debug_adapters_dir().join(self.name().as_ref());
let file_name_prefix = format!("{}_", self.name());
@@ -89,6 +78,13 @@ impl DebugAdapter for JsDebugAdapter {
.ok_or_else(|| anyhow!("Couldn't find JavaScript dap directory"))?
};
let Some(tcp_connection) = config.tcp_connection.clone() else {
anyhow::bail!(
"Javascript Debug Adapter expects tcp connection arguments to be provided"
);
};
let (host, port, timeout) = crate::configure_tcp_connection(tcp_connection).await?;
Ok(DebugAdapterBinary {
command: delegate
.node_runtime()
@@ -98,15 +94,15 @@ impl DebugAdapter for JsDebugAdapter {
.into_owned(),
arguments: Some(vec![
adapter_path.join(Self::ADAPTER_PATH).into(),
self.port.to_string().into(),
self.host.to_string().into(),
port.to_string().into(),
host.to_string().into(),
]),
cwd: config.cwd.clone(),
cwd: None,
envs: None,
connection: Some(adapters::TcpArguments {
host: self.host,
port: self.port,
timeout: self.timeout,
host,
port,
timeout,
}),
})
}
@@ -127,22 +123,35 @@ impl DebugAdapter for JsDebugAdapter {
return Ok(());
}
fn request_args(&self, config: &DebugAdapterConfig) -> Value {
let pid = if let DebugRequestType::Attach(attach_config) = &config.request {
attach_config.process_id
} else {
None
};
json!({
"program": config.program,
fn request_args(&self, config: &DebugTaskDefinition) -> Value {
let mut args = json!({
"type": "pwa-node",
"request": match config.request {
DebugRequestType::Launch => "launch",
DebugRequestType::Launch(_) => "launch",
DebugRequestType::Attach(_) => "attach",
},
"processId": pid,
"cwd": config.cwd,
})
});
let map = args.as_object_mut().unwrap();
match &config.request {
DebugRequestType::Attach(attach) => {
map.insert("processId".into(), attach.process_id.into());
}
DebugRequestType::Launch(launch) => {
map.insert("program".into(), launch.program.clone().into());
map.insert(
"cwd".into(),
launch
.cwd
.as_ref()
.map(|s| s.to_string_lossy().into_owned())
.into(),
);
}
}
args
}
fn attach_processes_filter(&self) -> Regex {
self.attach_processes.clone()
}
}

View File

@@ -1,25 +1,17 @@
use std::{collections::HashMap, ffi::OsStr, path::PathBuf};
use std::{ffi::OsStr, path::PathBuf};
use anyhow::Result;
use async_trait::async_trait;
use gpui::AsyncApp;
use sysinfo::{Pid, Process};
use task::{DebugAdapterConfig, DebugRequestType};
use task::{DebugAdapterConfig, DebugRequestType, DebugTaskDefinition};
use crate::*;
pub(crate) struct LldbDebugAdapter {}
#[derive(Default)]
pub(crate) struct LldbDebugAdapter;
impl LldbDebugAdapter {
const ADAPTER_NAME: &'static str = "lldb";
pub(crate) fn new() -> Self {
LldbDebugAdapter {}
}
pub fn attach_processes(processes: &HashMap<Pid, Process>) -> Vec<(&Pid, &Process)> {
processes.iter().collect::<Vec<_>>()
}
const ADAPTER_NAME: &'static str = "LLDB";
}
#[async_trait(?Send)]
@@ -31,7 +23,7 @@ impl DebugAdapter for LldbDebugAdapter {
async fn get_binary(
&self,
delegate: &dyn DapDelegate,
config: &DebugAdapterConfig,
_: &DebugAdapterConfig,
user_installed_path: Option<PathBuf>,
_: &mut AsyncApp,
) -> Result<DebugAdapterBinary> {
@@ -48,7 +40,7 @@ impl DebugAdapter for LldbDebugAdapter {
command: lldb_dap_path,
arguments: None,
envs: None,
cwd: config.cwd.clone(),
cwd: None,
connection: None,
})
}
@@ -75,21 +67,30 @@ impl DebugAdapter for LldbDebugAdapter {
unimplemented!("LLDB debug adapter cannot be installed by Zed (yet)")
}
fn request_args(&self, config: &DebugAdapterConfig) -> Value {
let pid = if let DebugRequestType::Attach(attach_config) = &config.request {
attach_config.process_id
} else {
None
};
json!({
"program": config.program,
fn request_args(&self, config: &DebugTaskDefinition) -> Value {
let mut args = json!({
"request": match config.request {
DebugRequestType::Launch => "launch",
DebugRequestType::Launch(_) => "launch",
DebugRequestType::Attach(_) => "attach",
},
"pid": pid,
"cwd": config.cwd,
})
});
let map = args.as_object_mut().unwrap();
match &config.request {
DebugRequestType::Attach(attach) => {
map.insert("pid".into(), attach.process_id.into());
}
DebugRequestType::Launch(launch) => {
map.insert("program".into(), launch.program.clone().into());
map.insert(
"cwd".into(),
launch
.cwd
.as_ref()
.map(|s| s.to_string_lossy().into_owned())
.into(),
);
}
}
args
}
}

View File

@@ -1,27 +1,19 @@
use adapters::latest_github_release;
use dap::{adapters::TcpArguments, transport::TcpTransport};
use anyhow::bail;
use dap::adapters::TcpArguments;
use gpui::AsyncApp;
use std::{net::Ipv4Addr, path::PathBuf};
use std::path::PathBuf;
use task::DebugTaskDefinition;
use crate::*;
pub(crate) struct PhpDebugAdapter {
port: u16,
host: Ipv4Addr,
timeout: Option<u64>,
}
#[derive(Default)]
pub(crate) struct PhpDebugAdapter;
impl PhpDebugAdapter {
const ADAPTER_NAME: &'static str = "vscode-php-debug";
const ADAPTER_NAME: &'static str = "PHP";
const ADAPTER_PACKAGE_NAME: &'static str = "vscode-php-debug";
const ADAPTER_PATH: &'static str = "extension/out/phpDebug.js";
pub(crate) async fn new(host: TCPHost) -> Result<Self> {
Ok(PhpDebugAdapter {
port: TcpTransport::port(&host).await?,
host: host.host(),
timeout: host.timeout,
})
}
}
#[async_trait(?Send)]
@@ -35,7 +27,7 @@ impl DebugAdapter for PhpDebugAdapter {
delegate: &dyn DapDelegate,
) -> Result<AdapterVersion> {
let release = latest_github_release(
&format!("{}/{}", "xdebug", Self::ADAPTER_NAME),
&format!("{}/{}", "xdebug", Self::ADAPTER_PACKAGE_NAME),
true,
false,
delegate.http_client(),
@@ -66,7 +58,7 @@ impl DebugAdapter for PhpDebugAdapter {
let adapter_path = if let Some(user_installed_path) = user_installed_path {
user_installed_path
} else {
let adapter_path = paths::debug_adapters_dir().join(self.name());
let adapter_path = paths::debug_adapters_dir().join(self.name().as_ref());
let file_name_prefix = format!("{}_", self.name());
@@ -77,6 +69,11 @@ impl DebugAdapter for PhpDebugAdapter {
.ok_or_else(|| anyhow!("Couldn't find PHP dap directory"))?
};
let Some(tcp_connection) = config.tcp_connection.clone() else {
bail!("PHP Debug Adapter expects tcp connection arguments to be provided");
};
let (host, port, timeout) = crate::configure_tcp_connection(tcp_connection).await?;
Ok(DebugAdapterBinary {
command: delegate
.node_runtime()
@@ -86,14 +83,14 @@ impl DebugAdapter for PhpDebugAdapter {
.into_owned(),
arguments: Some(vec![
adapter_path.join(Self::ADAPTER_PATH).into(),
format!("--server={}", self.port).into(),
format!("--server={}", port).into(),
]),
connection: Some(TcpArguments {
port: self.port,
host: self.host,
timeout: self.timeout,
port,
host,
timeout,
}),
cwd: config.cwd.clone(),
cwd: None,
envs: None,
})
}
@@ -114,10 +111,18 @@ impl DebugAdapter for PhpDebugAdapter {
Ok(())
}
fn request_args(&self, config: &DebugAdapterConfig) -> Value {
json!({
"program": config.program,
"cwd": config.cwd,
})
fn request_args(&self, config: &DebugTaskDefinition) -> Value {
match &config.request {
dap::DebugRequestType::Attach(_) => {
// php adapter does not support attaching
json!({})
}
dap::DebugRequestType::Launch(launch_config) => {
json!({
"program": launch_config.program,
"cwd": launch_config.cwd,
})
}
}
}
}

View File

@@ -1,26 +1,18 @@
use crate::*;
use dap::transport::TcpTransport;
use anyhow::bail;
use dap::DebugRequestType;
use gpui::AsyncApp;
use std::{ffi::OsStr, net::Ipv4Addr, path::PathBuf};
use std::{ffi::OsStr, path::PathBuf};
use task::DebugTaskDefinition;
pub(crate) struct PythonDebugAdapter {
port: u16,
host: Ipv4Addr,
timeout: Option<u64>,
}
#[derive(Default)]
pub(crate) struct PythonDebugAdapter;
impl PythonDebugAdapter {
const ADAPTER_NAME: &'static str = "debugpy";
const ADAPTER_NAME: &'static str = "Debugpy";
const ADAPTER_PACKAGE_NAME: &'static str = "debugpy";
const ADAPTER_PATH: &'static str = "src/debugpy/adapter";
const LANGUAGE_NAME: &'static str = "Python";
pub(crate) async fn new(host: &TCPHost) -> Result<Self> {
Ok(PythonDebugAdapter {
port: TcpTransport::port(host).await?,
host: host.host(),
timeout: host.timeout,
})
}
}
#[async_trait(?Send)]
@@ -34,7 +26,7 @@ impl DebugAdapter for PythonDebugAdapter {
delegate: &dyn DapDelegate,
) -> Result<AdapterVersion> {
let github_repo = GithubRepo {
repo_name: Self::ADAPTER_NAME.into(),
repo_name: Self::ADAPTER_PACKAGE_NAME.into(),
repo_owner: "microsoft".into(),
};
@@ -78,12 +70,16 @@ impl DebugAdapter for PythonDebugAdapter {
cx: &mut AsyncApp,
) -> Result<DebugAdapterBinary> {
const BINARY_NAMES: [&str; 3] = ["python3", "python", "py"];
let Some(tcp_connection) = config.tcp_connection.clone() else {
bail!("Python Debug Adapter expects tcp connection arguments to be provided");
};
let (host, port, timeout) = crate::configure_tcp_connection(tcp_connection).await?;
let debugpy_dir = if let Some(user_installed_path) = user_installed_path {
user_installed_path
} else {
let adapter_path = paths::debug_adapters_dir().join(self.name());
let file_name_prefix = format!("{}_", self.name());
let adapter_path = paths::debug_adapters_dir().join(self.name().as_ref());
let file_name_prefix = format!("{}_", Self::ADAPTER_PACKAGE_NAME);
util::fs::find_file_name_in_dir(adapter_path.as_path(), |file_name| {
file_name.starts_with(&file_name_prefix)
@@ -118,25 +114,36 @@ impl DebugAdapter for PythonDebugAdapter {
command: python_path.ok_or(anyhow!("failed to find binary path for python"))?,
arguments: Some(vec![
debugpy_dir.join(Self::ADAPTER_PATH).into(),
format!("--port={}", self.port).into(),
format!("--host={}", self.host).into(),
format!("--port={}", port).into(),
format!("--host={}", host).into(),
]),
connection: Some(adapters::TcpArguments {
host: self.host,
port: self.port,
timeout: self.timeout,
host,
port,
timeout,
}),
cwd: config.cwd.clone(),
cwd: None,
envs: None,
})
}
fn request_args(&self, config: &DebugAdapterConfig) -> Value {
json!({
"program": config.program,
"subProcess": true,
"cwd": config.cwd,
"redirectOutput": true,
})
fn request_args(&self, config: &DebugTaskDefinition) -> Value {
match &config.request {
DebugRequestType::Launch(launch_config) => {
json!({
"program": launch_config.program,
"subProcess": true,
"cwd": launch_config.cwd,
"redirectOutput": true,
})
}
dap::DebugRequestType::Attach(attach_config) => {
json!({
"subProcess": true,
"redirectOutput": true,
"processId": attach_config.process_id
})
}
}
}
}

View File

@@ -8,6 +8,10 @@ license = "GPL-3.0-or-later"
[lints]
workspace = true
[lib]
path = "src/debugger_ui.rs"
doctest = false
[features]
test-support = [
"dap/test-support",

View File

@@ -3,8 +3,8 @@ use fuzzy::{StringMatch, StringMatchCandidate};
use gpui::Subscription;
use gpui::{DismissEvent, Entity, EventEmitter, Focusable, Render};
use picker::{Picker, PickerDelegate};
use project::debugger::attach_processes;
use std::cell::LazyCell;
use std::sync::Arc;
use sysinfo::System;
use ui::{prelude::*, Context, Tooltip};
@@ -13,10 +13,10 @@ use util::debug_panic;
use workspace::ModalView;
#[derive(Debug, Clone)]
struct Candidate {
pid: u32,
name: String,
command: Vec<String>,
pub(super) struct Candidate {
pub(super) pid: u32,
pub(super) name: SharedString,
pub(super) command: Vec<String>,
}
pub(crate) struct AttachModalDelegate {
@@ -24,16 +24,20 @@ pub(crate) struct AttachModalDelegate {
matches: Vec<StringMatch>,
placeholder_text: Arc<str>,
project: Entity<project::Project>,
debug_config: task::DebugAdapterConfig,
candidates: Option<Vec<Candidate>>,
debug_config: task::DebugTaskDefinition,
candidates: Arc<[Candidate]>,
}
impl AttachModalDelegate {
pub fn new(project: Entity<project::Project>, debug_config: task::DebugAdapterConfig) -> Self {
fn new(
project: Entity<project::Project>,
debug_config: task::DebugTaskDefinition,
candidates: Arc<[Candidate]>,
) -> Self {
Self {
project,
debug_config,
candidates: None,
candidates,
selected_index: 0,
matches: Vec::default(),
placeholder_text: Arc::from("Select the process you want to attach the debugger to"),
@@ -49,12 +53,56 @@ pub struct AttachModal {
impl AttachModal {
pub fn new(
project: Entity<project::Project>,
debug_config: task::DebugAdapterConfig,
debug_config: task::DebugTaskDefinition,
window: &mut Window,
cx: &mut Context<Self>,
) -> Self {
let mut processes: Vec<_> = System::new_all()
.processes()
.values()
.map(|process| {
let name = process.name().to_string_lossy().into_owned();
Candidate {
name: name.into(),
pid: process.pid().as_u32(),
command: process
.cmd()
.iter()
.map(|s| s.to_string_lossy().to_string())
.collect::<Vec<_>>(),
}
})
.collect();
processes.sort_by_key(|k| k.name.clone());
Self::with_processes(project, debug_config, processes, window, cx)
}
pub(super) fn with_processes(
project: Entity<project::Project>,
debug_config: task::DebugTaskDefinition,
processes: Vec<Candidate>,
window: &mut Window,
cx: &mut Context<Self>,
) -> Self {
let adapter = project
.read(cx)
.debug_adapters()
.adapter(&debug_config.adapter);
let filter = LazyCell::new(|| adapter.map(|adapter| adapter.attach_processes_filter()));
let processes = processes
.into_iter()
.filter(|process| {
filter
.as_ref()
.map_or(false, |filter| filter.is_match(&process.name))
})
.collect();
let picker = cx.new(|cx| {
Picker::uniform_list(AttachModalDelegate::new(project, debug_config), window, cx)
Picker::uniform_list(
AttachModalDelegate::new(project, debug_config, processes),
window,
cx,
)
});
Self {
_subscription: cx.subscribe(&picker, |_, _, _, cx| {
@@ -116,32 +164,7 @@ impl PickerDelegate for AttachModalDelegate {
) -> gpui::Task<()> {
cx.spawn(async move |this, cx| {
let Some(processes) = this
.update(cx, |this, _| {
if let Some(processes) = this.delegate.candidates.clone() {
processes
} else {
let system = System::new_all();
let processes =
attach_processes(&this.delegate.debug_config.kind, &system.processes());
let candidates = processes
.into_iter()
.map(|(pid, process)| Candidate {
pid: pid.as_u32(),
name: process.name().to_string_lossy().into_owned(),
command: process
.cmd()
.iter()
.map(|s| s.to_string_lossy().to_string())
.collect::<Vec<_>>(),
})
.collect::<Vec<Candidate>>();
let _ = this.delegate.candidates.insert(candidates.clone());
candidates
}
})
.update(cx, |this, _| this.delegate.candidates.clone())
.ok()
else {
return;
@@ -176,7 +199,6 @@ impl PickerDelegate for AttachModalDelegate {
let delegate = &mut this.delegate;
delegate.matches = matches;
delegate.candidates = Some(processes);
if delegate.matches.is_empty() {
delegate.selected_index = 0;
@@ -195,7 +217,7 @@ impl PickerDelegate for AttachModalDelegate {
.get(self.selected_index())
.and_then(|current_match| {
let ix = current_match.candidate_id;
self.candidates.as_ref().map(|candidates| &candidates[ix])
self.candidates.get(ix)
});
let Some(candidate) = candidate else {
@@ -206,7 +228,7 @@ impl PickerDelegate for AttachModalDelegate {
DebugRequestType::Attach(config) => {
config.process_id = Some(candidate.pid);
}
DebugRequestType::Launch => {
DebugRequestType::Launch(_) => {
debug_panic!("Debugger attach modal used on launch debug config");
return;
}
@@ -214,7 +236,13 @@ impl PickerDelegate for AttachModalDelegate {
let config = self.debug_config.clone();
self.project
.update(cx, |project, cx| project.start_debug_session(config, cx))
.update(cx, |project, cx| {
#[cfg(any(test, feature = "test-support"))]
let ret = project.fake_debug_session(config.request, None, false, cx);
#[cfg(not(any(test, feature = "test-support")))]
let ret = project.start_debug_session(config.into(), cx);
ret
})
.detach_and_log_err(cx);
cx.emit(DismissEvent);
@@ -222,7 +250,6 @@ impl PickerDelegate for AttachModalDelegate {
fn dismissed(&mut self, _window: &mut Window, cx: &mut Context<Picker<Self>>) {
self.selected_index = 0;
self.candidates.take();
cx.emit(DismissEvent);
}
@@ -234,9 +261,8 @@ impl PickerDelegate for AttachModalDelegate {
_window: &mut Window,
_: &mut Context<Picker<Self>>,
) -> Option<Self::ListItem> {
let candidates = self.candidates.as_ref()?;
let hit = &self.matches[ix];
let candidate = &candidates.get(hit.candidate_id)?;
let candidate = self.candidates.get(hit.candidate_id)?;
Some(
ListItem::new(SharedString::from(format!("process-entry-{ix}")))
@@ -279,9 +305,8 @@ impl PickerDelegate for AttachModalDelegate {
}
}
#[allow(dead_code)]
#[cfg(any(test, feature = "test-support"))]
pub(crate) fn process_names(modal: &AttachModal, cx: &mut Context<AttachModal>) -> Vec<String> {
pub(crate) fn _process_names(modal: &AttachModal, cx: &mut Context<AttachModal>) -> Vec<String> {
modal.picker.update(cx, |picker, _| {
picker
.delegate

View File

@@ -3,8 +3,8 @@ use anyhow::{anyhow, Result};
use collections::HashMap;
use command_palette_hooks::CommandPaletteFilter;
use dap::{
client::SessionId, debugger_settings::DebuggerSettings, ContinuedEvent, DebugAdapterConfig,
LoadedSourceEvent, ModuleEvent, OutputEvent, StoppedEvent, ThreadEvent,
client::SessionId, debugger_settings::DebuggerSettings, ContinuedEvent, LoadedSourceEvent,
ModuleEvent, OutputEvent, StoppedEvent, ThreadEvent,
};
use futures::{channel::mpsc, SinkExt as _};
use gpui::{
@@ -19,6 +19,7 @@ use project::{
use rpc::proto::{self};
use settings::Settings;
use std::{any::TypeId, path::PathBuf};
use task::DebugTaskDefinition;
use terminal_view::terminal_panel::TerminalPanel;
use ui::prelude::*;
use util::ResultExt;
@@ -52,7 +53,7 @@ pub struct DebugPanel {
project: WeakEntity<Project>,
workspace: WeakEntity<Workspace>,
_subscriptions: Vec<Subscription>,
pub(crate) last_inert_config: Option<DebugAdapterConfig>,
pub(crate) last_inert_config: Option<DebugTaskDefinition>,
}
impl DebugPanel {

View File

@@ -6,7 +6,6 @@ mod starting;
use std::time::Duration;
use dap::client::SessionId;
use dap::DebugAdapterConfig;
use failed::FailedState;
use gpui::{
percentage, Animation, AnimationExt, AnyElement, App, Entity, EventEmitter, FocusHandle,
@@ -19,6 +18,7 @@ use project::Project;
use rpc::proto::{self, PeerId};
use running::RunningState;
use starting::{StartingEvent, StartingState};
use task::DebugTaskDefinition;
use ui::{prelude::*, Indicator};
use util::ResultExt;
use workspace::{
@@ -73,7 +73,7 @@ impl DebugSession {
project: Entity<Project>,
workspace: WeakEntity<Workspace>,
debug_panel: WeakEntity<DebugPanel>,
config: Option<DebugAdapterConfig>,
config: Option<DebugTaskDefinition>,
window: &mut Window,
cx: &mut App,
) -> Entity<Self> {
@@ -171,7 +171,7 @@ impl DebugSession {
.flatten()
.expect("worktree-less project");
let Ok((new_session_id, task)) = dap_store.update(cx, |store, cx| {
store.new_session(config, &worktree, None, cx)
store.new_session(config.into(), &worktree, None, cx)
}) else {
return;
};

View File

@@ -1,10 +1,10 @@
use std::path::PathBuf;
use dap::{DebugAdapterConfig, DebugAdapterKind, DebugRequestType};
use dap::DebugRequestType;
use editor::{Editor, EditorElement, EditorStyle};
use gpui::{App, AppContext, Entity, EventEmitter, FocusHandle, Focusable, TextStyle, WeakEntity};
use settings::Settings as _;
use task::TCPHost;
use task::{DebugTaskDefinition, LaunchConfig, TCPHost};
use theme::ThemeSettings;
use ui::{
div, h_flex, relative, v_flex, ActiveTheme as _, ButtonCommon, ButtonLike, Clickable, Context,
@@ -35,7 +35,7 @@ impl SpawnMode {
impl From<DebugRequestType> for SpawnMode {
fn from(request: DebugRequestType) -> Self {
match request {
DebugRequestType::Launch => SpawnMode::Launch,
DebugRequestType::Launch(_) => SpawnMode::Launch,
DebugRequestType::Attach(_) => SpawnMode::Attach,
}
}
@@ -55,18 +55,13 @@ impl InertState {
pub(super) fn new(
workspace: WeakEntity<Workspace>,
default_cwd: &str,
debug_config: Option<DebugAdapterConfig>,
debug_config: Option<DebugTaskDefinition>,
window: &mut Window,
cx: &mut Context<Self>,
) -> Self {
let selected_debugger = debug_config.as_ref().and_then(|config| match config.kind {
DebugAdapterKind::Lldb => Some("LLDB".into()),
DebugAdapterKind::Go(_) => Some("Delve".into()),
DebugAdapterKind::Php(_) => Some("PHP".into()),
DebugAdapterKind::Javascript(_) => Some("JavaScript".into()),
DebugAdapterKind::Python(_) => Some("Debugpy".into()),
_ => None,
});
let selected_debugger = debug_config
.as_ref()
.map(|config| SharedString::from(config.adapter.clone()));
let spawn_mode = debug_config
.as_ref()
@@ -75,7 +70,10 @@ impl InertState {
let program = debug_config
.as_ref()
.and_then(|config| config.program.to_owned());
.and_then(|config| match &config.request {
DebugRequestType::Attach(_) => None,
DebugRequestType::Launch(launch_config) => Some(launch_config.program.clone()),
});
let program_editor = cx.new(|cx| {
let mut editor = Editor::single_line(window, cx);
@@ -88,7 +86,10 @@ impl InertState {
});
let cwd = debug_config
.and_then(|config| config.cwd.map(|cwd| cwd.to_owned()))
.and_then(|config| match &config.request {
DebugRequestType::Attach(_) => None,
DebugRequestType::Launch(launch_config) => launch_config.cwd.clone(),
})
.unwrap_or_else(|| PathBuf::from(default_cwd));
let cwd_editor = cx.new(|cx| {
@@ -116,7 +117,7 @@ impl Focusable for InertState {
}
pub(crate) enum InertEvent {
Spawned { config: DebugAdapterConfig },
Spawned { config: DebugTaskDefinition },
}
impl EventEmitter<InertEvent> for InertState {}
@@ -130,6 +131,7 @@ impl Render for InertState {
cx: &mut ui::Context<'_, Self>,
) -> impl ui::IntoElement {
let weak = cx.weak_entity();
let workspace = self.workspace.clone();
let disable_buttons = self.selected_debugger.is_none();
let spawn_button = ButtonLike::new_rounded_left("spawn-debug-session")
.child(Label::new(self.spawn_mode.label()).size(LabelSize::Small))
@@ -137,21 +139,26 @@ impl Render for InertState {
if this.spawn_mode == SpawnMode::Launch {
let program = this.program_editor.read(cx).text(cx);
let cwd = PathBuf::from(this.cwd_editor.read(cx).text(cx));
let kind =
kind_for_label(this.selected_debugger.as_deref().unwrap_or_else(|| {
let kind = this
.selected_debugger
.as_deref()
.unwrap_or_else(|| {
unimplemented!(
"Automatic selection of a debugger based on users project"
)
}));
})
.to_string();
cx.emit(InertEvent::Spawned {
config: DebugAdapterConfig {
config: DebugTaskDefinition {
label: "hard coded".into(),
kind,
request: DebugRequestType::Launch,
program: Some(program),
cwd: Some(cwd),
adapter: kind,
request: DebugRequestType::Launch(LaunchConfig {
program,
cwd: Some(cwd),
}),
tcp_connection: Some(TCPHost::default()),
initialize_args: None,
supports_attach: false,
},
});
} else {
@@ -159,6 +166,7 @@ impl Render for InertState {
}
}))
.disabled(disable_buttons);
v_flex()
.track_focus(&self.focus_handle)
.size_full()
@@ -179,28 +187,36 @@ impl Render for InertState {
.as_ref()
.unwrap_or_else(|| &SELECT_DEBUGGER_LABEL)
.clone(),
ContextMenu::build(window, cx, move |this, _, _| {
let setter_for_name = |name: &'static str| {
ContextMenu::build(window, cx, move |mut this, _, cx| {
let setter_for_name = |name: SharedString| {
let weak = weak.clone();
move |_: &mut Window, cx: &mut App| {
let name = name;
(&weak)
.update(cx, move |this, _| {
this.selected_debugger = Some(name.into());
})
.ok();
let name = name.clone();
weak.update(cx, move |this, cx| {
this.selected_debugger = Some(name.clone());
cx.notify();
})
.ok();
}
};
this.entry("GDB", None, setter_for_name("GDB"))
.entry("Delve", None, setter_for_name("Delve"))
.entry("LLDB", None, setter_for_name("LLDB"))
.entry("PHP", None, setter_for_name("PHP"))
.entry(
"JavaScript",
let available_adapters = workspace
.update(cx, |this, cx| {
this.project()
.read(cx)
.debug_adapters()
.enumerate_adapters()
})
.ok()
.unwrap_or_default();
for adapter in available_adapters {
this = this.entry(
adapter.0.clone(),
None,
setter_for_name("JavaScript"),
)
.entry("Debugpy", None, setter_for_name("Debugpy"))
setter_for_name(adapter.0.clone()),
);
}
this
}),
)),
),
@@ -265,18 +281,6 @@ impl Render for InertState {
}
}
fn kind_for_label(label: &str) -> DebugAdapterKind {
match label {
"LLDB" => DebugAdapterKind::Lldb,
"Debugpy" => DebugAdapterKind::Python(TCPHost::default()),
"JavaScript" => DebugAdapterKind::Javascript(TCPHost::default()),
"PHP" => DebugAdapterKind::Php(TCPHost::default()),
"Delve" => DebugAdapterKind::Go(TCPHost::default()),
_ => {
unimplemented!()
} // Maybe we should set a toast notification here
}
}
impl InertState {
fn render_editor(editor: &Entity<Editor>, cx: &Context<Self>) -> impl IntoElement {
let settings = ThemeSettings::get_global(cx);
@@ -302,19 +306,20 @@ impl InertState {
}
fn attach(&self, window: &mut Window, cx: &mut Context<Self>) {
let cwd = PathBuf::from(self.cwd_editor.read(cx).text(cx));
let kind = kind_for_label(self.selected_debugger.as_deref().unwrap_or_else(|| {
unimplemented!("Automatic selection of a debugger based on users project")
}));
let kind = self
.selected_debugger
.as_deref()
.map(|s| s.to_string())
.unwrap_or_else(|| {
unimplemented!("Automatic selection of a debugger based on users project")
});
let config = DebugAdapterConfig {
let config = DebugTaskDefinition {
label: "hard coded attach".into(),
kind,
adapter: kind,
request: DebugRequestType::Attach(task::AttachConfig { process_id: None }),
program: None,
cwd: Some(cwd),
initialize_args: None,
supports_attach: true,
tcp_connection: Some(TCPHost::default()),
};
let _ = self.workspace.update(cx, |workspace, cx| {

View File

@@ -1,11 +1,11 @@
use crate::*;
use crate::{attach_modal::Candidate, *};
use attach_modal::AttachModal;
use dap::client::SessionId;
use dap::{client::SessionId, FakeAdapter};
use gpui::{BackgroundExecutor, TestAppContext, VisualTestContext};
use menu::Confirm;
use project::{FakeFs, Project};
use serde_json::json;
use task::AttachConfig;
use task::{AttachConfig, DebugTaskDefinition, TCPHost};
use tests::{init_test, init_test_workspace};
#[gpui::test]
@@ -27,14 +27,12 @@ async fn test_direct_attach_to_process(executor: BackgroundExecutor, cx: &mut Te
let cx = &mut VisualTestContext::from_window(*workspace, cx);
let task = project.update(cx, |project, cx| {
project.start_debug_session(
dap::test_config(
dap::DebugRequestType::Attach(AttachConfig {
process_id: Some(10),
}),
None,
None,
),
project.fake_debug_session(
dap::DebugRequestType::Attach(AttachConfig {
process_id: Some(10),
}),
None,
false,
cx,
)
});
@@ -83,13 +81,32 @@ async fn test_show_attach_modal_and_select_process(
let attach_modal = workspace
.update(cx, |workspace, window, cx| {
workspace.toggle_modal(window, cx, |window, cx| {
AttachModal::new(
AttachModal::with_processes(
project.clone(),
dap::test_config(
dap::DebugRequestType::Attach(AttachConfig { process_id: None }),
None,
None,
),
DebugTaskDefinition {
adapter: FakeAdapter::ADAPTER_NAME.into(),
request: dap::DebugRequestType::Attach(AttachConfig::default()),
label: "attach example".into(),
initialize_args: None,
tcp_connection: Some(TCPHost::default()),
},
vec![
Candidate {
pid: 0,
name: "fake-binary-1".into(),
command: vec![],
},
Candidate {
pid: 3,
name: "non-fake-binary-1".into(),
command: vec![],
},
Candidate {
pid: 1,
name: "fake-binary-2".into(),
command: vec![],
},
],
window,
cx,
)
@@ -105,10 +122,10 @@ async fn test_show_attach_modal_and_select_process(
workspace
.update(cx, |_, _, cx| {
let names =
attach_modal.update(cx, |modal, cx| attach_modal::process_names(&modal, cx));
attach_modal.update(cx, |modal, cx| attach_modal::_process_names(&modal, cx));
// we filtered out all processes that are not the current process(zed itself)
assert_eq!(1, names.len());
// we filtered out all processes that are not starting with `fake-binary`
assert_eq!(2, names.len());
})
.unwrap();

View File

@@ -3,6 +3,7 @@ use dap::requests::StackTrace;
use gpui::{BackgroundExecutor, TestAppContext, VisualTestContext};
use project::{FakeFs, Project};
use serde_json::json;
use task::LaunchConfig;
use tests::{init_test, init_test_workspace};
#[gpui::test]
@@ -29,8 +30,10 @@ async fn test_handle_output_event(executor: BackgroundExecutor, cx: &mut TestApp
.unwrap();
let task = project.update(cx, |project, cx| {
project.start_debug_session(
dap::test_config(dap::DebugRequestType::Launch, None, None),
project.fake_debug_session(
dap::DebugRequestType::Launch(LaunchConfig::default()),
None,
false,
cx,
)
});

View File

@@ -5,8 +5,8 @@ use dap::{
Continue, Disconnect, Launch, Next, RunInTerminal, SetBreakpoints, StackTrace,
StartDebugging, StepBack, StepIn, StepOut, Threads,
},
DebugRequestType, ErrorResponse, RunInTerminalRequestArguments, SourceBreakpoint,
StartDebuggingRequestArguments, StartDebuggingRequestArgumentsRequest,
ErrorResponse, RunInTerminalRequestArguments, SourceBreakpoint, StartDebuggingRequestArguments,
StartDebuggingRequestArgumentsRequest,
};
use editor::{
actions::{self},
@@ -25,6 +25,7 @@ use std::{
Arc,
},
};
use task::LaunchConfig;
use terminal_view::{terminal_panel::TerminalPanel, TerminalView};
use tests::{active_debug_session_panel, init_test, init_test_workspace};
use util::path;
@@ -49,7 +50,12 @@ async fn test_basic_show_debug_panel(executor: BackgroundExecutor, cx: &mut Test
let cx = &mut VisualTestContext::from_window(*workspace, cx);
let task = project.update(cx, |project, cx| {
project.start_debug_session(dap::test_config(DebugRequestType::Launch, None, None), cx)
project.fake_debug_session(
dap::DebugRequestType::Launch(LaunchConfig::default()),
None,
false,
cx,
)
});
let session = task.await.unwrap();
@@ -201,7 +207,12 @@ async fn test_we_can_only_have_one_panel_per_debug_session(
let cx = &mut VisualTestContext::from_window(*workspace, cx);
let task = project.update(cx, |project, cx| {
project.start_debug_session(dap::test_config(DebugRequestType::Launch, None, None), cx)
project.fake_debug_session(
dap::DebugRequestType::Launch(LaunchConfig::default()),
None,
false,
cx,
)
});
let session = task.await.unwrap();
@@ -385,7 +396,12 @@ async fn test_handle_successful_run_in_terminal_reverse_request(
let cx = &mut VisualTestContext::from_window(*workspace, cx);
let task = project.update(cx, |project, cx| {
project.start_debug_session(dap::test_config(DebugRequestType::Launch, None, None), cx)
project.fake_debug_session(
dap::DebugRequestType::Launch(LaunchConfig::default()),
None,
false,
cx,
)
});
let session = task.await.unwrap();
@@ -475,7 +491,12 @@ async fn test_handle_error_run_in_terminal_reverse_request(
let cx = &mut VisualTestContext::from_window(*workspace, cx);
let task = project.update(cx, |project, cx| {
project.start_debug_session(dap::test_config(DebugRequestType::Launch, None, None), cx)
project.fake_debug_session(
dap::DebugRequestType::Launch(LaunchConfig::default()),
None,
false,
cx,
)
});
let session = task.await.unwrap();
@@ -555,7 +576,12 @@ async fn test_handle_start_debugging_reverse_request(
let cx = &mut VisualTestContext::from_window(*workspace, cx);
let task = project.update(cx, |project, cx| {
project.start_debug_session(dap::test_config(DebugRequestType::Launch, None, None), cx)
project.fake_debug_session(
dap::DebugRequestType::Launch(LaunchConfig::default()),
None,
false,
cx,
)
});
let session = task.await.unwrap();
@@ -668,7 +694,12 @@ async fn test_shutdown_children_when_parent_session_shutdown(
let cx = &mut VisualTestContext::from_window(*workspace, cx);
let task = project.update(cx, |project, cx| {
project.start_debug_session(dap::test_config(DebugRequestType::Launch, None, None), cx)
project.fake_debug_session(
dap::DebugRequestType::Launch(LaunchConfig::default()),
None,
false,
cx,
)
});
let parent_session = task.await.unwrap();
@@ -776,7 +807,12 @@ async fn test_shutdown_parent_session_if_all_children_are_shutdown(
let cx = &mut VisualTestContext::from_window(*workspace, cx);
let task = project.update(cx, |project, cx| {
project.start_debug_session(dap::test_config(DebugRequestType::Launch, None, None), cx)
project.fake_debug_session(
dap::DebugRequestType::Launch(LaunchConfig::default()),
None,
false,
cx,
)
});
let parent_session = task.await.unwrap();
@@ -891,15 +927,13 @@ async fn test_debug_panel_item_thread_status_reset_on_failure(
let cx = &mut VisualTestContext::from_window(*workspace, cx);
let task = project.update(cx, |project, cx| {
project.start_debug_session(
dap::test_config(
DebugRequestType::Launch,
None,
Some(dap::Capabilities {
supports_step_back: Some(true),
..Default::default()
}),
),
project.fake_debug_session(
dap::DebugRequestType::Launch(LaunchConfig::default()),
Some(dap::Capabilities {
supports_step_back: Some(true),
..Default::default()
}),
false,
cx,
)
});
@@ -1122,7 +1156,12 @@ async fn test_send_breakpoints_when_editor_has_been_saved(
.unwrap();
let task = project.update(cx, |project, cx| {
project.start_debug_session(dap::test_config(DebugRequestType::Launch, None, None), cx)
project.fake_debug_session(
dap::DebugRequestType::Launch(LaunchConfig::default()),
None,
false,
cx,
)
});
let session = task.await.unwrap();
@@ -1347,7 +1386,12 @@ async fn test_unsetting_breakpoints_on_clear_breakpoint_action(
});
let task = project.update(cx, |project, cx| {
project.start_debug_session(dap::test_config(DebugRequestType::Launch, None, None), cx)
project.fake_debug_session(
dap::DebugRequestType::Launch(LaunchConfig::default()),
None,
false,
cx,
)
});
let session = task.await.unwrap();
@@ -1419,8 +1463,10 @@ async fn test_debug_session_is_shutdown_when_attach_and_launch_request_fails(
let cx = &mut VisualTestContext::from_window(*workspace, cx);
let task = project.update(cx, |project, cx| {
project.start_debug_session(
dap::test_config(DebugRequestType::Launch, Some(true), None),
project.fake_debug_session(
dap::DebugRequestType::Launch(LaunchConfig::default()),
None,
true,
cx,
)
});

View File

@@ -5,7 +5,7 @@ use crate::{
};
use dap::{
requests::{Modules, StackTrace, Threads},
DebugRequestType, StoppedEvent,
StoppedEvent,
};
use gpui::{BackgroundExecutor, TestAppContext, VisualTestContext};
use project::{FakeFs, Project};
@@ -13,6 +13,7 @@ use std::sync::{
atomic::{AtomicBool, AtomicI32, Ordering},
Arc,
};
use task::LaunchConfig;
#[gpui::test]
async fn test_module_list(executor: BackgroundExecutor, cx: &mut TestAppContext) {
@@ -30,15 +31,13 @@ async fn test_module_list(executor: BackgroundExecutor, cx: &mut TestAppContext)
let cx = &mut VisualTestContext::from_window(*workspace, cx);
let task = project.update(cx, |project, cx| {
project.start_debug_session(
dap::test_config(
DebugRequestType::Launch,
None,
Some(dap::Capabilities {
supports_modules_request: Some(true),
..Default::default()
}),
),
project.fake_debug_session(
dap::DebugRequestType::Launch(LaunchConfig::default()),
Some(dap::Capabilities {
supports_modules_request: Some(true),
..Default::default()
}),
false,
cx,
)
});

View File

@@ -12,6 +12,7 @@ use gpui::{BackgroundExecutor, TestAppContext, VisualTestContext};
use project::{FakeFs, Project};
use serde_json::json;
use std::sync::Arc;
use task::LaunchConfig;
use unindent::Unindent as _;
use util::path;
@@ -52,8 +53,10 @@ async fn test_fetch_initial_stack_frames_and_go_to_stack_frame(
let cx = &mut VisualTestContext::from_window(*workspace, cx);
let task = project.update(cx, |project, cx| {
project.start_debug_session(
dap::test_config(dap::DebugRequestType::Launch, None, None),
project.fake_debug_session(
dap::DebugRequestType::Launch(LaunchConfig::default()),
None,
false,
cx,
)
});
@@ -240,8 +243,10 @@ async fn test_select_stack_frame(executor: BackgroundExecutor, cx: &mut TestAppC
let cx = &mut VisualTestContext::from_window(*workspace, cx);
let task = project.update(cx, |project, cx| {
project.start_debug_session(
dap::test_config(dap::DebugRequestType::Launch, None, None),
project.fake_debug_session(
dap::DebugRequestType::Launch(LaunchConfig::default()),
None,
false,
cx,
)
});
@@ -513,8 +518,10 @@ async fn test_collapsed_entries(executor: BackgroundExecutor, cx: &mut TestAppCo
let cx = &mut VisualTestContext::from_window(*workspace, cx);
let task = project.update(cx, |project, cx| {
project.start_debug_session(
dap::test_config(dap::DebugRequestType::Launch, None, None),
project.fake_debug_session(
dap::DebugRequestType::Launch(LaunchConfig::default()),
None,
false,
cx,
)
});

View File

@@ -17,6 +17,7 @@ use gpui::{BackgroundExecutor, TestAppContext, VisualTestContext};
use menu::{SelectFirst, SelectNext, SelectPrevious};
use project::{FakeFs, Project};
use serde_json::json;
use task::LaunchConfig;
use unindent::Unindent as _;
use util::path;
@@ -56,8 +57,10 @@ async fn test_basic_fetch_initial_scope_and_variables(
let cx = &mut VisualTestContext::from_window(*workspace, cx);
let task = project.update(cx, |project, cx| {
project.start_debug_session(
dap::test_config(dap::DebugRequestType::Launch, None, None),
project.fake_debug_session(
dap::DebugRequestType::Launch(LaunchConfig::default()),
None,
false,
cx,
)
});
@@ -283,8 +286,10 @@ async fn test_fetch_variables_for_multiple_scopes(
let cx = &mut VisualTestContext::from_window(*workspace, cx);
let task = project.update(cx, |project, cx| {
project.start_debug_session(
dap::test_config(dap::DebugRequestType::Launch, None, None),
project.fake_debug_session(
dap::DebugRequestType::Launch(LaunchConfig::default()),
None,
false,
cx,
)
});
@@ -562,8 +567,10 @@ async fn test_keyboard_navigation(executor: BackgroundExecutor, cx: &mut TestApp
let cx = &mut VisualTestContext::from_window(*workspace, cx);
let task = project.update(cx, |project, cx| {
project.start_debug_session(
dap::test_config(dap::DebugRequestType::Launch, None, None),
project.fake_debug_session(
dap::DebugRequestType::Launch(LaunchConfig::default()),
None,
false,
cx,
)
});
@@ -1362,8 +1369,10 @@ async fn test_variable_list_only_sends_requests_when_rendering(
let cx = &mut VisualTestContext::from_window(*workspace, cx);
let task = project.update(cx, |project, cx| {
project.start_debug_session(
dap::test_config(dap::DebugRequestType::Launch, None, None),
project.fake_debug_session(
dap::DebugRequestType::Launch(LaunchConfig::default()),
None,
false,
cx,
)
});
@@ -1639,8 +1648,10 @@ async fn test_it_fetches_scopes_variables_when_you_select_a_stack_frame(
let cx = &mut VisualTestContext::from_window(*workspace, cx);
let task = project.update(cx, |project, cx| {
project.start_debug_session(
dap::test_config(dap::DebugRequestType::Launch, None, None),
project.fake_debug_session(
dap::DebugRequestType::Launch(LaunchConfig::default()),
None,
false,
cx,
)
});

View File

@@ -20,8 +20,8 @@ use crate::hover_popover::hover_markdown_style;
#[derive(Clone, Debug)]
pub struct CommitDetails {
pub sha: SharedString,
pub committer_name: SharedString,
pub committer_email: SharedString,
pub author_name: SharedString,
pub author_email: SharedString,
pub commit_time: OffsetDateTime,
pub message: Option<ParsedCommitMessage>,
}
@@ -133,16 +133,12 @@ impl CommitTooltip {
CommitDetails {
sha: blame.sha.to_string().into(),
commit_time,
committer_name: blame
.committer_name
author_name: blame
.author
.clone()
.unwrap_or("<no name>".to_string())
.into(),
committer_email: blame
.committer_email
.clone()
.unwrap_or("".to_string())
.into(),
author_email: blame.author_mail.clone().unwrap_or("".to_string()).into(),
message: details,
},
window,
@@ -180,9 +176,9 @@ impl Render for CommitTooltip {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let avatar = CommitAvatar::new(&self.commit).render(window, cx);
let author = self.commit.committer_name.clone();
let author = self.commit.author_name.clone();
let author_email = self.commit.committer_email.clone();
let author_email = self.commit.author_email.clone();
let short_commit_id = self
.commit

View File

@@ -4320,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 {
@@ -4368,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)
};
@@ -6117,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);
@@ -18031,6 +18013,10 @@ pub trait CompletionProvider {
fn sort_completions(&self) -> bool {
true
}
fn filter_completions(&self) -> bool {
true
}
}
pub trait CodeActionProvider {

View File

@@ -1338,7 +1338,8 @@ impl EditorElement {
fn layout_scrollbars(
&self,
snapshot: &EditorSnapshot,
scrollbar_range_data: ScrollbarLayoutInformation,
scrollbar_layout_information: ScrollbarLayoutInformation,
content_offset: gpui::Point<Pixels>,
scroll_position: gpui::Point<f32>,
non_visible_cursors: bool,
window: &mut Window,
@@ -1390,7 +1391,8 @@ impl EditorElement {
Some(EditorScrollbars::from_scrollbar_axes(
scrollbar_settings.axes,
&scrollbar_range_data,
&scrollbar_layout_information,
content_offset,
scroll_position,
self.style.scrollbar_width,
show_scrollbars,
@@ -1955,7 +1957,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;
}
@@ -6608,13 +6615,13 @@ impl Element for EditorElement {
// Offset the content_bounds from the text_bounds by the gutter margin (which
// is roughly half a character wide) to make hit testing work more like how we want.
let content_origin =
text_hitbox.origin + point(gutter_dimensions.margin, Pixels::ZERO);
let content_offset = point(gutter_dimensions.margin, Pixels::ZERO);
let content_origin = text_hitbox.origin + content_offset;
let scrollbar_bounds =
let editor_text_bounds =
Bounds::from_corners(content_origin, bounds.bottom_right());
let height_in_lines = scrollbar_bounds.size.height / line_height;
let height_in_lines = editor_text_bounds.size.height / line_height;
let max_row = snapshot.max_point().row().as_f32();
@@ -6942,7 +6949,7 @@ impl Element for EditorElement {
.width;
let scrollbar_layout_information = ScrollbarLayoutInformation::new(
scrollbar_bounds,
text_hitbox.bounds,
glyph_grid_cell,
size(longest_line_width, max_row.as_f32() * line_height),
longest_line_blame_width,
@@ -7014,7 +7021,7 @@ impl Element for EditorElement {
MultiBufferRow(end_anchor.to_point(&snapshot.buffer_snapshot).row);
let scroll_max = point(
((scroll_width - scrollbar_bounds.size.width) / em_width).max(0.0),
((scroll_width - editor_text_bounds.size.width) / em_width).max(0.0),
max_scroll_top,
);
@@ -7220,6 +7227,7 @@ impl Element for EditorElement {
let scrollbars_layout = self.layout_scrollbars(
&snapshot,
scrollbar_layout_information,
content_offset,
scroll_position,
non_visible_cursors,
window,
@@ -7589,8 +7597,8 @@ pub(super) fn gutter_bounds(
/// Holds information required for layouting the editor scrollbars.
struct ScrollbarLayoutInformation {
/// The bounds of the editor text area.
editor_text_bounds: Bounds<Pixels>,
/// The bounds of the editor area (excluding the content offset).
editor_bounds: Bounds<Pixels>,
/// The available range to scroll within the document.
scroll_range: Size<Pixels>,
/// The space available for one glyph in the editor.
@@ -7599,7 +7607,7 @@ struct ScrollbarLayoutInformation {
impl ScrollbarLayoutInformation {
pub fn new(
scrollbar_bounds: Bounds<Pixels>,
editor_bounds: Bounds<Pixels>,
glyph_grid_cell: Size<Pixels>,
document_size: Size<Pixels>,
longest_line_blame_width: Pixels,
@@ -7608,7 +7616,7 @@ impl ScrollbarLayoutInformation {
settings: &EditorSettings,
) -> Self {
let vertical_overscroll = match settings.scroll_beyond_last_line {
ScrollBeyondLastLine::OnePage => scrollbar_bounds.size.height,
ScrollBeyondLastLine::OnePage => editor_bounds.size.height,
ScrollBeyondLastLine::Off => glyph_grid_cell.height,
ScrollBeyondLastLine::VerticalScrollMargin => {
(1.0 + settings.vertical_scroll_margin) * glyph_grid_cell.height
@@ -7626,7 +7634,7 @@ impl ScrollbarLayoutInformation {
let scroll_range = document_size + overscroll;
ScrollbarLayoutInformation {
editor_text_bounds: scrollbar_bounds,
editor_bounds,
scroll_range,
glyph_grid_cell,
}
@@ -7732,13 +7740,14 @@ impl EditorScrollbars {
pub fn from_scrollbar_axes(
settings_visibility: ScrollbarAxes,
layout_information: &ScrollbarLayoutInformation,
content_offset: gpui::Point<Pixels>,
scroll_position: gpui::Point<f32>,
scrollbar_width: Pixels,
show_scrollbars: bool,
window: &mut Window,
) -> Self {
let ScrollbarLayoutInformation {
editor_text_bounds,
editor_bounds,
scroll_range,
glyph_grid_cell,
} = layout_information;
@@ -7746,20 +7755,20 @@ impl EditorScrollbars {
let scrollbar_bounds_for = |axis: ScrollbarAxis| match axis {
ScrollbarAxis::Horizontal => Bounds::from_corner_and_size(
Corner::BottomLeft,
editor_text_bounds.bottom_left(),
editor_bounds.bottom_left(),
size(
if settings_visibility.vertical {
editor_text_bounds.size.width - scrollbar_width
editor_bounds.size.width - scrollbar_width
} else {
editor_text_bounds.size.width
editor_bounds.size.width
},
scrollbar_width,
),
),
ScrollbarAxis::Vertical => Bounds::from_corner_and_size(
Corner::TopRight,
editor_text_bounds.top_right(),
size(scrollbar_width, editor_text_bounds.size.height),
editor_bounds.top_right(),
size(scrollbar_width, editor_bounds.size.height),
),
};
@@ -7768,23 +7777,24 @@ impl EditorScrollbars {
.along(axis)
.then(|| {
(
editor_text_bounds.size.along(axis),
editor_bounds.size.along(axis) - content_offset.along(axis),
scroll_range.along(axis),
)
})
.filter(|(editor_size, scroll_range)| {
.filter(|(editor_content_size, scroll_range)| {
// The scrollbar should only be rendered if the content does
// not entirely fit into the editor
// However, this only applies to the horizontal scrollbar, as information about the
// vertical scrollbar layout is always needed for scrollbar diagnostics.
axis != ScrollbarAxis::Horizontal || editor_size < scroll_range
axis != ScrollbarAxis::Horizontal || editor_content_size < scroll_range
})
.map(|(editor_size, scroll_range)| {
.map(|(editor_content_size, scroll_range)| {
ScrollbarLayout::new(
window.insert_hitbox(scrollbar_bounds_for(axis), false),
editor_size,
editor_content_size,
scroll_range,
glyph_grid_cell.along(axis),
content_offset.along(axis),
scroll_position.along(axis),
axis,
)
@@ -7819,6 +7829,7 @@ struct ScrollbarLayout {
hitbox: Hitbox,
visible_range: Range<f32>,
text_unit_size: Pixels,
content_offset: Pixels,
thumb_size: Pixels,
axis: ScrollbarAxis,
}
@@ -7831,30 +7842,34 @@ impl ScrollbarLayout {
fn new(
scrollbar_track_hitbox: Hitbox,
editor_size: Pixels,
editor_content_size: Pixels,
scroll_range: Pixels,
glyph_space: Pixels,
content_offset: Pixels,
scroll_position: f32,
axis: ScrollbarAxis,
) -> Self {
let scrollbar_track_bounds = scrollbar_track_hitbox.bounds;
let scrollbar_track_length = scrollbar_track_bounds.size.along(axis);
let track_bounds = scrollbar_track_hitbox.bounds;
// The length of the track available to the scrollbar thumb. We deliberately
// exclude the content size here so that the thumb aligns with the content.
let track_length = track_bounds.size.along(axis) - content_offset;
let text_units_per_page = editor_size / glyph_space;
let text_units_per_page = editor_content_size / glyph_space;
let visible_range = scroll_position..scroll_position + text_units_per_page;
let total_text_units = scroll_range / glyph_space;
let thumb_percentage = text_units_per_page / total_text_units;
let thumb_size = (scrollbar_track_length * thumb_percentage)
let thumb_size = (track_length * thumb_percentage)
.max(ScrollbarLayout::MIN_THUMB_SIZE)
.min(scrollbar_track_length);
let text_unit_size = (scrollbar_track_length - thumb_size)
/ (total_text_units - text_units_per_page).max(0.);
.min(track_length);
let text_unit_size =
(track_length - thumb_size) / (total_text_units - text_units_per_page).max(0.);
ScrollbarLayout {
hitbox: scrollbar_track_hitbox,
visible_range,
text_unit_size,
content_offset,
thumb_size,
axis,
}
@@ -7873,7 +7888,7 @@ impl ScrollbarLayout {
}
fn thumb_origin(&self, origin: Pixels) -> Pixels {
origin + self.visible_range.start * self.text_unit_size
origin + self.content_offset + self.visible_range.start * self.text_unit_size
}
fn marker_quads_for_ranges(

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