Compare commits

..

63 Commits

Author SHA1 Message Date
Smit Barmase
be4785fa65 use pathbuf 2025-06-10 12:41:48 +05:30
Smit Barmase
4cfd9bddcf manifest name 2025-06-10 12:41:48 +05:30
Smit Barmase
7372bd0919 feat. workspace scope 2025-06-10 12:41:46 +05:30
Smit Barmase
b3d63324e1 manifest provider 2025-06-10 12:41:07 +05:30
Smit Barmase
97e5ebade9 move eslint server out of ts-server 2025-06-10 12:41:03 +05:30
Burak Varlı
16853acbb1 Enable cross-region inference for Claude 4 family models on Amazon Bedrock provider (#32235)
These models require cross-region inference, and it currently fails if
you try to use them:
```
Invocation of model ID anthropic.claude-sonnet-4-20250514-v1:0 with on-demand throughput isn’t supported. 
```

Release Notes:

- Enable cross-region inference for Claude 4 family models on Amazon
Bedrock provider

Signed-off-by: Burak Varlı <burakvar@amazon.co.uk>
2025-06-09 23:38:39 -07:00
Michael Sloan
64d649245c Add missing #[track_caller] meant to be in #32433 (#32434)
Release Notes:

- N/A
2025-06-10 04:52:43 +00:00
Michael Sloan
08210b512d Don't push to selection history if selections are empty (#32433)
I got a panic during undo but haven't been able to repro it. Potentially
a consequence of my changes in #31731

> Thread "main" panicked with "There must be at least one selection" at
crates/editor/src/selections_collection.rs

Leaving release notes blank as I'm not sure this actually fixes the
panic

Release Notes:

- N/A
2025-06-10 04:29:45 +00:00
Michael Sloan
6070aea6c0 Skip adding initial 1:1 cursor position to selection history (#32432)
Also improves some minor corner cases in `undo_selection` and
`redo_selection` related to the use of `end_selection`. If the pending
selection was ended, this would separately get pushed to the redo or
undo stack and redundantly run all the other effects of selection
change.

Release Notes:

- N/A
2025-06-10 04:22:46 +00:00
Conrad Irwin
16b44d53f9 debugger: Use Delve to build Go binaries (#32221)
Release Notes:

- debugger: Use delve to build go debug executables, and pass arguments
through.

---------

Co-authored-by: sysradium <sysradium@users.noreply.github.com>
Co-authored-by: Zed AI <ai@zed.dev>
2025-06-09 21:49:04 -06:00
Haru Kim
3bed830a1f Use unix pipe to capture environment variables (#32136)
The use of `NamedTempFile` in #31799 was not secure and could
potentially cause write permission issues (see [this
comment](https://github.com/zed-industries/zed/issues/29528#issuecomment-2939672433)).
Therefore, it has been replaced with a Unix pipe.

Release Notes:
- N/A
2025-06-09 20:37:43 -06:00
Conrad Irwin
ee2a329981 Slow down reconnects on collab (#32418)
We believe that collab deploys currently cause outages because:

* All clients try to reconnect simultaneously
* This causes very high CPU usage on collab (and to some extent, the
database)
* This means that collab is slow to respond to clients
* So clients timeout and retry, over and over again.

We hope by letting clients in in buckets of 512, we can accept some
minor slowness to avoid
complete downtime, while we rewrite the system.

Release Notes:

- N/A
2025-06-09 19:59:04 -06:00
Joseph T. Lyons
6d64058fc6 Add pane: unpin all tabs (#32423)
After integrating pinned tabs into my workflow, I've come to the
conclusion that it is painfully slow to unpin all tabs by hand.


https://github.com/user-attachments/assets/ad087b8e-4595-4c4d-827f-188e36170c25

Release Notes:

- Added a `pane: unpin all tabs` action
2025-06-09 20:25:22 -04:00
Danilo Leal
7c2822a020 docs: Improve MCP-related pages (#32422)
While creating a new MCP extension this weekend, I visited these pages
and it felt like they could be improved a little bit. I'm renaming the
MCP-related page under the /extension directory to use the "MCP"
acronym, instead of "context servers".

Release Notes:

- N/A
2025-06-09 21:00:10 -03:00
Danilo Leal
3db00384f4 agent: Improve generating dots display (#32421)
Previously, upon hitting the "Continue" button to restart an interrupted
thread due to consecutive tool calls reaching its limit, you wouldn't
see the loading dots and the UI would be a weird state. This PR improves
when these loading dots actually show up, including in their conditional
a check for `message.is_hidden`.

Also took advantage of the opportunity to quickly organize some of these
variables. The `render_message` function could potentially be chopped up
in more smaller pieces. Lots of things going on here.

Release Notes:

- N/A
2025-06-09 20:52:50 -03:00
Conrad Irwin
3dfbd9e57c Fix ruby debugger (#32407)
Closes #ISSUE

Release Notes:

- debugger: Fix Ruby (was broken by #30833)

---------

Co-authored-by: Anthony Eid <hello@anthonyeid.me>
Co-authored-by: Piotr Osiewicz <peterosiewicz@gmail.com>
Co-authored-by: Cole Miller <m@cole-miller.net>
2025-06-09 16:11:24 -06:00
Agus Zubiaga
b103d7621b Improve handling of large output in embedded terminals (#32416)
#31922 made embedded terminals automatically grow to fit the content. We
since found some issues with large output which this PR addresses by:

- Only shaping / laying out lines that are visible in the viewport
(based on `window.content_mask`)
- Falling back to embedded scrolling after 1K lines. The perf fix above
actually makes it possible to handle a lot of lines, but:
- Alacrity uses a `u16` for rows internally, so we needed a limit to
prevent overflow.
- Scrolling through thousands of lines to get to the other side of a
terminal tool call isn't great UX, so we might as well set the limit
low.
- We can consider raising the limit when we make card headers sticky.

Release Notes:

- Agent: Improve handling of large terminal output
2025-06-09 18:11:31 -03:00
Danilo Leal
ab70e524c8 agent: Improve MCP status indicator tooltip and loading state (#32414)
Mostly a small tweak making sure that the indicator tooltip hit area is
bigger and the loading state is clearer (not using an indicator
anymore). Way more little improvement opportunities in this component to
do, though.

Release Notes:

- N/A
2025-06-09 18:04:48 -03:00
JonasKaplan
f0ce62ead8 editor: Add trailing whitespace rendering (#32329)
Closes #5237

- Adds "trailing" option for "show_whitespaces" in settings.json
- Supports importing this setting from vscode

The option in question will render only whitespace characters that
appear after every non-whitespace character in a given line.

Release Notes:

- Added trailing whitespace rendering
2025-06-09 20:48:49 +00:00
Cole Miller
f0345df479 debugger: Undo conversion of stack frames list to uniform list (#32413)
Partially reverts #30682

A uniform list is desirable for the scrolling behavior, but this breaks
badly when there are collapsed entries or entries without paths, both of
which seem common with the JS adapter.

It would be nice to go back to a uniform list if we can come up with a
set of design tweaks that allow all entries to be the same height.

Release Notes:

- Debugger Beta: fixed an issue that caused entries in the stack frame
list to overlap in some situations.
2025-06-09 20:45:01 +00:00
Michael Sloan
bbd2262a93 Fix buffer rendering on every mouse move (#32408)
Closes #32210

This notify was added in #13433. Solution is to only notify when the
breakpoint indicator state has changed.

Also improves the logic for enqueuing a task to delay showing - now only
does this if it isn't already visible, and that delay task now only
notifies if still hovering.

Release Notes:

- Fixed a bug where buffers render on every mouse move.
2025-06-09 14:10:03 -06:00
Antonio Scandurra
c4fd9e1a6b Switch to using weak transactions for queries happening on connection (#32411)
Release Notes:

- N/A

Co-authored-by: Conrad <conrad@zed.dev>
2025-06-09 21:37:48 +02:00
Nate Butler
0b7583bae5 Refine styling of merge conflicts (#31012)
- Improved colors
- Blank out diff hunk gutter highlights in conflict regions
- Paint conflict marker highlights all the way to the gutter

Release Notes:

- Improved the highlighting of merge conflict markers in editors.

---------

Co-authored-by: Marshall Bowers <git@maxdeviant.com>
Co-authored-by: Cole Miller <cole@zed.dev>
2025-06-09 19:03:19 +00:00
Ben Brandt
e4bd115a63 More resilient eval (#32257)
Bubbles up rate limit information so that we can retry after a certain
duration if needed higher up in the stack.

Also caps the number of concurrent evals running at once to also help.

Release Notes:

- N/A
2025-06-09 18:07:22 +00:00
Kirill Bulatov
fa54fa80d0 Store pulled diagnostics' result_ids more persistently (#32403)
Follow-up of https://github.com/zed-industries/zed/pull/19230

`BufferId` can change between file reopens: e.g. open the buffer, close
it, go back in history to reopen it — the 2nd one will have a different
`BufferId`, but the same `result_ids` semantically.

Release Notes:

- N/A
2025-06-09 17:05:33 +00:00
Antonio Scandurra
de16f2bbe6 Bypass account age check when feature flag is set (#32393)
Release Notes:

- N/A
2025-06-09 18:44:48 +02:00
Jason Lee
e3b13b54c9 title_bar: Merge Linux only code into platform_linux (#32401)
Release Notes:

- N/A
2025-06-09 19:19:24 +03:00
Tommy D. Rossi
2c5d2a58d8 Do not skip punctuation characters with alt-arrow if next character is \n (#32368)
Closes #32356

Release Notes:

- N/A
2025-06-09 09:25:32 -06:00
Peter Tripp
3485b7704b Update GitHub Issue Templates (June 2025) (#32399)
- Remove git/edit predictions templates
- Rename Agent to AI related (include edit predictions, copilot, etc)
- Other minor adjustments

Release Notes:

- N/A
2025-06-09 15:25:17 +00:00
Bennet Bo Fenner
6801b9137f context_server: Make notifications type safe (#32396)
Follow up to #32254 

Release Notes:

- N/A
2025-06-09 15:11:01 +00:00
Umesh Yadav
3853e83da7 git_ui: Improve error handling in commit message generation (#29005)
After and Before video of the issue and solution.


https://github.com/user-attachments/assets/40508f20-5549-4b3d-9331-85b8192a5b5a



Closes #27319

Release Notes:

- Provide Feedback on commit message generation error

---------

Signed-off-by: Umesh Yadav <umesh4257@gmail.com>
Co-authored-by: Cole Miller <cole@zed.dev>
2025-06-09 14:20:19 +00:00
Ben Hamment
047a7f5d29 Decrease the size of the branch picker icon (#32387)
<img width="323" alt="image"
src="https://github.com/user-attachments/assets/0060eaf3-35f9-4f0f-b9b6-e26ffad853c2"
/>

<img width="323" alt="image"
src="https://github.com/user-attachments/assets/57b66dae-2a74-401f-82c1-8fc730a98fb0
" />

Release Notes:

- Adjusted size of the icon inside the title bar's branch picker when
that's turned on.
2025-06-09 11:08:53 -03:00
Evan Simkowitz
8332e60ca9 language: Don't add final newline on format for an empty buffer (#32320)
Closes #32313

Release Notes:

- Fixed newline getting added on format to empty files
2025-06-09 09:00:17 -04:00
Bennet Bo Fenner
afab4b522e agent: Add tests for thread serialization code (#32383)
This adds some unit tests to ensure that the `update(...)`/migration
path to the latest versions works correctly

Release Notes:

- N/A
2025-06-09 12:20:19 +00:00
vipex
0cb7dd2972 git_panel: Persist dock size (#32111)
Closes #32054

The dock size for the git panel wasn't being persisted across Zed
restarts. This was because the git panel lacked the serialization
pattern used by other panels.

Please let me know if you have any sort of feedback or anything, as i'm
still trying to learn :]

Release Notes:

- Fixed Git Panel dock size not being remembered across Zed restarts

## TODO
- [x] Update/fix tests that may be broken by the GitPanel constructor
changes
2025-06-09 16:51:36 +05:30
Angelk90
387281fa5b project_panel: Add hide_root when only one folder in the project (#25289)
Closes #24188

Todo:
- [x] Hide root when only one worktree
- [x] Basic tests
- [x] Docs
- [x] Fix `select_first` + tests
- [x] Fix auto collapse dir + tests
- [x] Fix file / dir creation + tests
- [x] Fix root rename case

| Show root | Hide root |
|--------|--------|
| <img width="272" alt="Screenshot 2025-02-20 alle 22 35 55"
src="https://github.com/user-attachments/assets/361d93c7-e1ad-4419-a5f4-be62c9632807"
/> | <img width="269" alt="Screenshot 2025-02-20 alle 22 36 11"
src="https://github.com/user-attachments/assets/62011f76-a24b-4297-9734-f5c3b9f75760"
/> |
| <img width="275" alt="Screenshot 2025-02-20 alle 22 56 33"
src="https://github.com/user-attachments/assets/77e7e6e6-3dfe-4e88-b4b0-b620cb809d2b"
/> | <img width="267" alt="Screenshot 2025-02-20 alle 22 55 53"
src="https://github.com/user-attachments/assets/fa1099c8-7ed0-45ef-a7cf-aeb54b8283b1"
/> |


Release Notes:

- Added support to hide the root entry of the Project Panel when there’s
only one folder in the project. This can be enabled by setting
`hide_root` to `true` in the `project_panel` config.

---------

Co-authored-by: Smit Barmase <heysmitbarmase@gmail.com>
2025-06-09 16:46:31 +05:30
Piotr Osiewicz
72bcb0beb7 chore: Fix warnings for Rust 1.89 (#32378)
Closes #ISSUE

Release Notes:

- N/A
2025-06-09 13:11:57 +02:00
Bennet Bo Fenner
4ff41ba62e context_server: Update types to reflect latest protocol version (2025-03-26) (#32377)
This updates the `types.rs` file to reflect the latest version of the
MCP spec. Next up is making use of some of these new capabilities. Would
also be great to add support for [Streamable HTTP
Transport](https://modelcontextprotocol.io/specification/2025-03-26/basic/transports#streamable-http)

Release Notes:

- N/A
2025-06-09 13:03:47 +02:00
Bennet Bo Fenner
16e901fb8f docs: Remove reference to outdated Gemini models (#32379)
Release Notes:

- N/A
2025-06-09 10:50:30 +00:00
smaster
54b4587f9a Add bound checks for resizing right dock (#32246)
Closes #30293


[Before](https://github.com/user-attachments/assets/0b95e317-391a-4d90-ba78-ed3d4f10871d)
|
[After](https://github.com/user-attachments/assets/23002a73-103c-4a4f-a7a1-70950372c9d9)

Release Notes:

- Fixed right panel expanding in backwards, when dragged out of its
intended bounds, by adding a bounds check to ensure its size never gets
to high.
2025-06-09 10:39:53 +00:00
Clauses Kim
1fe10117b7 Add GitHub token environment variable support for Copilot (#31392)
Add support for environment variables as authentication alternatives to
OAuth flow for Copilot. Closes #31172

We can include the token in HTTPS request headers to hopefully resolve
the rate limiting issue in #9483. This change will be part of a separate
PR.

Release Notes:

- Added support for manually providing an OAuth token for GitHub Copilot
Chat by assigning the GH_COPILOT_TOKEN environment variable

---------

Co-authored-by: Bennet Bo Fenner <bennetbo@gmx.de>
2025-06-09 12:39:44 +02:00
Bennet Bo Fenner
78fd2685d5 gemini: Fix edge case when transforming MCP tool schema (#32373)
Closes #31766

Release Notes:

- Fixed an issue where some MCP tools would not work when using Gemini
2025-06-09 10:27:21 +00:00
Vitaly Slobodin
da9e958b15 ruby: Update documentation (#32345)
Hi! This pull request updates the Ruby extension documentation for [the
upcoming v0.9.0
upgrade](https://github.com/zed-extensions/ruby/pull/106):

- Added documentation for two newly added language servers: `sorbet` and
`steep`.
- Updated documentation on using the `ZED_CUSTOM_RUBY_TEST_NAME` symbol
for tasks.

Thanks!

Release Notes:

- N/A
2025-06-09 13:19:43 +03:00
dannybunschoten
3908ca9744 docs: Add JavaScript configuration to the example setup of Deno (#32104)
When using Deno with the example configuration as described here,
duplicate lsp information is displayed in Javascript files.

This pull request solves that issue by adding Javascript to the
configuration.

Release Notes:

- Improve LSP support when using Deno with Javascript using the default
configuration.
2025-06-09 13:18:06 +03:00
Alexander
6fe58a2c4e Allow to run dynamic TypeScript and JavaScript tests (#31499)
First of all thank you for such a fast editor!

I realized that the existing support for detecting runnable test cases
for typescript/javascript is not full. Meanwhile I can run most of test
by pressing "run button":

<img width="713" alt="image"
src="https://github.com/user-attachments/assets/e8bb1cb1-f0a5-4eb1-b9a6-7188a9fa47ae"
/>

I can't run dynamic tests:

<img width="703" alt="image"
src="https://github.com/user-attachments/assets/d7eef1bc-e99a-4f05-9d62-ec49b8194959"
/>

I was curious whether I can improve it on my own and created this pr. I
edited schemas and added minor changes in `TaskTemplate` to allow to
replace '%s' with regexp pattern, so it can match test cases:

<img width="772" alt="image"
src="https://github.com/user-attachments/assets/db3a6fe0-ad90-4853-8e98-4215e41dfe88"
/>

Release Notes:

- Allow to run dynamic TypeScript/JavaScript tests

---------

Co-authored-by: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com>
2025-06-09 12:13:25 +02:00
Dino
79e7ccc1fe vim: Handle case sensitive search editor setting (#32276)
Update the `vim::normal::search::Vim.search` method in order to
correctly set the search bar's case sensitive search option if the
`search.case_sensitive` setting is enabled.

Closes #32172 

Release Notes:

- vim: Fixed a bug where the `search.case_sensitive` setting was not respected when activating search with <kbd>/</kbd> (`vim::Search`)
2025-06-09 06:12:23 -04:00
Umesh Yadav
0bc9478b46 language_models: Add support for images to Mistral models (#32154)
Tested with following models. Hallucinates with whites outline images
like white lined zed logo but works fine with zed black outlined logo:

Pixtral 12B (pixtral-12b-latest)
Pixtral Large (pixtral-large-latest)
Mistral Medium (mistral-medium-latest)
Mistral Small (mistral-small-latest)

After this PR, almost all of the zed's llm provider who support images
are now supported. Only remaining one is LMStudio. Hopefully we will get
that one as well soon.

Release Notes:

- Add support for images to mistral models

---------

Signed-off-by: Umesh Yadav <git@umesh.dev>
Co-authored-by: Bennet Bo Fenner <bennetbo@gmx.de>
Co-authored-by: Bennet Bo Fenner <bennet@zed.dev>
2025-06-09 10:00:02 +00:00
Umesh Yadav
4ac7935589 language_models: Add thinking support to LM Studio provider (#32337)
It works similar to how deepseek works where the thinking is returned as
reasoning_content and we don't have to send the reasoning_content back
in the request.

This is a experiment feature which can be enabled from settings like
this:
<img width="1381" alt="Screenshot 2025-06-08 at 4 26 06 PM"
src="https://github.com/user-attachments/assets/d2f60f3c-0f93-45fc-bae2-4ded42981820"
/>

Here is how it looks to use(tested with
`deepseek/deepseek-r1-0528-qwen3-8b`

<img width="528" alt="Screenshot 2025-06-08 at 5 12 33 PM"
src="https://github.com/user-attachments/assets/f7716f52-5417-4f14-82b8-e853de054f63"
/>


Release Notes:

- Add thinking support to LM Studio provider
2025-06-09 11:55:34 +02:00
Umesh Yadav
c75ad2fd11 language_models: Add thinking support to DeepSeek provider (#32338)
For DeepSeek provider thinking is returned as reasoning_content and we
don't have to send the reasoning_content back in the request.

Release Notes:

- Add thinking support to DeepSeek provider
2025-06-09 11:10:55 +02:00
andrewkolda
365997d79d docs: Remove duplicate Clang-Format link (#32359)
Release Notes:

- N/A
2025-06-09 06:42:31 +00:00
Smit Barmase
c57a6263aa editor: Fix select when click on existing selection (#32365)
Follow-up for https://github.com/zed-industries/zed/pull/30671

Now, when clicking on an existing selection, the cursor will change on
`mouse_up` when `drag_and_drop_selection` is `true`. When
`drag_and_drop_selection` is `false`, it will change on `mouse_down`
(previous default).

Release Notes:

- N/A
2025-06-09 11:58:06 +05:30
Joseph T. Lyons
ebea734515 Coalesce consecutive spaces in new buffer tab titles (#32363)
VS Code has a behavior where it coalesces consecutive spaces in new
buffer tab titles, which I quite like. This presents the content better
and allows more meaningful content to be displayed, as consecutive
spaces don't count towards the 40 character limit.

VS Code

<img width="1013" alt="SCR-20250608-uelt"
src="https://github.com/user-attachments/assets/71a1fd4b-a506-4eab-b6a4-66096a12f1ad"
/>

Zed

<img width="1136" alt="SCR-20250608-ueif"
src="https://github.com/user-attachments/assets/f40fc3c9-0f0f-471d-93ed-be9568fbe778"
/>


Release Notes:

- N/A
2025-06-09 02:01:32 -04:00
CharlesChen0823
4fe05530b0 editor: Add support for drag_and_drop_selection (#30671)
Closes #4958 

Release Notes:

- Added support for drag and drop text selection. It can be disabled by
setting `drag_and_drop_selection` to `false`.

---------

Co-authored-by: Smit Barmase <heysmitbarmase@gmail.com>
2025-06-09 10:21:18 +05:30
Joseph T. Lyons
b15aef4310 Introduce dynamic tab titles for unsaved files based on buffer content (#32353)
https://github.com/user-attachments/assets/0bb08784-251c-4221-890a-2d6b3fb94e0f

For new, unsaved files:

- If a buffer has no content, or contains only whitespace, use
`untitled`
- If a buffer has content, take the first 40 chars of the first line

| Sublime | VS Code | Zed |
|---------|---------|-----|
| <img width="227" alt="SCR-20250608-ouux"
src="https://github.com/user-attachments/assets/d02b1e50-5775-4252-86e6-6c9d3f6c72fb"
/> | <img width="230" alt="SCR-20250608-ousn"
src="https://github.com/user-attachments/assets/7c9c016b-642f-4a80-9bc1-8c9bdc7bbd32"
/> | <img width="242" alt="SCR-20250608-ovbg"
src="https://github.com/user-attachments/assets/c7f4be5c-5bba-4a2a-b477-1392ca938cd5"
/> |

Note that this implementation also trims all leading whitespace, so that
if the buffer has any non-whitespace content, we use it. VS Code and
Sublime do not do this.

| Sublime | VS Code | Zed |
|---------|---------|-----|
| <img width="233" alt="SCR-20250608-oviq"
src="https://github.com/user-attachments/assets/ccffecc6-0f46-4d1b-8739-740240bc067b"
/> | <img width="198" alt="SCR-20250608-ovkq"
src="https://github.com/user-attachments/assets/35c20149-f898-417b-aff3-dda22b8cc1f3"
/> | <img width="233" alt="SCR-20250608-ovns"
src="https://github.com/user-attachments/assets/2509e8f6-254b-4fcb-a0ea-e18e95bb685b"
/> |

Release Notes:

- Introduced dynamic tab titles for unsaved files based on buffer
content
2025-06-08 17:30:33 -04:00
Michael Sloan
23adff6ff2 Add CI check that cmd- is not in linux keymaps + check other mods (#32334)
Motivation for the `cmd-` check is that there were a couple keybindings
using `cmd-` in the linux keymap and so these were bound to super /
windows

Release Notes:

- N/A
2025-06-08 09:34:07 +00:00
Michael Sloan
866fe427b3 Cleanup comments in linux keymaps (#32333)
Release Notes:

- N/A
2025-06-08 09:02:52 +00:00
Michael Sloan
f7b2faf64f Fix a few linux keybindings that use cmd- instead of ctrl- (#32332)
Release Notes:

- N/A
2025-06-08 08:50:35 +00:00
Kirill Bulatov
5187954711 Remove previous multi buffer hardcode from the outline panel (#32321)
Closes https://github.com/zed-industries/zed/issues/32316

Multi buffer design was changed so that control buttons are not
occupying extra lines, the hardcoded logic for that is obsolete thus
removed.

Release Notes:

- Fixed incorrect offsets during outline panel navigation in singleton
buffers
2025-06-07 23:54:47 +00:00
Michael Sloan
cabd22f36b No longer instantiate recently opened agent threads on startup (#32285)
This was causing a lot of work on startup, particularly due to
instantiating edit tool cards. The minor downside is that now these
threads don't open quite as fast.

Includes a few other improvements:

* On text thread rename, now immediately updates the metadata for
display in the UI instead of waiting for reload.

* On text thread rename, first renames the file before writing. Before
if the file removal failed you'd end up with a duplicate.

* Now only stores text thread file names instead of full paths. This is
more concise and allows for the app data dir changing location.

* Renames `ThreadStore::unordered_threads` to
`ThreadStore::reverse_chronological_threads` (and removes the old one
that sorted), since the recent change to use a SQL database queries them
in that order.

* Removes `ContextStore::reverse_chronological_contexts` since it was
only used in one location where it does sorting anyway - no need to sort
twice.

* `SavedContextMetadata::title` is now `SharedString` instead of
`String`.

Release Notes:

- Fixed regression in startup performance by not deserializing and
instantiating recently opened agent threads.
2025-06-07 14:53:36 -06:00
Tommy D. Rossi
1552198b55 Cursor keymap: Add cmd-enter to submit inline assistant (#32295)
Closes https://github.com/zed-industries/zed/discussions/29035

Release Notes:

- N/A
2025-06-07 16:37:45 -04:00
Richard Feldman
0da97b0c8b editor: Respect multi_cursor_modifier setting when making columnar selections using mouse (#32273)
Closes https://github.com/zed-industries/zed/issues/31181

Release Notes:

- Added the `multi_cursor_modifier` setting to be respected when making
columnar selections using the mouse drag.

---------

Co-authored-by: Smit Barmase <heysmitbarmase@gmail.com>
2025-06-08 01:51:13 +05:30
Joseph T. Lyons
037df8cec5 Simplify logic updating pinned tab count (#32310)
Just a tiny improvement to clean up the logic

Release Notes:

- N/A
2025-06-07 19:15:30 +00:00
Peter Tripp
05ac9f1f84 docs: Missing . from .sql-formatter.json (#32312)
See:
https://github.com/zed-industries/zed/issues/9537#issuecomment-2952784074

Release Notes:

- N/A
2025-06-07 19:11:19 +00:00
159 changed files with 4603 additions and 2310 deletions

View File

@@ -1,4 +1,4 @@
name: Bug Report (AI Related)
name: Bug Report (AI)
description: Zed Agent Panel Bugs
type: "Bug"
labels: ["ai"]
@@ -19,15 +19,14 @@ body:
2.
3.
Actual Behavior:
Expected Behavior:
**Expected Behavior**:
**Actual Behavior**:
### Model Provider Details
- Provider: (Anthropic via ZedPro, Anthropic via API key, Copilot Chat, Mistral, OpenAI, etc)
- Model Name:
- Mode: (Agent Panel, Inline Assistant, Terminal Assistant or Text Threads)
- MCP Servers in-use:
- Other Details:
- Other Details (MCPs, other settings, etc):
validations:
required: true

View File

@@ -1,36 +0,0 @@
name: Bug Report (Edit Predictions)
description: Zed Edit Predictions bugs
type: "Bug"
labels: ["ai", "inline completion", "zeta"]
title: "Edit Predictions: <a short description of the Edit Prediction bug>"
body:
- type: textarea
attributes:
label: Summary
description: Describe the bug with a one line summary, and provide detailed reproduction steps
value: |
<!-- Please insert a one line summary of the issue below -->
SUMMARY_SENTENCE_HERE
### Description
<!-- Describe with sufficient detail to reproduce from a clean Zed install. -->
<!-- Please include the LLM provider and model name you are using -->
Steps to trigger the problem:
1.
2.
3.
Actual Behavior:
Expected Behavior:
validations:
required: true
- type: textarea
id: environment
attributes:
label: Zed Version and System Specs
description: 'Open Zed, and in the command palette select "zed: copy system specs into clipboard"'
placeholder: |
Output of "zed: copy system specs into clipboard"
validations:
required: true

View File

@@ -1,35 +0,0 @@
name: Bug Report (Git)
description: Zed Git-Related Bugs
type: "Bug"
labels: ["git"]
title: "Git: <a short description of the Git bug>"
body:
- type: textarea
attributes:
label: Summary
description: Describe the bug with a one line summary, and provide detailed reproduction steps
value: |
<!-- Please insert a one line summary of the issue below -->
SUMMARY_SENTENCE_HERE
### Description
<!-- Describe with sufficient detail to reproduce from a clean Zed install. -->
Steps to trigger the problem:
1.
2.
3.
Actual Behavior:
Expected Behavior:
validations:
required: true
- type: textarea
id: environment
attributes:
label: Zed Version and System Specs
description: 'Open Zed, and in the command palette select "zed: copy system specs into clipboard"'
placeholder: |
Output of "zed: copy system specs into clipboard"
validations:
required: true

View File

@@ -19,8 +19,8 @@ body:
2.
3.
Actual Behavior:
Expected Behavior:
**Expected Behavior**:
**Actual Behavior**:
validations:
required: true

View File

@@ -18,14 +18,16 @@ body:
- Issues with insufficient detail may be summarily closed.
-->
DESCRIPTION_HERE
Steps to reproduce:
1.
2.
3.
4.
Expected Behavior:
Actual Behavior:
**Expected Behavior**:
**Actual Behavior**:
<!-- Before Submitting, did you:
1. Include settings.json, keymap.json, .editorconfig if relevant?

View File

@@ -183,6 +183,9 @@ jobs:
- name: Check for todo! and FIXME comments
run: script/check-todos
- name: Check modifier use in keymaps
run: script/check-keymaps
- name: Run style checks
uses: ./.github/actions/check_style
@@ -683,7 +686,7 @@ jobs:
timeout-minutes: 60
name: Linux arm64 release bundle
runs-on:
- hosted-linux-arm-1
- buildjet-16vcpu-ubuntu-2204-arm
if: |
startsWith(github.ref, 'refs/tags/v')
|| contains(github.event.pull_request.labels.*.name, 'run-bundling')

View File

@@ -62,7 +62,7 @@ jobs:
- name: Run unit evals
shell: bash -euxo pipefail {0}
run: cargo nextest run --workspace --no-fail-fast --features eval --no-capture -E 'test(::eval_)' --test-threads 1
run: cargo nextest run --workspace --no-fail-fast --features eval --no-capture -E 'test(::eval_)'
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}

27
Cargo.lock generated
View File

@@ -99,6 +99,7 @@ dependencies = [
"paths",
"picker",
"postage",
"pretty_assertions",
"project",
"prompt_store",
"proto",
@@ -704,6 +705,7 @@ dependencies = [
"serde_json",
"settings",
"smallvec",
"smol",
"streaming_diff",
"strsim",
"task",
@@ -3159,6 +3161,16 @@ dependencies = [
"memchr",
]
[[package]]
name = "command-fds"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2ec1052629a80c28594777d1252efc8a6b005d13f9edfd8c3fc0f44d5b32489a"
dependencies = [
"nix 0.30.1",
"thiserror 2.0.12",
]
[[package]]
name = "command_palette"
version = "0.1.0"
@@ -4051,6 +4063,7 @@ version = "0.1.0"
dependencies = [
"anyhow",
"async-trait",
"collections",
"dap",
"futures 0.3.31",
"gpui",
@@ -10129,6 +10142,18 @@ dependencies = [
"memoffset",
]
[[package]]
name = "nix"
version = "0.30.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6"
dependencies = [
"bitflags 2.9.0",
"cfg-if",
"cfg_aliases 0.2.1",
"libc",
]
[[package]]
name = "node_runtime"
version = "0.1.0"
@@ -12109,7 +12134,6 @@ dependencies = [
"unindent",
"url",
"util",
"uuid",
"which 6.0.3",
"workspace-hack",
"worktree",
@@ -17121,6 +17145,7 @@ dependencies = [
"async-fs",
"async_zip",
"collections",
"command-fds",
"dirs 4.0.0",
"dunce",
"futures 0.3.31",

View File

@@ -35,7 +35,7 @@
"ctrl-shift-f5": "debugger::Restart",
"f6": "debugger::Pause",
"f7": "debugger::StepOver",
"cmd-f11": "debugger::StepInto",
"ctrl-f11": "debugger::StepInto",
"shift-f11": "debugger::StepOut",
"f11": "zed::ToggleFullScreen",
"ctrl-alt-z": "edit_prediction::RateCompletions",
@@ -59,7 +59,6 @@
"tab": "editor::Tab",
"shift-tab": "editor::Backtab",
"ctrl-k": "editor::CutToEndOfLine",
// "ctrl-t": "editor::Transpose",
"ctrl-k ctrl-q": "editor::Rewrap",
"ctrl-k q": "editor::Rewrap",
"ctrl-backspace": "editor::DeleteToPreviousWordStart",
@@ -100,21 +99,16 @@
"shift-down": "editor::SelectDown",
"shift-left": "editor::SelectLeft",
"shift-right": "editor::SelectRight",
"ctrl-shift-left": "editor::SelectToPreviousWordStart", // cursorWordLeftSelect
"ctrl-shift-right": "editor::SelectToNextWordEnd", // cursorWordRightSelect
"ctrl-shift-left": "editor::SelectToPreviousWordStart",
"ctrl-shift-right": "editor::SelectToNextWordEnd",
"ctrl-shift-home": "editor::SelectToBeginning",
"ctrl-shift-end": "editor::SelectToEnd",
"ctrl-a": "editor::SelectAll",
"ctrl-l": "editor::SelectLine",
"ctrl-shift-i": "editor::Format",
"alt-shift-o": "editor::OrganizeImports",
// "cmd-shift-left": ["editor::SelectToBeginningOfLine", {"stop_at_soft_wraps": true, "stop_at_indent": true }],
// "ctrl-shift-a": ["editor::SelectToBeginningOfLine", { "stop_at_soft_wraps": true, "stop_at_indent": true }],
"shift-home": ["editor::SelectToBeginningOfLine", { "stop_at_soft_wraps": true, "stop_at_indent": true }],
// "cmd-shift-right": ["editor::SelectToEndOfLine", { "stop_at_soft_wraps": true }],
// "ctrl-shift-e": ["editor::SelectToEndOfLine", { "stop_at_soft_wraps": true }],
"shift-end": ["editor::SelectToEndOfLine", { "stop_at_soft_wraps": true }],
// "alt-v": ["editor::MovePageUp", { "center_cursor": true }],
"ctrl-alt-space": "editor::ShowCharacterPalette",
"ctrl-;": "editor::ToggleLineNumbers",
"ctrl-'": "editor::ToggleSelectedDiffHunks",
@@ -140,7 +134,6 @@
"find": "buffer_search::Deploy",
"ctrl-f": "buffer_search::Deploy",
"ctrl-h": "buffer_search::DeployReplace",
// "cmd-e": ["buffer_search::Deploy", { "focus": false }],
"ctrl->": "assistant::QuoteSelection",
"ctrl-<": "assistant::InsertIntoEditor",
"ctrl-alt-e": "editor::SelectEnclosingSymbol",
@@ -267,8 +260,8 @@
{
"context": "AgentPanel && prompt_editor",
"bindings": {
"cmd-n": "agent::NewTextThread",
"cmd-alt-t": "agent::NewThread"
"ctrl-n": "agent::NewTextThread",
"ctrl-alt-t": "agent::NewThread"
}
},
{

View File

@@ -38,7 +38,7 @@
"ctrl-shift-d": "editor::DuplicateSelection",
"alt-f3": "editor::SelectAllMatches", // find_all_under
// "ctrl-f3": "", // find_under (cancels any selections)
// "cmd-alt-shift-g": "" // find_under_prev (cancels any selections)
// "ctrl-alt-shift-g": "" // find_under_prev (cancels any selections)
"f9": "editor::SortLinesCaseSensitive",
"ctrl-f9": "editor::SortLinesCaseInsensitive",
"f12": "editor::GoToDefinition",

View File

@@ -28,7 +28,8 @@
"context": "InlineAssistEditor",
"use_key_equivalents": true,
"bindings": {
"cmd-shift-backspace": "editor::Cancel"
"cmd-shift-backspace": "editor::Cancel",
"cmd-enter": "menu::Confirm"
// "alt-enter": // Quick Question
// "cmd-shift-enter": // Full File Context
// "cmd-shift-k": // Toggle input focus (editor <> inline assist)

View File

@@ -101,9 +101,12 @@
// The second option is decimal.
"unit": "binary"
},
// The key to use for adding multiple cursors
// Currently "alt" or "cmd_or_ctrl" (also aliased as
// "cmd" and "ctrl") are supported.
// Determines the modifier to be used to add multiple cursors with the mouse. The open hover link mouse gestures will adapt such that it do not conflict with the multicursor modifier.
//
// 1. Maps to `Alt` on Linux and Windows and to `Option` on MacOS:
// "alt"
// 2. Maps `Control` on Linux and Windows and to `Command` on MacOS:
// "cmd_or_ctrl" (alias: "cmd", "ctrl")
"multi_cursor_modifier": "alt",
// Whether to enable vim modes and key bindings.
"vim_mode": false,
@@ -214,6 +217,8 @@
"show_signature_help_after_edits": false,
// Whether to show code action button at start of buffer line.
"inline_code_actions": true,
// Whether to allow drag and drop text selection in buffer.
"drag_and_drop_selection": true,
// What to do when go to definition yields no results.
//
// 1. Do nothing: `none`
@@ -599,7 +604,9 @@
// 2. Never show indent guides:
// "never"
"show": "always"
}
},
// Whether to hide the root entry when only one folder is open in the window.
"hide_root": false
},
"outline_panel": {
// Whether to show the outline panel button in the status bar

View File

@@ -99,6 +99,8 @@
"version_control.added": "#27a657ff",
"version_control.modified": "#d3b020ff",
"version_control.deleted": "#e06c76ff",
"version_control.conflict_marker.ours": "#a1c1811a",
"version_control.conflict_marker.theirs": "#74ade81a",
"conflict": "#dec184ff",
"conflict.background": "#dec1841a",
"conflict.border": "#5d4c2fff",

View File

@@ -109,5 +109,6 @@ gpui = { workspace = true, "features" = ["test-support"] }
indoc.workspace = true
language = { workspace = true, "features" = ["test-support"] }
language_model = { workspace = true, "features" = ["test-support"] }
pretty_assertions.workspace = true
project = { workspace = true, features = ["test-support"] }
rand.workspace = true

View File

@@ -1788,12 +1788,31 @@ impl ActiveThread {
fn render_message(&self, ix: usize, window: &mut Window, cx: &mut Context<Self>) -> AnyElement {
let message_id = self.messages[ix];
let Some(message) = self.thread.read(cx).message(message_id) else {
let workspace = self.workspace.clone();
let thread = self.thread.read(cx);
let is_first_message = ix == 0;
let is_last_message = ix == self.messages.len() - 1;
let Some(message) = thread.message(message_id) else {
return Empty.into_any();
};
let is_generating = thread.is_generating();
let is_generating_stale = thread.is_generation_stale().unwrap_or(false);
let loading_dots = (is_generating && is_last_message).then(|| {
h_flex()
.h_8()
.my_3()
.mx_5()
.when(is_generating_stale || message.is_hidden, |this| {
this.child(AnimatedLabel::new("").size(LabelSize::Small))
})
});
if message.is_hidden {
return Empty.into_any();
return div().children(loading_dots).into_any();
}
let message_creases = message.creases.clone();
@@ -1802,9 +1821,6 @@ impl ActiveThread {
return Empty.into_any();
};
let workspace = self.workspace.clone();
let thread = self.thread.read(cx);
// Get all the data we need from thread before we start using it in closures
let checkpoint = thread.checkpoint_for_message(message_id);
let configured_model = thread.configured_model().map(|m| m.model);
@@ -1815,14 +1831,6 @@ impl ActiveThread {
let tool_uses = thread.tool_uses_for_message(message_id, cx);
let has_tool_uses = !tool_uses.is_empty();
let is_generating = thread.is_generating();
let is_generating_stale = thread.is_generation_stale().unwrap_or(false);
let is_first_message = ix == 0;
let is_last_message = ix == self.messages.len() - 1;
let loading_dots = (is_generating_stale && is_last_message)
.then(|| AnimatedLabel::new("").size(LabelSize::Small));
let editing_message_state = self
.editing_message
@@ -2238,17 +2246,7 @@ impl ActiveThread {
parent.child(self.render_rules_item(cx))
})
.child(styled_message)
.when(is_generating && is_last_message, |this| {
this.child(
h_flex()
.h_8()
.mt_2()
.mb_4()
.ml_4()
.py_1p5()
.when_some(loading_dots, |this, loading_dots| this.child(loading_dots)),
)
})
.children(loading_dots)
.when(show_feedback, move |parent| {
parent.child(feedback_items).when_some(
self.open_feedback_editors.get(&message_id),

View File

@@ -12,7 +12,7 @@ use context_server::ContextServerId;
use fs::Fs;
use gpui::{
Action, Animation, AnimationExt as _, AnyView, App, Entity, EventEmitter, FocusHandle,
Focusable, ScrollHandle, Subscription, pulsating_between,
Focusable, ScrollHandle, Subscription, Transformation, percentage,
};
use language_model::{LanguageModelProvider, LanguageModelProviderId, LanguageModelRegistry};
use project::context_server_store::{ContextServerStatus, ContextServerStore};
@@ -475,7 +475,6 @@ impl AgentConfiguration {
.get(&context_server_id)
.copied()
.unwrap_or_default();
let tools = tools_by_source
.get(&ToolSource::ContextServer {
id: context_server_id.0.clone().into(),
@@ -484,25 +483,23 @@ impl AgentConfiguration {
let tool_count = tools.len();
let border_color = cx.theme().colors().border.opacity(0.6);
let success_color = Color::Success.color(cx);
let (status_indicator, tooltip_text) = match server_status {
ContextServerStatus::Starting => (
Indicator::dot()
.color(Color::Success)
Icon::new(IconName::LoadCircle)
.size(IconSize::XSmall)
.color(Color::Accent)
.with_animation(
SharedString::from(format!("{}-starting", context_server_id.0.clone(),)),
Animation::new(Duration::from_secs(2))
.repeat()
.with_easing(pulsating_between(0.4, 1.)),
move |this, delta| this.color(success_color.alpha(delta).into()),
Animation::new(Duration::from_secs(3)).repeat(),
|icon, delta| icon.transform(Transformation::rotate(percentage(delta))),
)
.into_any_element(),
"Server is starting.",
),
ContextServerStatus::Running => (
Indicator::dot().color(Color::Success).into_any_element(),
"Server is running.",
"Server is active.",
),
ContextServerStatus::Error(_) => (
Indicator::dot().color(Color::Error).into_any_element(),
@@ -526,12 +523,11 @@ impl AgentConfiguration {
.p_1()
.justify_between()
.when(
error.is_some() || are_tools_expanded && tool_count > 1,
error.is_some() || are_tools_expanded && tool_count >= 1,
|element| element.border_b_1().border_color(border_color),
)
.child(
h_flex()
.gap_1p5()
.child(
Disclosure::new(
"tool-list-disclosure",
@@ -551,12 +547,16 @@ impl AgentConfiguration {
})),
)
.child(
div()
.id(item_id.clone())
h_flex()
.id(SharedString::from(format!("tooltip-{}", item_id)))
.h_full()
.w_3()
.mx_1()
.justify_center()
.tooltip(Tooltip::text(tooltip_text))
.child(status_indicator),
)
.child(Label::new(context_server_id.0.clone()).ml_0p5())
.child(Label::new(item_id).ml_0p5().mr_1p5())
.when(is_running, |this| {
this.child(
Label::new(if tool_count == 1 {

View File

@@ -57,7 +57,7 @@ use zed_llm_client::{CompletionIntent, UsageLimit};
use crate::active_thread::{self, ActiveThread, ActiveThreadEvent};
use crate::agent_configuration::{AgentConfiguration, AssistantConfigurationEvent};
use crate::agent_diff::AgentDiff;
use crate::history_store::{HistoryStore, RecentEntry};
use crate::history_store::{HistoryEntryId, HistoryStore};
use crate::message_editor::{MessageEditor, MessageEditorEvent};
use crate::thread::{Thread, ThreadError, ThreadId, ThreadSummary, TokenUsageRatio};
use crate::thread_history::{HistoryEntryElement, ThreadHistory};
@@ -257,6 +257,7 @@ impl ActiveView {
pub fn prompt_editor(
context_editor: Entity<ContextEditor>,
history_store: Entity<HistoryStore>,
language_registry: Arc<LanguageRegistry>,
window: &mut Window,
cx: &mut App,
@@ -322,6 +323,19 @@ impl ActiveView {
editor.set_text(summary, window, cx);
})
}
ContextEvent::PathChanged { old_path, new_path } => {
history_store.update(cx, |history_store, cx| {
if let Some(old_path) = old_path {
history_store
.replace_recently_opened_text_thread(old_path, new_path, cx);
} else {
history_store.push_recently_opened_entry(
HistoryEntryId::Context(new_path.clone()),
cx,
);
}
});
}
_ => {}
}
}),
@@ -516,8 +530,7 @@ impl AgentPanel {
HistoryStore::new(
thread_store.clone(),
context_store.clone(),
[RecentEntry::Thread(thread_id, thread.clone())],
window,
[HistoryEntryId::Thread(thread_id)],
cx,
)
});
@@ -544,7 +557,13 @@ impl AgentPanel {
editor.insert_default_prompt(window, cx);
editor
});
ActiveView::prompt_editor(context_editor, language_registry.clone(), window, cx)
ActiveView::prompt_editor(
context_editor,
history_store.clone(),
language_registry.clone(),
window,
cx,
)
}
};
@@ -581,86 +600,9 @@ impl AgentPanel {
let panel = weak_panel.clone();
let assistant_navigation_menu =
ContextMenu::build_persistent(window, cx, move |mut menu, _window, cx| {
let recently_opened = panel
.update(cx, |this, cx| {
this.history_store.update(cx, |history_store, cx| {
history_store.recently_opened_entries(cx)
})
})
.unwrap_or_default();
if !recently_opened.is_empty() {
menu = menu.header("Recently Opened");
for entry in recently_opened.iter() {
if let RecentEntry::Context(context) = entry {
if context.read(cx).path().is_none() {
log::error!(
"bug: text thread in recent history list was never saved"
);
continue;
}
}
let summary = entry.summary(cx);
menu = menu.entry_with_end_slot_on_hover(
summary,
None,
{
let panel = panel.clone();
let entry = entry.clone();
move |window, cx| {
panel
.update(cx, {
let entry = entry.clone();
move |this, cx| match entry {
RecentEntry::Thread(_, thread) => {
this.open_thread(thread, window, cx)
}
RecentEntry::Context(context) => {
let Some(path) = context.read(cx).path()
else {
return;
};
this.open_saved_prompt_editor(
path.clone(),
window,
cx,
)
.detach_and_log_err(cx)
}
}
})
.ok();
}
},
IconName::Close,
"Close Entry".into(),
{
let panel = panel.clone();
let entry = entry.clone();
move |_window, cx| {
panel
.update(cx, |this, cx| {
this.history_store.update(
cx,
|history_store, cx| {
history_store.remove_recently_opened_entry(
&entry, cx,
);
},
);
})
.ok();
}
},
);
}
menu = menu.separator();
if let Some(panel) = panel.upgrade() {
menu = Self::populate_recently_opened_menu_section(menu, panel, cx);
}
menu.action("View All", Box::new(OpenHistory))
.end_slot_action(DeleteRecentlyOpenThread.boxed_clone())
.fixed_width(px(320.).into())
@@ -898,6 +840,7 @@ impl AgentPanel {
self.set_active_view(
ActiveView::prompt_editor(
context_editor.clone(),
self.history_store.clone(),
self.language_registry.clone(),
window,
cx,
@@ -984,7 +927,13 @@ impl AgentPanel {
)
});
self.set_active_view(
ActiveView::prompt_editor(editor.clone(), self.language_registry.clone(), window, cx),
ActiveView::prompt_editor(
editor.clone(),
self.history_store.clone(),
self.language_registry.clone(),
window,
cx,
),
window,
cx,
);
@@ -1383,16 +1332,6 @@ impl AgentPanel {
}
}
}
ActiveView::TextThread { context_editor, .. } => {
let context = context_editor.read(cx).context();
// When switching away from an unsaved text thread, delete its entry.
if context.read(cx).path().is_none() {
let context = context.clone();
self.history_store.update(cx, |store, cx| {
store.remove_recently_opened_entry(&RecentEntry::Context(context), cx);
});
}
}
_ => {}
}
@@ -1400,13 +1339,14 @@ impl AgentPanel {
ActiveView::Thread { thread, .. } => self.history_store.update(cx, |store, cx| {
if let Some(thread) = thread.upgrade() {
let id = thread.read(cx).id().clone();
store.push_recently_opened_entry(RecentEntry::Thread(id, thread), cx);
store.push_recently_opened_entry(HistoryEntryId::Thread(id), cx);
}
}),
ActiveView::TextThread { context_editor, .. } => {
self.history_store.update(cx, |store, cx| {
let context = context_editor.read(cx).context().clone();
store.push_recently_opened_entry(RecentEntry::Context(context), cx)
if let Some(path) = context_editor.read(cx).context().read(cx).path() {
store.push_recently_opened_entry(HistoryEntryId::Context(path.clone()), cx)
}
})
}
_ => {}
@@ -1425,6 +1365,70 @@ impl AgentPanel {
self.focus_handle(cx).focus(window);
}
fn populate_recently_opened_menu_section(
mut menu: ContextMenu,
panel: Entity<Self>,
cx: &mut Context<ContextMenu>,
) -> ContextMenu {
let entries = panel
.read(cx)
.history_store
.read(cx)
.recently_opened_entries(cx);
if entries.is_empty() {
return menu;
}
menu = menu.header("Recently Opened");
for entry in entries {
let title = entry.title().clone();
let id = entry.id();
menu = menu.entry_with_end_slot_on_hover(
title,
None,
{
let panel = panel.downgrade();
let id = id.clone();
move |window, cx| {
let id = id.clone();
panel
.update(cx, move |this, cx| match id {
HistoryEntryId::Thread(id) => this
.open_thread_by_id(&id, window, cx)
.detach_and_log_err(cx),
HistoryEntryId::Context(path) => this
.open_saved_prompt_editor(path.clone(), window, cx)
.detach_and_log_err(cx),
})
.ok();
}
},
IconName::Close,
"Close Entry".into(),
{
let panel = panel.downgrade();
let id = id.clone();
move |_window, cx| {
panel
.update(cx, |this, cx| {
this.history_store.update(cx, |history_store, cx| {
history_store.remove_recently_opened_entry(&id, cx);
});
})
.ok();
}
},
);
}
menu = menu.separator();
menu
}
}
impl Focusable for AgentPanel {

View File

@@ -386,8 +386,10 @@ impl CodegenAlternative {
async { Ok(LanguageModelTextStream::default()) }.boxed_local()
} else {
let request = self.build_request(&model, user_prompt, cx)?;
cx.spawn(async move |_, cx| model.stream_completion_text(request.await, &cx).await)
.boxed_local()
cx.spawn(async move |_, cx| {
Ok(model.stream_completion_text(request.await, &cx).await?)
})
.boxed_local()
};
self.handle_stream(telemetry_id, provider_id.to_string(), api_key, stream, cx);
Ok(())

View File

@@ -282,15 +282,18 @@ pub fn unordered_thread_entries(
text_thread_store: Entity<TextThreadStore>,
cx: &App,
) -> impl Iterator<Item = (DateTime<Utc>, ThreadContextEntry)> {
let threads = thread_store.read(cx).unordered_threads().map(|thread| {
(
thread.updated_at,
ThreadContextEntry::Thread {
id: thread.id.clone(),
title: thread.summary.clone(),
},
)
});
let threads = thread_store
.read(cx)
.reverse_chronological_threads()
.map(|thread| {
(
thread.updated_at,
ThreadContextEntry::Thread {
id: thread.id.clone(),
title: thread.summary.clone(),
},
)
});
let text_threads = text_thread_store
.read(cx)
@@ -300,7 +303,7 @@ pub fn unordered_thread_entries(
context.mtime.to_utc(),
ThreadContextEntry::Context {
path: context.path.clone(),
title: context.title.clone().into(),
title: context.title.clone(),
},
)
});

View File

@@ -105,7 +105,7 @@ impl Tool for ContextServerTool {
arguments
);
let response = protocol
.request::<context_server::types::request::CallTool>(
.request::<context_server::types::requests::CallTool>(
context_server::types::CallToolParams {
name: tool_name,
arguments,
@@ -123,6 +123,9 @@ impl Tool for ContextServerTool {
types::ToolResponseContent::Image { .. } => {
log::warn!("Ignoring image content from tool response");
}
types::ToolResponseContent::Audio { .. } => {
log::warn!("Ignoring audio content from tool response");
}
types::ToolResponseContent::Resource { .. } => {
log::warn!("Ignoring resource content from tool response");
}

View File

@@ -1,18 +1,17 @@
use std::{collections::VecDeque, path::Path, sync::Arc};
use anyhow::Context as _;
use assistant_context_editor::{AssistantContext, SavedContextMetadata};
use anyhow::{Context as _, Result};
use assistant_context_editor::SavedContextMetadata;
use chrono::{DateTime, Utc};
use futures::future::{TryFutureExt as _, join_all};
use gpui::{Entity, Task, prelude::*};
use gpui::{AsyncApp, Entity, SharedString, Task, prelude::*};
use itertools::Itertools;
use paths::contexts_dir;
use serde::{Deserialize, Serialize};
use smol::future::FutureExt;
use std::time::Duration;
use ui::{App, SharedString, Window};
use ui::App;
use util::ResultExt as _;
use crate::{
Thread,
thread::ThreadId,
thread_store::{SerializedThreadMetadata, ThreadStore},
};
@@ -41,52 +40,34 @@ impl HistoryEntry {
HistoryEntry::Context(context) => HistoryEntryId::Context(context.path.clone()),
}
}
pub fn title(&self) -> &SharedString {
match self {
HistoryEntry::Thread(thread) => &thread.summary,
HistoryEntry::Context(context) => &context.title,
}
}
}
/// Generic identifier for a history entry.
#[derive(Clone, PartialEq, Eq)]
#[derive(Clone, PartialEq, Eq, Debug)]
pub enum HistoryEntryId {
Thread(ThreadId),
Context(Arc<Path>),
}
#[derive(Clone, Debug)]
pub(crate) enum RecentEntry {
Thread(ThreadId, Entity<Thread>),
Context(Entity<AssistantContext>),
}
impl PartialEq for RecentEntry {
fn eq(&self, other: &Self) -> bool {
match (self, other) {
(Self::Thread(l0, _), Self::Thread(r0, _)) => l0 == r0,
(Self::Context(l0), Self::Context(r0)) => l0 == r0,
_ => false,
}
}
}
impl Eq for RecentEntry {}
impl RecentEntry {
pub(crate) fn summary(&self, cx: &App) -> SharedString {
match self {
RecentEntry::Thread(_, thread) => thread.read(cx).summary().or_default(),
RecentEntry::Context(context) => context.read(cx).summary().or_default(),
}
}
}
#[derive(Serialize, Deserialize)]
enum SerializedRecentEntry {
enum SerializedRecentOpen {
Thread(String),
ContextName(String),
/// Old format which stores the full path
Context(String),
}
pub struct HistoryStore {
thread_store: Entity<ThreadStore>,
context_store: Entity<assistant_context_editor::ContextStore>,
recently_opened_entries: VecDeque<RecentEntry>,
recently_opened_entries: VecDeque<HistoryEntryId>,
_subscriptions: Vec<gpui::Subscription>,
_save_recently_opened_entries_task: Task<()>,
}
@@ -95,8 +76,7 @@ impl HistoryStore {
pub fn new(
thread_store: Entity<ThreadStore>,
context_store: Entity<assistant_context_editor::ContextStore>,
initial_recent_entries: impl IntoIterator<Item = RecentEntry>,
window: &mut Window,
initial_recent_entries: impl IntoIterator<Item = HistoryEntryId>,
cx: &mut Context<Self>,
) -> Self {
let subscriptions = vec![
@@ -104,68 +84,20 @@ impl HistoryStore {
cx.observe(&context_store, |_, _, cx| cx.notify()),
];
window
.spawn(cx, {
let thread_store = thread_store.downgrade();
let context_store = context_store.downgrade();
let this = cx.weak_entity();
async move |cx| {
let path = paths::data_dir().join(NAVIGATION_HISTORY_PATH);
let contents = cx
.background_spawn(async move { std::fs::read_to_string(path) })
.await
.ok()?;
let entries = serde_json::from_str::<Vec<SerializedRecentEntry>>(&contents)
.context("deserializing persisted agent panel navigation history")
.log_err()?
.into_iter()
.take(MAX_RECENTLY_OPENED_ENTRIES)
.map(|serialized| match serialized {
SerializedRecentEntry::Thread(id) => thread_store
.update_in(cx, |thread_store, window, cx| {
let thread_id = ThreadId::from(id.as_str());
thread_store
.open_thread(&thread_id, window, cx)
.map_ok(|thread| RecentEntry::Thread(thread_id, thread))
.boxed()
})
.unwrap_or_else(|_| {
async {
anyhow::bail!("no thread store");
}
.boxed()
}),
SerializedRecentEntry::Context(id) => context_store
.update(cx, |context_store, cx| {
context_store
.open_local_context(Path::new(&id).into(), cx)
.map_ok(RecentEntry::Context)
.boxed()
})
.unwrap_or_else(|_| {
async {
anyhow::bail!("no context store");
}
.boxed()
}),
});
let entries = join_all(entries)
.await
.into_iter()
.filter_map(|result| result.log_with_level(log::Level::Debug))
.collect::<VecDeque<_>>();
this.update(cx, |this, _| {
this.recently_opened_entries.extend(entries);
this.recently_opened_entries
.truncate(MAX_RECENTLY_OPENED_ENTRIES);
})
.ok();
Some(())
}
cx.spawn(async move |this, cx| {
let entries = Self::load_recently_opened_entries(cx).await.log_err()?;
this.update(cx, |this, _| {
this.recently_opened_entries
.extend(
entries.into_iter().take(
MAX_RECENTLY_OPENED_ENTRIES
.saturating_sub(this.recently_opened_entries.len()),
),
);
})
.detach();
.ok()
})
.detach();
Self {
thread_store,
@@ -184,19 +116,20 @@ impl HistoryStore {
return history_entries;
}
for thread in self
.thread_store
.update(cx, |this, _cx| this.reverse_chronological_threads())
{
history_entries.push(HistoryEntry::Thread(thread));
}
for context in self
.context_store
.update(cx, |this, _cx| this.reverse_chronological_contexts())
{
history_entries.push(HistoryEntry::Context(context));
}
history_entries.extend(
self.thread_store
.read(cx)
.reverse_chronological_threads()
.cloned()
.map(HistoryEntry::Thread),
);
history_entries.extend(
self.context_store
.read(cx)
.unordered_contexts()
.cloned()
.map(HistoryEntry::Context),
);
history_entries.sort_unstable_by_key(|entry| std::cmp::Reverse(entry.updated_at()));
history_entries
@@ -206,15 +139,62 @@ impl HistoryStore {
self.entries(cx).into_iter().take(limit).collect()
}
pub fn recently_opened_entries(&self, cx: &App) -> Vec<HistoryEntry> {
#[cfg(debug_assertions)]
if std::env::var("ZED_SIMULATE_NO_THREAD_HISTORY").is_ok() {
return Vec::new();
}
let thread_entries = self
.thread_store
.read(cx)
.reverse_chronological_threads()
.flat_map(|thread| {
self.recently_opened_entries
.iter()
.enumerate()
.flat_map(|(index, entry)| match entry {
HistoryEntryId::Thread(id) if &thread.id == id => {
Some((index, HistoryEntry::Thread(thread.clone())))
}
_ => None,
})
});
let context_entries =
self.context_store
.read(cx)
.unordered_contexts()
.flat_map(|context| {
self.recently_opened_entries
.iter()
.enumerate()
.flat_map(|(index, entry)| match entry {
HistoryEntryId::Context(path) if &context.path == path => {
Some((index, HistoryEntry::Context(context.clone())))
}
_ => None,
})
});
thread_entries
.chain(context_entries)
// optimization to halt iteration early
.take(self.recently_opened_entries.len())
.sorted_unstable_by_key(|(index, _)| *index)
.map(|(_, entry)| entry)
.collect()
}
fn save_recently_opened_entries(&mut self, cx: &mut Context<Self>) {
let serialized_entries = self
.recently_opened_entries
.iter()
.filter_map(|entry| match entry {
RecentEntry::Context(context) => Some(SerializedRecentEntry::Context(
context.read(cx).path()?.to_str()?.to_owned(),
)),
RecentEntry::Thread(id, _) => Some(SerializedRecentEntry::Thread(id.to_string())),
HistoryEntryId::Context(path) => path.file_name().map(|file| {
SerializedRecentOpen::ContextName(file.to_string_lossy().to_string())
}),
HistoryEntryId::Thread(id) => Some(SerializedRecentOpen::Thread(id.to_string())),
})
.collect::<Vec<_>>();
@@ -233,7 +213,33 @@ impl HistoryStore {
});
}
pub fn push_recently_opened_entry(&mut self, entry: RecentEntry, cx: &mut Context<Self>) {
fn load_recently_opened_entries(cx: &AsyncApp) -> Task<Result<Vec<HistoryEntryId>>> {
cx.background_spawn(async move {
let path = paths::data_dir().join(NAVIGATION_HISTORY_PATH);
let contents = smol::fs::read_to_string(path).await?;
let entries = serde_json::from_str::<Vec<SerializedRecentOpen>>(&contents)
.context("deserializing persisted agent panel navigation history")?
.into_iter()
.take(MAX_RECENTLY_OPENED_ENTRIES)
.flat_map(|entry| match entry {
SerializedRecentOpen::Thread(id) => {
Some(HistoryEntryId::Thread(id.as_str().into()))
}
SerializedRecentOpen::ContextName(file_name) => Some(HistoryEntryId::Context(
contexts_dir().join(file_name).into(),
)),
SerializedRecentOpen::Context(path) => {
Path::new(&path).file_name().map(|file_name| {
HistoryEntryId::Context(contexts_dir().join(file_name).into())
})
}
})
.collect::<Vec<_>>();
Ok(entries)
})
}
pub fn push_recently_opened_entry(&mut self, entry: HistoryEntryId, cx: &mut Context<Self>) {
self.recently_opened_entries
.retain(|old_entry| old_entry != &entry);
self.recently_opened_entries.push_front(entry);
@@ -244,24 +250,33 @@ impl HistoryStore {
pub fn remove_recently_opened_thread(&mut self, id: ThreadId, cx: &mut Context<Self>) {
self.recently_opened_entries.retain(|entry| match entry {
RecentEntry::Thread(thread_id, _) if thread_id == &id => false,
HistoryEntryId::Thread(thread_id) if thread_id == &id => false,
_ => true,
});
self.save_recently_opened_entries(cx);
}
pub fn remove_recently_opened_entry(&mut self, entry: &RecentEntry, cx: &mut Context<Self>) {
pub fn replace_recently_opened_text_thread(
&mut self,
old_path: &Path,
new_path: &Arc<Path>,
cx: &mut Context<Self>,
) {
for entry in &mut self.recently_opened_entries {
match entry {
HistoryEntryId::Context(path) if path.as_ref() == old_path => {
*entry = HistoryEntryId::Context(new_path.clone());
break;
}
_ => {}
}
}
self.save_recently_opened_entries(cx);
}
pub fn remove_recently_opened_entry(&mut self, entry: &HistoryEntryId, cx: &mut Context<Self>) {
self.recently_opened_entries
.retain(|old_entry| old_entry != entry);
self.save_recently_opened_entries(cx);
}
pub fn recently_opened_entries(&self, _cx: &mut Context<Self>) -> VecDeque<RecentEntry> {
#[cfg(debug_assertions)]
if std::env::var("ZED_SIMULATE_NO_THREAD_HISTORY").is_ok() {
return VecDeque::new();
}
self.recently_opened_entries.clone()
}
}

View File

@@ -1331,7 +1331,7 @@ impl InlineAssistant {
editor.clear_gutter_highlights::<GutterPendingRange>(cx);
} else {
editor.highlight_gutter::<GutterPendingRange>(
&gutter_pending_ranges,
gutter_pending_ranges,
|cx| cx.theme().status().info_background,
cx,
)
@@ -1342,7 +1342,7 @@ impl InlineAssistant {
editor.clear_gutter_highlights::<GutterTransformedRange>(cx);
} else {
editor.highlight_gutter::<GutterTransformedRange>(
&gutter_transformed_ranges,
gutter_transformed_ranges,
|cx| cx.theme().status().info,
cx,
)

View File

@@ -195,20 +195,20 @@ impl MessageSegment {
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct ProjectSnapshot {
pub worktree_snapshots: Vec<WorktreeSnapshot>,
pub unsaved_buffer_paths: Vec<String>,
pub timestamp: DateTime<Utc>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct WorktreeSnapshot {
pub worktree_path: String,
pub git_state: Option<GitState>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct GitState {
pub remote_url: Option<String>,
pub head_sha: Option<String>,
@@ -247,7 +247,7 @@ impl LastRestoreCheckpoint {
}
}
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)]
pub enum DetailedSummaryState {
#[default]
NotGenerated,
@@ -391,7 +391,7 @@ impl ThreadSummary {
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct ExceededWindowError {
/// Model used when last message exceeded context window
model_id: LanguageModelId,
@@ -1563,6 +1563,9 @@ impl Thread {
Err(LanguageModelCompletionError::Other(error)) => {
return Err(error);
}
Err(err @ LanguageModelCompletionError::RateLimit(..)) => {
return Err(err.into());
}
};
match event {

View File

@@ -671,7 +671,7 @@ impl RenderOnce for HistoryEntryElement {
),
HistoryEntry::Context(context) => (
context.path.to_string_lossy().to_string(),
context.title.clone().into(),
context.title.clone(),
context.mtime.timestamp(),
),
};

View File

@@ -89,7 +89,7 @@ pub fn init(cx: &mut App) {
pub struct SharedProjectContext(Rc<RefCell<Option<ProjectContext>>>);
impl SharedProjectContext {
pub fn borrow(&self) -> Ref<Option<ProjectContext>> {
pub fn borrow(&self) -> Ref<'_, Option<ProjectContext>> {
self.0.borrow()
}
}
@@ -393,16 +393,11 @@ impl ThreadStore {
self.threads.len()
}
pub fn unordered_threads(&self) -> impl Iterator<Item = &SerializedThreadMetadata> {
pub fn reverse_chronological_threads(&self) -> impl Iterator<Item = &SerializedThreadMetadata> {
// ordering is from "ORDER BY" in `list_threads`
self.threads.iter()
}
pub fn reverse_chronological_threads(&self) -> Vec<SerializedThreadMetadata> {
let mut threads = self.threads.iter().cloned().collect::<Vec<_>>();
threads.sort_unstable_by_key(|thread| std::cmp::Reverse(thread.updated_at));
threads
}
pub fn create_thread(&mut self, cx: &mut Context<Self>) -> Entity<Thread> {
cx.new(|cx| {
Thread::new(
@@ -567,7 +562,7 @@ impl ThreadStore {
if protocol.capable(context_server::protocol::ServerCapability::Tools) {
if let Some(response) = protocol
.request::<context_server::types::request::ListTools>(())
.request::<context_server::types::requests::ListTools>(())
.await
.log_err()
{
@@ -608,7 +603,7 @@ pub struct SerializedThreadMetadata {
pub updated_at: DateTime<Utc>,
}
#[derive(Serialize, Deserialize, Debug)]
#[derive(Serialize, Deserialize, Debug, PartialEq)]
pub struct SerializedThread {
pub version: String,
pub summary: SharedString,
@@ -634,7 +629,7 @@ pub struct SerializedThread {
pub profile: Option<AgentProfileId>,
}
#[derive(Serialize, Deserialize, Debug)]
#[derive(Serialize, Deserialize, Debug, PartialEq)]
pub struct SerializedLanguageModel {
pub provider: String,
pub model: String,
@@ -695,11 +690,15 @@ impl SerializedThreadV0_1_0 {
messages.push(message);
}
SerializedThread { messages, ..self.0 }
SerializedThread {
messages,
version: SerializedThread::VERSION.to_string(),
..self.0
}
}
}
#[derive(Debug, Serialize, Deserialize)]
#[derive(Debug, Serialize, Deserialize, PartialEq)]
pub struct SerializedMessage {
pub id: MessageId,
pub role: Role,
@@ -717,7 +716,7 @@ pub struct SerializedMessage {
pub is_hidden: bool,
}
#[derive(Debug, Serialize, Deserialize)]
#[derive(Debug, Serialize, Deserialize, PartialEq)]
#[serde(tag = "type")]
pub enum SerializedMessageSegment {
#[serde(rename = "text")]
@@ -735,14 +734,14 @@ pub enum SerializedMessageSegment {
},
}
#[derive(Debug, Serialize, Deserialize)]
#[derive(Debug, Serialize, Deserialize, PartialEq)]
pub struct SerializedToolUse {
pub id: LanguageModelToolUseId,
pub name: SharedString,
pub input: serde_json::Value,
}
#[derive(Debug, Serialize, Deserialize)]
#[derive(Debug, Serialize, Deserialize, PartialEq)]
pub struct SerializedToolResult {
pub tool_use_id: LanguageModelToolUseId,
pub is_error: bool,
@@ -805,7 +804,7 @@ impl LegacySerializedMessage {
}
}
#[derive(Debug, Serialize, Deserialize)]
#[derive(Debug, Serialize, Deserialize, PartialEq)]
pub struct SerializedCrease {
pub start: usize,
pub end: usize,
@@ -924,7 +923,7 @@ impl ThreadsDatabase {
fn bytes_encode(
item: &Self::EItem,
) -> Result<std::borrow::Cow<[u8]>, heed::BoxedError> {
) -> Result<std::borrow::Cow<'_, [u8]>, heed::BoxedError> {
serde_json::to_vec(&item.0)
.map(std::borrow::Cow::Owned)
.map_err(Into::into)
@@ -1062,3 +1061,181 @@ impl ThreadsDatabase {
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::thread::{DetailedSummaryState, MessageId};
use chrono::Utc;
use language_model::{Role, TokenUsage};
use pretty_assertions::assert_eq;
#[test]
fn test_legacy_serialized_thread_upgrade() {
let updated_at = Utc::now();
let legacy_thread = LegacySerializedThread {
summary: "Test conversation".into(),
updated_at,
messages: vec![LegacySerializedMessage {
id: MessageId(1),
role: Role::User,
text: "Hello, world!".to_string(),
tool_uses: vec![],
tool_results: vec![],
}],
initial_project_snapshot: None,
};
let upgraded = legacy_thread.upgrade();
assert_eq!(
upgraded,
SerializedThread {
summary: "Test conversation".into(),
updated_at,
messages: vec![SerializedMessage {
id: MessageId(1),
role: Role::User,
segments: vec![SerializedMessageSegment::Text {
text: "Hello, world!".to_string()
}],
tool_uses: vec![],
tool_results: vec![],
context: "".to_string(),
creases: vec![],
is_hidden: false
}],
version: SerializedThread::VERSION.to_string(),
initial_project_snapshot: None,
cumulative_token_usage: TokenUsage::default(),
request_token_usage: vec![],
detailed_summary_state: DetailedSummaryState::default(),
exceeded_window_error: None,
model: None,
completion_mode: None,
tool_use_limit_reached: false,
profile: None
}
)
}
#[test]
fn test_serialized_threadv0_1_0_upgrade() {
let updated_at = Utc::now();
let thread_v0_1_0 = SerializedThreadV0_1_0(SerializedThread {
summary: "Test conversation".into(),
updated_at,
messages: vec![
SerializedMessage {
id: MessageId(1),
role: Role::User,
segments: vec![SerializedMessageSegment::Text {
text: "Use tool_1".to_string(),
}],
tool_uses: vec![],
tool_results: vec![],
context: "".to_string(),
creases: vec![],
is_hidden: false,
},
SerializedMessage {
id: MessageId(2),
role: Role::Assistant,
segments: vec![SerializedMessageSegment::Text {
text: "I want to use a tool".to_string(),
}],
tool_uses: vec![SerializedToolUse {
id: "abc".into(),
name: "tool_1".into(),
input: serde_json::Value::Null,
}],
tool_results: vec![],
context: "".to_string(),
creases: vec![],
is_hidden: false,
},
SerializedMessage {
id: MessageId(1),
role: Role::User,
segments: vec![SerializedMessageSegment::Text {
text: "Here is the tool result".to_string(),
}],
tool_uses: vec![],
tool_results: vec![SerializedToolResult {
tool_use_id: "abc".into(),
is_error: false,
content: LanguageModelToolResultContent::Text("abcdef".into()),
output: Some(serde_json::Value::Null),
}],
context: "".to_string(),
creases: vec![],
is_hidden: false,
},
],
version: SerializedThreadV0_1_0::VERSION.to_string(),
initial_project_snapshot: None,
cumulative_token_usage: TokenUsage::default(),
request_token_usage: vec![],
detailed_summary_state: DetailedSummaryState::default(),
exceeded_window_error: None,
model: None,
completion_mode: None,
tool_use_limit_reached: false,
profile: None,
});
let upgraded = thread_v0_1_0.upgrade();
assert_eq!(
upgraded,
SerializedThread {
summary: "Test conversation".into(),
updated_at,
messages: vec![
SerializedMessage {
id: MessageId(1),
role: Role::User,
segments: vec![SerializedMessageSegment::Text {
text: "Use tool_1".to_string()
}],
tool_uses: vec![],
tool_results: vec![],
context: "".to_string(),
creases: vec![],
is_hidden: false
},
SerializedMessage {
id: MessageId(2),
role: Role::Assistant,
segments: vec![SerializedMessageSegment::Text {
text: "I want to use a tool".to_string(),
}],
tool_uses: vec![SerializedToolUse {
id: "abc".into(),
name: "tool_1".into(),
input: serde_json::Value::Null,
}],
tool_results: vec![SerializedToolResult {
tool_use_id: "abc".into(),
is_error: false,
content: LanguageModelToolResultContent::Text("abcdef".into()),
output: Some(serde_json::Value::Null),
}],
context: "".to_string(),
creases: vec![],
is_hidden: false,
},
],
version: SerializedThread::VERSION.to_string(),
initial_project_snapshot: None,
cumulative_token_usage: TokenUsage::default(),
request_token_usage: vec![],
detailed_summary_state: DetailedSummaryState::default(),
exceeded_window_error: None,
model: None,
completion_mode: None,
tool_use_limit_reached: false,
profile: None
}
)
}
}

View File

@@ -1,4 +1,5 @@
use std::str::FromStr;
use std::time::Duration;
use anyhow::{Context as _, Result, anyhow};
use chrono::{DateTime, Utc};
@@ -406,6 +407,7 @@ impl RateLimit {
/// <https://docs.anthropic.com/en/api/rate-limits#response-headers>
#[derive(Debug)]
pub struct RateLimitInfo {
pub retry_after: Option<Duration>,
pub requests: Option<RateLimit>,
pub tokens: Option<RateLimit>,
pub input_tokens: Option<RateLimit>,
@@ -417,10 +419,11 @@ impl RateLimitInfo {
// Check if any rate limit headers exist
let has_rate_limit_headers = headers
.keys()
.any(|k| k.as_str().starts_with("anthropic-ratelimit-"));
.any(|k| k == "retry-after" || k.as_str().starts_with("anthropic-ratelimit-"));
if !has_rate_limit_headers {
return Self {
retry_after: None,
requests: None,
tokens: None,
input_tokens: None,
@@ -429,6 +432,11 @@ impl RateLimitInfo {
}
Self {
retry_after: headers
.get("retry-after")
.and_then(|v| v.to_str().ok())
.and_then(|v| v.parse::<u64>().ok())
.map(Duration::from_secs),
requests: RateLimit::from_headers("requests", headers).ok(),
tokens: RateLimit::from_headers("tokens", headers).ok(),
input_tokens: RateLimit::from_headers("input-tokens", headers).ok(),
@@ -481,8 +489,8 @@ pub async fn stream_completion_with_rate_limit_info(
.send(request)
.await
.context("failed to send request to Anthropic")?;
let rate_limits = RateLimitInfo::from_headers(response.headers());
if response.status().is_success() {
let rate_limits = RateLimitInfo::from_headers(response.headers());
let reader = BufReader::new(response.into_body());
let stream = reader
.lines()
@@ -500,6 +508,8 @@ pub async fn stream_completion_with_rate_limit_info(
})
.boxed();
Ok((stream, Some(rate_limits)))
} else if let Some(retry_after) = rate_limits.retry_after {
Err(AnthropicError::RateLimit(retry_after))
} else {
let mut body = Vec::new();
response
@@ -769,6 +779,8 @@ pub struct MessageDelta {
#[derive(Error, Debug)]
pub enum AnthropicError {
#[error("rate limit exceeded, retry after {0:?}")]
RateLimit(Duration),
#[error("an error occurred while interacting with the Anthropic API: {error_type}: {message}", error_type = .0.error_type, message = .0.message)]
ApiError(ApiError),
#[error("{0}")]

View File

@@ -11,7 +11,7 @@ use assistant_slash_commands::FileCommandMetadata;
use client::{self, proto, telemetry::Telemetry};
use clock::ReplicaId;
use collections::{HashMap, HashSet};
use fs::{Fs, RemoveOptions};
use fs::{Fs, RenameOptions};
use futures::{FutureExt, StreamExt, future::Shared};
use gpui::{
App, AppContext as _, Context, Entity, EventEmitter, RenderImage, SharedString, Subscription,
@@ -452,6 +452,10 @@ pub enum ContextEvent {
MessagesEdited,
SummaryChanged,
SummaryGenerated,
PathChanged {
old_path: Option<Arc<Path>>,
new_path: Arc<Path>,
},
StreamedCompletion,
StartedThoughtProcess(Range<language::Anchor>),
EndedThoughtProcess(language::Anchor),
@@ -2894,22 +2898,34 @@ impl AssistantContext {
}
fs.create_dir(contexts_dir().as_ref()).await?;
fs.atomic_write(new_path.clone(), serde_json::to_string(&context).unwrap())
.await?;
if let Some(old_path) = old_path {
// rename before write ensures that only one file exists
if let Some(old_path) = old_path.as_ref() {
if new_path.as_path() != old_path.as_ref() {
fs.remove_file(
fs.rename(
&old_path,
RemoveOptions {
recursive: false,
ignore_if_not_exists: true,
&new_path,
RenameOptions {
overwrite: true,
ignore_if_exists: true,
},
)
.await?;
}
}
this.update(cx, |this, _| this.path = Some(new_path.into()))?;
// update path before write in case it fails
this.update(cx, {
let new_path: Arc<Path> = new_path.clone().into();
move |this, cx| {
this.path = Some(new_path.clone());
cx.emit(ContextEvent::PathChanged { old_path, new_path });
}
})
.ok();
fs.atomic_write(new_path, serde_json::to_string(&context).unwrap())
.await?;
}
Ok(())
@@ -3277,7 +3293,7 @@ impl SavedContextV0_1_0 {
#[derive(Debug, Clone)]
pub struct SavedContextMetadata {
pub title: String,
pub title: SharedString,
pub path: Arc<Path>,
pub mtime: chrono::DateTime<chrono::Local>,
}

View File

@@ -580,6 +580,7 @@ impl ContextEditor {
});
}
ContextEvent::SummaryGenerated => {}
ContextEvent::PathChanged { .. } => {}
ContextEvent::StartedThoughtProcess(range) => {
let creases = self.insert_thought_process_output_sections(
[(

View File

@@ -347,12 +347,6 @@ impl ContextStore {
self.contexts_metadata.iter()
}
pub fn reverse_chronological_contexts(&self) -> Vec<SavedContextMetadata> {
let mut contexts = self.contexts_metadata.iter().cloned().collect::<Vec<_>>();
contexts.sort_unstable_by_key(|thread| std::cmp::Reverse(thread.mtime));
contexts
}
pub fn create(&mut self, cx: &mut Context<Self>) -> Entity<AssistantContext> {
let context = cx.new(|cx| {
AssistantContext::local(
@@ -618,6 +612,16 @@ impl ContextStore {
ContextEvent::SummaryChanged => {
self.advertise_contexts(cx);
}
ContextEvent::PathChanged { old_path, new_path } => {
if let Some(old_path) = old_path.as_ref() {
for metadata in &mut self.contexts_metadata {
if &metadata.path == old_path {
metadata.path = new_path.clone();
break;
}
}
}
}
ContextEvent::Operation(operation) => {
let context_id = context.read(cx).id().to_proto();
let operation = operation.to_proto();
@@ -792,7 +796,7 @@ impl ContextStore {
.next()
{
contexts.push(SavedContextMetadata {
title: title.to_string(),
title: title.to_string().into(),
path: path.into(),
mtime: metadata.mtime.timestamp_for_user().into(),
});
@@ -865,7 +869,7 @@ impl ContextStore {
if protocol.capable(context_server::protocol::ServerCapability::Prompts) {
if let Some(response) = protocol
.request::<context_server::types::request::PromptsList>(())
.request::<context_server::types::requests::PromptsList>(())
.await
.log_err()
{

View File

@@ -682,11 +682,12 @@ mod tests {
_: &AsyncApp,
) -> BoxFuture<
'static,
http_client::Result<
Result<
BoxStream<
'static,
http_client::Result<LanguageModelCompletionEvent, LanguageModelCompletionError>,
Result<LanguageModelCompletionEvent, LanguageModelCompletionError>,
>,
LanguageModelCompletionError,
>,
> {
unimplemented!()

View File

@@ -87,7 +87,7 @@ impl SlashCommand for ContextServerSlashCommand {
let protocol = server.client().context("Context server not initialized")?;
let response = protocol
.request::<context_server::types::request::CompletionComplete>(
.request::<context_server::types::requests::CompletionComplete>(
context_server::types::CompletionCompleteParams {
reference: context_server::types::CompletionReference::Prompt(
context_server::types::PromptReference {
@@ -145,7 +145,7 @@ impl SlashCommand for ContextServerSlashCommand {
cx.foreground_executor().spawn(async move {
let protocol = server.client().context("Context server not initialized")?;
let response = protocol
.request::<context_server::types::request::PromptsGet>(
.request::<context_server::types::requests::PromptsGet>(
context_server::types::PromptsGetParams {
name: prompt_name.clone(),
arguments: Some(prompt_args),

View File

@@ -46,15 +46,19 @@ fn adapt_to_json_schema_subset(json: &mut Value) -> Result<()> {
);
}
const KEYS_TO_REMOVE: [&str; 5] = [
"format",
"additionalProperties",
"exclusiveMinimum",
"exclusiveMaximum",
"optional",
const KEYS_TO_REMOVE: [(&str, fn(&Value) -> bool); 5] = [
("format", |value| value.is_string()),
("additionalProperties", |value| value.is_boolean()),
("exclusiveMinimum", |value| value.is_number()),
("exclusiveMaximum", |value| value.is_number()),
("optional", |value| value.is_boolean()),
];
for key in KEYS_TO_REMOVE {
obj.remove(key);
for (key, predicate) in KEYS_TO_REMOVE {
if let Some(value) = obj.get(key) {
if predicate(value) {
obj.remove(key);
}
}
}
// If a type is not specified for an input parameter, add a default type
@@ -153,6 +157,24 @@ mod tests {
"type": "integer"
})
);
// Ensure that we do not remove keys that are actually supported (e.g. "format" can just be used as another property)
let mut json = json!({
"description": "A test field",
"type": "integer",
"format": {},
});
adapt_to_json_schema_subset(&mut json).unwrap();
assert_eq!(
json,
json!({
"description": "A test field",
"type": "integer",
"format": {},
})
);
}
#[test]

View File

@@ -80,6 +80,7 @@ rand.workspace = true
pretty_assertions.workspace = true
reqwest_client.workspace = true
settings = { workspace = true, features = ["test-support"] }
smol.workspace = true
task = { workspace = true, features = ["test-support"]}
tempfile.workspace = true
theme.workspace = true

View File

@@ -11,7 +11,7 @@ use client::{Client, UserStore};
use collections::HashMap;
use fs::FakeFs;
use futures::{FutureExt, future::LocalBoxFuture};
use gpui::{AppContext, TestAppContext};
use gpui::{AppContext, TestAppContext, Timer};
use indoc::{formatdoc, indoc};
use language_model::{
LanguageModelRegistry, LanguageModelRequestTool, LanguageModelToolResult,
@@ -1255,9 +1255,12 @@ impl EvalAssertion {
}],
..Default::default()
};
let mut response = judge
.stream_completion_text(request, &cx.to_async())
.await?;
let mut response = retry_on_rate_limit(async || {
Ok(judge
.stream_completion_text(request.clone(), &cx.to_async())
.await?)
})
.await?;
let mut output = String::new();
while let Some(chunk) = response.stream.next().await {
let chunk = chunk?;
@@ -1308,10 +1311,17 @@ fn eval(
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();
executor.spawn(async move { run_eval(eval, tx) }).detach();
let semaphore = semaphore.clone();
executor
.spawn(async move {
let _guard = semaphore.acquire().await;
run_eval(eval, tx)
})
.detach();
}
drop(tx);
@@ -1577,21 +1587,31 @@ impl EditAgentTest {
if let Some(input_content) = eval.input_content.as_deref() {
buffer.update(cx, |buffer, cx| buffer.set_text(input_content, cx));
}
let (edit_output, _) = self.agent.edit(
buffer.clone(),
eval.edit_file_input.display_description,
&conversation,
&mut cx.to_async(),
);
edit_output.await?
retry_on_rate_limit(async || {
self.agent
.edit(
buffer.clone(),
eval.edit_file_input.display_description.clone(),
&conversation,
&mut cx.to_async(),
)
.0
.await
})
.await?
} else {
let (edit_output, _) = self.agent.overwrite(
buffer.clone(),
eval.edit_file_input.display_description,
&conversation,
&mut cx.to_async(),
);
edit_output.await?
retry_on_rate_limit(async || {
self.agent
.overwrite(
buffer.clone(),
eval.edit_file_input.display_description.clone(),
&conversation,
&mut cx.to_async(),
)
.0
.await
})
.await?
};
let buffer_text = buffer.read_with(cx, |buffer, _| buffer.text());
@@ -1613,6 +1633,26 @@ impl EditAgentTest {
}
}
async fn retry_on_rate_limit<R>(mut request: impl AsyncFnMut() -> Result<R>) -> Result<R> {
loop {
match request().await {
Ok(result) => return Ok(result),
Err(err) => match err.downcast::<LanguageModelCompletionError>() {
Ok(err) => match err {
LanguageModelCompletionError::RateLimit(duration) => {
// Wait until after we are allowed to try again
eprintln!("Rate limit exceeded. Waiting for {duration:?}...",);
Timer::after(duration).await;
continue;
}
_ => return Err(err.into()),
},
Err(err) => return Err(err),
},
}
}
}
#[derive(Clone, Debug, Eq, PartialEq, Hash)]
struct EvalAssertionOutcome {
score: usize,

View File

@@ -638,29 +638,36 @@ impl ToolCard for TerminalToolCard {
.bg(cx.theme().colors().editor_background)
.rounded_b_md()
.text_ui_sm(cx)
.child(
ToolOutputPreview::new(
terminal.clone().into_any_element(),
terminal.entity_id(),
)
.with_total_lines(self.content_line_count)
.toggle_state(!terminal.read(cx).is_content_limited(window))
.on_toggle({
let terminal = terminal.clone();
move |is_expanded, _, cx| {
terminal.update(cx, |terminal, cx| {
terminal.set_embedded_mode(
if is_expanded {
None
} else {
Some(COLLAPSED_LINES)
},
cx,
);
});
}
}),
),
.child({
let content_mode = terminal.read(cx).content_mode(window, cx);
if content_mode.is_scrollable() {
div().h_72().child(terminal.clone()).into_any_element()
} else {
ToolOutputPreview::new(
terminal.clone().into_any_element(),
terminal.entity_id(),
)
.with_total_lines(self.content_line_count)
.toggle_state(!content_mode.is_limited())
.on_toggle({
let terminal = terminal.clone();
move |is_expanded, _, cx| {
terminal.update(cx, |terminal, cx| {
terminal.set_embedded_mode(
if is_expanded {
None
} else {
Some(COLLAPSED_LINES)
},
cx,
);
});
}
})
.into_any_element()
}
}),
)
},
)

View File

@@ -452,6 +452,10 @@ impl Model {
| Model::Claude3_5SonnetV2
| Model::Claude3_7Sonnet
| Model::Claude3_7SonnetThinking
| Model::ClaudeSonnet4
| Model::ClaudeSonnet4Thinking
| Model::ClaudeOpus4
| Model::ClaudeOpus4Thinking
| Model::Claude3Haiku
| Model::Claude3Opus
| Model::Claude3Sonnet

View File

@@ -111,7 +111,7 @@ pub struct ChannelMembership {
pub role: proto::ChannelRole,
}
impl ChannelMembership {
pub fn sort_key(&self) -> MembershipSortKey {
pub fn sort_key(&self) -> MembershipSortKey<'_> {
MembershipSortKey {
role_order: match self.role {
proto::ChannelRole::Admin => 0,

View File

@@ -32,7 +32,7 @@ impl ChannelIndex {
.retain(|channel_id| !channels.contains(channel_id));
}
pub fn bulk_insert(&mut self) -> ChannelPathsInsertGuard {
pub fn bulk_insert(&mut self) -> ChannelPathsInsertGuard<'_> {
ChannelPathsInsertGuard {
channels_ordered: &mut self.channels_ordered,
channels_by_id: &mut self.channels_by_id,

View File

@@ -39,7 +39,7 @@ enum ProxyType<'t> {
HttpProxy(HttpProxyType<'t>),
}
fn parse_proxy_type(proxy: &Url) -> Option<((String, u16), ProxyType)> {
fn parse_proxy_type(proxy: &Url) -> Option<((String, u16), ProxyType<'_>)> {
let scheme = proxy.scheme();
let host = proxy.host()?.to_string();
let port = proxy.port_or_known_default()?;

View File

@@ -501,8 +501,10 @@ impl Database {
/// Returns all channels for the user with the given ID.
pub async fn get_channels_for_user(&self, user_id: UserId) -> Result<ChannelsForUser> {
self.transaction(|tx| async move { self.get_user_channels(user_id, None, true, &tx).await })
.await
self.weak_transaction(
|tx| async move { self.get_user_channels(user_id, None, true, &tx).await },
)
.await
}
/// Returns all channels for the user with the given ID that are descendants

View File

@@ -15,7 +15,7 @@ impl Database {
user_b_busy: bool,
}
self.transaction(|tx| async move {
self.weak_transaction(|tx| async move {
let user_a_participant = Alias::new("user_a_participant");
let user_b_participant = Alias::new("user_b_participant");
let mut db_contacts = contact::Entity::find()
@@ -91,7 +91,7 @@ impl Database {
/// Returns whether the given user is a busy (on a call).
pub async fn is_user_busy(&self, user_id: UserId) -> Result<bool> {
self.transaction(|tx| async move {
self.weak_transaction(|tx| async move {
let participant = room_participant::Entity::find()
.filter(room_participant::Column::UserId.eq(user_id))
.one(&*tx)

View File

@@ -80,7 +80,7 @@ impl Database {
&self,
user_id: UserId,
) -> Result<Option<proto::IncomingCall>> {
self.transaction(|tx| async move {
self.weak_transaction(|tx| async move {
let pending_participant = room_participant::Entity::find()
.filter(
room_participant::Column::UserId

View File

@@ -7,6 +7,12 @@ pub use token::*;
pub const AGENT_EXTENDED_TRIAL_FEATURE_FLAG: &str = "agent-extended-trial";
/// The name of the feature flag that bypasses the account age check.
pub const BYPASS_ACCOUNT_AGE_CHECK_FEATURE_FLAG: &str = "bypass-account-age-check";
/// The minimum account age an account must have in order to use the LLM service.
pub const MIN_ACCOUNT_AGE_FOR_LLM_USE: chrono::Duration = chrono::Duration::days(30);
/// The default value to use for maximum spend per month if the user did not
/// explicitly set a maximum spend.
///

View File

@@ -1,6 +1,6 @@
use crate::db::billing_subscription::SubscriptionKind;
use crate::db::{billing_customer, billing_subscription, user};
use crate::llm::AGENT_EXTENDED_TRIAL_FEATURE_FLAG;
use crate::llm::{AGENT_EXTENDED_TRIAL_FEATURE_FLAG, BYPASS_ACCOUNT_AGE_CHECK_FEATURE_FLAG};
use crate::{Config, db::billing_preference};
use anyhow::{Context as _, Result};
use chrono::{NaiveDateTime, Utc};
@@ -84,7 +84,7 @@ impl LlmTokenClaims {
.any(|flag| flag == "llm-closed-beta"),
bypass_account_age_check: feature_flags
.iter()
.any(|flag| flag == "bypass-account-age-check"),
.any(|flag| flag == BYPASS_ACCOUNT_AGE_CHECK_FEATURE_FLAG),
can_use_web_search_tool: true,
use_llm_request_queue: feature_flags.iter().any(|flag| flag == "llm-request-queue"),
plan,

View File

@@ -4,7 +4,10 @@ use crate::api::billing::find_or_create_billing_customer;
use crate::api::{CloudflareIpCountryHeader, SystemIdHeader};
use crate::db::billing_subscription::SubscriptionKind;
use crate::llm::db::LlmDatabase;
use crate::llm::{AGENT_EXTENDED_TRIAL_FEATURE_FLAG, LlmTokenClaims};
use crate::llm::{
AGENT_EXTENDED_TRIAL_FEATURE_FLAG, BYPASS_ACCOUNT_AGE_CHECK_FEATURE_FLAG, LlmTokenClaims,
MIN_ACCOUNT_AGE_FOR_LLM_USE,
};
use crate::stripe_client::StripeCustomerId;
use crate::{
AppState, Error, Result, auth,
@@ -65,7 +68,7 @@ use std::{
rc::Rc,
sync::{
Arc, OnceLock,
atomic::{AtomicBool, Ordering::SeqCst},
atomic::{AtomicBool, AtomicUsize, Ordering::SeqCst},
},
time::{Duration, Instant},
};
@@ -86,10 +89,36 @@ pub const CLEANUP_TIMEOUT: Duration = Duration::from_secs(15);
const MESSAGE_COUNT_PER_PAGE: usize = 100;
const MAX_MESSAGE_LEN: usize = 1024;
const NOTIFICATION_COUNT_PER_PAGE: usize = 50;
const MAX_CONCURRENT_CONNECTIONS: usize = 512;
static CONCURRENT_CONNECTIONS: AtomicUsize = AtomicUsize::new(0);
type MessageHandler =
Box<dyn Send + Sync + Fn(Box<dyn AnyTypedEnvelope>, Session) -> BoxFuture<'static, ()>>;
pub struct ConnectionGuard;
impl ConnectionGuard {
pub fn try_acquire() -> Result<Self, ()> {
let current_connections = CONCURRENT_CONNECTIONS.fetch_add(1, SeqCst);
if current_connections >= MAX_CONCURRENT_CONNECTIONS {
CONCURRENT_CONNECTIONS.fetch_sub(1, SeqCst);
tracing::error!(
"too many concurrent connections: {}",
current_connections + 1
);
return Err(());
}
Ok(ConnectionGuard)
}
}
impl Drop for ConnectionGuard {
fn drop(&mut self) {
CONCURRENT_CONNECTIONS.fetch_sub(1, SeqCst);
}
}
struct Response<R> {
peer: Arc<Peer>,
receipt: Receipt<R>,
@@ -722,6 +751,7 @@ impl Server {
system_id: Option<String>,
send_connection_id: Option<oneshot::Sender<ConnectionId>>,
executor: Executor,
connection_guard: Option<ConnectionGuard>,
) -> impl Future<Output = ()> + use<> {
let this = self.clone();
let span = info_span!("handle connection", %address,
@@ -742,6 +772,7 @@ impl Server {
tracing::error!("server is tearing down");
return
}
let (connection_id, handle_io, mut incoming_rx) = this
.peer
.add_connection(connection, {
@@ -783,6 +814,7 @@ impl Server {
tracing::error!(?error, "failed to send initial client update");
return;
}
drop(connection_guard);
let handle_io = handle_io.fuse();
futures::pin_mut!(handle_io);
@@ -1154,6 +1186,19 @@ pub async fn handle_websocket_request(
}
let socket_address = socket_address.to_string();
// Acquire connection guard before WebSocket upgrade
let connection_guard = match ConnectionGuard::try_acquire() {
Ok(guard) => guard,
Err(()) => {
return (
StatusCode::SERVICE_UNAVAILABLE,
"Too many concurrent connections",
)
.into_response();
}
};
ws.on_upgrade(move |socket| {
let socket = socket
.map_ok(to_tungstenite_message)
@@ -1171,6 +1216,7 @@ pub async fn handle_websocket_request(
system_id_header.map(|header| header.to_string()),
None,
Executor::Production,
Some(connection_guard),
)
.await;
}
@@ -2773,8 +2819,12 @@ async fn make_update_user_plan_message(
(None, None)
};
let account_too_young =
!matches!(plan, proto::Plan::ZedPro) && user.account_age() < MIN_ACCOUNT_AGE_FOR_LLM_USE;
let bypass_account_age_check = feature_flags
.iter()
.any(|flag| flag == BYPASS_ACCOUNT_AGE_CHECK_FEATURE_FLAG);
let account_too_young = !matches!(plan, proto::Plan::ZedPro)
&& !bypass_account_age_check
&& user.account_age() < MIN_ACCOUNT_AGE_FOR_LLM_USE;
Ok(proto::UpdateUserPlan {
plan: plan.into(),
@@ -4075,9 +4125,6 @@ async fn accept_terms_of_service(
Ok(())
}
/// The minimum account age an account must have in order to use the LLM service.
pub const MIN_ACCOUNT_AGE_FOR_LLM_USE: chrono::Duration = chrono::Duration::days(30);
async fn get_llm_api_token(
_request: proto::GetLlmToken,
response: Response<proto::GetLlmToken>,

View File

@@ -258,6 +258,7 @@ impl TestServer {
None,
Some(connection_id_tx),
Executor::Deterministic(cx.background_executor().clone()),
None,
))
.detach();
let connection_id = connection_id_rx.await.map_err(|e| {

View File

@@ -8,7 +8,7 @@
use anyhow::Result;
use crate::client::Client;
use crate::types::{self, Request};
use crate::types::{self, Notification, Request};
pub struct ModelContextProtocol {
inner: Client,
@@ -20,9 +20,10 @@ impl ModelContextProtocol {
}
fn supported_protocols() -> Vec<types::ProtocolVersion> {
vec![types::ProtocolVersion(
types::LATEST_PROTOCOL_VERSION.to_string(),
)]
vec![
types::ProtocolVersion(types::LATEST_PROTOCOL_VERSION.to_string()),
types::ProtocolVersion(types::VERSION_2024_11_05.to_string()),
]
}
pub async fn initialize(
@@ -42,7 +43,7 @@ impl ModelContextProtocol {
let response: types::InitializeResponse = self
.inner
.request(types::request::Initialize::METHOD, params)
.request(types::requests::Initialize::METHOD, params)
.await?;
anyhow::ensure!(
@@ -53,16 +54,13 @@ impl ModelContextProtocol {
log::trace!("mcp server info {:?}", response.server_info);
self.inner.notify(
types::NotificationType::Initialized.as_str(),
serde_json::json!({}),
)?;
let initialized_protocol = InitializedContextServerProtocol {
inner: self.inner,
initialize: response,
};
initialized_protocol.notify::<types::notifications::Initialized>(())?;
Ok(initialized_protocol)
}
}
@@ -96,4 +94,8 @@ impl InitializedContextServerProtocol {
pub async fn request<T: Request>(&self, params: T::Params) -> Result<T::Response> {
self.inner.request(T::METHOD, params).await
}
pub fn notify<T: Notification>(&self, params: T::Params) -> Result<()> {
self.inner.notify(T::METHOD, params)
}
}

View File

@@ -14,7 +14,7 @@ pub fn create_fake_transport(
executor: BackgroundExecutor,
) -> FakeTransport {
let name = name.into();
FakeTransport::new(executor).on_request::<crate::types::request::Initialize>(move |_params| {
FakeTransport::new(executor).on_request::<crate::types::requests::Initialize>(move |_params| {
create_initialize_response(name.clone())
})
}

View File

@@ -3,9 +3,10 @@ use serde::de::DeserializeOwned;
use serde::{Deserialize, Serialize};
use url::Url;
pub const LATEST_PROTOCOL_VERSION: &str = "2024-11-05";
pub const LATEST_PROTOCOL_VERSION: &str = "2025-03-26";
pub const VERSION_2024_11_05: &str = "2024-11-05";
pub mod request {
pub mod requests {
use super::*;
macro_rules! request {
@@ -82,6 +83,57 @@ pub trait Request {
const METHOD: &'static str;
}
pub mod notifications {
use super::*;
macro_rules! notification {
($method:expr, $name:ident, $params:ty) => {
pub struct $name;
impl Notification for $name {
type Params = $params;
const METHOD: &'static str = $method;
}
};
}
notification!("notifications/initialized", Initialized, ());
notification!("notifications/progress", Progress, ProgressParams);
notification!("notifications/message", Message, MessageParams);
notification!(
"notifications/resources/updated",
ResourcesUpdated,
ResourcesUpdatedParams
);
notification!(
"notifications/resources/list_changed",
ResourcesListChanged,
()
);
notification!("notifications/tools/list_changed", ToolsListChanged, ());
notification!("notifications/prompts/list_changed", PromptsListChanged, ());
notification!("notifications/roots/list_changed", RootsListChanged, ());
}
pub trait Notification {
type Params: DeserializeOwned + Serialize + Send + Sync + 'static;
const METHOD: &'static str;
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct MessageParams {
pub level: LoggingLevel,
pub logger: Option<String>,
pub data: serde_json::Value,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ResourcesUpdatedParams {
pub uri: String,
}
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(transparent)]
pub struct ProtocolVersion(pub String);
@@ -291,13 +343,20 @@ pub enum MessageContent {
#[serde(skip_serializing_if = "Option::is_none")]
annotations: Option<MessageAnnotations>,
},
#[serde(rename = "image")]
#[serde(rename = "image", rename_all = "camelCase")]
Image {
data: String,
mime_type: String,
#[serde(skip_serializing_if = "Option::is_none")]
annotations: Option<MessageAnnotations>,
},
#[serde(rename = "audio", rename_all = "camelCase")]
Audio {
data: String,
mime_type: String,
#[serde(skip_serializing_if = "Option::is_none")]
annotations: Option<MessageAnnotations>,
},
#[serde(rename = "resource")]
Resource {
resource: ResourceContents,
@@ -394,6 +453,8 @@ pub struct ServerCapabilities {
#[serde(skip_serializing_if = "Option::is_none")]
pub logging: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub completions: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub prompts: Option<PromptsCapabilities>,
#[serde(skip_serializing_if = "Option::is_none")]
pub resources: Option<ResourcesCapabilities>,
@@ -438,6 +499,28 @@ pub struct Tool {
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
pub input_schema: serde_json::Value,
#[serde(skip_serializing_if = "Option::is_none")]
pub annotations: Option<ToolAnnotations>,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ToolAnnotations {
/// A human-readable title for the tool.
#[serde(skip_serializing_if = "Option::is_none")]
pub title: Option<String>,
/// If true, the tool does not modify its environment.
#[serde(skip_serializing_if = "Option::is_none")]
pub read_only_hint: Option<bool>,
/// If true, the tool may perform destructive updates to its environment.
#[serde(skip_serializing_if = "Option::is_none")]
pub destructive_hint: Option<bool>,
/// If true, calling the tool repeatedly with the same arguments will have no additional effect on its environment.
#[serde(skip_serializing_if = "Option::is_none")]
pub idempotent_hint: Option<bool>,
/// If true, this tool may interact with an "open world" of external entities.
#[serde(skip_serializing_if = "Option::is_none")]
pub open_world_hint: Option<bool>,
}
#[derive(Debug, Serialize, Deserialize)]
@@ -528,34 +611,6 @@ pub struct ModelHint {
pub name: Option<String>,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum NotificationType {
Initialized,
Progress,
Message,
ResourcesUpdated,
ResourcesListChanged,
ToolsListChanged,
PromptsListChanged,
RootsListChanged,
}
impl NotificationType {
pub fn as_str(&self) -> &'static str {
match self {
NotificationType::Initialized => "notifications/initialized",
NotificationType::Progress => "notifications/progress",
NotificationType::Message => "notifications/message",
NotificationType::ResourcesUpdated => "notifications/resources/updated",
NotificationType::ResourcesListChanged => "notifications/resources/list_changed",
NotificationType::ToolsListChanged => "notifications/tools/list_changed",
NotificationType::PromptsListChanged => "notifications/prompts/list_changed",
NotificationType::RootsListChanged => "notifications/roots/list_changed",
}
}
}
#[derive(Debug, Serialize)]
#[serde(untagged)]
pub enum ClientNotification {
@@ -576,12 +631,14 @@ pub enum ProgressToken {
Number(f64),
}
#[derive(Debug, Serialize)]
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ProgressParams {
pub progress_token: ProgressToken,
pub progress: f64,
#[serde(skip_serializing_if = "Option::is_none")]
pub message: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub total: Option<f64>,
#[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
pub meta: Option<HashMap<String, serde_json::Value>>,
@@ -625,6 +682,8 @@ pub enum ToolResponseContent {
Text { text: String },
#[serde(rename = "image", rename_all = "camelCase")]
Image { data: String, mime_type: String },
#[serde(rename = "audio", rename_all = "camelCase")]
Audio { data: String, mime_type: String },
#[serde(rename = "resource")]
Resource { resource: ResourceContents },
}

View File

@@ -408,24 +408,30 @@ impl Copilot {
let proxy_url = copilot_settings.proxy.clone()?;
let no_verify = copilot_settings.proxy_no_verify;
let http_or_https_proxy = if proxy_url.starts_with("http:") {
"HTTP_PROXY"
Some("HTTP_PROXY")
} else if proxy_url.starts_with("https:") {
"HTTPS_PROXY"
Some("HTTPS_PROXY")
} else {
log::error!(
"Unsupported protocol scheme for language server proxy (must be http or https)"
);
return None;
None
};
let mut env = HashMap::default();
env.insert(http_or_https_proxy.to_string(), proxy_url);
if let Some(true) = no_verify {
env.insert("NODE_TLS_REJECT_UNAUTHORIZED".to_string(), "0".to_string());
};
if let Some(proxy_type) = http_or_https_proxy {
env.insert(proxy_type.to_string(), proxy_url);
if let Some(true) = no_verify {
env.insert("NODE_TLS_REJECT_UNAUTHORIZED".to_string(), "0".to_string());
};
}
Some(env)
if let Ok(oauth_token) = env::var(copilot_chat::COPILOT_OAUTH_ENV_VAR) {
env.insert(copilot_chat::COPILOT_OAUTH_ENV_VAR.to_string(), oauth_token);
}
if env.is_empty() { None } else { Some(env) }
}
#[cfg(any(test, feature = "test-support"))]

View File

@@ -16,6 +16,8 @@ use paths::home_dir;
use serde::{Deserialize, Serialize};
use settings::watch_config_dir;
pub const COPILOT_OAUTH_ENV_VAR: &str = "GH_COPILOT_TOKEN";
#[derive(Default, Clone, Debug, PartialEq)]
pub struct CopilotChatSettings {
pub api_url: Arc<str>,
@@ -405,13 +407,19 @@ impl CopilotChat {
})
.detach_and_log_err(cx);
Self {
oauth_token: None,
let this = Self {
oauth_token: std::env::var(COPILOT_OAUTH_ENV_VAR).ok(),
api_token: None,
models: None,
settings,
client,
};
if this.oauth_token.is_some() {
cx.spawn(async move |this, mut cx| Self::update_models(&this, &mut cx).await)
.detach_and_log_err(cx);
}
this
}
async fn update_models(this: &WeakEntity<Self>, cx: &mut AsyncApp) -> Result<()> {

View File

@@ -23,6 +23,7 @@ doctest = false
[dependencies]
anyhow.workspace = true
async-trait.workspace = true
collections.workspace = true
dap.workspace = true
futures.workspace = true
gpui.workspace = true

View File

@@ -1,16 +1,18 @@
use anyhow::Result;
use anyhow::{Result, bail};
use async_trait::async_trait;
use collections::FxHashMap;
use dap::{
DebugRequest, StartDebuggingRequestArguments,
DebugRequest, StartDebuggingRequestArguments, StartDebuggingRequestArgumentsRequest,
adapters::{
DapDelegate, DebugAdapter, DebugAdapterBinary, DebugAdapterName, DebugTaskDefinition,
},
};
use gpui::{AsyncApp, SharedString};
use language::LanguageName;
use serde::{Deserialize, Serialize};
use serde_json::json;
use std::path::PathBuf;
use std::sync::Arc;
use std::{ffi::OsStr, sync::Arc};
use task::{DebugScenario, ZedDebugConfig};
use util::command::new_smol_command;
@@ -21,6 +23,18 @@ impl RubyDebugAdapter {
const ADAPTER_NAME: &'static str = "Ruby";
}
#[derive(Serialize, Deserialize)]
struct RubyDebugConfig {
script_or_command: Option<String>,
script: Option<String>,
command: Option<String>,
#[serde(default)]
args: Vec<String>,
#[serde(default)]
env: FxHashMap<String, String>,
cwd: Option<PathBuf>,
}
#[async_trait(?Send)]
impl DebugAdapter for RubyDebugAdapter {
fn name(&self) -> DebugAdapterName {
@@ -31,185 +45,70 @@ impl DebugAdapter for RubyDebugAdapter {
Some(SharedString::new_static("Ruby").into())
}
fn request_kind(&self, _: &serde_json::Value) -> Result<StartDebuggingRequestArgumentsRequest> {
Ok(StartDebuggingRequestArgumentsRequest::Launch)
}
async fn dap_schema(&self) -> serde_json::Value {
json!({
"oneOf": [
{
"allOf": [
{
"type": "object",
"required": ["request"],
"properties": {
"request": {
"type": "string",
"enum": ["launch"],
"description": "Request to launch a new process"
}
}
},
{
"type": "object",
"required": ["script"],
"properties": {
"command": {
"type": "string",
"description": "Command name (ruby, rake, bin/rails, bundle exec ruby, etc)",
"default": "ruby"
},
"script": {
"type": "string",
"description": "Absolute path to a Ruby file."
},
"cwd": {
"type": "string",
"description": "Directory to execute the program in",
"default": "${ZED_WORKTREE_ROOT}"
},
"args": {
"type": "array",
"description": "Command line arguments passed to the program",
"items": {
"type": "string"
},
"default": []
},
"env": {
"type": "object",
"description": "Additional environment variables to pass to the debugging (and debugged) process",
"default": {}
},
"showProtocolLog": {
"type": "boolean",
"description": "Show a log of DAP requests, events, and responses",
"default": false
},
"useBundler": {
"type": "boolean",
"description": "Execute Ruby programs with `bundle exec` instead of directly",
"default": false
},
"bundlePath": {
"type": "string",
"description": "Location of the bundle executable"
},
"rdbgPath": {
"type": "string",
"description": "Location of the rdbg executable"
},
"askParameters": {
"type": "boolean",
"description": "Ask parameters at first."
},
"debugPort": {
"type": "string",
"description": "UNIX domain socket name or TPC/IP host:port"
},
"waitLaunchTime": {
"type": "number",
"description": "Wait time before connection in milliseconds"
},
"localfs": {
"type": "boolean",
"description": "true if the VSCode and debugger run on a same machine",
"default": false
},
"useTerminal": {
"type": "boolean",
"description": "Create a new terminal and then execute commands there",
"default": false
}
}
}
]
"type": "object",
"properties": {
"command": {
"type": "string",
"description": "Command name (ruby, rake, bin/rails, bundle exec ruby, etc)",
},
{
"allOf": [
{
"type": "object",
"required": ["request"],
"properties": {
"request": {
"type": "string",
"enum": ["attach"],
"description": "Request to attach to an existing process"
}
}
},
{
"type": "object",
"properties": {
"rdbgPath": {
"type": "string",
"description": "Location of the rdbg executable"
},
"debugPort": {
"type": "string",
"description": "UNIX domain socket name or TPC/IP host:port"
},
"showProtocolLog": {
"type": "boolean",
"description": "Show a log of DAP requests, events, and responses",
"default": false
},
"localfs": {
"type": "boolean",
"description": "true if the VSCode and debugger run on a same machine",
"default": false
},
"localfsMap": {
"type": "string",
"description": "Specify pairs of remote root path and local root path like `/remote_dir:/local_dir`. You can specify multiple pairs like `/rem1:/loc1,/rem2:/loc2` by concatenating with `,`."
},
"env": {
"type": "object",
"description": "Additional environment variables to pass to the rdbg process",
"default": {}
}
}
}
]
}
]
"script": {
"type": "string",
"description": "Absolute path to a Ruby file."
},
"cwd": {
"type": "string",
"description": "Directory to execute the program in",
"default": "${ZED_WORKTREE_ROOT}"
},
"args": {
"type": "array",
"description": "Command line arguments passed to the program",
"items": {
"type": "string"
},
"default": []
},
"env": {
"type": "object",
"description": "Additional environment variables to pass to the debugging (and debugged) process",
"default": {}
},
}
})
}
fn config_from_zed_format(&self, zed_scenario: ZedDebugConfig) -> Result<DebugScenario> {
let mut config = serde_json::Map::new();
match &zed_scenario.request {
match zed_scenario.request {
DebugRequest::Launch(launch) => {
config.insert("request".to_string(), json!("launch"));
config.insert("script".to_string(), json!(launch.program));
config.insert("command".to_string(), json!("ruby"));
let config = RubyDebugConfig {
script_or_command: Some(launch.program),
script: None,
command: None,
args: launch.args,
env: launch.env,
cwd: launch.cwd.clone(),
};
if !launch.args.is_empty() {
config.insert("args".to_string(), json!(launch.args));
}
let config = serde_json::to_value(config)?;
if !launch.env.is_empty() {
config.insert("env".to_string(), json!(launch.env));
}
if let Some(cwd) = &launch.cwd {
config.insert("cwd".to_string(), json!(cwd));
}
// Ruby stops on entry so there's no need to handle that case
Ok(DebugScenario {
adapter: zed_scenario.adapter,
label: zed_scenario.label,
config,
tcp_connection: None,
build: None,
})
}
DebugRequest::Attach(attach) => {
config.insert("request".to_string(), json!("attach"));
config.insert("processId".to_string(), json!(attach.process_id));
DebugRequest::Attach(_) => {
anyhow::bail!("Attach requests are unsupported");
}
}
Ok(DebugScenario {
adapter: zed_scenario.adapter,
label: zed_scenario.label,
config: serde_json::Value::Object(config),
tcp_connection: None,
build: None,
})
}
async fn get_binary(
@@ -247,13 +146,34 @@ impl DebugAdapter for RubyDebugAdapter {
let tcp_connection = definition.tcp_connection.clone().unwrap_or_default();
let (host, port, timeout) = crate::configure_tcp_connection(tcp_connection).await?;
let ruby_config = serde_json::from_value::<RubyDebugConfig>(definition.config.clone())?;
let arguments = vec![
let mut arguments = vec![
"--open".to_string(),
format!("--port={}", port),
format!("--host={}", host),
];
if let Some(script) = &ruby_config.script {
arguments.push(script.clone());
} else if let Some(command) = &ruby_config.command {
arguments.push("--command".to_string());
arguments.push(command.clone());
} else if let Some(command_or_script) = &ruby_config.script_or_command {
if delegate
.which(OsStr::new(&command_or_script))
.await
.is_some()
{
arguments.push("--command".to_string());
}
arguments.push(command_or_script.clone());
} else {
bail!("Ruby debug config must have 'script' or 'command' args");
}
arguments.extend(ruby_config.args);
Ok(DebugAdapterBinary {
command: rdbg_path.to_string_lossy().to_string(),
arguments,
@@ -262,8 +182,12 @@ impl DebugAdapter for RubyDebugAdapter {
port,
timeout,
}),
cwd: None,
envs: std::collections::HashMap::default(),
cwd: Some(
ruby_config
.cwd
.unwrap_or(delegate.worktree_root_path().to_owned()),
),
envs: ruby_config.env.into_iter().collect(),
request_args: StartDebuggingRequestArguments {
request: self.request_kind(&definition.config)?,
configuration: definition.config.clone(),

View File

@@ -5,8 +5,8 @@ use std::time::Duration;
use anyhow::{Context as _, Result, anyhow};
use dap::StackFrameId;
use gpui::{
AnyElement, Entity, EventEmitter, FocusHandle, Focusable, MouseButton, ScrollStrategy,
Stateful, Subscription, Task, UniformListScrollHandle, WeakEntity, uniform_list,
AnyElement, Entity, EventEmitter, FocusHandle, Focusable, ListState, MouseButton, Stateful,
Subscription, Task, WeakEntity, list,
};
use crate::StackTraceView;
@@ -35,7 +35,7 @@ pub struct StackFrameList {
selected_ix: Option<usize>,
opened_stack_frame_id: Option<StackFrameId>,
scrollbar_state: ScrollbarState,
scroll_handle: UniformListScrollHandle,
list_state: ListState,
_refresh_task: Task<()>,
}
@@ -54,7 +54,6 @@ impl StackFrameList {
cx: &mut Context<Self>,
) -> Self {
let focus_handle = cx.focus_handle();
let scroll_handle = UniformListScrollHandle::new();
let _subscription =
cx.subscribe_in(&session, window, |this, _, event, window, cx| match event {
@@ -67,8 +66,16 @@ impl StackFrameList {
_ => {}
});
let list_state = ListState::new(0, gpui::ListAlignment::Top, px(1000.), {
let this = cx.weak_entity();
move |ix, _window, cx| {
this.update(cx, |this, cx| this.render_entry(ix, cx))
.unwrap_or(div().into_any())
}
});
let scrollbar_state = ScrollbarState::new(list_state.clone());
let mut this = Self {
scrollbar_state: ScrollbarState::new(scroll_handle.clone()),
session,
workspace,
focus_handle,
@@ -77,7 +84,8 @@ impl StackFrameList {
entries: Default::default(),
selected_ix: None,
opened_stack_frame_id: None,
scroll_handle,
list_state,
scrollbar_state,
_refresh_task: Task::ready(()),
};
this.schedule_refresh(true, window, cx);
@@ -214,6 +222,7 @@ impl StackFrameList {
self.selected_ix = ix;
}
self.list_state.reset(self.entries.len());
cx.emit(StackFrameListEvent::BuiltEntries);
cx.notify();
}
@@ -555,10 +564,6 @@ impl StackFrameList {
fn select_ix(&mut self, ix: Option<usize>, cx: &mut Context<Self>) {
self.selected_ix = ix;
if let Some(ix) = self.selected_ix {
self.scroll_handle
.scroll_to_item(ix, ScrollStrategy::Center);
}
cx.notify();
}
@@ -642,15 +647,8 @@ impl StackFrameList {
self.activate_selected_entry(window, cx);
}
fn render_list(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
uniform_list(
cx.entity(),
"stack-frame-list",
self.entries.len(),
|this, range, _window, cx| range.map(|ix| this.render_entry(ix, cx)).collect(),
)
.track_scroll(self.scroll_handle.clone())
.size_full()
fn render_list(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
list(self.list_state.clone()).size_full()
}
}

View File

@@ -464,7 +464,7 @@ impl BlockMap {
map
}
pub fn read(&self, wrap_snapshot: WrapSnapshot, edits: Patch<u32>) -> BlockMapReader {
pub fn read(&self, wrap_snapshot: WrapSnapshot, edits: Patch<u32>) -> BlockMapReader<'_> {
self.sync(&wrap_snapshot, edits);
*self.wrap_snapshot.borrow_mut() = wrap_snapshot.clone();
BlockMapReader {
@@ -479,7 +479,7 @@ impl BlockMap {
}
}
pub fn write(&mut self, wrap_snapshot: WrapSnapshot, edits: Patch<u32>) -> BlockMapWriter {
pub fn write(&mut self, wrap_snapshot: WrapSnapshot, edits: Patch<u32>) -> BlockMapWriter<'_> {
self.sync(&wrap_snapshot, edits);
*self.wrap_snapshot.borrow_mut() = wrap_snapshot;
BlockMapWriter(self)
@@ -1327,7 +1327,7 @@ impl BlockSnapshot {
}
}
pub(super) fn row_infos(&self, start_row: BlockRow) -> BlockRows {
pub(super) fn row_infos(&self, start_row: BlockRow) -> BlockRows<'_> {
let mut cursor = self.transforms.cursor::<(BlockRow, WrapRow)>(&());
cursor.seek(&start_row, Bias::Right, &());
let (output_start, input_start) = cursor.start();

View File

@@ -357,7 +357,7 @@ impl FoldMap {
&mut self,
inlay_snapshot: InlaySnapshot,
edits: Vec<InlayEdit>,
) -> (FoldMapWriter, FoldSnapshot, Vec<FoldEdit>) {
) -> (FoldMapWriter<'_>, FoldSnapshot, Vec<FoldEdit>) {
let (snapshot, edits) = self.read(inlay_snapshot, edits);
(FoldMapWriter(self), snapshot, edits)
}
@@ -730,7 +730,7 @@ impl FoldSnapshot {
(line_end - line_start) as u32
}
pub fn row_infos(&self, start_row: u32) -> FoldRows {
pub fn row_infos(&self, start_row: u32) -> FoldRows<'_> {
if start_row > self.transforms.summary().output.lines.row {
panic!("invalid display row {}", start_row);
}

View File

@@ -726,7 +726,7 @@ impl WrapSnapshot {
self.transforms.summary().output.longest_row
}
pub fn row_infos(&self, start_row: u32) -> WrapRows {
pub fn row_infos(&self, start_row: u32) -> WrapRows<'_> {
let mut transforms = self.transforms.cursor::<(WrapPoint, TabPoint)>(&());
transforms.seek(&WrapPoint::new(start_row, 0), Bias::Left, &());
let mut input_row = transforms.start().1.row();

View File

@@ -213,11 +213,14 @@ use workspace::{
searchable::SearchEvent,
};
use crate::signature_help::{SignatureHelpHiddenBy, SignatureHelpState};
use crate::{
code_context_menus::CompletionsMenuSource,
hover_links::{find_url, find_url_from_range},
};
use crate::{
editor_settings::MultiCursorModifier,
signature_help::{SignatureHelpHiddenBy, SignatureHelpState},
};
pub const FILE_HEADER_HEIGHT: u32 = 2;
pub const MULTI_BUFFER_EXCERPT_HEADER_HEIGHT: u32 = 1;
@@ -253,14 +256,6 @@ pub type RenderDiffHunkControlsFn = Arc<
) -> AnyElement,
>;
const COLUMNAR_SELECTION_MODIFIERS: Modifiers = Modifiers {
alt: true,
shift: true,
control: false,
platform: false,
function: false,
};
struct InlineValueCache {
enabled: bool,
inlays: Vec<InlayId>,
@@ -703,7 +698,7 @@ impl EditorActionId {
// type OverrideTextStyle = dyn Fn(&EditorStyle) -> Option<HighlightStyle>;
type BackgroundHighlight = (fn(&ThemeColors) -> Hsla, Arc<[Range<Anchor>]>);
type GutterHighlight = (fn(&App) -> Hsla, Arc<[Range<Anchor>]>);
type GutterHighlight = (fn(&App) -> Hsla, Vec<Range<Anchor>>);
#[derive(Default)]
struct ScrollbarMarkerState {
@@ -911,9 +906,24 @@ struct InlineBlamePopover {
popover_state: InlineBlamePopoverState,
}
enum SelectionDragState {
/// State when no drag related activity is detected.
None,
/// State when the mouse is down on a selection that is about to be dragged.
ReadyToDrag {
selection: Selection<Anchor>,
click_position: gpui::Point<Pixels>,
},
/// State when the mouse is dragging the selection in the editor.
Dragging {
selection: Selection<Anchor>,
drop_cursor: Selection<Anchor>,
},
}
/// Represents a breakpoint indicator that shows up when hovering over lines in the gutter that don't have
/// a breakpoint on them.
#[derive(Clone, Copy, Debug)]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
struct PhantomBreakpointIndicator {
display_row: DisplayRow,
/// There's a small debounce between hovering over the line and showing the indicator.
@@ -921,6 +931,7 @@ struct PhantomBreakpointIndicator {
is_active: bool,
collides_with_existing_breakpoint: bool,
}
/// Zed's primary implementation of text input, allowing users to edit a [`MultiBuffer`].
///
/// See the [module level documentation](self) for more information.
@@ -1096,6 +1107,8 @@ pub struct Editor {
hide_mouse_mode: HideMouseMode,
pub change_list: ChangeList,
inline_value_cache: InlineValueCache,
selection_drag_state: SelectionDragState,
drag_and_drop_selection_enabled: bool,
}
#[derive(Copy, Clone, Debug, PartialEq, Eq, Default)]
@@ -1187,10 +1200,12 @@ struct SelectionHistoryEntry {
add_selections_state: Option<AddSelectionsState>,
}
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
enum SelectionHistoryMode {
Normal,
Undoing,
Redoing,
Skipping,
}
#[derive(Clone, PartialEq, Eq, Hash)]
@@ -1224,11 +1239,19 @@ struct SelectionHistory {
}
impl SelectionHistory {
#[track_caller]
fn insert_transaction(
&mut self,
transaction_id: TransactionId,
selections: Arc<[Selection<Anchor>]>,
) {
if selections.is_empty() {
log::error!(
"SelectionHistory::insert_transaction called with empty selections. Caller: {}",
std::panic::Location::caller()
);
return;
}
self.selections_by_transaction
.insert(transaction_id, (selections, None));
}
@@ -1258,6 +1281,7 @@ impl SelectionHistory {
}
SelectionHistoryMode::Undoing => self.push_redo(entry),
SelectionHistoryMode::Redoing => self.push_undo(entry),
SelectionHistoryMode::Skipping => {}
}
}
}
@@ -1990,6 +2014,8 @@ impl Editor {
.unwrap_or_default(),
change_list: ChangeList::new(),
mode,
selection_drag_state: SelectionDragState::None,
drag_and_drop_selection_enabled: EditorSettings::get_global(cx).drag_and_drop_selection,
};
if let Some(breakpoints) = editor.breakpoint_store.as_ref() {
editor
@@ -2075,7 +2101,11 @@ impl Editor {
}
}
// skip adding the initial selection to selection history
editor.selection_history.mode = SelectionHistoryMode::Skipping;
editor.end_selection(window, cx);
editor.selection_history.mode = SelectionHistoryMode::Normal;
editor.scroll_manager.show_scrollbars(window, cx);
jsx_tag_auto_close::refresh_enabled_in_any_buffer(&mut editor, &buffer, cx);
@@ -3535,6 +3565,7 @@ impl Editor {
pub fn cancel(&mut self, _: &Cancel, window: &mut Window, cx: &mut Context<Self>) {
self.selection_mark_mode = false;
self.selection_drag_state = SelectionDragState::None;
if self.clear_expanded_diff_hunks(cx) {
cx.notify();
@@ -7091,6 +7122,29 @@ impl Editor {
)
}
fn multi_cursor_modifier(
cursor_event: bool,
modifiers: &Modifiers,
cx: &mut Context<Self>,
) -> bool {
let multi_cursor_setting = EditorSettings::get_global(cx).multi_cursor_modifier;
if cursor_event {
match multi_cursor_setting {
MultiCursorModifier::Alt => modifiers.alt,
MultiCursorModifier::CmdOrCtrl => modifiers.secondary(),
}
} else {
match multi_cursor_setting {
MultiCursorModifier::Alt => modifiers.secondary(),
MultiCursorModifier::CmdOrCtrl => modifiers.alt,
}
}
}
fn columnar_selection_modifiers(multi_cursor_modifier: bool, modifiers: &Modifiers) -> bool {
modifiers.shift && multi_cursor_modifier && modifiers.number_of_modifiers() == 2
}
fn update_selection_mode(
&mut self,
modifiers: &Modifiers,
@@ -7098,7 +7152,10 @@ impl Editor {
window: &mut Window,
cx: &mut Context<Self>,
) {
if modifiers != &COLUMNAR_SELECTION_MODIFIERS || self.selections.pending.is_none() {
let multi_cursor_modifier = Self::multi_cursor_modifier(true, modifiers, cx);
if !Self::columnar_selection_modifiers(multi_cursor_modifier, modifiers)
|| self.selections.pending.is_none()
{
return;
}
@@ -10563,6 +10620,44 @@ impl Editor {
});
}
pub fn move_selection_on_drop(
&mut self,
selection: &Selection<Anchor>,
target: DisplayPoint,
is_cut: bool,
window: &mut Window,
cx: &mut Context<Self>,
) {
let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
let buffer = &display_map.buffer_snapshot;
let mut edits = Vec::new();
let insert_point = display_map
.clip_point(target, Bias::Left)
.to_point(&display_map);
let text = buffer
.text_for_range(selection.start..selection.end)
.collect::<String>();
if is_cut {
edits.push(((selection.start..selection.end), String::new()));
}
let insert_anchor = buffer.anchor_before(insert_point);
edits.push(((insert_anchor..insert_anchor), text));
let last_edit_start = insert_anchor.bias_left(buffer);
let last_edit_end = insert_anchor.bias_right(buffer);
self.transact(window, cx, |this, window, cx| {
this.buffer.update(cx, |buffer, cx| {
buffer.edit(edits, None, cx);
});
this.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
s.select_anchor_ranges([last_edit_start..last_edit_end]);
});
});
}
pub fn clear_selection_drag_state(&mut self) {
self.selection_drag_state = SelectionDragState::None;
}
pub fn duplicate(
&mut self,
upwards: bool,
@@ -14132,18 +14227,20 @@ impl Editor {
cx: &mut Context<Self>,
) {
self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction);
self.end_selection(window, cx);
self.selection_history.mode = SelectionHistoryMode::Undoing;
if let Some(entry) = self.selection_history.undo_stack.pop_back() {
self.change_selections(None, window, cx, |s| {
s.select_anchors(entry.selections.to_vec())
self.selection_history.mode = SelectionHistoryMode::Undoing;
self.with_selection_effects_deferred(window, cx, |this, window, cx| {
this.end_selection(window, cx);
this.change_selections(Some(Autoscroll::newest()), window, cx, |s| {
s.select_anchors(entry.selections.to_vec())
});
});
self.selection_history.mode = SelectionHistoryMode::Normal;
self.select_next_state = entry.select_next_state;
self.select_prev_state = entry.select_prev_state;
self.add_selections_state = entry.add_selections_state;
self.request_autoscroll(Autoscroll::newest(), cx);
}
self.selection_history.mode = SelectionHistoryMode::Normal;
}
pub fn redo_selection(
@@ -14153,18 +14250,20 @@ impl Editor {
cx: &mut Context<Self>,
) {
self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction);
self.end_selection(window, cx);
self.selection_history.mode = SelectionHistoryMode::Redoing;
if let Some(entry) = self.selection_history.redo_stack.pop_back() {
self.change_selections(None, window, cx, |s| {
s.select_anchors(entry.selections.to_vec())
self.selection_history.mode = SelectionHistoryMode::Redoing;
self.with_selection_effects_deferred(window, cx, |this, window, cx| {
this.end_selection(window, cx);
this.change_selections(Some(Autoscroll::newest()), window, cx, |s| {
s.select_anchors(entry.selections.to_vec())
});
});
self.selection_history.mode = SelectionHistoryMode::Normal;
self.select_next_state = entry.select_next_state;
self.select_prev_state = entry.select_prev_state;
self.add_selections_state = entry.add_selections_state;
self.request_autoscroll(Autoscroll::newest(), cx);
}
self.selection_history.mode = SelectionHistoryMode::Normal;
}
pub fn expand_excerpts(
@@ -18298,12 +18397,12 @@ impl Editor {
pub fn highlight_gutter<T: 'static>(
&mut self,
ranges: &[Range<Anchor>],
ranges: impl Into<Vec<Range<Anchor>>>,
color_fetcher: fn(&App) -> Hsla,
cx: &mut Context<Self>,
) {
self.gutter_highlights
.insert(TypeId::of::<T>(), (color_fetcher, Arc::from(ranges)));
.insert(TypeId::of::<T>(), (color_fetcher, ranges.into()));
cx.notify();
}
@@ -18315,6 +18414,65 @@ impl Editor {
self.gutter_highlights.remove(&TypeId::of::<T>())
}
pub fn insert_gutter_highlight<T: 'static>(
&mut self,
range: Range<Anchor>,
color_fetcher: fn(&App) -> Hsla,
cx: &mut Context<Self>,
) {
let snapshot = self.buffer().read(cx).snapshot(cx);
let mut highlights = self
.gutter_highlights
.remove(&TypeId::of::<T>())
.map(|(_, highlights)| highlights)
.unwrap_or_default();
let ix = highlights.binary_search_by(|highlight| {
Ordering::Equal
.then_with(|| highlight.start.cmp(&range.start, &snapshot))
.then_with(|| highlight.end.cmp(&range.end, &snapshot))
});
if let Err(ix) = ix {
highlights.insert(ix, range);
}
self.gutter_highlights
.insert(TypeId::of::<T>(), (color_fetcher, highlights));
}
pub fn remove_gutter_highlights<T: 'static>(
&mut self,
ranges_to_remove: Vec<Range<Anchor>>,
cx: &mut Context<Self>,
) {
let snapshot = self.buffer().read(cx).snapshot(cx);
let Some((color_fetcher, mut gutter_highlights)) =
self.gutter_highlights.remove(&TypeId::of::<T>())
else {
return;
};
let mut ranges_to_remove = ranges_to_remove.iter().peekable();
gutter_highlights.retain(|highlight| {
while let Some(range_to_remove) = ranges_to_remove.peek() {
match range_to_remove.end.cmp(&highlight.start, &snapshot) {
Ordering::Less | Ordering::Equal => {
ranges_to_remove.next();
}
Ordering::Greater => {
match range_to_remove.start.cmp(&highlight.end, &snapshot) {
Ordering::Less | Ordering::Equal => {
return false;
}
Ordering::Greater => break,
}
}
}
}
true
});
self.gutter_highlights
.insert(TypeId::of::<T>(), (color_fetcher, gutter_highlights));
}
#[cfg(feature = "test-support")]
pub fn all_text_background_highlights(
&self,
@@ -18770,6 +18928,11 @@ impl Editor {
cx.emit(EditorEvent::BufferEdited);
cx.emit(SearchEvent::MatchesInvalidated);
if *singleton_buffer_edited {
if let Some(buffer) = edited_buffer {
if buffer.read(cx).file().is_none() {
cx.emit(EditorEvent::TitleChanged);
}
}
if let Some(project) = &self.project {
#[allow(clippy::mutable_key_type)]
let languages_affected = multibuffer.update(cx, |multibuffer, cx| {
@@ -18961,6 +19124,7 @@ impl Editor {
self.show_breadcrumbs = editor_settings.toolbar.breadcrumbs;
self.cursor_shape = editor_settings.cursor_shape.unwrap_or_default();
self.hide_mouse_mode = editor_settings.hide_mouse.unwrap_or_default();
self.drag_and_drop_selection_enabled = editor_settings.drag_and_drop_selection;
}
if old_cursor_shape != self.cursor_shape {
@@ -19854,12 +20018,15 @@ impl Editor {
if !selections.is_empty() {
let snapshot =
buffer_snapshot.get_or_init(|| self.buffer.read(cx).snapshot(cx));
// skip adding the initial selection to selection history
self.selection_history.mode = SelectionHistoryMode::Skipping;
self.change_selections(None, window, cx, |s| {
s.select_ranges(selections.into_iter().map(|(start, end)| {
snapshot.clip_offset(start, Bias::Left)
..snapshot.clip_offset(end, Bias::Right)
}));
});
self.selection_history.mode = SelectionHistoryMode::Normal;
}
};
}

View File

@@ -49,6 +49,7 @@ pub struct EditorSettings {
#[serde(default)]
pub diagnostics_max_severity: Option<DiagnosticSeverity>,
pub inline_code_actions: bool,
pub drag_and_drop_selection: bool,
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
@@ -422,7 +423,7 @@ pub struct EditorSettingsContent {
/// Default: always
pub seed_search_query_from_cursor: Option<SeedQuerySetting>,
pub use_smartcase_search: Option<bool>,
/// The key to use for adding multiple cursors
/// Determines the modifier to be used to add multiple cursors with the mouse. The open hover link mouse gestures will adapt such that it do not conflict with the multicursor modifier.
///
/// Default: alt
pub multi_cursor_modifier: Option<MultiCursorModifier>,
@@ -495,6 +496,11 @@ pub struct EditorSettingsContent {
///
/// Default: true
pub inline_code_actions: Option<bool>,
/// Whether to allow drag and drop text selection in buffer.
///
/// Default: true
pub drag_and_drop_selection: Option<bool>,
}
// Toolbar related settings

View File

@@ -1907,7 +1907,6 @@ fn test_prev_next_word_boundary(cx: &mut TestAppContext) {
DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4),
])
});
editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
assert_selection_ranges("use std::ˇstr::{foo, bar}\n\n {ˇbaz.qux()}", editor, cx);
@@ -1927,29 +1926,29 @@ fn test_prev_next_word_boundary(cx: &mut TestAppContext) {
assert_selection_ranges("useˇ std::str::{foo, barˇ}\n\n {baz.qux()}", editor, cx);
editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
assert_selection_ranges("use stdˇ::str::{foo, bar}\nˇ\n {baz.qux()}", editor, cx);
assert_selection_ranges("use stdˇ::str::{foo, bar}ˇ\n\n {baz.qux()}", editor, cx);
editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
assert_selection_ranges("use std::ˇstr::{foo, bar}\n\n {ˇbaz.qux()}", editor, cx);
assert_selection_ranges("use std::ˇstr::{foo, bar}\nˇ\n {baz.qux()}", editor, cx);
editor.move_right(&MoveRight, window, cx);
editor.select_to_previous_word_start(&SelectToPreviousWordStart, window, cx);
assert_selection_ranges(
"use std::«ˇs»tr::{foo, bar}\n\n {«ˇb»az.qux()}",
"use std::«ˇs»tr::{foo, bar}\n«ˇ\n» {baz.qux()}",
editor,
cx,
);
editor.select_to_previous_word_start(&SelectToPreviousWordStart, window, cx);
assert_selection_ranges(
"use std«ˇ::s»tr::{foo, bar}\n\n«ˇ {b»az.qux()}",
"use std«ˇ::s»tr::{foo, bar«ˇ}\n\n» {baz.qux()}",
editor,
cx,
);
editor.select_to_next_word_end(&SelectToNextWordEnd, window, cx);
assert_selection_ranges(
"use std::«ˇs»tr::{foo, bar}\n\n {«ˇb»az.qux()}",
"use std::«ˇs»tr::{foo, bar}«ˇ\n\n» {baz.qux()}",
editor,
cx,
);
@@ -21942,7 +21941,7 @@ async fn test_pulling_diagnostics(cx: &mut TestAppContext) {
.expect("created a singleton buffer")
.read(cx)
.remote_id();
let buffer_result_id = project.lsp_store().read(cx).result_id(buffer_id);
let buffer_result_id = project.lsp_store().read(cx).result_id(buffer_id, cx);
assert_eq!(expected, buffer_result_id);
});
};
@@ -21988,7 +21987,6 @@ async fn test_pulling_diagnostics(cx: &mut TestAppContext) {
"Cursor movement should not trigger diagnostic request"
);
ensure_result_id(Some("2".to_string()), cx);
// Multiple rapid edits should be debounced
for _ in 0..5 {
editor.update_in(cx, |editor, window, cx| {

View File

@@ -1,14 +1,14 @@
use crate::{
ActiveDiagnostic, BlockId, COLUMNAR_SELECTION_MODIFIERS, CURSORS_VISIBLE_FOR,
ChunkRendererContext, ChunkReplacement, CodeActionSource, ConflictsOurs, ConflictsOursMarker,
ConflictsOuter, ConflictsTheirs, ConflictsTheirsMarker, ContextMenuPlacement, CursorShape,
CustomBlockId, DisplayDiffHunk, DisplayPoint, DisplayRow, DocumentHighlightRead,
DocumentHighlightWrite, EditDisplayMode, Editor, EditorMode, EditorSettings, EditorSnapshot,
EditorStyle, FILE_HEADER_HEIGHT, FocusedBlock, GutterDimensions, HalfPageDown, HalfPageUp,
HandleInput, HoveredCursor, InlayHintRefreshReason, InlineCompletion, JumpData, LineDown,
LineHighlight, LineUp, MAX_LINE_LEN, MIN_LINE_NUMBER_DIGITS, MINIMAP_FONT_SIZE,
MULTI_BUFFER_EXCERPT_HEADER_HEIGHT, OpenExcerpts, PageDown, PageUp, PhantomBreakpointIndicator,
Point, RowExt, RowRangeExt, SelectPhase, SelectedTextHighlight, Selection, SoftWrap,
ActiveDiagnostic, BlockId, CURSORS_VISIBLE_FOR, ChunkRendererContext, ChunkReplacement,
CodeActionSource, ConflictsOurs, ConflictsOursMarker, ConflictsOuter, ConflictsTheirs,
ConflictsTheirsMarker, ContextMenuPlacement, CursorShape, CustomBlockId, DisplayDiffHunk,
DisplayPoint, DisplayRow, DocumentHighlightRead, DocumentHighlightWrite, EditDisplayMode,
Editor, EditorMode, EditorSettings, EditorSnapshot, EditorStyle, FILE_HEADER_HEIGHT,
FocusedBlock, GutterDimensions, HalfPageDown, HalfPageUp, HandleInput, HoveredCursor,
InlayHintRefreshReason, InlineCompletion, JumpData, LineDown, LineHighlight, LineUp,
MAX_LINE_LEN, MIN_LINE_NUMBER_DIGITS, MINIMAP_FONT_SIZE, MULTI_BUFFER_EXCERPT_HEADER_HEIGHT,
OpenExcerpts, PageDown, PageUp, PhantomBreakpointIndicator, Point, RowExt, RowRangeExt,
SelectPhase, SelectedTextHighlight, Selection, SelectionDragState, SoftWrap,
StickyHeaderExcerpt, ToPoint, ToggleFold,
code_context_menus::{CodeActionsMenu, MENU_ASIDE_MAX_WIDTH, MENU_ASIDE_MIN_WIDTH, MENU_GAP},
display_map::{
@@ -17,8 +17,7 @@ use crate::{
},
editor_settings::{
CurrentLineHighlight, DoubleClickInMultibuffer, MinimapThumb, MinimapThumbBorder,
MultiCursorModifier, ScrollBeyondLastLine, ScrollbarAxes, ScrollbarDiagnostics,
ShowMinimap, ShowScrollbar,
ScrollBeyondLastLine, ScrollbarAxes, ScrollbarDiagnostics, ShowMinimap, ShowScrollbar,
},
git::blame::{BlameRenderer, GitBlame, GlobalBlameRenderer},
hover_popover::{
@@ -79,10 +78,11 @@ use std::{
time::Duration,
};
use sum_tree::Bias;
use text::BufferId;
use text::{BufferId, SelectionGoal};
use theme::{ActiveTheme, Appearance, BufferLineHeight, PlayerColor};
use ui::{ButtonLike, KeyBinding, POPOVER_Y_PADDING, Tooltip, h_flex, prelude::*};
use unicode_segmentation::UnicodeSegmentation;
use util::post_inc;
use util::{RangeExt, ResultExt, debug_panic};
use workspace::{CollaboratorId, Workspace, item::Item, notifications::NotifyTaskExt};
@@ -620,6 +620,7 @@ impl EditorElement {
let text_hitbox = &position_map.text_hitbox;
let gutter_hitbox = &position_map.gutter_hitbox;
let point_for_position = position_map.point_for_position(event.position);
let mut click_count = event.click_count;
let mut modifiers = event.modifiers;
@@ -633,6 +634,20 @@ impl EditorElement {
return;
}
if editor.drag_and_drop_selection_enabled && click_count == 1 {
let newest_anchor = editor.selections.newest_anchor();
let snapshot = editor.snapshot(window, cx);
let selection = newest_anchor.map(|anchor| anchor.to_display_point(&snapshot));
if point_for_position.intersects_selection(&selection) {
editor.selection_drag_state = SelectionDragState::ReadyToDrag {
selection: newest_anchor.clone(),
click_position: event.position,
};
cx.stop_propagation();
return;
}
}
let is_singleton = editor.buffer().read(cx).is_singleton();
if click_count == 2 && !is_singleton {
@@ -676,9 +691,9 @@ impl EditorElement {
}
}
let point_for_position = position_map.point_for_position(event.position);
let position = point_for_position.previous_valid;
if modifiers == COLUMNAR_SELECTION_MODIFIERS {
let multi_cursor_modifier = Editor::multi_cursor_modifier(true, &modifiers, cx);
if Editor::columnar_selection_modifiers(multi_cursor_modifier, &modifiers) {
editor.select(
SelectPhase::BeginColumnar {
position,
@@ -699,11 +714,6 @@ impl EditorElement {
cx,
);
} else {
let multi_cursor_setting = EditorSettings::get_global(cx).multi_cursor_modifier;
let multi_cursor_modifier = match multi_cursor_setting {
MultiCursorModifier::Alt => modifiers.alt,
MultiCursorModifier::CmdOrCtrl => modifiers.secondary(),
};
editor.select(
SelectPhase::Begin {
position,
@@ -821,6 +831,47 @@ impl EditorElement {
let text_hitbox = &position_map.text_hitbox;
let end_selection = editor.has_pending_selection();
let pending_nonempty_selections = editor.has_pending_nonempty_selection();
let point_for_position = position_map.point_for_position(event.position);
match editor.selection_drag_state {
SelectionDragState::ReadyToDrag {
selection: _,
ref click_position,
} => {
if event.position == *click_position {
editor.select(
SelectPhase::Begin {
position: point_for_position.previous_valid,
add: false,
click_count: 1, // ready to drag state only occurs on click count 1
},
window,
cx,
);
editor.selection_drag_state = SelectionDragState::None;
cx.stop_propagation();
return;
}
}
SelectionDragState::Dragging { ref selection, .. } => {
let snapshot = editor.snapshot(window, cx);
let selection_display = selection.map(|anchor| anchor.to_display_point(&snapshot));
if !point_for_position.intersects_selection(&selection_display) {
let is_cut = !event.modifiers.control;
editor.move_selection_on_drop(
&selection.clone(),
point_for_position.previous_valid,
is_cut,
window,
cx,
);
editor.selection_drag_state = SelectionDragState::None;
cx.stop_propagation();
return;
}
}
_ => {}
}
if end_selection {
editor.select(SelectPhase::End, window, cx);
@@ -867,13 +918,9 @@ impl EditorElement {
let text_hitbox = &position_map.text_hitbox;
let pending_nonempty_selections = editor.has_pending_nonempty_selection();
let multi_cursor_setting = EditorSettings::get_global(cx).multi_cursor_modifier;
let multi_cursor_modifier = match multi_cursor_setting {
MultiCursorModifier::Alt => event.modifiers().secondary(),
MultiCursorModifier::CmdOrCtrl => event.modifiers().alt,
};
let hovered_link_modifier = Editor::multi_cursor_modifier(false, &event.modifiers(), cx);
if !pending_nonempty_selections && multi_cursor_modifier && text_hitbox.is_hovered(window) {
if !pending_nonempty_selections && hovered_link_modifier && text_hitbox.is_hovered(window) {
let point = position_map.point_for_position(event.up.position);
editor.handle_click_hovered_link(point, event.modifiers(), window, cx);
@@ -888,12 +935,15 @@ impl EditorElement {
window: &mut Window,
cx: &mut Context<Editor>,
) {
if !editor.has_pending_selection() {
if !editor.has_pending_selection()
&& matches!(editor.selection_drag_state, SelectionDragState::None)
{
return;
}
let text_bounds = position_map.text_hitbox.bounds;
let point_for_position = position_map.point_for_position(event.position);
let mut scroll_delta = gpui::Point::<f32>::default();
let vertical_margin = position_map.line_height.min(text_bounds.size.height / 3.0);
let top = text_bounds.origin.y + vertical_margin;
@@ -925,15 +975,46 @@ impl EditorElement {
scroll_delta.x = scale_horizontal_mouse_autoscroll_delta(event.position.x - right);
}
editor.select(
SelectPhase::Update {
position: point_for_position.previous_valid,
goal_column: point_for_position.exact_unclipped.column(),
scroll_delta,
},
window,
cx,
);
if !editor.has_pending_selection() {
let drop_anchor = position_map
.snapshot
.display_point_to_anchor(point_for_position.previous_valid, Bias::Left);
match editor.selection_drag_state {
SelectionDragState::Dragging {
ref mut drop_cursor,
..
} => {
drop_cursor.start = drop_anchor;
drop_cursor.end = drop_anchor;
}
SelectionDragState::ReadyToDrag { ref selection, .. } => {
let drop_cursor = Selection {
id: post_inc(&mut editor.selections.next_selection_id),
start: drop_anchor,
end: drop_anchor,
reversed: false,
goal: SelectionGoal::None,
};
editor.selection_drag_state = SelectionDragState::Dragging {
selection: selection.clone(),
drop_cursor,
};
}
_ => {}
}
editor.apply_scroll_delta(scroll_delta, window, cx);
cx.notify();
} else {
editor.select(
SelectPhase::Update {
position: point_for_position.previous_valid,
goal_column: point_for_position.exact_unclipped.column(),
scroll_delta,
},
window,
cx,
);
}
}
fn mouse_moved(
@@ -950,7 +1031,7 @@ impl EditorElement {
editor.set_gutter_hovered(gutter_hovered, cx);
editor.mouse_cursor_hidden = false;
if gutter_hovered {
let breakpoint_indicator = if gutter_hovered {
let new_point = position_map
.point_for_position(event.position)
.previous_valid;
@@ -964,7 +1045,6 @@ impl EditorElement {
.buffer_for_excerpt(buffer_anchor.excerpt_id)
.and_then(|buffer| buffer.file().map(|file| (buffer, file)))
{
let was_hovered = editor.gutter_breakpoint_indicator.0.is_some();
let as_point = text::ToPoint::to_point(&buffer_anchor.text_anchor, buffer_snapshot);
let is_visible = editor
@@ -992,38 +1072,43 @@ impl EditorElement {
.is_some()
});
editor.gutter_breakpoint_indicator.0 = Some(PhantomBreakpointIndicator {
display_row: new_point.row(),
is_active: is_visible,
collides_with_existing_breakpoint: has_existing_breakpoint,
});
editor.gutter_breakpoint_indicator.1.get_or_insert_with(|| {
cx.spawn(async move |this, cx| {
if !was_hovered {
if !is_visible {
editor.gutter_breakpoint_indicator.1.get_or_insert_with(|| {
cx.spawn(async move |this, cx| {
cx.background_executor()
.timer(Duration::from_millis(200))
.await;
}
this.update(cx, |this, cx| {
if let Some(indicator) = this.gutter_breakpoint_indicator.0.as_mut() {
indicator.is_active = true;
}
cx.notify();
this.update(cx, |this, cx| {
if let Some(indicator) = this.gutter_breakpoint_indicator.0.as_mut()
{
indicator.is_active = true;
cx.notify();
}
})
.ok();
})
.ok();
})
});
});
}
Some(PhantomBreakpointIndicator {
display_row: new_point.row(),
is_active: is_visible,
collides_with_existing_breakpoint: has_existing_breakpoint,
})
} else {
editor.gutter_breakpoint_indicator = (None, None);
editor.gutter_breakpoint_indicator.1 = None;
None
}
} else {
editor.gutter_breakpoint_indicator = (None, None);
}
editor.gutter_breakpoint_indicator.1 = None;
None
};
cx.notify();
if &breakpoint_indicator != &editor.gutter_breakpoint_indicator.0 {
editor.gutter_breakpoint_indicator.0 = breakpoint_indicator;
cx.notify();
}
// Don't trigger hover popover if mouse is hovering over context menu
if text_hitbox.is_hovered(window) {
@@ -1162,6 +1247,34 @@ impl EditorElement {
let player = editor.current_user_player_color(cx);
selections.push((player, layouts));
if let SelectionDragState::Dragging {
ref selection,
ref drop_cursor,
} = editor.selection_drag_state
{
if drop_cursor
.start
.cmp(&selection.start, &snapshot.buffer_snapshot)
.eq(&Ordering::Less)
|| drop_cursor
.end
.cmp(&selection.end, &snapshot.buffer_snapshot)
.eq(&Ordering::Greater)
{
let drag_cursor_layout = SelectionLayout::new(
drop_cursor.clone(),
false,
CursorShape::Bar,
&snapshot.display_snapshot,
false,
false,
None,
);
let absent_color = cx.theme().players().absent();
selections.push((absent_color, vec![drag_cursor_layout]));
}
}
}
if let Some(collaboration_hub) = &editor.collaboration_hub {
@@ -7216,6 +7329,17 @@ impl LineWithInvisibles {
paint(window, cx);
}),
ShowWhitespaceSetting::Trailing => {
let mut previous_start = self.len;
for ([start, end], paint) in invisible_iter.rev() {
if previous_start != end {
break;
}
previous_start = start;
paint(window, cx);
}
}
// For a whitespace to be on a boundary, any of the following conditions need to be met:
// - It is a tab
// - It is adjacent to an edge (start or end)
@@ -9242,6 +9366,35 @@ impl PointForPosition {
None
}
}
pub fn intersects_selection(&self, selection: &Selection<DisplayPoint>) -> bool {
let Some(valid_point) = self.as_valid() else {
return false;
};
let range = selection.range();
let candidate_row = valid_point.row();
let candidate_col = valid_point.column();
let start_row = range.start.row();
let start_col = range.start.column();
let end_row = range.end.row();
let end_col = range.end.column();
if candidate_row < start_row || candidate_row > end_row {
false
} else if start_row == end_row {
candidate_col >= start_col && candidate_col < end_col
} else {
if candidate_row == start_row {
candidate_col >= start_col
} else if candidate_row == end_row {
candidate_col < end_col
} else {
true
}
}
}
}
impl PositionMap {

View File

@@ -1,7 +1,7 @@
use crate::{
Anchor, Editor, EditorSettings, EditorSnapshot, FindAllReferences, GoToDefinition,
GoToTypeDefinition, GotoDefinitionKind, InlayId, Navigated, PointForPosition, SelectPhase,
editor_settings::{GoToDefinitionFallback, MultiCursorModifier},
editor_settings::GoToDefinitionFallback,
hover_popover::{self, InlayHover},
scroll::ScrollAmount,
};
@@ -120,11 +120,7 @@ impl Editor {
window: &mut Window,
cx: &mut Context<Self>,
) {
let multi_cursor_setting = EditorSettings::get_global(cx).multi_cursor_modifier;
let hovered_link_modifier = match multi_cursor_setting {
MultiCursorModifier::Alt => modifiers.secondary(),
MultiCursorModifier::CmdOrCtrl => modifiers.alt,
};
let hovered_link_modifier = Editor::multi_cursor_modifier(false, &modifiers, cx);
if !hovered_link_modifier || self.has_pending_selection() {
self.hide_hovered_link(cx);
return;

View File

@@ -266,10 +266,11 @@ pub fn previous_word_start(map: &DisplaySnapshot, point: DisplayPoint) -> Displa
let mut is_first_iteration = true;
find_preceding_boundary_display_point(map, point, FindRange::MultiLine, |left, right| {
// Make alt-left skip punctuation on Mac OS to respect Mac VSCode behaviour. For example: hello.| goes to |hello.
// Make alt-left skip punctuation to respect VSCode behaviour. For example: hello.| goes to |hello.
if is_first_iteration
&& classifier.is_punctuation(right)
&& !classifier.is_punctuation(left)
&& left != '\n'
{
is_first_iteration = false;
return false;
@@ -318,10 +319,11 @@ pub fn next_word_end(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint
let classifier = map.buffer_snapshot.char_classifier_at(raw_point);
let mut is_first_iteration = true;
find_boundary(map, point, FindRange::MultiLine, |left, right| {
// Make alt-right skip punctuation on Mac OS to respect the Mac behaviour. For example: |.hello goes to .hello|
// Make alt-right skip punctuation to respect VSCode behaviour. For example: |.hello goes to .hello|
if is_first_iteration
&& classifier.is_punctuation(left)
&& !classifier.is_punctuation(right)
&& right != '\n'
{
is_first_iteration = false;
return false;

View File

@@ -411,7 +411,7 @@ impl<'a> MutableSelectionsCollection<'a> {
self.collection.display_map(self.cx)
}
pub fn buffer(&self) -> Ref<MultiBufferSnapshot> {
pub fn buffer(&self) -> Ref<'_, MultiBufferSnapshot> {
self.collection.buffer(self.cx)
}

View File

@@ -724,7 +724,7 @@ impl IncrementalCompilationCache {
}
impl CacheStore for IncrementalCompilationCache {
fn get(&self, key: &[u8]) -> Option<Cow<[u8]>> {
fn get(&self, key: &[u8]) -> Option<Cow<'_, [u8]>> {
self.cache.get(key).map(|v| v.into())
}

View File

@@ -323,7 +323,7 @@ pub trait GitRepository: Send + Sync {
/// Resolve a list of refs to SHAs.
fn revparse_batch(&self, revs: Vec<String>) -> BoxFuture<Result<Vec<Option<String>>>>;
fn head_sha(&self) -> BoxFuture<Option<String>> {
fn head_sha(&self) -> BoxFuture<'_, Option<String>> {
async move {
self.revparse_batch(vec!["HEAD".into()])
.await
@@ -525,7 +525,7 @@ impl GitRepository for RealGitRepository {
repo.commondir().into()
}
fn show(&self, commit: String) -> BoxFuture<Result<CommitDetails>> {
fn show(&self, commit: String) -> BoxFuture<'_, Result<CommitDetails>> {
let working_directory = self.working_directory();
self.executor
.spawn(async move {
@@ -561,7 +561,7 @@ impl GitRepository for RealGitRepository {
.boxed()
}
fn load_commit(&self, commit: String, cx: AsyncApp) -> BoxFuture<Result<CommitDiff>> {
fn load_commit(&self, commit: String, cx: AsyncApp) -> BoxFuture<'_, Result<CommitDiff>> {
let Some(working_directory) = self.repository.lock().workdir().map(ToOwned::to_owned)
else {
return future::ready(Err(anyhow!("no working directory"))).boxed();
@@ -668,7 +668,7 @@ impl GitRepository for RealGitRepository {
commit: String,
mode: ResetMode,
env: Arc<HashMap<String, String>>,
) -> BoxFuture<Result<()>> {
) -> BoxFuture<'_, Result<()>> {
async move {
let working_directory = self.working_directory();
@@ -698,7 +698,7 @@ impl GitRepository for RealGitRepository {
commit: String,
paths: Vec<RepoPath>,
env: Arc<HashMap<String, String>>,
) -> BoxFuture<Result<()>> {
) -> BoxFuture<'_, Result<()>> {
let working_directory = self.working_directory();
let git_binary_path = self.git_binary_path.clone();
async move {
@@ -723,7 +723,7 @@ impl GitRepository for RealGitRepository {
.boxed()
}
fn load_index_text(&self, path: RepoPath) -> BoxFuture<Option<String>> {
fn load_index_text(&self, path: RepoPath) -> BoxFuture<'_, Option<String>> {
// https://git-scm.com/book/en/v2/Git-Internals-Git-Objects
const GIT_MODE_SYMLINK: u32 = 0o120000;
@@ -756,7 +756,7 @@ impl GitRepository for RealGitRepository {
.boxed()
}
fn load_committed_text(&self, path: RepoPath) -> BoxFuture<Option<String>> {
fn load_committed_text(&self, path: RepoPath) -> BoxFuture<'_, Option<String>> {
let repo = self.repository.clone();
self.executor
.spawn(async move {
@@ -777,7 +777,7 @@ impl GitRepository for RealGitRepository {
path: RepoPath,
content: Option<String>,
env: Arc<HashMap<String, String>>,
) -> BoxFuture<anyhow::Result<()>> {
) -> BoxFuture<'_, anyhow::Result<()>> {
let working_directory = self.working_directory();
let git_binary_path = self.git_binary_path.clone();
self.executor
@@ -841,7 +841,7 @@ impl GitRepository for RealGitRepository {
remote.url().map(|url| url.to_string())
}
fn revparse_batch(&self, revs: Vec<String>) -> BoxFuture<Result<Vec<Option<String>>>> {
fn revparse_batch(&self, revs: Vec<String>) -> BoxFuture<'_, Result<Vec<Option<String>>>> {
let working_directory = self.working_directory();
self.executor
.spawn(async move {
@@ -891,14 +891,14 @@ impl GitRepository for RealGitRepository {
.boxed()
}
fn merge_message(&self) -> BoxFuture<Option<String>> {
fn merge_message(&self) -> BoxFuture<'_, Option<String>> {
let path = self.path().join("MERGE_MSG");
self.executor
.spawn(async move { std::fs::read_to_string(&path).ok() })
.boxed()
}
fn status(&self, path_prefixes: &[RepoPath]) -> BoxFuture<Result<GitStatus>> {
fn status(&self, path_prefixes: &[RepoPath]) -> BoxFuture<'_, Result<GitStatus>> {
let git_binary_path = self.git_binary_path.clone();
let working_directory = self.working_directory();
let path_prefixes = path_prefixes.to_owned();
@@ -919,7 +919,7 @@ impl GitRepository for RealGitRepository {
.boxed()
}
fn branches(&self) -> BoxFuture<Result<Vec<Branch>>> {
fn branches(&self) -> BoxFuture<'_, Result<Vec<Branch>>> {
let working_directory = self.working_directory();
let git_binary_path = self.git_binary_path.clone();
self.executor
@@ -986,7 +986,7 @@ impl GitRepository for RealGitRepository {
.boxed()
}
fn change_branch(&self, name: String) -> BoxFuture<Result<()>> {
fn change_branch(&self, name: String) -> BoxFuture<'_, Result<()>> {
let repo = self.repository.clone();
self.executor
.spawn(async move {
@@ -1018,7 +1018,7 @@ impl GitRepository for RealGitRepository {
.boxed()
}
fn create_branch(&self, name: String) -> BoxFuture<Result<()>> {
fn create_branch(&self, name: String) -> BoxFuture<'_, Result<()>> {
let repo = self.repository.clone();
self.executor
.spawn(async move {
@@ -1030,7 +1030,7 @@ impl GitRepository for RealGitRepository {
.boxed()
}
fn blame(&self, path: RepoPath, content: Rope) -> BoxFuture<Result<crate::blame::Blame>> {
fn blame(&self, path: RepoPath, content: Rope) -> BoxFuture<'_, Result<crate::blame::Blame>> {
let working_directory = self.working_directory();
let git_binary_path = self.git_binary_path.clone();
@@ -1052,7 +1052,7 @@ impl GitRepository for RealGitRepository {
.boxed()
}
fn diff(&self, diff: DiffType) -> BoxFuture<Result<String>> {
fn diff(&self, diff: DiffType) -> BoxFuture<'_, Result<String>> {
let working_directory = self.working_directory();
let git_binary_path = self.git_binary_path.clone();
self.executor
@@ -1083,7 +1083,7 @@ impl GitRepository for RealGitRepository {
&self,
paths: Vec<RepoPath>,
env: Arc<HashMap<String, String>>,
) -> BoxFuture<Result<()>> {
) -> BoxFuture<'_, Result<()>> {
let working_directory = self.working_directory();
let git_binary_path = self.git_binary_path.clone();
self.executor
@@ -1111,7 +1111,7 @@ impl GitRepository for RealGitRepository {
&self,
paths: Vec<RepoPath>,
env: Arc<HashMap<String, String>>,
) -> BoxFuture<Result<()>> {
) -> BoxFuture<'_, Result<()>> {
let working_directory = self.working_directory();
let git_binary_path = self.git_binary_path.clone();
@@ -1143,7 +1143,7 @@ impl GitRepository for RealGitRepository {
name_and_email: Option<(SharedString, SharedString)>,
options: CommitOptions,
env: Arc<HashMap<String, String>>,
) -> BoxFuture<Result<()>> {
) -> BoxFuture<'_, Result<()>> {
let working_directory = self.working_directory();
self.executor
.spawn(async move {
@@ -1182,7 +1182,7 @@ impl GitRepository for RealGitRepository {
ask_pass: AskPassDelegate,
env: Arc<HashMap<String, String>>,
cx: AsyncApp,
) -> BoxFuture<Result<RemoteCommandOutput>> {
) -> BoxFuture<'_, Result<RemoteCommandOutput>> {
let working_directory = self.working_directory();
let executor = cx.background_executor().clone();
async move {
@@ -1214,7 +1214,7 @@ impl GitRepository for RealGitRepository {
ask_pass: AskPassDelegate,
env: Arc<HashMap<String, String>>,
cx: AsyncApp,
) -> BoxFuture<Result<RemoteCommandOutput>> {
) -> BoxFuture<'_, Result<RemoteCommandOutput>> {
let working_directory = self.working_directory();
let executor = cx.background_executor().clone();
async move {
@@ -1239,7 +1239,7 @@ impl GitRepository for RealGitRepository {
ask_pass: AskPassDelegate,
env: Arc<HashMap<String, String>>,
cx: AsyncApp,
) -> BoxFuture<Result<RemoteCommandOutput>> {
) -> BoxFuture<'_, Result<RemoteCommandOutput>> {
let working_directory = self.working_directory();
let remote_name = format!("{}", fetch_options);
let executor = cx.background_executor().clone();
@@ -1257,7 +1257,7 @@ impl GitRepository for RealGitRepository {
.boxed()
}
fn get_remotes(&self, branch_name: Option<String>) -> BoxFuture<Result<Vec<Remote>>> {
fn get_remotes(&self, branch_name: Option<String>) -> BoxFuture<'_, Result<Vec<Remote>>> {
let working_directory = self.working_directory();
let git_binary_path = self.git_binary_path.clone();
self.executor
@@ -1303,7 +1303,7 @@ impl GitRepository for RealGitRepository {
.boxed()
}
fn check_for_pushed_commit(&self) -> BoxFuture<Result<Vec<SharedString>>> {
fn check_for_pushed_commit(&self) -> BoxFuture<'_, Result<Vec<SharedString>>> {
let working_directory = self.working_directory();
let git_binary_path = self.git_binary_path.clone();
self.executor
@@ -1396,7 +1396,7 @@ impl GitRepository for RealGitRepository {
.boxed()
}
fn restore_checkpoint(&self, checkpoint: GitRepositoryCheckpoint) -> BoxFuture<Result<()>> {
fn restore_checkpoint(&self, checkpoint: GitRepositoryCheckpoint) -> BoxFuture<'_, Result<()>> {
let working_directory = self.working_directory();
let git_binary_path = self.git_binary_path.clone();
@@ -1435,7 +1435,7 @@ impl GitRepository for RealGitRepository {
&self,
left: GitRepositoryCheckpoint,
right: GitRepositoryCheckpoint,
) -> BoxFuture<Result<bool>> {
) -> BoxFuture<'_, Result<bool>> {
let working_directory = self.working_directory();
let git_binary_path = self.git_binary_path.clone();
@@ -1474,7 +1474,7 @@ impl GitRepository for RealGitRepository {
&self,
base_checkpoint: GitRepositoryCheckpoint,
target_checkpoint: GitRepositoryCheckpoint,
) -> BoxFuture<Result<String>> {
) -> BoxFuture<'_, Result<String>> {
let working_directory = self.working_directory();
let git_binary_path = self.git_binary_path.clone();

View File

@@ -248,6 +248,8 @@ fn conflicts_updated(
removed_block_ids.insert(block_id);
}
editor.remove_gutter_highlights::<ConflictsOuter>(removed_highlighted_ranges.clone(), cx);
editor.remove_highlighted_rows::<ConflictsOuter>(removed_highlighted_ranges.clone(), cx);
editor.remove_highlighted_rows::<ConflictsOurs>(removed_highlighted_ranges.clone(), cx);
editor
@@ -325,8 +327,7 @@ fn update_conflict_highlighting(
cx: &mut Context<Editor>,
) {
log::debug!("update conflict highlighting for {conflict:?}");
let theme = cx.theme().clone();
let colors = theme.colors();
let outer_start = buffer
.anchor_in_excerpt(excerpt_id, conflict.range.start)
.unwrap();
@@ -346,26 +347,29 @@ fn update_conflict_highlighting(
.anchor_in_excerpt(excerpt_id, conflict.theirs.end)
.unwrap();
let ours_background = colors.version_control_conflict_ours_background;
let ours_marker = colors.version_control_conflict_ours_marker_background;
let theirs_background = colors.version_control_conflict_theirs_background;
let theirs_marker = colors.version_control_conflict_theirs_marker_background;
let divider_background = colors.version_control_conflict_divider_background;
let ours_background = cx.theme().colors().version_control_conflict_marker_ours;
let theirs_background = cx.theme().colors().version_control_conflict_marker_theirs;
let options = RowHighlightOptions {
include_gutter: false,
include_gutter: true,
..Default::default()
};
editor.insert_gutter_highlight::<ConflictsOuter>(
outer_start..their_end,
|cx| cx.theme().colors().editor_background,
cx,
);
// Prevent diff hunk highlighting within the entire conflict region.
editor.highlight_rows::<ConflictsOuter>(
outer_start..outer_end,
divider_background,
editor.highlight_rows::<ConflictsOuter>(outer_start..outer_end, theirs_background, options, cx);
editor.highlight_rows::<ConflictsOurs>(our_start..our_end, ours_background, options, cx);
editor.highlight_rows::<ConflictsOursMarker>(
outer_start..our_start,
ours_background,
options,
cx,
);
editor.highlight_rows::<ConflictsOurs>(our_start..our_end, ours_background, options, cx);
editor.highlight_rows::<ConflictsOursMarker>(outer_start..our_start, ours_marker, options, cx);
editor.highlight_rows::<ConflictsTheirs>(
their_start..their_end,
theirs_background,
@@ -374,7 +378,7 @@ fn update_conflict_highlighting(
);
editor.highlight_rows::<ConflictsTheirsMarker>(
their_end..outer_end,
theirs_marker,
theirs_background,
options,
cx,
);
@@ -512,6 +516,9 @@ pub(crate) fn resolve_conflict(
let end = snapshot
.anchor_in_excerpt(excerpt_id, resolved_conflict.range.end)
.unwrap();
editor.remove_gutter_highlights::<ConflictsOuter>(vec![start..end], cx);
editor.remove_highlighted_rows::<ConflictsOuter>(vec![start..end], cx);
editor.remove_highlighted_rows::<ConflictsOurs>(vec![start..end], cx);
editor.remove_highlighted_rows::<ConflictsTheirs>(vec![start..end], cx);

View File

@@ -27,11 +27,12 @@ use git::status::StageStatus;
use git::{Amend, ToggleStaged, repository::RepoPath, status::FileStatus};
use git::{ExpandCommitEditor, RestoreTrackedFiles, StageAll, TrashUntrackedFiles, UnstageAll};
use gpui::{
Action, Animation, AnimationExt as _, Axis, ClickEvent, Corner, DismissEvent, Entity,
EventEmitter, FocusHandle, Focusable, KeyContext, ListHorizontalSizingBehavior,
ListSizingBehavior, Modifiers, ModifiersChangedEvent, MouseButton, MouseDownEvent, Point,
PromptLevel, ScrollStrategy, Subscription, Task, Transformation, UniformListScrollHandle,
WeakEntity, actions, anchored, deferred, percentage, uniform_list,
Action, Animation, AnimationExt as _, AsyncApp, AsyncWindowContext, Axis, ClickEvent, Corner,
DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, KeyContext,
ListHorizontalSizingBehavior, ListSizingBehavior, Modifiers, ModifiersChangedEvent,
MouseButton, MouseDownEvent, Point, PromptLevel, ScrollStrategy, Subscription, Task,
Transformation, UniformListScrollHandle, WeakEntity, actions, anchored, deferred, percentage,
uniform_list,
};
use itertools::Itertools;
use language::{Buffer, File};
@@ -63,11 +64,11 @@ use ui::{
Tooltip, prelude::*,
};
use util::{ResultExt, TryFutureExt, maybe};
use workspace::AppState;
use workspace::{
Workspace,
dock::{DockPosition, Panel, PanelEvent},
notifications::DetachAndPromptErr,
notifications::{DetachAndPromptErr, ErrorMessagePrompt, NotificationId},
};
use zed_llm_client::CompletionIntent;
@@ -389,144 +390,148 @@ pub(crate) fn commit_message_editor(
}
impl GitPanel {
pub fn new(
workspace: Entity<Workspace>,
project: Entity<Project>,
app_state: Arc<AppState>,
fn new(
workspace: &mut Workspace,
window: &mut Window,
cx: &mut Context<Self>,
) -> Self {
cx: &mut Context<Workspace>,
) -> Entity<Self> {
let project = workspace.project().clone();
let app_state = workspace.app_state().clone();
let fs = app_state.fs.clone();
let git_store = project.read(cx).git_store().clone();
let active_repository = project.read(cx).active_repository(cx);
let workspace = workspace.downgrade();
let focus_handle = cx.focus_handle();
cx.on_focus(&focus_handle, window, Self::focus_in).detach();
cx.on_focus_out(&focus_handle, window, |this, _, window, cx| {
this.hide_scrollbars(window, cx);
})
.detach();
let git_panel = cx.new(|cx| {
let focus_handle = cx.focus_handle();
cx.on_focus(&focus_handle, window, Self::focus_in).detach();
cx.on_focus_out(&focus_handle, window, |this, _, window, cx| {
this.hide_scrollbars(window, cx);
})
.detach();
let mut was_sort_by_path = GitPanelSettings::get_global(cx).sort_by_path;
cx.observe_global::<SettingsStore>(move |this, cx| {
let is_sort_by_path = GitPanelSettings::get_global(cx).sort_by_path;
if is_sort_by_path != was_sort_by_path {
this.update_visible_entries(cx);
}
was_sort_by_path = is_sort_by_path
})
.detach();
let mut was_sort_by_path = GitPanelSettings::get_global(cx).sort_by_path;
cx.observe_global::<SettingsStore>(move |this, cx| {
let is_sort_by_path = GitPanelSettings::get_global(cx).sort_by_path;
if is_sort_by_path != was_sort_by_path {
this.update_visible_entries(cx);
}
was_sort_by_path = is_sort_by_path
})
.detach();
// just to let us render a placeholder editor.
// Once the active git repo is set, this buffer will be replaced.
let temporary_buffer = cx.new(|cx| Buffer::local("", cx));
let commit_editor = cx.new(|cx| {
commit_message_editor(temporary_buffer, None, project.clone(), true, window, cx)
// just to let us render a placeholder editor.
// Once the active git repo is set, this buffer will be replaced.
let temporary_buffer = cx.new(|cx| Buffer::local("", cx));
let commit_editor = cx.new(|cx| {
commit_message_editor(temporary_buffer, None, project.clone(), true, window, cx)
});
commit_editor.update(cx, |editor, cx| {
editor.clear(window, cx);
});
let scroll_handle = UniformListScrollHandle::new();
let vertical_scrollbar = ScrollbarProperties {
axis: Axis::Vertical,
state: ScrollbarState::new(scroll_handle.clone()).parent_entity(&cx.entity()),
show_scrollbar: false,
show_track: false,
auto_hide: false,
hide_task: None,
};
let horizontal_scrollbar = ScrollbarProperties {
axis: Axis::Horizontal,
state: ScrollbarState::new(scroll_handle.clone()).parent_entity(&cx.entity()),
show_scrollbar: false,
show_track: false,
auto_hide: false,
hide_task: None,
};
let mut assistant_enabled = AgentSettings::get_global(cx).enabled;
let _settings_subscription = cx.observe_global::<SettingsStore>(move |_, cx| {
if assistant_enabled != AgentSettings::get_global(cx).enabled {
assistant_enabled = AgentSettings::get_global(cx).enabled;
cx.notify();
}
});
cx.subscribe_in(
&git_store,
window,
move |this, _git_store, event, window, cx| match event {
GitStoreEvent::ActiveRepositoryChanged(_) => {
this.active_repository = this.project.read(cx).active_repository(cx);
this.schedule_update(true, window, cx);
}
GitStoreEvent::RepositoryUpdated(
_,
RepositoryEvent::Updated { full_scan },
true,
) => {
this.schedule_update(*full_scan, window, cx);
}
GitStoreEvent::RepositoryAdded(_) | GitStoreEvent::RepositoryRemoved(_) => {
this.schedule_update(false, window, cx);
}
GitStoreEvent::IndexWriteError(error) => {
this.workspace
.update(cx, |workspace, cx| {
workspace.show_error(error, cx);
})
.ok();
}
GitStoreEvent::RepositoryUpdated(_, _, _) => {}
GitStoreEvent::JobsUpdated | GitStoreEvent::ConflictsUpdated => {}
},
)
.detach();
let mut this = Self {
active_repository,
commit_editor,
conflicted_count: 0,
conflicted_staged_count: 0,
current_modifiers: window.modifiers(),
add_coauthors: true,
generate_commit_message_task: None,
entries: Vec::new(),
focus_handle: cx.focus_handle(),
fs,
new_count: 0,
new_staged_count: 0,
pending: Vec::new(),
pending_commit: None,
amend_pending: false,
pending_serialization: Task::ready(None),
single_staged_entry: None,
single_tracked_entry: None,
project,
scroll_handle,
max_width_item_index: None,
selected_entry: None,
marked_entries: Vec::new(),
tracked_count: 0,
tracked_staged_count: 0,
update_visible_entries_task: Task::ready(()),
width: None,
show_placeholders: false,
context_menu: None,
workspace: workspace.weak_handle(),
modal_open: false,
entry_count: 0,
horizontal_scrollbar,
vertical_scrollbar,
_settings_subscription,
};
this.schedule_update(false, window, cx);
this
});
commit_editor.update(cx, |editor, cx| {
editor.clear(window, cx);
});
let scroll_handle = UniformListScrollHandle::new();
cx.subscribe_in(
&git_store,
window,
move |this, git_store, event, window, cx| match event {
GitStoreEvent::ActiveRepositoryChanged(_) => {
this.active_repository = git_store.read(cx).active_repository();
this.schedule_update(true, window, cx);
}
GitStoreEvent::RepositoryUpdated(
_,
RepositoryEvent::Updated { full_scan },
true,
) => {
this.schedule_update(*full_scan, window, cx);
}
GitStoreEvent::RepositoryAdded(_) | GitStoreEvent::RepositoryRemoved(_) => {
this.schedule_update(false, window, cx);
}
GitStoreEvent::IndexWriteError(error) => {
this.workspace
.update(cx, |workspace, cx| {
workspace.show_error(error, cx);
})
.ok();
}
GitStoreEvent::RepositoryUpdated(_, _, _) => {}
GitStoreEvent::JobsUpdated | GitStoreEvent::ConflictsUpdated => {}
},
)
.detach();
let vertical_scrollbar = ScrollbarProperties {
axis: Axis::Vertical,
state: ScrollbarState::new(scroll_handle.clone()).parent_entity(&cx.entity()),
show_scrollbar: false,
show_track: false,
auto_hide: false,
hide_task: None,
};
let horizontal_scrollbar = ScrollbarProperties {
axis: Axis::Horizontal,
state: ScrollbarState::new(scroll_handle.clone()).parent_entity(&cx.entity()),
show_scrollbar: false,
show_track: false,
auto_hide: false,
hide_task: None,
};
let mut assistant_enabled = AgentSettings::get_global(cx).enabled;
let _settings_subscription = cx.observe_global::<SettingsStore>(move |_, cx| {
if assistant_enabled != AgentSettings::get_global(cx).enabled {
assistant_enabled = AgentSettings::get_global(cx).enabled;
cx.notify();
}
});
let mut git_panel = Self {
active_repository,
commit_editor,
conflicted_count: 0,
conflicted_staged_count: 0,
current_modifiers: window.modifiers(),
add_coauthors: true,
generate_commit_message_task: None,
entries: Vec::new(),
focus_handle: cx.focus_handle(),
fs,
new_count: 0,
new_staged_count: 0,
pending: Vec::new(),
pending_commit: None,
amend_pending: false,
pending_serialization: Task::ready(None),
single_staged_entry: None,
single_tracked_entry: None,
project,
scroll_handle,
max_width_item_index: None,
selected_entry: None,
marked_entries: Vec::new(),
tracked_count: 0,
tracked_staged_count: 0,
update_visible_entries_task: Task::ready(()),
width: None,
show_placeholders: false,
context_menu: None,
workspace,
modal_open: false,
entry_count: 0,
horizontal_scrollbar,
vertical_scrollbar,
_settings_subscription,
};
git_panel.schedule_update(false, window, cx);
git_panel
}
@@ -1774,7 +1779,19 @@ impl GitPanel {
this.generate_commit_message_task.take();
});
let mut diff_text = diff.await??;
let mut diff_text = match diff.await {
Ok(result) => match result {
Ok(text) => text,
Err(e) => {
Self::show_commit_message_error(&this, &e, cx);
return anyhow::Ok(());
}
},
Err(e) => {
Self::show_commit_message_error(&this, &e, cx);
return anyhow::Ok(());
}
};
const ONE_MB: usize = 1_000_000;
if diff_text.len() > ONE_MB {
@@ -1812,26 +1829,37 @@ impl GitPanel {
};
let stream = model.stream_completion_text(request, &cx);
let mut messages = stream.await?;
match stream.await {
Ok(mut messages) => {
if !text_empty {
this.update(cx, |this, cx| {
this.commit_message_buffer(cx).update(cx, |buffer, cx| {
let insert_position = buffer.anchor_before(buffer.len());
buffer.edit([(insert_position..insert_position, "\n")], None, cx)
});
})?;
}
if !text_empty {
this.update(cx, |this, cx| {
this.commit_message_buffer(cx).update(cx, |buffer, cx| {
let insert_position = buffer.anchor_before(buffer.len());
buffer.edit([(insert_position..insert_position, "\n")], None, cx)
});
})?;
}
while let Some(message) = messages.stream.next().await {
let text = message?;
this.update(cx, |this, cx| {
this.commit_message_buffer(cx).update(cx, |buffer, cx| {
let insert_position = buffer.anchor_before(buffer.len());
buffer.edit([(insert_position..insert_position, text)], None, cx);
});
})?;
while let Some(message) = messages.stream.next().await {
match message {
Ok(text) => {
this.update(cx, |this, cx| {
this.commit_message_buffer(cx).update(cx, |buffer, cx| {
let insert_position = buffer.anchor_before(buffer.len());
buffer.edit([(insert_position..insert_position, text)], None, cx);
});
})?;
}
Err(e) => {
Self::show_commit_message_error(&this, &e, cx);
break;
}
}
}
}
Err(e) => {
Self::show_commit_message_error(&this, &e, cx);
}
}
anyhow::Ok(())
@@ -2689,6 +2717,26 @@ impl GitPanel {
}
}
fn show_commit_message_error<E>(weak_this: &WeakEntity<Self>, err: &E, cx: &mut AsyncApp)
where
E: std::fmt::Debug + std::fmt::Display,
{
if let Ok(Some(workspace)) = weak_this.update(cx, |this, _cx| this.workspace.upgrade()) {
let _ = workspace.update(cx, |workspace, cx| {
struct CommitMessageError;
let notification_id = NotificationId::unique::<CommitMessageError>();
workspace.show_notification(notification_id, cx, |cx| {
cx.new(|cx| {
ErrorMessagePrompt::new(
format!("Failed to generate commit message: {err}"),
cx,
)
})
});
});
}
}
fn show_remote_output(&self, action: RemoteAction, info: RemoteCommandOutput, cx: &mut App) {
let Some(workspace) = self.workspace.upgrade() else {
return;
@@ -4141,6 +4189,32 @@ impl GitPanel {
self.amend_pending = value;
cx.notify();
}
pub async fn load(
workspace: WeakEntity<Workspace>,
mut cx: AsyncWindowContext,
) -> anyhow::Result<Entity<Self>> {
let serialized_panel = cx
.background_spawn(async move { KEY_VALUE_STORE.read_kvp(&GIT_PANEL_KEY) })
.await
.context("loading git panel")
.log_err()
.flatten()
.and_then(|panel| serde_json::from_str::<SerializedGitPanel>(&panel).log_err());
workspace.update_in(&mut cx, |workspace, window, cx| {
let panel = GitPanel::new(workspace, window, cx);
if let Some(serialized_panel) = serialized_panel {
panel.update(cx, |panel, cx| {
panel.width = serialized_panel.width;
cx.notify();
})
}
panel
})
}
}
fn current_language_model(cx: &Context<'_, GitPanel>) -> Option<Arc<dyn LanguageModel>> {
@@ -4852,7 +4926,7 @@ impl Component for PanelRepoFooter {
#[cfg(test)]
mod tests {
use git::status::StatusCode;
use gpui::TestAppContext;
use gpui::{TestAppContext, VisualTestContext};
use project::{FakeFs, WorktreeSettings};
use serde_json::json;
use settings::SettingsStore;
@@ -4916,8 +4990,9 @@ mod tests {
let project =
Project::test(fs.clone(), [path!("/root/zed/crates/gpui").as_ref()], cx).await;
let (workspace, cx) =
cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
let workspace =
cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
let cx = &mut VisualTestContext::from_window(*workspace, cx);
cx.read(|cx| {
project
@@ -4934,10 +5009,7 @@ mod tests {
cx.executor().run_until_parked();
let app_state = workspace.read_with(cx, |workspace, _| workspace.app_state().clone());
let panel = cx.new_window_entity(|window, cx| {
GitPanel::new(workspace.clone(), project.clone(), app_state, window, cx)
});
let panel = workspace.update(cx, GitPanel::new).unwrap();
let handle = cx.update_window_entity(&panel, |panel, _, _| {
std::mem::replace(&mut panel.update_visible_entries_task, Task::ready(()))

View File

@@ -64,7 +64,7 @@ pub struct AppCell {
impl AppCell {
#[doc(hidden)]
#[track_caller]
pub fn borrow(&self) -> AppRef {
pub fn borrow(&self) -> AppRef<'_> {
if option_env!("TRACK_THREAD_BORROWS").is_some() {
let thread_id = std::thread::current().id();
eprintln!("borrowed {thread_id:?}");
@@ -74,7 +74,7 @@ impl AppCell {
#[doc(hidden)]
#[track_caller]
pub fn borrow_mut(&self) -> AppRefMut {
pub fn borrow_mut(&self) -> AppRefMut<'_> {
if option_env!("TRACK_THREAD_BORROWS").is_some() {
let thread_id = std::thread::current().id();
eprintln!("borrowed {thread_id:?}");
@@ -84,7 +84,7 @@ impl AppCell {
#[doc(hidden)]
#[track_caller]
pub fn try_borrow_mut(&self) -> Result<AppRefMut, BorrowMutError> {
pub fn try_borrow_mut(&self) -> Result<AppRefMut<'_>, BorrowMutError> {
if option_env!("TRACK_THREAD_BORROWS").is_some() {
let thread_id = std::thread::current().id();
eprintln!("borrowed {thread_id:?}");

View File

@@ -718,7 +718,7 @@ impl<T> ops::Index<usize> for AtlasTextureList<T> {
impl<T> AtlasTextureList<T> {
#[allow(unused)]
fn drain(&mut self) -> std::vec::Drain<Option<T>> {
fn drain(&mut self) -> std::vec::Drain<'_, Option<T>> {
self.free_list.clear();
self.textures.drain(..)
}

View File

@@ -25,7 +25,7 @@ pub(crate) const ESCAPE_KEY: u16 = 0x1b;
const TAB_KEY: u16 = 0x09;
const SHIFT_TAB_KEY: u16 = 0x19;
pub fn key_to_native(key: &str) -> Cow<str> {
pub fn key_to_native(key: &str) -> Cow<'_, str> {
use cocoa::appkit::*;
let code = match key {
"space" => SPACE_KEY,

View File

@@ -149,7 +149,7 @@ impl Scene {
),
allow(dead_code)
)]
pub(crate) fn batches(&self) -> impl Iterator<Item = PrimitiveBatch> {
pub(crate) fn batches(&self) -> impl Iterator<Item = PrimitiveBatch<'_>> {
BatchIterator {
shadows: &self.shadows,
shadows_start: 0,

View File

@@ -616,7 +616,7 @@ impl Hash for (dyn AsCacheKeyRef + '_) {
}
impl AsCacheKeyRef for CacheKey {
fn as_cache_key_ref(&self) -> CacheKeyRef {
fn as_cache_key_ref(&self) -> CacheKeyRef<'_> {
CacheKeyRef {
text: &self.text,
font_size: self.font_size,
@@ -645,7 +645,7 @@ impl<'a> Borrow<dyn AsCacheKeyRef + 'a> for Arc<CacheKey> {
}
impl AsCacheKeyRef for CacheKeyRef<'_> {
fn as_cache_key_ref(&self) -> CacheKeyRef {
fn as_cache_key_ref(&self) -> CacheKeyRef<'_> {
*self
}
}

View File

@@ -1874,9 +1874,12 @@ impl Buffer {
}
/// Ensures that the buffer ends with a single newline character, and
/// no other whitespace.
/// no other whitespace. Skips if the buffer is empty.
pub fn ensure_final_newline(&mut self, cx: &mut Context<Self>) {
let len = self.len();
if len == 0 {
return;
}
let mut offset = len;
for chunk in self.as_rope().reversed_chunks_in_range(0..len) {
let non_whitespace_len = chunk
@@ -3127,7 +3130,7 @@ impl BufferSnapshot {
None
}
fn get_highlights(&self, range: Range<usize>) -> (SyntaxMapCaptures, Vec<HighlightMap>) {
fn get_highlights(&self, range: Range<usize>) -> (SyntaxMapCaptures<'_>, Vec<HighlightMap>) {
let captures = self.syntax.captures(range, &self.text, |grammar| {
grammar.highlights_query.as_ref()
});
@@ -3143,7 +3146,7 @@ impl BufferSnapshot {
/// in an arbitrary way due to being stored in a [`Rope`](text::Rope). The text is also
/// returned in chunks where each chunk has a single syntax highlighting style and
/// diagnostic status.
pub fn chunks<T: ToOffset>(&self, range: Range<T>, language_aware: bool) -> BufferChunks {
pub fn chunks<T: ToOffset>(&self, range: Range<T>, language_aware: bool) -> BufferChunks<'_> {
let range = range.start.to_offset(self)..range.end.to_offset(self);
let mut syntax = None;
@@ -3192,12 +3195,12 @@ impl BufferSnapshot {
}
/// Iterates over every [`SyntaxLayer`] in the buffer.
pub fn syntax_layers(&self) -> impl Iterator<Item = SyntaxLayer> + '_ {
pub fn syntax_layers(&self) -> impl Iterator<Item = SyntaxLayer<'_>> + '_ {
self.syntax
.layers_for_range(0..self.len(), &self.text, true)
}
pub fn syntax_layer_at<D: ToOffset>(&self, position: D) -> Option<SyntaxLayer> {
pub fn syntax_layer_at<D: ToOffset>(&self, position: D) -> Option<SyntaxLayer<'_>> {
let offset = position.to_offset(self);
self.syntax
.layers_for_range(offset..offset, &self.text, false)
@@ -3208,7 +3211,7 @@ impl BufferSnapshot {
pub fn smallest_syntax_layer_containing<D: ToOffset>(
&self,
range: Range<D>,
) -> Option<SyntaxLayer> {
) -> Option<SyntaxLayer<'_>> {
let range = range.to_offset(self);
return self
.syntax
@@ -3426,7 +3429,7 @@ impl BufferSnapshot {
}
/// Returns the root syntax node within the given row
pub fn syntax_root_ancestor(&self, position: Anchor) -> Option<tree_sitter::Node> {
pub fn syntax_root_ancestor(&self, position: Anchor) -> Option<tree_sitter::Node<'_>> {
let start_offset = position.to_offset(self);
let row = self.summary_for_anchor::<text::PointUtf16>(&position).row as usize;
@@ -3763,7 +3766,7 @@ impl BufferSnapshot {
&self,
range: Range<usize>,
query: fn(&Grammar) -> Option<&tree_sitter::Query>,
) -> SyntaxMapMatches {
) -> SyntaxMapMatches<'_> {
self.syntax.matches(range, self, query)
}

View File

@@ -323,6 +323,8 @@ pub trait LspAdapterDelegate: Send + Sync {
fn http_client(&self) -> Arc<dyn HttpClient>;
fn worktree_id(&self) -> WorktreeId;
fn worktree_root_path(&self) -> &Path;
fn subproject_root_path(&self) -> PathBuf;
fn exists(&self, path: &Path, is_dir: Option<bool>) -> bool;
fn update_status(&self, language: LanguageServerName, status: BinaryStatus);
fn registered_lsp_adapters(&self) -> Vec<Arc<dyn LspAdapter>>;
async fn language_server_download_dir(&self, name: &LanguageServerName) -> Option<Arc<Path>>;
@@ -335,6 +337,7 @@ pub trait LspAdapterDelegate: Send + Sync {
async fn shell_env(&self) -> HashMap<String, String>;
async fn read_text_file(&self, path: PathBuf) -> Result<String>;
async fn try_exec(&self, binary: LanguageServerBinary) -> Result<()>;
fn set_workspace_scope(&self, scope_uri: Option<lsp::Url>);
}
#[async_trait(?Send)]

View File

@@ -765,6 +765,8 @@ pub enum ShowWhitespaceSetting {
/// - It is adjacent to an edge (start or end)
/// - It is adjacent to a whitespace (left or right)
Boundary,
/// Draw whitespaces only after non-whitespace characters.
Trailing,
}
/// Controls which formatter should be used when formatting code.
@@ -1452,7 +1454,8 @@ impl settings::Settings for AllLanguageSettings {
vscode.bool_setting("editor.inlineSuggest.enabled", &mut d.show_edit_predictions);
vscode.enum_setting("editor.renderWhitespace", &mut d.show_whitespaces, |s| {
Some(match s {
"boundary" | "trailing" => ShowWhitespaceSetting::Boundary,
"boundary" => ShowWhitespaceSetting::Boundary,
"trailing" => ShowWhitespaceSetting::Trailing,
"selection" => ShowWhitespaceSetting::Selection,
"all" => ShowWhitespaceSetting::All,
_ => ShowWhitespaceSetting::None,

View File

@@ -1126,7 +1126,7 @@ impl<'a> SyntaxMapMatches<'a> {
&self.grammars
}
pub fn peek(&self) -> Option<SyntaxMapMatch> {
pub fn peek(&self) -> Option<SyntaxMapMatch<'_>> {
let layer = self.layers.first()?;
if !layer.has_next {
@@ -1550,7 +1550,7 @@ fn insert_newlines_between_ranges(
impl OwnedSyntaxLayer {
/// Returns the root syntax node for this layer.
pub fn node(&self) -> Node {
pub fn node(&self) -> Node<'_> {
self.tree
.root_node_with_offset(self.offset.0, self.offset.1)
}

View File

@@ -185,6 +185,7 @@ impl LanguageModel for FakeLanguageModel {
'static,
Result<
BoxStream<'static, Result<LanguageModelCompletionEvent, LanguageModelCompletionError>>,
LanguageModelCompletionError,
>,
> {
let (tx, rx) = mpsc::unbounded();

View File

@@ -22,6 +22,7 @@ use std::fmt;
use std::ops::{Add, Sub};
use std::str::FromStr as _;
use std::sync::Arc;
use std::time::Duration;
use thiserror::Error;
use util::serde::is_default;
use zed_llm_client::{
@@ -74,6 +75,8 @@ pub enum LanguageModelCompletionEvent {
#[derive(Error, Debug)]
pub enum LanguageModelCompletionError {
#[error("rate limit exceeded, retry after {0:?}")]
RateLimit(Duration),
#[error("received bad input JSON")]
BadInputJson {
id: LanguageModelToolUseId,
@@ -270,6 +273,7 @@ pub trait LanguageModel: Send + Sync {
'static,
Result<
BoxStream<'static, Result<LanguageModelCompletionEvent, LanguageModelCompletionError>>,
LanguageModelCompletionError,
>,
>;
@@ -277,7 +281,7 @@ pub trait LanguageModel: Send + Sync {
&self,
request: LanguageModelRequest,
cx: &AsyncApp,
) -> BoxFuture<'static, Result<LanguageModelTextStream>> {
) -> BoxFuture<'static, Result<LanguageModelTextStream, LanguageModelCompletionError>> {
let future = self.stream_completion(request, cx);
async move {

View File

@@ -1,4 +1,3 @@
use anyhow::Result;
use futures::Stream;
use smol::lock::{Semaphore, SemaphoreGuardArc};
use std::{
@@ -8,6 +7,8 @@ use std::{
task::{Context, Poll},
};
use crate::LanguageModelCompletionError;
#[derive(Clone)]
pub struct RateLimiter {
semaphore: Arc<Semaphore>,
@@ -36,9 +37,12 @@ impl RateLimiter {
}
}
pub fn run<'a, Fut, T>(&self, future: Fut) -> impl 'a + Future<Output = Result<T>>
pub fn run<'a, Fut, T>(
&self,
future: Fut,
) -> impl 'a + Future<Output = Result<T, LanguageModelCompletionError>>
where
Fut: 'a + Future<Output = Result<T>>,
Fut: 'a + Future<Output = Result<T, LanguageModelCompletionError>>,
{
let guard = self.semaphore.acquire_arc();
async move {
@@ -52,9 +56,12 @@ impl RateLimiter {
pub fn stream<'a, Fut, T>(
&self,
future: Fut,
) -> impl 'a + Future<Output = Result<impl Stream<Item = T::Item> + use<Fut, T>>>
) -> impl 'a
+ Future<
Output = Result<impl Stream<Item = T::Item> + use<Fut, T>, LanguageModelCompletionError>,
>
where
Fut: 'a + Future<Output = Result<T>>,
Fut: 'a + Future<Output = Result<T, LanguageModelCompletionError>>,
T: Stream,
{
let guard = self.semaphore.acquire_arc();

View File

@@ -387,22 +387,34 @@ impl AnthropicModel {
&self,
request: anthropic::Request,
cx: &AsyncApp,
) -> BoxFuture<'static, Result<BoxStream<'static, Result<anthropic::Event, AnthropicError>>>>
{
) -> BoxFuture<
'static,
Result<
BoxStream<'static, Result<anthropic::Event, AnthropicError>>,
LanguageModelCompletionError,
>,
> {
let http_client = self.http_client.clone();
let Ok((api_key, api_url)) = cx.read_entity(&self.state, |state, cx| {
let settings = &AllLanguageModelSettings::get_global(cx).anthropic;
(state.api_key.clone(), settings.api_url.clone())
}) else {
return futures::future::ready(Err(anyhow!("App state dropped"))).boxed();
return futures::future::ready(Err(anyhow!("App state dropped").into())).boxed();
};
async move {
let api_key = api_key.context("Missing Anthropic API Key")?;
let request =
anthropic::stream_completion(http_client.as_ref(), &api_url, &api_key, request);
request.await.context("failed to stream completion")
request.await.map_err(|err| match err {
AnthropicError::RateLimit(duration) => {
LanguageModelCompletionError::RateLimit(duration)
}
err @ (AnthropicError::ApiError(..) | AnthropicError::Other(..)) => {
LanguageModelCompletionError::Other(anthropic_err_to_anyhow(err))
}
})
}
.boxed()
}
@@ -473,6 +485,7 @@ impl LanguageModel for AnthropicModel {
'static,
Result<
BoxStream<'static, Result<LanguageModelCompletionEvent, LanguageModelCompletionError>>,
LanguageModelCompletionError,
>,
> {
let request = into_anthropic(
@@ -484,12 +497,7 @@ impl LanguageModel for AnthropicModel {
);
let request = self.stream_completion(request, cx);
let future = self.request_limiter.stream(async move {
let response = request
.await
.map_err(|err| match err.downcast::<AnthropicError>() {
Ok(anthropic_err) => anthropic_err_to_anyhow(anthropic_err),
Err(err) => anyhow!(err),
})?;
let response = request.await?;
Ok(AnthropicEventMapper::new().map_stream(response))
});
async move { Ok(future.await?.boxed()) }.boxed()

View File

@@ -527,6 +527,7 @@ impl LanguageModel for BedrockModel {
'static,
Result<
BoxStream<'static, Result<LanguageModelCompletionEvent, LanguageModelCompletionError>>,
LanguageModelCompletionError,
>,
> {
let Ok(region) = cx.read_entity(&self.state, |state, _cx| {
@@ -539,16 +540,13 @@ impl LanguageModel for BedrockModel {
.or(settings_region)
.unwrap_or(String::from("us-east-1"))
}) else {
return async move {
anyhow::bail!("App State Dropped");
}
.boxed();
return async move { Err(anyhow::anyhow!("App State Dropped").into()) }.boxed();
};
let model_id = match self.model.cross_region_inference_id(&region) {
Ok(s) => s,
Err(e) => {
return async move { Err(e) }.boxed();
return async move { Err(e.into()) }.boxed();
}
};
@@ -560,7 +558,7 @@ impl LanguageModel for BedrockModel {
self.model.mode(),
) {
Ok(request) => request,
Err(err) => return futures::future::ready(Err(err)).boxed(),
Err(err) => return futures::future::ready(Err(err.into())).boxed(),
};
let owned_handle = self.handler.clone();

View File

@@ -807,6 +807,7 @@ impl LanguageModel for CloudLanguageModel {
'static,
Result<
BoxStream<'static, Result<LanguageModelCompletionEvent, LanguageModelCompletionError>>,
LanguageModelCompletionError,
>,
> {
let thread_id = request.thread_id.clone();
@@ -848,7 +849,8 @@ impl LanguageModel for CloudLanguageModel {
mode,
provider: zed_llm_client::LanguageModelProvider::Anthropic,
model: request.model.clone(),
provider_request: serde_json::to_value(&request)?,
provider_request: serde_json::to_value(&request)
.map_err(|e| anyhow!(e))?,
},
)
.await
@@ -884,7 +886,7 @@ impl LanguageModel for CloudLanguageModel {
let client = self.client.clone();
let model = match open_ai::Model::from_id(&self.model.id.0) {
Ok(model) => model,
Err(err) => return async move { Err(anyhow!(err)) }.boxed(),
Err(err) => return async move { Err(anyhow!(err).into()) }.boxed(),
};
let request = into_open_ai(request, &model, None);
let llm_api_token = self.llm_api_token.clone();
@@ -905,7 +907,8 @@ impl LanguageModel for CloudLanguageModel {
mode,
provider: zed_llm_client::LanguageModelProvider::OpenAi,
model: request.model.clone(),
provider_request: serde_json::to_value(&request)?,
provider_request: serde_json::to_value(&request)
.map_err(|e| anyhow!(e))?,
},
)
.await?;
@@ -944,7 +947,8 @@ impl LanguageModel for CloudLanguageModel {
mode,
provider: zed_llm_client::LanguageModelProvider::Google,
model: request.model.model_id.clone(),
provider_request: serde_json::to_value(&request)?,
provider_request: serde_json::to_value(&request)
.map_err(|e| anyhow!(e))?,
},
)
.await?;

View File

@@ -265,13 +265,15 @@ impl LanguageModel for CopilotChatLanguageModel {
'static,
Result<
BoxStream<'static, Result<LanguageModelCompletionEvent, LanguageModelCompletionError>>,
LanguageModelCompletionError,
>,
> {
if let Some(message) = request.messages.last() {
if message.contents_empty() {
const EMPTY_PROMPT_MSG: &str =
"Empty prompts aren't allowed. Please provide a non-empty prompt.";
return futures::future::ready(Err(anyhow::anyhow!(EMPTY_PROMPT_MSG))).boxed();
return futures::future::ready(Err(anyhow::anyhow!(EMPTY_PROMPT_MSG).into()))
.boxed();
}
// Copilot Chat has a restriction that the final message must be from the user.
@@ -279,13 +281,13 @@ impl LanguageModel for CopilotChatLanguageModel {
// and provide a more helpful error message.
if !matches!(message.role, Role::User) {
const USER_ROLE_MSG: &str = "The final message must be from the user. To provide a system prompt, you must provide the system prompt followed by a user prompt.";
return futures::future::ready(Err(anyhow::anyhow!(USER_ROLE_MSG))).boxed();
return futures::future::ready(Err(anyhow::anyhow!(USER_ROLE_MSG).into())).boxed();
}
}
let copilot_request = match into_copilot_chat(&self.model, request) {
Ok(request) => request,
Err(err) => return futures::future::ready(Err(err)).boxed(),
Err(err) => return futures::future::ready(Err(err.into())).boxed(),
};
let is_streaming = copilot_request.stream;
@@ -863,6 +865,13 @@ impl Render for ConfigurationView {
copilot::initiate_sign_in(window, cx)
})),
)
.child(
Label::new(
format!("You can also assign the {} environment variable and restart Zed.", copilot::copilot_chat::COPILOT_OAUTH_ENV_VAR),
)
.size(LabelSize::Small)
.color(Color::Muted),
)
}
},
None => v_flex().gap_6().child(Label::new(ERROR_LABEL)),

View File

@@ -348,6 +348,7 @@ impl LanguageModel for DeepSeekLanguageModel {
'static,
Result<
BoxStream<'static, Result<LanguageModelCompletionEvent, LanguageModelCompletionError>>,
LanguageModelCompletionError,
>,
> {
let request = into_deepseek(request, &self.model, self.max_output_tokens());
@@ -372,15 +373,15 @@ pub fn into_deepseek(
for message in request.messages {
for content in message.content {
match content {
MessageContent::Text(text) | MessageContent::Thinking { text, .. } => messages
.push(match message.role {
Role::User => deepseek::RequestMessage::User { content: text },
Role::Assistant => deepseek::RequestMessage::Assistant {
content: Some(text),
tool_calls: Vec::new(),
},
Role::System => deepseek::RequestMessage::System { content: text },
}),
MessageContent::Text(text) => messages.push(match message.role {
Role::User => deepseek::RequestMessage::User { content: text },
Role::Assistant => deepseek::RequestMessage::Assistant {
content: Some(text),
tool_calls: Vec::new(),
},
Role::System => deepseek::RequestMessage::System { content: text },
}),
MessageContent::Thinking { .. } => {}
MessageContent::RedactedThinking(_) => {}
MessageContent::Image(_) => {}
MessageContent::ToolUse(tool_use) => {
@@ -485,6 +486,13 @@ impl DeepSeekEventMapper {
events.push(Ok(LanguageModelCompletionEvent::Text(content)));
}
if let Some(reasoning_content) = choice.delta.reasoning_content.clone() {
events.push(Ok(LanguageModelCompletionEvent::Thinking {
text: reasoning_content,
signature: None,
}));
}
if let Some(tool_calls) = choice.delta.tool_calls.as_ref() {
for tool_call in tool_calls {
let entry = self.tool_calls_by_index.entry(tool_call.index).or_default();

View File

@@ -409,6 +409,7 @@ impl LanguageModel for GoogleLanguageModel {
'static,
Result<LanguageModelCompletionEvent, LanguageModelCompletionError>,
>,
LanguageModelCompletionError,
>,
> {
let request = into_google(

View File

@@ -250,15 +250,15 @@ impl LmStudioLanguageModel {
for message in request.messages {
for content in message.content {
match content {
MessageContent::Text(text) | MessageContent::Thinking { text, .. } => messages
.push(match message.role {
Role::User => ChatMessage::User { content: text },
Role::Assistant => ChatMessage::Assistant {
content: Some(text),
tool_calls: Vec::new(),
},
Role::System => ChatMessage::System { content: text },
}),
MessageContent::Text(text) => messages.push(match message.role {
Role::User => ChatMessage::User { content: text },
Role::Assistant => ChatMessage::Assistant {
content: Some(text),
tool_calls: Vec::new(),
},
Role::System => ChatMessage::System { content: text },
}),
MessageContent::Thinking { .. } => {}
MessageContent::RedactedThinking(_) => {}
MessageContent::Image(_) => {}
MessageContent::ToolUse(tool_use) => {
@@ -420,6 +420,7 @@ impl LanguageModel for LmStudioLanguageModel {
'static,
Result<
BoxStream<'static, Result<LanguageModelCompletionEvent, LanguageModelCompletionError>>,
LanguageModelCompletionError,
>,
> {
let request = self.to_lmstudio_request(request);
@@ -471,6 +472,13 @@ impl LmStudioEventMapper {
events.push(Ok(LanguageModelCompletionEvent::Text(content)));
}
if let Some(reasoning_content) = choice.delta.reasoning_content {
events.push(Ok(LanguageModelCompletionEvent::Thinking {
text: reasoning_content,
signature: None,
}));
}
if let Some(tool_calls) = choice.delta.tool_calls {
for tool_call in tool_calls {
let entry = self.tool_calls_by_index.entry(tool_call.index).or_default();

View File

@@ -18,6 +18,8 @@ use language_model::{
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use settings::{Settings, SettingsStore};
use std::collections::HashMap;
use std::pin::Pin;
use std::str::FromStr;
use std::sync::Arc;
use strum::IntoEnumIterator;
@@ -27,9 +29,6 @@ use util::ResultExt;
use crate::{AllLanguageModelSettings, ui::InstructionListItem};
use std::collections::HashMap;
use std::pin::Pin;
const PROVIDER_ID: &str = "mistral";
const PROVIDER_NAME: &str = "Mistral";
@@ -48,6 +47,7 @@ pub struct AvailableModel {
pub max_output_tokens: Option<u32>,
pub max_completion_tokens: Option<u32>,
pub supports_tools: Option<bool>,
pub supports_images: Option<bool>,
}
pub struct MistralLanguageModelProvider {
@@ -215,6 +215,7 @@ impl LanguageModelProvider for MistralLanguageModelProvider {
max_output_tokens: model.max_output_tokens,
max_completion_tokens: model.max_completion_tokens,
supports_tools: model.supports_tools,
supports_images: model.supports_images,
},
);
}
@@ -314,7 +315,7 @@ impl LanguageModel for MistralLanguageModel {
}
fn supports_images(&self) -> bool {
false
self.model.supports_images()
}
fn telemetry_id(&self) -> String {
@@ -363,6 +364,7 @@ impl LanguageModel for MistralLanguageModel {
'static,
Result<
BoxStream<'static, Result<LanguageModelCompletionEvent, LanguageModelCompletionError>>,
LanguageModelCompletionError,
>,
> {
let request = into_mistral(
@@ -389,58 +391,113 @@ pub fn into_mistral(
let stream = true;
let mut messages = Vec::new();
for message in request.messages {
for content in message.content {
match content {
MessageContent::Text(text) | MessageContent::Thinking { text, .. } => messages
.push(match message.role {
Role::User => mistral::RequestMessage::User { content: text },
Role::Assistant => mistral::RequestMessage::Assistant {
content: Some(text),
tool_calls: Vec::new(),
},
Role::System => mistral::RequestMessage::System { content: text },
}),
MessageContent::RedactedThinking(_) => {}
MessageContent::Image(_) => {}
MessageContent::ToolUse(tool_use) => {
let tool_call = mistral::ToolCall {
id: tool_use.id.to_string(),
content: mistral::ToolCallContent::Function {
function: mistral::FunctionContent {
name: tool_use.name.to_string(),
arguments: serde_json::to_string(&tool_use.input)
.unwrap_or_default(),
},
},
};
if let Some(mistral::RequestMessage::Assistant { tool_calls, .. }) =
messages.last_mut()
{
tool_calls.push(tool_call);
} else {
messages.push(mistral::RequestMessage::Assistant {
content: None,
tool_calls: vec![tool_call],
});
for message in &request.messages {
match message.role {
Role::User => {
let mut message_content = mistral::MessageContent::empty();
for content in &message.content {
match content {
MessageContent::Text(text) => {
message_content
.push_part(mistral::MessagePart::Text { text: text.clone() });
}
MessageContent::Image(image_content) => {
message_content.push_part(mistral::MessagePart::ImageUrl {
image_url: image_content.to_base64_url(),
});
}
MessageContent::Thinking { text, .. } => {
message_content
.push_part(mistral::MessagePart::Text { text: text.clone() });
}
MessageContent::RedactedThinking(_) => {}
MessageContent::ToolUse(_) | MessageContent::ToolResult(_) => {
// Tool content is not supported in User messages for Mistral
}
}
}
MessageContent::ToolResult(tool_result) => {
let content = match &tool_result.content {
LanguageModelToolResultContent::Text(text) => text.to_string(),
LanguageModelToolResultContent::Image(_) => {
// TODO: Mistral image support
"[Tool responded with an image, but Zed doesn't support these in Mistral models yet]".to_string()
}
};
messages.push(mistral::RequestMessage::Tool {
content,
tool_call_id: tool_result.tool_use_id.to_string(),
if !matches!(message_content, mistral::MessageContent::Plain { ref content } if content.is_empty())
{
messages.push(mistral::RequestMessage::User {
content: message_content,
});
}
}
Role::Assistant => {
for content in &message.content {
match content {
MessageContent::Text(text) | MessageContent::Thinking { text, .. } => {
messages.push(mistral::RequestMessage::Assistant {
content: Some(text.clone()),
tool_calls: Vec::new(),
});
}
MessageContent::RedactedThinking(_) => {}
MessageContent::Image(_) => {}
MessageContent::ToolUse(tool_use) => {
let tool_call = mistral::ToolCall {
id: tool_use.id.to_string(),
content: mistral::ToolCallContent::Function {
function: mistral::FunctionContent {
name: tool_use.name.to_string(),
arguments: serde_json::to_string(&tool_use.input)
.unwrap_or_default(),
},
},
};
if let Some(mistral::RequestMessage::Assistant { tool_calls, .. }) =
messages.last_mut()
{
tool_calls.push(tool_call);
} else {
messages.push(mistral::RequestMessage::Assistant {
content: None,
tool_calls: vec![tool_call],
});
}
}
MessageContent::ToolResult(_) => {
// Tool results are not supported in Assistant messages
}
}
}
}
Role::System => {
for content in &message.content {
match content {
MessageContent::Text(text) | MessageContent::Thinking { text, .. } => {
messages.push(mistral::RequestMessage::System {
content: text.clone(),
});
}
MessageContent::RedactedThinking(_) => {}
MessageContent::Image(_)
| MessageContent::ToolUse(_)
| MessageContent::ToolResult(_) => {
// Images and tools are not supported in System messages
}
}
}
}
}
}
for message in &request.messages {
for content in &message.content {
if let MessageContent::ToolResult(tool_result) = content {
let content = match &tool_result.content {
LanguageModelToolResultContent::Text(text) => text.to_string(),
LanguageModelToolResultContent::Image(_) => {
"[Tool responded with an image, but Zed doesn't support these in Mistral models yet]".to_string()
}
};
messages.push(mistral::RequestMessage::Tool {
content,
tool_call_id: tool_result.tool_use_id.to_string(),
});
}
}
}
@@ -819,62 +876,88 @@ impl Render for ConfigurationView {
#[cfg(test)]
mod tests {
use super::*;
use language_model;
use language_model::{LanguageModelImage, LanguageModelRequestMessage, MessageContent};
#[test]
fn test_into_mistral_conversion() {
let request = language_model::LanguageModelRequest {
fn test_into_mistral_basic_conversion() {
let request = LanguageModelRequest {
messages: vec![
language_model::LanguageModelRequestMessage {
role: language_model::Role::System,
content: vec![language_model::MessageContent::Text(
"You are a helpful assistant.".to_string(),
)],
LanguageModelRequestMessage {
role: Role::System,
content: vec![MessageContent::Text("System prompt".into())],
cache: false,
},
language_model::LanguageModelRequestMessage {
role: language_model::Role::User,
content: vec![language_model::MessageContent::Text(
"Hello, how are you?".to_string(),
)],
LanguageModelRequestMessage {
role: Role::User,
content: vec![MessageContent::Text("Hello".into())],
cache: false,
},
],
temperature: Some(0.7),
tools: Vec::new(),
temperature: Some(0.5),
tools: vec![],
tool_choice: None,
thread_id: None,
prompt_id: None,
intent: None,
mode: None,
stop: Vec::new(),
stop: vec![],
};
let model_name = "mistral-medium-latest".to_string();
let max_output_tokens = Some(1000);
let mistral_request = into_mistral(request, model_name, max_output_tokens);
assert_eq!(mistral_request.model, "mistral-medium-latest");
assert_eq!(mistral_request.temperature, Some(0.7));
assert_eq!(mistral_request.max_tokens, Some(1000));
assert!(mistral_request.stream);
assert!(mistral_request.tools.is_empty());
assert!(mistral_request.tool_choice.is_none());
let mistral_request = into_mistral(request, "mistral-small-latest".into(), None);
assert_eq!(mistral_request.model, "mistral-small-latest");
assert_eq!(mistral_request.temperature, Some(0.5));
assert_eq!(mistral_request.messages.len(), 2);
assert!(mistral_request.stream);
}
match &mistral_request.messages[0] {
mistral::RequestMessage::System { content } => {
assert_eq!(content, "You are a helpful assistant.");
}
_ => panic!("Expected System message"),
}
#[test]
fn test_into_mistral_with_image() {
let request = LanguageModelRequest {
messages: vec![LanguageModelRequestMessage {
role: Role::User,
content: vec![
MessageContent::Text("What's in this image?".into()),
MessageContent::Image(LanguageModelImage {
source: "base64data".into(),
size: Default::default(),
}),
],
cache: false,
}],
tools: vec![],
tool_choice: None,
temperature: None,
thread_id: None,
prompt_id: None,
intent: None,
mode: None,
stop: vec![],
};
match &mistral_request.messages[1] {
mistral::RequestMessage::User { content } => {
assert_eq!(content, "Hello, how are you?");
let mistral_request = into_mistral(request, "pixtral-12b-latest".into(), None);
assert_eq!(mistral_request.messages.len(), 1);
assert!(matches!(
&mistral_request.messages[0],
mistral::RequestMessage::User {
content: mistral::MessageContent::Multipart { .. }
}
_ => panic!("Expected User message"),
));
if let mistral::RequestMessage::User {
content: mistral::MessageContent::Multipart { content },
} = &mistral_request.messages[0]
{
assert_eq!(content.len(), 2);
assert!(matches!(
&content[0],
mistral::MessagePart::Text { text } if text == "What's in this image?"
));
assert!(matches!(
&content[1],
mistral::MessagePart::ImageUrl { image_url } if image_url.starts_with("data:image/png;base64,")
));
}
}
}

View File

@@ -406,6 +406,7 @@ impl LanguageModel for OllamaLanguageModel {
'static,
Result<
BoxStream<'static, Result<LanguageModelCompletionEvent, LanguageModelCompletionError>>,
LanguageModelCompletionError,
>,
> {
let request = self.to_ollama_request(request);
@@ -415,7 +416,7 @@ impl LanguageModel for OllamaLanguageModel {
let settings = &AllLanguageModelSettings::get_global(cx).ollama;
settings.api_url.clone()
}) else {
return futures::future::ready(Err(anyhow!("App state dropped"))).boxed();
return futures::future::ready(Err(anyhow!("App state dropped").into())).boxed();
};
let future = self.request_limiter.stream(async move {

View File

@@ -339,6 +339,7 @@ impl LanguageModel for OpenAiLanguageModel {
'static,
Result<LanguageModelCompletionEvent, LanguageModelCompletionError>,
>,
LanguageModelCompletionError,
>,
> {
let request = into_open_ai(request, &self.model, self.max_output_tokens());

View File

@@ -367,6 +367,7 @@ impl LanguageModel for OpenRouterLanguageModel {
'static,
Result<LanguageModelCompletionEvent, LanguageModelCompletionError>,
>,
LanguageModelCompletionError,
>,
> {
let request = into_open_router(request, &self.model, self.max_output_tokens());

View File

@@ -0,0 +1,327 @@
use anyhow::{Context as _, Result};
use async_compression::futures::bufread::GzipDecoder;
use async_tar::Archive;
use async_trait::async_trait;
use gpui::{AsyncApp, SharedString};
use http_client::github::{AssetKind, GitHubLspBinaryVersion, build_asset_url};
use language::{
Attach, LanguageToolchainStore, LspAdapter, LspAdapterDelegate, ManifestName, ManifestProvider,
ManifestQuery,
};
use lsp::{CodeActionKind, LanguageServerBinary, LanguageServerName};
use node_runtime::NodeRuntime;
use project::{Fs, lsp_store::language_server_settings};
use serde_json::{Value, json};
use smol::{fs, io::BufReader, stream::StreamExt};
use std::{
any::Any,
ffi::OsString,
path::{Path, PathBuf},
sync::Arc,
};
use util::archive::extract_zip;
use util::{fs::remove_matching, merge_json_value_into};
fn eslint_server_binary_arguments(server_path: &Path) -> Vec<OsString> {
vec![
"--max-old-space-size=8192".into(),
server_path.into(),
"--stdio".into(),
]
}
pub struct EsLintLspAdapter {
node: NodeRuntime,
}
impl EsLintLspAdapter {
const CURRENT_VERSION: &'static str = "2.4.4";
const CURRENT_VERSION_TAG_NAME: &'static str = "release/2.4.4";
#[cfg(not(windows))]
const GITHUB_ASSET_KIND: AssetKind = AssetKind::TarGz;
#[cfg(windows)]
const GITHUB_ASSET_KIND: AssetKind = AssetKind::Zip;
const SERVER_PATH: &'static str = "vscode-eslint/server/out/eslintServer.js";
const SERVER_NAME: LanguageServerName = LanguageServerName::new_static("eslint");
const FLAT_CONFIG_FILE_NAMES: &'static [&'static str] = &[
"eslint.config.js",
"eslint.config.mjs",
"eslint.config.cjs",
"eslint.config.ts",
"eslint.config.cts",
"eslint.config.mts",
];
const LEGACY_CONFIG_FILE_NAMES: &'static [&'static str] = &[
".eslintrc.js",
".eslintrc.cjs",
".eslintrc.mjs",
".eslintrc.yaml",
".eslintrc.yml",
".eslintrc.json",
".eslintrc",
];
pub fn new(node: NodeRuntime) -> Self {
EsLintLspAdapter { node }
}
fn build_destination_path(container_dir: &Path) -> PathBuf {
container_dir.join(format!("vscode-eslint-{}", Self::CURRENT_VERSION))
}
}
pub(crate) struct EsLintConfigProvider;
impl ManifestProvider for EsLintConfigProvider {
fn name(&self) -> ManifestName {
SharedString::new_static("eslint").into()
}
fn search(
&self,
ManifestQuery {
path,
depth,
delegate,
}: ManifestQuery,
) -> Option<Arc<Path>> {
for path in path.ancestors().take(depth) {
for config_file in EsLintLspAdapter::FLAT_CONFIG_FILE_NAMES {
let config_path = path.join(config_file);
if delegate.exists(&config_path, Some(false)) {
return Some(path.into());
}
}
for config_file in EsLintLspAdapter::LEGACY_CONFIG_FILE_NAMES {
let config_path = path.join(config_file);
if delegate.exists(&config_path, Some(false)) {
return Some(path.into());
}
}
}
None
}
}
#[async_trait(?Send)]
impl LspAdapter for EsLintLspAdapter {
fn name(&self) -> LanguageServerName {
Self::SERVER_NAME.clone()
}
fn manifest_name(&self) -> Option<ManifestName> {
Some(SharedString::new_static("eslint").into())
}
fn code_action_kinds(&self) -> Option<Vec<CodeActionKind>> {
Some(vec![
CodeActionKind::QUICKFIX,
CodeActionKind::new("source.fixAll.eslint"),
])
}
async fn workspace_configuration(
self: Arc<Self>,
_: &dyn Fs,
delegate: &Arc<dyn LspAdapterDelegate>,
_: Arc<dyn LanguageToolchainStore>,
cx: &mut AsyncApp,
) -> Result<Value> {
let workspace_root = delegate.worktree_root_path();
dbg!(&delegate.worktree_root_path());
dbg!(&delegate.subproject_root_path());
let use_flat_config = Self::FLAT_CONFIG_FILE_NAMES
.iter()
.any(|file| workspace_root.join(file).is_file());
let mut default_workspace_configuration = json!({
"validate": "on",
"rulesCustomizations": [],
"run": "onType",
"nodePath": null,
"workingDirectory": {
"mode": "auto"
},
"workspaceFolder": {
"uri": workspace_root,
"name": workspace_root.file_name()
.unwrap_or(workspace_root.as_os_str())
.to_string_lossy(),
},
"problems": {},
"codeActionOnSave": {
// We enable this, but without also configuring code_actions_on_format
// in the Zed configuration, it doesn't have an effect.
"enable": true,
},
"codeAction": {
"disableRuleComment": {
"enable": true,
"location": "separateLine",
},
"showDocumentation": {
"enable": true
}
},
"experimental": {
"useFlatConfig": use_flat_config,
},
});
let override_options = cx.update(|cx| {
language_server_settings(delegate.as_ref(), &Self::SERVER_NAME, cx)
.and_then(|s| s.settings.clone())
})?;
if let Some(override_options) = override_options {
merge_json_value_into(override_options, &mut default_workspace_configuration);
}
Ok(json!({
"": default_workspace_configuration
}))
}
async fn fetch_latest_server_version(
&self,
_delegate: &dyn LspAdapterDelegate,
) -> Result<Box<dyn 'static + Send + Any>> {
let url = build_asset_url(
"zed-industries/vscode-eslint",
Self::CURRENT_VERSION_TAG_NAME,
Self::GITHUB_ASSET_KIND,
)?;
Ok(Box::new(GitHubLspBinaryVersion {
name: Self::CURRENT_VERSION.into(),
url,
}))
}
async fn fetch_server_binary(
&self,
version: Box<dyn 'static + Send + Any>,
container_dir: PathBuf,
delegate: &dyn LspAdapterDelegate,
) -> Result<LanguageServerBinary> {
let version = version.downcast::<GitHubLspBinaryVersion>().unwrap();
let destination_path = Self::build_destination_path(&container_dir);
let server_path = destination_path.join(Self::SERVER_PATH);
if fs::metadata(&server_path).await.is_err() {
remove_matching(&container_dir, |entry| entry != destination_path).await;
let mut response = delegate
.http_client()
.get(&version.url, Default::default(), true)
.await
.context("downloading release")?;
match Self::GITHUB_ASSET_KIND {
AssetKind::TarGz => {
let decompressed_bytes = GzipDecoder::new(BufReader::new(response.body_mut()));
let archive = Archive::new(decompressed_bytes);
archive.unpack(&destination_path).await.with_context(|| {
format!("extracting {} to {:?}", version.url, destination_path)
})?;
}
AssetKind::Gz => {
let mut decompressed_bytes =
GzipDecoder::new(BufReader::new(response.body_mut()));
let mut file =
fs::File::create(&destination_path).await.with_context(|| {
format!(
"creating a file {:?} for a download from {}",
destination_path, version.url,
)
})?;
futures::io::copy(&mut decompressed_bytes, &mut file)
.await
.with_context(|| {
format!("extracting {} to {:?}", version.url, destination_path)
})?;
}
AssetKind::Zip => {
extract_zip(&destination_path, response.body_mut())
.await
.with_context(|| {
format!("unzipping {} to {:?}", version.url, destination_path)
})?;
}
}
let mut dir = fs::read_dir(&destination_path).await?;
let first = dir.next().await.context("missing first file")??;
let repo_root = destination_path.join("vscode-eslint");
fs::rename(first.path(), &repo_root).await?;
#[cfg(target_os = "windows")]
{
handle_symlink(
repo_root.join("$shared"),
repo_root.join("client").join("src").join("shared"),
)
.await?;
handle_symlink(
repo_root.join("$shared"),
repo_root.join("server").join("src").join("shared"),
)
.await?;
}
self.node
.run_npm_subcommand(&repo_root, "install", &[])
.await?;
self.node
.run_npm_subcommand(&repo_root, "run-script", &["compile"])
.await?;
}
Ok(LanguageServerBinary {
path: self.node.binary_path().await?,
env: None,
arguments: eslint_server_binary_arguments(&server_path),
})
}
async fn cached_server_binary(
&self,
container_dir: PathBuf,
_: &dyn LspAdapterDelegate,
) -> Option<LanguageServerBinary> {
let server_path =
Self::build_destination_path(&container_dir).join(EsLintLspAdapter::SERVER_PATH);
Some(LanguageServerBinary {
path: self.node.binary_path().await.ok()?,
env: None,
arguments: eslint_server_binary_arguments(&server_path),
})
}
}
#[cfg(target_os = "windows")]
async fn handle_symlink(src_dir: PathBuf, dest_dir: PathBuf) -> Result<()> {
anyhow::ensure!(
fs::metadata(&src_dir).await.is_ok(),
"Directory {src_dir:?} is not present"
);
if fs::metadata(&dest_dir).await.is_ok() {
fs::remove_file(&dest_dir).await?;
}
fs::create_dir_all(&dest_dir).await?;
let mut entries = fs::read_dir(&src_dir).await?;
while let Some(entry) = entries.try_next().await? {
let entry_path = entry.path();
let entry_name = entry.file_name();
let dest_path = dest_dir.join(&entry_name);
fs::copy(&entry_path, &dest_path).await?;
}
Ok(())
}

View File

@@ -80,4 +80,21 @@
)
) @item
; Add support for parameterized tests
(
(call_expression
function: (call_expression
function: (member_expression
object: [(identifier) @_name (member_expression object: (identifier) @_name)]
property: (property_identifier) @_property
)
(#any-of? @_name "it" "test" "describe" "context" "suite")
(#eq? @_property "each")
)
arguments: (
arguments . (string (string_fragment) @name)
)
)
) @item
(comment) @annotation

View File

@@ -19,3 +19,22 @@
(#set! tag js-test)
)
; Add support for parameterized tests
(
(call_expression
function: (call_expression
function: (member_expression
object: [(identifier) @_name (member_expression object: (identifier) @_name)]
property: (property_identifier) @_property
)
(#any-of? @_name "it" "test" "describe" "context" "suite")
(#eq? @_property "each")
)
arguments: (
arguments . (string (string_fragment) @run)
)
) @_js-test
(#set! tag js-test)
)

View File

@@ -1,4 +1,5 @@
use anyhow::Context as _;
use eslint::EsLintConfigProvider;
use gpui::{App, UpdateGlobal};
use json::json_task_context;
use node_runtime::NodeRuntime;
@@ -15,6 +16,7 @@ pub use language::*;
mod bash;
mod c;
mod css;
mod eslint;
mod go;
mod json;
mod python;
@@ -75,7 +77,7 @@ pub fn init(languages: Arc<LanguageRegistry>, node: NodeRuntime, cx: &mut App) {
let c_lsp_adapter = Arc::new(c::CLspAdapter);
let css_lsp_adapter = Arc::new(css::CssLspAdapter::new(node.clone()));
let eslint_adapter = Arc::new(typescript::EsLintLspAdapter::new(node.clone()));
let eslint_adapter = Arc::new(eslint::EsLintLspAdapter::new(node.clone()));
let go_context_provider = Arc::new(go::GoContextProvider);
let go_lsp_adapter = Arc::new(go::GoLspAdapter);
let json_context_provider = Arc::new(json_task_context());
@@ -303,9 +305,10 @@ pub fn init(languages: Arc<LanguageRegistry>, node: NodeRuntime, cx: &mut App) {
anyhow::Ok(())
})
.detach();
let manifest_providers: [Arc<dyn ManifestProvider>; 2] = [
let manifest_providers: [Arc<dyn ManifestProvider>; 3] = [
Arc::from(CargoManifestProvider),
Arc::from(PyprojectTomlManifestProvider),
Arc::from(EsLintConfigProvider),
];
for provider in manifest_providers {
project::ManifestProviders::global(cx).register(provider);

View File

@@ -1,11 +1,8 @@
use anyhow::{Context as _, Result};
use async_compression::futures::bufread::GzipDecoder;
use async_tar::Archive;
use async_trait::async_trait;
use chrono::{DateTime, Local};
use collections::HashMap;
use gpui::{App, AppContext, AsyncApp, Task};
use http_client::github::{AssetKind, GitHubLspBinaryVersion, build_asset_url};
use language::{
ContextLocation, ContextProvider, File, LanguageToolchainStore, LspAdapter, LspAdapterDelegate,
};
@@ -13,7 +10,7 @@ use lsp::{CodeActionKind, LanguageServerBinary, LanguageServerName};
use node_runtime::NodeRuntime;
use project::{Fs, lsp_store::language_server_settings};
use serde_json::{Value, json};
use smol::{fs, io::BufReader, lock::RwLock, stream::StreamExt};
use smol::lock::RwLock;
use std::{
any::Any,
borrow::Cow,
@@ -22,9 +19,7 @@ use std::{
sync::Arc,
};
use task::{TaskTemplate, TaskTemplates, TaskVariables, VariableName};
use util::archive::extract_zip;
use util::merge_json_value_into;
use util::{ResultExt, fs::remove_matching, maybe};
use util::{ResultExt, maybe};
pub(crate) struct TypeScriptContextProvider {
last_package_json: PackageJsonContents,
@@ -34,11 +29,15 @@ const TYPESCRIPT_RUNNER_VARIABLE: VariableName =
VariableName::Custom(Cow::Borrowed("TYPESCRIPT_RUNNER"));
const TYPESCRIPT_JEST_TASK_VARIABLE: VariableName =
VariableName::Custom(Cow::Borrowed("TYPESCRIPT_JEST"));
const TYPESCRIPT_JEST_TEST_NAME_VARIABLE: VariableName =
VariableName::Custom(Cow::Borrowed("TYPESCRIPT_JEST_TEST_NAME"));
const TYPESCRIPT_MOCHA_TASK_VARIABLE: VariableName =
VariableName::Custom(Cow::Borrowed("TYPESCRIPT_MOCHA"));
const TYPESCRIPT_VITEST_TASK_VARIABLE: VariableName =
VariableName::Custom(Cow::Borrowed("TYPESCRIPT_VITEST"));
const TYPESCRIPT_VITEST_TEST_NAME_VARIABLE: VariableName =
VariableName::Custom(Cow::Borrowed("TYPESCRIPT_VITEST_TEST_NAME"));
const TYPESCRIPT_JASMINE_TASK_VARIABLE: VariableName =
VariableName::Custom(Cow::Borrowed("TYPESCRIPT_JASMINE"));
const TYPESCRIPT_BUILD_SCRIPT_TASK_VARIABLE: VariableName =
@@ -183,7 +182,10 @@ impl ContextProvider for TypeScriptContextProvider {
args: vec![
TYPESCRIPT_JEST_TASK_VARIABLE.template_value(),
"--testNamePattern".to_owned(),
format!("\"{}\"", VariableName::Symbol.template_value()),
format!(
"\"{}\"",
TYPESCRIPT_JEST_TEST_NAME_VARIABLE.template_value()
),
VariableName::RelativeFile.template_value(),
],
tags: vec![
@@ -221,7 +223,7 @@ impl ContextProvider for TypeScriptContextProvider {
TYPESCRIPT_VITEST_TASK_VARIABLE.template_value(),
"run".to_owned(),
"--testNamePattern".to_owned(),
format!("\"{}\"", VariableName::Symbol.template_value()),
format!("\"{}\"", TYPESCRIPT_VITEST_TASK_VARIABLE.template_value()),
VariableName::RelativeFile.template_value(),
],
tags: vec![
@@ -344,14 +346,27 @@ impl ContextProvider for TypeScriptContextProvider {
fn build_context(
&self,
_variables: &task::TaskVariables,
current_vars: &task::TaskVariables,
location: ContextLocation<'_>,
_project_env: Option<HashMap<String, String>>,
_toolchains: Arc<dyn LanguageToolchainStore>,
cx: &mut App,
) -> Task<Result<task::TaskVariables>> {
let mut vars = task::TaskVariables::default();
if let Some(symbol) = current_vars.get(&VariableName::Symbol) {
vars.insert(
TYPESCRIPT_JEST_TEST_NAME_VARIABLE,
replace_test_name_parameters(symbol),
);
vars.insert(
TYPESCRIPT_VITEST_TEST_NAME_VARIABLE,
replace_test_name_parameters(symbol),
);
}
let Some((fs, worktree_root)) = location.fs.zip(location.worktree_root) else {
return Task::ready(Ok(task::TaskVariables::default()));
return Task::ready(Ok(vars));
};
let package_json_contents = self.last_package_json.clone();
@@ -361,7 +376,10 @@ impl ContextProvider for TypeScriptContextProvider {
.context("package.json context retrieval")
.log_err()
.unwrap_or_else(task::TaskVariables::default);
Ok(variables)
vars.extend(variables);
Ok(vars)
})
}
}
@@ -426,6 +444,12 @@ fn eslint_server_binary_arguments(server_path: &Path) -> Vec<OsString> {
]
}
fn replace_test_name_parameters(test_name: &str) -> String {
let pattern = regex::Regex::new(r"(%|\$)[0-9a-zA-Z]+").unwrap();
pattern.replace_all(test_name, "(.+?)").to_string()
}
pub struct TypeScriptLspAdapter {
node: NodeRuntime,
}
@@ -684,250 +708,6 @@ async fn get_cached_ts_server_binary(
.log_err()
}
pub struct EsLintLspAdapter {
node: NodeRuntime,
}
impl EsLintLspAdapter {
const CURRENT_VERSION: &'static str = "2.4.4";
const CURRENT_VERSION_TAG_NAME: &'static str = "release/2.4.4";
#[cfg(not(windows))]
const GITHUB_ASSET_KIND: AssetKind = AssetKind::TarGz;
#[cfg(windows)]
const GITHUB_ASSET_KIND: AssetKind = AssetKind::Zip;
const SERVER_PATH: &'static str = "vscode-eslint/server/out/eslintServer.js";
const SERVER_NAME: LanguageServerName = LanguageServerName::new_static("eslint");
const FLAT_CONFIG_FILE_NAMES: &'static [&'static str] = &[
"eslint.config.js",
"eslint.config.mjs",
"eslint.config.cjs",
"eslint.config.ts",
"eslint.config.cts",
"eslint.config.mts",
];
pub fn new(node: NodeRuntime) -> Self {
EsLintLspAdapter { node }
}
fn build_destination_path(container_dir: &Path) -> PathBuf {
container_dir.join(format!("vscode-eslint-{}", Self::CURRENT_VERSION))
}
}
#[async_trait(?Send)]
impl LspAdapter for EsLintLspAdapter {
fn code_action_kinds(&self) -> Option<Vec<CodeActionKind>> {
Some(vec![
CodeActionKind::QUICKFIX,
CodeActionKind::new("source.fixAll.eslint"),
])
}
async fn workspace_configuration(
self: Arc<Self>,
_: &dyn Fs,
delegate: &Arc<dyn LspAdapterDelegate>,
_: Arc<dyn LanguageToolchainStore>,
cx: &mut AsyncApp,
) -> Result<Value> {
let workspace_root = delegate.worktree_root_path();
let use_flat_config = Self::FLAT_CONFIG_FILE_NAMES
.iter()
.any(|file| workspace_root.join(file).is_file());
let mut default_workspace_configuration = json!({
"validate": "on",
"rulesCustomizations": [],
"run": "onType",
"nodePath": null,
"workingDirectory": {
"mode": "auto"
},
"workspaceFolder": {
"uri": workspace_root,
"name": workspace_root.file_name()
.unwrap_or(workspace_root.as_os_str())
.to_string_lossy(),
},
"problems": {},
"codeActionOnSave": {
// We enable this, but without also configuring code_actions_on_format
// in the Zed configuration, it doesn't have an effect.
"enable": true,
},
"codeAction": {
"disableRuleComment": {
"enable": true,
"location": "separateLine",
},
"showDocumentation": {
"enable": true
}
},
"experimental": {
"useFlatConfig": use_flat_config,
},
});
let override_options = cx.update(|cx| {
language_server_settings(delegate.as_ref(), &Self::SERVER_NAME, cx)
.and_then(|s| s.settings.clone())
})?;
if let Some(override_options) = override_options {
merge_json_value_into(override_options, &mut default_workspace_configuration);
}
Ok(json!({
"": default_workspace_configuration
}))
}
fn name(&self) -> LanguageServerName {
Self::SERVER_NAME.clone()
}
async fn fetch_latest_server_version(
&self,
_delegate: &dyn LspAdapterDelegate,
) -> Result<Box<dyn 'static + Send + Any>> {
let url = build_asset_url(
"zed-industries/vscode-eslint",
Self::CURRENT_VERSION_TAG_NAME,
Self::GITHUB_ASSET_KIND,
)?;
Ok(Box::new(GitHubLspBinaryVersion {
name: Self::CURRENT_VERSION.into(),
url,
}))
}
async fn fetch_server_binary(
&self,
version: Box<dyn 'static + Send + Any>,
container_dir: PathBuf,
delegate: &dyn LspAdapterDelegate,
) -> Result<LanguageServerBinary> {
let version = version.downcast::<GitHubLspBinaryVersion>().unwrap();
let destination_path = Self::build_destination_path(&container_dir);
let server_path = destination_path.join(Self::SERVER_PATH);
if fs::metadata(&server_path).await.is_err() {
remove_matching(&container_dir, |entry| entry != destination_path).await;
let mut response = delegate
.http_client()
.get(&version.url, Default::default(), true)
.await
.context("downloading release")?;
match Self::GITHUB_ASSET_KIND {
AssetKind::TarGz => {
let decompressed_bytes = GzipDecoder::new(BufReader::new(response.body_mut()));
let archive = Archive::new(decompressed_bytes);
archive.unpack(&destination_path).await.with_context(|| {
format!("extracting {} to {:?}", version.url, destination_path)
})?;
}
AssetKind::Gz => {
let mut decompressed_bytes =
GzipDecoder::new(BufReader::new(response.body_mut()));
let mut file =
fs::File::create(&destination_path).await.with_context(|| {
format!(
"creating a file {:?} for a download from {}",
destination_path, version.url,
)
})?;
futures::io::copy(&mut decompressed_bytes, &mut file)
.await
.with_context(|| {
format!("extracting {} to {:?}", version.url, destination_path)
})?;
}
AssetKind::Zip => {
extract_zip(&destination_path, response.body_mut())
.await
.with_context(|| {
format!("unzipping {} to {:?}", version.url, destination_path)
})?;
}
}
let mut dir = fs::read_dir(&destination_path).await?;
let first = dir.next().await.context("missing first file")??;
let repo_root = destination_path.join("vscode-eslint");
fs::rename(first.path(), &repo_root).await?;
#[cfg(target_os = "windows")]
{
handle_symlink(
repo_root.join("$shared"),
repo_root.join("client").join("src").join("shared"),
)
.await?;
handle_symlink(
repo_root.join("$shared"),
repo_root.join("server").join("src").join("shared"),
)
.await?;
}
self.node
.run_npm_subcommand(&repo_root, "install", &[])
.await?;
self.node
.run_npm_subcommand(&repo_root, "run-script", &["compile"])
.await?;
}
Ok(LanguageServerBinary {
path: self.node.binary_path().await?,
env: None,
arguments: eslint_server_binary_arguments(&server_path),
})
}
async fn cached_server_binary(
&self,
container_dir: PathBuf,
_: &dyn LspAdapterDelegate,
) -> Option<LanguageServerBinary> {
let server_path =
Self::build_destination_path(&container_dir).join(EsLintLspAdapter::SERVER_PATH);
Some(LanguageServerBinary {
path: self.node.binary_path().await.ok()?,
env: None,
arguments: eslint_server_binary_arguments(&server_path),
})
}
}
#[cfg(target_os = "windows")]
async fn handle_symlink(src_dir: PathBuf, dest_dir: PathBuf) -> Result<()> {
anyhow::ensure!(
fs::metadata(&src_dir).await.is_ok(),
"Directory {src_dir:?} is not present"
);
if fs::metadata(&dest_dir).await.is_ok() {
fs::remove_file(&dest_dir).await?;
}
fs::create_dir_all(&dest_dir).await?;
let mut entries = fs::read_dir(&src_dir).await?;
while let Some(entry) = entries.try_next().await? {
let entry_path = entry.path();
let entry_name = entry.file_name();
let dest_path = dest_dir.join(&entry_name);
fs::copy(&entry_path, &dest_path).await?;
}
Ok(())
}
#[cfg(test)]
mod tests {
use gpui::{AppContext as _, TestAppContext};

View File

@@ -88,4 +88,21 @@
)
) @item
; Add support for parameterized tests
(
(call_expression
function: (call_expression
function: (member_expression
object: [(identifier) @_name (member_expression object: (identifier) @_name)]
property: (property_identifier) @_property
)
(#any-of? @_name "it" "test" "describe" "context" "suite")
(#any-of? @_property "each")
)
arguments: (
arguments . (string (string_fragment) @name)
)
)
) @item
(comment) @annotation

View File

@@ -19,3 +19,22 @@
(#set! tag js-test)
)
; Add support for parameterized tests
(
(call_expression
function: (call_expression
function: (member_expression
object: [(identifier) @_name (member_expression object: (identifier) @_name)]
property: (property_identifier) @_property
)
(#any-of? @_name "it" "test" "describe" "context" "suite")
(#any-of? @_property "each")
)
arguments: (
arguments . (string (string_fragment) @run)
)
) @_js-test
(#set! tag js-test)
)

View File

@@ -412,7 +412,7 @@ impl libwebrtc::native::audio_mixer::AudioMixerSource for AudioMixerSource {
self.sample_rate
}
fn get_audio_frame_with_info<'a>(&self, target_sample_rate: u32) -> Option<AudioFrame> {
fn get_audio_frame_with_info<'a>(&self, target_sample_rate: u32) -> Option<AudioFrame<'_>> {
assert_eq!(self.sample_rate, target_sample_rate);
let buf = self.buffer.lock().pop_front()?;
Some(AudioFrame {

View File

@@ -277,6 +277,8 @@ pub struct ResponseMessageDelta {
pub role: Option<Role>,
pub content: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub reasoning_content: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub tool_calls: Option<Vec<ToolCallChunk>>,
}

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