Compare commits

..

116 Commits

Author SHA1 Message Date
MrSubidubi
ea7a36a00e To clone or not to clone 2025-12-08 15:07:24 +01:00
MrSubidubi
014c49df4f extension_api: Update default trait implementations for language server settings 2025-12-05 00:33:42 +01:00
Danilo Leal
43f977c6b9 terminal view: Use tooltip element for the tab tooltip (#44169)
Just recently realized we don't need this custom component for it given
we now have `Tooltip::element`. UI result is exactly the same; nothing
changes.

Release Notes:

- N/A
2025-12-04 16:48:03 -03:00
Danilo Leal
bdb8caa42e git_ui: Fix indent guides not showing for file buffers in the commit view (#44166)
Follow up to https://github.com/zed-industries/zed/pull/44162 where my
strategy for not displaying the indent guides only in the commit message
was wrong given I ended up... disabling indent guides for all the
buffers. This PR adds a new method to the editor where we can disable it
for a specific buffer ID following the pattern of
`disable_header_for_buffer`.

Release Notes:

- N/A
2025-12-04 16:47:27 -03:00
vipex
9ae77ec3c9 markdown: Don't adjust indentation when inserting with multiple cursors (#40794)
Closes #40757

## Summary

This PR addresses an issue where Zed incorrectly adjusts the indentation
of Markdown lists when inserting text using multiple cursors. Currently:

- Editing individual lines with a single cursor behaves correctly (no
unwanted indentation changes).
- Using multiple cursors, Zed automatically adjusts the indentation,
unlike VS Code, which preserves the existing formatting.

## Tasks
- [x] Implement a new test to verify correct Markdown indentation
behavior with multiple cursors.
- [x] Apply the fix to prevent Zed from adjusting indentation when
inserting text on multiple cursors.

------------------------

Release Notes:

- Fixed an issue where inserting text with multiple cursors inside a
nested Markdown list would cause it to lose its indentation.

---------

Co-authored-by: Smit Barmase <heysmitbarmase@gmail.com>
2025-12-05 00:18:06 +05:30
Cole Miller
d5ed9d3e3a git: Don't call git2::Repository::find_remote for every blamed buffer (#44107)
We already store the remote URLs for `origin` and `upstream` in the
`RepositorySnapshot`, so just use that data. Follow-up to #44092.

Release Notes:

- N/A
2025-12-04 13:25:30 -05:00
Liffindra Angga Zaaldian
74a1b5d14d Update PHP language server docs (#44001)
Reformat document structure like other language docs, improve
information flow, add missing requirements, and fix typos.

Release Notes:

- N/A

---------

Co-authored-by: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com>
2025-12-04 19:04:06 +01:00
Lukas Wirth
07af011eb4 worktree: Fix git ignored directories dropping their contents when they are refreshed (#44143)
Closes https://github.com/zed-industries/zed/issues/38653

Release Notes:

- Fixed git ignored directories appearing as empty when their content
changes on windows

Co-authored by: Smit Barmase <smit@zed.dev>
2025-12-04 18:14:10 +01:00
Danilo Leal
c357dc25fc git_ui: Clean up the commit view UI (#44162) 2025-12-04 13:44:48 -03:00
Lukas Wirth
93bc6616c6 editor: Improve performance of update_visible_edit_prediction (#44161)
One half of https://github.com/zed-industries/zed/issues/42861

This basically reduces the main thread work for large enough json (and
other) files from multiple milliseconds (15ms was observed in that test
case) down to microseconds (100ms here).

Release Notes:

- Improved cursor movement performance when edit predictions are enabled
2025-12-04 15:41:48 +00:00
Lukas Wirth
a33e881906 remote: Recognize WSL interop to open browser for codex web login (#44136)
Closes #41521

Release Notes:

- Fixed codex web login not working on wsl remotes if no browser is
installed

Co-authored-by: Ben Brandt <benjamin.j.brandt@gmail.com>
2025-12-04 14:42:26 +00:00
Agus Zubiaga
c978db8626 Fix background scanner deadlock (#44109)
Fixes a deadlock in the background scanner that occurs on single-core
Linux devices. This happens because the background scanner would `block`
on a background thread waiting for a future, but on single-core Linux
devices there would be no other thread to pick it up. This mostly
affects SSH remoting use cases where it's common for servers to have 1
vCPU.

Closes #43884 
Closes #43809

Release Notes:

- Fix SSH remoting hang when connecting to 1 vCPU servers
2025-12-04 11:30:16 -03:00
Rawand Ahmed Shaswar
2dad46c5c0 gpui: Fix division by zero when chars/sec = 0 on Wayland (#44151)
Closes #44148

the existing rate == 0 check inside the timer callback already handles
disabling repeat - it just drops the timer immediately. So the fix
prevents the crash while preserving correct behavior. 

Release Notes:

- Linux (Wayland): Fixed a crash that could occur when
`characters_per_second` was zero
2025-12-04 11:26:17 -03:00
Coenen Benjamin
4c51fffbb5 Add support for git remotes (#42819)
Follow up of #42486 
Closes #26559



https://github.com/user-attachments/assets/e2f54dda-a78b-4d9b-a910-16d51f98a111



Release Notes:

- Added support for git remotes

---------

Signed-off-by: Benjamin <5719034+bnjjj@users.noreply.github.com>
2025-12-04 14:23:36 +01:00
Piotr Osiewicz
0d80b452fb python: Improve sorting order of toolchains to give higher precedence to project-local virtual environments that are within current subproject (#44141)
Closes #44090

Co-authored-by: Smit Barmase <heysmitbarmase@gmail.com>

Release Notes:

- python: Improved sorting order of toolchains in monorepos with
multiple local virtual environments.
- python: Fixed toolchain selector not having an active toolchain
selected on open.

---------

Co-authored-by: Smit Barmase <heysmitbarmase@gmail.com>
Co-authored-by: Smit <smit@zed.dev>
2025-12-04 12:33:13 +00:00
John Gibb
bad6bde03a Use buffer language when formatting with Prettier (#43368)
Set `prettier_parser` explicitly if the file extension for the buffer
does not match a known one for the current language

Release Notes:

- N/A

---------

Co-authored-by: Kirill Bulatov <kirill@zed.dev>
2025-12-04 12:07:40 +00:00
Piotr Osiewicz
4ec2d04ad9 search: Fix sort order not being maintained in presence of open buffers (#44135)
In project search UI code we were seeing an issue where "Go to next
match" would act up and behave weirdly. It would not wrap at times.
Stuff would be weird, yo. It turned out that match ranges reported by
core project search were sometimes out of sync with the state of the
multi-buffer. As in, the sort order of
`search::ProjectSearch::match_ranges` would not match up with
multi-buffer's sort order. This is ~because multi-buffers maintain their
own sort order.

What happened within project search is that we were skipping straight
from stage 1 (filtering paths) to stage 3 via an internal channel and in
the process we've dropped the channel used to maintain result sorting.
This made is so that, given 2 files to scan:
- project/file1.rs <- not open, has to go through stage2 (FS scan)
- project/file2.rs <- open, goes straight from stage1 (path filtering)
  to stage3 (finding all matches) We would report matches for
  project/file2.rs first, because we would notice that there's an
  existing language::Buffer for it. However, we should wait for
  project/file1.rs status to be reported first before we kick off
  project/file2.rs

The fix is to use the sorting channel instead of an internal one, as
that keeps the sorting worker "in the loop" about the state of the
world.

Closes #43672

Co-authored-by: Smit Barmase <heysmitbarmase@gmail.com>

Release Notes:

- Fixed "Select next match" in project search results misbehaving when
some of the buffers within the search results were open before search
was ran.
- Fixed project search results being scrolled to the last file active
prior to running the search.

---------

Co-authored-by: Smit Barmase <heysmitbarmase@gmail.com>
Co-authored-by: Smit <smit@zed.dev>
2025-12-04 12:21:02 +01:00
Shardul Vaidya
0f0017dc8e bedrock: Support global endpoints and new regional endpoints (#44103)
Closes #43598

Release Notes:

- bedrock: Added opt-in `allow_global` which enables global endpoints
- bedrock: Updated cross-region-inference endpoint and model list
- bedrock: Fixed Opus 4.5 access on Bedrock, now only accessible through the `allow_global` setting
2025-12-04 12:14:31 +01:00
Agus Zubiaga
9db0d66251 linux: Spawn at least two background threads (#44110)
Related to https://github.com/zed-industries/zed/pull/44109,
https://github.com/zed-industries/zed/issues/43884,
https://github.com/zed-industries/zed/issues/43809.

In the Linux dispatcher, we create one background thread per CPU, but
when a single core is available, having a single background thread
significantly hinders the perceived performance of Zed. This is
particularly helpful when SSH remoting to low-resource servers.

We may want to bump this to more than two threads actually, but I wanted
to be conservative, and this seems to make a big difference already.

Release Notes:

- N/A
2025-12-04 10:40:51 +00:00
Aero
b07389d9f3 macos: Add missing file access entitlements (#43609)
Adds `com.apple.security.files.user-selected.read-write` and
`com.apple.security.files.downloads.read-write` to zed.entitlements.

This resolves an issue where the integrated terminal could not access
external drives or user-selected files on macOS, even when "Full Disk
Access" was granted. These entitlements are required for the application
to properly inherit file access permissions.

Release Notes:

- Resolves an issue where the integrated terminal could not access
external drives or user-selected files on macOS.
2025-12-04 12:38:10 +02:00
Kirill Bulatov
db2e26f67b Re-colorize the brackets when the theme changes (#44130)
Closes https://github.com/zed-industries/zed/issues/44127

Release Notes:

- Fixed brackets not re-colorizing on theme change
2025-12-04 10:21:37 +00:00
John Tur
391c92b07a Reduce priority of Windows thread pool work items (#44121)
`WorkItemPriority::High` will enqueue the work items to threads with
higher-than-normal priority. If the work items are very intensive, this
can cause the system to become unresponsive. It's not clear what this
gets us, so let's avoid the responsiveness issue by deleting this.

Release Notes:

- N/A
2025-12-04 07:45:36 +00:00
Conrad Irwin
1e4d80a21f Update fancy-regex (#44120)
Fancy regex has a max backtracking limit which defaults to 1,000,000
backtracks. This avoids spinning the CPU forever in the case that a
match is taking a long time (though does mean that some matches may be
missed).

Unfortunately the verison we depended on causes an infinite loop when
the backtracking limit is hit
(https://github.com/fancy-regex/fancy-regex/issues/137), so we got the
worse of both worlds: matches were missed *and* we spun the CPU forever.

Updating fixes this.

Excitingly regex may gain support for lookarounds
(https://github.com/rust-lang/regex/pull/1315), which will make
fancy-regex much less load bearing.

Closes #43821

Release Notes:

- Fix a bug where search regexes with look-around or backreferences
could hang
  the CPU. They will now abort after a certain number of match attempts.
2025-12-04 07:29:31 +00:00
Joseph T. Lyons
f90d9d26a5 Prefer to disable options over hiding (git panel entry context menu) (#44102)
When adding the File History option here, I used the pattern to hide the
option, since that's what another option was already doing here, but I
see other menus in the git panel (`...`) that use disabling over hiding,
which is what I think is a nicer experience (allows you to learn of
actions, the full range of actions is always visible, don't have to
worry about how multiple hidden items might interact in various
configurations, etc).

<img width="336" height="241" alt="SCR-20251203-pnpy"
src="https://github.com/user-attachments/assets/0da90b9a-c230-4ce3-87b9-553ffb83604f"
/>

<img width="332" height="265" alt="SCR-20251203-pobg"
src="https://github.com/user-attachments/assets/5da95c7d-faa9-4f0f-a069-f1d099f952b9"
/>


In general, I think it would be good to move to being more consistent
with disabling over hiding - there are other places in the app that are
hiding - some might be valid, but others might just choices made on a
whim.

Release Notes:

- N/A
2025-12-03 22:56:51 +00:00
Andrew Farkas
40a611bf34 tab_switcher: Subscribe to workspace events instead of pane events (#44101)
Closes #43171

Previously the tab switcher only subscribed to events from a single pane
so closing tabs in other panes wouldn't cause the tab switcher to
update. This PR changes that so the tab switcher subscribes to the whole
workspace and thus updates when tabs in other panes are closed.

It also modifies the work in #44006 to sync selected index across the
whole workspace instead of just the original pane in the case of the
all-panes tab switcher.

Release Notes:

- Fixed all-panes tab switcher not updating in response to changes in
other panes
2025-12-03 22:49:44 +00:00
Smit Barmase
8ad3a150c8 editor: Add active match highlight for buffer and project search (#44098)
Closes #28617

<img width="400" alt="image"
src="https://github.com/user-attachments/assets/b1c2880c-5744-4bed-a687-5c5e7aa7fef5"
/>

Release Notes:

- Improved visibility of the currently active match when browsing
results in buffer or project search.

---------

Co-authored-by: DarkMatter-999 <darkmatter999official@gmail.com>
2025-12-04 03:55:04 +05:30
Andrew Farkas
87976e91cf Add more preview tab settings and fix janky behavior (#43921)
Closes #41495

Known issues:
- File path links always open as non-preview tabs. Fixing this is not
technically too difficult but requires more invasive changes and so
should be done in a future PR.

Release Notes:

- Fixed strange behavior when reopening closed preview tabs
- Overhauled preview tabs settings:
- Added setting `preview_tabs.enable_preview_from_project_panel`
(default `true`)
- Kept setting `preview_tabs.enable_preview_from_file_finder` (default
`false`)
- Added setting `preview_tabs.enable_preview_from_multibuffer` (default
`true`)
- Added setting
`preview_tabs.enable_preview_multibuffer_from_code_navigation` (default
`false`)
- Added setting `preview_tabs.enable_preview_file_from_code_navigation`
(default `true`)
- Renamed setting `preview_tabs.enable_preview_from_code_navigation` to
`preview_tabs.enable_keep_preview_on_code_navigation` (default `false`)

---------

Co-authored-by: Smit Barmase <heysmitbarmase@gmail.com>
Co-authored-by: Cole Miller <cole@zed.dev>
2025-12-03 21:56:39 +00:00
Michael Benfield
290a1550aa ai: Add an eval for the inline assistant (#43291)
Release Notes:

- N/A

---------

Co-authored-by: Mikayla Maki <mikayla.c.maki@gmail.com>
2025-12-03 20:32:25 +00:00
feeiyu
92dcfdef76 Fix circular reference issue around PopoverMenu again (#44084)
Follow up to https://github.com/zed-industries/zed/pull/42351

Release Notes:

- N/A
2025-12-03 21:34:01 +02:00
Cole Miller
4ef8433396 Run git2::Repository::find_remote in the background (#44092)
We were seeing this hog the main thread.

Release Notes:

- N/A

---------

Co-authored-by: cameron <cameron.studdstreet@gmail.com>
2025-12-03 19:33:40 +00:00
John Tur
a51e975b81 Improve support for multiple registrations of textDocument/diagnostic (#43703)
Closes https://github.com/zed-industries/zed/issues/41935

The registration ID responsible for generating each diagnostic is now
tracked. This allows us to replace only the diagnostics from the same
registration ID when a pull diagnostics report is applied.

Additionally, various deficiencies in our support for pull diagnostics
have been fixed:
- Document pulls are issued for all open buffers, not just the edited
one. A shorter debounce is used for the edited buffer. Workspace
diagnostics are also now ignored for open buffers.
- Tracking of `lastResultId` is improved.
- Stored pull diagnostics are discarded when the corresponding buffer is
closed.

Release Notes:

- Improved compatibility with language servers that use the "pull
diagnostics" feature of Language Server Protocol.

---------

Co-authored-by: Kirill Bulatov <mail4score@gmail.com>
Co-authored-by: Kirill Bulatov <kirill@zed.dev>
2025-12-03 20:47:43 +02:00
Conrad Irwin
493cfadb42 Revert "http_client: Add integrity checks for GitHub binaries using digest checks (#43737)" (#44086)
This reverts commit 05764e8af7.

Internally we've seen a much higher incidence of macOS code-signing
failing on
the download rust analyzer than we did before this change.

It's unclear why this would be a problem, but we want to try reverting
to see if that fixes it.

Release Notes:

- Reverted a change that seemed to cause problems with code-signing on
rust-analyzer
2025-12-03 11:40:47 -07:00
Mayank Verma
0818cedded editor: Fix blame hover not working when inline git blame is disabled (#42992)
Closes #42936

Release Notes:

- Fixed editor blame hover not working when inline git blame is disabled

Here's the before/after:


https://github.com/user-attachments/assets/a3875011-4a27-45b3-b638-3e146c06f1fe
2025-12-03 12:25:31 -05:00
Dino
6b46a71dd0 tab_switcher: Fix bug where selected index after closing tab did not match pane's active item (#44006)
Whenever an item is removed using the Tab Switcher, the list of matches
is automatically updated, which can lead to the order of the elements
being updated and changing in comparison to what the user was previously
seeing. Unfortunately this can lead to a situation where the selected
index, since it wasn't being updated, would end up in a different item
than the one that was actually active in the pane.

This Pull Request updates the handling of the `PaneEvent::RemovedItem`
event so that the `TabSwitcherDelegate.selected_index` field is
automatically updated to match the pane's new active item.

Seeing as this is being updated, the
`test_close_preserves_selected_position` test is also removed, as it no
longer makes sense with the current implementation. I believe a better
user experience would be to actually not update the order of the
matches, simply removing the ones that no longer exist, and keep the
selected index position, but will tackle that in a different Pull
Request.

Closes #44005 

Release Notes:

- Fixed a bug with the tab switcher where, after closing a tab, the
selected entry would not match the pane's active item
2025-12-03 17:12:04 +00:00
Ramon
575ea49aad Fix yank around paragraph missing newline (#43583)
Use `MotionKind::LineWise` in both
`vim::normal::change::Vim.change_object` and
`vim::normal::yank::Vim.yank_object` when dealing with objects that
target `Mode::VisualLine`, for example, paragraphs. This fixes an issue
where yanking and changing paragraphs would not include the trailing
newline character.

Closes #28804

Release Notes:

- Fixed linewise text object operations (`yap`, `cap`, etc.) omitting
trailing blank line in vim mode

---------

Co-authored-by: dino <dinojoaocosta@gmail.com>
2025-12-03 17:11:53 +00:00
Xipeng Jin
85ccd7c98b Fix not able to navigate to files in git commit multibuffer (#42558)
Closes #40851

Release Notes:

- Fixed: Commit diff multibuffers now open real project files whenever
possible, restoring navigation and annotations inside those excerpts.

---------

Co-authored-by: Anthony Eid <anthony@zed.dev>
2025-12-03 11:59:56 -05:00
Vitaly Slobodin
b168679c18 language: Remove old unused HTML/ERB language ID (#44081)
The `HTML/ERB` language was renamed
to `HTML+ERB` in https://github.com/zed-industries/zed/pull/40000 We can
remove the old name safely now.

Release Notes:

- N/A
2025-12-03 17:34:49 +01:00
Jeff Brennan
621ac16e35 go: Fix language injections (#43775)
Closes #43730


## Summary
This modifies the existing injections.scm file for go by adding more
specific prefix queries and *_content nodes to the existing
`raw_string_literal` and `interpreted_string_literal` sections

<details><summary>This PR</summary>

<img width="567" height="784" alt="image"
src="https://github.com/user-attachments/assets/bfe8c64e-1dc2-470c-9f85-2c664a6c5a15"
/>
<img width="383" height="909" alt="image"
src="https://github.com/user-attachments/assets/9349af8c-22d3-4c9b-a435-a73719f17ba3"
/>
<img width="572" height="800" alt="image"
src="https://github.com/user-attachments/assets/939ada3b-1440-443b-8492-0eb61a7ee90f"
/>

</details>

<details><summary>Current Release (0.214.7)</summary>

<img width="569" height="777" alt="image"
src="https://github.com/user-attachments/assets/e6fffe77-e4c6-48e3-9c6d-a140298225c5"
/>
<img width="381" height="896" alt="image"
src="https://github.com/user-attachments/assets/bf107950-c33a-4603-90d3-2304bef0a4af"
/>
<img width="574" height="798" alt="image"
src="https://github.com/user-attachments/assets/2c5e43e5-f101-4722-8f58-6b176ba950ca"
/>

</details>

<details><summary>Code</summary>

```go
func test_sql() {
	// const assignment
	const _ = /* sql */ "SELECT * FROM users"
	const _ = /* sql */ `SELECT id, name FROM products`

	// var assignment
	var _ = /* sql */ `SELECT id, name FROM products`
	var _ = /* sql */ "SELECT id, name FROM products"

	// := assignment
	test := /* sql */ "SELECT * FROM users"
	test2 := /* sql */ `SELECT * FROM users`
	println(test)
	println(test2)

	// = assignment
	_ = /* sql */ "SELECT * FROM users WHERE id = 1"
	_ = /* sql */ `SELECT * FROM users WHERE id = 1`

	// literal elements
	_ = testStruct{Field: /* sql */ "SELECT * FROM users"}
	_ = testStruct{Field: /* sql */ `SELECT * FROM users`}

	testFunc(/* sql */ "SELECT * FROM users")
	testFunc(/* sql */ `SELECT * FROM users`)

	const backtickString = /* sql */ `SELECT * FROM users;`
	const quotedString = /* sql */ "SELECT * FROM users;"

	const backtickStringNoHighlight = `SELECT * FROM users;`
	const quotedStringNoHighlight = "SELECT * FROM users;"
}

func test_yaml() {
	// const assignment
	const _ = /* yaml */ `
settings:
  enabled: true
  port: 8080
`

	// := assignment
	test := /* yaml */ `
settings:
  enabled: true
  port: 8080
`

	println(test)

	// = assignment
	_ = /* yaml */ `
settings:
  enabled: true
  port: 8080
`

	// literal elements in a struct
	_ = testStruct{Field: /* yaml */ `
settings:
  test: 1234
  port: 8080
`}

	// function argument
	testFunc(/* yaml */ `
settings:
  enabled: true
  port: 8080
`)
}

func test_css() {
	// const assignment
	const _ = /* css */ "body { margin: 0; }"
	const _ = /* css */ `body { margin: 0; }`

	const cssCodes = /* css */ `
h1 {
  color: #333;
}
`

	// := assignment
	test := /* css */ "body { margin: 0; }"
	println(test)

	// = assignment
	_ = /* css */ "body { margin: 0; }"
	_ = /* css */ `body { margin: 0; }`

	// literal elements
	_ = testStruct{Field: /* css */ "body { margin: 0; }"}
	_ = testStruct{Field: /* css */ `body { margin: 0; }`}

	testFunc(/* css */ "body { margin: 0; }")
	testFunc(/* css */ `body { margin: 0; }`)

	const backtickString = /* css */ `body { margin: 0; }`
	const quotedString = /* css */ "body { margin: 0; }"

	const backtickStringNoHighlight = `body { margin: 0; }`
	const quotedStringNoHighlight = "body { margin: 0; }"
}
```

</details>

Release Notes:

- Greatly improved the quality of comment-directed language injections
in Go
2025-12-03 10:32:51 -06:00
Arthur Schurhaus
c248a956e0 markdown: Fix rendering of inline HTML <code> tags (#43513)
Added support for rendering HTML `<code> `tags inside Markdown content.
Previously, these tags were ignored by the renderer and displayed as raw
text (inside LSP hover documentation).

Closes: #43166 

Release Notes:
- Fixed styling of `<code>` HTML tags in Markdown popovers.

Before:
<img width="445" height="145" alt="image"
src="https://github.com/user-attachments/assets/67c4f864-1fa7-46a9-bb25-8b07a335355d"
/>
After:
<img width="699" height="257" alt="image"
src="https://github.com/user-attachments/assets/8d784a75-28be-43cd-80b4-3aad8babb65b"
/>
2025-12-03 16:58:51 +01:00
Remco Smits
7e177c496c markdown_preview: Fix markdown tables taking up the full width of the parent element (#43555)
Closes #39152

This PR fixes an issue where we would render Markdown tables full width
based on their container size. We now render tables based on their
content min size, meaning you are still allowed to make the table render
as it was before by making the columns `w_full`.

I had to change the `div()` to `v_flex().items_start()` because this
introduced a weird displaying behavior of the outside table border,
because the grid container was not shrinking due to It was always taking
up the full width of their container.

**Before**
<img width="1273" height="800" alt="Screenshot 2025-11-26 at 14 37 19"
src="https://github.com/user-attachments/assets/2e152021-8679-48c2-b7bd-1c02768c0253"
/>

**After**
<img width="1273" height="797" alt="Screenshot 2025-11-26 at 14 56 12"
src="https://github.com/user-attachments/assets/4459d20e-8c3b-487b-a215-c95ee5c1fc8e"
/>

**Code example**

```markdown
|   Name   |   Age   |   Occupation   |
|:--------:|:-------:|:--------------:|
| Alice    |  28     | Engineer       |
| Bob      |  34     | Designer       |
| Carol    |  25     | Developer      |


| Syntax      | Description |
| ----------- | ----------- |
| Header      | Title       |
| Paragraph   | Text        |


| City           | Population (approx.) | Known For                          |
|----------------|----------------------|------------------------------------|
| New York       | 8,500,000            | Statue of Liberty, Wall Street     |
| Los Angeles    | 4,000,000            | Hollywood, film industry           |
| Chicago        | 2,700,000            | Architecture, deep-dish pizza      |
| Houston        | 2,300,000            | NASA, energy industry              |
| Miami          | 470,000              | Beaches, Latin culture             |
| San Francisco  | 800,000              | Golden Gate Bridge, Silicon Valley |
| Las Vegas      | 650,000              | Casinos, nightlife                 |


<table>
    <caption>Table Caption</caption>
  <thead>
    <tr>
    <th>ID asjkfjaslkf jalksjflksajflka jlksdla k</th>
    <th>Name</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>1</td>
      <td>Chris</td>
    </tr>
    <tr>
      <td>2</td>
      <td>Dennis</td>
    </tr>
    <tr>
      <td>3</td>
      <td>Sarah</td>
    </tr>
    <tr>
      <td>4</td>
      <td>Karen</td>
    </tr>
  </tbody>
</table>
```

cc @bennetbo

Release Notes:

- Markdown Preview: Markdown tables scale now based on their content
size
2025-12-03 16:49:40 +01:00
Joseph T. Lyons
e39dd2af67 Bump Zed to v0.217 (#44080)
Release Notes:

- N/A
2025-12-03 15:45:45 +00:00
Finn Evers
904d90bee7 extension_ci: Run tests on pushes to main (#44079)
This seems sensible to do - it already was the case prior but
indirectly, lets rather be explicit about this.

Release Notes:

- N/A

Co-authored-by: Agus Zubiaga <agus@zed.dev>
2025-12-03 15:13:15 +00:00
Xiaobo Liu
1e09cbfefa workspace: Scope tab tooltip to tab content only (#44076)
Release Notes:

- Fixed scope tab tooltip to tab content only

Signed-off-by: Xiaobo Liu <cppcoffee@gmail.com>
2025-12-03 12:08:49 -03:00
Finn Evers
8ca2571367 extension_ci: Do not trigger version bump on workflow file changes (#44077)
Release Notes:

- N/A

Co-authored-by: Agus Zubiaga <agus@zed.dev>
2025-12-03 15:05:15 +00:00
Agus Zubiaga
95a553ea94 Do not report rejected sweep predictions to cloud (#44075)
Release Notes:

- N/A

Co-authored-by: MrSubidubi <dev@bahn.sh>
2025-12-03 14:26:40 +00:00
Joseph T. Lyons
bf878e9a95 Remove unnecessary variable redeclaration (#44074)
Release Notes:

- N/A
2025-12-03 13:55:58 +00:00
Alexander Andreev
a688239113 python: Fix autocomplete sorting (#44050)
Closes:
#38727 (Python autocompletion being sorted alphabetically)
<img
src="https://github.com/user-attachments/assets/8208d511-f4a4-41f9-8550-3b24370bf776"
width="400">

<img
src="https://github.com/user-attachments/assets/e7c99d7a-b61e-463b-b0f1-36cd279b3887"
width="400">


Release Notes:
- Improve sort order of pyright/basedpyright code completions
2025-12-03 13:51:18 +01:00
Lukas Wirth
4e8f6ddae9 git: Fix unwrap in git2::Index::get_path (#44059)
Fixes ZED-1VR

Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-12-03 11:56:16 +00:00
Ben Brandt
0f67f08795 Update to ACP SDK v0.8.0 (#44063)
Uses the latest version of the SDK + schema crate. A bit painful because
we needed to move to `#[non_exhaustive]` on all of these structs/enums,
but will be much easier going forward.

Also, since we depend on unstable features, I am pinning the version so
we don't accidentally introduce compilation errors from other update
cycles.

Release Notes:

- N/A
2025-12-03 11:24:08 +00:00
Ben Brandt
fe6fa1bbdc Revert "acp: Add a timeout when initializing an ACP agent so the user isn't waiting forever" (#44066)
Reverts zed-industries/zed#43663
2025-12-03 11:11:23 +00:00
Lukas Wirth
50d0f29624 languages: Fix python run module task failing on windows (#44064)
Fixes #40155

Release Notes:

- Fixed python's run module task not working on windows platforms

Co-authored by: Smit Barmase <smit@zed.dev>
2025-12-03 10:29:18 +00:00
lipcut
9857fd233d Make highlighting of C preprocessing directive same as C++ (#44043)
Small fix for consistency between C and C++ highlighting. Related to
https://github.com/zed-industries/zed/issues/9461

Release Notes:

- Change syntax highlighting for preprocessing directive in C so it can
be configured with `keyword.directive` instead of being treated as other
`keyword`. The behavior should be like the C++ one now.

<img width="953" height="343" alt="圖片"
src="https://github.com/user-attachments/assets/6b45d2cc-323d-44ba-995f-d77996757669"
/>
2025-12-03 09:30:54 +01:00
Kirill Bulatov
ad51017f20 Properly filter out the greedy bracket pairs (#44022)
Follow-up of https://github.com/zed-industries/zed/pull/43607

Release Notes:

- N/A
2025-12-03 08:27:55 +00:00
Joseph T. Lyons
2bf47879de Hide "File History" for untracked files in Git Panel context menu (#44035) 2025-12-02 20:47:01 -05:00
Rani Malach
65b4e9b10a extensions_ui: Add upsell banners for integrated extensions (#43872)
Add informational banners for extensions that have been integrated into
Zed core:
- Basedpyright (Python language server)
- Ruff (Python linter)
- Ty (Python language server)

These banners appear when users search for these extensions, informing
them that the functionality is now built-in and linking to relevant
documentation.

The banners trigger when:
- Users search by extension ID (e.g., 'id:ruff')
- Users search using relevant keywords (e.g., 'basedpyright', 'pyright',
'ruff', 'ty')

Supersedes #43844

Closes #43837

Release Notes:

- Added banners to the extensions page when searching for Basedpyright,
Ruff, or Ty, indicating that these features are now built-in.

---------

Co-authored-by: Marshall Bowers <git@maxdeviant.com>
2025-12-02 18:44:08 -05:00
Marshall Bowers
98dec9246e zed: Promote comment to a doc comment (#44031)
This PR promotes a line comment above a variant member to a doc comment,
so that the docs show up on hover.

Release Notes:

- N/A
2025-12-02 23:28:30 +00:00
Joseph T. Lyons
39536cae83 docs: Add Conda package to Linux community-maintained packages list (#44029)
Release Notes:

- N/A
2025-12-02 22:44:22 +00:00
Mikhail Pertsev
b4e1d86a16 git: Use UI font in commit and blame popovers (#43975)
Closes #30353

Release Notes:

- Fixed: Hover tooltips in git commit and blame popovers now
consistently use the UI font
2025-12-02 19:44:03 -03:00
Agus Zubiaga
8a12ecf849 commit view: Display message within editor (#44024)
#42441 moved the commit message out of the multi-buffer editor into its
own header element which looks nicer, but unfortunately can make the
view become unusable when the commit message is too long since it
doesn't scroll with the diff.

This PR maintains the metadata in its own element, but moves the commit
message back to the editor so the user can scroll past it. This does
mean that we lose markdown rendering for now, but we think this is a
good solution for the moment.


https://github.com/user-attachments/assets/d67cf22e-1a79-451a-932a-cdc8a65e43de

Release Notes:

- N/A

---------

Co-authored-by: cameron <cameron.studdstreet@gmail.com>
2025-12-02 22:30:43 +00:00
Danilo Leal
22bf449b9e settings_ui: Fix some non-title case settings items (#44026)
Release Notes:

- N/A
2025-12-02 22:30:18 +00:00
Dino
bcf9142bbc Update tree-sitter-bash to 0.25.1 (#44009)
With the merging and publishing of
https://github.com/tree-sitter/tree-sitter-bash/pull/311 , we can now go
ahead and update the version of `tree-sitter-bash` that Zed relies on to
the latest version.

Closes #42091

Release Notes:

- Improved grammar for "Shell Script"
2025-12-02 22:29:48 +00:00
Marshall Bowers
a2d57fc7b6 zed_extension_api: Fork new version of extension API (#44025)
This PR forks a new version of the `zed_extension_api` in preparation
for new changes.

We're jumping from v0.6.0 to v0.8.0 for the WIT because we released
v0.7.0 of the `zed_extension_api` without any WIT changes (it probably
should have been v0.6.1, instead).

Release Notes:

- N/A
2025-12-02 22:26:40 +00:00
Andrew Farkas
96a917091a Apply show_completions_on_input: false to word & snippet completions (#44021)
Closes #43408

Previously, we checked the setting inside `is_completion_trigger()`,
which only affects LSP completions. This was ok because user-defined
snippets were tacked onto LSP completions. Then #42122 and #42398 made
snippet completions their own thing, similar to word completions,
surfacing #43408. This PR moves the settings check into
`open_or_update_completions_menu()` so it applies to all completions.

Release Notes:

- Fixed setting `show_completions_on_input: false` so that it affects
word and user-defined snippet completions as well as LSP completions
2025-12-02 21:33:41 +00:00
John Tur
a2ddb0f1cb Fix "busy" cursor appearing on startup (#44019)
Closes https://github.com/zed-industries/zed/issues/43910

Release Notes:

- N/A
2025-12-02 16:15:18 -05:00
Pranav Joglekar
23e5477a4c vim: Move to opening html tag from / in closing tag (#42513)
Closes #41582

Release Notes:

- Improves the '%' vim motion for html by moving the cursor to the
opening tag when its positioned on the `/` ( slash ) of the closing tag
2025-12-02 12:58:20 -08:00
David Kleingeld
4e043cd56b Add git team to git in REVIEWERS.conl (#41841)
Release Notes:

- N/A
2025-12-02 20:40:26 +00:00
Joseph T. Lyons
d283338885 Add "File History" option to Git Panel entry context menu (#44016)
Sublime Merge:

<img width="403" height="313" alt="image"
src="https://github.com/user-attachments/assets/3ebf3c20-324f-4453-88fb-ad93da546f92"
/>

Fork: 

<img width="347" height="474" alt="image"
src="https://github.com/user-attachments/assets/bcec8920-9415-4ffe-9f17-8ee3e764c5bb"
/>

Zed:

<img width="362" height="316" alt="image"
src="https://github.com/user-attachments/assets/06e577cb-f897-45e0-b4b3-59a2c28b3342"
/>


Release Notes:

- Added a "File History" option to Git Panel entry context menu
2025-12-02 20:39:29 +00:00
Danilo Leal
b1af02ca71 debugger: Improve step icons (#44017)
Closes https://github.com/zed-industries/zed/issues/38475

<img width="500" height="820" alt="Screenshot 2025-12-02 at 5  24@2x"
src="https://github.com/user-attachments/assets/802e4fd7-0d6d-4cc2-a77a-17df8c268fc7"
/>

Release Notes:

- N/A
2025-12-02 20:38:18 +00:00
Agus Zubiaga
59b5de5532 Update typos to 1.40.0 (#43739)
Noticed we had a few typos that weren't caught by the version we were
using

Release Notes:

- N/A
2025-12-02 18:47:25 +00:00
Agus Zubiaga
efa98a12fd Pin cargo-about to 0.8.2 (#44012)
cargo-about@0.8.3 doesn't seem to like our license identifiers. Pinning
to 0.8.2 for now.

Release Notes:

- N/A
2025-12-02 17:54:08 +00:00
Kirill Bulatov
7bea1ba555 Run commands if completion items require so (#44008)
Abide the LSP better and actually run commands if completion items
request those:


https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#completionItem
```
/**
   * An optional command that is executed *after* inserting this completion.
   * *Note* that additional modifications to the current document should be
   * described with the additionalTextEdits-property.
   */
   command?: [Command](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#command);
```

Release Notes:

- N/A
2025-12-02 19:38:01 +02:00
Novus Nota
7c95834b7b languages: Add injections to highlight script blocks of actions/github-script as JavaScript (#43771)
Hello, this is my first time contributing here! The issue creation
button noted that "feature request"-esque items should go to
discussions, so with this PR, I'm closing that discussion and not an
issue. I hope that's ok :)

---

Closes https://github.com/zed-industries/zed/discussions/43769

Preview:

<img width="500" alt="image"
src="https://github.com/user-attachments/assets/00b8d475-d452-42e9-934b-ee5dbe48586e"
/><p/>

Release Notes:

- Added JavaScript highlighting via YAML `injections.scm` for script
blocks of `actions/github-script`
2025-12-02 17:30:09 +00:00
Agus Zubiaga
3d58738548 collab: Add action to copy room id (#44004)
Adds a `collab: copy room id` action which copies the live kit room name
and session ID to the clipboard.

Release Notes:

- N/A
2025-12-02 16:27:36 +00:00
Agus Zubiaga
2db237aa52 Limit edit prediction reject batches to max (#43965)
We currently attempt to flush all rejected predictions at once even if
we have accumulated more than
`MAX_EDIT_PREDICTION_REJECTIONS_PER_REQUEST`. Instead, we will now flush
as many as possible, and then keep the rest for the next batch.


Release Notes:

- N/A
2025-12-02 13:22:16 -03:00
Lukas Wirth
305e73ebbb gpui(windows): Move interior mutability down into fields (#44002)
Windows applications tend to be fairly re-entrant which does not work
well with our current top-level `RefCell` approach. We have worked
around a bunch of panics in the past due to this, but ultimately this
will re-occur (and still does according to sentry) in the future. So
this PR moves all interior mutability down to the fields.

Fixes ZED-1HM
Fixes ZED-3SH
Fixes ZED-1YV
Fixes ZED-29S
Fixes ZED-29X
Fixes ZED-369
Fixes ZED-20W

Release Notes:

- Fixed panics on windows caused by unexpected re-entrancy for interior
mutability
2025-12-02 16:02:47 +00:00
Lukas Wirth
ec6e7b84b8 gpui(windows): Fix top resize edge being only 1px tall (#43995)
Closes https://github.com/zed-industries/zed/issues/41693

Release Notes:

- Fixed an issue on windows where the resize area on the title bar is
only 1px tall
2025-12-02 13:45:51 +00:00
Shuhei Kadowaki
4f5cc0a24b lsp: Send client process ID in LSP initialize request (#43988)
Per the [LSP
specification](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#initializeParams),
the client should send its process ID to the language server so the
server can monitor the client process and shut itself down if the client
exits unexpectedly.

Release Notes:

- N/A
2025-12-02 14:04:28 +02:00
Lukas Wirth
a2f69cd5bd remote: Finer grained --exec attempts (#43987)
Closes https://github.com/zed-industries/zed/issues/43328

Instead of trying to guess whether we can `--exec`, this now
restructures things to only attempt to use that flag where its
necessary. We only need to `--exec` when we are interested in the shell
output after all.

Release Notes:

- Fixed wsl remoting not working with some nixOS setups
2025-12-02 12:50:25 +01:00
Lukas Wirth
6a097298b0 terminal_view: Fix close tab button tooltip showing wrong keybinding on windows (#43981)
Closes https://github.com/zed-industries/zed/issues/43882

Release Notes:

- Fixed wrong button tooltips being shown for terminal pane on windows
2025-12-02 11:33:00 +00:00
Lukas Wirth
0df86e406a windows: Fix more vscode keybindings (#43983)
Closes https://github.com/zed-industries/zed/issues/43151 Closes
https://github.com/zed-industries/zed/issues/42022

Looking at other mappings, it seems like this should've been an alt
keybind after all

Release Notes:

- Fixed toggling focus to project panel via keybinding on windows not
always working
2025-12-02 11:31:34 +00:00
Bhuminjay Soni
a74aac88c9 Increase askpass timeout for git operations (#42946)
Closes #29903

Release Notes:

- Increased timeout from 17->300 & improved the error message.

---------

Signed-off-by: 11happy <soni5happy@gmail.com>
Signed-off-by: 11happy <bhuminjaysoni@gmail.com>
2025-12-02 12:18:13 +01:00
John Tur
e5105ccdbe Don't halt processing from WM_SETCURSOR (#43982)
Release Notes:

- N/A
2025-12-02 05:57:20 -05:00
Lukas Wirth
876b258088 zed: Fix unnecessary rebuilds of the main binary on windows (#43979)
Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-12-02 10:15:30 +00:00
Arjun Bajaj
19aba43f3e Add Tailwind CSS support for Gleam (#43968)
Currently, Zed does not provide suggestions and validations for Gleam,
as it is only available for languages specified in `tailwind.rs`. This
pull-request adds Gleam to that list of languages.

After this, if Tailwind is configured to work with Gleam, suggestions
and validation appear correctly.

Even after this change, Tailwind will not be able to detect and give
suggestions in Gleam directly. Below is the config required for Tailwind
classes to be detected in all Gleam strings.


<details><summary>Zed Config for Tailwind detection in Gleam</summary>
<p>

```
{
  "languages": {
    "Gleam": {
      "language_servers": [
        "gleam",
        "tailwindcss-language-server"
      ]
    }
  },
  "lsp": {
    "tailwindcss-language-server": {
      "settings": {
        "experimental": {
          "classRegex": [
            "\"([^\"]*)\""
          ]
        }
      }
    }
  }
}
```

The `classRegex` will match all Gleam strings, making it work seamlessly
with Lustre templates and plain string literals.

</p>
</details> 

Release Notes:

- Added support for Tailwind suggestions and validations for the [Gleam
programming language](https://gleam.run/).
2025-12-02 03:43:31 -05:00
Lukas Wirth
8d09610748 git_ui: Fix utf8 panic in compress_commit_diff (#43972)
Fixes ZED-3QG

Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-12-02 08:31:44 +00:00
Lukas Wirth
5b6663ef97 terminal: Try to fix flaky macOS test (#43971)
Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-12-02 08:21:18 +00:00
Josh Piasecki
f445f22fe6 Add an action listener to workspace for ActivatePreviousItem and ActivateNextItem (#42588)
Release Notes:

- pane::ActivatePreviousItem and pane::ActivateNextItem now toggle the
most recent pane when called from a dock panel


a couple months ago i posted a work around that used `SendKeystrokes` to
cycle through pane items when focused on a dock.
#35253

this pr would add this functionality to the these actions by default.
i implemented this by adding an action listener to the workspace level.

------
if the current context is a dock that does not hold a pane
it retrieves the most recent pane from `activation_history` and
activates the next item on that pane instead.

- `"Pane > Editor"`
cycles through the current pane like normal
- `"Dock > Pane > Terminal"`
also cycles through the pane items like normal
- `"Dock > (Any Child that is not a child of Pane)"`
cycles through the items of the most recent pane.

this is the standard behavior in VS Code i believe.

in the video below you can see the actions cycling through the editor
like normal when focus is on the editor.
then you can see the editor continue to cycle when the focus is on the
project panel.
and that the focus stays on the project panel.
and you can see the action cycle the terminal items when the focus is
moved to the terminal


https://github.com/user-attachments/assets/999ab740-d2fa-4d00-9e53-f7605217e6ac

the only thing i noticed is that for this to work the keybindings must
be set above `Pane`
so they have to be set globally or on workspace. otherwise they do not
match in the context
2025-12-01 22:01:11 -07:00
Connor Tsui
6216af9b5a Allow dynamic set_theme based on Appearance (#42812)
Tracking Issue (does not close):
https://github.com/zed-industries/zed/issues/35552

This is somewhat of a blocker for
https://github.com/zed-industries/zed/pull/40035 (but also the current
behavior doesn't really make sense).

The current behavior of `ThemeSelectorDelegate::set_theme` (the theme
selector menu) is to simply set the in-memory settings to `Static`,
regardless of if it is currently `Dynamic`. The reason this doesn't
matter now is that the `theme::set_theme` function that updates the
user's settings file _will_ make this check, so dynamic settings stay
dynamic in `settings.json`, but not in memory.

But this is also sort of strange, because `theme::set_theme` will set
the setting of whatever the old appearance was to the new theme name. In
other words, if I am currently on a light mode theme and I change my
theme to a dark mode theme using the theme selector, the `light` field
of `theme` in `settings.json` is set to a dark mode theme!

_I think this is because displaying the new theme in the theme selector
does not update the global context, so
`ThemeSettings::get_global(cx).theme.name(appearance).0` returns the
original theme appearance, not the new one._

---

This PR makes `ThemeSelectorDelegate::set_theme` keep the current
`ThemeSelection`, as well as changes the behavior of the
`theme::set_theme` call to always choose the correct setting to update.

One edge case that might be slightly strange now is that if the user has
specified the mode as `System`, this will now override that with the
appearance of the new theme. I think this is fine, as otherwise a user
might set a dark theme and nothing will change because the
`ThemeAppearanceMode` is set to `light` or `system` (where `system` is
also light).

I also have an `unreachable!` in there that I'm pretty sure is true but
I don't really know how to formally prove that...

Release Notes:

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

---------

Signed-off-by: Connor Tsui <connor.tsui20@gmail.com>
2025-12-01 20:52:57 -07:00
Anthony Eid
464c0be2b7 git: Add word diff highlighting (#43269)
This PR adds word/character diff for expanded diff hunks that have both
a deleted and added section, as well as a setting `word_diff_enabled` to
enable/disable word diffs per language.

- `word_diff_enabled`: Defaults to true. Whether or not expanded diff
hunks will show word diff highlights when they're able to.

### Preview
<img width="1502" height="430" alt="image"
src="https://github.com/user-attachments/assets/1a8d5b71-449e-44cd-bc87-d6b65bfca545"
/>

### Architecture

I had three architecture goals I wanted to have when adding word diff
support:

- Caching: We should only calculate word diffs once and save the result.
This is because calculating word diffs can be expensive, and Zed should
always be responsive.
- Don't block the main thread: Word diffs should be computed in the
background to prevent hanging Zed.
- Lazy calculation: We should calculate word diffs for buffers that are
not visible to a user.

To accomplish the three goals, word diffs are computed as a part of
`BufferDiff` diff hunk processing because it happens on a background
thread, is cached until the file is edited, and is only refreshed for
open buffers.

My original implementation calculated word diffs every frame in the
Editor element. This had the benefit of lazy evaluation because it only
calculated visible frames, but it didn't have caching for the
calculations, and the code wasn't organized. Because the hunk
calculations would happen in two separate places instead of just
`BufferDiff`. Finally, it always happened on the main thread because it
was during the `EditorElement` layout phase.

I used Zed's
[`diff_internal`](02b2aa6c50/crates/language/src/text_diff.rs (L230-L267))
as a starting place for word diff calculations because it uses
`Imara_diff` behind the scenes and already has language-specific
support.

#### Future Improvements

In the future, we could add `AST` based word diff highlights, e.g.
https://github.com/zed-industries/zed/pull/43691.

Release Notes:

- git: Show word diff highlight in expanded diff hunks with less than 5
lines.
- git: Add `word_diff_enabled` as a language setting that defaults to
true.

---------

Co-authored-by: David Kleingeld <davidsk@zed.dev>
Co-authored-by: Cole Miller <cole@zed.dev>
Co-authored-by: cameron <cameron.studdstreet@gmail.com>
Co-authored-by: Lukas Wirth <lukas@zed.dev>
2025-12-01 22:36:30 -05:00
Moritz Fröhlich
2df5993eb0 workspace: Add ctrl-w x support for vim (#42792)
Adds support for the vim CTRL-W x keybinding, which swaps the active
pane with the next adjacent one, prioritizing column over row and next
over previous. Upon swap, the pane which was swapped with is activated
(this is the vim behavior).

See also
ca6a260ef1/runtime/doc/windows.txt (L514C1-L521C24)

Release Notes:

- Added ctrl-w x keybinding in Vim mode, which swaps the active window
with the next adjacent one (aligning with Vim behavior)

**Vim behavior**


https://github.com/user-attachments/assets/435a8b52-5d1c-4d4b-964e-4f0f3c9aca31


https://github.com/user-attachments/assets/7aa40014-1eac-4cce-858f-516cd06d13f6

**Zed behavior**


https://github.com/user-attachments/assets/2431e860-4e11-45c6-a3f2-08f1a9b610c1


https://github.com/user-attachments/assets/30432d9d-5db1-4650-af30-232b1340229c

Note: There is a discrepancy where in Vim, if vertical and horizontal
splits are mixed, swapping from a column with a single window does not
work (see the vertical video), whilst in Zed it does. However, I don't
see a good reason as to why this should not be supported and would argue
that it makes more sense to keep the clear priority swap behavior,
instead of adding a workaround to supports such cases.

---------

Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
2025-12-02 03:36:06 +00:00
Hans
04e92fb2d2 vim: Fix :s command ignoring case sensitivity settings (#42457)
Closes #36260 

This PR fixes the vim :s Command Ignores Case Sensitivity Settings

Release Notes:

- N/A
2025-12-01 20:22:22 -07:00
Conrad Irwin
e27590432f Actually show settings errors on app load (#43268)
They were previously hidden by the global settings file succeeding to
parse :face-palm:

Release Notes:

- N/A
2025-12-01 19:53:36 -07:00
Cole Miller
a675eb1667 Fix regression in closing project diff (#43964)
Follow-up to #43586--when the diff is not split, just render the primary
editor. Otherwise the child `Pane` intercepts `CloseActiveItem`. (This
is still a bug for the actual split diff, but just fixing the
user-visible regression for now.)

Release Notes:

- N/A
2025-12-02 02:32:36 +00:00
Agus Zubiaga
b27ad98520 zeta: Put back 15s reject debounce (#43958)
Release Notes:

- N/A
2025-12-01 23:57:09 +00:00
Finn Evers
9c4e16088c extension_ci: Use more robust bash syntax for bump_extension_version (#43955)
Also does some more cleanup here and there.

Release Notes:

- N/A
2025-12-01 23:38:05 +00:00
Finn Evers
34a2bfd6b7 ci: Supply github_token to bufbuild_setup_action (#43957)
This should hopefully resolve errors like
https://github.com/zed-industries/zed/actions/runs/19839788214/job/56845583441

Release Notes:

- N/A
2025-12-01 23:37:33 +00:00
Finn Evers
99d8d34d48 Fix failing CI on main (#43956)
Release Notes:

- N/A
2025-12-01 23:27:27 +00:00
Finn Evers
bd79edee71 extension_ci: Improve behavior when no Rust is present (#43953)
Release Notes:

- N/A
2025-12-01 22:58:15 +00:00
Agus Zubiaga
0bb1c6ad3e Return json value instead of empty string from accept/reject endpoints 2025-12-01 19:53:06 -03:00
Clément Lap
fd146757cf Add Doxygen injection into C and C++ comments (#43581)
Release Notes:

- C/C++ files now support Doxygen grammars (if a Doxygen extension is installed).
2025-12-01 23:32:23 +01:00
mikeHag
6eb9f9add7 Debug a test annotated with the ignore attribute if the test name partially matches another test (#43110)
Related: #42574
If an integration test is annotated with the ignore attribute, allow the
"debug: Test" option of the debug scenario or Code Action to run with
the "--include-ignored" and "--exact" arguments. Inclusion of "--exact"
covers the case where more that one test shares a base name. For
example, consider two tests named "test_no_ace_in_middle_of_straight"
and "test_no_ace_in_middle_of_straight_flush." Without the "--exact"
argument both tests would run if a user attempts to debug
"test_no_ace_in_middle_of_straight".

Release Notes:

- Improved "debug test" experience in Rust with ignored tests.
2025-12-01 23:28:37 +01:00
Katie Geer
3d3d124e01 docs: Migrate from VS Code (#43438)
Add guide for users coming from VS Code

Release Notes:

- N/A

---------

Co-authored-by: Danilo Leal <daniloleal09@gmail.com>
2025-12-01 14:08:07 -08:00
Kirill Bulatov
de392cda39 Fix bracket colorization not working in file-less files with a proper language (#43947)
Follow-up of https://github.com/zed-industries/zed/pull/43172

Release Notes:

- Fixed bracket colorization in file-less files with a proper language
2025-12-01 21:53:54 +00:00
Piotr Osiewicz
33513292af script: Fix upload-nightly versioning (#43933)
Release Notes:

- N/A
2025-12-01 22:38:58 +01:00
Finn Evers
1535e95066 ci: Always run nextest for extensions (#43945)
This makes rolling this out across extensions a far bit easier and also
safer, because we don't have to manually set `run_tests` for every
extension (and never have to consider this when updating these).

Release Notes:

- N/A
2025-12-01 21:20:47 +00:00
Agus Zubiaga
26f77032a2 edit prediction: Do not attempt to gather context for non-zeta2 models (#43943)
We were running the LLM-based context gathering for zeta1 and sweep
which don't use it.

Release Notes:

- N/A
2025-12-01 21:14:00 +00:00
Agus Zubiaga
efff602909 zeta: Smaller reject batches (#43942)
We were allowing the client to build up to
`MAX_EDIT_PREDICTION_REJECTIONS_PER_REQUEST`. We'll now attempt to flush
the rejections when we reach half max.

Release Notes:

- N/A
2025-12-01 21:10:13 +00:00
Danilo Leal
58c9cbae40 git_ui: Clean up file history view design (#43941)
Mostly cleaning up the UI code. The UI looks fairly the same just a bit
more polished, with proper colors and spacing. Also added a scrollbar in
there. Next step would be to make it keyboard navigable.

<img width="500" height="1948" alt="Screenshot 2025-12-01 at 5  38@2x"
src="https://github.com/user-attachments/assets/c266b97c-4a79-4a0e-8e78-2f1ed1ba495f"
/>

Release Notes:

- N/A
2025-12-01 20:56:34 +00:00
Finn Evers
7881551dda ci: Request GitHub token for proper repository (#43940)
Release Notes:

- N/A
2025-12-01 21:30:51 +01:00
Ben Kunkle
ff6bd7d82e sweep: Add UI for setting Sweep API token in system keychain (#43502)
Closes #ISSUE

Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-12-01 14:36:49 -05:00
Finn Evers
bfb876c782 Improve extension CI concurrency (#43935)
While this does work for PRs and such, it does not work with main...
Hence, moving the token a few chars to the right to fix this issue.

Release Notes:

- N/A
2025-12-01 19:29:20 +00:00
Joseph T. Lyons
64b432e4ac Update crash report template (#43934)
- Updates tone to match bug template
- Removes the "current vs expected" behavior section
- It doesn't feel very useful. Users expect Zed to not crash, regardless
of what they are doing.

Release Notes:

- N/A
2025-12-01 19:05:10 +00:00
Richard Feldman
33ecb0a68f Clarify how outlining works in read_file_tool description (#43929)
<img width="698" height="218" alt="Screenshot 2025-12-01 at 1 27 02 PM"
src="https://github.com/user-attachments/assets/a5d9e121-4e68-40d0-a346-4dd39e77233b"
/>

Closes #419

Release Notes:

- Revise tool call description for read file tool to explain outlining
behavior
2025-12-01 14:00:18 -05:00
Danilo Leal
7b7ddbd1e8 Adjust edit prediction upsell copy and animation (#43931)
Release Notes:

- N/A
2025-12-01 18:45:45 +00:00
Finn Evers
ed81ef0442 ci: Add extension workflow concurrency rules (#43930)
Release Notes:

- N/A
2025-12-01 18:42:46 +00:00
Finn Evers
88fffae9dd Fix extension CI workflow disclaimer (#43926)
Release Notes:

- N/A
2025-12-01 18:22:52 +00:00
278 changed files with 11793 additions and 5894 deletions

View File

@@ -6,28 +6,18 @@ body:
- type: textarea
attributes:
label: Reproduction steps
description: A step-by-step description of how to reproduce the crash from a **clean Zed install**. **Be verbose**. **Issues with insufficient detail may be summarily closed**.
description: A step-by-step description of how to reproduce the crash from a **clean Zed install**. The more context you provide, the easier it is to find and fix the problem fast.
placeholder: |
1. Start Zed
2. Perform an action
3. Zed crashes
validations:
required: true
- type: textarea
attributes:
label: Current vs. Expected behavior
description: |
Go into depth about what actions youre performing in Zed to trigger the crash. If Zed crashes before it loads any windows, make sure to mention that. Again, **be verbose**.
**Skipping this/failure to provide complete information will result in the issue being closed.**
placeholder: "Based on my reproduction steps above, when I perform said action, I expect this to happen, but instead Zed crashes."
validations:
required: true
- type: textarea
attributes:
label: Zed version and system specs
description: |
Open the command palette in Zed, then type “zed: copy system specs into clipboard”. **Skipping this/failure to provide complete information will result in the issue being closed**.
Open the command palette in Zed, then type “zed: copy system specs into clipboard”.
placeholder: |
Zed: v0.215.0 (Zed Nightly bfe141ea79aa4984028934067ba75c48d99136ae)
OS: macOS 15.1
@@ -37,7 +27,7 @@ body:
required: true
- type: textarea
attributes:
label: If applicable, attach your Zed log file to this issue
label: Attach Zed log file
description: |
Open the command palette in Zed, then type `zed: open log` to see the last 1000 lines. Or type `zed: reveal log in file manager` in the command palette to reveal the log file itself.
value: |

View File

@@ -25,33 +25,6 @@ on:
description: The app secret for the corresponding app ID
required: true
jobs:
check_extension:
if: (github.repository_owner == 'zed-industries' || github.repository_owner == 'zed-extensions')
runs-on: namespace-profile-2x4-ubuntu-2404
steps:
- name: steps::checkout_repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
with:
clean: false
- id: cache-zed-extension-cli
name: extension_tests::cache_zed_extension_cli
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830
with:
path: zed-extension
key: zed-extension-${{ env.ZED_EXTENSION_CLI_SHA }}
- name: extension_tests::download_zed_extension_cli
if: steps.cache-zed-extension-cli.outputs.cache-hit != 'true'
run: |
wget --quiet "https://zed-extension-cli.nyc3.digitaloceanspaces.com/$ZED_EXTENSION_CLI_SHA/x86_64-unknown-linux-gnu/zed-extension"
chmod +x zed-extension
shell: bash -euxo pipefail {0}
- name: extension_tests::check
run: |
mkdir -p /tmp/ext-scratch
mkdir -p /tmp/ext-output
./zed-extension --source-dir . --scratch-dir /tmp/ext-scratch --output-dir /tmp/ext-output
shell: bash -euxo pipefail {0}
timeout-minutes: 1
check_bump_needed:
if: (github.repository_owner == 'zed-industries' || github.repository_owner == 'zed-extensions')
runs-on: namespace-profile-2x4-ubuntu-2404
@@ -89,7 +62,6 @@ jobs:
timeout-minutes: 1
bump_extension_version:
needs:
- check_extension
- check_bump_needed
if: |-
(github.repository_owner == 'zed-industries' || github.repository_owner == 'zed-extensions') &&
@@ -114,21 +86,18 @@ jobs:
run: |
OLD_VERSION="${{ needs.check_bump_needed.outputs.current_version }}"
cat <<EOF > .bumpversion.cfg
[bumpversion]
current_version = "$OLD_VERSION"
BUMP_FILES=("extension.toml")
if [[ -f "Cargo.toml" ]]; then
BUMP_FILES+=("Cargo.toml")
fi
[bumpversion:file:Cargo.toml]
bump2version --verbose --current-version "$OLD_VERSION" --no-configured-files ${{ inputs.bump-type }} "${BUMP_FILES[@]}"
[bumpversion:file:extension.toml]
if [[ -f "Cargo.toml" ]]; then
cargo update --workspace
fi
EOF
bump2version --verbose ${{ inputs.bump-type }}
NEW_VERSION="$(sed -n 's/version = \"\(.*\)\"/\1/p' < extension.toml)"
cargo update --workspace
rm .bumpversion.cfg
echo "new_version=${NEW_VERSION}" >> "$GITHUB_OUTPUT"
shell: bash -euxo pipefail {0}
@@ -147,7 +116,6 @@ jobs:
timeout-minutes: 1
create_version_label:
needs:
- check_extension
- check_bump_needed
if: (github.repository_owner == 'zed-industries' || github.repository_owner == 'zed-extensions') && github.event_name == 'push' && github.ref == 'refs/heads/main' && needs.check_bump_needed.outputs.needs_bump == 'false'
runs-on: namespace-profile-8x16-ubuntu-2204

View File

@@ -21,6 +21,8 @@ jobs:
with:
app-id: ${{ secrets.app-id }}
private-key: ${{ secrets.app-secret }}
owner: zed-industries
repositories: extensions
- name: steps::checkout_repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
with:

View File

@@ -7,12 +7,7 @@ env:
CARGO_INCREMENTAL: '0'
ZED_EXTENSION_CLI_SHA: 7cfce605704d41ca247e3f84804bf323f6c6caaf
on:
workflow_call:
inputs:
run_tests:
description: Whether the workflow should run rust tests
required: true
type: boolean
workflow_call: {}
jobs:
orchestrate:
if: (github.repository_owner == 'zed-industries' || github.repository_owner == 'zed-extensions')
@@ -73,12 +68,12 @@ jobs:
run: cargo clippy --release --all-targets --all-features -- --deny warnings
shell: bash -euxo pipefail {0}
- name: steps::cargo_install_nextest
if: inputs.run_tests
uses: taiki-e/install-action@nextest
- name: steps::cargo_nextest
if: inputs.run_tests
run: cargo nextest run --workspace --no-fail-fast
shell: bash -euxo pipefail {0}
env:
NEXTEST_NO_TESTS: warn
timeout-minutes: 3
check_extension:
needs:
@@ -108,7 +103,7 @@ jobs:
mkdir -p /tmp/ext-output
./zed-extension --source-dir . --scratch-dir /tmp/ext-scratch --output-dir /tmp/ext-output
shell: bash -euxo pipefail {0}
timeout-minutes: 1
timeout-minutes: 2
tests_pass:
needs:
- orchestrate

View File

@@ -84,7 +84,7 @@ jobs:
run: ./script/check-keymaps
shell: bash -euxo pipefail {0}
- name: run_tests::check_style::check_for_typos
uses: crate-ci/typos@80c8a4945eec0f6d464eaf9e65ed98ef085283d1
uses: crate-ci/typos@2d0ce569feab1f8752f1dde43cc2f2aa53236e06
with:
config: ./typos.toml
- name: steps::cargo_fmt
@@ -520,6 +520,7 @@ jobs:
uses: bufbuild/buf-setup-action@v1
with:
version: v1.29.0
github_token: ${{ secrets.GITHUB_TOKEN }}
- name: run_tests::check_postgres_and_protobuf_migrations::bufbuild_breaking_action
uses: bufbuild/buf-breaking-action@v1
with:

137
Cargo.lock generated
View File

@@ -159,6 +159,7 @@ dependencies = [
"derive_more 0.99.20",
"editor",
"env_logger 0.11.8",
"eval_utils",
"fs",
"futures 0.3.31",
"git",
@@ -215,9 +216,9 @@ dependencies = [
[[package]]
name = "agent-client-protocol"
version = "0.7.0"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "525705e39c11cd73f7bc784e3681a9386aa30c8d0630808d3dc2237eb4f9cb1b"
checksum = "3e639d6b544ad39f5b4e05802db5eb04e1518284eb05fda1839931003e0244c8"
dependencies = [
"agent-client-protocol-schema",
"anyhow",
@@ -226,16 +227,15 @@ dependencies = [
"derive_more 2.0.1",
"futures 0.3.31",
"log",
"parking_lot",
"serde",
"serde_json",
]
[[package]]
name = "agent-client-protocol-schema"
version = "0.6.2"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ecf16c18fea41282d6bbadd1549a06be6836bddb1893f44a6235f340fa24e2af"
checksum = "f182f5e14bef8232b239719bd99166bb11e986c08fc211f28e392f880d3093ba"
dependencies = [
"anyhow",
"derive_more 2.0.1",
@@ -328,6 +328,7 @@ dependencies = [
"buffer_diff",
"chrono",
"client",
"clock",
"cloud_llm_client",
"collections",
"command_palette_hooks",
@@ -335,6 +336,7 @@ dependencies = [
"context_server",
"db",
"editor",
"eval_utils",
"extension",
"extension_host",
"feature_flags",
@@ -343,6 +345,7 @@ dependencies = [
"futures 0.3.31",
"fuzzy",
"gpui",
"gpui_tokio",
"html_to_markdown",
"http_client",
"image",
@@ -370,6 +373,7 @@ dependencies = [
"proto",
"rand 0.9.2",
"release_channel",
"reqwest_client",
"rope",
"rules_library",
"schemars",
@@ -2126,30 +2130,15 @@ dependencies = [
"syn 2.0.106",
]
[[package]]
name = "bit-set"
version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1"
dependencies = [
"bit-vec 0.6.3",
]
[[package]]
name = "bit-set"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3"
dependencies = [
"bit-vec 0.8.0",
"bit-vec",
]
[[package]]
name = "bit-vec"
version = "0.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb"
[[package]]
name = "bit-vec"
version = "0.8.0"
@@ -2328,9 +2317,9 @@ dependencies = [
[[package]]
name = "borrow-or-share"
version = "0.2.2"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3eeab4423108c5d7c744f4d234de88d18d636100093ae04caf4825134b9c3a32"
checksum = "dc0b364ead1874514c8c2855ab558056ebfeb775653e7ae45ff72f28f8f3166c"
[[package]]
name = "borsh"
@@ -2423,6 +2412,7 @@ dependencies = [
"rand 0.9.2",
"rope",
"serde_json",
"settings",
"sum_tree",
"text",
"unindent",
@@ -4183,6 +4173,7 @@ dependencies = [
"serde_json",
"smol",
"system_specs",
"windows 0.61.3",
"zstd 0.11.2+zstd.1.5.2",
]
@@ -5299,6 +5290,7 @@ dependencies = [
"indoc",
"language",
"lsp",
"menu",
"paths",
"project",
"regex",
@@ -5308,6 +5300,7 @@ dependencies = [
"telemetry",
"theme",
"ui",
"ui_input",
"util",
"workspace",
"zed_actions",
@@ -5412,6 +5405,7 @@ dependencies = [
"tree-sitter-bash",
"tree-sitter-c",
"tree-sitter-html",
"tree-sitter-md",
"tree-sitter-python",
"tree-sitter-rust",
"tree-sitter-typescript",
@@ -5772,6 +5766,15 @@ dependencies = [
"watch",
]
[[package]]
name = "eval_utils"
version = "0.1.0"
dependencies = [
"gpui",
"serde",
"smol",
]
[[package]]
name = "event-listener"
version = "2.5.3"
@@ -5991,22 +5994,11 @@ checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a"
[[package]]
name = "fancy-regex"
version = "0.13.0"
version = "0.16.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "531e46835a22af56d1e3b66f04844bed63158bc094a628bec1d321d9b4c44bf2"
checksum = "998b056554fbe42e03ae0e152895cd1a7e1002aec800fdc6635d20270260c46f"
dependencies = [
"bit-set 0.5.3",
"regex-automata",
"regex-syntax",
]
[[package]]
name = "fancy-regex"
version = "0.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e24cb5a94bcae1e5408b0effca5cd7172ea3c5755049c5f3af4cd283a165298"
dependencies = [
"bit-set 0.8.0",
"bit-set",
"regex-automata",
"regex-syntax",
]
@@ -6228,9 +6220,9 @@ checksum = "8bf7cc16383c4b8d58b9905a8509f02926ce3058053c056376248d958c9df1e8"
[[package]]
name = "fluent-uri"
version = "0.3.2"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1918b65d96df47d3591bed19c5cca17e3fa5d0707318e4b5ef2eae01764df7e5"
checksum = "bc74ac4d8359ae70623506d512209619e5cf8f347124910440dbc221714b328e"
dependencies = [
"borrow-or-share",
"ref-cast",
@@ -6969,7 +6961,7 @@ dependencies = [
[[package]]
name = "gh-workflow"
version = "0.8.0"
source = "git+https://github.com/zed-industries/gh-workflow?rev=e5f883040530b4df36437f140084ee5cc7c1c9be#e5f883040530b4df36437f140084ee5cc7c1c9be"
source = "git+https://github.com/zed-industries/gh-workflow?rev=09acfdf2bd5c1d6254abefd609c808ff73547b2c#09acfdf2bd5c1d6254abefd609c808ff73547b2c"
dependencies = [
"async-trait",
"derive_more 2.0.1",
@@ -6986,7 +6978,7 @@ dependencies = [
[[package]]
name = "gh-workflow-macros"
version = "0.8.0"
source = "git+https://github.com/zed-industries/gh-workflow?rev=e5f883040530b4df36437f140084ee5cc7c1c9be#e5f883040530b4df36437f140084ee5cc7c1c9be"
source = "git+https://github.com/zed-industries/gh-workflow?rev=09acfdf2bd5c1d6254abefd609c808ff73547b2c#09acfdf2bd5c1d6254abefd609c808ff73547b2c"
dependencies = [
"heck 0.5.0",
"quote",
@@ -7526,6 +7518,17 @@ dependencies = [
"serde",
]
[[package]]
name = "hashbrown"
version = "0.16.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"
dependencies = [
"allocator-api2",
"equivalent",
"foldhash 0.2.0",
]
[[package]]
name = "hashlink"
version = "0.8.4"
@@ -8615,21 +8618,21 @@ dependencies = [
[[package]]
name = "jsonschema"
version = "0.30.0"
version = "0.37.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1b46a0365a611fbf1d2143104dcf910aada96fafd295bab16c60b802bf6fa1d"
checksum = "73c9ffb2b5c56d58030e1b532d8e8389da94590515f118cf35b5cb68e4764a7e"
dependencies = [
"ahash 0.8.12",
"base64 0.22.1",
"bytecount",
"data-encoding",
"email_address",
"fancy-regex 0.14.0",
"fancy-regex",
"fraction",
"getrandom 0.3.4",
"idna",
"itoa",
"num-cmp",
"num-traits",
"once_cell",
"percent-encoding",
"referencing",
"regex",
@@ -8637,6 +8640,7 @@ dependencies = [
"reqwest 0.12.24",
"serde",
"serde_json",
"unicode-general-category",
"uuid-simd",
]
@@ -10185,7 +10189,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b977c445f26e49757f9aca3631c3b8b836942cb278d69a92e7b80d3b24da632"
dependencies = [
"arrayvec",
"bit-set 0.8.0",
"bit-set",
"bitflags 2.9.4",
"cfg_aliases 0.2.1",
"codespan-reporting 0.12.0",
@@ -13041,7 +13045,7 @@ dependencies = [
"dap",
"dap_adapters",
"extension",
"fancy-regex 0.14.0",
"fancy-regex",
"fs",
"futures 0.3.31",
"fuzzy",
@@ -13912,13 +13916,14 @@ dependencies = [
[[package]]
name = "referencing"
version = "0.30.0"
version = "0.37.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c8eff4fa778b5c2a57e85c5f2fe3a709c52f0e60d23146e2151cbef5893f420e"
checksum = "4283168a506f0dcbdce31c9f9cce3129c924da4c6bca46e46707fcb746d2d70c"
dependencies = [
"ahash 0.8.12",
"fluent-uri",
"once_cell",
"getrandom 0.3.4",
"hashbrown 0.16.1",
"parking_lot",
"percent-encoding",
"serde_json",
@@ -17112,7 +17117,7 @@ dependencies = [
"alacritty_terminal",
"anyhow",
"collections",
"fancy-regex 0.14.0",
"fancy-regex",
"futures 0.3.31",
"gpui",
"itertools 0.14.0",
@@ -17346,12 +17351,12 @@ dependencies = [
[[package]]
name = "tiktoken-rs"
version = "0.9.1"
source = "git+https://github.com/zed-industries/tiktoken-rs?rev=7249f999c5fdf9bf3cc5c288c964454e4dac0c00#7249f999c5fdf9bf3cc5c288c964454e4dac0c00"
source = "git+https://github.com/zed-industries/tiktoken-rs?rev=2570c4387a8505fb8f1d3f3557454b474f1e8271#2570c4387a8505fb8f1d3f3557454b474f1e8271"
dependencies = [
"anyhow",
"base64 0.22.1",
"bstr",
"fancy-regex 0.13.0",
"fancy-regex",
"lazy_static",
"regex",
"rustc-hash 1.1.0",
@@ -18003,9 +18008,9 @@ dependencies = [
[[package]]
name = "tree-sitter-bash"
version = "0.25.0"
version = "0.25.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "871b0606e667e98a1237ebdc1b0d7056e0aebfdc3141d12b399865d4cb6ed8a6"
checksum = "9e5ec769279cc91b561d3df0d8a5deb26b0ad40d183127f409494d6d8fc53062"
dependencies = [
"cc",
"tree-sitter-language",
@@ -18483,6 +18488,12 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ce61d488bcdc9bc8b5d1772c404828b17fc481c0a582b5581e95fb233aef503e"
[[package]]
name = "unicode-general-category"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b993bddc193ae5bd0d623b49ec06ac3e9312875fdae725a975c51db1cc1677f"
[[package]]
name = "unicode-ident"
version = "1.0.19"
@@ -18717,7 +18728,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "23b082222b4f6619906941c17eb2297fff4c2fb96cb60164170522942a200bd8"
dependencies = [
"outref",
"uuid",
"vsimd",
]
@@ -21202,7 +21212,7 @@ dependencies = [
[[package]]
name = "zed"
version = "0.216.0"
version = "0.217.0"
dependencies = [
"acp_tools",
"activity_indicator",
@@ -21492,6 +21502,8 @@ dependencies = [
[[package]]
name = "zed_extension_api"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0729d50b4ca0a7e28e590bbe32e3ca0194d97ef654961451a424c661a366fca0"
dependencies = [
"serde",
"serde_json",
@@ -21500,9 +21512,7 @@ dependencies = [
[[package]]
name = "zed_extension_api"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0729d50b4ca0a7e28e590bbe32e3ca0194d97ef654961451a424c661a366fca0"
version = "0.8.0"
dependencies = [
"serde",
"serde_json",
@@ -21520,7 +21530,7 @@ dependencies = [
name = "zed_html"
version = "0.2.3"
dependencies = [
"zed_extension_api 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)",
"zed_extension_api 0.7.0",
]
[[package]]
@@ -21534,7 +21544,7 @@ dependencies = [
name = "zed_test_extension"
version = "0.1.0"
dependencies = [
"zed_extension_api 0.7.0",
"zed_extension_api 0.8.0",
]
[[package]]
@@ -21678,6 +21688,7 @@ dependencies = [
"collections",
"command_palette_hooks",
"copilot",
"credentials_provider",
"ctor",
"db",
"edit_prediction",

View File

@@ -59,6 +59,7 @@ members = [
"crates/zeta2_tools",
"crates/editor",
"crates/eval",
"crates/eval_utils",
"crates/explorer_command_injector",
"crates/extension",
"crates/extension_api",
@@ -288,6 +289,7 @@ deepseek = { path = "crates/deepseek" }
derive_refineable = { path = "crates/refineable/derive_refineable" }
diagnostics = { path = "crates/diagnostics" }
editor = { path = "crates/editor" }
eval_utils = { path = "crates/eval_utils" }
extension = { path = "crates/extension" }
extension_host = { path = "crates/extension_host" }
extensions_ui = { path = "crates/extensions_ui" }
@@ -439,7 +441,7 @@ zlog_settings = { path = "crates/zlog_settings" }
# External crates
#
agent-client-protocol = { version = "0.7.0", features = ["unstable"] }
agent-client-protocol = { version = "=0.8.0", features = ["unstable"] }
aho-corasick = "1.1"
alacritty_terminal = "0.25.1-rc1"
any_vec = "0.14"
@@ -503,12 +505,12 @@ ec4rs = "1.1"
emojis = "0.6.1"
env_logger = "0.11"
exec = "0.3.1"
fancy-regex = "0.14.0"
fancy-regex = "0.16.0"
fork = "0.4.0"
futures = "0.3"
futures-batch = "0.6.1"
futures-lite = "1.13"
gh-workflow = { git = "https://github.com/zed-industries/gh-workflow", rev = "e5f883040530b4df36437f140084ee5cc7c1c9be" }
gh-workflow = { git = "https://github.com/zed-industries/gh-workflow", rev = "09acfdf2bd5c1d6254abefd609c808ff73547b2c" }
git2 = { version = "0.20.1", default-features = false }
globset = "0.4"
handlebars = "4.3"
@@ -529,7 +531,7 @@ indoc = "2"
inventory = "0.3.19"
itertools = "0.14.0"
json_dotpath = "1.1"
jsonschema = "0.30.0"
jsonschema = "0.37.0"
jsonwebtoken = "9.3"
jupyter-protocol = "0.10.0"
jupyter-websocket-client = "0.15.0"
@@ -639,6 +641,7 @@ serde_urlencoded = "0.7"
sha2 = "0.10"
shellexpand = "2.1.0"
shlex = "1.3.0"
similar = "2.6"
simplelog = "0.12.2"
slotmap = "1.0.6"
smallvec = { version = "1.6", features = ["union"] }
@@ -655,7 +658,7 @@ sysinfo = "0.37.0"
take-until = "0.2.0"
tempfile = "3.20.0"
thiserror = "2.0.12"
tiktoken-rs = { git = "https://github.com/zed-industries/tiktoken-rs", rev = "7249f999c5fdf9bf3cc5c288c964454e4dac0c00" }
tiktoken-rs = { git = "https://github.com/zed-industries/tiktoken-rs", rev = "2570c4387a8505fb8f1d3f3557454b474f1e8271" }
time = { version = "0.3", features = [
"macros",
"parsing",
@@ -671,7 +674,7 @@ toml = "0.8"
toml_edit = { version = "0.22", default-features = false, features = ["display", "parse", "serde"] }
tower-http = "0.4.4"
tree-sitter = { version = "0.25.10", features = ["wasm"] }
tree-sitter-bash = "0.25.0"
tree-sitter-bash = "0.25.1"
tree-sitter-c = "0.23"
tree-sitter-cpp = { git = "https://github.com/tree-sitter/tree-sitter-cpp", rev = "5cb9b693cfd7bfacab1d9ff4acac1a4150700609" }
tree-sitter-css = "0.23"

View File

@@ -53,6 +53,10 @@ extension
git
= @cole-miller
= @danilo-leal
= @dvdsk
= @kubkon
= @Anthony-Eid
= @cameron1024
gpui
= @Anthony-Eid

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2" d="M14 11.333A6 6 0 0 0 4 6.867l-1 .9"/><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.333" d="M2 4.667v4h4"/><path fill="#000" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2" d="M8 12a.667.667 0 1 0 0-1.333A.667.667 0 0 0 8 12Z"/></svg>

Before

Width:  |  Height:  |  Size: 467 B

View File

@@ -1 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2" d="M3.333 10 8 14.667 12.667 10M8 5.333v9.334"/><path fill="#000" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2" d="M8 2.667a.667.667 0 1 0 0-1.334.667.667 0 0 0 0 1.334Z"/></svg>
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M2 13H5" stroke="#C6CAD0" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M11 13H14" stroke="#C6CAD0" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M11.5 8.5L8 12M8 12L4.5 8.5M8 12L8 3" stroke="#C6CAD0" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 374 B

After

Width:  |  Height:  |  Size: 443 B

View File

@@ -1 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2" d="M3.333 6 8 1.333 12.667 6M8 10.667V1.333"/><path fill="#000" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2" d="M8 13.333a.667.667 0 1 1 0 1.334.667.667 0 0 1 0-1.334Z"/></svg>
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M4.5 6.5L8 3M8 3L11.5 6.5M8 3V12" stroke="#C6CAD0" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M2 13H5" stroke="#C6CAD0" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M11 13H14" stroke="#C6CAD0" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 373 B

After

Width:  |  Height:  |  Size: 439 B

View File

@@ -1 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2" d="M2 11.333a6 6 0 0 1 10-4.466l1 .9"/><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.333" d="M14 4.667v4h-4"/><path fill="#000" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2" d="M8 12a.667.667 0 1 1 0-1.333A.667.667 0 0 1 8 12Z"/></svg>
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M2 11.333C2.00118 10.1752 2.33729 9.04258 2.96777 8.07159C3.59826 7.10059 4.49621 6.33274 5.55331 5.86064C6.61041 5.38853 7.78152 5.23235 8.9254 5.41091C10.0693 5.58947 11.1371 6.09516 12 6.86698L13 7.76698" stroke="#C6CAD0" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M14 4.66699V8.66699H10" stroke="#C6CAD0" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M7 13H10" stroke="#C6CAD0" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 468 B

After

Width:  |  Height:  |  Size: 627 B

View File

@@ -49,7 +49,8 @@
"ctrl-cmd-f": "zed::ToggleFullScreen",
"ctrl-cmd-z": "edit_prediction::RateCompletions",
"ctrl-cmd-i": "edit_prediction::ToggleMenu",
"ctrl-cmd-l": "lsp_tool::ToggleMenu"
"ctrl-cmd-l": "lsp_tool::ToggleMenu",
"ctrl-cmd-c": "editor::DisplayCursorNames"
}
},
{
@@ -589,8 +590,7 @@
"cmd-.": "editor::ToggleCodeActions",
"cmd-k r": "editor::RevealInFileManager",
"cmd-k p": "editor::CopyPath",
"cmd-\\": "pane::SplitRight",
"ctrl-cmd-c": "editor::DisplayCursorNames"
"cmd-\\": "pane::SplitRight"
}
},
{
@@ -730,7 +730,8 @@
"context": "Workspace && debugger_running",
"use_key_equivalents": true,
"bindings": {
"f5": "zed::NoAction"
"f5": "zed::NoAction",
"f11": "debugger::StepInto"
}
},
{

View File

@@ -36,12 +36,12 @@
"shift-f5": "debugger::Stop",
"ctrl-shift-f5": "debugger::RerunSession",
"f6": "debugger::Pause",
"f7": "debugger::StepOver",
"ctrl-f11": "debugger::StepInto",
"f10": "debugger::StepOver",
"shift-f11": "debugger::StepOut",
"f11": "zed::ToggleFullScreen",
"ctrl-shift-i": "edit_prediction::ToggleMenu",
"shift-alt-l": "lsp_tool::ToggleMenu"
"shift-alt-l": "lsp_tool::ToggleMenu",
"ctrl-shift-alt-c": "editor::DisplayCursorNames"
}
},
{
@@ -117,7 +117,7 @@
"alt-g m": "git::OpenModifiedFiles",
"menu": "editor::OpenContextMenu",
"shift-f10": "editor::OpenContextMenu",
"ctrl-shift-e": "editor::ToggleEditPrediction",
"ctrl-alt-e": "editor::ToggleEditPrediction",
"f9": "editor::ToggleBreakpoint",
"shift-f9": "editor::EditLogBreakpoint"
}
@@ -215,7 +215,7 @@
"context": "ContextEditor > Editor",
"use_key_equivalents": true,
"bindings": {
"ctrl-enter": "assistant::Assist",
"ctrl-i": "assistant::Assist",
"ctrl-s": "workspace::Save",
"ctrl-shift-,": "assistant::InsertIntoEditor",
"shift-enter": "assistant::Split",
@@ -500,10 +500,7 @@
"ctrl-shift-l": "editor::SelectAllMatches", // Select all occurrences of current selection
"ctrl-f2": "editor::SelectAllMatches", // Select all occurrences of current word
"ctrl-d": ["editor::SelectNext", { "replace_newest": false }], // editor.action.addSelectionToNextFindMatch / find_under_expand
"ctrl-shift-down": ["editor::SelectNext", { "replace_newest": false }], // editor.action.addSelectionToNextFindMatch
"ctrl-shift-up": ["editor::SelectPrevious", { "replace_newest": false }], // editor.action.addSelectionToPreviousFindMatch
"ctrl-k ctrl-d": ["editor::SelectNext", { "replace_newest": true }], // editor.action.moveSelectionToNextFindMatch / find_under_expand_skip
"ctrl-k ctrl-shift-d": ["editor::SelectPrevious", { "replace_newest": true }], // editor.action.moveSelectionToPreviousFindMatch
"ctrl-k ctrl-i": "editor::Hover",
"ctrl-k ctrl-b": "editor::BlameHover",
"ctrl-/": ["editor::ToggleComments", { "advance_downwards": false }],
@@ -512,12 +509,8 @@
"f2": "editor::Rename",
"f12": "editor::GoToDefinition",
"alt-f12": "editor::GoToDefinitionSplit",
"ctrl-shift-f10": "editor::GoToDefinitionSplit",
"ctrl-f12": "editor::GoToImplementation",
"shift-f12": "editor::GoToTypeDefinition",
"ctrl-alt-f12": "editor::GoToTypeDefinitionSplit",
"shift-alt-f12": "editor::FindAllReferences",
"ctrl-m": "editor::MoveToEnclosingBracket", // from jetbrains
"ctrl-shift-\\": "editor::MoveToEnclosingBracket",
"ctrl-shift-[": "editor::Fold",
"ctrl-shift-]": "editor::UnfoldLines",
@@ -541,7 +534,6 @@
"ctrl-k r": "editor::RevealInFileManager",
"ctrl-k p": "editor::CopyPath",
"ctrl-\\": "pane::SplitRight",
"ctrl-shift-alt-c": "editor::DisplayCursorNames",
"alt-.": "editor::GoToHunk",
"alt-,": "editor::GoToPreviousHunk"
}
@@ -1124,7 +1116,7 @@
"shift-insert": "terminal::Paste",
"ctrl-v": "terminal::Paste",
"ctrl-shift-v": "terminal::Paste",
"ctrl-enter": "assistant::InlineAssist",
"ctrl-i": "assistant::InlineAssist",
"alt-b": ["terminal::SendText", "\u001bb"],
"alt-f": ["terminal::SendText", "\u001bf"],
"alt-.": ["terminal::SendText", "\u001b."],

View File

@@ -857,6 +857,8 @@
"ctrl-w shift-right": "workspace::SwapPaneRight",
"ctrl-w shift-up": "workspace::SwapPaneUp",
"ctrl-w shift-down": "workspace::SwapPaneDown",
"ctrl-w x": "workspace::SwapPaneAdjacent",
"ctrl-w ctrl-x": "workspace::SwapPaneAdjacent",
"ctrl-w shift-h": "workspace::MovePaneLeft",
"ctrl-w shift-l": "workspace::MovePaneRight",
"ctrl-w shift-k": "workspace::MovePaneUp",

View File

@@ -1100,13 +1100,22 @@
"preview_tabs": {
// Whether preview tabs should be enabled.
// Preview tabs allow you to open files in preview mode, where they close automatically
// when you switch to another file unless you explicitly pin them.
// when you open another preview tab.
// This is useful for quickly viewing files without cluttering your workspace.
"enabled": true,
// Whether to open tabs in preview mode when opened from the project panel with a single click.
"enable_preview_from_project_panel": true,
// Whether to open tabs in preview mode when selected from the file finder.
"enable_preview_from_file_finder": false,
// Whether a preview tab gets replaced when code navigation is used to navigate away from the tab.
"enable_preview_from_code_navigation": false
// Whether to open tabs in preview mode when opened from a multibuffer.
"enable_preview_from_multibuffer": true,
// Whether to open tabs in preview mode when code navigation is used to open a multibuffer.
"enable_preview_multibuffer_from_code_navigation": false,
// Whether to open tabs in preview mode when code navigation is used to open a single file.
"enable_preview_file_from_code_navigation": true,
// Whether to keep tabs in preview mode when code navigation is used to navigate away from them.
// If `enable_preview_file_from_code_navigation` or `enable_preview_multibuffer_from_code_navigation` is also true, the new tab may replace the existing one.
"enable_keep_preview_on_code_navigation": false
},
// Settings related to the file finder.
"file_finder": {
@@ -1209,6 +1218,13 @@
"tab_size": 4,
// What debuggers are preferred by default for all languages.
"debuggers": [],
// Whether to enable word diff highlighting in the editor.
//
// When enabled, changed words within modified lines are highlighted
// to show exactly what changed.
//
// Default: true
"word_diff_enabled": true,
// Control what info is collected by Zed.
"telemetry": {
// Send debug info like crash reports.

View File

@@ -45,6 +45,7 @@
"tab.inactive_background": "#1f2127ff",
"tab.active_background": "#0d1016ff",
"search.match_background": "#5ac2fe66",
"search.active_match_background": "#ea570166",
"panel.background": "#1f2127ff",
"panel.focused_border": "#5ac1feff",
"pane.focused_border": null,
@@ -436,6 +437,7 @@
"tab.inactive_background": "#ececedff",
"tab.active_background": "#fcfcfcff",
"search.match_background": "#3b9ee566",
"search.active_match_background": "#f88b3666",
"panel.background": "#ececedff",
"panel.focused_border": "#3b9ee5ff",
"pane.focused_border": null,
@@ -827,6 +829,7 @@
"tab.inactive_background": "#353944ff",
"tab.active_background": "#242835ff",
"search.match_background": "#73cffe66",
"search.active_match_background": "#fd722b66",
"panel.background": "#353944ff",
"panel.focused_border": null,
"pane.focused_border": null,

View File

@@ -46,6 +46,7 @@
"tab.inactive_background": "#3a3735ff",
"tab.active_background": "#282828ff",
"search.match_background": "#83a59866",
"search.active_match_background": "#c09f3f66",
"panel.background": "#3a3735ff",
"panel.focused_border": "#83a598ff",
"pane.focused_border": null,
@@ -452,6 +453,7 @@
"tab.inactive_background": "#393634ff",
"tab.active_background": "#1d2021ff",
"search.match_background": "#83a59866",
"search.active_match_background": "#c9653666",
"panel.background": "#393634ff",
"panel.focused_border": "#83a598ff",
"pane.focused_border": null,
@@ -858,6 +860,7 @@
"tab.inactive_background": "#3b3735ff",
"tab.active_background": "#32302fff",
"search.match_background": "#83a59866",
"search.active_match_background": "#aea85166",
"panel.background": "#3b3735ff",
"panel.focused_border": null,
"pane.focused_border": null,
@@ -1264,6 +1267,7 @@
"tab.inactive_background": "#ecddb4ff",
"tab.active_background": "#fbf1c7ff",
"search.match_background": "#0b667866",
"search.active_match_background": "#ba2d1166",
"panel.background": "#ecddb4ff",
"panel.focused_border": null,
"pane.focused_border": null,
@@ -1670,6 +1674,7 @@
"tab.inactive_background": "#ecddb5ff",
"tab.active_background": "#f9f5d7ff",
"search.match_background": "#0b667866",
"search.active_match_background": "#dc351466",
"panel.background": "#ecddb5ff",
"panel.focused_border": null,
"pane.focused_border": null,
@@ -2076,6 +2081,7 @@
"tab.inactive_background": "#ecdcb3ff",
"tab.active_background": "#f2e5bcff",
"search.match_background": "#0b667866",
"search.active_match_background": "#d7331466",
"panel.background": "#ecdcb3ff",
"panel.focused_border": null,
"pane.focused_border": null,

View File

@@ -45,6 +45,7 @@
"tab.inactive_background": "#2f343eff",
"tab.active_background": "#282c33ff",
"search.match_background": "#74ade866",
"search.active_match_background": "#e8af7466",
"panel.background": "#2f343eff",
"panel.focused_border": null,
"pane.focused_border": null,
@@ -98,6 +99,8 @@
"link_text.hover": "#74ade8ff",
"version_control.added": "#27a657ff",
"version_control.modified": "#d3b020ff",
"version_control.word_added": "#2EA04859",
"version_control.word_deleted": "#78081BCC",
"version_control.deleted": "#e06c76ff",
"version_control.conflict_marker.ours": "#a1c1811a",
"version_control.conflict_marker.theirs": "#74ade81a",
@@ -446,6 +449,7 @@
"tab.inactive_background": "#ebebecff",
"tab.active_background": "#fafafaff",
"search.match_background": "#5c79e266",
"search.active_match_background": "#d0a92366",
"panel.background": "#ebebecff",
"panel.focused_border": null,
"pane.focused_border": null,
@@ -499,6 +503,8 @@
"link_text.hover": "#5c78e2ff",
"version_control.added": "#27a657ff",
"version_control.modified": "#d3b020ff",
"version_control.word_added": "#2EA04859",
"version_control.word_deleted": "#F85149CC",
"version_control.deleted": "#e06c76ff",
"conflict": "#a48819ff",
"conflict.background": "#faf2e6ff",

View File

@@ -201,17 +201,19 @@ impl ToolCall {
};
let mut content = Vec::with_capacity(tool_call.content.len());
for item in tool_call.content {
content.push(ToolCallContent::from_acp(
if let Some(item) = ToolCallContent::from_acp(
item,
language_registry.clone(),
path_style,
terminals,
cx,
)?);
)? {
content.push(item);
}
}
let result = Self {
id: tool_call.id,
id: tool_call.tool_call_id,
label: cx
.new(|cx| Markdown::new(title.into(), Some(language_registry.clone()), None, cx)),
kind: tool_call.kind,
@@ -241,6 +243,7 @@ impl ToolCall {
locations,
raw_input,
raw_output,
..
} = fields;
if let Some(kind) = kind {
@@ -262,21 +265,29 @@ impl ToolCall {
}
if let Some(content) = content {
let new_content_len = content.len();
let mut new_content_len = content.len();
let mut content = content.into_iter();
// Reuse existing content if we can
for (old, new) in self.content.iter_mut().zip(content.by_ref()) {
old.update_from_acp(new, language_registry.clone(), path_style, terminals, cx)?;
let valid_content =
old.update_from_acp(new, language_registry.clone(), path_style, terminals, cx)?;
if !valid_content {
new_content_len -= 1;
}
}
for new in content {
self.content.push(ToolCallContent::from_acp(
if let Some(new) = ToolCallContent::from_acp(
new,
language_registry.clone(),
path_style,
terminals,
cx,
)?)
)? {
self.content.push(new);
} else {
new_content_len -= 1;
}
}
self.content.truncate(new_content_len);
}
@@ -425,6 +436,7 @@ impl From<acp::ToolCallStatus> for ToolCallStatus {
acp::ToolCallStatus::InProgress => Self::InProgress,
acp::ToolCallStatus::Completed => Self::Completed,
acp::ToolCallStatus::Failed => Self::Failed,
_ => Self::Pending,
}
}
}
@@ -537,7 +549,7 @@ impl ContentBlock {
..
}) => Self::resource_link_md(&uri, path_style),
acp::ContentBlock::Image(image) => Self::image_md(&image),
acp::ContentBlock::Audio(_) | acp::ContentBlock::Resource(_) => String::new(),
_ => String::new(),
}
}
@@ -591,15 +603,17 @@ impl ToolCallContent {
path_style: PathStyle,
terminals: &HashMap<acp::TerminalId, Entity<Terminal>>,
cx: &mut App,
) -> Result<Self> {
) -> Result<Option<Self>> {
match content {
acp::ToolCallContent::Content { content } => Ok(Self::ContentBlock(ContentBlock::new(
content,
&language_registry,
path_style,
cx,
))),
acp::ToolCallContent::Diff { diff } => Ok(Self::Diff(cx.new(|cx| {
acp::ToolCallContent::Content(acp::Content { content, .. }) => {
Ok(Some(Self::ContentBlock(ContentBlock::new(
content,
&language_registry,
path_style,
cx,
))))
}
acp::ToolCallContent::Diff(diff) => Ok(Some(Self::Diff(cx.new(|cx| {
Diff::finalized(
diff.path.to_string_lossy().into_owned(),
diff.old_text,
@@ -607,12 +621,13 @@ impl ToolCallContent {
language_registry,
cx,
)
}))),
acp::ToolCallContent::Terminal { terminal_id } => terminals
})))),
acp::ToolCallContent::Terminal(acp::Terminal { terminal_id, .. }) => terminals
.get(&terminal_id)
.cloned()
.map(Self::Terminal)
.map(|terminal| Some(Self::Terminal(terminal)))
.ok_or_else(|| anyhow::anyhow!("Terminal with id `{}` not found", terminal_id)),
_ => Ok(None),
}
}
@@ -623,9 +638,9 @@ impl ToolCallContent {
path_style: PathStyle,
terminals: &HashMap<acp::TerminalId, Entity<Terminal>>,
cx: &mut App,
) -> Result<()> {
) -> Result<bool> {
let needs_update = match (&self, &new) {
(Self::Diff(old_diff), acp::ToolCallContent::Diff { diff: new_diff }) => {
(Self::Diff(old_diff), acp::ToolCallContent::Diff(new_diff)) => {
old_diff.read(cx).needs_update(
new_diff.old_text.as_deref().unwrap_or(""),
&new_diff.new_text,
@@ -635,10 +650,14 @@ impl ToolCallContent {
_ => true,
};
if needs_update {
*self = Self::from_acp(new, language_registry, path_style, terminals, cx)?;
if let Some(update) = Self::from_acp(new, language_registry, path_style, terminals, cx)? {
if needs_update {
*self = update;
}
Ok(true)
} else {
Ok(false)
}
Ok(())
}
pub fn to_markdown(&self, cx: &App) -> String {
@@ -660,7 +679,7 @@ pub enum ToolCallUpdate {
impl ToolCallUpdate {
fn id(&self) -> &acp::ToolCallId {
match self {
Self::UpdateFields(update) => &update.id,
Self::UpdateFields(update) => &update.tool_call_id,
Self::UpdateDiff(diff) => &diff.id,
Self::UpdateTerminal(terminal) => &terminal.id,
}
@@ -732,6 +751,7 @@ impl Plan {
acp::PlanEntryStatus::Completed => {
stats.completed += 1;
}
_ => {}
}
}
@@ -1154,6 +1174,7 @@ impl AcpThread {
current_mode_id,
..
}) => cx.emit(AcpThreadEvent::ModeUpdated(current_mode_id)),
_ => {}
}
Ok(())
}
@@ -1287,11 +1308,7 @@ impl AcpThread {
label: cx.new(|cx| Markdown::new("Tool call not found".into(), None, None, cx)),
kind: acp::ToolKind::Fetch,
content: vec![ToolCallContent::ContentBlock(ContentBlock::new(
acp::ContentBlock::Text(acp::TextContent {
text: "Tool call not found".to_string(),
annotations: None,
meta: None,
}),
"Tool call not found".into(),
&languages,
path_style,
cx,
@@ -1315,7 +1332,7 @@ impl AcpThread {
let location_updated = update.fields.locations.is_some();
call.update_fields(update.fields, languages, path_style, &self.terminals, cx)?;
if location_updated {
self.resolve_locations(update.id, cx);
self.resolve_locations(update.tool_call_id, cx);
}
}
ToolCallUpdate::UpdateDiff(update) => {
@@ -1353,7 +1370,7 @@ impl AcpThread {
) -> Result<(), acp::Error> {
let language_registry = self.project.read(cx).languages().clone();
let path_style = self.project.read(cx).path_style(cx);
let id = update.id.clone();
let id = update.tool_call_id.clone();
let agent = self.connection().telemetry_id();
let session = self.session_id();
@@ -1518,16 +1535,16 @@ impl AcpThread {
// some tools would (incorrectly) continue to auto-accept.
if let Some(allow_once_option) = options.iter().find_map(|option| {
if matches!(option.kind, acp::PermissionOptionKind::AllowOnce) {
Some(option.id.clone())
Some(option.option_id.clone())
} else {
None
}
}) {
self.upsert_tool_call_inner(tool_call, ToolCallStatus::Pending, cx)?;
return Ok(async {
acp::RequestPermissionOutcome::Selected {
option_id: allow_once_option,
}
acp::RequestPermissionOutcome::Selected(acp::SelectedPermissionOutcome::new(
allow_once_option,
))
}
.boxed());
}
@@ -1543,7 +1560,9 @@ impl AcpThread {
let fut = async {
match rx.await {
Ok(option) => acp::RequestPermissionOutcome::Selected { option_id: option },
Ok(option) => acp::RequestPermissionOutcome::Selected(
acp::SelectedPermissionOutcome::new(option),
),
Err(oneshot::Canceled) => acp::RequestPermissionOutcome::Cancelled,
}
}
@@ -1570,6 +1589,7 @@ impl AcpThread {
acp::PermissionOptionKind::AllowOnce | acp::PermissionOptionKind::AllowAlways => {
ToolCallStatus::InProgress
}
_ => ToolCallStatus::InProgress,
};
let curr_status = mem::replace(&mut call.status, new_status);
@@ -1648,14 +1668,7 @@ impl AcpThread {
message: &str,
cx: &mut Context<Self>,
) -> BoxFuture<'static, Result<()>> {
self.send(
vec![acp::ContentBlock::Text(acp::TextContent {
text: message.to_string(),
annotations: None,
meta: None,
})],
cx,
)
self.send(vec![message.into()], cx)
}
pub fn send(
@@ -1669,11 +1682,7 @@ impl AcpThread {
self.project.read(cx).path_style(cx),
cx,
);
let request = acp::PromptRequest {
prompt: message.clone(),
session_id: self.session_id.clone(),
meta: None,
};
let request = acp::PromptRequest::new(self.session_id.clone(), message.clone());
let git_store = self.project.read(cx).git_store().clone();
let message_id = if self.connection.truncate(&self.session_id, cx).is_some() {
@@ -1765,7 +1774,7 @@ impl AcpThread {
result,
Ok(Ok(acp::PromptResponse {
stop_reason: acp::StopReason::Cancelled,
meta: None,
..
}))
);
@@ -1781,7 +1790,7 @@ impl AcpThread {
// Handle refusal - distinguish between user prompt and tool call refusals
if let Ok(Ok(acp::PromptResponse {
stop_reason: acp::StopReason::Refusal,
meta: _,
..
})) = result
{
if let Some((user_msg_ix, _)) = this.last_user_message() {
@@ -2017,7 +2026,7 @@ impl AcpThread {
})?;
Ok(project.open_buffer(path, cx))
})
.map_err(|e| acp::Error::internal_error().with_data(e.to_string()))
.map_err(|e| acp::Error::internal_error().data(e.to_string()))
.flatten()?;
let buffer = load.await?;
@@ -2050,7 +2059,7 @@ impl AcpThread {
let start_position = Point::new(line, 0);
if start_position > max_point {
return Err(acp::Error::invalid_params().with_data(format!(
return Err(acp::Error::invalid_params().data(format!(
"Attempting to read beyond the end of the file, line {}:{}",
max_point.row + 1,
max_point.column
@@ -2202,7 +2211,7 @@ impl AcpThread {
let language_registry = project.read(cx).languages().clone();
let is_windows = project.read(cx).path_style(cx).is_windows();
let terminal_id = acp::TerminalId(Uuid::new_v4().to_string().into());
let terminal_id = acp::TerminalId::new(Uuid::new_v4().to_string());
let terminal_task = cx.spawn({
let terminal_id = terminal_id.clone();
async move |_this, cx| {
@@ -2412,7 +2421,7 @@ mod tests {
.await
.unwrap();
let terminal_id = acp::TerminalId(uuid::Uuid::new_v4().to_string().into());
let terminal_id = acp::TerminalId::new(uuid::Uuid::new_v4().to_string());
// Send Output BEFORE Created - should be buffered by acp_thread
thread.update(cx, |thread, cx| {
@@ -2474,7 +2483,7 @@ mod tests {
.await
.unwrap();
let terminal_id = acp::TerminalId(uuid::Uuid::new_v4().to_string().into());
let terminal_id = acp::TerminalId::new(uuid::Uuid::new_v4().to_string());
// Send Output BEFORE Created
thread.update(cx, |thread, cx| {
@@ -2492,11 +2501,7 @@ mod tests {
thread.on_terminal_provider_event(
TerminalProviderEvent::Exit {
terminal_id: terminal_id.clone(),
status: acp::TerminalExitStatus {
exit_code: Some(0),
signal: None,
meta: None,
},
status: acp::TerminalExitStatus::new().exit_code(0),
},
cx,
);
@@ -2553,15 +2558,7 @@ mod tests {
// Test creating a new user message
thread.update(cx, |thread, cx| {
thread.push_user_content_block(
None,
acp::ContentBlock::Text(acp::TextContent {
annotations: None,
text: "Hello, ".to_string(),
meta: None,
}),
cx,
);
thread.push_user_content_block(None, "Hello, ".into(), cx);
});
thread.update(cx, |thread, cx| {
@@ -2577,15 +2574,7 @@ mod tests {
// Test appending to existing user message
let message_1_id = UserMessageId::new();
thread.update(cx, |thread, cx| {
thread.push_user_content_block(
Some(message_1_id.clone()),
acp::ContentBlock::Text(acp::TextContent {
annotations: None,
text: "world!".to_string(),
meta: None,
}),
cx,
);
thread.push_user_content_block(Some(message_1_id.clone()), "world!".into(), cx);
});
thread.update(cx, |thread, cx| {
@@ -2600,26 +2589,14 @@ mod tests {
// Test creating new user message after assistant message
thread.update(cx, |thread, cx| {
thread.push_assistant_content_block(
acp::ContentBlock::Text(acp::TextContent {
annotations: None,
text: "Assistant response".to_string(),
meta: None,
}),
false,
cx,
);
thread.push_assistant_content_block("Assistant response".into(), false, cx);
});
let message_2_id = UserMessageId::new();
thread.update(cx, |thread, cx| {
thread.push_user_content_block(
Some(message_2_id.clone()),
acp::ContentBlock::Text(acp::TextContent {
annotations: None,
text: "New user message".to_string(),
meta: None,
}),
"New user message".into(),
cx,
);
});
@@ -2647,27 +2624,22 @@ mod tests {
thread.update(&mut cx, |thread, cx| {
thread
.handle_session_update(
acp::SessionUpdate::AgentThoughtChunk(acp::ContentChunk {
content: "Thinking ".into(),
meta: None,
}),
acp::SessionUpdate::AgentThoughtChunk(acp::ContentChunk::new(
"Thinking ".into(),
)),
cx,
)
.unwrap();
thread
.handle_session_update(
acp::SessionUpdate::AgentThoughtChunk(acp::ContentChunk {
content: "hard!".into(),
meta: None,
}),
acp::SessionUpdate::AgentThoughtChunk(acp::ContentChunk::new(
"hard!".into(),
)),
cx,
)
.unwrap();
})?;
Ok(acp::PromptResponse {
stop_reason: acp::StopReason::EndTurn,
meta: None,
})
Ok(acp::PromptResponse::new(acp::StopReason::EndTurn))
}
.boxed_local()
},
@@ -2735,10 +2707,7 @@ mod tests {
.unwrap()
.await
.unwrap();
Ok(acp::PromptResponse {
stop_reason: acp::StopReason::EndTurn,
meta: None,
})
Ok(acp::PromptResponse::new(acp::StopReason::EndTurn))
}
.boxed_local()
},
@@ -2969,7 +2938,7 @@ mod tests {
let fs = FakeFs::new(cx.executor());
let project = Project::test(fs, [], cx).await;
let id = acp::ToolCallId("test".into());
let id = acp::ToolCallId::new("test");
let connection = Rc::new(FakeAgentConnection::new().on_user_message({
let id = id.clone();
@@ -2979,26 +2948,17 @@ mod tests {
thread
.update(&mut cx, |thread, cx| {
thread.handle_session_update(
acp::SessionUpdate::ToolCall(acp::ToolCall {
id: id.clone(),
title: "Label".into(),
kind: acp::ToolKind::Fetch,
status: acp::ToolCallStatus::InProgress,
content: vec![],
locations: vec![],
raw_input: None,
raw_output: None,
meta: None,
}),
acp::SessionUpdate::ToolCall(
acp::ToolCall::new(id.clone(), "Label")
.kind(acp::ToolKind::Fetch)
.status(acp::ToolCallStatus::InProgress),
),
cx,
)
})
.unwrap()
.unwrap();
Ok(acp::PromptResponse {
stop_reason: acp::StopReason::EndTurn,
meta: None,
})
Ok(acp::PromptResponse::new(acp::StopReason::EndTurn))
}
.boxed_local()
}
@@ -3040,14 +3000,10 @@ mod tests {
thread
.update(cx, |thread, cx| {
thread.handle_session_update(
acp::SessionUpdate::ToolCallUpdate(acp::ToolCallUpdate {
acp::SessionUpdate::ToolCallUpdate(acp::ToolCallUpdate::new(
id,
fields: acp::ToolCallUpdateFields {
status: Some(acp::ToolCallStatus::Completed),
..Default::default()
},
meta: None,
}),
acp::ToolCallUpdateFields::new().status(acp::ToolCallStatus::Completed),
)),
cx,
)
})
@@ -3079,33 +3035,21 @@ mod tests {
thread
.update(&mut cx, |thread, cx| {
thread.handle_session_update(
acp::SessionUpdate::ToolCall(acp::ToolCall {
id: acp::ToolCallId("test".into()),
title: "Label".into(),
kind: acp::ToolKind::Edit,
status: acp::ToolCallStatus::Completed,
content: vec![acp::ToolCallContent::Diff {
diff: acp::Diff {
path: "/test/test.txt".into(),
old_text: None,
new_text: "foo".into(),
meta: None,
},
}],
locations: vec![],
raw_input: None,
raw_output: None,
meta: None,
}),
acp::SessionUpdate::ToolCall(
acp::ToolCall::new("test", "Label")
.kind(acp::ToolKind::Edit)
.status(acp::ToolCallStatus::Completed)
.content(vec![acp::ToolCallContent::Diff(acp::Diff::new(
"/test/test.txt",
"foo",
))]),
),
cx,
)
})
.unwrap()
.unwrap();
Ok(acp::PromptResponse {
stop_reason: acp::StopReason::EndTurn,
meta: None,
})
Ok(acp::PromptResponse::new(acp::StopReason::EndTurn))
}
.boxed_local()
}
@@ -3158,18 +3102,14 @@ mod tests {
thread.update(&mut cx, |thread, cx| {
thread
.handle_session_update(
acp::SessionUpdate::AgentMessageChunk(acp::ContentChunk {
content: content.text.to_uppercase().into(),
meta: None,
}),
acp::SessionUpdate::AgentMessageChunk(acp::ContentChunk::new(
content.text.to_uppercase().into(),
)),
cx,
)
.unwrap();
})?;
Ok(acp::PromptResponse {
stop_reason: acp::StopReason::EndTurn,
meta: None,
})
Ok(acp::PromptResponse::new(acp::StopReason::EndTurn))
}
.boxed_local()
}
@@ -3325,34 +3265,22 @@ mod tests {
thread.update(&mut cx, |thread, cx| {
thread
.handle_session_update(
acp::SessionUpdate::ToolCall(acp::ToolCall {
id: acp::ToolCallId("tool1".into()),
title: "Test Tool".into(),
kind: acp::ToolKind::Fetch,
status: acp::ToolCallStatus::Completed,
content: vec![],
locations: vec![],
raw_input: Some(serde_json::json!({"query": "test"})),
raw_output: Some(
serde_json::json!({"result": "inappropriate content"}),
),
meta: None,
}),
acp::SessionUpdate::ToolCall(
acp::ToolCall::new("tool1", "Test Tool")
.kind(acp::ToolKind::Fetch)
.status(acp::ToolCallStatus::Completed)
.raw_input(serde_json::json!({"query": "test"}))
.raw_output(serde_json::json!({"result": "inappropriate content"})),
),
cx,
)
.unwrap();
})?;
// Now return refusal because of the tool result
Ok(acp::PromptResponse {
stop_reason: acp::StopReason::Refusal,
meta: None,
})
Ok(acp::PromptResponse::new(acp::StopReason::Refusal))
} else {
Ok(acp::PromptResponse {
stop_reason: acp::StopReason::EndTurn,
meta: None,
})
Ok(acp::PromptResponse::new(acp::StopReason::EndTurn))
}
}
.boxed_local()
@@ -3380,16 +3308,7 @@ mod tests {
});
// Send a user message - this will trigger tool call and then refusal
let send_task = thread.update(cx, |thread, cx| {
thread.send(
vec![acp::ContentBlock::Text(acp::TextContent {
text: "Hello".into(),
annotations: None,
meta: None,
})],
cx,
)
});
let send_task = thread.update(cx, |thread, cx| thread.send(vec!["Hello".into()], cx));
cx.background_executor.spawn(send_task).detach();
cx.run_until_parked();
@@ -3435,21 +3354,11 @@ mod tests {
let refuse_next = refuse_next.clone();
move |_request, _thread, _cx| {
if refuse_next.load(SeqCst) {
async move {
Ok(acp::PromptResponse {
stop_reason: acp::StopReason::Refusal,
meta: None,
})
}
.boxed_local()
async move { Ok(acp::PromptResponse::new(acp::StopReason::Refusal)) }
.boxed_local()
} else {
async move {
Ok(acp::PromptResponse {
stop_reason: acp::StopReason::EndTurn,
meta: None,
})
}
.boxed_local()
async move { Ok(acp::PromptResponse::new(acp::StopReason::EndTurn)) }
.boxed_local()
}
}
}));
@@ -3506,10 +3415,7 @@ mod tests {
let refuse_next = refuse_next.clone();
async move {
if refuse_next.load(SeqCst) {
return Ok(acp::PromptResponse {
stop_reason: acp::StopReason::Refusal,
meta: None,
});
return Ok(acp::PromptResponse::new(acp::StopReason::Refusal));
}
let acp::ContentBlock::Text(content) = &request.prompt[0] else {
@@ -3518,18 +3424,14 @@ mod tests {
thread.update(&mut cx, |thread, cx| {
thread
.handle_session_update(
acp::SessionUpdate::AgentMessageChunk(acp::ContentChunk {
content: content.text.to_uppercase().into(),
meta: None,
}),
acp::SessionUpdate::AgentMessageChunk(acp::ContentChunk::new(
content.text.to_uppercase().into(),
)),
cx,
)
.unwrap();
})?;
Ok(acp::PromptResponse {
stop_reason: acp::StopReason::EndTurn,
meta: None,
})
Ok(acp::PromptResponse::new(acp::StopReason::EndTurn))
}
.boxed_local()
}
@@ -3668,13 +3570,12 @@ mod tests {
_cwd: &Path,
cx: &mut App,
) -> Task<gpui::Result<Entity<AcpThread>>> {
let session_id = acp::SessionId(
let session_id = acp::SessionId::new(
rand::rng()
.sample_iter(&distr::Alphanumeric)
.take(7)
.map(char::from)
.collect::<String>()
.into(),
.collect::<String>(),
);
let action_log = cx.new(|_| ActionLog::new(project.clone()));
let thread = cx.new(|cx| {
@@ -3684,12 +3585,12 @@ mod tests {
project,
action_log,
session_id.clone(),
watch::Receiver::constant(acp::PromptCapabilities {
image: true,
audio: true,
embedded_context: true,
meta: None,
}),
watch::Receiver::constant(
acp::PromptCapabilities::new()
.image(true)
.audio(true)
.embedded_context(true),
),
cx,
)
});
@@ -3718,10 +3619,7 @@ mod tests {
let thread = thread.clone();
cx.spawn(async move |cx| handler(params, thread, cx.clone()).await)
} else {
Task::ready(Ok(acp::PromptResponse {
stop_reason: acp::StopReason::EndTurn,
meta: None,
}))
Task::ready(Ok(acp::PromptResponse::new(acp::StopReason::EndTurn)))
}
}
@@ -3776,17 +3674,13 @@ mod tests {
.unwrap();
// Try to update a tool call that doesn't exist
let nonexistent_id = acp::ToolCallId("nonexistent-tool-call".into());
let nonexistent_id = acp::ToolCallId::new("nonexistent-tool-call");
thread.update(cx, |thread, cx| {
let result = thread.handle_session_update(
acp::SessionUpdate::ToolCallUpdate(acp::ToolCallUpdate {
id: nonexistent_id.clone(),
fields: acp::ToolCallUpdateFields {
status: Some(acp::ToolCallStatus::Completed),
..Default::default()
},
meta: None,
}),
acp::SessionUpdate::ToolCallUpdate(acp::ToolCallUpdate::new(
nonexistent_id.clone(),
acp::ToolCallUpdateFields::new().status(acp::ToolCallStatus::Completed),
)),
cx,
);
@@ -3861,7 +3755,7 @@ mod tests {
.unwrap();
// Create 2 terminals BEFORE the checkpoint that have completed running
let terminal_id_1 = acp::TerminalId(uuid::Uuid::new_v4().to_string().into());
let terminal_id_1 = acp::TerminalId::new(uuid::Uuid::new_v4().to_string());
let mock_terminal_1 = cx.new(|cx| {
let builder = ::terminal::TerminalBuilder::new_display_only(
::terminal::terminal_settings::CursorShape::default(),
@@ -3900,17 +3794,13 @@ mod tests {
thread.on_terminal_provider_event(
TerminalProviderEvent::Exit {
terminal_id: terminal_id_1.clone(),
status: acp::TerminalExitStatus {
exit_code: Some(0),
signal: None,
meta: None,
},
status: acp::TerminalExitStatus::new().exit_code(0),
},
cx,
);
});
let terminal_id_2 = acp::TerminalId(uuid::Uuid::new_v4().to_string().into());
let terminal_id_2 = acp::TerminalId::new(uuid::Uuid::new_v4().to_string());
let mock_terminal_2 = cx.new(|cx| {
let builder = ::terminal::TerminalBuilder::new_display_only(
::terminal::terminal_settings::CursorShape::default(),
@@ -3949,11 +3839,7 @@ mod tests {
thread.on_terminal_provider_event(
TerminalProviderEvent::Exit {
terminal_id: terminal_id_2.clone(),
status: acp::TerminalExitStatus {
exit_code: Some(0),
signal: None,
meta: None,
},
status: acp::TerminalExitStatus::new().exit_code(0),
},
cx,
);
@@ -3973,7 +3859,7 @@ mod tests {
// Create a terminal AFTER the checkpoint we'll restore to.
// This simulates the AI agent starting a long-running terminal command.
let terminal_id = acp::TerminalId(uuid::Uuid::new_v4().to_string().into());
let terminal_id = acp::TerminalId::new(uuid::Uuid::new_v4().to_string());
let mock_terminal = cx.new(|cx| {
let builder = ::terminal::TerminalBuilder::new_display_only(
::terminal::terminal_settings::CursorShape::default(),
@@ -4015,21 +3901,15 @@ mod tests {
thread.update(cx, |thread, cx| {
thread
.handle_session_update(
acp::SessionUpdate::ToolCall(acp::ToolCall {
id: acp::ToolCallId("terminal-tool-1".into()),
title: "Running command".into(),
kind: acp::ToolKind::Execute,
status: acp::ToolCallStatus::InProgress,
content: vec![acp::ToolCallContent::Terminal {
terminal_id: terminal_id.clone(),
}],
locations: vec![],
raw_input: Some(
serde_json::json!({"command": "sleep 1000", "cd": "/test"}),
),
raw_output: None,
meta: None,
}),
acp::SessionUpdate::ToolCall(
acp::ToolCall::new("terminal-tool-1", "Running command")
.kind(acp::ToolKind::Execute)
.status(acp::ToolCallStatus::InProgress)
.content(vec![acp::ToolCallContent::Terminal(acp::Terminal::new(
terminal_id.clone(),
))])
.raw_input(serde_json::json!({"command": "sleep 1000", "cd": "/test"})),
),
cx,
)
.unwrap();

View File

@@ -336,7 +336,7 @@ mod test_support {
_cwd: &Path,
cx: &mut gpui::App,
) -> Task<gpui::Result<Entity<AcpThread>>> {
let session_id = acp::SessionId(self.sessions.lock().len().to_string().into());
let session_id = acp::SessionId::new(self.sessions.lock().len().to_string());
let action_log = cx.new(|_| ActionLog::new(project.clone()));
let thread = cx.new(|cx| {
AcpThread::new(
@@ -345,12 +345,12 @@ mod test_support {
project,
action_log,
session_id.clone(),
watch::Receiver::constant(acp::PromptCapabilities {
image: true,
audio: true,
embedded_context: true,
meta: None,
}),
watch::Receiver::constant(
acp::PromptCapabilities::new()
.image(true)
.audio(true)
.embedded_context(true),
),
cx,
)
});
@@ -389,10 +389,7 @@ mod test_support {
response_tx.replace(tx);
cx.spawn(async move |_| {
let stop_reason = rx.await?;
Ok(acp::PromptResponse {
stop_reason,
meta: None,
})
Ok(acp::PromptResponse::new(stop_reason))
})
} else {
for update in self.next_prompt_updates.lock().drain(..) {
@@ -400,7 +397,7 @@ mod test_support {
let update = update.clone();
let permission_request = if let acp::SessionUpdate::ToolCall(tool_call) =
&update
&& let Some(options) = self.permission_requests.get(&tool_call.id)
&& let Some(options) = self.permission_requests.get(&tool_call.tool_call_id)
{
Some((tool_call.clone(), options.clone()))
} else {
@@ -429,10 +426,7 @@ mod test_support {
cx.spawn(async move |_| {
try_join_all(tasks).await?;
Ok(acp::PromptResponse {
stop_reason: acp::StopReason::EndTurn,
meta: None,
})
Ok(acp::PromptResponse::new(acp::StopReason::EndTurn))
})
}
}

View File

@@ -108,7 +108,7 @@ impl MentionUri {
if let Some(thread_id) = path.strip_prefix("/agent/thread/") {
let name = single_query_param(&url, "name")?.context("Missing thread name")?;
Ok(Self::Thread {
id: acp::SessionId(thread_id.into()),
id: acp::SessionId::new(thread_id),
name,
})
} else if let Some(path) = path.strip_prefix("/agent/text-thread/") {

View File

@@ -75,11 +75,15 @@ impl Terminal {
let exit_status = exit_status.map(portable_pty::ExitStatus::from);
acp::TerminalExitStatus {
exit_code: exit_status.as_ref().map(|e| e.exit_code()),
signal: exit_status.and_then(|e| e.signal().map(Into::into)),
meta: None,
let mut status = acp::TerminalExitStatus::new();
if let Some(exit_status) = exit_status.as_ref() {
status = status.exit_code(exit_status.exit_code());
if let Some(signal) = exit_status.signal() {
status = status.signal(signal);
}
}
status
})
.shared(),
}
@@ -101,27 +105,23 @@ impl Terminal {
pub fn current_output(&self, cx: &App) -> acp::TerminalOutputResponse {
if let Some(output) = self.output.as_ref() {
let exit_status = output.exit_status.map(portable_pty::ExitStatus::from);
acp::TerminalOutputResponse {
output: output.content.clone(),
truncated: output.original_content_len > output.content.len(),
exit_status: Some(acp::TerminalExitStatus {
exit_code: exit_status.as_ref().map(|e| e.exit_code()),
signal: exit_status.and_then(|e| e.signal().map(Into::into)),
meta: None,
}),
meta: None,
let mut exit_status = acp::TerminalExitStatus::new();
if let Some(status) = output.exit_status.map(portable_pty::ExitStatus::from) {
exit_status = exit_status.exit_code(status.exit_code());
if let Some(signal) = status.signal() {
exit_status = exit_status.signal(signal);
}
}
acp::TerminalOutputResponse::new(
output.content.clone(),
output.original_content_len > output.content.len(),
)
.exit_status(exit_status)
} else {
let (current_content, original_len) = self.truncated_output(cx);
acp::TerminalOutputResponse {
truncated: current_content.len() < original_len,
output: current_content,
exit_status: None,
meta: None,
}
let truncated = current_content.len() < original_len;
acp::TerminalOutputResponse::new(current_content, truncated)
}
}

View File

@@ -83,6 +83,7 @@ ctor.workspace = true
db = { workspace = true, "features" = ["test-support"] }
editor = { workspace = true, "features" = ["test-support"] }
env_logger.workspace = true
eval_utils.workspace = true
fs = { workspace = true, "features" = ["test-support"] }
git = { workspace = true, "features" = ["test-support"] }
gpui = { workspace = true, "features" = ["test-support"] }

View File

@@ -170,7 +170,7 @@ impl LanguageModels {
}
fn model_id(model: &Arc<dyn LanguageModel>) -> acp::ModelId {
acp::ModelId(format!("{}/{}", model.provider_id().0, model.id().0).into())
acp::ModelId::new(format!("{}/{}", model.provider_id().0, model.id().0))
}
fn authenticate_all_language_model_providers(cx: &mut App) -> Task<()> {
@@ -789,28 +789,12 @@ impl NativeAgentConnection {
}
ThreadEvent::AgentText(text) => {
acp_thread.update(cx, |thread, cx| {
thread.push_assistant_content_block(
acp::ContentBlock::Text(acp::TextContent {
text,
annotations: None,
meta: None,
}),
false,
cx,
)
thread.push_assistant_content_block(text.into(), false, cx)
})?;
}
ThreadEvent::AgentThinking(text) => {
acp_thread.update(cx, |thread, cx| {
thread.push_assistant_content_block(
acp::ContentBlock::Text(acp::TextContent {
text,
annotations: None,
meta: None,
}),
true,
cx,
)
thread.push_assistant_content_block(text.into(), true, cx)
})?;
}
ThreadEvent::ToolCallAuthorization(ToolCallAuthorization {
@@ -824,8 +808,9 @@ impl NativeAgentConnection {
)
})??;
cx.background_spawn(async move {
if let acp::RequestPermissionOutcome::Selected { option_id } =
outcome_task.await
if let acp::RequestPermissionOutcome::Selected(
acp::SelectedPermissionOutcome { option_id, .. },
) = outcome_task.await
{
response
.send(option_id)
@@ -852,10 +837,7 @@ impl NativeAgentConnection {
}
ThreadEvent::Stop(stop_reason) => {
log::debug!("Assistant message complete: {:?}", stop_reason);
return Ok(acp::PromptResponse {
stop_reason,
meta: None,
});
return Ok(acp::PromptResponse::new(stop_reason));
}
}
}
@@ -867,10 +849,7 @@ impl NativeAgentConnection {
}
log::debug!("Response stream completed");
anyhow::Ok(acp::PromptResponse {
stop_reason: acp::StopReason::EndTurn,
meta: None,
})
anyhow::Ok(acp::PromptResponse::new(acp::StopReason::EndTurn))
})
}
}
@@ -1374,7 +1353,7 @@ mod internal_tests {
IndexMap::from_iter([(
AgentModelGroupName("Fake".into()),
vec![AgentModelInfo {
id: acp::ModelId("fake/fake".into()),
id: acp::ModelId::new("fake/fake"),
name: "Fake".into(),
description: None,
icon: Some(ui::IconName::ZedAssistant),
@@ -1435,7 +1414,7 @@ mod internal_tests {
// Select a model
let selector = connection.model_selector(&session_id).unwrap();
let model_id = acp::ModelId("fake/fake".into());
let model_id = acp::ModelId::new("fake/fake");
cx.update(|cx| selector.select_model(model_id.clone(), cx))
.await
.unwrap();
@@ -1521,20 +1500,14 @@ mod internal_tests {
thread.send(
vec![
"What does ".into(),
acp::ContentBlock::ResourceLink(acp::ResourceLink {
name: "b.md".into(),
uri: MentionUri::File {
acp::ContentBlock::ResourceLink(acp::ResourceLink::new(
"b.md",
MentionUri::File {
abs_path: path!("/a/b.md").into(),
}
.to_uri()
.to_string(),
annotations: None,
description: None,
mime_type: None,
size: None,
title: None,
meta: None,
}),
)),
" mean?".into(),
],
cx,

View File

@@ -366,7 +366,7 @@ impl ThreadsDatabase {
for (id, summary, updated_at) in rows {
threads.push(DbThreadMetadata {
id: acp::SessionId(id),
id: acp::SessionId::new(id),
title: summary.into(),
updated_at: DateTime::parse_from_rfc3339(&updated_at)?.with_timezone(&Utc),
});

View File

@@ -4,7 +4,7 @@ use crate::{
};
use Role::*;
use client::{Client, UserStore};
use collections::HashMap;
use eval_utils::{EvalOutput, EvalOutputProcessor, OutcomeKind};
use fs::FakeFs;
use futures::{FutureExt, future::LocalBoxFuture};
use gpui::{AppContext, TestAppContext, Timer};
@@ -20,16 +20,62 @@ use rand::prelude::*;
use reqwest_client::ReqwestClient;
use serde_json::json;
use std::{
cmp::Reverse,
fmt::{self, Display},
io::Write as _,
path::Path,
str::FromStr,
sync::mpsc,
time::Duration,
};
use util::path;
#[derive(Default, Clone, Debug)]
struct EditAgentOutputProcessor {
mismatched_tag_threshold: f32,
cumulative_tags: usize,
cumulative_mismatched_tags: usize,
eval_outputs: Vec<EvalOutput<EditEvalMetadata>>,
}
fn mismatched_tag_threshold(mismatched_tag_threshold: f32) -> EditAgentOutputProcessor {
EditAgentOutputProcessor {
mismatched_tag_threshold,
cumulative_tags: 0,
cumulative_mismatched_tags: 0,
eval_outputs: Vec::new(),
}
}
#[derive(Clone, Debug)]
struct EditEvalMetadata {
tags: usize,
mismatched_tags: usize,
}
impl EvalOutputProcessor for EditAgentOutputProcessor {
type Metadata = EditEvalMetadata;
fn process(&mut self, output: &EvalOutput<Self::Metadata>) {
if matches!(output.outcome, OutcomeKind::Passed | OutcomeKind::Failed) {
self.cumulative_mismatched_tags += output.metadata.mismatched_tags;
self.cumulative_tags += output.metadata.tags;
self.eval_outputs.push(output.clone());
}
}
fn assert(&mut self) {
let mismatched_tag_ratio =
self.cumulative_mismatched_tags as f32 / self.cumulative_tags as f32;
if mismatched_tag_ratio > self.mismatched_tag_threshold {
for eval_output in &self.eval_outputs {
println!("{}", eval_output.data);
}
panic!(
"Too many mismatched tags: {:?}",
self.cumulative_mismatched_tags
);
}
}
}
#[test]
#[cfg_attr(not(feature = "unit-eval"), ignore)]
fn eval_extract_handle_command_output() {
@@ -55,22 +101,19 @@ fn eval_extract_handle_command_output() {
include_str!("evals/fixtures/extract_handle_command_output/possible-07.diff"),
];
let edit_description = "Extract `handle_command_output` method from `run_git_blame`.";
eval(
100,
0.95,
0.05,
EvalInput::from_conversation(
eval_utils::eval(100, 0.95, mismatched_tag_threshold(0.05), move || {
run_eval(EvalInput::from_conversation(
vec![
message(
User,
[text(formatdoc! {"
Read the `{input_file_path}` file and extract a method in
the final stanza of `run_git_blame` to deal with command failures,
call it `handle_command_output` and take the std::process::Output as the only parameter.
Do not document the method and do not add any comments.
Read the `{input_file_path}` file and extract a method in
the final stanza of `run_git_blame` to deal with command failures,
call it `handle_command_output` and take the std::process::Output as the only parameter.
Do not document the method and do not add any comments.
Add it right next to `run_git_blame` and copy it verbatim from `run_git_blame`.
"})],
Add it right next to `run_git_blame` and copy it verbatim from `run_git_blame`.
"})],
),
message(
Assistant,
@@ -102,9 +145,9 @@ fn eval_extract_handle_command_output() {
),
],
Some(input_file_content.into()),
EvalAssertion::assert_diff_any(possible_diffs),
),
);
EvalAssertion::assert_diff_any(possible_diffs.clone()),
))
});
}
#[test]
@@ -122,18 +165,16 @@ fn eval_delete_run_git_blame() {
let input_file_content = include_str!("evals/fixtures/delete_run_git_blame/before.rs");
let output_file_content = include_str!("evals/fixtures/delete_run_git_blame/after.rs");
let edit_description = "Delete the `run_git_blame` function.";
eval(
100,
0.95,
0.05,
EvalInput::from_conversation(
eval_utils::eval(100, 0.95, mismatched_tag_threshold(0.05), move || {
run_eval(EvalInput::from_conversation(
vec![
message(
User,
[text(formatdoc! {"
Read the `{input_file_path}` file and delete `run_git_blame`. Just that
one function, not its usages.
"})],
Read the `{input_file_path}` file and delete `run_git_blame`. Just that
one function, not its usages.
"})],
),
message(
Assistant,
@@ -166,8 +207,8 @@ fn eval_delete_run_git_blame() {
],
Some(input_file_content.into()),
EvalAssertion::assert_eq(output_file_content),
),
);
))
});
}
#[test]
@@ -185,18 +226,16 @@ fn eval_translate_doc_comments() {
let input_file_path = "root/canvas.rs";
let input_file_content = include_str!("evals/fixtures/translate_doc_comments/before.rs");
let edit_description = "Translate all doc comments to Italian";
eval(
200,
1.,
0.05,
EvalInput::from_conversation(
eval_utils::eval(200, 1., mismatched_tag_threshold(0.05), move || {
run_eval(EvalInput::from_conversation(
vec![
message(
User,
[text(formatdoc! {"
Read the {input_file_path} file and edit it (without overwriting it),
translating all the doc comments to italian.
"})],
Read the {input_file_path} file and edit it (without overwriting it),
translating all the doc comments to italian.
"})],
),
message(
Assistant,
@@ -229,8 +268,8 @@ fn eval_translate_doc_comments() {
],
Some(input_file_content.into()),
EvalAssertion::judge_diff("Doc comments were translated to Italian"),
),
);
))
});
}
#[test]
@@ -249,33 +288,31 @@ fn eval_use_wasi_sdk_in_compile_parser_to_wasm() {
let input_file_content =
include_str!("evals/fixtures/use_wasi_sdk_in_compile_parser_to_wasm/before.rs");
let edit_description = "Update compile_parser_to_wasm to use wasi-sdk instead of emscripten";
eval(
100,
0.95,
0.05,
EvalInput::from_conversation(
eval_utils::eval(100, 0.95, mismatched_tag_threshold(0.05), move || {
run_eval(EvalInput::from_conversation(
vec![
message(
User,
[text(formatdoc! {"
Read the `{input_file_path}` file and change `compile_parser_to_wasm` to use `wasi-sdk` instead of emscripten.
Use `ureq` to download the SDK for the current platform and architecture.
Extract the archive into a sibling of `lib` inside the `tree-sitter` directory in the cache_dir.
Compile the parser to wasm using the `bin/clang` executable (or `bin/clang.exe` on windows)
that's inside of the archive.
Don't re-download the SDK if that executable already exists.
Read the `{input_file_path}` file and change `compile_parser_to_wasm` to use `wasi-sdk` instead of emscripten.
Use `ureq` to download the SDK for the current platform and architecture.
Extract the archive into a sibling of `lib` inside the `tree-sitter` directory in the cache_dir.
Compile the parser to wasm using the `bin/clang` executable (or `bin/clang.exe` on windows)
that's inside of the archive.
Don't re-download the SDK if that executable already exists.
Use these clang flags: -fPIC -shared -Os -Wl,--export=tree_sitter_{{language_name}}
Use these clang flags: -fPIC -shared -Os -Wl,--export=tree_sitter_{{language_name}}
Here are the available wasi-sdk assets:
- wasi-sdk-25.0-x86_64-macos.tar.gz
- wasi-sdk-25.0-arm64-macos.tar.gz
- wasi-sdk-25.0-x86_64-linux.tar.gz
- wasi-sdk-25.0-arm64-linux.tar.gz
- wasi-sdk-25.0-x86_64-linux.tar.gz
- wasi-sdk-25.0-arm64-linux.tar.gz
- wasi-sdk-25.0-x86_64-windows.tar.gz
"})],
Here are the available wasi-sdk assets:
- wasi-sdk-25.0-x86_64-macos.tar.gz
- wasi-sdk-25.0-arm64-macos.tar.gz
- wasi-sdk-25.0-x86_64-linux.tar.gz
- wasi-sdk-25.0-arm64-linux.tar.gz
- wasi-sdk-25.0-x86_64-linux.tar.gz
- wasi-sdk-25.0-arm64-linux.tar.gz
- wasi-sdk-25.0-x86_64-windows.tar.gz
"})],
),
message(
Assistant,
@@ -352,11 +389,11 @@ fn eval_use_wasi_sdk_in_compile_parser_to_wasm() {
],
Some(input_file_content.into()),
EvalAssertion::judge_diff(indoc! {"
- The compile_parser_to_wasm method has been changed to use wasi-sdk
- ureq is used to download the SDK for current platform and architecture
"}),
),
);
- The compile_parser_to_wasm method has been changed to use wasi-sdk
- ureq is used to download the SDK for current platform and architecture
"}),
))
});
}
#[test]
@@ -380,11 +417,8 @@ fn eval_disable_cursor_blinking() {
include_str!("evals/fixtures/disable_cursor_blinking/possible-03.diff"),
include_str!("evals/fixtures/disable_cursor_blinking/possible-04.diff"),
];
eval(
100,
0.51,
0.05,
EvalInput::from_conversation(
eval_utils::eval(100, 0.51, mismatched_tag_threshold(0.05), move || {
run_eval(EvalInput::from_conversation(
vec![
message(User, [text("Let's research how to cursor blinking works.")]),
message(
@@ -421,10 +455,10 @@ fn eval_disable_cursor_blinking() {
message(
User,
[text(indoc! {"
Comment out the lines that interact with the BlinkManager.
Keep the outer `update` blocks, but comments everything that's inside (including if statements).
Don't add additional comments.
"})],
Comment out the lines that interact with the BlinkManager.
Keep the outer `update` blocks, but comments everything that's inside (including if statements).
Don't add additional comments.
"})],
),
message(
Assistant,
@@ -440,9 +474,9 @@ fn eval_disable_cursor_blinking() {
),
],
Some(input_file_content.into()),
EvalAssertion::assert_diff_any(possible_diffs),
),
);
EvalAssertion::assert_diff_any(possible_diffs.clone()),
))
});
}
#[test]
@@ -467,20 +501,16 @@ fn eval_from_pixels_constructor() {
let input_file_path = "root/canvas.rs";
let input_file_content = include_str!("evals/fixtures/from_pixels_constructor/before.rs");
let edit_description = "Implement from_pixels constructor and add tests.";
eval(
100,
0.95,
// For whatever reason, this eval produces more mismatched tags.
// Increasing for now, let's see if we can bring this down.
0.25,
EvalInput::from_conversation(
eval_utils::eval(100, 0.95, mismatched_tag_threshold(0.25), move || {
run_eval(EvalInput::from_conversation(
vec![
message(
User,
[text(indoc! {"
Introduce a new `from_pixels` constructor in Canvas and
also add tests for it in the same file.
"})],
Introduce a new `from_pixels` constructor in Canvas and
also add tests for it in the same file.
"})],
),
message(
Assistant,
@@ -545,92 +575,92 @@ fn eval_from_pixels_constructor() {
"tool_4",
"grep",
indoc! {"
Found 6 matches:
Found 6 matches:
## Matches in font-kit/src/loaders/core_text.rs
## Matches in font-kit/src/loaders/core_text.rs
### mod test L926-936
```
mod test {
use super::Font;
use crate::properties::{Stretch, Weight};
### mod test L926-936
```
mod test {
use super::Font;
use crate::properties::{Stretch, Weight};
#[cfg(feature = \"source\")]
use crate::source::SystemSource;
#[cfg(feature = \"source\")]
use crate::source::SystemSource;
static TEST_FONT_POSTSCRIPT_NAME: &'static str = \"ArialMT\";
static TEST_FONT_POSTSCRIPT_NAME: &'static str = \"ArialMT\";
#[cfg(feature = \"source\")]
#[test]
```
#[cfg(feature = \"source\")]
#[test]
```
55 lines remaining in ancestor node. Read the file to see all.
55 lines remaining in ancestor node. Read the file to see all.
### mod test L947-951
```
}
### mod test L947-951
```
}
#[test]
fn test_core_text_to_css_font_weight() {
// Exact matches
```
#[test]
fn test_core_text_to_css_font_weight() {
// Exact matches
```
### mod test L959-963
```
}
### mod test L959-963
```
}
#[test]
fn test_core_text_to_css_font_stretch() {
// Exact matches
```
#[test]
fn test_core_text_to_css_font_stretch() {
// Exact matches
```
## Matches in font-kit/src/loaders/freetype.rs
## Matches in font-kit/src/loaders/freetype.rs
### mod test L1238-1248
```
mod test {
use crate::loaders::freetype::Font;
### mod test L1238-1248
```
mod test {
use crate::loaders::freetype::Font;
static PCF_FONT_PATH: &str = \"resources/tests/times-roman-pcf/timR12.pcf\";
static PCF_FONT_POSTSCRIPT_NAME: &str = \"Times-Roman\";
static PCF_FONT_PATH: &str = \"resources/tests/times-roman-pcf/timR12.pcf\";
static PCF_FONT_POSTSCRIPT_NAME: &str = \"Times-Roman\";
#[test]
fn get_pcf_postscript_name() {
let font = Font::from_path(PCF_FONT_PATH, 0).unwrap();
assert_eq!(font.postscript_name().unwrap(), PCF_FONT_POSTSCRIPT_NAME);
}
```
#[test]
fn get_pcf_postscript_name() {
let font = Font::from_path(PCF_FONT_PATH, 0).unwrap();
assert_eq!(font.postscript_name().unwrap(), PCF_FONT_POSTSCRIPT_NAME);
}
```
1 lines remaining in ancestor node. Read the file to see all.
1 lines remaining in ancestor node. Read the file to see all.
## Matches in font-kit/src/sources/core_text.rs
## Matches in font-kit/src/sources/core_text.rs
### mod test L265-275
```
mod test {
use crate::properties::{Stretch, Weight};
### mod test L265-275
```
mod test {
use crate::properties::{Stretch, Weight};
#[test]
fn test_css_to_core_text_font_weight() {
// Exact matches
assert_eq!(super::css_to_core_text_font_weight(Weight(100.0)), -0.7);
assert_eq!(super::css_to_core_text_font_weight(Weight(400.0)), 0.0);
assert_eq!(super::css_to_core_text_font_weight(Weight(700.0)), 0.4);
assert_eq!(super::css_to_core_text_font_weight(Weight(900.0)), 0.8);
#[test]
fn test_css_to_core_text_font_weight() {
// Exact matches
assert_eq!(super::css_to_core_text_font_weight(Weight(100.0)), -0.7);
assert_eq!(super::css_to_core_text_font_weight(Weight(400.0)), 0.0);
assert_eq!(super::css_to_core_text_font_weight(Weight(700.0)), 0.4);
assert_eq!(super::css_to_core_text_font_weight(Weight(900.0)), 0.8);
```
```
27 lines remaining in ancestor node. Read the file to see all.
27 lines remaining in ancestor node. Read the file to see all.
### mod test L278-282
```
}
### mod test L278-282
```
}
#[test]
fn test_css_to_core_text_font_stretch() {
// Exact matches
```
"},
#[test]
fn test_css_to_core_text_font_stretch() {
// Exact matches
```
"},
)],
),
message(
@@ -648,11 +678,11 @@ fn eval_from_pixels_constructor() {
],
Some(input_file_content.into()),
EvalAssertion::judge_diff(indoc! {"
- The diff contains a new `from_pixels` constructor
- The diff contains new tests for the `from_pixels` constructor
"}),
),
);
- The diff contains a new `from_pixels` constructor
- The diff contains new tests for the `from_pixels` constructor
"}),
))
});
}
#[test]
@@ -670,11 +700,9 @@ fn eval_zode() {
let input_file_path = "root/zode.py";
let input_content = None;
let edit_description = "Create the main Zode CLI script";
eval(
50,
1.,
0.05,
EvalInput::from_conversation(
eval_utils::eval(50, 1., mismatched_tag_threshold(0.05), move || {
run_eval(EvalInput::from_conversation(
vec![
message(User, [text(include_str!("evals/fixtures/zode/prompt.md"))]),
message(
@@ -733,7 +761,7 @@ fn eval_zode() {
],
),
],
input_content,
input_content.clone(),
EvalAssertion::new(async move |sample, _, _cx| {
let invalid_starts = [' ', '`', '\n'];
let mut message = String::new();
@@ -758,8 +786,8 @@ fn eval_zode() {
})
}
}),
),
);
))
});
}
#[test]
@@ -777,19 +805,17 @@ fn eval_add_overwrite_test() {
let input_file_path = "root/action_log.rs";
let input_file_content = include_str!("evals/fixtures/add_overwrite_test/before.rs");
let edit_description = "Add a new test for overwriting a file in action_log.rs";
eval(
200,
0.5, // TODO: make this eval better
0.05,
EvalInput::from_conversation(
eval_utils::eval(200, 0.5, mismatched_tag_threshold(0.05), move || {
run_eval(EvalInput::from_conversation(
vec![
message(
User,
[text(indoc! {"
Introduce a new test in `action_log.rs` to test overwriting a file.
That is, a file already exists, but we call `buffer_created` as if the file were new.
Take inspiration from all the other tests in the file.
"})],
Introduce a new test in `action_log.rs` to test overwriting a file.
That is, a file already exists, but we call `buffer_created` as if the file were new.
Take inspiration from all the other tests in the file.
"})],
),
message(
Assistant,
@@ -809,81 +835,81 @@ fn eval_add_overwrite_test() {
"tool_1",
"read_file",
indoc! {"
pub struct ActionLog [L13-20]
tracked_buffers [L15]
edited_since_project_diagnostics_check [L17]
project [L19]
impl ActionLog [L22-498]
pub fn new [L24-30]
pub fn project [L32-34]
pub fn checked_project_diagnostics [L37-39]
pub fn has_edited_files_since_project_diagnostics_check [L42-44]
fn track_buffer_internal [L46-101]
fn handle_buffer_event [L103-116]
fn handle_buffer_edited [L118-123]
fn handle_buffer_file_changed [L125-158]
async fn maintain_diff [L160-264]
pub fn buffer_read [L267-269]
pub fn buffer_created [L272-276]
pub fn buffer_edited [L279-287]
pub fn will_delete_buffer [L289-304]
pub fn keep_edits_in_range [L306-364]
pub fn reject_edits_in_ranges [L366-459]
pub fn keep_all_edits [L461-473]
pub fn changed_buffers [L476-482]
pub fn stale_buffers [L485-497]
fn apply_non_conflicting_edits [L500-561]
fn diff_snapshots [L563-585]
fn point_to_row_edit [L587-614]
enum ChangeAuthor [L617-620]
User [L618]
Agent [L619]
enum TrackedBufferStatus [L623-627]
Created [L624]
Modified [L625]
Deleted [L626]
struct TrackedBuffer [L629-641]
buffer [L630]
base_text [L631]
unreviewed_changes [L632]
status [L633]
version [L634]
diff [L635]
snapshot [L636]
diff_update [L637]
_open_lsp_handle [L638]
_maintain_diff [L639]
_subscription [L640]
impl TrackedBuffer [L643-657]
fn has_changes [L644-650]
fn schedule_diff_update [L652-656]
pub struct ChangedBuffer [L659-661]
pub diff [L660]
mod tests [L664-1574]
fn init_logger [L678-682]
fn init_test [L684-691]
async fn test_keep_edits [L694-769]
async fn test_deletions [L772-854]
async fn test_overlapping_user_edits [L857-951]
async fn test_creating_files [L954-1010]
async fn test_deleting_files [L1013-1120]
async fn test_reject_edits [L1123-1255]
async fn test_reject_multiple_edits [L1258-1331]
async fn test_reject_deleted_file [L1334-1388]
async fn test_reject_created_file [L1391-1443]
async fn test_random_diffs [L1446-1535]
fn quiesce [L1510-1534]
struct HunkStatus [L1538-1542]
range [L1539]
diff_status [L1540]
old_text [L1541]
fn unreviewed_hunks [L1544-1573]
pub struct ActionLog [L13-20]
tracked_buffers [L15]
edited_since_project_diagnostics_check [L17]
project [L19]
impl ActionLog [L22-498]
pub fn new [L24-30]
pub fn project [L32-34]
pub fn checked_project_diagnostics [L37-39]
pub fn has_edited_files_since_project_diagnostics_check [L42-44]
fn track_buffer_internal [L46-101]
fn handle_buffer_event [L103-116]
fn handle_buffer_edited [L118-123]
fn handle_buffer_file_changed [L125-158]
async fn maintain_diff [L160-264]
pub fn buffer_read [L267-269]
pub fn buffer_created [L272-276]
pub fn buffer_edited [L279-287]
pub fn will_delete_buffer [L289-304]
pub fn keep_edits_in_range [L306-364]
pub fn reject_edits_in_ranges [L366-459]
pub fn keep_all_edits [L461-473]
pub fn changed_buffers [L476-482]
pub fn stale_buffers [L485-497]
fn apply_non_conflicting_edits [L500-561]
fn diff_snapshots [L563-585]
fn point_to_row_edit [L587-614]
enum ChangeAuthor [L617-620]
User [L618]
Agent [L619]
enum TrackedBufferStatus [L623-627]
Created [L624]
Modified [L625]
Deleted [L626]
struct TrackedBuffer [L629-641]
buffer [L630]
base_text [L631]
unreviewed_changes [L632]
status [L633]
version [L634]
diff [L635]
snapshot [L636]
diff_update [L637]
_open_lsp_handle [L638]
_maintain_diff [L639]
_subscription [L640]
impl TrackedBuffer [L643-657]
fn has_changes [L644-650]
fn schedule_diff_update [L652-656]
pub struct ChangedBuffer [L659-661]
pub diff [L660]
mod tests [L664-1574]
fn init_logger [L678-682]
fn init_test [L684-691]
async fn test_keep_edits [L694-769]
async fn test_deletions [L772-854]
async fn test_overlapping_user_edits [L857-951]
async fn test_creating_files [L954-1010]
async fn test_deleting_files [L1013-1120]
async fn test_reject_edits [L1123-1255]
async fn test_reject_multiple_edits [L1258-1331]
async fn test_reject_deleted_file [L1334-1388]
async fn test_reject_created_file [L1391-1443]
async fn test_random_diffs [L1446-1535]
fn quiesce [L1510-1534]
struct HunkStatus [L1538-1542]
range [L1539]
diff_status [L1540]
old_text [L1541]
fn unreviewed_hunks [L1544-1573]
Showing symbols 1-69 (total symbols: 69)
Showing symbols 1-69 (total symbols: 69)
Using the line numbers in this outline, you can call this tool again while specifying
the start_line and end_line fields to see the implementations of symbols in the outline.
"},
Using the line numbers in this outline, you can call this tool again while specifying
the start_line and end_line fields to see the implementations of symbols in the outline.
"},
)],
),
message(
@@ -963,17 +989,17 @@ fn eval_add_overwrite_test() {
Assistant,
[
text(indoc! {"
Based on my understanding of the existing tests and code, I'll now create a new test for overwriting a file. In this test, we'll:
Based on my understanding of the existing tests and code, I'll now create a new test for overwriting a file. In this test, we'll:
1. Create a file
2. Open a buffer for it
3. Track it in the action log
4. Modify the buffer
5. Call `buffer_created` again as if the file were new
6. Check that the changes are properly tracked
1. Create a file
2. Open a buffer for it
3. Track it in the action log
4. Modify the buffer
5. Call `buffer_created` again as if the file were new
6. Check that the changes are properly tracked
Let's write the test:
"}),
Let's write the test:
"}),
tool_use(
"tool_5",
"edit_file",
@@ -990,8 +1016,8 @@ fn eval_add_overwrite_test() {
EvalAssertion::judge_diff(
"A new test for overwritten files was created, without changing any previous test",
),
),
);
))
});
}
#[test]
@@ -1016,20 +1042,18 @@ fn eval_create_empty_file() {
let input_file_content = None;
let expected_output_content = String::new();
eval(
100,
0.99,
0.05,
EvalInput::from_conversation(
eval_utils::eval(100, 0.99, mismatched_tag_threshold(0.05), move || {
run_eval(EvalInput::from_conversation(
vec![
message(User, [text("Create a second empty todo file ")]),
message(
Assistant,
[
text(formatdoc! {"
I'll help you create a second empty todo file.
First, let me examine the project structure to see if there's already a todo file, which will help me determine the appropriate name and location for the second one.
"}),
I'll help you create a second empty todo file.
First, let me examine the project structure to see if there's already a todo file, which will help me determine the appropriate name and location for the second one.
"}),
tool_use(
"toolu_01GAF8TtsgpjKxCr8fgQLDgR",
"list_directory",
@@ -1051,8 +1075,8 @@ fn eval_create_empty_file() {
Assistant,
[
text(formatdoc! {"
I can see there's already a `TODO` file in the `root` directory. Let me create a second empty todo file called `TODO3` in the same directory:
"}),
I can see there's already a `TODO` file in the `root` directory. Let me create a second empty todo file called `TODO3` in the same directory:
"}),
tool_use(
"toolu_01Tb3iQ9griqSYMmVuykQPWU",
"edit_file",
@@ -1065,12 +1089,12 @@ fn eval_create_empty_file() {
],
),
],
input_file_content,
input_file_content.clone(),
// Bad behavior is to write something like
// "I'll create an empty TODO3 file as requested."
EvalAssertion::assert_eq(expected_output_content),
),
);
EvalAssertion::assert_eq(expected_output_content.clone()),
))
});
}
fn message(
@@ -1312,115 +1336,44 @@ impl EvalAssertion {
}
}
fn eval(
iterations: usize,
expected_pass_ratio: f32,
mismatched_tag_threshold: f32,
mut eval: EvalInput,
) {
let mut evaluated_count = 0;
let mut failed_count = 0;
report_progress(evaluated_count, failed_count, iterations);
let (tx, rx) = mpsc::channel();
// Cache the last message in the conversation, and run one instance of the eval so that
// all the next ones are cached.
eval.conversation.last_mut().unwrap().cache = true;
run_eval(eval.clone(), tx.clone());
let executor = gpui::background_executor();
let semaphore = Arc::new(smol::lock::Semaphore::new(32));
for _ in 1..iterations {
let eval = eval.clone();
let tx = tx.clone();
let semaphore = semaphore.clone();
executor
.spawn(async move {
let _guard = semaphore.acquire().await;
run_eval(eval, tx)
})
.detach();
}
drop(tx);
let mut failed_evals = HashMap::default();
let mut errored_evals = HashMap::default();
let mut eval_outputs = Vec::new();
let mut cumulative_parser_metrics = EditParserMetrics::default();
while let Ok(output) = rx.recv() {
match output {
Ok(output) => {
cumulative_parser_metrics += output.sample.edit_output.parser_metrics.clone();
eval_outputs.push(output.clone());
if output.assertion.score < 80 {
failed_count += 1;
failed_evals
.entry(output.sample.text_after.clone())
.or_insert(Vec::new())
.push(output);
}
}
Err(error) => {
failed_count += 1;
*errored_evals.entry(format!("{:?}", error)).or_insert(0) += 1;
}
}
evaluated_count += 1;
report_progress(evaluated_count, failed_count, iterations);
}
let actual_pass_ratio = (iterations - failed_count) as f32 / iterations as f32;
println!("Actual pass ratio: {}\n", actual_pass_ratio);
if actual_pass_ratio < expected_pass_ratio {
let mut errored_evals = errored_evals.into_iter().collect::<Vec<_>>();
errored_evals.sort_by_key(|(_, count)| Reverse(*count));
for (error, count) in errored_evals {
println!("Eval errored {} times. Error: {}", count, error);
}
let mut failed_evals = failed_evals.into_iter().collect::<Vec<_>>();
failed_evals.sort_by_key(|(_, evals)| Reverse(evals.len()));
for (_buffer_output, failed_evals) in failed_evals {
let eval_output = failed_evals.first().unwrap();
println!("Eval failed {} times", failed_evals.len());
println!("{}", eval_output);
}
panic!(
"Actual pass ratio: {}\nExpected pass ratio: {}",
actual_pass_ratio, expected_pass_ratio
);
}
let mismatched_tag_ratio =
cumulative_parser_metrics.mismatched_tags as f32 / cumulative_parser_metrics.tags as f32;
if mismatched_tag_ratio > mismatched_tag_threshold {
for eval_output in eval_outputs {
println!("{}", eval_output);
}
panic!("Too many mismatched tags: {:?}", cumulative_parser_metrics);
}
}
fn run_eval(eval: EvalInput, tx: mpsc::Sender<Result<EvalOutput>>) {
fn run_eval(eval: EvalInput) -> eval_utils::EvalOutput<EditEvalMetadata> {
let dispatcher = gpui::TestDispatcher::new(StdRng::from_os_rng());
let mut cx = TestAppContext::build(dispatcher, None);
let output = cx.executor().block_test(async {
let result = cx.executor().block_test(async {
let test = EditAgentTest::new(&mut cx).await;
test.eval(eval, &mut cx).await
});
tx.send(output).unwrap();
match result {
Ok(output) => eval_utils::EvalOutput {
data: output.to_string(),
outcome: if output.assertion.score < 80 {
eval_utils::OutcomeKind::Failed
} else {
eval_utils::OutcomeKind::Passed
},
metadata: EditEvalMetadata {
tags: output.sample.edit_output.parser_metrics.tags,
mismatched_tags: output.sample.edit_output.parser_metrics.mismatched_tags,
},
},
Err(e) => eval_utils::EvalOutput {
data: format!("{e:?}"),
outcome: eval_utils::OutcomeKind::Error,
metadata: EditEvalMetadata {
tags: 0,
mismatched_tags: 0,
},
},
}
}
#[derive(Clone)]
struct EvalOutput {
struct EditEvalOutput {
sample: EvalSample,
assertion: EvalAssertionOutcome,
}
impl Display for EvalOutput {
impl Display for EditEvalOutput {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
writeln!(f, "Score: {:?}", self.assertion.score)?;
if let Some(message) = self.assertion.message.as_ref() {
@@ -1439,22 +1392,6 @@ impl Display for EvalOutput {
}
}
fn report_progress(evaluated_count: usize, failed_count: usize, iterations: usize) {
let passed_count = evaluated_count - failed_count;
let passed_ratio = if evaluated_count == 0 {
0.0
} else {
passed_count as f64 / evaluated_count as f64
};
print!(
"\r\x1b[KEvaluated {}/{} ({:.2}% passed)",
evaluated_count,
iterations,
passed_ratio * 100.0
);
std::io::stdout().flush().unwrap();
}
struct EditAgentTest {
agent: EditAgent,
project: Entity<Project>,
@@ -1550,7 +1487,10 @@ impl EditAgentTest {
})
}
async fn eval(&self, eval: EvalInput, cx: &mut TestAppContext) -> Result<EvalOutput> {
async fn eval(&self, mut eval: EvalInput, cx: &mut TestAppContext) -> Result<EditEvalOutput> {
// Make sure the last message in the conversation is cached.
eval.conversation.last_mut().unwrap().cache = true;
let path = self
.project
.read_with(cx, |project, cx| {
@@ -1656,7 +1596,7 @@ impl EditAgentTest {
.run(&sample, self.judge_model.clone(), cx)
.await?;
Ok(EvalOutput { assertion, sample })
Ok(EditEvalOutput { assertion, sample })
}
}

View File

@@ -354,9 +354,9 @@ impl HistoryStore {
.into_iter()
.take(MAX_RECENTLY_OPENED_ENTRIES)
.flat_map(|entry| match entry {
SerializedRecentOpen::AcpThread(id) => Some(HistoryEntryId::AcpThread(
acp::SessionId(id.as_str().into()),
)),
SerializedRecentOpen::AcpThread(id) => {
Some(HistoryEntryId::AcpThread(acp::SessionId::new(id.as_str())))
}
SerializedRecentOpen::TextThread(file_name) => Some(
HistoryEntryId::TextThread(text_threads_dir().join(file_name).into()),
),

View File

@@ -66,11 +66,9 @@ pub async fn get_buffer_content_or_outline(
let outline_text = render_outline(outline_items, None, 0, usize::MAX).await?;
let text = if let Some(path) = path {
format!(
"# File outline for {path} (file too large to show full content)\n\n{outline_text}",
)
format!("# File outline for {path}\n\n{outline_text}",)
} else {
format!("# File outline (file too large to show full content)\n\n{outline_text}",)
format!("# File outline\n\n{outline_text}",)
};
Ok(BufferContent {
text,

View File

@@ -6,33 +6,6 @@ The text you output will be saved verbatim as the content of the file.
Tool calls have been disabled.
Start your response with ```.
IMPORTANT: Output ONLY the file content between the backticks. Do NOT include:
- The file path or name (like "path/to/file.txt")
- Any markdown formatting within the content
- Any explanatory text before or after the backticks
- Any nested code fences within your output
<example>
If asked to create a file with "hello" inside:
CORRECT output:
```
hello
```
INCORRECT output (includes file path):
```path/to/file.txt
hello
```
INCORRECT output (nested code fences):
```
```
hello
```
```
</example>
<file_path>
{{path}}
</file_path>

View File

@@ -493,14 +493,14 @@ async fn test_tool_authorization(cx: &mut TestAppContext) {
// Approve the first
tool_call_auth_1
.response
.send(tool_call_auth_1.options[1].id.clone())
.send(tool_call_auth_1.options[1].option_id.clone())
.unwrap();
cx.run_until_parked();
// Reject the second
tool_call_auth_2
.response
.send(tool_call_auth_1.options[2].id.clone())
.send(tool_call_auth_1.options[2].option_id.clone())
.unwrap();
cx.run_until_parked();
@@ -510,14 +510,14 @@ async fn test_tool_authorization(cx: &mut TestAppContext) {
message.content,
vec![
language_model::MessageContent::ToolResult(LanguageModelToolResult {
tool_use_id: tool_call_auth_1.tool_call.id.0.to_string().into(),
tool_use_id: tool_call_auth_1.tool_call.tool_call_id.0.to_string().into(),
tool_name: ToolRequiringPermission::name().into(),
is_error: false,
content: "Allowed".into(),
output: Some("Allowed".into())
}),
language_model::MessageContent::ToolResult(LanguageModelToolResult {
tool_use_id: tool_call_auth_2.tool_call.id.0.to_string().into(),
tool_use_id: tool_call_auth_2.tool_call.tool_call_id.0.to_string().into(),
tool_name: ToolRequiringPermission::name().into(),
is_error: true,
content: "Permission to run tool denied by user".into(),
@@ -543,7 +543,7 @@ async fn test_tool_authorization(cx: &mut TestAppContext) {
let tool_call_auth_3 = next_tool_call_authorization(&mut events).await;
tool_call_auth_3
.response
.send(tool_call_auth_3.options[0].id.clone())
.send(tool_call_auth_3.options[0].option_id.clone())
.unwrap();
cx.run_until_parked();
let completion = fake_model.pending_completions().pop().unwrap();
@@ -552,7 +552,7 @@ async fn test_tool_authorization(cx: &mut TestAppContext) {
message.content,
vec![language_model::MessageContent::ToolResult(
LanguageModelToolResult {
tool_use_id: tool_call_auth_3.tool_call.id.0.to_string().into(),
tool_use_id: tool_call_auth_3.tool_call.tool_call_id.0.to_string().into(),
tool_name: ToolRequiringPermission::name().into(),
is_error: false,
content: "Allowed".into(),
@@ -1353,20 +1353,20 @@ async fn test_cancellation(cx: &mut TestAppContext) {
ThreadEvent::ToolCall(tool_call) => {
assert_eq!(tool_call.title, expected_tools.remove(0));
if tool_call.title == "Echo" {
echo_id = Some(tool_call.id);
echo_id = Some(tool_call.tool_call_id);
}
}
ThreadEvent::ToolCallUpdate(acp_thread::ToolCallUpdate::UpdateFields(
acp::ToolCallUpdate {
id,
tool_call_id,
fields:
acp::ToolCallUpdateFields {
status: Some(acp::ToolCallStatus::Completed),
..
},
meta: None,
..
},
)) if Some(&id) == echo_id.as_ref() => {
)) if Some(&tool_call_id) == echo_id.as_ref() => {
echo_completed = true;
}
_ => {}
@@ -1995,11 +1995,7 @@ async fn test_agent_connection(cx: &mut TestAppContext) {
.update(|cx| {
connection.prompt(
Some(acp_thread::UserMessageId::new()),
acp::PromptRequest {
session_id: session_id.clone(),
prompt: vec!["ghi".into()],
meta: None,
},
acp::PromptRequest::new(session_id.clone(), vec!["ghi".into()]),
cx,
)
})
@@ -2056,68 +2052,50 @@ async fn test_tool_updates_to_completion(cx: &mut TestAppContext) {
let tool_call = expect_tool_call(&mut events).await;
assert_eq!(
tool_call,
acp::ToolCall {
id: acp::ToolCallId("1".into()),
title: "Thinking".into(),
kind: acp::ToolKind::Think,
status: acp::ToolCallStatus::Pending,
content: vec![],
locations: vec![],
raw_input: Some(json!({})),
raw_output: None,
meta: Some(json!({ "tool_name": "thinking" })),
}
acp::ToolCall::new("1", "Thinking")
.kind(acp::ToolKind::Think)
.raw_input(json!({}))
.meta(acp::Meta::from_iter([(
"tool_name".into(),
"thinking".into()
)]))
);
let update = expect_tool_call_update_fields(&mut events).await;
assert_eq!(
update,
acp::ToolCallUpdate {
id: acp::ToolCallId("1".into()),
fields: acp::ToolCallUpdateFields {
title: Some("Thinking".into()),
kind: Some(acp::ToolKind::Think),
raw_input: Some(json!({ "content": "Thinking hard!" })),
..Default::default()
},
meta: None,
}
acp::ToolCallUpdate::new(
"1",
acp::ToolCallUpdateFields::new()
.title("Thinking")
.kind(acp::ToolKind::Think)
.raw_input(json!({ "content": "Thinking hard!"}))
)
);
let update = expect_tool_call_update_fields(&mut events).await;
assert_eq!(
update,
acp::ToolCallUpdate {
id: acp::ToolCallId("1".into()),
fields: acp::ToolCallUpdateFields {
status: Some(acp::ToolCallStatus::InProgress),
..Default::default()
},
meta: None,
}
acp::ToolCallUpdate::new(
"1",
acp::ToolCallUpdateFields::new().status(acp::ToolCallStatus::InProgress)
)
);
let update = expect_tool_call_update_fields(&mut events).await;
assert_eq!(
update,
acp::ToolCallUpdate {
id: acp::ToolCallId("1".into()),
fields: acp::ToolCallUpdateFields {
content: Some(vec!["Thinking hard!".into()]),
..Default::default()
},
meta: None,
}
acp::ToolCallUpdate::new(
"1",
acp::ToolCallUpdateFields::new().content(vec!["Thinking hard!".into()])
)
);
let update = expect_tool_call_update_fields(&mut events).await;
assert_eq!(
update,
acp::ToolCallUpdate {
id: acp::ToolCallId("1".into()),
fields: acp::ToolCallUpdateFields {
status: Some(acp::ToolCallStatus::Completed),
raw_output: Some("Finished thinking.".into()),
..Default::default()
},
meta: None,
}
acp::ToolCallUpdate::new(
"1",
acp::ToolCallUpdateFields::new()
.status(acp::ToolCallStatus::Completed)
.raw_output("Finished thinking.".into())
)
);
}

View File

@@ -619,12 +619,9 @@ pub struct Thread {
impl Thread {
fn prompt_capabilities(model: Option<&dyn LanguageModel>) -> acp::PromptCapabilities {
let image = model.map_or(true, |model| model.supports_images());
acp::PromptCapabilities {
meta: None,
image,
audio: false,
embedded_context: true,
}
acp::PromptCapabilities::new()
.image(image)
.embedded_context(true)
}
pub fn new(
@@ -640,7 +637,7 @@ impl Thread {
let (prompt_capabilities_tx, prompt_capabilities_rx) =
watch::channel(Self::prompt_capabilities(model.as_deref()));
Self {
id: acp::SessionId(uuid::Uuid::new_v4().to_string().into()),
id: acp::SessionId::new(uuid::Uuid::new_v4().to_string()),
prompt_id: PromptId::new(),
updated_at: Utc::now(),
title: None,
@@ -737,17 +734,11 @@ impl Thread {
let Some(tool) = tool else {
stream
.0
.unbounded_send(Ok(ThreadEvent::ToolCall(acp::ToolCall {
meta: None,
id: acp::ToolCallId(tool_use.id.to_string().into()),
title: tool_use.name.to_string(),
kind: acp::ToolKind::Other,
status: acp::ToolCallStatus::Failed,
content: Vec::new(),
locations: Vec::new(),
raw_input: Some(tool_use.input.clone()),
raw_output: None,
})))
.unbounded_send(Ok(ThreadEvent::ToolCall(
acp::ToolCall::new(tool_use.id.to_string(), tool_use.name.to_string())
.status(acp::ToolCallStatus::Failed)
.raw_input(tool_use.input.clone()),
)))
.ok();
return;
};
@@ -775,24 +766,20 @@ impl Thread {
.log_err();
}
stream.update_tool_call_fields(
&tool_use.id,
acp::ToolCallUpdateFields {
status: Some(
tool_result
.as_ref()
.map_or(acp::ToolCallStatus::Failed, |result| {
if result.is_error {
acp::ToolCallStatus::Failed
} else {
acp::ToolCallStatus::Completed
}
}),
),
raw_output: output,
..Default::default()
let mut fields = acp::ToolCallUpdateFields::new().status(tool_result.as_ref().map_or(
acp::ToolCallStatus::Failed,
|result| {
if result.is_error {
acp::ToolCallStatus::Failed
} else {
acp::ToolCallStatus::Completed
}
},
);
));
if let Some(output) = output {
fields = fields.raw_output(output);
}
stream.update_tool_call_fields(&tool_use.id, fields);
}
pub fn from_db(
@@ -1272,18 +1259,15 @@ impl Thread {
while let Some(tool_result) = tool_results.next().await {
log::debug!("Tool finished {:?}", tool_result);
event_stream.update_tool_call_fields(
&tool_result.tool_use_id,
acp::ToolCallUpdateFields {
status: Some(if tool_result.is_error {
acp::ToolCallStatus::Failed
} else {
acp::ToolCallStatus::Completed
}),
raw_output: tool_result.output.clone(),
..Default::default()
},
);
let mut fields = acp::ToolCallUpdateFields::new().status(if tool_result.is_error {
acp::ToolCallStatus::Failed
} else {
acp::ToolCallStatus::Completed
});
if let Some(output) = &tool_result.output {
fields = fields.raw_output(output.clone());
}
event_stream.update_tool_call_fields(&tool_result.tool_use_id, fields);
this.update(cx, |this, _cx| {
this.pending_message()
.tool_results
@@ -1560,12 +1544,10 @@ impl Thread {
} else {
event_stream.update_tool_call_fields(
&tool_use.id,
acp::ToolCallUpdateFields {
title: Some(title.into()),
kind: Some(kind),
raw_input: Some(tool_use.input.clone()),
..Default::default()
},
acp::ToolCallUpdateFields::new()
.title(title)
.kind(kind)
.raw_input(tool_use.input.clone()),
);
}
@@ -1587,10 +1569,9 @@ impl Thread {
let fs = self.project.read(cx).fs().clone();
let tool_event_stream =
ToolCallEventStream::new(tool_use.id.clone(), event_stream.clone(), Some(fs));
tool_event_stream.update_fields(acp::ToolCallUpdateFields {
status: Some(acp::ToolCallStatus::InProgress),
..Default::default()
});
tool_event_stream.update_fields(
acp::ToolCallUpdateFields::new().status(acp::ToolCallStatus::InProgress),
);
let supports_images = self.model().is_some_and(|model| model.supports_images());
let tool_result = tool.run(tool_use.input, tool_event_stream, cx);
log::debug!("Running tool {}", tool_use.name);
@@ -2381,19 +2362,13 @@ impl ThreadEventStream {
kind: acp::ToolKind,
input: serde_json::Value,
) -> acp::ToolCall {
acp::ToolCall {
meta: Some(serde_json::json!({
"tool_name": tool_name
})),
id: acp::ToolCallId(id.to_string().into()),
title,
kind,
status: acp::ToolCallStatus::Pending,
content: vec![],
locations: vec![],
raw_input: Some(input),
raw_output: None,
}
acp::ToolCall::new(id.to_string(), title)
.kind(kind)
.raw_input(input)
.meta(acp::Meta::from_iter([(
"tool_name".into(),
tool_name.into(),
)]))
}
fn update_tool_call_fields(
@@ -2403,12 +2378,7 @@ impl ThreadEventStream {
) {
self.0
.unbounded_send(Ok(ThreadEvent::ToolCallUpdate(
acp::ToolCallUpdate {
meta: None,
id: acp::ToolCallId(tool_use_id.to_string().into()),
fields,
}
.into(),
acp::ToolCallUpdate::new(tool_use_id.to_string(), fields).into(),
)))
.ok();
}
@@ -2471,7 +2441,7 @@ impl ToolCallEventStream {
.0
.unbounded_send(Ok(ThreadEvent::ToolCallUpdate(
acp_thread::ToolCallUpdateDiff {
id: acp::ToolCallId(self.tool_use_id.to_string().into()),
id: acp::ToolCallId::new(self.tool_use_id.to_string()),
diff,
}
.into(),
@@ -2489,33 +2459,26 @@ impl ToolCallEventStream {
.0
.unbounded_send(Ok(ThreadEvent::ToolCallAuthorization(
ToolCallAuthorization {
tool_call: acp::ToolCallUpdate {
meta: None,
id: acp::ToolCallId(self.tool_use_id.to_string().into()),
fields: acp::ToolCallUpdateFields {
title: Some(title.into()),
..Default::default()
},
},
tool_call: acp::ToolCallUpdate::new(
self.tool_use_id.to_string(),
acp::ToolCallUpdateFields::new().title(title),
),
options: vec![
acp::PermissionOption {
id: acp::PermissionOptionId("always_allow".into()),
name: "Always Allow".into(),
kind: acp::PermissionOptionKind::AllowAlways,
meta: None,
},
acp::PermissionOption {
id: acp::PermissionOptionId("allow".into()),
name: "Allow".into(),
kind: acp::PermissionOptionKind::AllowOnce,
meta: None,
},
acp::PermissionOption {
id: acp::PermissionOptionId("deny".into()),
name: "Deny".into(),
kind: acp::PermissionOptionKind::RejectOnce,
meta: None,
},
acp::PermissionOption::new(
acp::PermissionOptionId::new("always_allow"),
"Always Allow",
acp::PermissionOptionKind::AllowAlways,
),
acp::PermissionOption::new(
acp::PermissionOptionId::new("allow"),
"Allow",
acp::PermissionOptionKind::AllowOnce,
),
acp::PermissionOption::new(
acp::PermissionOptionId::new("deny"),
"Deny",
acp::PermissionOptionKind::RejectOnce,
),
],
response: response_tx,
},
@@ -2660,7 +2623,15 @@ impl UserMessageContent {
// TODO
Self::Text("[blob]".to_string())
}
other => {
log::warn!("Unexpected content type: {:?}", other);
Self::Text("[unknown]".to_string())
}
},
other => {
log::warn!("Unexpected content type: {:?}", other);
Self::Text("[unknown]".to_string())
}
}
}
}
@@ -2668,32 +2639,15 @@ impl UserMessageContent {
impl From<UserMessageContent> for acp::ContentBlock {
fn from(content: UserMessageContent) -> Self {
match content {
UserMessageContent::Text(text) => acp::ContentBlock::Text(acp::TextContent {
text,
annotations: None,
meta: None,
}),
UserMessageContent::Image(image) => acp::ContentBlock::Image(acp::ImageContent {
data: image.source.to_string(),
mime_type: "image/png".to_string(),
meta: None,
annotations: None,
uri: None,
}),
UserMessageContent::Mention { uri, content } => {
acp::ContentBlock::Resource(acp::EmbeddedResource {
meta: None,
resource: acp::EmbeddedResourceResource::TextResourceContents(
acp::TextResourceContents {
meta: None,
mime_type: None,
text: content,
uri: uri.to_uri().to_string(),
},
),
annotations: None,
})
UserMessageContent::Text(text) => text.into(),
UserMessageContent::Image(image) => {
acp::ContentBlock::Image(acp::ImageContent::new(image.source, "image/png"))
}
UserMessageContent::Mention { uri, content } => acp::ContentBlock::Resource(
acp::EmbeddedResource::new(acp::EmbeddedResourceResource::TextResourceContents(
acp::TextResourceContents::new(content, uri.to_uri().to_string()),
)),
),
}
}
}

View File

@@ -44,16 +44,8 @@ pub struct EditFileToolInput {
///
/// NEVER mention the file path in this description.
///
/// IMPORTANT: Do NOT include markdown code fences (```) or other markdown formatting in this description.
/// Just describe what should be done - another model will generate the actual content.
///
/// <example>Fix API endpoint URLs</example>
/// <example>Update copyright year in `page_footer`</example>
/// <example>Create a Python script that prints hello world</example>
///
/// INCORRECT examples (do not do this):
/// <example>Create a file with:\n```python\nprint('hello')\n```</example>
/// <example>Add this code:\n```\nif err:\n return\n```</example>
///
/// Make sure to include this field before all the others in the input object so that we can display it immediately.
pub display_description: String,
@@ -281,14 +273,9 @@ impl AgentTool for EditFileTool {
};
let abs_path = project.read(cx).absolute_path(&project_path, cx);
if let Some(abs_path) = abs_path.clone() {
event_stream.update_fields(ToolCallUpdateFields {
locations: Some(vec![acp::ToolCallLocation {
path: abs_path,
line: None,
meta: None,
}]),
..Default::default()
});
event_stream.update_fields(
ToolCallUpdateFields::new().locations(vec![acp::ToolCallLocation::new(abs_path)]),
);
}
let authorize = self.authorize(&input, &event_stream, cx);
@@ -397,10 +384,11 @@ impl AgentTool for EditFileTool {
range.start.to_point(&buffer.snapshot()).row
}).ok();
if let Some(abs_path) = abs_path.clone() {
event_stream.update_fields(ToolCallUpdateFields {
locations: Some(vec![ToolCallLocation { path: abs_path, line, meta: None }]),
..Default::default()
});
let mut location = ToolCallLocation::new(abs_path);
if let Some(line) = line {
location = location.line(line);
}
event_stream.update_fields(ToolCallUpdateFields::new().locations(vec![location]));
}
emitted_location = true;
}

View File

@@ -118,33 +118,29 @@ impl AgentTool for FindPathTool {
let paginated_matches: &[PathBuf] = &matches[cmp::min(input.offset, matches.len())
..cmp::min(input.offset + RESULTS_PER_PAGE, matches.len())];
event_stream.update_fields(acp::ToolCallUpdateFields {
title: Some(if paginated_matches.is_empty() {
"No matches".into()
} else if paginated_matches.len() == 1 {
"1 match".into()
} else {
format!("{} matches", paginated_matches.len())
}),
content: Some(
paginated_matches
.iter()
.map(|path| acp::ToolCallContent::Content {
content: acp::ContentBlock::ResourceLink(acp::ResourceLink {
uri: format!("file://{}", path.display()),
name: path.to_string_lossy().into(),
annotations: None,
description: None,
mime_type: None,
size: None,
title: None,
meta: None,
}),
})
.collect(),
),
..Default::default()
});
event_stream.update_fields(
acp::ToolCallUpdateFields::new()
.title(if paginated_matches.is_empty() {
"No matches".into()
} else if paginated_matches.len() == 1 {
"1 match".into()
} else {
format!("{} matches", paginated_matches.len())
})
.content(
paginated_matches
.iter()
.map(|path| {
acp::ToolCallContent::Content(acp::Content::new(
acp::ContentBlock::ResourceLink(acp::ResourceLink::new(
path.to_string_lossy(),
format!("file://{}", path.display()),
)),
))
})
.collect(),
),
);
Ok(FindPathToolOutput {
offset: input.offset,

View File

@@ -17,6 +17,9 @@ use crate::{AgentTool, Thread, ToolCallEventStream, outline};
/// Reads the content of the given file in the project.
///
/// - Never attempt to read a path that hasn't been previously mentioned.
/// - For large files, this tool returns a file outline with symbol names and line numbers instead of the full content.
/// This outline IS a successful response - use the line numbers to read specific sections with start_line/end_line.
/// Do NOT retry reading the same file without line numbers if you receive an outline.
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct ReadFileToolInput {
/// The relative path of the file to read.
@@ -149,15 +152,12 @@ impl AgentTool for ReadFileTool {
}
let file_path = input.path.clone();
let mut location = acp::ToolCallLocation::new(&abs_path);
if let Some(line) = input.start_line {
location = location.line(line.saturating_sub(1));
}
event_stream.update_fields(ToolCallUpdateFields {
locations: Some(vec![acp::ToolCallLocation {
path: abs_path.clone(),
line: input.start_line.map(|line| line.saturating_sub(1)),
meta: None,
}]),
..Default::default()
});
event_stream.update_fields(ToolCallUpdateFields::new().locations(vec![location]));
if image_store::is_image_file(&self.project, &project_path, cx) {
return cx.spawn(async move |cx| {
@@ -254,16 +254,15 @@ impl AgentTool for ReadFileTool {
if buffer_content.is_outline {
Ok(formatdoc! {"
This file was too big to read all at once.
SUCCESS: File outline retrieved. This file is too large to read all at once, so the outline below shows the file's structure with line numbers.
IMPORTANT: Do NOT retry this call without line numbers - you will get the same outline.
Instead, use the line numbers below to read specific sections by calling this tool again with start_line and end_line parameters.
{}
Using the line numbers in this outline, you can call this tool again
while specifying the start_line and end_line fields to see the
implementations of symbols in the outline.
Alternatively, you can fall back to the `grep` tool (if available)
to search the file for specific content.", buffer_content.text
NEXT STEPS: To read a specific symbol's implementation, call read_file with the same path plus start_line and end_line from the outline above.
For example, to read a function shown as [L100-150], use start_line: 100 and end_line: 150.", buffer_content.text
}
.into())
} else {
@@ -287,12 +286,9 @@ impl AgentTool for ReadFileTool {
text,
}
.to_string();
event_stream.update_fields(ToolCallUpdateFields {
content: Some(vec![acp::ToolCallContent::Content {
content: markdown.into(),
}]),
..Default::default()
})
event_stream.update_fields(ToolCallUpdateFields::new().content(vec![
acp::ToolCallContent::Content(acp::Content::new(markdown)),
]));
}
})?;
@@ -440,7 +436,7 @@ mod test {
let content = result.to_str().unwrap();
assert_eq!(
content.lines().skip(4).take(6).collect::<Vec<_>>(),
content.lines().skip(7).take(6).collect::<Vec<_>>(),
vec![
"struct Test0 [L1-4]",
" a [L2]",
@@ -475,7 +471,7 @@ mod test {
pretty_assertions::assert_eq!(
content
.lines()
.skip(4)
.skip(7)
.take(expected_content.len())
.collect::<Vec<_>>(),
expected_content

View File

@@ -112,10 +112,9 @@ impl AgentTool for TerminalTool {
.await?;
let terminal_id = terminal.id(cx)?;
event_stream.update_fields(acp::ToolCallUpdateFields {
content: Some(vec![acp::ToolCallContent::Terminal { terminal_id }]),
..Default::default()
});
event_stream.update_fields(acp::ToolCallUpdateFields::new().content(vec![
acp::ToolCallContent::Terminal(acp::Terminal::new(terminal_id)),
]));
let exit_status = terminal.wait_for_exit(cx)?.await;
let output = terminal.current_output(cx)?;

View File

@@ -43,10 +43,8 @@ impl AgentTool for ThinkingTool {
event_stream: ToolCallEventStream,
_cx: &mut App,
) -> Task<Result<String>> {
event_stream.update_fields(acp::ToolCallUpdateFields {
content: Some(vec![input.content.into()]),
..Default::default()
});
event_stream
.update_fields(acp::ToolCallUpdateFields::new().content(vec![input.content.into()]));
Task::ready(Ok("Finished thinking.".to_string()))
}
}

View File

@@ -76,10 +76,8 @@ impl AgentTool for WebSearchTool {
let response = match search_task.await {
Ok(response) => response,
Err(err) => {
event_stream.update_fields(acp::ToolCallUpdateFields {
title: Some("Web Search Failed".to_string()),
..Default::default()
});
event_stream
.update_fields(acp::ToolCallUpdateFields::new().title("Web Search Failed"));
return Err(err);
}
};
@@ -107,26 +105,23 @@ fn emit_update(response: &WebSearchResponse, event_stream: &ToolCallEventStream)
} else {
format!("{} results", response.results.len())
};
event_stream.update_fields(acp::ToolCallUpdateFields {
title: Some(format!("Searched the web: {result_text}")),
content: Some(
response
.results
.iter()
.map(|result| acp::ToolCallContent::Content {
content: acp::ContentBlock::ResourceLink(acp::ResourceLink {
name: result.title.clone(),
uri: result.url.clone(),
title: Some(result.title.clone()),
description: Some(result.text.clone()),
mime_type: None,
annotations: None,
size: None,
meta: None,
}),
})
.collect(),
),
..Default::default()
});
event_stream.update_fields(
acp::ToolCallUpdateFields::new()
.title(format!("Searched the web: {result_text}"))
.content(
response
.results
.iter()
.map(|result| {
acp::ToolCallContent::Content(acp::Content::new(
acp::ContentBlock::ResourceLink(
acp::ResourceLink::new(result.title.clone(), result.url.clone())
.title(result.title.clone())
.description(result.text.clone()),
),
))
})
.collect(),
),
);
}

View File

@@ -76,7 +76,7 @@ pub async fn connect(
Ok(Rc::new(conn) as _)
}
const MINIMUM_SUPPORTED_VERSION: acp::ProtocolVersion = acp::V1;
const MINIMUM_SUPPORTED_VERSION: acp::ProtocolVersion = acp::ProtocolVersion::V1;
impl AcpConnection {
pub async fn stdio(
@@ -173,29 +173,27 @@ impl AcpConnection {
});
})?;
let mut client_info = acp::Implementation::new("zed", version);
if let Some(release_channel) = release_channel {
client_info = client_info.title(release_channel);
}
let response = connection
.initialize(acp::InitializeRequest {
protocol_version: acp::VERSION,
client_capabilities: acp::ClientCapabilities {
fs: acp::FileSystemCapability {
read_text_file: true,
write_text_file: true,
meta: None,
},
terminal: true,
meta: Some(serde_json::json!({
// Experimental: Allow for rendering terminal output from the agents
"terminal_output": true,
"terminal-auth": true,
})),
},
client_info: Some(acp::Implementation {
name: "zed".to_owned(),
title: release_channel.map(|c| c.to_owned()),
version,
}),
meta: None,
})
.initialize(
acp::InitializeRequest::new(acp::ProtocolVersion::V1)
.client_capabilities(
acp::ClientCapabilities::new()
.fs(acp::FileSystemCapability::new()
.read_text_file(true)
.write_text_file(true))
.terminal(true)
// Experimental: Allow for rendering terminal output from the agents
.meta(acp::Meta::from_iter([
("terminal_output".into(), true.into()),
("terminal-auth".into(), true.into()),
])),
)
.client_info(client_info),
)
.await?;
if response.protocol_version < MINIMUM_SUPPORTED_VERSION {
@@ -253,14 +251,13 @@ impl AgentConnection for AcpConnection {
let default_model = self.default_model.clone();
let cwd = cwd.to_path_buf();
let context_server_store = project.read(cx).context_server_store().read(cx);
let mcp_servers =
if project.read(cx).is_local() {
context_server_store
.configured_server_ids()
.iter()
.filter_map(|id| {
let configuration = context_server_store.configuration_for_server(id)?;
match &*configuration {
let mcp_servers = if project.read(cx).is_local() {
context_server_store
.configured_server_ids()
.iter()
.filter_map(|id| {
let configuration = context_server_store.configuration_for_server(id)?;
match &*configuration {
project::context_server_store::ContextServerConfiguration::Custom {
command,
..
@@ -268,47 +265,41 @@ impl AgentConnection for AcpConnection {
| project::context_server_store::ContextServerConfiguration::Extension {
command,
..
} => Some(acp::McpServer::Stdio {
name: id.0.to_string(),
command: command.path.clone(),
args: command.args.clone(),
env: if let Some(env) = command.env.as_ref() {
env.iter()
.map(|(name, value)| acp::EnvVariable {
name: name.clone(),
value: value.clone(),
meta: None,
})
.collect()
} else {
vec![]
},
}),
} => Some(acp::McpServer::Stdio(
acp::McpServerStdio::new(id.0.to_string(), &command.path)
.args(command.args.clone())
.env(if let Some(env) = command.env.as_ref() {
env.iter()
.map(|(name, value)| acp::EnvVariable::new(name, value))
.collect()
} else {
vec![]
}),
)),
project::context_server_store::ContextServerConfiguration::Http {
url,
headers,
} => Some(acp::McpServer::Http {
name: id.0.to_string(),
url: url.to_string(),
headers: headers.iter().map(|(name, value)| acp::HttpHeader {
name: name.clone(),
value: value.clone(),
meta: None,
}).collect(),
}),
} => Some(acp::McpServer::Http(
acp::McpServerHttp::new(id.0.to_string(), url.to_string()).headers(
headers
.iter()
.map(|(name, value)| acp::HttpHeader::new(name, value))
.collect(),
),
)),
}
})
.collect()
} else {
// In SSH projects, the external agent is running on the remote
// machine, and currently we only run MCP servers on the local
// machine. So don't pass any MCP servers to the agent in that case.
Vec::new()
};
})
.collect()
} else {
// In SSH projects, the external agent is running on the remote
// machine, and currently we only run MCP servers on the local
// machine. So don't pass any MCP servers to the agent in that case.
Vec::new()
};
cx.spawn(async move |cx| {
let response = conn
.new_session(acp::NewSessionRequest { mcp_servers, cwd, meta: None })
.new_session(acp::NewSessionRequest::new(cwd).mcp_servers(mcp_servers))
.await
.map_err(|err| {
if err.code == acp::ErrorCode::AUTH_REQUIRED.code {
@@ -341,11 +332,7 @@ impl AgentConnection for AcpConnection {
let modes = modes.clone();
let conn = conn.clone();
async move |_| {
let result = conn.set_session_mode(acp::SetSessionModeRequest {
session_id,
mode_id: default_mode,
meta: None,
})
let result = conn.set_session_mode(acp::SetSessionModeRequest::new(session_id, default_mode))
.await.log_err();
if result.is_none() {
@@ -388,11 +375,7 @@ impl AgentConnection for AcpConnection {
let models = models.clone();
let conn = conn.clone();
async move |_| {
let result = conn.set_session_model(acp::SetSessionModelRequest {
session_id,
model_id: default_model,
meta: None,
})
let result = conn.set_session_model(acp::SetSessionModelRequest::new(session_id, default_model))
.await.log_err();
if result.is_none() {
@@ -456,12 +439,8 @@ impl AgentConnection for AcpConnection {
fn authenticate(&self, method_id: acp::AuthMethodId, cx: &mut App) -> Task<Result<()>> {
let conn = self.connection.clone();
cx.foreground_executor().spawn(async move {
conn.authenticate(acp::AuthenticateRequest {
method_id: method_id.clone(),
meta: None,
})
.await?;
conn.authenticate(acp::AuthenticateRequest::new(method_id))
.await?;
Ok(())
})
}
@@ -515,10 +494,7 @@ impl AgentConnection for AcpConnection {
&& (details.contains("This operation was aborted")
|| details.contains("The user aborted a request"))
{
Ok(acp::PromptResponse {
stop_reason: acp::StopReason::Cancelled,
meta: None,
})
Ok(acp::PromptResponse::new(acp::StopReason::Cancelled))
} else {
Err(anyhow!(details))
}
@@ -535,10 +511,7 @@ impl AgentConnection for AcpConnection {
session.suppress_abort_err = true;
}
let conn = self.connection.clone();
let params = acp::CancelNotification {
session_id: session_id.clone(),
meta: None,
};
let params = acp::CancelNotification::new(session_id.clone());
cx.foreground_executor()
.spawn(async move { conn.cancel(params).await })
.detach();
@@ -619,11 +592,7 @@ impl acp_thread::AgentSessionModes for AcpSessionModes {
let state = self.state.clone();
cx.foreground_executor().spawn(async move {
let result = connection
.set_session_mode(acp::SetSessionModeRequest {
session_id,
mode_id,
meta: None,
})
.set_session_mode(acp::SetSessionModeRequest::new(session_id, mode_id))
.await;
if result.is_err() {
@@ -682,11 +651,7 @@ impl acp_thread::AgentModelSelector for AcpModelSelector {
let state = self.state.clone();
cx.foreground_executor().spawn(async move {
let result = connection
.set_session_model(acp::SetSessionModelRequest {
session_id,
model_id,
meta: None,
})
.set_session_model(acp::SetSessionModelRequest::new(session_id, model_id))
.await;
if result.is_err() {
@@ -748,10 +713,7 @@ impl acp::Client for ClientDelegate {
let outcome = task.await;
Ok(acp::RequestPermissionResponse {
outcome,
meta: None,
})
Ok(acp::RequestPermissionResponse::new(outcome))
}
async fn write_text_file(
@@ -783,10 +745,7 @@ impl acp::Client for ClientDelegate {
let content = task.await?;
Ok(acp::ReadTextFileResponse {
content,
meta: None,
})
Ok(acp::ReadTextFileResponse::new(content))
}
async fn session_notification(
@@ -821,7 +780,7 @@ impl acp::Client for ClientDelegate {
if let Some(terminal_info) = meta.get("terminal_info") {
if let Some(id_str) = terminal_info.get("terminal_id").and_then(|v| v.as_str())
{
let terminal_id = acp::TerminalId(id_str.into());
let terminal_id = acp::TerminalId::new(id_str);
let cwd = terminal_info
.get("cwd")
.and_then(|v| v.as_str().map(PathBuf::from));
@@ -837,7 +796,7 @@ impl acp::Client for ClientDelegate {
let lower = cx.new(|cx| builder.subscribe(cx));
thread.on_terminal_provider_event(
TerminalProviderEvent::Created {
terminal_id: terminal_id.clone(),
terminal_id,
label: tc.title.clone(),
cwd,
output_byte_limit: None,
@@ -862,15 +821,12 @@ impl acp::Client for ClientDelegate {
if let Some(meta) = &tcu.meta {
if let Some(term_out) = meta.get("terminal_output") {
if let Some(id_str) = term_out.get("terminal_id").and_then(|v| v.as_str()) {
let terminal_id = acp::TerminalId(id_str.into());
let terminal_id = acp::TerminalId::new(id_str);
if let Some(s) = term_out.get("data").and_then(|v| v.as_str()) {
let data = s.as_bytes().to_vec();
let _ = session.thread.update(&mut self.cx.clone(), |thread, cx| {
thread.on_terminal_provider_event(
TerminalProviderEvent::Output {
terminal_id: terminal_id.clone(),
data,
},
TerminalProviderEvent::Output { terminal_id, data },
cx,
);
});
@@ -881,21 +837,19 @@ impl acp::Client for ClientDelegate {
// terminal_exit
if let Some(term_exit) = meta.get("terminal_exit") {
if let Some(id_str) = term_exit.get("terminal_id").and_then(|v| v.as_str()) {
let terminal_id = acp::TerminalId(id_str.into());
let status = acp::TerminalExitStatus {
exit_code: term_exit
.get("exit_code")
.and_then(|v| v.as_u64())
.map(|i| i as u32),
signal: term_exit
.get("signal")
.and_then(|v| v.as_str().map(|s| s.to_string())),
meta: None,
};
let terminal_id = acp::TerminalId::new(id_str);
let mut status = acp::TerminalExitStatus::new();
if let Some(code) = term_exit.get("exit_code").and_then(|v| v.as_u64()) {
status = status.exit_code(code as u32)
}
if let Some(signal) = term_exit.get("signal").and_then(|v| v.as_str()) {
status = status.signal(signal);
}
let _ = session.thread.update(&mut self.cx.clone(), |thread, cx| {
thread.on_terminal_provider_event(
TerminalProviderEvent::Exit {
terminal_id: terminal_id.clone(),
terminal_id,
status,
},
cx,
@@ -932,7 +886,7 @@ impl acp::Client for ClientDelegate {
// Register with renderer
let terminal_entity = thread.update(&mut self.cx.clone(), |thread, cx| {
thread.register_terminal_created(
acp::TerminalId(uuid::Uuid::new_v4().to_string().into()),
acp::TerminalId::new(uuid::Uuid::new_v4().to_string()),
format!("{} {}", args.command, args.args.join(" ")),
args.cwd.clone(),
args.output_byte_limit,
@@ -942,10 +896,7 @@ impl acp::Client for ClientDelegate {
})?;
let terminal_id =
terminal_entity.read_with(&self.cx, |terminal, _| terminal.id().clone())?;
Ok(acp::CreateTerminalResponse {
terminal_id,
meta: None,
})
Ok(acp::CreateTerminalResponse::new(terminal_id))
}
async fn kill_terminal_command(
@@ -1006,10 +957,7 @@ impl acp::Client for ClientDelegate {
})??
.await;
Ok(acp::WaitForTerminalExitResponse {
exit_status,
meta: None,
})
Ok(acp::WaitForTerminalExitResponse::new(exit_status))
}
}

View File

@@ -41,7 +41,7 @@ impl AgentServer for ClaudeCode {
settings
.as_ref()
.and_then(|s| s.default_mode.clone().map(|m| acp::SessionModeId(m.into())))
.and_then(|s| s.default_mode.clone().map(acp::SessionModeId::new))
}
fn set_default_mode(&self, mode_id: Option<acp::SessionModeId>, fs: Arc<dyn Fs>, cx: &mut App) {
@@ -62,7 +62,7 @@ impl AgentServer for ClaudeCode {
settings
.as_ref()
.and_then(|s| s.default_model.clone().map(|m| acp::ModelId(m.into())))
.and_then(|s| s.default_model.clone().map(acp::ModelId::new))
}
fn set_default_model(&self, model_id: Option<acp::ModelId>, fs: Arc<dyn Fs>, cx: &mut App) {

View File

@@ -42,7 +42,7 @@ impl AgentServer for Codex {
settings
.as_ref()
.and_then(|s| s.default_mode.clone().map(|m| acp::SessionModeId(m.into())))
.and_then(|s| s.default_mode.clone().map(acp::SessionModeId::new))
}
fn set_default_mode(&self, mode_id: Option<acp::SessionModeId>, fs: Arc<dyn Fs>, cx: &mut App) {
@@ -63,7 +63,7 @@ impl AgentServer for Codex {
settings
.as_ref()
.and_then(|s| s.default_model.clone().map(|m| acp::ModelId(m.into())))
.and_then(|s| s.default_model.clone().map(acp::ModelId::new))
}
fn set_default_model(&self, model_id: Option<acp::ModelId>, fs: Arc<dyn Fs>, cx: &mut App) {

View File

@@ -44,7 +44,7 @@ impl crate::AgentServer for CustomAgentServer {
settings
.as_ref()
.and_then(|s| s.default_mode().map(|m| acp::SessionModeId(m.into())))
.and_then(|s| s.default_mode().map(acp::SessionModeId::new))
}
fn set_default_mode(&self, mode_id: Option<acp::SessionModeId>, fs: Arc<dyn Fs>, cx: &mut App) {
@@ -80,7 +80,7 @@ impl crate::AgentServer for CustomAgentServer {
settings
.as_ref()
.and_then(|s| s.default_model().map(|m| acp::ModelId(m.into())))
.and_then(|s| s.default_model().map(acp::ModelId::new))
}
fn set_default_model(&self, model_id: Option<acp::ModelId>, fs: Arc<dyn Fs>, cx: &mut App) {

View File

@@ -82,26 +82,9 @@ where
.update(cx, |thread, cx| {
thread.send(
vec![
acp::ContentBlock::Text(acp::TextContent {
text: "Read the file ".into(),
annotations: None,
meta: None,
}),
acp::ContentBlock::ResourceLink(acp::ResourceLink {
uri: "foo.rs".into(),
name: "foo.rs".into(),
annotations: None,
description: None,
mime_type: None,
size: None,
title: None,
meta: None,
}),
acp::ContentBlock::Text(acp::TextContent {
text: " and tell me what the content of the println! is".into(),
annotations: None,
meta: None,
}),
"Read the file ".into(),
acp::ContentBlock::ResourceLink(acp::ResourceLink::new("foo.rs", "foo.rs")),
" and tell me what the content of the println! is".into(),
],
cx,
)
@@ -429,7 +412,7 @@ macro_rules! common_e2e_tests {
async fn tool_call_with_permission(cx: &mut ::gpui::TestAppContext) {
$crate::e2e_tests::test_tool_call_with_permission(
$server,
::agent_client_protocol::PermissionOptionId($allow_option_id.into()),
::agent_client_protocol::PermissionOptionId::new($allow_option_id),
cx,
)
.await;

View File

@@ -13,7 +13,8 @@ path = "src/agent_ui.rs"
doctest = false
[features]
test-support = ["gpui/test-support", "language/test-support"]
test-support = ["gpui/test-support", "language/test-support", "reqwest_client"]
unit-eval = []
[dependencies]
acp_thread.workspace = true
@@ -47,6 +48,7 @@ fs.workspace = true
futures.workspace = true
fuzzy.workspace = true
gpui.workspace = true
gpui_tokio.workspace = true
html_to_markdown.workspace = true
http_client.workspace = true
indoc.workspace = true
@@ -98,14 +100,17 @@ workspace.workspace = true
zed_actions.workspace = true
image.workspace = true
async-fs.workspace = true
reqwest_client = { workspace = true, optional = true }
[dev-dependencies]
acp_thread = { workspace = true, features = ["test-support"] }
agent = { workspace = true, features = ["test-support"] }
assistant_text_thread = { workspace = true, features = ["test-support"] }
buffer_diff = { workspace = true, features = ["test-support"] }
clock.workspace = true
db = { workspace = true, features = ["test-support"] }
editor = { workspace = true, features = ["test-support"] }
eval_utils.workspace = true
gpui = { workspace = true, "features" = ["test-support"] }
indoc.workspace = true
language = { workspace = true, "features" = ["test-support"] }
@@ -115,5 +120,6 @@ pretty_assertions.workspace = true
project = { workspace = true, features = ["test-support"] }
semver.workspace = true
rand.workspace = true
reqwest_client.workspace = true
tree-sitter-md.workspace = true
unindent.workspace = true

View File

@@ -432,24 +432,11 @@ mod tests {
let (workspace, cx) =
cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
let tool_call = acp::ToolCall {
id: acp::ToolCallId("tool".into()),
title: "Tool call".into(),
kind: acp::ToolKind::Other,
status: acp::ToolCallStatus::InProgress,
content: vec![acp::ToolCallContent::Diff {
diff: acp::Diff {
path: "/project/hello.txt".into(),
old_text: Some("hi world".into()),
new_text: "hello world".into(),
meta: None,
},
}],
locations: vec![],
raw_input: None,
raw_output: None,
meta: None,
};
let tool_call = acp::ToolCall::new("tool", "Tool call")
.status(acp::ToolCallStatus::InProgress)
.content(vec![acp::ToolCallContent::Diff(
acp::Diff::new("/project/hello.txt", "hello world").old_text("hi world"),
)]);
let connection = Rc::new(StubAgentConnection::new());
let thread = cx
.update(|_, cx| {

View File

@@ -225,8 +225,13 @@ impl MessageEditor {
.iter()
.find(|command| command.name == command_name)?;
let acp::AvailableCommandInput::Unstructured { mut hint } =
available_command.input.clone()?;
let acp::AvailableCommandInput::Unstructured(acp::UnstructuredCommandInput {
mut hint,
..
}) = available_command.input.clone()?
else {
return None;
};
let mut hint_pos = MultiBufferOffset(parsed_command.source_range.end) + 1usize;
if hint_pos > snapshot.len() {
@@ -403,34 +408,28 @@ impl MessageEditor {
} => {
all_tracked_buffers.extend(tracked_buffers.iter().cloned());
if supports_embedded_context {
acp::ContentBlock::Resource(acp::EmbeddedResource {
annotations: None,
resource:
acp::EmbeddedResourceResource::TextResourceContents(
acp::TextResourceContents {
mime_type: None,
text: content.clone(),
uri: uri.to_uri().to_string(),
meta: None,
},
acp::ContentBlock::Resource(acp::EmbeddedResource::new(
acp::EmbeddedResourceResource::TextResourceContents(
acp::TextResourceContents::new(
content.clone(),
uri.to_uri().to_string(),
),
meta: None,
})
),
))
} else {
acp::ContentBlock::ResourceLink(acp::ResourceLink {
name: uri.name(),
uri: uri.to_uri().to_string(),
annotations: None,
description: None,
mime_type: None,
size: None,
title: None,
meta: None,
})
acp::ContentBlock::ResourceLink(acp::ResourceLink::new(
uri.name(),
uri.to_uri().to_string(),
))
}
}
Mention::Image(mention_image) => {
let uri = match uri {
let mut image = acp::ImageContent::new(
mention_image.data.clone(),
mention_image.format.mime_type(),
);
if let Some(uri) = match uri {
MentionUri::File { .. } => Some(uri.to_uri().to_string()),
MentionUri::PastedImage => None,
other => {
@@ -440,25 +439,14 @@ impl MessageEditor {
);
None
}
} {
image = image.uri(uri)
};
acp::ContentBlock::Image(acp::ImageContent {
annotations: None,
data: mention_image.data.to_string(),
mime_type: mention_image.format.mime_type().into(),
uri,
meta: None,
})
acp::ContentBlock::Image(image)
}
Mention::Link => acp::ContentBlock::ResourceLink(acp::ResourceLink {
name: uri.name(),
uri: uri.to_uri().to_string(),
annotations: None,
description: None,
mime_type: None,
size: None,
title: None,
meta: None,
}),
Mention::Link => acp::ContentBlock::ResourceLink(
acp::ResourceLink::new(uri.name(), uri.to_uri().to_string()),
),
};
chunks.push(chunk);
ix = crease_range.end.0;
@@ -746,8 +734,7 @@ impl MessageEditor {
uri,
data,
mime_type,
annotations: _,
meta: _,
..
}) => {
let mention_uri = if let Some(uri) = uri {
MentionUri::parse(&uri, path_style)
@@ -773,7 +760,7 @@ impl MessageEditor {
}),
));
}
acp::ContentBlock::Audio(_) | acp::ContentBlock::Resource(_) => {}
_ => {}
}
}
@@ -1092,12 +1079,7 @@ mod tests {
assert!(error_message.contains("Available commands: none"));
// Now simulate Claude providing its list of available commands (which doesn't include file)
available_commands.replace(vec![acp::AvailableCommand {
name: "help".to_string(),
description: "Get help".to_string(),
input: None,
meta: None,
}]);
available_commands.replace(vec![acp::AvailableCommand::new("help", "Get help")]);
// Test that unsupported slash commands trigger an error when we have a list of available commands
editor.update_in(cx, |editor, window, cx| {
@@ -1211,20 +1193,12 @@ mod tests {
let history_store = cx.new(|cx| HistoryStore::new(text_thread_store, cx));
let prompt_capabilities = Rc::new(RefCell::new(acp::PromptCapabilities::default()));
let available_commands = Rc::new(RefCell::new(vec![
acp::AvailableCommand {
name: "quick-math".to_string(),
description: "2 + 2 = 4 - 1 = 3".to_string(),
input: None,
meta: None,
},
acp::AvailableCommand {
name: "say-hello".to_string(),
description: "Say hello to whoever you want".to_string(),
input: Some(acp::AvailableCommandInput::Unstructured {
hint: "<name>".to_string(),
}),
meta: None,
},
acp::AvailableCommand::new("quick-math", "2 + 2 = 4 - 1 = 3"),
acp::AvailableCommand::new("say-hello", "Say hello to whoever you want").input(
acp::AvailableCommandInput::Unstructured(acp::UnstructuredCommandInput::new(
"<name>",
)),
),
]));
let editor = workspace.update_in(&mut cx, |workspace, window, cx| {
@@ -1504,12 +1478,12 @@ mod tests {
editor.set_text("", window, cx);
});
prompt_capabilities.replace(acp::PromptCapabilities {
image: true,
audio: true,
embedded_context: true,
meta: None,
});
prompt_capabilities.replace(
acp::PromptCapabilities::new()
.image(true)
.audio(true)
.embedded_context(true),
);
cx.simulate_input("Lorem ");
@@ -1960,11 +1934,9 @@ mod tests {
cx,
);
// Enable embedded context so files are actually included
editor.prompt_capabilities.replace(acp::PromptCapabilities {
embedded_context: true,
meta: None,
..Default::default()
});
editor
.prompt_capabilities
.replace(acp::PromptCapabilities::new().embedded_context(true));
editor
})
});
@@ -2043,7 +2015,7 @@ mod tests {
// Create a thread metadata to insert as summary
let thread_metadata = agent::DbThreadMetadata {
id: acp::SessionId("thread-123".into()),
id: acp::SessionId::new("thread-123"),
title: "Previous Conversation".into(),
updated_at: chrono::Utc::now(),
};
@@ -2150,14 +2122,7 @@ mod tests {
.await
.unwrap();
assert_eq!(
content,
vec![acp::ContentBlock::Text(acp::TextContent {
text: "してhello world".into(),
annotations: None,
meta: None
})]
);
assert_eq!(content, vec!["してhello world".into()]);
}
#[gpui::test]
@@ -2236,38 +2201,24 @@ mod tests {
.0;
let main_rs_uri = if cfg!(windows) {
"file:///C:/project/src/main.rs".to_string()
"file:///C:/project/src/main.rs"
} else {
"file:///project/src/main.rs".to_string()
"file:///project/src/main.rs"
};
// When embedded context is `false` we should get a resource link
pretty_assertions::assert_eq!(
content,
vec![
acp::ContentBlock::Text(acp::TextContent {
text: "What is in ".to_string(),
annotations: None,
meta: None
}),
acp::ContentBlock::ResourceLink(acp::ResourceLink {
uri: main_rs_uri.clone(),
name: "main.rs".to_string(),
annotations: None,
meta: None,
description: None,
mime_type: None,
size: None,
title: None,
})
"What is in ".into(),
acp::ContentBlock::ResourceLink(acp::ResourceLink::new("main.rs", main_rs_uri))
]
);
message_editor.update(cx, |editor, _cx| {
editor.prompt_capabilities.replace(acp::PromptCapabilities {
embedded_context: true,
..Default::default()
})
editor
.prompt_capabilities
.replace(acp::PromptCapabilities::new().embedded_context(true))
});
let content = message_editor
@@ -2280,23 +2231,12 @@ mod tests {
pretty_assertions::assert_eq!(
content,
vec![
acp::ContentBlock::Text(acp::TextContent {
text: "What is in ".to_string(),
annotations: None,
meta: None
}),
acp::ContentBlock::Resource(acp::EmbeddedResource {
resource: acp::EmbeddedResourceResource::TextResourceContents(
acp::TextResourceContents {
text: file_content.to_string(),
uri: main_rs_uri,
mime_type: None,
meta: None
}
),
annotations: None,
meta: None
})
"What is in ".into(),
acp::ContentBlock::Resource(acp::EmbeddedResource::new(
acp::EmbeddedResourceResource::TextResourceContents(
acp::TextResourceContents::new(file_content, main_rs_uri)
)
))
]
);
}

View File

@@ -161,7 +161,7 @@ impl Render for ModeSelector {
.map(|mode| mode.name.clone())
.unwrap_or_else(|| "Unknown".into());
let this = cx.entity();
let this = cx.weak_entity();
let icon = if self.menu_handle.is_deployed() {
IconName::ChevronUp
@@ -222,7 +222,8 @@ impl Render for ModeSelector {
y: px(-2.0),
})
.menu(move |window, cx| {
Some(this.update(cx, |this, cx| this.build_context_menu(window, cx)))
this.update(cx, |this, cx| this.build_context_menu(window, cx))
.ok()
})
}
}

View File

@@ -464,7 +464,7 @@ mod tests {
models
.into_iter()
.map(|model| acp_thread::AgentModelInfo {
id: acp::ModelId(model.to_string().into()),
id: acp::ModelId::new(model.to_string()),
name: model.to_string().into(),
description: None,
icon: None,

View File

@@ -498,17 +498,7 @@ impl AcpThreadView {
Some(new_version_available_tx),
);
let agent_name = agent.name();
let timeout = cx.background_executor().timer(Duration::from_secs(30));
let connect_task = smol::future::or(
agent.connect(root_dir.as_deref(), delegate, cx),
async move {
timeout.await;
Err(anyhow::Error::new(LoadError::Other(
format!("{agent_name} is unable to initialize after 30 seconds.").into(),
)))
},
);
let connect_task = agent.connect(root_dir.as_deref(), delegate, cx);
let load_task = cx.spawn_in(window, async move |this, cx| {
let connection = match connect_task.await {
Ok((connection, login)) => {
@@ -1486,18 +1476,8 @@ impl AcpThreadView {
.iter()
.any(|method| method.id.0.as_ref() == "claude-login")
{
available_commands.push(acp::AvailableCommand {
name: "login".to_owned(),
description: "Authenticate".to_owned(),
input: None,
meta: None,
});
available_commands.push(acp::AvailableCommand {
name: "logout".to_owned(),
description: "Authenticate".to_owned(),
input: None,
meta: None,
});
available_commands.push(acp::AvailableCommand::new("login", "Authenticate"));
available_commands.push(acp::AvailableCommand::new("logout", "Authenticate"));
}
let has_commands = !available_commands.is_empty();
@@ -2572,7 +2552,7 @@ impl AcpThreadView {
acp::ToolKind::Think => IconName::ToolThink,
acp::ToolKind::Fetch => IconName::ToolWeb,
acp::ToolKind::SwitchMode => IconName::ArrowRightLeft,
acp::ToolKind::Other => IconName::ToolHammer,
acp::ToolKind::Other | _ => IconName::ToolHammer,
})
}
.size(IconSize::Small)
@@ -2824,7 +2804,7 @@ impl AcpThreadView {
})
.gap_0p5()
.children(options.iter().map(move |option| {
let option_id = SharedString::from(option.id.0.clone());
let option_id = SharedString::from(option.option_id.0.clone());
Button::new((option_id, entry_ix), option.name.clone())
.map(|this| {
let (this, action) = match option.kind {
@@ -2840,7 +2820,7 @@ impl AcpThreadView {
this.icon(IconName::Close).icon_color(Color::Error),
Some(&RejectOnce as &dyn Action),
),
acp::PermissionOptionKind::RejectAlways => {
acp::PermissionOptionKind::RejectAlways | _ => {
(this.icon(IconName::Close).icon_color(Color::Error), None)
}
};
@@ -2865,7 +2845,7 @@ impl AcpThreadView {
.label_size(LabelSize::Small)
.on_click(cx.listener({
let tool_call_id = tool_call_id.clone();
let option_id = option.id.clone();
let option_id = option.option_id.clone();
let option_kind = option.kind;
move |this, _, window, cx| {
this.authorize_tool_call(
@@ -3553,7 +3533,7 @@ impl AcpThreadView {
);
this.authenticate(
acp::AuthMethodId(method_id.clone()),
acp::AuthMethodId::new(method_id.clone()),
window,
cx,
)
@@ -3847,10 +3827,6 @@ impl AcpThreadView {
.text_xs()
.text_color(cx.theme().colors().text_muted)
.child(match entry.status {
acp::PlanEntryStatus::Pending => Icon::new(IconName::TodoPending)
.size(IconSize::Small)
.color(Color::Muted)
.into_any_element(),
acp::PlanEntryStatus::InProgress => {
Icon::new(IconName::TodoProgress)
.size(IconSize::Small)
@@ -3864,6 +3840,12 @@ impl AcpThreadView {
.color(Color::Success)
.into_any_element()
}
acp::PlanEntryStatus::Pending | _ => {
Icon::new(IconName::TodoPending)
.size(IconSize::Small)
.color(Color::Muted)
.into_any_element()
}
})
.child(MarkdownElement::new(
entry.content.clone(),
@@ -4437,7 +4419,7 @@ impl AcpThreadView {
self.authorize_tool_call(
tool_call.id.clone(),
option.id.clone(),
option.option_id.clone(),
option.kind,
window,
cx,
@@ -6253,27 +6235,18 @@ pub(crate) mod tests {
async fn test_notification_for_tool_authorization(cx: &mut TestAppContext) {
init_test(cx);
let tool_call_id = acp::ToolCallId("1".into());
let tool_call = acp::ToolCall {
id: tool_call_id.clone(),
title: "Label".into(),
kind: acp::ToolKind::Edit,
status: acp::ToolCallStatus::Pending,
content: vec!["hi".into()],
locations: vec![],
raw_input: None,
raw_output: None,
meta: None,
};
let tool_call_id = acp::ToolCallId::new("1");
let tool_call = acp::ToolCall::new(tool_call_id.clone(), "Label")
.kind(acp::ToolKind::Edit)
.content(vec!["hi".into()]);
let connection =
StubAgentConnection::new().with_permission_requests(HashMap::from_iter([(
tool_call_id,
vec![acp::PermissionOption {
id: acp::PermissionOptionId("1".into()),
name: "Allow".into(),
kind: acp::PermissionOptionKind::AllowOnce,
meta: None,
}],
vec![acp::PermissionOption::new(
"1".into(),
"Allow",
acp::PermissionOptionKind::AllowOnce,
)],
)]));
connection.set_next_prompt_updates(vec![acp::SessionUpdate::ToolCall(tool_call)]);
@@ -6492,10 +6465,7 @@ pub(crate) mod tests {
fn default_response() -> Self {
let conn = StubAgentConnection::new();
conn.set_next_prompt_updates(vec![acp::SessionUpdate::AgentMessageChunk(
acp::ContentChunk {
content: "Default response".into(),
meta: None,
},
acp::ContentChunk::new("Default response".into()),
)]);
Self::new(conn)
}
@@ -6552,13 +6522,13 @@ pub(crate) mod tests {
self,
project,
action_log,
SessionId("test".into()),
watch::Receiver::constant(acp::PromptCapabilities {
image: true,
audio: true,
embedded_context: true,
meta: None,
}),
SessionId::new("test"),
watch::Receiver::constant(
acp::PromptCapabilities::new()
.image(true)
.audio(true)
.embedded_context(true),
),
cx,
)
})))
@@ -6616,13 +6586,13 @@ pub(crate) mod tests {
self,
project,
action_log,
SessionId("test".into()),
watch::Receiver::constant(acp::PromptCapabilities {
image: true,
audio: true,
embedded_context: true,
meta: None,
}),
SessionId::new("test"),
watch::Receiver::constant(
acp::PromptCapabilities::new()
.image(true)
.audio(true)
.embedded_context(true),
),
cx,
)
})))
@@ -6646,10 +6616,7 @@ pub(crate) mod tests {
_params: acp::PromptRequest,
_cx: &mut App,
) -> Task<gpui::Result<acp::PromptResponse>> {
Task::ready(Ok(acp::PromptResponse {
stop_reason: acp::StopReason::Refusal,
meta: None,
}))
Task::ready(Ok(acp::PromptResponse::new(acp::StopReason::Refusal)))
}
fn cancel(&self, _session_id: &acp::SessionId, _cx: &mut App) {
@@ -6717,24 +6684,14 @@ pub(crate) mod tests {
.unwrap();
// First user message
connection.set_next_prompt_updates(vec![acp::SessionUpdate::ToolCall(acp::ToolCall {
id: acp::ToolCallId("tool1".into()),
title: "Edit file 1".into(),
kind: acp::ToolKind::Edit,
status: acp::ToolCallStatus::Completed,
content: vec![acp::ToolCallContent::Diff {
diff: acp::Diff {
path: "/project/test1.txt".into(),
old_text: Some("old content 1".into()),
new_text: "new content 1".into(),
meta: None,
},
}],
locations: vec![],
raw_input: None,
raw_output: None,
meta: None,
})]);
connection.set_next_prompt_updates(vec![acp::SessionUpdate::ToolCall(
acp::ToolCall::new("tool1", "Edit file 1")
.kind(acp::ToolKind::Edit)
.status(acp::ToolCallStatus::Completed)
.content(vec![acp::ToolCallContent::Diff(
acp::Diff::new("/project/test1.txt", "new content 1").old_text("old content 1"),
)]),
)]);
thread
.update(cx, |thread, cx| thread.send_raw("Give me a diff", cx))
@@ -6760,24 +6717,14 @@ pub(crate) mod tests {
});
// Second user message
connection.set_next_prompt_updates(vec![acp::SessionUpdate::ToolCall(acp::ToolCall {
id: acp::ToolCallId("tool2".into()),
title: "Edit file 2".into(),
kind: acp::ToolKind::Edit,
status: acp::ToolCallStatus::Completed,
content: vec![acp::ToolCallContent::Diff {
diff: acp::Diff {
path: "/project/test2.txt".into(),
old_text: Some("old content 2".into()),
new_text: "new content 2".into(),
meta: None,
},
}],
locations: vec![],
raw_input: None,
raw_output: None,
meta: None,
})]);
connection.set_next_prompt_updates(vec![acp::SessionUpdate::ToolCall(
acp::ToolCall::new("tool2", "Edit file 2")
.kind(acp::ToolKind::Edit)
.status(acp::ToolCallStatus::Completed)
.content(vec![acp::ToolCallContent::Diff(
acp::Diff::new("/project/test2.txt", "new content 2").old_text("old content 2"),
)]),
)]);
thread
.update(cx, |thread, cx| thread.send_raw("Another one", cx))
@@ -6851,14 +6798,7 @@ pub(crate) mod tests {
let connection = StubAgentConnection::new();
connection.set_next_prompt_updates(vec![acp::SessionUpdate::AgentMessageChunk(
acp::ContentChunk {
content: acp::ContentBlock::Text(acp::TextContent {
text: "Response".into(),
annotations: None,
meta: None,
}),
meta: None,
},
acp::ContentChunk::new("Response".into()),
)]);
let (thread_view, cx) = setup_thread_view(StubAgentServer::new(connection), cx).await;
@@ -6944,14 +6884,7 @@ pub(crate) mod tests {
let connection = StubAgentConnection::new();
connection.set_next_prompt_updates(vec![acp::SessionUpdate::AgentMessageChunk(
acp::ContentChunk {
content: acp::ContentBlock::Text(acp::TextContent {
text: "Response".into(),
annotations: None,
meta: None,
}),
meta: None,
},
acp::ContentChunk::new("Response".into()),
)]);
let (thread_view, cx) =
@@ -6991,14 +6924,7 @@ pub(crate) mod tests {
// Send
connection.set_next_prompt_updates(vec![acp::SessionUpdate::AgentMessageChunk(
acp::ContentChunk {
content: acp::ContentBlock::Text(acp::TextContent {
text: "New Response".into(),
annotations: None,
meta: None,
}),
meta: None,
},
acp::ContentChunk::new("New Response".into()),
)]);
user_message_editor.update_in(cx, |_editor, window, cx| {
@@ -7086,14 +7012,7 @@ pub(crate) mod tests {
cx.update(|_, cx| {
connection.send_update(
session_id.clone(),
acp::SessionUpdate::AgentMessageChunk(acp::ContentChunk {
content: acp::ContentBlock::Text(acp::TextContent {
text: "Response".into(),
annotations: None,
meta: None,
}),
meta: None,
}),
acp::SessionUpdate::AgentMessageChunk(acp::ContentChunk::new("Response".into())),
cx,
);
connection.end_turn(session_id, acp::StopReason::EndTurn);
@@ -7145,10 +7064,9 @@ pub(crate) mod tests {
cx.update(|_, cx| {
connection.send_update(
session_id.clone(),
acp::SessionUpdate::AgentMessageChunk(acp::ContentChunk {
content: "Message 1 resp".into(),
meta: None,
}),
acp::SessionUpdate::AgentMessageChunk(acp::ContentChunk::new(
"Message 1 resp".into(),
)),
cx,
);
});
@@ -7182,10 +7100,7 @@ pub(crate) mod tests {
// Simulate a response sent after beginning to cancel
connection.send_update(
session_id.clone(),
acp::SessionUpdate::AgentMessageChunk(acp::ContentChunk {
content: "onse".into(),
meta: None,
}),
acp::SessionUpdate::AgentMessageChunk(acp::ContentChunk::new("onse".into())),
cx,
);
});
@@ -7216,10 +7131,9 @@ pub(crate) mod tests {
cx.update(|_, cx| {
connection.send_update(
session_id.clone(),
acp::SessionUpdate::AgentMessageChunk(acp::ContentChunk {
content: "Message 2 response".into(),
meta: None,
}),
acp::SessionUpdate::AgentMessageChunk(acp::ContentChunk::new(
"Message 2 response".into(),
)),
cx,
);
connection.end_turn(session_id.clone(), acp::StopReason::EndTurn);
@@ -7258,14 +7172,7 @@ pub(crate) mod tests {
let connection = StubAgentConnection::new();
connection.set_next_prompt_updates(vec![acp::SessionUpdate::AgentMessageChunk(
acp::ContentChunk {
content: acp::ContentBlock::Text(acp::TextContent {
text: "Response".into(),
annotations: None,
meta: None,
}),
meta: None,
},
acp::ContentChunk::new("Response".into()),
)]);
let (thread_view, cx) = setup_thread_view(StubAgentServer::new(connection), cx).await;
@@ -7344,14 +7251,7 @@ pub(crate) mod tests {
let connection = StubAgentConnection::new();
connection.set_next_prompt_updates(vec![acp::SessionUpdate::AgentMessageChunk(
acp::ContentChunk {
content: acp::ContentBlock::Text(acp::TextContent {
text: "Response".into(),
annotations: None,
meta: None,
}),
meta: None,
},
acp::ContentChunk::new("Response".into()),
)]);
let (thread_view, cx) = setup_thread_view(StubAgentServer::new(connection), cx).await;
@@ -7399,54 +7299,4 @@ pub(crate) mod tests {
assert_eq!(text, expected_txt);
})
}
#[gpui::test]
async fn test_initialize_timeout(cx: &mut TestAppContext) {
init_test(cx);
struct InfiniteInitialize;
impl AgentServer for InfiniteInitialize {
fn telemetry_id(&self) -> &'static str {
"test"
}
fn logo(&self) -> ui::IconName {
ui::IconName::Ai
}
fn name(&self) -> SharedString {
"Test".into()
}
fn connect(
&self,
_root_dir: Option<&Path>,
_delegate: AgentServerDelegate,
cx: &mut App,
) -> Task<gpui::Result<(Rc<dyn AgentConnection>, Option<task::SpawnInTerminal>)>>
{
cx.spawn(async |_| futures::future::pending().await)
}
fn into_any(self: Rc<Self>) -> Rc<dyn Any> {
self
}
}
let (thread_view, cx) = setup_thread_view(InfiniteInitialize, cx).await;
cx.executor().advance_clock(Duration::from_secs(31));
cx.run_until_parked();
let error = thread_view.read_with(cx, |thread_view, _| match &thread_view.thread_state {
ThreadState::LoadError(err) => err.clone(),
_ => panic!("Incorrect thread state"),
});
match error {
LoadError::Other(str) => assert!(str.contains("initialize")),
_ => panic!("Unexpected load error"),
}
}
}

View File

@@ -2685,16 +2685,17 @@ impl rules_library::InlineAssistDelegate for PromptLibraryInlineAssist {
return;
};
let project = workspace.read(cx).project().downgrade();
let thread_store = panel.read(cx).thread_store().clone();
assistant.assist(
prompt_editor,
self.workspace.clone(),
project,
panel.read(cx).thread_store().clone(),
thread_store,
None,
initial_prompt,
window,
cx,
)
);
})
}

View File

@@ -7,6 +7,8 @@ mod buffer_codegen;
mod completion_provider;
mod context;
mod context_server_configuration;
#[cfg(test)]
mod evals;
mod inline_assistant;
mod inline_prompt_editor;
mod language_model_selector;

View File

@@ -719,6 +719,7 @@ impl CodegenAlternative {
output_tokens = usage.output_tokens,
)
}
cx.emit(CodegenEvent::Finished);
cx.notify();
})

View File

@@ -1114,7 +1114,6 @@ impl<T: PromptCompletionProviderDelegate> CompletionProvider for PromptCompletio
position: language::Anchor,
_text: &str,
_trigger_in_words: bool,
_menu_is_open: bool,
cx: &mut Context<Editor>,
) -> bool {
let buffer = buffer.read(cx);

View File

@@ -0,0 +1,89 @@
use std::str::FromStr;
use crate::inline_assistant::test::run_inline_assistant_test;
use eval_utils::{EvalOutput, NoProcessor};
use gpui::TestAppContext;
use language_model::{LanguageModelRegistry, SelectedModel};
use rand::{SeedableRng as _, rngs::StdRng};
#[test]
#[cfg_attr(not(feature = "unit-eval"), ignore)]
fn eval_single_cursor_edit() {
eval_utils::eval(20, 1.0, NoProcessor, move || {
run_eval(
&EvalInput {
prompt: "Rename this variable to buffer_text".to_string(),
buffer: indoc::indoc! {"
struct EvalExampleStruct {
text: Strˇing,
prompt: String,
}
"}
.to_string(),
},
&|_, output| {
let expected = indoc::indoc! {"
struct EvalExampleStruct {
buffer_text: String,
prompt: String,
}
"};
if output == expected {
EvalOutput {
outcome: eval_utils::OutcomeKind::Passed,
data: "Passed!".to_string(),
metadata: (),
}
} else {
EvalOutput {
outcome: eval_utils::OutcomeKind::Failed,
data: format!("Failed to rename variable, output: {}", output),
metadata: (),
}
}
},
)
});
}
struct EvalInput {
buffer: String,
prompt: String,
}
fn run_eval(
input: &EvalInput,
judge: &dyn Fn(&EvalInput, &str) -> eval_utils::EvalOutput<()>,
) -> eval_utils::EvalOutput<()> {
let dispatcher = gpui::TestDispatcher::new(StdRng::from_os_rng());
let mut cx = TestAppContext::build(dispatcher, None);
cx.skip_drawing();
let buffer_text = run_inline_assistant_test(
input.buffer.clone(),
input.prompt.clone(),
|cx| {
// Reconfigure to use a real model instead of the fake one
let model_name = std::env::var("ZED_AGENT_MODEL")
.unwrap_or("anthropic/claude-sonnet-4-latest".into());
let selected_model = SelectedModel::from_str(&model_name)
.expect("Invalid model format. Use 'provider/model-id'");
log::info!("Selected model: {selected_model:?}");
cx.update(|_, cx| {
LanguageModelRegistry::global(cx).update(cx, |registry, cx| {
registry.select_inline_assistant_model(Some(&selected_model), cx);
});
});
},
|_cx| {
log::info!("Waiting for actual response from the LLM...");
},
&mut cx,
);
judge(input, &buffer_text)
}

View File

@@ -32,7 +32,7 @@ use editor::{
},
};
use fs::Fs;
use futures::FutureExt;
use futures::{FutureExt, channel::mpsc};
use gpui::{
App, Context, Entity, Focusable, Global, HighlightStyle, Subscription, Task, UpdateGlobal,
WeakEntity, Window, point,
@@ -102,6 +102,7 @@ pub struct InlineAssistant {
prompt_builder: Arc<PromptBuilder>,
telemetry: Arc<Telemetry>,
fs: Arc<dyn Fs>,
_inline_assistant_completions: Option<mpsc::UnboundedSender<anyhow::Result<InlineAssistId>>>,
}
impl Global for InlineAssistant {}
@@ -123,9 +124,18 @@ impl InlineAssistant {
prompt_builder,
telemetry,
fs,
_inline_assistant_completions: None,
}
}
#[cfg(any(test, feature = "test-support"))]
pub fn set_completion_receiver(
&mut self,
sender: mpsc::UnboundedSender<anyhow::Result<InlineAssistId>>,
) {
self._inline_assistant_completions = Some(sender);
}
pub fn register_workspace(
&mut self,
workspace: &Entity<Workspace>,
@@ -287,7 +297,7 @@ impl InlineAssistant {
action.prompt.clone(),
window,
cx,
)
);
})
}
InlineAssistTarget::Terminal(active_terminal) => {
@@ -301,8 +311,8 @@ impl InlineAssistant {
action.prompt.clone(),
window,
cx,
)
})
);
});
}
};
@@ -598,13 +608,13 @@ impl InlineAssistant {
initial_prompt: Option<String>,
window: &mut Window,
cx: &mut App,
) {
) -> Option<InlineAssistId> {
let snapshot = editor.update(cx, |editor, cx| editor.snapshot(window, cx));
let Some((codegen_ranges, newest_selection)) =
self.codegen_ranges(editor, &snapshot, window, cx)
else {
return;
return None;
};
let assist_to_focus = self.batch_assist(
@@ -624,6 +634,8 @@ impl InlineAssistant {
if let Some(assist_id) = assist_to_focus {
self.focus_assist(assist_id, window, cx);
}
assist_to_focus
}
pub fn suggest_assist(
@@ -1740,6 +1752,16 @@ impl InlineAssist {
&& assist.decorations.is_none()
&& let Some(workspace) = assist.workspace.upgrade()
{
#[cfg(any(test, feature = "test-support"))]
if let Some(sender) = &mut this._inline_assistant_completions {
sender
.unbounded_send(Err(anyhow::anyhow!(
"Inline assistant error: {}",
error
)))
.ok();
}
let error = format!("Inline assistant error: {}", error);
workspace.update(cx, |workspace, cx| {
struct InlineAssistantError;
@@ -1750,6 +1772,11 @@ impl InlineAssist {
workspace.show_toast(Toast::new(id, error), cx);
})
} else {
#[cfg(any(test, feature = "test-support"))]
if let Some(sender) = &mut this._inline_assistant_completions {
sender.unbounded_send(Ok(assist_id)).ok();
}
}
if assist.decorations.is_none() {
@@ -1943,3 +1970,160 @@ fn merge_ranges(ranges: &mut Vec<Range<Anchor>>, buffer: &MultiBufferSnapshot) {
}
}
}
#[cfg(any(test, feature = "test-support"))]
pub mod test {
use std::sync::Arc;
use agent::HistoryStore;
use assistant_text_thread::TextThreadStore;
use client::{Client, UserStore};
use editor::{Editor, MultiBuffer, MultiBufferOffset};
use fs::FakeFs;
use futures::channel::mpsc;
use gpui::{AppContext, TestAppContext, UpdateGlobal as _};
use language::Buffer;
use language_model::LanguageModelRegistry;
use project::Project;
use prompt_store::PromptBuilder;
use smol::stream::StreamExt as _;
use util::test::marked_text_ranges;
use workspace::Workspace;
use crate::InlineAssistant;
pub fn run_inline_assistant_test<SetupF, TestF>(
base_buffer: String,
prompt: String,
setup: SetupF,
test: TestF,
cx: &mut TestAppContext,
) -> String
where
SetupF: FnOnce(&mut gpui::VisualTestContext),
TestF: FnOnce(&mut gpui::VisualTestContext),
{
let fs = FakeFs::new(cx.executor());
let app_state = cx.update(|cx| workspace::AppState::test(cx));
let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
let http = Arc::new(reqwest_client::ReqwestClient::user_agent("agent tests").unwrap());
let client = cx.update(|cx| {
cx.set_http_client(http);
Client::production(cx)
});
let mut inline_assistant =
InlineAssistant::new(fs.clone(), prompt_builder, client.telemetry().clone());
let (tx, mut completion_rx) = mpsc::unbounded();
inline_assistant.set_completion_receiver(tx);
// Initialize settings and client
cx.update(|cx| {
gpui_tokio::init(cx);
settings::init(cx);
client::init(&client, cx);
workspace::init(app_state.clone(), cx);
let user_store = cx.new(|cx| UserStore::new(client.clone(), cx));
language_model::init(client.clone(), cx);
language_models::init(user_store, client.clone(), cx);
cx.set_global(inline_assistant);
});
let project = cx
.executor()
.block_test(async { Project::test(fs.clone(), [], cx).await });
// Create workspace with window
let (workspace, cx) = cx.add_window_view(|window, cx| {
window.activate_window();
Workspace::new(None, project.clone(), app_state.clone(), window, cx)
});
setup(cx);
let (_editor, buffer) = cx.update(|window, cx| {
let buffer = cx.new(|cx| Buffer::local("", cx));
let multibuffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx));
let editor = cx.new(|cx| Editor::for_multibuffer(multibuffer, None, window, cx));
editor.update(cx, |editor, cx| {
let (unmarked_text, selection_ranges) = marked_text_ranges(&base_buffer, true);
editor.set_text(unmarked_text, window, cx);
editor.change_selections(Default::default(), window, cx, |s| {
s.select_ranges(
selection_ranges.into_iter().map(|range| {
MultiBufferOffset(range.start)..MultiBufferOffset(range.end)
}),
)
})
});
let text_thread_store = cx.new(|cx| TextThreadStore::fake(project.clone(), cx));
let history_store = cx.new(|cx| HistoryStore::new(text_thread_store, cx));
// Add editor to workspace
workspace.update(cx, |workspace, cx| {
workspace.add_item_to_active_pane(Box::new(editor.clone()), None, true, window, cx);
});
// Call assist method
InlineAssistant::update_global(cx, |inline_assistant, cx| {
let assist_id = inline_assistant
.assist(
&editor,
workspace.downgrade(),
project.downgrade(),
history_store, // thread_store
None, // prompt_store
Some(prompt),
window,
cx,
)
.unwrap();
inline_assistant.start_assist(assist_id, window, cx);
});
(editor, buffer)
});
cx.run_until_parked();
test(cx);
cx.executor()
.block_test(async { completion_rx.next().await });
buffer.read_with(cx, |buffer, _| buffer.text())
}
#[allow(unused)]
pub fn test_inline_assistant(
base_buffer: &'static str,
llm_output: &'static str,
cx: &mut TestAppContext,
) -> String {
run_inline_assistant_test(
base_buffer.to_string(),
"Prompt doesn't matter because we're using a fake model".to_string(),
|cx| {
cx.update(|_, cx| LanguageModelRegistry::test(cx));
},
|cx| {
let fake_model = cx.update(|_, cx| {
LanguageModelRegistry::global(cx)
.update(cx, |registry, _| registry.fake_model())
});
let fake = fake_model.as_fake();
// let fake = fake_model;
fake.send_last_completion_stream_text_chunk(llm_output.to_string());
fake.end_last_completion_stream();
// Run again to process the model's response
cx.run_until_parked();
},
cx,
)
}
}

View File

@@ -341,7 +341,6 @@ impl CompletionProvider for SlashCommandCompletionProvider {
position: language::Anchor,
_text: &str,
_trigger_in_words: bool,
_menu_is_open: bool,
cx: &mut Context<Editor>,
) -> bool {
let buffer = buffer.read(cx);

View File

@@ -2622,11 +2622,13 @@ impl SearchableItem for TextThreadEditor {
fn update_matches(
&mut self,
matches: &[Self::Match],
active_match_index: Option<usize>,
window: &mut Window,
cx: &mut Context<Self>,
) {
self.editor
.update(cx, |editor, cx| editor.update_matches(matches, window, cx));
self.editor.update(cx, |editor, cx| {
editor.update_matches(matches, active_match_index, window, cx)
});
}
fn query_suggestion(&mut self, window: &mut Window, cx: &mut Context<Self>) -> String {

View File

@@ -584,41 +584,100 @@ impl Model {
}
}
pub fn cross_region_inference_id(&self, region: &str) -> anyhow::Result<String> {
pub fn cross_region_inference_id(
&self,
region: &str,
allow_global: bool,
) -> anyhow::Result<String> {
// List derived from here:
// https://docs.aws.amazon.com/bedrock/latest/userguide/inference-profiles-support.html#inference-profiles-support-system
let model_id = self.request_id();
let supports_global = matches!(
self,
Model::ClaudeOpus4_5
| Model::ClaudeOpus4_5Thinking
| Model::ClaudeHaiku4_5
| Model::ClaudeSonnet4
| Model::ClaudeSonnet4Thinking
| Model::ClaudeSonnet4_5
| Model::ClaudeSonnet4_5Thinking
);
let region_group = if region.starts_with("us-gov-") {
"us-gov"
} else if region.starts_with("us-") {
"us"
} else if region.starts_with("us-")
|| region.starts_with("ca-")
|| region.starts_with("sa-")
{
if allow_global && supports_global {
"global"
} else {
"us"
}
} else if region.starts_with("eu-") {
"eu"
if allow_global && supports_global {
"global"
} else {
"eu"
}
} else if region.starts_with("ap-") || region == "me-central-1" || region == "me-south-1" {
"apac"
} else if region.starts_with("ca-") || region.starts_with("sa-") {
// Canada and South America regions - default to US profiles
"us"
if allow_global && supports_global {
"global"
} else {
"apac"
}
} else {
anyhow::bail!("Unsupported Region {region}");
};
let model_id = self.request_id();
match (self, region_group, region) {
(Model::Custom { .. }, _, _) => Ok(self.request_id().into()),
match (self, region_group) {
// Custom models can't have CRI IDs
(Model::Custom { .. }, _) => Ok(self.request_id().into()),
(
Model::ClaudeOpus4_5
| Model::ClaudeOpus4_5Thinking
| Model::ClaudeHaiku4_5
| Model::ClaudeSonnet4
| Model::ClaudeSonnet4Thinking
| Model::ClaudeSonnet4_5
| Model::ClaudeSonnet4_5Thinking,
"global",
_,
) => Ok(format!("{}.{}", region_group, model_id)),
// Models with US Gov only
(Model::Claude3_5Sonnet, "us-gov") | (Model::Claude3Haiku, "us-gov") => {
Ok(format!("{}.{}", region_group, model_id))
(
Model::Claude3Haiku
| Model::Claude3_5Sonnet
| Model::Claude3_7Sonnet
| Model::Claude3_7SonnetThinking
| Model::ClaudeSonnet4_5
| Model::ClaudeSonnet4_5Thinking,
"us-gov",
_,
) => Ok(format!("{}.{}", region_group, model_id)),
(
Model::ClaudeHaiku4_5 | Model::ClaudeSonnet4_5 | Model::ClaudeSonnet4_5Thinking,
"apac",
"ap-southeast-2" | "ap-southeast-4",
) => Ok(format!("au.{}", model_id)),
(
Model::ClaudeHaiku4_5 | Model::ClaudeSonnet4_5 | Model::ClaudeSonnet4_5Thinking,
"apac",
"ap-northeast-1" | "ap-northeast-3",
) => Ok(format!("jp.{}", model_id)),
(Model::AmazonNovaLite, "us", r) if r.starts_with("ca-") => {
Ok(format!("ca.{}", model_id))
}
// Available everywhere
(Model::AmazonNovaLite | Model::AmazonNovaMicro | Model::AmazonNovaPro, _) => {
Ok(format!("{}.{}", region_group, model_id))
}
// Models in US
(
Model::AmazonNovaPremier
| Model::AmazonNovaLite
| Model::AmazonNovaMicro
| Model::AmazonNovaPro
| Model::Claude3_5Haiku
| Model::ClaudeHaiku4_5
| Model::Claude3_5Sonnet
@@ -655,16 +714,18 @@ impl Model {
| Model::PalmyraWriterX4
| Model::PalmyraWriterX5,
"us",
_,
) => Ok(format!("{}.{}", region_group, model_id)),
// Models available in EU
(
Model::Claude3_5Sonnet
Model::AmazonNovaLite
| Model::AmazonNovaMicro
| Model::AmazonNovaPro
| Model::Claude3_5Sonnet
| Model::ClaudeHaiku4_5
| Model::Claude3_7Sonnet
| Model::Claude3_7SonnetThinking
| Model::ClaudeSonnet4
| Model::ClaudeSonnet4Thinking
| Model::ClaudeSonnet4_5
| Model::ClaudeSonnet4_5Thinking
| Model::Claude3Haiku
@@ -673,26 +734,26 @@ impl Model {
| Model::MetaLlama323BInstructV1
| Model::MistralPixtralLarge2502V1,
"eu",
_,
) => Ok(format!("{}.{}", region_group, model_id)),
// Models available in APAC
(
Model::Claude3_5Sonnet
Model::AmazonNovaLite
| Model::AmazonNovaMicro
| Model::AmazonNovaPro
| Model::Claude3_5Sonnet
| Model::Claude3_5SonnetV2
| Model::ClaudeHaiku4_5
| Model::Claude3Haiku
| Model::Claude3Sonnet
| Model::Claude3_7Sonnet
| Model::Claude3_7SonnetThinking
| Model::ClaudeSonnet4
| Model::ClaudeSonnet4Thinking
| Model::ClaudeSonnet4_5
| Model::ClaudeSonnet4_5Thinking,
| Model::Claude3Haiku
| Model::Claude3Sonnet,
"apac",
_,
) => Ok(format!("{}.{}", region_group, model_id)),
// Any other combination is not supported
_ => Ok(self.request_id().into()),
_ => Ok(model_id.into()),
}
}
}
@@ -705,15 +766,15 @@ mod tests {
fn test_us_region_inference_ids() -> anyhow::Result<()> {
// Test US regions
assert_eq!(
Model::Claude3_5SonnetV2.cross_region_inference_id("us-east-1")?,
Model::Claude3_5SonnetV2.cross_region_inference_id("us-east-1", false)?,
"us.anthropic.claude-3-5-sonnet-20241022-v2:0"
);
assert_eq!(
Model::Claude3_5SonnetV2.cross_region_inference_id("us-west-2")?,
Model::Claude3_5SonnetV2.cross_region_inference_id("us-west-2", false)?,
"us.anthropic.claude-3-5-sonnet-20241022-v2:0"
);
assert_eq!(
Model::AmazonNovaPro.cross_region_inference_id("us-east-2")?,
Model::AmazonNovaPro.cross_region_inference_id("us-east-2", false)?,
"us.amazon.nova-pro-v1:0"
);
Ok(())
@@ -723,19 +784,19 @@ mod tests {
fn test_eu_region_inference_ids() -> anyhow::Result<()> {
// Test European regions
assert_eq!(
Model::ClaudeSonnet4.cross_region_inference_id("eu-west-1")?,
Model::ClaudeSonnet4.cross_region_inference_id("eu-west-1", false)?,
"eu.anthropic.claude-sonnet-4-20250514-v1:0"
);
assert_eq!(
Model::ClaudeSonnet4_5.cross_region_inference_id("eu-west-1")?,
Model::ClaudeSonnet4_5.cross_region_inference_id("eu-west-1", false)?,
"eu.anthropic.claude-sonnet-4-5-20250929-v1:0"
);
assert_eq!(
Model::Claude3Sonnet.cross_region_inference_id("eu-west-1")?,
Model::Claude3Sonnet.cross_region_inference_id("eu-west-1", false)?,
"eu.anthropic.claude-3-sonnet-20240229-v1:0"
);
assert_eq!(
Model::AmazonNovaMicro.cross_region_inference_id("eu-north-1")?,
Model::AmazonNovaMicro.cross_region_inference_id("eu-north-1", false)?,
"eu.amazon.nova-micro-v1:0"
);
Ok(())
@@ -745,15 +806,15 @@ mod tests {
fn test_apac_region_inference_ids() -> anyhow::Result<()> {
// Test Asia-Pacific regions
assert_eq!(
Model::Claude3_5SonnetV2.cross_region_inference_id("ap-northeast-1")?,
Model::Claude3_5SonnetV2.cross_region_inference_id("ap-northeast-1", false)?,
"apac.anthropic.claude-3-5-sonnet-20241022-v2:0"
);
assert_eq!(
Model::Claude3_5SonnetV2.cross_region_inference_id("ap-southeast-2")?,
Model::Claude3_5SonnetV2.cross_region_inference_id("ap-southeast-2", false)?,
"apac.anthropic.claude-3-5-sonnet-20241022-v2:0"
);
assert_eq!(
Model::AmazonNovaLite.cross_region_inference_id("ap-south-1")?,
Model::AmazonNovaLite.cross_region_inference_id("ap-south-1", false)?,
"apac.amazon.nova-lite-v1:0"
);
Ok(())
@@ -763,11 +824,11 @@ mod tests {
fn test_gov_region_inference_ids() -> anyhow::Result<()> {
// Test Government regions
assert_eq!(
Model::Claude3_5Sonnet.cross_region_inference_id("us-gov-east-1")?,
Model::Claude3_5Sonnet.cross_region_inference_id("us-gov-east-1", false)?,
"us-gov.anthropic.claude-3-5-sonnet-20240620-v1:0"
);
assert_eq!(
Model::Claude3Haiku.cross_region_inference_id("us-gov-west-1")?,
Model::Claude3Haiku.cross_region_inference_id("us-gov-west-1", false)?,
"us-gov.anthropic.claude-3-haiku-20240307-v1:0"
);
Ok(())
@@ -777,15 +838,15 @@ mod tests {
fn test_meta_models_inference_ids() -> anyhow::Result<()> {
// Test Meta models
assert_eq!(
Model::MetaLlama370BInstructV1.cross_region_inference_id("us-east-1")?,
Model::MetaLlama370BInstructV1.cross_region_inference_id("us-east-1", false)?,
"meta.llama3-70b-instruct-v1:0"
);
assert_eq!(
Model::MetaLlama3170BInstructV1.cross_region_inference_id("us-east-1")?,
Model::MetaLlama3170BInstructV1.cross_region_inference_id("us-east-1", false)?,
"us.meta.llama3-1-70b-instruct-v1:0"
);
assert_eq!(
Model::MetaLlama321BInstructV1.cross_region_inference_id("eu-west-1")?,
Model::MetaLlama321BInstructV1.cross_region_inference_id("eu-west-1", false)?,
"eu.meta.llama3-2-1b-instruct-v1:0"
);
Ok(())
@@ -796,11 +857,11 @@ mod tests {
// Mistral models don't follow the regional prefix pattern,
// so they should return their original IDs
assert_eq!(
Model::MistralMistralLarge2402V1.cross_region_inference_id("us-east-1")?,
Model::MistralMistralLarge2402V1.cross_region_inference_id("us-east-1", false)?,
"mistral.mistral-large-2402-v1:0"
);
assert_eq!(
Model::MistralMixtral8x7BInstructV0.cross_region_inference_id("eu-west-1")?,
Model::MistralMixtral8x7BInstructV0.cross_region_inference_id("eu-west-1", false)?,
"mistral.mixtral-8x7b-instruct-v0:1"
);
Ok(())
@@ -811,11 +872,11 @@ mod tests {
// AI21 models don't follow the regional prefix pattern,
// so they should return their original IDs
assert_eq!(
Model::AI21J2UltraV1.cross_region_inference_id("us-east-1")?,
Model::AI21J2UltraV1.cross_region_inference_id("us-east-1", false)?,
"ai21.j2-ultra-v1"
);
assert_eq!(
Model::AI21JambaInstructV1.cross_region_inference_id("eu-west-1")?,
Model::AI21JambaInstructV1.cross_region_inference_id("eu-west-1", false)?,
"ai21.jamba-instruct-v1:0"
);
Ok(())
@@ -826,11 +887,11 @@ mod tests {
// Cohere models don't follow the regional prefix pattern,
// so they should return their original IDs
assert_eq!(
Model::CohereCommandRV1.cross_region_inference_id("us-east-1")?,
Model::CohereCommandRV1.cross_region_inference_id("us-east-1", false)?,
"cohere.command-r-v1:0"
);
assert_eq!(
Model::CohereCommandTextV14_4k.cross_region_inference_id("ap-southeast-1")?,
Model::CohereCommandTextV14_4k.cross_region_inference_id("ap-southeast-1", false)?,
"cohere.command-text-v14:7:4k"
);
Ok(())
@@ -850,10 +911,17 @@ mod tests {
// Custom model should return its name unchanged
assert_eq!(
custom_model.cross_region_inference_id("us-east-1")?,
custom_model.cross_region_inference_id("us-east-1", false)?,
"custom.my-model-v1:0"
);
// Test that models without global support fall back to regional when allow_global is true
assert_eq!(
Model::AmazonNovaPro.cross_region_inference_id("us-east-1", true)?,
"us.amazon.nova-pro-v1:0",
"Nova Pro should fall back to regional profile even when allow_global is true"
);
Ok(())
}
@@ -892,3 +960,28 @@ mod tests {
);
}
}
#[test]
fn test_global_inference_ids() -> anyhow::Result<()> {
// Test global inference for models that support it when allow_global is true
assert_eq!(
Model::ClaudeSonnet4.cross_region_inference_id("us-east-1", true)?,
"global.anthropic.claude-sonnet-4-20250514-v1:0"
);
assert_eq!(
Model::ClaudeSonnet4_5.cross_region_inference_id("eu-west-1", true)?,
"global.anthropic.claude-sonnet-4-5-20250929-v1:0"
);
assert_eq!(
Model::ClaudeHaiku4_5.cross_region_inference_id("ap-south-1", true)?,
"global.anthropic.claude-haiku-4-5-20251001-v1:0"
);
// Test that regional prefix is used when allow_global is false
assert_eq!(
Model::ClaudeSonnet4.cross_region_inference_id("us-east-1", false)?,
"us.anthropic.claude-sonnet-4-20250514-v1:0"
);
Ok(())
}

View File

@@ -12,7 +12,7 @@ workspace = true
path = "src/buffer_diff.rs"
[features]
test-support = []
test-support = ["settings"]
[dependencies]
anyhow.workspace = true
@@ -24,6 +24,7 @@ language.workspace = true
log.workspace = true
pretty_assertions.workspace = true
rope.workspace = true
settings = { workspace = true, optional = true }
sum_tree.workspace = true
text.workspace = true
util.workspace = true
@@ -33,6 +34,7 @@ ctor.workspace = true
gpui = { workspace = true, features = ["test-support"] }
rand.workspace = true
serde_json.workspace = true
settings.workspace = true
text = { workspace = true, features = ["test-support"] }
unindent.workspace = true
zlog.workspace = true

View File

@@ -1,7 +1,10 @@
use futures::channel::oneshot;
use git2::{DiffLineType as GitDiffLineType, DiffOptions as GitOptions, Patch as GitPatch};
use gpui::{App, AppContext as _, AsyncApp, Context, Entity, EventEmitter, Task, TaskLabel};
use language::{BufferRow, Language, LanguageRegistry};
use language::{
BufferRow, DiffOptions, File, Language, LanguageName, LanguageRegistry,
language_settings::language_settings, word_diff_ranges,
};
use rope::Rope;
use std::{
cmp::Ordering,
@@ -15,10 +18,12 @@ use text::{Anchor, Bias, BufferId, OffsetRangeExt, Point, ToOffset as _, ToPoint
use util::ResultExt;
pub static CALCULATE_DIFF_TASK: LazyLock<TaskLabel> = LazyLock::new(TaskLabel::new);
pub const MAX_WORD_DIFF_LINE_COUNT: usize = 5;
pub struct BufferDiff {
pub buffer_id: BufferId,
inner: BufferDiffInner,
// diff of the index vs head
secondary_diff: Option<Entity<BufferDiff>>,
}
@@ -31,6 +36,7 @@ pub struct BufferDiffSnapshot {
#[derive(Clone)]
struct BufferDiffInner {
hunks: SumTree<InternalDiffHunk>,
// Used for making staging mo
pending_hunks: SumTree<PendingHunk>,
base_text: language::BufferSnapshot,
base_text_exists: bool,
@@ -50,11 +56,18 @@ pub enum DiffHunkStatusKind {
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
/// Diff of Working Copy vs Index
/// aka 'is this hunk staged or not'
pub enum DiffHunkSecondaryStatus {
/// Unstaged
HasSecondaryHunk,
/// Partially staged
OverlapsWithSecondaryHunk,
/// Staged
NoSecondaryHunk,
/// We are unstaging
SecondaryHunkAdditionPending,
/// We are stagind
SecondaryHunkRemovalPending,
}
@@ -68,6 +81,10 @@ pub struct DiffHunk {
/// The range in the buffer's diff base text to which this hunk corresponds.
pub diff_base_byte_range: Range<usize>,
pub secondary_status: DiffHunkSecondaryStatus,
// Anchors representing the word diff locations in the active buffer
pub buffer_word_diffs: Vec<Range<Anchor>>,
// Offsets relative to the start of the deleted diff that represent word diff locations
pub base_word_diffs: Vec<Range<usize>>,
}
/// We store [`InternalDiffHunk`]s internally so we don't need to store the additional row range.
@@ -75,6 +92,8 @@ pub struct DiffHunk {
struct InternalDiffHunk {
buffer_range: Range<Anchor>,
diff_base_byte_range: Range<usize>,
base_word_diffs: Vec<Range<usize>>,
buffer_word_diffs: Vec<Range<Anchor>>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
@@ -208,6 +227,13 @@ impl BufferDiffSnapshot {
let base_text_pair;
let base_text_exists;
let base_text_snapshot;
let diff_options = build_diff_options(
None,
language.as_ref().map(|l| l.name()),
language.as_ref().map(|l| l.default_scope()),
cx,
);
if let Some(text) = &base_text {
let base_text_rope = Rope::from(text.as_str());
base_text_pair = Some((text.clone(), base_text_rope.clone()));
@@ -225,7 +251,7 @@ impl BufferDiffSnapshot {
.background_executor()
.spawn_labeled(*CALCULATE_DIFF_TASK, {
let buffer = buffer.clone();
async move { compute_hunks(base_text_pair, buffer) }
async move { compute_hunks(base_text_pair, buffer, diff_options) }
});
async move {
@@ -248,6 +274,12 @@ impl BufferDiffSnapshot {
base_text_snapshot: language::BufferSnapshot,
cx: &App,
) -> impl Future<Output = Self> + use<> {
let diff_options = build_diff_options(
base_text_snapshot.file(),
base_text_snapshot.language().map(|l| l.name()),
base_text_snapshot.language().map(|l| l.default_scope()),
cx,
);
let base_text_exists = base_text.is_some();
let base_text_pair = base_text.map(|text| {
debug_assert_eq!(&*text, &base_text_snapshot.text());
@@ -259,7 +291,7 @@ impl BufferDiffSnapshot {
inner: BufferDiffInner {
base_text: base_text_snapshot,
pending_hunks: SumTree::new(&buffer),
hunks: compute_hunks(base_text_pair, buffer),
hunks: compute_hunks(base_text_pair, buffer, diff_options),
base_text_exists,
},
secondary_diff: None,
@@ -602,11 +634,15 @@ impl BufferDiffInner {
[
(
&hunk.buffer_range.start,
(hunk.buffer_range.start, hunk.diff_base_byte_range.start),
(
hunk.buffer_range.start,
hunk.diff_base_byte_range.start,
hunk,
),
),
(
&hunk.buffer_range.end,
(hunk.buffer_range.end, hunk.diff_base_byte_range.end),
(hunk.buffer_range.end, hunk.diff_base_byte_range.end, hunk),
),
]
});
@@ -625,8 +661,11 @@ impl BufferDiffInner {
let mut summaries = buffer.summaries_for_anchors_with_payload::<Point, _, _>(anchor_iter);
iter::from_fn(move || {
loop {
let (start_point, (start_anchor, start_base)) = summaries.next()?;
let (mut end_point, (mut end_anchor, end_base)) = summaries.next()?;
let (start_point, (start_anchor, start_base, hunk)) = summaries.next()?;
let (mut end_point, (mut end_anchor, end_base, _)) = summaries.next()?;
let base_word_diffs = hunk.base_word_diffs.clone();
let buffer_word_diffs = hunk.buffer_word_diffs.clone();
if !start_anchor.is_valid(buffer) {
continue;
@@ -696,6 +735,8 @@ impl BufferDiffInner {
range: start_point..end_point,
diff_base_byte_range: start_base..end_base,
buffer_range: start_anchor..end_anchor,
base_word_diffs,
buffer_word_diffs,
secondary_status,
});
}
@@ -727,6 +768,8 @@ impl BufferDiffInner {
buffer_range: hunk.buffer_range.clone(),
// The secondary status is not used by callers of this method.
secondary_status: DiffHunkSecondaryStatus::NoSecondaryHunk,
base_word_diffs: hunk.base_word_diffs.clone(),
buffer_word_diffs: hunk.buffer_word_diffs.clone(),
})
})
}
@@ -795,9 +838,36 @@ impl BufferDiffInner {
}
}
fn build_diff_options(
file: Option<&Arc<dyn File>>,
language: Option<LanguageName>,
language_scope: Option<language::LanguageScope>,
cx: &App,
) -> Option<DiffOptions> {
#[cfg(any(test, feature = "test-support"))]
{
if !cx.has_global::<settings::SettingsStore>() {
return Some(DiffOptions {
language_scope,
max_word_diff_line_count: MAX_WORD_DIFF_LINE_COUNT,
..Default::default()
});
}
}
language_settings(language, file, cx)
.word_diff_enabled
.then_some(DiffOptions {
language_scope,
max_word_diff_line_count: MAX_WORD_DIFF_LINE_COUNT,
..Default::default()
})
}
fn compute_hunks(
diff_base: Option<(Arc<String>, Rope)>,
buffer: text::BufferSnapshot,
diff_options: Option<DiffOptions>,
) -> SumTree<InternalDiffHunk> {
let mut tree = SumTree::new(&buffer);
@@ -823,6 +893,8 @@ fn compute_hunks(
InternalDiffHunk {
buffer_range: buffer.anchor_before(0)..buffer.anchor_before(0),
diff_base_byte_range: 0..diff_base.len() - 1,
base_word_diffs: Vec::default(),
buffer_word_diffs: Vec::default(),
},
&buffer,
);
@@ -838,6 +910,7 @@ fn compute_hunks(
&diff_base_rope,
&buffer,
&mut divergence,
diff_options.as_ref(),
);
tree.push(hunk, &buffer);
}
@@ -847,6 +920,8 @@ fn compute_hunks(
InternalDiffHunk {
buffer_range: Anchor::min_max_range_for_buffer(buffer.remote_id()),
diff_base_byte_range: 0..0,
base_word_diffs: Vec::default(),
buffer_word_diffs: Vec::default(),
},
&buffer,
);
@@ -861,6 +936,7 @@ fn process_patch_hunk(
diff_base: &Rope,
buffer: &text::BufferSnapshot,
buffer_row_divergence: &mut i64,
diff_options: Option<&DiffOptions>,
) -> InternalDiffHunk {
let line_item_count = patch.num_lines_in_hunk(hunk_index).unwrap();
assert!(line_item_count > 0);
@@ -925,9 +1001,49 @@ fn process_patch_hunk(
let start = Point::new(buffer_row_range.start, 0);
let end = Point::new(buffer_row_range.end, 0);
let buffer_range = buffer.anchor_before(start)..buffer.anchor_before(end);
let base_line_count = line_item_count.saturating_sub(buffer_row_range.len());
let (base_word_diffs, buffer_word_diffs) = if let Some(diff_options) = diff_options
&& !buffer_row_range.is_empty()
&& base_line_count == buffer_row_range.len()
&& diff_options.max_word_diff_line_count >= base_line_count
{
let base_text: String = diff_base
.chunks_in_range(diff_base_byte_range.clone())
.collect();
let buffer_text: String = buffer.text_for_range(buffer_range.clone()).collect();
let (base_word_diffs, buffer_word_diffs_relative) = word_diff_ranges(
&base_text,
&buffer_text,
DiffOptions {
language_scope: diff_options.language_scope.clone(),
..*diff_options
},
);
let buffer_start_offset = buffer_range.start.to_offset(buffer);
let buffer_word_diffs = buffer_word_diffs_relative
.into_iter()
.map(|range| {
let start = buffer.anchor_after(buffer_start_offset + range.start);
let end = buffer.anchor_after(buffer_start_offset + range.end);
start..end
})
.collect();
(base_word_diffs, buffer_word_diffs)
} else {
(Vec::default(), Vec::default())
};
InternalDiffHunk {
buffer_range,
diff_base_byte_range,
base_word_diffs,
buffer_word_diffs,
}
}

View File

@@ -524,6 +524,16 @@ impl Room {
self.id
}
pub fn room_id(&self) -> impl Future<Output = Option<String>> + 'static {
let room = self.live_kit.as_ref().map(|lk| lk.room.clone());
async move {
let room = room?;
let sid = room.sid().await;
let name = room.name();
Some(format!("{} (sid: {sid})", name))
}
}
pub fn status(&self) -> RoomStatus {
self.status
}

View File

@@ -1723,6 +1723,10 @@ impl ProtoClient for Client {
fn is_via_collab(&self) -> bool {
true
}
fn has_wsl_interop(&self) -> bool {
false
}
}
/// prefix for the zed:// url scheme

View File

@@ -206,11 +206,16 @@ pub struct AcceptEditPredictionBody {
pub request_id: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[derive(Debug, Clone, Deserialize)]
pub struct RejectEditPredictionsBody {
pub rejections: Vec<EditPredictionRejection>,
}
#[derive(Debug, Clone, Serialize)]
pub struct RejectEditPredictionsBodyRef<'a> {
pub rejections: &'a [EditPredictionRejection],
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct EditPredictionRejection {
pub request_id: String,

View File

@@ -121,6 +121,8 @@ CREATE TABLE "project_repositories" (
"merge_message" VARCHAR,
"branch_summary" VARCHAR,
"head_commit_details" VARCHAR,
"remote_upstream_url" VARCHAR,
"remote_origin_url" VARCHAR,
PRIMARY KEY (project_id, id)
);

View File

@@ -0,0 +1,2 @@
ALTER TABLE "project_repositories" ADD COLUMN "remote_upstream_url" VARCHAR;
ALTER TABLE "project_repositories" ADD COLUMN "remote_origin_url" VARCHAR;

View File

@@ -362,6 +362,8 @@ impl Database {
entry_ids: ActiveValue::set("[]".into()),
head_commit_details: ActiveValue::set(None),
merge_message: ActiveValue::set(None),
remote_upstream_url: ActiveValue::set(None),
remote_origin_url: ActiveValue::set(None),
}
}),
)
@@ -511,6 +513,8 @@ impl Database {
serde_json::to_string(&update.current_merge_conflicts).unwrap(),
)),
merge_message: ActiveValue::set(update.merge_message.clone()),
remote_upstream_url: ActiveValue::set(update.remote_upstream_url.clone()),
remote_origin_url: ActiveValue::set(update.remote_origin_url.clone()),
})
.on_conflict(
OnConflict::columns([
@@ -1005,6 +1009,8 @@ impl Database {
is_last_update: true,
merge_message: db_repository_entry.merge_message,
stash_entries: Vec::new(),
remote_upstream_url: db_repository_entry.remote_upstream_url.clone(),
remote_origin_url: db_repository_entry.remote_origin_url.clone(),
});
}
}

View File

@@ -796,6 +796,8 @@ impl Database {
is_last_update: true,
merge_message: db_repository.merge_message,
stash_entries: Vec::new(),
remote_upstream_url: db_repository.remote_upstream_url.clone(),
remote_origin_url: db_repository.remote_origin_url.clone(),
});
}
}

View File

@@ -22,6 +22,8 @@ pub struct Model {
pub branch_summary: Option<String>,
// A JSON object representing the current Head commit values
pub head_commit_details: Option<String>,
pub remote_upstream_url: Option<String>,
pub remote_origin_url: Option<String>,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]

View File

@@ -469,6 +469,8 @@ impl Server {
.add_request_handler(forward_mutating_project_request::<proto::GetBlobContent>)
.add_request_handler(forward_mutating_project_request::<proto::GitCreateBranch>)
.add_request_handler(forward_mutating_project_request::<proto::GitChangeBranch>)
.add_request_handler(forward_mutating_project_request::<proto::GitCreateRemote>)
.add_request_handler(forward_mutating_project_request::<proto::GitRemoveRemote>)
.add_request_handler(forward_mutating_project_request::<proto::CheckForPushedCommits>)
.add_message_handler(broadcast_project_message_from_host::<proto::AdvertiseContexts>)
.add_message_handler(update_context)

View File

@@ -25,6 +25,7 @@ use gpui::{
use indoc::indoc;
use language::FakeLspAdapter;
use lsp::LSP_REQUEST_TIMEOUT;
use pretty_assertions::assert_eq;
use project::{
ProgressToken, ProjectPath, SERVER_PROGRESS_THROTTLE_TIMEOUT,
lsp_store::lsp_ext_command::{ExpandedMacro, LspExtExpandMacro},
@@ -3192,13 +3193,12 @@ async fn test_lsp_pull_diagnostics(
.collect::<Vec<_>>();
let expected_messages = [
expected_pull_diagnostic_lib_message,
// TODO bug: the pushed diagnostics are not being sent to the client when they open the corresponding buffer.
// expected_push_diagnostic_lib_message,
expected_push_diagnostic_lib_message,
];
assert_eq!(
all_diagnostics.len(),
1,
"Expected pull diagnostics, but got: {all_diagnostics:?}"
2,
"Expected pull and push diagnostics, but got: {all_diagnostics:?}"
);
for diagnostic in all_diagnostics {
assert!(
@@ -3258,14 +3258,15 @@ async fn test_lsp_pull_diagnostics(
.diagnostics_in_range(MultiBufferOffset(0)..snapshot.len())
.collect::<Vec<_>>();
let expected_messages = [
expected_workspace_pull_diagnostics_lib_message,
// TODO bug: the pushed diagnostics are not being sent to the client when they open the corresponding buffer.
// expected_push_diagnostic_lib_message,
// Despite workspace diagnostics provided,
// the currently open file's diagnostics should be preferred, as LSP suggests.
expected_pull_diagnostic_lib_message,
expected_push_diagnostic_lib_message,
];
assert_eq!(
all_diagnostics.len(),
1,
"Expected pull diagnostics, but got: {all_diagnostics:?}"
2,
"Expected pull and push diagnostics, but got: {all_diagnostics:?}"
);
for diagnostic in all_diagnostics {
assert!(
@@ -3378,8 +3379,9 @@ async fn test_lsp_pull_diagnostics(
"Another workspace diagnostics pull should happen after the diagnostics refresh server request"
);
{
assert!(
diagnostics_pulls_result_ids.lock().await.len() == diagnostic_pulls_result_ids,
assert_eq!(
diagnostics_pulls_result_ids.lock().await.len(),
diagnostic_pulls_result_ids,
"Pulls should not happen hence no extra ids should appear"
);
assert!(
@@ -3397,7 +3399,7 @@ async fn test_lsp_pull_diagnostics(
expected_pull_diagnostic_lib_message,
expected_push_diagnostic_lib_message,
];
assert_eq!(all_diagnostics.len(), 1);
assert_eq!(all_diagnostics.len(), 2);
for diagnostic in &all_diagnostics {
assert!(
expected_messages.contains(&diagnostic.diagnostic.message.as_str()),
@@ -3516,7 +3518,6 @@ async fn test_git_blame_is_forwarded(cx_a: &mut TestAppContext, cx_b: &mut TestA
.into_iter()
.map(|(sha, message)| (sha.parse().unwrap(), message.into()))
.collect(),
remote_url: Some("git@github.com:zed-industries/zed.git".to_string()),
};
client_a.fs().set_blame_for_repo(
Path::new(path!("/my-repo/.git")),
@@ -3601,10 +3602,6 @@ async fn test_git_blame_is_forwarded(cx_a: &mut TestAppContext, cx_b: &mut TestA
for (idx, (buffer, entry)) in entries.iter().flatten().enumerate() {
let details = blame.details_for_entry(*buffer, entry).unwrap();
assert_eq!(details.message, format!("message for idx-{}", idx));
assert_eq!(
details.permalink.unwrap().to_string(),
format!("https://github.com/zed-industries/zed/commit/{}", entry.sha)
);
}
});
});

View File

@@ -37,7 +37,7 @@ use ui::{
};
use util::{ResultExt, TryFutureExt, maybe};
use workspace::{
Deafen, LeaveCall, Mute, OpenChannelNotes, ScreenShare, ShareProject, Workspace,
CopyRoomId, Deafen, LeaveCall, Mute, OpenChannelNotes, ScreenShare, ShareProject, Workspace,
dock::{DockPosition, Panel, PanelEvent},
notifications::{DetachAndPromptErr, NotifyResultExt},
};
@@ -128,6 +128,32 @@ pub fn init(cx: &mut App) {
workspace.register_action(|_, _: &LeaveCall, window, cx| {
CollabPanel::leave_call(window, cx);
});
workspace.register_action(|workspace, _: &CopyRoomId, window, cx| {
use workspace::notifications::{NotificationId, NotifyTaskExt as _};
struct RoomIdCopiedToast;
if let Some(room) = ActiveCall::global(cx).read(cx).room() {
let romo_id_fut = room.read(cx).room_id();
cx.spawn(async move |workspace, cx| {
let room_id = romo_id_fut.await.context("Failed to get livekit room")?;
workspace.update(cx, |workspace, cx| {
cx.write_to_clipboard(ClipboardItem::new_string(room_id));
workspace.show_toast(
workspace::Toast::new(
NotificationId::unique::<RoomIdCopiedToast>(),
"Room ID copied to clipboard",
)
.autohide(),
cx,
);
})
})
.detach_and_notify_err(window, cx);
} else {
workspace.show_error(&"Theres no active call; join one first.", cx);
}
});
workspace.register_action(|workspace, _: &ShareProject, window, cx| {
let project = workspace.project().clone();
println!("{project:?}");

View File

@@ -23,6 +23,9 @@ zstd.workspace = true
[target.'cfg(target_os = "macos")'.dependencies]
mach2.workspace = true
[target.'cfg(target_os = "windows")'.dependencies]
windows.workspace = true
[lints]
workspace = true

View File

@@ -3,6 +3,8 @@ use log::info;
use minidumper::{Client, LoopAction, MinidumpBinary};
use release_channel::{RELEASE_CHANNEL, ReleaseChannel};
use serde::{Deserialize, Serialize};
#[cfg(not(target_os = "windows"))]
use smol::process::Command;
#[cfg(target_os = "macos")]
@@ -70,11 +72,16 @@ pub async fn init(crash_init: InitCrashHandler) {
// used by the crash handler isn't destroyed correctly which causes it to stay on the file
// system and block further attempts to initialize crash handlers with that socket path.
let socket_name = paths::temp_dir().join(format!("zed-crash-handler-{zed_pid}"));
#[cfg(not(target_os = "windows"))]
let _crash_handler = Command::new(exe)
.arg("--crash-handler")
.arg(&socket_name)
.spawn()
.expect("unable to spawn server process");
#[cfg(target_os = "windows")]
spawn_crash_handler_windows(&exe, &socket_name);
#[cfg(target_os = "linux")]
let server_pid = _crash_handler.id();
info!("spawning crash handler process");
@@ -342,6 +349,57 @@ pub fn panic_hook(info: &PanicHookInfo) {
}
}
#[cfg(target_os = "windows")]
fn spawn_crash_handler_windows(exe: &Path, socket_name: &Path) {
use std::ffi::OsStr;
use std::iter::once;
use std::os::windows::ffi::OsStrExt;
use windows::Win32::System::Threading::{
CreateProcessW, PROCESS_CREATION_FLAGS, PROCESS_INFORMATION, STARTF_FORCEOFFFEEDBACK,
STARTUPINFOW,
};
use windows::core::PWSTR;
let mut command_line: Vec<u16> = OsStr::new(&format!(
"\"{}\" --crash-handler \"{}\"",
exe.display(),
socket_name.display()
))
.encode_wide()
.chain(once(0))
.collect();
let mut startup_info = STARTUPINFOW::default();
startup_info.cb = std::mem::size_of::<STARTUPINFOW>() as u32;
// By default, Windows enables a "busy" cursor when a GUI application is launched.
// This cursor is disabled once the application starts processing window messages.
// Since the crash handler process doesn't process messages, this "busy" cursor stays enabled for a long time.
// Disable the cursor feedback to prevent this from happening.
startup_info.dwFlags = STARTF_FORCEOFFFEEDBACK;
let mut process_info = PROCESS_INFORMATION::default();
unsafe {
CreateProcessW(
None,
Some(PWSTR(command_line.as_mut_ptr())),
None,
None,
false,
PROCESS_CREATION_FLAGS(0),
None,
None,
&startup_info,
&mut process_info,
)
.expect("unable to spawn server process");
windows::Win32::Foundation::CloseHandle(process_info.hProcess).ok();
windows::Win32::Foundation::CloseHandle(process_info.hThread).ok();
}
}
pub fn crash_server(socket: &Path) {
let Ok(mut server) = minidumper::Server::with_name(socket) else {
log::info!("Couldn't create socket, there may already be a running crash server");

View File

@@ -1017,11 +1017,13 @@ impl SearchableItem for DapLogView {
fn update_matches(
&mut self,
matches: &[Self::Match],
active_match_index: Option<usize>,
window: &mut Window,
cx: &mut Context<Self>,
) {
self.editor
.update(cx, |e, cx| e.update_matches(matches, window, cx))
self.editor.update(cx, |e, cx| {
e.update_matches(matches, active_match_index, window, cx)
})
}
fn query_suggestion(&mut self, window: &mut Window, cx: &mut Context<Self>) -> String {

View File

@@ -740,7 +740,7 @@ impl DebugPanel {
}
})
.child(
IconButton::new("debug-step-over", IconName::ArrowRight)
IconButton::new("step-over", IconName::DebugStepOver)
.icon_size(IconSize::Small)
.on_click(window.listener_for(
running_state,
@@ -762,32 +762,29 @@ impl DebugPanel {
}),
)
.child(
IconButton::new(
"debug-step-into",
IconName::ArrowDownRight,
)
.icon_size(IconSize::Small)
.on_click(window.listener_for(
running_state,
|this, _, _window, cx| {
this.step_in(cx);
},
))
.disabled(thread_status != ThreadStatus::Stopped)
.tooltip({
let focus_handle = focus_handle.clone();
move |_window, cx| {
Tooltip::for_action_in(
"Step In",
&StepInto,
&focus_handle,
cx,
)
}
}),
IconButton::new("step-into", IconName::DebugStepInto)
.icon_size(IconSize::Small)
.on_click(window.listener_for(
running_state,
|this, _, _window, cx| {
this.step_in(cx);
},
))
.disabled(thread_status != ThreadStatus::Stopped)
.tooltip({
let focus_handle = focus_handle.clone();
move |_window, cx| {
Tooltip::for_action_in(
"Step In",
&StepInto,
&focus_handle,
cx,
)
}
}),
)
.child(
IconButton::new("debug-step-out", IconName::ArrowUpRight)
IconButton::new("step-out", IconName::DebugStepOut)
.icon_size(IconSize::Small)
.on_click(window.listener_for(
running_state,

View File

@@ -18,14 +18,14 @@ use gpui::{
use language::{Anchor, Buffer, CharScopeContext, CodeLabel, TextBufferSnapshot, ToOffset};
use menu::{Confirm, SelectNext, SelectPrevious};
use project::{
Completion, CompletionDisplayOptions, CompletionResponse,
CompletionDisplayOptions, CompletionResponse,
debugger::session::{CompletionsQuery, OutputToken, Session},
lsp_store::CompletionDocumentation,
search_history::{SearchHistory, SearchHistoryCursor},
};
use settings::Settings;
use std::fmt::Write;
use std::{cell::RefCell, ops::Range, rc::Rc, usize};
use std::{ops::Range, rc::Rc, usize};
use theme::{Theme, ThemeSettings};
use ui::{ContextMenu, Divider, PopoverMenu, SplitButton, Tooltip, prelude::*};
use util::ResultExt;
@@ -252,10 +252,11 @@ impl Console {
let start_offset = range.start;
let range = buffer.anchor_after(MultiBufferOffset(range.start))
..buffer.anchor_before(MultiBufferOffset(range.end));
let color_fn = color_fetcher(color);
console.highlight_background_key::<ConsoleAnsiHighlight>(
start_offset,
&[range],
color_fetcher(color),
move |_, theme| color_fn(theme),
cx,
);
}
@@ -553,24 +554,12 @@ impl CompletionProvider for ConsoleQueryBarCompletionProvider {
}
}
fn apply_additional_edits_for_completion(
&self,
_buffer: Entity<Buffer>,
_completions: Rc<RefCell<Box<[Completion]>>>,
_completion_index: usize,
_push_to_history: bool,
_cx: &mut Context<Editor>,
) -> gpui::Task<anyhow::Result<Option<language::Transaction>>> {
Task::ready(Ok(None))
}
fn is_completion_trigger(
&self,
buffer: &Entity<Buffer>,
position: language::Anchor,
text: &str,
trigger_in_words: bool,
menu_is_open: bool,
cx: &mut Context<Editor>,
) -> bool {
let mut chars = text.chars();
@@ -581,9 +570,6 @@ impl CompletionProvider for ConsoleQueryBarCompletionProvider {
};
let snapshot = buffer.read(cx).snapshot();
if !menu_is_open && !snapshot.settings_at(position, cx).show_completions_on_input {
return false;
}
let classifier = snapshot
.char_classifier_at(position)

View File

@@ -32,6 +32,8 @@ settings.workspace = true
supermaven.workspace = true
telemetry.workspace = true
ui.workspace = true
ui_input.workspace = true
menu.workspace = true
util.workspace = true
workspace.workspace = true
zed_actions.workspace = true

View File

@@ -1,3 +1,7 @@
mod sweep_api_token_modal;
pub use sweep_api_token_modal::SweepApiKeyModal;
use anyhow::Result;
use client::{Client, UserStore, zed_urls};
use cloud_llm_client::UsageLimit;
@@ -40,8 +44,7 @@ use workspace::{
notifications::NotificationId,
};
use zed_actions::OpenBrowser;
use zeta::RateCompletions;
use zeta::{SweepFeatureFlag, Zeta2FeatureFlag};
use zeta::{RateCompletions, SweepFeatureFlag, Zeta2FeatureFlag};
actions!(
edit_prediction,
@@ -313,6 +316,10 @@ impl Render for EditPredictionButton {
)
);
let sweep_missing_token = is_sweep
&& !zeta::Zeta::try_global(cx)
.map_or(false, |zeta| zeta.read(cx).has_sweep_api_token());
let zeta_icon = match (is_sweep, enabled) {
(true, _) => IconName::SweepAi,
(false, true) => IconName::ZedPredict,
@@ -360,19 +367,24 @@ impl Render for EditPredictionButton {
let show_editor_predictions = self.editor_show_predictions;
let user = self.user_store.read(cx).current_user();
let indicator_color = if sweep_missing_token {
Some(Color::Error)
} else if enabled && (!show_editor_predictions || over_limit) {
Some(if over_limit {
Color::Error
} else {
Color::Muted
})
} else {
None
};
let icon_button = IconButton::new("zed-predict-pending-button", zeta_icon)
.shape(IconButtonShape::Square)
.when(
enabled && (!show_editor_predictions || over_limit),
|this| {
this.indicator(Indicator::dot().when_else(
over_limit,
|dot| dot.color(Color::Error),
|dot| dot.color(Color::Muted),
))
.when_some(indicator_color, |this, color| {
this.indicator(Indicator::dot().color(color))
.indicator_border_color(Some(cx.theme().colors().status_bar_background))
},
)
})
.when(!self.popover_menu_handle.is_deployed(), |element| {
let user = user.clone();
element.tooltip(move |_window, cx| {
@@ -537,23 +549,23 @@ impl EditPredictionButton {
const ZED_AI_CALLOUT: &str =
"Zed's edit prediction is powered by Zeta, an open-source, dataset mode.";
const USE_SWEEP_API_TOKEN_CALLOUT: &str =
"Set the SWEEP_API_TOKEN environment variable to use Sweep";
let other_providers: Vec<_> = available_providers
let providers: Vec<_> = available_providers
.into_iter()
.filter(|p| *p != current_provider && *p != EditPredictionProvider::None)
.filter(|p| *p != EditPredictionProvider::None)
.collect();
if !other_providers.is_empty() {
menu = menu.separator().header("Switch Providers");
if !providers.is_empty() {
menu = menu.separator().header("Providers");
for provider in other_providers {
for provider in providers {
let is_current = provider == current_provider;
let fs = self.fs.clone();
menu = match provider {
EditPredictionProvider::Zed => menu.item(
ContextMenuEntry::new("Zed AI")
.toggleable(IconPosition::Start, is_current)
.documentation_aside(
DocumentationSide::Left,
DocumentationEdge::Bottom,
@@ -563,46 +575,77 @@ impl EditPredictionButton {
set_completion_provider(fs.clone(), cx, provider);
}),
),
EditPredictionProvider::Copilot => {
menu.entry("GitHub Copilot", None, move |_, cx| {
set_completion_provider(fs.clone(), cx, provider);
})
}
EditPredictionProvider::Supermaven => {
menu.entry("Supermaven", None, move |_, cx| {
set_completion_provider(fs.clone(), cx, provider);
})
}
EditPredictionProvider::Codestral => {
menu.entry("Codestral", None, move |_, cx| {
set_completion_provider(fs.clone(), cx, provider);
})
}
EditPredictionProvider::Copilot => menu.item(
ContextMenuEntry::new("GitHub Copilot")
.toggleable(IconPosition::Start, is_current)
.handler(move |_, cx| {
set_completion_provider(fs.clone(), cx, provider);
}),
),
EditPredictionProvider::Supermaven => menu.item(
ContextMenuEntry::new("Supermaven")
.toggleable(IconPosition::Start, is_current)
.handler(move |_, cx| {
set_completion_provider(fs.clone(), cx, provider);
}),
),
EditPredictionProvider::Codestral => menu.item(
ContextMenuEntry::new("Codestral")
.toggleable(IconPosition::Start, is_current)
.handler(move |_, cx| {
set_completion_provider(fs.clone(), cx, provider);
}),
),
EditPredictionProvider::Experimental(
EXPERIMENTAL_SWEEP_EDIT_PREDICTION_PROVIDER_NAME,
) => {
let has_api_token = zeta::Zeta::try_global(cx)
.map_or(false, |zeta| zeta.read(cx).has_sweep_api_token());
let entry = ContextMenuEntry::new("Sweep")
.when(!has_api_token, |this| {
this.disabled(true).documentation_aside(
let should_open_modal = !has_api_token || is_current;
let entry = if has_api_token {
ContextMenuEntry::new("Sweep")
.toggleable(IconPosition::Start, is_current)
} else {
ContextMenuEntry::new("Sweep")
.icon(IconName::XCircle)
.icon_color(Color::Error)
.documentation_aside(
DocumentationSide::Left,
DocumentationEdge::Bottom,
|_| Label::new(USE_SWEEP_API_TOKEN_CALLOUT).into_any_element(),
|_| {
Label::new("Click to configure your Sweep API token")
.into_any_element()
},
)
})
.handler(move |_, cx| {
};
let entry = entry.handler(move |window, cx| {
if should_open_modal {
if let Some(workspace) = window.root::<Workspace>().flatten() {
workspace.update(cx, |workspace, cx| {
workspace.toggle_modal(window, cx, |window, cx| {
SweepApiKeyModal::new(window, cx)
});
});
};
} else {
set_completion_provider(fs.clone(), cx, provider);
});
}
});
menu.item(entry)
}
EditPredictionProvider::Experimental(
EXPERIMENTAL_ZETA2_EDIT_PREDICTION_PROVIDER_NAME,
) => menu.entry("Zeta2", None, move |_, cx| {
set_completion_provider(fs.clone(), cx, provider);
}),
) => menu.item(
ContextMenuEntry::new("Zeta2")
.toggleable(IconPosition::Start, is_current)
.handler(move |_, cx| {
set_completion_provider(fs.clone(), cx, provider);
}),
),
EditPredictionProvider::None | EditPredictionProvider::Experimental(_) => {
continue;
}
@@ -1078,8 +1121,8 @@ impl EditPredictionButton {
menu = menu
.custom_row(move |_window, cx| {
let description = indoc! {
"Sign in for 2,000 worth of accepted suggestions at every keystroke, \
powered by Zeta, our open-source, open-data model."
"You get 2,000 accepted suggestions at every keystroke for free, \
powered by Zeta, our open-source, open-data model"
};
v_flex()
@@ -1332,21 +1375,28 @@ fn render_zeta_tab_animation(cx: &App) -> impl IntoElement {
.child("tab")
.with_animation(
ElementId::Integer(n),
Animation::new(Duration::from_secs(4)).repeat(),
Animation::new(Duration::from_secs(3)).repeat(),
move |tab, delta| {
let n_f32 = n as f32;
let delta = if inverted {
(delta - 0.15 * (5.0 - n_f32)) / 0.7
let offset = if inverted {
0.2 * (4.0 - n_f32)
} else {
(delta - 0.15 * n_f32) / 0.7
0.2 * n_f32
};
let delta = 1.0 - (0.5 - delta).abs() * 2.;
let delta = ease_in_out(delta.clamp(0., 1.));
let delta = 0.1 + 0.5 * delta;
let phase = (delta - offset + 1.0) % 1.0;
let pulse = if phase < 0.6 {
let t = phase / 0.6;
1.0 - (0.5 - t).abs() * 2.0
} else {
0.0
};
tab.text_color(text_color.opacity(delta))
let eased = ease_in_out(pulse);
let opacity = 0.1 + 0.5 * eased;
tab.text_color(text_color.opacity(opacity))
},
),
)

View File

@@ -0,0 +1,84 @@
use gpui::{
DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, IntoElement, ParentElement, Render,
};
use ui::{Button, ButtonStyle, Clickable, Headline, HeadlineSize, prelude::*};
use ui_input::InputField;
use workspace::ModalView;
use zeta::Zeta;
pub struct SweepApiKeyModal {
api_key_input: Entity<InputField>,
focus_handle: FocusHandle,
}
impl SweepApiKeyModal {
pub fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
let api_key_input = cx.new(|cx| InputField::new(window, cx, "Enter your Sweep API token"));
Self {
api_key_input,
focus_handle: cx.focus_handle(),
}
}
fn cancel(&mut self, _: &menu::Cancel, _window: &mut Window, cx: &mut Context<Self>) {
cx.emit(DismissEvent);
}
fn confirm(&mut self, _: &menu::Confirm, _window: &mut Window, cx: &mut Context<Self>) {
let api_key = self.api_key_input.read(cx).text(cx);
let api_key = (!api_key.trim().is_empty()).then_some(api_key);
if let Some(zeta) = Zeta::try_global(cx) {
zeta.update(cx, |zeta, cx| {
zeta.sweep_ai
.set_api_token(api_key, cx)
.detach_and_log_err(cx);
});
}
cx.emit(DismissEvent);
}
}
impl EventEmitter<DismissEvent> for SweepApiKeyModal {}
impl ModalView for SweepApiKeyModal {}
impl Focusable for SweepApiKeyModal {
fn focus_handle(&self, _cx: &App) -> FocusHandle {
self.focus_handle.clone()
}
}
impl Render for SweepApiKeyModal {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
v_flex()
.key_context("SweepApiKeyModal")
.on_action(cx.listener(Self::cancel))
.on_action(cx.listener(Self::confirm))
.elevation_2(cx)
.w(px(400.))
.p_4()
.gap_3()
.child(Headline::new("Sweep API Token").size(HeadlineSize::Small))
.child(self.api_key_input.clone())
.child(
h_flex()
.justify_end()
.gap_2()
.child(Button::new("cancel", "Cancel").on_click(cx.listener(
|_, _, _window, cx| {
cx.emit(DismissEvent);
},
)))
.child(
Button::new("save", "Save")
.style(ButtonStyle::Filled)
.on_click(cx.listener(|this, _, window, cx| {
this.confirm(&menu::Confirm, window, cx);
})),
),
)
}
}

View File

@@ -118,6 +118,7 @@ tree-sitter-rust.workspace = true
tree-sitter-typescript.workspace = true
tree-sitter-yaml.workspace = true
tree-sitter-bash.workspace = true
tree-sitter-md.workspace = true
unindent.workspace = true
util = { workspace = true, features = ["test-support"] }
workspace = { workspace = true, features = ["test-support"] }

View File

@@ -43,7 +43,7 @@ impl Editor {
.collect_array()
};
let bracket_matches_by_accent = self.visible_excerpts(cx).into_iter().fold(
let bracket_matches_by_accent = self.visible_excerpts(false, cx).into_iter().fold(
HashMap::default(),
|mut acc, (excerpt_id, (buffer, buffer_version, buffer_range))| {
let buffer_snapshot = buffer.read(cx).snapshot();
@@ -164,7 +164,7 @@ mod tests {
use super::*;
use crate::{
DisplayPoint, EditorSnapshot, MoveToBeginning, MoveToEnd, MoveUp,
DisplayPoint, EditorMode, EditorSnapshot, MoveToBeginning, MoveToEnd, MoveUp,
display_map::{DisplayRow, ToDisplayPoint},
editor_tests::init_test,
test::{
@@ -276,6 +276,40 @@ where
);
}
#[gpui::test]
async fn test_file_less_file_colorization(cx: &mut gpui::TestAppContext) {
init_test(cx, |language_settings| {
language_settings.defaults.colorize_brackets = Some(true);
});
let editor = cx.add_window(|window, cx| {
let multi_buffer = MultiBuffer::build_simple("fn main() {}", cx);
multi_buffer.update(cx, |multi_buffer, cx| {
multi_buffer
.as_singleton()
.unwrap()
.update(cx, |buffer, cx| {
buffer.set_language(Some(rust_lang()), cx);
});
});
Editor::new(EditorMode::full(), multi_buffer, None, window, cx)
});
cx.executor().advance_clock(Duration::from_millis(100));
cx.executor().run_until_parked();
assert_eq!(
"fn main«1()1» «1{}1»
1 hsla(207.80, 16.20%, 69.19%, 1.00)
",
editor
.update(cx, |editor, window, cx| {
editor_bracket_colors_markup(&editor.snapshot(window, cx))
})
.unwrap(),
"File-less buffer should still have its brackets colorized"
);
}
#[gpui::test]
async fn test_markdown_bracket_colorization(cx: &mut gpui::TestAppContext) {
init_test(cx, |language_settings| {
@@ -299,6 +333,19 @@ where
&bracket_colors_markup(&mut cx),
"All markdown brackets should be colored based on their depth"
);
cx.set_state(indoc! {r#"ˇ{{}}"#});
cx.executor().advance_clock(Duration::from_millis(100));
cx.executor().run_until_parked();
assert_eq!(
r#"«1{«2{}2»}1»
1 hsla(207.80, 16.20%, 69.19%, 1.00)
2 hsla(29.00, 54.00%, 65.88%, 1.00)
"#,
&bracket_colors_markup(&mut cx),
"All markdown brackets should be colored based on their depth, again"
);
}
#[gpui::test]

View File

@@ -181,6 +181,8 @@ impl DisplayMap {
.update(cx, |map, cx| map.sync(tab_snapshot, edits, cx));
let block_snapshot = self.block_map.read(wrap_snapshot, edits).snapshot;
// todo word diff here?
DisplaySnapshot {
block_snapshot,
diagnostics_max_severity: self.diagnostics_max_severity,

View File

@@ -146,8 +146,8 @@ use persistence::DB;
use project::{
BreakpointWithPosition, CodeAction, Completion, CompletionDisplayOptions, CompletionIntent,
CompletionResponse, CompletionSource, DisableAiSettings, DocumentHighlight, InlayHint, InlayId,
InvalidationStrategy, Location, LocationLink, PrepareRenameResponse, Project, ProjectItem,
ProjectPath, ProjectTransaction, TaskSourceKind,
InvalidationStrategy, Location, LocationLink, LspAction, PrepareRenameResponse, Project,
ProjectItem, ProjectPath, ProjectTransaction, TaskSourceKind,
debugger::{
breakpoint_store::{
Breakpoint, BreakpointEditAction, BreakpointSessionState, BreakpointState,
@@ -182,7 +182,7 @@ use std::{
iter::{self, Peekable},
mem,
num::NonZeroU32,
ops::{Deref, DerefMut, Not, Range, RangeInclusive},
ops::{ControlFlow, Deref, DerefMut, Not, Range, RangeInclusive},
path::{Path, PathBuf},
rc::Rc,
sync::Arc,
@@ -191,7 +191,7 @@ use std::{
use task::{ResolvedTask, RunnableTag, TaskTemplate, TaskVariables};
use text::{BufferId, FromAnchor, OffsetUtf16, Rope, ToOffset as _};
use theme::{
ActiveTheme, PlayerColor, StatusColors, SyntaxTheme, Theme, ThemeSettings,
AccentColors, ActiveTheme, PlayerColor, StatusColors, SyntaxTheme, Theme, ThemeSettings,
observe_buffer_font_size_adjustment,
};
use ui::{
@@ -284,6 +284,9 @@ pub enum ConflictsTheirs {}
pub enum ConflictsOursMarker {}
pub enum ConflictsTheirsMarker {}
pub struct HunkAddedColor;
pub struct HunkRemovedColor;
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub enum Navigated {
Yes,
@@ -307,6 +310,7 @@ enum DisplayDiffHunk {
display_row_range: Range<DisplayRow>,
multi_buffer_range: Range<Anchor>,
status: DiffHunkStatus,
word_diffs: Vec<Range<MultiBufferOffset>>,
},
}
@@ -722,7 +726,10 @@ impl EditorActionId {
// type GetFieldEditorTheme = dyn Fn(&theme::Theme) -> theme::FieldEditor;
// type OverrideTextStyle = dyn Fn(&EditorStyle) -> Option<HighlightStyle>;
type BackgroundHighlight = (fn(&Theme) -> Hsla, Arc<[Range<Anchor>]>);
type BackgroundHighlight = (
Arc<dyn Fn(&usize, &Theme) -> Hsla + Send + Sync>,
Arc<[Range<Anchor>]>,
);
type GutterHighlight = (fn(&App) -> Hsla, Vec<Range<Anchor>>);
#[derive(Default)]
@@ -1072,6 +1079,7 @@ pub struct Editor {
show_breakpoints: Option<bool>,
show_wrap_guides: Option<bool>,
show_indent_guides: Option<bool>,
buffers_with_disabled_indent_guides: HashSet<BufferId>,
highlight_order: usize,
highlighted_rows: HashMap<TypeId, Vec<RowHighlight>>,
background_highlights: HashMap<HighlightKey, BackgroundHighlight>,
@@ -1168,6 +1176,7 @@ pub struct Editor {
gutter_breakpoint_indicator: (Option<PhantomBreakpointIndicator>, Option<Task<()>>),
hovered_diff_hunk_row: Option<DisplayRow>,
pull_diagnostics_task: Task<()>,
pull_diagnostics_background_task: Task<()>,
in_project_search: bool,
previous_search_ranges: Option<Arc<[Range<Anchor>]>>,
breadcrumb_header: Option<String>,
@@ -1198,11 +1207,17 @@ pub struct Editor {
select_next_is_case_sensitive: Option<bool>,
pub lookup_key: Option<Box<dyn Any + Send + Sync>>,
applicable_language_settings: HashMap<Option<LanguageName>, LanguageSettings>,
accent_overrides: Vec<SharedString>,
accent_data: Option<AccentData>,
fetched_tree_sitter_chunks: HashMap<ExcerptId, HashSet<Range<BufferRow>>>,
use_base_text_line_numbers: bool,
}
#[derive(Debug, PartialEq)]
struct AccentData {
colors: AccentColors,
overrides: Vec<SharedString>,
}
fn debounce_value(debounce_ms: u64) -> Option<Duration> {
if debounce_ms > 0 {
Some(Duration::from_millis(debounce_ms))
@@ -2190,6 +2205,7 @@ impl Editor {
show_breakpoints: None,
show_wrap_guides: None,
show_indent_guides,
buffers_with_disabled_indent_guides: HashSet::default(),
highlight_order: 0,
highlighted_rows: HashMap::default(),
background_highlights: HashMap::default(),
@@ -2312,6 +2328,7 @@ impl Editor {
.unwrap_or_default(),
tasks_update_task: None,
pull_diagnostics_task: Task::ready(()),
pull_diagnostics_background_task: Task::ready(()),
colors: None,
refresh_colors_task: Task::ready(()),
inlay_hints: None,
@@ -2345,7 +2362,7 @@ impl Editor {
lookup_key: None,
select_next_is_case_sensitive: None,
applicable_language_settings: HashMap::default(),
accent_overrides: Vec::new(),
accent_data: None,
fetched_tree_sitter_chunks: HashMap::default(),
use_base_text_line_numbers: false,
};
@@ -2355,7 +2372,7 @@ impl Editor {
}
editor.applicable_language_settings = editor.fetch_applicable_language_settings(cx);
editor.accent_overrides = editor.fetch_accent_overrides(cx);
editor.accent_data = editor.fetch_accent_data(cx);
if let Some(breakpoints) = editor.breakpoint_store.as_ref() {
editor
@@ -2488,7 +2505,6 @@ impl Editor {
if let Some(buffer) = multi_buffer.read(cx).as_singleton() {
editor.register_buffer(buffer.read(cx).remote_id(), cx);
}
editor.update_lsp_data(None, window, cx);
editor.report_editor_event(ReportEditorEvent::EditorOpened, None, cx);
}
@@ -5308,12 +5324,10 @@ impl Editor {
pub fn visible_excerpts(
&self,
lsp_related_only: bool,
cx: &mut Context<Editor>,
) -> HashMap<ExcerptId, (Entity<Buffer>, clock::Global, Range<usize>)> {
let Some(project) = self.project() else {
return HashMap::default();
};
let project = project.read(cx);
let project = self.project().cloned();
let multi_buffer = self.buffer().read(cx);
let multi_buffer_snapshot = multi_buffer.snapshot(cx);
let multi_buffer_visible_start = self
@@ -5331,6 +5345,18 @@ impl Editor {
.into_iter()
.filter(|(_, excerpt_visible_range, _)| !excerpt_visible_range.is_empty())
.filter_map(|(buffer, excerpt_visible_range, excerpt_id)| {
if !lsp_related_only {
return Some((
excerpt_id,
(
multi_buffer.buffer(buffer.remote_id()).unwrap(),
buffer.version().clone(),
excerpt_visible_range.start.0..excerpt_visible_range.end.0,
),
));
}
let project = project.as_ref()?.read(cx);
let buffer_file = project::File::from_dyn(buffer.file())?;
let buffer_worktree = project.worktree_for_id(buffer_file.worktree_id(cx), cx)?;
let worktree_entry = buffer_worktree
@@ -5495,6 +5521,22 @@ impl Editor {
};
let buffer_snapshot = buffer.read(cx).snapshot();
let menu_is_open = matches!(
self.context_menu.borrow().as_ref(),
Some(CodeContextMenu::Completions(_))
);
let language = buffer_snapshot
.language_at(buffer_position.text_anchor)
.map(|language| language.name());
let language_settings = language_settings(language.clone(), buffer_snapshot.file(), cx);
let completion_settings = language_settings.completions.clone();
if !menu_is_open && trigger.is_some() && !language_settings.show_completions_on_input {
return;
}
let query: Option<Arc<String>> =
Self::completion_query(&multibuffer_snapshot, buffer_position)
.map(|query| query.into());
@@ -5503,14 +5545,8 @@ impl Editor {
// Hide the current completions menu when query is empty. Without this, cached
// completions from before the trigger char may be reused (#32774).
if query.is_none() {
let menu_is_open = matches!(
self.context_menu.borrow().as_ref(),
Some(CodeContextMenu::Completions(_))
);
if menu_is_open {
self.hide_context_menu(window, cx);
}
if query.is_none() && menu_is_open {
self.hide_context_menu(window, cx);
}
let mut ignore_word_threshold = false;
@@ -5599,14 +5635,6 @@ impl Editor {
(buffer_position..buffer_position, None)
};
let language = buffer_snapshot
.language_at(buffer_position)
.map(|language| language.name());
let completion_settings = language_settings(language.clone(), buffer_snapshot.file(), cx)
.completions
.clone();
let show_completion_documentation = buffer_snapshot
.settings_at(buffer_position, cx)
.show_completion_documentation;
@@ -5637,7 +5665,6 @@ impl Editor {
position.text_anchor,
trigger,
trigger_in_words,
completions_source.is_some(),
cx,
)
})
@@ -6137,9 +6164,43 @@ impl Editor {
}
let provider = self.completion_provider.as_ref()?;
let lsp_store = self.project().map(|project| project.read(cx).lsp_store());
let command = lsp_store.as_ref().and_then(|lsp_store| {
let CompletionSource::Lsp {
lsp_completion,
server_id,
..
} = &completion.source
else {
return None;
};
let lsp_command = lsp_completion.command.as_ref()?;
let available_commands = lsp_store
.read(cx)
.lsp_server_capabilities
.get(server_id)
.and_then(|server_capabilities| {
server_capabilities
.execute_command_provider
.as_ref()
.map(|options| options.commands.as_slice())
})?;
if available_commands.contains(&lsp_command.command) {
Some(CodeAction {
server_id: *server_id,
range: language::Anchor::MIN..language::Anchor::MIN,
lsp_action: LspAction::Command(lsp_command.clone()),
resolved: false,
})
} else {
None
}
});
drop(completion);
let apply_edits = provider.apply_additional_edits_for_completion(
buffer_handle,
buffer_handle.clone(),
completions_menu.completions.clone(),
candidate_id,
true,
@@ -6153,8 +6214,29 @@ impl Editor {
self.show_signature_help(&ShowSignatureHelp, window, cx);
}
Some(cx.foreground_executor().spawn(async move {
Some(cx.spawn_in(window, async move |editor, cx| {
apply_edits.await?;
if let Some((lsp_store, command)) = lsp_store.zip(command) {
let title = command.lsp_action.title().to_owned();
let project_transaction = lsp_store
.update(cx, |lsp_store, cx| {
lsp_store.apply_code_action(buffer_handle, command, false, cx)
})?
.await
.context("applying post-completion command")?;
if let Some(workspace) = editor.read_with(cx, |editor, _| editor.workspace())? {
Self::open_project_transaction(
&editor,
workspace.downgrade(),
project_transaction,
title,
cx,
)
.await?;
}
}
Ok(())
}))
}
@@ -6539,7 +6621,7 @@ impl Editor {
editor.update(cx, |editor, cx| {
editor.highlight_background::<Self>(
&ranges_to_highlight,
|theme| theme.colors().editor_highlighted_line_background,
|_, theme| theme.colors().editor_highlighted_line_background,
cx,
);
});
@@ -6740,6 +6822,9 @@ impl Editor {
return;
};
if self.blame.is_none() {
self.start_git_blame(true, window, cx);
}
let Some(blame) = self.blame.as_ref() else {
return;
};
@@ -6938,12 +7023,12 @@ impl Editor {
this.highlight_background::<DocumentHighlightRead>(
&read_ranges,
|theme| theme.colors().editor_document_highlight_read_background,
|_, theme| theme.colors().editor_document_highlight_read_background,
cx,
);
this.highlight_background::<DocumentHighlightWrite>(
&write_ranges,
|theme| theme.colors().editor_document_highlight_write_background,
|_, theme| theme.colors().editor_document_highlight_write_background,
cx,
);
cx.notify();
@@ -7051,7 +7136,7 @@ impl Editor {
if !match_ranges.is_empty() {
editor.highlight_background::<SelectedTextHighlight>(
&match_ranges,
|theme| theme.colors().editor_document_highlight_bracket_background,
|_, theme| theme.colors().editor_document_highlight_bracket_background,
cx,
)
}
@@ -7990,10 +8075,17 @@ impl Editor {
if self.edit_prediction_indent_conflict {
let cursor_point = cursor.to_point(&multibuffer);
let mut suggested_indent = None;
multibuffer.suggested_indents_callback(
cursor_point.row..cursor_point.row + 1,
|_, indent| {
suggested_indent = Some(indent);
ControlFlow::Break(())
},
cx,
);
let indents = multibuffer.suggested_indents(cursor_point.row..cursor_point.row + 1, cx);
if let Some((_, indent)) = indents.iter().next()
if let Some(indent) = suggested_indent
&& indent.len == cursor_point.column
{
self.edit_prediction_indent_conflict = false;
@@ -16938,7 +17030,9 @@ impl Editor {
})
.collect();
let workspace = self.workspace();
let Some(workspace) = self.workspace() else {
return Task::ready(Ok(Navigated::No));
};
cx.spawn_in(window, async move |editor, cx| {
let locations: Vec<Location> = future::join_all(definitions)
@@ -16964,10 +17058,6 @@ impl Editor {
}
if num_locations > 1 {
let Some(workspace) = workspace else {
return Ok(Navigated::No);
};
let tab_kind = match kind {
Some(GotoDefinitionKind::Implementation) => "Implementations",
Some(GotoDefinitionKind::Symbol) | None => "Definitions",
@@ -16999,11 +17089,14 @@ impl Editor {
let opened = workspace
.update_in(cx, |workspace, window, cx| {
let allow_preview = PreviewTabsSettings::get_global(cx)
.enable_preview_multibuffer_from_code_navigation;
Self::open_locations_in_multibuffer(
workspace,
locations,
title,
split,
allow_preview,
MultibufferSelectionMode::First,
window,
cx,
@@ -17020,10 +17113,9 @@ impl Editor {
Ok(Navigated::Yes)
}
Some(Either::Right(path)) => {
let Some(workspace) = workspace else {
return Ok(Navigated::No);
};
// TODO(andrew): respect preview tab settings
// `enable_keep_preview_on_code_navigation` and
// `enable_preview_file_from_code_navigation`
workspace
.update_in(cx, |workspace, window, cx| {
workspace.open_resolved_path(path, window, cx)
@@ -17034,10 +17126,6 @@ impl Editor {
None => Ok(Navigated::No),
}
} else {
let Some(workspace) = workspace else {
return Ok(Navigated::No);
};
let (target_buffer, target_ranges) = locations.into_iter().next().unwrap();
let target_range = target_ranges.first().unwrap().clone();
@@ -17061,11 +17149,19 @@ impl Editor {
workspace.active_pane().clone()
};
let preview_tabs_settings = PreviewTabsSettings::get_global(cx);
let keep_old_preview = preview_tabs_settings
.enable_keep_preview_on_code_navigation;
let allow_new_preview = preview_tabs_settings
.enable_preview_file_from_code_navigation;
workspace.open_project_item(
pane,
target_buffer.clone(),
true,
true,
keep_old_preview,
allow_new_preview,
window,
cx,
)
@@ -17342,11 +17438,14 @@ impl Editor {
} else {
format!("References to {target}")
};
let allow_preview = PreviewTabsSettings::get_global(cx)
.enable_preview_multibuffer_from_code_navigation;
Self::open_locations_in_multibuffer(
workspace,
locations,
title,
false,
allow_preview,
MultibufferSelectionMode::First,
window,
cx,
@@ -17362,6 +17461,7 @@ impl Editor {
locations: std::collections::HashMap<Entity<Buffer>, Vec<Range<Point>>>,
title: String,
split: bool,
allow_preview: bool,
multibuffer_selection_mode: MultibufferSelectionMode,
window: &mut Window,
cx: &mut Context<Workspace>,
@@ -17409,6 +17509,7 @@ impl Editor {
.is_some_and(|it| *it == key)
})
});
let was_existing = existing.is_some();
let editor = existing.unwrap_or_else(|| {
cx.new(|cx| {
let mut editor = Editor::for_multibuffer(
@@ -17436,7 +17537,7 @@ impl Editor {
}
editor.highlight_background::<Self>(
&ranges,
|theme| theme.colors().editor_highlighted_line_background,
|_, theme| theme.colors().editor_highlighted_line_background,
cx,
);
}
@@ -17449,29 +17550,23 @@ impl Editor {
});
let item = Box::new(editor);
let item_id = item.item_id();
if split {
let pane = workspace.adjacent_pane(window, cx);
workspace.add_item(pane, item, None, true, true, window, cx);
} else if PreviewTabsSettings::get_global(cx).enable_preview_from_code_navigation {
let (preview_item_id, preview_item_idx) =
workspace.active_pane().read_with(cx, |pane, _| {
(pane.preview_item_id(), pane.preview_item_idx())
});
workspace.add_item_to_active_pane(item, preview_item_idx, true, window, cx);
if let Some(preview_item_id) = preview_item_id {
workspace.active_pane().update(cx, |pane, cx| {
pane.remove_item(preview_item_id, false, false, window, cx);
});
}
let pane = if split {
workspace.adjacent_pane(window, cx)
} else {
workspace.add_item_to_active_pane(item, None, true, window, cx);
}
workspace.active_pane().update(cx, |pane, cx| {
pane.set_preview_item_id(Some(item_id), cx);
workspace.active_pane().clone()
};
let activate_pane = split;
let mut destination_index = None;
pane.update(cx, |pane, cx| {
if allow_preview && !was_existing {
destination_index = pane.replace_preview_item_id(item.item_id(), window, cx);
}
if was_existing && !allow_preview {
pane.unpreview_item_if_preview(item.item_id());
}
pane.add_item(item, activate_pane, true, destination_index, window, cx);
});
}
@@ -18327,54 +18422,101 @@ impl Editor {
return None;
}
let project = self.project()?.downgrade();
let debounce = Duration::from_millis(pull_diagnostics_settings.debounce_ms);
let mut buffers = self.buffer.read(cx).all_buffers();
buffers.retain(|buffer| {
let buffer_id_to_retain = buffer.read(cx).remote_id();
buffer_id.is_none_or(|buffer_id| buffer_id == buffer_id_to_retain)
&& self.registered_buffers.contains_key(&buffer_id_to_retain)
});
if buffers.is_empty() {
let mut edited_buffer_ids = HashSet::default();
let mut edited_worktree_ids = HashSet::default();
let edited_buffers = match buffer_id {
Some(buffer_id) => {
let buffer = self.buffer().read(cx).buffer(buffer_id)?;
let worktree_id = buffer.read(cx).file().map(|f| f.worktree_id(cx))?;
edited_buffer_ids.insert(buffer.read(cx).remote_id());
edited_worktree_ids.insert(worktree_id);
vec![buffer]
}
None => self
.buffer()
.read(cx)
.all_buffers()
.into_iter()
.filter(|buffer| {
let buffer = buffer.read(cx);
match buffer.file().map(|f| f.worktree_id(cx)) {
Some(worktree_id) => {
edited_buffer_ids.insert(buffer.remote_id());
edited_worktree_ids.insert(worktree_id);
true
}
None => false,
}
})
.collect::<Vec<_>>(),
};
if edited_buffers.is_empty() {
self.pull_diagnostics_task = Task::ready(());
self.pull_diagnostics_background_task = Task::ready(());
return None;
}
self.pull_diagnostics_task = cx.spawn_in(window, async move |editor, cx| {
cx.background_executor().timer(debounce).await;
let Ok(mut pull_diagnostics_tasks) = cx.update(|_, cx| {
buffers
.into_iter()
.filter_map(|buffer| {
project
.update(cx, |project, cx| {
project.lsp_store().update(cx, |lsp_store, cx| {
lsp_store.pull_diagnostics_for_buffer(buffer, cx)
})
})
.ok()
})
.collect::<FuturesUnordered<_>>()
}) else {
return;
};
while let Some(pull_task) = pull_diagnostics_tasks.next().await {
match pull_task {
Ok(()) => {
if editor
.update_in(cx, |editor, window, cx| {
editor.update_diagnostics_state(window, cx);
})
.is_err()
{
return;
}
let mut already_used_buffers = HashSet::default();
let related_open_buffers = self
.workspace
.as_ref()
.and_then(|(workspace, _)| workspace.upgrade())
.into_iter()
.flat_map(|workspace| workspace.read(cx).panes())
.flat_map(|pane| pane.read(cx).items_of_type::<Editor>())
.filter(|editor| editor != &cx.entity())
.flat_map(|editor| editor.read(cx).buffer().read(cx).all_buffers())
.filter(|buffer| {
let buffer = buffer.read(cx);
let buffer_id = buffer.remote_id();
if already_used_buffers.insert(buffer_id) {
if let Some(worktree_id) = buffer.file().map(|f| f.worktree_id(cx)) {
return !edited_buffer_ids.contains(&buffer_id)
&& !edited_worktree_ids.contains(&worktree_id);
}
Err(e) => log::error!("Failed to update project diagnostics: {e:#}"),
}
false
})
.collect::<Vec<_>>();
let debounce = Duration::from_millis(pull_diagnostics_settings.debounce_ms);
let make_spawn = |buffers: Vec<Entity<Buffer>>, delay: Duration| {
if buffers.is_empty() {
return Task::ready(());
}
});
let project_weak = project.clone();
cx.spawn_in(window, async move |_, cx| {
cx.background_executor().timer(delay).await;
let Ok(mut pull_diagnostics_tasks) = cx.update(|_, cx| {
buffers
.into_iter()
.filter_map(|buffer| {
project_weak
.update(cx, |project, cx| {
project.lsp_store().update(cx, |lsp_store, cx| {
lsp_store.pull_diagnostics_for_buffer(buffer, cx)
})
})
.ok()
})
.collect::<FuturesUnordered<_>>()
}) else {
return;
};
while let Some(pull_task) = pull_diagnostics_tasks.next().await {
if let Err(e) = pull_task {
log::error!("Failed to update project diagnostics: {e:#}");
}
}
})
};
self.pull_diagnostics_task = make_spawn(edited_buffers, debounce);
self.pull_diagnostics_background_task = make_spawn(related_open_buffers, debounce * 2);
Some(())
}
@@ -19447,6 +19589,10 @@ impl Editor {
&hunks
.map(|hunk| buffer_diff::DiffHunk {
buffer_range: hunk.buffer_range,
// We don't need to pass in word diffs here because they're only used for rendering and
// this function changes internal state
base_word_diffs: Vec::default(),
buffer_word_diffs: Vec::default(),
diff_base_byte_range: hunk.diff_base_byte_range.start.0
..hunk.diff_base_byte_range.end.0,
secondary_status: hunk.secondary_status,
@@ -19946,6 +20092,20 @@ impl Editor {
self.show_indent_guides
}
pub fn disable_indent_guides_for_buffer(
&mut self,
buffer_id: BufferId,
cx: &mut Context<Self>,
) {
self.buffers_with_disabled_indent_guides.insert(buffer_id);
cx.notify();
}
pub fn has_indent_guides_disabled_for_buffer(&self, buffer_id: BufferId) -> bool {
self.buffers_with_disabled_indent_guides
.contains(&buffer_id)
}
pub fn toggle_line_numbers(
&mut self,
_: &ToggleLineNumbers,
@@ -20658,6 +20818,7 @@ impl Editor {
locations,
format!("Selections for '{title}'"),
false,
false,
MultibufferSelectionMode::All,
window,
cx,
@@ -20860,7 +21021,7 @@ impl Editor {
pub fn set_search_within_ranges(&mut self, ranges: &[Range<Anchor>], cx: &mut Context<Self>) {
self.highlight_background::<SearchWithinRange>(
ranges,
|colors| colors.colors().editor_document_highlight_read_background,
|_, colors| colors.colors().editor_document_highlight_read_background,
cx,
)
}
@@ -20876,12 +21037,12 @@ impl Editor {
pub fn highlight_background<T: 'static>(
&mut self,
ranges: &[Range<Anchor>],
color_fetcher: fn(&Theme) -> Hsla,
color_fetcher: impl Fn(&usize, &Theme) -> Hsla + Send + Sync + 'static,
cx: &mut Context<Self>,
) {
self.background_highlights.insert(
HighlightKey::Type(TypeId::of::<T>()),
(color_fetcher, Arc::from(ranges)),
(Arc::new(color_fetcher), Arc::from(ranges)),
);
self.scrollbar_marker_state.dirty = true;
cx.notify();
@@ -20891,12 +21052,12 @@ impl Editor {
&mut self,
key: usize,
ranges: &[Range<Anchor>],
color_fetcher: fn(&Theme) -> Hsla,
color_fetcher: impl Fn(&usize, &Theme) -> Hsla + Send + Sync + 'static,
cx: &mut Context<Self>,
) {
self.background_highlights.insert(
HighlightKey::TypePlus(TypeId::of::<T>(), key),
(color_fetcher, Arc::from(ranges)),
(Arc::new(color_fetcher), Arc::from(ranges)),
);
self.scrollbar_marker_state.dirty = true;
cx.notify();
@@ -21121,7 +21282,6 @@ impl Editor {
) -> Vec<(Range<DisplayPoint>, Hsla)> {
let mut results = Vec::new();
for (color_fetcher, ranges) in self.background_highlights.values() {
let color = color_fetcher(theme);
let start_ix = match ranges.binary_search_by(|probe| {
let cmp = probe
.end
@@ -21134,7 +21294,7 @@ impl Editor {
}) {
Ok(i) | Err(i) => i,
};
for range in &ranges[start_ix..] {
for (index, range) in ranges[start_ix..].iter().enumerate() {
if range
.start
.cmp(&search_range.end, &display_snapshot.buffer_snapshot())
@@ -21143,6 +21303,7 @@ impl Editor {
break;
}
let color = color_fetcher(&(start_ix + index), theme);
let start = range.start.to_display_point(display_snapshot);
let end = range.end.to_display_point(display_snapshot);
results.push((start..end, color))
@@ -21574,16 +21735,18 @@ impl Editor {
cx.notify();
}
fn fetch_accent_overrides(&self, cx: &App) -> Vec<SharedString> {
fn fetch_accent_data(&self, cx: &App) -> Option<AccentData> {
if !self.mode.is_full() {
return Vec::new();
return None;
}
let theme_settings = theme::ThemeSettings::get_global(cx);
let theme = cx.theme();
let accent_colors = theme.accents().clone();
theme_settings
let accent_overrides = theme_settings
.theme_overrides
.get(cx.theme().name.as_ref())
.get(theme.name.as_ref())
.map(|theme_style| &theme_style.accents)
.into_iter()
.flatten()
@@ -21596,7 +21759,12 @@ impl Editor {
.flatten(),
)
.flat_map(|accent| accent.0.clone())
.collect()
.collect();
Some(AccentData {
colors: accent_colors,
overrides: accent_overrides,
})
}
fn fetch_applicable_language_settings(
@@ -21626,9 +21794,9 @@ impl Editor {
let language_settings_changed = new_language_settings != self.applicable_language_settings;
self.applicable_language_settings = new_language_settings;
let new_accent_overrides = self.fetch_accent_overrides(cx);
let accent_overrides_changed = new_accent_overrides != self.accent_overrides;
self.accent_overrides = new_accent_overrides;
let new_accents = self.fetch_accent_data(cx);
let accents_changed = new_accents != self.accent_data;
self.accent_data = new_accents;
if self.diagnostics_enabled() {
let new_severity = EditorSettings::get_global(cx)
@@ -21702,7 +21870,7 @@ impl Editor {
}
}
if language_settings_changed || accent_overrides_changed {
if language_settings_changed || accents_changed {
self.colorize_brackets(true, cx);
}
@@ -21862,43 +22030,64 @@ impl Editor {
};
for (buffer, (ranges, scroll_offset)) in new_selections_by_buffer {
let editor = buffer
.read(cx)
.file()
.is_none()
let buffer_read = buffer.read(cx);
let (has_file, is_project_file) = if let Some(file) = buffer_read.file() {
(true, project::File::from_dyn(Some(file)).is_some())
} else {
(false, false)
};
// If project file is none workspace.open_project_item will fail to open the excerpt
// in a pre existing workspace item if one exists, because Buffer entity_id will be None
// so we check if there's a tab match in that case first
let editor = (!has_file || !is_project_file)
.then(|| {
// Handle file-less buffers separately: those are not really the project items, so won't have a project path or entity id,
// so `workspace.open_project_item` will never find them, always opening a new editor.
// Instead, we try to activate the existing editor in the pane first.
let (editor, pane_item_index) =
let (editor, pane_item_index, pane_item_id) =
pane.read(cx).items().enumerate().find_map(|(i, item)| {
let editor = item.downcast::<Editor>()?;
let singleton_buffer =
editor.read(cx).buffer().read(cx).as_singleton()?;
if singleton_buffer == buffer {
Some((editor, i))
Some((editor, i, item.item_id()))
} else {
None
}
})?;
pane.update(cx, |pane, cx| {
pane.activate_item(pane_item_index, true, true, window, cx)
pane.activate_item(pane_item_index, true, true, window, cx);
if !PreviewTabsSettings::get_global(cx)
.enable_preview_from_multibuffer
{
pane.unpreview_item_if_preview(pane_item_id);
}
});
Some(editor)
})
.flatten()
.unwrap_or_else(|| {
let keep_old_preview = PreviewTabsSettings::get_global(cx)
.enable_keep_preview_on_code_navigation;
let allow_new_preview =
PreviewTabsSettings::get_global(cx).enable_preview_from_multibuffer;
workspace.open_project_item::<Self>(
pane.clone(),
buffer,
true,
true,
keep_old_preview,
allow_new_preview,
window,
cx,
)
});
editor.update(cx, |editor, cx| {
if has_file && !is_project_file {
editor.set_read_only(true);
}
let autoscroll = match scroll_offset {
Some(scroll_offset) => Autoscroll::top_relative(scroll_offset as usize),
None => Autoscroll::newest(),
@@ -21922,10 +22111,11 @@ impl Editor {
});
}
// For now, don't allow opening excerpts in buffers that aren't backed by
// regular project files.
// Allow opening excerpts for buffers that either belong to the current project
// or represent synthetic/non-local files (e.g., git blobs). File-less buffers
// are also supported so tests and other in-memory views keep working.
fn can_open_excerpts_in_file(file: Option<&Arc<dyn language::File>>) -> bool {
file.is_none_or(|file| project::File::from_dyn(Some(file)).is_some())
file.is_none_or(|file| project::File::from_dyn(Some(file)).is_some() || !file.is_local())
}
fn marked_text_ranges(&self, cx: &App) -> Option<Vec<Range<MultiBufferOffsetUtf16>>> {
@@ -22524,6 +22714,10 @@ impl Editor {
}
}
pub fn last_gutter_dimensions(&self) -> &GutterDimensions {
&self.gutter_dimensions
}
pub fn wait_for_diff_to_load(&self) -> Option<Shared<Task<()>>> {
self.load_diff_task.clone()
}
@@ -22592,7 +22786,7 @@ impl Editor {
if self.ignore_lsp_data() {
return;
}
for (_, (visible_buffer, _, _)) in self.visible_excerpts(cx) {
for (_, (visible_buffer, _, _)) in self.visible_excerpts(true, cx) {
self.register_buffer(visible_buffer.read(cx).remote_id(), cx);
}
}
@@ -23413,7 +23607,6 @@ pub trait CompletionProvider {
position: language::Anchor,
text: &str,
trigger_in_words: bool,
menu_is_open: bool,
cx: &mut Context<Editor>,
) -> bool;
@@ -23792,7 +23985,6 @@ impl CompletionProvider for Entity<Project> {
position: language::Anchor,
text: &str,
trigger_in_words: bool,
menu_is_open: bool,
cx: &mut Context<Editor>,
) -> bool {
let mut chars = text.chars();
@@ -23807,9 +23999,6 @@ impl CompletionProvider for Entity<Project> {
let buffer = buffer.read(cx);
let snapshot = buffer.snapshot();
if !menu_is_open && !snapshot.settings_at(position, cx).show_completions_on_input {
return false;
}
let classifier = snapshot
.char_classifier_at(position)
.scope_context(Some(CharScopeContext::Completion));
@@ -24116,10 +24305,12 @@ impl EditorSnapshot {
end_row.0 += 1;
}
let is_created_file = hunk.is_created_file();
DisplayDiffHunk::Unfolded {
status: hunk.status(),
diff_base_byte_range: hunk.diff_base_byte_range.start.0
..hunk.diff_base_byte_range.end.0,
word_diffs: hunk.word_diffs,
display_row_range: hunk_display_start.row()..end_row,
multi_buffer_range: Anchor::range_in_buffer(
hunk.excerpt_id,

View File

@@ -14755,6 +14755,180 @@ async fn test_completion(cx: &mut TestAppContext) {
apply_additional_edits.await.unwrap();
}
#[gpui::test]
async fn test_completion_can_run_commands(cx: &mut TestAppContext) {
init_test(cx, |_| {});
let fs = FakeFs::new(cx.executor());
fs.insert_tree(
path!("/a"),
json!({
"main.rs": "",
}),
)
.await;
let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
let language_registry = project.read_with(cx, |project, _| project.languages().clone());
language_registry.add(rust_lang());
let command_calls = Arc::new(AtomicUsize::new(0));
let registered_command = "_the/command";
let closure_command_calls = command_calls.clone();
let mut fake_servers = language_registry.register_fake_lsp(
"Rust",
FakeLspAdapter {
capabilities: lsp::ServerCapabilities {
completion_provider: Some(lsp::CompletionOptions {
trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
..lsp::CompletionOptions::default()
}),
execute_command_provider: Some(lsp::ExecuteCommandOptions {
commands: vec![registered_command.to_owned()],
..lsp::ExecuteCommandOptions::default()
}),
..lsp::ServerCapabilities::default()
},
initializer: Some(Box::new(move |fake_server| {
fake_server.set_request_handler::<lsp::request::Completion, _, _>(
move |params, _| async move {
Ok(Some(lsp::CompletionResponse::Array(vec![
lsp::CompletionItem {
label: "registered_command".to_owned(),
text_edit: gen_text_edit(&params, ""),
command: Some(lsp::Command {
title: registered_command.to_owned(),
command: "_the/command".to_owned(),
arguments: Some(vec![serde_json::Value::Bool(true)]),
}),
..lsp::CompletionItem::default()
},
lsp::CompletionItem {
label: "unregistered_command".to_owned(),
text_edit: gen_text_edit(&params, ""),
command: Some(lsp::Command {
title: "????????????".to_owned(),
command: "????????????".to_owned(),
arguments: Some(vec![serde_json::Value::Null]),
}),
..lsp::CompletionItem::default()
},
])))
},
);
fake_server.set_request_handler::<lsp::request::ExecuteCommand, _, _>({
let command_calls = closure_command_calls.clone();
move |params, _| {
assert_eq!(params.command, registered_command);
let command_calls = command_calls.clone();
async move {
command_calls.fetch_add(1, atomic::Ordering::Release);
Ok(Some(json!(null)))
}
}
});
})),
..FakeLspAdapter::default()
},
);
let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
let cx = &mut VisualTestContext::from_window(*workspace, cx);
let editor = workspace
.update(cx, |workspace, window, cx| {
workspace.open_abs_path(
PathBuf::from(path!("/a/main.rs")),
OpenOptions::default(),
window,
cx,
)
})
.unwrap()
.await
.unwrap()
.downcast::<Editor>()
.unwrap();
let _fake_server = fake_servers.next().await.unwrap();
editor.update_in(cx, |editor, window, cx| {
cx.focus_self(window);
editor.move_to_end(&MoveToEnd, window, cx);
editor.handle_input(".", window, cx);
});
cx.run_until_parked();
editor.update(cx, |editor, _| {
assert!(editor.context_menu_visible());
if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
{
let completion_labels = menu
.completions
.borrow()
.iter()
.map(|c| c.label.text.clone())
.collect::<Vec<_>>();
assert_eq!(
completion_labels,
&["registered_command", "unregistered_command",],
);
} else {
panic!("expected completion menu to be open");
}
});
editor
.update_in(cx, |editor, window, cx| {
editor
.confirm_completion(&ConfirmCompletion::default(), window, cx)
.unwrap()
})
.await
.unwrap();
cx.run_until_parked();
assert_eq!(
command_calls.load(atomic::Ordering::Acquire),
1,
"For completion with a registered command, Zed should send a command execution request",
);
editor.update_in(cx, |editor, window, cx| {
cx.focus_self(window);
editor.handle_input(".", window, cx);
});
cx.run_until_parked();
editor.update(cx, |editor, _| {
assert!(editor.context_menu_visible());
if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
{
let completion_labels = menu
.completions
.borrow()
.iter()
.map(|c| c.label.text.clone())
.collect::<Vec<_>>();
assert_eq!(
completion_labels,
&["registered_command", "unregistered_command",],
);
} else {
panic!("expected completion menu to be open");
}
});
editor
.update_in(cx, |editor, window, cx| {
editor.context_menu_next(&Default::default(), window, cx);
editor
.confirm_completion(&ConfirmCompletion::default(), window, cx)
.unwrap()
})
.await
.unwrap();
cx.run_until_parked();
assert_eq!(
command_calls.load(atomic::Ordering::Acquire),
1,
"For completion with an unregistered command, Zed should not send a command execution request",
);
}
#[gpui::test]
async fn test_completion_reuse(cx: &mut TestAppContext) {
init_test(cx, |_| {});
@@ -16804,7 +16978,7 @@ fn test_highlighted_ranges(cx: &mut TestAppContext) {
anchor_range(Point::new(6, 3)..Point::new(6, 5)),
anchor_range(Point::new(8, 4)..Point::new(8, 6)),
],
|_| Hsla::red(),
|_, _| Hsla::red(),
cx,
);
editor.highlight_background::<Type2>(
@@ -16814,7 +16988,7 @@ fn test_highlighted_ranges(cx: &mut TestAppContext) {
anchor_range(Point::new(7, 4)..Point::new(7, 7)),
anchor_range(Point::new(9, 5)..Point::new(9, 8)),
],
|_| Hsla::green(),
|_, _| Hsla::green(),
cx,
);
@@ -18921,6 +19095,109 @@ async fn test_document_format_with_prettier(cx: &mut TestAppContext) {
);
}
#[gpui::test]
async fn test_document_format_with_prettier_explicit_language(cx: &mut TestAppContext) {
init_test(cx, |settings| {
settings.defaults.formatter = Some(FormatterList::Single(Formatter::Prettier))
});
let fs = FakeFs::new(cx.executor());
fs.insert_file(path!("/file.settings"), Default::default())
.await;
let project = Project::test(fs, [path!("/file.settings").as_ref()], cx).await;
let language_registry = project.read_with(cx, |project, _| project.languages().clone());
let ts_lang = Arc::new(Language::new(
LanguageConfig {
name: "TypeScript".into(),
matcher: LanguageMatcher {
path_suffixes: vec!["ts".to_string()],
..LanguageMatcher::default()
},
prettier_parser_name: Some("typescript".to_string()),
..LanguageConfig::default()
},
Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
));
language_registry.add(ts_lang.clone());
update_test_language_settings(cx, |settings| {
settings.defaults.prettier.get_or_insert_default().allowed = Some(true);
});
let test_plugin = "test_plugin";
let _ = language_registry.register_fake_lsp(
"TypeScript",
FakeLspAdapter {
prettier_plugins: vec![test_plugin],
..Default::default()
},
);
let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX;
let buffer = project
.update(cx, |project, cx| {
project.open_local_buffer(path!("/file.settings"), cx)
})
.await
.unwrap();
project.update(cx, |project, cx| {
project.set_language_for_buffer(&buffer, ts_lang, cx)
});
let buffer_text = "one\ntwo\nthree\n";
let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
editor.update_in(cx, |editor, window, cx| {
editor.set_text(buffer_text, window, cx)
});
editor
.update_in(cx, |editor, window, cx| {
editor.perform_format(
project.clone(),
FormatTrigger::Manual,
FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
window,
cx,
)
})
.unwrap()
.await;
assert_eq!(
editor.update(cx, |editor, cx| editor.text(cx)),
buffer_text.to_string() + prettier_format_suffix + "\ntypescript",
"Test prettier formatting was not applied to the original buffer text",
);
update_test_language_settings(cx, |settings| {
settings.defaults.formatter = Some(FormatterList::default())
});
let format = editor.update_in(cx, |editor, window, cx| {
editor.perform_format(
project.clone(),
FormatTrigger::Manual,
FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
window,
cx,
)
});
format.await.unwrap();
assert_eq!(
editor.update(cx, |editor, cx| editor.text(cx)),
buffer_text.to_string()
+ prettier_format_suffix
+ "\ntypescript\n"
+ prettier_format_suffix
+ "\ntypescript",
"Autoformatting (via test prettier) was not applied to the original buffer text",
);
}
#[gpui::test]
async fn test_addition_reverts(cx: &mut TestAppContext) {
init_test(cx, |_| {});
@@ -23799,7 +24076,7 @@ async fn test_rename_with_duplicate_edits(cx: &mut TestAppContext) {
let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
editor.highlight_background::<DocumentHighlightRead>(
&[highlight_range],
|theme| theme.colors().editor_document_highlight_read_background,
|_, theme| theme.colors().editor_document_highlight_read_background,
cx,
);
});
@@ -23877,7 +24154,7 @@ async fn test_rename_without_prepare(cx: &mut TestAppContext) {
let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
editor.highlight_background::<DocumentHighlightRead>(
&[highlight_range],
|theme| theme.colors().editor_document_highlight_read_background,
|_, theme| theme.colors().editor_document_highlight_read_background,
cx,
);
});
@@ -26415,7 +26692,7 @@ async fn test_pulling_diagnostics(cx: &mut TestAppContext) {
}
});
let ensure_result_id = |expected: Option<String>, cx: &mut TestAppContext| {
let ensure_result_id = |expected: Option<SharedString>, cx: &mut TestAppContext| {
project.update(cx, |project, cx| {
let buffer_id = editor
.read(cx)
@@ -26428,7 +26705,7 @@ async fn test_pulling_diagnostics(cx: &mut TestAppContext) {
let buffer_result_id = project
.lsp_store()
.read(cx)
.result_id(server_id, buffer_id, cx);
.result_id_for_buffer_pull(server_id, buffer_id, &None, cx);
assert_eq!(expected, buffer_result_id);
});
};
@@ -26445,7 +26722,7 @@ async fn test_pulling_diagnostics(cx: &mut TestAppContext) {
.next()
.await
.expect("should have sent the first diagnostics pull request");
ensure_result_id(Some("1".to_string()), cx);
ensure_result_id(Some(SharedString::new("1")), cx);
// Editing should trigger diagnostics
editor.update_in(cx, |editor, window, cx| {
@@ -26458,7 +26735,7 @@ async fn test_pulling_diagnostics(cx: &mut TestAppContext) {
2,
"Editing should trigger diagnostic request"
);
ensure_result_id(Some("2".to_string()), cx);
ensure_result_id(Some(SharedString::new("2")), cx);
// Moving cursor should not trigger diagnostic request
editor.update_in(cx, |editor, window, cx| {
@@ -26473,7 +26750,7 @@ async fn test_pulling_diagnostics(cx: &mut TestAppContext) {
2,
"Cursor movement should not trigger diagnostic request"
);
ensure_result_id(Some("2".to_string()), cx);
ensure_result_id(Some(SharedString::new("2")), cx);
// Multiple rapid edits should be debounced
for _ in 0..5 {
editor.update_in(cx, |editor, window, cx| {
@@ -26488,7 +26765,7 @@ async fn test_pulling_diagnostics(cx: &mut TestAppContext) {
final_requests <= 4,
"Multiple rapid edits should be debounced (got {final_requests} requests)",
);
ensure_result_id(Some(final_requests.to_string()), cx);
ensure_result_id(Some(SharedString::new(final_requests.to_string())), cx);
}
#[gpui::test]
@@ -27125,7 +27402,7 @@ let result = variable * 2;",
editor.highlight_background::<DocumentHighlightRead>(
&anchor_ranges,
|theme| theme.colors().editor_document_highlight_read_background,
|_, theme| theme.colors().editor_document_highlight_read_background,
cx,
);
});
@@ -27221,6 +27498,65 @@ async fn test_paste_url_from_other_app_creates_markdown_link_over_selected_text(
));
}
#[gpui::test]
async fn test_markdown_list_indent_with_multi_cursor(cx: &mut gpui::TestAppContext) {
init_test(cx, |_| {});
let markdown_language = languages::language("markdown", tree_sitter_md::LANGUAGE.into());
let mut cx = EditorTestContext::new(cx).await;
cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
cx.set_state(&indoc! {"
- [ ] Item 1
- [ ] Item 1.a
- [ˇ] Item 2
- [ˇ] Item 2.a
- [ˇ] Item 2.b
"
});
cx.update_editor(|editor, window, cx| {
editor.handle_input("X", window, cx);
});
cx.assert_editor_state(indoc! {"
- [ ] Item 1
- [ ] Item 1.a
- [Xˇ] Item 2
- [Xˇ] Item 2.a
- [Xˇ] Item 2.b
"
});
}
#[gpui::test]
async fn test_markdown_list_indent_with_newline(cx: &mut gpui::TestAppContext) {
init_test(cx, |_| {});
let markdown_language = languages::language("markdown", tree_sitter_md::LANGUAGE.into());
let mut cx = EditorTestContext::new(cx).await;
cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
cx.set_state(indoc! {"
- [x] list item
- [x] sub list itemˇ
"
});
cx.update_editor(|editor, window, cx| {
editor.newline(&Newline, window, cx);
});
cx.assert_editor_state(indoc! {"
- [x] list item
- [x] sub list item
ˇ
"
});
}
#[gpui::test]
async fn test_paste_url_from_zed_copy_creates_markdown_link_over_selected_text(
cx: &mut gpui::TestAppContext,

View File

@@ -1227,7 +1227,13 @@ impl EditorElement {
editor.hide_blame_popover(false, cx);
}
} else {
editor.hide_blame_popover(false, cx);
let keyboard_grace = editor
.inline_blame_popover
.as_ref()
.is_some_and(|state| state.keyboard_grace);
if !keyboard_grace {
editor.hide_blame_popover(false, cx);
}
}
let breakpoint_indicator = if gutter_hovered {
@@ -2511,7 +2517,6 @@ impl EditorElement {
scroll_position: gpui::Point<ScrollOffset>,
scroll_pixel_position: gpui::Point<ScrollPixelOffset>,
line_height: Pixels,
text_hitbox: &Hitbox,
window: &mut Window,
cx: &mut App,
) -> Option<InlineBlameLayout> {
@@ -2580,16 +2585,6 @@ impl EditorElement {
let size = element.layout_as_root(AvailableSpace::min_size(), window, cx);
let bounds = Bounds::new(absolute_offset, size);
self.layout_blame_entry_popover(
entry.clone(),
blame,
line_height,
text_hitbox,
row_info.buffer_id?,
window,
cx,
);
element.prepaint_as_root(absolute_offset, AvailableSpace::min_size(), window, cx);
Some(InlineBlameLayout {
@@ -2600,16 +2595,48 @@ impl EditorElement {
})
}
fn layout_blame_entry_popover(
fn layout_blame_popover(
&self,
blame_entry: BlameEntry,
blame: Entity<GitBlame>,
line_height: Pixels,
editor_snapshot: &EditorSnapshot,
text_hitbox: &Hitbox,
buffer: BufferId,
line_height: Pixels,
window: &mut Window,
cx: &mut App,
) {
if !self.editor.read(cx).inline_blame_popover.is_some() {
return;
}
let Some(blame) = self.editor.read(cx).blame.clone() else {
return;
};
let cursor_point = self
.editor
.read(cx)
.selections
.newest::<language::Point>(&editor_snapshot.display_snapshot)
.head();
let Some((buffer, buffer_point, _)) = editor_snapshot
.buffer_snapshot()
.point_to_buffer_point(cursor_point)
else {
return;
};
let row_info = RowInfo {
buffer_id: Some(buffer.remote_id()),
buffer_row: Some(buffer_point.row),
..Default::default()
};
let Some((buffer_id, blame_entry)) = blame
.update(cx, |blame, cx| blame.blame_for_rows(&[row_info], cx).next())
.flatten()
else {
return;
};
let Some((popover_state, target_point)) = self.editor.read_with(cx, |editor, _| {
editor
.inline_blame_popover
@@ -2631,7 +2658,7 @@ impl EditorElement {
popover_state.markdown,
workspace,
&blame,
buffer,
buffer_id,
window,
cx,
)
@@ -3888,6 +3915,8 @@ impl EditorElement {
) -> impl IntoElement {
let editor = self.editor.read(cx);
let multi_buffer = editor.buffer.read(cx);
let is_read_only = self.editor.read(cx).read_only(cx);
let file_status = multi_buffer
.all_diff_hunks_expanded()
.then(|| editor.status_for_buffer_id(for_excerpt.buffer_id, cx))
@@ -3940,7 +3969,7 @@ impl EditorElement {
.gap_1p5()
.when(is_sticky, |el| el.shadow_md())
.border_1()
.map(|div| {
.map(|border| {
let border_color = if is_selected
&& is_folded
&& focus_handle.contains_focused(window, cx)
@@ -3949,7 +3978,7 @@ impl EditorElement {
} else {
colors.border
};
div.border_color(border_color)
border.border_color(border_color)
})
.bg(colors.editor_subheader_background)
.hover(|style| style.bg(colors.element_hover))
@@ -4029,13 +4058,15 @@ impl EditorElement {
})
.take(1),
)
.child(
h_flex()
.size_3()
.justify_center()
.flex_shrink_0()
.children(indicator),
)
.when(!is_read_only, |this| {
this.child(
h_flex()
.size_3()
.justify_center()
.flex_shrink_0()
.children(indicator),
)
})
.child(
h_flex()
.cursor_pointer()
@@ -5572,6 +5603,50 @@ impl EditorElement {
}
}
fn layout_word_diff_highlights(
display_hunks: &[(DisplayDiffHunk, Option<Hitbox>)],
row_infos: &[RowInfo],
start_row: DisplayRow,
snapshot: &EditorSnapshot,
highlighted_ranges: &mut Vec<(Range<DisplayPoint>, Hsla)>,
cx: &mut App,
) {
let colors = cx.theme().colors();
let word_highlights = display_hunks
.into_iter()
.filter_map(|(hunk, _)| match hunk {
DisplayDiffHunk::Unfolded {
word_diffs, status, ..
} => Some((word_diffs, status)),
_ => None,
})
.filter(|(_, status)| status.is_modified())
.flat_map(|(word_diffs, _)| word_diffs)
.filter_map(|word_diff| {
let start_point = word_diff.start.to_display_point(&snapshot.display_snapshot);
let end_point = word_diff.end.to_display_point(&snapshot.display_snapshot);
let start_row_offset = start_point.row().0.saturating_sub(start_row.0) as usize;
row_infos
.get(start_row_offset)
.and_then(|row_info| row_info.diff_status)
.and_then(|diff_status| {
let background_color = match diff_status.kind {
DiffHunkStatusKind::Added => colors.version_control_word_added,
DiffHunkStatusKind::Deleted => colors.version_control_word_deleted,
DiffHunkStatusKind::Modified => {
debug_panic!("modified diff status for row info");
return None;
}
};
Some((start_point..end_point, background_color))
})
});
highlighted_ranges.extend(word_highlights);
}
fn layout_diff_hunk_controls(
&self,
row_range: Range<DisplayRow>,
@@ -9122,7 +9197,7 @@ impl Element for EditorElement {
);
let end_row = DisplayRow(end_row);
let row_infos = snapshot
let row_infos = snapshot // note we only get the visual range
.row_infos(start_row)
.take((start_row..end_row).len())
.collect::<Vec<RowInfo>>();
@@ -9153,16 +9228,27 @@ impl Element for EditorElement {
let is_light = cx.theme().appearance().is_light();
let mut highlighted_ranges = self
.editor_with_selections(cx)
.map(|editor| {
editor.read(cx).background_highlights_in_range(
start_anchor..end_anchor,
&snapshot.display_snapshot,
cx.theme(),
)
})
.unwrap_or_default();
for (ix, row_info) in row_infos.iter().enumerate() {
let Some(diff_status) = row_info.diff_status else {
continue;
};
let background_color = match diff_status.kind {
DiffHunkStatusKind::Added => cx.theme().colors().version_control_added,
DiffHunkStatusKind::Deleted => {
cx.theme().colors().version_control_deleted
}
DiffHunkStatusKind::Added =>
cx.theme().colors().version_control_added,
DiffHunkStatusKind::Deleted =>
cx.theme().colors().version_control_deleted,
DiffHunkStatusKind::Modified => {
debug_panic!("modified diff status for row info");
continue;
@@ -9200,21 +9286,14 @@ impl Element for EditorElement {
filled_highlight
};
let base_display_point =
DisplayPoint::new(start_row + DisplayRow(ix as u32), 0);
highlighted_rows
.entry(start_row + DisplayRow(ix as u32))
.entry(base_display_point.row())
.or_insert(background);
}
let highlighted_ranges = self
.editor_with_selections(cx)
.map(|editor| {
editor.read(cx).background_highlights_in_range(
start_anchor..end_anchor,
&snapshot.display_snapshot,
cx.theme(),
)
})
.unwrap_or_default();
let highlighted_gutter_ranges =
self.editor.read(cx).gutter_highlights_in_range(
start_anchor..end_anchor,
@@ -9387,7 +9466,7 @@ impl Element for EditorElement {
let crease_trailers =
window.with_element_namespace("crease_trailers", |window| {
self.layout_crease_trailers(
row_infos.iter().copied(),
row_infos.iter().cloned(),
&snapshot,
window,
cx,
@@ -9403,6 +9482,15 @@ impl Element for EditorElement {
cx,
);
Self::layout_word_diff_highlights(
&display_hunks,
&row_infos,
start_row,
&snapshot,
&mut highlighted_ranges,
cx,
);
let merged_highlighted_ranges =
if let Some((_, colors)) = document_colors.as_ref() {
&highlighted_ranges
@@ -9756,7 +9844,6 @@ impl Element for EditorElement {
scroll_position,
scroll_pixel_position,
line_height,
&text_hitbox,
window,
cx,
) {
@@ -9954,6 +10041,8 @@ impl Element for EditorElement {
window,
cx,
);
self.layout_blame_popover(&snapshot, &hitbox, line_height, window, cx);
}
let mouse_context_menu = self.layout_mouse_context_menu(

View File

@@ -508,7 +508,19 @@ impl GitBlame {
let buffer_edits = buffer.update(cx, |buffer, _| buffer.subscribe());
let blame_buffer = project.blame_buffer(&buffer, None, cx);
Some(async move { (id, snapshot, buffer_edits, blame_buffer.await) })
let remote_url = project
.git_store()
.read(cx)
.repository_and_path_for_buffer_id(buffer.read(cx).remote_id(), cx)
.and_then(|(repo, _)| {
repo.read(cx)
.remote_upstream_url
.clone()
.or(repo.read(cx).remote_origin_url.clone())
});
Some(
async move { (id, snapshot, buffer_edits, blame_buffer.await, remote_url) },
)
})
.collect::<Vec<_>>()
});
@@ -524,13 +536,9 @@ impl GitBlame {
.await;
let mut res = vec![];
let mut errors = vec![];
for (id, snapshot, buffer_edits, blame) in blame {
for (id, snapshot, buffer_edits, blame, remote_url) in blame {
match blame {
Ok(Some(Blame {
entries,
messages,
remote_url,
})) => {
Ok(Some(Blame { entries, messages })) => {
let entries = build_blame_entry_sum_tree(
entries,
snapshot.max_point().row,

View File

@@ -518,7 +518,7 @@ fn show_hover(
// Highlight the selected symbol using a background highlight
editor.highlight_background::<HoverState>(
&hover_highlights,
|theme| theme.colors().element_hover, // todo update theme
|_, theme| theme.colors().element_hover, // todo update theme
cx,
);
}

View File

@@ -181,6 +181,10 @@ pub fn indent_guides_in_range(
.buffer_snapshot()
.indent_guides_in_range(start_anchor..end_anchor, ignore_disabled_for_language, cx)
.filter(|indent_guide| {
if editor.has_indent_guides_disabled_for_buffer(indent_guide.buffer_id) {
return false;
}
if editor.is_buffer_folded(indent_guide.buffer_id, cx) {
return false;
}

View File

@@ -291,7 +291,7 @@ impl Editor {
}),
};
let mut visible_excerpts = self.visible_excerpts(cx);
let mut visible_excerpts = self.visible_excerpts(true, cx);
let mut invalidate_hints_for_buffers = HashSet::default();
let ignore_previous_fetches = match reason {
InlayHintRefreshReason::ModifiersChanged(_)
@@ -2211,7 +2211,7 @@ pub mod tests {
cx: &mut gpui::TestAppContext,
) -> Range<Point> {
let ranges = editor
.update(cx, |editor, _window, cx| editor.visible_excerpts(cx))
.update(cx, |editor, _window, cx| editor.visible_excerpts(true, cx))
.unwrap();
assert_eq!(
ranges.len(),

View File

@@ -1487,6 +1487,7 @@ impl SearchableItem for Editor {
fn update_matches(
&mut self,
matches: &[Range<Anchor>],
active_match_index: Option<usize>,
_: &mut Window,
cx: &mut Context<Self>,
) {
@@ -1497,7 +1498,13 @@ impl SearchableItem for Editor {
let updated = existing_range != Some(matches);
self.highlight_background::<BufferSearchHighlights>(
matches,
|theme| theme.colors().search_match_background,
move |index, theme| {
if active_match_index == Some(*index) {
theme.colors().search_active_match_background
} else {
theme.colors().search_match_background
}
},
cx,
);
if updated {
@@ -1891,15 +1898,20 @@ fn path_for_buffer<'a>(
cx: &'a App,
) -> Option<Cow<'a, str>> {
let file = buffer.read(cx).as_singleton()?.read(cx).file()?;
path_for_file(file.as_ref(), height, include_filename, cx)
path_for_file(file, height, include_filename, cx)
}
fn path_for_file<'a>(
file: &'a dyn language::File,
file: &'a Arc<dyn language::File>,
mut height: usize,
include_filename: bool,
cx: &'a App,
) -> Option<Cow<'a, str>> {
if project::File::from_dyn(Some(file)).is_none() {
return None;
}
let file = file.as_ref();
// Ensure we always render at least the filename.
height += 1;
@@ -1946,11 +1958,11 @@ mod tests {
#[gpui::test]
fn test_path_for_file(cx: &mut App) {
let file = TestFile {
let file: Arc<dyn language::File> = Arc::new(TestFile {
path: RelPath::empty().into(),
root_name: String::new(),
local_root: None,
};
});
assert_eq!(path_for_file(&file, 0, false, cx), None);
}

View File

@@ -164,7 +164,7 @@ impl Editor {
}
let visible_buffers = self
.visible_excerpts(cx)
.visible_excerpts(true, cx)
.into_values()
.map(|(buffer, ..)| buffer)
.filter(|editor_buffer| {

View File

@@ -243,20 +243,25 @@ impl Render for SplittableEditor {
window: &mut ui::Window,
cx: &mut ui::Context<Self>,
) -> impl ui::IntoElement {
let Some(active) = self.panes.panes().into_iter().next() else {
return div().into_any_element();
let inner = if self.secondary.is_none() {
self.primary_editor.clone().into_any_element()
} else if let Some(active) = self.panes.panes().into_iter().next() {
self.panes
.render(
None,
&ActivePaneDecorator::new(active, &self.workspace),
window,
cx,
)
.into_any_element()
} else {
div().into_any_element()
};
div()
.id("splittable-editor")
.on_action(cx.listener(Self::split))
.on_action(cx.listener(Self::unsplit))
.size_full()
.child(self.panes.render(
None,
&ActivePaneDecorator::new(active, &self.workspace),
window,
cx,
))
.into_any_element()
.child(inner)
}
}

View File

@@ -261,7 +261,7 @@ impl ExampleContext {
.expect("Unknown tool_name content in meta");
tool_uses_by_id.insert(
tool_call.id,
tool_call.tool_call_id,
ToolUse {
name: tool_name.to_string(),
value: tool_call.raw_input.unwrap_or_default(),
@@ -277,7 +277,9 @@ impl ExampleContext {
ThreadEvent::ToolCallUpdate(tool_call_update) => {
if let acp_thread::ToolCallUpdate::UpdateFields(update) = tool_call_update {
if let Some(raw_input) = update.fields.raw_input {
if let Some(tool_use) = tool_uses_by_id.get_mut(&update.id) {
if let Some(tool_use) =
tool_uses_by_id.get_mut(&update.tool_call_id)
{
tool_use.value = raw_input;
}
}
@@ -290,7 +292,7 @@ impl ExampleContext {
update.fields.status == Some(acp::ToolCallStatus::Completed);
let tool_use = tool_uses_by_id
.remove(&update.id)
.remove(&update.tool_call_id)
.expect("Unrecognized tool call completed");
let log_message = if succeeded {
@@ -337,10 +339,7 @@ impl ExampleContext {
acp::StopReason::MaxTurnRequests => {
return Err(anyhow!("Exceeded maximum turn requests"));
}
acp::StopReason::Refusal => {
return Err(anyhow!("Refusal"));
}
acp::StopReason::Cancelled => return Err(anyhow!("Cancelled")),
stop_reason => return Err(anyhow!("{stop_reason:?}")),
},
}
}

View File

@@ -303,13 +303,12 @@ impl ExampleInstance {
let context_server_registry = cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
let thread = if let Some(json) = &meta.existing_thread_json {
let session_id = acp::SessionId(
let session_id = acp::SessionId::new(
rand::rng()
.sample_iter(&distr::Alphanumeric)
.take(7)
.map(char::from)
.collect::<String>()
.into(),
.collect::<String>(),
);
let db_thread = agent::DbThread::from_json(json.as_bytes()).expect("Can't read serialized thread");
@@ -640,7 +639,7 @@ impl agent::ThreadEnvironment for EvalThreadEnvironment {
cx.spawn(async move |cx| {
let language_registry =
project.read_with(cx, |project, _cx| project.languages().clone())?;
let id = acp::TerminalId(uuid::Uuid::new_v4().to_string().into());
let id = acp::TerminalId::new(uuid::Uuid::new_v4().to_string());
let terminal =
acp_thread::create_terminal_entity(command, &[], vec![], cwd.clone(), &project, cx)
.await?;

View File

@@ -0,0 +1,18 @@
[package]
name = "eval_utils"
version = "0.1.0"
edition.workspace = true
publish.workspace = true
license = "GPL-3.0-or-later"
[lints]
workspace = true
[lib]
path = "src/eval_utils.rs"
doctest = false
[dependencies]
gpui.workspace = true
serde.workspace = true
smol.workspace = true

View File

@@ -0,0 +1 @@
LICENSE-GPL

View File

@@ -0,0 +1,3 @@
# eval_utils
Utilities for evals of agents.

View File

@@ -0,0 +1,128 @@
//! Utilities for evaluation and benchmarking.
use std::{
collections::HashMap,
sync::{Arc, mpsc},
};
fn report_progress(evaluated_count: usize, failed_count: usize, iterations: usize) {
let passed_count = evaluated_count - failed_count;
let passed_ratio = if evaluated_count == 0 {
0.0
} else {
passed_count as f64 / evaluated_count as f64
};
println!(
"\r\x1b[KEvaluated {}/{} ({:.2}% passed)",
evaluated_count,
iterations,
passed_ratio * 100.0
)
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum OutcomeKind {
Passed,
Failed,
Error,
}
pub trait EvalOutputProcessor {
type Metadata: 'static + Send;
fn process(&mut self, output: &EvalOutput<Self::Metadata>);
fn assert(&mut self);
}
#[derive(Clone, Debug)]
pub struct EvalOutput<M> {
pub outcome: OutcomeKind,
pub data: String,
pub metadata: M,
}
pub struct NoProcessor;
impl EvalOutputProcessor for NoProcessor {
type Metadata = ();
fn process(&mut self, _output: &EvalOutput<Self::Metadata>) {}
fn assert(&mut self) {}
}
pub fn eval<P>(
iterations: usize,
expected_pass_ratio: f32,
mut processor: P,
evalf: impl Fn() -> EvalOutput<P::Metadata> + Send + Sync + 'static,
) where
P: EvalOutputProcessor,
{
let mut evaluated_count = 0;
let mut failed_count = 0;
let evalf = Arc::new(evalf);
report_progress(evaluated_count, failed_count, iterations);
let (tx, rx) = mpsc::channel();
let executor = gpui::background_executor();
let semaphore = Arc::new(smol::lock::Semaphore::new(32));
let evalf = Arc::new(evalf);
// Warm the cache once
let first_output = evalf();
tx.send(first_output).ok();
for _ in 1..iterations {
let tx = tx.clone();
let semaphore = semaphore.clone();
let evalf = evalf.clone();
executor
.spawn(async move {
let _guard = semaphore.acquire().await;
let output = evalf();
tx.send(output).ok();
})
.detach();
}
drop(tx);
let mut failed_evals = Vec::new();
let mut errored_evals = HashMap::new();
while let Ok(output) = rx.recv() {
processor.process(&output);
match output.outcome {
OutcomeKind::Passed => {}
OutcomeKind::Failed => {
failed_count += 1;
failed_evals.push(output);
}
OutcomeKind::Error => {
failed_count += 1;
*errored_evals.entry(output.data).or_insert(0) += 1;
}
}
evaluated_count += 1;
report_progress(evaluated_count, failed_count, iterations);
}
let actual_pass_ratio = (iterations - failed_count) as f32 / iterations as f32;
println!("Actual pass ratio: {}\n", actual_pass_ratio);
if actual_pass_ratio < expected_pass_ratio {
for (error, count) in errored_evals {
println!("Eval errored {} times. Error: {}", count, error);
}
for failed in failed_evals {
println!("Eval failed");
println!("{}", failed.data);
}
panic!(
"Actual pass ratio: {}\nExpected pass ratio: {}",
actual_pass_ratio, expected_pass_ratio
);
}
processor.assert();
}

View File

@@ -1,12 +1,13 @@
[package]
name = "zed_extension_api"
version = "0.7.0"
version = "0.8.0"
description = "APIs for creating Zed extensions in Rust"
repository = "https://github.com/zed-industries/zed"
documentation = "https://docs.rs/zed_extension_api"
keywords = ["zed", "extension"]
edition.workspace = true
publish = true
# Change back to `true` when we're ready to publish v0.8.0.
publish = false
license = "Apache-2.0"
[lints]

View File

@@ -6,6 +6,7 @@ pub mod settings;
use core::fmt;
use crate::settings::LspSettings;
use wit::*;
pub use serde_json;
@@ -85,19 +86,21 @@ pub trait Extension: Send + Sync {
/// Returns the initialization options to pass to the specified language server.
fn language_server_initialization_options(
&mut self,
_language_server_id: &LanguageServerId,
_worktree: &Worktree,
language_server_id: &LanguageServerId,
worktree: &Worktree,
) -> Result<Option<serde_json::Value>> {
Ok(None)
LspSettings::for_worktree(language_server_id.as_ref(), worktree)
.map(|settings| settings.initialization_options)
}
/// Returns the workspace configuration options to pass to the language server.
fn language_server_workspace_configuration(
&mut self,
_language_server_id: &LanguageServerId,
_worktree: &Worktree,
language_server_id: &LanguageServerId,
worktree: &Worktree,
) -> Result<Option<serde_json::Value>> {
Ok(None)
LspSettings::for_worktree(language_server_id.as_ref(), worktree)
.map(|settings| settings.settings)
}
/// Returns the initialization options to pass to the other language server.
@@ -334,7 +337,7 @@ mod wit {
wit_bindgen::generate!({
skip: ["init-extension"],
path: "./wit/since_v0.6.0",
path: "./wit/since_v0.8.0",
});
}

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