Compare commits

..

196 Commits

Author SHA1 Message Date
Julia Ryan
0ebf440247 Temporary change to test job 2025-09-03 13:11:13 -07:00
Julia Ryan
70c05262f4 Don't fail on error 2025-09-03 13:11:13 -07:00
Julia Ryan
82e55e4145 Change project name 2025-09-03 13:11:12 -07:00
Julia Ryan
fee9cf4101 Add deploy step 2025-09-03 13:11:12 -07:00
Julia Ryan
143d9fc95e Add TODO 2025-09-03 13:11:12 -07:00
Julia Ryan
3d6b46cf9a ci: Add nightly action to build gpui docs 2025-09-03 13:11:12 -07:00
localcc
bb2d833373 Revert "gpui: Fix overflow_hidden to support clip with border radius" (#37480)
This reverts commit 40199266b6.

The issue with the commit is: ContentMask<Pixels>::intersect is doing
intersection of corner radii which makes inner containers use the max
corner radius out of all the parents when it should be more complex to
correctly clip children (clip sorting..?)

Release Notes:

- N/A
2025-09-03 19:52:47 +00:00
Cole Miller
eedfc5be5a acp: Improve handling of invalid external agent server downloads (#37465)
Related to #37213, #37150

When listing previously-downloaded versions of an external agent, don't
try to use any downloads that are missing the agent entrypoint
(indicating that they're corrupt/unusable), and delete those versions,
so that we can attempt to download the latest version again.

Also report clearer errors when failing to start a session due to an
agent server entrypoint or root directory not existing.

Release Notes:

- N/A
2025-09-03 15:47:39 -04:00
Agus Zubiaga
0e76cc8036 acp: Display a new version call out when one is available (#37479)
<img width="500" alt="CleanShot 2025-09-03 at 16 13 59@2x"
src="https://github.com/user-attachments/assets/beb91365-28e2-4f87-a2c5-7136d37382c7"></img>



Release Notes:

- Agent Panel: Display a callout when a new version of an external agent
is available

---------

Co-authored-by: Cole Miller <cole@zed.dev>
2025-09-03 19:39:04 +00:00
Ben Kunkle
6bd5251882 settings_ui: Add test for default values (#37466)
Closes #ISSUE

Adds a test that checks that all settings have default values in
`default.json`. Currently only tests that settings supported by
SettingsUi have defaults, as more settings are added to the settings
editor they will be added to the test as well.

Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-09-03 15:25:30 -04:00
Smit Barmase
13de400a2a editor: Do not correct text contrast on non-opaque editor (#37471)
We don’t know the background color behind a non-opaque editor, so we
should skip contrast correction in that case. This prevents
single-editor mode (which is always transparent) from showing weird text
colors when text is selected.

We can’t account for the actual background during contrast correction
because we compute contrast outside gpui, while the actual color
blending happens inside gpui during drawing.

<img width="522" height="145" alt="image"
src="https://github.com/user-attachments/assets/6ee71475-f666-482d-87e6-15cf4c4fceef"
/>

Release Notes:

- Fixed an issue where Command Palette text looked faded when selected.
2025-09-04 00:03:48 +05:30
Danilo Leal
c3480c3d6f docs: Update external agents content (#37413)
Release Notes:

- N/A

---------

Co-authored-by: Bennet Bo Fenner <bennetbo@gmx.de>
2025-09-03 09:59:49 -05:00
Kirill Bulatov
0cbacb8500 Make word deletions less greedy (#37352)
Closes https://github.com/zed-industries/zed/issues/37144

Adjusts `editor::DeleteToPreviousWordStart`,
`editor::DeleteToNextWordEnd`, `editor::DeleteToNextSubwordEnd` and
`editor::DeleteToPreviousSubwordStart` actions to

* take whitespace sequences with length >= 2 into account and stop after
removing them (whilst movement would also include the word after such
sequences)

* take current language's brackets into account and stop after removing
the text before them

The latter is configurable and can be disabled with `"ignore_brackets":
true` parameter in the action.

Release Notes:

- Improved word deletions to consider whitespace sequences and brackets
by default
2025-09-03 17:48:17 +03:00
Moritz von Göwels
7327ef662b terminal_view: Fix focusing of center-pane terminals (#37359)
With `reveal_stragegy=always` + `reveal_target=center`,
`TerminalPanel::spawn_task` activates & focuses the pane of the task.
This works fine in the terminal pane but doesn't for
`reveal_target=center`.

Please note: I'm not verified familiar with the architecture and
internal APIs of zed. If there's a better way or if this fix is a bad
idea, I'm fine with adapting this 😃

Closes #35908

Release Notes:

- Fixed task focus when re-spawning a task with `reveal_target=center`

---------

Co-authored-by: Marshall Bowers <git@maxdeviant.com>
2025-09-03 14:23:46 +00:00
Lukas Wirth
c1ca7303a8 editor: Make blame and inline blame work for multibuffers (#37366)
Release Notes:

- Added blame view and inline blame support for multi buffer editors

---------

Co-authored-by: Kirill Bulatov <kirill@zed.dev>
2025-09-03 14:22:35 +00:00
localcc
92283285ae Fix rendering on devices that don't support MapOnDefaultTextures (#37456)
Closes #37231

Release Notes:

- N/A
2025-09-03 14:14:56 +00:00
Lukas Wirth
d80f9dda75 languages: Fix python tasks failing when binary contains whitespaces (#37454)
Fixes https://github.com/zed-industries/zed/issues/33459

Release Notes:

- Fixed python tasks failing when the python binary path contains
whitespaces
2025-09-03 16:11:36 +02:00
Nia
ebc22c290b gpui: Don't risk accidentally panicking during tests (#37457)
See the failure in
https://github.com/zed-industries/zed/actions/runs/17413839503/job/49437345296

Release Notes:

- N/A
2025-09-03 15:44:07 +02:00
Bennet Bo Fenner
7633bbf55a acp: Fix issue with claude code /logout command (#37452)
### First issue

In the scenario where you have an API key configured in Zed and you run
`/logout`, clicking on `Use Anthropic API Key` would show `Method not
implemented`.

This happened because we were only intercepting the `Use Anthropic API
Key` click if the provider was NOT authenticated, which would not be the
case when the user has an API key set.

### Second issue

When clicking on `Reset API Key` the modal would be dismissed even
though you picked no Authentication Method (which means you still would
be unauthenticated)

---

This PR fixes both of these issues

Release Notes:

- N/A
2025-09-03 12:08:48 +00:00
Bennet Bo Fenner
91cbb2ec25 Add onboarding banner for claude code support (#37443)
Release Notes:

- N/A

---------

Co-authored-by: Danilo Leal <daniloleal09@gmail.com>
2025-09-03 10:59:14 +00:00
Jason Lee
40199266b6 gpui: Fix overflow_hidden to support clip with border radius (#35083)
Release Notes:

- N/A

---

Same case in HTML example:


https://developer.mozilla.org/en-US/play?id=p7FhB3JAhiVfLHAXnsbrn7JYYX%2Byq1gje%2B%2BTZarnXvvjmaAx3NlrXqMAoI35s4zeakShKee6lydHYeHr

```html
<div style="padding: 50px; text-align: center;">
  <div style="overflow: hidden; border-radius: 24px">
    <div style="background: #000; border: 3px solid red; color: #fff; padding: 8px 28px;">
      Let build applications with GPUI.
    </div>
    <div style="background: #333; border: 3px dashed black; color: #fff; padding: 8px 28px;">
      Let build applications with GPUI.
    </div>
  </div>

  <div style="margin-top: 20px; border-radius: 24px">
    <div style="background: #000; color: #fff; padding: 8px 28px;">
      This is not overflow: hidden.
    </div>
  </div>
</div>
```

<img width="610" height="213" alt="image"
src="https://github.com/user-attachments/assets/5f95e263-e52c-414f-8f0c-e6aa04ceb802"
/>

### Before

<img width="912" height="740" alt="image"
src="https://github.com/user-attachments/assets/f09c1936-52fc-4381-9a50-93977e9d64a6"
/>

### After 

```bash
cargo run -p gpui --example content_mask
```

<img width="912" height="740" alt="image"
src="https://github.com/user-attachments/assets/4bde58f3-c850-418d-9dc7-d2245852e7d7"
/> |


- [x] Metal
- [x] Blade
- [x] DirectX
- [x] ContentMask radius must reduce the container border widths.
- [x] The dash border render not correct, when not all side have
borders.
2025-09-03 12:44:33 +02:00
Danilo Leal
9a8c5053c2 agent: Update message editor placeholder (#37441)
Release Notes:

- N/A
2025-09-03 06:54:31 -03:00
localcc
c446662862 Fix font rendering at very large scales (#37440)
Release Notes:

- Fixed fonts disappearing at very large scales on windows
2025-09-03 09:21:45 +00:00
Finn Evers
6feae92616 rust: Improve highlighting in derive macros (#37439)
Follow-up to https://github.com/zed-industries/zed/pull/37049

This fixes an issue where we would lose highlighting in derive macros if
one of the names was qualified.

| Before | After |
| --- | --- |
| <img width="886" height="398" alt="Bildschirmfoto 2025-09-03 um 10 39
25"
src="https://github.com/user-attachments/assets/dbc680e3-6ce3-4059-9934-9daa4c59d4a0"
/> | <img width="886" height="398" alt="Bildschirmfoto 2025-09-03 um 10
38 14"
src="https://github.com/user-attachments/assets/6e10df6f-5158-4bfd-81ab-8f2b384f1e99"
/> |


Release Notes:

- N/A
2025-09-03 09:02:21 +00:00
Cole Miller
ae840c6ef3 acp: Fix handling of single-file worktrees (#37412)
When the first visible worktree is a single-file worktree, we would
previously try to use the absolute path of that file as the root
directory for external agents, causing an error. This PR changes how we
handle this situation: we'll use the root of the first non-single-file
visible worktree if there are any, and if there are none, the parent
directory of the first single-file visible worktree.

Related to #37213

Release Notes:

- acp: Fixed being unable to run external agents when a single file (not
part of a project) was opened in Zed.
2025-09-03 03:40:14 -04:00
Michael Sloan
d7fd5910d7 Use slice from Rope chunk when possible while iterating lines (#37430)
Release Notes:

- N/A
2025-09-03 06:35:31 +00:00
Kirill Bulatov
8d5861322b Allow wrapping markdown text into * by selecting text and writing the * (#37426)
Release Notes:

- Allowed wrapping markdown text into `*` by selecting text and writing
the `*`
2025-09-03 05:50:53 +00:00
Jakub Konka
5a9e18603d gpui: Fix intra rustdoc links (#37320)
The only warnings remaining are links to private modules/items, but I
lack knowledge to work out if the referenced modules/items should be
made public, or if the links should be rewritten into exposed
traits/items.

Links to associated items such as trait implementations have to be
written using full markdown format such as:

... [[ `App::update_global` ]](( BorrowAppContext::update_global ))

This is due to https://github.com/rust-lang/rust/issues/74563 which
sadly prohibits fully-qualified syntax:

... [[ `<App as BorrowAppContext>::update_global` ]]

Release Notes:

- N/A

Probably related to https://github.com/zed-industries/zed/pull/37072
2025-09-03 07:31:48 +02:00
chris
2a7761fe17 Instruct macOS users to run xcodebuild -downloadComponent MetalToolchain (#37411)
Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>

Closes #ISSUE

Release Notes:

- N/A

Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
2025-09-02 21:36:36 -07:00
Max Brunsfeld
f23096034b Remove wsl command line args on non-windows platforms (#37422)
Release Notes:

- N/A
2025-09-03 03:49:04 +00:00
Peter Tripp
1ed17fdd94 Bump Zed to v0.204 (#37415)
Release Notes:

-N/A
2025-09-02 21:00:19 -04:00
Marshall Bowers
7ea7f4e767 reqwest_client: Remove example (#37410)
This PR removes the example from the `reqwest_client` crate, as it
doesn't seem worth maintaining.

Release Notes:

- N/A
2025-09-03 00:52:04 +00:00
Peter Tripp
035d7ddcf8 ci: Skip Nix for commits on release branches and tags (#37407)
When doing stable/preview releases simultaneously there are two tags and
two branches pushed. Previously nix was attempting 1 job for each. Our
current mac parallelism is 4.
 
Can't easily test this. 🤷 

Release Notes:

- N/A
2025-09-02 20:37:40 -04:00
Danilo Leal
9d67276090 agent: Fix cut off slash command descriptions (#37408)
Release Notes:

- N/A
2025-09-03 00:28:35 +00:00
Richard Feldman
161d128d45 Handle model refusal in ACP threads (#37383)
If the model refuses a prompt, we now:
* Show an error if it was a user prompt (and truncate it out of the
history)
* Respond with a failed tool call if the refusal was for a tool call

<img width="607" height="260" alt="Screenshot 2025-09-02 at 5 11 45 PM"
src="https://github.com/user-attachments/assets/070b5ee7-6ad6-4a63-8395-f9a5093cc40e"
/>
<img width="607" height="265" alt="Screenshot 2025-09-02 at 5 11 38 PM"
src="https://github.com/user-attachments/assets/98862586-390b-494e-b1f8-71d8341c8d9d"
/>



Release Notes:

- Improve handling of model refusals in ACP threads
2025-09-02 20:25:10 -04:00
Cole Miller
e1b0a98c34 ci: Remove Windows crash analysis CI scripts (#36694)
We'll just SSH into the Windows runners and look for crashes there.

Reverts #35926 

Release Notes:

- N/A

---------

Co-authored-by: Peter Tripp <petertripp@gmail.com>
2025-09-03 00:24:00 +00:00
Rafał Krzyważnia
ae0ee70abd Add configurable timeout for context server tool calls (#33348)
Closes: #32668

- Add
[tool_call_timeout_millis](https://github.com/cline/cline/pull/1904)
field to ContextServerCommand, like in Cline
- Update ModelContextServerBinary to include timeout configuration
- Modify Client to store and use configurable request timeout
- Replace hardcoded REQUEST_TIMEOUT with self.request_timeout
- Rename REQUEST_TIMEOUT to DEFAULT_REQUEST_TIMEOUT for clarity
- Maintain backward compatibility with 60-second default

Release Notes:

- context_server: Add support for configurable timeout for MCP tool
calls

---------

Co-authored-by: Ben Brandt <benjamin.j.brandt@gmail.com>
2025-09-03 00:03:56 +00:00
versecafe
893eb92f91 docs: Note edge case for macOS 26 (#37392)
- I believe this is caused by metal not being found due to it being on
the XcodeBeta path, not sure if there's a better fix for this but it'll
work until 26 is the latest release

Release Notes:

- N/A
2025-09-02 19:40:07 -04:00
Vitaly Slobodin
45fa6d81ac tailwind: Add HTML+ERB to the list of supported languages (#36797)
Hi! As part of https://github.com/zed-extensions/ruby/issues/162 we
would like to rename HTML/ERB to HTML+ERB since it is more syntactically
correct to treat such language as ERB on top of HTML rather than HTML or
ERB.

To keep the user experience intact, we outlined the prerequisites in the
linked issue. This is the first PR that adds the HTML+ERB language name
to the list of enabled languages for the Emmet extension. We will do the
same for the Tailwind configuration in the Zed codebase. Once the new
versions of Emmet and Zed are released, we will merge the pull request
in the Ruby extension repository and release the updated version. After
that, we will remove the old HTML/ERB and YAML/ERB languages. Let me
know if that sounds good. Thanks!

Release Notes:

- N/A

Co-authored-by: Marshall Bowers <git@maxdeviant.com>
2025-09-02 23:32:43 +00:00
Ben Brandt
60ad82cc94 Fix typo in clippy lint name (#37405)
Release Notes:

- N/A
2025-09-02 23:30:32 +00:00
Cole Miller
564ded71c1 acp: Disable external agents over SSH (#37402)
Follow-up to #37377 

Show a clearer error here until SSH support is implemented.

Release Notes:

- N/A
2025-09-02 19:29:21 -04:00
Umesh Yadav
63b3839a83 language_models: Prevent sending the tools object to unsupported models for Ollama (#37221)
Closes #32758

Release Notes:

- Resolved an issue with the Ollama provider that caused requests to
fail with a 400 error for models that don't support tools. The tools
object is now only sent to compatible models to ensure successful
requests.
2025-09-03 01:28:36 +02:00
Umesh Yadav
9f749881b3 language_models: Fix tool_choice null issue for other providers (#34554)
Follow up: #34532

Closes #35434 

Mostly fixes a issue were when the tool_choice is none it was getting
serialised as null. This was fixed for openrouter just wanted to follow
up and cleanup for other providers which might have this issue as this
is against the spec.

Release Notes:

- N/A
2025-09-03 01:22:57 +02:00
Danilo Leal
946efb03df Add option for code context menu items to have dynamic width (#37404)
Follow up to https://github.com/zed-industries/zed/pull/30598

This PR introduces the `display_options` field in the
`CompletionResponse`, allowing a code context menu width to be
dynamically dictated based on its larger item. This will allow us to
have the @-mentions and slash commands completion menus in the agent
panel not be bigger than it needs to be. It may also be relevant/useful
in the future for other use cases.

For now, we set all instances of code context menus to use a fixed
width, as defined in the PR linked above, which means this PR shouldn't
cause any visual change.

Release Notes:

- N/A

Co-authored-by: Michael Sloan <mgsloan+github@gmail.com>
2025-09-02 20:18:15 -03:00
Marshall Bowers
4b96ad3fba gpui: Remove http_client feature (#37401)
This PR removes the `http_client` feature from the `gpui` crate, as it
wasn't really doing anything.

It only controlled whether we depend on the `http_client` crate, but
from what I can tell we always depended on it anyways.

Obviates https://github.com/zed-industries/zed/pull/36615.

Release Notes:

- N/A
2025-09-02 23:14:47 +00:00
Umesh Yadav
4368c1b56b language_models: Add OpenRouterError and map OpenRouter errors to LanguageModelCompletionError (#34227)
Improves the error handling for openrouter and adds automatic retry like
anthropic for few of the status codes.
Release Notes:

- Improves error messages for Openrouter provider
- Automatic retry when rate limited or Server error from Openrouter
2025-09-03 01:13:46 +02:00
Dino
e5a968b709 vim: Fix change surround with any brackets text object (#37386)
This commit fixes an issue with how the `AnyBrackets` object was handled
with change surrounds (`cs`). With the keymap below, if one was to use
`csb{` with the text `(bracketed)` and the cursor inside the
parentheses, the text would not change.

```json
{
  "context": "vim_operator == a || vim_operator == i || vim_operator == cs",
  "bindings": {
    "b": "vim::AnyBrackets"
  }
}
```

Unfortunately there was no implementation for finding a corresponding
`BracketPair` for the `AnyBrackets` object, meaning that, when using
`cs` (change surrounds) the code would simply do nothing.

This commit updates this logic so as to try and find the nearest
surrounding bracket (parentheses, curly brackets, square brackets or
angle brackets), ensuring that `cs` also works with `AnyBrackets`.

Closes #24439

Release Notes:

- Fixed handling of `AnyBrackets` in vim's change surrounds (`cs`)
2025-09-02 16:03:14 -07:00
Ben Brandt
7aecab8e14 agent2: Only setup real client for real models (#37403)
Before we were setting up lots of test setup regardless of if we were
actually going to be making real requests or not.

This will hopefully help with intermittent test errors we're seeing on
Windows in CI.

Release Notes:

- N/A
2025-09-02 23:02:36 +00:00
Smit Barmase
e4df866664 editor: Do not show edit prediction during in-progress IME composition (#37400)
Closes #37249

We no longer show edit prediction when composing IME since it isn't
useful for unfinished alphabet.

Release Notes:

- Fixed edit predictions showing up during partial IME composition.
2025-09-03 03:41:10 +05:30
Bennet Bo Fenner
8770fcc841 acp: Enable claude code feature flag for everyone (#37390)
Release Notes:

- N/A
2025-09-02 17:57:29 -04:00
Smit Barmase
6dcae2711d terminal: Fix not able to select text during continuous output (#37395)
Closes #37211

Regressed in https://github.com/zed-industries/zed/pull/33305

Every time the terminal updates, we emit
`SearchEvent::MatchesInvalidated` to trigger a re-run of the buffer
search, which calls `clear_matches` to drop stale results.
https://github.com/zed-industries/zed/pull/33305 PR also cleared the
selection when clearing matches, which caused this issue. We could fix
it by only clearing matches and selection when they’re non-empty, but
it’s better to not clear the selection at all. This matches how the
editor behaves and keeps it consistent. This PR reverts that part of
code.


Release Notes:

- Fixed an issue where text selection was lost during continuous
terminal output.
2025-09-03 03:00:09 +05:30
Richard Feldman
5e01fb8f1c Nice errors for unsupported ACP slash commands (#37393)
If we get back slash-commands that aren't supported, tell the user that
this is the problem.

Release Notes:

- Improve error messages for unsupported ACP slash-commands

---------

Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
2025-09-02 20:39:24 +00:00
Conrad Irwin
88a79750cc Disable external agents over collab (#37377)
Release Notes:

- Disable UI to boot external agents in collab projects (as they don't
work)
2025-09-02 12:53:53 -07:00
Umesh Yadav
4c411b9fc8 language_models: Make JsonSchemaSubset the default tool_input_format for the OpenAI-compatible provider (#34921)
Closes #30188
Closes #34911
Closes #34906

Many OpenAI-compatible providers do not automatically filter the tool
schema to comply with the underlying model's requirements; they simply
proxy the request. This creates issues, as models like **Gemini**,
**Grok**, and **Claude** (when accessed via LiteLLM on Bedrock) are
incompatible with Zed's default tool schema.

This PR addresses this by defaulting to a more compatible schema subset
instead of the full schema.

### Why this approach?

* **Avoids Poor User Experience:** One alternative was to add an option
for users to manually set the JSON schema for models that return a `400
Bad Request` due to an invalid tool schema. This was discarded as it
provides a poor user experience.
* **Simplifies Complex Logic:** Another option was to filter the schema
based on the model ID. However, as demonstrated in the attached issues,
this is unreliable. For instance, `claude-4-sonnet` fails when proxied
through LiteLLM on Bedrock. Reliably determining behavior would require
a non-trivial implementation to manage provider-and-model combinations.
* **Better Default Behavior:** The current approach ensures that tool
usage works out-of-the-box for the majority of cases by default,
providing the most robust and user-friendly solution.


Release Notes:

- Improved tool compatibility with OpenAI API-compatible providers

Signed-off-by: Umesh Yadav <git@umesh.dev>
Co-authored-by: Peter Tripp <peter@zed.dev>
2025-09-02 14:29:07 -04:00
Peter Tripp
5ac6ae501f docs: Link glossary (#37387)
Follow-up to: https://github.com/zed-industries/zed/pull/37360

Add glossary.md to SUMMARY.md so it's linked to the public
documentation.

Release Notes:

- N/A
2025-09-02 17:57:48 +00:00
Michael Sloan
c01f12b15d zeta: Small refactoring in license detection check - rfind instead of iterated ends_with (#37329)
Release Notes:

- N/A
2025-09-02 17:23:35 +00:00
Agus Zubiaga
dfa066dfe8 acp: Display slash command hints (#37376)
Displays the slash command's argument hint while it hasn't been
provided:


https://github.com/user-attachments/assets/f3bb148c-247d-43bc-810d-92055a313514


Release Notes:

- N/A

---------

Co-authored-by: Bennet Bo Fenner <bennetbo@gmx.de>
2025-09-02 16:39:55 +00:00
Richard Feldman
ac8c653ae6 Fix race condition between feature flag and deserialization (#37381)
Right now if you open Zed, and we deserialize an agent that's behind a
feature flag (e.g. CC), we don't restore it because the feature flag
check hasn't happened yet at the time we're deserializing (due to auth
not having finished yet).

This is a simple fix: assume that if you had serialized it in the first
place, you must have had the feature flag enabled, so go ahead and
reopen it for you.

Release Notes:

- N/A
2025-09-02 12:28:07 -04:00
Danilo Leal
d2318be8d9 terminal view: Hide inline assist button if AI is disabled (#37378)
Closes https://github.com/zed-industries/zed/issues/37372

Release Notes:

- Fix the terminal inline assistant button showing despite `disable_ai`
being turned on.

---------

Co-authored-by: MrSubidubi <finn@zed.dev>
2025-09-02 13:27:06 -03:00
Danilo Leal
a026163746 inline assistant: Adjust completion menu item font size (#37375)
Now the @ completion menu items font size respect/match the buffer's
font size, as opposed to being rendered a bit bigger.

| Before | After |
|--------|--------|
| <img width="1226" height="468" alt="Screenshot 2025-09-02 at 11 
09@2x"
src="https://github.com/user-attachments/assets/a6d37110-b544-40c3-bf7a-447ea003d4d7"
/> | <img width="1218" height="462" alt="Screenshot 2025-09-02 at 11  09
2@2x"
src="https://github.com/user-attachments/assets/19e58bf8-2db5-442e-8f60-02dd9ee1308f"
/> |

Release Notes:

- inline assistant: Improved @-mention menu item font size, better
matching the buffer's font size.
2025-09-02 13:26:56 -03:00
Marshall Bowers
ad3ddd381d Revert "gpui: Do not render ligatures between different styled text runs (#37175) (#37382)
This reverts commit 62083fe796.

We're reverting this as it causes layout shift when typing/selecting
with ligatures:


https://github.com/user-attachments/assets/80b78909-62f5-404f-8cca-3535c5594ceb

Release Notes:

- Reverted #37175
2025-09-02 16:18:49 +00:00
David Kleingeld
7e3fbeb59d Add the Glossary from the channel into Zed (#37360)
This should make it easier for contributors to learn all the terms used
in the Zed code base.

Release Notes:

- N/A
2025-09-02 15:59:58 +00:00
Jonathan Camp
8e7caa429d remove extra brace in rules template (#37356)
Release Notes:

- Fixed: remove extra brace in rules template
2025-09-02 15:26:12 +00:00
Dino
c894351544 vim: Fix change surrounding quotes with whitespace within (#37321)
This commit fixes a bug with Zed's vim mode surrounds plugin when
dealing with replacing pairs with quote and the contents between the
pairs had some whitespace within them.

For example, with the following string:

```
' str '
```

If one was to use the `cs'"` command, to replace single quotes with
double quotes, the result would actually be:

```
"str"
```

As the whitespace before and after the closing character was removed.

This happens because of the way the plugin decides whether to add or
remove whitespace after and before the opening and closing characters,
repsectively. For example, using `cs{[` yields a different result from
using `cs{]`, the former adds a space while the latter does not.

However, since for quotes the opening and closing character is exactly
the same, this behavior is not possible, so this commit updates the code
in `vim::surrounds::Vim.change_surrounds` so that it never adds or
removes whitespace when dealing with any type of quotes.

Closes #12247 

Release Notes:

- Fixed whitespace handling when changing surrounding pairs to quotes in
vim mode
2025-09-02 09:11:35 -06:00
Finn Evers
a96015b3c5 activity_indicator: Show extension installation and updates (#37374)
This PR fixes an issue where extension operations would never show in
the activity indicator despite this being implemented for ages. This
happened because we were always returning `None` whenever the app has a
global auto updater, which is always the case, so the code path for
showing extension updates in the indicator could never be hit despite
existing prior. Also slightly improves the messages shown for ongoing
extension operations, as these were previously context unaware.

While I was at this, I also quickly took a stab at cleaning up some
remotely related stuff, namely:
- The `AnimationExt` trait is now by default only implemented for
anything that also implements `IntoElement`. This prevents
`with_animation` from showing up for e.g. `u32` within the suggestions
(finally).
- Commonly used animations are now implemented in the
`CommonAnimationExt` trait within the `ui` crate so the needed code does
not always need to be copied and element IDs for the animations are
truly unique.

Relevant change here regarding the original issue is the change from the
`return match` to just a `match` within the activitiy indicator, which
solved the issue at hand.

If we find this to be too noisy at some point, we can easily revisit,
but I think this holds important enough information to be shown in the
activity indicator, especially whilst developing extensions.

Release Notes:

- Extension installation and updates will now be shown in the activity
indicator.
2025-09-02 16:51:13 +02:00
张小白
2eb7ac97e0 windows: Use a message-only window for WindowsPlatform (#37313)
Previously, we were using `PostThreadMessage` to pass messages to
`WindowsPlatform`. This PR switches to an approach similar to `winit`
which using a hidden window as the message window (I guess that’s why
winit uses a hidden window?). The difference is that this PR creates it
as a message-only window.

Thanks to @reflectronic for the original PR #37255, this implementation
just fits better with the current code style.


Release Notes:

- N/A

---------

Co-authored-by: reflectronic <john-tur@outlook.com>
2025-09-02 22:32:24 +08:00
张小白
f06c18765f Rename from create_ssh_worktree to create_remote_worktree (#37358)
This is a left-over issue of #37035

Release Notes:

- N/A
2025-09-02 14:12:24 +00:00
Max Brunsfeld
2f279c5de4 Fix small errors preventing WSL support from working (#37350)
On nightly, when I run `zed` under WSL, I get an error parsing the
shebang line

```
/usr/bin/env: ‘sh\r’: No such file or directory
```

I believe that this is because in CI, Git checks out the file with CRLF
line endings, and that is how it is copied into the installer.

Also, the file extension was incorrect when downloading the production
remote server (a gzipped binary), preventing extraction from working
properly.

Release Notes:

- N/A
2025-09-02 07:07:23 -07:00
localcc
60b95d9253 Use premultiplied alpha for emoji rendering (#37370)
This improves emoji rendering on windows removing artifacts at the edges
by using premultiplied alpha. A bit more context can be found in #37167

Release Notes:

- N/A
2025-09-02 13:59:27 +00:00
Cole Miller
47ad1b2143 agent2: Fix terminal tool call content not being shown once truncated (#37318)
We render terminals as inline if their content is below a certain line
count, and scrollable past that point. In the scrollable case we weren't
setting a height for the terminal's container, causing it to be rendered
at height 0, which means no lines would be displayed. This PR fixes that
by setting an explicit height for the scrollable case, like we do in the
agent1 UI code.

Release Notes:

- agent: Fixed a bug that caused terminals in the panel to be empty
after their content reached a certain size.
2025-09-02 09:03:11 -04:00
Lukas Wirth
35c0d02c7c project: Temporarily disable terminal activation scripts on windows (#37361)
They seem to break things on window right now

Release Notes:

- N/A
2025-09-02 10:42:29 +00:00
Bennet Bo Fenner
374a8bc4cb acp: Add support for slash commands (#37304)
Depends on
https://github.com/zed-industries/agent-client-protocol/pull/45

Release Notes:

- N/A

---------

Co-authored-by: Antonio Scandurra <me@as-cii.com>
Co-authored-by: Agus Zubiaga <agus@zed.dev>
2025-09-02 08:48:33 +00:00
Maksim Bondarenkov
f06be6f3ec docs: Add link to msys2 docs page (#37327)
it was removed earlier. better to keep this link because the page
contains some useful information

Release Notes:

- N/A
2025-09-02 07:02:41 +00:00
Ben Kunkle
970242480a settings_ui: Improve case handling (#37342)
Closes #ISSUE

Improves the derive macro for `SettingsUi` so that titles generated from
struct and field names are shown in title case, and toggle button groups
use title case for rendering, while using lower case/snake case in JSON

Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-09-02 01:17:27 +00:00
Ben Kunkle
54cec5b484 settings_ui: Get editor settings working (#37330)
Closes #ISSUE

This PR includes the necessary work to get `EditorSettings` showing up
in the settings UI. Including making the `path` field on
`SettingsUiItem`'s optional so that top level items such as
`EditorSettings` which have `Settings::KEY = None` (i.e. are treated
like `serde(flatten)`) have their paths computed correctly for JSON
reading/updating.

It includes the first examples of a pattern I expect to continue with
the `SettingsUi` work with respect to settings reorganization, that
being adding missing defaults, and adding explicit values (or aliases)
to settings which previously relied on `null` being a value for optional
fields.

Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-09-02 00:26:42 +00:00
Ben Kunkle
60d17cccd3 settings_ui: Move settings UI trait to file content (#37337)
Closes #ISSUE

Initially, the `SettingsUi` trait was tied to `Settings`, however, given
that the `Settings::FileContent` type (which may be the same as the type
that implements `Settings`) will be the type that more directly maps to
the JSON structure (and therefore have the documentation, correct field
names (or `serde` rename attributes), etc) it makes more sense to have
the deriving of `SettingsUi` occur on the `FileContent` type rather than
the `Settings` type.

In order for this to work a relatively important change had to be made
to the derive macro, that being that it now "unwraps" options into their
inner type, so a field with type `Option<Foo>` where `Foo: SettingsUi`
will treat the field as if it were just `Foo`, expecting there to be a
default set in `default.json`. This imposes some restrictions on what
`Settings::FileContent` can be as seen in 1e19398 where `FileContent`
itself can't be optional without manually implementing `SettingsUi`, as
well as introducing some risk that if the `FileContent` type has
`serde(default)`, the default value will override the default value from
`default.json` in the UI even though it may differ (but it should!).

A future PR should probably replace the other settings with `FileContent
= Option<T>` (all of which currently have `T == bool`) with wrapper
structs and have `KEY = None` so the further niceties
`derive(SettingsUi)` will provide such as path renaming, custom UI, auto
naming and doc comment extraction can be used.

Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-09-01 18:42:33 -04:00
Ben Kunkle
8a8a9a4f07 settings_ui: Add dynamic settings UI item (#37331)
Closes #ISSUE

Adds a first draft of a way for "Dynamic" settings items to be added,
where Dynamic means settings where multiple sets of options are possible
(i.e. discriminated union, rust enum, etc). The implementation is very
similar to that of `Group`, except that instead of rendering all of it's
descendants, it contains a function to determine _which_ descendant to
render, whether that be a single item or a nested group of items.
Currently this is done in a type-unsafe way with indices, a future
improvement could be to make the API more type safe, and easier to
manually implement correctly.

An example of a "Dynamic" setting is `theme`, where it can either be a
string of the desired theme name, or an object with `mode: "light" |
"dark" | "system"` as well as theme names for `light` and `dark`. In the
system implemented by this PR, this would become a dynamic settings UI
item, where option `0` is a single item, the theme name selector, and
option `1` is a group, containing items for the `mode`, and
`light`/`dark` options.

Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-09-01 17:53:43 -04:00
Kirill Bulatov
634a1343dd Bump xcb dependency (#37335)
Deals with https://github.com/zed-industries/zed/security/dependabot/65

Release Notes:

- N/A
2025-09-01 21:51:15 +00:00
claytonrcarter
2ba25b5c94 editor: Support rewrap in block comments (#34418)
This updates `editor: rewrap` to work within doc comments, based on the
code that extends such comments on newline. I added some tests, and I've
tested it out in JS, C and PHP. (Though PHP depends on
https://github.com/zed-extensions/php/pull/40)

Closes #19794
Closes #18221

**Caveat:**
~~This will not rewrap an existing single-line block comment, such as
the one provided in #18221:~~ this will now rewrap as expected
```c
/* we can triangulate any convex polygon by picking a vertex and connecting it to the next two vertices; we first read two vertices, and then, for every subsequent vertex, we can form a triangle by connecting it to the first and previous vertex */
```
However, it will rewrap a similar comment if it is shaped like a doc
comment. In other words, this will rewrap as expected:
```c
/* 
 * we can triangulate any convex polygon by picking a vertex and connecting it to the next two vertices; we first read two vertices, and then, for every subsequent vertex, we can form a triangle by connecting it to the first and previous vertex 
 */
```

This seems like a reasonable improvement and limitation to me,
especially as a first step.

cc @smitbarmase because I think that you've been making a lot of the
`newline` and `rewrap` changes recently. (Thank you for those, by the
way!)

Release Notes:

- Added support for rewrap in block comments.

---------

Co-authored-by: Smit Barmase <heysmitbarmase@gmail.com>
2025-09-01 20:00:01 +00:00
Marshall Bowers
965dbc988f gpui: Fix typo in Windows alpha correction shader (#37328)
This PR fixes a typo in the Windows alpha correction shader that is now
caught by https://github.com/zed-industries/zed/pull/37314.

Another case that could be addressed by Bors.

Release Notes:

- N/A
2025-09-01 15:33:11 -04:00
Agus Zubiaga
5b73b40df8 ACP Terminal support (#37129)
Exposes terminal support via ACP and migrates our agent to use it.

- N/A

---------

Co-authored-by: Bennet Bo Fenner <bennetbo@gmx.de>
2025-09-01 18:57:15 +00:00
localcc
d910feac1d Implement perceptual gamma / contrast correction (#37167)
Closes #36023 

This improves font rendering quality by doing perceptual gamma+contrast
correction which makes font edges look nicer and more legible.

A comparison image: (left is old, right is new)
<img width="1638" height="854" alt="Screenshot 2025-08-29 140015"
src="https://github.com/user-attachments/assets/85ca9818-0d55-4af0-a796-19e8cf9ed36b"
/>

This is most noticeable on smaller fonts / low-dpi displays

Release Notes:

- Improved font rendering quality
2025-09-01 20:07:45 +02:00
张小白
61175ab9cd windows: Don’t skip the typo check for the windows folder (#37314)
Try to narrow down the scope of typo checking


Release Notes:

- N/A
2025-09-01 15:26:25 +00:00
雷电梅
2790eb604a deepseek: Fix API URL (#33905)
Closes #33904 

Release Notes:

- Add support for custom API Urls for DeepSeek Provider

---------

Co-authored-by: Peter Tripp <peter@zed.dev>
2025-09-01 10:49:09 +02:00
张小白
acff65ed3f windows: Update documents about WSL (#37292)
Release Notes:

- N/A
2025-09-01 08:33:59 +00:00
Ivan Trubach
3315fd94d2 editor: Add an option to disable rounded corners for text selection (#36987)
Closes #19891

Similar to VSCode’s `editor.roundedSelection` option.

#### Before/after

<table>
<tr><th><th>Enabled (default)</th><th>Disabled</th>
<tr><td>Editor-based UIs<td><img width="268" height="58" alt="image"
src="https://github.com/user-attachments/assets/f58c6817-88fc-4cba-b2bc-f7eff58ec6e5"
/>
<img width="146" height="97" alt="image"
src="https://github.com/user-attachments/assets/0cd08afa-8243-4d4e-a5c6-9055f6834ecf"
/><td><img width="272" height="54" alt="image"
src="https://github.com/user-attachments/assets/286c8f53-1973-442e-8446-4f48e3feca30"
/>
<img width="133" height="90" alt="image"
src="https://github.com/user-attachments/assets/4aea2044-403c-47a5-bb6d-a88a0b65814e"
/></td>
<tr><td>Terminal<td><img width="287" height="84" alt="image"
src="https://github.com/user-attachments/assets/b1594f68-2ef6-4bdc-9030-e67d55a5bf99"
/><td><img width="289" height="79" alt="image"
src="https://github.com/user-attachments/assets/6d095d9d-b408-4440-a9f5-6a2af2b84b61"
/></td>
</table>

Release Notes:

- Added setting `rounded_selection` to disable rounded corners for text
selection.
2025-09-01 11:21:55 +03:00
Lukas Wirth
62083fe796 gpui: Do not render ligatures between different styled text runs (#37175)
Currently when we render text with differing styles adjacently we might
form a ligature between the text, causing the ligature forming
characters to take on one of the two styles. This can especially become
confusing when a ligature is formed between actual text and inlay hints.

Annoyingly, the only ways to prevent this with core text is to either
render each run separately, or to insert a zero-width non-joiner to
force core text to break the ligatures apart, as it otherwise will merge
subsequent font runs of the same fonts.

We currently do layouting on a per line basis and it is unlikely we want
to change that as it would incur a lot of complexity and annoyances to
merge things back into a line, so this goes with the other approach of
inserting ZWNJ characters instead.

Note that neither linux nor windows seem to currently render ligatures,
so this only concerns macOS rendering at the moment.

Release Notes:

- Fixed ligatures forming between real text and inlay hints on macOS
2025-09-01 09:49:52 +02:00
Gaauwe Rombouts
a852bcc094 Improve system window tabs visibility (#37244)
Follow up of https://github.com/zed-industries/zed/pull/33334

After chatting with @MrSubidubi we found out that he had an old defaults
setting (most likely from when he encountered a previous window tabbing
bug):
```
❯ defaults read dev.zed.Zed-Nightly
{
    NSNavPanelExpandedSizeForOpenMode = "{800, 448}";
    NSNavPanelExpandedSizeForSaveMode = "{800, 448}";
    NSNavPanelExpandedStateForSaveMode = 1;
    NSOSPLastRootDirectory = {length = 828, bytes = 0x626f6f6b 3c030000 00000410 30000000 ... dc010000 00000000 };
    "NSWindow Frame NSNavPanelAutosaveName" = "557 1726 800 448 -323 982 2560 1440 ";
    "NSWindowTabbingShoudShowTabBarKey-GPUIWindow-GPUIWindow-(null)-HT-FS" = 1;
}
```

> That suffix is AppKit’s fallback autosave name when no tabbing
identifier is set. It encodes the NSWindow subclass (GPUIWindow), plus
traits like HT (hidden titlebar) and FS (fullscreen).

Which explains why it only happened on the Nightly build, since each
bundle has it's own defaults. It also explains why the tabbar would
disappear when he activated the `use_system_window_tabs` setting,
because with that setting activated, the tabbing identifier becomes
"zed" (instead of the default one when omitted) for which he didn't have
the `NSWindowTabbingShoudShowTabBarKey` default.

The original implementation was perhaps a bit naive and relied fully on
macOS to determine if the tabbar should be shown. I've updated the code
to always hide the tabbar, if the setting is turned off and there is
only 1 tab entry.

While testing, I also noticed that the menu's like 'merge all windows'
wouldn't become active when the setting was turned on, only after a full
workspace reload. So I added a setting observer as well, to immediately
set the correct window properties to enable all the features without a
reload.

Release Notes:

- N/A
2025-08-31 18:24:00 -06:00
Peter Tripp
f290daf7ea docs: Improve Bedrock suggested IAM policy (#37278)
Closes https://github.com/zed-industries/zed/issues/37251

H/T: @brandon-fryslie

Release Notes:

- N/A
2025-08-31 20:08:17 -04:00
Peter Tripp
129bff8358 agent: Make it so delete_path tool needs user confirmation (#37191)
Closes https://github.com/zed-industries/zed/issues/37048

Release Notes:

- agent: Make delete_path tool require user confirmation by default
2025-08-31 19:52:43 -04:00
Umesh Yadav
c833f8905b language_models: Fix grok-code-fast-1 support for Copilot (#37116)
This PR fixes a deserialization issue in GitHub Copilot Chat that was
causing warnings when encountering xAI models from the GitHub Copilot
API and skipping the Grok model from model selector.

Release Notes:

- Fixed support for xAI models that are now available through GitHub
Copilot Chat.
2025-08-31 18:51:17 -04:00
tidely
d74384f6e2 anthropic: Remove logging when no credentials are available (#37276)
Removes excess log which got through on each start of Zed
```
ERROR [agent_ui::language_model_selector] Failed to authenticate provider: Anthropic: credentials not found
```

The `AnthropicLanguageModelProvider::api_key` method returned a
`anyhow::Result` which would convert
`AuthenticateError::CredentialsNotFound` into a generic error because of
the implicit `Into` when using the `?` operator. This would then get
converted into a `AuthenticateError::Other` later.

By specifying the error type as `AuthenticateError`, we remove this
implicit conversion and the log gets removed.

Release Notes:

- N/A
2025-09-01 00:42:57 +03:00
Jakub Konka
5abc398a0a nix: Update flake, remove legacy Darwin SDK usage (#37254)
`darwin.apple_sdk.frameworks` has been obsoleted and is no longer
required to be specified explicitly as per [Nixpkgs Reference
Manual](https://nixos.org/manual/nixpkgs/stable/#sec-darwin-legacy-frameworks).

@P1n3appl3 not sure what the process for updating Nix is, so lemme know
if this is desired/acceptable!

Release Notes:

- N/A
2025-08-31 14:09:09 -07:00
Peter Tripp
9c8c3966df linux: Support ctrl-insert in markdown previews (#37273)
Closes: https://github.com/zed-industries/zed/issues/37240

Release Notes:

- Added support for copying in Markdown preview using `ctrl-insert` on Linux/Windows
2025-08-31 19:57:24 +00:00
Finn Evers
e48be30266 vim: Fix NormalBefore with completions shown (#37272)
Follow-up to https://github.com/zed-industries/zed/pull/35985

The `!menu` is actually not needed and breaks other keybinds from that
context.

Release Notes:

- N/A
2025-08-31 18:39:26 +00:00
Kirill Bulatov
babc0c09f0 Add a "mandatory PR contents" section in the contribution docs (#37259)
The LLM part is inspired by (and paraphrased from)
https://github.com/ghostty-org/ghostty?tab=contributing-ov-file#ai-assistance-notice

Release Notes:

- N/A
2025-08-31 20:56:23 +03:00
Kirill Bulatov
39d41ed822 Add another entry to show how to hide the Sign In button from the interface (#37260)
Release Notes:

- N/A
2025-08-31 10:29:29 +00:00
Kirill Bulatov
b69ebbd7b7 Bump pnpm dependencies (#37258)
Takes care of
https://github.com/zed-industries/zed/security/dependabot/64

Release Notes:

- N/A
2025-08-31 10:19:12 +00:00
renovate[bot]
f348737e8c Update Rust crate tracing-subscriber to v0.3.20 [SECURITY] (#37195)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [tracing-subscriber](https://tokio.rs)
([source](https://redirect.github.com/tokio-rs/tracing)) | dependencies
| patch | `0.3.19` -> `0.3.20` |

### GitHub Vulnerability Alerts

####
[CVE-2025-58160](https://redirect.github.com/tokio-rs/tracing/security/advisories/GHSA-xwfj-jgwm-7wp5)

### Impact

Previous versions of tracing-subscriber were vulnerable to ANSI escape
sequence injection attacks. Untrusted user input containing ANSI escape
sequences could be injected into terminal output when logged,
potentially allowing attackers to:

- Manipulate terminal title bars
- Clear screens or modify terminal display
- Potentially mislead users through terminal manipulation

In isolation, impact is minimal, however security issues have been found
in terminal emulators that enabled an attacker to use ANSI escape
sequences via logs to exploit vulnerabilities in the terminal emulator.

### Patches

`tracing-subscriber` version 0.3.20 fixes this vulnerability by escaping
ANSI control characters in when writing events to destinations that may
be printed to the terminal.

### Workarounds

Avoid printing logs to terminal emulators without escaping ANSI control
sequences.

### References

https://www.packetlabs.net/posts/weaponizing-ansi-escape-sequences/

### Acknowledgments

We would like to thank [zefr0x](http://github.com/zefr0x) who
responsibly reported the issue at `security@tokio.rs`.

If you believe you have found a security vulnerability in any tokio-rs
project, please email us at `security@tokio.rs`.

---

### Release Notes

<details>
<summary>tokio-rs/tracing (tracing-subscriber)</summary>

###
[`v0.3.20`](https://redirect.github.com/tokio-rs/tracing/releases/tag/tracing-subscriber-0.3.20):
tracing-subscriber 0.3.20

[Compare
Source](https://redirect.github.com/tokio-rs/tracing/compare/tracing-subscriber-0.3.19...tracing-subscriber-0.3.20)

**Security Fix**: ANSI Escape Sequence Injection (CVE-TBD)

#### Impact

Previous versions of tracing-subscriber were vulnerable to ANSI escape
sequence injection attacks. Untrusted user input containing ANSI escape
sequences could be injected into terminal output when logged,
potentially allowing attackers to:

- Manipulate terminal title bars
- Clear screens or modify terminal display
- Potentially mislead users through terminal manipulation

In isolation, impact is minimal, however security issues have been found
in terminal emulators that enabled an attacker to use ANSI escape
sequences via logs to exploit vulnerabilities in the terminal emulator.

#### Solution

Version 0.3.20 fixes this vulnerability by escaping ANSI control
characters in when writing events to destinations that may be printed to
the terminal.

#### Affected Versions

All versions of tracing-subscriber prior to 0.3.20 are affected by this
vulnerability.

#### Recommendations

Immediate Action Required: We recommend upgrading to tracing-subscriber
0.3.20 immediately, especially if your application:

- Logs user-provided input (form data, HTTP headers, query parameters,
etc.)
- Runs in environments where terminal output is displayed to users

#### Migration

This is a patch release with no breaking API changes. Simply update your
Cargo.toml:

```toml
[dependencies]
tracing-subscriber = "0.3.20"
```

#### Acknowledgments

We would like to thank [zefr0x](http://github.com/zefr0x) who
responsibly reported the issue at `security@tokio.rs`.

If you believe you have found a security vulnerability in any tokio-rs
project, please email us at `security@tokio.rs`.

</details>

---

### Configuration

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

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

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

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

---

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

---

Release Notes:

- N/A

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0MS44Mi43IiwidXBkYXRlZEluVmVyIjoiNDEuODIuNyIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOltdfQ==-->

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Kirill Bulatov <kirill@zed.dev>
2025-08-31 08:54:22 +00:00
Remco Smits
1ca5e84019 markdown: Add HTML img tag support (#36700)
Closes #21992

<img width="1406" height="1184" alt="Screenshot 2025-08-21 at 18 09 24"
src="https://github.com/user-attachments/assets/5f14a0d8-c4d9-48ad-b10d-fadfaca258ea"
/>

Code example:

```markdown
# Html Tag
<img src="https://picsum.photos/200/300" alt="Description of image" />

# Html Tag with width and height
<img src="https://picsum.photos/200/300" alt="Description of image" width="100" height="200" />

# Html Tag with style attribute with width and height
<img src="https://picsum.photos/200/300" alt="Description of image" style="width: 100px; height: 200px" />

# Normal Tag
![alt text](https://picsum.photos/200/300)
```

Release Notes:

- Markdown: Added HTML `<img src="/some-image.svg">` tag support
2025-08-31 11:43:24 +03:00
Gerd Augsburg
d80f13242b Support for "Insert" from character key location (#37219)
Release Notes:
- Added support for the Insert-Key from a character key location for
keyboard layouts like neo2
2025-08-31 11:26:28 +03:00
Dan Dascalescu
e115584896 docs: Copyedit debugger.md and clarify settings location (#36996)
Release Notes:

- N/A
2025-08-31 11:19:25 +03:00
Jason Lee
fe0ab30e8f Fix auto size rendering of SVG images in Markdown (#36663)
Release Notes:

- Fixed auto size rendering of SVG images in Markdown.

## Before

<img width="836" height="844" alt="image"
src="https://github.com/user-attachments/assets/0782e17e-620f-4c29-a5bc-a2ffe877d220"
/>
<img width="691" height="678" alt="image"
src="https://github.com/user-attachments/assets/dbe2dd5f-fd5b-48f9-bd09-0ee35e116aec"
/>


## After

<img width="873" height="1015" alt="image"
src="https://github.com/user-attachments/assets/59cbb69f-6a81-43cb-989f-3bcea873d81e"
/>
<img width="647" height="598" alt="image"
src="https://github.com/user-attachments/assets/11b67d8e-2b6c-4245-ad13-d4616fdabf22"
/>

For GPUI example

```
cargo run -p gpui --example image
```

<img width="1212" height="740" alt="SCR-20250821-ojoy"
src="https://github.com/user-attachments/assets/62bb2847-c533-4c4d-b5f7-c9764796262a"
/>
2025-08-31 11:14:57 +03:00
Michael Sloan
253765aaa1 zeta: Improve efficiency and clarity of license detection patterns (#37242)
See discussion on #36564

Adds a simple ad-hoc substring matching pattern language which allows
skipping a bounded number of chars between matched substrings. Before
this change compiling the regex was taking ~120ms on a fast machine and
~8mb of memory. This new version is way faster and uses minimal memory.

Checked the behavior of this vs by running it against 10k licenses that
happened to be in my home dir. There were only 4 differences of behavior
with the regex implementation, and these were false negatives for the
regex implementation that are true positives with the new one.

Of the ~10k licenses in my home dir, ~1k do not match one of these
licenses, usually because it's GPL/MPL/etc.

Release Notes:

- N/A
2025-08-31 07:23:21 +00:00
Michael Sloan
ad746f25f2 zeta: Add zlib to license detection + ignore symbol differences (#37238)
See discussion on #36564. Makes the license regexes a less fragile by
not matching on symbols, while also excluding cases where a long file
ends with a valid license. Also adds Zlib license, a commented out test
to check all license-like files discovered in the homedir, and more
testcases.

Not too happy with the efficiency here, on my quite good computer it
takes ~120ms to compile the regex and allocates ~8mb for it. This is
just not a great use of regexes, I think something using eager substring
matching would be much more efficient - hoping to followup with that.

Release Notes:

- Edit Prediction: Added Zlib license to open-source licenses eligible
for data collection.
2025-08-30 14:13:39 -06:00
Cole Miller
de576bd1b8 agent: Fix agent panel header not updating when opening a history entry (#37189)
Closes #37171

Release Notes:

- agent: Fixed a bug that caused the agent information in the panel
header to be incorrect when opening a thread from history.
2025-08-30 19:51:08 +00:00
Ben Kunkle
af26b627bf settings: Improve parse errors (#37234)
Closes #ISSUE

Adds a dependency on `serde_path_to_error` to the workspace allowing us
to include the path to the setting that failed to parse on settings
parse failure.

Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-08-30 17:59:04 +00:00
Umesh Yadav
0a32aa8db1 language_models: Fix GitHub Copilot thread summary by removing unnecessary noop tool logic (#37152)
Closes #37025 

This PR fixes GitHub Copilot thread summary failures by removing the
unnecessary `noop` tool insertion logic. The code was originally added
as a workaround in https://github.com/zed-industries/zed/pull/30007 for
supposed GitHub Copilot API issues when tools were used previously in a
conversation but no tools are provided in the current request. However,
testing revealed that this scenario works fine without the workaround,
and the `noop` tool insertion was actually causing "Invalid schema for
function 'noop'" errors that prevented thread summarization from
working. Removing this logic eliminates the errors and allows thread
summarization to function correctly with GitHub Copilot models.

The best way to see if removing that part of code works is just
triggering thread summarisation.

Error Log:
```
2025-08-27T13:47:50-04:00 ERROR [workspace::notifications] "Failed to connect to API: 400 Bad Request {"error":{"message":"Invalid schema for function 'noop': In context=(), object schema missing properties.","code":"invalid_function_parameters"}}\n"
```

Release Notes:

- Fixed GitHub Copilot thread summary failures by removing unnecessary
noop tool insertion logic.
2025-08-30 10:42:15 -04:00
Finn Evers
b473f4a130 Fix SQL error in recent projects query (#37220)
Follow-up to https://github.com/zed-industries/zed/pull/37035

In the WSL PR, `ssh_connection_id` was renamed to
`remote_connection_id`. However, that was not accounted for within the
`recent_workspaces_query`. This caused a query fail:

```
2025-08-30T14:45:44+02:00 ERROR [recent_projects] Prepare call failed for query:
SELECT
  workspace_id,
  paths,
  paths_order,
  ssh_connection_id
FROM
  workspaces
WHERE
  paths IS NOT NULL
  OR ssh_connection_id IS NOT NULL
ORDER BY
  timestamp DESC

Caused by:
    Sqlite call failed with code 1 and message: Some("no such column: ssh_connection_id")
```

and resulted in no recent workspaces being shown within the recent
projects picker.

This change updates the column name to the new name and thus fixes the
error.

Release Notes:

- N/A
2025-08-30 13:13:23 +00:00
Joseph T. Lyons
7d0a303785 Add xAI to supported language model providers (#37206)
After setting a `grok` model via the agent panel, the settings complains
that it doesn't recognize the language model provider:

<img width="1005" height="188" alt="SCR-20250829-tqqd"
src="https://github.com/user-attachments/assets/a25fc7e0-60f0-44fd-96d2-b1cb316d06b6"
/>

Also, sorted the list, in the follow-up commit.

Release Notes:

- N/A
2025-08-30 03:03:47 +00:00
Max Brunsfeld
f78f3e7729 Add initial support for WSL (#37035)
Closes #36188

## Todo

* [x] CLI
* [x] terminals
* [x] tasks

## For future PRs
* debugging
* UI for opening WSL projects
* fixing workspace state restoration

Release Notes:

- Windows alpha: Zed now supports editing folders in WSL.

---------

Co-authored-by: Junkui Zhang <364772080@qq.com>
2025-08-29 17:18:52 -07:00
Cole Miller
1c2e2a00fe agent: Re-add workaround for language model behavior with empty tool result (#37196)
This is just copying over the same workaround here:


a790e514af/crates/agent/src/thread.rs (L1455-L1459)

Into the agent2 code.

Release Notes:

- agent: Fixed an issue where some tool calls in the Zed agent could
return an error like "`tool_use` ids were found without `tool_result`
blocks immediately after"
2025-08-29 18:26:11 -04:00
Shardul Vaidya
a70cf3f1d4 bedrock: Inference Config updates (#35808)
Fixes #36866

- Updated internal naming for Claude 4 models to be consistent.
- Corrected max output tokens for Anthropic Bedrock models to match docs

Shoutout to @tlehn for noticing the bug, and finding the resolution.

Release Notes:

- bedrock: Fixed inference config errors causing Opus 4 Thinking and
Opus 4.1 Thinking to fail (thanks [@tlehn](https://github.com/tlehn) and
[@5herlocked](https://github.com/5herlocked])
- bedrock: Fixed an issue which prevented Rules / System prompts not
functioning with Bedrock models (thanks
[@tlehn](https://github.com/tlehn) and
[@5herlocked](https://github.com/5herlocked])
2025-08-29 18:13:06 -04:00
Peter Tripp
bdedb18c30 docs: Fix msys2 (#37199)
I accidentally pushed
db508bbbe2
to main instead of to a branch.

That broke tests.

Release Notes:

- N/A
2025-08-29 21:36:22 +00:00
Peter Tripp
db508bbbe2 docs: Remove MSYS2 instructions 2025-08-29 17:29:58 -04:00
Michael Sloan
515282d719 zeta: Add detection of BSD licenses + efficiency improvements + more lenient whitespace handling (#37194)
Closes #36564

Release Notes:

- Edit Prediction: Added various BSD licenses to open-source licenses
eligible for data collection.
2025-08-29 21:16:42 +00:00
Anthony Eid
f2c3f3b168 settings ui: Start work on creating the initial structure (#36904)
## Goal 

This PR creates the initial settings ui structure with the primary goal
of making a settings UI that is
- Comprehensive: All settings are available through the UI
- Correct: Easy to understand the underlying JSON file from the UI
- Intuitive
- Easy to implement per setting so that UI is not a hindrance to future
settings changes

### Structure

The overall structure is settings layer -> data layer -> ui layer.

The settings layer is the pre-existing settings definitions, that
implement the `Settings` trait. The data layer is constructed from
settings primarily through the `SettingsUi` trait, and it's associated
derive macro. The data layer tracks the grouping of the settings, the
json path of the settings, and a data representation of how to render
the controls for the setting in the UI, that is either a marker value
for the component to use (avoiding a dependency on the `ui` crate) or a
custom render function.

Abstracting the data layer from the ui layer allows crates depending on
`settings` to implement their own UI without having to add additional UI
dependencies, thus avoiding circular dependencies. In cases where custom
UI is desired, and a creating a custom render function in the same crate
is infeasible due to circular dependencies, the current solution is to
implement a marker for the component in the `settings` crate, and then
handle the rendering of that component in `settings_ui`.

### Foundation 

This PR creates a macro and a trait both called `SettingsUi`. The
`SettingsUi` trait is added as a new trait bound on the `Settings`
trait, this allows the type system to guarantee that all settings
implement UI functionality. The macro is used to derived the trait for
most types, and can be modified through attributes for unique cases as
well.

A derive-macro is used to generate the settings UI trait impl, allowing
it the UI generation to be generated from the static information in our
code base (`default.json`, Struct/Enum names, field names, `serde`
attributes, etc). This allows the UI to be auto-generated for the most
part, and ensures consistency across the UI.


#### Immediate Follow ups

- Add a new `SettingsPath` trait that will be a trait bound on
`SettingsUi` and `Settings`
- This trait will replace the `Settings::key` value to enable
`SettingsUi` to infer the json path of it's derived type
- Figure out how to render `Option<T> where T: SettingsUi` correctly
- Handle `serde` attributes in the `SettingsUi` proc macro to correctly
get json path from a type's field and identity

Release Notes:

- N/A

---------

Co-authored-by: Ben Kunkle <ben@zed.dev>
2025-08-29 16:56:10 -04:00
Dino
e9252a7a74 editor: Context menu aside scrolling (#35985)
Add support for scrolling the contents rendered aside an
`editor::code_context_menus::CodeContextMenu` by introducing the
`scroll_aside` method.

For now this method is only implemented for the
`CodeContextMenu::Completions` variant, which will scroll the aside
contents for an `editor::code_context_menus::CompletionsMenu` element,
as a `ScrollHandle` is added to the aside content that is rendered.

In order to be possible to trigger this via keybindings, a new editor
action is introduced, `ContextMenuScrollAside`, which accepts a number
of lines or pages to scroll the content by.

Lastly, the default keymaps for both MacOS and Linux, as well as for
Zed's vim mode, are updated to ensure that the following keybindings are
supported when a completion menu is open and the completion item's
documentation is rendered aside:

- `ctrl-e`
- `ctrl-y`
- `ctrl-d`
- `ctrl-u`

### Recording


https://github.com/user-attachments/assets/02043763-87ea-46f5-9768-00e907127b69

---

Closes #13194 

Release Notes:

- Added support for scrolling the documentation panel shown alongside
the completion menu in the editor with `cltr-d`, `ctrl-u`, `ctrl-e` and
`ctrl-y`

---------

Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
Co-authored-by: MrSubidubi <finn@zed.dev>
2025-08-29 20:23:44 +00:00
Raphael Lüthy
fcc3d1092f supermaven: Improve completion caching and position validation (#37047)
Closes #36981 

- Add completion text and position caching to reduce redundant API calls
- Only trigger new completion requests on text changes, not cursor
movement
- Validate cursor position to ensure completions show at correct
location
- Improve end-of-line range calculation for more accurate deletions
- Extract reset_completion_cache helper for cleaner code organization
- Update completion diff algorithm documentation for clarity

Edit: Sorry this is the 2nd PR, I forgot that the forks history was
messy; I cherrypicked and cleaned it properly with this PR

Release Notes:

- supermaven: Improved caching of predictions
- supermaven: Fixed an issue where changing cursor position would
incorrectly trigger new completions
2025-08-29 16:17:22 -04:00
Agus Zubiaga
a790e514af Fix ACP permission request with new tool calls (#37182)
Release Notes:

- Gemini integration: Fixed a bug with permission requests when
`always_allow_tool_calls` is enabled
2025-08-29 17:58:54 +00:00
Cole Miller
92f739dbb9 acp: Improve error reporting and log more information when failing to launch gemini (#37178)
In the case where we fail to create an ACP connection to Gemini, only
report the "unsupported version" error if the version for the found
binary is at least our minimum version. That means we'll surface the
real error in this situation.

This also fixes incorrect sorting of downloaded Gemini versions--as @kpe
pointed out we were effectively using the version string as a key. Now
we'll correctly use the parsed semver::Version instead.

Release Notes:

- N/A
2025-08-29 17:40:39 +00:00
Danilo Leal
3d4f917204 Make project symbols picker entry consistent with outline picker (#37176)
Closes https://github.com/zed-industries/zed/issues/36383

The project symbols modal didn't use the buffer font and highlighted
matches through modifying the font weight, which is inconsistent with
the outline picker, which presents code in list items in a similar way,
as well as project _and_ buffer search highlighting design.

Release Notes:

- N/A
2025-08-29 14:07:27 -03:00
Smit Barmase
a13881746a editor: APCA contrast (#37165)
Closes #35787
Closes #17890
Closes #28789
Closes #36495

How it works:

For highlights (and selections) within the visible rows of the editor,
we split them row by row. This is efficient since the number of visible
rows is constant. For each row, all highlights and selections, which may
overlap, are flattened using a line sweep. This produces non-overlapping
consecutive segments for each row, each with a blended background color.

Next, for each row, we split text runs into smaller runs to adjust its
color using APCA contrast. Since both text runs and segment are
non-overlapping and consecutive, we can use two-pointer on them to do
this.

For example, a text run for the variable red might be split into two
runs if a highlight partially covers it. As a result, one part may
appear as red, while the other appears as a lighter red, depending on
the background behind it.


Result:

<img width="1458" height="949" alt="image"
src="https://github.com/user-attachments/assets/4814c93d-12e7-4b4d-8542-d912acccfb8e"
/>

<img width="1459" height="952" alt="image"
src="https://github.com/user-attachments/assets/9e497b6c-3e66-43e8-8e5b-f634dd5ee8d3"
/>

<img width="1457" height="621" alt="image"
src="https://github.com/user-attachments/assets/8dfa6ce5-f46b-45b9-8008-66169d5aecd4"
/>

Release Notes:

- Improved text contrast when selected or highlighted in the editor.
2025-08-29 22:22:43 +05:30
Antonio Scandurra
11fb57a6d9 acp: Use the custom claude installation to perform login (#37169)
Release Notes:

- N/A

---------

Co-authored-by: Bennet Bo Fenner <bennetbo@gmx.de>
Co-authored-by: Agus Zubiaga <agus@zed.dev>
Co-authored-by: Nathan Sobo <nathan@zed.dev>
Co-authored-by: Cole Miller <cole@zed.dev>
Co-authored-by: morgankrey <morgan@zed.dev>
2025-08-29 14:16:02 +00:00
Kirill Bulatov
5001c03711 Properly process files that cannot be open for a reason (#37170)
Follow-up of https://github.com/zed-industries/zed/pull/36764

* Fix `anyhow!({e})` conversion lossing Collab error codes context when
opening a buffer remotely

* Use this context to only allow opening files that had not specific
Collab error code

Release Notes:

- N/A
2025-08-29 14:14:27 +00:00
Wouter Kayser
20d32d111c Update lsp-types to properly handle brackets (#37166)
Closes #21062

See also this pull request:
https://github.com/zed-industries/lsp-types/pull/6.

Release Notes:

- Fixed incorrect URL encoding of file paths with `[` `]` in them
2025-08-29 17:08:42 +03:00
Danilo Leal
ff035e8a22 agent: Add CC item in the settings view (#37164)
Release Notes:

- N/A
2025-08-29 09:26:52 -03:00
Kirill Bulatov
01266d10d6 Do not send any LSP logs by default to collab clients (#37163)
Follow-up https://github.com/zed-industries/zed/pull/37083

Noisy RPC LSP logs were functioning this way already, but to keep Collab
loaded even less, do not send any kind of logs to the client if the
client has a corresponding log tab not opened.

This change is pretty raw and does not fully cover scenarious with
multiple clients: if one client has a log tab open and another opens tab
with another kind of log, the 2nd kind of logs will be streamed only.
Also, it should be possible to forward the host logs to the client on
enabling — that is not done to keep the change smaller.

Release Notes:

- N/A
2025-08-29 12:23:45 +00:00
Lukas Wirth
4507f60b8d languages: Fix python activation scripts not being quoted (#37159)
Release Notes:

- N/A
2025-08-29 11:39:38 +00:00
Antonio Scandurra
d13ba0162a Require authorization for MCP tools (#37155)
Release Notes:

- Fixed a regression that caused MCP tools to run without requesting
authorization first.
2025-08-29 10:44:47 +00:00
Lukas Wirth
7403a4ba17 Add basic PyEnv and pixi support for python environments (#37156)
cc https://github.com/zed-industries/zed/issues/29807

Release Notes:

- Fixed terminals and tasks not respecting python pyenv and pixi
environments
2025-08-29 10:19:27 +00:00
Cole Miller
52da72d80a acp: Install new versions of agent binaries in the background (#37141)
Release Notes:

- acp: New releases of external agents are now installed in the
background.

Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
2025-08-29 04:16:49 +00:00
Mikayla Maki
384ffb883f Fix method documentation (#37140)
Release Notes:

- N/A
2025-08-29 04:07:52 +00:00
Mikayla Maki
c3ccdc0b44 Add a setting to control the number of context lines in excerpts (#37138)
Fixes https://github.com/zed-industries/zed/discussions/28739

Release Notes:

- Added a setting, `excerpt_context_lines`, for setting the number of
context lines shown in a multibuffer
2025-08-29 03:50:24 +00:00
Conrad Irwin
e5cea54cbb acp: Load agent panel even if serialized config is bogus (#37134)
Closes #ISSUE

Release Notes:

- N/A
2025-08-28 20:09:20 -06:00
Michael Sloan
cfd56a744d zeta: Show update required notification on appropriate window(s) (#37130)
To show these notifications, Zeta was being initialized with the initial
workspace it's used on - which may not even still exist! This removes a
confusing/misleading workspace field from Zeta.

Release Notes:

- N/A
2025-08-29 00:22:56 +00:00
Marshall Bowers
960d9ce48c Disable Expert language server by default for Elixir (#37126)
This PR updates the language server configuration for Elixir and HEEx to
not start the [Expert](https://github.com/elixir-lang/expert) language
server by default.

While Expert is the official Elixir language server, it is still early,
so we don't want to make it the default just yet.

Release Notes:

- Updated the default Elixir and HEEx language server settings to not
start the Expert language server.
2025-08-28 22:50:27 +00:00
Marshall Bowers
52d119b637 docs: Add Expert to Elixir docs (#37127)
This PR adds documentation for
[Expert](https://github.com/elixir-lang/expert) to the Elixir docs.

Also updated the examples for the other language servers to be
representative of all the supported language servers.

Release Notes:

- N/A
2025-08-28 22:45:09 +00:00
Richard Feldman
8c18f059f1 Always enable acp accept/reject buttons for now (#37121)
We have a bug in our ACP implementation where sometimes the
Accept/Reject buttons are disabled (and stay disabled even after the
thread has finished). I haven't found a complete fix for this yet, so in
the meantime I'm putting out the fire by making it so those buttons are
always enabled. That way you're never blocked, and the only consequence
of the bug is that sometimes they should be disabled but are enabled
instead.

Release Notes:

- N/A
2025-08-28 21:42:12 +00:00
Cole Miller
930189ed83 acp: Support automatic installation of Claude Code (#37120)
Release Notes:

- N/A
2025-08-28 21:38:14 +00:00
Ben Brandt
08c23c92ca acp: Bump to 0.1.1 (#37119)
No big changes, just tracking the latest version after the official
release

Release Notes:

- N/A
2025-08-28 21:16:06 +00:00
Julia Ryan
88e8f7af68 Activate preview for initially selected item (#37112)
@JosephTLyons pointed out that it's a bit weird that we only show a
preview for items selected after the initial one, so this does it for
that too.

It makes tab switching feel even faster!

Release Notes:

- N/A

Co-authored-by: David Kleingeld <davidsk@zed.dev>
2025-08-28 21:07:02 +00:00
Finn Evers
f2e62c98d1 docs: Fix broken link in agent-panel.md (#37113)
This fixes a small typo I stumbled upon, which caused a 404 within the
docs.

Release Notes:

- N/A
2025-08-28 19:48:35 +00:00
Cole Miller
8697b91ea0 acp: Automatically install gemini under Zed's data dir (#37054)
Closes: https://github.com/zed-industries/zed/issues/37089

Instead of looking for the gemini command on `$PATH`, by default we'll
install our own copy on demand under our data dir, as we already do for
language servers and debug adapters. This also means we can handle
keeping the binary up to date instead of prompting the user to upgrade.

Notes:

- The download is only triggered if you open a new Gemini thread
- Custom commands from `agent_servers.gemini` in settings are respected
as before
- A new `agent_servers.gemini.ignore_system_version` setting is added,
similar to the existing settings for language servers. It's `true` by
default, and setting it to `false` disables the automatic download and
makes Zed search `$PATH` as before.
- If `agent_servers.gemini.ignore_system_version` is `false` and no
binary is found on `$PATH`, we'll fall back to automatic installation.
If it's `false` and a binary is found, but the version is older than
v0.2.1, we'll show an error.

Release Notes:

- acp: By default, Zed will now download and use a private copy of the
Gemini CLI binary, instead of searching your `$PATH`. To make Zed search
your `$PATH` for Gemini CLI before attempting to download it, use the
following setting:

```
{
  "agent_servers": {
    "gemini": {
      "ignore_system_version": false
    }
  }
}
```
2025-08-28 19:33:00 +00:00
Michael Sloan
47aaaa8bcf Make SanitizedPath wrap Path instead of Arc<Path> to avoid allocation (#37106)
Release Notes:

- N/A
2025-08-28 13:32:30 -06:00
Conrad Irwin
69933d5b81 Add support for Claude Code auth (#37103)
Co-authored-by: Antonio Scandurra <me@as-cii.com>

Closes #ISSUE

Release Notes:

- N/A

Co-authored-by: Antonio Scandurra <me@as-cii.com>
2025-08-28 12:59:31 -06:00
Joseph T. Lyons
909d7215c0 Update patch and nightly release docs (#37109)
Release Notes:

- N/A
2025-08-28 18:49:03 +00:00
Richard Feldman
27777d4b8f Have ACP respect always_allow_tool_actions (#37104)
Release Notes:

- ACP agents now respect the always_allow_tool_actions setting
2025-08-28 14:18:25 -04:00
Marshall Bowers
4469b14512 collab_ui: Show channel list while reconnecting (#37107)
This PR makes it so the channel list will still be shown while
reconnecting to Collab instead of showing the signed-out state.

In order to model the transitional states that occur while reconnecting,
we needed to introduce a new `Status::Reauthenticated` state that we go
through when signing in as part of a reconnect. This is because we
cannot tell from `Status::Authenticated` alone if we're authenticating
for the first time or reauthenticating.

Release Notes:

- N/A
2025-08-28 18:15:08 +00:00
Finn Evers
29fc324a78 html: Bump to v0.2.2 (#37102)
This PR bumps the HTML extension to v0.2.2.

Changes:

- https://github.com/zed-industries/zed/pull/28184
- https://github.com/zed-industries/zed/pull/36948
- https://github.com/zed-industries/zed/pull/37098

Release Notes:

- N/A
2025-08-28 17:07:06 +00:00
Finn Evers
4ef9294123 html: Add outline (#37098)
We were missing an outline definition for HTML flies, hence this PR adds
one for that

<img width="255" height="726" alt="image"
src="https://github.com/user-attachments/assets/ae59cb8d-6c69-4019-966a-d5baf744329d"
/>

Release Notes:

- N/A
2025-08-28 18:44:30 +02:00
Kai Ren
4b0609840b go: Fix highlighting of fields (#37026)
Closes #36420

## Synopsis

The issue in #36420 is caused by #7276, which bound the appropriate
tree-sitter queries to the `@variable.member` color. However, I have
found neither this color's declaration nor its other usages in the
codebase (neither on the latest `main` nor on
79c1003b34).

Other languages use for such situations the `@property` color.


## Solution

Just change the used `@variable.member` color to the `@property` one.

Seems fully inline with the changes illustrated in #7276.


## Screenshots

<img width="856" height="465" alt="Screenshot 2025-08-28 at 13 18 38"
src="https://github.com/user-attachments/assets/9d1f3542-8749-421f-864f-959c1242cc64"
/>

<img width="837" height="462" alt="Screenshot 2025-08-28 at 13 20 08"
src="https://github.com/user-attachments/assets/36a80c22-9de9-46b1-87e3-7fdeaa62978f"
/>


## Changelog

Release Notes:

- go: Fixed highlighting of fields.
2025-08-28 17:40:48 +02:00
Liam
2cb697e9f4 copilot: Use updated Copilot Chat model schema (#33007)
Use the latest Copilot Chat model schema, matching what is used in
VSCode, to get more data about available models than was previously
accessible. Replace hardcoded default model (gpt-4.1) with the default
model included in JSON. Other data like premium request multipliers
could be used in the future if Zed implements a way for models to
display additional details about themselves, such as with tooltips on
hover.

Release Notes:

- N/A

---------

Co-authored-by: Peter Tripp <peter@zed.dev>
2025-08-28 15:32:15 +00:00
Umesh Yadav
c8e99125bd language_models: Fix tool calling for x-ai/grok-code-fast-1 model via OpenRouter (#37094)
Closes #37022
Closes #36994

This update ensures all Grok models use the JsonSchemaSubset format for
tool schemas.

A previous fix for this issue was too specific, only targeting grok-4
models. This caused other variants, like grok-code-fast-1, to be missed.
We've now broadened the logic to correctly apply the setting to the
entire Grok model family.

Release Notes:

- Fix tool calling for `x-ai/grok-code-fast-1` model via OpenRouter.
2025-08-28 11:28:22 -04:00
Lukas Wirth
835e5ba662 Inject venv environment via the toolchain (#36576)
Instead of manually constructing the venv we now ask the python
toolchain for the relevant information, unifying the approach of vent
inspection

Fixes https://github.com/zed-industries/zed/issues/27350

Release Notes:

- Improved the detection of python virtual environments for terminals
and tasks in remote projects.
2025-08-28 14:40:43 +00:00
Umesh Yadav
24ee98b3e1 agent2: Fix model deduplication to use provider ID and model ID (#37088)
Closes #37043

Previously claude sonnet 4 was missing from copilot as it was colliding
with zed's claude-sonnet-4 model id. Now we do deduplication based upon
model and provider id both.

| Before | After |
|--------|--------|
| <img width="784" height="950" alt="CleanShot 2025-08-28 at 18 31
28@2x"
src="https://github.com/user-attachments/assets/d49d5a17-7271-417d-bb5e-bc380071e810"
/> | <img width="720" height="876" alt="CleanShot 2025-08-28 at 18 31
42@2x"
src="https://github.com/user-attachments/assets/a5100c05-994e-4e19-ab20-34c0258b977c"
/> |

Release Notes:

- Fixed an issue where models with the same ID from different providers
(such as Claude Sonnet 4 from both Zed and Copilot) were incorrectly
deduplicated in the model selector—now all variants are shown.
2025-08-28 16:12:59 +02:00
Danilo Leal
213ee32b94 docs: Make unsupported features more prominent in external agents (#37090)
Use the notes component to better highlight that in the docs UI.

Release Notes:

- N/A
2025-08-28 10:47:19 -03:00
Kirill Bulatov
f127ba82d1 Remote LSP logs (#37083)
Take 2: https://github.com/zed-industries/zed/pull/36709 but without the
very bad `cfg`-based approach for storing the RPC logs.

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

Enables LSP log tracing in both remote collab and remote ssh
environments.
Server logs and server RPC traces can now be viewed remotely, and the
LSP button is now shown in such projects too.

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

Co-Authored-By: Kirill <kirill@zed.dev>
Co-Authored-By: Lukas <lukas@zed.dev>

Release Notes:

- Enabled LSP log tracing in both remote collab and remote ssh
environments

---------

Co-authored-by: Ben Kunkle <ben@zed.dev>
Co-authored-by: Lukas Wirth <lukas@zed.dev>
2025-08-28 15:32:44 +03:00
Antonio Scandurra
39d86eeb7f Trim API key when submitting requests to LLM providers (#37082)
This prevents the common footgun of copy/pasting an API key
starting/ending with extra newlines, which would lead to a "bad request"
error.

Closes #37038 

Release Notes:

- agent: Support pasting language model API keys that contain newlines.
2025-08-28 12:00:44 +00:00
Antonio Scandurra
4981c33bf3 acp: Don't cancel editing when scrolling message out of view (#37020)
Release Notes:

- agent: Fixed a bug that canceled editing when scrolling the user
message out of view.

Co-authored-by: Bennet Bo Fenner <bennetbo@gmx.de>
2025-08-28 12:57:09 +02:00
Lorenzo Stella
54609d4d00 Fix boolean settings in "Agent Settings" documentation page (#37068)
This fixes some errors in the examples in the "Agent Settings" page at
https://zed.dev/docs/ai/agent-settings#agent-settings, where strings
"true" and "false" are used in place of the proper boolean JSON values:
strings don't work for all those settings, and are marked as errors when
editing settings.json, while booleans do work.

Release Notes:

- N/A
2025-08-28 10:35:30 +00:00
张小白
ff03dda90a Refactor KeybindingKeystroke (#37065)
This pull request refactors the `KeybindingKeystroke` struct and related
code to improve platform abstraction. The changes centralize
platform-specific logic within `KeybindingKeystroke` and update its
usage throughout the codebase, making the API more consistent and less
error-prone.



Release Notes:

- N/A
2025-08-28 08:40:43 +00:00
Anthony Eid
73b38c8306 debugger: Add ability to only show stack frame entries from visible work trees (#37061)
This PR adds a toggleable filter to the stack frame list that filters
out entries that don't exist within a user's project (visible work
trees). This works by keeping a vector of entry indices that exist
within a user's project and updates the list state based on these
entries when filtering the list.

I went with this approach so the stack frame list wouldn't have to
rebuild itself whenever the filter is toggled and it could persist its
state across toggles (uncollapsing a collapse list). It was also easier
to keep track of selected entries on toggle using the vector as well.

### Preview

https://github.com/user-attachments/assets/d86c7485-c885-4bbb-bebb-2f6385674925



Release Notes:

- debugger: Add option to only show stack frames from user's project in
stack frame list
2025-08-28 07:53:32 +00:00
张小白
38e5c8fb66 keymap_editor: Fix incorrect keystroke being reported (#36998)
This PR fixes two bugs and also changes one behavior in the **Keymap
Editor**.

As shown in the video, when I press `ctrl-shift-2` in the Keymap Editor,
the first keystroke is displayed as `ctrl-shift-@`, which is incorrect.
On macOS and Linux, it should be `ctrl-@`.



https://github.com/user-attachments/assets/69cfcfa0-b422-45d6-8e69-80f8608180fd



Also, after pressing `ctrl-shift-2` and then releasing `2` and `ctrl`, a
`shift` keystroke was incorrectly added.



https://github.com/user-attachments/assets/892124fd-847d-4fde-9b20-a27ba49ac934



Now, when you enter a sequence like `+ctrl+alt-alt+f` in the Keymap
Editor, it will output `ctrl-f` instead of `ctrl-alt-f`, matching VS
Code’s behavior.


Release Notes:

- Fixed incorrect keystroke reporting in the Keymap Editor.
2025-08-28 15:16:13 +08:00
Gaauwe Rombouts
78c2f1621d Add macOS window tabs (#33334)
Closes https://github.com/zed-industries/zed/issues/14722
Closes https://github.com/zed-industries/zed/issues/4948
Closes https://github.com/zed-industries/zed/issues/7136

Follow up of https://github.com/zed-industries/zed/pull/20557 and
https://github.com/zed-industries/zed/pull/32238.

Based on the discussions in the previous PRs and the pairing session
with @ConradIrwin I've decided to rewrite it from scratch, to properly
incorporate all the requirements. The feature is opt-in, the settings is
set to false by default. Once enabled via the Zed settings, it will
behave according to the user’s system preference, without requiring a
restart — the next window opened will adopt the new behavior (similar to
Ghostty).

I’m not entirely sure if the changes to the Window class are the best
approach. I’ve tried to keep things flexible enough that other
applications built with GPUI won’t be affected (while giving them the
option to still use it), but I’d appreciate input on whether this
direction makes sense long-term.



https://github.com/user-attachments/assets/9573e094-4394-41ad-930c-5375a8204cbf

### Features
* System-aware tabbing behavior
* Respects the three system modes: Always, Never, and Fullscreen
(default on macOS)
* Changing the Zed setting does not require a restart — the next window
reflects the change
* Full theme support
    * Integrates with light and dark themes
* [One
Dark](https://github.com/user-attachments/assets/d1f55ff7-2339-4b09-9faf-d3d610ba7ca2)
* [One
Light](https://github.com/user-attachments/assets/7776e30c-2686-493e-9598-cdcd7e476ecf)
    * Supports opaque/blurred/transparent themes as best as possible
* [One Dark -
blurred](https://github.com/user-attachments/assets/c4521311-66cb-4cee-9e37-15146f6869aa)
* Dynamic layout adjustments
    * Only reserves tab bar space when tabs are actually visible
* [With
tabs](https://github.com/user-attachments/assets/3b6db943-58c5-4f55-bdf4-33d23ca7d820)
* [Without
tabs](https://github.com/user-attachments/assets/2d175959-5efc-4e4f-a15c-0108925c582e)
* VS Code compatibility
* Supports the `window.nativeTabs` setting in the VS Code settings
importer
* Command palette integration
    * Adds commands for managing tabs to the command palette
* These can be assigned to keyboard shortcuts as well, but didn't add
defaults as to not reserve precious default key combinations

Happy to pair again if things can be improved codewise, or if
explanations are necessary for certain choices!



Release Notes:
* Added support for native macOS window tabbing. When you set
`"use_system_window_tabs": true`, Zed will merge windows in the same was
as macOS: by default this happens only when full screened, but you can
adjust your macOS settings to have this happen on all windows.

---------

Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
2025-08-28 03:51:22 +00:00
Dino
0a9f407872 search: Add support for case-sensitivity pattern items (#34762)
This Pull Request introduces support for pattern items in the buffer
search. It does so by splitting the `query` methods into two new
methods:

- `BufferSearchBar.raw_query` – returns the text from the search query
editor
- `BufferSearchBar.query` - returns the search query with pattern items
removed

Whenever the search query is updated, processing of the
`EditorEvent::Edited` event ends up calling the
`BufferSearchBar.apply_pattern_items` method, which parses the pattern
items from the raw query, and updates the buffer search bar's search
options accordingly. This `apply_pattern_items` function avoids updating
the `BufferSearchBar.default_options` field in order to be able to reset
the search options when a pattern items is removed. Lastly, new pattern
items can easily be added by updating the `PATTERN_ITEMS` array.

### Screen Capture


https://github.com/user-attachments/assets/ebd83c38-e480-4c24-9b8c-6edde69cf392

---

Closes #32390

Release Notes:

- Added support for the `\c` and `\C` query pattern items to control
case-sensitivity in buffer search

---------

Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
2025-08-27 19:27:02 -06:00
Romans Malinovskis
4e1a901059 helix: Improve "x" behavior (#35611)
Closes #32020 

Release Notes:
- Helix: Improve `x` behaviour. Will respect modifiers (`5 x`). Pressing
`x` on a empty line, will select current+next line, because helix
considers current line to be already selected without the need of
pressing `x`.
2025-08-27 20:25:00 -05:00
Michael Sloan
8af212e785 Fix watching of Git repo in presence of scanner restarts (#37052)
The scanner is restarted after loading initial settings, and there was
an optimization to not re-discover and re-watch git repositories if they
already exist in the snapshot. #35865 added cleanup of watches that
occurred when the scanner restarts, and so in some cases repos were no
longer watched.

Release Notes:

- Linux: Fixed a case where Git repositories might not be watched for
changes, causing branch switching to not update the UI.

Co-authored-by: Julia <julia@zed.dev>
2025-08-27 18:34:14 -06:00
Kirill Bulatov
b233df8343 Revert "Remote LSP logs (#36709)" (#37051)
This reverts commit e2bf8e5d9c.

See
https://github.com/zed-industries/zed/pull/37050#issuecomment-3230017137
for the context: musl builds started to fail and the amount of `cfg!`s
to fix this is too large.

Instead, the lsp_log.rs has to be split and repurposed better for the
remote headless server.

Release Notes:

- N/A
2025-08-27 23:24:19 +00:00
Finn Evers
9a97f9465b rust: Improve highlighting within macros (#37049)
This makes sure we do not apply the highlights for snake case
identifiers as well as paths for attributes too broadly to all types of
macros, which should make macros much more readable overall whilst
keeping the highlighting for the attribute items.

| Before | After |
| --- | --- |
| <img width="1414" height="958" alt="Bildschirmfoto 2025-08-28 um 00 37
58"
src="https://github.com/user-attachments/assets/1254b9a2-d07a-4be4-9b4f-555a7c640302"
/> | <img width="1414" height="958" alt="Bildschirmfoto 2025-08-28 um 00
37 38"
src="https://github.com/user-attachments/assets/5f6dd66c-5469-4f27-9f1d-0a6e6e8d8085"
/> |

Release Notes:

- rust: Improved highlighting within macros.
2025-08-28 00:57:08 +02:00
tidely
48299b5b24 search: Preserve SearchOptions across dismisses (#36954)
Closes #36931 and #21956

Preserves `SearchOptions` across dismisses of the buffer search bar.
This behavior is consistent with VSCode, which seems reasonable. The
`configured_options` field is then no longer being used. The
configuration is still read during initialization of the
`BufferSearchBar`, but not after.

Something to consider is that there are other elements in the search bar
which are not kept across dismisses such as replace status. However
these are visually separated in the UI, leading me to believe this is a
okay change to make.

Release Notes:

- Preserve search options between buffer search dismisses
2025-08-28 00:17:21 +03:00
Jordan Pittman
4e4bfd6f4e editor: Add "Wrap Selections in Tag" action (#36948)
This PR adds the ability for a user to select one or more blocks of text
and wrap each selection in an HTML tag — which works by placing multiple
cursors inside the open and close tags so the appropriate element name
can be typed in to all places simultaneously.

This is similar to the emmet "Wrap with Abbreviation" functionality
discussed in #15588 but is a simpler version that does not rely on
Emmet's language server.

Here's a preview of the feature in action:


https://github.com/user-attachments/assets/1931e717-136c-4766-a585-e4ba939d9adf


Some notes and questions:
- The current implementation is a hardcoded with regards to supported
languages. I'd love some direction on how much of this information to
push into the relevant language structs.
- I can see this feature as something that languages added by an
extension would want to enable support for — is this something you'd
want?
- The syntax is hardcoded to support HTML/XML/JSX-like languages. I
don't suppose this is a problem but figured I'd point it out anyway.
- I called it "Wrap in tag" but open to whatever naming you feel is
appropriate.
- The implementation doesn't use `manipulate_lines` — I wasn't sure how
make use of that without extra overhead / bookkeeping — does this seem
fine?
- I could also investigate adding wrap in abbreviation support by
communicating with the Emmet language server but I think I'll need some
direction on how to handle Emmet's custom LSP message. I could do this
either in addition to or instead of this feature — though imo this
feature is a nice "shortcut" regardless.

Release Notes:

- Added a new "Wrap Selections in Tag" action that lets you wrap one or
more selections in tags based on language. Works in HTML, JSX, and
similar languages, and places cursors inside both opening and closing
tags so you can type the tag name once and apply it everywhere.

---------

Co-authored-by: Smit Barmase <heysmitbarmase@gmail.com>
2025-08-28 02:37:32 +05:30
Piotr Osiewicz
5444fbd8fe python: Look for local venvs in all directories between root of the worktree and current pyproject.toml (#37037)
cc @michael-ud - if you can build Zed, I'd appreciate it if you could
give this a go with your project. Otherwise I can provide a link to
download of current nightly via an e-mail for you to try out (if you
want).
This change will land in Preview (if merged) on next Wednesday and then
it'll be in Stable a week after that.
Related to: #20402
Release Notes:

- python: Zed now searches for virtual environments in intermediate
directories between a root of the worktree and the location of
pyproject.toml applicable to the currently focused file.
2025-08-27 22:59:56 +02:00
Joseph T. Lyons
58f896e5cd Update Wednesday release process docs (#37033)
Release Notes:

- N/A
2025-08-27 15:59:08 -04:00
Joseph T. Lyons
d43cf2c486 Link out to release channel FAQ in Docs (#37029)
This PR links users to the FAQ on the release channels, which has more
in-depth coverage of the process.

Release Notes:

- N/A
2025-08-27 19:10:10 +00:00
Ben Kunkle
e2bf8e5d9c Remote LSP logs (#36709)
Enables LSP log tracing in both remote collab and remote ssh
environments.
Server logs and server RPC traces can now be viewed remotely, and the
LSP button is now shown in such projects too.

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

Co-Authored-By: Kirill <kirill@zed.dev>
Co-Authored-By: Lukas <lukas@zed.dev>

Release Notes:

- Enabled LSP log tracing in both remote collab and remote ssh
environments

---------

Co-authored-by: Kirill Bulatov <kirill@zed.dev>
Co-authored-by: Lukas Wirth <lukas@zed.dev>
2025-08-27 21:55:34 +03:00
Cole Miller
c158eb2442 docs: Note that Gemini CLI is not supported over SSH (#37023)
Release Notes:

- N/A
2025-08-27 14:34:40 -04:00
Peter Tripp
71f900346c Add ';' and '*' to word_chars to improve softwrap (#37024)
Follow-up to: https://github.com/zed-industries/zed/pull/37019
See also: https://github.com/zed-industries/zed/issues/37010

Before/After:
<img width="418" height="402" alt="Screenshot 2025-08-27 at 13 54 52"
src="https://github.com/user-attachments/assets/1b2e02dd-c216-4372-b23e-5a3a619d2b77"
/>


Release Notes:

- N/A
2025-08-27 18:12:34 +00:00
Floyd Wang
9ca4fb16b2 gpui: Support disabling window resizing and minimizing (#36859)
Add support to disable both window resizing and minimizing.

| | macOS | Windows |
| - | - | - |
| **Unresizable** | <img width="412" height="440"
alt="SCR-20250822-qpea"
src="https://github.com/user-attachments/assets/d6d45510-dc4b-436f-a9fa-ce9cb0b0c411"
/> | <img width="276" height="298" alt="2025-08-22 110757"
src="https://github.com/user-attachments/assets/9deff498-e903-4173-9c26-072dd9409fc1"
/> |
| **Unminimizable** | <img width="412" height="440"
alt="SCR-20250822-qpfl"
src="https://github.com/user-attachments/assets/e1d5f9eb-6de5-4908-8b52-38ccb2e65689"
/> | <img width="276" height="298" alt="2025-08-22 110814"
src="https://github.com/user-attachments/assets/da94b006-3544-4274-8b02-1cab7ca8dd70"
/> |

Release Notes:

- N/A
2025-08-27 10:26:57 -07:00
Julia Ryan
45ff22f793 Add bang to word chars for wrapping (#37019)
Fixes #37010

Release Notes:

- N/A
2025-08-27 17:17:34 +00:00
Umesh Yadav
fead511df9 docs: Update Gemini CLI version requirements and install instructions (#37008)
Gemini cli - 0.2.0 is no longer in preview it's the latest version and
released as of today.

Release Notes:

- N/A

Signed-off-by: Umesh Yadav <git@umesh.dev>
2025-08-27 12:59:30 -04:00
Cole Miller
07373d15ef acp: Fix gemini process being leaked (#37012)
Release Notes:

- acp: Fixed a bug that caused external agent server subprocesses to be
leaked.

---------

Co-authored-by: Agus Zubiaga <agus@zed.dev>
Co-authored-by: Bennet Bo Fenner <bennetbo@gmx.de>
Co-authored-by: Antonio Scandurra <me@as-cii.com>
2025-08-27 16:21:28 +00:00
Bennet Bo Fenner
b5e9b65e8c acp: Fix model selector sometimes showing no models (#37006)
Release Notes:

- acp: Fix an issue where the model selector would sometimes be empty

---------

Co-authored-by: Antonio Scandurra <me@as-cii.com>
2025-08-27 15:39:39 +00:00
Smit Barmase
5d7f12ce88 project: Add dynamic capabilities registration for "workspace/didChangeWorkspaceFolders" (#37005)
Fixes missing capability registration for
"workspace/didChangeWorkspaceFolders".

```
WARN  [project::lsp_store] unhandled capability registration: Registration { id: "e288546c-4458-401a-a029-bbba759d5a71", method: "workspace/didChangeWorkspaceFolders", register_options: Some(Object {}) }
```

We already correctly send back events to server on workspace add and
remove by checking this capability.
cf89691b85/crates/lsp/src/lsp.rs (L1353)


cf89691b85/crates/lsp/src/lsp.rs (L1388)

Release Notes:

- N/A
2025-08-27 21:01:36 +05:30
Peter Tripp
1b9c471204 Fix 'Edit in Debug.json' in debugger::Start modal (#37002)
Closes https://github.com/zed-industries/zed/issues/36992

Release Notes:

- N/A
2025-08-27 10:51:26 -04:00
Bennet Bo Fenner
8cf663011f acp: Add more logs to model selector to diagnose issue (#36997)
Release Notes:

- N/A

---------

Co-authored-by: Antonio Scandurra <me@as-cii.com>
Co-authored-by: Cole Miller <cole@zed.dev>
Co-authored-by: Joseph T. Lyons <JosephTLyons@gmail.com>
Co-authored-by: Katie Geer <katie@zed.dev>
2025-08-27 12:53:07 +00:00
Finn Evers
54f9b67de2 docs: Document more settings (#36993)
Within our hosted docs, we are missing documentation for quite a lot of
settings - sometimes for newer settings, sometimes for settings that are
more than two years old. This leads (amongst other things) to feature
requests for features that are already supported, false issue reports
(because people couldn't find the setting for what caused the issue
within the documentation) and generally just takes time for for both
these affected by the missing documentation as well as these handling
the questions around it.

This change here takes a stab at the problem by adding more
documentation for a lot supported setting (not all of it) as well as
reorganizing some settings so that some stuff can (hopefully) be found
more easily. Eventually, we should find a better method for this, but
it's still better than informing people for the n-th time that we e.g.
have `agent_font_size` for the agent panel. Manually audited twice but
I'll take another thorough look before merging.

Release Notes:

- N/A
2025-08-27 14:51:06 +02:00
Danilo Leal
d99a17e357 docs: Add ACP-related content (#36966)
Release Notes:

- N/A

---------

Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
Co-authored-by: Bennet Bo Fenner <bennetbo@gmx.de>
Co-authored-by: Antonio Scandurra <me@as-cii.com>
Co-authored-by: Matt Miller <mattrx@gmail.com>
2025-08-27 14:12:52 +02:00
Bennet Bo Fenner
c72e594afe acp: Fix model selector sometimes showing no models (#36995)
Release Notes:

- N/A
2025-08-27 13:08:03 +02:00
Antonio Scandurra
b4d4294bee Restore token count for text threads (#36989)
Release Notes:

- N/A

Co-authored-by: Bennet Bo Fenner <bennetbo@gmx.de>
2025-08-27 09:29:17 +00:00
Antonio Scandurra
e5c0614e88 Ensure we use the new agent when opening the panel for the first time (#36988)
Release Notes:

- N/A
2025-08-27 09:18:15 +00:00
Smit Barmase
ea347b0aa1 project: Handle capabilities parse for more methods when registerOptions doesn't exist (#36984)
Closes #36938

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

When `registerOptions` is `None`, we should fall back instead of
skipping capability registration.

1. `Option<OneOf<bool, T>>`, where `T` is struct – handled in the
attached PR 
2. `Option<T>`, where `T` is an enum that can be `Simple(bool)` or
`Options(S)` – this PR 
3. `Option<T>`, where `T` is struct – we should fall back to default
values for these options ⚠️

Release Notes:

- Fixed an issue where hover popovers would not appear in language
servers like Java.
2025-08-27 13:00:10 +05:30
Finn Evers
a03897012e Swap NewlineBelow and NewlineAbove bindings for default linux keymap (#36939)
Closes https://github.com/zed-industries/zed/issues/33725

The default bindings for the `editor::NewlineAbove` and
`editor::NewlineBelow` actions in the default keymap were accidentally
swapped some time ago. This causes confusion, as normally these are the
other way around.

This PR fixes this by swapping these back, which also matches what
[VSCode does by
default](https://code.visualstudio.com/shortcuts/keyboard-shortcuts-linux.pdf).

Release Notes:

- Swapped the default bindings for `editor::NewlineBelow` and
`editor::NewlineAbove` for Linux and Windows to align more with other
editors.
2025-08-27 07:06:33 +00:00
Conrad Irwin
f4071bdd8e acp: Upgrade errors (#36980)
- **Pass --engine-strict to gemini install command**
- **Make it clearer that if upgrading fails, you need to fix i**

Closes #ISSUE

Release Notes:

- N/A
2025-08-27 00:24:56 -06:00
Caio Piccirillo
abd6009b41 Enhance syntax highlight for C++20 keywords (#36817)
Closes #36439 and #32999 

## C++20 modules:
Before (Zed Preview v0.201.3):
<img width="1048" height="704" alt="image"
src="https://github.com/user-attachments/assets/8eaaf77f-4e27-4a5a-9e87-4e5ba7293990"
/>
After:
<img width="1048" height="704" alt="image"
src="https://github.com/user-attachments/assets/df8d0b2c-f2d0-4b0e-9a52-495e6be5a8c0"
/>

## C++20 coroutines:
Before (Zed Preview v0.201.3):
<img width="1048" height="704" alt="image"
src="https://github.com/user-attachments/assets/652191ec-a653-444d-a239-da3e4e4b661e"
/>
After:
<img width="1048" height="704" alt="image"
src="https://github.com/user-attachments/assets/36947eb5-8997-483a-b36c-8af84872b158"
/>

## Logical operators:
Before (Zed Preview v0.201.3):
<img width="511" height="102" alt="image"
src="https://github.com/user-attachments/assets/9bf95bac-b076-4edd-a1f3-c3dfee98c2fd"
/>

After:
<img width="511" height="102" alt="image"
src="https://github.com/user-attachments/assets/82c7564d-b94d-41f5-9c48-e39fe3ba3b3e"
/>

## Operator keyword:
Before (Zed Preview v0.201.3):
<img width="591" height="381" alt="image"
src="https://github.com/user-attachments/assets/1d9dad05-2d86-4566-97f4-aff440dcd1df"
/>

After:
<img width="591" height="381" alt="image"
src="https://github.com/user-attachments/assets/a1ca289a-8a5d-4ffd-96db-0d511405da4b"
/>

## Goto:
Before (Zed Preview v0.201.3):
<img width="610" height="430" alt="image"
src="https://github.com/user-attachments/assets/2d00382b-d1ad-4e36-a3ee-88e06ec528ed"
/>

After:
<img width="610" height="430" alt="image"
src="https://github.com/user-attachments/assets/de887b21-66f0-4a70-9ed2-e18dbb3c81c9"
/>

Release Notes:

- Enhance keyword highlighting for C++
2025-08-27 04:31:57 +00:00
Joseph T. Lyons
a3e1611fa8 Bump Zed to v0.203 (#36975)
Release Notes:

- N/A
2025-08-27 02:52:24 +00:00
363 changed files with 15160 additions and 5181 deletions

3
.gitattributes vendored
View File

@@ -1,2 +1,5 @@
# Prevent GitHub from displaying comments within JSON files as errors.
*.json linguist-language=JSON-with-Comments
# Ensure the WSL script always has LF line endings, even on Windows
crates/zed/resources/windows/zed-wsl text eol=lf

View File

@@ -20,167 +20,8 @@ runs:
with:
node-version: "18"
- name: Configure crash dumps
shell: powershell
run: |
# Record the start time for this CI run
$runStartTime = Get-Date
$runStartTimeStr = $runStartTime.ToString("yyyy-MM-dd HH:mm:ss")
Write-Host "CI run started at: $runStartTimeStr"
# Save the timestamp for later use
echo "CI_RUN_START_TIME=$($runStartTime.Ticks)" >> $env:GITHUB_ENV
# Create crash dump directory in workspace (non-persistent)
$dumpPath = "$env:GITHUB_WORKSPACE\crash_dumps"
New-Item -ItemType Directory -Force -Path $dumpPath | Out-Null
Write-Host "Setting up crash dump detection..."
Write-Host "Workspace dump path: $dumpPath"
# Note: We're NOT modifying registry on stateful runners
# Instead, we'll check default Windows crash locations after tests
- name: Run tests
shell: powershell
working-directory: ${{ inputs.working-directory }}
run: |
$env:RUST_BACKTRACE = "full"
# Enable Windows debugging features
$env:_NT_SYMBOL_PATH = "srv*https://msdl.microsoft.com/download/symbols"
# .NET crash dump environment variables (ephemeral)
$env:COMPlus_DbgEnableMiniDump = "1"
$env:COMPlus_DbgMiniDumpType = "4"
$env:COMPlus_CreateDumpDiagnostics = "1"
cargo nextest run --workspace --no-fail-fast
- name: Analyze crash dumps
if: always()
shell: powershell
run: |
Write-Host "Checking for crash dumps..."
# Get the CI run start time from the environment
$runStartTime = [DateTime]::new([long]$env:CI_RUN_START_TIME)
Write-Host "Only analyzing dumps created after: $($runStartTime.ToString('yyyy-MM-dd HH:mm:ss'))"
# Check all possible crash dump locations
$searchPaths = @(
"$env:GITHUB_WORKSPACE\crash_dumps",
"$env:LOCALAPPDATA\CrashDumps",
"$env:TEMP",
"$env:GITHUB_WORKSPACE",
"$env:USERPROFILE\AppData\Local\CrashDumps",
"C:\Windows\System32\config\systemprofile\AppData\Local\CrashDumps"
)
$dumps = @()
foreach ($path in $searchPaths) {
if (Test-Path $path) {
Write-Host "Searching in: $path"
$found = Get-ChildItem "$path\*.dmp" -ErrorAction SilentlyContinue | Where-Object {
$_.CreationTime -gt $runStartTime
}
if ($found) {
$dumps += $found
Write-Host " Found $($found.Count) dump(s) from this CI run"
}
}
}
if ($dumps) {
Write-Host "Found $($dumps.Count) crash dump(s)"
# Install debugging tools if not present
$cdbPath = "C:\Program Files (x86)\Windows Kits\10\Debuggers\x64\cdb.exe"
if (-not (Test-Path $cdbPath)) {
Write-Host "Installing Windows Debugging Tools..."
$url = "https://go.microsoft.com/fwlink/?linkid=2237387"
Invoke-WebRequest -Uri $url -OutFile winsdksetup.exe
Start-Process -Wait winsdksetup.exe -ArgumentList "/features OptionId.WindowsDesktopDebuggers /quiet"
}
foreach ($dump in $dumps) {
Write-Host "`n=================================="
Write-Host "Analyzing crash dump: $($dump.Name)"
Write-Host "Size: $([math]::Round($dump.Length / 1MB, 2)) MB"
Write-Host "Time: $($dump.CreationTime)"
Write-Host "=================================="
# Set symbol path
$env:_NT_SYMBOL_PATH = "srv*C:\symbols*https://msdl.microsoft.com/download/symbols"
# Run analysis
$analysisOutput = & $cdbPath -z $dump.FullName -c "!analyze -v; ~*k; lm; q" 2>&1 | Out-String
# Extract key information
if ($analysisOutput -match "ExceptionCode:\s*([\w]+)") {
Write-Host "Exception Code: $($Matches[1])"
if ($Matches[1] -eq "c0000005") {
Write-Host "Exception Type: ACCESS VIOLATION"
}
}
if ($analysisOutput -match "EXCEPTION_RECORD:\s*(.+)") {
Write-Host "Exception Record: $($Matches[1])"
}
if ($analysisOutput -match "FAULTING_IP:\s*\n(.+)") {
Write-Host "Faulting Instruction: $($Matches[1])"
}
# Save full analysis
$analysisFile = "$($dump.FullName).analysis.txt"
$analysisOutput | Out-File -FilePath $analysisFile
Write-Host "`nFull analysis saved to: $analysisFile"
# Print stack trace section
Write-Host "`n--- Stack Trace Preview ---"
$stackSection = $analysisOutput -split "STACK_TEXT:" | Select-Object -Last 1
$stackLines = $stackSection -split "`n" | Select-Object -First 20
$stackLines | ForEach-Object { Write-Host $_ }
Write-Host "--- End Stack Trace Preview ---"
}
Write-Host "`n⚠ Crash dumps detected! Download the 'crash-dumps' artifact for detailed analysis."
# Copy dumps to workspace for artifact upload
$artifactPath = "$env:GITHUB_WORKSPACE\crash_dumps_collected"
New-Item -ItemType Directory -Force -Path $artifactPath | Out-Null
foreach ($dump in $dumps) {
$destName = "$($dump.Directory.Name)_$($dump.Name)"
Copy-Item $dump.FullName -Destination "$artifactPath\$destName"
if (Test-Path "$($dump.FullName).analysis.txt") {
Copy-Item "$($dump.FullName).analysis.txt" -Destination "$artifactPath\$destName.analysis.txt"
}
}
Write-Host "Copied $($dumps.Count) dump(s) to artifact directory"
} else {
Write-Host "No crash dumps from this CI run found"
}
- name: Upload crash dumps
if: always()
uses: actions/upload-artifact@v4
with:
name: crash-dumps-${{ github.run_id }}-${{ github.run_attempt }}
path: |
crash_dumps_collected/*.dmp
crash_dumps_collected/*.txt
if-no-files-found: ignore
retention-days: 7
- name: Check test results
shell: powershell
working-directory: ${{ inputs.working-directory }}
run: |
# Re-check test results to fail the job if tests failed
if ($LASTEXITCODE -ne 0) {
Write-Host "Tests failed with exit code: $LASTEXITCODE"
exit $LASTEXITCODE
}

View File

@@ -5,8 +5,8 @@ on:
# Fire every day at 7:00am UTC (Roughly before EU workday and after US workday)
- cron: "0 7 * * *"
push:
tags:
- "nightly"
branches:
- rustdoc-action
env:
CARGO_TERM_COLOR: always
@@ -18,118 +18,284 @@ env:
DIGITALOCEAN_SPACES_SECRET_KEY: ${{ secrets.DIGITALOCEAN_SPACES_SECRET_KEY }}
jobs:
style:
# style:
# timeout-minutes: 60
# name: Check formatting and Clippy lints
# if: github.repository_owner == 'zed-industries'
# runs-on:
# - self-hosted
# - macOS
# steps:
# - name: Checkout repo
# uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
# with:
# clean: false
# fetch-depth: 0
# - name: Run style checks
# uses: ./.github/actions/check_style
# - name: Run clippy
# run: ./script/clippy
# tests:
# timeout-minutes: 60
# name: Run tests
# if: github.repository_owner == 'zed-industries'
# runs-on:
# - self-hosted
# - macOS
# needs: style
# steps:
# - name: Checkout repo
# uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
# with:
# clean: false
# - name: Run tests
# uses: ./.github/actions/run_tests
# windows-tests:
# timeout-minutes: 60
# name: Run tests on Windows
# if: github.repository_owner == 'zed-industries'
# runs-on: [self-32vcpu-windows-2022]
# steps:
# - name: Checkout repo
# uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
# with:
# clean: false
# - name: Configure CI
# run: |
# New-Item -ItemType Directory -Path "./../.cargo" -Force
# Copy-Item -Path "./.cargo/ci-config.toml" -Destination "./../.cargo/config.toml"
# - name: Run tests
# uses: ./.github/actions/run_tests_windows
# - name: Limit target directory size
# run: ./script/clear-target-dir-if-larger-than.ps1 1024
# - name: Clean CI config file
# if: always()
# run: Remove-Item -Recurse -Path "./../.cargo" -Force -ErrorAction SilentlyContinue
# bundle-mac:
# timeout-minutes: 60
# name: Create a macOS bundle
# if: github.repository_owner == 'zed-industries'
# runs-on:
# - self-mini-macos
# needs: tests
# env:
# MACOS_CERTIFICATE: ${{ secrets.MACOS_CERTIFICATE }}
# MACOS_CERTIFICATE_PASSWORD: ${{ secrets.MACOS_CERTIFICATE_PASSWORD }}
# APPLE_NOTARIZATION_KEY: ${{ secrets.APPLE_NOTARIZATION_KEY }}
# APPLE_NOTARIZATION_KEY_ID: ${{ secrets.APPLE_NOTARIZATION_KEY_ID }}
# APPLE_NOTARIZATION_ISSUER_ID: ${{ secrets.APPLE_NOTARIZATION_ISSUER_ID }}
# steps:
# - name: Install Node
# uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
# with:
# node-version: "18"
# - name: Checkout repo
# uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
# with:
# clean: false
# - name: Set release channel to nightly
# run: |
# set -eu
# version=$(git rev-parse --short HEAD)
# echo "Publishing version: ${version} on release channel nightly"
# echo "nightly" > crates/zed/RELEASE_CHANNEL
# - name: Setup Sentry CLI
# uses: matbour/setup-sentry-cli@3e938c54b3018bdd019973689ef984e033b0454b #v2
# with:
# token: ${{ SECRETS.SENTRY_AUTH_TOKEN }}
# - name: Create macOS app bundle
# run: script/bundle-mac
# - name: Upload Zed Nightly
# run: script/upload-nightly macos
# bundle-linux-x86:
# timeout-minutes: 60
# name: Create a Linux *.tar.gz bundle for x86
# if: github.repository_owner == 'zed-industries'
# runs-on:
# - namespace-profile-16x32-ubuntu-2004 # ubuntu 20.04 for minimal glibc
# needs: tests
# steps:
# - name: Checkout repo
# uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
# with:
# clean: false
# - name: Add Rust to the PATH
# run: echo "$HOME/.cargo/bin" >> "$GITHUB_PATH"
# - name: Install Linux dependencies
# run: ./script/linux && ./script/install-mold 2.34.0
# - name: Setup Sentry CLI
# uses: matbour/setup-sentry-cli@3e938c54b3018bdd019973689ef984e033b0454b #v2
# with:
# token: ${{ SECRETS.SENTRY_AUTH_TOKEN }}
# - name: Limit target directory size
# run: script/clear-target-dir-if-larger-than 100
# - name: Set release channel to nightly
# run: |
# set -euo pipefail
# version=$(git rev-parse --short HEAD)
# echo "Publishing version: ${version} on release channel nightly"
# echo "nightly" > crates/zed/RELEASE_CHANNEL
# - name: Create Linux .tar.gz bundle
# run: script/bundle-linux
# - name: Upload Zed Nightly
# run: script/upload-nightly linux-targz
# bundle-linux-arm:
# timeout-minutes: 60
# name: Create a Linux *.tar.gz bundle for ARM
# if: github.repository_owner == 'zed-industries'
# runs-on:
# - namespace-profile-8x32-ubuntu-2004-arm-m4 # ubuntu 20.04 for minimal glibc
# needs: tests
# steps:
# - name: Checkout repo
# uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
# with:
# clean: false
# - name: Install Linux dependencies
# run: ./script/linux
# - name: Setup Sentry CLI
# uses: matbour/setup-sentry-cli@3e938c54b3018bdd019973689ef984e033b0454b #v2
# with:
# token: ${{ SECRETS.SENTRY_AUTH_TOKEN }}
# - name: Limit target directory size
# run: script/clear-target-dir-if-larger-than 100
# - name: Set release channel to nightly
# run: |
# set -euo pipefail
# version=$(git rev-parse --short HEAD)
# echo "Publishing version: ${version} on release channel nightly"
# echo "nightly" > crates/zed/RELEASE_CHANNEL
# - name: Create Linux .tar.gz bundle
# run: script/bundle-linux
# - name: Upload Zed Nightly
# run: script/upload-nightly linux-targz
# freebsd:
# timeout-minutes: 60
# if: false && github.repository_owner == 'zed-industries'
# runs-on: github-8vcpu-ubuntu-2404
# needs: tests
# name: Build Zed on FreeBSD
# steps:
# - uses: actions/checkout@v4
# - name: Build FreeBSD remote-server
# id: freebsd-build
# uses: vmactions/freebsd-vm@c3ae29a132c8ef1924775414107a97cac042aad5 # v1.2.0
# with:
# # envs: "MYTOKEN MYTOKEN2"
# usesh: true
# release: 13.5
# copyback: true
# prepare: |
# pkg install -y \
# bash curl jq git \
# rustup-init cmake-core llvm-devel-lite pkgconf protobuf # ibx11 alsa-lib rust-bindgen-cli
# run: |
# freebsd-version
# sysctl hw.model
# sysctl hw.ncpu
# sysctl hw.physmem
# sysctl hw.usermem
# git config --global --add safe.directory /home/runner/work/zed/zed
# rustup-init --profile minimal --default-toolchain none -y
# . "$HOME/.cargo/env"
# ./script/bundle-freebsd
# mkdir -p out/
# mv "target/zed-remote-server-freebsd-x86_64.gz" out/
# rm -rf target/
# cargo clean
# - name: Upload Zed Nightly
# run: script/upload-nightly freebsd
# bundle-nix:
# name: Build and cache Nix package
# needs: tests
# secrets: inherit
# uses: ./.github/workflows/nix.yml
# bundle-windows-x64:
# timeout-minutes: 60
# name: Create a Windows installer
# if: github.repository_owner == 'zed-industries'
# runs-on: [self-32vcpu-windows-2022]
# needs: windows-tests
# env:
# AZURE_TENANT_ID: ${{ secrets.AZURE_SIGNING_TENANT_ID }}
# AZURE_CLIENT_ID: ${{ secrets.AZURE_SIGNING_CLIENT_ID }}
# AZURE_CLIENT_SECRET: ${{ secrets.AZURE_SIGNING_CLIENT_SECRET }}
# ACCOUNT_NAME: ${{ vars.AZURE_SIGNING_ACCOUNT_NAME }}
# CERT_PROFILE_NAME: ${{ vars.AZURE_SIGNING_CERT_PROFILE_NAME }}
# ENDPOINT: ${{ vars.AZURE_SIGNING_ENDPOINT }}
# FILE_DIGEST: SHA256
# TIMESTAMP_DIGEST: SHA256
# TIMESTAMP_SERVER: "http://timestamp.acs.microsoft.com"
# steps:
# - name: Checkout repo
# uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
# with:
# clean: false
# - name: Set release channel to nightly
# working-directory: ${{ env.ZED_WORKSPACE }}
# run: |
# $ErrorActionPreference = "Stop"
# $version = git rev-parse --short HEAD
# Write-Host "Publishing version: $version on release channel nightly"
# "nightly" | Set-Content -Path "crates/zed/RELEASE_CHANNEL"
# - name: Setup Sentry CLI
# uses: matbour/setup-sentry-cli@3e938c54b3018bdd019973689ef984e033b0454b #v2
# with:
# token: ${{ SECRETS.SENTRY_AUTH_TOKEN }}
# - name: Build Zed installer
# working-directory: ${{ env.ZED_WORKSPACE }}
# run: script/bundle-windows.ps1
# - name: Upload Zed Nightly
# working-directory: ${{ env.ZED_WORKSPACE }}
# run: script/upload-nightly.ps1 windows
gpui-docs:
timeout-minutes: 60
name: Check formatting and Clippy lints
name: Render rust docs for gpui
if: github.repository_owner == 'zed-industries'
# TODO: build for multiple targets?
runs-on:
- self-hosted
- macOS
steps:
- name: Checkout repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
with:
clean: false
fetch-depth: 0
- name: Run style checks
uses: ./.github/actions/check_style
- name: Run clippy
run: ./script/clippy
tests:
timeout-minutes: 60
name: Run tests
if: github.repository_owner == 'zed-industries'
runs-on:
- self-hosted
- macOS
needs: style
steps:
- name: Checkout repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
with:
clean: false
- name: Run tests
uses: ./.github/actions/run_tests
windows-tests:
timeout-minutes: 60
name: Run tests on Windows
if: github.repository_owner == 'zed-industries'
runs-on: [self-32vcpu-windows-2022]
steps:
- name: Checkout repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
with:
clean: false
- name: Configure CI
run: |
New-Item -ItemType Directory -Path "./../.cargo" -Force
Copy-Item -Path "./.cargo/ci-config.toml" -Destination "./../.cargo/config.toml"
- name: Run tests
uses: ./.github/actions/run_tests_windows
- name: Limit target directory size
run: ./script/clear-target-dir-if-larger-than.ps1 1024
- name: Clean CI config file
if: always()
run: Remove-Item -Recurse -Path "./../.cargo" -Force -ErrorAction SilentlyContinue
bundle-mac:
timeout-minutes: 60
name: Create a macOS bundle
if: github.repository_owner == 'zed-industries'
runs-on:
- self-mini-macos
needs: tests
env:
MACOS_CERTIFICATE: ${{ secrets.MACOS_CERTIFICATE }}
MACOS_CERTIFICATE_PASSWORD: ${{ secrets.MACOS_CERTIFICATE_PASSWORD }}
APPLE_NOTARIZATION_KEY: ${{ secrets.APPLE_NOTARIZATION_KEY }}
APPLE_NOTARIZATION_KEY_ID: ${{ secrets.APPLE_NOTARIZATION_KEY_ID }}
APPLE_NOTARIZATION_ISSUER_ID: ${{ secrets.APPLE_NOTARIZATION_ISSUER_ID }}
steps:
- name: Install Node
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: "18"
- name: Checkout repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
with:
clean: false
- name: Set release channel to nightly
run: |
set -eu
version=$(git rev-parse --short HEAD)
echo "Publishing version: ${version} on release channel nightly"
echo "nightly" > crates/zed/RELEASE_CHANNEL
- name: Setup Sentry CLI
uses: matbour/setup-sentry-cli@3e938c54b3018bdd019973689ef984e033b0454b #v2
with:
token: ${{ SECRETS.SENTRY_AUTH_TOKEN }}
- name: Create macOS app bundle
run: script/bundle-mac
- name: Upload Zed Nightly
run: script/upload-nightly macos
bundle-linux-x86:
timeout-minutes: 60
name: Create a Linux *.tar.gz bundle for x86
if: github.repository_owner == 'zed-industries'
runs-on:
- namespace-profile-16x32-ubuntu-2004 # ubuntu 20.04 for minimal glibc
needs: tests
- github-16vcpu-ubuntu-2404
# needs: tests
continue-on-error: true
steps:
- name: Checkout repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
@@ -137,187 +303,61 @@ jobs:
clean: false
- name: Add Rust to the PATH
run: echo "$HOME/.cargo/bin" >> "$GITHUB_PATH"
run: echo "$HOME/.cargo/bin" >> $GITHUB_PATH
- name: Install Linux dependencies
run: ./script/linux && ./script/install-mold 2.34.0
- name: Setup Sentry CLI
uses: matbour/setup-sentry-cli@3e938c54b3018bdd019973689ef984e033b0454b #v2
with:
token: ${{ SECRETS.SENTRY_AUTH_TOKEN }}
- name: Limit target directory size
run: script/clear-target-dir-if-larger-than 100
- name: Set release channel to nightly
run: |
set -euo pipefail
version=$(git rev-parse --short HEAD)
echo "Publishing version: ${version} on release channel nightly"
echo "nightly" > crates/zed/RELEASE_CHANNEL
- name: Build rustdocs
run: cargo doc -p gpui --no-deps
- name: Create Linux .tar.gz bundle
run: script/bundle-linux
- name: Upload Zed Nightly
run: script/upload-nightly linux-targz
bundle-linux-arm:
timeout-minutes: 60
name: Create a Linux *.tar.gz bundle for ARM
if: github.repository_owner == 'zed-industries'
runs-on:
- namespace-profile-8x32-ubuntu-2004-arm-m4 # ubuntu 20.04 for minimal glibc
needs: tests
steps:
- name: Checkout repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
- name: Check for broken links (in HTML)
uses: lycheeverse/lychee-action@82202e5e9c2f4ef1a55a3d02563e1cb6041e5332 # v2.4.1
with:
clean: false
args: --no-progress --exclude '^http' 'target/doc/'
fail: true
- name: Install Linux dependencies
run: ./script/linux
- name: Setup Sentry CLI
uses: matbour/setup-sentry-cli@3e938c54b3018bdd019973689ef984e033b0454b #v2
- name: Deploy rustdoc
uses: cloudflare/wrangler-action@da0e0dfe58b7a431659754fdf3f186c529afbe65 # v3
with:
token: ${{ SECRETS.SENTRY_AUTH_TOKEN }}
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
command: pages deploy target/doc --project-name=gpui-rustdoc
- name: Limit target directory size
run: script/clear-target-dir-if-larger-than 100
# update-nightly-tag:
# name: Update nightly tag
# if: github.repository_owner == 'zed-industries'
# runs-on: namespace-profile-2x4-ubuntu-2404
# needs:
# - bundle-mac
# - bundle-linux-x86
# - bundle-linux-arm
# - bundle-windows-x64
# steps:
# - name: Checkout repo
# uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
# with:
# fetch-depth: 0
- name: Set release channel to nightly
run: |
set -euo pipefail
version=$(git rev-parse --short HEAD)
echo "Publishing version: ${version} on release channel nightly"
echo "nightly" > crates/zed/RELEASE_CHANNEL
# - name: Update nightly tag
# run: |
# if [ "$(git rev-parse nightly)" = "$(git rev-parse HEAD)" ]; then
# echo "Nightly tag already points to current commit. Skipping tagging."
# exit 0
# fi
# git config user.name github-actions
# git config user.email github-actions@github.com
# git tag -f nightly
# git push origin nightly --force
- name: Create Linux .tar.gz bundle
run: script/bundle-linux
- name: Upload Zed Nightly
run: script/upload-nightly linux-targz
freebsd:
timeout-minutes: 60
if: false && github.repository_owner == 'zed-industries'
runs-on: github-8vcpu-ubuntu-2404
needs: tests
name: Build Zed on FreeBSD
steps:
- uses: actions/checkout@v4
- name: Build FreeBSD remote-server
id: freebsd-build
uses: vmactions/freebsd-vm@c3ae29a132c8ef1924775414107a97cac042aad5 # v1.2.0
with:
# envs: "MYTOKEN MYTOKEN2"
usesh: true
release: 13.5
copyback: true
prepare: |
pkg install -y \
bash curl jq git \
rustup-init cmake-core llvm-devel-lite pkgconf protobuf # ibx11 alsa-lib rust-bindgen-cli
run: |
freebsd-version
sysctl hw.model
sysctl hw.ncpu
sysctl hw.physmem
sysctl hw.usermem
git config --global --add safe.directory /home/runner/work/zed/zed
rustup-init --profile minimal --default-toolchain none -y
. "$HOME/.cargo/env"
./script/bundle-freebsd
mkdir -p out/
mv "target/zed-remote-server-freebsd-x86_64.gz" out/
rm -rf target/
cargo clean
- name: Upload Zed Nightly
run: script/upload-nightly freebsd
bundle-nix:
name: Build and cache Nix package
needs: tests
secrets: inherit
uses: ./.github/workflows/nix.yml
bundle-windows-x64:
timeout-minutes: 60
name: Create a Windows installer
if: github.repository_owner == 'zed-industries'
runs-on: [self-32vcpu-windows-2022]
needs: windows-tests
env:
AZURE_TENANT_ID: ${{ secrets.AZURE_SIGNING_TENANT_ID }}
AZURE_CLIENT_ID: ${{ secrets.AZURE_SIGNING_CLIENT_ID }}
AZURE_CLIENT_SECRET: ${{ secrets.AZURE_SIGNING_CLIENT_SECRET }}
ACCOUNT_NAME: ${{ vars.AZURE_SIGNING_ACCOUNT_NAME }}
CERT_PROFILE_NAME: ${{ vars.AZURE_SIGNING_CERT_PROFILE_NAME }}
ENDPOINT: ${{ vars.AZURE_SIGNING_ENDPOINT }}
FILE_DIGEST: SHA256
TIMESTAMP_DIGEST: SHA256
TIMESTAMP_SERVER: "http://timestamp.acs.microsoft.com"
steps:
- name: Checkout repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
with:
clean: false
- name: Set release channel to nightly
working-directory: ${{ env.ZED_WORKSPACE }}
run: |
$ErrorActionPreference = "Stop"
$version = git rev-parse --short HEAD
Write-Host "Publishing version: $version on release channel nightly"
"nightly" | Set-Content -Path "crates/zed/RELEASE_CHANNEL"
- name: Setup Sentry CLI
uses: matbour/setup-sentry-cli@3e938c54b3018bdd019973689ef984e033b0454b #v2
with:
token: ${{ SECRETS.SENTRY_AUTH_TOKEN }}
- name: Build Zed installer
working-directory: ${{ env.ZED_WORKSPACE }}
run: script/bundle-windows.ps1
- name: Upload Zed Nightly
working-directory: ${{ env.ZED_WORKSPACE }}
run: script/upload-nightly.ps1 windows
update-nightly-tag:
name: Update nightly tag
if: github.repository_owner == 'zed-industries'
runs-on: namespace-profile-2x4-ubuntu-2404
needs:
- bundle-mac
- bundle-linux-x86
- bundle-linux-arm
- bundle-windows-x64
steps:
- name: Checkout repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
with:
fetch-depth: 0
- name: Update nightly tag
run: |
if [ "$(git rev-parse nightly)" = "$(git rev-parse HEAD)" ]; then
echo "Nightly tag already points to current commit. Skipping tagging."
exit 0
fi
git config user.name github-actions
git config user.email github-actions@github.com
git tag -f nightly
git push origin nightly --force
- name: Create Sentry release
uses: getsentry/action-release@526942b68292201ac6bbb99b9a0747d4abee354c # v3
env:
SENTRY_ORG: zed-dev
SENTRY_PROJECT: zed
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
with:
environment: production
# - name: Create Sentry release
# uses: getsentry/action-release@526942b68292201ac6bbb99b9a0747d4abee354c # v3
# env:
# SENTRY_ORG: zed-dev
# SENTRY_PROJECT: zed
# SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
# with:
# environment: production

View File

@@ -27,6 +27,22 @@ By effectively engaging with the Zed team and community early in your process, w
We plan to set aside time each week to pair program with contributors on promising pull requests in Zed. This will be an experiment. We tend to prefer pairing over async code review on our team, and we'd like to see how well it works in an open source setting. If we're finding it difficult to get on the same page with async review, we may ask you to pair with us if you're open to it. The closer a contribution is to the goals outlined in our roadmap, the more likely we'll be to spend time pairing on it.
## Mandatory PR contents
Please ensure the PR contains
- Before & after screenshots, if there are visual adjustments introduced.
Examples of visual adjustments: tree-sitter query updates, UI changes, etc.
- A disclosure of the AI assistance usage, if any was used.
Any kind of AI assistance must be disclosed in the PR, along with the extent to which AI assistance was used (e.g. docs only vs. code generation).
If the PR responses are being generated by an AI, disclose that as well.
As a small exception, trivial tab-completion doesn't need to be disclosed, as long as it's limited to single keywords or short phrases.
## Tips to improve the chances of your PR getting reviewed and merged
- Discuss your plans ahead of time with the team
@@ -49,6 +65,8 @@ If you would like to add a new icon to the Zed icon theme, [open a Discussion](h
## Bird's-eye view of Zed
We suggest you keep the [zed glossary](docs/src/development/GLOSSARY.md) at your side when starting out. It lists and explains some of the structures and terms you will see throughout the codebase.
Zed is made up of several smaller crates - let's go over those you're most likely to interact with:
- [`gpui`](/crates/gpui) is a GPU-accelerated UI framework which provides all of the building blocks for Zed. **We recommend familiarizing yourself with the root level GPUI documentation.**

186
Cargo.lock generated
View File

@@ -195,9 +195,9 @@ dependencies = [
[[package]]
name = "agent-client-protocol"
version = "0.2.0-alpha.6"
version = "0.2.0-alpha.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6d02292efd75080932b6466471d428c70e2ac06908ae24792fc7c36ecbaf67ca"
checksum = "603941db1d130ee275840c465b73a2312727d4acef97449550ccf033de71301f"
dependencies = [
"anyhow",
"async-broadcast",
@@ -508,7 +508,7 @@ dependencies = [
"parking_lot",
"piper",
"polling",
"regex-automata 0.4.9",
"regex-automata",
"rustix-openpty",
"serde",
"signal-hook",
@@ -2458,7 +2458,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "234113d19d0d7d613b40e86fb654acf958910802bcceab913a4f9e7cda03b1a4"
dependencies = [
"memchr",
"regex-automata 0.4.9",
"regex-automata",
"serde",
]
@@ -4733,7 +4733,7 @@ version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b545b8c50194bdd008283985ab0b31dba153cfd5b3066a92770634fbc0d7d291"
dependencies = [
"nu-ansi-term 0.50.1",
"nu-ansi-term",
]
[[package]]
@@ -5632,8 +5632,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "531e46835a22af56d1e3b66f04844bed63158bc094a628bec1d321d9b4c44bf2"
dependencies = [
"bit-set 0.5.3",
"regex-automata 0.4.9",
"regex-syntax 0.8.5",
"regex-automata",
"regex-syntax",
]
[[package]]
@@ -5643,8 +5643,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e24cb5a94bcae1e5408b0effca5cd7172ea3c5755049c5f3af4cd283a165298"
dependencies = [
"bit-set 0.8.0",
"regex-automata 0.4.9",
"regex-syntax 0.8.5",
"regex-automata",
"regex-syntax",
]
[[package]]
@@ -7294,8 +7294,8 @@ dependencies = [
"aho-corasick",
"bstr",
"log",
"regex-automata 0.4.9",
"regex-syntax 0.8.5",
"regex-automata",
"regex-syntax",
]
[[package]]
@@ -8300,7 +8300,7 @@ dependencies = [
"globset",
"log",
"memchr",
"regex-automata 0.4.9",
"regex-automata",
"same-file",
"walkdir",
"winapi-util",
@@ -8899,7 +8899,7 @@ dependencies = [
"percent-encoding",
"referencing",
"regex",
"regex-syntax 0.8.5",
"regex-syntax",
"reqwest 0.12.15 (registry+https://github.com/rust-lang/crates.io-index)",
"serde",
"serde_json",
@@ -8952,6 +8952,44 @@ dependencies = [
"uuid",
]
[[package]]
name = "keymap_editor"
version = "0.1.0"
dependencies = [
"anyhow",
"collections",
"command_palette",
"component",
"db",
"editor",
"fs",
"fuzzy",
"gpui",
"itertools 0.14.0",
"language",
"log",
"menu",
"notifications",
"paths",
"project",
"search",
"serde",
"serde_json",
"settings",
"telemetry",
"tempfile",
"theme",
"tree-sitter-json",
"tree-sitter-rust",
"ui",
"ui_input",
"util",
"vim",
"workspace",
"workspace-hack",
"zed_actions",
]
[[package]]
name = "khronos-egl"
version = "6.0.0"
@@ -9109,6 +9147,7 @@ dependencies = [
"icons",
"image",
"log",
"open_router",
"parking_lot",
"proto",
"schemars",
@@ -9701,7 +9740,7 @@ dependencies = [
"lazy_static",
"proc-macro2",
"quote",
"regex-syntax 0.8.5",
"regex-syntax",
"rustc_version",
"syn 2.0.101",
]
@@ -9773,7 +9812,7 @@ dependencies = [
[[package]]
name = "lsp-types"
version = "0.95.1"
source = "git+https://github.com/zed-industries/lsp-types?rev=39f629bdd03d59abd786ed9fc27e8bca02c0c0ec#39f629bdd03d59abd786ed9fc27e8bca02c0c0ec"
source = "git+https://github.com/zed-industries/lsp-types?rev=0874f8742fe55b4dc94308c1e3c0069710d8eeaf#0874f8742fe55b4dc94308c1e3c0069710d8eeaf"
dependencies = [
"bitflags 1.3.2",
"serde",
@@ -9916,9 +9955,11 @@ dependencies = [
"editor",
"fs",
"gpui",
"html5ever 0.27.0",
"language",
"linkify",
"log",
"markup5ever_rcdom",
"pretty_assertions",
"pulldown-cmark 0.12.2",
"settings",
@@ -9979,11 +10020,11 @@ dependencies = [
[[package]]
name = "matchers"
version = "0.1.0"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558"
checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9"
dependencies = [
"regex-automata 0.1.10",
"regex-automata",
]
[[package]]
@@ -10684,16 +10725,6 @@ dependencies = [
"winapi",
]
[[package]]
name = "nu-ansi-term"
version = "0.46.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84"
dependencies = [
"overload",
"winapi",
]
[[package]]
name = "nu-ansi-term"
version = "0.50.1"
@@ -11192,6 +11223,8 @@ dependencies = [
"schemars",
"serde",
"serde_json",
"strum 0.27.1",
"thiserror 2.0.12",
"workspace-hack",
]
@@ -11387,12 +11420,6 @@ version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a80800c0488c3a21695ea981a54918fbb37abf04f4d0720c453632255e2ff0e"
[[package]]
name = "overload"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39"
[[package]]
name = "p256"
version = "0.11.1"
@@ -13383,17 +13410,8 @@ checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191"
dependencies = [
"aho-corasick",
"memchr",
"regex-automata 0.4.9",
"regex-syntax 0.8.5",
]
[[package]]
name = "regex-automata"
version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132"
dependencies = [
"regex-syntax 0.6.29",
"regex-automata",
"regex-syntax",
]
[[package]]
@@ -13404,7 +13422,7 @@ checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908"
dependencies = [
"aho-corasick",
"memchr",
"regex-syntax 0.8.5",
"regex-syntax",
]
[[package]]
@@ -13413,12 +13431,6 @@ version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "53a49587ad06b26609c52e423de037e7f57f20d53535d66e08c695f347df952a"
[[package]]
name = "regex-syntax"
version = "0.6.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1"
[[package]]
name = "regex-syntax"
version = "0.8.5"
@@ -13736,7 +13748,6 @@ dependencies = [
"regex",
"reqwest 0.12.15 (git+https://github.com/zed-industries/reqwest.git?rev=951c770a32f1998d6e999cef3e59e0013e6c4415)",
"serde",
"smol",
"tokio",
"workspace-hack",
]
@@ -14857,6 +14868,8 @@ dependencies = [
"serde_derive",
"serde_json",
"serde_json_lenient",
"serde_path_to_error",
"settings_ui_macros",
"smallvec",
"tree-sitter",
"tree-sitter-json",
@@ -14892,39 +14905,30 @@ name = "settings_ui"
version = "0.1.0"
dependencies = [
"anyhow",
"collections",
"command_palette",
"command_palette_hooks",
"component",
"db",
"debugger_ui",
"editor",
"feature_flags",
"fs",
"fuzzy",
"gpui",
"itertools 0.14.0",
"language",
"log",
"menu",
"notifications",
"paths",
"project",
"search",
"serde",
"serde_json",
"settings",
"telemetry",
"tempfile",
"smallvec",
"theme",
"tree-sitter-json",
"tree-sitter-rust",
"ui",
"ui_input",
"util",
"vim",
"workspace",
"workspace-hack",
"zed_actions",
]
[[package]]
name = "settings_ui_macros"
version = "0.1.0"
dependencies = [
"heck 0.5.0",
"proc-macro2",
"quote",
"syn 2.0.101",
"workspace-hack",
]
[[package]]
@@ -15310,7 +15314,6 @@ dependencies = [
"futures 0.3.31",
"indoc",
"libsqlite3-sys",
"log",
"parking_lot",
"smol",
"sqlformat",
@@ -16741,6 +16744,7 @@ dependencies = [
"db",
"gpui",
"http_client",
"keymap_editor",
"notifications",
"pretty_assertions",
"project",
@@ -16749,7 +16753,6 @@ dependencies = [
"schemars",
"serde",
"settings",
"settings_ui",
"smallvec",
"story",
"telemetry",
@@ -17118,14 +17121,14 @@ dependencies = [
[[package]]
name = "tracing-subscriber"
version = "0.3.19"
version = "0.3.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008"
checksum = "2054a14f5307d601f88daf0553e1cbf472acc4f2c51afab632431cdcd72124d5"
dependencies = [
"matchers",
"nu-ansi-term 0.46.0",
"nu-ansi-term",
"once_cell",
"regex",
"regex-automata",
"serde",
"serde_json",
"sharded-slab",
@@ -17156,7 +17159,7 @@ checksum = "a7cf18d43cbf0bfca51f657132cc616a5097edc4424d538bae6fa60142eaf9f0"
dependencies = [
"cc",
"regex",
"regex-syntax 0.8.5",
"regex-syntax",
"serde_json",
"streaming-iterator",
"tree-sitter-language",
@@ -17186,8 +17189,7 @@ dependencies = [
[[package]]
name = "tree-sitter-cpp"
version = "0.23.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df2196ea9d47b4ab4a31b9297eaa5a5d19a0b121dceb9f118f6790ad0ab94743"
source = "git+https://github.com/tree-sitter/tree-sitter-cpp?rev=5cb9b693cfd7bfacab1d9ff4acac1a4150700609#5cb9b693cfd7bfacab1d9ff4acac1a4150700609"
dependencies = [
"cc",
"tree-sitter-language",
@@ -19955,8 +19957,8 @@ dependencies = [
"rand_core 0.6.4",
"regalloc2",
"regex",
"regex-automata 0.4.9",
"regex-syntax 0.8.5",
"regex-automata",
"regex-syntax",
"ring",
"rust_decimal",
"rustc-hash 1.1.0",
@@ -20138,9 +20140,9 @@ dependencies = [
[[package]]
name = "xcb"
version = "1.5.0"
version = "1.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1e2f212bb1a92cd8caac8051b829a6582ede155ccb60b5d5908b81b100952be"
checksum = "f07c123b796139bfe0603e654eaf08e132e52387ba95b252c78bad3640ba37ea"
dependencies = [
"bitflags 1.3.2",
"libc",
@@ -20397,7 +20399,7 @@ dependencies = [
[[package]]
name = "zed"
version = "0.202.8"
version = "0.204.0"
dependencies = [
"acp_tools",
"activity_indicator",
@@ -20461,6 +20463,7 @@ dependencies = [
"itertools 0.14.0",
"jj_ui",
"journal",
"keymap_editor",
"language",
"language_extension",
"language_model",
@@ -20591,7 +20594,7 @@ dependencies = [
[[package]]
name = "zed_html"
version = "0.2.1"
version = "0.2.2"
dependencies = [
"zed_extension_api 0.1.0",
]
@@ -20790,6 +20793,7 @@ dependencies = [
"gpui",
"http_client",
"indoc",
"itertools 0.14.0",
"language",
"language_model",
"log",
@@ -20804,6 +20808,7 @@ dependencies = [
"serde",
"serde_json",
"settings",
"strum 0.27.1",
"telemetry",
"telemetry_events",
"theme",
@@ -20811,7 +20816,6 @@ dependencies = [
"tree-sitter-go",
"tree-sitter-rust",
"ui",
"unindent",
"util",
"uuid",
"workspace",

View File

@@ -54,6 +54,8 @@ members = [
"crates/deepseek",
"crates/diagnostics",
"crates/docs_preprocessor",
"crates/edit_prediction",
"crates/edit_prediction_button",
"crates/editor",
"crates/eval",
"crates/explorer_command_injector",
@@ -82,13 +84,12 @@ members = [
"crates/http_client_tls",
"crates/icons",
"crates/image_viewer",
"crates/edit_prediction",
"crates/edit_prediction_button",
"crates/inspector_ui",
"crates/install_cli",
"crates/jj",
"crates/jj_ui",
"crates/journal",
"crates/keymap_editor",
"crates/language",
"crates/language_extension",
"crates/language_model",
@@ -146,6 +147,7 @@ members = [
"crates/settings",
"crates/settings_profile_selector",
"crates/settings_ui",
"crates/settings_ui_macros",
"crates/snippet",
"crates/snippet_provider",
"crates/snippets_ui",
@@ -156,9 +158,9 @@ members = [
"crates/streaming_diff",
"crates/sum_tree",
"crates/supermaven",
"crates/system_specs",
"crates/supermaven_api",
"crates/svg_preview",
"crates/system_specs",
"crates/tab_switcher",
"crates/task",
"crates/tasks_ui",
@@ -297,9 +299,7 @@ git_hosting_providers = { path = "crates/git_hosting_providers" }
git_ui = { path = "crates/git_ui" }
go_to_line = { path = "crates/go_to_line" }
google_ai = { path = "crates/google_ai" }
gpui = { path = "crates/gpui", default-features = false, features = [
"http_client",
] }
gpui = { path = "crates/gpui", default-features = false }
gpui_macros = { path = "crates/gpui_macros" }
gpui_tokio = { path = "crates/gpui_tokio" }
html_to_markdown = { path = "crates/html_to_markdown" }
@@ -314,6 +314,7 @@ install_cli = { path = "crates/install_cli" }
jj = { path = "crates/jj" }
jj_ui = { path = "crates/jj_ui" }
journal = { path = "crates/journal" }
keymap_editor = { path = "crates/keymap_editor" }
language = { path = "crates/language" }
language_extension = { path = "crates/language_extension" }
language_model = { path = "crates/language_model" }
@@ -373,6 +374,7 @@ semantic_version = { path = "crates/semantic_version" }
session = { path = "crates/session" }
settings = { path = "crates/settings" }
settings_ui = { path = "crates/settings_ui" }
settings_ui_macros = { path = "crates/settings_ui_macros" }
snippet = { path = "crates/snippet" }
snippet_provider = { path = "crates/snippet_provider" }
snippets_ui = { path = "crates/snippets_ui" }
@@ -426,7 +428,7 @@ zlog_settings = { path = "crates/zlog_settings" }
# External crates
#
agent-client-protocol = { version = "0.2.0-alpha.6", features = ["unstable"]}
agent-client-protocol = { version = "0.2.0-alpha.4", features = ["unstable"]}
aho-corasick = "1.1"
alacritty_terminal = { git = "https://github.com/zed-industries/alacritty.git", branch = "add-hush-login-flag" }
any_vec = "0.14"
@@ -519,7 +521,7 @@ libc = "0.2"
libsqlite3-sys = { version = "0.30.1", features = ["bundled"] }
linkify = "0.10.0"
log = { version = "0.4.16", features = ["kv_unstable_serde", "serde"] }
lsp-types = { git = "https://github.com/zed-industries/lsp-types", rev = "39f629bdd03d59abd786ed9fc27e8bca02c0c0ec" }
lsp-types = { git = "https://github.com/zed-industries/lsp-types", rev = "0874f8742fe55b4dc94308c1e3c0069710d8eeaf" }
mach2 = "0.5"
markup5ever_rcdom = "0.3.0"
metal = "0.29"
@@ -588,6 +590,7 @@ serde_json_lenient = { version = "0.2", features = [
"preserve_order",
"raw_value",
] }
serde_path_to_error = "0.1.17"
serde_repr = "0.1"
serde_urlencoded = "0.7"
sha2 = "0.10"
@@ -624,7 +627,7 @@ tower-http = "0.4.4"
tree-sitter = { version = "0.25.6", features = ["wasm"] }
tree-sitter-bash = "0.25.0"
tree-sitter-c = "0.23"
tree-sitter-cpp = "0.23"
tree-sitter-cpp = { git = "https://github.com/tree-sitter/tree-sitter-cpp", rev = "5cb9b693cfd7bfacab1d9ff4acac1a4150700609" }
tree-sitter-css = "0.23"
tree-sitter-diff = "0.1.0"
tree-sitter-elixir = "0.3"
@@ -691,6 +694,7 @@ features = [
"Win32_Graphics_Dxgi_Common",
"Win32_Graphics_Gdi",
"Win32_Graphics_Imaging",
"Win32_Graphics_Hlsl",
"Win32_Networking_WinSock",
"Win32_Security",
"Win32_Security_Credentials",

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-list-filter-icon lucide-list-filter"><path d="M3 6h18"/><path d="M7 12h10"/><path d="M10 18h4"/></svg>

After

Width:  |  Height:  |  Size: 305 B

View File

@@ -16,7 +16,6 @@
"up": "menu::SelectPrevious",
"enter": "menu::Confirm",
"ctrl-enter": "menu::SecondaryConfirm",
"ctrl-escape": "menu::Cancel",
"ctrl-c": "menu::Cancel",
"escape": "menu::Cancel",
"alt-shift-enter": "menu::Restart",
@@ -64,8 +63,8 @@
"ctrl-k": "editor::CutToEndOfLine",
"ctrl-k ctrl-q": "editor::Rewrap",
"ctrl-k q": "editor::Rewrap",
"ctrl-backspace": "editor::DeleteToPreviousWordStart",
"ctrl-delete": "editor::DeleteToNextWordEnd",
"ctrl-backspace": ["editor::DeleteToPreviousWordStart", { "ignore_newlines": false, "ignore_brackets": false }],
"ctrl-delete": ["editor::DeleteToNextWordEnd", { "ignore_newlines": false, "ignore_brackets": false }],
"cut": "editor::Cut",
"shift-delete": "editor::Cut",
"ctrl-x": "editor::Cut",
@@ -131,8 +130,8 @@
"bindings": {
"shift-enter": "editor::Newline",
"enter": "editor::Newline",
"ctrl-enter": "editor::NewlineAbove",
"ctrl-shift-enter": "editor::NewlineBelow",
"ctrl-enter": "editor::NewlineBelow",
"ctrl-shift-enter": "editor::NewlineAbove",
"ctrl-k ctrl-z": "editor::ToggleSoftWrap",
"ctrl-k z": "editor::ToggleSoftWrap",
"find": "buffer_search::Deploy",
@@ -171,6 +170,7 @@
"context": "Markdown",
"bindings": {
"copy": "markdown::Copy",
"ctrl-insert": "markdown::Copy",
"ctrl-c": "markdown::Copy"
}
},
@@ -259,6 +259,7 @@
"context": "AgentPanel > Markdown",
"bindings": {
"copy": "markdown::CopyAsMarkdown",
"ctrl-insert": "markdown::CopyAsMarkdown",
"ctrl-c": "markdown::CopyAsMarkdown"
}
},

View File

@@ -70,9 +70,9 @@
"cmd-k q": "editor::Rewrap",
"cmd-backspace": "editor::DeleteToBeginningOfLine",
"cmd-delete": "editor::DeleteToEndOfLine",
"alt-backspace": "editor::DeleteToPreviousWordStart",
"ctrl-w": "editor::DeleteToPreviousWordStart",
"alt-delete": "editor::DeleteToNextWordEnd",
"alt-backspace": ["editor::DeleteToPreviousWordStart", { "ignore_newlines": false, "ignore_brackets": false }],
"ctrl-w": ["editor::DeleteToPreviousWordStart", { "ignore_newlines": false, "ignore_brackets": false }],
"alt-delete": ["editor::DeleteToNextWordEnd", { "ignore_newlines": false, "ignore_brackets": false }],
"cmd-x": "editor::Cut",
"cmd-c": "editor::Copy",
"cmd-v": "editor::Paste",

View File

@@ -66,8 +66,8 @@
"ctrl-k": "editor::CutToEndOfLine",
"ctrl-k ctrl-q": "editor::Rewrap",
"ctrl-k q": "editor::Rewrap",
"ctrl-backspace": "editor::DeleteToPreviousWordStart",
"ctrl-delete": "editor::DeleteToNextWordEnd",
"ctrl-backspace": ["editor::DeleteToPreviousWordStart", { "ignore_newlines": false, "ignore_brackets": false }],
"ctrl-delete": ["editor::DeleteToNextWordEnd", { "ignore_newlines": false, "ignore_brackets": false }],
"cut": "editor::Cut",
"shift-delete": "editor::Cut",
"ctrl-x": "editor::Cut",
@@ -134,8 +134,8 @@
"bindings": {
"shift-enter": "editor::Newline",
"enter": "editor::Newline",
"ctrl-enter": "editor::NewlineAbove",
"ctrl-shift-enter": "editor::NewlineBelow",
"ctrl-enter": "editor::NewlineBelow",
"ctrl-shift-enter": "editor::NewlineAbove",
"ctrl-k ctrl-z": "editor::ToggleSoftWrap",
"ctrl-k z": "editor::ToggleSoftWrap",
"find": "buffer_search::Deploy",

View File

@@ -42,7 +42,7 @@
"alt-,": "pane::GoBack", // xref-pop-marker-stack
"ctrl-x h": "editor::SelectAll", // mark-whole-buffer
"ctrl-d": "editor::Delete", // delete-char
"alt-d": "editor::DeleteToNextWordEnd", // kill-word
"alt-d": ["editor::DeleteToNextWordEnd", { "ignore_newlines": false, "ignore_brackets": false }], // kill-word
"ctrl-k": "editor::KillRingCut", // kill-line
"ctrl-w": "editor::Cut", // kill-region
"alt-w": "editor::Copy", // kill-ring-save

View File

@@ -50,8 +50,8 @@
"ctrl-k ctrl-u": "editor::ConvertToUpperCase",
"ctrl-k ctrl-l": "editor::ConvertToLowerCase",
"shift-alt-m": "markdown::OpenPreviewToTheSide",
"ctrl-backspace": "editor::DeleteToPreviousWordStart",
"ctrl-delete": "editor::DeleteToNextWordEnd",
"ctrl-backspace": ["editor::DeleteToPreviousWordStart", { "ignore_newlines": false, "ignore_brackets": false }],
"ctrl-delete": ["editor::DeleteToNextWordEnd", { "ignore_newlines": false, "ignore_brackets": false }],
"alt-right": "editor::MoveToNextSubwordEnd",
"alt-left": "editor::MoveToPreviousSubwordStart",
"alt-shift-right": "editor::SelectToNextSubwordEnd",

View File

@@ -42,7 +42,7 @@
"alt-,": "pane::GoBack", // xref-pop-marker-stack
"ctrl-x h": "editor::SelectAll", // mark-whole-buffer
"ctrl-d": "editor::Delete", // delete-char
"alt-d": "editor::DeleteToNextWordEnd", // kill-word
"alt-d": ["editor::DeleteToNextWordEnd", { "ignore_newlines": false, "ignore_brackets": false }], // kill-word
"ctrl-k": "editor::KillRingCut", // kill-line
"ctrl-w": "editor::Cut", // kill-region
"alt-w": "editor::Copy", // kill-ring-save

View File

@@ -52,8 +52,8 @@
"cmd-k cmd-l": "editor::ConvertToLowerCase",
"cmd-shift-j": "editor::JoinLines",
"shift-alt-m": "markdown::OpenPreviewToTheSide",
"ctrl-backspace": "editor::DeleteToPreviousWordStart",
"ctrl-delete": "editor::DeleteToNextWordEnd",
"ctrl-backspace": ["editor::DeleteToPreviousWordStart", { "ignore_newlines": false, "ignore_brackets": false }],
"ctrl-delete": ["editor::DeleteToNextWordEnd", { "ignore_newlines": false, "ignore_brackets": false }],
"ctrl-right": "editor::MoveToNextSubwordEnd",
"ctrl-left": "editor::MoveToPreviousSubwordStart",
"ctrl-shift-right": "editor::SelectToNextSubwordEnd",

View File

@@ -21,10 +21,10 @@
{
"context": "Editor",
"bindings": {
"alt-backspace": "editor::DeleteToPreviousWordStart",
"alt-shift-backspace": "editor::DeleteToNextWordEnd",
"alt-delete": "editor::DeleteToNextWordEnd",
"alt-shift-delete": "editor::DeleteToNextWordEnd",
"alt-backspace": ["editor::DeleteToPreviousWordStart", { "ignore_newlines": false, "ignore_brackets": false }],
"alt-shift-backspace": ["editor::DeleteToNextWordEnd", { "ignore_newlines": false, "ignore_brackets": false }],
"alt-delete": ["editor::DeleteToNextWordEnd", { "ignore_newlines": false, "ignore_brackets": false }],
"alt-shift-delete": ["editor::DeleteToNextWordEnd", { "ignore_newlines": false, "ignore_brackets": false }],
"ctrl-backspace": "editor::DeleteToPreviousSubwordStart",
"ctrl-delete": "editor::DeleteToNextSubwordEnd",
"alt-left": ["editor::MoveToPreviousWordStart", { "stop_at_soft_wraps": true }],

View File

@@ -337,7 +337,7 @@
"ctrl-x ctrl-z": "editor::Cancel",
"ctrl-x ctrl-e": "vim::LineDown",
"ctrl-x ctrl-y": "vim::LineUp",
"ctrl-w": "editor::DeleteToPreviousWordStart",
"ctrl-w": ["editor::DeleteToPreviousWordStart", { "ignore_newlines": false, "ignore_brackets": false }],
"ctrl-u": "editor::DeleteToBeginningOfLine",
"ctrl-t": "vim::Indent",
"ctrl-d": "vim::Outdent",
@@ -354,6 +354,15 @@
"ctrl-s": "editor::ShowSignatureHelp"
}
},
{
"context": "showing_completions",
"bindings": {
"ctrl-d": "vim::ScrollDown",
"ctrl-u": "vim::ScrollUp",
"ctrl-e": "vim::LineDown",
"ctrl-y": "vim::LineUp"
}
},
{
"context": "(vim_mode == normal || vim_mode == helix_normal) && !menu",
"bindings": {
@@ -435,7 +444,7 @@
"g b": "vim::WindowBottom",
"shift-r": "editor::Paste",
"x": "editor::SelectLine",
"x": "vim::HelixSelectLine",
"shift-x": "editor::SelectLine",
"%": "editor::SelectAll",
// Window mode

View File

@@ -172,7 +172,7 @@ The user has specified the following rules that should be applied:
Rules title: {{title}}
{{/if}}
``````
{{contents}}}
{{contents}}
``````
{{/each}}
{{/if}}

View File

@@ -188,8 +188,8 @@
// 4. A box drawn around the following character
// "hollow"
//
// Default: not set, defaults to "bar"
"cursor_shape": null,
// Default: "bar"
"cursor_shape": "bar",
// Determines when the mouse cursor should be hidden in an editor or input box.
//
// 1. Never hide the mouse cursor:
@@ -223,9 +223,25 @@
"current_line_highlight": "all",
// Whether to highlight all occurrences of the selected text in an editor.
"selection_highlight": true,
// Whether the text selection should have rounded corners.
"rounded_selection": true,
// The debounce delay before querying highlights from the language
// server based on the current cursor location.
"lsp_highlight_debounce": 75,
// The minimum APCA perceptual contrast between foreground and background colors.
// APCA (Accessible Perceptual Contrast Algorithm) is more accurate than WCAG 2.x,
// especially for dark mode. Values range from 0 to 106.
//
// Based on APCA Readability Criterion (ARC) Bronze Simple Mode:
// https://readtech.org/ARC/tests/bronze-simple-mode/
// - 0: No contrast adjustment
// - 45: Minimum for large fluent text (36px+)
// - 60: Minimum for other content text
// - 75: Minimum for body text
// - 90: Preferred for body text
//
// This only affects text drawn over highlight backgrounds in the editor.
"minimum_contrast_for_highlights": 45,
// Whether to pop the completions menu while typing in an editor without
// explicitly requesting it.
"show_completions_on_input": true,
@@ -266,8 +282,8 @@
// - "warning"
// - "info"
// - "hint"
// - null — allow all diagnostics (default)
"diagnostics_max_severity": null,
// - "all" — allow all diagnostics (default)
"diagnostics_max_severity": "all",
// Whether to show wrap guides (vertical rulers) in the editor.
// Setting this to true will show a guide at the 'preferred_line_length' value
// if 'soft_wrap' is set to 'preferred_line_length', and will show any
@@ -279,6 +295,8 @@
"redact_private_values": false,
// The default number of lines to expand excerpts in the multibuffer by.
"expand_excerpt_lines": 5,
// The default number of context lines shown in multibuffer excerpts.
"excerpt_context_lines": 2,
// Globs to match against file paths to determine if a file is private.
"private_files": ["**/.env*", "**/*.pem", "**/*.key", "**/*.cert", "**/*.crt", "**/secrets.yml"],
// Whether to use additional LSP queries to format (and amend) the code after
@@ -363,6 +381,8 @@
// Whether to show code action buttons in the editor toolbar.
"code_actions": false
},
// Whether to allow windows to tab together based on the users tabbing preference (macOS only).
"use_system_window_tabs": false,
// Titlebar related settings
"title_bar": {
// Whether to show the branch icon beside branch switcher in the titlebar.
@@ -1756,7 +1776,7 @@
"api_url": "http://localhost:1234/api/v0"
},
"deepseek": {
"api_url": "https://api.deepseek.com"
"api_url": "https://api.deepseek.com/v1"
},
"mistral": {
"api_url": "https://api.mistral.ai/v1"
@@ -1904,7 +1924,10 @@
"debugger": {
"stepping_granularity": "line",
"save_breakpoints": true,
"timeout": 2000,
"dock": "bottom",
"log_dap_communications": true,
"format_dap_log_messages": true,
"button": true
},
// Configures any number of settings profiles that are temporarily applied on

View File

@@ -785,6 +785,7 @@ pub struct AcpThread {
session_id: acp::SessionId,
token_usage: Option<TokenUsage>,
prompt_capabilities: acp::PromptCapabilities,
available_commands: Vec<acp::AvailableCommand>,
_observe_prompt_capabilities: Task<anyhow::Result<()>>,
determine_shell: Shared<Task<String>>,
terminals: HashMap<acp::TerminalId, Entity<Terminal>>,
@@ -804,7 +805,6 @@ pub enum AcpThreadEvent {
LoadError(LoadError),
PromptCapabilitiesUpdated,
Refusal,
AvailableCommandsUpdated(Vec<acp::AvailableCommand>),
}
impl EventEmitter<AcpThreadEvent> for AcpThread {}
@@ -860,6 +860,7 @@ impl AcpThread {
action_log: Entity<ActionLog>,
session_id: acp::SessionId,
mut prompt_capabilities_rx: watch::Receiver<acp::PromptCapabilities>,
available_commands: Vec<acp::AvailableCommand>,
cx: &mut Context<Self>,
) -> Self {
let prompt_capabilities = *prompt_capabilities_rx.borrow();
@@ -899,6 +900,7 @@ impl AcpThread {
session_id,
token_usage: None,
prompt_capabilities,
available_commands,
_observe_prompt_capabilities: task,
terminals: HashMap::default(),
determine_shell,
@@ -909,6 +911,10 @@ impl AcpThread {
self.prompt_capabilities
}
pub fn available_commands(&self) -> Vec<acp::AvailableCommand> {
self.available_commands.clone()
}
pub fn connection(&self) -> &Rc<dyn AgentConnection> {
&self.connection
}
@@ -1004,9 +1010,6 @@ impl AcpThread {
acp::SessionUpdate::Plan(plan) => {
self.update_plan(plan, cx);
}
acp::SessionUpdate::AvailableCommandsUpdate { available_commands } => {
cx.emit(AcpThreadEvent::AvailableCommandsUpdated(available_commands))
}
}
Ok(())
}
@@ -3077,6 +3080,7 @@ mod tests {
audio: true,
embedded_context: true,
}),
vec![],
cx,
)
});

View File

@@ -231,13 +231,6 @@ impl AgentModelList {
AgentModelList::Grouped(groups) => groups.is_empty(),
}
}
pub fn len(&self) -> usize {
match self {
AgentModelList::Flat(models) => models.len(),
AgentModelList::Grouped(groups) => groups.values().len(),
}
}
}
#[cfg(feature = "test-support")]
@@ -345,6 +338,7 @@ mod test_support {
audio: true,
embedded_context: true,
}),
vec![],
cx,
)
});

View File

@@ -1,6 +1,6 @@
use anyhow::Result;
use buffer_diff::{BufferDiff, BufferDiffSnapshot};
use editor::{MultiBuffer, PathKey};
use editor::{MultiBuffer, PathKey, multibuffer_context_lines};
use gpui::{App, AppContext, AsyncApp, Context, Entity, Subscription, Task};
use itertools::Itertools;
use language::{
@@ -64,7 +64,7 @@ impl Diff {
PathKey::for_buffer(&buffer, cx),
buffer.clone(),
hunk_ranges,
editor::DEFAULT_MULTIBUFFER_CONTEXT,
multibuffer_context_lines(cx),
cx,
);
multibuffer.add_diff(diff, cx);
@@ -279,7 +279,7 @@ impl PendingDiff {
path_key,
buffer,
ranges,
editor::DEFAULT_MULTIBUFFER_CONTEXT,
multibuffer_context_lines(cx),
cx,
);
multibuffer.add_diff(buffer_diff.clone(), cx);
@@ -305,7 +305,7 @@ impl PendingDiff {
PathKey::for_buffer(&self.new_buffer, cx),
self.new_buffer.clone(),
ranges,
editor::DEFAULT_MULTIBUFFER_CONTEXT,
multibuffer_context_lines(cx),
cx,
);
let end = multibuffer.len(cx);

View File

@@ -1,11 +1,10 @@
use auto_update::{AutoUpdateStatus, AutoUpdater, DismissErrorMessage, VersionCheckType};
use editor::Editor;
use extension_host::ExtensionStore;
use extension_host::{ExtensionOperation, ExtensionStore};
use futures::StreamExt;
use gpui::{
Animation, AnimationExt as _, App, Context, CursorStyle, Entity, EventEmitter,
InteractiveElement as _, ParentElement as _, Render, SharedString, StatefulInteractiveElement,
Styled, Transformation, Window, actions, percentage,
App, Context, CursorStyle, Entity, EventEmitter, InteractiveElement as _, ParentElement as _,
Render, SharedString, StatefulInteractiveElement, Styled, Window, actions,
};
use language::{
BinaryStatus, LanguageRegistry, LanguageServerId, LanguageServerName,
@@ -25,7 +24,10 @@ use std::{
sync::Arc,
time::{Duration, Instant},
};
use ui::{ButtonLike, ContextMenu, PopoverMenu, PopoverMenuHandle, Tooltip, prelude::*};
use ui::{
ButtonLike, CommonAnimationExt, ContextMenu, PopoverMenu, PopoverMenuHandle, Tooltip,
prelude::*,
};
use util::truncate_and_trailoff;
use workspace::{StatusItemView, Workspace, item::ItemHandle};
@@ -405,13 +407,7 @@ impl ActivityIndicator {
icon: Some(
Icon::new(IconName::ArrowCircle)
.size(IconSize::Small)
.with_animation(
"arrow-circle",
Animation::new(Duration::from_secs(2)).repeat(),
|icon, delta| {
icon.transform(Transformation::rotate(percentage(delta)))
},
)
.with_rotate_animation(2)
.into_any_element(),
),
message,
@@ -433,11 +429,7 @@ impl ActivityIndicator {
icon: Some(
Icon::new(IconName::ArrowCircle)
.size(IconSize::Small)
.with_animation(
"arrow-circle",
Animation::new(Duration::from_secs(2)).repeat(),
|icon, delta| icon.transform(Transformation::rotate(percentage(delta))),
)
.with_rotate_animation(2)
.into_any_element(),
),
message: format!("Debug: {}", session.read(cx).adapter()),
@@ -460,11 +452,7 @@ impl ActivityIndicator {
icon: Some(
Icon::new(IconName::ArrowCircle)
.size(IconSize::Small)
.with_animation(
"arrow-circle",
Animation::new(Duration::from_secs(2)).repeat(),
|icon, delta| icon.transform(Transformation::rotate(percentage(delta))),
)
.with_rotate_animation(2)
.into_any_element(),
),
message: job_info.message.into(),
@@ -671,8 +659,9 @@ impl ActivityIndicator {
}
// Show any application auto-update info.
if let Some(updater) = &self.auto_updater {
return match &updater.read(cx).status() {
self.auto_updater
.as_ref()
.and_then(|updater| match &updater.read(cx).status() {
AutoUpdateStatus::Checking => Some(Content {
icon: Some(
Icon::new(IconName::Download)
@@ -728,28 +717,49 @@ impl ActivityIndicator {
tooltip_message: None,
}),
AutoUpdateStatus::Idle => None,
};
}
})
.or_else(|| {
if let Some(extension_store) =
ExtensionStore::try_global(cx).map(|extension_store| extension_store.read(cx))
&& let Some((extension_id, operation)) =
extension_store.outstanding_operations().iter().next()
{
let (message, icon, rotate) = match operation {
ExtensionOperation::Install => (
format!("Installing {extension_id} extension…"),
IconName::LoadCircle,
true,
),
ExtensionOperation::Upgrade => (
format!("Updating {extension_id} extension…"),
IconName::Download,
false,
),
ExtensionOperation::Remove => (
format!("Removing {extension_id} extension…"),
IconName::LoadCircle,
true,
),
};
if let Some(extension_store) =
ExtensionStore::try_global(cx).map(|extension_store| extension_store.read(cx))
&& let Some(extension_id) = extension_store.outstanding_operations().keys().next()
{
return Some(Content {
icon: Some(
Icon::new(IconName::Download)
.size(IconSize::Small)
.into_any_element(),
),
message: format!("Updating {extension_id} extension…"),
on_click: Some(Arc::new(|this, window, cx| {
this.dismiss_error_message(&DismissErrorMessage, window, cx)
})),
tooltip_message: None,
});
}
None
Some(Content {
icon: Some(Icon::new(icon).size(IconSize::Small).map(|this| {
if rotate {
this.with_rotate_animation(3).into_any_element()
} else {
this.into_any_element()
}
})),
message,
on_click: Some(Arc::new(|this, window, cx| {
this.dismiss_error_message(&Default::default(), window, cx)
})),
tooltip_message: None,
})
} else {
None
}
})
}
fn version_tooltip_message(version: &VersionCheckType) -> String {

View File

@@ -292,6 +292,7 @@ impl NativeAgent {
action_log.clone(),
session_id.clone(),
prompt_capabilities_rx,
vec![],
cx,
)
});

View File

@@ -2477,6 +2477,7 @@ fn setup_context_server(
path: "somebinary".into(),
args: Vec::new(),
env: None,
timeout: None,
},
},
);

View File

@@ -224,6 +224,7 @@ impl AgentConnection for AcpConnection {
session_id.clone(),
// ACP doesn't currently support per-session prompt capabilities or changing capabilities dynamically.
watch::Receiver::constant(self.agent_capabilities.prompt_capabilities),
response.available_commands,
cx,
)
})?;

View File

@@ -1,3 +1,4 @@
use language_models::provider::anthropic::AnthropicLanguageModelProvider;
use settings::SettingsStore;
use std::path::Path;
use std::rc::Rc;
@@ -39,7 +40,7 @@ impl ClaudeCode {
Self::PACKAGE_NAME.into(),
"node_modules/@anthropic-ai/claude-code/cli.js".into(),
true,
Some("0.2.5".parse().unwrap()),
None,
cx,
)
})?
@@ -80,15 +81,8 @@ impl AgentServer for ClaudeCode {
let settings = cx.read_global(|settings: &SettingsStore, _| {
settings.get::<AllAgentServersSettings>(None).claude.clone()
});
let project = delegate.project().clone();
cx.spawn(async move |cx| {
let mut project_env = project
.update(cx, |project, cx| {
project.directory_environment(root_dir.as_path().into(), cx)
})?
.await
.unwrap_or_default();
let mut command = if let Some(settings) = settings {
settings.command
} else {
@@ -104,13 +98,17 @@ impl AgentServer for ClaudeCode {
})?
.await?
};
project_env.extend(command.env.take().unwrap_or_default());
command.env = Some(project_env);
command
.env
.get_or_insert_default()
.insert("ANTHROPIC_API_KEY".to_owned(), "".to_owned());
if let Some(api_key) = cx
.update(AnthropicLanguageModelProvider::api_key)?
.await
.ok()
{
command
.env
.get_or_insert_default()
.insert("ANTHROPIC_API_KEY".to_owned(), api_key.key);
}
let root_dir_exists = fs.is_dir(&root_dir).await;
anyhow::ensure!(

View File

@@ -41,19 +41,12 @@ impl AgentServer for Gemini {
let settings = cx.read_global(|settings: &SettingsStore, _| {
settings.get::<AllAgentServersSettings>(None).gemini.clone()
});
let project = delegate.project().clone();
cx.spawn(async move |cx| {
let ignore_system_version = settings
.as_ref()
.and_then(|settings| settings.ignore_system_version)
.unwrap_or(true);
let mut project_env = project
.update(cx, |project, cx| {
project.directory_environment(root_dir.as_path().into(), cx)
})?
.await
.unwrap_or_default();
let mut command = if let Some(settings) = settings
&& let Some(command) = settings.custom_command()
{
@@ -74,12 +67,13 @@ impl AgentServer for Gemini {
if !command.args.contains(&ACP_ARG.into()) {
command.args.push(ACP_ARG.into());
}
if let Some(api_key) = cx.update(GoogleLanguageModelProvider::api_key)?.await.ok() {
project_env
command
.env
.get_or_insert_default()
.insert("GEMINI_API_KEY".to_owned(), api_key.key);
}
project_env.extend(command.env.take().unwrap_or_default());
command.env = Some(project_env);
let root_dir_exists = fs.is_dir(&root_dir).await;
anyhow::ensure!(

View File

@@ -6,13 +6,13 @@ use collections::HashMap;
use gpui::{App, SharedString};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use settings::{Settings, SettingsSources};
use settings::{Settings, SettingsSources, SettingsUi};
pub fn init(cx: &mut App) {
AllAgentServersSettings::register(cx);
}
#[derive(Default, Deserialize, Serialize, Clone, JsonSchema, Debug)]
#[derive(Default, Deserialize, Serialize, Clone, JsonSchema, Debug, SettingsUi)]
pub struct AllAgentServersSettings {
pub gemini: Option<BuiltinAgentServerSettings>,
pub claude: Option<CustomAgentServerSettings>,

View File

@@ -8,7 +8,7 @@ use gpui::{App, Pixels, SharedString};
use language_model::LanguageModel;
use schemars::{JsonSchema, json_schema};
use serde::{Deserialize, Serialize};
use settings::{Settings, SettingsSources};
use settings::{Settings, SettingsSources, SettingsUi};
use std::borrow::Cow;
pub use crate::agent_profile::*;
@@ -223,7 +223,7 @@ impl AgentSettingsContent {
}
}
#[derive(Clone, Serialize, Deserialize, JsonSchema, Debug, Default)]
#[derive(Clone, Serialize, Deserialize, JsonSchema, Debug, Default, SettingsUi)]
pub struct AgentSettingsContent {
/// Whether the Agent is enabled.
///

View File

@@ -207,7 +207,7 @@ impl EntryViewState {
self.entries.drain(range);
}
pub fn agent_font_size_changed(&mut self, cx: &mut App) {
pub fn settings_changed(&mut self, cx: &mut App) {
for entry in self.entries.iter() {
match entry {
Entry::UserMessage { .. } | Entry::AssistantMessage { .. } => {}

View File

@@ -2408,7 +2408,7 @@ mod tests {
lsp::SymbolInformation {
name: "MySymbol".into(),
location: lsp::Location {
uri: lsp::Url::from_file_path(path!("/dir/a/one.txt")).unwrap(),
uri: lsp::Uri::from_file_path(path!("/dir/a/one.txt")).unwrap(),
range: lsp::Range::new(
lsp::Position::new(0, 0),
lsp::Position::new(0, 1),

View File

@@ -71,7 +71,7 @@ impl AcpModelPickerDelegate {
let (models, selected_model) = futures::join!(models_task, selected_model_task);
this.update_in(cx, |this, window, cx| {
this.delegate.models = models.log_err();
this.delegate.models = models.ok();
this.delegate.selected_model = selected_model.ok();
this.refresh(window, cx)
})
@@ -141,11 +141,6 @@ impl PickerDelegate for AcpModelPickerDelegate {
cx.spawn_in(window, async move |this, cx| {
let filtered_models = match this
.read_with(cx, |this, cx| {
if let Some(models) = this.delegate.models.as_ref() {
log::debug!("Filtering {} models.", models.len());
} else {
log::debug!("No models available.");
}
this.delegate.models.clone().map(move |models| {
fuzzy_search(models, query, cx.background_executor().clone())
})
@@ -157,8 +152,6 @@ impl PickerDelegate for AcpModelPickerDelegate {
None => AgentModelList::Flat(vec![]),
};
log::debug!("Filtered models. {} available.", filtered_models.len());
this.update_in(cx, |this, window, cx| {
this.delegate.filtered_entries =
info_list_to_picker_entries(filtered_models).collect();

View File

@@ -23,9 +23,9 @@ use gpui::{
Action, Animation, AnimationExt, AnyView, App, BorderStyle, ClickEvent, ClipboardItem,
CursorStyle, EdgesRefinement, ElementId, Empty, Entity, FocusHandle, Focusable, Hsla, Length,
ListOffset, ListState, MouseButton, PlatformDisplay, SharedString, Stateful, StyleRefinement,
Subscription, Task, TextStyle, TextStyleRefinement, Transformation, UnderlineStyle, WeakEntity,
Window, WindowHandle, div, ease_in_out, linear_color_stop, linear_gradient, list, percentage,
point, prelude::*, pulsating_between,
Subscription, Task, TextStyle, TextStyleRefinement, UnderlineStyle, WeakEntity, Window,
WindowHandle, div, ease_in_out, linear_color_stop, linear_gradient, list, point, prelude::*,
pulsating_between,
};
use language::Buffer;
@@ -43,10 +43,10 @@ use std::{collections::BTreeMap, rc::Rc, time::Duration};
use task::SpawnInTerminal;
use terminal_view::terminal_panel::TerminalPanel;
use text::Anchor;
use theme::{AgentFontSize, ThemeSettings};
use theme::ThemeSettings;
use ui::{
Callout, Disclosure, Divider, DividerColor, ElevationIndex, KeyBinding, PopoverMenuHandle,
Scrollbar, ScrollbarState, SpinnerLabel, TintColor, Tooltip, prelude::*,
Callout, CommonAnimationExt, Disclosure, Divider, DividerColor, ElevationIndex, KeyBinding,
PopoverMenuHandle, Scrollbar, ScrollbarState, SpinnerLabel, TintColor, Tooltip, prelude::*,
};
use util::{ResultExt, size::format_file_size, time::duration_alt_display};
use workspace::{CollaboratorId, Workspace};
@@ -290,7 +290,7 @@ pub struct AcpThreadView {
is_loading_contents: bool,
new_server_version_available: Option<SharedString>,
_cancel_task: Option<Task<()>>,
_subscriptions: [Subscription; 4],
_subscriptions: [Subscription; 3],
}
enum ThreadState {
@@ -380,8 +380,7 @@ impl AcpThreadView {
});
let subscriptions = [
cx.observe_global_in::<SettingsStore>(window, Self::agent_font_size_changed),
cx.observe_global_in::<AgentFontSize>(window, Self::agent_font_size_changed),
cx.observe_global_in::<SettingsStore>(window, Self::settings_changed),
cx.subscribe_in(&message_editor, window, Self::handle_message_editor_event),
cx.subscribe_in(&entry_view_state, window, Self::handle_entry_view_event),
];
@@ -431,7 +430,6 @@ impl AcpThreadView {
window,
cx,
);
self.available_commands.replace(vec![]);
self.new_server_version_available.take();
cx.notify();
}
@@ -537,6 +535,26 @@ impl AcpThreadView {
Ok(thread) => {
let action_log = thread.read(cx).action_log().clone();
let mut available_commands = thread.read(cx).available_commands();
if connection
.auth_methods()
.iter()
.any(|method| method.id.0.as_ref() == "claude-login")
{
available_commands.push(acp::AvailableCommand {
name: "login".to_owned(),
description: "Authenticate".to_owned(),
input: None,
});
available_commands.push(acp::AvailableCommand {
name: "logout".to_owned(),
description: "Authenticate".to_owned(),
input: None,
});
}
this.available_commands.replace(available_commands);
this.prompt_capabilities
.set(thread.read(cx).prompt_capabilities());
@@ -984,7 +1002,7 @@ impl AcpThreadView {
this,
AuthRequired {
description: None,
provider_id: None,
provider_id: Some(language_model::ANTHROPIC_PROVIDER_ID),
},
agent,
connection,
@@ -1325,30 +1343,6 @@ impl AcpThreadView {
.set(thread.read(cx).prompt_capabilities());
}
AcpThreadEvent::TokenUsageUpdated => {}
AcpThreadEvent::AvailableCommandsUpdated(available_commands) => {
let mut available_commands = available_commands.clone();
if thread
.read(cx)
.connection()
.auth_methods()
.iter()
.any(|method| method.id.0.as_ref() == "claude-login")
{
available_commands.push(acp::AvailableCommand {
name: "login".to_owned(),
description: "Authenticate".to_owned(),
input: None,
});
available_commands.push(acp::AvailableCommand {
name: "logout".to_owned(),
description: "Authenticate".to_owned(),
input: None,
});
}
self.available_commands.replace(available_commands);
}
}
cx.notify();
}
@@ -2640,13 +2634,7 @@ impl AcpThreadView {
Icon::new(IconName::ArrowCircle)
.size(IconSize::XSmall)
.color(Color::Info)
.with_animation(
"arrow-circle",
Animation::new(Duration::from_secs(2)).repeat(),
|icon, delta| {
icon.transform(Transformation::rotate(percentage(delta)))
},
),
.with_rotate_animation(2)
)
})
.child(
@@ -3017,8 +3005,6 @@ impl AcpThreadView {
let show_description =
configuration_view.is_none() && description.is_none() && pending_auth_method.is_none();
let auth_methods = connection.auth_methods();
v_flex().flex_1().size_full().justify_end().child(
v_flex()
.p_2()
@@ -3049,23 +3035,21 @@ impl AcpThreadView {
.cloned()
.map(|view| div().w_full().child(view)),
)
.when(show_description, |el| {
el.child(
Label::new(format!(
"You are not currently authenticated with {}.{}",
self.agent.name(),
if auth_methods.len() > 1 {
" Please choose one of the following options:"
} else {
""
}
))
.size(LabelSize::Small)
.color(Color::Muted)
.mb_1()
.ml_5(),
)
})
.when(
show_description,
|el| {
el.child(
Label::new(format!(
"You are not currently authenticated with {}. Please choose one of the following options:",
self.agent.name()
))
.size(LabelSize::Small)
.color(Color::Muted)
.mb_1()
.ml_5(),
)
},
)
.when_some(pending_auth_method, |el, _| {
el.child(
h_flex()
@@ -3077,21 +3061,12 @@ impl AcpThreadView {
Icon::new(IconName::ArrowCircle)
.size(IconSize::Small)
.color(Color::Muted)
.with_animation(
"arrow-circle",
Animation::new(Duration::from_secs(2)).repeat(),
|icon, delta| {
icon.transform(Transformation::rotate(percentage(
delta,
)))
},
)
.into_any_element(),
.with_rotate_animation(2)
)
.child(Label::new("Authenticating…").size(LabelSize::Small)),
)
})
.when(!auth_methods.is_empty(), |this| {
.when(!connection.auth_methods().is_empty(), |this| {
this.child(
h_flex()
.justify_end()
@@ -3103,32 +3078,38 @@ impl AcpThreadView {
.pt_2()
.border_color(cx.theme().colors().border.opacity(0.8))
})
.children(connection.auth_methods().iter().enumerate().rev().map(
|(ix, method)| {
Button::new(
SharedString::from(method.id.0.clone()),
method.name.clone(),
)
.when(ix == 0, |el| {
el.style(ButtonStyle::Tinted(ui::TintColor::Warning))
})
.label_size(LabelSize::Small)
.on_click({
let method_id = method.id.clone();
cx.listener(move |this, _, window, cx| {
telemetry::event!(
"Authenticate Agent Started",
agent = this.agent.telemetry_id(),
method = method_id
);
this.authenticate(method_id.clone(), window, cx)
.children(
connection
.auth_methods()
.iter()
.enumerate()
.rev()
.map(|(ix, method)| {
Button::new(
SharedString::from(method.id.0.clone()),
method.name.clone(),
)
.when(ix == 0, |el| {
el.style(ButtonStyle::Tinted(ui::TintColor::Warning))
})
})
},
)),
.label_size(LabelSize::Small)
.on_click({
let method_id = method.id.clone();
cx.listener(move |this, _, window, cx| {
telemetry::event!(
"Authenticate Agent Started",
agent = this.agent.telemetry_id(),
method = method_id
);
this.authenticate(method_id.clone(), window, cx)
})
})
}),
),
)
}),
})
)
}
@@ -3393,13 +3374,7 @@ impl AcpThreadView {
acp::PlanEntryStatus::InProgress => Icon::new(IconName::TodoProgress)
.size(IconSize::Small)
.color(Color::Accent)
.with_animation(
"running",
Animation::new(Duration::from_secs(2)).repeat(),
|icon, delta| {
icon.transform(Transformation::rotate(percentage(delta)))
},
)
.with_rotate_animation(2)
.into_any_element(),
acp::PlanEntryStatus::Completed => Icon::new(IconName::TodoComplete)
.size(IconSize::Small)
@@ -4757,9 +4732,9 @@ impl AcpThreadView {
)
}
fn agent_font_size_changed(&mut self, _window: &mut Window, cx: &mut Context<Self>) {
fn settings_changed(&mut self, _window: &mut Window, cx: &mut Context<Self>) {
self.entry_view_state.update(cx, |entry_view_state, cx| {
entry_view_state.agent_font_size_changed(cx);
entry_view_state.settings_changed(cx);
});
}
@@ -5193,11 +5168,7 @@ fn loading_contents_spinner(size: IconSize) -> AnyElement {
Icon::new(IconName::LoadCircle)
.size(size)
.color(Color::Accent)
.with_animation(
"load_context_circle",
Animation::new(Duration::from_secs(3)).repeat(),
|icon, delta| icon.transform(Transformation::rotate(percentage(delta))),
)
.with_rotate_animation(3)
.into_any_element()
}
@@ -5774,6 +5745,7 @@ pub(crate) mod tests {
audio: true,
embedded_context: true,
}),
vec![],
cx,
)
})))
@@ -5833,6 +5805,7 @@ pub(crate) mod tests {
audio: true,
embedded_context: true,
}),
Vec::new(),
cx,
)
})))

View File

@@ -23,9 +23,8 @@ use gpui::{
AbsoluteLength, Animation, AnimationExt, AnyElement, App, ClickEvent, ClipboardEntry,
ClipboardItem, DefiniteLength, EdgesRefinement, Empty, Entity, EventEmitter, Focusable, Hsla,
ListAlignment, ListOffset, ListState, MouseButton, PlatformDisplay, ScrollHandle, Stateful,
StyleRefinement, Subscription, Task, TextStyle, TextStyleRefinement, Transformation,
UnderlineStyle, WeakEntity, WindowHandle, linear_color_stop, linear_gradient, list, percentage,
pulsating_between,
StyleRefinement, Subscription, Task, TextStyle, TextStyleRefinement, UnderlineStyle,
WeakEntity, WindowHandle, linear_color_stop, linear_gradient, list, pulsating_between,
};
use language::{Buffer, Language, LanguageRegistry};
use language_model::{
@@ -46,8 +45,8 @@ use std::time::Duration;
use text::ToPoint;
use theme::ThemeSettings;
use ui::{
Banner, Disclosure, KeyBinding, PopoverMenuHandle, Scrollbar, ScrollbarState, TextSize,
Tooltip, prelude::*,
Banner, CommonAnimationExt, Disclosure, KeyBinding, PopoverMenuHandle, Scrollbar,
ScrollbarState, TextSize, Tooltip, prelude::*,
};
use util::ResultExt as _;
use util::markdown::MarkdownCodeBlock;
@@ -2661,15 +2660,7 @@ impl ActiveThread {
Icon::new(IconName::ArrowCircle)
.color(Color::Accent)
.size(IconSize::Small)
.with_animation(
"arrow-circle",
Animation::new(Duration::from_secs(2)).repeat(),
|icon, delta| {
icon.transform(Transformation::rotate(
percentage(delta),
))
},
)
.with_rotate_animation(2)
}),
),
)
@@ -2845,17 +2836,11 @@ impl ActiveThread {
}
ToolUseStatus::Pending
| ToolUseStatus::InputStillStreaming
| ToolUseStatus::Running => {
let icon = Icon::new(IconName::ArrowCircle)
.color(Color::Accent)
.size(IconSize::Small);
icon.with_animation(
"arrow-circle",
Animation::new(Duration::from_secs(2)).repeat(),
|icon, delta| icon.transform(Transformation::rotate(percentage(delta))),
)
.into_any_element()
}
| ToolUseStatus::Running => Icon::new(IconName::ArrowCircle)
.color(Color::Accent)
.size(IconSize::Small)
.with_rotate_animation(2)
.into_any_element(),
ToolUseStatus::Finished(_) => div().w_0().into_any_element(),
ToolUseStatus::Error(_) => {
let icon = Icon::new(IconName::Close)
@@ -2944,15 +2929,7 @@ impl ActiveThread {
Icon::new(IconName::ArrowCircle)
.size(IconSize::Small)
.color(Color::Accent)
.with_animation(
"arrow-circle",
Animation::new(Duration::from_secs(2)).repeat(),
|icon, delta| {
icon.transform(Transformation::rotate(percentage(
delta,
)))
},
),
.with_rotate_animation(2),
)
.child(
Label::new("Running…")

View File

@@ -3,7 +3,7 @@ mod configure_context_server_modal;
mod manage_profiles_modal;
mod tool_picker;
use std::{ops::Range, sync::Arc, time::Duration};
use std::{ops::Range, sync::Arc};
use agent_servers::{AgentServerCommand, AllAgentServersSettings, CustomAgentServerSettings};
use agent_settings::AgentSettings;
@@ -17,9 +17,8 @@ use extension::ExtensionManifest;
use extension_host::ExtensionStore;
use fs::Fs;
use gpui::{
Action, Animation, AnimationExt as _, AnyView, App, AsyncWindowContext, Corner, Entity,
EventEmitter, FocusHandle, Focusable, Hsla, ScrollHandle, Subscription, Task, Transformation,
WeakEntity, percentage,
Action, AnyView, App, AsyncWindowContext, Corner, Entity, EventEmitter, FocusHandle, Focusable,
Hsla, ScrollHandle, Subscription, Task, WeakEntity,
};
use language::LanguageRegistry;
use language_model::{
@@ -32,8 +31,9 @@ use project::{
};
use settings::{Settings, SettingsStore, update_settings_file};
use ui::{
Chip, ContextMenu, Disclosure, Divider, DividerColor, ElevationIndex, Indicator, PopoverMenu,
Scrollbar, ScrollbarState, Switch, SwitchColor, SwitchField, Tooltip, prelude::*,
Chip, CommonAnimationExt, ContextMenu, Disclosure, Divider, DividerColor, ElevationIndex,
Indicator, PopoverMenu, Scrollbar, ScrollbarState, Switch, SwitchColor, SwitchField, Tooltip,
prelude::*,
};
use util::ResultExt as _;
use workspace::{Workspace, create_and_open_local_file};
@@ -670,10 +670,9 @@ impl AgentConfiguration {
Icon::new(IconName::LoadCircle)
.size(IconSize::XSmall)
.color(Color::Accent)
.with_animation(
SharedString::from(format!("{}-starting", context_server_id.0,)),
Animation::new(Duration::from_secs(3)).repeat(),
|icon, delta| icon.transform(Transformation::rotate(percentage(delta))),
.with_keyed_rotate_animation(
SharedString::from(format!("{}-starting", context_server_id.0)),
3,
)
.into_any_element(),
"Server is starting.",

View File

@@ -1,16 +1,14 @@
use std::{
path::PathBuf,
sync::{Arc, Mutex},
time::Duration,
};
use anyhow::{Context as _, Result};
use context_server::{ContextServerCommand, ContextServerId};
use editor::{Editor, EditorElement, EditorStyle};
use gpui::{
Animation, AnimationExt as _, AsyncWindowContext, DismissEvent, Entity, EventEmitter,
FocusHandle, Focusable, Task, TextStyle, TextStyleRefinement, Transformation, UnderlineStyle,
WeakEntity, percentage, prelude::*,
AsyncWindowContext, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, Task,
TextStyle, TextStyleRefinement, UnderlineStyle, WeakEntity, prelude::*,
};
use language::{Language, LanguageRegistry};
use markdown::{Markdown, MarkdownElement, MarkdownStyle};
@@ -24,7 +22,9 @@ use project::{
};
use settings::{Settings as _, update_settings_file};
use theme::ThemeSettings;
use ui::{KeyBinding, Modal, ModalFooter, ModalHeader, Section, Tooltip, prelude::*};
use ui::{
CommonAnimationExt, KeyBinding, Modal, ModalFooter, ModalHeader, Section, Tooltip, prelude::*,
};
use util::ResultExt as _;
use workspace::{ModalView, Workspace};
@@ -638,11 +638,7 @@ impl ConfigureContextServerModal {
Icon::new(IconName::ArrowCircle)
.size(IconSize::XSmall)
.color(Color::Info)
.with_animation(
"arrow-circle",
Animation::new(Duration::from_secs(2)).repeat(),
|icon, delta| icon.transform(Transformation::rotate(percentage(delta))),
)
.with_rotate_animation(2)
.into_any_element(),
)
.child(

View File

@@ -10,12 +10,12 @@ use editor::{
Direction, Editor, EditorEvent, EditorSettings, MultiBuffer, MultiBufferSnapshot,
SelectionEffects, ToPoint,
actions::{GoToHunk, GoToPreviousHunk},
multibuffer_context_lines,
scroll::Autoscroll,
};
use gpui::{
Action, Animation, AnimationExt, AnyElement, AnyView, App, AppContext, Empty, Entity,
EventEmitter, FocusHandle, Focusable, Global, SharedString, Subscription, Task, Transformation,
WeakEntity, Window, percentage, prelude::*,
Action, AnyElement, AnyView, App, AppContext, Empty, Entity, EventEmitter, FocusHandle,
Focusable, Global, SharedString, Subscription, Task, WeakEntity, Window, prelude::*,
};
use language::{Buffer, Capability, DiskState, OffsetRangeExt, Point};
@@ -28,9 +28,8 @@ use std::{
collections::hash_map::Entry,
ops::Range,
sync::Arc,
time::Duration,
};
use ui::{IconButtonShape, KeyBinding, Tooltip, prelude::*, vertical_divider};
use ui::{CommonAnimationExt, IconButtonShape, KeyBinding, Tooltip, prelude::*, vertical_divider};
use util::ResultExt;
use workspace::{
Item, ItemHandle, ItemNavHistory, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView,
@@ -257,7 +256,7 @@ impl AgentDiffPane {
path_key.clone(),
buffer.clone(),
diff_hunk_ranges,
editor::DEFAULT_MULTIBUFFER_CONTEXT,
multibuffer_context_lines(cx),
cx,
);
multibuffer.add_diff(diff_handle, cx);
@@ -1083,11 +1082,7 @@ impl Render for AgentDiffToolbar {
Icon::new(IconName::LoadCircle)
.size(IconSize::Small)
.color(Color::Accent)
.with_animation(
"load_circle",
Animation::new(Duration::from_secs(3)).repeat(),
|icon, delta| icon.transform(Transformation::rotate(percentage(delta))),
),
.with_rotate_animation(3),
)
.into_any();
@@ -1533,7 +1528,6 @@ impl AgentDiff {
| AcpThreadEvent::EntriesRemoved(_)
| AcpThreadEvent::ToolAuthorizationRequired
| AcpThreadEvent::PromptCapabilitiesUpdated
| AcpThreadEvent::AvailableCommandsUpdated(_)
| AcpThreadEvent::Retry(_) => {}
}
}

View File

@@ -144,7 +144,8 @@ impl InlineAssistant {
let Some(terminal_panel) = workspace.read(cx).panel::<TerminalPanel>(cx) else {
return;
};
let enabled = AgentSettings::get_global(cx).enabled;
let enabled = !DisableAiSettings::get_global(cx).disable_ai
&& AgentSettings::get_global(cx).enabled;
terminal_panel.update(cx, |terminal_panel, cx| {
terminal_panel.set_assistant_enabled(enabled, cx)
});

View File

@@ -93,8 +93,8 @@ impl<T: 'static> Render for PromptEditor<T> {
};
let bottom_padding = match &self.mode {
PromptEditorMode::Buffer { .. } => Pixels::from(0.),
PromptEditorMode::Terminal { .. } => Pixels::from(8.0),
PromptEditorMode::Buffer { .. } => rems_from_px(2.0),
PromptEditorMode::Terminal { .. } => rems_from_px(8.0),
};
buttons.extend(self.render_buttons(window, cx));
@@ -762,20 +762,22 @@ impl<T: 'static> PromptEditor<T> {
)
}
fn render_editor(&mut self, window: &mut Window, cx: &mut Context<Self>) -> AnyElement {
let font_size = TextSize::Default.rems(cx);
let line_height = font_size.to_pixels(window.rem_size()) * 1.3;
fn render_editor(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> AnyElement {
let colors = cx.theme().colors();
div()
.key_context("InlineAssistEditor")
.size_full()
.p_2()
.pl_1()
.bg(cx.theme().colors().editor_background)
.bg(colors.editor_background)
.child({
let settings = ThemeSettings::get_global(cx);
let font_size = settings.buffer_font_size(cx);
let line_height = font_size * 1.2;
let text_style = TextStyle {
color: cx.theme().colors().editor_foreground,
color: colors.editor_foreground,
font_family: settings.buffer_font.family.clone(),
font_features: settings.buffer_font.features.clone(),
font_size: font_size.into(),
@@ -786,7 +788,7 @@ impl<T: 'static> PromptEditor<T> {
EditorElement::new(
&self.editor,
EditorStyle {
background: cx.theme().colors().editor_background,
background: colors.editor_background,
local_player: cx.theme().players().local(),
text: text_style,
..Default::default()

View File

@@ -2,10 +2,10 @@ use anyhow::Result;
use gpui::App;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use settings::{Settings, SettingsSources};
use settings::{Settings, SettingsSources, SettingsUi};
/// Settings for slash commands.
#[derive(Deserialize, Serialize, Debug, Default, Clone, JsonSchema)]
#[derive(Deserialize, Serialize, Debug, Default, Clone, JsonSchema, SettingsUi)]
pub struct SlashCommandSettings {
/// Settings for the `/cargo-workspace` slash command.
#[serde(default)]

View File

@@ -25,8 +25,8 @@ use gpui::{
Action, Animation, AnimationExt, AnyElement, AnyView, App, ClipboardEntry, ClipboardItem,
Empty, Entity, EventEmitter, FocusHandle, Focusable, FontWeight, Global, InteractiveElement,
IntoElement, ParentElement, Pixels, Render, RenderImage, SharedString, Size,
StatefulInteractiveElement, Styled, Subscription, Task, Transformation, WeakEntity, actions,
div, img, percentage, point, prelude::*, pulsating_between, size,
StatefulInteractiveElement, Styled, Subscription, Task, WeakEntity, actions, div, img, point,
prelude::*, pulsating_between, size,
};
use language::{
BufferSnapshot, LspAdapterDelegate, ToOffset,
@@ -53,8 +53,8 @@ use std::{
};
use text::SelectionGoal;
use ui::{
ButtonLike, Disclosure, ElevationIndex, KeyBinding, PopoverMenuHandle, TintColor, Tooltip,
prelude::*,
ButtonLike, CommonAnimationExt, Disclosure, ElevationIndex, KeyBinding, PopoverMenuHandle,
TintColor, Tooltip, prelude::*,
};
use util::{ResultExt, maybe};
use workspace::{
@@ -1061,15 +1061,7 @@ impl TextThreadEditor {
Icon::new(IconName::ArrowCircle)
.size(IconSize::XSmall)
.color(Color::Info)
.with_animation(
"arrow-circle",
Animation::new(Duration::from_secs(2)).repeat(),
|icon, delta| {
icon.transform(Transformation::rotate(
percentage(delta),
))
},
)
.with_rotate_animation(2)
.into_any_element(),
);
note = Some(Self::esc_kbd(cx).into_any_element());
@@ -2790,11 +2782,7 @@ fn invoked_slash_command_fold_placeholder(
.child(Label::new(format!("/{}", command.name)))
.map(|parent| match &command.status {
InvokedSlashCommandStatus::Running(_) => {
parent.child(Icon::new(IconName::ArrowCircle).with_animation(
"arrow-circle",
Animation::new(Duration::from_secs(4)).repeat(),
|icon, delta| icon.transform(Transformation::rotate(percentage(delta))),
))
parent.child(Icon::new(IconName::ArrowCircle).with_rotate_animation(4))
}
InvokedSlashCommandStatus::Error(message) => parent.child(
Label::new(format!("error: {message}"))

View File

@@ -62,6 +62,8 @@ impl AgentNotification {
app_id: Some(app_id.to_owned()),
window_min_size: None,
window_decorations: Some(WindowDecorations::Client),
tabbing_identifier: None,
..Default::default()
}
}
}

View File

@@ -1,12 +1,9 @@
use std::{sync::Arc, time::Duration};
use std::sync::Arc;
use client::{Client, UserStore, zed_urls};
use cloud_llm_client::Plan;
use gpui::{
Animation, AnimationExt, AnyElement, App, Entity, IntoElement, RenderOnce, Transformation,
Window, percentage,
};
use ui::{Divider, Vector, VectorName, prelude::*};
use gpui::{AnyElement, App, Entity, IntoElement, RenderOnce, Window};
use ui::{CommonAnimationExt, Divider, Vector, VectorName, prelude::*};
use crate::{SignInStatus, YoungAccountBanner, plan_definitions::PlanDefinitions};
@@ -147,11 +144,7 @@ impl RenderOnce for AiUpsellCard {
rems_from_px(72.),
)
.color(Color::Custom(cx.theme().colors().text_accent.alpha(0.3)))
.with_animation(
"loading_stamp",
Animation::new(Duration::from_secs(10)).repeat(),
|this, delta| this.transform(Transformation::rotate(percentage(delta))),
),
.with_rotate_animation(10),
);
let pro_trial_stamp = div()

View File

@@ -373,7 +373,7 @@ pub async fn complete(
.uri(uri)
.header("Anthropic-Version", "2023-06-01")
.header("Anthropic-Beta", beta_headers)
.header("X-Api-Key", api_key)
.header("X-Api-Key", api_key.trim())
.header("Content-Type", "application/json");
let serialized_request =
@@ -526,7 +526,7 @@ pub async fn stream_completion_with_rate_limit_info(
.uri(uri)
.header("Anthropic-Version", "2023-06-01")
.header("Anthropic-Beta", beta_headers)
.header("X-Api-Key", api_key)
.header("X-Api-Key", api_key.trim())
.header("Content-Type", "application/json");
let serialized_request =
serde_json::to_string(&request).map_err(AnthropicError::SerializeRequest)?;

View File

@@ -492,7 +492,7 @@ mod custom_path_matcher {
pub fn new(globs: &[String]) -> Result<Self, globset::Error> {
let globs = globs
.iter()
.map(|glob| Glob::new(&SanitizedPath::from(glob).to_glob_string()))
.map(|glob| Glob::new(&SanitizedPath::new(glob).to_glob_string()))
.collect::<Result<Vec<_>, _>>()?;
let sources = globs.iter().map(|glob| glob.glob().to_owned()).collect();
let sources_with_trailing_slash = globs

View File

@@ -35,7 +35,7 @@ impl Tool for DeletePathTool {
}
fn needs_confirmation(&self, _: &serde_json::Value, _: &Entity<Project>, _: &App) -> bool {
false
true
}
fn may_perform_edits(&self) -> bool {

View File

@@ -11,11 +11,13 @@ use assistant_tool::{
AnyToolCard, Tool, ToolCard, ToolResult, ToolResultContent, ToolResultOutput, ToolUseStatus,
};
use buffer_diff::{BufferDiff, BufferDiffSnapshot};
use editor::{Editor, EditorMode, MinimapVisibility, MultiBuffer, PathKey};
use editor::{
Editor, EditorMode, MinimapVisibility, MultiBuffer, PathKey, multibuffer_context_lines,
};
use futures::StreamExt;
use gpui::{
Animation, AnimationExt, AnyWindowHandle, App, AppContext, AsyncApp, Entity, Task,
TextStyleRefinement, Transformation, WeakEntity, percentage, pulsating_between, px,
TextStyleRefinement, WeakEntity, pulsating_between, px,
};
use indoc::formatdoc;
use language::{
@@ -42,7 +44,7 @@ use std::{
time::Duration,
};
use theme::ThemeSettings;
use ui::{Disclosure, Tooltip, prelude::*};
use ui::{CommonAnimationExt, Disclosure, Tooltip, prelude::*};
use util::ResultExt;
use workspace::Workspace;
@@ -474,7 +476,7 @@ impl Tool for EditFileTool {
PathKey::for_buffer(&buffer, cx),
buffer,
diff_hunk_ranges,
editor::DEFAULT_MULTIBUFFER_CONTEXT,
multibuffer_context_lines(cx),
cx,
);
multibuffer.add_diff(buffer_diff, cx);
@@ -703,7 +705,7 @@ impl EditFileToolCard {
PathKey::for_buffer(buffer, cx),
buffer.clone(),
ranges,
editor::DEFAULT_MULTIBUFFER_CONTEXT,
multibuffer_context_lines(cx),
cx,
);
let end = multibuffer.len(cx);
@@ -791,7 +793,7 @@ impl EditFileToolCard {
path_key,
buffer,
ranges,
editor::DEFAULT_MULTIBUFFER_CONTEXT,
multibuffer_context_lines(cx),
cx,
);
multibuffer.add_diff(buffer_diff.clone(), cx);
@@ -937,11 +939,7 @@ impl ToolCard for EditFileToolCard {
Icon::new(IconName::ArrowCircle)
.size(IconSize::XSmall)
.color(Color::Info)
.with_animation(
"arrow-circle",
Animation::new(Duration::from_secs(2)).repeat(),
|icon, delta| icon.transform(Transformation::rotate(percentage(delta))),
),
.with_rotate_animation(2),
)
})
.when_some(error_message, |header, error_message| {

View File

@@ -8,8 +8,8 @@ use anyhow::{Context as _, Result, anyhow};
use assistant_tool::{Tool, ToolCard, ToolResult, ToolUseStatus};
use futures::{FutureExt as _, future::Shared};
use gpui::{
Animation, AnimationExt, AnyWindowHandle, App, AppContext, Empty, Entity, EntityId, Task,
TextStyleRefinement, Transformation, WeakEntity, Window, percentage,
AnyWindowHandle, App, AppContext, Empty, Entity, EntityId, Task, TextStyleRefinement,
WeakEntity, Window,
};
use language::LineEnding;
use language_model::{LanguageModel, LanguageModelRequest, LanguageModelToolSchemaFormat};
@@ -28,7 +28,7 @@ use std::{
};
use terminal_view::TerminalView;
use theme::ThemeSettings;
use ui::{Disclosure, Tooltip, prelude::*};
use ui::{CommonAnimationExt, Disclosure, Tooltip, prelude::*};
use util::{
ResultExt, get_system_shell, markdown::MarkdownInlineCode, size::format_file_size,
time::duration_alt_display,
@@ -522,11 +522,7 @@ impl ToolCard for TerminalToolCard {
Icon::new(IconName::ArrowCircle)
.size(IconSize::XSmall)
.color(Color::Info)
.with_animation(
"arrow-circle",
Animation::new(Duration::from_secs(2)).repeat(),
|icon, delta| icon.transform(Transformation::rotate(percentage(delta))),
),
.with_rotate_animation(2),
)
})
.when(tool_failed || command_failed, |header| {

View File

@@ -2,9 +2,9 @@ use anyhow::Result;
use gpui::App;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use settings::{Settings, SettingsSources};
use settings::{Settings, SettingsSources, SettingsUi};
#[derive(Deserialize, Debug)]
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)]
pub struct AudioSettings {
/// Opt into the new audio system.
#[serde(rename = "experimental.rodio_audio", default)]
@@ -12,7 +12,7 @@ pub struct AudioSettings {
}
/// Configuration of audio in Zed.
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)]
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug, SettingsUi)]
#[serde(default)]
pub struct AudioSettingsContent {
/// Whether to use the experimental audio system

View File

@@ -10,7 +10,7 @@ use paths::remote_servers_dir;
use release_channel::{AppCommitSha, ReleaseChannel};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use settings::{Settings, SettingsSources, SettingsStore};
use settings::{Settings, SettingsSources, SettingsStore, SettingsUi};
use smol::{fs, io::AsyncReadExt};
use smol::{fs::File, process::Command};
use std::{
@@ -118,14 +118,14 @@ struct AutoUpdateSetting(bool);
/// Whether or not to automatically check for updates.
///
/// Default: true
#[derive(Clone, Copy, Default, JsonSchema, Deserialize, Serialize)]
#[derive(Clone, Copy, Default, JsonSchema, Deserialize, Serialize, SettingsUi)]
#[serde(transparent)]
struct AutoUpdateSettingContent(bool);
impl Settings for AutoUpdateSetting {
const KEY: Option<&'static str> = Some("auto_update");
type FileContent = Option<AutoUpdateSettingContent>;
type FileContent = AutoUpdateSettingContent;
fn load(sources: SettingsSources<Self::FileContent>, _: &mut App) -> Result<Self> {
let auto_update = [
@@ -135,17 +135,19 @@ impl Settings for AutoUpdateSetting {
sources.user,
]
.into_iter()
.find_map(|value| value.copied().flatten())
.unwrap_or(sources.default.ok_or_else(Self::missing_default)?);
.find_map(|value| value.copied())
.unwrap_or(*sources.default);
Ok(Self(auto_update.0))
}
fn import_from_vscode(vscode: &settings::VsCodeSettings, current: &mut Self::FileContent) {
vscode.enum_setting("update.mode", current, |s| match s {
let mut cur = &mut Some(*current);
vscode.enum_setting("update.mode", &mut cur, |s| match s {
"none" | "manual" => Some(AutoUpdateSettingContent(false)),
_ => Some(AutoUpdateSettingContent(true)),
});
*current = cur.unwrap();
}
}

View File

@@ -16,7 +16,7 @@ use crate::windows_impl::WM_JOB_UPDATED;
type Job = fn(&Path) -> Result<()>;
#[cfg(not(test))]
pub(crate) const JOBS: [Job; 6] = [
pub(crate) const JOBS: &[Job] = &[
// Delete old files
|app_dir| {
let zed_executable = app_dir.join("Zed.exe");
@@ -32,6 +32,12 @@ pub(crate) const JOBS: [Job; 6] = [
std::fs::remove_file(&zed_cli)
.context(format!("Failed to remove old file {}", zed_cli.display()))
},
|app_dir| {
let zed_wsl = app_dir.join("bin\\zed");
log::info!("Removing old file: {}", zed_wsl.display());
std::fs::remove_file(&zed_wsl)
.context(format!("Failed to remove old file {}", zed_wsl.display()))
},
// Copy new files
|app_dir| {
let zed_executable_source = app_dir.join("install\\Zed.exe");
@@ -65,6 +71,22 @@ pub(crate) const JOBS: [Job; 6] = [
zed_cli_dest.display()
))
},
|app_dir| {
let zed_wsl_source = app_dir.join("install\\bin\\zed");
let zed_wsl_dest = app_dir.join("bin\\zed");
log::info!(
"Copying new file {} to {}",
zed_wsl_source.display(),
zed_wsl_dest.display()
);
std::fs::copy(&zed_wsl_source, &zed_wsl_dest)
.map(|_| ())
.context(format!(
"Failed to copy new file {} to {}",
zed_wsl_source.display(),
zed_wsl_dest.display()
))
},
// Clean up installer folder and updates folder
|app_dir| {
let updates_folder = app_dir.join("updates");
@@ -85,7 +107,7 @@ pub(crate) const JOBS: [Job; 6] = [
];
#[cfg(test)]
pub(crate) const JOBS: [Job; 2] = [
pub(crate) const JOBS: &[Job] = &[
|_| {
std::thread::sleep(Duration::from_millis(1000));
if let Ok(config) = std::env::var("ZED_AUTO_UPDATE") {

View File

@@ -2,7 +2,7 @@ use anyhow::Result;
use gpui::App;
use schemars::JsonSchema;
use serde_derive::{Deserialize, Serialize};
use settings::{Settings, SettingsSources};
use settings::{Settings, SettingsSources, SettingsUi};
#[derive(Deserialize, Debug)]
pub struct CallSettings {
@@ -11,7 +11,7 @@ pub struct CallSettings {
}
/// Configuration of voice calls in Zed.
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)]
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug, SettingsUi)]
pub struct CallSettingsContent {
/// Whether the microphone should be muted when joining a channel or a call.
///

View File

@@ -14,6 +14,7 @@ pub enum CliRequest {
paths: Vec<String>,
urls: Vec<String>,
diff_paths: Vec<[String; 2]>,
wsl: Option<String>,
wait: bool,
open_new_workspace: Option<bool>,
env: Option<HashMap<String, String>>,

View File

@@ -6,7 +6,6 @@
use anyhow::{Context as _, Result};
use clap::Parser;
use cli::{CliRequest, CliResponse, IpcHandshake, ipc::IpcOneShotServer};
use collections::HashMap;
use parking_lot::Mutex;
use std::{
env, fs, io,
@@ -85,6 +84,18 @@ struct Args {
/// Run zed in dev-server mode
#[arg(long)]
dev_server_token: Option<String>,
/// The username and WSL distribution to use when opening paths. If not specified,
/// Zed will attempt to open the paths directly.
///
/// The username is optional, and if not specified, the default user for the distribution
/// will be used.
///
/// Example: `me@Ubuntu` or `Ubuntu`.
///
/// WARN: You should not fill in this field by hand.
#[cfg(target_os = "windows")]
#[arg(long, value_name = "USER@DISTRO")]
wsl: Option<String>,
/// Not supported in Zed CLI, only supported on Zed binary
/// Will attempt to give the correct command to run
#[arg(long)]
@@ -129,14 +140,41 @@ fn parse_path_with_position(argument_str: &str) -> anyhow::Result<String> {
Ok(canonicalized.to_string(|path| path.to_string_lossy().to_string()))
}
fn main() -> Result<()> {
#[cfg(all(not(debug_assertions), target_os = "windows"))]
unsafe {
use ::windows::Win32::System::Console::{ATTACH_PARENT_PROCESS, AttachConsole};
fn parse_path_in_wsl(source: &str, wsl: &str) -> Result<String> {
let mut command = util::command::new_std_command("wsl.exe");
let _ = AttachConsole(ATTACH_PARENT_PROCESS);
let (user, distro_name) = if let Some((user, distro)) = wsl.split_once('@') {
if user.is_empty() {
anyhow::bail!("user is empty in wsl argument");
}
(Some(user), distro)
} else {
(None, wsl)
};
if let Some(user) = user {
command.arg("--user").arg(user);
}
let output = command
.arg("--distribution")
.arg(distro_name)
.arg("wslpath")
.arg("-m")
.arg(source)
.output()?;
let result = String::from_utf8_lossy(&output.stdout);
let prefix = format!("//wsl.localhost/{}", distro_name);
Ok(result
.trim()
.strip_prefix(&prefix)
.unwrap_or(&result)
.to_string())
}
fn main() -> Result<()> {
#[cfg(unix)]
util::prevent_root_execution();
@@ -223,6 +261,8 @@ fn main() -> Result<()> {
let env = {
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
{
use collections::HashMap;
// On Linux, the desktop entry uses `cli` to spawn `zed`.
// We need to handle env vars correctly since std::env::vars() may not contain
// project-specific vars (e.g. those set by direnv).
@@ -235,8 +275,19 @@ fn main() -> Result<()> {
}
}
#[cfg(not(any(target_os = "linux", target_os = "freebsd")))]
Some(std::env::vars().collect::<HashMap<_, _>>())
#[cfg(target_os = "windows")]
{
// On Windows, by default, a child process inherits a copy of the environment block of the parent process.
// So we don't need to pass env vars explicitly.
None
}
#[cfg(not(any(target_os = "linux", target_os = "freebsd", target_os = "windows")))]
{
use collections::HashMap;
Some(std::env::vars().collect::<HashMap<_, _>>())
}
};
let exit_status = Arc::new(Mutex::new(None));
@@ -253,6 +304,11 @@ fn main() -> Result<()> {
]);
}
#[cfg(target_os = "windows")]
let wsl = args.wsl.as_ref();
#[cfg(not(target_os = "windows"))]
let wsl = None;
for path in args.paths_with_position.iter() {
if path.starts_with("zed://")
|| path.starts_with("http://")
@@ -271,8 +327,10 @@ fn main() -> Result<()> {
paths.push(tmp_file.path().to_string_lossy().to_string());
let (tmp_file, _) = tmp_file.keep()?;
anonymous_fd_tmp_files.push((file, tmp_file));
} else if let Some(wsl) = wsl {
urls.push(format!("file://{}", parse_path_in_wsl(path, wsl)?));
} else {
paths.push(parse_path_with_position(path)?)
paths.push(parse_path_with_position(path)?);
}
}
@@ -288,10 +346,16 @@ fn main() -> Result<()> {
let (_, handshake) = server.accept().context("Handshake after Zed spawn")?;
let (tx, rx) = (handshake.requests, handshake.responses);
#[cfg(target_os = "windows")]
let wsl = args.wsl;
#[cfg(not(target_os = "windows"))]
let wsl = None;
tx.send(CliRequest::Open {
paths,
urls,
diff_paths,
wsl,
wait: args.wait,
open_new_workspace,
env,
@@ -644,15 +708,15 @@ mod windows {
Storage::FileSystem::{
CreateFileW, FILE_FLAGS_AND_ATTRIBUTES, FILE_SHARE_MODE, OPEN_EXISTING, WriteFile,
},
System::Threading::CreateMutexW,
System::Threading::{CREATE_NEW_PROCESS_GROUP, CreateMutexW},
},
core::HSTRING,
};
use crate::{Detect, InstalledApp};
use std::io;
use std::path::{Path, PathBuf};
use std::process::ExitStatus;
use std::{io, os::windows::process::CommandExt};
fn check_single_instance() -> bool {
let mutex = unsafe {
@@ -691,6 +755,7 @@ mod windows {
fn launch(&self, ipc_url: String) -> anyhow::Result<()> {
if check_single_instance() {
std::process::Command::new(self.0.clone())
.creation_flags(CREATE_NEW_PROCESS_GROUP.0)
.arg(ipc_url)
.spawn()?;
} else {

View File

@@ -31,7 +31,7 @@ use release_channel::{AppVersion, ReleaseChannel};
use rpc::proto::{AnyTypedEnvelope, EnvelopedMessage, PeerId, RequestMessage};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use settings::{Settings, SettingsSources};
use settings::{Settings, SettingsSources, SettingsUi};
use std::{
any::TypeId,
convert::TryFrom,
@@ -96,7 +96,7 @@ actions!(
]
);
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema)]
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, SettingsUi)]
pub struct ClientSettingsContent {
server_url: Option<String>,
}
@@ -122,7 +122,7 @@ impl Settings for ClientSettings {
fn import_from_vscode(_vscode: &settings::VsCodeSettings, _current: &mut Self::FileContent) {}
}
#[derive(Default, Clone, Serialize, Deserialize, JsonSchema)]
#[derive(Default, Clone, Serialize, Deserialize, JsonSchema, SettingsUi)]
pub struct ProxySettingsContent {
proxy: Option<String>,
}
@@ -287,6 +287,7 @@ pub enum Status {
},
ConnectionLost,
Reauthenticating,
Reauthenticated,
Reconnecting,
ReconnectionError {
next_reconnection: Instant,
@@ -298,6 +299,21 @@ impl Status {
matches!(self, Self::Connected { .. })
}
pub fn was_connected(&self) -> bool {
matches!(
self,
Self::ConnectionLost
| Self::Reauthenticating
| Self::Reauthenticated
| Self::Reconnecting
)
}
/// Returns whether the client is currently connected or was connected at some point.
pub fn is_or_was_connected(&self) -> bool {
self.is_connected() || self.was_connected()
}
pub fn is_signing_in(&self) -> bool {
matches!(
self,
@@ -511,7 +527,7 @@ pub struct TelemetrySettings {
}
/// Control what info is collected by Zed.
#[derive(Default, Clone, Serialize, Deserialize, JsonSchema, Debug)]
#[derive(Default, Clone, Serialize, Deserialize, JsonSchema, Debug, SettingsUi)]
pub struct TelemetrySettingsContent {
/// Send debug info like crash reports.
///
@@ -857,11 +873,13 @@ impl Client {
try_provider: bool,
cx: &AsyncApp,
) -> Result<Credentials> {
if self.status().borrow().is_signed_out() {
let is_reauthenticating = if self.status().borrow().is_signed_out() {
self.set_status(Status::Authenticating, cx);
false
} else {
self.set_status(Status::Reauthenticating, cx);
}
true
};
let mut credentials = None;
@@ -919,7 +937,14 @@ impl Client {
self.cloud_client
.set_credentials(credentials.user_id as u32, credentials.access_token.clone());
self.state.write().credentials = Some(credentials.clone());
self.set_status(Status::Authenticated, cx);
self.set_status(
if is_reauthenticating {
Status::Reauthenticated
} else {
Status::Authenticated
},
cx,
);
Ok(credentials)
}
@@ -1034,6 +1059,7 @@ impl Client {
| Status::Authenticating
| Status::AuthenticationError
| Status::Reauthenticating
| Status::Reauthenticated
| Status::ReconnectionError { .. } => false,
Status::Connected { .. } | Status::Connecting | Status::Reconnecting => {
return ConnectionResult::Result(Ok(()));
@@ -1670,21 +1696,10 @@ impl Client {
);
cx.spawn(async move |_| match future.await {
Ok(()) => {
log::debug!(
"rpc message handled. client_id:{}, sender_id:{:?}, type:{}",
client_id,
original_sender_id,
type_name
);
log::debug!("rpc message handled. client_id:{client_id}, sender_id:{original_sender_id:?}, type:{type_name}");
}
Err(error) => {
log::error!(
"error handling message. client_id:{}, sender_id:{:?}, type:{}, error:{:?}",
client_id,
original_sender_id,
type_name,
error
);
log::error!("error handling message. client_id:{client_id}, sender_id:{original_sender_id:?}, type:{type_name}, error:{error:#}");
}
})
.detach();

View File

@@ -216,7 +216,9 @@ impl UserStore {
return Ok(());
};
match status {
Status::Authenticated | Status::Connected { .. } => {
Status::Authenticated
| Status::Reauthenticated
| Status::Connected { .. } => {
if let Some(user_id) = client.user_id() {
let response = client
.cloud_client()

View File

@@ -369,7 +369,7 @@ async fn test_collaborating_with_completion(cx_a: &mut TestAppContext, cx_b: &mu
.set_request_handler::<lsp::request::Completion, _, _>(|params, _| async move {
assert_eq!(
params.text_document_position.text_document.uri,
lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(),
lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(),
);
assert_eq!(
params.text_document_position.position,
@@ -488,7 +488,7 @@ async fn test_collaborating_with_completion(cx_a: &mut TestAppContext, cx_b: &mu
.set_request_handler::<lsp::request::Completion, _, _>(|params, _| async move {
assert_eq!(
params.text_document_position.text_document.uri,
lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(),
lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(),
);
assert_eq!(
params.text_document_position.position,
@@ -615,7 +615,7 @@ async fn test_collaborating_with_code_actions(
.set_request_handler::<lsp::request::CodeActionRequest, _, _>(|params, _| async move {
assert_eq!(
params.text_document.uri,
lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(),
lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(),
);
assert_eq!(params.range.start, lsp::Position::new(0, 0));
assert_eq!(params.range.end, lsp::Position::new(0, 0));
@@ -637,7 +637,7 @@ async fn test_collaborating_with_code_actions(
.set_request_handler::<lsp::request::CodeActionRequest, _, _>(|params, _| async move {
assert_eq!(
params.text_document.uri,
lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(),
lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(),
);
assert_eq!(params.range.start, lsp::Position::new(1, 31));
assert_eq!(params.range.end, lsp::Position::new(1, 31));
@@ -649,7 +649,7 @@ async fn test_collaborating_with_code_actions(
changes: Some(
[
(
lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(),
lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(),
vec![lsp::TextEdit::new(
lsp::Range::new(
lsp::Position::new(1, 22),
@@ -659,7 +659,7 @@ async fn test_collaborating_with_code_actions(
)],
),
(
lsp::Url::from_file_path(path!("/a/other.rs")).unwrap(),
lsp::Uri::from_file_path(path!("/a/other.rs")).unwrap(),
vec![lsp::TextEdit::new(
lsp::Range::new(
lsp::Position::new(0, 0),
@@ -721,7 +721,7 @@ async fn test_collaborating_with_code_actions(
changes: Some(
[
(
lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(),
lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(),
vec![lsp::TextEdit::new(
lsp::Range::new(
lsp::Position::new(1, 22),
@@ -731,7 +731,7 @@ async fn test_collaborating_with_code_actions(
)],
),
(
lsp::Url::from_file_path(path!("/a/other.rs")).unwrap(),
lsp::Uri::from_file_path(path!("/a/other.rs")).unwrap(),
vec![lsp::TextEdit::new(
lsp::Range::new(
lsp::Position::new(0, 0),
@@ -949,14 +949,14 @@ async fn test_collaborating_with_renames(cx_a: &mut TestAppContext, cx_b: &mut T
changes: Some(
[
(
lsp::Url::from_file_path(path!("/dir/one.rs")).unwrap(),
lsp::Uri::from_file_path(path!("/dir/one.rs")).unwrap(),
vec![lsp::TextEdit::new(
lsp::Range::new(lsp::Position::new(0, 6), lsp::Position::new(0, 9)),
"THREE".to_string(),
)],
),
(
lsp::Url::from_file_path(path!("/dir/two.rs")).unwrap(),
lsp::Uri::from_file_path(path!("/dir/two.rs")).unwrap(),
vec![
lsp::TextEdit::new(
lsp::Range::new(
@@ -1574,7 +1574,7 @@ async fn test_on_input_format_from_host_to_guest(
|params, _| async move {
assert_eq!(
params.text_document_position.text_document.uri,
lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(),
lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(),
);
assert_eq!(
params.text_document_position.position,
@@ -1717,7 +1717,7 @@ async fn test_on_input_format_from_guest_to_host(
.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(|params, _| async move {
assert_eq!(
params.text_document_position.text_document.uri,
lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(),
lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(),
);
assert_eq!(
params.text_document_position.position,
@@ -1901,7 +1901,7 @@ async fn test_mutual_editor_inlay_hint_cache_update(
async move {
assert_eq!(
params.text_document.uri,
lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(),
lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(),
);
let edits_made = task_edits_made.load(atomic::Ordering::Acquire);
Ok(Some(vec![lsp::InlayHint {
@@ -2151,7 +2151,7 @@ async fn test_inlay_hint_refresh_is_forwarded(
async move {
assert_eq!(
params.text_document.uri,
lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(),
lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(),
);
let other_hints = task_other_hints.load(atomic::Ordering::Acquire);
let character = if other_hints { 0 } else { 2 };
@@ -2332,7 +2332,7 @@ async fn test_lsp_document_color(cx_a: &mut TestAppContext, cx_b: &mut TestAppCo
async move {
assert_eq!(
params.text_document.uri,
lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(),
lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(),
);
requests_made.fetch_add(1, atomic::Ordering::Release);
Ok(vec![lsp::ColorInformation {
@@ -2621,11 +2621,11 @@ async fn test_lsp_pull_diagnostics(
let requests_made = closure_diagnostics_pulls_made.clone();
let diagnostics_pulls_result_ids = closure_diagnostics_pulls_result_ids.clone();
async move {
let message = if lsp::Url::from_file_path(path!("/a/main.rs")).unwrap()
let message = if lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap()
== params.text_document.uri
{
expected_pull_diagnostic_main_message.to_string()
} else if lsp::Url::from_file_path(path!("/a/lib.rs")).unwrap()
} else if lsp::Uri::from_file_path(path!("/a/lib.rs")).unwrap()
== params.text_document.uri
{
expected_pull_diagnostic_lib_message.to_string()
@@ -2717,7 +2717,7 @@ async fn test_lsp_pull_diagnostics(
items: vec![
lsp::WorkspaceDocumentDiagnosticReport::Full(
lsp::WorkspaceFullDocumentDiagnosticReport {
uri: lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(),
uri: lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(),
version: None,
full_document_diagnostic_report:
lsp::FullDocumentDiagnosticReport {
@@ -2746,7 +2746,7 @@ async fn test_lsp_pull_diagnostics(
),
lsp::WorkspaceDocumentDiagnosticReport::Full(
lsp::WorkspaceFullDocumentDiagnosticReport {
uri: lsp::Url::from_file_path(path!("/a/lib.rs")).unwrap(),
uri: lsp::Uri::from_file_path(path!("/a/lib.rs")).unwrap(),
version: None,
full_document_diagnostic_report:
lsp::FullDocumentDiagnosticReport {
@@ -2821,7 +2821,7 @@ async fn test_lsp_pull_diagnostics(
fake_language_server.notify::<lsp::notification::PublishDiagnostics>(
&lsp::PublishDiagnosticsParams {
uri: lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(),
uri: lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(),
diagnostics: vec![lsp::Diagnostic {
range: lsp::Range {
start: lsp::Position {
@@ -2842,7 +2842,7 @@ async fn test_lsp_pull_diagnostics(
);
fake_language_server.notify::<lsp::notification::PublishDiagnostics>(
&lsp::PublishDiagnosticsParams {
uri: lsp::Url::from_file_path(path!("/a/lib.rs")).unwrap(),
uri: lsp::Uri::from_file_path(path!("/a/lib.rs")).unwrap(),
diagnostics: vec![lsp::Diagnostic {
range: lsp::Range {
start: lsp::Position {
@@ -2870,7 +2870,7 @@ async fn test_lsp_pull_diagnostics(
items: vec![
lsp::WorkspaceDocumentDiagnosticReport::Full(
lsp::WorkspaceFullDocumentDiagnosticReport {
uri: lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(),
uri: lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(),
version: None,
full_document_diagnostic_report:
lsp::FullDocumentDiagnosticReport {
@@ -2902,7 +2902,7 @@ async fn test_lsp_pull_diagnostics(
),
lsp::WorkspaceDocumentDiagnosticReport::Full(
lsp::WorkspaceFullDocumentDiagnosticReport {
uri: lsp::Url::from_file_path(path!("/a/lib.rs")).unwrap(),
uri: lsp::Uri::from_file_path(path!("/a/lib.rs")).unwrap(),
version: None,
full_document_diagnostic_report:
lsp::FullDocumentDiagnosticReport {
@@ -3051,7 +3051,7 @@ async fn test_lsp_pull_diagnostics(
lsp::WorkspaceDiagnosticReportResult::Report(lsp::WorkspaceDiagnosticReport {
items: vec![lsp::WorkspaceDocumentDiagnosticReport::Full(
lsp::WorkspaceFullDocumentDiagnosticReport {
uri: lsp::Url::from_file_path(path!("/a/lib.rs")).unwrap(),
uri: lsp::Uri::from_file_path(path!("/a/lib.rs")).unwrap(),
version: None,
full_document_diagnostic_report: lsp::FullDocumentDiagnosticReport {
result_id: Some(format!(
@@ -3425,16 +3425,16 @@ async fn test_git_blame_is_forwarded(cx_a: &mut TestAppContext, cx_b: &mut TestA
assert_eq!(
entries,
vec![
Some(blame_entry("1b1b1b", 0..1)),
Some(blame_entry("0d0d0d", 1..2)),
Some(blame_entry("3a3a3a", 2..3)),
Some(blame_entry("4c4c4c", 3..4)),
Some((buffer_id_b, blame_entry("1b1b1b", 0..1))),
Some((buffer_id_b, blame_entry("0d0d0d", 1..2))),
Some((buffer_id_b, blame_entry("3a3a3a", 2..3))),
Some((buffer_id_b, blame_entry("4c4c4c", 3..4))),
]
);
blame.update(cx, |blame, _| {
for (idx, entry) in entries.iter().flatten().enumerate() {
let details = blame.details_for_entry(entry).unwrap();
for (idx, (buffer, entry)) in entries.iter().flatten().enumerate() {
let details = blame.details_for_entry(*buffer, entry).unwrap();
assert_eq!(details.message, format!("message for idx-{}", idx));
assert_eq!(
details.permalink.unwrap().to_string(),
@@ -3474,9 +3474,9 @@ async fn test_git_blame_is_forwarded(cx_a: &mut TestAppContext, cx_b: &mut TestA
entries,
vec![
None,
Some(blame_entry("0d0d0d", 1..2)),
Some(blame_entry("3a3a3a", 2..3)),
Some(blame_entry("4c4c4c", 3..4)),
Some((buffer_id_b, blame_entry("0d0d0d", 1..2))),
Some((buffer_id_b, blame_entry("3a3a3a", 2..3))),
Some((buffer_id_b, blame_entry("4c4c4c", 3..4))),
]
);
});
@@ -3511,8 +3511,8 @@ async fn test_git_blame_is_forwarded(cx_a: &mut TestAppContext, cx_b: &mut TestA
vec![
None,
None,
Some(blame_entry("3a3a3a", 2..3)),
Some(blame_entry("4c4c4c", 3..4)),
Some((buffer_id_b, blame_entry("3a3a3a", 2..3))),
Some((buffer_id_b, blame_entry("4c4c4c", 3..4))),
]
);
});
@@ -4040,7 +4040,7 @@ async fn test_client_can_query_lsp_ext(cx_a: &mut TestAppContext, cx_b: &mut Tes
|params, _| async move {
assert_eq!(
params.text_document.uri,
lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(),
lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(),
);
assert_eq!(params.position, lsp::Position::new(0, 0));
Ok(Some(ExpandedMacro {
@@ -4075,7 +4075,7 @@ async fn test_client_can_query_lsp_ext(cx_a: &mut TestAppContext, cx_b: &mut Tes
|params, _| async move {
assert_eq!(
params.text_document.uri,
lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(),
lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(),
);
assert_eq!(
params.position,

View File

@@ -4075,7 +4075,7 @@ async fn test_collaborating_with_diagnostics(
.await;
fake_language_server.notify::<lsp::notification::PublishDiagnostics>(
&lsp::PublishDiagnosticsParams {
uri: lsp::Url::from_file_path(path!("/a/a.rs")).unwrap(),
uri: lsp::Uri::from_file_path(path!("/a/a.rs")).unwrap(),
version: None,
diagnostics: vec![lsp::Diagnostic {
severity: Some(lsp::DiagnosticSeverity::WARNING),
@@ -4095,7 +4095,7 @@ async fn test_collaborating_with_diagnostics(
.unwrap();
fake_language_server.notify::<lsp::notification::PublishDiagnostics>(
&lsp::PublishDiagnosticsParams {
uri: lsp::Url::from_file_path(path!("/a/a.rs")).unwrap(),
uri: lsp::Uri::from_file_path(path!("/a/a.rs")).unwrap(),
version: None,
diagnostics: vec![lsp::Diagnostic {
severity: Some(lsp::DiagnosticSeverity::ERROR),
@@ -4169,7 +4169,7 @@ async fn test_collaborating_with_diagnostics(
// Simulate a language server reporting more errors for a file.
fake_language_server.notify::<lsp::notification::PublishDiagnostics>(
&lsp::PublishDiagnosticsParams {
uri: lsp::Url::from_file_path(path!("/a/a.rs")).unwrap(),
uri: lsp::Uri::from_file_path(path!("/a/a.rs")).unwrap(),
version: None,
diagnostics: vec![
lsp::Diagnostic {
@@ -4265,7 +4265,7 @@ async fn test_collaborating_with_diagnostics(
// Simulate a language server reporting no errors for a file.
fake_language_server.notify::<lsp::notification::PublishDiagnostics>(
&lsp::PublishDiagnosticsParams {
uri: lsp::Url::from_file_path(path!("/a/a.rs")).unwrap(),
uri: lsp::Uri::from_file_path(path!("/a/a.rs")).unwrap(),
version: None,
diagnostics: Vec::new(),
},
@@ -4372,7 +4372,7 @@ async fn test_collaborating_with_lsp_progress_updates_and_diagnostics_ordering(
for file_name in file_names {
fake_language_server.notify::<lsp::notification::PublishDiagnostics>(
&lsp::PublishDiagnosticsParams {
uri: lsp::Url::from_file_path(Path::new(path!("/test")).join(file_name)).unwrap(),
uri: lsp::Uri::from_file_path(Path::new(path!("/test")).join(file_name)).unwrap(),
version: None,
diagnostics: vec![lsp::Diagnostic {
severity: Some(lsp::DiagnosticSeverity::WARNING),
@@ -4838,7 +4838,7 @@ async fn test_definition(
|_, _| async move {
Ok(Some(lsp::GotoDefinitionResponse::Scalar(
lsp::Location::new(
lsp::Url::from_file_path(path!("/root/dir-2/b.rs")).unwrap(),
lsp::Uri::from_file_path(path!("/root/dir-2/b.rs")).unwrap(),
lsp::Range::new(lsp::Position::new(0, 6), lsp::Position::new(0, 9)),
),
)))
@@ -4876,7 +4876,7 @@ async fn test_definition(
|_, _| async move {
Ok(Some(lsp::GotoDefinitionResponse::Scalar(
lsp::Location::new(
lsp::Url::from_file_path(path!("/root/dir-2/b.rs")).unwrap(),
lsp::Uri::from_file_path(path!("/root/dir-2/b.rs")).unwrap(),
lsp::Range::new(lsp::Position::new(1, 6), lsp::Position::new(1, 11)),
),
)))
@@ -4914,7 +4914,7 @@ async fn test_definition(
);
Ok(Some(lsp::GotoDefinitionResponse::Scalar(
lsp::Location::new(
lsp::Url::from_file_path(path!("/root/dir-2/c.rs")).unwrap(),
lsp::Uri::from_file_path(path!("/root/dir-2/c.rs")).unwrap(),
lsp::Range::new(lsp::Position::new(0, 5), lsp::Position::new(0, 7)),
),
)))
@@ -5049,15 +5049,15 @@ async fn test_references(
lsp_response_tx
.unbounded_send(Ok(Some(vec![
lsp::Location {
uri: lsp::Url::from_file_path(path!("/root/dir-1/two.rs")).unwrap(),
uri: lsp::Uri::from_file_path(path!("/root/dir-1/two.rs")).unwrap(),
range: lsp::Range::new(lsp::Position::new(0, 24), lsp::Position::new(0, 27)),
},
lsp::Location {
uri: lsp::Url::from_file_path(path!("/root/dir-1/two.rs")).unwrap(),
uri: lsp::Uri::from_file_path(path!("/root/dir-1/two.rs")).unwrap(),
range: lsp::Range::new(lsp::Position::new(0, 35), lsp::Position::new(0, 38)),
},
lsp::Location {
uri: lsp::Url::from_file_path(path!("/root/dir-2/three.rs")).unwrap(),
uri: lsp::Uri::from_file_path(path!("/root/dir-2/three.rs")).unwrap(),
range: lsp::Range::new(lsp::Position::new(0, 37), lsp::Position::new(0, 40)),
},
])))
@@ -5625,7 +5625,7 @@ async fn test_project_symbols(
lsp::SymbolInformation {
name: "TWO".into(),
location: lsp::Location {
uri: lsp::Url::from_file_path(path!("/code/crate-2/two.rs")).unwrap(),
uri: lsp::Uri::from_file_path(path!("/code/crate-2/two.rs")).unwrap(),
range: lsp::Range::new(lsp::Position::new(0, 6), lsp::Position::new(0, 9)),
},
kind: lsp::SymbolKind::CONSTANT,
@@ -5737,7 +5737,7 @@ async fn test_open_buffer_while_getting_definition_pointing_to_it(
|_, _| async move {
Ok(Some(lsp::GotoDefinitionResponse::Scalar(
lsp::Location::new(
lsp::Url::from_file_path(path!("/root/b.rs")).unwrap(),
lsp::Uri::from_file_path(path!("/root/b.rs")).unwrap(),
lsp::Range::new(lsp::Position::new(0, 6), lsp::Position::new(0, 9)),
),
)))

View File

@@ -1101,7 +1101,7 @@ impl RandomizedTest for ProjectCollaborationTest {
files
.into_iter()
.map(|file| lsp::Location {
uri: lsp::Url::from_file_path(file).unwrap(),
uri: lsp::Uri::from_file_path(file).unwrap(),
range: Default::default(),
})
.collect(),

View File

@@ -3047,7 +3047,7 @@ impl Render for CollabPanel {
.on_action(cx.listener(CollabPanel::move_channel_down))
.track_focus(&self.focus_handle)
.size_full()
.child(if !self.client.status().borrow().is_connected() {
.child(if !self.client.status().borrow().is_or_was_connected() {
self.render_signed_out(cx)
} else {
self.render_signed_in(window, cx)

View File

@@ -66,5 +66,7 @@ fn notification_window_options(
app_id: Some(app_id.to_owned()),
window_min_size: None,
window_decorations: Some(WindowDecorations::Client),
tabbing_identifier: None,
..Default::default()
}
}

View File

@@ -1,7 +1,7 @@
use gpui::Pixels;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use settings::{Settings, SettingsSources};
use settings::{Settings, SettingsSources, SettingsUi};
use workspace::dock::DockPosition;
#[derive(Deserialize, Debug)]
@@ -27,7 +27,7 @@ pub struct ChatPanelSettings {
pub default_width: Pixels,
}
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)]
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug, SettingsUi)]
pub struct ChatPanelSettingsContent {
/// When to show the panel button in the status bar.
///
@@ -50,7 +50,7 @@ pub struct NotificationPanelSettings {
pub default_width: Pixels,
}
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)]
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug, SettingsUi)]
pub struct PanelSettingsContent {
/// Whether to show the panel button in the status bar.
///
@@ -66,7 +66,7 @@ pub struct PanelSettingsContent {
pub default_width: Option<f32>,
}
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)]
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug, SettingsUi)]
pub struct MessageEditorSettings {
/// Whether to automatically replace emoji shortcodes with emoji characters.
/// For example: typing `:wave:` gets replaced with `👋`.

View File

@@ -25,7 +25,7 @@ use crate::{
};
const JSON_RPC_VERSION: &str = "2.0";
const REQUEST_TIMEOUT: Duration = Duration::from_secs(60);
const DEFAULT_REQUEST_TIMEOUT: Duration = Duration::from_secs(60);
// Standard JSON-RPC error codes
pub const PARSE_ERROR: i32 = -32700;
@@ -60,6 +60,7 @@ pub(crate) struct Client {
executor: BackgroundExecutor,
#[allow(dead_code)]
transport: Arc<dyn Transport>,
request_timeout: Option<Duration>,
}
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
@@ -143,6 +144,7 @@ pub struct ModelContextServerBinary {
pub executable: PathBuf,
pub args: Vec<String>,
pub env: Option<HashMap<String, String>>,
pub timeout: Option<u64>,
}
impl Client {
@@ -169,8 +171,9 @@ impl Client {
.map(|name| name.to_string_lossy().to_string())
.unwrap_or_else(String::new);
let timeout = binary.timeout.map(Duration::from_millis);
let transport = Arc::new(StdioTransport::new(binary, working_directory, &cx)?);
Self::new(server_id, server_name.into(), transport, cx)
Self::new(server_id, server_name.into(), transport, timeout, cx)
}
/// Creates a new Client instance for a context server.
@@ -178,6 +181,7 @@ impl Client {
server_id: ContextServerId,
server_name: Arc<str>,
transport: Arc<dyn Transport>,
request_timeout: Option<Duration>,
cx: AsyncApp,
) -> Result<Self> {
let (outbound_tx, outbound_rx) = channel::unbounded::<String>();
@@ -237,6 +241,7 @@ impl Client {
io_tasks: Mutex::new(Some((input_task, output_task))),
output_done_rx: Mutex::new(Some(output_done_rx)),
transport,
request_timeout,
})
}
@@ -327,8 +332,13 @@ impl Client {
method: &str,
params: impl Serialize,
) -> Result<T> {
self.request_with(method, params, None, Some(REQUEST_TIMEOUT))
.await
self.request_with(
method,
params,
None,
self.request_timeout.or(Some(DEFAULT_REQUEST_TIMEOUT)),
)
.await
}
pub async fn request_with<T: DeserializeOwned>(

View File

@@ -34,6 +34,8 @@ pub struct ContextServerCommand {
pub path: PathBuf,
pub args: Vec<String>,
pub env: Option<HashMap<String, String>>,
/// Timeout for tool calls in milliseconds. Defaults to 60000 (60 seconds) if not specified.
pub timeout: Option<u64>,
}
impl std::fmt::Debug for ContextServerCommand {
@@ -123,6 +125,7 @@ impl ContextServer {
executable: Path::new(&command.path).to_path_buf(),
args: command.args.clone(),
env: command.env.clone(),
timeout: command.timeout,
},
working_directory,
cx.clone(),
@@ -131,6 +134,7 @@ impl ContextServer {
client::ContextServerId(self.id.0.clone()),
self.id().0,
transport.clone(),
None,
cx.clone(),
)?,
})

View File

@@ -197,7 +197,7 @@ impl Status {
}
struct RegisteredBuffer {
uri: lsp::Url,
uri: lsp::Uri,
language_id: String,
snapshot: BufferSnapshot,
snapshot_version: i32,
@@ -1108,9 +1108,9 @@ fn id_for_language(language: Option<&Arc<Language>>) -> String {
.unwrap_or_else(|| "plaintext".to_string())
}
fn uri_for_buffer(buffer: &Entity<Buffer>, cx: &App) -> Result<lsp::Url, ()> {
fn uri_for_buffer(buffer: &Entity<Buffer>, cx: &App) -> Result<lsp::Uri, ()> {
if let Some(file) = buffer.read(cx).file().and_then(|file| file.as_local()) {
lsp::Url::from_file_path(file.abs_path(cx))
lsp::Uri::from_file_path(file.abs_path(cx))
} else {
format!("buffer://{}", buffer.entity_id())
.parse()
@@ -1201,7 +1201,7 @@ mod tests {
let (copilot, mut lsp) = Copilot::fake(cx);
let buffer_1 = cx.new(|cx| Buffer::local("Hello", cx));
let buffer_1_uri: lsp::Url = format!("buffer://{}", buffer_1.entity_id().as_u64())
let buffer_1_uri: lsp::Uri = format!("buffer://{}", buffer_1.entity_id().as_u64())
.parse()
.unwrap();
copilot.update(cx, |copilot, cx| copilot.register_buffer(&buffer_1, cx));
@@ -1219,7 +1219,7 @@ mod tests {
);
let buffer_2 = cx.new(|cx| Buffer::local("Goodbye", cx));
let buffer_2_uri: lsp::Url = format!("buffer://{}", buffer_2.entity_id().as_u64())
let buffer_2_uri: lsp::Uri = format!("buffer://{}", buffer_2.entity_id().as_u64())
.parse()
.unwrap();
copilot.update(cx, |copilot, cx| copilot.register_buffer(&buffer_2, cx));
@@ -1270,7 +1270,7 @@ mod tests {
text_document: lsp::TextDocumentIdentifier::new(buffer_1_uri),
}
);
let buffer_1_uri = lsp::Url::from_file_path(path!("/root/child/buffer-1")).unwrap();
let buffer_1_uri = lsp::Uri::from_file_path(path!("/root/child/buffer-1")).unwrap();
assert_eq!(
lsp.receive_notification::<lsp::notification::DidOpenTextDocument>()
.await,

View File

@@ -62,12 +62,6 @@ impl CopilotChatConfiguration {
}
}
// Copilot's base model; defined by Microsoft in premium requests table
// This will be moved to the front of the Copilot model list, and will be used for
// 'fast' requests (e.g. title generation)
// https://docs.github.com/en/copilot/managing-copilot/monitoring-usage-and-entitlements/about-premium-requests
const DEFAULT_MODEL_ID: &str = "gpt-4.1";
#[derive(Clone, Copy, Serialize, Deserialize, Debug, Eq, PartialEq)]
#[serde(rename_all = "lowercase")]
pub enum Role {
@@ -101,22 +95,39 @@ where
Ok(models)
}
#[derive(Clone, Serialize, Deserialize, Debug, Eq, PartialEq)]
#[derive(Clone, Serialize, Deserialize, Debug, PartialEq)]
pub struct Model {
billing: ModelBilling,
capabilities: ModelCapabilities,
id: String,
name: String,
policy: Option<ModelPolicy>,
vendor: ModelVendor,
is_chat_default: bool,
// The model with this value true is selected by VSCode copilot if a premium request limit is
// reached. Zed does not currently implement this behaviour
is_chat_fallback: bool,
model_picker_enabled: bool,
}
#[derive(Clone, Serialize, Deserialize, Debug, PartialEq)]
struct ModelBilling {
is_premium: bool,
multiplier: f64,
// List of plans a model is restricted to
// Field is not present if a model is available for all plans
#[serde(default)]
restricted_to: Option<Vec<String>>,
}
#[derive(Clone, Serialize, Deserialize, Debug, Eq, PartialEq)]
struct ModelCapabilities {
family: String,
#[serde(default)]
limits: ModelLimits,
supports: ModelSupportedFeatures,
#[serde(rename = "type")]
model_type: String,
}
#[derive(Default, Clone, Serialize, Deserialize, Debug, Eq, PartialEq)]
@@ -604,6 +615,7 @@ async fn get_models(
.into_iter()
.filter(|model| {
model.model_picker_enabled
&& model.capabilities.model_type.as_str() == "chat"
&& model
.policy
.as_ref()
@@ -612,9 +624,7 @@ async fn get_models(
.dedup_by(|a, b| a.capabilities.family == b.capabilities.family)
.collect();
if let Some(default_model_position) =
models.iter().position(|model| model.id == DEFAULT_MODEL_ID)
{
if let Some(default_model_position) = models.iter().position(|model| model.is_chat_default) {
let default_model = models.remove(default_model_position);
models.insert(0, default_model);
}
@@ -632,7 +642,9 @@ async fn request_models(
.uri(models_url.as_ref())
.header("Authorization", format!("Bearer {}", api_token))
.header("Content-Type", "application/json")
.header("Copilot-Integration-Id", "vscode-chat");
.header("Copilot-Integration-Id", "vscode-chat")
.header("Editor-Version", "vscode/1.103.2")
.header("x-github-api-version", "2025-05-01");
let request = request_builder.body(AsyncBody::empty())?;
@@ -803,6 +815,10 @@ mod tests {
let json = r#"{
"data": [
{
"billing": {
"is_premium": false,
"multiplier": 0
},
"capabilities": {
"family": "gpt-4",
"limits": {
@@ -816,6 +832,8 @@ mod tests {
"type": "chat"
},
"id": "gpt-4",
"is_chat_default": false,
"is_chat_fallback": false,
"model_picker_enabled": false,
"name": "GPT 4",
"object": "model",
@@ -827,6 +845,16 @@ mod tests {
"some-unknown-field": 123
},
{
"billing": {
"is_premium": true,
"multiplier": 1,
"restricted_to": [
"pro",
"pro_plus",
"business",
"enterprise"
]
},
"capabilities": {
"family": "claude-3.7-sonnet",
"limits": {
@@ -850,6 +878,8 @@ mod tests {
"type": "chat"
},
"id": "claude-3.7-sonnet",
"is_chat_default": false,
"is_chat_fallback": false,
"model_picker_enabled": true,
"name": "Claude 3.7 Sonnet",
"object": "model",

View File

@@ -102,7 +102,7 @@ pub struct GetCompletionsDocument {
pub tab_size: u32,
pub indent_size: u32,
pub insert_spaces: bool,
pub uri: lsp::Url,
pub uri: lsp::Uri,
pub relative_path: String,
pub position: lsp::Position,
pub version: usize,

View File

@@ -2,9 +2,9 @@ use dap_types::SteppingGranularity;
use gpui::{App, Global};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use settings::{Settings, SettingsSources};
use settings::{Settings, SettingsSources, SettingsUi};
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq, SettingsUi)]
#[serde(rename_all = "snake_case")]
pub enum DebugPanelDockPosition {
Left,
@@ -12,12 +12,16 @@ pub enum DebugPanelDockPosition {
Right,
}
#[derive(Serialize, Deserialize, JsonSchema, Clone, Copy)]
#[derive(Serialize, Deserialize, JsonSchema, Clone, Copy, SettingsUi)]
#[serde(default)]
// todo(settings_ui) @ben: I'm pretty sure not having the fields be optional here is a bug,
// it means the defaults will override previously set values if a single key is missing
#[settings_ui(group = "Debugger", path = "debugger")]
pub struct DebuggerSettings {
/// Determines the stepping granularity.
///
/// Default: line
#[settings_ui(skip)]
pub stepping_granularity: SteppingGranularity,
/// Whether the breakpoints should be reused across Zed sessions.
///

View File

@@ -234,6 +234,7 @@ impl PythonDebugAdapter {
.await
.map_err(|e| format!("{e:#?}"))?
.success();
if !did_succeed {
return Err("Failed to create base virtual environment".into());
}

View File

@@ -85,6 +85,10 @@ actions!(
Rerun,
/// Toggles expansion of the selected item in the debugger UI.
ToggleExpandItem,
/// Toggle the user frame filter in the stack frame list
/// When toggled on, only frames from the user's code are shown
/// When toggled off, all frames are shown
ToggleUserFrames,
]
);
@@ -272,12 +276,25 @@ pub fn init(cx: &mut App) {
}
})
.on_action({
let active_item = active_item.clone();
move |_: &ToggleIgnoreBreakpoints, _, cx| {
active_item
.update(cx, |item, cx| item.toggle_ignore_breakpoints(cx))
.ok();
}
})
.on_action(move |_: &ToggleUserFrames, _, cx| {
if let Some((thread_status, stack_frame_list)) = active_item
.read_with(cx, |item, cx| {
(item.thread_status(cx), item.stack_frame_list().clone())
})
.ok()
{
stack_frame_list.update(cx, |stack_frame_list, cx| {
stack_frame_list.toggle_frame_filter(thread_status, cx);
})
}
})
});
})
.detach();

View File

@@ -1,9 +1,9 @@
use std::{rc::Rc, time::Duration};
use std::rc::Rc;
use collections::HashMap;
use gpui::{Animation, AnimationExt as _, Entity, Transformation, WeakEntity, percentage};
use gpui::{Entity, WeakEntity};
use project::debugger::session::{ThreadId, ThreadStatus};
use ui::{ContextMenu, DropdownMenu, DropdownStyle, Indicator, prelude::*};
use ui::{CommonAnimationExt, ContextMenu, DropdownMenu, DropdownStyle, Indicator, prelude::*};
use util::{maybe, truncate_and_trailoff};
use crate::{
@@ -152,11 +152,7 @@ impl DebugPanel {
Icon::new(IconName::ArrowCircle)
.size(IconSize::Small)
.color(Color::Muted)
.with_animation(
"arrow-circle",
Animation::new(Duration::from_secs(2)).repeat(),
|icon, delta| icon.transform(Transformation::rotate(percentage(delta))),
)
.with_rotate_animation(2)
.into_any_element()
} else {
match running_state.thread_status(cx).unwrap_or_default() {

View File

@@ -1383,14 +1383,28 @@ impl PickerDelegate for DebugDelegate {
.border_color(cx.theme().colors().border_variant)
.children({
let action = menu::SecondaryConfirm.boxed_clone();
KeyBinding::for_action(&*action, window, cx).map(|keybind| {
Button::new("edit-debug-task", "Edit in debug.json")
.label_size(LabelSize::Small)
.key_binding(keybind)
.on_click(move |_, window, cx| {
window.dispatch_action(action.boxed_clone(), cx)
})
})
if self.matches.is_empty() {
Some(
Button::new("edit-debug-json", "Edit debug.json")
.label_size(LabelSize::Small)
.on_click(cx.listener(|_picker, _, window, cx| {
window.dispatch_action(
zed_actions::OpenProjectDebugTasks.boxed_clone(),
cx,
);
cx.emit(DismissEvent);
})),
)
} else {
KeyBinding::for_action(&*action, window, cx).map(|keybind| {
Button::new("edit-debug-task", "Edit in debug.json")
.label_size(LabelSize::Small)
.key_binding(keybind)
.on_click(move |_, window, cx| {
window.dispatch_action(action.boxed_clone(), cx)
})
})
}
})
.map(|this| {
if (current_modifiers.alt || self.matches.is_empty()) && !self.prompt.is_empty() {

View File

@@ -270,12 +270,9 @@ pub(crate) fn deserialize_pane_layout(
.children
.iter()
.map(|child| match child {
DebuggerPaneItem::Frames => Box::new(SubView::new(
stack_frame_list.focus_handle(cx),
stack_frame_list.clone().into(),
DebuggerPaneItem::Frames,
cx,
)),
DebuggerPaneItem::Frames => {
Box::new(SubView::stack_frame_list(stack_frame_list.clone(), cx))
}
DebuggerPaneItem::Variables => Box::new(SubView::new(
variable_list.focus_handle(cx),
variable_list.clone().into(),

View File

@@ -157,6 +157,29 @@ impl SubView {
})
}
pub(crate) fn stack_frame_list(
stack_frame_list: Entity<StackFrameList>,
cx: &mut App,
) -> Entity<Self> {
let weak_list = stack_frame_list.downgrade();
let this = Self::new(
stack_frame_list.focus_handle(cx),
stack_frame_list.into(),
DebuggerPaneItem::Frames,
cx,
);
this.update(cx, |this, _| {
this.with_actions(Box::new(move |_, cx| {
weak_list
.update(cx, |this, _| this.render_control_strip())
.unwrap_or_else(|_| div().into_any_element())
}));
});
this
}
pub(crate) fn console(console: Entity<Console>, cx: &mut App) -> Entity<Self> {
let weak_console = console.downgrade();
let this = Self::new(

View File

@@ -4,16 +4,17 @@ use std::time::Duration;
use anyhow::{Context as _, Result, anyhow};
use dap::StackFrameId;
use db::kvp::KEY_VALUE_STORE;
use gpui::{
AnyElement, Entity, EventEmitter, FocusHandle, Focusable, FontWeight, ListState, MouseButton,
Stateful, Subscription, Task, WeakEntity, list,
Action, AnyElement, Entity, EventEmitter, FocusHandle, Focusable, FontWeight, ListState,
MouseButton, Stateful, Subscription, Task, WeakEntity, list,
};
use util::debug_panic;
use crate::StackTraceView;
use crate::{StackTraceView, ToggleUserFrames};
use language::PointUtf16;
use project::debugger::breakpoint_store::ActiveStackFrame;
use project::debugger::session::{Session, SessionEvent, StackFrame};
use project::debugger::session::{Session, SessionEvent, StackFrame, ThreadStatus};
use project::{ProjectItem, ProjectPath};
use ui::{Scrollbar, ScrollbarState, Tooltip, prelude::*};
use workspace::{ItemHandle, Workspace};
@@ -26,6 +27,34 @@ pub enum StackFrameListEvent {
BuiltEntries,
}
/// Represents the filter applied to the stack frame list
#[derive(PartialEq, Eq, Copy, Clone)]
enum StackFrameFilter {
/// Show all frames
All,
/// Show only frames from the user's code
OnlyUserFrames,
}
impl StackFrameFilter {
fn from_str_or_default(s: impl AsRef<str>) -> Self {
match s.as_ref() {
"user" => StackFrameFilter::OnlyUserFrames,
"all" => StackFrameFilter::All,
_ => StackFrameFilter::All,
}
}
}
impl From<StackFrameFilter> for String {
fn from(filter: StackFrameFilter) -> Self {
match filter {
StackFrameFilter::All => "all".to_string(),
StackFrameFilter::OnlyUserFrames => "user".to_string(),
}
}
}
pub struct StackFrameList {
focus_handle: FocusHandle,
_subscription: Subscription,
@@ -37,6 +66,8 @@ pub struct StackFrameList {
opened_stack_frame_id: Option<StackFrameId>,
scrollbar_state: ScrollbarState,
list_state: ListState,
list_filter: StackFrameFilter,
filter_entries_indices: Vec<usize>,
error: Option<SharedString>,
_refresh_task: Task<()>,
}
@@ -73,6 +104,16 @@ impl StackFrameList {
let list_state = ListState::new(0, gpui::ListAlignment::Top, px(1000.));
let scrollbar_state = ScrollbarState::new(list_state.clone());
let list_filter = KEY_VALUE_STORE
.read_kvp(&format!(
"stack-frame-list-filter-{}",
session.read(cx).adapter().0
))
.ok()
.flatten()
.map(StackFrameFilter::from_str_or_default)
.unwrap_or(StackFrameFilter::All);
let mut this = Self {
session,
workspace,
@@ -80,9 +121,11 @@ impl StackFrameList {
state,
_subscription,
entries: Default::default(),
filter_entries_indices: Vec::default(),
error: None,
selected_ix: None,
opened_stack_frame_id: None,
list_filter,
list_state,
scrollbar_state,
_refresh_task: Task::ready(()),
@@ -103,7 +146,15 @@ impl StackFrameList {
) -> Vec<dap::StackFrame> {
self.entries
.iter()
.flat_map(|frame| match frame {
.enumerate()
.filter(|(ix, _)| {
self.list_filter == StackFrameFilter::All
|| self
.filter_entries_indices
.binary_search_by_key(&ix, |ix| ix)
.is_ok()
})
.flat_map(|(_, frame)| match frame {
StackFrameEntry::Normal(frame) => vec![frame.clone()],
StackFrameEntry::Label(frame) if show_labels => vec![frame.clone()],
StackFrameEntry::Collapsed(frames) if show_collapsed => frames.clone(),
@@ -126,7 +177,15 @@ impl StackFrameList {
self.stack_frames(cx)
.unwrap_or_default()
.into_iter()
.map(|stack_frame| stack_frame.dap)
.enumerate()
.filter(|(ix, _)| {
self.list_filter == StackFrameFilter::All
|| self
.filter_entries_indices
.binary_search_by_key(&ix, |ix| ix)
.is_ok()
})
.map(|(_, stack_frame)| stack_frame.dap)
.collect()
}
@@ -192,7 +251,32 @@ impl StackFrameList {
return;
}
};
for stack_frame in &stack_frames {
let worktree_prefixes: Vec<_> = self
.workspace
.read_with(cx, |workspace, cx| {
workspace
.visible_worktrees(cx)
.map(|tree| tree.read(cx).abs_path())
.collect()
})
.unwrap_or_default();
let mut filter_entries_indices = Vec::default();
for (ix, stack_frame) in stack_frames.iter().enumerate() {
let frame_in_visible_worktree = stack_frame.dap.source.as_ref().is_some_and(|source| {
source.path.as_ref().is_some_and(|path| {
worktree_prefixes
.iter()
.filter_map(|tree| tree.to_str())
.any(|tree| path.starts_with(tree))
})
});
if frame_in_visible_worktree {
filter_entries_indices.push(ix);
}
match stack_frame.dap.presentation_hint {
Some(dap::StackFramePresentationHint::Deemphasize)
| Some(dap::StackFramePresentationHint::Subtle) => {
@@ -225,8 +309,10 @@ impl StackFrameList {
let collapsed_entries = std::mem::take(&mut collapsed_entries);
if !collapsed_entries.is_empty() {
entries.push(StackFrameEntry::Collapsed(collapsed_entries));
self.filter_entries_indices.push(entries.len() - 1);
}
self.entries = entries;
self.filter_entries_indices = filter_entries_indices;
if let Some(ix) = first_stack_frame_with_path
.or(first_stack_frame)
@@ -242,7 +328,14 @@ impl StackFrameList {
self.selected_ix = ix;
}
self.list_state.reset(self.entries.len());
match self.list_filter {
StackFrameFilter::All => {
self.list_state.reset(self.entries.len());
}
StackFrameFilter::OnlyUserFrames => {
self.list_state.reset(self.filter_entries_indices.len());
}
}
cx.emit(StackFrameListEvent::BuiltEntries);
cx.notify();
}
@@ -572,6 +665,11 @@ impl StackFrameList {
}
fn render_entry(&self, ix: usize, cx: &mut Context<Self>) -> AnyElement {
let ix = match self.list_filter {
StackFrameFilter::All => ix,
StackFrameFilter::OnlyUserFrames => self.filter_entries_indices[ix],
};
match &self.entries[ix] {
StackFrameEntry::Label(stack_frame) => self.render_label_entry(stack_frame, cx),
StackFrameEntry::Normal(stack_frame) => self.render_normal_entry(ix, stack_frame, cx),
@@ -702,6 +800,67 @@ impl StackFrameList {
self.activate_selected_entry(window, cx);
}
pub(crate) fn toggle_frame_filter(
&mut self,
thread_status: Option<ThreadStatus>,
cx: &mut Context<Self>,
) {
self.list_filter = match self.list_filter {
StackFrameFilter::All => StackFrameFilter::OnlyUserFrames,
StackFrameFilter::OnlyUserFrames => StackFrameFilter::All,
};
if let Some(database_id) = self
.workspace
.read_with(cx, |workspace, _| workspace.database_id())
.ok()
.flatten()
{
let database_id: i64 = database_id.into();
let save_task = KEY_VALUE_STORE.write_kvp(
format!(
"stack-frame-list-filter-{}-{}",
self.session.read(cx).adapter().0,
database_id,
),
self.list_filter.into(),
);
cx.background_spawn(save_task).detach();
}
if let Some(ThreadStatus::Stopped) = thread_status {
match self.list_filter {
StackFrameFilter::All => {
self.list_state.reset(self.entries.len());
}
StackFrameFilter::OnlyUserFrames => {
self.list_state.reset(self.filter_entries_indices.len());
if !self
.selected_ix
.map(|ix| self.filter_entries_indices.contains(&ix))
.unwrap_or_default()
{
self.selected_ix = None;
}
}
}
if let Some(ix) = self.selected_ix {
let scroll_to = match self.list_filter {
StackFrameFilter::All => ix,
StackFrameFilter::OnlyUserFrames => self
.filter_entries_indices
.binary_search_by_key(&ix, |ix| *ix)
.expect("This index will always exist"),
};
self.list_state.scroll_to_reveal_item(scroll_to);
}
cx.emit(StackFrameListEvent::BuiltEntries);
cx.notify();
}
}
fn render_list(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
div().p_1().size_full().child(
list(
@@ -711,6 +870,30 @@ impl StackFrameList {
.size_full(),
)
}
pub(crate) fn render_control_strip(&self) -> AnyElement {
let tooltip_title = match self.list_filter {
StackFrameFilter::All => "Show stack frames from your project",
StackFrameFilter::OnlyUserFrames => "Show all stack frames",
};
h_flex()
.child(
IconButton::new(
"filter-by-visible-worktree-stack-frame-list",
IconName::ListFilter,
)
.tooltip(move |window, cx| {
Tooltip::for_action(tooltip_title, &ToggleUserFrames, window, cx)
})
.toggle_state(self.list_filter == StackFrameFilter::OnlyUserFrames)
.icon_size(IconSize::Small)
.on_click(|_, window, cx| {
window.dispatch_action(ToggleUserFrames.boxed_clone(), cx)
}),
)
.into_any_element()
}
}
impl Render for StackFrameList {

View File

@@ -752,3 +752,288 @@ async fn test_collapsed_entries(executor: BackgroundExecutor, cx: &mut TestAppCo
});
});
}
#[gpui::test]
async fn test_stack_frame_filter(executor: BackgroundExecutor, cx: &mut TestAppContext) {
init_test(cx);
let fs = FakeFs::new(executor.clone());
let test_file_content = r#"
function main() {
doSomething();
}
function doSomething() {
console.log('doing something');
}
"#
.unindent();
fs.insert_tree(
path!("/project"),
json!({
"src": {
"test.js": test_file_content,
}
}),
)
.await;
let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
let workspace = init_test_workspace(&project, cx).await;
let cx = &mut VisualTestContext::from_window(*workspace, cx);
let session = start_debug_session(&workspace, cx, |_| {}).unwrap();
let client = session.update(cx, |session, _| session.adapter_client().unwrap());
client.on_request::<Threads, _>(move |_, _| {
Ok(dap::ThreadsResponse {
threads: vec![dap::Thread {
id: 1,
name: "Thread 1".into(),
}],
})
});
client.on_request::<Scopes, _>(move |_, _| Ok(dap::ScopesResponse { scopes: vec![] }));
let stack_frames = vec![
StackFrame {
id: 1,
name: "main".into(),
source: Some(dap::Source {
name: Some("test.js".into()),
path: Some(path!("/project/src/test.js").into()),
source_reference: None,
presentation_hint: None,
origin: None,
sources: None,
adapter_data: None,
checksums: None,
}),
line: 2,
column: 1,
end_line: None,
end_column: None,
can_restart: None,
instruction_pointer_reference: None,
module_id: None,
presentation_hint: None,
},
StackFrame {
id: 2,
name: "node:internal/modules/cjs/loader".into(),
source: Some(dap::Source {
name: Some("loader.js".into()),
path: Some(path!("/usr/lib/node/internal/modules/cjs/loader.js").into()),
source_reference: None,
presentation_hint: None,
origin: None,
sources: None,
adapter_data: None,
checksums: None,
}),
line: 100,
column: 1,
end_line: None,
end_column: None,
can_restart: None,
instruction_pointer_reference: None,
module_id: None,
presentation_hint: Some(dap::StackFramePresentationHint::Deemphasize),
},
StackFrame {
id: 3,
name: "node:internal/modules/run_main".into(),
source: Some(dap::Source {
name: Some("run_main.js".into()),
path: Some(path!("/usr/lib/node/internal/modules/run_main.js").into()),
source_reference: None,
presentation_hint: None,
origin: None,
sources: None,
adapter_data: None,
checksums: None,
}),
line: 50,
column: 1,
end_line: None,
end_column: None,
can_restart: None,
instruction_pointer_reference: None,
module_id: None,
presentation_hint: Some(dap::StackFramePresentationHint::Deemphasize),
},
StackFrame {
id: 4,
name: "doSomething".into(),
source: Some(dap::Source {
name: Some("test.js".into()),
path: Some(path!("/project/src/test.js").into()),
source_reference: None,
presentation_hint: None,
origin: None,
sources: None,
adapter_data: None,
checksums: None,
}),
line: 3,
column: 1,
end_line: None,
end_column: None,
can_restart: None,
instruction_pointer_reference: None,
module_id: None,
presentation_hint: None,
},
];
// Store a copy for assertions
let stack_frames_for_assertions = stack_frames.clone();
client.on_request::<StackTrace, _>({
let stack_frames = Arc::new(stack_frames.clone());
move |_, args| {
assert_eq!(1, args.thread_id);
Ok(dap::StackTraceResponse {
stack_frames: (*stack_frames).clone(),
total_frames: None,
})
}
});
client
.fake_event(dap::messages::Events::Stopped(dap::StoppedEvent {
reason: dap::StoppedEventReason::Pause,
description: None,
thread_id: Some(1),
preserve_focus_hint: None,
text: None,
all_threads_stopped: None,
hit_breakpoint_ids: None,
}))
.await;
cx.run_until_parked();
// trigger threads to load
active_debug_session_panel(workspace, cx).update(cx, |session, cx| {
session.running_state().update(cx, |running_state, cx| {
running_state
.session()
.update(cx, |session, cx| session.threads(cx));
});
});
cx.run_until_parked();
// select first thread
active_debug_session_panel(workspace, cx).update_in(cx, |session, window, cx| {
session.running_state().update(cx, |running_state, cx| {
running_state.select_current_thread(
&running_state
.session()
.update(cx, |session, cx| session.threads(cx)),
window,
cx,
);
});
});
cx.run_until_parked();
// trigger stack frames to load
active_debug_session_panel(workspace, cx).update(cx, |debug_panel_item, cx| {
let stack_frame_list = debug_panel_item
.running_state()
.update(cx, |state, _| state.stack_frame_list().clone());
stack_frame_list.update(cx, |stack_frame_list, cx| {
stack_frame_list.dap_stack_frames(cx);
});
});
cx.run_until_parked();
active_debug_session_panel(workspace, cx).update_in(cx, |debug_panel_item, window, cx| {
let stack_frame_list = debug_panel_item
.running_state()
.update(cx, |state, _| state.stack_frame_list().clone());
stack_frame_list.update(cx, |stack_frame_list, cx| {
stack_frame_list.build_entries(true, window, cx);
// Verify we have the expected collapsed structure
assert_eq!(
stack_frame_list.entries(),
&vec![
StackFrameEntry::Normal(stack_frames_for_assertions[0].clone()),
StackFrameEntry::Collapsed(vec![
stack_frames_for_assertions[1].clone(),
stack_frames_for_assertions[2].clone()
]),
StackFrameEntry::Normal(stack_frames_for_assertions[3].clone()),
]
);
// Test 1: Verify filtering works
let all_frames = stack_frame_list.flatten_entries(true, false);
assert_eq!(all_frames.len(), 4, "Should see all 4 frames initially");
// Toggle to user frames only
stack_frame_list
.toggle_frame_filter(Some(project::debugger::session::ThreadStatus::Stopped), cx);
let user_frames = stack_frame_list.dap_stack_frames(cx);
assert_eq!(user_frames.len(), 2, "Should only see 2 user frames");
assert_eq!(user_frames[0].name, "main");
assert_eq!(user_frames[1].name, "doSomething");
// Test 2: Verify filtering toggles correctly
// Check we can toggle back and see all frames again
// Toggle back to all frames
stack_frame_list
.toggle_frame_filter(Some(project::debugger::session::ThreadStatus::Stopped), cx);
let all_frames_again = stack_frame_list.flatten_entries(true, false);
assert_eq!(
all_frames_again.len(),
4,
"Should see all 4 frames after toggling back"
);
// Test 3: Verify collapsed entries stay expanded
stack_frame_list.expand_collapsed_entry(1, cx);
assert_eq!(
stack_frame_list.entries(),
&vec![
StackFrameEntry::Normal(stack_frames_for_assertions[0].clone()),
StackFrameEntry::Normal(stack_frames_for_assertions[1].clone()),
StackFrameEntry::Normal(stack_frames_for_assertions[2].clone()),
StackFrameEntry::Normal(stack_frames_for_assertions[3].clone()),
]
);
// Toggle filter twice
stack_frame_list
.toggle_frame_filter(Some(project::debugger::session::ThreadStatus::Stopped), cx);
stack_frame_list
.toggle_frame_filter(Some(project::debugger::session::ThreadStatus::Stopped), cx);
// Verify entries remain expanded
assert_eq!(
stack_frame_list.entries(),
&vec![
StackFrameEntry::Normal(stack_frames_for_assertions[0].clone()),
StackFrameEntry::Normal(stack_frames_for_assertions[1].clone()),
StackFrameEntry::Normal(stack_frames_for_assertions[2].clone()),
StackFrameEntry::Normal(stack_frames_for_assertions[3].clone()),
],
"Expanded entries should remain expanded after toggling filter"
);
});
});
}

View File

@@ -9,7 +9,7 @@ use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::convert::TryFrom;
pub const DEEPSEEK_API_URL: &str = "https://api.deepseek.com";
pub const DEEPSEEK_API_URL: &str = "https://api.deepseek.com/v1";
#[derive(Clone, Copy, Serialize, Deserialize, Debug, Eq, PartialEq)]
#[serde(rename_all = "lowercase")]
@@ -263,12 +263,12 @@ pub async fn stream_completion(
api_key: &str,
request: Request,
) -> Result<BoxStream<'static, Result<StreamResponse>>> {
let uri = format!("{api_url}/v1/chat/completions");
let uri = format!("{api_url}/chat/completions");
let request_builder = HttpRequest::builder()
.method(Method::POST)
.uri(uri)
.header("Content-Type", "application/json")
.header("Authorization", format!("Bearer {}", api_key));
.header("Authorization", format!("Bearer {}", api_key.trim()));
let request = request_builder.body(AsyncBody::from(serde_json::to_string(&request)?))?;
let mut response = client.send(request).await?;

View File

@@ -10,8 +10,9 @@ use anyhow::Result;
use collections::{BTreeSet, HashMap};
use diagnostic_renderer::DiagnosticBlock;
use editor::{
DEFAULT_MULTIBUFFER_CONTEXT, Editor, EditorEvent, ExcerptRange, MultiBuffer, PathKey,
Editor, EditorEvent, ExcerptRange, MultiBuffer, PathKey,
display_map::{BlockPlacement, BlockProperties, BlockStyle, CustomBlockId},
multibuffer_context_lines,
};
use gpui::{
AnyElement, AnyView, App, AsyncApp, Context, Entity, EventEmitter, FocusHandle, Focusable,
@@ -493,10 +494,11 @@ impl ProjectDiagnosticsEditor {
}
let mut excerpt_ranges: Vec<ExcerptRange<Point>> = Vec::new();
let context_lines = cx.update(|_, cx| multibuffer_context_lines(cx))?;
for b in blocks.iter() {
let excerpt_range = context_range_for_entry(
b.initial_range.clone(),
DEFAULT_MULTIBUFFER_CONTEXT,
context_lines,
buffer_snapshot.clone(),
cx,
)

View File

@@ -24,6 +24,7 @@ use settings::SettingsStore;
use std::{
env,
path::{Path, PathBuf},
str::FromStr,
};
use unindent::Unindent as _;
use util::{RandomCharIter, path, post_inc};
@@ -70,7 +71,7 @@ async fn test_diagnostics(cx: &mut TestAppContext) {
let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
let cx = &mut VisualTestContext::from_window(*window, cx);
let workspace = window.root(cx).unwrap();
let uri = lsp::Url::from_file_path(path!("/test/main.rs")).unwrap();
let uri = lsp::Uri::from_file_path(path!("/test/main.rs")).unwrap();
// Create some diagnostics
lsp_store.update(cx, |lsp_store, cx| {
@@ -167,7 +168,7 @@ async fn test_diagnostics(cx: &mut TestAppContext) {
.update_diagnostics(
language_server_id,
lsp::PublishDiagnosticsParams {
uri: lsp::Url::from_file_path(path!("/test/consts.rs")).unwrap(),
uri: lsp::Uri::from_file_path(path!("/test/consts.rs")).unwrap(),
diagnostics: vec![lsp::Diagnostic {
range: lsp::Range::new(
lsp::Position::new(0, 15),
@@ -243,7 +244,7 @@ async fn test_diagnostics(cx: &mut TestAppContext) {
.update_diagnostics(
language_server_id,
lsp::PublishDiagnosticsParams {
uri: lsp::Url::from_file_path(path!("/test/consts.rs")).unwrap(),
uri: lsp::Uri::from_file_path(path!("/test/consts.rs")).unwrap(),
diagnostics: vec![
lsp::Diagnostic {
range: lsp::Range::new(
@@ -356,14 +357,14 @@ async fn test_diagnostics_with_folds(cx: &mut TestAppContext) {
.update_diagnostics(
server_id_1,
lsp::PublishDiagnosticsParams {
uri: lsp::Url::from_file_path(path!("/test/main.js")).unwrap(),
uri: lsp::Uri::from_file_path(path!("/test/main.js")).unwrap(),
diagnostics: vec![lsp::Diagnostic {
range: lsp::Range::new(lsp::Position::new(4, 0), lsp::Position::new(4, 4)),
severity: Some(lsp::DiagnosticSeverity::WARNING),
message: "no method `tset`".to_string(),
related_information: Some(vec![lsp::DiagnosticRelatedInformation {
location: lsp::Location::new(
lsp::Url::from_file_path(path!("/test/main.js")).unwrap(),
lsp::Uri::from_file_path(path!("/test/main.js")).unwrap(),
lsp::Range::new(
lsp::Position::new(0, 9),
lsp::Position::new(0, 13),
@@ -465,7 +466,7 @@ async fn test_diagnostics_multiple_servers(cx: &mut TestAppContext) {
.update_diagnostics(
server_id_1,
lsp::PublishDiagnosticsParams {
uri: lsp::Url::from_file_path(path!("/test/main.js")).unwrap(),
uri: lsp::Uri::from_file_path(path!("/test/main.js")).unwrap(),
diagnostics: vec![lsp::Diagnostic {
range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 1)),
severity: Some(lsp::DiagnosticSeverity::WARNING),
@@ -509,7 +510,7 @@ async fn test_diagnostics_multiple_servers(cx: &mut TestAppContext) {
.update_diagnostics(
server_id_2,
lsp::PublishDiagnosticsParams {
uri: lsp::Url::from_file_path(path!("/test/main.js")).unwrap(),
uri: lsp::Uri::from_file_path(path!("/test/main.js")).unwrap(),
diagnostics: vec![lsp::Diagnostic {
range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 1)),
severity: Some(lsp::DiagnosticSeverity::ERROR),
@@ -552,7 +553,7 @@ async fn test_diagnostics_multiple_servers(cx: &mut TestAppContext) {
.update_diagnostics(
server_id_1,
lsp::PublishDiagnosticsParams {
uri: lsp::Url::from_file_path(path!("/test/main.js")).unwrap(),
uri: lsp::Uri::from_file_path(path!("/test/main.js")).unwrap(),
diagnostics: vec![lsp::Diagnostic {
range: lsp::Range::new(lsp::Position::new(2, 0), lsp::Position::new(2, 1)),
severity: Some(lsp::DiagnosticSeverity::WARNING),
@@ -571,7 +572,7 @@ async fn test_diagnostics_multiple_servers(cx: &mut TestAppContext) {
.update_diagnostics(
server_id_2,
lsp::PublishDiagnosticsParams {
uri: lsp::Url::from_file_path(path!("/test/main.rs")).unwrap(),
uri: lsp::Uri::from_file_path(path!("/test/main.rs")).unwrap(),
diagnostics: vec![],
version: None,
},
@@ -608,7 +609,7 @@ async fn test_diagnostics_multiple_servers(cx: &mut TestAppContext) {
.update_diagnostics(
server_id_2,
lsp::PublishDiagnosticsParams {
uri: lsp::Url::from_file_path(path!("/test/main.js")).unwrap(),
uri: lsp::Uri::from_file_path(path!("/test/main.js")).unwrap(),
diagnostics: vec![lsp::Diagnostic {
range: lsp::Range::new(lsp::Position::new(3, 0), lsp::Position::new(3, 1)),
severity: Some(lsp::DiagnosticSeverity::WARNING),
@@ -745,8 +746,8 @@ async fn test_random_diagnostics_blocks(cx: &mut TestAppContext, mut rng: StdRng
.update_diagnostics(
server_id,
lsp::PublishDiagnosticsParams {
uri: lsp::Url::from_file_path(&path).unwrap_or_else(|_| {
lsp::Url::parse("file:///test/fallback.rs").unwrap()
uri: lsp::Uri::from_file_path(&path).unwrap_or_else(|_| {
lsp::Uri::from_str("file:///test/fallback.rs").unwrap()
}),
diagnostics: diagnostics.clone(),
version: None,
@@ -934,8 +935,8 @@ async fn test_random_diagnostics_with_inlays(cx: &mut TestAppContext, mut rng: S
.update_diagnostics(
server_id,
lsp::PublishDiagnosticsParams {
uri: lsp::Url::from_file_path(&path).unwrap_or_else(|_| {
lsp::Url::parse("file:///test/fallback.rs").unwrap()
uri: lsp::Uri::from_file_path(&path).unwrap_or_else(|_| {
lsp::Uri::from_str("file:///test/fallback.rs").unwrap()
}),
diagnostics: diagnostics.clone(),
version: None,
@@ -985,7 +986,7 @@ async fn active_diagnostics_dismiss_after_invalidation(cx: &mut TestAppContext)
.update_diagnostics(
LanguageServerId(0),
lsp::PublishDiagnosticsParams {
uri: lsp::Url::from_file_path(path!("/root/file")).unwrap(),
uri: lsp::Uri::from_file_path(path!("/root/file")).unwrap(),
version: None,
diagnostics: vec![lsp::Diagnostic {
range: lsp::Range::new(
@@ -1028,7 +1029,7 @@ async fn active_diagnostics_dismiss_after_invalidation(cx: &mut TestAppContext)
.update_diagnostics(
LanguageServerId(0),
lsp::PublishDiagnosticsParams {
uri: lsp::Url::from_file_path(path!("/root/file")).unwrap(),
uri: lsp::Uri::from_file_path(path!("/root/file")).unwrap(),
version: None,
diagnostics: Vec::new(),
},
@@ -1078,7 +1079,7 @@ async fn cycle_through_same_place_diagnostics(cx: &mut TestAppContext) {
.update_diagnostics(
LanguageServerId(0),
lsp::PublishDiagnosticsParams {
uri: lsp::Url::from_file_path(path!("/root/file")).unwrap(),
uri: lsp::Uri::from_file_path(path!("/root/file")).unwrap(),
version: None,
diagnostics: vec![
lsp::Diagnostic {
@@ -1246,7 +1247,7 @@ async fn test_diagnostics_with_links(cx: &mut TestAppContext) {
lsp_store.update_diagnostics(
LanguageServerId(0),
lsp::PublishDiagnosticsParams {
uri: lsp::Url::from_file_path(path!("/root/file")).unwrap(),
uri: lsp::Uri::from_file_path(path!("/root/file")).unwrap(),
version: None,
diagnostics: vec![lsp::Diagnostic {
range: lsp::Range::new(lsp::Position::new(0, 8), lsp::Position::new(0, 12)),
@@ -1299,7 +1300,7 @@ async fn test_hover_diagnostic_and_info_popovers(cx: &mut gpui::TestAppContext)
lsp_store.update_diagnostics(
LanguageServerId(0),
lsp::PublishDiagnosticsParams {
uri: lsp::Url::from_file_path(path!("/root/dir/file.rs")).unwrap(),
uri: lsp::Uri::from_file_path(path!("/root/dir/file.rs")).unwrap(),
version: None,
diagnostics: vec![lsp::Diagnostic {
range,
@@ -1376,7 +1377,7 @@ async fn test_diagnostics_with_code(cx: &mut TestAppContext) {
let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
let cx = &mut VisualTestContext::from_window(*window, cx);
let workspace = window.root(cx).unwrap();
let uri = lsp::Url::from_file_path(path!("/root/main.js")).unwrap();
let uri = lsp::Uri::from_file_path(path!("/root/main.js")).unwrap();
// Create diagnostics with code fields
lsp_store.update(cx, |lsp_store, cx| {
@@ -1460,7 +1461,7 @@ async fn go_to_diagnostic_with_severity(cx: &mut TestAppContext) {
.update_diagnostics(
LanguageServerId(0),
lsp::PublishDiagnosticsParams {
uri: lsp::Url::from_file_path(path!("/root/file")).unwrap(),
uri: lsp::Uri::from_file_path(path!("/root/file")).unwrap(),
version: None,
diagnostics: vec![
lsp::Diagnostic {
@@ -1673,7 +1674,7 @@ fn random_lsp_diagnostic(
);
related_info.push(lsp::DiagnosticRelatedInformation {
location: lsp::Location::new(lsp::Url::from_file_path(path).unwrap(), info_range),
location: lsp::Location::new(lsp::Uri::from_file_path(path).unwrap(), info_range),
message: format!("related info {i} for diagnostic {unique_id}"),
});
}

View File

@@ -228,21 +228,29 @@ pub struct ShowCompletions {
pub struct HandleInput(pub String);
/// Deletes from the cursor to the end of the next word.
/// Stops before the end of the next word, if whitespace sequences of length >= 2 are encountered.
#[derive(PartialEq, Clone, Deserialize, Default, JsonSchema, Action)]
#[action(namespace = editor)]
#[serde(deny_unknown_fields)]
pub struct DeleteToNextWordEnd {
#[serde(default)]
pub ignore_newlines: bool,
// Whether to stop before the end of the next word, if language-defined bracket is encountered.
#[serde(default)]
pub ignore_brackets: bool,
}
/// Deletes from the cursor to the start of the previous word.
/// Stops before the start of the previous word, if whitespace sequences of length >= 2 are encountered.
#[derive(PartialEq, Clone, Deserialize, Default, JsonSchema, Action)]
#[action(namespace = editor)]
#[serde(deny_unknown_fields)]
pub struct DeleteToPreviousWordStart {
#[serde(default)]
pub ignore_newlines: bool,
// Whether to stop before the start of the previous word, if language-defined bracket is encountered.
#[serde(default)]
pub ignore_brackets: bool,
}
/// Folds all code blocks at the specified indentation level.
@@ -753,6 +761,8 @@ actions!(
UniqueLinesCaseInsensitive,
/// Removes duplicate lines (case-sensitive).
UniqueLinesCaseSensitive,
UnwrapSyntaxNode
UnwrapSyntaxNode,
/// Wraps selections in tag specified by language.
WrapSelectionsInTag
]
);

View File

@@ -1,7 +1,9 @@
use crate::scroll::ScrollAmount;
use fuzzy::{StringMatch, StringMatchCandidate};
use gpui::{
AnyElement, Entity, Focusable, FontWeight, ListSizingBehavior, ScrollStrategy, SharedString,
Size, StrikethroughStyle, StyledText, Task, UniformListScrollHandle, div, px, uniform_list,
AnyElement, Entity, Focusable, FontWeight, ListSizingBehavior, ScrollHandle, ScrollStrategy,
SharedString, Size, StrikethroughStyle, StyledText, Task, UniformListScrollHandle, div, px,
uniform_list,
};
use itertools::Itertools;
use language::CodeLabel;
@@ -184,6 +186,20 @@ impl CodeContextMenu {
CodeContextMenu::CodeActions(_) => false,
}
}
pub fn scroll_aside(
&mut self,
scroll_amount: ScrollAmount,
window: &mut Window,
cx: &mut Context<Editor>,
) {
match self {
CodeContextMenu::Completions(completions_menu) => {
completions_menu.scroll_aside(scroll_amount, window, cx)
}
CodeContextMenu::CodeActions(_) => (),
}
}
}
pub enum ContextMenuOrigin {
@@ -207,6 +223,9 @@ pub struct CompletionsMenu {
filter_task: Task<()>,
cancel_filter: Arc<AtomicBool>,
scroll_handle: UniformListScrollHandle,
// The `ScrollHandle` used on the Markdown documentation rendered on the
// side of the completions menu.
pub scroll_handle_aside: ScrollHandle,
resolve_completions: bool,
show_completion_documentation: bool,
last_rendered_range: Rc<RefCell<Option<Range<usize>>>>,
@@ -281,6 +300,7 @@ impl CompletionsMenu {
filter_task: Task::ready(()),
cancel_filter: Arc::new(AtomicBool::new(false)),
scroll_handle: UniformListScrollHandle::new(),
scroll_handle_aside: ScrollHandle::new(),
resolve_completions: true,
last_rendered_range: RefCell::new(None).into(),
markdown_cache: RefCell::new(VecDeque::new()).into(),
@@ -351,6 +371,7 @@ impl CompletionsMenu {
filter_task: Task::ready(()),
cancel_filter: Arc::new(AtomicBool::new(false)),
scroll_handle: UniformListScrollHandle::new(),
scroll_handle_aside: ScrollHandle::new(),
resolve_completions: false,
show_completion_documentation: false,
last_rendered_range: RefCell::new(None).into(),
@@ -948,6 +969,7 @@ impl CompletionsMenu {
.max_w(max_size.width)
.max_h(max_size.height)
.overflow_y_scroll()
.track_scroll(&self.scroll_handle_aside)
.occlude(),
)
.into_any_element(),
@@ -1212,6 +1234,23 @@ impl CompletionsMenu {
}
});
}
pub fn scroll_aside(
&mut self,
amount: ScrollAmount,
window: &mut Window,
cx: &mut Context<Editor>,
) {
let mut offset = self.scroll_handle_aside.offset();
offset.y -= amount.pixels(
window.line_height(),
self.scroll_handle_aside.bounds().size.height - px(16.),
) / 2.0;
cx.notify();
self.scroll_handle_aside.set_offset(offset);
}
}
#[derive(Clone)]

View File

@@ -190,7 +190,6 @@ use std::{
sync::Arc,
time::{Duration, Instant},
};
use sum_tree::TreeMap;
use task::{ResolvedTask, RunnableTag, TaskTemplate, TaskVariables};
use text::{BufferId, FromAnchor, OffsetUtf16, Rope};
use theme::{
@@ -220,7 +219,6 @@ use crate::{
pub const FILE_HEADER_HEIGHT: u32 = 2;
pub const MULTI_BUFFER_EXCERPT_HEADER_HEIGHT: u32 = 1;
pub const DEFAULT_MULTIBUFFER_CONTEXT: u32 = 2;
const CURSOR_BLINK_INTERVAL: Duration = Duration::from_millis(500);
const MAX_LINE_LEN: usize = 1024;
const MIN_NAVIGATION_HISTORY_ROW_DELTA: i64 = 10;
@@ -228,7 +226,7 @@ const MAX_SELECTION_HISTORY_LEN: usize = 1024;
pub(crate) const CURSORS_VISIBLE_FOR: Duration = Duration::from_millis(2000);
#[doc(hidden)]
pub const CODE_ACTIONS_DEBOUNCE_TIMEOUT: Duration = Duration::from_millis(250);
const SELECTION_HIGHLIGHT_DEBOUNCE_TIMEOUT: Duration = Duration::from_millis(100);
pub const SELECTION_HIGHLIGHT_DEBOUNCE_TIMEOUT: Duration = Duration::from_millis(100);
pub(crate) const CODE_ACTION_TIMEOUT: Duration = Duration::from_secs(5);
pub(crate) const FORMAT_TIMEOUT: Duration = Duration::from_secs(5);
@@ -1061,8 +1059,8 @@ pub struct Editor {
placeholder_text: Option<Arc<str>>,
highlight_order: usize,
highlighted_rows: HashMap<TypeId, Vec<RowHighlight>>,
background_highlights: TreeMap<HighlightKey, BackgroundHighlight>,
gutter_highlights: TreeMap<TypeId, GutterHighlight>,
background_highlights: HashMap<HighlightKey, BackgroundHighlight>,
gutter_highlights: HashMap<TypeId, GutterHighlight>,
scrollbar_marker_state: ScrollbarMarkerState,
active_indent_guides_state: ActiveIndentGuidesState,
nav_history: Option<ItemNavHistory>,
@@ -2113,8 +2111,8 @@ impl Editor {
placeholder_text: None,
highlight_order: 0,
highlighted_rows: HashMap::default(),
background_highlights: TreeMap::default(),
gutter_highlights: TreeMap::default(),
background_highlights: HashMap::default(),
gutter_highlights: HashMap::default(),
scrollbar_marker_state: ScrollbarMarkerState::default(),
active_indent_guides_state: ActiveIndentGuidesState::default(),
nav_history: None,
@@ -2589,7 +2587,7 @@ impl Editor {
|| binding
.keystrokes()
.first()
.is_some_and(|keystroke| keystroke.display_modifiers.modified())
.is_some_and(|keystroke| keystroke.modifiers().modified())
}))
}
@@ -6412,7 +6410,7 @@ impl Editor {
PathKey::for_buffer(buffer_handle, cx),
buffer_handle.clone(),
edited_ranges,
DEFAULT_MULTIBUFFER_CONTEXT,
multibuffer_context_lines(cx),
cx,
);
@@ -6631,7 +6629,7 @@ impl Editor {
buffer_row: Some(point.row),
..Default::default()
};
let Some(blame_entry) = blame
let Some((buffer, blame_entry)) = blame
.update(cx, |blame, cx| blame.blame_for_rows(&[row_info], cx).next())
.flatten()
else {
@@ -6641,12 +6639,19 @@ impl Editor {
let anchor = self.selections.newest_anchor().head();
let position = self.to_pixel_point(anchor, &snapshot, window);
if let (Some(position), Some(last_bounds)) = (position, self.last_bounds) {
self.show_blame_popover(&blame_entry, position + last_bounds.origin, true, cx);
self.show_blame_popover(
buffer,
&blame_entry,
position + last_bounds.origin,
true,
cx,
);
};
}
fn show_blame_popover(
&mut self,
buffer: BufferId,
blame_entry: &BlameEntry,
position: gpui::Point<Pixels>,
ignore_timeout: bool,
@@ -6670,7 +6675,7 @@ impl Editor {
return;
};
let blame = blame.read(cx);
let details = blame.details_for_entry(&blame_entry);
let details = blame.details_for_entry(buffer, &blame_entry);
let markdown = cx.new(|cx| {
Markdown::new(
details
@@ -7696,16 +7701,16 @@ impl Editor {
.keystroke()
{
modifiers_held = modifiers_held
|| (&accept_keystroke.display_modifiers == modifiers
&& accept_keystroke.display_modifiers.modified());
|| (accept_keystroke.modifiers() == modifiers
&& accept_keystroke.modifiers().modified());
};
if let Some(accept_partial_keystroke) = self
.accept_edit_prediction_keybind(true, window, cx)
.keystroke()
{
modifiers_held = modifiers_held
|| (&accept_partial_keystroke.display_modifiers == modifiers
&& accept_partial_keystroke.display_modifiers.modified());
|| (accept_partial_keystroke.modifiers() == modifiers
&& accept_partial_keystroke.modifiers().modified());
}
if modifiers_held {
@@ -9059,7 +9064,7 @@ impl Editor {
let is_platform_style_mac = PlatformStyle::platform() == PlatformStyle::Mac;
let modifiers_color = if accept_keystroke.display_modifiers == window.modifiers() {
let modifiers_color = if *accept_keystroke.modifiers() == window.modifiers() {
Color::Accent
} else {
Color::Muted
@@ -9071,19 +9076,19 @@ impl Editor {
.font(theme::ThemeSettings::get_global(cx).buffer_font.clone())
.text_size(TextSize::XSmall.rems(cx))
.child(h_flex().children(ui::render_modifiers(
&accept_keystroke.display_modifiers,
accept_keystroke.modifiers(),
PlatformStyle::platform(),
Some(modifiers_color),
Some(IconSize::XSmall.rems().into()),
true,
)))
.when(is_platform_style_mac, |parent| {
parent.child(accept_keystroke.display_key.clone())
parent.child(accept_keystroke.key().to_string())
})
.when(!is_platform_style_mac, |parent| {
parent.child(
Key::new(
util::capitalize(&accept_keystroke.display_key),
util::capitalize(accept_keystroke.key()),
Some(Color::Default),
)
.size(Some(IconSize::XSmall.rems().into())),
@@ -9264,7 +9269,7 @@ impl Editor {
accept_keystroke.as_ref(),
|el, accept_keystroke| {
el.child(h_flex().children(ui::render_modifiers(
&accept_keystroke.display_modifiers,
accept_keystroke.modifiers(),
PlatformStyle::platform(),
Some(Color::Default),
Some(IconSize::XSmall.rems().into()),
@@ -9334,7 +9339,7 @@ impl Editor {
.child(completion),
)
.when_some(accept_keystroke, |el, accept_keystroke| {
if !accept_keystroke.display_modifiers.modified() {
if !accept_keystroke.modifiers().modified() {
return el;
}
@@ -9353,7 +9358,7 @@ impl Editor {
.font(theme::ThemeSettings::get_global(cx).buffer_font.clone())
.when(is_platform_style_mac, |parent| parent.gap_1())
.child(h_flex().children(ui::render_modifiers(
&accept_keystroke.display_modifiers,
accept_keystroke.modifiers(),
PlatformStyle::platform(),
Some(if !has_completion {
Color::Muted
@@ -10462,6 +10467,86 @@ impl Editor {
})
}
fn enable_wrap_selections_in_tag(&self, cx: &App) -> bool {
let snapshot = self.buffer.read(cx).snapshot(cx);
for selection in self.selections.disjoint_anchors().iter() {
if snapshot
.language_at(selection.start)
.and_then(|lang| lang.config().wrap_characters.as_ref())
.is_some()
{
return true;
}
}
false
}
fn wrap_selections_in_tag(
&mut self,
_: &WrapSelectionsInTag,
window: &mut Window,
cx: &mut Context<Self>,
) {
self.hide_mouse_cursor(HideMouseCursorOrigin::TypingAction, cx);
let snapshot = self.buffer.read(cx).snapshot(cx);
let mut edits = Vec::new();
let mut boundaries = Vec::new();
for selection in self.selections.all::<Point>(cx).iter() {
let Some(wrap_config) = snapshot
.language_at(selection.start)
.and_then(|lang| lang.config().wrap_characters.clone())
else {
continue;
};
let open_tag = format!("{}{}", wrap_config.start_prefix, wrap_config.start_suffix);
let close_tag = format!("{}{}", wrap_config.end_prefix, wrap_config.end_suffix);
let start_before = snapshot.anchor_before(selection.start);
let end_after = snapshot.anchor_after(selection.end);
edits.push((start_before..start_before, open_tag));
edits.push((end_after..end_after, close_tag));
boundaries.push((
start_before,
end_after,
wrap_config.start_prefix.len(),
wrap_config.end_suffix.len(),
));
}
if edits.is_empty() {
return;
}
self.transact(window, cx, |this, window, cx| {
let buffer = this.buffer.update(cx, |buffer, cx| {
buffer.edit(edits, None, cx);
buffer.snapshot(cx)
});
let mut new_selections = Vec::with_capacity(boundaries.len() * 2);
for (start_before, end_after, start_prefix_len, end_suffix_len) in
boundaries.into_iter()
{
let open_offset = start_before.to_offset(&buffer) + start_prefix_len;
let close_offset = end_after.to_offset(&buffer).saturating_sub(end_suffix_len);
new_selections.push(open_offset..open_offset);
new_selections.push(close_offset..close_offset);
}
this.change_selections(Default::default(), window, cx, |s| {
s.select_ranges(new_selections);
});
this.request_autoscroll(Autoscroll::fit(), cx);
});
}
pub fn reload_file(&mut self, _: &ReloadFile, window: &mut Window, cx: &mut Context<Self>) {
let Some(project) = self.project.clone() else {
return;
@@ -11752,6 +11837,18 @@ impl Editor {
let buffer = self.buffer.read(cx).snapshot(cx);
let selections = self.selections.all::<Point>(cx);
#[derive(Clone, Debug, PartialEq)]
enum CommentFormat {
/// single line comment, with prefix for line
Line(String),
/// single line within a block comment, with prefix for line
BlockLine(String),
/// a single line of a block comment that includes the initial delimiter
BlockCommentWithStart(BlockCommentConfig),
/// a single line of a block comment that includes the ending delimiter
BlockCommentWithEnd(BlockCommentConfig),
}
// Split selections to respect paragraph, indent, and comment prefix boundaries.
let wrap_ranges = selections.into_iter().flat_map(|selection| {
let mut non_blank_rows_iter = (selection.start.row..=selection.end.row)
@@ -11768,37 +11865,75 @@ impl Editor {
let language_scope = buffer.language_scope_at(selection.head());
let indent_and_prefix_for_row =
|row: u32| -> (IndentSize, Option<String>, Option<String>) {
|row: u32| -> (IndentSize, Option<CommentFormat>, Option<String>) {
let indent = buffer.indent_size_for_line(MultiBufferRow(row));
let (comment_prefix, rewrap_prefix) =
if let Some(language_scope) = &language_scope {
let indent_end = Point::new(row, indent.len);
let comment_prefix = language_scope
let (comment_prefix, rewrap_prefix) = if let Some(language_scope) =
&language_scope
{
let indent_end = Point::new(row, indent.len);
let line_end = Point::new(row, buffer.line_len(MultiBufferRow(row)));
let line_text_after_indent = buffer
.text_for_range(indent_end..line_end)
.collect::<String>();
let is_within_comment_override = buffer
.language_scope_at(indent_end)
.is_some_and(|scope| scope.override_name() == Some("comment"));
let comment_delimiters = if is_within_comment_override {
// we are within a comment syntax node, but we don't
// yet know what kind of comment: block, doc or line
match (
language_scope.documentation_comment(),
language_scope.block_comment(),
) {
(Some(config), _) | (_, Some(config))
if buffer.contains_str_at(indent_end, &config.start) =>
{
Some(CommentFormat::BlockCommentWithStart(config.clone()))
}
(Some(config), _) | (_, Some(config))
if line_text_after_indent.ends_with(config.end.as_ref()) =>
{
Some(CommentFormat::BlockCommentWithEnd(config.clone()))
}
(Some(config), _) | (_, Some(config))
if buffer.contains_str_at(indent_end, &config.prefix) =>
{
Some(CommentFormat::BlockLine(config.prefix.to_string()))
}
(_, _) => language_scope
.line_comment_prefixes()
.iter()
.find(|prefix| buffer.contains_str_at(indent_end, prefix))
.map(|prefix| CommentFormat::Line(prefix.to_string())),
}
} else {
// we not in an overridden comment node, but we may
// be within a non-overridden line comment node
language_scope
.line_comment_prefixes()
.iter()
.find(|prefix| buffer.contains_str_at(indent_end, prefix))
.map(|prefix| prefix.to_string());
let line_end = Point::new(row, buffer.line_len(MultiBufferRow(row)));
let line_text_after_indent = buffer
.text_for_range(indent_end..line_end)
.collect::<String>();
let rewrap_prefix = language_scope
.rewrap_prefixes()
.iter()
.find_map(|prefix_regex| {
prefix_regex.find(&line_text_after_indent).map(|mat| {
if mat.start() == 0 {
Some(mat.as_str().to_string())
} else {
None
}
})
})
.flatten();
(comment_prefix, rewrap_prefix)
} else {
(None, None)
.map(|prefix| CommentFormat::Line(prefix.to_string()))
};
let rewrap_prefix = language_scope
.rewrap_prefixes()
.iter()
.find_map(|prefix_regex| {
prefix_regex.find(&line_text_after_indent).map(|mat| {
if mat.start() == 0 {
Some(mat.as_str().to_string())
} else {
None
}
})
})
.flatten();
(comment_delimiters, rewrap_prefix)
} else {
(None, None)
};
(indent, comment_prefix, rewrap_prefix)
};
@@ -11809,22 +11944,22 @@ impl Editor {
let mut prev_row = first_row;
let (
mut current_range_indent,
mut current_range_comment_prefix,
mut current_range_comment_delimiters,
mut current_range_rewrap_prefix,
) = indent_and_prefix_for_row(first_row);
for row in non_blank_rows_iter.skip(1) {
let has_paragraph_break = row > prev_row + 1;
let (row_indent, row_comment_prefix, row_rewrap_prefix) =
let (row_indent, row_comment_delimiters, row_rewrap_prefix) =
indent_and_prefix_for_row(row);
let has_indent_change = row_indent != current_range_indent;
let has_comment_change = row_comment_prefix != current_range_comment_prefix;
let has_comment_change = row_comment_delimiters != current_range_comment_delimiters;
let has_boundary_change = has_comment_change
|| row_rewrap_prefix.is_some()
|| (has_indent_change && current_range_comment_prefix.is_some());
|| (has_indent_change && current_range_comment_delimiters.is_some());
if has_paragraph_break || has_boundary_change {
ranges.push((
@@ -11832,13 +11967,13 @@ impl Editor {
Point::new(current_range_start, 0)
..Point::new(prev_row, buffer.line_len(MultiBufferRow(prev_row))),
current_range_indent,
current_range_comment_prefix.clone(),
current_range_comment_delimiters.clone(),
current_range_rewrap_prefix.clone(),
from_empty_selection,
));
current_range_start = row;
current_range_indent = row_indent;
current_range_comment_prefix = row_comment_prefix;
current_range_comment_delimiters = row_comment_delimiters;
current_range_rewrap_prefix = row_rewrap_prefix;
}
prev_row = row;
@@ -11849,7 +11984,7 @@ impl Editor {
Point::new(current_range_start, 0)
..Point::new(prev_row, buffer.line_len(MultiBufferRow(prev_row))),
current_range_indent,
current_range_comment_prefix,
current_range_comment_delimiters,
current_range_rewrap_prefix,
from_empty_selection,
));
@@ -11863,7 +11998,7 @@ impl Editor {
for (
language_settings,
wrap_range,
indent_size,
mut indent_size,
comment_prefix,
rewrap_prefix,
from_empty_selection,
@@ -11883,16 +12018,26 @@ impl Editor {
let tab_size = language_settings.tab_size;
let (line_prefix, inside_comment) = match &comment_prefix {
Some(CommentFormat::Line(prefix) | CommentFormat::BlockLine(prefix)) => {
(Some(prefix.as_str()), true)
}
Some(CommentFormat::BlockCommentWithEnd(BlockCommentConfig { prefix, .. })) => {
(Some(prefix.as_ref()), true)
}
Some(CommentFormat::BlockCommentWithStart(BlockCommentConfig {
start: _,
end: _,
prefix,
tab_size,
})) => {
indent_size.len += tab_size;
(Some(prefix.as_ref()), true)
}
None => (None, false),
};
let indent_prefix = indent_size.chars().collect::<String>();
let mut line_prefix = indent_prefix.clone();
let mut inside_comment = false;
if let Some(prefix) = &comment_prefix {
line_prefix.push_str(prefix);
inside_comment = true;
}
if let Some(prefix) = &rewrap_prefix {
line_prefix.push_str(prefix);
}
let line_prefix = format!("{indent_prefix}{}", line_prefix.unwrap_or(""));
let allow_rewrap_based_on_language = match language_settings.allow_rewrap {
RewrapBehavior::InComments => inside_comment,
@@ -11937,6 +12082,8 @@ impl Editor {
let start_offset = start.to_offset(&buffer);
let end = Point::new(end_row, buffer.line_len(MultiBufferRow(end_row)));
let selection_text = buffer.text_for_range(start..end).collect::<String>();
let mut first_line_delimiter = None;
let mut last_line_delimiter = None;
let Some(lines_without_prefixes) = selection_text
.lines()
.enumerate()
@@ -11944,6 +12091,46 @@ impl Editor {
let line_trimmed = line.trim_start();
if rewrap_prefix.is_some() && ix > 0 {
Ok(line_trimmed)
} else if let Some(
CommentFormat::BlockCommentWithStart(BlockCommentConfig {
start,
prefix,
end,
tab_size,
})
| CommentFormat::BlockCommentWithEnd(BlockCommentConfig {
start,
prefix,
end,
tab_size,
}),
) = &comment_prefix
{
let line_trimmed = line_trimmed
.strip_prefix(start.as_ref())
.map(|s| {
let mut indent_size = indent_size;
indent_size.len -= tab_size;
let indent_prefix: String = indent_size.chars().collect();
first_line_delimiter = Some((indent_prefix, start));
s.trim_start()
})
.unwrap_or(line_trimmed);
let line_trimmed = line_trimmed
.strip_suffix(end.as_ref())
.map(|s| {
last_line_delimiter = Some(end);
s.trim_end()
})
.unwrap_or(line_trimmed);
let line_trimmed = line_trimmed
.strip_prefix(prefix.as_ref())
.unwrap_or(line_trimmed);
Ok(line_trimmed)
} else if let Some(CommentFormat::BlockLine(prefix)) = &comment_prefix {
line_trimmed.strip_prefix(prefix).with_context(|| {
format!("line did not start with prefix {prefix:?}: {line:?}")
})
} else {
line_trimmed
.strip_prefix(&line_prefix.trim_start())
@@ -11970,14 +12157,25 @@ impl Editor {
line_prefix.clone()
};
let wrapped_text = wrap_with_prefix(
line_prefix,
subsequent_lines_prefix,
lines_without_prefixes.join("\n"),
wrap_column,
tab_size,
options.preserve_existing_whitespace,
);
let wrapped_text = {
let mut wrapped_text = wrap_with_prefix(
line_prefix,
subsequent_lines_prefix,
lines_without_prefixes.join("\n"),
wrap_column,
tab_size,
options.preserve_existing_whitespace,
);
if let Some((indent, delimiter)) = first_line_delimiter {
wrapped_text = format!("{indent}{delimiter}\n{wrapped_text}");
}
if let Some(last_line) = last_line_delimiter {
wrapped_text = format!("{wrapped_text}\n{indent_prefix}{last_line}");
}
wrapped_text
};
// TODO: should always use char-based diff while still supporting cursor behavior that
// matches vim.
@@ -12955,11 +13153,17 @@ impl Editor {
this.change_selections(Default::default(), window, cx, |s| {
s.move_with(|map, selection| {
if selection.is_empty() {
let cursor = if action.ignore_newlines {
let mut cursor = if action.ignore_newlines {
movement::previous_word_start(map, selection.head())
} else {
movement::previous_word_start_or_newline(map, selection.head())
};
cursor = movement::adjust_greedy_deletion(
map,
selection.head(),
cursor,
action.ignore_brackets,
);
selection.set_head(cursor, SelectionGoal::None);
}
});
@@ -12980,7 +13184,9 @@ impl Editor {
this.change_selections(Default::default(), window, cx, |s| {
s.move_with(|map, selection| {
if selection.is_empty() {
let cursor = movement::previous_subword_start(map, selection.head());
let mut cursor = movement::previous_subword_start(map, selection.head());
cursor =
movement::adjust_greedy_deletion(map, selection.head(), cursor, false);
selection.set_head(cursor, SelectionGoal::None);
}
});
@@ -13056,11 +13262,17 @@ impl Editor {
this.change_selections(Default::default(), window, cx, |s| {
s.move_with(|map, selection| {
if selection.is_empty() {
let cursor = if action.ignore_newlines {
let mut cursor = if action.ignore_newlines {
movement::next_word_end(map, selection.head())
} else {
movement::next_word_end_or_newline(map, selection.head())
};
cursor = movement::adjust_greedy_deletion(
map,
selection.head(),
cursor,
action.ignore_brackets,
);
selection.set_head(cursor, SelectionGoal::None);
}
});
@@ -13080,7 +13292,9 @@ impl Editor {
this.change_selections(Default::default(), window, cx, |s| {
s.move_with(|map, selection| {
if selection.is_empty() {
let cursor = movement::next_subword_end(map, selection.head());
let mut cursor = movement::next_subword_end(map, selection.head());
cursor =
movement::adjust_greedy_deletion(map, selection.head(), cursor, false);
selection.set_head(cursor, SelectionGoal::None);
}
});
@@ -16172,7 +16386,7 @@ impl Editor {
PathKey::for_buffer(&location.buffer, cx),
location.buffer.clone(),
ranges_for_buffer,
DEFAULT_MULTIBUFFER_CONTEXT,
multibuffer_context_lines(cx),
cx,
);
ranges.extend(new_ranges)
@@ -18879,7 +19093,7 @@ impl Editor {
let snapshot = self.snapshot(window, cx);
let cursor = self.selections.newest::<Point>(cx).head();
let (buffer, point, _) = snapshot.buffer_snapshot.point_to_buffer_point(cursor)?;
let blame_entry = blame
let (_, blame_entry) = blame
.update(cx, |blame, cx| {
blame
.blame_for_rows(
@@ -18894,7 +19108,7 @@ impl Editor {
})
.flatten()?;
let renderer = cx.global::<GlobalBlameRenderer>().0.clone();
let repo = blame.read(cx).repository(cx)?;
let repo = blame.read(cx).repository(cx, buffer.remote_id())?;
let workspace = self.workspace()?.downgrade();
renderer.open_blame_commit(blame_entry, repo, workspace, window, cx);
None
@@ -18930,18 +19144,17 @@ impl Editor {
cx: &mut Context<Self>,
) {
if let Some(project) = self.project() {
let Some(buffer) = self.buffer().read(cx).as_singleton() else {
return;
};
if buffer.read(cx).file().is_none() {
if let Some(buffer) = self.buffer().read(cx).as_singleton()
&& buffer.read(cx).file().is_none()
{
return;
}
let focused = self.focus_handle(cx).contains_focused(window, cx);
let project = project.clone();
let blame = cx.new(|cx| GitBlame::new(buffer, project, user_triggered, focused, cx));
let blame = cx
.new(|cx| GitBlame::new(self.buffer.clone(), project, user_triggered, focused, cx));
self.blame_subscription =
Some(cx.observe_in(&blame, window, |_, _, _, cx| cx.notify()));
self.blame = Some(blame);
@@ -19591,7 +19804,24 @@ impl Editor {
let buffer = &snapshot.buffer_snapshot;
let start = buffer.anchor_before(0);
let end = buffer.anchor_after(buffer.len());
self.background_highlights_in_range(start..end, &snapshot, cx.theme())
self.sorted_background_highlights_in_range(start..end, &snapshot, cx.theme())
}
#[cfg(any(test, feature = "test-support"))]
pub fn sorted_background_highlights_in_range(
&self,
search_range: Range<Anchor>,
display_snapshot: &DisplaySnapshot,
theme: &Theme,
) -> Vec<(Range<DisplayPoint>, Hsla)> {
let mut res = self.background_highlights_in_range(search_range, display_snapshot, theme);
res.sort_by(|a, b| {
a.0.start
.cmp(&b.0.start)
.then_with(|| a.0.end.cmp(&b.0.end))
.then_with(|| a.1.cmp(&b.1))
});
res
}
#[cfg(feature = "test-support")]
@@ -19656,6 +19886,9 @@ impl Editor {
.is_some_and(|(_, highlights)| !highlights.is_empty())
}
/// Returns all background highlights for a given range.
///
/// The order of highlights is not deterministic, do sort the ranges if needed for the logic.
pub fn background_highlights_in_range(
&self,
search_range: Range<Anchor>,
@@ -19694,84 +19927,6 @@ impl Editor {
results
}
pub fn background_highlight_row_ranges<T: 'static>(
&self,
search_range: Range<Anchor>,
display_snapshot: &DisplaySnapshot,
count: usize,
) -> Vec<RangeInclusive<DisplayPoint>> {
let mut results = Vec::new();
let Some((_, ranges)) = self
.background_highlights
.get(&HighlightKey::Type(TypeId::of::<T>()))
else {
return vec![];
};
let start_ix = match ranges.binary_search_by(|probe| {
let cmp = probe
.end
.cmp(&search_range.start, &display_snapshot.buffer_snapshot);
if cmp.is_gt() {
Ordering::Greater
} else {
Ordering::Less
}
}) {
Ok(i) | Err(i) => i,
};
let mut push_region = |start: Option<Point>, end: Option<Point>| {
if let (Some(start_display), Some(end_display)) = (start, end) {
results.push(
start_display.to_display_point(display_snapshot)
..=end_display.to_display_point(display_snapshot),
);
}
};
let mut start_row: Option<Point> = None;
let mut end_row: Option<Point> = None;
if ranges.len() > count {
return Vec::new();
}
for range in &ranges[start_ix..] {
if range
.start
.cmp(&search_range.end, &display_snapshot.buffer_snapshot)
.is_ge()
{
break;
}
let end = range.end.to_point(&display_snapshot.buffer_snapshot);
if let Some(current_row) = &end_row
&& end.row == current_row.row
{
continue;
}
let start = range.start.to_point(&display_snapshot.buffer_snapshot);
if start_row.is_none() {
assert_eq!(end_row, None);
start_row = Some(start);
end_row = Some(end);
continue;
}
if let Some(current_end) = end_row.as_mut() {
if start.row > current_end.row + 1 {
push_region(start_row, end_row);
start_row = Some(start);
end_row = Some(end);
} else {
// Merge two hunks.
*current_end = end;
}
} else {
unreachable!();
}
}
// We might still have a hunk that was not rendered (if there was a search hit on the last line)
push_region(start_row, end_row);
results
}
pub fn gutter_highlights_in_range(
&self,
search_range: Range<Anchor>,
@@ -24016,3 +24171,10 @@ fn render_diff_hunk_controls(
)
.into_any_element()
}
pub fn multibuffer_context_lines(cx: &App) -> u32 {
EditorSettings::try_get(cx)
.map(|settings| settings.excerpt_context_lines)
.unwrap_or(2)
.clamp(1, 32)
}

View File

@@ -6,7 +6,7 @@ use language::CursorShape;
use project::project_settings::DiagnosticSeverity;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use settings::{Settings, SettingsSources, VsCodeSettings};
use settings::{Settings, SettingsSources, SettingsUi, VsCodeSettings};
use util::serde::default_true;
/// Imports from the VSCode settings at
@@ -17,6 +17,7 @@ pub struct EditorSettings {
pub cursor_shape: Option<CursorShape>,
pub current_line_highlight: CurrentLineHighlight,
pub selection_highlight: bool,
pub rounded_selection: bool,
pub lsp_highlight_debounce: u64,
pub hover_popover_enabled: bool,
pub hover_popover_delay: u64,
@@ -37,6 +38,7 @@ pub struct EditorSettings {
pub multi_cursor_modifier: MultiCursorModifier,
pub redact_private_values: bool,
pub expand_excerpt_lines: u32,
pub excerpt_context_lines: u32,
pub middle_click_paste: bool,
#[serde(default)]
pub double_click_in_multibuffer: DoubleClickInMultibuffer,
@@ -55,10 +57,13 @@ pub struct EditorSettings {
pub inline_code_actions: bool,
pub drag_and_drop_selection: DragAndDropSelection,
pub lsp_document_colors: DocumentColorsRenderMode,
pub minimum_contrast_for_highlights: f32,
}
/// How to render LSP `textDocument/documentColor` colors in the editor.
#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
#[derive(
Copy, Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema, SettingsUi,
)]
#[serde(rename_all = "snake_case")]
pub enum DocumentColorsRenderMode {
/// Do not query and render document colors.
@@ -72,7 +77,7 @@ pub enum DocumentColorsRenderMode {
Background,
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema, SettingsUi)]
#[serde(rename_all = "snake_case")]
pub enum CurrentLineHighlight {
// Don't highlight the current line.
@@ -86,7 +91,7 @@ pub enum CurrentLineHighlight {
}
/// When to populate a new search's query based on the text under the cursor.
#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema, SettingsUi)]
#[serde(rename_all = "snake_case")]
pub enum SeedQuerySetting {
/// Always populate the search query with the word under the cursor.
@@ -98,7 +103,9 @@ pub enum SeedQuerySetting {
}
/// What to do when multibuffer is double clicked in some of its excerpts (parts of singleton buffers).
#[derive(Default, Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
#[derive(
Default, Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema, SettingsUi,
)]
#[serde(rename_all = "snake_case")]
pub enum DoubleClickInMultibuffer {
/// Behave as a regular buffer and select the whole word.
@@ -117,7 +124,9 @@ pub struct Jupyter {
pub enabled: bool,
}
#[derive(Default, Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
#[derive(
Default, Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema, SettingsUi,
)]
#[serde(rename_all = "snake_case")]
pub struct JupyterContent {
/// Whether the Jupyter feature is enabled.
@@ -289,7 +298,9 @@ pub struct ScrollbarAxes {
}
/// Whether to allow drag and drop text selection in buffer.
#[derive(Copy, Clone, Default, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
#[derive(
Copy, Clone, Default, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq, SettingsUi,
)]
pub struct DragAndDropSelection {
/// When true, enables drag and drop text selection in buffer.
///
@@ -329,7 +340,7 @@ pub enum ScrollbarDiagnostics {
/// The key to use for adding multiple cursors
///
/// Default: alt
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq, SettingsUi)]
#[serde(rename_all = "snake_case")]
pub enum MultiCursorModifier {
Alt,
@@ -340,7 +351,7 @@ pub enum MultiCursorModifier {
/// Whether the editor will scroll beyond the last line.
///
/// Default: one_page
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq, SettingsUi)]
#[serde(rename_all = "snake_case")]
pub enum ScrollBeyondLastLine {
/// The editor will not scroll beyond the last line.
@@ -354,7 +365,9 @@ pub enum ScrollBeyondLastLine {
}
/// Default options for buffer and project search items.
#[derive(Copy, Clone, Default, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
#[derive(
Copy, Clone, Default, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq, SettingsUi,
)]
pub struct SearchSettings {
/// Whether to show the project search button in the status bar.
#[serde(default = "default_true")]
@@ -370,7 +383,9 @@ pub struct SearchSettings {
}
/// What to do when go to definition yields no results.
#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
#[derive(
Copy, Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema, SettingsUi,
)]
#[serde(rename_all = "snake_case")]
pub enum GoToDefinitionFallback {
/// Disables the fallback.
@@ -383,7 +398,9 @@ pub enum GoToDefinitionFallback {
/// Determines when the mouse cursor should be hidden in an editor or input box.
///
/// Default: on_typing_and_movement
#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
#[derive(
Copy, Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema, SettingsUi,
)]
#[serde(rename_all = "snake_case")]
pub enum HideMouseMode {
/// Never hide the mouse cursor
@@ -398,7 +415,9 @@ pub enum HideMouseMode {
/// Determines how snippets are sorted relative to other completion items.
///
/// Default: inline
#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
#[derive(
Copy, Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema, SettingsUi,
)]
#[serde(rename_all = "snake_case")]
pub enum SnippetSortOrder {
/// Place snippets at the top of the completion list
@@ -412,7 +431,8 @@ pub enum SnippetSortOrder {
None,
}
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema)]
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, SettingsUi)]
#[settings_ui(group = "Editor")]
pub struct EditorSettingsContent {
/// Whether the cursor blinks in the editor.
///
@@ -421,7 +441,7 @@ pub struct EditorSettingsContent {
/// Cursor shape for the default editor.
/// Can be "bar", "block", "underline", or "hollow".
///
/// Default: None
/// Default: bar
pub cursor_shape: Option<CursorShape>,
/// Determines when the mouse cursor should be hidden in an editor or input box.
///
@@ -439,6 +459,10 @@ pub struct EditorSettingsContent {
///
/// Default: true
pub selection_highlight: Option<bool>,
/// Whether the text selection should have rounded corners.
///
/// Default: true
pub rounded_selection: Option<bool>,
/// The debounce delay before querying highlights from the language
/// server based on the current cursor location.
///
@@ -515,6 +539,11 @@ pub struct EditorSettingsContent {
/// Default: 3
pub expand_excerpt_lines: Option<u32>,
/// How many lines of context to provide in multibuffer excerpts by default
///
/// Default: 2
pub excerpt_context_lines: Option<u32>,
/// Whether to enable middle-click paste on Linux
///
/// Default: true
@@ -544,6 +573,12 @@ pub struct EditorSettingsContent {
///
/// Default: false
pub show_signature_help_after_edits: Option<bool>,
/// The minimum APCA perceptual contrast to maintain when
/// rendering text over highlight backgrounds in the editor.
///
/// Values range from 0 to 106. Set to 0 to disable adjustments.
/// Default: 45
pub minimum_contrast_for_highlights: Option<f32>,
/// Whether to follow-up empty go to definition responses from the language server or not.
/// `FindAllReferences` allows to look up references of the same symbol instead.
@@ -583,7 +618,7 @@ pub struct EditorSettingsContent {
}
// Status bar related settings
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, PartialEq, Eq, SettingsUi)]
pub struct StatusBarContent {
/// Whether to display the active language button in the status bar.
///
@@ -596,7 +631,7 @@ pub struct StatusBarContent {
}
// Toolbar related settings
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, PartialEq, Eq, SettingsUi)]
pub struct ToolbarContent {
/// Whether to display breadcrumbs in the editor toolbar.
///
@@ -622,7 +657,9 @@ pub struct ToolbarContent {
}
/// Scrollbar related settings
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Default)]
#[derive(
Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Default, SettingsUi,
)]
pub struct ScrollbarContent {
/// When to show the scrollbar in the editor.
///
@@ -657,7 +694,9 @@ pub struct ScrollbarContent {
}
/// Minimap related settings
#[derive(Copy, Clone, Default, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
#[derive(
Copy, Clone, Default, Debug, Serialize, Deserialize, JsonSchema, PartialEq, SettingsUi,
)]
pub struct MinimapContent {
/// When to show the minimap in the editor.
///
@@ -705,7 +744,9 @@ pub struct ScrollbarAxesContent {
}
/// Gutter related settings
#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
#[derive(
Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema, PartialEq, Eq, SettingsUi,
)]
pub struct GutterContent {
/// Whether to show line numbers in the gutter.
///
@@ -781,6 +822,7 @@ impl Settings for EditorSettings {
"editor.selectionHighlight",
&mut current.selection_highlight,
);
vscode.bool_setting("editor.roundedSelection", &mut current.rounded_selection);
vscode.bool_setting("editor.hover.enabled", &mut current.hover_popover_enabled);
vscode.u64_setting("editor.hover.delay", &mut current.hover_popover_delay);

File diff suppressed because it is too large Load Diff

View File

@@ -82,6 +82,7 @@ use std::{
use sum_tree::Bias;
use text::{BufferId, SelectionGoal};
use theme::{ActiveTheme, Appearance, BufferLineHeight, PlayerColor};
use ui::utils::ensure_minimum_contrast;
use ui::{
ButtonLike, ContextMenu, Indicator, KeyBinding, POPOVER_Y_PADDING, Tooltip, h_flex, prelude::*,
right_click_menu,
@@ -116,6 +117,7 @@ struct SelectionLayout {
struct InlineBlameLayout {
element: AnyElement,
bounds: Bounds<Pixels>,
buffer_id: BufferId,
entry: BlameEntry,
}
@@ -585,6 +587,9 @@ impl EditorElement {
register_action(editor, window, Editor::edit_log_breakpoint);
register_action(editor, window, Editor::enable_breakpoint);
register_action(editor, window, Editor::disable_breakpoint);
if editor.read(cx).enable_wrap_selections_in_tag(cx) {
register_action(editor, window, Editor::wrap_selections_in_tag);
}
}
fn register_key_listeners(&self, window: &mut Window, _: &mut App, layout: &EditorLayout) {
@@ -1153,7 +1158,7 @@ impl EditorElement {
cx.notify();
}
if let Some((bounds, blame_entry)) = &position_map.inline_blame_bounds {
if let Some((bounds, buffer_id, blame_entry)) = &position_map.inline_blame_bounds {
let mouse_over_inline_blame = bounds.contains(&event.position);
let mouse_over_popover = editor
.inline_blame_popover
@@ -1166,7 +1171,7 @@ impl EditorElement {
.is_some_and(|state| state.keyboard_grace);
if mouse_over_inline_blame || mouse_over_popover {
editor.show_blame_popover(blame_entry, event.position, false, cx);
editor.show_blame_popover(*buffer_id, blame_entry, event.position, false, cx);
} else if !keyboard_grace {
editor.hide_blame_popover(cx);
}
@@ -2450,7 +2455,7 @@ impl EditorElement {
padding * em_width
};
let entry = blame
let (buffer_id, entry) = blame
.update(cx, |blame, cx| {
blame.blame_for_rows(&[*row_info], cx).next()
})
@@ -2485,13 +2490,22 @@ impl EditorElement {
let size = element.layout_as_root(AvailableSpace::min_size(), window, cx);
let bounds = Bounds::new(absolute_offset, size);
self.layout_blame_entry_popover(entry.clone(), blame, line_height, text_hitbox, window, cx);
self.layout_blame_entry_popover(
entry.clone(),
blame,
line_height,
text_hitbox,
row_info.buffer_id?,
window,
cx,
);
element.prepaint_as_root(absolute_offset, AvailableSpace::min_size(), window, cx);
Some(InlineBlameLayout {
element,
bounds,
buffer_id,
entry,
})
}
@@ -2502,6 +2516,7 @@ impl EditorElement {
blame: Entity<GitBlame>,
line_height: Pixels,
text_hitbox: &Hitbox,
buffer: BufferId,
window: &mut Window,
cx: &mut App,
) {
@@ -2526,6 +2541,7 @@ impl EditorElement {
popover_state.markdown,
workspace,
&blame,
buffer,
window,
cx,
)
@@ -2600,14 +2616,16 @@ impl EditorElement {
.into_iter()
.enumerate()
.flat_map(|(ix, blame_entry)| {
let (buffer_id, blame_entry) = blame_entry?;
let mut element = render_blame_entry(
ix,
&blame,
blame_entry?,
blame_entry,
&self.style,
&mut last_used_color,
self.editor.clone(),
workspace.clone(),
buffer_id,
blame_renderer.clone(),
cx,
)?;
@@ -3257,12 +3275,165 @@ impl EditorElement {
.collect()
}
fn bg_segments_per_row(
rows: Range<DisplayRow>,
selections: &[(PlayerColor, Vec<SelectionLayout>)],
highlight_ranges: &[(Range<DisplayPoint>, Hsla)],
base_background: Hsla,
) -> Vec<Vec<(Range<DisplayPoint>, Hsla)>> {
if rows.start >= rows.end {
return Vec::new();
}
if !base_background.is_opaque() {
// We don't actually know what color is behind this editor.
return Vec::new();
}
let highlight_iter = highlight_ranges.iter().cloned();
let selection_iter = selections.iter().flat_map(|(player_color, layouts)| {
let color = player_color.selection;
layouts.iter().filter_map(move |selection_layout| {
if selection_layout.range.start != selection_layout.range.end {
Some((selection_layout.range.clone(), color))
} else {
None
}
})
});
let mut per_row_map = vec![Vec::new(); rows.len()];
for (range, color) in highlight_iter.chain(selection_iter) {
let covered_rows = if range.end.column() == 0 {
cmp::max(range.start.row(), rows.start)..cmp::min(range.end.row(), rows.end)
} else {
cmp::max(range.start.row(), rows.start)
..cmp::min(range.end.row().next_row(), rows.end)
};
for row in covered_rows.iter_rows() {
let seg_start = if row == range.start.row() {
range.start
} else {
DisplayPoint::new(row, 0)
};
let seg_end = if row == range.end.row() && range.end.column() != 0 {
range.end
} else {
DisplayPoint::new(row, u32::MAX)
};
let ix = row.minus(rows.start) as usize;
debug_assert!(row >= rows.start && row < rows.end);
debug_assert!(ix < per_row_map.len());
per_row_map[ix].push((seg_start..seg_end, color));
}
}
for row_segments in per_row_map.iter_mut() {
if row_segments.is_empty() {
continue;
}
let segments = mem::take(row_segments);
let merged = Self::merge_overlapping_ranges(segments, base_background);
*row_segments = merged;
}
per_row_map
}
/// Merge overlapping ranges by splitting at all range boundaries and blending colors where
/// multiple ranges overlap. The result contains non-overlapping ranges ordered from left to right.
///
/// Expects `start.row() == end.row()` for each range.
fn merge_overlapping_ranges(
ranges: Vec<(Range<DisplayPoint>, Hsla)>,
base_background: Hsla,
) -> Vec<(Range<DisplayPoint>, Hsla)> {
struct Boundary {
pos: DisplayPoint,
is_start: bool,
index: usize,
color: Hsla,
}
let mut boundaries: SmallVec<[Boundary; 16]> = SmallVec::with_capacity(ranges.len() * 2);
for (index, (range, color)) in ranges.iter().enumerate() {
debug_assert!(
range.start.row() == range.end.row(),
"expects single-row ranges"
);
if range.start < range.end {
boundaries.push(Boundary {
pos: range.start,
is_start: true,
index,
color: *color,
});
boundaries.push(Boundary {
pos: range.end,
is_start: false,
index,
color: *color,
});
}
}
if boundaries.is_empty() {
return Vec::new();
}
boundaries
.sort_unstable_by(|a, b| a.pos.cmp(&b.pos).then_with(|| a.is_start.cmp(&b.is_start)));
let mut processed_ranges: Vec<(Range<DisplayPoint>, Hsla)> = Vec::new();
let mut active_ranges: SmallVec<[(usize, Hsla); 8]> = SmallVec::new();
let mut i = 0;
let mut start_pos = boundaries[0].pos;
let boundaries_len = boundaries.len();
while i < boundaries_len {
let current_boundary_pos = boundaries[i].pos;
if start_pos < current_boundary_pos {
if !active_ranges.is_empty() {
let mut color = base_background;
for &(_, c) in &active_ranges {
color = Hsla::blend(color, c);
}
if let Some((last_range, last_color)) = processed_ranges.last_mut() {
if *last_color == color && last_range.end == start_pos {
last_range.end = current_boundary_pos;
} else {
processed_ranges.push((start_pos..current_boundary_pos, color));
}
} else {
processed_ranges.push((start_pos..current_boundary_pos, color));
}
}
}
while i < boundaries_len && boundaries[i].pos == current_boundary_pos {
let active_range = &boundaries[i];
if active_range.is_start {
let idx = active_range.index;
let pos = active_ranges
.binary_search_by_key(&idx, |(i, _)| *i)
.unwrap_or_else(|p| p);
active_ranges.insert(pos, (idx, active_range.color));
} else {
let idx = active_range.index;
if let Ok(pos) = active_ranges.binary_search_by_key(&idx, |(i, _)| *i) {
active_ranges.remove(pos);
}
}
i += 1;
}
start_pos = current_boundary_pos;
}
processed_ranges
}
fn layout_lines(
rows: Range<DisplayRow>,
snapshot: &EditorSnapshot,
style: &EditorStyle,
editor_width: Pixels,
is_row_soft_wrapped: impl Copy + Fn(usize) -> bool,
bg_segments_per_row: &[Vec<(Range<DisplayPoint>, Hsla)>],
window: &mut Window,
cx: &mut App,
) -> Vec<LineWithInvisibles> {
@@ -3318,6 +3489,7 @@ impl EditorElement {
&snapshot.mode,
editor_width,
is_row_soft_wrapped,
bg_segments_per_row,
window,
cx,
)
@@ -5909,7 +6081,7 @@ impl EditorElement {
};
self.paint_lines_background(layout, window, cx);
let invisible_display_ranges = self.paint_highlights(layout, window);
let invisible_display_ranges = self.paint_highlights(layout, window, cx);
self.paint_document_colors(layout, window);
self.paint_lines(&invisible_display_ranges, layout, window, cx);
self.paint_redactions(layout, window);
@@ -5931,6 +6103,7 @@ impl EditorElement {
&mut self,
layout: &mut EditorLayout,
window: &mut Window,
cx: &mut App,
) -> SmallVec<[Range<DisplayPoint>; 32]> {
window.paint_layer(layout.position_map.text_hitbox.bounds, |window| {
let mut invisible_display_ranges = SmallVec::<[Range<DisplayPoint>; 32]>::new();
@@ -5947,7 +6120,11 @@ impl EditorElement {
);
}
let corner_radius = 0.15 * layout.position_map.line_height;
let corner_radius = if EditorSettings::get_global(cx).rounded_selection {
0.15 * layout.position_map.line_height
} else {
Pixels::ZERO
};
for (player_color, selections) in &layout.selections {
for selection in selections.iter() {
@@ -7235,12 +7412,13 @@ fn render_blame_entry_popover(
markdown: Entity<Markdown>,
workspace: WeakEntity<Workspace>,
blame: &Entity<GitBlame>,
buffer: BufferId,
window: &mut Window,
cx: &mut App,
) -> Option<AnyElement> {
let renderer = cx.global::<GlobalBlameRenderer>().0.clone();
let blame = blame.read(cx);
let repository = blame.repository(cx)?;
let repository = blame.repository(cx, buffer)?;
renderer.render_blame_entry_popover(
blame_entry,
scroll_handle,
@@ -7261,6 +7439,7 @@ fn render_blame_entry(
last_used_color: &mut Option<(PlayerColor, Oid)>,
editor: Entity<Editor>,
workspace: Entity<Workspace>,
buffer: BufferId,
renderer: Arc<dyn BlameRenderer>,
cx: &mut App,
) -> Option<AnyElement> {
@@ -7281,8 +7460,8 @@ fn render_blame_entry(
last_used_color.replace((sha_color, blame_entry.sha));
let blame = blame.read(cx);
let details = blame.details_for_entry(&blame_entry);
let repository = blame.repository(cx)?;
let details = blame.details_for_entry(buffer, &blame_entry);
let repository = blame.repository(cx, buffer)?;
renderer.render_blame_entry(
&style.text,
blame_entry,
@@ -7337,6 +7516,7 @@ impl LineWithInvisibles {
editor_mode: &EditorMode,
text_width: Pixels,
is_row_soft_wrapped: impl Copy + Fn(usize) -> bool,
bg_segments_per_row: &[Vec<(Range<DisplayPoint>, Hsla)>],
window: &mut Window,
cx: &mut App,
) -> Vec<Self> {
@@ -7352,6 +7532,7 @@ impl LineWithInvisibles {
let mut row = 0;
let mut line_exceeded_max_len = false;
let font_size = text_style.font_size.to_pixels(window.rem_size());
let min_contrast = EditorSettings::get_global(cx).minimum_contrast_for_highlights;
let ellipsis = SharedString::from("");
@@ -7364,10 +7545,16 @@ impl LineWithInvisibles {
}]) {
if let Some(replacement) = highlighted_chunk.replacement {
if !line.is_empty() {
let segments = bg_segments_per_row.get(row).map(|v| &v[..]).unwrap_or(&[]);
let text_runs: &[TextRun] = if segments.is_empty() {
&styles
} else {
&Self::split_runs_by_bg_segments(&styles, segments, min_contrast)
};
let shaped_line = window.text_system().shape_line(
line.clone().into(),
font_size,
&styles,
text_runs,
None,
);
width += shaped_line.width;
@@ -7445,10 +7632,16 @@ impl LineWithInvisibles {
} else {
for (ix, mut line_chunk) in highlighted_chunk.text.split('\n').enumerate() {
if ix > 0 {
let segments = bg_segments_per_row.get(row).map(|v| &v[..]).unwrap_or(&[]);
let text_runs = if segments.is_empty() {
&styles
} else {
&Self::split_runs_by_bg_segments(&styles, segments, min_contrast)
};
let shaped_line = window.text_system().shape_line(
line.clone().into(),
font_size,
&styles,
text_runs,
None,
);
width += shaped_line.width;
@@ -7536,6 +7729,81 @@ impl LineWithInvisibles {
layouts
}
/// Takes text runs and non-overlapping left-to-right background ranges with color.
/// Returns new text runs with adjusted contrast as per background ranges.
fn split_runs_by_bg_segments(
text_runs: &[TextRun],
bg_segments: &[(Range<DisplayPoint>, Hsla)],
min_contrast: f32,
) -> Vec<TextRun> {
let mut output_runs: Vec<TextRun> = Vec::with_capacity(text_runs.len());
let mut line_col = 0usize;
let mut segment_ix = 0usize;
for text_run in text_runs.iter() {
let run_start_col = line_col;
let run_end_col = run_start_col + text_run.len;
while segment_ix < bg_segments.len()
&& (bg_segments[segment_ix].0.end.column() as usize) <= run_start_col
{
segment_ix += 1;
}
let mut cursor_col = run_start_col;
let mut local_segment_ix = segment_ix;
while local_segment_ix < bg_segments.len() {
let (range, segment_color) = &bg_segments[local_segment_ix];
let segment_start_col = range.start.column() as usize;
let segment_end_col = range.end.column() as usize;
if segment_start_col >= run_end_col {
break;
}
if segment_start_col > cursor_col {
let span_len = segment_start_col - cursor_col;
output_runs.push(TextRun {
len: span_len,
font: text_run.font.clone(),
color: text_run.color,
background_color: text_run.background_color,
underline: text_run.underline,
strikethrough: text_run.strikethrough,
});
cursor_col = segment_start_col;
}
let segment_slice_end_col = segment_end_col.min(run_end_col);
if segment_slice_end_col > cursor_col {
let new_text_color =
ensure_minimum_contrast(text_run.color, *segment_color, min_contrast);
output_runs.push(TextRun {
len: segment_slice_end_col - cursor_col,
font: text_run.font.clone(),
color: new_text_color,
background_color: text_run.background_color,
underline: text_run.underline,
strikethrough: text_run.strikethrough,
});
cursor_col = segment_slice_end_col;
}
if segment_end_col >= run_end_col {
break;
}
local_segment_ix += 1;
}
if cursor_col < run_end_col {
output_runs.push(TextRun {
len: run_end_col - cursor_col,
font: text_run.font.clone(),
color: text_run.color,
background_color: text_run.background_color,
underline: text_run.underline,
strikethrough: text_run.strikethrough,
});
}
line_col = run_end_col;
segment_ix = local_segment_ix;
}
output_runs
}
fn prepaint(
&mut self,
line_height: Pixels,
@@ -8449,12 +8717,20 @@ impl Element for EditorElement {
cx,
);
let bg_segments_per_row = Self::bg_segments_per_row(
start_row..end_row,
&selections,
&highlighted_ranges,
self.style.background,
);
let mut line_layouts = Self::layout_lines(
start_row..end_row,
&snapshot,
&self.style,
editor_width,
is_row_soft_wrapped,
&bg_segments_per_row,
window,
cx,
);
@@ -8488,7 +8764,7 @@ impl Element for EditorElement {
return None;
}
let blame = editor.blame.as_ref()?;
let blame_entry = blame
let (_, blame_entry) = blame
.update(cx, |blame, cx| {
let row_infos =
snapshot.row_infos(snapshot.longest_row()).next()?;
@@ -9038,7 +9314,7 @@ impl Element for EditorElement {
text_hitbox: text_hitbox.clone(),
inline_blame_bounds: inline_blame_layout
.as_ref()
.map(|layout| (layout.bounds, layout.entry.clone())),
.map(|layout| (layout.bounds, layout.buffer_id, layout.entry.clone())),
display_hunks: display_hunks.clone(),
diff_hunk_control_bounds,
});
@@ -9698,7 +9974,7 @@ pub(crate) struct PositionMap {
pub snapshot: EditorSnapshot,
pub text_hitbox: Hitbox,
pub gutter_hitbox: Hitbox,
pub inline_blame_bounds: Option<(Bounds<Pixels>, BlameEntry)>,
pub inline_blame_bounds: Option<(Bounds<Pixels>, BufferId, BlameEntry)>,
pub display_hunks: Vec<(DisplayDiffHunk, Option<Hitbox>)>,
pub diff_hunk_control_bounds: Vec<(DisplayRow, Bounds<Pixels>)>,
}
@@ -9814,6 +10090,7 @@ pub fn layout_line(
&snapshot.mode,
text_width,
is_row_soft_wrapped,
&[],
window,
cx,
)
@@ -10714,4 +10991,289 @@ mod tests {
.cloned()
.collect()
}
#[gpui::test]
fn test_merge_overlapping_ranges() {
let base_bg = Hsla::white();
let color1 = Hsla {
h: 0.0,
s: 0.5,
l: 0.5,
a: 0.5,
};
let color2 = Hsla {
h: 120.0,
s: 0.5,
l: 0.5,
a: 0.5,
};
let display_point = |col| DisplayPoint::new(DisplayRow(0), col);
let cols = |v: &Vec<(Range<DisplayPoint>, Hsla)>| -> Vec<(u32, u32)> {
v.iter()
.map(|(r, _)| (r.start.column(), r.end.column()))
.collect()
};
// Test overlapping ranges blend colors
let overlapping = vec![
(display_point(5)..display_point(15), color1),
(display_point(10)..display_point(20), color2),
];
let result = EditorElement::merge_overlapping_ranges(overlapping, base_bg);
assert_eq!(cols(&result), vec![(5, 10), (10, 15), (15, 20)]);
// Test middle segment should have blended color
let blended = Hsla::blend(Hsla::blend(base_bg, color1), color2);
assert_eq!(result[1].1, blended);
// Test adjacent same-color ranges merge
let adjacent_same = vec![
(display_point(5)..display_point(10), color1),
(display_point(10)..display_point(15), color1),
];
let result = EditorElement::merge_overlapping_ranges(adjacent_same, base_bg);
assert_eq!(cols(&result), vec![(5, 15)]);
// Test contained range splits
let contained = vec![
(display_point(5)..display_point(20), color1),
(display_point(10)..display_point(15), color2),
];
let result = EditorElement::merge_overlapping_ranges(contained, base_bg);
assert_eq!(cols(&result), vec![(5, 10), (10, 15), (15, 20)]);
// Test multiple overlaps split at every boundary
let color3 = Hsla {
h: 240.0,
s: 0.5,
l: 0.5,
a: 0.5,
};
let complex = vec![
(display_point(5)..display_point(12), color1),
(display_point(8)..display_point(16), color2),
(display_point(10)..display_point(14), color3),
];
let result = EditorElement::merge_overlapping_ranges(complex, base_bg);
assert_eq!(
cols(&result),
vec![(5, 8), (8, 10), (10, 12), (12, 14), (14, 16)]
);
}
#[gpui::test]
fn test_bg_segments_per_row() {
let base_bg = Hsla::white();
// Case A: selection spans three display rows: row 1 [5, end), full row 2, row 3 [0, 7)
{
let selection_color = Hsla {
h: 200.0,
s: 0.5,
l: 0.5,
a: 0.5,
};
let player_color = PlayerColor {
cursor: selection_color,
background: selection_color,
selection: selection_color,
};
let spanning_selection = SelectionLayout {
head: DisplayPoint::new(DisplayRow(3), 7),
cursor_shape: CursorShape::Bar,
is_newest: true,
is_local: true,
range: DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(3), 7),
active_rows: DisplayRow(1)..DisplayRow(4),
user_name: None,
};
let selections = vec![(player_color, vec![spanning_selection])];
let result = EditorElement::bg_segments_per_row(
DisplayRow(0)..DisplayRow(5),
&selections,
&[],
base_bg,
);
assert_eq!(result.len(), 5);
assert!(result[0].is_empty());
assert_eq!(result[1].len(), 1);
assert_eq!(result[2].len(), 1);
assert_eq!(result[3].len(), 1);
assert!(result[4].is_empty());
assert_eq!(result[1][0].0.start, DisplayPoint::new(DisplayRow(1), 5));
assert_eq!(result[1][0].0.end.row(), DisplayRow(1));
assert_eq!(result[1][0].0.end.column(), u32::MAX);
assert_eq!(result[2][0].0.start, DisplayPoint::new(DisplayRow(2), 0));
assert_eq!(result[2][0].0.end.row(), DisplayRow(2));
assert_eq!(result[2][0].0.end.column(), u32::MAX);
assert_eq!(result[3][0].0.start, DisplayPoint::new(DisplayRow(3), 0));
assert_eq!(result[3][0].0.end, DisplayPoint::new(DisplayRow(3), 7));
}
// Case B: selection ends exactly at the start of row 3, excluding row 3
{
let selection_color = Hsla {
h: 120.0,
s: 0.5,
l: 0.5,
a: 0.5,
};
let player_color = PlayerColor {
cursor: selection_color,
background: selection_color,
selection: selection_color,
};
let selection = SelectionLayout {
head: DisplayPoint::new(DisplayRow(2), 0),
cursor_shape: CursorShape::Bar,
is_newest: true,
is_local: true,
range: DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(3), 0),
active_rows: DisplayRow(1)..DisplayRow(3),
user_name: None,
};
let selections = vec![(player_color, vec![selection])];
let result = EditorElement::bg_segments_per_row(
DisplayRow(0)..DisplayRow(4),
&selections,
&[],
base_bg,
);
assert_eq!(result.len(), 4);
assert!(result[0].is_empty());
assert_eq!(result[1].len(), 1);
assert_eq!(result[2].len(), 1);
assert!(result[3].is_empty());
assert_eq!(result[1][0].0.start, DisplayPoint::new(DisplayRow(1), 5));
assert_eq!(result[1][0].0.end.row(), DisplayRow(1));
assert_eq!(result[1][0].0.end.column(), u32::MAX);
assert_eq!(result[2][0].0.start, DisplayPoint::new(DisplayRow(2), 0));
assert_eq!(result[2][0].0.end.row(), DisplayRow(2));
assert_eq!(result[2][0].0.end.column(), u32::MAX);
}
}
#[cfg(test)]
fn generate_test_run(len: usize, color: Hsla) -> TextRun {
TextRun {
len,
font: gpui::font(".SystemUIFont"),
color,
background_color: None,
underline: None,
strikethrough: None,
}
}
#[gpui::test]
fn test_split_runs_by_bg_segments(cx: &mut gpui::TestAppContext) {
init_test(cx, |_| {});
let text_color = Hsla {
h: 210.0,
s: 0.1,
l: 0.4,
a: 1.0,
};
let bg1 = Hsla {
h: 30.0,
s: 0.6,
l: 0.8,
a: 1.0,
};
let bg2 = Hsla {
h: 200.0,
s: 0.6,
l: 0.2,
a: 1.0,
};
let min_contrast = 45.0;
// Case A: single run; disjoint segments inside the run
let runs = vec![generate_test_run(20, text_color)];
let segs = vec![
(
DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 10),
bg1,
),
(
DisplayPoint::new(DisplayRow(0), 12)..DisplayPoint::new(DisplayRow(0), 16),
bg2,
),
];
let out = LineWithInvisibles::split_runs_by_bg_segments(&runs, &segs, min_contrast);
// Expected slices: [0,5) [5,10) [10,12) [12,16) [16,20)
assert_eq!(
out.iter().map(|r| r.len).collect::<Vec<_>>(),
vec![5, 5, 2, 4, 4]
);
assert_eq!(out[0].color, text_color);
assert_eq!(
out[1].color,
ensure_minimum_contrast(text_color, bg1, min_contrast)
);
assert_eq!(out[2].color, text_color);
assert_eq!(
out[3].color,
ensure_minimum_contrast(text_color, bg2, min_contrast)
);
assert_eq!(out[4].color, text_color);
// Case B: multiple runs; segment extends to end of line (u32::MAX)
let runs = vec![
generate_test_run(8, text_color),
generate_test_run(7, text_color),
];
let segs = vec![(
DisplayPoint::new(DisplayRow(0), 6)..DisplayPoint::new(DisplayRow(0), u32::MAX),
bg1,
)];
let out = LineWithInvisibles::split_runs_by_bg_segments(&runs, &segs, min_contrast);
// Expected slices across runs: [0,6) [6,8) | [0,7)
assert_eq!(out.iter().map(|r| r.len).collect::<Vec<_>>(), vec![6, 2, 7]);
let adjusted = ensure_minimum_contrast(text_color, bg1, min_contrast);
assert_eq!(out[0].color, text_color);
assert_eq!(out[1].color, adjusted);
assert_eq!(out[2].color, adjusted);
// Case C: multi-byte characters
// for text: "Hello 🌍 世界!"
let runs = vec![
generate_test_run(5, text_color), // "Hello"
generate_test_run(6, text_color), // " 🌍 "
generate_test_run(6, text_color), // "世界"
generate_test_run(1, text_color), // "!"
];
// selecting "🌍 世"
let segs = vec![(
DisplayPoint::new(DisplayRow(0), 6)..DisplayPoint::new(DisplayRow(0), 14),
bg1,
)];
let out = LineWithInvisibles::split_runs_by_bg_segments(&runs, &segs, min_contrast);
// "Hello" | " " | "🌍 " | "世" | "界" | "!"
assert_eq!(
out.iter().map(|r| r.len).collect::<Vec<_>>(),
vec![5, 1, 5, 3, 3, 1]
);
assert_eq!(out[0].color, text_color); // "Hello"
assert_eq!(
out[2].color,
ensure_minimum_contrast(text_color, bg1, min_contrast)
); // "🌍 "
assert_eq!(
out[3].color,
ensure_minimum_contrast(text_color, bg1, min_contrast)
); // "世"
assert_eq!(out[4].color, text_color); // "界"
assert_eq!(out[5].color, text_color); // "!"
}
}

View File

@@ -10,16 +10,18 @@ use gpui::{
AnyElement, App, AppContext as _, Context, Entity, Hsla, ScrollHandle, Subscription, Task,
TextStyle, WeakEntity, Window,
};
use language::{Bias, Buffer, BufferSnapshot, Edit};
use itertools::Itertools;
use language::{Bias, BufferSnapshot, Edit};
use markdown::Markdown;
use multi_buffer::RowInfo;
use multi_buffer::{MultiBuffer, RowInfo};
use project::{
Project, ProjectItem,
Project, ProjectItem as _,
git_store::{GitStoreEvent, Repository, RepositoryEvent},
};
use smallvec::SmallVec;
use std::{sync::Arc, time::Duration};
use sum_tree::SumTree;
use text::BufferId;
use workspace::Workspace;
#[derive(Clone, Debug, Default)]
@@ -63,16 +65,19 @@ impl<'a> sum_tree::Dimension<'a, GitBlameEntrySummary> for u32 {
}
}
pub struct GitBlame {
project: Entity<Project>,
buffer: Entity<Buffer>,
struct GitBlameBuffer {
entries: SumTree<GitBlameEntry>,
commit_details: HashMap<Oid, ParsedCommitMessage>,
buffer_snapshot: BufferSnapshot,
buffer_edits: text::Subscription,
commit_details: HashMap<Oid, ParsedCommitMessage>,
}
pub struct GitBlame {
project: Entity<Project>,
multi_buffer: WeakEntity<MultiBuffer>,
buffers: HashMap<BufferId, GitBlameBuffer>,
task: Task<Result<()>>,
focused: bool,
generated: bool,
changed_while_blurred: bool,
user_triggered: bool,
regenerate_on_edit_task: Task<Result<()>>,
@@ -184,44 +189,44 @@ impl gpui::Global for GlobalBlameRenderer {}
impl GitBlame {
pub fn new(
buffer: Entity<Buffer>,
multi_buffer: Entity<MultiBuffer>,
project: Entity<Project>,
user_triggered: bool,
focused: bool,
cx: &mut Context<Self>,
) -> Self {
let entries = SumTree::from_item(
GitBlameEntry {
rows: buffer.read(cx).max_point().row + 1,
blame: None,
let multi_buffer_subscription = cx.subscribe(
&multi_buffer,
|git_blame, multi_buffer, event, cx| match event {
multi_buffer::Event::DirtyChanged => {
if !multi_buffer.read(cx).is_dirty(cx) {
git_blame.generate(cx);
}
}
multi_buffer::Event::ExcerptsAdded { .. }
| multi_buffer::Event::ExcerptsEdited { .. } => git_blame.regenerate_on_edit(cx),
_ => {}
},
&(),
);
let buffer_subscriptions = cx.subscribe(&buffer, |this, buffer, event, cx| match event {
language::BufferEvent::DirtyChanged => {
if !buffer.read(cx).is_dirty() {
this.generate(cx);
}
}
language::BufferEvent::Edited => {
this.regenerate_on_edit(cx);
}
_ => {}
});
let project_subscription = cx.subscribe(&project, {
let buffer = buffer.clone();
let multi_buffer = multi_buffer.downgrade();
move |this, _, event, cx| {
move |git_blame, _, event, cx| {
if let project::Event::WorktreeUpdatedEntries(_, updated) = event {
let project_entry_id = buffer.read(cx).entry_id(cx);
let Some(multi_buffer) = multi_buffer.upgrade() else {
return;
};
let project_entry_id = multi_buffer
.read(cx)
.as_singleton()
.and_then(|it| it.read(cx).entry_id(cx));
if updated
.iter()
.any(|(_, entry_id, _)| project_entry_id == Some(*entry_id))
{
log::debug!("Updated buffers. Regenerating blame data...",);
this.generate(cx);
git_blame.generate(cx);
}
}
}
@@ -239,24 +244,17 @@ impl GitBlame {
_ => {}
});
let buffer_snapshot = buffer.read(cx).snapshot();
let buffer_edits = buffer.update(cx, |buffer, _| buffer.subscribe());
let mut this = Self {
project,
buffer,
buffer_snapshot,
entries,
buffer_edits,
multi_buffer: multi_buffer.downgrade(),
buffers: HashMap::default(),
user_triggered,
focused,
changed_while_blurred: false,
commit_details: HashMap::default(),
task: Task::ready(Ok(())),
generated: false,
regenerate_on_edit_task: Task::ready(Ok(())),
_regenerate_subscriptions: vec![
buffer_subscriptions,
multi_buffer_subscription,
project_subscription,
git_store_subscription,
],
@@ -265,56 +263,63 @@ impl GitBlame {
this
}
pub fn repository(&self, cx: &App) -> Option<Entity<Repository>> {
pub fn repository(&self, cx: &App, id: BufferId) -> Option<Entity<Repository>> {
self.project
.read(cx)
.git_store()
.read(cx)
.repository_and_path_for_buffer_id(self.buffer.read(cx).remote_id(), cx)
.repository_and_path_for_buffer_id(id, cx)
.map(|(repo, _)| repo)
}
pub fn has_generated_entries(&self) -> bool {
self.generated
!self.buffers.is_empty()
}
pub fn details_for_entry(&self, entry: &BlameEntry) -> Option<ParsedCommitMessage> {
self.commit_details.get(&entry.sha).cloned()
pub fn details_for_entry(
&self,
buffer: BufferId,
entry: &BlameEntry,
) -> Option<ParsedCommitMessage> {
self.buffers
.get(&buffer)?
.commit_details
.get(&entry.sha)
.cloned()
}
pub fn blame_for_rows<'a>(
&'a mut self,
rows: &'a [RowInfo],
cx: &App,
) -> impl 'a + Iterator<Item = Option<BlameEntry>> {
self.sync(cx);
let buffer_id = self.buffer_snapshot.remote_id();
let mut cursor = self.entries.cursor::<u32>(&());
cx: &'a mut App,
) -> impl Iterator<Item = Option<(BufferId, BlameEntry)>> + use<'a> {
rows.iter().map(move |info| {
let row = info
.buffer_row
.filter(|_| info.buffer_id == Some(buffer_id))?;
cursor.seek_forward(&row, Bias::Right);
cursor.item()?.blame.clone()
let buffer_id = info.buffer_id?;
self.sync(cx, buffer_id);
let buffer_row = info.buffer_row?;
let mut cursor = self.buffers.get(&buffer_id)?.entries.cursor::<u32>(&());
cursor.seek_forward(&buffer_row, Bias::Right);
Some((buffer_id, cursor.item()?.blame.clone()?))
})
}
pub fn max_author_length(&mut self, cx: &App) -> usize {
self.sync(cx);
pub fn max_author_length(&mut self, cx: &mut App) -> usize {
let mut max_author_length = 0;
self.sync_all(cx);
for entry in self.entries.iter() {
let author_len = entry
.blame
.as_ref()
.and_then(|entry| entry.author.as_ref())
.map(|author| author.len());
if let Some(author_len) = author_len
&& author_len > max_author_length
{
max_author_length = author_len;
for buffer in self.buffers.values() {
for entry in buffer.entries.iter() {
let author_len = entry
.blame
.as_ref()
.and_then(|entry| entry.author.as_ref())
.map(|author| author.len());
if let Some(author_len) = author_len
&& author_len > max_author_length
{
max_author_length = author_len;
}
}
}
@@ -336,22 +341,48 @@ impl GitBlame {
}
}
fn sync(&mut self, cx: &App) {
let edits = self.buffer_edits.consume();
let new_snapshot = self.buffer.read(cx).snapshot();
fn sync_all(&mut self, cx: &mut App) {
let Some(multi_buffer) = self.multi_buffer.upgrade() else {
return;
};
multi_buffer
.read(cx)
.excerpt_buffer_ids()
.into_iter()
.for_each(|id| self.sync(cx, id));
}
fn sync(&mut self, cx: &mut App, buffer_id: BufferId) {
let Some(blame_buffer) = self.buffers.get_mut(&buffer_id) else {
return;
};
let Some(buffer) = self
.multi_buffer
.upgrade()
.and_then(|multi_buffer| multi_buffer.read(cx).buffer(buffer_id))
else {
return;
};
let edits = blame_buffer.buffer_edits.consume();
let new_snapshot = buffer.read(cx).snapshot();
let mut row_edits = edits
.into_iter()
.map(|edit| {
let old_point_range = self.buffer_snapshot.offset_to_point(edit.old.start)
..self.buffer_snapshot.offset_to_point(edit.old.end);
let old_point_range = blame_buffer.buffer_snapshot.offset_to_point(edit.old.start)
..blame_buffer.buffer_snapshot.offset_to_point(edit.old.end);
let new_point_range = new_snapshot.offset_to_point(edit.new.start)
..new_snapshot.offset_to_point(edit.new.end);
if old_point_range.start.column
== self.buffer_snapshot.line_len(old_point_range.start.row)
== blame_buffer
.buffer_snapshot
.line_len(old_point_range.start.row)
&& (new_snapshot.chars_at(edit.new.start).next() == Some('\n')
|| self.buffer_snapshot.line_len(old_point_range.end.row) == 0)
|| blame_buffer
.buffer_snapshot
.line_len(old_point_range.end.row)
== 0)
{
Edit {
old: old_point_range.start.row + 1..old_point_range.end.row + 1,
@@ -375,7 +406,7 @@ impl GitBlame {
.peekable();
let mut new_entries = SumTree::default();
let mut cursor = self.entries.cursor::<u32>(&());
let mut cursor = blame_buffer.entries.cursor::<u32>(&());
while let Some(mut edit) = row_edits.next() {
while let Some(next_edit) = row_edits.peek() {
@@ -433,17 +464,28 @@ impl GitBlame {
new_entries.append(cursor.suffix(), &());
drop(cursor);
self.buffer_snapshot = new_snapshot;
self.entries = new_entries;
blame_buffer.buffer_snapshot = new_snapshot;
blame_buffer.entries = new_entries;
}
#[cfg(test)]
fn check_invariants(&mut self, cx: &mut Context<Self>) {
self.sync(cx);
assert_eq!(
self.entries.summary().rows,
self.buffer.read(cx).max_point().row + 1
);
self.sync_all(cx);
for (&id, buffer) in &self.buffers {
assert_eq!(
buffer.entries.summary().rows,
self.multi_buffer
.upgrade()
.unwrap()
.read(cx)
.buffer(id)
.unwrap()
.read(cx)
.max_point()
.row
+ 1
);
}
}
fn generate(&mut self, cx: &mut Context<Self>) {
@@ -451,62 +493,105 @@ impl GitBlame {
self.changed_while_blurred = true;
return;
}
let buffer_edits = self.buffer.update(cx, |buffer, _| buffer.subscribe());
let snapshot = self.buffer.read(cx).snapshot();
let blame = self.project.update(cx, |project, cx| {
project.blame_buffer(&self.buffer, None, cx)
let Some(multi_buffer) = self.multi_buffer.upgrade() else {
return Vec::new();
};
multi_buffer
.read(cx)
.all_buffer_ids()
.into_iter()
.filter_map(|id| {
let buffer = multi_buffer.read(cx).buffer(id)?;
let snapshot = buffer.read(cx).snapshot();
let buffer_edits = buffer.update(cx, |buffer, _| buffer.subscribe());
let blame_buffer = project.blame_buffer(&buffer, None, cx);
Some((id, snapshot, buffer_edits, blame_buffer))
})
.collect::<Vec<_>>()
});
let provider_registry = GitHostingProviderRegistry::default_global(cx);
self.task = cx.spawn(async move |this, cx| {
let result = cx
let (result, errors) = cx
.background_spawn({
let snapshot = snapshot.clone();
async move {
let Some(Blame {
entries,
messages,
remote_url,
}) = blame.await?
else {
return Ok(None);
};
let mut res = vec![];
let mut errors = vec![];
for (id, snapshot, buffer_edits, blame) in blame {
match blame.await {
Ok(Some(Blame {
entries,
messages,
remote_url,
})) => {
let entries = build_blame_entry_sum_tree(
entries,
snapshot.max_point().row,
);
let commit_details = parse_commit_messages(
messages,
remote_url,
provider_registry.clone(),
)
.await;
let entries = build_blame_entry_sum_tree(entries, snapshot.max_point().row);
let commit_details =
parse_commit_messages(messages, remote_url, provider_registry).await;
anyhow::Ok(Some((entries, commit_details)))
res.push((
id,
snapshot,
buffer_edits,
Some(entries),
commit_details,
));
}
Ok(None) => {
res.push((id, snapshot, buffer_edits, None, Default::default()))
}
Err(e) => errors.push(e),
}
}
(res, errors)
}
})
.await;
this.update(cx, |this, cx| match result {
Ok(None) => {
// Nothing to do, e.g. no repository found
this.update(cx, |this, cx| {
this.buffers.clear();
for (id, snapshot, buffer_edits, entries, commit_details) in result {
let Some(entries) = entries else {
continue;
};
this.buffers.insert(
id,
GitBlameBuffer {
buffer_edits,
buffer_snapshot: snapshot,
entries,
commit_details,
},
);
}
Ok(Some((entries, commit_details))) => {
this.buffer_edits = buffer_edits;
this.buffer_snapshot = snapshot;
this.entries = entries;
this.commit_details = commit_details;
this.generated = true;
cx.notify();
cx.notify();
if !errors.is_empty() {
this.project.update(cx, |_, cx| {
if this.user_triggered {
log::error!("failed to get git blame data: {errors:?}");
let notification = errors
.into_iter()
.format_with(",", |e, f| f(&format_args!("{:#}", e)))
.to_string();
cx.emit(project::Event::Toast {
notification_id: "git-blame".into(),
message: notification,
});
} else {
// If we weren't triggered by a user, we just log errors in the background, instead of sending
// notifications.
log::debug!("failed to get git blame data: {errors:?}");
}
})
}
Err(error) => this.project.update(cx, |_, cx| {
if this.user_triggered {
log::error!("failed to get git blame data: {error:?}");
let notification = format!("{:#}", error).trim().to_string();
cx.emit(project::Event::Toast {
notification_id: "git-blame".into(),
message: notification,
});
} else {
// If we weren't triggered by a user, we just log errors in the background, instead of sending
// notifications.
log::debug!("failed to get git blame data: {error:?}");
}
}),
})
});
}
@@ -520,7 +605,7 @@ impl GitBlame {
this.update(cx, |this, cx| {
this.generate(cx);
})
})
});
}
}
@@ -659,6 +744,9 @@ mod tests {
)
.collect::<Vec<_>>(),
expected
.into_iter()
.map(|it| Some((buffer_id, it?)))
.collect::<Vec<_>>()
);
}
@@ -705,6 +793,7 @@ mod tests {
})
.await
.unwrap();
let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
let blame = cx.new(|cx| GitBlame::new(buffer.clone(), project.clone(), true, true, cx));
@@ -785,6 +874,7 @@ mod tests {
.await
.unwrap();
let buffer_id = buffer.read_with(cx, |buffer, _| buffer.remote_id());
let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
let git_blame = cx.new(|cx| GitBlame::new(buffer.clone(), project, false, true, cx));
@@ -806,14 +896,14 @@ mod tests {
)
.collect::<Vec<_>>(),
vec![
Some(blame_entry("1b1b1b", 0..1)),
Some(blame_entry("0d0d0d", 1..2)),
Some(blame_entry("3a3a3a", 2..3)),
Some((buffer_id, blame_entry("1b1b1b", 0..1))),
Some((buffer_id, blame_entry("0d0d0d", 1..2))),
Some((buffer_id, blame_entry("3a3a3a", 2..3))),
None,
None,
Some(blame_entry("3a3a3a", 5..6)),
Some(blame_entry("0d0d0d", 6..7)),
Some(blame_entry("3a3a3a", 7..8)),
Some((buffer_id, blame_entry("3a3a3a", 5..6))),
Some((buffer_id, blame_entry("0d0d0d", 6..7))),
Some((buffer_id, blame_entry("3a3a3a", 7..8))),
]
);
// Subset of lines
@@ -831,8 +921,8 @@ mod tests {
)
.collect::<Vec<_>>(),
vec![
Some(blame_entry("0d0d0d", 1..2)),
Some(blame_entry("3a3a3a", 2..3)),
Some((buffer_id, blame_entry("0d0d0d", 1..2))),
Some((buffer_id, blame_entry("3a3a3a", 2..3))),
None
]
);
@@ -852,7 +942,7 @@ mod tests {
cx
)
.collect::<Vec<_>>(),
vec![Some(blame_entry("0d0d0d", 1..2)), None, None]
vec![Some((buffer_id, blame_entry("0d0d0d", 1..2))), None, None]
);
});
}
@@ -895,6 +985,7 @@ mod tests {
.await
.unwrap();
let buffer_id = buffer.read_with(cx, |buffer, _| buffer.remote_id());
let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
let git_blame = cx.new(|cx| GitBlame::new(buffer.clone(), project, false, true, cx));
@@ -1061,8 +1152,9 @@ mod tests {
})
.await
.unwrap();
let mbuffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx));
let git_blame = cx.new(|cx| GitBlame::new(buffer.clone(), project, false, true, cx));
let git_blame = cx.new(|cx| GitBlame::new(mbuffer.clone(), project, false, true, cx));
cx.executor().run_until_parked();
git_blame.update(cx, |blame, cx| blame.check_invariants(cx));

View File

@@ -188,22 +188,26 @@ impl Editor {
pub fn scroll_hover(
&mut self,
amount: &ScrollAmount,
amount: ScrollAmount,
window: &mut Window,
cx: &mut Context<Self>,
) -> bool {
let selection = self.selections.newest_anchor().head();
let snapshot = self.snapshot(window, cx);
let Some(popover) = self.hover_state.info_popovers.iter().find(|popover| {
if let Some(popover) = self.hover_state.info_popovers.iter().find(|popover| {
popover
.symbol_range
.point_within_range(&TriggerPoint::Text(selection), &snapshot)
}) else {
return false;
};
popover.scroll(amount, window, cx);
true
}) {
popover.scroll(amount, window, cx);
true
} else if let Some(context_menu) = self.context_menu.borrow_mut().as_mut() {
context_menu.scroll_aside(amount, window, cx);
true
} else {
false
}
}
fn cmd_click_reveal_task(

View File

@@ -896,7 +896,7 @@ impl InfoPopover {
.into_any_element()
}
pub fn scroll(&self, amount: &ScrollAmount, window: &mut Window, cx: &mut Context<Editor>) {
pub fn scroll(&self, amount: ScrollAmount, window: &mut Window, cx: &mut Context<Editor>) {
let mut current = self.scroll_handle.offset();
current.y -= amount.pixels(
window.line_height(),

View File

@@ -1339,7 +1339,7 @@ pub mod tests {
let i = task_lsp_request_count.fetch_add(1, Ordering::Release) + 1;
assert_eq!(
params.text_document.uri,
lsp::Url::from_file_path(file_with_hints).unwrap(),
lsp::Uri::from_file_path(file_with_hints).unwrap(),
);
Ok(Some(vec![lsp::InlayHint {
position: lsp::Position::new(0, i),
@@ -1449,7 +1449,7 @@ pub mod tests {
async move {
assert_eq!(
params.text_document.uri,
lsp::Url::from_file_path(file_with_hints).unwrap(),
lsp::Uri::from_file_path(file_with_hints).unwrap(),
);
let current_call_id =
Arc::clone(&task_lsp_request_count).fetch_add(1, Ordering::SeqCst);
@@ -1594,7 +1594,7 @@ pub mod tests {
"Rust" => {
assert_eq!(
params.text_document.uri,
lsp::Url::from_file_path(path!("/a/main.rs"))
lsp::Uri::from_file_path(path!("/a/main.rs"))
.unwrap(),
);
rs_lsp_request_count.fetch_add(1, Ordering::Release)
@@ -1603,7 +1603,7 @@ pub mod tests {
"Markdown" => {
assert_eq!(
params.text_document.uri,
lsp::Url::from_file_path(path!("/a/other.md"))
lsp::Uri::from_file_path(path!("/a/other.md"))
.unwrap(),
);
md_lsp_request_count.fetch_add(1, Ordering::Release)
@@ -1789,7 +1789,7 @@ pub mod tests {
async move {
assert_eq!(
params.text_document.uri,
lsp::Url::from_file_path(file_with_hints).unwrap(),
lsp::Uri::from_file_path(file_with_hints).unwrap(),
);
Ok(Some(vec![
lsp::InlayHint {
@@ -2127,7 +2127,7 @@ pub mod tests {
let i = lsp_request_count.fetch_add(1, Ordering::SeqCst) + 1;
assert_eq!(
params.text_document.uri,
lsp::Url::from_file_path(file_with_hints).unwrap(),
lsp::Uri::from_file_path(file_with_hints).unwrap(),
);
Ok(Some(vec![lsp::InlayHint {
position: lsp::Position::new(0, i),
@@ -2290,7 +2290,7 @@ pub mod tests {
async move {
assert_eq!(
params.text_document.uri,
lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(),
lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(),
);
task_lsp_request_ranges.lock().push(params.range);
@@ -2633,11 +2633,11 @@ pub mod tests {
let task_editor_edited = Arc::clone(&closure_editor_edited);
async move {
let hint_text = if params.text_document.uri
== lsp::Url::from_file_path(path!("/a/main.rs")).unwrap()
== lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap()
{
"main hint"
} else if params.text_document.uri
== lsp::Url::from_file_path(path!("/a/other.rs")).unwrap()
== lsp::Uri::from_file_path(path!("/a/other.rs")).unwrap()
{
"other hint"
} else {
@@ -2944,11 +2944,11 @@ pub mod tests {
let task_editor_edited = Arc::clone(&closure_editor_edited);
async move {
let hint_text = if params.text_document.uri
== lsp::Url::from_file_path(path!("/a/main.rs")).unwrap()
== lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap()
{
"main hint"
} else if params.text_document.uri
== lsp::Url::from_file_path(path!("/a/other.rs")).unwrap()
== lsp::Uri::from_file_path(path!("/a/other.rs")).unwrap()
{
"other hint"
} else {
@@ -3116,7 +3116,7 @@ pub mod tests {
async move {
assert_eq!(
params.text_document.uri,
lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(),
lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(),
);
let query_start = params.range.start;
Ok(Some(vec![lsp::InlayHint {
@@ -3188,7 +3188,7 @@ pub mod tests {
async move {
assert_eq!(
params.text_document.uri,
lsp::Url::from_file_path(file_with_hints).unwrap(),
lsp::Uri::from_file_path(file_with_hints).unwrap(),
);
let i = lsp_request_count.fetch_add(1, Ordering::SeqCst) + 1;
@@ -3351,7 +3351,7 @@ pub mod tests {
move |params, _| async move {
assert_eq!(
params.text_document.uri,
lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(),
lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(),
);
Ok(Some(
serde_json::from_value(json!([

View File

@@ -289,12 +289,114 @@ pub fn previous_word_start_or_newline(map: &DisplaySnapshot, point: DisplayPoint
let classifier = map.buffer_snapshot.char_classifier_at(raw_point);
find_preceding_boundary_display_point(map, point, FindRange::MultiLine, |left, right| {
(classifier.kind(left) != classifier.kind(right) && !right.is_whitespace())
(classifier.kind(left) != classifier.kind(right) && !classifier.is_whitespace(right))
|| left == '\n'
|| right == '\n'
})
}
/// Text movements are too greedy, making deletions too greedy too.
/// Makes deletions more ergonomic by potentially reducing the deletion range based on its text contents:
/// * whitespace sequences with length >= 2 stop the deletion after removal (despite movement jumping over the word behind the whitespaces)
/// * brackets stop the deletion after removal (despite movement currently not accounting for these and jumping over)
pub fn adjust_greedy_deletion(
map: &DisplaySnapshot,
delete_from: DisplayPoint,
delete_until: DisplayPoint,
ignore_brackets: bool,
) -> DisplayPoint {
if delete_from == delete_until {
return delete_until;
}
let is_backward = delete_from > delete_until;
let delete_range = if is_backward {
map.display_point_to_point(delete_until, Bias::Left)
.to_offset(&map.buffer_snapshot)
..map
.display_point_to_point(delete_from, Bias::Right)
.to_offset(&map.buffer_snapshot)
} else {
map.display_point_to_point(delete_from, Bias::Left)
.to_offset(&map.buffer_snapshot)
..map
.display_point_to_point(delete_until, Bias::Right)
.to_offset(&map.buffer_snapshot)
};
let trimmed_delete_range = if ignore_brackets {
delete_range
} else {
let brackets_in_delete_range = map
.buffer_snapshot
.bracket_ranges(delete_range.clone())
.into_iter()
.flatten()
.flat_map(|(left_bracket, right_bracket)| {
[
left_bracket.start,
left_bracket.end,
right_bracket.start,
right_bracket.end,
]
})
.filter(|&bracket| delete_range.start < bracket && bracket < delete_range.end);
let closest_bracket = if is_backward {
brackets_in_delete_range.max()
} else {
brackets_in_delete_range.min()
};
if is_backward {
closest_bracket.unwrap_or(delete_range.start)..delete_range.end
} else {
delete_range.start..closest_bracket.unwrap_or(delete_range.end)
}
};
let mut whitespace_sequences = Vec::new();
let mut current_offset = trimmed_delete_range.start;
let mut whitespace_sequence_length = 0;
let mut whitespace_sequence_start = 0;
for ch in map
.buffer_snapshot
.text_for_range(trimmed_delete_range.clone())
.flat_map(str::chars)
{
if ch.is_whitespace() {
if whitespace_sequence_length == 0 {
whitespace_sequence_start = current_offset;
}
whitespace_sequence_length += 1;
} else {
if whitespace_sequence_length >= 2 {
whitespace_sequences.push((whitespace_sequence_start, current_offset));
}
whitespace_sequence_start = 0;
whitespace_sequence_length = 0;
}
current_offset += ch.len_utf8();
}
if whitespace_sequence_length >= 2 {
whitespace_sequences.push((whitespace_sequence_start, current_offset));
}
let closest_whitespace_end = if is_backward {
whitespace_sequences.last().map(|&(start, _)| start)
} else {
whitespace_sequences.first().map(|&(_, end)| end)
};
closest_whitespace_end
.unwrap_or_else(|| {
if is_backward {
trimmed_delete_range.start
} else {
trimmed_delete_range.end
}
})
.to_display_point(map)
}
/// Returns a position of the previous subword boundary, where a subword is defined as a run of
/// word characters of the same "subkind" - where subcharacter kinds are '_' character,
/// lowerspace characters and uppercase characters.

View File

@@ -15,7 +15,7 @@ impl ScrollDirection {
}
}
#[derive(Debug, Clone, PartialEq, Deserialize)]
#[derive(Debug, Clone, Copy, PartialEq, Deserialize)]
pub enum ScrollAmount {
// Scroll N lines (positive is towards the end of the document)
Line(f32),

View File

@@ -29,7 +29,7 @@ pub struct EditorLspTestContext {
pub cx: EditorTestContext,
pub lsp: lsp::FakeLanguageServer,
pub workspace: Entity<Workspace>,
pub buffer_lsp_url: lsp::Url,
pub buffer_lsp_url: lsp::Uri,
}
pub(crate) fn rust_lang() -> Arc<Language> {
@@ -189,7 +189,7 @@ impl EditorLspTestContext {
},
lsp,
workspace,
buffer_lsp_url: lsp::Url::from_file_path(root.join("dir").join(file_name)).unwrap(),
buffer_lsp_url: lsp::Uri::from_file_path(root.join("dir").join(file_name)).unwrap(),
}
}
@@ -358,7 +358,7 @@ impl EditorLspTestContext {
where
T: 'static + request::Request,
T::Params: 'static + Send,
F: 'static + Send + FnMut(lsp::Url, T::Params, gpui::AsyncApp) -> Fut,
F: 'static + Send + FnMut(lsp::Uri, T::Params, gpui::AsyncApp) -> Fut,
Fut: 'static + Future<Output = Result<T::Result>>,
{
let url = self.buffer_lsp_url.clone();

View File

@@ -43,7 +43,7 @@ use language::{
use node_runtime::NodeRuntime;
use project::ContextProviderWithTasks;
use release_channel::ReleaseChannel;
use remote::RemoteClient;
use remote::{RemoteClient, RemoteConnectionOptions};
use semantic_version::SemanticVersion;
use serde::{Deserialize, Serialize};
use settings::Settings;
@@ -117,7 +117,7 @@ pub struct ExtensionStore {
pub wasm_host: Arc<WasmHost>,
pub wasm_extensions: Vec<(Arc<ExtensionManifest>, WasmExtension)>,
pub tasks: Vec<Task<()>>,
pub remote_clients: HashMap<String, WeakEntity<RemoteClient>>,
pub remote_clients: HashMap<RemoteConnectionOptions, WeakEntity<RemoteClient>>,
pub ssh_registered_tx: UnboundedSender<()>,
}
@@ -1779,16 +1779,15 @@ impl ExtensionStore {
}
pub fn register_remote_client(&mut self, client: Entity<RemoteClient>, cx: &mut Context<Self>) {
let connection_options = client.read(cx).connection_options();
let ssh_url = connection_options.ssh_url();
let options = client.read(cx).connection_options();
if let Some(existing_client) = self.remote_clients.get(&ssh_url)
if let Some(existing_client) = self.remote_clients.get(&options)
&& existing_client.upgrade().is_some()
{
return;
}
self.remote_clients.insert(ssh_url, client.downgrade());
self.remote_clients.insert(options, client.downgrade());
self.ssh_registered_tx.unbounded_send(()).ok();
}
}

View File

@@ -3,10 +3,10 @@ use collections::HashMap;
use gpui::App;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use settings::{Settings, SettingsSources};
use settings::{Settings, SettingsSources, SettingsUi};
use std::sync::Arc;
#[derive(Deserialize, Serialize, Debug, Default, Clone, JsonSchema)]
#[derive(Deserialize, Serialize, Debug, Default, Clone, JsonSchema, SettingsUi)]
pub struct ExtensionSettings {
/// The extensions that should be automatically installed by Zed.
///

View File

@@ -1,7 +1,7 @@
use anyhow::Result;
use schemars::JsonSchema;
use serde_derive::{Deserialize, Serialize};
use settings::{Settings, SettingsSources};
use settings::{Settings, SettingsSources, SettingsUi};
#[derive(Deserialize, Debug, Clone, Copy, PartialEq)]
pub struct FileFinderSettings {
@@ -11,7 +11,7 @@ pub struct FileFinderSettings {
pub include_ignored: Option<bool>,
}
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)]
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug, SettingsUi)]
pub struct FileFinderSettingsContent {
/// Whether to show file icons in the file finder.
///

View File

@@ -495,7 +495,8 @@ impl Fs for RealFs {
};
// todo(windows)
// When new version of `windows-rs` release, make this operation `async`
let path = SanitizedPath::from(path.canonicalize()?);
let path = path.canonicalize()?;
let path = SanitizedPath::new(&path);
let path_string = path.to_string();
let file = StorageFile::GetFileFromPathAsync(&HSTRING::from(path_string))?.get()?;
file.DeleteAsync(StorageDeleteOption::Default)?.get()?;
@@ -522,7 +523,8 @@ impl Fs for RealFs {
// todo(windows)
// When new version of `windows-rs` release, make this operation `async`
let path = SanitizedPath::from(path.canonicalize()?);
let path = path.canonicalize()?;
let path = SanitizedPath::new(&path);
let path_string = path.to_string();
let folder = StorageFolder::GetFolderFromPathAsync(&HSTRING::from(path_string))?.get()?;
folder.DeleteAsync(StorageDeleteOption::Default)?.get()?;
@@ -783,7 +785,7 @@ impl Fs for RealFs {
{
target = parent.join(target);
if let Ok(canonical) = self.canonicalize(&target).await {
target = SanitizedPath::from(canonical).as_path().to_path_buf();
target = SanitizedPath::new(&canonical).as_path().to_path_buf();
}
}
watcher.add(&target).ok();

View File

@@ -42,7 +42,7 @@ impl Drop for FsWatcher {
impl Watcher for FsWatcher {
fn add(&self, path: &std::path::Path) -> anyhow::Result<()> {
let root_path = SanitizedPath::from(path);
let root_path = SanitizedPath::new_arc(path);
let tx = self.tx.clone();
let pending_paths = self.pending_path_events.clone();
@@ -70,7 +70,7 @@ impl Watcher for FsWatcher {
.paths
.iter()
.filter_map(|event_path| {
let event_path = SanitizedPath::from(event_path);
let event_path = SanitizedPath::new(event_path);
event_path.starts_with(&root_path).then(|| PathEvent {
path: event_path.as_path().to_path_buf(),
kind,

View File

@@ -5,7 +5,7 @@ use git::GitHostingProviderRegistry;
use gpui::App;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use settings::{Settings, SettingsStore};
use settings::{Settings, SettingsStore, SettingsUi};
use url::Url;
use util::ResultExt as _;
@@ -78,7 +78,7 @@ pub struct GitHostingProviderConfig {
pub name: String,
}
#[derive(Default, Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[derive(Default, Debug, Clone, Serialize, Deserialize, JsonSchema, SettingsUi)]
pub struct GitHostingProviderSettings {
/// The list of custom Git hosting providers.
#[serde(default)]

View File

@@ -1,6 +1,6 @@
use anyhow::{Context as _, Result};
use buffer_diff::{BufferDiff, BufferDiffSnapshot};
use editor::{Editor, EditorEvent, MultiBuffer, SelectionEffects};
use editor::{Editor, EditorEvent, MultiBuffer, SelectionEffects, multibuffer_context_lines};
use git::repository::{CommitDetails, CommitDiff, CommitSummary, RepoPath};
use gpui::{
AnyElement, AnyView, App, AppContext as _, AsyncApp, Context, Entity, EventEmitter,
@@ -195,7 +195,7 @@ impl CommitView {
PathKey::namespaced(FILE_NAMESPACE, path),
buffer,
diff_hunk_ranges,
editor::DEFAULT_MULTIBUFFER_CONTEXT,
multibuffer_context_lines(cx),
cx,
);
multibuffer.add_diff(buffer_diff, cx);

View File

@@ -31,11 +31,11 @@ use git::{
UnstageAll,
};
use gpui::{
Action, Animation, AnimationExt as _, AsyncApp, AsyncWindowContext, Axis, ClickEvent, Corner,
DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, KeyContext,
ListHorizontalSizingBehavior, ListSizingBehavior, MouseButton, MouseDownEvent, Point,
PromptLevel, ScrollStrategy, Subscription, Task, Transformation, UniformListScrollHandle,
WeakEntity, actions, anchored, deferred, percentage, uniform_list,
Action, AsyncApp, AsyncWindowContext, Axis, ClickEvent, Corner, DismissEvent, Entity,
EventEmitter, FocusHandle, Focusable, KeyContext, ListHorizontalSizingBehavior,
ListSizingBehavior, MouseButton, MouseDownEvent, Point, PromptLevel, ScrollStrategy,
Subscription, Task, UniformListScrollHandle, WeakEntity, actions, anchored, deferred,
uniform_list,
};
use itertools::Itertools;
use language::{Buffer, File};
@@ -63,8 +63,8 @@ use std::{collections::HashSet, sync::Arc, time::Duration, usize};
use strum::{IntoEnumIterator, VariantNames};
use time::OffsetDateTime;
use ui::{
Checkbox, ContextMenu, ElevationIndex, IconPosition, Label, LabelSize, PopoverMenu, Scrollbar,
ScrollbarState, SplitButton, Tooltip, prelude::*,
Checkbox, CommonAnimationExt, ContextMenu, ElevationIndex, IconPosition, Label, LabelSize,
PopoverMenu, Scrollbar, ScrollbarState, SplitButton, Tooltip, prelude::*,
};
use util::{ResultExt, TryFutureExt, maybe};
use workspace::SERIALIZATION_THROTTLE_TIME;
@@ -3088,13 +3088,7 @@ impl GitPanel {
Icon::new(IconName::ArrowCircle)
.size(IconSize::XSmall)
.color(Color::Info)
.with_animation(
"arrow-circle",
Animation::new(Duration::from_secs(2)).repeat(),
|icon, delta| {
icon.transform(Transformation::rotate(percentage(delta)))
},
),
.with_rotate_animation(2),
)
.child(
Label::new("Generating Commit...")

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